cranpose_ui/widgets/
text.rs1#![allow(non_snake_case)]
9
10use crate::composable;
11use crate::layout::policies::EmptyMeasurePolicy;
12use crate::modifier::Modifier;
13use crate::text::{TextLayoutOptions, TextOverflow, 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;
19
20#[derive(Clone)]
21pub struct DynamicTextSource(Rc<dyn Fn() -> Rc<crate::text::AnnotatedString>>);
22
23impl DynamicTextSource {
24 pub fn new<F>(resolver: F) -> Self
25 where
26 F: Fn() -> Rc<crate::text::AnnotatedString> + 'static,
27 {
28 Self(Rc::new(resolver))
29 }
30
31 fn resolve(&self) -> Rc<crate::text::AnnotatedString> {
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
42#[derive(Clone, PartialEq)]
43enum TextSource {
44 Static(Rc<crate::text::AnnotatedString>),
45 Dynamic(DynamicTextSource),
46}
47
48impl TextSource {
49 fn resolve(&self) -> Rc<crate::text::AnnotatedString> {
50 match self {
51 TextSource::Static(text) => text.clone(),
52 TextSource::Dynamic(dynamic) => dynamic.resolve(),
53 }
54 }
55}
56
57trait IntoTextSource {
58 fn into_text_source(self) -> TextSource;
59}
60
61impl IntoTextSource for String {
62 fn into_text_source(self) -> TextSource {
63 TextSource::Static(Rc::new(crate::text::AnnotatedString::from(self)))
64 }
65}
66
67impl IntoTextSource for &str {
68 fn into_text_source(self) -> TextSource {
69 TextSource::Static(Rc::new(crate::text::AnnotatedString::from(self)))
70 }
71}
72
73impl IntoTextSource for crate::text::AnnotatedString {
74 fn into_text_source(self) -> TextSource {
75 TextSource::Static(Rc::new(self))
76 }
77}
78
79impl IntoTextSource for Rc<crate::text::AnnotatedString> {
80 fn into_text_source(self) -> TextSource {
81 TextSource::Static(self)
82 }
83}
84
85impl<T> IntoTextSource for State<T>
86where
87 T: ToString + Clone + 'static,
88{
89 fn into_text_source(self) -> TextSource {
90 let state = self;
91 TextSource::Dynamic(DynamicTextSource::new(move || {
92 Rc::new(crate::text::AnnotatedString::from(
93 state.value().to_string(),
94 ))
95 }))
96 }
97}
98
99impl<T> IntoTextSource for MutableState<T>
100where
101 T: ToString + Clone + 'static,
102{
103 fn into_text_source(self) -> TextSource {
104 let state = self;
105 TextSource::Dynamic(DynamicTextSource::new(move || {
106 Rc::new(crate::text::AnnotatedString::from(
107 state.value().to_string(),
108 ))
109 }))
110 }
111}
112
113impl<F> IntoTextSource for F
114where
115 F: Fn() -> String + 'static,
116{
117 fn into_text_source(self) -> TextSource {
118 TextSource::Dynamic(DynamicTextSource::new(move || {
119 Rc::new(crate::text::AnnotatedString::from(self()))
120 }))
121 }
122}
123
124impl IntoTextSource for DynamicTextSource {
125 fn into_text_source(self) -> TextSource {
126 TextSource::Dynamic(self)
127 }
128}
129
130#[composable]
148pub fn BasicText<S>(
149 text: S,
150 modifier: Modifier,
151 style: TextStyle,
152 overflow: TextOverflow,
153 soft_wrap: bool,
154 max_lines: usize,
155 min_lines: usize,
156) -> NodeId
157where
158 S: IntoTextSource + Clone + PartialEq + 'static,
159{
160 let current = text.into_text_source().resolve();
161
162 let options = TextLayoutOptions {
163 overflow,
164 soft_wrap,
165 max_lines,
166 min_lines,
167 }
168 .normalized();
169
170 let text_element = modifier_element(TextModifierElement::new(current, style, options));
173 let final_modifier = Modifier::from_parts(vec![text_element]);
174 let combined_modifier = modifier.then(final_modifier);
175
176 Layout(
178 combined_modifier,
179 EmptyMeasurePolicy,
180 || {}, )
182}
183
184#[composable]
185pub fn Text<S>(value: S, modifier: Modifier, style: TextStyle) -> NodeId
186where
187 S: IntoTextSource + Clone + PartialEq + 'static,
188{
189 BasicText(
190 value,
191 modifier,
192 style,
193 TextOverflow::Clip,
194 true,
195 usize::MAX,
196 1,
197 )
198}
199
200#[cfg(test)]
201mod tests {
202 use super::*;
203 use crate::run_test_composition;
204
205 #[test]
206 fn basic_text_creates_node() {
207 let composition = run_test_composition(|| {
208 BasicText(
209 "Hello",
210 Modifier::empty(),
211 TextStyle::default(),
212 TextOverflow::Clip,
213 true,
214 usize::MAX,
215 1,
216 );
217 });
218
219 assert!(composition.root().is_some());
220 }
221}