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