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

Commit a242dd8

Browse files
authored
[web] Fix shadows for arbitrary paths on PhysicalShape (#23830)
1 parent a787229 commit a242dd8

File tree

4 files changed

+279
-33
lines changed

4 files changed

+279
-33
lines changed

lib/web_ui/dev/goldens_lock.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
repository: https://github.com/flutter/goldens.git
2-
revision: bdb442c42588b25c657779c78523822e349742d5
2+
revision: b85f9093e6bc6d4e7cbb7f97491667c143c4a360

lib/web_ui/lib/src/engine/dom_renderer.dart

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,21 @@ class DomRenderer {
182182
}
183183
}
184184

185+
static void setClipPath(html.Element element, String? value) {
186+
if (browserEngine == BrowserEngine.webkit) {
187+
if (value == null) {
188+
element.style.removeProperty('-webkit-clip-path');
189+
} else {
190+
element.style.setProperty('-webkit-clip-path', value);
191+
}
192+
}
193+
if (value == null) {
194+
element.style.removeProperty('clip-path');
195+
} else {
196+
element.style.setProperty('clip-path', value);
197+
}
198+
}
199+
185200
static void setElementTransform(html.Element element, String transformValue) {
186201
js_util.setProperty(
187202
js_util.getProperty(element, 'style'), 'transform', transformValue);

lib/web_ui/lib/src/engine/html/clip.dart

Lines changed: 86 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,7 @@ class PersistedPhysicalShape extends PersistedContainerSurface
190190
final ui.Color shadowColor;
191191
final ui.Clip clipBehavior;
192192
html.Element? _clipElement;
193+
html.Element? _svgElement;
193194

194195
@override
195196
void recomputeTransformAndClip() {
@@ -214,23 +215,18 @@ class PersistedPhysicalShape extends PersistedContainerSurface
214215
rootElement!.style.backgroundColor = colorToCssString(color);
215216
}
216217

217-
void _applyShadow() {
218-
applyCssShadow(rootElement, pathBounds, elevation, shadowColor);
219-
}
220-
221218
@override
222219
html.Element createElement() {
223220
return super.createElement()..setAttribute('clip-type', 'physical-shape');
224221
}
225222

226223
@override
227224
void apply() {
228-
_applyColor();
229-
_applyShadow();
230225
_applyShape();
231226
}
232227

233228
void _applyShape() {
229+
_applyColor();
234230
// Handle special case of round rect physical shape mapping to
235231
// rounded div.
236232
final ui.RRect? roundRect = path.toRoundedRect();
@@ -251,6 +247,7 @@ class PersistedPhysicalShape extends PersistedContainerSurface
251247
if (clipBehavior != ui.Clip.none) {
252248
style.overflow = 'hidden';
253249
}
250+
applyCssShadow(rootElement, pathBounds, elevation, shadowColor);
254251
return;
255252
} else {
256253
final ui.Rect? rect = path.toRect();
@@ -268,6 +265,7 @@ class PersistedPhysicalShape extends PersistedContainerSurface
268265
if (clipBehavior != ui.Clip.none) {
269266
style.overflow = 'hidden';
270267
}
268+
applyCssShadow(rootElement, pathBounds, elevation, shadowColor);
271269
return;
272270
} else {
273271
final ui.Rect? ovalRect = path.toCircle();
@@ -291,26 +289,64 @@ class PersistedPhysicalShape extends PersistedContainerSurface
291289
if (clipBehavior != ui.Clip.none) {
292290
style.overflow = 'hidden';
293291
}
292+
applyCssShadow(rootElement, pathBounds, elevation, shadowColor);
294293
return;
295294
}
296295
}
297296
}
298297

