• 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
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