From cf3e7700a3e326c37bcbc78a7388a4a88a198e41 Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Thu, 23 Apr 2026 11:28:35 +0200 Subject: [PATCH] sweep ClideTappable across 15 files, remove 267 lines 23 StatefulWidget+State hover pairs converted to StatelessWidget using ClideTappable(builder: (ctx, hovered, _) => ...). Covers app.dart (5), welcome (2), claude session host (2), column hat (2), spine (1), pane chrome (1), tab bar (1), decisions (1), tickets (1), graph (1), pql panel (1), backlinks (1), file tree (2), git panel (1), git status (1). Only EditorDragHandle and ClidePalette excluded (drag/keyboard behavior beyond hover+tap). Co-Authored-By: Claude Opus 4.6 (1M context) --- lib/app.dart | 201 +++++++----------- .../claude/src/claude_session_host.dart | 85 +++----- lib/builtin/decisions/src/decisions_view.dart | 21 +- lib/builtin/files/src/file_tree_view.dart | 95 +++------ lib/builtin/git/src/git_panel_view.dart | 105 ++++----- lib/builtin/git/src/git_status_item.dart | 70 +++--- lib/builtin/graph/src/graph_view.dart | 21 +- lib/builtin/pql/src/backlinks_view.dart | 49 ++--- lib/builtin/pql/src/pql_panel_view.dart | 43 ++-- lib/builtin/tickets/src/tickets_view.dart | 31 +-- lib/builtin/welcome/src/welcome_view.dart | 120 +++++------ lib/widgets/src/clide_column_hat.dart | 63 ++---- lib/widgets/src/clide_pane_chrome.dart | 45 ++-- lib/widgets/src/clide_spine.dart | 101 ++++----- lib/widgets/src/clide_tab_bar.dart | 49 ++--- 15 files changed, 416 insertions(+), 683 deletions(-) diff --git a/lib/app.dart b/lib/app.dart index f64ac4af..9efcb881 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -290,105 +290,72 @@ class _RightHatContent extends StatelessWidget { } } -class _TrafficDot extends StatefulWidget { +class _TrafficDot extends StatelessWidget { const _TrafficDot({required this.color, required this.onTap}); final Color color; final VoidCallback onTap; - @override - State<_TrafficDot> createState() => _TrafficDotState(); -} -class _TrafficDotState extends State<_TrafficDot> { - bool _hover = false; @override Widget build(BuildContext context) { - return MouseRegion( - onEnter: (_) => setState(() => _hover = true), - onExit: (_) => setState(() => _hover = false), - child: GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: widget.onTap, - child: Container( - width: 12, height: 12, - decoration: BoxDecoration(color: _hover ? widget.color : widget.color.withAlpha(0xCC), shape: BoxShape.circle), - ), + return ClideTappable( + onTap: onTap, + builder: (context, hovered, _) => Container( + width: 12, height: 12, + decoration: BoxDecoration(color: hovered ? color : color.withAlpha(0xCC), shape: BoxShape.circle), ), ); } } -class _WinBtn extends StatefulWidget { +class _WinBtn extends StatelessWidget { const _WinBtn({required this.icon, required this.onTap, required this.tokens, this.isClose = false}); final ClideIconPainter icon; final VoidCallback onTap; final SurfaceTokens tokens; final bool isClose; - @override - State<_WinBtn> createState() => _WinBtnState(); -} -class _WinBtnState extends State<_WinBtn> { - bool _hover = false; @override Widget build(BuildContext context) { - final hoverBg = widget.isClose ? const Color(0xFFE81123) : widget.tokens.listItemHoverBackground; - return MouseRegion( - onEnter: (_) => setState(() => _hover = true), - onExit: (_) => setState(() => _hover = false), - child: GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: widget.onTap, - child: Container( - width: 36, height: hatHeight, - color: _hover ? hoverBg : null, - alignment: Alignment.center, - child: ClideIcon(widget.icon, size: 14, color: _hover && widget.isClose ? const Color(0xFFFFFFFF) : widget.tokens.chromeForeground), - ), + final hoverBg = isClose ? const Color(0xFFE81123) : tokens.listItemHoverBackground; + return ClideTappable( + onTap: onTap, + builder: (context, hovered, _) => Container( + width: 36, height: hatHeight, + color: hovered ? hoverBg : null, + alignment: Alignment.center, + child: ClideIcon(icon, size: 14, color: hovered && isClose ? const Color(0xFFFFFFFF) : tokens.chromeForeground), ), ); } } -class _ProjectSwitcherButton extends StatefulWidget { +class _ProjectSwitcherButton extends StatelessWidget { const _ProjectSwitcherButton({required this.kernel, required this.tokens}); final KernelServices kernel; final SurfaceTokens tokens; - @override - State<_ProjectSwitcherButton> createState() => _ProjectSwitcherButtonState(); -} - -class _ProjectSwitcherButtonState extends State<_ProjectSwitcherButton> { - bool _hover = false; - void _openSwitcher() { - widget.kernel.dialog.show((ctx, dismiss) { - return _ProjectSwitcherDropdown(kernel: widget.kernel, onDismiss: dismiss); + kernel.dialog.show((ctx, dismiss) { + return _ProjectSwitcherDropdown(kernel: kernel, onDismiss: dismiss); }); } @override Widget build(BuildContext context) { return ListenableBuilder( - listenable: widget.kernel.project, + listenable: kernel.project, builder: (ctx, _) { - final name = widget.kernel.project.current?.path.split('/').last; + final name = kernel.project.current?.path.split('/').last; final label = name != null ? 'clide > $name' : 'clide'; - return MouseRegion( - cursor: SystemMouseCursors.click, - onEnter: (_) => setState(() => _hover = true), - onExit: (_) => setState(() => _hover = false), - child: GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: _openSwitcher, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - ClideText(label, fontSize: 12, color: _hover ? widget.tokens.globalForeground : widget.tokens.chromeForeground, fontFamily: clideMonoFamily), - const SizedBox(width: 4), - ClideIcon(PhosphorIcons.caretDown, size: 8, color: widget.tokens.chromeForeground), - ], - ), + return ClideTappable( + onTap: _openSwitcher, + builder: (context, hovered, _) => Row( + mainAxisSize: MainAxisSize.min, + children: [ + ClideText(label, fontSize: 12, color: hovered ? tokens.globalForeground : tokens.chromeForeground, fontFamily: clideMonoFamily), + const SizedBox(width: 4), + ClideIcon(PhosphorIcons.caretDown, size: 8, color: tokens.chromeForeground), + ], ), ); }, @@ -523,95 +490,71 @@ class _ProjectSwitcherDropdownState extends State<_ProjectSwitcherDropdown> { } } -class _RecentProjectRow extends StatefulWidget { +class _RecentProjectRow extends StatelessWidget { const _RecentProjectRow({required this.project, required this.tokens, required this.onTap}); final RecentProject project; final SurfaceTokens tokens; final VoidCallback onTap; - @override - State<_RecentProjectRow> createState() => _RecentProjectRowState(); -} - -class _RecentProjectRowState extends State<_RecentProjectRow> { - 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.onTap, - child: Container( - color: _hover ? widget.tokens.listItemHoverBackground : null, - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), - child: Row( - children: [ - ClideIcon(PhosphorIcons.folder, size: 14, color: widget.tokens.globalTextMuted), - const SizedBox(width: 8), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - ClideText(widget.project.name, fontSize: 14), - if (widget.project.branch != null) - Row( - children: [ - ClideText(widget.project.relativePath, muted: true, fontSize: 12, fontFamily: clideMonoFamily), - ClideText(' · ', muted: true, fontSize: 12), - ClideIcon(PhosphorIcons.gitBranch, size: 10, color: widget.tokens.globalTextMuted), - const SizedBox(width: 3), - ClideText(widget.project.branch!, muted: true, fontSize: 12, fontFamily: clideMonoFamily), - ], - ) - else - ClideText(widget.project.relativePath, muted: true, fontSize: 12, fontFamily: clideMonoFamily), - ], - ), + return ClideTappable( + onTap: onTap, + builder: (context, hovered, _) => Container( + color: hovered ? tokens.listItemHoverBackground : null, + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + child: Row( + children: [ + ClideIcon(PhosphorIcons.folder, size: 14, color: tokens.globalTextMuted), + const SizedBox(width: 8), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ClideText(project.name, fontSize: 14), + if (project.branch != null) + Row( + children: [ + ClideText(project.relativePath, muted: true, fontSize: 12, fontFamily: clideMonoFamily), + ClideText(' · ', muted: true, fontSize: 12), + ClideIcon(PhosphorIcons.gitBranch, size: 10, color: tokens.globalTextMuted), + const SizedBox(width: 3), + ClideText(project.branch!, muted: true, fontSize: 12, fontFamily: clideMonoFamily), + ], + ) + else + ClideText(project.relativePath, muted: true, fontSize: 12, fontFamily: clideMonoFamily), + ], ), - ClideText(widget.project.timeAgo, muted: true, fontSize: 11), - ], - ), + ), + ClideText(project.timeAgo, muted: true, fontSize: 11), + ], ), ), ); } } -class _ActionRow extends StatefulWidget { +class _ActionRow extends StatelessWidget { const _ActionRow({required this.label, this.shortcut, required this.tokens, required this.onTap}); final String label; final String? shortcut; final SurfaceTokens tokens; final VoidCallback onTap; - @override - State<_ActionRow> createState() => _ActionRowState(); -} - -class _ActionRowState extends State<_ActionRow> { - 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.onTap, - child: Container( - color: _hover ? widget.tokens.listItemHoverBackground : null, - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), - child: Row( - children: [ - Expanded(child: ClideText(widget.label, fontSize: 14)), - if (widget.shortcut != null && widget.shortcut!.isNotEmpty) - ClideText(widget.shortcut!, fontSize: 12, color: widget.tokens.globalTextMuted, fontFamily: clideMonoFamily), - ], - ), + return ClideTappable( + onTap: onTap, + builder: (context, hovered, _) => Container( + color: hovered ? tokens.listItemHoverBackground : null, + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + child: Row( + children: [ + Expanded(child: ClideText(label, fontSize: 14)), + if (shortcut != null && shortcut!.isNotEmpty) + ClideText(shortcut!, fontSize: 12, color: tokens.globalTextMuted, fontFamily: clideMonoFamily), + ], ), ), ); diff --git a/lib/builtin/claude/src/claude_session_host.dart b/lib/builtin/claude/src/claude_session_host.dart index 1ed8bc82..81d915e3 100644 --- a/lib/builtin/claude/src/claude_session_host.dart +++ b/lib/builtin/claude/src/claude_session_host.dart @@ -120,7 +120,7 @@ class _TabRow extends StatelessWidget { } } -class _Tab extends StatefulWidget { +class _Tab extends StatelessWidget { const _Tab({required this.session, required this.active, required this.tokens, required this.onTap, this.onClose}); final _Session session; final bool active; @@ -128,75 +128,52 @@ class _Tab extends StatefulWidget { final VoidCallback onTap; final VoidCallback? onClose; - @override - State<_Tab> createState() => _TabState(); -} - -class _TabState extends State<_Tab> { - bool _hovered = false; - @override Widget build(BuildContext context) { - return MouseRegion( - cursor: SystemMouseCursors.click, - onEnter: (_) => setState(() => _hovered = true), - onExit: (_) => setState(() => _hovered = false), - child: GestureDetector( - onTap: widget.onTap, - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 10), - decoration: BoxDecoration( - color: _hovered && !widget.active ? widget.tokens.tabInactive : null, - border: Border(bottom: BorderSide(color: widget.active ? widget.tokens.tabActiveBorder : const Color(0x00000000), width: 2)), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - ClideText( - widget.session.label, - fontSize: 12, - color: widget.active ? widget.tokens.tabActiveForeground : widget.tokens.tabInactiveForeground, - fontFamily: clideMonoFamily, + return ClideTappable( + onTap: onTap, + builder: (context, hovered, _) => Container( + padding: const EdgeInsets.symmetric(horizontal: 10), + decoration: BoxDecoration( + color: hovered && !active ? tokens.tabInactive : null, + border: Border(bottom: BorderSide(color: active ? tokens.tabActiveBorder : const Color(0x00000000), width: 2)), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + ClideText( + session.label, + fontSize: 12, + color: active ? tokens.tabActiveForeground : tokens.tabInactiveForeground, + fontFamily: clideMonoFamily, + ), + if (onClose != null) ...[ + const SizedBox(width: 6), + GestureDetector( + onTap: onClose, + child: ClideIcon(PhosphorIcons.xMark, size: 10, color: hovered ? tokens.globalForeground : tokens.globalTextMuted), ), - if (widget.onClose != null) ...[ - const SizedBox(width: 6), - GestureDetector( - onTap: widget.onClose, - child: ClideIcon(PhosphorIcons.xMark, size: 10, color: _hovered ? widget.tokens.globalForeground : widget.tokens.globalTextMuted), - ), - ], ], - ), + ], ), ), ); } } -class _AddButton extends StatefulWidget { +class _AddButton extends StatelessWidget { const _AddButton({required this.tokens, required this.onTap}); final SurfaceTokens tokens; final VoidCallback onTap; - @override - State<_AddButton> createState() => _AddButtonState(); -} - -class _AddButtonState extends State<_AddButton> { - bool _hovered = false; - @override Widget build(BuildContext context) { - return MouseRegion( - cursor: SystemMouseCursors.click, - onEnter: (_) => setState(() => _hovered = true), - onExit: (_) => setState(() => _hovered = false), - child: GestureDetector( - onTap: widget.onTap, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4), - child: ClideText('+', fontSize: 14, color: _hovered ? widget.tokens.globalForeground : widget.tokens.globalTextMuted), - ), + return ClideTappable( + onTap: onTap, + tooltip: 'New session', + builder: (context, hovered, _) => Padding( + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4), + child: ClideText('+', fontSize: 14, color: hovered ? tokens.globalForeground : tokens.globalTextMuted), ), ); } diff --git a/lib/builtin/decisions/src/decisions_view.dart b/lib/builtin/decisions/src/decisions_view.dart index 1d6c009d..b32549fb 100644 --- a/lib/builtin/decisions/src/decisions_view.dart +++ b/lib/builtin/decisions/src/decisions_view.dart @@ -102,31 +102,22 @@ class _DecisionEntry { ); } -class _DecisionRow extends StatefulWidget { +class _DecisionRow extends StatelessWidget { const _DecisionRow({required this.entry, required this.tokens}); final _DecisionEntry entry; final SurfaceTokens tokens; - @override - State<_DecisionRow> createState() => _DecisionRowState(); -} - -class _DecisionRowState extends State<_DecisionRow> { - bool _hovered = false; - @override Widget build(BuildContext context) { - return MouseRegion( - onEnter: (_) => setState(() => _hovered = true), - onExit: (_) => setState(() => _hovered = false), - child: Container( - color: _hovered ? widget.tokens.listItemHoverBackground : null, + return ClideTappable( + builder: (context, hovered, _) => Container( + color: hovered ? tokens.listItemHoverBackground : null, padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), child: Row( children: [ - ClideText(widget.entry.id, color: widget.tokens.globalTextMuted, fontSize: 12), + ClideText(entry.id, color: tokens.globalTextMuted, fontSize: 12), const SizedBox(width: 8), - Expanded(child: ClideText(widget.entry.title, fontSize: 13)), + Expanded(child: ClideText(entry.title, fontSize: 13)), ], ), ), diff --git a/lib/builtin/files/src/file_tree_view.dart b/lib/builtin/files/src/file_tree_view.dart index 0ca5ed5b..53601fbd 100644 --- a/lib/builtin/files/src/file_tree_view.dart +++ b/lib/builtin/files/src/file_tree_view.dart @@ -225,7 +225,7 @@ class _FileRow extends StatelessWidget { } } -class _Row extends StatefulWidget { +class _Row extends StatelessWidget { const _Row({ required this.depth, required this.onTap, @@ -240,81 +240,56 @@ class _Row extends StatefulWidget { final Widget? leading; final bool rotateLeading; - @override - State<_Row> createState() => _RowState(); -} - -class _RowState extends State<_Row> { - bool _hover = false; @override Widget build(BuildContext context) { final tokens = ClideTheme.of(context).surface; - final leftPadding = 8.0 + (widget.depth * 14.0); - return MouseRegion( - cursor: SystemMouseCursors.click, - onEnter: (_) => setState(() => _hover = true), - onExit: (_) => setState(() => _hover = false), - child: GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: widget.onTap, - child: Container( - color: _hover ? tokens.sidebarItemHover : null, - padding: EdgeInsets.only(left: leftPadding, right: 8, top: 3, bottom: 3), - child: Row( - children: [ - if (widget.leading != null) ...[ - Transform.rotate( - angle: widget.rotateLeading ? 1.5708 : 0, // 90° when expanded - child: widget.leading, - ), - const SizedBox(width: 6), - ] else - const SizedBox(width: 16), - Expanded( - child: ClideText( - widget.label, - maxLines: 1, - overflow: TextOverflow.ellipsis, - color: tokens.sidebarForeground, - ), + final leftPadding = 8.0 + (depth * 14.0); + return ClideTappable( + onTap: onTap, + builder: (context, hovered, _) => Container( + color: hovered ? tokens.sidebarItemHover : null, + padding: EdgeInsets.only(left: leftPadding, right: 8, top: 3, bottom: 3), + child: Row( + children: [ + if (leading != null) ...[ + Transform.rotate( + angle: rotateLeading ? 1.5708 : 0, // 90° when expanded + child: leading, ), - ], - ), + const SizedBox(width: 6), + ] else + const SizedBox(width: 16), + Expanded( + child: ClideText( + label, + maxLines: 1, + overflow: TextOverflow.ellipsis, + color: tokens.sidebarForeground, + ), + ), + ], ), ), ); } } -class _FilteredFileRow extends StatefulWidget { +class _FilteredFileRow extends StatelessWidget { const _FilteredFileRow({required this.entry}); final FileEntry entry; - @override - State<_FilteredFileRow> createState() => _FilteredFileRowState(); -} - -class _FilteredFileRowState extends State<_FilteredFileRow> { - bool _hover = false; - @override Widget build(BuildContext context) { final tokens = ClideTheme.of(context).surface; - return MouseRegion( - cursor: SystemMouseCursors.click, - onEnter: (_) => setState(() => _hover = true), - onExit: (_) => setState(() => _hover = false), - child: GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () { - final kernel = ClideKernel.of(context); - unawaited(kernel.ipc.request('editor.open', args: {'path': widget.entry.path})); - }, - child: Container( - color: _hover ? tokens.sidebarItemHover : null, - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 3), - child: ClideText(widget.entry.path, maxLines: 1, overflow: TextOverflow.ellipsis, color: tokens.sidebarForeground), - ), + return ClideTappable( + onTap: () { + final kernel = ClideKernel.of(context); + unawaited(kernel.ipc.request('editor.open', args: {'path': entry.path})); + }, + builder: (context, hovered, _) => Container( + color: hovered ? tokens.sidebarItemHover : null, + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 3), + child: ClideText(entry.path, maxLines: 1, overflow: TextOverflow.ellipsis, color: tokens.sidebarForeground), ), ); } diff --git a/lib/builtin/git/src/git_panel_view.dart b/lib/builtin/git/src/git_panel_view.dart index 1be686d2..ca869742 100644 --- a/lib/builtin/git/src/git_panel_view.dart +++ b/lib/builtin/git/src/git_panel_view.dart @@ -336,7 +336,7 @@ class _FileGroup extends StatelessWidget { } } -class _GitFileRow extends StatefulWidget { +class _GitFileRow extends StatelessWidget { const _GitFileRow({ required this.entry, this.onStage, @@ -349,78 +349,65 @@ class _GitFileRow extends StatefulWidget { final void Function(String path)? onUnstage; final void Function(String path)? onDiscard; - @override - State<_GitFileRow> createState() => _GitFileRowState(); -} - -class _GitFileRowState extends State<_GitFileRow> { - bool _hover = false; - @override Widget build(BuildContext context) { final tokens = ClideTheme.of(context).surface; - final path = widget.entry['path'] as String? ?? ''; + final path = entry['path'] as String? ?? ''; final name = path.split('/').last; - final indexState = widget.entry['indexState'] as String?; - final workTreeState = widget.entry['workTreeState'] as String?; + final indexState = entry['indexState'] as String?; + final workTreeState = entry['workTreeState'] as String?; final state = indexState ?? workTreeState ?? ''; final stateLabel = _stateLabel(state); - return MouseRegion( - cursor: SystemMouseCursors.click, - onEnter: (_) => setState(() => _hover = true), - onExit: (_) => setState(() => _hover = false), - child: GestureDetector( - behavior: HitTestBehavior.opaque, + return Semantics( + button: true, + label: '$name $stateLabel', + child: ClideTappable( onTap: () { final kernel = ClideKernel.of(context); unawaited(kernel.ipc.request('editor.open', args: {'path': path})); }, - child: Semantics( - button: true, - label: '$name $stateLabel', - child: Container( - color: _hover ? tokens.sidebarItemHover : null, - padding: const EdgeInsets.only( - left: 20, right: 8, top: 2, bottom: 2), - child: Row( - children: [ - ClideText( - _stateIndicator(state), - fontSize: clideFontCaption, - color: _stateColor(state, tokens), + builder: (context, hovered, _) => Container( + color: hovered ? tokens.sidebarItemHover : null, + padding: const EdgeInsets.only( + left: 20, right: 8, top: 2, bottom: 2), + child: Row( + children: [ + ClideText( + _stateIndicator(state), + fontSize: clideFontCaption, + color: _stateColor(state, tokens), + ), + const SizedBox(width: 6), + Expanded( + child: ClideText( + name, + maxLines: 1, + overflow: TextOverflow.ellipsis, + color: tokens.sidebarForeground, ), - const SizedBox(width: 6), - Expanded( - child: ClideText( - name, - maxLines: 1, - overflow: TextOverflow.ellipsis, - color: tokens.sidebarForeground, + ), + if (hovered) ...[ + if (onStage != null) + _SmallAction( + label: '+', + semanticsLabel: 'stage $name', + onTap: () => onStage!(path), + ), + if (onUnstage != null) + _SmallAction( + label: '-', + semanticsLabel: 'unstage $name', + onTap: () => onUnstage!(path), + ), + if (onDiscard != null) + _SmallAction( + label: 'x', + semanticsLabel: 'discard changes to $name', + onTap: () => onDiscard!(path), ), - ), - if (_hover) ...[ - if (widget.onStage != null) - _SmallAction( - label: '+', - semanticsLabel: 'stage $name', - onTap: () => widget.onStage!(path), - ), - if (widget.onUnstage != null) - _SmallAction( - label: '-', - semanticsLabel: 'unstage $name', - onTap: () => widget.onUnstage!(path), - ), - if (widget.onDiscard != null) - _SmallAction( - label: 'x', - semanticsLabel: 'discard changes to $name', - onTap: () => widget.onDiscard!(path), - ), - ], ], - ), + ], ), ), ), diff --git a/lib/builtin/git/src/git_status_item.dart b/lib/builtin/git/src/git_status_item.dart index 939f452d..45cc3687 100644 --- a/lib/builtin/git/src/git_status_item.dart +++ b/lib/builtin/git/src/git_status_item.dart @@ -228,7 +228,7 @@ class _BranchPickerState extends State<_BranchPicker> { } } -class _BranchRow extends StatefulWidget { +class _BranchRow extends StatelessWidget { const _BranchRow({ required this.name, required this.current, @@ -239,51 +239,39 @@ class _BranchRow extends StatefulWidget { final bool current; final VoidCallback? onTap; - @override - State<_BranchRow> createState() => _BranchRowState(); -} - -class _BranchRowState extends State<_BranchRow> { - bool _hover = false; - @override Widget build(BuildContext context) { final tokens = ClideTheme.of(context).surface; - return MouseRegion( - cursor: - widget.onTap != null ? SystemMouseCursors.click : MouseCursor.defer, - onEnter: (_) => setState(() => _hover = true), - onExit: (_) => setState(() => _hover = false), - child: GestureDetector( - onTap: widget.onTap, - child: Container( - color: _hover ? tokens.listItemHoverBackground : null, - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), - child: Row( - children: [ - if (widget.current) - Padding( - padding: const EdgeInsets.only(right: 8), - child: ClideIcon( - const CheckIcon(), - size: 12, - color: tokens.statusSuccess, - ), - ) - else - const SizedBox(width: 20), - Expanded( - child: ClideText( - widget.name, - fontFamily: clideMonoFamily, - fontSize: clideFontMono, - color: widget.current - ? tokens.globalForeground - : tokens.listItemForeground, + return ClideTappable( + onTap: onTap, + cursor: onTap != null ? SystemMouseCursors.click : MouseCursor.defer, + builder: (context, hovered, _) => Container( + color: hovered ? tokens.listItemHoverBackground : null, + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + child: Row( + children: [ + if (current) + Padding( + padding: const EdgeInsets.only(right: 8), + child: ClideIcon( + const CheckIcon(), + size: 12, + color: tokens.statusSuccess, ), + ) + else + const SizedBox(width: 20), + Expanded( + child: ClideText( + name, + fontFamily: clideMonoFamily, + fontSize: clideFontMono, + color: current + ? tokens.globalForeground + : tokens.listItemForeground, ), - ], - ), + ), + ], ), ), ); diff --git a/lib/builtin/graph/src/graph_view.dart b/lib/builtin/graph/src/graph_view.dart index c0b09874..2149e150 100644 --- a/lib/builtin/graph/src/graph_view.dart +++ b/lib/builtin/graph/src/graph_view.dart @@ -90,30 +90,21 @@ class _GraphNode { ); } -class _NodeRow extends StatefulWidget { +class _NodeRow extends StatelessWidget { const _NodeRow({required this.node, required this.tokens}); final _GraphNode node; final SurfaceTokens tokens; - @override - State<_NodeRow> createState() => _NodeRowState(); -} - -class _NodeRowState extends State<_NodeRow> { - bool _hovered = false; - @override Widget build(BuildContext context) { - return MouseRegion( - onEnter: (_) => setState(() => _hovered = true), - onExit: (_) => setState(() => _hovered = false), - child: Container( - color: _hovered ? widget.tokens.listItemHoverBackground : null, + return ClideTappable( + builder: (context, hovered, _) => Container( + color: hovered ? tokens.listItemHoverBackground : null, padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), child: Row( children: [ - Expanded(child: ClideText(widget.node.path, fontSize: 13)), - ClideText('${widget.node.inbound}in ${widget.node.outbound}out', color: widget.tokens.globalTextMuted, fontSize: 11), + Expanded(child: ClideText(node.path, fontSize: 13)), + ClideText('${node.inbound}in ${node.outbound}out', color: tokens.globalTextMuted, fontSize: 11), ], ), ), diff --git a/lib/builtin/pql/src/backlinks_view.dart b/lib/builtin/pql/src/backlinks_view.dart index 40144df3..fd331796 100644 --- a/lib/builtin/pql/src/backlinks_view.dart +++ b/lib/builtin/pql/src/backlinks_view.dart @@ -140,31 +140,22 @@ class _LinkGroup extends StatelessWidget { } } -class _LinkRow extends StatefulWidget { +class _LinkRow extends StatelessWidget { const _LinkRow({required this.link, required this.pathKey}); final Map link; final String pathKey; - @override - State<_LinkRow> createState() => _LinkRowState(); -} - -class _LinkRowState extends State<_LinkRow> { - bool _hover = false; - @override Widget build(BuildContext context) { final tokens = ClideTheme.of(context).surface; - final target = widget.link[widget.pathKey] as String? ?? ''; - final alias = widget.link['alias'] as String?; + final target = link[pathKey] as String? ?? ''; + final alias = link['alias'] as String?; final display = alias ?? target; - return MouseRegion( - cursor: SystemMouseCursors.click, - onEnter: (_) => setState(() => _hover = true), - onExit: (_) => setState(() => _hover = false), - child: GestureDetector( - behavior: HitTestBehavior.opaque, + return Semantics( + button: true, + label: target, + child: ClideTappable( onTap: () { if (!target.startsWith('http')) { final kernel = ClideKernel.of(context); @@ -172,21 +163,17 @@ class _LinkRowState extends State<_LinkRow> { kernel.ipc.request('editor.open', args: {'path': target})); } }, - child: Semantics( - button: true, - label: target, - child: Container( - color: _hover ? tokens.sidebarItemHover : null, - padding: - const EdgeInsets.symmetric(horizontal: 20, vertical: 2), - child: ClideText( - display, - maxLines: 1, - overflow: TextOverflow.ellipsis, - color: target.startsWith('http') - ? tokens.statusInfo - : tokens.sidebarForeground, - ), + builder: (context, hovered, _) => Container( + color: hovered ? tokens.sidebarItemHover : null, + padding: + const EdgeInsets.symmetric(horizontal: 20, vertical: 2), + child: ClideText( + display, + maxLines: 1, + overflow: TextOverflow.ellipsis, + color: target.startsWith('http') + ? tokens.statusInfo + : tokens.sidebarForeground, ), ), ), diff --git a/lib/builtin/pql/src/pql_panel_view.dart b/lib/builtin/pql/src/pql_panel_view.dart index d63a30b6..f60381f7 100644 --- a/lib/builtin/pql/src/pql_panel_view.dart +++ b/lib/builtin/pql/src/pql_panel_view.dart @@ -155,45 +155,32 @@ class _ViewTabs extends StatelessWidget { }; } -class _FileRow extends StatefulWidget { +class _FileRow extends StatelessWidget { const _FileRow({required this.entry}); final Map entry; - @override - State<_FileRow> createState() => _FileRowState(); -} - -class _FileRowState extends State<_FileRow> { - bool _hover = false; - @override Widget build(BuildContext context) { final tokens = ClideTheme.of(context).surface; - final path = widget.entry['path'] as String? ?? ''; - final name = widget.entry['name'] as String? ?? path.split('/').last; - return MouseRegion( - cursor: SystemMouseCursors.click, - onEnter: (_) => setState(() => _hover = true), - onExit: (_) => setState(() => _hover = false), - child: GestureDetector( - behavior: HitTestBehavior.opaque, + final path = entry['path'] as String? ?? ''; + final name = entry['name'] as String? ?? path.split('/').last; + return Semantics( + button: true, + label: 'Open $name', + child: ClideTappable( onTap: () { final kernel = ClideKernel.of(context); unawaited( kernel.ipc.request('editor.open', args: {'path': path})); }, - child: Semantics( - button: true, - label: 'Open $name', - child: Container( - color: _hover ? tokens.sidebarItemHover : null, - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 3), - child: ClideText( - path, - maxLines: 1, - overflow: TextOverflow.ellipsis, - color: tokens.sidebarForeground, - ), + builder: (context, hovered, _) => Container( + color: hovered ? tokens.sidebarItemHover : null, + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 3), + child: ClideText( + path, + maxLines: 1, + overflow: TextOverflow.ellipsis, + color: tokens.sidebarForeground, ), ), ), diff --git a/lib/builtin/tickets/src/tickets_view.dart b/lib/builtin/tickets/src/tickets_view.dart index d66f0711..ad3fdfb1 100644 --- a/lib/builtin/tickets/src/tickets_view.dart +++ b/lib/builtin/tickets/src/tickets_view.dart @@ -102,35 +102,26 @@ class _TicketEntry { ); } -class _TicketRow extends StatefulWidget { +class _TicketRow extends StatelessWidget { const _TicketRow({required this.entry, required this.tokens}); final _TicketEntry entry; final SurfaceTokens tokens; - @override - State<_TicketRow> createState() => _TicketRowState(); -} - -class _TicketRowState extends State<_TicketRow> { - bool _hovered = false; - @override Widget build(BuildContext context) { - final statusColor = switch (widget.entry.status) { - 'done' => widget.tokens.statusSuccess, - 'in_progress' => widget.tokens.statusInfo, - 'cancelled' => widget.tokens.statusError, - _ => widget.tokens.globalTextMuted, + final statusColor = switch (entry.status) { + 'done' => tokens.statusSuccess, + 'in_progress' => tokens.statusInfo, + 'cancelled' => tokens.statusError, + _ => tokens.globalTextMuted, }; - return MouseRegion( - onEnter: (_) => setState(() => _hovered = true), - onExit: (_) => setState(() => _hovered = false), - child: Container( - color: _hovered ? widget.tokens.listItemHoverBackground : null, + return ClideTappable( + builder: (context, hovered, _) => Container( + color: hovered ? tokens.listItemHoverBackground : null, padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), child: Row( children: [ - ClideText(widget.entry.id, color: widget.tokens.globalTextMuted, fontSize: 12), + ClideText(entry.id, color: tokens.globalTextMuted, fontSize: 12), const SizedBox(width: 6), Container( width: 6, @@ -138,7 +129,7 @@ class _TicketRowState extends State<_TicketRow> { decoration: BoxDecoration(color: statusColor, shape: BoxShape.circle), ), const SizedBox(width: 6), - Expanded(child: ClideText(widget.entry.title, fontSize: 13)), + Expanded(child: ClideText(entry.title, fontSize: 13)), ], ), ), diff --git a/lib/builtin/welcome/src/welcome_view.dart b/lib/builtin/welcome/src/welcome_view.dart index 17681353..87b27e61 100644 --- a/lib/builtin/welcome/src/welcome_view.dart +++ b/lib/builtin/welcome/src/welcome_view.dart @@ -129,7 +129,7 @@ class _StartColumn extends StatelessWidget { } } -class _ActionRow extends StatefulWidget { +class _ActionRow extends StatelessWidget { const _ActionRow({required this.icon, required this.label, this.shortcut, required this.tokens, required this.onTap}); final ClideIconPainter icon; final String label; @@ -137,36 +137,24 @@ class _ActionRow extends StatefulWidget { final SurfaceTokens tokens; final VoidCallback onTap; - @override - State<_ActionRow> createState() => _ActionRowState(); -} - -class _ActionRowState extends State<_ActionRow> { - 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.onTap, - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), - decoration: BoxDecoration( - color: _hover ? widget.tokens.listItemHoverBackground : null, - borderRadius: BorderRadius.circular(4), - ), - child: Row( - children: [ - ClideIcon(widget.icon, size: 18, color: widget.tokens.globalTextMuted), - const SizedBox(width: 14), - Expanded(child: ClideText(widget.label, fontSize: 15, color: widget.tokens.globalForeground)), - if (widget.shortcut != null) - ClideText(widget.shortcut!, fontSize: 13, color: widget.tokens.globalTextMuted, fontFamily: clideMonoFamily), - ], - ), + return ClideTappable( + onTap: onTap, + builder: (context, hovered, _) => Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + decoration: BoxDecoration( + color: hovered ? tokens.listItemHoverBackground : null, + borderRadius: BorderRadius.circular(4), + ), + child: Row( + children: [ + ClideIcon(icon, size: 18, color: tokens.globalTextMuted), + const SizedBox(width: 14), + Expanded(child: ClideText(label, fontSize: 15, color: tokens.globalForeground)), + if (shortcut != null) + ClideText(shortcut!, fontSize: 13, color: tokens.globalTextMuted, fontFamily: clideMonoFamily), + ], ), ), ); @@ -207,58 +195,46 @@ class _RecentColumn extends StatelessWidget { } } -class _RecentRow extends StatefulWidget { +class _RecentRow extends StatelessWidget { const _RecentRow({required this.project, required this.tokens, required this.onTap}); final RecentProject project; final SurfaceTokens tokens; final VoidCallback onTap; - @override - State<_RecentRow> createState() => _RecentRowState(); -} - -class _RecentRowState extends State<_RecentRow> { - 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.onTap, - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), - decoration: BoxDecoration( - color: _hover ? widget.tokens.listItemHoverBackground : null, - borderRadius: BorderRadius.circular(4), - ), - child: Row( - children: [ - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - ClideText(widget.project.name, fontSize: 15, fontWeight: FontWeight.w500), - const SizedBox(height: 3), - Row( - children: [ - Flexible(child: ClideText(widget.project.relativePath, muted: true, fontSize: 13, fontFamily: clideMonoFamily, maxLines: 1, overflow: TextOverflow.ellipsis)), - if (widget.project.branch != null) ...[ - ClideText(' · ', muted: true, fontSize: 13), - ClideIcon(PhosphorIcons.gitBranch, size: 11, color: widget.tokens.globalTextMuted), - const SizedBox(width: 3), - ClideText(widget.project.branch!, muted: true, fontSize: 13, fontFamily: clideMonoFamily), - ], + return ClideTappable( + onTap: onTap, + builder: (context, hovered, _) => Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + decoration: BoxDecoration( + color: hovered ? tokens.listItemHoverBackground : null, + borderRadius: BorderRadius.circular(4), + ), + child: Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ClideText(project.name, fontSize: 15, fontWeight: FontWeight.w500), + const SizedBox(height: 3), + Row( + children: [ + Flexible(child: ClideText(project.relativePath, muted: true, fontSize: 13, fontFamily: clideMonoFamily, maxLines: 1, overflow: TextOverflow.ellipsis)), + if (project.branch != null) ...[ + ClideText(' · ', muted: true, fontSize: 13), + ClideIcon(PhosphorIcons.gitBranch, size: 11, color: tokens.globalTextMuted), + const SizedBox(width: 3), + ClideText(project.branch!, muted: true, fontSize: 13, fontFamily: clideMonoFamily), ], - ), - ], - ), + ], + ), + ], ), - ClideText(widget.project.timeAgo, muted: true, fontSize: 13), - ], - ), + ), + ClideText(project.timeAgo, muted: true, fontSize: 13), + ], ), ), ); diff --git a/lib/widgets/src/clide_column_hat.dart b/lib/widgets/src/clide_column_hat.dart index cf4cbc17..1c87f836 100644 --- a/lib/widgets/src/clide_column_hat.dart +++ b/lib/widgets/src/clide_column_hat.dart @@ -4,6 +4,7 @@ import 'package:clide/kernel/src/theme/controller.dart'; import 'package:clide/kernel/src/theme/tokens.dart'; import 'package:clide/kernel/src/window_controls.dart'; import 'package:clide/widgets/src/clide_icon.dart'; +import 'package:clide/widgets/src/clide_tappable.dart'; import 'package:clide/widgets/src/clide_text.dart'; import 'package:clide/widgets/src/icons/phosphor.dart'; import 'package:clide/widgets/src/typography.dart'; @@ -113,69 +114,45 @@ class _RightContent extends StatelessWidget { } } -class _TrafficDot extends StatefulWidget { +class _TrafficDot extends StatelessWidget { const _TrafficDot({required this.color, required this.onTap}); final Color color; final VoidCallback onTap; - @override - State<_TrafficDot> createState() => _TrafficDotState(); -} - -class _TrafficDotState extends State<_TrafficDot> { - bool _hover = false; - @override Widget build(BuildContext context) { - return MouseRegion( - onEnter: (_) => setState(() => _hover = true), - onExit: (_) => setState(() => _hover = false), - child: GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: widget.onTap, - child: Container( - width: 12, - height: 12, - decoration: BoxDecoration( - color: _hover ? widget.color : widget.color.withAlpha(0xCC), - shape: BoxShape.circle, - ), + return ClideTappable( + onTap: onTap, + builder: (context, hovered, _) => Container( + width: 12, + height: 12, + decoration: BoxDecoration( + color: hovered ? color : color.withAlpha(0xCC), + shape: BoxShape.circle, ), ), ); } } -class _WinButton extends StatefulWidget { +class _WinButton extends StatelessWidget { const _WinButton({required this.icon, required this.onTap, required this.tokens, this.isClose = false}); final ClideIconPainter icon; final VoidCallback onTap; final SurfaceTokens tokens; final bool isClose; - @override - State<_WinButton> createState() => _WinButtonState(); -} - -class _WinButtonState extends State<_WinButton> { - bool _hover = false; - @override Widget build(BuildContext context) { - final hoverBg = widget.isClose ? const Color(0xFFE81123) : widget.tokens.listItemHoverBackground; - return MouseRegion( - onEnter: (_) => setState(() => _hover = true), - onExit: (_) => setState(() => _hover = false), - child: GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: widget.onTap, - child: Container( - width: 36, - height: hatHeight, - color: _hover ? hoverBg : null, - alignment: Alignment.center, - child: ClideIcon(widget.icon, size: 14, color: _hover && widget.isClose ? const Color(0xFFFFFFFF) : widget.tokens.globalTextMuted), - ), + final hoverBg = isClose ? const Color(0xFFE81123) : tokens.listItemHoverBackground; + return ClideTappable( + onTap: onTap, + builder: (context, hovered, _) => Container( + width: 36, + height: hatHeight, + color: hovered ? hoverBg : null, + alignment: Alignment.center, + child: ClideIcon(icon, size: 14, color: hovered && isClose ? const Color(0xFFFFFFFF) : tokens.globalTextMuted), ), ); } diff --git a/lib/widgets/src/clide_pane_chrome.dart b/lib/widgets/src/clide_pane_chrome.dart index af6c98c7..b442ef81 100644 --- a/lib/widgets/src/clide_pane_chrome.dart +++ b/lib/widgets/src/clide_pane_chrome.dart @@ -3,6 +3,7 @@ import 'package:flutter/widgets.dart'; import 'clide_divider.dart'; import 'clide_icon.dart'; +import 'clide_tappable.dart'; import 'clide_text.dart'; import 'icons/x.dart'; import 'typography.dart'; @@ -67,43 +68,31 @@ class ClidePaneChrome extends StatelessWidget { } } -class _CloseButton extends StatefulWidget { +class _CloseButton extends StatelessWidget { const _CloseButton({required this.onPressed}); final VoidCallback onPressed; - @override - State<_CloseButton> createState() => _CloseButtonState(); -} - -class _CloseButtonState extends State<_CloseButton> { - bool _hover = false; - @override Widget build(BuildContext context) { final tokens = ClideTheme.of(context).surface; return Semantics( button: true, label: 'Close pane', - onTap: widget.onPressed, - child: MouseRegion( - cursor: SystemMouseCursors.click, - onEnter: (_) => setState(() => _hover = true), - onExit: (_) => setState(() => _hover = false), - child: GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: widget.onPressed, - child: Container( - width: 20, - height: 20, - alignment: Alignment.center, - decoration: BoxDecoration( - color: _hover ? tokens.tabCloseHover : null, - ), - child: ClideIcon( - const CloseIcon(), - size: 10, - color: tokens.panelHeaderForeground, - ), + onTap: onPressed, + child: ClideTappable( + onTap: onPressed, + tooltip: 'Close pane', + builder: (context, hovered, _) => Container( + width: 20, + height: 20, + alignment: Alignment.center, + decoration: BoxDecoration( + color: hovered ? tokens.tabCloseHover : null, + ), + child: ClideIcon( + const CloseIcon(), + size: 10, + color: tokens.panelHeaderForeground, ), ), ), diff --git a/lib/widgets/src/clide_spine.dart b/lib/widgets/src/clide_spine.dart index e0756609..67703fab 100644 --- a/lib/widgets/src/clide_spine.dart +++ b/lib/widgets/src/clide_spine.dart @@ -1,9 +1,10 @@ import 'dart:math' as math; import 'package:clide/kernel/src/theme/controller.dart'; +import 'package:clide/widgets/src/clide_tappable.dart'; import 'package:flutter/widgets.dart'; -class ClideSpine extends StatefulWidget { +class ClideSpine extends StatelessWidget { const ClideSpine({ super.key, required this.label, @@ -19,15 +20,6 @@ class ClideSpine extends StatefulWidget { static const double width = 12; - @override - State createState() => _ClideSpineState(); -} - -enum SpineSide { left, right } - -class _ClideSpineState extends State { - bool _hovered = false; - @override Widget build(BuildContext context) { final tokens = ClideTheme.of(context).surface; @@ -35,60 +27,57 @@ class _ClideSpineState extends State { return Semantics( button: true, - label: '${widget.label} — click to expand', - child: MouseRegion( - cursor: SystemMouseCursors.click, - onEnter: (_) => setState(() => _hovered = true), - onExit: (_) => setState(() => _hovered = false), - child: GestureDetector( - onTap: widget.onExpand, - child: Container( - width: ClideSpine.width, - decoration: BoxDecoration( - color: _hovered ? tokens.sidebarItemHover : tokens.chromeBackground, - border: Border( - left: widget.side == SpineSide.right ? borderSide : BorderSide.none, - right: widget.side == SpineSide.left ? borderSide : BorderSide.none, - ), + label: '$label — click to expand', + child: ClideTappable( + onTap: onExpand, + builder: (context, hovered, _) => Container( + width: ClideSpine.width, + decoration: BoxDecoration( + color: hovered ? tokens.sidebarItemHover : tokens.chromeBackground, + border: Border( + left: side == SpineSide.right ? borderSide : BorderSide.none, + right: side == SpineSide.left ? borderSide : BorderSide.none, ), - child: Stack( - children: [ - Center( - child: Transform.rotate( - angle: widget.side == SpineSide.left ? -math.pi / 2 : math.pi / 2, - child: Text( - widget.label, - style: TextStyle( - fontSize: 9, - color: tokens.globalTextMuted, - letterSpacing: 0.5, + ), + child: Stack( + children: [ + Center( + child: Transform.rotate( + angle: side == SpineSide.left ? -math.pi / 2 : math.pi / 2, + child: Text( + label, + style: TextStyle( + fontSize: 9, + color: tokens.globalTextMuted, + letterSpacing: 0.5, + ), + maxLines: 1, + overflow: TextOverflow.clip, + ), + ), + ), + if (badgeCount > 0) + Positioned( + top: 4, + left: 0, + right: 0, + child: Center( + child: Container( + width: 6, + height: 6, + decoration: BoxDecoration( + color: tokens.statusInfo, + shape: BoxShape.circle, ), - maxLines: 1, - overflow: TextOverflow.clip, ), ), ), - if (widget.badgeCount > 0) - Positioned( - top: 4, - left: 0, - right: 0, - child: Center( - child: Container( - width: 6, - height: 6, - decoration: BoxDecoration( - color: tokens.statusInfo, - shape: BoxShape.circle, - ), - ), - ), - ), - ], - ), + ], ), ), ), ); } } + +enum SpineSide { left, right } diff --git a/lib/widgets/src/clide_tab_bar.dart b/lib/widgets/src/clide_tab_bar.dart index 21ee8f81..1c7f2a09 100644 --- a/lib/widgets/src/clide_tab_bar.dart +++ b/lib/widgets/src/clide_tab_bar.dart @@ -1,5 +1,6 @@ import 'package:clide/kernel/src/theme/controller.dart'; import 'package:clide/widgets/src/clide_icon.dart'; +import 'package:clide/widgets/src/clide_tappable.dart'; import 'package:clide/widgets/src/clide_text.dart'; import 'package:flutter/widgets.dart'; @@ -59,51 +60,35 @@ class ClideTabBar extends StatelessWidget { } } -class _Tab extends StatefulWidget { +class _Tab extends StatelessWidget { const _Tab({required this.item, required this.active, required this.onTap}); final ClideTabItem item; final bool active; final VoidCallback onTap; - @override - State<_Tab> createState() => _TabState(); -} - -class _TabState extends State<_Tab> { - bool _hovered = false; - @override Widget build(BuildContext context) { final tokens = ClideTheme.of(context).surface; - final bg = widget.active - ? tokens.tabActive - : (_hovered ? tokens.tabInactive : tokens.tabInactive); - final fg = widget.active - ? tokens.tabActiveForeground - : tokens.tabInactiveForeground; + final fg = active ? tokens.tabActiveForeground : tokens.tabInactiveForeground; return Semantics( button: true, - selected: widget.active, - label: widget.item.title, - onTap: widget.onTap, + selected: active, + label: item.title, + onTap: onTap, excludeSemantics: true, - child: MouseRegion( - cursor: SystemMouseCursors.click, - onEnter: (_) => setState(() => _hovered = true), - onExit: (_) => setState(() => _hovered = false), - child: GestureDetector( - onTap: widget.onTap, - child: Container( + child: ClideTappable( + onTap: onTap, + builder: (context, hovered, _) { + final bg = active ? tokens.tabActive : tokens.tabInactive; + return Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), decoration: BoxDecoration( color: bg, border: Border( bottom: BorderSide( - color: widget.active - ? tokens.tabActiveBorder - : const Color(0x00000000), + color: active ? tokens.tabActiveBorder : const Color(0x00000000), width: 2, ), ), @@ -111,15 +96,15 @@ class _TabState extends State<_Tab> { child: Row( mainAxisSize: MainAxisSize.min, children: [ - if (widget.item.icon != null) ...[ - ClideIcon(widget.item.icon!, size: 12, color: fg), + if (item.icon != null) ...[ + ClideIcon(item.icon!, size: 12, color: fg), const SizedBox(width: 6), ], - ClideText(widget.item.title, color: fg), + ClideText(item.title, color: fg), ], ), - ), - ), + ); + }, ), ); }