use std::any::TypeId;
use accesskit::{Node, Role};
use masonry_core::core::HasProperty;
use tracing::{Span, trace_span};
use vello::Scene;
use vello::kurbo::{Affine, Size};
use vello::peniko::{BlendMode, ImageBrush};
use crate::core::{
AccessCtx, BoxConstraints, ChildrenIds, LayoutCtx, NoAction, PaintCtx, PropertiesMut,
PropertiesRef, RegisterCtx, Update, UpdateCtx, Widget, WidgetId, WidgetMut,
};
use crate::properties::ObjectFit;
pub struct Image {
image_data: ImageBrush,
}
impl Image {
#[inline]
pub fn new(image_data: impl Into<ImageBrush>) -> Self {
Self {
image_data: image_data.into(),
}
}
}
impl Image {
#[inline]
pub fn set_image_data(this: &mut WidgetMut<'_, Self>, image_data: impl Into<ImageBrush>) {
this.widget.image_data = image_data.into();
this.ctx.request_layout();
}
}
impl HasProperty<ObjectFit> for Image {}
impl Widget for Image {
type Action = NoAction;
fn register_children(&mut self, _ctx: &mut RegisterCtx<'_>) {}
fn property_changed(&mut self, ctx: &mut UpdateCtx<'_>, property_type: TypeId) {
ObjectFit::prop_changed(ctx, property_type);
}
fn update(
&mut self,
_ctx: &mut UpdateCtx<'_>,
_props: &mut PropertiesMut<'_>,
_event: &Update,
) {
}
fn layout(
&mut self,
_ctx: &mut LayoutCtx<'_>,
props: &mut PropertiesMut<'_>,
bc: &BoxConstraints,
) -> Size {
let image_size = Size::new(
self.image_data.image.width as f64,
self.image_data.image.height as f64,
);
if image_size.is_zero_area() {
let size = bc.min();
return size;
}
let image_aspect_ratio = image_size.height / image_size.width;
let object_fit = props.get::<ObjectFit>();
match object_fit {
ObjectFit::Contain => bc.constrain_aspect_ratio(image_aspect_ratio, image_size.width),
ObjectFit::Cover => Size::new(bc.max().width, bc.max().width * image_aspect_ratio),
ObjectFit::Fill => bc.max(),
ObjectFit::FitHeight => {
Size::new(bc.max().height / image_aspect_ratio, bc.max().height)
}
ObjectFit::FitWidth => Size::new(bc.max().width, bc.max().width * image_aspect_ratio),
ObjectFit::None => image_size,
ObjectFit::ScaleDown => {
let mut size = image_size;
if !bc.contains(size) {
size = bc.constrain_aspect_ratio(image_aspect_ratio, size.width);
}
size
}
}
}
fn paint(&mut self, ctx: &mut PaintCtx<'_>, props: &PropertiesRef<'_>, scene: &mut Scene) {
let object_fit = props.get::<ObjectFit>();
let image_size = Size::new(
self.image_data.image.width as f64,
self.image_data.image.height as f64,
);
let transform = object_fit.affine_to_fill(ctx.size(), image_size);
let clip_rect = ctx.size().to_rect();
scene.push_layer(BlendMode::default(), 1., Affine::IDENTITY, &clip_rect);
scene.draw_image(&self.image_data, transform);
scene.pop_layer();
}
fn accessibility_role(&self) -> Role {
Role::Image
}
fn accessibility(
&mut self,
_ctx: &mut AccessCtx<'_>,
_props: &PropertiesRef<'_>,
_node: &mut Node,
) {
}
fn children_ids(&self) -> ChildrenIds {
ChildrenIds::new()
}
fn make_trace_span(&self, id: WidgetId) -> Span {
trace_span!("Image", id = id.trace())
}
}
#[cfg(not(target_arch = "wasm32"))]
#[cfg(test)]
mod tests {
use masonry_core::core::NewWidget;
use vello::peniko::{ImageAlphaType, ImageData, ImageFormat};
use super::*;
use crate::testing::{TestHarness, assert_render_snapshot};
use crate::theme::default_property_set;
#[test]
fn empty_paint() {
let image_data = ImageData {
data: Vec::new().into(),
format: ImageFormat::Rgba8,
alpha_type: ImageAlphaType::Alpha,
width: 0,
height: 0,
};
let image_widget = NewWidget::new(Image::new(image_data));
let mut harness = TestHarness::create(default_property_set(), image_widget);
let _ = harness.render();
}
#[test]
fn tall_paint() {
let image_data = ImageData {
data: [
[255, 255, 255, 255],
[000, 000, 000, 255],
[000, 000, 000, 255],
[255, 255, 255, 255],
]
.into_iter()
.flatten()
.collect::<Vec<_>>()
.into(),
format: ImageFormat::Rgba8,
alpha_type: ImageAlphaType::Alpha,
width: 2,
height: 2,
};
let image_widget = NewWidget::new(Image::new(image_data));
let mut harness = TestHarness::create_with_size(
default_property_set(),
image_widget,
Size::new(40., 60.),
);
assert_render_snapshot!(harness, "image_tall_paint");
}
#[test]
fn edit_image() {
let image_data = ImageData {
data: vec![255; 4 * 8 * 8].into(),
format: ImageFormat::Rgba8,
alpha_type: ImageAlphaType::Alpha,
width: 8,
height: 8,
};
let render_1 = {
let image_widget = NewWidget::new(Image::new(image_data.clone()));
let mut harness = TestHarness::create_with_size(
default_property_set(),
image_widget,
Size::new(40.0, 60.0),
);
harness.render()
};
let render_2 = {
let other_image_data = ImageData {
data: vec![10; 4 * 8 * 8].into(),
format: ImageFormat::Rgba8,
alpha_type: ImageAlphaType::Alpha,
width: 8,
height: 8,
};
let image_widget = NewWidget::new(Image::new(other_image_data));
let mut harness = TestHarness::create_with_size(
default_property_set(),
image_widget,
Size::new(40.0, 60.0),
);
harness.edit_root_widget(|mut image| {
Image::set_image_data(&mut image, image_data);
});
harness.render()
};
assert!(render_1 == render_2);
}
#[test]
fn layout() {
let image_data = ImageData {
data: vec![255; 4 * 8 * 8].into(),
format: ImageFormat::Rgba8,
alpha_type: ImageAlphaType::Alpha,
width: 8,
height: 8,
};
let harness_size = Size::new(100.0, 50.0);
let image_widget = NewWidget::new(Image::new(image_data.clone()));
let mut harness =
TestHarness::create_with_size(default_property_set(), image_widget, harness_size);
harness.edit_root_widget(|mut image| {
image.insert_prop(ObjectFit::Contain);
});
assert_render_snapshot!(harness, "image_layout_contain");
harness.edit_root_widget(|mut image| {
image.insert_prop(ObjectFit::Cover);
});
assert_render_snapshot!(harness, "image_layout_cover");
harness.edit_root_widget(|mut image| {
image.insert_prop(ObjectFit::Fill);
});
assert_render_snapshot!(harness, "image_layout_fill");
harness.edit_root_widget(|mut image| {
image.insert_prop(ObjectFit::FitHeight);
});
assert_render_snapshot!(harness, "image_layout_fitheight");
harness.edit_root_widget(|mut image| {
image.insert_prop(ObjectFit::FitWidth);
});
assert_render_snapshot!(harness, "image_layout_fitwidth");
harness.edit_root_widget(|mut image| {
image.insert_prop(ObjectFit::None);
});
assert_render_snapshot!(harness, "image_layout_none");
harness.edit_root_widget(|mut image| {
image.insert_prop(ObjectFit::ScaleDown);
});
assert_render_snapshot!(harness, "image_layout_scaledown");
}
}