@@ -0,0 +1,50 @@
+use gpui::{
+ div, img, prelude::*, App, AppContext, ImageSource, Render, ViewContext, WindowOptions,
+};
+use std::path::PathBuf;
+
+struct GifViewer {
+ gif_path: PathBuf,
+}
+
+impl GifViewer {
+ fn new(gif_path: PathBuf) -> Self {
+ Self { gif_path }
+ }
+}
+
+impl Render for GifViewer {
+ fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
+ div().size_full().child(
+ img(ImageSource::File(self.gif_path.clone().into()))
+ .size_full()
+ .object_fit(gpui::ObjectFit::Contain)
+ .id("gif"),
+ )
+ }
+}
+
+fn main() {
+ env_logger::init();
+ App::new().run(|cx: &mut AppContext| {
+ let cwd = std::env::current_dir().expect("Failed to get current working directory");
+ let gif_path = cwd.join("crates/gpui/examples/image/black-cat-typing.gif");
+
+ if !gif_path.exists() {
+ eprintln!("Image file not found at {:?}", gif_path);
+ eprintln!("Make sure you're running this example from the root of the gpui crate");
+ cx.quit();
+ return;
+ }
+
+ cx.open_window(
+ WindowOptions {
+ focus: true,
+ ..Default::default()
+ },
+ |cx| cx.new_view(|_cx| GifViewer::new(gif_path)),
+ )
+ .unwrap();
+ cx.activate(true);
+ });
+}
@@ -1,6 +1,7 @@
use crate::{size, DevicePixels, Result, SharedString, Size};
+use smallvec::SmallVec;
-use image::RgbaImage;
+use image::{Delay, Frame};
use std::{
borrow::Cow,
fmt,
@@ -34,43 +35,54 @@ pub struct ImageId(usize);
#[derive(PartialEq, Eq, Hash, Clone)]
pub(crate) struct RenderImageParams {
pub(crate) image_id: ImageId,
+ pub(crate) frame_index: usize,
}
/// A cached and processed image.
pub struct ImageData {
/// The ID associated with this image
pub id: ImageId,
- data: RgbaImage,
+ data: SmallVec<[Frame; 1]>,
}
impl ImageData {
/// Create a new image from the given data.
- pub fn new(data: RgbaImage) -> Self {
+ pub fn new(data: impl Into<SmallVec<[Frame; 1]>>) -> Self {
static NEXT_ID: AtomicUsize = AtomicUsize::new(0);
Self {
id: ImageId(NEXT_ID.fetch_add(1, SeqCst)),
- data,
+ data: data.into(),
}
}
/// Convert this image into a byte slice.
- pub fn as_bytes(&self) -> &[u8] {
- &self.data
+ pub fn as_bytes(&self, frame_index: usize) -> &[u8] {
+ &self.data[frame_index].buffer()
}
- /// Get the size of this image, in pixels
- pub fn size(&self) -> Size<DevicePixels> {
- let (width, height) = self.data.dimensions();
+ /// Get the size of this image, in pixels.
+ pub fn size(&self, frame_index: usize) -> Size<DevicePixels> {
+ let (width, height) = self.data[frame_index].buffer().dimensions();
size(width.into(), height.into())
}
+
+ /// Get the delay of this frame from the previous
+ pub fn delay(&self, frame_index: usize) -> Delay {
+ self.data[frame_index].delay()
+ }
+
+ /// Get the number of frames for this image.
+ pub fn frame_count(&self) -> usize {
+ self.data.len()
+ }
}
impl fmt::Debug for ImageData {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ImageData")
.field("id", &self.id)
- .field("size", &self.data.dimensions())
+ .field("size", &self.size(0))
.finish()
}
}
@@ -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);
+ }
}
}
- _ => {}
+
+ 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, ())
+ 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();
+
+ 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);
+ }
- // Convert from RGBA to BGRA.
- for pixel in data.chunks_exact_mut(4) {
- pixel.swap(0, 2);
- }
+ 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))
@@ -1158,6 +1158,23 @@ impl<'a> WindowContext<'a> {
RefCell::borrow_mut(&self.window.next_frame_callbacks).push(Box::new(callback));
}
+ /// Schedule a frame to be drawn on the next animation frame.
+ ///
+ /// This is useful for elements that need to animate continuously, such as a video player or an animated GIF.
+ /// It will cause the window to redraw on the next frame, even if no other changes have occurred.
+ ///
+ /// If called from within a view, it will notify that view on the next frame. Otherwise, it will refresh the entire window.
+ pub fn request_animation_frame(&mut self) {
+ let parent_id = self.parent_view_id();
+ self.on_next_frame(move |cx| {
+ if let Some(parent_id) = parent_id {
+ cx.notify(parent_id)
+ } else {
+ cx.refresh()
+ }
+ });
+ }
+
/// Spawn the future returned by the given closure on the application thread pool.
/// The closure is provided a handle to the current window and an `AsyncWindowContext` for
/// use within your future.
@@ -2602,6 +2619,7 @@ impl<'a> WindowContext<'a> {
bounds: Bounds<Pixels>,
corner_radii: Corners<Pixels>,
data: Arc<ImageData>,
+ frame_index: usize,
grayscale: bool,
) -> Result<()> {
debug_assert_eq!(
@@ -2612,13 +2630,19 @@ impl<'a> WindowContext<'a> {
let scale_factor = self.scale_factor();
let bounds = bounds.scale(scale_factor);
- let params = RenderImageParams { image_id: data.id };
+ let params = RenderImageParams {
+ image_id: data.id,
+ frame_index,
+ };
let tile = self
.window
.sprite_atlas
.get_or_insert_with(¶ms.clone().into(), &mut || {
- Ok(Some((data.size(), Cow::Borrowed(data.as_bytes()))))
+ Ok(Some((
+ data.size(frame_index),
+ Cow::Borrowed(data.as_bytes(frame_index)),
+ )))
})?
.expect("Callback above only returns Some");
let content_mask = self.content_mask().scale(scale_factor);