Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/refactor pressable #202

Merged
merged 7 commits into from
Feb 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 7 additions & 8 deletions lib/mix.dart
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
library mix;

// This file is generated by the update_exports.dart script.
// DO NOT MODIFY MANUALLY
/// This file is generated by the update_exports.dart script.
/// DO NOT MODIFY MANUALLY

// Some default exports
export 'src/deprecations.dart';
library mix;

// Automated file exports
export 'src/attributes/border/border_dto.dart';
Expand Down Expand Up @@ -50,6 +47,7 @@ export 'src/core/styled_widget.dart';
export 'src/decorators/widget_decorator_widget.dart';
export 'src/decorators/widget_decorators.dart';
export 'src/decorators/widget_decorators_util.dart';
export 'src/deprecations.dart';
export 'src/factory/mix_provider.dart';
export 'src/factory/mix_provider_data.dart';
export 'src/factory/style_mix.dart';
Expand Down Expand Up @@ -94,9 +92,10 @@ export 'src/utils/context_variant_util/on_brightness_util.dart';
export 'src/utils/context_variant_util/on_directionality_util.dart';
export 'src/utils/context_variant_util/on_helper_util.dart';
export 'src/utils/context_variant_util/on_orientation_util.dart';
export 'src/utils/custom_focusable_action_detector.dart';
export 'src/utils/helper_util.dart';
export 'src/utils/style_recipe.dart';
export 'src/variants/variant.dart';
export 'src/widgets/pressable/pressable_state.notifier.dart';
export 'src/widgets/pressable/pressable_data.notifier.dart';
export 'src/widgets/pressable/pressable_util.dart';
export 'src/widgets/pressable/pressable_widget.dart';
export 'src/widgets/pressable/widget_state_util.dart';
2 changes: 1 addition & 1 deletion lib/src/deprecations.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@ import '../src/utils/context_variant_util/on_helper_util.dart';
import '../src/utils/context_variant_util/on_orientation_util.dart';
import '../src/utils/helper_util.dart';
import '../src/variants/variant.dart';
import '../src/widgets/pressable/widget_state_util.dart';
import 'core/attribute.dart';
import 'factory/style_mix.dart';
import 'widgets/pressable/pressable_util.dart';

