Skip to content

Commit 1312954

Browse files
authored
Add the focus state related methods to the platform dispatcher (flutter#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#141711 * Focus in web multiview: 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
1 parent 12adbbb commit 1312954

4 files changed

Lines changed: 272 additions & 0 deletions

File tree

lib/ui/platform_dispatcher.dart

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,93 @@ class PlatformDispatcher {
308308
_invoke(onMetricsChanged, _onMetricsChangedZone);
309309
}
310310

311+
/// A callback invoked immediately after the focus is transitioned across [FlutterView]s.
312+
///
313+
/// When the platform moves the focus from one [FlutterView] to another, this
314+
/// callback is invoked indicating the new view that has focus and the direction
315+
/// in which focus was received. For example, if focus is moved to the [FlutterView]
316+
/// with ID 2 in the forward direction (could be the result of pressing tab)
317+
/// the callback receives the following [ViewFocusEvent]:
318+
///
319+
/// ```dart
320+
/// ViewFocusEvent(
321+
/// viewId: 2,
322+
/// state: ViewFocusState.focused,
323+
/// direction: ViewFocusDirection.forward,
324+
/// )
325+
/// ```
326+
///
327+
/// Typically, receivers of this event respond by moving the focus to the first
328+
/// focusable widget inside the [FlutterView] with ID 2. If a view receives
329+
/// focus in the backwards direction (could be the result of pressing shift + tab),
330+
/// typically the last focusable widget inside that view is focused.
331+
///
332+
/// The platform may remove focus from a [FlutterView]. For example, on the web,
333+
/// the browser can move focus to another element, or to the browser's built-in UI.
334+
/// On desktop, the operating system can switch to another window (e.g. using Alt + Tab on Windows).
335+
/// In scenarios like these, [onViewFocusChange] will be invoked with an event like this:
336+
///
337+
/// ```dart
338+
/// ViewFocusEvent(
339+
/// viewId: 2,
340+
/// state: ViewFocusState.unfocused,
341+
/// direction: ViewFocusDirection.undefined,
342+
/// )
343+
/// ```
344+
///
345+
/// Receivers typically respond to this event by removing all focus indications
346+
/// from the app.
347+
///
348+
/// Apps can also programmatically request to move the focus to a desired
349+
/// [FlutterView] by calling [requestViewFocusChange].
350+
///
351+
/// The callback is invoked in the same zone in which the callback was set.
352+
///
353+
/// See also:
354+
///
355+
/// * [requestViewFocusChange] to programmatically instruct the platform to move focus to a different [FlutterView].
356+
/// * [ViewFocusState] for a list of allowed focus transitions.
357+
/// * [ViewFocusDirection] for a list of allowed focus directions.
358+
/// * [ViewFocusEvent], which is the event object provided to the callback.
359+
ViewFocusChangeCallback? get onViewFocusChange => _onViewFocusChange;
360+
ViewFocusChangeCallback? _onViewFocusChange;
361+
// ignore: unused_field, field will be used when platforms other than web use these focus APIs.
362+
Zone _onViewFocusChangeZone = Zone.root;
363+
set onViewFocusChange(ViewFocusChangeCallback? callback) {
364+
_onViewFocusChange = callback;
365+
_onViewFocusChangeZone = Zone.current;
366+
}
367+
368+
/// Requests a focus change of the [FlutterView] with ID [viewId].
369+
///
370+
/// If an app would like to request the engine to move focus, in forward direction,
371+
/// to the [FlutterView] with ID 1 the following call should be made:
372+
///
373+
/// ```dart
374+
/// PlatformDispatcher.instance.requestViewFocusChange(
375+
/// viewId: 1,
376+
/// state: ViewFocusSate.focused,
377+
/// direction: ViewFocusDirection.forward,
378+
/// );
379+
/// ```
380+
///
381+
/// There is no need to call this method if the view in question already has
382+
/// focus as it won't have any effect.
383+
///
384+
/// A call to this method will lead to the engine calling [onViewFocusChange]
385+
/// if the request is successfully fulfilled.
386+
///
387+
/// See also:
388+
///
389+
/// * [onViewFocusChange], a callback to subscribe to view focus change events.
390+
void requestViewFocusChange({
391+
required int viewId,
392+
required ViewFocusState state,
393+
required ViewFocusDirection direction,
394+
}) {
395+
// TODO(tugorez): implement this method. At the moment will be a no op call.
396+
}
397+
311398
/// A callback invoked when any view begins a frame.
312399
///
313400
/// A callback that is invoked to notify the application that it is an
@@ -2552,3 +2639,80 @@ class SemanticsActionEvent {
25522639
);
25532640
}
25542641
}
2642+
2643+
/// Signature for [PlatformDispatcher.onViewFocusChange].
2644+
typedef ViewFocusChangeCallback = void Function(ViewFocusEvent viewFocusEvent);
2645+
2646+
/// An event for the engine to communicate view focus changes to the app.
2647+
///
2648+
/// This value will be typically passed to the [PlatformDispatcher.onViewFocusChange]
2649+
/// callback.
2650+
final class ViewFocusEvent {
2651+
/// Creates a [ViewFocusChange].
2652+
const ViewFocusEvent({
2653+
required this.viewId,
2654+
required this.state,
2655+
required this.direction,
2656+
});
2657+
2658+
/// The ID of the [FlutterView] that experienced a focus change.
2659+
final int viewId;
2660+
2661+
/// The state focus changed to.
2662+
final ViewFocusState state;
2663+
2664+
/// The direction focus changed to.
2665+
final ViewFocusDirection direction;
2666+
2667+
@override
2668+
String toString() {
2669+
return 'ViewFocusEvent(viewId: $viewId, state: $state, direction: $direction)';
2670+
}
2671+
}
2672+
2673+
/// Represents the focus state of a given [FlutterView].
2674+
///
2675+
/// When focus is lost, the view's focus state changes to [ViewFocusState.unfocused].
2676+
///
2677+
/// When focus is gained, the view's focus state changes to [ViewFocusState.focused].
2678+
///
2679+
/// Valid transitions within a view are:
2680+
///
2681+
/// - [ViewFocusState.focused] to [ViewFocusState.unfocused].
2682+
/// - [ViewFocusState.unfocused] to [ViewFocusState.focused].
2683+
///
2684+
/// See also:
2685+
///
2686+
/// * [ViewFocusDirection], that specifies the focus direction.
2687+
/// * [ViewFocusEvent], that conveys information about a [FlutterView] focus change.
2688+
enum ViewFocusState {
2689+
/// Specifies that a view does not have platform focus.
2690+
unfocused,
2691+
2692+
/// Specifies that a view has platform focus.
2693+
focused,
2694+
}
2695+
2696+
/// Represents the direction in which the focus transitioned across [FlutterView]s.
2697+
///
2698+
/// See also:
2699+
///
2700+
/// * [ViewFocusState], that specifies the current focus state of a [FlutterView].
2701+
/// * [ViewFocusEvent], that conveys information about a [FlutterView] focus change.
2702+
enum ViewFocusDirection {
2703+
/// Indicates the focus transition did not have a direction.
2704+
///
2705+
/// This is typically associated with focus being programmatically requested or
2706+
/// when focus is lost.
2707+
undefined,
2708+
2709+
/// Indicates the focus transition was performed in a forward direction.
2710+
///
2711+
/// This is typically result of the user pressing tab.
2712+
forward,
2713+
2714+
/// Indicates the focus transition was performed in a backwards direction.
2715+
///
2716+
/// This is typically result of the user pressing shift + tab.
2717+
backwards,
2718+
}

