Skip to main content

yog_ui/
layout.rs

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, Default)]
12pub struct LayoutNode {
13    pub rect: Rect,
14    pub id: Option<String>,
15    pub on_click: Option<String>,
16    pub children: Vec<LayoutNode>,
17}
18
19/// Compute layout starting at (0,0) with given available size.
20/// Returns the root LayoutNode with absolute coordinates.
21pub fn compute(widget: &Widget, avail_w: f32, avail_h: f32) -> LayoutNode {
22    let mut node = LayoutNode { id: widget.id.clone(), on_click: widget.on_click.clone(), ..Default::default() };
23    layout_widget(widget, &mut node, 0.0, 0.0, avail_w, avail_h);
24    node
25}
26
27fn layout_widget(w: &Widget, node: &mut LayoutNode, x: f32, y: f32, max_w: f32, max_h: f32) {
28    let s = &w.style;
29    let has_children = !w.children.is_empty();
30
31    // Determine own size
32    let mut ww = if s.w > 0.0 { s.w.min(max_w) } else { max_w };
33    let mut hh = if s.h > 0.0 { s.h.min(max_h) } else { max_h };
34
35    if !has_children {
36        // Leaf: size to content (text, item slot, spacer)
37        match &w.kind {
38            WidgetKind::Label(text) | WidgetKind::Button(text) => {
39                // Approximate text size ~7px per char, 10px height
40                let tw = (text.len() as f32 * 7.0 * s.font_scale).min(max_w - s.pad[1] - s.pad[3]);
41                ww = (tw + s.pad[1] + s.pad[3]).max(s.min_w).min(max_w);
42                hh = (10.0 * s.font_scale + s.pad[0] + s.pad[2]).max(s.min_h).min(max_h);
43            }
44            WidgetKind::ItemSlot(_) => {
45                ww = (18.0 + s.pad[1] + s.pad[3]).max(s.min_w).min(max_w);
46                hh = (18.0 + s.pad[0] + s.pad[2]).max(s.min_h).min(max_h);
47            }
48            WidgetKind::Spacer => {
49                ww = s.min_w.max(1.0).min(max_w);
50                hh = s.min_h.max(1.0).min(max_h);
51            }
52            WidgetKind::Panel(_) => {} // panel with no children → size to min or available
53            WidgetKind::McImage { img_w, img_h, .. } => {
54                ww = (*img_w + s.pad[1] + s.pad[3]).max(s.min_w).min(max_w);
55                hh = (*img_h + s.pad[0] + s.pad[2]).max(s.min_h).min(max_h);
56            }
57        }
58    }
59
60    node.rect = Rect { x, y, w: ww, h: hh };
61
62    if !has_children { return; }
63
64    // Flex layout for children
65    let dir = if matches!(w.kind, WidgetKind::Panel(_)) && w.flex_dir == FlexDir::Row { FlexDir::Row } else { FlexDir::Column };
66    let content_w = ww - s.pad[1] - s.pad[3];
67    let content_h = hh - s.pad[0] - s.pad[2];
68
69    // Measure children
70    let mut child_nodes: Vec<LayoutNode> = Vec::new();
71    let mut total_flex: f32 = 0.0;
72    let mut used_main: f32 = 0.0;
73
74    for child in &w.children {
75        let mut cn = LayoutNode::default();
76        let cmw = if dir == FlexDir::Row { f32::MAX } else { content_w };
77        let cmh = if dir == FlexDir::Column { f32::MAX } else { content_h };
78        layout_widget(child, &mut cn, 0.0, 0.0, cmw, cmh);
79        if dir == FlexDir::Row { used_main += cn.rect.w; }
80        else { used_main += cn.rect.h; }
81        total_flex += child.style.flex;
82        child_nodes.push(cn);
83    }
84    let gaps = s.gap * (w.children.len().saturating_sub(1) as f32);
85    used_main += gaps;
86
87    let available = (if dir == FlexDir::Row { content_w } else { content_h }) - used_main;
88    let mut pos = if dir == FlexDir::Row { s.pad[3] } else { s.pad[0] };
89
90    // Position children
91    for (i, child) in w.children.iter().enumerate() {
92        let cn = &mut child_nodes[i];
93        if dir == FlexDir::Row {
94            if child.style.flex > 0.0 && total_flex > 0.0 && available > 0.0 {
95                cn.rect.w += available * child.style.flex / total_flex;
96            }
97            cn.rect.x = x + pos;
98            cn.rect.y = y + s.pad[0] + match s.align {
99                Align::Center => (content_h - cn.rect.h) / 2.0,
100                Align::End => content_h - cn.rect.h,
101                _ => 0.0,
102            };
103            pos += cn.rect.w + s.gap;
104        } else {
105            if child.style.flex > 0.0 && total_flex > 0.0 && available > 0.0 {
106                cn.rect.h += available * child.style.flex / total_flex;
107            }
108            cn.rect.x = x + s.pad[3] + match s.align {
109                Align::Center => (content_w - cn.rect.w) / 2.0,
110                Align::End => content_w - cn.rect.w,
111                _ => 0.0,
112            };
113            cn.rect.y = y + pos;
114            pos += cn.rect.h + s.gap;
115        }
116        // Recursively layout children of children
117        if !child.children.is_empty() {
118            layout_widget(child, cn, cn.rect.x, cn.rect.y, cn.rect.w, cn.rect.h);
119        }
120    }
121
122    // Auto-size: shrink to content
123    if s.w <= 0.0 {
124        let cw: f32 = child_nodes.iter().map(|c| c.rect.x - x + c.rect.w).fold(0.0f32, f32::max);
125        node.rect.w = (cw + s.pad[1] + s.pad[3]).max(s.min_w).min(max_w);
126    }
127    if s.h <= 0.0 {
128        let ch: f32 = child_nodes.iter().map(|c| c.rect.y - y + c.rect.h).fold(0.0f32, f32::max);
129        node.rect.h = (ch + s.pad[0] + s.pad[2]).max(s.min_h).min(max_h);
130    }
131
132    node.children = child_nodes;
133}
134
135/// Hit-test: find deepest clickable node at (mx, my).
136pub fn hit_test(node: &LayoutNode, mx: f32, my: f32) -> Option<&LayoutNode> {
137    let r = &node.rect;
138    if mx < r.x || my < r.y || mx > r.x + r.w || my > r.y + r.h { return None; }
139    for child in node.children.iter().rev() {
140        if let Some(hit) = hit_test(child, mx, my) { return Some(hit); }
141    }
142    if node.on_click.is_some() { Some(node) } else { None }
143}