improved quick tab switcher & general tab design/logic;

This commit is contained in:
Fabian Freund
2026-06-12 03:47:20 +02:00
parent 83c78b53d3
commit 3c5f472bdf
35 changed files with 2715 additions and 768 deletions
@@ -78,16 +78,25 @@ const List<SettingsSectionDefinition> toolbarLayoutSettingsSections = [
title: 'Quick Tab Switcher',
entries: [
SettingsEntryDefinition(
title: 'Show Quick Tab Switcher Bar',
subtitle: 'Show a bar for switching to recent tabs',
keywords: ['recent tabs'],
child: _ShowQuickTabSwitcherBarTile(),
title: 'Tab Stacking',
subtitle: 'Choose how the quick tab switcher bar arranges tabs',
keywords: [
'recent tabs',
'recently used',
'container tabs',
'accordion',
'two level',
'rows',
'stacking',
'disabled',
],
child: _TabBarStackingModeSection(),
),
SettingsEntryDefinition(
title: 'Quick Tab Switcher Mode',
subtitle: 'Choose how the switcher orders and groups tabs',
keywords: ['recently used', 'container tabs'],
child: _QuickTabSwitcherModeSection(),
title: 'Close Buttons on All Tabs',
subtitle: 'Show a close button on every switcher chip',
keywords: ['close', 'x button'],
child: _QuickTabSwitcherCloseButtonsTile(),
),
SettingsEntryDefinition(
title: 'History Fallback in Quick Tab Switcher',
@@ -101,6 +110,12 @@ const List<SettingsSectionDefinition> toolbarLayoutSettingsSections = [
keywords: ['page titles'],
child: _QuickTabSwitcherShowTitlesTile(),
),
SettingsEntryDefinition(
title: 'Title Width in Quick Tab Switcher',
subtitle: 'Maximum width of tab titles on switcher chips',
keywords: ['width', 'title', 'chip', 'length'],
child: _QuickTabSwitcherTitleWidthTile(),
),
SettingsEntryDefinition(
title: 'Hierarchy Depth in Quick Tab Switcher',
subtitle: 'How many nesting chevrons to show on switcher chips',
@@ -303,43 +318,13 @@ class _CustomizeToolbarButtonsTile extends HookConsumerWidget {
}
}
class _ShowQuickTabSwitcherBarTile extends HookConsumerWidget {
const _ShowQuickTabSwitcherBarTile();
@override
Widget build(BuildContext context, WidgetRef ref) {
final tabBarShowQuickTabSwitcherBar = ref.watch(
generalSettingsWithDefaultsProvider.select(
(s) => s.tabBarShowQuickTabSwitcherBar,
),
);
return SwitchListTile.adaptive(
title: const Text('Show Quick Tab Switcher Bar'),
subtitle: const Text(
'Show additional toolbar to quickly switch to recently used tabs',
),
secondary: const Icon(MdiIcons.dockBottom),
value: tabBarShowQuickTabSwitcherBar,
onChanged: (value) async {
await ref
.read(saveGeneralSettingsControllerProvider.notifier)
.save(
(currentSettings) =>
currentSettings.copyWith.tabBarShowQuickTabSwitcherBar(value),
);
},
);
}
}
class _QuickTabSwitcherModeSection extends HookConsumerWidget {
const _QuickTabSwitcherModeSection();
class _TabBarStackingModeSection extends HookConsumerWidget {
const _TabBarStackingModeSection();
@override
Widget build(BuildContext context, WidgetRef ref) {
final settings = ref.watch(generalSettingsWithDefaultsProvider);
final quickTabSwitcherMode = settings.effectiveUiQuickTabSwitcherMode();
final stackingMode = settings.effectiveTabBarStackingMode();
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8),
@@ -348,35 +333,60 @@ class _QuickTabSwitcherModeSection extends HookConsumerWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const ListTile(
title: Text('Quick Tab Switcher Mode'),
title: Text('Tab Stacking'),
subtitle: Text(
'How the quick tab switcher bar arranges its tabs',
),
leading: Icon(MdiIcons.folderSettings),
contentPadding: EdgeInsets.zero,
),
RadioGroup(
groupValue: quickTabSwitcherMode,
groupValue: stackingMode,
onChanged: (value) async {
if (value != null) {
await ref
.read(saveGeneralSettingsControllerProvider.notifier)
.save(
(currentSettings) =>
currentSettings.copyWith.quickTabSwitcherMode(value),
currentSettings.copyWith.tabBarStackingMode(value),
);
}
},
child: Column(
children: [
const RadioListTile.adaptive(
value: QuickTabSwitcherMode.lastUsedTabs,
value: TabBarStackingMode.lastUsedTabs,
title: Text('Recently Used Tabs'),
subtitle: Text('Recently used tabs across all containers'),
),
if (settings.showContainerUi)
const RadioListTile.adaptive(
value: QuickTabSwitcherMode.containerTabs,
if (settings.showContainerUi) ...const [
RadioListTile.adaptive(
value: TabBarStackingMode.containerTabs,
title: Text('Container Tabs'),
subtitle: Text('Ordered tabs of the selected container'),
),
RadioListTile.adaptive(
value: TabBarStackingMode.accordion,
title: Text('Accordion'),
subtitle: Text(
"All containers as chips, with the selected "
"container's tabs expanded inline",
),
),
RadioListTile.adaptive(
value: TabBarStackingMode.twoLevel,
title: Text('Two Rows'),
subtitle: Text(
'Tabs of the selected container on top, recently used '
'tabs below',
),
),
],
const RadioListTile.adaptive(
value: TabBarStackingMode.disabled,
title: Text('Disabled'),
subtitle: Text('Hide the quick tab switcher bar'),
),
],
),
),
@@ -386,6 +396,143 @@ class _QuickTabSwitcherModeSection extends HookConsumerWidget {
}
}
class _QuickTabSwitcherCloseButtonsTile extends HookConsumerWidget {
const _QuickTabSwitcherCloseButtonsTile();
@override
Widget build(BuildContext context, WidgetRef ref) {
final showCloseButtonOnAllTabs = ref.watch(
generalSettingsWithDefaultsProvider.select(
(s) => s.quickTabSwitcherShowCloseButtonOnAllTabs,
),
);
final switcherEnabled = ref.watch(
generalSettingsWithDefaultsProvider.select(
(s) => s.effectiveTabBarStackingMode() != TabBarStackingMode.disabled,
),
);
return SwitchListTile.adaptive(
title: const Text('Close Buttons on All Tabs'),
subtitle: const Text(
"Show a close button on every switcher chip; the active tab's chip "
"always has one",
),
secondary: const Icon(MdiIcons.closeCircleOutline),
value: showCloseButtonOnAllTabs,
onChanged: switcherEnabled
? (value) async {
await ref
.read(saveGeneralSettingsControllerProvider.notifier)
.save(
(currentSettings) => currentSettings.copyWith
.quickTabSwitcherShowCloseButtonOnAllTabs(value),
);
}
: null,
);
}
}
class _QuickTabSwitcherTitleWidthTile extends HookConsumerWidget {
const _QuickTabSwitcherTitleWidthTile();
static final _divisions =
((maxQuickTabSwitcherTitleWidth - minQuickTabSwitcherTitleWidth) /
quickTabSwitcherTitleWidthStep)
.round();
static String _label(double width) => '${width.round()} px';
@override
Widget build(BuildContext context, WidgetRef ref) {
final titleWidth = ref.watch(
generalSettingsWithDefaultsProvider.select(
(s) => s.quickTabSwitcherTitleWidth,
),
);
final showTitles = ref.watch(
generalSettingsWithDefaultsProvider.select(
(s) => s.quickTabSwitcherShowTitles,
),
);
final switcherEnabled = ref.watch(
generalSettingsWithDefaultsProvider.select(
(s) => s.effectiveTabBarStackingMode() != TabBarStackingMode.disabled,
),
);
final sliderValue = useState(titleWidth);
useEffect(() {
sliderValue.value = titleWidth;
return null;
}, [titleWidth]);
final enabled = switcherEnabled && showTitles;
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ListTile(
title: const Text('Title Width in Quick Tab Switcher'),
subtitle: const Text(
'Maximum width of tab titles on switcher chips',
),
leading: const Icon(MdiIcons.arrowExpandHorizontal),
contentPadding: EdgeInsets.zero,
enabled: enabled,
),
Row(
children: [
Expanded(
child: Slider(
min: minQuickTabSwitcherTitleWidth,
max: maxQuickTabSwitcherTitleWidth,
divisions: _divisions,
label: _label(sliderValue.value),
value: sliderValue.value.clamp(
minQuickTabSwitcherTitleWidth,
maxQuickTabSwitcherTitleWidth,
),
onChanged: enabled
? (value) {
sliderValue.value = value;
}
: null,
onChangeEnd: enabled
? (value) async {
final normalized =
(value / quickTabSwitcherTitleWidthStep)
.round() *
quickTabSwitcherTitleWidthStep;
sliderValue.value = normalized;
await ref
.read(
saveGeneralSettingsControllerProvider.notifier,
)
.save(
(currentSettings) => currentSettings.copyWith
.quickTabSwitcherTitleWidth(normalized),
);
}
: null,
),
),
Text(
_label(sliderValue.value),
style: Theme.of(context).textTheme.titleMedium,
),
],
),
],
),
);
}
}
class _QuickTabSwitcherHistorySuggestionsTile extends HookConsumerWidget {
const _QuickTabSwitcherHistorySuggestionsTile();
@@ -396,9 +543,9 @@ class _QuickTabSwitcherHistorySuggestionsTile extends HookConsumerWidget {
(s) => s.quickTabSwitcherShowHistorySuggestions,
),
);
final tabBarShowQuickTabSwitcherBar = ref.watch(
final switcherEnabled = ref.watch(
generalSettingsWithDefaultsProvider.select(
(s) => s.tabBarShowQuickTabSwitcherBar,
(s) => s.effectiveTabBarStackingMode() != TabBarStackingMode.disabled,
),
);
@@ -409,7 +556,7 @@ class _QuickTabSwitcherHistorySuggestionsTile extends HookConsumerWidget {
),
secondary: const Icon(MdiIcons.history),
value: showHistorySuggestions,
onChanged: tabBarShowQuickTabSwitcherBar
onChanged: switcherEnabled
? (value) async {
await ref
.read(saveGeneralSettingsControllerProvider.notifier)
@@ -433,9 +580,9 @@ class _QuickTabSwitcherShowTitlesTile extends HookConsumerWidget {
(s) => s.quickTabSwitcherShowTitles,
),
);
final tabBarShowQuickTabSwitcherBar = ref.watch(
final switcherEnabled = ref.watch(
generalSettingsWithDefaultsProvider.select(
(s) => s.tabBarShowQuickTabSwitcherBar,
(s) => s.effectiveTabBarStackingMode() != TabBarStackingMode.disabled,
),
);
@@ -446,7 +593,7 @@ class _QuickTabSwitcherShowTitlesTile extends HookConsumerWidget {
),
secondary: const Icon(MdiIcons.textRecognition),
value: quickTabSwitcherShowTitles,
onChanged: tabBarShowQuickTabSwitcherBar
onChanged: switcherEnabled
? (value) async {
await ref
.read(saveGeneralSettingsControllerProvider.notifier)
@@ -476,9 +623,9 @@ class _QuickTabSwitcherHierarchyGlyphsTile extends HookConsumerWidget {
(s) => s.quickTabSwitcherHierarchyGlyphs,
),
);
final tabBarShowQuickTabSwitcherBar = ref.watch(
final switcherEnabled = ref.watch(
generalSettingsWithDefaultsProvider.select(
(s) => s.tabBarShowQuickTabSwitcherBar,
(s) => s.effectiveTabBarStackingMode() != TabBarStackingMode.disabled,
),
);
@@ -489,7 +636,7 @@ class _QuickTabSwitcherHierarchyGlyphsTile extends HookConsumerWidget {
}, [hierarchyGlyphs]);
final currentGlyphs = sliderValue.value.round();
final enabled = tabBarShowQuickTabSwitcherBar;
final enabled = switcherEnabled;
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8),
@@ -57,11 +57,15 @@ class TabBarPreviewHeaderDelegate extends SliverPersistentHeaderDelegate {
height += BrowserTabBar.contextualToolabarHeight;
}
if (settings.tabBarShowQuickTabSwitcherBar) {
height += BrowserTabBar.quickTabSwitcherHeight;
}
final quickTabSwitcherRows = switch (settings
.effectiveTabBarStackingMode()) {
TabBarStackingMode.disabled => 0,
TabBarStackingMode.twoLevel => 2,
_ => 1,
};
return height;
return height +
BrowserTabBar.quickTabSwitcherHeight * quickTabSwitcherRows;
}
double get _baseHeight =>
@@ -114,6 +118,7 @@ class TabBarPreviewCard extends HookWidget {
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
final quickTabsController = useScrollController();
final quickTabsSecondRowController = useScrollController();
final showMainToolbarActionButtons = !settings.tabBarShowContextualBar;
final previewTabState = TabState.$default('preview-tab').copyWith(
@@ -134,8 +139,8 @@ class TabBarPreviewCard extends HookWidget {
tabMode: TabMode.regular,
isHistory: false,
isPinned:
settings.effectiveUiQuickTabSwitcherMode() ==
QuickTabSwitcherMode.containerTabs,
settings.effectiveTabBarStackingMode() ==
TabBarStackingMode.containerTabs,
url: Uri.parse('https://example.com/news'),
color: settings.showContainerUi ? colorScheme.primary : null,
avatar: const Icon(MdiIcons.web, size: 20),
@@ -196,19 +201,39 @@ class TabBarPreviewCard extends HookWidget {
},
);
Widget buildQuickTabSwitcher() {
Widget buildQuickTabSwitcherRow(ScrollController scrollController) {
return QuickTabSwitcherView(
availableItems: previewQuickItems,
activeItem: previewQuickItems.firstWhere((item) => item.isActive),
scrollController: quickTabsController,
scrollController: scrollController,
showTitles: settings.quickTabSwitcherShowTitles,
showIsolatedTabUi: settings.showIsolatedTabUi,
hierarchyGlyphs: settings.quickTabSwitcherHierarchyGlyphs,
titleMaxWidth: settings.quickTabSwitcherTitleWidth,
showCloseButtonOnAllTabs:
settings.quickTabSwitcherShowCloseButtonOnAllTabs,
enablePinTabInMenu: false,
onSelected: (_) async {},
onCloseItem: (_) async {},
);
}
Widget buildQuickTabSwitcher() {
// The accordion preview reuses the single-row layout; container header
// chips need live container data that the static preview doesn't have.
if (settings.effectiveTabBarStackingMode() ==
TabBarStackingMode.twoLevel) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
buildQuickTabSwitcherRow(quickTabsController),
buildQuickTabSwitcherRow(quickTabsSecondRowController),
],
);
}
return buildQuickTabSwitcherRow(quickTabsController);
}
Widget buildContextualToolbar() {
return ContextualToolbarView(
buttons: [
@@ -235,10 +260,13 @@ class TabBarPreviewCard extends HookWidget {
if (showMainToolbarActionButtons) NavigationMenuButtonView(onTap: () {}),
];
final showQuickTabSwitcherBar =
settings.effectiveTabBarStackingMode() != TabBarStackingMode.disabled;
final bottomCombinedToolbar = BrowserTabBarView(
showMainToolbar: true,
showContextualToolbar: settings.tabBarShowContextualBar,
showQuickTabSwitcherBar: settings.tabBarShowQuickTabSwitcherBar,
showQuickTabSwitcherBar: showQuickTabSwitcherBar,
displayAppBar: true,
displayQuickTabSwitcher: true,
backgroundColor:
@@ -270,7 +298,7 @@ class TabBarPreviewCard extends HookWidget {
final topBottomToolbar = BrowserTabBarView(
showMainToolbar: false,
showContextualToolbar: settings.tabBarShowContextualBar,
showQuickTabSwitcherBar: settings.tabBarShowQuickTabSwitcherBar,
showQuickTabSwitcherBar: showQuickTabSwitcherBar,
displayAppBar: false,
displayQuickTabSwitcher: true,
backgroundColor: colorScheme.surfaceContainer,