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