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 /// Labels/buttons: a single line (implies `no_wrap`) cut with `...` when
142 /// wider than the box, instead of overflowing it.
143 pub ellipsis: bool,
144 /// Containers: children are drawn (and hit) only inside this widget's
145 /// rect — anything scrolled or overflowing past it is cut off.
146 pub clip: bool,
147 /// Containers: vertical scroll offset of the children in GUI pixels
148 /// (0 = top). Layout clamps it to the content — the effective value is
149 /// [`LayoutNode::scroll_y`](crate::LayoutNode::scroll_y).
150 pub scroll_y: f32,
151 /// Containers: reserve a strip on the right edge and draw a vertical
152 /// scrollbar there whenever the content overflows (see
153 /// [`ScrollState`](crate::ScrollState) for driving it).
154 pub scrollbar: bool,
155}
156
157impl Default for Style {
158 fn default() -> Self {
159 Self {
160 w: Units::Auto,
161 h: Units::Auto,
162 min_w: 4.0,
163 min_h: 4.0,
164 flex: 0.0,
165 dock: Dock::None,
166 gap: 2.0,
167 pad: [0.0; 4],
168 margin: [0.0; 4],
169 bg: 0,
170 color: 0xFF_CCCCAA,
171 align: Align::Start,
172 font_scale: 1.0,
173 text_shadow: true,
174 focus_style: FocusStyle::default(),
175 focus_color: 0,
176 no_wrap: false,
177 pos: None,
178 ellipsis: false,
179 clip: false,
180 scroll_y: 0.0,
181 scrollbar: false,
182 }
183 }
184}
185
186// ── Builder API ───────────────────────────────────────────────────────────────
187
188impl Widget {
189 fn new(kind: WidgetKind) -> Self {
190 let flex_dir = if let WidgetKind::Panel(dir) = &kind {
191 *dir
192 } else {
193 FlexDir::Column
194 };
195 Self {
196 kind,
197 id: None,
198 style: Style::default(),
199 flex_dir,
200 children: Vec::new(),
201 on_click: None,
202 enabled: true,
203 focused: false,
204 }
205 }
206
207 pub fn id(mut self, id: impl Into<String>) -> Self {
208 self.id = Some(id.into());
209 self
210 }
211 pub fn on_click(mut self, ev: impl Into<String>) -> Self {
212 self.on_click = Some(ev.into());
213 self
214 }
215 pub fn child(mut self, w: Widget) -> Self {
216 self.children.push(w);
217 self
218 }
219
220 /// Accepts a plain px value (`0.0` = auto) or a [`Units`] directly —
221 /// e.g. `.w(18.0)` or `.w(Units::Percent(60.0))`.
222 pub fn w(mut self, w: impl Into<Units>) -> Self {
223 self.style.w = w.into();
224 self
225 }
226 pub fn h(mut self, h: impl Into<Units>) -> Self {
227 self.style.h = h.into();
228 self
229 }
230 pub fn min_w(mut self, v: f32) -> Self {
231 self.style.min_w = v;
232 self
233 }
234 pub fn min_h(mut self, v: f32) -> Self {
235 self.style.min_h = v;
236 self
237 }
238 pub fn flex(mut self, v: f32) -> Self {
239 self.style.flex = v;
240 self
241 }
242 pub fn flex_dir(mut self, v: FlexDir) -> Self {
243 self.flex_dir = v;
244 self
245 }
246 pub fn gap(mut self, v: f32) -> Self {
247 self.style.gap = v;
248 self
249 }
250 pub fn bg(mut self, v: u32) -> Self {
251 self.style.bg = v;
252 self
253 }
254 pub fn color(mut self, v: u32) -> Self {
255 self.style.color = v;
256 self
257 }
258 pub fn align(mut self, v: Align) -> Self {
259 self.style.align = v;
260 self
261 }
262 pub fn font_scale(mut self, v: f32) -> Self {
263 self.style.font_scale = v;
264 self
265 }
266 pub fn shadow(mut self, v: bool) -> Self {
267 self.style.text_shadow = v;
268 self
269 }
270 pub fn no_wrap(mut self) -> Self {
271 self.style.no_wrap = true;
272 self
273 }
274 pub fn focus_style(mut self, v: FocusStyle) -> Self {
275 self.style.focus_style = v;
276 self
277 }
278 pub fn focus_color(mut self, v: u32) -> Self {
279 self.style.focus_color = v;
280 self
281 }
282 pub fn dock(mut self, v: Dock) -> Self {
283 self.style.dock = v;
284 self
285 }
286 pub fn enabled(mut self, v: bool) -> Self {
287 self.enabled = v;
288 self
289 }
290 pub fn focused(mut self, v: bool) -> Self {
291 self.focused = v;
292 self
293 }
294 /// Position absolutely at `(x, y)` inside the parent's content box — see [`Style::pos`].
295 pub fn at(mut self, x: f32, y: f32) -> Self {
296 self.style.pos = Some((x, y));
297 self
298 }
299 /// Single line, cut with `...` when too wide — see [`Style::ellipsis`].
300 pub fn ellipsis(mut self) -> Self {
301 self.style.no_wrap = true;
302 self.style.ellipsis = true;
303 self
304 }
305 /// Clip children to this widget's rect — see [`Style::clip`].
306 pub fn clip(mut self, v: bool) -> Self {
307 self.style.clip = v;
308 self
309 }
310 /// Vertical scroll offset of the children — see [`Style::scroll_y`].
311 pub fn scroll_y(mut self, v: f32) -> Self {
312 self.style.scroll_y = v;
313 self
314 }
315 /// Draw a vertical scrollbar on overflow — see [`Style::scrollbar`].
316 pub fn scrollbar(mut self, v: bool) -> Self {
317 self.style.scrollbar = v;
318 self
319 }
320 /// Adds every widget in `children`, in order.
321 pub fn children(mut self, children: impl IntoIterator<Item = Widget>) -> Self {
322 self.children.extend(children);
323 self
324 }
325
326 /// The first widget in this subtree (depth-first, self included) whose
327 /// [`Widget::id`] is `id` — for changing a built tree before laying it
328 /// out (dynamic text, enabled state, ...).
329 pub fn find_mut(&mut self, id: &str) -> Option<&mut Widget> {
330 if self.id.as_deref() == Some(id) {
331 return Some(self);
332 }
333 self.children.iter_mut().find_map(|c| c.find_mut(id))
334 }
335
336 /// Replaces the text of a label or button; `false` for any other kind.
337 pub fn set_text(&mut self, text: impl Into<String>) -> bool {
338 match &mut self.kind {
339 WidgetKind::Label(t) | WidgetKind::Button(t) => {
340 *t = text.into();
341 true
342 }
343 _ => false,
344 }
345 }
346
347 pub fn padding(mut self, top: f32, right: f32, bottom: f32, left: f32) -> Self {
348 self.style.pad = [top, right, bottom, left];
349 self
350 }
351 pub fn margin(mut self, top: f32, right: f32, bottom: f32, left: f32) -> Self {
352 self.style.margin = [top, right, bottom, left];
353 self
354 }
355}
356
357// ── Widget constructors ───────────────────────────────────────────────────────
358
359pub fn panel(dir: FlexDir) -> Widget {
360 Widget::new(WidgetKind::Panel(dir))
361}
362/// A vertically scrolling column: clips its children, scrolls them by
363/// [`Widget::scroll_y`] and draws a scrollbar when they overflow — WPF's
364/// `ScrollViewer` around a `StackPanel`. Give it a fixed (or flex) height.
365pub fn scroll_view() -> Widget {
366 panel(FlexDir::Column).clip(true).scrollbar(true)
367}
368pub fn label(text: impl Into<String>) -> Widget {
369 Widget::new(WidgetKind::Label(text.into()))
370}
371pub fn button(text: impl Into<String>) -> Widget {
372 Widget::new(WidgetKind::Button(text.into()))
373}
374pub fn item_slot(item_id: impl Into<String>) -> Widget {
375 Widget::new(WidgetKind::ItemSlot(item_id.into()))
376}
377/// An inventory-slot decoration (icon + stack-count overlay) for `item_id`
378/// (empty string = empty slot) with `count` items. Purely decorative — real
379/// drag-and-drop still goes through the vanilla `Slot` this overlays.
380pub fn inv_slot(item_id: impl Into<String>, count: u32) -> Widget {
381 Widget::new(WidgetKind::InvSlot {
382 item_id: item_id.into(),
383 count,
384 })
385 .w(18.0)
386 .h(18.0)
387}
388pub fn mc_image(id: impl Into<String>, img_w: f32, img_h: f32) -> Widget {
389 Widget::new(WidgetKind::McImage {
390 id: id.into(),
391 img_w,
392 img_h,
393 })
394 .w(img_w)
395 .h(img_h)
396}
397pub fn spacer() -> Widget {
398 Widget::new(WidgetKind::Spacer)
399}