diff --git a/crates/gpui/src/elements/img.rs b/crates/gpui/src/elements/img.rs index 7e92ecba3d0ef4..0b571a6f431e6f 100644 --- a/crates/gpui/src/elements/img.rs +++ b/crates/gpui/src/elements/img.rs @@ -488,12 +488,10 @@ impl Element for Img { .style .object_fit .get_bounds(bounds, data.size(layout_state.frame_index)); - let corner_radii = style - .corner_radii - .to_pixels(window.rem_size()) - .clamp_radii_for_quad_size(new_bounds.size); + let corner_radii = style.corner_radii.to_pixels(window.rem_size()); window .paint_image( + bounds, new_bounds, corner_radii, data, @@ -802,6 +800,11 @@ mod tests { ))) } + fn test_image_with_size(width: u32, height: u32) -> Arc { + let frame = Frame::new(ImageBuffer::from_pixel(width, height, Rgba([0, 0, 0, 0]))); + Arc::new(RenderImage::new(SmallVec::from_elem(frame, 1))) + } + /// Overwrites the cached `frame_index` of the sibling `img` during paint. fn seed_frame_index(frame_index: usize) -> impl IntoElement { canvas( @@ -826,6 +829,86 @@ mod tests { }); } + #[gpui::test] + fn image_object_fit_cover_crops_to_element_bounds(cx: &mut TestAppContext) { + let window = cx.add_empty_window(); + let image = test_image_with_size(200, 100); + window.draw(point(px(0.), px(0.)), size(px(100.), px(100.)), |_, _| { + img(ImageSource::Render(image.clone())) + .size_full() + .object_fit(ObjectFit::Fill) + .into_any_element() + }); + let full_tile_bounds = window.update(|window, _| { + window + .rendered_frame + .scene + .polychrome_sprites + .last() + .expect("fill image should paint a sprite") + .tile + .bounds + }); + + window.draw(point(px(10.), px(20.)), size(px(100.), px(100.)), |_, _| { + img(ImageSource::Render(image)) + .size_full() + .object_fit(ObjectFit::Cover) + .into_any_element() + }); + + let (rendered_bounds, rendered_tile_bounds, scale_factor) = window.update(|window, _| { + let sprite = window + .rendered_frame + .scene + .polychrome_sprites + .last() + .expect("cover image should paint a sprite"); + (sprite.bounds, sprite.tile.bounds, window.scale_factor()) + }); + assert_eq!( + rendered_bounds, + Bounds { + origin: point(px(10.).scale(scale_factor), px(20.).scale(scale_factor)), + size: size(px(100.).scale(scale_factor), px(100.).scale(scale_factor)), + } + ); + assert_eq!( + ( + rendered_tile_bounds.origin.x.0 - full_tile_bounds.origin.x.0, + rendered_tile_bounds.origin.y.0 - full_tile_bounds.origin.y.0, + rendered_tile_bounds.size.width.0, + rendered_tile_bounds.size.height.0, + ), + (50, 0, 100, 100), + ); + } + + #[gpui::test] + fn image_object_fit_cover_clamps_corner_radii_to_visible_bounds(cx: &mut TestAppContext) { + let window = cx.add_empty_window(); + window.draw(point(px(0.), px(0.)), size(px(100.), px(100.)), |_, _| { + img(ImageSource::Render(test_image_with_size(200, 100))) + .size_full() + .rounded(px(100.)) + .object_fit(ObjectFit::Cover) + .into_any_element() + }); + + let (corner_radius, expected_corner_radius) = window.update(|window, _| { + ( + window + .rendered_frame + .scene + .polychrome_sprites + .last() + .map(|sprite| sprite.corner_radii.top_left), + px(50.).scale(window.scale_factor()), + ) + }); + assert_eq!(corner_radius, Some(expected_corner_radius)); + } + #[gpui::test] fn stale_frame_index_is_clamped_when_image_changes(cx: &mut TestAppContext) { let window = cx.add_empty_window(); diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index 312d797876bcfa..6e85b9989dcdb2 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -2,8 +2,8 @@ use crate::Inspector; use crate::{ Action, AnyDrag, AnyElement, AnyImageCache, AnyTooltip, AnyView, App, AppContext, Arena, Asset, - AsyncWindowContext, AvailableSpace, Background, BorderStyle, Bounds, BoxShadow, Capslock, - Context, Corners, CursorHideMode, CursorStyle, Decorations, DevicePixels, + AsyncWindowContext, AtlasTile, AvailableSpace, Background, BorderStyle, Bounds, BoxShadow, + Capslock, Context, Corners, CursorHideMode, CursorStyle, Decorations, DevicePixels, DispatchActionListener, DispatchNodeId, DispatchTree, DisplayId, Edges, Effect, Entity, EntityId, EventEmitter, FileDropEvent, FontId, Global, GlobalElementId, GlyphId, GpuSpecs, Hsla, InputHandler, IsZero, KeyBinding, KeyContext, KeyDownEvent, KeyEvent, Keystroke, @@ -4356,9 +4356,14 @@ impl Window { /// This method will panic if the frame_index is not valid /// /// This method should only be called as part of the paint phase of element drawing. + /// Paint an image into `bounds`, positioning and scaling it according to `image_bounds`. + /// + /// The visible region rendered is `bounds.intersect(&image_bounds)`, with `corner_radii` + /// applied to `bounds`. pub fn paint_image( &mut self, bounds: Bounds, + image_bounds: Bounds, corner_radii: Corners, data: Arc, frame_index: usize, @@ -4366,7 +4371,14 @@ impl Window { ) -> Result<()> { self.invalidator.debug_assert_paint(); - let bounds = self.snap_bounds(bounds); + let visible_bounds = bounds.intersect(&image_bounds); + if visible_bounds.size.width <= Pixels::ZERO || visible_bounds.size.height <= Pixels::ZERO { + return Ok(()); + } + if image_bounds.size.width <= Pixels::ZERO || image_bounds.size.height <= Pixels::ZERO { + return Ok(()); + } + let params = RenderImageParams { image_id: data.id, frame_index, @@ -4384,18 +4396,63 @@ impl Window { ))) })? .expect("Callback above only returns Some"); + + let visible_bounds_snapped = self.snap_bounds(visible_bounds); + + let sub_tile = if visible_bounds == image_bounds { + tile + } else { + let x_offset_ratio = + (visible_bounds.origin.x - image_bounds.origin.x) / image_bounds.size.width; + let y_offset_ratio = + (visible_bounds.origin.y - image_bounds.origin.y) / image_bounds.size.height; + let width_ratio = visible_bounds.size.width / image_bounds.size.width; + let height_ratio = visible_bounds.size.height / image_bounds.size.height; + + let tile_origin_x = tile.bounds.origin.x.0; + let tile_origin_y = tile.bounds.origin.y.0; + let tile_width = tile.bounds.size.width.0; + let tile_height = tile.bounds.size.height.0; + + let sub_origin_x = tile_origin_x + (x_offset_ratio * tile_width as f32).round() as i32; + let sub_origin_y = tile_origin_y + (y_offset_ratio * tile_height as f32).round() as i32; + let sub_width = (width_ratio * tile_width as f32).round() as i32; + let sub_height = (height_ratio * tile_height as f32).round() as i32; + + let max_x = tile_origin_x + tile_width; + let max_y = tile_origin_y + tile_height; + + let clamped_origin_x = sub_origin_x.clamp(tile_origin_x, max_x); + let clamped_origin_y = sub_origin_y.clamp(tile_origin_y, max_y); + let clamped_width = sub_width.min(max_x - clamped_origin_x).max(0); + let clamped_height = sub_height.min(max_y - clamped_origin_y).max(0); + + AtlasTile { + bounds: Bounds { + origin: point( + DevicePixels(clamped_origin_x), + DevicePixels(clamped_origin_y), + ), + size: size(DevicePixels(clamped_width), DevicePixels(clamped_height)), + }, + ..tile + } + }; + let content_mask = self.snapped_content_mask(); - let corner_radii = corner_radii.scale(self.scale_factor()); + let corner_radii = corner_radii + .clamp_radii_for_quad_size(visible_bounds.size) + .scale(self.scale_factor()); let opacity = self.element_opacity(); self.next_frame.scene.insert_primitive(PolychromeSprite { order: 0, pad: 0, grayscale: grayscale.into(), - bounds, + bounds: visible_bounds_snapped, content_mask, corner_radii, - tile, + tile: sub_tile, opacity, }); Ok(())