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