Implement ObjectFit::ScaleDown for images (#10063)

Kyle Kelley and Mikayla Maki created

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>

Change summary

crates/gpui/src/elements/img.rs         |  44 +++++++++
crates/image_viewer/src/image_viewer.rs | 116 +++++++++++++-------------
2 files changed, 102 insertions(+), 58 deletions(-)

Detailed changes

crates/gpui/src/elements/img.rs 🔗

@@ -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
     }
 }
 

crates/image_viewer/src/image_viewer.rs 🔗

@@ -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(),
+                    ),
             )
     }
 }