const kShortAliasDeprecation =
'Short aliases will be deprecated, you can create your own. Example: final p = padding;';
Expand Down
7 changes: 2 additions & 5 deletions lib/src/factory/mix_provider.dart
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,9 @@ class MixProvider extends InheritedWidget {
/// Retrieves the nearest [MixData] from the widget tree. Throws if not found.
static MixData of(BuildContext context) {
final mixData = maybeOf(context);
if (mixData == null) {
throw StateError('MixProvider not found in widget tree. '
'Ensure that Mix is present in the widget tree and try again.');
}
assert(mixData != null, 'MixProvider not found in widget tree.');

return mixData;
return mixData!;
}

final MixData? data;
Expand Down
7 changes: 4 additions & 3 deletions lib/src/factory/mix_provider_data.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import '../core/attribute.dart';
import '../core/attributes_map.dart';
import '../helpers/compare_mixin.dart';
import '../theme/token_resolver.dart';
import '../widgets/pressable/widget_state_util.dart';
import '../widgets/pressable/pressable_util.dart';
import 'mix_provider.dart';
import 'style_mix.dart';

Expand Down Expand Up @@ -120,15 +120,16 @@ List<StyleAttribute> applyContextToVisualAttributes(
List<WhenVariant> gestureVariantTypes = [];

for (ContextVariantAttribute attr in contextVariants) {
if (attr.variant is WidgetStateVariant) {
if (attr.variant is PressableDataVariant) {
gestureVariantTypes.add(attr);
} else {
contextVariantTypes.add(attr);
}
}

for (MultiVariantAttribute attr in multiVariants) {
if (attr.variant.variants.any((variant) => variant is WidgetStateVariant)) {
if (attr.variant.variants
.any((variant) => variant is PressableDataVariant)) {
gestureVariantTypes.add(attr);
} else {
contextVariantTypes.add(attr);
Expand Down
287 changes: 287 additions & 0 deletions lib/src/utils/custom_focusable_action_detector.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,287 @@
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';

class CustomFocusableActionDetector extends StatefulWidget {
/// Create a const [CustomFocusableActionDetector].
const CustomFocusableActionDetector({
super.key,
this.enabled = true,
this.focusNode,
this.autofocus = false,
this.descendantsAreFocusable = true,
this.descendantsAreTraversable = true,
this.shortcuts,
this.actions,
this.onShowFocusHighlight,
this.onShowHoverHighlight,
this.onFocusChange,
this.mouseCursor = MouseCursor.defer,
this.includeFocusSemantics = true,
this.onMouseEnter,
this.onMouseExit,
this.onMouseHover,
required this.child,
});

/// Is this widget enabled or not.
///
/// If disabled, will not send any notifications needed to update highlight or
/// focus state, and will not define or respond to any actions or shortcuts.
///
/// When disabled, adds [Focus] to the widget tree, but sets
/// [Focus.canRequestFocus] to false.
final bool enabled;

/// {@macro flutter.widgets.Focus.focusNode}
final FocusNode? focusNode;

/// {@macro flutter.widgets.Focus.autofocus}
final bool autofocus;

/// {@macro flutter.widgets.Focus.descendantsAreFocusable}
final bool descendantsAreFocusable;

/// {@macro flutter.widgets.Focus.descendantsAreTraversable}
final bool descendantsAreTraversable;

/// {@macro flutter.widgets.actions.actions}
final Map<Type, Action<Intent>>? actions;

/// {@macro flutter.widgets.shortcuts.shortcuts}
final Map<ShortcutActivator, Intent>? shortcuts;

/// A function that will be called when the focus highlight should be shown or
/// hidden.
///
/// This method is not triggered at the unmount of the widget.
final ValueChanged<bool>? onShowFocusHighlight;

/// A function that will be called when the hover highlight should be shown or hidden.
///
/// This method is not triggered at the unmount of the widget.
final ValueChanged<bool>? onShowHoverHighlight;

/// A function that will be called when the focus changes.
///
/// Called with true if the [focusNode] has primary focus.
final ValueChanged<bool>? onFocusChange;

/// A function that will be called when the mouse enters
///
/// Called with the [PointerEnterEvent] when the mouse enters the widget.
final void Function(PointerExitEvent event)? onMouseExit;

/// A function that will be called when the mouse exits
///
/// Called with the [PointerExitEvent] when the mouse exits the widget.
final void Function(PointerEnterEvent event)? onMouseEnter;

/// A function that will be called when the pointer hovers over the widget.
///
/// Called with the [PointerHoverEvent] when the pointer hovers over the widget.
final void Function(PointerHoverEvent event)? onMouseHover;

/// The cursor for a mouse pointer when it enters or is hovering over the
/// widget.
///
/// The [mouseCursor] defaults to [MouseCursor.defer], deferring the choice of
/// cursor to the next region behind it in hit-test order.
final MouseCursor mouseCursor;

/// Whether to include semantics from [Focus].
///
/// Defaults to true.
final bool includeFocusSemantics;

/// The child widget for this [CustomFocusableActionDetector] widget.
///
/// {@macro flutter.widgets.ProxyWidget.child}
final Widget child;

@override
State<CustomFocusableActionDetector> createState() =>
_CustomFocusableActionDetectorState();
}

class _CustomFocusableActionDetectorState
extends State<CustomFocusableActionDetector> {
// This global key is needed to keep only the necessary widgets in the tree
// while maintaining the subtree's state.
//
// See https://github.com/flutter/flutter/issues/64058 for an explanation of
// why using a global key over keeping the shape of the tree.
final GlobalKey _mouseRegionKey = GlobalKey();

@override
void initState() {
super.initState();
SchedulerBinding.instance.addPostFrameCallback((Duration duration) {
_updateHighlightMode(FocusManager.instance.highlightMode);
});
FocusManager.instance
.addHighlightModeListener(_handleFocusHighlightModeChange);
}

void _updateHighlightMode(FocusHighlightMode mode) {
_mayTriggerCallback(task: () {
switch (mode) {
case FocusHighlightMode.touch:
_canShowHighlight = false;
break;
case FocusHighlightMode.traditional:
_canShowHighlight = true;
break;
}
});
}

// Have to have this separate from the _updateHighlightMode because it gets
// called in initState, where things aren't mounted yet.
// Since this method is a highlight mode listener, it is only called
// immediately following pointer events.
void _handleFocusHighlightModeChange(FocusHighlightMode mode) {
if (!mounted) {
return;
}
_updateHighlightMode(mode);
}

void _handleMouseEnter(PointerEnterEvent event) {
if (!_hovering) {
_mayTriggerCallback(task: () {
_hovering = true;
});
widget.onMouseEnter?.call(event);
}
}

void _handleMouseExit(PointerExitEvent event) {
if (_hovering) {
_mayTriggerCallback(task: () {
_hovering = false;
});
widget.onMouseExit?.call(event);
}
}

void _handleFocusChange(bool focused) {
if (_focused != focused) {
_mayTriggerCallback(task: () {
_focused = focused;
});
widget.onFocusChange?.call(_focused);
}
}

// Record old states, do `task` if not null, then compare old states with the
// new states, and trigger callbacks if necessary.
//
// The old states are collected from `oldWidget` if it is provided, or the
// current widget (before doing `task`) otherwise. The new states are always
// collected from the current widget.
void _mayTriggerCallback({
VoidCallback? task,
CustomFocusableActionDetector? oldWidget,
}) {
bool shouldShowHoverHighlight(CustomFocusableActionDetector target) {
return _hovering && target.enabled && _canShowHighlight;
}

bool canRequestFocus(CustomFocusableActionDetector target) {
final NavigationMode mode = MediaQuery.maybeNavigationModeOf(context) ??
NavigationMode.traditional;
switch (mode) {
case NavigationMode.traditional:
return target.enabled;
case NavigationMode.directional:
return true;
}
}

bool shouldShowFocusHighlight(CustomFocusableActionDetector target) {
return _focused && _canShowHighlight && canRequestFocus(target);
}

assert(SchedulerBinding.instance.schedulerPhase !=
SchedulerPhase.persistentCallbacks);
final CustomFocusableActionDetector oldTarget = oldWidget ?? widget;
final bool didShowHoverHighlight = shouldShowHoverHighlight(oldTarget);
final bool didShowFocusHighlight = shouldShowFocusHighlight(oldTarget);
if (task != null) {
task();
}
final bool doShowHoverHighlight = shouldShowHoverHighlight(widget);
final bool doShowFocusHighlight = shouldShowFocusHighlight(widget);
if (didShowFocusHighlight != doShowFocusHighlight) {
widget.onShowFocusHighlight?.call(doShowFocusHighlight);
}
if (didShowHoverHighlight != doShowHoverHighlight) {
widget.onShowHoverHighlight?.call(doShowHoverHighlight);
}
}

@override
void dispose() {
FocusManager.instance
.removeHighlightModeListener(_handleFocusHighlightModeChange);
super.dispose();
}

@override
void didUpdateWidget(CustomFocusableActionDetector oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.enabled != oldWidget.enabled) {
SchedulerBinding.instance.addPostFrameCallback((Duration duration) {
_mayTriggerCallback(oldWidget: oldWidget);
});
}
}

bool _canShowHighlight = false;
bool _hovering = false;
bool _focused = false;
bool get _canRequestFocus {
final NavigationMode mode =
MediaQuery.maybeNavigationModeOf(context) ?? NavigationMode.traditional;
switch (mode) {
case NavigationMode.traditional:
return widget.enabled;
case NavigationMode.directional:
return true;
}
}

@override
Widget build(BuildContext context) {
Widget child = MouseRegion(
key: _mouseRegionKey,
onEnter: _handleMouseEnter,
onExit: _handleMouseExit,
onHover: widget.onMouseHover,
cursor: widget.mouseCursor,
child: Focus(
focusNode: widget.focusNode,
autofocus: widget.autofocus,
onFocusChange: _handleFocusChange,
canRequestFocus: _canRequestFocus,
descendantsAreFocusable: widget.descendantsAreFocusable,
descendantsAreTraversable: widget.descendantsAreTraversable,
includeSemantics: widget.includeFocusSemantics,
child: widget.child,
),
);
if (widget.enabled &&
widget.actions != null &&
widget.actions!.isNotEmpty) {
child = Actions(actions: widget.actions!, child: child);
}
if (widget.enabled &&
widget.shortcuts != null &&
widget.shortcuts!.isNotEmpty) {
child = Shortcuts(shortcuts: widget.shortcuts!, child: child);
}

return child;
}
}
Loading
Loading