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,
/// The image will be scaled to cover the bounds of the element.
Cover,
/// The image will be scaled down to fit within the bounds of the element.
ScaleDown,
/// The image will maintain its original size.
None,
}
@ -114,7 +116,7 @@ impl ObjectFit {
let image_ratio = image_size.width / image_size.height;
let bounds_ratio = bounds.size.width / bounds.size.height;
match self {
let result_bounds = match self {
ObjectFit::Fill => bounds,
ObjectFit::Contain => {
let new_size = if bounds_ratio > image_ratio {
@ -137,6 +139,42 @@ impl ObjectFit {
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 => {
let new_size = if bounds_ratio > image_ratio {
size(
@ -162,7 +200,9 @@ impl ObjectFit {
origin: bounds.origin,
size: image_size,
},
}
};
result_bounds
}
}

View file

@ -1,10 +1,11 @@
use gpui::{
canvas, div, fill, img, opaque_grey, point, size, AnyElement, AppContext, Bounds, Context,
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 ui::{h_flex, prelude::*};
use ui::prelude::*;
use project::{Project, ProjectEntryId, ProjectPath};
use std::{ffi::OsStr, path::PathBuf};
@ -155,64 +156,67 @@ impl FocusableView for ImageView {
impl Render for ImageView {
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()
.track_focus(&self.focus_handle)
.size_full()
.child(checkered_background)
.child(
// Checkered background behind the image
canvas(
|_, _| (),
|bounds, _, cx| {
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;
}
},
)
.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())),
),
div()
.flex()
.justify_center()
.items_center()
.w_full()
// TODO: In browser based Tailwind & Flex this would be h-screen and we'd use w-full
.h_full()
.child(
img(self.path.clone())
.object_fit(ObjectFit::ScaleDown)
.max_w_full()
.max_h_full(),
),
)
}
}