cotis-wgpu 0.1.0-alpha

Desktop wgpu renderer backend for Cotis
Documentation
//! [`crate::drawable::WgpuDrawable`] implementations for standard `cotis-defaults` render commands.
//!
//! These impls connect layout pipe output to the wgpu geometry batch and glyphon text system.
//!
//! # Supported commands
//!
//! | Command | Behavior |
//! |---------|----------|
//! | [`cotis_defaults::render_commands::Rectangle`] | Filled rounded rectangle |
//! | [`cotis_defaults::render_commands::Border`] | Rounded border outline |
//! | [`cotis_defaults::render_commands::Text`] | Glyphon text at bounding box origin |
//! | [`cotis_defaults::render_commands::Image`] | **Placeholder:** filled rectangle using background color (no texture) |
//! | [`cotis_defaults::render_commands::ClipStart`] / [`cotis_defaults::render_commands::ClipEnd`] | Text scissor region (not geometry shader clipping) |
//! | [`cotis_defaults::render_commands::render_t_list::RenderTList`] | Recursive list traversal |
//!
//! # `complex-color` feature
//!
//! When enabled, color fields become [`ColorLayer`](cotis_defaults::colors::ColorLayer) instead of
//! plain [`cotis_defaults::colors::Color`]. Non-solid layers are collapsed to a single color:
//!
//! - `Solid` — used as-is.
//! - `Linear`, `Radial`, `RoundedRect` — resolve to transparent black (gradients are **not** rendered).
//! - `Layered` — only the first sub-layer is used, recursively.
//!
//! Enable with `cotis-wgpu` feature `complex-color` or in `Cargo.toml`:
//!
//! ```toml
//! cotis-wgpu = { version = "0.1.0-alpha", features = ["complex-color"] }
//! ```

use cotis_defaults::colors::Color;
#[cfg(feature = "complex-color")]
use cotis_defaults::colors::ColorLayer;
use cotis_defaults::render_commands::render_t_list::{IsRenderList, RenderTList};
use cotis_defaults::render_commands::*;
use glyphon::Color as GlyphColor;

use crate::color::cotis_color_to_ui_color;
use crate::drawable::{WgpuDrawContext, WgpuDrawable};
use crate::pipeline::{UIBorderThickness, UICornerRadii, UIPosition};

/// Draws a nested [`RenderTList`] command and its tail list recursively.
impl<T: WgpuDrawable, L: IsRenderList + WgpuDrawable> WgpuDrawable for RenderTList<T, L> {
    fn draw(self: Box<Self>, ctx: &mut WgpuDrawContext<'_>) {
        match *self {
            RenderTList::Type(command) => Box::new(command).draw(ctx),
            RenderTList::List(list) => Box::new(list).draw(ctx),
        }
    }
}

/// Draws a single-command [`RenderTList`] (empty tail).
impl<T: WgpuDrawable> WgpuDrawable for RenderTList<T, ()> {
    fn draw(self: Box<Self>, ctx: &mut WgpuDrawContext<'_>) {
        match *self {
            RenderTList::Type(command) => Box::new(command).draw(ctx),
            RenderTList::List(()) => {}
        }
    }
}

/// Draws a filled rounded rectangle at the command's bounding box.
impl<'a, ExtraData> WgpuDrawable for Rectangle<'a, ExtraData> {
    fn draw(self: Box<Self>, ctx: &mut WgpuDrawContext<'_>) {
        let bb = self.info.bounding_box;
        let color = resolve_rectangle_color(self.as_ref());
        ctx.geometry.filled_rectangle(
            UIPosition {
                x: bb.x,
                y: bb.y,
                z: *ctx.depth,
            },
            UIPosition {
                x: bb.width,
                y: bb.height,
                z: *ctx.depth,
            },
            color,
            corner_radii_to_ui(&self.corner_radii),
        );
        *ctx.depth -= 0.000_1;
    }
}

/// Draws a rounded border outline at the command's bounding box.
impl<'a, ExtraData> WgpuDrawable for Border<'a, ExtraData> {
    fn draw(self: Box<Self>, ctx: &mut WgpuDrawContext<'_>) {
        let bb = self.info.bounding_box;
        ctx.geometry.rectangle(
            UIPosition {
                x: bb.x,
                y: bb.y,
                z: *ctx.depth,
            },
            UIPosition {
                x: bb.width,
                y: bb.height,
                z: *ctx.depth,
            },
            UIBorderThickness {
                top: self.width.top,
                left: self.width.left,
                bottom: self.width.bottom,
                right: self.width.right,
            },
            cotis_color_to_ui_color(self.color),
            corner_radii_to_ui(&self.corner_radii),
        );
        *ctx.depth -= 0.000_1;
    }
}

