1use crate::layout::{Align, FlexDir};
4
5#[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 pub action: Option<Action>,
19 pub visible: bool,
22 pub state: StateFlags,
25 pub states: Option<Box<StateStyles>>,
27}
28
29#[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 pub const SELECT: u16 = u16::MAX;
42
43 pub const fn new(slot: u16) -> Self {
45 Self {
46 owner: 0,
47 slot,
48 index: 0,
49 }
50 }
51}
52
53#[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 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 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#[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 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#[derive(Debug, Clone, Default, PartialEq)]
133pub struct StateStyles {
134 pub rules: Vec<(StateFlags, StylePatch)>,
135}
136
137#[derive(Debug, Clone)]
138pub enum WidgetKind {
139 Panel(FlexDir),
141 Label(String),
143 Button(String),
145 ItemSlot(String),
147 InvSlot { item_id: String, count: u32 },
153 Image(Image),
156 Spacer,
158 TextBox(TextBox),
161}
162
163#[derive(Debug, Clone, PartialEq)]
165pub struct Image {
166 pub source: String,
168 pub stretch: Stretch,
170 pub region: Option<[f32; 4]>,
173 pub tint: u32,
175}
176
177pub use yog_gfx::image::Stretch;
178
179#[derive(Debug, Clone, Default, PartialEq)]
184pub struct TextBox {
185 pub text: String,
186 pub placeholder: String,
188 pub max_length: usize,
190 pub on_change: Option<String>,
192 pub on_submit: Option<String>,
194 pub caret: Option<usize>,
196 pub caret_visible: bool,
198 pub selection: (usize, usize),
200 pub first: usize,
202}
203
204pub const PLACEHOLDER_COLOR: u32 = 0xFF_707070;
206pub const TEXT_BOX_BORDER: u32 = 0xFF_A0A0A0;
208pub const TEXT_BOX_FOCUS_BORDER: u32 = 0xFF_FFFFFF;
210pub const SELECTION_COLOR: u32 = 0xFF_2850A0;
212
213#[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#[derive(Debug, Clone, Copy, PartialEq, Default)]
234pub enum FocusStyle {
235 #[default]
237 Outline,
238 Fill,
240 None,
242}
243
244#[derive(Debug, Clone, Copy, PartialEq, Default)]
248pub enum Units {
249 #[default]
250 Auto,
251 Px(f32),
252 Percent(f32),
254}
255
256impl From<f32> for Units {
257 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 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#[derive(Debug, Clone)]
286pub struct Style {
287 pub w: Units, pub h: Units, pub min_w: f32,
290 pub min_h: f32,
291 pub flex: f32, pub dock: Dock, pub gap: f32, pub pad: [f32; 4], pub margin: [f32; 4], pub bg: u32, pub color: u32, pub align: Align,
299 pub font_scale: f32,
300 pub text_shadow: bool, pub focus_style: FocusStyle,
302 pub focus_color: u32, pub no_wrap: bool,
308 pub pos: Option<(f32, f32)>,
314 pub ellipsis: bool,
317 pub clip: bool,
320 pub scroll_y: f32,
324 pub scrollbar: bool,
328 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
365impl 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 pub fn action(mut self, a: Action) -> Self {
392 self.action = Some(a);
393 self
394 }
395 pub fn visible(mut self, v: bool) -> Self {
397 self.visible = v;
398 self
399 }
400 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 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 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 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 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 pub fn is_interactive(&self) -> bool {
441 self.action.is_some() || self.on_click.is_some()
442 }
443
444 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 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 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 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 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 pub fn at(mut self, x: f32, y: f32) -> Self {
583 self.style.pos = Some((x, y));
584 self
585 }
586 pub fn ellipsis(mut self) -> Self {
588 self.style.no_wrap = true;
589 self.style.ellipsis = true;
590 self
591 }
592 pub fn clip(mut self, v: bool) -> Self {
594 self.style.clip = v;
595 self
596 }
597 pub fn scroll_y(mut self, v: f32) -> Self {
599 self.style.scroll_y = v;
600 self
601 }
602 pub fn scrollbar(mut self, v: bool) -> Self {
604 self.style.scrollbar = v;
605 self
606 }
607 pub fn font(mut self, font: impl Into<String>) -> Self {
609 self.style.font = font.into();
610 self
611 }
612 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 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 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 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 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 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 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 pub fn children(mut self, children: impl IntoIterator<Item = Widget>) -> Self {
663 self.children.extend(children);
664 self
665 }
666
667 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 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 with_text(mut self, text: impl Into<String>) -> Self {
695 self.set_text(text);
696 self
697 }
698
699 pub fn with_source(mut self, source: impl Into<String>) -> Self {
701 match &mut self.kind {
702 WidgetKind::Image(i) => i.source = source.into(),
703 WidgetKind::ItemSlot(id) | WidgetKind::InvSlot { item_id: id, .. } => *id = source.into(),
704 _ => {}
705 }
706 self
707 }
708
709 pub fn with_count(mut self, n: u32) -> Self {
711 if let WidgetKind::InvSlot { count, .. } = &mut self.kind {
712 *count = n;
713 }
714 self
715 }
716
717 pub fn padding(mut self, top: f32, right: f32, bottom: f32, left: f32) -> Self {
718 self.style.pad = [top, right, bottom, left];
719 self
720 }
721 pub fn margin(mut self, top: f32, right: f32, bottom: f32, left: f32) -> Self {
722 self.style.margin = [top, right, bottom, left];
723 self
724 }
725}
726
727pub fn panel(dir: FlexDir) -> Widget {
730 Widget::new(WidgetKind::Panel(dir))
731}
732pub fn scroll_view() -> Widget {
736 panel(FlexDir::Column).clip(true).scrollbar(true)
737}
738pub fn label(text: impl Into<String>) -> Widget {
739 Widget::new(WidgetKind::Label(text.into()))
740}
741pub fn button(text: impl Into<String>) -> Widget {
742 Widget::new(WidgetKind::Button(text.into()))
743}
744pub fn item_slot(item_id: impl Into<String>) -> Widget {
745 Widget::new(WidgetKind::ItemSlot(item_id.into()))
746}
747pub fn inv_slot(item_id: impl Into<String>, count: u32) -> Widget {
751 Widget::new(WidgetKind::InvSlot {
752 item_id: item_id.into(),
753 count,
754 })
755 .w(18.0)
756 .h(18.0)
757}
758pub fn image(source: impl Into<String>) -> Widget {
764 Widget::new(WidgetKind::Image(Image {
765 source: source.into(),
766 stretch: Stretch::Uniform,
767 region: None,
768 tint: 0xFFFF_FFFF,
769 }))
770}
771pub fn spacer() -> Widget {
772 Widget::new(WidgetKind::Spacer)
773}
774pub fn text_box(text: impl Into<String>) -> Widget {
779 Widget::new(WidgetKind::TextBox(TextBox {
780 text: text.into(),
781 ..TextBox::default()
782 }))
783 .bg(0xFF_000000)
784 .color(0xFF_E0E0E0)
785 .padding(0.0, 4.0, 0.0, 4.0)
786}