summaryrefslogtreecommitdiffstats
path: root/runtime/src/user_interface.rs
diff options
context:
space:
mode:
authorLibravatar Héctor Ramón Jiménez <hector0193@gmail.com>2023-03-05 06:35:20 +0100
committerLibravatar Héctor Ramón Jiménez <hector0193@gmail.com>2023-03-05 06:35:20 +0100
commit99e0a71504456976ba88040f5d1d3bbc347694ea (patch)
treea228c064fd3847831ff8072aa9375dc59db47f47 /runtime/src/user_interface.rs
parent8af69be47e88896b3c5f70174db609eee0c67971 (diff)
downloadiced-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.rs592
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>,
+ },
+}