diff options
Diffstat (limited to 'examples/gallery/src/main.rs')
-rw-r--r-- | examples/gallery/src/main.rs | 308 |
1 files changed, 308 insertions, 0 deletions
diff --git a/examples/gallery/src/main.rs b/examples/gallery/src/main.rs new file mode 100644 index 00000000..a7c107f7 --- /dev/null +++ b/examples/gallery/src/main.rs @@ -0,0 +1,308 @@ +//! A simple gallery that displays the daily featured images of Civitai. +//! +//! Showcases lazy loading of images in the background, as well as +//! some smooth animations. +mod civitai; + +use crate::civitai::{Error, Id, Image, Rgba, Size}; + +use iced::animation; +use iced::time::Instant; +use iced::widget::{ + button, center_x, container, horizontal_space, image, mouse_area, opaque, + pop, row, scrollable, stack, +}; +use iced::window; +use iced::{ + color, Animation, ContentFit, Element, Fill, Subscription, Task, Theme, +}; + +use std::collections::HashMap; + +fn main() -> iced::Result { + iced::application("Gallery - Iced", Gallery::update, Gallery::view) + .subscription(Gallery::subscription) + .theme(Gallery::theme) + .run_with(Gallery::new) +} + +struct Gallery { + images: Vec<Image>, + thumbnails: HashMap<Id, Thumbnail>, + viewer: Viewer, + now: Instant, +} + +#[derive(Debug, Clone)] +enum Message { + ImagesListed(Result<Vec<Image>, Error>), + ImagePoppedIn(Id), + ImageDownloaded(Result<Rgba, Error>), + ThumbnailDownloaded(Id, Result<Rgba, Error>), + ThumbnailHovered(Id, bool), + Open(Id), + Close, + Animate(Instant), +} + +impl Gallery { + pub fn new() -> (Self, Task<Message>) { + ( + Self { + images: Vec::new(), + thumbnails: HashMap::new(), + viewer: Viewer::new(), + now: Instant::now(), + }, + Task::perform(Image::list(), Message::ImagesListed), + ) + } + + pub fn theme(&self) -> Theme { + Theme::TokyoNight + } + + pub fn subscription(&self) -> Subscription<Message> { + let is_animating = self + .thumbnails + .values() + .any(|thumbnail| thumbnail.is_animating(self.now)) + || self.viewer.is_animating(self.now); + + if is_animating { + window::frames().map(Message::Animate) + } else { + Subscription::none() + } + } + + pub fn update(&mut self, message: Message) -> Task<Message> { + match message { + Message::ImagesListed(Ok(images)) => { + self.images = images; + + Task::none() + } + Message::ImagePoppedIn(id) => { + let Some(image) = self + .images + .iter() + .find(|candidate| candidate.id == id) + .cloned() + else { + return Task::none(); + }; + + Task::perform(image.download(Size::Thumbnail), move |result| { + Message::ThumbnailDownloaded(id, result) + }) + } + Message::ImageDownloaded(Ok(rgba)) => { + self.viewer.show(rgba); + + Task::none() + } + Message::ThumbnailDownloaded(id, Ok(rgba)) => { + let thumbnail = Thumbnail { + handle: image::Handle::from_rgba( + rgba.width, + rgba.height, + rgba.pixels, + ), + fade_in: Animation::new(false).slow().go(true), + zoom: Animation::new(false) + .quick() + .easing(animation::Easing::EaseInOut), + }; + + let _ = self.thumbnails.insert(id, thumbnail); + + Task::none() + } + Message::ThumbnailHovered(id, is_hovered) => { + if let Some(thumbnail) = self.thumbnails.get_mut(&id) { + thumbnail.zoom.go_mut(is_hovered); + } + + Task::none() + } + Message::Open(id) => { + let Some(image) = self + .images + .iter() + .find(|candidate| candidate.id == id) + .cloned() + else { + return Task::none(); + }; + + self.viewer.open(); + + Task::perform( + image.download(Size::Original), + Message::ImageDownloaded, + ) + } + Message::Close => { + self.viewer.close(); + + Task::none() + } + Message::Animate(now) => { + self.now = now; + + Task::none() + } + Message::ImagesListed(Err(error)) + | Message::ImageDownloaded(Err(error)) + | Message::ThumbnailDownloaded(_, Err(error)) => { + dbg!(error); + + Task::none() + } + } + } + + pub fn view(&self) -> Element<'_, Message> { + let gallery = row(self.images.iter().map(|image| { + card(image, self.thumbnails.get(&image.id), self.now) + })) + .spacing(10) + .wrap(); + + let content = + container(scrollable(center_x(gallery)).spacing(10)).padding(10); + + let viewer = self.viewer.view(self.now); + + stack![content, viewer].into() + } +} + +struct Thumbnail { + handle: image::Handle, + fade_in: Animation<bool>, + zoom: Animation<bool>, +} + +impl Thumbnail { + fn is_animating(&self, now: Instant) -> bool { + self.fade_in.in_progress(now) || self.zoom.in_progress(now) + } +} + +fn card<'a>( + metadata: &'a Image, + thumbnail: Option<&'a Thumbnail>, + now: Instant, +) -> Element<'a, Message> { + let image: Element<'_, _> = if let Some(thumbnail) = thumbnail { + image(&thumbnail.handle) + .width(Fill) + .height(Fill) + .content_fit(ContentFit::Cover) + .opacity(thumbnail.fade_in.interpolate(0.0, 1.0, now)) + .scale(thumbnail.zoom.interpolate(1.0, 1.1, now)) + .into() + } else { + horizontal_space().into() + }; + + let card = mouse_area( + container(image) + .width(320) + .height(410) + .style(container::dark), + ) + .on_enter(Message::ThumbnailHovered(metadata.id, true)) + .on_exit(Message::ThumbnailHovered(metadata.id, false)); + + if thumbnail.is_some() { + button(card) + .on_press(Message::Open(metadata.id)) + .padding(0) + .style(button::text) + .into() + } else { + pop(card) + .on_show(Message::ImagePoppedIn(metadata.id)) + .into() + } +} + +struct Viewer { + image: Option<image::Handle>, + background_fade_in: Animation<bool>, + image_fade_in: Animation<bool>, +} + +impl Viewer { + fn new() -> Self { + Self { + image: None, + background_fade_in: Animation::new(false) + .very_slow() + .easing(animation::Easing::EaseInOut), + image_fade_in: Animation::new(false) + .very_slow() + .easing(animation::Easing::EaseInOut), + } + } + + fn open(&mut self) { + self.image = None; + self.background_fade_in.go_mut(true); + } + + fn show(&mut self, rgba: Rgba) { + self.image = Some(image::Handle::from_rgba( + rgba.width, + rgba.height, + rgba.pixels, + )); + self.background_fade_in.go_mut(true); + self.image_fade_in.go_mut(true); + } + + fn close(&mut self) { + self.background_fade_in.go_mut(false); + self.image_fade_in.go_mut(false); + } + + fn is_animating(&self, now: Instant) -> bool { + self.background_fade_in.in_progress(now) + || self.image_fade_in.in_progress(now) + } + + fn view(&self, now: Instant) -> Element<'_, Message> { + let opacity = self.background_fade_in.interpolate(0.0, 0.8, now); + + let image: Element<'_, _> = if let Some(handle) = &self.image { + image(handle) + .width(Fill) + .height(Fill) + .opacity(self.image_fade_in.interpolate(0.0, 1.0, now)) + .scale(self.image_fade_in.interpolate(1.5, 1.0, now)) + .into() + } else { + horizontal_space().into() + }; + + if opacity > 0.0 { + opaque( + mouse_area( + container(image) + .center(Fill) + .style(move |_theme| { + container::Style::default() + .background(color!(0x000000, opacity)) + }) + .padding(20), + ) + .on_press(Message::Close), + ) + } else { + horizontal_space().into() + } + } +} |