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/user_interface.rs | |
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/user_interface.rs')
-rw-r--r-- | runtime/src/user_interface.rs | 592 |
1 files changed, 592 insertions, 0 deletions
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>, + }, +} |