Skip to main content

yog_kit/
bind.rs

1//! Binding helpers used by generated `.yui` code: a [`Slot`] remembers a
2//! binding's last value so widgets are only patched when it changes, and
3//! the conversion functions turn view-model values into widget values with
4//! readable compile errors.
5
6use std::fmt::Display;
7use std::hash::{Hash, Hasher};
8
9use yog_api::ui::{StateFlags, Units, Widget};
10
11/// The last value of one binding.
12#[derive(Debug, Clone, Default)]
13pub struct Slot<T>(Option<T>);
14
15impl<T: PartialEq> Slot<T> {
16    pub const fn new() -> Self {
17        Slot(None)
18    }
19    /// Stores `v`; `true` when it differs from the previous value (or is
20    /// the first).
21    pub fn set(&mut self, v: T) -> bool {
22        if self.0.as_ref() == Some(&v) {
23            return false;
24        }
25        self.0 = Some(v);
26        true
27    }
28    /// The current value. Panics before the first [`Self::set`].
29    pub fn get(&self) -> &T {
30        self.0.as_ref().expect("yog-kit: binding read before it was set")
31    }
32    /// Forgets the value, so the next `set` reports a change.
33    pub fn reset(&mut self) {
34        self.0 = None;
35    }
36}
37
38/// A value shown as text by a `.yui` binding.
39#[diagnostic::on_unimplemented(
40    message = "`{Self}` can't be shown as text in a .yui binding",
41    note = "bind a value that implements Display, or call a view-model method returning String"
42)]
43pub trait BindText {
44    fn bind_text(&self) -> String;
45}
46
47impl<T: Display + ?Sized> BindText for T {
48    fn bind_text(&self) -> String {
49        self.to_string()
50    }
51}
52
53/// A value used as a `0xAARRGGBB` colour by a `.yui` binding.
54#[diagnostic::on_unimplemented(
55    message = "`{Self}` can't be used as a colour in a .yui binding",
56    note = "colours are u32 values 0xAARRGGBB (a .yres colour, or a view-model method returning u32)"
57)]
58pub trait BindColor {
59    fn bind_color(&self) -> u32;
60}
61
62impl BindColor for u32 {
63    fn bind_color(&self) -> u32 {
64        *self
65    }
66}
67
68impl<T: BindColor + ?Sized> BindColor for &T {
69    fn bind_color(&self) -> u32 {
70        (**self).bind_color()
71    }
72}
73
74/// Text of a bound value.
75pub fn text<T: BindText + ?Sized>(v: &T) -> String {
76    v.bind_text()
77}
78
79/// Colour of a bound value.
80pub fn color<T: BindColor>(v: T) -> u32 {
81    v.bind_color()
82}
83
84/// A bound flag (`enabled`, `visible`, `checked`).
85pub fn flag(v: impl Into<bool>) -> bool {
86    v.into()
87}
88
89/// A bound size (`w`, `h`).
90pub fn units(v: impl Into<Units>) -> Units {
91    v.into()
92}
93
94/// A bound number (`gap`, `font_scale`, ...).
95pub fn number(v: impl Into<f64>) -> f32 {
96    v.into() as f32
97}
98
99/// A bound count (`count`); out-of-range values become 0.
100pub fn count<T: TryInto<u32>>(v: T) -> u32 {
101    v.try_into().unwrap_or(0)
102}
103
104/// Hash of a bound key (`key`, `scroll_key`) — stable within a process.
105pub fn key<K: Hash + ?Sized>(k: &K) -> u64 {
106    let mut h = std::collections::hash_map::DefaultHasher::new();
107    k.hash(&mut h);
108    h.finish()
109}
110
111/// Whether the item with `key` is the selected one.
112pub fn is_selected<S: PartialEq<K>, K: ?Sized>(selected: &Option<S>, key: &K) -> bool {
113    selected.as_ref().is_some_and(|s| s == key)
114}
115
116/// Sets the placeholder of a text box; `false` for other kinds.
117pub fn set_placeholder(w: &mut Widget, text: &str) -> bool {
118    match &mut w.kind {
119        yog_api::ui::widget::WidgetKind::TextBox(tb) => {
120            tb.placeholder = text.to_owned();
121            true
122        }
123        _ => false,
124    }
125}
126
127/// Sets the item of an item slot / inventory slot; `false` for other kinds.
128pub fn set_item(w: &mut Widget, item: &str) -> bool {
129    use yog_api::ui::widget::WidgetKind;
130    match &mut w.kind {
131        WidgetKind::ItemSlot(id) | WidgetKind::InvSlot { item_id: id, .. } => {
132            *id = item.to_owned();
133            true
134        }
135        _ => false,
136    }
137}
138
139/// Sets the stack count of an inventory slot; `false` for other kinds.
140pub fn set_count(w: &mut Widget, n: u32) -> bool {
141    match &mut w.kind {
142        yog_api::ui::widget::WidgetKind::InvSlot { count, .. } => {
143            *count = n;
144            true
145        }
146        _ => false,
147    }
148}
149
150/// Sets the image source of an image; `false` for other kinds.
151pub fn set_source(w: &mut Widget, source: &str) -> bool {
152    use yog_api::ui::widget::WidgetKind;
153    match &mut w.kind {
154        WidgetKind::Image(img) => {
155            img.source = source.to_owned();
156            true
157        }
158        _ => false,
159    }
160}
161
162/// Sets or clears the CHECKED state (inherited by non-interactive children).
163pub fn set_checked(w: &mut Widget, on: bool) {
164    w.set_state_inherited(StateFlags::CHECKED, on);
165}
166
167#[cfg(test)]
168mod tests {
169    use super::*;
170
171    #[test]
172    fn slot_reports_changes_only() {
173        let mut s = Slot::new();
174        assert!(s.set(1));
175        assert!(!s.set(1));
176        assert!(s.set(2));
177        assert_eq!(*s.get(), 2);
178        s.reset();
179        assert!(s.set(2));
180    }
181
182    #[test]
183    fn conversions() {
184        assert_eq!(text(&5), "5");
185        assert_eq!(text("x"), "x");
186        assert_eq!(color(0xFF00FF00u32), 0xFF00FF00);
187        let seven: &u32 = &7;
188        assert_eq!(color(seven), 7);
189        assert_eq!(count(-1i32), 0);
190        assert_eq!(count(3usize), 3);
191        assert_eq!(key("a"), key(&"a".to_string()));
192        let sel: Option<&str> = Some("b");
193        assert!(is_selected(&sel, &"b".to_string()));
194        assert!(!is_selected(&None::<&str>, &"b".to_string()));
195    }
196}