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
24pub const SCROLLBAR_W: f32 = 6.0;
27pub const MIN_KNOB_H: f32 = 16.0;
29
30impl Rect {
31 pub fn contains(&self, px: f32, py: f32) -> bool {
33 px >= self.x && py >= self.y && px <= self.x + self.w && py <= self.y + self.h
34 }
35
36 pub fn intersect(&self, o: &Rect) -> Option<Rect> {
38 let x0 = self.x.max(o.x);
39 let y0 = self.y.max(o.y);
40 let x1 = (self.x + self.w).min(o.x + o.w);
41 let y1 = (self.y + self.h).min(o.y + o.h);
42 (x1 > x0 && y1 > y0).then_some(Rect {
43 x: x0,
44 y: y0,
45 w: x1 - x0,
46 h: y1 - y0,
47 })
48 }
49}
50
51#[derive(Debug, Clone)]
52pub struct LayoutNode {
53 pub rect: Rect,
54 pub id: Option<String>,
55 pub on_click: Option<String>,
56 pub children: Vec<LayoutNode>,
57 pub enabled: bool,
58 pub focused: bool,
59 pub content_h: f32,
62 pub scroll_y: f32,
66 pub clip: bool,
68 pub scrollbar: bool,
70}
71
72impl LayoutNode {
73 pub fn max_scroll(&self) -> f32 {
75 (self.content_h - self.rect.h).max(0.0)
76 }
77
78 pub fn scrollbar_rects(&self) -> Option<(Rect, Rect)> {
81 let max = self.max_scroll();
82 if !self.scrollbar || max <= 0.0 {
83 return None;
84 }
85 let r = self.rect;
86 let track = Rect {
87 x: r.x + r.w - SCROLLBAR_W,
88 y: r.y,
89 w: SCROLLBAR_W,
90 h: r.h,
91 };
92 let knob_h = (r.h * r.h / self.content_h).max(MIN_KNOB_H).min(r.h);
93 let knob = Rect {
94 y: r.y + (r.h - knob_h) * (self.scroll_y / max),
95 h: knob_h,
96 ..track
97 };
98 Some((track, knob))
99 }
100
101 fn translate_y(&mut self, dy: f32) {
102 self.rect.y += dy;
103 for c in &mut self.children {
104 c.translate_y(dy);
105 }
106 }
107}
108
109impl Default for LayoutNode {
110 fn default() -> Self {
111 Self {
112 rect: Rect::default(),
113 id: None,
114 on_click: None,
115 children: Vec::new(),
116 enabled: true,
117 focused: false,
118 content_h: 0.0,
119 scroll_y: 0.0,
120 clip: false,
121 scrollbar: false,
122 }
123 }
124}
125
126pub fn compute(widget: &Widget, avail_w: f32, avail_h: f32) -> LayoutNode {
129 let mut node = LayoutNode {
130 id: widget.id.clone(),
131 on_click: widget.on_click.clone(),
132 enabled: widget.enabled,
133 focused: widget.focused,
134 ..Default::default()
135 };
136 layout_widget(widget, &mut node, 0.0, 0.0, avail_w, avail_h);
137 node
138}
139
140fn layout_widget(w: &Widget, node: &mut LayoutNode, x: f32, y: f32, max_w: f32, max_h: f32) {
141 let s = &w.style;
142 let has_children = !w.children.is_empty();
143
144 let mut ww = s.w.resolve(max_w).map(|v| v.min(max_w)).unwrap_or(max_w);
146 let mut hh = s.h.resolve(max_h).map(|v| v.min(max_h)).unwrap_or(max_h);
147
148 if !has_children {
149 match &w.kind {
151 WidgetKind::Label(t) | WidgetKind::Button(t) => {
152 let avail_w = (max_w - s.pad[1] - s.pad[3]).max(0.0);
153 if avail_w < 4096.0 && !s.no_wrap {
156 ww = (avail_w + s.pad[1] + s.pad[3]).max(s.min_w).min(max_w);
157 hh = (text::text_height(t, avail_w, s.font_scale) + s.pad[0] + s.pad[2])
158 .max(s.min_h)
159 .min(max_h);
160 } else {
161 let tw = text::str_width(t, s.font_scale);
162 ww = (tw + s.pad[1] + s.pad[3]).max(s.min_w).min(max_w);
163 hh = (text::LINE_H * s.font_scale + s.pad[0] + s.pad[2])
164 .max(s.min_h)
165 .min(max_h);
166 }
167 }
168 WidgetKind::ItemSlot(_) | WidgetKind::InvSlot { .. } => {
169 ww = (18.0 + s.pad[1] + s.pad[3]).max(s.min_w).min(max_w);
170 hh = (18.0 + s.pad[0] + s.pad[2]).max(s.min_h).min(max_h);
171 }
172 WidgetKind::Spacer => {
173 ww = s.min_w.max(1.0).min(max_w);
174 hh = s.min_h.max(1.0).min(max_h);
175 }
176 WidgetKind::Panel(_) => {} WidgetKind::McImage { img_w, img_h, .. } => {
178 ww = (*img_w + s.pad[1] + s.pad[3]).max(s.min_w).min(max_w);
179 hh = (*img_h + s.pad[0] + s.pad[2]).max(s.min_h).min(max_h);
180 }
181 }
182 if let Some(v) = s.w.resolve(max_w) {
184 ww = v.min(max_w);
185 }
186 if let Some(v) = s.h.resolve(max_h) {
187 hh = v.min(max_h);
188 }
189 }
190
191 node.rect = Rect { x, y, w: ww, h: hh };
192 node.content_h = hh;
193 node.scroll_y = 0.0;
194 node.clip = s.clip;
195 node.scrollbar = s.scrollbar;
196
197 if !has_children {
198 return;
199 }
200
201 let dir = if matches!(w.kind, WidgetKind::Panel(_)) && w.flex_dir == FlexDir::Row {
203 FlexDir::Row
204 } else {
205 FlexDir::Column
206 };
207 let bar_w = if s.scrollbar { SCROLLBAR_W } else { 0.0 };
208 let content_w = (ww - s.pad[1] - s.pad[3] - bar_w).max(0.0);
209 let content_h = hh - s.pad[0] - s.pad[2];
210
211 let effective_flex = |child: &Widget| -> f32 {
214 if child.style.dock == Dock::Fill {
215 child.style.flex.max(1.0)
216 } else {
217 child.style.flex
218 }
219 };
220 let in_flow = |child: &Widget| -> bool {
222 child.style.pos.is_none()
223 && !matches!(
224 (dir, child.style.dock),
225 (FlexDir::Row, Dock::Right) | (FlexDir::Column, Dock::Bottom)
226 )
227 };
228
229 let mut child_nodes: Vec<LayoutNode> = Vec::new();
231 let mut total_flex: f32 = 0.0;
232 let mut used_main: f32 = 0.0;
233
234 for child in &w.children {
235 let dock = child.style.dock;
236 let mut cn = LayoutNode {
237 id: child.id.clone(),
238 on_click: child.on_click.clone(),
239 enabled: child.enabled,
240 focused: child.focused,
241 ..Default::default()
242 };
243 if let Some((px, py)) = child.style.pos {
246 let mw = (content_w - px).max(0.0);
247 let mh = (content_h - py).max(0.0);
248 layout_widget(child, &mut cn, 0.0, 0.0, mw, mh);
249 child_nodes.push(cn);
250 continue;
251 }
252 let (cmw, cmh) = match (dir, dock) {
254 (FlexDir::Row, Dock::Fill) => (content_w, content_h),
256 (FlexDir::Column, Dock::Fill) => (content_w, content_h),
257 (FlexDir::Row, Dock::Left | Dock::Right) => (f32::MAX, content_h),
259 (FlexDir::Column, Dock::Top | Dock::Bottom) => (content_w, f32::MAX),
260 (FlexDir::Row, _) => (f32::MAX, content_h),
262 (FlexDir::Column, _) => (content_w, f32::MAX),
263 };
264 layout_widget(child, &mut cn, 0.0, 0.0, cmw, cmh);
265 if in_flow(child) {
266 if dir == FlexDir::Row {
267 used_main += cn.rect.w;
268 } else {
269 used_main += cn.rect.h;
270 }
271 }
272 total_flex += effective_flex(child);
273 child_nodes.push(cn);
274 }
275 let flow_count = w.children.iter().filter(|c| in_flow(c)).count();
276 let gaps = s.gap * (flow_count.saturating_sub(1) as f32);
277 used_main += gaps;
278
279 let available = (if dir == FlexDir::Row {
280 content_w
281 } else {
282 content_h
283 }) - used_main;
284 let mut pos = if dir == FlexDir::Row {
285 s.pad[3]
286 } else {
287 s.pad[0]
288 };
289
290 for (i, child) in w.children.iter().enumerate() {
292 if !in_flow(child) {
293 continue;
294 }
295 let dock = child.style.dock;
296 let cn = &mut child_nodes[i];
297 if dir == FlexDir::Row {
298 let ef = effective_flex(child);
299 if ef > 0.0 && total_flex > 0.0 && available > 0.0 {
300 cn.rect.w += available * ef / total_flex;
301 }
302 if dock == Dock::Fill || dock == Dock::Left || dock == Dock::Right {
303 cn.rect.h = content_h; }
305 cn.rect.x = x + pos;
306 cn.rect.y = y
307 + s.pad[0]
308 + match s.align {
309 Align::Center => (content_h - cn.rect.h) / 2.0,
310 Align::End => content_h - cn.rect.h,
311 _ => 0.0,
312 };
313 pos += cn.rect.w + s.gap;
314 } else {
315 let ef = effective_flex(child);
316 if ef > 0.0 && total_flex > 0.0 && available > 0.0 {
317 cn.rect.h += available * ef / total_flex;
318 }
319 if dock == Dock::Fill || dock == Dock::Top || dock == Dock::Bottom {
320 cn.rect.w = content_w; }
322 cn.rect.x = x
323 + s.pad[3]
324 + match s.align {
325 Align::Center => (content_w - cn.rect.w) / 2.0,
326 Align::End => content_w - cn.rect.w,
327 _ => 0.0,
328 };
329 cn.rect.y = y + pos;
330 pos += cn.rect.h + s.gap;
331 }
332 if !child.children.is_empty() {
333 layout_widget(child, cn, cn.rect.x, cn.rect.y, cn.rect.w, cn.rect.h);
334 }
335 }
336
337 let mut rpos = if dir == FlexDir::Row {
339 x + s.pad[3] + content_w
340 } else {
341 y + s.pad[0] + content_h
342 };
343 for (i, child) in w.children.iter().enumerate() {
344 if in_flow(child) || child.style.pos.is_some() {
345 continue;
346 }
347 let dock = child.style.dock;
348 let cn = &mut child_nodes[i];
349 if dir == FlexDir::Row {
350 if dock == Dock::Fill || dock == Dock::Left || dock == Dock::Right {
351 cn.rect.h = content_h;
352 }
353 rpos -= cn.rect.w;
354 cn.rect.x = rpos;
355 cn.rect.y = y
356 + s.pad[0]
357 + match s.align {
358 Align::Center => (content_h - cn.rect.h) / 2.0,
359 Align::End => content_h - cn.rect.h,
360 _ => 0.0,
361 };
362 rpos -= s.gap;
363 } else {
364 if dock == Dock::Fill || dock == Dock::Top || dock == Dock::Bottom {
365 cn.rect.w = content_w;
366 }
367 rpos -= cn.rect.h;
368 cn.rect.y = rpos;
369 cn.rect.x = x
370 + s.pad[3]
371 + match s.align {
372 Align::Center => (content_w - cn.rect.w) / 2.0,
373 Align::End => content_w - cn.rect.w,
374 _ => 0.0,
375 };
376 rpos -= s.gap;
377 }
378 if !child.children.is_empty() {
379 layout_widget(child, cn, cn.rect.x, cn.rect.y, cn.rect.w, cn.rect.h);
380 }
381 }
382
383 for (i, child) in w.children.iter().enumerate() {
385 let Some((px, py)) = child.style.pos else {
386 continue;
387 };
388 let cn = &mut child_nodes[i];
389 cn.rect.x = x + s.pad[3] + px;
390 cn.rect.y = y + s.pad[0] + py;
391 if !child.children.is_empty() {
392 layout_widget(child, cn, cn.rect.x, cn.rect.y, cn.rect.w, cn.rect.h);
393 }
394 }
395
396 if s.w.is_auto() {
398 let cw: f32 = child_nodes
399 .iter()
400 .map(|c| c.rect.x - x + c.rect.w)
401 .fold(0.0f32, f32::max);
402 node.rect.w = (cw + s.pad[1] + s.pad[3] + bar_w).max(s.min_w).min(max_w);
403 }
404 let ch: f32 = child_nodes
405 .iter()
406 .map(|c| c.rect.y - y + c.rect.h)
407 .fold(0.0f32, f32::max);
408 if s.h.is_auto() {
409 node.rect.h = (ch + s.pad[0] + s.pad[2]).max(s.min_h).min(max_h);
410 }
411
412 node.content_h = ch + s.pad[2];
415 node.scroll_y = s.scroll_y.clamp(0.0, node.max_scroll());
416 if node.scroll_y > 0.0 {
417 for c in &mut child_nodes {
418 c.translate_y(-node.scroll_y);
419 }
420 }
421
422 node.children = child_nodes;
423}
424
425pub fn hit_test(node: &LayoutNode, mx: f32, my: f32) -> Option<&LayoutNode> {
429 if !node.rect.contains(mx, my) {
430 return None;
431 }
432 for child in node.children.iter().rev() {
433 if let Some(hit) = hit_test(child, mx, my) {
434 return Some(hit);
435 }
436 }
437 if node.on_click.is_some() && node.enabled {
438 Some(node)
439 } else {
440 None
441 }
442}
443
444pub fn find_by_id(node: &LayoutNode, id: &str) -> Option<Rect> {
449 if node.id.as_deref() == Some(id) {
450 return Some(node.rect);
451 }
452 node.children.iter().find_map(|c| find_by_id(c, id))
453}
454
455pub fn find_node<'a>(node: &'a LayoutNode, id: &str) -> Option<&'a LayoutNode> {
458 if node.id.as_deref() == Some(id) {
459 return Some(node);
460 }
461 node.children.iter().find_map(|c| find_node(c, id))
462}
463
464pub fn set_focus(node: &mut LayoutNode, focused_id: Option<&str>) {
466 node.focused = focused_id.is_some() && node.id.as_deref() == focused_id;
467 for child in &mut node.children {
468 set_focus(child, focused_id);
469 }
470}
471
472#[cfg(test)]
473mod tests {
474 use super::*;
475 use crate::widget::{self, Units};
476
477 #[test]
478 fn positioned_children_leave_the_flow() {
479 let root = widget::panel(FlexDir::Column)
480 .w(200.0)
481 .h(100.0)
482 .padding(4.0, 4.0, 4.0, 4.0)
483 .child(widget::spacer().min_w(10.0).min_h(10.0))
484 .child(widget::button("Hi").w(40.0).h(20.0).at(50.0, 30.0))
485 .child(widget::spacer().min_w(10.0).min_h(10.0));
486 let node = compute(&root, 400.0, 400.0);
487 let r = node.children[1].rect;
488 assert_eq!((r.x, r.y, r.w, r.h), (54.0, 34.0, 40.0, 20.0));
489 assert_eq!(node.children[0].rect.y, 4.0);
491 assert_eq!(node.children[2].rect.y, 4.0 + 10.0 + 2.0);
492 }
493
494 #[test]
495 fn positioned_container_lays_out_its_children_in_place() {
496 let root = widget::panel(FlexDir::Column)
497 .w(Units::Percent(100.0))
498 .h(Units::Percent(100.0))
499 .child(
500 widget::panel(FlexDir::Row)
501 .w(100.0)
502 .h(20.0)
503 .gap(0.0)
504 .at(10.0, 20.0)
505 .child(widget::spacer().min_w(30.0).min_h(5.0))
506 .child(widget::spacer().min_w(30.0).min_h(5.0)),
507 );
508 let node = compute(&root, 320.0, 240.0);
509 let panel = &node.children[0];
510 assert_eq!((panel.rect.x, panel.rect.y), (10.0, 20.0));
511 assert_eq!(panel.children[1].rect.x, 40.0);
512 assert_eq!(panel.children[1].rect.y, 20.0);
513 }
514
515 #[test]
516 fn auto_sized_parent_grows_to_fit_positioned_children() {
517 let root = widget::panel(FlexDir::Column)
518 .child(widget::spacer().min_w(10.0).min_h(10.0).at(30.0, 40.0));
519 let node = compute(&root, 300.0, 300.0);
520 assert_eq!((node.rect.w, node.rect.h), (40.0, 50.0));
521 }
522
523 #[test]
524 fn find_mut_and_set_text() {
525 let mut root = widget::panel(FlexDir::Column).child(widget::label("a").id("title"));
526 assert!(root.find_mut("title").unwrap().set_text("b"));
527 match &root.children[0].kind {
528 crate::widget::WidgetKind::Label(t) => assert_eq!(t, "b"),
529 _ => unreachable!(),
530 }
531 assert!(root.find_mut("missing").is_none());
532 }
533}