Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

Commit 2119e9d

Browse files
authored
Add the focus state related methods to the platform dispatcher (#50273)
These changes were originally landed on #49841 but reverted in 0eb7b10. I fixed the offending dart snippets and (think) they now will work without issues (I think I was able to verify them locally by manually patching my bin/cache/... copy of these files with these 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
1 parent ac27c8a commit 2119e9d

File tree

4 files changed

+251
-0
lines changed

4 files changed

+251
-0
lines changed

lib/ui/platform_dispatcher.dart

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,72 @@ 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 a [ViewFocusEvent] with [ViewFocusState.focused] and
318+
/// [ViewFocusDirection.forward].
319+
///
320+
/// Typically, receivers of this event respond by moving the focus to the first
321+
/// focusable widget inside the [FlutterView] with ID 2. If a view receives
322+
/// focus in the backwards direction (could be the result of pressing shift + tab),
323+
/// typically the last focusable widget inside that view is focused.
324+
///
325+
/// The platform may remove focus from a [FlutterView]. For example, on the web,
326+
/// the browser can move focus to another element, or to the browser's built-in UI.
327+
/// On desktop, the operating system can switch to another window (e.g. using Alt + Tab on Windows).
328+
/// In scenarios like these, [onViewFocusChange] will be invoked with [ViewFocusState.unfocused] and
329+
/// [ViewFocusDirection.undefined].
330+
///
331+
/// Receivers typically respond to this event by removing all focus indications
332+
/// from the app.
333+
///
334+
/// Apps can also programmatically request to move the focus to a desired
335+
/// [FlutterView] by calling [requestViewFocusChange].
336+
///
337+
/// The callback is invoked in the same zone in which the callback was set.
338+
///
339+
/// See also:
340+
///
341+
/// * [requestViewFocusChange] to programmatically instruct the platform to move focus to a different [FlutterView].
342+
/// * [ViewFocusState] for a list of allowed focus transitions.
343+
/// * [ViewFocusDirection] for a list of allowed focus directions.
344+
/// * [ViewFocusEvent], which is the event object provided to the callback.
345+
ViewFocusChangeCallback? get onViewFocusChange => _onViewFocusChange;
346+
ViewFocusChangeCallback? _onViewFocusChange;
347+
// ignore: unused_field, field will be used when platforms other than web use these focus APIs.
348+
Zone _onViewFocusChangeZone = Zone.root;
349+
set onViewFocusChange(ViewFocusChangeCallback? callback) {
350+
_onViewFocusChange = callback;
351+
_onViewFocusChangeZone = Zone.current;
352+
}
353+
354+
/// Requests a focus change of the [FlutterView] with ID [viewId].
355+
///
356+
/// If an app would like to request the engine to move focus, in forward direction,
357+
/// to the [FlutterView] with ID 1 it should call this method with [ViewFocusState.focused]
358+
/// and [ViewFocusDirection.forward].
359+
///
360+
/// There is no need to call this method if the view in question already has
361+
/// focus as it won't have any effect.
362+
///
363+
/// A call to this method will lead to the engine calling [onViewFocusChange]
364+
/// if the request is successfully fulfilled.
365+
///
366+
/// See also:
367+
///
368+
/// * [onViewFocusChange], a callback to subscribe to view focus change events.
369+
void requestViewFocusChange({
370+
required int viewId,
371+
required ViewFocusState state,
372+
required ViewFocusDirection direction,
373+
}) {
374+
// TODO(tugorez): implement this method. At the moment will be a no op call.
375+
}
376+
311377
/// A callback invoked when any view begins a frame.
312378
///
313379
/// A callback that is invoked to notify the application that it is an
@@ -2552,3 +2618,80 @@ class SemanticsActionEvent {
25522618
);
25532619
}
25542620
}
2621+
2622+
/// Signature for [PlatformDispatcher.onViewFocusChange].
2623+
typedef ViewFocusChangeCallback = void Function(ViewFocusEvent viewFocusEvent);
2624+
2625+
/// An event for the engine to communicate view focus changes to the app.
2626+
///
2627+
/// This value will be typically passed to the [PlatformDispatcher.onViewFocusChange]
2628+
/// callback.
2629+
final class ViewFocusEvent {
2630+
/// Creates a [ViewFocusChange].
2631+
const ViewFocusEvent({
2632+
required this.viewId,
2633+
required this.state,
2634+
required this.direction,
2635+
});
2636+
2637+
/// The ID of the [FlutterView] that experienced a focus change.
2638+
final int viewId;
2639+
2640+
/// The state focus changed to.
2641+
final ViewFocusState state;
2642+
2643+
/// The direction focus changed to.
2644+
final ViewFocusDirection direction;
2645+
2646+
@override
2647+
String toString() {
2648+
return 'ViewFocusEvent(viewId: $viewId, state: $state, direction: $direction)';
2649+
}
2650+
}
2651+
2652+
/// Represents the focus state of a given [FlutterView].
2653+
///
2654+
/// When focus is lost, the view's focus state changes to [ViewFocusState.unfocused].
2655+
///
2656+
/// When focus is gained, the view's focus state changes to [ViewFocusState.focused].
2657+
///
2658+
/// Valid transitions within a view are:
2659+
///
2660+
/// - [ViewFocusState.focused] to [ViewFocusState.unfocused].
2661+
/// - [ViewFocusState.unfocused] to [ViewFocusState.focused].
2662+
///
2663+
/// See also:
2664+
///
2665+
/// * [ViewFocusDirection], that specifies the focus direction.
2666+
/// * [ViewFocusEvent], that conveys information about a [FlutterView] focus change.
2667+
enum ViewFocusState {
2668+
/// Specifies that a view does not have platform focus.
2669+
unfocused,
2670+
2671+
/// Specifies that a view has platform focus.
2672+
focused,
2673+
}
2674+
2675+
/// Represents the direction in which the focus transitioned across [FlutterView]s.
2676+
///
2677+
/// See also:
2678+
///
2679+
/// * [ViewFocusState], that specifies the current focus state of a [FlutterView].
2680+
/// * [ViewFocusEvent], that conveys information about a [FlutterView] focus change.
2681+
enum ViewFocusDirection {
2682+
/// Indicates the focus transition did not have a direction.
2683+
///
2684+
/// This is typically associated with focus being programmatically requested or
2685+
/// when focus is lost.
2686+
undefined,
2687+
2688+
/// Indicates the focus transition was performed in a forward direction.
2689+
///
2690+
/// This is typically result of the user pressing tab.
2691+
forward,
2692+
2693+
/// Indicates the focus transition was performed in a backwards direction.
2694+
///
2695+
/// This is typically result of the user pressing shift + tab.
2696+
backwards,
2697+
}

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)