use crate::layout::LayoutStyle;
use crate::style::Style;
use std::any::{Any, TypeId};
pub trait Component: 'static {
type Props: Default + 'static;
fn render(props: &Self::Props) -> Element;
}
#[derive(Default)]
pub enum Element {
#[default]
Empty,
Text {
content: String,
style: Style,
},
Node {
type_id: TypeId,
props: Box<dyn Any>,
layout_style: Box<LayoutStyle>,
children: Vec<Element>,
render_fn: fn(&dyn Any) -> Element,
},
Fragment(Vec<Element>),
}
impl Element {
pub fn empty() -> Self {
Element::Empty
}
pub fn text(s: impl Into<String>) -> Self {
Element::Text {
content: s.into(),
style: Style::default(),
}
}
pub fn styled_text(s: impl Into<String>, style: Style) -> Self {
Element::Text {
content: s.into(),
style,
}
}
pub fn node<C: Component>(props: C::Props, children: Vec<Element>) -> Self {
Element::Node {
type_id: TypeId::of::<C>(),
props: Box::new(props),
layout_style: Box::new(LayoutStyle::default()),
children,
render_fn: |props_any| {
let props = props_any.downcast_ref::<C::Props>().unwrap();
C::render(props)
},
}
}
pub fn node_with_layout<C: Component>(
props: C::Props,
layout_style: LayoutStyle,
children: Vec<Element>,
) -> Self {
Element::Node {
type_id: TypeId::of::<C>(),
props: Box::new(props),
layout_style: Box::new(layout_style),
children,
render_fn: |props_any| {
let props = props_any.downcast_ref::<C::Props>().unwrap();
C::render(props)
},
}
}
pub fn type_id(&self) -> Option<TypeId> {
match self {
Element::Node { type_id, .. } => Some(*type_id),
_ => None,
}
}
pub fn is_empty(&self) -> bool {
matches!(self, Element::Empty)
}
pub fn is_text(&self) -> bool {
matches!(self, Element::Text { .. })
}
pub fn is_node(&self) -> bool {
matches!(self, Element::Node { .. })
}
pub fn is_fragment(&self) -> bool {
matches!(self, Element::Fragment(_))
}
pub fn fragment(elements: Vec<Element>) -> Self {
Element::Fragment(elements)
}
pub fn column(children: Vec<Element>) -> Self {
use crate::components::{Box, BoxProps};
Element::node::<Box>(
BoxProps {
flex_direction: crate::layout::FlexDirection::Column,
..Default::default()
},
children,
)
}
pub fn row(children: Vec<Element>) -> Self {
use crate::components::{Box, BoxProps};
Element::node::<Box>(
BoxProps {
flex_direction: crate::layout::FlexDirection::Row,
..Default::default()
},
children,
)
}
pub fn children(&self) -> &[Element] {
match self {
Element::Node { children, .. } => children,
_ => &[],
}
}
pub fn layout_style(&self) -> &LayoutStyle {
use std::sync::LazyLock;
static DEFAULT_LAYOUT: LazyLock<LayoutStyle> = LazyLock::new(LayoutStyle::default);
match self {
Element::Node { layout_style, .. } => layout_style,
_ => &DEFAULT_LAYOUT,
}
}
pub fn render_component(&self) -> Option<Element> {
match self {
Element::Node {
props, render_fn, ..
} => Some(render_fn(props.as_ref())),
_ => None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::style::Color;
struct TestComponent;
#[derive(Default)]
struct TestProps {
value: i32,
}
impl Component for TestComponent {
type Props = TestProps;
fn render(props: &Self::Props) -> Element {
Element::text(format!("Value: {}", props.value))
}
}
struct ContainerComponent;
#[derive(Default)]
struct ContainerProps {
label: String,
}
impl Component for ContainerComponent {
type Props = ContainerProps;
fn render(props: &Self::Props) -> Element {
Element::text(format!("Container: {}", props.label))
}
}
#[test]
fn test_element_empty() {
let elem = Element::empty();
assert!(elem.is_empty());
assert!(!elem.is_text());
assert!(!elem.is_node());
}
#[test]
fn test_element_text() {
let elem = Element::text("Hello");
match &elem {
Element::Text { content, style } => {
assert_eq!(content, "Hello");
assert_eq!(*style, Style::default());
}
_ => panic!("Expected Text"),
}
assert!(elem.is_text());
assert!(!elem.is_empty());
assert!(!elem.is_node());
}
#[test]
fn test_element_styled_text() {
let style = Style::new().fg(Color::Red).bold();
let elem = Element::styled_text("Styled", style);
match &elem {
Element::Text {
content, style: s, ..
} => {
assert_eq!(content, "Styled");
assert_eq!(s.fg, Color::Red);
}
_ => panic!("Expected Text"),
}
}
#[test]
fn test_element_node() {
let elem = Element::node::<TestComponent>(TestProps { value: 42 }, vec![]);
match &elem {
Element::Node { children, .. } => assert!(children.is_empty()),
_ => panic!("Expected Node"),
}
assert!(elem.is_node());
assert!(!elem.is_empty());
assert!(!elem.is_text());
}
#[test]
fn test_element_with_children() {
let child = Element::text("Child");
let elem = Element::node::<TestComponent>(TestProps::default(), vec![child]);
match &elem {
Element::Node { children, .. } => assert_eq!(children.len(), 1),
_ => panic!("Expected Node"),
}
}
#[test]
fn test_element_type_id() {
let elem = Element::node::<TestComponent>(TestProps::default(), vec![]);
assert_eq!(elem.type_id(), Some(TypeId::of::<TestComponent>()));
let text_elem = Element::text("Hello");
assert_eq!(text_elem.type_id(), None);
}
#[test]
fn test_element_children() {
let child1 = Element::text("A");
let child2 = Element::text("B");
let elem = Element::node::<TestComponent>(TestProps::default(), vec![child1, child2]);
assert_eq!(elem.children().len(), 2);
let empty = Element::empty();
assert!(empty.children().is_empty());
}
#[test]
fn test_element_default() {
let elem = Element::default();
assert!(elem.is_empty());
}
#[test]
fn test_component_render() {
let elem = Element::node::<TestComponent>(TestProps { value: 99 }, vec![]);
let rendered = elem.render_component().unwrap();
match rendered {
Element::Text { content, .. } => {
assert_eq!(content, "Value: 99");
}
_ => panic!("Expected Text from render"),
}
}
#[test]
fn test_nested_components() {
let inner = Element::node::<TestComponent>(TestProps { value: 10 }, vec![]);
let outer = Element::node::<ContainerComponent>(
ContainerProps {
label: "Outer".to_string(),
},
vec![inner],
);
assert!(outer.is_node());
assert_eq!(outer.children().len(), 1);
assert!(outer.children()[0].is_node());
}
#[test]
fn test_element_node_with_layout() {
use crate::layout::FlexDirection;
let layout = LayoutStyle {
width: Some(100.0),
height: Some(50.0),
flex_direction: FlexDirection::Row,
..Default::default()
};
let elem = Element::node_with_layout::<TestComponent>(TestProps::default(), layout, vec![]);
let style = elem.layout_style();
assert_eq!(style.width, Some(100.0));
assert_eq!(style.height, Some(50.0));
assert_eq!(style.flex_direction, FlexDirection::Row);
}
#[test]
fn test_render_component_on_non_node() {
let text = Element::text("Hello");
assert!(text.render_component().is_none());
let empty = Element::empty();
assert!(empty.render_component().is_none());
}
}