diff --git a/.typos.toml b/.typos.toml index 6d8d9cdea..6cffe08b8 100644 --- a/.typos.toml +++ b/.typos.toml @@ -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 diff --git a/masonry/doc/ROADMAP.md b/masonry/doc/ROADMAP.md index 880593193..048bfd40b 100644 --- a/masonry/doc/ROADMAP.md +++ b/masonry/doc/ROADMAP.md @@ -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 diff --git a/masonry/examples/custom_widget.rs b/masonry/examples/custom_widget.rs index 14a611997..db54e73d4 100644 --- a/masonry/examples/custom_widget.rs +++ b/masonry/examples/custom_widget.rs @@ -10,7 +10,7 @@ use accesskit::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, LifeCycle, LifeCycleCtx, PaintCtx, Point, PointerEvent, Rect, Size, StatusChange, TextEvent, Widget, @@ -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); } diff --git a/masonry/examples/simple_image.rs b/masonry/examples/simple_image.rs index e31b3ceb7..da474f283 100644 --- a/masonry/examples/simple_image.rs +++ b/masonry/examples/simple_image.rs @@ -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() diff --git a/masonry/src/widget/image.rs b/masonry/src/widget/image.rs index 019984d6e..2cf15c2d1 100644 --- a/masonry/src/widget/image.rs +++ b/masonry/src/widget/image.rs @@ -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, 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(); } @@ -91,17 +91,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); @@ -198,4 +214,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"); + } } diff --git a/masonry/src/widget/mod.rs b/masonry/src/widget/mod.rs index 7a3720ca2..433110f3e 100644 --- a/masonry/src/widget/mod.rs +++ b/masonry/src/widget/mod.rs @@ -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, - }; -} diff --git a/masonry/src/widget/screenshots/masonry__widget__image__tests__layout_contain.png b/masonry/src/widget/screenshots/masonry__widget__image__tests__layout_contain.png new file mode 100644 index 000000000..70e67797a --- /dev/null +++ b/masonry/src/widget/screenshots/masonry__widget__image__tests__layout_contain.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c11cb16bc2b5be9b5070fee3d2e25e88f3056f36f6ddc71853e636ee7a101e39 +size 635 diff --git a/masonry/src/widget/screenshots/masonry__widget__image__tests__layout_cover.png b/masonry/src/widget/screenshots/masonry__widget__image__tests__layout_cover.png new file mode 100644 index 000000000..a846c198a --- /dev/null +++ b/masonry/src/widget/screenshots/masonry__widget__image__tests__layout_cover.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4436cde23164bedef2f699a4105cdd650516449a3921d82a8ad17da9227462e9 +size 493 diff --git a/masonry/src/widget/screenshots/masonry__widget__image__tests__layout_fill.png b/masonry/src/widget/screenshots/masonry__widget__image__tests__layout_fill.png new file mode 100644 index 000000000..b525f154a --- /dev/null +++ b/masonry/src/widget/screenshots/masonry__widget__image__tests__layout_fill.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5270d4741adf1d2572030e5f2dace4efaabdc4ea1a5c8c2fb08708fd513cdd6a +size 699 diff --git a/masonry/src/widget/screenshots/masonry__widget__image__tests__layout_fitheight.png b/masonry/src/widget/screenshots/masonry__widget__image__tests__layout_fitheight.png new file mode 100644 index 000000000..65c576d3c --- /dev/null +++ b/masonry/src/widget/screenshots/masonry__widget__image__tests__layout_fitheight.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2c599ae83a3318550d7e261ae499ff3a876f0b8b1a1424754c47181f466b67a6 +size 613 diff --git a/masonry/src/widget/screenshots/masonry__widget__image__tests__layout_fitwidth.png b/masonry/src/widget/screenshots/masonry__widget__image__tests__layout_fitwidth.png new file mode 100644 index 000000000..a846c198a --- /dev/null +++ b/masonry/src/widget/screenshots/masonry__widget__image__tests__layout_fitwidth.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4436cde23164bedef2f699a4105cdd650516449a3921d82a8ad17da9227462e9 +size 493 diff --git a/masonry/src/widget/screenshots/masonry__widget__image__tests__layout_none.png b/masonry/src/widget/screenshots/masonry__widget__image__tests__layout_none.png new file mode 100644 index 000000000..5f00fa19a --- /dev/null +++ b/masonry/src/widget/screenshots/masonry__widget__image__tests__layout_none.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b2c09fa1fe70be069709471d7fe6c185b9865bbdee0ff6d9ef062ae241253ee9 +size 461 diff --git a/masonry/src/widget/screenshots/masonry__widget__image__tests__layout_scaledown.png b/masonry/src/widget/screenshots/masonry__widget__image__tests__layout_scaledown.png new file mode 100644 index 000000000..099300693 --- /dev/null +++ b/masonry/src/widget/screenshots/masonry__widget__image__tests__layout_scaledown.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1f95712cfcf8aed71cbfb537cfd12097aa90bd720f64e94bf7db4bfdc896ef7b +size 469 diff --git a/xilem/src/view/image.rs b/xilem/src/view/image.rs index 26b41dbeb..e722bf869 100644 --- a/xilem/src/view/image.rs +++ b/xilem/src/view/image.rs @@ -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 View 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());