lib/web_ui/lib/platform_dispatcher.dart

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
part of ui;
66

77
typedef VoidCallback = void Function();
8+
typedef ViewFocusChangeCallback = void Function(ViewFocusEvent viewFocusEvent);
89
typedef FrameCallback = void Function(Duration duration);
910
typedef TimingsCallback = void Function(List<FrameTiming> timings);
1011
typedef PointerDataPacketCallback = void Function(PointerDataPacket packet);
@@ -40,6 +41,15 @@ abstract class PlatformDispatcher {
4041
VoidCallback? get onMetricsChanged;
4142
set onMetricsChanged(VoidCallback? callback);
4243

44+
ViewFocusChangeCallback? get onViewFocusChange;
45+
set onViewFocusChange(ViewFocusChangeCallback? callback);
46+
47+
void requestViewFocusChange({
48+
required int viewId,
49+
required ViewFocusState state,
50+
required ViewFocusDirection direction,
51+
});
52+
4353
FrameCallback? get onBeginFrame;
4454
set onBeginFrame(FrameCallback? callback);
4555

@@ -549,3 +559,33 @@ class SemanticsActionEvent {
549559
@override
550560
String toString() => 'SemanticsActionEvent($type, view: $viewId, node: $nodeId)';
551561
}
562+
563+
final class ViewFocusEvent {
564+
const ViewFocusEvent({
565+
required this.viewId,
566+
required this.state,
567+
required this.direction,
568+
});
569+
570+
final int viewId;
571+
572+
final ViewFocusState state;
573+
574+
final ViewFocusDirection direction;
575+
576+
@override
577+
String toString() {
578+
return 'ViewFocusEvent(viewId: $viewId, state: $state, direction: $direction)';
579+
}
580+
}
581+
582+
enum ViewFocusState {
583+
unfocused,
584+
focused,
585+
}
586+
587+
enum ViewFocusDirection {
588+
undefined,
589+
forward,
590+
backwards,
591+
}

lib/web_ui/lib/src/engine/platform_dispatcher.dart

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,37 @@ class EnginePlatformDispatcher extends ui.PlatformDispatcher {
217217
}
218218
}
219219

