OLD | NEW |
| (Empty) |
1 // Copyright (c) 2011, the Dart project authors. Please see the AUTHORS file | |
2 // for details. All rights reserved. Use of this source code is governed by a | |
3 // BSD-style license that can be found in the LICENSE file. | |
4 | |
5 /** The interface that the layout algorithms use to talk to the view. */ | |
6 interface Positionable { | |
7 ViewLayout get layout(); | |
8 | |
9 /** Gets our custom CSS properties, as provided by the CSS preprocessor. */ | |
10 Map<String, String> get customStyle(); | |
11 | |
12 /** Gets the root DOM used for layout. */ | |
13 Element get node(); | |
14 | |
15 /** Gets the collection of child views. */ | |
16 Collection<Positionable> get childViews(); | |
17 | |
18 /** Causes a view to layout its children. */ | |
19 void doLayout(); | |
20 } | |
21 | |
22 | |
23 /** | |
24 * Caches the layout parameters that were specified in CSS during a layout | |
25 * computation. These values are immutable during a layout. | |
26 */ | |
27 class LayoutParams { | |
28 // TODO(jmesserly): should be const, but there's a bug in DartC preventing us | |
29 // from calling "window." in an initializer. See b/5332777 | |
30 Future<CSSStyleDeclaration> style; | |
31 | |
32 int get layer() => 0; | |
33 | |
34 LayoutParams(Element node) { | |
35 style = node.computedStyle; | |
36 } | |
37 } | |
38 | |
39 // TODO(jmesserly): enums would really help here | |
40 class Dimension { | |
41 // TODO(jmesserly): perhaps this should be X and Y | |
42 static final WIDTH = const Dimension._internal('width'); | |
43 static final HEIGHT = const Dimension._internal('height'); | |
44 | |
45 final String name; // for debugging | |
46 const Dimension._internal(this.name); | |
47 } | |
48 | |
49 class ContentSizeMode { | |
50 /** Minimum content size, e.g. min-width and min-height in CSS. */ | |
51 static final MIN = const ContentSizeMode._internal('min'); | |
52 | |
53 /** Maximum content size, e.g. min-width and min-height in CSS. */ | |
54 static final MAX = const ContentSizeMode._internal('max'); | |
55 | |
56 // TODO(jmesserly): we probably want some sort of "auto" or "best fit" mode | |
57 // Don't need it yet though. | |
58 | |
59 final String name; // for debugging | |
60 const ContentSizeMode._internal(this.name); | |
61 } | |
62 | |
63 /** | |
64 * Abstract base class for View layout. Tracks relevant layout state. | |
65 * This code was inspired by code in Android's View.java; it's needed for the | |
66 * rest of the layout system. | |
67 */ | |
68 class ViewLayout { | |
69 /** | |
70 * The layout parameters associated with this view and used by the parent | |
71 * to determine how this view should be laid out. | |
72 */ | |
73 LayoutParams layoutParams; | |
74 Future<ElementRect> _cachedViewRect; | |
75 | |
76 /** The view that this layout belongs to. */ | |
77 final Positionable view; | |
78 | |
79 /** | |
80 * To get a perforant positioning model on top of the DOM, we read all | |
81 * properties in the first pass while computing positions. Then we have a | |
82 * second pass that actually moves everything. | |
83 */ | |
84 int _measuredLeft, _measuredTop, _measuredWidth, _measuredHeight; | |
85 | |
86 ViewLayout(this.view); | |
87 | |
88 /** | |
89 * Creates the appropriate view layout, depending on the properties. | |
90 */ | |
91 // TODO(jmesserly): we should support user defined layouts somehow. Perhaps | |
92 // registered with a LayoutProvider. | |
93 factory ViewLayout.fromView(Positionable view) { | |
94 if (hasCustomLayout(view)) { | |
95 return new GridLayout(view); | |
96 } else { | |
97 return new ViewLayout(view); | |
98 } | |
99 } | |
100 | |
101 static bool hasCustomLayout(Positionable view) { | |
102 return view.customStyle['display'] == "-dart-grid"; | |
103 } | |
104 | |
105 CSSStyleDeclaration get _style() => layoutParams.style.value; | |
106 | |
107 void cacheExistingBrowserLayout() { | |
108 _cachedViewRect = view.node.rect; | |
109 } | |
110 | |
111 int get currentWidth() { | |
112 return _cachedViewRect.value.offset.width; | |
113 } | |
114 | |
115 int get currentHeight() { | |
116 return _cachedViewRect.value.offset.height; | |
117 } | |
118 | |
119 int get borderLeftWidth() => _toPixels(_style.borderLeftWidth); | |
120 int get borderTopWidth() => _toPixels(_style.borderTopWidth); | |
121 int get borderRightWidth() => _toPixels(_style.borderRightWidth); | |
122 int get borderBottomWidth() => _toPixels(_style.borderBottomWidth); | |
123 int get borderWidth() => borderLeftWidth + borderRightWidth; | |
124 int get borderHeight() => borderTopWidth + borderBottomWidth; | |
125 | |
126 /** Implements the custom layout computation. */ | |
127 void measureLayout(Future<Size> size, Completer<bool> changed) { | |
128 } | |
129 | |
130 /** | |
131 * Positions the view within its parent container. | |
132 * Also performs a layout of its children. | |
133 */ | |
134 void setBounds(int left, int top, int width, int height) { | |
135 assert(width >= 0 && height >= 0); | |
136 | |
137 _measuredLeft = left; | |
138 _measuredTop = top; | |
139 | |
140 // Note: we need to save the client height | |
141 _measuredWidth = width - borderWidth; | |
142 _measuredHeight = height - borderHeight; | |
143 final completer = new Completer<Size>(); | |
144 completer.complete(new Size(_measuredWidth, _measuredHeight)); | |
145 measureLayout(completer.future, null); | |
146 } | |
147 | |
148 /** Applies the layout to the node. */ | |
149 void applyLayout() { | |
150 if (_measuredLeft != null) { | |
151 // TODO(jmesserly): benchmark the performance of this DOM interaction | |
152 final style = view.node.style; | |
153 style.position = 'absolute'; | |
154 style.left = '${_measuredLeft}px'; | |
155 style.top = '${_measuredTop}px'; | |
156 style.width = '${_measuredWidth}px'; | |
157 style.height = '${_measuredHeight}px'; | |
158 style.zIndex = '${layoutParams.layer}'; | |
159 | |
160 _measuredLeft = null; | |
161 _measuredTop = null; | |
162 _measuredWidth = null; | |
163 _measuredHeight = null; | |
164 | |
165 // Ensure we can handle our custom layout when it is a child of a | |
166 // DOM-positioned node. For example, say we have a View tree like this: | |
167 // | |
168 // ViewWithLayout <-- uses our layout engine | |
169 // childView1 <-- is positioned by our layout engine, but uses | |
170 // HTML layout internally | |
171 // childOfChild <-- uses our layout engine for its own children | |
172 if (!hasCustomLayout(view)) { | |
173 for (final child in view.childViews) { | |
174 child.doLayout(); | |
175 } | |
176 } | |
177 } | |
178 } | |
179 | |
180 int measureContent(ViewLayout parent, Dimension dimension, | |
181 [ContentSizeMode mode = null]) { | |
182 switch (dimension) { | |
183 case Dimension.WIDTH: | |
184 return measureWidth(parent, mode); | |
185 case Dimension.HEIGHT: | |
186 return measureHeight(parent, mode); | |
187 } | |
188 } | |
189 | |
190 int measureWidth(ViewLayout parent, ContentSizeMode mode) { | |
191 final style = layoutParams.style.value; | |
192 switch (mode) { | |
193 case ContentSizeMode.MIN: | |
194 return _styleToPixels( | |
195 style.minWidth, currentWidth, parent.currentWidth); | |
196 | |
197 case ContentSizeMode.MAX: | |
198 return _styleToPixels( | |
199 style.maxWidth, currentWidth, parent.currentWidth); | |
200 } | |
201 } | |
202 | |
203 int measureHeight(ViewLayout parent, ContentSizeMode mode) { | |
204 final style = layoutParams.style.value; | |
205 switch (mode) { | |
206 case ContentSizeMode.MIN: | |
207 return _styleToPixels( | |
208 style.minHeight, currentHeight, parent.currentHeight); | |
209 | |
210 case ContentSizeMode.MAX: | |
211 return _styleToPixels( | |
212 style.maxHeight, currentHeight, parent.currentHeight); | |
213 } | |
214 } | |
215 | |
216 static int _toPixels(String style) { | |
217 if (style.endsWith('px')) { | |
218 return Math.parseInt(style.substring(0, style.length - 2)); | |
219 } else { | |
220 // TODO(jmesserly): other size units | |
221 throw new UnsupportedOperationException( | |
222 'Unknown min/max content size format: "$style"'); | |
223 } | |
224 } | |
225 | |
226 static int _styleToPixels(String style, num size, num parentSize) { | |
227 if (style == 'none') { | |
228 // For an unset max-content size, use the actual size | |
229 return size; | |
230 } | |
231 if (style.endsWith('%')) { | |
232 num percent = Math.parseDouble(style.substring(0, style.length - 1)); | |
233 return ((percent / 100) * parentSize).toInt(); | |
234 } | |
235 return _toPixels(style); | |
236 } | |
237 } | |
OLD | NEW |