diff --git a/lib/focus/focusable_chip_mixin.dart b/lib/focus/focusable_chip_mixin.dart index dd6f924a..0da516bf 100644 --- a/lib/focus/focusable_chip_mixin.dart +++ b/lib/focus/focusable_chip_mixin.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import '../utils/scroll_utils.dart'; import 'dpad_navigator.dart'; import 'key_event_utils.dart'; @@ -86,6 +87,10 @@ mixin FocusableChipStateMixin on State { void _onFocusChange() { if (mounted) { setState(() => _isFocused = focusNode.hasFocus); + // Same convention as FocusableTileStateMixin: a chip inside a + // scrollable strip (TabChipStrip, filter bars) reveals itself on + // focus; a no-op when no ancestor scrollable exists. + if (focusNode.hasFocus) scrollContextToCenter(context); } } diff --git a/lib/screens/downloads/downloads_screen.dart b/lib/screens/downloads/downloads_screen.dart index 9a4a8cb8..ef57ff6b 100644 --- a/lib/screens/downloads/downloads_screen.dart +++ b/lib/screens/downloads/downloads_screen.dart @@ -12,6 +12,7 @@ import '../../services/music/music_playback_service.dart'; import '../../theme/mono_tokens.dart'; import '../../utils/music_navigation.dart'; import '../../widgets/app_icon.dart'; +import '../../widgets/focusable_tab_chip.dart'; import '../../widgets/music/mini_player.dart'; import '../../widgets/music/track_row.dart'; import '../../services/settings_service.dart'; @@ -114,7 +115,7 @@ class DownloadsScreenState extends State Widget _buildAppBarTitle() { // On desktop/TV with side nav, show tabs in app bar if (PlatformDetector.shouldUseSideNavigation(context)) { - return Row( + return TabChipStrip( children: [ _buildTabChip(t.downloads.manage, 0), const SizedBox(width: 8), diff --git a/lib/screens/libraries/libraries_screen.dart b/lib/screens/libraries/libraries_screen.dart index 52209e54..9067898e 100644 --- a/lib/screens/libraries/libraries_screen.dart +++ b/lib/screens/libraries/libraries_screen.dart @@ -24,6 +24,7 @@ import '../../utils/content_utils.dart'; import '../../widgets/app_menu.dart'; import '../../widgets/backend_badge.dart'; import '../../widgets/desktop_app_bar.dart'; +import '../../widgets/focusable_tab_chip.dart'; import '../../widgets/library_management_sheet.dart'; import '../../services/storage_service.dart'; import '../../mixins/refreshable.dart'; @@ -681,8 +682,7 @@ class _LibrariesScreenState extends State // On desktop/TV with side nav, show tabs in app bar (library name is in side nav) if (PlatformDetector.shouldUseSideNavigation(context)) { - return Row( - mainAxisSize: .min, + return TabChipStrip( children: [ for (int i = 0; i < _visibleTabs.length; i++) ...[ if (i > 0) const SizedBox(width: 8), diff --git a/lib/screens/livetv/live_tv_screen.dart b/lib/screens/livetv/live_tv_screen.dart index 940ce97b..2cd7fd55 100644 --- a/lib/screens/livetv/live_tv_screen.dart +++ b/lib/screens/livetv/live_tv_screen.dart @@ -23,6 +23,7 @@ import '../../utils/desktop_window_padding.dart'; import '../../utils/platform_detector.dart'; import '../../utils/snackbar_helper.dart'; import '../../widgets/app_icon.dart'; +import '../../widgets/focusable_tab_chip.dart'; import '../../widgets/overlay_sheet.dart'; import 'reorder_favorites_sheet.dart'; import 'tabs/guide_tab.dart'; @@ -549,7 +550,7 @@ class _LiveTvScreenState extends State final isRecordings = _currentTab == LiveTvTab.recordings; return Scaffold( appBar: AppBar( - title: useSideNav ? Row(children: _buildTabChipItems()) : Text(t.liveTv.title), + title: useSideNav ? TabChipStrip(children: _buildTabChipItems()) : Text(t.liveTv.title), actions: DesktopAppBarHelper.buildAdjustedActions([ FocusableActionBar( key: _actionBarKey, @@ -646,10 +647,7 @@ class _LiveTvScreenState extends State Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), alignment: .centerLeft, - child: SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: Row(children: _buildTabChipItems()), - ), + child: TabChipStrip(children: _buildTabChipItems()), ), Expanded( child: TabBarView( diff --git a/lib/widgets/focusable_tab_chip.dart b/lib/widgets/focusable_tab_chip.dart index 5419bf7a..38aa8870 100644 --- a/lib/widgets/focusable_tab_chip.dart +++ b/lib/widgets/focusable_tab_chip.dart @@ -5,6 +5,27 @@ import '../focus/input_mode_tracker.dart'; import '../utils/platform_detector.dart'; import 'focus_builders.dart'; +/// Horizontally scrollable host for a row of [FocusableTabChip]s. +/// +/// App-bar titles and header rows give the strip a bounded width; a plain +/// Row overflows it on narrow windows (visible as the striped overflow +/// indicator). The strip shrink-wraps like `mainAxisSize: min` and scrolls +/// instead. D-pad stays correct: chips center themselves on focus via the +/// chip mixin, so LEFT/RIGHT reaches off-screen tabs. +class TabChipStrip extends StatelessWidget { + final List children; + + const TabChipStrip({super.key, required this.children}); + + @override + Widget build(BuildContext context) { + return SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row(mainAxisSize: .min, children: children), + ); + } +} + /// A focusable tab chip that shows a color change when focused or selected. /// /// Used for tab navigation in LibrariesScreen. Handles: