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 /// Static or dynamic text label.
142 Label(String),
143 /// Clickable button with text.
144 Button(String),
145 /// Minecraft item icon (static, known at build time).
146 ItemSlot(String),
147 /// Inventory-slot decoration: an item icon plus a stack-count overlay,
148 /// both supplied as plain data at tree-build time (like every other
149 /// widget here) — this crate has no notion of "the currently open menu"
150 /// or how to query it; that's the caller's job (see `yog-inventory`).
151 /// Empty `item_id` draws an empty slot box.
152 InvSlot { item_id: String, count: u32 },
153 /// An image from a Yog image source (`item:apple`, `sprite:button`,
154 /// `mymod:gui/logo`, ...) — see [`Image`] and [`image`].
155 Image(Image),
156 /// Invisible spacer.
157 Spacer,
158 /// Single-line text input — see [`TextBox`] and
159 /// [`TextBoxState`](crate::TextBoxState), which owns the text and drives it.
160 TextBox(TextBox),
161}
162
163/// An image widget's data — see [`image`].
164#[derive(Debug, Clone, PartialEq)]
165pub struct Image {
166 /// A Yog image source (see [`yog_gfx::image`]).
167 pub source: String,
168 /// How the image fills the widget (default [`Stretch::Uniform`]).
169 pub stretch: Stretch,
170 /// The part of the image drawn, `[u, v, w, h]` in the image's pixels
171 /// (`None` = all of it). Textures only — items and sprites are whole.
172 pub region: Option<[f32; 4]>,
173 /// Color multiplier, `0xAARRGGBB` (`0xFFFFFFFF` = none; items ignore it).
174 pub tint: u32,
175}
176
177pub use yog_gfx::image::Stretch;
178
179/// A text box's content and editing state, as drawn this frame. Built by
180/// [`text_box`] (text, placeholder, max length, event names); the caret,
181/// selection and horizontal scroll are filled in each frame by
182/// [`TextBoxState::apply`](crate::TextBoxState::apply).
183#[derive(Debug, Clone, Default, PartialEq)]
184pub struct TextBox {
185 pub text: String,
186 /// Shown greyed while the text is empty.
187 pub placeholder: String,
188 /// Longest text allowed, in characters; 0 = unlimited.
189 pub max_length: usize,
190 /// Event name for text changes (see `TextBoxState::event_name`).
191 pub on_change: Option<String>,
192 /// Event name for Enter (see `TextBoxState::event_name`).
193 pub on_submit: Option<String>,
194 /// Caret position in characters — `Some` while the box has focus.
195 pub caret: Option<usize>,
196 /// Draw the caret this frame (it blinks).
197 pub caret_visible: bool,
198 /// Selected characters `start..end` (empty = none).
199 pub selection: (usize, usize),
200 /// First visible character (horizontal scroll).
201 pub first: usize,
202}
203
204/// Placeholder text color of a text box.
205pub const PLACEHOLDER_COLOR: u32 = 0xFF_707070;
206/// Border of an unfocused text box (vanilla `EditBox`).
207pub const TEXT_BOX_BORDER: u32 = 0xFF_A0A0A0;
208/// Border of a focused text box unless `focus_color` is set.
209pub const TEXT_BOX_FOCUS_BORDER: u32 = 0xFF_FFFFFF;
210/// Selection highlight behind selected text.
211pub const SELECTION_COLOR: u32 = 0xFF_2850A0;
212
213/// WinForms-style dock — which edge(s) the widget attaches to inside its parent.
214///
215/// - `None` — normal flex positioning (default)
216/// - `Fill` — stretch to fill all remaining space (both axes)
217/// - `Left` — full height, natural width, hugs left edge; other children flow right
218/// - `Right` — full height, natural width, hugs right edge
219/// - `Top` — full width, natural height, hugs top edge; other children flow down
220/// - `Bottom` — full width, natural height, hugs bottom edge
221#[derive(Debug, Clone, Copy, PartialEq, Default)]
222pub enum Dock {
223 #[default]
224 None,
225 Fill,
226 Left,
227 Right,
228 Top,
229 Bottom,
230}
231
232/// How a focused widget shows its focus indicator.
233#[derive(Debug, Clone, Copy, PartialEq, Default)]
234pub enum FocusStyle {
235 /// 1px outline using `focus_color` (default, amber).
236 #[default]
237 Outline,
238 /// Solid background fill using `focus_color`.
239 Fill,
240 /// No visual indicator.
241 None,
242}
243
244/// A size value — either a fixed pixel amount, a CSS-style percentage of the
245/// parent's own resolved content size (0.0–100.0), or `Auto` (size to
246/// content/available space, this crate's original behaviour).
247#[derive(Debug, Clone, Copy, PartialEq, Default)]
248pub enum Units {
249 #[default]
250 Auto,
251 Px(f32),
252 /// Percent of the parent's resolved content size along the same axis.
253 Percent(f32),
254}
255
256impl From<f32> for Units {
257 /// `0.0` maps to `Auto` (matches this crate's original `0.0 = auto` convention,
258 /// so every existing `.w(18.0)`/`.h(0.0)` call keeps meaning exactly what it did).
259 fn from(v: f32) -> Self {
260 if v <= 0.0 {
261 Units::Auto
262 } else {
263 Units::Px(v)
264 }
265 }
266}
267
268impl Units {
269 /// Resolve against `avail` (the parent's resolved content size on this
270 /// axis). `None` for `Auto` — caller falls back to its own auto-sizing.
271 pub fn resolve(self, avail: f32) -> Option<f32> {
272 match self {
273 Units::Auto => None,
274 Units::Px(v) => Some(v),
275 Units::Percent(p) => Some(avail * p / 100.0),
276 }
277 }
278
279 pub fn is_auto(self) -> bool {
280 matches!(self, Units::Auto)
281 }
282}
283
284/// Visual and layout style for a widget.
285#[derive(Debug, Clone)]
286pub struct Style {
287 pub w: Units, // explicit width; Auto = size to content/available space
288 pub h: Units, // explicit height; Auto = size to content/available space
289 pub min_w: f32,
290 pub min_h: f32,
291 pub flex: f32, // grow factor inside flex container (main axis)
292 pub dock: Dock, // WinForms-style edge attachment
293 pub gap: f32, // spacing between children
294 pub pad: [f32; 4], // top, right, bottom, left — space INSIDE the border
295 pub margin: [f32; 4], // top, right, bottom, left — space OUTSIDE the border
296 pub bg: u32, // background colour 0xAARRGGBB; 0 = transparent
297 pub color: u32, // text colour
298 pub align: Align,
299 pub font_scale: f32,
300 pub text_shadow: bool, // MC drop-shadow behind text (HUD default; books disable it)
301 pub focus_style: FocusStyle,
302 pub focus_color: u32, // 0 = default amber 0xFF_FFE040
303 /// Labels/buttons: never word-wrap, even if narrower than the text.
304 /// For single-line titles/headers that must stay on one line — matches
305 /// Patchouli, where such text overflows its box horizontally rather than
306 /// wrapping onto a second line.
307 pub no_wrap: bool,
308 /// Absolute position `(x, y)` inside the parent's content box (inside its
309 /// padding) — WPF `Canvas` / WinForms `Location` style. A positioned
310 /// widget is taken out of the parent's flex flow: it takes no main-axis
311 /// space, ignores `flex`/`dock`, and is drawn in tree order (later
312 /// siblings on top). `None` (default) = normal flex positioning.
313 pub pos: Option<(f32, f32)>,
314 /// Labels/buttons: a single line (implies `no_wrap`) cut with `...` when
315 /// wider than the box, instead of overflowing it.
316 pub ellipsis: bool,
317 /// Containers: children are drawn (and hit) only inside this widget's
318 /// rect — anything scrolled or overflowing past it is cut off.
319 pub clip: bool,
320 /// Containers: vertical scroll offset of the children in GUI pixels
321 /// (0 = top). Layout clamps it to the content — the effective value is
322 /// [`LayoutNode::scroll_y`](crate::LayoutNode::scroll_y).
323 pub scroll_y: f32,
324 /// Containers: reserve a strip on the right edge and draw a vertical
325 /// scrollbar there whenever the content overflows (see
326 /// [`ScrollState`](crate::ScrollState) for driving it).
327 pub scrollbar: bool,
328 /// Font id for text — empty (default) = Minecraft's default font
329 /// (`minecraft:default`); also `minecraft:uniform`, `minecraft:alt`,
330 /// `minecraft:illageralt` or a resource-pack font such as `mymod:fancy`.
331 /// See [`crate::text`] for how non-default fonts are measured.
332 pub font: String,
333}
334
335impl Default for Style {
336 fn default() -> Self {
337 Self {
338 w: Units::Auto,
339 h: Units::Auto,
340 min_w: 4.0,
341 min_h: 4.0,
342 flex: 0.0,
343 dock: Dock::None,
344 gap: 2.0,
345 pad: [0.0; 4],
346 margin: [0.0; 4],
347 bg: 0,
348 color: 0xFF_CCCCAA,
349 align: Align::Start,
350 font_scale: 1.0,
351 text_shadow: true,
352 focus_style: FocusStyle::default(),
353 focus_color: 0,
354 no_wrap: false,
355 pos: None,
356 ellipsis: false,
357 clip: false,
358 scroll_y: 0.0,
359 scrollbar: false,
360 font: String::new(),
361 }
362 }
363}
364
365// ── Builder API ───────────────────────────────────────────────────────────────
366
367impl Widget {
368 fn new(kind: WidgetKind) -> Self {
369 let flex_dir = if let WidgetKind::Panel(dir) = &kind {
370 *dir
371 } else {
372 FlexDir::Column
373 };
374 Self {
375 kind,
376 id: None,
377 style: Style::default(),
378 flex_dir,
379 children: Vec::new(),
380 on_click: None,
381 enabled: true,
382 focused: false,
383 action: None,
384 visible: true,
385 state: StateFlags::NONE,
386 states: None,
387 }
388 }
389
390 /// Typed click target — see [`Action`].
391 pub fn action(mut self, a: Action) -> Self {
392 self.action = Some(a);
393 self
394 }
395 /// `false` = collapsed — see [`Widget::visible`].
396 pub fn visible(mut self, v: bool) -> Self {
397 self.visible = v;
398 self
399 }
400 /// Adds a paint-only override applied while the state contains `when`
401 /// (later calls win over earlier ones).
402 pub fn state_style(mut self, when: StateFlags, patch: StylePatch) -> Self {
403 self.states
404 .get_or_insert_with(Default::default)
405 .rules
406 .push((when, patch));
407 self
408 }
409
410 /// The descendant at child-index `path` (`&[]` = self); `None` past the
411 /// tree.
412 pub fn get_path(&self, path: &[u16]) -> Option<&Widget> {
413 let mut w = self;
414 for &i in path {
415 w = w.children.get(i as usize)?;
416 }
417 Some(w)
418 }
419 /// [`Self::get_path`], mutably.
420 pub fn get_path_mut(&mut self, path: &[u16]) -> Option<&mut Widget> {
421 let mut w = self;
422 for &i in path {
423 w = w.children.get_mut(i as usize)?;
424 }
425 Some(w)
426 }
427 /// The descendant at `path`. Panics on a bad path — for generated code,
428 /// whose paths are fixed when it's generated.
429 pub fn at_path(&self, path: &[u16]) -> &Widget {
430 self.get_path(path)
431 .unwrap_or_else(|| panic!("yog-ui: no widget at path {path:?}"))
432 }
433 /// [`Self::at_path`], mutably.
434 pub fn at_path_mut(&mut self, path: &[u16]) -> &mut Widget {
435 self.get_path_mut(path)
436 .unwrap_or_else(|| panic!("yog-ui: no widget at path {path:?}"))
437 }
438
439 /// Whether clicking it does something (an [`Action`] or `on_click`).
440 pub fn is_interactive(&self) -> bool {
441 self.action.is_some() || self.on_click.is_some()
442 }
443
444 /// Sets (or clears) `mask` on this widget and its descendants, stopping
445 /// at descendants that are interactive themselves (a button in a row
446 /// keeps its own hover) — so a row's labels follow the row's state.
447 pub fn set_state_inherited(&mut self, mask: StateFlags, on: bool) {
448 self.state.set(mask, on);
449 for c in &mut self.children {
450 if !c.is_interactive() {
451 c.set_state_inherited(mask, on);
452 }
453 }
454 }
455
456 /// Clears `mask` on the whole subtree (interactive descendants too).
457 pub fn clear_state_deep(&mut self, mask: StateFlags) {
458 self.state.remove(mask);
459 for c in &mut self.children {
460 c.clear_state_deep(mask);
461 }
462 }
463
464 /// The style drawn: [`Self::style`] plus the [`Self::states`] patches
465 /// matching [`Self::state`] (with DISABLED while `enabled` is false).
466 pub fn effective_style(&self) -> std::borrow::Cow<'_, Style> {
467 let mut state = self.state;
468 if !self.enabled {
469 state.insert(StateFlags::DISABLED);
470 }
471 self.effective_style_in(state)
472 }
473
474 /// [`Self::effective_style`] for an explicit `state`.
475 pub fn effective_style_in(&self, state: StateFlags) -> std::borrow::Cow<'_, Style> {
476 let Some(states) = &self.states else {
477 return std::borrow::Cow::Borrowed(&self.style);
478 };
479 let mut matching = states
480 .rules
481 .iter()
482 .filter(|(when, _)| !when.is_empty() && state.contains(*when))
483 .peekable();
484 if matching.peek().is_none() {
485 return std::borrow::Cow::Borrowed(&self.style);
486 }
487 let mut s = self.style.clone();
488 for (_, patch) in matching {
489 patch.apply(&mut s);
490 }
491 std::borrow::Cow::Owned(s)
492 }
493
494 pub fn id(mut self, id: impl Into<String>) -> Self {
495 self.id = Some(id.into());
496 self
497 }
498 pub fn on_click(mut self, ev: impl Into<String>) -> Self {
499 self.on_click = Some(ev.into());
500 self
501 }
502 pub fn child(mut self, w: Widget) -> Self {
503 self.children.push(w);
504 self
505 }
506
507 /// Accepts a plain px value (`0.0` = auto) or a [`Units`] directly —
508 /// e.g. `.w(18.0)` or `.w(Units::Percent(60.0))`.
509 pub fn w(mut self, w: impl Into<Units>) -> Self {
510 self.style.w = w.into();
511 self
512 }
513 pub fn h(mut self, h: impl Into<Units>) -> Self {
514 self.style.h = h.into();
515 self
516 }
517 pub fn min_w(mut self, v: f32) -> Self {
518 self.style.min_w = v;
519 self
520 }
521 pub fn min_h(mut self, v: f32) -> Self {
522 self.style.min_h = v;
523 self
524 }
525 pub fn flex(mut self, v: f32) -> Self {
526 self.style.flex = v;
527 self
528 }
529 pub fn flex_dir(mut self, v: FlexDir) -> Self {
530 self.flex_dir = v;
531 self
532 }
533 pub fn gap(mut self, v: f32) -> Self {
534 self.style.gap = v;
535 self
536 }
537 pub fn bg(mut self, v: u32) -> Self {
538 self.style.bg = v;
539 self
540 }
541 pub fn color(mut self, v: u32) -> Self {
542 self.style.color = v;
543 self
544 }
545 pub fn align(mut self, v: Align) -> Self {
546 self.style.align = v;
547 self
548 }
549 pub fn font_scale(mut self, v: f32) -> Self {
550 self.style.font_scale = v;
551 self
552 }
553 pub fn shadow(mut self, v: bool) -> Self {
554 self.style.text_shadow = v;
555 self
556 }
557 pub fn no_wrap(mut self) -> Self {
558 self.style.no_wrap = true;
559 self
560 }
561 pub fn focus_style(mut self, v: FocusStyle) -> Self {
562 self.style.focus_style = v;
563 self
564 }
565 pub fn focus_color(mut self, v: u32) -> Self {
566 self.style.focus_color = v;
567 self
568 }
569 pub fn dock(mut self, v: Dock) -> Self {
570 self.style.dock = v;
571 self
572 }
573 pub fn enabled(mut self, v: bool) -> Self {
574 self.enabled = v;
575 self
576 }
577 pub fn focused(mut self, v: bool) -> Self {
578 self.focused = v;
579 self
580 }
581 /// Position absolutely at `(x, y)` inside the parent's content box — see [`Style::pos`].
582 pub fn at(mut self, x: f32, y: f32) -> Self {
583 self.style.pos = Some((x, y));
584 self
585 }
586 /// Single line, cut with `...` when too wide — see [`Style::ellipsis`].
587 pub fn ellipsis(mut self) -> Self {
588 self.style.no_wrap = true;
589 self.style.ellipsis = true;
590 self
591 }
592 /// Clip children to this widget's rect — see [`Style::clip`].
593 pub fn clip(mut self, v: bool) -> Self {
594 self.style.clip = v;
595 self
596 }
597 /// Vertical scroll offset of the children — see [`Style::scroll_y`].
598 pub fn scroll_y(mut self, v: f32) -> Self {
599 self.style.scroll_y = v;
600 self
601 }
602 /// Draw a vertical scrollbar on overflow — see [`Style::scrollbar`].
603 pub fn scrollbar(mut self, v: bool) -> Self {
604 self.style.scrollbar = v;
605 self
606 }
607 /// Font for this widget's text — see [`Style::font`].
608 pub fn font(mut self, font: impl Into<String>) -> Self {
609 self.style.font = font.into();
610 self
611 }
612 /// Text box: greyed text shown while it's empty.
613 pub fn placeholder(mut self, text: impl Into<String>) -> Self {
614 if let WidgetKind::TextBox(t) = &mut self.kind {
615 t.placeholder = text.into();
616 }
617 self
618 }
619 /// Text box: longest text allowed, in characters (0 = unlimited).
620 pub fn max_length(mut self, n: usize) -> Self {
621 if let WidgetKind::TextBox(t) = &mut self.kind {
622 t.max_length = n;
623 }
624 self
625 }
626 /// Text box: event name raised when the text changes.
627 pub fn on_change(mut self, ev: impl Into<String>) -> Self {
628 if let WidgetKind::TextBox(t) = &mut self.kind {
629 t.on_change = Some(ev.into());
630 }
631 self
632 }
633 /// Text box: event name raised by Enter.
634 pub fn on_submit(mut self, ev: impl Into<String>) -> Self {
635 if let WidgetKind::TextBox(t) = &mut self.kind {
636 t.on_submit = Some(ev.into());
637 }
638 self
639 }
640 /// Image: how it fills the widget (WPF's `Stretch`).
641 pub fn stretch(mut self, stretch: Stretch) -> Self {
642 if let WidgetKind::Image(i) = &mut self.kind {
643 i.stretch = stretch;
644 }
645 self
646 }
647 /// Image: draw only `[u, v, w, h]` of it (texture pixels).
648 pub fn region(mut self, region: [f32; 4]) -> Self {
649 if let WidgetKind::Image(i) = &mut self.kind {
650 i.region = Some(region);
651 }
652 self
653 }
654 /// Image: multiply its colors by `tint` (`0xAARRGGBB`).
655 pub fn tint(mut self, tint: u32) -> Self {
656 if let WidgetKind::Image(i) = &mut self.kind {
657 i.tint = tint;
658 }
659 self
660 }
661 /// Adds every widget in `children`, in order.
662 pub fn children(mut self, children: impl IntoIterator<Item = Widget>) -> Self {
663 self.children.extend(children);
664 self
665 }
666
667 /// The first widget in this subtree (depth-first, self included) whose
668 /// [`Widget::id`] is `id` — for changing a built tree before laying it
669 /// out (dynamic text, enabled state, ...).
670 pub fn find_mut(&mut self, id: &str) -> Option<&mut Widget> {
671 if self.id.as_deref() == Some(id) {
672 return Some(self);
673 }
674 self.children.iter_mut().find_map(|c| c.find_mut(id))
675 }
676
677 /// Replaces the text of a label, button or text box; `false` for any
678 /// other kind.
679 pub fn set_text(&mut self, text: impl Into<String>) -> bool {
680 match &mut self.kind {
681 WidgetKind::Label(t) | WidgetKind::Button(t) => {
682 *t = text.into();
683 true
684 }
685 WidgetKind::TextBox(t) => {
686 t.text = text.into();
687 true
688 }
689 _ => false,
690 }
691 }
692
693 pub fn padding(mut self, top: f32, right: f32, bottom: f32, left: f32) -> Self {
694 self.style.pad = [top, right, bottom, left];
695 self
696 }
697 pub fn margin(mut self, top: f32, right: f32, bottom: f32, left: f32) -> Self {
698 self.style.margin = [top, right, bottom, left];
699 self
700 }
701}
702
703// ── Widget constructors ───────────────────────────────────────────────────────
704
705pub fn panel(dir: FlexDir) -> Widget {
706 Widget::new(WidgetKind::Panel(dir))
707}
708/// A vertically scrolling column: clips its children, scrolls them by
709/// [`Widget::scroll_y`] and draws a scrollbar when they overflow — WPF's
710/// `ScrollViewer` around a `StackPanel`. Give it a fixed (or flex) height.
711pub fn scroll_view() -> Widget {
712 panel(FlexDir::Column).clip(true).scrollbar(true)
713}
714pub fn label(text: impl Into<String>) -> Widget {
715 Widget::new(WidgetKind::Label(text.into()))
716}
717pub fn button(text: impl Into<String>) -> Widget {
718 Widget::new(WidgetKind::Button(text.into()))
719}
720pub fn item_slot(item_id: impl Into<String>) -> Widget {
721 Widget::new(WidgetKind::ItemSlot(item_id.into()))
722}
723/// An inventory-slot decoration (icon + stack-count overlay) for `item_id`
724/// (empty string = empty slot) with `count` items. Purely decorative — real
725/// drag-and-drop still goes through the vanilla `Slot` this overlays.
726pub fn inv_slot(item_id: impl Into<String>, count: u32) -> Widget {
727 Widget::new(WidgetKind::InvSlot {
728 item_id: item_id.into(),
729 count,
730 })
731 .w(18.0)
732 .h(18.0)
733}
734/// An image from a Yog image source — `"item:apple"`, `"block:stone"`,
735/// `"sprite:button"`, `"mymod:gui/logo"` (see [`yog_gfx::image`]) — the
736/// host resolves it for its Minecraft version. Sized to the image's natural
737/// size (16×16 until the host told it) unless given `w`/`h`; scaled into the
738/// widget per [`Widget::stretch`] (default `Uniform`), nearest-neighbour.
739pub fn image(source: impl Into<String>) -> Widget {
740 Widget::new(WidgetKind::Image(Image {
741 source: source.into(),
742 stretch: Stretch::Uniform,
743 region: None,
744 tint: 0xFFFF_FFFF,
745 }))
746}
747pub fn spacer() -> Widget {
748 Widget::new(WidgetKind::Spacer)
749}
750/// A single-line text input holding `text` — black box, grey border (white
751/// while focused), like vanilla's `EditBox`. Its text, caret and focus live
752/// in a [`TextBoxState`](crate::TextBoxState) that the code-behind keeps and
753/// applies to it each frame.
754pub fn text_box(text: impl Into<String>) -> Widget {
755 Widget::new(WidgetKind::TextBox(TextBox {
756 text: text.into(),
757 ..TextBox::default()
758 }))
759 .bg(0xFF_000000)
760 .color(0xFF_E0E0E0)
761 .padding(0.0, 4.0, 0.0, 4.0)
762}