diff options
author | 2023-03-05 06:35:20 +0100 | |
---|---|---|
committer | 2023-03-05 06:35:20 +0100 | |
commit | 99e0a71504456976ba88040f5d1d3bbc347694ea (patch) | |
tree | a228c064fd3847831ff8072aa9375dc59db47f47 /runtime/src | |
parent | 8af69be47e88896b3c5f70174db609eee0c67971 (diff) | |
download | iced-99e0a71504456976ba88040f5d1d3bbc347694ea.tar.gz iced-99e0a71504456976ba88040f5d1d3bbc347694ea.tar.bz2 iced-99e0a71504456976ba88040f5d1d3bbc347694ea.zip |
Rename `iced_native` to `iced_runtime`
Diffstat (limited to 'runtime/src')
-rw-r--r-- | runtime/src/clipboard.rs | 53 | ||||
-rw-r--r-- | runtime/src/command.rs | 108 | ||||
-rw-r--r-- | runtime/src/command/action.rs | 86 | ||||
-rw-r--r-- | runtime/src/debug/basic.rs | 226 | ||||
-rw-r--r-- | runtime/src/debug/null.rs | 47 | ||||
-rw-r--r-- | runtime/src/font.rs | 19 | ||||
-rw-r--r-- | runtime/src/keyboard.rs | 2 | ||||
-rw-r--r-- | runtime/src/lib.rs | 71 | ||||
-rw-r--r-- | runtime/src/program.rs | 33 | ||||
-rw-r--r-- | runtime/src/program/state.rs | 194 | ||||
-rw-r--r-- | runtime/src/system.rs | 6 | ||||
-rw-r--r-- | runtime/src/system/action.rs | 39 | ||||
-rw-r--r-- | runtime/src/system/information.rs | 29 | ||||
-rw-r--r-- | runtime/src/user_interface.rs | 592 | ||||
-rw-r--r-- | runtime/src/window.rs | 112 | ||||
-rw-r--r-- | runtime/src/window/action.rs | 147 |
16 files changed, 1764 insertions, 0 deletions
diff --git a/runtime/src/clipboard.rs b/runtime/src/clipboard.rs new file mode 100644 index 00000000..bc450912 --- /dev/null +++ b/runtime/src/clipboard.rs @@ -0,0 +1,53 @@ +//! Access the clipboard. +use crate::command::{self, Command}; +use crate::futures::MaybeSend; + +use std::fmt; + +/// A clipboard action to be performed by some [`Command`]. +/// +/// [`Command`]: crate::Command +pub enum Action<T> { + /// Read the clipboard and produce `T` with the result. + Read(Box<dyn Fn(Option<String>) -> T>), + + /// Write the given contents to the clipboard. + Write(String), +} + +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) => Action::Read(Box::new(move |s| f(o(s)))), + Self::Write(content) => Action::Write(content), + } + } +} + +impl<T> fmt::Debug for Action<T> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Read(_) => write!(f, "Action::Read"), + Self::Write(_) => write!(f, "Action::Write"), + } + } +} + +/// 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/runtime/src/command.rs b/runtime/src/command.rs new file mode 100644 index 00000000..cd4c51ff --- /dev/null +++ b/runtime/src/command.rs @@ -0,0 +1,108 @@ +//! Run asynchronous actions. +mod action; + +pub use action::Action; + +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>(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 = T> + 'static + MaybeSend, + f: impl FnOnce(T) -> A + 'static + MaybeSend, + ) -> Command<A> { + use iced_futures::futures::FutureExt; + + Command::single(Action::Future(Box::pin(future.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<T> fmt::Debug for Command<T> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let Command(command) = self; + + command.fmt(f) + } +} diff --git a/runtime/src/command/action.rs b/runtime/src/command/action.rs new file mode 100644 index 00000000..6c74f0ef --- /dev/null +++ b/runtime/src/command/action.rs @@ -0,0 +1,86 @@ +use crate::clipboard; +use crate::core::widget; +use crate::font; +use crate::system; +use crate::window; + +use iced_futures::MaybeSend; + +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 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>, + }, +} + +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; + + match self { + Self::Future(future) => Action::Future(Box::pin(future.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))), + }, + } + } +} + +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::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"), + } + } +} diff --git a/runtime/src/debug/basic.rs b/runtime/src/debug/basic.rs new file mode 100644 index 00000000..32f725a1 --- /dev/null +++ b/runtime/src/debug/basic.rs @@ -0,0 +1,226 @@ +#![allow(missing_docs)] +use crate::core::time; + +use std::collections::VecDeque; + +/// A bunch of time measurements for debugging purposes. +#[derive(Debug)] +pub struct Debug { + is_enabled: bool, + + startup_start: time::Instant, + startup_duration: time::Duration, + + update_start: time::Instant, + update_durations: TimeBuffer, + + view_start: time::Instant, + view_durations: TimeBuffer, + + layout_start: time::Instant, + layout_durations: TimeBuffer, + + event_start: time::Instant, + event_durations: TimeBuffer, + + draw_start: time::Instant, + draw_durations: TimeBuffer, + + render_start: time::Instant, + render_durations: TimeBuffer, + + message_count: usize, + last_messages: VecDeque<String>, +} + +impl Debug { + /// Creates a new [`struct@Debug`]. + pub fn new() -> Self { + let now = time::Instant::now(); + + Self { + is_enabled: false, + startup_start: now, + startup_duration: time::Duration::from_secs(0), + + update_start: now, + update_durations: TimeBuffer::new(200), + + view_start: now, + view_durations: TimeBuffer::new(200), + + layout_start: now, + layout_durations: TimeBuffer::new(200), + + event_start: now, + event_durations: TimeBuffer::new(200), + + draw_start: now, + draw_durations: TimeBuffer::new(200), + + render_start: now, + render_durations: TimeBuffer::new(50), + + message_count: 0, + last_messages: VecDeque::new(), + } + } + + pub fn toggle(&mut self) { + self.is_enabled = !self.is_enabled; + } + + pub fn startup_started(&mut self) { + self.startup_start = time::Instant::now(); + } + + pub fn startup_finished(&mut self) { + self.startup_duration = time::Instant::now() - self.startup_start; + } + + pub fn update_started(&mut self) { + self.update_start = time::Instant::now(); + } + + pub fn update_finished(&mut self) { + self.update_durations + .push(time::Instant::now() - self.update_start); + } + + pub fn view_started(&mut self) { + self.view_start = time::Instant::now(); + } + + pub fn view_finished(&mut self) { + self.view_durations + .push(time::Instant::now() - self.view_start); + } + + pub fn layout_started(&mut self) { + self.layout_start = time::Instant::now(); + } + + pub fn layout_finished(&mut self) { + self.layout_durations + .push(time::Instant::now() - self.layout_start); + } + + pub fn event_processing_started(&mut self) { + self.event_start = time::Instant::now(); + } + + pub fn event_processing_finished(&mut self) { + self.event_durations + .push(time::Instant::now() - self.event_start); + } + + pub fn draw_started(&mut self) { + self.draw_start = time::Instant::now(); + } + + pub fn draw_finished(&mut self) { + self.draw_durations + .push(time::Instant::now() - self.draw_start); + } + + pub fn render_started(&mut self) { + self.render_start = time::Instant::now(); + } + + pub fn render_finished(&mut self) { + self.render_durations + .push(time::Instant::now() - self.render_start); + } + + pub fn log_message<Message: std::fmt::Debug>(&mut self, message: &Message) { + self.last_messages.push_back(format!("{message:?}")); + + if self.last_messages.len() > 10 { + let _ = self.last_messages.pop_front(); + } + + self.message_count += 1; + } + + pub fn overlay(&self) -> Vec<String> { + if !self.is_enabled { + return Vec::new(); + } + + let mut lines = Vec::new(); + + fn key_value<T: std::fmt::Debug>(key: &str, value: T) -> String { + format!("{key} {value:?}") + } + + lines.push(format!( + "{} {} - {}", + env!("CARGO_PKG_NAME"), + env!("CARGO_PKG_VERSION"), + env!("CARGO_PKG_REPOSITORY"), + )); + lines.push(key_value("Startup:", self.startup_duration)); + lines.push(key_value("Update:", self.update_durations.average())); + lines.push(key_value("View:", self.view_durations.average())); + lines.push(key_value("Layout:", self.layout_durations.average())); + lines.push(key_value( + "Event processing:", + self.event_durations.average(), + )); + lines.push(key_value( + "Primitive generation:", + self.draw_durations.average(), + )); + lines.push(key_value("Render:", self.render_durations.average())); + lines.push(key_value("Message count:", self.message_count)); + lines.push(String::from("Last messages:")); + lines.extend(self.last_messages.iter().map(|msg| { + if msg.len() <= 100 { + format!(" {msg}") + } else { + format!(" {msg:.100}...") + } + })); + + lines + } +} + +impl Default for Debug { + fn default() -> Self { + Self::new() + } +} + +#[derive(Debug)] +struct TimeBuffer { + head: usize, + size: usize, + contents: Vec<time::Duration>, +} + +impl TimeBuffer { + fn new(capacity: usize) -> TimeBuffer { + TimeBuffer { + head: 0, + size: 0, + contents: vec![time::Duration::from_secs(0); capacity], + } + } + + fn push(&mut self, duration: time::Duration) { + self.head = (self.head + 1) % self.contents.len(); + self.contents[self.head] = duration; + self.size = (self.size + 1).min(self.contents.len()); + } + + fn average(&self) -> time::Duration { + let sum: time::Duration = if self.size == self.contents.len() { + self.contents[..].iter().sum() + } else { + self.contents[..self.size].iter().sum() + }; + + sum / self.size.max(1) as u32 + } +} diff --git a/runtime/src/debug/null.rs b/runtime/src/debug/null.rs new file mode 100644 index 00000000..2db0eebb --- /dev/null +++ b/runtime/src/debug/null.rs @@ -0,0 +1,47 @@ +#![allow(missing_docs)] +#[derive(Debug, Default)] +pub struct Debug; + +impl Debug { + pub fn new() -> Self { + Self + } + + pub fn startup_started(&mut self) {} + + pub fn startup_finished(&mut self) {} + + pub fn update_started(&mut self) {} + + pub fn update_finished(&mut self) {} + + pub fn view_started(&mut self) {} + + pub fn view_finished(&mut self) {} + + pub fn layout_started(&mut self) {} + + pub fn layout_finished(&mut self) {} + + pub fn event_processing_started(&mut self) {} + + pub fn event_processing_finished(&mut self) {} + + pub fn draw_started(&mut self) {} + + pub fn draw_finished(&mut self) {} + + pub fn render_started(&mut self) {} + + pub fn render_finished(&mut self) {} + + pub fn log_message<Message: std::fmt::Debug>( + &mut self, + _message: &Message, + ) { + } + + pub fn overlay(&self) -> Vec<String> { + Vec::new() + } +} 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/runtime/src/keyboard.rs b/runtime/src/keyboard.rs new file mode 100644 index 00000000..012538e3 --- /dev/null +++ b/runtime/src/keyboard.rs @@ -0,0 +1,2 @@ +//! Track keyboard events. +pub use iced_core::keyboard::*; diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs new file mode 100644 index 00000000..aa45e57a --- /dev/null +++ b/runtime/src/lib.rs @@ -0,0 +1,71 @@ +//! A renderer-agnostic native GUI runtime. +//! +//!  +//! +//! `iced_native` takes [`iced_core`] and builds a native runtime on top of it, +//! featuring: +//! +//! - A custom layout engine, greatly inspired by [`druid`] +//! - Event handling for all the built-in widgets +//! - A renderer-agnostic API +//! +//! To achieve this, it introduces a couple of reusable interfaces: +//! +//! - A [`Widget`] trait, which is used to implement new widgets: from layout +//! requirements to event and drawing logic. +//! - A bunch of `Renderer` traits, meant to keep the crate renderer-agnostic. +//! +//! # Usage +//! The strategy to use this crate depends on your particular use case. If you +//! want to: +//! - Implement a custom shell or integrate it in your own system, check out the +//! [`UserInterface`] type. +//! - 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 +//! [`druid`]: https://github.com/xi-editor/druid +//! [`raw-window-handle`]: https://github.com/rust-windowing/raw-window-handle +//! [renderer]: crate::renderer +#![doc( + html_logo_url = "https://raw.githubusercontent.com/iced-rs/iced/9ab6923e943f784985e9ef9ca28b10278297225d/docs/logo.svg" +)] +#![deny( + missing_debug_implementations, + //missing_docs, + unused_results, + clippy::extra_unused_lifetimes, + clippy::from_over_into, + clippy::needless_borrow, + clippy::new_without_default, + clippy::useless_conversion +)] +#![forbid(unsafe_code, rust_2018_idioms)] +#![cfg_attr(docsrs, feature(doc_cfg))] +pub mod clipboard; +pub mod command; +pub mod font; +pub mod keyboard; +pub mod program; +pub mod system; +pub mod user_interface; +pub mod window; + +// We disable debug capabilities on release builds unless the `debug` feature +// is explicitly enabled. +#[cfg(feature = "debug")] +#[path = "debug/basic.rs"] +mod debug; +#[cfg(not(feature = "debug"))] +#[path = "debug/null.rs"] +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 user_interface::UserInterface; diff --git a/runtime/src/program.rs b/runtime/src/program.rs new file mode 100644 index 00000000..44585cc5 --- /dev/null +++ b/runtime/src/program.rs @@ -0,0 +1,33 @@ +//! Build interactive programs using The Elm Architecture. +use crate::Command; + +use iced_core::text; +use iced_core::{Element, Renderer}; + +mod state; + +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 + text::Renderer; + + /// The type of __messages__ your [`Program`] will produce. + type Message: std::fmt::Debug + Send; + + /// Handles a __message__ and updates the state of the [`Program`]. + /// + /// This is where you define your __update logic__. All the __messages__, + /// 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>; + + /// Returns the widgets to display in the [`Program`]. + /// + /// These widgets can produce __messages__ based on user interaction. + fn view(&self) -> Element<'_, Self::Message, Self::Renderer>; +} diff --git a/runtime/src/program/state.rs b/runtime/src/program/state.rs new file mode 100644 index 00000000..2fa9934d --- /dev/null +++ b/runtime/src/program/state.rs @@ -0,0 +1,194 @@ +use crate::core::event::{self, Event}; +use crate::core::mouse; +use crate::core::renderer; +use crate::core::{Clipboard, Point, Size}; +use crate::user_interface::{self, UserInterface}; +use crate::{Command, Debug, Program}; + +/// The execution state of a [`Program`]. It leverages caching, event +/// processing, and rendering primitive storage. +#[allow(missing_debug_implementations)] +pub struct State<P> +where + P: Program + 'static, +{ + program: P, + cache: Option<user_interface::Cache>, + queued_events: Vec<Event>, + queued_messages: Vec<P::Message>, + mouse_interaction: mouse::Interaction, +} + +impl<P> State<P> +where + P: Program + 'static, +{ + /// Creates a new [`State`] with the provided [`Program`], initializing its + /// primitive with the given logical bounds and renderer. + pub fn new( + mut program: P, + bounds: Size, + renderer: &mut P::Renderer, + debug: &mut Debug, + ) -> Self { + let user_interface = build_user_interface( + &mut program, + user_interface::Cache::default(), + renderer, + bounds, + debug, + ); + + let cache = Some(user_interface.into_cache()); + + State { + program, + cache, + queued_events: Vec::new(), + queued_messages: Vec::new(), + mouse_interaction: mouse::Interaction::Idle, + } + } + + /// Returns a reference to the [`Program`] of the [`State`]. + pub fn program(&self) -> &P { + &self.program + } + + /// Queues an event in the [`State`] for processing during an [`update`]. + /// + /// [`update`]: Self::update + pub fn queue_event(&mut self, event: Event) { + self.queued_events.push(event); + } + + /// Queues a message in the [`State`] for processing during an [`update`]. + /// + /// [`update`]: Self::update + pub fn queue_message(&mut self, message: P::Message) { + self.queued_messages.push(message); + } + + /// Returns whether the event queue of the [`State`] is empty or not. + pub fn is_queue_empty(&self) -> bool { + self.queued_events.is_empty() && self.queued_messages.is_empty() + } + + /// Returns the current [`mouse::Interaction`] of the [`State`]. + pub fn mouse_interaction(&self) -> mouse::Interaction { + self.mouse_interaction + } + + /// Processes all the queued events and messages, rebuilding and redrawing + /// 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`] + /// after updating it, only if an update was necessary. + pub fn update( + &mut self, + bounds: Size, + cursor_position: Point, + renderer: &mut P::Renderer, + theme: &<P::Renderer as iced_core::Renderer>::Theme, + style: &renderer::Style, + clipboard: &mut dyn Clipboard, + debug: &mut Debug, + ) -> (Vec<Event>, Option<Command<P::Message>>) { + let mut user_interface = build_user_interface( + &mut self.program, + self.cache.take().unwrap(), + renderer, + bounds, + debug, + ); + + debug.event_processing_started(); + let mut messages = Vec::new(); + + let (_, event_statuses) = user_interface.update( + &self.queued_events, + cursor_position, + renderer, + clipboard, + &mut messages, + ); + + let uncaptured_events = self + .queued_events + .iter() + .zip(event_statuses) + .filter_map(|(event, status)| { + matches!(status, event::Status::Ignored).then_some(event) + }) + .cloned() + .collect(); + + self.queued_events.clear(); + messages.append(&mut self.queued_messages); + debug.event_processing_finished(); + + let command = if messages.is_empty() { + debug.draw_started(); + self.mouse_interaction = + user_interface.draw(renderer, theme, style, cursor_position); + debug.draw_finished(); + + self.cache = Some(user_interface.into_cache()); + + None + } else { + // When there are messages, we are forced to rebuild twice + // for now :^) + let temp_cache = user_interface.into_cache(); + + let commands = + Command::batch(messages.into_iter().map(|message| { + debug.log_message(&message); + + debug.update_started(); + let command = self.program.update(message); + debug.update_finished(); + + command + })); + + let mut user_interface = build_user_interface( + &mut self.program, + temp_cache, + renderer, + bounds, + debug, + ); + + debug.draw_started(); + self.mouse_interaction = + user_interface.draw(renderer, theme, style, cursor_position); + debug.draw_finished(); + + self.cache = Some(user_interface.into_cache()); + + Some(commands) + }; + + (uncaptured_events, command) + } +} + +fn build_user_interface<'a, P: Program>( + program: &'a mut P, + cache: user_interface::Cache, + renderer: &mut P::Renderer, + size: Size, + debug: &mut Debug, +) -> UserInterface<'a, P::Message, P::Renderer> { + debug.view_started(); + let view = program.view(); + debug.view_finished(); + + debug.layout_started(); + let user_interface = UserInterface::build(view, size, cache, renderer); + debug.layout_finished(); + + user_interface +} diff --git a/runtime/src/system.rs b/runtime/src/system.rs new file mode 100644 index 00000000..61c8ff29 --- /dev/null +++ b/runtime/src/system.rs @@ -0,0 +1,6 @@ +//! Access the native system. +mod action; +mod information; + +pub use action::Action; +pub use information::Information; diff --git a/runtime/src/system/action.rs b/runtime/src/system/action.rs new file mode 100644 index 00000000..dea9536f --- /dev/null +++ b/runtime/src/system/action.rs @@ -0,0 +1,39 @@ +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 new file mode 100644 index 00000000..93e7a5a4 --- /dev/null +++ b/runtime/src/system/information.rs @@ -0,0 +1,29 @@ +/// 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, KB + pub memory_total: u64, + /// Memory used by this process, KB + 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/user_interface.rs b/runtime/src/user_interface.rs new file mode 100644 index 00000000..2c76fd8a --- /dev/null +++ b/runtime/src/user_interface.rs @@ -0,0 +1,592 @@ +//! Implement your own event loop to drive a user interface. +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, Point, Rectangle, Size, Vector}; +use crate::core::{Element, Layout, Shell}; + +/// A set of interactive graphical elements with a specific [`Layout`]. +/// +/// It can be updated and drawn. +/// +/// Iced tries to avoid dictating how to write your event loop. You are in +/// 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. +/// +/// [`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 +#[allow(missing_debug_implementations)] +pub struct UserInterface<'a, Message, Renderer> { + root: Element<'a, Message, Renderer>, + base: layout::Node, + state: widget::Tree, + overlay: Option<layout::Node>, + bounds: Size, +} + +impl<'a, Message, Renderer> UserInterface<'a, Message, Renderer> +where + Renderer: crate::core::Renderer, +{ + /// Builds a user interface for an [`Element`]. + /// + /// It is able to avoid expensive computations when using a [`Cache`] + /// obtained from a previous instance of a [`UserInterface`]. + /// + /// # Example + /// Imagine we want to build a [`UserInterface`] for + /// [the counter example that we previously wrote](index.html#usage). Here + /// is naive way to set up our application loop: + /// + /// ```no_run + /// # mod iced_wgpu { + /// # pub use iced_runtime::core::renderer::Null as Renderer; + /// # } + /// # + /// # pub struct Counter; + /// # + /// # impl Counter { + /// # pub fn new() -> Self { Counter } + /// # 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(); + /// let mut renderer = Renderer::new(); + /// let mut window_size = Size::new(1024.0, 768.0); + /// + /// // Application loop + /// loop { + /// // Process system events here... + /// + /// // Build the user interface + /// let user_interface = UserInterface::build( + /// counter.view(), + /// window_size, + /// cache, + /// &mut renderer, + /// ); + /// + /// // Update and draw the user interface here... + /// // ... + /// + /// // Obtain the cache for the next iteration + /// cache = user_interface.into_cache(); + /// } + /// ``` + pub fn build<E: Into<Element<'a, Message, Renderer>>>( + root: E, + bounds: Size, + cache: Cache, + renderer: &mut Renderer, + ) -> Self { + let root = root.into(); + + let Cache { mut state } = cache; + state.diff(root.as_widget()); + + let base = + renderer.layout(&root, &layout::Limits::new(Size::ZERO, bounds)); + + UserInterface { + root, + base, + state, + overlay: None, + bounds, + } + } + + /// Updates the [`UserInterface`] by processing each provided [`Event`]. + /// + /// It returns __messages__ that may have been produced as a result of user + /// interactions. You should feed these to your __update logic__. + /// + /// # Example + /// Let's allow our [counter](index.html#usage) to change state by + /// completing [the previous example](#example): + /// + /// ```no_run + /// # mod iced_wgpu { + /// # pub use iced_runtime::core::renderer::Null as Renderer; + /// # } + /// # + /// # pub struct Counter; + /// # + /// # impl Counter { + /// # pub fn new() -> Self { Counter } + /// # pub fn view(&self) -> iced_core::Element<(), Renderer> { unimplemented!() } + /// # pub fn update(&mut self, _: ()) {} + /// # } + /// use iced_runtime::core::{clipboard, Size, Point}; + /// 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 clipboard = clipboard::Null; + /// + /// // Initialize our event storage + /// let mut events = Vec::new(); + /// let mut messages = Vec::new(); + /// + /// loop { + /// // Obtain system events... + /// + /// let mut user_interface = UserInterface::build( + /// counter.view(), + /// window_size, + /// cache, + /// &mut renderer, + /// ); + /// + /// // Update the user interface + /// let (state, event_statuses) = user_interface.update( + /// &events, + /// cursor_position, + /// &mut renderer, + /// &mut clipboard, + /// &mut messages + /// ); + /// + /// cache = user_interface.into_cache(); + /// + /// // Process the produced messages + /// for message in messages.drain(..) { + /// counter.update(message); + /// } + /// } + /// ``` + pub fn update( + &mut self, + events: &[Event], + cursor_position: Point, + renderer: &mut Renderer, + clipboard: &mut dyn Clipboard, + messages: &mut Vec<Message>, + ) -> (State, Vec<event::Status>) { + use std::mem::ManuallyDrop; + + 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 (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 event_statuses = Vec::new(); + + for event in events.iter().cloned() { + let mut shell = Shell::new(messages); + + let event_status = overlay.on_event( + event, + Layout::new(&layout), + cursor_position, + renderer, + clipboard, + &mut shell, + ); + + event_statuses.push(event_status); + + match (redraw_request, shell.redraw_request()) { + (None, Some(at)) => { + redraw_request = Some(at); + } + (Some(current), Some(new)) if new < current => { + redraw_request = Some(new); + } + _ => {} + } + + if shell.is_layout_invalid() { + let _ = ManuallyDrop::into_inner(manual_overlay); + + self.base = renderer.layout( + &self.root, + &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, + )); + + if manual_overlay.is_none() { + break; + } + + overlay = manual_overlay.as_mut().unwrap(); + + shell.revalidate_layout(|| { + layout = overlay.layout(renderer, bounds, Vector::ZERO); + }); + } + + if shell.are_widgets_invalid() { + outdated = true; + } + } + + 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) + }) + .unwrap_or(cursor_position); + + self.overlay = Some(layout); + + (base_cursor, event_statuses) + } else { + (cursor_position, vec![event::Status::Ignored; events.len()]) + }; + + let _ = ManuallyDrop::into_inner(manual_overlay); + + let event_statuses = events + .iter() + .cloned() + .zip(overlay_statuses.into_iter()) + .map(|(event, overlay_status)| { + if matches!(overlay_status, event::Status::Captured) { + return overlay_status; + } + + let mut shell = Shell::new(messages); + + let event_status = self.root.as_widget_mut().on_event( + &mut self.state, + event, + Layout::new(&self.base), + base_cursor, + renderer, + clipboard, + &mut shell, + ); + + if matches!(event_status, event::Status::Captured) { + self.overlay = None; + } + + match (redraw_request, shell.redraw_request()) { + (None, Some(at)) => { + redraw_request = Some(at); + } + (Some(current), Some(new)) if new < current => { + redraw_request = Some(new); + } + _ => {} + } + + shell.revalidate_layout(|| { + self.base = renderer.layout( + &self.root, + &layout::Limits::new(Size::ZERO, self.bounds), + ); + + self.overlay = None; + }); + + if shell.are_widgets_invalid() { + outdated = true; + } + + event_status.merge(overlay_status) + }) + .collect(); + + ( + if outdated { + State::Outdated + } else { + State::Updated { redraw_request } + }, + event_statuses, + ) + } + + /// Draws the [`UserInterface`] with the provided [`Renderer`]. + /// + /// It returns the current [`mouse::Interaction`]. You should update the + /// icon of the mouse cursor accordingly in your system. + /// + /// [`Renderer`]: crate::Renderer + /// + /// # Example + /// We can finally draw our [counter](index.html#usage) by + /// [completing the last example](#example-1): + /// + /// ```no_run + /// # mod iced_wgpu { + /// # pub use iced_runtime::core::renderer::Null as Renderer; + /// # pub type Theme = (); + /// # } + /// # + /// # pub struct Counter; + /// # + /// # impl Counter { + /// # pub fn new() -> Self { Counter } + /// # pub fn view(&self) -> Element<(), Renderer> { unimplemented!() } + /// # pub fn update(&mut self, _: ()) {} + /// # } + /// use iced_runtime::core::clipboard; + /// use iced_runtime::core::renderer; + /// use iced_runtime::core::{Element, Size, Point}; + /// 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 clipboard = clipboard::Null; + /// let mut events = Vec::new(); + /// let mut messages = Vec::new(); + /// let mut theme = Theme::default(); + /// + /// loop { + /// // Obtain system events... + /// + /// let mut user_interface = UserInterface::build( + /// counter.view(), + /// window_size, + /// cache, + /// &mut renderer, + /// ); + /// + /// // Update the user interface + /// let event_statuses = user_interface.update( + /// &events, + /// cursor_position, + /// &mut renderer, + /// &mut clipboard, + /// &mut messages + /// ); + /// + /// // Draw the user interface + /// let mouse_cursor = user_interface.draw(&mut renderer, &theme, &renderer::Style::default(), cursor_position); + /// + /// cache = user_interface.into_cache(); + /// + /// for message in messages.drain(..) { + /// counter.update(message); + /// } + /// + /// // Update mouse cursor icon... + /// // Flush rendering operations... + /// } + /// ``` + pub fn draw( + &mut self, + renderer: &mut Renderer, + theme: &Renderer::Theme, + style: &renderer::Style, + cursor_position: Point, + ) -> mouse::Interaction { + // TODO: Move to shell level (?) + renderer.clear(); + + let viewport = Rectangle::with_size(self.bounds); + + let base_cursor = if let Some(overlay) = self + .root + .as_widget_mut() + .overlay(&mut self.state, Layout::new(&self.base), renderer) + { + let overlay_layout = self.overlay.take().unwrap_or_else(|| { + overlay.layout(renderer, self.bounds, Vector::ZERO) + }); + + let new_cursor_position = if overlay + .is_over(Layout::new(&overlay_layout), cursor_position) + { + Point::new(-1.0, -1.0) + } else { + cursor_position + }; + + self.overlay = Some(overlay_layout); + + new_cursor_position + } else { + cursor_position + }; + + self.root.as_widget().draw( + &self.state, + renderer, + theme, + style, + Layout::new(&self.base), + base_cursor, + &viewport, + ); + + let base_interaction = self.root.as_widget().mouse_interaction( + &self.state, + Layout::new(&self.base), + cursor_position, + &viewport, + renderer, + ); + + let Self { + overlay, + root, + base, + .. + } = self; + + // TODO: Currently, we need to call Widget::overlay twice to + // implement the painter's algorithm properly. + // + // Once we have a proper persistent widget tree, we should be able to + // avoid this additional call. + overlay + .as_ref() + .and_then(|layout| { + root.as_widget_mut() + .overlay(&mut self.state, Layout::new(base), renderer) + .map(|overlay| { + let overlay_interaction = overlay.mouse_interaction( + Layout::new(layout), + cursor_position, + &viewport, + renderer, + ); + + let overlay_bounds = layout.bounds(); + + renderer.with_layer(overlay_bounds, |renderer| { + overlay.draw( + renderer, + theme, + style, + Layout::new(layout), + cursor_position, + ); + }); + + if overlay.is_over(Layout::new(layout), cursor_position) + { + overlay_interaction + } else { + base_interaction + } + }) + }) + .unwrap_or(base_interaction) + } + + /// Applies a [`widget::Operation`] to the [`UserInterface`]. + pub fn operate( + &mut self, + renderer: &Renderer, + operation: &mut dyn widget::Operation<Message>, + ) { + self.root.as_widget().operate( + &mut self.state, + Layout::new(&self.base), + renderer, + operation, + ); + + if let Some(mut overlay) = self.root.as_widget_mut().overlay( + &mut self.state, + Layout::new(&self.base), + renderer, + ) { + if self.overlay.is_none() { + self.overlay = + Some(overlay.layout(renderer, self.bounds, Vector::ZERO)); + } + + overlay.operate( + Layout::new(self.overlay.as_ref().unwrap()), + renderer, + operation, + ); + } + } + + /// Relayouts and returns a new [`UserInterface`] using the provided + /// bounds. + pub fn relayout(self, bounds: Size, renderer: &mut Renderer) -> Self { + Self::build(self.root, bounds, Cache { state: self.state }, renderer) + } + + /// Extract the [`Cache`] of the [`UserInterface`], consuming it in the + /// process. + pub fn into_cache(self) -> Cache { + Cache { state: self.state } + } +} + +/// Reusable data of a specific [`UserInterface`]. +#[derive(Debug)] +pub struct Cache { + state: widget::Tree, +} + +impl Cache { + /// Creates an empty [`Cache`]. + /// + /// You should use this to initialize a [`Cache`] before building your first + /// [`UserInterface`]. + pub fn new() -> Cache { + Cache { + state: widget::Tree::empty(), + } + } +} + +impl Default for Cache { + fn default() -> Cache { + Cache::new() + } +} + +/// The resulting state after updating a [`UserInterface`]. +#[derive(Debug, Clone, Copy)] +pub enum State { + /// The [`UserInterface`] is outdated and needs to be rebuilt. + Outdated, + + /// The [`UserInterface`] is up-to-date and can be reused without + /// rebuilding. + Updated { + /// The [`Instant`] when a redraw should be performed. + redraw_request: Option<window::RedrawRequest>, + }, +} diff --git a/runtime/src/window.rs b/runtime/src/window.rs new file mode 100644 index 00000000..236064f7 --- /dev/null +++ b/runtime/src/window.rs @@ -0,0 +1,112 @@ +//! Build window-based GUI applications. +mod action; + +pub use action::Action; + +use crate::command::{self, Command}; +use crate::core::time::Instant; +use crate::core::window::{Event, Mode, UserAttention}; +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>(width: u32, height: u32) -> Command<Message> { + Command::single(command::Action::Window(Action::Resize { width, height })) +} + +/// 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 })) +} + +/// Sets 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 whether or not the window will always be on top of other windows. +pub fn change_always_on_top<Message>(on_top: bool) -> Command<Message> { + Command::single(command::Action::Window(Action::ChangeAlwaysOnTop(on_top))) +} + +/// 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)))) +} diff --git a/runtime/src/window/action.rs b/runtime/src/window/action.rs new file mode 100644 index 00000000..c1dbd84f --- /dev/null +++ b/runtime/src/window/action.rs @@ -0,0 +1,147 @@ +use crate::core::window::{Mode, UserAttention}; +use crate::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, + /// Moves 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, + /// 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 + Maximize(bool), + /// Set the window to minimized or back + Minimize(bool), + /// Move the window. + /// + /// Unsupported on Wayland. + Move { + /// The new logical x location of the window + x: i32, + /// The new logical y location of the window + y: i32, + }, + /// Change the [`Mode`] of the window. + ChangeMode(Mode), + /// Fetch the current [`Mode`] of the window. + FetchMode(Box<dyn FnOnce(Mode) -> T + 'static>), + /// Toggle the window to maximized or back + ToggleMaximize, + /// Toggle whether window has decorations. + /// + /// ## Platform-specific + /// - **X11:** Not implemented. + /// - **Web:** Unsupported. + 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. + /// + /// ## 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(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, + /// Change whether or not the window will always be on top of other windows. + /// + /// ## Platform-specific + /// + /// - **Web / Wayland:** Unsupported. + ChangeAlwaysOnTop(bool), + /// Fetch an identifier unique to the window. + FetchId(Box<dyn FnOnce(u64) -> 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::Close => Action::Close, + Self::Drag => Action::Drag, + Self::Resize { width, height } => Action::Resize { width, height }, + Self::Maximize(maximized) => Action::Maximize(maximized), + Self::Minimize(minimized) => Action::Minimize(minimized), + Self::Move { x, y } => Action::Move { x, y }, + Self::ChangeMode(mode) => Action::ChangeMode(mode), + Self::FetchMode(o) => Action::FetchMode(Box::new(move |s| f(o(s)))), + Self::ToggleMaximize => Action::ToggleMaximize, + Self::ToggleDecorations => Action::ToggleDecorations, + Self::RequestUserAttention(attention_type) => { + Action::RequestUserAttention(attention_type) + } + Self::GainFocus => Action::GainFocus, + Self::ChangeAlwaysOnTop(on_top) => { + Action::ChangeAlwaysOnTop(on_top) + } + Self::FetchId(o) => Action::FetchId(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::Close => write!(f, "Action::Close"), + Self::Drag => write!(f, "Action::Drag"), + Self::Resize { width, height } => write!( + f, + "Action::Resize {{ widget: {width}, height: {height} }}" + ), + Self::Maximize(maximized) => { + write!(f, "Action::Maximize({maximized})") + } + Self::Minimize(minimized) => { + write!(f, "Action::Minimize({minimized}") + } + Self::Move { x, y } => { + write!(f, "Action::Move {{ x: {x}, y: {y} }}") + } + Self::ChangeMode(mode) => write!(f, "Action::SetMode({mode:?})"), + Self::FetchMode(_) => write!(f, "Action::FetchMode"), + Self::ToggleMaximize => write!(f, "Action::ToggleMaximize"), + Self::ToggleDecorations => write!(f, "Action::ToggleDecorations"), + Self::RequestUserAttention(_) => { + write!(f, "Action::RequestUserAttention") + } + Self::GainFocus => write!(f, "Action::GainFocus"), + Self::ChangeAlwaysOnTop(on_top) => { + write!(f, "Action::AlwaysOnTop({on_top})") + } + Self::FetchId(_) => write!(f, "Action::FetchId"), + } + } +} |