1use crate::text;
2use crate::widget::{Dock, Widget, WidgetKind};
3
4#[derive(Debug, Clone, Copy, PartialEq)]
5pub enum FlexDir {
6 Row,
7 Column,
8}
9#[derive(Debug, Clone, Copy, PartialEq)]
10pub enum Align {
11 Start,
12 Center,
13 End,
14}
15
16#[derive(Debug, Clone, Copy, Default)]
17pub struct Rect {
18 pub x: f32,
19 pub y: f32,
20 pub w: f32,
21 pub h: f32,
22}
23
24#[derive(Debug, Clone)]
25pub struct LayoutNode {
26 pub rect: Rect,
27 pub id: Option<String>,
28 pub on_click: Option<String>,
29 pub children: Vec<LayoutNode>,
30 pub enabled: bool,
31 pub focused: bool,
32}
33
34impl Default for LayoutNode {
35 fn default() -> Self {
36 Self {
37 rect: Rect::default(),
38 id: None,
39 on_click: None,
40 children: Vec::new(),
41 enabled: true,
42 focused: false,
43 }
44 }
45}
46
47pub fn compute(widget: &Widget, avail_w: f32, avail_h: f32) -> LayoutNode {
50 let mut node = LayoutNode {
51 id: widget.id.clone(),
52 on_click: widget.on_click.clone(),
53 enabled: widget.enabled,
54 focused: widget.focused,
55 ..Default::default()
56 };
57 layout_widget(widget, &mut node, 0.0, 0.0, avail_w, avail_h);
58 node
59}
60
61fn layout_widget(w: &Widget, node: &mut LayoutNode, x: f32, y: f32, max_w: f32, max_h: f32) {
62 let s = &w.style;
63 let has_children = !w.children.is_empty();
64
65 let mut ww = s.w.resolve(max_w).map(|v| v.min(max_w)).unwrap_or(max_w);
67 let mut hh = s.h.resolve(max_h).map(|v| v.min(max_h)).unwrap_or(max_h);
68
69 if !has_children {
70 match &w.kind {
72 WidgetKind::Label(t) | WidgetKind::Button(t) => {
73 let avail_w = (max_w - s.pad[1] - s.pad[3]).max(0.0);
74 if avail_w < 4096.0 && !s.no_wrap {
77 ww = (avail_w + s.pad[1] + s.pad[3]).max(s.min_w).min(max_w);
78 hh = (text::text_height(t, avail_w, s.font_scale) + s.pad[0] + s.pad[2])
79 .max(s.min_h)
80 .min(max_h);
81 } else {
82 let tw = text::str_width(t, s.font_scale);
83 ww = (tw + s.pad[1] + s.pad[3]).max(s.min_w).min(max_w);
84 hh = (text::LINE_H * s.font_scale + s.pad[0] + s.pad[2])
85 .max(s.min_h)
86 .min(max_h);
87 }
88 }
89 WidgetKind::ItemSlot(_) | WidgetKind::InvSlot { .. } => {
90 ww = (18.0 + s.pad[1] + s.pad[3]).max(s.min_w).min(max_w);
91 hh = (18.0 + s.pad[0] + s.pad[2]).max(s.min_h).min(max_h);
92 }
93 WidgetKind::Spacer => {
94 ww = s.min_w.max(1.0).min(max_w);
95 hh = s.min_h.max(1.0).min(max_h);
96 }
97 WidgetKind::Panel(_) => {} WidgetKind::McImage { img_w, img_h, .. } => {
99 ww = (*img_w + s.pad[1] + s.pad[3]).max(s.min_w).min(max_w);
100 hh = (*img_h + s.pad[0] + s.pad[2]).max(s.min_h).min(max_h);
101 }
102 }
103 if let Some(v) = s.w.resolve(max_w) {
105 ww = v.min(max_w);
106 }
107 if let Some(v) = s.h.resolve(max_h) {
108 hh = v.min(max_h);
109 }
110 }
111
112 node.rect = Rect { x, y, w: ww, h: hh };
113
114 if !has_children {
115 return;
116 }
117
118 let dir = if matches!(w.kind, WidgetKind::Panel(_)) && w.flex_dir == FlexDir::Row {
120 FlexDir::Row
121 } else {
122 FlexDir::Column
123 };
124 let content_w = ww - s.pad[1] - s.pad[3];
125 let content_h = hh - s.pad[0] - s.pad[2];
126
127 let effective_flex = |child: &Widget| -> f32 {
130 if child.style.dock == Dock::Fill {
131 child.style.flex.max(1.0)
132 } else {
133 child.style.flex
134 }
135 };
136 let in_flow = |child: &Widget| -> bool {
138 child.style.pos.is_none()
139 && !matches!(
140 (dir, child.style.dock),
141 (FlexDir::Row, Dock::Right) | (FlexDir::Column, Dock::Bottom)
142 )
143 };
144
145 let mut child_nodes: Vec<LayoutNode> = Vec::new();
147 let mut total_flex: f32 = 0.0;
148 let mut used_main: f32 = 0.0;
149
150 for child in &w.children {
151 let dock = child.style.dock;
152 let mut cn = LayoutNode {
153 id: child.id.clone(),
154 on_click: child.on_click.clone(),
155 enabled: child.enabled,
156 focused: child.focused,
157 ..Default::default()
158 };
159 if let Some((px, py)) = child.style.pos {
162 let mw = (content_w - px).max(0.0);
163 let mh = (content_h - py).max(0.0);
164 layout_widget(child, &mut cn, 0.0, 0.0, mw, mh);
165 child_nodes.push(cn);
166 continue;
167 }
168 let (cmw, cmh) = match (dir, dock) {
170 (FlexDir::Row, Dock::Fill) => (content_w, content_h),
172 (FlexDir::Column, Dock::Fill) => (content_w, content_h),
173 (FlexDir::Row, Dock::Left | Dock::Right) => (f32::MAX, content_h),
175 (FlexDir::Column, Dock::Top | Dock::Bottom) => (content_w, f32::MAX),
176 (FlexDir::Row, _) => (f32::MAX, content_h),
178 (FlexDir::Column, _) => (content_w, f32::MAX),
179 };
180 layout_widget(child, &mut cn, 0.0, 0.0, cmw, cmh);
181 if in_flow(child) {
182 if dir == FlexDir::Row {
183 used_main += cn.rect.w;
184 } else {
185 used_main += cn.rect.h;
186 }
187 }
188 total_flex += effective_flex(child);
189 child_nodes.push(cn);
190 }
191 let flow_count = w.children.iter().filter(|c| in_flow(c)).count();
192 let gaps = s.gap * (flow_count.saturating_sub(1) as f32);
193 used_main += gaps;
194
195 let available = (if dir == FlexDir::Row {
196 content_w
197 } else {
198 content_h
199 }) - used_main;
200 let mut pos = if dir == FlexDir::Row {
201 s.pad[3]
202 } else {
203 s.pad[0]
204 };
205
206 for (i, child) in w.children.iter().enumerate() {
208 if !in_flow(child) {
209 continue;
210 }
211 let dock = child.style.dock;
212 let cn = &mut child_nodes[i];
213 if dir == FlexDir::Row {
214 let ef = effective_flex(child);
215 if ef > 0.0 && total_flex > 0.0 && available > 0.0 {
216 cn.rect.w += available * ef / total_flex;
217 }
218 if dock == Dock::Fill || dock == Dock::Left || dock == Dock::Right {
219 cn.rect.h = content_h; }
221 cn.rect.x = x + pos;
222 cn.rect.y = y
223 + s.pad[0]
224 + match s.align {
225 Align::Center => (content_h - cn.rect.h) / 2.0,
226 Align::End => content_h - cn.rect.h,
227 _ => 0.0,
228 };
229 pos += cn.rect.w + s.gap;
230 } else {
231 let ef = effective_flex(child);
232 if ef > 0.0 && total_flex > 0.0 && available > 0.0 {
233 cn.rect.h += available * ef / total_flex;
234 }
235 if dock == Dock::Fill || dock == Dock::Top || dock == Dock::Bottom {
236 cn.rect.w = content_w; }
238 cn.rect.x = x
239 + s.pad[3]
240 + match s.align {
241 Align::Center => (content_w - cn.rect.w) / 2.0,
242 Align::End => content_w - cn.rect.w,
243 _ => 0.0,
244 };
245 cn.rect.y = y + pos;
246 pos += cn.rect.h + s.gap;
247 }
248 if !child.children.is_empty() {
249 layout_widget(child, cn, cn.rect.x, cn.rect.y, cn.rect.w, cn.rect.h);
250 }
251 }
252
253 let mut rpos = if dir == FlexDir::Row {
255 x + s.pad[3] + content_w
256 } else {
257 y + s.pad[0] + content_h
258 };
259 for (i, child) in w.children.iter().enumerate() {
260 if in_flow(child) || child.style.pos.is_some() {
261 continue;
262 }
263 let dock = child.style.dock;
264 let cn = &mut child_nodes[i];
265 if dir == FlexDir::Row {
266 if dock == Dock::Fill || dock == Dock::Left || dock == Dock::Right {
267 cn.rect.h = content_h;
268 }
269 rpos -= cn.rect.w;
270 cn.rect.x = rpos;
271 cn.rect.y = y
272 + s.pad[0]
273 + match s.align {
274 Align::Center => (content_h - cn.rect.h) / 2.0,
275 Align::End => content_h - cn.rect.h,
276 _ => 0.0,
277 };
278 rpos -= s.gap;
279 } else {
280 if dock == Dock::Fill || dock == Dock::Top || dock == Dock::Bottom {
281 cn.rect.w = content_w;
282 }
283 rpos -= cn.rect.h;
284 cn.rect.y = rpos;
285 cn.rect.x = x
286 + s.pad[3]
287 + match s.align {
288 Align::Center => (content_w - cn.rect.w) / 2.0,
289 Align::End => content_w - cn.rect.w,
290 _ => 0.0,
291 };
292 rpos -= s.gap;
293 }
294 if !child.children.is_empty() {
295 layout_widget(child, cn, cn.rect.x, cn.rect.y, cn.rect.w, cn.rect.h);
296 }
297 }
298
299 for (i, child) in w.children.iter().enumerate() {
301 let Some((px, py)) = child.style.pos else {
302 continue;
303 };
304 let cn = &mut child_nodes[i];
305 cn.rect.x = x + s.pad[3] + px;
306 cn.rect.y = y + s.pad[0] + py;
307 if !child.children.is_empty() {
308 layout_widget(child, cn, cn.rect.x, cn.rect.y, cn.rect.w, cn.rect.h);
309 }
310 }
311
312 if s.w.is_auto() {
314 let cw: f32 = child_nodes
315 .iter()
316 .map(|c| c.rect.x - x + c.rect.w)
317 .fold(0.0f32, f32::max);
318 node.rect.w = (cw + s.pad[1] + s.pad[3]).max(s.min_w).min(max_w);
319 }
320 if s.h.is_auto() {
321 let ch: f32 = child_nodes
322 .iter()
323 .map(|c| c.rect.y - y + c.rect.h)
324 .fold(0.0f32, f32::max);
325 node.rect.h = (ch + s.pad[0] + s.pad[2]).max(s.min_h).min(max_h);
326 }
327
328 node.children = child_nodes;
329}
330
331pub fn hit_test(node: &LayoutNode, mx: f32, my: f32) -> Option<&LayoutNode> {
333 let r = &node.rect;
334 if mx < r.x || my < r.y || mx > r.x + r.w || my > r.y + r.h {
335 return None;
336 }
337 for child in node.children.iter().rev() {
338 if let Some(hit) = hit_test(child, mx, my) {
339 return Some(hit);
340 }
341 }
342 if node.on_click.is_some() && node.enabled {
343 Some(node)
344 } else {
345 None
346 }
347}
348
349pub fn find_by_id(node: &LayoutNode, id: &str) -> Option<Rect> {
354 if node.id.as_deref() == Some(id) {
355 return Some(node.rect);
356 }
357 node.children.iter().find_map(|c| find_by_id(c, id))
358}
359
360pub fn set_focus(node: &mut LayoutNode, focused_id: Option<&str>) {
362 node.focused = focused_id.is_some() && node.id.as_deref() == focused_id;
363 for child in &mut node.children {
364 set_focus(child, focused_id);
365 }
366}
367
368#[cfg(test)]
369mod tests {
370 use super::*;
371 use crate::widget::{self, Units};
372
373 #[test]
374 fn positioned_children_leave_the_flow() {
375 let root = widget::panel(FlexDir::Column)
376 .w(200.0)
377 .h(100.0)
378 .padding(4.0, 4.0, 4.0, 4.0)
379 .child(widget::spacer().min_w(10.0).min_h(10.0))
380 .child(widget::button("Hi").w(40.0).h(20.0).at(50.0, 30.0))
381 .child(widget::spacer().min_w(10.0).min_h(10.0));
382 let node = compute(&root, 400.0, 400.0);
383 let r = node.children[1].rect;
384 assert_eq!((r.x, r.y, r.w, r.h), (54.0, 34.0, 40.0, 20.0));
385 assert_eq!(node.children[0].rect.y, 4.0);
387 assert_eq!(node.children[2].rect.y, 4.0 + 10.0 + 2.0);
388 }
389
390 #[test]
391 fn positioned_container_lays_out_its_children_in_place() {
392 let root = widget::panel(FlexDir::Column)
393 .w(Units::Percent(100.0))
394 .h(Units::Percent(100.0))
395 .child(
396 widget::panel(FlexDir::Row)
397 .w(100.0)
398 .h(20.0)
399 .gap(0.0)
400 .at(10.0, 20.0)
401 .child(widget::spacer().min_w(30.0).min_h(5.0))
402 .child(widget::spacer().min_w(30.0).min_h(5.0)),
403 );
404 let node = compute(&root, 320.0, 240.0);
405 let panel = &node.children[0];
406 assert_eq!((panel.rect.x, panel.rect.y), (10.0, 20.0));
407 assert_eq!(panel.children[1].rect.x, 40.0);
408 assert_eq!(panel.children[1].rect.y, 20.0);
409 }
410
411 #[test]
412 fn auto_sized_parent_grows_to_fit_positioned_children() {
413 let root = widget::panel(FlexDir::Column)
414 .child(widget::spacer().min_w(10.0).min_h(10.0).at(30.0, 40.0));
415 let node = compute(&root, 300.0, 300.0);
416 assert_eq!((node.rect.w, node.rect.h), (40.0, 50.0));
417 }
418
419 #[test]
420 fn find_mut_and_set_text() {
421 let mut root = widget::panel(FlexDir::Column).child(widget::label("a").id("title"));
422 assert!(root.find_mut("title").unwrap().set_text("b"));
423 match &root.children[0].kind {
424 crate::widget::WidgetKind::Label(t) => assert_eq!(t, "b"),
425 _ => unreachable!(),
426 }
427 assert!(root.find_mut("missing").is_none());
428 }
429}