use std::{collections::HashMap, rc::Rc, sync::Arc};
use crate::{component::Component, state::Handler};
#[derive(Debug, Clone)]
pub struct Element {
pub element_type: ElementType,
pub props: HashMap<String, String>,
pub handlers: HashMap<String, Handler>,
pub children: Vec<Box<Element>>,
}
#[derive(Debug, Clone)]
pub enum ElementType {
Window,
Button,
Div,
Text(String),
Input,
Image,
List,
Component(Rc<dyn Component>),
}
pub struct WindowSpec {
pub width: Option<f64>,
pub height: Option<f64>,
pub resizable: bool,
pub title: String,
}
pub fn window_spec(tree: &Element) -> WindowSpec {
let num = |key: &str| tree.props.get(key).and_then(|value| value.parse().ok());
WindowSpec {
width: num("width"),
height: num("height"),
title: tree.props.get("title").unwrap_or(&"".to_string()).clone(),
resizable: tree
.props
.get("resizable")
.map(|v| v != "false")
.unwrap_or(true),
}
}
#[macro_export]
macro_rules! ui {
(Window $($val:tt) *) => {
ui! { @element Window $($val)* }
};
(Div $($val:tt) *) => {
ui! { @element Div $($val)* }
};
(Button $($val:tt) *) => {
ui! { @element Button $($val)* }
};
(Input $($val:tt) *) => {
ui! { @element Input $($val)* }
};
(Image $($val:tt) *) => {
ui! { @element Image $($val)* }
};
(List $($val:tt) *) => {
ui! { @element List $($val)* }
};
(Text $($prop:ident ($($val:tt)*))* { $contents:expr }) => {
{
use std::collections::HashMap;
let mut el = $crate::element::Element {
element_type: $crate::element::ElementType::Text($contents.into()),
props: HashMap::new(),
handlers: HashMap::new(),
children: vec![],
};
$( ui!(@prop el, $prop ($($val)*)); )*
Box::new(el)
}
};
(Text $contents:expr) => {
{
use std::collections::HashMap;
Box::new($crate::element::Element {
element_type: $crate::element::ElementType::Text($contents.into()),
props: HashMap::new(),
handlers: HashMap::new(),
children: vec![],
})
}
};
(CHILDREN $expr:expr) => {
$expr
};
($comp:ident) => {
{
use std::collections::HashMap;
Box::new($crate::element::Element {
element_type: $crate::element::ElementType::Component(std::rc::Rc::new($comp {})),
props: HashMap::new(),
handlers: HashMap::new(),
children: vec![],
})
}
};
($comp:ident { $($inner:tt)* }) => {
{
use std::collections::HashMap;
let mut el = $crate::element::Element {
element_type: $crate::element::ElementType::Component(std::rc::Rc::new($comp {})),
props: HashMap::new(),
handlers: HashMap::new(),
children: vec![],
};
ui!(@children el, $($inner)*);
Box::new(el)
}
};
(@element $el:ident $($prop:ident ($($val:tt)*))* { $($children:tt)* }) => {
{
use std::collections::HashMap;
let mut el = $crate::element::Element {
element_type: $crate::element::ElementType::$el,
props: HashMap::new(),
handlers: HashMap::new(),
children: vec![],
};
$( ui!(@prop el, $prop ($($val)*)); )*
ui!(@children el, $($children)*);
Box::new(el)
}
};
(@element $el:ident $($prop:ident ($($val:tt)*))*) => {
{
use std::collections::HashMap;
let mut el = $crate::element::Element {
element_type: $crate::element::ElementType::$el,
props: HashMap::new(),
handlers: HashMap::new(),
children: vec![],
};
$( ui!(@prop el, $prop ($($val)*)); )*
Box::new(el)
}
};
(@prop $el:ident, on_click($($val:tt)*)) => {
$el.handlers.insert("on_click".to_string(), $crate::state::Handler::Simple(($($val)*)));
};
(@prop $el:ident, on_resize($($val:tt)*)) => {
$el.handlers
.insert("on_resize".to_string(), $crate::state::Handler::Resize(std::rc::Rc::new(($($val)*))));
};
(@prop $el:ident, on_change($($val:tt)*)) => {
$el.handlers
.insert("on_change".to_string(), $crate::state::Handler::Change(std::rc::Rc::new(($($val)*))));
};
(@prop $el:ident, on_display_item($($val:tt)*)) => {
$el.handlers
.insert("on_display_item".to_string(), $crate::state::Handler::ListItem(std::rc::Rc::new(($($val)*))));
};
(@prop $el:ident, $prop:ident($($val:tt)*)) => {
$el.props
.insert(stringify!($prop).to_string(), format!("{}", ($($val)*)));
};
(@children $v:ident,) => {};
(@children $v:ident, Text $text:literal $($rest:tt)*) => {
$v.children.push(ui!(Text $text));
ui!(@children $v, $($rest)*);
};
(@children $v:ident, { $($inner:tt)* } $($rest:tt)*) => {
$v.children.push(ui! { $($inner)* });
ui!(@children $v, $($rest)*);
};
(@children $v:ident, CHILDREN $splice:ident $($rest:tt)*) => {
$v.children.push($splice);
ui!(@children $v, $($rest)*);
};
(@children $v:ident, $comp:ident { $($inner:tt)* } $($rest:tt)*) => {
$v.children.push(ui!($comp { $($inner)* }));
ui!(@children $v, $($rest)*);
};
($expr:expr) => {
{
let child: Box<$crate::element::Element> = $expr;
child
}
};
}
#[derive(Clone)]
pub struct BlitFrame {
pub width: usize,
pub height: usize,
pub samples: u64,
pub pixels: Arc<Vec<u8>>,
pub version: u64,
}
impl BlitFrame {
pub fn empty() -> Self {
BlitFrame {
width: 0,
height: 0,
samples: 0,
pixels: Arc::new(Vec::new()),
version: 0,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn helper(_x: usize) -> String {
"helper!".to_string()
}
#[test]
fn text_props_are_recorded_and_content_survives() {
let el = crate::ui! { Text background("blue") { "Complete".to_string() } };
assert_eq!(
el.props.get("background").map(String::as_str),
Some("blue"),
"prop must land in el.props"
);
assert!(
matches!(&el.element_type, ElementType::Text(t) if t == "Complete"),
"content must survive: {:?}",
el.element_type
);
}
#[test]
fn text_plain_content_forms() {
let el = crate::ui! { Text "plain" };
assert!(matches!(&el.element_type, ElementType::Text(t) if t == "plain"));
let el = crate::ui! { Text helper(3) };
assert!(matches!(&el.element_type, ElementType::Text(t) if t == "helper!"));
let el = crate::ui! { Text format!("number: {}", 3) };
assert!(matches!(&el.element_type, ElementType::Text(t) if t == "number: 3"));
let item = String::from("field");
let el = crate::ui! { Text item.clone() };
assert!(matches!(&el.element_type, ElementType::Text(t) if t == "field"));
assert!(
el.props.is_empty(),
"no props on plain content: {:?}",
el.props
);
let el = crate::ui! { Text prop1(1) prop2(2) { item.clone() } };
assert!(matches!(&el.element_type, ElementType::Text(t) if t == "field"));
assert_eq!(el.props.get("prop1").map(String::as_str), Some("1"));
assert_eq!(el.props.get("prop2").map(String::as_str), Some("2"));
}
}