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}