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 7class DomRenderer { 8 DomRenderer() { 9 if (assertionsEnabled) { 10 _debugFrameStatistics = DebugDomRendererFrameStatistics(); 11 } 12 13 reset(); 14 15 TextMeasurementService.initialize(rulerCacheCapacity: 10); 16 17 assert(() { 18 _setupHotRestart(); 19 return true; 20 }()); 21 } 22 23 static const int vibrateLongPress = 50; 24 static const int vibrateLightImpact = 10; 25 static const int vibrateMediumImpact = 20; 26 static const int vibrateHeavyImpact = 30; 27 static const int vibrateSelectionClick = 10; 28 29 /// Listens to window resize events. 30 StreamSubscription<html.Event> _resizeSubscription; 31 32 /// Contains Flutter-specific CSS rules, such as default margins and 33 /// paddings. 34 html.StyleElement _styleElement; 35 36 /// Configures the screen, such as scaling. 37 html.MetaElement _viewportMeta; 38 39 /// The canvaskit script, downloaded from a CDN. Only created if 40 /// [experimentalUseSkia] is set to true. 41 html.ScriptElement get canvasKitScript => _canvasKitScript; 42 html.ScriptElement _canvasKitScript; 43 44 /// The element that contains the [sceneElement]. 45 /// 46 /// This element is created and inserted in the HTML DOM once. It is never 47 /// removed or moved. However the [sceneElement] may be replaced inside it. 48 /// 49 /// This element precedes the [glassPaneElement] so that it never receives 50 /// input events. All input events are processed by [glassPaneElement] and the 51 /// semantics tree. 52 html.Element get sceneHostElement => _sceneHostElement; 53 html.Element _sceneHostElement; 54 55 /// The last scene element rendered by the [render] method. 56 html.Element get sceneElement => _sceneElement; 57 html.Element _sceneElement; 58 59 /// This is state persistant across hot restarts that indicates what 60 /// to clear. We delay removal of old visible state to make the 61 /// transition appear smooth. 62 static const String _staleHotRestartStore = '__flutter_state'; 63 List<html.Element> _staleHotRestartState; 64 65 void _setupHotRestart() { 66 // This persists across hot restarts to clear stale DOM. 67 _staleHotRestartState = 68 js_util.getProperty(html.window, _staleHotRestartStore); 69 if (_staleHotRestartState == null) { 70 _staleHotRestartState = <html.Element>[]; 71 js_util.setProperty( 72 html.window, _staleHotRestartStore, _staleHotRestartState); 73 } 74 75 registerHotRestartListener(() { 76 _resizeSubscription?.cancel(); 77 _staleHotRestartState.addAll(<html.Element>[ 78 _glassPaneElement, 79 _styleElement, 80 _viewportMeta, 81 _canvasKitScript, 82 ]); 83 }); 84 } 85 86 void _clearOnHotRestart() { 87 if (_staleHotRestartState.isNotEmpty) { 88 for (html.Element element in _staleHotRestartState) { 89 element?.remove(); 90 } 91 _staleHotRestartState.clear(); 92 } 93 } 94 95 /// We don't want to unnecessarily move DOM nodes around. If a DOM node is 96 /// already in the right place, skip DOM mutation. This is both faster and 97 /// more correct, because moving DOM nodes loses internal state, such as 98 /// text selection. 99 void renderScene(html.Element sceneElement) { 100 if (sceneElement != _sceneElement) { 101 _sceneElement?.remove(); 102 _sceneElement = sceneElement; 103 append(_sceneHostElement, sceneElement); 104 } 105 assert(() { 106 _clearOnHotRestart(); 107 return true; 108 }()); 109 } 110 111 /// The element that captures input events, such as pointer events. 112 /// 113 /// If semantics is enabled this element also contains the semantics DOM tree, 114 /// which captures semantics input events. The semantics DOM tree must be a 115 /// child of the glass pane element so that events bubble up to the glass pane 116 /// if they are not handled by semantics. 117 html.Element get glassPaneElement => _glassPaneElement; 118 html.Element _glassPaneElement; 119 120 final html.Element rootElement = html.document.body; 121 122 void addElementClass(html.Element element, String className) { 123 element.classes.add(className); 124 } 125 126 void attachBeforeElement( 127 html.Element parent, html.Element before, html.Element newElement) { 128 assert(parent != null); 129 if (parent != null) { 130 assert(() { 131 if (before == null) { 132 return true; 133 } 134 if (before.parent != parent) { 135 throw Exception( 136 'attachBeforeElement was called with `before` element that\'s ' 137 'not a child of the `parent` element:\n' 138 ' before: $before\n' 139 ' parent: $parent', 140 ); 141 } 142 return true; 143 }()); 144 parent.insertBefore(newElement, before); 145 } 146 } 147 148 html.Element createElement(String tagName, {html.Element parent}) { 149 final html.Element element = html.document.createElement(tagName); 150 parent?.append(element); 151 return element; 152 } 153 154 void append(html.Element parent, html.Element child) { 155 parent.append(child); 156 } 157 158 void appendText(html.Element parent, String text) { 159 parent.appendText(text); 160 } 161 162 void detachElement(html.Element element) { 163 element.remove(); 164 } 165 166 void removeElementClass(html.Element element, String className) { 167 element.classes.remove(className); 168 } 169 170 void setElementAttribute(html.Element element, String name, String value) { 171 element.setAttribute(name, value); 172 } 173 174 void setElementProperty(html.Element element, String name, Object value) { 175 js_util.setProperty(element, name, value); 176 } 177 178 void setElementStyle(html.Element element, String name, String value) { 179 if (value == null) { 180 element.style.removeProperty(name); 181 } else { 182 element.style.setProperty(name, value); 183 } 184 } 185 186 void setText(html.Element element, String text) { 187 element.text = text; 188 } 189 190 void removeAllChildren(html.Element element) { 191 element.children.clear(); 192 } 193 194 html.Element getParent(html.Element element) => element.parent; 195 196 void setTitle(String title) { 197 html.document.title = title; 198 } 199 200 void setThemeColor(ui.Color color) { 201 html.MetaElement theme = html.document.querySelector('#flutterweb-theme'); 202 if (theme == null) { 203 theme = html.MetaElement() 204 ..id = 'flutterweb-theme' 205 ..name = 'theme-color'; 206 html.document.head.append(theme); 207 } 208 theme.content = color.toCssString(); 209 } 210 211 static const String defaultFontStyle = 'normal'; 212 static const String defaultFontWeight = 'normal'; 213 static const String defaultFontSize = '14px'; 214 static const String defaultFontFamily = 'sans-serif'; 215 static const String defaultCssFont = 216 '$defaultFontStyle $defaultFontWeight $defaultFontSize $defaultFontFamily'; 217 218 void reset() { 219 _styleElement?.remove(); 220 _styleElement = html.StyleElement(); 221 html.document.head.append(_styleElement); 222 final html.CssStyleSheet sheet = _styleElement.sheet; 223 224 // TODO(butterfly): use more efficient CSS selectors; descendant selectors 225 // are slow. More info: 226 // 227 // https://csswizardry.com/2011/09/writing-efficient-css-selectors/ 228 229 // This undoes browser's default layout attributes for paragraphs. We 230 // compute paragraph layout ourselves. 231 sheet.insertRule(''' 232flt-ruler-host p, flt-scene p { 233 margin: 0; 234}''', sheet.cssRules.length); 235 236 // This undoes browser's default painting and layout attributes of range 237 // input, which is used in semantics. 238 sheet.insertRule(''' 239flt-semantics input[type=range] { 240 appearance: none; 241 -webkit-appearance: none; 242 width: 100%; 243 position: absolute; 244 border: none; 245 top: 0; 246 right: 0; 247 bottom: 0; 248 left: 0; 249}''', sheet.cssRules.length); 250 251 if (browserEngine == BrowserEngine.webkit) { 252 sheet.insertRule( 253 'flt-semantics input[type=range]::-webkit-slider-thumb {' 254 ' -webkit-appearance: none;' 255 '}', 256 sheet.cssRules.length); 257 258 // On iOS, the invisible semantic text field has a visible cursor and 259 // selection highlight. The following 2 CSS rules force everything to be 260 // transparent. 261 sheet.insertRule( 262 'flt-semantics ::selection {' 263 ' background-color: transparent;' 264 '}', 265 sheet.cssRules.length); 266 } 267 sheet.insertRule(''' 268flt-semantics input, 269flt-semantics textarea, 270flt-semantics [contentEditable="true"] { 271 caret-color: transparent; 272} 273''', sheet.cssRules.length); 274 275 // By default on iOS, Safari would highlight the element that's being tapped 276 // on using gray background. This CSS rule disables that. 277 if (browserEngine == BrowserEngine.webkit) { 278 sheet.insertRule(''' 279flt-glass-pane * { 280 -webkit-tap-highlight-color: transparent; 281} 282''', sheet.cssRules.length); 283 } 284 285 final html.BodyElement bodyElement = html.document.body; 286 setElementStyle(bodyElement, 'position', 'fixed'); 287 setElementStyle(bodyElement, 'top', '0'); 288 setElementStyle(bodyElement, 'right', '0'); 289 setElementStyle(bodyElement, 'bottom', '0'); 290 setElementStyle(bodyElement, 'left', '0'); 291 setElementStyle(bodyElement, 'overflow', 'hidden'); 292 setElementStyle(bodyElement, 'padding', '0'); 293 setElementStyle(bodyElement, 'margin', '0'); 294 295 // TODO(yjbanov): fix this when we support KVM I/O. Currently we scroll 296 // using drag, and text selection interferes. 297 setElementStyle(bodyElement, 'user-select', 'none'); 298 setElementStyle(bodyElement, '-webkit-user-select', 'none'); 299 setElementStyle(bodyElement, '-ms-user-select', 'none'); 300 setElementStyle(bodyElement, '-moz-user-select', 'none'); 301 302 // This is required to prevent the browser from doing any native touch 303 // handling. If we don't do this, the browser doesn't report 'pointermove' 304 // events properly. 305 setElementStyle(bodyElement, 'touch-action', 'none'); 306 307 // These are intentionally outrageous font parameters to make sure that the 308 // apps fully specifies their text styles. 309 setElementStyle(bodyElement, 'font', defaultCssFont); 310 setElementStyle(bodyElement, 'color', 'red'); 311 312 for (html.Element viewportMeta 313 in html.document.head.querySelectorAll('meta[name="viewport"]')) { 314 if (assertionsEnabled) { 315 // Filter out the meta tag that we ourselves placed on the page. This is 316 // to avoid UI flicker during hot restart. Hot restart will clean up the 317 // old meta tag synchronously with the first post-restart frame. 318 if (!viewportMeta.hasAttribute('flt-viewport')) { 319 print( 320 'WARNING: found an existing <meta name="viewport"> tag. Flutter ' 321 'Web uses its own viewport configuration for better compatibility ' 322 'with Flutter. This tag will be replaced.', 323 ); 324 } 325 } 326 viewportMeta.remove(); 327 } 328 329 // This removes a previously created meta tag. Note, however, that this does 330 // not remove the meta tag during hot restart. Hot restart resets all static 331 // variables, so this will be null upon hot restart. Instead, this tag is 332 // removed by _clearOnHotRestart. 333 _viewportMeta?.remove(); 334 _viewportMeta = html.MetaElement() 335 ..setAttribute('flt-viewport', '') 336 ..name = 'viewport' 337 ..content = 'width=device-width, initial-scale=1.0, ' 338 'maximum-scale=1.0, user-scalable=no'; 339 html.document.head.append(_viewportMeta); 340 341 // IMPORTANT: the glass pane element must come after the scene element in the DOM node list so 342 // it can intercept input events. 343 _glassPaneElement?.remove(); 344 _glassPaneElement = createElement('flt-glass-pane'); 345 _glassPaneElement.style 346 ..position = 'absolute' 347 ..top = '0' 348 ..right = '0' 349 ..bottom = '0' 350 ..left = '0'; 351 bodyElement.append(_glassPaneElement); 352 353 _sceneHostElement = createElement('flt-scene-host'); 354 355 // Don't allow the scene to receive pointer events. 356 _sceneHostElement.style.pointerEvents = 'none'; 357 358 _glassPaneElement.append(_sceneHostElement); 359 360 EngineSemanticsOwner.instance.autoEnableOnTap(this); 361 PointerBinding(this); 362 363 // Hide the DOM nodes used to render the scene from accessibility, because 364 // the accessibility tree is built from the SemanticsNode tree as a parallel 365 // DOM tree. 366 setElementAttribute(_sceneHostElement, 'aria-hidden', 'true'); 367 368 // We treat browser pixels as device pixels because pointer events, 369 // position, and sizes all use browser pixel as the unit (i.e. "px" in CSS). 370 // Therefore, as far as the framework is concerned the device pixel ratio 371 // is 1.0. 372 window.debugOverrideDevicePixelRatio(1.0); 373 374 if (browserEngine == BrowserEngine.webkit) { 375 // Safari sometimes gives us bogus innerWidth/innerHeight values when the 376 // page loads. When it changes the values to correct ones it does not 377 // notify of the change via `onResize`. As a workaround, we setup a 378 // temporary periodic timer that polls innerWidth and triggers the 379 // resizeListener so that the framework can react to the change. 380 final int initialInnerWidth = html.window.innerWidth; 381 // Counts how many times we checked screen size. We check up to 5 times. 382 int checkCount = 0; 383 Timer.periodic(const Duration(milliseconds: 100), (Timer t) { 384 checkCount += 1; 385 if (initialInnerWidth != html.window.innerWidth) { 386 // Window size changed. Notify. 387 t.cancel(); 388 _metricsDidChange(null); 389 } else if (checkCount > 5) { 390 // Checked enough times. Stop. 391 t.cancel(); 392 } 393 }); 394 } 395 396 if (experimentalUseSkia) { 397 _canvasKitScript?.remove(); 398 _canvasKitScript = html.ScriptElement(); 399 _canvasKitScript.src = canvasKitBaseUrl + 'canvaskit.js'; 400 html.document.head.append(_canvasKitScript); 401 } 402 403 _resizeSubscription = html.window.onResize.listen(_metricsDidChange); 404 } 405 406 /// Called immediately after browser window metrics change. 407 void _metricsDidChange(html.Event event) { 408 if (ui.window.onMetricsChanged != null) { 409 ui.window.onMetricsChanged(); 410 } 411 } 412 413 void focus(html.Element element) { 414 element.focus(); 415 } 416 417 /// Removes all children of a DOM node. 418 void clearDom(html.Node node) { 419 while (node.lastChild != null) { 420 node.lastChild.remove(); 421 } 422 } 423 424 /// The element corresponding to the only child of the root surface. 425 html.Element get _rootApplicationElement { 426 final html.Element lastElement = rootElement.children.last; 427 return lastElement.children.singleWhere((html.Element element) { 428 return element.tagName == 'FLT-SCENE'; 429 }, orElse: () => null); 430 } 431 432 /// Provides haptic feedback. 433 void vibrate(int durationMs) { 434 final html.Navigator navigator = html.window.navigator; 435 if (js_util.hasProperty(navigator, 'vibrate')) { 436 js_util.callMethod(navigator, 'vibrate', <num>[durationMs]); 437 } 438 } 439 440 String get currentHtml => _rootApplicationElement?.outerHtml ?? ''; 441 442 DebugDomRendererFrameStatistics _debugFrameStatistics; 443 444 DebugDomRendererFrameStatistics debugFlushFrameStatistics() { 445 if (!assertionsEnabled) { 446 throw Exception('This code should not be reachable in production.'); 447 } 448 final DebugDomRendererFrameStatistics current = _debugFrameStatistics; 449 _debugFrameStatistics = DebugDomRendererFrameStatistics(); 450 return current; 451 } 452 453 void debugRulerCacheHit() => _debugFrameStatistics.paragraphRulerCacheHits++; 454 void debugRulerCacheMiss() => 455 _debugFrameStatistics.paragraphRulerCacheMisses++; 456 void debugRichTextLayout() => _debugFrameStatistics.richTextLayouts++; 457 void debugPlainTextLayout() => _debugFrameStatistics.plainTextLayouts++; 458} 459 460/// Miscellaneous statistics collecting during a single frame's execution. 461/// 462/// This is useful when profiling the app. This class should only be used when 463/// assertions are enabled and therefore is not suitable for collecting any 464/// time measurements. It is mostly useful for counting certain events. 465class DebugDomRendererFrameStatistics { 466 /// The number of times we reused a previously initialized paragraph ruler to 467 /// measure a paragraph of text. 468 int paragraphRulerCacheHits = 0; 469 470 /// The number of times we had to create a new paragraph ruler to measure a 471 /// paragraph of text. 472 int paragraphRulerCacheMisses = 0; 473 474 /// The number of times we used a paragraph ruler to measure a paragraph of 475 /// text. 476 int get totalParagraphRulerAccesses => 477 paragraphRulerCacheHits + paragraphRulerCacheMisses; 478 479 /// The number of times a paragraph of rich text was laid out this frame. 480 int richTextLayouts = 0; 481 482 /// The number of times a paragraph of plain text was laid out this frame. 483 int plainTextLayouts = 0; 484 485 @override 486 String toString() { 487 return ''' 488Frame statistics: 489 Paragraph ruler cache hits: $paragraphRulerCacheHits 490 Paragraph ruler cache misses: $paragraphRulerCacheMisses 491 Paragraph ruler accesses: $totalParagraphRulerAccesses 492 Rich text layouts: $richTextLayouts 493 Plain text layouts: $plainTextLayouts 494''' 495 .trim(); 496 } 497} 498 499// TODO(yjbanov): Replace this with an explicit initialization function. The 500// lazy initialization of statics makes it very unpredictable, as 501// the constructor has side-effects. 502/// Singleton DOM renderer. 503final DomRenderer domRenderer = DomRenderer(); 504