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; #[derive(Clone)]
21pub struct DynamicTextSource(Rc<dyn Fn() -> crate::text::AnnotatedString>);
22
23impl DynamicTextSource {
24 pub fn new<F>(resolver: F) -> Self
25 where
26 F: Fn() -> crate::text::AnnotatedString + 'static,
27 {
28 Self(Rc::new(resolver))
29 }
30
31 fn resolve(&self) -> 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(crate::text::AnnotatedString),
45 Dynamic(DynamicTextSource),
46}
47
48impl TextSource {
49 fn resolve(&self) -> 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(crate::text::AnnotatedString::from(self))
64 }
65}
66
67impl IntoTextSource for &str {
68 fn into_text_source(self) -> TextSource {
69 TextSource::Static(crate::text::AnnotatedString::from(self))
70 }
71}
72
73impl IntoTextSource for crate::text::AnnotatedString {
74 fn into_text_source(self) -> TextSource {
75 TextSource::Static(self)
76 }
77}
78
79impl<T> IntoTextSource for State<T>
80where
81 T: ToString + Clone + 'static,
82{
83 fn into_text_source(self) -> TextSource {
84 let state = self;
85 TextSource::Dynamic(DynamicTextSource::new(move || {
86 crate::text::AnnotatedString::from(state.value().to_string())
87 }))
88 }
89}
90
91impl<T> IntoTextSource for MutableState<T>
92where
93 T: ToString + Clone + 'static,
94{
95 fn into_text_source(self) -> TextSource {
96 let state = self;
97 TextSource::Dynamic(DynamicTextSource::new(move || {
98 crate::text::AnnotatedString::from(state.value().to_string())
99 }))
100 }
101}
102
103impl<F> IntoTextSource for F
104where
105 F: Fn() -> String + 'static,
106{
107 fn into_text_source(self) -> TextSource {
108 TextSource::Dynamic(DynamicTextSource::new(move || {
109 crate::text::AnnotatedString::from(self())
110 }))
111 }
112}
113
114impl IntoTextSource for DynamicTextSource {
115 fn into_text_source(self) -> TextSource {
116 TextSource::Dynamic(self)
117 }
118}
119
120#[composable]
138pub fn BasicText<S>(
139 text: S,
140 modifier: Modifier,
141 style: TextStyle,
142 overflow: TextOverflow,
143 soft_wrap: bool,
144 max_lines: usize,
145 min_lines: usize,
146) -> NodeId
147where
148 S: IntoTextSource + Clone + PartialEq + 'static,
149{
150 let current = text.into_text_source().resolve();
151
152 let options = TextLayoutOptions {
153 overflow,
154 soft_wrap,
155 max_lines,
156 min_lines,
157 }
158 .normalized();
159
160 let text_element = modifier_element(TextModifierElement::new(current, style, options));
163 let final_modifier = Modifier::from_parts(vec![text_element]);
164 let combined_modifier = modifier.then(final_modifier);
165
166 Layout(
168 combined_modifier,
169 EmptyMeasurePolicy,
170 || {}, )
172}
173
174#[composable]
175pub fn Text<S>(value: S, modifier: Modifier, style: TextStyle) -> NodeId
176where
177 S: IntoTextSource + Clone + PartialEq + 'static,
178{
179 BasicText(
180 value,
181 modifier,
182 style,
183 TextOverflow::Clip,
184 true,
185 usize::MAX,
186 1,
187 )
188}
189
190#[cfg(test)]
191mod tests {
192 use super::*;
193 use crate::run_test_composition;
194
195 #[test]
196 fn basic_text_creates_node() {
197 let composition = run_test_composition(|| {
198 BasicText(
199 "Hello",
200 Modifier::empty(),
201 TextStyle::default(),
202 TextOverflow::Clip,
203 true,
204 usize::MAX,
205 1,
206 );
207 });
208
209 assert!(composition.root().is_some());
210 }
211}