220+
@override
221+
ui.ViewFocusChangeCallback? get onViewFocusChange => _onViewFocusChange;
222+
ui.ViewFocusChangeCallback? _onViewFocusChange;
223+
Zone? _onViewFocusChangeZone;
224+
@override
225+
set onViewFocusChange(ui.ViewFocusChangeCallback? callback) {
226+
_onViewFocusChange = callback;
227+
_onViewFocusChangeZone = Zone.current;
228+
}
229+
230+
// Engine code should use this method instead of the callback directly.
231+
// Otherwise zones won't work properly.
232+
void invokeOnViewFocusChange(ui.ViewFocusEvent viewFocusEvent) {
233+
invoke1<ui.ViewFocusEvent>(
234+
_onViewFocusChange,
235+
_onViewFocusChangeZone,
236+
viewFocusEvent,
237+
);
238+
}
239+
240+
241+
@override
242+
void requestViewFocusChange({
243+
required int viewId,
244+
required ui.ViewFocusState state,
245+
required ui.ViewFocusDirection direction,
246+
}) {
247+
// TODO(tugorez): implement this method. At the moment will be a no op call.
248+
}
249+
250+
220251
/// A set of views which have rendered in the current `onBeginFrame` or
221252
/// `onDrawFrame` scope.
222253
Set<ui.FlutterView>? _viewsRenderedInCurrentFrame;

lib/web_ui/test/engine/platform_dispatcher/platform_dispatcher_test.dart

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -365,6 +365,43 @@ void testMain() {
365365
expect(onMetricsChangedCalled, isFalse);
366366
expect(view1.isDisposed, isTrue);
367367
});
368+
369+
test('invokeOnViewFocusChange calls onViewFocusChange', () {
370+
final EnginePlatformDispatcher dispatcher = EnginePlatformDispatcher();
371+
final List<ui.ViewFocusEvent> dispatchedViewFocusEvents = <ui.ViewFocusEvent>[];
372+
const ui.ViewFocusEvent viewFocusEvent = ui.ViewFocusEvent(
373+
viewId: 0,
374+
state: ui.ViewFocusState.focused,
375+
direction: ui.ViewFocusDirection.undefined,
376+
);
377+
378+
dispatcher.onViewFocusChange = dispatchedViewFocusEvents.add;
379+
dispatcher.invokeOnViewFocusChange(viewFocusEvent);
380+
381+
expect(dispatchedViewFocusEvents, hasLength(1));
382+
expect(dispatchedViewFocusEvents.single, viewFocusEvent);
383+
});
384+
385+
test('invokeOnViewFocusChange preserves the zone', () {
386+
final EnginePlatformDispatcher dispatcher = EnginePlatformDispatcher();
387+
final Zone zone1 = Zone.current.fork();
388+
final Zone zone2 = Zone.current.fork();
389+
const ui.ViewFocusEvent viewFocusEvent = ui.ViewFocusEvent(
390+
viewId: 0,
391+
state: ui.ViewFocusState.focused,
392+
direction: ui.ViewFocusDirection.undefined,
393+
);
394+
395+
zone1.runGuarded(() {
396+
dispatcher.onViewFocusChange = (_) {
397+
expect(Zone.current, zone1);
398+
};
399+
});
400+
401+
zone2.runGuarded(() {
402+
dispatcher.invokeOnViewFocusChange(viewFocusEvent);
403+
});
404+
});
368405
});
369406
}
370407

0 commit comments

Comments
 (0)