cotis-pipes 0.1.0-alpha.2

Layout-to-renderer pipe for Cotis
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
//! Decodes [`RenderCommandOutput`] into
//! [`RenderTList`] command streams.
//!
//! # Decode algorithm
//!
//! [`CotisLayoutToRenderListPipeForGenerics`] implements [`cotis::pipes::LayoutRenderPipe`].
//! For each incoming layout command, `transform` calls
//! [`RenderCommandOutputDecodable::decode_list`] on your target `RenderTList` type:
//!
//! 1. At each nesting level, the **head** command type is tried first via
//!    [`RenderCommandOutputTListDecodable::decode_element`].
//! 2. The **tail** is then recursed until the base case [`()`] is reached.
//! 3. List order in the type alias determines emission order per layout command — e.g.
//!    `Rectangle` before `Image` before `Border` before `Text`.
//!
//! A single `RenderCommandOutput::Element` can therefore produce multiple draw commands when
//! several decoders match (opaque background, image payload, border widths, text fragment).
//!
//! # Built-in decode rules
//!
//! | Command | `RenderCommandOutput` variant | Emit when | Notes |
//! |---------|-------------------------------|-----------|-------|
//! | [`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. |
//! | [`ClipEnd`] | `ClipEnd` | on match | Unit command, no fields. |
//! | [`Rectangle`] | `Element` | `background_color.a > 0.0` | Copies corner radii and `extra_data` (shared clone). Requires `ExtraData: Send + Sync`. |
//! | [`Border`] | `Element` | any width `> 0` | Skipped when every width is `<= 0`. Copies all five widths, color, corner radii, `extra_data`. Requires `ExtraData: Send + Sync`. |
//! | [`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`. |
//! | [`Text`] | `Element` | `element.text` present | **Moves** text via [`.take()`](std::option::Option::take), clones string into [`OwnedOrRef::AsOwnedRef`]. Requires `ImageElementData: 'render`, `ExtraData: Send + Sync`. |
//!
//! # `ElementConfig` generic parameters
//!
//! Built-in decoders target [`ElementConfig`].
//! The pipe impl uses shorthand type params (`S`, `I`, `C`, `E`); in `ElementConfig` they are:
//!
//! ```text
//! ElementConfig<'render, Sizing, ImageElementData, CustomElementData, ExtraData>
//!   'render           — lifetime of config references
//!   Sizing (S)         — sizing strategy type parameter to CotisStyle<Sizing>
//!   ImageElementData (I) — image payload type (Image decoder)
//!   CustomElementData (C) — custom per-element payload (FromRenderCommandOutput for compound elements)
//!   ExtraData (E)      — extra_data type forwarded into RenderCommandInfo
//! ```
//!
//! Custom [`FromRenderCommandOutput`] impls typically specialize `CustomElementData` — see
//! `cotis-compound-elements` for `HandledTextInput`.
//!
//! # `RenderTList` requirements
//!
//! The decode target `Decodable` must implement
//! [`RenderCommandOutputDecodable`]`<ElementConfig<…>>`, which is satisfied when the list is
//! built from types that implement either:
//!
//! - [`RenderCommandOutputTListDecodable`] (built-in commands), or
//! - [`FromRenderCommandOutput`] (custom commands; a blanket impl bridges to
//!   `RenderCommandOutputTListDecodable`).
//!
//! Each tail node requires [`IsRenderList`].

use cotis::pipes::LayoutRenderPipe;
use cotis::utils::OwnedOrRef;
use cotis_defaults::element_configs::ElementConfig;
use cotis_defaults::render_commands::render_t_list::{IsRenderList, RenderTList};
use cotis_defaults::render_commands::{
    Border, BorderWidth, ClipEnd, ClipStart, CornerRadii, Image, Rectangle, RenderCommandInfo, Text,
};
use cotis_layout::layout_struct::RenderCommandOutput;
use std::sync::Arc;

