fix side rail overflows
This commit is contained in:
+7
-26
@@ -481,35 +481,14 @@ class BrowserTabBar extends HookConsumerWidget {
|
|||||||
axis: switcherAxis,
|
axis: switcherAxis,
|
||||||
),
|
),
|
||||||
// History fallback only on the MRU row, so empty-state history
|
// History fallback only on the MRU row, so empty-state history
|
||||||
// chips don't show twice. On the rail the two rows stack as two
|
// chips don't show twice. Only reached on the horizontal bar:
|
||||||
// equal-height vertical lists.
|
// effectiveTabBarStackingMode() degrades twoLevel to containerTabs
|
||||||
TabBarStackingMode.twoLevel =>
|
// on the narrow vertical rail, where two stacked rows don't fit.
|
||||||
isVertical
|
TabBarStackingMode.twoLevel => const Column(
|
||||||
? Column(
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: QuickTabSwitcher(
|
|
||||||
quickTabSwitcherMode:
|
|
||||||
QuickTabSwitcherMode.containerTabs,
|
|
||||||
enableHistoryFallback: false,
|
|
||||||
axis: switcherAxis,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Expanded(
|
|
||||||
child: QuickTabSwitcher(
|
|
||||||
quickTabSwitcherMode:
|
|
||||||
QuickTabSwitcherMode.lastUsedTabs,
|
|
||||||
axis: switcherAxis,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
: const Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
QuickTabSwitcher(
|
QuickTabSwitcher(
|
||||||
quickTabSwitcherMode:
|
quickTabSwitcherMode: QuickTabSwitcherMode.containerTabs,
|
||||||
QuickTabSwitcherMode.containerTabs,
|
|
||||||
enableHistoryFallback: false,
|
enableHistoryFallback: false,
|
||||||
),
|
),
|
||||||
QuickTabSwitcher(
|
QuickTabSwitcher(
|
||||||
@@ -1228,6 +1207,7 @@ class QuickTabSwitcherView extends StatelessWidget {
|
|||||||
context,
|
context,
|
||||||
showTitles: showTitles,
|
showTitles: showTitles,
|
||||||
hierarchyGlyphs: hierarchyGlyphs,
|
hierarchyGlyphs: hierarchyGlyphs,
|
||||||
|
isVertical: _isVertical,
|
||||||
canDelete: _canShowCloseButton,
|
canDelete: _canShowCloseButton,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1245,6 +1225,7 @@ class QuickTabSwitcherView extends StatelessWidget {
|
|||||||
showIsolatedTabUi: showIsolatedTabUi,
|
showIsolatedTabUi: showIsolatedTabUi,
|
||||||
hierarchyGlyphs: hierarchyGlyphs,
|
hierarchyGlyphs: hierarchyGlyphs,
|
||||||
titleMaxWidth: titleMaxWidth,
|
titleMaxWidth: titleMaxWidth,
|
||||||
|
isVertical: _isVertical,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-1
@@ -162,6 +162,7 @@ class AccordionQuickTabSwitcher extends HookConsumerWidget {
|
|||||||
context,
|
context,
|
||||||
showTitles: showTitles,
|
showTitles: showTitles,
|
||||||
hierarchyGlyphs: hierarchyGlyphs,
|
hierarchyGlyphs: hierarchyGlyphs,
|
||||||
|
isVertical: isVertical,
|
||||||
// The tray is painted in the container color, so the active tab's normal
|
// The tray is painted in the container color, so the active tab's normal
|
||||||
// (transparent) selected border would blend in; give it a thicker border
|
// (transparent) selected border would blend in; give it a thicker border
|
||||||
// in the container's outline color instead.
|
// in the container's outline color instead.
|
||||||
@@ -201,6 +202,7 @@ class AccordionQuickTabSwitcher extends HookConsumerWidget {
|
|||||||
showIsolatedTabUi: showIsolatedTabUi,
|
showIsolatedTabUi: showIsolatedTabUi,
|
||||||
hierarchyGlyphs: hierarchyGlyphs,
|
hierarchyGlyphs: hierarchyGlyphs,
|
||||||
titleMaxWidth: titleMaxWidth,
|
titleMaxWidth: titleMaxWidth,
|
||||||
|
isVertical: isVertical,
|
||||||
),
|
),
|
||||||
// Spacing inside the expanded group is owned by the surrounding tray
|
// Spacing inside the expanded group is owned by the surrounding tray
|
||||||
// slice so the slices abut into one continuous background.
|
// slice so the slices abut into one continuous background.
|
||||||
@@ -527,7 +529,9 @@ class _AccordionHeaderChip extends StatelessWidget {
|
|||||||
if (iconAvatar != null) iconAvatar,
|
if (iconAvatar != null) iconAvatar,
|
||||||
if (countBadge != null) ...[
|
if (countBadge != null) ...[
|
||||||
if (iconAvatar != null) const SizedBox(height: 4),
|
if (iconAvatar != null) const SizedBox(height: 4),
|
||||||
countBadge,
|
// Multi-digit counts can exceed the narrow rail's fixed 48px chip
|
||||||
|
// width; scale the badge down to fit instead of overflowing.
|
||||||
|
FittedBox(fit: BoxFit.scaleDown, child: countBadge),
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
+77
-3
@@ -146,6 +146,10 @@ buildQuickTabSwitcherChipDecoration(
|
|||||||
BuildContext context, {
|
BuildContext context, {
|
||||||
required bool showTitles,
|
required bool showTitles,
|
||||||
required int hierarchyGlyphs,
|
required int hierarchyGlyphs,
|
||||||
|
// On the vertical rail nesting is shown as a corner badge on the favicon (not
|
||||||
|
// a leading pill), so a nested chip carries no extra inline width and can use
|
||||||
|
// the same zero label padding as a leaf chip instead of overflowing.
|
||||||
|
bool isVertical = false,
|
||||||
bool Function(QuickTabSwitcherItem item)? canDelete,
|
bool Function(QuickTabSwitcherItem item)? canDelete,
|
||||||
// When true, the selected item gets a thicker border in its container's
|
// When true, the selected item gets a thicker border in its container's
|
||||||
// outline color (instead of the usual transparent selected border). The
|
// outline color (instead of the usual transparent selected border). The
|
||||||
@@ -195,7 +199,7 @@ buildQuickTabSwitcherChipDecoration(
|
|||||||
!item.isHistory &&
|
!item.isHistory &&
|
||||||
!item.isPinned &&
|
!item.isPinned &&
|
||||||
!item.isSandbox &&
|
!item.isSandbox &&
|
||||||
(item.depth == 0 || hierarchyGlyphs == 0) &&
|
(item.depth == 0 || hierarchyGlyphs == 0 || isVertical) &&
|
||||||
item.tabMode is! PrivateTabMode &&
|
item.tabMode is! PrivateTabMode &&
|
||||||
item.tabMode is! IsolatedTabMode)
|
item.tabMode is! IsolatedTabMode)
|
||||||
? EdgeInsets.zero
|
? EdgeInsets.zero
|
||||||
@@ -213,13 +217,22 @@ Widget buildQuickTabSwitcherChipLabel(
|
|||||||
required bool showIsolatedTabUi,
|
required bool showIsolatedTabUi,
|
||||||
required int hierarchyGlyphs,
|
required int hierarchyGlyphs,
|
||||||
required double titleMaxWidth,
|
required double titleMaxWidth,
|
||||||
|
// On the narrow vertical rail the leading depth pill has no room beside the
|
||||||
|
// favicon (it clips the icon), so nesting is shown as a compact corner badge
|
||||||
|
// overlaid on the favicon instead.
|
||||||
|
bool isVertical = false,
|
||||||
}) {
|
}) {
|
||||||
final appColors = AppColors.of(context);
|
final appColors = AppColors.of(context);
|
||||||
final hasTitle = item.isHistory || showTitles;
|
final hasTitle = item.isHistory || showTitles;
|
||||||
|
final isNested = item.depth > 0 && hierarchyGlyphs > 0;
|
||||||
|
final showInlineDepth = isNested && !isVertical;
|
||||||
|
final avatar = (isNested && isVertical)
|
||||||
|
? _RailDepthAvatar(depth: item.depth, child: item.avatar)
|
||||||
|
: item.avatar;
|
||||||
final row = Row(
|
final row = Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
if (item.depth > 0 && hierarchyGlyphs > 0)
|
if (showInlineDepth)
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(right: 6.0),
|
padding: const EdgeInsets.only(right: 6.0),
|
||||||
child: TabDepthIndicator(
|
child: TabDepthIndicator(
|
||||||
@@ -232,7 +245,7 @@ Widget buildQuickTabSwitcherChipLabel(
|
|||||||
),
|
),
|
||||||
Padding(
|
Padding(
|
||||||
padding: EdgeInsets.only(right: hasTitle ? 6.0 : 0.0),
|
padding: EdgeInsets.only(right: hasTitle ? 6.0 : 0.0),
|
||||||
child: item.avatar,
|
child: avatar,
|
||||||
),
|
),
|
||||||
if (hasTitle)
|
if (hasTitle)
|
||||||
ConstrainedBox(
|
ConstrainedBox(
|
||||||
@@ -305,6 +318,67 @@ Widget buildQuickTabSwitcherChipLabel(
|
|||||||
row;
|
row;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Favicon with a compact nesting badge overlaid on its bottom-right corner,
|
||||||
|
/// used on the vertical side rail where the inline [TabDepthIndicator] pill
|
||||||
|
/// would clip the favicon. The badge shows a subdirectory-arrow glyph for a
|
||||||
|
/// direct child and the depth number for deeper nesting.
|
||||||
|
class _RailDepthAvatar extends StatelessWidget {
|
||||||
|
final int depth;
|
||||||
|
final Widget child;
|
||||||
|
|
||||||
|
const _RailDepthAvatar({required this.depth, required this.child});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final scheme = Theme.of(context).colorScheme;
|
||||||
|
|
||||||
|
// Keep the exact 20px footprint of a plain favicon so a nested chip stays
|
||||||
|
// the same width as a leaf chip on the narrow rail; the badge is anchored
|
||||||
|
// inside the horizontal bounds (only the vertical corner is allowed to
|
||||||
|
// bleed, where there's ample room) so it never widens the chip.
|
||||||
|
return SizedBox(
|
||||||
|
width: 20,
|
||||||
|
height: 20,
|
||||||
|
child: Stack(
|
||||||
|
clipBehavior: Clip.none,
|
||||||
|
children: [
|
||||||
|
Positioned.fill(child: child),
|
||||||
|
Positioned(
|
||||||
|
right: -3,
|
||||||
|
bottom: -5,
|
||||||
|
child: Container(
|
||||||
|
constraints: const BoxConstraints(minWidth: 12, minHeight: 12),
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 1.0),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: scheme.secondaryContainer,
|
||||||
|
borderRadius: const BorderRadius.all(Radius.circular(6.0)),
|
||||||
|
border: Border.all(color: scheme.surfaceContainer, width: 1.0),
|
||||||
|
),
|
||||||
|
child: Center(
|
||||||
|
child: depth > 1
|
||||||
|
? Text(
|
||||||
|
'$depth',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 9,
|
||||||
|
height: 1.0,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
color: scheme.onSecondaryContainer,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: Icon(
|
||||||
|
MdiIcons.subdirectoryArrowRight,
|
||||||
|
size: 10,
|
||||||
|
color: scheme.onSecondaryContainer,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// FilterChip matching [SelectableChips]' visual contract, used by render
|
/// FilterChip matching [SelectableChips]' visual contract, used by render
|
||||||
/// paths that can't go through [SelectableChips] itself (the reorderable
|
/// paths that can't go through [SelectableChips] itself (the reorderable
|
||||||
/// list and the accordion view). Stateless wrapper so a parent
|
/// list and the accordion view). Stateless wrapper so a parent
|
||||||
|
|||||||
+8
-4
@@ -400,13 +400,13 @@ class _TabBarStackingModeSection extends HookConsumerWidget {
|
|||||||
title: Text('Recently Used Tabs'),
|
title: Text('Recently Used Tabs'),
|
||||||
subtitle: Text('Recently used tabs across all containers'),
|
subtitle: Text('Recently used tabs across all containers'),
|
||||||
),
|
),
|
||||||
if (settings.showContainerUi) ...const [
|
if (settings.showContainerUi) ...[
|
||||||
RadioListTile.adaptive(
|
const RadioListTile.adaptive(
|
||||||
value: TabBarStackingMode.containerTabs,
|
value: TabBarStackingMode.containerTabs,
|
||||||
title: Text('Container Tabs'),
|
title: Text('Container Tabs'),
|
||||||
subtitle: Text('Ordered tabs of the selected container'),
|
subtitle: Text('Ordered tabs of the selected container'),
|
||||||
),
|
),
|
||||||
RadioListTile.adaptive(
|
const RadioListTile.adaptive(
|
||||||
value: TabBarStackingMode.accordion,
|
value: TabBarStackingMode.accordion,
|
||||||
title: Text('Accordion'),
|
title: Text('Accordion'),
|
||||||
subtitle: Text(
|
subtitle: Text(
|
||||||
@@ -414,7 +414,11 @@ class _TabBarStackingModeSection extends HookConsumerWidget {
|
|||||||
"container's tabs expanded inline",
|
"container's tabs expanded inline",
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
RadioListTile.adaptive(
|
// Two stacked rows don't fit the narrow vertical side rail,
|
||||||
|
// where the mode degrades to Container Tabs; hide the option
|
||||||
|
// for left/right positions to avoid a no-op choice.
|
||||||
|
if (!settings.tabBarPosition.isVertical)
|
||||||
|
const RadioListTile.adaptive(
|
||||||
value: TabBarStackingMode.twoLevel,
|
value: TabBarStackingMode.twoLevel,
|
||||||
title: Text('Two Rows'),
|
title: Text('Two Rows'),
|
||||||
subtitle: Text(
|
subtitle: Text(
|
||||||
|
|||||||
@@ -511,17 +511,26 @@ class GeneralSettings with FastEquatable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Container-dependent stacking modes degrade to a single recently-used
|
/// Container-dependent stacking modes degrade to a single recently-used
|
||||||
/// row when the container UI is disabled.
|
/// row when the container UI is disabled. Two-level stacking additionally
|
||||||
|
/// degrades to the single container-tabs row on the narrow vertical rail,
|
||||||
|
/// where two stacked chip lists have no room.
|
||||||
TabBarStackingMode effectiveTabBarStackingMode() {
|
TabBarStackingMode effectiveTabBarStackingMode() {
|
||||||
|
var mode = tabBarStackingMode;
|
||||||
|
|
||||||
|
if (tabBarPosition.isVertical && mode == TabBarStackingMode.twoLevel) {
|
||||||
|
mode = TabBarStackingMode.containerTabs;
|
||||||
|
}
|
||||||
|
|
||||||
if (!showContainerUi &&
|
if (!showContainerUi &&
|
||||||
const {
|
const {
|
||||||
TabBarStackingMode.containerTabs,
|
TabBarStackingMode.containerTabs,
|
||||||
TabBarStackingMode.accordion,
|
TabBarStackingMode.accordion,
|
||||||
TabBarStackingMode.twoLevel,
|
TabBarStackingMode.twoLevel,
|
||||||
}.contains(tabBarStackingMode)) {
|
}.contains(mode)) {
|
||||||
return TabBarStackingMode.lastUsedTabs;
|
return TabBarStackingMode.lastUsedTabs;
|
||||||
}
|
}
|
||||||
return tabBarStackingMode;
|
|
||||||
|
return mode;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
Reference in New Issue
Block a user