feat(#574): reimplementation of `Image` widget `layout` function (#605)

Reimplement calculation of image size in `Image` widget `layout`
function.
Makes it match against `self.fill` before setting size.

Tests still missing.

Fixes #574
This commit is contained in:
failingprovince 2024-10-02 12:08:00 +02:00 committed by GitHub
parent b245a61429
commit fe140afc9a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 121 additions and 54 deletions

View File

@ -17,7 +17,6 @@ extend-ignore-re = [
# is treated as always incorrect.
[default.extend-identifiers]
FillStrat = "FillStrat" # short for strategy
wdth = "wdth" # Variable font parameter
# Case insensitive

View File

@ -209,7 +209,7 @@
-> [X] Label
-> [X] SizedBox
-> [X] Spinner
-> [ ] FillStrat
-> [ ] ObjectFit
-> [ ] text
-> [ ] TextBox
@ -255,7 +255,7 @@ Make library of commonly desired layouts
- Side gutters
- Document format
How do make easily-readable test of Flex, FillStrat layout?
How do make easily-readable test of Flex, ObjectFit layout?
## Passes

View File

@ -10,7 +10,7 @@
use accesskit::{NodeBuilder, Role};
use masonry::app_driver::{AppDriver, DriverCtx};
use masonry::kurbo::{BezPath, Stroke};
use masonry::widget::{FillStrat, RootWidget};
use masonry::widget::{ObjectFit, RootWidget};
use masonry::{
AccessCtx, AccessEvent, Action, Affine, BoxConstraints, Color, EventCtx, LayoutCtx,
LifeCycleCtx, PaintCtx, Point, PointerEvent, Rect, RegisterCtx, Size, StatusChange, TextEvent,
@ -124,7 +124,7 @@ impl Widget for CustomWidget {
// Let's burn some CPU to make a (partially transparent) image buffer
let image_data = make_image_data(256, 256);
let image_data = Image::new(image_data.into(), Format::Rgba8, 256, 256);
let transform = FillStrat::Fill.affine_to_fill(ctx.size(), size);
let transform = ObjectFit::Fill.affine_to_fill(ctx.size(), size);
scene.draw_image(&image_data, transform);
}

View File

@ -10,7 +10,7 @@
use masonry::app_driver::{AppDriver, DriverCtx};
use masonry::dpi::LogicalSize;
use masonry::widget::{FillStrat, Image, RootWidget};
use masonry::widget::{Image, ObjectFit, RootWidget};
use masonry::{Action, WidgetId};
use vello::peniko::{Format, Image as ImageBuf};
use winit::window::Window;
@ -26,7 +26,7 @@ pub fn main() {
let image_data = image::load_from_memory(image_bytes).unwrap().to_rgba8();
let (width, height) = image_data.dimensions();
let png_data = ImageBuf::new(image_data.to_vec().into(), Format::Rgba8, width, height);
let image = Image::new(png_data).fill_mode(FillStrat::Contain);
let image = Image::new(png_data).fit_mode(ObjectFit::Contain);
let window_size = LogicalSize::new(650.0, 450.0);
let window_attributes = Window::default_attributes()

View File

@ -11,7 +11,7 @@ use vello::kurbo::Affine;
use vello::peniko::{BlendMode, Image as ImageBuf};
use vello::Scene;
use crate::widget::{FillStrat, WidgetMut};
use crate::widget::{ObjectFit, WidgetMut};
use crate::{
AccessCtx, AccessEvent, BoxConstraints, EventCtx, LayoutCtx, LifeCycle, LifeCycleCtx, PaintCtx,
PointerEvent, RegisterCtx, Size, StatusChange, TextEvent, Widget, WidgetId,
@ -28,36 +28,36 @@ use crate::{
/// than the image size).
pub struct Image {
image_data: ImageBuf,
fill: FillStrat,
object_fit: ObjectFit,
}
// --- MARK: BUILDERS ---
impl Image {
/// Create an image drawing widget from an image buffer.
///
/// By default, the Image will scale to fit its box constraints ([`FillStrat::Fill`]).
/// By default, the Image will scale to fit its box constraints ([`ObjectFit::Fill`]).
#[inline]
pub fn new(image_data: ImageBuf) -> Self {
Image {
image_data,
fill: FillStrat::default(),
object_fit: ObjectFit::default(),
}
}
/// Builder-style method for specifying the fill strategy.
/// Builder-style method for specifying the object fit.
#[inline]
pub fn fill_mode(mut self, mode: FillStrat) -> Self {
self.fill = mode;
pub fn fit_mode(mut self, mode: ObjectFit) -> Self {
self.object_fit = mode;
self
}
}
// --- MARK: WIDGETMUT ---
impl<'a> WidgetMut<'a, Image> {
/// Modify the widget's fill strategy.
/// Modify the widget's object fit.
#[inline]
pub fn set_fill_mode(&mut self, newfil: FillStrat) {
self.widget.fill = newfil;
pub fn set_fit_mode(&mut self, new_object_fit: ObjectFit) {
self.widget.object_fit = new_object_fit;
self.ctx.request_paint();
}
@ -93,17 +93,33 @@ impl Widget for Image {
trace!("Computed size: {}", size);
return size;
}
// This size logic has NOT been carefully considered, in particular with regards to self.fill.
// TODO: Carefully consider it
let size =
bc.constrain_aspect_ratio(image_size.height / image_size.width, image_size.width);
let image_aspect_ratio = image_size.height / image_size.width;
let size = match self.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
}
};
trace!("Computed size: {}", size);
size
}
fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut Scene) {
let image_size = Size::new(self.image_data.width as f64, self.image_data.height as f64);
let transform = self.fill.affine_to_fill(ctx.size(), image_size);
let transform = self.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);
@ -200,4 +216,45 @@ mod tests {
// We don't use assert_eq because we don't want rich assert
assert!(render_1 == render_2);
}
#[test]
fn layout() {
let image_data = ImageBuf::new(vec![255; 4 * 8 * 8].into(), Format::Rgba8, 8, 8);
let harness_size = Size::new(100.0, 50.0);
// Contain.
let image_widget = Image::new(image_data.clone()).fit_mode(ObjectFit::Contain);
let mut harness = TestHarness::create_with_size(image_widget, harness_size);
assert_render_snapshot!(harness, "layout_contain");
// Cover.
let image_widget = Image::new(image_data.clone()).fit_mode(ObjectFit::Cover);
let mut harness = TestHarness::create_with_size(image_widget, harness_size);
assert_render_snapshot!(harness, "layout_cover");
// Fill.
let image_widget = Image::new(image_data.clone()).fit_mode(ObjectFit::Fill);
let mut harness = TestHarness::create_with_size(image_widget, harness_size);
assert_render_snapshot!(harness, "layout_fill");
// FitHeight.
let image_widget = Image::new(image_data.clone()).fit_mode(ObjectFit::FitHeight);
let mut harness = TestHarness::create_with_size(image_widget, harness_size);
assert_render_snapshot!(harness, "layout_fitheight");
// FitWidth.
let image_widget = Image::new(image_data.clone()).fit_mode(ObjectFit::FitWidth);
let mut harness = TestHarness::create_with_size(image_widget, harness_size);
assert_render_snapshot!(harness, "layout_fitwidth");
// None.
let image_widget = Image::new(image_data.clone()).fit_mode(ObjectFit::None);
let mut harness = TestHarness::create_with_size(image_widget, harness_size);
assert_render_snapshot!(harness, "layout_none");
// ScaleDown.
let image_widget = Image::new(image_data.clone()).fit_mode(ObjectFit::ScaleDown);
let mut harness = TestHarness::create_with_size(image_widget, harness_size);
assert_render_snapshot!(harness, "layout_scaledown");
}
}

View File

@ -58,10 +58,10 @@ pub(crate) use widget_arena::WidgetArena;
use crate::{Affine, Size};
// These are based on https://api.flutter.dev/flutter/painting/BoxFit-class.html
// These are based on https://developer.mozilla.org/en-US/docs/Web/CSS/object-fit
/// Strategies for inscribing a rectangle inside another rectangle.
#[derive(Clone, Copy, Default, PartialEq)]
pub enum FillStrat {
pub enum ObjectFit {
/// As large as possible without changing aspect ratio of image and all of image shown
#[default]
Contain,
@ -81,32 +81,32 @@ pub enum FillStrat {
// TODO - Need to write tests for this, in a way that's relatively easy to visualize.
impl FillStrat {
/// Calculate an origin and scale for an image with a given `FillStrat`.
impl ObjectFit {
/// Calculate an origin and scale for an image with a given `ObjectFit`.
///
/// This takes some properties of a widget and a fill strategy and returns an affine matrix
/// This takes some properties of a widget and an object fit and returns an affine matrix
/// used to position and scale the image in the widget.
pub fn affine_to_fill(self, parent: Size, fit_box: Size) -> Affine {
let raw_scalex = parent.width / fit_box.width;
let raw_scaley = parent.height / fit_box.height;
let (scalex, scaley) = match self {
FillStrat::Contain => {
ObjectFit::Contain => {
let scale = raw_scalex.min(raw_scaley);
(scale, scale)
}
FillStrat::Cover => {
ObjectFit::Cover => {
let scale = raw_scalex.max(raw_scaley);
(scale, scale)
}
FillStrat::Fill => (raw_scalex, raw_scaley),
FillStrat::FitHeight => (raw_scaley, raw_scaley),
FillStrat::FitWidth => (raw_scalex, raw_scalex),
FillStrat::ScaleDown => {
ObjectFit::Fill => (raw_scalex, raw_scaley),
ObjectFit::FitHeight => (raw_scaley, raw_scaley),
ObjectFit::FitWidth => (raw_scalex, raw_scalex),
ObjectFit::ScaleDown => {
let scale = raw_scalex.min(raw_scaley).min(1.0);
(scale, scale)
}
FillStrat::None => (1.0, 1.0),
ObjectFit::None => (1.0, 1.0),
};
let origin_x = (parent.width - (fit_box.width * scalex)) / 2.0;
@ -115,13 +115,3 @@ impl FillStrat {
Affine::new([scalex, 0., 0., scaley, origin_x, origin_y])
}
}
// TODO - remove prelude
#[allow(missing_docs)]
pub mod prelude {
#[doc(hidden)]
pub use crate::{
BoxConstraints, EventCtx, LayoutCtx, LifeCycle, LifeCycleCtx, PaintCtx, PointerEvent, Size,
StatusChange, TextEvent, Widget, WidgetId,
};
}

View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c11cb16bc2b5be9b5070fee3d2e25e88f3056f36f6ddc71853e636ee7a101e39
size 635

View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:4436cde23164bedef2f699a4105cdd650516449a3921d82a8ad17da9227462e9
size 493

View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:5270d4741adf1d2572030e5f2dace4efaabdc4ea1a5c8c2fb08708fd513cdd6a
size 699

View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:2c599ae83a3318550d7e261ae499ff3a876f0b8b1a1424754c47181f466b67a6
size 613

View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:4436cde23164bedef2f699a4105cdd650516449a3921d82a8ad17da9227462e9
size 493

View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:b2c09fa1fe70be069709471d7fe6c185b9865bbdee0ff6d9ef062ae241253ee9
size 461

View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:1f95712cfcf8aed71cbfb537cfd12097aa90bd720f64e94bf7db4bfdc896ef7b
size 469

View File

@ -3,15 +3,15 @@
//! The bitmap image widget.
use masonry::widget::{self, FillStrat};
use masonry::widget::{self, ObjectFit};
use xilem_core::{Mut, ViewMarker};
use crate::{MessageResult, Pod, View, ViewCtx, ViewId};
/// Displays the bitmap `image`.
///
/// By default, the Image will scale to fit its box constraints ([`FillStrat::Fill`]).
/// To configure this, call [`fill`](Image::fill) on the returned value.
/// By default, the Image will scale to fit its box constraints ([`ObjectFit::Fill`]).
/// To configure this, call [`fit`](Image::fit) on the returned value.
///
/// Corresponds to the [`Image`](widget::Image) widget.
///
@ -24,7 +24,7 @@ pub fn image(image: &vello::peniko::Image) -> Image {
// We take by reference as we expect all users of this API will need to clone, and it's
// easier than documenting that cloning is cheap.
image: image.clone(),
fill: FillStrat::default(),
object_fit: ObjectFit::default(),
}
}
@ -33,13 +33,13 @@ pub fn image(image: &vello::peniko::Image) -> Image {
/// See `image`'s docs for more details.
pub struct Image {
image: vello::peniko::Image,
fill: FillStrat,
object_fit: ObjectFit,
}
impl Image {
/// Specify the fill strategy.
pub fn fill(mut self, fill: FillStrat) -> Self {
self.fill = fill;
/// Specify the object fit.
pub fn fit(mut self, fill: ObjectFit) -> Self {
self.object_fit = fill;
self
}
}
@ -60,8 +60,8 @@ impl<State, Action> View<State, Action, ViewCtx> for Image {
_: &mut ViewCtx,
mut element: Mut<'el, Self::Element>,
) -> Mut<'el, Self::Element> {
if prev.fill != self.fill {
element.set_fill_mode(self.fill);
if prev.object_fit != self.object_fit {
element.set_fit_mode(self.object_fit);
}
if prev.image != self.image {
element.set_image_data(self.image.clone());