visual nesting indicator for quick tab switcher

This commit is contained in:
Fabian Freund
2026-06-06 06:33:39 +02:00
parent bafd77af5a
commit beb9a27a9c
9 changed files with 167 additions and 10 deletions
@@ -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<void> 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(
@@ -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(
@@ -35,7 +35,7 @@ final class GestureSettingsRepositoryProvider
}
String _$gestureSettingsRepositoryHash() =>
r'fc898b669a484fe45f89024d8456190f08d56085';
r'fcd226987b9748e9da2c1dbc454b5f6853232ea1';
abstract class _$GestureSettingsRepository
extends $StreamNotifier<GestureSettings> {
@@ -18,6 +18,7 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
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<SettingsSectionDefinition> 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();
@@ -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 {},
);
@@ -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,
@@ -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<String, dynamic> _$GeneralSettingsToJson(
'allowClipboardAccess': instance.allowClipboardAccess,
'tabListShowFavicons': instance.tabListShowFavicons,
'quickTabSwitcherShowTitles': instance.quickTabSwitcherShowTitles,
'quickTabSwitcherHierarchyGlyphs': instance.quickTabSwitcherHierarchyGlyphs,
'quickTabSwitcherShowHistorySuggestions':
instance.quickTabSwitcherShowHistorySuggestions,
'syncServerOverride': instance.syncServerOverride,
@@ -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,
@@ -35,7 +35,7 @@ final class GeneralSettingsRepositoryProvider
}
String _$generalSettingsRepositoryHash() =>
r'b269ac847d1ae1547bdfe0b926d8d4ffc5ec8f7a';
r'1e7c7955669933a2dac5a279bcfbac54246672ca';
abstract class _$GeneralSettingsRepository
extends $StreamNotifier<GeneralSettings> {