//! [`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 { 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;