From 0c528be2ea74f9aae1d4ac80b282ba9c16674649 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Tue, 11 Feb 2025 03:39:42 +0100 Subject: [PATCH] Introduce `with` helper and use `sipper` in `gallery` example --- Cargo.lock | 1 + core/src/lib.rs | 60 ++++++++++++++ examples/download_progress/src/main.rs | 4 +- examples/gallery/Cargo.toml | 1 + examples/gallery/src/civitai.rs | 106 +++++++++++++++---------- examples/gallery/src/main.rs | 30 +++---- runtime/src/task.rs | 46 +---------- src/lib.rs | 2 +- 8 files changed, 144 insertions(+), 106 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a86070dbdd..5c015e2a7a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1875,6 +1875,7 @@ dependencies = [ "image", "reqwest", "serde", + "sipper", "tokio", ] diff --git a/core/src/lib.rs b/core/src/lib.rs index d5c221ac3a..ac0a228f87 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -93,3 +93,63 @@ pub use smol_str::SmolStr; pub fn never(never: std::convert::Infallible) -> T { match never {} } + +/// Applies the given prefix value to the provided closure and returns +/// a new closure that takes the other argument. +/// +/// This lets you partially "apply" a function—equivalent to currying, +/// but it only works with binary functions. If you want to apply an +/// arbitrary number of arguments, use the [`with!`] macro instead. +/// +/// # When is this useful? +/// Sometimes you will want to identify the source or target +/// of some message in your user interface. This can be achieved through +/// normal means by defining a closure and moving the identifier +/// inside: +/// +/// ```rust +/// # let element: Option<()> = Some(()); +/// # enum Message { ButtonPressed(u32, ()) } +/// let id = 123; +/// +/// # let _ = { +/// element.map(move |result| Message::ButtonPressed(id, result)) +/// # }; +/// ``` +/// +/// That's quite a mouthful. [`with()`] lets you write: +/// +/// ```rust +/// # use iced_core::with; +/// # let element: Option<()> = Some(()); +/// # enum Message { ButtonPressed(u32, ()) } +/// let id = 123; +/// +/// # let _ = { +/// element.map(with(Message::ButtonPressed, id)) +/// # }; +/// ``` +/// +/// Effectively creating the same closure that partially applies +/// the `id` to the message—but much more concise! +pub fn with( + mut f: impl FnMut(T, R) -> O, + prefix: T, +) -> impl FnMut(R) -> O +where + T: Clone, +{ + move |result| f(prefix.clone(), result) +} + +/// Applies the given prefix values to the provided closure in the first +/// argument and returns a new closure that takes its last argument. +/// +/// This is variadic version of [`with()`] which works with any number of +/// arguments. +#[macro_export] +macro_rules! with { + ($f:expr, $($x:expr),+ $(,)?) => { + move |result| $f($($x),+, result) + }; +} diff --git a/examples/download_progress/src/main.rs b/examples/download_progress/src/main.rs index cc7a78006c..9b316f528e 100644 --- a/examples/download_progress/src/main.rs +++ b/examples/download_progress/src/main.rs @@ -4,7 +4,7 @@ use download::download; use iced::task; use iced::widget::{button, center, column, progress_bar, text, Column}; -use iced::{Center, Element, Right, Task}; +use iced::{with, Center, Element, Right, Task}; pub fn main() -> iced::Result { iced::application( @@ -52,7 +52,7 @@ impl Example { let task = download.start(); - task.map_with(index, Message::DownloadUpdated) + task.map(with(Message::DownloadUpdated, index)) } Message::DownloadUpdated(id, update) => { if let Some(download) = diff --git a/examples/gallery/Cargo.toml b/examples/gallery/Cargo.toml index c9dc1e9d44..6e8aba0645 100644 --- a/examples/gallery/Cargo.toml +++ b/examples/gallery/Cargo.toml @@ -17,6 +17,7 @@ serde.features = ["derive"] bytes.workspace = true image.workspace = true +sipper.workspace = true tokio.workspace = true blurhash = "0.2.3" diff --git a/examples/gallery/src/civitai.rs b/examples/gallery/src/civitai.rs index 18d2a04026..04589030fc 100644 --- a/examples/gallery/src/civitai.rs +++ b/examples/gallery/src/civitai.rs @@ -1,5 +1,6 @@ use bytes::Bytes; use serde::Deserialize; +use sipper::{sipper, Straw}; use tokio::task; use std::fmt; @@ -45,58 +46,72 @@ impl Image { self, width: u32, height: u32, - ) -> Result { + ) -> Result { task::spawn_blocking(move || { let pixels = blurhash::decode(&self.hash, width, height, 1.0)?; - Ok::<_, Error>(Rgba { - width, - height, - pixels: Bytes::from(pixels), + Ok::<_, Error>(Blurhash { + rgba: Rgba { + width, + height, + pixels: Bytes::from(pixels), + }, }) }) .await? } - pub async fn download(self, size: Size) -> Result { - let client = reqwest::Client::new(); - - let bytes = client - .get(match size { - Size::Original => self.url, - Size::Thumbnail { width } => self - .url - .split("/") - .map(|part| { - if part.starts_with("width=") { - format!("width={}", width * 2) // High DPI - } else { - part.to_owned() - } - }) - .collect::>() - .join("/"), + pub fn download(self, size: Size) -> impl Straw { + sipper(move |mut sender| async move { + let client = reqwest::Client::new(); + + if let Size::Thumbnail { width, height } = size { + let image = self.clone(); + + drop(task::spawn(async move { + if let Ok(blurhash) = image.blurhash(width, height).await { + sender.send(blurhash).await; + } + })); + } + + let bytes = client + .get(match size { + Size::Original => self.url, + Size::Thumbnail { width, .. } => self + .url + .split("/") + .map(|part| { + if part.starts_with("width=") { + format!("width={}", width * 2) // High DPI + } else { + part.to_owned() + } + }) + .collect::>() + .join("/"), + }) + .send() + .await? + .error_for_status()? + .bytes() + .await?; + + let image = task::spawn_blocking(move || { + Ok::<_, Error>( + image::ImageReader::new(io::Cursor::new(bytes)) + .with_guessed_format()? + .decode()? + .to_rgba8(), + ) }) - .send() - .await? - .error_for_status()? - .bytes() - .await?; + .await??; - let image = task::spawn_blocking(move || { - Ok::<_, Error>( - image::ImageReader::new(io::Cursor::new(bytes)) - .with_guessed_format()? - .decode()? - .to_rgba8(), - ) - }) - .await??; - - Ok(Rgba { - width: image.width(), - height: image.height(), - pixels: Bytes::from(image.into_raw()), + Ok(Rgba { + width: image.width(), + height: image.height(), + pixels: Bytes::from(image.into_raw()), + }) }) } } @@ -106,6 +121,11 @@ impl Image { )] pub struct Id(u32); +#[derive(Debug, Clone)] +pub struct Blurhash { + pub rgba: Rgba, +} + #[derive(Clone)] pub struct Rgba { pub width: u32, @@ -125,7 +145,7 @@ impl fmt::Debug for Rgba { #[derive(Debug, Clone, Copy)] pub enum Size { Original, - Thumbnail { width: u32 }, + Thumbnail { width: u32, height: u32 }, } #[derive(Debug, Clone)] diff --git a/examples/gallery/src/main.rs b/examples/gallery/src/main.rs index 441ad9245f..0ed2a8624e 100644 --- a/examples/gallery/src/main.rs +++ b/examples/gallery/src/main.rs @@ -14,7 +14,8 @@ use iced::widget::{ }; use iced::window; use iced::{ - color, Animation, ContentFit, Element, Fill, Subscription, Task, Theme, + color, with, Animation, ContentFit, Element, Fill, Subscription, Task, + Theme, }; use std::collections::HashMap; @@ -40,7 +41,7 @@ enum Message { ImageDownloaded(Result), ThumbnailDownloaded(Id, Result), ThumbnailHovered(Id, bool), - BlurhashDecoded(Id, Result), + BlurhashDecoded(Id, civitai::Blurhash), Open(Id), Close, Animate(Instant), @@ -94,16 +95,14 @@ impl Gallery { return Task::none(); }; - Task::batch([ - Task::future( - image.clone().blurhash(Preview::WIDTH, Preview::HEIGHT), - ) - .map_with(id, Message::BlurhashDecoded), - Task::future(image.download(Size::Thumbnail { + Task::sip( + image.download(Size::Thumbnail { width: Preview::WIDTH, - })) - .map_with(id, Message::ThumbnailDownloaded), - ]) + height: Preview::HEIGHT, + }), + with(Message::BlurhashDecoded, id), + with(Message::ThumbnailDownloaded, id), + ) } Message::ImageDownloaded(Ok(rgba)) => { self.viewer.show(rgba); @@ -129,9 +128,11 @@ impl Gallery { Task::none() } - Message::BlurhashDecoded(id, Ok(rgba)) => { + Message::BlurhashDecoded(id, blurhash) => { if !self.previews.contains_key(&id) { - let _ = self.previews.insert(id, Preview::loading(rgba)); + let _ = self + .previews + .insert(id, Preview::loading(blurhash.rgba)); } Task::none() @@ -165,8 +166,7 @@ impl Gallery { } Message::ImagesListed(Err(error)) | Message::ImageDownloaded(Err(error)) - | Message::ThumbnailDownloaded(_, Err(error)) - | Message::BlurhashDecoded(_, Err(error)) => { + | Message::ThumbnailDownloaded(_, Err(error)) => { dbg!(error); Task::none() diff --git a/runtime/src/task.rs b/runtime/src/task.rs index 1a1ef6990f..022483f7e1 100644 --- a/runtime/src/task.rs +++ b/runtime/src/task.rs @@ -63,7 +63,7 @@ impl Task { /// progress with the first closure and the output with the second one. pub fn sip( sipper: S, - on_progress: impl Fn(S::Progress) -> T + MaybeSend + 'static, + on_progress: impl FnMut(S::Progress) -> T + MaybeSend + 'static, on_output: impl FnOnce(::Output) -> T + MaybeSend + 'static, ) -> Self where @@ -98,50 +98,6 @@ impl Task { self.then(move |output| Task::done(f(output))) } - /// Combines a prefix value with the result of the [`Task`] using - /// the provided closure. - /// - /// Sometimes you will want to identify the source or target - /// of some [`Task`] in your UI. This can be achieved through - /// normal means by using [`map`]: - /// - /// ```rust - /// # use iced_runtime::Task; - /// # let task = Task::none(); - /// # enum Message { TaskCompleted(u32, ()) } - /// let id = 123; - /// - /// # let _ = { - /// task.map(move |result| Message::TaskCompleted(id, result)) - /// # }; - /// ``` - /// - /// Quite a mouthful. [`map_with`] lets you write: - /// - /// ```rust - /// # use iced_runtime::Task; - /// # let task = Task::none(); - /// # enum Message { TaskCompleted(u32, ()) } - /// # let id = 123; - /// # let _ = { - /// task.map_with(id, Message::TaskCompleted) - /// # }; - /// ``` - /// - /// Much nicer! - pub fn map_with( - self, - prefix: P, - mut f: impl FnMut(P, T) -> O + MaybeSend + 'static, - ) -> Task - where - T: MaybeSend + 'static, - P: MaybeSend + Clone + 'static, - O: MaybeSend + 'static, - { - self.map(move |result| f(prefix.clone(), result)) - } - /// Performs a new [`Task`] for every output of the current [`Task`] using the /// given closure. /// diff --git a/src/lib.rs b/src/lib.rs index e4649938af..ce4a1e9da0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -505,7 +505,7 @@ pub use crate::core::gradient; pub use crate::core::padding; pub use crate::core::theme; pub use crate::core::{ - never, Alignment, Animation, Background, Border, Color, ContentFit, + never, with, Alignment, Animation, Background, Border, Color, ContentFit, Degrees, Gradient, Length, Padding, Pixels, Point, Radians, Rectangle, Rotation, Settings, Shadow, Size, Theme, Transformation, Vector, };