1use std::{rc::Rc, sync::Arc};
9
10use cranpose_core::{MutableState, NodeId, State};
11use cranpose_foundation::modifier_element;
12
13use crate::{
14 modifier::Modifier,
15 text::{TextLayoutOptions, TextOptions, TextOverflow, TextStyle},
16 text_modifier_node::{TextModifierElement, shared_text_style},
17 widgets::layout::compose_empty_layout,
18};
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)]
43pub enum 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
57#[doc(hidden)]
58pub trait 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::new(crate::text::AnnotatedString::from(self)))
65 }
66}
67
68impl IntoTextSource for &str {
69 fn into_text_source(self) -> TextSource {
70 TextSource::Static(Rc::new(crate::text::AnnotatedString::from(self)))
71 }
72}
73
74impl IntoTextSource for crate::text::AnnotatedString {
75 fn into_text_source(self) -> TextSource {
76 TextSource::Static(Rc::new(self))
77 }
78}
79
80impl IntoTextSource for Rc<crate::text::AnnotatedString> {
81 fn into_text_source(self) -> TextSource {
82 TextSource::Static(self)
83 }
84}
85
86impl<T> IntoTextSource for State<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::new(crate::text::AnnotatedString::from(
94 state.value().to_string(),
95 ))
96 }))
97 }
98}
99
100impl<T> IntoTextSource for MutableState<T>
101where
102 T: ToString + Clone + 'static,
103{
104 fn into_text_source(self) -> TextSource {
105 let state = self;
106 TextSource::Dynamic(DynamicTextSource::new(move || {
107 Rc::new(crate::text::AnnotatedString::from(
108 state.value().to_string(),
109 ))
110 }))
111 }
112}
113
114impl<F> IntoTextSource for F
115where
116 F: Fn() -> String + 'static,
117{
118 fn into_text_source(self) -> TextSource {
119 TextSource::Dynamic(DynamicTextSource::new(move || {
120 Rc::new(crate::text::AnnotatedString::from(self()))
121 }))
122 }
123}
124
125impl IntoTextSource for DynamicTextSource {
126 fn into_text_source(self) -> TextSource {
127 TextSource::Dynamic(self)
128 }
129}
130
131fn compose_basic_text_group(
132 text: TextSource,
133 modifier: Modifier,
134 style: Arc<TextStyle>,
135 options: TextLayoutOptions,
136) -> NodeId {
137 #[cfg(feature = "localization")]
138 let style = crate::localization::apply_shared_text_locale(style);
139 let current = text.resolve();
140
141 let options = options.normalized();
142
143 let modifier = match crate::widgets::local_selection_registrar().current() {
144 Some(registrar) => modifier.then(crate::widgets::selection_container::selectable_text(
145 registrar,
146 Rc::clone(¤t),
147 TextStyle::clone(&style),
148 options,
149 )),
150 None => modifier,
151 };
152 let density = crate::density::density();
153 let text_element = modifier_element(TextModifierElement::with_shared_style(
154 current, style, options, density,
155 ));
156 let final_modifier = Modifier::from_parts(&[text_element]);
157 let combined_modifier = modifier.then(final_modifier);
158
159 compose_empty_layout(combined_modifier, density)
160}
161
162mod shared_style {
165 use std::sync::Arc;
166
167 use cranpose_core::NodeId;
168
169 use super::{IntoTextSource, compose_basic_text_group};
170 use crate::{
171 composable,
172 modifier::Modifier,
173 text::{TextLayoutOptions, TextStyle},
174 };
175
176 #[composable]
180 pub(super) fn Text<S>(
181 text: S,
182 modifier: Modifier,
183 style: Arc<TextStyle>,
184 options: TextLayoutOptions,
185 ) -> NodeId
186 where
187 S: IntoTextSource + Clone + PartialEq + 'static,
188 {
189 compose_basic_text_group(text.into_text_source(), modifier, style, options)
190 }
191}
192
193#[track_caller]
194#[expect(non_snake_case)]
195pub fn BasicTextWithOptions<S>(
196 text: S,
197 modifier: Modifier,
198 style: TextStyle,
199 options: TextLayoutOptions,
200) -> NodeId
201where
202 S: IntoTextSource + Clone + PartialEq + 'static,
203{
204 shared_style::Text(text, modifier, shared_text_style(style), options)
205}
206
207#[track_caller]
208#[expect(non_snake_case)]
209pub fn BasicText<S>(
210 text: S,
211 modifier: Modifier,
212 style: TextStyle,
213 overflow: TextOverflow,
214 soft_wrap: bool,
215 max_lines: usize,
216 min_lines: usize,
217) -> NodeId
218where
219 S: IntoTextSource + Clone + PartialEq + 'static,
220{
221 shared_style::Text(
222 text,
223 modifier,
224 shared_text_style(style),
225 TextLayoutOptions {
226 overflow,
227 soft_wrap,
228 max_lines,
229 min_lines,
230 },
231 )
232}
233
234#[track_caller]
235#[expect(non_snake_case)]
236pub fn TextWithOptions<S>(
237 value: S,
238 modifier: Modifier,
239 style: TextStyle,
240 options: TextOptions,
241) -> NodeId
242where
243 S: IntoTextSource + Clone + PartialEq + 'static,
244{
245 shared_style::Text(
246 value,
247 modifier,
248 shared_text_style(style),
249 TextLayoutOptions::from(options),
250 )
251}
252
253#[track_caller]
271#[expect(non_snake_case)]
272pub fn Text<S>(value: S, modifier: Modifier, style: TextStyle) -> NodeId
273where
274 S: IntoTextSource + Clone + PartialEq + 'static,
275{
276 shared_style::Text(
277 value,
278 modifier,
279 shared_text_style(style),
280 TextLayoutOptions::from(TextOptions::default()),
281 )
282}
283
284#[cfg(test)]
285#[path = "tests/text_tests.rs"]
286mod tests;