Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 87 additions & 4 deletions crates/gpui/src/elements/img.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -802,6 +800,11 @@ mod tests {
)))
}

fn test_image_with_size(width: u32, height: u32) -> Arc<RenderImage> {
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(
Expand All @@ -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();
Expand Down
69 changes: 63 additions & 6 deletions crates/gpui/src/window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -4356,17 +4356,29 @@ 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<Pixels>,
image_bounds: Bounds<Pixels>,
corner_radii: Corners<Pixels>,
data: Arc<RenderImage>,
frame_index: usize,
grayscale: bool,
) -> 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,
Expand All @@ -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(())
Expand Down
Loading