1use crate::yui_resources::{Entry, Resources, local_name};
7use std::collections::HashMap;
8use yog_ui::attr::Attr;
9
10use yog_ui::layout::Align;
11use yog_ui::widget::{self, Widget};
12use yog_ui::{FlexDir, Track, Units};
13
14use crate::yui::Anchor;
15
16#[derive(Debug, Clone, yog_data::Schema)]
18pub struct YuiNode {
19 pub uid: String,
20 pub kind: String,
21 #[data(with = "yog_data::with::Values<yog_data::with::Spelled>", default)]
23 pub attrs: HashMap<String, String>,
24 #[data(default)]
25 pub children: Vec<YuiNode>,
26}
27
28impl YuiNode {
29 pub fn attr(&self, key: &str) -> Option<&str> {
30 self.attrs.get(key).map(String::as_str)
31 }
32}
33
34const CONTAINERS: &[&str] = &[
35 yog_ids::yui::SCREEN,
36 yog_ids::yui::CANVAS,
37 yog_ids::yui::COLUMN,
38 yog_ids::yui::ROW,
39 yog_ids::yui::SCROLL_VIEW,
40 yog_ids::yui::GRID,
41];
42
43pub fn is_container(kind: &str) -> bool {
44 CONTAINERS.contains(&kind)
45}
46
47pub fn is_property_element(node: &YuiNode) -> bool {
49 node.kind.contains('.')
50}
51
52pub fn widget_children(node: &YuiNode) -> impl Iterator<Item = &YuiNode> {
54 node.children.iter().filter(|c| !is_property_element(c))
55}
56
57fn track(v: &str) -> Option<Track> {
59 let v = v.trim();
60 if v.eq_ignore_ascii_case("auto") {
61 return Some(Track::auto());
62 }
63 if let Some(w) = v.strip_suffix('*') {
64 let w = if w.trim().is_empty() {
65 1.0
66 } else {
67 w.trim().parse().ok()?
68 };
69 return Some(Track::star(w));
70 }
71 v.parse().ok().map(Track::px)
72}
73
74fn tracks(node: &YuiNode, row: bool) -> Option<Vec<Track>> {
77 let (attr, prop, size, min, max) = if row {
78 (
79 "rows",
80 "Grid.RowDefinitions",
81 "height",
82 "min_height",
83 "max_height",
84 )
85 } else {
86 (
87 "columns",
88 "Grid.ColumnDefinitions",
89 "width",
90 "min_width",
91 "max_width",
92 )
93 };
94 if let Some(v) = node.attr(attr) {
95 return Some(
96 v.split(',')
97 .map(|p| track(p).unwrap_or(Track::star(1.0)))
98 .collect(),
99 );
100 }
101 let defs = node.children.iter().find(|c| c.kind == prop)?;
102 Some(
103 defs.children
104 .iter()
105 .map(|d| {
106 let mut t = track(d.attr(size).unwrap_or("*")).unwrap_or(Track::star(1.0));
107 if let Some(n) = num(d.attr(min)) {
108 t = t.min(n);
109 }
110 if let Some(n) = num(d.attr(max)) {
111 t = t.max(n);
112 }
113 t
114 })
115 .collect(),
116 )
117}
118
119fn cell_index(v: Option<&str>) -> Option<u16> {
120 v?.trim().parse().ok()
121}
122
123fn num(v: Option<&str>) -> Option<f32> {
124 v?.trim().parse().ok()
125}
126
127pub use crate::yui::color;
129
130const LANG: &str = yog_ids::concatcp!(yog_ids::typed::LANG, yog_ids::sep::NAMESPACE);
132
133pub fn text(value: Option<&str>) -> String {
136 let v = value.unwrap_or_default();
137 match v.strip_prefix('@') {
138 Some(rest) if rest.starts_with('@') => rest.to_string(),
139 Some(rest) if let Some(key) = rest.strip_prefix(LANG) => key.to_string(),
140 _ => v.to_string(),
141 }
142}
143
144pub fn apply_resources(root: &mut YuiNode) {
152 let read = match root
153 .children
154 .iter()
155 .find(|c| c.kind.ends_with(yog_ids::yui_markup::RESOURCES))
156 {
157 Some(res) => Resources::read(&res.children.iter().map(entry).collect::<Vec<_>>()),
159 None => Resources::default(),
160 };
161 resolve(root, &read);
162}
163
164fn entry(n: &YuiNode) -> Entry<'_> {
167 let mut attrs: Vec<(&str, &str)> = n
168 .attrs
169 .iter()
170 .map(|(k, v)| (k.as_str(), v.as_str()))
171 .collect();
172 attrs.sort();
173 Entry {
174 kind: &n.kind,
175 line: 0,
176 attrs,
177 }
178}
179
180fn resolve(n: &mut YuiNode, res: &Resources) {
183 use yog_ids::yui_markup::{BIND, BINDING, DESIGN};
184 let design: Vec<(String, String)> = n
188 .attrs
189 .iter()
190 .filter_map(|(k, v)| Some((k.strip_prefix(DESIGN)?.to_string(), v.clone())))
191 .collect();
192 n.attrs.retain(|k, v| {
193 !k.starts_with(DESIGN)
194 && !(v.trim_start().starts_with(BIND) || v.trim_start().starts_with(BINDING))
195 });
196 n.attrs.extend(design);
197 for v in n.attrs.values_mut() {
198 if let Some(r) = local_name(v).and_then(|name| res.values.get(name)) {
199 *v = r.clone();
200 }
201 }
202 if let Some(name) = n.attrs.remove(yog_ids::yui_attr::STYLE)
203 && let Some(attrs) = res
204 .styles
205 .get(name.trim().trim_start_matches(yog_ids::sep::TYPED))
206 {
207 for (k, v) in attrs {
208 n.attrs.entry(k.clone()).or_insert_with(|| v.clone());
209 }
210 }
211 if let Some(name) = n.attrs.get(yog_ids::yui_attr::X_NAME).cloned() {
212 n.attrs.entry(yog_ids::yui_attr::ID.into()).or_insert(name);
213 }
214 n.kind = as_widget(n).to_string();
215 for c in &mut n.children {
216 resolve(c, res);
217 }
218}
219
220fn as_widget(n: &YuiNode) -> &str {
223 use yog_ids::yui;
224 let horizontal = n
225 .attr(yog_ids::yui_attr::ORIENTATION)
226 .is_some_and(|o| o.trim() == HORIZONTAL);
227 match n.kind.as_str() {
228 yui::TOGGLE_BUTTON => yui::BUTTON,
229 yui::LIST_VIEW => yui::SCROLL_VIEW,
230 yui::ITEMS_CONTROL if horizontal => yui::ROW,
231 yui::ITEMS_CONTROL | yui::CONTENT_PRESENTER => yui::COLUMN,
232 k => k,
233 }
234}
235
236const HORIZONTAL: &str = "horizontal";
238
239pub fn apply_theme(root: &mut YuiNode) {
242 let Some(theme) = root
243 .attr(yog_ids::yui_attr::THEME)
244 .and_then(yog_ui::theme::Theme::builtin)
245 else {
246 return;
247 };
248 fn walk(n: &mut YuiNode, theme: &yog_ui::theme::Theme) {
249 for (k, v) in theme.attrs(&n.kind) {
250 n.attrs.entry(k.clone()).or_insert_with(|| v.clone());
251 }
252 for c in &mut n.children {
253 walk(c, theme);
254 }
255 }
256 walk(root, &theme);
257}
258
259pub fn widget(node: &YuiNode, in_canvas: bool, scroll: &HashMap<String, f32>) -> Widget {
262 let a = |k: &str| node.attr(k);
263 let mut w = grid_and_cell(of_kind(node), node);
264 let (x, y) = (num(a(yog_ids::yui_attr::X)), num(a(yog_ids::yui_attr::Y)));
265 if in_canvas || x.is_some() || y.is_some() {
266 w = w.at(x.unwrap_or(0.0), y.unwrap_or(0.0));
267 }
268 let mut keys: Vec<&String> = node.attrs.keys().collect();
270 keys.sort();
271 for key in keys {
272 w = attribute(w, node, key);
273 }
274 if node.kind == yog_ids::yui::SCROLL_VIEW
275 && let Some(offset) = scroll.get(&node.uid)
276 {
277 w = w.scroll_y(*offset);
278 }
279 if is_container(&node.kind) {
280 let canvas = matches!(
281 node.kind.as_str(),
282 yog_ids::yui::SCREEN | yog_ids::yui::CANVAS
283 );
284 for child in widget_children(node) {
285 w = w.child(widget(child, canvas, scroll));
286 }
287 }
288 w
289}
290
291fn of_kind(node: &YuiNode) -> Widget {
293 let a = |k: &str| node.attr(k);
294 match node.kind.as_str() {
295 yog_ids::yui::SCREEN | yog_ids::yui::CANVAS | yog_ids::yui::COLUMN => {
296 widget::panel(FlexDir::Column)
297 }
298 yog_ids::yui::ROW => widget::panel(FlexDir::Row),
299 yog_ids::yui::GRID => widget::grid(),
300 yog_ids::yui::SCROLL_VIEW => widget::scroll_view(),
301 yog_ids::yui::LABEL => widget::label(text(a(yog_ids::yui_attr::TEXT))),
302 yog_ids::yui::BUTTON => widget::button(text(a(yog_ids::yui_attr::TEXT))),
303 yog_ids::yui::ITEM_SLOT => {
304 widget::item_slot(a(yog_ids::yui_attr::ITEM).unwrap_or_default())
305 }
306 yog_ids::yui::RECIPE_SLOT | yog_ids::yui::RECIPE_RESULT => {
309 widget::item_slot(a(yog_ids::yui_attr::ITEM).unwrap_or_default())
310 }
311 yog_ids::yui::RECIPE_COUNT => widget::label(a(yog_ids::yui_attr::TEXT).unwrap_or("2")),
312 yog_ids::yui::INV_SLOT => widget::inv_slot(
313 a(yog_ids::yui_attr::ITEM).unwrap_or_default(),
314 a(yog_ids::yui_attr::COUNT)
315 .and_then(|c| c.trim().parse().ok())
316 .unwrap_or(1),
317 ),
318 yog_ids::yui::SLOT => widget::slot(
319 a(yog_ids::yui_attr::INDEX)
320 .and_then(|i| i.trim().parse().ok())
321 .unwrap_or(0),
322 ),
323 yog_ids::yui::PLAYER_INVENTORY => widget::player_inventory(),
324 yog_ids::yui::IMAGE => super::images::widget(node),
325 yog_ids::yui::SURFACE => widget::surface(a(yog_ids::yui_attr::SOURCE).unwrap_or_default()),
327 yog_ids::yui::TEXT_BOX => widget::text_box(text(a(yog_ids::yui_attr::TEXT))),
328 _ => widget::spacer(),
329 }
330}
331
332fn grid_and_cell(mut w: Widget, node: &YuiNode) -> Widget {
334 let a = |k: &str| node.attr(k);
335 if node.kind == yog_ids::yui::SCREEN {
336 if let Some(v) = num(a(yog_ids::yui_attr::WIDTH)) {
337 w = w.w(Units::Px(v));
338 }
339 if let Some(v) = num(a(yog_ids::yui_attr::HEIGHT)) {
340 w = w.h(Units::Px(v));
341 }
342 }
343 if node.kind == yog_ids::yui::GRID {
344 if let Some(rows) = tracks(node, true) {
345 w = w.rows(rows);
346 }
347 if let Some(cols) = tracks(node, false) {
348 w = w.cols(cols);
349 }
350 if let Some(n) = num(a(yog_ids::yui_attr::ROW_SPACING)) {
351 w = w.row_spacing(n);
352 }
353 if let Some(n) = num(a(yog_ids::yui_attr::COLUMN_SPACING)) {
354 w = w.col_spacing(n);
355 }
356 }
357 let (row, col) = (
358 cell_index(a(yog_ids::yui_attr::GRID_ROW)),
359 cell_index(a(yog_ids::yui_attr::GRID_COLUMN)),
360 );
361 if row.is_some() || col.is_some() {
362 w = w.cell(row.unwrap_or(0), col.unwrap_or(0));
363 }
364 let (rs, cs) = (
365 cell_index(a(yog_ids::yui_attr::GRID_ROWSPAN)),
366 cell_index(a(yog_ids::yui_attr::GRID_COLUMNSPAN)),
367 );
368 if rs.is_some() || cs.is_some() {
369 w = w.span(rs.unwrap_or(1), cs.unwrap_or(1));
370 }
371 w
372}
373
374fn attribute(w: Widget, node: &YuiNode, key: &str) -> Widget {
378 use yog_ids::yui_attr as at;
379 let v = node.attrs[key].as_str();
380 if let Ok(Some(attr)) = Attr::parse(key, v) {
381 return attr.apply(w);
382 }
383 match key {
384 at::ID if node.kind != yog_ids::yui::SCREEN => w.id(v),
385 at::ON_CLICK => w.on_click(v),
386 at::ON_CHANGE => w.on_change(v),
387 at::ON_SUBMIT => w.on_submit(v),
388 at::FONT if !v.trim().is_empty() => w.font(v.trim()),
389 at::PLACEHOLDER => w.placeholder(text(Some(v))),
390 _ => w,
391 }
392}
393
394const WHOLE: f32 = 100.0;
398
399pub fn screen(
400 root: &YuiNode,
401 screen_w: f32,
402 screen_h: f32,
403 scroll: &HashMap<String, f32>,
404) -> Widget {
405 let width = num(root.attr(yog_ids::yui_attr::WIDTH)).unwrap_or(widget::SCREEN_W);
406 let height = num(root.attr(yog_ids::yui_attr::HEIGHT)).unwrap_or(widget::SCREEN_H);
407 let content = widget(root, false, scroll);
408 let at = |align: Align, screen: f32, size: f32| match align {
409 Align::Start => 0.0,
410 Align::Center => ((screen - size) / 2.0).floor(),
411 Align::End => screen - size,
412 };
413 let content = match Anchor::of(root.attr(yog_ids::yui_attr::ANCHOR)) {
414 Some(Anchor::Fill) => content
415 .w(Units::Percent(WHOLE))
416 .h(Units::Percent(WHOLE))
417 .at(0.0, 0.0),
418 Some(Anchor::At(h, v)) => content.at(
419 at(h, screen_w, width).max(0.0),
420 at(v, screen_h, height).max(0.0),
421 ),
422 None => content.at(
423 at(Align::Center, screen_w, width).max(0.0),
424 at(Align::Center, screen_h, height).max(0.0),
425 ),
426 };
427 widget::panel(FlexDir::Column)
428 .w(Units::Px(screen_w))
429 .h(Units::Px(screen_h))
430 .child(content)
431}
432
433#[cfg(test)]
434mod tests {
435 use super::*;
436
437 fn node(kind: &str, attrs: &[(&str, &str)], children: Vec<YuiNode>) -> YuiNode {
438 YuiNode {
439 uid: kind.into(),
440 kind: kind.into(),
441 attrs: attrs
442 .iter()
443 .map(|(k, v)| (k.to_string(), v.to_string()))
444 .collect(),
445 children,
446 }
447 }
448
449 #[test]
450 fn styles_resources_and_kit_elements_apply_as_yog_build_does() {
451 let mut root = node(
452 "Screen",
453 &[],
454 vec![
455 node(
456 yog_ids::yui_vm::SCREEN_RESOURCES,
457 &[],
458 vec![
459 node(
460 "Color",
461 &[("x:Key", "Accent"), ("value", "#FF112233")],
462 vec![],
463 ),
464 node(
465 "Style",
466 &[("x:Key", "Base"), ("target", "Button"), ("h", "16")],
467 vec![],
468 ),
469 node(
470 "Style",
471 &[
472 ("x:Key", "Filter"),
473 ("based_on", "@Base"),
474 ("w", "58"),
475 ("color", "@Accent"),
476 ],
477 vec![],
478 ),
479 ],
480 ),
481 node(
482 "ToggleButton",
483 &[
484 ("style", "@Filter"),
485 ("w", "64"),
486 ("text", "{x:Bind label()}"),
487 ("d:text", "Yog"),
488 ],
489 vec![],
490 ),
491 node(
492 "TextBox",
493 &[("text", "{x:Bind search, Mode=TwoWay}")],
494 vec![],
495 ),
496 ],
497 );
498 apply_resources(&mut root);
499 let b = &root.children[1];
500 assert_eq!(b.kind, "Button");
501 assert_eq!(b.attr("w"), Some("64"), "its own attribute wins");
502 assert_eq!(b.attr("h"), Some("16"), "from the base style");
503 assert_eq!(b.attr("color"), Some("#FF112233"));
504 assert_eq!(b.attr("text"), Some("Yog"), "the design-time value");
505 assert_eq!(
506 root.children[2].attr("text"),
507 None,
508 "a bound value shows nothing"
509 );
510 }
511}