import 'package:flutter/material.dart'; import '../focus/focusable_chip_mixin.dart'; 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: /// - SELECT key to activate the tab /// - LEFT/RIGHT arrows to switch between tabs /// - DOWN arrow to navigate to tab content /// - BACK key to navigate to sidenav class FocusableTabChip extends StatefulWidget { final String label; final bool isSelected; final VoidCallback onSelect; /// Optional external focus node for programmatic focus control. final FocusNode? focusNode; /// Called when the user presses LEFT from this chip. /// Should switch to the previous tab. final VoidCallback? onNavigateLeft; /// Called when the user presses RIGHT from this chip. /// Should switch to the next tab. final VoidCallback? onNavigateRight; /// Called when the user presses DOWN from this chip. final VoidCallback? onNavigateDown; /// Called when the user presses UP from this chip. final VoidCallback? onNavigateUp; /// Called when the user presses BACK from this chip. final VoidCallback? onBack; /// Called when SELECT key is held (D-pad long press). final VoidCallback? onLongPress; /// Optional image to show above the label (e.g. a poster). /// When provided, the chip lays out vertically with image on top, label below. final Widget? topImage; const FocusableTabChip({ super.key, required this.label, required this.isSelected, required this.onSelect, this.focusNode, this.onNavigateLeft, this.onNavigateRight, this.onNavigateDown, this.onNavigateUp, this.onBack, this.onLongPress, this.topImage, }); @override State createState() => _FocusableTabChipState(); } class _FocusableTabChipState extends State with FocusableChipStateMixin { @override FocusNode? get widgetFocusNode => widget.focusNode; @override String get debugLabel => 'tab_chip_${widget.label}'; @override void initState() { super.initState(); initFocusNode(); } @override void didUpdateWidget(FocusableTabChip oldWidget) { super.didUpdateWidget(oldWidget); updateFocusNode(oldWidget.focusNode); } @override void dispose() { disposeFocusNode(); super.dispose(); } KeyEventResult _handleKeyEvent(FocusNode node, KeyEvent event) { return handleChipKeyEvent( node, event, ChipKeyCallbacks( onSelect: widget.onSelect, onLongPress: widget.onLongPress, onNavigateLeft: widget.onNavigateLeft, onNavigateRight: widget.onNavigateRight, onNavigateDown: widget.onNavigateDown, onNavigateUp: widget.onNavigateUp, onBack: widget.onBack, ), ); } @override Widget build(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; // Only show focus effects during keyboard/d-pad navigation final showFocus = isFocused && InputModeTracker.isKeyboardMode(context); // Determine background color based on focus and selection state // - Selected + Focused: slightly dimmed primary (to show focus distinction) // - Selected only: primary color // - Focused only: primary color // - Neither: surface color Color backgroundColor; Color foregroundColor; if (widget.isSelected && showFocus) { // Selected + focused: dim the primary color slightly backgroundColor = Color.lerp(colorScheme.primary, colorScheme.surface, 0.25)!; foregroundColor = colorScheme.onPrimary; } else if (widget.isSelected || showFocus) { // Selected or focused (but not both): full primary backgroundColor = colorScheme.primary; foregroundColor = colorScheme.onPrimary; } else { // Neither selected nor focused if (PlatformDetector.isTV()) { backgroundColor = colorScheme.secondaryContainer.withValues(alpha: 0.38); foregroundColor = colorScheme.onSecondaryContainer; } else { backgroundColor = colorScheme.surfaceContainerHighest; foregroundColor = colorScheme.onSurfaceVariant; } } final isHighlighted = showFocus || widget.isSelected; final label = Text( widget.label, style: Theme.of(context).textTheme.labelLarge?.copyWith( color: foregroundColor, fontWeight: isHighlighted ? FontWeight.w600 : FontWeight.normal, ), ); final hasImage = widget.topImage != null; return FocusBuilders.buildFocusableChip( context: context, focusNode: focusNode, onKeyEvent: _handleKeyEvent, onTap: widget.onSelect, semanticLabel: widget.label, selected: widget.isSelected, padding: hasImage ? const EdgeInsets.all(8) : const EdgeInsets.symmetric(horizontal: 16, vertical: 8), backgroundColor: backgroundColor, borderRadius: hasImage ? 12 : 20, child: hasImage ? Column( mainAxisSize: .min, children: [ ClipRRect(borderRadius: BorderRadius.circular(6), child: widget.topImage!), const SizedBox(height: 6), label, ], ) : label, ); } }