Skip to main content

cotis_wgpu/
default_drawables.rs

1//! [`crate::drawable::WgpuDrawable`] implementations for standard `cotis-defaults` render commands.
2//!
3//! These impls connect layout pipe output to the wgpu geometry batch and glyphon text system.
4//!
5//! # Supported commands
6//!
7//! | Command | Behavior |
8//! |---------|----------|
9//! | [`cotis_defaults::render_commands::Rectangle`] | Filled rounded rectangle |
10//! | [`cotis_defaults::render_commands::Border`] | Rounded border outline |
11//! | [`cotis_defaults::render_commands::Text`] | Glyphon text at bounding box origin |
12//! | [`cotis_defaults::render_commands::Image`] | **Placeholder:** filled rectangle using background color (no texture) |
13//! | [`cotis_defaults::render_commands::ClipStart`] / [`cotis_defaults::render_commands::ClipEnd`] | Text scissor region (not geometry shader clipping) |
14//! | [`cotis_defaults::render_commands::render_t_list::RenderTList`] | Recursive list traversal |
15//!
16//! # `complex-color` feature
17//!
18//! When enabled, color fields become [`ColorLayer`](cotis_defaults::colors::ColorLayer) instead of
19//! plain [`cotis_defaults::colors::Color`]. Non-solid layers are collapsed to a single color:
20//!
21//! - `Solid` — used as-is.
22//! - `Linear`, `Radial`, `RoundedRect` — resolve to transparent black (gradients are **not** rendered).
23//! - `Layered` — only the first sub-layer is used, recursively.
24//!
25//! Enable with `cotis-wgpu` feature `complex-color` or in `Cargo.toml`:
26//!
27//! ```toml
28//! cotis-wgpu = { version = "0.1.0-alpha", features = ["complex-color"] }
29//! ```
30
31use cotis_defaults::colors::Color;
32#[cfg(feature = "complex-color")]
33use cotis_defaults::colors::ColorLayer;
34use cotis_defaults::render_commands::render_t_list::{IsRenderList, RenderTList};
35use cotis_defaults::render_commands::*;
36use glyphon::Color as GlyphColor;
37
38use crate::color::cotis_color_to_ui_color;
39use crate::drawable::{WgpuDrawContext, WgpuDrawable};
40use crate::pipeline::{UIBorderThickness, UICornerRadii, UIPosition};
41
42/// Draws a nested [`RenderTList`] command and its tail list recursively.
43impl<T: WgpuDrawable, L: IsRenderList + WgpuDrawable> WgpuDrawable for RenderTList<T, L> {
44    fn draw(self: Box<Self>, ctx: &mut WgpuDrawContext<'_>) {
45        match *self {
46            RenderTList::Type(command) => Box::new(command).draw(ctx),
47            RenderTList::List(list) => Box::new(list).draw(ctx),
48        }
49    }
50}
51
52/// Draws a single-command [`RenderTList`] (empty tail).
53impl<T: WgpuDrawable> WgpuDrawable for RenderTList<T, ()> {
54    fn draw(self: Box<Self>, ctx: &mut WgpuDrawContext<'_>) {
55        match *self {
56            RenderTList::Type(command) => Box::new(command).draw(ctx),
57            RenderTList::List(()) => {}
58        }
59    }
60}
61
62/// Draws a filled rounded rectangle at the command's bounding box.
63impl<'a, ExtraData> WgpuDrawable for Rectangle<'a, ExtraData> {
64    fn draw(self: Box<Self>, ctx: &mut WgpuDrawContext<'_>) {
65        let bb = self.info.bounding_box;
66        let color = resolve_rectangle_color(self.as_ref());
67        ctx.geometry.filled_rectangle(
68            UIPosition {
69                x: bb.x,
70                y: bb.y,
71                z: *ctx.depth,
72            },
73            UIPosition {
74                x: bb.width,
75                y: bb.height,
76                z: *ctx.depth,
77            },
78            color,
79            corner_radii_to_ui(&self.corner_radii),
80        );
81        *ctx.depth -= 0.000_1;
82    }
83}
84
85/// Draws a rounded border outline at the command's bounding box.
86impl<'a, ExtraData> WgpuDrawable for Border<'a, ExtraData> {
87    fn draw(self: Box<Self>, ctx: &mut WgpuDrawContext<'_>) {
88        let bb = self.info.bounding_box;
89        ctx.geometry.rectangle(
90            UIPosition {
91                x: bb.x,
92                y: bb.y,
93                z: *ctx.depth,
94            },
95            UIPosition {
96                x: bb.width,
97                y: bb.height,
98                z: *ctx.depth,
99            },
100            UIBorderThickness {
101                top: self.width.top,
102                left: self.width.left,
103                bottom: self.width.bottom,
104                right: self.width.right,
105            },
106            cotis_color_to_ui_color(self.color),
107            corner_radii_to_ui(&self.corner_radii),
108        );
109        *ctx.depth -= 0.000_1;
110    }
111}
112
113/// Queues glyphon text at the command's bounding box origin.
114///
115/// Font size and line height are multiplied by `ctx.dpi_scale`. `font_id` from the layout pipe
116/// is not used — all text renders with glyphon `Family::SansSerif`.
117impl<'a, ExtraData> WgpuDrawable for Text<'a, ExtraData> {
118    fn draw(self: Box<Self>, ctx: &mut WgpuDrawContext<'_>) {
119        let bb = self.info.bounding_box;
120        let color = resolve_text_color(self.as_ref());
121        let line_height = if self.line_height > 0.0 {
122            self.line_height * ctx.dpi_scale
123        } else {
124            self.font_size * 1.5 * ctx.dpi_scale
125        };
126
127        let scissor_bounds = if ctx.scissor_active {
128            Some((ctx.scissor_position, ctx.scissor_bounds))
129        } else {
130            None
131        };
132
133        ctx.text.queue_text(
134            self.text.as_ref(),
135            self.font_size * ctx.dpi_scale,
136            line_height,
137            UIPosition {
138                x: bb.x,
139                y: bb.y,
140                z: *ctx.depth,
141            },
142            scissor_bounds,
143            color,
144            *ctx.depth,
145        );
146        *ctx.depth -= 0.000_1;
147    }
148}
149
150/// Draws a filled rectangle placeholder for an image command.
151///
152/// Texture loading is not implemented; only `background_color` is rendered.
153impl<'a, ImageElementData, ExtraData> WgpuDrawable for Image<'a, ImageElementData, ExtraData> {
154    fn draw(self: Box<Self>, ctx: &mut WgpuDrawContext<'_>) {
155        let bb = self.info.bounding_box;
156        let color = resolve_image_background_color(self.as_ref());
157        ctx.geometry.filled_rectangle(
158            UIPosition {
159                x: bb.x,
160                y: bb.y,
161                z: *ctx.depth,
162            },
163            UIPosition {
164                x: bb.width,
165                y: bb.height,
166                z: *ctx.depth,
167            },
168            color,
169            corner_radii_to_ui(&self.corner_radii),
170        );
171        *ctx.depth -= 0.000_1;
172    }
173}
174
175/// Activates a text scissor region for subsequent [`Text`] commands.
176///
177/// Scissor affects glyphon text bounds only, not the geometry shader.
178impl<'a, ExtraData> WgpuDrawable for ClipStart<'a, ExtraData> {
179    fn draw(self: Box<Self>, ctx: &mut WgpuDrawContext<'_>) {
180        let bb = self.info.bounding_box;
181        ctx.scissor_position = UIPosition {
182            x: bb.x,
183            y: bb.y,
184            z: 0.0,
185        };
186        ctx.scissor_bounds = UIPosition {
187            x: bb.width.max(0.0),
188            y: bb.height.max(0.0),
189            z: 0.0,
190        };
191        ctx.scissor_active = self.horizontal || self.vertical;
192        *ctx.depth -= 0.000_1;
193    }
194}
195
196/// Deactivates the text scissor region opened by [`ClipStart`].
197impl WgpuDrawable for ClipEnd {
198    fn draw(self: Box<Self>, ctx: &mut WgpuDrawContext<'_>) {
199        let _ = self;
200        ctx.scissor_active = false;
201        *ctx.depth -= 0.000_1;
202    }
203}
204
205fn corner_radii_to_ui(radii: &CornerRadii) -> UICornerRadii {
206    UICornerRadii {
207        top_left: radii.top_left,
208        top_right: radii.top_right,
209        bottom_left: radii.bottom_left,
210        bottom_right: radii.bottom_right,
211    }
212}
213
214#[cfg(not(feature = "complex-color"))]
215fn resolve_rectangle_color<ExtraData>(
216    rectangle: &Rectangle<'_, ExtraData>,
217) -> crate::pipeline::UIColor {
218    cotis_color_to_ui_color(rectangle.color)
219}
220
221#[cfg(feature = "complex-color")]
222fn resolve_rectangle_color<ExtraData>(
223    rectangle: &Rectangle<'_, ExtraData>,
224) -> crate::pipeline::UIColor {
225    cotis_color_to_ui_color(color_layer_to_solid(&rectangle.color))
226}
227
228#[cfg(not(feature = "complex-color"))]
229fn resolve_image_background_color<ImageElementData, ExtraData>(
230    image: &Image<'_, ImageElementData, ExtraData>,
231) -> crate::pipeline::UIColor {
232    cotis_color_to_ui_color(image.background_color)
233}
234
235#[cfg(feature = "complex-color")]
236fn resolve_image_background_color<ImageElementData, ExtraData>(
237    image: &Image<'_, ImageElementData, ExtraData>,
238) -> crate::pipeline::UIColor {
239    cotis_color_to_ui_color(color_layer_to_solid(&image.background_color))
240}
241
242#[cfg(not(feature = "complex-color"))]
243fn resolve_text_color<ExtraData>(text: &Text<'_, ExtraData>) -> GlyphColor {
244    cotis_color_to_glyph(text.color)
245}
246
247#[cfg(feature = "complex-color")]
248fn resolve_text_color<ExtraData>(text: &Text<'_, ExtraData>) -> GlyphColor {
249    cotis_color_to_glyph(color_layer_to_solid(&text.color))
250}
251
252fn cotis_color_to_glyph(color: Color) -> GlyphColor {
253    GlyphColor::rgba(
254        color.r.round() as u8,
255        color.g.round() as u8,
256        color.b.round() as u8,
257        color.a.round() as u8,
258    )
259}
260
261#[cfg(feature = "complex-color")]
262fn color_layer_to_solid(layer: &ColorLayer) -> Color {
263    match layer {
264        ColorLayer::Solid(color) => *color,
265        ColorLayer::Linear(_) | ColorLayer::Radial(_) | ColorLayer::RoundedRect(_) => {
266            Color::rgba(0.0, 0.0, 0.0, 0.0)
267        }
268        ColorLayer::Layered(layered) => layered
269            .layers
270            .first()
271            .map(color_layer_to_solid)
272            .unwrap_or_else(|| Color::rgba(0.0, 0.0, 0.0, 0.0)),
273    }
274}