Implement ObjectFit::ScaleDown for images (#10063)

While working towards fixes for the image viewer, @mikayla-maki and I
discovered that we didn't have `object-fit: scale-down` implemented.
This doesn't _fully_ solve the image issues as there is some issue where
only the bounds width is updating on layout change that I haven't fully
chased down.

Co-Authored-By: @mikayla-maki 

Release Notes:

- N/A

---------

Co-authored-by: Mikayla Maki <mikayla@zed.dev>
This commit is contained in:
Kyle Kelley 2024-04-06 15:20:30 -07:00 committed by GitHub
parent c64c2758c0
commit c7961b9054
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 102 additions and 58 deletions

View file

@ -99,6 +99,8 @@ pub enum ObjectFit {
Contain, Contain,
/// The image will be scaled to cover the bounds of the element. /// The image will be scaled to cover the bounds of the element.
Cover, Cover,
/// The image will be scaled down to fit within the bounds of the element.
ScaleDown,
/// The image will maintain its original size. /// The image will maintain its original size.
None, None,
} }
@ -114,7 +116,7 @@ impl ObjectFit {
let image_ratio = image_size.width / image_size.height; let image_ratio = image_size.width / image_size.height;
let bounds_ratio = bounds.size.width / bounds.size.height; let bounds_ratio = bounds.size.width / bounds.size.height;
match self { let result_bounds = match self {
ObjectFit::Fill => bounds, ObjectFit::Fill => bounds,
ObjectFit::Contain => { ObjectFit::Contain => {
let new_size = if bounds_ratio > image_ratio { let new_size = if bounds_ratio > image_ratio {
@ -137,6 +139,42 @@ impl ObjectFit {
size: new_size, size: new_size,
} }
} }
ObjectFit::ScaleDown => {
// Check if the image is larger than the bounds in either dimension.
if image_size.width > bounds.size.width || image_size.height > bounds.size.height {
// If the image is larger, use the same logic as Contain to scale it down.
let new_size = if bounds_ratio > image_ratio {
size(
image_size.width * (bounds.size.height / image_size.height),
bounds.size.height,
)
} else {
size(
bounds.size.width,
image_size.height * (bounds.size.width / image_size.width),
)
};
Bounds {
origin: point(
bounds.origin.x + (bounds.size.width - new_size.width) / 2.0,
bounds.origin.y + (bounds.size.height - new_size.height) / 2.0,
),
size: new_size,
}
} else {
// If the image is smaller than or equal to the container, display it at its original size,
// centered within the container.
let original_size = size(image_size.width, image_size.height);
Bounds {
origin: point(
bounds.origin.x + (bounds.size.width - original_size.width) / 2.0,
bounds.origin.y + (bounds.size.height - original_size.height) / 2.0,
),
size: original_size,
}
}
}
ObjectFit::Cover => { ObjectFit::Cover => {
let new_size = if bounds_ratio > image_ratio { let new_size = if bounds_ratio > image_ratio {
size( size(
@ -162,7 +200,9 @@ impl ObjectFit {
origin: bounds.origin, origin: bounds.origin,
size: image_size, size: image_size,
}, },
} };
result_bounds
} }
} }

View file

@ -1,10 +1,11 @@
use gpui::{ use gpui::{
canvas, div, fill, img, opaque_grey, point, size, AnyElement, AppContext, Bounds, Context, canvas, div, fill, img, opaque_grey, point, size, AnyElement, AppContext, Bounds, Context,
EventEmitter, FocusHandle, FocusableView, Img, InteractiveElement, IntoElement, Model, EventEmitter, FocusHandle, FocusableView, Img, InteractiveElement, IntoElement, Model,
ParentElement, Render, Styled, Task, View, ViewContext, VisualContext, WeakView, WindowContext, ObjectFit, ParentElement, Render, Styled, Task, View, ViewContext, VisualContext, WeakView,
WindowContext,
}; };
use persistence::IMAGE_VIEWER; use persistence::IMAGE_VIEWER;
use ui::{h_flex, prelude::*}; use ui::prelude::*;
use project::{Project, ProjectEntryId, ProjectPath}; use project::{Project, ProjectEntryId, ProjectPath};
use std::{ffi::OsStr, path::PathBuf}; use std::{ffi::OsStr, path::PathBuf};
@ -155,64 +156,67 @@ impl FocusableView for ImageView {
impl Render for ImageView { impl Render for ImageView {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement { fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let checkered_background = |bounds: Bounds<Pixels>, _, cx: &mut ElementContext| {
let square_size = 32.0;
let start_y = bounds.origin.y.0;
let height = bounds.size.height.0;
let start_x = bounds.origin.x.0;
let width = bounds.size.width.0;
let mut y = start_y;
let mut x = start_x;
let mut color_swapper = true;
// draw checkerboard pattern
while y <= start_y + height {
// Keeping track of the grid in order to be resilient to resizing
let start_swap = color_swapper;
while x <= start_x + width {
let rect =
Bounds::new(point(px(x), px(y)), size(px(square_size), px(square_size)));
let color = if color_swapper {
opaque_grey(0.6, 0.4)
} else {
opaque_grey(0.7, 0.4)
};
cx.paint_quad(fill(rect, color));
color_swapper = !color_swapper;
x += square_size;
}
x = start_x;
color_swapper = !start_swap;
y += square_size;
}
};
let checkered_background = canvas(|_, _| (), checkered_background)
.border_2()
.border_color(cx.theme().styles.colors.border)
.size_full()
.absolute()
.top_0()
.left_0();
div() div()
.track_focus(&self.focus_handle) .track_focus(&self.focus_handle)
.size_full() .size_full()
.child(checkered_background)
.child( .child(
// Checkered background behind the image div()
canvas( .flex()
|_, _| (), .justify_center()
|bounds, _, cx| { .items_center()
let square_size = 32.0; .w_full()
// TODO: In browser based Tailwind & Flex this would be h-screen and we'd use w-full
let start_y = bounds.origin.y.0; .h_full()
let height = bounds.size.height.0; .child(
let start_x = bounds.origin.x.0; img(self.path.clone())
let width = bounds.size.width.0; .object_fit(ObjectFit::ScaleDown)
.max_w_full()
let mut y = start_y; .max_h_full(),
let mut x = start_x; ),
let mut color_swapper = true;
// draw checkerboard pattern
while y <= start_y + height {
// Keeping track of the grid in order to be resilient to resizing
let start_swap = color_swapper;
while x <= start_x + width {
let rect = Bounds::new(
point(px(x), px(y)),
size(px(square_size), px(square_size)),
);
let color = if color_swapper {
opaque_grey(0.6, 0.4)
} else {
opaque_grey(0.7, 0.4)
};
cx.paint_quad(fill(rect, color));
color_swapper = !color_swapper;
x += square_size;
}
x = start_x;
color_swapper = !start_swap;
y += square_size;
}
},
)
.border_2()
.border_color(cx.theme().styles.colors.border)
.size_full()
.absolute()
.top_0()
.left_0(),
)
.child(
v_flex().h_full().justify_around().child(
h_flex()
.w_full()
.justify_around()
.child(img(self.path.clone())),
),
) )
} }
} }