1use crate::layout_management::CotisLayoutRun;
2use crate::layout_struct::TextDrawPayload;
3use crate::layout_struct::info_types::TextConfig as MeasureTextConfig;
4use crate::layout_struct::layout_states::ChildWrapping;
5use crate::layout_traits::CotisLayoutCompatible;
6use crate::text::{
7 TextLineChildSpec, line_word_space_pieces, measure_text_fragment, newline_line_ranges,
8 text_line_child_specs,
9};
10use cotis::utils::ElementId;
11use cotis_defaults::colors::Color;
12use cotis_defaults::element_configs::style::sizing::AxisSizing;
13use cotis_defaults::element_configs::style::types::{LayoutDirection, Padding};
14use cotis_defaults::element_configs::style::{
15 BorderElementStyle, BorderWidthStyle, CotisStyle, TRANSPARENT_BACKGROUND,
16};
17use cotis_defaults::element_configs::text_config::{
18 TextConfig, TextElementConfig, TextElementConfigWrapMode, TextStyle,
19};
20use cotis_utils::math::Dimensions;
21use std::ops::Range;
22use std::sync::Arc;
23
24pub trait TextConfigurable: Sized {
55 fn synthetic_text_child(
60 &self,
61 layout_direction: LayoutDirection,
62 child_gap: f32,
63 width: AxisSizing,
64 height: AxisSizing,
65 ) -> Self;
66}
67
68pub(crate) fn reset_text_child_style<S>(
70 style: &mut CotisStyle<S>,
71 layout_direction: LayoutDirection,
72 child_gap: f32,
73) {
74 style.background_color = TRANSPARENT_BACKGROUND;
75 style.layout.layout_direction = layout_direction;
76 style.layout.child_gap = child_gap;
77 style.layout.padding = Padding {
78 top: 0.0,
79 bottom: 0.0,
80 left: 0.0,
81 right: 0.0,
82 };
83 style.floating = None;
84 style.border = BorderElementStyle {
85 color: Color {
86 r: 0.0,
87 g: 0.0,
88 b: 0.0,
89 a: 0.0,
90 },
91 width: BorderWidthStyle {
92 left: 0.0,
93 right: 0.0,
94 top: 0.0,
95 bottom: 0.0,
96 between_children: 0.0,
97 },
98 };
99}
100
101fn measuring_text_config(config: &TextConfig) -> MeasureTextConfig {
104 MeasureTextConfig {
105 font_id: config.font_id as usize,
106 font_size: config.font_size,
107 letter_spacing: config.letter_spacing,
108 line_height: config.line_height,
109 wrap_mode: config.wrap_mode,
110 alignment: config.alignment,
111 }
112}
113
114impl<'layout, 'frame, Config: TextConfigurable + CotisLayoutCompatible<'frame>>
115 CotisLayoutRun<'layout, Config>
116{
117 fn create_text_child(
120 &self,
121 container_id: ElementId,
122 layout_direction: LayoutDirection,
123 child_gap: f32,
124 measured: Dimensions,
125 intrinsic_fit: bool,
126 ) -> Config {
127 let w = measured.width.max(0.0);
128 let h = measured.height.max(0.0);
129
130 let width = if intrinsic_fit {
131 AxisSizing::Fit(0.0, w)
132 } else {
133 AxisSizing::Fixed(w)
134 };
135 let height = if intrinsic_fit {
136 AxisSizing::Fit(h, f32::MAX)
137 } else {
138 AxisSizing::Fixed(h)
139 };
140
141 let parent = self
142 .get_config(container_id)
143 .expect("[Cotis Layout] text leaf needs set_config before set_leaf_config");
144
145 parent.synthetic_text_child(layout_direction, child_gap, width, height)
146 }
147
148 fn push_spacer(&mut self, container_id: ElementId, measured: Dimensions) {
149 self.new_child_element();
150
151 let spacer_el = self.create_text_child(
152 container_id,
153 LayoutDirection::LeftToRight,
154 0.0,
155 measured,
156 false,
157 );
158
159 let layout = spacer_el.get_style();
160 let id = self.open_element_id();
161
162 self.set_open_element_config(layout, id, None, spacer_el);
163
164 self.close_open_element();
165 }
166
167 fn push_text_fragment(
168 &mut self,
169 container_id: ElementId,
170 source: &Arc<str>,
171 range: Range<usize>,
172 text_config: TextConfig,
173 text_style: TextStyle,
174 measured: Dimensions,
175 ) {
176 self.new_child_element();
177
178 let word_el = self.create_text_child(
179 container_id,
180 LayoutDirection::LeftToRight,
181 0.0,
182 measured,
183 false,
184 );
185
186 let layout = word_el.get_style();
187 let id = self.open_element_id();
188
189 self.set_open_element_config(layout, id, None, word_el);
190
191 let wid = self.open_element_id();
192
193 self.text_fragments.insert(
194 wid,
195 TextDrawPayload::new_from_arc(source, range, text_config, text_style),
196 );
197
198 self.close_open_element();
199 }
200
201 pub(crate) fn apply_text_leaf(&mut self, leaf: TextElementConfig<'frame>) {
202 let container_id = self.open_element_id();
203
204 let source: Arc<str> = Arc::from(leaf.text.as_ref());
205
206 #[allow(clippy::clone_on_copy)] let text_style = leaf.style.clone();
208 let text_config = leaf.config;
209 let measure_style = measuring_text_config(&text_config);
210
211 self.set_open_element_layout_direction(LayoutDirection::TopToBottom);
212
213 let word_row_gap = match text_config.wrap_mode {
214 TextElementConfigWrapMode::Words => {
215 measure_text_fragment(" ", &measure_style, self.layout_manager.text_dim_fun()).width
216 }
217 _ => 0.0,
218 };
219
220 for line_range in newline_line_ranges(source.as_ref()) {
221 let line_slice_full = &source[line_range.start..line_range.end];
222
223 let (line_dims, words_specs) = match text_config.wrap_mode {
224 TextElementConfigWrapMode::Words => {
225 let empty_dims = measure_text_fragment(
226 "",
227 &measure_style,
228 self.layout_manager.text_dim_fun(),
229 );
230
231 let pieces = line_word_space_pieces(line_slice_full);
232
233 let specs = text_line_child_specs(&pieces);
234
235 let dims = if specs.is_empty() {
236 empty_dims
237 } else {
238 let mut sum_w = 0.0f32;
239 let mut max_h = empty_dims.height;
240
241 for spec in &specs {
242 if let TextLineChildSpec::Word(r) = spec {
243 let fragment = &line_slice_full[r.clone()];
244
245 let d = measure_text_fragment(
246 fragment,
247 &measure_style,
248 self.layout_manager.text_dim_fun(),
249 );
250
251 sum_w += d.width;
252 max_h = max_h.max(d.height);
253 }
254 }
255
256 let gap_total = (specs.len().saturating_sub(1)) as f32 * word_row_gap;
257
258 Dimensions {
259 width: sum_w + gap_total,
260 height: max_h,
261 }
262 };
263
264 (dims, specs)
265 }
266
267 TextElementConfigWrapMode::Newline | TextElementConfigWrapMode::None => {
268 let dims = measure_text_fragment(
269 line_slice_full,
270 &measure_style,
271 self.layout_manager.text_dim_fun(),
272 );
273
274 (dims, Vec::new())
275 }
276 };
277
278 self.new_child_element();
279
280 let line_el = self.create_text_child(
281 container_id,
282 LayoutDirection::LeftToRight,
283 word_row_gap,
284 line_dims,
285 true,
286 );
287
288 let mut line_layout = line_el.get_style();
289 line_layout.layout.wrapping = ChildWrapping::Text;
290
291 let line_id = self.open_element_id();
292
293 self.set_open_element_config(line_layout, line_id, None, line_el);
294
295 match text_config.wrap_mode {
296 TextElementConfigWrapMode::Words => {
297 let spacer_measured = Dimensions {
298 width: 0.0,
299 height: line_dims.height,
300 };
301
302 for spec in words_specs {
303 match spec {
304 TextLineChildSpec::Spacer => {
305 self.push_spacer(container_id, spacer_measured);
306 }
307
308 TextLineChildSpec::Word(w) => {
309 let global = line_range.start + w.start..line_range.start + w.end;
310
311 let word_text = &source[global.clone()];
312
313 let measured = measure_text_fragment(
314 word_text,
315 &measure_style,
316 self.layout_manager.text_dim_fun(),
317 );
318
319 self.push_text_fragment(
320 container_id,
321 &source,
322 global,
323 text_config,
324 #[allow(clippy::clone_on_copy)]
325 text_style.clone(),
327 measured,
328 );
329 }
330 }
331 }
332 }
333
334 TextElementConfigWrapMode::Newline | TextElementConfigWrapMode::None => {
335 if !line_range.is_empty() {
336 let global = line_range.clone();
337
338 let measured = measure_text_fragment(
339 &source[global.clone()],
340 &measure_style,
341 self.layout_manager.text_dim_fun(),
342 );
343
344 self.push_text_fragment(
345 container_id,
346 &source,
347 global,
348 text_config,
349 #[allow(clippy::clone_on_copy)] text_style.clone(),
351 measured,
352 );
353 }
354 }
355 }
356
357 self.close_open_element();
358 }
359 }
360}