import 'package:flutter/material.dart'; import 'theme.dart'; /// Marks the edges of a horizontal scrollable that have more content beyond /// them, so it is obvious the row scrolls. /// /// Wrap directly around a horizontal [SingleChildScrollView] or [ListView]. /// Each overflowing edge gets a soft scrim in the surface colour — the chips /// appear to slide *under* it — topped by a chevron pointing outward. The /// scrim only shows while content remains beyond that edge; it disappears once /// the row reaches that end (or does not overflow at all). Edge state is /// tracked per logical side (start/end) and mapped to physical left/right via /// [Directionality], so RTL mirrors correctly. class EdgeFade extends StatefulWidget { const EdgeFade({super.key, required this.child, this.scrimWidth = 40}); /// The horizontal scrollable to mark. final Widget child; /// Width of each edge scrim in logical pixels. final double scrimWidth; @override State createState() => EdgeFadeState(); } class EdgeFadeState extends State { bool _fadeStart = false; bool _fadeEnd = false; /// Whether the physical left edge currently shows a scroll cue. @visibleForTesting bool get fadeLeft => _isRtl ? _fadeEnd : _fadeStart; /// Whether the physical right edge currently shows a scroll cue. @visibleForTesting bool get fadeRight => _isRtl ? _fadeStart : _fadeEnd; bool get _isRtl => Directionality.of(context) == TextDirection.rtl; bool _onMetrics(ScrollMetrics metrics) { if (metrics.axis != Axis.horizontal) return false; final start = metrics.extentBefore > 0.5; final end = metrics.extentAfter > 0.5; if (start != _fadeStart || end != _fadeEnd) { setState(() { _fadeStart = start; _fadeEnd = end; }); } return false; } @override Widget build(BuildContext context) { final surface = Theme.of(context).scaffoldBackgroundColor; return NotificationListener( onNotification: (n) => n.depth == 0 ? _onMetrics(n.metrics) : false, child: NotificationListener( onNotification: (n) => n.depth == 0 ? _onMetrics(n.metrics) : false, child: Stack( children: [ widget.child, if (fadeLeft) _EdgeCue( left: true, width: widget.scrimWidth, surface: surface, ), if (fadeRight) _EdgeCue( left: false, width: widget.scrimWidth, surface: surface, ), ], ), ), ); } } /// A gradient scrim over one physical edge of the row, opaque at the very edge /// and fading inward, with a chevron pointing outward toward the hidden chips. /// Ignores pointers so taps still reach the chip underneath. class _EdgeCue extends StatelessWidget { const _EdgeCue({ required this.left, required this.width, required this.surface, }); final bool left; final double width; final Color surface; @override Widget build(BuildContext context) { return Positioned( top: 0, bottom: 0, left: left ? 0 : null, right: left ? null : 0, width: width, child: IgnorePointer( child: Container( alignment: left ? Alignment.centerLeft : Alignment.centerRight, decoration: BoxDecoration( gradient: LinearGradient( begin: left ? Alignment.centerLeft : Alignment.centerRight, end: left ? Alignment.centerRight : Alignment.centerLeft, colors: [surface, surface.withValues(alpha: 0)], ), ), child: Icon( left ? Icons.chevron_left : Icons.chevron_right, size: 22, color: seedMuted, ), ), ), ); } }