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;
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
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    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
101/// Native text config used only for measuring. The cotis `TextConfig` carries rendering data
102/// (e.g. color) that the measuring functions don't need.
103fn 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    /// When `intrinsic_fit` is true, uses `AxisSizing::Fit` (min 0, max measured)
118    /// so the node can shrink when the parent is tight.
119    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)] // To allow or disallow complex color
207        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                                    // To allow or disallow complex color
326                                    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)] // To allow or disallow complex color
350                            text_style.clone(),
351                            measured,
352                        );
353                    }
354                }
355            }
356
357            self.close_open_element();
358        }
359    }
360}