diff --git a/engine/src/flutter/lib/web_ui/dev/goldens_lock.yaml b/engine/src/flutter/lib/web_ui/dev/goldens_lock.yaml index 1c492e721e7..ba7f23eb799 100644 --- a/engine/src/flutter/lib/web_ui/dev/goldens_lock.yaml +++ b/engine/src/flutter/lib/web_ui/dev/goldens_lock.yaml @@ -1,2 +1,2 @@ repository: https://github.com/flutter/goldens.git -revision: 25fad8d9927e443345c28b408220ded99b65d647 +revision: 10ed22e7e6a5039b84e8c028828a86a7ff98b0be diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/dom_renderer.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/dom_renderer.dart index 20065b48dd3..a01f26a1624 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/dom_renderer.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/dom_renderer.dart @@ -76,6 +76,10 @@ class DomRenderer { html.Element? get sceneHostElement => _sceneHostElement; html.Element? _sceneHostElement; + /// A child element of body outside the shadowroot that hosts + /// global resources such svg filters and clip paths when using webkit. + html.Element? _resourcesHost; + /// The element that contains the semantics tree. /// /// This element is created and inserted in the HTML DOM once. It is never @@ -275,6 +279,8 @@ class DomRenderer { _styleElement?.remove(); _styleElement = html.StyleElement(); + _resourcesHost?.remove(); + _resourcesHost = null; html.document.head!.append(_styleElement!); final html.CssStyleSheet sheet = _styleElement!.sheet as html.CssStyleSheet; applyGlobalCssRulesToSheet( @@ -615,6 +621,33 @@ class DomRenderer { return null; } + /// Add an element as a global resource to be referenced by CSS. + /// + /// This call create a global resource host element on demand and either + /// place it as first element of body(webkit), or as a child of + /// glass pane element for other browsers to make sure url resolution + /// works correctly when content is inside a shadow root. + void addResource(html.Element element) { + final bool isWebKit = browserEngine == BrowserEngine.webkit; + if (_resourcesHost == null) { + _resourcesHost = html.DivElement() + ..style.visibility = 'hidden'; + if (isWebKit) { + final html.Node bodyNode = html.document.body!; + bodyNode.insertBefore(_resourcesHost!, bodyNode.firstChild); + } else { + _glassPaneShadow!.node.insertBefore( + _resourcesHost!, _glassPaneShadow!.node.firstChild); + } + } + _resourcesHost!.append(element); + } + + /// Removes a global resource element. + void removeResource(html.Element? element) { + element?.remove(); + } + /// Provides haptic feedback. void vibrate(int durationMs) { final html.Navigator navigator = html.window.navigator; diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/html/bitmap_canvas.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/html/bitmap_canvas.dart index 8f7d7514dd5..fca0a37be60 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/html/bitmap_canvas.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/html/bitmap_canvas.dart @@ -368,7 +368,7 @@ class BitmapCanvas extends EngineCanvas { /// prefer DOM if canvas has not been allocated yet. /// bool _useDomForRenderingFill(SurfacePaintData paint) => - _renderStrategy.isInsideShaderMask || + _renderStrategy.isInsideSvgFilterTree || (_preserveImageData == false && _contains3dTransform) || (_childOverdraw && _canvasPool.canvas == null && @@ -380,7 +380,7 @@ class BitmapCanvas extends EngineCanvas { /// /// DOM canvas is generated for simple strokes using borders. bool _useDomForRenderingFillAndStroke(SurfacePaintData paint) => - _renderStrategy.isInsideShaderMask || + _renderStrategy.isInsideSvgFilterTree || (_preserveImageData == false && _contains3dTransform) || ((_childOverdraw || _renderStrategy.hasImageElements || diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/html/clip.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/html/clip.dart index c0a90ddd402..3a05ae25f4a 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/html/clip.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/html/clip.dart @@ -243,6 +243,15 @@ class PersistedPhysicalShape extends PersistedContainerSurface return super.createElement()..setAttribute('clip-type', 'physical-shape'); } + @override + void discard() { + super.discard(); + _clipElement?.remove(); + _clipElement = null; + _svgElement?.remove(); + _svgElement = null; + } + @override void apply() { _applyShape(); @@ -443,6 +452,7 @@ class PersistedPhysicalShape extends PersistedContainerSurface if (_svgElement != null) { rootElement!.insertBefore(_svgElement!, childContainer); } + oldSurface._svgElement = null; } } } diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/html/color_filter.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/html/color_filter.dart index a6945315048..c64f21f7e6f 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/html/color_filter.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/html/color_filter.dart @@ -9,6 +9,7 @@ import 'package:ui/ui.dart' as ui; import '../../engine.dart' show NullTreeSanitizer; import '../canvaskit/color_filter.dart'; import '../color_filter.dart'; +import '../dom_renderer.dart'; import '../util.dart'; import 'bitmap_canvas.dart'; import 'path_to_svg_clip.dart'; @@ -36,12 +37,21 @@ class PersistedColorFilter extends PersistedContainerSurface void adoptElements(PersistedColorFilter oldSurface) { super.adoptElements(oldSurface); _childContainer = oldSurface._childContainer; + _filterElement = oldSurface._filterElement; oldSurface._childContainer = null; } + @override + void preroll(PrerollSurfaceContext prerollContext) { + ++prerollContext.activeColorFilterCount; + super.preroll(prerollContext); + --prerollContext.activeColorFilterCount; + } + @override void discard() { super.discard(); + domRenderer.removeResource(_filterElement); // Do not detach the child container from the root. It is permanently // attached. The elements are reused together and are detached from the DOM // together. @@ -60,9 +70,8 @@ class PersistedColorFilter extends PersistedContainerSurface @override void apply() { - if (_filterElement != null) { - _filterElement?.remove(); - } + domRenderer.removeResource(_filterElement); + _filterElement = null; final EngineColorFilter? engineValue = filter as EngineColorFilter?; if (engineValue == null) { rootElement!.style.backgroundColor = ''; @@ -81,12 +90,12 @@ class PersistedColorFilter extends PersistedContainerSurface void _applyBlendModeFilter(CkBlendModeColorFilter colorFilter) { final ui.Color filterColor = colorFilter.color; ui.BlendMode colorFilterBlendMode = colorFilter.blendMode; - final html.CssStyleDeclaration style = rootElement!.style; + final html.CssStyleDeclaration style = childContainer!.style; switch (colorFilterBlendMode) { case ui.BlendMode.clear: case ui.BlendMode.dstOut: case ui.BlendMode.srcOut: - childContainer?.style.visibility = 'hidden'; + style.visibility = 'hidden'; return; case ui.BlendMode.dst: case ui.BlendMode.dstIn: @@ -130,8 +139,9 @@ class PersistedColorFilter extends PersistedContainerSurface if (svgFilter != null) { _filterElement = html.Element.html(svgFilter, treeSanitizer: NullTreeSanitizer()); - rootElement!.append(_filterElement!); - rootElement!.style.filter = 'url(#_fcf${filterIdCounter})'; + //rootElement!.insertBefore(_filterElement!, childContainer!); + domRenderer.addResource(_filterElement!); + style.filter = 'url(#_fcf${filterIdCounter})'; if (colorFilterBlendMode == ui.BlendMode.saturation || colorFilterBlendMode == ui.BlendMode.multiply || colorFilterBlendMode == ui.BlendMode.modulate) { @@ -145,8 +155,8 @@ class PersistedColorFilter extends PersistedContainerSurface if (svgFilter != null) { _filterElement = html.Element.html(svgFilter, treeSanitizer: NullTreeSanitizer()); - rootElement!.append(_filterElement!); - rootElement!.style.filter = 'url(#_fcf${filterIdCounter})'; + domRenderer.addResource(_filterElement!); + childContainer!.style.filter = 'url(#_fcf${filterIdCounter})'; } } @@ -220,7 +230,7 @@ String? svgFilterFromBlendMode( case ui.BlendMode.difference: case ui.BlendMode.exclusion: svgFilter = _blendColorFilterToSvg( - filterColor, stringForBlendMode(colorFilterBlendMode)); + filterColor, stringForBlendMode(colorFilterBlendMode)!); break; case ui.BlendMode.src: case ui.BlendMode.dst: @@ -251,7 +261,7 @@ String? svgFilterFromColorMatrix(List matrix) { return '$kSvgResourceHeader' '' - '' + '' ''; } @@ -273,12 +283,12 @@ int filterIdCounter = 0; String _srcInColorFilterToSvg(ui.Color? color) { filterIdCounter += 1; return '$kSvgResourceHeader' - '' - '' // Just take alpha channel of destination + '>' // Just take alpha channel of destination '' '' '(kProfileApplyFrame, () { if (_lastFrameScene == null) { diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/html/shader_mask.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/html/shader_mask.dart index cb5bb2276c8..e658eda3f76 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/html/shader_mask.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/html/shader_mask.dart @@ -8,6 +8,7 @@ import 'package:ui/ui.dart' as ui; import '../../engine.dart' show NullTreeSanitizer; import '../browser_detection.dart'; +import '../dom_renderer.dart'; import 'bitmap_canvas.dart'; import 'path_to_svg_clip.dart'; import 'shaders/shader.dart'; @@ -40,7 +41,6 @@ class PersistedShaderMask extends PersistedContainerSurface final ui.BlendMode blendMode; final ui.FilterQuality filterQuality; html.Element? _shaderElement; - static int activeShaderMaskCount = 0; final bool isWebKit = browserEngine == BrowserEngine.webkit; @override @@ -58,6 +58,7 @@ class PersistedShaderMask extends PersistedContainerSurface @override void discard() { super.discard(); + domRenderer.removeResource(_shaderElement); // Do not detach the child container from the root. It is permanently // attached. The elements are reused together and are detached from the DOM // together. @@ -65,10 +66,10 @@ class PersistedShaderMask extends PersistedContainerSurface } @override - void preroll() { - ++activeShaderMaskCount; - super.preroll(); - --activeShaderMaskCount; + void preroll(PrerollSurfaceContext prerollContext) { + ++prerollContext.activeShaderMaskCount; + super.preroll(prerollContext); + --prerollContext.activeShaderMaskCount; } @override @@ -83,7 +84,7 @@ class PersistedShaderMask extends PersistedContainerSurface @override void apply() { - _shaderElement?.remove(); + domRenderer.removeResource(_shaderElement); _shaderElement = null; if (shader is ui.Gradient) { rootElement!.style @@ -164,7 +165,7 @@ class PersistedShaderMask extends PersistedContainerSurface } else { rootElement!.style.filter = 'url(#_fmf${_maskFilterIdCounter}'; } - rootElement!.append(_shaderElement!); + domRenderer.addResource(_shaderElement!); } } diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/html/surface.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/html/surface.dart index c0eb292b0dc..ecb658a615f 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/html/surface.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/html/surface.dart @@ -568,7 +568,7 @@ abstract class PersistedSurface implements ui.EngineLayer { /// /// This method recursively walks the surface tree calling `preroll` on all /// descendants. - void preroll() { + void preroll(PrerollSurfaceContext prerollContext) { recomputeTransformAndClip(); } @@ -651,11 +651,11 @@ abstract class PersistedContainerSurface extends PersistedSurface { } @override - void preroll() { - super.preroll(); + void preroll(PrerollSurfaceContext prerollContext) { + super.preroll(prerollContext); final int length = _children.length; for (int i = 0; i < length; i += 1) { - _children[i].preroll(); + _children[i].preroll(prerollContext); } } @@ -1227,3 +1227,13 @@ class _PersistedSurfaceMatch { } } } + +/// Data used during preroll to pass rendering hints efficiently to children +/// by optimizing (prevent parent lookups) and in cases like svg filters +/// drive the decision on whether canvas elements can be used to render. +class PrerollSurfaceContext { + /// Number of active color filters in parent surfaces. + int activeColorFilterCount = 0; + /// Number of active shader masks in parent surfaces. + int activeShaderMaskCount = 0; +} diff --git a/engine/src/flutter/lib/web_ui/test/dom_renderer_test.dart b/engine/src/flutter/lib/web_ui/test/dom_renderer_test.dart index ac17175a3e4..050c92ec323 100644 --- a/engine/src/flutter/lib/web_ui/test/dom_renderer_test.dart +++ b/engine/src/flutter/lib/web_ui/test/dom_renderer_test.dart @@ -158,6 +158,17 @@ void testMain() { attachShadow = oldAttachShadow; // Restore ShadowDOM }); + + test('should add/remove global resource', () { + final DomRenderer renderer = DomRenderer(); + final html.DivElement resource = html.DivElement(); + renderer.addResource(resource); + final html.Element? resourceRoot = resource.parent; + expect(resourceRoot, isNotNull); + expect(resourceRoot!.childNodes.length, 1); + renderer.removeResource(resource); + expect(resourceRoot.childNodes.length, 0); + }); } @JS('Element.prototype.attachShadow') diff --git a/engine/src/flutter/lib/web_ui/test/engine/surface/scene_builder_test.dart b/engine/src/flutter/lib/web_ui/test/engine/surface/scene_builder_test.dart index dfe12b79c8a..af1b96017a2 100644 --- a/engine/src/flutter/lib/web_ui/test/engine/surface/scene_builder_test.dart +++ b/engine/src/flutter/lib/web_ui/test/engine/surface/scene_builder_test.dart @@ -143,7 +143,7 @@ void testMain() { expect(picture.updateCount, 0); expect(picture.applyPaintCount, 0); - scene1.preroll(); + scene1.preroll(PrerollSurfaceContext()); scene1.build(); commitScene(scene1); expect(picture.retainCount, 0); @@ -162,7 +162,7 @@ void testMain() { opacity.state = PersistedSurfaceState.pendingRetention; clip2.appendChild(opacity); - scene2.preroll(); + scene2.preroll(PrerollSurfaceContext()); scene2.update(scene1); commitScene(scene2); expect(picture.retainCount, 1); @@ -181,7 +181,7 @@ void testMain() { opacity.state = PersistedSurfaceState.pendingRetention; clip3.appendChild(opacity); - scene3.preroll(); + scene3.preroll(PrerollSurfaceContext()); scene3.update(scene2); commitScene(scene3); expect(picture.retainCount, 2); diff --git a/engine/src/flutter/lib/web_ui/test/golden_tests/engine/color_filter_golden_test.dart b/engine/src/flutter/lib/web_ui/test/golden_tests/engine/color_filter_golden_test.dart index d184076d7d6..9282cdacf00 100644 --- a/engine/src/flutter/lib/web_ui/test/golden_tests/engine/color_filter_golden_test.dart +++ b/engine/src/flutter/lib/web_ui/test/golden_tests/engine/color_filter_golden_test.dart @@ -68,6 +68,24 @@ void testMain() async { maxDiffRatePercent: 12.0); }); + /// Regression test for https://github.com/flutter/flutter/issues/85733 + test('Should apply mode color filter to circles', () async { + final SurfaceSceneBuilder builder = SurfaceSceneBuilder(); + final Picture backgroundPicture = _drawBackground(); + builder.addPicture(Offset.zero, backgroundPicture); + builder.pushColorFilter( + ColorFilter.mode( + Color(0xFFFF0000), + BlendMode.srcIn, + )); + final Picture circles1 = _drawTestPictureWithCircles(30, 30); + builder.addPicture(Offset.zero, circles1); + builder.pop(); + html.document.body!.append(builder.build().webOnlyRootElement!); + await matchGoldenFile('color_filter_mode.png', region: region, + maxDiffRatePercent: 12.0); + }); + /// Regression test for https://github.com/flutter/flutter/issues/59451. /// /// Picture with overlay blend inside a physical shape. Should show image diff --git a/engine/src/flutter/lib/web_ui/test/golden_tests/engine/shader_mask_golden_test.dart b/engine/src/flutter/lib/web_ui/test/golden_tests/engine/shader_mask_golden_test.dart index f56fdcdd1a4..85e8438a5fc 100644 --- a/engine/src/flutter/lib/web_ui/test/golden_tests/engine/shader_mask_golden_test.dart +++ b/engine/src/flutter/lib/web_ui/test/golden_tests/engine/shader_mask_golden_test.dart @@ -33,7 +33,8 @@ void testMain() async { setUp(() async { debugShowClipLayers = true; SurfaceSceneBuilder.debugForgetFrameScene(); - for (html.Node scene in html.document.querySelectorAll('flt-scene')) { + for (html.Node scene in + domRenderer.sceneHostElement!.querySelectorAll('flt-scene')) { scene.remove(); } initWebGl(); @@ -147,5 +148,5 @@ void _renderScene(BlendMode blendMode) { builder.addPicture(Offset.zero, circles2); builder.pop(); - html.document.body!.append(builder.build().webOnlyRootElement!); + domRenderer.sceneHostElement!.append(builder.build().webOnlyRootElement!); }