diff options
Diffstat (limited to '')
-rw-r--r-- | runtime/Cargo.toml (renamed from native/Cargo.toml) | 14 | ||||
-rw-r--r-- | runtime/README.md | 18 | ||||
-rw-r--r-- | runtime/src/clipboard.rs (renamed from native/src/clipboard.rs) | 37 | ||||
-rw-r--r-- | runtime/src/command.rs (renamed from native/src/command.rs) | 55 | ||||
-rw-r--r-- | runtime/src/command/action.rs (renamed from native/src/command/action.rs) | 24 | ||||
-rw-r--r-- | runtime/src/debug/basic.rs (renamed from native/src/debug/basic.rs) | 2 | ||||
-rw-r--r-- | runtime/src/debug/null.rs (renamed from native/src/debug/null.rs) | 0 | ||||
-rw-r--r-- | runtime/src/font.rs | 19 | ||||
-rw-r--r-- | runtime/src/keyboard.rs (renamed from native/src/keyboard.rs) | 0 | ||||
-rw-r--r-- | runtime/src/lib.rs (renamed from native/src/lib.rs) | 50 | ||||
-rw-r--r-- | runtime/src/overlay.rs | 4 | ||||
-rw-r--r-- | runtime/src/overlay/nested.rs | 353 | ||||
-rw-r--r-- | runtime/src/program.rs (renamed from native/src/program.rs) | 7 | ||||
-rw-r--r-- | runtime/src/program/state.rs (renamed from native/src/program/state.rs) | 64 | ||||
-rw-r--r-- | runtime/src/system.rs (renamed from native/src/system.rs) | 0 | ||||
-rw-r--r-- | runtime/src/system/action.rs (renamed from native/src/system/action.rs) | 0 | ||||
-rw-r--r-- | runtime/src/system/information.rs (renamed from native/src/system/information.rs) | 0 | ||||
-rw-r--r-- | runtime/src/user_interface.rs (renamed from native/src/user_interface.rs) | 224 | ||||
-rw-r--r-- | runtime/src/window.rs | 135 | ||||
-rw-r--r-- | runtime/src/window/action.rs (renamed from native/src/window/action.rs) | 73 | ||||
-rw-r--r-- | runtime/src/window/screenshot.rs | 92 |
21 files changed, 930 insertions, 241 deletions
diff --git a/native/Cargo.toml b/runtime/Cargo.toml index 3f92783e..a65f07f2 100644 --- a/native/Cargo.toml +++ b/runtime/Cargo.toml @@ -1,6 +1,6 @@ [package] -name = "iced_native" -version = "0.9.1" +name = "iced_runtime" +version = "0.1.0" authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"] edition = "2021" description = "A renderer-agnostic library for native GUIs" @@ -11,19 +11,13 @@ repository = "https://github.com/iced-rs/iced" debug = [] [dependencies] -twox-hash = { version = "1.5", default-features = false } -unicode-segmentation = "1.6" -num-traits = "0.2" +thiserror = "1" [dependencies.iced_core] -version = "0.8" +version = "0.9" path = "../core" [dependencies.iced_futures] version = "0.6" path = "../futures" features = ["thread-pool"] - -[dependencies.iced_style] -version = "0.7" -path = "../style" diff --git a/runtime/README.md b/runtime/README.md new file mode 100644 index 00000000..1b0fa857 --- /dev/null +++ b/runtime/README.md @@ -0,0 +1,18 @@ +# `iced_runtime` +[][documentation] +[](https://crates.io/crates/iced_native) +[](https://github.com/iced-rs/iced/blob/master/LICENSE) +[](https://discord.gg/3xZJ65GAhd) + +`iced_runtime` takes [`iced_core`] and builds a native runtime on top of it. + +[documentation]: https://docs.rs/iced_native +[`iced_core`]: ../core +[`iced_winit`]: ../winit +[`druid`]: https://github.com/xi-editor/druid +[`raw-window-handle`]: https://github.com/rust-windowing/raw-window-handle + +__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/native/src/clipboard.rs b/runtime/src/clipboard.rs index c9105bc0..bc450912 100644 --- a/native/src/clipboard.rs +++ b/runtime/src/clipboard.rs @@ -1,30 +1,9 @@ //! Access the clipboard. -use iced_futures::MaybeSend; +use crate::command::{self, Command}; +use crate::futures::MaybeSend; use std::fmt; -/// A buffer for short-term storage and transfer within and between -/// applications. -pub trait Clipboard { - /// Reads the current content of the [`Clipboard`] as text. - fn read(&self) -> Option<String>; - - /// Writes the given text contents to the [`Clipboard`]. - fn write(&mut self, contents: String); -} - -/// A null implementation of the [`Clipboard`] trait. -#[derive(Debug, Clone, Copy)] -pub struct Null; - -impl Clipboard for Null { - fn read(&self) -> Option<String> { - None - } - - fn write(&mut self, _contents: String) {} -} - /// A clipboard action to be performed by some [`Command`]. /// /// [`Command`]: crate::Command @@ -60,3 +39,15 @@ impl<T> fmt::Debug for Action<T> { } } } + +/// 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)))) +} + +/// Write the given contents to the clipboard. +pub fn write<Message>(contents: String) -> Command<Message> { + Command::single(command::Action::Clipboard(Action::Write(contents))) +} diff --git a/native/src/command.rs b/runtime/src/command.rs index ca9d0b64..cd4c51ff 100644 --- a/native/src/command.rs +++ b/runtime/src/command.rs @@ -3,35 +3,39 @@ mod action; pub use action::Action; -use crate::widget; - -use iced_futures::MaybeSend; +use crate::core::widget; +use crate::futures::MaybeSend; 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>(iced_futures::Command<Action<T>>); +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(iced_futures::Command::none()) + Self(Internal::None) } /// Creates a [`Command`] that performs a single [`Action`]. pub const fn single(action: Action<T>) -> Self { - Self(iced_futures::Command::single(action)) + Self(Internal::Single(action)) } /// Creates a [`Command`] that performs a [`widget::Operation`]. pub fn widget(operation: impl widget::Operation<T> + 'static) -> Self { - Self(iced_futures::Command::single(Action::Widget( - widget::Action::new(operation), - ))) + Self::single(Action::Widget(Box::new(operation))) } /// Creates a [`Command`] that performs the action of the given future. @@ -49,9 +53,17 @@ impl<T> Command<T> { /// /// Once this command is run, all the commands will be executed at once. pub fn batch(commands: impl IntoIterator<Item = Command<T>>) -> Self { - Self(iced_futures::Command::batch( - commands.into_iter().map(|Command(command)| command), - )) + 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`]. @@ -63,16 +75,27 @@ impl<T> Command<T> { T: 'static, A: 'static, { - let Command(command) = self; - - Command(command.map(move |action| action.map(f.clone()))) + 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; - command.actions() + match command { + Internal::None => Vec::new(), + Internal::Single(action) => vec![action], + Internal::Batch(batch) => batch, + } } } diff --git a/native/src/command/action.rs b/runtime/src/command/action.rs index dcb79672..b2594379 100644 --- a/native/src/command/action.rs +++ b/runtime/src/command/action.rs @@ -1,10 +1,12 @@ use crate::clipboard; +use crate::core::widget; +use crate::font; use crate::system; -use crate::widget; use crate::window; use iced_futures::MaybeSend; +use std::borrow::Cow; use std::fmt; /// An action that a [`Command`] can perform. @@ -26,7 +28,16 @@ pub enum Action<T> { System(system::Action<T>), /// Run a widget action. - Widget(widget::Action<T>), + 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>, + }, } impl<T> Action<T> { @@ -48,7 +59,13 @@ impl<T> Action<T> { Self::Clipboard(action) => Action::Clipboard(action.map(f)), Self::Window(id, window) => Action::Window(id, window.map(f)), Self::System(system) => Action::System(system.map(f)), - Self::Widget(widget) => Action::Widget(widget.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))), + }, } } } @@ -65,6 +82,7 @@ impl<T> fmt::Debug for Action<T> { } Self::System(action) => write!(f, "Action::System({action:?})"), Self::Widget(_action) => write!(f, "Action::Widget"), + Self::LoadFont { .. } => write!(f, "Action::LoadFont"), } } } diff --git a/native/src/debug/basic.rs b/runtime/src/debug/basic.rs index 92f614da..32f725a1 100644 --- a/native/src/debug/basic.rs +++ b/runtime/src/debug/basic.rs @@ -1,5 +1,5 @@ #![allow(missing_docs)] -use crate::time; +use crate::core::time; use std::collections::VecDeque; diff --git a/native/src/debug/null.rs b/runtime/src/debug/null.rs index 2db0eebb..2db0eebb 100644 --- a/native/src/debug/null.rs +++ b/runtime/src/debug/null.rs diff --git a/runtime/src/font.rs b/runtime/src/font.rs new file mode 100644 index 00000000..15359694 --- /dev/null +++ b/runtime/src/font.rs @@ -0,0 +1,19 @@ +//! Load and use fonts. +pub use iced_core::font::*; + +use crate::command::{self, Command}; +use std::borrow::Cow; + +/// An error while loading a font. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +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 { + bytes: bytes.into(), + tagger: Box::new(std::convert::identity), + }) +} diff --git a/native/src/keyboard.rs b/runtime/src/keyboard.rs index 012538e3..012538e3 100644 --- a/native/src/keyboard.rs +++ b/runtime/src/keyboard.rs diff --git a/native/src/lib.rs b/runtime/src/lib.rs index ebdc8490..4bbf9687 100644 --- a/native/src/lib.rs +++ b/runtime/src/lib.rs @@ -23,8 +23,8 @@ //! - Build a new renderer, see the [renderer] module. //! - Build a custom widget, start at the [`Widget`] trait. //! -//! [`iced_core`]: https://github.com/iced-rs/iced/tree/0.8/core -//! [`iced_winit`]: https://github.com/iced-rs/iced/tree/0.8/winit +//! [`iced_core`]: https://github.com/iced-rs/iced/tree/0.9/core +//! [`iced_winit`]: https://github.com/iced-rs/iced/tree/0.9/winit //! [`druid`]: https://github.com/xi-editor/druid //! [`raw-window-handle`]: https://github.com/rust-windowing/raw-window-handle //! [renderer]: crate::renderer @@ -42,32 +42,17 @@ clippy::useless_conversion )] #![forbid(unsafe_code, rust_2018_idioms)] -#![allow(clippy::inherent_to_string, clippy::type_complexity)] -#![cfg_attr(docsrs, feature(doc_cfg))] +#![cfg_attr(docsrs, feature(doc_auto_cfg))] pub mod clipboard; pub mod command; -pub mod event; -pub mod image; +pub mod font; pub mod keyboard; -pub mod layout; -pub mod mouse; pub mod overlay; pub mod program; -pub mod renderer; -pub mod subscription; -pub mod svg; pub mod system; -pub mod text; -pub mod touch; pub mod user_interface; -pub mod widget; pub mod window; -mod element; -mod hasher; -mod runtime; -mod shell; - // We disable debug capabilities on release builds unless the `debug` feature // is explicitly enabled. #[cfg(feature = "debug")] @@ -77,32 +62,11 @@ mod debug; #[path = "debug/null.rs"] mod debug; -pub use iced_core::alignment; -pub use iced_core::time; -pub use iced_core::{ - color, Alignment, Background, Color, ContentFit, Font, Length, Padding, - Pixels, Point, Rectangle, Size, Vector, -}; -pub use iced_futures::{executor, futures}; -pub use iced_style::application; -pub use iced_style::theme; - -#[doc(no_inline)] -pub use executor::Executor; +pub use iced_core as core; +pub use iced_futures as futures; -pub use clipboard::Clipboard; pub use command::Command; pub use debug::Debug; -pub use element::Element; -pub use event::Event; -pub use hasher::Hasher; -pub use layout::Layout; -pub use overlay::Overlay; +pub use font::Font; pub use program::Program; -pub use renderer::Renderer; -pub use runtime::Runtime; -pub use shell::Shell; -pub use subscription::Subscription; -pub use theme::Theme; pub use user_interface::UserInterface; -pub use widget::Widget; diff --git a/runtime/src/overlay.rs b/runtime/src/overlay.rs new file mode 100644 index 00000000..03390980 --- /dev/null +++ b/runtime/src/overlay.rs @@ -0,0 +1,4 @@ +//! Overlays for user interfaces. +mod nested; + +pub use nested::Nested; diff --git a/runtime/src/overlay/nested.rs b/runtime/src/overlay/nested.rs new file mode 100644 index 00000000..b729f769 --- /dev/null +++ b/runtime/src/overlay/nested.rs @@ -0,0 +1,353 @@ +use crate::core::event; +use crate::core::layout; +use crate::core::mouse; +use crate::core::overlay; +use crate::core::renderer; +use crate::core::widget; +use crate::core::{Clipboard, Event, Layout, Point, Rectangle, Shell, Size}; + +/// An [`Overlay`] container that displays nested overlays +#[allow(missing_debug_implementations)] +pub struct Nested<'a, Message, Renderer> { + overlay: overlay::Element<'a, Message, Renderer>, +} + +impl<'a, Message, Renderer> Nested<'a, Message, Renderer> +where + Renderer: renderer::Renderer, +{ + /// Creates a nested overlay from the provided [`overlay::Element`] + pub fn new(element: overlay::Element<'a, Message, Renderer>) -> Self { + Self { overlay: element } + } + + /// Returns the position of the [`Nested`] overlay. + pub fn position(&self) -> Point { + self.overlay.position() + } + + /// Returns the layout [`Node`] of the [`Nested`] overlay. + pub fn layout( + &mut self, + renderer: &Renderer, + bounds: Size, + position: Point, + ) -> layout::Node { + fn recurse<Message, Renderer>( + element: &mut overlay::Element<'_, Message, Renderer>, + renderer: &Renderer, + bounds: Size, + position: Point, + ) -> layout::Node + where + Renderer: renderer::Renderer, + { + let translation = position - Point::ORIGIN; + + let node = element.layout(renderer, bounds, translation); + + if let Some(mut nested) = + element.overlay(Layout::new(&node), renderer) + { + layout::Node::with_children( + node.size(), + vec![ + node, + recurse(&mut nested, renderer, bounds, position), + ], + ) + } else { + layout::Node::with_children(node.size(), vec![node]) + } + } + + recurse(&mut self.overlay, renderer, bounds, position) + } + + /// Draws the [`Nested`] overlay using the associated `Renderer`. + pub fn draw( + &mut self, + renderer: &mut Renderer, + theme: &<Renderer as renderer::Renderer>::Theme, + style: &renderer::Style, + layout: Layout<'_>, + cursor: mouse::Cursor, + ) { + fn recurse<Message, Renderer>( + element: &mut overlay::Element<'_, Message, Renderer>, + layout: Layout<'_>, + renderer: &mut Renderer, + theme: &<Renderer as renderer::Renderer>::Theme, + style: &renderer::Style, + cursor: mouse::Cursor, + ) where + Renderer: renderer::Renderer, + { + let mut layouts = layout.children(); + + if let Some(layout) = layouts.next() { + let nested_layout = layouts.next(); + + let is_over = cursor + .position() + .zip(nested_layout) + .and_then(|(cursor_position, nested_layout)| { + element.overlay(layout, renderer).map(|nested| { + nested.is_over( + nested_layout.children().next().unwrap(), + renderer, + cursor_position, + ) + }) + }) + .unwrap_or_default(); + + renderer.with_layer(layout.bounds(), |renderer| { + element.draw( + renderer, + theme, + style, + layout, + if is_over { + mouse::Cursor::Unavailable + } else { + cursor + }, + ); + }); + + if let Some((mut nested, nested_layout)) = + element.overlay(layout, renderer).zip(nested_layout) + { + recurse( + &mut nested, + nested_layout, + renderer, + theme, + style, + cursor, + ); + } + } + } + + recurse(&mut self.overlay, layout, renderer, theme, style, cursor); + } + + /// Applies a [`widget::Operation`] to the [`Nested`] overlay. + pub fn operate( + &mut self, + layout: Layout<'_>, + renderer: &Renderer, + operation: &mut dyn widget::Operation<Message>, + ) { + fn recurse<Message, Renderer>( + element: &mut overlay::Element<'_, Message, Renderer>, + layout: Layout<'_>, + renderer: &Renderer, + operation: &mut dyn widget::Operation<Message>, + ) where + Renderer: renderer::Renderer, + { + let mut layouts = layout.children(); + + if let Some(layout) = layouts.next() { + element.operate(layout, renderer, operation); + + if let Some((mut nested, nested_layout)) = + element.overlay(layout, renderer).zip(layouts.next()) + { + recurse(&mut nested, nested_layout, renderer, operation); + } + } + } + + recurse(&mut self.overlay, layout, renderer, operation) + } + + /// Processes a runtime [`Event`]. + pub fn on_event( + &mut self, + event: Event, + layout: Layout<'_>, + cursor: mouse::Cursor, + renderer: &Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + ) -> event::Status { + fn recurse<Message, Renderer>( + element: &mut overlay::Element<'_, Message, Renderer>, + layout: Layout<'_>, + event: Event, + cursor: mouse::Cursor, + renderer: &Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + ) -> (event::Status, bool) + where + Renderer: renderer::Renderer, + { + let mut layouts = layout.children(); + + if let Some(layout) = layouts.next() { + let (nested_status, nested_is_over) = + if let Some((mut nested, nested_layout)) = + element.overlay(layout, renderer).zip(layouts.next()) + { + recurse( + &mut nested, + nested_layout, + event.clone(), + cursor, + renderer, + clipboard, + shell, + ) + } else { + (event::Status::Ignored, false) + }; + + if matches!(nested_status, event::Status::Ignored) { + let is_over = nested_is_over + || cursor + .position() + .map(|cursor_position| { + element.is_over( + layout, + renderer, + cursor_position, + ) + }) + .unwrap_or_default(); + + ( + element.on_event( + event, + layout, + if nested_is_over { + mouse::Cursor::Unavailable + } else { + cursor + }, + renderer, + clipboard, + shell, + ), + is_over, + ) + } else { + (nested_status, nested_is_over) + } + } else { + (event::Status::Ignored, false) + } + } + + let (status, _) = recurse( + &mut self.overlay, + layout, + event, + cursor, + renderer, + clipboard, + shell, + ); + + status + } + + /// Returns the current [`mouse::Interaction`] of the [`Nested`] overlay. + pub fn mouse_interaction( + &mut self, + layout: Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + renderer: &Renderer, + ) -> mouse::Interaction { + fn recurse<Message, Renderer>( + element: &mut overlay::Element<'_, Message, Renderer>, + layout: Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + renderer: &Renderer, + ) -> Option<mouse::Interaction> + where + Renderer: renderer::Renderer, + { + let mut layouts = layout.children(); + + let layout = layouts.next()?; + let cursor_position = cursor.position()?; + + if !element.is_over(layout, renderer, cursor_position) { + return None; + } + + Some( + element + .overlay(layout, renderer) + .zip(layouts.next()) + .and_then(|(mut overlay, layout)| { + recurse( + &mut overlay, + layout, + cursor, + viewport, + renderer, + ) + }) + .unwrap_or_else(|| { + element.mouse_interaction( + layout, cursor, viewport, renderer, + ) + }), + ) + } + + recurse(&mut self.overlay, layout, cursor, viewport, renderer) + .unwrap_or_default() + } + + /// Returns true if the cursor is over the [`Nested`] overlay. + pub fn is_over( + &mut self, + layout: Layout<'_>, + renderer: &Renderer, + cursor_position: Point, + ) -> bool { + fn recurse<Message, Renderer>( + element: &mut overlay::Element<'_, Message, Renderer>, + layout: Layout<'_>, + renderer: &Renderer, + cursor_position: Point, + ) -> bool + where + Renderer: renderer::Renderer, + { + let mut layouts = layout.children(); + + if let Some(layout) = layouts.next() { + if element.is_over(layout, renderer, cursor_position) { + return true; + } + + if let Some((mut nested, nested_layout)) = + element.overlay(layout, renderer).zip(layouts.next()) + { + recurse( + &mut nested, + nested_layout, + renderer, + cursor_position, + ) + } else { + false + } + } else { + false + } + } + + recurse(&mut self.overlay, layout, renderer, cursor_position) + } +} diff --git a/native/src/program.rs b/runtime/src/program.rs index c71c237f..44585cc5 100644 --- a/native/src/program.rs +++ b/runtime/src/program.rs @@ -1,5 +1,8 @@ //! Build interactive programs using The Elm Architecture. -use crate::{Command, Element, Renderer}; +use crate::Command; + +use iced_core::text; +use iced_core::{Element, Renderer}; mod state; @@ -8,7 +11,7 @@ pub use state::State; /// The core of a user interface application following The Elm Architecture. pub trait Program: Sized { /// The graphics backend to use to draw the [`Program`]. - type Renderer: Renderer; + type Renderer: Renderer + text::Renderer; /// The type of __messages__ your [`Program`] will produce. type Message: std::fmt::Debug + Send; diff --git a/native/src/program/state.rs b/runtime/src/program/state.rs index 8ae1cacb..35df6078 100644 --- a/native/src/program/state.rs +++ b/runtime/src/program/state.rs @@ -1,9 +1,10 @@ -use crate::application; -use crate::event::{self, Event}; -use crate::mouse; -use crate::renderer; +use crate::core::event::{self, Event}; +use crate::core::mouse; +use crate::core::renderer; +use crate::core::widget::operation::{self, Operation}; +use crate::core::{Clipboard, Size}; use crate::user_interface::{self, UserInterface}; -use crate::{Clipboard, Command, Debug, Point, Program, Size}; +use crate::{Command, Debug, Program}; /// The execution state of a [`Program`]. It leverages caching, event /// processing, and rendering primitive storage. @@ -22,7 +23,6 @@ where impl<P> State<P> where P: Program + 'static, - <P::Renderer as crate::Renderer>::Theme: application::StyleSheet, { /// Creates a new [`State`] with the provided [`Program`], initializing its /// primitive with the given logical bounds and renderer. @@ -89,9 +89,9 @@ where pub fn update( &mut self, bounds: Size, - cursor_position: Point, + cursor: mouse::Cursor, renderer: &mut P::Renderer, - theme: &<P::Renderer as crate::Renderer>::Theme, + theme: &<P::Renderer as iced_core::Renderer>::Theme, style: &renderer::Style, clipboard: &mut dyn Clipboard, debug: &mut Debug, @@ -109,7 +109,7 @@ where let (_, event_statuses) = user_interface.update( &self.queued_events, - cursor_position, + cursor, renderer, clipboard, &mut messages, @@ -132,7 +132,7 @@ where let command = if messages.is_empty() { debug.draw_started(); self.mouse_interaction = - user_interface.draw(renderer, theme, style, cursor_position); + user_interface.draw(renderer, theme, style, cursor); debug.draw_finished(); self.cache = Some(user_interface.into_cache()); @@ -164,7 +164,7 @@ where debug.draw_started(); self.mouse_interaction = - user_interface.draw(renderer, theme, style, cursor_position); + user_interface.draw(renderer, theme, style, cursor); debug.draw_finished(); self.cache = Some(user_interface.into_cache()); @@ -174,6 +174,43 @@ where (uncaptured_events, command) } + + /// Applies [`widget::Operation`]s to the [`State`] + pub fn operate( + &mut self, + renderer: &mut P::Renderer, + operations: impl Iterator<Item = Box<dyn Operation<P::Message>>>, + bounds: Size, + debug: &mut Debug, + ) { + let mut user_interface = build_user_interface( + &mut self.program, + self.cache.take().unwrap(), + renderer, + bounds, + debug, + ); + + for operation in operations { + 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(message) => { + self.queued_messages.push(message) + } + operation::Outcome::Chain(next) => { + current_operation = Some(next); + } + }; + } + } + + self.cache = Some(user_interface.into_cache()); + } } fn build_user_interface<'a, P: Program>( @@ -182,10 +219,7 @@ fn build_user_interface<'a, P: Program>( renderer: &mut P::Renderer, size: Size, debug: &mut Debug, -) -> UserInterface<'a, P::Message, P::Renderer> -where - <P::Renderer as crate::Renderer>::Theme: application::StyleSheet, -{ +) -> UserInterface<'a, P::Message, P::Renderer> { debug.view_started(); let view = program.view(); debug.view_finished(); diff --git a/native/src/system.rs b/runtime/src/system.rs index 61c8ff29..61c8ff29 100644 --- a/native/src/system.rs +++ b/runtime/src/system.rs diff --git a/native/src/system/action.rs b/runtime/src/system/action.rs index dea9536f..dea9536f 100644 --- a/native/src/system/action.rs +++ b/runtime/src/system/action.rs diff --git a/native/src/system/information.rs b/runtime/src/system/information.rs index 93e7a5a4..93e7a5a4 100644 --- a/native/src/system/information.rs +++ b/runtime/src/system/information.rs diff --git a/native/src/user_interface.rs b/runtime/src/user_interface.rs index 68ccda55..619423fd 100644 --- a/native/src/user_interface.rs +++ b/runtime/src/user_interface.rs @@ -1,14 +1,12 @@ //! Implement your own event loop to drive a user interface. -use crate::application; -use crate::event::{self, Event}; -use crate::layout; -use crate::mouse; -use crate::renderer; -use crate::widget; -use crate::window; -use crate::{ - Clipboard, Element, Layout, Point, Rectangle, Shell, Size, Vector, -}; +use crate::core::event::{self, Event}; +use crate::core::layout; +use crate::core::mouse; +use crate::core::renderer; +use crate::core::widget; +use crate::core::window; +use crate::core::{Clipboard, Element, Layout, Point, Rectangle, Shell, Size}; +use crate::overlay; /// A set of interactive graphical elements with a specific [`Layout`]. /// @@ -18,11 +16,10 @@ use crate::{ /// charge of using this type in your system in any way you want. /// /// # Example -/// The [`integration_opengl`] & [`integration_wgpu`] examples use a -/// [`UserInterface`] to integrate Iced in an existing graphical application. +/// The [`integration`] example uses a [`UserInterface`] to integrate Iced in an +/// existing graphical application. /// -/// [`integration_opengl`]: https://github.com/iced-rs/iced/tree/0.8/examples/integration_opengl -/// [`integration_wgpu`]: https://github.com/iced-rs/iced/tree/0.8/examples/integration_wgpu +/// [`integration`]: https://github.com/iced-rs/iced/tree/0.9/examples/integration #[allow(missing_debug_implementations)] pub struct UserInterface<'a, Message, Renderer> { root: Element<'a, Message, Renderer>, @@ -34,8 +31,7 @@ pub struct UserInterface<'a, Message, Renderer> { impl<'a, Message, Renderer> UserInterface<'a, Message, Renderer> where - Renderer: crate::Renderer, - Renderer::Theme: application::StyleSheet, + Renderer: crate::core::Renderer, { /// Builds a user interface for an [`Element`]. /// @@ -48,24 +44,21 @@ where /// is naive way to set up our application loop: /// /// ```no_run - /// use iced_native::Size; - /// use iced_native::user_interface::{self, UserInterface}; - /// use iced_wgpu::Renderer; - /// /// # mod iced_wgpu { - /// # pub use iced_native::renderer::Null as Renderer; + /// # pub use iced_runtime::core::renderer::Null as Renderer; /// # } /// # - /// # use iced_native::widget::Column; - /// # /// # pub struct Counter; /// # /// # impl Counter { /// # pub fn new() -> Self { Counter } - /// # pub fn view(&self) -> Column<(), Renderer> { - /// # Column::new() - /// # } + /// # pub fn view(&self) -> iced_core::Element<(), Renderer> { unimplemented!() } + /// # pub fn update(&mut self, _: ()) {} /// # } + /// use iced_runtime::core::Size; + /// use iced_runtime::user_interface::{self, UserInterface}; + /// use iced_wgpu::Renderer; + /// /// // Initialization /// let mut counter = Counter::new(); /// let mut cache = user_interface::Cache::new(); @@ -124,30 +117,28 @@ where /// completing [the previous example](#example): /// /// ```no_run - /// use iced_native::{clipboard, Size, Point}; - /// use iced_native::user_interface::{self, UserInterface}; - /// use iced_wgpu::Renderer; - /// /// # mod iced_wgpu { - /// # pub use iced_native::renderer::Null as Renderer; + /// # pub use iced_runtime::core::renderer::Null as Renderer; /// # } /// # - /// # use iced_native::widget::Column; - /// # /// # pub struct Counter; /// # /// # impl Counter { /// # pub fn new() -> Self { Counter } - /// # pub fn view(&self) -> Column<(), Renderer> { - /// # Column::new() - /// # } - /// # pub fn update(&mut self, message: ()) {} + /// # pub fn view(&self) -> iced_core::Element<(), Renderer> { unimplemented!() } + /// # pub fn update(&mut self, _: ()) {} /// # } + /// use iced_runtime::core::clipboard; + /// use iced_runtime::core::mouse; + /// use iced_runtime::core::Size; + /// use iced_runtime::user_interface::{self, UserInterface}; + /// use iced_wgpu::Renderer; + /// /// let mut counter = Counter::new(); /// let mut cache = user_interface::Cache::new(); /// let mut renderer = Renderer::new(); /// let mut window_size = Size::new(1024.0, 768.0); - /// let mut cursor_position = Point::default(); + /// let mut cursor = mouse::Cursor::default(); /// let mut clipboard = clipboard::Null; /// /// // Initialize our event storage @@ -167,7 +158,7 @@ where /// // Update the user interface /// let (state, event_statuses) = user_interface.update( /// &events, - /// cursor_position, + /// cursor, /// &mut renderer, /// &mut clipboard, /// &mut messages @@ -184,7 +175,7 @@ where pub fn update( &mut self, events: &[Event], - cursor_position: Point, + cursor: mouse::Cursor, renderer: &mut Renderer, clipboard: &mut dyn Clipboard, messages: &mut Vec<Message>, @@ -194,18 +185,18 @@ where let mut outdated = false; let mut redraw_request = None; - let mut manual_overlay = - ManuallyDrop::new(self.root.as_widget_mut().overlay( - &mut self.state, - Layout::new(&self.base), - renderer, - )); + let mut manual_overlay = ManuallyDrop::new( + self.root + .as_widget_mut() + .overlay(&mut self.state, Layout::new(&self.base), renderer) + .map(overlay::Nested::new), + ); let (base_cursor, overlay_statuses) = if manual_overlay.is_some() { let bounds = self.bounds; let mut overlay = manual_overlay.as_mut().unwrap(); - let mut layout = overlay.layout(renderer, bounds, Vector::ZERO); + let mut layout = overlay.layout(renderer, bounds, Point::ORIGIN); let mut event_statuses = Vec::new(); for event in events.iter().cloned() { @@ -214,7 +205,7 @@ where let event_status = overlay.on_event( event, Layout::new(&layout), - cursor_position, + cursor, renderer, clipboard, &mut shell, @@ -240,12 +231,16 @@ where &layout::Limits::new(Size::ZERO, self.bounds), ); - manual_overlay = - ManuallyDrop::new(self.root.as_widget_mut().overlay( - &mut self.state, - Layout::new(&self.base), - renderer, - )); + manual_overlay = ManuallyDrop::new( + self.root + .as_widget_mut() + .overlay( + &mut self.state, + Layout::new(&self.base), + renderer, + ) + .map(overlay::Nested::new), + ); if manual_overlay.is_none() { break; @@ -254,7 +249,8 @@ where overlay = manual_overlay.as_mut().unwrap(); shell.revalidate_layout(|| { - layout = overlay.layout(renderer, bounds, Vector::ZERO); + layout = + overlay.layout(renderer, bounds, Point::ORIGIN); }); } @@ -263,22 +259,29 @@ where } } - let base_cursor = manual_overlay - .as_ref() - .filter(|overlay| { - overlay.is_over(Layout::new(&layout), cursor_position) - }) - .map(|_| { - // TODO: Type-safe cursor availability - Point::new(-1.0, -1.0) + let base_cursor = if manual_overlay + .as_mut() + .and_then(|overlay| { + cursor.position().map(|cursor_position| { + overlay.is_over( + Layout::new(&layout), + renderer, + cursor_position, + ) + }) }) - .unwrap_or(cursor_position); + .unwrap_or_default() + { + mouse::Cursor::Unavailable + } else { + cursor + }; self.overlay = Some(layout); (base_cursor, event_statuses) } else { - (cursor_position, vec![event::Status::Ignored; events.len()]) + (cursor, vec![event::Status::Ignored; events.len()]) }; let _ = ManuallyDrop::into_inner(manual_overlay); @@ -357,35 +360,34 @@ where /// [completing the last example](#example-1): /// /// ```no_run - /// use iced_native::clipboard; - /// use iced_native::renderer; - /// use iced_native::user_interface::{self, UserInterface}; - /// use iced_native::{Size, Point, Theme}; - /// use iced_wgpu::Renderer; - /// /// # mod iced_wgpu { - /// # pub use iced_native::renderer::Null as Renderer; + /// # pub use iced_runtime::core::renderer::Null as Renderer; + /// # pub type Theme = (); /// # } /// # - /// # use iced_native::widget::Column; - /// # /// # pub struct Counter; /// # /// # impl Counter { /// # pub fn new() -> Self { Counter } - /// # pub fn view(&self) -> Column<(), Renderer> { - /// # Column::new() - /// # } - /// # pub fn update(&mut self, message: ()) {} + /// # pub fn view(&self) -> Element<(), Renderer> { unimplemented!() } + /// # pub fn update(&mut self, _: ()) {} /// # } + /// use iced_runtime::core::clipboard; + /// use iced_runtime::core::mouse; + /// use iced_runtime::core::renderer; + /// use iced_runtime::core::{Element, Size}; + /// use iced_runtime::user_interface::{self, UserInterface}; + /// use iced_wgpu::{Renderer, Theme}; + /// /// let mut counter = Counter::new(); /// let mut cache = user_interface::Cache::new(); /// let mut renderer = Renderer::new(); /// let mut window_size = Size::new(1024.0, 768.0); - /// let mut cursor_position = Point::default(); + /// let mut cursor = mouse::Cursor::default(); /// let mut clipboard = clipboard::Null; /// let mut events = Vec::new(); /// let mut messages = Vec::new(); + /// let mut theme = Theme::default(); /// /// loop { /// // Obtain system events... @@ -400,14 +402,14 @@ where /// // Update the user interface /// let event_statuses = user_interface.update( /// &events, - /// cursor_position, + /// cursor, /// &mut renderer, /// &mut clipboard, /// &mut messages /// ); /// /// // Draw the user interface - /// let mouse_cursor = user_interface.draw(&mut renderer, &Theme::default(), &renderer::Style::default(), cursor_position); + /// let mouse_interaction = user_interface.draw(&mut renderer, &theme, &renderer::Style::default(), cursor); /// /// cache = user_interface.into_cache(); /// @@ -424,35 +426,44 @@ where renderer: &mut Renderer, theme: &Renderer::Theme, style: &renderer::Style, - cursor_position: Point, + cursor: mouse::Cursor, ) -> mouse::Interaction { // TODO: Move to shell level (?) renderer.clear(); let viewport = Rectangle::with_size(self.bounds); - let base_cursor = if let Some(overlay) = self + let base_cursor = if let Some(mut overlay) = self .root .as_widget_mut() .overlay(&mut self.state, Layout::new(&self.base), renderer) + .map(overlay::Nested::new) { let overlay_layout = self.overlay.take().unwrap_or_else(|| { - overlay.layout(renderer, self.bounds, Vector::ZERO) + overlay.layout(renderer, self.bounds, Point::ORIGIN) }); - let new_cursor_position = if overlay - .is_over(Layout::new(&overlay_layout), cursor_position) + let cursor = if cursor + .position() + .map(|cursor_position| { + overlay.is_over( + Layout::new(&overlay_layout), + renderer, + cursor_position, + ) + }) + .unwrap_or_default() { - Point::new(-1.0, -1.0) + mouse::Cursor::Unavailable } else { - cursor_position + cursor }; self.overlay = Some(overlay_layout); - new_cursor_position + cursor } else { - cursor_position + cursor }; self.root.as_widget().draw( @@ -468,7 +479,7 @@ where let base_interaction = self.root.as_widget().mouse_interaction( &self.state, Layout::new(&self.base), - cursor_position, + base_cursor, &viewport, renderer, ); @@ -490,10 +501,11 @@ where .and_then(|layout| { root.as_widget_mut() .overlay(&mut self.state, Layout::new(base), renderer) - .map(|overlay| { + .map(overlay::Nested::new) + .map(|mut overlay| { let overlay_interaction = overlay.mouse_interaction( Layout::new(layout), - cursor_position, + cursor, &viewport, renderer, ); @@ -506,11 +518,20 @@ where theme, style, Layout::new(layout), - cursor_position, + cursor, ); }); - if overlay.is_over(Layout::new(layout), cursor_position) + if cursor + .position() + .map(|cursor_position| { + overlay.is_over( + Layout::new(layout), + renderer, + cursor_position, + ) + }) + .unwrap_or_default() { overlay_interaction } else { @@ -534,14 +555,15 @@ where operation, ); - if let Some(mut overlay) = self.root.as_widget_mut().overlay( - &mut self.state, - Layout::new(&self.base), - renderer, - ) { + if let Some(mut overlay) = self + .root + .as_widget_mut() + .overlay(&mut self.state, Layout::new(&self.base), renderer) + .map(overlay::Nested::new) + { if self.overlay.is_none() { self.overlay = - Some(overlay.layout(renderer, self.bounds, Vector::ZERO)); + Some(overlay.layout(renderer, self.bounds, Point::ORIGIN)); } overlay.operate( diff --git a/runtime/src/window.rs b/runtime/src/window.rs new file mode 100644 index 00000000..5219fbfd --- /dev/null +++ b/runtime/src/window.rs @@ -0,0 +1,135 @@ +//! 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, Level, Mode, UserAttention}; +use crate::core::Size; +use crate::futures::subscription::{self, Subscription}; + +/// Subscribes to the frames of the window of the running application. +/// +/// The resulting [`Subscription`] will produce items at a rate equal to the +/// refresh rate of the window. Note that this rate may be variable, as it is +/// normally managed by the graphics driver and/or the OS. +/// +/// In any case, this [`Subscription`] is useful to smoothly draw application-driven +/// animations without missing any frames. +pub fn frames() -> Subscription<Instant> { + subscription::raw_events(|event, _status| match event { + iced_core::Event::Window(Event::RedrawRequested(at)) => Some(at), + _ => None, + }) +} + +/// Closes the current window and exits the application. +pub fn close<Message>() -> Command<Message> { + Command::single(command::Action::Window(Action::Close)) +} + +/// Begins dragging the window while the left mouse button is held. +pub fn drag<Message>() -> Command<Message> { + Command::single(command::Action::Window(Action::Drag)) +} + +/// Resizes the window to the given logical dimensions. +pub fn resize<Message>(new_size: Size<u32>) -> Command<Message> { + Command::single(command::Action::Window(Action::Resize(new_size))) +} + +/// Fetches the current window size in logical dimensions. +pub fn fetch_size<Message>( + f: impl FnOnce(Size<u32>) -> Message + 'static, +) -> Command<Message> { + Command::single(command::Action::Window(Action::FetchSize(Box::new(f)))) +} + +/// Maximizes the window. +pub fn maximize<Message>(maximized: bool) -> Command<Message> { + Command::single(command::Action::Window(Action::Maximize(maximized))) +} + +/// Minimes the window. +pub fn minimize<Message>(minimized: bool) -> Command<Message> { + Command::single(command::Action::Window(Action::Minimize(minimized))) +} + +/// Moves a window to the given logical coordinates. +pub fn move_to<Message>(x: i32, y: i32) -> Command<Message> { + Command::single(command::Action::Window(Action::Move { x, y })) +} + +/// Changes the [`Mode`] of the window. +pub fn change_mode<Message>(mode: Mode) -> Command<Message> { + Command::single(command::Action::Window(Action::ChangeMode(mode))) +} + +/// Fetches the current [`Mode`] of the window. +pub fn fetch_mode<Message>( + f: impl FnOnce(Mode) -> Message + 'static, +) -> Command<Message> { + Command::single(command::Action::Window(Action::FetchMode(Box::new(f)))) +} + +/// Toggles the window to maximized or back. +pub fn toggle_maximize<Message>() -> Command<Message> { + Command::single(command::Action::Window(Action::ToggleMaximize)) +} + +/// Toggles the window decorations. +pub fn toggle_decorations<Message>() -> Command<Message> { + Command::single(command::Action::Window(Action::ToggleDecorations)) +} + +/// 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. +pub fn request_user_attention<Message>( + user_attention: Option<UserAttention>, +) -> Command<Message> { + Command::single(command::Action::Window(Action::RequestUserAttention( + user_attention, + ))) +} + +/// 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 +/// you are certain that's what the user wants. Focus stealing can cause an extremely disruptive +/// user experience. +pub fn gain_focus<Message>() -> Command<Message> { + Command::single(command::Action::Window(Action::GainFocus)) +} + +/// Changes the window [`Level`]. +pub fn change_level<Message>(level: Level) -> Command<Message> { + Command::single(command::Action::Window(Action::ChangeLevel(level))) +} + +/// Fetches an identifier unique to the window. +pub fn fetch_id<Message>( + f: impl FnOnce(u64) -> Message + 'static, +) -> Command<Message> { + Command::single(command::Action::Window(Action::FetchId(Box::new(f)))) +} + +/// Changes the [`Icon`] of the window. +pub fn change_icon<Message>(icon: Icon) -> Command<Message> { + Command::single(command::Action::Window(Action::ChangeIcon(icon))) +} + +/// Captures a [`Screenshot`] from the window. +pub fn screenshot<Message>( + f: impl FnOnce(Screenshot) -> Message + Send + 'static, +) -> Command<Message> { + Command::single(command::Action::Window(Action::Screenshot(Box::new(f)))) +} diff --git a/native/src/window/action.rs b/runtime/src/window/action.rs index 5751bf97..cebec4ae 100644 --- a/native/src/window/action.rs +++ b/runtime/src/window/action.rs @@ -1,13 +1,15 @@ -use crate::window::{Mode, UserAttention, Settings}; +use crate::core::window::{Icon, Level, Mode, UserAttention, Settings}; +use crate::core::Size; +use crate::futures::MaybeSend; +use crate::window::Screenshot; -use iced_futures::MaybeSend; use std::fmt; /// An operation to be performed on some window. pub enum Action<T> { - /// Closes the current window and exits the application. + /// Close the current window and exits the application. Close, - /// Moves the window with the left mouse button until the button is + /// 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 @@ -19,13 +21,10 @@ pub enum Action<T> { settings: Settings, }, /// Resize the window. - Resize { - /// The new logical width of the window - width: u32, - /// The new logical height of the window - height: u32, - }, - /// Sets the window to maximized or back + Resize(Size<u32>), + /// Fetch the current size of the window. + FetchSize(Box<dyn FnOnce(Size<u32>) -> T + 'static>), + /// Set the window to maximized or back Maximize(bool), /// Set the window to minimized or back Minimize(bool), @@ -75,14 +74,27 @@ pub enum Action<T> { /// /// - **Web / Wayland:** Unsupported. GainFocus, - /// Change whether or not the window will always be on top of other windows. + /// Change the window [`Level`]. + ChangeLevel(Level), + /// Fetch an identifier unique to the window. + FetchId(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:** Unsupported. - ChangeAlwaysOnTop(bool), - /// Fetch an identifier unique to the window. - FetchId(Box<dyn FnOnce(u64) -> T + 'static>), + /// - **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(Icon), + /// Screenshot the viewport of the window. + Screenshot(Box<dyn FnOnce(Screenshot) -> T + 'static>), } impl<T> Action<T> { @@ -95,10 +107,11 @@ impl<T> Action<T> { T: 'static, { match self { - Self::Spawn { settings } => Action::Spawn { settings }, Self::Close => Action::Close, Self::Drag => Action::Drag, - Self::Resize { width, height } => Action::Resize { width, height }, + Self::Spawn { settings } => Action::Spawn { settings }, + Self::Resize(size) => Action::Resize(size), + Self::FetchSize(o) => Action::FetchSize(Box::new(move |s| f(o(s)))), Self::Maximize(maximized) => Action::Maximize(maximized), Self::Minimize(minimized) => Action::Minimize(minimized), Self::Move { x, y } => Action::Move { x, y }, @@ -110,10 +123,14 @@ impl<T> Action<T> { Action::RequestUserAttention(attention_type) } Self::GainFocus => Action::GainFocus, - Self::ChangeAlwaysOnTop(on_top) => { - Action::ChangeAlwaysOnTop(on_top) - } + Self::ChangeLevel(level) => Action::ChangeLevel(level), Self::FetchId(o) => Action::FetchId(Box::new(move |s| f(o(s)))), + Self::ChangeIcon(icon) => Action::ChangeIcon(icon), + Self::Screenshot(tag) => { + Action::Screenshot(Box::new(move |screenshot| { + f(tag(screenshot)) + })) + } } } } @@ -126,10 +143,8 @@ impl<T> fmt::Debug for Action<T> { Self::Spawn { settings } => { write!(f, "Action::Spawn {{ settings: {:?} }}", settings) } - Self::Resize { width, height } => write!( - f, - "Action::Resize {{ widget: {width}, height: {height} }}" - ), + Self::Resize(size) => write!(f, "Action::Resize({size:?})"), + Self::FetchSize(_) => write!(f, "Action::FetchSize"), Self::Maximize(maximized) => { write!(f, "Action::Maximize({maximized})") } @@ -147,10 +162,14 @@ impl<T> fmt::Debug for Action<T> { write!(f, "Action::RequestUserAttention") } Self::GainFocus => write!(f, "Action::GainFocus"), - Self::ChangeAlwaysOnTop(on_top) => { - write!(f, "Action::AlwaysOnTop({on_top})") + Self::ChangeLevel(level) => { + write!(f, "Action::ChangeLevel({level:?})") } Self::FetchId(_) => write!(f, "Action::FetchId"), + Self::ChangeIcon(_icon) => { + write!(f, "Action::ChangeIcon(icon)") + } + Self::Screenshot(_) => write!(f, "Action::Screenshot"), } } } diff --git a/runtime/src/window/screenshot.rs b/runtime/src/window/screenshot.rs new file mode 100644 index 00000000..c84286b6 --- /dev/null +++ b/runtime/src/window/screenshot.rs @@ -0,0 +1,92 @@ +//! Take screenshots of a window. +use crate::core::{Rectangle, Size}; + +use std::fmt::{Debug, Formatter}; +use std::sync::Arc; + +/// Data of a screenshot, captured with `window::screenshot()`. +/// +/// The `bytes` of this screenshot will always be ordered as `RGBA` in the sRGB color space. +#[derive(Clone)] +pub struct Screenshot { + /// The bytes of the [`Screenshot`]. + pub bytes: Arc<Vec<u8>>, + /// The size of the [`Screenshot`]. + pub size: Size<u32>, +} + +impl Debug for Screenshot { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!( + f, + "Screenshot: {{ \n bytes: {}\n size: {:?} }}", + self.bytes.len(), + self.size + ) + } +} + +impl Screenshot { + /// Creates a new [`Screenshot`]. + pub fn new(bytes: Vec<u8>, size: Size<u32>) -> Self { + Self { + bytes: Arc::new(bytes), + size, + } + } + + /// Crops a [`Screenshot`] to the provided `region`. This will always be relative to the + /// top-left corner of the [`Screenshot`]. + pub fn crop(&self, region: Rectangle<u32>) -> Result<Self, CropError> { + if region.width == 0 || region.height == 0 { + return Err(CropError::Zero); + } + + if region.x + region.width > self.size.width + || region.y + region.height > self.size.height + { + return Err(CropError::OutOfBounds); + } + + // Image is always RGBA8 = 4 bytes per pixel + const PIXEL_SIZE: usize = 4; + + let bytes_per_row = self.size.width as usize * PIXEL_SIZE; + let row_range = region.y as usize..(region.y + region.height) as usize; + let column_range = region.x as usize * PIXEL_SIZE + ..(region.x + region.width) as usize * PIXEL_SIZE; + + let chopped = self.bytes.chunks(bytes_per_row).enumerate().fold( + vec![], + |mut acc, (row, bytes)| { + if row_range.contains(&row) { + acc.extend(&bytes[column_range.clone()]); + } + + acc + }, + ); + + Ok(Self { + bytes: Arc::new(chopped), + size: Size::new(region.width, region.height), + }) + } +} + +impl AsRef<[u8]> for Screenshot { + fn as_ref(&self) -> &[u8] { + &self.bytes + } +} + +#[derive(Debug, thiserror::Error)] +/// Errors that can occur when cropping a [`Screenshot`]. +pub enum CropError { + #[error("The cropped region is out of bounds.")] + /// The cropped region's size is out of bounds. + OutOfBounds, + #[error("The cropped region is not visible.")] + /// The cropped region's size is zero. + Zero, +} |