diff options
122 files changed, 2424 insertions, 2657 deletions
diff --git a/.github/ISSUE_TEMPLATE/BUG-REPORT.yml b/.github/ISSUE_TEMPLATE/BUG-REPORT.yml index 20ef2b73..79441958 100644 --- a/.github/ISSUE_TEMPLATE/BUG-REPORT.yml +++ b/.github/ISSUE_TEMPLATE/BUG-REPORT.yml @@ -8,6 +8,22 @@ body: Thanks for taking the time to fill out this bug report! - type: checkboxes attributes: + label: Is your issue REALLY a bug? + description: | + This issue tracker is for __BUG REPORTS ONLY__. + + It's obvious, right? This is a bug report form, after all! Still, some crazy users seem to forcefully fill out this form just to ask questions and request features. + + The core team does not appreciate that. Don't do it. + + If you want to ask a question or request a feature, please [go back here](https://github.com/iced-rs/iced/issues/new/choose) and read carefully. + options: + - label: My issue is indeed a bug! + required: true + - label: I am not crazy! I will not fill out this form just to ask a question or request a feature. Pinky promise. + required: true + - type: checkboxes + attributes: label: Is there an existing issue for this? description: | Please, search [the existing issues] and see if an issue already exists for the bug you encountered. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 5177386c..3e2486d3 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -3,7 +3,7 @@ contact_links: - name: I have a question url: https://discourse.iced.rs/c/learn/6 about: Ask and learn from others in the Discourse forum. - - name: I want to start a discussion + - name: I want to request a feature or start a discussion url: https://discourse.iced.rs/c/request-feedback/7 about: Share your idea and gather feedback in the Discourse forum. - name: I want to chat with other users of the library diff --git a/.github/workflows/document.yml b/.github/workflows/document.yml index 827a2ca8..a213e590 100644 --- a/.github/workflows/document.yml +++ b/.github/workflows/document.yml @@ -8,7 +8,7 @@ jobs: steps: - uses: hecrj/setup-rust-action@v2 with: - rust-version: nightly-2023-12-11 + rust-version: nightly - uses: actions/checkout@v2 - name: Generate documentation run: | @@ -142,7 +142,7 @@ cosmic-text = "0.10" dark-light = "1.0" futures = "0.3" glam = "0.25" -glyphon = { git = "https://github.com/hecrj/glyphon.git", rev = "ceed55403ce53e120ce9d1fae17dcfe388726118" } +glyphon = { git = "https://github.com/hecrj/glyphon.git", rev = "f07e7bab705e69d39a5e6e52c73039a93c4552f8" } guillotiere = "0.6" half = "2.2" image = "0.24" @@ -176,7 +176,7 @@ web-time = "1.1" wgpu = "0.19" winapi = "0.3" window_clipboard = "0.4.1" -winit = { git = "https://github.com/iced-rs/winit.git", rev = "8affa522bc6dcc497d332a28c03491d22a22f5a7" } +winit = { git = "https://github.com/iced-rs/winit.git", rev = "254d6b3420ce4e674f516f7a2bd440665e05484d" } [workspace.lints.rust] rust_2018_idioms = "forbid" diff --git a/DEPENDENCIES.md b/DEPENDENCIES.md index 5d738d85..87fd8c7c 100644 --- a/DEPENDENCIES.md +++ b/DEPENDENCIES.md @@ -25,6 +25,8 @@ pkgs.mkShell rec { xorg.libXcursor xorg.libXi xorg.libXrandr + wayland + libxkbcommon ]; LD_LIBRARY_PATH = @@ -63,22 +63,6 @@ __Iced is currently experimental software.__ [Take a look at the roadmap], [check out the issues]: https://github.com/iced-rs/iced/issues [feel free to contribute!]: #contributing--feedback -## Installation - -Add `iced` as a dependency in your `Cargo.toml`: - -```toml -iced = "0.12" -``` - -If your project is using a Rust edition older than 2021, then you will need to -set `resolver = "2"` in the `[package]` section as well. - -__Iced moves fast and the `master` branch can contain breaking changes!__ If -you want to learn about a specific release, check out [the release list]. - -[the release list]: https://github.com/iced-rs/iced/releases - ## Overview Inspired by [The Elm Architecture], Iced expects you to split user interfaces @@ -217,7 +201,7 @@ The development of Iced is sponsored by the [Cryptowatch] team at [Kraken.com] [book]: https://book.iced.rs/ [documentation]: https://docs.rs/iced/ -[examples]: https://github.com/iced-rs/iced/tree/master/examples +[examples]: https://github.com/iced-rs/iced/tree/master/examples#examples [Coffee]: https://github.com/hecrj/coffee [Elm]: https://elm-lang.org/ [The Elm Architecture]: https://guide.elm-lang.org/architecture/ diff --git a/benches/ipsum.txt b/benches/ipsum.txt new file mode 100644 index 00000000..3e2d6396 --- /dev/null +++ b/benches/ipsum.txt @@ -0,0 +1,9 @@ +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur at elit mollis, dictum nunc non, tempus metus. Sed iaculis ac mauris eu lobortis. Integer elementum venenatis eros, id placerat odio feugiat vel. Maecenas consequat convallis tincidunt. Nunc eu lorem justo. Praesent quis ornare sapien. Aliquam interdum tortor ut rhoncus faucibus. Suspendisse molestie scelerisque nulla, eget sodales lacus sodales vel. Nunc placerat id arcu sodales venenatis. Praesent ullamcorper viverra nibh eget efficitur. Aliquam molestie felis vehicula, finibus sapien eget, accumsan purus. Praesent vestibulum eleifend consectetur. Sed tincidunt lectus a libero efficitur, non scelerisque lectus tincidunt. + +Cras ullamcorper tincidunt tellus non tempor. Integer pulvinar turpis quam, nec pharetra purus egestas non. Vivamus sed ipsum consequat, dignissim ante et, suscipit nibh. Quisque et mauris eu erat rutrum cursus. Pellentesque ut neque eu neque eleifend auctor ac hendrerit dolor. Morbi eget egestas ex. Integer hendrerit ipsum in enim bibendum, at vehicula ipsum dapibus. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce tempus consectetur tortor, vel fermentum sem pulvinar eget. Maecenas rutrum fringilla eros a pellentesque. Cras quis magna consectetur, tristique massa vel, aliquet nunc. Aliquam erat volutpat. Suspendisse porttitor risus id auctor fermentum. Vivamus efficitur tellus sed tortor cursus tincidunt. Sed auctor varius arcu, non congue tellus vehicula finibus. + +Fusce a tincidunt urna. Nunc at quam ac enim tempor vehicula imperdiet in sapien. Donec lobortis tristique felis vel semper. Quisque vulputate felis eu enim vestibulum malesuada. Fusce a lobortis mauris, iaculis eleifend ligula. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Vivamus sodales vel elit dignissim mattis. + +Aliquam placerat vulputate dignissim. Proin pellentesque vitae arcu ut feugiat. Nunc mi felis, ornare at gravida sed, vestibulum sed urna. Duis fermentum maximus viverra. Donec imperdiet pellentesque sollicitudin. Cras non sem quis metus bibendum molestie. Duis imperdiet nec lectus eu rutrum. Mauris congue enim purus, in iaculis arcu dapibus ut. Nullam id erat tincidunt, iaculis dolor non, lobortis magna. Proin convallis scelerisque maximus. Morbi at lorem fringilla libero blandit fringilla. Ut aliquet tellus non sem dictum viverra. Aenean venenatis purus eget lacus placerat, non mollis mauris pellentesque. + +Etiam elit diam, aliquet quis suscipit non, condimentum viverra odio. Praesent mi enim, suscipit id mi in, rhoncus ultricies lorem. Nulla facilisi. Integer convallis sagittis euismod. Vestibulum porttitor sodales turpis ac accumsan. Nullam molestie turpis vel lacus tincidunt, sed finibus erat pharetra. Nullam vestibulum turpis id sollicitudin accumsan. Praesent eget posuere lacus. Donec vehicula, nisl nec suscipit porta, felis lorem gravida orci, a hendrerit tellus nibh sit amet elit. diff --git a/benches/wgpu.rs b/benches/wgpu.rs index 2d308666..0e407253 100644 --- a/benches/wgpu.rs +++ b/benches/wgpu.rs @@ -3,7 +3,7 @@ use criterion::{criterion_group, criterion_main, Bencher, Criterion}; use iced::alignment; use iced::mouse; -use iced::widget::{canvas, stack, text}; +use iced::widget::{canvas, scrollable, stack, text}; use iced::{ Color, Element, Font, Length, Pixels, Point, Rectangle, Size, Theme, }; @@ -14,20 +14,31 @@ criterion_group!(benches, wgpu_benchmark); #[allow(unused_results)] pub fn wgpu_benchmark(c: &mut Criterion) { - c.bench_function("wgpu — canvas (light)", |b| benchmark(b, scene(10))); - c.bench_function("wgpu — canvas (heavy)", |b| benchmark(b, scene(1_000))); + c.bench_function("wgpu — canvas (light)", |b| { + benchmark(b, |_| scene(10)); + }); + c.bench_function("wgpu — canvas (heavy)", |b| { + benchmark(b, |_| scene(1_000)); + }); c.bench_function("wgpu - layered text (light)", |b| { - benchmark(b, layered_text(10)); + benchmark(b, |_| layered_text(10)); }); c.bench_function("wgpu - layered text (heavy)", |b| { - benchmark(b, layered_text(1_000)); + benchmark(b, |_| layered_text(1_000)); + }); + + c.bench_function("wgpu - dynamic text (light)", |b| { + benchmark(b, |i| dynamic_text(1_000, i)); + }); + c.bench_function("wgpu - dynamic text (heavy)", |b| { + benchmark(b, |i| dynamic_text(100_000, i)); }); } -fn benchmark( +fn benchmark<'a>( bencher: &mut Bencher<'_>, - widget: Element<'_, (), Theme, Renderer>, + view: impl Fn(usize) -> Element<'a, (), Theme, Renderer>, ) { use iced_futures::futures::executor; use iced_wgpu::graphics; @@ -94,14 +105,17 @@ fn benchmark( let texture_view = texture.create_view(&wgpu::TextureViewDescriptor::default()); - let mut user_interface = runtime::UserInterface::build( - widget, - viewport.logical_size(), - runtime::user_interface::Cache::default(), - &mut renderer, - ); + let mut i = 0; + let mut cache = Some(runtime::user_interface::Cache::default()); bencher.iter(|| { + let mut user_interface = runtime::UserInterface::build( + view(i), + viewport.logical_size(), + cache.take().unwrap(), + &mut renderer, + ); + let _ = user_interface.draw( &mut renderer, &Theme::Dark, @@ -111,6 +125,8 @@ fn benchmark( mouse::Cursor::Unavailable, ); + cache = Some(user_interface.into_cache()); + let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None, @@ -130,6 +146,8 @@ fn benchmark( let submission = engine.submit(&queue, encoder); let _ = device.poll(wgpu::Maintain::WaitForSubmissionIndex(submission)); + + i += 1; }); } @@ -189,3 +207,22 @@ fn layered_text<'a, Message: 'a>( .height(Length::Fill) .into() } + +fn dynamic_text<'a, Message: 'a>( + n: usize, + i: usize, +) -> Element<'a, Message, Theme, Renderer> { + const LOREM_IPSUM: &str = include_str!("ipsum.txt"); + + scrollable( + text(format!( + "{}... Iteration {i}", + std::iter::repeat(LOREM_IPSUM.chars()) + .flatten() + .take(n) + .collect::<String>(), + )) + .size(10), + ) + .into() +} diff --git a/core/Cargo.toml b/core/Cargo.toml index 3c557bca..a1228909 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -33,8 +33,5 @@ web-time.workspace = true dark-light.workspace = true dark-light.optional = true -[target.'cfg(windows)'.dependencies] -raw-window-handle.workspace = true - [dev-dependencies] approx = "0.5" diff --git a/core/README.md b/core/README.md index 519e0608..de11acad 100644 --- a/core/README.md +++ b/core/README.md @@ -13,15 +13,3 @@ This crate is meant to be a starting point for an Iced runtime. </p> [documentation]: https://docs.rs/iced_core - -## Installation -Add `iced_core` as a dependency in your `Cargo.toml`: - -```toml -iced_core = "0.9" -``` - -__Iced moves fast and the `master` branch can contain breaking changes!__ If -you want to learn about a specific release, check out [the release list]. - -[the release list]: https://github.com/iced-rs/iced/releases diff --git a/core/src/element.rs b/core/src/element.rs index 7d918a2e..385d8295 100644 --- a/core/src/element.rs +++ b/core/src/element.rs @@ -10,7 +10,6 @@ use crate::{ Widget, }; -use std::any::Any; use std::borrow::Borrow; /// A generic [`Widget`]. @@ -305,63 +304,9 @@ where tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn widget::Operation<B>, + operation: &mut dyn widget::Operation<()>, ) { - struct MapOperation<'a, B> { - operation: &'a mut dyn widget::Operation<B>, - } - - impl<'a, T, B> widget::Operation<T> for MapOperation<'a, B> { - fn container( - &mut self, - id: Option<&widget::Id>, - bounds: Rectangle, - operate_on_children: &mut dyn FnMut( - &mut dyn widget::Operation<T>, - ), - ) { - self.operation.container(id, bounds, &mut |operation| { - operate_on_children(&mut MapOperation { operation }); - }); - } - - fn focusable( - &mut self, - state: &mut dyn widget::operation::Focusable, - id: Option<&widget::Id>, - ) { - self.operation.focusable(state, id); - } - - fn scrollable( - &mut self, - state: &mut dyn widget::operation::Scrollable, - id: Option<&widget::Id>, - bounds: Rectangle, - translation: Vector, - ) { - self.operation.scrollable(state, id, bounds, translation); - } - - fn text_input( - &mut self, - state: &mut dyn widget::operation::TextInput, - id: Option<&widget::Id>, - ) { - self.operation.text_input(state, id); - } - - fn custom(&mut self, state: &mut dyn Any, id: Option<&widget::Id>) { - self.operation.custom(state, id); - } - } - - self.widget.operate( - tree, - layout, - renderer, - &mut MapOperation { operation }, - ); + self.widget.operate(tree, layout, renderer, operation); } fn on_event( @@ -495,7 +440,7 @@ where state: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn widget::Operation<Message>, + operation: &mut dyn widget::Operation<()>, ) { self.element .widget diff --git a/core/src/event.rs b/core/src/event.rs index 870b3074..b6cf321e 100644 --- a/core/src/event.rs +++ b/core/src/event.rs @@ -19,31 +19,10 @@ pub enum Event { Mouse(mouse::Event), /// A window event - Window(window::Id, window::Event), + Window(window::Event), /// A touch event Touch(touch::Event), - - /// A platform specific event - PlatformSpecific(PlatformSpecific), -} - -/// A platform specific event -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum PlatformSpecific { - /// A MacOS specific event - MacOS(MacOS), -} - -/// Describes an event specific to MacOS -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum MacOS { - /// Triggered when the app receives an URL from the system - /// - /// _**Note:** For this event to be triggered, the executable needs to be properly [bundled]!_ - /// - /// [bundled]: https://developer.apple.com/library/archive/documentation/CoreFoundation/Conceptual/CFBundles/BundleTypes/BundleTypes.html#//apple_ref/doc/uid/10000123i-CH101-SW19 - ReceivedUrl(String), } /// The status of an [`Event`] after being processed. diff --git a/core/src/keyboard/modifiers.rs b/core/src/keyboard/modifiers.rs index e531510f..edbf6d38 100644 --- a/core/src/keyboard/modifiers.rs +++ b/core/src/keyboard/modifiers.rs @@ -84,4 +84,28 @@ impl Modifiers { is_pressed } + + /// Returns true if the "jump key" is pressed in the [`Modifiers`]. + /// + /// The "jump key" is the modifier key used to widen text motions. It is the `Alt` + /// key in macOS and the `Ctrl` key in other platforms. + pub fn jump(self) -> bool { + if cfg!(target_os = "macos") { + self.alt() + } else { + self.control() + } + } + + /// Returns true if the "command key" is pressed on a macOS device. + /// + /// This is relevant for macOS-specific actions (e.g. `⌘ + ArrowLeft` moves the cursor + /// to the beginning of the line). + pub fn macos_command(self) -> bool { + if cfg!(target_os = "macos") { + self.logo() + } else { + false + } + } } diff --git a/core/src/overlay.rs b/core/src/overlay.rs index 3a57fe16..16f867da 100644 --- a/core/src/overlay.rs +++ b/core/src/overlay.rs @@ -41,7 +41,7 @@ where &mut self, _layout: Layout<'_>, _renderer: &Renderer, - _operation: &mut dyn widget::Operation<Message>, + _operation: &mut dyn widget::Operation<()>, ) { } diff --git a/core/src/overlay/element.rs b/core/src/overlay/element.rs index 695b88b3..61e75e8a 100644 --- a/core/src/overlay/element.rs +++ b/core/src/overlay/element.rs @@ -5,9 +5,7 @@ use crate::layout; use crate::mouse; use crate::renderer; use crate::widget; -use crate::{Clipboard, Layout, Point, Rectangle, Shell, Size, Vector}; - -use std::any::Any; +use crate::{Clipboard, Layout, Point, Rectangle, Shell, Size}; /// A generic [`Overlay`]. #[allow(missing_debug_implementations)] @@ -94,7 +92,7 @@ where &mut self, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn widget::Operation<Message>, + operation: &mut dyn widget::Operation<()>, ) { self.overlay.operate(layout, renderer, operation); } @@ -146,59 +144,9 @@ where &mut self, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn widget::Operation<B>, + operation: &mut dyn widget::Operation<()>, ) { - struct MapOperation<'a, B> { - operation: &'a mut dyn widget::Operation<B>, - } - - impl<'a, T, B> widget::Operation<T> for MapOperation<'a, B> { - fn container( - &mut self, - id: Option<&widget::Id>, - bounds: Rectangle, - operate_on_children: &mut dyn FnMut( - &mut dyn widget::Operation<T>, - ), - ) { - self.operation.container(id, bounds, &mut |operation| { - operate_on_children(&mut MapOperation { operation }); - }); - } - - fn focusable( - &mut self, - state: &mut dyn widget::operation::Focusable, - id: Option<&widget::Id>, - ) { - self.operation.focusable(state, id); - } - - fn scrollable( - &mut self, - state: &mut dyn widget::operation::Scrollable, - id: Option<&widget::Id>, - bounds: Rectangle, - translation: Vector, - ) { - self.operation.scrollable(state, id, bounds, translation); - } - - fn text_input( - &mut self, - state: &mut dyn widget::operation::TextInput, - id: Option<&widget::Id>, - ) { - self.operation.text_input(state, id); - } - - fn custom(&mut self, state: &mut dyn Any, id: Option<&widget::Id>) { - self.operation.custom(state, id); - } - } - - self.content - .operate(layout, renderer, &mut MapOperation { operation }); + self.content.operate(layout, renderer, operation); } fn on_event( diff --git a/core/src/overlay/group.rs b/core/src/overlay/group.rs index 7e4bebd0..cd12eac9 100644 --- a/core/src/overlay/group.rs +++ b/core/src/overlay/group.rs @@ -132,7 +132,7 @@ where &mut self, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn widget::Operation<Message>, + operation: &mut dyn widget::Operation<()>, ) { operation.container(None, layout.bounds(), &mut |operation| { self.children.iter_mut().zip(layout.children()).for_each( diff --git a/core/src/widget.rs b/core/src/widget.rs index b02e3a4f..0d12deba 100644 --- a/core/src/widget.rs +++ b/core/src/widget.rs @@ -105,7 +105,7 @@ where _state: &mut Tree, _layout: Layout<'_>, _renderer: &Renderer, - _operation: &mut dyn Operation<Message>, + _operation: &mut dyn Operation<()>, ) { } diff --git a/core/src/widget/operation.rs b/core/src/widget/operation.rs index b91cf9ac..3e4ed618 100644 --- a/core/src/widget/operation.rs +++ b/core/src/widget/operation.rs @@ -12,11 +12,11 @@ use crate::{Rectangle, Vector}; use std::any::Any; use std::fmt; -use std::rc::Rc; +use std::sync::Arc; /// A piece of logic that can traverse the widget tree of an application in /// order to query or update some widget state. -pub trait Operation<T> { +pub trait Operation<T>: Send { /// Operates on a widget that contains other widgets. /// /// The `operate_on_children` function can be called to return control to @@ -81,7 +81,7 @@ where /// Maps the output of an [`Operation`] using the given function. pub fn map<A, B>( operation: Box<dyn Operation<A>>, - f: impl Fn(A) -> B + 'static, + f: impl Fn(A) -> B + Send + Sync + 'static, ) -> impl Operation<B> where A: 'static, @@ -90,7 +90,7 @@ where #[allow(missing_debug_implementations)] struct Map<A, B> { operation: Box<dyn Operation<A>>, - f: Rc<dyn Fn(A) -> B>, + f: Arc<dyn Fn(A) -> B + Send + Sync>, } impl<A, B> Operation<B> for Map<A, B> @@ -197,7 +197,7 @@ where Map { operation, - f: Rc::new(f), + f: Arc::new(f), } } diff --git a/core/src/window/position.rs b/core/src/window/position.rs index 73391e75..1c8e86b6 100644 --- a/core/src/window/position.rs +++ b/core/src/window/position.rs @@ -1,4 +1,4 @@ -use crate::Point; +use crate::{Point, Size}; /// The position of a window in a given screen. #[derive(Debug, Clone, Copy, PartialEq)] @@ -15,6 +15,12 @@ pub enum Position { /// at (0, 0) you would have to set the position to /// `(PADDING_X, PADDING_Y)`. Specific(Point), + /// Like [`Specific`], but the window is positioned with the specific coordinates returned by the function. + /// + /// The function receives the window size and the monitor's resolution as input. + /// + /// [`Specific`]: Self::Specific + SpecificWith(fn(Size, Size) -> Point), } impl Default for Position { diff --git a/core/src/window/settings/windows.rs b/core/src/window/settings/windows.rs index d3bda259..88fe2fbd 100644 --- a/core/src/window/settings/windows.rs +++ b/core/src/window/settings/windows.rs @@ -1,12 +1,8 @@ //! Platform specific settings for Windows. -use raw_window_handle::RawWindowHandle; /// The platform specific window settings of an application. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct PlatformSpecific { - /// Parent window - pub parent: Option<RawWindowHandle>, - /// Drag and drop support pub drag_and_drop: bool, @@ -17,7 +13,6 @@ pub struct PlatformSpecific { impl Default for PlatformSpecific { fn default() -> Self { Self { - parent: None, drag_and_drop: true, skip_taskbar: false, } diff --git a/examples/README.md b/examples/README.md index 71dad13e..232b6042 100644 --- a/examples/README.md +++ b/examples/README.md @@ -1,8 +1,6 @@ # Examples -__Iced moves fast and the `master` branch can contain breaking changes!__ If -you want to learn about a specific release, check out [the release list]. - -[the release list]: https://github.com/iced-rs/iced/releases +__Iced moves fast and the `master` branch can contain breaking changes!__ If you want to browse examples that are compatible with the latest release, +then [switch to the `latest` branch](https://github.com/iced-rs/iced/tree/latest/examples#examples). ## [Tour](tour) A simple UI tour that can run both on native platforms and the web! It showcases different widgets that can be built using Iced. diff --git a/examples/clock/Cargo.toml b/examples/clock/Cargo.toml index dc2e5382..bc6c202b 100644 --- a/examples/clock/Cargo.toml +++ b/examples/clock/Cargo.toml @@ -8,6 +8,5 @@ publish = false [dependencies] iced.workspace = true iced.features = ["canvas", "tokio", "debug"] - -time = { version = "0.3", features = ["local-offset"] } +chrono = "0.4" tracing-subscriber = "0.3" diff --git a/examples/clock/src/main.rs b/examples/clock/src/main.rs index d717db36..7c4685c4 100644 --- a/examples/clock/src/main.rs +++ b/examples/clock/src/main.rs @@ -1,5 +1,6 @@ use iced::alignment; use iced::mouse; +use iced::time; use iced::widget::canvas::{stroke, Cache, Geometry, LineCap, Path, Stroke}; use iced::widget::{canvas, container}; use iced::{ @@ -18,13 +19,13 @@ pub fn main() -> iced::Result { } struct Clock { - now: time::OffsetDateTime, + now: chrono::DateTime<chrono::Local>, clock: Cache, } #[derive(Debug, Clone, Copy)] enum Message { - Tick(time::OffsetDateTime), + Tick(chrono::DateTime<chrono::Local>), } impl Clock { @@ -54,16 +55,12 @@ impl Clock { } fn subscription(&self) -> Subscription<Message> { - iced::time::every(std::time::Duration::from_millis(500)).map(|_| { - Message::Tick( - time::OffsetDateTime::now_local() - .unwrap_or_else(|_| time::OffsetDateTime::now_utc()), - ) - }) + time::every(time::Duration::from_millis(500)) + .map(|_| Message::Tick(chrono::offset::Local::now())) } fn theme(&self) -> Theme { - Theme::ALL[(self.now.unix_timestamp() as usize / 10) % Theme::ALL.len()] + Theme::ALL[(self.now.timestamp() as usize / 10) % Theme::ALL.len()] .clone() } } @@ -71,8 +68,7 @@ impl Clock { impl Default for Clock { fn default() -> Self { Self { - now: time::OffsetDateTime::now_local() - .unwrap_or_else(|_| time::OffsetDateTime::now_utc()), + now: chrono::offset::Local::now(), clock: Cache::default(), } } @@ -89,6 +85,8 @@ impl<Message> canvas::Program<Message> for Clock { bounds: Rectangle, _cursor: mouse::Cursor, ) -> Vec<Geometry> { + use chrono::Timelike; + let clock = self.clock.draw(renderer, bounds.size(), |frame| { let palette = theme.extended_palette(); @@ -169,7 +167,7 @@ impl<Message> canvas::Program<Message> for Clock { } } -fn hand_rotation(n: u8, total: u8) -> Degrees { +fn hand_rotation(n: u32, total: u32) -> Degrees { let turns = n as f32 / total as f32; Degrees(360.0 * turns) diff --git a/examples/custom_quad/src/main.rs b/examples/custom_quad/src/main.rs index b3eee218..b53a40d6 100644 --- a/examples/custom_quad/src/main.rs +++ b/examples/custom_quad/src/main.rs @@ -165,7 +165,7 @@ impl Example { self.border_width, self.shadow ), - text(format!("Radius: {tl:.2}/{tr:.2}/{br:.2}/{bl:.2}")), + text!("Radius: {tl:.2}/{tr:.2}/{br:.2}/{bl:.2}"), slider(1.0..=100.0, tl, Message::RadiusTopLeftChanged).step(0.01), slider(1.0..=100.0, tr, Message::RadiusTopRightChanged).step(0.01), slider(1.0..=100.0, br, Message::RadiusBottomRightChanged) @@ -174,7 +174,7 @@ impl Example { .step(0.01), slider(1.0..=10.0, self.border_width, Message::BorderWidthChanged) .step(0.01), - text(format!("Shadow: {sx:.2}x{sy:.2}, {sr:.2}")), + text!("Shadow: {sx:.2}x{sy:.2}, {sr:.2}"), slider(-100.0..=100.0, sx, Message::ShadowXOffsetChanged) .step(0.01), slider(-100.0..=100.0, sy, Message::ShadowYOffsetChanged) diff --git a/examples/custom_widget/src/main.rs b/examples/custom_widget/src/main.rs index 261dcb81..3cf10e22 100644 --- a/examples/custom_widget/src/main.rs +++ b/examples/custom_widget/src/main.rs @@ -114,7 +114,7 @@ impl Example { fn view(&self) -> Element<Message> { let content = column![ circle(self.radius), - text(format!("Radius: {:.2}", self.radius)), + text!("Radius: {:.2}", self.radius), slider(1.0..=100.0, self.radius, Message::RadiusChanged).step(0.01), ] .padding(20) diff --git a/examples/download_progress/src/main.rs b/examples/download_progress/src/main.rs index e031ac44..7974d5a0 100644 --- a/examples/download_progress/src/main.rs +++ b/examples/download_progress/src/main.rs @@ -160,7 +160,7 @@ impl Download { .into() } State::Downloading { .. } => { - text(format!("Downloading... {current_progress:.2}%")).into() + text!("Downloading... {current_progress:.2}%").into() } State::Errored => column![ "Something went wrong :(", diff --git a/examples/editor/src/main.rs b/examples/editor/src/main.rs index af05031a..ec65e2fa 100644 --- a/examples/editor/src/main.rs +++ b/examples/editor/src/main.rs @@ -4,7 +4,7 @@ use iced::widget::{ button, column, container, horizontal_space, pick_list, row, text, text_editor, tooltip, }; -use iced::{Alignment, Command, Element, Font, Length, Subscription, Theme}; +use iced::{Alignment, Element, Font, Length, Subscription, Task, Theme}; use std::ffi; use std::io; @@ -51,26 +51,26 @@ impl Editor { } } - fn load() -> Command<Message> { - Command::perform( + fn load() -> Task<Message> { + Task::perform( load_file(format!("{}/src/main.rs", env!("CARGO_MANIFEST_DIR"))), Message::FileOpened, ) } - fn update(&mut self, message: Message) -> Command<Message> { + fn update(&mut self, message: Message) -> Task<Message> { match message { Message::ActionPerformed(action) => { self.is_dirty = self.is_dirty || action.is_edit(); self.content.perform(action); - Command::none() + Task::none() } Message::ThemeSelected(theme) => { self.theme = theme; - Command::none() + Task::none() } Message::NewFile => { if !self.is_loading { @@ -78,15 +78,15 @@ impl Editor { self.content = text_editor::Content::new(); } - Command::none() + Task::none() } Message::OpenFile => { if self.is_loading { - Command::none() + Task::none() } else { self.is_loading = true; - Command::perform(open_file(), Message::FileOpened) + Task::perform(open_file(), Message::FileOpened) } } Message::FileOpened(result) => { @@ -98,15 +98,15 @@ impl Editor { self.content = text_editor::Content::with_text(&contents); } - Command::none() + Task::none() } Message::SaveFile => { if self.is_loading { - Command::none() + Task::none() } else { self.is_loading = true; - Command::perform( + Task::perform( save_file(self.file.clone(), self.content.text()), Message::FileSaved, ) @@ -120,7 +120,7 @@ impl Editor { self.is_dirty = false; } - Command::none() + Task::none() } } } @@ -277,7 +277,7 @@ fn action<'a, Message: Clone + 'a>( label: &'a str, on_press: Option<Message>, ) -> Element<'a, Message> { - let action = button(container(content).center_x().width(30)); + let action = button(container(content).center_x(30)); if let Some(on_press) = on_press { tooltip( diff --git a/examples/events/src/main.rs b/examples/events/src/main.rs index 999ce8ef..504ed5d8 100644 --- a/examples/events/src/main.rs +++ b/examples/events/src/main.rs @@ -2,7 +2,7 @@ use iced::alignment; use iced::event::{self, Event}; use iced::widget::{button, center, checkbox, text, Column}; use iced::window; -use iced::{Alignment, Command, Element, Length, Subscription}; +use iced::{Alignment, Element, Length, Subscription, Task}; pub fn main() -> iced::Result { iced::program("Events - Iced", Events::update, Events::view) @@ -25,7 +25,7 @@ enum Message { } impl Events { - fn update(&mut self, message: Message) -> Command<Message> { + fn update(&mut self, message: Message) -> Task<Message> { match message { Message::EventOccurred(event) if self.enabled => { self.last.push(event); @@ -34,20 +34,19 @@ impl Events { let _ = self.last.remove(0); } - Command::none() + Task::none() } Message::EventOccurred(event) => { - if let Event::Window(id, window::Event::CloseRequested) = event - { - window::close(id) + if let Event::Window(window::Event::CloseRequested) = event { + window::close(window::Id::MAIN) } else { - Command::none() + Task::none() } } Message::Toggled(enabled) => { self.enabled = enabled; - Command::none() + Task::none() } Message::Exit => window::close(window::Id::MAIN), } @@ -61,7 +60,7 @@ impl Events { let events = Column::with_children( self.last .iter() - .map(|event| text(format!("{event:?}")).size(40)) + .map(|event| text!("{event:?}").size(40)) .map(Element::from), ); diff --git a/examples/exit/src/main.rs b/examples/exit/src/main.rs index 2de97e20..8ba180a5 100644 --- a/examples/exit/src/main.rs +++ b/examples/exit/src/main.rs @@ -1,6 +1,6 @@ use iced::widget::{button, center, column}; use iced::window; -use iced::{Alignment, Command, Element}; +use iced::{Alignment, Element, Task}; pub fn main() -> iced::Result { iced::program("Exit - Iced", Exit::update, Exit::view).run() @@ -18,13 +18,13 @@ enum Message { } impl Exit { - fn update(&mut self, message: Message) -> Command<Message> { + fn update(&mut self, message: Message) -> Task<Message> { match message { Message::Confirm => window::close(window::Id::MAIN), Message::Exit => { self.show_confirm = true; - Command::none() + Task::none() } } } diff --git a/examples/game_of_life/src/main.rs b/examples/game_of_life/src/main.rs index 0716b2a4..7e6d461d 100644 --- a/examples/game_of_life/src/main.rs +++ b/examples/game_of_life/src/main.rs @@ -9,7 +9,7 @@ use iced::time; use iced::widget::{ button, checkbox, column, container, pick_list, row, slider, text, }; -use iced::{Alignment, Command, Element, Length, Subscription, Theme}; +use iced::{Alignment, Element, Length, Subscription, Task, Theme}; use std::time::Duration; pub fn main() -> iced::Result { @@ -56,7 +56,7 @@ impl GameOfLife { } } - fn update(&mut self, message: Message) -> Command<Message> { + fn update(&mut self, message: Message) -> Task<Message> { match message { Message::Grid(message, version) => { if version == self.version { @@ -75,7 +75,7 @@ impl GameOfLife { let version = self.version; - return Command::perform(task, move |message| { + return Task::perform(task, move |message| { Message::Grid(message, version) }); } @@ -103,7 +103,7 @@ impl GameOfLife { } } - Command::none() + Task::none() } fn subscription(&self) -> Subscription<Message> { @@ -163,7 +163,7 @@ fn view_controls<'a>( let speed_controls = row![ slider(1.0..=1000.0, speed as f32, Message::SpeedChanged), - text(format!("x{speed}")).size(16), + text!("x{speed}").size(16), ] .align_items(Alignment::Center) .spacing(10); diff --git a/examples/geometry/src/main.rs b/examples/geometry/src/main.rs index bf7801a9..3c7969c5 100644 --- a/examples/geometry/src/main.rs +++ b/examples/geometry/src/main.rs @@ -153,7 +153,7 @@ mod rainbow { } use iced::widget::{column, container, scrollable}; -use iced::Element; +use iced::{Element, Length}; use rainbow::rainbow; pub fn main() -> iced::Result { @@ -176,7 +176,7 @@ fn view(_state: &()) -> Element<'_, ()> { .spacing(20) .max_width(500); - let scrollable = scrollable(container(content).center_x()); + let scrollable = scrollable(container(content).center_x(Length::Fill)); - container(scrollable).center_y().into() + container(scrollable).center_y(Length::Fill).into() } diff --git a/examples/integration/Cargo.toml b/examples/integration/Cargo.toml index a4a961f8..7f8feb3f 100644 --- a/examples/integration/Cargo.toml +++ b/examples/integration/Cargo.toml @@ -8,7 +8,9 @@ publish = false [dependencies] iced_winit.workspace = true iced_wgpu.workspace = true + iced_widget.workspace = true +iced_widget.features = ["wgpu"] [target.'cfg(not(target_arch = "wasm32"))'.dependencies] tracing-subscriber = "0.3" diff --git a/examples/integration/src/controls.rs b/examples/integration/src/controls.rs index 28050f8a..d0654996 100644 --- a/examples/integration/src/controls.rs +++ b/examples/integration/src/controls.rs @@ -2,7 +2,7 @@ use iced_wgpu::Renderer; use iced_widget::{column, container, row, slider, text, text_input}; use iced_winit::core::alignment; use iced_winit::core::{Color, Element, Length, Theme}; -use iced_winit::runtime::{Command, Program}; +use iced_winit::runtime::{Program, Task}; pub struct Controls { background_color: Color, @@ -33,7 +33,7 @@ impl Program for Controls { type Message = Message; type Renderer = Renderer; - fn update(&mut self, message: Message) -> Command<Message> { + fn update(&mut self, message: Message) -> Task<Message> { match message { Message::BackgroundColorChanged(color) => { self.background_color = color; @@ -43,7 +43,7 @@ impl Program for Controls { } } - Command::none() + Task::none() } fn view(&self) -> Element<Message, Theme, Renderer> { @@ -78,9 +78,7 @@ impl Program for Controls { container( column![ text("Background color").color(Color::WHITE), - text(format!("{background_color:?}")) - .size(14) - .color(Color::WHITE), + text!("{background_color:?}").size(14).color(Color::WHITE), text_input("Placeholder", &self.input) .on_input(Message::InputChanged), sliders, diff --git a/examples/integration/src/main.rs b/examples/integration/src/main.rs index e1c7d62f..9818adf3 100644 --- a/examples/integration/src/main.rs +++ b/examples/integration/src/main.rs @@ -9,7 +9,6 @@ use iced_wgpu::{wgpu, Engine, Renderer}; use iced_winit::conversion; use iced_winit::core::mouse; use iced_winit::core::renderer; -use iced_winit::core::window; use iced_winit::core::{Color, Font, Pixels, Size, Theme}; use iced_winit::futures; use iced_winit::runtime::program; @@ -317,7 +316,6 @@ pub fn main() -> Result<(), winit::error::EventLoopError> { // Map window event to iced event if let Some(event) = iced_winit::conversion::window_event( - window::Id::MAIN, event, window.scale_factor(), *modifiers, diff --git a/examples/layout/src/main.rs b/examples/layout/src/main.rs index 5f14c03b..c40ac820 100644 --- a/examples/layout/src/main.rs +++ b/examples/layout/src/main.rs @@ -251,7 +251,7 @@ fn application<'a>() -> Element<'a, Message> { .align_items(Alignment::Center), ) .style(container::rounded_box) - .center_y(); + .center_y(Length::Fill); let content = container( scrollable( diff --git a/examples/lazy/src/main.rs b/examples/lazy/src/main.rs index c3f6b8de..f24c0d62 100644 --- a/examples/lazy/src/main.rs +++ b/examples/lazy/src/main.rs @@ -192,7 +192,7 @@ impl App { text_input("Add a new option", &self.input) .on_input(Message::InputChanged) .on_submit(Message::AddItem(self.input.clone())), - button(text(format!("Toggle Order ({})", self.order))) + button(text!("Toggle Order ({})", self.order)) .on_press(Message::ToggleOrder) ] .spacing(10) diff --git a/examples/loading_spinners/src/circular.rs b/examples/loading_spinners/src/circular.rs index de728af2..bf70e190 100644 --- a/examples/loading_spinners/src/circular.rs +++ b/examples/loading_spinners/src/circular.rs @@ -275,7 +275,7 @@ where ) -> event::Status { let state = tree.state.downcast_mut::<State>(); - if let Event::Window(_, window::Event::RedrawRequested(now)) = event { + if let Event::Window(window::Event::RedrawRequested(now)) = event { state.animation = state.animation.timed_transition( self.cycle_duration, self.rotation_duration, diff --git a/examples/loading_spinners/src/easing.rs b/examples/loading_spinners/src/easing.rs index 665b3329..45089ef6 100644 --- a/examples/loading_spinners/src/easing.rs +++ b/examples/loading_spinners/src/easing.rs @@ -119,10 +119,7 @@ impl Builder { fn point(p: impl Into<Point>) -> lyon_algorithms::geom::Point<f32> { let p: Point = p.into(); - lyon_algorithms::geom::point( - p.x.min(1.0).max(0.0), - p.y.min(1.0).max(0.0), - ) + lyon_algorithms::geom::point(p.x.clamp(0.0, 1.0), p.y.clamp(0.0, 1.0)) } } diff --git a/examples/loading_spinners/src/linear.rs b/examples/loading_spinners/src/linear.rs index ce375621..164993c6 100644 --- a/examples/loading_spinners/src/linear.rs +++ b/examples/loading_spinners/src/linear.rs @@ -189,7 +189,7 @@ where ) -> event::Status { let state = tree.state.downcast_mut::<State>(); - if let Event::Window(_, window::Event::RedrawRequested(now)) = event { + if let Event::Window(window::Event::RedrawRequested(now)) = event { *state = state.timed_transition(self.cycle_duration, now); shell.request_redraw(RedrawRequest::NextFrame); diff --git a/examples/loading_spinners/src/main.rs b/examples/loading_spinners/src/main.rs index e8d67ab5..a63c51d4 100644 --- a/examples/loading_spinners/src/main.rs +++ b/examples/loading_spinners/src/main.rs @@ -81,7 +81,7 @@ impl LoadingSpinners { Message::CycleDurationChanged(x / 100.0) }) .width(200.0), - text(format!("{:.2}s", self.cycle_duration)), + text!("{:.2}s", self.cycle_duration), ] .align_items(iced::Alignment::Center) .spacing(20.0), diff --git a/examples/modal/src/main.rs b/examples/modal/src/main.rs index a012c310..d185cf3b 100644 --- a/examples/modal/src/main.rs +++ b/examples/modal/src/main.rs @@ -5,7 +5,7 @@ use iced::widget::{ self, button, center, column, container, horizontal_space, mouse_area, opaque, pick_list, row, stack, text, text_input, }; -use iced::{Alignment, Color, Command, Element, Length, Subscription}; +use iced::{Alignment, Color, Element, Length, Subscription, Task}; use std::fmt; @@ -39,7 +39,7 @@ impl App { event::listen().map(Message::Event) } - fn update(&mut self, message: Message) -> Command<Message> { + fn update(&mut self, message: Message) -> Task<Message> { match message { Message::ShowModal => { self.show_modal = true; @@ -47,26 +47,26 @@ impl App { } Message::HideModal => { self.hide_modal(); - Command::none() + Task::none() } Message::Email(email) => { self.email = email; - Command::none() + Task::none() } Message::Password(password) => { self.password = password; - Command::none() + Task::none() } Message::Plan(plan) => { self.plan = plan; - Command::none() + Task::none() } Message::Submit => { if !self.email.is_empty() && !self.password.is_empty() { self.hide_modal(); } - Command::none() + Task::none() } Message::Event(event) => match event { Event::Keyboard(keyboard::Event::KeyPressed { @@ -85,9 +85,9 @@ impl App { .. }) => { self.hide_modal(); - Command::none() + Task::none() } - _ => Command::none(), + _ => Task::none(), }, } } diff --git a/examples/multi_window/src/main.rs b/examples/multi_window/src/main.rs index 74339e0c..b82ad1f3 100644 --- a/examples/multi_window/src/main.rs +++ b/examples/multi_window/src/main.rs @@ -1,16 +1,15 @@ -use iced::event; use iced::executor; use iced::multi_window::{self, Application}; use iced::widget::{ - button, center, column, container, scrollable, text, text_input, + button, center, column, container, horizontal_space, scrollable, text, + text_input, }; use iced::window; use iced::{ - Alignment, Command, Element, Length, Point, Settings, Subscription, Theme, - Vector, + Alignment, Element, Length, Settings, Subscription, Task, Theme, Vector, }; -use std::collections::HashMap; +use std::collections::BTreeMap; fn main() -> iced::Result { Example::run(Settings::default()) @@ -18,8 +17,7 @@ fn main() -> iced::Result { #[derive(Default)] struct Example { - windows: HashMap<window::Id, Window>, - next_window_pos: window::Position, + windows: BTreeMap<window::Id, Window>, } #[derive(Debug)] @@ -33,13 +31,12 @@ struct Window { #[derive(Debug, Clone)] enum Message { + OpenWindow, + WindowOpened(window::Id), + WindowClosed(window::Id), ScaleInputChanged(window::Id, String), ScaleChanged(window::Id, String), TitleChanged(window::Id, String), - CloseWindow(window::Id), - WindowOpened(window::Id, Option<Point>), - WindowClosed(window::Id), - NewWindow, } impl multi_window::Application for Example { @@ -48,13 +45,12 @@ impl multi_window::Application for Example { type Theme = Theme; type Flags = (); - fn new(_flags: ()) -> (Self, Command<Message>) { + fn new(_flags: ()) -> (Self, Task<Message>) { ( Example { - windows: HashMap::from([(window::Id::MAIN, Window::new(1))]), - next_window_pos: window::Position::Default, + windows: BTreeMap::from([(window::Id::MAIN, Window::new(1))]), }, - Command::none(), + Task::none(), ) } @@ -62,79 +58,88 @@ impl multi_window::Application for Example { self.windows .get(&window) .map(|window| window.title.clone()) - .unwrap_or("Example".to_string()) + .unwrap_or_default() } - fn update(&mut self, message: Message) -> Command<Message> { + fn update(&mut self, message: Message) -> Task<Message> { match message { - Message::ScaleInputChanged(id, scale) => { - let window = - self.windows.get_mut(&id).expect("Window not found!"); - window.scale_input = scale; - - Command::none() - } - Message::ScaleChanged(id, scale) => { - let window = - self.windows.get_mut(&id).expect("Window not found!"); - - window.current_scale = scale - .parse::<f64>() - .unwrap_or(window.current_scale) - .clamp(0.5, 5.0); - - Command::none() + Message::OpenWindow => { + let Some(last_window) = self.windows.keys().last() else { + return Task::none(); + }; + + window::fetch_position(*last_window) + .then(|last_position| { + let position = last_position.map_or( + window::Position::Default, + |last_position| { + window::Position::Specific( + last_position + Vector::new(20.0, 20.0), + ) + }, + ); + + window::open(window::Settings { + position, + ..window::Settings::default() + }) + }) + .map(Message::WindowOpened) } - Message::TitleChanged(id, title) => { - let window = - self.windows.get_mut(&id).expect("Window not found."); + Message::WindowOpened(id) => { + let window = Window::new(self.windows.len() + 1); + let focus_input = text_input::focus(window.input_id.clone()); - window.title = title; + self.windows.insert(id, window); - Command::none() + focus_input } - Message::CloseWindow(id) => window::close(id), Message::WindowClosed(id) => { self.windows.remove(&id); - Command::none() + + Task::none() } - Message::WindowOpened(id, position) => { - if let Some(position) = position { - self.next_window_pos = window::Position::Specific( - position + Vector::new(20.0, 20.0), - ); + Message::ScaleInputChanged(id, scale) => { + if let Some(window) = self.windows.get_mut(&id) { + window.scale_input = scale; } - if let Some(window) = self.windows.get(&id) { - text_input::focus(window.input_id.clone()) - } else { - Command::none() - } + Task::none() } - Message::NewWindow => { - let count = self.windows.len() + 1; - - let (id, spawn_window) = window::spawn(window::Settings { - position: self.next_window_pos, - exit_on_close_request: count % 2 == 0, - ..Default::default() - }); + Message::ScaleChanged(id, scale) => { + if let Some(window) = self.windows.get_mut(&id) { + window.current_scale = scale + .parse::<f64>() + .unwrap_or(window.current_scale) + .clamp(0.5, 5.0); + } - self.windows.insert(id, Window::new(count)); + Task::none() + } + Message::TitleChanged(id, title) => { + if let Some(window) = self.windows.get_mut(&id) { + window.title = title; + } - spawn_window + Task::none() } } } - fn view(&self, window: window::Id) -> Element<Message> { - let content = self.windows.get(&window).unwrap().view(window); - - center(content).into() + fn view(&self, window_id: window::Id) -> Element<Message> { + if let Some(window) = self.windows.get(&window_id) { + center(window.view(window_id)).into() + } else { + horizontal_space().into() + } } - fn theme(&self, window: window::Id) -> Self::Theme { - self.windows.get(&window).unwrap().theme.clone() + fn theme(&self, window: window::Id) -> Theme { + if let Some(window) = self.windows.get(&window) { + window.theme.clone() + } else { + Theme::default() + } } fn scale_factor(&self, window: window::Id) -> f64 { @@ -145,22 +150,7 @@ impl multi_window::Application for Example { } fn subscription(&self) -> Subscription<Self::Message> { - event::listen_with(|event, _| { - if let iced::Event::Window(id, window_event) = event { - match window_event { - window::Event::CloseRequested => { - Some(Message::CloseWindow(id)) - } - window::Event::Opened { position, .. } => { - Some(Message::WindowOpened(id, position)) - } - window::Event::Closed => Some(Message::WindowClosed(id)), - _ => None, - } - } else { - None - } - }) + window::close_events().map(Message::WindowClosed) } } @@ -170,11 +160,7 @@ impl Window { title: format!("Window_{}", count), scale_input: "1.0".to_string(), current_scale: 1.0, - theme: if count % 2 == 0 { - Theme::Light - } else { - Theme::Dark - }, + theme: Theme::ALL[count % Theme::ALL.len()].clone(), input_id: text_input::Id::unique(), } } @@ -198,7 +184,7 @@ impl Window { ]; let new_window_button = - button(text("New Window")).on_press(Message::NewWindow); + button(text("New Window")).on_press(Message::OpenWindow); let content = scrollable( column![scale_input, title_input, new_window_button] @@ -207,6 +193,6 @@ impl Window { .align_items(Alignment::Center), ); - container(content).center_x().width(200).into() + container(content).center_x(200).into() } } diff --git a/examples/pane_grid/src/main.rs b/examples/pane_grid/src/main.rs index e74ea1ee..6b5bd332 100644 --- a/examples/pane_grid/src/main.rs +++ b/examples/pane_grid/src/main.rs @@ -284,14 +284,15 @@ fn view_content<'a>( .spacing(5) .max_width(160); - let content = column![ - text(format!("{}x{}", size.width, size.height)).size(24), - controls, - ] - .spacing(10) - .align_items(Alignment::Center); - - container(scrollable(content)).center_y().padding(5).into() + let content = + column![text!("{}x{}", size.width, size.height).size(24), controls,] + .spacing(10) + .align_items(Alignment::Center); + + container(scrollable(content)) + .center_y(Length::Fill) + .padding(5) + .into() } fn view_controls<'a>( diff --git a/examples/pokedex/src/main.rs b/examples/pokedex/src/main.rs index be20094d..e62ed70b 100644 --- a/examples/pokedex/src/main.rs +++ b/examples/pokedex/src/main.rs @@ -1,6 +1,6 @@ use iced::futures; use iced::widget::{self, center, column, image, row, text}; -use iced::{Alignment, Command, Element, Length}; +use iced::{Alignment, Element, Length, Task}; pub fn main() -> iced::Result { iced::program(Pokedex::title, Pokedex::update, Pokedex::view) @@ -25,8 +25,8 @@ enum Message { } impl Pokedex { - fn search() -> Command<Message> { - Command::perform(Pokemon::search(), Message::PokemonFound) + fn search() -> Task<Message> { + Task::perform(Pokemon::search(), Message::PokemonFound) } fn title(&self) -> String { @@ -39,20 +39,20 @@ impl Pokedex { format!("{subtitle} - Pokédex") } - fn update(&mut self, message: Message) -> Command<Message> { + fn update(&mut self, message: Message) -> Task<Message> { match message { Message::PokemonFound(Ok(pokemon)) => { *self = Pokedex::Loaded { pokemon }; - Command::none() + Task::none() } Message::PokemonFound(Err(_error)) => { *self = Pokedex::Errored; - Command::none() + Task::none() } Message::Search => match self { - Pokedex::Loading => Command::none(), + Pokedex::Loading => Task::none(), _ => { *self = Pokedex::Loading; @@ -104,9 +104,7 @@ impl Pokemon { column![ row![ text(&self.name).size(30).width(Length::Fill), - text(format!("#{}", self.number)) - .size(20) - .color([0.5, 0.5, 0.5]), + text!("#{}", self.number).size(20).color([0.5, 0.5, 0.5]), ] .align_items(Alignment::Center) .spacing(20), diff --git a/examples/screenshot/src/main.rs b/examples/screenshot/src/main.rs index 82495a1a..78d3e9ff 100644 --- a/examples/screenshot/src/main.rs +++ b/examples/screenshot/src/main.rs @@ -4,7 +4,7 @@ use iced::widget::{button, column, container, image, row, text, text_input}; use iced::window; use iced::window::screenshot::{self, Screenshot}; use iced::{ - Alignment, Command, ContentFit, Element, Length, Rectangle, Subscription, + Alignment, ContentFit, Element, Length, Rectangle, Subscription, Task, }; use ::image as img; @@ -34,7 +34,7 @@ struct Example { enum Message { Crop, Screenshot, - ScreenshotData(Screenshot), + Screenshotted(Screenshot), Png, PngSaved(Result<String, PngError>), XInputChanged(Option<u32>), @@ -44,22 +44,20 @@ enum Message { } impl Example { - fn update(&mut self, message: Message) -> Command<Message> { + fn update(&mut self, message: Message) -> Task<Message> { match message { Message::Screenshot => { - return iced::window::screenshot( - window::Id::MAIN, - Message::ScreenshotData, - ); + return iced::window::screenshot(window::Id::MAIN) + .map(Message::Screenshotted); } - Message::ScreenshotData(screenshot) => { + Message::Screenshotted(screenshot) => { self.screenshot = Some(screenshot); } Message::Png => { if let Some(screenshot) = &self.screenshot { self.png_saving = true; - return Command::perform( + return Task::perform( save_to_png(screenshot.clone()), Message::PngSaved, ); @@ -103,7 +101,7 @@ impl Example { } } - Command::none() + Task::none() } fn view(&self) -> Element<'_, Message> { @@ -123,10 +121,9 @@ impl Example { }; let image = container(image) - .center_y() + .center_y(Length::FillPortion(2)) .padding(10) - .style(container::rounded_box) - .width(Length::FillPortion(2)); + .style(container::rounded_box); let crop_origin_controls = row![ text("X:") @@ -161,7 +158,7 @@ impl Example { .push_maybe( self.crop_error .as_ref() - .map(|error| text(format!("Crop error! \n{error}"))), + .map(|error| text!("Crop error! \n{error}")), ) .spacing(10) .align_items(Alignment::Center); @@ -211,7 +208,7 @@ impl Example { .spacing(40) }; - let side_content = container(controls).center_y(); + let side_content = container(controls).center_y(Length::Fill); let content = row![side_content, image] .spacing(10) diff --git a/examples/scrollable/src/main.rs b/examples/scrollable/src/main.rs index bbb6497f..a0dcf82c 100644 --- a/examples/scrollable/src/main.rs +++ b/examples/scrollable/src/main.rs @@ -3,7 +3,7 @@ use iced::widget::{ button, column, container, horizontal_space, progress_bar, radio, row, scrollable, slider, text, vertical_space, Scrollable, }; -use iced::{Alignment, Border, Color, Command, Element, Length, Theme}; +use iced::{Alignment, Border, Color, Element, Length, Task, Theme}; use once_cell::sync::Lazy; @@ -59,7 +59,7 @@ impl ScrollableDemo { } } - fn update(&mut self, message: Message) -> Command<Message> { + fn update(&mut self, message: Message) -> Task<Message> { match message { Message::SwitchDirection(direction) => { self.current_scroll_offset = scrollable::RelativeOffset::START; @@ -82,17 +82,17 @@ impl ScrollableDemo { Message::ScrollbarWidthChanged(width) => { self.scrollbar_width = width; - Command::none() + Task::none() } Message::ScrollbarMarginChanged(margin) => { self.scrollbar_margin = margin; - Command::none() + Task::none() } Message::ScrollerWidthChanged(width) => { self.scroller_width = width; - Command::none() + Task::none() } Message::ScrollToBeginning => { self.current_scroll_offset = scrollable::RelativeOffset::START; @@ -113,7 +113,7 @@ impl ScrollableDemo { Message::Scrolled(viewport) => { self.current_scroll_offset = viewport.relative_offset(); - Command::none() + Task::none() } } } diff --git a/examples/sierpinski_triangle/src/main.rs b/examples/sierpinski_triangle/src/main.rs index 9cd6237f..7dd7be5e 100644 --- a/examples/sierpinski_triangle/src/main.rs +++ b/examples/sierpinski_triangle/src/main.rs @@ -54,7 +54,7 @@ impl SierpinskiEmulator { .width(Length::Fill) .height(Length::Fill), row![ - text(format!("Iteration: {:?}", self.graph.iteration)), + text!("Iteration: {:?}", self.graph.iteration), slider(0..=10000, self.graph.iteration, Message::IterationSet) ] .padding(10) diff --git a/examples/slider/src/main.rs b/examples/slider/src/main.rs index 5ffdc9c6..0b4c29aa 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; +use iced::{Element, Length}; pub fn main() -> iced::Result { iced::run("Slider - Iced", Slider::update, Slider::view) @@ -56,9 +56,9 @@ impl Slider { center( column![ - container(v_slider).center_x(), - container(h_slider).center_x(), - container(text).center_x() + container(v_slider).center_x(Length::Fill), + container(h_slider).center_x(Length::Fill), + container(text).center_x(Length::Fill) ] .spacing(25), ) diff --git a/examples/stopwatch/src/main.rs b/examples/stopwatch/src/main.rs index bbe9d0ff..a8149753 100644 --- a/examples/stopwatch/src/main.rs +++ b/examples/stopwatch/src/main.rs @@ -92,13 +92,13 @@ impl Stopwatch { let seconds = self.duration.as_secs(); - let duration = text(format!( + let duration = text!( "{:0>2}:{:0>2}:{:0>2}.{:0>2}", seconds / HOUR, (seconds % HOUR) / MINUTE, seconds % MINUTE, self.duration.subsec_millis() / 10, - )) + ) .size(40); let button = |label| { diff --git a/examples/svg/src/main.rs b/examples/svg/src/main.rs index 45b46716..e071c3af 100644 --- a/examples/svg/src/main.rs +++ b/examples/svg/src/main.rs @@ -45,7 +45,7 @@ impl Tiger { .on_toggle(Message::ToggleColorFilter); center( - column![svg, container(apply_color_filter).center_x()] + column![svg, container(apply_color_filter).center_x(Length::Fill)] .spacing(20) .height(Length::Fill), ) diff --git a/examples/system_information/src/main.rs b/examples/system_information/src/main.rs index 89a8383a..e2808edd 100644 --- a/examples/system_information/src/main.rs +++ b/examples/system_information/src/main.rs @@ -1,5 +1,5 @@ -use iced::widget::{button, column, container, text}; -use iced::{system, Command, Element}; +use iced::widget::{button, center, column, text}; +use iced::{system, Element, Task}; pub fn main() -> iced::Result { iced::program("System Information - Iced", Example::update, Example::view) @@ -24,19 +24,20 @@ enum Message { } impl Example { - fn update(&mut self, message: Message) -> Command<Message> { + fn update(&mut self, message: Message) -> Task<Message> { match message { Message::Refresh => { *self = Self::Loading; - return system::fetch_information(Message::InformationReceived); + return system::fetch_information() + .map(Message::InformationReceived); } Message::InformationReceived(information) => { *self = Self::Loaded { information }; } } - Command::none() + Task::none() } fn view(&self) -> Element<Message> { @@ -45,56 +46,56 @@ impl Example { let content: Element<_> = match self { Example::Loading => text("Loading...").size(40).into(), Example::Loaded { information } => { - let system_name = text(format!( + let system_name = text!( "System name: {}", information .system_name .as_ref() .unwrap_or(&"unknown".to_string()) - )); + ); - let system_kernel = text(format!( + let system_kernel = text!( "System kernel: {}", information .system_kernel .as_ref() .unwrap_or(&"unknown".to_string()) - )); + ); - let system_version = text(format!( + let system_version = text!( "System version: {}", information .system_version .as_ref() .unwrap_or(&"unknown".to_string()) - )); + ); - let system_short_version = text(format!( + let system_short_version = text!( "System short version: {}", information .system_short_version .as_ref() .unwrap_or(&"unknown".to_string()) - )); + ); let cpu_brand = - text(format!("Processor brand: {}", information.cpu_brand)); + text!("Processor brand: {}", information.cpu_brand); - let cpu_cores = text(format!( + let cpu_cores = text!( "Processor cores: {}", information .cpu_cores .map_or("unknown".to_string(), |cores| cores .to_string()) - )); + ); let memory_readable = ByteSize::b(information.memory_total).to_string(); - let memory_total = text(format!( + let memory_total = text!( "Memory (total): {} bytes ({memory_readable})", information.memory_total, - )); + ); let memory_text = if let Some(memory_used) = information.memory_used @@ -106,17 +107,13 @@ impl Example { String::from("None") }; - let memory_used = text(format!("Memory (used): {memory_text}")); + let memory_used = text!("Memory (used): {memory_text}"); - let graphics_adapter = text(format!( - "Graphics adapter: {}", - information.graphics_adapter - )); + let graphics_adapter = + text!("Graphics adapter: {}", information.graphics_adapter); - let graphics_backend = text(format!( - "Graphics backend: {}", - information.graphics_backend - )); + let graphics_backend = + text!("Graphics backend: {}", information.graphics_backend); column![ system_name.size(30), @@ -136,6 +133,6 @@ impl Example { } }; - container(content).center().into() + center(content).into() } } diff --git a/examples/toast/src/main.rs b/examples/toast/src/main.rs index 0fcf08c4..aee2479e 100644 --- a/examples/toast/src/main.rs +++ b/examples/toast/src/main.rs @@ -4,7 +4,7 @@ use iced::keyboard::key; use iced::widget::{ self, button, center, column, pick_list, row, slider, text, text_input, }; -use iced::{Alignment, Command, Element, Length, Subscription}; +use iced::{Alignment, Element, Length, Subscription, Task}; use toast::{Status, Toast}; @@ -49,7 +49,7 @@ impl App { event::listen().map(Message::Event) } - fn update(&mut self, message: Message) -> Command<Message> { + fn update(&mut self, message: Message) -> Task<Message> { match message { Message::Add => { if !self.editing.title.is_empty() @@ -57,27 +57,27 @@ impl App { { self.toasts.push(std::mem::take(&mut self.editing)); } - Command::none() + Task::none() } Message::Close(index) => { self.toasts.remove(index); - Command::none() + Task::none() } Message::Title(title) => { self.editing.title = title; - Command::none() + Task::none() } Message::Body(body) => { self.editing.body = body; - Command::none() + Task::none() } Message::Status(status) => { self.editing.status = status; - Command::none() + Task::none() } Message::Timeout(timeout) => { self.timeout_secs = timeout as u64; - Command::none() + Task::none() } Message::Event(Event::Keyboard(keyboard::Event::KeyPressed { key: keyboard::Key::Named(key::Named::Tab), @@ -88,7 +88,7 @@ impl App { key: keyboard::Key::Named(key::Named::Tab), .. })) => widget::focus_next(), - Message::Event(_) => Command::none(), + Message::Event(_) => Task::none(), } } @@ -131,7 +131,7 @@ impl App { subtitle( "Timeout", row![ - text(format!("{:0>2} sec", self.timeout_secs)), + text!("{:0>2} sec", self.timeout_secs), slider( 1.0..=30.0, self.timeout_secs as f64, @@ -347,7 +347,7 @@ mod toast { state: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn Operation<Message>, + operation: &mut dyn Operation<()>, ) { operation.container(None, layout.bounds(), &mut |operation| { self.content.as_widget().operate( @@ -499,9 +499,7 @@ mod toast { clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, ) -> event::Status { - if let Event::Window(_, window::Event::RedrawRequested(now)) = - &event - { + if let Event::Window(window::Event::RedrawRequested(now)) = &event { let mut next_redraw: Option<window::RedrawRequest> = None; self.instants.iter_mut().enumerate().for_each( @@ -591,7 +589,7 @@ mod toast { &mut self, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn widget::Operation<Message>, + operation: &mut dyn widget::Operation<()>, ) { operation.container(None, layout.bounds(), &mut |operation| { self.toasts diff --git a/examples/todos/src/main.rs b/examples/todos/src/main.rs index 8119bc91..c21e1a96 100644 --- a/examples/todos/src/main.rs +++ b/examples/todos/src/main.rs @@ -5,7 +5,7 @@ use iced::widget::{ scrollable, text, text_input, Text, }; use iced::window; -use iced::{Command, Element, Font, Length, Subscription}; +use iced::{Element, Font, Length, Subscription, Task as Command}; use once_cell::sync::Lazy; use serde::{Deserialize, Serialize}; @@ -238,7 +238,10 @@ impl Todos { .spacing(20) .max_width(800); - scrollable(container(content).center_x().padding(40)).into() + scrollable( + container(content).center_x(Length::Fill).padding(40), + ) + .into() } } } @@ -396,10 +399,10 @@ fn view_controls(tasks: &[Task], current_filter: Filter) -> Element<Message> { }; row![ - text(format!( + text!( "{tasks_left} {} left", if tasks_left == 1 { "task" } else { "tasks" } - )) + ) .width(Length::Fill), row![ filter_button("All", Filter::All, current_filter), diff --git a/examples/tour/src/main.rs b/examples/tour/src/main.rs index bae6490d..78086ce9 100644 --- a/examples/tour/src/main.rs +++ b/examples/tour/src/main.rs @@ -21,190 +21,27 @@ pub fn main() -> iced::Result { .run() } -#[derive(Default)] pub struct Tour { - steps: Steps, + screen: Screen, + slider: u8, + layout: Layout, + spacing: u16, + text_size: u16, + text_color: Color, + language: Option<Language>, + toggler: bool, + image_width: u16, + image_filter_method: image::FilterMethod, + input_value: String, + input_is_secure: bool, + input_is_showing_icon: bool, debug: bool, } -impl Tour { - fn title(&self) -> String { - format!("{} - Iced", self.steps.title()) - } - - fn update(&mut self, event: Message) { - match event { - Message::BackPressed => { - self.steps.go_back(); - } - Message::NextPressed => { - self.steps.advance(); - } - Message::StepMessage(step_msg) => { - self.steps.update(step_msg, &mut self.debug); - } - } - } - - fn view(&self) -> Element<Message> { - let Tour { steps, .. } = self; - - let controls = - row![] - .push_maybe(steps.has_previous().then(|| { - padded_button("Back") - .on_press(Message::BackPressed) - .style(button::secondary) - })) - .push(horizontal_space()) - .push_maybe(steps.can_continue().then(|| { - padded_button("Next").on_press(Message::NextPressed) - })); - - let content: Element<_> = column![ - steps.view(self.debug).map(Message::StepMessage), - controls, - ] - .max_width(540) - .spacing(20) - .padding(20) - .into(); - - let scrollable = scrollable( - container(if self.debug { - content.explain(Color::BLACK) - } else { - content - }) - .center_x(), - ); - - container(scrollable).center_y().into() - } -} - #[derive(Debug, Clone)] pub enum Message { BackPressed, NextPressed, - StepMessage(StepMessage), -} - -struct Steps { - steps: Vec<Step>, - current: usize, -} - -impl Steps { - fn new() -> Steps { - Steps { - steps: vec![ - Step::Welcome, - Step::Slider { value: 50 }, - Step::RowsAndColumns { - layout: Layout::Row, - spacing: 20, - }, - Step::Text { - size: 30, - color: Color::BLACK, - }, - Step::Radio { selection: None }, - Step::Toggler { - can_continue: false, - }, - Step::Image { - width: 300, - filter_method: image::FilterMethod::Linear, - }, - Step::Scrollable, - Step::TextInput { - value: String::new(), - is_secure: false, - is_showing_icon: false, - }, - Step::Debugger, - Step::End, - ], - current: 0, - } - } - - fn update(&mut self, msg: StepMessage, debug: &mut bool) { - self.steps[self.current].update(msg, debug); - } - - fn view(&self, debug: bool) -> Element<StepMessage> { - self.steps[self.current].view(debug) - } - - fn advance(&mut self) { - if self.can_continue() { - self.current += 1; - } - } - - fn go_back(&mut self) { - if self.has_previous() { - self.current -= 1; - } - } - - fn has_previous(&self) -> bool { - self.current > 0 - } - - fn can_continue(&self) -> bool { - self.current + 1 < self.steps.len() - && self.steps[self.current].can_continue() - } - - fn title(&self) -> &str { - self.steps[self.current].title() - } -} - -impl Default for Steps { - fn default() -> Self { - Steps::new() - } -} - -enum Step { - Welcome, - Slider { - value: u8, - }, - RowsAndColumns { - layout: Layout, - spacing: u16, - }, - Text { - size: u16, - color: Color, - }, - Radio { - selection: Option<Language>, - }, - Toggler { - can_continue: bool, - }, - Image { - width: u16, - filter_method: image::FilterMethod, - }, - Scrollable, - TextInput { - value: String, - is_secure: bool, - is_showing_icon: bool, - }, - Debugger, - End, -} - -#[derive(Debug, Clone)] -pub enum StepMessage { SliderChanged(u8), LayoutChanged(Layout), SpacingChanged(u16), @@ -220,147 +57,145 @@ pub enum StepMessage { TogglerChanged(bool), } -impl<'a> Step { - fn update(&mut self, msg: StepMessage, debug: &mut bool) { - match msg { - StepMessage::DebugToggled(value) => { - if let Step::Debugger = self { - *debug = value; +impl Tour { + fn title(&self) -> String { + let screen = match self.screen { + Screen::Welcome => "Welcome", + Screen::Radio => "Radio button", + Screen::Toggler => "Toggler", + Screen::Slider => "Slider", + Screen::Text => "Text", + Screen::Image => "Image", + Screen::RowsAndColumns => "Rows and columns", + Screen::Scrollable => "Scrollable", + Screen::TextInput => "Text input", + Screen::Debugger => "Debugger", + Screen::End => "End", + }; + + format!("{} - Iced", screen) + } + + fn update(&mut self, event: Message) { + match event { + Message::BackPressed => { + if let Some(screen) = self.screen.previous() { + self.screen = screen; } } - StepMessage::LanguageSelected(language) => { - if let Step::Radio { selection } = self { - *selection = Some(language); + Message::NextPressed => { + if let Some(screen) = self.screen.next() { + self.screen = screen; } } - StepMessage::SliderChanged(new_value) => { - if let Step::Slider { value, .. } = self { - *value = new_value; - } + Message::SliderChanged(value) => { + self.slider = value; } - StepMessage::TextSizeChanged(new_size) => { - if let Step::Text { size, .. } = self { - *size = new_size; - } + Message::LayoutChanged(layout) => { + self.layout = layout; } - StepMessage::TextColorChanged(new_color) => { - if let Step::Text { color, .. } = self { - *color = new_color; - } + Message::SpacingChanged(spacing) => { + self.spacing = spacing; } - StepMessage::LayoutChanged(new_layout) => { - if let Step::RowsAndColumns { layout, .. } = self { - *layout = new_layout; - } + Message::TextSizeChanged(text_size) => { + self.text_size = text_size; } - StepMessage::SpacingChanged(new_spacing) => { - if let Step::RowsAndColumns { spacing, .. } = self { - *spacing = new_spacing; - } + Message::TextColorChanged(text_color) => { + self.text_color = text_color; } - StepMessage::ImageWidthChanged(new_width) => { - if let Step::Image { width, .. } = self { - *width = new_width; - } + Message::LanguageSelected(language) => { + self.language = Some(language); } - StepMessage::ImageUseNearestToggled(use_nearest) => { - if let Step::Image { filter_method, .. } = self { - *filter_method = if use_nearest { - image::FilterMethod::Nearest - } else { - image::FilterMethod::Linear - }; - } + Message::ImageWidthChanged(image_width) => { + self.image_width = image_width; } - StepMessage::InputChanged(new_value) => { - if let Step::TextInput { value, .. } = self { - *value = new_value; - } + Message::ImageUseNearestToggled(use_nearest) => { + self.image_filter_method = if use_nearest { + image::FilterMethod::Nearest + } else { + image::FilterMethod::Linear + }; } - StepMessage::ToggleSecureInput(toggle) => { - if let Step::TextInput { is_secure, .. } = self { - *is_secure = toggle; - } + Message::InputChanged(input_value) => { + self.input_value = input_value; } - StepMessage::TogglerChanged(value) => { - if let Step::Toggler { can_continue, .. } = self { - *can_continue = value; - } + Message::ToggleSecureInput(is_secure) => { + self.input_is_secure = is_secure; } - StepMessage::ToggleTextInputIcon(toggle) => { - if let Step::TextInput { - is_showing_icon, .. - } = self - { - *is_showing_icon = toggle; - } + Message::ToggleTextInputIcon(show_icon) => { + self.input_is_showing_icon = show_icon; + } + Message::DebugToggled(debug) => { + self.debug = debug; + } + Message::TogglerChanged(toggler) => { + self.toggler = toggler; } - }; - } - - fn title(&self) -> &str { - match self { - Step::Welcome => "Welcome", - Step::Radio { .. } => "Radio button", - Step::Toggler { .. } => "Toggler", - Step::Slider { .. } => "Slider", - Step::Text { .. } => "Text", - Step::Image { .. } => "Image", - Step::RowsAndColumns { .. } => "Rows and columns", - Step::Scrollable => "Scrollable", - Step::TextInput { .. } => "Text input", - Step::Debugger => "Debugger", - Step::End => "End", } } - fn can_continue(&self) -> bool { - match self { - Step::Welcome => true, - Step::Radio { selection } => *selection == Some(Language::Rust), - Step::Toggler { can_continue } => *can_continue, - Step::Slider { .. } => true, - Step::Text { .. } => true, - Step::Image { .. } => true, - Step::RowsAndColumns { .. } => true, - Step::Scrollable => true, - Step::TextInput { value, .. } => !value.is_empty(), - Step::Debugger => true, - Step::End => false, - } - } + fn view(&self) -> Element<Message> { + let controls = + row![] + .push_maybe(self.screen.previous().is_some().then(|| { + padded_button("Back") + .on_press(Message::BackPressed) + .style(button::secondary) + })) + .push(horizontal_space()) + .push_maybe(self.can_continue().then(|| { + padded_button("Next").on_press(Message::NextPressed) + })); - fn view(&self, debug: bool) -> Element<StepMessage> { - match self { - Step::Welcome => Self::welcome(), - Step::Radio { selection } => Self::radio(*selection), - Step::Toggler { can_continue } => Self::toggler(*can_continue), - Step::Slider { value } => Self::slider(*value), - Step::Text { size, color } => Self::text(*size, *color), - Step::Image { - width, - filter_method, - } => Self::image(*width, *filter_method), - Step::RowsAndColumns { layout, spacing } => { - Self::rows_and_columns(*layout, *spacing) - } - Step::Scrollable => Self::scrollable(), - Step::TextInput { - value, - is_secure, - is_showing_icon, - } => Self::text_input(value, *is_secure, *is_showing_icon), - Step::Debugger => Self::debugger(debug), - Step::End => Self::end(), - } - .into() + let screen = match self.screen { + Screen::Welcome => self.welcome(), + Screen::Radio => self.radio(), + Screen::Toggler => self.toggler(), + Screen::Slider => self.slider(), + Screen::Text => self.text(), + Screen::Image => self.image(), + Screen::RowsAndColumns => self.rows_and_columns(), + Screen::Scrollable => self.scrollable(), + Screen::TextInput => self.text_input(), + Screen::Debugger => self.debugger(), + Screen::End => self.end(), + }; + + let content: Element<_> = column![screen, controls,] + .max_width(540) + .spacing(20) + .padding(20) + .into(); + + let scrollable = scrollable( + container(if self.debug { + content.explain(Color::BLACK) + } else { + content + }) + .center_x(Length::Fill), + ); + + container(scrollable).center_y(Length::Fill).into() } - fn container(title: &str) -> Column<'_, StepMessage> { - column![text(title).size(50)].spacing(20) + fn can_continue(&self) -> bool { + match self.screen { + Screen::Welcome => true, + Screen::Radio => self.language == Some(Language::Rust), + Screen::Toggler => self.toggler, + Screen::Slider => true, + Screen::Text => true, + Screen::Image => true, + Screen::RowsAndColumns => true, + Screen::Scrollable => true, + Screen::TextInput => !self.input_value.is_empty(), + Screen::Debugger => true, + Screen::End => false, + } } - fn welcome() -> Column<'a, StepMessage> { + fn welcome(&self) -> Column<Message> { Self::container("Welcome!") .push( "This is a simple tour meant to showcase a bunch of widgets \ @@ -389,7 +224,7 @@ impl<'a> Step { ) } - fn slider(value: u8) -> Column<'a, StepMessage> { + fn slider(&self) -> Column<Message> { Self::container("Slider") .push( "A slider allows you to smoothly select a value from a range \ @@ -399,47 +234,48 @@ impl<'a> Step { "The following slider lets you choose an integer from \ 0 to 100:", ) - .push(slider(0..=100, value, StepMessage::SliderChanged)) + .push(slider(0..=100, self.slider, Message::SliderChanged)) .push( - text(value.to_string()) + text(self.slider.to_string()) .width(Length::Fill) .horizontal_alignment(alignment::Horizontal::Center), ) } - fn rows_and_columns( - layout: Layout, - spacing: u16, - ) -> Column<'a, StepMessage> { - let row_radio = - radio("Row", Layout::Row, Some(layout), StepMessage::LayoutChanged); + fn rows_and_columns(&self) -> Column<Message> { + let row_radio = radio( + "Row", + Layout::Row, + Some(self.layout), + Message::LayoutChanged, + ); let column_radio = radio( "Column", Layout::Column, - Some(layout), - StepMessage::LayoutChanged, + Some(self.layout), + Message::LayoutChanged, ); - let layout_section: Element<_> = match layout { + let layout_section: Element<_> = match self.layout { Layout::Row => { - row![row_radio, column_radio].spacing(spacing).into() - } - Layout::Column => { - column![row_radio, column_radio].spacing(spacing).into() + row![row_radio, column_radio].spacing(self.spacing).into() } + Layout::Column => column![row_radio, column_radio] + .spacing(self.spacing) + .into(), }; let spacing_section = column![ - slider(0..=80, spacing, StepMessage::SpacingChanged), - text(format!("{spacing} px")) + slider(0..=80, self.spacing, Message::SpacingChanged), + text!("{} px", self.spacing) .width(Length::Fill) .horizontal_alignment(alignment::Horizontal::Center), ] .spacing(10); Self::container("Rows and columns") - .spacing(spacing) + .spacing(self.spacing) .push( "Iced uses a layout model based on flexbox to position UI \ elements.", @@ -453,11 +289,14 @@ impl<'a> Step { .push(spacing_section) } - fn text(size: u16, color: Color) -> Column<'a, StepMessage> { + fn text(&self) -> Column<Message> { + let size = self.text_size; + let color = self.text_color; + let size_section = column![ "You can change its size:", - text(format!("This text is {size} pixels")).size(size), - slider(10..=70, size, StepMessage::TextSizeChanged), + text!("This text is {size} pixels").size(size), + slider(10..=70, size, Message::TextSizeChanged), ] .padding(20) .spacing(20); @@ -471,7 +310,7 @@ impl<'a> Step { let color_section = column![ "And its color:", - text(format!("{color:?}")).color(color), + text!("{color:?}").color(color), color_sliders, ] .padding(20) @@ -486,7 +325,7 @@ impl<'a> Step { .push(color_section) } - fn radio(selection: Option<Language>) -> Column<'a, StepMessage> { + fn radio(&self) -> Column<Message> { let question = column![ text("Iced is written in...").size(24), column( @@ -497,8 +336,8 @@ impl<'a> Step { radio( language, language, - selection, - StepMessage::LanguageSelected, + self.language, + Message::LanguageSelected, ) }) .map(Element::from) @@ -521,29 +360,29 @@ impl<'a> Step { ) } - fn toggler(can_continue: bool) -> Column<'a, StepMessage> { + fn toggler(&self) -> Column<Message> { Self::container("Toggler") .push("A toggler is mostly used to enable or disable something.") .push( Container::new(toggler( "Toggle me to continue...".to_owned(), - can_continue, - StepMessage::TogglerChanged, + self.toggler, + Message::TogglerChanged, )) .padding([0, 40]), ) } - fn image( - width: u16, - filter_method: image::FilterMethod, - ) -> Column<'a, StepMessage> { + fn image(&self) -> Column<Message> { + let width = self.image_width; + let filter_method = self.image_filter_method; + Self::container("Image") .push("An image that tries to keep its aspect ratio.") .push(ferris(width, filter_method)) - .push(slider(100..=500, width, StepMessage::ImageWidthChanged)) + .push(slider(100..=500, width, Message::ImageWidthChanged)) .push( - text(format!("Width: {width} px")) + text!("Width: {width} px") .width(Length::Fill) .horizontal_alignment(alignment::Horizontal::Center), ) @@ -552,12 +391,12 @@ impl<'a> Step { "Use nearest interpolation", filter_method == image::FilterMethod::Nearest, ) - .on_toggle(StepMessage::ImageUseNearestToggled), + .on_toggle(Message::ImageUseNearestToggled), ) .align_items(Alignment::Center) } - fn scrollable() -> Column<'a, StepMessage> { + fn scrollable(&self) -> Column<Message> { Self::container("Scrollable") .push( "Iced supports scrollable content. Try it out! Find the \ @@ -584,13 +423,13 @@ impl<'a> Step { ) } - fn text_input( - value: &str, - is_secure: bool, - is_showing_icon: bool, - ) -> Column<'_, StepMessage> { + fn text_input(&self) -> Column<Message> { + let value = &self.input_value; + let is_secure = self.input_is_secure; + let is_showing_icon = self.input_is_showing_icon; + let mut text_input = text_input("Type something to continue...", value) - .on_input(StepMessage::InputChanged) + .on_input(Message::InputChanged) .padding(10) .size(30); @@ -609,11 +448,11 @@ impl<'a> Step { .push(text_input.secure(is_secure)) .push( checkbox("Enable password mode", is_secure) - .on_toggle(StepMessage::ToggleSecureInput), + .on_toggle(Message::ToggleSecureInput), ) .push( checkbox("Show icon", is_showing_icon) - .on_toggle(StepMessage::ToggleTextInputIcon), + .on_toggle(Message::ToggleTextInputIcon), ) .push( "A text input produces a message every time it changes. It is \ @@ -630,7 +469,7 @@ impl<'a> Step { ) } - fn debugger(debug: bool) -> Column<'a, StepMessage> { + fn debugger(&self) -> Column<Message> { Self::container("Debugger") .push( "You can ask Iced to visually explain the layouting of the \ @@ -641,23 +480,85 @@ impl<'a> Step { see element boundaries.", ) .push( - checkbox("Explain layout", debug) - .on_toggle(StepMessage::DebugToggled), + checkbox("Explain layout", self.debug) + .on_toggle(Message::DebugToggled), ) .push("Feel free to go back and take a look.") } - fn end() -> Column<'a, StepMessage> { + fn end(&self) -> Column<Message> { Self::container("You reached the end!") .push("This tour will be updated as more features are added.") .push("Make sure to keep an eye on it!") } + + fn container(title: &str) -> Column<'_, Message> { + column![text(title).size(50)].spacing(20) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum Screen { + Welcome, + Slider, + RowsAndColumns, + Text, + Radio, + Toggler, + Image, + Scrollable, + TextInput, + Debugger, + End, +} + +impl Screen { + const ALL: &'static [Self] = &[ + Self::Welcome, + Self::Slider, + Self::RowsAndColumns, + Self::Text, + Self::Radio, + Self::Toggler, + Self::Image, + Self::Scrollable, + Self::TextInput, + Self::Debugger, + Self::End, + ]; + + pub fn next(self) -> Option<Screen> { + Self::ALL + .get( + Self::ALL + .iter() + .copied() + .position(|screen| screen == self) + .expect("Screen must exist") + + 1, + ) + .copied() + } + + pub fn previous(self) -> Option<Screen> { + let position = Self::ALL + .iter() + .copied() + .position(|screen| screen == self) + .expect("Screen must exist"); + + if position > 0 { + Some(Self::ALL[position - 1]) + } else { + None + } + } } fn ferris<'a>( width: u16, filter_method: image::FilterMethod, -) -> Container<'a, StepMessage> { +) -> Container<'a, Message> { container( // This should go away once we unify resource loading on native // platforms @@ -669,7 +570,7 @@ fn ferris<'a>( .filter_method(filter_method) .width(width), ) - .center_x() + .center_x(Length::Fill) } fn padded_button<Message: Clone>(label: &str) -> Button<'_, Message> { @@ -679,9 +580,9 @@ fn padded_button<Message: Clone>(label: &str) -> Button<'_, Message> { fn color_slider<'a>( component: f32, update: impl Fn(f32) -> Color + 'a, -) -> Slider<'a, f64, StepMessage> { +) -> Slider<'a, f64, Message> { slider(0.0..=1.0, f64::from(component), move |c| { - StepMessage::TextColorChanged(update(c as f32)) + Message::TextColorChanged(update(c as f32)) }) .step(0.01) } @@ -727,3 +628,24 @@ pub enum Layout { Row, Column, } + +impl Default for Tour { + fn default() -> Self { + Self { + screen: Screen::Welcome, + slider: 50, + layout: Layout::Row, + spacing: 20, + text_size: 30, + text_color: Color::BLACK, + language: None, + toggler: false, + image_width: 300, + image_filter_method: image::FilterMethod::Linear, + input_value: String::new(), + input_is_secure: false, + input_is_showing_icon: false, + debug: false, + } + } +} diff --git a/examples/url_handler/src/main.rs b/examples/url_handler/src/main.rs index 800a188b..3ab19252 100644 --- a/examples/url_handler/src/main.rs +++ b/examples/url_handler/src/main.rs @@ -1,4 +1,4 @@ -use iced::event::{self, Event}; +use iced::event; use iced::widget::{center, text}; use iced::{Element, Subscription}; @@ -15,27 +15,20 @@ struct App { #[derive(Debug, Clone)] enum Message { - EventOccurred(Event), + UrlReceived(String), } impl App { fn update(&mut self, message: Message) { match message { - Message::EventOccurred(event) => { - if let Event::PlatformSpecific( - event::PlatformSpecific::MacOS(event::MacOS::ReceivedUrl( - url, - )), - ) = event - { - self.url = Some(url); - } + Message::UrlReceived(url) => { + self.url = Some(url); } } } fn subscription(&self) -> Subscription<Message> { - event::listen().map(Message::EventOccurred) + event::listen_url().map(Message::UrlReceived) } fn view(&self) -> Element<Message> { diff --git a/examples/vectorial_text/src/main.rs b/examples/vectorial_text/src/main.rs index a7391e23..1ed7a2b1 100644 --- a/examples/vectorial_text/src/main.rs +++ b/examples/vectorial_text/src/main.rs @@ -52,11 +52,7 @@ impl VectorialText { fn view(&self) -> Element<Message> { let slider_with_label = |label, range, value, message: fn(f32) -> _| { column![ - row![ - text(label), - horizontal_space(), - text(format!("{:.2}", value)) - ], + row![text(label), horizontal_space(), text!("{:.2}", value)], slider(range, value, message).step(0.01) ] .spacing(2) diff --git a/examples/visible_bounds/src/main.rs b/examples/visible_bounds/src/main.rs index 332b6a7b..b43c0cca 100644 --- a/examples/visible_bounds/src/main.rs +++ b/examples/visible_bounds/src/main.rs @@ -5,8 +5,8 @@ use iced::widget::{ }; use iced::window; use iced::{ - Alignment, Color, Command, Element, Font, Length, Point, Rectangle, - Subscription, Theme, + Alignment, Color, Element, Font, Length, Point, Rectangle, Subscription, + Task, Theme, }; pub fn main() -> iced::Result { @@ -33,14 +33,14 @@ enum Message { } impl Example { - fn update(&mut self, message: Message) -> Command<Message> { + fn update(&mut self, message: Message) -> Task<Message> { match message { Message::MouseMoved(position) => { self.mouse_position = Some(position); - Command::none() + Task::none() } - Message::Scrolled | Message::WindowResized => Command::batch(vec![ + Message::Scrolled | Message::WindowResized => Task::batch(vec![ container::visible_bounds(OUTER_CONTAINER.clone()) .map(Message::OuterBoundsFetched), container::visible_bounds(INNER_CONTAINER.clone()) @@ -49,12 +49,12 @@ impl Example { Message::OuterBoundsFetched(outer_bounds) => { self.outer_bounds = outer_bounds; - Command::none() + Task::none() } Message::InnerBoundsFetched(inner_bounds) => { self.inner_bounds = inner_bounds; - Command::none() + Task::none() } } } @@ -145,11 +145,11 @@ impl Example { } fn subscription(&self) -> Subscription<Message> { - event::listen_with(|event, _| match event { + event::listen_with(|event, _status, _window| match event { Event::Mouse(mouse::Event::CursorMoved { position }) => { Some(Message::MouseMoved(position)) } - Event::Window(_, window::Event::Resized { .. }) => { + Event::Window(window::Event::Resized { .. }) => { Some(Message::WindowResized) } _ => None, diff --git a/examples/websocket/src/main.rs b/examples/websocket/src/main.rs index ba1e1029..8c0fa1d0 100644 --- a/examples/websocket/src/main.rs +++ b/examples/websocket/src/main.rs @@ -4,7 +4,7 @@ use iced::alignment::{self, Alignment}; use iced::widget::{ self, button, center, column, row, scrollable, text, text_input, }; -use iced::{color, Command, Element, Length, Subscription}; +use iced::{color, Element, Length, Subscription, Task}; use once_cell::sync::Lazy; pub fn main() -> iced::Result { @@ -30,19 +30,19 @@ enum Message { } impl WebSocket { - fn load() -> Command<Message> { - Command::batch([ - Command::perform(echo::server::run(), |_| Message::Server), + fn load() -> Task<Message> { + Task::batch([ + Task::perform(echo::server::run(), |_| Message::Server), widget::focus_next(), ]) } - fn update(&mut self, message: Message) -> Command<Message> { + fn update(&mut self, message: Message) -> Task<Message> { match message { Message::NewMessageChanged(new_message) => { self.new_message = new_message; - Command::none() + Task::none() } Message::Send(message) => match &mut self.state { State::Connected(connection) => { @@ -50,9 +50,9 @@ impl WebSocket { connection.send(message); - Command::none() + Task::none() } - State::Disconnected => Command::none(), + State::Disconnected => Task::none(), }, Message::Echo(event) => match event { echo::Event::Connected(connection) => { @@ -60,14 +60,14 @@ impl WebSocket { self.messages.push(echo::Message::connected()); - Command::none() + Task::none() } echo::Event::Disconnected => { self.state = State::Disconnected; self.messages.push(echo::Message::disconnected()); - Command::none() + Task::none() } echo::Event::MessageReceived(message) => { self.messages.push(message); @@ -78,7 +78,7 @@ impl WebSocket { ) } }, - Message::Server => Command::none(), + Message::Server => Task::none(), } } diff --git a/futures/src/event.rs b/futures/src/event.rs index 97224506..72ea78ad 100644 --- a/futures/src/event.rs +++ b/futures/src/event.rs @@ -9,7 +9,7 @@ use crate::MaybeSend; /// This subscription will notify your application of any [`Event`] that was /// not captured by any widget. pub fn listen() -> Subscription<Event> { - listen_with(|event, status| match status { + listen_with(|event, status, _window| match status { event::Status::Ignored => Some(event), event::Status::Captured => None, }) @@ -24,7 +24,7 @@ pub fn listen() -> Subscription<Event> { /// - Returns `None`, the [`Event`] will be discarded. /// - Returns `Some` message, the `Message` will be produced. pub fn listen_with<Message>( - f: fn(Event, event::Status) -> Option<Message>, + f: fn(Event, event::Status, window::Id) -> Option<Message>, ) -> Subscription<Message> where Message: 'static + MaybeSend, @@ -32,13 +32,18 @@ where #[derive(Hash)] struct EventsWith; - subscription::filter_map( - (EventsWith, f), - move |event, status| match event { - Event::Window(_, window::Event::RedrawRequested(_)) => None, - _ => f(event, status), - }, - ) + subscription::filter_map((EventsWith, f), move |event| match event { + subscription::Event::Interaction { + event: Event::Window(window::Event::RedrawRequested(_)), + .. + } + | subscription::Event::PlatformSpecific(_) => None, + subscription::Event::Interaction { + window, + event, + status, + } => f(event, status, window), + }) } /// Creates a [`Subscription`] that produces a message for every runtime event, @@ -47,7 +52,7 @@ where /// **Warning:** This [`Subscription`], if unfiltered, may produce messages in /// an infinite loop. pub fn listen_raw<Message>( - f: fn(Event, event::Status) -> Option<Message>, + f: fn(Event, event::Status, window::Id) -> Option<Message>, ) -> Subscription<Message> where Message: 'static + MaybeSend, @@ -55,5 +60,32 @@ where #[derive(Hash)] struct RawEvents; - subscription::filter_map((RawEvents, f), f) + subscription::filter_map((RawEvents, f), move |event| match event { + subscription::Event::Interaction { + window, + event, + status, + } => f(event, status, window), + subscription::Event::PlatformSpecific(_) => None, + }) +} + +/// Creates a [`Subscription`] that notifies of custom application URL +/// received from the system. +/// +/// _**Note:** Currently, it only triggers on macOS and the executable needs to be properly [bundled]!_ +/// +/// [bundled]: https://developer.apple.com/library/archive/documentation/CoreFoundation/Conceptual/CFBundles/BundleTypes/BundleTypes.html#//apple_ref/doc/uid/10000123i-CH101-SW19 +pub fn listen_url() -> Subscription<String> { + #[derive(Hash)] + struct ListenUrl; + + subscription::filter_map(ListenUrl, move |event| match event { + subscription::Event::PlatformSpecific( + subscription::PlatformSpecific::MacOS( + subscription::MacOS::ReceivedUrl(url), + ), + ) => Some(url), + _ => None, + }) } diff --git a/futures/src/executor.rs b/futures/src/executor.rs index 5ac76081..3b0d4af1 100644 --- a/futures/src/executor.rs +++ b/futures/src/executor.rs @@ -1,5 +1,6 @@ //! Choose your preferred executor to power a runtime. use crate::MaybeSend; + use futures::Future; /// A type that can run futures. diff --git a/futures/src/keyboard.rs b/futures/src/keyboard.rs index 8e7da38f..f0d7d757 100644 --- a/futures/src/keyboard.rs +++ b/futures/src/keyboard.rs @@ -1,5 +1,6 @@ //! Listen to keyboard events. use crate::core; +use crate::core::event; use crate::core::keyboard::{Event, Key, Modifiers}; use crate::subscription::{self, Subscription}; use crate::MaybeSend; @@ -18,16 +19,14 @@ where #[derive(Hash)] struct OnKeyPress; - subscription::filter_map((OnKeyPress, f), move |event, status| { - match (event, status) { - ( - core::Event::Keyboard(Event::KeyPressed { - key, modifiers, .. - }), - core::event::Status::Ignored, - ) => f(key, modifiers), - _ => None, - } + subscription::filter_map((OnKeyPress, f), move |event| match event { + subscription::Event::Interaction { + event: + core::Event::Keyboard(Event::KeyPressed { key, modifiers, .. }), + status: event::Status::Ignored, + .. + } => f(key, modifiers), + _ => None, }) } @@ -45,17 +44,13 @@ where #[derive(Hash)] struct OnKeyRelease; - subscription::filter_map((OnKeyRelease, f), move |event, status| { - match (event, status) { - ( - core::Event::Keyboard(Event::KeyReleased { - key, - modifiers, - .. - }), - core::event::Status::Ignored, - ) => f(key, modifiers), - _ => None, - } + subscription::filter_map((OnKeyRelease, f), move |event| match event { + subscription::Event::Interaction { + event: + core::Event::Keyboard(Event::KeyReleased { key, modifiers, .. }), + status: event::Status::Ignored, + .. + } => f(key, modifiers), + _ => None, }) } diff --git a/futures/src/runtime.rs b/futures/src/runtime.rs index cac7b7e1..157e2c67 100644 --- a/futures/src/runtime.rs +++ b/futures/src/runtime.rs @@ -1,5 +1,4 @@ //! Run commands and keep track of subscriptions. -use crate::core::event::{self, Event}; use crate::subscription; use crate::{BoxFuture, BoxStream, Executor, MaybeSend}; @@ -127,7 +126,7 @@ where /// See [`Tracker::broadcast`] to learn more. /// /// [`Tracker::broadcast`]: subscription::Tracker::broadcast - pub fn broadcast(&mut self, event: Event, status: event::Status) { - self.subscriptions.broadcast(event, status); + pub fn broadcast(&mut self, event: subscription::Event) { + self.subscriptions.broadcast(event); } } diff --git a/futures/src/subscription.rs b/futures/src/subscription.rs index 93e35608..316fc44d 100644 --- a/futures/src/subscription.rs +++ b/futures/src/subscription.rs @@ -3,7 +3,8 @@ mod tracker; pub use tracker::Tracker; -use crate::core::event::{self, Event}; +use crate::core::event; +use crate::core::window; use crate::futures::{Future, Stream}; use crate::{BoxStream, MaybeSend}; @@ -12,10 +13,48 @@ use futures::never::Never; use std::any::TypeId; use std::hash::Hash; +/// A subscription event. +#[derive(Debug, Clone, PartialEq)] +pub enum Event { + /// A user interacted with a user interface in a window. + Interaction { + /// The window holding the interface of the interaction. + window: window::Id, + /// The [`Event`] describing the interaction. + /// + /// [`Event`]: event::Event + event: event::Event, + + /// The [`event::Status`] of the interaction. + status: event::Status, + }, + + /// A platform specific event. + PlatformSpecific(PlatformSpecific), +} + +/// A platform specific event +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum PlatformSpecific { + /// A MacOS specific event + MacOS(MacOS), +} + +/// Describes an event specific to MacOS +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum MacOS { + /// Triggered when the app receives an URL from the system + /// + /// _**Note:** For this event to be triggered, the executable needs to be properly [bundled]!_ + /// + /// [bundled]: https://developer.apple.com/library/archive/documentation/CoreFoundation/Conceptual/CFBundles/BundleTypes/BundleTypes.html#//apple_ref/doc/uid/10000123i-CH101-SW19 + ReceivedUrl(String), +} + /// A stream of runtime events. /// /// It is the input of a [`Subscription`]. -pub type EventStream = BoxStream<(Event, event::Status)>; +pub type EventStream = BoxStream<Event>; /// The hasher used for identifying subscriptions. pub type Hasher = rustc_hash::FxHasher; @@ -289,7 +328,7 @@ where pub(crate) fn filter_map<I, F, Message>(id: I, f: F) -> Subscription<Message> where I: Hash + 'static, - F: Fn(Event, event::Status) -> Option<Message> + MaybeSend + 'static, + F: Fn(Event) -> Option<Message> + MaybeSend + 'static, Message: 'static + MaybeSend, { Subscription::from_recipe(Runner { @@ -298,9 +337,7 @@ where use futures::future; use futures::stream::StreamExt; - events.filter_map(move |(event, status)| { - future::ready(f(event, status)) - }) + events.filter_map(move |event| future::ready(f(event))) }, }) } diff --git a/futures/src/subscription/tracker.rs b/futures/src/subscription/tracker.rs index 277a446b..f17e3ea3 100644 --- a/futures/src/subscription/tracker.rs +++ b/futures/src/subscription/tracker.rs @@ -1,5 +1,4 @@ -use crate::core::event::{self, Event}; -use crate::subscription::{Hasher, Recipe}; +use crate::subscription::{Event, Hasher, Recipe}; use crate::{BoxFuture, MaybeSend}; use futures::channel::mpsc; @@ -23,7 +22,7 @@ pub struct Tracker { #[derive(Debug)] pub struct Execution { _cancel: futures::channel::oneshot::Sender<()>, - listener: Option<futures::channel::mpsc::Sender<(Event, event::Status)>>, + listener: Option<futures::channel::mpsc::Sender<Event>>, } impl Tracker { @@ -139,12 +138,12 @@ impl Tracker { /// currently open. /// /// [`Recipe::stream`]: crate::subscription::Recipe::stream - pub fn broadcast(&mut self, event: Event, status: event::Status) { + pub fn broadcast(&mut self, event: Event) { self.subscriptions .values_mut() .filter_map(|connection| connection.listener.as_mut()) .for_each(|listener| { - if let Err(error) = listener.try_send((event.clone(), status)) { + if let Err(error) = listener.try_send(event.clone()) { log::warn!( "Error sending event to subscription: {error:?}" ); diff --git a/graphics/src/text/editor.rs b/graphics/src/text/editor.rs index 4b8f0f2a..c488a51c 100644 --- a/graphics/src/text/editor.rs +++ b/graphics/src/text/editor.rs @@ -456,14 +456,10 @@ impl editor::Editor for Editor { } } Action::Scroll { lines } => { - let (_, height) = editor.buffer().size(); - - if height < i32::MAX as f32 { - editor.action( - font_system.raw(), - cosmic_text::Action::Scroll { lines }, - ); - } + editor.action( + font_system.raw(), + cosmic_text::Action::Scroll { lines }, + ); } } diff --git a/renderer/src/lib.rs b/renderer/src/lib.rs index 056da5ed..220542e1 100644 --- a/renderer/src/lib.rs +++ b/renderer/src/lib.rs @@ -48,6 +48,13 @@ mod renderer { #[cfg(not(any(feature = "wgpu", feature = "tiny-skia")))] mod renderer { + #[cfg(not(debug_assertions))] + compile_error!( + "Cannot compile `iced_renderer` in release mode \ + without a renderer feature enabled. \ + Enable either the `wgpu` or `tiny-skia` feature, or both." + ); + pub type Renderer = (); pub type Compositor = (); } diff --git a/runtime/src/clipboard.rs b/runtime/src/clipboard.rs index dd47c47d..19950d01 100644 --- a/runtime/src/clipboard.rs +++ b/runtime/src/clipboard.rs @@ -1,80 +1,62 @@ //! Access the clipboard. -use crate::command::{self, Command}; use crate::core::clipboard::Kind; -use crate::futures::MaybeSend; +use crate::futures::futures::channel::oneshot; +use crate::Task; -use std::fmt; - -/// A clipboard action to be performed by some [`Command`]. +/// A clipboard action to be performed by some [`Task`]. /// -/// [`Command`]: crate::Command -pub enum Action<T> { +/// [`Task`]: crate::Task +#[derive(Debug)] +pub enum Action { /// Read the clipboard and produce `T` with the result. - Read(Box<dyn Fn(Option<String>) -> T>, Kind), + Read { + /// The clipboard target. + target: Kind, + /// The channel to send the read contents. + channel: oneshot::Sender<Option<String>>, + }, /// Write the given contents to the clipboard. - Write(String, Kind), -} - -impl<T> Action<T> { - /// Maps the output of a clipboard [`Action`] using the provided closure. - pub fn map<A>( - self, - f: impl Fn(T) -> A + 'static + MaybeSend + Sync, - ) -> Action<A> - where - T: 'static, - { - match self { - Self::Read(o, target) => { - Action::Read(Box::new(move |s| f(o(s))), target) - } - Self::Write(content, target) => Action::Write(content, target), - } - } -} - -impl<T> fmt::Debug for Action<T> { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::Read(_, target) => write!(f, "Action::Read{target:?}"), - Self::Write(_, target) => write!(f, "Action::Write({target:?})"), - } - } + Write { + /// The clipboard target. + target: Kind, + /// The contents to be written. + contents: String, + }, } /// Read the current contents of the clipboard. -pub fn read<Message>( - f: impl Fn(Option<String>) -> Message + 'static, -) -> Command<Message> { - Command::single(command::Action::Clipboard(Action::Read( - Box::new(f), - Kind::Standard, - ))) +pub fn read() -> Task<Option<String>> { + Task::oneshot(|channel| { + crate::Action::Clipboard(Action::Read { + target: Kind::Standard, + channel, + }) + }) } /// Read the current contents of the primary clipboard. -pub fn read_primary<Message>( - f: impl Fn(Option<String>) -> Message + 'static, -) -> Command<Message> { - Command::single(command::Action::Clipboard(Action::Read( - Box::new(f), - Kind::Primary, - ))) +pub fn read_primary() -> Task<Option<String>> { + Task::oneshot(|channel| { + crate::Action::Clipboard(Action::Read { + target: Kind::Primary, + channel, + }) + }) } /// Write the given contents to the clipboard. -pub fn write<Message>(contents: String) -> Command<Message> { - Command::single(command::Action::Clipboard(Action::Write( +pub fn write<T>(contents: String) -> Task<T> { + Task::effect(crate::Action::Clipboard(Action::Write { + target: Kind::Standard, contents, - Kind::Standard, - ))) + })) } /// Write the given contents to the primary clipboard. -pub fn write_primary<Message>(contents: String) -> Command<Message> { - Command::single(command::Action::Clipboard(Action::Write( +pub fn write_primary<Message>(contents: String) -> Task<Message> { + Task::effect(crate::Action::Clipboard(Action::Write { + target: Kind::Primary, contents, - Kind::Primary, - ))) + })) } diff --git a/runtime/src/command.rs b/runtime/src/command.rs deleted file mode 100644 index f7a746fe..00000000 --- a/runtime/src/command.rs +++ /dev/null @@ -1,147 +0,0 @@ -//! Run asynchronous actions. -mod action; - -pub use action::Action; - -use crate::core::widget; -use crate::futures::futures; -use crate::futures::MaybeSend; - -use futures::channel::mpsc; -use futures::Stream; -use std::fmt; -use std::future::Future; - -/// A set of asynchronous actions to be performed by some runtime. -#[must_use = "`Command` must be returned to runtime to take effect"] -pub struct Command<T>(Internal<Action<T>>); - -#[derive(Debug)] -enum Internal<T> { - None, - Single(T), - Batch(Vec<T>), -} - -impl<T> Command<T> { - /// Creates an empty [`Command`]. - /// - /// In other words, a [`Command`] that does nothing. - pub const fn none() -> Self { - Self(Internal::None) - } - - /// Creates a [`Command`] that performs a single [`Action`]. - pub const fn single(action: Action<T>) -> Self { - Self(Internal::Single(action)) - } - - /// Creates a [`Command`] that performs a [`widget::Operation`]. - pub fn widget(operation: impl widget::Operation<T> + 'static) -> Self { - Self::single(Action::Widget(Box::new(operation))) - } - - /// Creates a [`Command`] that performs the action of the given future. - pub fn perform<A>( - future: impl Future<Output = A> + 'static + MaybeSend, - f: impl FnOnce(A) -> T + 'static + MaybeSend, - ) -> Command<T> { - use futures::FutureExt; - - Command::single(Action::Future(Box::pin(future.map(f)))) - } - - /// Creates a [`Command`] that runs the given stream to completion. - pub fn run<A>( - stream: impl Stream<Item = A> + 'static + MaybeSend, - f: impl Fn(A) -> T + 'static + MaybeSend, - ) -> Command<T> { - use futures::StreamExt; - - Command::single(Action::Stream(Box::pin(stream.map(f)))) - } - - /// Creates a [`Command`] that performs the actions of all the given - /// commands. - /// - /// Once this command is run, all the commands will be executed at once. - pub fn batch(commands: impl IntoIterator<Item = Command<T>>) -> Self { - let mut batch = Vec::new(); - - for Command(command) in commands { - match command { - Internal::None => {} - Internal::Single(command) => batch.push(command), - Internal::Batch(commands) => batch.extend(commands), - } - } - - Self(Internal::Batch(batch)) - } - - /// Applies a transformation to the result of a [`Command`]. - pub fn map<A>( - self, - f: impl Fn(T) -> A + 'static + MaybeSend + Sync + Clone, - ) -> Command<A> - where - T: 'static, - A: 'static, - { - match self.0 { - Internal::None => Command::none(), - Internal::Single(action) => Command::single(action.map(f)), - Internal::Batch(batch) => Command(Internal::Batch( - batch - .into_iter() - .map(|action| action.map(f.clone())) - .collect(), - )), - } - } - - /// Returns all of the actions of the [`Command`]. - pub fn actions(self) -> Vec<Action<T>> { - let Command(command) = self; - - match command { - Internal::None => Vec::new(), - Internal::Single(action) => vec![action], - Internal::Batch(batch) => batch, - } - } -} - -impl<Message> From<()> for Command<Message> { - fn from(_value: ()) -> Self { - Self::none() - } -} - -impl<T> fmt::Debug for Command<T> { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let Command(command) = self; - - command.fmt(f) - } -} - -/// Creates a [`Command`] that produces the `Message`s published from a [`Future`] -/// to an [`mpsc::Sender`] with the given bounds. -pub fn channel<Fut, Message>( - size: usize, - f: impl FnOnce(mpsc::Sender<Message>) -> Fut + MaybeSend + 'static, -) -> Command<Message> -where - Fut: Future<Output = ()> + MaybeSend + 'static, - Message: 'static + MaybeSend, -{ - use futures::future; - use futures::stream::{self, StreamExt}; - - let (sender, receiver) = mpsc::channel(size); - - let runner = stream::once(f(sender)).filter_map(|_| future::ready(None)); - - Command::single(Action::Stream(Box::pin(stream::select(receiver, runner)))) -} diff --git a/runtime/src/command/action.rs b/runtime/src/command/action.rs deleted file mode 100644 index c9ffe801..00000000 --- a/runtime/src/command/action.rs +++ /dev/null @@ -1,100 +0,0 @@ -use crate::clipboard; -use crate::core::widget; -use crate::font; -use crate::futures::MaybeSend; -use crate::system; -use crate::window; - -use std::any::Any; -use std::borrow::Cow; -use std::fmt; - -/// An action that a [`Command`] can perform. -/// -/// [`Command`]: crate::Command -pub enum Action<T> { - /// Run a [`Future`] to completion. - /// - /// [`Future`]: iced_futures::BoxFuture - Future(iced_futures::BoxFuture<T>), - - /// Run a [`Stream`] to completion. - /// - /// [`Stream`]: iced_futures::BoxStream - Stream(iced_futures::BoxStream<T>), - - /// Run a clipboard action. - Clipboard(clipboard::Action<T>), - - /// Run a window action. - Window(window::Action<T>), - - /// Run a system action. - System(system::Action<T>), - - /// Run a widget action. - Widget(Box<dyn widget::Operation<T>>), - - /// Load a font from its bytes. - LoadFont { - /// The bytes of the font to load. - bytes: Cow<'static, [u8]>, - - /// The message to produce when the font has been loaded. - tagger: Box<dyn Fn(Result<(), font::Error>) -> T>, - }, - - /// A custom action supported by a specific runtime. - Custom(Box<dyn Any>), -} - -impl<T> Action<T> { - /// Applies a transformation to the result of a [`Command`]. - /// - /// [`Command`]: crate::Command - pub fn map<A>( - self, - f: impl Fn(T) -> A + 'static + MaybeSend + Sync, - ) -> Action<A> - where - A: 'static, - T: 'static, - { - use iced_futures::futures::{FutureExt, StreamExt}; - - match self { - Self::Future(future) => Action::Future(Box::pin(future.map(f))), - Self::Stream(stream) => Action::Stream(Box::pin(stream.map(f))), - Self::Clipboard(action) => Action::Clipboard(action.map(f)), - Self::Window(window) => Action::Window(window.map(f)), - Self::System(system) => Action::System(system.map(f)), - Self::Widget(operation) => { - Action::Widget(Box::new(widget::operation::map(operation, f))) - } - Self::LoadFont { bytes, tagger } => Action::LoadFont { - bytes, - tagger: Box::new(move |result| f(tagger(result))), - }, - Self::Custom(custom) => Action::Custom(custom), - } - } -} - -impl<T> fmt::Debug for Action<T> { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::Future(_) => write!(f, "Action::Future"), - Self::Stream(_) => write!(f, "Action::Stream"), - Self::Clipboard(action) => { - write!(f, "Action::Clipboard({action:?})") - } - Self::Window(action) => { - write!(f, "Action::Window({action:?})") - } - Self::System(action) => write!(f, "Action::System({action:?})"), - Self::Widget(_action) => write!(f, "Action::Widget"), - Self::LoadFont { .. } => write!(f, "Action::LoadFont"), - Self::Custom(_) => write!(f, "Action::Custom"), - } - } -} diff --git a/runtime/src/font.rs b/runtime/src/font.rs index 15359694..d54eb6a8 100644 --- a/runtime/src/font.rs +++ b/runtime/src/font.rs @@ -1,7 +1,5 @@ //! Load and use fonts. -pub use iced_core::font::*; - -use crate::command::{self, Command}; +use crate::{Action, Task}; use std::borrow::Cow; /// An error while loading a font. @@ -9,11 +7,9 @@ use std::borrow::Cow; pub enum Error {} /// Load a font from its bytes. -pub fn load( - bytes: impl Into<Cow<'static, [u8]>>, -) -> Command<Result<(), Error>> { - Command::single(command::Action::LoadFont { +pub fn load(bytes: impl Into<Cow<'static, [u8]>>) -> Task<Result<(), Error>> { + Task::oneshot(|channel| Action::LoadFont { bytes: bytes.into(), - tagger: Box::new(std::convert::identity), + channel, }) } diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 5f054c46..5fde3039 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -10,7 +10,6 @@ )] #![cfg_attr(docsrs, feature(doc_auto_cfg))] pub mod clipboard; -pub mod command; pub mod font; pub mod keyboard; pub mod overlay; @@ -22,6 +21,8 @@ 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")] @@ -34,8 +35,81 @@ mod debug; pub use iced_core as core; pub use iced_futures as futures; -pub use command::Command; pub use debug::Debug; -pub use font::Font; pub use program::Program; +pub use task::Task; pub use user_interface::UserInterface; + +use crate::core::widget; +use crate::futures::futures::channel::oneshot; + +use std::borrow::Cow; +use std::fmt; + +/// An action that the iced runtime can perform. +pub enum Action<T> { + /// Output some value. + Output(T), + + /// Load a font from its bytes. + LoadFont { + /// The bytes of the font to load. + bytes: Cow<'static, [u8]>, + /// The channel to send back the load result. + channel: oneshot::Sender<Result<(), font::Error>>, + }, + + /// Run a widget operation. + Widget(Box<dyn widget::Operation<()> + Send>), + + /// Run a clipboard action. + Clipboard(clipboard::Action), + + /// Run a window action. + Window(window::Action), + + /// Run a system action. + System(system::Action), +} + +impl<T> Action<T> { + /// Creates a new [`Action::Widget`] with the given [`widget::Operation`]. + pub fn widget(operation: impl widget::Operation<()> + 'static) -> Self { + Self::Widget(Box::new(operation)) + } + + fn output<O>(self) -> Result<T, Action<O>> { + match self { + Action::Output(output) => Ok(output), + Action::LoadFont { bytes, channel } => { + Err(Action::LoadFont { bytes, channel }) + } + Action::Widget(operation) => Err(Action::Widget(operation)), + Action::Clipboard(action) => Err(Action::Clipboard(action)), + Action::Window(action) => Err(Action::Window(action)), + Action::System(action) => Err(Action::System(action)), + } + } +} + +impl<T> fmt::Debug for Action<T> +where + T: fmt::Debug, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Action::Output(output) => write!(f, "Action::Output({output:?})"), + Action::LoadFont { .. } => { + write!(f, "Action::LoadFont") + } + Action::Widget { .. } => { + write!(f, "Action::Widget") + } + Action::Clipboard(action) => { + write!(f, "Action::Clipboard({action:?})") + } + Action::Window(_) => write!(f, "Action::Window"), + Action::System(action) => write!(f, "Action::System({action:?})"), + } + } +} diff --git a/runtime/src/multi_window/program.rs b/runtime/src/multi_window/program.rs index 963a09d7..e8c71b26 100644 --- a/runtime/src/multi_window/program.rs +++ b/runtime/src/multi_window/program.rs @@ -2,7 +2,7 @@ use crate::core::text; use crate::core::window; use crate::core::{Element, Renderer}; -use crate::Command; +use crate::Task; /// The core of a user interface for a multi-window application following The Elm Architecture. pub trait Program: Sized { @@ -21,9 +21,9 @@ pub trait Program: Sized { /// produced by either user interactions or commands, will be handled by /// this method. /// - /// Any [`Command`] returned will be executed immediately in the - /// background by shells. - fn update(&mut self, message: Self::Message) -> Command<Self::Message>; + /// Any [`Task`] returned will be executed immediately in the background by the + /// runtime. + fn update(&mut self, message: Self::Message) -> Task<Self::Message>; /// Returns the widgets to display in the [`Program`] for the `window`. /// diff --git a/runtime/src/multi_window/state.rs b/runtime/src/multi_window/state.rs index 10366ec0..72ce6933 100644 --- a/runtime/src/multi_window/state.rs +++ b/runtime/src/multi_window/state.rs @@ -5,7 +5,7 @@ use crate::core::renderer; use crate::core::widget::operation::{self, Operation}; use crate::core::{Clipboard, Size}; use crate::user_interface::{self, UserInterface}; -use crate::{Command, Debug, Program}; +use crate::{Debug, Program, Task}; /// The execution state of a multi-window [`Program`]. It leverages caching, event /// processing, and rendering primitive storage. @@ -85,7 +85,7 @@ where /// the widgets of the linked [`Program`] if necessary. /// /// Returns a list containing the instances of [`Event`] that were not - /// captured by any widget, and the [`Command`] obtained from [`Program`] + /// captured by any widget, and the [`Task`] obtained from [`Program`] /// after updating it, only if an update was necessary. pub fn update( &mut self, @@ -96,7 +96,7 @@ where style: &renderer::Style, clipboard: &mut dyn Clipboard, debug: &mut Debug, - ) -> (Vec<Event>, Option<Command<P::Message>>) { + ) -> (Vec<Event>, Option<Task<P::Message>>) { let mut user_interfaces = build_user_interfaces( &self.program, self.caches.take().unwrap(), @@ -163,14 +163,14 @@ where drop(user_interfaces); - let commands = Command::batch(messages.into_iter().map(|msg| { + let commands = Task::batch(messages.into_iter().map(|msg| { debug.log_message(&msg); debug.update_started(); - let command = self.program.update(msg); + let task = self.program.update(msg); debug.update_finished(); - command + task })); let mut user_interfaces = build_user_interfaces( @@ -205,7 +205,7 @@ where pub fn operate( &mut self, renderer: &mut P::Renderer, - operations: impl Iterator<Item = Box<dyn Operation<P::Message>>>, + operations: impl Iterator<Item = Box<dyn Operation<()>>>, bounds: Size, debug: &mut Debug, ) { @@ -227,9 +227,7 @@ where match operation.finish() { operation::Outcome::None => {} - operation::Outcome::Some(message) => { - self.queued_messages.push(message); - } + operation::Outcome::Some(()) => {} operation::Outcome::Chain(next) => { current_operation = Some(next); } diff --git a/runtime/src/overlay/nested.rs b/runtime/src/overlay/nested.rs index ddb9532b..11eee41c 100644 --- a/runtime/src/overlay/nested.rs +++ b/runtime/src/overlay/nested.rs @@ -131,13 +131,13 @@ where &mut self, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn widget::Operation<Message>, + operation: &mut dyn widget::Operation<()>, ) { fn recurse<Message, Theme, Renderer>( element: &mut overlay::Element<'_, Message, Theme, Renderer>, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn widget::Operation<Message>, + operation: &mut dyn widget::Operation<()>, ) where Renderer: renderer::Renderer, { diff --git a/runtime/src/program.rs b/runtime/src/program.rs index 0ea94d3b..77acf497 100644 --- a/runtime/src/program.rs +++ b/runtime/src/program.rs @@ -1,5 +1,5 @@ //! Build interactive programs using The Elm Architecture. -use crate::Command; +use crate::Task; use iced_core::text; use iced_core::Element; @@ -25,9 +25,9 @@ pub trait Program: Sized { /// produced by either user interactions or commands, will be handled by /// this method. /// - /// Any [`Command`] returned will be executed immediately in the + /// Any [`Task`] returned will be executed immediately in the /// background by shells. - fn update(&mut self, message: Self::Message) -> Command<Self::Message>; + fn update(&mut self, message: Self::Message) -> Task<Self::Message>; /// Returns the widgets to display in the [`Program`]. /// diff --git a/runtime/src/program/state.rs b/runtime/src/program/state.rs index c6589c22..e51ad0cb 100644 --- a/runtime/src/program/state.rs +++ b/runtime/src/program/state.rs @@ -4,7 +4,7 @@ use crate::core::renderer; use crate::core::widget::operation::{self, Operation}; use crate::core::{Clipboard, Size}; use crate::user_interface::{self, UserInterface}; -use crate::{Command, Debug, Program}; +use crate::{Debug, Program, Task}; /// The execution state of a [`Program`]. It leverages caching, event /// processing, and rendering primitive storage. @@ -84,7 +84,7 @@ where /// the widgets of the linked [`Program`] if necessary. /// /// Returns a list containing the instances of [`Event`] that were not - /// captured by any widget, and the [`Command`] obtained from [`Program`] + /// captured by any widget, and the [`Task`] obtained from [`Program`] /// after updating it, only if an update was necessary. pub fn update( &mut self, @@ -95,7 +95,7 @@ where style: &renderer::Style, clipboard: &mut dyn Clipboard, debug: &mut Debug, - ) -> (Vec<Event>, Option<Command<P::Message>>) { + ) -> (Vec<Event>, Option<Task<P::Message>>) { let mut user_interface = build_user_interface( &mut self.program, self.cache.take().unwrap(), @@ -129,7 +129,7 @@ where messages.append(&mut self.queued_messages); debug.event_processing_finished(); - let command = if messages.is_empty() { + let task = if messages.is_empty() { debug.draw_started(); self.mouse_interaction = user_interface.draw(renderer, theme, style, cursor); @@ -143,16 +143,15 @@ where // for now :^) let temp_cache = user_interface.into_cache(); - let commands = - Command::batch(messages.into_iter().map(|message| { - debug.log_message(&message); + let tasks = Task::batch(messages.into_iter().map(|message| { + debug.log_message(&message); - debug.update_started(); - let command = self.program.update(message); - debug.update_finished(); + debug.update_started(); + let task = self.program.update(message); + debug.update_finished(); - command - })); + task + })); let mut user_interface = build_user_interface( &mut self.program, @@ -169,17 +168,17 @@ where self.cache = Some(user_interface.into_cache()); - Some(commands) + Some(tasks) }; - (uncaptured_events, command) + (uncaptured_events, task) } /// Applies [`Operation`]s to the [`State`] pub fn operate( &mut self, renderer: &mut P::Renderer, - operations: impl Iterator<Item = Box<dyn Operation<P::Message>>>, + operations: impl Iterator<Item = Box<dyn Operation<()>>>, bounds: Size, debug: &mut Debug, ) { @@ -199,9 +198,7 @@ where match operation.finish() { operation::Outcome::None => {} - operation::Outcome::Some(message) => { - self.queued_messages.push(message); - } + operation::Outcome::Some(()) => {} operation::Outcome::Chain(next) => { current_operation = Some(next); } diff --git a/runtime/src/system.rs b/runtime/src/system.rs index 61c8ff29..b6fb4fdf 100644 --- a/runtime/src/system.rs +++ b/runtime/src/system.rs @@ -1,6 +1,39 @@ //! Access the native system. -mod action; -mod information; +use crate::futures::futures::channel::oneshot; -pub use action::Action; -pub use information::Information; +/// An operation to be performed on the system. +#[derive(Debug)] +pub enum Action { + /// Query system information and produce `T` with the result. + QueryInformation(oneshot::Sender<Information>), +} + +/// Contains informations about the system (e.g. system name, processor, memory, graphics adapter). +#[derive(Clone, Debug)] +pub struct Information { + /// The operating system name + pub system_name: Option<String>, + /// Operating system kernel version + pub system_kernel: Option<String>, + /// Long operating system version + /// + /// Examples: + /// - MacOS 10.15 Catalina + /// - Windows 10 Pro + /// - Ubuntu 20.04 LTS (Focal Fossa) + pub system_version: Option<String>, + /// Short operating system version number + pub system_short_version: Option<String>, + /// Detailed processor model information + pub cpu_brand: String, + /// The number of physical cores on the processor + pub cpu_cores: Option<usize>, + /// Total RAM size, in bytes + pub memory_total: u64, + /// Memory used by this process, in bytes + pub memory_used: Option<u64>, + /// Underlying graphics backend for rendering + pub graphics_backend: String, + /// Model information for the active graphics adapter + pub graphics_adapter: String, +} diff --git a/runtime/src/system/action.rs b/runtime/src/system/action.rs deleted file mode 100644 index dea9536f..00000000 --- a/runtime/src/system/action.rs +++ /dev/null @@ -1,39 +0,0 @@ -use crate::system; - -use iced_futures::MaybeSend; -use std::fmt; - -/// An operation to be performed on the system. -pub enum Action<T> { - /// Query system information and produce `T` with the result. - QueryInformation(Box<dyn Closure<T>>), -} - -pub trait Closure<T>: Fn(system::Information) -> T + MaybeSend {} - -impl<T, O> Closure<O> for T where T: Fn(system::Information) -> O + MaybeSend {} - -impl<T> Action<T> { - /// Maps the output of a system [`Action`] using the provided closure. - pub fn map<A>( - self, - f: impl Fn(T) -> A + 'static + MaybeSend + Sync, - ) -> Action<A> - where - T: 'static, - { - match self { - Self::QueryInformation(o) => { - Action::QueryInformation(Box::new(move |s| f(o(s)))) - } - } - } -} - -impl<T> fmt::Debug for Action<T> { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::QueryInformation(_) => write!(f, "Action::QueryInformation"), - } - } -} diff --git a/runtime/src/system/information.rs b/runtime/src/system/information.rs deleted file mode 100644 index 0f78f5e9..00000000 --- a/runtime/src/system/information.rs +++ /dev/null @@ -1,29 +0,0 @@ -/// Contains informations about the system (e.g. system name, processor, memory, graphics adapter). -#[derive(Clone, Debug)] -pub struct Information { - /// The operating system name - pub system_name: Option<String>, - /// Operating system kernel version - pub system_kernel: Option<String>, - /// Long operating system version - /// - /// Examples: - /// - MacOS 10.15 Catalina - /// - Windows 10 Pro - /// - Ubuntu 20.04 LTS (Focal Fossa) - pub system_version: Option<String>, - /// Short operating system version number - pub system_short_version: Option<String>, - /// Detailed processor model information - pub cpu_brand: String, - /// The number of physical cores on the processor - pub cpu_cores: Option<usize>, - /// Total RAM size, in bytes - pub memory_total: u64, - /// Memory used by this process, in bytes - pub memory_used: Option<u64>, - /// Underlying graphics backend for rendering - pub graphics_backend: String, - /// Model information for the active graphics adapter - pub graphics_adapter: String, -} diff --git a/runtime/src/task.rs b/runtime/src/task.rs new file mode 100644 index 00000000..740360ac --- /dev/null +++ b/runtime/src/task.rs @@ -0,0 +1,252 @@ +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; + +use std::future::Future; + +/// A set of concurrent actions to be performed by the iced runtime. +/// +/// A [`Task`] _may_ produce a bunch of values of type `T`. +#[allow(missing_debug_implementations)] +pub struct Task<T>(Option<BoxStream<Action<T>>>); + +impl<T> Task<T> { + /// Creates a [`Task`] that does nothing. + pub fn none() -> Self { + Self(None) + } + + /// Creates a new [`Task`] that instantly produces the given value. + pub fn done(value: T) -> Self + where + T: MaybeSend + 'static, + { + 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>( + future: impl Future<Output = A> + MaybeSend + 'static, + f: impl Fn(A) -> T + MaybeSend + 'static, + ) -> Self + where + T: MaybeSend + 'static, + A: MaybeSend + 'static, + { + Self::future(future.map(f)) + } + + /// Creates a [`Task`] that runs the given [`Stream`] to completion and maps each + /// item with the given closure. + pub fn run<A>( + stream: impl Stream<Item = A> + MaybeSend + 'static, + f: impl Fn(A) -> T + MaybeSend + 'static, + ) -> Self + where + T: 'static, + { + Self::stream(stream.map(f)) + } + + /// 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 + where + T: 'static, + { + Self(Some(boxed_stream(stream::select_all( + tasks.into_iter().filter_map(|task| task.0), + )))) + } + + /// 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, + mut f: impl FnMut(T) -> O + MaybeSend + 'static, + ) -> Task<O> + where + T: MaybeSend + 'static, + O: MaybeSend + 'static, + { + self.then(move |output| Task::done(f(output))) + } + + /// Performs a new [`Task`] for every output of the current [`Task`] using the + /// given closure. + /// + /// This is the monadic interface of [`Task`]—analogous to [`Future`] and + /// [`Stream`]. + pub fn then<O>( + self, + mut f: impl FnMut(T) -> Task<O> + MaybeSend + 'static, + ) -> Task<O> + where + T: MaybeSend + 'static, + O: MaybeSend + 'static, + { + Task(match self.0 { + None => None, + Some(stream) => { + Some(boxed_stream(stream.flat_map(move |action| { + match action.output() { + Ok(output) => f(output) + .0 + .unwrap_or_else(|| boxed_stream(stream::empty())), + Err(action) => { + boxed_stream(stream::once(async move { action })) + } + } + }))) + } + }) + } + + /// Chains a new [`Task`] to be performed once the current one finishes completely. + pub fn chain(self, task: Self) -> Self + where + T: 'static, + { + match self.0 { + None => task, + Some(first) => match task.0 { + None => Task::none(), + Some(second) => Task(Some(boxed_stream(first.chain(second)))), + }, + } + } + + /// Creates a new [`Task`] that collects all the output of the current one into a [`Vec`]. + pub fn collect(self) -> Task<Vec<T>> + where + T: MaybeSend + 'static, + { + match self.0 { + None => Task::done(Vec::new()), + Some(stream) => Task(Some(boxed_stream( + stream::unfold( + (stream, Some(Vec::new())), + move |(mut stream, outputs)| async move { + let mut outputs = outputs?; + + let Some(action) = stream.next().await else { + return Some(( + Some(Action::Output(outputs)), + (stream, None), + )); + }; + + match action.output() { + Ok(output) => { + outputs.push(output); + + Some((None, (stream, Some(outputs)))) + } + Err(action) => { + Some((Some(action), (stream, Some(outputs)))) + } + } + }, + ) + .filter_map(future::ready), + ))), + } + } + + /// Returns the underlying [`Stream`] of the [`Task`]. + pub fn into_stream(self) -> Option<BoxStream<Action<T>>> { + self.0 + } +} + +impl<T> From<()> for Task<T> +where + T: MaybeSend + 'static, +{ + fn from(_value: ()) -> Self { + Self::none() + } +} diff --git a/runtime/src/user_interface.rs b/runtime/src/user_interface.rs index 006225ed..858b1a2d 100644 --- a/runtime/src/user_interface.rs +++ b/runtime/src/user_interface.rs @@ -566,7 +566,7 @@ where pub fn operate( &mut self, renderer: &Renderer, - operation: &mut dyn widget::Operation<Message>, + operation: &mut dyn widget::Operation<()>, ) { self.root.as_widget().operate( &mut self.state, diff --git a/runtime/src/window.rs b/runtime/src/window.rs index 24171e3e..956a20e1 100644 --- a/runtime/src/window.rs +++ b/runtime/src/window.rs @@ -1,24 +1,145 @@ //! Build window-based GUI applications. -mod action; - pub mod screenshot; -pub use action::Action; pub use screenshot::Screenshot; -use crate::command::{self, Command}; use crate::core::time::Instant; use crate::core::window::{ Event, Icon, Id, Level, Mode, Settings, UserAttention, }; use crate::core::{Point, Size}; use crate::futures::event; +use crate::futures::futures::channel::oneshot; use crate::futures::Subscription; +use crate::Task; pub use raw_window_handle; use raw_window_handle::WindowHandle; +/// An operation to be performed on some window. +#[allow(missing_debug_implementations)] +pub enum Action { + /// Opens a new window with some [`Settings`]. + Open(Id, Settings, oneshot::Sender<Id>), + + /// Close the window and exits the application. + Close(Id), + + /// Move the window with the left mouse button until the button is + /// released. + /// + /// There’s no guarantee that this will work unless the left mouse + /// button was pressed immediately before this function is called. + Drag(Id), + + /// Resize the window to the given logical dimensions. + Resize(Id, Size), + + /// Fetch the current logical dimensions of the window. + FetchSize(Id, oneshot::Sender<Size>), + + /// Fetch if the current window is maximized or not. + FetchMaximized(Id, oneshot::Sender<bool>), + + /// Set the window to maximized or back + Maximize(Id, bool), + + /// Fetch if the current window is minimized or not. + /// + /// ## Platform-specific + /// - **Wayland:** Always `None`. + FetchMinimized(Id, oneshot::Sender<Option<bool>>), + + /// Set the window to minimized or back + Minimize(Id, bool), + + /// Fetch the current logical coordinates of the window. + FetchPosition(Id, oneshot::Sender<Option<Point>>), + + /// Move the window to the given logical coordinates. + /// + /// Unsupported on Wayland. + Move(Id, Point), + + /// Change the [`Mode`] of the window. + ChangeMode(Id, Mode), + + /// Fetch the current [`Mode`] of the window. + FetchMode(Id, oneshot::Sender<Mode>), + + /// Toggle the window to maximized or back + ToggleMaximize(Id), + + /// Toggle whether window has decorations. + /// + /// ## Platform-specific + /// - **X11:** Not implemented. + /// - **Web:** Unsupported. + ToggleDecorations(Id), + + /// Request user attention to the window, this has no effect if the application + /// is already focused. How requesting for user attention manifests is platform dependent, + /// see [`UserAttention`] for details. + /// + /// Providing `None` will unset the request for user attention. Unsetting the request for + /// user attention might not be done automatically by the WM when the window receives input. + /// + /// ## Platform-specific + /// + /// - **iOS / Android / Web:** Unsupported. + /// - **macOS:** `None` has no effect. + /// - **X11:** Requests for user attention must be manually cleared. + /// - **Wayland:** Requires `xdg_activation_v1` protocol, `None` has no effect. + RequestUserAttention(Id, Option<UserAttention>), + + /// Bring the window to the front and sets input focus. Has no effect if the window is + /// already in focus, minimized, or not visible. + /// + /// This method steals input focus from other applications. Do not use this method unless + /// you are certain that's what the user wants. Focus stealing can cause an extremely disruptive + /// user experience. + /// + /// ## Platform-specific + /// + /// - **Web / Wayland:** Unsupported. + GainFocus(Id), + + /// Change the window [`Level`]. + ChangeLevel(Id, Level), + + /// Show the system menu at cursor position. + /// + /// ## Platform-specific + /// Android / iOS / macOS / Orbital / Web / X11: Unsupported. + ShowSystemMenu(Id), + + /// Fetch the raw identifier unique to the window. + FetchRawId(Id, oneshot::Sender<u64>), + + /// Change the window [`Icon`]. + /// + /// On Windows and X11, this is typically the small icon in the top-left + /// corner of the titlebar. + /// + /// ## Platform-specific + /// + /// - **Web / Wayland / macOS:** Unsupported. + /// + /// - **Windows:** Sets `ICON_SMALL`. The base size for a window icon is 16x16, but it's + /// recommended to account for screen scaling and pick a multiple of that, i.e. 32x32. + /// + /// - **X11:** Has no universal guidelines for icon sizes, so you're at the whims of the WM. That + /// said, it's usually in the same ballpark as on Windows. + ChangeIcon(Id, Icon), + + /// Runs the closure with the native window handle of the window with the given [`Id`]. + RunWithHandle(Id, Box<dyn FnOnce(WindowHandle<'_>) + Send>), + + /// Screenshot the viewport of the window. + Screenshot(Id, oneshot::Sender<Screenshot>), +} + /// Subscribes to the frames of the window of the running application. /// /// The resulting [`Subscription`] will produce items at a rate equal to the @@ -28,116 +149,144 @@ use raw_window_handle::WindowHandle; /// In any case, this [`Subscription`] is useful to smoothly draw application-driven /// animations without missing any frames. pub fn frames() -> Subscription<Instant> { - event::listen_raw(|event, _status| match event { - crate::core::Event::Window(_, Event::RedrawRequested(at)) => Some(at), + event::listen_raw(|event, _status, _window| match event { + crate::core::Event::Window(Event::RedrawRequested(at)) => Some(at), _ => None, }) } -/// Spawns a new window with the given `settings`. -/// -/// Returns the new window [`Id`] alongside the [`Command`]. -pub fn spawn<Message>(settings: Settings) -> (Id, Command<Message>) { +/// Subscribes to all window events of the running application. +pub fn events() -> Subscription<(Id, Event)> { + event::listen_with(|event, _status, id| { + if let crate::core::Event::Window(event) = event { + Some((id, event)) + } else { + None + } + }) +} + +/// Subscribes to all [`Event::Closed`] occurrences in the running application. +pub fn open_events() -> Subscription<Id> { + event::listen_with(|event, _status, id| { + if let crate::core::Event::Window(Event::Closed) = event { + Some(id) + } else { + None + } + }) +} + +/// Subscribes to all [`Event::Closed`] occurrences in the running application. +pub fn close_events() -> Subscription<Id> { + event::listen_with(|event, _status, id| { + if let crate::core::Event::Window(Event::Closed) = event { + Some(id) + } else { + None + } + }) +} + +/// Subscribes to all [`Event::CloseRequested`] occurences in the running application. +pub fn close_requests() -> Subscription<Id> { + event::listen_with(|event, _status, id| { + if let crate::core::Event::Window(Event::CloseRequested) = event { + Some(id) + } else { + None + } + }) +} + +/// Opens a new window with the given [`Settings`]; producing the [`Id`] +/// of the new window on completion. +pub fn open(settings: Settings) -> Task<Id> { let id = Id::unique(); - ( - id, - Command::single(command::Action::Window(Action::Spawn(id, settings))), - ) + Task::oneshot(|channel| { + crate::Action::Window(Action::Open(id, settings, channel)) + }) } /// Closes the window with `id`. -pub fn close<Message>(id: Id) -> Command<Message> { - Command::single(command::Action::Window(Action::Close(id))) +pub fn close<T>(id: Id) -> Task<T> { + Task::effect(crate::Action::Window(Action::Close(id))) } /// Begins dragging the window while the left mouse button is held. -pub fn drag<Message>(id: Id) -> Command<Message> { - Command::single(command::Action::Window(Action::Drag(id))) +pub fn drag<T>(id: Id) -> Task<T> { + Task::effect(crate::Action::Window(Action::Drag(id))) } /// Resizes the window to the given logical dimensions. -pub fn resize<Message>(id: Id, new_size: Size) -> Command<Message> { - Command::single(command::Action::Window(Action::Resize(id, new_size))) +pub fn resize<T>(id: Id, new_size: Size) -> Task<T> { + Task::effect(crate::Action::Window(Action::Resize(id, new_size))) } /// Fetches the window's size in logical dimensions. -pub fn fetch_size<Message>( - id: Id, - f: impl FnOnce(Size) -> Message + 'static, -) -> Command<Message> { - Command::single(command::Action::Window(Action::FetchSize(id, Box::new(f)))) +pub fn fetch_size(id: Id) -> Task<Size> { + Task::oneshot(move |channel| { + crate::Action::Window(Action::FetchSize(id, channel)) + }) } /// Fetches if the window is maximized. -pub fn fetch_maximized<Message>( - id: Id, - f: impl FnOnce(bool) -> Message + 'static, -) -> Command<Message> { - Command::single(command::Action::Window(Action::FetchMaximized( - id, - Box::new(f), - ))) +pub fn fetch_maximized(id: Id) -> Task<bool> { + Task::oneshot(move |channel| { + crate::Action::Window(Action::FetchMaximized(id, channel)) + }) } /// Maximizes the window. -pub fn maximize<Message>(id: Id, maximized: bool) -> Command<Message> { - Command::single(command::Action::Window(Action::Maximize(id, maximized))) +pub fn maximize<T>(id: Id, maximized: bool) -> Task<T> { + Task::effect(crate::Action::Window(Action::Maximize(id, maximized))) } /// Fetches if the window is minimized. -pub fn fetch_minimized<Message>( - id: Id, - f: impl FnOnce(Option<bool>) -> Message + 'static, -) -> Command<Message> { - Command::single(command::Action::Window(Action::FetchMinimized( - id, - Box::new(f), - ))) +pub fn fetch_minimized(id: Id) -> Task<Option<bool>> { + Task::oneshot(move |channel| { + crate::Action::Window(Action::FetchMinimized(id, channel)) + }) } /// Minimizes the window. -pub fn minimize<Message>(id: Id, minimized: bool) -> Command<Message> { - Command::single(command::Action::Window(Action::Minimize(id, minimized))) +pub fn minimize<T>(id: Id, minimized: bool) -> Task<T> { + Task::effect(crate::Action::Window(Action::Minimize(id, minimized))) } /// Fetches the current window position in logical coordinates. -pub fn fetch_position<Message>( - id: Id, - f: impl FnOnce(Option<Point>) -> Message + 'static, -) -> Command<Message> { - Command::single(command::Action::Window(Action::FetchPosition( - id, - Box::new(f), - ))) +pub fn fetch_position(id: Id) -> Task<Option<Point>> { + Task::oneshot(move |channel| { + crate::Action::Window(Action::FetchPosition(id, channel)) + }) } /// Moves the window to the given logical coordinates. -pub fn move_to<Message>(id: Id, position: Point) -> Command<Message> { - Command::single(command::Action::Window(Action::Move(id, position))) +pub fn move_to<T>(id: Id, position: Point) -> Task<T> { + Task::effect(crate::Action::Window(Action::Move(id, position))) } /// Changes the [`Mode`] of the window. -pub fn change_mode<Message>(id: Id, mode: Mode) -> Command<Message> { - Command::single(command::Action::Window(Action::ChangeMode(id, mode))) +pub fn change_mode<T>(id: Id, mode: Mode) -> Task<T> { + Task::effect(crate::Action::Window(Action::ChangeMode(id, mode))) } /// Fetches the current [`Mode`] of the window. -pub fn fetch_mode<Message>( - id: Id, - f: impl FnOnce(Mode) -> Message + 'static, -) -> Command<Message> { - Command::single(command::Action::Window(Action::FetchMode(id, Box::new(f)))) +pub fn fetch_mode(id: Id) -> Task<Mode> { + Task::oneshot(move |channel| { + crate::Action::Window(Action::FetchMode(id, channel)) + }) } /// Toggles the window to maximized or back. -pub fn toggle_maximize<Message>(id: Id) -> Command<Message> { - Command::single(command::Action::Window(Action::ToggleMaximize(id))) +pub fn toggle_maximize<T>(id: Id) -> Task<T> { + Task::effect(crate::Action::Window(Action::ToggleMaximize(id))) } /// Toggles the window decorations. -pub fn toggle_decorations<Message>(id: Id) -> Command<Message> { - Command::single(command::Action::Window(Action::ToggleDecorations(id))) +pub fn toggle_decorations<T>(id: Id) -> Task<T> { + Task::effect(crate::Action::Window(Action::ToggleDecorations(id))) } /// Request user attention to the window. This has no effect if the application @@ -146,11 +295,11 @@ pub fn toggle_decorations<Message>(id: Id) -> Command<Message> { /// /// Providing `None` will unset the request for user attention. Unsetting the request for /// user attention might not be done automatically by the WM when the window receives input. -pub fn request_user_attention<Message>( +pub fn request_user_attention<T>( id: Id, user_attention: Option<UserAttention>, -) -> Command<Message> { - Command::single(command::Action::Window(Action::RequestUserAttention( +) -> Task<T> { + Task::effect(crate::Action::Window(Action::RequestUserAttention( id, user_attention, ))) @@ -159,59 +308,61 @@ pub fn request_user_attention<Message>( /// Brings the window to the front and sets input focus. Has no effect if the window is /// already in focus, minimized, or not visible. /// -/// This [`Command`] steals input focus from other applications. Do not use this method unless +/// This [`Task`] steals input focus from other applications. Do not use this method unless /// you are certain that's what the user wants. Focus stealing can cause an extremely disruptive /// user experience. -pub fn gain_focus<Message>(id: Id) -> Command<Message> { - Command::single(command::Action::Window(Action::GainFocus(id))) +pub fn gain_focus<T>(id: Id) -> Task<T> { + Task::effect(crate::Action::Window(Action::GainFocus(id))) } /// Changes the window [`Level`]. -pub fn change_level<Message>(id: Id, level: Level) -> Command<Message> { - Command::single(command::Action::Window(Action::ChangeLevel(id, level))) +pub fn change_level<T>(id: Id, level: Level) -> Task<T> { + 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<Message>(id: Id) -> Command<Message> { - Command::single(command::Action::Window(Action::ShowSystemMenu(id))) +pub fn show_system_menu<T>(id: Id) -> Task<T> { + Task::effect(crate::Action::Window(Action::ShowSystemMenu(id))) } /// Fetches an identifier unique to the window, provided by the underlying windowing system. This is /// not to be confused with [`Id`]. -pub fn fetch_id<Message>( - id: Id, - f: impl FnOnce(u64) -> Message + 'static, -) -> Command<Message> { - Command::single(command::Action::Window(Action::FetchId(id, Box::new(f)))) +pub fn fetch_raw_id<Message>(id: Id) -> Task<u64> { + Task::oneshot(|channel| { + crate::Action::Window(Action::FetchRawId(id, channel)) + }) } /// Changes the [`Icon`] of the window. -pub fn change_icon<Message>(id: Id, icon: Icon) -> Command<Message> { - Command::single(command::Action::Window(Action::ChangeIcon(id, icon))) +pub fn change_icon<T>(id: Id, icon: Icon) -> Task<T> { + 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. /// /// Note that if the window closes before this call is processed the callback will not be run. -pub fn run_with_handle<Message>( +pub fn run_with_handle<T>( id: Id, - f: impl FnOnce(&WindowHandle<'_>) -> Message + 'static, -) -> Command<Message> { - Command::single(command::Action::Window(Action::RunWithHandle( - id, - Box::new(f), - ))) + f: impl FnOnce(WindowHandle<'_>) -> T + Send + 'static, +) -> Task<T> +where + T: Send + 'static, +{ + Task::oneshot(move |channel| { + crate::Action::Window(Action::RunWithHandle( + id, + Box::new(move |handle| { + let _ = channel.send(f(handle)); + }), + )) + }) } /// Captures a [`Screenshot`] from the window. -pub fn screenshot<Message>( - id: Id, - f: impl FnOnce(Screenshot) -> Message + Send + 'static, -) -> Command<Message> { - Command::single(command::Action::Window(Action::Screenshot( - id, - Box::new(f), - ))) +pub fn screenshot(id: Id) -> Task<Screenshot> { + Task::oneshot(move |channel| { + crate::Action::Window(Action::Screenshot(id, channel)) + }) } diff --git a/runtime/src/window/action.rs b/runtime/src/window/action.rs deleted file mode 100644 index e44ff5a6..00000000 --- a/runtime/src/window/action.rs +++ /dev/null @@ -1,230 +0,0 @@ -use crate::core::window::{Icon, Id, Level, Mode, Settings, UserAttention}; -use crate::core::{Point, Size}; -use crate::futures::MaybeSend; -use crate::window::Screenshot; - -use raw_window_handle::WindowHandle; - -use std::fmt; - -/// An operation to be performed on some window. -pub enum Action<T> { - /// Spawns a new window with some [`Settings`]. - Spawn(Id, Settings), - /// Close the window and exits the application. - Close(Id), - /// Move the window with the left mouse button until the button is - /// released. - /// - /// There’s no guarantee that this will work unless the left mouse - /// button was pressed immediately before this function is called. - Drag(Id), - /// Resize the window to the given logical dimensions. - Resize(Id, Size), - /// Fetch the current logical dimensions of the window. - FetchSize(Id, Box<dyn FnOnce(Size) -> T + 'static>), - /// Fetch if the current window is maximized or not. - /// - /// ## Platform-specific - /// - **iOS / Android / Web:** Unsupported. - FetchMaximized(Id, Box<dyn FnOnce(bool) -> T + 'static>), - /// Set the window to maximized or back - Maximize(Id, bool), - /// Fetch if the current window is minimized or not. - /// - /// ## Platform-specific - /// - **Wayland:** Always `None`. - /// - **iOS / Android / Web:** Unsupported. - FetchMinimized(Id, Box<dyn FnOnce(Option<bool>) -> T + 'static>), - /// Set the window to minimized or back - Minimize(Id, bool), - /// Fetch the current logical coordinates of the window. - FetchPosition(Id, Box<dyn FnOnce(Option<Point>) -> T + 'static>), - /// Move the window to the given logical coordinates. - /// - /// Unsupported on Wayland. - Move(Id, Point), - /// Change the [`Mode`] of the window. - ChangeMode(Id, Mode), - /// Fetch the current [`Mode`] of the window. - FetchMode(Id, Box<dyn FnOnce(Mode) -> T + 'static>), - /// Toggle the window to maximized or back - ToggleMaximize(Id), - /// Toggle whether window has decorations. - /// - /// ## Platform-specific - /// - **X11:** Not implemented. - /// - **Web:** Unsupported. - ToggleDecorations(Id), - /// Request user attention to the window, this has no effect if the application - /// is already focused. How requesting for user attention manifests is platform dependent, - /// see [`UserAttention`] for details. - /// - /// Providing `None` will unset the request for user attention. Unsetting the request for - /// user attention might not be done automatically by the WM when the window receives input. - /// - /// ## Platform-specific - /// - /// - **iOS / Android / Web:** Unsupported. - /// - **macOS:** `None` has no effect. - /// - **X11:** Requests for user attention must be manually cleared. - /// - **Wayland:** Requires `xdg_activation_v1` protocol, `None` has no effect. - RequestUserAttention(Id, Option<UserAttention>), - /// Bring the window to the front and sets input focus. Has no effect if the window is - /// already in focus, minimized, or not visible. - /// - /// This method steals input focus from other applications. Do not use this method unless - /// you are certain that's what the user wants. Focus stealing can cause an extremely disruptive - /// user experience. - /// - /// ## Platform-specific - /// - /// - **Web / Wayland:** Unsupported. - GainFocus(Id), - /// Change the window [`Level`]. - ChangeLevel(Id, Level), - /// Show the system menu at cursor position. - /// - /// ## Platform-specific - /// Android / iOS / macOS / Orbital / Web / X11: Unsupported. - ShowSystemMenu(Id), - /// Fetch the raw identifier unique to the window. - FetchId(Id, Box<dyn FnOnce(u64) -> T + 'static>), - /// Change the window [`Icon`]. - /// - /// On Windows and X11, this is typically the small icon in the top-left - /// corner of the titlebar. - /// - /// ## Platform-specific - /// - /// - **Web / Wayland / macOS:** Unsupported. - /// - /// - **Windows:** Sets `ICON_SMALL`. The base size for a window icon is 16x16, but it's - /// recommended to account for screen scaling and pick a multiple of that, i.e. 32x32. - /// - /// - **X11:** Has no universal guidelines for icon sizes, so you're at the whims of the WM. That - /// said, it's usually in the same ballpark as on Windows. - ChangeIcon(Id, Icon), - /// Runs the closure with the native window handle of the window with the given [`Id`]. - RunWithHandle(Id, Box<dyn FnOnce(&WindowHandle<'_>) -> T + 'static>), - /// Screenshot the viewport of the window. - Screenshot(Id, Box<dyn FnOnce(Screenshot) -> T + 'static>), -} - -impl<T> Action<T> { - /// Maps the output of a window [`Action`] using the provided closure. - pub fn map<A>( - self, - f: impl Fn(T) -> A + 'static + MaybeSend + Sync, - ) -> Action<A> - where - T: 'static, - { - match self { - Self::Spawn(id, settings) => Action::Spawn(id, settings), - Self::Close(id) => Action::Close(id), - Self::Drag(id) => Action::Drag(id), - Self::Resize(id, size) => Action::Resize(id, size), - Self::FetchSize(id, o) => { - Action::FetchSize(id, Box::new(move |s| f(o(s)))) - } - Self::FetchMaximized(id, o) => { - Action::FetchMaximized(id, Box::new(move |s| f(o(s)))) - } - Self::Maximize(id, maximized) => Action::Maximize(id, maximized), - Self::FetchMinimized(id, o) => { - Action::FetchMinimized(id, Box::new(move |s| f(o(s)))) - } - Self::Minimize(id, minimized) => Action::Minimize(id, minimized), - Self::FetchPosition(id, o) => { - Action::FetchPosition(id, Box::new(move |s| f(o(s)))) - } - Self::Move(id, position) => Action::Move(id, position), - Self::ChangeMode(id, mode) => Action::ChangeMode(id, mode), - Self::FetchMode(id, o) => { - Action::FetchMode(id, Box::new(move |s| f(o(s)))) - } - Self::ToggleMaximize(id) => Action::ToggleMaximize(id), - Self::ToggleDecorations(id) => Action::ToggleDecorations(id), - Self::RequestUserAttention(id, attention_type) => { - Action::RequestUserAttention(id, attention_type) - } - Self::GainFocus(id) => Action::GainFocus(id), - Self::ChangeLevel(id, level) => Action::ChangeLevel(id, level), - Self::ShowSystemMenu(id) => Action::ShowSystemMenu(id), - Self::FetchId(id, o) => { - Action::FetchId(id, Box::new(move |s| f(o(s)))) - } - Self::ChangeIcon(id, icon) => Action::ChangeIcon(id, icon), - Self::RunWithHandle(id, o) => { - Action::RunWithHandle(id, Box::new(move |s| f(o(s)))) - } - Self::Screenshot(id, tag) => Action::Screenshot( - id, - Box::new(move |screenshot| f(tag(screenshot))), - ), - } - } -} - -impl<T> fmt::Debug for Action<T> { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::Spawn(id, settings) => { - write!(f, "Action::Spawn({id:?}, {settings:?})") - } - Self::Close(id) => write!(f, "Action::Close({id:?})"), - Self::Drag(id) => write!(f, "Action::Drag({id:?})"), - Self::Resize(id, size) => { - write!(f, "Action::Resize({id:?}, {size:?})") - } - Self::FetchSize(id, _) => write!(f, "Action::FetchSize({id:?})"), - Self::FetchMaximized(id, _) => { - write!(f, "Action::FetchMaximized({id:?})") - } - Self::Maximize(id, maximized) => { - write!(f, "Action::Maximize({id:?}, {maximized})") - } - Self::FetchMinimized(id, _) => { - write!(f, "Action::FetchMinimized({id:?})") - } - Self::Minimize(id, minimized) => { - write!(f, "Action::Minimize({id:?}, {minimized}") - } - Self::FetchPosition(id, _) => { - write!(f, "Action::FetchPosition({id:?})") - } - Self::Move(id, position) => { - write!(f, "Action::Move({id:?}, {position})") - } - Self::ChangeMode(id, mode) => { - write!(f, "Action::SetMode({id:?}, {mode:?})") - } - Self::FetchMode(id, _) => write!(f, "Action::FetchMode({id:?})"), - Self::ToggleMaximize(id) => { - write!(f, "Action::ToggleMaximize({id:?})") - } - Self::ToggleDecorations(id) => { - write!(f, "Action::ToggleDecorations({id:?})") - } - Self::RequestUserAttention(id, _) => { - write!(f, "Action::RequestUserAttention({id:?})") - } - Self::GainFocus(id) => write!(f, "Action::GainFocus({id:?})"), - Self::ChangeLevel(id, level) => { - write!(f, "Action::ChangeLevel({id:?}, {level:?})") - } - Self::ShowSystemMenu(id) => { - write!(f, "Action::ShowSystemMenu({id:?})") - } - Self::FetchId(id, _) => write!(f, "Action::FetchId({id:?})"), - Self::ChangeIcon(id, _icon) => { - write!(f, "Action::ChangeIcon({id:?})") - } - Self::RunWithHandle(id, _) => { - write!(f, "Action::RunWithHandle({id:?})") - } - Self::Screenshot(id, _) => write!(f, "Action::Screenshot({id:?})"), - } - } -} diff --git a/runtime/src/window/screenshot.rs b/runtime/src/window/screenshot.rs index fb318110..d9adbc01 100644 --- a/runtime/src/window/screenshot.rs +++ b/runtime/src/window/screenshot.rs @@ -11,16 +11,20 @@ use std::fmt::{Debug, Formatter}; pub struct Screenshot { /// The bytes of the [`Screenshot`]. pub bytes: Bytes, - /// The size of the [`Screenshot`]. + /// The size of the [`Screenshot`] in physical pixels. pub size: Size<u32>, + /// The scale factor of the [`Screenshot`]. This can be useful when converting between widget + /// bounds (which are in logical pixels) to crop screenshots. + pub scale_factor: f64, } impl Debug for Screenshot { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!( f, - "Screenshot: {{ \n bytes: {}\n size: {:?} }}", + "Screenshot: {{ \n bytes: {}\n scale: {}\n size: {:?} }}", self.bytes.len(), + self.scale_factor, self.size ) } @@ -28,10 +32,15 @@ impl Debug for Screenshot { impl Screenshot { /// Creates a new [`Screenshot`]. - pub fn new(bytes: impl Into<Bytes>, size: Size<u32>) -> Self { + pub fn new( + bytes: impl Into<Bytes>, + size: Size<u32>, + scale_factor: f64, + ) -> Self { Self { bytes: bytes.into(), size, + scale_factor, } } @@ -70,6 +79,7 @@ impl Screenshot { Ok(Self { bytes: Bytes::from(chopped), size: Size::new(region.width, region.height), + scale_factor: self.scale_factor, }) } } diff --git a/src/application.rs b/src/application.rs index d12ba73d..4cd4a87d 100644 --- a/src/application.rs +++ b/src/application.rs @@ -2,7 +2,7 @@ use crate::core::text; use crate::graphics::compositor; use crate::shell::application; -use crate::{Command, Element, Executor, Settings, Subscription}; +use crate::{Element, Executor, Settings, Subscription, Task}; pub use application::{Appearance, DefaultStyle}; @@ -16,7 +16,7 @@ pub use application::{Appearance, DefaultStyle}; /// document. /// /// An [`Application`] can execute asynchronous actions by returning a -/// [`Command`] in some of its methods. +/// [`Task`] in some of its methods. /// /// When using an [`Application`] with the `debug` feature enabled, a debug view /// can be toggled by pressing `F12`. @@ -62,7 +62,7 @@ pub use application::{Appearance, DefaultStyle}; /// ```no_run /// use iced::advanced::Application; /// use iced::executor; -/// use iced::{Command, Element, Settings, Theme, Renderer}; +/// use iced::{Task, Element, Settings, Theme, Renderer}; /// /// pub fn main() -> iced::Result { /// Hello::run(Settings::default()) @@ -77,16 +77,16 @@ pub use application::{Appearance, DefaultStyle}; /// type Theme = Theme; /// type Renderer = Renderer; /// -/// fn new(_flags: ()) -> (Hello, Command<Self::Message>) { -/// (Hello, Command::none()) +/// fn new(_flags: ()) -> (Hello, Task<Self::Message>) { +/// (Hello, Task::none()) /// } /// /// fn title(&self) -> String { /// String::from("A cool application") /// } /// -/// fn update(&mut self, _message: Self::Message) -> Command<Self::Message> { -/// Command::none() +/// fn update(&mut self, _message: Self::Message) -> Task<Self::Message> { +/// Task::none() /// } /// /// fn view(&self) -> Element<Self::Message> { @@ -123,12 +123,12 @@ where /// /// Here is where you should return the initial state of your app. /// - /// Additionally, you can return a [`Command`] if you need to perform some + /// Additionally, you can return a [`Task`] if you need to perform some /// async action in the background on startup. This is useful if you want to /// load state from a file, perform an initial HTTP request, etc. /// /// [`run`]: Self::run - fn new(flags: Self::Flags) -> (Self, Command<Self::Message>); + fn new(flags: Self::Flags) -> (Self, Task<Self::Message>); /// Returns the current title of the [`Application`]. /// @@ -142,8 +142,8 @@ where /// produced by either user interactions or commands, will be handled by /// this method. /// - /// Any [`Command`] returned will be executed immediately in the background. - fn update(&mut self, message: Self::Message) -> Command<Self::Message>; + /// Any [`Task`] returned will be executed immediately in the background. + fn update(&mut self, message: Self::Message) -> Task<Self::Message>; /// Returns the widgets to display in the [`Application`]. /// @@ -234,7 +234,7 @@ where type Theme = A::Theme; type Renderer = A::Renderer; - fn update(&mut self, message: Self::Message) -> Command<Self::Message> { + fn update(&mut self, message: Self::Message) -> Task<Self::Message> { self.0.update(message) } @@ -250,7 +250,7 @@ where { type Flags = A::Flags; - fn new(flags: Self::Flags) -> (Self, Command<A::Message>) { + fn new(flags: Self::Flags) -> (Self, Task<A::Message>) { let (app, command) = A::new(flags); (Instance(app), command) @@ -203,6 +203,7 @@ pub use crate::core::{ Length, Padding, Pixels, Point, Radians, Rectangle, Rotation, Shadow, Size, Theme, Transformation, Vector, }; +pub use crate::runtime::Task; pub mod clipboard { //! Access the clipboard. @@ -236,8 +237,10 @@ pub mod font { pub mod event { //! Handle events of a user interface. - pub use crate::core::event::{Event, MacOS, PlatformSpecific, Status}; - pub use iced_futures::event::{listen, listen_raw, listen_with}; + pub use crate::core::event::{Event, Status}; + pub use iced_futures::event::{ + listen, listen_raw, listen_url, listen_with, + }; } pub mod keyboard { @@ -254,11 +257,6 @@ pub mod mouse { }; } -pub mod command { - //! Run asynchronous actions. - pub use crate::runtime::command::{channel, Command}; -} - pub mod subscription { //! Listen to external events in your application. pub use iced_futures::subscription::{ @@ -310,7 +308,6 @@ pub mod widget { mod runtime {} } -pub use command::Command; pub use error::Error; pub use event::Event; pub use executor::Executor; diff --git a/src/multi_window.rs b/src/multi_window.rs index b81297dc..4900bb85 100644 --- a/src/multi_window.rs +++ b/src/multi_window.rs @@ -1,6 +1,6 @@ //! Leverage multi-window support in your application. use crate::window; -use crate::{Command, Element, Executor, Settings, Subscription}; +use crate::{Element, Executor, Settings, Subscription, Task}; pub use crate::application::{Appearance, DefaultStyle}; @@ -14,7 +14,7 @@ pub use crate::application::{Appearance, DefaultStyle}; /// document and display only the contents of the `window::Id::MAIN` window. /// /// An [`Application`] can execute asynchronous actions by returning a -/// [`Command`] in some of its methods. +/// [`Task`] in some of its methods. /// /// When using an [`Application`] with the `debug` feature enabled, a debug view /// can be toggled by pressing `F12`. @@ -29,7 +29,7 @@ pub use crate::application::{Appearance, DefaultStyle}; /// /// ```no_run /// use iced::{executor, window}; -/// use iced::{Command, Element, Settings, Theme}; +/// use iced::{Task, Element, Settings, Theme}; /// use iced::multi_window::{self, Application}; /// /// pub fn main() -> iced::Result { @@ -44,16 +44,16 @@ pub use crate::application::{Appearance, DefaultStyle}; /// type Message = (); /// type Theme = Theme; /// -/// fn new(_flags: ()) -> (Hello, Command<Self::Message>) { -/// (Hello, Command::none()) +/// fn new(_flags: ()) -> (Hello, Task<Self::Message>) { +/// (Hello, Task::none()) /// } /// /// fn title(&self, _window: window::Id) -> String { /// String::from("A cool application") /// } /// -/// fn update(&mut self, _message: Self::Message) -> Command<Self::Message> { -/// Command::none() +/// fn update(&mut self, _message: Self::Message) -> Task<Self::Message> { +/// Task::none() /// } /// /// fn view(&self, _window: window::Id) -> Element<Self::Message> { @@ -89,12 +89,12 @@ where /// /// Here is where you should return the initial state of your app. /// - /// Additionally, you can return a [`Command`] if you need to perform some + /// Additionally, you can return a [`Task`] if you need to perform some /// async action in the background on startup. This is useful if you want to /// load state from a file, perform an initial HTTP request, etc. /// /// [`run`]: Self::run - fn new(flags: Self::Flags) -> (Self, Command<Self::Message>); + fn new(flags: Self::Flags) -> (Self, Task<Self::Message>); /// Returns the current title of the `window` of the [`Application`]. /// @@ -108,8 +108,8 @@ where /// produced by either user interactions or commands, will be handled by /// this method. /// - /// Any [`Command`] returned will be executed immediately in the background. - fn update(&mut self, message: Self::Message) -> Command<Self::Message>; + /// Any [`Task`] returned will be executed immediately in the background. + fn update(&mut self, message: Self::Message) -> Task<Self::Message>; /// Returns the widgets to display in the `window` of the [`Application`]. /// @@ -207,7 +207,7 @@ where type Theme = A::Theme; type Renderer = crate::Renderer; - fn update(&mut self, message: Self::Message) -> Command<Self::Message> { + fn update(&mut self, message: Self::Message) -> Task<Self::Message> { self.0.update(message) } @@ -226,7 +226,7 @@ where { type Flags = A::Flags; - fn new(flags: Self::Flags) -> (Self, Command<A::Message>) { + fn new(flags: Self::Flags) -> (Self, Task<A::Message>) { let (app, command) = A::new(flags); (Instance(app), command) diff --git a/src/program.rs b/src/program.rs index d4c2a266..ea6b0e8e 100644 --- a/src/program.rs +++ b/src/program.rs @@ -35,7 +35,7 @@ use crate::core::text; use crate::executor::{self, Executor}; use crate::graphics::compositor; use crate::window; -use crate::{Command, Element, Font, Result, Settings, Size, Subscription}; +use crate::{Element, Font, Result, Settings, Size, Subscription, Task}; pub use crate::application::{Appearance, DefaultStyle}; @@ -76,7 +76,7 @@ pub fn program<State, Message, Theme, Renderer>( ) -> Program<impl Definition<State = State, Message = Message, Theme = Theme>> where State: 'static, - Message: Send + std::fmt::Debug, + Message: Send + std::fmt::Debug + 'static, Theme: Default + DefaultStyle, Renderer: self::Renderer, { @@ -94,7 +94,7 @@ where impl<State, Message, Theme, Renderer, Update, View> Definition for Application<State, Message, Theme, Renderer, Update, View> where - Message: Send + std::fmt::Debug, + Message: Send + std::fmt::Debug + 'static, Theme: Default + DefaultStyle, Renderer: self::Renderer, Update: self::Update<State, Message>, @@ -106,15 +106,15 @@ where type Renderer = Renderer; type Executor = executor::Default; - fn load(&self) -> Command<Self::Message> { - Command::none() + fn load(&self) -> Task<Self::Message> { + Task::none() } fn update( &self, state: &mut Self::State, message: Self::Message, - ) -> Command<Self::Message> { + ) -> Task<Self::Message> { self.update.update(state, message).into() } @@ -197,7 +197,7 @@ impl<P: Definition> Program<P> { fn new( (program, initialize): Self::Flags, - ) -> (Self, Command<Self::Message>) { + ) -> (Self, Task<Self::Message>) { let state = initialize(); let command = program.load(); @@ -218,7 +218,7 @@ impl<P: Definition> Program<P> { fn update( &mut self, message: Self::Message, - ) -> Command<Self::Message> { + ) -> Task<Self::Message> { self.program.update(&mut self.state, message) } @@ -357,10 +357,10 @@ impl<P: Definition> Program<P> { } } - /// Runs the [`Command`] produced by the closure at startup. + /// Runs the [`Task`] produced by the closure at startup. pub fn load( self, - f: impl Fn() -> Command<P::Message>, + f: impl Fn() -> Task<P::Message>, ) -> Program< impl Definition<State = P::State, Message = P::Message, Theme = P::Theme>, > { @@ -420,7 +420,7 @@ pub trait Definition: Sized { type State; /// The message of the program. - type Message: Send + std::fmt::Debug; + type Message: Send + std::fmt::Debug + 'static; /// The theme of the program. type Theme: Default + DefaultStyle; @@ -431,13 +431,13 @@ pub trait Definition: Sized { /// The executor of the program. type Executor: Executor; - fn load(&self) -> Command<Self::Message>; + fn load(&self) -> Task<Self::Message>; fn update( &self, state: &mut Self::State, message: Self::Message, - ) -> Command<Self::Message>; + ) -> Task<Self::Message>; fn view<'a>( &self, @@ -484,7 +484,7 @@ fn with_title<P: Definition>( type Renderer = P::Renderer; type Executor = P::Executor; - fn load(&self) -> Command<Self::Message> { + fn load(&self) -> Task<Self::Message> { self.program.load() } @@ -496,7 +496,7 @@ fn with_title<P: Definition>( &self, state: &mut Self::State, message: Self::Message, - ) -> Command<Self::Message> { + ) -> Task<Self::Message> { self.program.update(state, message) } @@ -532,7 +532,7 @@ fn with_title<P: Definition>( fn with_load<P: Definition>( program: P, - f: impl Fn() -> Command<P::Message>, + f: impl Fn() -> Task<P::Message>, ) -> impl Definition<State = P::State, Message = P::Message, Theme = P::Theme> { struct WithLoad<P, F> { program: P, @@ -541,7 +541,7 @@ fn with_load<P: Definition>( impl<P: Definition, F> Definition for WithLoad<P, F> where - F: Fn() -> Command<P::Message>, + F: Fn() -> Task<P::Message>, { type State = P::State; type Message = P::Message; @@ -549,15 +549,15 @@ fn with_load<P: Definition>( type Renderer = P::Renderer; type Executor = executor::Default; - fn load(&self) -> Command<Self::Message> { - Command::batch([self.program.load(), (self.load)()]) + fn load(&self) -> Task<Self::Message> { + Task::batch([self.program.load(), (self.load)()]) } fn update( &self, state: &mut Self::State, message: Self::Message, - ) -> Command<Self::Message> { + ) -> Task<Self::Message> { self.program.update(state, message) } @@ -621,7 +621,7 @@ fn with_subscription<P: Definition>( (self.subscription)(state) } - fn load(&self) -> Command<Self::Message> { + fn load(&self) -> Task<Self::Message> { self.program.load() } @@ -629,7 +629,7 @@ fn with_subscription<P: Definition>( &self, state: &mut Self::State, message: Self::Message, - ) -> Command<Self::Message> { + ) -> Task<Self::Message> { self.program.update(state, message) } @@ -686,7 +686,7 @@ fn with_theme<P: Definition>( (self.theme)(state) } - fn load(&self) -> Command<Self::Message> { + fn load(&self) -> Task<Self::Message> { self.program.load() } @@ -698,7 +698,7 @@ fn with_theme<P: Definition>( &self, state: &mut Self::State, message: Self::Message, - ) -> Command<Self::Message> { + ) -> Task<Self::Message> { self.program.update(state, message) } @@ -755,7 +755,7 @@ fn with_style<P: Definition>( (self.style)(state, theme) } - fn load(&self) -> Command<Self::Message> { + fn load(&self) -> Task<Self::Message> { self.program.load() } @@ -767,7 +767,7 @@ fn with_style<P: Definition>( &self, state: &mut Self::State, message: Self::Message, - ) -> Command<Self::Message> { + ) -> Task<Self::Message> { self.program.update(state, message) } @@ -822,26 +822,26 @@ where /// The update logic of some [`Program`]. /// /// This trait allows the [`program`] builder to take any closure that -/// returns any `Into<Command<Message>>`. +/// returns any `Into<Task<Message>>`. pub trait Update<State, Message> { /// Processes the message and updates the state of the [`Program`]. fn update( &self, state: &mut State, message: Message, - ) -> impl Into<Command<Message>>; + ) -> impl Into<Task<Message>>; } impl<T, State, Message, C> Update<State, Message> for T where T: Fn(&mut State, Message) -> C, - C: Into<Command<Message>>, + C: Into<Task<Message>>, { fn update( &self, state: &mut State, message: Message, - ) -> impl Into<Command<Message>> { + ) -> impl Into<Task<Message>> { self(state, message) } } diff --git a/wgpu/README.md b/wgpu/README.md index 95d7028a..8e9602ea 100644 --- a/wgpu/README.md +++ b/wgpu/README.md @@ -6,14 +6,7 @@ `iced_wgpu` is a [`wgpu`] renderer for [`iced_runtime`]. For now, it is the default renderer of Iced on [native platforms]. -[`wgpu`] supports most modern graphics backends: Vulkan, Metal, and DX12 (OpenGL and WebGL are still WIP). Additionally, it will support the incoming [WebGPU API]. - -Currently, `iced_wgpu` supports the following primitives: -- Text, which is rendered using [`wgpu_glyph`]. No shaping at all. -- Quads or rectangles, with rounded borders and a solid background color. -- Clip areas, useful to implement scrollables or hide overflowing content. -- Images and SVG, loaded from memory or the file system. -- Meshes of triangles, useful to draw geometry freely. +[`wgpu`] supports most modern graphics backends: Vulkan, Metal, DX12, OpenGL, and WebGPU. <p align="center"> <img alt="The native target" src="../docs/graphs/native.png" width="80%"> @@ -25,29 +18,3 @@ Currently, `iced_wgpu` supports the following primitives: [native platforms]: https://github.com/gfx-rs/wgpu#supported-platforms [WebGPU API]: https://gpuweb.github.io/gpuweb/ [`wgpu_glyph`]: https://github.com/hecrj/wgpu_glyph - -## Installation -Add `iced_wgpu` as a dependency in your `Cargo.toml`: - -```toml -iced_wgpu = "0.10" -``` - -__Iced moves fast and the `master` branch can contain breaking changes!__ If -you want to learn about a specific release, check out [the release list]. - -[the release list]: https://github.com/iced-rs/iced/releases - -## Current limitations - -The current implementation is quite naive; it uses: - -- A different pipeline/shader for each primitive -- A very simplistic layer model: every `Clip` primitive will generate new layers -- _Many_ render passes instead of preparing everything upfront -- A glyph cache that is trimmed incorrectly when there are multiple layers (a [`glyph_brush`] limitation) - -Some of these issues are already being worked on! If you want to help, [get in touch!] - -[get in touch!]: ../CONTRIBUTING.md -[`glyph_brush`]: https://github.com/alexheretic/glyph-brush diff --git a/wgpu/src/lib.rs b/wgpu/src/lib.rs index 1fbdbe9a..ad88ce3e 100644 --- a/wgpu/src/lib.rs +++ b/wgpu/src/lib.rs @@ -79,6 +79,7 @@ pub struct Renderer { triangle_storage: triangle::Storage, text_storage: text::Storage, + text_viewport: text::Viewport, // TODO: Centralize all the image feature handling #[cfg(any(feature = "svg", feature = "image"))] @@ -87,8 +88,8 @@ pub struct Renderer { impl Renderer { pub fn new( - _device: &wgpu::Device, - _engine: &Engine, + device: &wgpu::Device, + engine: &Engine, default_font: Font, default_text_size: Pixels, ) -> Self { @@ -99,10 +100,11 @@ impl Renderer { triangle_storage: triangle::Storage::new(), text_storage: text::Storage::new(), + text_viewport: engine.text_pipeline.create_viewport(device), #[cfg(any(feature = "svg", feature = "image"))] image_cache: std::cell::RefCell::new( - _engine.create_image_cache(_device), + engine.create_image_cache(device), ), } } @@ -141,6 +143,8 @@ impl Renderer { ) { let scale_factor = viewport.scale_factor() as f32; + self.text_viewport.update(queue, viewport.physical_size()); + for layer in self.layers.iter_mut() { if !layer.quads.is_empty() { engine.quad_pipeline.prepare( @@ -182,12 +186,12 @@ impl Renderer { engine.text_pipeline.prepare( device, queue, + &self.text_viewport, encoder, &mut self.text_storage, &layer.text, layer.bounds, Transformation::scale(scale_factor), - viewport.physical_size(), ); } @@ -357,6 +361,7 @@ impl Renderer { if !layer.text.is_empty() { text_layer += engine.text_pipeline.render( + &self.text_viewport, &self.text_storage, text_layer, &layer.text, diff --git a/wgpu/src/quad/gradient.rs b/wgpu/src/quad/gradient.rs index 5b32c52a..13dc10f8 100644 --- a/wgpu/src/quad/gradient.rs +++ b/wgpu/src/quad/gradient.rs @@ -188,9 +188,6 @@ impl Pipeline { layer: &'a Layer, range: Range<usize>, ) { - #[cfg(feature = "tracing")] - let _ = tracing::info_span!("Wgpu::Quad::Gradient", "DRAW").entered(); - #[cfg(not(target_arch = "wasm32"))] { render_pass.set_pipeline(&self.pipeline); diff --git a/wgpu/src/quad/solid.rs b/wgpu/src/quad/solid.rs index 1cead367..45039a2d 100644 --- a/wgpu/src/quad/solid.rs +++ b/wgpu/src/quad/solid.rs @@ -144,9 +144,6 @@ impl Pipeline { layer: &'a Layer, range: Range<usize>, ) { - #[cfg(feature = "tracing")] - let _ = tracing::info_span!("Wgpu::Quad::Solid", "DRAW").entered(); - render_pass.set_pipeline(&self.pipeline); render_pass.set_bind_group(0, constants, &[]); render_pass.set_vertex_buffer(0, layer.instances.slice(..)); diff --git a/wgpu/src/shader/image.wgsl b/wgpu/src/shader/image.wgsl index accefc17..0eeb100f 100644 --- a/wgpu/src/shader/image.wgsl +++ b/wgpu/src/shader/image.wgsl @@ -38,7 +38,7 @@ fn vs_main(input: VertexInput) -> VertexOutput { out.opacity = input.opacity; // Calculate the vertex position and move the center to the origin - v_pos = input.pos + v_pos * input.scale - input.center; + v_pos = round(input.pos) + v_pos * input.scale - input.center; // Apply the rotation around the center of the image let cos_rot = cos(input.rotation); diff --git a/wgpu/src/text.rs b/wgpu/src/text.rs index 7e683c77..05db5f80 100644 --- a/wgpu/src/text.rs +++ b/wgpu/src/text.rs @@ -113,12 +113,13 @@ impl Storage { &mut self, device: &wgpu::Device, queue: &wgpu::Queue, + viewport: &glyphon::Viewport, encoder: &mut wgpu::CommandEncoder, format: wgpu::TextureFormat, + state: &glyphon::Cache, cache: &Cache, new_transformation: Transformation, bounds: Rectangle, - target_size: Size<u32>, ) { let group_count = self.groups.len(); @@ -131,7 +132,7 @@ impl Storage { Group { atlas: glyphon::TextAtlas::with_color_mode( - device, queue, format, COLOR_MODE, + device, queue, state, format, COLOR_MODE, ), version: 0, should_trim: false, @@ -151,6 +152,7 @@ impl Storage { let _ = prepare( device, queue, + viewport, encoder, &mut upload.renderer, &mut group.atlas, @@ -158,7 +160,6 @@ impl Storage { &cache.text, bounds, new_transformation, - target_size, ); } @@ -188,6 +189,7 @@ impl Storage { let _ = prepare( device, queue, + viewport, encoder, &mut renderer, &mut group.atlas, @@ -195,7 +197,6 @@ impl Storage { &cache.text, bounds, new_transformation, - target_size, ); } @@ -257,8 +258,23 @@ impl Storage { } } +pub struct Viewport(glyphon::Viewport); + +impl Viewport { + pub fn update(&mut self, queue: &wgpu::Queue, resolution: Size<u32>) { + self.0.update( + queue, + glyphon::Resolution { + width: resolution.width, + height: resolution.height, + }, + ); + } +} + #[allow(missing_debug_implementations)] pub struct Pipeline { + state: glyphon::Cache, format: wgpu::TextureFormat, atlas: glyphon::TextAtlas, renderers: Vec<glyphon::TextRenderer>, @@ -272,12 +288,16 @@ impl Pipeline { queue: &wgpu::Queue, format: wgpu::TextureFormat, ) -> Self { + let state = glyphon::Cache::new(device); + let atlas = glyphon::TextAtlas::with_color_mode( + device, queue, &state, format, COLOR_MODE, + ); + Pipeline { + state, format, renderers: Vec::new(), - atlas: glyphon::TextAtlas::with_color_mode( - device, queue, format, COLOR_MODE, - ), + atlas, prepare_layer: 0, cache: BufferCache::new(), } @@ -287,12 +307,12 @@ impl Pipeline { &mut self, device: &wgpu::Device, queue: &wgpu::Queue, + viewport: &Viewport, encoder: &mut wgpu::CommandEncoder, storage: &mut Storage, batch: &Batch, layer_bounds: Rectangle, layer_transformation: Transformation, - target_size: Size<u32>, ) { for item in batch { match item { @@ -313,6 +333,7 @@ impl Pipeline { let result = prepare( device, queue, + &viewport.0, encoder, renderer, &mut self.atlas, @@ -320,7 +341,6 @@ impl Pipeline { text, layer_bounds * layer_transformation, layer_transformation * *transformation, - target_size, ); match result { @@ -341,12 +361,13 @@ impl Pipeline { storage.prepare( device, queue, + &viewport.0, encoder, self.format, + &self.state, cache, layer_transformation * *transformation, layer_bounds * layer_transformation, - target_size, ); } } @@ -355,6 +376,7 @@ impl Pipeline { pub fn render<'a>( &'a self, + viewport: &'a Viewport, storage: &'a Storage, start: usize, batch: &'a Batch, @@ -376,7 +398,7 @@ impl Pipeline { let renderer = &self.renderers[start + layer_count]; renderer - .render(&self.atlas, render_pass) + .render(&self.atlas, &viewport.0, render_pass) .expect("Render text"); layer_count += 1; @@ -385,7 +407,7 @@ impl Pipeline { if let Some((atlas, upload)) = storage.get(cache) { upload .renderer - .render(atlas, render_pass) + .render(atlas, &viewport.0, render_pass) .expect("Render cached text"); } } @@ -395,6 +417,10 @@ impl Pipeline { layer_count } + pub fn create_viewport(&self, device: &wgpu::Device) -> Viewport { + Viewport(glyphon::Viewport::new(device, &self.state)) + } + pub fn end_frame(&mut self) { self.atlas.trim(); self.cache.trim(); @@ -406,6 +432,7 @@ impl Pipeline { fn prepare( device: &wgpu::Device, queue: &wgpu::Queue, + viewport: &glyphon::Viewport, encoder: &mut wgpu::CommandEncoder, renderer: &mut glyphon::TextRenderer, atlas: &mut glyphon::TextAtlas, @@ -413,7 +440,6 @@ fn prepare( sections: &[Text], layer_bounds: Rectangle, layer_transformation: Transformation, - target_size: Size<u32>, ) -> Result<(), glyphon::PrepareError> { let mut font_system = font_system().write().expect("Write font system"); let font_system = font_system.raw(); @@ -610,10 +636,7 @@ fn prepare( encoder, font_system, atlas, - glyphon::Resolution { - width: target_size.width, - height: target_size.height, - }, + viewport, text_areas, &mut glyphon::SwashCache::new(), ) diff --git a/widget/src/button.rs b/widget/src/button.rs index dc949671..5d446fea 100644 --- a/widget/src/button.rs +++ b/widget/src/button.rs @@ -205,7 +205,7 @@ where tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn Operation<Message>, + operation: &mut dyn Operation<()>, ) { operation.container(None, layout.bounds(), &mut |operation| { self.content.as_widget().operate( diff --git a/widget/src/canvas.rs b/widget/src/canvas.rs index be09f163..73cef087 100644 --- a/widget/src/canvas.rs +++ b/widget/src/canvas.rs @@ -172,7 +172,7 @@ where core::Event::Keyboard(keyboard_event) => { Some(Event::Keyboard(keyboard_event)) } - _ => None, + core::Event::Window(_) => None, }; if let Some(canvas_event) = canvas_event { diff --git a/widget/src/column.rs b/widget/src/column.rs index df7829b3..0b81c545 100644 --- a/widget/src/column.rs +++ b/widget/src/column.rs @@ -161,6 +161,19 @@ where } } +impl<'a, Message, Theme, Renderer: crate::core::Renderer> + FromIterator<Element<'a, Message, Theme, Renderer>> + for Column<'a, Message, Theme, Renderer> +{ + fn from_iter< + T: IntoIterator<Item = Element<'a, Message, Theme, Renderer>>, + >( + iter: T, + ) -> Self { + Self::with_children(iter) + } +} + impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer> for Column<'a, Message, Theme, Renderer> where @@ -208,7 +221,7 @@ where tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn Operation<Message>, + operation: &mut dyn Operation<()>, ) { operation.container(None, layout.bounds(), &mut |operation| { self.children diff --git a/widget/src/container.rs b/widget/src/container.rs index 8b6638d4..e917471f 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::Command; +use crate::runtime::Task; /// An element decorating some content. /// @@ -122,9 +122,6 @@ where /// Sets the [`Container`] to fill all the available space. /// - /// This can be useful to quickly position content when chained with - /// alignment functions—like [`center`]. - /// /// Calling this method is equivalent to chaining [`fill_x`] and /// [`fill_y`]. /// @@ -159,20 +156,14 @@ where self } - /// Sets the [`Container`] to fill the available space in the horizontal axis - /// and centers its contents there. - pub fn center_x(mut self) -> Self { - self.width = Length::Fill; - self.horizontal_alignment = alignment::Horizontal::Center; - self + /// Sets the width of the [`Container`] and centers its contents horizontally. + pub fn center_x(self, width: impl Into<Length>) -> Self { + self.width(width).align_x(alignment::Horizontal::Center) } - /// Sets the [`Container`] to fill the available space in the vertical axis - /// and centers its contents there. - pub fn center_y(mut self) -> Self { - self.height = Length::Fill; - self.vertical_alignment = alignment::Vertical::Center; - self + /// Sets the height of the [`Container`] and centers its contents vertically. + pub fn center_y(self, height: impl Into<Length>) -> Self { + self.height(height).align_y(alignment::Vertical::Center) } /// Centers the contents in both the horizontal and vertical axes of the @@ -182,8 +173,10 @@ where /// /// [`center_x`]: Self::center_x /// [`center_y`]: Self::center_y - pub fn center(self) -> Self { - self.center_x().center_y() + pub fn center(self, length: impl Into<Length>) -> Self { + let length = length.into(); + + self.center_x(length).center_y(length) } /// Sets whether the contents of the [`Container`] should be clipped on @@ -265,7 +258,7 @@ where tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn Operation<Message>, + operation: &mut dyn Operation<()>, ) { operation.container( self.id.as_ref().map(|id| &id.0), @@ -464,9 +457,9 @@ impl From<Id> for widget::Id { } } -/// Produces a [`Command`] that queries the visible screen bounds of the +/// Produces a [`Task`] that queries the visible screen bounds of the /// [`Container`] with the given [`Id`]. -pub fn visible_bounds(id: Id) -> Command<Option<Rectangle>> { +pub fn visible_bounds(id: Id) -> Task<Option<Rectangle>> { struct VisibleBounds { target: widget::Id, depth: usize, @@ -545,7 +538,7 @@ pub fn visible_bounds(id: Id) -> Command<Option<Rectangle>> { } } - Command::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 fd8614f5..62343a55 100644 --- a/widget/src/helpers.rs +++ b/widget/src/helpers.rs @@ -12,7 +12,7 @@ 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::Command; +use crate::runtime::{Action, Task}; use crate::scrollable::{self, Scrollable}; use crate::slider::{self, Slider}; use crate::text::{self, Text}; @@ -65,6 +65,52 @@ macro_rules! stack { ); } +/// Creates a new [`Text`] widget with the provided content. +/// +/// [`Text`]: core::widget::Text +/// +/// This macro uses the same syntax as [`format!`], but creates a new [`Text`] widget instead. +/// +/// See [the formatting documentation in `std::fmt`](std::fmt) +/// for details of the macro argument syntax. +/// +/// # Examples +/// +/// ```no_run +/// # mod iced { +/// # pub struct Element<Message>(pub std::marker::PhantomData<Message>); +/// # pub mod widget { +/// # macro_rules! text { +/// # ($($arg:tt)*) => {unimplemented!()} +/// # } +/// # pub(crate) use text; +/// # } +/// # } +/// # struct Example; +/// # enum Message {} +/// use iced::Element; +/// use iced::widget::text; +/// +/// impl Example { +/// fn view(&self) -> Element<Message> { +/// let simple = text!("Hello, world!"); +/// +/// let keyword = text!("Hello, {}", "world!"); +/// +/// let planet = "Earth"; +/// let local_variable = text!("Hello, {planet}!"); +/// // ... +/// # iced::Element(std::marker::PhantomData) +/// } +/// } +/// ``` +#[macro_export] +macro_rules! text { + ($($arg:tt)*) => { + $crate::Text::new(format!($($arg)*)) + }; +} + /// Creates a new [`Container`] with the provided content. /// /// [`Container`]: crate::Container @@ -83,9 +129,10 @@ where /// /// This is equivalent to: /// ```rust,no_run +/// # use iced_widget::core::Length; /// # use iced_widget::Container; /// # fn container<A>(x: A) -> Container<'static, ()> { unreachable!() } -/// let centered = container("Centered!").center(); +/// let centered = container("Centered!").center(Length::Fill); /// ``` /// /// [`Container`]: crate::Container @@ -96,7 +143,7 @@ where Theme: container::Catalog + 'a, Renderer: core::Renderer, { - container(content).fill().center() + container(content).center(Length::Fill) } /// Creates a new [`Column`] with the given children. @@ -228,7 +275,7 @@ where state: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn operation::Operation<Message>, + operation: &mut dyn operation::Operation<()>, ) { self.content .as_widget() @@ -430,7 +477,7 @@ where tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn operation::Operation<Message>, + operation: &mut dyn operation::Operation<()>, ) { let children = [&self.base, &self.top] .into_iter() @@ -882,19 +929,13 @@ where } /// Focuses the previous focusable widget. -pub fn focus_previous<Message>() -> Command<Message> -where - Message: 'static, -{ - Command::widget(operation::focusable::focus_previous()) +pub fn focus_previous<T>() -> Task<T> { + Task::effect(Action::widget(operation::focusable::focus_previous())) } /// Focuses the next focusable widget. -pub fn focus_next<Message>() -> Command<Message> -where - Message: 'static, -{ - Command::widget(operation::focusable::focus_next()) +pub fn focus_next<T>() -> Task<T> { + Task::effect(Action::widget(operation::focusable::focus_next())) } /// A container intercepting mouse events. diff --git a/widget/src/keyed/column.rs b/widget/src/keyed/column.rs index fdaadefa..69991d1f 100644 --- a/widget/src/keyed/column.rs +++ b/widget/src/keyed/column.rs @@ -265,7 +265,7 @@ where tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn Operation<Message>, + operation: &mut dyn Operation<()>, ) { operation.container(None, layout.bounds(), &mut |operation| { self.children diff --git a/widget/src/lazy.rs b/widget/src/lazy.rs index 04783dbe..606da22d 100644 --- a/widget/src/lazy.rs +++ b/widget/src/lazy.rs @@ -182,7 +182,7 @@ where tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn widget::Operation<Message>, + operation: &mut dyn widget::Operation<()>, ) { self.with_element(|element| { element.as_widget().operate( diff --git a/widget/src/lazy/cache.rs b/widget/src/lazy/cache.rs index f922fd19..b341c234 100644 --- a/widget/src/lazy/cache.rs +++ b/widget/src/lazy/cache.rs @@ -1,3 +1,4 @@ +#![allow(dead_code)] use crate::core::overlay; use crate::core::Element; diff --git a/widget/src/lazy/component.rs b/widget/src/lazy/component.rs index 7ba71a02..f079c0df 100644 --- a/widget/src/lazy/component.rs +++ b/widget/src/lazy/component.rs @@ -59,7 +59,7 @@ pub trait Component<Message, Theme = crate::Theme, Renderer = crate::Renderer> { fn operate( &self, _state: &mut Self::State, - _operation: &mut dyn widget::Operation<Message>, + _operation: &mut dyn widget::Operation<()>, ) { } @@ -172,7 +172,7 @@ where fn rebuild_element_with_operation( &self, - operation: &mut dyn widget::Operation<Message>, + operation: &mut dyn widget::Operation<()>, ) { let heads = self.state.borrow_mut().take().unwrap().into_heads(); @@ -358,70 +358,17 @@ where tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn widget::Operation<Message>, + operation: &mut dyn widget::Operation<()>, ) { self.rebuild_element_with_operation(operation); - struct MapOperation<'a, B> { - operation: &'a mut dyn widget::Operation<B>, - } - - impl<'a, T, B> widget::Operation<T> for MapOperation<'a, B> { - fn container( - &mut self, - id: Option<&widget::Id>, - bounds: Rectangle, - operate_on_children: &mut dyn FnMut( - &mut dyn widget::Operation<T>, - ), - ) { - self.operation.container(id, bounds, &mut |operation| { - operate_on_children(&mut MapOperation { operation }); - }); - } - - fn focusable( - &mut self, - state: &mut dyn widget::operation::Focusable, - id: Option<&widget::Id>, - ) { - self.operation.focusable(state, id); - } - - fn text_input( - &mut self, - state: &mut dyn widget::operation::TextInput, - id: Option<&widget::Id>, - ) { - self.operation.text_input(state, id); - } - - fn scrollable( - &mut self, - state: &mut dyn widget::operation::Scrollable, - id: Option<&widget::Id>, - bounds: Rectangle, - translation: Vector, - ) { - self.operation.scrollable(state, id, bounds, translation); - } - - fn custom( - &mut self, - state: &mut dyn std::any::Any, - id: Option<&widget::Id>, - ) { - self.operation.custom(state, id); - } - } - let tree = tree.state.downcast_mut::<Rc<RefCell<Option<Tree>>>>(); self.with_element(|element| { element.as_widget().operate( &mut tree.borrow_mut().as_mut().unwrap().children[0], layout, renderer, - &mut MapOperation { operation }, + operation, ); }); } diff --git a/widget/src/lazy/responsive.rs b/widget/src/lazy/responsive.rs index f612102e..27f52617 100644 --- a/widget/src/lazy/responsive.rs +++ b/widget/src/lazy/responsive.rs @@ -161,7 +161,7 @@ where tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn widget::Operation<Message>, + operation: &mut dyn widget::Operation<()>, ) { let state = tree.state.downcast_mut::<State>(); let mut content = self.content.borrow_mut(); diff --git a/widget/src/mouse_area.rs b/widget/src/mouse_area.rs index d7235cf6..17cae53b 100644 --- a/widget/src/mouse_area.rs +++ b/widget/src/mouse_area.rs @@ -178,7 +178,7 @@ where tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn Operation<Message>, + operation: &mut dyn Operation<()>, ) { self.content.as_widget().operate( &mut tree.children[0], diff --git a/widget/src/pane_grid.rs b/widget/src/pane_grid.rs index acfa9d44..c3da3879 100644 --- a/widget/src/pane_grid.rs +++ b/widget/src/pane_grid.rs @@ -324,7 +324,7 @@ where tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn widget::Operation<Message>, + operation: &mut dyn widget::Operation<()>, ) { operation.container(None, layout.bounds(), &mut |operation| { self.contents diff --git a/widget/src/pane_grid/content.rs b/widget/src/pane_grid/content.rs index 30ad52ca..d45fc0cd 100644 --- a/widget/src/pane_grid/content.rs +++ b/widget/src/pane_grid/content.rs @@ -214,7 +214,7 @@ where tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn widget::Operation<Message>, + operation: &mut dyn widget::Operation<()>, ) { let body_layout = if let Some(title_bar) = &self.title_bar { let mut children = layout.children(); diff --git a/widget/src/pane_grid/title_bar.rs b/widget/src/pane_grid/title_bar.rs index c2eeebb7..c05f1252 100644 --- a/widget/src/pane_grid/title_bar.rs +++ b/widget/src/pane_grid/title_bar.rs @@ -278,7 +278,7 @@ where tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn widget::Operation<Message>, + operation: &mut dyn widget::Operation<()>, ) { let mut children = layout.children(); let padded = children.next().unwrap(); diff --git a/widget/src/pick_list.rs b/widget/src/pick_list.rs index edccfdaa..97de5b48 100644 --- a/widget/src/pick_list.rs +++ b/widget/src/pick_list.rs @@ -161,6 +161,19 @@ where self } + /// Sets the style of the [`Menu`]. + #[must_use] + pub fn menu_style( + mut self, + style: impl Fn(&Theme) -> menu::Style + 'a, + ) -> Self + where + <Theme as menu::Catalog>::Class<'a>: From<menu::StyleFn<'a, Theme>>, + { + self.menu_class = (Box::new(style) as menu::StyleFn<'a, Theme>).into(); + self + } + /// Sets the style class of the [`PickList`]. #[cfg(feature = "advanced")] #[must_use] @@ -171,6 +184,17 @@ where self.class = class.into(); self } + + /// Sets the style class of the [`Menu`]. + #[cfg(feature = "advanced")] + #[must_use] + pub fn menu_class( + mut self, + class: impl Into<<Theme as menu::Catalog>::Class<'a>>, + ) -> Self { + self.menu_class = class.into(); + self + } } impl<'a, T, L, V, Message, Theme, Renderer> Widget<Message, Theme, Renderer> diff --git a/widget/src/row.rs b/widget/src/row.rs index fa352171..c8fcdb61 100644 --- a/widget/src/row.rs +++ b/widget/src/row.rs @@ -152,6 +152,19 @@ where } } +impl<'a, Message, Theme, Renderer: crate::core::Renderer> + FromIterator<Element<'a, Message, Theme, Renderer>> + for Row<'a, Message, Theme, Renderer> +{ + fn from_iter< + T: IntoIterator<Item = Element<'a, Message, Theme, Renderer>>, + >( + iter: T, + ) -> Self { + Self::with_children(iter) + } +} + impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer> for Row<'a, Message, Theme, Renderer> where @@ -197,7 +210,7 @@ where tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn Operation<Message>, + operation: &mut dyn Operation<()>, ) { operation.container(None, layout.bounds(), &mut |operation| { self.children diff --git a/widget/src/scrollable.rs b/widget/src/scrollable.rs index 6fc00f87..c3d08223 100644 --- a/widget/src/scrollable.rs +++ b/widget/src/scrollable.rs @@ -15,7 +15,7 @@ use crate::core::{ self, Background, Border, Clipboard, Color, Element, Layout, Length, Pixels, Point, Rectangle, Shell, Size, Theme, Vector, Widget, }; -use crate::runtime::Command; +use crate::runtime::{Action, Task}; pub use operation::scrollable::{AbsoluteOffset, RelativeOffset}; @@ -295,7 +295,7 @@ where tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn Operation<Message>, + operation: &mut dyn Operation<()>, ) { let state = tree.state.downcast_mut::<State>(); @@ -952,22 +952,18 @@ impl From<Id> for widget::Id { } } -/// Produces a [`Command`] that snaps the [`Scrollable`] with the given [`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<Message: 'static>( - id: Id, - offset: RelativeOffset, -) -> Command<Message> { - Command::widget(operation::scrollable::snap_to(id.0, offset)) +pub fn snap_to<T>(id: Id, offset: RelativeOffset) -> Task<T> { + Task::effect(Action::widget(operation::scrollable::snap_to(id.0, offset))) } -/// Produces a [`Command`] that scrolls the [`Scrollable`] with the given [`Id`] +/// Produces a [`Task`] that scrolls the [`Scrollable`] with the given [`Id`] /// to the provided [`AbsoluteOffset`] along the x & y axis. -pub fn scroll_to<Message: 'static>( - id: Id, - offset: AbsoluteOffset, -) -> Command<Message> { - Command::widget(operation::scrollable::scroll_to(id.0, offset)) +pub fn scroll_to<T>(id: Id, offset: AbsoluteOffset) -> Task<T> { + Task::effect(Action::widget(operation::scrollable::scroll_to( + id.0, offset, + ))) } /// Returns [`true`] if the viewport actually changed. diff --git a/widget/src/shader.rs b/widget/src/shader.rs index fad2f4eb..3c81f8ed 100644 --- a/widget/src/shader.rs +++ b/widget/src/shader.rs @@ -107,10 +107,10 @@ where Some(Event::Keyboard(keyboard_event)) } core::Event::Touch(touch_event) => Some(Event::Touch(touch_event)), - core::Event::Window(_, window::Event::RedrawRequested(instant)) => { + core::Event::Window(window::Event::RedrawRequested(instant)) => { Some(Event::RedrawRequested(instant)) } - _ => None, + core::Event::Window(_) => None, }; if let Some(custom_shader_event) = custom_shader_event { diff --git a/widget/src/stack.rs b/widget/src/stack.rs index 5035541b..efa9711d 100644 --- a/widget/src/stack.rs +++ b/widget/src/stack.rs @@ -189,7 +189,7 @@ where tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn Operation<Message>, + operation: &mut dyn Operation<()>, ) { operation.container(None, layout.bounds(), &mut |operation| { self.children diff --git a/widget/src/text_editor.rs b/widget/src/text_editor.rs index 7c0b98ea..fc2ade43 100644 --- a/widget/src/text_editor.rs +++ b/widget/src/text_editor.rs @@ -466,6 +466,12 @@ where shell.publish(on_edit(action)); } Update::Scroll(lines) => { + let bounds = self.content.0.borrow().editor.bounds(); + + if bounds.height >= i32::MAX as f32 { + return event::Status::Ignored; + } + let lines = lines + state.partial_scroll; state.partial_scroll = lines.fract(); @@ -768,9 +774,17 @@ impl Update { if let keyboard::Key::Named(named_key) = key.as_ref() { if let Some(motion) = motion(named_key) { - let motion = if platform::is_jump_modifier_pressed( - modifiers, - ) { + let motion = if modifiers.macos_command() { + match motion { + Motion::Left => Motion::Home, + Motion::Right => Motion::End, + _ => motion, + } + } else { + motion + }; + + let motion = if modifiers.jump() { motion.widen() } else { motion @@ -807,18 +821,6 @@ fn motion(key: key::Named) -> Option<Motion> { } } -mod platform { - use crate::core::keyboard; - - pub fn is_jump_modifier_pressed(modifiers: keyboard::Modifiers) -> bool { - if cfg!(target_os = "macos") { - modifiers.alt() - } else { - modifiers.control() - } - } -} - /// The possible status of a [`TextEditor`]. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Status { diff --git a/widget/src/text_input.rs b/widget/src/text_input.rs index e9f07838..4e89236b 100644 --- a/widget/src/text_input.rs +++ b/widget/src/text_input.rs @@ -30,7 +30,7 @@ use crate::core::{ Background, Border, Color, Element, Layout, Length, Padding, Pixels, Point, Rectangle, Shell, Size, Theme, Vector, Widget, }; -use crate::runtime::Command; +use crate::runtime::{Action, Task}; /// A field that can be filled with text. /// @@ -540,7 +540,7 @@ where tree: &mut Tree, _layout: Layout<'_>, _renderer: &Renderer, - operation: &mut dyn Operation<Message>, + operation: &mut dyn Operation<()>, ) { let state = tree.state.downcast_mut::<State<Renderer::Paragraph>>(); @@ -826,7 +826,7 @@ where } } keyboard::Key::Named(key::Named::Backspace) => { - if platform::is_jump_modifier_pressed(modifiers) + if modifiers.jump() && state.cursor.selection(&self.value).is_none() { if self.is_secure { @@ -850,7 +850,7 @@ where update_cache(state, &self.value); } keyboard::Key::Named(key::Named::Delete) => { - if platform::is_jump_modifier_pressed(modifiers) + if modifiers.jump() && state.cursor.selection(&self.value).is_none() { if self.is_secure { @@ -876,10 +876,52 @@ where update_cache(state, &self.value); } + keyboard::Key::Named(key::Named::Home) => { + if modifiers.shift() { + state.cursor.select_range( + state.cursor.start(&self.value), + 0, + ); + } else { + state.cursor.move_to(0); + } + } + keyboard::Key::Named(key::Named::End) => { + if modifiers.shift() { + state.cursor.select_range( + state.cursor.start(&self.value), + self.value.len(), + ); + } else { + state.cursor.move_to(self.value.len()); + } + } + keyboard::Key::Named(key::Named::ArrowLeft) + if modifiers.macos_command() => + { + if modifiers.shift() { + state.cursor.select_range( + state.cursor.start(&self.value), + 0, + ); + } else { + state.cursor.move_to(0); + } + } + keyboard::Key::Named(key::Named::ArrowRight) + if modifiers.macos_command() => + { + if modifiers.shift() { + state.cursor.select_range( + state.cursor.start(&self.value), + self.value.len(), + ); + } else { + state.cursor.move_to(self.value.len()); + } + } keyboard::Key::Named(key::Named::ArrowLeft) => { - if platform::is_jump_modifier_pressed(modifiers) - && !self.is_secure - { + if modifiers.jump() && !self.is_secure { if modifiers.shift() { state .cursor @@ -896,9 +938,7 @@ where } } keyboard::Key::Named(key::Named::ArrowRight) => { - if platform::is_jump_modifier_pressed(modifiers) - && !self.is_secure - { + if modifiers.jump() && !self.is_secure { if modifiers.shift() { state .cursor @@ -914,26 +954,6 @@ where state.cursor.move_right(&self.value); } } - keyboard::Key::Named(key::Named::Home) => { - if modifiers.shift() { - state.cursor.select_range( - state.cursor.start(&self.value), - 0, - ); - } else { - state.cursor.move_to(0); - } - } - keyboard::Key::Named(key::Named::End) => { - if modifiers.shift() { - state.cursor.select_range( - state.cursor.start(&self.value), - self.value.len(), - ); - } else { - state.cursor.move_to(self.value.len()); - } - } keyboard::Key::Named(key::Named::Escape) => { state.is_focused = None; state.is_dragging = false; @@ -983,14 +1003,14 @@ where state.keyboard_modifiers = modifiers; } - Event::Window(_, window::Event::Unfocused) => { + Event::Window(window::Event::Unfocused) => { let state = state::<Renderer>(tree); if let Some(focus) = &mut state.is_focused { focus.is_window_focused = false; } } - Event::Window(_, window::Event::Focused) => { + Event::Window(window::Event::Focused) => { let state = state::<Renderer>(tree); if let Some(focus) = &mut state.is_focused { @@ -1000,7 +1020,7 @@ where shell.request_redraw(window::RedrawRequest::NextFrame); } } - Event::Window(_, window::Event::RedrawRequested(now)) => { + Event::Window(window::Event::RedrawRequested(now)) => { let state = state::<Renderer>(tree); if let Some(focus) = &mut state.is_focused { @@ -1120,35 +1140,38 @@ impl From<Id> for widget::Id { } } -/// Produces a [`Command`] that focuses the [`TextInput`] with the given [`Id`]. -pub fn focus<Message: 'static>(id: Id) -> Command<Message> { - Command::widget(operation::focusable::focus(id.0)) +/// 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))) } -/// Produces a [`Command`] that moves the cursor of the [`TextInput`] with the given [`Id`] to the +/// Produces a [`Task`] that moves the cursor of the [`TextInput`] with the given [`Id`] to the /// end. -pub fn move_cursor_to_end<Message: 'static>(id: Id) -> Command<Message> { - Command::widget(operation::text_input::move_cursor_to_end(id.0)) +pub fn move_cursor_to_end<T>(id: Id) -> Task<T> { + Task::effect(Action::widget(operation::text_input::move_cursor_to_end( + id.0, + ))) } -/// Produces a [`Command`] that moves the cursor of the [`TextInput`] with the given [`Id`] to the +/// Produces a [`Task`] that moves the cursor of the [`TextInput`] with the given [`Id`] to the /// front. -pub fn move_cursor_to_front<Message: 'static>(id: Id) -> Command<Message> { - Command::widget(operation::text_input::move_cursor_to_front(id.0)) +pub fn move_cursor_to_front<T>(id: Id) -> Task<T> { + Task::effect(Action::widget(operation::text_input::move_cursor_to_front( + id.0, + ))) } -/// Produces a [`Command`] that moves the cursor of the [`TextInput`] with the given [`Id`] to the +/// Produces a [`Task`] that moves the cursor of the [`TextInput`] with the given [`Id`] to the /// provided position. -pub fn move_cursor_to<Message: 'static>( - id: Id, - position: usize, -) -> Command<Message> { - Command::widget(operation::text_input::move_cursor_to(id.0, position)) +pub fn move_cursor_to<T>(id: Id, position: usize) -> Task<T> { + Task::effect(Action::widget(operation::text_input::move_cursor_to( + id.0, position, + ))) } -/// Produces a [`Command`] that selects all the content of the [`TextInput`] with the given [`Id`]. -pub fn select_all<Message: 'static>(id: Id) -> Command<Message> { - Command::widget(operation::text_input::select_all(id.0)) +/// 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))) } /// The state of a [`TextInput`]. @@ -1281,18 +1304,6 @@ impl<P: text::Paragraph> operation::TextInput for State<P> { } } -mod platform { - use crate::core::keyboard; - - pub fn is_jump_modifier_pressed(modifiers: keyboard::Modifiers) -> bool { - if cfg!(target_os = "macos") { - modifiers.alt() - } else { - modifiers.control() - } - } -} - fn offset<P: text::Paragraph>( text_bounds: Rectangle, value: &Value, diff --git a/widget/src/themer.rs b/widget/src/themer.rs index f4597458..9eb47d84 100644 --- a/widget/src/themer.rs +++ b/widget/src/themer.rs @@ -104,7 +104,7 @@ where tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn Operation<Message>, + operation: &mut dyn Operation<()>, ) { self.content .as_widget() @@ -236,7 +236,7 @@ where &mut self, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn Operation<Message>, + operation: &mut dyn Operation<()>, ) { self.content.operate(layout, renderer, operation); } diff --git a/winit/README.md b/winit/README.md index 91307970..c60e81f9 100644 --- a/winit/README.md +++ b/winit/README.md @@ -15,15 +15,3 @@ It exposes a renderer-agnostic `Application` trait that can be implemented and t [documentation]: https://docs.rs/iced_winit [`iced_native`]: ../native [`winit`]: https://github.com/rust-windowing/winit - -## Installation -Add `iced_winit` as a dependency in your `Cargo.toml`: - -```toml -iced_winit = "0.9" -``` - -__Iced moves fast and the `master` branch can contain breaking changes!__ If -you want to learn about a specific release, check out [the release list]. - -[the release list]: https://github.com/iced-rs/iced/releases diff --git a/winit/src/application.rs b/winit/src/application.rs index 3bc29255..a93878ea 100644 --- a/winit/src/application.rs +++ b/winit/src/application.rs @@ -12,13 +12,14 @@ use crate::core::widget::operation; use crate::core::window; use crate::core::{Color, Event, Point, Size, Theme}; use crate::futures::futures; -use crate::futures::{Executor, Runtime, Subscription}; +use crate::futures::subscription::{self, Subscription}; +use crate::futures::{Executor, Runtime}; use crate::graphics; use crate::graphics::compositor::{self, Compositor}; use crate::runtime::clipboard; use crate::runtime::program::Program; use crate::runtime::user_interface::{self, UserInterface}; -use crate::runtime::{Command, Debug}; +use crate::runtime::{Action, Debug, Task}; use crate::{Clipboard, Error, Proxy, Settings}; use futures::channel::mpsc; @@ -35,7 +36,7 @@ use std::sync::Arc; /// its own window. /// /// An [`Application`] can execute asynchronous actions by returning a -/// [`Command`] in some of its methods. +/// [`Task`] in some of its methods. /// /// When using an [`Application`] with the `debug` feature enabled, a debug view /// can be toggled by pressing `F12`. @@ -51,10 +52,10 @@ where /// /// Here is where you should return the initial state of your app. /// - /// Additionally, you can return a [`Command`] if you need to perform some + /// Additionally, you can return a [`Task`] if you need to perform some /// async action in the background on startup. This is useful if you want to /// load state from a file, perform an initial HTTP request, etc. - fn new(flags: Self::Flags) -> (Self, Command<Self::Message>); + fn new(flags: Self::Flags) -> (Self, Task<Self::Message>); /// Returns the current title of the [`Application`]. /// @@ -154,19 +155,23 @@ where let (proxy, worker) = Proxy::new(event_loop.create_proxy()); - let runtime = { + let mut runtime = { let executor = E::new().map_err(Error::ExecutorCreationFailed)?; executor.spawn(worker); Runtime::new(executor, proxy.clone()) }; - let (application, init_command) = { + let (application, task) = { let flags = settings.flags; runtime.enter(|| A::new(flags)) }; + if let Some(stream) = task.into_stream() { + runtime.run(stream); + } + let id = settings.id; let title = application.title(); @@ -182,7 +187,6 @@ where boot_receiver, event_receiver, control_sender, - init_command, settings.fonts, )); @@ -192,13 +196,13 @@ where instance: std::pin::Pin<Box<F>>, context: task::Context<'static>, boot: Option<BootConfig<C>>, - sender: mpsc::UnboundedSender<winit::event::Event<Message>>, + sender: mpsc::UnboundedSender<winit::event::Event<Action<Message>>>, receiver: mpsc::UnboundedReceiver<winit::event_loop::ControlFlow>, error: Option<Error>, #[cfg(target_arch = "wasm32")] is_booted: std::rc::Rc<std::cell::RefCell<bool>>, #[cfg(target_arch = "wasm32")] - queued_events: Vec<winit::event::Event<Message>>, + queued_events: Vec<winit::event::Event<Action<Message>>>, } struct BootConfig<C> { @@ -228,7 +232,7 @@ where queued_events: Vec::new(), }; - impl<Message, F, C> winit::application::ApplicationHandler<Message> + impl<Message, F, C> winit::application::ApplicationHandler<Action<Message>> for Runner<Message, F, C> where F: Future<Output = ()>, @@ -392,11 +396,11 @@ where fn user_event( &mut self, event_loop: &winit::event_loop::ActiveEventLoop, - message: Message, + action: Action<Message>, ) { self.process_event( event_loop, - winit::event::Event::UserEvent(message), + winit::event::Event::UserEvent(action), ); } @@ -415,7 +419,7 @@ where fn process_event( &mut self, event_loop: &winit::event_loop::ActiveEventLoop, - event: winit::event::Event<Message>, + event: winit::event::Event<Action<Message>>, ) { // On Wasm, events may start being processed before the compositor // boots up. We simply queue them and process them once ready. @@ -479,15 +483,14 @@ struct Boot<C> { async fn run_instance<A, E, C>( mut application: A, - mut runtime: Runtime<E, Proxy<A::Message>, A::Message>, + mut runtime: Runtime<E, Proxy<A::Message>, Action<A::Message>>, mut proxy: Proxy<A::Message>, mut debug: Debug, mut boot: oneshot::Receiver<Boot<C>>, mut event_receiver: mpsc::UnboundedReceiver< - winit::event::Event<A::Message>, + winit::event::Event<Action<A::Message>>, >, mut control_sender: mpsc::UnboundedSender<winit::event_loop::ControlFlow>, - init_command: Command<A::Message>, fonts: Vec<Cow<'static, [u8]>>, ) where A: Application + 'static, @@ -517,7 +520,7 @@ async fn run_instance<A, E, C>( let physical_size = state.physical_size(); let mut clipboard = Clipboard::connect(&window); - let mut cache = user_interface::Cache::default(); + let cache = user_interface::Cache::default(); let mut surface = compositor.create_surface( window.clone(), physical_size.width, @@ -529,22 +532,12 @@ async fn run_instance<A, E, C>( window.set_visible(true); } - run_command( - &application, - &mut compositor, - &mut surface, - &mut cache, - &state, - &mut renderer, - init_command, - &mut runtime, - &mut clipboard, - &mut should_exit, - &mut proxy, - &mut debug, - &window, + runtime.track( + application + .subscription() + .map(Action::Output) + .into_recipes(), ); - runtime.track(application.subscription().into_recipes()); let mut user_interface = ManuallyDrop::new(build_user_interface( &application, @@ -574,16 +567,27 @@ async fn run_instance<A, E, C>( event::Event::PlatformSpecific(event::PlatformSpecific::MacOS( event::MacOS::ReceivedUrl(url), )) => { - use crate::core::event; - - events.push(Event::PlatformSpecific( - event::PlatformSpecific::MacOS(event::MacOS::ReceivedUrl( - url, - )), + runtime.broadcast(subscription::Event::PlatformSpecific( + subscription::PlatformSpecific::MacOS( + subscription::MacOS::ReceivedUrl(url), + ), )); } - event::Event::UserEvent(message) => { - messages.push(message); + event::Event::UserEvent(action) => { + run_action( + action, + &mut user_interface, + &mut compositor, + &mut surface, + &state, + &mut renderer, + &mut messages, + &mut clipboard, + &mut should_exit, + &mut debug, + &window, + ); + user_events += 1; } event::Event::WindowEvent { @@ -623,7 +627,6 @@ async fn run_instance<A, E, C>( // Then, we can use the `interface_state` here to decide if a redraw // is needed right away, or simply wait until a specific time. let redraw_event = Event::Window( - window::Id::MAIN, window::Event::RedrawRequested(Instant::now()), ); @@ -651,7 +654,11 @@ async fn run_instance<A, E, C>( _ => ControlFlow::Wait, }); - runtime.broadcast(redraw_event, core::event::Status::Ignored); + runtime.broadcast(subscription::Event::Interaction { + window: window::Id::MAIN, + event: redraw_event, + status: core::event::Status::Ignored, + }); debug.draw_started(); let new_mouse_interaction = user_interface.draw( @@ -714,7 +721,6 @@ async fn run_instance<A, E, C>( state.update(&window, &window_event, &mut debug); if let Some(event) = conversion::window_event( - window::Id::MAIN, window_event, state.scale_factor(), state.modifiers(), @@ -742,7 +748,11 @@ async fn run_instance<A, E, C>( for (event, status) in events.drain(..).zip(statuses.into_iter()) { - runtime.broadcast(event, status); + runtime.broadcast(subscription::Event::Interaction { + window: window::Id::MAIN, + event, + status, + }); } if !messages.is_empty() @@ -751,21 +761,14 @@ async fn run_instance<A, E, C>( user_interface::State::Outdated ) { - let mut cache = + let cache = ManuallyDrop::into_inner(user_interface).into_cache(); // Update application update( &mut application, - &mut compositor, - &mut surface, - &mut cache, &mut state, - &mut renderer, &mut runtime, - &mut clipboard, - &mut should_exit, - &mut proxy, &mut debug, &mut messages, &window, @@ -850,281 +853,230 @@ where } /// Updates an [`Application`] by feeding it the provided messages, spawning any -/// resulting [`Command`], and tracking its [`Subscription`]. -pub fn update<A: Application, C, E: Executor>( +/// resulting [`Task`], and tracking its [`Subscription`]. +pub fn update<A: Application, E: Executor>( application: &mut A, - compositor: &mut C, - surface: &mut C::Surface, - cache: &mut user_interface::Cache, state: &mut State<A>, - renderer: &mut A::Renderer, - runtime: &mut Runtime<E, Proxy<A::Message>, A::Message>, - clipboard: &mut Clipboard, - should_exit: &mut bool, - proxy: &mut Proxy<A::Message>, + runtime: &mut Runtime<E, Proxy<A::Message>, Action<A::Message>>, debug: &mut Debug, messages: &mut Vec<A::Message>, window: &winit::window::Window, ) where - C: Compositor<Renderer = A::Renderer> + 'static, A::Theme: DefaultStyle, { for message in messages.drain(..) { debug.log_message(&message); debug.update_started(); - let command = runtime.enter(|| application.update(message)); + let task = runtime.enter(|| application.update(message)); debug.update_finished(); - run_command( - application, - compositor, - surface, - cache, - state, - renderer, - command, - runtime, - clipboard, - should_exit, - proxy, - debug, - window, - ); + if let Some(stream) = task.into_stream() { + runtime.run(stream); + } } state.synchronize(application, window); let subscription = application.subscription(); - runtime.track(subscription.into_recipes()); + runtime.track(subscription.map(Action::Output).into_recipes()); } -/// Runs the actions of a [`Command`]. -pub fn run_command<A, C, E>( - application: &A, +/// Runs the actions of a [`Task`]. +pub fn run_action<A, C>( + action: Action<A::Message>, + user_interface: &mut UserInterface<'_, A::Message, A::Theme, C::Renderer>, compositor: &mut C, surface: &mut C::Surface, - cache: &mut user_interface::Cache, state: &State<A>, renderer: &mut A::Renderer, - command: Command<A::Message>, - runtime: &mut Runtime<E, Proxy<A::Message>, A::Message>, + messages: &mut Vec<A::Message>, clipboard: &mut Clipboard, should_exit: &mut bool, - proxy: &mut Proxy<A::Message>, debug: &mut Debug, window: &winit::window::Window, ) where A: Application, - E: Executor, C: Compositor<Renderer = A::Renderer> + 'static, A::Theme: DefaultStyle, { - use crate::runtime::command; use crate::runtime::system; use crate::runtime::window; - for action in command.actions() { - match action { - command::Action::Future(future) => { - runtime.spawn(future); + match action { + Action::Clipboard(action) => match action { + clipboard::Action::Read { target, channel } => { + let _ = channel.send(clipboard.read(target)); } - command::Action::Stream(stream) => { - runtime.run(stream); + clipboard::Action::Write { target, contents } => { + clipboard.write(target, contents); } - command::Action::Clipboard(action) => match action { - clipboard::Action::Read(tag, kind) => { - let message = tag(clipboard.read(kind)); - - proxy.send(message); - } - clipboard::Action::Write(contents, kind) => { - clipboard.write(kind, contents); - } - }, - command::Action::Window(action) => match action { - window::Action::Close(_id) => { - *should_exit = true; - } - window::Action::Drag(_id) => { - let _res = window.drag_window(); - } - window::Action::Spawn { .. } => { - log::warn!( - "Spawning a window is only available with \ + }, + Action::Window(action) => match action { + window::Action::Close(_id) => { + *should_exit = true; + } + window::Action::Drag(_id) => { + let _res = window.drag_window(); + } + window::Action::Open { .. } => { + log::warn!( + "Spawning a window is only available with \ multi-window applications." - ); - } - window::Action::Resize(_id, size) => { - let _ = - window.request_inner_size(winit::dpi::LogicalSize { - width: size.width, - height: size.height, - }); - } - window::Action::FetchSize(_id, callback) => { - let size = - window.inner_size().to_logical(window.scale_factor()); + ); + } + window::Action::Resize(_id, size) => { + let _ = window.request_inner_size(winit::dpi::LogicalSize { + width: size.width, + height: size.height, + }); + } + window::Action::FetchSize(_id, channel) => { + let size = + window.inner_size().to_logical(window.scale_factor()); - proxy.send(callback(Size::new(size.width, size.height))); - } - window::Action::FetchMaximized(_id, callback) => { - proxy.send(callback(window.is_maximized())); - } - window::Action::Maximize(_id, maximized) => { - window.set_maximized(maximized); - } - window::Action::FetchMinimized(_id, callback) => { - proxy.send(callback(window.is_minimized())); - } - window::Action::Minimize(_id, minimized) => { - window.set_minimized(minimized); - } - window::Action::FetchPosition(_id, callback) => { - let position = window - .inner_position() - .map(|position| { - let position = position - .to_logical::<f32>(window.scale_factor()); - - Point::new(position.x, position.y) - }) - .ok(); + let _ = channel.send(Size::new(size.width, size.height)); + } + window::Action::FetchMaximized(_id, channel) => { + let _ = channel.send(window.is_maximized()); + } + window::Action::Maximize(_id, maximized) => { + window.set_maximized(maximized); + } + window::Action::FetchMinimized(_id, channel) => { + let _ = channel.send(window.is_minimized()); + } + window::Action::Minimize(_id, minimized) => { + window.set_minimized(minimized); + } + window::Action::FetchPosition(_id, channel) => { + let position = window + .inner_position() + .map(|position| { + let position = + position.to_logical::<f32>(window.scale_factor()); + + Point::new(position.x, position.y) + }) + .ok(); + + let _ = channel.send(position); + } + window::Action::Move(_id, position) => { + window.set_outer_position(winit::dpi::LogicalPosition { + x: position.x, + y: position.y, + }); + } + window::Action::ChangeMode(_id, mode) => { + window.set_visible(conversion::visible(mode)); + window.set_fullscreen(conversion::fullscreen( + window.current_monitor(), + mode, + )); + } + window::Action::ChangeIcon(_id, icon) => { + window.set_window_icon(conversion::icon(icon)); + } + window::Action::FetchMode(_id, channel) => { + let mode = if window.is_visible().unwrap_or(true) { + conversion::mode(window.fullscreen()) + } else { + core::window::Mode::Hidden + }; - proxy.send(callback(position)); - } - window::Action::Move(_id, position) => { - window.set_outer_position(winit::dpi::LogicalPosition { - x: position.x, - y: position.y, + let _ = channel.send(mode); + } + window::Action::ToggleMaximize(_id) => { + window.set_maximized(!window.is_maximized()); + } + window::Action::ToggleDecorations(_id) => { + window.set_decorations(!window.is_decorated()); + } + window::Action::RequestUserAttention(_id, user_attention) => { + window.request_user_attention( + user_attention.map(conversion::user_attention), + ); + } + window::Action::GainFocus(_id) => { + window.focus_window(); + } + window::Action::ChangeLevel(_id, level) => { + window.set_window_level(conversion::window_level(level)); + } + window::Action::ShowSystemMenu(_id) => { + if let mouse::Cursor::Available(point) = state.cursor() { + window.show_window_menu(winit::dpi::LogicalPosition { + x: point.x, + y: point.y, }); } - window::Action::ChangeMode(_id, mode) => { - window.set_visible(conversion::visible(mode)); - window.set_fullscreen(conversion::fullscreen( - window.current_monitor(), - mode, - )); - } - window::Action::ChangeIcon(_id, icon) => { - window.set_window_icon(conversion::icon(icon)); - } - window::Action::FetchMode(_id, tag) => { - let mode = if window.is_visible().unwrap_or(true) { - conversion::mode(window.fullscreen()) - } else { - core::window::Mode::Hidden - }; - - proxy.send(tag(mode)); - } - window::Action::ToggleMaximize(_id) => { - window.set_maximized(!window.is_maximized()); - } - window::Action::ToggleDecorations(_id) => { - window.set_decorations(!window.is_decorated()); - } - window::Action::RequestUserAttention(_id, user_attention) => { - window.request_user_attention( - user_attention.map(conversion::user_attention), - ); - } - window::Action::GainFocus(_id) => { - window.focus_window(); - } - window::Action::ChangeLevel(_id, level) => { - window.set_window_level(conversion::window_level(level)); - } - window::Action::ShowSystemMenu(_id) => { - if let mouse::Cursor::Available(point) = state.cursor() { - window.show_window_menu(winit::dpi::LogicalPosition { - x: point.x, - y: point.y, - }); - } - } - window::Action::FetchId(_id, tag) => { - proxy.send(tag(window.id().into())); - } - window::Action::RunWithHandle(_id, tag) => { - use window::raw_window_handle::HasWindowHandle; + } + window::Action::FetchRawId(_id, channel) => { + let _ = channel.send(window.id().into()); + } + window::Action::RunWithHandle(_id, f) => { + use window::raw_window_handle::HasWindowHandle; - if let Ok(handle) = window.window_handle() { - proxy.send(tag(&handle)); - } + if let Ok(handle) = window.window_handle() { + f(handle); } + } - window::Action::Screenshot(_id, tag) => { - let bytes = compositor.screenshot( - renderer, - surface, - state.viewport(), - state.background_color(), - &debug.overlay(), - ); - - proxy.send(tag(window::Screenshot::new( - bytes, - state.physical_size(), - ))); - } - }, - command::Action::System(action) => match action { - system::Action::QueryInformation(_tag) => { - #[cfg(feature = "system")] - { - let graphics_info = compositor.fetch_information(); - let mut proxy = proxy.clone(); - - let _ = std::thread::spawn(move || { - let information = - crate::system::information(graphics_info); - - let message = _tag(information); - - proxy.send(message); - }); - } - } - }, - command::Action::Widget(action) => { - let mut current_cache = std::mem::take(cache); - let mut current_operation = Some(action); - - let mut user_interface = build_user_interface( - application, - current_cache, + window::Action::Screenshot(_id, channel) => { + let bytes = compositor.screenshot( renderer, - state.logical_size(), - debug, + surface, + state.viewport(), + state.background_color(), + &debug.overlay(), ); - while let Some(mut operation) = current_operation.take() { - user_interface.operate(renderer, operation.as_mut()); + let _ = channel.send(window::Screenshot::new( + bytes, + state.physical_size(), + state.viewport().scale_factor(), + )); + } + }, + Action::System(action) => match action { + system::Action::QueryInformation(_channel) => { + #[cfg(feature = "system")] + { + let graphics_info = compositor.fetch_information(); - match operation.finish() { - operation::Outcome::None => {} - operation::Outcome::Some(message) => { - proxy.send(message); - } - operation::Outcome::Chain(next) => { - current_operation = Some(next); - } + let _ = std::thread::spawn(move || { + let information = + crate::system::information(graphics_info); + + let _ = _channel.send(information); + }); + } + } + }, + Action::Widget(operation) => { + let mut current_operation = Some(operation); + + while let Some(mut operation) = current_operation.take() { + user_interface.operate(renderer, operation.as_mut()); + + match operation.finish() { + operation::Outcome::None => {} + operation::Outcome::Some(()) => {} + operation::Outcome::Chain(next) => { + current_operation = Some(next); } } - - current_cache = user_interface.into_cache(); - *cache = current_cache; } - command::Action::LoadFont { bytes, tagger } => { - // TODO: Error handling (?) - compositor.load_font(bytes); + } + Action::LoadFont { bytes, channel } => { + // TODO: Error handling (?) + compositor.load_font(bytes); - proxy.send(tagger(Ok(()))); - } - command::Action::Custom(_) => { - log::warn!("Unsupported custom action in `iced_winit` shell"); - } + let _ = channel.send(Ok(())); + } + Action::Output(message) => { + messages.push(message); } } } diff --git a/winit/src/conversion.rs b/winit/src/conversion.rs index d04fc2f1..0ed10c88 100644 --- a/winit/src/conversion.rs +++ b/winit/src/conversion.rs @@ -73,11 +73,7 @@ pub fn window_attributes( #[cfg(target_os = "windows")] { use winit::platform::windows::WindowAttributesExtWindows; - #[allow(unsafe_code)] - unsafe { - attributes = attributes - .with_parent_window(settings.platform_specific.parent); - } + attributes = attributes .with_drag_and_drop(settings.platform_specific.drag_and_drop); @@ -126,7 +122,6 @@ pub fn window_attributes( /// Converts a winit window event into an iced event. pub fn window_event( - id: window::Id, event: winit::event::WindowEvent, scale_factor: f64, modifiers: winit::keyboard::ModifiersState, @@ -137,16 +132,13 @@ pub fn window_event( WindowEvent::Resized(new_size) => { let logical_size = new_size.to_logical(scale_factor); - Some(Event::Window( - id, - window::Event::Resized { - width: logical_size.width, - height: logical_size.height, - }, - )) + Some(Event::Window(window::Event::Resized { + width: logical_size.width, + height: logical_size.height, + })) } WindowEvent::CloseRequested => { - Some(Event::Window(id, window::Event::CloseRequested)) + Some(Event::Window(window::Event::CloseRequested)) } WindowEvent::CursorMoved { position, .. } => { let position = position.to_logical::<f64>(scale_factor); @@ -264,22 +256,19 @@ pub fn window_event( self::modifiers(new_modifiers.state()), ))) } - WindowEvent::Focused(focused) => Some(Event::Window( - id, - if focused { - window::Event::Focused - } else { - window::Event::Unfocused - }, - )), + WindowEvent::Focused(focused) => Some(Event::Window(if focused { + window::Event::Focused + } else { + window::Event::Unfocused + })), WindowEvent::HoveredFile(path) => { - Some(Event::Window(id, window::Event::FileHovered(path.clone()))) + Some(Event::Window(window::Event::FileHovered(path.clone()))) } WindowEvent::DroppedFile(path) => { - Some(Event::Window(id, window::Event::FileDropped(path.clone()))) + Some(Event::Window(window::Event::FileDropped(path.clone()))) } WindowEvent::HoveredFileCancelled => { - Some(Event::Window(id, window::Event::FilesHoveredLeft)) + Some(Event::Window(window::Event::FilesHoveredLeft)) } WindowEvent::Touch(touch) => { Some(Event::Touch(touch_event(touch, scale_factor))) @@ -288,7 +277,7 @@ pub fn window_event( let winit::dpi::LogicalPosition { x, y } = position.to_logical(scale_factor); - Some(Event::Window(id, window::Event::Moved { x, y })) + Some(Event::Window(window::Event::Moved { x, y })) } _ => None, } @@ -323,6 +312,35 @@ pub fn position( y: f64::from(position.y), })) } + window::Position::SpecificWith(to_position) => { + if let Some(monitor) = monitor { + let start = monitor.position(); + + let resolution: winit::dpi::LogicalSize<f32> = + monitor.size().to_logical(monitor.scale_factor()); + + let position = to_position( + size, + Size::new(resolution.width, resolution.height), + ); + + let centered: winit::dpi::PhysicalPosition<i32> = + winit::dpi::LogicalPosition { + x: position.x, + y: position.y, + } + .to_physical(monitor.scale_factor()); + + Some(winit::dpi::Position::Physical( + winit::dpi::PhysicalPosition { + x: start.x + centered.x, + y: start.y + centered.y, + }, + )) + } else { + None + } + } window::Position::Centered => { if let Some(monitor) = monitor { let start = monitor.position(); diff --git a/winit/src/multi_window.rs b/winit/src/multi_window.rs index 673a6f30..8bd8a64d 100644 --- a/winit/src/multi_window.rs +++ b/winit/src/multi_window.rs @@ -16,14 +16,15 @@ use crate::futures::futures::channel::oneshot; use crate::futures::futures::executor; use crate::futures::futures::task; use crate::futures::futures::{Future, StreamExt}; -use crate::futures::{Executor, Runtime, Subscription}; +use crate::futures::subscription::{self, Subscription}; +use crate::futures::{Executor, Runtime}; use crate::graphics; use crate::graphics::{compositor, Compositor}; use crate::multi_window::window_manager::WindowManager; -use crate::runtime::command::{self, Command}; use crate::runtime::multi_window::Program; use crate::runtime::user_interface::{self, UserInterface}; use crate::runtime::Debug; +use crate::runtime::{Action, Task}; use crate::{Clipboard, Error, Proxy, Settings}; pub use crate::application::{default, Appearance, DefaultStyle}; @@ -40,7 +41,7 @@ use std::time::Instant; /// its own window. /// /// An [`Application`] can execute asynchronous actions by returning a -/// [`Command`] in some of its methods. +/// [`Task`] in some of its methods. /// /// When using an [`Application`] with the `debug` feature enabled, a debug view /// can be toggled by pressing `F12`. @@ -56,10 +57,10 @@ where /// /// Here is where you should return the initial state of your app. /// - /// Additionally, you can return a [`Command`] if you need to perform some + /// Additionally, you can return a [`Task`] if you need to perform some /// async action in the background on startup. This is useful if you want to /// load state from a file, perform an initial HTTP request, etc. - fn new(flags: Self::Flags) -> (Self, Command<Self::Message>); + fn new(flags: Self::Flags) -> (Self, Task<Self::Message>); /// Returns the current title of the [`Application`]. /// @@ -126,19 +127,23 @@ where let (proxy, worker) = Proxy::new(event_loop.create_proxy()); - let runtime = { + let mut runtime = { let executor = E::new().map_err(Error::ExecutorCreationFailed)?; executor.spawn(worker); Runtime::new(executor, proxy.clone()) }; - let (application, init_command) = { + let (application, task) = { let flags = settings.flags; runtime.enter(|| A::new(flags)) }; + if let Some(stream) = task.into_stream() { + runtime.run(stream); + } + let id = settings.id; let title = application.title(window::Id::MAIN); @@ -154,7 +159,6 @@ where boot_receiver, event_receiver, control_sender, - init_command, )); let context = task::Context::from_waker(task::noop_waker_ref()); @@ -447,13 +451,12 @@ enum Control { async fn run_instance<A, E, C>( mut application: A, - mut runtime: Runtime<E, Proxy<A::Message>, A::Message>, + mut runtime: Runtime<E, Proxy<A::Message>, Action<A::Message>>, mut proxy: Proxy<A::Message>, mut debug: Debug, mut boot: oneshot::Receiver<Boot<C>>, - mut event_receiver: mpsc::UnboundedReceiver<Event<A::Message>>, + mut event_receiver: mpsc::UnboundedReceiver<Event<Action<A::Message>>>, mut control_sender: mpsc::UnboundedSender<Control>, - init_command: Command<A::Message>, ) where A: Application + 'static, E: Executor + 'static, @@ -474,7 +477,7 @@ async fn run_instance<A, E, C>( let _ = window_manager.insert( window::Id::MAIN, - main_window.clone(), + main_window, &application, &mut compositor, exit_on_close_request, @@ -491,14 +494,11 @@ async fn run_instance<A, E, C>( let mut clipboard = Clipboard::connect(&main_window.raw); let mut events = { vec![( - Some(window::Id::MAIN), - core::Event::Window( - window::Id::MAIN, - window::Event::Opened { - position: main_window.position(), - size: main_window.size(), - }, - ), + window::Id::MAIN, + core::Event::Window(window::Event::Opened { + position: main_window.position(), + size: main_window.size(), + }), )] }; @@ -513,21 +513,13 @@ async fn run_instance<A, E, C>( )]), )); - run_command( - &application, - &mut compositor, - init_command, - &mut runtime, - &mut clipboard, - &mut control_sender, - &mut proxy, - &mut debug, - &mut window_manager, - &mut ui_caches, + runtime.track( + application + .subscription() + .map(Action::Output) + .into_recipes(), ); - runtime.track(application.subscription().into_recipes()); - let mut messages = Vec::new(); let mut user_events = 0; @@ -564,14 +556,11 @@ async fn run_instance<A, E, C>( let _ = ui_caches.insert(id, user_interface::Cache::default()); events.push(( - Some(id), - core::Event::Window( - id, - window::Event::Opened { - position: window.position(), - size: window.size(), - }, - ), + id, + core::Event::Window(window::Event::Opened { + position: window.position(), + size: window.size(), + }), )); } Event::EventLoopAwakened(event) => { @@ -591,19 +580,27 @@ async fn run_instance<A, E, C>( event::MacOS::ReceivedUrl(url), ), ) => { - use crate::core::event; - - events.push(( - None, - event::Event::PlatformSpecific( - event::PlatformSpecific::MacOS( - event::MacOS::ReceivedUrl(url), + runtime.broadcast( + subscription::Event::PlatformSpecific( + subscription::PlatformSpecific::MacOS( + subscription::MacOS::ReceivedUrl(url), ), ), - )); + ); } - event::Event::UserEvent(message) => { - messages.push(message); + event::Event::UserEvent(action) => { + run_action( + action, + &application, + &mut compositor, + &mut messages, + &mut clipboard, + &mut control_sender, + &mut debug, + &mut user_interfaces, + &mut window_manager, + &mut ui_caches, + ); user_events += 1; } event::Event::WindowEvent { @@ -623,7 +620,6 @@ async fn run_instance<A, E, C>( // Then, we can use the `interface_state` here to decide if a redraw // is needed right away, or simply wait until a specific time. let redraw_event = core::Event::Window( - id, window::Event::RedrawRequested(Instant::now()), ); @@ -662,10 +658,11 @@ async fn run_instance<A, E, C>( window.mouse_interaction = new_mouse_interaction; } - runtime.broadcast( - redraw_event.clone(), - core::event::Status::Ignored, - ); + runtime.broadcast(subscription::Event::Interaction { + window: id, + event: redraw_event, + status: core::event::Status::Ignored, + }); let _ = control_sender.start_send(Control::ChangeFlow( match ui_state { @@ -801,8 +798,8 @@ async fn run_instance<A, E, C>( let _ = ui_caches.remove(&id); events.push(( - None, - core::Event::Window(id, window::Event::Closed), + id, + core::Event::Window(window::Event::Closed), )); if window_manager.is_empty() { @@ -816,12 +813,11 @@ async fn run_instance<A, E, C>( ); if let Some(event) = conversion::window_event( - id, window_event, window.state.scale_factor(), window.state.modifiers(), ) { - events.push((Some(id), event)); + events.push((id, event)); } } } @@ -837,8 +833,7 @@ async fn run_instance<A, E, C>( let mut window_events = vec![]; events.retain(|(window_id, event)| { - if *window_id == Some(id) || window_id.is_none() - { + if *window_id == id { window_events.push(event.clone()); false } else { @@ -874,15 +869,31 @@ async fn run_instance<A, E, C>( .into_iter() .zip(statuses.into_iter()) { - runtime.broadcast(event, status); + runtime.broadcast( + subscription::Event::Interaction { + window: id, + event, + status, + }, + ); } } + for (id, event) in events.drain(..) { + runtime.broadcast( + subscription::Event::Interaction { + window: id, + event, + status: core::event::Status::Ignored, + }, + ); + } + debug.event_processing_finished(); // TODO mw application update returns which window IDs to update if !messages.is_empty() || uis_stale { - let mut cached_interfaces: FxHashMap< + let cached_interfaces: FxHashMap< window::Id, user_interface::Cache, > = ManuallyDrop::into_inner(user_interfaces) @@ -893,15 +904,9 @@ async fn run_instance<A, E, C>( // Update application update( &mut application, - &mut compositor, &mut runtime, - &mut clipboard, - &mut control_sender, - &mut proxy, &mut debug, &mut messages, - &mut window_manager, - &mut cached_interfaces, ); // we must synchronize all window states with application state after an @@ -965,63 +970,46 @@ where user_interface } -/// Updates a multi-window [`Application`] by feeding it messages, spawning any -/// resulting [`Command`], and tracking its [`Subscription`]. -fn update<A: Application, C, E: Executor>( +fn update<A: Application, E: Executor>( application: &mut A, - compositor: &mut C, - runtime: &mut Runtime<E, Proxy<A::Message>, A::Message>, - clipboard: &mut Clipboard, - control_sender: &mut mpsc::UnboundedSender<Control>, - proxy: &mut Proxy<A::Message>, + runtime: &mut Runtime<E, Proxy<A::Message>, Action<A::Message>>, debug: &mut Debug, messages: &mut Vec<A::Message>, - window_manager: &mut WindowManager<A, C>, - ui_caches: &mut FxHashMap<window::Id, user_interface::Cache>, ) where - C: Compositor<Renderer = A::Renderer> + 'static, A::Theme: DefaultStyle, { for message in messages.drain(..) { debug.log_message(&message); debug.update_started(); - let command = runtime.enter(|| application.update(message)); + let task = runtime.enter(|| application.update(message)); debug.update_finished(); - run_command( - application, - compositor, - command, - runtime, - clipboard, - control_sender, - proxy, - debug, - window_manager, - ui_caches, - ); + if let Some(stream) = task.into_stream() { + runtime.run(stream); + } } let subscription = application.subscription(); - runtime.track(subscription.into_recipes()); + runtime.track(subscription.map(Action::Output).into_recipes()); } -/// Runs the actions of a [`Command`]. -fn run_command<A, C, E>( +fn run_action<A, C>( + action: Action<A::Message>, application: &A, compositor: &mut C, - command: Command<A::Message>, - runtime: &mut Runtime<E, Proxy<A::Message>, A::Message>, + messages: &mut Vec<A::Message>, clipboard: &mut Clipboard, control_sender: &mut mpsc::UnboundedSender<Control>, - proxy: &mut Proxy<A::Message>, debug: &mut Debug, + interfaces: &mut FxHashMap< + window::Id, + UserInterface<'_, A::Message, A::Theme, A::Renderer>, + >, window_manager: &mut WindowManager<A, C>, ui_caches: &mut FxHashMap<window::Id, user_interface::Cache>, ) where A: Application, - E: Executor, C: Compositor<Renderer = A::Renderer> + 'static, A::Theme: DefaultStyle, { @@ -1029,283 +1017,254 @@ fn run_command<A, C, E>( use crate::runtime::system; use crate::runtime::window; - for action in command.actions() { - match action { - command::Action::Future(future) => { - runtime.spawn(Box::pin(future)); + match action { + Action::Output(message) => { + messages.push(message); + } + Action::Clipboard(action) => match action { + clipboard::Action::Read { target, channel } => { + let _ = channel.send(clipboard.read(target)); } - command::Action::Stream(stream) => { - runtime.run(Box::pin(stream)); + clipboard::Action::Write { target, contents } => { + clipboard.write(target, contents); } - command::Action::Clipboard(action) => match action { - clipboard::Action::Read(tag, kind) => { - let message = tag(clipboard.read(kind)); + }, + Action::Window(action) => match action { + window::Action::Open(id, settings, channel) => { + let monitor = window_manager.last_monitor(); - proxy.send(message); - } - clipboard::Action::Write(contents, kind) => { - clipboard.write(kind, contents); - } - }, - command::Action::Window(action) => match action { - window::Action::Spawn(id, settings) => { - let monitor = window_manager.last_monitor(); + control_sender + .start_send(Control::CreateWindow { + id, + settings, + title: application.title(id), + monitor, + }) + .expect("Send control action"); + let _ = channel.send(id); + } + window::Action::Close(id) => { + let _ = window_manager.remove(id); + let _ = ui_caches.remove(&id); + + if window_manager.is_empty() { control_sender - .start_send(Control::CreateWindow { - id, - settings, - title: application.title(id), - monitor, - }) + .start_send(Control::Exit) .expect("Send control action"); } - window::Action::Close(id) => { - let _ = window_manager.remove(id); - let _ = ui_caches.remove(&id); - - if window_manager.is_empty() { - control_sender - .start_send(Control::Exit) - .expect("Send control action"); - } - } - window::Action::Drag(id) => { - if let Some(window) = window_manager.get_mut(id) { - let _ = window.raw.drag_window(); - } + } + window::Action::Drag(id) => { + if let Some(window) = window_manager.get_mut(id) { + let _ = window.raw.drag_window(); } - window::Action::Resize(id, size) => { - if let Some(window) = window_manager.get_mut(id) { - let _ = window.raw.request_inner_size( - winit::dpi::LogicalSize { - width: size.width, - height: size.height, - }, - ); - } + } + window::Action::Resize(id, size) => { + if let Some(window) = window_manager.get_mut(id) { + let _ = window.raw.request_inner_size( + winit::dpi::LogicalSize { + width: size.width, + height: size.height, + }, + ); } - window::Action::FetchSize(id, callback) => { - if let Some(window) = window_manager.get_mut(id) { - let size = window - .raw - .inner_size() - .to_logical(window.raw.scale_factor()); - - proxy - .send(callback(Size::new(size.width, size.height))); - } + } + window::Action::FetchSize(id, channel) => { + if let Some(window) = window_manager.get_mut(id) { + let size = window + .raw + .inner_size() + .to_logical(window.raw.scale_factor()); + + let _ = channel.send(Size::new(size.width, size.height)); } - window::Action::FetchMaximized(id, callback) => { - if let Some(window) = window_manager.get_mut(id) { - proxy.send(callback(window.raw.is_maximized())); - } + } + window::Action::FetchMaximized(id, channel) => { + if let Some(window) = window_manager.get_mut(id) { + let _ = channel.send(window.raw.is_maximized()); } - window::Action::Maximize(id, maximized) => { - if let Some(window) = window_manager.get_mut(id) { - window.raw.set_maximized(maximized); - } + } + window::Action::Maximize(id, maximized) => { + if let Some(window) = window_manager.get_mut(id) { + window.raw.set_maximized(maximized); } - window::Action::FetchMinimized(id, callback) => { - if let Some(window) = window_manager.get_mut(id) { - proxy.send(callback(window.raw.is_minimized())); - } + } + window::Action::FetchMinimized(id, channel) => { + if let Some(window) = window_manager.get_mut(id) { + let _ = channel.send(window.raw.is_minimized()); } - window::Action::Minimize(id, minimized) => { - if let Some(window) = window_manager.get_mut(id) { - window.raw.set_minimized(minimized); - } + } + window::Action::Minimize(id, minimized) => { + if let Some(window) = window_manager.get_mut(id) { + window.raw.set_minimized(minimized); } - window::Action::FetchPosition(id, callback) => { - if let Some(window) = window_manager.get_mut(id) { - let position = window - .raw - .inner_position() - .map(|position| { - let position = position.to_logical::<f32>( - window.raw.scale_factor(), - ); - - Point::new(position.x, position.y) - }) - .ok(); + } + window::Action::FetchPosition(id, channel) => { + if let Some(window) = window_manager.get_mut(id) { + let position = window + .raw + .inner_position() + .map(|position| { + let position = position + .to_logical::<f32>(window.raw.scale_factor()); + + Point::new(position.x, position.y) + }) + .ok(); - proxy.send(callback(position)); - } + let _ = channel.send(position); } - window::Action::Move(id, position) => { - if let Some(window) = window_manager.get_mut(id) { - window.raw.set_outer_position( - winit::dpi::LogicalPosition { - x: position.x, - y: position.y, - }, - ); - } + } + window::Action::Move(id, position) => { + if let Some(window) = window_manager.get_mut(id) { + window.raw.set_outer_position( + winit::dpi::LogicalPosition { + x: position.x, + y: position.y, + }, + ); } - window::Action::ChangeMode(id, mode) => { - if let Some(window) = window_manager.get_mut(id) { - window.raw.set_visible(conversion::visible(mode)); - window.raw.set_fullscreen(conversion::fullscreen( - window.raw.current_monitor(), - mode, - )); - } + } + window::Action::ChangeMode(id, mode) => { + if let Some(window) = window_manager.get_mut(id) { + window.raw.set_visible(conversion::visible(mode)); + window.raw.set_fullscreen(conversion::fullscreen( + window.raw.current_monitor(), + mode, + )); } - window::Action::ChangeIcon(id, icon) => { - if let Some(window) = window_manager.get_mut(id) { - window.raw.set_window_icon(conversion::icon(icon)); - } + } + window::Action::ChangeIcon(id, icon) => { + if let Some(window) = window_manager.get_mut(id) { + window.raw.set_window_icon(conversion::icon(icon)); } - window::Action::FetchMode(id, tag) => { - if let Some(window) = window_manager.get_mut(id) { - let mode = if window.raw.is_visible().unwrap_or(true) { - conversion::mode(window.raw.fullscreen()) - } else { - core::window::Mode::Hidden - }; - - proxy.send(tag(mode)); - } + } + window::Action::FetchMode(id, channel) => { + if let Some(window) = window_manager.get_mut(id) { + let mode = if window.raw.is_visible().unwrap_or(true) { + conversion::mode(window.raw.fullscreen()) + } else { + core::window::Mode::Hidden + }; + + let _ = channel.send(mode); } - window::Action::ToggleMaximize(id) => { - if let Some(window) = window_manager.get_mut(id) { - window.raw.set_maximized(!window.raw.is_maximized()); - } + } + window::Action::ToggleMaximize(id) => { + if let Some(window) = window_manager.get_mut(id) { + window.raw.set_maximized(!window.raw.is_maximized()); } - window::Action::ToggleDecorations(id) => { - if let Some(window) = window_manager.get_mut(id) { - window.raw.set_decorations(!window.raw.is_decorated()); - } + } + window::Action::ToggleDecorations(id) => { + if let Some(window) = window_manager.get_mut(id) { + window.raw.set_decorations(!window.raw.is_decorated()); } - window::Action::RequestUserAttention(id, attention_type) => { - if let Some(window) = window_manager.get_mut(id) { - window.raw.request_user_attention( - attention_type.map(conversion::user_attention), - ); - } + } + window::Action::RequestUserAttention(id, attention_type) => { + if let Some(window) = window_manager.get_mut(id) { + window.raw.request_user_attention( + attention_type.map(conversion::user_attention), + ); } - window::Action::GainFocus(id) => { - if let Some(window) = window_manager.get_mut(id) { - window.raw.focus_window(); - } + } + window::Action::GainFocus(id) => { + if let Some(window) = window_manager.get_mut(id) { + window.raw.focus_window(); } - window::Action::ChangeLevel(id, level) => { - if let Some(window) = window_manager.get_mut(id) { - window - .raw - .set_window_level(conversion::window_level(level)); - } + } + window::Action::ChangeLevel(id, level) => { + if let Some(window) = window_manager.get_mut(id) { + window + .raw + .set_window_level(conversion::window_level(level)); } - window::Action::ShowSystemMenu(id) => { - if let Some(window) = window_manager.get_mut(id) { - if let mouse::Cursor::Available(point) = - window.state.cursor() - { - window.raw.show_window_menu( - winit::dpi::LogicalPosition { - x: point.x, - y: point.y, - }, - ); - } + } + window::Action::ShowSystemMenu(id) => { + if let Some(window) = window_manager.get_mut(id) { + if let mouse::Cursor::Available(point) = + window.state.cursor() + { + window.raw.show_window_menu( + winit::dpi::LogicalPosition { + x: point.x, + y: point.y, + }, + ); } } - window::Action::FetchId(id, tag) => { - if let Some(window) = window_manager.get_mut(id) { - proxy.send(tag(window.raw.id().into())); - } + } + window::Action::FetchRawId(id, channel) => { + if let Some(window) = window_manager.get_mut(id) { + let _ = channel.send(window.raw.id().into()); } - window::Action::RunWithHandle(id, tag) => { - use window::raw_window_handle::HasWindowHandle; + } + window::Action::RunWithHandle(id, f) => { + use window::raw_window_handle::HasWindowHandle; - if let Some(handle) = window_manager - .get_mut(id) - .and_then(|window| window.raw.window_handle().ok()) - { - proxy.send(tag(&handle)); - } + if let Some(handle) = window_manager + .get_mut(id) + .and_then(|window| window.raw.window_handle().ok()) + { + f(handle); } - window::Action::Screenshot(id, tag) => { - if let Some(window) = window_manager.get_mut(id) { - let bytes = compositor.screenshot( - &mut window.renderer, - &mut window.surface, - window.state.viewport(), - window.state.background_color(), - &debug.overlay(), - ); + } + window::Action::Screenshot(id, channel) => { + if let Some(window) = window_manager.get_mut(id) { + let bytes = compositor.screenshot( + &mut window.renderer, + &mut window.surface, + window.state.viewport(), + window.state.background_color(), + &debug.overlay(), + ); - proxy.send(tag(window::Screenshot::new( - bytes, - window.state.physical_size(), - ))); - } + let _ = channel.send(window::Screenshot::new( + bytes, + window.state.physical_size(), + window.state.viewport().scale_factor(), + )); } - }, - command::Action::System(action) => match action { - system::Action::QueryInformation(_tag) => { - #[cfg(feature = "system")] - { - let graphics_info = compositor.fetch_information(); - let mut proxy = proxy.clone(); - - let _ = std::thread::spawn(move || { - let information = - crate::system::information(graphics_info); + } + }, + Action::System(action) => match action { + system::Action::QueryInformation(_channel) => { + #[cfg(feature = "system")] + { + let graphics_info = compositor.fetch_information(); - let message = _tag(information); + let _ = std::thread::spawn(move || { + let information = + crate::system::information(graphics_info); - proxy.send(message); - }); + let _ = _channel.send(information); + }); + } + } + }, + Action::Widget(operation) => { + let mut current_operation = Some(operation); + + while let Some(mut operation) = current_operation.take() { + for (id, ui) in interfaces.iter_mut() { + if let Some(window) = window_manager.get_mut(*id) { + ui.operate(&window.renderer, operation.as_mut()); } } - }, - command::Action::Widget(action) => { - let mut current_operation = Some(action); - - let mut uis = build_user_interfaces( - application, - debug, - window_manager, - std::mem::take(ui_caches), - ); - - 'operate: while let Some(mut operation) = - current_operation.take() - { - for (id, ui) in uis.iter_mut() { - if let Some(window) = window_manager.get_mut(*id) { - ui.operate(&window.renderer, operation.as_mut()); - match operation.finish() { - operation::Outcome::None => {} - operation::Outcome::Some(message) => { - proxy.send(message); - - // operation completed, don't need to try to operate on rest of UIs - break 'operate; - } - operation::Outcome::Chain(next) => { - current_operation = Some(next); - } - } - } + match operation.finish() { + operation::Outcome::None => {} + operation::Outcome::Some(()) => {} + operation::Outcome::Chain(next) => { + current_operation = Some(next); } } - - *ui_caches = - uis.drain().map(|(id, ui)| (id, ui.into_cache())).collect(); } - command::Action::LoadFont { bytes, tagger } => { - // TODO: Error handling (?) - compositor.load_font(bytes.clone()); + } + Action::LoadFont { bytes, channel } => { + // TODO: Error handling (?) + compositor.load_font(bytes.clone()); - proxy.send(tagger(Ok(()))); - } - command::Action::Custom(_) => { - log::warn!("Unsupported custom action in `iced_winit` shell"); - } + let _ = channel.send(Ok(())); } } } diff --git a/winit/src/proxy.rs b/winit/src/proxy.rs index 3edc30ad..0ab61375 100644 --- a/winit/src/proxy.rs +++ b/winit/src/proxy.rs @@ -4,17 +4,18 @@ use crate::futures::futures::{ task::{Context, Poll}, Future, Sink, StreamExt, }; +use crate::runtime::Action; use std::pin::Pin; /// An event loop proxy with backpressure that implements `Sink`. #[derive(Debug)] -pub struct Proxy<Message: 'static> { - raw: winit::event_loop::EventLoopProxy<Message>, - sender: mpsc::Sender<Message>, +pub struct Proxy<T: 'static> { + raw: winit::event_loop::EventLoopProxy<Action<T>>, + sender: mpsc::Sender<Action<T>>, notifier: mpsc::Sender<usize>, } -impl<Message: 'static> Clone for Proxy<Message> { +impl<T: 'static> Clone for Proxy<T> { fn clone(&self) -> Self { Self { raw: self.raw.clone(), @@ -24,12 +25,12 @@ impl<Message: 'static> Clone for Proxy<Message> { } } -impl<Message: 'static> Proxy<Message> { +impl<T: 'static> Proxy<T> { const MAX_SIZE: usize = 100; /// Creates a new [`Proxy`] from an `EventLoopProxy`. pub fn new( - raw: winit::event_loop::EventLoopProxy<Message>, + raw: winit::event_loop::EventLoopProxy<Action<T>>, ) -> (Self, impl Future<Output = ()>) { let (notifier, mut processed) = mpsc::channel(Self::MAX_SIZE); let (sender, mut receiver) = mpsc::channel(Self::MAX_SIZE); @@ -72,16 +73,16 @@ impl<Message: 'static> Proxy<Message> { ) } - /// Sends a `Message` to the event loop. + /// Sends a value to the event loop. /// /// Note: This skips the backpressure mechanism with an unbounded /// channel. Use sparingly! - pub fn send(&mut self, message: Message) + pub fn send(&mut self, value: T) where - Message: std::fmt::Debug, + T: std::fmt::Debug, { self.raw - .send_event(message) + .send_event(Action::Output(value)) .expect("Send message to event loop"); } @@ -92,7 +93,7 @@ impl<Message: 'static> Proxy<Message> { } } -impl<Message: 'static> Sink<Message> for Proxy<Message> { +impl<T: 'static> Sink<Action<T>> for Proxy<T> { type Error = mpsc::SendError; fn poll_ready( @@ -104,9 +105,9 @@ impl<Message: 'static> Sink<Message> for Proxy<Message> { fn start_send( mut self: Pin<&mut Self>, - message: Message, + action: Action<T>, ) -> Result<(), Self::Error> { - self.sender.start_send(message) + self.sender.start_send(action) } fn poll_flush( diff --git a/winit/src/system.rs b/winit/src/system.rs index c5a5b219..7997f311 100644 --- a/winit/src/system.rs +++ b/winit/src/system.rs @@ -1,15 +1,13 @@ //! Access the native system. use crate::graphics::compositor; -use crate::runtime::command::{self, Command}; use crate::runtime::system::{Action, Information}; +use crate::runtime::{self, Task}; /// Query for available system information. -pub fn fetch_information<Message>( - f: impl Fn(Information) -> Message + Send + 'static, -) -> Command<Message> { - Command::single(command::Action::System(Action::QueryInformation( - Box::new(f), - ))) +pub fn fetch_information() -> Task<Information> { + Task::oneshot(|channel| { + runtime::Action::System(Action::QueryInformation(channel)) + }) } pub(crate) fn information( |