use std::{any::Any, rc::Rc};
use crate::element::{
content::TextContent,
style::{PaintProps, StyleProps},
};
pub type ComponentFn = fn(&crate::runtime::cx::Cx) -> crate::element::Element;
fn component_identity(view: ComponentFn) -> usize {
view as *const () as usize
}
pub trait AnyProps: 'static {
fn clone_box(&self) -> Box<dyn AnyProps>;
fn eq_box(&self, other: &dyn AnyProps) -> bool;
fn as_any(&self) -> &dyn Any;
}
impl<T: Clone + PartialEq + 'static> AnyProps for T {
fn clone_box(&self) -> Box<dyn AnyProps> {
Box::new(self.clone())
}
fn eq_box(&self, other: &dyn AnyProps) -> bool {
other
.as_any()
.downcast_ref::<T>()
.is_some_and(|other| self == other)
}
fn as_any(&self) -> &dyn Any {
self
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct Key(pub u64);
#[derive(Clone, Debug, Default, PartialEq)]
pub struct TextInputMeta {
pub cursor: usize,
pub value: String,
}
#[derive(Clone, Default, Debug)]
pub struct BoxElement {
pub style: StyleProps,
pub paint: PaintProps,
pub children: Vec<crate::element::Element>,
pub key: Option<Key>,
pub handlers: crate::retained::EventHandlers,
pub text_input: Option<TextInputMeta>,
pub scroll_viewport: bool,
pub scroll_bar: bool,
}
#[derive(Clone)]
pub struct TextElement {
pub content: TextContent,
pub style: crate::element::style::TextStyle,
pub key: Option<Key>,
}
impl std::fmt::Debug for TextElement {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("TextElement")
.field("content", &format!("{:?}", self.content.resolve()))
.field("style", &self.style)
.field("key", &self.key)
.finish()
}
}
#[derive(Clone)]
pub struct ButtonElement {
pub label: TextContent,
pub style: StyleProps,
pub paint: PaintProps,
pub on_click: Option<Rc<dyn Fn()>>,
pub key: Option<Key>,
}
impl std::fmt::Debug for ButtonElement {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ButtonElement")
.field("label", &format!("{:?}", self.label.resolve()))
.field("style", &self.style)
.field("paint", &self.paint)
.field("on_click", &self.on_click.as_ref().map(|_| "Box<dyn Fn()>"))
.field("key", &self.key)
.finish()
}
}
#[derive(Clone)]
pub struct ImageElement {
pub src: String,
pub style: StyleProps,
pub key: Option<Key>,
}
impl std::fmt::Debug for ImageElement {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ImageElement")
.field("src", &self.src)
.field("style", &self.style)
.field("key", &self.key)
.finish()
}
}
pub struct ComponentElement {
view: Rc<dyn Fn(&crate::runtime::cx::Cx) -> crate::element::Element>,
type_id: std::any::TypeId,
identity: usize,
key: Option<Key>,
props: Option<Box<dyn AnyProps>>,
}
impl ComponentElement {
fn new(
type_id: std::any::TypeId,
identity: usize,
view: Rc<dyn Fn(&crate::runtime::cx::Cx) -> crate::element::Element>,
) -> Self {
Self {
view,
type_id,
identity,
key: None,
props: None,
}
}
pub fn from_component_fn(view: ComponentFn) -> Self {
Self::new(
std::any::TypeId::of::<ComponentFn>(),
component_identity(view),
Rc::new(view),
)
}
pub fn from_component_fn_with_props<P: Clone + PartialEq + 'static>(
view: fn(&crate::runtime::cx::Cx, &P) -> crate::element::Element,
props: P,
) -> Self {
let identity = view as *const () as usize;
let props_for_view = props.clone();
Self {
view: Rc::new(move |cx| view(cx, &props_for_view)),
type_id: std::any::TypeId::of::<
fn(&crate::runtime::cx::Cx, &P) -> crate::element::Element,
>(),
identity,
key: None,
props: Some(Box::new(props)),
}
}
pub fn type_id(&self) -> std::any::TypeId {
self.type_id
}
pub(crate) fn identity(&self) -> usize {
self.identity
}
pub fn key(&self) -> Option<&Key> {
self.key.as_ref()
}
pub fn view(&self) -> Rc<dyn Fn(&crate::runtime::cx::Cx) -> crate::element::Element> {
self.view.clone()
}
pub(crate) fn props_eq(&self, other: &Self) -> bool {
match (&self.props, &other.props) {
(Some(left), Some(right)) => left.eq_box(right.as_ref()),
(None, None) => true,
_ => false,
}
}
pub(crate) fn has_props(&self) -> bool {
self.props.is_some()
}
pub(crate) fn with_key(mut self, key: Key) -> Self {
self.key = Some(key);
self
}
}
impl Clone for ComponentElement {
fn clone(&self) -> Self {
Self {
view: self.view.clone(),
type_id: self.type_id,
identity: self.identity,
key: self.key.clone(),
props: self.props.as_ref().map(|props| props.clone_box()),
}
}
}
impl std::fmt::Debug for ComponentElement {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let _ = &self.view;
f.debug_struct("ComponentElement")
.field("view", &"Box<dyn Fn()>")
.field("type_id", &self.type_id)
.field("identity", &self.identity)
.field("key", &self.key)
.field("props", &self.props.as_ref().map(|_| "Box<dyn AnyProps>"))
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::element::builders::{Component, Text};
use crate::element::content::TextContent;
use crate::element::Element;
#[test]
fn box_element_default_has_no_children() {
let el = BoxElement::default();
assert!(el.children.is_empty());
}
#[test]
fn text_element_resolves_static_content() {
let el = TextElement {
content: TextContent::Static("hello".into()),
style: Default::default(),
key: None,
};
assert_eq!(el.content.resolve(), "hello");
}
#[test]
fn text_element_resolves_dynamic_content() {
let el = TextElement {
content: TextContent::Dynamic(Rc::new(|| "dynamic".to_owned())),
style: Default::default(),
key: None,
};
assert_eq!(el.content.resolve(), "dynamic");
}
#[test]
fn component_element_new_preserves_explicit_identity_for_wrapped_view() {
fn child(_cx: &crate::runtime::cx::Cx) -> Element {
Text::new("child").into_element()
}
let type_id = std::any::TypeId::of::<ComponentFn>();
let component = ComponentElement::new(type_id, component_identity(child), Rc::new(child))
.with_key(Key(3));
assert_eq!(component.type_id(), type_id);
assert_eq!(component.identity(), component_identity(child));
assert_eq!(component.key(), Some(&Key(3)));
let Element::Text(rendered) = (component.view())(&crate::runtime::cx::Cx::new()) else {
panic!("expected text element");
};
assert_eq!(rendered.content.resolve(), "child");
}
#[test]
fn component_fn_identity_distinguishes_different_functions_of_same_type() {
fn first(_cx: &crate::runtime::cx::Cx) -> Element {
Text::new("first").into_element()
}
fn second(_cx: &crate::runtime::cx::Cx) -> Element {
Text::new("second").into_element()
}
let first_component = ComponentElement::from_component_fn(first);
let second_component = ComponentElement::from_component_fn(second);
assert_eq!(first_component.type_id(), second_component.type_id());
assert_ne!(first_component.identity(), second_component.identity());
}
#[test]
fn component_element_with_props_stores_and_compares_props() {
#[derive(Clone, PartialEq)]
struct MyProps {
value: i32,
}
fn child(_cx: &crate::runtime::cx::Cx, props: &MyProps) -> Element {
Text::new(props.value.to_string()).into_element()
}
fn child_no_props(_cx: &crate::runtime::cx::Cx) -> Element {
Text::new("no props").into_element()
}
let a = ComponentElement::from_component_fn_with_props(child, MyProps { value: 1 });
let b = ComponentElement::from_component_fn_with_props(child, MyProps { value: 1 });
let c = ComponentElement::from_component_fn_with_props(child, MyProps { value: 2 });
let d = ComponentElement::from_component_fn(child_no_props);
assert_eq!(a.identity(), b.identity());
assert!(a.props_eq(&b));
assert!(!a.props_eq(&c));
assert!(!a.props_eq(&d));
}
#[test]
fn component_new_with_props_renders_with_props() {
#[derive(Clone, PartialEq)]
struct Props {
label: String,
}
fn child(_cx: &crate::runtime::cx::Cx, props: &Props) -> Element {
Text::new(props.label.clone()).into_element()
}
let Element::Component(component) =
Component::new_with_props(child, Props { label: "hi".into() }).into_element()
else {
panic!("expected component element");
};
let Element::Text(rendered) = (component.view())(&crate::runtime::cx::Cx::new()) else {
panic!("expected text element");
};
assert_eq!(rendered.content.resolve(), "hi");
}
}