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)]
61pub enum FocusStyle {
62    /// 1px outline using `focus_color` (default, amber).
63    Outline,
64    /// Solid background fill using `focus_color`.
65    Fill,
66    /// No visual indicator.
67    None,
68}
69
70impl Default for FocusStyle {
71    fn default() -> Self {
72        FocusStyle::Outline
73    }
74}
75
76/// A size value — either a fixed pixel amount, a CSS-style percentage of the
77/// parent's own resolved content size (0.0–100.0), or `Auto` (size to
78/// content/available space, this crate's original behaviour).
79#[derive(Debug, Clone, Copy, PartialEq)]
80pub enum Units {
81    Auto,
82    Px(f32),
83    /// Percent of the parent's resolved content size along the same axis.
84    Percent(f32),
85}
86
87impl Default for Units {
88    fn default() -> Self {
89        Units::Auto
90    }
91}
92
93impl From<f32> for Units {
94    /// `0.0` maps to `Auto` (matches this crate's original `0.0 = auto` convention,
95    /// so every existing `.w(18.0)`/`.h(0.0)` call keeps meaning exactly what it did).
96    fn from(v: f32) -> Self {
97        if v <= 0.0 {
98            Units::Auto
99        } else {
100            Units::Px(v)
101        }
102    }
103}
104
105impl Units {
106    /// Resolve against `avail` (the parent's resolved content size on this
107    /// axis). `None` for `Auto` — caller falls back to its own auto-sizing.
108    pub fn resolve(self, avail: f32) -> Option<f32> {
109        match self {
110            Units::Auto => None,
111            Units::Px(v) => Some(v),
112            Units::Percent(p) => Some(avail * p / 100.0),
113        }
114    }
115
116    pub fn is_auto(self) -> bool {
117        matches!(self, Units::Auto)
118    }
119}
120
121/// Visual and layout style for a widget.
122#[derive(Debug, Clone)]
123pub struct Style {
124    pub w: Units, // explicit width; Auto = size to content/available space
125    pub h: Units, // explicit height; Auto = size to content/available space
126    pub min_w: f32,
127    pub min_h: f32,
128    pub flex: f32,        // grow factor inside flex container (main axis)
129    pub dock: Dock,       // WinForms-style edge attachment
130    pub gap: f32,         // spacing between children
131    pub pad: [f32; 4],    // top, right, bottom, left — space INSIDE the border
132    pub margin: [f32; 4], // top, right, bottom, left — space OUTSIDE the border
133    pub bg: u32,          // background colour 0xAARRGGBB; 0 = transparent
134    pub color: u32,       // text colour
135    pub align: Align,
136    pub font_scale: f32,
137    pub text_shadow: bool, // MC drop-shadow behind text (HUD default; books disable it)
138    pub focus_style: FocusStyle,
139    pub focus_color: u32, // 0 = default amber 0xFF_FFE040
140    /// Labels/buttons: never word-wrap, even if narrower than the text.
141    /// For single-line titles/headers that must stay on one line — matches
142    /// Patchouli, where such text overflows its box horizontally rather than
143    /// wrapping onto a second line.
144    pub no_wrap: bool,
145}
146
147impl Default for Style {
148    fn default() -> Self {
149        Self {
150            w: Units::Auto,
151            h: Units::Auto,
152            min_w: 4.0,
153            min_h: 4.0,
154            flex: 0.0,
155            dock: Dock::None,
156            gap: 2.0,
157            pad: [0.0; 4],
158            margin: [0.0; 4],
159            bg: 0,
160            color: 0xFF_CCCCAA,
161            align: Align::Start,
162            font_scale: 1.0,
163            text_shadow: true,
164            focus_style: FocusStyle::default(),
165            focus_color: 0,
166            no_wrap: false,
167        }
168    }
169}
170
171// ── Builder API ───────────────────────────────────────────────────────────────
172
173impl Widget {
174    fn new(kind: WidgetKind) -> Self {
175        let flex_dir = if let WidgetKind::Panel(dir) = &kind {
176            *dir
177        } else {
178            FlexDir::Column
179        };
180        Self {
181            kind,
182            id: None,
183            style: Style::default(),
184            flex_dir,
185            children: Vec::new(),
186            on_click: None,
187            enabled: true,
188            focused: false,
189        }
190    }
191
192    pub fn id(mut self, id: impl Into<String>) -> Self {
193        self.id = Some(id.into());
194        self
195    }
196    pub fn on_click(mut self, ev: impl Into<String>) -> Self {
197        self.on_click = Some(ev.into());
198        self
199    }
200    pub fn child(mut self, w: Widget) -> Self {
201        self.children.push(w);
202        self
203    }
204
205    /// Accepts a plain px value (`0.0` = auto) or a [`Units`] directly —
206    /// e.g. `.w(18.0)` or `.w(Units::Percent(60.0))`.
207    pub fn w(mut self, w: impl Into<Units>) -> Self {
208        self.style.w = w.into();
209        self
210    }
211    pub fn h(mut self, h: impl Into<Units>) -> Self {
212        self.style.h = h.into();
213        self
214    }
215    pub fn min_w(mut self, v: f32) -> Self {
216        self.style.min_w = v;
217        self
218    }
219    pub fn min_h(mut self, v: f32) -> Self {
220        self.style.min_h = v;
221        self
222    }
223    pub fn flex(mut self, v: f32) -> Self {
224        self.style.flex = v;
225        self
226    }
227    pub fn flex_dir(mut self, v: FlexDir) -> Self {
228        self.flex_dir = v;
229        self
230    }
231    pub fn gap(mut self, v: f32) -> Self {
232        self.style.gap = v;
233        self
234    }
235    pub fn bg(mut self, v: u32) -> Self {
236        self.style.bg = v;
237        self
238    }
239    pub fn color(mut self, v: u32) -> Self {
240        self.style.color = v;
241        self
242    }
243    pub fn align(mut self, v: Align) -> Self {
244        self.style.align = v;
245        self
246    }
247    pub fn font_scale(mut self, v: f32) -> Self {
248        self.style.font_scale = v;
249        self
250    }
251    pub fn shadow(mut self, v: bool) -> Self {
252        self.style.text_shadow = v;
253        self
254    }
255    pub fn no_wrap(mut self) -> Self {
256        self.style.no_wrap = true;
257        self
258    }
259    pub fn focus_style(mut self, v: FocusStyle) -> Self {
260        self.style.focus_style = v;
261        self
262    }
263    pub fn focus_color(mut self, v: u32) -> Self {
264        self.style.focus_color = v;
265        self
266    }
267    pub fn dock(mut self, v: Dock) -> Self {
268        self.style.dock = v;
269        self
270    }
271    pub fn enabled(mut self, v: bool) -> Self {
272        self.enabled = v;
273        self
274    }
275    pub fn focused(mut self, v: bool) -> Self {
276        self.focused = v;
277        self
278    }
279    pub fn padding(mut self, top: f32, right: f32, bottom: f32, left: f32) -> Self {
280        self.style.pad = [top, right, bottom, left];
281        self
282    }
283    pub fn margin(mut self, top: f32, right: f32, bottom: f32, left: f32) -> Self {
284        self.style.margin = [top, right, bottom, left];
285        self
286    }
287}
288
289// ── Widget constructors ───────────────────────────────────────────────────────
290
291pub fn panel(dir: FlexDir) -> Widget {
292    Widget::new(WidgetKind::Panel(dir))
293}
294pub fn label(text: impl Into<String>) -> Widget {
295    Widget::new(WidgetKind::Label(text.into()))
296}
297pub fn button(text: impl Into<String>) -> Widget {
298    Widget::new(WidgetKind::Button(text.into()))
299}
300pub fn item_slot(item_id: impl Into<String>) -> Widget {
301    Widget::new(WidgetKind::ItemSlot(item_id.into()))
302}
303/// An inventory-slot decoration (icon + stack-count overlay) for `item_id`
304/// (empty string = empty slot) with `count` items. Purely decorative — real
305/// drag-and-drop still goes through the vanilla `Slot` this overlays.
306pub fn inv_slot(item_id: impl Into<String>, count: u32) -> Widget {
307    Widget::new(WidgetKind::InvSlot {
308        item_id: item_id.into(),
309        count,
310    })
311    .w(18.0)
312    .h(18.0)
313}
314pub fn mc_image(id: impl Into<String>, img_w: f32, img_h: f32) -> Widget {
315    Widget::new(WidgetKind::McImage {
316        id: id.into(),
317        img_w,
318        img_h,
319    })
320    .w(img_w)
321    .h(img_h)
322}
323pub fn spacer() -> Widget {
324    Widget::new(WidgetKind::Spacer)
325}