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}