diff options
27 files changed, 541 insertions, 226 deletions
@@ -665,6 +665,12 @@ dependencies = [ ] [[package]] +name = "blurhash" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e79769241dcd44edf79a732545e8b5cec84c247ac060f5252cd51885d093a8fc" + +[[package]] name = "built" version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1863,11 +1869,13 @@ dependencies = [ name = "gallery" version = "0.1.0" dependencies = [ + "blurhash", "bytes", "iced", "image", "reqwest", "serde", + "sipper", "tokio", ] @@ -2585,6 +2593,7 @@ dependencies = [ "iced_core", "iced_futures", "raw-window-handle 0.6.2", + "sipper", "thiserror 1.0.69", ] @@ -5221,6 +5230,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" [[package]] +name = "sipper" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bccb4192828b3d9a08e0b5a73f17795080dfb278b50190216e3ae2132cf4f95" +dependencies = [ + "futures", + "pin-project-lite", +] + +[[package]] name = "skrifa" version = "0.22.3" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -172,6 +172,7 @@ raw-window-handle = "0.6" resvg = "0.42" rustc-hash = "2.0" sha2 = "0.10" +sipper = "0.1" smol = "1.0" smol_str = "0.2" softbuffer = "0.4" @@ -200,6 +201,7 @@ unused_results = "deny" [workspace.lints.clippy] type-complexity = "allow" +map-entry = "allow" semicolon_if_nothing_returned = "deny" trivially-copy-pass-by-ref = "deny" default_trait_access = "deny" diff --git a/core/src/animation.rs b/core/src/animation.rs index 258fd084..14cbb5c3 100644 --- a/core/src/animation.rs +++ b/core/src/animation.rs @@ -13,6 +13,7 @@ where T: Clone + Copy + PartialEq + Float, { raw: lilt::Animated<T, Instant>, + duration: Duration, // TODO: Expose duration getter in `lilt` } impl<T> Animation<T> @@ -23,6 +24,7 @@ where pub fn new(state: T) -> Self { Self { raw: lilt::Animated::new(state), + duration: Duration::from_millis(100), } } @@ -58,6 +60,7 @@ where /// Sets the duration of the [`Animation`] to the given value. pub fn duration(mut self, duration: Duration) -> Self { self.raw = self.raw.duration(duration.as_secs_f32() * 1_000.0); + self.duration = duration; self } @@ -133,4 +136,13 @@ impl Animation<bool> { { self.raw.animate_bool(start, end, at) } + + /// Returns the remaining [`Duration`] of the [`Animation`]. + pub fn remaining(&self, at: Instant) -> Duration { + Duration::from_secs_f32(self.interpolate( + self.duration.as_secs_f32(), + 0.0, + at, + )) + } } diff --git a/core/src/element.rs b/core/src/element.rs index ede9e16c..b7d51aeb 100644 --- a/core/src/element.rs +++ b/core/src/element.rs @@ -93,6 +93,7 @@ impl<'a, Message, Theme, Renderer> Element<'a, Message, Theme, Renderer> { /// /// ```no_run /// # mod iced { + /// # pub use iced_core::Function; /// # pub type Element<'a, Message> = iced_core::Element<'a, Message, iced_core::Theme, ()>; /// # /// # pub mod widget { @@ -119,7 +120,7 @@ impl<'a, Message, Theme, Renderer> Element<'a, Message, Theme, Renderer> { /// use counter::Counter; /// /// use iced::widget::row; - /// use iced::Element; + /// use iced::{Element, Function}; /// /// struct ManyCounters { /// counters: Vec<Counter>, @@ -142,7 +143,7 @@ impl<'a, Message, Theme, Renderer> Element<'a, Message, Theme, Renderer> { /// // Here we turn our `Element<counter::Message>` into /// // an `Element<Message>` by combining the `index` and the /// // message of the `element`. - /// counter.map(move |message| Message::Counter(index, message)) + /// counter.map(Message::Counter.with(index)) /// }), /// ) /// .into() diff --git a/core/src/length.rs b/core/src/length.rs index 5f24169f..363833c4 100644 --- a/core/src/length.rs +++ b/core/src/length.rs @@ -77,8 +77,8 @@ impl From<f32> for Length { } } -impl From<u16> for Length { - fn from(units: u16) -> Self { - Length::Fixed(f32::from(units)) +impl From<u32> for Length { + fn from(units: u32) -> Self { + Length::Fixed(units as f32) } } diff --git a/core/src/lib.rs b/core/src/lib.rs index d5c221ac..03cc0632 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -93,3 +93,60 @@ pub use smol_str::SmolStr; pub fn never<T>(never: std::convert::Infallible) -> T { match never {} } + +/// A trait extension for binary functions (`Fn(A, B) -> O`). +/// +/// It enables you to use a bunch of nifty functional programming paradigms +/// that work well with iced. +pub trait Function<A, B, O> { + /// Applies the given first argument to a binary function and returns + /// a new function 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, create a little struct for them. + /// + /// # 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`](Self::with) lets you write: + /// + /// ```rust + /// # use iced_core::Function; + /// # let element: Option<()> = Some(()); + /// # enum Message { ButtonPressed(u32, ()) } + /// let id = 123; + /// + /// # let _ = { + /// element.map(Message::ButtonPressed.with(id)) + /// # }; + /// ``` + /// + /// Effectively creating the same closure that partially applies + /// the `id` to the message—but much more concise! + fn with(self, prefix: A) -> impl Fn(B) -> O; +} + +impl<F, A, B, O> Function<A, B, O> for F +where + F: Fn(A, B) -> O, + Self: Sized, + A: Copy, +{ + fn with(self, prefix: A) -> impl Fn(B) -> O { + move |result| self(prefix, result) + } +} diff --git a/core/src/pixels.rs b/core/src/pixels.rs index 7d6267cf..c87e2b31 100644 --- a/core/src/pixels.rs +++ b/core/src/pixels.rs @@ -20,9 +20,9 @@ impl From<f32> for Pixels { } } -impl From<u16> for Pixels { - fn from(amount: u16) -> Self { - Self(f32::from(amount)) +impl From<u32> for Pixels { + fn from(amount: u32) -> Self { + Self(amount as f32) } } diff --git a/core/src/widget/operation/focusable.rs b/core/src/widget/operation/focusable.rs index 8f66e575..a1327bc1 100644 --- a/core/src/widget/operation/focusable.rs +++ b/core/src/widget/operation/focusable.rs @@ -61,6 +61,33 @@ pub fn focus<T>(target: Id) -> impl Operation<T> { Focus { target } } +/// Produces an [`Operation`] that unfocuses the focused widget. +pub fn unfocus<T>() -> impl Operation<T> { + struct Unfocus; + + impl<T> Operation<T> for Unfocus { + fn focusable( + &mut self, + _id: Option<&Id>, + _bounds: Rectangle, + state: &mut dyn Focusable, + ) { + state.unfocus(); + } + + fn container( + &mut self, + _id: Option<&Id>, + _bounds: Rectangle, + operate_on_children: &mut dyn FnMut(&mut dyn Operation<T>), + ) { + operate_on_children(self); + } + } + + Unfocus +} + /// Produces an [`Operation`] that generates a [`Count`] and chains it with the /// provided function to build a new [`Operation`]. pub fn count() -> impl Operation<Count> { diff --git a/examples/download_progress/src/download.rs b/examples/download_progress/src/download.rs index d63fb906..5b81f7a2 100644 --- a/examples/download_progress/src/download.rs +++ b/examples/download_progress/src/download.rs @@ -1,16 +1,14 @@ -use iced::futures::{SinkExt, Stream, StreamExt}; -use iced::stream::try_channel; +use iced::futures::StreamExt; +use iced::task::{sipper, Straw}; use std::sync::Arc; -pub fn download( - url: impl AsRef<str>, -) -> impl Stream<Item = Result<Progress, Error>> { - try_channel(1, move |mut output| async move { +pub fn download(url: impl AsRef<str>) -> impl Straw<(), Progress, Error> { + sipper(move |mut progress| async move { let response = reqwest::get(url.as_ref()).await?; let total = response.content_length().ok_or(Error::NoContentLength)?; - let _ = output.send(Progress::Downloading { percent: 0.0 }).await; + let _ = progress.send(Progress { percent: 0.0 }).await; let mut byte_stream = response.bytes_stream(); let mut downloaded = 0; @@ -19,23 +17,20 @@ pub fn download( let bytes = next_bytes?; downloaded += bytes.len(); - let _ = output - .send(Progress::Downloading { + let _ = progress + .send(Progress { percent: 100.0 * downloaded as f32 / total as f32, }) .await; } - let _ = output.send(Progress::Finished).await; - Ok(()) }) } #[derive(Debug, Clone)] -pub enum Progress { - Downloading { percent: f32 }, - Finished, +pub struct Progress { + pub percent: f32, } #[derive(Debug, Clone)] diff --git a/examples/download_progress/src/main.rs b/examples/download_progress/src/main.rs index f4b07203..8082eccd 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::{Center, Element, Function, Right, Task}; pub fn main() -> iced::Result { iced::application( @@ -25,7 +25,7 @@ struct Example { pub enum Message { Add, Download(usize), - DownloadProgressed(usize, Result<download::Progress, download::Error>), + DownloadUpdated(usize, Update), } impl Example { @@ -52,15 +52,13 @@ impl Example { let task = download.start(); - task.map(move |progress| { - Message::DownloadProgressed(index, progress) - }) + task.map(Message::DownloadUpdated.with(index)) } - Message::DownloadProgressed(id, progress) => { + Message::DownloadUpdated(id, update) => { if let Some(download) = self.downloads.iter_mut().find(|download| download.id == id) { - download.progress(progress); + download.update(update); } Task::none() @@ -95,6 +93,12 @@ struct Download { state: State, } +#[derive(Debug, Clone)] +pub enum Update { + Downloading(download::Progress), + Finished(Result<(), download::Error>), +} + #[derive(Debug)] enum State { Idle, @@ -111,18 +115,20 @@ impl Download { } } - pub fn start( - &mut self, - ) -> Task<Result<download::Progress, download::Error>> { + pub fn start(&mut self) -> Task<Update> { match self.state { State::Idle { .. } | State::Finished { .. } | State::Errored { .. } => { - let (task, handle) = Task::stream(download( - "https://huggingface.co/\ + let (task, handle) = Task::sip( + download( + "https://huggingface.co/\ mattshumer/Reflection-Llama-3.1-70B/\ resolve/main/model-00001-of-00162.safetensors", - )) + ), + Update::Downloading, + Update::Finished, + ) .abortable(); self.state = State::Downloading { @@ -136,20 +142,18 @@ impl Download { } } - pub fn progress( - &mut self, - new_progress: Result<download::Progress, download::Error>, - ) { + pub fn update(&mut self, update: Update) { if let State::Downloading { progress, .. } = &mut self.state { - match new_progress { - Ok(download::Progress::Downloading { percent }) => { - *progress = percent; - } - Ok(download::Progress::Finished) => { - self.state = State::Finished; + match update { + Update::Downloading(new_progress) => { + *progress = new_progress.percent; } - Err(_error) => { - self.state = State::Errored; + Update::Finished(result) => { + self.state = if result.is_ok() { + State::Finished + } else { + State::Errored + }; } } } diff --git a/examples/gallery/Cargo.toml b/examples/gallery/Cargo.toml index 573389b1..6e8aba06 100644 --- a/examples/gallery/Cargo.toml +++ b/examples/gallery/Cargo.toml @@ -17,7 +17,10 @@ serde.features = ["derive"] bytes.workspace = true image.workspace = true +sipper.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..04589030 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; @@ -10,6 +11,7 @@ use std::sync::Arc; pub struct Image { pub id: Id, url: String, + hash: String, } impl Image { @@ -40,45 +42,76 @@ impl Image { Ok(response.items) } - 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 - .url - .split("/") - .map(|part| { - if part.starts_with("width=") { - "width=640" - } else { - part - } - }) - .collect::<Vec<_>>() - .join("/"), + pub async fn blurhash( + self, + width: u32, + height: u32, + ) -> Result<Blurhash, Error> { + task::spawn_blocking(move || { + let pixels = blurhash::decode(&self.hash, width, height, 1.0)?; + + Ok::<_, Error>(Blurhash { + rgba: Rgba { + width, + height, + pixels: Bytes::from(pixels), + }, }) - .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(), - ) }) - .await??; + .await? + } - Ok(Rgba { - width: image.width(), - height: image.height(), - pixels: Bytes::from(image.into_raw()), + pub fn download(self, size: Size) -> impl Straw<Rgba, Blurhash, Error> { + 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::<Vec<_>>() + .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(), + ) + }) + .await??; + + Ok(Rgba { + width: image.width(), + height: image.height(), + pixels: Bytes::from(image.into_raw()), + }) }) } } @@ -88,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, @@ -107,7 +145,7 @@ impl fmt::Debug for Rgba { #[derive(Debug, Clone, Copy)] pub enum Size { Original, - Thumbnail, + Thumbnail { width: u32, height: u32 }, } #[derive(Debug, Clone)] @@ -117,6 +155,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 +181,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..abafaf2d 100644 --- a/examples/gallery/src/main.rs +++ b/examples/gallery/src/main.rs @@ -7,14 +7,15 @@ 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, }; use iced::window; use iced::{ - color, Animation, ContentFit, Element, Fill, Subscription, Task, Theme, + color, Animation, ContentFit, Element, Fill, Function, Subscription, Task, + Theme, }; use std::collections::HashMap; @@ -28,7 +29,7 @@ fn main() -> iced::Result { struct Gallery { images: Vec<Image>, - thumbnails: HashMap<Id, Thumbnail>, + previews: HashMap<Id, Preview>, viewer: Viewer, now: Instant, } @@ -40,6 +41,7 @@ enum Message { ImageDownloaded(Result<Rgba, Error>), ThumbnailDownloaded(Id, Result<Rgba, Error>), ThumbnailHovered(Id, bool), + BlurhashDecoded(Id, civitai::Blurhash), Open(Id), Close, Animate(Instant), @@ -50,7 +52,7 @@ impl Gallery { ( Self { images: Vec::new(), - thumbnails: HashMap::new(), + previews: HashMap::new(), viewer: Viewer::new(), now: Instant::now(), }, @@ -64,9 +66,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 +95,14 @@ impl Gallery { return Task::none(); }; - Task::perform(image.download(Size::Thumbnail), move |result| { - Message::ThumbnailDownloaded(id, result) - }) + Task::sip( + image.download(Size::Thumbnail { + width: Preview::WIDTH, + height: Preview::HEIGHT, + }), + Message::BlurhashDecoded.with(id), + Message::ThumbnailDownloaded.with(id), + ) } Message::ImageDownloaded(Ok(rgba)) => { self.viewer.show(rgba); @@ -103,14 +110,29 @@ 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, blurhash) => { + if !self.previews.contains_key(&id) { + let _ = self + .previews + .insert(id, Preview::loading(blurhash.rgba)); } Task::none() @@ -157,7 +179,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 +196,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 +254,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 +362,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/game_of_life/src/main.rs b/examples/game_of_life/src/main.rs index dec3df7f..9516f832 100644 --- a/examples/game_of_life/src/main.rs +++ b/examples/game_of_life/src/main.rs @@ -9,7 +9,7 @@ use iced::time::{self, milliseconds}; use iced::widget::{ button, checkbox, column, container, pick_list, row, slider, text, }; -use iced::{Center, Element, Fill, Subscription, Task, Theme}; +use iced::{Center, Element, Fill, Function, Subscription, Task, Theme}; pub fn main() -> iced::Result { tracing_subscriber::fmt::init(); @@ -37,7 +37,7 @@ struct GameOfLife { #[derive(Debug, Clone)] enum Message { - Grid(grid::Message, usize), + Grid(usize, grid::Message), Tick, TogglePlayback, ToggleGrid(bool), @@ -61,7 +61,7 @@ impl GameOfLife { fn update(&mut self, message: Message) -> Task<Message> { match message { - Message::Grid(message, version) => { + Message::Grid(version, message) => { if version == self.version { self.grid.update(message); } @@ -78,9 +78,7 @@ impl GameOfLife { let version = self.version; - return Task::perform(task, move |message| { - Message::Grid(message, version) - }); + return Task::perform(task, Message::Grid.with(version)); } } Message::TogglePlayback => { @@ -129,9 +127,7 @@ impl GameOfLife { ); let content = column![ - self.grid - .view() - .map(move |message| Message::Grid(message, version)), + self.grid.view().map(Message::Grid.with(version)), controls, ] .height(Fill); diff --git a/examples/multi_window/src/main.rs b/examples/multi_window/src/main.rs index f9021c8d..8cec9d4c 100644 --- a/examples/multi_window/src/main.rs +++ b/examples/multi_window/src/main.rs @@ -3,7 +3,9 @@ use iced::widget::{ text_input, }; use iced::window; -use iced::{Center, Element, Fill, Subscription, Task, Theme, Vector}; +use iced::{ + Center, Element, Fill, Function, Subscription, Task, Theme, Vector, +}; use std::collections::BTreeMap; @@ -169,7 +171,7 @@ impl Window { let scale_input = column![ text("Window scale factor:"), text_input("Window Scale", &self.scale_input) - .on_input(move |msg| { Message::ScaleInputChanged(id, msg) }) + .on_input(Message::ScaleInputChanged.with(id)) .on_submit(Message::ScaleChanged( id, self.scale_input.to_string() @@ -179,7 +181,7 @@ impl Window { let title_input = column![ text("Window title:"), text_input("Window Title", &self.title) - .on_input(move |msg| { Message::TitleChanged(id, msg) }) + .on_input(Message::TitleChanged.with(id)) .id(format!("input-{id}")) ]; 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/todos/src/main.rs b/examples/todos/src/main.rs index 7faf742e..dfb73d96 100644 --- a/examples/todos/src/main.rs +++ b/examples/todos/src/main.rs @@ -4,7 +4,9 @@ use iced::widget::{ scrollable, text, text_input, Text, }; use iced::window; -use iced::{Center, Element, Fill, Font, Subscription, Task as Command}; +use iced::{ + Center, Element, Fill, Font, Function, Subscription, Task as Command, +}; use serde::{Deserialize, Serialize}; use uuid::Uuid; @@ -215,9 +217,8 @@ impl Todos { .map(|(i, task)| { ( task.id, - task.view(i).map(move |message| { - Message::TaskMessage(i, message) - }), + task.view(i) + .map(Message::TaskMessage.with(i)), ) }), ) 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( diff --git a/examples/websocket/src/echo.rs b/examples/websocket/src/echo.rs index 14652936..149a260c 100644 --- a/examples/websocket/src/echo.rs +++ b/examples/websocket/src/echo.rs @@ -1,73 +1,59 @@ pub mod server; use iced::futures; -use iced::stream; +use iced::task::{sipper, Never, Sipper}; use iced::widget::text; use futures::channel::mpsc; use futures::sink::SinkExt; -use futures::stream::{Stream, StreamExt}; +use futures::stream::StreamExt; use async_tungstenite::tungstenite; use std::fmt; -pub fn connect() -> impl Stream<Item = Event> { - stream::channel(100, |mut output| async move { - let mut state = State::Disconnected; - +pub fn connect() -> impl Sipper<Never, Event> { + sipper(|mut output| async move { loop { - match &mut state { - State::Disconnected => { - const ECHO_SERVER: &str = "ws://127.0.0.1:3030"; + const ECHO_SERVER: &str = "ws://127.0.0.1:3030"; - match async_tungstenite::tokio::connect_async(ECHO_SERVER) - .await - { - Ok((websocket, _)) => { - let (sender, receiver) = mpsc::channel(100); + let (mut websocket, mut input) = + match async_tungstenite::tokio::connect_async(ECHO_SERVER).await + { + Ok((websocket, _)) => { + let (sender, receiver) = mpsc::channel(100); - let _ = output - .send(Event::Connected(Connection(sender))) - .await; + output.send(Event::Connected(Connection(sender))).await; - state = State::Connected(websocket, receiver); - } - Err(_) => { - tokio::time::sleep( - tokio::time::Duration::from_secs(1), - ) + (websocket.fuse(), receiver) + } + Err(_) => { + tokio::time::sleep(tokio::time::Duration::from_secs(1)) .await; - let _ = output.send(Event::Disconnected).await; - } + output.send(Event::Disconnected).await; + continue; } - } - State::Connected(websocket, input) => { - let mut fused_websocket = websocket.by_ref().fuse(); - - futures::select! { - received = fused_websocket.select_next_some() => { - match received { - Ok(tungstenite::Message::Text(message)) => { - let _ = output.send(Event::MessageReceived(Message::User(message))).await; - } - Err(_) => { - let _ = output.send(Event::Disconnected).await; - - state = State::Disconnected; - } - Ok(_) => continue, + }; + + loop { + futures::select! { + received = websocket.select_next_some() => { + match received { + Ok(tungstenite::Message::Text(message)) => { + output.send(Event::MessageReceived(Message::User(message))).await; + } + Err(_) => { + output.send(Event::Disconnected).await; + break; } + Ok(_) => {}, } + } + message = input.select_next_some() => { + let result = websocket.send(tungstenite::Message::Text(message.to_string())).await; - message = input.select_next_some() => { - let result = websocket.send(tungstenite::Message::Text(message.to_string())).await; - - if result.is_err() { - let _ = output.send(Event::Disconnected).await; - - state = State::Disconnected; - } + if result.is_err() { + output.send(Event::Disconnected).await; } } } @@ -76,18 +62,6 @@ pub fn connect() -> impl Stream<Item = Event> { }) } -#[derive(Debug)] -#[allow(clippy::large_enum_variant)] -enum State { - Disconnected, - Connected( - async_tungstenite::WebSocketStream< - async_tungstenite::tokio::ConnectStream, - >, - mpsc::Receiver<Message>, - ), -} - #[derive(Debug, Clone)] pub enum Event { Connected(Connection), diff --git a/futures/src/backend/native/tokio.rs b/futures/src/backend/native/tokio.rs index e0be83a6..c38ef566 100644 --- a/futures/src/backend/native/tokio.rs +++ b/futures/src/backend/native/tokio.rs @@ -23,11 +23,10 @@ impl crate::Executor for Executor { pub mod time { //! Listen and react to time. use crate::core::time::{Duration, Instant}; - use crate::stream; use crate::subscription::Subscription; use crate::MaybeSend; - use futures::SinkExt; + use futures::stream; use std::future::Future; /// Returns a [`Subscription`] that produces messages at a set interval. @@ -66,12 +65,12 @@ pub mod time { let f = *f; let interval = *interval; - stream::channel(1, move |mut output| async move { - loop { - let _ = output.send(f().await).await; - + stream::unfold(0, move |i| async move { + if i > 0 { tokio::time::sleep(interval).await; } + + Some((f().await, i + 1)) }) }) } diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index 703c3ed9..fc212ef8 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -23,5 +23,6 @@ iced_core.workspace = true iced_futures.workspace = true iced_futures.features = ["thread-pool"] -thiserror.workspace = true raw-window-handle.workspace = true +sipper.workspace = true +thiserror.workspace = true diff --git a/runtime/src/task.rs b/runtime/src/task.rs index 22cfb63e..022483f7 100644 --- a/runtime/src/task.rs +++ b/runtime/src/task.rs @@ -3,7 +3,6 @@ use crate::core::widget; use crate::futures::futures::channel::mpsc; use crate::futures::futures::channel::oneshot; use crate::futures::futures::future::{self, FutureExt}; -use crate::futures::futures::never::Never; use crate::futures::futures::stream::{self, Stream, StreamExt}; use crate::futures::{boxed_stream, BoxStream, MaybeSend}; use crate::Action; @@ -11,6 +10,9 @@ use crate::Action; use std::future::Future; use std::sync::Arc; +#[doc(no_inline)] +pub use sipper::{sipper, stream, Never, Sender, Sipper, Straw}; + /// A set of concurrent actions to be performed by the iced runtime. /// /// A [`Task`] _may_ produce a bunch of values of type `T`. @@ -57,6 +59,22 @@ impl<T> Task<T> { Self::stream(stream.map(f)) } + /// Creates a [`Task`] that runs the given [`Sipper`] to completion, mapping + /// progress with the first closure and the output with the second one. + pub fn sip<S>( + sipper: S, + on_progress: impl FnMut(S::Progress) -> T + MaybeSend + 'static, + on_output: impl FnOnce(<S as Future>::Output) -> T + MaybeSend + 'static, + ) -> Self + where + S: sipper::Core + MaybeSend + 'static, + T: MaybeSend + 'static, + { + Self::stream(stream(sipper::sipper(move |sender| async move { + on_output(sipper.with(on_progress).run(sender).await) + }))) + } + /// Combines the given tasks and produces a single [`Task`] that will run all of them /// in parallel. pub fn batch(tasks: impl IntoIterator<Item = Self>) -> Self @@ -506,8 +506,8 @@ pub use crate::core::padding; pub use crate::core::theme; pub use crate::core::{ never, Alignment, Animation, Background, Border, Color, ContentFit, - Degrees, Gradient, Length, Padding, Pixels, Point, Radians, Rectangle, - Rotation, Settings, Shadow, Size, Theme, Transformation, Vector, + Degrees, Function, Gradient, Length, Padding, Pixels, Point, Radians, + Rectangle, Rotation, Settings, Shadow, Size, Theme, Transformation, Vector, }; pub use crate::runtime::exit; pub use iced_futures::Subscription; @@ -519,7 +519,9 @@ pub use Length::{Fill, FillPortion, Shrink}; pub mod task { //! Create runtime tasks. - pub use crate::runtime::task::{Handle, Task}; + pub use crate::runtime::task::{ + sipper, stream, Handle, Never, Sipper, Straw, Task, + }; } pub mod clipboard { diff --git a/widget/src/container.rs b/widget/src/container.rs index 82dc3141..86c1c7a8 100644 --- a/widget/src/container.rs +++ b/widget/src/container.rs @@ -26,6 +26,7 @@ use crate::core::layout; use crate::core::mouse; use crate::core::overlay; use crate::core::renderer; +use crate::core::theme; use crate::core::widget::tree::{self, Tree}; use crate::core::widget::{self, Operation}; use crate::core::{ @@ -714,9 +715,44 @@ pub fn bordered_box(theme: &Theme) -> Style { /// A [`Container`] with a dark background and white text. pub fn dark(_theme: &Theme) -> Style { + style(theme::palette::Pair { + color: color!(0x111111), + text: Color::WHITE, + }) +} + +/// A [`Container`] with a primary background color. +pub fn primary(theme: &Theme) -> Style { + let palette = theme.extended_palette(); + + style(palette.primary.base) +} + +/// A [`Container`] with a secondary background color. +pub fn secondary(theme: &Theme) -> Style { + let palette = theme.extended_palette(); + + style(palette.secondary.base) +} + +/// A [`Container`] with a success background color. +pub fn success(theme: &Theme) -> Style { + let palette = theme.extended_palette(); + + style(palette.success.base) +} + +/// A [`Container`] with a danger background color. +pub fn danger(theme: &Theme) -> Style { + let palette = theme.extended_palette(); + + style(palette.danger.base) +} + +fn style(pair: theme::palette::Pair) -> Style { Style { - background: Some(color!(0x111111).into()), - text_color: Some(Color::WHITE), + background: Some(pair.color.into()), + text_color: Some(pair.text), border: border::rounded(2), ..Style::default() } diff --git a/widget/src/image/viewer.rs b/widget/src/image/viewer.rs index 06652ff7..811241a9 100644 --- a/widget/src/image/viewer.rs +++ b/widget/src/image/viewer.rs @@ -215,6 +215,7 @@ where } } + shell.request_redraw(); shell.capture_event(); } Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => { @@ -226,6 +227,8 @@ where state.cursor_grabbed_at = Some(cursor_position); state.starting_offset = state.current_offset; + + shell.request_redraw(); shell.capture_event(); } Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) => { @@ -233,6 +236,7 @@ where if state.cursor_grabbed_at.is_some() { state.cursor_grabbed_at = None; + shell.request_redraw(); shell.capture_event(); } } @@ -273,6 +277,7 @@ where }; state.current_offset = Vector::new(x, y); + shell.request_redraw(); shell.capture_event(); } } diff --git a/widget/src/lib.rs b/widget/src/lib.rs index b8cfa98f..31dcc205 100644 --- a/widget/src/lib.rs +++ b/widget/src/lib.rs @@ -12,7 +12,6 @@ mod action; mod column; mod mouse_area; mod pin; -mod row; mod space; mod stack; mod themer; @@ -28,6 +27,7 @@ pub mod pick_list; pub mod pop; pub mod progress_bar; pub mod radio; +pub mod row; pub mod rule; pub mod scrollable; pub mod slider; diff --git a/widget/src/scrollable.rs b/widget/src/scrollable.rs index 8adf5136..fe71fd6b 100644 --- a/widget/src/scrollable.rs +++ b/widget/src/scrollable.rs @@ -753,7 +753,7 @@ where if let InputMethod::Open { position, .. } = shell.input_method_mut() { - *position = *position + translation; + *position = *position - translation; } } }; |