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/// High-level element that displays text.
114///
115/// # When to use
116/// Use this widget to display read-only text on the screen. For editable text,
117/// use [`BasicTextField`](crate::widgets::BasicTextField).
118///
119/// # Arguments
120///
121/// * `value` - The string to display. Can be a `&str`, `String`, or `State<String>`.
122/// * `modifier` - Modifiers to apply (e.g., padding, background, layout instructions).
123///   Note: Text styling (color, font size) is typically applied via the
124///   `text_style` modifier (coming soon) or specific style modifiers.
125///
126/// # Example
127///
128/// ```rust,ignore
129/// Text("Hello World", Modifier::padding(16.0));
130/// ```
131#[composable]
132pub fn Text<S>(value: S, modifier: Modifier) -> 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));
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}