From c9870bb044b18b4aeacf8e6ae798eb1296264127 Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Thu, 23 Apr 2026 11:12:04 +0200 Subject: [PATCH] extract ClideTappable shared widget Encapsulates the hover + click + cursor pattern repeated across the codebase. Builder receives (context, hovered) so callers control their own hover styling. Refactored _ThemeLink to use it. Co-Authored-By: Claude Opus 4.6 (1M context) --- lib/builtin/welcome/src/welcome_view.dart | 30 ++++++------------- lib/widgets/src/clide_tappable.dart | 35 +++++++++++++++++++++++ lib/widgets/widgets.dart | 1 + 3 files changed, 45 insertions(+), 21 deletions(-) create mode 100644 lib/widgets/src/clide_tappable.dart diff --git a/lib/builtin/welcome/src/welcome_view.dart b/lib/builtin/welcome/src/welcome_view.dart index 7706311e..73f1c145 100644 --- a/lib/builtin/welcome/src/welcome_view.dart +++ b/lib/builtin/welcome/src/welcome_view.dart @@ -297,34 +297,22 @@ class _StatusLine extends StatelessWidget { } } -class _ThemeLink extends StatefulWidget { +class _ThemeLink extends StatelessWidget { const _ThemeLink({required this.tokens, required this.kernel, required this.themeName}); final SurfaceTokens tokens; final KernelServices kernel; final String themeName; - @override - State<_ThemeLink> createState() => _ThemeLinkState(); -} - -class _ThemeLinkState extends State<_ThemeLink> { - bool _hover = false; - @override Widget build(BuildContext context) { - return MouseRegion( - cursor: SystemMouseCursors.click, - onEnter: (_) => setState(() => _hover = true), - onExit: (_) => setState(() => _hover = false), - child: GestureDetector( - onTap: () => widget.kernel.commands.execute('theme.pick'), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - ClideText('theme: ', muted: true, fontSize: 12, fontFamily: clideMonoFamily), - ClideText(widget.themeName, fontSize: 12, fontFamily: clideMonoFamily, color: _hover ? widget.tokens.globalForeground : widget.tokens.globalFocus), - ], - ), + return ClideTappable( + onTap: () => kernel.commands.execute('theme.pick'), + builder: (ctx, hovered) => Row( + mainAxisSize: MainAxisSize.min, + children: [ + ClideText('theme: ', muted: true, fontSize: 12, fontFamily: clideMonoFamily), + ClideText(themeName, fontSize: 12, fontFamily: clideMonoFamily, color: hovered ? tokens.globalForeground : tokens.globalFocus), + ], ), ); } diff --git a/lib/widgets/src/clide_tappable.dart b/lib/widgets/src/clide_tappable.dart new file mode 100644 index 00000000..9459ecae --- /dev/null +++ b/lib/widgets/src/clide_tappable.dart @@ -0,0 +1,35 @@ +import 'package:flutter/widgets.dart'; + +class ClideTappable extends StatefulWidget { + const ClideTappable({ + super.key, + required this.onTap, + required this.builder, + this.cursor = SystemMouseCursors.click, + }); + + final VoidCallback onTap; + final Widget Function(BuildContext context, bool hovered) builder; + final MouseCursor cursor; + + @override + State createState() => _ClideTappableState(); +} + +class _ClideTappableState extends State { + bool _hover = false; + + @override + Widget build(BuildContext context) { + return MouseRegion( + cursor: widget.cursor, + onEnter: (_) => setState(() => _hover = true), + onExit: (_) => setState(() => _hover = false), + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: widget.onTap, + child: widget.builder(context, _hover), + ), + ); + } +} diff --git a/lib/widgets/widgets.dart b/lib/widgets/widgets.dart index bc888657..0c2566a1 100644 --- a/lib/widgets/widgets.dart +++ b/lib/widgets/widgets.dart @@ -19,6 +19,7 @@ export 'src/clide_scrollbar.dart'; export 'src/clide_spine.dart'; export 'src/clide_surface.dart'; export 'src/clide_tab_bar.dart'; +export 'src/clide_tappable.dart'; export 'src/clide_text.dart'; export 'src/clide_tooltip.dart'; export 'src/typography.dart';