blob: 472f99121d42808c3700cbee62ab4ed9d761ecb1 [file] [log] [blame]
/*
* 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(layoutReason)
{
if (!this._dirty && this._didInitialLayout)
return;
this.updateLayout(layoutReason);
}
needsLayout(layoutReason)
{
this._setLayoutReason(layoutReason);
if (this._dirty)
return;
WebInspector.View._scheduleLayoutForView(this);
}
cancelLayout()
{
WebInspector.View._cancelScheduledLayoutForView(this);
}
// Protected
get layoutReason() { return this._layoutReason; }
get didInitialLayout() { return this._didInitialLayout; }
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;