Skip to main content

yog_ui/
attr.rs

1//! The attributes every `.yui` element takes, read once: [`Attr::parse`]
2//! turns `key="value"` into a typed attribute (or says what's wrong with
3//! it), [`Attr::apply`] sets it on a widget. The game's runtime, the screen
4//! designer and `yog build`'s code generator all read them here.
5
6use yog_gfx::Color;
7use yog_ids::yui_attr as a;
8use yog_ids::yui_value as w;
9
10use crate::layout::Align;
11use crate::widget::{Dock, FocusStyle, HAlign, Units, VAlign, Widget};
12
13/// One attribute, its value read.
14#[derive(Debug, Clone, PartialEq)]
15pub enum Attr {
16    W(Units),
17    H(Units),
18    MinW(f32),
19    MinH(f32),
20    Flex(f32),
21    Gap(f32),
22    FontScale(f32),
23    Dock(Dock),
24    Align(Align),
25    FocusStyle(FocusStyle),
26    HAlign(HAlign),
27    VAlign(VAlign),
28    Padding([f32; 4]),
29    Margin([f32; 4]),
30    Bg(Color),
31    /// An image as the background: `sprite:panel`, `mymod:gui/frame`.
32    BgImage(String),
33    Color(Color),
34    FocusColor(Color),
35    Shadow(bool),
36    Enabled(bool),
37    NoWrap(bool),
38    Ellipsis(bool),
39    MaxLength(usize),
40}
41
42/// What a color written in hex starts with: `#RRGGBB`, `0xAARRGGBB`.
43pub const HEX_MARKS: [&str; 3] = ["#", "0x", "0X"];
44
45/// `#RRGGBB`, `#AARRGGBB` or `0xAARRGGBB` → the color (a file's color is
46/// written with its `#` or `0x`, never as a decimal).
47pub fn written_color(value: &str) -> Option<Color> {
48    let v = value.trim();
49    HEX_MARKS
50        .iter()
51        .any(|p| v.starts_with(p))
52        .then(|| Color::parse(v))
53        .flatten()
54}
55
56/// A size: pixels, `50%`, or `auto` (and empty).
57pub fn size(value: &str) -> Option<Units> {
58    let v = value.trim();
59    if v.is_empty() || v == w::AUTO {
60        return Some(Units::Auto);
61    }
62    match v.strip_suffix('%') {
63        Some(p) => p.trim().parse().ok().map(Units::Percent),
64        None => v.parse().ok().map(Units::Px),
65    }
66}
67
68/// CSS-style shorthand: 1 value = all sides, 2 = vertical horizontal, 4 =
69/// top right bottom left.
70pub fn sides(value: &str) -> Option<[f32; 4]> {
71    let parts: Vec<f32> = value
72        .split([' ', ','])
73        .filter(|s| !s.is_empty())
74        .map(|s| s.parse().ok())
75        .collect::<Option<_>>()?;
76    match parts.as_slice() {
77        [all] => Some([*all; 4]),
78        [v, h] => Some([*v, *h, *v, *h]),
79        [t, r, b, l] => Some([*t, *r, *b, *l]),
80        _ => None,
81    }
82}
83
84/// The word `value` is among `words`, as its value; the words when it isn't one.
85fn pick<T: Copy>(value: &str, words: &[(&str, T)]) -> Result<T, String> {
86    let v = value.trim();
87    words
88        .iter()
89        .find(|(k, _)| *k == v)
90        .map(|(_, t)| *t)
91        .ok_or_else(|| {
92            let all: Vec<&str> = words.iter().map(|(k, _)| *k).collect();
93            format!("one of {}", all.join(", "))
94        })
95}
96
97const DOCKS: [(&str, Dock); 6] = [
98    (w::NONE, Dock::None),
99    (w::FILL, Dock::Fill),
100    (w::LEFT, Dock::Left),
101    (w::RIGHT, Dock::Right),
102    (w::TOP, Dock::Top),
103    (w::BOTTOM, Dock::Bottom),
104];
105const ALIGNS: [(&str, Align); 3] = [
106    (w::START, Align::Start),
107    (w::CENTER, Align::Center),
108    (w::END, Align::End),
109];
110const FOCUS_STYLES: [(&str, FocusStyle); 3] = [
111    (w::OUTLINE, FocusStyle::Outline),
112    (w::FILL, FocusStyle::Fill),
113    (w::NONE, FocusStyle::None),
114];
115const HALIGNS: [(&str, HAlign); 4] = [
116    (w::LEFT, HAlign::Left),
117    (w::CENTER, HAlign::Center),
118    (w::RIGHT, HAlign::Right),
119    (w::STRETCH, HAlign::Stretch),
120];
121const VALIGNS: [(&str, VAlign); 4] = [
122    (w::TOP, VAlign::Top),
123    (w::CENTER, VAlign::Center),
124    (w::BOTTOM, VAlign::Bottom),
125    (w::STRETCH, VAlign::Stretch),
126];
127const BOOLS: [(&str, bool); 2] = [(w::TRUE, true), (w::FALSE, false)];
128
129/// An image-like value: an image source with its scheme or namespace.
130fn is_image(v: &str) -> bool {
131    v.contains(yog_ids::sep::NAMESPACE)
132}
133
134impl Attr {
135    /// `key="value"` as an attribute; none when `key` isn't one of these
136    /// (an id, an event, a text, a kind's own); what's wrong otherwise.
137    pub fn parse(key: &str, value: &str) -> Result<Option<Attr>, String> {
138        let num = || {
139            value
140                .trim()
141                .parse::<f32>()
142                .map_err(|_| "a number".to_string())
143        };
144        let color = || written_color(value).ok_or_else(|| "#RRGGBB or #AARRGGBB".to_string());
145        let flag = || pick(value, &BOOLS).map_err(|_| "true or false".to_string());
146        Ok(Some(match key {
147            a::W => Attr::W(size(value).ok_or("a size: pixels, 50% or auto")?),
148            a::H => Attr::H(size(value).ok_or("a size: pixels, 50% or auto")?),
149            a::MIN_W => Attr::MinW(num()?),
150            a::MIN_H => Attr::MinH(num()?),
151            a::FLEX => Attr::Flex(num()?),
152            a::GAP => Attr::Gap(num()?),
153            a::FONT_SCALE => Attr::FontScale(num()?),
154            a::DOCK => Attr::Dock(pick(value, &DOCKS)?),
155            a::ALIGN => Attr::Align(pick(value, &ALIGNS)?),
156            a::FOCUS_STYLE => Attr::FocusStyle(pick(value, &FOCUS_STYLES)?),
157            a::HALIGN => Attr::HAlign(pick(value, &HALIGNS)?),
158            a::VALIGN => Attr::VAlign(pick(value, &VALIGNS)?),
159            a::PADDING => Attr::Padding(sides(value).ok_or("1, 2 or 4 numbers")?),
160            a::MARGIN => Attr::Margin(sides(value).ok_or("1, 2 or 4 numbers")?),
161            a::BG if is_image(value) => Attr::BgImage(value.trim().to_string()),
162            a::BG => Attr::Bg(color()?),
163            a::COLOR => Attr::Color(color()?),
164            a::FOCUS_COLOR => Attr::FocusColor(color()?),
165            a::SHADOW => Attr::Shadow(flag()?),
166            a::ENABLED => Attr::Enabled(flag()?),
167            a::NO_WRAP => Attr::NoWrap(flag()?),
168            a::ELLIPSIS => Attr::Ellipsis(flag()?),
169            a::MAX_LENGTH => Attr::MaxLength(
170                value
171                    .trim()
172                    .parse()
173                    .map_err(|_| "a whole number".to_string())?,
174            ),
175            _ => return Ok(None),
176        }))
177    }
178
179    /// Whether it styles the widget — not where it goes or how big it is.
180    pub fn is_style(&self) -> bool {
181        !matches!(
182            self,
183            Attr::W(_)
184                | Attr::H(_)
185                | Attr::Flex(_)
186                | Attr::Dock(_)
187                | Attr::Margin(_)
188                | Attr::HAlign(_)
189                | Attr::VAlign(_)
190        )
191    }
192
193    /// The attribute set on `widget`.
194    pub fn apply(self, widget: Widget) -> Widget {
195        match self {
196            Attr::W(Units::Auto) | Attr::H(Units::Auto) => widget,
197            Attr::W(u) => widget.w(u),
198            Attr::H(u) => widget.h(u),
199            Attr::MinW(n) => widget.min_w(n),
200            Attr::MinH(n) => widget.min_h(n),
201            Attr::Flex(n) => widget.flex(n),
202            Attr::Gap(n) => widget.gap(n),
203            Attr::FontScale(n) => widget.font_scale(n),
204            Attr::Dock(d) => widget.dock(d),
205            Attr::Align(al) => widget.align(al),
206            Attr::FocusStyle(s) => widget.focus_style(s),
207            Attr::HAlign(h) => widget.halign(h),
208            Attr::VAlign(v) => widget.valign(v),
209            Attr::Padding([t, r, b, l]) => widget.padding(t, r, b, l),
210            Attr::Margin([t, r, b, l]) => widget.margin(t, r, b, l),
211            Attr::Bg(c) => widget.bg(c),
212            Attr::BgImage(src) => widget.bg_image(src),
213            Attr::Color(c) => widget.color(c),
214            Attr::FocusColor(c) => widget.focus_color(c),
215            Attr::Shadow(on) => widget.shadow(on),
216            Attr::Enabled(on) => widget.enabled(on),
217            Attr::NoWrap(true) => widget.no_wrap(),
218            Attr::Ellipsis(true) => widget.ellipsis(),
219            Attr::NoWrap(false) | Attr::Ellipsis(false) => widget,
220            Attr::MaxLength(n) => widget.max_length(n),
221        }
222    }
223}