Web Inspector: REGRESSION (r244157): Timelines: ruler size appears wrong on first...
[WebKit-https.git] / Source / WebInspectorUI / UserInterface / Views / View.js
1 /*
2  * Copyright (C) 2015 Apple Inc. All rights reserved.
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions
6  * are met:
7  * 1. Redistributions of source code must retain the above copyright
8  *    notice, this list of conditions and the following disclaimer.
9  * 2. Redistributions in binary form must reproduce the above copyright
10  *    notice, this list of conditions and the following disclaimer in the
11  *    documentation and/or other materials provided with the distribution.
12  *
13  * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
14  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
15  * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16  * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
17  * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
18  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
19  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
22  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
23  * THE POSSIBILITY OF SUCH DAMAGE.
24  */
25
26 WI.View = class View extends WI.Object
27 {
28     constructor(element)
29     {
30         super();
31
32         this._element = element || document.createElement("div");
33         this._element.__view = this;
34         this._parentView = null;
35         this._subviews = [];
36         this._dirty = false;
37         this._dirtyDescendantsCount = 0;
38         this._isAttachedToRoot = false;
39         this._layoutReason = null;
40         this._didInitialLayout = false;
41     }
42
43     // Static
44
45     static fromElement(element)
46     {
47         if (!element || !(element instanceof HTMLElement))
48             return null;
49
50         if (element.__view instanceof WI.View)
51             return element.__view;
52         return null;
53     }
54
55     static rootView()
56     {
57         if (!WI.View._rootView) {
58             // Since the root view is attached by definition, it does not go through the
59             // normal view attachment process. Simply mark it as attached.
60             WI.View._rootView = new WI.View(document.body);
61             WI.View._rootView._isAttachedToRoot = true;
62         }
63
64         return WI.View._rootView;
65     }
66
67     // Public
68
69     get element() { return this._element; }
70     get layoutPending() { return this._dirty; }
71     get parentView() { return this._parentView; }
72     get subviews() { return this._subviews; }
73     get isAttached() { return this._isAttachedToRoot; }
74
75     isDescendantOf(view)
76     {
77         let parentView = this._parentView;
78         while (parentView) {
79             if (parentView === view)
80                 return true;
81             parentView = parentView.parentView;
82         }
83
84         return false;
85     }
86
87     addSubview(view)
88     {
89         this.insertSubviewBefore(view, null);
90     }
91
92     insertSubviewBefore(view, referenceView)
93     {
94         console.assert(view instanceof WI.View);
95         console.assert(!referenceView || referenceView instanceof WI.View);
96         console.assert(view !== WI.View._rootView, "Root view cannot be a subview.");
97
98         if (this._subviews.includes(view)) {
99             console.assert(false, "Cannot add view that is already a subview.", view);
100             return;
101         }
102
103         const beforeIndex = referenceView ? this._subviews.indexOf(referenceView) : this._subviews.length;
104         if (beforeIndex === -1) {
105             console.assert(false, "Cannot insert view. Invalid reference view.", referenceView);
106             return;
107         }
108
109         this._subviews.insertAtIndex(view, beforeIndex);
110
111         console.assert(!view.element.parentNode || this._element.contains(view.element.parentNode), "Subview DOM element must be a descendant of the parent view element.");
112         if (!view.element.parentNode)
113             this._element.insertBefore(view.element, referenceView ? referenceView.element : null);
114
115         view._didMoveToParent(this);
116     }
117
118     removeSubview(view)
119     {
120         console.assert(view instanceof WI.View);
121         console.assert(this._element.contains(view.element), "Subview DOM element must be a child of the parent view element.");
122
123         let index = this._subviews.lastIndexOf(view);
124         if (index === -1) {
125             console.assert(false, "Cannot remove view which isn't a subview.", view);
126             return;
127         }
128
129         this._subviews.splice(index, 1);
130         view.element.remove();
131
132         view._didMoveToParent(null);
133     }
134
135     removeAllSubviews()
136     {
137         for (let subview of this._subviews)
138             subview._didMoveToParent(null);
139
140         this._subviews = [];
141         this._element.removeChildren();
142     }
143
144     replaceSubview(oldView, newView)
145     {
146         console.assert(oldView !== newView, "Cannot replace subview with itself.");
147         if (oldView === newView)
148             return;
149
150         this.insertSubviewBefore(newView, oldView);
151         this.removeSubview(oldView);
152     }
153
154     updateLayout(layoutReason)
155     {
156         this.cancelLayout();
157
158         this._setLayoutReason(layoutReason);
159         this._layoutSubtree();
160     }
161
162     updateLayoutIfNeeded(layoutReason)
163     {
164         if (!this._dirty && this._didInitialLayout)
165             return;
166
167         this.updateLayout(layoutReason);
168     }
169
170     needsLayout(layoutReason)
171     {
172         this._setLayoutReason(layoutReason);
173
174         if (this._dirty)
175             return;
176
177         WI.View._scheduleLayoutForView(this);
178     }
179
180     cancelLayout()
181     {
182         WI.View._cancelScheduledLayoutForView(this);
183     }
184
185     // Protected
186
187     get layoutReason() { return this._layoutReason; }
188     get didInitialLayout() { return this._didInitialLayout; }
189
190     attached()
191     {
192         // Implemented by subclasses.
193     }
194
195     detached()
196     {
197         // Implemented by subclasses.
198     }
199
200     initialLayout()
201     {
202         // Implemented by subclasses.
203
204         // Called once when the view is shown for the first time.
205         // Views with complex DOM subtrees should create UI elements in
206         // initialLayout rather than at construction time.
207     }
208
209     layout()
210     {
211         // Implemented by subclasses.
212
213         // Not responsible for recursing to child views.
214         // Should not be called directly; use updateLayout() instead.
215     }
216
217     didLayoutSubtree()
218     {
219         // Implemented by subclasses.
220
221         // Called after the view and its entire subtree have finished layout.
222     }
223
224     sizeDidChange()
225     {
226         // Implemented by subclasses.
227
228         // Called after initialLayout, and before layout.
229     }
230
231     // Private
232
233     _didMoveToParent(parentView)
234     {
235         this._parentView = parentView;
236
237         let isAttachedToRoot = this.isDescendantOf(WI.View._rootView);
238         this._didMoveToWindow(isAttachedToRoot);
239
240         if (!this._parentView)
241             return;
242
243         let pendingLayoutsCount = this._dirtyDescendantsCount;
244         if (this._dirty)
245             pendingLayoutsCount++;
246
247         let view = this._parentView;
248         while (view) {
249             view._dirtyDescendantsCount += pendingLayoutsCount;
250             view = view.parentView;
251         }
252     }
253
254     _didMoveToWindow(isAttachedToRoot)
255     {
256         if (this._isAttachedToRoot === isAttachedToRoot)
257             return;
258
259         this._isAttachedToRoot = isAttachedToRoot;
260         if (this._isAttachedToRoot) {
261             WI.View._scheduleLayoutForView(this);
262             this.attached();
263         } else {
264             if (this._dirty)
265                 this.cancelLayout();
266             this.detached();
267         }
268
269         for (let view of this._subviews)
270             view._didMoveToWindow(isAttachedToRoot);
271     }
272
273     _layoutSubtree()
274     {
275         this._dirty = false;
276         this._dirtyDescendantsCount = 0;
277         let isInitialLayout = !this._didInitialLayout;
278
279         if (isInitialLayout) {
280             this.initialLayout();
281             this._didInitialLayout = true;
282         }
283
284         if (this._layoutReason === WI.View.LayoutReason.Resize || isInitialLayout)
285             this.sizeDidChange();
286
287         let savedLayoutReason = this._layoutReason;
288         if (isInitialLayout) {
289             // The initial layout should always be treated as dirty.
290             this._setLayoutReason();
291         }
292
293         this.layout();
294
295         // Ensure that the initial layout override doesn't affects to subviews.
296         this._layoutReason = savedLayoutReason;
297
298         if (WI.settings.enableLayoutFlashing.value)
299             this._drawLayoutFlashingOutline(isInitialLayout);
300
301         for (let view of this._subviews) {
302             view._setLayoutReason(this._layoutReason);
303             view._layoutSubtree();
304         }
305
306         this._layoutReason = null;
307
308         this.didLayoutSubtree();
309     }
310
311     _setLayoutReason(layoutReason)
312     {
313         this._layoutReason = layoutReason || WI.View.LayoutReason.Dirty;
314     }
315
316     _drawLayoutFlashingOutline(isInitialLayout)
317     {
318         if (this._layoutFlashingTimeout)
319             clearTimeout(this._layoutFlashingTimeout);
320         else
321             this._layoutFlashingPreviousOutline = this._element.style.outline;
322
323         let hue = isInitialLayout ? 20 : 40;
324         this._element.style.outline = `1px solid hsla(${hue}, 100%, 51%, 0.8)`;
325
326         this._layoutFlashingTimeout = setTimeout(() => {
327             if (this._element)
328                 this._element.style.outline = this._layoutFlashingPreviousOutline;
329
330             this._layoutFlashingTimeout = undefined;
331             this._layoutFlashingPreviousOutline = null;
332         }, 500);
333     }
334
335     // Layout controller logic
336
337     static _scheduleLayoutForView(view)
338     {
339         view._dirty = true;
340
341         let parentView = view.parentView;
342         while (parentView) {
343             parentView._dirtyDescendantsCount++;
344             parentView = parentView.parentView;
345         }
346
347         if (!view._isAttachedToRoot)
348             return;
349
350         if (WI.View._scheduledLayoutUpdateIdentifier)
351             return;
352
353         WI.View._scheduledLayoutUpdateIdentifier = requestAnimationFrame(WI.View._visitViewTreeForLayout);
354     }
355
356     static _cancelScheduledLayoutForView(view)
357     {
358         let cancelledLayoutsCount = view._dirtyDescendantsCount;
359         if (view.layoutPending)
360             cancelledLayoutsCount++;
361
362         let parentView = view.parentView;
363         while (parentView) {
364             parentView._dirtyDescendantsCount = Math.max(0, parentView._dirtyDescendantsCount - cancelledLayoutsCount);
365             parentView = parentView.parentView;
366         }
367
368         view._dirty = false;
369
370         if (!WI.View._scheduledLayoutUpdateIdentifier)
371             return;
372
373         let rootView = WI.View._rootView;
374         if (!rootView || rootView._dirtyDescendantsCount)
375             return;
376
377         // No views need layout, so cancel the pending requestAnimationFrame.
378         cancelAnimationFrame(WI.View._scheduledLayoutUpdateIdentifier);
379         WI.View._scheduledLayoutUpdateIdentifier = undefined;
380     }
381
382     static _visitViewTreeForLayout()
383     {
384         console.assert(WI.View._rootView, "Cannot layout view tree without a root.");
385
386         WI.View._scheduledLayoutUpdateIdentifier = undefined;
387
388         let views = [WI.View._rootView];
389         while (views.length) {
390             let view = views.shift();
391             if (view.layoutPending)
392                 view._layoutSubtree();
393             else if (view._dirtyDescendantsCount) {
394                 views = views.concat(view.subviews);
395                 view._dirtyDescendantsCount = 0;
396             }
397         }
398     }
399 };
400
401 WI.View.LayoutReason = {
402     Dirty: Symbol("layout-reason-dirty"),
403     Resize: Symbol("layout-reason-resize")
404 };
405
406 WI.View._rootView = null;
407 WI.View._scheduledLayoutUpdateIdentifier = undefined;