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