diff options
Diffstat (limited to '')
| -rw-r--r-- | examples/gallery/Cargo.toml | 2 | ||||
| -rw-r--r-- | examples/gallery/src/civitai.rs | 33 | ||||
| -rw-r--r-- | examples/gallery/src/main.rs | 192 | ||||
| -rw-r--r-- | examples/scrollable/src/main.rs | 12 | ||||
| -rw-r--r-- | examples/tour/src/main.rs | 14 | 
5 files changed, 199 insertions, 54 deletions
| diff --git a/examples/gallery/Cargo.toml b/examples/gallery/Cargo.toml index 573389b1..c9dc1e9d 100644 --- a/examples/gallery/Cargo.toml +++ b/examples/gallery/Cargo.toml @@ -19,5 +19,7 @@ bytes.workspace = true  image.workspace = true  tokio.workspace = true +blurhash = "0.2.3" +  [lints]  workspace = true diff --git a/examples/gallery/src/civitai.rs b/examples/gallery/src/civitai.rs index 986b6bf2..18d2a040 100644 --- a/examples/gallery/src/civitai.rs +++ b/examples/gallery/src/civitai.rs @@ -10,6 +10,7 @@ use std::sync::Arc;  pub struct Image {      pub id: Id,      url: String, +    hash: String,  }  impl Image { @@ -40,20 +41,37 @@ impl Image {          Ok(response.items)      } +    pub async fn blurhash( +        self, +        width: u32, +        height: u32, +    ) -> Result<Rgba, Error> { +        task::spawn_blocking(move || { +            let pixels = blurhash::decode(&self.hash, width, height, 1.0)?; + +            Ok::<_, Error>(Rgba { +                width, +                height, +                pixels: Bytes::from(pixels), +            }) +        }) +        .await? +    } +      pub async fn download(self, size: Size) -> Result<Rgba, Error> {          let client = reqwest::Client::new();          let bytes = client              .get(match size {                  Size::Original => self.url, -                Size::Thumbnail => self +                Size::Thumbnail { width } => self                      .url                      .split("/")                      .map(|part| {                          if part.starts_with("width=") { -                            "width=640" +                            format!("width={}", width * 2) // High DPI                          } else { -                            part +                            part.to_owned()                          }                      })                      .collect::<Vec<_>>() @@ -107,7 +125,7 @@ impl fmt::Debug for Rgba {  #[derive(Debug, Clone, Copy)]  pub enum Size {      Original, -    Thumbnail, +    Thumbnail { width: u32 },  }  #[derive(Debug, Clone)] @@ -117,6 +135,7 @@ pub enum Error {      IOFailed(Arc<io::Error>),      JoinFailed(Arc<task::JoinError>),      ImageDecodingFailed(Arc<image::ImageError>), +    BlurhashDecodingFailed(Arc<blurhash::Error>),  }  impl From<reqwest::Error> for Error { @@ -142,3 +161,9 @@ impl From<image::ImageError> for Error {          Self::ImageDecodingFailed(Arc::new(error))      }  } + +impl From<blurhash::Error> for Error { +    fn from(error: blurhash::Error) -> Self { +        Self::BlurhashDecodingFailed(Arc::new(error)) +    } +} diff --git a/examples/gallery/src/main.rs b/examples/gallery/src/main.rs index 290fa6a0..ab22679d 100644 --- a/examples/gallery/src/main.rs +++ b/examples/gallery/src/main.rs @@ -7,7 +7,7 @@ mod civitai;  use crate::civitai::{Error, Id, Image, Rgba, Size};  use iced::animation; -use iced::time::Instant; +use iced::time::{milliseconds, Instant};  use iced::widget::{      button, center_x, container, horizontal_space, image, mouse_area, opaque,      pop, row, scrollable, stack, @@ -28,7 +28,7 @@ fn main() -> iced::Result {  struct Gallery {      images: Vec<Image>, -    thumbnails: HashMap<Id, Thumbnail>, +    previews: HashMap<Id, Preview>,      viewer: Viewer,      now: Instant,  } @@ -40,6 +40,7 @@ enum Message {      ImageDownloaded(Result<Rgba, Error>),      ThumbnailDownloaded(Id, Result<Rgba, Error>),      ThumbnailHovered(Id, bool), +    BlurhashDecoded(Id, Result<Rgba, Error>),      Open(Id),      Close,      Animate(Instant), @@ -50,7 +51,7 @@ impl Gallery {          (              Self {                  images: Vec::new(), -                thumbnails: HashMap::new(), +                previews: HashMap::new(),                  viewer: Viewer::new(),                  now: Instant::now(),              }, @@ -64,9 +65,9 @@ impl Gallery {      pub fn subscription(&self) -> Subscription<Message> {          let is_animating = self -            .thumbnails +            .previews              .values() -            .any(|thumbnail| thumbnail.is_animating(self.now)) +            .any(|preview| preview.is_animating(self.now))              || self.viewer.is_animating(self.now);          if is_animating { @@ -93,9 +94,18 @@ impl Gallery {                      return Task::none();                  }; -                Task::perform(image.download(Size::Thumbnail), move |result| { -                    Message::ThumbnailDownloaded(id, result) -                }) +                Task::batch(vec![ +                    Task::perform( +                        image.clone().blurhash(Preview::WIDTH, Preview::HEIGHT), +                        move |result| Message::BlurhashDecoded(id, result), +                    ), +                    Task::perform( +                        image.download(Size::Thumbnail { +                            width: Preview::WIDTH, +                        }), +                        move |result| Message::ThumbnailDownloaded(id, result), +                    ), +                ])              }              Message::ImageDownloaded(Ok(rgba)) => {                  self.viewer.show(rgba); @@ -103,14 +113,27 @@ impl Gallery {                  Task::none()              }              Message::ThumbnailDownloaded(id, Ok(rgba)) => { -                let thumbnail = Thumbnail::new(rgba); -                let _ = self.thumbnails.insert(id, thumbnail); +                let thumbnail = if let Some(preview) = self.previews.remove(&id) +                { +                    preview.load(rgba) +                } else { +                    Preview::ready(rgba) +                }; + +                let _ = self.previews.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); +                if let Some(preview) = self.previews.get_mut(&id) { +                    preview.toggle_zoom(is_hovered); +                } + +                Task::none() +            } +            Message::BlurhashDecoded(id, Ok(rgba)) => { +                if !self.previews.contains_key(&id) { +                    let _ = self.previews.insert(id, Preview::loading(rgba));                  }                  Task::none() @@ -144,7 +167,8 @@ impl Gallery {              }              Message::ImagesListed(Err(error))              | Message::ImageDownloaded(Err(error)) -            | Message::ThumbnailDownloaded(_, Err(error)) => { +            | Message::ThumbnailDownloaded(_, Err(error)) +            | Message::BlurhashDecoded(_, Err(error)) => {                  dbg!(error);                  Task::none() @@ -157,7 +181,7 @@ impl Gallery {              row((0..=Image::LIMIT).map(|_| placeholder()))          } else {              row(self.images.iter().map(|image| { -                card(image, self.thumbnails.get(&image.id), self.now) +                card(image, self.previews.get(&image.id), self.now)              }))          }          .spacing(10) @@ -174,33 +198,52 @@ impl Gallery {  fn card<'a>(      metadata: &'a Image, -    thumbnail: Option<&'a Thumbnail>, +    preview: Option<&'a Preview>,      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() +    let image = if let Some(preview) = preview { +        let thumbnail: Element<'_, _> = +            if let Preview::Ready { thumbnail, .. } = &preview { +                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() +            }; + +        if let Some(blurhash) = preview.blurhash(now) { +            let blurhash = image(&blurhash.handle) +                .width(Fill) +                .height(Fill) +                .content_fit(ContentFit::Cover) +                .opacity(blurhash.fade_in.interpolate(0.0, 1.0, now)); + +            stack![blurhash, thumbnail].into() +        } else { +            thumbnail +        }      } else {          horizontal_space().into()      };      let card = mouse_area(          container(image) -            .width(Thumbnail::WIDTH) -            .height(Thumbnail::HEIGHT) +            .width(Preview::WIDTH) +            .height(Preview::HEIGHT)              .style(container::dark),      )      .on_enter(Message::ThumbnailHovered(metadata.id, true))      .on_exit(Message::ThumbnailHovered(metadata.id, false)); -    if thumbnail.is_some() { +    if let Some(preview) = preview { +        let is_thumbnail = matches!(preview, Preview::Ready { .. }); +          button(card) -            .on_press(Message::Open(metadata.id)) +            .on_press_maybe(is_thumbnail.then_some(Message::Open(metadata.id)))              .padding(0)              .style(button::text)              .into() @@ -213,23 +256,102 @@ fn card<'a>(  fn placeholder<'a>() -> Element<'a, Message> {      container(horizontal_space()) -        .width(Thumbnail::WIDTH) -        .height(Thumbnail::HEIGHT) +        .width(Preview::WIDTH) +        .height(Preview::HEIGHT)          .style(container::dark)          .into()  } +enum Preview { +    Loading { +        blurhash: Blurhash, +    }, +    Ready { +        blurhash: Option<Blurhash>, +        thumbnail: Thumbnail, +    }, +} + +struct Blurhash { +    handle: image::Handle, +    fade_in: Animation<bool>, +} +  struct Thumbnail {      handle: image::Handle,      fade_in: Animation<bool>,      zoom: Animation<bool>,  } -impl Thumbnail { -    const WIDTH: u16 = 320; -    const HEIGHT: u16 = 410; +impl Preview { +    const WIDTH: u32 = 320; +    const HEIGHT: u32 = 410; + +    fn loading(rgba: Rgba) -> Self { +        Self::Loading { +            blurhash: Blurhash { +                fade_in: Animation::new(false) +                    .duration(milliseconds(700)) +                    .easing(animation::Easing::EaseIn) +                    .go(true), +                handle: image::Handle::from_rgba( +                    rgba.width, +                    rgba.height, +                    rgba.pixels, +                ), +            }, +        } +    } -    fn new(rgba: Rgba) -> Self { +    fn ready(rgba: Rgba) -> Self { +        Self::Ready { +            blurhash: None, +            thumbnail: Thumbnail::new(rgba), +        } +    } + +    fn load(self, rgba: Rgba) -> Self { +        let Self::Loading { blurhash } = self else { +            return self; +        }; + +        Self::Ready { +            blurhash: Some(blurhash), +            thumbnail: Thumbnail::new(rgba), +        } +    } + +    fn toggle_zoom(&mut self, enabled: bool) { +        if let Self::Ready { thumbnail, .. } = self { +            thumbnail.zoom.go_mut(enabled); +        } +    } + +    fn is_animating(&self, now: Instant) -> bool { +        match &self { +            Self::Loading { blurhash } => blurhash.fade_in.is_animating(now), +            Self::Ready { thumbnail, .. } => { +                thumbnail.fade_in.is_animating(now) +                    || thumbnail.zoom.is_animating(now) +            } +        } +    } + +    fn blurhash(&self, now: Instant) -> Option<&Blurhash> { +        match self { +            Self::Loading { blurhash, .. } => Some(blurhash), +            Self::Ready { +                blurhash: Some(blurhash), +                thumbnail, +                .. +            } if thumbnail.fade_in.is_animating(now) => Some(blurhash), +            Self::Ready { .. } => None, +        } +    } +} + +impl Thumbnail { +    pub fn new(rgba: Rgba) -> Self {          Self {              handle: image::Handle::from_rgba(                  rgba.width, @@ -242,10 +364,6 @@ impl Thumbnail {                  .easing(animation::Easing::EaseInOut),          }      } - -    fn is_animating(&self, now: Instant) -> bool { -        self.fade_in.is_animating(now) || self.zoom.is_animating(now) -    }  }  struct Viewer { diff --git a/examples/scrollable/src/main.rs b/examples/scrollable/src/main.rs index 6359fb5a..fec4e1b4 100644 --- a/examples/scrollable/src/main.rs +++ b/examples/scrollable/src/main.rs @@ -21,9 +21,9 @@ pub fn main() -> iced::Result {  struct ScrollableDemo {      scrollable_direction: Direction, -    scrollbar_width: u16, -    scrollbar_margin: u16, -    scroller_width: u16, +    scrollbar_width: u32, +    scrollbar_margin: u32, +    scroller_width: u32,      current_scroll_offset: scrollable::RelativeOffset,      anchor: scrollable::Anchor,  } @@ -39,9 +39,9 @@ enum Direction {  enum Message {      SwitchDirection(Direction),      AlignmentChanged(scrollable::Anchor), -    ScrollbarWidthChanged(u16), -    ScrollbarMarginChanged(u16), -    ScrollerWidthChanged(u16), +    ScrollbarWidthChanged(u32), +    ScrollbarMarginChanged(u32), +    ScrollerWidthChanged(u32),      ScrollToBeginning,      ScrollToEnd,      Scrolled(scrollable::Viewport), diff --git a/examples/tour/src/main.rs b/examples/tour/src/main.rs index 32720c47..2ca1df44 100644 --- a/examples/tour/src/main.rs +++ b/examples/tour/src/main.rs @@ -24,12 +24,12 @@ pub struct Tour {      screen: Screen,      slider: u8,      layout: Layout, -    spacing: u16, -    text_size: u16, +    spacing: u32, +    text_size: u32,      text_color: Color,      language: Option<Language>,      toggler: bool, -    image_width: u16, +    image_width: u32,      image_filter_method: image::FilterMethod,      input_value: String,      input_is_secure: bool, @@ -43,11 +43,11 @@ pub enum Message {      NextPressed,      SliderChanged(u8),      LayoutChanged(Layout), -    SpacingChanged(u16), -    TextSizeChanged(u16), +    SpacingChanged(u32), +    TextSizeChanged(u32),      TextColorChanged(Color),      LanguageSelected(Language), -    ImageWidthChanged(u16), +    ImageWidthChanged(u32),      ImageUseNearestToggled(bool),      InputChanged(String),      ToggleSecureInput(bool), @@ -537,7 +537,7 @@ impl Screen {  }  fn ferris<'a>( -    width: u16, +    width: u32,      filter_method: image::FilterMethod,  ) -> Container<'a, Message> {      center_x( | 
