diff options
42 files changed, 662 insertions, 626 deletions
diff --git a/core/src/text/editor.rs b/core/src/text/editor.rs index fbf60696..aea00921 100644 --- a/core/src/text/editor.rs +++ b/core/src/text/editor.rs @@ -70,6 +70,8 @@ pub enum Action { SelectWord, /// Select the line at the current cursor. SelectLine, + /// Select the entire buffer. + SelectAll, /// Perform an [`Edit`]. Edit(Edit), /// Click the [`Editor`] at the given [`Point`]. diff --git a/examples/download_progress/Cargo.toml b/examples/download_progress/Cargo.toml index 18a49f66..f78df529 100644 --- a/examples/download_progress/Cargo.toml +++ b/examples/download_progress/Cargo.toml @@ -10,6 +10,6 @@ iced.workspace = true iced.features = ["tokio"] [dependencies.reqwest] -version = "0.11" +version = "0.12" default-features = false features = ["rustls-tls"] diff --git a/examples/download_progress/src/download.rs b/examples/download_progress/src/download.rs index d6cc1e24..bdf57290 100644 --- a/examples/download_progress/src/download.rs +++ b/examples/download_progress/src/download.rs @@ -1,4 +1,5 @@ -use iced::subscription; +use iced::futures; +use iced::Subscription; use std::hash::Hash; @@ -7,9 +8,14 @@ pub fn file<I: 'static + Hash + Copy + Send + Sync, T: ToString>( id: I, url: T, ) -> iced::Subscription<(I, Progress)> { - subscription::unfold(id, State::Ready(url.to_string()), move |state| { - download(id, state) - }) + Subscription::run_with_id( + id, + futures::stream::unfold(State::Ready(url.to_string()), move |state| { + use iced::futures::FutureExt; + + download(id, state).map(Some) + }), + ) } async fn download<I: Copy>(id: I, state: State) -> ((I, Progress), State) { diff --git a/examples/editor/src/main.rs b/examples/editor/src/main.rs index bed9d94a..ce3c478d 100644 --- a/examples/editor/src/main.rs +++ b/examples/editor/src/main.rs @@ -13,12 +13,11 @@ use std::sync::Arc; pub fn main() -> iced::Result { iced::application("Editor - Iced", Editor::update, Editor::view) - .load(Editor::load) .subscription(Editor::subscription) .theme(Editor::theme) .font(include_bytes!("../fonts/icons.ttf").as_slice()) .default_font(Font::MONOSPACE) - .run() + .run_with(Editor::new) } struct Editor { @@ -41,20 +40,22 @@ enum Message { } impl Editor { - fn new() -> Self { - Self { - file: None, - content: text_editor::Content::new(), - theme: highlighter::Theme::SolarizedDark, - is_loading: true, - is_dirty: false, - } - } - - fn load() -> Task<Message> { - Task::perform( - load_file(format!("{}/src/main.rs", env!("CARGO_MANIFEST_DIR"))), - Message::FileOpened, + fn new() -> (Self, Task<Message>) { + ( + Self { + file: None, + content: text_editor::Content::new(), + theme: highlighter::Theme::SolarizedDark, + is_loading: true, + is_dirty: false, + }, + Task::perform( + load_file(format!( + "{}/src/main.rs", + env!("CARGO_MANIFEST_DIR") + )), + Message::FileOpened, + ), ) } @@ -214,12 +215,6 @@ impl Editor { } } -impl Default for Editor { - fn default() -> Self { - Self::new() - } -} - #[derive(Debug, Clone)] pub enum Error { DialogClosed, diff --git a/examples/multi_window/src/main.rs b/examples/multi_window/src/main.rs index 98e753ab..460ca3b5 100644 --- a/examples/multi_window/src/main.rs +++ b/examples/multi_window/src/main.rs @@ -9,16 +9,12 @@ use std::collections::BTreeMap; fn main() -> iced::Result { iced::daemon(Example::title, Example::update, Example::view) - .load(|| { - window::open(window::Settings::default()).map(Message::WindowOpened) - }) .subscription(Example::subscription) .theme(Example::theme) .scale_factor(Example::scale_factor) - .run() + .run_with(Example::new) } -#[derive(Default)] struct Example { windows: BTreeMap<window::Id, Window>, } @@ -43,6 +39,16 @@ enum Message { } impl Example { + fn new() -> (Self, Task<Message>) { + ( + Self { + windows: BTreeMap::new(), + }, + window::open(window::Settings::default()) + .map(Message::WindowOpened), + ) + } + fn title(&self, window: window::Id) -> String { self.windows .get(&window) diff --git a/examples/pokedex/Cargo.toml b/examples/pokedex/Cargo.toml index bf7e1e35..1a6d5445 100644 --- a/examples/pokedex/Cargo.toml +++ b/examples/pokedex/Cargo.toml @@ -16,7 +16,7 @@ version = "1.0" features = ["derive"] [dependencies.reqwest] -version = "0.11" +version = "0.12" default-features = false features = ["json", "rustls-tls"] diff --git a/examples/pokedex/src/main.rs b/examples/pokedex/src/main.rs index b22ffe7f..7414ae54 100644 --- a/examples/pokedex/src/main.rs +++ b/examples/pokedex/src/main.rs @@ -4,17 +4,13 @@ use iced::{Alignment, Element, Length, Task}; pub fn main() -> iced::Result { iced::application(Pokedex::title, Pokedex::update, Pokedex::view) - .load(Pokedex::search) - .run() + .run_with(Pokedex::new) } -#[derive(Debug, Default)] +#[derive(Debug)] enum Pokedex { - #[default] Loading, - Loaded { - pokemon: Pokemon, - }, + Loaded { pokemon: Pokemon }, Errored, } @@ -25,6 +21,10 @@ enum Message { } impl Pokedex { + fn new() -> (Self, Task<Message>) { + (Self::Loading, Self::search()) + } + fn search() -> Task<Message> { Task::perform(Pokemon::search(), Message::PokemonFound) } diff --git a/examples/slider/Cargo.toml b/examples/slider/Cargo.toml index fad8916e..05e74d2c 100644 --- a/examples/slider/Cargo.toml +++ b/examples/slider/Cargo.toml @@ -7,3 +7,4 @@ publish = false [dependencies] iced.workspace = true +iced.features = ["svg"] diff --git a/examples/slider/src/main.rs b/examples/slider/src/main.rs index 0b4c29aa..fd312763 100644 --- a/examples/slider/src/main.rs +++ b/examples/slider/src/main.rs @@ -1,5 +1,5 @@ -use iced::widget::{center, column, container, slider, text, vertical_slider}; -use iced::{Element, Length}; +use iced::widget::{column, container, iced, slider, text, vertical_slider}; +use iced::{Alignment, Element, Length}; pub fn main() -> iced::Result { iced::run("Slider - Iced", Slider::update, Slider::view) @@ -12,19 +12,11 @@ pub enum Message { pub struct Slider { value: u8, - default: u8, - step: u8, - shift_step: u8, } impl Slider { fn new() -> Self { - Slider { - value: 50, - default: 50, - step: 5, - shift_step: 1, - } + Slider { value: 50 } } fn update(&mut self, message: Message) { @@ -37,32 +29,27 @@ impl Slider { fn view(&self) -> Element<Message> { let h_slider = container( - slider(0..=100, self.value, Message::SliderChanged) - .default(self.default) - .step(self.step) - .shift_step(self.shift_step), + slider(1..=100, self.value, Message::SliderChanged) + .default(50) + .shift_step(5), ) .width(250); let v_slider = container( - vertical_slider(0..=100, self.value, Message::SliderChanged) - .default(self.default) - .step(self.step) - .shift_step(self.shift_step), + vertical_slider(1..=100, self.value, Message::SliderChanged) + .default(50) + .shift_step(5), ) .height(200); let text = text(self.value); - center( - column![ - container(v_slider).center_x(Length::Fill), - container(h_slider).center_x(Length::Fill), - container(text).center_x(Length::Fill) - ] - .spacing(25), - ) - .into() + column![v_slider, h_slider, text, iced(self.value as f32),] + .width(Length::Fill) + .align_items(Alignment::Center) + .spacing(20) + .padding(20) + .into() } } diff --git a/examples/todos/src/main.rs b/examples/todos/src/main.rs index 6ed50d31..b34f71ce 100644 --- a/examples/todos/src/main.rs +++ b/examples/todos/src/main.rs @@ -18,16 +18,14 @@ pub fn main() -> iced::Result { tracing_subscriber::fmt::init(); iced::application(Todos::title, Todos::update, Todos::view) - .load(Todos::load) .subscription(Todos::subscription) .font(include_bytes!("../fonts/icons.ttf").as_slice()) .window_size((500.0, 800.0)) - .run() + .run_with(Todos::new) } -#[derive(Default, Debug)] +#[derive(Debug)] enum Todos { - #[default] Loading, Loaded(State), } @@ -54,8 +52,11 @@ enum Message { } impl Todos { - fn load() -> Command<Message> { - Command::perform(SavedState::load(), Message::Loaded) + fn new() -> (Self, Command<Message>) { + ( + Self::Loading, + Command::perform(SavedState::load(), Message::Loaded), + ) } fn title(&self) -> String { diff --git a/examples/websocket/src/echo.rs b/examples/websocket/src/echo.rs index cd32cb66..14652936 100644 --- a/examples/websocket/src/echo.rs +++ b/examples/websocket/src/echo.rs @@ -1,87 +1,79 @@ pub mod server; use iced::futures; -use iced::subscription::{self, Subscription}; +use iced::stream; use iced::widget::text; use futures::channel::mpsc; use futures::sink::SinkExt; -use futures::stream::StreamExt; +use futures::stream::{Stream, StreamExt}; use async_tungstenite::tungstenite; use std::fmt; -pub fn connect() -> Subscription<Event> { - struct Connect; +pub fn connect() -> impl Stream<Item = Event> { + stream::channel(100, |mut output| async move { + let mut state = State::Disconnected; - subscription::channel( - std::any::TypeId::of::<Connect>(), - 100, - |mut output| async move { - let mut state = State::Disconnected; + loop { + match &mut state { + State::Disconnected => { + const ECHO_SERVER: &str = "ws://127.0.0.1:3030"; - loop { - match &mut state { - State::Disconnected => { - const ECHO_SERVER: &str = "ws://127.0.0.1:3030"; - - match async_tungstenite::tokio::connect_async( - ECHO_SERVER, - ) + 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; + { + Ok((websocket, _)) => { + let (sender, receiver) = mpsc::channel(100); - state = State::Connected(websocket, receiver); - } - Err(_) => { - tokio::time::sleep( - tokio::time::Duration::from_secs(1), - ) + let _ = output + .send(Event::Connected(Connection(sender))) .await; - let _ = output.send(Event::Disconnected).await; - } + state = State::Connected(websocket, receiver); + } + Err(_) => { + tokio::time::sleep( + tokio::time::Duration::from_secs(1), + ) + .await; + + let _ = output.send(Event::Disconnected).await; } } - 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, + } + 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; } - } - - message = input.select_next_some() => { - let result = websocket.send(tungstenite::Message::Text(message.to_string())).await; - - if result.is_err() { + Err(_) => { let _ = output.send(Event::Disconnected).await; state = State::Disconnected; } + Ok(_) => continue, + } + } + + 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; } } } } } - }, - ) + } + }) } #[derive(Debug)] diff --git a/examples/websocket/src/main.rs b/examples/websocket/src/main.rs index 8422ce16..d8246436 100644 --- a/examples/websocket/src/main.rs +++ b/examples/websocket/src/main.rs @@ -9,12 +9,10 @@ use once_cell::sync::Lazy; pub fn main() -> iced::Result { iced::application("WebSocket - Iced", WebSocket::update, WebSocket::view) - .load(WebSocket::load) .subscription(WebSocket::subscription) - .run() + .run_with(WebSocket::new) } -#[derive(Default)] struct WebSocket { messages: Vec<echo::Message>, new_message: String, @@ -30,11 +28,18 @@ enum Message { } impl WebSocket { - fn load() -> Task<Message> { - Task::batch([ - Task::perform(echo::server::run(), |_| Message::Server), - widget::focus_next(), - ]) + fn new() -> (Self, Task<Message>) { + ( + Self { + messages: Vec::new(), + new_message: String::new(), + state: State::Disconnected, + }, + Task::batch([ + Task::perform(echo::server::run(), |_| Message::Server), + widget::focus_next(), + ]), + ) } fn update(&mut self, message: Message) -> Task<Message> { @@ -83,7 +88,7 @@ impl WebSocket { } fn subscription(&self) -> Subscription<Message> { - echo::connect().map(Message::Echo) + Subscription::run(echo::connect).map(Message::Echo) } fn view(&self) -> Element<Message> { @@ -140,10 +145,4 @@ enum State { Connected(echo::Connection), } -impl Default for State { - fn default() -> Self { - Self::Disconnected - } -} - static MESSAGE_LOG: Lazy<scrollable::Id> = Lazy::new(scrollable::Id::unique); diff --git a/futures/src/backend/native/async_std.rs b/futures/src/backend/native/async_std.rs index b7da5e90..86714f45 100644 --- a/futures/src/backend/native/async_std.rs +++ b/futures/src/backend/native/async_std.rs @@ -27,7 +27,7 @@ pub mod time { pub fn every( duration: std::time::Duration, ) -> Subscription<std::time::Instant> { - Subscription::from_recipe(Every(duration)) + subscription::from_recipe(Every(duration)) } #[derive(Debug)] diff --git a/futures/src/backend/native/smol.rs b/futures/src/backend/native/smol.rs index aaf1518c..8d448e7f 100644 --- a/futures/src/backend/native/smol.rs +++ b/futures/src/backend/native/smol.rs @@ -26,7 +26,7 @@ pub mod time { pub fn every( duration: std::time::Duration, ) -> Subscription<std::time::Instant> { - Subscription::from_recipe(Every(duration)) + subscription::from_recipe(Every(duration)) } #[derive(Debug)] diff --git a/futures/src/backend/native/tokio.rs b/futures/src/backend/native/tokio.rs index df91d798..9dc3593d 100644 --- a/futures/src/backend/native/tokio.rs +++ b/futures/src/backend/native/tokio.rs @@ -31,7 +31,7 @@ pub mod time { pub fn every( duration: std::time::Duration, ) -> Subscription<std::time::Instant> { - Subscription::from_recipe(Every(duration)) + subscription::from_recipe(Every(duration)) } #[derive(Debug)] diff --git a/futures/src/backend/wasm/wasm_bindgen.rs b/futures/src/backend/wasm/wasm_bindgen.rs index 3228dd18..f7846c01 100644 --- a/futures/src/backend/wasm/wasm_bindgen.rs +++ b/futures/src/backend/wasm/wasm_bindgen.rs @@ -26,7 +26,7 @@ pub mod time { pub fn every( duration: std::time::Duration, ) -> Subscription<wasm_timer::Instant> { - Subscription::from_recipe(Every(duration)) + subscription::from_recipe(Every(duration)) } #[derive(Debug)] diff --git a/futures/src/lib.rs b/futures/src/lib.rs index a874a618..31738823 100644 --- a/futures/src/lib.rs +++ b/futures/src/lib.rs @@ -15,6 +15,7 @@ pub mod backend; pub mod event; pub mod executor; pub mod keyboard; +pub mod stream; pub mod subscription; pub use executor::Executor; diff --git a/futures/src/stream.rs b/futures/src/stream.rs new file mode 100644 index 00000000..af2f8c99 --- /dev/null +++ b/futures/src/stream.rs @@ -0,0 +1,46 @@ +//! Create asynchronous streams of data. +use futures::channel::mpsc; +use futures::stream::{self, Stream, StreamExt}; + +use std::future::Future; + +/// Creates a new [`Stream`] that produces the items sent from a [`Future`] +/// to the [`mpsc::Sender`] provided to the closure. +/// +/// This is a more ergonomic [`stream::unfold`], which allows you to go +/// from the "world of futures" to the "world of streams" by simply looping +/// and publishing to an async channel from inside a [`Future`]. +pub fn channel<T, F>( + size: usize, + f: impl FnOnce(mpsc::Sender<T>) -> F, +) -> impl Stream<Item = T> +where + F: Future<Output = ()>, +{ + let (sender, receiver) = mpsc::channel(size); + + let runner = stream::once(f(sender)).filter_map(|_| async { None }); + + stream::select(receiver, runner) +} + +/// Creates a new [`Stream`] that produces the items sent from a [`Future`] +/// that can fail to the [`mpsc::Sender`] provided to the closure. +pub fn try_channel<T, E, F>( + size: usize, + f: impl FnOnce(mpsc::Sender<T>) -> F, +) -> impl Stream<Item = Result<T, E>> +where + F: Future<Output = Result<(), E>>, +{ + let (sender, receiver) = mpsc::channel(size); + + let runner = stream::once(f(sender)).filter_map(|result| async { + match result { + Ok(()) => None, + Err(error) => Some(Err(error)), + } + }); + + stream::select(receiver.map(Ok), runner) +} diff --git a/futures/src/subscription.rs b/futures/src/subscription.rs index 316fc44d..1a0d454d 100644 --- a/futures/src/subscription.rs +++ b/futures/src/subscription.rs @@ -5,11 +5,9 @@ pub use tracker::Tracker; use crate::core::event; use crate::core::window; -use crate::futures::{Future, Stream}; +use crate::futures::Stream; use crate::{BoxStream, MaybeSend}; -use futures::channel::mpsc; -use futures::never::Never; use std::any::TypeId; use std::hash::Hash; @@ -61,20 +59,66 @@ pub type Hasher = rustc_hash::FxHasher; /// A request to listen to external events. /// -/// Besides performing async actions on demand with `Command`, most +/// Besides performing async actions on demand with `Task`, most /// applications also need to listen to external events passively. /// -/// A [`Subscription`] is normally provided to some runtime, like a `Command`, +/// A [`Subscription`] is normally provided to some runtime, like a `Task`, /// and it will generate events as long as the user keeps requesting it. /// /// For instance, you can use a [`Subscription`] to listen to a `WebSocket` /// connection, keyboard presses, mouse events, time ticks, etc. +/// +/// # The Lifetime of a [`Subscription`] +/// Much like a [`Future`] or a [`Stream`], a [`Subscription`] does not produce any effects +/// on its own. For a [`Subscription`] to run, it must be returned to the iced runtime—normally +/// in the `subscription` function of an `application` or a `daemon`. +/// +/// When a [`Subscription`] is provided to the runtime for the first time, the runtime will +/// start running it asynchronously. Running a [`Subscription`] consists in building its underlying +/// [`Stream`] and executing it in an async runtime. +/// +/// Therefore, you can think of a [`Subscription`] as a "stream builder". It simply represents a way +/// to build a certain [`Stream`] together with some way to _identify_ it. +/// +/// Identification is important because when a specific [`Subscription`] stops being returned to the +/// iced runtime, the runtime will kill its associated [`Stream`]. The runtime uses the identity of a +/// [`Subscription`] to keep track of it. +/// +/// This way, iced allows you to declaratively __subscribe__ to particular streams of data temporarily +/// and whenever necessary. +/// +/// ``` +/// # mod iced { +/// # pub mod time { +/// # pub use iced_futures::backend::default::time::every; +/// # pub use std::time::{Duration, Instant}; +/// # } +/// # +/// # pub use iced_futures::Subscription; +/// # } +/// use iced::time::{self, Duration, Instant}; +/// use iced::Subscription; +/// +/// struct State { +/// timer_enabled: bool, +/// } +/// +/// fn subscription(state: &State) -> Subscription<Instant> { +/// if state.timer_enabled { +/// time::every(Duration::from_secs(1)) +/// } else { +/// Subscription::none() +/// } +/// } +/// ``` +/// +/// [`Future`]: std::future::Future #[must_use = "`Subscription` must be returned to runtime to take effect"] -pub struct Subscription<Message> { - recipes: Vec<Box<dyn Recipe<Output = Message>>>, +pub struct Subscription<T> { + recipes: Vec<Box<dyn Recipe<Output = T>>>, } -impl<Message> Subscription<Message> { +impl<T> Subscription<T> { /// Returns an empty [`Subscription`] that will not produce any output. pub fn none() -> Self { Self { @@ -82,19 +126,102 @@ impl<Message> Subscription<Message> { } } - /// Creates a [`Subscription`] from a [`Recipe`] describing it. - pub fn from_recipe( - recipe: impl Recipe<Output = Message> + 'static, - ) -> Self { - Self { - recipes: vec![Box::new(recipe)], - } + /// Returns a [`Subscription`] that will call the given function to create and + /// asynchronously run the given [`Stream`]. + /// + /// # Creating an asynchronous worker with bidirectional communication + /// You can leverage this helper to create a [`Subscription`] that spawns + /// an asynchronous worker in the background and establish a channel of + /// communication with an `iced` application. + /// + /// You can achieve this by creating an `mpsc` channel inside the closure + /// and returning the `Sender` as a `Message` for the `Application`: + /// + /// ``` + /// use iced_futures::subscription::{self, Subscription}; + /// use iced_futures::stream; + /// use iced_futures::futures::channel::mpsc; + /// use iced_futures::futures::sink::SinkExt; + /// use iced_futures::futures::Stream; + /// + /// pub enum Event { + /// Ready(mpsc::Sender<Input>), + /// WorkFinished, + /// // ... + /// } + /// + /// enum Input { + /// DoSomeWork, + /// // ... + /// } + /// + /// fn some_worker() -> impl Stream<Item = Event> { + /// stream::channel(100, |mut output| async move { + /// // Create channel + /// let (sender, mut receiver) = mpsc::channel(100); + /// + /// // Send the sender back to the application + /// output.send(Event::Ready(sender)).await; + /// + /// loop { + /// use iced_futures::futures::StreamExt; + /// + /// // Read next input sent from `Application` + /// let input = receiver.select_next_some().await; + /// + /// match input { + /// Input::DoSomeWork => { + /// // Do some async work... + /// + /// // Finally, we can optionally produce a message to tell the + /// // `Application` the work is done + /// output.send(Event::WorkFinished).await; + /// } + /// } + /// } + /// }) + /// } + /// + /// fn subscription() -> Subscription<Event> { + /// Subscription::run(some_worker) + /// } + /// ``` + /// + /// Check out the [`websocket`] example, which showcases this pattern to maintain a `WebSocket` + /// connection open. + /// + /// [`websocket`]: https://github.com/iced-rs/iced/tree/0.12/examples/websocket + pub fn run<S>(builder: fn() -> S) -> Self + where + S: Stream<Item = T> + MaybeSend + 'static, + T: 'static, + { + from_recipe(Runner { + id: builder, + spawn: move |_| builder(), + }) + } + + /// Returns a [`Subscription`] that will create and asynchronously run the + /// given [`Stream`]. + /// + /// The `id` will be used to uniquely identify the [`Subscription`]. + pub fn run_with_id<I, S>(id: I, stream: S) -> Subscription<T> + where + I: Hash + 'static, + S: Stream<Item = T> + MaybeSend + 'static, + T: 'static, + { + from_recipe(Runner { + id, + spawn: move |_| stream, + }) } /// Batches all the provided subscriptions and returns the resulting /// [`Subscription`]. pub fn batch( - subscriptions: impl IntoIterator<Item = Subscription<Message>>, + subscriptions: impl IntoIterator<Item = Subscription<T>>, ) -> Self { Self { recipes: subscriptions @@ -104,18 +231,13 @@ impl<Message> Subscription<Message> { } } - /// Returns the different recipes of the [`Subscription`]. - pub fn into_recipes(self) -> Vec<Box<dyn Recipe<Output = Message>>> { - self.recipes - } - /// Adds a value to the [`Subscription`] context. /// /// The value will be part of the identity of a [`Subscription`]. - pub fn with<T>(mut self, value: T) -> Subscription<(T, Message)> + pub fn with<A>(mut self, value: A) -> Subscription<(A, T)> where - Message: 'static, - T: std::hash::Hash + Clone + Send + Sync + 'static, + T: 'static, + A: std::hash::Hash + Clone + Send + Sync + 'static, { Subscription { recipes: self @@ -123,7 +245,7 @@ impl<Message> Subscription<Message> { .drain(..) .map(|recipe| { Box::new(With::new(recipe, value.clone())) - as Box<dyn Recipe<Output = (T, Message)>> + as Box<dyn Recipe<Output = (A, T)>> }) .collect(), } @@ -136,8 +258,8 @@ impl<Message> Subscription<Message> { /// will panic in debug mode otherwise. pub fn map<F, A>(mut self, f: F) -> Subscription<A> where - Message: 'static, - F: Fn(Message) -> A + MaybeSend + Clone + 'static, + T: 'static, + F: Fn(T) -> A + MaybeSend + Clone + 'static, A: 'static, { debug_assert!( @@ -159,7 +281,23 @@ impl<Message> Subscription<Message> { } } -impl<Message> std::fmt::Debug for Subscription<Message> { +/// Creates a [`Subscription`] from a [`Recipe`] describing it. +pub fn from_recipe<T>( + recipe: impl Recipe<Output = T> + 'static, +) -> Subscription<T> { + Subscription { + recipes: vec![Box::new(recipe)], + } +} + +/// Returns the different recipes of the [`Subscription`]. +pub fn into_recipes<T>( + subscription: Subscription<T>, +) -> Vec<Box<dyn Recipe<Output = T>>> { + subscription.recipes +} + +impl<T> std::fmt::Debug for Subscription<T> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("Subscription").finish() } @@ -273,65 +411,13 @@ where } } -/// Returns a [`Subscription`] that will call the given function to create and -/// asynchronously run the given [`Stream`]. -pub fn run<S, Message>(builder: fn() -> S) -> Subscription<Message> -where - S: Stream<Item = Message> + MaybeSend + 'static, - Message: 'static, -{ - Subscription::from_recipe(Runner { - id: builder, - spawn: move |_| builder(), - }) -} - -/// Returns a [`Subscription`] that will create and asynchronously run the -/// given [`Stream`]. -/// -/// The `id` will be used to uniquely identify the [`Subscription`]. -pub fn run_with_id<I, S, Message>(id: I, stream: S) -> Subscription<Message> -where - I: Hash + 'static, - S: Stream<Item = Message> + MaybeSend + 'static, - Message: 'static, -{ - Subscription::from_recipe(Runner { - id, - spawn: move |_| stream, - }) -} - -/// Returns a [`Subscription`] that will create and asynchronously run a -/// [`Stream`] that will call the provided closure to produce every `Message`. -/// -/// The `id` will be used to uniquely identify the [`Subscription`]. -pub fn unfold<I, T, Fut, Message>( - id: I, - initial: T, - mut f: impl FnMut(T) -> Fut + MaybeSend + Sync + 'static, -) -> Subscription<Message> +pub(crate) fn filter_map<I, F, T>(id: I, f: F) -> Subscription<T> where I: Hash + 'static, - T: MaybeSend + 'static, - Fut: Future<Output = (Message, T)> + MaybeSend + 'static, - Message: 'static + MaybeSend, + F: Fn(Event) -> Option<T> + MaybeSend + 'static, + T: 'static + MaybeSend, { - use futures::future::FutureExt; - - run_with_id( - id, - futures::stream::unfold(initial, move |state| f(state).map(Some)), - ) -} - -pub(crate) fn filter_map<I, F, Message>(id: I, f: F) -> Subscription<Message> -where - I: Hash + 'static, - F: Fn(Event) -> Option<Message> + MaybeSend + 'static, - Message: 'static + MaybeSend, -{ - Subscription::from_recipe(Runner { + from_recipe(Runner { id, spawn: |events| { use futures::future; @@ -342,122 +428,22 @@ where }) } -/// Creates a [`Subscription`] that publishes the events sent from a [`Future`] -/// to an [`mpsc::Sender`] with the given bounds. -/// -/// # Creating an asynchronous worker with bidirectional communication -/// You can leverage this helper to create a [`Subscription`] that spawns -/// an asynchronous worker in the background and establish a channel of -/// communication with an `iced` application. -/// -/// You can achieve this by creating an `mpsc` channel inside the closure -/// and returning the `Sender` as a `Message` for the `Application`: -/// -/// ``` -/// use iced_futures::subscription::{self, Subscription}; -/// use iced_futures::futures::channel::mpsc; -/// use iced_futures::futures::sink::SinkExt; -/// -/// pub enum Event { -/// Ready(mpsc::Sender<Input>), -/// WorkFinished, -/// // ... -/// } -/// -/// enum Input { -/// DoSomeWork, -/// // ... -/// } -/// -/// enum State { -/// Starting, -/// Ready(mpsc::Receiver<Input>), -/// } -/// -/// fn some_worker() -> Subscription<Event> { -/// struct SomeWorker; -/// -/// subscription::channel(std::any::TypeId::of::<SomeWorker>(), 100, |mut output| async move { -/// let mut state = State::Starting; -/// -/// loop { -/// match &mut state { -/// State::Starting => { -/// // Create channel -/// let (sender, receiver) = mpsc::channel(100); -/// -/// // Send the sender back to the application -/// output.send(Event::Ready(sender)).await; -/// -/// // We are ready to receive messages -/// state = State::Ready(receiver); -/// } -/// State::Ready(receiver) => { -/// use iced_futures::futures::StreamExt; -/// -/// // Read next input sent from `Application` -/// let input = receiver.select_next_some().await; -/// -/// match input { -/// Input::DoSomeWork => { -/// // Do some async work... -/// -/// // Finally, we can optionally produce a message to tell the -/// // `Application` the work is done -/// output.send(Event::WorkFinished).await; -/// } -/// } -/// } -/// } -/// } -/// }) -/// } -/// ``` -/// -/// Check out the [`websocket`] example, which showcases this pattern to maintain a `WebSocket` -/// connection open. -/// -/// [`websocket`]: https://github.com/iced-rs/iced/tree/0.12/examples/websocket -pub fn channel<I, Fut, Message>( - id: I, - size: usize, - f: impl FnOnce(mpsc::Sender<Message>) -> Fut + MaybeSend + 'static, -) -> Subscription<Message> -where - I: Hash + 'static, - Fut: Future<Output = Never> + MaybeSend + 'static, - Message: 'static + MaybeSend, -{ - use futures::stream::{self, StreamExt}; - - Subscription::from_recipe(Runner { - id, - spawn: move |_| { - let (sender, receiver) = mpsc::channel(size); - - let runner = stream::once(f(sender)).map(|_| unreachable!()); - - stream::select(receiver, runner) - }, - }) -} - -struct Runner<I, F, S, Message> +struct Runner<I, F, S, T> where F: FnOnce(EventStream) -> S, - S: Stream<Item = Message>, + S: Stream<Item = T>, { id: I, spawn: F, } -impl<I, S, F, Message> Recipe for Runner<I, F, S, Message> +impl<I, F, S, T> Recipe for Runner<I, F, S, T> where I: Hash + 'static, F: FnOnce(EventStream) -> S, - S: Stream<Item = Message> + MaybeSend + 'static, + S: Stream<Item = T> + MaybeSend + 'static, { - type Output = Message; + type Output = T; fn hash(&self, state: &mut Hasher) { std::any::TypeId::of::<I>().hash(state); diff --git a/graphics/src/text/editor.rs b/graphics/src/text/editor.rs index c488a51c..36b4ca6e 100644 --- a/graphics/src/text/editor.rs +++ b/graphics/src/text/editor.rs @@ -385,6 +385,27 @@ impl editor::Editor for Editor { })); } } + Action::SelectAll => { + let buffer = editor.buffer(); + if buffer.lines.len() > 1 + || buffer + .lines + .first() + .is_some_and(|line| !line.text().is_empty()) + { + let cursor = editor.cursor(); + editor.set_select_opt(Some(cosmic_text::Cursor { + line: 0, + index: 0, + ..cursor + })); + + editor.action( + font_system.raw(), + motion_to_action(Motion::DocumentEnd), + ); + } + } // Editing events Action::Edit(edit) => { diff --git a/runtime/src/clipboard.rs b/runtime/src/clipboard.rs index 19950d01..a02cc011 100644 --- a/runtime/src/clipboard.rs +++ b/runtime/src/clipboard.rs @@ -1,7 +1,7 @@ //! Access the clipboard. use crate::core::clipboard::Kind; use crate::futures::futures::channel::oneshot; -use crate::Task; +use crate::task::{self, Task}; /// A clipboard action to be performed by some [`Task`]. /// @@ -27,7 +27,7 @@ pub enum Action { /// Read the current contents of the clipboard. pub fn read() -> Task<Option<String>> { - Task::oneshot(|channel| { + task::oneshot(|channel| { crate::Action::Clipboard(Action::Read { target: Kind::Standard, channel, @@ -37,7 +37,7 @@ pub fn read() -> Task<Option<String>> { /// Read the current contents of the primary clipboard. pub fn read_primary() -> Task<Option<String>> { - Task::oneshot(|channel| { + task::oneshot(|channel| { crate::Action::Clipboard(Action::Read { target: Kind::Primary, channel, @@ -47,7 +47,7 @@ pub fn read_primary() -> Task<Option<String>> { /// Write the given contents to the clipboard. pub fn write<T>(contents: String) -> Task<T> { - Task::effect(crate::Action::Clipboard(Action::Write { + task::effect(crate::Action::Clipboard(Action::Write { target: Kind::Standard, contents, })) @@ -55,7 +55,7 @@ pub fn write<T>(contents: String) -> Task<T> { /// Write the given contents to the primary clipboard. pub fn write_primary<Message>(contents: String) -> Task<Message> { - Task::effect(crate::Action::Clipboard(Action::Write { + task::effect(crate::Action::Clipboard(Action::Write { target: Kind::Primary, contents, })) diff --git a/runtime/src/font.rs b/runtime/src/font.rs index d54eb6a8..75fdfc11 100644 --- a/runtime/src/font.rs +++ b/runtime/src/font.rs @@ -1,5 +1,6 @@ //! Load and use fonts. -use crate::{Action, Task}; +use crate::task::{self, Task}; +use crate::Action; use std::borrow::Cow; /// An error while loading a font. @@ -8,7 +9,7 @@ pub enum Error {} /// Load a font from its bytes. pub fn load(bytes: impl Into<Cow<'static, [u8]>>) -> Task<Result<(), Error>> { - Task::oneshot(|channel| Action::LoadFont { + task::oneshot(|channel| Action::LoadFont { bytes: bytes.into(), channel, }) diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 7e46593a..f27657d1 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -15,14 +15,13 @@ pub mod keyboard; pub mod overlay; pub mod program; pub mod system; +pub mod task; pub mod user_interface; pub mod window; #[cfg(feature = "multi-window")] pub mod multi_window; -mod task; - // We disable debug capabilities on release builds unless the `debug` feature // is explicitly enabled. #[cfg(feature = "debug")] @@ -127,5 +126,5 @@ where /// This will normally close any application windows and /// terminate the runtime loop. pub fn exit<T>() -> Task<T> { - Task::effect(Action::Exit) + task::effect(Action::Exit) } diff --git a/runtime/src/task.rs b/runtime/src/task.rs index b8a83d6d..b75aca89 100644 --- a/runtime/src/task.rs +++ b/runtime/src/task.rs @@ -1,3 +1,4 @@ +//! Create runtime tasks. use crate::core::widget; use crate::futures::futures::channel::mpsc; use crate::futures::futures::channel::oneshot; @@ -29,24 +30,6 @@ impl<T> Task<T> { Self::future(future::ready(value)) } - /// Creates a new [`Task`] that runs the given [`Future`] and produces - /// its output. - pub fn future(future: impl Future<Output = T> + MaybeSend + 'static) -> Self - where - T: 'static, - { - Self::stream(stream::once(future)) - } - - /// Creates a new [`Task`] that runs the given [`Stream`] and produces - /// each of its items. - pub fn stream(stream: impl Stream<Item = T> + MaybeSend + 'static) -> Self - where - T: 'static, - { - Self(Some(boxed_stream(stream.map(Action::Output)))) - } - /// Creates a [`Task`] that runs the given [`Future`] to completion and maps its /// output with the given closure. pub fn perform<A>( @@ -83,66 +66,6 @@ impl<T> Task<T> { )))) } - /// Creates a new [`Task`] that runs the given [`widget::Operation`] and produces - /// its output. - pub fn widget(operation: impl widget::Operation<T> + 'static) -> Task<T> - where - T: Send + 'static, - { - Self::channel(move |sender| { - let operation = - widget::operation::map(Box::new(operation), move |value| { - let _ = sender.clone().try_send(value); - }); - - Action::Widget(Box::new(operation)) - }) - } - - /// Creates a new [`Task`] that executes the [`Action`] returned by the closure and - /// produces the value fed to the [`oneshot::Sender`]. - pub fn oneshot(f: impl FnOnce(oneshot::Sender<T>) -> Action<T>) -> Task<T> - where - T: MaybeSend + 'static, - { - let (sender, receiver) = oneshot::channel(); - - let action = f(sender); - - Self(Some(boxed_stream( - stream::once(async move { action }).chain( - receiver.into_stream().filter_map(|result| async move { - Some(Action::Output(result.ok()?)) - }), - ), - ))) - } - - /// Creates a new [`Task`] that executes the [`Action`] returned by the closure and - /// produces the values fed to the [`mpsc::Sender`]. - pub fn channel(f: impl FnOnce(mpsc::Sender<T>) -> Action<T>) -> Task<T> - where - T: MaybeSend + 'static, - { - let (sender, receiver) = mpsc::channel(1); - - let action = f(sender); - - Self(Some(boxed_stream( - stream::once(async move { action }) - .chain(receiver.map(|result| Action::Output(result))), - ))) - } - - /// Creates a new [`Task`] that executes the given [`Action`] and produces no output. - pub fn effect(action: impl Into<Action<Never>>) -> Self { - let action = action.into(); - - Self(Some(boxed_stream(stream::once(async move { - action.output().expect_err("no output") - })))) - } - /// Maps the output of a [`Task`] with the given closure. pub fn map<O>( self, @@ -236,9 +159,59 @@ impl<T> Task<T> { } } - /// Returns the underlying [`Stream`] of the [`Task`]. - pub fn into_stream(self) -> Option<BoxStream<Action<T>>> { - self.0 + /// Creates a new [`Task`] that can be aborted with the returned [`Handle`]. + pub fn abortable(self) -> (Self, Handle) + where + T: 'static, + { + match self.0 { + Some(stream) => { + let (stream, handle) = stream::abortable(stream); + + (Self(Some(boxed_stream(stream))), Handle(Some(handle))) + } + None => (Self(None), Handle(None)), + } + } + + /// Creates a new [`Task`] that runs the given [`Future`] and produces + /// its output. + pub fn future(future: impl Future<Output = T> + MaybeSend + 'static) -> Self + where + T: 'static, + { + Self::stream(stream::once(future)) + } + + /// Creates a new [`Task`] that runs the given [`Stream`] and produces + /// each of its items. + pub fn stream(stream: impl Stream<Item = T> + MaybeSend + 'static) -> Self + where + T: 'static, + { + Self(Some(boxed_stream(stream.map(Action::Output)))) + } +} + +/// A handle to a [`Task`] that can be used for aborting it. +#[derive(Debug, Clone)] +pub struct Handle(Option<stream::AbortHandle>); + +impl Handle { + /// Aborts the [`Task`] of this [`Handle`]. + pub fn abort(&self) { + if let Some(handle) = &self.0 { + handle.abort(); + } + } + + /// Returns `true` if the [`Task`] of this [`Handle`] has been aborted. + pub fn is_aborted(&self) -> bool { + if let Some(handle) = &self.0 { + handle.is_aborted() + } else { + true + } } } @@ -283,3 +256,68 @@ where Self::none() } } + +/// Creates a new [`Task`] that runs the given [`widget::Operation`] and produces +/// its output. +pub fn widget<T>(operation: impl widget::Operation<T> + 'static) -> Task<T> +where + T: Send + 'static, +{ + channel(move |sender| { + let operation = + widget::operation::map(Box::new(operation), move |value| { + let _ = sender.clone().try_send(value); + }); + + Action::Widget(Box::new(operation)) + }) +} + +/// Creates a new [`Task`] that executes the [`Action`] returned by the closure and +/// produces the value fed to the [`oneshot::Sender`]. +pub fn oneshot<T>(f: impl FnOnce(oneshot::Sender<T>) -> Action<T>) -> Task<T> +where + T: MaybeSend + 'static, +{ + let (sender, receiver) = oneshot::channel(); + + let action = f(sender); + + Task(Some(boxed_stream( + stream::once(async move { action }).chain( + receiver.into_stream().filter_map(|result| async move { + Some(Action::Output(result.ok()?)) + }), + ), + ))) +} + +/// Creates a new [`Task`] that executes the [`Action`] returned by the closure and +/// produces the values fed to the [`mpsc::Sender`]. +pub fn channel<T>(f: impl FnOnce(mpsc::Sender<T>) -> Action<T>) -> Task<T> +where + T: MaybeSend + 'static, +{ + let (sender, receiver) = mpsc::channel(1); + + let action = f(sender); + + Task(Some(boxed_stream( + stream::once(async move { action }) + .chain(receiver.map(|result| Action::Output(result))), + ))) +} + +/// Creates a new [`Task`] that executes the given [`Action`] and produces no output. +pub fn effect<T>(action: impl Into<Action<Never>>) -> Task<T> { + let action = action.into(); + + Task(Some(boxed_stream(stream::once(async move { + action.output().expect_err("no output") + })))) +} + +/// Returns the underlying [`Stream`] of the [`Task`]. +pub fn into_stream<T>(task: Task<T>) -> Option<BoxStream<Action<T>>> { + task.0 +} diff --git a/runtime/src/window.rs b/runtime/src/window.rs index 3e53dd55..815827d1 100644 --- a/runtime/src/window.rs +++ b/runtime/src/window.rs @@ -11,7 +11,7 @@ use crate::core::{Point, Size}; use crate::futures::event; use crate::futures::futures::channel::oneshot; use crate::futures::Subscription; -use crate::Task; +use crate::task::{self, Task}; pub use raw_window_handle; @@ -210,99 +210,99 @@ pub fn close_requests() -> Subscription<Id> { pub fn open(settings: Settings) -> Task<Id> { let id = Id::unique(); - Task::oneshot(|channel| { + task::oneshot(|channel| { crate::Action::Window(Action::Open(id, settings, channel)) }) } /// Closes the window with `id`. pub fn close<T>(id: Id) -> Task<T> { - Task::effect(crate::Action::Window(Action::Close(id))) + task::effect(crate::Action::Window(Action::Close(id))) } /// Gets the window [`Id`] of the oldest window. pub fn get_oldest() -> Task<Option<Id>> { - Task::oneshot(|channel| crate::Action::Window(Action::GetOldest(channel))) + task::oneshot(|channel| crate::Action::Window(Action::GetOldest(channel))) } /// Gets the window [`Id`] of the latest window. pub fn get_latest() -> Task<Option<Id>> { - Task::oneshot(|channel| crate::Action::Window(Action::GetLatest(channel))) + task::oneshot(|channel| crate::Action::Window(Action::GetLatest(channel))) } /// Begins dragging the window while the left mouse button is held. pub fn drag<T>(id: Id) -> Task<T> { - Task::effect(crate::Action::Window(Action::Drag(id))) + task::effect(crate::Action::Window(Action::Drag(id))) } /// Resizes the window to the given logical dimensions. pub fn resize<T>(id: Id, new_size: Size) -> Task<T> { - Task::effect(crate::Action::Window(Action::Resize(id, new_size))) + task::effect(crate::Action::Window(Action::Resize(id, new_size))) } /// Get the window's size in logical dimensions. pub fn get_size(id: Id) -> Task<Size> { - Task::oneshot(move |channel| { + task::oneshot(move |channel| { crate::Action::Window(Action::GetSize(id, channel)) }) } /// Gets the maximized state of the window with the given [`Id`]. pub fn get_maximized(id: Id) -> Task<bool> { - Task::oneshot(move |channel| { + task::oneshot(move |channel| { crate::Action::Window(Action::GetMaximized(id, channel)) }) } /// Maximizes the window. pub fn maximize<T>(id: Id, maximized: bool) -> Task<T> { - Task::effect(crate::Action::Window(Action::Maximize(id, maximized))) + task::effect(crate::Action::Window(Action::Maximize(id, maximized))) } /// Gets the minimized state of the window with the given [`Id`]. pub fn get_minimized(id: Id) -> Task<Option<bool>> { - Task::oneshot(move |channel| { + task::oneshot(move |channel| { crate::Action::Window(Action::GetMinimized(id, channel)) }) } /// Minimizes the window. pub fn minimize<T>(id: Id, minimized: bool) -> Task<T> { - Task::effect(crate::Action::Window(Action::Minimize(id, minimized))) + task::effect(crate::Action::Window(Action::Minimize(id, minimized))) } /// Gets the position in logical coordinates of the window with the given [`Id`]. pub fn get_position(id: Id) -> Task<Option<Point>> { - Task::oneshot(move |channel| { + task::oneshot(move |channel| { crate::Action::Window(Action::GetPosition(id, channel)) }) } /// Moves the window to the given logical coordinates. pub fn move_to<T>(id: Id, position: Point) -> Task<T> { - Task::effect(crate::Action::Window(Action::Move(id, position))) + task::effect(crate::Action::Window(Action::Move(id, position))) } /// Changes the [`Mode`] of the window. pub fn change_mode<T>(id: Id, mode: Mode) -> Task<T> { - Task::effect(crate::Action::Window(Action::ChangeMode(id, mode))) + task::effect(crate::Action::Window(Action::ChangeMode(id, mode))) } /// Gets the current [`Mode`] of the window. pub fn get_mode(id: Id) -> Task<Mode> { - Task::oneshot(move |channel| { + task::oneshot(move |channel| { crate::Action::Window(Action::GetMode(id, channel)) }) } /// Toggles the window to maximized or back. pub fn toggle_maximize<T>(id: Id) -> Task<T> { - Task::effect(crate::Action::Window(Action::ToggleMaximize(id))) + task::effect(crate::Action::Window(Action::ToggleMaximize(id))) } /// Toggles the window decorations. pub fn toggle_decorations<T>(id: Id) -> Task<T> { - Task::effect(crate::Action::Window(Action::ToggleDecorations(id))) + task::effect(crate::Action::Window(Action::ToggleDecorations(id))) } /// Request user attention to the window. This has no effect if the application @@ -315,7 +315,7 @@ pub fn request_user_attention<T>( id: Id, user_attention: Option<UserAttention>, ) -> Task<T> { - Task::effect(crate::Action::Window(Action::RequestUserAttention( + task::effect(crate::Action::Window(Action::RequestUserAttention( id, user_attention, ))) @@ -328,32 +328,32 @@ pub fn request_user_attention<T>( /// you are certain that's what the user wants. Focus stealing can cause an extremely disruptive /// user experience. pub fn gain_focus<T>(id: Id) -> Task<T> { - Task::effect(crate::Action::Window(Action::GainFocus(id))) + task::effect(crate::Action::Window(Action::GainFocus(id))) } /// Changes the window [`Level`]. pub fn change_level<T>(id: Id, level: Level) -> Task<T> { - Task::effect(crate::Action::Window(Action::ChangeLevel(id, level))) + task::effect(crate::Action::Window(Action::ChangeLevel(id, level))) } /// Show the [system menu] at cursor position. /// /// [system menu]: https://en.wikipedia.org/wiki/Common_menus_in_Microsoft_Windows#System_menu pub fn show_system_menu<T>(id: Id) -> Task<T> { - Task::effect(crate::Action::Window(Action::ShowSystemMenu(id))) + task::effect(crate::Action::Window(Action::ShowSystemMenu(id))) } /// Gets an identifier unique to the window, provided by the underlying windowing system. This is /// not to be confused with [`Id`]. pub fn get_raw_id<Message>(id: Id) -> Task<u64> { - Task::oneshot(|channel| { + task::oneshot(|channel| { crate::Action::Window(Action::GetRawId(id, channel)) }) } /// Changes the [`Icon`] of the window. pub fn change_icon<T>(id: Id, icon: Icon) -> Task<T> { - Task::effect(crate::Action::Window(Action::ChangeIcon(id, icon))) + task::effect(crate::Action::Window(Action::ChangeIcon(id, icon))) } /// Runs the given callback with the native window handle for the window with the given id. @@ -366,7 +366,7 @@ pub fn run_with_handle<T>( where T: Send + 'static, { - Task::oneshot(move |channel| { + task::oneshot(move |channel| { crate::Action::Window(Action::RunWithHandle( id, Box::new(move |handle| { @@ -378,7 +378,7 @@ where /// Captures a [`Screenshot`] from the window. pub fn screenshot(id: Id) -> Task<Screenshot> { - Task::oneshot(move |channel| { + task::oneshot(move |channel| { crate::Action::Window(Action::Screenshot(id, channel)) }) } diff --git a/src/advanced.rs b/src/advanced.rs index 8d06e805..b817bbf9 100644 --- a/src/advanced.rs +++ b/src/advanced.rs @@ -14,6 +14,6 @@ pub use crate::renderer::graphics; pub mod subscription { //! Write your own subscriptions. pub use crate::runtime::futures::subscription::{ - EventStream, Hasher, Recipe, + from_recipe, into_recipes, EventStream, Hasher, Recipe, }; } diff --git a/src/application.rs b/src/application.rs index 5d16b40f..f5e06471 100644 --- a/src/application.rs +++ b/src/application.rs @@ -103,10 +103,6 @@ where type Renderer = Renderer; type Executor = iced_futures::backend::default::Executor; - fn load(&self) -> Task<Self::Message> { - Task::none() - } - fn update( &self, state: &mut Self::State, @@ -166,14 +162,14 @@ impl<P: Program> Application<P> { Self: 'static, P::State: Default, { - self.run_with(P::State::default) + self.raw.run(self.settings, Some(self.window)) } /// Runs the [`Application`] with a closure that creates the initial state. pub fn run_with<I>(self, initialize: I) -> Result where Self: 'static, - I: Fn() -> P::State + Clone + 'static, + I: Fn() -> (P::State, Task<P::Message>) + Clone + 'static, { self.raw .run_with(self.settings, Some(self.window), initialize) @@ -323,20 +319,6 @@ impl<P: Program> Application<P> { } } - /// Runs the [`Task`] produced by the closure at startup. - pub fn load( - self, - f: impl Fn() -> Task<P::Message>, - ) -> Application< - impl Program<State = P::State, Message = P::Message, Theme = P::Theme>, - > { - Application { - raw: program::with_load(self.raw, f), - settings: self.settings, - window: self.window, - } - } - /// Sets the subscription logic of the [`Application`]. pub fn subscription( self, diff --git a/src/daemon.rs b/src/daemon.rs index 58293949..d2de2db7 100644 --- a/src/daemon.rs +++ b/src/daemon.rs @@ -55,10 +55,6 @@ where type Renderer = Renderer; type Executor = iced_futures::backend::default::Executor; - fn load(&self) -> Task<Self::Message> { - Task::none() - } - fn update( &self, state: &mut Self::State, @@ -116,14 +112,14 @@ impl<P: Program> Daemon<P> { Self: 'static, P::State: Default, { - self.run_with(P::State::default) + self.raw.run(self.settings, None) } /// Runs the [`Daemon`] with a closure that creates the initial state. pub fn run_with<I>(self, initialize: I) -> Result where Self: 'static, - I: Fn() -> P::State + Clone + 'static, + I: Fn() -> (P::State, Task<P::Message>) + Clone + 'static, { self.raw.run_with(self.settings, None, initialize) } @@ -176,19 +172,6 @@ impl<P: Program> Daemon<P> { } } - /// Runs the [`Task`] produced by the closure at startup. - pub fn load( - self, - f: impl Fn() -> Task<P::Message>, - ) -> Daemon< - impl Program<State = P::State, Message = P::Message, Theme = P::Theme>, - > { - Daemon { - raw: program::with_load(self.raw, f), - settings: self.settings, - } - } - /// Sets the subscription logic of the [`Daemon`]. pub fn subscription( self, @@ -175,6 +175,7 @@ use iced_winit::core; use iced_winit::runtime; pub use iced_futures::futures; +pub use iced_futures::stream; #[cfg(feature = "highlighter")] pub use iced_highlighter as highlighter; @@ -201,7 +202,13 @@ pub use crate::core::{ Length, Padding, Pixels, Point, Radians, Rectangle, Rotation, Shadow, Size, Theme, Transformation, Vector, }; -pub use crate::runtime::{exit, Task}; +pub use crate::runtime::exit; +pub use iced_futures::Subscription; + +pub mod task { + //! Create runtime tasks. + pub use crate::runtime::task::{Handle, Task}; +} pub mod clipboard { //! Access the clipboard. @@ -255,13 +262,6 @@ pub mod mouse { }; } -pub mod subscription { - //! Listen to external events in your application. - pub use iced_futures::subscription::{ - channel, run, run_with_id, unfold, Subscription, - }; -} - #[cfg(feature = "system")] pub mod system { //! Retrieve system information. @@ -314,7 +314,7 @@ pub use executor::Executor; pub use font::Font; pub use renderer::Renderer; pub use settings::Settings; -pub use subscription::Subscription; +pub use task::Task; #[doc(inline)] pub use application::application; diff --git a/src/program.rs b/src/program.rs index 3f9d2d0c..939b0047 100644 --- a/src/program.rs +++ b/src/program.rs @@ -27,8 +27,6 @@ pub trait Program: Sized { /// The executor of the program. type Executor: Executor; - fn load(&self) -> Task<Self::Message>; - fn update( &self, state: &mut Self::State, @@ -80,7 +78,9 @@ pub trait Program: Sized { Self: 'static, Self::State: Default, { - self.run_with(settings, window_settings, Self::State::default) + self.run_with(settings, window_settings, || { + (Self::State::default(), Task::none()) + }) } /// Runs the [`Program`] with the given [`Settings`] and a closure that creates the initial state. @@ -92,7 +92,7 @@ pub trait Program: Sized { ) -> Result where Self: 'static, - I: Fn() -> Self::State + Clone + 'static, + I: Fn() -> (Self::State, Task<Self::Message>) + Clone + 'static, { use std::marker::PhantomData; @@ -102,7 +102,9 @@ pub trait Program: Sized { _initialize: PhantomData<I>, } - impl<P: Program, I: Fn() -> P::State> shell::Program for Instance<P, I> { + impl<P: Program, I: Fn() -> (P::State, Task<P::Message>)> shell::Program + for Instance<P, I> + { type Message = P::Message; type Theme = P::Theme; type Renderer = P::Renderer; @@ -112,8 +114,7 @@ pub trait Program: Sized { fn new( (program, initialize): Self::Flags, ) -> (Self, Task<Self::Message>) { - let state = initialize(); - let command = program.load(); + let (state, task) = initialize(); ( Self { @@ -121,7 +122,7 @@ pub trait Program: Sized { state, _initialize: PhantomData, }, - command, + task, ) } @@ -212,10 +213,6 @@ pub fn with_title<P: Program>( type Renderer = P::Renderer; type Executor = P::Executor; - fn load(&self) -> Task<Self::Message> { - self.program.load() - } - fn title(&self, state: &Self::State, window: window::Id) -> String { (self.title)(state, window) } @@ -267,80 +264,6 @@ pub fn with_title<P: Program>( WithTitle { program, title } } -pub fn with_load<P: Program>( - program: P, - f: impl Fn() -> Task<P::Message>, -) -> impl Program<State = P::State, Message = P::Message, Theme = P::Theme> { - struct WithLoad<P, F> { - program: P, - load: F, - } - - impl<P: Program, F> Program for WithLoad<P, F> - where - F: Fn() -> Task<P::Message>, - { - type State = P::State; - type Message = P::Message; - type Theme = P::Theme; - type Renderer = P::Renderer; - type Executor = P::Executor; - - fn load(&self) -> Task<Self::Message> { - Task::batch([self.program.load(), (self.load)()]) - } - - fn update( - &self, - state: &mut Self::State, - message: Self::Message, - ) -> Task<Self::Message> { - self.program.update(state, message) - } - - fn view<'a>( - &self, - state: &'a Self::State, - window: window::Id, - ) -> Element<'a, Self::Message, Self::Theme, Self::Renderer> { - self.program.view(state, window) - } - - fn title(&self, state: &Self::State, window: window::Id) -> String { - self.program.title(state, window) - } - - fn subscription( - &self, - state: &Self::State, - ) -> Subscription<Self::Message> { - self.program.subscription(state) - } - - fn theme( - &self, - state: &Self::State, - window: window::Id, - ) -> Self::Theme { - self.program.theme(state, window) - } - - fn style( - &self, - state: &Self::State, - theme: &Self::Theme, - ) -> Appearance { - self.program.style(state, theme) - } - - fn scale_factor(&self, state: &Self::State, window: window::Id) -> f64 { - self.program.scale_factor(state, window) - } - } - - WithLoad { program, load: f } -} - pub fn with_subscription<P: Program>( program: P, f: impl Fn(&P::State) -> Subscription<P::Message>, @@ -367,10 +290,6 @@ pub fn with_subscription<P: Program>( (self.subscription)(state) } - fn load(&self) -> Task<Self::Message> { - self.program.load() - } - fn update( &self, state: &mut Self::State, @@ -445,10 +364,6 @@ pub fn with_theme<P: Program>( (self.theme)(state, window) } - fn load(&self) -> Task<Self::Message> { - self.program.load() - } - fn title(&self, state: &Self::State, window: window::Id) -> String { self.program.title(state, window) } @@ -519,10 +434,6 @@ pub fn with_style<P: Program>( (self.style)(state, theme) } - fn load(&self) -> Task<Self::Message> { - self.program.load() - } - fn title(&self, state: &Self::State, window: window::Id) -> String { self.program.title(state, window) } @@ -585,10 +496,6 @@ pub fn with_scale_factor<P: Program>( type Renderer = P::Renderer; type Executor = P::Executor; - fn load(&self) -> Task<Self::Message> { - self.program.load() - } - fn title(&self, state: &Self::State, window: window::Id) -> String { self.program.title(state, window) } diff --git a/widget/Cargo.toml b/widget/Cargo.toml index 3c9f6a54..498a768b 100644 --- a/widget/Cargo.toml +++ b/widget/Cargo.toml @@ -31,6 +31,7 @@ iced_renderer.workspace = true iced_runtime.workspace = true num-traits.workspace = true +once_cell.workspace = true rustc-hash.workspace = true thiserror.workspace = true unicode-segmentation.workspace = true diff --git a/widget/assets/iced-logo.svg b/widget/assets/iced-logo.svg new file mode 100644 index 00000000..459b7fbb --- /dev/null +++ b/widget/assets/iced-logo.svg @@ -0,0 +1,2 @@ +<?xml version="1.0" encoding="UTF-8"?> +<svg xmlns="http://www.w3.org/2000/svg" width="140" height="140" fill="none" version="1.1" viewBox="35 31 179 171"><rect x="42" y="31.001" width="169.9" height="169.9" rx="49.815" fill="url(#paint1_linear)"/><path d="m182.62 65.747-28.136 28.606-6.13-6.0291 28.136-28.606 6.13 6.0291zm-26.344 0.218-42.204 42.909-6.13-6.029 42.204-42.909 6.13 6.0291zm-61.648 23.913c5.3254-5.3831 10.65-10.765 21.569-21.867l6.13 6.0291c-10.927 11.11-16.258 16.498-21.587 21.885-4.4007 4.4488-8.8009 8.8968-16.359 16.573l31.977 8.358 25.968-26.402 6.13 6.0292-25.968 26.402 8.907 31.908 42.138-42.087 6.076 6.083-49.109 49.05-45.837-12.628-13.394-45.646 1.7714-1.801c10.928-11.111 16.258-16.499 21.588-21.886zm28.419 70.99-8.846-31.689-31.831-8.32 9.1945 31.335 31.482 8.674zm47.734-56.517 7.122-7.1221-6.08-6.0797-7.147 7.1474-30.171 30.674 6.13 6.029 30.146-30.649z" clip-rule="evenodd" fill="url(#paint2_linear)" fill-rule="evenodd"/><defs><filter id="filter0_f" x="55" y="47.001" width="144" height="168" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur" stdDeviation="2"/></filter><linearGradient id="paint0_linear" x1="127" x2="127" y1="51.001" y2="211" gradientUnits="userSpaceOnUse"><stop offset=".052083"/><stop stop-opacity=".08" offset="1"/></linearGradient><linearGradient id="paint1_linear" x1="212" x2="57.5" y1="31.001" y2="189" gradientUnits="userSpaceOnUse"><stop stop-color="#00A3FF" offset="0"/><stop stop-color="#30f" offset="1"/></linearGradient><linearGradient id="paint2_linear" x1="86.098" x2="206.01" y1="158.28" y2="35.327" gradientUnits="userSpaceOnUse"><stop stop-color="#fff" offset="0"/><stop stop-color="#fff" offset="1"/></linearGradient></defs></svg> diff --git a/widget/src/combo_box.rs b/widget/src/combo_box.rs index 253850df..0a4624cb 100644 --- a/widget/src/combo_box.rs +++ b/widget/src/combo_box.rs @@ -280,6 +280,15 @@ where } } +impl<T> Default for State<T> +where + T: Display + Clone, +{ + fn default() -> Self { + Self::new(Vec::new()) + } +} + impl<T> Filtered<T> where T: Clone, diff --git a/widget/src/container.rs b/widget/src/container.rs index e917471f..08d5cb17 100644 --- a/widget/src/container.rs +++ b/widget/src/container.rs @@ -13,7 +13,7 @@ use crate::core::{ Padding, Pixels, Point, Rectangle, Shadow, Shell, Size, Theme, Vector, Widget, }; -use crate::runtime::Task; +use crate::runtime::task::{self, Task}; /// An element decorating some content. /// @@ -538,7 +538,7 @@ pub fn visible_bounds(id: Id) -> Task<Option<Rectangle>> { } } - Task::widget(VisibleBounds { + task::widget(VisibleBounds { target: id.into(), depth: 0, scrollables: Vec::new(), diff --git a/widget/src/helpers.rs b/widget/src/helpers.rs index 62343a55..d7631959 100644 --- a/widget/src/helpers.rs +++ b/widget/src/helpers.rs @@ -12,7 +12,8 @@ use crate::pick_list::{self, PickList}; use crate::progress_bar::{self, ProgressBar}; use crate::radio::{self, Radio}; use crate::rule::{self, Rule}; -use crate::runtime::{Action, Task}; +use crate::runtime::task::{self, Task}; +use crate::runtime::Action; use crate::scrollable::{self, Scrollable}; use crate::slider::{self, Slider}; use crate::text::{self, Text}; @@ -889,6 +890,41 @@ where crate::Svg::new(handle) } +/// Creates an [`Element`] that displays the iced logo with the given `text_size`. +/// +/// Useful for showing some love to your favorite GUI library in your "About" screen, +/// for instance. +#[cfg(feature = "svg")] +pub fn iced<'a, Message, Theme, Renderer>( + text_size: impl Into<Pixels>, +) -> Element<'a, Message, Theme, Renderer> +where + Message: 'a, + Renderer: core::Renderer + + core::text::Renderer<Font = core::Font> + + core::svg::Renderer + + 'a, + Theme: text::Catalog + crate::svg::Catalog + 'a, +{ + use crate::core::{Alignment, Font}; + use crate::svg; + use once_cell::sync::Lazy; + + static LOGO: Lazy<svg::Handle> = Lazy::new(|| { + svg::Handle::from_memory(include_bytes!("../assets/iced-logo.svg")) + }); + + let text_size = text_size.into(); + + row![ + svg(LOGO.clone()).width(text_size * 1.3), + text("iced").size(text_size).font(Font::MONOSPACE) + ] + .spacing(text_size.0 / 3.0) + .align_items(Alignment::Center) + .into() +} + /// Creates a new [`Canvas`]. /// /// [`Canvas`]: crate::Canvas @@ -930,12 +966,12 @@ where /// Focuses the previous focusable widget. pub fn focus_previous<T>() -> Task<T> { - Task::effect(Action::widget(operation::focusable::focus_previous())) + task::effect(Action::widget(operation::focusable::focus_previous())) } /// Focuses the next focusable widget. pub fn focus_next<T>() -> Task<T> { - Task::effect(Action::widget(operation::focusable::focus_next())) + task::effect(Action::widget(operation::focusable::focus_next())) } /// A container intercepting mouse events. diff --git a/widget/src/scrollable.rs b/widget/src/scrollable.rs index bd612fa6..e0875bbf 100644 --- a/widget/src/scrollable.rs +++ b/widget/src/scrollable.rs @@ -14,7 +14,8 @@ use crate::core::{ self, Background, Border, Clipboard, Color, Element, Layout, Length, Pixels, Point, Rectangle, Shell, Size, Theme, Vector, Widget, }; -use crate::runtime::{Action, Task}; +use crate::runtime::task::{self, Task}; +use crate::runtime::Action; pub use operation::scrollable::{AbsoluteOffset, RelativeOffset}; @@ -107,6 +108,32 @@ where self } + /// Inverts the alignment of the horizontal direction of the [`Scrollable`], if applicable. + pub fn align_x(mut self, alignment: Alignment) -> Self { + match &mut self.direction { + Direction::Horizontal(horizontal) + | Direction::Both { horizontal, .. } => { + horizontal.alignment = alignment; + } + Direction::Vertical(_) => {} + } + + self + } + + /// Sets the alignment of the vertical direction of the [`Scrollable`], if applicable. + pub fn align_y(mut self, alignment: Alignment) -> Self { + match &mut self.direction { + Direction::Vertical(vertical) + | Direction::Both { vertical, .. } => { + vertical.alignment = alignment; + } + Direction::Horizontal(_) => {} + } + + self + } + /// Sets the style of this [`Scrollable`]. #[must_use] pub fn style(mut self, style: impl Fn(&Theme, Status) -> Style + 'a) -> Self @@ -954,13 +981,13 @@ impl From<Id> for widget::Id { /// Produces a [`Task`] that snaps the [`Scrollable`] with the given [`Id`] /// to the provided `percentage` along the x & y axis. pub fn snap_to<T>(id: Id, offset: RelativeOffset) -> Task<T> { - Task::effect(Action::widget(operation::scrollable::snap_to(id.0, offset))) + task::effect(Action::widget(operation::scrollable::snap_to(id.0, offset))) } /// Produces a [`Task`] that scrolls the [`Scrollable`] with the given [`Id`] /// to the provided [`AbsoluteOffset`] along the x & y axis. pub fn scroll_to<T>(id: Id, offset: AbsoluteOffset) -> Task<T> { - Task::effect(Action::widget(operation::scrollable::scroll_to( + task::effect(Action::widget(operation::scrollable::scroll_to( id.0, offset, ))) } diff --git a/widget/src/slider.rs b/widget/src/slider.rs index a8f1d192..74e6f8d3 100644 --- a/widget/src/slider.rs +++ b/widget/src/slider.rs @@ -237,7 +237,7 @@ where let steps = (percent * (end - start) / step).round(); let value = steps * step + start; - T::from_f64(value) + T::from_f64(value.min(end)) }; new_value diff --git a/widget/src/text_editor.rs b/widget/src/text_editor.rs index fc2ade43..0156b960 100644 --- a/widget/src/text_editor.rs +++ b/widget/src/text_editor.rs @@ -762,6 +762,11 @@ impl Update { { return Some(Self::Paste); } + keyboard::Key::Character("a") + if modifiers.command() => + { + return Some(Self::Action(Action::SelectAll)); + } _ => {} } diff --git a/widget/src/text_input.rs b/widget/src/text_input.rs index 4e89236b..ba2fbc13 100644 --- a/widget/src/text_input.rs +++ b/widget/src/text_input.rs @@ -30,7 +30,8 @@ use crate::core::{ Background, Border, Color, Element, Layout, Length, Padding, Pixels, Point, Rectangle, Shell, Size, Theme, Vector, Widget, }; -use crate::runtime::{Action, Task}; +use crate::runtime::task::{self, Task}; +use crate::runtime::Action; /// A field that can be filled with text. /// @@ -1142,13 +1143,13 @@ impl From<Id> for widget::Id { /// Produces a [`Task`] that focuses the [`TextInput`] with the given [`Id`]. pub fn focus<T>(id: Id) -> Task<T> { - Task::effect(Action::widget(operation::focusable::focus(id.0))) + task::effect(Action::widget(operation::focusable::focus(id.0))) } /// Produces a [`Task`] that moves the cursor of the [`TextInput`] with the given [`Id`] to the /// end. pub fn move_cursor_to_end<T>(id: Id) -> Task<T> { - Task::effect(Action::widget(operation::text_input::move_cursor_to_end( + task::effect(Action::widget(operation::text_input::move_cursor_to_end( id.0, ))) } @@ -1156,7 +1157,7 @@ pub fn move_cursor_to_end<T>(id: Id) -> Task<T> { /// Produces a [`Task`] that moves the cursor of the [`TextInput`] with the given [`Id`] to the /// front. pub fn move_cursor_to_front<T>(id: Id) -> Task<T> { - Task::effect(Action::widget(operation::text_input::move_cursor_to_front( + task::effect(Action::widget(operation::text_input::move_cursor_to_front( id.0, ))) } @@ -1164,14 +1165,14 @@ pub fn move_cursor_to_front<T>(id: Id) -> Task<T> { /// Produces a [`Task`] that moves the cursor of the [`TextInput`] with the given [`Id`] to the /// provided position. pub fn move_cursor_to<T>(id: Id, position: usize) -> Task<T> { - Task::effect(Action::widget(operation::text_input::move_cursor_to( + task::effect(Action::widget(operation::text_input::move_cursor_to( id.0, position, ))) } /// Produces a [`Task`] that selects all the content of the [`TextInput`] with the given [`Id`]. pub fn select_all<T>(id: Id) -> Task<T> { - Task::effect(Action::widget(operation::text_input::select_all(id.0))) + task::effect(Action::widget(operation::text_input::select_all(id.0))) } /// The state of a [`TextInput`]. diff --git a/widget/src/vertical_slider.rs b/widget/src/vertical_slider.rs index defb442f..33c591f5 100644 --- a/widget/src/vertical_slider.rs +++ b/widget/src/vertical_slider.rs @@ -239,7 +239,7 @@ where let steps = (percent * (end - start) / step).round(); let value = steps * step + start; - T::from_f64(value) + T::from_f64(value.min(end)) }; new_value diff --git a/winit/src/program.rs b/winit/src/program.rs index d55aedf1..3a4e2e48 100644 --- a/winit/src/program.rs +++ b/winit/src/program.rs @@ -203,11 +203,13 @@ where let (program, task) = runtime.enter(|| P::new(flags)); - if let Some(stream) = task.into_stream() { + if let Some(stream) = runtime::task::into_stream(task) { runtime.run(stream); } - runtime.track(program.subscription().map(Action::Output).into_recipes()); + runtime.track(subscription::into_recipes( + program.subscription().map(Action::Output), + )); let (boot_sender, boot_receiver) = oneshot::channel(); let (event_sender, event_receiver) = mpsc::unbounded(); @@ -1114,13 +1116,13 @@ fn update<P: Program, E: Executor>( let task = runtime.enter(|| program.update(message)); debug.update_finished(); - if let Some(stream) = task.into_stream() { + if let Some(stream) = runtime::task::into_stream(task) { runtime.run(stream); } } let subscription = program.subscription(); - runtime.track(subscription.map(Action::Output).into_recipes()); + runtime.track(subscription::into_recipes(subscription.map(Action::Output))); } fn run_action<P, C>( diff --git a/winit/src/system.rs b/winit/src/system.rs index 7997f311..361135be 100644 --- a/winit/src/system.rs +++ b/winit/src/system.rs @@ -5,7 +5,7 @@ use crate::runtime::{self, Task}; /// Query for available system information. pub fn fetch_information() -> Task<Information> { - Task::oneshot(|channel| { + runtime::task::oneshot(|channel| { runtime::Action::System(Action::QueryInformation(channel)) }) } |