/// Zero-config [`LayoutRenderPipe`] for
/// [`ElementConfig`] and standard
/// [`RenderTList`] command types.
///
/// Pass this struct to [`CotisApp::new`](cotis::cotis_app::CotisApp::new) as the third argument.
/// It is stateless (zero-sized) and needs no initialization beyond the unit value.
///
/// # Type parameters
///
/// The pipe itself has no type parameters. The *output* type `Decodable` is inferred from your
/// renderer's [`CotisRenderer`](cotis::renders::CotisRenderer) impl — typically a `RenderTList`
/// alias such as `StandardRenderCmds` from `cotis-layout/examples/support.rs`.
///
/// `Decodable` must implement [`RenderCommandOutputDecodable`] for
/// `RenderCommandOutput<ElementConfig<'render, Sizing, ImageElementData, CustomElementData, ExtraData>>`.
///
/// # `transform` behavior
///
/// Consumes an iterator of layout commands and eagerly collects decoded `RenderTList` values into
/// a `Vec` for the frame. Each layout command is decoded independently; multiple draw commands
/// may be emitted from a single `Element` variant when several built-in decoders match.
///
/// See the [module-level decode rules](self#built-in-decode-rules) for per-command mapping.
///
/// # Examples
///
/// ```rust,ignore
/// use cotis::cotis_app::CotisApp;
/// use cotis_layout::preamble::CotisLayoutManager;
/// use cotis_pipes::cotis_layout_pipes::CotisLayoutToRenderListPipeForGenerics;
///
/// // CotisApp::new(renderer, CotisLayoutManager::new(dimensions), CotisLayoutToRenderListPipeForGenerics);
/// ```
pub struct CotisLayoutToRenderListPipeForGenerics;

/// Walks a [`RenderTList`] type for
/// one [`RenderCommandOutput`], producing
/// flattened list values.
///
/// Implemented for [`()`] (base case, no-op) and `RenderTList<Head, Tail>` (recursive case).
/// At each level the head decoders run first, then the tail recurses.
///
/// # Examples
///
/// ```rust,ignore
/// use cotis_defaults::render_commands::render_t_list::RenderTList;
/// use cotis_defaults::render_commands::{Rectangle, Text};
///
/// // Decode order for Element commands: Rectangle first, then Text.
/// type Cmds = RenderTList<Rectangle<'static, ()>, RenderTList<Text<'static, ()>, ()>>;
/// ```
pub trait RenderCommandOutputDecodable<Custom>: Sized {
    /// Decodes `output` into zero or more values appended to `buffer`.
    fn decode_list(output: &mut RenderCommandOutput<Custom>, buffer: &mut Vec<Self>);
}

/// Decodes a single head command type from one [`RenderCommandOutput`].
///
/// Built-in draw commands (`Rectangle`, `Border`, etc.) implement this directly.
/// Custom types implement [`FromRenderCommandOutput`], which provides a blanket impl of this
/// trait.
pub trait RenderCommandOutputTListDecodable<Custom>: Sized {
    /// Tries to decode `output` into `Self` and appends matches to `buffer`.
    fn decode_element(output: &mut RenderCommandOutput<Custom>, buffer: &mut Vec<Self>);
}

/// Extension hook for custom drawable types in a [`RenderTList`].
///
/// Implement this trait on your command type and append it to the tail of your `RenderTList`
/// alias. The blanket impl of [`RenderCommandOutputTListDecodable`] bridges your type into the
/// decode walk automatically.
///
/// # Contract
///
/// - Return [`Some`] to emit a draw command for this layout step.
/// - Return [`None`] to skip silently (the next decoder in the list may still match).
/// - The `output` reference is `&mut` so impls may [`.take()`](std::option::Option::take)
///   fields — the same consume-once pattern used by the built-in `Image` and `Text` decoders.
///
/// Returning [`Some`] for every `Element` variant is valid — see `cotis-layout/examples/circle_example.rs`.
///
/// # Examples
///
/// ```rust,ignore
/// use cotis_layout::layout_struct::RenderCommandOutput;
/// use cotis_pipes::cotis_layout_pipes::FromRenderCommandOutput;
/// use cotis_utils::math::BoundingBox;
///
/// pub struct CircleDrawable {
///     center_x: f32,
///     center_y: f32,
///     radius: f32,
/// }
///
/// impl<Custom> FromRenderCommandOutput<Custom> for CircleDrawable {
///     fn decode_element(output: &mut RenderCommandOutput<Custom>) -> Option<Self> {
///         if let RenderCommandOutput::Element(el) = output {
///             let bb = el.bounds;
///             Some(CircleDrawable {
///                 center_x: bb.x + bb.width / 2.0,
///                 center_y: bb.y + bb.height / 2.0,
///                 radius: (bb.width.min(bb.height) / 4.0).max(5.0),
///             })
///         } else {
///             None
///         }
///     }
/// }
/// ```
pub trait FromRenderCommandOutput<Custom>: Sized {
    /// Tries to decode `output` into `Self`.
    fn decode_element(output: &mut RenderCommandOutput<Custom>) -> Option<Self>;
}

