part of dart._engine; // 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. /// Mixin used by surfaces that clip their contents using an overflowing DOM /// element. mixin _DomClip on PersistedContainerSurface { /// The dedicated child container element that's separate from the /// [rootElement] is used to compensate for the coordinate system shift /// introduced by the [rootElement] translation. @override DomElement? get childContainer => _childContainer; DomElement? _childContainer; @override void adoptElements(_DomClip oldSurface) { super.adoptElements(oldSurface); _childContainer = oldSurface._childContainer; oldSurface._childContainer = null; } @override DomElement createElement() { final DomElement element = defaultCreateElement('flt-clip'); _childContainer = createDomElement('flt-clip-interior'); if (debugExplainSurfaceStats) { // This creates an additional interior element. Count it too. surfaceStatsFor(this).allocatedDomNodeCount++; } _childContainer!.style.position = 'absolute'; element.append(_childContainer!); return element; } @override void discard() { super.discard(); // 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. _childContainer = null; } void applyOverflow(DomElement element, ui.Clip? clipBehaviour) { if (!debugShowClipLayers) { // Hide overflow in production mode. When debugging we want to see the // clipped picture in full. if (clipBehaviour != ui.Clip.none) { element.style ..overflow = 'hidden' ..zIndex = '0'; } } else { // Display the outline of the clipping region. When debugShowClipLayers is // `true` we don't hide clip overflow (see above). This outline helps // visualizing clip areas. element.style.boxShadow = 'inset 0 0 10px green'; } } } /// A surface that creates a rectangular clip. class PersistedClipRect extends PersistedContainerSurface with _DomClip implements ui.ClipRectEngineLayer { PersistedClipRect(PersistedClipRect? super.oldLayer, this.rect, this.clipBehavior); final ui.Clip? clipBehavior; final ui.Rect rect; @override void recomputeTransformAndClip() { transform = parent!.transform; if (clipBehavior != ui.Clip.none) { localClipBounds = rect; } else { localClipBounds = null; } projectedClip = null; } @override DomElement createElement() { return super.createElement()..setAttribute('clip-type', 'rect'); } @override void apply() { rootElement!.style ..left = '${rect.left}px' ..top = '${rect.top}px' ..width = '${rect.right - rect.left}px' ..height = '${rect.bottom - rect.top}px'; applyOverflow(rootElement!, clipBehavior); // Translate the child container in the opposite direction to compensate for // the shift in the coordinate system introduced by the translation of the // rootElement. Clipping in Flutter has no effect on the coordinate system. childContainer!.style ..left = '${-rect.left}px' ..top = '${-rect.top}px'; } @override void update(PersistedClipRect oldSurface) { super.update(oldSurface); if (rect != oldSurface.rect || clipBehavior != oldSurface.clipBehavior) { localClipBounds = null; apply(); } } @override bool get isClipping => true; } /// A surface that creates a rounded rectangular clip. class PersistedClipRRect extends PersistedContainerSurface with _DomClip implements ui.ClipRRectEngineLayer { PersistedClipRRect(ui.EngineLayer? oldLayer, this.rrect, this.clipBehavior) : super(oldLayer as PersistedSurface?); final ui.RRect rrect; // TODO(yjbanov): can this be controlled in the browser? final ui.Clip? clipBehavior; @override void recomputeTransformAndClip() { transform = parent!.transform; if (clipBehavior != ui.Clip.none) { localClipBounds = rrect.outerRect; } else { localClipBounds = null; } projectedClip = null; } @override DomElement createElement() { return super.createElement()..setAttribute('clip-type', 'rrect'); } @override void apply() { final DomCSSStyleDeclaration style = rootElement!.style; style ..left = '${rrect.left}px' ..top = '${rrect.top}px' ..width = '${rrect.width}px' ..height = '${rrect.height}px' ..borderTopLeftRadius = '${rrect.tlRadiusX}px' ..borderTopRightRadius = '${rrect.trRadiusX}px' ..borderBottomRightRadius = '${rrect.brRadiusX}px' ..borderBottomLeftRadius = '${rrect.blRadiusX}px'; applyOverflow(rootElement!, clipBehavior); // Translate the child container in the opposite direction to compensate for // the shift in the coordinate system introduced by the translation of the // rootElement. Clipping in Flutter has no effect on the coordinate system. childContainer!.style ..left = '${-rrect.left}px' ..top = '${-rrect.top}px'; } @override void update(PersistedClipRRect oldSurface) { super.update(oldSurface); if (rrect != oldSurface.rrect || clipBehavior != oldSurface.clipBehavior) { localClipBounds = null; apply(); } } @override bool get isClipping => true; } /// A surface that clips it's children. class PersistedClipPath extends PersistedContainerSurface implements ui.ClipPathEngineLayer { PersistedClipPath(PersistedClipPath? super.oldLayer, this.clipPath, this.clipBehavior); final ui.Path clipPath; final ui.Clip clipBehavior; DomElement? _clipElement; @override DomElement createElement() { return defaultCreateElement('flt-clippath'); } @override void recomputeTransformAndClip() { super.recomputeTransformAndClip(); if (clipBehavior != ui.Clip.none) { localClipBounds ??= clipPath.getBounds(); } else { localClipBounds = null; } } @override void apply() { _clipElement?.remove(); _clipElement = createSvgClipDef(childContainer!, clipPath); childContainer!.append(_clipElement!); } @override void update(PersistedClipPath oldSurface) { super.update(oldSurface); if (oldSurface.clipPath != clipPath) { localClipBounds = null; oldSurface._clipElement?.remove(); apply(); } else { _clipElement = oldSurface._clipElement; } oldSurface._clipElement = null; } @override void discard() { _clipElement?.remove(); _clipElement = null; super.discard(); } @override bool get isClipping => true; } /// Creates an svg clipPath and applies it to [element]. SVGSVGElement createSvgClipDef(DomElement element, ui.Path clipPath) { final ui.Rect pathBounds = clipPath.getBounds(); final SVGSVGElement svgClipPath = pathToSvgClipPath( clipPath, scaleX: 1.0 / pathBounds.right, scaleY: 1.0 / pathBounds.bottom, ); setClipPath(element, createSvgClipUrl()); // We need to set width and height for the clipElement to cover the // bounds of the path since browsers such as Safari and Edge // seem to incorrectly intersect the element bounding rect with // the clip path. Chrome and Firefox don't perform intersect instead they // use the path itself as source of truth. element.style ..width = '${pathBounds.right}px' ..height = '${pathBounds.bottom}px'; return svgClipPath; }