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();
}
}
}