Skip to content

Commit

Permalink
Add the focus state related methods to the platform dispatcher (flutt…
Browse files Browse the repository at this point in the history
…er#49841)

This change augments the platform dispatcher to allow the engine <===>
framework to communicate flutter and platform focus changes.

Relevant Issues are:
* Design doc link:
https://github.com/flutter/website/actions/runs/7560898849/job/20588395967
* Design doc: flutter/flutter#141711
* Focus in web multiview:
flutter/flutter#137443

## Pre-launch Checklist

- [x] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [x] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [x] I read and followed the [Flutter Style Guide] and the [C++,
Objective-C, Java style guides].
- [x] I listed at least one issue that this PR fixes in the description
above.
- [x] I added new tests to check the change I am making or feature I am
adding, or the PR is [test-exempt]. See [testing the engine] for
instructions on writing and running engine tests.
- [x] I updated/added relevant documentation (doc comments with `///`).
- [x] I signed the [CLA].
- [x] All existing and new tests are passing.

If you need help, consider asking for advice on the #hackers-new channel
on [Discord].

<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/wiki/Tree-hygiene#overview
[Tree Hygiene]: https://github.com/flutter/flutter/wiki/Tree-hygiene
[test-exempt]:
https://github.com/flutter/flutter/wiki/Tree-hygiene#tests
[Flutter Style Guide]:
https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo
[C++, Objective-C, Java style guides]:
https://github.com/flutter/engine/blob/main/CONTRIBUTING.md#style
[testing the engine]:
https://github.com/flutter/flutter/wiki/Testing-the-engine
[CLA]: https://cla.developers.google.com/
[flutter/tests]: https://github.com/flutter/tests
[breaking change policy]:
https://github.com/flutter/flutter/wiki/Tree-hygiene#handling-breaking-changes
[Discord]: https://github.com/flutter/flutter/wiki/Chat
  • Loading branch information
tugorez authored Feb 1, 2024
1 parent 12adbbb commit 1312954
Show file tree
Hide file tree
Showing 4 changed files with 272 additions and 0 deletions.
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

0 comments on commit 1312954

Please sign in to comment.