/// Queues glyphon text at the command's bounding box origin.
///
/// Font size and line height are multiplied by `ctx.dpi_scale`. `font_id` from the layout pipe
/// is not used — all text renders with glyphon `Family::SansSerif`.
impl<'a, ExtraData> WgpuDrawable for Text<'a, ExtraData> {
    fn draw(self: Box<Self>, ctx: &mut WgpuDrawContext<'_>) {
        let bb = self.info.bounding_box;
        let color = resolve_text_color(self.as_ref());
        let line_height = if self.line_height > 0.0 {
            self.line_height * ctx.dpi_scale
        } else {
            self.font_size * 1.5 * ctx.dpi_scale
        };

        let scissor_bounds = if ctx.scissor_active {
            Some((ctx.scissor_position, ctx.scissor_bounds))
        } else {
            None
        };

        ctx.text.queue_text(
            self.text.as_ref(),
            self.font_size * ctx.dpi_scale,
            line_height,
            UIPosition {
                x: bb.x,
                y: bb.y,
                z: *ctx.depth,
            },
            scissor_bounds,
            color,
            *ctx.depth,
        );
        *ctx.depth -= 0.000_1;
    }
}

/// Draws a filled rectangle placeholder for an image command.
///
/// Texture loading is not implemented; only `background_color` is rendered.
impl<'a, ImageElementData, ExtraData> WgpuDrawable for Image<'a, ImageElementData, ExtraData> {
    fn draw(self: Box<Self>, ctx: &mut WgpuDrawContext<'_>) {
        let bb = self.info.bounding_box;
        let color = resolve_image_background_color(self.as_ref());
        ctx.geometry.filled_rectangle(
            UIPosition {
                x: bb.x,
                y: bb.y,
                z: *ctx.depth,
            },
            UIPosition {
                x: bb.width,
                y: bb.height,
                z: *ctx.depth,
            },
            color,
            corner_radii_to_ui(&self.corner_radii),
        );
        *ctx.depth -= 0.000_1;
    }
}

/// Activates a text scissor region for subsequent [`Text`] commands.
///
/// Scissor affects glyphon text bounds only, not the geometry shader.
impl<'a, ExtraData> WgpuDrawable for ClipStart<'a, ExtraData> {
    fn draw(self: Box<Self>, ctx: &mut WgpuDrawContext<'_>) {
        let bb = self.info.bounding_box;
        ctx.scissor_position = UIPosition {
            x: bb.x,
            y: bb.y,
            z: 0.0,
        };
        ctx.scissor_bounds = UIPosition {
            x: bb.width.max(0.0),
            y: bb.height.max(0.0),
            z: 0.0,
        };
        ctx.scissor_active = self.horizontal || self.vertical;
        *ctx.depth -= 0.000_1;
    }
}

/// Deactivates the text scissor region opened by [`ClipStart`].
impl WgpuDrawable for ClipEnd {
    fn draw(self: Box<Self>, ctx: &mut WgpuDrawContext<'_>) {
        let _ = self;
        ctx.scissor_active = false;
        *ctx.depth -= 0.000_1;
    }
}

fn corner_radii_to_ui(radii: &CornerRadii) -> UICornerRadii {
    UICornerRadii {
        top_left: radii.top_left,
        top_right: radii.top_right,
        bottom_left: radii.bottom_left,
        bottom_right: radii.bottom_right,
    }
}

#[cfg(not(feature = "complex-color"))]
fn resolve_rectangle_color<ExtraData>(
    rectangle: &Rectangle<'_, ExtraData>,
) -> crate::pipeline::UIColor {
    cotis_color_to_ui_color(rectangle.color)
}

#[cfg(feature = "complex-color")]
fn resolve_rectangle_color<ExtraData>(
    rectangle: &Rectangle<'_, ExtraData>,
) -> crate::pipeline::UIColor {
    cotis_color_to_ui_color(color_layer_to_solid(&rectangle.color))
}

#[cfg(not(feature = "complex-color"))]
fn resolve_image_background_color<ImageElementData, ExtraData>(
    image: &Image<'_, ImageElementData, ExtraData>,
) -> crate::pipeline::UIColor {
    cotis_color_to_ui_color(image.background_color)
}

#[cfg(feature = "complex-color")]
fn resolve_image_background_color<ImageElementData, ExtraData>(
    image: &Image<'_, ImageElementData, ExtraData>,
) -> crate::pipeline::UIColor {
    cotis_color_to_ui_color(color_layer_to_solid(&image.background_color))
}

#[cfg(not(feature = "complex-color"))]
fn resolve_text_color<ExtraData>(text: &Text<'_, ExtraData>) -> GlyphColor {
    cotis_color_to_glyph(text.color)
}

#[cfg(feature = "complex-color")]
fn resolve_text_color<ExtraData>(text: &Text<'_, ExtraData>) -> GlyphColor {
    cotis_color_to_glyph(color_layer_to_solid(&text.color))
}

fn cotis_color_to_glyph(color: Color) -> GlyphColor {
    GlyphColor::rgba(
        color.r.round() as u8,
        color.g.round() as u8,
        color.b.round() as u8,
        color.a.round() as u8,
    )
}

#[cfg(feature = "complex-color")]
fn color_layer_to_solid(layer: &ColorLayer) -> Color {
    match layer {
        ColorLayer::Solid(color) => *color,
        ColorLayer::Linear(_) | ColorLayer::Radial(_) | ColorLayer::RoundedRect(_) => {
            Color::rgba(0.0, 0.0, 0.0, 0.0)
        }
        ColorLayer::Layered(layered) => layered
            .layers
            .first()
            .map(color_layer_to_solid)
            .unwrap_or_else(|| Color::rgba(0.0, 0.0, 0.0, 0.0)),
    }
}