extend ClideTappable with press/longpress, retrofit button + rail

ClideTappable builder now receives (context, hovered, pressed).
Optional onLongPress and onPressChanged callbacks. ClideButton
is now a StatelessWidget wrapping ClideTappable. ClideIconRail
refactored — _RailButton uses ClideTappable with tooltip.
ClideTooltip positions smart (above when near screen bottom).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-23 11:21:05 +02:00
co-authored by Claude Opus 4.6
parent c9870bb044
commit fbd5f1ae04
5 changed files with 129 additions and 139 deletions
+37 -62
View File
@@ -1,10 +1,11 @@
import 'package:clide/kernel/src/theme/controller.dart';
import 'package:clide/widgets/src/clide_tappable.dart';
import 'package:clide/widgets/src/clide_text.dart';
import 'package:flutter/widgets.dart';
enum ClideButtonVariant { normal, primary, subtle }
class ClideButton extends StatefulWidget {
class ClideButton extends StatelessWidget {
const ClideButton({
super.key,
required this.label,
@@ -13,92 +14,66 @@ class ClideButton extends StatefulWidget {
this.padding = const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
this.semanticLabel,
this.semanticHint,
this.tooltip,
});
final String label;
final VoidCallback? onPressed;
final ClideButtonVariant variant;
final EdgeInsetsGeometry padding;
/// Overrides [label] for screen readers (use when the visible label is
/// an icon-only glyph or a noun that reads oddly when announced).
final String? semanticLabel;
/// Screen-reader hint describing the button's effect. Optional.
final String? semanticHint;
@override
State<ClideButton> createState() => _ClideButtonState();
}
class _ClideButtonState extends State<ClideButton> {
bool _hovered = false;
bool _pressed = false;
final String? tooltip;
@override
Widget build(BuildContext context) {
final tokens = ClideTheme.of(context).surface;
final enabled = widget.onPressed != null;
Color bg;
Color fg;
switch (widget.variant) {
case ClideButtonVariant.normal:
bg = _pressed
? tokens.buttonActiveBackground
: _hovered
? tokens.buttonHoverBackground
: tokens.buttonBackground;
fg = tokens.buttonForeground;
case ClideButtonVariant.primary:
bg = _pressed
? tokens.panelActiveBorder
: _hovered
? tokens.buttonActiveBackground
: tokens.buttonActiveBackground;
fg = tokens.globalBackground;
case ClideButtonVariant.subtle:
bg = _hovered
? tokens.listItemHoverBackground
: tokens.listItemBackground;
fg = tokens.listItemForeground;
}
final enabled = onPressed != null;
return Semantics(
button: true,
enabled: enabled,
label: widget.semanticLabel ?? widget.label,
hint: widget.semanticHint,
onTap: enabled ? widget.onPressed : null,
label: semanticLabel ?? label,
hint: semanticHint,
onTap: enabled ? onPressed : null,
excludeSemantics: true,
child: MouseRegion(
cursor:
enabled ? SystemMouseCursors.click : SystemMouseCursors.forbidden,
onEnter: (_) => setState(() => _hovered = true),
onExit: (_) => setState(() => _hovered = false),
child: GestureDetector(
onTapDown: (_) => setState(() => _pressed = true),
onTapCancel: () => setState(() => _pressed = false),
onTapUp: (_) {
setState(() => _pressed = false);
widget.onPressed?.call();
},
child: Container(
padding: widget.padding,
child: ClideTappable(
cursor: enabled ? SystemMouseCursors.click : SystemMouseCursors.forbidden,
tooltip: tooltip,
onTap: onPressed,
builder: (ctx, hovered, pressed) {
Color bg;
Color fg;
switch (variant) {
case ClideButtonVariant.normal:
bg = pressed
? tokens.buttonActiveBackground
: hovered
? tokens.buttonHoverBackground
: tokens.buttonBackground;
fg = tokens.buttonForeground;
case ClideButtonVariant.primary:
bg = pressed ? tokens.panelActiveBorder : tokens.buttonActiveBackground;
fg = tokens.globalBackground;
case ClideButtonVariant.subtle:
bg = hovered ? tokens.listItemHoverBackground : tokens.listItemBackground;
fg = tokens.listItemForeground;
}
return Container(
padding: padding,
decoration: BoxDecoration(
color: bg,
border: Border.all(color: tokens.buttonBorder, width: 1),
borderRadius: BorderRadius.circular(3),
),
child: ClideText(
widget.label,
label,
color: fg,
fontWeight: widget.variant == ClideButtonVariant.primary
? FontWeight.w600
: FontWeight.w500,
fontWeight: variant == ClideButtonVariant.primary ? FontWeight.w600 : FontWeight.w500,
),
),
),
);
},
),
);
}