diff options
author | 2023-03-04 05:37:11 +0100 | |
---|---|---|
committer | 2023-03-04 05:37:11 +0100 | |
commit | 3a0d34c0240f4421737a6a08761f99d6f8140d02 (patch) | |
tree | c9a4a6b8e9c1db1b8fcd05bc98e3f131d5ef4bd5 /core | |
parent | c54409d1711e1f615c7ea4b02c082954e340632a (diff) | |
download | iced-3a0d34c0240f4421737a6a08761f99d6f8140d02.tar.gz iced-3a0d34c0240f4421737a6a08761f99d6f8140d02.tar.bz2 iced-3a0d34c0240f4421737a6a08761f99d6f8140d02.zip |
Create `iced_widget` subcrate and re-organize the whole codebase
Diffstat (limited to 'core')
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, +} |