1use 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#[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 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
42pub const HEX_MARKS: [&str; 3] = ["#", "0x", "0X"];
44
45pub 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
56pub 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
68pub 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
84fn 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
129fn is_image(v: &str) -> bool {
131 v.contains(yog_ids::sep::NAMESPACE)
132}
133
134impl Attr {
135 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 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 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}