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
33 changes: 21 additions & 12 deletions lib/web_ui/lib/src/engine/canvas_pool.dart
Original file line number Diff line number Diff line change
Expand Up @@ -76,13 +76,6 @@ class CanvasPool extends _SaveStackTracking {
translate(transform.dx, transform.dy);
}

/// Returns true if no canvas has been allocated yet.
bool get isEmpty => _canvas == null;

/// Returns true if a canvas has been allocated for use.
bool get isNotEmpty => _canvas != null;


/// Returns [CanvasRenderingContext2D] api to draw into this canvas.
html.CanvasRenderingContext2D get context {
html.CanvasRenderingContext2D? ctx = _context;
Expand All @@ -106,12 +99,28 @@ class CanvasPool extends _SaveStackTracking {
return _contextHandle!;
}

/// Prevents active canvas to be used for rendering and prepares a new
/// canvas allocation on next drawing request that will require one.
/// Returns true if a canvas is currently available for drawing.
///
/// Calling [contextHandle] or, transitively, any of the `draw*` methods while
/// this returns true will reuse the existing canvas. Otherwise, a new canvas
/// will be allocated.
///
/// Previously allocated and closed canvases (see [closeCanvas]) are not
/// considered by this getter.
bool get hasCanvas => _canvas != null;

/// Stops the currently available canvas from receiving any further drawing
/// commands.
///
/// After calling this method, a subsequent call to [contextHandle] or,
/// transitively, any of the `draw*` methods will cause a new canvas to be
/// allocated.
///
/// Saves current canvas so we can dispose
/// and replay the clip/transform stack on top of new canvas.
void closeCurrentCanvas() {
/// The closed canvas becomes an "active" canvas, that is a canvas that's used
/// to render picture content in the current frame. Active canvases may be
/// reused in other pictures if their contents are no longer needed for this
/// picture.
void closeCanvas() {
assert(_rootElement != null);
// Place clean copy of current canvas with context stack restored and paint
// reset into pool.
Expand Down
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
69 changes: 49 additions & 20 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 @@ -83,7 +87,7 @@ class FlutterViewEmbedder {
/// This element is created and inserted in the HTML DOM once. It is never
/// removed or moved.
///
/// We render semantics inside the glasspane for proper focus and event
/// Render semantics inside the glasspane for proper focus and event
/// handling. If semantics is behind the glasspane, the phone will disable
/// focusing by touch, only by tabbing around the UI. If semantics is in
/// front of glasspane, then DOM event won't bubble up to the glasspane so
Expand All @@ -99,11 +103,15 @@ class FlutterViewEmbedder {
html.Element? _sceneElement;

/// This is state persistent across hot restarts that indicates what
/// to clear. We delay removal of old visible state to make the
/// to clear. Delay removal of old visible state to make the
/// transition appear smooth.
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 @@ -133,11 +146,11 @@ class FlutterViewEmbedder {
}
}

/// We don't want to unnecessarily move DOM nodes around. If a DOM node is
/// Don't unnecessarily move DOM nodes around. If a DOM node is
/// 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 @@ -203,15 +216,15 @@ class FlutterViewEmbedder {
setElementStyle(bodyElement, 'padding', '0');
setElementStyle(bodyElement, 'margin', '0');

// TODO(yjbanov): fix this when we support KVM I/O. Currently we scroll
// TODO(yjbanov): fix this when KVM I/O support is added. Currently scroll
// using drag, and text selection interferes.
setElementStyle(bodyElement, 'user-select', 'none');
setElementStyle(bodyElement, '-webkit-user-select', 'none');
setElementStyle(bodyElement, '-ms-user-select', 'none');
setElementStyle(bodyElement, '-moz-user-select', 'none');

// This is required to prevent the browser from doing any native touch
// handling. If we don't do this, the browser doesn't report 'pointermove'
// handling. If this is not done, the browser doesn't report 'pointermove'
// events properly.
setElementStyle(bodyElement, 'touch-action', 'none');

Expand All @@ -227,7 +240,7 @@ class FlutterViewEmbedder {
for (final html.Element viewportMeta
in html.document.head!.querySelectorAll('meta[name="viewport"]')) {
if (assertionsEnabled) {
// Filter out the meta tag that we ourselves placed on the page. This is
// Filter out the meta tag that the engine placed on the page. This is
// to avoid UI flicker during hot restart. Hot restart will clean up the
// old meta tag synchronously with the first post-restart frame.
if (!viewportMeta.hasAttribute('flt-viewport')) {
Expand Down Expand Up @@ -265,7 +278,8 @@ class FlutterViewEmbedder {
..bottom = '0'
..left = '0';

// This must be appended to the body, so we can create a host node properly.
// This must be appended to the body, so the engine can create a host node
// properly.
bodyElement.append(glassPaneElement);

// Create a [HostNode] under the glass pane element, and attach everything
Expand All @@ -277,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 All @@ -290,25 +312,31 @@ class FlutterViewEmbedder {
.prepareAccessibilityPlaceholder();

glassPaneElementHostNode.nodes.addAll(<html.Node>[
semanticsHostElement,
_accessibilityPlaceholder,
_sceneHostElement!,

// The semantic host goes last because hit-test order-wise it must be
// first. If semantics goes under the scene host, platform views will
// obscure semantic elements.
//
// You may be wondering: wouldn't semantics obscure platform views and
// make then not accessible? At least with some careful planning, that
// should not be the case. The semantics tree makes all of its non-leaf
// elements transparent. This way, if a platform view appears among other
// interactive Flutter widgets, as long as those widgets do not intersect
// with the platform view, the platform view will be reachable.
semanticsHostElement,
]);

// When debugging semantics, make the scene semi-transparent so that the
// semantics tree is visible.
// semantics tree is more prominent.
if (configuration.debugShowSemanticsNodes) {
_sceneHostElement!.style.opacity = '0.3';
}

PointerBinding.initInstance(glassPaneElement);
KeyboardBinding.initInstance(glassPaneElement);

// Hide the DOM nodes used to render the scene from accessibility, because
// the accessibility tree is built from the SemanticsNode tree as a parallel
// DOM tree.
_sceneHostElement!.setAttribute('aria-hidden', 'true');

if (html.window.visualViewport == null && isWebKit) {
// Older Safari versions sometimes give us bogus innerWidth/innerHeight
// values when the page loads. When it changes the values to correct ones
Expand All @@ -321,10 +349,11 @@ class FlutterViewEmbedder {
//
// VisualViewport API is not enabled in Firefox as well. On the other hand
// Firefox returns correct values for innerHeight, innerWidth.
// Firefox also triggers html.window.onResize therefore we don't need this
// timer to be set up for Firefox.
// Firefox also triggers html.window.onResize therefore this timer does
// not need to be set up for Firefox.
final int initialInnerWidth = html.window.innerWidth!;
// Counts how many times we checked screen size. We check up to 5 times.
// Counts how many times screen size was checked. It is checked up to 5
// times.
int checkCount = 0;
Timer.periodic(const Duration(milliseconds: 100), (Timer t) {
checkCount += 1;
Expand Down Expand Up @@ -361,7 +390,7 @@ class FlutterViewEmbedder {
}

/// The framework specifies semantics in physical pixels, but CSS uses
/// logical pixels. To compensate, we inject an inverse scale at the root
/// logical pixels. To compensate, an inverse scale is injected at the root
/// level.
void updateSemanticsScreenProperties() {
_semanticsHostElement!.style.transform =
Expand Down
44 changes: 26 additions & 18 deletions lib/web_ui/lib/src/engine/html/bitmap_canvas.dart
Original file line number Diff line number Diff line change
Expand Up @@ -370,7 +370,7 @@ class BitmapCanvas extends EngineCanvas {
_renderStrategy.isInsideSvgFilterTree ||
(_preserveImageData == false && _contains3dTransform) ||
(_childOverdraw &&
_canvasPool.isEmpty &&
!_canvasPool.hasCanvas &&
paint.maskFilter == null &&
paint.shader == null &&
paint.style != ui.PaintingStyle.stroke);
Expand All @@ -384,7 +384,7 @@ class BitmapCanvas extends EngineCanvas {
((_childOverdraw ||
_renderStrategy.hasImageElements ||
_renderStrategy.hasParagraphs) &&
_canvasPool.isEmpty &&
!_canvasPool.hasCanvas &&
paint.maskFilter == null &&
paint.shader == null);

Expand Down Expand Up @@ -469,7 +469,7 @@ class BitmapCanvas extends EngineCanvas {
element.style.mixBlendMode = blendModeToCssMixBlendMode(blendMode) ?? '';
}
// Switch to preferring DOM from now on, and close the current canvas.
_closeCurrentCanvas();
_closeCanvas();
}

@override
Expand Down Expand Up @@ -626,7 +626,7 @@ class BitmapCanvas extends EngineCanvas {
_applyTargetSize(
imageElement, image.width.toDouble(), image.height.toDouble());
}
_closeCurrentCanvas();
_closeCanvas();
}

html.ImageElement _reuseOrCreateImage(HtmlImage htmlImage) {
Expand Down Expand Up @@ -770,7 +770,7 @@ class BitmapCanvas extends EngineCanvas {
restore();
}
}
_closeCurrentCanvas();
_closeCanvas();
}

void _applyTargetSize(
Expand Down Expand Up @@ -882,8 +882,8 @@ class BitmapCanvas extends EngineCanvas {
// |--- <img>
// Any drawing operations after these tags should allocate a new canvas,
// instead of drawing into earlier canvas.
void _closeCurrentCanvas() {
_canvasPool.closeCurrentCanvas();
void _closeCanvas() {
_canvasPool.closeCanvas();
_childOverdraw = true;
_cachedLastCssFont = null;
}
Expand Down Expand Up @@ -939,16 +939,24 @@ class BitmapCanvas extends EngineCanvas {
void drawParagraph(CanvasParagraph paragraph, ui.Offset offset) {
assert(paragraph.isLaidOut);

/// - paragraph.drawOnCanvas checks that the text styling doesn't include
/// features that prevent text from being rendered correctly using canvas.
/// - _childOverdraw check prevents sandwitching multiple canvas elements
/// when we have alternating paragraphs and other drawing commands that are
/// suitable for canvas.
/// - To make sure an svg filter is applied correctly to paragraph we
/// check isInsideSvgFilterTree to make sure dom node doesn't have any
/// parents that apply one.
if (paragraph.drawOnCanvas && _childOverdraw == false &&
!_renderStrategy.isInsideSvgFilterTree) {
// Normally, text is composited as a plain HTML <p> tag. However, if a
// bitmap canvas was used for a preceding drawing command, then it's more
// efficient to continue compositing into the existing canvas, if possible.
// Whether it's possible to composite a paragraph into a 2D canvas depends
// on the following:
final bool canCompositeIntoBitmapCanvas =
// Cannot composite if the paragraph cannot be drawn into bitmap canvas
// in the first place.
paragraph.canDrawOnCanvas &&
// Cannot composite if there's no bitmap canvas to composite into.
// Creating a new bitmap canvas just to draw text doesn't make sense.
_canvasPool.hasCanvas &&
!_childOverdraw &&
// Bitmap canvas introduces correctness issues in the presence of SVG
// filters, so prefer plain HTML in this case.
!_renderStrategy.isInsideSvgFilterTree;

if (canCompositeIntoBitmapCanvas) {
paragraph.paint(this, offset);
return;
}
Expand Down Expand Up @@ -977,7 +985,7 @@ class BitmapCanvas extends EngineCanvas {
paragraphElement.style
..left = '0px'
..top = '0px';
_closeCurrentCanvas();
_closeCanvas();
}

/// Draws vertices on a gl context.
Expand Down
15 changes: 14 additions & 1 deletion lib/web_ui/lib/src/engine/html/picture.dart
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,20 @@ class PersistedPicture extends PersistedLeafSurface {

@override
html.Element createElement() {
return defaultCreateElement('flt-picture');
final html.Element element = defaultCreateElement('flt-picture');

// 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.
element.setAttribute('aria-hidden', 'true');

return element;
}

@override
Expand Down
Loading