OLD | NEW |
| (Empty) |
1 // Copyright 2015 The Chromium 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 | |
5 import 'dart:math' as math; | |
6 import 'dart:sky' as sky; | |
7 import 'dart:sky' show Point, Size, Rect, Color, Paint, Path; | |
8 | |
9 import '../node.dart'; | |
10 import '../scheduler.dart' as scheduler; | |
11 | |
12 export 'dart:sky' show Point, Size, Rect, Color, Paint, Path; | |
13 | |
14 class ParentData { | |
15 void detach() { | |
16 detachSiblings(); | |
17 } | |
18 void detachSiblings() { } // workaround for lack of inter-class mixins in Dart | |
19 void merge(ParentData other) { | |
20 // override this in subclasses to merge in data from other into this | |
21 assert(other.runtimeType == this.runtimeType); | |
22 } | |
23 String toString() => '<none>'; | |
24 } | |
25 | |
26 const kLayoutDirections = 4; | |
27 | |
28 class RenderObjectDisplayList extends sky.PictureRecorder { | |
29 RenderObjectDisplayList(double width, double height) : super(width, height); | |
30 void paintChild(RenderObject child, Point position) { | |
31 translate(position.x, position.y); | |
32 child.paint(this); | |
33 translate(-position.x, -position.y); | |
34 } | |
35 } | |
36 | |
37 abstract class RenderObject extends AbstractNode { | |
38 | |
39 // LAYOUT | |
40 | |
41 // parentData is only for use by the RenderObject that actually lays this | |
42 // node out, and any other nodes who happen to know exactly what | |
43 // kind of node that is. | |
44 dynamic parentData; // TODO(ianh): change the type of this back to ParentData
once the analyzer is cleverer | |
45 void setParentData(RenderObject child) { | |
46 // override this to setup .parentData correctly for your class | |
47 assert(!debugDoingLayout); | |
48 assert(!debugDoingPaint); | |
49 if (child.parentData is! ParentData) | |
50 child.parentData = new ParentData(); | |
51 } | |
52 | |
53 void adoptChild(RenderObject child) { // only for use by subclasses | |
54 // call this whenever you decide a node is a child | |
55 assert(!debugDoingLayout); | |
56 assert(!debugDoingPaint); | |
57 assert(child != null); | |
58 setParentData(child); | |
59 super.adoptChild(child); | |
60 markNeedsLayout(); | |
61 } | |
62 void dropChild(RenderObject child) { // only for use by subclasses | |
63 assert(!debugDoingLayout); | |
64 assert(!debugDoingPaint); | |
65 assert(child != null); | |
66 assert(child.parentData != null); | |
67 child.parentData.detach(); | |
68 if (child._relayoutSubtreeRoot != child) { | |
69 child._relayoutSubtreeRoot = null; | |
70 child._needsLayout = true; | |
71 } | |
72 super.dropChild(child); | |
73 markNeedsLayout(); | |
74 } | |
75 | |
76 static List<RenderObject> _nodesNeedingLayout = new List<RenderObject>(); | |
77 static bool _debugDoingLayout = false; | |
78 static bool get debugDoingLayout => _debugDoingLayout; | |
79 bool _needsLayout = true; | |
80 bool get needsLayout => _needsLayout; | |
81 RenderObject _relayoutSubtreeRoot; | |
82 dynamic _constraints; | |
83 dynamic get constraints => _constraints; | |
84 bool debugAncestorsAlreadyMarkedNeedsLayout() { | |
85 if (_relayoutSubtreeRoot == null) | |
86 return true; // we haven't yet done layout even once, so there's nothing f
or us to do | |
87 RenderObject node = this; | |
88 while (node != _relayoutSubtreeRoot) { | |
89 assert(node._relayoutSubtreeRoot == _relayoutSubtreeRoot); | |
90 assert(node.parent != null); | |
91 node = node.parent as RenderObject; | |
92 if (!node._needsLayout) | |
93 return false; | |
94 } | |
95 assert(node._relayoutSubtreeRoot == node); | |
96 return true; | |
97 } | |
98 void markNeedsLayout() { | |
99 assert(!debugDoingLayout); | |
100 assert(!debugDoingPaint); | |
101 if (_needsLayout) { | |
102 assert(debugAncestorsAlreadyMarkedNeedsLayout()); | |
103 return; | |
104 } | |
105 _needsLayout = true; | |
106 assert(_relayoutSubtreeRoot != null); | |
107 if (_relayoutSubtreeRoot != this) { | |
108 final parent = this.parent; // TODO(ianh): Remove this once the analyzer i
s cleverer | |
109 assert(parent is RenderObject); | |
110 parent.markNeedsLayout(); | |
111 assert(parent == this.parent); // TODO(ianh): Remove this once the analyze
r is cleverer | |
112 } else { | |
113 _nodesNeedingLayout.add(this); | |
114 scheduler.ensureVisualUpdate(); | |
115 } | |
116 } | |
117 void scheduleInitialLayout() { | |
118 assert(attached); | |
119 assert(parent == null); | |
120 assert(_relayoutSubtreeRoot == null); | |
121 _relayoutSubtreeRoot = this; | |
122 _nodesNeedingLayout.add(this); | |
123 scheduler.ensureVisualUpdate(); | |
124 } | |
125 static void flushLayout() { | |
126 _debugDoingLayout = true; | |
127 List<RenderObject> dirtyNodes = _nodesNeedingLayout; | |
128 _nodesNeedingLayout = new List<RenderObject>(); | |
129 dirtyNodes..sort((a, b) => a.depth - b.depth)..forEach((node) { | |
130 if (node._needsLayout && node.attached) | |
131 node.layoutWithoutResize(); | |
132 }); | |
133 _debugDoingLayout = false; | |
134 } | |
135 void layoutWithoutResize() { | |
136 try { | |
137 assert(_relayoutSubtreeRoot == this); | |
138 performLayout(); | |
139 } catch (e, stack) { | |
140 print('Exception raised during layout of ${this}: ${e}'); | |
141 print(stack); | |
142 return; | |
143 } | |
144 _needsLayout = false; | |
145 } | |
146 void layout(dynamic constraints, { bool parentUsesSize: false }) { | |
147 final parent = this.parent; // TODO(ianh): Remove this once the analyzer is
cleverer | |
148 RenderObject relayoutSubtreeRoot; | |
149 if (!parentUsesSize || sizedByParent || parent is! RenderObject) | |
150 relayoutSubtreeRoot = this; | |
151 else | |
152 relayoutSubtreeRoot = parent._relayoutSubtreeRoot; | |
153 assert(parent == this.parent); // TODO(ianh): Remove this once the analyzer
is cleverer | |
154 if (!needsLayout && constraints == _constraints && relayoutSubtreeRoot == _r
elayoutSubtreeRoot) | |
155 return; | |
156 _constraints = constraints; | |
157 _relayoutSubtreeRoot = relayoutSubtreeRoot; | |
158 if (sizedByParent) | |
159 performResize(); | |
160 performLayout(); | |
161 _needsLayout = false; | |
162 markNeedsPaint(); | |
163 assert(parent == this.parent); // TODO(ianh): Remove this once the analyzer
is cleverer | |
164 } | |
165 bool get sizedByParent => false; // return true if the constraints are the onl
y input to the sizing algorithm (in particular, child nodes have no impact) | |
166 void performResize(); // set the local dimensions, using only the constraints
(only called if sizedByParent is true) | |
167 void performLayout(); | |
168 // Override this to perform relayout without your parent's | |
169 // involvement. | |
170 // | |
171 // This is called during layout. If sizedByParent is true, then | |
172 // performLayout() should not change your dimensions, only do that | |
173 // in performResize(). If sizedByParent is false, then set both | |
174 // your dimensions and do your children's layout here. | |
175 // | |
176 // When calling layout() on your children, pass in | |
177 // "parentUsesSize: true" if your size or layout is dependent on | |
178 // your child's size. | |
179 | |
180 // when the parent has rotated (e.g. when the screen has been turned | |
181 // 90 degrees), immediately prior to layout() being called for the | |
182 // new dimensions, rotate() is called with the old and new angles. | |
183 // The next time paint() is called, the coordinate space will have | |
184 // been rotated N quarter-turns clockwise, where: | |
185 // N = newAngle-oldAngle | |
186 // ...but the rendering is expected to remain the same, pixel for | |
187 // pixel, on the output device. Then, the layout() method or | |
188 // equivalent will be invoked. | |
189 | |
190 void rotate({ | |
191 int oldAngle, // 0..3 | |
192 int newAngle, // 0..3 | |
193 Duration time | |
194 }) { } | |
195 | |
196 | |
197 // PAINTING | |
198 | |
199 static bool debugDoingPaint = false; | |
200 void markNeedsPaint() { | |
201 assert(!debugDoingPaint); | |
202 scheduler.ensureVisualUpdate(); | |
203 } | |
204 void paint(RenderObjectDisplayList canvas) { } | |
205 | |
206 | |
207 // EVENTS | |
208 | |
209 void handleEvent(sky.Event event, HitTestEntry entry) { | |
210 // override this if you have a client, to hand it to the client | |
211 // override this if you want to do anything with the event | |
212 } | |
213 | |
214 | |
215 // HIT TESTING | |
216 | |
217 // RenderObject subclasses are expected to have a method like the | |
218 // following (with the signature being whatever passes for coordinates | |
219 // for this particular class): | |
220 // bool hitTest(HitTestResult result, { Point position }) { | |
221 // // If (x,y) is not inside this node, then return false. (You | |
222 // // can assume that the given coordinate is inside your | |
223 // // dimensions. You only need to check this if you're an | |
224 // // irregular shape, e.g. if you have a hole.) | |
225 // // Otherwise: | |
226 // // For each child that intersects x,y, in z-order starting from the top, | |
227 // // call hitTest() for that child, passing it /result/, and the coordinate
s | |
228 // // converted to the child's coordinate origin, and stop at the first chil
d | |
229 // // that returns true. | |
230 // // Then, add yourself to /result/, and return true. | |
231 // } | |
232 // You must not add yourself to /result/ if you return false. | |
233 | |
234 | |
235 String toString([String prefix = '']) { | |
236 String header = '${runtimeType}'; | |
237 if (_relayoutSubtreeRoot != null && _relayoutSubtreeRoot != this) { | |
238 int count = 1; | |
239 RenderObject target = parent; | |
240 while (target != null && target != _relayoutSubtreeRoot) { | |
241 target = target.parent as RenderObject; | |
242 count += 1; | |
243 } | |
244 header += ' relayoutSubtreeRoot=up$count'; | |
245 } | |
246 if (_needsLayout) | |
247 header += ' NEEDS-LAYOUT'; | |
248 if (!attached) | |
249 header += ' DETACHED'; | |
250 prefix += ' '; | |
251 return '${header}\n${debugDescribeSettings(prefix)}${debugDescribeChildren(p
refix)}'; | |
252 } | |
253 String debugDescribeSettings(String prefix) => '${prefix}parentData: ${parentD
ata}\n'; | |
254 String debugDescribeChildren(String prefix) => ''; | |
255 | |
256 } | |
257 | |
258 class HitTestEntry { | |
259 const HitTestEntry(this.target); | |
260 | |
261 final RenderObject target; | |
262 } | |
263 | |
264 class HitTestResult { | |
265 final List<HitTestEntry> path = new List<HitTestEntry>(); | |
266 | |
267 void add(HitTestEntry data) { | |
268 path.add(data); | |
269 } | |
270 } | |
271 | |
272 double clamp({ double min: 0.0, double value: 0.0, double max: double.INFINITY }
) { | |
273 assert(min != null); | |
274 assert(value != null); | |
275 assert(max != null); | |
276 return math.max(min, math.min(max, value)); | |
277 } | |
278 | |
279 | |
280 // GENERIC MIXIN FOR RENDER NODES WITH ONE CHILD | |
281 | |
282 abstract class RenderObjectWithChildMixin<ChildType extends RenderObject> implem
ents RenderObject { | |
283 ChildType _child; | |
284 ChildType get child => _child; | |
285 void set child (ChildType value) { | |
286 if (_child != null) | |
287 dropChild(_child); | |
288 _child = value; | |
289 if (_child != null) | |
290 adoptChild(_child); | |
291 } | |
292 void attachChildren() { | |
293 if (_child != null) | |
294 _child.attach(); | |
295 } | |
296 void detachChildren() { | |
297 if (_child != null) | |
298 _child.detach(); | |
299 } | |
300 String debugDescribeChildren(String prefix) { | |
301 if (child != null) | |
302 return '${prefix}child: ${child.toString(prefix)}'; | |
303 return ''; | |
304 } | |
305 } | |
306 | |
307 | |
308 // GENERIC MIXIN FOR RENDER NODES WITH A LIST OF CHILDREN | |
309 | |
310 abstract class ContainerParentDataMixin<ChildType extends RenderObject> { | |
311 ChildType previousSibling; | |
312 ChildType nextSibling; | |
313 void detachSiblings() { | |
314 if (previousSibling != null) { | |
315 assert(previousSibling.parentData is ContainerParentDataMixin<ChildType>); | |
316 assert(previousSibling != this); | |
317 assert(previousSibling.parentData.nextSibling == this); | |
318 previousSibling.parentData.nextSibling = nextSibling; | |
319 } | |
320 if (nextSibling != null) { | |
321 assert(nextSibling.parentData is ContainerParentDataMixin<ChildType>); | |
322 assert(nextSibling != this); | |
323 assert(nextSibling.parentData.previousSibling == this); | |
324 nextSibling.parentData.previousSibling = previousSibling; | |
325 } | |
326 previousSibling = null; | |
327 nextSibling = null; | |
328 } | |
329 } | |
330 | |
331 abstract class ContainerRenderObjectMixin<ChildType extends RenderObject, Parent
DataType extends ContainerParentDataMixin<ChildType>> implements RenderObject { | |
332 // abstract class that has only InlineNode children | |
333 | |
334 bool _debugUltimatePreviousSiblingOf(ChildType child, { ChildType equals }) { | |
335 assert(child.parentData is ParentDataType); | |
336 while (child.parentData.previousSibling != null) { | |
337 assert(child.parentData.previousSibling != child); | |
338 child = child.parentData.previousSibling; | |
339 assert(child.parentData is ParentDataType); | |
340 } | |
341 return child == equals; | |
342 } | |
343 bool _debugUltimateNextSiblingOf(ChildType child, { ChildType equals }) { | |
344 assert(child.parentData is ParentDataType); | |
345 while (child.parentData.nextSibling != null) { | |
346 assert(child.parentData.nextSibling != child); | |
347 child = child.parentData.nextSibling; | |
348 assert(child.parentData is ParentDataType); | |
349 } | |
350 return child == equals; | |
351 } | |
352 | |
353 ChildType _firstChild; | |
354 ChildType _lastChild; | |
355 void add(ChildType child, { ChildType before }) { | |
356 assert(child != this); | |
357 assert(before != this); | |
358 assert(child != before); | |
359 assert(child != _firstChild); | |
360 assert(child != _lastChild); | |
361 adoptChild(child); | |
362 assert(child.parentData is ParentDataType); | |
363 assert(child.parentData.nextSibling == null); | |
364 assert(child.parentData.previousSibling == null); | |
365 if (before == null) { | |
366 // append at the end (_lastChild) | |
367 child.parentData.previousSibling = _lastChild; | |
368 if (_lastChild != null) { | |
369 assert(_lastChild.parentData is ParentDataType); | |
370 _lastChild.parentData.nextSibling = child; | |
371 } | |
372 _lastChild = child; | |
373 if (_firstChild == null) | |
374 _firstChild = child; | |
375 } else { | |
376 assert(_firstChild != null); | |
377 assert(_lastChild != null); | |
378 assert(_debugUltimatePreviousSiblingOf(before, equals: _firstChild)); | |
379 assert(_debugUltimateNextSiblingOf(before, equals: _lastChild)); | |
380 assert(before.parentData is ParentDataType); | |
381 if (before.parentData.previousSibling == null) { | |
382 // insert at the start (_firstChild); we'll end up with two or more chil
dren | |
383 assert(before == _firstChild); | |
384 child.parentData.nextSibling = before; | |
385 before.parentData.previousSibling = child; | |
386 _firstChild = child; | |
387 } else { | |
388 // insert in the middle; we'll end up with three or more children | |
389 // set up links from child to siblings | |
390 child.parentData.previousSibling = before.parentData.previousSibling; | |
391 child.parentData.nextSibling = before; | |
392 // set up links from siblings to child | |
393 assert(child.parentData.previousSibling.parentData is ParentDataType); | |
394 assert(child.parentData.nextSibling.parentData is ParentDataType); | |
395 child.parentData.previousSibling.parentData.nextSibling = child; | |
396 child.parentData.nextSibling.parentData.previousSibling = child; | |
397 assert(before.parentData.previousSibling == child); | |
398 } | |
399 } | |
400 } | |
401 void remove(ChildType child) { | |
402 assert(child.parentData is ParentDataType); | |
403 assert(_debugUltimatePreviousSiblingOf(child, equals: _firstChild)); | |
404 assert(_debugUltimateNextSiblingOf(child, equals: _lastChild)); | |
405 if (child.parentData.previousSibling == null) { | |
406 assert(_firstChild == child); | |
407 _firstChild = child.parentData.nextSibling; | |
408 } else { | |
409 assert(child.parentData.previousSibling.parentData is ParentDataType); | |
410 child.parentData.previousSibling.parentData.nextSibling = child.parentData
.nextSibling; | |
411 } | |
412 if (child.parentData.nextSibling == null) { | |
413 assert(_lastChild == child); | |
414 _lastChild = child.parentData.previousSibling; | |
415 } else { | |
416 assert(child.parentData.nextSibling.parentData is ParentDataType); | |
417 child.parentData.nextSibling.parentData.previousSibling = child.parentData
.previousSibling; | |
418 } | |
419 child.parentData.previousSibling = null; | |
420 child.parentData.nextSibling = null; | |
421 dropChild(child); | |
422 } | |
423 void redepthChildren() { | |
424 ChildType child = _firstChild; | |
425 while (child != null) { | |
426 redepthChild(child); | |
427 assert(child.parentData is ParentDataType); | |
428 child = child.parentData.nextSibling; | |
429 } | |
430 } | |
431 void attachChildren() { | |
432 ChildType child = _firstChild; | |
433 while (child != null) { | |
434 child.attach(); | |
435 assert(child.parentData is ParentDataType); | |
436 child = child.parentData.nextSibling; | |
437 } | |
438 } | |
439 void detachChildren() { | |
440 ChildType child = _firstChild; | |
441 while (child != null) { | |
442 child.detach(); | |
443 assert(child.parentData is ParentDataType); | |
444 child = child.parentData.nextSibling; | |
445 } | |
446 } | |
447 | |
448 ChildType get firstChild => _firstChild; | |
449 ChildType get lastChild => _lastChild; | |
450 ChildType childAfter(ChildType child) { | |
451 assert(child.parentData is ParentDataType); | |
452 return child.parentData.nextSibling; | |
453 } | |
454 | |
455 String debugDescribeChildren(String prefix) { | |
456 String result = ''; | |
457 int count = 1; | |
458 ChildType child = _firstChild; | |
459 while (child != null) { | |
460 result += '${prefix}child ${count}: ${child.toString(prefix)}'; | |
461 count += 1; | |
462 child = child.parentData.nextSibling; | |
463 } | |
464 return result; | |
465 } | |
466 } | |
OLD | NEW |