cranpose_ui/widgets/
text.rs1use std::rc::Rc;
9
10use cranpose_core::{MutableState, NodeId, State};
11use cranpose_foundation::modifier_element;
12
13use crate::{
14 composable,
15 layout::policies::EmptyMeasurePolicy,
16 modifier::Modifier,
17 text::{TextLayoutOptions, TextOptions, TextOverflow, TextStyle},
18 text_modifier_node::TextModifierElement,
19 widgets::layout::compose_layout,
20};
21
22#[derive(Clone)]
23pub struct DynamicTextSource(Rc<dyn Fn() -> Rc<crate::text::AnnotatedString>>);
24
25impl DynamicTextSource {
26 pub fn new<F>(resolver: F) -> Self
27 where
28 F: Fn() -> Rc<crate::text::AnnotatedString> + 'static,
29 {
30 Self(Rc::new(resolver))
31 }
32
33 fn resolve(&self) -> Rc<crate::text::AnnotatedString> {
34 (self.0)()
35 }
36}
37
38impl PartialEq for DynamicTextSource {
39 fn eq(&self, other: &Self) -> bool {
40 Rc::ptr_eq(&self.0, &other.0)
41 }
42}
43
44#[derive(Clone, PartialEq)]
45pub enum TextSource {
46 Static(Rc<crate::text::AnnotatedString>),
47 Dynamic(DynamicTextSource),
48}
49
50impl TextSource {
51 fn resolve(&self) -> Rc<crate::text::AnnotatedString> {
52 match self {
53 TextSource::Static(text) => text.clone(),
54 TextSource::Dynamic(dynamic) => dynamic.resolve(),
55 }
56 }
57}
58
59#[doc(hidden)]
60pub trait IntoTextSource {
61 fn into_text_source(self) -> TextSource;
62}
63
64impl IntoTextSource for String {
65 fn into_text_source(self) -> TextSource {
66 TextSource::Static(Rc::new(crate::text::AnnotatedString::from(self)))
67 }
68}
69
70impl IntoTextSource for &str {
71 fn into_text_source(self) -> TextSource {
72 TextSource::Static(Rc::new(crate::text::AnnotatedString::from(self)))
73 }
74}
75
76impl IntoTextSource for crate::text::AnnotatedString {
77 fn into_text_source(self) -> TextSource {
78 TextSource::Static(Rc::new(self))
79 }
80}
81
82impl IntoTextSource for Rc<crate::text::AnnotatedString> {
83 fn into_text_source(self) -> TextSource {
84 TextSource::Static(self)
85 }
86}
87
88impl<T> IntoTextSource for State<T>
89where
90 T: ToString + Clone + 'static,
91{
92 fn into_text_source(self) -> TextSource {
93 let state = self;
94 TextSource::Dynamic(DynamicTextSource::new(move || {
95 Rc::new(crate::text::AnnotatedString::from(
96 state.value().to_string(),
97 ))
98 }))
99 }
100}
101
102impl<T> IntoTextSource for MutableState<T>
103where
104 T: ToString + Clone + 'static,
105{
106 fn into_text_source(self) -> TextSource {
107 let state = self;
108 TextSource::Dynamic(DynamicTextSource::new(move || {
109 Rc::new(crate::text::AnnotatedString::from(
110 state.value().to_string(),
111 ))
112 }))
113 }
114}
115
116impl<F> IntoTextSource for F
117where
118 F: Fn() -> String + 'static,
119{
120 fn into_text_source(self) -> TextSource {
121 TextSource::Dynamic(DynamicTextSource::new(move || {
122 Rc::new(crate::text::AnnotatedString::from(self()))
123 }))
124 }
125}
126
127impl IntoTextSource for DynamicTextSource {
128 fn into_text_source(self) -> TextSource {
129 TextSource::Dynamic(self)
130 }
131}
132
133fn compose_basic_text_group(
134 text: TextSource,
135 modifier: Modifier,
136 style: TextStyle,
137 options: TextLayoutOptions,
138) -> NodeId {
139 #[cfg(feature = "localization")]
140 let style = crate::localization::apply_text_locale(style);
141 let current = text.resolve();
142
143 let options = options.normalized();
144
145 let modifier = match crate::widgets::local_selection_registrar().current() {
146 Some(registrar) => modifier.then(crate::widgets::selection_container::selectable_text(
147 registrar,
148 Rc::clone(¤t),
149 style.clone(),
150 options,
151 )),
152 None => modifier,
153 };
154 let density = crate::density::density();
155 let text_element = modifier_element(TextModifierElement::new(current, style, options, density));
156 let final_modifier = Modifier::from_parts(&[text_element]);
157 let combined_modifier = modifier.then(final_modifier);
158
159 compose_layout(combined_modifier, EmptyMeasurePolicy, density, || {})
160}
161
162#[composable]
163pub fn BasicTextWithOptions<S>(
164 text: S,
165 modifier: Modifier,
166 style: TextStyle,
167 options: TextLayoutOptions,
168) -> NodeId
169where
170 S: IntoTextSource + Clone + PartialEq + 'static,
171{
172 compose_basic_text_group(text.into_text_source(), modifier, style, options)
173}
174
175#[composable]
176pub fn BasicText<S>(
177 text: S,
178 modifier: Modifier,
179 style: TextStyle,
180 overflow: TextOverflow,
181 soft_wrap: bool,
182 max_lines: usize,
183 min_lines: usize,
184) -> NodeId
185where
186 S: IntoTextSource + Clone + PartialEq + 'static,
187{
188 compose_basic_text_group(
189 text.into_text_source(),
190 modifier,
191 style,
192 TextLayoutOptions {
193 overflow,
194 soft_wrap,
195 max_lines,
196 min_lines,
197 },
198 )
199}
200
201#[composable]
202pub fn TextWithOptions<S>(
203 value: S,
204 modifier: Modifier,
205 style: TextStyle,
206 options: TextOptions,
207) -> NodeId
208where
209 S: IntoTextSource + Clone + PartialEq + 'static,
210{
211 compose_basic_text_group(
212 value.into_text_source(),
213 modifier,
214 style,
215 TextLayoutOptions::from(options),
216 )
217}
218
219#[composable]
237pub fn Text<S>(value: S, modifier: Modifier, style: TextStyle) -> NodeId
238where
239 S: IntoTextSource + Clone + PartialEq + 'static,
240{
241 compose_basic_text_group(
242 value.into_text_source(),
243 modifier,
244 style,
245 TextLayoutOptions::from(TextOptions::default()),
246 )
247}
248
249#[cfg(test)]
250#[path = "tests/text_tests.rs"]
251mod tests;