Skip to main content

cranpose_ui/widgets/
text.rs

1//! Text widget implementation
2//!
3//! This implementation follows Jetpack Compose's BasicText architecture where text content
4//! is implemented as a modifier node rather than as a measure policy. This properly separates
5//! concerns: MeasurePolicy handles child layout, while TextModifierNode handles text content
6//! measurement, drawing, and semantics.
7
8#![allow(non_snake_case)]
9
10use crate::composable;
11use crate::layout::policies::EmptyMeasurePolicy;
12use crate::modifier::Modifier;
13use crate::text_modifier_node::TextModifierElement;
14use crate::widgets::Layout;
15use cranpose_core::{MutableState, NodeId, State};
16use cranpose_foundation::modifier_element;
17use std::rc::Rc;
18
19#[derive(Clone)]
20pub struct DynamicTextSource(Rc<dyn Fn() -> Rc<str>>);
21
22impl DynamicTextSource {
23    pub fn new<F>(resolver: F) -> Self
24    where
25        F: Fn() -> Rc<str> + 'static,
26    {
27        Self(Rc::new(resolver))
28    }
29
30    fn resolve(&self) -> Rc<str> {
31        (self.0)()
32    }
33}
34
35impl PartialEq for DynamicTextSource {
36    fn eq(&self, other: &Self) -> bool {
37        Rc::ptr_eq(&self.0, &other.0)
38    }
39}
40
41impl Eq for DynamicTextSource {}
42
43#[derive(Clone, PartialEq, Eq)]
44enum TextSource {
45    Static(Rc<str>),
46    Dynamic(DynamicTextSource),
47}
48
49impl TextSource {
50    fn resolve(&self) -> Rc<str> {
51        match self {
52            TextSource::Static(text) => text.clone(),
53            TextSource::Dynamic(dynamic) => dynamic.resolve(),
54        }
55    }
56}
57
58trait IntoTextSource {
59    fn into_text_source(self) -> TextSource;
60}
61
62impl IntoTextSource for String {
63    fn into_text_source(self) -> TextSource {
64        TextSource::Static(Rc::from(self))
65    }
66}
67
68impl IntoTextSource for &str {
69    fn into_text_source(self) -> TextSource {
70        TextSource::Static(Rc::from(self))
71    }
72}
73
74impl<T> IntoTextSource for State<T>
75where
76    T: ToString + Clone + 'static,
77{
78    fn into_text_source(self) -> TextSource {
79        let state = self;
80        TextSource::Dynamic(DynamicTextSource::new(move || {
81            Rc::from(state.value().to_string())
82        }))
83    }
84}
85
86impl<T> IntoTextSource for MutableState<T>
87where
88    T: ToString + Clone + 'static,
89{
90    fn into_text_source(self) -> TextSource {
91        let state = self;
92        TextSource::Dynamic(DynamicTextSource::new(move || {
93            Rc::from(state.value().to_string())
94        }))
95    }
96}
97
98impl<F> IntoTextSource for F
99where
100    F: Fn() -> String + 'static,
101{
102    fn into_text_source(self) -> TextSource {
103        TextSource::Dynamic(DynamicTextSource::new(move || Rc::from(self())))
104    }
105}
106
107impl IntoTextSource for DynamicTextSource {
108    fn into_text_source(self) -> TextSource {
109        TextSource::Dynamic(self)
110    }
111}
112
113/// Creates a text widget displaying the specified content.
114///
115/// # Architecture
116///
117/// Following Jetpack Compose's BasicText pattern, this implementation uses:
118/// - **TextModifierElement**: Adds text content as a modifier node
119/// - **EmptyMeasurePolicy**: Delegates all measurement to modifier nodes
120///
121/// This matches Kotlin's pattern:
122/// ```kotlin
123/// Layout(modifier.then(TextStringSimpleElement(...)), EmptyMeasurePolicy)
124/// ```
125///
126/// Text content lives in the modifier node (TextModifierNode), not in the measure policy,
127/// which properly separates layout policy (child arrangement) from content rendering (text).
128#[composable]
129pub fn Text<S>(value: S, modifier: Modifier) -> NodeId
130where
131    S: IntoTextSource + Clone + PartialEq + 'static,
132{
133    let current = value.into_text_source().resolve();
134
135    // Create a text modifier element that will add TextModifierNode to the chain
136    // TextModifierNode handles measurement, drawing, and semantics
137    let text_element = modifier_element(TextModifierElement::new(current));
138    let final_modifier = Modifier::from_parts(vec![text_element]);
139    let combined_modifier = modifier.then(final_modifier);
140
141    // Use EmptyMeasurePolicy - TextModifierNode handles all measurement via LayoutModifierNode::measure()
142    // This matches Jetpack Compose's BasicText architecture where TextStringSimpleNode provides measurement
143    Layout(
144        combined_modifier,
145        EmptyMeasurePolicy,
146        || {}, // No children
147    )
148}