Skip to main content

cotis_layout/text/
text_leafs.rs

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;
12#[cfg(feature = "complex_color")]
13use cotis_defaults::colors::ColorLayer;
14use cotis_defaults::element_configs::style::sizing::AxisSizing;
15use cotis_defaults::element_configs::style::types::{LayoutDirection, Padding};
16use cotis_defaults::element_configs::style::{BorderElementStyle, BorderWidthStyle, CotisStyle};
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
24/// A configuration that can spawn transparent synthetic children used to lay out text.
25///
26/// Each line, word, and inter-word spacer of a text leaf becomes one of these children, so the
27/// layout engine can size and wrap them while keeping the real config type generic.
28///
29/// Implement this trait alongside [`CotisLayoutCompatible`](crate::layout_traits::CotisLayoutCompatible)
30/// for custom config types that support [`ConfigureLeafElements`](cotis::element_configuring::ConfigureLeafElements)
31/// with [`TextElementConfig`](cotis_defaults::element_configs::text_config::TextElementConfig).
32///
33/// The built-in implementation for [`ElementConfig`](cotis_defaults::element_configs::ElementConfig)
34/// strips background, border, and padding from the parent style so synthetic nodes contribute
35/// layout size only, not visuals.
36///
37/// # Examples
38///
39/// ```rust,ignore
40/// use cotis_defaults::element_configs::ElementConfig;
41/// use cotis_defaults::element_configs::style::sizing::AxisSizing;
42/// use cotis_defaults::element_configs::style::types::LayoutDirection;
43/// use cotis_layout::preamble::TextConfigurable;
44///
45/// let parent: ElementConfig<'_, _, _, _, _> = /* ... */;
46/// let child = parent.synthetic_text_child(
47///     LayoutDirection::LeftToRight,
48///     4.0,
49///     AxisSizing::Fixed(100.0),
50///     AxisSizing::Fixed(20.0),
51/// );
52/// // child has transparent background and zero padding
53/// ```
54pub trait TextConfigurable: Sized {
55    /// Build a transparent, border-less, padding-less child derived from this (parent) config.
56    ///
57    /// Called when expanding text leaves into line, word, and spacer nodes. The returned config
58    /// is opened as a synthetic child with the given layout direction, gap, and axis sizing.
59    fn synthetic_text_child(
60        &self,
61        layout_direction: LayoutDirection,
62        child_gap: f32,
63        width: AxisSizing,
64        height: AxisSizing,
65    ) -> Self;
66}
67
68/// Strips background, border and padding so the synthetic node only contributes layout, not visuals.
69pub(crate) fn reset_text_child_style<S>(
70    style: &mut CotisStyle<S>,
71    layout_direction: LayoutDirection,
72    child_gap: f32,
73) {
74    #[cfg(not(feature = "complex_color"))]
75    {
76        style.background_color.a = 0.0;
77    }
78    #[cfg(feature = "complex_color")]
79    {
80        style.background_color = ColorLayer::Solid(Color::rgba(0.0, 0.0, 0.0, 0.0));
81    }
82    style.layout.layout_direction = layout_direction;
83    style.layout.child_gap = child_gap;
84    style.layout.padding = Padding {
85        top: 0.0,
86        bottom: 0.0,
87        left: 0.0,
88        right: 0.0,
89    };
90    style.floating = None;
91    style.border = BorderElementStyle {
92        color: Color {
93            r: 0.0,
94            g: 0.0,
95            b: 0.0,
96            a: 0.0,
97        },
98        width: BorderWidthStyle {
99            left: 0.0,
100            right: 0.0,
101            top: 0.0,
102            bottom: 0.0,
103            between_children: 0.0,
104        },
105    };
106}
107
108/// Native text config used only for measuring. The cotis `TextConfig` carries rendering data
109/// (e.g. color) that the measuring functions don't need.
110fn measuring_text_config(config: &TextConfig) -> MeasureTextConfig {
111    MeasureTextConfig {
112        font_id: config.font_id as usize,
113        font_size: config.font_size,
114        letter_spacing: config.letter_spacing,
115        line_height: config.line_height,
116        wrap_mode: config.wrap_mode,
117        alignment: config.alignment,
118    }
119}
120
121impl<'layout, 'frame, Config: TextConfigurable + CotisLayoutCompatible<'frame>>
122    CotisLayoutRun<'layout, Config>
123{
124    /// When `intrinsic_fit` is true, uses `AxisSizing::Fit` (min 0, max measured)
125    /// so the node can shrink when the parent is tight.
126    fn create_text_child(
127        &self,
128        container_id: ElementId,
129        layout_direction: LayoutDirection,
130        child_gap: f32,
131        measured: Dimensions,
132        intrinsic_fit: bool,
133    ) -> Config {
134        let w = measured.width.max(0.0);
135        let h = measured.height.max(0.0);
136
137        let width = if intrinsic_fit {
138            AxisSizing::Fit(0.0, w)
139        } else {
140            AxisSizing::Fixed(w)
141        };
142        let height = if intrinsic_fit {
143            AxisSizing::Fit(h, f32::MAX)
144        } else {
145            AxisSizing::Fixed(h)
146        };
147
148        let parent = self
149            .get_config(container_id)
150            .expect("[Cotis Layout] text leaf needs set_config before set_leaf_config");
151
152        parent.synthetic_text_child(layout_direction, child_gap, width, height)
153    }
154
155    fn push_spacer(&mut self, container_id: ElementId, measured: Dimensions) {
156        self.new_child_element();
157
158        let spacer_el = self.create_text_child(
159            container_id,
160            LayoutDirection::LeftToRight,
161            0.0,
162            measured,
163            false,
164        );
165
166        let layout = spacer_el.get_style();
167        let id = self.open_element_id();
168
169        self.set_open_element_config(layout, id, None, spacer_el);
170
171        self.close_open_element();
172    }
173
174    fn push_text_fragment(
175        &mut self,
176        container_id: ElementId,
177        source: &Arc<str>,
178        range: Range<usize>,
179        text_config: TextConfig,
180        text_style: TextStyle,
181        measured: Dimensions,
182    ) {
183        self.new_child_element();
184
185        let word_el = self.create_text_child(
186            container_id,
187            LayoutDirection::LeftToRight,
188            0.0,
189            measured,
190            false,
191        );
192
193        let layout = word_el.get_style();
194        let id = self.open_element_id();
195
196        self.set_open_element_config(layout, id, None, word_el);
197
198        let wid = self.open_element_id();
199
200        self.text_fragments.insert(
201            wid,
202            TextDrawPayload::new_from_arc(source, range, text_config, text_style),
203        );
204
205        self.close_open_element();
206    }
207
208    pub(crate) fn apply_text_leaf(&mut self, leaf: TextElementConfig<'frame>) {
209        let container_id = self.open_element_id();
210
211        let source: Arc<str> = Arc::from(leaf.text.as_ref());
212
213        let text_style = leaf.style;
214        let text_config = leaf.config;
215        let measure_style = measuring_text_config(&text_config);
216
217        self.set_open_element_layout_direction(LayoutDirection::TopToBottom);
218
219        let word_row_gap = match text_config.wrap_mode {
220            TextElementConfigWrapMode::Words => {
221                measure_text_fragment(" ", &measure_style, self.layout_manager.text_dim_fun()).width
222            }
223            _ => 0.0,
224        };
225
226        for line_range in newline_line_ranges(source.as_ref()) {
227            let line_slice_full = &source[line_range.start..line_range.end];
228
229            let (line_dims, words_specs) = match text_config.wrap_mode {
230                TextElementConfigWrapMode::Words => {
231                    let empty_dims = measure_text_fragment(
232                        "",
233                        &measure_style,
234                        self.layout_manager.text_dim_fun(),
235                    );
236
237                    let pieces = line_word_space_pieces(line_slice_full);
238
239                    let specs = text_line_child_specs(&pieces);
240
241                    let dims = if specs.is_empty() {
242                        empty_dims
243                    } else {
244                        let mut sum_w = 0.0f32;
245                        let mut max_h = empty_dims.height;
246
247                        for spec in &specs {
248                            if let TextLineChildSpec::Word(r) = spec {
249                                let fragment = &line_slice_full[r.clone()];
250
251                                let d = measure_text_fragment(
252                                    fragment,
253                                    &measure_style,
254                                    self.layout_manager.text_dim_fun(),
255                                );
256
257                                sum_w += d.width;
258                                max_h = max_h.max(d.height);
259                            }
260                        }
261
262                        let gap_total = (specs.len().saturating_sub(1)) as f32 * word_row_gap;
263
264                        Dimensions {
265                            width: sum_w + gap_total,
266                            height: max_h,
267                        }
268                    };
269
270                    (dims, specs)
271                }
272
273                TextElementConfigWrapMode::Newline | TextElementConfigWrapMode::None => {
274                    let dims = measure_text_fragment(
275                        line_slice_full,
276                        &measure_style,
277                        self.layout_manager.text_dim_fun(),
278                    );
279
280                    (dims, Vec::new())
281                }
282            };
283
284            self.new_child_element();
285
286            let line_el = self.create_text_child(
287                container_id,
288                LayoutDirection::LeftToRight,
289                word_row_gap,
290                line_dims,
291                true,
292            );
293
294            let mut line_layout = line_el.get_style();
295            line_layout.layout.wrapping = ChildWrapping::Text;
296
297            let line_id = self.open_element_id();
298
299            self.set_open_element_config(line_layout, line_id, None, line_el);
300
301            match text_config.wrap_mode {
302                TextElementConfigWrapMode::Words => {
303                    let spacer_measured = Dimensions {
304                        width: 0.0,
305                        height: line_dims.height,
306                    };
307
308                    for spec in words_specs {
309                        match spec {
310                            TextLineChildSpec::Spacer => {
311                                self.push_spacer(container_id, spacer_measured);
312                            }
313
314                            TextLineChildSpec::Word(w) => {
315                                let global = line_range.start + w.start..line_range.start + w.end;
316
317                                let word_text = &source[global.clone()];
318
319                                let measured = measure_text_fragment(
320                                    word_text,
321                                    &measure_style,
322                                    self.layout_manager.text_dim_fun(),
323                                );
324
325                                self.push_text_fragment(
326                                    container_id,
327                                    &source,
328                                    global,
329                                    text_config,
330                                    text_style.clone(),
331                                    measured,
332                                );
333                            }
334                        }
335                    }
336                }
337
338                TextElementConfigWrapMode::Newline | TextElementConfigWrapMode::None => {
339                    if !line_range.is_empty() {
340                        let global = line_range.clone();
341
342                        let measured = measure_text_fragment(
343                            &source[global.clone()],
344                            &measure_style,
345                            self.layout_manager.text_dim_fun(),
346                        );
347
348                        self.push_text_fragment(
349                            container_id,
350                            &source,
351                            global,
352                            text_config,
353                            text_style.clone(),
354                            measured,
355                        );
356                    }
357                }
358            }
359
360            self.close_open_element();
361        }
362    }
363}