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::TextStyle;
14use crate::text_modifier_node::TextModifierElement;
15use crate::widgets::Layout;
16use cranpose_core::{MutableState, NodeId, State};
17use cranpose_foundation::modifier_element;
18use std::rc::Rc; // Added Rc import
19
20#[derive(Clone)]
21pub struct DynamicTextSource(Rc<dyn Fn() -> Rc<str>>);
22
23impl DynamicTextSource {
24    pub fn new<F>(resolver: F) -> Self
25    where
26        F: Fn() -> Rc<str> + 'static,
27    {
28        Self(Rc::new(resolver))
29    }
30
31    fn resolve(&self) -> Rc<str> {
32        (self.0)()
33    }
34}
35
36impl PartialEq for DynamicTextSource {
37    fn eq(&self, other: &Self) -> bool {
38        Rc::ptr_eq(&self.0, &other.0)
39    }
40}
41
42impl Eq for DynamicTextSource {}
43
44#[derive(Clone, PartialEq, Eq)]
45enum TextSource {
46    Static(Rc<str>),
47    Dynamic(DynamicTextSource),
48}
49
50impl TextSource {
51    fn resolve(&self) -> Rc<str> {
52        match self {
53            TextSource::Static(text) => text.clone(),
54            TextSource::Dynamic(dynamic) => dynamic.resolve(),
55        }
56    }
57}
58
59trait IntoTextSource {
60    fn into_text_source(self) -> TextSource;
61}
62
63impl IntoTextSource for String {
64    fn into_text_source(self) -> TextSource {
65        TextSource::Static(Rc::from(self))
66    }
67}
68
69impl IntoTextSource for &str {
70    fn into_text_source(self) -> TextSource {
71        TextSource::Static(Rc::from(self))
72    }
73}
74
75impl<T> IntoTextSource for State<T>
76where
77    T: ToString + Clone + 'static,
78{
79    fn into_text_source(self) -> TextSource {
80        let state = self;
81        TextSource::Dynamic(DynamicTextSource::new(move || {
82            Rc::from(state.value().to_string())
83        }))
84    }
85}
86
87impl<T> IntoTextSource for MutableState<T>
88where
89    T: ToString + Clone + 'static,
90{
91    fn into_text_source(self) -> TextSource {
92        let state = self;
93        TextSource::Dynamic(DynamicTextSource::new(move || {
94            Rc::from(state.value().to_string())
95        }))
96    }
97}
98
99impl<F> IntoTextSource for F
100where
101    F: Fn() -> String + 'static,
102{
103    fn into_text_source(self) -> TextSource {
104        TextSource::Dynamic(DynamicTextSource::new(move || Rc::from(self())))
105    }
106}
107
108impl IntoTextSource for DynamicTextSource {
109    fn into_text_source(self) -> TextSource {
110        TextSource::Dynamic(self)
111    }
112}
113
114/// High-level element that displays text.
115///
116/// # When to use
117/// Use this widget to display read-only text on the screen. For editable text,
118/// use [`BasicTextField`](crate::widgets::BasicTextField).
119///
120/// # Arguments
121///
122/// * `value` - The string to display. Can be a `&str`, `String`, or `State<String>`.
123/// * `modifier` - Modifiers to apply (e.g., padding, background, layout instructions).
124/// * `style` - Text styling (color, font size).
125///
126/// # Example
127///
128/// ```rust,ignore
129/// Text("Hello World", Modifier::padding(16.0), TextStyle::default());
130/// ```
131#[composable]
132pub fn Text<S>(value: S, modifier: Modifier, style: TextStyle) -> NodeId
133where
134    S: IntoTextSource + Clone + PartialEq + 'static,
135{
136    let current = value.into_text_source().resolve();
137
138    // Create a text modifier element that will add TextModifierNode to the chain
139    // TextModifierNode handles measurement, drawing, and semantics
140    let text_element = modifier_element(TextModifierElement::new(current, style));
141    let final_modifier = Modifier::from_parts(vec![text_element]);
142    let combined_modifier = modifier.then(final_modifier);
143
144    // text_modifier is inclusive of layout effects
145    Layout(
146        combined_modifier,
147        EmptyMeasurePolicy,
148        || {}, // No children
149    )
150}