#![allow(dead_code)]
use swiftui_native::prelude::*;
use swiftui_native::views;
#[derive(Debug, Clone)]
pub struct Node {
pub name: String,
pub value: Option<String>,
pub attrs: Vec<(String, String)>,
pub children: Vec<Node>,
pub line: usize,
}
impl Node {
fn attr(&self, key: &str) -> Option<&str> {
self.attrs.iter().find(|(k, _)| k == key).map(|(_, v)| v.as_str())
}
fn flag(&self, key: &str) -> bool {
self.attrs.iter().any(|(k, v)| k == key && (v.is_empty() || v == "true"))
}
fn f64_attr(&self, key: &str) -> Option<f64> {
self.attr(key).and_then(|v| v.parse().ok())
}
}
#[derive(Debug, PartialEq)]
enum Tok {
Word(String),
Str(String),
Eq,
Open,
Close,
}
struct Lexer<'a> {
rest: &'a str,
line: usize,
}
impl<'a> Lexer<'a> {
fn new(src: &'a str) -> Self {
Self { rest: src, line: 1 }
}
fn next(&mut self) -> Result<Option<(Tok, usize)>, String> {
loop {
let mut chars = self.rest.char_indices();
match chars.next() {
None => return Ok(None),
Some((_, '\n')) => {
self.line += 1;
self.rest = &self.rest[1..];
}
Some((_, c)) if c.is_whitespace() => {
self.rest = &self.rest[c.len_utf8()..];
}
Some((_, '/')) if self.rest.as_bytes().get(1) == Some(&b'/') => {
let end = self.rest.find('\n').unwrap_or(self.rest.len());
self.rest = &self.rest[end..];
}
Some((_, '{')) => {
self.rest = &self.rest[1..];
return Ok(Some((Tok::Open, self.line)));
}
Some((_, '}')) => {
self.rest = &self.rest[1..];
return Ok(Some((Tok::Close, self.line)));
}
Some((_, '=')) => {
self.rest = &self.rest[1..];
return Ok(Some((Tok::Eq, self.line)));
}
Some((_, '"')) => {
let line = self.line;
let body = &self.rest[1..];
let end = body.find('"').ok_or_else(|| {
format!("line {line}: string is never closed")
})?;
let s = body[..end].to_string();
self.line += s.matches('\n').count();
self.rest = &body[end + 1..];
return Ok(Some((Tok::Str(s), line)));
}
Some((_, _)) => {
let line = self.line;
let end = self
.rest
.find(|c: char| c.is_whitespace() || "{}=\"".contains(c))
.unwrap_or(self.rest.len());
let w = self.rest[..end].to_string();
self.rest = &self.rest[end..];
return Ok(Some((Tok::Word(w), line)));
}
}
}
}
}
fn tokens(src: &str) -> Result<Vec<(Tok, usize)>, String> {
let mut lex = Lexer::new(src);
let mut out = Vec::new();
while let Some(t) = lex.next()? {
out.push(t);
}
Ok(out)
}
struct Parser {
toks: Vec<(Tok, usize)>,
at: usize,
}
impl Parser {
fn peek(&self) -> Option<&Tok> {
self.toks.get(self.at).map(|(t, _)| t)
}
fn peek_at(&self, n: usize) -> Option<&Tok> {
self.toks.get(self.at + n).map(|(t, _)| t)
}
fn line(&self) -> usize {
self.toks.get(self.at).map_or_else(
|| self.toks.last().map_or(1, |(_, l)| *l),
|(_, l)| *l,
)
}
fn bump(&mut self) -> Option<(Tok, usize)> {
if self.at < self.toks.len() {
self.at += 1;
Some(core::mem::replace(&mut self.toks[self.at - 1], (Tok::Eq, 0)))
} else {
None
}
}
fn nodes(&mut self) -> Result<Vec<Node>, String> {
let mut out = Vec::new();
loop {
match self.peek() {
None | Some(Tok::Close) => return Ok(out),
Some(Tok::Word(_)) => out.push(self.node()?),
Some(other) => {
return Err(format!(
"line {}: expected a view name, found {other:?}",
self.line()
))
}
}
}
}
fn node(&mut self) -> Result<Node, String> {
let (name, line) = match self.bump() {
Some((Tok::Word(w), l)) => (w, l),
other => return Err(format!("internal: node() on {other:?}")),
};
let mut node =
Node { name, value: None, attrs: Vec::new(), children: Vec::new(), line };
loop {
match self.peek() {
Some(Tok::Str(_)) if node.value.is_none() && node.attrs.is_empty() => {
if let Some((Tok::Str(s), _)) = self.bump() {
node.value = Some(s);
}
}
Some(Tok::Word(w)) => {
if matches!(self.peek_at(1), Some(Tok::Eq)) {
let key = w.clone();
self.bump();
self.bump();
let value = match self.bump() {
Some((Tok::Word(v), _)) | Some((Tok::Str(v), _)) => v,
other => {
return Err(format!(
"line {line}: `{key}=` wants a value, found {other:?}"
))
}
};
node.attrs.push((key, value));
continue;
}
if node.value.is_none()
&& node.attrs.is_empty()
&& VALUED.contains(&node.name.as_str())
{
if let Some((Tok::Word(v), _)) = self.bump() {
node.value = Some(v);
}
continue;
}
if is_known(w) {
return Ok(node);
}
if let Some((Tok::Word(f), _)) = self.bump() {
node.attrs.push((f, String::new()));
}
}
Some(Tok::Open) => {
self.bump();
node.children = self.nodes()?;
match self.bump() {
Some((Tok::Close, _)) => {}
_ => {
return Err(format!(
"line {line}: `{}` is never closed",
node.name
))
}
}
return Ok(node);
}
None | Some(Tok::Close) => return Ok(node),
Some(Tok::Str(s)) => {
return Err(format!(
"line {}: `{}` already has a value; {s:?} is extra",
self.line(),
node.name
))
}
Some(Tok::Eq) => {
return Err(format!("line {}: `=` with nothing before it", self.line()))
}
}
}
}
}
pub fn parse(src: &str) -> Result<Vec<Node>, String> {
let mut p = Parser { toks: tokens(src)?, at: 0 };
let out = p.nodes()?;
if p.at < p.toks.len() {
return Err(format!("line {}: unexpected `}}`", p.line()));
}
Ok(out)
}
fn is_known(w: &str) -> bool {
NAMES.contains(&w)
}
const VALUED: &[&str] =
&["text", "button", "color", "image", "progress", "gauge", "label"];
pub const NAMES: &[&str] = &[
"vstack", "hstack", "zstack", "text", "spacer", "divider", "button",
"color", "image", "rect", "circle", "capsule", "progress", "gauge",
"group_box", "list", "scroll", "form", "label",
"toggle", "slider", "stepper", "field",
];
#[derive(Default)]
struct Slots {
flags: std::collections::HashMap<String, views::BoundState<bool>>,
numbers: std::collections::HashMap<String, views::BoundState<f64>>,
strings: std::collections::HashMap<String, views::BoundState<String>>,
}
fn slots() -> &'static std::sync::Mutex<Slots> {
static SLOTS: std::sync::OnceLock<std::sync::Mutex<Slots>> = std::sync::OnceLock::new();
SLOTS.get_or_init(Default::default)
}
pub fn set_flag(name: &str, value: bool) {
slots()
.lock()
.expect("slots")
.flags
.get(name)
.unwrap_or_else(|| panic!("no flag slot called {name:?}"))
.set(value);
}
pub fn slot_value(name: &str) -> Option<String> {
let g = slots().lock().expect("slots");
g.flags
.get(name)
.map(|v| v.get().to_string())
.or_else(|| g.numbers.get(name).map(|v| format!("{:.2}", v.get())))
.or_else(|| g.strings.get(name).map(|v| format!("{:?}", v.get())))
}
pub fn slot_summary() -> Vec<(String, String)> {
let g = slots().lock().expect("slots");
let mut out: Vec<(String, String)> = g
.flags
.iter()
.map(|(k, v)| (k.clone(), v.get().to_string()))
.chain(g.numbers.iter().map(|(k, v)| (k.clone(), format!("{:.2}", v.get()))))
.chain(g.strings.iter().map(|(k, v)| (k.clone(), format!("{:?}", v.get()))))
.collect();
out.sort();
out
}
fn slot_name(node: &Node) -> Option<String> {
if let Some(s) = node.attr("state") {
return Some(s.to_string());
}
node.attrs
.iter()
.find_map(|(k, v)| v.is_empty().then(|| k.strip_prefix('@')).flatten())
.map(str::to_string)
}
fn require_slot(node: &Node) -> Result<String, String> {
slot_name(node).ok_or_else(|| {
format!(
"line {}: `{}` needs a slot to keep its value in — \
write `{} @name`",
node.line, node.name, node.name
)
})
}
fn flag_slot(name: &str, initial: bool) -> views::BoundState<bool> {
slots()
.lock()
.expect("slots")
.flags
.entry(name.to_string())
.or_insert_with(|| views::BoundState::new(initial))
.clone()
}
fn number_slot(name: &str, initial: f64) -> views::BoundState<f64> {
slots()
.lock()
.expect("slots")
.numbers
.entry(name.to_string())
.or_insert_with(|| views::BoundState::new(initial))
.clone()
}
fn string_slot(name: &str, initial: &str) -> views::BoundState<String> {
slots()
.lock()
.expect("slots")
.strings
.entry(name.to_string())
.or_insert_with(|| views::BoundState::new(initial.to_string()))
.clone()
}
pub fn build(nodes: &[Node]) -> Result<View, String> {
match nodes {
[] => Err("the spec is empty".to_string()),
[one] => node_view(one),
many => {
let kids: Result<Vec<View>, String> = many.iter().map(node_view).collect();
Ok(vstack_spacing(&kids?, 8.0))
}
}
}
fn children(node: &Node) -> Result<Vec<View>, String> {
node.children.iter().map(node_view).collect()
}
fn node_view(node: &Node) -> Result<View, String> {
let spacing = node.f64_attr("spacing").unwrap_or(8.0);
let base = match node.name.as_str() {
"vstack" => {
let kids = children(node)?;
match node.attr("align") {
Some(a) => vstack_aligned_spacing(&kids, h_align(a, node.line)?, Some(spacing)),
None => vstack_spacing(&kids, spacing),
}
}
"hstack" => {
let kids = children(node)?;
match node.attr("align") {
Some(a) => hstack_aligned_spacing(&kids, v_align(a, node.line)?, Some(spacing)),
None => hstack_spacing(&kids, spacing),
}
}
"zstack" => zstack_aligned(&children(node)?, align(node.attr("align").unwrap_or("center"), node.line)?),
"text" => {
let s = node.value.as_deref().unwrap_or("");
let mut v = text(s);
if let Some(style) = node.attr("style") {
v = v.text_style(text_style(style, node.line)?);
}
if let Some(size) = node.f64_attr("size") {
v = v.font_size(size);
}
if node.flag("bold") {
v = v.bold();
}
if node.flag("italic") {
v = v.italic();
}
if let Some(c) = node.attr("color") {
v = match tint(c, node.line)? {
Tint::System(sc) => v.fg_system(sc),
Tint::Literal(lc) => v.fg(lc),
};
}
v
}
"spacer" => spacer(),
"divider" => divider(),
"button" => button(node.value.as_deref().unwrap_or("Button"), || {}),
"color" => color(parse_color(node.value.as_deref().unwrap_or("#808080"), node.line)?),
"image" => system_image(node.value.as_deref().unwrap_or("star")).into_view(),
"rect" => match node.attr("color") {
Some(c) => match tint(c, node.line)? {
Tint::System(sc) => rectangle_shape().fill_system(sc),
Tint::Literal(lc) => rectangle_shape().fill(lc),
},
None => rectangle_shape().into_view(),
},
"circle" => match node.attr("color") {
Some(c) => match tint(c, node.line)? {
Tint::System(sc) => circle_shape().fill_system(sc),
Tint::Literal(lc) => circle_shape().fill(lc),
},
None => circle_shape().into_view(),
},
"capsule" => match node.attr("color") {
Some(c) => match tint(c, node.line)? {
Tint::System(sc) => capsule_shape().fill_system(sc),
Tint::Literal(lc) => capsule_shape().fill(lc),
},
None => capsule_shape().into_view(),
},
"progress" => progress(
node.value.as_deref().and_then(|v| v.parse().ok()).unwrap_or(0.5),
node.f64_attr("total").unwrap_or(1.0),
),
"gauge" => gauge(
node.value.as_deref().and_then(|v| v.parse().ok()).unwrap_or(0.5),
node.f64_attr("min").unwrap_or(0.0),
node.f64_attr("max").unwrap_or(1.0),
text(node.attr("label").unwrap_or("")),
),
"group_box" => group_box(stack_children(node)?),
"list" => list(stack_children(node)?),
"scroll" => stack_children(node)?.scroll(),
"form" => form(stack_children(node)?),
"toggle" => toggle_bound(
node.value.as_deref().unwrap_or("Toggle"),
&flag_slot(&require_slot(node)?, node.flag("on")),
),
"slider" => {
let lo = node.f64_attr("min").unwrap_or(0.0);
let hi = node.f64_attr("max").unwrap_or(1.0);
slider_bound(
&number_slot(&require_slot(node)?, node.f64_attr("value").unwrap_or(lo)),
lo,
hi,
)
}
"stepper" => stepper_bound(
node.value.as_deref().unwrap_or("Stepper"),
&number_slot(&require_slot(node)?, node.f64_attr("value").unwrap_or(0.0)),
node.f64_attr("min").unwrap_or(0.0),
node.f64_attr("max").unwrap_or(100.0),
node.f64_attr("step").unwrap_or(1.0),
),
"field" => textfield_bound(
node.value.as_deref().unwrap_or(""),
&string_slot(&require_slot(node)?, node.attr("text").unwrap_or("")),
),
"label" => label(
node.value.as_deref().unwrap_or(""),
node.attr("icon").unwrap_or("star"),
),
other => {
return Err(format!(
"line {}: no view called `{other}`; try one of {}",
node.line,
NAMES.join(", ")
))
}
};
decorate(base, node)
}
fn stack_children(node: &Node) -> Result<View, String> {
let kids = children(node)?;
Ok(match kids.len() {
1 => kids.into_iter().next().expect("one"),
_ => vstack_spacing(&kids, node.f64_attr("spacing").unwrap_or(8.0)),
})
}
fn decorate(mut v: View, node: &Node) -> Result<View, String> {
if let Some(p) = node.f64_attr("pad") {
v = v.padding(p);
}
if let Some(size) = node.attr("frame") {
let (w, h) = parse_size(size, node.line)?;
v = v.frame(w, h);
}
if let Some(o) = node.f64_attr("opacity") {
v = v.opacity(o);
}
if let Some(c) = node.attr("bg") {
let c = parse_color(c, node.line)?;
v = v.bg(c);
}
if let Some(r) = node.f64_attr("corner") {
v = v.corner_radius(r);
}
Ok(v)
}
fn parse_size(s: &str, line: usize) -> Result<(f64, f64), String> {
let (w, h) = s
.split_once('x')
.ok_or_else(|| format!("line {line}: `frame={s}` should be WIDTHxHEIGHT"))?;
let w = w.trim().parse().map_err(|_| format!("line {line}: `{w}` is not a number"))?;
let h = h.trim().parse().map_err(|_| format!("line {line}: `{h}` is not a number"))?;
Ok((w, h))
}
fn parse_color(s: &str, line: usize) -> Result<Color, String> {
if let Some(hex) = s.strip_prefix('#') {
let v = u32::from_str_radix(hex, 16)
.map_err(|_| format!("line {line}: `#{hex}` is not a hex colour"))?;
return Ok(Color::hex(v));
}
Ok(match s {
"red" => Color::RED,
"green" => Color::GREEN,
"blue" => Color::BLUE,
"white" => Color::WHITE,
"black" => Color::BLACK,
"dark" => Color::DARK,
other => return Err(format!("line {line}: no colour called `{other}`")),
})
}
enum Tint {
System(SystemColor),
Literal(Color),
}
fn tint(s: &str, line: usize) -> Result<Tint, String> {
match system_color(s, line) {
Ok(sc) => Ok(Tint::System(sc)),
Err(_) => Ok(Tint::Literal(parse_color(s, line)?)),
}
}
fn system_color(s: &str, line: usize) -> Result<SystemColor, String> {
Ok(match s {
"red" => SystemColor::Red,
"orange" => SystemColor::Orange,
"yellow" => SystemColor::Yellow,
"green" => SystemColor::Green,
"mint" => SystemColor::Mint,
"teal" => SystemColor::Teal,
"cyan" => SystemColor::Cyan,
"blue" => SystemColor::Blue,
"indigo" => SystemColor::Indigo,
"purple" => SystemColor::Purple,
"pink" => SystemColor::Pink,
"brown" => SystemColor::Brown,
"gray" | "grey" => SystemColor::Gray,
"primary" => SystemColor::Primary,
"secondary" => SystemColor::Secondary,
other => return Err(format!("line {line}: no system colour called `{other}`")),
})
}
fn text_style(s: &str, line: usize) -> Result<TextStyle, String> {
Ok(match s {
"large_title" => TextStyle::LargeTitle,
"title" => TextStyle::Title,
"title2" => TextStyle::Title2,
"title3" => TextStyle::Title3,
"headline" => TextStyle::Headline,
"subheadline" => TextStyle::Subheadline,
"body" => TextStyle::Body,
"callout" => TextStyle::Callout,
"footnote" => TextStyle::Footnote,
"caption" => TextStyle::Caption,
"caption2" => TextStyle::Caption2,
other => return Err(format!("line {line}: no text style called `{other}`")),
})
}
fn align(s: &str, line: usize) -> Result<views::Alignment, String> {
Ok(match s {
"center" => views::Alignment::Center,
"leading" => views::Alignment::Leading,
"trailing" => views::Alignment::Trailing,
"top" => views::Alignment::Top,
"bottom" => views::Alignment::Bottom,
"top_leading" => views::Alignment::TopLeading,
"top_trailing" => views::Alignment::TopTrailing,
"bottom_leading" => views::Alignment::BottomLeading,
"bottom_trailing" => views::Alignment::BottomTrailing,
other => return Err(format!("line {line}: no alignment called `{other}`")),
})
}
fn h_align(s: &str, line: usize) -> Result<HorizontalAlignment, String> {
Ok(match s {
"leading" => HorizontalAlignment::Leading,
"center" => HorizontalAlignment::Center,
"trailing" => HorizontalAlignment::Trailing,
other => return Err(format!("line {line}: no horizontal alignment called `{other}`")),
})
}
fn v_align(s: &str, line: usize) -> Result<views::VerticalAlignment, String> {
Ok(match s {
"top" => views::VerticalAlignment::Top,
"center" => views::VerticalAlignment::Center,
"bottom" => views::VerticalAlignment::Bottom,
"first_baseline" => views::VerticalAlignment::FirstTextBaseline,
"last_baseline" => views::VerticalAlignment::LastTextBaseline,
other => return Err(format!("line {line}: no vertical alignment called `{other}`")),
})
}