image.rs raw

//! 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<String>) -> Self {
        self.set_alt(alt);
        self
    }

    pub fn set_alt(&mut self, alt: impl Into<String>) {
        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<Option<f32>>,
        _available: taffy::Size<AvailableSpace>,
    ) -> taffy::Size<f32> {
        // 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();
        }
    }
}