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() -> String>);
21
22impl DynamicTextSource {
23    pub fn new<F>(resolver: F) -> Self
24    where
25        F: Fn() -> String + 'static,
26    {
27        Self(Rc::new(resolver))
28    }
29
30    fn resolve(&self) -> String {
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(String),
46    Dynamic(DynamicTextSource),
47}
48
49impl TextSource {
50    fn resolve(&self) -> String {
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(self)
65    }
66}
67
68impl IntoTextSource for &str {
69    fn into_text_source(self) -> TextSource {
70        TextSource::Static(self.to_string())
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 || state.value().to_string()))
81    }
82}
83
84impl<T> IntoTextSource for MutableState<T>
85where
86    T: ToString + Clone + 'static,
87{
88    fn into_text_source(self) -> TextSource {
89        let state = self;
90        TextSource::Dynamic(DynamicTextSource::new(move || state.value().to_string()))
91    }
92}
93
94impl<F> IntoTextSource for F
95where
96    F: Fn() -> String + 'static,
97{
98    fn into_text_source(self) -> TextSource {
99        TextSource::Dynamic(DynamicTextSource::new(self))
100    }
101}
102
103impl IntoTextSource for DynamicTextSource {
104    fn into_text_source(self) -> TextSource {
105        TextSource::Dynamic(self)
106    }
107}
108
109/// Creates a text widget displaying the specified content.
110///
111/// # Architecture
112///
113/// Following Jetpack Compose's BasicText pattern, this implementation uses:
114/// - **TextModifierElement**: Adds text content as a modifier node
115/// - **EmptyMeasurePolicy**: Delegates all measurement to modifier nodes
116///
117/// This matches Kotlin's pattern:
118/// ```kotlin
119/// Layout(modifier.then(TextStringSimpleElement(...)), EmptyMeasurePolicy)
120/// ```
121///
122/// Text content lives in the modifier node (TextModifierNode), not in the measure policy,
123/// which properly separates layout policy (child arrangement) from content rendering (text).
124#[composable]
125pub fn Text<S>(value: S, modifier: Modifier) -> NodeId
126where
127    S: IntoTextSource + Clone + PartialEq + 'static,
128{
129    let current = value.into_text_source().resolve();
130
131    // Create a text modifier element that will add TextModifierNode to the chain
132    // TextModifierNode handles measurement, drawing, and semantics
133    let text_element = modifier_element(TextModifierElement::new(current.clone()));
134    let final_modifier = Modifier::from_parts(vec![text_element]);
135    let combined_modifier = modifier.then(final_modifier);
136
137    // Use EmptyMeasurePolicy - TextModifierNode handles all measurement via LayoutModifierNode::measure()
138    // This matches Jetpack Compose's BasicText architecture where TextStringSimpleNode provides measurement
139    Layout(
140        combined_modifier,
141        EmptyMeasurePolicy,
142        || {}, // No children
143    )
144}