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), ], ), - ), - ), + ); + }, ), ); }