impl<Custom, T: FromRenderCommandOutput<Custom> + Sized> RenderCommandOutputTListDecodable<Custom>
    for T
{
    fn decode_element(output: &mut RenderCommandOutput<Custom>, buffer: &mut Vec<Self>) {
        match <T as FromRenderCommandOutput<Custom>>::decode_element(output) {
            None => {}
            Some(s) => {
                buffer.push(s);
            }
        }
    }
}

impl<Custom> RenderCommandOutputDecodable<Custom> for () {
    fn decode_list(_output: &mut RenderCommandOutput<Custom>, _buffer: &mut Vec<Self>) {}
}

impl<Custom, Head, Tail> RenderCommandOutputDecodable<Custom> for RenderTList<Head, Tail>
where
    Tail: IsRenderList + RenderCommandOutputDecodable<Custom>,
    Head: RenderCommandOutputTListDecodable<Custom>,
{
    fn decode_list(output: &mut RenderCommandOutput<Custom>, buffer: &mut Vec<Self>) {
        let mut head_results = Vec::<Head>::new();

        Head::decode_element(output, &mut head_results);

        buffer.extend(head_results.into_iter().map(RenderTList::Type));

        let mut tail_results = Vec::<Tail>::new();

        Tail::decode_list(output, &mut tail_results);

        buffer.extend(tail_results.into_iter().map(RenderTList::List));
    }
}

impl<'render, S, I, C, E, Decodable>
    LayoutRenderPipe<RenderCommandOutput<ElementConfig<'render, S, I, C, E>>, Decodable>
    for CotisLayoutToRenderListPipeForGenerics
where
    Decodable: RenderCommandOutputDecodable<ElementConfig<'render, S, I, C, E>>,
{
    fn transform(
        &mut self,
        render_commands: impl Iterator<Item = RenderCommandOutput<ElementConfig<'render, S, I, C, E>>>,
    ) -> impl Iterator<Item = Decodable> {
        let mut out = Vec::new();

        for mut command in render_commands {
            Decodable::decode_list(&mut command, &mut out);
        }

        out.into_iter()
    }
}

impl<'render, S, I, C, E: 'render>
    RenderCommandOutputTListDecodable<ElementConfig<'render, S, I, C, E>>
    for ClipStart<'render, E>
{
    fn decode_element(
        output: &mut RenderCommandOutput<ElementConfig<'render, S, I, C, E>>,
        buffer: &mut Vec<Self>,
    ) {
        match output {
            RenderCommandOutput::Element(_) => {}
            RenderCommandOutput::ClipStart(start) => {
                buffer.push(ClipStart {
                    info: RenderCommandInfo {
                        bounding_box: start.bounds,
                        id: start.id.get_id() as u32,
                        z_index: start.z_index as i16,
                        extra_data: None,
                    },
                    horizontal: false,
                    vertical: false,
                    corner_radii: CornerRadii {
                        // TODO: propagate axis flags and corner radii from layout clip metadata.
                        top_left: 0.0,
                        top_right: 0.0,
                        bottom_left: 0.0,
                        bottom_right: 0.0,
                    },
                })
            }
            RenderCommandOutput::ClipEnd(_) => {}
        }
    }
}

