summaryrefslogtreecommitdiffstats
path: root/core
diff options
context:
space:
mode:
authorLibravatar Héctor Ramón Jiménez <hector0193@gmail.com>2023-03-04 05:37:11 +0100
committerLibravatar Héctor Ramón Jiménez <hector0193@gmail.com>2023-03-04 05:37:11 +0100
commit3a0d34c0240f4421737a6a08761f99d6f8140d02 (patch)
treec9a4a6b8e9c1db1b8fcd05bc98e3f131d5ef4bd5 /core
parentc54409d1711e1f615c7ea4b02c082954e340632a (diff)
downloadiced-3a0d34c0240f4421737a6a08761f99d6f8140d02.tar.gz
iced-3a0d34c0240f4421737a6a08761f99d6f8140d02.tar.bz2
iced-3a0d34c0240f4421737a6a08761f99d6f8140d02.zip
Create `iced_widget` subcrate and re-organize the whole codebase
Diffstat (limited to 'core')
-rw-r--r--core/Cargo.toml1
-rw-r--r--core/src/clipboard.rs23
-rw-r--r--core/src/element.rs608
-rw-r--r--core/src/event.rs78
-rw-r--r--core/src/hasher.rs13
-rw-r--r--core/src/image.rs174
-rw-r--r--core/src/layout.rs65
-rw-r--r--core/src/layout/DRUID_LICENSE202
-rw-r--r--core/src/layout/flex.rs232
-rw-r--r--core/src/layout/limits.rs163
-rw-r--r--core/src/layout/node.rs91
-rw-r--r--core/src/lib.rs24
-rw-r--r--core/src/mouse.rs3
-rw-r--r--core/src/mouse/click.rs76
-rw-r--r--core/src/overlay.rs122
-rw-r--r--core/src/overlay/element.rs270
-rw-r--r--core/src/overlay/group.rs172
-rw-r--r--core/src/renderer.rs99
-rw-r--r--core/src/renderer/null.rs82
-rw-r--r--core/src/shell.rs108
-rw-r--r--core/src/svg.rs89
-rw-r--r--core/src/text.rs111
-rw-r--r--core/src/touch.rs23
-rw-r--r--core/src/widget.rs145
-rw-r--r--core/src/widget/id.rs43
-rw-r--r--core/src/widget/operation.rs112
-rw-r--r--core/src/widget/operation/focusable.rs203
-rw-r--r--core/src/widget/operation/scrollable.rs54
-rw-r--r--core/src/widget/operation/text_input.rs131
-rw-r--r--core/src/widget/text.rs277
-rw-r--r--core/src/widget/tree.rs187
-rw-r--r--core/src/window.rs10
-rw-r--r--core/src/window/event.rs58
-rw-r--r--core/src/window/mode.rs12
-rw-r--r--core/src/window/redraw_request.rs38
-rw-r--r--core/src/window/user_attention.rs21
36 files changed, 4120 insertions, 0 deletions
diff --git a/core/Cargo.toml b/core/Cargo.toml
index 7ccb7b7a..9edc20f6 100644
--- a/core/Cargo.toml
+++ b/core/Cargo.toml
@@ -10,6 +10,7 @@ repository = "https://github.com/iced-rs/iced"
[dependencies]
bitflags = "1.2"
thiserror = "1"
+twox-hash = { version = "1.5", default-features = false }
[dependencies.palette]
version = "0.6"
diff --git a/core/src/clipboard.rs b/core/src/clipboard.rs
new file mode 100644
index 00000000..081b4004
--- /dev/null
+++ b/core/src/clipboard.rs
@@ -0,0 +1,23 @@
+//! Access the clipboard.
+
+/// A buffer for short-term storage and transfer within and between
+/// applications.
+pub trait Clipboard {
+ /// Reads the current content of the [`Clipboard`] as text.
+ fn read(&self) -> Option<String>;
+
+ /// Writes the given text contents to the [`Clipboard`].
+ fn write(&mut self, contents: String);
+}
+
+/// A null implementation of the [`Clipboard`] trait.
+#[derive(Debug, Clone, Copy)]
+pub struct Null;
+
+impl Clipboard for Null {
+ fn read(&self) -> Option<String> {
+ None
+ }
+
+ fn write(&mut self, _contents: String) {}
+}
diff --git a/core/src/element.rs b/core/src/element.rs
new file mode 100644
index 00000000..98c53737
--- /dev/null
+++ b/core/src/element.rs
@@ -0,0 +1,608 @@
+use crate::event::{self, Event};
+use crate::layout;
+use crate::mouse;
+use crate::overlay;
+use crate::renderer;
+use crate::widget;
+use crate::widget::tree::{self, Tree};
+use crate::{
+ Clipboard, Color, Layout, Length, Point, Rectangle, Shell, Widget,
+};
+
+use std::any::Any;
+use std::borrow::Borrow;
+
+/// A generic [`Widget`].
+///
+/// It is useful to build composable user interfaces that do not leak
+/// implementation details in their __view logic__.
+///
+/// If you have a [built-in widget], you should be able to use `Into<Element>`
+/// to turn it into an [`Element`].
+///
+/// [built-in widget]: crate::widget
+#[allow(missing_debug_implementations)]
+pub struct Element<'a, Message, Renderer> {
+ widget: Box<dyn Widget<Message, Renderer> + 'a>,
+}
+
+impl<'a, Message, Renderer> Element<'a, Message, Renderer> {
+ /// Creates a new [`Element`] containing the given [`Widget`].
+ pub fn new(widget: impl Widget<Message, Renderer> + 'a) -> Self
+ where
+ Renderer: crate::Renderer,
+ {
+ Self {
+ widget: Box::new(widget),
+ }
+ }
+
+ /// Returns a reference to the [`Widget`] of the [`Element`],
+ pub fn as_widget(&self) -> &dyn Widget<Message, Renderer> {
+ self.widget.as_ref()
+ }
+
+ /// Returns a mutable reference to the [`Widget`] of the [`Element`],
+ pub fn as_widget_mut(&mut self) -> &mut dyn Widget<Message, Renderer> {
+ self.widget.as_mut()
+ }
+
+ /// Applies a transformation to the produced message of the [`Element`].
+ ///
+ /// This method is useful when you want to decouple different parts of your
+ /// UI and make them __composable__.
+ ///
+ /// # Example
+ /// Imagine we want to use [our counter](index.html#usage). But instead of
+ /// showing a single counter, we want to display many of them. We can reuse
+ /// the `Counter` type as it is!
+ ///
+ /// We use composition to model the __state__ of our new application:
+ ///
+ /// ```
+ /// # mod counter {
+ /// # pub struct Counter;
+ /// # }
+ /// use counter::Counter;
+ ///
+ /// struct ManyCounters {
+ /// counters: Vec<Counter>,
+ /// }
+ /// ```
+ ///
+ /// We can store the state of multiple counters now. However, the
+ /// __messages__ we implemented before describe the user interactions
+ /// of a __single__ counter. Right now, we need to also identify which
+ /// counter is receiving user interactions. Can we use composition again?
+ /// Yes.
+ ///
+ /// ```
+ /// # mod counter {
+ /// # #[derive(Debug, Clone, Copy)]
+ /// # pub enum Message {}
+ /// # }
+ /// #[derive(Debug, Clone, Copy)]
+ /// pub enum Message {
+ /// Counter(usize, counter::Message)
+ /// }
+ /// ```
+ ///
+ /// We compose the previous __messages__ with the index of the counter
+ /// producing them. Let's implement our __view logic__ now:
+ ///
+ /// ```no_run
+ /// # mod counter {
+ /// # #[derive(Debug, Clone, Copy)]
+ /// # pub enum Message {}
+ /// # pub struct Counter;
+ /// #
+ /// # impl Counter {
+ /// # pub fn view(
+ /// # &self,
+ /// # ) -> iced_core::Element<Message, iced_core::renderer::Null> {
+ /// # unimplemented!()
+ /// # }
+ /// # }
+ /// # }
+ /// #
+ /// # mod iced {
+ /// # pub use iced_core::renderer::Null as Renderer;
+ /// # pub use iced_core::Element;
+ /// #
+ /// # pub mod widget {
+ /// # pub struct Row<Message> {
+ /// # _t: std::marker::PhantomData<Message>,
+ /// # }
+ /// #
+ /// # impl<Message> Row<Message> {
+ /// # pub fn new() -> Self {
+ /// # unimplemented!()
+ /// # }
+ /// #
+ /// # pub fn spacing(mut self, _: u32) -> Self {
+ /// # unimplemented!()
+ /// # }
+ /// #
+ /// # pub fn push(
+ /// # mut self,
+ /// # _: iced_core::Element<Message, iced_core::renderer::Null>,
+ /// # ) -> Self {
+ /// # unimplemented!()
+ /// # }
+ /// # }
+ /// # }
+ /// # }
+ /// #
+ /// use counter::Counter;
+ ///
+ /// use iced::widget::Row;
+ /// use iced::{Element, Renderer};
+ ///
+ /// struct ManyCounters {
+ /// counters: Vec<Counter>,
+ /// }
+ ///
+ /// #[derive(Debug, Clone, Copy)]
+ /// pub enum Message {
+ /// Counter(usize, counter::Message),
+ /// }
+ ///
+ /// impl ManyCounters {
+ /// pub fn view(&mut self) -> Row<Message> {
+ /// // We can quickly populate a `Row` by folding over our counters
+ /// self.counters.iter_mut().enumerate().fold(
+ /// Row::new().spacing(20),
+ /// |row, (index, counter)| {
+ /// // We display the counter
+ /// let element: Element<counter::Message, Renderer> =
+ /// counter.view().into();
+ ///
+ /// row.push(
+ /// // Here we turn our `Element<counter::Message>` into
+ /// // an `Element<Message>` by combining the `index` and the
+ /// // message of the `element`.
+ /// element
+ /// .map(move |message| Message::Counter(index, message)),
+ /// )
+ /// },
+ /// )
+ /// }
+ /// }
+ /// ```
+ ///
+ /// Finally, our __update logic__ is pretty straightforward: simple
+ /// delegation.
+ ///
+ /// ```
+ /// # mod counter {
+ /// # #[derive(Debug, Clone, Copy)]
+ /// # pub enum Message {}
+ /// # pub struct Counter;
+ /// #
+ /// # impl Counter {
+ /// # pub fn update(&mut self, _message: Message) {}
+ /// # }
+ /// # }
+ /// #
+ /// # use counter::Counter;
+ /// #
+ /// # struct ManyCounters {
+ /// # counters: Vec<Counter>,
+ /// # }
+ /// #
+ /// # #[derive(Debug, Clone, Copy)]
+ /// # pub enum Message {
+ /// # Counter(usize, counter::Message)
+ /// # }
+ /// impl ManyCounters {
+ /// pub fn update(&mut self, message: Message) {
+ /// match message {
+ /// Message::Counter(index, counter_msg) => {
+ /// if let Some(counter) = self.counters.get_mut(index) {
+ /// counter.update(counter_msg);
+ /// }
+ /// }
+ /// }
+ /// }
+ /// }
+ /// ```
+ pub fn map<B>(
+ self,
+ f: impl Fn(Message) -> B + 'a,
+ ) -> Element<'a, B, Renderer>
+ where
+ Message: 'a,
+ Renderer: crate::Renderer + 'a,
+ B: 'a,
+ {
+ Element::new(Map::new(self.widget, f))
+ }
+
+ /// Marks the [`Element`] as _to-be-explained_.
+ ///
+ /// The [`Renderer`] will explain the layout of the [`Element`] graphically.
+ /// This can be very useful for debugging your layout!
+ ///
+ /// [`Renderer`]: crate::Renderer
+ pub fn explain<C: Into<Color>>(
+ self,
+ color: C,
+ ) -> Element<'a, Message, Renderer>
+ where
+ Message: 'static,
+ Renderer: crate::Renderer + 'a,
+ {
+ Element {
+ widget: Box::new(Explain::new(self, color.into())),
+ }
+ }
+}
+
+impl<'a, Message, Renderer> Borrow<dyn Widget<Message, Renderer> + 'a>
+ for Element<'a, Message, Renderer>
+{
+ fn borrow(&self) -> &(dyn Widget<Message, Renderer> + 'a) {
+ self.widget.borrow()
+ }
+}
+
+impl<'a, Message, Renderer> Borrow<dyn Widget<Message, Renderer> + 'a>
+ for &Element<'a, Message, Renderer>
+{
+ fn borrow(&self) -> &(dyn Widget<Message, Renderer> + 'a) {
+ self.widget.borrow()
+ }
+}
+
+struct Map<'a, A, B, Renderer> {
+ widget: Box<dyn Widget<A, Renderer> + 'a>,
+ mapper: Box<dyn Fn(A) -> B + 'a>,
+}
+
+impl<'a, A, B, Renderer> Map<'a, A, B, Renderer> {
+ pub fn new<F>(
+ widget: Box<dyn Widget<A, Renderer> + 'a>,
+ mapper: F,
+ ) -> Map<'a, A, B, Renderer>
+ where
+ F: 'a + Fn(A) -> B,
+ {
+ Map {
+ widget,
+ mapper: Box::new(mapper),
+ }
+ }
+}
+
+impl<'a, A, B, Renderer> Widget<B, Renderer> for Map<'a, A, B, Renderer>
+where
+ Renderer: crate::Renderer + 'a,
+ A: 'a,
+ B: 'a,
+{
+ fn tag(&self) -> tree::Tag {
+ self.widget.tag()
+ }
+
+ fn state(&self) -> tree::State {
+ self.widget.state()
+ }
+
+ fn children(&self) -> Vec<Tree> {
+ self.widget.children()
+ }
+
+ fn diff(&self, tree: &mut Tree) {
+ self.widget.diff(tree)
+ }
+
+ fn width(&self) -> Length {
+ self.widget.width()
+ }
+
+ fn height(&self) -> Length {
+ self.widget.height()
+ }
+
+ fn layout(
+ &self,
+ renderer: &Renderer,
+ limits: &layout::Limits,
+ ) -> layout::Node {
+ self.widget.layout(renderer, limits)
+ }
+
+ fn operate(
+ &self,
+ tree: &mut Tree,
+ layout: Layout<'_>,
+ renderer: &Renderer,
+ operation: &mut dyn widget::Operation<B>,
+ ) {
+ struct MapOperation<'a, B> {
+ operation: &'a mut dyn widget::Operation<B>,
+ }
+
+ impl<'a, T, B> widget::Operation<T> for MapOperation<'a, B> {
+ fn container(
+ &mut self,
+ id: Option<&widget::Id>,
+ operate_on_children: &mut dyn FnMut(
+ &mut dyn widget::Operation<T>,
+ ),
+ ) {
+ self.operation.container(id, &mut |operation| {
+ operate_on_children(&mut MapOperation { operation });
+ });
+ }
+
+ fn focusable(
+ &mut self,
+ state: &mut dyn widget::operation::Focusable,
+ id: Option<&widget::Id>,
+ ) {
+ self.operation.focusable(state, id);
+ }
+
+ fn scrollable(
+ &mut self,
+ state: &mut dyn widget::operation::Scrollable,
+ id: Option<&widget::Id>,
+ ) {
+ self.operation.scrollable(state, id);
+ }
+
+ fn text_input(
+ &mut self,
+ state: &mut dyn widget::operation::TextInput,
+ id: Option<&widget::Id>,
+ ) {
+ self.operation.text_input(state, id);
+ }
+
+ fn custom(&mut self, state: &mut dyn Any, id: Option<&widget::Id>) {
+ self.operation.custom(state, id);
+ }
+ }
+
+ self.widget.operate(
+ tree,
+ layout,
+ renderer,
+ &mut MapOperation { operation },
+ );
+ }
+
+ fn on_event(
+ &mut self,
+ tree: &mut Tree,
+ event: Event,
+ layout: Layout<'_>,
+ cursor_position: Point,
+ renderer: &Renderer,
+ clipboard: &mut dyn Clipboard,
+ shell: &mut Shell<'_, B>,
+ ) -> event::Status {
+ let mut local_messages = Vec::new();
+ let mut local_shell = Shell::new(&mut local_messages);
+
+ let status = self.widget.on_event(
+ tree,
+ event,
+ layout,
+ cursor_position,
+ renderer,
+ clipboard,
+ &mut local_shell,
+ );
+
+ shell.merge(local_shell, &self.mapper);
+
+ status
+ }
+
+ fn draw(
+ &self,
+ tree: &Tree,
+ renderer: &mut Renderer,
+ theme: &Renderer::Theme,
+ style: &renderer::Style,
+ layout: Layout<'_>,
+ cursor_position: Point,
+ viewport: &Rectangle,
+ ) {
+ self.widget.draw(
+ tree,
+ renderer,
+ theme,
+ style,
+ layout,
+ cursor_position,
+ viewport,
+ )
+ }
+
+ fn mouse_interaction(
+ &self,
+ tree: &Tree,
+ layout: Layout<'_>,
+ cursor_position: Point,
+ viewport: &Rectangle,
+ renderer: &Renderer,
+ ) -> mouse::Interaction {
+ self.widget.mouse_interaction(
+ tree,
+ layout,
+ cursor_position,
+ viewport,
+ renderer,
+ )
+ }
+
+ fn overlay<'b>(
+ &'b mut self,
+ tree: &'b mut Tree,
+ layout: Layout<'_>,
+ renderer: &Renderer,
+ ) -> Option<overlay::Element<'b, B, Renderer>> {
+ let mapper = &self.mapper;
+
+ self.widget
+ .overlay(tree, layout, renderer)
+ .map(move |overlay| overlay.map(mapper))
+ }
+}
+
+struct Explain<'a, Message, Renderer: crate::Renderer> {
+ element: Element<'a, Message, Renderer>,
+ color: Color,
+}
+
+impl<'a, Message, Renderer> Explain<'a, Message, Renderer>
+where
+ Renderer: crate::Renderer,
+{
+ fn new(element: Element<'a, Message, Renderer>, color: Color) -> Self {
+ Explain { element, color }
+ }
+}
+
+impl<'a, Message, Renderer> Widget<Message, Renderer>
+ for Explain<'a, Message, Renderer>
+where
+ Renderer: crate::Renderer,
+{
+ fn width(&self) -> Length {
+ self.element.widget.width()
+ }
+
+ fn height(&self) -> Length {
+ self.element.widget.height()
+ }
+
+ fn tag(&self) -> tree::Tag {
+ self.element.widget.tag()
+ }
+
+ fn state(&self) -> tree::State {
+ self.element.widget.state()
+ }
+
+ fn children(&self) -> Vec<Tree> {
+ self.element.widget.children()
+ }
+
+ fn diff(&self, tree: &mut Tree) {
+ self.element.widget.diff(tree);
+ }
+
+ fn layout(
+ &self,
+ renderer: &Renderer,
+ limits: &layout::Limits,
+ ) -> layout::Node {
+ self.element.widget.layout(renderer, limits)
+ }
+
+ fn operate(
+ &self,
+ state: &mut Tree,
+ layout: Layout<'_>,
+ renderer: &Renderer,
+ operation: &mut dyn widget::Operation<Message>,
+ ) {
+ self.element
+ .widget
+ .operate(state, layout, renderer, operation)
+ }
+
+ fn on_event(
+ &mut self,
+ state: &mut Tree,
+ event: Event,
+ layout: Layout<'_>,
+ cursor_position: Point,
+ renderer: &Renderer,
+ clipboard: &mut dyn Clipboard,
+ shell: &mut Shell<'_, Message>,
+ ) -> event::Status {
+ self.element.widget.on_event(
+ state,
+ event,
+ layout,
+ cursor_position,
+ renderer,
+ clipboard,
+ shell,
+ )
+ }
+
+ fn draw(
+ &self,
+ state: &Tree,
+ renderer: &mut Renderer,
+ theme: &Renderer::Theme,
+ style: &renderer::Style,
+ layout: Layout<'_>,
+ cursor_position: Point,
+ viewport: &Rectangle,
+ ) {
+ fn explain_layout<Renderer: crate::Renderer>(
+ renderer: &mut Renderer,
+ color: Color,
+ layout: Layout<'_>,
+ ) {
+ renderer.fill_quad(
+ renderer::Quad {
+ bounds: layout.bounds(),
+ border_color: color,
+ border_width: 1.0,
+ border_radius: 0.0.into(),
+ },
+ Color::TRANSPARENT,
+ );
+
+ for child in layout.children() {
+ explain_layout(renderer, color, child);
+ }
+ }
+
+ self.element.widget.draw(
+ state,
+ renderer,
+ theme,
+ style,
+ layout,
+ cursor_position,
+ viewport,
+ );
+
+ explain_layout(renderer, self.color, layout);
+ }
+
+ fn mouse_interaction(
+ &self,
+ state: &Tree,
+ layout: Layout<'_>,
+ cursor_position: Point,
+ viewport: &Rectangle,
+ renderer: &Renderer,
+ ) -> mouse::Interaction {
+ self.element.widget.mouse_interaction(
+ state,
+ layout,
+ cursor_position,
+ viewport,
+ renderer,
+ )
+ }
+
+ fn overlay<'b>(
+ &'b mut self,
+ state: &'b mut Tree,
+ layout: Layout<'_>,
+ renderer: &Renderer,
+ ) -> Option<overlay::Element<'b, Message, Renderer>> {
+ self.element.widget.overlay(state, layout, renderer)
+ }
+}
diff --git a/core/src/event.rs b/core/src/event.rs
new file mode 100644
index 00000000..953cd73f
--- /dev/null
+++ b/core/src/event.rs
@@ -0,0 +1,78 @@
+//! Handle events of a user interface.
+use crate::keyboard;
+use crate::mouse;
+use crate::touch;
+use crate::window;
+
+/// A user interface event.
+///
+/// _**Note:** This type is largely incomplete! If you need to track
+/// additional events, feel free to [open an issue] and share your use case!_
+///
+/// [open an issue]: https://github.com/iced-rs/iced/issues
+#[derive(Debug, Clone, PartialEq)]
+pub enum Event {
+ /// A keyboard event
+ Keyboard(keyboard::Event),
+
+ /// A mouse event
+ Mouse(mouse::Event),
+
+ /// A window event
+ Window(window::Event),
+
+ /// A touch event
+ Touch(touch::Event),
+
+ /// A platform specific event
+ PlatformSpecific(PlatformSpecific),
+}
+
+/// A platform specific event
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub enum PlatformSpecific {
+ /// A MacOS specific event
+ MacOS(MacOS),
+}
+
+/// Describes an event specific to MacOS
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub enum MacOS {
+ /// Triggered when the app receives an URL from the system
+ ///
+ /// _**Note:** For this event to be triggered, the executable needs to be properly [bundled]!_
+ ///
+ /// [bundled]: https://developer.apple.com/library/archive/documentation/CoreFoundation/Conceptual/CFBundles/BundleTypes/BundleTypes.html#//apple_ref/doc/uid/10000123i-CH101-SW19
+ ReceivedUrl(String),
+}
+
+/// The status of an [`Event`] after being processed.
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum Status {
+ /// The [`Event`] was **NOT** handled by any widget.
+ Ignored,
+
+ /// The [`Event`] was handled and processed by a widget.
+ Captured,
+}
+
+impl Status {
+ /// Merges two [`Status`] into one.
+ ///
+ /// `Captured` takes precedence over `Ignored`:
+ ///
+ /// ```
+ /// use iced_core::event::Status;
+ ///
+ /// assert_eq!(Status::Ignored.merge(Status::Ignored), Status::Ignored);
+ /// assert_eq!(Status::Ignored.merge(Status::Captured), Status::Captured);
+ /// assert_eq!(Status::Captured.merge(Status::Ignored), Status::Captured);
+ /// assert_eq!(Status::Captured.merge(Status::Captured), Status::Captured);
+ /// ```
+ pub fn merge(self, b: Self) -> Self {
+ match self {
+ Status::Ignored => b,
+ Status::Captured => Status::Captured,
+ }
+ }
+}
diff --git a/core/src/hasher.rs b/core/src/hasher.rs
new file mode 100644
index 00000000..fa52f16d
--- /dev/null
+++ b/core/src/hasher.rs
@@ -0,0 +1,13 @@
+/// The hasher used to compare layouts.
+#[derive(Debug, Default)]
+pub struct Hasher(twox_hash::XxHash64);
+
+impl core::hash::Hasher for Hasher {
+ fn write(&mut self, bytes: &[u8]) {
+ self.0.write(bytes)
+ }
+
+ fn finish(&self) -> u64 {
+ self.0.finish()
+ }
+}
diff --git a/core/src/image.rs b/core/src/image.rs
new file mode 100644
index 00000000..70fbade0
--- /dev/null
+++ b/core/src/image.rs
@@ -0,0 +1,174 @@
+//! Load and draw raster graphics.
+use crate::{Hasher, Rectangle, Size};
+
+use std::hash::{Hash, Hasher as _};
+use std::path::PathBuf;
+use std::sync::Arc;
+
+/// A handle of some image data.
+#[derive(Debug, Clone)]
+pub struct Handle {
+ id: u64,
+ data: Data,
+}
+
+impl Handle {
+ /// Creates an image [`Handle`] pointing to the image of the given path.
+ ///
+ /// Makes an educated guess about the image format by examining the data in the file.
+ pub fn from_path<T: Into<PathBuf>>(path: T) -> Handle {
+ Self::from_data(Data::Path(path.into()))
+ }
+
+ /// Creates an image [`Handle`] containing the image pixels directly. This
+ /// function expects the input data to be provided as a `Vec<u8>` of RGBA
+ /// pixels.
+ ///
+ /// This is useful if you have already decoded your image.
+ pub fn from_pixels(
+ width: u32,
+ height: u32,
+ pixels: impl AsRef<[u8]> + Send + Sync + 'static,
+ ) -> Handle {
+ Self::from_data(Data::Rgba {
+ width,
+ height,
+ pixels: Bytes::new(pixels),
+ })
+ }
+
+ /// Creates an image [`Handle`] containing the image data directly.
+ ///
+ /// Makes an educated guess about the image format by examining the given data.
+ ///
+ /// This is useful if you already have your image loaded in-memory, maybe
+ /// because you downloaded or generated it procedurally.
+ pub fn from_memory(
+ bytes: impl AsRef<[u8]> + Send + Sync + 'static,
+ ) -> Handle {
+ Self::from_data(Data::Bytes(Bytes::new(bytes)))
+ }
+
+ fn from_data(data: Data) -> Handle {
+ let mut hasher = Hasher::default();
+ data.hash(&mut hasher);
+
+ Handle {
+ id: hasher.finish(),
+ data,
+ }
+ }
+
+ /// Returns the unique identifier of the [`Handle`].
+ pub fn id(&self) -> u64 {
+ self.id
+ }
+
+ /// Returns a reference to the image [`Data`].
+ pub fn data(&self) -> &Data {
+ &self.data
+ }
+}
+
+impl<T> From<T> for Handle
+where
+ T: Into<PathBuf>,
+{
+ fn from(path: T) -> Handle {
+ Handle::from_path(path.into())
+ }
+}
+
+impl Hash for Handle {
+ fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
+ self.id.hash(state);
+ }
+}
+
+/// A wrapper around raw image data.
+///
+/// It behaves like a `&[u8]`.
+#[derive(Clone)]
+pub struct Bytes(Arc<dyn AsRef<[u8]> + Send + Sync + 'static>);
+
+impl Bytes {
+ /// Creates new [`Bytes`] around `data`.
+ pub fn new(data: impl AsRef<[u8]> + Send + Sync + 'static) -> Self {
+ Self(Arc::new(data))
+ }
+}
+
+impl std::fmt::Debug for Bytes {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ self.0.as_ref().as_ref().fmt(f)
+ }
+}
+
+impl std::hash::Hash for Bytes {
+ fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
+ self.0.as_ref().as_ref().hash(state);
+ }
+}
+
+impl AsRef<[u8]> for Bytes {
+ fn as_ref(&self) -> &[u8] {
+ self.0.as_ref().as_ref()
+ }
+}
+
+impl std::ops::Deref for Bytes {
+ type Target = [u8];
+
+ fn deref(&self) -> &[u8] {
+ self.0.as_ref().as_ref()
+ }
+}
+
+/// The data of a raster image.
+#[derive(Clone, Hash)]
+pub enum Data {
+ /// File data
+ Path(PathBuf),
+
+ /// In-memory data
+ Bytes(Bytes),
+
+ /// Decoded image pixels in RGBA format.
+ Rgba {
+ /// The width of the image.
+ width: u32,
+ /// The height of the image.
+ height: u32,
+ /// The pixels.
+ pixels: Bytes,
+ },
+}
+
+impl std::fmt::Debug for Data {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ match self {
+ Data::Path(path) => write!(f, "Path({path:?})"),
+ Data::Bytes(_) => write!(f, "Bytes(...)"),
+ Data::Rgba { width, height, .. } => {
+ write!(f, "Pixels({width} * {height})")
+ }
+ }
+ }
+}
+
+/// A [`Renderer`] that can render raster graphics.
+///
+/// [renderer]: crate::renderer
+pub trait Renderer: crate::Renderer {
+ /// The image Handle to be displayed. Iced exposes its own default implementation of a [`Handle`]
+ ///
+ /// [`Handle`]: Self::Handle
+ type Handle: Clone + Hash;
+
+ /// Returns the dimensions of an image for the given [`Handle`].
+ fn dimensions(&self, handle: &Self::Handle) -> Size<u32>;
+
+ /// Draws an image with the given [`Handle`] and inside the provided
+ /// `bounds`.
+ fn draw(&mut self, handle: Self::Handle, bounds: Rectangle);
+}
diff --git a/core/src/layout.rs b/core/src/layout.rs
new file mode 100644
index 00000000..04954fb9
--- /dev/null
+++ b/core/src/layout.rs
@@ -0,0 +1,65 @@
+//! Position your widgets properly.
+mod limits;
+mod node;
+
+pub mod flex;
+
+pub use limits::Limits;
+pub use node::Node;
+
+use crate::{Point, Rectangle, Vector};
+
+/// The bounds of a [`Node`] and its children, using absolute coordinates.
+#[derive(Debug, Clone, Copy)]
+pub struct Layout<'a> {
+ position: Point,
+ node: &'a Node,
+}
+
+impl<'a> Layout<'a> {
+ /// Creates a new [`Layout`] for the given [`Node`] at the origin.
+ pub fn new(node: &'a Node) -> Self {
+ Self::with_offset(Vector::new(0.0, 0.0), node)
+ }
+
+ /// Creates a new [`Layout`] for the given [`Node`] with the provided offset
+ /// from the origin.
+ pub fn with_offset(offset: Vector, node: &'a Node) -> Self {
+ let bounds = node.bounds();
+
+ Self {
+ position: Point::new(bounds.x, bounds.y) + offset,
+ node,
+ }
+ }
+
+ /// Returns the position of the [`Layout`].
+ pub fn position(&self) -> Point {
+ self.position
+ }
+
+ /// Returns the bounds of the [`Layout`].
+ ///
+ /// The returned [`Rectangle`] describes the position and size of a
+ /// [`Node`].
+ pub fn bounds(&self) -> Rectangle {
+ let bounds = self.node.bounds();
+
+ Rectangle {
+ x: self.position.x,
+ y: self.position.y,
+ width: bounds.width,
+ height: bounds.height,
+ }
+ }
+
+ /// Returns an iterator over the [`Layout`] of the children of a [`Node`].
+ pub fn children(self) -> impl Iterator<Item = Layout<'a>> {
+ self.node.children().iter().map(move |node| {
+ Layout::with_offset(
+ Vector::new(self.position.x, self.position.y),
+ node,
+ )
+ })
+ }
+}
diff --git a/core/src/layout/DRUID_LICENSE b/core/src/layout/DRUID_LICENSE
new file mode 100644
index 00000000..d6456956
--- /dev/null
+++ b/core/src/layout/DRUID_LICENSE
@@ -0,0 +1,202 @@
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/core/src/layout/flex.rs b/core/src/layout/flex.rs
new file mode 100644
index 00000000..5d70c2fc
--- /dev/null
+++ b/core/src/layout/flex.rs
@@ -0,0 +1,232 @@
+//! Distribute elements using a flex-based layout.
+// This code is heavily inspired by the [`druid`] codebase.
+//
+// [`druid`]: https://github.com/xi-editor/druid
+//
+// Copyright 2018 The xi-editor Authors, Héctor Ramón
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+use crate::Element;
+
+use crate::layout::{Limits, Node};
+use crate::{Alignment, Padding, Point, Size};
+
+/// The main axis of a flex layout.
+#[derive(Debug)]
+pub enum Axis {
+ /// The horizontal axis
+ Horizontal,
+
+ /// The vertical axis
+ Vertical,
+}
+
+impl Axis {
+ fn main(&self, size: Size) -> f32 {
+ match self {
+ Axis::Horizontal => size.width,
+ Axis::Vertical => size.height,
+ }
+ }
+
+ fn cross(&self, size: Size) -> f32 {
+ match self {
+ Axis::Horizontal => size.height,
+ Axis::Vertical => size.width,
+ }
+ }
+
+ fn pack(&self, main: f32, cross: f32) -> (f32, f32) {
+ match self {
+ Axis::Horizontal => (main, cross),
+ Axis::Vertical => (cross, main),
+ }
+ }
+}
+
+/// Computes the flex layout with the given axis and limits, applying spacing,
+/// padding and alignment to the items as needed.
+///
+/// It returns a new layout [`Node`].
+pub fn resolve<Message, Renderer>(
+ axis: Axis,
+ renderer: &Renderer,
+ limits: &Limits,
+ padding: Padding,
+ spacing: f32,
+ align_items: Alignment,
+ items: &[Element<'_, Message, Renderer>],
+) -> Node
+where
+ Renderer: crate::Renderer,
+{
+ let limits = limits.pad(padding);
+ let total_spacing = spacing * items.len().saturating_sub(1) as f32;
+ let max_cross = axis.cross(limits.max());
+
+ let mut fill_sum = 0;
+ let mut cross = axis.cross(limits.min()).max(axis.cross(limits.fill()));
+ let mut available = axis.main(limits.max()) - total_spacing;
+
+ let mut nodes: Vec<Node> = Vec::with_capacity(items.len());
+ nodes.resize(items.len(), Node::default());
+
+ if align_items == Alignment::Fill {
+ let mut fill_cross = axis.cross(limits.min());
+
+ items.iter().for_each(|child| {
+ let cross_fill_factor = match axis {
+ Axis::Horizontal => child.as_widget().height(),
+ Axis::Vertical => child.as_widget().width(),
+ }
+ .fill_factor();
+
+ if cross_fill_factor == 0 {
+ let (max_width, max_height) = axis.pack(available, max_cross);
+
+ let child_limits =
+ Limits::new(Size::ZERO, Size::new(max_width, max_height));
+
+ let layout = child.as_widget().layout(renderer, &child_limits);
+ let size = layout.size();
+
+ fill_cross = fill_cross.max(axis.cross(size));
+ }
+ });
+
+ cross = fill_cross;
+ }
+
+ for (i, child) in items.iter().enumerate() {
+ let fill_factor = match axis {
+ Axis::Horizontal => child.as_widget().width(),
+ Axis::Vertical => child.as_widget().height(),
+ }
+ .fill_factor();
+
+ if fill_factor == 0 {
+ let (min_width, min_height) = if align_items == Alignment::Fill {
+ axis.pack(0.0, cross)
+ } else {
+ axis.pack(0.0, 0.0)
+ };
+
+ let (max_width, max_height) = if align_items == Alignment::Fill {
+ axis.pack(available, cross)
+ } else {
+ axis.pack(available, max_cross)
+ };
+
+ let child_limits = Limits::new(
+ Size::new(min_width, min_height),
+ Size::new(max_width, max_height),
+ );
+
+ let layout = child.as_widget().layout(renderer, &child_limits);
+ let size = layout.size();
+
+ available -= axis.main(size);
+
+ if align_items != Alignment::Fill {
+ cross = cross.max(axis.cross(size));
+ }
+
+ nodes[i] = layout;
+ } else {
+ fill_sum += fill_factor;
+ }
+ }
+
+ let remaining = available.max(0.0);
+
+ for (i, child) in items.iter().enumerate() {
+ let fill_factor = match axis {
+ Axis::Horizontal => child.as_widget().width(),
+ Axis::Vertical => child.as_widget().height(),
+ }
+ .fill_factor();
+
+ if fill_factor != 0 {
+ let max_main = remaining * fill_factor as f32 / fill_sum as f32;
+ let min_main = if max_main.is_infinite() {
+ 0.0
+ } else {
+ max_main
+ };
+
+ let (min_width, min_height) = if align_items == Alignment::Fill {
+ axis.pack(min_main, cross)
+ } else {
+ axis.pack(min_main, axis.cross(limits.min()))
+ };
+
+ let (max_width, max_height) = if align_items == Alignment::Fill {
+ axis.pack(max_main, cross)
+ } else {
+ axis.pack(max_main, max_cross)
+ };
+
+ let child_limits = Limits::new(
+ Size::new(min_width, min_height),
+ Size::new(max_width, max_height),
+ );
+
+ let layout = child.as_widget().layout(renderer, &child_limits);
+
+ if align_items != Alignment::Fill {
+ cross = cross.max(axis.cross(layout.size()));
+ }
+
+ nodes[i] = layout;
+ }
+ }
+
+ let pad = axis.pack(padding.left, padding.top);
+ let mut main = pad.0;
+
+ for (i, node) in nodes.iter_mut().enumerate() {
+ if i > 0 {
+ main += spacing;
+ }
+
+ let (x, y) = axis.pack(main, pad.1);
+
+ node.move_to(Point::new(x, y));
+
+ match axis {
+ Axis::Horizontal => {
+ node.align(
+ Alignment::Start,
+ align_items,
+ Size::new(0.0, cross),
+ );
+ }
+ Axis::Vertical => {
+ node.align(
+ align_items,
+ Alignment::Start,
+ Size::new(cross, 0.0),
+ );
+ }
+ }
+
+ let size = node.size();
+
+ main += axis.main(size);
+ }
+
+ let (width, height) = axis.pack(main - pad.0, cross);
+ let size = limits.resolve(Size::new(width, height));
+
+ Node::with_children(size.pad(padding), nodes)
+}
diff --git a/core/src/layout/limits.rs b/core/src/layout/limits.rs
new file mode 100644
index 00000000..5d3c1556
--- /dev/null
+++ b/core/src/layout/limits.rs
@@ -0,0 +1,163 @@
+#![allow(clippy::manual_clamp)]
+use crate::{Length, Padding, Size};
+
+/// A set of size constraints for layouting.
+#[derive(Debug, Clone, Copy)]
+pub struct Limits {
+ min: Size,
+ max: Size,
+ fill: Size,
+}
+
+impl Limits {
+ /// No limits
+ pub const NONE: Limits = Limits {
+ min: Size::ZERO,
+ max: Size::INFINITY,
+ fill: Size::INFINITY,
+ };
+
+ /// Creates new [`Limits`] with the given minimum and maximum [`Size`].
+ pub const fn new(min: Size, max: Size) -> Limits {
+ Limits {
+ min,
+ max,
+ fill: Size::INFINITY,
+ }
+ }
+
+ /// Returns the minimum [`Size`] of the [`Limits`].
+ pub fn min(&self) -> Size {
+ self.min
+ }
+
+ /// Returns the maximum [`Size`] of the [`Limits`].
+ pub fn max(&self) -> Size {
+ self.max
+ }
+
+ /// Returns the fill [`Size`] of the [`Limits`].
+ pub fn fill(&self) -> Size {
+ self.fill
+ }
+
+ /// Applies a width constraint to the current [`Limits`].
+ pub fn width(mut self, width: impl Into<Length>) -> Limits {
+ match width.into() {
+ Length::Shrink => {
+ self.fill.width = self.min.width;
+ }
+ Length::Fill | Length::FillPortion(_) => {
+ self.fill.width = self.fill.width.min(self.max.width);
+ }
+ Length::Fixed(amount) => {
+ let new_width = amount.min(self.max.width).max(self.min.width);
+
+ self.min.width = new_width;
+ self.max.width = new_width;
+ self.fill.width = new_width;
+ }
+ }
+
+ self
+ }
+
+ /// Applies a height constraint to the current [`Limits`].
+ pub fn height(mut self, height: impl Into<Length>) -> Limits {
+ match height.into() {
+ Length::Shrink => {
+ self.fill.height = self.min.height;
+ }
+ Length::Fill | Length::FillPortion(_) => {
+ self.fill.height = self.fill.height.min(self.max.height);
+ }
+ Length::Fixed(amount) => {
+ let new_height =
+ amount.min(self.max.height).max(self.min.height);
+
+ self.min.height = new_height;
+ self.max.height = new_height;
+ self.fill.height = new_height;
+ }
+ }
+
+ self
+ }
+
+ /// Applies a minimum width constraint to the current [`Limits`].
+ pub fn min_width(mut self, min_width: f32) -> Limits {
+ self.min.width = self.min.width.max(min_width).min(self.max.width);
+
+ self
+ }
+
+ /// Applies a maximum width constraint to the current [`Limits`].
+ pub fn max_width(mut self, max_width: f32) -> Limits {
+ self.max.width = self.max.width.min(max_width).max(self.min.width);
+
+ self
+ }
+
+ /// Applies a minimum height constraint to the current [`Limits`].
+ pub fn min_height(mut self, min_height: f32) -> Limits {
+ self.min.height = self.min.height.max(min_height).min(self.max.height);
+
+ self
+ }
+
+ /// Applies a maximum height constraint to the current [`Limits`].
+ pub fn max_height(mut self, max_height: f32) -> Limits {
+ self.max.height = self.max.height.min(max_height).max(self.min.height);
+
+ self
+ }
+
+ /// Shrinks the current [`Limits`] to account for the given padding.
+ pub fn pad(&self, padding: Padding) -> Limits {
+ self.shrink(Size::new(padding.horizontal(), padding.vertical()))
+ }
+
+ /// Shrinks the current [`Limits`] by the given [`Size`].
+ pub fn shrink(&self, size: Size) -> Limits {
+ let min = Size::new(
+ (self.min().width - size.width).max(0.0),
+ (self.min().height - size.height).max(0.0),
+ );
+
+ let max = Size::new(
+ (self.max().width - size.width).max(0.0),
+ (self.max().height - size.height).max(0.0),
+ );
+
+ let fill = Size::new(
+ (self.fill.width - size.width).max(0.0),
+ (self.fill.height - size.height).max(0.0),
+ );
+
+ Limits { min, max, fill }
+ }
+
+ /// Removes the minimum width constraint for the current [`Limits`].
+ pub fn loose(&self) -> Limits {
+ Limits {
+ min: Size::ZERO,
+ max: self.max,
+ fill: self.fill,
+ }
+ }
+
+ /// Computes the resulting [`Size`] that fits the [`Limits`] given the
+ /// intrinsic size of some content.
+ pub fn resolve(&self, intrinsic_size: Size) -> Size {
+ Size::new(
+ intrinsic_size
+ .width
+ .min(self.max.width)
+ .max(self.fill.width),
+ intrinsic_size
+ .height
+ .min(self.max.height)
+ .max(self.fill.height),
+ )
+ }
+}
diff --git a/core/src/layout/node.rs b/core/src/layout/node.rs
new file mode 100644
index 00000000..e0c7dcb2
--- /dev/null
+++ b/core/src/layout/node.rs
@@ -0,0 +1,91 @@
+use crate::{Alignment, Point, Rectangle, Size, Vector};
+
+/// The bounds of an element and its children.
+#[derive(Debug, Clone, Default)]
+pub struct Node {
+ bounds: Rectangle,
+ children: Vec<Node>,
+}
+
+impl Node {
+ /// Creates a new [`Node`] with the given [`Size`].
+ pub const fn new(size: Size) -> Self {
+ Self::with_children(size, Vec::new())
+ }
+
+ /// Creates a new [`Node`] with the given [`Size`] and children.
+ pub const fn with_children(size: Size, children: Vec<Node>) -> Self {
+ Node {
+ bounds: Rectangle {
+ x: 0.0,
+ y: 0.0,
+ width: size.width,
+ height: size.height,
+ },
+ children,
+ }
+ }
+
+ /// Returns the [`Size`] of the [`Node`].
+ pub fn size(&self) -> Size {
+ Size::new(self.bounds.width, self.bounds.height)
+ }
+
+ /// Returns the bounds of the [`Node`].
+ pub fn bounds(&self) -> Rectangle {
+ self.bounds
+ }
+
+ /// Returns the children of the [`Node`].
+ pub fn children(&self) -> &[Node] {
+ &self.children
+ }
+
+ /// Aligns the [`Node`] in the given space.
+ pub fn align(
+ &mut self,
+ horizontal_alignment: Alignment,
+ vertical_alignment: Alignment,
+ space: Size,
+ ) {
+ match horizontal_alignment {
+ Alignment::Start => {}
+ Alignment::Center => {
+ self.bounds.x += (space.width - self.bounds.width) / 2.0;
+ }
+ Alignment::End => {
+ self.bounds.x += space.width - self.bounds.width;
+ }
+ Alignment::Fill => {
+ self.bounds.width = space.width;
+ }
+ }
+
+ match vertical_alignment {
+ Alignment::Start => {}
+ Alignment::Center => {
+ self.bounds.y += (space.height - self.bounds.height) / 2.0;
+ }
+ Alignment::End => {
+ self.bounds.y += space.height - self.bounds.height;
+ }
+ Alignment::Fill => {
+ self.bounds.height = space.height;
+ }
+ }
+ }
+
+ /// Moves the [`Node`] to the given position.
+ pub fn move_to(&mut self, position: Point) {
+ self.bounds.x = position.x;
+ self.bounds.y = position.y;
+ }
+
+ /// Translates the [`Node`] by the given translation.
+ pub fn translate(self, translation: Vector) -> Self {
+ Self {
+ bounds: self.bounds + translation,
+ ..self
+ }
+ }
+}
diff --git a/core/src/lib.rs b/core/src/lib.rs
index 1e4f0411..5bdcee6a 100644
--- a/core/src/lib.rs
+++ b/core/src/lib.rs
@@ -25,33 +25,57 @@
#![forbid(unsafe_code, rust_2018_idioms)]
#![allow(clippy::inherent_to_string, clippy::type_complexity)]
pub mod alignment;
+pub mod clipboard;
+pub mod event;
pub mod font;
pub mod gradient;
+pub mod image;
pub mod keyboard;
+pub mod layout;
pub mod mouse;
+pub mod overlay;
+pub mod renderer;
+pub mod svg;
+pub mod text;
pub mod time;
+pub mod touch;
+pub mod widget;
+pub mod window;
mod background;
mod color;
mod content_fit;
+mod element;
+mod hasher;
mod length;
mod padding;
mod pixels;
mod point;
mod rectangle;
+mod shell;
mod size;
mod vector;
pub use alignment::Alignment;
pub use background::Background;
+pub use clipboard::Clipboard;
pub use color::Color;
pub use content_fit::ContentFit;
+pub use element::Element;
+pub use event::Event;
pub use font::Font;
pub use gradient::Gradient;
+pub use hasher::Hasher;
+pub use layout::Layout;
pub use length::Length;
+pub use overlay::Overlay;
pub use padding::Padding;
pub use pixels::Pixels;
pub use point::Point;
pub use rectangle::Rectangle;
+pub use renderer::Renderer;
+pub use shell::Shell;
pub use size::Size;
+pub use text::Text;
pub use vector::Vector;
+pub use widget::Widget;
diff --git a/core/src/mouse.rs b/core/src/mouse.rs
index 48214f65..0c405ce6 100644
--- a/core/src/mouse.rs
+++ b/core/src/mouse.rs
@@ -1,8 +1,11 @@
//! Handle mouse events.
+pub mod click;
+
mod button;
mod event;
mod interaction;
pub use button::Button;
+pub use click::Click;
pub use event::{Event, ScrollDelta};
pub use interaction::Interaction;
diff --git a/core/src/mouse/click.rs b/core/src/mouse/click.rs
new file mode 100644
index 00000000..4a7d796c
--- /dev/null
+++ b/core/src/mouse/click.rs
@@ -0,0 +1,76 @@
+//! Track mouse clicks.
+use crate::time::Instant;
+use crate::Point;
+
+/// A mouse click.
+#[derive(Debug, Clone, Copy)]
+pub struct Click {
+ kind: Kind,
+ position: Point,
+ time: Instant,
+}
+
+/// The kind of mouse click.
+#[derive(Debug, Clone, Copy)]
+pub enum Kind {
+ /// A single click
+ Single,
+
+ /// A double click
+ Double,
+
+ /// A triple click
+ Triple,
+}
+
+impl Kind {
+ fn next(&self) -> Kind {
+ match self {
+ Kind::Single => Kind::Double,
+ Kind::Double => Kind::Triple,
+ Kind::Triple => Kind::Double,
+ }
+ }
+}
+
+impl Click {
+ /// Creates a new [`Click`] with the given position and previous last
+ /// [`Click`].
+ pub fn new(position: Point, previous: Option<Click>) -> Click {
+ let time = Instant::now();
+
+ let kind = if let Some(previous) = previous {
+ if previous.is_consecutive(position, time) {
+ previous.kind.next()
+ } else {
+ Kind::Single
+ }
+ } else {
+ Kind::Single
+ };
+
+ Click {
+ kind,
+ position,
+ time,
+ }
+ }
+
+ /// Returns the [`Kind`] of [`Click`].
+ pub fn kind(&self) -> Kind {
+ self.kind
+ }
+
+ fn is_consecutive(&self, new_position: Point, time: Instant) -> bool {
+ let duration = if time > self.time {
+ Some(time - self.time)
+ } else {
+ None
+ };
+
+ self.position == new_position
+ && duration
+ .map(|duration| duration.as_millis() <= 300)
+ .unwrap_or(false)
+ }
+}
diff --git a/core/src/overlay.rs b/core/src/overlay.rs
new file mode 100644
index 00000000..b9f3c735
--- /dev/null
+++ b/core/src/overlay.rs
@@ -0,0 +1,122 @@
+//! Display interactive elements on top of other widgets.
+mod element;
+mod group;
+
+pub use element::Element;
+pub use group::Group;
+
+use crate::event::{self, Event};
+use crate::layout;
+use crate::mouse;
+use crate::renderer;
+use crate::widget;
+use crate::widget::Tree;
+use crate::{Clipboard, Layout, Point, Rectangle, Shell, Size};
+
+/// An interactive component that can be displayed on top of other widgets.
+pub trait Overlay<Message, Renderer>
+where
+ Renderer: crate::Renderer,
+{
+ /// Returns the layout [`Node`] of the [`Overlay`].
+ ///
+ /// This [`Node`] is used by the runtime to compute the [`Layout`] of the
+ /// user interface.
+ ///
+ /// [`Node`]: layout::Node
+ fn layout(
+ &self,
+ renderer: &Renderer,
+ bounds: Size,
+ position: Point,
+ ) -> layout::Node;
+
+ /// Draws the [`Overlay`] using the associated `Renderer`.
+ fn draw(
+ &self,
+ renderer: &mut Renderer,
+ theme: &Renderer::Theme,
+ style: &renderer::Style,
+ layout: Layout<'_>,
+ cursor_position: Point,
+ );
+
+ /// Applies a [`widget::Operation`] to the [`Overlay`].
+ fn operate(
+ &mut self,
+ _layout: Layout<'_>,
+ _renderer: &Renderer,
+ _operation: &mut dyn widget::Operation<Message>,
+ ) {
+ }
+
+ /// Processes a runtime [`Event`].
+ ///
+ /// It receives:
+ /// * an [`Event`] describing user interaction
+ /// * the computed [`Layout`] of the [`Overlay`]
+ /// * the current cursor position
+ /// * a mutable `Message` list, allowing the [`Overlay`] to produce
+ /// new messages based on user interaction.
+ /// * the `Renderer`
+ /// * a [`Clipboard`], if available
+ ///
+ /// By default, it does nothing.
+ fn on_event(
+ &mut self,
+ _event: Event,
+ _layout: Layout<'_>,
+ _cursor_position: Point,
+ _renderer: &Renderer,
+ _clipboard: &mut dyn Clipboard,
+ _shell: &mut Shell<'_, Message>,
+ ) -> event::Status {
+ event::Status::Ignored
+ }
+
+ /// Returns the current [`mouse::Interaction`] of the [`Overlay`].
+ ///
+ /// By default, it returns [`mouse::Interaction::Idle`].
+ fn mouse_interaction(
+ &self,
+ _layout: Layout<'_>,
+ _cursor_position: Point,
+ _viewport: &Rectangle,
+ _renderer: &Renderer,
+ ) -> mouse::Interaction {
+ mouse::Interaction::Idle
+ }
+
+ /// Returns true if the cursor is over the [`Overlay`].
+ ///
+ /// By default, it returns true if the bounds of the `layout` contain
+ /// the `cursor_position`.
+ fn is_over(&self, layout: Layout<'_>, cursor_position: Point) -> bool {
+ layout.bounds().contains(cursor_position)
+ }
+}
+
+/// Returns a [`Group`] of overlay [`Element`] children.
+///
+/// This method will generally only be used by advanced users that are
+/// implementing the [`Widget`](crate::Widget) trait.
+pub fn from_children<'a, Message, Renderer>(
+ children: &'a mut [crate::Element<'_, Message, Renderer>],
+ tree: &'a mut Tree,
+ layout: Layout<'_>,
+ renderer: &Renderer,
+) -> Option<Element<'a, Message, Renderer>>
+where
+ Renderer: crate::Renderer,
+{
+ let children = children
+ .iter_mut()
+ .zip(&mut tree.children)
+ .zip(layout.children())
+ .filter_map(|((child, state), layout)| {
+ child.as_widget_mut().overlay(state, layout, renderer)
+ })
+ .collect::<Vec<_>>();
+
+ (!children.is_empty()).then(|| Group::with_children(children).overlay())
+}
diff --git a/core/src/overlay/element.rs b/core/src/overlay/element.rs
new file mode 100644
index 00000000..237d25d1
--- /dev/null
+++ b/core/src/overlay/element.rs
@@ -0,0 +1,270 @@
+pub use crate::Overlay;
+
+use crate::event::{self, Event};
+use crate::layout;
+use crate::mouse;
+use crate::renderer;
+use crate::widget;
+use crate::{Clipboard, Layout, Point, Rectangle, Shell, Size, Vector};
+
+use std::any::Any;
+
+/// A generic [`Overlay`].
+#[allow(missing_debug_implementations)]
+pub struct Element<'a, Message, Renderer> {
+ position: Point,
+ overlay: Box<dyn Overlay<Message, Renderer> + 'a>,
+}
+
+impl<'a, Message, Renderer> Element<'a, Message, Renderer>
+where
+ Renderer: crate::Renderer,
+{
+ /// Creates a new [`Element`] containing the given [`Overlay`].
+ pub fn new(
+ position: Point,
+ overlay: Box<dyn Overlay<Message, Renderer> + 'a>,
+ ) -> Self {
+ Self { position, overlay }
+ }
+
+ /// Returns the position of the [`Element`].
+ pub fn position(&self) -> Point {
+ self.position
+ }
+
+ /// Translates the [`Element`].
+ pub fn translate(mut self, translation: Vector) -> Self {
+ self.position = self.position + translation;
+ self
+ }
+
+ /// Applies a transformation to the produced message of the [`Element`].
+ pub fn map<B>(self, f: &'a dyn Fn(Message) -> B) -> Element<'a, B, Renderer>
+ where
+ Message: 'a,
+ Renderer: 'a,
+ B: 'a,
+ {
+ Element {
+ position: self.position,
+ overlay: Box::new(Map::new(self.overlay, f)),
+ }
+ }
+
+ /// Computes the layout of the [`Element`] in the given bounds.
+ pub fn layout(
+ &self,
+ renderer: &Renderer,
+ bounds: Size,
+ translation: Vector,
+ ) -> layout::Node {
+ self.overlay
+ .layout(renderer, bounds, self.position + translation)
+ }
+
+ /// Processes a runtime [`Event`].
+ pub fn on_event(
+ &mut self,
+ event: Event,
+ layout: Layout<'_>,
+ cursor_position: Point,
+ renderer: &Renderer,
+ clipboard: &mut dyn Clipboard,
+ shell: &mut Shell<'_, Message>,
+ ) -> event::Status {
+ self.overlay.on_event(
+ event,
+ layout,
+ cursor_position,
+ renderer,
+ clipboard,
+ shell,
+ )
+ }
+
+ /// Returns the current [`mouse::Interaction`] of the [`Element`].
+ pub fn mouse_interaction(
+ &self,
+ layout: Layout<'_>,
+ cursor_position: Point,
+ viewport: &Rectangle,
+ renderer: &Renderer,
+ ) -> mouse::Interaction {
+ self.overlay.mouse_interaction(
+ layout,
+ cursor_position,
+ viewport,
+ renderer,
+ )
+ }
+
+ /// Draws the [`Element`] and its children using the given [`Layout`].
+ pub fn draw(
+ &self,
+ renderer: &mut Renderer,
+ theme: &Renderer::Theme,
+ style: &renderer::Style,
+ layout: Layout<'_>,
+ cursor_position: Point,
+ ) {
+ self.overlay
+ .draw(renderer, theme, style, layout, cursor_position)
+ }
+
+ /// Applies a [`widget::Operation`] to the [`Element`].
+ pub fn operate(
+ &mut self,
+ layout: Layout<'_>,
+ renderer: &Renderer,
+ operation: &mut dyn widget::Operation<Message>,
+ ) {
+ self.overlay.operate(layout, renderer, operation);
+ }
+
+ /// Returns true if the cursor is over the [`Element`].
+ pub fn is_over(&self, layout: Layout<'_>, cursor_position: Point) -> bool {
+ self.overlay.is_over(layout, cursor_position)
+ }
+}
+
+struct Map<'a, A, B, Renderer> {
+ content: Box<dyn Overlay<A, Renderer> + 'a>,
+ mapper: &'a dyn Fn(A) -> B,
+}
+
+impl<'a, A, B, Renderer> Map<'a, A, B, Renderer> {
+ pub fn new(
+ content: Box<dyn Overlay<A, Renderer> + 'a>,
+ mapper: &'a dyn Fn(A) -> B,
+ ) -> Map<'a, A, B, Renderer> {
+ Map { content, mapper }
+ }
+}
+
+impl<'a, A, B, Renderer> Overlay<B, Renderer> for Map<'a, A, B, Renderer>
+where
+ Renderer: crate::Renderer,
+{
+ fn layout(
+ &self,
+ renderer: &Renderer,
+ bounds: Size,
+ position: Point,
+ ) -> layout::Node {
+ self.content.layout(renderer, bounds, position)
+ }
+
+ fn operate(
+ &mut self,
+ layout: Layout<'_>,
+ renderer: &Renderer,
+ operation: &mut dyn widget::Operation<B>,
+ ) {
+ struct MapOperation<'a, B> {
+ operation: &'a mut dyn widget::Operation<B>,
+ }
+
+ impl<'a, T, B> widget::Operation<T> for MapOperation<'a, B> {
+ fn container(
+ &mut self,
+ id: Option<&widget::Id>,
+ operate_on_children: &mut dyn FnMut(
+ &mut dyn widget::Operation<T>,
+ ),
+ ) {
+ self.operation.container(id, &mut |operation| {
+ operate_on_children(&mut MapOperation { operation });
+ });
+ }
+
+ fn focusable(
+ &mut self,
+ state: &mut dyn widget::operation::Focusable,
+ id: Option<&widget::Id>,
+ ) {
+ self.operation.focusable(state, id);
+ }
+
+ fn scrollable(
+ &mut self,
+ state: &mut dyn widget::operation::Scrollable,
+ id: Option<&widget::Id>,
+ ) {
+ self.operation.scrollable(state, id);
+ }
+
+ fn text_input(
+ &mut self,
+ state: &mut dyn widget::operation::TextInput,
+ id: Option<&widget::Id>,
+ ) {
+ self.operation.text_input(state, id)
+ }
+
+ fn custom(&mut self, state: &mut dyn Any, id: Option<&widget::Id>) {
+ self.operation.custom(state, id);
+ }
+ }
+
+ self.content
+ .operate(layout, renderer, &mut MapOperation { operation });
+ }
+
+ fn on_event(
+ &mut self,
+ event: Event,
+ layout: Layout<'_>,
+ cursor_position: Point,
+ renderer: &Renderer,
+ clipboard: &mut dyn Clipboard,
+ shell: &mut Shell<'_, B>,
+ ) -> event::Status {
+ let mut local_messages = Vec::new();
+ let mut local_shell = Shell::new(&mut local_messages);
+
+ let event_status = self.content.on_event(
+ event,
+ layout,
+ cursor_position,
+ renderer,
+ clipboard,
+ &mut local_shell,
+ );
+
+ shell.merge(local_shell, self.mapper);
+
+ event_status
+ }
+
+ fn mouse_interaction(
+ &self,
+ layout: Layout<'_>,
+ cursor_position: Point,
+ viewport: &Rectangle,
+ renderer: &Renderer,
+ ) -> mouse::Interaction {
+ self.content.mouse_interaction(
+ layout,
+ cursor_position,
+ viewport,
+ renderer,
+ )
+ }
+
+ fn draw(
+ &self,
+ renderer: &mut Renderer,
+ theme: &Renderer::Theme,
+ style: &renderer::Style,
+ layout: Layout<'_>,
+ cursor_position: Point,
+ ) {
+ self.content
+ .draw(renderer, theme, style, layout, cursor_position)
+ }
+
+ fn is_over(&self, layout: Layout<'_>, cursor_position: Point) -> bool {
+ self.content.is_over(layout, cursor_position)
+ }
+}
diff --git a/core/src/overlay/group.rs b/core/src/overlay/group.rs
new file mode 100644
index 00000000..0c48df34
--- /dev/null
+++ b/core/src/overlay/group.rs
@@ -0,0 +1,172 @@
+use crate::event;
+use crate::layout;
+use crate::mouse;
+use crate::overlay;
+use crate::renderer;
+use crate::widget;
+use crate::{Clipboard, Event, Layout, Overlay, Point, Rectangle, Shell, Size};
+
+/// An [`Overlay`] container that displays multiple overlay [`overlay::Element`]
+/// children.
+#[allow(missing_debug_implementations)]
+pub struct Group<'a, Message, Renderer> {
+ children: Vec<overlay::Element<'a, Message, Renderer>>,
+}
+
+impl<'a, Message, Renderer> Group<'a, Message, Renderer>
+where
+ Renderer: 'a + crate::Renderer,
+ Message: 'a,
+{
+ /// Creates an empty [`Group`].
+ pub fn new() -> Self {
+ Self::default()
+ }
+
+ /// Creates a [`Group`] with the given elements.
+ pub fn with_children(
+ children: Vec<overlay::Element<'a, Message, Renderer>>,
+ ) -> Self {
+ Group { children }
+ }
+
+ /// Adds an [`overlay::Element`] to the [`Group`].
+ pub fn push(
+ mut self,
+ child: impl Into<overlay::Element<'a, Message, Renderer>>,
+ ) -> Self {
+ self.children.push(child.into());
+ self
+ }
+
+ /// Turns the [`Group`] into an overlay [`overlay::Element`].
+ pub fn overlay(self) -> overlay::Element<'a, Message, Renderer> {
+ overlay::Element::new(Point::ORIGIN, Box::new(self))
+ }
+}
+
+impl<'a, Message, Renderer> Default for Group<'a, Message, Renderer>
+where
+ Renderer: 'a + crate::Renderer,
+ Message: 'a,
+{
+ fn default() -> Self {
+ Self::with_children(Vec::new())
+ }
+}
+
+impl<'a, Message, Renderer> Overlay<Message, Renderer>
+ for Group<'a, Message, Renderer>
+where
+ Renderer: crate::Renderer,
+{
+ fn layout(
+ &self,
+ renderer: &Renderer,
+ bounds: Size,
+ position: Point,
+ ) -> layout::Node {
+ let translation = position - Point::ORIGIN;
+
+ layout::Node::with_children(
+ bounds,
+ self.children
+ .iter()
+ .map(|child| child.layout(renderer, bounds, translation))
+ .collect(),
+ )
+ }
+
+ fn on_event(
+ &mut self,
+ event: Event,
+ layout: Layout<'_>,
+ cursor_position: Point,
+ renderer: &Renderer,
+ clipboard: &mut dyn Clipboard,
+ shell: &mut Shell<'_, Message>,
+ ) -> event::Status {
+ self.children
+ .iter_mut()
+ .zip(layout.children())
+ .map(|(child, layout)| {
+ child.on_event(
+ event.clone(),
+ layout,
+ cursor_position,
+ renderer,
+ clipboard,
+ shell,
+ )
+ })
+ .fold(event::Status::Ignored, event::Status::merge)
+ }
+
+ fn draw(
+ &self,
+ renderer: &mut Renderer,
+ theme: &<Renderer as crate::Renderer>::Theme,
+ style: &renderer::Style,
+ layout: Layout<'_>,
+ cursor_position: Point,
+ ) {
+ for (child, layout) in self.children.iter().zip(layout.children()) {
+ child.draw(renderer, theme, style, layout, cursor_position);
+ }
+ }
+
+ fn mouse_interaction(
+ &self,
+ layout: Layout<'_>,
+ cursor_position: Point,
+ viewport: &Rectangle,
+ renderer: &Renderer,
+ ) -> mouse::Interaction {
+ self.children
+ .iter()
+ .zip(layout.children())
+ .map(|(child, layout)| {
+ child.mouse_interaction(
+ layout,
+ cursor_position,
+ viewport,
+ renderer,
+ )
+ })
+ .max()
+ .unwrap_or_default()
+ }
+
+ fn operate(
+ &mut self,
+ layout: Layout<'_>,
+ renderer: &Renderer,
+ operation: &mut dyn widget::Operation<Message>,
+ ) {
+ operation.container(None, &mut |operation| {
+ self.children.iter_mut().zip(layout.children()).for_each(
+ |(child, layout)| {
+ child.operate(layout, renderer, operation);
+ },
+ )
+ });
+ }
+
+ fn is_over(&self, layout: Layout<'_>, cursor_position: Point) -> bool {
+ self.children
+ .iter()
+ .zip(layout.children())
+ .any(|(child, layout)| child.is_over(layout, cursor_position))
+ }
+}
+
+impl<'a, Message, Renderer> From<Group<'a, Message, Renderer>>
+ for overlay::Element<'a, Message, Renderer>
+where
+ Renderer: 'a + crate::Renderer,
+ Message: 'a,
+{
+ fn from(group: Group<'a, Message, Renderer>) -> Self {
+ group.overlay()
+ }
+}
diff --git a/core/src/renderer.rs b/core/src/renderer.rs
new file mode 100644
index 00000000..d6247e39
--- /dev/null
+++ b/core/src/renderer.rs
@@ -0,0 +1,99 @@
+//! Write your own renderer.
+#[cfg(debug_assertions)]
+mod null;
+
+#[cfg(debug_assertions)]
+pub use null::Null;
+
+use crate::layout;
+use crate::{Background, Color, Element, Rectangle, Vector};
+
+/// A component that can be used by widgets to draw themselves on a screen.
+pub trait Renderer: Sized {
+ /// The supported theme of the [`Renderer`].
+ type Theme;
+
+ /// Lays out the elements of a user interface.
+ ///
+ /// You should override this if you need to perform any operations before or
+ /// after layouting. For instance, trimming the measurements cache.
+ fn layout<Message>(
+ &mut self,
+ element: &Element<'_, Message, Self>,
+ limits: &layout::Limits,
+ ) -> layout::Node {
+ element.as_widget().layout(self, limits)
+ }
+
+ /// Draws the primitives recorded in the given closure in a new layer.
+ ///
+ /// The layer will clip its contents to the provided `bounds`.
+ fn with_layer(&mut self, bounds: Rectangle, f: impl FnOnce(&mut Self));
+
+ /// Applies a `translation` to the primitives recorded in the given closure.
+ fn with_translation(
+ &mut self,
+ translation: Vector,
+ f: impl FnOnce(&mut Self),
+ );
+
+ /// Fills a [`Quad`] with the provided [`Background`].
+ fn fill_quad(&mut self, quad: Quad, background: impl Into<Background>);
+
+ /// Clears all of the recorded primitives in the [`Renderer`].
+ fn clear(&mut self);
+}
+
+/// A polygon with four sides.
+#[derive(Debug, Clone, Copy, PartialEq)]
+pub struct Quad {
+ /// The bounds of the [`Quad`].
+ pub bounds: Rectangle,
+
+ /// The border radius of the [`Quad`].
+ pub border_radius: BorderRadius,
+
+ /// The border width of the [`Quad`].
+ pub border_width: f32,
+
+ /// The border color of the [`Quad`].
+ pub border_color: Color,
+}
+
+/// The border radi for the corners of a graphics primitive in the order:
+/// top-left, top-right, bottom-right, bottom-left.
+#[derive(Debug, Clone, Copy, PartialEq, Default)]
+pub struct BorderRadius([f32; 4]);
+
+impl From<f32> for BorderRadius {
+ fn from(w: f32) -> Self {
+ Self([w; 4])
+ }
+}
+
+impl From<[f32; 4]> for BorderRadius {
+ fn from(radi: [f32; 4]) -> Self {
+ Self(radi)
+ }
+}
+
+impl From<BorderRadius> for [f32; 4] {
+ fn from(radi: BorderRadius) -> Self {
+ radi.0
+ }
+}
+
+/// The styling attributes of a [`Renderer`].
+#[derive(Debug, Clone, Copy, PartialEq)]
+pub struct Style {
+ /// The text color
+ pub text_color: Color,
+}
+
+impl Default for Style {
+ fn default() -> Self {
+ Style {
+ text_color: Color::BLACK,
+ }
+ }
+}
diff --git a/core/src/renderer/null.rs b/core/src/renderer/null.rs
new file mode 100644
index 00000000..d93338ae
--- /dev/null
+++ b/core/src/renderer/null.rs
@@ -0,0 +1,82 @@
+use crate::renderer::{self, Renderer};
+use crate::text::{self, Text};
+use crate::{Background, Font, Point, Rectangle, Size, Vector};
+
+use std::borrow::Cow;
+
+/// A renderer that does nothing.
+///
+/// It can be useful if you are writing tests!
+#[derive(Debug, Clone, Copy, Default)]
+pub struct Null;
+
+impl Null {
+ /// Creates a new [`Null`] renderer.
+ pub fn new() -> Null {
+ Null
+ }
+}
+
+impl Renderer for Null {
+ type Theme = ();
+
+ fn with_layer(&mut self, _bounds: Rectangle, _f: impl FnOnce(&mut Self)) {}
+
+ fn with_translation(
+ &mut self,
+ _translation: Vector,
+ _f: impl FnOnce(&mut Self),
+ ) {
+ }
+
+ fn clear(&mut self) {}
+
+ fn fill_quad(
+ &mut self,
+ _quad: renderer::Quad,
+ _background: impl Into<Background>,
+ ) {
+ }
+}
+
+impl text::Renderer for Null {
+ type Font = Font;
+
+ const ICON_FONT: Font = Font::SansSerif;
+ const CHECKMARK_ICON: char = '0';
+ const ARROW_DOWN_ICON: char = '0';
+
+ fn default_font(&self) -> Self::Font {
+ Font::SansSerif
+ }
+
+ fn default_size(&self) -> f32 {
+ 16.0
+ }
+
+ fn load_font(&mut self, _font: Cow<'static, [u8]>) {}
+
+ fn measure(
+ &self,
+ _content: &str,
+ _size: f32,
+ _font: Font,
+ _bounds: Size,
+ ) -> (f32, f32) {
+ (0.0, 20.0)
+ }
+
+ fn hit_test(
+ &self,
+ _contents: &str,
+ _size: f32,
+ _font: Self::Font,
+ _bounds: Size,
+ _point: Point,
+ _nearest_only: bool,
+ ) -> Option<text::Hit> {
+ None
+ }
+
+ fn fill_text(&mut self, _text: Text<'_, Self::Font>) {}
+}
diff --git a/core/src/shell.rs b/core/src/shell.rs
new file mode 100644
index 00000000..74a5c616
--- /dev/null
+++ b/core/src/shell.rs
@@ -0,0 +1,108 @@
+use crate::window;
+
+/// A connection to the state of a shell.
+///
+/// A [`Widget`] can leverage a [`Shell`] to trigger changes in an application,
+/// like publishing messages or invalidating the current layout.
+///
+/// [`Widget`]: crate::Widget
+#[derive(Debug)]
+pub struct Shell<'a, Message> {
+ messages: &'a mut Vec<Message>,
+ redraw_request: Option<window::RedrawRequest>,
+ is_layout_invalid: bool,
+ are_widgets_invalid: bool,
+}
+
+impl<'a, Message> Shell<'a, Message> {
+ /// Creates a new [`Shell`] with the provided buffer of messages.
+ pub fn new(messages: &'a mut Vec<Message>) -> Self {
+ Self {
+ messages,
+ redraw_request: None,
+ is_layout_invalid: false,
+ are_widgets_invalid: false,
+ }
+ }
+
+ /// Returns true if the [`Shell`] contains no published messages
+ pub fn is_empty(&self) -> bool {
+ self.messages.is_empty()
+ }
+
+ /// Publish the given `Message` for an application to process it.
+ pub fn publish(&mut self, message: Message) {
+ self.messages.push(message);
+ }
+
+ /// Requests a new frame to be drawn at the given [`Instant`].
+ pub fn request_redraw(&mut self, request: window::RedrawRequest) {
+ match self.redraw_request {
+ None => {
+ self.redraw_request = Some(request);
+ }
+ Some(current) if request < current => {
+ self.redraw_request = Some(request);
+ }
+ _ => {}
+ }
+ }
+
+ /// Returns the requested [`Instant`] a redraw should happen, if any.
+ pub fn redraw_request(&self) -> Option<window::RedrawRequest> {
+ self.redraw_request
+ }
+
+ /// Returns whether the current layout is invalid or not.
+ pub fn is_layout_invalid(&self) -> bool {
+ self.is_layout_invalid
+ }
+
+ /// Invalidates the current application layout.
+ ///
+ /// The shell will relayout the application widgets.
+ pub fn invalidate_layout(&mut self) {
+ self.is_layout_invalid = true;
+ }
+
+ /// Triggers the given function if the layout is invalid, cleaning it in the
+ /// process.
+ pub fn revalidate_layout(&mut self, f: impl FnOnce()) {
+ if self.is_layout_invalid {
+ self.is_layout_invalid = false;
+
+ f()
+ }
+ }
+
+ /// Returns whether the widgets of the current application have been
+ /// invalidated.
+ pub fn are_widgets_invalid(&self) -> bool {
+ self.are_widgets_invalid
+ }
+
+ /// Invalidates the current application widgets.
+ ///
+ /// The shell will rebuild and relayout the widget tree.
+ pub fn invalidate_widgets(&mut self) {
+ self.are_widgets_invalid = true;
+ }
+
+ /// Merges the current [`Shell`] with another one by applying the given
+ /// function to the messages of the latter.
+ ///
+ /// This method is useful for composition.
+ pub fn merge<B>(&mut self, other: Shell<'_, B>, f: impl Fn(B) -> Message) {
+ self.messages.extend(other.messages.drain(..).map(f));
+
+ if let Some(at) = other.redraw_request {
+ self.request_redraw(at);
+ }
+
+ self.is_layout_invalid =
+ self.is_layout_invalid || other.is_layout_invalid;
+
+ self.are_widgets_invalid =
+ self.are_widgets_invalid || other.are_widgets_invalid;
+ }
+}
diff --git a/core/src/svg.rs b/core/src/svg.rs
new file mode 100644
index 00000000..9b98877a
--- /dev/null
+++ b/core/src/svg.rs
@@ -0,0 +1,89 @@
+//! Load and draw vector graphics.
+use crate::{Color, Hasher, Rectangle, Size};
+
+use std::borrow::Cow;
+use std::hash::{Hash, Hasher as _};
+use std::path::PathBuf;
+use std::sync::Arc;
+
+/// A handle of Svg data.
+#[derive(Debug, Clone)]
+pub struct Handle {
+ id: u64,
+ data: Arc<Data>,
+}
+
+impl Handle {
+ /// Creates an SVG [`Handle`] pointing to the vector image of the given
+ /// path.
+ pub fn from_path(path: impl Into<PathBuf>) -> Handle {
+ Self::from_data(Data::Path(path.into()))
+ }
+
+ /// Creates an SVG [`Handle`] from raw bytes containing either an SVG string
+ /// or gzip compressed data.
+ ///
+ /// This is useful if you already have your SVG data in-memory, maybe
+ /// because you downloaded or generated it procedurally.
+ pub fn from_memory(bytes: impl Into<Cow<'static, [u8]>>) -> Handle {
+ Self::from_data(Data::Bytes(bytes.into()))
+ }
+
+ fn from_data(data: Data) -> Handle {
+ let mut hasher = Hasher::default();
+ data.hash(&mut hasher);
+
+ Handle {
+ id: hasher.finish(),
+ data: Arc::new(data),
+ }
+ }
+
+ /// Returns the unique identifier of the [`Handle`].
+ pub fn id(&self) -> u64 {
+ self.id
+ }
+
+ /// Returns a reference to the SVG [`Data`].
+ pub fn data(&self) -> &Data {
+ &self.data
+ }
+}
+
+impl Hash for Handle {
+ fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
+ self.id.hash(state);
+ }
+}
+
+/// The data of a vectorial image.
+#[derive(Clone, Hash)]
+pub enum Data {
+ /// File data
+ Path(PathBuf),
+
+ /// In-memory data
+ ///
+ /// Can contain an SVG string or a gzip compressed data.
+ Bytes(Cow<'static, [u8]>),
+}
+
+impl std::fmt::Debug for Data {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ match self {
+ Data::Path(path) => write!(f, "Path({path:?})"),
+ Data::Bytes(_) => write!(f, "Bytes(...)"),
+ }
+ }
+}
+
+/// A [`Renderer`] that can render vector graphics.
+///
+/// [renderer]: crate::renderer
+pub trait Renderer: crate::Renderer {
+ /// Returns the default dimensions of an SVG for the given [`Handle`].
+ fn dimensions(&self, handle: &Handle) -> Size<u32>;
+
+ /// Draws an SVG with the given [`Handle`], an optional [`Color`] filter, and inside the provided `bounds`.
+ fn draw(&mut self, handle: Handle, color: Option<Color>, bounds: Rectangle);
+}
diff --git a/core/src/text.rs b/core/src/text.rs
new file mode 100644
index 00000000..4c72abc3
--- /dev/null
+++ b/core/src/text.rs
@@ -0,0 +1,111 @@
+//! Draw and interact with text.
+use crate::alignment;
+use crate::{Color, Point, Rectangle, Size};
+
+use std::borrow::Cow;
+
+/// A paragraph.
+#[derive(Debug, Clone, Copy)]
+pub struct Text<'a, Font> {
+ /// The content of the paragraph.
+ pub content: &'a str,
+
+ /// The bounds of the paragraph.
+ pub bounds: Rectangle,
+
+ /// The size of the [`Text`].
+ pub size: f32,
+
+ /// The color of the [`Text`].
+ pub color: Color,
+
+ /// The font of the [`Text`].
+ pub font: Font,
+
+ /// The horizontal alignment of the [`Text`].
+ pub horizontal_alignment: alignment::Horizontal,
+
+ /// The vertical alignment of the [`Text`].
+ pub vertical_alignment: alignment::Vertical,
+}
+
+/// The result of hit testing on text.
+#[derive(Debug, Clone, Copy, PartialEq)]
+pub enum Hit {
+ /// The point was within the bounds of the returned character index.
+ CharOffset(usize),
+}
+
+impl Hit {
+ /// Computes the cursor position of the [`Hit`] .
+ pub fn cursor(self) -> usize {
+ match self {
+ Self::CharOffset(i) => i,
+ }
+ }
+}
+
+/// A renderer capable of measuring and drawing [`Text`].
+pub trait Renderer: crate::Renderer {
+ /// The font type used.
+ type Font: Copy;
+
+ /// The icon font of the backend.
+ const ICON_FONT: Self::Font;
+
+ /// The `char` representing a ✔ icon in the [`ICON_FONT`].
+ ///
+ /// [`ICON_FONT`]: Self::ICON_FONT
+ const CHECKMARK_ICON: char;
+
+ /// The `char` representing a ▼ icon in the built-in [`ICON_FONT`].
+ ///
+ /// [`ICON_FONT`]: Self::ICON_FONT
+ const ARROW_DOWN_ICON: char;
+
+ /// Returns the default [`Self::Font`].
+ fn default_font(&self) -> Self::Font;
+
+ /// Returns the default size of [`Text`].
+ fn default_size(&self) -> f32;
+
+ /// Measures the text in the given bounds and returns the minimum boundaries
+ /// that can fit the contents.
+ fn measure(
+ &self,
+ content: &str,
+ size: f32,
+ font: Self::Font,
+ bounds: Size,
+ ) -> (f32, f32);
+
+ /// Measures the width of the text as if it were laid out in a single line.
+ fn measure_width(&self, content: &str, size: f32, font: Self::Font) -> f32 {
+ let (width, _) = self.measure(content, size, font, Size::INFINITY);
+
+ width
+ }
+
+ /// Tests whether the provided point is within the boundaries of text
+ /// laid out with the given parameters, returning information about
+ /// the nearest character.
+ ///
+ /// If `nearest_only` is true, the hit test does not consider whether the
+ /// the point is interior to any glyph bounds, returning only the character
+ /// with the nearest centeroid.
+ fn hit_test(
+ &self,
+ contents: &str,
+ size: f32,
+ font: Self::Font,
+ bounds: Size,
+ point: Point,
+ nearest_only: bool,
+ ) -> Option<Hit>;
+
+ /// Loads a [`Self::Font`] from its bytes.
+ fn load_font(&mut self, font: Cow<'static, [u8]>);
+
+ /// Draws the given [`Text`].
+ fn fill_text(&mut self, text: Text<'_, Self::Font>);
+}
diff --git a/core/src/touch.rs b/core/src/touch.rs
new file mode 100644
index 00000000..18120644
--- /dev/null
+++ b/core/src/touch.rs
@@ -0,0 +1,23 @@
+//! Build touch events.
+use crate::Point;
+
+/// A touch interaction.
+#[derive(Debug, Clone, Copy, PartialEq)]
+#[allow(missing_docs)]
+pub enum Event {
+ /// A touch interaction was started.
+ FingerPressed { id: Finger, position: Point },
+
+ /// An on-going touch interaction was moved.
+ FingerMoved { id: Finger, position: Point },
+
+ /// A touch interaction was ended.
+ FingerLifted { id: Finger, position: Point },
+
+ /// A touch interaction was canceled.
+ FingerLost { id: Finger, position: Point },
+}
+
+/// A unique identifier representing a finger on a touch interaction.
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
+pub struct Finger(pub u64);
diff --git a/core/src/widget.rs b/core/src/widget.rs
new file mode 100644
index 00000000..70e2c2d9
--- /dev/null
+++ b/core/src/widget.rs
@@ -0,0 +1,145 @@
+//! Create custom widgets and operate on them.
+pub mod operation;
+pub mod text;
+pub mod tree;
+
+mod id;
+
+pub use id::Id;
+pub use operation::Operation;
+pub use text::Text;
+pub use tree::Tree;
+
+use crate::event::{self, Event};
+use crate::layout::{self, Layout};
+use crate::mouse;
+use crate::overlay;
+use crate::renderer;
+use crate::{Clipboard, Length, Point, Rectangle, Shell};
+
+/// A component that displays information and allows interaction.
+///
+/// If you want to build your own widgets, you will need to implement this
+/// trait.
+///
+/// # Examples
+/// The repository has some [examples] showcasing how to implement a custom
+/// widget:
+///
+/// - [`bezier_tool`], a Paint-like tool for drawing Bézier curves using
+/// [`lyon`].
+/// - [`custom_widget`], a demonstration of how to build a custom widget that
+/// draws a circle.
+/// - [`geometry`], a custom widget showcasing how to draw geometry with the
+/// `Mesh2D` primitive in [`iced_wgpu`].
+///
+/// [examples]: https://github.com/iced-rs/iced/tree/0.8/examples
+/// [`bezier_tool`]: https://github.com/iced-rs/iced/tree/0.8/examples/bezier_tool
+/// [`custom_widget`]: https://github.com/iced-rs/iced/tree/0.8/examples/custom_widget
+/// [`geometry`]: https://github.com/iced-rs/iced/tree/0.8/examples/geometry
+/// [`lyon`]: https://github.com/nical/lyon
+/// [`iced_wgpu`]: https://github.com/iced-rs/iced/tree/0.8/wgpu
+pub trait Widget<Message, Renderer>
+where
+ Renderer: crate::Renderer,
+{
+ /// Returns the width of the [`Widget`].
+ fn width(&self) -> Length;
+
+ /// Returns the height of the [`Widget`].
+ fn height(&self) -> Length;
+
+ /// Returns the [`layout::Node`] of the [`Widget`].
+ ///
+ /// This [`layout::Node`] is used by the runtime to compute the [`Layout`] of the
+ /// user interface.
+ fn layout(
+ &self,
+ renderer: &Renderer,
+ limits: &layout::Limits,
+ ) -> layout::Node;
+
+ /// Draws the [`Widget`] using the associated `Renderer`.
+ fn draw(
+ &self,
+ state: &Tree,
+ renderer: &mut Renderer,
+ theme: &Renderer::Theme,
+ style: &renderer::Style,
+ layout: Layout<'_>,
+ cursor_position: Point,
+ viewport: &Rectangle,
+ );
+
+ /// Returns the [`Tag`] of the [`Widget`].
+ ///
+ /// [`Tag`]: tree::Tag
+ fn tag(&self) -> tree::Tag {
+ tree::Tag::stateless()
+ }
+
+ /// Returns the [`State`] of the [`Widget`].
+ ///
+ /// [`State`]: tree::State
+ fn state(&self) -> tree::State {
+ tree::State::None
+ }
+
+ /// Returns the state [`Tree`] of the children of the [`Widget`].
+ fn children(&self) -> Vec<Tree> {
+ Vec::new()
+ }
+
+ /// Reconciliates the [`Widget`] with the provided [`Tree`].
+ fn diff(&self, _tree: &mut Tree) {}
+
+ /// Applies an [`Operation`] to the [`Widget`].
+ fn operate(
+ &self,
+ _state: &mut Tree,
+ _layout: Layout<'_>,
+ _renderer: &Renderer,
+ _operation: &mut dyn Operation<Message>,
+ ) {
+ }
+
+ /// Processes a runtime [`Event`].
+ ///
+ /// By default, it does nothing.
+ fn on_event(
+ &mut self,
+ _state: &mut Tree,
+ _event: Event,
+ _layout: Layout<'_>,
+ _cursor_position: Point,
+ _renderer: &Renderer,
+ _clipboard: &mut dyn Clipboard,
+ _shell: &mut Shell<'_, Message>,
+ ) -> event::Status {
+ event::Status::Ignored
+ }
+
+ /// Returns the current [`mouse::Interaction`] of the [`Widget`].
+ ///
+ /// By default, it returns [`mouse::Interaction::Idle`].
+ fn mouse_interaction(
+ &self,
+ _state: &Tree,
+ _layout: Layout<'_>,
+ _cursor_position: Point,
+ _viewport: &Rectangle,
+ _renderer: &Renderer,
+ ) -> mouse::Interaction {
+ mouse::Interaction::Idle
+ }
+
+ /// Returns the overlay of the [`Widget`], if there is any.
+ fn overlay<'a>(
+ &'a mut self,
+ _state: &'a mut Tree,
+ _layout: Layout<'_>,
+ _renderer: &Renderer,
+ ) -> Option<overlay::Element<'a, Message, Renderer>> {
+ None
+ }
+}
diff --git a/core/src/widget/id.rs b/core/src/widget/id.rs
new file mode 100644
index 00000000..ae739bb7
--- /dev/null
+++ b/core/src/widget/id.rs
@@ -0,0 +1,43 @@
+use std::borrow;
+use std::sync::atomic::{self, AtomicUsize};
+
+static NEXT_ID: AtomicUsize = AtomicUsize::new(0);
+
+/// The identifier of a generic widget.
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+pub struct Id(Internal);
+
+impl Id {
+ /// Creates a custom [`Id`].
+ pub fn new(id: impl Into<borrow::Cow<'static, str>>) -> Self {
+ Self(Internal::Custom(id.into()))
+ }
+
+ /// Creates a unique [`Id`].
+ ///
+ /// This function produces a different [`Id`] every time it is called.
+ pub fn unique() -> Self {
+ let id = NEXT_ID.fetch_add(1, atomic::Ordering::Relaxed);
+
+ Self(Internal::Unique(id))
+ }
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+enum Internal {
+ Unique(usize),
+ Custom(borrow::Cow<'static, str>),
+}
+
+#[cfg(test)]
+mod tests {
+ use super::Id;
+
+ #[test]
+ fn unique_generates_different_ids() {
+ let a = Id::unique();
+ let b = Id::unique();
+
+ assert_ne!(a, b);
+ }
+}
diff --git a/core/src/widget/operation.rs b/core/src/widget/operation.rs
new file mode 100644
index 00000000..53688a21
--- /dev/null
+++ b/core/src/widget/operation.rs
@@ -0,0 +1,112 @@
+//! Query or update internal widget state.
+pub mod focusable;
+pub mod scrollable;
+pub mod text_input;
+
+pub use focusable::Focusable;
+pub use scrollable::Scrollable;
+pub use text_input::TextInput;
+
+use crate::widget::Id;
+
+use std::any::Any;
+use std::fmt;
+
+/// A piece of logic that can traverse the widget tree of an application in
+/// order to query or update some widget state.
+pub trait Operation<T> {
+ /// Operates on a widget that contains other widgets.
+ ///
+ /// The `operate_on_children` function can be called to return control to
+ /// the widget tree and keep traversing it.
+ fn container(
+ &mut self,
+ id: Option<&Id>,
+ operate_on_children: &mut dyn FnMut(&mut dyn Operation<T>),
+ );
+
+ /// Operates on a widget that can be focused.
+ fn focusable(&mut self, _state: &mut dyn Focusable, _id: Option<&Id>) {}
+
+ /// Operates on a widget that can be scrolled.
+ fn scrollable(&mut self, _state: &mut dyn Scrollable, _id: Option<&Id>) {}
+
+ /// Operates on a widget that has text input.
+ fn text_input(&mut self, _state: &mut dyn TextInput, _id: Option<&Id>) {}
+
+ /// Operates on a custom widget with some state.
+ fn custom(&mut self, _state: &mut dyn Any, _id: Option<&Id>) {}
+
+ /// Finishes the [`Operation`] and returns its [`Outcome`].
+ fn finish(&self) -> Outcome<T> {
+ Outcome::None
+ }
+}
+
+/// The result of an [`Operation`].
+pub enum Outcome<T> {
+ /// The [`Operation`] produced no result.
+ None,
+
+ /// The [`Operation`] produced some result.
+ Some(T),
+
+ /// The [`Operation`] needs to be followed by another [`Operation`].
+ Chain(Box<dyn Operation<T>>),
+}
+
+impl<T> fmt::Debug for Outcome<T>
+where
+ T: fmt::Debug,
+{
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self {
+ Self::None => write!(f, "Outcome::None"),
+ Self::Some(output) => write!(f, "Outcome::Some({output:?})"),
+ Self::Chain(_) => write!(f, "Outcome::Chain(...)"),
+ }
+ }
+}
+
+/// Produces an [`Operation`] that applies the given [`Operation`] to the
+/// children of a container with the given [`Id`].
+pub fn scoped<T: 'static>(
+ target: Id,
+ operation: impl Operation<T> + 'static,
+) -> impl Operation<T> {
+ struct ScopedOperation<Message> {
+ target: Id,
+ operation: Box<dyn Operation<Message>>,
+ }
+
+ impl<Message: 'static> Operation<Message> for ScopedOperation<Message> {
+ fn container(
+ &mut self,
+ id: Option<&Id>,
+ operate_on_children: &mut dyn FnMut(&mut dyn Operation<Message>),
+ ) {
+ if id == Some(&self.target) {
+ operate_on_children(self.operation.as_mut());
+ } else {
+ operate_on_children(self);
+ }
+ }
+
+ fn finish(&self) -> Outcome<Message> {
+ match self.operation.finish() {
+ Outcome::Chain(next) => {
+ Outcome::Chain(Box::new(ScopedOperation {
+ target: self.target.clone(),
+ operation: next,
+ }))
+ }
+ outcome => outcome,
+ }
+ }
+ }
+
+ ScopedOperation {
+ target,
+ operation: Box::new(operation),
+ }
+}
diff --git a/core/src/widget/operation/focusable.rs b/core/src/widget/operation/focusable.rs
new file mode 100644
index 00000000..312e4894
--- /dev/null
+++ b/core/src/widget/operation/focusable.rs
@@ -0,0 +1,203 @@
+//! Operate on widgets that can be focused.
+use crate::widget::operation::{Operation, Outcome};
+use crate::widget::Id;
+
+/// The internal state of a widget that can be focused.
+pub trait Focusable {
+ /// Returns whether the widget is focused or not.
+ fn is_focused(&self) -> bool;
+
+ /// Focuses the widget.
+ fn focus(&mut self);
+
+ /// Unfocuses the widget.
+ fn unfocus(&mut self);
+}
+
+/// A summary of the focusable widgets present on a widget tree.
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
+pub struct Count {
+ /// The index of the current focused widget, if any.
+ pub focused: Option<usize>,
+
+ /// The total amount of focusable widgets.
+ pub total: usize,
+}
+
+/// Produces an [`Operation`] that focuses the widget with the given [`Id`].
+pub fn focus<T>(target: Id) -> impl Operation<T> {
+ struct Focus {
+ target: Id,
+ }
+
+ impl<T> Operation<T> for Focus {
+ fn focusable(&mut self, state: &mut dyn Focusable, id: Option<&Id>) {
+ match id {
+ Some(id) if id == &self.target => {
+ state.focus();
+ }
+ _ => {
+ state.unfocus();
+ }
+ }
+ }
+
+ fn container(
+ &mut self,
+ _id: Option<&Id>,
+ operate_on_children: &mut dyn FnMut(&mut dyn Operation<T>),
+ ) {
+ operate_on_children(self)
+ }
+ }
+
+ Focus { target }
+}
+
+/// Produces an [`Operation`] that generates a [`Count`] and chains it with the
+/// provided function to build a new [`Operation`].
+pub fn count<T, O>(f: fn(Count) -> O) -> impl Operation<T>
+where
+ O: Operation<T> + 'static,
+{
+ struct CountFocusable<O> {
+ count: Count,
+ next: fn(Count) -> O,
+ }
+
+ impl<T, O> Operation<T> for CountFocusable<O>
+ where
+ O: Operation<T> + 'static,
+ {
+ fn focusable(&mut self, state: &mut dyn Focusable, _id: Option<&Id>) {
+ if state.is_focused() {
+ self.count.focused = Some(self.count.total);
+ }
+
+ self.count.total += 1;
+ }
+
+ fn container(
+ &mut self,
+ _id: Option<&Id>,
+ operate_on_children: &mut dyn FnMut(&mut dyn Operation<T>),
+ ) {
+ operate_on_children(self)
+ }
+
+ fn finish(&self) -> Outcome<T> {
+ Outcome::Chain(Box::new((self.next)(self.count)))
+ }
+ }
+
+ CountFocusable {
+ count: Count::default(),
+ next: f,
+ }
+}
+
+/// Produces an [`Operation`] that searches for the current focused widget, and
+/// - if found, focuses the previous focusable widget.
+/// - if not found, focuses the last focusable widget.
+pub fn focus_previous<T>() -> impl Operation<T> {
+ struct FocusPrevious {
+ count: Count,
+ current: usize,
+ }
+
+ impl<T> Operation<T> for FocusPrevious {
+ fn focusable(&mut self, state: &mut dyn Focusable, _id: Option<&Id>) {
+ if self.count.total == 0 {
+ return;
+ }
+
+ match self.count.focused {
+ None if self.current == self.count.total - 1 => state.focus(),
+ Some(0) if self.current == 0 => state.unfocus(),
+ Some(0) => {}
+ Some(focused) if focused == self.current => state.unfocus(),
+ Some(focused) if focused - 1 == self.current => state.focus(),
+ _ => {}
+ }
+
+ self.current += 1;
+ }
+
+ fn container(
+ &mut self,
+ _id: Option<&Id>,
+ operate_on_children: &mut dyn FnMut(&mut dyn Operation<T>),
+ ) {
+ operate_on_children(self)
+ }
+ }
+
+ count(|count| FocusPrevious { count, current: 0 })
+}
+
+/// Produces an [`Operation`] that searches for the current focused widget, and
+/// - if found, focuses the next focusable widget.
+/// - if not found, focuses the first focusable widget.
+pub fn focus_next<T>() -> impl Operation<T> {
+ struct FocusNext {
+ count: Count,
+ current: usize,
+ }
+
+ impl<T> Operation<T> for FocusNext {
+ fn focusable(&mut self, state: &mut dyn Focusable, _id: Option<&Id>) {
+ match self.count.focused {
+ None if self.current == 0 => state.focus(),
+ Some(focused) if focused == self.current => state.unfocus(),
+ Some(focused) if focused + 1 == self.current => state.focus(),
+ _ => {}
+ }
+
+ self.current += 1;
+ }
+
+ fn container(
+ &mut self,
+ _id: Option<&Id>,
+ operate_on_children: &mut dyn FnMut(&mut dyn Operation<T>),
+ ) {
+ operate_on_children(self)
+ }
+ }
+
+ count(|count| FocusNext { count, current: 0 })
+}
+
+/// Produces an [`Operation`] that searches for the current focused widget
+/// and stores its ID. This ignores widgets that do not have an ID.
+pub fn find_focused() -> impl Operation<Id> {
+ struct FindFocused {
+ focused: Option<Id>,
+ }
+
+ impl Operation<Id> for FindFocused {
+ fn focusable(&mut self, state: &mut dyn Focusable, id: Option<&Id>) {
+ if state.is_focused() && id.is_some() {
+ self.focused = id.cloned();
+ }
+ }
+
+ fn container(
+ &mut self,
+ _id: Option<&Id>,
+ operate_on_children: &mut dyn FnMut(&mut dyn Operation<Id>),
+ ) {
+ operate_on_children(self)
+ }
+
+ fn finish(&self) -> Outcome<Id> {
+ if let Some(id) = &self.focused {
+ Outcome::Some(id.clone())
+ } else {
+ Outcome::None
+ }
+ }
+ }
+
+ FindFocused { focused: None }
+}
diff --git a/core/src/widget/operation/scrollable.rs b/core/src/widget/operation/scrollable.rs
new file mode 100644
index 00000000..3b20631f
--- /dev/null
+++ b/core/src/widget/operation/scrollable.rs
@@ -0,0 +1,54 @@
+//! Operate on widgets that can be scrolled.
+use crate::widget::{Id, Operation};
+
+/// The internal state of a widget that can be scrolled.
+pub trait Scrollable {
+ /// Snaps the scroll of the widget to the given `percentage` along the horizontal & vertical axis.
+ fn snap_to(&mut self, offset: RelativeOffset);
+}
+
+/// Produces an [`Operation`] that snaps the widget with the given [`Id`] to
+/// the provided `percentage`.
+pub fn snap_to<T>(target: Id, offset: RelativeOffset) -> impl Operation<T> {
+ struct SnapTo {
+ target: Id,
+ offset: RelativeOffset,
+ }
+
+ impl<T> Operation<T> for SnapTo {
+ fn container(
+ &mut self,
+ _id: Option<&Id>,
+ operate_on_children: &mut dyn FnMut(&mut dyn Operation<T>),
+ ) {
+ operate_on_children(self)
+ }
+
+ fn scrollable(&mut self, state: &mut dyn Scrollable, id: Option<&Id>) {
+ if Some(&self.target) == id {
+ state.snap_to(self.offset);
+ }
+ }
+ }
+
+ SnapTo { target, offset }
+}
+
+/// The amount of offset in each direction of a [`Scrollable`].
+///
+/// A value of `0.0` means start, while `1.0` means end.
+#[derive(Debug, Clone, Copy, PartialEq, Default)]
+pub struct RelativeOffset {
+ /// The amount of horizontal offset
+ pub x: f32,
+ /// The amount of vertical offset
+ pub y: f32,
+}
+
+impl RelativeOffset {
+ /// A relative offset that points to the top-left of a [`Scrollable`].
+ pub const START: Self = Self { x: 0.0, y: 0.0 };
+
+ /// A relative offset that points to the bottom-right of a [`Scrollable`].
+ pub const END: Self = Self { x: 1.0, y: 1.0 };
+}
diff --git a/core/src/widget/operation/text_input.rs b/core/src/widget/operation/text_input.rs
new file mode 100644
index 00000000..4c773e99
--- /dev/null
+++ b/core/src/widget/operation/text_input.rs
@@ -0,0 +1,131 @@
+//! Operate on widgets that have text input.
+use crate::widget::operation::Operation;
+use crate::widget::Id;
+
+/// The internal state of a widget that has text input.
+pub trait TextInput {
+ /// Moves the cursor of the text input to the front of the input text.
+ fn move_cursor_to_front(&mut self);
+ /// Moves the cursor of the text input to the end of the input text.
+ fn move_cursor_to_end(&mut self);
+ /// Moves the cursor of the text input to an arbitrary location.
+ fn move_cursor_to(&mut self, position: usize);
+ /// Selects all the content of the text input.
+ fn select_all(&mut self);
+}
+
+/// Produces an [`Operation`] that moves the cursor of the widget with the given [`Id`] to the
+/// front.
+pub fn move_cursor_to_front<T>(target: Id) -> impl Operation<T> {
+ struct MoveCursor {
+ target: Id,
+ }
+
+ impl<T> Operation<T> for MoveCursor {
+ fn text_input(&mut self, state: &mut dyn TextInput, id: Option<&Id>) {
+ match id {
+ Some(id) if id == &self.target => {
+ state.move_cursor_to_front();
+ }
+ _ => {}
+ }
+ }
+
+ fn container(
+ &mut self,
+ _id: Option<&Id>,
+ operate_on_children: &mut dyn FnMut(&mut dyn Operation<T>),
+ ) {
+ operate_on_children(self)
+ }
+ }
+
+ MoveCursor { target }
+}
+
+/// Produces an [`Operation`] that moves the cursor of the widget with the given [`Id`] to the
+/// end.
+pub fn move_cursor_to_end<T>(target: Id) -> impl Operation<T> {
+ struct MoveCursor {
+ target: Id,
+ }
+
+ impl<T> Operation<T> for MoveCursor {
+ fn text_input(&mut self, state: &mut dyn TextInput, id: Option<&Id>) {
+ match id {
+ Some(id) if id == &self.target => {
+ state.move_cursor_to_end();
+ }
+ _ => {}
+ }
+ }
+
+ fn container(
+ &mut self,
+ _id: Option<&Id>,
+ operate_on_children: &mut dyn FnMut(&mut dyn Operation<T>),
+ ) {
+ operate_on_children(self)
+ }
+ }
+
+ MoveCursor { target }
+}
+
+/// Produces an [`Operation`] that moves the cursor of the widget with the given [`Id`] to the
+/// provided position.
+pub fn move_cursor_to<T>(target: Id, position: usize) -> impl Operation<T> {
+ struct MoveCursor {
+ target: Id,
+ position: usize,
+ }
+
+ impl<T> Operation<T> for MoveCursor {
+ fn text_input(&mut self, state: &mut dyn TextInput, id: Option<&Id>) {
+ match id {
+ Some(id) if id == &self.target => {
+ state.move_cursor_to(self.position);
+ }
+ _ => {}
+ }
+ }
+
+ fn container(
+ &mut self,
+ _id: Option<&Id>,
+ operate_on_children: &mut dyn FnMut(&mut dyn Operation<T>),
+ ) {
+ operate_on_children(self)
+ }
+ }
+
+ MoveCursor { target, position }
+}
+
+/// Produces an [`Operation`] that selects all the content of the widget with the given [`Id`].
+pub fn select_all<T>(target: Id) -> impl Operation<T> {
+ struct MoveCursor {
+ target: Id,
+ }
+
+ impl<T> Operation<T> for MoveCursor {
+ fn text_input(&mut self, state: &mut dyn TextInput, id: Option<&Id>) {
+ match id {
+ Some(id) if id == &self.target => {
+ state.select_all();
+ }
+ _ => {}
+ }
+ }
+
+ fn container(
+ &mut self,
+ _id: Option<&Id>,
+ operate_on_children: &mut dyn FnMut(&mut dyn Operation<T>),
+ ) {
+ operate_on_children(self)
+ }
+ }
+
+ MoveCursor { target }
+}
diff --git a/core/src/widget/text.rs b/core/src/widget/text.rs
new file mode 100644
index 00000000..485bb542
--- /dev/null
+++ b/core/src/widget/text.rs
@@ -0,0 +1,277 @@
+//! Write some text for your users to read.
+use crate::alignment;
+use crate::layout;
+use crate::renderer;
+use crate::text;
+use crate::widget::Tree;
+use crate::{
+ Color, Element, Layout, Length, Pixels, Point, Rectangle, Size, Widget,
+};
+
+use std::borrow::Cow;
+
+/// A paragraph of text.
+#[allow(missing_debug_implementations)]
+pub struct Text<'a, Renderer>
+where
+ Renderer: text::Renderer,
+ Renderer::Theme: StyleSheet,
+{
+ content: Cow<'a, str>,
+ size: Option<f32>,
+ width: Length,
+ height: Length,
+ horizontal_alignment: alignment::Horizontal,
+ vertical_alignment: alignment::Vertical,
+ font: Option<Renderer::Font>,
+ style: <Renderer::Theme as StyleSheet>::Style,
+}
+
+impl<'a, Renderer> Text<'a, Renderer>
+where
+ Renderer: text::Renderer,
+ Renderer::Theme: StyleSheet,
+{
+ /// Create a new fragment of [`Text`] with the given contents.
+ pub fn new(content: impl Into<Cow<'a, str>>) -> Self {
+ Text {
+ content: content.into(),
+ size: None,
+ font: None,
+ width: Length::Shrink,
+ height: Length::Shrink,
+ horizontal_alignment: alignment::Horizontal::Left,
+ vertical_alignment: alignment::Vertical::Top,
+ style: Default::default(),
+ }
+ }
+
+ /// Sets the size of the [`Text`].
+ pub fn size(mut self, size: impl Into<Pixels>) -> Self {
+ self.size = Some(size.into().0);
+ self
+ }
+
+ /// Sets the [`Font`] of the [`Text`].
+ ///
+ /// [`Font`]: crate::text::Renderer::Font
+ pub fn font(mut self, font: impl Into<Renderer::Font>) -> Self {
+ self.font = Some(font.into());
+ self
+ }
+
+ /// Sets the style of the [`Text`].
+ pub fn style(
+ mut self,
+ style: impl Into<<Renderer::Theme as StyleSheet>::Style>,
+ ) -> Self {
+ self.style = style.into();
+ self
+ }
+
+ /// Sets the width of the [`Text`] boundaries.
+ pub fn width(mut self, width: impl Into<Length>) -> Self {
+ self.width = width.into();
+ self
+ }
+
+ /// Sets the height of the [`Text`] boundaries.
+ pub fn height(mut self, height: impl Into<Length>) -> Self {
+ self.height = height.into();
+ self
+ }
+
+ /// Sets the [`alignment::Horizontal`] of the [`Text`].
+ pub fn horizontal_alignment(
+ mut self,
+ alignment: alignment::Horizontal,
+ ) -> Self {
+ self.horizontal_alignment = alignment;
+ self
+ }
+
+ /// Sets the [`alignment::Vertical`] of the [`Text`].
+ pub fn vertical_alignment(
+ mut self,
+ alignment: alignment::Vertical,
+ ) -> Self {
+ self.vertical_alignment = alignment;
+ self
+ }
+}
+
+impl<'a, Message, Renderer> Widget<Message, Renderer> for Text<'a, Renderer>
+where
+ Renderer: text::Renderer,
+ Renderer::Theme: StyleSheet,
+{
+ fn width(&self) -> Length {
+ self.width
+ }
+
+ fn height(&self) -> Length {
+ self.height
+ }
+
+ fn layout(
+ &self,
+ renderer: &Renderer,
+ limits: &layout::Limits,
+ ) -> layout::Node {
+ let limits = limits.width(self.width).height(self.height);
+
+ let size = self.size.unwrap_or_else(|| renderer.default_size());
+
+ let bounds = limits.max();
+
+ let (width, height) = renderer.measure(
+ &self.content,
+ size,
+ self.font.unwrap_or_else(|| renderer.default_font()),
+ bounds,
+ );
+
+ let size = limits.resolve(Size::new(width, height));
+
+ layout::Node::new(size)
+ }
+
+ fn draw(
+ &self,
+ _state: &Tree,
+ renderer: &mut Renderer,
+ theme: &Renderer::Theme,
+ style: &renderer::Style,
+ layout: Layout<'_>,
+ _cursor_position: Point,
+ _viewport: &Rectangle,
+ ) {
+ draw(
+ renderer,
+ style,
+ layout,
+ &self.content,
+ self.size,
+ self.font,
+ theme.appearance(self.style),
+ self.horizontal_alignment,
+ self.vertical_alignment,
+ );
+ }
+}
+
+/// Draws text using the same logic as the [`Text`] widget.
+///
+/// Specifically:
+///
+/// * If no `size` is provided, the default text size of the `Renderer` will be
+/// used.
+/// * If no `color` is provided, the [`renderer::Style::text_color`] will be
+/// used.
+/// * The alignment attributes do not affect the position of the bounds of the
+/// [`Layout`].
+pub fn draw<Renderer>(
+ renderer: &mut Renderer,
+ style: &renderer::Style,
+ layout: Layout<'_>,
+ content: &str,
+ size: Option<f32>,
+ font: Option<Renderer::Font>,
+ appearance: Appearance,
+ horizontal_alignment: alignment::Horizontal,
+ vertical_alignment: alignment::Vertical,
+) where
+ Renderer: text::Renderer,
+{
+ let bounds = layout.bounds();
+
+ let x = match horizontal_alignment {
+ alignment::Horizontal::Left => bounds.x,
+ alignment::Horizontal::Center => bounds.center_x(),
+ alignment::Horizontal::Right => bounds.x + bounds.width,
+ };
+
+ let y = match vertical_alignment {
+ alignment::Vertical::Top => bounds.y,
+ alignment::Vertical::Center => bounds.center_y(),
+ alignment::Vertical::Bottom => bounds.y + bounds.height,
+ };
+
+ renderer.fill_text(crate::Text {
+ content,
+ size: size.unwrap_or_else(|| renderer.default_size()),
+ bounds: Rectangle { x, y, ..bounds },
+ color: appearance.color.unwrap_or(style.text_color),
+ font: font.unwrap_or_else(|| renderer.default_font()),
+ horizontal_alignment,
+ vertical_alignment,
+ });
+}
+
+impl<'a, Message, Renderer> From<Text<'a, Renderer>>
+ for Element<'a, Message, Renderer>
+where
+ Renderer: text::Renderer + 'a,
+ Renderer::Theme: StyleSheet,
+{
+ fn from(text: Text<'a, Renderer>) -> Element<'a, Message, Renderer> {
+ Element::new(text)
+ }
+}
+
+impl<'a, Renderer> Clone for Text<'a, Renderer>
+where
+ Renderer: text::Renderer,
+ Renderer::Theme: StyleSheet,
+{
+ fn clone(&self) -> Self {
+ Self {
+ content: self.content.clone(),
+ size: self.size,
+ width: self.width,
+ height: self.height,
+ horizontal_alignment: self.horizontal_alignment,
+ vertical_alignment: self.vertical_alignment,
+ font: self.font,
+ style: self.style,
+ }
+ }
+}
+
+impl<'a, Renderer> From<&'a str> for Text<'a, Renderer>
+where
+ Renderer: text::Renderer,
+ Renderer::Theme: StyleSheet,
+{
+ fn from(content: &'a str) -> Self {
+ Self::new(content)
+ }
+}
+
+impl<'a, Message, Renderer> From<&'a str> for Element<'a, Message, Renderer>
+where
+ Renderer: text::Renderer + 'a,
+ Renderer::Theme: StyleSheet,
+{
+ fn from(content: &'a str) -> Self {
+ Text::from(content).into()
+ }
+}
+
+/// The style sheet of some text.
+pub trait StyleSheet {
+ /// The supported style of the [`StyleSheet`].
+ type Style: Default + Copy;
+
+ /// Produces the [`Appearance`] of some text.
+ fn appearance(&self, style: Self::Style) -> Appearance;
+}
+
+/// The apperance of some text.
+#[derive(Debug, Clone, Copy, Default)]
+pub struct Appearance {
+ /// The [`Color`] of the text.
+ ///
+ /// The default, `None`, means using the inherited color.
+ pub color: Option<Color>,
+}
diff --git a/core/src/widget/tree.rs b/core/src/widget/tree.rs
new file mode 100644
index 00000000..0af40c33
--- /dev/null
+++ b/core/src/widget/tree.rs
@@ -0,0 +1,187 @@
+//! Store internal widget state in a state tree to ensure continuity.
+use crate::Widget;
+
+use std::any::{self, Any};
+use std::borrow::Borrow;
+use std::fmt;
+
+/// A persistent state widget tree.
+///
+/// A [`Tree`] is normally associated with a specific widget in the widget tree.
+#[derive(Debug)]
+pub struct Tree {
+ /// The tag of the [`Tree`].
+ pub tag: Tag,
+
+ /// The [`State`] of the [`Tree`].
+ pub state: State,
+
+ /// The children of the root widget of the [`Tree`].
+ pub children: Vec<Tree>,
+}
+
+impl Tree {
+ /// Creates an empty, stateless [`Tree`] with no children.
+ pub fn empty() -> Self {
+ Self {
+ tag: Tag::stateless(),
+ state: State::None,
+ children: Vec::new(),
+ }
+ }
+
+ /// Creates a new [`Tree`] for the provided [`Widget`].
+ pub fn new<'a, Message, Renderer>(
+ widget: impl Borrow<dyn Widget<Message, Renderer> + 'a>,
+ ) -> Self
+ where
+ Renderer: crate::Renderer,
+ {
+ let widget = widget.borrow();
+
+ Self {
+ tag: widget.tag(),
+ state: widget.state(),
+ children: widget.children(),
+ }
+ }
+
+ /// Reconciliates the current tree with the provided [`Widget`].
+ ///
+ /// If the tag of the [`Widget`] matches the tag of the [`Tree`], then the
+ /// [`Widget`] proceeds with the reconciliation (i.e. [`Widget::diff`] is called).
+ ///
+ /// Otherwise, the whole [`Tree`] is recreated.
+ ///
+ /// [`Widget::diff`]: crate::Widget::diff
+ pub fn diff<'a, Message, Renderer>(
+ &mut self,
+ new: impl Borrow<dyn Widget<Message, Renderer> + 'a>,
+ ) where
+ Renderer: crate::Renderer,
+ {
+ if self.tag == new.borrow().tag() {
+ new.borrow().diff(self)
+ } else {
+ *self = Self::new(new);
+ }
+ }
+
+ /// Reconciliates the children of the tree with the provided list of widgets.
+ pub fn diff_children<'a, Message, Renderer>(
+ &mut self,
+ new_children: &[impl Borrow<dyn Widget<Message, Renderer> + 'a>],
+ ) where
+ Renderer: crate::Renderer,
+ {
+ self.diff_children_custom(
+ new_children,
+ |tree, widget| tree.diff(widget.borrow()),
+ |widget| Self::new(widget.borrow()),
+ )
+ }
+
+ /// Reconciliates the children of the tree with the provided list of widgets using custom
+ /// logic both for diffing and creating new widget state.
+ pub fn diff_children_custom<T>(
+ &mut self,
+ new_children: &[T],
+ diff: impl Fn(&mut Tree, &T),
+ new_state: impl Fn(&T) -> Self,
+ ) {
+ if self.children.len() > new_children.len() {
+ self.children.truncate(new_children.len());
+ }
+
+ for (child_state, new) in
+ self.children.iter_mut().zip(new_children.iter())
+ {
+ diff(child_state, new);
+ }
+
+ if self.children.len() < new_children.len() {
+ self.children.extend(
+ new_children[self.children.len()..].iter().map(new_state),
+ );
+ }
+ }
+}
+
+/// The identifier of some widget state.
+#[derive(Debug, Clone, Copy, PartialOrd, Ord, PartialEq, Eq, Hash)]
+pub struct Tag(any::TypeId);
+
+impl Tag {
+ /// Creates a [`Tag`] for a state of type `T`.
+ pub fn of<T>() -> Self
+ where
+ T: 'static,
+ {
+ Self(any::TypeId::of::<T>())
+ }
+
+ /// Creates a [`Tag`] for a stateless widget.
+ pub fn stateless() -> Self {
+ Self::of::<()>()
+ }
+}
+
+/// The internal [`State`] of a widget.
+pub enum State {
+ /// No meaningful internal state.
+ None,
+
+ /// Some meaningful internal state.
+ Some(Box<dyn Any>),
+}
+
+impl State {
+ /// Creates a new [`State`].
+ pub fn new<T>(state: T) -> Self
+ where
+ T: 'static,
+ {
+ State::Some(Box::new(state))
+ }
+
+ /// Downcasts the [`State`] to `T` and returns a reference to it.
+ ///
+ /// # Panics
+ /// This method will panic if the downcast fails or the [`State`] is [`State::None`].
+ pub fn downcast_ref<T>(&self) -> &T
+ where
+ T: 'static,
+ {
+ match self {
+ State::None => panic!("Downcast on stateless state"),
+ State::Some(state) => {
+ state.downcast_ref().expect("Downcast widget state")
+ }
+ }
+ }
+
+ /// Downcasts the [`State`] to `T` and returns a mutable reference to it.
+ ///
+ /// # Panics
+ /// This method will panic if the downcast fails or the [`State`] is [`State::None`].
+ pub fn downcast_mut<T>(&mut self) -> &mut T
+ where
+ T: 'static,
+ {
+ match self {
+ State::None => panic!("Downcast on stateless state"),
+ State::Some(state) => {
+ state.downcast_mut().expect("Downcast widget state")
+ }
+ }
+ }
+}
+
+impl fmt::Debug for State {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self {
+ Self::None => write!(f, "State::None"),
+ Self::Some(_) => write!(f, "State::Some"),
+ }
+ }
+}
diff --git a/core/src/window.rs b/core/src/window.rs
new file mode 100644
index 00000000..d829a4b4
--- /dev/null
+++ b/core/src/window.rs
@@ -0,0 +1,10 @@
+//! Build window-based GUI applications.
+mod event;
+mod mode;
+mod redraw_request;
+mod user_attention;
+
+pub use event::Event;
+pub use mode::Mode;
+pub use redraw_request::RedrawRequest;
+pub use user_attention::UserAttention;
diff --git a/core/src/window/event.rs b/core/src/window/event.rs
new file mode 100644
index 00000000..e2fb5e66
--- /dev/null
+++ b/core/src/window/event.rs
@@ -0,0 +1,58 @@
+use crate::time::Instant;
+
+use std::path::PathBuf;
+
+/// A window-related event.
+#[derive(PartialEq, Eq, Clone, Debug)]
+pub enum Event {
+ /// A window was moved.
+ Moved {
+ /// The new logical x location of the window
+ x: i32,
+ /// The new logical y location of the window
+ y: i32,
+ },
+
+ /// A window was resized.
+ Resized {
+ /// The new logical width of the window
+ width: u32,
+ /// The new logical height of the window
+ height: u32,
+ },
+
+ /// A window redraw was requested.
+ ///
+ /// The [`Instant`] contains the current time.
+ RedrawRequested(Instant),
+
+ /// The user has requested for the window to close.
+ ///
+ /// Usually, you will want to terminate the execution whenever this event
+ /// occurs.
+ CloseRequested,
+
+ /// A window was focused.
+ Focused,
+
+ /// A window was unfocused.
+ Unfocused,
+
+ /// A file is being hovered over the window.
+ ///
+ /// When the user hovers multiple files at once, this event will be emitted
+ /// for each file separately.
+ FileHovered(PathBuf),
+
+ /// A file has beend dropped into the window.
+ ///
+ /// When the user drops multiple files at once, this event will be emitted
+ /// for each file separately.
+ FileDropped(PathBuf),
+
+ /// A file was hovered, but has exited the window.
+ ///
+ /// There will be a single `FilesHoveredLeft` event triggered even if
+ /// multiple files were hovered.
+ FilesHoveredLeft,
+}
diff --git a/core/src/window/mode.rs b/core/src/window/mode.rs
new file mode 100644
index 00000000..fdce8e23
--- /dev/null
+++ b/core/src/window/mode.rs
@@ -0,0 +1,12 @@
+/// The mode of a window-based application.
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum Mode {
+ /// The application appears in its own window.
+ Windowed,
+
+ /// The application takes the whole screen of its current monitor.
+ Fullscreen,
+
+ /// The application is hidden
+ Hidden,
+}
diff --git a/core/src/window/redraw_request.rs b/core/src/window/redraw_request.rs
new file mode 100644
index 00000000..3b4f0fd3
--- /dev/null
+++ b/core/src/window/redraw_request.rs
@@ -0,0 +1,38 @@
+use crate::time::Instant;
+
+/// A request to redraw a window.
+#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
+pub enum RedrawRequest {
+ /// Redraw the next frame.
+ NextFrame,
+
+ /// Redraw at the given time.
+ At(Instant),
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use std::time::{Duration, Instant};
+
+ #[test]
+ fn ordering() {
+ let now = Instant::now();
+ let later = now + Duration::from_millis(10);
+
+ assert_eq!(RedrawRequest::NextFrame, RedrawRequest::NextFrame);
+ assert_eq!(RedrawRequest::At(now), RedrawRequest::At(now));
+
+ assert!(RedrawRequest::NextFrame < RedrawRequest::At(now));
+ assert!(RedrawRequest::At(now) > RedrawRequest::NextFrame);
+ assert!(RedrawRequest::At(now) < RedrawRequest::At(later));
+ assert!(RedrawRequest::At(later) > RedrawRequest::At(now));
+
+ assert!(RedrawRequest::NextFrame <= RedrawRequest::NextFrame);
+ assert!(RedrawRequest::NextFrame <= RedrawRequest::At(now));
+ assert!(RedrawRequest::At(now) >= RedrawRequest::NextFrame);
+ assert!(RedrawRequest::At(now) <= RedrawRequest::At(now));
+ assert!(RedrawRequest::At(now) <= RedrawRequest::At(later));
+ assert!(RedrawRequest::At(later) >= RedrawRequest::At(now));
+ }
+}
diff --git a/core/src/window/user_attention.rs b/core/src/window/user_attention.rs
new file mode 100644
index 00000000..b03dfeef
--- /dev/null
+++ b/core/src/window/user_attention.rs
@@ -0,0 +1,21 @@
+/// The type of user attention to request.
+///
+/// ## Platform-specific
+///
+/// - **X11:** Sets the WM's `XUrgencyHint`. No distinction between [`Critical`] and [`Informational`].
+///
+/// [`Critical`]: Self::Critical
+/// [`Informational`]: Self::Informational
+#[derive(Debug, Clone, Copy)]
+pub enum UserAttention {
+ /// ## Platform-specific
+ ///
+ /// - **macOS:** Bounces the dock icon until the application is in focus.
+ /// - **Windows:** Flashes both the window and the taskbar button until the application is in focus.
+ Critical,
+ /// ## Platform-specific
+ ///
+ /// - **macOS:** Bounces the dock icon once.
+ /// - **Windows:** Flashes the taskbar button until the application is in focus.
+ Informational,
+}