Skip to main content

yog_ui/
widget.rs

1//! Widget types and styling.
2
3use crate::layout::{Align, FlexDir};
4
5/// A single widget in the UI tree.
6#[derive(Debug, Clone)]
7pub struct Widget {
8    pub kind: WidgetKind,
9    pub id: Option<String>,
10    pub style: Style,
11    pub flex_dir: FlexDir,
12    pub children: Vec<Widget>,
13    pub on_click: Option<String>,
14    pub enabled: bool,
15    pub focused: bool,
16}
17
18#[derive(Debug, Clone)]
19pub enum WidgetKind {
20    /// Container — arranges children according to `flex_dir`.
21    Panel(FlexDir),
22    /// Static or dynamic text label.
23    Label(String),
24    /// Clickable button with text.
25    Button(String),
26    /// Minecraft item icon (static, known at build time).
27    ItemSlot(String),
28    /// Inventory-slot decoration: an item icon plus a stack-count overlay,
29    /// both supplied as plain data at tree-build time (like every other
30    /// widget here) — this crate has no notion of "the currently open menu"
31    /// or how to query it; that's the caller's job (see `yog-inventory`).
32    /// Empty `item_id` draws an empty slot box.
33    InvSlot { item_id: String, count: u32 },
34    /// Minecraft texture blit (via `draw2d_mc_tex`).
35    McImage { id: String, img_w: f32, img_h: f32 },
36    /// Invisible spacer.
37    Spacer,
38}
39
40/// WinForms-style dock — which edge(s) the widget attaches to inside its parent.
41///
42/// - `None`   — normal flex positioning (default)
43/// - `Fill`   — stretch to fill all remaining space (both axes)
44/// - `Left`   — full height, natural width, hugs left edge; other children flow right
45/// - `Right`  — full height, natural width, hugs right edge
46/// - `Top`    — full width, natural height, hugs top edge; other children flow down
47/// - `Bottom` — full width, natural height, hugs bottom edge
48#[derive(Debug, Clone, Copy, PartialEq, Default)]
49pub enum Dock {
50    #[default]
51    None,
52    Fill,
53    Left,
54    Right,
55    Top,
56    Bottom,
57}
58
59/// How a focused widget shows its focus indicator.
60#[derive(Debug, Clone, Copy, PartialEq, Default)]
61pub enum FocusStyle {
62    /// 1px outline using `focus_color` (default, amber).
63    #[default]
64    Outline,
65    /// Solid background fill using `focus_color`.
66    Fill,
67    /// No visual indicator.
68    None,
69}
70
71/// A size value — either a fixed pixel amount, a CSS-style percentage of the
72/// parent's own resolved content size (0.0–100.0), or `Auto` (size to
73/// content/available space, this crate's original behaviour).
74#[derive(Debug, Clone, Copy, PartialEq, Default)]
75pub enum Units {
76    #[default]
77    Auto,
78    Px(f32),
79    /// Percent of the parent's resolved content size along the same axis.
80    Percent(f32),
81}
82
83impl From<f32> for Units {
84    /// `0.0` maps to `Auto` (matches this crate's original `0.0 = auto` convention,
85    /// so every existing `.w(18.0)`/`.h(0.0)` call keeps meaning exactly what it did).
86    fn from(v: f32) -> Self {
87        if v <= 0.0 {
88            Units::Auto
89        } else {
90            Units::Px(v)
91        }
92    }
93}
94
95impl Units {
96    /// Resolve against `avail` (the parent's resolved content size on this
97    /// axis). `None` for `Auto` — caller falls back to its own auto-sizing.
98    pub fn resolve(self, avail: f32) -> Option<f32> {
99        match self {
100            Units::Auto => None,
101            Units::Px(v) => Some(v),
102            Units::Percent(p) => Some(avail * p / 100.0),
103        }
104    }
105
106    pub fn is_auto(self) -> bool {
107        matches!(self, Units::Auto)
108    }
109}
110
111/// Visual and layout style for a widget.
112#[derive(Debug, Clone)]
113pub struct Style {
114    pub w: Units, // explicit width; Auto = size to content/available space
115    pub h: Units, // explicit height; Auto = size to content/available space
116    pub min_w: f32,
117    pub min_h: f32,
118    pub flex: f32,        // grow factor inside flex container (main axis)
119    pub dock: Dock,       // WinForms-style edge attachment
120    pub gap: f32,         // spacing between children
121    pub pad: [f32; 4],    // top, right, bottom, left — space INSIDE the border
122    pub margin: [f32; 4], // top, right, bottom, left — space OUTSIDE the border
123    pub bg: u32,          // background colour 0xAARRGGBB; 0 = transparent
124    pub color: u32,       // text colour
125    pub align: Align,
126    pub font_scale: f32,
127    pub text_shadow: bool, // MC drop-shadow behind text (HUD default; books disable it)
128    pub focus_style: FocusStyle,
129    pub focus_color: u32, // 0 = default amber 0xFF_FFE040
130    /// Labels/buttons: never word-wrap, even if narrower than the text.
131    /// For single-line titles/headers that must stay on one line — matches
132    /// Patchouli, where such text overflows its box horizontally rather than
133    /// wrapping onto a second line.
134    pub no_wrap: bool,
135    /// Absolute position `(x, y)` inside the parent's content box (inside its
136    /// padding) — WPF `Canvas` / WinForms `Location` style. A positioned
137    /// widget is taken out of the parent's flex flow: it takes no main-axis
138    /// space, ignores `flex`/`dock`, and is drawn in tree order (later
139    /// siblings on top). `None` (default) = normal flex positioning.
140    pub pos: Option<(f32, f32)>,
141}
142
143impl Default for Style {
144    fn default() -> Self {
145        Self {
146            w: Units::Auto,
147            h: Units::Auto,
148            min_w: 4.0,
149            min_h: 4.0,
150            flex: 0.0,
151            dock: Dock::None,
152            gap: 2.0,
153            pad: [0.0; 4],
154            margin: [0.0; 4],
155            bg: 0,
156            color: 0xFF_CCCCAA,
157            align: Align::Start,
158            font_scale: 1.0,
159            text_shadow: true,
160            focus_style: FocusStyle::default(),
161            focus_color: 0,
162            no_wrap: false,
163            pos: None,
164        }
165    }
166}
167
168// ── Builder API ───────────────────────────────────────────────────────────────
169
170impl Widget {
171    fn new(kind: WidgetKind) -> Self {
172        let flex_dir = if let WidgetKind::Panel(dir) = &kind {
173            *dir
174        } else {
175            FlexDir::Column
176        };
177        Self {
178            kind,
179            id: None,
180            style: Style::default(),
181            flex_dir,
182            children: Vec::new(),
183            on_click: None,
184            enabled: true,
185            focused: false,
186        }
187    }
188
189    pub fn id(mut self, id: impl Into<String>) -> Self {
190        self.id = Some(id.into());
191        self
192    }
193    pub fn on_click(mut self, ev: impl Into<String>) -> Self {
194        self.on_click = Some(ev.into());
195        self
196    }
197    pub fn child(mut self, w: Widget) -> Self {
198        self.children.push(w);
199        self
200    }
201
202    /// Accepts a plain px value (`0.0` = auto) or a [`Units`] directly —
203    /// e.g. `.w(18.0)` or `.w(Units::Percent(60.0))`.
204    pub fn w(mut self, w: impl Into<Units>) -> Self {
205        self.style.w = w.into();
206        self
207    }
208    pub fn h(mut self, h: impl Into<Units>) -> Self {
209        self.style.h = h.into();
210        self
211    }
212    pub fn min_w(mut self, v: f32) -> Self {
213        self.style.min_w = v;
214        self
215    }
216    pub fn min_h(mut self, v: f32) -> Self {
217        self.style.min_h = v;
218        self
219    }
220    pub fn flex(mut self, v: f32) -> Self {
221        self.style.flex = v;
222        self
223    }
224    pub fn flex_dir(mut self, v: FlexDir) -> Self {
225        self.flex_dir = v;
226        self
227    }
228    pub fn gap(mut self, v: f32) -> Self {
229        self.style.gap = v;
230        self
231    }
232    pub fn bg(mut self, v: u32) -> Self {
233        self.style.bg = v;
234        self
235    }
236    pub fn color(mut self, v: u32) -> Self {
237        self.style.color = v;
238        self
239    }
240    pub fn align(mut self, v: Align) -> Self {
241        self.style.align = v;
242        self
243    }
244    pub fn font_scale(mut self, v: f32) -> Self {
245        self.style.font_scale = v;
246        self
247    }
248    pub fn shadow(mut self, v: bool) -> Self {
249        self.style.text_shadow = v;
250        self
251    }
252    pub fn no_wrap(mut self) -> Self {
253        self.style.no_wrap = true;
254        self
255    }
256    pub fn focus_style(mut self, v: FocusStyle) -> Self {
257        self.style.focus_style = v;
258        self
259    }
260    pub fn focus_color(mut self, v: u32) -> Self {
261        self.style.focus_color = v;
262        self
263    }
264    pub fn dock(mut self, v: Dock) -> Self {
265        self.style.dock = v;
266        self
267    }
268    pub fn enabled(mut self, v: bool) -> Self {
269        self.enabled = v;
270        self
271    }
272    pub fn focused(mut self, v: bool) -> Self {
273        self.focused = v;
274        self
275    }
276    /// Position absolutely at `(x, y)` inside the parent's content box — see [`Style::pos`].
277    pub fn at(mut self, x: f32, y: f32) -> Self {
278        self.style.pos = Some((x, y));
279        self
280    }
281    /// Adds every widget in `children`, in order.
282    pub fn children(mut self, children: impl IntoIterator<Item = Widget>) -> Self {
283        self.children.extend(children);
284        self
285    }
286
287    /// The first widget in this subtree (depth-first, self included) whose
288    /// [`Widget::id`] is `id` — for changing a built tree before laying it
289    /// out (dynamic text, enabled state, ...).
290    pub fn find_mut(&mut self, id: &str) -> Option<&mut Widget> {
291        if self.id.as_deref() == Some(id) {
292            return Some(self);
293        }
294        self.children.iter_mut().find_map(|c| c.find_mut(id))
295    }
296
297    /// Replaces the text of a label or button; `false` for any other kind.
298    pub fn set_text(&mut self, text: impl Into<String>) -> bool {
299        match &mut self.kind {
300            WidgetKind::Label(t) | WidgetKind::Button(t) => {
301                *t = text.into();
302                true
303            }
304            _ => false,
305        }
306    }
307
308    pub fn padding(mut self, top: f32, right: f32, bottom: f32, left: f32) -> Self {
309        self.style.pad = [top, right, bottom, left];
310        self
311    }
312    pub fn margin(mut self, top: f32, right: f32, bottom: f32, left: f32) -> Self {
313        self.style.margin = [top, right, bottom, left];
314        self
315    }
316}
317
318// ── Widget constructors ───────────────────────────────────────────────────────
319
320pub fn panel(dir: FlexDir) -> Widget {
321    Widget::new(WidgetKind::Panel(dir))
322}
323pub fn label(text: impl Into<String>) -> Widget {
324    Widget::new(WidgetKind::Label(text.into()))
325}
326pub fn button(text: impl Into<String>) -> Widget {
327    Widget::new(WidgetKind::Button(text.into()))
328}
329pub fn item_slot(item_id: impl Into<String>) -> Widget {
330    Widget::new(WidgetKind::ItemSlot(item_id.into()))
331}
332/// An inventory-slot decoration (icon + stack-count overlay) for `item_id`
333/// (empty string = empty slot) with `count` items. Purely decorative — real
334/// drag-and-drop still goes through the vanilla `Slot` this overlays.
335pub fn inv_slot(item_id: impl Into<String>, count: u32) -> Widget {
336    Widget::new(WidgetKind::InvSlot {
337        item_id: item_id.into(),
338        count,
339    })
340    .w(18.0)
341    .h(18.0)
342}
343pub fn mc_image(id: impl Into<String>, img_w: f32, img_h: f32) -> Widget {
344    Widget::new(WidgetKind::McImage {
345        id: id.into(),
346        img_w,
347        img_h,
348    })
349    .w(img_w)
350    .h(img_h)
351}
352pub fn spacer() -> Widget {
353    Widget::new(WidgetKind::Spacer)
354}