diff options
Diffstat (limited to 'runtime')
-rw-r--r-- | runtime/src/lib.rs | 5 | ||||
-rw-r--r-- | runtime/src/overlay.rs | 4 | ||||
-rw-r--r-- | runtime/src/overlay/nested.rs | 353 | ||||
-rw-r--r-- | runtime/src/program/state.rs | 10 | ||||
-rw-r--r-- | runtime/src/user_interface.rs | 152 | ||||
-rw-r--r-- | runtime/src/window.rs | 20 | ||||
-rw-r--r-- | runtime/src/window/action.rs | 35 | ||||
-rw-r--r-- | runtime/src/window/screenshot.rs | 92 |
8 files changed, 584 insertions, 87 deletions
diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 8a277e47..4bbf9687 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -33,7 +33,7 @@ )] #![deny( missing_debug_implementations, - //missing_docs, + missing_docs, unused_results, clippy::extra_unused_lifetimes, clippy::from_over_into, @@ -42,11 +42,12 @@ clippy::useless_conversion )] #![forbid(unsafe_code, rust_2018_idioms)] -#![cfg_attr(docsrs, feature(doc_cfg))] +#![cfg_attr(docsrs, feature(doc_auto_cfg))] pub mod clipboard; pub mod command; pub mod font; pub mod keyboard; +pub mod overlay; pub mod program; pub mod system; pub mod user_interface; 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/runtime/src/program/state.rs b/runtime/src/program/state.rs index 2fa9934d..d83e3f54 100644 --- a/runtime/src/program/state.rs +++ b/runtime/src/program/state.rs @@ -1,7 +1,7 @@ use crate::core::event::{self, Event}; use crate::core::mouse; use crate::core::renderer; -use crate::core::{Clipboard, Point, Size}; +use crate::core::{Clipboard, Size}; use crate::user_interface::{self, UserInterface}; use crate::{Command, Debug, Program}; @@ -88,7 +88,7 @@ where pub fn update( &mut self, bounds: Size, - cursor_position: Point, + cursor: mouse::Cursor, renderer: &mut P::Renderer, theme: &<P::Renderer as iced_core::Renderer>::Theme, style: &renderer::Style, @@ -108,7 +108,7 @@ where let (_, event_statuses) = user_interface.update( &self.queued_events, - cursor_position, + cursor, renderer, clipboard, &mut messages, @@ -131,7 +131,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()); @@ -163,7 +163,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()); diff --git a/runtime/src/user_interface.rs b/runtime/src/user_interface.rs index c29de7db..619423fd 100644 --- a/runtime/src/user_interface.rs +++ b/runtime/src/user_interface.rs @@ -5,8 +5,8 @@ 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}; +use crate::core::{Clipboard, Element, Layout, Point, Rectangle, Shell, Size}; +use crate::overlay; /// A set of interactive graphical elements with a specific [`Layout`]. /// @@ -16,11 +16,10 @@ use crate::core::{Element, Layout, Shell}; /// 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.9/examples/integration_opengl -/// [`integration_wgpu`]: https://github.com/iced-rs/iced/tree/0.9/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>, @@ -129,7 +128,9 @@ where /// # pub fn view(&self) -> iced_core::Element<(), Renderer> { unimplemented!() } /// # pub fn update(&mut self, _: ()) {} /// # } - /// use iced_runtime::core::{clipboard, Size, Point}; + /// 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; /// @@ -137,7 +138,7 @@ where /// 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 @@ -157,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 @@ -174,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>, @@ -184,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() { @@ -204,7 +205,7 @@ where let event_status = overlay.on_event( event, Layout::new(&layout), - cursor_position, + cursor, renderer, clipboard, &mut shell, @@ -230,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; @@ -244,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); }); } @@ -253,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); @@ -360,8 +373,9 @@ where /// # 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, Point}; + /// use iced_runtime::core::{Element, Size}; /// use iced_runtime::user_interface::{self, UserInterface}; /// use iced_wgpu::{Renderer, Theme}; /// @@ -369,7 +383,7 @@ where /// 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(); @@ -388,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, &renderer::Style::default(), cursor_position); + /// let mouse_interaction = user_interface.draw(&mut renderer, &theme, &renderer::Style::default(), cursor); /// /// cache = user_interface.into_cache(); /// @@ -412,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( @@ -456,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, ); @@ -478,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, ); @@ -494,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 { @@ -522,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 index 833a1125..e448edef 100644 --- a/runtime/src/window.rs +++ b/runtime/src/window.rs @@ -1,11 +1,14 @@ //! 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, Mode, UserAttention}; +use crate::core::window::{Event, Icon, Level, Mode, UserAttention}; use crate::futures::subscription::{self, Subscription}; /// Subscribes to the frames of the window of the running application. @@ -53,7 +56,7 @@ 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. +/// Changes the [`Mode`] of the window. pub fn change_mode<Message>(mode: Mode) -> Command<Message> { Command::single(command::Action::Window(Action::ChangeMode(mode))) } @@ -99,9 +102,9 @@ 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))) +/// 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. @@ -115,3 +118,10 @@ pub fn fetch_id<Message>( 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/runtime/src/window/action.rs b/runtime/src/window/action.rs index 83b71c75..09be1810 100644 --- a/runtime/src/window/action.rs +++ b/runtime/src/window/action.rs @@ -1,13 +1,14 @@ -use crate::core::window::{Icon, Mode, UserAttention}; +use crate::core::window::{Icon, Level, Mode, UserAttention}; use crate::futures::MaybeSend; +use crate::window::Screenshot; 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 @@ -20,7 +21,7 @@ pub enum Action<T> { /// The new logical height of the window height: u32, }, - /// Sets the window to maximized or back + /// Set the window to maximized or back Maximize(bool), /// Set the window to minimized or back Minimize(bool), @@ -70,15 +71,11 @@ pub enum Action<T> { /// /// - **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), + /// Change the window [`Level`]. + ChangeLevel(Level), /// Fetch an identifier unique to the window. FetchId(Box<dyn FnOnce(u64) -> T + 'static>), - /// Changes the window [`Icon`]. + /// Change the window [`Icon`]. /// /// On Windows and X11, this is typically the small icon in the top-left /// corner of the titlebar. @@ -93,6 +90,8 @@ pub enum Action<T> { /// - **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> { @@ -119,11 +118,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)) + })) + } } } } @@ -154,13 +156,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, +} |