Skip to main content

yog_ui/
layout.rs

1//! Flexbox-inspired layout engine.
2
3use crate::widget::Widget;
4
5/// 2D size in logical pixels.
6#[derive(Debug, Clone, Copy, Default)]
7pub struct Size { pub w: f32, pub h: f32 }
8
9/// Position + size rectangle.
10#[derive(Debug, Clone, Copy, Default)]
11pub struct Rect { pub x: f32, pub y: f32, pub w: f32, pub h: f32 }
12
13/// Layout direction for flex containers.
14#[derive(Debug, Clone, Copy, PartialEq)]
15pub enum FlexDir { Row, Column }
16
17/// Cross-axis alignment.
18#[derive(Debug, Clone, Copy, PartialEq)]
19pub enum Align { Start, Center, End }
20
21/// Computed layout node — positions are absolute screen coordinates.
22#[derive(Debug, Clone, Default)]
23pub struct LayoutNode {
24    pub rect: Rect,
25    pub id: Option<String>,
26    pub on_click: Option<String>,
27    pub children: Vec<LayoutNode>,
28}
29
30/// Recursively compute layout for the widget tree inside given screen bounds.
31pub fn compute(widget: &Widget, screen_w: f32, screen_h: f32) -> LayoutNode {
32    let ctx = LayoutCtx {
33        available_w: screen_w - widget.style.pad[1] - widget.style.pad[3]
34                      - widget.style.margin[1] - widget.style.margin[3],
35        available_h: screen_h - widget.style.pad[0] - widget.style.pad[2]
36                      - widget.style.margin[0] - widget.style.margin[2],
37    };
38    let mut node = LayoutNode {
39        id: widget.id.clone(),
40        on_click: widget.on_click.clone(),
41        ..Default::default()
42    };
43    layout_node(widget, &mut node, &ctx, 0.0, 0.0,
44                screen_w - widget.style.margin[1] - widget.style.margin[3],
45                screen_h - widget.style.margin[0] - widget.style.margin[2]);
46    node
47}
48
49struct LayoutCtx { available_w: f32, available_h: f32 }
50
51fn layout_node(w: &Widget, node: &mut LayoutNode, ctx: &LayoutCtx,
52               px: f32, py: f32, max_w: f32, max_h: f32) {
53    let s = &w.style;
54    // Apply explicit size or auto
55    let ww = if s.w > 0.0 { s.w } else { max_w };
56    let hh = if s.h > 0.0 { s.h } else { max_h };
57
58    node.rect = Rect { x: px + s.margin[3], y: py + s.margin[0], w: ww, h: hh };
59
60    if w.children.is_empty() {
61        // Auto-size leaf: use min-size or available
62        if s.w <= 0.0 { node.rect.w = s.min_w.max(ww.min(ctx.available_w)).max(1.0); }
63        if s.h <= 0.0 { node.rect.h = s.min_h.max(hh.min(ctx.available_h)).max(1.0); }
64        return;
65    }
66
67    let dir = if w.flex_dir == FlexDir::Row { FlexDir::Row } else { FlexDir::Column };
68    let content_w = node.rect.w - s.pad[1] - s.pad[3];
69    let content_h = node.rect.h - s.pad[0] - s.pad[2];
70
71    // First pass: measure non-flex children
72    let mut main_size: f32 = 0.0;
73    let mut total_flex: f32 = 0.0;
74    for child in &w.children {
75        let mut cn = LayoutNode::default();
76        layout_node(child, &mut cn, ctx,
77            node.rect.x + s.pad[3], node.rect.y + s.pad[0],
78            if dir == FlexDir::Row { f32::MAX / 4.0 } else { content_w },
79            if dir == FlexDir::Column { f32::MAX / 4.0 } else { content_h },
80        );
81        if dir == FlexDir::Row { main_size += cn.rect.w; }
82        else { main_size += cn.rect.h; }
83        total_flex += child.style.flex;
84        node.children.push(cn);
85    }
86    let gap = (s.gap * (w.children.len().saturating_sub(1) as f32)).max(0.0);
87    main_size += gap;
88
89    let available = (if dir == FlexDir::Row { content_w } else { content_h }) - main_size;
90    let mut pos = if dir == FlexDir::Row { s.pad[3] } else { s.pad[0] };
91
92    // Second pass: position children
93    for (i, child) in w.children.iter().enumerate() {
94        let cn = &mut node.children[i];
95        if dir == FlexDir::Row {
96            if child.style.flex > 0.0 && total_flex > 0.0 {
97                cn.rect.w += available * child.style.flex / total_flex;
98            }
99            cn.rect.x = node.rect.x + pos;
100            cn.rect.y = node.rect.y + s.pad[0] + align_offset(s.align, content_h, cn.rect.h);
101            pos += cn.rect.w + s.gap;
102        } else {
103            if child.style.flex > 0.0 && total_flex > 0.0 {
104                cn.rect.h += available * child.style.flex / total_flex;
105            }
106            cn.rect.x = node.rect.x + s.pad[3] + align_offset(s.align, content_w, cn.rect.w);
107            cn.rect.y = node.rect.y + pos;
108            pos += cn.rect.h + s.gap;
109        }
110    }
111
112    // Shrink to content
113    if s.w <= 0.0 {
114        let cw: f32 = node.children.iter().map(|c| c.rect.x - node.rect.x + c.rect.w).fold(0.0f32, f32::max);
115        node.rect.w = (cw + s.pad[1] + s.pad[3]).max(s.min_w);
116    }
117    if s.h <= 0.0 {
118        let ch: f32 = node.children.iter().map(|c| c.rect.y - node.rect.y + c.rect.h).fold(0.0f32, f32::max);
119        node.rect.h = (ch + s.pad[0] + s.pad[2]).max(s.min_h);
120    }
121}
122
123fn align_offset(align: Align, container: f32, child: f32) -> f32 {
124    let diff = container - child;
125    if diff <= 0.0 { return 0.0; }
126    match align {
127        Align::Start  => 0.0,
128        Align::Center => diff / 2.0,
129        Align::End    => diff,
130    }
131}
132
133/// Find the deepest clickable node at `(mx, my)`.
134pub fn hit_test(node: &LayoutNode, mx: f32, my: f32) -> Option<&LayoutNode> {
135    if mx < node.rect.x || my < node.rect.y
136        || mx > node.rect.x + node.rect.w || my > node.rect.y + node.rect.h { return None; }
137    for child in node.children.iter().rev() {
138        if let Some(hit) = hit_test(child, mx, my) { return Some(hit); }
139    }
140    if node.on_click.is_some() { Some(node) } else { None }
141}