libcosmic/src/widget/frames.rs

400 lines
11 KiB
Rust

//! Display an animated image in your user interface
//! Based on <https://github.com/tarkah/iced_gif/>
use std::ffi::OsStr;
use std::fmt;
use std::io;
use std::path::Path;
use std::time::{Duration, Instant};
use ::image as image_rs;
use iced::Task;
use iced::mouse;
use iced_core::image::Renderer as ImageRenderer;
use iced_core::mouse::Cursor;
use iced_core::widget::{Tree, tree};
use iced_core::{
Clipboard, ContentFit, Element, Event, Layout, Length, Rectangle, Rotation, Shell, Size,
Widget, event, layout, renderer, window,
};
use iced_widget::image::{self, FilterMethod, Handle};
use image_rs::AnimationDecoder;
use image_rs::codecs::gif::GifDecoder;
use image_rs::codecs::png::PngDecoder;
use image_rs::codecs::webp::WebPDecoder;
#[cfg(not(feature = "tokio"))]
use iced_futures::futures::{AsyncRead, AsyncReadExt};
#[cfg(feature = "tokio")]
use tokio::io::{AsyncRead, AsyncReadExt};
use crate::widget::icon;
#[must_use]
/// Creates a new [`AnimatedImage`] with the given [`animated_image::Frames`]
pub fn animated_image(frames: &Frames) -> AnimatedImage {
AnimatedImage::new(frames)
}
/// Error loading or decoding a animated_image
#[derive(Debug, thiserror::Error)]
pub enum Error {
/// Decode error
#[error(transparent)]
Image(#[from] image_rs::ImageError),
/// Load error
#[error(transparent)]
Io(#[from] std::io::Error),
/// Missing image
#[error("The image with the requested name is missing")]
Missing,
/// Unsupported Extension
#[error("The extension is unsupported")]
Extension,
}
#[derive(Clone)]
/// The frames of a decoded gif
pub struct Frames {
first: Frame,
frames: Vec<Frame>,
total_bytes: u64,
}
impl fmt::Debug for Frames {
#[cold]
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Frames").finish()
}
}
impl Frames {
/// Load [`Frames`] from the supplied name
pub fn load_from_name(
name: &str,
size: u16,
theme: Option<&str>,
default_fallbacks: bool,
) -> Task<Result<Frames, Error>> {
let mut name_path_buffer = None;
if let Some(path) = icon::Named::new(name).size(size).path() {
name_path_buffer = Some(path);
} else if default_fallbacks {
for name in name.rmatch_indices('-').map(|(pos, _)| &name[..pos]) {
if let Some(path) = icon::Named::new(name).size(size).path() {
name_path_buffer = Some(path);
break;
}
}
};
if let Some(name_path_buffer) = name_path_buffer {
Self::load_from_path(name_path_buffer)
} else {
Task::perform(async { Err(Error::Missing) }, std::convert::identity)
}
}
/// Load [`Frames`] from the supplied path
pub fn load_from_path(path: impl AsRef<Path>) -> Task<Result<Frames, Error>> {
#[inline(never)]
fn inner(path: &Path) -> Task<Result<Frames, Error>> {
#[cfg(feature = "tokio")]
use tokio::fs::File;
#[cfg(feature = "tokio")]
use tokio::io::BufReader;
#[cfg(not(feature = "tokio"))]
use async_fs::File;
#[cfg(not(feature = "tokio"))]
use iced_futures::futures::io::BufReader;
let path = path.to_path_buf();
let f = async move {
let image_type = match &path.extension() {
Some(ext) if ext == &OsStr::new("gif") => ImageType::Gif,
Some(ext) if ext == &OsStr::new("apng") => ImageType::Apng,
Some(ext) if ext == &OsStr::new("webp") => ImageType::WebP,
_ => return Err(Error::Extension),
};
let reader = BufReader::new(File::open(path).await?);
Frames::from_reader(reader, image_type).await
};
Task::perform(f, std::convert::identity)
}
inner(path.as_ref())
}
/// Decode [`Frames`] from the supplied async reader
/// # Errors
/// If the type of image is not supported this function will error. IO errors may also occur.
pub async fn from_reader<R: AsyncRead>(
reader: R,
image_type: ImageType,
) -> Result<Self, Error> {
use iced_futures::futures::pin_mut;
pin_mut!(reader);
let mut bytes = vec![];
reader.read_to_end(&mut bytes).await?;
match image_type {
ImageType::Gif => Self::from_decoder(GifDecoder::new(io::Cursor::new(bytes))?),
ImageType::Apng => Self::from_decoder(PngDecoder::new(io::Cursor::new(bytes))?.apng()?),
ImageType::WebP => Self::from_decoder(WebPDecoder::new(io::Cursor::new(bytes))?),
}
}
/// Decode [`Frames`] from the supplied bytes
/// # Errors
///
/// IO errors may occur.
///
/// # Panics
///
/// If there are no frames in the image, this panics.
pub fn from_decoder<'a, T: AnimationDecoder<'a>>(decoder: T) -> Result<Self, Error> {
let frames = decoder
.into_frames()
.map(|result| result.map(Frame::from))
.collect::<Result<Vec<_>, _>>()?;
let first = frames.first().cloned().unwrap();
let total_bytes = frames
.iter()
.map(|f| match &f.handle {
Handle::Path(..) => 0,
Handle::Bytes(_, b) => b.len(),
Handle::Rgba { pixels, .. } => pixels.len(),
})
.sum::<usize>()
.try_into()
.unwrap_or_default();
Ok(Frames {
first,
frames,
total_bytes,
})
}
}
#[derive(Clone)]
struct Frame {
delay: Duration,
handle: image::Handle,
}
impl From<image_rs::Frame> for Frame {
fn from(frame: image_rs::Frame) -> Self {
let (width, height) = frame.buffer().dimensions();
let delay = frame.delay().into();
let handle = image::Handle::from_rgba(width, height, frame.into_buffer().into_vec());
Self { delay, handle }
}
}
struct State {
index: usize,
current: Current,
total_bytes: u64,
}
struct Current {
frame: Frame,
started: Instant,
}
impl From<Frame> for Current {
fn from(frame: Frame) -> Self {
Self {
started: Instant::now(),
frame,
}
}
}
/// A frame that displays an animated image while keeping aspect ratio
#[derive(Debug)]
pub struct AnimatedImage<'a> {
frames: &'a Frames,
width: Length,
height: Length,
content_fit: ContentFit,
}
pub enum ImageType {
Gif,
Apng,
WebP,
}
impl<'a> AnimatedImage<'a> {
#[must_use]
/// Creates a new [`AnimatedImage`] with the given [`Frames`]
pub fn new(frames: &'a Frames) -> Self {
AnimatedImage {
frames,
width: Length::Shrink,
height: Length::Shrink,
content_fit: ContentFit::Contain,
}
}
#[must_use]
/// Sets the width of the [`AnimatedImage`] boundaries.
pub fn width(mut self, width: Length) -> Self {
self.width = width;
self
}
#[must_use]
/// Sets the height of the [`AnimatedImage`] boundaries.
pub fn height(mut self, height: Length) -> Self {
self.height = height;
self
}
#[must_use]
/// Sets the [`ContentFit`] of the [`AnimatedImage`].
///
/// Defaults to [`ContentFit::Contain`]
pub fn content_fit(self, content_fit: ContentFit) -> Self {
Self {
content_fit,
..self
}
}
}
impl<'a, Message, Renderer> Widget<Message, crate::Theme, Renderer> for AnimatedImage<'a>
where
Renderer: ImageRenderer<Handle = Handle>,
{
fn size(&self) -> Size<Length> {
Size::new(self.width.into(), self.height.into())
}
fn tag(&self) -> tree::Tag {
tree::Tag::of::<State>()
}
fn state(&self) -> tree::State {
tree::State::new(State {
index: 0,
current: self.frames.first.clone().into(),
total_bytes: self.frames.total_bytes,
})
}
fn diff(&mut self, tree: &mut Tree) {
let state = tree.state.downcast_mut::<State>();
// Reset state if new gif Frames is used w/
// same state tree.
//
// Total bytes of the gif should be a good enough
// proxy for it changing.
if state.total_bytes != self.frames.total_bytes {
*state = State {
index: 0,
current: self.frames.first.clone().into(),
total_bytes: self.frames.total_bytes,
};
}
}
fn layout(
&mut self,
tree: &mut Tree,
renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
iced_widget::image::layout(
renderer,
limits,
&self.frames.first.handle,
self.width,
self.height,
None,
self.content_fit,
Rotation::default(),
false,
[0.0; 4],
)
}
fn update(
&mut self,
tree: &mut Tree,
event: &Event,
layout: Layout<'_>,
cursor_position: mouse::Cursor,
renderer: &Renderer,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
viewport: &Rectangle,
) {
let state = tree.state.downcast_mut::<State>();
if let Event::Window(window::Event::RedrawRequested(now)) = event {
let elapsed = now.duration_since(state.current.started);
if elapsed > state.current.frame.delay {
state.index = (state.index + 1) % self.frames.frames.len();
state.current = self.frames.frames[state.index].clone().into();
shell
.request_redraw_at(window::RedrawRequest::At(*now + state.current.frame.delay));
} else {
let remaining = state.current.frame.delay - elapsed;
shell.request_redraw_at(window::RedrawRequest::At(*now + remaining));
}
}
}
fn draw(
&self,
tree: &Tree,
renderer: &mut Renderer,
_theme: &crate::Theme,
_style: &renderer::Style,
layout: Layout<'_>,
_cursor_position: Cursor,
_viewport: &Rectangle,
) {
let state = tree.state.downcast_ref::<State>();
iced_widget::image::draw(
renderer,
layout,
&state.current.frame.handle,
None,
iced_core::border::Radius::default(),
self.content_fit,
FilterMethod::default(),
Rotation::default(),
1.0,
1.0,
);
}
}
impl<'a, Message, Renderer> From<AnimatedImage<'a>> for Element<'a, Message, crate::Theme, Renderer>
where
Renderer: ImageRenderer<Handle = Handle> + 'a,
{
fn from(gif: AnimatedImage<'a>) -> Element<'a, Message, crate::Theme, Renderer> {
Element::new(gif)
}
}