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