Compare commits
3 Commits
outline-in
...
animated-g
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5be5637979 | ||
|
|
5822c821bb | ||
|
|
9c3fefebe4 |
@@ -1,6 +1,6 @@
|
||||
use crate::{size, DevicePixels, Result, SharedString, Size};
|
||||
|
||||
use image::RgbaImage;
|
||||
use image::{Delay, Frame};
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
fmt,
|
||||
@@ -34,18 +34,19 @@ 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: Vec<Frame>,
|
||||
}
|
||||
|
||||
impl ImageData {
|
||||
/// Create a new image from the given data.
|
||||
pub fn new(data: RgbaImage) -> Self {
|
||||
pub fn new(data: Vec<Frame>) -> Self {
|
||||
static NEXT_ID: AtomicUsize = AtomicUsize::new(0);
|
||||
|
||||
Self {
|
||||
@@ -55,22 +56,32 @@ impl ImageData {
|
||||
}
|
||||
|
||||
/// 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,6 +1,6 @@
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use std::{fs, io::Cursor};
|
||||
|
||||
use crate::{
|
||||
point, px, size, AbsoluteLength, Asset, Bounds, DefiniteLength, DevicePixels, Element,
|
||||
@@ -8,12 +8,15 @@ use crate::{
|
||||
LayoutId, Length, Pixels, SharedUri, Size, StyleRefinement, Styled, SvgSize, UriOrPath,
|
||||
WindowContext,
|
||||
};
|
||||
|
||||
use futures::{AsyncReadExt, Future};
|
||||
use image::{ImageBuffer, ImageError};
|
||||
use image::codecs::gif::GifDecoder;
|
||||
use image::{AnimationDecoder, Frame, ImageBuffer, ImageError, ImageFormat};
|
||||
#[cfg(target_os = "macos")]
|
||||
use media::core_video::CVImageBuffer;
|
||||
|
||||
use http;
|
||||
use std::time::{Duration, Instant};
|
||||
use thiserror::Error;
|
||||
use util::ResultExt;
|
||||
|
||||
@@ -230,8 +233,14 @@ impl Img {
|
||||
}
|
||||
}
|
||||
|
||||
/// The image state between frames
|
||||
struct ImgState {
|
||||
frame_index: usize,
|
||||
last_frame_time: Instant,
|
||||
}
|
||||
|
||||
impl Element for Img {
|
||||
type RequestLayoutState = ();
|
||||
type RequestLayoutState = usize;
|
||||
type PrepaintState = Option<Hitbox>;
|
||||
|
||||
fn id(&self) -> Option<ElementId> {
|
||||
@@ -243,29 +252,72 @@ 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: Instant::now(),
|
||||
})
|
||||
});
|
||||
|
||||
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 {
|
||||
if state.last_frame_time.elapsed()
|
||||
>= Duration::from(data.delay(state.frame_index))
|
||||
{
|
||||
let time_difference = state.last_frame_time.elapsed()
|
||||
- Duration::from(data.delay(state.frame_index));
|
||||
|
||||
if state.frame_index == frame_count - 1 {
|
||||
state.frame_index = 0;
|
||||
} else {
|
||||
state.frame_index = state.frame_index + 1;
|
||||
};
|
||||
|
||||
state.last_frame_time = Instant::now() - time_difference
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
let parent_id = cx.parent_view_id();
|
||||
cx.on_next_frame(move |cx| {
|
||||
if let Some(parent_id) = parent_id {
|
||||
cx.notify(parent_id)
|
||||
} else {
|
||||
cx.refresh()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
cx.request_layout(style, [])
|
||||
});
|
||||
|
||||
((layout_id, frame_index), state)
|
||||
})
|
||||
}
|
||||
|
||||
fn prepaint(
|
||||
@@ -283,7 +335,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 +345,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 {
|
||||
@@ -384,12 +442,33 @@ 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 mut gif = GifDecoder::new(Cursor::new(&bytes))?
|
||||
.into_frames()
|
||||
.collect_frames()?;
|
||||
|
||||
// Convert from RGBA to BGRA.
|
||||
for pixel in data.chunks_exact_mut(4) {
|
||||
pixel.swap(0, 2);
|
||||
}
|
||||
for frame in &mut gif {
|
||||
// Convert from RGBA to BGRA.
|
||||
for pixel in frame.buffer_mut().chunks_exact_mut(4) {
|
||||
pixel.swap(0, 2);
|
||||
}
|
||||
}
|
||||
|
||||
gif
|
||||
}
|
||||
_ => {
|
||||
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);
|
||||
}
|
||||
|
||||
vec![Frame::new(data)]
|
||||
}
|
||||
};
|
||||
|
||||
ImageData::new(data)
|
||||
} else {
|
||||
@@ -399,7 +478,7 @@ impl Asset for Image {
|
||||
let buffer =
|
||||
ImageBuffer::from_raw(pixmap.width(), pixmap.height(), pixmap.take()).unwrap();
|
||||
|
||||
ImageData::new(buffer)
|
||||
ImageData::new(vec![Frame::new(buffer)])
|
||||
};
|
||||
|
||||
Ok(Arc::new(data))
|
||||
|
||||
@@ -2567,6 +2567,7 @@ impl<'a> WindowContext<'a> {
|
||||
bounds: Bounds<Pixels>,
|
||||
corner_radii: Corners<Pixels>,
|
||||
data: Arc<ImageData>,
|
||||
frame_index: usize,
|
||||
grayscale: bool,
|
||||
) -> Result<()> {
|
||||
debug_assert_eq!(
|
||||
@@ -2577,13 +2578,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);
|
||||
|
||||
@@ -70,7 +70,7 @@ impl ImageView {
|
||||
let height = data.height();
|
||||
let width = data.width();
|
||||
|
||||
let gpui_image_data = ImageData::new(data);
|
||||
let gpui_image_data = ImageData::new(vec![image::Frame::new(data)]);
|
||||
|
||||
return Ok(ImageView {
|
||||
height,
|
||||
|
||||
Reference in New Issue
Block a user