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