use std::path::PathBuf;
use std::sync::Arc;
use crate::image_decode::DecodeHandle;
use crate::layout::{Constraints, Size};
use crate::reactive::{IntoSignal, Prop, Signal, create_memo};
use crate::renderer::PaintContext;
use crate::tree::LayoutCtx;
use super::widget::{Color, Rect, Widget};
#[derive(Debug, Clone, PartialEq)]
pub enum ImageSource {
Path(PathBuf),
Bytes(Arc<[u8]>),
Rgba {
width: u32,
height: u32,
pixels: Arc<[u8]>,
},
SvgPath(PathBuf),
SvgBytes(Arc<[u8]>),
}
impl ImageSource {
pub fn is_svg(&self) -> bool {
matches!(self, ImageSource::SvgPath(_) | ImageSource::SvgBytes(_))
}
}
impl From<&str> for ImageSource {
fn from(path: &str) -> Self {
let path = PathBuf::from(path);
if path
.extension()
.is_some_and(|ext| ext.eq_ignore_ascii_case("svg"))
{
ImageSource::SvgPath(path)
} else {
ImageSource::Path(path)
}
}
}
impl From<String> for ImageSource {
fn from(path: String) -> Self {
ImageSource::from(path.as_str())
}
}
impl From<PathBuf> for ImageSource {
fn from(path: PathBuf) -> Self {
if path
.extension()
.is_some_and(|ext| ext.eq_ignore_ascii_case("svg"))
{
ImageSource::SvgPath(path)
} else {
ImageSource::Path(path)
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub enum ContentFit {
#[default]
Contain,
Cover,
Fill,
None,
}
pub struct Image {
source: Signal<ImageSource>,
content_fit: Prop<ContentFit>,
tint: Prop<Color>,
cached_content_fit: ContentFit,
intrinsic_size: Option<(f32, f32)>,
cached_source: Option<ImageSource>,
decode: Option<DecodeHandle>,
}
impl Image {
pub fn new<M>(source: impl IntoSignal<ImageSource, M>) -> Self {
Self {
source: source.into_signal(),
content_fit: Prop::Unset,
tint: Prop::Unset,
cached_content_fit: ContentFit::default(),
intrinsic_size: None,
cached_source: None,
decode: None,
}
}
pub fn content_fit<M>(mut self, fit: impl IntoSignal<ContentFit, M>) -> Self {
self.content_fit = fit.into_prop();
self
}
pub fn tint<M>(mut self, color: impl IntoSignal<Color, M>) -> Self {
self.tint = color.into_prop();
self
}
pub fn ready(&self) -> Signal<bool> {
let source = self.source;
create_memo(move || crate::image_decode::is_ready(&source.get())).into_signal()
}
pub fn intrinsic_size(&self) -> Option<(f32, f32)> {
self.intrinsic_size
}
fn calculate_size(&self, constraints: &Constraints) -> Size {
let (intrinsic_w, intrinsic_h) = self.intrinsic_size.unwrap_or((100.0, 100.0));
let aspect = intrinsic_w / intrinsic_h;
let offered_w = if constraints.max_width.is_finite() {
constraints.max_width
} else {
intrinsic_w
};
let offered_h = if constraints.max_height.is_finite() {
constraints.max_height
} else {
intrinsic_h
};
let size = match self.cached_content_fit {
ContentFit::None => Size::new(intrinsic_w, intrinsic_h),
ContentFit::Fill | ContentFit::Cover => Size::new(offered_w, offered_h),
ContentFit::Contain => {
let tight_w = constraints.min_width == constraints.max_width;
let tight_h = constraints.min_height == constraints.max_height;
match (tight_w, tight_h) {
(true, true) => Size::new(offered_w, offered_h),
(true, false) => Size::new(offered_w, offered_w / aspect),
(false, true) => Size::new(offered_h * aspect, offered_h),
(false, false) => {
if offered_w / offered_h > aspect {
Size::new(offered_h * aspect, offered_h)
} else {
Size::new(offered_w, offered_w / aspect)
}
}
}
}
};
constraints.constrain(size)
}
}
impl Widget for Image {
fn layout(&mut self, ctx: &mut LayoutCtx, constraints: Constraints) -> Size {
let id = ctx.id();
ctx.tree().set_relayout_boundary(id, false);
let (current_source, fit) = (
self.source.get(),
self.content_fit.get_or(ContentFit::default()),
);
self.cached_content_fit = fit;
let source_changed = self
.cached_source
.as_ref()
.map(|cached| cached != ¤t_source)
.unwrap_or(true);
if source_changed {
self.decode = crate::image_decode::acquire(¤t_source);
}
if source_changed || self.intrinsic_size.is_none() {
self.intrinsic_size = match &self.decode {
Some(decode) => decode.size(),
None => crate::image_metadata::get_intrinsic_size(¤t_source),
};
}
self.cached_source = Some(current_source);
self.calculate_size(&constraints)
}
fn paint(&self, ctx: &mut PaintContext) {
let (tree, id) = (ctx.tree(), ctx.id());
if let Some(ref source) = self.cached_source {
let size = tree.cached_size(id).unwrap_or_default();
let local_bounds = Rect::new(0.0, 0.0, size.width, size.height);
let tint = self.tint.get();
match &self.decode {
Some(decode) => {
let (state, decoded) = decode.state();
if crate::image_decode::paints(state, &decoded) {
ctx.push_image(
source.clone(),
Some(decoded),
local_bounds,
self.cached_content_fit,
tint,
);
}
}
None => ctx.draw_image(source.clone(), local_bounds, self.cached_content_fit, tint),
}
}
}
}
pub fn image<M>(source: impl IntoSignal<ImageSource, M>) -> Image {
Image::new(source)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::jobs;
use crate::reactive::create_signal;
use crate::tree::{Tree, WidgetId};
fn measured(tree: &mut Tree, root: WidgetId, c: Constraints) -> Size {
jobs::pump_and_layout(tree, root, c).expect("the root is registered")
}
fn svg(side: u32) -> ImageSource {
let src = format!(
r#"<svg xmlns="http://www.w3.org/2000/svg" width="{side}" height="{side}"><rect width="{side}" height="{side}" fill="red"/></svg>"#
);
ImageSource::SvgBytes(src.into_bytes().into())
}
#[test]
fn an_image_draws_its_source_into_its_own_box() {
let mut tree = Tree::new();
let root = tree.register(Box::new(Image::new(svg(20)).content_fit(ContentFit::Fill)));
tree.with_widget_mut(root, |w, id, t| w.register_children(t, id));
measured(&mut tree, root, Constraints::new(0.0, 0.0, 40.0, 40.0));
let mut node = crate::renderer::RenderNode::new(root.as_u64());
tree.paint_widget(root, &mut node, 1.0);
let drawn = node
.commands
.iter()
.find_map(|cmd| match &**cmd {
crate::renderer::DrawCommand::Image {
rect, content_fit, ..
} => Some((*rect, *content_fit)),
_ => None,
})
.expect("an image command");
assert_eq!(
(drawn.0.width, drawn.0.height),
(40.0, 40.0),
"the image is drawn into the box layout gave it, got {:?}",
drawn.0
);
assert_eq!(drawn.1, ContentFit::Fill, "and with the fit it declares");
}
#[test]
fn content_fit_answers_to_a_signal() {
let fills = create_signal(true);
let fit = move || {
if fills.get() {
ContentFit::Fill
} else {
ContentFit::None
}
};
let mut tree = Tree::new();
let root = tree.register(Box::new(crate::widgets::container().child(
Image::new(ImageSource::Path("does-not-exist.png".into())).content_fit(fit),
)));
tree.with_widget_mut(root, |w, id, t| w.register_children(t, id));
let offered = Constraints::new(0.0, 0.0, 200.0, 120.0);
let filled = measured(&mut tree, root, offered);
fills.set(false);
let intrinsic = measured(&mut tree, root, offered);
assert_ne!(
filled, intrinsic,
"the fit was read once: both passes measured {filled:?}"
);
assert_eq!(
filled,
Size::new(200.0, 120.0),
"Fill takes what is offered"
);
}
}