summaryrefslogtreecommitdiff
path: root/Source/WebInspectorUI/UserInterface/Views/View.js
diff options
context:
space:
mode:
Diffstat (limited to 'Source/WebInspectorUI/UserInterface/Views/View.js')
-rw-r--r--Source/WebInspectorUI/UserInterface/Views/View.js342
1 files changed, 342 insertions, 0 deletions
diff --git a/Source/WebInspectorUI/UserInterface/Views/View.js b/Source/WebInspectorUI/UserInterface/Views/View.js
new file mode 100644
index 000000000..ace04b96b
--- /dev/null
+++ b/Source/WebInspectorUI/UserInterface/Views/View.js
@@ -0,0 +1,342 @@
+/*
+ * Copyright (C) 2015 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
+ * THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+WebInspector.View = class View extends WebInspector.Object
+{
+ constructor(element)
+ {
+ super();
+
+ this._element = element || document.createElement("div");
+ this._element.__view = this;
+ this._parentView = null;
+ this._subviews = [];
+ this._dirty = false;
+ this._dirtyDescendantsCount = 0;
+ this._needsLayoutWhenAttachedToRoot = false;
+ this._isAttachedToRoot = false;
+ this._layoutReason = null;
+ this._didInitialLayout = false;
+ }
+
+ // Static
+
+ static rootView()
+ {
+ if (!WebInspector.View._rootView)
+ WebInspector.View._rootView = new WebInspector.View(document.body);
+
+ return WebInspector.View._rootView;
+ }
+
+ // Public
+
+ get element()
+ {
+ return this._element;
+ }
+
+ get layoutPending()
+ {
+ return this._dirty;
+ }
+
+ get parentView()
+ {
+ return this._parentView;
+ }
+
+ get subviews()
+ {
+ return this._subviews;
+ }
+
+ isDescendantOf(view)
+ {
+ let parentView = this._parentView;
+ while (parentView) {
+ if (parentView === view)
+ return true;
+ parentView = parentView.parentView;
+ }
+
+ return false;
+ }
+
+ addSubview(view)
+ {
+ this.insertSubviewBefore(view, null);
+ }
+
+ insertSubviewBefore(view, referenceView)
+ {
+ console.assert(view instanceof WebInspector.View);
+ console.assert(!referenceView || referenceView instanceof WebInspector.View);
+ console.assert(view !== WebInspector.View._rootView, "Root view cannot be a subview.");
+
+ if (this._subviews.includes(view)) {
+ console.assert(false, "Cannot add view that is already a subview.", view);
+ return;
+ }
+
+ const beforeIndex = referenceView ? this._subviews.indexOf(referenceView) : this._subviews.length;
+ if (beforeIndex === -1) {
+ console.assert(false, "Cannot insert view. Invalid reference view.", referenceView);
+ return;
+ }
+
+ this._subviews.insertAtIndex(view, beforeIndex);
+
+ if (!view.element.parentNode)
+ this._element.insertBefore(view.element, referenceView ? referenceView.element : null);
+
+ view.didMoveToParent(this);
+ }
+
+ removeSubview(view)
+ {
+ console.assert(view instanceof WebInspector.View);
+ console.assert(view.element.parentNode === this._element, "Subview DOM element must be a child of the parent view element.");
+
+ if (!this._subviews.includes(view)) {
+ console.assert(false, "Cannot remove view which isn't a subview.", view);
+ return;
+ }
+
+ this._subviews.remove(view, true);
+ this._element.removeChild(view.element);
+
+ view.didMoveToParent(null);
+ }
+
+ replaceSubview(oldView, newView)
+ {
+ console.assert(oldView !== newView, "Cannot replace subview with itself.");
+
+ this.insertSubviewBefore(newView, oldView);
+ this.removeSubview(oldView);
+ }
+
+ updateLayout(layoutReason)
+ {
+ this.cancelLayout();
+
+ this._setLayoutReason(layoutReason);
+ this._layoutSubtree();
+ }
+
+ updateLayoutIfNeeded()
+ {
+ if (!this._dirty && this._didInitialLayout)
+ return;
+
+ this.updateLayout();
+ }
+
+ needsLayout(layoutReason)
+ {
+ this._setLayoutReason(layoutReason);
+
+ if (this._dirty)
+ return;
+
+ WebInspector.View._scheduleLayoutForView(this);
+ }
+
+ cancelLayout()
+ {
+ WebInspector.View._cancelScheduledLayoutForView(this);
+ }
+
+ // Protected
+
+ get layoutReason() { return this._layoutReason; }
+
+ didMoveToWindow(isAttachedToRoot)
+ {
+ this._isAttachedToRoot = isAttachedToRoot;
+
+ if (this._isAttachedToRoot && this._needsLayoutWhenAttachedToRoot) {
+ WebInspector.View._scheduleLayoutForView(this);
+ this._needsLayoutWhenAttachedToRoot = false;
+ }
+
+ for (let view of this._subviews)
+ view.didMoveToWindow(isAttachedToRoot);
+ }
+
+ didMoveToParent(parentView)
+ {
+ this._parentView = parentView;
+
+ let isAttachedToRoot = this.isDescendantOf(WebInspector.View._rootView);
+ this.didMoveToWindow(isAttachedToRoot);
+
+ if (!this._parentView)
+ return;
+
+ let pendingLayoutsCount = this._dirtyDescendantsCount;
+ if (this._dirty)
+ pendingLayoutsCount++;
+
+ let view = this._parentView;
+ while (view) {
+ view._dirtyDescendantsCount += pendingLayoutsCount;
+ view = view.parentView;
+ }
+ }
+
+ initialLayout()
+ {
+ // Implemented by subclasses.
+
+ // Called once when the view is shown for the first time.
+ // Views with complex DOM subtrees should create UI elements in
+ // initialLayout rather than at construction time.
+ }
+
+ layout()
+ {
+ // Implemented by subclasses.
+
+ // Not responsible for recursing to child views.
+ // Should not be called directly; use updateLayout() instead.
+ }
+
+ sizeDidChange()
+ {
+ // Implemented by subclasses.
+
+ // Called after initialLayout, and before layout.
+ }
+
+ // Private
+
+ _layoutSubtree()
+ {
+ this._dirty = false;
+ this._dirtyDescendantsCount = 0;
+
+ if (!this._didInitialLayout) {
+ this.initialLayout();
+ this._didInitialLayout = true;
+ }
+
+ if (this._layoutReason === WebInspector.View.LayoutReason.Resize)
+ this.sizeDidChange();
+
+ this.layout();
+
+ for (let view of this._subviews) {
+ view._setLayoutReason(this._layoutReason);
+ view._layoutSubtree();
+ }
+
+ this._layoutReason = null;
+ }
+
+ _setLayoutReason(layoutReason)
+ {
+ if (this._layoutReason === WebInspector.View.LayoutReason.Resize)
+ return;
+
+ this._layoutReason = layoutReason || WebInspector.View.LayoutReason.Dirty;
+ }
+
+ // Layout controller logic
+
+ static _scheduleLayoutForView(view)
+ {
+ view._dirty = true;
+
+ let parentView = view.parentView;
+ while (parentView) {
+ parentView._dirtyDescendantsCount++;
+ parentView = parentView.parentView;
+ }
+
+ if (!view._isAttachedToRoot) {
+ // Don't schedule layout of the view unless it is a descendant of the root view.
+ // When it moves to a rooted view tree, schedule an initial layout.
+ view._needsLayoutWhenAttachedToRoot = true;
+ return;
+ }
+
+ if (WebInspector.View._scheduledLayoutUpdateIdentifier)
+ return;
+
+ WebInspector.View._scheduledLayoutUpdateIdentifier = requestAnimationFrame(WebInspector.View._visitViewTreeForLayout);
+ }
+
+ static _cancelScheduledLayoutForView(view)
+ {
+ let cancelledLayoutsCount = view._dirtyDescendantsCount;
+ if (view.layoutPending)
+ cancelledLayoutsCount++;
+
+ let parentView = view.parentView;
+ while (parentView) {
+ parentView._dirtyDescendantsCount = Math.max(0, parentView._dirtyDescendantsCount - cancelledLayoutsCount);
+ parentView = parentView.parentView;
+ }
+
+ if (!WebInspector.View._scheduledLayoutUpdateIdentifier)
+ return;
+
+ let rootView = WebInspector.View._rootView;
+ if (!rootView || rootView._dirtyDescendantsCount)
+ return;
+
+ // No views need layout, so cancel the pending requestAnimationFrame.
+ cancelAnimationFrame(WebInspector.View._scheduledLayoutUpdateIdentifier);
+ WebInspector.View._scheduledLayoutUpdateIdentifier = undefined;
+ }
+
+ static _visitViewTreeForLayout()
+ {
+ console.assert(WebInspector.View._rootView, "Cannot layout view tree without a root.");
+
+ WebInspector.View._scheduledLayoutUpdateIdentifier = undefined;
+
+ let views = [WebInspector.View._rootView];
+ while (views.length) {
+ let view = views.shift();
+ if (view.layoutPending)
+ view._layoutSubtree();
+ else if (view._dirtyDescendantsCount) {
+ views = views.concat(view.subviews);
+ view._dirtyDescendantsCount = 0;
+ }
+ }
+ }
+};
+
+WebInspector.View.LayoutReason = {
+ Dirty: Symbol("layout-reason-dirty"),
+ Resize: Symbol("layout-reason-resize")
+};
+
+WebInspector.View._rootView = null;
+WebInspector.View._scheduledLayoutUpdateIdentifier = undefined;