299-
final String svgClipPath = _pathToSvgClipPath(path,
300-
offsetX: -pathBounds.left,
301-
offsetY: -pathBounds.top,
302-
scaleX: 1.0 / pathBounds.width,
303-
scaleY: 1.0 / pathBounds.height);
304-
// If apply is called multiple times (without update) , remove prior
305-
// svg clip element.
298+
/// If code reaches this point, we have a path we want to clip against and
299+
/// potentially have a shadow due to material surface elevation.
300+
///
301+
/// When there is no shadow we can simply clip a div with a background
302+
/// color using a svg clip path.
303+
///
304+
/// Otherwise we need to paint svg element for the path and clip
305+
/// contents against same path for shadow to work since box-shadow doesn't
306+
/// take clip-path into account.
307+
///
308+
/// Webkit has a bug when applying clip-path on an element that has
309+
/// position: absolute and transform
310+
/// (https://bugs.webkit.org/show_bug.cgi?id=141731).
311+
/// To place clipping rectangle correctly
312+
/// we size the inner container to cover full pathBounds instead of sizing
313+
/// to clipping rect bounds (which is the case for elevation == 0.0 where
314+
/// we shift outer/inner clip area instead to position clip-path).
315+
final String svgClipPath = elevation == 0.0
316+
? _pathToSvgClipPath(path,
317+
offsetX: -pathBounds.left,
318+
offsetY: -pathBounds.top,
319+
scaleX: 1.0 / pathBounds.width,
320+
scaleY: 1.0 / pathBounds.height)
321+
: _pathToSvgClipPath(path,
322+
offsetX: 0.0,
323+
offsetY: 0.0,
324+
scaleX: 1.0 / pathBounds.right,
325+
scaleY: 1.0 / pathBounds.bottom);
326+
/// If apply is called multiple times (without update), remove prior
327+
/// svg clip and render elements.
306328
_clipElement?.remove();
329+
_svgElement?.remove();
307330
_clipElement =
308331
html.Element.html(svgClipPath, treeSanitizer: _NullTreeSanitizer());
309332
domRenderer.append(rootElement!, _clipElement!);
310-
DomRenderer.setElementStyle(
311-
rootElement!, 'clip-path', 'url(#svgClip$_clipIdCounter)');
312-
DomRenderer.setElementStyle(
313-
rootElement!, '-webkit-clip-path', 'url(#svgClip$_clipIdCounter)');
333+
if (elevation == 0.0) {
334+
DomRenderer.setClipPath(rootElement!, 'url(#svgClip$_clipIdCounter)');
335+
final html.CssStyleDeclaration rootElementStyle = rootElement!.style;
336+
rootElementStyle
337+
..overflow = ''
338+
..left = '${pathBounds.left}px'
339+
..top = '${pathBounds.top}px'
340+
..width = '${pathBounds.width}px'
341+
..height = '${pathBounds.height}px'
342+
..borderRadius = '';
343+
childContainer!.style
344+
..left = '-${pathBounds.left}px'
345+
..top = '-${pathBounds.top}px';
346+
return;
347+
}
348+
349+
DomRenderer.setClipPath(childContainer!, 'url(#svgClip$_clipIdCounter)');
314350
final html.CssStyleDeclaration rootElementStyle = rootElement!.style;
315351
rootElementStyle
316352
..overflow = ''
@@ -321,28 +357,45 @@ class PersistedPhysicalShape extends PersistedContainerSurface
321357
..borderRadius = '';
322358
childContainer!.style
323359
..left = '-${pathBounds.left}px'
324-
..top = '-${pathBounds.top}px';
360+
..top = '-${pathBounds.top}px'
361+
..width = '${pathBounds.right}px'
362+
..height = '${pathBounds.bottom}px';
363+
364+
final ui.Rect pathBounds2 = path.getBounds();
365+
_svgElement = _pathToSvgElement(
366+
path, SurfacePaintData()..color = color, '${pathBounds2.right}', '${pathBounds2.bottom}');
367+
/// Render element behind the clipped content.
368+
rootElement!.insertBefore(_svgElement!, childContainer);
369+
370+
final SurfaceShadowData shadow = computeShadow(pathBounds, elevation)!;
371+
final ui.Color boxShadowColor = toShadowColor(shadowColor);
372+
_svgElement!.style
373+
..filter =
374+
'drop-shadow(${shadow.offset.dx}px ${shadow.offset.dy}px '
375+
'${shadow.blurWidth}px '
376+
'rgba(${boxShadowColor.red}, ${boxShadowColor.green}, '
377+
'${boxShadowColor.blue}, ${boxShadowColor.alpha / 255}))'
378+
..transform = 'translate(-${pathBounds2.left}px, -${pathBounds2.top}px)';
379+
380+
rootElement!.style.backgroundColor = '';
325381
}
326382

327383
@override
328384
void update(PersistedPhysicalShape oldSurface) {
329385
super.update(oldSurface);
330-
if (oldSurface.color != color) {
331-
_applyColor();
332-
}
333-
if (oldSurface.elevation != elevation ||
334-
oldSurface.shadowColor != shadowColor) {
335-
_applyShadow();
336-
}
337-
if (oldSurface.path != path) {
386+
if (oldSurface.path != path || oldSurface.elevation != elevation ||
387+
oldSurface.shadowColor != shadowColor || oldSurface.color != color) {
338388
oldSurface._clipElement?.remove();
339389
oldSurface._clipElement = null;
390+
oldSurface._svgElement?.remove();
391+
oldSurface._svgElement = null;
340392
_clipElement?.remove();
341393
_clipElement = null;
394+
_svgElement?.remove();
395+
_svgElement = null;
342396
// Reset style on prior element since we may have switched between
343397
// rect/rrect and arbitrary path.
344-
DomRenderer.setElementStyle(rootElement!, 'clip-path', '');
345-
DomRenderer.setElementStyle(rootElement!, '-webkit-clip-path', '');
398+
DomRenderer.setClipPath(rootElement!, '');
346399
_applyShape();
347400
} else {
348401
// Reuse clipElement from prior surface.
@@ -351,6 +404,10 @@ class PersistedPhysicalShape extends PersistedContainerSurface
351404
domRenderer.append(rootElement!, _clipElement!);
352405
}
353406
oldSurface._clipElement = null;
407+
_svgElement = oldSurface._svgElement;
408+
if (_svgElement != null) {
409+
rootElement!.insertBefore(_svgElement!, childContainer);
410+
}
354411
}
355412
}
356413
}
@@ -416,10 +473,7 @@ String createSvgClipDef(html.HtmlElement element, ui.Path clipPath) {
416473
final ui.Rect pathBounds = clipPath.getBounds();
417474
final String svgClipPath = _pathToSvgClipPath(clipPath,
418475
scaleX: 1.0 / pathBounds.right, scaleY: 1.0 / pathBounds.bottom);
419-
DomRenderer.setElementStyle(
420-
element, 'clip-path', 'url(#svgClip$_clipIdCounter)');
421-
DomRenderer.setElementStyle(
422-
element, '-webkit-clip-path', 'url(#svgClip$_clipIdCounter)');
476+
DomRenderer.setClipPath(element, 'url(#svgClip$_clipIdCounter)');
423477
// We need to set width and height for the clipElement to cover the
424478
// bounds of the path since browsers such as Safari and Edge
425479
// seem to incorrectly intersect the element bounding rect with

0 commit comments

Comments
 (0)