impl<'render, S, I, C, E: 'render>
    RenderCommandOutputTListDecodable<ElementConfig<'render, S, I, C, E>> for ClipEnd
{
    fn decode_element(
        output: &mut RenderCommandOutput<ElementConfig<'render, S, I, C, E>>,
        buffer: &mut Vec<Self>,
    ) {
        match output {
            RenderCommandOutput::Element(_) => {}
            RenderCommandOutput::ClipStart(_) => {}
            RenderCommandOutput::ClipEnd(_) => {
                buffer.push(ClipEnd);
            }
        }
    }
}

impl<'render, S, I, C, E: 'render + Send + Sync>
    RenderCommandOutputTListDecodable<ElementConfig<'render, S, I, C, E>>
    for Rectangle<'render, E>
{
    fn decode_element(
        output: &mut RenderCommandOutput<ElementConfig<'render, S, I, C, E>>,
        buffer: &mut Vec<Self>,
    ) {
        if let RenderCommandOutput::Element(element) = output {
            #[cfg(not(feature = "complex_color"))]
            if element.custom.style.background_color.a <= 0.0 {
                return;
            }

            buffer.push(Rectangle {
                info: RenderCommandInfo {
                    bounding_box: element.bounds,
                    id: element.custom.id_config.get_handle().get_id() as u32,
                    z_index: element.z_index as i16,
                    extra_data: element.custom.extra_data.as_mut().map(|s| s.share_clone()),
                },
                #[cfg(not(feature = "complex_color"))]
                color: element.custom.style.background_color,
                #[cfg(feature = "complex_color")]
                color: element.custom.style.background_color.clone(),
                corner_radii: CornerRadii {
                    top_left: element.custom.style.corner_radius.top_left,
                    top_right: element.custom.style.corner_radius.top_right,
                    bottom_left: element.custom.style.corner_radius.bottom_left,
                    bottom_right: element.custom.style.corner_radius.bottom_right,
                },
            })
        }
    }
}

impl<'render, S, I, C, E: 'render + Send + Sync>
    RenderCommandOutputTListDecodable<ElementConfig<'render, S, I, C, E>> for Border<'render, E>
{
    fn decode_element(
        output: &mut RenderCommandOutput<ElementConfig<'render, S, I, C, E>>,
        buffer: &mut Vec<Self>,
    ) {
        if let RenderCommandOutput::Element(element) = output {
            let w = &element.custom.style.border.width;
            if w.left <= 0.0
                && w.right <= 0.0
                && w.top <= 0.0
                && w.bottom <= 0.0
                && w.between_children <= 0.0
            {
                return;
            }

            buffer.push(Border {
                info: RenderCommandInfo {
                    bounding_box: element.bounds,
                    id: element.custom.id_config.get_handle().get_id() as u32,
                    z_index: element.z_index as i16,
                    extra_data: element.custom.extra_data.as_mut().map(|s| s.share_clone()),
                },
                color: element.custom.style.border.color,
                width: BorderWidth {
                    left: w.left,
                    right: w.right,
                    top: w.top,
                    bottom: w.bottom,
                    between_children: w.between_children,
                },
                corner_radii: CornerRadii {
                    top_left: element.custom.style.corner_radius.top_left,
                    top_right: element.custom.style.corner_radius.top_right,
                    bottom_left: element.custom.style.corner_radius.bottom_left,
                    bottom_right: element.custom.style.corner_radius.bottom_right,
                },
            })
        }
    }
}

impl<'render, S, I: 'render, C, E: 'render + Send + Sync>
    RenderCommandOutputTListDecodable<ElementConfig<'render, S, I, C, E>> for Image<'render, I, E>
{
    fn decode_element(
        output: &mut RenderCommandOutput<ElementConfig<'render, S, I, C, E>>,
        buffer: &mut Vec<Self>,
    ) {
        if let RenderCommandOutput::Element(element) = output {
            buffer.push(Image {
                data: match element.custom.image.take() {
                    None => return,
                    Some(s) => s.image,
                },
                info: RenderCommandInfo {
                    bounding_box: element.bounds,
                    id: element.custom.id_config.get_handle().get_id() as u32,
                    z_index: element.z_index as i16,
                    extra_data: element.custom.extra_data.as_mut().map(|s| s.share_clone()),
                },
                background_color: {
                    #[cfg(not(feature = "complex_color"))]
                    {
                        element.custom.style.background_color
                    }
                    #[cfg(feature = "complex_color")]
                    {
                        element.custom.style.background_color.clone()
                    }
                },
                corner_radii: CornerRadii {
                    top_left: element.custom.style.corner_radius.top_left,
                    top_right: element.custom.style.corner_radius.top_right,
                    bottom_left: element.custom.style.corner_radius.bottom_left,
                    bottom_right: element.custom.style.corner_radius.bottom_right,
                },
            })
        }
    }
}

