Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 5 commits into from
Oct 2, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .typos.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions masonry/doc/ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@
-> [X] Label
-> [X] SizedBox
-> [X] Spinner
-> [ ] FillStrat
-> [ ] ObjectFit
-> [ ] text
-> [ ] TextBox

Expand Down Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions masonry/examples/custom_widget.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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);
}

Expand Down
4 changes: 2 additions & 2 deletions masonry/examples/simple_image.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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).fill_mode(ObjectFit::Contain);

let window_size = LogicalSize::new(650.0, 450.0);
let window_attributes = Window::default_attributes()
Expand Down
77 changes: 67 additions & 10 deletions masonry/src/widget/image.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -28,25 +28,25 @@ use crate::{
/// than the image size).
pub struct Image {
image_data: ImageBuf,
fill: FillStrat,
fill: ObjectFit,
failingprovince marked this conversation as resolved.
Show resolved Hide resolved
}

// --- 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(),
fill: ObjectFit::default(),
}
}

/// Builder-style method for specifying the fill strategy.
#[inline]
pub fn fill_mode(mut self, mode: FillStrat) -> Self {
pub fn fill_mode(mut self, mode: ObjectFit) -> Self {
failingprovince marked this conversation as resolved.
Show resolved Hide resolved
self.fill = mode;
self
}
Expand All @@ -56,7 +56,7 @@ impl Image {
impl<'a> WidgetMut<'a, Image> {
/// Modify the widget's fill strategy.
#[inline]
pub fn set_fill_mode(&mut self, newfil: FillStrat) {
pub fn set_fill_mode(&mut self, newfil: ObjectFit) {
failingprovince marked this conversation as resolved.
Show resolved Hide resolved
self.widget.fill = newfil;
self.ctx.request_paint();
}
Expand Down Expand Up @@ -91,10 +91,26 @@ 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.fill {
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
}
Expand Down Expand Up @@ -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()).fill_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()).fill_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()).fill_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()).fill_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()).fill_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()).fill_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()).fill_mode(ObjectFit::ScaleDown);
let mut harness = TestHarness::create_with_size(image_widget, harness_size);
assert_render_snapshot!(harness, "layout_scaledown");
}
}
30 changes: 10 additions & 20 deletions masonry/src/widget/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ use crate::{Affine, Size};
// These are based on https://api.flutter.dev/flutter/painting/BoxFit-class.html
/// Strategies for inscribing a rectangle inside another rectangle.
failingprovince marked this conversation as resolved.
Show resolved Hide resolved
#[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,
Expand All @@ -81,8 +81,8 @@ 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
/// used to position and scale the image in the widget.
Expand All @@ -91,22 +91,22 @@ impl FillStrat {
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;
Expand All @@ -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,
};
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 5 additions & 5 deletions xilem/src/view/image.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@

//! 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`]).
/// By default, the Image will scale to fit its box constraints ([`ObjectFit::Fill`]).
/// To configure this, call [`fill`](Image::fill) on the returned value.
///
/// Corresponds to the [`Image`](widget::Image) widget.
Expand All @@ -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(),
fill: ObjectFit::default(),
failingprovince marked this conversation as resolved.
Show resolved Hide resolved
}
}

Expand All @@ -33,12 +33,12 @@ pub fn image(image: &vello::peniko::Image) -> Image {
/// See `image`'s docs for more details.
pub struct Image {
image: vello::peniko::Image,
fill: FillStrat,
fill: ObjectFit,
}

impl Image {
/// Specify the fill strategy.
pub fn fill(mut self, fill: FillStrat) -> Self {
pub fn fill(mut self, fill: ObjectFit) -> Self {
self.fill = fill;
self
}
Expand Down