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;