From 1d0c44fb255f5fbf5a03e7c737e40ab66d39de9e Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Thu, 2 Feb 2023 01:24:27 +0100 Subject: Implement basic text caching in `iced_wgpu` --- core/src/font.rs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) (limited to 'core') diff --git a/core/src/font.rs b/core/src/font.rs index 3f9ad2b5..130e378e 100644 --- a/core/src/font.rs +++ b/core/src/font.rs @@ -1,3 +1,5 @@ +use std::hash::{Hash, Hasher}; + /// A font. #[derive(Debug, Clone, Copy)] pub enum Font { @@ -22,3 +24,17 @@ impl Default for Font { Font::Default } } + +impl Hash for Font { + fn hash(&self, hasher: &mut H) { + match self { + Self::Default => { + 0.hash(hasher); + } + Self::External { name, .. } => { + 1.hash(hasher); + name.hash(hasher); + } + } + } +} -- cgit From b29de28d1f0f608f8029c93d154cfd1b0f8b8cbb Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Sat, 4 Feb 2023 07:33:33 +0100 Subject: Overhaul `Font` type to allow font family selection --- core/src/font.rs | 53 +++++++++++++++++++++-------------------------------- 1 file changed, 21 insertions(+), 32 deletions(-) (limited to 'core') diff --git a/core/src/font.rs b/core/src/font.rs index 130e378e..1f774a70 100644 --- a/core/src/font.rs +++ b/core/src/font.rs @@ -1,40 +1,29 @@ -use std::hash::{Hash, Hasher}; +use std::hash::Hash; /// A font. -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum Font { - /// The default font. - /// - /// This is normally a font configured in a renderer or loaded from the - /// system. - Default, + /// The name of a font family of choice. + Name(&'static str), - /// An external font. - External { - /// The name of the external font - name: &'static str, + /// Serif fonts represent the formal text style for a script. + Serif, - /// The bytes of the external font - bytes: &'static [u8], - }, -} + /// Glyphs in sans-serif fonts, as the term is used in CSS, are generally low + /// contrast and have stroke endings that are plain — without any flaring, + /// cross stroke, or other ornamentation. + SansSerif, -impl Default for Font { - fn default() -> Font { - Font::Default - } -} + /// Glyphs in cursive fonts generally use a more informal script style, and + /// the result looks more like handwritten pen or brush writing than printed + /// letterwork. + Cursive, + + /// Fantasy fonts are primarily decorative or expressive fonts that contain + /// decorative or expressive representations of characters. + Fantasy, -impl Hash for Font { - fn hash(&self, hasher: &mut H) { - match self { - Self::Default => { - 0.hash(hasher); - } - Self::External { name, .. } => { - 1.hash(hasher); - name.hash(hasher); - } - } - } + /// The sole criterion of a monospace font is that all glyphs have the same + /// fixed width. + Monospace, } -- cgit From 32309f0140efb4ea92e3e35e3adc5c740909f196 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Sun, 5 Feb 2023 18:26:09 +0100 Subject: Introduce `Weight` enum to `font` --- core/src/font.rs | 16 ++++++++++++++++ core/src/lib.rs | 2 +- 2 files changed, 17 insertions(+), 1 deletion(-) (limited to 'core') diff --git a/core/src/font.rs b/core/src/font.rs index 1f774a70..b67c08af 100644 --- a/core/src/font.rs +++ b/core/src/font.rs @@ -1,3 +1,4 @@ +//! Load and use fonts. use std::hash::Hash; /// A font. @@ -27,3 +28,18 @@ pub enum Font { /// fixed width. Monospace, } + +/// The weight of some text. +#[allow(missing_docs)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum Weight { + Thin, + ExtraLight, + Light, + Normal, + Medium, + Semibold, + Bold, + ExtraBold, + Black, +} diff --git a/core/src/lib.rs b/core/src/lib.rs index d3596b4d..d7314851 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -25,6 +25,7 @@ #![forbid(unsafe_code, rust_2018_idioms)] #![allow(clippy::inherent_to_string, clippy::type_complexity)] pub mod alignment; +pub mod font; pub mod keyboard; pub mod mouse; pub mod time; @@ -32,7 +33,6 @@ pub mod time; mod background; mod color; mod content_fit; -mod font; mod length; mod padding; mod pixels; -- cgit From 5fd5d1cdf8e5354788dc40729c4565ef377d3bba Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Wed, 1 Mar 2023 21:34:26 +0100 Subject: Implement `Canvas` support for `iced_tiny_skia` --- core/Cargo.toml | 1 + core/src/gradient.rs | 117 ++++++++++++++++++++++++++++++++++++++++++++ core/src/gradient/linear.rs | 112 ++++++++++++++++++++++++++++++++++++++++++ core/src/lib.rs | 2 + 4 files changed, 232 insertions(+) create mode 100644 core/src/gradient.rs create mode 100644 core/src/gradient/linear.rs (limited to 'core') diff --git a/core/Cargo.toml b/core/Cargo.toml index 43865e4d..7ccb7b7a 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -9,6 +9,7 @@ repository = "https://github.com/iced-rs/iced" [dependencies] bitflags = "1.2" +thiserror = "1" [dependencies.palette] version = "0.6" diff --git a/core/src/gradient.rs b/core/src/gradient.rs new file mode 100644 index 00000000..61e919d6 --- /dev/null +++ b/core/src/gradient.rs @@ -0,0 +1,117 @@ +//! For creating a Gradient. +pub mod linear; + +pub use linear::Linear; + +use crate::{Color, Point, Size}; + +#[derive(Debug, Clone, PartialEq)] +/// A fill which transitions colors progressively along a direction, either linearly, radially (TBD), +/// or conically (TBD). +pub enum Gradient { + /// A linear gradient interpolates colors along a direction from its `start` to its `end` + /// point. + Linear(Linear), +} + +impl Gradient { + /// Creates a new linear [`linear::Builder`]. + pub fn linear(position: impl Into) -> linear::Builder { + linear::Builder::new(position.into()) + } +} + +#[derive(Debug, Clone, Copy, PartialEq)] +/// A point along the gradient vector where the specified [`color`] is unmixed. +/// +/// [`color`]: Self::color +pub struct ColorStop { + /// Offset along the gradient vector. + pub offset: f32, + + /// The color of the gradient at the specified [`offset`]. + /// + /// [`offset`]: Self::offset + pub color: Color, +} + +#[derive(Debug)] +/// The position of the gradient within its bounds. +pub enum Position { + /// The gradient will be positioned with respect to two points. + Absolute { + /// The starting point of the gradient. + start: Point, + /// The ending point of the gradient. + end: Point, + }, + /// The gradient will be positioned relative to the provided bounds. + Relative { + /// The top left position of the bounds. + top_left: Point, + /// The width & height of the bounds. + size: Size, + /// The start [Location] of the gradient. + start: Location, + /// The end [Location] of the gradient. + end: Location, + }, +} + +impl From<(Point, Point)> for Position { + fn from((start, end): (Point, Point)) -> Self { + Self::Absolute { start, end } + } +} + +#[derive(Debug, Clone, Copy)] +/// The location of a relatively-positioned gradient. +pub enum Location { + /// Top left. + TopLeft, + /// Top. + Top, + /// Top right. + TopRight, + /// Right. + Right, + /// Bottom right. + BottomRight, + /// Bottom. + Bottom, + /// Bottom left. + BottomLeft, + /// Left. + Left, +} + +impl Location { + fn to_absolute(self, top_left: Point, size: Size) -> Point { + match self { + Location::TopLeft => top_left, + Location::Top => { + Point::new(top_left.x + size.width / 2.0, top_left.y) + } + Location::TopRight => { + Point::new(top_left.x + size.width, top_left.y) + } + Location::Right => Point::new( + top_left.x + size.width, + top_left.y + size.height / 2.0, + ), + Location::BottomRight => { + Point::new(top_left.x + size.width, top_left.y + size.height) + } + Location::Bottom => Point::new( + top_left.x + size.width / 2.0, + top_left.y + size.height, + ), + Location::BottomLeft => { + Point::new(top_left.x, top_left.y + size.height) + } + Location::Left => { + Point::new(top_left.x, top_left.y + size.height / 2.0) + } + } + } +} diff --git a/core/src/gradient/linear.rs b/core/src/gradient/linear.rs new file mode 100644 index 00000000..c886db47 --- /dev/null +++ b/core/src/gradient/linear.rs @@ -0,0 +1,112 @@ +//! Linear gradient builder & definition. +use crate::gradient::{ColorStop, Gradient, Position}; +use crate::{Color, Point}; + +/// A linear gradient that can be used in the style of [`Fill`] or [`Stroke`]. +/// +/// [`Fill`]: crate::widget::canvas::Fill +/// [`Stroke`]: crate::widget::canvas::Stroke +#[derive(Debug, Clone, PartialEq)] +pub struct Linear { + /// The point where the linear gradient begins. + pub start: Point, + /// The point where the linear gradient ends. + pub end: Point, + /// [`ColorStop`]s along the linear gradient path. + pub color_stops: Vec, +} + +/// A [`Linear`] builder. +#[derive(Debug)] +pub struct Builder { + start: Point, + end: Point, + stops: Vec, + error: Option, +} + +impl Builder { + /// Creates a new [`Builder`]. + pub fn new(position: Position) -> Self { + let (start, end) = match position { + Position::Absolute { start, end } => (start, end), + Position::Relative { + top_left, + size, + start, + end, + } => ( + start.to_absolute(top_left, size), + end.to_absolute(top_left, size), + ), + }; + + Self { + start, + end, + stops: vec![], + error: None, + } + } + + /// Adds a new stop, defined by an offset and a color, to the gradient. + /// + /// `offset` must be between `0.0` and `1.0` or the gradient cannot be built. + /// + /// Note: when using the [`glow`] backend, any color stop added after the 16th + /// will not be displayed. + /// + /// On the [`wgpu`] backend this limitation does not exist (technical limit is 524,288 stops). + /// + /// [`glow`]: https://docs.rs/iced_glow + /// [`wgpu`]: https://docs.rs/iced_wgpu + pub fn add_stop(mut self, offset: f32, color: Color) -> Self { + if offset.is_finite() && (0.0..=1.0).contains(&offset) { + match self.stops.binary_search_by(|stop| { + stop.offset.partial_cmp(&offset).unwrap() + }) { + Ok(_) => { + self.error = Some(BuilderError::DuplicateOffset(offset)) + } + Err(index) => { + self.stops.insert(index, ColorStop { offset, color }); + } + } + } else { + self.error = Some(BuilderError::InvalidOffset(offset)) + }; + + self + } + + /// Builds the linear [`Gradient`] of this [`Builder`]. + /// + /// Returns `BuilderError` if gradient in invalid. + pub fn build(self) -> Result { + if self.stops.is_empty() { + Err(BuilderError::MissingColorStop) + } else if let Some(error) = self.error { + Err(error) + } else { + Ok(Gradient::Linear(Linear { + start: self.start, + end: self.end, + color_stops: self.stops, + })) + } + } +} + +/// An error that happened when building a [`Linear`] gradient. +#[derive(Debug, thiserror::Error)] +pub enum BuilderError { + #[error("Gradients must contain at least one color stop.")] + /// Gradients must contain at least one color stop. + MissingColorStop, + #[error("Offset {0} must be a unique, finite number.")] + /// Offsets in a gradient must all be unique & finite. + DuplicateOffset(f32), + #[error("Offset {0} must be between 0.0..=1.0.")] + /// Offsets in a gradient must be between 0.0..=1.0. + InvalidOffset(f32), +} diff --git a/core/src/lib.rs b/core/src/lib.rs index d7314851..1e4f0411 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -26,6 +26,7 @@ #![allow(clippy::inherent_to_string, clippy::type_complexity)] pub mod alignment; pub mod font; +pub mod gradient; pub mod keyboard; pub mod mouse; pub mod time; @@ -46,6 +47,7 @@ pub use background::Background; pub use color::Color; pub use content_fit::ContentFit; pub use font::Font; +pub use gradient::Gradient; pub use length::Length; pub use padding::Padding; pub use pixels::Pixels; -- cgit From 3a0d34c0240f4421737a6a08761f99d6f8140d02 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Sat, 4 Mar 2023 05:37:11 +0100 Subject: Create `iced_widget` subcrate and re-organize the whole codebase --- core/Cargo.toml | 1 + core/src/clipboard.rs | 23 ++ core/src/element.rs | 608 ++++++++++++++++++++++++++++++++ core/src/event.rs | 78 ++++ core/src/hasher.rs | 13 + core/src/image.rs | 174 +++++++++ core/src/layout.rs | 65 ++++ core/src/layout/DRUID_LICENSE | 202 +++++++++++ core/src/layout/flex.rs | 232 ++++++++++++ core/src/layout/limits.rs | 163 +++++++++ core/src/layout/node.rs | 91 +++++ core/src/lib.rs | 24 ++ core/src/mouse.rs | 3 + core/src/mouse/click.rs | 76 ++++ core/src/overlay.rs | 122 +++++++ core/src/overlay/element.rs | 270 ++++++++++++++ core/src/overlay/group.rs | 172 +++++++++ core/src/renderer.rs | 99 ++++++ core/src/renderer/null.rs | 82 +++++ core/src/shell.rs | 108 ++++++ core/src/svg.rs | 89 +++++ core/src/text.rs | 111 ++++++ core/src/touch.rs | 23 ++ core/src/widget.rs | 145 ++++++++ core/src/widget/id.rs | 43 +++ core/src/widget/operation.rs | 112 ++++++ core/src/widget/operation/focusable.rs | 203 +++++++++++ core/src/widget/operation/scrollable.rs | 54 +++ core/src/widget/operation/text_input.rs | 131 +++++++ core/src/widget/text.rs | 277 +++++++++++++++ core/src/widget/tree.rs | 187 ++++++++++ core/src/window.rs | 10 + core/src/window/event.rs | 58 +++ core/src/window/mode.rs | 12 + core/src/window/redraw_request.rs | 38 ++ core/src/window/user_attention.rs | 21 ++ 36 files changed, 4120 insertions(+) create mode 100644 core/src/clipboard.rs create mode 100644 core/src/element.rs create mode 100644 core/src/event.rs create mode 100644 core/src/hasher.rs create mode 100644 core/src/image.rs create mode 100644 core/src/layout.rs create mode 100644 core/src/layout/DRUID_LICENSE create mode 100644 core/src/layout/flex.rs create mode 100644 core/src/layout/limits.rs create mode 100644 core/src/layout/node.rs create mode 100644 core/src/mouse/click.rs create mode 100644 core/src/overlay.rs create mode 100644 core/src/overlay/element.rs create mode 100644 core/src/overlay/group.rs create mode 100644 core/src/renderer.rs create mode 100644 core/src/renderer/null.rs create mode 100644 core/src/shell.rs create mode 100644 core/src/svg.rs create mode 100644 core/src/text.rs create mode 100644 core/src/touch.rs create mode 100644 core/src/widget.rs create mode 100644 core/src/widget/id.rs create mode 100644 core/src/widget/operation.rs create mode 100644 core/src/widget/operation/focusable.rs create mode 100644 core/src/widget/operation/scrollable.rs create mode 100644 core/src/widget/operation/text_input.rs create mode 100644 core/src/widget/text.rs create mode 100644 core/src/widget/tree.rs create mode 100644 core/src/window.rs create mode 100644 core/src/window/event.rs create mode 100644 core/src/window/mode.rs create mode 100644 core/src/window/redraw_request.rs create mode 100644 core/src/window/user_attention.rs (limited to 'core') 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; + + /// 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 { + 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` +/// to turn it into an [`Element`]. +/// +/// [built-in widget]: crate::widget +#[allow(missing_debug_implementations)] +pub struct Element<'a, Message, Renderer> { + widget: Box + 'a>, +} + +impl<'a, Message, Renderer> Element<'a, Message, Renderer> { + /// Creates a new [`Element`] containing the given [`Widget`]. + pub fn new(widget: impl Widget + '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 { + self.widget.as_ref() + } + + /// Returns a mutable reference to the [`Widget`] of the [`Element`], + pub fn as_widget_mut(&mut self) -> &mut dyn Widget { + 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, + /// } + /// ``` + /// + /// 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 { + /// # unimplemented!() + /// # } + /// # } + /// # } + /// # + /// # mod iced { + /// # pub use iced_core::renderer::Null as Renderer; + /// # pub use iced_core::Element; + /// # + /// # pub mod widget { + /// # pub struct Row { + /// # _t: std::marker::PhantomData, + /// # } + /// # + /// # impl Row { + /// # pub fn new() -> Self { + /// # unimplemented!() + /// # } + /// # + /// # pub fn spacing(mut self, _: u32) -> Self { + /// # unimplemented!() + /// # } + /// # + /// # pub fn push( + /// # mut self, + /// # _: iced_core::Element, + /// # ) -> Self { + /// # unimplemented!() + /// # } + /// # } + /// # } + /// # } + /// # + /// use counter::Counter; + /// + /// use iced::widget::Row; + /// use iced::{Element, Renderer}; + /// + /// struct ManyCounters { + /// counters: Vec, + /// } + /// + /// #[derive(Debug, Clone, Copy)] + /// pub enum Message { + /// Counter(usize, counter::Message), + /// } + /// + /// impl ManyCounters { + /// pub fn view(&mut self) -> Row { + /// // 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.view().into(); + /// + /// row.push( + /// // Here we turn our `Element` into + /// // an `Element` 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, + /// # } + /// # + /// # #[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( + 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>( + 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 + 'a> + for Element<'a, Message, Renderer> +{ + fn borrow(&self) -> &(dyn Widget + 'a) { + self.widget.borrow() + } +} + +impl<'a, Message, Renderer> Borrow + 'a> + for &Element<'a, Message, Renderer> +{ + fn borrow(&self) -> &(dyn Widget + 'a) { + self.widget.borrow() + } +} + +struct Map<'a, A, B, Renderer> { + widget: Box + 'a>, + mapper: Box B + 'a>, +} + +impl<'a, A, B, Renderer> Map<'a, A, B, Renderer> { + pub fn new( + widget: Box + '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 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 { + 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, + ) { + struct MapOperation<'a, B> { + operation: &'a mut dyn widget::Operation, + } + + impl<'a, T, B> widget::Operation for MapOperation<'a, B> { + fn container( + &mut self, + id: Option<&widget::Id>, + operate_on_children: &mut dyn FnMut( + &mut dyn widget::Operation, + ), + ) { + 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> { + 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 + 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 { + 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, + ) { + 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: &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> { + 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>(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` 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 From for Handle +where + T: Into, +{ + fn from(path: T) -> Handle { + Handle::from_path(path.into()) + } +} + +impl Hash for Handle { + fn hash(&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 + 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(&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; + + /// 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> { + 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( + 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 = 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) -> 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) -> 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, +} + +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) -> 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 { + 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 +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, + ) { + } + + /// 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> +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::>(); + + (!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 + '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 + '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(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, + ) { + 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 + 'a>, + mapper: &'a dyn Fn(A) -> B, +} + +impl<'a, A, B, Renderer> Map<'a, A, B, Renderer> { + pub fn new( + content: Box + 'a>, + mapper: &'a dyn Fn(A) -> B, + ) -> Map<'a, A, B, Renderer> { + Map { content, mapper } + } +} + +impl<'a, A, B, Renderer> Overlay 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, + ) { + struct MapOperation<'a, B> { + operation: &'a mut dyn widget::Operation, + } + + impl<'a, T, B> widget::Operation for MapOperation<'a, B> { + fn container( + &mut self, + id: Option<&widget::Id>, + operate_on_children: &mut dyn FnMut( + &mut dyn widget::Operation, + ), + ) { + 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>, +} + +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>, + ) -> Self { + Group { children } + } + + /// Adds an [`overlay::Element`] to the [`Group`]. + pub fn push( + mut self, + child: impl Into>, + ) -> 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 + 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: &::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, + ) { + 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> + 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( + &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); + + /// 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 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 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, + ) { + } +} + +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 { + 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, + redraw_request: Option, + 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) -> 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 { + 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(&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, +} + +impl Handle { + /// Creates an SVG [`Handle`] pointing to the vector image of the given + /// path. + pub fn from_path(path: impl Into) -> 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>) -> 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(&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; + + /// Draws an SVG with the given [`Handle`], an optional [`Color`] filter, and inside the provided `bounds`. + fn draw(&mut self, handle: Handle, color: Option, 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; + + /// 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 +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 { + 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, + ) { + } + + /// 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> { + 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>) -> 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 { + /// 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), + ); + + /// 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 { + Outcome::None + } +} + +/// The result of an [`Operation`]. +pub enum Outcome { + /// The [`Operation`] produced no result. + None, + + /// The [`Operation`] produced some result. + Some(T), + + /// The [`Operation`] needs to be followed by another [`Operation`]. + Chain(Box>), +} + +impl fmt::Debug for Outcome +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( + target: Id, + operation: impl Operation + 'static, +) -> impl Operation { + struct ScopedOperation { + target: Id, + operation: Box>, + } + + impl Operation for ScopedOperation { + fn container( + &mut self, + id: Option<&Id>, + operate_on_children: &mut dyn FnMut(&mut dyn Operation), + ) { + if id == Some(&self.target) { + operate_on_children(self.operation.as_mut()); + } else { + operate_on_children(self); + } + } + + fn finish(&self) -> Outcome { + 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, + + /// The total amount of focusable widgets. + pub total: usize, +} + +/// Produces an [`Operation`] that focuses the widget with the given [`Id`]. +pub fn focus(target: Id) -> impl Operation { + struct Focus { + target: Id, + } + + impl Operation 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), + ) { + 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(f: fn(Count) -> O) -> impl Operation +where + O: Operation + 'static, +{ + struct CountFocusable { + count: Count, + next: fn(Count) -> O, + } + + impl Operation for CountFocusable + where + O: Operation + '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), + ) { + operate_on_children(self) + } + + fn finish(&self) -> Outcome { + 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() -> impl Operation { + struct FocusPrevious { + count: Count, + current: usize, + } + + impl Operation 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), + ) { + 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() -> impl Operation { + struct FocusNext { + count: Count, + current: usize, + } + + impl Operation 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), + ) { + 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 { + struct FindFocused { + focused: Option, + } + + impl Operation 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), + ) { + operate_on_children(self) + } + + fn finish(&self) -> Outcome { + 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(target: Id, offset: RelativeOffset) -> impl Operation { + struct SnapTo { + target: Id, + offset: RelativeOffset, + } + + impl Operation for SnapTo { + fn container( + &mut self, + _id: Option<&Id>, + operate_on_children: &mut dyn FnMut(&mut dyn Operation), + ) { + 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(target: Id) -> impl Operation { + struct MoveCursor { + target: Id, + } + + impl Operation 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), + ) { + 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(target: Id) -> impl Operation { + struct MoveCursor { + target: Id, + } + + impl Operation 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), + ) { + 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(target: Id, position: usize) -> impl Operation { + struct MoveCursor { + target: Id, + position: usize, + } + + impl Operation 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), + ) { + 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(target: Id) -> impl Operation { + struct MoveCursor { + target: Id, + } + + impl Operation 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), + ) { + 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, + width: Length, + height: Length, + horizontal_alignment: alignment::Horizontal, + vertical_alignment: alignment::Vertical, + font: Option, + style: ::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>) -> 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) -> 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) -> Self { + self.font = Some(font.into()); + self + } + + /// Sets the style of the [`Text`]. + pub fn style( + mut self, + style: impl Into<::Style>, + ) -> Self { + self.style = style.into(); + self + } + + /// Sets the width of the [`Text`] boundaries. + pub fn width(mut self, width: impl Into) -> Self { + self.width = width.into(); + self + } + + /// Sets the height of the [`Text`] boundaries. + pub fn height(mut self, height: impl Into) -> 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 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: &mut Renderer, + style: &renderer::Style, + layout: Layout<'_>, + content: &str, + size: Option, + font: Option, + 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> + 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, +} 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, +} + +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 + '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 + '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 + '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( + &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() -> Self + where + T: 'static, + { + Self(any::TypeId::of::()) + } + + /// 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), +} + +impl State { + /// Creates a new [`State`]. + pub fn new(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(&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(&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, +} -- cgit From 8af69be47e88896b3c5f70174db609eee0c67971 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Sun, 5 Mar 2023 06:23:40 +0100 Subject: Converge `Command` types from `iced_futures` and `iced_native` --- core/src/widget/operation.rs | 116 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 115 insertions(+), 1 deletion(-) (limited to 'core') diff --git a/core/src/widget/operation.rs b/core/src/widget/operation.rs index 53688a21..ad188c36 100644 --- a/core/src/widget/operation.rs +++ b/core/src/widget/operation.rs @@ -11,6 +11,7 @@ use crate::widget::Id; use std::any::Any; use std::fmt; +use std::rc::Rc; /// A piece of logic that can traverse the widget tree of an application in /// order to query or update some widget state. @@ -68,9 +69,122 @@ where } } +/// Maps the output of an [`Operation`] using the given function. +pub fn map( + operation: Box>, + f: impl Fn(A) -> B + 'static, +) -> impl Operation +where + A: 'static, + B: 'static, +{ + #[allow(missing_debug_implementations)] + struct Map { + operation: Box>, + f: Rc B>, + } + + impl Operation for Map + where + A: 'static, + B: 'static, + { + fn container( + &mut self, + id: Option<&Id>, + operate_on_children: &mut dyn FnMut(&mut dyn Operation), + ) { + struct MapRef<'a, A> { + operation: &'a mut dyn Operation, + } + + impl<'a, A, B> Operation for MapRef<'a, A> { + fn container( + &mut self, + id: Option<&Id>, + operate_on_children: &mut dyn FnMut(&mut dyn Operation), + ) { + let Self { operation, .. } = self; + + operation.container(id, &mut |operation| { + operate_on_children(&mut MapRef { operation }); + }); + } + + fn scrollable( + &mut self, + state: &mut dyn Scrollable, + id: Option<&Id>, + ) { + self.operation.scrollable(state, id); + } + + fn focusable( + &mut self, + state: &mut dyn Focusable, + id: Option<&Id>, + ) { + self.operation.focusable(state, id); + } + + fn text_input( + &mut self, + state: &mut dyn TextInput, + id: Option<&Id>, + ) { + self.operation.text_input(state, id); + } + + fn custom(&mut self, state: &mut dyn Any, id: Option<&Id>) { + self.operation.custom(state, id); + } + } + + let Self { operation, .. } = self; + + MapRef { + operation: operation.as_mut(), + } + .container(id, operate_on_children); + } + + fn focusable(&mut self, state: &mut dyn Focusable, id: Option<&Id>) { + self.operation.focusable(state, id); + } + + fn scrollable(&mut self, state: &mut dyn Scrollable, id: Option<&Id>) { + self.operation.scrollable(state, id); + } + + fn text_input(&mut self, state: &mut dyn TextInput, id: Option<&Id>) { + self.operation.text_input(state, id); + } + + fn custom(&mut self, state: &mut dyn Any, id: Option<&Id>) { + self.operation.custom(state, id); + } + + fn finish(&self) -> Outcome { + match self.operation.finish() { + Outcome::None => Outcome::None, + Outcome::Some(output) => Outcome::Some((self.f)(output)), + Outcome::Chain(next) => Outcome::Chain(Box::new(Map { + operation: next, + f: self.f.clone(), + })), + } + } + } + + Map { + operation, + f: Rc::new(f), + } +} + /// Produces an [`Operation`] that applies the given [`Operation`] to the /// children of a container with the given [`Id`]. -pub fn scoped( +pub fn scope( target: Id, operation: impl Operation + 'static, ) -> impl Operation { -- cgit From 5b3977daf6df624ca5d5e1a21ce282161234b22d Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Tue, 7 Mar 2023 06:09:51 +0100 Subject: Implement `vector` pipeline in `iced_tiny_skia` --- core/src/size.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'core') diff --git a/core/src/size.rs b/core/src/size.rs index fbe940ef..7ef2f602 100644 --- a/core/src/size.rs +++ b/core/src/size.rs @@ -1,7 +1,7 @@ use crate::{Padding, Vector}; /// An amount of space in 2 dimensions. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct Size { /// The width. pub width: T, -- cgit From 707de9d788dc3c49d4ac57a19afac1bb938b78d9 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Thu, 30 Mar 2023 00:56:00 +0200 Subject: Introduce support for `Font` attributes --- core/src/font.rs | 44 +++++++++++++++++++++++++++++++++++++++++--- core/src/renderer/null.rs | 4 ++-- 2 files changed, 43 insertions(+), 5 deletions(-) (limited to 'core') diff --git a/core/src/font.rs b/core/src/font.rs index b67c08af..920b3dd5 100644 --- a/core/src/font.rs +++ b/core/src/font.rs @@ -2,8 +2,44 @@ use std::hash::Hash; /// A font. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub enum Font { +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)] +pub struct Font { + /// The [`Family`] of the [`Font`]. + pub family: Family, + /// The [`Weight`] of the [`Font`]. + pub weight: Weight, + /// Whether if the [`Font`] is monospaced or not. + pub monospaced: bool, +} + +impl Font { + /// A non-monospaced sans-serif font with normal [`Weight`]. + pub const DEFAULT: Font = Font { + family: Family::SansSerif, + weight: Weight::Normal, + monospaced: false, + }; + + /// A monospaced font with normal [`Weight`]. + pub const MONOSPACE: Font = Font { + family: Family::Monospace, + monospaced: true, + ..Self::DEFAULT + }; + + /// Creates a non-monospaced [`Font`] with the given [`Family::Name`] and + /// normal [`Weight`]. + pub const fn with_name(name: &'static str) -> Self { + Font { + family: Family::Name(name), + ..Self::DEFAULT + } + } +} + +/// A font family. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)] +pub enum Family { /// The name of a font family of choice. Name(&'static str), @@ -13,6 +49,7 @@ pub enum Font { /// Glyphs in sans-serif fonts, as the term is used in CSS, are generally low /// contrast and have stroke endings that are plain — without any flaring, /// cross stroke, or other ornamentation. + #[default] SansSerif, /// Glyphs in cursive fonts generally use a more informal script style, and @@ -31,11 +68,12 @@ pub enum Font { /// The weight of some text. #[allow(missing_docs)] -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)] pub enum Weight { Thin, ExtraLight, Light, + #[default] Normal, Medium, Semibold, diff --git a/core/src/renderer/null.rs b/core/src/renderer/null.rs index d93338ae..88c58825 100644 --- a/core/src/renderer/null.rs +++ b/core/src/renderer/null.rs @@ -42,12 +42,12 @@ impl Renderer for Null { impl text::Renderer for Null { type Font = Font; - const ICON_FONT: Font = Font::SansSerif; + const ICON_FONT: Font = Font::DEFAULT; const CHECKMARK_ICON: char = '0'; const ARROW_DOWN_ICON: char = '0'; fn default_font(&self) -> Self::Font { - Font::SansSerif + Font::default() } fn default_size(&self) -> f32 { -- cgit From 0b459c8e240abf83bb62902a504c018194acdbb6 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Thu, 30 Mar 2023 02:01:20 +0200 Subject: Introduce `font::Stretch` --- core/src/font.rs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) (limited to 'core') diff --git a/core/src/font.rs b/core/src/font.rs index 920b3dd5..bb425fd6 100644 --- a/core/src/font.rs +++ b/core/src/font.rs @@ -8,6 +8,8 @@ pub struct Font { pub family: Family, /// The [`Weight`] of the [`Font`]. pub weight: Weight, + /// The [`Stretch`] of the [`Font`]. + pub stretch: Stretch, /// Whether if the [`Font`] is monospaced or not. pub monospaced: bool, } @@ -17,6 +19,7 @@ impl Font { pub const DEFAULT: Font = Font { family: Family::SansSerif, weight: Weight::Normal, + stretch: Stretch::Normal, monospaced: false, }; @@ -81,3 +84,19 @@ pub enum Weight { ExtraBold, Black, } + +/// The width of some text. +#[allow(missing_docs)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)] +pub enum Stretch { + UltraCondensed, + ExtraCondensed, + Condensed, + SemiCondensed, + #[default] + Normal, + SemiExpanded, + Expanded, + ExtraExpanded, + UltraExpanded, +} -- cgit From 0f7abffc0e94b4bb9f8117db633bfd07d900eb93 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Wed, 22 Mar 2023 00:36:57 +0100 Subject: Draft (very) basic incremental rendering for `iced_tiny_skia` --- core/src/image.rs | 30 +++++++++++++++++++++++++++++- core/src/rectangle.rs | 34 ++++++++++++++++++++++++++++++++++ core/src/svg.rs | 4 ++-- 3 files changed, 65 insertions(+), 3 deletions(-) (limited to 'core') diff --git a/core/src/image.rs b/core/src/image.rs index 70fbade0..618235ef 100644 --- a/core/src/image.rs +++ b/core/src/image.rs @@ -6,7 +6,7 @@ use std::path::PathBuf; use std::sync::Arc; /// A handle of some image data. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct Handle { id: u64, data: Data, @@ -156,6 +156,34 @@ impl std::fmt::Debug for Data { } } +impl PartialEq for Data { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (Self::Path(a), Self::Path(b)) => a == b, + (Self::Bytes(a), Self::Bytes(b)) => a.as_ref() == b.as_ref(), + ( + Self::Rgba { + width: width_a, + height: height_a, + pixels: pixels_a, + }, + Self::Rgba { + width: width_b, + height: height_b, + pixels: pixels_b, + }, + ) => { + width_a == width_b + && height_a == height_b + && pixels_a.as_ref() == pixels_b.as_ref() + } + _ => false, + } + } +} + +impl Eq for Data {} + /// A [`Renderer`] that can render raster graphics. /// /// [renderer]: crate::renderer diff --git a/core/src/rectangle.rs b/core/src/rectangle.rs index 4fe91519..5cdcbe78 100644 --- a/core/src/rectangle.rs +++ b/core/src/rectangle.rs @@ -100,6 +100,30 @@ impl Rectangle { } } + /// Returns whether the [`Rectangle`] intersects with the given one. + pub fn intersects(&self, other: &Self) -> bool { + self.intersection(other).is_some() + } + + /// Computes the union with the given [`Rectangle`]. + pub fn union(&self, other: &Self) -> Self { + let x = self.x.min(other.x); + let y = self.y.min(other.y); + + let lower_right_x = (self.x + self.width).max(other.x + other.width); + let lower_right_y = (self.y + self.height).max(other.y + other.height); + + let width = lower_right_x - x; + let height = lower_right_y - y; + + Rectangle { + x, + y, + width, + height, + } + } + /// Snaps the [`Rectangle`] to __unsigned__ integer coordinates. pub fn snap(self) -> Rectangle { Rectangle { @@ -109,6 +133,16 @@ impl Rectangle { height: self.height as u32, } } + + /// Expands the [`Rectangle`] a given amount. + pub fn expand(self, amount: f32) -> Self { + Self { + x: self.x - amount, + y: self.y - amount, + width: self.width + amount * 2.0, + height: self.height + amount * 2.0, + } + } } impl std::ops::Mul for Rectangle { diff --git a/core/src/svg.rs b/core/src/svg.rs index 9b98877a..54e9434e 100644 --- a/core/src/svg.rs +++ b/core/src/svg.rs @@ -7,7 +7,7 @@ use std::path::PathBuf; use std::sync::Arc; /// A handle of Svg data. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct Handle { id: u64, data: Arc, @@ -57,7 +57,7 @@ impl Hash for Handle { } /// The data of a vectorial image. -#[derive(Clone, Hash)] +#[derive(Clone, Hash, PartialEq, Eq)] pub enum Data { /// File data Path(PathBuf), -- cgit From f8cd1faa286daaf34cc532bf6d34b932b32eb35a Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Wed, 5 Apr 2023 04:10:00 +0200 Subject: Group damage regions by area increase --- core/src/rectangle.rs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) (limited to 'core') diff --git a/core/src/rectangle.rs b/core/src/rectangle.rs index 5cdcbe78..7ff324cb 100644 --- a/core/src/rectangle.rs +++ b/core/src/rectangle.rs @@ -66,6 +66,11 @@ impl Rectangle { Size::new(self.width, self.height) } + /// Returns the area of the [`Rectangle`]. + pub fn area(&self) -> f32 { + self.width * self.height + } + /// Returns true if the given [`Point`] is contained in the [`Rectangle`]. pub fn contains(&self, point: Point) -> bool { self.x <= point.x @@ -74,6 +79,15 @@ impl Rectangle { && point.y <= self.y + self.height } + /// Returns true if the current [`Rectangle`] is completely within the given + /// `container`. + pub fn is_within(&self, container: &Rectangle) -> bool { + container.contains(self.position()) + && container.contains( + self.position() + Vector::new(self.width, self.height), + ) + } + /// Computes the intersection with the given [`Rectangle`]. pub fn intersection( &self, -- cgit From 4cae262d22cde458cbb5d0667e4bcf6adce9242a Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Wed, 5 Apr 2023 05:47:29 +0200 Subject: Implement `PartialEq` and `Eq` for `image::Bytes` --- core/src/image.rs | 38 +++++++++----------------------------- 1 file changed, 9 insertions(+), 29 deletions(-) (limited to 'core') diff --git a/core/src/image.rs b/core/src/image.rs index 618235ef..85d9d475 100644 --- a/core/src/image.rs +++ b/core/src/image.rs @@ -110,6 +110,14 @@ impl std::hash::Hash for Bytes { } } +impl PartialEq for Bytes { + fn eq(&self, other: &Self) -> bool { + self.as_ref() == other.as_ref() + } +} + +impl Eq for Bytes {} + impl AsRef<[u8]> for Bytes { fn as_ref(&self) -> &[u8] { self.0.as_ref().as_ref() @@ -125,7 +133,7 @@ impl std::ops::Deref for Bytes { } /// The data of a raster image. -#[derive(Clone, Hash)] +#[derive(Clone, PartialEq, Eq, Hash)] pub enum Data { /// File data Path(PathBuf), @@ -156,34 +164,6 @@ impl std::fmt::Debug for Data { } } -impl PartialEq for Data { - fn eq(&self, other: &Self) -> bool { - match (self, other) { - (Self::Path(a), Self::Path(b)) => a == b, - (Self::Bytes(a), Self::Bytes(b)) => a.as_ref() == b.as_ref(), - ( - Self::Rgba { - width: width_a, - height: height_a, - pixels: pixels_a, - }, - Self::Rgba { - width: width_b, - height: height_b, - pixels: pixels_b, - }, - ) => { - width_a == width_b - && height_a == height_b - && pixels_a.as_ref() == pixels_b.as_ref() - } - _ => false, - } - } -} - -impl Eq for Data {} - /// A [`Renderer`] that can render raster graphics. /// /// [renderer]: crate::renderer -- cgit From 47016a37dfe51ebc3cdf290d8afb34ceadb8edae Mon Sep 17 00:00:00 2001 From: Ian Douglas Scott Date: Thu, 27 Apr 2023 15:36:45 -0700 Subject: Do not require `Copy` for text `StyleSheet::Style` For most widgets, `Style` only requires `Default`. A few require `Clone`. Only this one requires `Copy`. Some of the types in the default theme has a custom variant requiring `Box`, or `Rc` to provide `Clone`, but this isn't possible if `Copy` is required. It would be good to also address the inconsistency of requiring `Clone` in some places and not others. This removes `style/src/text.rs` which is unused in this branch and thus confusing. If there's a reason to keep it, that can be removed from the change. --- core/src/widget/text.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'core') diff --git a/core/src/widget/text.rs b/core/src/widget/text.rs index 485bb542..3193ba84 100644 --- a/core/src/widget/text.rs +++ b/core/src/widget/text.rs @@ -153,7 +153,7 @@ where &self.content, self.size, self.font, - theme.appearance(self.style), + theme.appearance(self.style.clone()), self.horizontal_alignment, self.vertical_alignment, ); @@ -233,7 +233,7 @@ where horizontal_alignment: self.horizontal_alignment, vertical_alignment: self.vertical_alignment, font: self.font, - style: self.style, + style: self.style.clone(), } } } @@ -261,7 +261,7 @@ where /// The style sheet of some text. pub trait StyleSheet { /// The supported style of the [`StyleSheet`]. - type Style: Default + Copy; + type Style: Default + Clone; /// Produces the [`Appearance`] of some text. fn appearance(&self, style: Self::Style) -> Appearance; -- cgit From 33b5a900197e2798a393d6d9a0834039666eddbb Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Wed, 19 Apr 2023 01:19:56 +0200 Subject: Make basic text shaping the default shaping strategy --- core/src/renderer/null.rs | 2 ++ core/src/text.rs | 22 ++++++++++++++++++++-- core/src/widget/text.rs | 21 +++++++++++++++++++++ 3 files changed, 43 insertions(+), 2 deletions(-) (limited to 'core') diff --git a/core/src/renderer/null.rs b/core/src/renderer/null.rs index 88c58825..f62a4338 100644 --- a/core/src/renderer/null.rs +++ b/core/src/renderer/null.rs @@ -62,6 +62,7 @@ impl text::Renderer for Null { _size: f32, _font: Font, _bounds: Size, + _needs_shaping: bool, ) -> (f32, f32) { (0.0, 20.0) } @@ -74,6 +75,7 @@ impl text::Renderer for Null { _bounds: Size, _point: Point, _nearest_only: bool, + _advanced_shape: bool, ) -> Option { None } diff --git a/core/src/text.rs b/core/src/text.rs index 4c72abc3..0111cf31 100644 --- a/core/src/text.rs +++ b/core/src/text.rs @@ -27,6 +27,15 @@ pub struct Text<'a, Font> { /// The vertical alignment of the [`Text`]. pub vertical_alignment: alignment::Vertical, + + /// Whether the [`Text`] needs advanced shaping and font fallback. + /// + /// You will need to enable this flag if the text contains a complex + /// script, the font used needs it, and/or multiple fonts in your system + /// may be needed to display all of the glyphs. + /// + /// Advanced shaping is expensive! You should only enable it when necessary. + pub advanced_shape: bool, } /// The result of hit testing on text. @@ -77,11 +86,19 @@ pub trait Renderer: crate::Renderer { size: f32, font: Self::Font, bounds: Size, + advanced_shape: bool, ) -> (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); + fn measure_width( + &self, + content: &str, + size: f32, + font: Self::Font, + advanced_shape: bool, + ) -> f32 { + let (width, _) = + self.measure(content, size, font, Size::INFINITY, advanced_shape); width } @@ -101,6 +118,7 @@ pub trait Renderer: crate::Renderer { bounds: Size, point: Point, nearest_only: bool, + advanced_shape: bool, ) -> Option; /// Loads a [`Self::Font`] from its bytes. diff --git a/core/src/widget/text.rs b/core/src/widget/text.rs index 3193ba84..2df4556d 100644 --- a/core/src/widget/text.rs +++ b/core/src/widget/text.rs @@ -25,6 +25,7 @@ where vertical_alignment: alignment::Vertical, font: Option, style: ::Style, + advanced_shape: bool, } impl<'a, Renderer> Text<'a, Renderer> @@ -43,6 +44,7 @@ where horizontal_alignment: alignment::Horizontal::Left, vertical_alignment: alignment::Vertical::Top, style: Default::default(), + advanced_shape: false, } } @@ -98,6 +100,20 @@ where self.vertical_alignment = alignment; self } + + /// Enables advanced text shaping and font fallback for the [`Text`]. + /// + /// You will need to enable this if the text contains a complex script, the + /// font used needs it, and/or multiple fonts in your system may be needed + /// to display all of the glyphs. + /// + /// If your text isn't displaying properly, try enabling this! + /// + /// Advanced shaping is expensive! You should only enable it when necessary. + pub fn advanced_shape(mut self) -> Self { + self.advanced_shape = true; + self + } } impl<'a, Message, Renderer> Widget for Text<'a, Renderer> @@ -129,6 +145,7 @@ where size, self.font.unwrap_or_else(|| renderer.default_font()), bounds, + self.advanced_shape, ); let size = limits.resolve(Size::new(width, height)); @@ -156,6 +173,7 @@ where theme.appearance(self.style.clone()), self.horizontal_alignment, self.vertical_alignment, + self.advanced_shape, ); } } @@ -180,6 +198,7 @@ pub fn draw( appearance: Appearance, horizontal_alignment: alignment::Horizontal, vertical_alignment: alignment::Vertical, + advanced_shape: bool, ) where Renderer: text::Renderer, { @@ -205,6 +224,7 @@ pub fn draw( font: font.unwrap_or_else(|| renderer.default_font()), horizontal_alignment, vertical_alignment, + advanced_shape, }); } @@ -234,6 +254,7 @@ where vertical_alignment: self.vertical_alignment, font: self.font, style: self.style.clone(), + advanced_shape: self.advanced_shape, } } } -- cgit From 4bd290afe7d81d9aaf7467b3ce91491f6600261a Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Wed, 19 Apr 2023 02:00:45 +0200 Subject: Introduce `text::Shaping` enum and replace magic boolean --- core/src/renderer/null.rs | 4 ++-- core/src/text.rs | 30 ++++++++++++++++++++++++------ core/src/widget/text.rs | 28 ++++++++++------------------ 3 files changed, 36 insertions(+), 26 deletions(-) (limited to 'core') diff --git a/core/src/renderer/null.rs b/core/src/renderer/null.rs index f62a4338..22afb058 100644 --- a/core/src/renderer/null.rs +++ b/core/src/renderer/null.rs @@ -62,7 +62,7 @@ impl text::Renderer for Null { _size: f32, _font: Font, _bounds: Size, - _needs_shaping: bool, + _shaping: text::Shaping, ) -> (f32, f32) { (0.0, 20.0) } @@ -73,9 +73,9 @@ impl text::Renderer for Null { _size: f32, _font: Self::Font, _bounds: Size, + _shaping: text::Shaping, _point: Point, _nearest_only: bool, - _advanced_shape: bool, ) -> Option { None } diff --git a/core/src/text.rs b/core/src/text.rs index 0111cf31..c59d8fce 100644 --- a/core/src/text.rs +++ b/core/src/text.rs @@ -28,14 +28,32 @@ pub struct Text<'a, Font> { /// The vertical alignment of the [`Text`]. pub vertical_alignment: alignment::Vertical, - /// Whether the [`Text`] needs advanced shaping and font fallback. + /// The [`Shaping`] strategy of the [`Text`]. + pub shaping: Shaping, +} + +/// The shaping strategy of some text. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)] +pub enum Shaping { + /// No shaping and no font fallback. + /// + /// This shaping strategy is very cheap, but it will not display complex + /// scripts properly nor try to find missing glyphs in your system fonts. + /// + /// You should use this strategy when you have complete control of the text + /// and the font you are displaying in your application. + /// + /// This is the default. + #[default] + Basic, + /// Advanced text shaping and font fallback. /// /// You will need to enable this flag if the text contains a complex /// script, the font used needs it, and/or multiple fonts in your system /// may be needed to display all of the glyphs. /// /// Advanced shaping is expensive! You should only enable it when necessary. - pub advanced_shape: bool, + Advanced, } /// The result of hit testing on text. @@ -86,7 +104,7 @@ pub trait Renderer: crate::Renderer { size: f32, font: Self::Font, bounds: Size, - advanced_shape: bool, + shaping: Shaping, ) -> (f32, f32); /// Measures the width of the text as if it were laid out in a single line. @@ -95,10 +113,10 @@ pub trait Renderer: crate::Renderer { content: &str, size: f32, font: Self::Font, - advanced_shape: bool, + shaping: Shaping, ) -> f32 { let (width, _) = - self.measure(content, size, font, Size::INFINITY, advanced_shape); + self.measure(content, size, font, Size::INFINITY, shaping); width } @@ -116,9 +134,9 @@ pub trait Renderer: crate::Renderer { size: f32, font: Self::Font, bounds: Size, + shaping: Shaping, point: Point, nearest_only: bool, - advanced_shape: bool, ) -> Option; /// Loads a [`Self::Font`] from its bytes. diff --git a/core/src/widget/text.rs b/core/src/widget/text.rs index 2df4556d..f0392168 100644 --- a/core/src/widget/text.rs +++ b/core/src/widget/text.rs @@ -24,8 +24,8 @@ where horizontal_alignment: alignment::Horizontal, vertical_alignment: alignment::Vertical, font: Option, + shaping: text::Shaping, style: ::Style, - advanced_shape: bool, } impl<'a, Renderer> Text<'a, Renderer> @@ -43,8 +43,8 @@ where height: Length::Shrink, horizontal_alignment: alignment::Horizontal::Left, vertical_alignment: alignment::Vertical::Top, + shaping: text::Shaping::Basic, style: Default::default(), - advanced_shape: false, } } @@ -101,17 +101,9 @@ where self } - /// Enables advanced text shaping and font fallback for the [`Text`]. - /// - /// You will need to enable this if the text contains a complex script, the - /// font used needs it, and/or multiple fonts in your system may be needed - /// to display all of the glyphs. - /// - /// If your text isn't displaying properly, try enabling this! - /// - /// Advanced shaping is expensive! You should only enable it when necessary. - pub fn advanced_shape(mut self) -> Self { - self.advanced_shape = true; + /// Sets the [`text::Shaping`] strategy of the [`Text`]. + pub fn shaping(mut self, shaping: text::Shaping) -> Self { + self.shaping = shaping; self } } @@ -145,7 +137,7 @@ where size, self.font.unwrap_or_else(|| renderer.default_font()), bounds, - self.advanced_shape, + self.shaping, ); let size = limits.resolve(Size::new(width, height)); @@ -173,7 +165,7 @@ where theme.appearance(self.style.clone()), self.horizontal_alignment, self.vertical_alignment, - self.advanced_shape, + self.shaping, ); } } @@ -198,7 +190,7 @@ pub fn draw( appearance: Appearance, horizontal_alignment: alignment::Horizontal, vertical_alignment: alignment::Vertical, - advanced_shape: bool, + shaping: text::Shaping, ) where Renderer: text::Renderer, { @@ -224,7 +216,7 @@ pub fn draw( font: font.unwrap_or_else(|| renderer.default_font()), horizontal_alignment, vertical_alignment, - advanced_shape, + shaping, }); } @@ -254,7 +246,7 @@ where vertical_alignment: self.vertical_alignment, font: self.font, style: self.style.clone(), - advanced_shape: self.advanced_shape, + shaping: self.shaping, } } } -- cgit From 9499a8f9e6f9971dedfae563cb133232aa3cebc2 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Thu, 4 May 2023 13:00:16 +0200 Subject: Support configurable `LineHeight` in text widgets --- core/src/length.rs | 8 ++++++ core/src/pixels.rs | 6 ++++ core/src/renderer/null.rs | 2 ++ core/src/text.rs | 73 ++++++++++++++++++++++++++++++++++++++++++++--- core/src/widget/text.rs | 20 ++++++++++++- 5 files changed, 104 insertions(+), 5 deletions(-) (limited to 'core') diff --git a/core/src/length.rs b/core/src/length.rs index bb925c4b..3adb996e 100644 --- a/core/src/length.rs +++ b/core/src/length.rs @@ -1,3 +1,5 @@ +use crate::Pixels; + /// The strategy used to fill space in a specific dimension. #[derive(Debug, Clone, Copy, PartialEq)] pub enum Length { @@ -36,6 +38,12 @@ impl Length { } } +impl From for Length { + fn from(amount: Pixels) -> Self { + Length::Fixed(f32::from(amount)) + } +} + impl From for Length { fn from(amount: f32) -> Self { Length::Fixed(amount) diff --git a/core/src/pixels.rs b/core/src/pixels.rs index e42cd9f9..6a9e5c88 100644 --- a/core/src/pixels.rs +++ b/core/src/pixels.rs @@ -20,3 +20,9 @@ impl From for Pixels { Self(f32::from(amount)) } } + +impl From for f32 { + fn from(pixels: Pixels) -> Self { + pixels.0 + } +} diff --git a/core/src/renderer/null.rs b/core/src/renderer/null.rs index 22afb058..f0cc952e 100644 --- a/core/src/renderer/null.rs +++ b/core/src/renderer/null.rs @@ -60,6 +60,7 @@ impl text::Renderer for Null { &self, _content: &str, _size: f32, + _line_height: text::LineHeight, _font: Font, _bounds: Size, _shaping: text::Shaping, @@ -71,6 +72,7 @@ impl text::Renderer for Null { &self, _contents: &str, _size: f32, + _line_height: text::LineHeight, _font: Self::Font, _bounds: Size, _shaping: text::Shaping, diff --git a/core/src/text.rs b/core/src/text.rs index c59d8fce..1a867be3 100644 --- a/core/src/text.rs +++ b/core/src/text.rs @@ -1,8 +1,9 @@ //! Draw and interact with text. use crate::alignment; -use crate::{Color, Point, Rectangle, Size}; +use crate::{Color, Pixels, Point, Rectangle, Size}; use std::borrow::Cow; +use std::hash::{Hash, Hasher}; /// A paragraph. #[derive(Debug, Clone, Copy)] @@ -13,9 +14,12 @@ pub struct Text<'a, Font> { /// The bounds of the paragraph. pub bounds: Rectangle, - /// The size of the [`Text`]. + /// The size of the [`Text`] in logical pixels. pub size: f32, + /// The line height of the [`Text`]. + pub line_height: LineHeight, + /// The color of the [`Text`]. pub color: Color, @@ -56,6 +60,59 @@ pub enum Shaping { Advanced, } +/// The height of a line of text in a paragraph. +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum LineHeight { + /// A factor of the size of the text. + Relative(f32), + + /// An absolute height in logical pixels. + Absolute(Pixels), +} + +impl LineHeight { + /// Returns the [`LineHeight`] in absolute logical pixels. + pub fn to_absolute(self, text_size: Pixels) -> Pixels { + match self { + Self::Relative(factor) => Pixels(factor * text_size.0), + Self::Absolute(pixels) => pixels, + } + } +} + +impl Default for LineHeight { + fn default() -> Self { + Self::Relative(1.2) + } +} + +impl From for LineHeight { + fn from(factor: f32) -> Self { + Self::Relative(factor) + } +} + +impl From for LineHeight { + fn from(pixels: Pixels) -> Self { + Self::Absolute(pixels) + } +} + +impl Hash for LineHeight { + fn hash(&self, state: &mut H) { + match self { + Self::Relative(factor) => { + state.write_u8(0); + factor.to_bits().hash(state); + } + Self::Absolute(pixels) => { + state.write_u8(1); + f32::from(*pixels).to_bits().hash(state); + } + } + } +} + /// The result of hit testing on text. #[derive(Debug, Clone, Copy, PartialEq)] pub enum Hit { @@ -102,6 +159,7 @@ pub trait Renderer: crate::Renderer { &self, content: &str, size: f32, + line_height: LineHeight, font: Self::Font, bounds: Size, shaping: Shaping, @@ -115,8 +173,14 @@ pub trait Renderer: crate::Renderer { font: Self::Font, shaping: Shaping, ) -> f32 { - let (width, _) = - self.measure(content, size, font, Size::INFINITY, shaping); + let (width, _) = self.measure( + content, + size, + LineHeight::Absolute(Pixels(size)), + font, + Size::INFINITY, + shaping, + ); width } @@ -132,6 +196,7 @@ pub trait Renderer: crate::Renderer { &self, contents: &str, size: f32, + line_height: LineHeight, font: Self::Font, bounds: Size, shaping: Shaping, diff --git a/core/src/widget/text.rs b/core/src/widget/text.rs index f0392168..2d86e735 100644 --- a/core/src/widget/text.rs +++ b/core/src/widget/text.rs @@ -19,6 +19,7 @@ where { content: Cow<'a, str>, size: Option, + line_height: text::LineHeight, width: Length, height: Length, horizontal_alignment: alignment::Horizontal, @@ -38,6 +39,7 @@ where Text { content: content.into(), size: None, + line_height: text::LineHeight::default(), font: None, width: Length::Shrink, height: Length::Shrink, @@ -54,6 +56,15 @@ where self } + /// Sets the [`LineHeight`] of the [`Text`]. + pub fn line_height( + mut self, + line_height: impl Into, + ) -> Self { + self.line_height = line_height.into(); + self + } + /// Sets the [`Font`] of the [`Text`]. /// /// [`Font`]: crate::text::Renderer::Font @@ -135,6 +146,7 @@ where let (width, height) = renderer.measure( &self.content, size, + self.line_height, self.font.unwrap_or_else(|| renderer.default_font()), bounds, self.shaping, @@ -161,6 +173,7 @@ where layout, &self.content, self.size, + self.line_height, self.font, theme.appearance(self.style.clone()), self.horizontal_alignment, @@ -186,6 +199,7 @@ pub fn draw( layout: Layout<'_>, content: &str, size: Option, + line_height: text::LineHeight, font: Option, appearance: Appearance, horizontal_alignment: alignment::Horizontal, @@ -208,9 +222,12 @@ pub fn draw( alignment::Vertical::Bottom => bounds.y + bounds.height, }; + let size = size.unwrap_or_else(|| renderer.default_size()); + renderer.fill_text(crate::Text { content, - size: size.unwrap_or_else(|| renderer.default_size()), + size, + line_height, bounds: Rectangle { x, y, ..bounds }, color: appearance.color.unwrap_or(style.text_color), font: font.unwrap_or_else(|| renderer.default_font()), @@ -240,6 +257,7 @@ where Self { content: self.content.clone(), size: self.size, + line_height: self.line_height, width: self.width, height: self.height, horizontal_alignment: self.horizontal_alignment, -- cgit From 16bf8fc7622fbe67a7d81cbe6daad329385cc44c Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Mon, 8 May 2023 15:37:29 +0200 Subject: Export `Shaping` and `LineHeight` in `widget::text` --- core/src/widget/text.rs | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) (limited to 'core') diff --git a/core/src/widget/text.rs b/core/src/widget/text.rs index 2d86e735..90af88b7 100644 --- a/core/src/widget/text.rs +++ b/core/src/widget/text.rs @@ -10,6 +10,8 @@ use crate::{ use std::borrow::Cow; +pub use text::{LineHeight, Shaping}; + /// A paragraph of text. #[allow(missing_debug_implementations)] pub struct Text<'a, Renderer> @@ -19,13 +21,13 @@ where { content: Cow<'a, str>, size: Option, - line_height: text::LineHeight, + line_height: LineHeight, width: Length, height: Length, horizontal_alignment: alignment::Horizontal, vertical_alignment: alignment::Vertical, font: Option, - shaping: text::Shaping, + shaping: Shaping, style: ::Style, } @@ -39,13 +41,13 @@ where Text { content: content.into(), size: None, - line_height: text::LineHeight::default(), + line_height: LineHeight::default(), font: None, width: Length::Shrink, height: Length::Shrink, horizontal_alignment: alignment::Horizontal::Left, vertical_alignment: alignment::Vertical::Top, - shaping: text::Shaping::Basic, + shaping: Shaping::Basic, style: Default::default(), } } @@ -57,10 +59,7 @@ where } /// Sets the [`LineHeight`] of the [`Text`]. - pub fn line_height( - mut self, - line_height: impl Into, - ) -> Self { + pub fn line_height(mut self, line_height: impl Into) -> Self { self.line_height = line_height.into(); self } @@ -112,8 +111,8 @@ where self } - /// Sets the [`text::Shaping`] strategy of the [`Text`]. - pub fn shaping(mut self, shaping: text::Shaping) -> Self { + /// Sets the [`Shaping`] strategy of the [`Text`]. + pub fn shaping(mut self, shaping: Shaping) -> Self { self.shaping = shaping; self } @@ -199,12 +198,12 @@ pub fn draw( layout: Layout<'_>, content: &str, size: Option, - line_height: text::LineHeight, + line_height: LineHeight, font: Option, appearance: Appearance, horizontal_alignment: alignment::Horizontal, vertical_alignment: alignment::Vertical, - shaping: text::Shaping, + shaping: Shaping, ) where Renderer: text::Renderer, { -- cgit From f75b8bd9ec4677b4469910b91ca2aa9ec5636e7e Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Wed, 10 May 2023 00:13:51 +0200 Subject: Increase default `LineHeight` to `Relative(1.3)` --- core/src/text.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'core') diff --git a/core/src/text.rs b/core/src/text.rs index 1a867be3..c154cc27 100644 --- a/core/src/text.rs +++ b/core/src/text.rs @@ -82,7 +82,7 @@ impl LineHeight { impl Default for LineHeight { fn default() -> Self { - Self::Relative(1.2) + Self::Relative(1.3) } } -- cgit