Skip to main content

cotis_pipes/
cotis_layout_pipes.rs

1//! Decodes [`RenderCommandOutput`] into
2//! [`RenderTList`] command streams.
3//!
4//! # Decode algorithm
5//!
6//! [`CotisLayoutToRenderListPipeForGenerics`] implements [`cotis::pipes::LayoutRenderPipe`].
7//! For each incoming layout command, `transform` calls
8//! [`RenderCommandOutputDecodable::decode_list`] on your target `RenderTList` type:
9//!
10//! 1. At each nesting level, the **head** command type is tried first via
11//!    [`RenderCommandOutputTListDecodable::decode_element`].
12//! 2. The **tail** is then recursed until the base case [`()`] is reached.
13//! 3. List order in the type alias determines emission order per layout command — e.g.
14//!    `Rectangle` before `Image` before `Border` before `Text`.
15//!
16//! A single `RenderCommandOutput::Element` can therefore produce multiple draw commands when
17//! several decoders match (opaque background, image payload, border widths, text fragment).
18//!
19//! # Built-in decode rules
20//!
21//! | Command | `RenderCommandOutput` variant | Emit when | Notes |
22//! |---------|-------------------------------|-----------|-------|
23//! | [`ClipStart`] | `ClipStart` | on match | Maps `bounds`, `id`, `z_index`. **Known limitation:** `horizontal`, `vertical`, and `corner_radii` are hard-coded to `false` / zero — layout clip metadata does not yet propagate rounded or axis-specific clipping. |
24//! | [`ClipEnd`] | `ClipEnd` | on match | Unit command, no fields. |
25//! | [`Rectangle`] | `Element` | `background_color.a > 0.0` | Copies corner radii and `extra_data` (shared clone). Requires `ExtraData: Send + Sync`. |
26//! | [`Border`] | `Element` | any width `> 0` | Skipped when every width is `<= 0`. Copies all five widths, color, corner radii, `extra_data`. Requires `ExtraData: Send + Sync`. |
27//! | [`Image`] | `Element` | `element.custom.image` present | **Moves** image payload via [`.take()`](std::option::Option::take) — consume-once per `Element` command. Requires `ImageElementData: 'render`, `ExtraData: Send + Sync`. |
28//! | [`Text`] | `Element` | `element.text` present | **Moves** text via [`.take()`](std::option::Option::take), clones string into [`OwnedOrRef::AsOwnedRef`]. Requires `ImageElementData: 'render`, `ExtraData: Send + Sync`. |
29//!
30//! # `ElementConfig` generic parameters
31//!
32//! Built-in decoders target [`ElementConfig`].
33//! The pipe impl uses shorthand type params (`S`, `I`, `C`, `E`); in `ElementConfig` they are:
34//!
35//! ```text
36//! ElementConfig<'render, Sizing, ImageElementData, CustomElementData, ExtraData>
37//!   'render           — lifetime of config references
38//!   Sizing (S)         — sizing strategy type parameter to CotisStyle<Sizing>
39//!   ImageElementData (I) — image payload type (Image decoder)
40//!   CustomElementData (C) — custom per-element payload (FromRenderCommandOutput for compound elements)
41//!   ExtraData (E)      — extra_data type forwarded into RenderCommandInfo
42//! ```
43//!
44//! Custom [`FromRenderCommandOutput`] impls typically specialize `CustomElementData` — see
45//! `cotis-compound-elements` for `HandledTextInput`.
46//!
47//! # `RenderTList` requirements
48//!
49//! The decode target `Decodable` must implement
50//! [`RenderCommandOutputDecodable`]`<ElementConfig<…>>`, which is satisfied when the list is
51//! built from types that implement either:
52//!
53//! - [`RenderCommandOutputTListDecodable`] (built-in commands), or
54//! - [`FromRenderCommandOutput`] (custom commands; a blanket impl bridges to
55//!   `RenderCommandOutputTListDecodable`).
56//!
57//! Each tail node requires [`IsRenderList`].
58
59use cotis::pipes::LayoutRenderPipe;
60use cotis::utils::OwnedOrRef;
61use cotis_defaults::element_configs::ElementConfig;
62use cotis_defaults::render_commands::render_t_list::{IsRenderList, RenderTList};
63use cotis_defaults::render_commands::{
64    Border, BorderWidth, ClipEnd, ClipStart, CornerRadii, Image, Rectangle, RenderCommandInfo, Text,
65};
66use cotis_layout::layout_struct::RenderCommandOutput;
67use std::sync::Arc;
68
69/// Zero-config [`LayoutRenderPipe`] for
70/// [`ElementConfig`] and standard
71/// [`RenderTList`] command types.
72///
73/// Pass this struct to [`CotisApp::new`](cotis::cotis_app::CotisApp::new) as the third argument.
74/// It is stateless (zero-sized) and needs no initialization beyond the unit value.
75///
76/// # Type parameters
77///
78/// The pipe itself has no type parameters. The *output* type `Decodable` is inferred from your
79/// renderer's [`CotisRenderer`](cotis::renders::CotisRenderer) impl — typically a `RenderTList`
80/// alias such as `StandardRenderCmds` from `cotis-layout/examples/support.rs`.
81///
82/// `Decodable` must implement [`RenderCommandOutputDecodable`] for
83/// `RenderCommandOutput<ElementConfig<'render, Sizing, ImageElementData, CustomElementData, ExtraData>>`.
84///
85/// # `transform` behavior
86///
87/// Consumes an iterator of layout commands and eagerly collects decoded `RenderTList` values into
88/// a `Vec` for the frame. Each layout command is decoded independently; multiple draw commands
89/// may be emitted from a single `Element` variant when several built-in decoders match.
90///
91/// See the [module-level decode rules](self#built-in-decode-rules) for per-command mapping.
92///
93/// # Examples
94///
95/// ```rust,ignore
96/// use cotis::cotis_app::CotisApp;
97/// use cotis_layout::preamble::CotisLayoutManager;
98/// use cotis_pipes::cotis_layout_pipes::CotisLayoutToRenderListPipeForGenerics;
99///
100/// // CotisApp::new(renderer, CotisLayoutManager::new(dimensions), CotisLayoutToRenderListPipeForGenerics);
101/// ```
102pub struct CotisLayoutToRenderListPipeForGenerics;
103
104/// Walks a [`RenderTList`] type for
105/// one [`RenderCommandOutput`], producing
106/// flattened list values.
107///
108/// Implemented for [`()`] (base case, no-op) and `RenderTList<Head, Tail>` (recursive case).
109/// At each level the head decoders run first, then the tail recurses.
110///
111/// # Examples
112///
113/// ```rust,ignore
114/// use cotis_defaults::render_commands::render_t_list::RenderTList;
115/// use cotis_defaults::render_commands::{Rectangle, Text};
116///
117/// // Decode order for Element commands: Rectangle first, then Text.
118/// type Cmds = RenderTList<Rectangle<'static, ()>, RenderTList<Text<'static, ()>, ()>>;
119/// ```
120pub trait RenderCommandOutputDecodable<Custom>: Sized {
121    /// Decodes `output` into zero or more values appended to `buffer`.
122    fn decode_list(output: &mut RenderCommandOutput<Custom>, buffer: &mut Vec<Self>);
123}
124
125/// Decodes a single head command type from one [`RenderCommandOutput`].
126///
127/// Built-in draw commands (`Rectangle`, `Border`, etc.) implement this directly.
128/// Custom types implement [`FromRenderCommandOutput`], which provides a blanket impl of this
129/// trait.
130pub trait RenderCommandOutputTListDecodable<Custom>: Sized {
131    /// Tries to decode `output` into `Self` and appends matches to `buffer`.
132    fn decode_element(output: &mut RenderCommandOutput<Custom>, buffer: &mut Vec<Self>);
133}
134
135/// Extension hook for custom drawable types in a [`RenderTList`].
136///
137/// Implement this trait on your command type and append it to the tail of your `RenderTList`
138/// alias. The blanket impl of [`RenderCommandOutputTListDecodable`] bridges your type into the
139/// decode walk automatically.
140///
141/// # Contract
142///
143/// - Return [`Some`] to emit a draw command for this layout step.
144/// - Return [`None`] to skip silently (the next decoder in the list may still match).
145/// - The `output` reference is `&mut` so impls may [`.take()`](std::option::Option::take)
146///   fields — the same consume-once pattern used by the built-in `Image` and `Text` decoders.
147///
148/// Returning [`Some`] for every `Element` variant is valid — see `cotis-layout/examples/circle_example.rs`.
149///
150/// # Examples
151///
152/// ```rust,ignore
153/// use cotis_layout::layout_struct::RenderCommandOutput;
154/// use cotis_pipes::cotis_layout_pipes::FromRenderCommandOutput;
155/// use cotis_utils::math::BoundingBox;
156///
157/// pub struct CircleDrawable {
158///     center_x: f32,
159///     center_y: f32,
160///     radius: f32,
161/// }
162///
163/// impl<Custom> FromRenderCommandOutput<Custom> for CircleDrawable {
164///     fn decode_element(output: &mut RenderCommandOutput<Custom>) -> Option<Self> {
165///         if let RenderCommandOutput::Element(el) = output {
166///             let bb = el.bounds;
167///             Some(CircleDrawable {
168///                 center_x: bb.x + bb.width / 2.0,
169///                 center_y: bb.y + bb.height / 2.0,
170///                 radius: (bb.width.min(bb.height) / 4.0).max(5.0),
171///             })
172///         } else {
173///             None
174///         }
175///     }
176/// }
177/// ```
178pub trait FromRenderCommandOutput<Custom>: Sized {
179    /// Tries to decode `output` into `Self`.
180    fn decode_element(output: &mut RenderCommandOutput<Custom>) -> Option<Self>;
181}
182
183impl<Custom, T: FromRenderCommandOutput<Custom> + Sized> RenderCommandOutputTListDecodable<Custom>
184    for T
185{
186    fn decode_element(output: &mut RenderCommandOutput<Custom>, buffer: &mut Vec<Self>) {
187        match <T as FromRenderCommandOutput<Custom>>::decode_element(output) {
188            None => {}
189            Some(s) => {
190                buffer.push(s);
191            }
192        }
193    }
194}
195
196impl<Custom> RenderCommandOutputDecodable<Custom> for () {
197    fn decode_list(_output: &mut RenderCommandOutput<Custom>, _buffer: &mut Vec<Self>) {}
198}
199
200impl<Custom, Head, Tail> RenderCommandOutputDecodable<Custom> for RenderTList<Head, Tail>
201where
202    Tail: IsRenderList + RenderCommandOutputDecodable<Custom>,
203    Head: RenderCommandOutputTListDecodable<Custom>,
204{
205    fn decode_list(output: &mut RenderCommandOutput<Custom>, buffer: &mut Vec<Self>) {
206        let mut head_results = Vec::<Head>::new();
207
208        Head::decode_element(output, &mut head_results);
209
210        buffer.extend(head_results.into_iter().map(RenderTList::Type));
211
212        let mut tail_results = Vec::<Tail>::new();
213
214        Tail::decode_list(output, &mut tail_results);
215
216        buffer.extend(tail_results.into_iter().map(RenderTList::List));
217    }
218}
219
220impl<'render, S, I, C, E, Decodable>
221    LayoutRenderPipe<RenderCommandOutput<ElementConfig<'render, S, I, C, E>>, Decodable>
222    for CotisLayoutToRenderListPipeForGenerics
223where
224    Decodable: RenderCommandOutputDecodable<ElementConfig<'render, S, I, C, E>>,
225{
226    fn transform(
227        &mut self,
228        render_commands: impl Iterator<Item = RenderCommandOutput<ElementConfig<'render, S, I, C, E>>>,
229    ) -> impl Iterator<Item = Decodable> {
230        let mut out = Vec::new();
231
232        for mut command in render_commands {
233            Decodable::decode_list(&mut command, &mut out);
234        }
235
236        out.into_iter()
237    }
238}
239
240impl<'render, S, I, C, E: 'render>
241    RenderCommandOutputTListDecodable<ElementConfig<'render, S, I, C, E>>
242    for ClipStart<'render, E>
243{
244    fn decode_element(
245        output: &mut RenderCommandOutput<ElementConfig<'render, S, I, C, E>>,
246        buffer: &mut Vec<Self>,
247    ) {
248        match output {
249            RenderCommandOutput::Element(_) => {}
250            RenderCommandOutput::ClipStart(start) => {
251                buffer.push(ClipStart {
252                    info: RenderCommandInfo {
253                        bounding_box: start.bounds,
254                        id: start.id.get_id() as u32,
255                        z_index: start.z_index as i16,
256                        extra_data: None,
257                    },
258                    horizontal: false,
259                    vertical: false,
260                    corner_radii: CornerRadii {
261                        // TODO: propagate axis flags and corner radii from layout clip metadata.
262                        top_left: 0.0,
263                        top_right: 0.0,
264                        bottom_left: 0.0,
265                        bottom_right: 0.0,
266                    },
267                })
268            }
269            RenderCommandOutput::ClipEnd(_) => {}
270        }
271    }
272}
273
274impl<'render, S, I, C, E: 'render>
275    RenderCommandOutputTListDecodable<ElementConfig<'render, S, I, C, E>> for ClipEnd
276{
277    fn decode_element(
278        output: &mut RenderCommandOutput<ElementConfig<'render, S, I, C, E>>,
279        buffer: &mut Vec<Self>,
280    ) {
281        match output {
282            RenderCommandOutput::Element(_) => {}
283            RenderCommandOutput::ClipStart(_) => {}
284            RenderCommandOutput::ClipEnd(_) => {
285                buffer.push(ClipEnd);
286            }
287        }
288    }
289}
290
291impl<'render, S, I, C, E: 'render + Send + Sync>
292    RenderCommandOutputTListDecodable<ElementConfig<'render, S, I, C, E>>
293    for Rectangle<'render, E>
294{
295    fn decode_element(
296        output: &mut RenderCommandOutput<ElementConfig<'render, S, I, C, E>>,
297        buffer: &mut Vec<Self>,
298    ) {
299        if let RenderCommandOutput::Element(element) = output {
300            #[cfg(not(feature = "complex_color"))]
301            if element.custom.style.background_color.a <= 0.0 {
302                return;
303            }
304
305            buffer.push(Rectangle {
306                info: RenderCommandInfo {
307                    bounding_box: element.bounds,
308                    id: element.custom.id_config.get_handle().get_id() as u32,
309                    z_index: element.z_index as i16,
310                    extra_data: element.custom.extra_data.as_mut().map(|s| s.share_clone()),
311                },
312                #[cfg(not(feature = "complex_color"))]
313                color: element.custom.style.background_color,
314                #[cfg(feature = "complex_color")]
315                color: element.custom.style.background_color.clone(),
316                corner_radii: CornerRadii {
317                    top_left: element.custom.style.corner_radius.top_left,
318                    top_right: element.custom.style.corner_radius.top_right,
319                    bottom_left: element.custom.style.corner_radius.bottom_left,
320                    bottom_right: element.custom.style.corner_radius.bottom_right,
321                },
322            })
323        }
324    }
325}
326
327impl<'render, S, I, C, E: 'render + Send + Sync>
328    RenderCommandOutputTListDecodable<ElementConfig<'render, S, I, C, E>> for Border<'render, E>
329{
330    fn decode_element(
331        output: &mut RenderCommandOutput<ElementConfig<'render, S, I, C, E>>,
332        buffer: &mut Vec<Self>,
333    ) {
334        if let RenderCommandOutput::Element(element) = output {
335            let w = &element.custom.style.border.width;
336            if w.left <= 0.0
337                && w.right <= 0.0
338                && w.top <= 0.0
339                && w.bottom <= 0.0
340                && w.between_children <= 0.0
341            {
342                return;
343            }
344
345            buffer.push(Border {
346                info: RenderCommandInfo {
347                    bounding_box: element.bounds,
348                    id: element.custom.id_config.get_handle().get_id() as u32,
349                    z_index: element.z_index as i16,
350                    extra_data: element.custom.extra_data.as_mut().map(|s| s.share_clone()),
351                },
352                color: element.custom.style.border.color,
353                width: BorderWidth {
354                    left: w.left,
355                    right: w.right,
356                    top: w.top,
357                    bottom: w.bottom,
358                    between_children: w.between_children,
359                },
360                corner_radii: CornerRadii {
361                    top_left: element.custom.style.corner_radius.top_left,
362                    top_right: element.custom.style.corner_radius.top_right,
363                    bottom_left: element.custom.style.corner_radius.bottom_left,
364                    bottom_right: element.custom.style.corner_radius.bottom_right,
365                },
366            })
367        }
368    }
369}
370
371impl<'render, S, I: 'render, C, E: 'render + Send + Sync>
372    RenderCommandOutputTListDecodable<ElementConfig<'render, S, I, C, E>> for Image<'render, I, E>
373{
374    fn decode_element(
375        output: &mut RenderCommandOutput<ElementConfig<'render, S, I, C, E>>,
376        buffer: &mut Vec<Self>,
377    ) {
378        if let RenderCommandOutput::Element(element) = output {
379            buffer.push(Image {
380                data: match element.custom.image.take() {
381                    None => return,
382                    Some(s) => s.image,
383                },
384                info: RenderCommandInfo {
385                    bounding_box: element.bounds,
386                    id: element.custom.id_config.get_handle().get_id() as u32,
387                    z_index: element.z_index as i16,
388                    extra_data: element.custom.extra_data.as_mut().map(|s| s.share_clone()),
389                },
390                background_color: {
391                    #[cfg(not(feature = "complex_color"))]
392                    {
393                        element.custom.style.background_color
394                    }
395                    #[cfg(feature = "complex_color")]
396                    {
397                        element.custom.style.background_color.clone()
398                    }
399                },
400                corner_radii: CornerRadii {
401                    top_left: element.custom.style.corner_radius.top_left,
402                    top_right: element.custom.style.corner_radius.top_right,
403                    bottom_left: element.custom.style.corner_radius.bottom_left,
404                    bottom_right: element.custom.style.corner_radius.bottom_right,
405                },
406            })
407        }
408    }
409}
410
411impl<'render, S, I: 'render, C, E: 'render + Send + Sync>
412    RenderCommandOutputTListDecodable<ElementConfig<'render, S, I, C, E>> for Text<'render, E>
413{
414    fn decode_element(
415        output: &mut RenderCommandOutput<ElementConfig<'render, S, I, C, E>>,
416        buffer: &mut Vec<Self>,
417    ) {
418        if let RenderCommandOutput::Element(element) = output {
419            let text = match element.text.take() {
420                None => return,
421                Some(s) => s,
422            };
423            let text_s = text.get_str().to_owned();
424            buffer.push(Text {
425                info: RenderCommandInfo {
426                    bounding_box: element.bounds,
427                    id: element.custom.id_config.get_handle().get_id() as u32,
428                    z_index: element.z_index as i16,
429                    extra_data: element.custom.extra_data.as_mut().map(|s| s.share_clone()),
430                },
431                #[cfg(not(feature = "complex_colored_text"))]
432                color: text.style.color,
433                #[cfg(feature = "complex_colored_text")]
434                color: text.style.color.clone(),
435                font_id: text.config.font_id,
436                font_size: text.config.font_size,
437                letter_spacing: text.config.letter_spacing,
438                line_height: text.config.line_height,
439                text: OwnedOrRef::AsOwnedRef(Arc::new(text_s)),
440            })
441        }
442    }
443}
444
445#[cfg(test)]
446mod tests {
447    use super::*;
448    use cotis::utils::ElementIdConfig;
449    use cotis_defaults::colors::Color;
450    use cotis_defaults::element_configs::premade_configs::GenericElementConfig;
451    use cotis_defaults::element_configs::style::sizing::Sizing;
452    use cotis_defaults::element_configs::style::{
453        BorderElementStyle, BorderWidthStyle, CotisStyle,
454    };
455    use cotis_layout::layout_struct::RenderCommandOutputElement;
456    use cotis_utils::math::BoundingBox;
457
458    type TestConfig = GenericElementConfig<'static, (), ()>;
459
460    fn element_with_border(width: BorderWidthStyle) -> RenderCommandOutput<TestConfig> {
461        RenderCommandOutput::Element(RenderCommandOutputElement {
462            bounds: BoundingBox {
463                x: 0.0,
464                y: 0.0,
465                width: 100.0,
466                height: 50.0,
467            },
468            z_index: 0,
469            text: None,
470            custom: TestConfig {
471                id_config: ElementIdConfig::new_empty(),
472                style: CotisStyle {
473                    border: BorderElementStyle {
474                        color: Color::rgba(255.0, 0.0, 0.0, 255.0),
475                        width,
476                    },
477                    ..CotisStyle::<Sizing>::default()
478                },
479                image: None,
480                custom: None,
481                extra_data: None,
482            },
483        })
484    }
485
486    #[test]
487    fn border_emits_when_perimeter_positive_and_between_children_zero() {
488        let mut output = element_with_border(BorderWidthStyle {
489            left: 2.0,
490            right: 2.0,
491            top: 2.0,
492            bottom: 2.0,
493            between_children: 0.0,
494        });
495
496        let mut buffer = Vec::<Border<'static, ()>>::new();
497        Border::decode_element(&mut output, &mut buffer);
498
499        assert_eq!(buffer.len(), 1);
500        assert_eq!(buffer[0].width.left, 2.0);
501        assert_eq!(buffer[0].width.between_children, 0.0);
502    }
503
504    #[test]
505    fn border_skipped_when_all_widths_zero() {
506        let mut output = element_with_border(BorderWidthStyle {
507            left: 0.0,
508            right: 0.0,
509            top: 0.0,
510            bottom: 0.0,
511            between_children: 0.0,
512        });
513
514        let mut buffer = Vec::<Border<'static, ()>>::new();
515        Border::decode_element(&mut output, &mut buffer);
516
517        assert!(buffer.is_empty());
518    }
519}