Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.
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
164 changes: 164 additions & 0 deletions lib/ui/platform_dispatcher.dart
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,93 @@ class PlatformDispatcher {
_invoke(onMetricsChanged, _onMetricsChangedZone);
}

/// A callback invoked immediately after the focus is transitioned across [FlutterView]s.
///
/// When the platform moves the focus from one [FlutterView] to another, this
/// callback is invoked indicating the new view that has focus and the direction
/// in which focus was received. For example, if focus is moved to the [FlutterView]
/// with ID 2 in the forward direction (could be the result of pressing tab)
/// the callback receives the following [ViewFocusEvent]:
///
/// ```dart
/// ViewFocusEvent(
/// viewId: 2,
/// state: ViewFocusState.focused,
/// direction: ViewFocusDirection.forward,
/// )
/// ```
///
/// Typically, receivers of this event respond by moving the focus to the first
/// focusable widget inside the [FlutterView] with ID 2. If a view receives
/// focus in the backwards direction (could be the result of pressing shift + tab),
/// typically the last focusable widget inside that view is focused.
///
/// The platform may remove focus from a [FlutterView]. For example, on the web,
/// the browser can move focus to another element, or to the browser's built-in UI.
/// On desktop, the operating system can switch to another window (e.g. using Alt + Tab on Windows).
/// In scenarios like these, [onViewFocusChange] will be invoked with an event like this:
///
/// ```dart
/// ViewFocusEvent(
/// viewId: 2,
/// state: ViewFocusState.unfocused,
/// direction: ViewFocusDirection.undefined,
/// )
/// ```
///
/// Receivers typically respond to this event by removing all focus indications
/// from the app.
///
/// Apps can also programmatically request to move the focus to a desired
/// [FlutterView] by calling [requestViewFocusChange].
///
/// The callback is invoked in the same zone in which the callback was set.
///
/// See also:
///
/// * [requestViewFocusChange] to programmatically instruct the platform to move focus to a different [FlutterView].
/// * [ViewFocusState] for a list of allowed focus transitions.
/// * [ViewFocusDirection] for a list of allowed focus directions.
/// * [ViewFocusEvent], which is the event object provided to the callback.
ViewFocusChangeCallback? get onViewFocusChange => _onViewFocusChange;
ViewFocusChangeCallback? _onViewFocusChange;
// ignore: unused_field, field will be used when platforms other than web use these focus APIs.
Zone _onViewFocusChangeZone = Zone.root;
set onViewFocusChange(ViewFocusChangeCallback? callback) {
_onViewFocusChange = callback;
_onViewFocusChangeZone = Zone.current;
}

/// Requests a focus change of the [FlutterView] with ID [viewId].
///
/// If an app would like to request the engine to move focus, in forward direction,
/// to the [FlutterView] with ID 1 the following call should be made:
///
/// ```dart
/// PlatformDispatcher.instance.requestViewFocusChange(
/// viewId: 1,
/// state: ViewFocusSate.focused,
/// direction: ViewFocusDirection.forward,
/// );
/// ```
///
/// There is no need to call this method if the view in question already has
/// focus as it won't have any effect.
///
/// A call to this method will lead to the engine calling [onViewFocusChange]
/// if the request is successfully fulfilled.
///
/// See also:
///
/// * [onViewFocusChange], a callback to subscribe to view focus change events.
void requestViewFocusChange({
required int viewId,
required ViewFocusState state,
required ViewFocusDirection direction,
}) {
// TODO(tugorez): implement this method. At the moment will be a no op call.
}

/// A callback invoked when any view begins a frame.
///
/// A callback that is invoked to notify the application that it is an
Expand Down Expand Up @@ -2552,3 +2639,80 @@ class SemanticsActionEvent {
);
}
}

/// Signature for [PlatformDispatcher.onViewFocusChange].
typedef ViewFocusChangeCallback = void Function(ViewFocusEvent viewFocusEvent);

/// An event for the engine to communicate view focus changes to the app.
///
/// This value will be typically passed to the [PlatformDispatcher.onViewFocusChange]
/// callback.
final class ViewFocusEvent {
/// Creates a [ViewFocusChange].
const ViewFocusEvent({
required this.viewId,
required this.state,
required this.direction,
});

/// The ID of the [FlutterView] that experienced a focus change.
final int viewId;

/// The state focus changed to.
final ViewFocusState state;

/// The direction focus changed to.
final ViewFocusDirection direction;

@override
String toString() {
return 'ViewFocusEvent(viewId: $viewId, state: $state, direction: $direction)';
}
}

/// Represents the focus state of a given [FlutterView].
///
/// When focus is lost, the view's focus state changes to [ViewFocusState.unfocused].
///
/// When focus is gained, the view's focus state changes to [ViewFocusState.focused].
///
/// Valid transitions within a view are:
///
/// - [ViewFocusState.focused] to [ViewFocusState.unfocused].
/// - [ViewFocusState.unfocused] to [ViewFocusState.focused].
///
/// See also:
///
/// * [ViewFocusDirection], that specifies the focus direction.
/// * [ViewFocusEvent], that conveys information about a [FlutterView] focus change.
enum ViewFocusState {
/// Specifies that a view does not have platform focus.
unfocused,

/// Specifies that a view has platform focus.
focused,
}

