diff options
37 files changed, 696 insertions, 354 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.6"  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",  ] @@ -5251,6 +5260,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/input_method.rs b/core/src/input_method.rs index 4e8c383b..cd8d459d 100644 --- a/core/src/input_method.rs +++ b/core/src/input_method.rs @@ -1,19 +1,15 @@  //! Listen to input method events. -use crate::Point; +use crate::{Pixels, Point};  use std::ops::Range;  /// The input method strategy of a widget.  #[derive(Debug, Clone, PartialEq)]  pub enum InputMethod<T = String> { -    /// No input method strategy has been specified. -    None, -    /// No input method is allowed. +    /// Input method is disabled.      Disabled, -    /// Input methods are allowed, but not open yet. -    Allowed, -    /// Input method is open. -    Open { +    /// Input method is enabled. +    Enabled {          /// The position at which the input method dialog should be placed.          position: Point,          /// The [`Purpose`] of the input method. @@ -34,6 +30,8 @@ pub struct Preedit<T = String> {      pub content: T,      /// The selected range of the content.      pub selection: Option<Range<usize>>, +    /// The text size of the content. +    pub text_size: Option<Pixels>,  }  impl<T> Preedit<T> { @@ -53,6 +51,7 @@ impl<T> Preedit<T> {          Preedit {              content: self.content.as_ref().to_owned(),              selection: self.selection.clone(), +            text_size: self.text_size,          }      }  } @@ -63,6 +62,7 @@ impl Preedit {          Preedit {              content: &self.content,              selection: self.selection.clone(), +            text_size: self.text_size,          }      }  } @@ -87,26 +87,20 @@ impl InputMethod {      /// # use iced_core::input_method::{InputMethod, Purpose, Preedit};      /// # use iced_core::Point;      /// -    /// let open = InputMethod::Open { +    /// let open = InputMethod::Enabled {      ///     position: Point::ORIGIN,      ///     purpose: Purpose::Normal, -    ///     preedit: Some(Preedit { content: "1".to_owned(), selection: None }), +    ///     preedit: Some(Preedit { content: "1".to_owned(), selection: None, text_size: None }),      /// };      /// -    /// let open_2 = InputMethod::Open { +    /// let open_2 = InputMethod::Enabled {      ///     position: Point::ORIGIN,      ///     purpose: Purpose::Secure, -    ///     preedit: Some(Preedit { content: "2".to_owned(), selection: None }), +    ///     preedit: Some(Preedit { content: "2".to_owned(), selection: None, text_size: None }),      /// };      ///      /// let mut ime = InputMethod::Disabled;      /// -    /// ime.merge(&InputMethod::<String>::Allowed); -    /// assert_eq!(ime, InputMethod::Allowed); -    /// -    /// ime.merge(&InputMethod::<String>::Disabled); -    /// assert_eq!(ime, InputMethod::Allowed); -    ///      /// ime.merge(&open);      /// assert_eq!(ime, open);      /// @@ -114,22 +108,16 @@ impl InputMethod {      /// assert_eq!(ime, open);      /// ```      pub fn merge<T: AsRef<str>>(&mut self, other: &InputMethod<T>) { -        match (&self, other) { -            (InputMethod::Open { .. }, _) -            | ( -                InputMethod::Allowed, -                InputMethod::None | InputMethod::Disabled, -            ) -            | (InputMethod::Disabled, InputMethod::None) => {} -            _ => { -                *self = other.to_owned(); -            } +        if let InputMethod::Enabled { .. } = self { +            return;          } + +        *self = other.to_owned();      }      /// Returns true if the [`InputMethod`] is open. -    pub fn is_open(&self) -> bool { -        matches!(self, Self::Open { .. }) +    pub fn is_enabled(&self) -> bool { +        matches!(self, Self::Enabled { .. })      }  } @@ -140,14 +128,12 @@ impl<T> InputMethod<T> {          T: AsRef<str>,      {          match self { -            Self::None => InputMethod::None,              Self::Disabled => InputMethod::Disabled, -            Self::Allowed => InputMethod::Allowed, -            Self::Open { +            Self::Enabled {                  position,                  purpose,                  preedit, -            } => InputMethod::Open { +            } => InputMethod::Enabled {                  position: *position,                  purpose: *purpose,                  preedit: preedit.as_ref().map(Preedit::to_owned), 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/shell.rs b/core/src/shell.rs index 509e3822..56250e2e 100644 --- a/core/src/shell.rs +++ b/core/src/shell.rs @@ -27,7 +27,7 @@ impl<'a, Message> Shell<'a, Message> {              redraw_request: window::RedrawRequest::Wait,              is_layout_invalid: false,              are_widgets_invalid: false, -            input_method: InputMethod::None, +            input_method: InputMethod::Disabled,          }      } 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/futures/src/subscription.rs b/futures/src/subscription.rs index 82cba9a1..3577d19f 100644 --- a/futures/src/subscription.rs +++ b/futures/src/subscription.rs @@ -210,7 +210,8 @@ impl<T> Subscription<T> {      /// Returns a [`Subscription`] that will create and asynchronously run the      /// given [`Stream`].      /// -    /// The `id` will be used to uniquely identify the [`Subscription`]. +    /// Both the `data` and the function pointer will be used to uniquely identify +    /// the [`Subscription`].      pub fn run_with<D, S>(data: D, builder: fn(&D) -> S) -> Self      where          D: Hash + 'static, diff --git a/graphics/src/text/editor.rs b/graphics/src/text/editor.rs index c73d189c..765de07e 100644 --- a/graphics/src/text/editor.rs +++ b/graphics/src/text/editor.rs @@ -11,7 +11,7 @@ use cosmic_text::Edit as _;  use std::borrow::Cow;  use std::fmt; -use std::sync::{self, Arc}; +use std::sync::{self, Arc, RwLock};  /// A multi-line text editor.  #[derive(Debug, PartialEq)] @@ -19,6 +19,7 @@ pub struct Editor(Option<Arc<Internal>>);  struct Internal {      editor: cosmic_text::Editor<'static>, +    cursor: RwLock<Option<Cursor>>,      font: Font,      bounds: Size,      topmost_line_changed: Option<usize>, @@ -114,10 +115,14 @@ impl editor::Editor for Editor {      fn cursor(&self) -> editor::Cursor {          let internal = self.internal(); +        if let Ok(Some(cursor)) = internal.cursor.read().as_deref() { +            return cursor.clone(); +        } +          let cursor = internal.editor.cursor();          let buffer = buffer_from_editor(&internal.editor); -        match internal.editor.selection_bounds() { +        let cursor = match internal.editor.selection_bounds() {              Some((start, end)) => {                  let line_height = buffer.metrics().line_height;                  let selected_lines = end.line - start.line + 1; @@ -237,7 +242,12 @@ impl editor::Editor for Editor {                          - buffer.scroll().vertical,                  ))              } -        } +        }; + +        *internal.cursor.write().expect("Write to cursor cache") = +            Some(cursor.clone()); + +        cursor      }      fn cursor_position(&self) -> (usize, usize) { @@ -259,6 +269,13 @@ impl editor::Editor for Editor {          let editor = &mut internal.editor; +        // Clear cursor cache +        let _ = internal +            .cursor +            .write() +            .expect("Write to cursor cache") +            .take(); +          match action {              // Motion events              Action::Move(motion) => { @@ -527,6 +544,13 @@ impl editor::Editor for Editor {          internal.editor.shape_as_needed(font_system.raw(), false); +        // Clear cursor cache +        let _ = internal +            .cursor +            .write() +            .expect("Write to cursor cache") +            .take(); +          self.0 = Some(Arc::new(internal));      } @@ -635,6 +659,7 @@ impl Default for Internal {                      line_height: 1.0,                  },              )), +            cursor: RwLock::new(None),              font: Font::default(),              bounds: Size::ZERO,              topmost_line_changed: None, 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 diff --git a/runtime/src/user_interface.rs b/runtime/src/user_interface.rs index cb441678..9b396c69 100644 --- a/runtime/src/user_interface.rs +++ b/runtime/src/user_interface.rs @@ -189,7 +189,7 @@ where          let mut outdated = false;          let mut redraw_request = window::RedrawRequest::Wait; -        let mut input_method = InputMethod::None; +        let mut input_method = InputMethod::Disabled;          let mut manual_overlay = ManuallyDrop::new(              self.root @@ -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/wgpu/src/lib.rs b/wgpu/src/lib.rs index 9c9fcb31..b1998da7 100644 --- a/wgpu/src/lib.rs +++ b/wgpu/src/lib.rs @@ -280,13 +280,16 @@ impl Renderer {          let scale = Transformation::scale(scale_factor);          for layer in self.layers.iter() { -            let Some(scissor_rect) = physical_bounds -                .intersection(&(layer.bounds * scale_factor)) -                .and_then(Rectangle::snap) +            let Some(physical_bounds) = +                physical_bounds.intersection(&(layer.bounds * scale_factor))              else {                  continue;              }; +            let Some(scissor_rect) = physical_bounds.snap() else { +                continue; +            }; +              if !layer.quads.is_empty() {                  engine.quad_pipeline.render(                      quad_layer, 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/lazy/component.rs b/widget/src/lazy/component.rs index 30822b7d..c215de7a 100644 --- a/widget/src/lazy/component.rs +++ b/widget/src/lazy/component.rs @@ -266,7 +266,10 @@ where              state: tree::State::new(S::default()),              children: vec![Tree::empty()],          }))); +          *self.tree.borrow_mut() = state.clone(); +        self.diff_self(); +          tree::State::new(state)      } 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..0cf75c04 100644 --- a/widget/src/scrollable.rs +++ b/widget/src/scrollable.rs @@ -729,7 +729,7 @@ where                      _ => mouse::Cursor::Unavailable,                  }; -                let had_input_method = shell.input_method().is_open(); +                let had_input_method = shell.input_method().is_enabled();                  let translation =                      state.translation(self.direction, bounds, content_bounds); @@ -750,10 +750,10 @@ where                  );                  if !had_input_method { -                    if let InputMethod::Open { position, .. } = +                    if let InputMethod::Enabled { position, .. } =                          shell.input_method_mut()                      { -                        *position = *position + translation; +                        *position = *position - translation;                      }                  }              }; diff --git a/widget/src/text_editor.rs b/widget/src/text_editor.rs index e685256b..7e40a56a 100644 --- a/widget/src/text_editor.rs +++ b/widget/src/text_editor.rs @@ -339,10 +339,6 @@ where              return InputMethod::Disabled;          }; -        let Some(preedit) = &state.preedit else { -            return InputMethod::Allowed; -        }; -          let bounds = layout.bounds();          let internal = self.content.0.borrow_mut(); @@ -363,10 +359,10 @@ where          let position =              cursor + translation + Vector::new(0.0, f32::from(line_height)); -        InputMethod::Open { +        InputMethod::Enabled {              position,              purpose: input_method::Purpose::Normal, -            preedit: Some(preedit.as_ref()), +            preedit: state.preedit.as_ref().map(input_method::Preedit::as_ref),          }      }  } @@ -759,8 +755,11 @@ where                          shell.request_redraw();                      }                      Ime::Preedit { content, selection } => { -                        state.preedit = -                            Some(input_method::Preedit { content, selection }); +                        state.preedit = Some(input_method::Preedit { +                            content, +                            selection, +                            text_size: self.text_size, +                        });                          shell.request_redraw();                      } diff --git a/widget/src/text_input.rs b/widget/src/text_input.rs index 7be5bbd9..ae3dfe4c 100644 --- a/widget/src/text_input.rs +++ b/widget/src/text_input.rs @@ -406,10 +406,6 @@ where              return InputMethod::Disabled;          }; -        let Some(preedit) = &state.is_ime_open else { -            return InputMethod::Allowed; -        }; -          let secure_value = self.is_secure.then(|| value.secure());          let value = secure_value.as_ref().unwrap_or(value); @@ -433,14 +429,14 @@ where          let x = (text_bounds.x + cursor_x).floor() - scroll_offset              + alignment_offset; -        InputMethod::Open { +        InputMethod::Enabled {              position: Point::new(x, text_bounds.y + text_bounds.height),              purpose: if self.is_secure {                  input_method::Purpose::Secure              } else {                  input_method::Purpose::Normal              }, -            preedit: Some(preedit.as_ref()), +            preedit: state.preedit.as_ref().map(input_method::Preedit::as_ref),          }      } @@ -584,7 +580,7 @@ where          let draw = |renderer: &mut Renderer, viewport| {              let paragraph = if text.is_empty()                  && state -                    .is_ime_open +                    .preedit                      .as_ref()                      .map(|preedit| preedit.content.is_empty())                      .unwrap_or(true) @@ -1260,7 +1256,7 @@ where                  input_method::Event::Opened | input_method::Event::Closed => {                      let state = state::<Renderer>(tree); -                    state.is_ime_open = +                    state.preedit =                          matches!(event, input_method::Event::Opened)                              .then(input_method::Preedit::new); @@ -1270,9 +1266,10 @@ where                      let state = state::<Renderer>(tree);                      if state.is_focused.is_some() { -                        state.is_ime_open = Some(input_method::Preedit { +                        state.preedit = Some(input_method::Preedit {                              content: content.to_owned(),                              selection: selection.clone(), +                            text_size: self.size,                          });                          shell.request_redraw(); @@ -1322,23 +1319,30 @@ where                  let state = state::<Renderer>(tree);                  if let Some(focus) = &mut state.is_focused { -                    if focus.is_window_focused -                        && matches!( +                    if focus.is_window_focused { +                        if matches!(                              state.cursor.state(&self.value),                              cursor::State::Index(_) -                        ) -                    { -                        focus.now = *now; - -                        let millis_until_redraw = CURSOR_BLINK_INTERVAL_MILLIS -                            - (*now - focus.updated_at).as_millis() -                                % CURSOR_BLINK_INTERVAL_MILLIS; - -                        shell.request_redraw_at( -                            *now + Duration::from_millis( -                                millis_until_redraw as u64, -                            ), -                        ); +                        ) { +                            focus.now = *now; + +                            let millis_until_redraw = +                                CURSOR_BLINK_INTERVAL_MILLIS +                                    - (*now - focus.updated_at).as_millis() +                                        % CURSOR_BLINK_INTERVAL_MILLIS; + +                            shell.request_redraw_at( +                                *now + Duration::from_millis( +                                    millis_until_redraw as u64, +                                ), +                            ); +                        } + +                        shell.request_input_method(&self.input_method( +                            state, +                            layout, +                            &self.value, +                        ));                      }                  }              } @@ -1362,12 +1366,6 @@ where          if let Event::Window(window::Event::RedrawRequested(_now)) = event {              self.last_status = Some(status); - -            shell.request_input_method(&self.input_method( -                state, -                layout, -                &self.value, -            ));          } else if self              .last_status              .is_some_and(|last_status| status != last_status) @@ -1527,9 +1525,9 @@ pub struct State<P: text::Paragraph> {      placeholder: paragraph::Plain<P>,      icon: paragraph::Plain<P>,      is_focused: Option<Focus>, -    is_ime_open: Option<input_method::Preedit>,      is_dragging: bool,      is_pasting: Option<Value>, +    preedit: Option<input_method::Preedit>,      last_click: Option<mouse::Click>,      cursor: Cursor,      keyboard_modifiers: keyboard::Modifiers, @@ -1725,7 +1723,7 @@ fn replace_paragraph<Renderer>(          bounds: Size::new(f32::INFINITY, text_bounds.height),          size: text_size,          horizontal_alignment: alignment::Horizontal::Left, -        vertical_alignment: alignment::Vertical::Top, +        vertical_alignment: alignment::Vertical::Center,          shaping: text::Shaping::Advanced,          wrapping: text::Wrapping::default(),      }); diff --git a/winit/src/program.rs b/winit/src/program.rs index 7ead4c3b..9a64fa51 100644 --- a/winit/src/program.rs +++ b/winit/src/program.rs @@ -363,7 +363,7 @@ where                                      (                                          ControlFlow::WaitUntil(current),                                          ControlFlow::WaitUntil(new), -                                    ) if new < current => {} +                                    ) if current < new => {}                                      (                                          ControlFlow::WaitUntil(target),                                          ControlFlow::Wait, diff --git a/winit/src/program/window_manager.rs b/winit/src/program/window_manager.rs index ae214e7c..139d787a 100644 --- a/winit/src/program/window_manager.rs +++ b/winit/src/program/window_manager.rs @@ -75,6 +75,7 @@ where                  mouse_interaction: mouse::Interaction::None,                  redraw_at: None,                  preedit: None, +                ime_state: None,              },          ); @@ -166,6 +167,7 @@ where      pub renderer: P::Renderer,      pub redraw_at: Option<Instant>,      preedit: Option<Preedit<P::Renderer>>, +    ime_state: Option<(Point, input_method::Purpose)>,  }  impl<P, C> Window<P, C> @@ -206,52 +208,36 @@ where      pub fn request_input_method(&mut self, input_method: InputMethod) {          match input_method { -            InputMethod::None => {}              InputMethod::Disabled => { -                self.raw.set_ime_allowed(false); +                self.disable_ime();              } -            InputMethod::Allowed | InputMethod::Open { .. } => { -                self.raw.set_ime_allowed(true); -            } -        } - -        if let InputMethod::Open { -            position, -            purpose, -            preedit, -        } = input_method -        { -            self.raw.set_ime_cursor_area( -                LogicalPosition::new(position.x, position.y), -                LogicalSize::new(10, 10), // TODO? -            ); - -            self.raw.set_ime_purpose(conversion::ime_purpose(purpose)); - -            if let Some(preedit) = preedit { -                if preedit.content.is_empty() { -                    self.preedit = None; -                } else if let Some(overlay) = &mut self.preedit { -                    overlay.update( -                        position, -                        &preedit, -                        self.state.background_color(), -                        &self.renderer, -                    ); +            InputMethod::Enabled { +                position, +                purpose, +                preedit, +            } => { +                self.enable_ime(position, purpose); + +                if let Some(preedit) = preedit { +                    if preedit.content.is_empty() { +                        self.preedit = None; +                    } else { +                        let mut overlay = +                            self.preedit.take().unwrap_or_else(Preedit::new); + +                        overlay.update( +                            position, +                            &preedit, +                            self.state.background_color(), +                            &self.renderer, +                        ); + +                        self.preedit = Some(overlay); +                    }                  } else { -                    let mut overlay = Preedit::new(); -                    overlay.update( -                        position, -                        &preedit, -                        self.state.background_color(), -                        &self.renderer, -                    ); - -                    self.preedit = Some(overlay); +                    self.preedit = None;                  }              } -        } else { -            self.preedit = None;          }      } @@ -268,6 +254,31 @@ where              );          }      } + +    fn enable_ime(&mut self, position: Point, purpose: input_method::Purpose) { +        if self.ime_state.is_none() { +            self.raw.set_ime_allowed(true); +        } + +        if self.ime_state != Some((position, purpose)) { +            self.raw.set_ime_cursor_area( +                LogicalPosition::new(position.x, position.y), +                LogicalSize::new(10, 10), // TODO? +            ); +            self.raw.set_ime_purpose(conversion::ime_purpose(purpose)); + +            self.ime_state = Some((position, purpose)); +        } +    } + +    fn disable_ime(&mut self) { +        if self.ime_state.is_some() { +            self.raw.set_ime_allowed(false); +            self.ime_state = None; +        } + +        self.preedit = None; +    }  }  struct Preedit<Renderer> @@ -322,7 +333,9 @@ where              self.content = Renderer::Paragraph::with_spans(Text {                  content: &spans,                  bounds: Size::INFINITY, -                size: renderer.default_size(), +                size: preedit +                    .text_size +                    .unwrap_or_else(|| renderer.default_size()),                  line_height: text::LineHeight::default(),                  font: renderer.default_font(),                  horizontal_alignment: alignment::Horizontal::Left, @@ -330,6 +343,10 @@ where                  shaping: text::Shaping::Advanced,                  wrapping: text::Wrapping::None,              }); + +            self.spans.clear(); +            self.spans +                .extend(spans.into_iter().map(text::Span::to_static));          }      } | 
