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/// Visual and layout style for a widget.
77#[derive(Debug, Clone)]
78pub struct Style {
79    pub w: f32, // explicit width; 0 = auto
80    pub h: f32, // explicit height; 0 = auto
81    pub min_w: f32,
82    pub min_h: f32,
83    pub flex: f32,        // grow factor inside flex container (main axis)
84    pub dock: Dock,       // WinForms-style edge attachment
85    pub gap: f32,         // spacing between children
86    pub pad: [f32; 4],    // top, right, bottom, left — space INSIDE the border
87    pub margin: [f32; 4], // top, right, bottom, left — space OUTSIDE the border
88    pub bg: u32,          // background colour 0xAARRGGBB; 0 = transparent
89    pub color: u32,       // text colour
90    pub align: Align,
91    pub font_scale: f32,
92    pub text_shadow: bool, // MC drop-shadow behind text (HUD default; books disable it)
93    pub focus_style: FocusStyle,
94    pub focus_color: u32, // 0 = default amber 0xFF_FFE040
95    /// Labels/buttons: never word-wrap, even if narrower than the text.
96    /// For single-line titles/headers that must stay on one line — matches
97    /// Patchouli, where such text overflows its box horizontally rather than
98    /// wrapping onto a second line.
99    pub no_wrap: bool,
100}
101
102impl Default for Style {
103    fn default() -> Self {
104        Self {
105            w: 0.0,
106            h: 0.0,
107            min_w: 4.0,
108            min_h: 4.0,
109            flex: 0.0,
110            dock: Dock::None,
111            gap: 2.0,
112            pad: [0.0; 4],
113            margin: [0.0; 4],
114            bg: 0,
115            color: 0xFF_CCCCAA,
116            align: Align::Start,
117            font_scale: 1.0,
118            text_shadow: true,
119            focus_style: FocusStyle::default(),
120            focus_color: 0,
121            no_wrap: false,
122        }
123    }
124}
125
126// ── Builder API ───────────────────────────────────────────────────────────────
127
128impl Widget {
129    fn new(kind: WidgetKind) -> Self {
130        let flex_dir = if let WidgetKind::Panel(dir) = &kind {
131            *dir
132        } else {
133            FlexDir::Column
134        };
135        Self {
136            kind,
137            id: None,
138            style: Style::default(),
139            flex_dir,
140            children: Vec::new(),
141            on_click: None,
142            enabled: true,
143            focused: false,
144        }
145    }
146
147    pub fn id(mut self, id: impl Into<String>) -> Self {
148        self.id = Some(id.into());
149        self
150    }
151    pub fn on_click(mut self, ev: impl Into<String>) -> Self {
152        self.on_click = Some(ev.into());
153        self
154    }
155    pub fn child(mut self, w: Widget) -> Self {
156        self.children.push(w);
157        self
158    }
159
160    pub fn w(mut self, w: f32) -> Self {
161        self.style.w = w;
162        self
163    }
164    pub fn h(mut self, h: f32) -> Self {
165        self.style.h = h;
166        self
167    }
168    pub fn min_w(mut self, v: f32) -> Self {
169        self.style.min_w = v;
170        self
171    }
172    pub fn min_h(mut self, v: f32) -> Self {
173        self.style.min_h = v;
174        self
175    }
176    pub fn flex(mut self, v: f32) -> Self {
177        self.style.flex = v;
178        self
179    }
180    pub fn flex_dir(mut self, v: FlexDir) -> Self {
181        self.flex_dir = v;
182        self
183    }
184    pub fn gap(mut self, v: f32) -> Self {
185        self.style.gap = v;
186        self
187    }
188    pub fn bg(mut self, v: u32) -> Self {
189        self.style.bg = v;
190        self
191    }
192    pub fn color(mut self, v: u32) -> Self {
193        self.style.color = v;
194        self
195    }
196    pub fn align(mut self, v: Align) -> Self {
197        self.style.align = v;
198        self
199    }
200    pub fn font_scale(mut self, v: f32) -> Self {
201        self.style.font_scale = v;
202        self
203    }
204    pub fn shadow(mut self, v: bool) -> Self {
205        self.style.text_shadow = v;
206        self
207    }
208    pub fn no_wrap(mut self) -> Self {
209        self.style.no_wrap = true;
210        self
211    }
212    pub fn focus_style(mut self, v: FocusStyle) -> Self {
213        self.style.focus_style = v;
214        self
215    }
216    pub fn focus_color(mut self, v: u32) -> Self {
217        self.style.focus_color = v;
218        self
219    }
220    pub fn dock(mut self, v: Dock) -> Self {
221        self.style.dock = v;
222        self
223    }
224    pub fn enabled(mut self, v: bool) -> Self {
225        self.enabled = v;
226        self
227    }
228    pub fn focused(mut self, v: bool) -> Self {
229        self.focused = v;
230        self
231    }
232    pub fn padding(mut self, top: f32, right: f32, bottom: f32, left: f32) -> Self {
233        self.style.pad = [top, right, bottom, left];
234        self
235    }
236    pub fn margin(mut self, top: f32, right: f32, bottom: f32, left: f32) -> Self {
237        self.style.margin = [top, right, bottom, left];
238        self
239    }
240}
241
242// ── Widget constructors ───────────────────────────────────────────────────────
243
244pub fn panel(dir: FlexDir) -> Widget {
245    Widget::new(WidgetKind::Panel(dir))
246}
247pub fn label(text: impl Into<String>) -> Widget {
248    Widget::new(WidgetKind::Label(text.into()))
249}
250pub fn button(text: impl Into<String>) -> Widget {
251    Widget::new(WidgetKind::Button(text.into()))
252}
253pub fn item_slot(item_id: impl Into<String>) -> Widget {
254    Widget::new(WidgetKind::ItemSlot(item_id.into()))
255}
256/// An inventory-slot decoration (icon + stack-count overlay) for `item_id`
257/// (empty string = empty slot) with `count` items. Purely decorative — real
258/// drag-and-drop still goes through the vanilla `Slot` this overlays.
259pub fn inv_slot(item_id: impl Into<String>, count: u32) -> Widget {
260    Widget::new(WidgetKind::InvSlot {
261        item_id: item_id.into(),
262        count,
263    })
264    .w(18.0)
265    .h(18.0)
266}
267pub fn mc_image(id: impl Into<String>, img_w: f32, img_h: f32) -> Widget {
268    Widget::new(WidgetKind::McImage {
269        id: id.into(),
270        img_w,
271        img_h,
272    })
273    .w(img_w)
274    .h(img_h)
275}
276pub fn spacer() -> Widget {
277    Widget::new(WidgetKind::Spacer)
278}