diff options
Diffstat (limited to 'core')
31 files changed, 1400 insertions, 291 deletions
diff --git a/core/src/alignment.rs b/core/src/alignment.rs index 51b7fca9..8f01ef71 100644 --- a/core/src/alignment.rs +++ b/core/src/alignment.rs @@ -46,6 +46,16 @@ pub enum Horizontal { Right, } +impl From<Alignment> for Horizontal { + fn from(alignment: Alignment) -> Self { + match alignment { + Alignment::Start => Self::Left, + Alignment::Center => Self::Center, + Alignment::End => Self::Right, + } + } +} + /// The vertical [`Alignment`] of some resource. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum Vertical { @@ -58,3 +68,13 @@ pub enum Vertical { /// Align bottom Bottom, } + +impl From<Alignment> for Vertical { + fn from(alignment: Alignment) -> Self { + match alignment { + Alignment::Start => Self::Top, + Alignment::Center => Self::Center, + Alignment::End => Self::Bottom, + } + } +} diff --git a/core/src/border.rs b/core/src/border.rs index 2df24988..da0aaa28 100644 --- a/core/src/border.rs +++ b/core/src/border.rs @@ -10,40 +10,64 @@ pub struct Border { /// The width of the border. pub width: f32, - /// The radius of the border. + /// The [`Radius`] of the border. pub radius: Radius, } +/// Creates a new [`Border`] with the given [`Radius`]. +/// +/// ``` +/// # use iced_core::border::{self, Border}; +/// # +/// assert_eq!(border::rounded(10), Border::default().rounded(10)); +/// ``` +pub fn rounded(radius: impl Into<Radius>) -> Border { + Border::default().rounded(radius) +} + +/// Creates a new [`Border`] with the given [`Color`]. +/// +/// ``` +/// # use iced_core::border::{self, Border}; +/// # use iced_core::Color; +/// # +/// assert_eq!(border::color(Color::BLACK), Border::default().color(Color::BLACK)); +/// ``` +pub fn color(color: impl Into<Color>) -> Border { + Border::default().color(color) +} + +/// Creates a new [`Border`] with the given `width`. +/// +/// ``` +/// # use iced_core::border::{self, Border}; +/// # use iced_core::Color; +/// # +/// assert_eq!(border::width(10), Border::default().width(10)); +/// ``` +pub fn width(width: impl Into<Pixels>) -> Border { + Border::default().width(width) +} + impl Border { - /// Creates a new default rounded [`Border`] with the given [`Radius`]. - /// - /// ``` - /// # use iced_core::Border; - /// # - /// assert_eq!(Border::rounded(10), Border::default().with_radius(10)); - /// ``` - pub fn rounded(radius: impl Into<Radius>) -> Self { - Self::default().with_radius(radius) - } - - /// Updates the [`Color`] of the [`Border`]. - pub fn with_color(self, color: impl Into<Color>) -> Self { + /// Sets the [`Color`] of the [`Border`]. + pub fn color(self, color: impl Into<Color>) -> Self { Self { color: color.into(), ..self } } - /// Updates the [`Radius`] of the [`Border`]. - pub fn with_radius(self, radius: impl Into<Radius>) -> Self { + /// Sets the [`Radius`] of the [`Border`]. + pub fn rounded(self, radius: impl Into<Radius>) -> Self { Self { radius: radius.into(), ..self } } - /// Updates the width of the [`Border`]. - pub fn with_width(self, width: impl Into<Pixels>) -> Self { + /// Sets the width of the [`Border`]. + pub fn width(self, width: impl Into<Pixels>) -> Self { Self { width: width.into().0, ..self @@ -54,11 +78,160 @@ impl Border { /// The border radii 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 Radius([f32; 4]); +pub struct Radius { + /// Top left radius + pub top_left: f32, + /// Top right radius + pub top_right: f32, + /// Bottom right radius + pub bottom_right: f32, + /// Bottom left radius + pub bottom_left: f32, +} + +/// Creates a new [`Radius`] with the same value for each corner. +pub fn radius(value: impl Into<Pixels>) -> Radius { + Radius::new(value) +} + +/// Creates a new [`Radius`] with the given top left value. +pub fn top_left(value: impl Into<Pixels>) -> Radius { + Radius::default().top_left(value) +} + +/// Creates a new [`Radius`] with the given top right value. +pub fn top_right(value: impl Into<Pixels>) -> Radius { + Radius::default().top_right(value) +} + +/// Creates a new [`Radius`] with the given bottom right value. +pub fn bottom_right(value: impl Into<Pixels>) -> Radius { + Radius::default().bottom_right(value) +} + +/// Creates a new [`Radius`] with the given bottom left value. +pub fn bottom_left(value: impl Into<Pixels>) -> Radius { + Radius::default().bottom_left(value) +} + +/// Creates a new [`Radius`] with the given value as top left and top right. +pub fn top(value: impl Into<Pixels>) -> Radius { + Radius::default().top(value) +} + +/// Creates a new [`Radius`] with the given value as bottom left and bottom right. +pub fn bottom(value: impl Into<Pixels>) -> Radius { + Radius::default().bottom(value) +} + +/// Creates a new [`Radius`] with the given value as top left and bottom left. +pub fn left(value: impl Into<Pixels>) -> Radius { + Radius::default().left(value) +} + +/// Creates a new [`Radius`] with the given value as top right and bottom right. +pub fn right(value: impl Into<Pixels>) -> Radius { + Radius::default().right(value) +} + +impl Radius { + /// Creates a new [`Radius`] with the same value for each corner. + pub fn new(value: impl Into<Pixels>) -> Self { + let value = value.into().0; + + Self { + top_left: value, + top_right: value, + bottom_right: value, + bottom_left: value, + } + } + + /// Sets the top left value of the [`Radius`]. + pub fn top_left(self, value: impl Into<Pixels>) -> Self { + Self { + top_left: value.into().0, + ..self + } + } + + /// Sets the top right value of the [`Radius`]. + pub fn top_right(self, value: impl Into<Pixels>) -> Self { + Self { + top_right: value.into().0, + ..self + } + } + + /// Sets the bottom right value of the [`Radius`]. + pub fn bottom_right(self, value: impl Into<Pixels>) -> Self { + Self { + bottom_right: value.into().0, + ..self + } + } + + /// Sets the bottom left value of the [`Radius`]. + pub fn bottom_left(self, value: impl Into<Pixels>) -> Self { + Self { + bottom_left: value.into().0, + ..self + } + } + + /// Sets the top left and top right values of the [`Radius`]. + pub fn top(self, value: impl Into<Pixels>) -> Self { + let value = value.into().0; + + Self { + top_left: value, + top_right: value, + ..self + } + } + + /// Sets the bottom left and bottom right values of the [`Radius`]. + pub fn bottom(self, value: impl Into<Pixels>) -> Self { + let value = value.into().0; + + Self { + bottom_left: value, + bottom_right: value, + ..self + } + } + + /// Sets the top left and bottom left values of the [`Radius`]. + pub fn left(self, value: impl Into<Pixels>) -> Self { + let value = value.into().0; + + Self { + top_left: value, + bottom_left: value, + ..self + } + } + + /// Sets the top right and bottom right values of the [`Radius`]. + pub fn right(self, value: impl Into<Pixels>) -> Self { + let value = value.into().0; + + Self { + top_right: value, + bottom_right: value, + ..self + } + } +} impl From<f32> for Radius { - fn from(w: f32) -> Self { - Self([w; 4]) + fn from(radius: f32) -> Self { + Self { + top_left: radius, + top_right: radius, + bottom_right: radius, + bottom_left: radius, + } } } @@ -80,14 +253,13 @@ impl From<i32> for Radius { } } -impl From<[f32; 4]> for Radius { - fn from(radi: [f32; 4]) -> Self { - Self(radi) - } -} - impl From<Radius> for [f32; 4] { fn from(radi: Radius) -> Self { - radi.0 + [ + radi.top_left, + radi.top_right, + radi.bottom_right, + radi.bottom_left, + ] } } diff --git a/core/src/border_radius.rs b/core/src/border_radius.rs deleted file mode 100644 index a444dd74..00000000 --- a/core/src/border_radius.rs +++ /dev/null @@ -1,22 +0,0 @@ -/// The border radii for the corners of a graphics primitive in the order: -/// top-left, top-right, bottom-right, bottom-left. -#[derive(Debug, Clone, Copy, PartialEq, Default)] -pub struct BorderRadius([f32; 4]); - -impl From<f32> for BorderRadius { - fn from(w: f32) -> Self { - Self([w; 4]) - } -} - -impl From<[f32; 4]> for BorderRadius { - fn from(radi: [f32; 4]) -> Self { - Self(radi) - } -} - -impl From<BorderRadius> for [f32; 4] { - fn from(radi: BorderRadius) -> Self { - radi.0 - } -} diff --git a/core/src/element.rs b/core/src/element.rs index 385d8295..6ebb8a15 100644 --- a/core/src/element.rs +++ b/core/src/element.rs @@ -304,7 +304,7 @@ where tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn widget::Operation<()>, + operation: &mut dyn widget::Operation, ) { self.widget.operate(tree, layout, renderer, operation); } @@ -440,7 +440,7 @@ where state: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn widget::Operation<()>, + operation: &mut dyn widget::Operation, ) { self.element .widget diff --git a/core/src/image.rs b/core/src/image.rs index 82ecdd0f..f985636a 100644 --- a/core/src/image.rs +++ b/core/src/image.rs @@ -7,6 +7,73 @@ use rustc_hash::FxHasher; use std::hash::{Hash, Hasher}; use std::path::{Path, PathBuf}; +/// A raster image that can be drawn. +#[derive(Debug, Clone, PartialEq)] +pub struct Image<H = Handle> { + /// The handle of the image. + pub handle: H, + + /// The filter method of the image. + pub filter_method: FilterMethod, + + /// The rotation to be applied to the image; on its center. + pub rotation: Radians, + + /// The opacity of the image. + /// + /// 0 means transparent. 1 means opaque. + pub opacity: f32, + + /// If set to `true`, the image will be snapped to the pixel grid. + /// + /// This can avoid graphical glitches, specially when using + /// [`FilterMethod::Nearest`]. + pub snap: bool, +} + +impl Image<Handle> { + /// Creates a new [`Image`] with the given handle. + pub fn new(handle: impl Into<Handle>) -> Self { + Self { + handle: handle.into(), + filter_method: FilterMethod::default(), + rotation: Radians(0.0), + opacity: 1.0, + snap: false, + } + } + + /// Sets the filter method of the [`Image`]. + pub fn filter_method(mut self, filter_method: FilterMethod) -> Self { + self.filter_method = filter_method; + self + } + + /// Sets the rotation of the [`Image`]. + pub fn rotation(mut self, rotation: impl Into<Radians>) -> Self { + self.rotation = rotation.into(); + self + } + + /// Sets the opacity of the [`Image`]. + pub fn opacity(mut self, opacity: impl Into<f32>) -> Self { + self.opacity = opacity.into(); + self + } + + /// Sets whether the [`Image`] should be snapped to the pixel grid. + pub fn snap(mut self, snap: bool) -> Self { + self.snap = snap; + self + } +} + +impl From<&Handle> for Image { + fn from(handle: &Handle) -> Self { + Image::new(handle.clone()) + } +} + /// A handle of some image data. #[derive(Clone, PartialEq, Eq)] pub enum Handle { @@ -101,6 +168,12 @@ where } } +impl From<&Handle> for Handle { + fn from(value: &Handle) -> Self { + value.clone() + } +} + impl std::fmt::Debug for Handle { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { @@ -166,14 +239,6 @@ pub trait Renderer: crate::Renderer { /// Returns the dimensions of an image for the given [`Handle`]. fn measure_image(&self, handle: &Self::Handle) -> Size<u32>; - /// Draws an image with the given [`Handle`] and inside the provided - /// `bounds`. - fn draw_image( - &mut self, - handle: Self::Handle, - filter_method: FilterMethod, - bounds: Rectangle, - rotation: Radians, - opacity: f32, - ); + /// Draws an [`Image`] inside the provided `bounds`. + fn draw_image(&mut self, image: Image<Self::Handle>, bounds: Rectangle); } diff --git a/core/src/layout/node.rs b/core/src/layout/node.rs index 5743a9bd..0c0f90fb 100644 --- a/core/src/layout/node.rs +++ b/core/src/layout/node.rs @@ -103,12 +103,13 @@ impl Node { } /// Translates the [`Node`] by the given translation. - pub fn translate(self, translation: impl Into<Vector>) -> Self { - let translation = translation.into(); + pub fn translate(mut self, translation: impl Into<Vector>) -> Self { + self.translate_mut(translation); + self + } - Self { - bounds: self.bounds + translation, - ..self - } + /// Translates the [`Node`] by the given translation. + pub fn translate_mut(&mut self, translation: impl Into<Vector>) { + self.bounds = self.bounds + translation.into(); } } diff --git a/core/src/lib.rs b/core/src/lib.rs index 32156441..df599f45 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -20,6 +20,7 @@ pub mod keyboard; pub mod layout; pub mod mouse; pub mod overlay; +pub mod padding; pub mod renderer; pub mod svg; pub mod text; @@ -35,7 +36,6 @@ mod color; mod content_fit; mod element; mod length; -mod padding; mod pixels; mod point; mod rectangle; @@ -57,6 +57,7 @@ pub use element::Element; pub use event::Event; pub use font::Font; pub use gradient::Gradient; +pub use image::Image; pub use layout::Layout; pub use length::Length; pub use overlay::Overlay; @@ -69,6 +70,7 @@ pub use rotation::Rotation; pub use shadow::Shadow; pub use shell::Shell; pub use size::Size; +pub use svg::Svg; pub use text::Text; pub use theme::Theme; pub use transformation::Transformation; diff --git a/core/src/mouse/click.rs b/core/src/mouse/click.rs index 6f3844be..07a4db5a 100644 --- a/core/src/mouse/click.rs +++ b/core/src/mouse/click.rs @@ -1,4 +1,5 @@ //! Track mouse clicks. +use crate::mouse::Button; use crate::time::Instant; use crate::Point; @@ -6,6 +7,7 @@ use crate::Point; #[derive(Debug, Clone, Copy)] pub struct Click { kind: Kind, + button: Button, position: Point, time: Instant, } @@ -36,11 +38,17 @@ impl Kind { impl Click { /// Creates a new [`Click`] with the given position and previous last /// [`Click`]. - pub fn new(position: Point, previous: Option<Click>) -> Click { + pub fn new( + position: Point, + button: Button, + previous: Option<Click>, + ) -> Click { let time = Instant::now(); let kind = if let Some(previous) = previous { - if previous.is_consecutive(position, time) { + if previous.is_consecutive(position, time) + && button == previous.button + { previous.kind.next() } else { Kind::Single @@ -51,6 +59,7 @@ impl Click { Click { kind, + button, position, time, } diff --git a/core/src/overlay.rs b/core/src/overlay.rs index 16f867da..f09de831 100644 --- a/core/src/overlay.rs +++ b/core/src/overlay.rs @@ -41,7 +41,7 @@ where &mut self, _layout: Layout<'_>, _renderer: &Renderer, - _operation: &mut dyn widget::Operation<()>, + _operation: &mut dyn widget::Operation, ) { } @@ -52,7 +52,7 @@ where /// * 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. + /// new messages based on user interaction. /// * the `Renderer` /// * a [`Clipboard`], if available /// diff --git a/core/src/overlay/element.rs b/core/src/overlay/element.rs index 61e75e8a..32e987a3 100644 --- a/core/src/overlay/element.rs +++ b/core/src/overlay/element.rs @@ -92,7 +92,7 @@ where &mut self, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn widget::Operation<()>, + operation: &mut dyn widget::Operation, ) { self.overlay.operate(layout, renderer, operation); } @@ -144,7 +144,7 @@ where &mut self, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn widget::Operation<()>, + operation: &mut dyn widget::Operation, ) { self.content.operate(layout, renderer, operation); } diff --git a/core/src/overlay/group.rs b/core/src/overlay/group.rs index cd12eac9..6541d311 100644 --- a/core/src/overlay/group.rs +++ b/core/src/overlay/group.rs @@ -132,7 +132,7 @@ where &mut self, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn widget::Operation<()>, + operation: &mut dyn widget::Operation, ) { operation.container(None, layout.bounds(), &mut |operation| { self.children.iter_mut().zip(layout.children()).for_each( diff --git a/core/src/padding.rs b/core/src/padding.rs index a63f6e29..e26cdd9b 100644 --- a/core/src/padding.rs +++ b/core/src/padding.rs @@ -1,4 +1,5 @@ -use crate::Size; +//! Space stuff around the perimeter. +use crate::{Pixels, Size}; /// An amount of space to pad for each side of a box /// @@ -9,7 +10,6 @@ use crate::Size; /// # /// let padding = Padding::from(20); // 20px on all sides /// let padding = Padding::from([10, 20]); // top/bottom, left/right -/// let padding = Padding::from([5, 10, 15, 20]); // top, right, bottom, left /// ``` /// /// Normally, the `padding` method of a widget will ask for an `Into<Padding>`, @@ -31,9 +31,8 @@ use crate::Size; /// /// let widget = Widget::new().padding(20); // 20px on all sides /// let widget = Widget::new().padding([10, 20]); // top/bottom, left/right -/// let widget = Widget::new().padding([5, 10, 15, 20]); // top, right, bottom, left /// ``` -#[derive(Debug, Copy, Clone)] +#[derive(Debug, Copy, Clone, PartialEq, Default)] pub struct Padding { /// Top padding pub top: f32, @@ -45,6 +44,31 @@ pub struct Padding { pub left: f32, } +/// Create a [`Padding`] that is equal on all sides. +pub fn all(padding: impl Into<Pixels>) -> Padding { + Padding::new(padding.into().0) +} + +/// Create some top [`Padding`]. +pub fn top(padding: impl Into<Pixels>) -> Padding { + Padding::default().top(padding) +} + +/// Create some bottom [`Padding`]. +pub fn bottom(padding: impl Into<Pixels>) -> Padding { + Padding::default().bottom(padding) +} + +/// Create some left [`Padding`]. +pub fn left(padding: impl Into<Pixels>) -> Padding { + Padding::default().left(padding) +} + +/// Create some right [`Padding`]. +pub fn right(padding: impl Into<Pixels>) -> Padding { + Padding::default().right(padding) +} + impl Padding { /// Padding of zero pub const ZERO: Padding = Padding { @@ -54,7 +78,7 @@ impl Padding { left: 0.0, }; - /// Create a Padding that is equal on all sides + /// Create a [`Padding`] that is equal on all sides. pub const fn new(padding: f32) -> Padding { Padding { top: padding, @@ -64,6 +88,46 @@ impl Padding { } } + /// Sets the [`top`] of the [`Padding`]. + /// + /// [`top`]: Self::top + pub fn top(self, top: impl Into<Pixels>) -> Self { + Self { + top: top.into().0, + ..self + } + } + + /// Sets the [`bottom`] of the [`Padding`]. + /// + /// [`bottom`]: Self::bottom + pub fn bottom(self, bottom: impl Into<Pixels>) -> Self { + Self { + bottom: bottom.into().0, + ..self + } + } + + /// Sets the [`left`] of the [`Padding`]. + /// + /// [`left`]: Self::left + pub fn left(self, left: impl Into<Pixels>) -> Self { + Self { + left: left.into().0, + ..self + } + } + + /// Sets the [`right`] of the [`Padding`]. + /// + /// [`right`]: Self::right + pub fn right(self, right: impl Into<Pixels>) -> Self { + Self { + right: right.into().0, + ..self + } + } + /// Returns the total amount of vertical [`Padding`]. pub fn vertical(self) -> f32 { self.top + self.bottom @@ -111,17 +175,6 @@ impl From<[u16; 2]> for Padding { } } -impl From<[u16; 4]> for Padding { - fn from(p: [u16; 4]) -> Self { - Padding { - top: f32::from(p[0]), - right: f32::from(p[1]), - bottom: f32::from(p[2]), - left: f32::from(p[3]), - } - } -} - impl From<f32> for Padding { fn from(p: f32) -> Self { Padding { @@ -144,17 +197,6 @@ impl From<[f32; 2]> for Padding { } } -impl From<[f32; 4]> for Padding { - fn from(p: [f32; 4]) -> Self { - Padding { - top: p[0], - right: p[1], - bottom: p[2], - left: p[3], - } - } -} - impl From<Padding> for Size { fn from(padding: Padding) -> Self { Self::new(padding.horizontal(), padding.vertical()) diff --git a/core/src/pixels.rs b/core/src/pixels.rs index 425c0028..a1ea0f15 100644 --- a/core/src/pixels.rs +++ b/core/src/pixels.rs @@ -6,9 +6,14 @@ /// (e.g. `impl Into<Pixels>`) and, since `Pixels` implements `From` both for /// `f32` and `u16`, you should be able to provide both integers and float /// literals as needed. -#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)] +#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Default)] pub struct Pixels(pub f32); +impl Pixels { + /// Zero pixels. + pub const ZERO: Self = Self(0.0); +} + impl From<f32> for Pixels { fn from(amount: f32) -> Self { Self(amount) @@ -27,6 +32,30 @@ impl From<Pixels> for f32 { } } +impl std::ops::Add for Pixels { + type Output = Pixels; + + fn add(self, rhs: Self) -> Self { + Pixels(self.0 + rhs.0) + } +} + +impl std::ops::Add<f32> for Pixels { + type Output = Pixels; + + fn add(self, rhs: f32) -> Self { + Pixels(self.0 + rhs) + } +} + +impl std::ops::Mul for Pixels { + type Output = Pixels; + + fn mul(self, rhs: Self) -> Self { + Pixels(self.0 * rhs.0) + } +} + impl std::ops::Mul<f32> for Pixels { type Output = Pixels; @@ -34,3 +63,19 @@ impl std::ops::Mul<f32> for Pixels { Pixels(self.0 * rhs) } } + +impl std::ops::Div for Pixels { + type Output = Pixels; + + fn div(self, rhs: Self) -> Self { + Pixels(self.0 / rhs.0) + } +} + +impl std::ops::Div<f32> for Pixels { + type Output = Pixels; + + fn div(self, rhs: f32) -> Self { + Pixels(self.0 / rhs) + } +} diff --git a/core/src/rectangle.rs b/core/src/rectangle.rs index 1556e072..cff33991 100644 --- a/core/src/rectangle.rs +++ b/core/src/rectangle.rs @@ -1,4 +1,4 @@ -use crate::{Point, Radians, Size, Vector}; +use crate::{Padding, Point, Radians, Size, Vector}; /// An axis-aligned rectangle. #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] @@ -47,6 +47,62 @@ impl Rectangle<f32> { } } + /// Creates a new square [`Rectangle`] with the center at the origin and + /// with the given radius. + pub fn with_radius(radius: f32) -> Self { + Self { + x: -radius, + y: -radius, + width: radius * 2.0, + height: radius * 2.0, + } + } + + /// Creates a new axis-aligned [`Rectangle`] from the given vertices; returning the + /// rotation in [`Radians`] that must be applied to the axis-aligned [`Rectangle`] + /// to obtain the desired result. + pub fn with_vertices( + top_left: Point, + top_right: Point, + bottom_left: Point, + ) -> (Rectangle, Radians) { + let width = (top_right.x - top_left.x).hypot(top_right.y - top_left.y); + + let height = + (bottom_left.x - top_left.x).hypot(bottom_left.y - top_left.y); + + let rotation = + (top_right.y - top_left.y).atan2(top_right.x - top_left.x); + + let rotation = if rotation < 0.0 { + 2.0 * std::f32::consts::PI + rotation + } else { + rotation + }; + + let position = { + let center = Point::new( + (top_right.x + bottom_left.x) / 2.0, + (top_right.y + bottom_left.y) / 2.0, + ); + + let rotation = -rotation - std::f32::consts::PI * 2.0; + + Point::new( + center.x + (top_left.x - center.x) * rotation.cos() + - (top_left.y - center.y) * rotation.sin(), + center.y + + (top_left.x - center.x) * rotation.sin() + + (top_left.y - center.y) * rotation.cos(), + ) + }; + + ( + Rectangle::new(position, Size::new(width, height)), + Radians(rotation), + ) + } + /// Returns the [`Point`] at the center of the [`Rectangle`]. pub fn center(&self) -> Point { Point::new(self.center_x(), self.center_y()) @@ -164,12 +220,26 @@ impl Rectangle<f32> { } /// Expands the [`Rectangle`] a given amount. - pub fn expand(self, amount: f32) -> Self { + pub fn expand(self, padding: impl Into<Padding>) -> Self { + let padding = padding.into(); + + Self { + x: self.x - padding.left, + y: self.y - padding.top, + width: self.width + padding.horizontal(), + height: self.height + padding.vertical(), + } + } + + /// Shrinks the [`Rectangle`] a given amount. + pub fn shrink(self, padding: impl Into<Padding>) -> Self { + let padding = padding.into(); + Self { - x: self.x - amount, - y: self.y - amount, - width: self.width + amount * 2.0, - height: self.height + amount * 2.0, + x: self.x + padding.left, + y: self.y + padding.top, + width: self.width - padding.horizontal(), + height: self.height - padding.vertical(), } } diff --git a/core/src/renderer.rs b/core/src/renderer.rs index a2785ae8..6684517f 100644 --- a/core/src/renderer.rs +++ b/core/src/renderer.rs @@ -69,7 +69,7 @@ pub struct Quad { /// The bounds of the [`Quad`]. pub bounds: Rectangle, - /// The [`Border`] of the [`Quad`]. + /// The [`Border`] of the [`Quad`]. The border is drawn on the inside of the [`Quad`]. pub border: Border, /// The [`Shadow`] of the [`Quad`]. diff --git a/core/src/renderer/null.rs b/core/src/renderer/null.rs index e8709dbc..bbcdd8ff 100644 --- a/core/src/renderer/null.rs +++ b/core/src/renderer/null.rs @@ -1,11 +1,10 @@ use crate::alignment; -use crate::image; +use crate::image::{self, Image}; use crate::renderer::{self, Renderer}; use crate::svg; use crate::text::{self, Text}; use crate::{ - Background, Color, Font, Pixels, Point, Radians, Rectangle, Size, - Transformation, + Background, Color, Font, Pixels, Point, Rectangle, Size, Transformation, }; impl Renderer for () { @@ -77,9 +76,14 @@ impl text::Paragraph for () { fn with_text(_text: Text<&str>) -> Self {} + fn with_spans<Link>( + _text: Text<&[text::Span<'_, Link, Self::Font>], Self::Font>, + ) -> Self { + } + fn resize(&mut self, _new_bounds: Size) {} - fn compare(&self, _text: Text<&str>) -> text::Difference { + fn compare(&self, _text: Text<()>) -> text::Difference { text::Difference::None } @@ -102,6 +106,14 @@ impl text::Paragraph for () { fn hit_test(&self, _point: Point) -> Option<text::Hit> { None } + + fn hit_span(&self, _point: Point) -> Option<usize> { + None + } + + fn span_bounds(&self, _index: usize) -> Vec<Rectangle> { + vec![] + } } impl text::Editor for () { @@ -109,6 +121,10 @@ impl text::Editor for () { fn with_text(_text: &str) -> Self {} + fn is_empty(&self) -> bool { + true + } + fn cursor(&self) -> text::editor::Cursor { text::editor::Cursor::Caret(Point::ORIGIN) } @@ -145,6 +161,7 @@ impl text::Editor for () { _new_font: Self::Font, _new_size: Pixels, _new_line_height: text::LineHeight, + _new_wrapping: text::Wrapping, _new_highlighter: &mut impl text::Highlighter, ) { } @@ -161,21 +178,13 @@ impl text::Editor for () { } impl image::Renderer for () { - type Handle = (); + type Handle = image::Handle; fn measure_image(&self, _handle: &Self::Handle) -> Size<u32> { Size::default() } - fn draw_image( - &mut self, - _handle: Self::Handle, - _filter_method: image::FilterMethod, - _bounds: Rectangle, - _rotation: Radians, - _opacity: f32, - ) { - } + fn draw_image(&mut self, _image: Image, _bounds: Rectangle) {} } impl svg::Renderer for () { @@ -183,13 +192,5 @@ impl svg::Renderer for () { Size::default() } - fn draw_svg( - &mut self, - _handle: svg::Handle, - _color: Option<Color>, - _bounds: Rectangle, - _rotation: Radians, - _opacity: f32, - ) { - } + fn draw_svg(&mut self, _svg: svg::Svg, _bounds: Rectangle) {} } diff --git a/core/src/size.rs b/core/src/size.rs index d7459355..95089236 100644 --- a/core/src/size.rs +++ b/core/src/size.rs @@ -99,6 +99,20 @@ impl<T> From<Size<T>> for Vector<T> { } } +impl<T> std::ops::Add for Size<T> +where + T: std::ops::Add<Output = T>, +{ + type Output = Size<T>; + + fn add(self, rhs: Self) -> Self::Output { + Size { + width: self.width + rhs.width, + height: self.height + rhs.height, + } + } +} + impl<T> std::ops::Sub for Size<T> where T: std::ops::Sub<Output = T>, diff --git a/core/src/svg.rs b/core/src/svg.rs index 946b8156..ac19b223 100644 --- a/core/src/svg.rs +++ b/core/src/svg.rs @@ -7,6 +7,66 @@ use std::hash::{Hash, Hasher as _}; use std::path::PathBuf; use std::sync::Arc; +/// A raster image that can be drawn. +#[derive(Debug, Clone, PartialEq)] +pub struct Svg<H = Handle> { + /// The handle of the [`Svg`]. + pub handle: H, + + /// The [`Color`] filter to be applied to the [`Svg`]. + /// + /// If some [`Color`] is set, the whole [`Svg`] will be + /// painted with it—ignoring any intrinsic colors. + /// + /// This can be useful for coloring icons programmatically + /// (e.g. with a theme). + pub color: Option<Color>, + + /// The rotation to be applied to the image; on its center. + pub rotation: Radians, + + /// The opacity of the [`Svg`]. + /// + /// 0 means transparent. 1 means opaque. + pub opacity: f32, +} + +impl Svg<Handle> { + /// Creates a new [`Svg`] with the given handle. + pub fn new(handle: impl Into<Handle>) -> Self { + Self { + handle: handle.into(), + color: None, + rotation: Radians(0.0), + opacity: 1.0, + } + } + + /// Sets the [`Color`] filter of the [`Svg`]. + pub fn color(mut self, color: impl Into<Color>) -> Self { + self.color = Some(color.into()); + self + } + + /// Sets the rotation of the [`Svg`]. + pub fn rotation(mut self, rotation: impl Into<Radians>) -> Self { + self.rotation = rotation.into(); + self + } + + /// Sets the opacity of the [`Svg`]. + pub fn opacity(mut self, opacity: impl Into<f32>) -> Self { + self.opacity = opacity.into(); + self + } +} + +impl From<&Handle> for Svg { + fn from(handle: &Handle) -> Self { + Svg::new(handle.clone()) + } +} + /// A handle of Svg data. #[derive(Debug, Clone, PartialEq, Eq)] pub struct Handle { @@ -95,12 +155,5 @@ pub trait Renderer: crate::Renderer { fn measure_svg(&self, handle: &Handle) -> Size<u32>; /// Draws an SVG with the given [`Handle`], an optional [`Color`] filter, and inside the provided `bounds`. - fn draw_svg( - &mut self, - handle: Handle, - color: Option<Color>, - bounds: Rectangle, - rotation: Radians, - opacity: f32, - ); + fn draw_svg(&mut self, svg: Svg, bounds: Rectangle); } diff --git a/core/src/text.rs b/core/src/text.rs index b30feae0..d7b7fee4 100644 --- a/core/src/text.rs +++ b/core/src/text.rs @@ -1,16 +1,18 @@ //! Draw and interact with text. -mod paragraph; - pub mod editor; pub mod highlighter; +pub mod paragraph; pub use editor::Editor; pub use highlighter::Highlighter; pub use paragraph::Paragraph; use crate::alignment; -use crate::{Color, Pixels, Point, Rectangle, Size}; +use crate::{ + Background, Border, Color, Padding, Pixels, Point, Rectangle, Size, +}; +use std::borrow::Cow; use std::hash::{Hash, Hasher}; /// A paragraph. @@ -39,6 +41,9 @@ pub struct Text<Content = String, Font = crate::Font> { /// The [`Shaping`] strategy of the [`Text`]. pub shaping: Shaping, + + /// The [`Wrapping`] strategy of the [`Text`]. + pub wrapping: Wrapping, } /// The shaping strategy of some text. @@ -65,6 +70,22 @@ pub enum Shaping { Advanced, } +/// The wrapping strategy of some text. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)] +pub enum Wrapping { + /// No wrapping. + None, + /// Wraps at the word level. + /// + /// This is the default. + #[default] + Word, + /// Wraps at the glyph level. + Glyph, + /// Wraps at the word level, or fallback to glyph level if a word can't fit on a line by itself. + WordOrGlyph, +} + /// The height of a line of text in a paragraph. #[derive(Debug, Clone, Copy, PartialEq)] pub enum LineHeight { @@ -221,3 +242,294 @@ pub trait Renderer: crate::Renderer { clip_bounds: Rectangle, ); } + +/// A span of text. +#[derive(Debug, Clone)] +pub struct Span<'a, Link = (), Font = crate::Font> { + /// The [`Fragment`] of text. + pub text: Fragment<'a>, + /// The size of the [`Span`] in [`Pixels`]. + pub size: Option<Pixels>, + /// The [`LineHeight`] of the [`Span`]. + pub line_height: Option<LineHeight>, + /// The font of the [`Span`]. + pub font: Option<Font>, + /// The [`Color`] of the [`Span`]. + pub color: Option<Color>, + /// The link of the [`Span`]. + pub link: Option<Link>, + /// The [`Highlight`] of the [`Span`]. + pub highlight: Option<Highlight>, + /// The [`Padding`] of the [`Span`]. + /// + /// Currently, it only affects the bounds of the [`Highlight`]. + pub padding: Padding, + /// Whether the [`Span`] should be underlined or not. + pub underline: bool, + /// Whether the [`Span`] should be struck through or not. + pub strikethrough: bool, +} + +/// A text highlight. +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct Highlight { + /// The [`Background`] of the highlight. + pub background: Background, + /// The [`Border`] of the highlight. + pub border: Border, +} + +impl<'a, Link, Font> Span<'a, Link, Font> { + /// Creates a new [`Span`] of text with the given text fragment. + pub fn new(fragment: impl IntoFragment<'a>) -> Self { + Self { + text: fragment.into_fragment(), + size: None, + line_height: None, + font: None, + color: None, + highlight: None, + link: None, + padding: Padding::ZERO, + underline: false, + strikethrough: false, + } + } + + /// Sets the size of the [`Span`]. + pub fn size(mut self, size: impl Into<Pixels>) -> Self { + self.size = Some(size.into()); + self + } + + /// Sets the [`LineHeight`] of the [`Span`]. + pub fn line_height(mut self, line_height: impl Into<LineHeight>) -> Self { + self.line_height = Some(line_height.into()); + self + } + + /// Sets the font of the [`Span`]. + pub fn font(mut self, font: impl Into<Font>) -> Self { + self.font = Some(font.into()); + self + } + + /// Sets the font of the [`Span`], if any. + pub fn font_maybe(mut self, font: Option<impl Into<Font>>) -> Self { + self.font = font.map(Into::into); + self + } + + /// Sets the [`Color`] of the [`Span`]. + pub fn color(mut self, color: impl Into<Color>) -> Self { + self.color = Some(color.into()); + self + } + + /// Sets the [`Color`] of the [`Span`], if any. + pub fn color_maybe(mut self, color: Option<impl Into<Color>>) -> Self { + self.color = color.map(Into::into); + self + } + + /// Sets the link of the [`Span`]. + pub fn link(mut self, link: impl Into<Link>) -> Self { + self.link = Some(link.into()); + self + } + + /// Sets the link of the [`Span`], if any. + pub fn link_maybe(mut self, link: Option<impl Into<Link>>) -> Self { + self.link = link.map(Into::into); + self + } + + /// Sets the [`Background`] of the [`Span`]. + pub fn background(self, background: impl Into<Background>) -> Self { + self.background_maybe(Some(background)) + } + + /// Sets the [`Background`] of the [`Span`], if any. + pub fn background_maybe( + mut self, + background: Option<impl Into<Background>>, + ) -> Self { + let Some(background) = background else { + return self; + }; + + match &mut self.highlight { + Some(highlight) => { + highlight.background = background.into(); + } + None => { + self.highlight = Some(Highlight { + background: background.into(), + border: Border::default(), + }); + } + } + + self + } + + /// Sets the [`Border`] of the [`Span`]. + pub fn border(self, border: impl Into<Border>) -> Self { + self.border_maybe(Some(border)) + } + + /// Sets the [`Border`] of the [`Span`], if any. + pub fn border_maybe(mut self, border: Option<impl Into<Border>>) -> Self { + let Some(border) = border else { + return self; + }; + + match &mut self.highlight { + Some(highlight) => { + highlight.border = border.into(); + } + None => { + self.highlight = Some(Highlight { + border: border.into(), + background: Background::Color(Color::TRANSPARENT), + }); + } + } + + self + } + + /// Sets the [`Padding`] of the [`Span`]. + /// + /// It only affects the [`background`] and [`border`] of the + /// [`Span`], currently. + /// + /// [`background`]: Self::background + /// [`border`]: Self::border + pub fn padding(mut self, padding: impl Into<Padding>) -> Self { + self.padding = padding.into(); + self + } + + /// Sets whether the [`Span`] shoud be underlined or not. + pub fn underline(mut self, underline: bool) -> Self { + self.underline = underline; + self + } + + /// Sets whether the [`Span`] shoud be struck through or not. + pub fn strikethrough(mut self, strikethrough: bool) -> Self { + self.strikethrough = strikethrough; + self + } + + /// Turns the [`Span`] into a static one. + pub fn to_static(self) -> Span<'static, Link, Font> { + Span { + text: Cow::Owned(self.text.into_owned()), + size: self.size, + line_height: self.line_height, + font: self.font, + color: self.color, + link: self.link, + highlight: self.highlight, + padding: self.padding, + underline: self.underline, + strikethrough: self.strikethrough, + } + } +} + +impl<'a, Link, Font> From<&'a str> for Span<'a, Link, Font> { + fn from(value: &'a str) -> Self { + Span::new(value) + } +} + +impl<'a, Link, Font: PartialEq> PartialEq for Span<'a, Link, Font> { + fn eq(&self, other: &Self) -> bool { + self.text == other.text + && self.size == other.size + && self.line_height == other.line_height + && self.font == other.font + && self.color == other.color + } +} + +/// A fragment of [`Text`]. +/// +/// This is just an alias to a string that may be either +/// borrowed or owned. +pub type Fragment<'a> = Cow<'a, str>; + +/// A trait for converting a value to some text [`Fragment`]. +pub trait IntoFragment<'a> { + /// Converts the value to some text [`Fragment`]. + fn into_fragment(self) -> Fragment<'a>; +} + +impl<'a> IntoFragment<'a> for Fragment<'a> { + fn into_fragment(self) -> Fragment<'a> { + self + } +} + +impl<'a, 'b> IntoFragment<'a> for &'a Fragment<'b> { + fn into_fragment(self) -> Fragment<'a> { + Fragment::Borrowed(self) + } +} + +impl<'a> IntoFragment<'a> for &'a str { + fn into_fragment(self) -> Fragment<'a> { + Fragment::Borrowed(self) + } +} + +impl<'a> IntoFragment<'a> for &'a String { + fn into_fragment(self) -> Fragment<'a> { + Fragment::Borrowed(self.as_str()) + } +} + +impl<'a> IntoFragment<'a> for String { + fn into_fragment(self) -> Fragment<'a> { + Fragment::Owned(self) + } +} + +macro_rules! into_fragment { + ($type:ty) => { + impl<'a> IntoFragment<'a> for $type { + fn into_fragment(self) -> Fragment<'a> { + Fragment::Owned(self.to_string()) + } + } + + impl<'a> IntoFragment<'a> for &$type { + fn into_fragment(self) -> Fragment<'a> { + Fragment::Owned(self.to_string()) + } + } + }; +} + +into_fragment!(char); +into_fragment!(bool); + +into_fragment!(u8); +into_fragment!(u16); +into_fragment!(u32); +into_fragment!(u64); +into_fragment!(u128); +into_fragment!(usize); + +into_fragment!(i8); +into_fragment!(i16); +into_fragment!(i32); +into_fragment!(i64); +into_fragment!(i128); +into_fragment!(isize); + +into_fragment!(f32); +into_fragment!(f64); diff --git a/core/src/text/editor.rs b/core/src/text/editor.rs index fbf60696..cd30db3a 100644 --- a/core/src/text/editor.rs +++ b/core/src/text/editor.rs @@ -1,6 +1,6 @@ //! Edit text. use crate::text::highlighter::{self, Highlighter}; -use crate::text::LineHeight; +use crate::text::{LineHeight, Wrapping}; use crate::{Pixels, Point, Rectangle, Size}; use std::sync::Arc; @@ -13,6 +13,9 @@ pub trait Editor: Sized + Default { /// Creates a new [`Editor`] laid out with the given text. fn with_text(text: &str) -> Self; + /// Returns true if the [`Editor`] has no contents. + fn is_empty(&self) -> bool; + /// Returns the current [`Cursor`] of the [`Editor`]. fn cursor(&self) -> Cursor; @@ -47,6 +50,7 @@ pub trait Editor: Sized + Default { new_font: Self::Font, new_size: Pixels, new_line_height: LineHeight, + new_wrapping: Wrapping, new_highlighter: &mut impl Highlighter, ); @@ -70,6 +74,8 @@ pub enum Action { SelectWord, /// Select the line at the current cursor. SelectLine, + /// Select the entire buffer. + SelectAll, /// Perform an [`Edit`]. Edit(Edit), /// Click the [`Editor`] at the given [`Point`]. diff --git a/core/src/text/paragraph.rs b/core/src/text/paragraph.rs index 8ff04015..924276c3 100644 --- a/core/src/text/paragraph.rs +++ b/core/src/text/paragraph.rs @@ -1,6 +1,7 @@ +//! Draw paragraphs. use crate::alignment; -use crate::text::{Difference, Hit, Text}; -use crate::{Point, Size}; +use crate::text::{Difference, Hit, Span, Text}; +use crate::{Point, Rectangle, Size}; /// A text paragraph. pub trait Paragraph: Sized + Default { @@ -10,12 +11,17 @@ pub trait Paragraph: Sized + Default { /// Creates a new [`Paragraph`] laid out with the given [`Text`]. fn with_text(text: Text<&str, Self::Font>) -> Self; + /// Creates a new [`Paragraph`] laid out with the given [`Text`]. + fn with_spans<Link>( + text: Text<&[Span<'_, Link, Self::Font>], Self::Font>, + ) -> Self; + /// Lays out the [`Paragraph`] with some new boundaries. fn resize(&mut self, new_bounds: Size); /// Compares the [`Paragraph`] with some desired [`Text`] and returns the /// [`Difference`]. - fn compare(&self, text: Text<&str, Self::Font>) -> Difference; + fn compare(&self, text: Text<(), Self::Font>) -> Difference; /// Returns the horizontal alignment of the [`Paragraph`]. fn horizontal_alignment(&self) -> alignment::Horizontal; @@ -31,29 +37,100 @@ pub trait Paragraph: Sized + Default { /// [`Paragraph`], returning information about the nearest character. fn hit_test(&self, point: Point) -> Option<Hit>; + /// Tests whether the provided point is within the boundaries of a + /// [`Span`] in the [`Paragraph`], returning the index of the [`Span`] + /// that was hit. + fn hit_span(&self, point: Point) -> Option<usize>; + + /// Returns all bounds for the provided [`Span`] index of the [`Paragraph`]. + /// A [`Span`] can have multiple bounds for each line it's on. + fn span_bounds(&self, index: usize) -> Vec<Rectangle>; + /// Returns the distance to the given grapheme index in the [`Paragraph`]. fn grapheme_position(&self, line: usize, index: usize) -> Option<Point>; - /// Updates the [`Paragraph`] to match the given [`Text`], if needed. - fn update(&mut self, text: Text<&str, Self::Font>) { - match self.compare(text) { + /// Returns the minimum width that can fit the contents of the [`Paragraph`]. + fn min_width(&self) -> f32 { + self.min_bounds().width + } + + /// Returns the minimum height that can fit the contents of the [`Paragraph`]. + fn min_height(&self) -> f32 { + self.min_bounds().height + } +} + +/// A [`Paragraph`] of plain text. +#[derive(Debug, Clone, Default)] +pub struct Plain<P: Paragraph> { + raw: P, + content: String, +} + +impl<P: Paragraph> Plain<P> { + /// Creates a new [`Plain`] paragraph. + pub fn new(text: Text<&str, P::Font>) -> Self { + let content = text.content.to_owned(); + + Self { + raw: P::with_text(text), + content, + } + } + + /// Updates the plain [`Paragraph`] to match the given [`Text`], if needed. + pub fn update(&mut self, text: Text<&str, P::Font>) { + if self.content != text.content { + text.content.clone_into(&mut self.content); + self.raw = P::with_text(text); + return; + } + + match self.raw.compare(Text { + content: (), + bounds: text.bounds, + size: text.size, + line_height: text.line_height, + font: text.font, + horizontal_alignment: text.horizontal_alignment, + vertical_alignment: text.vertical_alignment, + shaping: text.shaping, + wrapping: text.wrapping, + }) { Difference::None => {} Difference::Bounds => { - self.resize(text.bounds); + self.raw.resize(text.bounds); } Difference::Shape => { - *self = Self::with_text(text); + self.raw = P::with_text(text); } } } - /// Returns the minimum width that can fit the contents of the [`Paragraph`]. - fn min_width(&self) -> f32 { - self.min_bounds().width + /// Returns the horizontal alignment of the [`Paragraph`]. + pub fn horizontal_alignment(&self) -> alignment::Horizontal { + self.raw.horizontal_alignment() } - /// Returns the minimum height that can fit the contents of the [`Paragraph`]. - fn min_height(&self) -> f32 { - self.min_bounds().height + /// Returns the vertical alignment of the [`Paragraph`]. + pub fn vertical_alignment(&self) -> alignment::Vertical { + self.raw.vertical_alignment() + } + + /// Returns the minimum boundaries that can fit the contents of the + /// [`Paragraph`]. + pub fn min_bounds(&self) -> Size { + self.raw.min_bounds() + } + + /// Returns the minimum width that can fit the contents of the + /// [`Paragraph`]. + pub fn min_width(&self) -> f32 { + self.raw.min_width() + } + + /// Returns the cached [`Paragraph`]. + pub fn raw(&self) -> &P { + &self.raw } } diff --git a/core/src/vector.rs b/core/src/vector.rs index 049e648f..ff848c4f 100644 --- a/core/src/vector.rs +++ b/core/src/vector.rs @@ -18,9 +18,17 @@ impl<T> Vector<T> { impl Vector { /// The zero [`Vector`]. pub const ZERO: Self = Self::new(0.0, 0.0); +} + +impl<T> std::ops::Neg for Vector<T> +where + T: std::ops::Neg<Output = T>, +{ + type Output = Self; - /// The unit [`Vector`]. - pub const UNIT: Self = Self::new(0.0, 0.0); + fn neg(self) -> Self::Output { + Self::new(-self.x, -self.y) + } } impl<T> std::ops::Add for Vector<T> diff --git a/core/src/widget.rs b/core/src/widget.rs index 0d12deba..c5beea54 100644 --- a/core/src/widget.rs +++ b/core/src/widget.rs @@ -27,11 +27,11 @@ use crate::{Clipboard, Length, Rectangle, Shell, Size, Vector}; /// widget: /// /// - [`bezier_tool`], a Paint-like tool for drawing Bézier curves using -/// [`lyon`]. +/// [`lyon`]. /// - [`custom_widget`], a demonstration of how to build a custom widget that -/// draws a circle. +/// draws a circle. /// - [`geometry`], a custom widget showcasing how to draw geometry with the -/// `Mesh2D` primitive in [`iced_wgpu`]. +/// `Mesh2D` primitive in [`iced_wgpu`]. /// /// [examples]: https://github.com/iced-rs/iced/tree/0.12/examples /// [`bezier_tool`]: https://github.com/iced-rs/iced/tree/0.12/examples/bezier_tool @@ -96,7 +96,7 @@ where Vec::new() } - /// Reconciliates the [`Widget`] with the provided [`Tree`]. + /// Reconciles the [`Widget`] with the provided [`Tree`]. fn diff(&self, _tree: &mut Tree) {} /// Applies an [`Operation`] to the [`Widget`]. @@ -105,7 +105,7 @@ where _state: &mut Tree, _layout: Layout<'_>, _renderer: &Renderer, - _operation: &mut dyn Operation<()>, + _operation: &mut dyn Operation, ) { } diff --git a/core/src/widget/operation.rs b/core/src/widget/operation.rs index 3e4ed618..097c3601 100644 --- a/core/src/widget/operation.rs +++ b/core/src/widget/operation.rs @@ -12,11 +12,12 @@ use crate::{Rectangle, Vector}; use std::any::Any; use std::fmt; +use std::marker::PhantomData; use std::sync::Arc; /// A piece of logic that can traverse the widget tree of an application in /// order to query or update some widget state. -pub trait Operation<T>: Send { +pub trait Operation<T = ()>: Send { /// Operates on a widget that contains other widgets. /// /// The `operate_on_children` function can be called to return control to @@ -37,6 +38,7 @@ pub trait Operation<T>: Send { _state: &mut dyn Scrollable, _id: Option<&Id>, _bounds: Rectangle, + _content_bounds: Rectangle, _translation: Vector, ) { } @@ -53,6 +55,53 @@ pub trait Operation<T>: Send { } } +impl<T, O> Operation<O> for Box<T> +where + T: Operation<O> + ?Sized, +{ + fn container( + &mut self, + id: Option<&Id>, + bounds: Rectangle, + operate_on_children: &mut dyn FnMut(&mut dyn Operation<O>), + ) { + self.as_mut().container(id, bounds, operate_on_children); + } + + fn focusable(&mut self, state: &mut dyn Focusable, id: Option<&Id>) { + self.as_mut().focusable(state, id); + } + + fn scrollable( + &mut self, + state: &mut dyn Scrollable, + id: Option<&Id>, + bounds: Rectangle, + content_bounds: Rectangle, + translation: Vector, + ) { + self.as_mut().scrollable( + state, + id, + bounds, + content_bounds, + translation, + ); + } + + fn text_input(&mut self, state: &mut dyn TextInput, id: Option<&Id>) { + self.as_mut().text_input(state, id); + } + + fn custom(&mut self, state: &mut dyn Any, id: Option<&Id>) { + self.as_mut().custom(state, id); + } + + fn finish(&self) -> Outcome<O> { + self.as_ref().finish() + } +} + /// The result of an [`Operation`]. pub enum Outcome<T> { /// The [`Operation`] produced no result. @@ -78,9 +127,69 @@ where } } +/// Wraps the [`Operation`] in a black box, erasing its returning type. +pub fn black_box<'a, T, O>( + operation: &'a mut dyn Operation<T>, +) -> impl Operation<O> + 'a +where + T: 'a, +{ + struct BlackBox<'a, T> { + operation: &'a mut dyn Operation<T>, + } + + impl<'a, T, O> Operation<O> for BlackBox<'a, T> { + fn container( + &mut self, + id: Option<&Id>, + bounds: Rectangle, + operate_on_children: &mut dyn FnMut(&mut dyn Operation<O>), + ) { + self.operation.container(id, bounds, &mut |operation| { + operate_on_children(&mut BlackBox { operation }); + }); + } + + 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>, + bounds: Rectangle, + content_bounds: Rectangle, + translation: Vector, + ) { + self.operation.scrollable( + state, + id, + bounds, + content_bounds, + translation, + ); + } + + 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<O> { + Outcome::None + } + } + + BlackBox { operation } +} + /// Maps the output of an [`Operation`] using the given function. pub fn map<A, B>( - operation: Box<dyn Operation<A>>, + operation: impl Operation<A>, f: impl Fn(A) -> B + Send + Sync + 'static, ) -> impl Operation<B> where @@ -88,13 +197,14 @@ where B: 'static, { #[allow(missing_debug_implementations)] - struct Map<A, B> { - operation: Box<dyn Operation<A>>, + struct Map<O, A, B> { + operation: O, f: Arc<dyn Fn(A) -> B + Send + Sync>, } - impl<A, B> Operation<B> for Map<A, B> + impl<O, A, B> Operation<B> for Map<O, A, B> where + O: Operation<A>, A: 'static, B: 'static, { @@ -127,9 +237,16 @@ where state: &mut dyn Scrollable, id: Option<&Id>, bounds: Rectangle, + content_bounds: Rectangle, translation: Vector, ) { - self.operation.scrollable(state, id, bounds, translation); + self.operation.scrollable( + state, + id, + bounds, + content_bounds, + translation, + ); } fn focusable( @@ -155,10 +272,7 @@ where let Self { operation, .. } = self; - MapRef { - operation: operation.as_mut(), - } - .container(id, bounds, operate_on_children); + MapRef { operation }.container(id, bounds, operate_on_children); } fn focusable(&mut self, state: &mut dyn Focusable, id: Option<&Id>) { @@ -170,9 +284,16 @@ where state: &mut dyn Scrollable, id: Option<&Id>, bounds: Rectangle, + content_bounds: Rectangle, translation: Vector, ) { - self.operation.scrollable(state, id, bounds, translation); + self.operation.scrollable( + state, + id, + bounds, + content_bounds, + translation, + ); } fn text_input(&mut self, state: &mut dyn TextInput, id: Option<&Id>) { @@ -201,6 +322,94 @@ where } } +/// Chains the output of an [`Operation`] with the provided function to +/// build a new [`Operation`]. +pub fn then<A, B, O>( + operation: impl Operation<A> + 'static, + f: fn(A) -> O, +) -> impl Operation<B> +where + A: 'static, + B: Send + 'static, + O: Operation<B> + 'static, +{ + struct Chain<T, O, A, B> + where + T: Operation<A>, + O: Operation<B>, + { + operation: T, + next: fn(A) -> O, + _result: PhantomData<B>, + } + + impl<T, O, A, B> Operation<B> for Chain<T, O, A, B> + where + T: Operation<A> + 'static, + O: Operation<B> + 'static, + A: 'static, + B: Send + 'static, + { + fn container( + &mut self, + id: Option<&Id>, + bounds: Rectangle, + operate_on_children: &mut dyn FnMut(&mut dyn Operation<B>), + ) { + self.operation.container(id, bounds, &mut |operation| { + operate_on_children(&mut black_box(operation)); + }); + } + + 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>, + bounds: Rectangle, + content_bounds: Rectangle, + translation: crate::Vector, + ) { + self.operation.scrollable( + state, + id, + bounds, + content_bounds, + translation, + ); + } + + 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 std::any::Any, id: Option<&Id>) { + self.operation.custom(state, id); + } + + fn finish(&self) -> Outcome<B> { + match self.operation.finish() { + Outcome::None => Outcome::None, + Outcome::Some(value) => { + Outcome::Chain(Box::new((self.next)(value))) + } + Outcome::Chain(operation) => { + Outcome::Chain(Box::new(then(operation, self.next))) + } + } + } + } + + Chain { + operation, + next: f, + _result: PhantomData, + } +} + /// Produces an [`Operation`] that applies the given [`Operation`] to the /// children of a container with the given [`Id`]. pub fn scope<T: 'static>( diff --git a/core/src/widget/operation/focusable.rs b/core/src/widget/operation/focusable.rs index 68c22faa..867c682e 100644 --- a/core/src/widget/operation/focusable.rs +++ b/core/src/widget/operation/focusable.rs @@ -1,5 +1,5 @@ //! Operate on widgets that can be focused. -use crate::widget::operation::{Operation, Outcome}; +use crate::widget::operation::{self, Operation, Outcome}; use crate::widget::Id; use crate::Rectangle; @@ -58,19 +58,12 @@ pub fn focus<T>(target: Id) -> impl Operation<T> { /// Produces an [`Operation`] that generates a [`Count`] and chains it with the /// provided function to build a new [`Operation`]. -pub fn count<T, O>(f: fn(Count) -> O) -> impl Operation<T> -where - O: Operation<T> + 'static, -{ - struct CountFocusable<O> { +pub fn count() -> impl Operation<Count> { + struct CountFocusable { count: Count, - next: fn(Count) -> O, } - impl<T, O> Operation<T> for CountFocusable<O> - where - O: Operation<T> + 'static, - { + impl Operation<Count> for CountFocusable { fn focusable(&mut self, state: &mut dyn Focusable, _id: Option<&Id>) { if state.is_focused() { self.count.focused = Some(self.count.total); @@ -83,26 +76,28 @@ where &mut self, _id: Option<&Id>, _bounds: Rectangle, - operate_on_children: &mut dyn FnMut(&mut dyn Operation<T>), + operate_on_children: &mut dyn FnMut(&mut dyn Operation<Count>), ) { operate_on_children(self); } - fn finish(&self) -> Outcome<T> { - Outcome::Chain(Box::new((self.next)(self.count))) + fn finish(&self) -> Outcome<Count> { + Outcome::Some(self.count) } } CountFocusable { count: Count::default(), - next: f, } } /// Produces an [`Operation`] that searches for the current focused widget, and /// - if found, focuses the previous focusable widget. /// - if not found, focuses the last focusable widget. -pub fn focus_previous<T>() -> impl Operation<T> { +pub fn focus_previous<T>() -> impl Operation<T> +where + T: Send + 'static, +{ struct FocusPrevious { count: Count, current: usize, @@ -136,13 +131,16 @@ pub fn focus_previous<T>() -> impl Operation<T> { } } - count(|count| FocusPrevious { count, current: 0 }) + operation::then(count(), |count| FocusPrevious { count, current: 0 }) } /// Produces an [`Operation`] that searches for the current focused widget, and /// - if found, focuses the next focusable widget. /// - if not found, focuses the first focusable widget. -pub fn focus_next<T>() -> impl Operation<T> { +pub fn focus_next<T>() -> impl Operation<T> +where + T: Send + 'static, +{ struct FocusNext { count: Count, current: usize, @@ -170,7 +168,7 @@ pub fn focus_next<T>() -> impl Operation<T> { } } - count(|count| FocusNext { count, current: 0 }) + operation::then(count(), |count| FocusNext { count, current: 0 }) } /// Produces an [`Operation`] that searches for the current focused widget diff --git a/core/src/widget/operation/scrollable.rs b/core/src/widget/operation/scrollable.rs index 12161255..c2fecf56 100644 --- a/core/src/widget/operation/scrollable.rs +++ b/core/src/widget/operation/scrollable.rs @@ -9,6 +9,14 @@ pub trait Scrollable { /// Scroll the widget to the given [`AbsoluteOffset`] along the horizontal & vertical axis. fn scroll_to(&mut self, offset: AbsoluteOffset); + + /// Scroll the widget by the given [`AbsoluteOffset`] along the horizontal & vertical axis. + fn scroll_by( + &mut self, + offset: AbsoluteOffset, + bounds: Rectangle, + content_bounds: Rectangle, + ); } /// Produces an [`Operation`] that snaps the widget with the given [`Id`] to @@ -34,6 +42,7 @@ pub fn snap_to<T>(target: Id, offset: RelativeOffset) -> impl Operation<T> { state: &mut dyn Scrollable, id: Option<&Id>, _bounds: Rectangle, + _content_bounds: Rectangle, _translation: Vector, ) { if Some(&self.target) == id { @@ -68,6 +77,7 @@ pub fn scroll_to<T>(target: Id, offset: AbsoluteOffset) -> impl Operation<T> { state: &mut dyn Scrollable, id: Option<&Id>, _bounds: Rectangle, + _content_bounds: Rectangle, _translation: Vector, ) { if Some(&self.target) == id { @@ -79,6 +89,41 @@ pub fn scroll_to<T>(target: Id, offset: AbsoluteOffset) -> impl Operation<T> { ScrollTo { target, offset } } +/// Produces an [`Operation`] that scrolls the widget with the given [`Id`] by +/// the provided [`AbsoluteOffset`]. +pub fn scroll_by<T>(target: Id, offset: AbsoluteOffset) -> impl Operation<T> { + struct ScrollBy { + target: Id, + offset: AbsoluteOffset, + } + + impl<T> Operation<T> for ScrollBy { + fn container( + &mut self, + _id: Option<&Id>, + _bounds: Rectangle, + operate_on_children: &mut dyn FnMut(&mut dyn Operation<T>), + ) { + operate_on_children(self); + } + + fn scrollable( + &mut self, + state: &mut dyn Scrollable, + id: Option<&Id>, + bounds: Rectangle, + content_bounds: Rectangle, + _translation: Vector, + ) { + if Some(&self.target) == id { + state.scroll_by(self.offset, bounds, content_bounds); + } + } + } + + ScrollBy { target, offset } +} + /// The amount of absolute offset in each direction of a [`Scrollable`]. #[derive(Debug, Clone, Copy, PartialEq, Default)] pub struct AbsoluteOffset { diff --git a/core/src/widget/text.rs b/core/src/widget/text.rs index f1f0b345..d8d6e4c6 100644 --- a/core/src/widget/text.rs +++ b/core/src/widget/text.rs @@ -3,16 +3,15 @@ use crate::alignment; use crate::layout; use crate::mouse; use crate::renderer; -use crate::text::{self, Paragraph}; +use crate::text; +use crate::text::paragraph::{self, Paragraph}; use crate::widget::tree::{self, Tree}; use crate::{ Color, Element, Layout, Length, Pixels, Point, Rectangle, Size, Theme, Widget, }; -use std::borrow::Cow; - -pub use text::{LineHeight, Shaping}; +pub use text::{LineHeight, Shaping, Wrapping}; /// A paragraph of text. #[allow(missing_debug_implementations)] @@ -21,7 +20,7 @@ where Theme: Catalog, Renderer: text::Renderer, { - fragment: Fragment<'a>, + fragment: text::Fragment<'a>, size: Option<Pixels>, line_height: LineHeight, width: Length, @@ -30,6 +29,7 @@ where vertical_alignment: alignment::Vertical, font: Option<Renderer::Font>, shaping: Shaping, + wrapping: Wrapping, class: Theme::Class<'a>, } @@ -39,7 +39,7 @@ where Renderer: text::Renderer, { /// Create a new fragment of [`Text`] with the given contents. - pub fn new(fragment: impl IntoFragment<'a>) -> Self { + pub fn new(fragment: impl text::IntoFragment<'a>) -> Self { Text { fragment: fragment.into_fragment(), size: None, @@ -49,7 +49,8 @@ where height: Length::Shrink, horizontal_alignment: alignment::Horizontal::Left, vertical_alignment: alignment::Vertical::Top, - shaping: Shaping::Basic, + shaping: Shaping::default(), + wrapping: Wrapping::default(), class: Theme::default(), } } @@ -86,21 +87,27 @@ where self } + /// Centers the [`Text`], both horizontally and vertically. + pub fn center(self) -> Self { + self.align_x(alignment::Horizontal::Center) + .align_y(alignment::Vertical::Center) + } + /// Sets the [`alignment::Horizontal`] of the [`Text`]. - pub fn horizontal_alignment( + pub fn align_x( mut self, - alignment: alignment::Horizontal, + alignment: impl Into<alignment::Horizontal>, ) -> Self { - self.horizontal_alignment = alignment; + self.horizontal_alignment = alignment.into(); self } /// Sets the [`alignment::Vertical`] of the [`Text`]. - pub fn vertical_alignment( + pub fn align_y( mut self, - alignment: alignment::Vertical, + alignment: impl Into<alignment::Vertical>, ) -> Self { - self.vertical_alignment = alignment; + self.vertical_alignment = alignment.into(); self } @@ -110,6 +117,12 @@ where self } + /// Sets the [`Wrapping`] strategy of the [`Text`]. + pub fn wrapping(mut self, wrapping: Wrapping) -> Self { + self.wrapping = wrapping; + self + } + /// Sets the style of the [`Text`]. #[must_use] pub fn style(mut self, style: impl Fn(&Theme) -> Style + 'a) -> Self @@ -149,7 +162,7 @@ where /// The internal state of a [`Text`] widget. #[derive(Debug, Default)] -pub struct State<P: Paragraph>(P); +pub struct State<P: Paragraph>(pub paragraph::Plain<P>); impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer> for Text<'a, Theme, Renderer> @@ -162,7 +175,9 @@ where } fn state(&self) -> tree::State { - tree::State::new(State(Renderer::Paragraph::default())) + tree::State::new(State::<Renderer::Paragraph>( + paragraph::Plain::default(), + )) } fn size(&self) -> Size<Length> { @@ -191,6 +206,7 @@ where self.horizontal_alignment, self.vertical_alignment, self.shaping, + self.wrapping, ) } @@ -207,7 +223,7 @@ where let state = tree.state.downcast_ref::<State<Renderer::Paragraph>>(); let style = theme.style(&self.class); - draw(renderer, defaults, layout, state, style, viewport); + draw(renderer, defaults, layout, state.0.raw(), style, viewport); } } @@ -225,6 +241,7 @@ pub fn layout<Renderer>( horizontal_alignment: alignment::Horizontal, vertical_alignment: alignment::Vertical, shaping: Shaping, + wrapping: Wrapping, ) -> layout::Node where Renderer: text::Renderer, @@ -246,6 +263,7 @@ where horizontal_alignment, vertical_alignment, shaping, + wrapping, }); paragraph.min_bounds() @@ -266,13 +284,12 @@ pub fn draw<Renderer>( renderer: &mut Renderer, style: &renderer::Style, layout: Layout<'_>, - state: &State<Renderer::Paragraph>, + paragraph: &Renderer::Paragraph, appearance: Style, viewport: &Rectangle, ) where Renderer: text::Renderer, { - let State(ref paragraph) = state; let bounds = layout.bounds(); let x = match paragraph.horizontal_alignment() { @@ -367,80 +384,42 @@ impl Catalog for Theme { } } -/// A fragment of [`Text`]. -/// -/// This is just an alias to a string that may be either -/// borrowed or owned. -pub type Fragment<'a> = Cow<'a, str>; - -/// A trait for converting a value to some text [`Fragment`]. -pub trait IntoFragment<'a> { - /// Converts the value to some text [`Fragment`]. - fn into_fragment(self) -> Fragment<'a>; +/// The default text styling; color is inherited. +pub fn default(_theme: &Theme) -> Style { + Style { color: None } } -impl<'a> IntoFragment<'a> for Fragment<'a> { - fn into_fragment(self) -> Fragment<'a> { - self +/// Text with the default base color. +pub fn base(theme: &Theme) -> Style { + Style { + color: Some(theme.palette().text), } } -impl<'a, 'b> IntoFragment<'a> for &'a Fragment<'b> { - fn into_fragment(self) -> Fragment<'a> { - Fragment::Borrowed(self) +/// Text conveying some important information, like an action. +pub fn primary(theme: &Theme) -> Style { + Style { + color: Some(theme.palette().primary), } } -impl<'a> IntoFragment<'a> for &'a str { - fn into_fragment(self) -> Fragment<'a> { - Fragment::Borrowed(self) +/// Text conveying some secondary information, like a footnote. +pub fn secondary(theme: &Theme) -> Style { + Style { + color: Some(theme.extended_palette().secondary.strong.color), } } -impl<'a> IntoFragment<'a> for &'a String { - fn into_fragment(self) -> Fragment<'a> { - Fragment::Borrowed(self.as_str()) +/// Text conveying some positive information, like a successful event. +pub fn success(theme: &Theme) -> Style { + Style { + color: Some(theme.palette().success), } } -impl<'a> IntoFragment<'a> for String { - fn into_fragment(self) -> Fragment<'a> { - Fragment::Owned(self) +/// Text conveying some negative information, like an error. +pub fn danger(theme: &Theme) -> Style { + Style { + color: Some(theme.palette().danger), } } - -macro_rules! into_fragment { - ($type:ty) => { - impl<'a> IntoFragment<'a> for $type { - fn into_fragment(self) -> Fragment<'a> { - Fragment::Owned(self.to_string()) - } - } - - impl<'a> IntoFragment<'a> for &$type { - fn into_fragment(self) -> Fragment<'a> { - Fragment::Owned(self.to_string()) - } - } - }; -} - -into_fragment!(char); -into_fragment!(bool); - -into_fragment!(u8); -into_fragment!(u16); -into_fragment!(u32); -into_fragment!(u64); -into_fragment!(u128); -into_fragment!(usize); - -into_fragment!(i8); -into_fragment!(i16); -into_fragment!(i32); -into_fragment!(i64); -into_fragment!(i128); -into_fragment!(isize); - -into_fragment!(f32); -into_fragment!(f64); diff --git a/core/src/widget/tree.rs b/core/src/widget/tree.rs index 6b1a1309..2600cfc6 100644 --- a/core/src/widget/tree.rs +++ b/core/src/widget/tree.rs @@ -46,7 +46,7 @@ impl Tree { } } - /// Reconciliates the current tree with the provided [`Widget`]. + /// Reconciles 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). @@ -81,7 +81,7 @@ impl Tree { ); } - /// Reconciliates the children of the tree with the provided list of widgets using custom + /// Reconciles the children of the tree with the provided list of widgets using custom /// logic both for diffing and creating new widget state. pub fn diff_children_custom<T>( &mut self, @@ -107,7 +107,7 @@ impl Tree { } } -/// Reconciliates the `current_children` with the provided list of widgets using +/// Reconciles the `current_children` with the provided list of widgets using /// custom logic both for diffing and creating new widget state. /// /// The algorithm will try to minimize the impact of diffing by querying the diff --git a/core/src/window/event.rs b/core/src/window/event.rs index a14d127f..c9532e0d 100644 --- a/core/src/window/event.rs +++ b/core/src/window/event.rs @@ -23,20 +23,10 @@ pub enum Event { Closed, /// 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, - }, + Moved(Point), /// A window was resized. - Resized { - /// The new logical width of the window - width: u32, - /// The new logical height of the window - height: u32, - }, + Resized(Size), /// A window redraw was requested. /// diff --git a/core/src/window/settings/linux.rs b/core/src/window/settings/linux.rs index 009b9d9e..0a1e11cd 100644 --- a/core/src/window/settings/linux.rs +++ b/core/src/window/settings/linux.rs @@ -8,4 +8,10 @@ pub struct PlatformSpecific { /// As a best practice, it is suggested to select an application id that match /// the basename of the application’s .desktop file. pub application_id: String, + + /// Whether bypass the window manager mapping for x11 windows + /// + /// This flag is particularly useful for creating UI elements that need precise + /// positioning and immediate display without window manager interference. + pub override_redirect: bool, } diff --git a/core/src/window/settings/windows.rs b/core/src/window/settings/windows.rs index 88fe2fbd..a47582a6 100644 --- a/core/src/window/settings/windows.rs +++ b/core/src/window/settings/windows.rs @@ -8,6 +8,12 @@ pub struct PlatformSpecific { /// Whether show or hide the window icon in the taskbar. pub skip_taskbar: bool, + + /// Shows or hides the background drop shadow for undecorated windows. + /// + /// The shadow is hidden by default. + /// Enabling the shadow causes a thin 1px line to appear on the top of the window. + pub undecorated_shadow: bool, } impl Default for PlatformSpecific { @@ -15,6 +21,7 @@ impl Default for PlatformSpecific { Self { drag_and_drop: true, skip_taskbar: false, + undecorated_shadow: false, } } } |