/// Represents the direction in which the focus transitioned across [FlutterView]s.
///
/// See also:
///
/// * [ViewFocusState], that specifies the current focus state of a [FlutterView].
/// * [ViewFocusEvent], that conveys information about a [FlutterView] focus change.
enum ViewFocusDirection {
/// Indicates the focus transition did not have a direction.
///
/// This is typically associated with focus being programmatically requested or
/// when focus is lost.
undefined,

/// Indicates the focus transition was performed in a forward direction.
///
/// This is typically result of the user pressing tab.
forward,

/// Indicates the focus transition was performed in a backwards direction.
///
/// This is typically result of the user pressing shift + tab.
backwards,
}
40 changes: 40 additions & 0 deletions lib/web_ui/lib/platform_dispatcher.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
part of ui;

typedef VoidCallback = void Function();
typedef ViewFocusChangeCallback = void Function(ViewFocusEvent viewFocusEvent);
typedef FrameCallback = void Function(Duration duration);
typedef TimingsCallback = void Function(List<FrameTiming> timings);
typedef PointerDataPacketCallback = void Function(PointerDataPacket packet);
Expand Down Expand Up @@ -40,6 +41,15 @@ abstract class PlatformDispatcher {
VoidCallback? get onMetricsChanged;
set onMetricsChanged(VoidCallback? callback);

ViewFocusChangeCallback? get onViewFocusChange;
set onViewFocusChange(ViewFocusChangeCallback? callback);

void requestViewFocusChange({
required int viewId,
required ViewFocusState state,
required ViewFocusDirection direction,
});

FrameCallback? get onBeginFrame;
set onBeginFrame(FrameCallback? callback);

Expand Down Expand Up @@ -549,3 +559,33 @@ class SemanticsActionEvent {
@override
String toString() => 'SemanticsActionEvent($type, view: $viewId, node: $nodeId)';
}

final class ViewFocusEvent {
const ViewFocusEvent({
required this.viewId,
required this.state,
required this.direction,
});

final int viewId;

final ViewFocusState state;

final ViewFocusDirection direction;

@override
String toString() {
return 'ViewFocusEvent(viewId: $viewId, state: $state, direction: $direction)';
}
}

enum ViewFocusState {
unfocused,
focused,
}

enum ViewFocusDirection {
undefined,
forward,
backwards,
}
31 changes: 31 additions & 0 deletions lib/web_ui/lib/src/engine/platform_dispatcher.dart
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,37 @@ class EnginePlatformDispatcher extends ui.PlatformDispatcher {
}
}

@override
ui.ViewFocusChangeCallback? get onViewFocusChange => _onViewFocusChange;
ui.ViewFocusChangeCallback? _onViewFocusChange;
Zone? _onViewFocusChangeZone;
@override
set onViewFocusChange(ui.ViewFocusChangeCallback? callback) {
_onViewFocusChange = callback;
_onViewFocusChangeZone = Zone.current;
}

// Engine code should use this method instead of the callback directly.
// Otherwise zones won't work properly.
void invokeOnViewFocusChange(ui.ViewFocusEvent viewFocusEvent) {
invoke1<ui.ViewFocusEvent>(
_onViewFocusChange,
_onViewFocusChangeZone,
viewFocusEvent,
);
}


@override
void requestViewFocusChange({
required int viewId,
required ui.ViewFocusState state,
required ui.ViewFocusDirection direction,
}) {
// TODO(tugorez): implement this method. At the moment will be a no op call.
}


/// A set of views which have rendered in the current `onBeginFrame` or
/// `onDrawFrame` scope.
Set<ui.FlutterView>? _viewsRenderedInCurrentFrame;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,43 @@ void testMain() {
expect(onMetricsChangedCalled, isFalse);
expect(view1.isDisposed, isTrue);
});

test('invokeOnViewFocusChange calls onViewFocusChange', () {
final EnginePlatformDispatcher dispatcher = EnginePlatformDispatcher();
final List<ui.ViewFocusEvent> dispatchedViewFocusEvents = <ui.ViewFocusEvent>[];
const ui.ViewFocusEvent viewFocusEvent = ui.ViewFocusEvent(
viewId: 0,
state: ui.ViewFocusState.focused,
direction: ui.ViewFocusDirection.undefined,
);

dispatcher.onViewFocusChange = dispatchedViewFocusEvents.add;
dispatcher.invokeOnViewFocusChange(viewFocusEvent);

expect(dispatchedViewFocusEvents, hasLength(1));
expect(dispatchedViewFocusEvents.single, viewFocusEvent);
});

test('invokeOnViewFocusChange preserves the zone', () {
final EnginePlatformDispatcher dispatcher = EnginePlatformDispatcher();
final Zone zone1 = Zone.current.fork();
final Zone zone2 = Zone.current.fork();
const ui.ViewFocusEvent viewFocusEvent = ui.ViewFocusEvent(
viewId: 0,
state: ui.ViewFocusState.focused,
direction: ui.ViewFocusDirection.undefined,
);

zone1.runGuarded(() {
dispatcher.onViewFocusChange = (_) {
expect(Zone.current, zone1);
};
});

zone2.runGuarded(() {
dispatcher.invokeOnViewFocusChange(viewFocusEvent);
});
});
});
}

Expand Down