1#![allow(dead_code)]
36
37use swiftui_native::prelude::*;
38use swiftui_native::views;
39
40#[derive(Debug, Clone)]
42pub struct Node {
43 pub name: String,
44 pub value: Option<String>,
46 pub attrs: Vec<(String, String)>,
48 pub children: Vec<Node>,
49 pub line: usize,
51}
52
53impl Node {
54 fn attr(&self, key: &str) -> Option<&str> {
55 self.attrs.iter().find(|(k, _)| k == key).map(|(_, v)| v.as_str())
56 }
57
58 fn flag(&self, key: &str) -> bool {
60 self.attrs.iter().any(|(k, v)| k == key && (v.is_empty() || v == "true"))
61 }
62
63 fn f64_attr(&self, key: &str) -> Option<f64> {
64 self.attr(key).and_then(|v| v.parse().ok())
65 }
66}
67
68#[derive(Debug, PartialEq)]
71enum Tok {
72 Word(String),
73 Str(String),
74 Eq,
75 Open,
76 Close,
77}
78
79struct Lexer<'a> {
80 rest: &'a str,
81 line: usize,
82}
83
84impl<'a> Lexer<'a> {
85 fn new(src: &'a str) -> Self {
86 Self { rest: src, line: 1 }
87 }
88
89 fn next(&mut self) -> Result<Option<(Tok, usize)>, String> {
91 loop {
92 let mut chars = self.rest.char_indices();
93 match chars.next() {
94 None => return Ok(None),
95 Some((_, '\n')) => {
96 self.line += 1;
97 self.rest = &self.rest[1..];
98 }
99 Some((_, c)) if c.is_whitespace() => {
100 self.rest = &self.rest[c.len_utf8()..];
101 }
102 Some((_, '/')) if self.rest.as_bytes().get(1) == Some(&b'/') => {
107 let end = self.rest.find('\n').unwrap_or(self.rest.len());
108 self.rest = &self.rest[end..];
109 }
110 Some((_, '{')) => {
111 self.rest = &self.rest[1..];
112 return Ok(Some((Tok::Open, self.line)));
113 }
114 Some((_, '}')) => {
115 self.rest = &self.rest[1..];
116 return Ok(Some((Tok::Close, self.line)));
117 }
118 Some((_, '=')) => {
119 self.rest = &self.rest[1..];
120 return Ok(Some((Tok::Eq, self.line)));
121 }
122 Some((_, '"')) => {
123 let line = self.line;
124 let body = &self.rest[1..];
125 let end = body.find('"').ok_or_else(|| {
126 format!("line {line}: string is never closed")
127 })?;
128 let s = body[..end].to_string();
129 self.line += s.matches('\n').count();
130 self.rest = &body[end + 1..];
131 return Ok(Some((Tok::Str(s), line)));
132 }
133 Some((_, _)) => {
134 let line = self.line;
135 let end = self
136 .rest
137 .find(|c: char| c.is_whitespace() || "{}=\"".contains(c))
138 .unwrap_or(self.rest.len());
139 let w = self.rest[..end].to_string();
140 self.rest = &self.rest[end..];
141 return Ok(Some((Tok::Word(w), line)));
142 }
143 }
144 }
145 }
146}
147
148fn tokens(src: &str) -> Result<Vec<(Tok, usize)>, String> {
158 let mut lex = Lexer::new(src);
159 let mut out = Vec::new();
160 while let Some(t) = lex.next()? {
161 out.push(t);
162 }
163 Ok(out)
164}
165
166struct Parser {
167 toks: Vec<(Tok, usize)>,
168 at: usize,
169}
170
171impl Parser {
172 fn peek(&self) -> Option<&Tok> {
173 self.toks.get(self.at).map(|(t, _)| t)
174 }
175
176 fn peek_at(&self, n: usize) -> Option<&Tok> {
177 self.toks.get(self.at + n).map(|(t, _)| t)
178 }
179
180 fn line(&self) -> usize {
181 self.toks.get(self.at).map_or_else(
182 || self.toks.last().map_or(1, |(_, l)| *l),
183 |(_, l)| *l,
184 )
185 }
186
187 fn bump(&mut self) -> Option<(Tok, usize)> {
188 if self.at < self.toks.len() {
189 self.at += 1;
190 Some(core::mem::replace(&mut self.toks[self.at - 1], (Tok::Eq, 0)))
193 } else {
194 None
195 }
196 }
197
198 fn nodes(&mut self) -> Result<Vec<Node>, String> {
200 let mut out = Vec::new();
201 loop {
202 match self.peek() {
203 None | Some(Tok::Close) => return Ok(out),
204 Some(Tok::Word(_)) => out.push(self.node()?),
205 Some(other) => {
206 return Err(format!(
207 "line {}: expected a view name, found {other:?}",
208 self.line()
209 ))
210 }
211 }
212 }
213 }
214
215 fn node(&mut self) -> Result<Node, String> {
216 let (name, line) = match self.bump() {
217 Some((Tok::Word(w), l)) => (w, l),
218 other => return Err(format!("internal: node() on {other:?}")),
219 };
220 let mut node =
221 Node { name, value: None, attrs: Vec::new(), children: Vec::new(), line };
222
223 loop {
224 match self.peek() {
225 Some(Tok::Str(_)) if node.value.is_none() && node.attrs.is_empty() => {
227 if let Some((Tok::Str(s), _)) = self.bump() {
228 node.value = Some(s);
229 }
230 }
231 Some(Tok::Word(w)) => {
232 if matches!(self.peek_at(1), Some(Tok::Eq)) {
234 let key = w.clone();
235 self.bump();
236 self.bump();
237 let value = match self.bump() {
238 Some((Tok::Word(v), _)) | Some((Tok::Str(v), _)) => v,
239 other => {
240 return Err(format!(
241 "line {line}: `{key}=` wants a value, found {other:?}"
242 ))
243 }
244 };
245 node.attrs.push((key, value));
246 continue;
247 }
248 if node.value.is_none()
255 && node.attrs.is_empty()
256 && VALUED.contains(&node.name.as_str())
257 {
258 if let Some((Tok::Word(v), _)) = self.bump() {
259 node.value = Some(v);
260 }
261 continue;
262 }
263 if is_known(w) {
268 return Ok(node);
269 }
270 if let Some((Tok::Word(f), _)) = self.bump() {
271 node.attrs.push((f, String::new()));
272 }
273 }
274 Some(Tok::Open) => {
275 self.bump();
276 node.children = self.nodes()?;
277 match self.bump() {
278 Some((Tok::Close, _)) => {}
279 _ => {
280 return Err(format!(
281 "line {line}: `{}` is never closed",
282 node.name
283 ))
284 }
285 }
286 return Ok(node);
287 }
288 None | Some(Tok::Close) => return Ok(node),
289 Some(Tok::Str(s)) => {
290 return Err(format!(
291 "line {}: `{}` already has a value; {s:?} is extra",
292 self.line(),
293 node.name
294 ))
295 }
296 Some(Tok::Eq) => {
297 return Err(format!("line {}: `=` with nothing before it", self.line()))
298 }
299 }
300 }
301 }
302}
303
304pub fn parse(src: &str) -> Result<Vec<Node>, String> {
306 let mut p = Parser { toks: tokens(src)?, at: 0 };
307 let out = p.nodes()?;
308 if p.at < p.toks.len() {
309 return Err(format!("line {}: unexpected `}}`", p.line()));
310 }
311 Ok(out)
312}
313
314fn is_known(w: &str) -> bool {
319 NAMES.contains(&w)
320}
321
322const VALUED: &[&str] =
327 &["text", "button", "color", "image", "progress", "gauge", "label"];
328
329pub const NAMES: &[&str] = &[
335 "vstack", "hstack", "zstack", "text", "spacer", "divider", "button",
336 "color", "image", "rect", "circle", "capsule", "progress", "gauge",
337 "group_box", "list", "scroll", "form", "label",
338 "toggle", "slider", "stepper", "field",
342];
343
344#[derive(Default)]
357struct Slots {
358 flags: std::collections::HashMap<String, views::BoundState<bool>>,
359 numbers: std::collections::HashMap<String, views::BoundState<f64>>,
360 strings: std::collections::HashMap<String, views::BoundState<String>>,
361}
362
363fn slots() -> &'static std::sync::Mutex<Slots> {
364 static SLOTS: std::sync::OnceLock<std::sync::Mutex<Slots>> = std::sync::OnceLock::new();
365 SLOTS.get_or_init(Default::default)
366}
367
368pub fn set_flag(name: &str, value: bool) {
373 slots()
374 .lock()
375 .expect("slots")
376 .flags
377 .get(name)
378 .unwrap_or_else(|| panic!("no flag slot called {name:?}"))
379 .set(value);
380}
381
382pub fn slot_value(name: &str) -> Option<String> {
388 let g = slots().lock().expect("slots");
389 g.flags
390 .get(name)
391 .map(|v| v.get().to_string())
392 .or_else(|| g.numbers.get(name).map(|v| format!("{:.2}", v.get())))
393 .or_else(|| g.strings.get(name).map(|v| format!("{:?}", v.get())))
394}
395
396pub fn slot_summary() -> Vec<(String, String)> {
398 let g = slots().lock().expect("slots");
399 let mut out: Vec<(String, String)> = g
400 .flags
401 .iter()
402 .map(|(k, v)| (k.clone(), v.get().to_string()))
403 .chain(g.numbers.iter().map(|(k, v)| (k.clone(), format!("{:.2}", v.get()))))
404 .chain(g.strings.iter().map(|(k, v)| (k.clone(), format!("{:?}", v.get()))))
405 .collect();
406 out.sort();
407 out
408}
409
410fn slot_name(node: &Node) -> Option<String> {
416 if let Some(s) = node.attr("state") {
417 return Some(s.to_string());
418 }
419 node.attrs
420 .iter()
421 .find_map(|(k, v)| v.is_empty().then(|| k.strip_prefix('@')).flatten())
422 .map(str::to_string)
423}
424
425fn require_slot(node: &Node) -> Result<String, String> {
427 slot_name(node).ok_or_else(|| {
428 format!(
429 "line {}: `{}` needs a slot to keep its value in — \
430 write `{} @name`",
431 node.line, node.name, node.name
432 )
433 })
434}
435
436fn flag_slot(name: &str, initial: bool) -> views::BoundState<bool> {
437 slots()
438 .lock()
439 .expect("slots")
440 .flags
441 .entry(name.to_string())
442 .or_insert_with(|| views::BoundState::new(initial))
443 .clone()
444}
445
446fn number_slot(name: &str, initial: f64) -> views::BoundState<f64> {
447 slots()
448 .lock()
449 .expect("slots")
450 .numbers
451 .entry(name.to_string())
452 .or_insert_with(|| views::BoundState::new(initial))
453 .clone()
454}
455
456fn string_slot(name: &str, initial: &str) -> views::BoundState<String> {
457 slots()
458 .lock()
459 .expect("slots")
460 .strings
461 .entry(name.to_string())
462 .or_insert_with(|| views::BoundState::new(initial.to_string()))
463 .clone()
464}
465
466pub fn build(nodes: &[Node]) -> Result<View, String> {
473 match nodes {
474 [] => Err("the spec is empty".to_string()),
475 [one] => node_view(one),
476 many => {
477 let kids: Result<Vec<View>, String> = many.iter().map(node_view).collect();
478 Ok(vstack_spacing(&kids?, 8.0))
479 }
480 }
481}
482
483fn children(node: &Node) -> Result<Vec<View>, String> {
484 node.children.iter().map(node_view).collect()
485}
486
487fn node_view(node: &Node) -> Result<View, String> {
488 let spacing = node.f64_attr("spacing").unwrap_or(8.0);
489 let base = match node.name.as_str() {
490 "vstack" => {
491 let kids = children(node)?;
492 match node.attr("align") {
493 Some(a) => vstack_aligned_spacing(&kids, h_align(a, node.line)?, Some(spacing)),
494 None => vstack_spacing(&kids, spacing),
495 }
496 }
497 "hstack" => {
498 let kids = children(node)?;
499 match node.attr("align") {
500 Some(a) => hstack_aligned_spacing(&kids, v_align(a, node.line)?, Some(spacing)),
501 None => hstack_spacing(&kids, spacing),
502 }
503 }
504 "zstack" => zstack_aligned(&children(node)?, align(node.attr("align").unwrap_or("center"), node.line)?),
505 "text" => {
506 let s = node.value.as_deref().unwrap_or("");
507 let mut v = text(s);
508 if let Some(style) = node.attr("style") {
509 v = v.text_style(text_style(style, node.line)?);
510 }
511 if let Some(size) = node.f64_attr("size") {
512 v = v.font_size(size);
513 }
514 if node.flag("bold") {
515 v = v.bold();
516 }
517 if node.flag("italic") {
518 v = v.italic();
519 }
520 if let Some(c) = node.attr("color") {
521 v = match tint(c, node.line)? {
522 Tint::System(sc) => v.fg_system(sc),
523 Tint::Literal(lc) => v.fg(lc),
524 };
525 }
526 v
527 }
528 "spacer" => spacer(),
529 "divider" => divider(),
530 "button" => button(node.value.as_deref().unwrap_or("Button"), || {}),
535 "color" => color(parse_color(node.value.as_deref().unwrap_or("#808080"), node.line)?),
536 "image" => system_image(node.value.as_deref().unwrap_or("star")).into_view(),
537 "rect" => match node.attr("color") {
538 Some(c) => match tint(c, node.line)? {
539 Tint::System(sc) => rectangle_shape().fill_system(sc),
540 Tint::Literal(lc) => rectangle_shape().fill(lc),
541 },
542 None => rectangle_shape().into_view(),
543 },
544 "circle" => match node.attr("color") {
545 Some(c) => match tint(c, node.line)? {
546 Tint::System(sc) => circle_shape().fill_system(sc),
547 Tint::Literal(lc) => circle_shape().fill(lc),
548 },
549 None => circle_shape().into_view(),
550 },
551 "capsule" => match node.attr("color") {
552 Some(c) => match tint(c, node.line)? {
553 Tint::System(sc) => capsule_shape().fill_system(sc),
554 Tint::Literal(lc) => capsule_shape().fill(lc),
555 },
556 None => capsule_shape().into_view(),
557 },
558 "progress" => progress(
559 node.value.as_deref().and_then(|v| v.parse().ok()).unwrap_or(0.5),
560 node.f64_attr("total").unwrap_or(1.0),
561 ),
562 "gauge" => gauge(
563 node.value.as_deref().and_then(|v| v.parse().ok()).unwrap_or(0.5),
564 node.f64_attr("min").unwrap_or(0.0),
565 node.f64_attr("max").unwrap_or(1.0),
566 text(node.attr("label").unwrap_or("")),
567 ),
568 "group_box" => group_box(stack_children(node)?),
569 "list" => list(stack_children(node)?),
570 "scroll" => stack_children(node)?.scroll(),
571 "form" => form(stack_children(node)?),
572 "toggle" => toggle_bound(
580 node.value.as_deref().unwrap_or("Toggle"),
581 &flag_slot(&require_slot(node)?, node.flag("on")),
582 ),
583 "slider" => {
584 let lo = node.f64_attr("min").unwrap_or(0.0);
585 let hi = node.f64_attr("max").unwrap_or(1.0);
586 slider_bound(
587 &number_slot(&require_slot(node)?, node.f64_attr("value").unwrap_or(lo)),
588 lo,
589 hi,
590 )
591 }
592 "stepper" => stepper_bound(
593 node.value.as_deref().unwrap_or("Stepper"),
594 &number_slot(&require_slot(node)?, node.f64_attr("value").unwrap_or(0.0)),
595 node.f64_attr("min").unwrap_or(0.0),
596 node.f64_attr("max").unwrap_or(100.0),
597 node.f64_attr("step").unwrap_or(1.0),
598 ),
599 "field" => textfield_bound(
600 node.value.as_deref().unwrap_or(""),
601 &string_slot(&require_slot(node)?, node.attr("text").unwrap_or("")),
602 ),
603 "label" => label(
604 node.value.as_deref().unwrap_or(""),
605 node.attr("icon").unwrap_or("star"),
606 ),
607 other => {
608 return Err(format!(
609 "line {}: no view called `{other}`; try one of {}",
610 node.line,
611 NAMES.join(", ")
612 ))
613 }
614 };
615 decorate(base, node)
616}
617
618fn stack_children(node: &Node) -> Result<View, String> {
620 let kids = children(node)?;
621 Ok(match kids.len() {
622 1 => kids.into_iter().next().expect("one"),
623 _ => vstack_spacing(&kids, node.f64_attr("spacing").unwrap_or(8.0)),
624 })
625}
626
627fn decorate(mut v: View, node: &Node) -> Result<View, String> {
633 if let Some(p) = node.f64_attr("pad") {
634 v = v.padding(p);
635 }
636 if let Some(size) = node.attr("frame") {
637 let (w, h) = parse_size(size, node.line)?;
638 v = v.frame(w, h);
639 }
640 if let Some(o) = node.f64_attr("opacity") {
641 v = v.opacity(o);
642 }
643 if let Some(c) = node.attr("bg") {
644 let c = parse_color(c, node.line)?;
645 v = v.bg(c);
646 }
647 if let Some(r) = node.f64_attr("corner") {
648 v = v.corner_radius(r);
649 }
650 Ok(v)
651}
652
653fn parse_size(s: &str, line: usize) -> Result<(f64, f64), String> {
656 let (w, h) = s
657 .split_once('x')
658 .ok_or_else(|| format!("line {line}: `frame={s}` should be WIDTHxHEIGHT"))?;
659 let w = w.trim().parse().map_err(|_| format!("line {line}: `{w}` is not a number"))?;
660 let h = h.trim().parse().map_err(|_| format!("line {line}: `{h}` is not a number"))?;
661 Ok((w, h))
662}
663
664fn parse_color(s: &str, line: usize) -> Result<Color, String> {
665 if let Some(hex) = s.strip_prefix('#') {
666 let v = u32::from_str_radix(hex, 16)
667 .map_err(|_| format!("line {line}: `#{hex}` is not a hex colour"))?;
668 return Ok(Color::hex(v));
669 }
670 Ok(match s {
671 "red" => Color::RED,
672 "green" => Color::GREEN,
673 "blue" => Color::BLUE,
674 "white" => Color::WHITE,
675 "black" => Color::BLACK,
676 "dark" => Color::DARK,
677 other => return Err(format!("line {line}: no colour called `{other}`")),
678 })
679}
680
681enum Tint {
690 System(SystemColor),
691 Literal(Color),
692}
693
694fn tint(s: &str, line: usize) -> Result<Tint, String> {
695 match system_color(s, line) {
696 Ok(sc) => Ok(Tint::System(sc)),
697 Err(_) => Ok(Tint::Literal(parse_color(s, line)?)),
700 }
701}
702
703fn system_color(s: &str, line: usize) -> Result<SystemColor, String> {
704 Ok(match s {
705 "red" => SystemColor::Red,
706 "orange" => SystemColor::Orange,
707 "yellow" => SystemColor::Yellow,
708 "green" => SystemColor::Green,
709 "mint" => SystemColor::Mint,
710 "teal" => SystemColor::Teal,
711 "cyan" => SystemColor::Cyan,
712 "blue" => SystemColor::Blue,
713 "indigo" => SystemColor::Indigo,
714 "purple" => SystemColor::Purple,
715 "pink" => SystemColor::Pink,
716 "brown" => SystemColor::Brown,
717 "gray" | "grey" => SystemColor::Gray,
718 "primary" => SystemColor::Primary,
719 "secondary" => SystemColor::Secondary,
720 other => return Err(format!("line {line}: no system colour called `{other}`")),
721 })
722}
723
724fn text_style(s: &str, line: usize) -> Result<TextStyle, String> {
725 Ok(match s {
726 "large_title" => TextStyle::LargeTitle,
727 "title" => TextStyle::Title,
728 "title2" => TextStyle::Title2,
729 "title3" => TextStyle::Title3,
730 "headline" => TextStyle::Headline,
731 "subheadline" => TextStyle::Subheadline,
732 "body" => TextStyle::Body,
733 "callout" => TextStyle::Callout,
734 "footnote" => TextStyle::Footnote,
735 "caption" => TextStyle::Caption,
736 "caption2" => TextStyle::Caption2,
737 other => return Err(format!("line {line}: no text style called `{other}`")),
738 })
739}
740
741fn align(s: &str, line: usize) -> Result<views::Alignment, String> {
742 Ok(match s {
743 "center" => views::Alignment::Center,
744 "leading" => views::Alignment::Leading,
745 "trailing" => views::Alignment::Trailing,
746 "top" => views::Alignment::Top,
747 "bottom" => views::Alignment::Bottom,
748 "top_leading" => views::Alignment::TopLeading,
749 "top_trailing" => views::Alignment::TopTrailing,
750 "bottom_leading" => views::Alignment::BottomLeading,
751 "bottom_trailing" => views::Alignment::BottomTrailing,
752 other => return Err(format!("line {line}: no alignment called `{other}`")),
753 })
754}
755
756fn h_align(s: &str, line: usize) -> Result<HorizontalAlignment, String> {
757 Ok(match s {
758 "leading" => HorizontalAlignment::Leading,
759 "center" => HorizontalAlignment::Center,
760 "trailing" => HorizontalAlignment::Trailing,
761 other => return Err(format!("line {line}: no horizontal alignment called `{other}`")),
762 })
763}
764
765fn v_align(s: &str, line: usize) -> Result<views::VerticalAlignment, String> {
766 Ok(match s {
767 "top" => views::VerticalAlignment::Top,
768 "center" => views::VerticalAlignment::Center,
769 "bottom" => views::VerticalAlignment::Bottom,
770 "first_baseline" => views::VerticalAlignment::FirstTextBaseline,
771 "last_baseline" => views::VerticalAlignment::LastTextBaseline,
772 other => return Err(format!("line {line}: no vertical alignment called `{other}`")),
773 })
774}