Skip to main content

yog_ui/
widget.rs

1//! Widget types and styling.
2
3use yog_gfx::{Color, Rect};
4
5use crate::layout::{Align, FlexDir};
6
7/// A single widget in the UI tree.
8#[derive(Debug, Clone)]
9pub struct Widget {
10    pub kind: WidgetKind,
11    pub id: Option<String>,
12    pub style: Style,
13    pub flex_dir: FlexDir,
14    pub children: Vec<Widget>,
15    pub on_click: Option<String>,
16    pub enabled: bool,
17    pub focused: bool,
18    /// Typed click target set by generated code (yog-kit) — like
19    /// [`Self::on_click`], but no string to parse. See [`Action`].
20    pub action: Option<Action>,
21    /// `false` = collapsed: takes no space, isn't drawn and can't be hit
22    /// (its layout node is a zero rect without children).
23    pub visible: bool,
24    /// Interaction state (hover, pressed, selected, ...) the host sets each
25    /// frame — picks the [`Self::states`] patches applied when drawing.
26    pub state: StateFlags,
27    /// Paint-only style overrides per state (`hover:bg="..."` in `.yui`).
28    pub states: Option<Box<StateStyles>>,
29    /// Text shown in a tooltip while the cursor rests on it (`\n` breaks
30    /// lines) — see [`crate::UiRoot::tooltip_at`].
31    pub tooltip: Option<String>,
32}
33
34/// A typed click target: `owner` 0 = the screen, `k` = items control `k`;
35/// `slot` = the command (or [`Action::SELECT`]); `index` = the item index
36/// inside an items control (0 elsewhere).
37#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
38pub struct Action {
39    pub owner: u16,
40    pub slot: u16,
41    pub index: u32,
42}
43
44impl Action {
45    /// The `slot` of an items control's item container: clicking selects it.
46    pub const SELECT: u16 = u16::MAX;
47
48    /// A screen-level command `slot`.
49    pub const fn new(slot: u16) -> Self {
50        Self {
51            owner: 0,
52            slot,
53            index: 0,
54        }
55    }
56}
57
58/// Interaction state bits of a widget — see [`Widget::state`].
59#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
60pub struct StateFlags(pub u8);
61
62impl StateFlags {
63    pub const NONE: Self = Self(0);
64    pub const HOVER: Self = Self(1);
65    pub const PRESSED: Self = Self(2);
66    pub const FOCUSED: Self = Self(4);
67    pub const SELECTED: Self = Self(8);
68    pub const CHECKED: Self = Self(16);
69    pub const DISABLED: Self = Self(32);
70
71    /// Every bit of `o` is set.
72    pub const fn contains(self, o: Self) -> bool {
73        self.0 & o.0 == o.0
74    }
75    pub fn insert(&mut self, o: Self) {
76        self.0 |= o.0;
77    }
78    pub fn remove(&mut self, o: Self) {
79        self.0 &= !o.0;
80    }
81    /// Sets or clears the bits of `o`.
82    pub fn set(&mut self, o: Self, on: bool) {
83        if on { self.insert(o) } else { self.remove(o) }
84    }
85    pub const fn union(self, o: Self) -> Self {
86        Self(self.0 | o.0)
87    }
88    pub const fn is_empty(self) -> bool {
89        self.0 == 0
90    }
91}
92
93impl std::ops::BitOr for StateFlags {
94    type Output = Self;
95    fn bitor(self, o: Self) -> Self {
96        self.union(o)
97    }
98}
99
100/// Paint-only style overrides — never affect layout.
101#[derive(Debug, Clone, Default, PartialEq)]
102pub struct StylePatch {
103    pub bg: Option<Color>,
104    pub bg_image: Option<String>,
105    pub color: Option<Color>,
106    pub shadow: Option<bool>,
107    pub focus_color: Option<Color>,
108}
109
110impl StylePatch {
111    /// `true` when it overrides nothing.
112    pub fn is_empty(&self) -> bool {
113        *self == Self::default()
114    }
115
116    fn apply(&self, s: &mut Style) {
117        if let Some(v) = self.bg {
118            s.bg = v;
119        }
120        if let Some(v) = &self.bg_image {
121            s.bg_image.clone_from(v);
122        }
123        if let Some(v) = self.color {
124            s.color = v;
125        }
126        if let Some(v) = self.shadow {
127            s.text_shadow = v;
128        }
129        if let Some(v) = self.focus_color {
130            s.focus_color = v;
131        }
132    }
133}
134
135/// A widget's state patches: each applies when the widget's state contains
136/// its flags; later rules win.
137#[derive(Debug, Clone, Default, PartialEq)]
138pub struct StateStyles {
139    pub rules: Vec<(StateFlags, StylePatch)>,
140}
141
142#[derive(Debug, Clone)]
143pub enum WidgetKind {
144    /// Container — arranges children according to `flex_dir`.
145    Panel(FlexDir),
146    /// Container — WPF's `Grid`: rows and columns sized in pixels, to
147    /// content (`Auto`) or as shares of the rest (`*`); each child sits in
148    /// the cell(s) its [`Style::cell`] names — see [`grid`] and
149    /// [`GridSpec`].
150    Grid(Box<GridSpec>),
151    /// Static or dynamic text label.
152    Label(String),
153    /// Clickable button with text.
154    Button(String),
155    /// Minecraft item icon (static, known at build time).
156    ItemSlot(String),
157    /// Inventory-slot decoration: an item icon plus a stack-count overlay,
158    /// both supplied as plain data at tree-build time (like every other
159    /// widget here) — this crate has no notion of "the currently open menu"
160    /// or how to query it; that's the caller's job (see `yog-inventory`).
161    /// Empty `item_id` draws an empty slot box.
162    InvSlot { item_id: String, count: u32 },
163    /// An image from a Yog image source (`item:apple`, `sprite:button`,
164    /// `mymod:gui/logo`, ...) — see [`Image`] and [`image`].
165    Image(Image),
166    /// Invisible spacer.
167    Spacer,
168    /// Drawn by the code: the [`Surface`](crate::Surface) registered under
169    /// this id (see [`crate::surface`] and [`surface`]).
170    Surface(String),
171    /// Single-line text input — see [`TextBox`] and
172    /// [`TextBoxState`](crate::TextBoxState), which owns the text and drives it.
173    TextBox(TextBox),
174}
175
176/// How a grid row or column is sized — WPF's `GridLength`.
177#[derive(Debug, Clone, Copy, PartialEq)]
178pub enum GridLength {
179    /// Fits its content: the largest child in it (`Auto`).
180    Auto,
181    /// A fixed size in GUI pixels (`26`).
182    Px(f32),
183    /// A share of the space the other rows/columns leave (`*` = `Star(1.0)`,
184    /// `2*` = `Star(2.0)`). Along an axis being measured for content (an
185    /// auto-sized grid in a scroll view, ...) it acts as [`Self::Auto`].
186    Star(f32),
187}
188
189impl GridLength {
190    /// `*`.
191    pub const STAR: Self = Self::Star(1.0);
192}
193
194/// One row or column of a [`GridSpec`]: its size and the limits on it.
195#[derive(Debug, Clone, Copy, PartialEq)]
196pub struct Track {
197    pub size: GridLength,
198    /// Never smaller than this (WPF's `MinHeight`/`MinWidth`).
199    pub min: f32,
200    /// Never larger than this (`f32::INFINITY` = no limit).
201    pub max: f32,
202}
203
204impl Track {
205    pub const fn new(size: GridLength) -> Self {
206        Self {
207            size,
208            min: 0.0,
209            max: f32::INFINITY,
210        }
211    }
212    /// `Auto`.
213    pub const fn auto() -> Self {
214        Self::new(GridLength::Auto)
215    }
216    /// A fixed `px`.
217    pub const fn px(px: f32) -> Self {
218        Self::new(GridLength::Px(px))
219    }
220    /// `weight*`.
221    pub const fn star(weight: f32) -> Self {
222        Self::new(GridLength::Star(weight))
223    }
224    pub const fn min(mut self, v: f32) -> Self {
225        self.min = v;
226        self
227    }
228    pub const fn max(mut self, v: f32) -> Self {
229        self.max = v;
230        self
231    }
232}
233
234impl From<GridLength> for Track {
235    fn from(size: GridLength) -> Self {
236        Self::new(size)
237    }
238}
239
240/// A grid's rows, columns and the space between them. No rows (columns)
241/// = one `*` row (column), like WPF.
242#[derive(Debug, Clone, Default, PartialEq)]
243pub struct GridSpec {
244    pub rows: Vec<Track>,
245    pub cols: Vec<Track>,
246    /// Space between rows (WinUI's `RowSpacing`).
247    pub row_spacing: f32,
248    /// Space between columns (WinUI's `ColumnSpacing`).
249    pub col_spacing: f32,
250}
251
252/// The grid cell(s) a widget occupies — WPF's `Grid.Row`, `Grid.Column`,
253/// `Grid.RowSpan`, `Grid.ColumnSpan`. Only a [`grid`] reads it; out-of-range
254/// values are clamped to the grid (like WPF).
255#[derive(Debug, Clone, Copy, PartialEq, Eq)]
256pub struct Cell {
257    pub row: u16,
258    pub col: u16,
259    /// Rows spanned, at least 1.
260    pub row_span: u16,
261    /// Columns spanned, at least 1.
262    pub col_span: u16,
263}
264
265impl Default for Cell {
266    fn default() -> Self {
267        Self {
268            row: 0,
269            col: 0,
270            row_span: 1,
271            col_span: 1,
272        }
273    }
274}
275
276/// Where a widget sits horizontally in the space its parent gives it — WPF's
277/// `HorizontalAlignment`. In a grid cell `None` means `Stretch`; in a
278/// Column it overrides the parent's `align` for this child.
279#[derive(Debug, Clone, Copy, PartialEq, Eq)]
280pub enum HAlign {
281    Left,
282    Center,
283    Right,
284    /// Fill the width (unless the widget has an explicit `w`).
285    Stretch,
286}
287
288/// Where a widget sits vertically — WPF's `VerticalAlignment`; see [`HAlign`].
289#[derive(Debug, Clone, Copy, PartialEq, Eq)]
290pub enum VAlign {
291    Top,
292    Center,
293    Bottom,
294    /// Fill the height (unless the widget has an explicit `h`).
295    Stretch,
296}
297
298/// An image widget's data — see [`image`].
299#[derive(Debug, Clone, PartialEq)]
300pub struct Image {
301    /// A Yog image source (see [`yog_gfx::image`]).
302    pub source: String,
303    /// How the image fills the widget (default [`Stretch::Uniform`]).
304    pub stretch: Stretch,
305    /// The part of the image drawn, `[u, v, w, h]` in the image's pixels
306    /// (`None` = all of it). Textures only — items and sprites are whole.
307    pub region: Option<Rect>,
308    /// Color multiplier, `0xAARRGGBB` (`0xFFFFFFFF` = none; items ignore it).
309    pub tint: Color,
310}
311
312pub use yog_gfx::image::Stretch;
313
314/// A text box's content and editing state, as drawn this frame. Built by
315/// [`text_box`] (text, placeholder, max length, event names); the caret,
316/// selection and horizontal scroll are filled in each frame by
317/// [`TextBoxState::apply`](crate::TextBoxState::apply).
318#[derive(Debug, Clone, Default, PartialEq)]
319pub struct TextBox {
320    pub text: String,
321    /// Shown greyed while the text is empty.
322    pub placeholder: String,
323    /// Longest text allowed, in characters; 0 = unlimited.
324    pub max_length: usize,
325    /// Event name for text changes (see `TextBoxState::event_name`).
326    pub on_change: Option<String>,
327    /// Event name for Enter (see `TextBoxState::event_name`).
328    pub on_submit: Option<String>,
329    /// Caret position in characters — `Some` while the box has focus.
330    pub caret: Option<usize>,
331    /// Draw the caret this frame (it blinks).
332    pub caret_visible: bool,
333    /// Selected characters `start..end` (empty = none).
334    pub selection: (usize, usize),
335    /// First visible character (horizontal scroll).
336    pub first: usize,
337}
338
339/// Side of an item / inventory slot box in GUI pixels (vanilla's 18).
340pub const SLOT_SIZE: f32 = 18.0;
341/// A `<Screen>`'s size when it doesn't give one: the game's container screens' (176 × 166).
342pub const SCREEN_W: f32 = 176.0;
343pub const SCREEN_H: f32 = 166.0;
344/// Focus indicator color when `focus_color` is 0 (amber).
345pub const DEFAULT_FOCUS_COLOR: Color = Color(0xFF_FFE040);
346
347/// A text box's default left/right padding.
348pub(crate) const TEXT_BOX_PAD_X: f32 = 4.0;
349/// Placeholder text color of a text box.
350pub const PLACEHOLDER_COLOR: Color = Color(0xFF_707070);
351/// Border of an unfocused text box (vanilla `EditBox`).
352pub const TEXT_BOX_BORDER: Color = Color(0xFF_A0A0A0);
353/// Border of a focused text box unless `focus_color` is set.
354pub const TEXT_BOX_FOCUS_BORDER: Color = Color(0xFF_FFFFFF);
355/// Selection highlight behind selected text.
356pub const SELECTION_COLOR: Color = Color(0xFF_2850A0);
357
358/// WinForms-style dock — which edge(s) the widget attaches to inside its parent.
359///
360/// - `None`   — normal flex positioning (default)
361/// - `Fill`   — stretch to fill all remaining space (both axes)
362/// - `Left`   — full height, natural width, hugs left edge; other children flow right
363/// - `Right`  — full height, natural width, hugs right edge
364/// - `Top`    — full width, natural height, hugs top edge; other children flow down
365/// - `Bottom` — full width, natural height, hugs bottom edge
366#[derive(Debug, Clone, Copy, PartialEq, Default)]
367pub enum Dock {
368    #[default]
369    None,
370    Fill,
371    Left,
372    Right,
373    Top,
374    Bottom,
375}
376
377/// How a focused widget shows its focus indicator.
378#[derive(Debug, Clone, Copy, PartialEq, Default)]
379pub enum FocusStyle {
380    /// 1px outline using `focus_color` (default, amber).
381    #[default]
382    Outline,
383    /// Solid background fill using `focus_color`.
384    Fill,
385    /// No visual indicator.
386    None,
387}
388
389/// A size value — either a fixed pixel amount, a CSS-style percentage of the
390/// parent's own resolved content size (0.0–100.0), or `Auto` (size to
391/// content/available space, this crate's original behaviour).
392#[derive(Debug, Clone, Copy, PartialEq, Default)]
393pub enum Units {
394    #[default]
395    Auto,
396    Px(f32),
397    /// Percent of the parent's resolved content size along the same axis.
398    Percent(f32),
399}
400
401impl From<f32> for Units {
402    /// `0.0` maps to `Auto` (matches this crate's original `0.0 = auto` convention,
403    /// so every existing `.w(18.0)`/`.h(0.0)` call keeps meaning exactly what it did).
404    fn from(v: f32) -> Self {
405        if v <= 0.0 { Units::Auto } else { Units::Px(v) }
406    }
407}
408
409impl Units {
410    /// Resolve against `avail` (the parent's resolved content size on this
411    /// axis). `None` for `Auto` — caller falls back to its own auto-sizing.
412    pub fn resolve(self, avail: f32) -> Option<f32> {
413        match self {
414            Units::Auto => None,
415            Units::Px(v) => Some(v),
416            Units::Percent(p) => Some(avail * p / 100.0),
417        }
418    }
419
420    pub fn is_auto(self) -> bool {
421        matches!(self, Units::Auto)
422    }
423}
424
425/// Visual and layout style for a widget.
426#[derive(Debug, Clone)]
427pub struct Style {
428    pub w: Units, // explicit width; Auto = size to content/available space
429    pub h: Units, // explicit height; Auto = size to content/available space
430    pub min_w: f32,
431    pub min_h: f32,
432    pub flex: f32,        // grow factor inside flex container (main axis)
433    pub dock: Dock,       // WinForms-style edge attachment
434    pub gap: f32,         // spacing between children
435    pub pad: [f32; 4],    // top, right, bottom, left — space INSIDE the border
436    pub margin: [f32; 4], // top, right, bottom, left — space OUTSIDE the border
437    /// The grid cell(s) it occupies inside a [`grid`] — see [`Cell`].
438    pub cell: Cell,
439    /// Horizontal placement in its slot (grid cell, or a Column's width) —
440    /// see [`HAlign`]. `None` = the parent's default.
441    pub halign: Option<HAlign>,
442    /// Vertical placement in its slot (grid cell, or a Row's height) — see
443    /// [`VAlign`]. `None` = the parent's default.
444    pub valign: Option<VAlign>,
445    pub bg: Color, // background color; transparent = none
446    /// Background image, drawn over `bg` and stretched to the box — a Yog
447    /// image source (`sprite:button`, `sprite:panel`, `mymod:gui/frame`);
448    /// sprites are nine-sliced, so frames keep their edges. Empty = none.
449    pub bg_image: String,
450    pub color: Color, // text color
451    pub align: Align,
452    pub font_scale: f32,
453    pub text_shadow: bool, // MC drop-shadow behind text (HUD default; books disable it)
454    pub focus_style: FocusStyle,
455    pub focus_color: Color, // transparent = DEFAULT_FOCUS_COLOR
456    /// Labels/buttons: never word-wrap, even if narrower than the text.
457    /// For single-line titles/headers that must stay on one line — matches
458    /// Patchouli, where such text overflows its box horizontally rather than
459    /// wrapping onto a second line.
460    pub no_wrap: bool,
461    /// Absolute position `(x, y)` inside the parent's content box (inside its
462    /// padding) — WPF `Canvas` / WinForms `Location` style. A positioned
463    /// widget is taken out of the parent's flex flow: it takes no main-axis
464    /// space, ignores `flex`/`dock`, and is drawn in tree order (later
465    /// siblings on top). `None` (default) = normal flex positioning.
466    pub pos: Option<(f32, f32)>,
467    /// Labels/buttons: a single line (implies `no_wrap`) cut with `...` when
468    /// wider than the box, instead of overflowing it.
469    pub ellipsis: bool,
470    /// Containers: children are drawn (and hit) only inside this widget's
471    /// rect — anything scrolled or overflowing past it is cut off.
472    pub clip: bool,
473    /// Containers: vertical scroll offset of the children in GUI pixels
474    /// (0 = top). Layout clamps it to the content — the effective value is
475    /// [`LayoutNode::scroll_y`](crate::LayoutNode::scroll_y).
476    pub scroll_y: f32,
477    /// Containers: reserve a strip on the right edge and draw a vertical
478    /// scrollbar there whenever the content overflows (see
479    /// [`ScrollState`](crate::ScrollState) for driving it).
480    pub scrollbar: bool,
481    /// Font id for text — empty (default) = Minecraft's default font
482    /// (`minecraft:default`); also `minecraft:uniform`, `minecraft:alt`,
483    /// `minecraft:illageralt` or a resource-pack font such as `mymod:fancy`.
484    /// See [`crate::text`] for how non-default fonts are measured.
485    pub font: String,
486}
487
488impl Default for Style {
489    fn default() -> Self {
490        Self {
491            w: Units::Auto,
492            h: Units::Auto,
493            min_w: 4.0,
494            min_h: 4.0,
495            flex: 0.0,
496            dock: Dock::None,
497            gap: 2.0,
498            pad: [0.0; 4],
499            margin: [0.0; 4],
500            cell: Cell::default(),
501            halign: None,
502            valign: None,
503            bg: Color::TRANSPARENT,
504            bg_image: String::new(),
505            color: Color(0xFF_CCCCAA),
506            align: Align::Start,
507            font_scale: 1.0,
508            text_shadow: true,
509            focus_style: FocusStyle::default(),
510            focus_color: Color::TRANSPARENT,
511            no_wrap: false,
512            pos: None,
513            ellipsis: false,
514            clip: false,
515            scroll_y: 0.0,
516            scrollbar: false,
517            font: String::new(),
518        }
519    }
520}
521
522// ── Builder API ───────────────────────────────────────────────────────────────
523
524impl Widget {
525    fn new(kind: WidgetKind) -> Self {
526        let flex_dir = if let WidgetKind::Panel(dir) = &kind {
527            *dir
528        } else {
529            FlexDir::Column
530        };
531        Self {
532            kind,
533            id: None,
534            style: Style::default(),
535            flex_dir,
536            children: Vec::new(),
537            on_click: None,
538            enabled: true,
539            focused: false,
540            action: None,
541            visible: true,
542            state: StateFlags::NONE,
543            states: None,
544            tooltip: None,
545        }
546    }
547
548    /// See [`Self::tooltip`]; empty = none.
549    pub fn tooltip(mut self, text: impl Into<String>) -> Self {
550        self.set_tooltip(text);
551        self
552    }
553    /// Sets (empty: clears) [`Self::tooltip`].
554    pub fn set_tooltip(&mut self, text: impl Into<String>) {
555        let t = text.into();
556        self.tooltip = (!t.is_empty()).then_some(t);
557    }
558
559    /// Typed click target — see [`Action`].
560    pub fn action(mut self, a: Action) -> Self {
561        self.action = Some(a);
562        self
563    }
564    /// `false` = collapsed — see [`Widget::visible`].
565    pub fn visible(mut self, v: bool) -> Self {
566        self.visible = v;
567        self
568    }
569    /// Adds a paint-only override applied while the state contains `when`
570    /// (later calls win over earlier ones).
571    pub fn state_style(mut self, when: StateFlags, patch: StylePatch) -> Self {
572        self.states
573            .get_or_insert_with(Default::default)
574            .rules
575            .push((when, patch));
576        self
577    }
578
579    /// The descendant at child-index `path` (`&[]` = self); `None` past the
580    /// tree.
581    pub fn get_path(&self, path: &[u16]) -> Option<&Widget> {
582        let mut w = self;
583        for &i in path {
584            w = w.children.get(i as usize)?;
585        }
586        Some(w)
587    }
588    /// [`Self::get_path`], mutably.
589    pub fn get_path_mut(&mut self, path: &[u16]) -> Option<&mut Widget> {
590        let mut w = self;
591        for &i in path {
592            w = w.children.get_mut(i as usize)?;
593        }
594        Some(w)
595    }
596    /// The descendant at `path`. Panics on a bad path — for generated code,
597    /// whose paths are fixed when it's generated.
598    pub fn at_path(&self, path: &[u16]) -> &Widget {
599        self.get_path(path)
600            .unwrap_or_else(|| panic!("yog-ui: no widget at path {path:?}"))
601    }
602    /// [`Self::at_path`], mutably.
603    pub fn at_path_mut(&mut self, path: &[u16]) -> &mut Widget {
604        self.get_path_mut(path)
605            .unwrap_or_else(|| panic!("yog-ui: no widget at path {path:?}"))
606    }
607
608    /// Whether clicking it does something (an [`Action`] or `on_click`).
609    pub fn is_interactive(&self) -> bool {
610        self.action.is_some() || self.on_click.is_some()
611    }
612
613    /// Sets (or clears) `mask` on this widget and its descendants, stopping
614    /// at descendants that are interactive themselves (a button in a row
615    /// keeps its own hover) — so a row's labels follow the row's state.
616    pub fn set_state_inherited(&mut self, mask: StateFlags, on: bool) {
617        self.state.set(mask, on);
618        for c in &mut self.children {
619            if !c.is_interactive() {
620                c.set_state_inherited(mask, on);
621            }
622        }
623    }
624
625    /// Clears `mask` on the whole subtree (interactive descendants too).
626    pub fn clear_state_deep(&mut self, mask: StateFlags) {
627        self.state.remove(mask);
628        for c in &mut self.children {
629            c.clear_state_deep(mask);
630        }
631    }
632
633    /// The style drawn: [`Self::style`] plus the [`Self::states`] patches
634    /// matching [`Self::state`] (with DISABLED while `enabled` is false).
635    pub fn effective_style(&self) -> std::borrow::Cow<'_, Style> {
636        let mut state = self.state;
637        if !self.enabled {
638            state.insert(StateFlags::DISABLED);
639        }
640        self.effective_style_in(state)
641    }
642
643    /// [`Self::effective_style`] for an explicit `state`.
644    pub fn effective_style_in(&self, state: StateFlags) -> std::borrow::Cow<'_, Style> {
645        let Some(states) = &self.states else {
646            return std::borrow::Cow::Borrowed(&self.style);
647        };
648        let mut matching = states
649            .rules
650            .iter()
651            .filter(|(when, _)| !when.is_empty() && state.contains(*when))
652            .peekable();
653        if matching.peek().is_none() {
654            return std::borrow::Cow::Borrowed(&self.style);
655        }
656        let mut s = self.style.clone();
657        for (_, patch) in matching {
658            patch.apply(&mut s);
659        }
660        std::borrow::Cow::Owned(s)
661    }
662
663    pub fn id(mut self, id: impl Into<String>) -> Self {
664        self.id = Some(id.into());
665        self
666    }
667    pub fn on_click(mut self, ev: impl Into<String>) -> Self {
668        self.on_click = Some(ev.into());
669        self
670    }
671    pub fn child(mut self, w: Widget) -> Self {
672        self.children.push(w);
673        self
674    }
675
676    /// Accepts a plain px value (`0.0` = auto) or a [`Units`] directly —
677    /// e.g. `.w(18.0)` or `.w(Units::Percent(60.0))`.
678    pub fn w(mut self, w: impl Into<Units>) -> Self {
679        self.style.w = w.into();
680        self
681    }
682    pub fn h(mut self, h: impl Into<Units>) -> Self {
683        self.style.h = h.into();
684        self
685    }
686    pub fn min_w(mut self, v: f32) -> Self {
687        self.style.min_w = v;
688        self
689    }
690    pub fn min_h(mut self, v: f32) -> Self {
691        self.style.min_h = v;
692        self
693    }
694    pub fn flex(mut self, v: f32) -> Self {
695        self.style.flex = v;
696        self
697    }
698    pub fn flex_dir(mut self, v: FlexDir) -> Self {
699        self.flex_dir = v;
700        self
701    }
702    pub fn gap(mut self, v: f32) -> Self {
703        self.style.gap = v;
704        self
705    }
706    pub fn bg(mut self, v: Color) -> Self {
707        self.style.bg = v;
708        self
709    }
710    /// See [`Style::bg_image`].
711    pub fn bg_image(mut self, source: impl Into<String>) -> Self {
712        self.style.bg_image = source.into();
713        self
714    }
715    pub fn color(mut self, v: Color) -> Self {
716        self.style.color = v;
717        self
718    }
719    pub fn align(mut self, v: Align) -> Self {
720        self.style.align = v;
721        self
722    }
723    pub fn font_scale(mut self, v: f32) -> Self {
724        self.style.font_scale = v;
725        self
726    }
727    pub fn shadow(mut self, v: bool) -> Self {
728        self.style.text_shadow = v;
729        self
730    }
731    pub fn no_wrap(mut self) -> Self {
732        self.style.no_wrap = true;
733        self
734    }
735    pub fn focus_style(mut self, v: FocusStyle) -> Self {
736        self.style.focus_style = v;
737        self
738    }
739    pub fn focus_color(mut self, v: Color) -> Self {
740        self.style.focus_color = v;
741        self
742    }
743    pub fn dock(mut self, v: Dock) -> Self {
744        self.style.dock = v;
745        self
746    }
747    pub fn enabled(mut self, v: bool) -> Self {
748        self.enabled = v;
749        self
750    }
751    pub fn focused(mut self, v: bool) -> Self {
752        self.focused = v;
753        self
754    }
755    /// Position absolutely at `(x, y)` inside the parent's content box — see [`Style::pos`].
756    pub fn at(mut self, x: f32, y: f32) -> Self {
757        self.style.pos = Some((x, y));
758        self
759    }
760    /// Single line, cut with `...` when too wide — see [`Style::ellipsis`].
761    pub fn ellipsis(mut self) -> Self {
762        self.style.no_wrap = true;
763        self.style.ellipsis = true;
764        self
765    }
766    /// Clip children to this widget's rect — see [`Style::clip`].
767    pub fn clip(mut self, v: bool) -> Self {
768        self.style.clip = v;
769        self
770    }
771    /// Vertical scroll offset of the children — see [`Style::scroll_y`].
772    pub fn scroll_y(mut self, v: f32) -> Self {
773        self.style.scroll_y = v;
774        self
775    }
776    /// Draw a vertical scrollbar on overflow — see [`Style::scrollbar`].
777    pub fn scrollbar(mut self, v: bool) -> Self {
778        self.style.scrollbar = v;
779        self
780    }
781    /// Font for this widget's text — see [`Style::font`].
782    pub fn font(mut self, font: impl Into<String>) -> Self {
783        self.style.font = font.into();
784        self
785    }
786    /// Text box: greyed text shown while it's empty.
787    pub fn placeholder(mut self, text: impl Into<String>) -> Self {
788        if let WidgetKind::TextBox(t) = &mut self.kind {
789            t.placeholder = text.into();
790        }
791        self
792    }
793    /// Text box: longest text allowed, in characters (0 = unlimited).
794    pub fn max_length(mut self, n: usize) -> Self {
795        if let WidgetKind::TextBox(t) = &mut self.kind {
796            t.max_length = n;
797        }
798        self
799    }
800    /// Text box: event name raised when the text changes.
801    pub fn on_change(mut self, ev: impl Into<String>) -> Self {
802        if let WidgetKind::TextBox(t) = &mut self.kind {
803            t.on_change = Some(ev.into());
804        }
805        self
806    }
807    /// Text box: event name raised by Enter.
808    pub fn on_submit(mut self, ev: impl Into<String>) -> Self {
809        if let WidgetKind::TextBox(t) = &mut self.kind {
810            t.on_submit = Some(ev.into());
811        }
812        self
813    }
814    /// Image: how it fills the widget (WPF's `Stretch`).
815    pub fn stretch(mut self, stretch: Stretch) -> Self {
816        if let WidgetKind::Image(i) = &mut self.kind {
817            i.stretch = stretch;
818        }
819        self
820    }
821    /// Image: draw only `region` of it (texture pixels).
822    pub fn region(mut self, region: Rect) -> Self {
823        if let WidgetKind::Image(i) = &mut self.kind {
824            i.region = Some(region);
825        }
826        self
827    }
828    /// Image: multiply its colors by `tint`.
829    pub fn tint(mut self, tint: Color) -> Self {
830        if let WidgetKind::Image(i) = &mut self.kind {
831            i.tint = tint;
832        }
833        self
834    }
835    /// Adds every widget in `children`, in order.
836    pub fn children(mut self, children: impl IntoIterator<Item = Widget>) -> Self {
837        self.children.extend(children);
838        self
839    }
840
841    /// The first widget in this subtree (depth-first, self included) whose
842    /// [`Widget::id`] is `id` — for changing a built tree before laying it
843    /// out (dynamic text, enabled state, ...).
844    pub fn find_mut(&mut self, id: &str) -> Option<&mut Widget> {
845        if self.id.as_deref() == Some(id) {
846            return Some(self);
847        }
848        self.children.iter_mut().find_map(|c| c.find_mut(id))
849    }
850
851    /// Replaces the text of a label, button or text box; `false` for any
852    /// other kind.
853    pub fn set_text(&mut self, text: impl Into<String>) -> bool {
854        match &mut self.kind {
855            WidgetKind::Label(t) | WidgetKind::Button(t) => {
856                *t = text.into();
857                true
858            }
859            WidgetKind::TextBox(t) => {
860                t.text = text.into();
861                true
862            }
863            _ => false,
864        }
865    }
866
867    /// [`set_text`](Self::set_text) as a builder call.
868    pub fn with_text(mut self, text: impl Into<String>) -> Self {
869        self.set_text(text);
870        self
871    }
872
873    /// Replaces an image's source or a slot's item id (other kinds: no-op).
874    pub fn with_source(mut self, source: impl Into<String>) -> Self {
875        match &mut self.kind {
876            WidgetKind::Image(i) => i.source = source.into(),
877            WidgetKind::Surface(id) => *id = source.into(),
878            WidgetKind::ItemSlot(id) | WidgetKind::InvSlot { item_id: id, .. } => {
879                *id = source.into()
880            }
881            _ => {}
882        }
883        self
884    }
885
886    /// Replaces an inventory slot's stack count (other kinds: no-op).
887    pub fn with_count(mut self, n: u32) -> Self {
888        if let WidgetKind::InvSlot { count, .. } = &mut self.kind {
889            *count = n;
890        }
891        self
892    }
893
894    pub fn padding(mut self, top: f32, right: f32, bottom: f32, left: f32) -> Self {
895        self.style.pad = [top, right, bottom, left];
896        self
897    }
898    pub fn margin(mut self, top: f32, right: f32, bottom: f32, left: f32) -> Self {
899        self.style.margin = [top, right, bottom, left];
900        self
901    }
902
903    /// Puts it in grid cell `(row, col)` (spanning one row and column) —
904    /// WPF's `Grid.Row` / `Grid.Column`. See [`Self::span`].
905    pub fn cell(mut self, row: u16, col: u16) -> Self {
906        self.style.cell.row = row;
907        self.style.cell.col = col;
908        self
909    }
910    /// Spans `rows` × `cols` grid cells from its [`Self::cell`] — WPF's
911    /// `Grid.RowSpan` / `Grid.ColumnSpan` (0 counts as 1).
912    pub fn span(mut self, rows: u16, cols: u16) -> Self {
913        self.style.cell.row_span = rows.max(1);
914        self.style.cell.col_span = cols.max(1);
915        self
916    }
917    /// Horizontal placement in its slot — see [`HAlign`].
918    pub fn halign(mut self, v: HAlign) -> Self {
919        self.style.halign = Some(v);
920        self
921    }
922    /// Vertical placement in its slot — see [`VAlign`].
923    pub fn valign(mut self, v: VAlign) -> Self {
924        self.style.valign = Some(v);
925        self
926    }
927
928    /// Grid: its rows (no rows = one `*` row).
929    pub fn rows(mut self, rows: impl IntoIterator<Item = impl Into<Track>>) -> Self {
930        if let WidgetKind::Grid(g) = &mut self.kind {
931            g.rows = rows.into_iter().map(Into::into).collect();
932        }
933        self
934    }
935    /// Grid: its columns (no columns = one `*` column).
936    pub fn cols(mut self, cols: impl IntoIterator<Item = impl Into<Track>>) -> Self {
937        if let WidgetKind::Grid(g) = &mut self.kind {
938            g.cols = cols.into_iter().map(Into::into).collect();
939        }
940        self
941    }
942    /// Grid: space between rows.
943    pub fn row_spacing(mut self, v: f32) -> Self {
944        if let WidgetKind::Grid(g) = &mut self.kind {
945            g.row_spacing = v;
946        }
947        self
948    }
949    /// Grid: space between columns.
950    pub fn col_spacing(mut self, v: f32) -> Self {
951        if let WidgetKind::Grid(g) = &mut self.kind {
952            g.col_spacing = v;
953        }
954        self
955    }
956}
957
958// ── Widget constructors ───────────────────────────────────────────────────────
959
960pub fn panel(dir: FlexDir) -> Widget {
961    Widget::new(WidgetKind::Panel(dir))
962}
963/// WPF's `Grid`: a container of rows and columns — give it [`Widget::rows`]
964/// and [`Widget::cols`] ([`Track`]s: pixels, `Auto` or `*` shares) and put
965/// each child in a cell with [`Widget::cell`] / [`Widget::span`]. A child
966/// fills its cell unless it has [`Widget::halign`] / [`Widget::valign`] (or
967/// an explicit size); [`Widget::margin`] insets it.
968///
969/// ```
970/// use yog_ui::widget::{self, GridLength, Track};
971/// let ui = widget::grid()
972///     .rows([Track::auto(), Track::star(1.0), Track::px(20.0)])
973///     .cols([GridLength::Px(80.0), GridLength::STAR])
974///     .child(widget::label("Title").cell(0, 0).span(1, 2))
975///     .child(widget::scroll_view().cell(1, 0))
976///     .child(widget::label("Details").cell(1, 1))
977///     .child(widget::button("OK").cell(2, 1));
978/// ```
979pub fn grid() -> Widget {
980    Widget::new(WidgetKind::Grid(Box::default()))
981}
982/// A vertically scrolling column: clips its children, scrolls them by
983/// [`Widget::scroll_y`] and draws a scrollbar when they overflow — WPF's
984/// `ScrollViewer` around a `StackPanel`. Give it a fixed (or flex) height.
985pub fn scroll_view() -> Widget {
986    panel(FlexDir::Column).clip(true).scrollbar(true)
987}
988pub fn label(text: impl Into<String>) -> Widget {
989    Widget::new(WidgetKind::Label(text.into()))
990}
991pub fn button(text: impl Into<String>) -> Widget {
992    Widget::new(WidgetKind::Button(text.into()))
993}
994pub fn item_slot(item_id: impl Into<String>) -> Widget {
995    Widget::new(WidgetKind::ItemSlot(item_id.into()))
996}
997/// An inventory-slot decoration (icon + stack-count overlay) for `item_id`
998/// (empty string = empty slot) with `count` items. Purely decorative — real
999/// drag-and-drop still goes through the vanilla `Slot` this overlays.
1000pub fn inv_slot(item_id: impl Into<String>, count: u32) -> Widget {
1001    Widget::new(WidgetKind::InvSlot {
1002        item_id: item_id.into(),
1003        count,
1004    })
1005    .w(SLOT_SIZE)
1006    .h(SLOT_SIZE)
1007}
1008/// The ids of inventory widgets: a [`slot`]'s `<SLOT_PREFIX><index>` (and
1009/// `=<accepts>`, `|`-joined, for a [`slot_accepting`]), the
1010/// [`player_inventory`]'s, and a [`machine`] marker's
1011/// `<MACHINE_PREFIX><kind>;<result slot>;` then its places, `,`-joined
1012/// `<kind's slot>/<index>=<inventory slot>`.
1013pub use yog_ids::yog::{MACHINE_PREFIX, PLAYER_INVENTORY, SLOT_PREFIX};
1014
1015/// Marks an inventory screen as a machine making recipes of `kind`: its
1016/// `places` (`(kind's slot, index, inventory slot)`) and its `result` slot
1017/// — what a `.yui` `<RecipeArea>` becomes. Draws nothing.
1018pub fn machine(kind: &str, places: &[(&str, usize, usize)], result: usize) -> Widget {
1019    let places: Vec<String> = places.iter().map(|(s, i, inv)| format!("{s}/{i}={inv}")).collect();
1020    spacer().w(0.0).h(0.0).id(format!("{MACHINE_PREFIX}{kind};{result};{}", places.join(",")))
1021}
1022/// Gap between the player's main inventory and their hotbar (vanilla).
1023pub const HOTBAR_GAP: f32 = 4.0;
1024
1025/// Slot `index` of an inventory screen's container: the frame its real
1026/// slot sits in — the game draws the item, the hover and the drag over it.
1027/// Where it lands in the layout is where the slot is.
1028pub fn slot(index: usize) -> Widget {
1029    inv_slot("", 0).id(format!("{SLOT_PREFIX}{index}"))
1030}
1031/// A [`slot`] that only takes `accepts`: item ids (`minecraft:redstone`)
1032/// and item tags (`#c:dusts`).
1033pub fn slot_accepting(index: usize, accepts: &[&str]) -> Widget {
1034    if accepts.is_empty() {
1035        return slot(index);
1036    }
1037    inv_slot("", 0).id(format!("{SLOT_PREFIX}{index}={}", accepts.join("|")))
1038}
1039/// The player's inventory on an inventory screen: 3 rows of 9, then the
1040/// hotbar [`HOTBAR_GAP`] below — frames for the real slots, like [`slot`].
1041pub fn player_inventory() -> Widget {
1042    let row = || (0..9).fold(panel(FlexDir::Row).gap(0.0), |r, _| r.child(inv_slot("", 0)));
1043    panel(FlexDir::Column)
1044        .id(PLAYER_INVENTORY)
1045        .gap(0.0)
1046        .child(row())
1047        .child(row())
1048        .child(row())
1049        .child(row().margin(HOTBAR_GAP, 0.0, 0.0, 0.0))
1050}
1051/// An image from a Yog image source — `"item:apple"`, `"block:stone"`,
1052/// `"sprite:button"`, `"mymod:gui/logo"` (see [`yog_gfx::image`]) — the
1053/// host resolves it for its Minecraft version. Sized to the image's natural
1054/// size (16×16 until the host told it) unless given `w`/`h`; scaled into the
1055/// widget per [`Widget::stretch`] (default `Uniform`), nearest-neighbour.
1056pub fn image(source: impl Into<String>) -> Widget {
1057    Widget::new(WidgetKind::Image(Image {
1058        source: source.into(),
1059        stretch: Stretch::Uniform,
1060        region: None,
1061        tint: Color::WHITE,
1062    }))
1063}
1064pub fn spacer() -> Widget {
1065    Widget::new(WidgetKind::Spacer)
1066}
1067/// A box drawn by the code — the [`Surface`](crate::Surface) registered
1068/// under `id` with [`crate::surface::register`]. No natural size: give it
1069/// `w`/`h` (or let a parent stretch it).
1070pub fn surface(id: impl Into<String>) -> Widget {
1071    Widget::new(WidgetKind::Surface(id.into()))
1072}
1073/// A single-line text input holding `text` — black box, grey border (white
1074/// while focused), like vanilla's `EditBox`. Its text, caret and focus live
1075/// in a [`TextBoxState`](crate::TextBoxState) that the code-behind keeps and
1076/// applies to it each frame.
1077pub fn text_box(text: impl Into<String>) -> Widget {
1078    Widget::new(WidgetKind::TextBox(TextBox {
1079        text: text.into(),
1080        ..TextBox::default()
1081    }))
1082    .bg(Color::BLACK)
1083    .color(Color(0xFF_E0E0E0))
1084    .padding(0.0, TEXT_BOX_PAD_X, 0.0, TEXT_BOX_PAD_X)
1085}