//! The Image primitive: a [`Graphic`] drawn into the widget's bounds. //! //! It draws either kind. Pixels carry their own color; a `(path …)` source is //! bare geometry, so it is filled with the themable `color` token — the same //! division of labor the checkbox mark follows. //! //! Sizing follows from what each kind actually knows. Whatever box the widget //! ends up with, the graphic scales to fill it — the target defines the size. //! What differs is only what the widget can tell *layout* when nothing else //! constrains it: an image reports its natural pixels, and a path reports //! nothing, because scale-free geometry has no size and inventing one would be //! a lie. So a path source must be given a box, by an explicit size or by //! flex; an unsized one draws nothing. use guiduck_scene::Fragment; use guiduck_scene::geom::{Rect, Size}; use guiduck_scene::paint::{Brush, Color}; use taffy::AvailableSpace; use super::Widget; use crate::dirty::Dirt; use crate::graphic::{Graphic, GraphicPaint}; use crate::style::{ComputedStyle, StyleReader}; use crate::text::TextContext; /// The appearance tokens an image reads from the theme. struct Tokens { /// The brush a `(path …)` source is filled with. A raster source ignores /// it: its pixels already carry color. color: Brush, } impl Tokens { fn blank() -> Self { Self { color: Color::TRANSPARENT.into(), } } fn read(&mut self, style: &ComputedStyle) -> bool { let mut reader = StyleReader::new(style); reader.brush(&mut self.color, "color"); reader.changed() } } impl Default for Tokens { fn default() -> Self { let mut tokens = Self::blank(); tokens.read(&super::fallback_style("image")); tokens } } pub struct Image { source: Graphic, /// What the image *says*, for assistive technology. An image with no name /// is invisible to a screen reader, so a decorative one should say so by /// staying empty and a meaningful one must not. alt: String, tokens: Tokens, dirt: Dirt, } impl Default for Image { fn default() -> Self { Self { source: Graphic::default(), alt: String::new(), tokens: Tokens::default(), dirt: Dirt::CLEAN, } } } impl Image { pub fn new(source: Graphic) -> Self { let mut image = Self::default(); image.set_source(source); image } /// The graphic the image draws. Layout-affecting: a raster's intrinsic /// size is its natural pixels. pub fn set_source(&mut self, source: Graphic) { if self.source != source { self.source = source; self.dirt.mark_layout(); } } /// What the image says, for assistive technology. Leave it off only when /// the image is decoration that a reader loses nothing by skipping. pub fn alt(mut self, alt: impl Into) -> Self { self.set_alt(alt); self } pub fn set_alt(&mut self, alt: impl Into) { let alt = alt.into(); if self.alt != alt { self.alt = alt; self.dirt.mark_paint(); } } pub fn source(&self) -> &Graphic { &self.source } } impl Widget for Image { fn measure( &mut self, _text: &mut TextContext, known: taffy::Size>, _available: taffy::Size, ) -> taffy::Size { // Anything layout has already decided stands. if let (Some(width), Some(height)) = (known.width, known.height) { return taffy::Size { width, height }; } // A path has nothing to contribute; it takes whatever it is given, // which is zero if it was given nothing. let Some(natural) = self.source.natural_size() else { return taffy::Size { width: known.width.unwrap_or(0.0), height: known.height.unwrap_or(0.0), }; }; let aspect = if natural.height > 0.0 { (natural.width / natural.height) as f32 } else { 1.0 }; match (known.width, known.height) { (Some(width), None) => taffy::Size { width, height: width / aspect, }, (None, Some(height)) => taffy::Size { width: height * aspect, height, }, _ => taffy::Size { width: natural.width as f32, height: natural.height as f32, }, } } fn paint(&mut self, fragment: &mut Fragment, size: Size) { self.source.paint_into( fragment, Rect::from_origin_size((0.0, 0.0), size), &GraphicPaint::Fill(self.tokens.color.clone()), ); } fn role(&self) -> accesskit::Role { accesskit::Role::Image } fn accessibility(&self, node: &mut accesskit::Node) { if !self.alt.is_empty() { node.set_label(self.alt.clone()); } } fn take_dirt(&mut self) -> Dirt { std::mem::take(&mut self.dirt) } fn type_name(&self) -> &'static str { "image" } fn apply_style(&mut self, style: &ComputedStyle) { if self.tokens.read(style) { self.dirt.mark_paint(); } } }