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: Props,
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 {
WindowSpec {
width: tree.props.get_float(PropType::Width),
height: tree.props.get_float(PropType::Height),
title: tree
.props
.get_string(PropType::Title)
.unwrap_or_default()
.to_string(),
resizable: tree.props.get_bool(PropType::Resizable).unwrap_or(true),
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum Prop {
Float(f64),
Usize(usize),
String(String),
Bool(bool),
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Copy)]
pub enum PropType {
Width,
Height,
Title,
Resizable,
Background,
Rows,
Color,
FontSize,
Value,
Placeholder,
Source,
Direction,
Gap,
Padding,
Grow,
}
#[derive(Debug, Clone, Default)]
pub struct Props(HashMap<PropType, Prop>);
impl Props {
pub fn new() -> Self {
Props::default()
}
pub fn insert(&mut self, prop_type: PropType, prop: Prop) {
self.0.insert(prop_type, prop);
}
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
pub fn get_string(&self, prop: PropType) -> Option<&str> {
if let Some(Prop::String(value)) = self.0.get(&prop) {
Some(value)
} else {
None
}
}
pub fn get(&self, prop: PropType) -> Option<&Prop> {
self.0.get(&prop)
}
pub fn get_usize(&self, prop: PropType) -> Option<usize> {
if let Some(Prop::Usize(val)) = self.0.get(&prop) {
Some(*val)
} else {
None
}
}
pub fn get_float(&self, prop: PropType) -> Option<f64> {
if let Some(Prop::Float(val)) = self.0.get(&prop) {
Some(*val)
} else {
None
}
}
pub fn get_bool(&self, prop: PropType) -> Option<bool> {
if let Some(Prop::Bool(val)) = self.0.get(&prop) {
Some(*val)
} else {
None
}
}
}
#[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: $crate::element::Props::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: $crate::element::Props::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::default())),
props: $crate::element::Props::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::default())),
props: $crate::element::Props::new(),
handlers: HashMap::new(),
children: vec![],
};
ui!(@children el, $($inner)*);
Box::new(el)
}
};
($comp:ident $($field:ident ($($val:tt)*))+ { $($inner:tt)* }) => {
{
use std::collections::HashMap;
let mut comp = $comp::default();
$( comp.$field = ($($val)*); )*
let mut el = $crate::element::Element {
element_type: $crate::element::ElementType::Component(std::rc::Rc::new(comp)),
props: $crate::element::Props::new(),
handlers: HashMap::new(),
children: vec![],
};
ui!(@children el, $($inner)*);
Box::new(el)
}
};
($comp:ident $($field:ident ($($val:tt)*))+) => {
{
use std::collections::HashMap;
let mut comp = $comp::default();
$( comp.$field = ($($val)*); )*
Box::new($crate::element::Element {
element_type: $crate::element::ElementType::Component(std::rc::Rc::new(comp)),
props: $crate::element::Props::new(),
handlers: HashMap::new(),
children: vec![],
})
}
};
(@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: $crate::element::Props::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: $crate::element::Props::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, background($($val:tt)*)) => {
ui!(@type_prop $el Background String ($($val)*));
};
(@prop $el:ident, rows($($val:tt)*)) => {
ui!(@type_prop $el Rows Usize ($($val)*));
};
(@prop $el:ident, width($($val:tt)*)) => {
ui!(@type_prop $el Width Float ($($val)*));
};
(@prop $el:ident, height($($val:tt)*)) => {
ui!(@type_prop $el Height Float ($($val)*));
};
(@prop $el:ident, padding($($val:tt)*)) => {
ui!(@type_prop $el Padding Float ($($val)*));
};
(@prop $el:ident, grow($($val:tt)*)) => {
ui!(@type_prop $el Grow Bool ($($val)*));
};
(@prop $el:ident, resizable($($val:tt)*)) => {
ui!(@type_prop $el Resizable Bool ($($val)*));
};
(@prop $el:ident, src($($val:tt)*)) => {
ui!(@type_prop $el Source String ($($val)*));
};
(@prop $el:ident, gap($($val:tt)*)) => {
ui!(@type_prop $el Gap Float ($($val)*));
};
(@prop $el:ident, font_size($($val:tt)*)) => {
ui!(@type_prop $el FontSize Float ($($val)*));
};
(@prop $el:ident, direction($($val:tt)*)) => {
ui!(@type_prop $el Direction String ($($val)*));
};
(@prop $el:ident, value($($val:tt)*)) => {
ui!(@type_prop $el Value String ($($val)*));
};
(@prop $el:ident, placeholder($($val:tt)*)) => {
ui!(@type_prop $el Placeholder String ($($val)*));
};
(@prop $el:ident, title($($val:tt)*)) => {
ui!(@type_prop $el Title String ($($val)*));
};
(@prop $el:ident, color($($val:tt)*)) => {
ui!(@type_prop $el Color String ($($val)*));
};
(@prop $el:ident, $prop:ident($($val:tt)*)) => {
compile_error!(concat!(
"unknown prop `", stringify!($prop),
"` — known props: width, height, padding, gap, grow, direction, background, color, font_size, title, value, placeholder, src, resizable, rows",
" (handlers: on_click, on_resize, on_change, on_display_item)"
));
};
(@type_prop $el:ident $prop:ident $prop_type:ident ($($val:tt)*)) => {
$el.props.insert($crate::element::PropType::$prop, $crate::element::Prop::$prop_type($($val.into())*));
};
(@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_string(PropType::Background),
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 width(1) height(2) { item.clone() } };
assert!(matches!(&el.element_type, ElementType::Text(t) if t == "field"));
assert_eq!(el.props.get_float(PropType::Width), Some(1.));
assert_eq!(el.props.get_float(PropType::Height), Some(2.));
}
}
#[cfg(test)]
mod typed_prop_tests {
#[test]
fn every_framework_consumed_prop_has_an_arm() {
let el = crate::ui! { Image src("slot") width(2.) };
assert_eq!(
el.props.get_string(crate::element::PropType::Source),
Some("slot")
);
let el = crate::ui! { Div grow(true) gap(4.) { { crate::ui! { Text "x" } } } };
assert_eq!(
el.props.get_bool(crate::element::PropType::Grow),
Some(true)
);
assert_eq!(el.props.get_float(crate::element::PropType::Gap), Some(4.));
let el = crate::ui! { Window width(400.) resizable(false) };
assert_eq!(
el.props.get_bool(crate::element::PropType::Resizable),
Some(false)
);
}
}
#[cfg(test)]
mod component_prop_tests {
use crate::component::Component;
#[derive(Debug)]
struct Tag(&'static str);
#[derive(Debug)]
struct Fancy {
tag: Tag,
count: usize,
label: String,
}
impl Default for Fancy {
fn default() -> Self {
Self {
tag: Tag("none"),
count: 0,
label: String::new(),
}
}
}
impl Component for Fancy {
fn render(
&self,
_ctx: &crate::state::Ctx,
_children: Vec<Box<crate::element::Element>>,
) -> Box<crate::element::Element> {
crate::ui! { Text self.label.clone() }
}
}
#[test]
fn component_props_are_fields_with_custom_types() {
let el = crate::ui! { Fancy tag(Tag("hi")) count(2) };
let debug = format!("{:?}", el.element_type);
assert!(debug.contains("hi"), "custom type landed: {debug}");
assert!(debug.contains("count: 2"), "primitive landed: {debug}");
assert!(debug.contains("label: \"\""), "unset field kept Default: {debug}");
let el = crate::ui! { Fancy };
let debug = format!("{:?}", el.element_type);
assert!(debug.contains("tag: Tag(\"none\")"), "bare = Default: {debug}");
let el = crate::ui! { Fancy count(7) { { crate::ui! { Text "child" } } } };
assert_eq!(el.children.len(), 1);
}
}