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