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.



/// Manages DOM elements and the DOM structure for a [ui.FlutterView].
///
/// Here's the general DOM structure of a Flutter View:
///
/// [rootElement] <flutter-view>
///   |
///   +- [platformViewsHost] <flt-glass-pane>
///   |    |
///   |    +- [renderingHost] #shadow-root
///   |    |    |
///   |    |    +- <flt-semantics-placeholder>
///   |    |    |
///   |    |    +- [sceneHost] <flt-scene-host>
///   |    |    |    |
///   |    |    |    +- <flt-scene>
///   |    |    |
///   |    |    +- <style>
///   |    |
///   |    +- ...platform views
///   |
///   +- [textEditingHost] <flt-text-editing-host>
///   |    |
///   |    +- ...text fields
///   |
///   +- [semanticsHost] <flt-semantics-host>
///   |    |
///   |    +- ...semantics nodes
///   |
///   +- <style>
///
class DomManager {
  factory DomManager({required double devicePixelRatio}) {
    final DomElement rootElement = domDocument.createElement(DomManager.flutterViewTagName);
    final DomElement platformViewsHost = domDocument.createElement(DomManager.glassPaneTagName);
    final DomShadowRoot renderingHost = _attachShadowRoot(platformViewsHost);
    final DomElement sceneHost = domDocument.createElement(DomManager.sceneHostTagName);
    final DomElement textEditingHost = domDocument.createElement(DomManager.textEditingHostTagName);
    final DomElement semanticsHost = domDocument.createElement(DomManager.semanticsHostTagName);

    // Root element children.
    rootElement.appendChild(platformViewsHost);
    rootElement.appendChild(textEditingHost);

    // The semantic host goes last because hit-test order-wise it must be
    // first. If semantics goes under the scene host, platform views will
    // obscure semantic elements.
    //
    // You may be wondering: wouldn't semantics obscure platform views and
    // make then not accessible? At least with some careful planning, that
    // should not be the case. The semantics tree makes all of its non-leaf
    // elements transparent. This way, if a platform view appears among other
    // interactive Flutter widgets, as long as those widgets do not intersect
    // with the platform view, the platform view will be reachable.
    rootElement.appendChild(semanticsHost);

    // Rendering host (shadow root) children.

    renderingHost.append(sceneHost);

    // Styling.

    StyleManager.attachGlobalStyles(
      node: rootElement,
      styleId: 'flt-text-editing-stylesheet',
      styleNonce: configuration.nonce,
      cssSelectorPrefix: DomManager.flutterViewTagName,
    );

    StyleManager.attachGlobalStyles(
      node: renderingHost,
      styleId: 'flt-internals-stylesheet',
      styleNonce: configuration.nonce,
      cssSelectorPrefix: '',
    );

    StyleManager.styleSceneHost(
      sceneHost,
      debugShowSemanticsNodes: configuration.debugShowSemanticsNodes,
    );

    StyleManager.styleSemanticsHost(semanticsHost, devicePixelRatio);

    return DomManager._(
      rootElement: rootElement,
      platformViewsHost: platformViewsHost,
      renderingHost: renderingHost,
      sceneHost: sceneHost,
      textEditingHost: textEditingHost,
      semanticsHost: semanticsHost,
    );
  }

  DomManager._({
    required this.rootElement,
    required this.platformViewsHost,
    required this.renderingHost,
    required this.sceneHost,
    required this.textEditingHost,
    required this.semanticsHost,
  });

  /// The tag name for the Flutter View root element.
  static const String flutterViewTagName = 'flutter-view';

  /// The tag name for the glass-pane.
  static const String glassPaneTagName = 'flt-glass-pane';

  /// The tag name for the scene host.
  static const String sceneHostTagName = 'flt-scene-host';

  /// The tag name for the text editing host.
  static const String textEditingHostTagName = 'flt-text-editing-host';

  /// The tag name for the semantics host.
  static const String semanticsHostTagName = 'flt-semantics-host';

  /// The root DOM element for the entire Flutter View.
  ///
  /// This is where input events are captured, such as pointer events.
  ///
  /// If semantics is enabled, this element also contains the semantics DOM tree,
  /// which captures semantics input events.
  final DomElement rootElement;

  /// Hosts all platform view elements.
  final DomElement platformViewsHost;

  /// Hosts all rendering elements and canvases.
  final DomShadowRoot renderingHost;

  /// Hosts the <flt-scene> element.
  ///
  /// This element is created and inserted in the HTML DOM once. It is never
  /// removed or moved. However the <flt-scene> inside of it may be replaced.
  final DomElement sceneHost;

  /// Hosts all text editing elements.
  final DomElement textEditingHost;

  /// Hosts the semantics tree.
  ///
  /// This element is in front of the [renderingHost] and [platformViewsHost].
  /// Otherwise, the phone will disable focusing by touch, only by tabbing
  /// around the UI.
  final DomElement semanticsHost;

  DomElement? _lastSceneElement;

  /// Inserts the [sceneElement] into the DOM and removes the existing scene (if
  /// any).
  ///
  /// The [sceneElement] is inserted  as a child of the <flt-scene-host> element
  /// inside the [renderingHost].
  ///
  /// If the [sceneElement] has already been inserted, this method does nothing
  /// to avoid unnecessary DOM mutations. This is both faster and more correct,
  /// because moving DOM nodes loses internal state, such as text selection.
  void setScene(DomElement sceneElement) {
    if (sceneElement != _lastSceneElement) {
      _lastSceneElement?.remove();
      _lastSceneElement = sceneElement;
      sceneHost.append(sceneElement);
    }
  }

  /// Injects a platform view with [platformViewId] into [platformViewsHost].
  ///
  /// If the platform view is already injected, this method does *nothing*.
  ///
  /// The `platformViewsHost` can only be different if `platformViewId` is moving
  /// from one [FlutterView] to another. In that case, the browser will move the
  /// slot contents from the old `platformViewsHost` to the new one, but that
  /// will cause the platformView to reset its state (an iframe will re-render,
  /// text selections will be lost, video playback interrupted, etc...)
  ///
  /// Try not to move platform views across views!
  void injectPlatformView(int platformViewId) {
    // For now, we don't need anything fancier. If needed, this can be converted
    // to a PlatformViewStrategy class for each web-renderer backend?
    final DomElement? pv = PlatformViewManager.instance.getSlottedContent(platformViewId);
    if (pv == null) {
      domWindow.console.debug(
        'Failed to inject Platform View Id: $platformViewId. '
        'Render seems to be happening before a `flutter/platform_views:create` platform message!',
      );
      return;
    }
    // If pv is already a descendant of platformViewsHost -> noop
    if (pv.parent == platformViewsHost) {
      return;
    }
    platformViewsHost.append(pv);
  }
}

DomShadowRoot _attachShadowRoot(DomElement element) {
  assert(
    getJsProperty<Object?>(element, 'attachShadow') != null,
    'ShadowDOM is not supported in this browser.',
  );

  return element.attachShadow(<String, dynamic>{
    'mode': 'open',
    // This needs to stay false to prevent issues like this:
    // - https://github.com/flutter/flutter/issues/85759
    'delegatesFocus': false,
  });
}
