• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright 2013 The Flutter Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5part of engine;
6
7/// Mixin used by surfaces that clip their contents using an overflowing DOM
8/// element.
9mixin _DomClip on PersistedContainerSurface {
10  /// The dedicated child container element that's separate from the
11  /// [rootElement] is used to compensate for the coordinate system shift
12  /// introduced by the [rootElement] translation.
13  @override
14  html.Element get childContainer => _childContainer;
15  html.Element _childContainer;
16
17  @override
18  void adoptElements(_DomClip oldSurface) {
19    super.adoptElements(oldSurface);
20    _childContainer = oldSurface._childContainer;
21    oldSurface._childContainer = null;
22  }
23
24  @override
25  html.Element createElement() {
26    final html.Element element = defaultCreateElement('flt-clip');
27    if (!debugShowClipLayers) {
28      // Hide overflow in production mode. When debugging we want to see the
29      // clipped picture in full.
30      element.style.overflow = 'hidden';
31    } else {
32      // Display the outline of the clipping region. When debugShowClipLayers is
33      // `true` we don't hide clip overflow (see above). This outline helps
34      // visualizing clip areas.
35      element.style.boxShadow = 'inset 0 0 10px green';
36    }
37    _childContainer = html.Element.tag('flt-clip-interior');
38    if (_debugExplainSurfaceStats) {
39      // This creates an additional interior element. Count it too.
40      _surfaceStatsFor(this).allocatedDomNodeCount++;
41    }
42    _childContainer.style.position = 'absolute';
43    element.append(_childContainer);
44    return element;
45  }
46
47  @override
48  void discard() {
49    super.discard();
50
51    // Do not detach the child container from the root. It is permanently
52    // attached. The elements are reused together and are detached from the DOM
53    // together.
54    _childContainer = null;
55  }
56}
57
58/// A surface that creates a rectangular clip.
59class PersistedClipRect extends PersistedContainerSurface
60    with _DomClip
61    implements ui.ClipRectEngineLayer {
62  PersistedClipRect(PersistedClipRect oldLayer, this.rect) : super(oldLayer);
63
64  final ui.Rect rect;
65
66  @override
67  void recomputeTransformAndClip() {
68    _transform = parent._transform;
69    _globalClip = parent._globalClip.intersect(localClipRectToGlobalClip(
70      localClip: rect,
71      transform: _transform,
72    ));
73  }
74
75  @override
76  html.Element createElement() {
77    return super.createElement()..setAttribute('clip-type', 'rect');
78  }
79
80  @override
81  void apply() {
82    rootElement.style
83      ..transform = 'translate(${rect.left}px, ${rect.top}px)'
84      ..width = '${rect.right - rect.left}px'
85      ..height = '${rect.bottom - rect.top}px';
86
87    // Translate the child container in the opposite direction to compensate for
88    // the shift in the coordinate system introduced by the translation of the
89    // rootElement. Clipping in Flutter has no effect on the coordinate system.
90    childContainer.style.transform =
91        'translate(${-rect.left}px, ${-rect.top}px)';
92  }
93
94  @override
95  void update(PersistedClipRect oldSurface) {
96    super.update(oldSurface);
97    if (rect != oldSurface.rect) {
98      apply();
99    }
100  }
101}
102
103/// A surface that creates a rounded rectangular clip.
104class PersistedClipRRect extends PersistedContainerSurface
105    with _DomClip
106    implements ui.ClipRRectEngineLayer {
107  PersistedClipRRect(ui.EngineLayer oldLayer, this.rrect, this.clipBehavior)
108      : super(oldLayer);
109
110  final ui.RRect rrect;
111  // TODO(yjbanov): can this be controlled in the browser?
112  final ui.Clip clipBehavior;
113
114  @override
115  void recomputeTransformAndClip() {
116    _transform = parent._transform;
117    _globalClip = parent._globalClip.intersect(localClipRectToGlobalClip(
118      localClip: rrect.outerRect,
119      transform: _transform,
120    ));
121  }
122
123  @override
124  html.Element createElement() {
125    return super.createElement()..setAttribute('clip-type', 'rrect');
126  }
127
128  @override
129  void apply() {
130    rootElement.style
131      ..transform = 'translate(${rrect.left}px, ${rrect.top}px)'
132      ..width = '${rrect.width}px'
133      ..height = '${rrect.height}px'
134      ..borderTopLeftRadius = '${rrect.tlRadiusX}px'
135      ..borderTopRightRadius = '${rrect.trRadiusX}px'
136      ..borderBottomRightRadius = '${rrect.brRadiusX}px'
137      ..borderBottomLeftRadius = '${rrect.blRadiusX}px';
138
139    // Translate the child container in the opposite direction to compensate for
140    // the shift in the coordinate system introduced by the translation of the
141    // rootElement. Clipping in Flutter has no effect on the coordinate system.
142    childContainer.style.transform =
143        'translate(${-rrect.left}px, ${-rrect.top}px)';
144  }
145
146  @override
147  void update(PersistedClipRRect oldSurface) {
148    super.update(oldSurface);
149    if (rrect != oldSurface.rrect) {
150      apply();
151    }
152  }
153}
154
155class PersistedPhysicalShape extends PersistedContainerSurface
156    with _DomClip
157    implements ui.PhysicalShapeEngineLayer {
158  PersistedPhysicalShape(PersistedPhysicalShape oldLayer, this.path,
159      this.elevation, int color, int shadowColor, this.clipBehavior)
160      : color = ui.Color(color),
161        shadowColor = ui.Color(shadowColor),
162        super(oldLayer);
163
164  final ui.Path path;
165  final double elevation;
166  final ui.Color color;
167  final ui.Color shadowColor;
168  final ui.Clip clipBehavior;
169  html.Element _clipElement;
170
171  @override
172  void recomputeTransformAndClip() {
173    _transform = parent._transform;
174
175    final ui.RRect roundRect = path.webOnlyPathAsRoundedRect;
176    if (roundRect != null) {
177      _globalClip = parent._globalClip.intersect(localClipRectToGlobalClip(
178        localClip: roundRect.outerRect,
179        transform: transform,
180      ));
181    } else {
182      final ui.Rect rect = path.webOnlyPathAsRect;
183      if (rect != null) {
184        _globalClip = parent._globalClip.intersect(localClipRectToGlobalClip(
185          localClip: rect,
186          transform: transform,
187        ));
188      } else {
189        _globalClip = parent._globalClip;
190      }
191    }
192  }
193
194  void _applyColor() {
195    rootElement.style.backgroundColor = color.toCssString();
196  }
197
198  void _applyShadow() {
199    ElevationShadow.applyShadow(rootElement.style, elevation, shadowColor);
200  }
201
202  @override
203  html.Element createElement() {
204    return super.createElement()..setAttribute('clip-type', 'physical-shape');
205  }
206
207  @override
208  void apply() {
209    _applyColor();
210    _applyShadow();
211    _applyShape();
212  }
213
214  void _applyShape() {
215    if (path == null) {
216      return;
217    }
218    // Handle special case of round rect physical shape mapping to
219    // rounded div.
220    final ui.RRect roundRect = path.webOnlyPathAsRoundedRect;
221    if (roundRect != null) {
222      final String borderRadius =
223          '${roundRect.tlRadiusX}px ${roundRect.trRadiusX}px '
224          '${roundRect.brRadiusX}px ${roundRect.blRadiusX}px';
225      final html.CssStyleDeclaration style = rootElement.style;
226      style
227        ..transform = 'translate(${roundRect.left}px, ${roundRect.top}px)'
228        ..width = '${roundRect.width}px'
229        ..height = '${roundRect.height}px'
230        ..borderRadius = borderRadius;
231      childContainer.style.transform =
232          'translate(${-roundRect.left}px, ${-roundRect.top}px)';
233      if (clipBehavior != ui.Clip.none) {
234        style.overflow = 'hidden';
235      }
236      return;
237    } else {
238      final ui.Rect rect = path.webOnlyPathAsRect;
239      if (rect != null) {
240        final html.CssStyleDeclaration style = rootElement.style;
241        style
242          ..transform = 'translate(${rect.left}px, ${rect.top}px)'
243          ..width = '${rect.width}px'
244          ..height = '${rect.height}px'
245          ..borderRadius = '';
246        childContainer.style.transform =
247            'translate(${-rect.left}px, ${-rect.top}px)';
248        if (clipBehavior != ui.Clip.none) {
249          style.overflow = 'hidden';
250        }
251        return;
252      } else {
253        final Ellipse ellipse = path.webOnlyPathAsCircle;
254        if (ellipse != null) {
255          final double rx = ellipse.radiusX;
256          final double ry = ellipse.radiusY;
257          final String borderRadius =
258              rx == ry ? '${rx}px ' : '${rx}px ${ry}px ';
259          final html.CssStyleDeclaration style = rootElement.style;
260          final double left = ellipse.x - rx;
261          final double top = ellipse.y - ry;
262          style
263            ..transform = 'translate(${left}px, ${top}px)'
264            ..width = '${rx * 2}px'
265            ..height = '${ry * 2}px'
266            ..borderRadius = borderRadius;
267          childContainer.style.transform = 'translate(${-left}px, ${-top}px)';
268          if (clipBehavior != ui.Clip.none) {
269            style.overflow = 'hidden';
270          }
271          return;
272        }
273      }
274    }
275
276    final ui.Rect bounds = path.getBounds();
277    final String svgClipPath =
278        _pathToSvgClipPath(path, offsetX: -bounds.left, offsetY: -bounds.top);
279    assert(_clipElement == null);
280    _clipElement =
281        html.Element.html(svgClipPath, treeSanitizer: _NullTreeSanitizer());
282    domRenderer.append(rootElement, _clipElement);
283    domRenderer.setElementStyle(
284        rootElement, 'clip-path', 'url(#svgClip$_clipIdCounter)');
285    domRenderer.setElementStyle(
286        rootElement, '-webkit-clip-path', 'url(#svgClip$_clipIdCounter)');
287    final html.CssStyleDeclaration rootElementStyle = rootElement.style;
288    rootElementStyle
289      ..overflow = ''
290      ..transform = 'translate(${bounds.left}px, ${bounds.top}px)'
291      ..width = '${bounds.width}px'
292      ..height = '${bounds.height}px'
293      ..borderRadius = '';
294    childContainer.style.transform =
295        'translate(${-bounds.left}px, ${-bounds.top}px)';
296  }
297
298  @override
299  void update(PersistedPhysicalShape oldSurface) {
300    super.update(oldSurface);
301    if (oldSurface.color != color) {
302      _applyColor();
303    }
304    if (oldSurface.elevation != elevation ||
305        oldSurface.shadowColor != shadowColor) {
306      _applyShadow();
307    }
308    if (oldSurface.path != path) {
309      oldSurface._clipElement?.remove();
310      // Reset style on prior element since we may have switched between
311      // rect/rrect and arbitrary path.
312      final html.CssStyleDeclaration style = rootElement.style;
313      style.transform = '';
314      style.borderRadius = '';
315      domRenderer.setElementStyle(rootElement, 'clip-path', '');
316      domRenderer.setElementStyle(rootElement, '-webkit-clip-path', '');
317      _applyShape();
318    } else {
319      _clipElement = oldSurface._clipElement;
320    }
321    oldSurface._clipElement = null;
322  }
323}
324
325/// A surface that clips it's children.
326class PersistedClipPath extends PersistedContainerSurface
327    implements ui.ClipPathEngineLayer {
328  PersistedClipPath(
329      PersistedClipPath oldLayer, this.clipPath, this.clipBehavior)
330      : super(oldLayer);
331
332  final ui.Path clipPath;
333  final ui.Clip clipBehavior;
334  html.Element _clipElement;
335
336  @override
337  html.Element createElement() {
338    return defaultCreateElement('flt-clippath');
339  }
340
341  @override
342  void apply() {
343    if (clipPath == null) {
344      if (_clipElement != null) {
345        domRenderer.setElementStyle(childContainer, 'clip-path', '');
346        domRenderer.setElementStyle(childContainer, '-webkit-clip-path', '');
347        _clipElement.remove();
348        _clipElement = null;
349      }
350      return;
351    }
352    final String svgClipPath = _pathToSvgClipPath(clipPath);
353    _clipElement?.remove();
354    _clipElement =
355        html.Element.html(svgClipPath, treeSanitizer: _NullTreeSanitizer());
356    domRenderer.append(childContainer, _clipElement);
357    domRenderer.setElementStyle(
358        childContainer, 'clip-path', 'url(#svgClip$_clipIdCounter)');
359    domRenderer.setElementStyle(
360        childContainer, '-webkit-clip-path', 'url(#svgClip$_clipIdCounter)');
361  }
362
363  @override
364  void update(PersistedClipPath oldSurface) {
365    super.update(oldSurface);
366    if (oldSurface.clipPath != clipPath) {
367      oldSurface._clipElement?.remove();
368      apply();
369    } else {
370      _clipElement = oldSurface._clipElement;
371    }
372    oldSurface._clipElement = null;
373  }
374
375  @override
376  void discard() {
377    _clipElement?.remove();
378    _clipElement = null;
379    super.discard();
380  }
381}
382