mirror of https://github.com/linebender/xilem
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:
parent
b245a61429
commit
fe140afc9a
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:c11cb16bc2b5be9b5070fee3d2e25e88f3056f36f6ddc71853e636ee7a101e39
|
||||
size 635
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:4436cde23164bedef2f699a4105cdd650516449a3921d82a8ad17da9227462e9
|
||||
size 493
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:5270d4741adf1d2572030e5f2dace4efaabdc4ea1a5c8c2fb08708fd513cdd6a
|
||||
size 699
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:2c599ae83a3318550d7e261ae499ff3a876f0b8b1a1424754c47181f466b67a6
|
||||
size 613
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:4436cde23164bedef2f699a4105cdd650516449a3921d82a8ad17da9227462e9
|
||||
size 493
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:b2c09fa1fe70be069709471d7fe6c185b9865bbdee0ff6d9ef062ae241253ee9
|
||||
size 461
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:1f95712cfcf8aed71cbfb537cfd12097aa90bd720f64e94bf7db4bfdc896ef7b
|
||||
size 469
|
|
@ -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());
|
||||
|
|
Loading…
Reference in New Issue