Skip to main content

cliffy_core/
component.rs

1//! Component model for Algebraic TSX
2//!
3//! Components are geometric morphisms from state to renderable elements.
4//! They compose naturally via geometric product of their state spaces.
5//!
6//! # Key Concepts
7//!
8//! - **Component**: A function from geometric state to Element
9//! - **Element**: A node in the virtual element tree (not virtual DOM - dataflow graph)
10//! - **Composition**: Components compose via geometric product
11//! - **Props**: Constraints on a component's state space
12//!
13//! # Example
14//!
15//! ```rust
16//! use cliffy_core::component::{Component, Element, ElementKind, Props};
17//! use cliffy_core::GA3;
18//!
19//! // Define a simple counter component
20//! struct Counter {
21//!     initial: i32,
22//! }
23//!
24//! impl Component for Counter {
25//!     fn render(&self, state: &GA3) -> Element {
26//!         let count = state.get(0) as i32;
27//!         Element::new(ElementKind::Text(format!("Count: {}", count)))
28//!     }
29//! }
30//!
31//! let counter = Counter { initial: 0 };
32//! let element = counter.render(&GA3::scalar(42.0));
33//! ```
34
35use crate::GA3;
36use std::collections::HashMap;
37use std::sync::Arc;
38
39/// A component is a geometric morphism from state to renderable output.
40///
41/// Components transform geometric state into Element trees. The state
42/// is always a GA3 multivector, but components can interpret it in
43/// any way they choose (scalar counter, 3D position, etc.).
44pub trait Component: Send + Sync {
45    /// Render the component given the current geometric state.
46    ///
47    /// The state represents the component's local state. Components
48    /// should be pure functions of their state.
49    fn render(&self, state: &GA3) -> Element;
50
51    /// Get the initial state for this component.
52    ///
53    /// Returns the geometric state to use when the component mounts.
54    fn initial_state(&self) -> GA3 {
55        GA3::zero()
56    }
57
58    /// Get the component's type name for debugging.
59    fn type_name(&self) -> &'static str {
60        std::any::type_name::<Self>()
61    }
62}
63
64/// A renderable element in the algebraic element tree.
65///
66/// Unlike virtual DOM nodes, Elements are nodes in a geometric dataflow graph.
67/// They represent projections from geometric state to DOM operations.
68#[derive(Debug, Clone)]
69pub struct Element {
70    /// The kind of element (tag, text, component reference)
71    pub kind: ElementKind,
72    /// Props/attributes for this element
73    pub props: Props,
74    /// Child elements
75    pub children: Vec<Element>,
76    /// Unique key for reconciliation (optional)
77    pub key: Option<String>,
78}
79
80impl Element {
81    /// Create a new element with the given kind.
82    pub fn new(kind: ElementKind) -> Self {
83        Self {
84            kind,
85            props: Props::new(),
86            children: Vec::new(),
87            key: None,
88        }
89    }
90
91    /// Create a text element.
92    pub fn text(content: impl Into<String>) -> Self {
93        Self::new(ElementKind::Text(content.into()))
94    }
95
96    /// Create an element with a tag name.
97    pub fn tag(name: impl Into<String>) -> Self {
98        Self::new(ElementKind::Tag(name.into()))
99    }
100
101    /// Create a fragment (multiple elements without a wrapper).
102    pub fn fragment(children: Vec<Element>) -> Self {
103        Self {
104            kind: ElementKind::Fragment,
105            props: Props::new(),
106            children,
107            key: None,
108        }
109    }
110
111    /// Create an empty element (renders nothing).
112    pub fn empty() -> Self {
113        Self::new(ElementKind::Empty)
114    }
115
116    /// Add a child element.
117    pub fn child(mut self, child: Element) -> Self {
118        self.children.push(child);
119        self
120    }
121
122    /// Add multiple children.
123    pub fn children(mut self, children: impl IntoIterator<Item = Element>) -> Self {
124        self.children.extend(children);
125        self
126    }
127
128    /// Set a prop/attribute.
129    pub fn prop(mut self, key: impl Into<String>, value: PropValue) -> Self {
130        self.props.set(key, value);
131        self
132    }
133
134    /// Set a string prop.
135    pub fn attr(self, key: impl Into<String>, value: impl Into<String>) -> Self {
136        self.prop(key, PropValue::String(value.into()))
137    }
138
139    /// Set a numeric prop.
140    pub fn num(self, key: impl Into<String>, value: f64) -> Self {
141        self.prop(key, PropValue::Number(value))
142    }
143
144    /// Set a boolean prop.
145    pub fn bool(self, key: impl Into<String>, value: bool) -> Self {
146        self.prop(key, PropValue::Bool(value))
147    }
148
149    /// Set the element's key.
150    pub fn with_key(mut self, key: impl Into<String>) -> Self {
151        self.key = Some(key.into());
152        self
153    }
154
155    /// Check if this is an empty element.
156    pub fn is_empty(&self) -> bool {
157        matches!(self.kind, ElementKind::Empty)
158    }
159
160    /// Count total nodes in this element tree.
161    pub fn node_count(&self) -> usize {
162        1 + self.children.iter().map(|c| c.node_count()).sum::<usize>()
163    }
164}
165
166/// The kind of element.
167#[derive(Debug, Clone, PartialEq)]
168pub enum ElementKind {
169    /// An HTML/DOM tag (div, span, button, etc.)
170    Tag(String),
171    /// Plain text content
172    Text(String),
173    /// A fragment (multiple elements without wrapper)
174    Fragment,
175    /// A component reference (for nested components)
176    ComponentRef(ComponentRef),
177    /// An empty element (renders nothing)
178    Empty,
179}
180
181/// A reference to a child component.
182#[derive(Clone)]
183pub struct ComponentRef {
184    /// Component type name for debugging
185    pub type_name: String,
186    /// The component instance (wrapped for Clone)
187    pub component: Arc<dyn Component>,
188    /// Props passed to the component
189    pub props: Props,
190}
191
192impl std::fmt::Debug for ComponentRef {
193    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
194        f.debug_struct("ComponentRef")
195            .field("type_name", &self.type_name)
196            .field("props", &self.props)
197            .finish_non_exhaustive()
198    }
199}
200
201impl PartialEq for ComponentRef {
202    fn eq(&self, other: &Self) -> bool {
203        self.type_name == other.type_name
204    }
205}
206
207/// Props (properties/attributes) for an element.
208#[derive(Debug, Clone, Default)]
209pub struct Props {
210    values: HashMap<String, PropValue>,
211}
212
213impl Props {
214    /// Create empty props.
215    pub fn new() -> Self {
216        Self::default()
217    }
218
219    /// Set a prop value.
220    pub fn set(&mut self, key: impl Into<String>, value: PropValue) {
221        self.values.insert(key.into(), value);
222    }
223
224    /// Get a prop value.
225    pub fn get(&self, key: &str) -> Option<&PropValue> {
226        self.values.get(key)
227    }
228
229    /// Check if a prop exists.
230    pub fn has(&self, key: &str) -> bool {
231        self.values.contains_key(key)
232    }
233
234    /// Get all prop keys.
235    pub fn keys(&self) -> impl Iterator<Item = &String> {
236        self.values.keys()
237    }
238
239    /// Iterate over all props.
240    pub fn iter(&self) -> impl Iterator<Item = (&String, &PropValue)> {
241        self.values.iter()
242    }
243
244    /// Merge with another props, other takes precedence.
245    pub fn merge(&mut self, other: Props) {
246        for (k, v) in other.values {
247            self.values.insert(k, v);
248        }
249    }
250}
251
252/// A property value.
253#[derive(Debug, Clone, PartialEq)]
254pub enum PropValue {
255    /// String value
256    String(String),
257    /// Numeric value
258    Number(f64),
259    /// Boolean value
260    Bool(bool),
261    /// Array of values
262    Array(Vec<PropValue>),
263    /// Nested object
264    Object(HashMap<String, PropValue>),
265    /// Null/undefined
266    Null,
267}
268
269impl PropValue {
270    /// Get as string, if it is one.
271    pub fn as_str(&self) -> Option<&str> {
272        match self {
273            PropValue::String(s) => Some(s),
274            _ => None,
275        }
276    }
277
278    /// Get as f64, if it is a number.
279    pub fn as_f64(&self) -> Option<f64> {
280        match self {
281            PropValue::Number(n) => Some(*n),
282            _ => None,
283        }
284    }
285
286    /// Get as bool, if it is one.
287    pub fn as_bool(&self) -> Option<bool> {
288        match self {
289            PropValue::Bool(b) => Some(*b),
290            _ => None,
291        }
292    }
293}
294
295impl From<&str> for PropValue {
296    fn from(s: &str) -> Self {
297        PropValue::String(s.to_string())
298    }
299}
300
301impl From<String> for PropValue {
302    fn from(s: String) -> Self {
303        PropValue::String(s)
304    }
305}
306
307impl From<f64> for PropValue {
308    fn from(n: f64) -> Self {
309        PropValue::Number(n)
310    }
311}
312
313impl From<i32> for PropValue {
314    fn from(n: i32) -> Self {
315        PropValue::Number(n as f64)
316    }
317}
318
319impl From<bool> for PropValue {
320    fn from(b: bool) -> Self {
321        PropValue::Bool(b)
322    }
323}
324
325/// A composed component that combines two components.
326///
327/// The state space is the geometric product of the child states.
328pub struct ComposedComponent<A, B>
329where
330    A: Component,
331    B: Component,
332{
333    /// First component
334    pub a: A,
335    /// Second component
336    pub b: B,
337    /// How to split state between components
338    pub split: StateSplit,
339}
340
341/// How to split geometric state between composed components.
342#[derive(Debug, Clone, Copy, PartialEq, Eq)]
343pub enum StateSplit {
344    /// Both components share the same state
345    Shared,
346    /// Split by grade: A gets scalar+vector, B gets bivector+pseudoscalar
347    ByGrade,
348    /// A gets first 4 coefficients, B gets last 4
349    ByCoefficient,
350}
351
352impl<A, B> ComposedComponent<A, B>
353where
354    A: Component,
355    B: Component,
356{
357    /// Create a new composed component with shared state.
358    pub fn new(a: A, b: B) -> Self {
359        Self {
360            a,
361            b,
362            split: StateSplit::Shared,
363        }
364    }
365
366    /// Create with specified state split.
367    pub fn with_split(a: A, b: B, split: StateSplit) -> Self {
368        Self { a, b, split }
369    }
370}
371
372impl<A, B> Component for ComposedComponent<A, B>
373where
374    A: Component,
375    B: Component,
376{
377    fn render(&self, state: &GA3) -> Element {
378        let (state_a, state_b) = match self.split {
379            StateSplit::Shared => (state.clone(), state.clone()),
380            StateSplit::ByGrade => {
381                // A gets grades 0,1 (scalar + vector)
382                // B gets grades 2,3 (bivector + pseudoscalar)
383                let coeffs = state.as_slice();
384                let a_coeffs = vec![
385                    coeffs[0], coeffs[1], coeffs[2], coeffs[3], 0.0, 0.0, 0.0, 0.0,
386                ];
387                let b_coeffs = vec![
388                    0.0, 0.0, 0.0, 0.0, coeffs[4], coeffs[5], coeffs[6], coeffs[7],
389                ];
390                (GA3::from_slice(&a_coeffs), GA3::from_slice(&b_coeffs))
391            }
392            StateSplit::ByCoefficient => {
393                let coeffs = state.as_slice();
394                let a_coeffs = vec![
395                    coeffs[0], coeffs[1], coeffs[2], coeffs[3], 0.0, 0.0, 0.0, 0.0,
396                ];
397                let b_coeffs = vec![
398                    coeffs[4], coeffs[5], coeffs[6], coeffs[7], 0.0, 0.0, 0.0, 0.0,
399                ];
400                (GA3::from_slice(&a_coeffs), GA3::from_slice(&b_coeffs))
401            }
402        };
403
404        let elem_a = self.a.render(&state_a);
405        let elem_b = self.b.render(&state_b);
406
407        Element::fragment(vec![elem_a, elem_b])
408    }
409
410    fn initial_state(&self) -> GA3 {
411        // Combine initial states
412        let a_init = self.a.initial_state();
413        let b_init = self.b.initial_state();
414
415        match self.split {
416            StateSplit::Shared => a_init, // Use A's initial state
417            StateSplit::ByGrade | StateSplit::ByCoefficient => {
418                // Combine: A in first 4, B in last 4
419                let a_coeffs = a_init.as_slice();
420                let b_coeffs = b_init.as_slice();
421                GA3::from_slice(&[
422                    a_coeffs[0],
423                    a_coeffs[1],
424                    a_coeffs[2],
425                    a_coeffs[3],
426                    b_coeffs[0],
427                    b_coeffs[1],
428                    b_coeffs[2],
429                    b_coeffs[3],
430                ])
431            }
432        }
433    }
434}
435
436/// Compose two components with shared state.
437pub fn compose<A, B>(a: A, b: B) -> ComposedComponent<A, B>
438where
439    A: Component,
440    B: Component,
441{
442    ComposedComponent::new(a, b)
443}
444
445/// A function component (simple render function).
446pub struct FnComponent<F>
447where
448    F: Fn(&GA3) -> Element + Send + Sync,
449{
450    render_fn: F,
451    initial: GA3,
452}
453
454impl<F> FnComponent<F>
455where
456    F: Fn(&GA3) -> Element + Send + Sync,
457{
458    /// Create a new function component.
459    pub fn new(render_fn: F) -> Self {
460        Self {
461            render_fn,
462            initial: GA3::zero(),
463        }
464    }
465
466    /// Create with initial state.
467    pub fn with_initial(render_fn: F, initial: GA3) -> Self {
468        Self { render_fn, initial }
469    }
470}
471
472impl<F> Component for FnComponent<F>
473where
474    F: Fn(&GA3) -> Element + Send + Sync,
475{
476    fn render(&self, state: &GA3) -> Element {
477        (self.render_fn)(state)
478    }
479
480    fn initial_state(&self) -> GA3 {
481        self.initial.clone()
482    }
483}
484
485/// Create a function component from a closure.
486pub fn component<F>(render_fn: F) -> FnComponent<F>
487where
488    F: Fn(&GA3) -> Element + Send + Sync,
489{
490    FnComponent::new(render_fn)
491}
492
493#[cfg(test)]
494mod tests {
495    use super::*;
496
497    #[test]
498    fn test_element_creation() {
499        let elem = Element::tag("div")
500            .attr("class", "container")
501            .child(Element::text("Hello"));
502
503        assert!(matches!(elem.kind, ElementKind::Tag(ref t) if t == "div"));
504        assert_eq!(elem.children.len(), 1);
505        assert!(elem.props.has("class"));
506    }
507
508    #[test]
509    fn test_element_text() {
510        let elem = Element::text("Hello, World!");
511
512        if let ElementKind::Text(content) = &elem.kind {
513            assert_eq!(content, "Hello, World!");
514        } else {
515            panic!("Expected text element");
516        }
517    }
518
519    #[test]
520    fn test_element_fragment() {
521        let frag = Element::fragment(vec![Element::text("A"), Element::text("B")]);
522
523        assert!(matches!(frag.kind, ElementKind::Fragment));
524        assert_eq!(frag.children.len(), 2);
525    }
526
527    #[test]
528    fn test_element_node_count() {
529        let elem = Element::tag("div")
530            .child(Element::tag("span").child(Element::text("Hello")))
531            .child(Element::text("World"));
532
533        // div(1) + span(1) + text(1) + text(1) = 4
534        assert_eq!(elem.node_count(), 4);
535    }
536
537    #[test]
538    fn test_props() {
539        let mut props = Props::new();
540        props.set("name", PropValue::String("test".into()));
541        props.set("count", PropValue::Number(42.0));
542        props.set("enabled", PropValue::Bool(true));
543
544        assert_eq!(props.get("name").unwrap().as_str(), Some("test"));
545        assert_eq!(props.get("count").unwrap().as_f64(), Some(42.0));
546        assert_eq!(props.get("enabled").unwrap().as_bool(), Some(true));
547    }
548
549    #[test]
550    fn test_fn_component() {
551        let counter = component(|state: &GA3| {
552            let count = state.get(0) as i32;
553            Element::text(format!("Count: {}", count))
554        });
555
556        let elem = counter.render(&GA3::scalar(5.0));
557
558        if let ElementKind::Text(content) = &elem.kind {
559            assert_eq!(content, "Count: 5");
560        } else {
561            panic!("Expected text element");
562        }
563    }
564
565    #[test]
566    fn test_composed_component() {
567        let a = component(|state: &GA3| Element::text(format!("A: {}", state.get(0) as i32)));
568
569        let b = component(|state: &GA3| Element::text(format!("B: {}", state.get(0) as i32)));
570
571        let composed = compose(a, b);
572        let elem = composed.render(&GA3::scalar(10.0));
573
574        assert!(matches!(elem.kind, ElementKind::Fragment));
575        assert_eq!(elem.children.len(), 2);
576    }
577
578    #[test]
579    fn test_prop_value_conversions() {
580        let s: PropValue = "hello".into();
581        assert!(matches!(s, PropValue::String(_)));
582
583        let n: PropValue = 42.0_f64.into();
584        assert!(matches!(n, PropValue::Number(_)));
585
586        let b: PropValue = true.into();
587        assert!(matches!(b, PropValue::Bool(_)));
588
589        let i: PropValue = 123_i32.into();
590        assert!(matches!(i, PropValue::Number(_)));
591    }
592
593    #[test]
594    fn test_element_empty() {
595        let elem = Element::empty();
596        assert!(elem.is_empty());
597    }
598
599    #[test]
600    fn test_element_with_key() {
601        let elem = Element::tag("li").with_key("item-1");
602        assert_eq!(elem.key, Some("item-1".to_string()));
603    }
604
605    #[test]
606    fn test_props_merge() {
607        let mut props1 = Props::new();
608        props1.set("a", PropValue::Number(1.0));
609        props1.set("b", PropValue::Number(2.0));
610
611        let mut props2 = Props::new();
612        props2.set("b", PropValue::Number(3.0));
613        props2.set("c", PropValue::Number(4.0));
614
615        props1.merge(props2);
616
617        assert_eq!(props1.get("a").unwrap().as_f64(), Some(1.0));
618        assert_eq!(props1.get("b").unwrap().as_f64(), Some(3.0)); // Overwritten
619        assert_eq!(props1.get("c").unwrap().as_f64(), Some(4.0));
620    }
621}