1use crate::widget::{Widget, WidgetKind};
2
3#[derive(Debug, Clone, Copy, PartialEq)]
4pub enum FlexDir { Row, Column }
5#[derive(Debug, Clone, Copy, PartialEq)]
6pub enum Align { Start, Center, End }
7
8#[derive(Debug, Clone, Copy, Default)]
9pub struct Rect { pub x: f32, pub y: f32, pub w: f32, pub h: f32 }
10
11#[derive(Debug, Clone)]
12pub struct LayoutNode {
13 pub rect: Rect,
14 pub id: Option<String>,
15 pub on_click: Option<String>,
16 pub children: Vec<LayoutNode>,
17 pub enabled: bool,
18 pub focused: bool,
19}
20
21impl Default for LayoutNode {
22 fn default() -> Self {
23 Self { rect: Rect::default(), id: None, on_click: None,
24 children: Vec::new(), enabled: true, focused: false }
25 }
26}
27
28pub fn compute(widget: &Widget, avail_w: f32, avail_h: f32) -> LayoutNode {
31 let mut node = LayoutNode {
32 id: widget.id.clone(), on_click: widget.on_click.clone(),
33 enabled: widget.enabled, focused: widget.focused,
34 ..Default::default()
35 };
36 layout_widget(widget, &mut node, 0.0, 0.0, avail_w, avail_h);
37 node
38}
39
40fn layout_widget(w: &Widget, node: &mut LayoutNode, x: f32, y: f32, max_w: f32, max_h: f32) {
41 let s = &w.style;
42 let has_children = !w.children.is_empty();
43
44 let mut ww = if s.w > 0.0 { s.w.min(max_w) } else { max_w };
46 let mut hh = if s.h > 0.0 { s.h.min(max_h) } else { max_h };
47
48 if !has_children {
49 match &w.kind {
51 WidgetKind::Label(text) | WidgetKind::Button(text) => {
52 let tw = (text.len() as f32 * 7.0 * s.font_scale).min(max_w - s.pad[1] - s.pad[3]);
54 ww = (tw + s.pad[1] + s.pad[3]).max(s.min_w).min(max_w);
55 hh = (10.0 * s.font_scale + s.pad[0] + s.pad[2]).max(s.min_h).min(max_h);
56 }
57 WidgetKind::ItemSlot(_) => {
58 ww = (18.0 + s.pad[1] + s.pad[3]).max(s.min_w).min(max_w);
59 hh = (18.0 + s.pad[0] + s.pad[2]).max(s.min_h).min(max_h);
60 }
61 WidgetKind::Spacer => {
62 ww = s.min_w.max(1.0).min(max_w);
63 hh = s.min_h.max(1.0).min(max_h);
64 }
65 WidgetKind::Panel(_) => {} WidgetKind::McImage { img_w, img_h, .. } => {
67 ww = (*img_w + s.pad[1] + s.pad[3]).max(s.min_w).min(max_w);
68 hh = (*img_h + s.pad[0] + s.pad[2]).max(s.min_h).min(max_h);
69 }
70 }
71 }
72
73 node.rect = Rect { x, y, w: ww, h: hh };
74
75 if !has_children { return; }
76
77 let dir = if matches!(w.kind, WidgetKind::Panel(_)) && w.flex_dir == FlexDir::Row { FlexDir::Row } else { FlexDir::Column };
79 let content_w = ww - s.pad[1] - s.pad[3];
80 let content_h = hh - s.pad[0] - s.pad[2];
81
82 let mut child_nodes: Vec<LayoutNode> = Vec::new();
84 let mut total_flex: f32 = 0.0;
85 let mut used_main: f32 = 0.0;
86
87 for child in &w.children {
88 let mut cn = LayoutNode {
89 id: child.id.clone(), on_click: child.on_click.clone(),
90 enabled: child.enabled, focused: child.focused,
91 ..Default::default()
92 };
93 let cmw = if dir == FlexDir::Row { f32::MAX } else { content_w };
94 let cmh = if dir == FlexDir::Column { f32::MAX } else { content_h };
95 layout_widget(child, &mut cn, 0.0, 0.0, cmw, cmh);
96 if dir == FlexDir::Row { used_main += cn.rect.w; }
97 else { used_main += cn.rect.h; }
98 total_flex += child.style.flex;
99 child_nodes.push(cn);
100 }
101 let gaps = s.gap * (w.children.len().saturating_sub(1) as f32);
102 used_main += gaps;
103
104 let available = (if dir == FlexDir::Row { content_w } else { content_h }) - used_main;
105 let mut pos = if dir == FlexDir::Row { s.pad[3] } else { s.pad[0] };
106
107 for (i, child) in w.children.iter().enumerate() {
109 let cn = &mut child_nodes[i];
110 if dir == FlexDir::Row {
111 if child.style.flex > 0.0 && total_flex > 0.0 && available > 0.0 {
112 cn.rect.w += available * child.style.flex / total_flex;
113 }
114 cn.rect.x = x + pos;
115 cn.rect.y = y + s.pad[0] + match s.align {
116 Align::Center => (content_h - cn.rect.h) / 2.0,
117 Align::End => content_h - cn.rect.h,
118 _ => 0.0,
119 };
120 pos += cn.rect.w + s.gap;
121 } else {
122 if child.style.flex > 0.0 && total_flex > 0.0 && available > 0.0 {
123 cn.rect.h += available * child.style.flex / total_flex;
124 }
125 cn.rect.x = x + s.pad[3] + match s.align {
126 Align::Center => (content_w - cn.rect.w) / 2.0,
127 Align::End => content_w - cn.rect.w,
128 _ => 0.0,
129 };
130 cn.rect.y = y + pos;
131 pos += cn.rect.h + s.gap;
132 }
133 if !child.children.is_empty() {
135 layout_widget(child, cn, cn.rect.x, cn.rect.y, cn.rect.w, cn.rect.h);
136 }
137 }
138
139 if s.w <= 0.0 {
141 let cw: f32 = child_nodes.iter().map(|c| c.rect.x - x + c.rect.w).fold(0.0f32, f32::max);
142 node.rect.w = (cw + s.pad[1] + s.pad[3]).max(s.min_w).min(max_w);
143 }
144 if s.h <= 0.0 {
145 let ch: f32 = child_nodes.iter().map(|c| c.rect.y - y + c.rect.h).fold(0.0f32, f32::max);
146 node.rect.h = (ch + s.pad[0] + s.pad[2]).max(s.min_h).min(max_h);
147 }
148
149 node.children = child_nodes;
150}
151
152pub fn hit_test(node: &LayoutNode, mx: f32, my: f32) -> Option<&LayoutNode> {
154 let r = &node.rect;
155 if mx < r.x || my < r.y || mx > r.x + r.w || my > r.y + r.h { return None; }
156 for child in node.children.iter().rev() {
157 if let Some(hit) = hit_test(child, mx, my) { return Some(hit); }
158 }
159 if node.on_click.is_some() && node.enabled { Some(node) } else { None }
160}
161
162pub fn set_focus(node: &mut LayoutNode, focused_id: Option<&str>) {
164 node.focused = focused_id.is_some() && node.id.as_deref() == focused_id;
165 for child in &mut node.children { set_focus(child, focused_id); }
166}