Skip to main content

yog_ui/
widget.rs

1//! Widget types and styling.
2
3use yog_gfx::{Color, Rect};
4
5use crate::layout::{Align, FlexDir};
6
7mod builder;
8mod make;
9
10/// The ids of inventory widgets ([`crate::inventory_ids`]).
11pub use crate::inventory_ids::{MACHINE_PREFIX, PLAYER_INVENTORY, SLOT_PREFIX};
12
13pub use make::{
14    HOTBAR_GAP, button, grid, image, inv_slot, item_slot, label, machine, panel, player_inventory,
15    scroll_view, slot, slot_accepting, spacer, surface, text_box,
16};
17
18/// A single widget in the UI tree.
19#[derive(Debug, Clone)]
20pub struct Widget {
21    pub kind: WidgetKind,
22    pub id: Option<String>,
23    pub style: Style,
24    pub flex_dir: FlexDir,
25    pub children: Vec<Widget>,
26    pub on_click: Option<String>,
27    pub enabled: bool,
28    pub focused: bool,
29    /// Typed click target set by generated code (yog-kit) — like
30    /// [`Self::on_click`], but no string to parse. See [`Action`].
31    pub action: Option<Action>,
32    /// `false` = collapsed: takes no space, isn't drawn and can't be hit
33    /// (its layout node is a zero rect without children).
34    pub visible: bool,
35    /// Interaction state (hover, pressed, selected, ...) the host sets each
36    /// frame — picks the [`Self::states`] patches applied when drawing.
37    pub state: StateFlags,
38    /// Paint-only style overrides per state (`hover:bg="..."` in `.yui`).
39    pub states: Option<Box<StateStyles>>,
40    /// Text shown in a tooltip while the cursor rests on it (`\n` breaks
41    /// lines) — see [`crate::UiRoot::tooltip_at`].
42    pub tooltip: Option<String>,
43}
44
45/// A typed click target: `owner` 0 = the screen, `k` = items control `k`;
46/// `slot` = the command (or [`Action::SELECT`]); `index` = the item index
47/// inside an items control (0 elsewhere).
48#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
49pub struct Action {
50    pub owner: u16,
51    pub slot: u16,
52    pub index: u32,
53}
54
55impl Action {
56    /// The `slot` of an items control's item container: clicking selects it.
57    pub const SELECT: u16 = u16::MAX;
58
59    /// A screen-level command `slot`.
60    pub const fn new(slot: u16) -> Self {
61        Self {
62            owner: 0,
63            slot,
64            index: 0,
65        }
66    }
67}
68
69/// Interaction state bits of a widget — see [`Widget::state`].
70#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
71pub struct StateFlags(pub u8);
72
73impl StateFlags {
74    pub const NONE: Self = Self(0);
75    pub const HOVER: Self = Self(1);
76    pub const PRESSED: Self = Self(2);
77    pub const FOCUSED: Self = Self(4);
78    pub const SELECTED: Self = Self(8);
79    pub const CHECKED: Self = Self(16);
80    pub const DISABLED: Self = Self(32);
81
82    /// Every bit of `o` is set.
83    pub const fn contains(self, o: Self) -> bool {
84        self.0 & o.0 == o.0
85    }
86    pub fn insert(&mut self, o: Self) {
87        self.0 |= o.0;
88    }
89    pub fn remove(&mut self, o: Self) {
90        self.0 &= !o.0;
91    }
92    /// Sets or clears the bits of `o`.
93    pub fn set(&mut self, o: Self, on: bool) {
94        if on { self.insert(o) } else { self.remove(o) }
95    }
96    pub const fn union(self, o: Self) -> Self {
97        Self(self.0 | o.0)
98    }
99    pub const fn is_empty(self) -> bool {
100        self.0 == 0
101    }
102}
103
104impl std::ops::BitOr for StateFlags {
105    type Output = Self;
106    fn bitor(self, o: Self) -> Self {
107        self.union(o)
108    }
109}
110
111/// Paint-only style overrides — never affect layout.
112#[derive(Debug, Clone, Default, PartialEq)]
113pub struct StylePatch {
114    pub bg: Option<Color>,
115    pub bg_image: Option<String>,
116    pub color: Option<Color>,
117    pub shadow: Option<bool>,
118    pub focus_color: Option<Color>,
119}
120
121impl StylePatch {
122    /// `true` when it overrides nothing.
123    pub fn is_empty(&self) -> bool {
124        *self == Self::default()
125    }
126
127    fn apply(&self, s: &mut Style) {
128        if let Some(v) = self.bg {
129            s.bg = v;
130        }
131        if let Some(v) = &self.bg_image {
132            s.bg_image.clone_from(v);
133        }
134        if let Some(v) = self.color {
135            s.color = v;
136        }
137        if let Some(v) = self.shadow {
138            s.text_shadow = v;
139        }
140        if let Some(v) = self.focus_color {
141            s.focus_color = v;
142        }
143    }
144}
145
146/// A widget's state patches: each applies when the widget's state contains
147/// its flags; later rules win.
148#[derive(Debug, Clone, Default, PartialEq)]
149pub struct StateStyles {
150    pub rules: Vec<(StateFlags, StylePatch)>,
151}
152
153#[derive(Debug, Clone)]
154pub enum WidgetKind {
155    /// Container — arranges children according to `flex_dir`.
156    Panel(FlexDir),
157    /// Container — WPF's `Grid`: rows and columns sized in pixels, to
158    /// content (`Auto`) or as shares of the rest (`*`); each child sits in
159    /// the cell(s) its [`Style::cell`] names — see [`grid`] and
160    /// [`GridSpec`].
161    Grid(Box<GridSpec>),
162    /// Static or dynamic text label.
163    Label(String),
164    /// Clickable button with text.
165    Button(String),
166    /// Minecraft item icon (static, known at build time).
167    ItemSlot(String),
168    /// Inventory-slot decoration: an item icon plus a stack-count overlay,
169    /// both supplied as plain data at tree-build time (like every other
170    /// widget here) — this crate has no notion of "the currently open menu"
171    /// or how to query it; that's the caller's job (see `yog-inventory`).
172    /// Empty `item_id` draws an empty slot box.
173    InvSlot { item_id: String, count: u32 },
174    /// An image from a Yog image source (`item:apple`, `sprite:button`,
175    /// `mymod:gui/logo`, ...) — see [`Image`] and [`image`].
176    Image(Image),
177    /// Invisible spacer.
178    Spacer,
179    /// Drawn by the code: the [`Surface`](crate::Surface) registered under
180    /// this id (see [`crate::surface`] and [`surface`]).
181    Surface(String),
182    /// Single-line text input — see [`TextBox`] and
183    /// [`TextBoxState`](crate::TextBoxState), which owns the text and drives it.
184    TextBox(TextBox),
185}
186
187/// How a grid row or column is sized — WPF's `GridLength`.
188#[derive(Debug, Clone, Copy, PartialEq)]
189pub enum GridLength {
190    /// Fits its content: the largest child in it (`Auto`).
191    Auto,
192    /// A fixed size in GUI pixels (`26`).
193    Px(f32),
194    /// A share of the space the other rows/columns leave (`*` = `Star(1.0)`,
195    /// `2*` = `Star(2.0)`). Along an axis being measured for content (an
196    /// auto-sized grid in a scroll view, ...) it acts as [`Self::Auto`].
197    Star(f32),
198}
199
200impl GridLength {
201    /// `*`.
202    pub const STAR: Self = Self::Star(1.0);
203}
204
205/// One row or column of a [`GridSpec`]: its size and the limits on it.
206#[derive(Debug, Clone, Copy, PartialEq)]
207pub struct Track {
208    pub size: GridLength,
209    /// Never smaller than this (WPF's `MinHeight`/`MinWidth`).
210    pub min: f32,
211    /// Never larger than this (`f32::INFINITY` = no limit).
212    pub max: f32,
213}
214
215impl Track {
216    pub const fn new(size: GridLength) -> Self {
217        Self {
218            size,
219            min: 0.0,
220            max: f32::INFINITY,
221        }
222    }
223    /// `Auto`.
224    pub const fn auto() -> Self {
225        Self::new(GridLength::Auto)
226    }
227    /// A fixed `px`.
228    pub const fn px(px: f32) -> Self {
229        Self::new(GridLength::Px(px))
230    }
231    /// `weight*`.
232    pub const fn star(weight: f32) -> Self {
233        Self::new(GridLength::Star(weight))
234    }
235    pub const fn min(mut self, v: f32) -> Self {
236        self.min = v;
237        self
238    }
239    pub const fn max(mut self, v: f32) -> Self {
240        self.max = v;
241        self
242    }
243}
244
245impl From<GridLength> for Track {
246    fn from(size: GridLength) -> Self {
247        Self::new(size)
248    }
249}
250
251/// A grid's rows, columns and the space between them. No rows (columns)
252/// = one `*` row (column), like WPF.
253#[derive(Debug, Clone, Default, PartialEq)]
254pub struct GridSpec {
255    pub rows: Vec<Track>,
256    pub cols: Vec<Track>,
257    /// Space between rows (WinUI's `RowSpacing`).
258    pub row_spacing: f32,
259    /// Space between columns (WinUI's `ColumnSpacing`).
260    pub col_spacing: f32,
261}
262
263/// The grid cell(s) a widget occupies — WPF's `Grid.Row`, `Grid.Column`,
264/// `Grid.RowSpan`, `Grid.ColumnSpan`. Only a [`grid`] reads it; out-of-range
265/// values are clamped to the grid (like WPF).
266#[derive(Debug, Clone, Copy, PartialEq, Eq)]
267pub struct Cell {
268    pub row: u16,
269    pub col: u16,
270    /// Rows spanned, at least 1.
271    pub row_span: u16,
272    /// Columns spanned, at least 1.
273    pub col_span: u16,
274}
275
276impl Default for Cell {
277    fn default() -> Self {
278        Self {
279            row: 0,
280            col: 0,
281            row_span: 1,
282            col_span: 1,
283        }
284    }
285}
286
287/// Where a widget sits horizontally in the space its parent gives it — WPF's
288/// `HorizontalAlignment`. In a grid cell `None` means `Stretch`; in a
289/// Column it overrides the parent's `align` for this child.
290#[derive(Debug, Clone, Copy, PartialEq, Eq)]
291pub enum HAlign {
292    Left,
293    Center,
294    Right,
295    /// Fill the width (unless the widget has an explicit `w`).
296    Stretch,
297}
298
299/// Where a widget sits vertically — WPF's `VerticalAlignment`; see [`HAlign`].
300#[derive(Debug, Clone, Copy, PartialEq, Eq)]
301pub enum VAlign {
302    Top,
303    Center,
304    Bottom,
305    /// Fill the height (unless the widget has an explicit `h`).
306    Stretch,
307}
308
309/// An image widget's data — see [`image`].
310#[derive(Debug, Clone, PartialEq)]
311pub struct Image {
312    /// A Yog image source (see [`yog_gfx::image`]).
313    pub source: String,
314    /// How the image fills the widget (default [`Stretch::Uniform`]).
315    pub stretch: Stretch,
316    /// The part of the image drawn, `[u, v, w, h]` in the image's pixels
317    /// (`None` = all of it). Textures only — items and sprites are whole.
318    pub region: Option<Rect>,
319    /// Color multiplier, `0xAARRGGBB` (`0xFFFFFFFF` = none; items ignore it).
320    pub tint: Color,
321}
322
323pub use yog_gfx::image::Stretch;
324
325/// A text box's content and editing state, as drawn this frame. Built by
326/// [`text_box`] (text, placeholder, max length, event names); the caret,
327/// selection and horizontal scroll are filled in each frame by
328/// [`TextBoxState::apply`](crate::TextBoxState::apply).
329#[derive(Debug, Clone, Default, PartialEq)]
330pub struct TextBox {
331    pub text: String,
332    /// Shown greyed while the text is empty.
333    pub placeholder: String,
334    /// Longest text allowed, in characters; 0 = unlimited.
335    pub max_length: usize,
336    /// Event name for text changes (see `TextBoxState::event_name`).
337    pub on_change: Option<String>,
338    /// Event name for Enter (see `TextBoxState::event_name`).
339    pub on_submit: Option<String>,
340    /// Caret position in characters — `Some` while the box has focus.
341    pub caret: Option<usize>,
342    /// Draw the caret this frame (it blinks).
343    pub caret_visible: bool,
344    /// Selected characters `start..end` (empty = none).
345    pub selection: (usize, usize),
346    /// First visible character (horizontal scroll).
347    pub first: usize,
348}
349
350/// Side of an item / inventory slot box in GUI pixels (vanilla's 18).
351pub const SLOT_SIZE: f32 = 18.0;
352/// A `<Screen>`'s size when it doesn't give one: the game's container screens' (176 × 166).
353pub const SCREEN_W: f32 = 176.0;
354pub const SCREEN_H: f32 = 166.0;
355/// Focus indicator color when `focus_color` is 0 (amber).
356pub const DEFAULT_FOCUS_COLOR: Color = Color(0xFF_FFE040);
357
358/// A text box's default left/right padding.
359pub(crate) const TEXT_BOX_PAD_X: f32 = 4.0;
360/// Placeholder text color of a text box.
361pub const PLACEHOLDER_COLOR: Color = Color(0xFF_707070);
362/// Border of an unfocused text box (vanilla `EditBox`).
363pub const TEXT_BOX_BORDER: Color = Color(0xFF_A0A0A0);
364/// A text box's text, as vanilla's `EditBox` draws it.
365pub const TEXT_BOX_TEXT: Color = Color(0xFF_E0E0E0);
366/// Border of a focused text box unless `focus_color` is set.
367pub const TEXT_BOX_FOCUS_BORDER: Color = Color(0xFF_FFFFFF);
368/// Selection highlight behind selected text.
369pub const SELECTION_COLOR: Color = Color(0xFF_2850A0);
370
371/// WinForms-style dock — which edge(s) the widget attaches to inside its parent.
372///
373/// - `None`   — normal flex positioning (default)
374/// - `Fill`   — stretch to fill all remaining space (both axes)
375/// - `Left`   — full height, natural width, hugs left edge; other children flow right
376/// - `Right`  — full height, natural width, hugs right edge
377/// - `Top`    — full width, natural height, hugs top edge; other children flow down
378/// - `Bottom` — full width, natural height, hugs bottom edge
379#[derive(Debug, Clone, Copy, PartialEq, Default)]
380pub enum Dock {
381    #[default]
382    None,
383    Fill,
384    Left,
385    Right,
386    Top,
387    Bottom,
388}
389
390/// How a focused widget shows its focus indicator.
391#[derive(Debug, Clone, Copy, PartialEq, Default)]
392pub enum FocusStyle {
393    /// 1px outline using `focus_color` (default, amber).
394    #[default]
395    Outline,
396    /// Solid background fill using `focus_color`.
397    Fill,
398    /// No visual indicator.
399    None,
400}
401
402/// A size value — either a fixed pixel amount, a CSS-style percentage of the
403/// parent's own resolved content size (0.0–100.0), or `Auto` (size to
404/// content/available space, this crate's original behaviour).
405#[derive(Debug, Clone, Copy, PartialEq, Default)]
406pub enum Units {
407    #[default]
408    Auto,
409    Px(f32),
410    /// Percent of the parent's resolved content size along the same axis.
411    Percent(f32),
412}
413
414impl From<f32> for Units {
415    /// `0.0` maps to `Auto` (matches this crate's original `0.0 = auto` convention,
416    /// so every existing `.w(18.0)`/`.h(0.0)` call keeps meaning exactly what it did).
417    fn from(v: f32) -> Self {
418        if v <= 0.0 { Units::Auto } else { Units::Px(v) }
419    }
420}
421
422/// A whole, in [`Units::Percent`].
423const PERCENT: f32 = 100.0;
424/// A widget's least size when its style gives none.
425const MIN_SIZE: f32 = 4.0;
426/// Text's color when its style gives none.
427pub const DEFAULT_TEXT_COLOR: Color = Color(0xFF_CCCCAA);
428
429impl Units {
430    /// Resolve against `avail` (the parent's resolved content size on this
431    /// axis). `None` for `Auto` — caller falls back to its own auto-sizing.
432    pub fn resolve(self, avail: f32) -> Option<f32> {
433        match self {
434            Units::Auto => None,
435            Units::Px(v) => Some(v),
436            Units::Percent(p) => Some(avail * p / PERCENT),
437        }
438    }
439
440    pub fn is_auto(self) -> bool {
441        matches!(self, Units::Auto)
442    }
443}
444
445/// Visual and layout style for a widget.
446#[derive(Debug, Clone)]
447pub struct Style {
448    pub w: Units, // explicit width; Auto = size to content/available space
449    pub h: Units, // explicit height; Auto = size to content/available space
450    pub min_w: f32,
451    pub min_h: f32,
452    pub flex: f32,        // grow factor inside flex container (main axis)
453    pub dock: Dock,       // WinForms-style edge attachment
454    pub gap: f32,         // spacing between children
455    pub pad: [f32; 4],    // top, right, bottom, left — space INSIDE the border
456    pub margin: [f32; 4], // top, right, bottom, left — space OUTSIDE the border
457    /// The grid cell(s) it occupies inside a [`grid`] — see [`Cell`].
458    pub cell: Cell,
459    /// Horizontal placement in its slot (grid cell, or a Column's width) —
460    /// see [`HAlign`]. `None` = the parent's default.
461    pub halign: Option<HAlign>,
462    /// Vertical placement in its slot (grid cell, or a Row's height) — see
463    /// [`VAlign`]. `None` = the parent's default.
464    pub valign: Option<VAlign>,
465    pub bg: Color, // background color; transparent = none
466    /// Background image, drawn over `bg` and stretched to the box — a Yog
467    /// image source (`sprite:button`, `sprite:panel`, `mymod:gui/frame`);
468    /// sprites are nine-sliced, so frames keep their edges. Empty = none.
469    pub bg_image: String,
470    pub color: Color, // text color
471    pub align: Align,
472    pub font_scale: f32,
473    pub text_shadow: bool, // MC drop-shadow behind text (HUD default; books disable it)
474    pub focus_style: FocusStyle,
475    pub focus_color: Color, // transparent = DEFAULT_FOCUS_COLOR
476    /// Labels/buttons: never word-wrap, even if narrower than the text.
477    /// For single-line titles/headers that must stay on one line — matches
478    /// Patchouli, where such text overflows its box horizontally rather than
479    /// wrapping onto a second line.
480    pub no_wrap: bool,
481    /// Absolute position `(x, y)` inside the parent's content box (inside its
482    /// padding) — WPF `Canvas` / WinForms `Location` style. A positioned
483    /// widget is taken out of the parent's flex flow: it takes no main-axis
484    /// space, ignores `flex`/`dock`, and is drawn in tree order (later
485    /// siblings on top). `None` (default) = normal flex positioning.
486    pub pos: Option<(f32, f32)>,
487    /// Labels/buttons: a single line (implies `no_wrap`) cut with `...` when
488    /// wider than the box, instead of overflowing it.
489    pub ellipsis: bool,
490    /// Containers: children are drawn (and hit) only inside this widget's
491    /// rect — anything scrolled or overflowing past it is cut off.
492    pub clip: bool,
493    /// Containers: vertical scroll offset of the children in GUI pixels
494    /// (0 = top). Layout clamps it to the content — the effective value is
495    /// [`LayoutNode::scroll_y`](crate::LayoutNode::scroll_y).
496    pub scroll_y: f32,
497    /// Containers: reserve a strip on the right edge and draw a vertical
498    /// scrollbar there whenever the content overflows (see
499    /// [`ScrollState`](crate::ScrollState) for driving it).
500    pub scrollbar: bool,
501    /// Font id for text — empty (default) = the game's default font
502    /// (`yog:font/default`); a Yog font such as `mymod:fancy`, or another
503    /// game font by Yog's alias: `yog:font/uniform`, `yog:font/alt`,
504    /// `yog:font/illageralt`.
505    /// See [`crate::text`] for how non-default fonts are measured.
506    pub font: String,
507}
508
509impl Default for Style {
510    fn default() -> Self {
511        Self {
512            w: Units::Auto,
513            h: Units::Auto,
514            min_w: MIN_SIZE,
515            min_h: MIN_SIZE,
516            flex: 0.0,
517            dock: Dock::None,
518            gap: 2.0,
519            pad: [0.0; 4],
520            margin: [0.0; 4],
521            cell: Cell::default(),
522            halign: None,
523            valign: None,
524            bg: Color::TRANSPARENT,
525            bg_image: String::new(),
526            color: DEFAULT_TEXT_COLOR,
527            align: Align::Start,
528            font_scale: 1.0,
529            text_shadow: true,
530            focus_style: FocusStyle::default(),
531            focus_color: Color::TRANSPARENT,
532            no_wrap: false,
533            pos: None,
534            ellipsis: false,
535            clip: false,
536            scroll_y: 0.0,
537            scrollbar: false,
538            font: String::new(),
539        }
540    }
541}