import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'circle_nav_icon_button.dart'; typedef HoverScrollableRowBuilder = Widget Function(BuildContext context, ScrollController controller); class _MouseDragScrollBehavior extends MaterialScrollBehavior { const _MouseDragScrollBehavior(); @override Set get dragDevices => const { PointerDeviceKind.touch, PointerDeviceKind.mouse, PointerDeviceKind.trackpad, PointerDeviceKind.stylus, }; } class HoverScrollableRow extends StatefulWidget { final HoverScrollableRowBuilder builder; final ScrollController? controller; final double scrollFraction; final double buttonInset; final double iconSize; const HoverScrollableRow({ super.key, required this.builder, this.controller, this.scrollFraction = 0.6, this.buttonInset = 10, this.iconSize = 26, }); @override State createState() => _HoverScrollableRowState(); } class _HoverScrollableRowState extends State { late ScrollController _controller; bool _ownsController = false; bool _hovered = false; bool _canScrollLeft = false; bool _canScrollRight = false; @override void initState() { super.initState(); _attachController(widget.controller); WidgetsBinding.instance.addPostFrameCallback((_) => _updateScrollState()); } @override void didUpdateWidget(covariant HoverScrollableRow oldWidget) { super.didUpdateWidget(oldWidget); if (oldWidget.controller != widget.controller) { _detachController(); _attachController(widget.controller); WidgetsBinding.instance.addPostFrameCallback((_) => _updateScrollState()); } } void _attachController(ScrollController? external) { if (external != null) { _controller = external; _ownsController = false; } else { _controller = ScrollController(); _ownsController = true; } _controller.addListener(_updateScrollState); } void _detachController() { _controller.removeListener(_updateScrollState); if (_ownsController) { _controller.dispose(); } } @override void dispose() { _detachController(); super.dispose(); } void _updateScrollState() { if (!mounted || !_controller.hasClients) return; final maxScroll = _controller.position.maxScrollExtent; final offset = _controller.offset; final canLeft = offset > 1; final canRight = maxScroll - offset > 1; if (canLeft != _canScrollLeft || canRight != _canScrollRight) { setState(() { _canScrollLeft = canLeft; _canScrollRight = canRight; }); } } void _scrollByPage({required bool left}) { if (!_controller.hasClients) return; final viewport = _controller.position.viewportDimension; final delta = viewport * widget.scrollFraction; final target = (_controller.offset + (left ? -delta : delta)).clamp( 0.0, _controller.position.maxScrollExtent, ); _controller.animateTo( target, duration: const Duration(milliseconds: 320), curve: Curves.easeOutCubic, ); } @override Widget build(BuildContext context) { final hasOverflow = _canScrollLeft || _canScrollRight; final showPager = _hovered && hasOverflow; return MouseRegion( onEnter: (_) => setState(() => _hovered = true), onExit: (_) => setState(() => _hovered = false), child: NotificationListener( onNotification: (_) { WidgetsBinding.instance.addPostFrameCallback( (_) => _updateScrollState(), ); return false; }, child: Stack( children: [ ScrollConfiguration( behavior: const _MouseDragScrollBehavior(), child: widget.builder(context, _controller), ), if (hasOverflow) Positioned( left: widget.buttonInset, top: 0, bottom: 0, child: Center( child: AnimatedOpacity( opacity: showPager ? 1 : 0, duration: const Duration(milliseconds: 140), child: IgnorePointer( ignoring: !showPager || !_canScrollLeft, child: CircleNavIconButton( icon: Icons.chevron_left_rounded, iconSize: widget.iconSize, enabled: _canScrollLeft, onPressed: _canScrollLeft ? () => _scrollByPage(left: true) : null, ), ), ), ), ), if (hasOverflow) Positioned( right: widget.buttonInset, top: 0, bottom: 0, child: Center( child: AnimatedOpacity( opacity: showPager ? 1 : 0, duration: const Duration(milliseconds: 140), child: IgnorePointer( ignoring: !showPager || !_canScrollRight, child: CircleNavIconButton( icon: Icons.chevron_right_rounded, iconSize: widget.iconSize, enabled: _canScrollRight, onPressed: _canScrollRight ? () => _scrollByPage(left: false) : null, ), ), ), ), ), ], ), ), ); } }