Skip to content

Instantly share code, notes, and snippets.

@davidhicks980
Created March 10, 2026 19:58
Show Gist options
  • Select an option

  • Save davidhicks980/45ab05be46c7a72a2ee59d52fba1d6a8 to your computer and use it in GitHub Desktop.

Select an option

Save davidhicks980/45ab05be46c7a72a2ee59d52fba1d6a8 to your computer and use it in GitHub Desktop.
Button Demo
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/semantics.dart';
void main() {
runApp(const ButtonDemoApp());
}
class ButtonDemoApp extends StatelessWidget {
const ButtonDemoApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Button Demo',
debugShowCheckedModeBanner: false,
home: Scaffold(
appBar: AppBar(title: const Text('Button Example')),
body: const SafeArea(child: ButtonExample()),
),
);
}
}
class ButtonExample extends StatelessWidget {
const ButtonExample({super.key});
@override
Widget build(BuildContext context) {
return Center(
child: Button(
onPressed: () {
ScaffoldMessenger.of(
context,
).showSnackBar(const SnackBar(content: Text('Button Pressed!')));
},
behavior: HitTestBehavior.opaque,
mouseCursor: WidgetStateProperty.all(SystemMouseCursors.click),
child: Builder(
builder: (context) {
final ButtonState state = Button.of(context)!;
final bool isHovered = state.hovered;
final bool isPressed = state.pressed;
final bool isFocused = state.focused;
return AnimatedContainer(
duration: const Duration(milliseconds: 50),
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
decoration: BoxDecoration(
color: isPressed
? Colors.blue.shade700
: isHovered
? Colors.blue.shade600
: Colors.blue.shade500,
borderRadius: BorderRadius.circular(8),
border: isFocused ? Border.all(color: Colors.yellow, width: 2) : null,
),
child: const Text('Button', style: TextStyle(color: Colors.white, fontSize: 16)),
);
},
),
),
);
}
}
// Impl
class _ButtonStateScope extends InheritedNotifier<WidgetStatesController> {
const _ButtonStateScope({required super.notifier, required super.child});
}
extension type ButtonState._(Set<WidgetState> value) {
bool get hovered => value.contains(WidgetState.hovered);
bool get pressed => value.contains(WidgetState.pressed);
bool get focused => value.contains(WidgetState.focused);
bool get disabled => value.contains(WidgetState.disabled);
Set<WidgetState> get states => value;
}
class Button extends StatefulWidget {
const Button({
super.key,
this.onHover,
this.onPressed,
this.onFocusChange,
this.focusNode,
this.autofocus = false,
this.behavior = HitTestBehavior.deferToChild,
this.state,
this.mouseCursor,
required this.child,
});
final ValueChanged<bool>? onHover;
final VoidCallback? onPressed;
final ValueChanged<bool>? onFocusChange;
final FocusNode? focusNode;
final bool autofocus;
final HitTestBehavior behavior;
final Set<WidgetState>? state;
final WidgetStateProperty<MouseCursor>? mouseCursor;
final Widget child;
static ButtonState of(BuildContext context) {
final Set<WidgetState> value = context
.dependOnInheritedWidgetOfExactType<_ButtonStateScope>()
!.notifier
!.value;
return ButtonState._(value);
}
@override
State<Button> createState() => _ButtonState();
}
class _ButtonState extends State<Button> {
late final Map<Type, Action<Intent>> _actions = <Type, Action<Intent>>{
ActivateIntent: CallbackAction<ActivateIntent>(onInvoke: _handleActivation),
ButtonActivateIntent: CallbackAction<ButtonActivateIntent>(onInvoke: _handleActivation),
};
Map<Type, GestureRecognizerFactory>? _gestures;
DeviceGestureSettings? _gestureSettings;
// If a focus node isn't given to the widget, then we have to manage our own.
FocusNode? _internalFocusNode;
FocusNode get _focusNode => widget.focusNode ?? _internalFocusNode!;
final WidgetStatesController _statesController = WidgetStatesController();
bool get isHovered => _isHovered;
bool _isHovered = false;
set isHovered(bool value) {
if (_isHovered != value) {
_isHovered = value;
_statesController.update(WidgetState.hovered, value);
}
}
bool get isPressed => _isPressed;
bool _isPressed = false;
set isPressed(bool value) {
if (_isPressed != value) {
_isPressed = value;
_statesController.update(WidgetState.pressed, value);
}
}
bool get isFocused => _isFocused;
bool _isFocused = false;
set isFocused(bool value) {
if (_isFocused != value) {
_isFocused = value;
_statesController.update(WidgetState.focused, value);
}
}
bool get isEnabled => _isEnabled;
bool _isEnabled = false;
set isEnabled(bool value) {
if (_isEnabled != value) {
_isEnabled = value;
_statesController.update(WidgetState.disabled, !value);
}
}
@override
void initState() {
super.initState();
if (widget.focusNode == null) {
_internalFocusNode = FocusNode();
}
_statesController.value = widget.state ?? <WidgetState>{};
isEnabled = widget.onPressed != null;
isFocused = _focusNode.hasPrimaryFocus;
}
@override
void didUpdateWidget(Button oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.focusNode != oldWidget.focusNode) {
if (widget.focusNode != null) {
_internalFocusNode?.dispose();
_internalFocusNode = null;
} else {
assert(_internalFocusNode == null);
_internalFocusNode = FocusNode();
}
isFocused = _focusNode.hasPrimaryFocus;
}
if (widget.state != oldWidget.state && widget.state != null) {
_statesController.value = widget.state!;
}
if (widget.onPressed != oldWidget.onPressed) {
if (widget.onPressed == null) {
isEnabled = isHovered = isPressed = isFocused = false;
} else {
isEnabled = true;
}
}
}
@override
void dispose() {
_statesController.dispose();
_internalFocusNode?.dispose();
_internalFocusNode = null;
super.dispose();
}
void _handleFocusChange([bool? focused]) {
isFocused = _focusNode.hasPrimaryFocus;
widget.onFocusChange?.call(isFocused);
}
void _handleActivation([Intent? intent]) {
isPressed = false;
widget.onPressed?.call();
}
void _handleTapDown(TapDownDetails details) {
isPressed = true;
}
void _handleTapUp(TapUpDetails? details) {
isPressed = false;
widget.onPressed?.call();
}
void _handleTapCancel() {
isPressed = false;
}
void _handlePointerExit(PointerExitEvent event) {
if (isHovered) {
isHovered = false;
widget.onHover?.call(false);
}
}
void _handlePointerEnter(PointerEnterEvent event) {
if (!isHovered) {
isHovered = true;
widget.onHover?.call(true);
}
}
void _handleDismiss() {
Actions.invoke(context, const DismissIntent());
}
@override
Widget build(BuildContext context) {
final DeviceGestureSettings? newGestureSettings = MediaQuery.maybeGestureSettingsOf(context);
if (_gestureSettings != newGestureSettings) {
_gestureSettings = newGestureSettings;
_gestures = null;
}
_gestures ??= <Type, GestureRecognizerFactory>{
TapGestureRecognizer: GestureRecognizerFactoryWithHandlers<TapGestureRecognizer>(
() => TapGestureRecognizer(debugOwner: this),
(TapGestureRecognizer instance) {
instance
..onTapDown = _handleTapDown
..onTapUp = _handleTapUp
..onTapCancel = _handleTapCancel
..gestureSettings = _gestureSettings;
},
),
};
final child = RawGestureDetector(
behavior: widget.behavior,
gestures: isEnabled ? _gestures! : const <Type, GestureRecognizerFactory>{},
child: widget.child,
);
return MergeSemantics(
child: Semantics.fromProperties(
properties: SemanticsProperties(
enabled: isEnabled,
onDismiss: isEnabled ? _handleDismiss : null,
),
child: Actions(
actions: isEnabled ? _actions : <Type, Action<Intent>>{},
child: Focus(
autofocus: isEnabled && widget.autofocus,
focusNode: _focusNode,
canRequestFocus: isEnabled,
skipTraversal: !isEnabled,
onFocusChange: _handleFocusChange,
child: _ButtonStateScope(
notifier: _statesController,
child: Builder(
builder: (context) {
final MouseCursor? cursor = widget.mouseCursor?.resolve(
Button.of(context)?.states ?? <WidgetState>{},
);
return MouseRegion(
onEnter: isEnabled ? _handlePointerEnter : null,
onExit: isEnabled ? _handlePointerExit : null,
hitTestBehavior: HitTestBehavior.deferToChild,
cursor: cursor ?? MouseCursor.defer,
child: child,
);
},
),
),
),
),
),
);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment