1use crate::text;
2use crate::widget::{Dock, Widget, WidgetKind};
3
4#[derive(Debug, Clone, Copy, PartialEq)]
5pub enum FlexDir { Row, Column }
6#[derive(Debug, Clone, Copy, PartialEq)]
7pub enum Align { Start, Center, End }
8
9#[derive(Debug, Clone, Copy, Default)]
10pub struct Rect { pub x: f32, pub y: f32, pub w: f32, pub h: f32 }
11
12#[derive(Debug, Clone)]
13pub struct LayoutNode {
14 pub rect: Rect,
15 pub id: Option<String>,
16 pub on_click: Option<String>,
17 pub children: Vec<LayoutNode>,
18 pub enabled: bool,
19 pub focused: bool,
20}
21
22impl Default for LayoutNode {
23 fn default() -> Self {
24 Self { rect: Rect::default(), id: None, on_click: None,
25 children: Vec::new(), enabled: true, focused: false }
26 }
27}
28
29pub fn compute(widget: &Widget, avail_w: f32, avail_h: f32) -> LayoutNode {
32 let mut node = LayoutNode {
33 id: widget.id.clone(), on_click: widget.on_click.clone(),
34 enabled: widget.enabled, focused: widget.focused,
35 ..Default::default()
36 };
37 layout_widget(widget, &mut node, 0.0, 0.0, avail_w, avail_h);
38 node
39}
40
41fn layout_widget(w: &Widget, node: &mut LayoutNode, x: f32, y: f32, max_w: f32, max_h: f32) {
42 let s = &w.style;
43 let has_children = !w.children.is_empty();
44
45 let mut ww = if s.w > 0.0 { s.w.min(max_w) } else { max_w };
47 let mut hh = if s.h > 0.0 { s.h.min(max_h) } else { max_h };
48
49 if !has_children {
50 match &w.kind {
52 WidgetKind::Label(t) | WidgetKind::Button(t) => {
53 let avail_w = (max_w - s.pad[1] - s.pad[3]).max(0.0);
54 if avail_w < 4096.0 {
56 ww = (avail_w + s.pad[1] + s.pad[3]).max(s.min_w).min(max_w);
57 hh = (text::text_height(t, avail_w, s.font_scale) + s.pad[0] + s.pad[2])
58 .max(s.min_h).min(max_h);
59 } else {
60 let tw = text::str_width(t, s.font_scale);
61 ww = (tw + s.pad[1] + s.pad[3]).max(s.min_w).min(max_w);
62 hh = (text::LINE_H * s.font_scale + s.pad[0] + s.pad[2]).max(s.min_h).min(max_h);
63 }
64 }
65 WidgetKind::ItemSlot(_) => {
66 ww = (18.0 + s.pad[1] + s.pad[3]).max(s.min_w).min(max_w);
67 hh = (18.0 + s.pad[0] + s.pad[2]).max(s.min_h).min(max_h);
68 }
69 WidgetKind::Spacer => {
70 ww = s.min_w.max(1.0).min(max_w);
71 hh = s.min_h.max(1.0).min(max_h);
72 }
73 WidgetKind::Panel(_) => {} WidgetKind::McImage { img_w, img_h, .. } => {
75 ww = (*img_w + s.pad[1] + s.pad[3]).max(s.min_w).min(max_w);
76 hh = (*img_h + s.pad[0] + s.pad[2]).max(s.min_h).min(max_h);
77 }
78 }
79 if s.w > 0.0 { ww = s.w.min(max_w); }
81 if s.h > 0.0 { hh = s.h.min(max_h); }
82 }
83
84 node.rect = Rect { x, y, w: ww, h: hh };
85
86 if !has_children { return; }
87
88 let dir = if matches!(w.kind, WidgetKind::Panel(_)) && w.flex_dir == FlexDir::Row { FlexDir::Row } else { FlexDir::Column };
90 let content_w = ww - s.pad[1] - s.pad[3];
91 let content_h = hh - s.pad[0] - s.pad[2];
92
93 let effective_flex = |child: &Widget| -> f32 {
96 if child.style.dock == Dock::Fill { child.style.flex.max(1.0) } else { child.style.flex }
97 };
98 let in_flow = |child: &Widget| -> bool {
100 match (dir, child.style.dock) {
101 (FlexDir::Row, Dock::Right) => false,
102 (FlexDir::Column, Dock::Bottom) => false,
103 _ => true,
104 }
105 };
106
107 let mut child_nodes: Vec<LayoutNode> = Vec::new();
109 let mut total_flex: f32 = 0.0;
110 let mut used_main: f32 = 0.0;
111
112 for child in &w.children {
113 let dock = child.style.dock;
114 let mut cn = LayoutNode {
115 id: child.id.clone(), on_click: child.on_click.clone(),
116 enabled: child.enabled, focused: child.focused,
117 ..Default::default()
118 };
119 let (cmw, cmh) = match (dir, dock) {
121 (FlexDir::Row, Dock::Fill) => (content_w, content_h),
123 (FlexDir::Column, Dock::Fill) => (content_w, content_h),
124 (FlexDir::Row, Dock::Left | Dock::Right) => (f32::MAX, content_h),
126 (FlexDir::Column, Dock::Top | Dock::Bottom) => (content_w, f32::MAX),
127 (FlexDir::Row, _) => (f32::MAX, content_h),
129 (FlexDir::Column, _) => (content_w, f32::MAX),
130 };
131 layout_widget(child, &mut cn, 0.0, 0.0, cmw, cmh);
132 if in_flow(child) {
133 if dir == FlexDir::Row { used_main += cn.rect.w; }
134 else { used_main += cn.rect.h; }
135 }
136 total_flex += effective_flex(child);
137 child_nodes.push(cn);
138 }
139 let flow_count = w.children.iter().filter(|c| in_flow(c)).count();
140 let gaps = s.gap * (flow_count.saturating_sub(1) as f32);
141 used_main += gaps;
142
143 let available = (if dir == FlexDir::Row { content_w } else { content_h }) - used_main;
144 let mut pos = if dir == FlexDir::Row { s.pad[3] } else { s.pad[0] };
145
146 for (i, child) in w.children.iter().enumerate() {
148 if !in_flow(child) { continue; }
149 let dock = child.style.dock;
150 let cn = &mut child_nodes[i];
151 if dir == FlexDir::Row {
152 let ef = effective_flex(child);
153 if ef > 0.0 && total_flex > 0.0 && available > 0.0 {
154 cn.rect.w += available * ef / total_flex;
155 }
156 if dock == Dock::Fill || dock == Dock::Left || dock == Dock::Right {
157 cn.rect.h = content_h; }
159 cn.rect.x = x + pos;
160 cn.rect.y = y + s.pad[0] + match s.align {
161 Align::Center => (content_h - cn.rect.h) / 2.0,
162 Align::End => content_h - cn.rect.h,
163 _ => 0.0,
164 };
165 pos += cn.rect.w + s.gap;
166 } else {
167 let ef = effective_flex(child);
168 if ef > 0.0 && total_flex > 0.0 && available > 0.0 {
169 cn.rect.h += available * ef / total_flex;
170 }
171 if dock == Dock::Fill || dock == Dock::Top || dock == Dock::Bottom {
172 cn.rect.w = content_w; }
174 cn.rect.x = x + s.pad[3] + match s.align {
175 Align::Center => (content_w - cn.rect.w) / 2.0,
176 Align::End => content_w - cn.rect.w,
177 _ => 0.0,
178 };
179 cn.rect.y = y + pos;
180 pos += cn.rect.h + s.gap;
181 }
182 if !child.children.is_empty() {
183 layout_widget(child, cn, cn.rect.x, cn.rect.y, cn.rect.w, cn.rect.h);
184 }
185 }
186
187 let mut rpos = if dir == FlexDir::Row {
189 x + s.pad[3] + content_w
190 } else {
191 y + s.pad[0] + content_h
192 };
193 for (i, child) in w.children.iter().enumerate() {
194 if in_flow(child) { continue; }
195 let dock = child.style.dock;
196 let cn = &mut child_nodes[i];
197 if dir == FlexDir::Row {
198 if dock == Dock::Fill || dock == Dock::Left || dock == Dock::Right {
199 cn.rect.h = content_h;
200 }
201 rpos -= cn.rect.w;
202 cn.rect.x = rpos;
203 cn.rect.y = y + s.pad[0] + match s.align {
204 Align::Center => (content_h - cn.rect.h) / 2.0,
205 Align::End => content_h - cn.rect.h,
206 _ => 0.0,
207 };
208 rpos -= s.gap;
209 } else {
210 if dock == Dock::Fill || dock == Dock::Top || dock == Dock::Bottom {
211 cn.rect.w = content_w;
212 }
213 rpos -= cn.rect.h;
214 cn.rect.y = rpos;
215 cn.rect.x = x + s.pad[3] + match s.align {
216 Align::Center => (content_w - cn.rect.w) / 2.0,
217 Align::End => content_w - cn.rect.w,
218 _ => 0.0,
219 };
220 rpos -= s.gap;
221 }
222 if !child.children.is_empty() {
223 layout_widget(child, cn, cn.rect.x, cn.rect.y, cn.rect.w, cn.rect.h);
224 }
225 }
226
227 if s.w <= 0.0 {
229 let cw: f32 = child_nodes.iter().map(|c| c.rect.x - x + c.rect.w).fold(0.0f32, f32::max);
230 node.rect.w = (cw + s.pad[1] + s.pad[3]).max(s.min_w).min(max_w);
231 }
232 if s.h <= 0.0 {
233 let ch: f32 = child_nodes.iter().map(|c| c.rect.y - y + c.rect.h).fold(0.0f32, f32::max);
234 node.rect.h = (ch + s.pad[0] + s.pad[2]).max(s.min_h).min(max_h);
235 }
236
237 node.children = child_nodes;
238}
239
240pub fn hit_test(node: &LayoutNode, mx: f32, my: f32) -> Option<&LayoutNode> {
242 let r = &node.rect;
243 if mx < r.x || my < r.y || mx > r.x + r.w || my > r.y + r.h { return None; }
244 for child in node.children.iter().rev() {
245 if let Some(hit) = hit_test(child, mx, my) { return Some(hit); }
246 }
247 if node.on_click.is_some() && node.enabled { Some(node) } else { None }
248}
249
250pub fn set_focus(node: &mut LayoutNode, focused_id: Option<&str>) {
252 node.focused = focused_id.is_some() && node.id.as_deref() == focused_id;
253 for child in &mut node.children { set_focus(child, focused_id); }
254}