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 /// A size as `.yui` writes it: `Auto` (any case), `*`, `2*`, `26`.
205 pub fn parse(text: &str) -> Option<GridLength> {
206 let text = text.trim();
207 if text.eq_ignore_ascii_case(AUTO) {
208 return Some(GridLength::Auto);
209 }
210 if let Some(weight) = text.strip_suffix(STAR) {
211 let weight = weight.trim();
212 return if weight.is_empty() {
213 Some(GridLength::STAR)
214 } else {
215 weight.parse().ok().map(GridLength::Star)
216 };
217 }
218 text.parse().ok().map(GridLength::Px)
219 }
220}
221
222/// How `.yui` writes [`GridLength::Auto`] and a share ([`GridLength::Star`]).
223const AUTO: &str = "auto";
224const STAR: char = '*';
225
226/// One row or column of a [`GridSpec`]: its size and the limits on it.
227#[derive(Debug, Clone, Copy, PartialEq)]
228pub struct Track {
229 pub size: GridLength,
230 /// Never smaller than this (WPF's `MinHeight`/`MinWidth`).
231 pub min: f32,
232 /// Never larger than this (`f32::INFINITY` = no limit).
233 pub max: f32,
234}
235
236impl Track {
237 pub const fn new(size: GridLength) -> Self {
238 Self {
239 size,
240 min: 0.0,
241 max: f32::INFINITY,
242 }
243 }
244 /// `Auto`.
245 pub const fn auto() -> Self {
246 Self::new(GridLength::Auto)
247 }
248 /// A fixed `px`.
249 pub const fn px(px: f32) -> Self {
250 Self::new(GridLength::Px(px))
251 }
252 /// `weight*`.
253 pub const fn star(weight: f32) -> Self {
254 Self::new(GridLength::Star(weight))
255 }
256 pub const fn min(mut self, v: f32) -> Self {
257 self.min = v;
258 self
259 }
260 pub const fn max(mut self, v: f32) -> Self {
261 self.max = v;
262 self
263 }
264}
265
266impl From<GridLength> for Track {
267 fn from(size: GridLength) -> Self {
268 Self::new(size)
269 }
270}
271
272/// A grid's rows, columns and the space between them. No rows (columns)
273/// = one `*` row (column), like WPF.
274#[derive(Debug, Clone, Default, PartialEq)]
275pub struct GridSpec {
276 pub rows: Vec<Track>,
277 pub cols: Vec<Track>,
278 /// Space between rows (WinUI's `RowSpacing`).
279 pub row_spacing: f32,
280 /// Space between columns (WinUI's `ColumnSpacing`).
281 pub col_spacing: f32,
282}
283
284/// The grid cell(s) a widget occupies — WPF's `Grid.Row`, `Grid.Column`,
285/// `Grid.RowSpan`, `Grid.ColumnSpan`. Only a [`grid`] reads it; out-of-range
286/// values are clamped to the grid (like WPF).
287#[derive(Debug, Clone, Copy, PartialEq, Eq)]
288pub struct Cell {
289 pub row: u16,
290 pub col: u16,
291 /// Rows spanned, at least 1.
292 pub row_span: u16,
293 /// Columns spanned, at least 1.
294 pub col_span: u16,
295}
296
297impl Default for Cell {
298 fn default() -> Self {
299 Self {
300 row: 0,
301 col: 0,
302 row_span: 1,
303 col_span: 1,
304 }
305 }
306}
307
308/// Where a widget sits horizontally in the space its parent gives it — WPF's
309/// `HorizontalAlignment`. In a grid cell `None` means `Stretch`; in a
310/// Column it overrides the parent's `align` for this child.
311#[derive(Debug, Clone, Copy, PartialEq, Eq)]
312pub enum HAlign {
313 Left,
314 Center,
315 Right,
316 /// Fill the width (unless the widget has an explicit `w`).
317 Stretch,
318}
319
320/// Where a widget sits vertically — WPF's `VerticalAlignment`; see [`HAlign`].
321#[derive(Debug, Clone, Copy, PartialEq, Eq)]
322pub enum VAlign {
323 Top,
324 Center,
325 Bottom,
326 /// Fill the height (unless the widget has an explicit `h`).
327 Stretch,
328}
329
330/// An image widget's data — see [`image`].
331#[derive(Debug, Clone, PartialEq)]
332pub struct Image {
333 /// A Yog image source (see [`yog_gfx::image`]).
334 pub source: String,
335 /// How the image fills the widget (default [`Stretch::Uniform`]).
336 pub stretch: Stretch,
337 /// The part of the image drawn, `[u, v, w, h]` in the image's pixels
338 /// (`None` = all of it). Textures only — items and sprites are whole.
339 pub region: Option<Rect>,
340 /// Color multiplier, `0xAARRGGBB` (`0xFFFFFFFF` = none; items ignore it).
341 pub tint: Color,
342}
343
344pub use yog_gfx::image::Stretch;
345
346/// A text box's content and editing state, as drawn this frame. Built by
347/// [`text_box`] (text, placeholder, max length, event names); the caret,
348/// selection and horizontal scroll are filled in each frame by
349/// [`TextBoxState::apply`](crate::TextBoxState::apply).
350#[derive(Debug, Clone, Default, PartialEq)]
351pub struct TextBox {
352 pub text: String,
353 /// Shown greyed while the text is empty.
354 pub placeholder: String,
355 /// Longest text allowed, in characters; 0 = unlimited.
356 pub max_length: usize,
357 /// Event name for text changes (see `TextBoxState::event_name`).
358 pub on_change: Option<String>,
359 /// Event name for Enter (see `TextBoxState::event_name`).
360 pub on_submit: Option<String>,
361 /// Caret position in characters — `Some` while the box has focus.
362 pub caret: Option<usize>,
363 /// Draw the caret this frame (it blinks).
364 pub caret_visible: bool,
365 /// Selected characters `start..end` (empty = none).
366 pub selection: (usize, usize),
367 /// First visible character (horizontal scroll).
368 pub first: usize,
369}
370
371/// Side of an item / inventory slot box in GUI pixels (vanilla's 18).
372pub const SLOT_SIZE: f32 = 18.0;
373/// A `<Screen>`'s size when it doesn't give one: the game's container screens' (176 × 166).
374pub const SCREEN_W: f32 = 176.0;
375pub const SCREEN_H: f32 = 166.0;
376/// Focus indicator color when `focus_color` is 0 (amber).
377pub const DEFAULT_FOCUS_COLOR: Color = Color(0xFF_FFE040);
378
379/// A text box's default left/right padding.
380pub(crate) const TEXT_BOX_PAD_X: f32 = 4.0;
381/// Placeholder text color of a text box.
382pub const PLACEHOLDER_COLOR: Color = Color(0xFF_707070);
383/// Border of an unfocused text box (vanilla `EditBox`).
384pub const TEXT_BOX_BORDER: Color = Color(0xFF_A0A0A0);
385/// A text box's text, as vanilla's `EditBox` draws it.
386pub const TEXT_BOX_TEXT: Color = Color(0xFF_E0E0E0);
387/// Border of a focused text box unless `focus_color` is set.
388pub const TEXT_BOX_FOCUS_BORDER: Color = Color(0xFF_FFFFFF);
389/// Selection highlight behind selected text.
390pub const SELECTION_COLOR: Color = Color(0xFF_2850A0);
391
392/// WinForms-style dock — which edge(s) the widget attaches to inside its parent.
393///
394/// - `None` — normal flex positioning (default)
395/// - `Fill` — stretch to fill all remaining space (both axes)
396/// - `Left` — full height, natural width, hugs left edge; other children flow right
397/// - `Right` — full height, natural width, hugs right edge
398/// - `Top` — full width, natural height, hugs top edge; other children flow down
399/// - `Bottom` — full width, natural height, hugs bottom edge
400#[derive(Debug, Clone, Copy, PartialEq, Default)]
401pub enum Dock {
402 #[default]
403 None,
404 Fill,
405 Left,
406 Right,
407 Top,
408 Bottom,
409}
410
411/// How a focused widget shows its focus indicator.
412#[derive(Debug, Clone, Copy, PartialEq, Default)]
413pub enum FocusStyle {
414 /// 1px outline using `focus_color` (default, amber).
415 #[default]
416 Outline,
417 /// Solid background fill using `focus_color`.
418 Fill,
419 /// No visual indicator.
420 None,
421}
422
423/// A size value — either a fixed pixel amount, a CSS-style percentage of the
424/// parent's own resolved content size (0.0–100.0), or `Auto` (size to
425/// content/available space, this crate's original behaviour).
426#[derive(Debug, Clone, Copy, PartialEq, Default)]
427pub enum Units {
428 #[default]
429 Auto,
430 Px(f32),
431 /// Percent of the parent's resolved content size along the same axis.
432 Percent(f32),
433}
434
435impl From<f32> for Units {
436 /// `0.0` maps to `Auto` (matches this crate's original `0.0 = auto` convention,
437 /// so every existing `.w(18.0)`/`.h(0.0)` call keeps meaning exactly what it did).
438 fn from(v: f32) -> Self {
439 if v <= 0.0 { Units::Auto } else { Units::Px(v) }
440 }
441}
442
443/// A whole, in [`Units::Percent`].
444const PERCENT: f32 = 100.0;
445/// A widget's least size when its style gives none.
446const MIN_SIZE: f32 = 4.0;
447/// Text's color when its style gives none.
448pub const DEFAULT_TEXT_COLOR: Color = Color(0xFF_CCCCAA);
449
450impl Units {
451 /// Resolve against `avail` (the parent's resolved content size on this
452 /// axis). `None` for `Auto` — caller falls back to its own auto-sizing.
453 pub fn resolve(self, avail: f32) -> Option<f32> {
454 match self {
455 Units::Auto => None,
456 Units::Px(v) => Some(v),
457 Units::Percent(p) => Some(avail * p / PERCENT),
458 }
459 }
460
461 pub fn is_auto(self) -> bool {
462 matches!(self, Units::Auto)
463 }
464}
465
466/// Visual and layout style for a widget.
467#[derive(Debug, Clone)]
468pub struct Style {
469 pub w: Units, // explicit width; Auto = size to content/available space
470 pub h: Units, // explicit height; Auto = size to content/available space
471 pub min_w: f32,
472 pub min_h: f32,
473 pub flex: f32, // grow factor inside flex container (main axis)
474 pub dock: Dock, // WinForms-style edge attachment
475 pub gap: f32, // spacing between children
476 pub pad: [f32; 4], // top, right, bottom, left — space INSIDE the border
477 pub margin: [f32; 4], // top, right, bottom, left — space OUTSIDE the border
478 /// The grid cell(s) it occupies inside a [`grid`] — see [`Cell`].
479 pub cell: Cell,
480 /// Horizontal placement in its slot (grid cell, or a Column's width) —
481 /// see [`HAlign`]. `None` = the parent's default.
482 pub halign: Option<HAlign>,
483 /// Vertical placement in its slot (grid cell, or a Row's height) — see
484 /// [`VAlign`]. `None` = the parent's default.
485 pub valign: Option<VAlign>,
486 pub bg: Color, // background color; transparent = none
487 /// Background image, drawn over `bg` and stretched to the box — a Yog
488 /// image source (`sprite:button`, `sprite:panel`, `mymod:gui/frame`);
489 /// sprites are nine-sliced, so frames keep their edges. Empty = none.
490 pub bg_image: String,
491 pub color: Color, // text color
492 pub align: Align,
493 pub font_scale: f32,
494 pub text_shadow: bool, // MC drop-shadow behind text (HUD default; books disable it)
495 pub focus_style: FocusStyle,
496 pub focus_color: Color, // transparent = DEFAULT_FOCUS_COLOR
497 /// Labels/buttons: never word-wrap, even if narrower than the text.
498 /// For single-line titles/headers that must stay on one line — matches
499 /// Patchouli, where such text overflows its box horizontally rather than
500 /// wrapping onto a second line.
501 pub no_wrap: bool,
502 /// Absolute position `(x, y)` inside the parent's content box (inside its
503 /// padding) — WPF `Canvas` / WinForms `Location` style. A positioned
504 /// widget is taken out of the parent's flex flow: it takes no main-axis
505 /// space, ignores `flex`/`dock`, and is drawn in tree order (later
506 /// siblings on top). `None` (default) = normal flex positioning.
507 pub pos: Option<(f32, f32)>,
508 /// Labels/buttons: a single line (implies `no_wrap`) cut with `...` when
509 /// wider than the box, instead of overflowing it.
510 pub ellipsis: bool,
511 /// Containers: children are drawn (and hit) only inside this widget's
512 /// rect — anything scrolled or overflowing past it is cut off.
513 pub clip: bool,
514 /// Containers: vertical scroll offset of the children in GUI pixels
515 /// (0 = top). Layout clamps it to the content — the effective value is
516 /// [`LayoutNode::scroll_y`](crate::LayoutNode::scroll_y).
517 pub scroll_y: f32,
518 /// Containers: reserve a strip on the right edge and draw a vertical
519 /// scrollbar there whenever the content overflows (see
520 /// [`ScrollState`](crate::ScrollState) for driving it).
521 pub scrollbar: bool,
522 /// Font id for text — empty (default) = the game's default font
523 /// (`yog:font/default`); a Yog font such as `mymod:fancy`, or another
524 /// game font by Yog's alias: `yog:font/uniform`, `yog:font/alt`,
525 /// `yog:font/illageralt`.
526 /// See [`crate::text`] for how non-default fonts are measured.
527 pub font: String,
528}
529
530impl Default for Style {
531 fn default() -> Self {
532 Self {
533 w: Units::Auto,
534 h: Units::Auto,
535 min_w: MIN_SIZE,
536 min_h: MIN_SIZE,
537 flex: 0.0,
538 dock: Dock::None,
539 gap: 2.0,
540 pad: [0.0; 4],
541 margin: [0.0; 4],
542 cell: Cell::default(),
543 halign: None,
544 valign: None,
545 bg: Color::TRANSPARENT,
546 bg_image: String::new(),
547 color: DEFAULT_TEXT_COLOR,
548 align: Align::Start,
549 font_scale: 1.0,
550 text_shadow: true,
551 focus_style: FocusStyle::default(),
552 focus_color: Color::TRANSPARENT,
553 no_wrap: false,
554 pos: None,
555 ellipsis: false,
556 clip: false,
557 scroll_y: 0.0,
558 scrollbar: false,
559 font: String::new(),
560 }
561 }
562}