Skip to content

Instantly share code, notes, and snippets.

@davidhicks980
Last active November 14, 2025 09:12
Show Gist options
  • Select an option

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

Select an option

Save davidhicks980/bc70d14c81942224a58e2caa2706c484 to your computer and use it in GitHub Desktop.
WidgetStateScope demo
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
void main() {
runApp(const RawButtonApp());
}
class RawButtonApp extends StatelessWidget {
const RawButtonApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Threshold Demo',
theme: ThemeData(primarySwatch: Colors.blue),
home: const Scaffold(body: Center(child: RawButtonDemo())),
);
}
}
class RawButtonDemo extends StatefulWidget {
const RawButtonDemo({super.key});
@override
State<RawButtonDemo> createState() => _RawButtonDemoState();
}
class _RawButtonDemoState extends State<RawButtonDemo> with TickerProviderStateMixin {
static final decoration = WidgetStateProperty.fromMap({
WidgetState.pressed: const AnimatedBoxDecoration(
color: Colors.indigoAccent,
duration: Duration(milliseconds: 100),
curve: Curves.easeIn,
),
WidgetState.hovered: const AnimatedBoxDecoration(
color: Colors.indigo,
duration: Duration(milliseconds: 200),
curve: Curves.easeOutExpo,
),
WidgetState.any: AnimatedBoxDecoration(
color: Colors.indigo[100],
duration: const Duration(milliseconds: 300),
curve: Curves.easeOut,
),
});
static final textStyle = WidgetStateProperty.fromMap({
WidgetState.hovered: const TextStyle(fontWeight: FontWeight.w600, color: Colors.white),
WidgetState.any: TextStyle(color: Colors.indigo[900], fontWeight: FontWeight.w600),
});
@override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
RawButton(
onPressed: () {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Button Pressed!')),
);
},
child: AnimatedWidgetStatesDecoration(
decoration: decoration,
child: WidgetStatesDefaultTextStyle(
textStyle: textStyle,
child: const Padding(
padding: EdgeInsets.all(16.0),
child: Text('Press Me'),
),
),
),
),
],
);
}
}
class WidgetStateScope extends InheritedNotifier<WidgetStatesController> {
const WidgetStateScope({
super.key,
required super.notifier,
required super.child,
});
@override
WidgetStatesController? get notifier => super.notifier;
static Set<WidgetState>? maybeOf(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<WidgetStateScope>()?.notifier?.value;
}
}
class AnimatedBoxDecoration extends BoxDecoration {
const AnimatedBoxDecoration({
this.duration = Duration.zero,
this.curve = Curves.linear,
super.backgroundBlendMode,
super.border,
super.borderRadius,
super.boxShadow,
super.gradient,
super.image,
super.color,
super.shape,
});
final Duration duration;
final Curve curve;
}
class AnimatedWidgetStatesDecoration extends StatelessWidget {
const AnimatedWidgetStatesDecoration({
super.key,
required this.child,
required this.decoration,
});
final Widget child;
final WidgetStateProperty<AnimatedBoxDecoration> decoration;
@override
Widget build(BuildContext context) {
final state = WidgetStateScope.maybeOf(context)!;
final resolvedDecoration = decoration.resolve(state);
return AnimatedContainer(
duration: resolvedDecoration.duration,
curve: resolvedDecoration.curve,
decoration: resolvedDecoration,
child: child,
);
}
}
class WidgetStatesDefaultTextStyle extends StatelessWidget {
const WidgetStatesDefaultTextStyle({
super.key,
required this.child,
required this.textStyle,
});
final Widget child;
final WidgetStateProperty<TextStyle> textStyle;
@override
Widget build(BuildContext context) {
final state = WidgetStateScope.maybeOf(context)!;
final resolvedTextStyle = textStyle.resolve(state);
return DefaultTextStyle(
style: resolvedTextStyle,
child: child,
);
}
}
class RawButton extends StatefulWidget {
const RawButton({
super.key,
this.onHover,
this.onPressed,
this.onFocusChange,
this.focusNode,
this.autofocus = false,
this.behavior = HitTestBehavior.deferToChild,
this.statesController,
required this.child,
});
final ValueChanged<bool>? onHover;
final VoidCallback? onPressed;
final ValueChanged<bool>? onFocusChange;
final FocusNode? focusNode;
final bool autofocus;
final HitTestBehavior behavior;
final WidgetStatesController? statesController;
final Widget child;
@override
State<RawButton> createState() => _RawButtonState();
}
class _RawButtonState extends State<RawButton> {
late final Map<Type, Action<Intent>> _actionMap = <Type, Action<Intent>>{
ActivateIntent: CallbackAction<ActivateIntent>(onInvoke: _handleActivation),
ButtonActivateIntent: CallbackAction<ButtonActivateIntent>(onInvoke: _handleActivation),
};
// 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!;
WidgetStatesController? _internalStatesController;
WidgetStatesController get _statesController {
return widget.statesController ?? _internalStatesController!;
}
Map<Type, GestureRecognizerFactory>? gestures;
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();
}
if (widget.statesController == null) {
_internalStatesController = WidgetStatesController();
}
isEnabled = widget.onPressed != null;
isFocused = _focusNode.hasPrimaryFocus;
}
@override
void didUpdateWidget(RawButton 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.statesController != oldWidget.statesController) {
if (widget.statesController != null) {
_internalStatesController?.dispose();
_internalStatesController = null;
} else {
assert(_internalStatesController == null);
_internalStatesController = WidgetStatesController();
}
}
if (widget.onPressed != oldWidget.onPressed) {
if (widget.onPressed == null) {
isEnabled = isHovered = isPressed = isFocused = false;
} else {
isEnabled = true;
}
}
}
@override
void dispose() {
_internalStatesController?.dispose();
_internalStatesController = null;
_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 = isFocused = false;
widget.onHover?.call(false);
}
}
// TextButton.onHover and MouseRegion.onHover can't be used without triggering
// focus on scroll.
void _handlePointerHover(PointerHoverEvent event) {
if (!isHovered) {
isHovered = true;
widget.onHover?.call(true);
}
}
@override
Widget build(BuildContext context) {
if (isEnabled) {
final DeviceGestureSettings? gestureSettings = MediaQuery.maybeGestureSettingsOf(context);
gestures ??= <Type, GestureRecognizerFactory>{
TapGestureRecognizer: GestureRecognizerFactoryWithHandlers<TapGestureRecognizer>(
() => TapGestureRecognizer(),
(TapGestureRecognizer instance) {
instance
..onTapDown = _handleTapDown
..onTapUp = _handleTapUp
..onTapCancel = _handleTapCancel
..gestureSettings = gestureSettings;
},
),
};
} else {
gestures = null;
}
return Actions(
actions: isEnabled ? _actionMap : <Type, Action<Intent>>{},
child: Focus(
autofocus: isEnabled && widget.autofocus,
focusNode: _focusNode,
canRequestFocus: isEnabled,
skipTraversal: !isEnabled,
onFocusChange: _handleFocusChange,
child: WidgetStateScope(
notifier: _statesController,
child: MouseRegion(
onExit: isEnabled ? _handlePointerExit : null,
onHover: isEnabled ? _handlePointerHover : null,
cursor: isEnabled ? MouseCursor.defer : SystemMouseCursors.forbidden,
hitTestBehavior: widget.behavior,
child: RawGestureDetector(
behavior: widget.behavior,
gestures: gestures ?? const <Type, GestureRecognizerFactory>{},
child: widget.child,
),
),
),
),
);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment