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::{LayoutEnvironment, compose_empty_layout},
18};
19
20#[derive(Clone)]
23pub struct DynamicTextSource(Rc<dyn Fn() -> Rc<crate::text::AnnotatedString>>);
24
25impl DynamicTextSource {
26 pub fn new<F>(resolver: F) -> Self
28 where
29 F: Fn() -> Rc<crate::text::AnnotatedString> + 'static,
30 {
31 Self(Rc::new(resolver))
32 }
33
34 fn resolve(&self) -> Rc<crate::text::AnnotatedString> {
35 (self.0)()
36 }
37}
38
39impl PartialEq for DynamicTextSource {
40 fn eq(&self, other: &Self) -> bool {
41 Rc::ptr_eq(&self.0, &other.0)
42 }
43}
44
45#[derive(Clone, PartialEq)]
47pub enum TextSource {
48 Static(Rc<crate::text::AnnotatedString>),
50 Dynamic(DynamicTextSource),
52}
53
54impl TextSource {
55 fn resolve(&self) -> Rc<crate::text::AnnotatedString> {
56 match self {
57 TextSource::Static(text) => text.clone(),
58 TextSource::Dynamic(dynamic) => dynamic.resolve(),
59 }
60 }
61}
62
63#[doc(hidden)]
64pub trait IntoTextSource {
65 fn into_text_source(self) -> TextSource;
66}
67
68impl IntoTextSource for String {
69 fn into_text_source(self) -> TextSource {
70 TextSource::Static(Rc::new(crate::text::AnnotatedString::from(self)))
71 }
72}
73
74impl IntoTextSource for &str {
75 fn into_text_source(self) -> TextSource {
76 TextSource::Static(Rc::new(crate::text::AnnotatedString::from(self)))
77 }
78}
79
80impl IntoTextSource for crate::text::AnnotatedString {
81 fn into_text_source(self) -> TextSource {
82 TextSource::Static(Rc::new(self))
83 }
84}
85
86impl IntoTextSource for Rc<crate::text::AnnotatedString> {
87 fn into_text_source(self) -> TextSource {
88 TextSource::Static(self)
89 }
90}
91
92impl<T> IntoTextSource for State<T>
93where
94 T: ToString + Clone + 'static,
95{
96 fn into_text_source(self) -> TextSource {
97 let state = self;
98 TextSource::Dynamic(DynamicTextSource::new(move || {
99 Rc::new(crate::text::AnnotatedString::from(
100 state.value().to_string(),
101 ))
102 }))
103 }
104}
105
106impl<T> IntoTextSource for MutableState<T>
107where
108 T: ToString + Clone + 'static,
109{
110 fn into_text_source(self) -> TextSource {
111 let state = self;
112 TextSource::Dynamic(DynamicTextSource::new(move || {
113 Rc::new(crate::text::AnnotatedString::from(
114 state.value().to_string(),
115 ))
116 }))
117 }
118}
119
120impl<F> IntoTextSource for F
121where
122 F: Fn() -> String + 'static,
123{
124 fn into_text_source(self) -> TextSource {
125 TextSource::Dynamic(DynamicTextSource::new(move || {
126 Rc::new(crate::text::AnnotatedString::from(self()))
127 }))
128 }
129}
130
131impl IntoTextSource for DynamicTextSource {
132 fn into_text_source(self) -> TextSource {
133 TextSource::Dynamic(self)
134 }
135}
136
137fn compose_basic_text_group(
138 text: TextSource,
139 modifier: Modifier,
140 style: Arc<TextStyle>,
141 options: TextLayoutOptions,
142) -> NodeId {
143 #[cfg(feature = "localization")]
144 let style = crate::localization::apply_shared_text_locale(style);
145 let current = text.resolve();
146
147 let options = options.normalized();
148
149 let modifier = match crate::widgets::local_selection_registrar().current() {
150 Some(registrar) => modifier.then(crate::widgets::selection_container::selectable_text(
151 registrar,
152 Rc::clone(¤t),
153 TextStyle::clone(&style),
154 options,
155 )),
156 None => modifier,
157 };
158 let environment = LayoutEnvironment::current();
159 let text_element = modifier_element(TextModifierElement::with_shared_style(
160 current,
161 style,
162 options,
163 environment.density,
164 ));
165 let final_modifier = Modifier::from_parts(&[text_element]);
166 let combined_modifier = modifier.then(final_modifier);
167
168 compose_empty_layout(combined_modifier, environment)
169}
170
171mod shared_style {
174 use std::sync::Arc;
175
176 use cranpose_core::NodeId;
177
178 use super::{IntoTextSource, compose_basic_text_group};
179 use crate::{
180 composable,
181 modifier::Modifier,
182 text::{TextLayoutOptions, TextStyle},
183 };
184
185 #[composable]
189 pub(super) fn Text<S>(
190 text: S,
191 modifier: Modifier,
192 style: Arc<TextStyle>,
193 options: TextLayoutOptions,
194 ) -> NodeId
195 where
196 S: IntoTextSource + Clone + PartialEq + 'static,
197 {
198 compose_basic_text_group(text.into_text_source(), modifier, style, options)
199 }
200}
201
202#[track_caller]
204#[expect(non_snake_case)]
205pub fn BasicTextWithOptions<S>(
206 text: S,
207 modifier: Modifier,
208 style: TextStyle,
209 options: TextLayoutOptions,
210) -> NodeId
211where
212 S: IntoTextSource + Clone + PartialEq + 'static,
213{
214 shared_style::Text(text, modifier, shared_text_style(style), options)
215}
216
217#[track_caller]
220#[expect(non_snake_case)]
221pub fn BasicText<S>(
222 text: S,
223 modifier: Modifier,
224 style: TextStyle,
225 overflow: TextOverflow,
226 soft_wrap: bool,
227 max_lines: usize,
228 min_lines: usize,
229) -> NodeId
230where
231 S: IntoTextSource + Clone + PartialEq + 'static,
232{
233 shared_style::Text(
234 text,
235 modifier,
236 shared_text_style(style),
237 TextLayoutOptions {
238 overflow,
239 soft_wrap,
240 max_lines,
241 min_lines,
242 },
243 )
244}
245
246#[track_caller]
248#[expect(non_snake_case)]
249pub fn TextWithOptions<S>(
250 value: S,
251 modifier: Modifier,
252 style: TextStyle,
253 options: TextOptions,
254) -> NodeId
255where
256 S: IntoTextSource + Clone + PartialEq + 'static,
257{
258 shared_style::Text(
259 value,
260 modifier,
261 shared_text_style(style),
262 TextLayoutOptions::from(options),
263 )
264}
265
266#[track_caller]
284#[expect(non_snake_case)]
285pub fn Text<S>(value: S, modifier: Modifier, style: TextStyle) -> NodeId
286where
287 S: IntoTextSource + Clone + PartialEq + 'static,
288{
289 shared_style::Text(
290 value,
291 modifier,
292 shared_text_style(style),
293 TextLayoutOptions::from(TextOptions::default()),
294 )
295}
296
297#[cfg(test)]
298#[path = "tests/text_tests.rs"]
299mod tests;