graphic.rs raw

//! [`Graphic`]: something that draws itself into a rectangle — vector
//! geometry, or pixels.
//!
//! The two are interchangeable in *what gets drawn*: wherever a widget wants
//! a mark, an icon, or a picture, either kind will do, and the widget hands
//! it a rect without caring which it got. They are not interchangeable in
//! *who supplies the color* — a path is bare geometry that the caller paints,
//! while an image carries its own pixels — so [`paint_into`] takes a
//! [`GraphicPaint`] that a path obeys and an image ignores. A theme varies an
//! image mark across states by swapping the asset rather than by recoloring
//! it, which is how icon themes work anyway.
//!
//! [`paint_into`]: Graphic::paint_into

use guiduck_scene::Fragment;
use guiduck_scene::geom::{Affine, BezPath, Rect, Size, Stroke};
use guiduck_scene::paint::{Brush, ImageBrush};

/// The compiler's drawing commands, re-exported: both the style engine and
/// generated code build a [`Graphic::Path`] from these.
pub use guiduck_component_core::ir::PathCmd;

/// A drawable: vector geometry, or pixels.
#[derive(Clone, Debug, PartialEq)]
pub enum Graphic {
    /// A path in its own coordinate space — conventionally the unit square —
    /// scaled into whatever rectangle it is drawn into.
    Path(BezPath),
    /// Decoded pixels, scaled into whatever rectangle they are drawn into.
    Image(ImageBrush),
}

/// How to paint a [`Graphic::Path`]. A [`Graphic::Image`] ignores it: its
/// pixels already carry color.
#[derive(Clone, Debug, PartialEq)]
pub enum GraphicPaint {
    /// Fill the enclosed area — the natural reading of a shape.
    Fill(Brush),
    /// Stroke the outline at a width in device pixels (applied after the
    /// path is scaled, so the line does not thicken with the element).
    Stroke(Brush, f64),
}

impl Default for Graphic {
    /// The empty path: no geometry, so no natural size and nothing drawn.
    /// This is what a graphic-valued property holds before it is given one.
    fn default() -> Self {
        Self::Path(BezPath::new())
    }
}

impl Graphic {
    /// Build a path graphic from the compiler's drawing commands. The one
    /// translation from compiled `(path …)` / `(svg-path …)` data into
    /// geometry, shared by the style engine, generated code, and the
    /// interpreter.
    pub fn from_path_cmds(cmds: &[PathCmd]) -> Self {
        let mut path = BezPath::new();
        for cmd in cmds {
            match *cmd {
                PathCmd::Move(x, y) => path.move_to((x, y)),
                PathCmd::Line(x, y) => path.line_to((x, y)),
                PathCmd::Quad(cx, cy, x, y) => path.quad_to((cx, cy), (x, y)),
                PathCmd::Cubic(a, b, c, d, x, y) => path.curve_to((a, b), (c, d), (x, y)),
                PathCmd::Close => path.close_path(),
            }
        }
        Self::Path(path)
    }

    /// Draw into `rect`, scaling to fill it.
    ///
    /// The one definition of what drawing a graphic means, so a checkbox's
    /// mark and an image widget's content cannot disagree about it. The
    /// target rectangle defines the size in both cases: a path has no size of
    /// its own, and an image's intrinsic size informs *layout*, never the
    /// draw.
    pub fn paint_into(&self, fragment: &mut Fragment, rect: Rect, paint: &GraphicPaint) {
        if rect.width() <= 0.0 || rect.height() <= 0.0 {
            return;
        }
        match self {
            Self::Path(path) => {
                let mut scaled = path.clone();
                scaled.apply_affine(
                    Affine::translate((rect.x0, rect.y0))
                        * Affine::scale_non_uniform(rect.width(), rect.height()),
                );
                match paint {
                    GraphicPaint::Fill(brush) => fragment.fill(scaled, brush.clone()),
                    GraphicPaint::Stroke(brush, width) => {
                        fragment.stroke(scaled, brush.clone(), Stroke::new(*width));
                    }
                }
            }
            Self::Image(brush) => fragment.image(brush.clone(), rect),
        }
    }

    /// The size the graphic would like to be when nothing else constrains it,
    /// for layout only.
    ///
    /// An image knows: its pixels. A path does not — it is scale-free
    /// geometry, and inventing a size for it would be a lie — so it must be
    /// given a box, by an explicit size or by flex.
    pub fn natural_size(&self) -> Option<Size> {
        match self {
            Self::Path(_) => None,
            Self::Image(brush) => Some(Size::new(
                f64::from(brush.image.width),
                f64::from(brush.image.height),
            )),
        }
    }
}

#[cfg(test)]
mod tests;