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 1 commit
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
Prev Previous commit
Next Next commit
add hit test test; fix platform view offset and scene host init
  • Loading branch information
yjbanov committed Apr 18, 2022
commit 52076d3cb75818e181860c60b937b0d23554ab49
9 changes: 6 additions & 3 deletions lib/web_ui/lib/src/engine/canvaskit/embedded_views.dart
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,7 @@ class HtmlViewEmbedder {
}

// Apply mutators to the slot
_applyMutators(params.mutators, slot, viewId);
_applyMutators(params, slot, viewId);
}

int _countClips(MutatorsStack mutators) {
Expand Down Expand Up @@ -309,9 +309,12 @@ class HtmlViewEmbedder {
}

void _applyMutators(
MutatorsStack mutators, html.Element embeddedView, int viewId) {
EmbeddedViewParams params, html.Element embeddedView, int viewId) {
final MutatorsStack mutators = params.mutators;
html.Element head = embeddedView;
Matrix4 headTransform = Matrix4.identity();
Matrix4 headTransform = params.offset == ui.Offset.zero
? Matrix4.identity()
: Matrix4.translationValues(params.offset.dx, params.offset.dy, 0);
double embeddedOpacity = 1.0;
_resetAnchor(head);
_cleanUpClipDefs(viewId);
Expand Down
12 changes: 12 additions & 0 deletions lib/web_ui/lib/src/engine/canvaskit/surface.dart
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,18 @@ class Surface {
height: _pixelHeight,
);
this.htmlCanvas = htmlCanvas;

// The DOM elements used to render pictures are used purely to put pixels on
// the screen. They have no semantic information. If an assistive technology
// attempts to scan picture content it will look like garbage and confuse
// users. UI semantics are exported as a separate DOM tree rendered parallel
// to pictures.
//
// Why are layer and scene elements not hidden from ARIA? Because those
// elements may contain platform views, and platform views must be
// accessible.
htmlCanvas.setAttribute('aria-hidden', 'true');

htmlCanvas.style.position = 'absolute';
_updateLogicalHtmlCanvasSize();

Expand Down
27 changes: 24 additions & 3 deletions lib/web_ui/lib/src/engine/embedder.dart
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,15 @@ import 'window.dart';
/// - [semanticsHostElement], hosts the ARIA-annotated semantics tree.
class FlutterViewEmbedder {
FlutterViewEmbedder() {
reset();
assert(() {
_setupHotRestart();
return true;
}());
reset();
assert(() {
_registerHotRestartCleanUp();
return true;
}());
}

// The tag name for the root view of the flutter app (glass-pane)
Expand Down Expand Up @@ -104,6 +108,10 @@ class FlutterViewEmbedder {
static const String _staleHotRestartStore = '__flutter_state';
List<html.Element?>? _staleHotRestartState;

/// Creates a container for DOM elements that need to be cleaned up between
/// hot restarts.
///
/// If a contains already exists, reuses the existing one.
void _setupHotRestart() {
// This persists across hot restarts to clear stale DOM.
_staleHotRestartState = getJsProperty<List<html.Element?>?>(html.window, _staleHotRestartStore);
Expand All @@ -112,7 +120,12 @@ class FlutterViewEmbedder {
setJsProperty(
html.window, _staleHotRestartStore, _staleHotRestartState);
}
}

/// Registers DOM elements that need to be cleaned up before hot restarting.
///
/// [_setupHotRestart] must have been called prior to calling this method.
void _registerHotRestartCleanUp() {
registerHotRestartListener(() {
_resizeSubscription?.cancel();
_localeSubscription?.cancel();
Expand All @@ -137,7 +150,7 @@ class FlutterViewEmbedder {
/// already in the right place, skip DOM mutation. This is both faster and
/// more correct, because moving DOM nodes loses internal state, such as
/// text selection.
void renderScene(html.Element? sceneElement) {
void addSceneToSceneHost(html.Element? sceneElement) {
if (sceneElement != _sceneElement) {
_sceneElement?.remove();
_sceneElement = sceneElement;
Expand Down Expand Up @@ -278,6 +291,14 @@ class FlutterViewEmbedder {
_sceneHostElement = html.document.createElement('flt-scene-host')
..style.pointerEvents = 'none';

/// CanvasKit uses a static scene element that never gets replaced, so it's
/// added eagerly during initialization here and never touched, unless the
/// system is reset due to hot restart or in a test.
if (useCanvasKit) {
skiaSceneHost = html.Element.tag('flt-scene');
addSceneToSceneHost(skiaSceneHost);
}

final html.Element semanticsHostElement =
html.document.createElement('flt-semantics-host');
semanticsHostElement.style
Expand Down Expand Up @@ -308,7 +329,7 @@ class FlutterViewEmbedder {
]);

// When debugging semantics, make the scene semi-transparent so that the
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now that the accessibility tree renders on top of the scene host, this is probably not required anymore :P

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. Strictly speaking, no. But reducing the transparency of the scene makes the semantics overlay more visible, making it easier to work with. I'll keep the transparency, but I will update the comment.

// semantics tree is visible.
// semantics tree is more prominent.
if (configuration.debugShowSemanticsNodes) {
_sceneHostElement!.style.opacity = '0.3';
}
Expand Down
6 changes: 0 additions & 6 deletions lib/web_ui/lib/src/engine/initialization.dart
Original file line number Diff line number Diff line change
Expand Up @@ -256,12 +256,6 @@ Future<void> initializeEngineUi() async {
Keyboard.initialize(onMacOs: operatingSystem == OperatingSystem.macOs);
MouseCursor.initialize();
ensureFlutterViewEmbedderInitialized();

if (useCanvasKit) {
/// Add a Skia scene host.
skiaSceneHost = html.Element.tag('flt-scene');
flutterViewEmbedder.renderScene(skiaSceneHost);
}
_initializationState = DebugEngineInitializationState.initialized;
}

Expand Down
2 changes: 1 addition & 1 deletion lib/web_ui/lib/src/engine/platform_dispatcher.dart
Original file line number Diff line number Diff line change
Expand Up @@ -607,7 +607,7 @@ class EnginePlatformDispatcher extends ui.PlatformDispatcher {
rasterizer!.draw(layerScene.layerTree);
} else {
final SurfaceScene surfaceScene = scene as SurfaceScene;
flutterViewEmbedder.renderScene(surfaceScene.webOnlyRootElement);
flutterViewEmbedder.addSceneToSceneHost(surfaceScene.webOnlyRootElement);
}
frameTimingsOnRasterFinish();
}
Expand Down
18 changes: 15 additions & 3 deletions lib/web_ui/lib/src/engine/semantics/semantics.dart
Original file line number Diff line number Diff line change
Expand Up @@ -972,6 +972,20 @@ class SemanticsObject {

// Both non-empty case.

// Problem: child nodes have been added, removed, and/or reordered. On the
// web, many assistive technologies cannot track DOM elements
// moving around, losing focus. The best approach is to try to keep
// child elements as stable as possible.
// Solution: find all common elements in both lists and record their indices
// in the old list (in the `intersectionIndicesOld` variable). The
// longest increases subsequence provides the longest chain of
// semantics nodes that didn't move relative to each other. Those
// nodes (represented by the `stationaryIds` variable) are kept
// stationary, while all others are moved/inserted/deleted around
// them. This gives the maximum node stability, and covers most
// use-cases, including scrolling in any direction, insertions,
// deletions, drag'n'drop, etc.

// Indices into the old child list pointing at children that also exist in
// the new child list.
final List<int> intersectionIndicesOld = <int>[];
Expand All @@ -997,9 +1011,7 @@ class SemanticsObject {
// If child lists are not identical, continue computing the intersection
// between the two lists.
while (newIndex < childCount) {
for (int oldIndex = 0;
oldIndex < previousCount;
oldIndex += 1) {
for (int oldIndex = 0; oldIndex < previousCount; oldIndex += 1) {
if (previousChildrenInRenderOrder[oldIndex] ==
childrenInRenderOrder[newIndex]) {
intersectionIndicesOld.add(oldIndex);
Expand Down
28 changes: 28 additions & 0 deletions lib/web_ui/test/canvaskit/embedded_views_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,34 @@ void testMain() {
);
});

test('correctly offsets platform views', () async {
ui.platformViewRegistry.registerViewFactory(
'test-platform-view',
(int viewId) => html.DivElement()..id = 'view-0',
);
await createPlatformView(0, 'test-platform-view');

final EnginePlatformDispatcher dispatcher =
ui.window.platformDispatcher as EnginePlatformDispatcher;
final LayerSceneBuilder sb = LayerSceneBuilder();
sb.addPlatformView(0, offset: const ui.Offset(3, 4), width: 5, height: 6);
dispatcher.rasterizer!.draw(sb.build().layerTree);

final html.Element slotHost =
flutterViewEmbedder.sceneElement!.querySelector('flt-platform-view-slot')!;
final html.CssStyleDeclaration style = slotHost.style;

expect(style.transform, 'matrix(1, 0, 0, 1, 3, 4)');
expect(style.width, '5px');
expect(style.height, '6px');

final html.Rectangle<num> slotRect = slotHost.getBoundingClientRect();
expect(slotRect.left, 3);
expect(slotRect.top, 4);
expect(slotRect.right, 8);
expect(slotRect.bottom, 10);
});

// Returns the list of CSS transforms applied to the ancestor chain of
// elements starting from `viewHost`, up until and excluding <flt-scene>.
List<String> getTransformChain(html.Element viewHost) {
Expand Down
25 changes: 25 additions & 0 deletions lib/web_ui/test/canvaskit/semantics_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

@TestOn('chrome || safari || firefox')

import 'dart:async';

import 'package:test/bootstrap/browser.dart';
import 'package:test/test.dart';

import '../engine/semantics/semantics_test.dart';
import 'common.dart';

void main() {
internalBootstrapBrowserTest(() => testMain);
}

// Run the same semantics tests in CanvasKit mode because as of today we do not
// yet share platform view logic with the HTML renderer, which affects
// semantics.
Future<void> testMain() async {
setUpCanvasKitTest();
runSemanticsTests();
}
Loading