impl<'render, S, I: 'render, C, E: 'render + Send + Sync>
    RenderCommandOutputTListDecodable<ElementConfig<'render, S, I, C, E>> for Text<'render, E>
{
    fn decode_element(
        output: &mut RenderCommandOutput<ElementConfig<'render, S, I, C, E>>,
        buffer: &mut Vec<Self>,
    ) {
        if let RenderCommandOutput::Element(element) = output {
            let text = match element.text.take() {
                None => return,
                Some(s) => s,
            };
            let text_s = text.get_str().to_owned();
            buffer.push(Text {
                info: RenderCommandInfo {
                    bounding_box: element.bounds,
                    id: element.custom.id_config.get_handle().get_id() as u32,
                    z_index: element.z_index as i16,
                    extra_data: element.custom.extra_data.as_mut().map(|s| s.share_clone()),
                },
                #[cfg(not(feature = "complex_colored_text"))]
                color: text.style.color,
                #[cfg(feature = "complex_colored_text")]
                color: text.style.color.clone(),
                font_id: text.config.font_id,
                font_size: text.config.font_size,
                letter_spacing: text.config.letter_spacing,
                line_height: text.config.line_height,
                text: OwnedOrRef::AsOwnedRef(Arc::new(text_s)),
            })
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use cotis::utils::ElementIdConfig;
    use cotis_defaults::colors::Color;
    use cotis_defaults::element_configs::premade_configs::GenericElementConfig;
    use cotis_defaults::element_configs::style::sizing::Sizing;
    use cotis_defaults::element_configs::style::{
        BorderElementStyle, BorderWidthStyle, CotisStyle,
    };
    use cotis_layout::layout_struct::RenderCommandOutputElement;
    use cotis_utils::math::BoundingBox;

    type TestConfig = GenericElementConfig<'static, (), ()>;

    fn element_with_border(width: BorderWidthStyle) -> RenderCommandOutput<TestConfig> {
        RenderCommandOutput::Element(RenderCommandOutputElement {
            bounds: BoundingBox {
                x: 0.0,
                y: 0.0,
                width: 100.0,
                height: 50.0,
            },
            z_index: 0,
            text: None,
            custom: TestConfig {
                id_config: ElementIdConfig::new_empty(),
                style: CotisStyle {
                    border: BorderElementStyle {
                        color: Color::rgba(255.0, 0.0, 0.0, 255.0),
                        width,
                    },
                    ..CotisStyle::<Sizing>::default()
                },
                image: None,
                custom: None,
                extra_data: None,
            },
        })
    }

    #[test]
    fn border_emits_when_perimeter_positive_and_between_children_zero() {
        let mut output = element_with_border(BorderWidthStyle {
            left: 2.0,
            right: 2.0,
            top: 2.0,
            bottom: 2.0,
            between_children: 0.0,
        });

        let mut buffer = Vec::<Border<'static, ()>>::new();
        Border::decode_element(&mut output, &mut buffer);

        assert_eq!(buffer.len(), 1);
        assert_eq!(buffer[0].width.left, 2.0);
        assert_eq!(buffer[0].width.between_children, 0.0);
    }

    #[test]
    fn border_skipped_when_all_widths_zero() {
        let mut output = element_with_border(BorderWidthStyle {
            left: 0.0,
            right: 0.0,
            top: 0.0,
            bottom: 0.0,
            between_children: 0.0,
        });

        let mut buffer = Vec::<Border<'static, ()>>::new();
        Border::decode_element(&mut output, &mut buffer);

        assert!(buffer.is_empty());
    }
}