gpui: Add support for animated images (#13809)
This PR adds support for animated images. The image requires a id for it to actually animate across frames. Currently it only has support for `GIF`, I tried adding decoding a animated `WebP` into frames but it seems to error. This issue in the image crate seems to document this https://github.com/image-rs/image/issues/2263. Not sure if this is the best way or the desired way for animated images to work in GPUI but I would really like support for animated images. Open to feedback. Example Video: https://github.com/zed-industries/zed/assets/76515905/011f790f-d070-499b-96c9-bbff141fb002 Closes https://github.com/zed-industries/zed/issues/9993 Release Notes: - N/A --------- Co-authored-by: Antonio Scandurra <me@as-cii.com> Co-authored-by: Nathan <nathan@zed.dev>
This commit is contained in:
parent
c0df1e1846
commit
4bd935b409
6 changed files with 212 additions and 51 deletions
|
@ -1,7 +1,3 @@
|
|||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::{
|
||||
point, px, size, AbsoluteLength, Asset, Bounds, DefiniteLength, DevicePixels, Element,
|
||||
ElementId, GlobalElementId, Hitbox, ImageData, InteractiveElement, Interactivity, IntoElement,
|
||||
|
@ -9,11 +5,20 @@ use crate::{
|
|||
WindowContext,
|
||||
};
|
||||
use futures::{AsyncReadExt, Future};
|
||||
use image::{ImageBuffer, ImageError};
|
||||
use http_client;
|
||||
use image::{
|
||||
codecs::gif::GifDecoder, AnimationDecoder, Frame, ImageBuffer, ImageError, ImageFormat,
|
||||
};
|
||||
#[cfg(target_os = "macos")]
|
||||
use media::core_video::CVImageBuffer;
|
||||
|
||||
use http_client;
|
||||
use smallvec::SmallVec;
|
||||
use std::{
|
||||
fs,
|
||||
io::Cursor,
|
||||
path::PathBuf,
|
||||
sync::Arc,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use thiserror::Error;
|
||||
use util::ResultExt;
|
||||
|
||||
|
@ -230,8 +235,14 @@ impl Img {
|
|||
}
|
||||
}
|
||||
|
||||
/// The image state between frames
|
||||
struct ImgState {
|
||||
frame_index: usize,
|
||||
last_frame_time: Option<Instant>,
|
||||
}
|
||||
|
||||
impl Element for Img {
|
||||
type RequestLayoutState = ();
|
||||
type RequestLayoutState = usize;
|
||||
type PrepaintState = Option<Hitbox>;
|
||||
|
||||
fn id(&self) -> Option<ElementId> {
|
||||
|
@ -243,29 +254,65 @@ impl Element for Img {
|
|||
global_id: Option<&GlobalElementId>,
|
||||
cx: &mut WindowContext,
|
||||
) -> (LayoutId, Self::RequestLayoutState) {
|
||||
let layout_id = self
|
||||
.interactivity
|
||||
.request_layout(global_id, cx, |mut style, cx| {
|
||||
if let Some(data) = self.source.data(cx) {
|
||||
let image_size = data.size();
|
||||
match (style.size.width, style.size.height) {
|
||||
(Length::Auto, Length::Auto) => {
|
||||
style.size = Size {
|
||||
width: Length::Definite(DefiniteLength::Absolute(
|
||||
AbsoluteLength::Pixels(px(image_size.width.0 as f32)),
|
||||
)),
|
||||
height: Length::Definite(DefiniteLength::Absolute(
|
||||
AbsoluteLength::Pixels(px(image_size.height.0 as f32)),
|
||||
)),
|
||||
cx.with_optional_element_state(global_id, |state, cx| {
|
||||
let mut state = state.map(|state| {
|
||||
state.unwrap_or(ImgState {
|
||||
frame_index: 0,
|
||||
last_frame_time: None,
|
||||
})
|
||||
});
|
||||
|
||||
let frame_index = state.as_ref().map(|state| state.frame_index).unwrap_or(0);
|
||||
|
||||
let layout_id = self
|
||||
.interactivity
|
||||
.request_layout(global_id, cx, |mut style, cx| {
|
||||
if let Some(data) = self.source.data(cx) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
cx.request_layout(style, [])
|
||||
});
|
||||
(layout_id, ())
|
||||
let image_size = data.size(frame_index);
|
||||
match (style.size.width, style.size.height) {
|
||||
(Length::Auto, Length::Auto) => {
|
||||
style.size = Size {
|
||||
width: Length::Definite(DefiniteLength::Absolute(
|
||||
AbsoluteLength::Pixels(px(image_size.width.0 as f32)),
|
||||
)),
|
||||
height: Length::Definite(DefiniteLength::Absolute(
|
||||
AbsoluteLength::Pixels(px(image_size.height.0 as f32)),
|
||||
)),
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
if global_id.is_some() && data.frame_count() > 1 {
|
||||
cx.request_animation_frame();
|
||||
}
|
||||
}
|
||||
|
||||
cx.request_layout(style, [])
|
||||
});
|
||||
|
||||
((layout_id, frame_index), state)
|
||||
})
|
||||
}
|
||||
|
||||
fn prepaint(
|
||||
|
@ -283,7 +330,7 @@ impl Element for Img {
|
|||
&mut self,
|
||||
global_id: Option<&GlobalElementId>,
|
||||
bounds: Bounds<Pixels>,
|
||||
_: &mut Self::RequestLayoutState,
|
||||
frame_index: &mut Self::RequestLayoutState,
|
||||
hitbox: &mut Self::PrepaintState,
|
||||
cx: &mut WindowContext,
|
||||
) {
|
||||
|
@ -293,9 +340,15 @@ impl Element for Img {
|
|||
let corner_radii = style.corner_radii.to_pixels(bounds.size, cx.rem_size());
|
||||
|
||||
if let Some(data) = source.data(cx) {
|
||||
let new_bounds = self.object_fit.get_bounds(bounds, data.size());
|
||||
cx.paint_image(new_bounds, corner_radii, data.clone(), self.grayscale)
|
||||
.log_err();
|
||||
let new_bounds = self.object_fit.get_bounds(bounds, data.size(*frame_index));
|
||||
cx.paint_image(
|
||||
new_bounds,
|
||||
corner_radii,
|
||||
data.clone(),
|
||||
*frame_index,
|
||||
self.grayscale,
|
||||
)
|
||||
.log_err();
|
||||
}
|
||||
|
||||
match source {
|
||||
|
@ -385,12 +438,34 @@ impl Asset for Image {
|
|||
};
|
||||
|
||||
let data = if let Ok(format) = image::guess_format(&bytes) {
|
||||
let mut data = image::load_from_memory_with_format(&bytes, format)?.into_rgba8();
|
||||
let data = match format {
|
||||
ImageFormat::Gif => {
|
||||
let decoder = GifDecoder::new(Cursor::new(&bytes))?;
|
||||
let mut frames = SmallVec::new();
|
||||
|
||||
// Convert from RGBA to BGRA.
|
||||
for pixel in data.chunks_exact_mut(4) {
|
||||
pixel.swap(0, 2);
|
||||
}
|
||||
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
|
||||
}
|
||||
_ => {
|
||||
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)
|
||||
}
|
||||
};
|
||||
|
||||
ImageData::new(data)
|
||||
} else {
|
||||
|
@ -400,7 +475,7 @@ impl Asset for Image {
|
|||
let buffer =
|
||||
ImageBuffer::from_raw(pixmap.width(), pixmap.height(), pixmap.take()).unwrap();
|
||||
|
||||
ImageData::new(buffer)
|
||||
ImageData::new(SmallVec::from_elem(Frame::new(buffer), 1))
|
||||
};
|
||||
|
||||
Ok(Arc::new(data))
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue