From beb9a27a9ca9087c804c5308abeb8d9d793735ea Mon Sep 17 00:00:00 2001 From: Fabian Freund Date: Sat, 6 Jun 2026 06:33:39 +0200 Subject: [PATCH] visual nesting indicator for quick tab switcher --- .../browser_modules/bottom_app_bar.dart | 19 +++- .../widgets/tab_view/tab_depth_indicator.dart | 11 +- .../repositories/gesture_settings.g.dart | 2 +- .../widgets/toolbar_layout_content.dart | 101 ++++++++++++++++++ .../presentation/widgets/toolbar_preview.dart | 3 +- .../user/data/models/general_settings.dart | 14 +++ .../user/data/models/general_settings.g.dart | 20 ++++ .../domain/repositories/general_settings.dart | 5 + .../repositories/general_settings.g.dart | 2 +- 9 files changed, 167 insertions(+), 10 deletions(-) diff --git a/apps/weblibre/lib/features/geckoview/features/browser/presentation/widgets/browser_modules/bottom_app_bar.dart b/apps/weblibre/lib/features/geckoview/features/browser/presentation/widgets/browser_modules/bottom_app_bar.dart index 20c69d58..855d67a1 100644 --- a/apps/weblibre/lib/features/geckoview/features/browser/presentation/widgets/browser_modules/bottom_app_bar.dart +++ b/apps/weblibre/lib/features/geckoview/features/browser/presentation/widgets/browser_modules/bottom_app_bar.dart @@ -608,9 +608,12 @@ class QuickTabSwitcher extends HookConsumerWidget { final sortPinnedFirst = ref.watch( tabViewFilterControllerProvider.select((v) => v.sortPinnedFirst), ); - final showHierarchicalTabs = ref.watch( - tabViewFilterControllerProvider.select((v) => v.showHierarchicalTabs), + final hierarchyGlyphs = ref.watch( + generalSettingsWithDefaultsProvider.select( + (s) => s.quickTabSwitcherHierarchyGlyphs, + ), ); + final showHierarchicalTabs = hierarchyGlyphs > 0; final selectedContainerId = ref.watch(selectedContainerProvider); final hierarchyContainerId = effectiveMode == QuickTabSwitcherMode.containerTabs @@ -761,6 +764,7 @@ class QuickTabSwitcher extends HookConsumerWidget { activeItemKey: activeItemKey.value, showTitles: showTitles, showIsolatedTabUi: showIsolatedTabUi, + hierarchyGlyphs: hierarchyGlyphs, enablePinTabInMenu: effectiveMode == QuickTabSwitcherMode.containerTabs, onSelected: (item) async { if (!item.isHistory && item.isActive) { @@ -832,6 +836,7 @@ class QuickTabSwitcherView extends StatelessWidget { this.activeItemKey, required this.showTitles, required this.showIsolatedTabUi, + this.hierarchyGlyphs = defaultQuickTabSwitcherHierarchyGlyphs, required this.enablePinTabInMenu, required this.onSelected, this.onReorderItem, @@ -845,6 +850,11 @@ class QuickTabSwitcherView extends StatelessWidget { final GlobalKey? activeItemKey; final bool showTitles; final bool showIsolatedTabUi; + + /// Max inline chevron glyphs on a chip's depth indicator before collapsing + /// into an icon + count badge. A value of 0 hides the indicator entirely. + final int hierarchyGlyphs; + final bool enablePinTabInMenu; final Future Function(QuickTabSwitcherItem item) onSelected; @@ -1007,7 +1017,7 @@ class QuickTabSwitcherView extends StatelessWidget { !item.isHistory && !item.isPinned && !item.isSandbox && - item.depth == 0 && + (item.depth == 0 || hierarchyGlyphs == 0) && item.tabMode is! PrivateTabMode && item.tabMode is! IsolatedTabMode) ? EdgeInsets.zero @@ -1025,7 +1035,7 @@ class QuickTabSwitcherView extends StatelessWidget { final row = Row( mainAxisSize: MainAxisSize.min, children: [ - if (item.depth > 0) + if (item.depth > 0 && hierarchyGlyphs > 0) Padding( padding: const EdgeInsets.only(right: 6.0), child: TabDepthIndicator( @@ -1033,6 +1043,7 @@ class QuickTabSwitcherView extends StatelessWidget { height: 24.0, iconSize: 14.0, horizontalPadding: 4.0, + maxInlineGlyphs: hierarchyGlyphs, ), ), Padding( diff --git a/apps/weblibre/lib/features/geckoview/features/browser/presentation/widgets/tab_view/tab_depth_indicator.dart b/apps/weblibre/lib/features/geckoview/features/browser/presentation/widgets/tab_view/tab_depth_indicator.dart index 29e147eb..caa593f0 100644 --- a/apps/weblibre/lib/features/geckoview/features/browser/presentation/widgets/tab_view/tab_depth_indicator.dart +++ b/apps/weblibre/lib/features/geckoview/features/browser/presentation/widgets/tab_view/tab_depth_indicator.dart @@ -21,8 +21,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_material_design_icons/flutter_material_design_icons.dart'; import 'package:weblibre/presentation/widgets/inline_count_badge.dart'; -// Above this depth, collapse the chevrons into a single icon + count badge. -const int _maxInlineGlyphs = 2; +// Default cap on inline chevrons before collapsing into a single icon + badge. +const int _defaultMaxInlineGlyphs = 2; class TabDepthIndicator extends StatelessWidget { final int depth; @@ -30,18 +30,23 @@ class TabDepthIndicator extends StatelessWidget { final double iconSize; final double horizontalPadding; + /// Number of chevron glyphs shown inline before the indicator collapses + /// into a single icon + count badge. + final int maxInlineGlyphs; + const TabDepthIndicator({ required this.depth, this.height = 28.0, this.iconSize = 16.0, this.horizontalPadding = 6.0, + this.maxInlineGlyphs = _defaultMaxInlineGlyphs, super.key, }); @override Widget build(BuildContext context) { final scheme = Theme.of(context).colorScheme; - final showBadge = depth > _maxInlineGlyphs; + final showBadge = depth > maxInlineGlyphs; final glyphCount = showBadge ? 1 : depth; return SizedBox( diff --git a/apps/weblibre/lib/features/gestures/domain/repositories/gesture_settings.g.dart b/apps/weblibre/lib/features/gestures/domain/repositories/gesture_settings.g.dart index 61699cbe..32b71ad0 100644 --- a/apps/weblibre/lib/features/gestures/domain/repositories/gesture_settings.g.dart +++ b/apps/weblibre/lib/features/gestures/domain/repositories/gesture_settings.g.dart @@ -35,7 +35,7 @@ final class GestureSettingsRepositoryProvider } String _$gestureSettingsRepositoryHash() => - r'fc898b669a484fe45f89024d8456190f08d56085'; + r'fcd226987b9748e9da2c1dbc454b5f6853232ea1'; abstract class _$GestureSettingsRepository extends $StreamNotifier { diff --git a/apps/weblibre/lib/features/settings/presentation/widgets/toolbar_layout_content.dart b/apps/weblibre/lib/features/settings/presentation/widgets/toolbar_layout_content.dart index ec4fdad3..667b3438 100644 --- a/apps/weblibre/lib/features/settings/presentation/widgets/toolbar_layout_content.dart +++ b/apps/weblibre/lib/features/settings/presentation/widgets/toolbar_layout_content.dart @@ -18,6 +18,7 @@ * along with this program. If not, see . */ import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_material_design_icons/flutter_material_design_icons.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:weblibre/core/routing/routes.dart'; @@ -100,6 +101,12 @@ const List toolbarLayoutSettingsSections = [ keywords: ['page titles'], child: _QuickTabSwitcherShowTitlesTile(), ), + SettingsEntryDefinition( + title: 'Hierarchy Depth in Quick Tab Switcher', + subtitle: 'How many nesting chevrons to show on switcher chips', + keywords: ['hierarchy', 'nesting', 'depth', 'tree', 'chevrons'], + child: _QuickTabSwitcherHierarchyGlyphsTile(), + ), ], ), SettingsSectionDefinition( @@ -453,6 +460,100 @@ class _QuickTabSwitcherShowTitlesTile extends HookConsumerWidget { } } +class _QuickTabSwitcherHierarchyGlyphsTile extends HookConsumerWidget { + const _QuickTabSwitcherHierarchyGlyphsTile(); + + static String _label(int glyphs) => switch (glyphs) { + 0 => 'Off', + 1 => '1 level', + _ => '$glyphs levels', + }; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final hierarchyGlyphs = ref.watch( + generalSettingsWithDefaultsProvider.select( + (s) => s.quickTabSwitcherHierarchyGlyphs, + ), + ); + final tabBarShowQuickTabSwitcherBar = ref.watch( + generalSettingsWithDefaultsProvider.select( + (s) => s.tabBarShowQuickTabSwitcherBar, + ), + ); + + final sliderValue = useState(hierarchyGlyphs.toDouble()); + useEffect(() { + sliderValue.value = hierarchyGlyphs.toDouble(); + return null; + }, [hierarchyGlyphs]); + + final currentGlyphs = sliderValue.value.round(); + final enabled = tabBarShowQuickTabSwitcherBar; + + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ListTile( + title: const Text('Hierarchy Depth in Quick Tab Switcher'), + subtitle: const Text( + 'How many nesting chevrons to show on switcher chips before ' + 'collapsing into a count badge (0 hides the indicator)', + ), + leading: const Icon(MdiIcons.fileTree), + contentPadding: EdgeInsets.zero, + enabled: enabled, + ), + Row( + children: [ + Expanded( + child: Slider( + min: minQuickTabSwitcherHierarchyGlyphs.toDouble(), + max: maxQuickTabSwitcherHierarchyGlyphs.toDouble(), + divisions: + maxQuickTabSwitcherHierarchyGlyphs - + minQuickTabSwitcherHierarchyGlyphs, + label: _label(currentGlyphs), + value: sliderValue.value.clamp( + minQuickTabSwitcherHierarchyGlyphs.toDouble(), + maxQuickTabSwitcherHierarchyGlyphs.toDouble(), + ), + onChanged: enabled + ? (value) { + sliderValue.value = value; + } + : null, + onChangeEnd: enabled + ? (value) async { + final normalized = value.round(); + sliderValue.value = normalized.toDouble(); + await ref + .read( + saveGeneralSettingsControllerProvider.notifier, + ) + .save( + (currentSettings) => currentSettings.copyWith + .quickTabSwitcherHierarchyGlyphs(normalized), + ); + } + : null, + ), + ), + Text( + _label(currentGlyphs), + style: Theme.of(context).textTheme.titleMedium, + ), + ], + ), + ], + ), + ); + } +} + class _AutoHideTabBarTile extends HookConsumerWidget { const _AutoHideTabBarTile(); diff --git a/apps/weblibre/lib/features/settings/presentation/widgets/toolbar_preview.dart b/apps/weblibre/lib/features/settings/presentation/widgets/toolbar_preview.dart index 8ae1d79a..e1d4f1bc 100644 --- a/apps/weblibre/lib/features/settings/presentation/widgets/toolbar_preview.dart +++ b/apps/weblibre/lib/features/settings/presentation/widgets/toolbar_preview.dart @@ -160,7 +160,7 @@ class TabBarPreviewCard extends HookWidget { tabMode: TabMode.isolated('preview-isolated-context'), isHistory: false, isPinned: false, - depth: 2, + depth: 3, url: Uri.parse('https://example.com/bank'), color: null, avatar: const Icon(MdiIcons.web, size: 20), @@ -203,6 +203,7 @@ class TabBarPreviewCard extends HookWidget { scrollController: quickTabsController, showTitles: settings.quickTabSwitcherShowTitles, showIsolatedTabUi: settings.showIsolatedTabUi, + hierarchyGlyphs: settings.quickTabSwitcherHierarchyGlyphs, enablePinTabInMenu: false, onSelected: (_) async {}, ); diff --git a/apps/weblibre/lib/features/user/data/models/general_settings.dart b/apps/weblibre/lib/features/user/data/models/general_settings.dart index 86ac4b1e..e88c8998 100644 --- a/apps/weblibre/lib/features/user/data/models/general_settings.dart +++ b/apps/weblibre/lib/features/user/data/models/general_settings.dart @@ -40,6 +40,13 @@ const minUiScaleFactor = 0.5; const maxUiScaleFactor = 1.5; const uiScaleFactorStep = 0.05; +/// Max number of inline chevron glyphs shown on a quick tab switcher chip +/// before the depth indicator collapses into a single icon + count badge. +/// A value of 0 hides the hierarchy indicator entirely. +const defaultQuickTabSwitcherHierarchyGlyphs = 2; +const minQuickTabSwitcherHierarchyGlyphs = 0; +const maxQuickTabSwitcherHierarchyGlyphs = 4; + enum TabBarSwipeAction { switchLastOpened, navigateOrderedTabs } enum QuickTabSwitcherMode { lastUsedTabs, containerTabs } @@ -106,6 +113,7 @@ class GeneralSettings with FastEquatable { final bool allowClipboardAccess; final bool tabListShowFavicons; final bool quickTabSwitcherShowTitles; + final int quickTabSwitcherHierarchyGlyphs; final bool quickTabSwitcherShowHistorySuggestions; final String syncServerOverride; final String syncTokenServerOverride; @@ -173,6 +181,7 @@ class GeneralSettings with FastEquatable { required this.allowClipboardAccess, required this.tabListShowFavicons, required this.quickTabSwitcherShowTitles, + required this.quickTabSwitcherHierarchyGlyphs, required this.quickTabSwitcherShowHistorySuggestions, required this.syncServerOverride, required this.syncTokenServerOverride, @@ -232,6 +241,7 @@ class GeneralSettings with FastEquatable { bool? allowClipboardAccess, bool? tabListShowFavicons, bool? quickTabSwitcherShowTitles, + int? quickTabSwitcherHierarchyGlyphs, bool? quickTabSwitcherShowHistorySuggestions, String? syncServerOverride, String? syncTokenServerOverride, @@ -293,6 +303,9 @@ class GeneralSettings with FastEquatable { allowClipboardAccess = allowClipboardAccess ?? true, tabListShowFavicons = tabListShowFavicons ?? false, quickTabSwitcherShowTitles = quickTabSwitcherShowTitles ?? true, + quickTabSwitcherHierarchyGlyphs = + quickTabSwitcherHierarchyGlyphs ?? + defaultQuickTabSwitcherHierarchyGlyphs, quickTabSwitcherShowHistorySuggestions = quickTabSwitcherShowHistorySuggestions ?? true, syncServerOverride = syncServerOverride ?? '', @@ -407,6 +420,7 @@ class GeneralSettings with FastEquatable { allowClipboardAccess, tabListShowFavicons, quickTabSwitcherShowTitles, + quickTabSwitcherHierarchyGlyphs, quickTabSwitcherShowHistorySuggestions, syncServerOverride, syncTokenServerOverride, diff --git a/apps/weblibre/lib/features/user/data/models/general_settings.g.dart b/apps/weblibre/lib/features/user/data/models/general_settings.g.dart index 6725f044..78b050f3 100644 --- a/apps/weblibre/lib/features/user/data/models/general_settings.g.dart +++ b/apps/weblibre/lib/features/user/data/models/general_settings.g.dart @@ -91,6 +91,10 @@ abstract class _$GeneralSettingsCWProxy { GeneralSettings quickTabSwitcherShowTitles(bool quickTabSwitcherShowTitles); + GeneralSettings quickTabSwitcherHierarchyGlyphs( + int quickTabSwitcherHierarchyGlyphs, + ); + GeneralSettings quickTabSwitcherShowHistorySuggestions( bool quickTabSwitcherShowHistorySuggestions, ); @@ -182,6 +186,7 @@ abstract class _$GeneralSettingsCWProxy { bool allowClipboardAccess, bool tabListShowFavicons, bool quickTabSwitcherShowTitles, + int quickTabSwitcherHierarchyGlyphs, bool quickTabSwitcherShowHistorySuggestions, String syncServerOverride, String syncTokenServerOverride, @@ -360,6 +365,11 @@ class _$GeneralSettingsCWProxyImpl implements _$GeneralSettingsCWProxy { GeneralSettings quickTabSwitcherShowTitles(bool quickTabSwitcherShowTitles) => call(quickTabSwitcherShowTitles: quickTabSwitcherShowTitles); + @override + GeneralSettings quickTabSwitcherHierarchyGlyphs( + int quickTabSwitcherHierarchyGlyphs, + ) => call(quickTabSwitcherHierarchyGlyphs: quickTabSwitcherHierarchyGlyphs); + @override GeneralSettings quickTabSwitcherShowHistorySuggestions( bool quickTabSwitcherShowHistorySuggestions, @@ -495,6 +505,7 @@ class _$GeneralSettingsCWProxyImpl implements _$GeneralSettingsCWProxy { Object? allowClipboardAccess = const $CopyWithPlaceholder(), Object? tabListShowFavicons = const $CopyWithPlaceholder(), Object? quickTabSwitcherShowTitles = const $CopyWithPlaceholder(), + Object? quickTabSwitcherHierarchyGlyphs = const $CopyWithPlaceholder(), Object? quickTabSwitcherShowHistorySuggestions = const $CopyWithPlaceholder(), Object? syncServerOverride = const $CopyWithPlaceholder(), @@ -723,6 +734,12 @@ class _$GeneralSettingsCWProxyImpl implements _$GeneralSettingsCWProxy { ? _value.quickTabSwitcherShowTitles // ignore: cast_nullable_to_non_nullable : quickTabSwitcherShowTitles as bool, + quickTabSwitcherHierarchyGlyphs: + quickTabSwitcherHierarchyGlyphs == const $CopyWithPlaceholder() || + quickTabSwitcherHierarchyGlyphs == null + ? _value.quickTabSwitcherHierarchyGlyphs + // ignore: cast_nullable_to_non_nullable + : quickTabSwitcherHierarchyGlyphs as int, quickTabSwitcherShowHistorySuggestions: quickTabSwitcherShowHistorySuggestions == const $CopyWithPlaceholder() || @@ -943,6 +960,8 @@ GeneralSettings _$GeneralSettingsFromJson( allowClipboardAccess: json['allowClipboardAccess'] as bool?, tabListShowFavicons: json['tabListShowFavicons'] as bool?, quickTabSwitcherShowTitles: json['quickTabSwitcherShowTitles'] as bool?, + quickTabSwitcherHierarchyGlyphs: + (json['quickTabSwitcherHierarchyGlyphs'] as num?)?.toInt(), quickTabSwitcherShowHistorySuggestions: json['quickTabSwitcherShowHistorySuggestions'] as bool?, syncServerOverride: json['syncServerOverride'] as String?, @@ -1023,6 +1042,7 @@ Map _$GeneralSettingsToJson( 'allowClipboardAccess': instance.allowClipboardAccess, 'tabListShowFavicons': instance.tabListShowFavicons, 'quickTabSwitcherShowTitles': instance.quickTabSwitcherShowTitles, + 'quickTabSwitcherHierarchyGlyphs': instance.quickTabSwitcherHierarchyGlyphs, 'quickTabSwitcherShowHistorySuggestions': instance.quickTabSwitcherShowHistorySuggestions, 'syncServerOverride': instance.syncServerOverride, diff --git a/apps/weblibre/lib/features/user/domain/repositories/general_settings.dart b/apps/weblibre/lib/features/user/domain/repositories/general_settings.dart index fc49c759..dc8dc7e7 100644 --- a/apps/weblibre/lib/features/user/domain/repositories/general_settings.dart +++ b/apps/weblibre/lib/features/user/domain/repositories/general_settings.dart @@ -181,6 +181,11 @@ class GeneralSettingsRepository extends _$GeneralSettingsRepository { ), 'quickTabSwitcherShowTitles': settings['quickTabSwitcherShowTitles'] ?.readAs(DriftSqlType.bool, db.typeMapping), + 'quickTabSwitcherHierarchyGlyphs': + settings['quickTabSwitcherHierarchyGlyphs']?.readAs( + DriftSqlType.int, + db.typeMapping, + ), 'quickTabSwitcherShowHistorySuggestions': settings['quickTabSwitcherShowHistorySuggestions']?.readAs( DriftSqlType.bool, diff --git a/apps/weblibre/lib/features/user/domain/repositories/general_settings.g.dart b/apps/weblibre/lib/features/user/domain/repositories/general_settings.g.dart index f1f0456e..42a5ff58 100644 --- a/apps/weblibre/lib/features/user/domain/repositories/general_settings.g.dart +++ b/apps/weblibre/lib/features/user/domain/repositories/general_settings.g.dart @@ -35,7 +35,7 @@ final class GeneralSettingsRepositoryProvider } String _$generalSettingsRepositoryHash() => - r'b269ac847d1ae1547bdfe0b926d8d4ffc5ec8f7a'; + r'1e7c7955669933a2dac5a279bcfbac54246672ca'; abstract class _$GeneralSettingsRepository extends $StreamNotifier {