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