ZIm/crates/gpui/src/elements/img.rs
Michael Sloan ab59982bf7
Add initial element inspector for Zed development (#31315)
Open inspector with `dev: toggle inspector` from command palette or
`cmd-alt-i` on mac or `ctrl-alt-i` on linux.

https://github.com/user-attachments/assets/54c43034-d40b-414e-ba9b-190bed2e6d2f

* Picking of elements via the mouse, with scroll wheel to inspect
occluded elements.

* Temporary manipulation of the selected element.

* Layout info and JSON-based style manipulation for `Div`.

* Navigation to code that constructed the element.

Big thanks to @as-cii and @maxdeviant for sorting out how to implement
the core of an inspector.

Release Notes:

- N/A

---------

Co-authored-by: Antonio Scandurra <me@as-cii.com>
Co-authored-by: Marshall Bowers <git@maxdeviant.com>
Co-authored-by: Federico Dionisi <code@fdionisi.me>
2025-05-23 23:08:59 +00:00

767 lines
26 KiB
Rust

use crate::{
AbsoluteLength, AnyElement, AnyImageCache, App, Asset, AssetLogger, Bounds, DefiniteLength,
Element, ElementId, Entity, GlobalElementId, Hitbox, Image, ImageCache, InspectorElementId,
InteractiveElement, Interactivity, IntoElement, LayoutId, Length, ObjectFit, Pixels,
RenderImage, Resource, SMOOTH_SVG_SCALE_FACTOR, SharedString, SharedUri, StyleRefinement,
Styled, SvgSize, Task, Window, px, swap_rgba_pa_to_bgra,
};
use anyhow::{Context as _, Result};
use futures::{AsyncReadExt, Future};
use image::{
AnimationDecoder, DynamicImage, Frame, ImageBuffer, ImageError, ImageFormat, Rgba,
codecs::{gif::GifDecoder, webp::WebPDecoder},
};
use smallvec::SmallVec;
use std::{
fs,
io::{self, Cursor},
ops::{Deref, DerefMut},
path::{Path, PathBuf},
str::FromStr,
sync::Arc,
time::{Duration, Instant},
};
use thiserror::Error;
use util::ResultExt;
use super::{FocusableElement, Stateful, StatefulInteractiveElement};
/// The delay before showing the loading state.
pub const LOADING_DELAY: Duration = Duration::from_millis(200);
/// A type alias to the resource loader that the `img()` element uses.
///
/// Note: that this is only for Resources, like URLs or file paths.
/// Custom loaders, or external images will not use this asset loader
pub type ImgResourceLoader = AssetLogger<ImageAssetLoader>;
/// A source of image content.
#[derive(Clone)]
pub enum ImageSource {
/// The image content will be loaded from some resource location
Resource(Resource),
/// Cached image data
Render(Arc<RenderImage>),
/// Cached image data
Image(Arc<Image>),
/// A custom loading function to use
Custom(Arc<dyn Fn(&mut Window, &mut App) -> Option<Result<Arc<RenderImage>, ImageCacheError>>>),
}
fn is_uri(uri: &str) -> bool {
http_client::Uri::from_str(uri).is_ok()
}
impl From<SharedUri> for ImageSource {
fn from(value: SharedUri) -> Self {
Self::Resource(Resource::Uri(value))
}
}
impl<'a> From<&'a str> for ImageSource {
fn from(s: &'a str) -> Self {
if is_uri(s) {
Self::Resource(Resource::Uri(s.to_string().into()))
} else {
Self::Resource(Resource::Embedded(s.to_string().into()))
}
}
}
impl From<String> for ImageSource {
fn from(s: String) -> Self {
if is_uri(&s) {
Self::Resource(Resource::Uri(s.into()))
} else {
Self::Resource(Resource::Embedded(s.into()))
}
}
}
impl From<SharedString> for ImageSource {
fn from(s: SharedString) -> Self {
s.as_ref().into()
}
}
impl From<&Path> for ImageSource {
fn from(value: &Path) -> Self {
Self::Resource(value.to_path_buf().into())
}
}
impl From<Arc<Path>> for ImageSource {
fn from(value: Arc<Path>) -> Self {
Self::Resource(value.into())
}
}
impl From<PathBuf> for ImageSource {
fn from(value: PathBuf) -> Self {
Self::Resource(value.into())
}
}
impl From<Arc<RenderImage>> for ImageSource {
fn from(value: Arc<RenderImage>) -> Self {
Self::Render(value)
}
}
impl From<Arc<Image>> for ImageSource {
fn from(value: Arc<Image>) -> Self {
Self::Image(value)
}
}
impl<F> From<F> for ImageSource
where
F: Fn(&mut Window, &mut App) -> Option<Result<Arc<RenderImage>, ImageCacheError>> + 'static,
{
fn from(value: F) -> Self {
Self::Custom(Arc::new(value))
}
}
/// The style of an image element.
pub struct ImageStyle {
grayscale: bool,
object_fit: ObjectFit,
loading: Option<Box<dyn Fn() -> AnyElement>>,
fallback: Option<Box<dyn Fn() -> AnyElement>>,
}
impl Default for ImageStyle {
fn default() -> Self {
Self {
grayscale: false,
object_fit: ObjectFit::Contain,
loading: None,
fallback: None,
}
}
}
/// Style an image element.
pub trait StyledImage: Sized {
/// Get a mutable [ImageStyle] from the element.
fn image_style(&mut self) -> &mut ImageStyle;
/// Set the image to be displayed in grayscale.
fn grayscale(mut self, grayscale: bool) -> Self {
self.image_style().grayscale = grayscale;
self
}
/// Set the object fit for the image.
fn object_fit(mut self, object_fit: ObjectFit) -> Self {
self.image_style().object_fit = object_fit;
self
}
/// Set the object fit for the image.
fn with_fallback(mut self, fallback: impl Fn() -> AnyElement + 'static) -> Self {
self.image_style().fallback = Some(Box::new(fallback));
self
}
/// Set the object fit for the image.
fn with_loading(mut self, loading: impl Fn() -> AnyElement + 'static) -> Self {
self.image_style().loading = Some(Box::new(loading));
self
}
}
impl StyledImage for Img {
fn image_style(&mut self) -> &mut ImageStyle {
&mut self.style
}
}
impl StyledImage for Stateful<Img> {
fn image_style(&mut self) -> &mut ImageStyle {
&mut self.element.style
}
}
/// An image element.
pub struct Img {
interactivity: Interactivity,
source: ImageSource,
style: ImageStyle,
image_cache: Option<AnyImageCache>,
}
/// Create a new image element.
#[track_caller]
pub fn img(source: impl Into<ImageSource>) -> Img {
Img {
interactivity: Interactivity::new(),
source: source.into(),
style: ImageStyle::default(),
image_cache: None,
}
}
impl Img {
/// A list of all format extensions currently supported by this img element
pub fn extensions() -> &'static [&'static str] {
// This is the list in [image::ImageFormat::from_extension] + `svg`
&[
"avif", "jpg", "jpeg", "png", "gif", "webp", "tif", "tiff", "tga", "dds", "bmp", "ico",
"hdr", "exr", "pbm", "pam", "ppm", "pgm", "ff", "farbfeld", "qoi", "svg",
]
}
/// Sets the image cache for the current node.
///
/// If the `image_cache` is not explicitly provided, the function will determine the image cache by:
///
/// 1. Checking if any ancestor node of the current node contains an `ImageCacheElement`, If such a node exists, the image cache specified by that ancestor will be used.
/// 2. If no ancestor node contains an `ImageCacheElement`, the global image cache will be used as a fallback.
///
/// This mechanism provides a flexible way to manage image caching, allowing precise control when needed,
/// while ensuring a default behavior when no cache is explicitly specified.
#[inline]
pub fn image_cache<I: ImageCache>(self, image_cache: &Entity<I>) -> Self {
Self {
image_cache: Some(image_cache.clone().into()),
..self
}
}
}
impl Deref for Stateful<Img> {
type Target = Img;
fn deref(&self) -> &Self::Target {
&self.element
}
}
impl DerefMut for Stateful<Img> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.element
}
}
/// The image state between frames
struct ImgState {
frame_index: usize,
last_frame_time: Option<Instant>,
started_loading: Option<(Instant, Task<()>)>,
}
/// The image layout state between frames
pub struct ImgLayoutState {
frame_index: usize,
replacement: Option<AnyElement>,
}
impl Element for Img {
type RequestLayoutState = ImgLayoutState;
type PrepaintState = Option<Hitbox>;
fn id(&self) -> Option<ElementId> {
self.interactivity.element_id.clone()
}
fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
self.interactivity.source_location()
}
fn request_layout(
&mut self,
global_id: Option<&GlobalElementId>,
inspector_id: Option<&InspectorElementId>,
window: &mut Window,
cx: &mut App,
) -> (LayoutId, Self::RequestLayoutState) {
let mut layout_state = ImgLayoutState {
frame_index: 0,
replacement: None,
};
window.with_optional_element_state(global_id, |state, window| {
let mut state = state.map(|state| {
state.unwrap_or(ImgState {
frame_index: 0,
last_frame_time: None,
started_loading: None,
})
});
let frame_index = state.as_ref().map(|state| state.frame_index).unwrap_or(0);
let layout_id = self.interactivity.request_layout(
global_id,
inspector_id,
window,
cx,
|mut style, window, cx| {
let mut replacement_id = None;
match self.source.use_data(
self.image_cache
.clone()
.or_else(|| window.image_cache_stack.last().cloned()),
window,
cx,
) {
Some(Ok(data)) => {
if let Some(state) = &mut state {
let frame_count = data.frame_count();
if frame_count > 1 {
let current_time = Instant::now();
if let Some(last_frame_time) = state.last_frame_time {
let elapsed = current_time - last_frame_time;
let frame_duration =
Duration::from(data.delay(state.frame_index));
if elapsed >= frame_duration {
state.frame_index =
(state.frame_index + 1) % frame_count;
state.last_frame_time =
Some(current_time - (elapsed - frame_duration));
}
} else {
state.last_frame_time = Some(current_time);
}
}
state.started_loading = None;
}
let image_size = data.size(frame_index);
style.aspect_ratio =
Some(image_size.width.0 as f32 / image_size.height.0 as f32);
if let Length::Auto = style.size.width {
style.size.width = match style.size.height {
Length::Definite(DefiniteLength::Absolute(
AbsoluteLength::Pixels(height),
)) => Length::Definite(
px(image_size.width.0 as f32 * height.0
/ image_size.height.0 as f32)
.into(),
),
_ => Length::Definite(px(image_size.width.0 as f32).into()),
};
}
if let Length::Auto = style.size.height {
style.size.height = match style.size.width {
Length::Definite(DefiniteLength::Absolute(
AbsoluteLength::Pixels(width),
)) => Length::Definite(
px(image_size.height.0 as f32 * width.0
/ image_size.width.0 as f32)
.into(),
),
_ => Length::Definite(px(image_size.height.0 as f32).into()),
};
}
if global_id.is_some() && data.frame_count() > 1 {
window.request_animation_frame();
}
}
Some(_err) => {
if let Some(fallback) = self.style.fallback.as_ref() {
let mut element = fallback();
replacement_id = Some(element.request_layout(window, cx));
layout_state.replacement = Some(element);
}
if let Some(state) = &mut state {
state.started_loading = None;
}
}
None => {
if let Some(state) = &mut state {
if let Some((started_loading, _)) = state.started_loading {
if started_loading.elapsed() > LOADING_DELAY {
if let Some(loading) = self.style.loading.as_ref() {
let mut element = loading();
replacement_id =
Some(element.request_layout(window, cx));
layout_state.replacement = Some(element);
}
}
} else {
let current_view = window.current_view();
let task = window.spawn(cx, async move |cx| {
cx.background_executor().timer(LOADING_DELAY).await;
cx.update(move |_, cx| {
cx.notify(current_view);
})
.ok();
});
state.started_loading = Some((Instant::now(), task));
}
}
}
}
window.request_layout(style, replacement_id, cx)
},
);
layout_state.frame_index = frame_index;
((layout_id, layout_state), state)
})
}
fn prepaint(
&mut self,
global_id: Option<&GlobalElementId>,
inspector_id: Option<&InspectorElementId>,
bounds: Bounds<Pixels>,
request_layout: &mut Self::RequestLayoutState,
window: &mut Window,
cx: &mut App,
) -> Self::PrepaintState {
self.interactivity.prepaint(
global_id,
inspector_id,
bounds,
bounds.size,
window,
cx,
|_, _, hitbox, window, cx| {
if let Some(replacement) = &mut request_layout.replacement {
replacement.prepaint(window, cx);
}
hitbox
},
)
}
fn paint(
&mut self,
global_id: Option<&GlobalElementId>,
inspector_id: Option<&InspectorElementId>,
bounds: Bounds<Pixels>,
layout_state: &mut Self::RequestLayoutState,
hitbox: &mut Self::PrepaintState,
window: &mut Window,
cx: &mut App,
) {
let source = self.source.clone();
self.interactivity.paint(
global_id,
inspector_id,
bounds,
hitbox.as_ref(),
window,
cx,
|style, window, cx| {
if let Some(Ok(data)) = source.use_data(
self.image_cache
.clone()
.or_else(|| window.image_cache_stack.last().cloned()),
window,
cx,
) {
let new_bounds = self
.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);
window
.paint_image(
new_bounds,
corner_radii,
data.clone(),
layout_state.frame_index,
self.style.grayscale,
)
.log_err();
} else if let Some(replacement) = &mut layout_state.replacement {
replacement.paint(window, cx);
}
},
)
}
}
impl Styled for Img {
fn style(&mut self) -> &mut StyleRefinement {
&mut self.interactivity.base_style
}
}
impl InteractiveElement for Img {
fn interactivity(&mut self) -> &mut Interactivity {
&mut self.interactivity
}
}
impl IntoElement for Img {
type Element = Self;
fn into_element(self) -> Self::Element {
self
}
}
impl FocusableElement for Img {}
impl StatefulInteractiveElement for Img {}
impl ImageSource {
pub(crate) fn use_data(
&self,
cache: Option<AnyImageCache>,
window: &mut Window,
cx: &mut App,
) -> Option<Result<Arc<RenderImage>, ImageCacheError>> {
match self {
ImageSource::Resource(resource) => {
if let Some(cache) = cache {
cache.load(resource, window, cx)
} else {
window.use_asset::<ImgResourceLoader>(resource, cx)
}
}
ImageSource::Custom(loading_fn) => loading_fn(window, cx),
ImageSource::Render(data) => Some(Ok(data.to_owned())),
ImageSource::Image(data) => window.use_asset::<AssetLogger<ImageDecoder>>(data, cx),
}
}
pub(crate) fn get_data(
&self,
cache: Option<AnyImageCache>,
window: &mut Window,
cx: &mut App,
) -> Option<Result<Arc<RenderImage>, ImageCacheError>> {
match self {
ImageSource::Resource(resource) => {
if let Some(cache) = cache {
cache.load(resource, window, cx)
} else {
window.get_asset::<ImgResourceLoader>(resource, cx)
}
}
ImageSource::Custom(loading_fn) => loading_fn(window, cx),
ImageSource::Render(data) => Some(Ok(data.to_owned())),
ImageSource::Image(data) => window.get_asset::<AssetLogger<ImageDecoder>>(data, cx),
}
}
/// Remove this image source from the asset system
pub fn remove_asset(&self, cx: &mut App) {
match self {
ImageSource::Resource(resource) => {
cx.remove_asset::<ImgResourceLoader>(resource);
}
ImageSource::Custom(_) | ImageSource::Render(_) => {}
ImageSource::Image(data) => cx.remove_asset::<AssetLogger<ImageDecoder>>(data),
}
}
}
#[derive(Clone)]
enum ImageDecoder {}
impl Asset for ImageDecoder {
type Source = Arc<Image>;
type Output = Result<Arc<RenderImage>, ImageCacheError>;
fn load(
source: Self::Source,
cx: &mut App,
) -> impl Future<Output = Self::Output> + Send + 'static {
let renderer = cx.svg_renderer();
async move { source.to_image_data(renderer).map_err(Into::into) }
}
}
/// An image loader for the GPUI asset system
#[derive(Clone)]
pub enum ImageAssetLoader {}
impl Asset for ImageAssetLoader {
type Source = Resource;
type Output = Result<Arc<RenderImage>, ImageCacheError>;
fn load(
source: Self::Source,
cx: &mut App,
) -> impl Future<Output = Self::Output> + Send + 'static {
let client = cx.http_client();
// TODO: Can we make SVGs always rescale?
// let scale_factor = cx.scale_factor();
let svg_renderer = cx.svg_renderer();
let asset_source = cx.asset_source().clone();
async move {
let bytes = match source.clone() {
Resource::Path(uri) => fs::read(uri.as_ref())?,
Resource::Uri(uri) => {
let mut response = client
.get(uri.as_ref(), ().into(), true)
.await
.with_context(|| format!("loading image asset from {uri:?}"))?;
let mut body = Vec::new();
response.body_mut().read_to_end(&mut body).await?;
if !response.status().is_success() {
let mut body = String::from_utf8_lossy(&body).into_owned();
let first_line = body.lines().next().unwrap_or("").trim_end();
body.truncate(first_line.len());
return Err(ImageCacheError::BadStatus {
uri,
status: response.status(),
body,
});
}
body
}
Resource::Embedded(path) => {
let data = asset_source.load(&path).ok().flatten();
if let Some(data) = data {
data.to_vec()
} else {
return Err(ImageCacheError::Asset(
format!("Embedded resource not found: {}", path).into(),
));
}
}
};
let data = if let Ok(format) = image::guess_format(&bytes) {
let data = match format {
ImageFormat::Gif => {
let decoder = GifDecoder::new(Cursor::new(&bytes))?;
let mut frames = SmallVec::new();
for frame in decoder.into_frames() {
let mut frame = frame?;
// Convert from RGBA to BGRA.
for pixel in frame.buffer_mut().chunks_exact_mut(4) {
pixel.swap(0, 2);
}
frames.push(frame);
}
frames
}
ImageFormat::WebP => {
let mut decoder = WebPDecoder::new(Cursor::new(&bytes))?;
if decoder.has_animation() {
let _ = decoder.set_background_color(Rgba([0, 0, 0, 0]));
let mut frames = SmallVec::new();
for frame in decoder.into_frames() {
let mut frame = frame?;
// Convert from RGBA to BGRA.
for pixel in frame.buffer_mut().chunks_exact_mut(4) {
pixel.swap(0, 2);
}
frames.push(frame);
}
frames
} else {
let mut data = DynamicImage::from_decoder(decoder)?.into_rgba8();
// Convert from RGBA to BGRA.
for pixel in data.chunks_exact_mut(4) {
pixel.swap(0, 2);
}
SmallVec::from_elem(Frame::new(data), 1)
}
}
_ => {
let mut data =
image::load_from_memory_with_format(&bytes, format)?.into_rgba8();
// Convert from RGBA to BGRA.
for pixel in data.chunks_exact_mut(4) {
pixel.swap(0, 2);
}
SmallVec::from_elem(Frame::new(data), 1)
}
};
RenderImage::new(data)
} else {
let pixmap =
// TODO: Can we make svgs always rescale?
svg_renderer.render_pixmap(&bytes, SvgSize::ScaleFactor(SMOOTH_SVG_SCALE_FACTOR))?;
let mut buffer =
ImageBuffer::from_raw(pixmap.width(), pixmap.height(), pixmap.take()).unwrap();
for pixel in buffer.chunks_exact_mut(4) {
swap_rgba_pa_to_bgra(pixel);
}
RenderImage::new(SmallVec::from_elem(Frame::new(buffer), 1))
};
Ok(Arc::new(data))
}
}
}
/// An error that can occur when interacting with the image cache.
#[derive(Debug, Error, Clone)]
pub enum ImageCacheError {
/// Some other kind of error occurred
#[error("error: {0}")]
Other(#[from] Arc<anyhow::Error>),
/// An error that occurred while reading the image from disk.
#[error("IO error: {0}")]
Io(Arc<std::io::Error>),
/// An error that occurred while processing an image.
#[error("unexpected http status for {uri}: {status}, body: {body}")]
BadStatus {
/// The URI of the image.
uri: SharedUri,
/// The HTTP status code.
status: http_client::StatusCode,
/// The HTTP response body.
body: String,
},
/// An error that occurred while processing an asset.
#[error("asset error: {0}")]
Asset(SharedString),
/// An error that occurred while processing an image.
#[error("image error: {0}")]
Image(Arc<ImageError>),
/// An error that occurred while processing an SVG.
#[error("svg error: {0}")]
Usvg(Arc<usvg::Error>),
}
impl From<anyhow::Error> for ImageCacheError {
fn from(value: anyhow::Error) -> Self {
Self::Other(Arc::new(value))
}
}
impl From<io::Error> for ImageCacheError {
fn from(value: io::Error) -> Self {
Self::Io(Arc::new(value))
}
}
impl From<usvg::Error> for ImageCacheError {
fn from(value: usvg::Error) -> Self {
Self::Usvg(Arc::new(value))
}
}
impl From<image::ImageError> for ImageCacheError {
fn from(value: image::ImageError) -> Self {
Self::Image(Arc::new(value))
}
}