diff options
192 files changed, 7663 insertions, 3270 deletions
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ba1ab003..e7af3b03 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -19,7 +19,7 @@ jobs: - name: Build todos binary run: cargo build --verbose --profile release-opt --package todos - name: Archive todos binary - uses: actions/upload-artifact@v1 + uses: actions/upload-artifact@v4 with: name: todos-x86_64-unknown-linux-gnu path: target/release-opt/todos @@ -28,7 +28,7 @@ jobs: - name: Rename todos .deb package run: mv target/debian/*.deb target/debian/iced_todos-x86_64-debian-linux-gnu.deb - name: Archive todos .deb package - uses: actions/upload-artifact@v1 + uses: actions/upload-artifact@v4 with: name: todos-x86_64-debian-linux-gnu path: target/debian/iced_todos-x86_64-debian-linux-gnu.deb @@ -48,7 +48,7 @@ jobs: - name: Build todos binary run: cargo build --verbose --profile release-opt --package todos - name: Archive todos binary - uses: actions/upload-artifact@v1 + uses: actions/upload-artifact@v4 with: name: todos-x86_64-pc-windows-msvc path: target/release-opt/todos.exe @@ -65,7 +65,7 @@ jobs: - name: Open binary via double-click run: chmod +x target/release-opt/todos - name: Archive todos binary - uses: actions/upload-artifact@v1 + uses: actions/upload-artifact@v4 with: name: todos-x86_64-apple-darwin path: target/release-opt/todos @@ -80,14 +80,14 @@ jobs: - name: Build todos binary for Raspberry Pi 3/4 (64 bits) run: cross build --verbose --profile release-opt --package todos --target aarch64-unknown-linux-gnu - name: Archive todos binary - uses: actions/upload-artifact@v1 + uses: actions/upload-artifact@v4 with: name: todos-aarch64-unknown-linux-gnu path: target/aarch64-unknown-linux-gnu/release-opt/todos - name: Build todos binary for Raspberry Pi 2/3/4 (32 bits) run: cross build --verbose --profile release-opt --package todos --target armv7-unknown-linux-gnueabihf - name: Archive todos binary - uses: actions/upload-artifact@v1 + uses: actions/upload-artifact@v4 with: name: todos-armv7-unknown-linux-gnueabihf path: target/armv7-unknown-linux-gnueabihf/release-opt/todos diff --git a/.github/workflows/document.yml b/.github/workflows/document.yml index a213e590..57dc1375 100644 --- a/.github/workflows/document.yml +++ b/.github/workflows/document.yml @@ -14,6 +14,7 @@ jobs: run: | RUSTDOCFLAGS="--cfg docsrs" \ cargo doc --no-deps --all-features \ + -p futures-core \ -p iced_core \ -p iced_highlighter \ -p iced_futures \ @@ -27,13 +27,17 @@ wgpu = ["iced_renderer/wgpu", "iced_widget/wgpu"] # Enable the `tiny-skia` software renderer backend tiny-skia = ["iced_renderer/tiny-skia"] # Enables the `Image` widget -image = ["iced_widget/image", "dep:image"] +image = ["image-without-codecs", "image/default"] +# Enables the `Image` widget, without any built-in codecs of the `image` crate +image-without-codecs = ["iced_widget/image", "dep:image"] # Enables the `Svg` widget svg = ["iced_widget/svg"] # Enables the `Canvas` widget canvas = ["iced_widget/canvas"] # Enables the `QRCode` widget qr_code = ["iced_widget/qr_code"] +# Enables the `markdown` widget +markdown = ["iced_widget/markdown"] # Enables lazy widgets lazy = ["iced_widget/lazy"] # Enables a debug view in native platforms (press F12) @@ -51,7 +55,7 @@ web-colors = ["iced_renderer/web-colors"] # Enables the WebGL backend, replacing WebGPU webgl = ["iced_renderer/webgl"] # Enables the syntax `highlighter` module -highlighter = ["iced_highlighter"] +highlighter = ["iced_highlighter", "iced_widget/highlighter"] # Enables experimental multi-window support. multi-window = ["iced_winit/multi-window"] # Enables the advanced module @@ -138,14 +142,14 @@ async-std = "1.0" bitflags = "2.0" bytemuck = { version = "1.0", features = ["derive"] } bytes = "1.6" -cosmic-text = "0.10" +cosmic-text = "0.12" dark-light = "1.0" futures = "0.3" glam = "0.25" -glyphon = { git = "https://github.com/hecrj/glyphon.git", rev = "f07e7bab705e69d39a5e6e52c73039a93c4552f8" } +glyphon = { git = "https://github.com/hecrj/glyphon.git", rev = "feef9f5630c2adb3528937e55f7bfad2da561a65" } guillotiere = "0.6" half = "2.2" -image = "0.24" +image = { version = "0.24", default-features = false } kamadak-exif = "0.5" kurbo = "0.10" log = "0.4" @@ -155,10 +159,11 @@ num-traits = "0.2" once_cell = "1.0" ouroboros = "0.18" palette = "0.7" +pulldown-cmark = "0.11" qrcode = { version = "0.13", default-features = false } raw-window-handle = "0.6" -resvg = "0.36" -rustc-hash = "1.0" +resvg = "0.42" +rustc-hash = "2.0" smol = "1.0" smol_str = "0.2" softbuffer = "0.4" @@ -169,9 +174,10 @@ tiny-skia = "0.11" tokio = "1.0" tracing = "0.1" unicode-segmentation = "1.0" +url = "2.5" wasm-bindgen-futures = "0.4" wasm-timer = "0.2" -web-sys = "=0.3.67" +web-sys = "0.3.69" web-time = "1.1" wgpu = "0.19" winapi = "0.3" @@ -179,7 +185,7 @@ window_clipboard = "0.4.1" winit = { git = "https://github.com/iced-rs/winit.git", rev = "254d6b3420ce4e674f516f7a2bd440665e05484d" } [workspace.lints.rust] -rust_2018_idioms = "forbid" +rust_2018_idioms = { level = "forbid", priority = -1 } missing_debug_implementations = "deny" missing_docs = "deny" unsafe_code = "deny" 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, } } } diff --git a/examples/arc/src/main.rs b/examples/arc/src/main.rs index b1e8402a..18873259 100644 --- a/examples/arc/src/main.rs +++ b/examples/arc/src/main.rs @@ -4,7 +4,7 @@ use iced::mouse; use iced::widget::canvas::{ self, stroke, Cache, Canvas, Geometry, Path, Stroke, }; -use iced::{Element, Length, Point, Rectangle, Renderer, Subscription, Theme}; +use iced::{Element, Fill, Point, Rectangle, Renderer, Subscription, Theme}; pub fn main() -> iced::Result { iced::application("Arc - Iced", Arc::update, Arc::view) @@ -30,10 +30,7 @@ impl Arc { } fn view(&self) -> Element<Message> { - Canvas::new(self) - .width(Length::Fill) - .height(Length::Fill) - .into() + Canvas::new(self).width(Fill).height(Fill).into() } fn subscription(&self) -> Subscription<Message> { diff --git a/examples/bezier_tool/src/main.rs b/examples/bezier_tool/src/main.rs index eaf84b97..949bfad7 100644 --- a/examples/bezier_tool/src/main.rs +++ b/examples/bezier_tool/src/main.rs @@ -1,7 +1,6 @@ //! This example showcases an interactive `Canvas` for drawing Bézier curves. -use iced::alignment; use iced::widget::{button, container, horizontal_space, hover}; -use iced::{Element, Length, Theme}; +use iced::{Element, Fill, Theme}; pub fn main() -> iced::Result { iced::application("Bezier Tool - Iced", Example::update, Example::view) @@ -48,8 +47,7 @@ impl Example { .on_press(Message::Clear), ) .padding(10) - .width(Length::Fill) - .align_x(alignment::Horizontal::Right) + .align_right(Fill) }, )) .padding(20) @@ -61,7 +59,7 @@ mod bezier { use iced::mouse; use iced::widget::canvas::event::{self, Event}; use iced::widget::canvas::{self, Canvas, Frame, Geometry, Path, Stroke}; - use iced::{Element, Length, Point, Rectangle, Renderer, Theme}; + use iced::{Element, Fill, Point, Rectangle, Renderer, Theme}; #[derive(Default)] pub struct State { @@ -74,8 +72,8 @@ mod bezier { state: self, curves, }) - .width(Length::Fill) - .height(Length::Fill) + .width(Fill) + .height(Fill) .into() } diff --git a/examples/clock/src/main.rs b/examples/clock/src/main.rs index 4584a0c7..ef3064c7 100644 --- a/examples/clock/src/main.rs +++ b/examples/clock/src/main.rs @@ -4,7 +4,7 @@ use iced::time; use iced::widget::canvas::{stroke, Cache, Geometry, LineCap, Path, Stroke}; use iced::widget::{canvas, container}; use iced::{ - Degrees, Element, Font, Length, Point, Rectangle, Renderer, Subscription, + Degrees, Element, Fill, Font, Point, Rectangle, Renderer, Subscription, Theme, Vector, }; @@ -43,15 +43,9 @@ impl Clock { } fn view(&self) -> Element<Message> { - let canvas = canvas(self as &Self) - .width(Length::Fill) - .height(Length::Fill); - - container(canvas) - .width(Length::Fill) - .height(Length::Fill) - .padding(20) - .into() + let canvas = canvas(self as &Self).width(Fill).height(Fill); + + container(canvas).padding(20).into() } fn subscription(&self) -> Subscription<Message> { diff --git a/examples/color_palette/src/main.rs b/examples/color_palette/src/main.rs index e4b19731..7f21003b 100644 --- a/examples/color_palette/src/main.rs +++ b/examples/color_palette/src/main.rs @@ -1,10 +1,10 @@ -use iced::alignment::{self, Alignment}; +use iced::alignment; use iced::mouse; use iced::widget::canvas::{self, Canvas, Frame, Geometry, Path}; use iced::widget::{column, row, text, Slider}; use iced::{ - Color, Element, Font, Length, Pixels, Point, Rectangle, Renderer, Size, - Vector, + Center, Color, Element, Fill, Font, Pixels, Point, Rectangle, Renderer, + Size, Vector, }; use palette::{convert::FromColor, rgb::Rgb, Darken, Hsl, Lighten, ShiftHue}; use std::marker::PhantomData; @@ -150,10 +150,7 @@ impl Theme { } pub fn view(&self) -> Element<Message> { - Canvas::new(self) - .width(Length::Fill) - .height(Length::Fill) - .into() + Canvas::new(self).width(Fill).height(Fill).into() } fn draw(&self, frame: &mut Frame, text_color: Color) { @@ -320,7 +317,7 @@ impl<C: ColorSpace + Copy> ColorPicker<C> { text(color.to_string()).width(185).size(12), ] .spacing(10) - .align_items(Alignment::Center) + .align_y(Center) .into() } } diff --git a/examples/combo_box/src/main.rs b/examples/combo_box/src/main.rs index ff759ab4..af53b17a 100644 --- a/examples/combo_box/src/main.rs +++ b/examples/combo_box/src/main.rs @@ -1,7 +1,7 @@ use iced::widget::{ center, column, combo_box, scrollable, text, vertical_space, }; -use iced::{Alignment, Element, Length}; +use iced::{Center, Element, Fill}; pub fn main() -> iced::Result { iced::run("Combo Box - Iced", Example::update, Example::view) @@ -64,8 +64,8 @@ impl Example { combo_box, vertical_space().height(150), ] - .width(Length::Fill) - .align_items(Alignment::Center) + .width(Fill) + .align_x(Center) .spacing(10); center(scrollable(content)).into() diff --git a/examples/component/src/main.rs b/examples/component/src/main.rs deleted file mode 100644 index 5625f12a..00000000 --- a/examples/component/src/main.rs +++ /dev/null @@ -1,156 +0,0 @@ -use iced::widget::center; -use iced::Element; - -use numeric_input::numeric_input; - -pub fn main() -> iced::Result { - iced::run("Component - Iced", Component::update, Component::view) -} - -#[derive(Default)] -struct Component { - value: Option<u32>, -} - -#[derive(Debug, Clone, Copy)] -enum Message { - NumericInputChanged(Option<u32>), -} - -impl Component { - fn update(&mut self, message: Message) { - match message { - Message::NumericInputChanged(value) => { - self.value = value; - } - } - } - - fn view(&self) -> Element<Message> { - center(numeric_input(self.value, Message::NumericInputChanged)) - .padding(20) - .into() - } -} - -mod numeric_input { - use iced::alignment::{self, Alignment}; - use iced::widget::{button, component, row, text, text_input, Component}; - use iced::{Element, Length, Size}; - - pub struct NumericInput<Message> { - value: Option<u32>, - on_change: Box<dyn Fn(Option<u32>) -> Message>, - } - - pub fn numeric_input<Message>( - value: Option<u32>, - on_change: impl Fn(Option<u32>) -> Message + 'static, - ) -> NumericInput<Message> { - NumericInput::new(value, on_change) - } - - #[derive(Debug, Clone)] - pub enum Event { - InputChanged(String), - IncrementPressed, - DecrementPressed, - } - - impl<Message> NumericInput<Message> { - pub fn new( - value: Option<u32>, - on_change: impl Fn(Option<u32>) -> Message + 'static, - ) -> Self { - Self { - value, - on_change: Box::new(on_change), - } - } - } - - impl<Message, Theme> Component<Message, Theme> for NumericInput<Message> - where - Theme: text::Catalog + button::Catalog + text_input::Catalog + 'static, - { - type State = (); - type Event = Event; - - fn update( - &mut self, - _state: &mut Self::State, - event: Event, - ) -> Option<Message> { - match event { - Event::IncrementPressed => Some((self.on_change)(Some( - self.value.unwrap_or_default().saturating_add(1), - ))), - Event::DecrementPressed => Some((self.on_change)(Some( - self.value.unwrap_or_default().saturating_sub(1), - ))), - Event::InputChanged(value) => { - if value.is_empty() { - Some((self.on_change)(None)) - } else { - value - .parse() - .ok() - .map(Some) - .map(self.on_change.as_ref()) - } - } - } - } - - fn view(&self, _state: &Self::State) -> Element<'_, Event, Theme> { - let button = |label, on_press| { - button( - text(label) - .width(Length::Fill) - .height(Length::Fill) - .horizontal_alignment(alignment::Horizontal::Center) - .vertical_alignment(alignment::Vertical::Center), - ) - .width(40) - .height(40) - .on_press(on_press) - }; - - row![ - button("-", Event::DecrementPressed), - text_input( - "Type a number", - self.value - .as_ref() - .map(u32::to_string) - .as_deref() - .unwrap_or(""), - ) - .on_input(Event::InputChanged) - .padding(10), - button("+", Event::IncrementPressed), - ] - .align_items(Alignment::Center) - .spacing(10) - .into() - } - - fn size_hint(&self) -> Size<Length> { - Size { - width: Length::Fill, - height: Length::Shrink, - } - } - } - - impl<'a, Message, Theme> From<NumericInput<Message>> - for Element<'a, Message, Theme> - where - Theme: text::Catalog + button::Catalog + text_input::Catalog + 'static, - Message: 'a, - { - fn from(numeric_input: NumericInput<Message>) -> Self { - component(numeric_input) - } - } -} diff --git a/examples/counter/src/main.rs b/examples/counter/src/main.rs index 0dd7a976..81684c1c 100644 --- a/examples/counter/src/main.rs +++ b/examples/counter/src/main.rs @@ -1,5 +1,5 @@ use iced::widget::{button, column, text, Column}; -use iced::Alignment; +use iced::Center; pub fn main() -> iced::Result { iced::run("A cool counter", Counter::update, Counter::view) @@ -35,6 +35,6 @@ impl Counter { button("Decrement").on_press(Message::Decrement) ] .padding(20) - .align_items(Alignment::Center) + .align_x(Center) } } diff --git a/examples/custom_quad/src/main.rs b/examples/custom_quad/src/main.rs index b53a40d6..dc425cc6 100644 --- a/examples/custom_quad/src/main.rs +++ b/examples/custom_quad/src/main.rs @@ -3,12 +3,13 @@ mod quad { use iced::advanced::layout::{self, Layout}; use iced::advanced::renderer; use iced::advanced::widget::{self, Widget}; + use iced::border; use iced::mouse; use iced::{Border, Color, Element, Length, Rectangle, Shadow, Size}; pub struct CustomQuad { size: f32, - radius: [f32; 4], + radius: border::Radius, border_width: f32, shadow: Shadow, } @@ -16,7 +17,7 @@ mod quad { impl CustomQuad { pub fn new( size: f32, - radius: [f32; 4], + radius: border::Radius, border_width: f32, shadow: Shadow, ) -> Self { @@ -63,7 +64,7 @@ mod quad { renderer::Quad { bounds: layout.bounds(), border: Border { - radius: self.radius.into(), + radius: self.radius, width: self.border_width, color: Color::from_rgb(1.0, 0.0, 0.0), }, @@ -81,15 +82,16 @@ mod quad { } } +use iced::border; use iced::widget::{center, column, slider, text}; -use iced::{Alignment, Color, Element, Shadow, Vector}; +use iced::{Center, Color, Element, Shadow, Vector}; pub fn main() -> iced::Result { iced::run("Custom Quad - Iced", Example::update, Example::view) } struct Example { - radius: [f32; 4], + radius: border::Radius, border_width: f32, shadow: Shadow, } @@ -110,7 +112,7 @@ enum Message { impl Example { fn new() -> Self { Self { - radius: [50.0; 4], + radius: border::radius(50), border_width: 0.0, shadow: Shadow { color: Color::from_rgba(0.0, 0.0, 0.0, 0.8), @@ -121,19 +123,18 @@ impl Example { } fn update(&mut self, message: Message) { - let [tl, tr, br, bl] = self.radius; match message { Message::RadiusTopLeftChanged(radius) => { - self.radius = [radius, tr, br, bl]; + self.radius = self.radius.top_left(radius); } Message::RadiusTopRightChanged(radius) => { - self.radius = [tl, radius, br, bl]; + self.radius = self.radius.top_right(radius); } Message::RadiusBottomRightChanged(radius) => { - self.radius = [tl, tr, radius, bl]; + self.radius = self.radius.bottom_right(radius); } Message::RadiusBottomLeftChanged(radius) => { - self.radius = [tl, tr, br, radius]; + self.radius = self.radius.bottom_left(radius); } Message::BorderWidthChanged(width) => { self.border_width = width; @@ -151,7 +152,13 @@ impl Example { } fn view(&self) -> Element<Message> { - let [tl, tr, br, bl] = self.radius; + let border::Radius { + top_left, + top_right, + bottom_right, + bottom_left, + } = self.radius; + let Shadow { offset: Vector { x: sx, y: sy }, blur_radius: sr, @@ -165,12 +172,12 @@ impl Example { self.border_width, self.shadow ), - text!("Radius: {tl:.2}/{tr:.2}/{br:.2}/{bl:.2}"), - slider(1.0..=100.0, tl, Message::RadiusTopLeftChanged).step(0.01), - slider(1.0..=100.0, tr, Message::RadiusTopRightChanged).step(0.01), - slider(1.0..=100.0, br, Message::RadiusBottomRightChanged) + text!("Radius: {top_left:.2}/{top_right:.2}/{bottom_right:.2}/{bottom_left:.2}"), + slider(1.0..=100.0, top_left, Message::RadiusTopLeftChanged).step(0.01), + slider(1.0..=100.0, top_right, Message::RadiusTopRightChanged).step(0.01), + slider(1.0..=100.0, bottom_right, Message::RadiusBottomRightChanged) .step(0.01), - slider(1.0..=100.0, bl, Message::RadiusBottomLeftChanged) + slider(1.0..=100.0, bottom_left, Message::RadiusBottomLeftChanged) .step(0.01), slider(1.0..=10.0, self.border_width, Message::BorderWidthChanged) .step(0.01), @@ -185,7 +192,7 @@ impl Example { .padding(20) .spacing(20) .max_width(500) - .align_items(Alignment::Center); + .align_x(Center); center(content).into() } diff --git a/examples/custom_shader/src/main.rs b/examples/custom_shader/src/main.rs index b04a8183..5886f6bb 100644 --- a/examples/custom_shader/src/main.rs +++ b/examples/custom_shader/src/main.rs @@ -6,7 +6,7 @@ use iced::time::Instant; use iced::widget::shader::wgpu; use iced::widget::{center, checkbox, column, row, shader, slider, text}; use iced::window; -use iced::{Alignment, Color, Element, Length, Subscription}; +use iced::{Center, Color, Element, Fill, Subscription}; fn main() -> iced::Result { iced::application( @@ -122,12 +122,11 @@ impl IcedCubes { let controls = column![top_controls, bottom_controls,] .spacing(10) .padding(20) - .align_items(Alignment::Center); + .align_x(Center); - let shader = - shader(&self.scene).width(Length::Fill).height(Length::Fill); + let shader = shader(&self.scene).width(Fill).height(Fill); - center(column![shader, controls].align_items(Alignment::Center)).into() + center(column![shader, controls].align_x(Center)).into() } fn subscription(&self) -> Subscription<Message> { diff --git a/examples/custom_widget/src/main.rs b/examples/custom_widget/src/main.rs index 3cf10e22..58f3c54a 100644 --- a/examples/custom_widget/src/main.rs +++ b/examples/custom_widget/src/main.rs @@ -1,19 +1,11 @@ //! This example showcases a simple native custom widget that draws a circle. mod circle { - // For now, to implement a custom native widget you will need to add - // `iced_native` and `iced_wgpu` to your dependencies. - // - // Then, you simply need to define your widget type and implement the - // `iced_native::Widget` trait with the `iced_wgpu::Renderer`. - // - // Of course, you can choose to make the implementation renderer-agnostic, - // if you wish to, by creating your own `Renderer` trait, which could be - // implemented by `iced_wgpu` and other renderers. use iced::advanced::layout::{self, Layout}; use iced::advanced::renderer; use iced::advanced::widget::{self, Widget}; + use iced::border; use iced::mouse; - use iced::{Border, Color, Element, Length, Rectangle, Size}; + use iced::{Color, Element, Length, Rectangle, Size}; pub struct Circle { radius: f32, @@ -62,7 +54,7 @@ mod circle { renderer.fill_quad( renderer::Quad { bounds: layout.bounds(), - border: Border::rounded(self.radius), + border: border::rounded(self.radius), ..renderer::Quad::default() }, Color::BLACK, @@ -83,7 +75,7 @@ mod circle { use circle::circle; use iced::widget::{center, column, slider, text}; -use iced::{Alignment, Element}; +use iced::{Center, Element}; pub fn main() -> iced::Result { iced::run("Custom Widget - Iced", Example::update, Example::view) @@ -120,7 +112,7 @@ impl Example { .padding(20) .spacing(20) .max_width(500) - .align_items(Alignment::Center); + .align_x(Center); center(content).into() } diff --git a/examples/download_progress/Cargo.toml b/examples/download_progress/Cargo.toml index 18a49f66..61a1b257 100644 --- a/examples/download_progress/Cargo.toml +++ b/examples/download_progress/Cargo.toml @@ -10,6 +10,6 @@ iced.workspace = true iced.features = ["tokio"] [dependencies.reqwest] -version = "0.11" +version = "0.12" default-features = false -features = ["rustls-tls"] +features = ["stream", "rustls-tls"] diff --git a/examples/download_progress/index.html b/examples/download_progress/index.html new file mode 100644 index 00000000..c79e32c1 --- /dev/null +++ b/examples/download_progress/index.html @@ -0,0 +1,12 @@ +<!DOCTYPE html> +<html lang="en" style="height: 100%"> +<head> + <meta charset="utf-8" content="text/html; charset=utf-8" /> + <meta name="viewport" content="width=device-width, initial-scale=1" /> + <title>Download_Progress - Iced</title> + <base data-trunk-public-url /> +</head> +<body style="height: 100%; margin: 0"> +<link data-trunk rel="rust" href="Cargo.toml" data-wasm-opt="z" data-bin="download_progress" /> +</body> +</html> diff --git a/examples/download_progress/src/download.rs b/examples/download_progress/src/download.rs index d6cc1e24..a8e7b404 100644 --- a/examples/download_progress/src/download.rs +++ b/examples/download_progress/src/download.rs @@ -1,85 +1,62 @@ -use iced::subscription; +use iced::futures::{SinkExt, Stream, StreamExt}; +use iced::stream::try_channel; +use iced::Subscription; use std::hash::Hash; +use std::sync::Arc; // Just a little utility function pub fn file<I: 'static + Hash + Copy + Send + Sync, T: ToString>( id: I, url: T, -) -> iced::Subscription<(I, Progress)> { - subscription::unfold(id, State::Ready(url.to_string()), move |state| { - download(id, state) - }) +) -> iced::Subscription<(I, Result<Progress, Error>)> { + Subscription::run_with_id( + id, + download(url.to_string()).map(move |progress| (id, progress)), + ) } -async fn download<I: Copy>(id: I, state: State) -> ((I, Progress), State) { - match state { - State::Ready(url) => { - let response = reqwest::get(&url).await; +fn download(url: String) -> impl Stream<Item = Result<Progress, Error>> { + try_channel(1, move |mut output| async move { + let response = reqwest::get(&url).await?; + let total = response.content_length().ok_or(Error::NoContentLength)?; - match response { - Ok(response) => { - if let Some(total) = response.content_length() { - ( - (id, Progress::Started), - State::Downloading { - response, - total, - downloaded: 0, - }, - ) - } else { - ((id, Progress::Errored), State::Finished) - } - } - Err(_) => ((id, Progress::Errored), State::Finished), - } - } - State::Downloading { - mut response, - total, - downloaded, - } => match response.chunk().await { - Ok(Some(chunk)) => { - let downloaded = downloaded + chunk.len() as u64; + let _ = output.send(Progress::Downloading { percent: 0.0 }).await; + + let mut byte_stream = response.bytes_stream(); + let mut downloaded = 0; - let percentage = (downloaded as f32 / total as f32) * 100.0; + while let Some(next_bytes) = byte_stream.next().await { + let bytes = next_bytes?; + downloaded += bytes.len(); - ( - (id, Progress::Advanced(percentage)), - State::Downloading { - response, - total, - downloaded, - }, - ) - } - Ok(None) => ((id, Progress::Finished), State::Finished), - Err(_) => ((id, Progress::Errored), State::Finished), - }, - State::Finished => { - // We do not let the stream die, as it would start a - // new download repeatedly if the user is not careful - // in case of errors. - iced::futures::future::pending().await + let _ = output + .send(Progress::Downloading { + percent: 100.0 * downloaded as f32 / total as f32, + }) + .await; } - } + + let _ = output.send(Progress::Finished).await; + + Ok(()) + }) } #[derive(Debug, Clone)] pub enum Progress { - Started, - Advanced(f32), + Downloading { percent: f32 }, Finished, - Errored, } -pub enum State { - Ready(String), - Downloading { - response: reqwest::Response, - total: u64, - downloaded: u64, - }, - Finished, +#[derive(Debug, Clone)] +pub enum Error { + RequestFailed(Arc<reqwest::Error>), + NoContentLength, +} + +impl From<reqwest::Error> for Error { + fn from(error: reqwest::Error) -> Self { + Error::RequestFailed(Arc::new(error)) + } } diff --git a/examples/download_progress/src/main.rs b/examples/download_progress/src/main.rs index d91e5eab..bcc01606 100644 --- a/examples/download_progress/src/main.rs +++ b/examples/download_progress/src/main.rs @@ -1,7 +1,7 @@ mod download; use iced::widget::{button, center, column, progress_bar, text, Column}; -use iced::{Alignment, Element, Subscription}; +use iced::{Center, Element, Right, Subscription}; pub fn main() -> iced::Result { iced::application( @@ -23,7 +23,7 @@ struct Example { pub enum Message { Add, Download(usize), - DownloadProgressed((usize, download::Progress)), + DownloadProgressed((usize, Result<download::Progress, download::Error>)), } impl Example { @@ -69,7 +69,7 @@ impl Example { .padding(10), ) .spacing(20) - .align_items(Alignment::End); + .align_x(Right); center(downloads).padding(20).into() } @@ -114,19 +114,19 @@ impl Download { } } - pub fn progress(&mut self, new_progress: download::Progress) { + pub fn progress( + &mut self, + new_progress: Result<download::Progress, download::Error>, + ) { if let State::Downloading { progress } = &mut self.state { match new_progress { - download::Progress::Started => { - *progress = 0.0; + Ok(download::Progress::Downloading { percent }) => { + *progress = percent; } - download::Progress::Advanced(percentage) => { - *progress = percentage; - } - download::Progress::Finished => { + Ok(download::Progress::Finished) => { self.state = State::Finished; } - download::Progress::Errored => { + Err(_error) => { self.state = State::Errored; } } @@ -136,7 +136,7 @@ impl Download { pub fn subscription(&self) -> Subscription<Message> { match self.state { State::Downloading { .. } => { - download::file(self.id, "https://speed.hetzner.de/100MB.bin?") + download::file(self.id, "https://huggingface.co/mattshumer/Reflection-Llama-3.1-70B/resolve/main/model-00001-of-00162.safetensors") .map(Message::DownloadProgressed) } _ => Subscription::none(), @@ -160,7 +160,7 @@ impl Download { State::Finished => { column!["Download finished!", button("Start again")] .spacing(10) - .align_items(Alignment::Center) + .align_x(Center) .into() } State::Downloading { .. } => { @@ -171,14 +171,14 @@ impl Download { button("Try again").on_press(Message::Download(self.id)), ] .spacing(10) - .align_items(Alignment::Center) + .align_x(Center) .into(), }; Column::new() .spacing(10) .padding(10) - .align_items(Alignment::Center) + .align_x(Center) .push(progress_bar) .push(control) .into() diff --git a/examples/editor/src/main.rs b/examples/editor/src/main.rs index bed9d94a..d55f9bdf 100644 --- a/examples/editor/src/main.rs +++ b/examples/editor/src/main.rs @@ -1,10 +1,10 @@ -use iced::highlighter::{self, Highlighter}; +use iced::highlighter; use iced::keyboard; use iced::widget::{ - button, column, container, horizontal_space, pick_list, row, text, - text_editor, tooltip, + self, button, column, container, horizontal_space, pick_list, row, text, + text_editor, toggler, tooltip, }; -use iced::{Alignment, Element, Font, Length, Subscription, Task, Theme}; +use iced::{Center, Element, Fill, Font, Task, Theme}; use std::ffi; use std::io; @@ -13,18 +13,17 @@ use std::sync::Arc; pub fn main() -> iced::Result { iced::application("Editor - Iced", Editor::update, Editor::view) - .load(Editor::load) - .subscription(Editor::subscription) .theme(Editor::theme) .font(include_bytes!("../fonts/icons.ttf").as_slice()) .default_font(Font::MONOSPACE) - .run() + .run_with(Editor::new) } struct Editor { file: Option<PathBuf>, content: text_editor::Content, theme: highlighter::Theme, + word_wrap: bool, is_loading: bool, is_dirty: bool, } @@ -33,6 +32,7 @@ struct Editor { enum Message { ActionPerformed(text_editor::Action), ThemeSelected(highlighter::Theme), + WordWrapToggled(bool), NewFile, OpenFile, FileOpened(Result<(PathBuf, Arc<String>), Error>), @@ -41,20 +41,26 @@ enum Message { } impl Editor { - fn new() -> Self { - Self { - file: None, - content: text_editor::Content::new(), - theme: highlighter::Theme::SolarizedDark, - is_loading: true, - is_dirty: false, - } - } - - fn load() -> Task<Message> { - Task::perform( - load_file(format!("{}/src/main.rs", env!("CARGO_MANIFEST_DIR"))), - Message::FileOpened, + fn new() -> (Self, Task<Message>) { + ( + Self { + file: None, + content: text_editor::Content::new(), + theme: highlighter::Theme::SolarizedDark, + word_wrap: true, + is_loading: true, + is_dirty: false, + }, + Task::batch([ + Task::perform( + load_file(format!( + "{}/src/main.rs", + env!("CARGO_MANIFEST_DIR") + )), + Message::FileOpened, + ), + widget::focus_next(), + ]), ) } @@ -72,6 +78,11 @@ impl Editor { Task::none() } + Message::WordWrapToggled(word_wrap) => { + self.word_wrap = word_wrap; + + Task::none() + } Message::NewFile => { if !self.is_loading { self.file = None; @@ -125,15 +136,6 @@ impl Editor { } } - fn subscription(&self) -> Subscription<Message> { - keyboard::on_key_press(|key, modifiers| match key.as_ref() { - keyboard::Key::Character("s") if modifiers.command() => { - Some(Message::SaveFile) - } - _ => None, - }) - } - fn view(&self) -> Element<Message> { let controls = row![ action(new_icon(), "New file", Some(Message::NewFile)), @@ -148,6 +150,9 @@ impl Editor { self.is_dirty.then_some(Message::SaveFile) ), horizontal_space(), + toggler(self.word_wrap) + .label("Word Wrap") + .on_toggle(Message::WordWrapToggled), pick_list( highlighter::Theme::ALL, Some(self.theme), @@ -157,7 +162,7 @@ impl Editor { .padding([5, 10]) ] .spacing(10) - .align_items(Alignment::Center); + .align_y(Center); let status = row![ text(if let Some(path) = &self.file { @@ -183,21 +188,33 @@ impl Editor { column![ controls, text_editor(&self.content) - .height(Length::Fill) + .height(Fill) .on_action(Message::ActionPerformed) - .highlight::<Highlighter>( - highlighter::Settings { - theme: self.theme, - extension: self - .file - .as_deref() - .and_then(Path::extension) - .and_then(ffi::OsStr::to_str) - .map(str::to_string) - .unwrap_or(String::from("rs")), - }, - |highlight, _theme| highlight.to_format() - ), + .wrapping(if self.word_wrap { + text::Wrapping::Word + } else { + text::Wrapping::None + }) + .highlight( + self.file + .as_deref() + .and_then(Path::extension) + .and_then(ffi::OsStr::to_str) + .unwrap_or("rs"), + self.theme, + ) + .key_binding(|key_press| { + match key_press.key.as_ref() { + keyboard::Key::Character("s") + if key_press.modifiers.command() => + { + Some(text_editor::Binding::Custom( + Message::SaveFile, + )) + } + _ => text_editor::Binding::from_key_press(key_press), + } + }), status, ] .spacing(10) @@ -214,12 +231,6 @@ impl Editor { } } -impl Default for Editor { - fn default() -> Self { - Self::new() - } -} - #[derive(Debug, Clone)] pub enum Error { DialogClosed, diff --git a/examples/events/src/main.rs b/examples/events/src/main.rs index 2cd3c5d8..5bada9b5 100644 --- a/examples/events/src/main.rs +++ b/examples/events/src/main.rs @@ -1,8 +1,7 @@ -use iced::alignment; use iced::event::{self, Event}; use iced::widget::{button, center, checkbox, text, Column}; use iced::window; -use iced::{Alignment, Element, Length, Subscription, Task}; +use iced::{Center, Element, Fill, Subscription, Task}; pub fn main() -> iced::Result { iced::application("Events - Iced", Events::update, Events::view) @@ -67,17 +66,13 @@ impl Events { let toggle = checkbox("Listen to runtime events", self.enabled) .on_toggle(Message::Toggled); - let exit = button( - text("Exit") - .width(Length::Fill) - .horizontal_alignment(alignment::Horizontal::Center), - ) - .width(100) - .padding(10) - .on_press(Message::Exit); + let exit = button(text("Exit").width(Fill).align_x(Center)) + .width(100) + .padding(10) + .on_press(Message::Exit); let content = Column::new() - .align_items(Alignment::Center) + .align_x(Center) .spacing(20) .push(events) .push(toggle) diff --git a/examples/exit/src/main.rs b/examples/exit/src/main.rs index 1f108df2..48b0864c 100644 --- a/examples/exit/src/main.rs +++ b/examples/exit/src/main.rs @@ -1,6 +1,6 @@ use iced::widget::{button, center, column}; use iced::window; -use iced::{Alignment, Element, Task}; +use iced::{Center, Element, Task}; pub fn main() -> iced::Result { iced::application("Exit - Iced", Exit::update, Exit::view).run() @@ -44,7 +44,7 @@ impl Exit { ] } .spacing(10) - .align_items(Alignment::Center); + .align_x(Center); center(content).padding(20).into() } diff --git a/examples/ferris/src/main.rs b/examples/ferris/src/main.rs index 88006898..eaf51354 100644 --- a/examples/ferris/src/main.rs +++ b/examples/ferris/src/main.rs @@ -4,8 +4,8 @@ use iced::widget::{ }; use iced::window; use iced::{ - Alignment, Color, ContentFit, Degrees, Element, Length, Radians, Rotation, - Subscription, Theme, + Bottom, Center, Color, ContentFit, Degrees, Element, Fill, Radians, + Rotation, Subscription, Theme, }; pub fn main() -> iced::Result { @@ -108,7 +108,7 @@ impl Image { "I am Ferris!" ] .spacing(20) - .align_items(Alignment::Center); + .align_x(Center); let fit = row![ pick_list( @@ -122,7 +122,7 @@ impl Image { Some(self.content_fit), Message::ContentFitChanged ) - .width(Length::Fill), + .width(Fill), pick_list( [RotationStrategy::Floating, RotationStrategy::Solid], Some(match self.rotation { @@ -131,10 +131,10 @@ impl Image { }), Message::RotationStrategyChanged, ) - .width(Length::Fill), + .width(Fill), ] .spacing(10) - .align_items(Alignment::End); + .align_y(Bottom); let properties = row![ with_value( @@ -159,12 +159,12 @@ impl Image { .size(12) ] .spacing(10) - .align_items(Alignment::Center), + .align_y(Center), format!("Rotation: {:.0}°", f32::from(self.rotation.degrees())) ) ] .spacing(10) - .align_items(Alignment::End); + .align_y(Bottom); container(column![fit, center(i_am_ferris), properties].spacing(10)) .padding(10) @@ -206,6 +206,6 @@ fn with_value<'a>( ) -> Element<'a, Message> { column![control.into(), text(value).size(12).line_height(1.0)] .spacing(2) - .align_items(Alignment::Center) + .align_x(Center) .into() } diff --git a/examples/game_of_life/src/main.rs b/examples/game_of_life/src/main.rs index 421f862a..9dcebecc 100644 --- a/examples/game_of_life/src/main.rs +++ b/examples/game_of_life/src/main.rs @@ -9,7 +9,7 @@ use iced::time; use iced::widget::{ button, checkbox, column, container, pick_list, row, slider, text, }; -use iced::{Alignment, Element, Length, Subscription, Task, Theme}; +use iced::{Center, Element, Fill, Subscription, Task, Theme}; use std::time::Duration; pub fn main() -> iced::Result { @@ -135,12 +135,9 @@ impl GameOfLife { .map(move |message| Message::Grid(message, version)), controls, ] - .height(Length::Fill); + .height(Fill); - container(content) - .width(Length::Fill) - .height(Length::Fill) - .into() + container(content).width(Fill).height(Fill).into() } } @@ -169,7 +166,7 @@ fn view_controls<'a>( slider(1.0..=1000.0, speed as f32, Message::SpeedChanged), text!("x{speed}").size(16), ] - .align_items(Alignment::Center) + .align_y(Center) .spacing(10); row![ @@ -186,7 +183,7 @@ fn view_controls<'a>( ] .padding(10) .spacing(20) - .align_items(Alignment::Center) + .align_y(Center) .into() } @@ -199,7 +196,7 @@ mod grid { use iced::widget::canvas::event::{self, Event}; use iced::widget::canvas::{Cache, Canvas, Frame, Geometry, Path, Text}; use iced::{ - Color, Element, Length, Point, Rectangle, Renderer, Size, Theme, Vector, + Color, Element, Fill, Point, Rectangle, Renderer, Size, Theme, Vector, }; use rustc_hash::{FxHashMap, FxHashSet}; use std::future::Future; @@ -333,10 +330,7 @@ mod grid { } pub fn view(&self) -> Element<Message> { - Canvas::new(self) - .width(Length::Fill) - .height(Length::Fill) - .into() + Canvas::new(self).width(Fill).height(Fill).into() } pub fn clear(&mut self) { diff --git a/examples/gradient/src/main.rs b/examples/gradient/src/main.rs index e5b19443..b2de069f 100644 --- a/examples/gradient/src/main.rs +++ b/examples/gradient/src/main.rs @@ -3,7 +3,7 @@ use iced::gradient; use iced::widget::{ checkbox, column, container, horizontal_space, row, slider, text, }; -use iced::{Alignment, Color, Element, Length, Radians, Theme}; +use iced::{Center, Color, Element, Fill, Radians, Theme}; pub fn main() -> iced::Result { tracing_subscriber::fmt::init(); @@ -67,8 +67,8 @@ impl Gradient { gradient.into() }) - .width(Length::Fill) - .height(Length::Fill); + .width(Fill) + .height(Fill); let angle_picker = row![ text("Angle").width(64), @@ -77,7 +77,7 @@ impl Gradient { ] .spacing(8) .padding(8) - .align_items(Alignment::Center); + .align_y(Center); let transparency_toggle = iced::widget::Container::new( checkbox("Transparent window", transparent) @@ -129,6 +129,6 @@ fn color_picker(label: &str, color: Color) -> Element<'_, Color> { ] .spacing(8) .padding(8) - .align_items(Alignment::Center) + .align_y(Center) .into() } diff --git a/examples/integration/src/controls.rs b/examples/integration/src/controls.rs index d0654996..0b11a323 100644 --- a/examples/integration/src/controls.rs +++ b/examples/integration/src/controls.rs @@ -1,7 +1,6 @@ use iced_wgpu::Renderer; use iced_widget::{column, container, row, slider, text, text_input}; -use iced_winit::core::alignment; -use iced_winit::core::{Color, Element, Length, Theme}; +use iced_winit::core::{Color, Element, Length::*, Theme}; use iced_winit::runtime::{Program, Task}; pub struct Controls { @@ -86,8 +85,7 @@ impl Program for Controls { .spacing(10), ) .padding(10) - .height(Length::Fill) - .align_y(alignment::Vertical::Bottom) + .align_bottom(Fill) .into() } } diff --git a/examples/integration/src/main.rs b/examples/integration/src/main.rs index 9818adf3..5b64cbd1 100644 --- a/examples/integration/src/main.rs +++ b/examples/integration/src/main.rs @@ -68,7 +68,7 @@ pub fn main() -> Result<(), winit::error::EventLoopError> { Size::new(physical_size.width, physical_size.height), window.scale_factor(), ); - let clipboard = Clipboard::connect(&window); + let clipboard = Clipboard::connect(window.clone()); let backend = wgpu::util::backend_bits_from_env().unwrap_or_default(); diff --git a/examples/layout/src/main.rs b/examples/layout/src/main.rs index 2e774415..cb33369b 100644 --- a/examples/layout/src/main.rs +++ b/examples/layout/src/main.rs @@ -1,3 +1,4 @@ +use iced::border; use iced::keyboard; use iced::mouse; use iced::widget::{ @@ -5,7 +6,7 @@ use iced::widget::{ pick_list, row, scrollable, text, }; use iced::{ - color, Alignment, Element, Font, Length, Point, Rectangle, Renderer, + color, Center, Element, Fill, Font, Length, Point, Rectangle, Renderer, Subscription, Theme, }; @@ -74,7 +75,7 @@ impl Layout { pick_list(Theme::ALL, Some(&self.theme), Message::ThemeSelected), ] .spacing(20) - .align_items(Alignment::Center); + .align_y(Center); let example = center(if self.explain { self.example.view().explain(color!(0x0000ff)) @@ -85,7 +86,7 @@ impl Layout { let palette = theme.extended_palette(); container::Style::default() - .with_border(palette.background.strong.color, 4.0) + .border(border::color(palette.background.strong.color).width(4)) }) .padding(4); @@ -234,13 +235,13 @@ fn application<'a>() -> Element<'a, Message> { square(40), ] .padding(10) - .align_items(Alignment::Center), + .align_y(Center), ) .style(|theme| { let palette = theme.extended_palette(); container::Style::default() - .with_border(palette.background.strong.color, 1) + .border(border::color(palette.background.strong.color).width(1)) }); let sidebar = container( @@ -248,25 +249,26 @@ fn application<'a>() -> Element<'a, Message> { .spacing(40) .padding(10) .width(200) - .align_items(Alignment::Center), + .align_x(Center), ) .style(container::rounded_box) - .center_y(Length::Fill); + .center_y(Fill); let content = container( scrollable( column![ "Content!", - square(400), - square(200), - square(400), + row((1..10).map(|i| square(if i % 2 == 0 { 80 } else { 160 }))) + .spacing(20) + .align_y(Center) + .wrap(), "The end" ] .spacing(40) - .align_items(Alignment::Center) - .width(Length::Fill), + .align_x(Center) + .width(Fill), ) - .height(Length::Fill), + .height(Fill), ) .padding(10); diff --git a/examples/lazy/src/main.rs b/examples/lazy/src/main.rs index f24c0d62..8f756210 100644 --- a/examples/lazy/src/main.rs +++ b/examples/lazy/src/main.rs @@ -2,7 +2,7 @@ use iced::widget::{ button, column, horizontal_space, lazy, pick_list, row, scrollable, text, text_input, }; -use iced::{Element, Length}; +use iced::{Element, Fill}; use std::collections::HashSet; use std::hash::Hash; @@ -187,7 +187,7 @@ impl App { }); column![ - scrollable(options).height(Length::Fill), + scrollable(options).height(Fill), row![ text_input("Add a new option", &self.input) .on_input(Message::InputChanged) diff --git a/examples/loading_spinners/src/main.rs b/examples/loading_spinners/src/main.rs index 503f2d7a..3b178148 100644 --- a/examples/loading_spinners/src/main.rs +++ b/examples/loading_spinners/src/main.rs @@ -1,5 +1,5 @@ use iced::widget::{center, column, row, slider, text}; -use iced::Element; +use iced::{Center, Element}; use std::time::Duration; @@ -67,7 +67,7 @@ impl LoadingSpinners { Duration::from_secs_f32(self.cycle_duration) ) ] - .align_items(iced::Alignment::Center) + .align_y(Center) .spacing(20.0), ) }) @@ -83,7 +83,7 @@ impl LoadingSpinners { .width(200.0), text!("{:.2}s", self.cycle_duration), ] - .align_items(iced::Alignment::Center) + .align_y(Center) .spacing(20.0), ), ) diff --git a/examples/loupe/src/main.rs b/examples/loupe/src/main.rs index c4d3b449..1c748d42 100644 --- a/examples/loupe/src/main.rs +++ b/examples/loupe/src/main.rs @@ -1,5 +1,5 @@ use iced::widget::{button, center, column, text}; -use iced::{Alignment, Element}; +use iced::{Center, Element}; use loupe::loupe; @@ -39,7 +39,7 @@ impl Loupe { button("Decrement").on_press(Message::Decrement) ] .padding(20) - .align_items(Alignment::Center), + .align_x(Center), )) .into() } diff --git a/examples/component/Cargo.toml b/examples/markdown/Cargo.toml index 83b7b8a4..cb74b954 100644 --- a/examples/component/Cargo.toml +++ b/examples/markdown/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "component" +name = "markdown" version = "0.1.0" authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"] edition = "2021" @@ -7,4 +7,6 @@ publish = false [dependencies] iced.workspace = true -iced.features = ["debug", "lazy"] +iced.features = ["markdown", "highlighter", "debug"] + +open = "5.3" diff --git a/examples/markdown/overview.md b/examples/markdown/overview.md new file mode 100644 index 00000000..66336c5b --- /dev/null +++ b/examples/markdown/overview.md @@ -0,0 +1,93 @@ +# Overview + +Inspired by [The Elm Architecture], Iced expects you to split user interfaces into four different concepts: + +* __State__ — the state of your application +* __Messages__ — user interactions or meaningful events that you care about +* __View logic__ — a way to display your __state__ as widgets that may produce __messages__ on user interaction +* __Update logic__ — a way to react to __messages__ and update your __state__ + +We can build something to see how this works! Let's say we want a simple counter that can be incremented and decremented using two buttons. + +We start by modelling the __state__ of our application: + +```rust +#[derive(Default)] +struct Counter { + value: i32, +} +``` + +Next, we need to define the possible user interactions of our counter: the button presses. These interactions are our __messages__: + +```rust +#[derive(Debug, Clone, Copy)] +pub enum Message { + Increment, + Decrement, +} +``` + +Now, let's show the actual counter by putting it all together in our __view logic__: + +```rust +use iced::widget::{button, column, text, Column}; + +impl Counter { + pub fn view(&self) -> Column<Message> { + // We use a column: a simple vertical layout + column![ + // The increment button. We tell it to produce an + // `Increment` message when pressed + button("+").on_press(Message::Increment), + + // We show the value of the counter here + text(self.value).size(50), + + // The decrement button. We tell it to produce a + // `Decrement` message when pressed + button("-").on_press(Message::Decrement), + ] + } +} +``` + +Finally, we need to be able to react to any produced __messages__ and change our __state__ accordingly in our __update logic__: + +```rust +impl Counter { + // ... + + pub fn update(&mut self, message: Message) { + match message { + Message::Increment => { + self.value += 1; + } + Message::Decrement => { + self.value -= 1; + } + } + } +} +``` + +And that's everything! We just wrote a whole user interface. Let's run it: + +```rust +fn main() -> iced::Result { + iced::run("A cool counter", Counter::update, Counter::view) +} +``` + +Iced will automatically: + + 1. Take the result of our __view logic__ and layout its widgets. + 1. Process events from our system and produce __messages__ for our __update logic__. + 1. Draw the resulting user interface. + +Read the [book], the [documentation], and the [examples] to learn more! + +[book]: https://book.iced.rs/ +[documentation]: https://docs.rs/iced/ +[examples]: https://github.com/iced-rs/iced/tree/master/examples#examples +[The Elm Architecture]: https://guide.elm-lang.org/architecture/ diff --git a/examples/markdown/src/main.rs b/examples/markdown/src/main.rs new file mode 100644 index 00000000..5605478f --- /dev/null +++ b/examples/markdown/src/main.rs @@ -0,0 +1,82 @@ +use iced::highlighter; +use iced::widget::{self, markdown, row, scrollable, text_editor}; +use iced::{Element, Fill, Font, Task, Theme}; + +pub fn main() -> iced::Result { + iced::application("Markdown - Iced", Markdown::update, Markdown::view) + .theme(Markdown::theme) + .run_with(Markdown::new) +} + +struct Markdown { + content: text_editor::Content, + items: Vec<markdown::Item>, + theme: Theme, +} + +#[derive(Debug, Clone)] +enum Message { + Edit(text_editor::Action), + LinkClicked(markdown::Url), +} + +impl Markdown { + fn new() -> (Self, Task<Message>) { + const INITIAL_CONTENT: &str = include_str!("../overview.md"); + + let theme = Theme::TokyoNight; + + ( + Self { + content: text_editor::Content::with_text(INITIAL_CONTENT), + items: markdown::parse(INITIAL_CONTENT).collect(), + theme, + }, + widget::focus_next(), + ) + } + + fn update(&mut self, message: Message) { + match message { + Message::Edit(action) => { + let is_edit = action.is_edit(); + + self.content.perform(action); + + if is_edit { + self.items = + markdown::parse(&self.content.text()).collect(); + } + } + Message::LinkClicked(link) => { + let _ = open::that_in_background(link.to_string()); + } + } + } + + fn view(&self) -> Element<Message> { + let editor = text_editor(&self.content) + .placeholder("Type your Markdown here...") + .on_action(Message::Edit) + .height(Fill) + .padding(10) + .font(Font::MONOSPACE) + .highlight("markdown", highlighter::Theme::Base16Ocean); + + let preview = markdown( + &self.items, + markdown::Settings::default(), + markdown::Style::from_palette(self.theme.palette()), + ) + .map(Message::LinkClicked); + + row![editor, scrollable(preview).spacing(10).height(Fill)] + .spacing(10) + .padding(10) + .into() + } + + fn theme(&self) -> Theme { + self.theme.clone() + } +} diff --git a/examples/modal/src/main.rs b/examples/modal/src/main.rs index 413485e7..067ca24d 100644 --- a/examples/modal/src/main.rs +++ b/examples/modal/src/main.rs @@ -5,7 +5,7 @@ use iced::widget::{ self, button, center, column, container, horizontal_space, mouse_area, opaque, pick_list, row, stack, text, text_input, }; -use iced::{Alignment, Color, Element, Length, Subscription, Task}; +use iced::{Bottom, Color, Element, Fill, Subscription, Task}; use std::fmt; @@ -96,18 +96,17 @@ impl App { let content = container( column![ row![text("Top Left"), horizontal_space(), text("Top Right")] - .align_items(Alignment::Start) - .height(Length::Fill), + .height(Fill), center(button(text("Show Modal")).on_press(Message::ShowModal)), row![ text("Bottom Left"), horizontal_space(), text("Bottom Right") ] - .align_items(Alignment::End) - .height(Length::Fill), + .align_y(Bottom) + .height(Fill), ] - .height(Length::Fill), + .height(Fill), ) .padding(10); @@ -202,19 +201,21 @@ where { stack![ base.into(), - mouse_area(center(opaque(content)).style(|_theme| { - container::Style { - background: Some( - Color { - a: 0.8, - ..Color::BLACK - } - .into(), - ), - ..container::Style::default() - } - })) - .on_press(on_blur) + opaque( + mouse_area(center(opaque(content)).style(|_theme| { + container::Style { + background: Some( + Color { + a: 0.8, + ..Color::BLACK + } + .into(), + ), + ..container::Style::default() + } + })) + .on_press(on_blur) + ) ] .into() } diff --git a/examples/multi_window/src/main.rs b/examples/multi_window/src/main.rs index 98e753ab..ab09116e 100644 --- a/examples/multi_window/src/main.rs +++ b/examples/multi_window/src/main.rs @@ -3,22 +3,18 @@ use iced::widget::{ text_input, }; use iced::window; -use iced::{Alignment, Element, Length, Subscription, Task, Theme, Vector}; +use iced::{Center, Element, Fill, Subscription, Task, Theme, Vector}; use std::collections::BTreeMap; fn main() -> iced::Result { iced::daemon(Example::title, Example::update, Example::view) - .load(|| { - window::open(window::Settings::default()).map(Message::WindowOpened) - }) .subscription(Example::subscription) .theme(Example::theme) .scale_factor(Example::scale_factor) - .run() + .run_with(Example::new) } -#[derive(Default)] struct Example { windows: BTreeMap<window::Id, Window>, } @@ -43,6 +39,17 @@ enum Message { } impl Example { + fn new() -> (Self, Task<Message>) { + let (_id, open) = window::open(window::Settings::default()); + + ( + Self { + windows: BTreeMap::new(), + }, + open.map(Message::WindowOpened), + ) + } + fn title(&self, window: window::Id) -> String { self.windows .get(&window) @@ -68,10 +75,12 @@ impl Example { }, ); - window::open(window::Settings { + let (_id, open) = window::open(window::Settings { position, ..window::Settings::default() - }) + }); + + open }) .map(Message::WindowOpened) } @@ -182,8 +191,8 @@ impl Window { let content = scrollable( column![scale_input, title_input, new_window_button] .spacing(50) - .width(Length::Fill) - .align_items(Alignment::Center), + .width(Fill) + .align_x(Center), ); container(content).center_x(200).into() diff --git a/examples/multitouch/src/main.rs b/examples/multitouch/src/main.rs index 69717310..a0105a8a 100644 --- a/examples/multitouch/src/main.rs +++ b/examples/multitouch/src/main.rs @@ -6,7 +6,7 @@ use iced::touch; use iced::widget::canvas::event; use iced::widget::canvas::stroke::{self, Stroke}; use iced::widget::canvas::{self, Canvas, Geometry}; -use iced::{Color, Element, Length, Point, Rectangle, Renderer, Theme}; +use iced::{Color, Element, Fill, Point, Rectangle, Renderer, Theme}; use std::collections::HashMap; @@ -46,10 +46,7 @@ impl Multitouch { } fn view(&self) -> Element<Message> { - Canvas::new(self) - .width(Length::Fill) - .height(Length::Fill) - .into() + Canvas::new(self).width(Fill).height(Fill).into() } } diff --git a/examples/pane_grid/src/main.rs b/examples/pane_grid/src/main.rs index db9f7a05..67f4d27f 100644 --- a/examples/pane_grid/src/main.rs +++ b/examples/pane_grid/src/main.rs @@ -1,10 +1,9 @@ -use iced::alignment::{self, Alignment}; use iced::keyboard; use iced::widget::pane_grid::{self, PaneGrid}; use iced::widget::{ button, column, container, responsive, row, scrollable, text, }; -use iced::{Color, Element, Length, Size, Subscription}; +use iced::{Center, Color, Element, Fill, Size, Subscription}; pub fn main() -> iced::Result { iced::application("Pane Grid - Iced", Example::update, Example::view) @@ -155,11 +154,23 @@ impl Example { .spacing(5); let title_bar = pane_grid::TitleBar::new(title) - .controls(view_controls( - id, - total_panes, - pane.is_pinned, - is_maximized, + .controls(pane_grid::Controls::dynamic( + view_controls( + id, + total_panes, + pane.is_pinned, + is_maximized, + ), + button(text("X").size(14)) + .style(button::danger) + .padding(3) + .on_press_maybe( + if total_panes > 1 && !pane.is_pinned { + Some(Message::Close(id)) + } else { + None + }, + ), )) .padding(10) .style(if is_focused { @@ -178,16 +189,16 @@ impl Example { style::pane_active }) }) - .width(Length::Fill) - .height(Length::Fill) + .width(Fill) + .height(Fill) .spacing(10) .on_click(Message::Clicked) .on_drag(Message::Dragged) .on_resize(10, Message::Resized); container(pane_grid) - .width(Length::Fill) - .height(Length::Fill) + .width(Fill) + .height(Fill) .padding(10) .into() } @@ -255,15 +266,10 @@ fn view_content<'a>( size: Size, ) -> Element<'a, Message> { let button = |label, message| { - button( - text(label) - .width(Length::Fill) - .horizontal_alignment(alignment::Horizontal::Center) - .size(16), - ) - .width(Length::Fill) - .padding(8) - .on_press(message) + button(text(label).width(Fill).align_x(Center).size(16)) + .width(Fill) + .padding(8) + .on_press(message) }; let controls = column![ @@ -287,10 +293,10 @@ fn view_content<'a>( let content = column![text!("{}x{}", size.width, size.height).size(24), controls,] .spacing(10) - .align_items(Alignment::Center); + .align_x(Center); container(scrollable(content)) - .center_y(Length::Fill) + .center_y(Fill) .padding(5) .into() } diff --git a/examples/pick_list/src/main.rs b/examples/pick_list/src/main.rs index 2be6f5b0..d8b2b389 100644 --- a/examples/pick_list/src/main.rs +++ b/examples/pick_list/src/main.rs @@ -1,5 +1,5 @@ use iced::widget::{column, pick_list, scrollable, vertical_space}; -use iced::{Alignment, Element, Length}; +use iced::{Center, Element, Fill}; pub fn main() -> iced::Result { iced::run("Pick List - Iced", Example::update, Example::view) @@ -38,8 +38,8 @@ impl Example { pick_list, vertical_space().height(600), ] - .width(Length::Fill) - .align_items(Alignment::Center) + .width(Fill) + .align_x(Center) .spacing(10); scrollable(content).into() diff --git a/examples/pokedex/Cargo.toml b/examples/pokedex/Cargo.toml index bf7e1e35..1a6d5445 100644 --- a/examples/pokedex/Cargo.toml +++ b/examples/pokedex/Cargo.toml @@ -16,7 +16,7 @@ version = "1.0" features = ["derive"] [dependencies.reqwest] -version = "0.11" +version = "0.12" default-features = false features = ["json", "rustls-tls"] diff --git a/examples/pokedex/src/main.rs b/examples/pokedex/src/main.rs index b22ffe7f..2e972f6b 100644 --- a/examples/pokedex/src/main.rs +++ b/examples/pokedex/src/main.rs @@ -1,20 +1,16 @@ use iced::futures; use iced::widget::{self, center, column, image, row, text}; -use iced::{Alignment, Element, Length, Task}; +use iced::{Center, Element, Fill, Right, Task}; pub fn main() -> iced::Result { iced::application(Pokedex::title, Pokedex::update, Pokedex::view) - .load(Pokedex::search) - .run() + .run_with(Pokedex::new) } -#[derive(Debug, Default)] +#[derive(Debug)] enum Pokedex { - #[default] Loading, - Loaded { - pokemon: Pokemon, - }, + Loaded { pokemon: Pokemon }, Errored, } @@ -25,6 +21,10 @@ enum Message { } impl Pokedex { + fn new() -> (Self, Task<Message>) { + (Self::Loading, Self::search()) + } + fn search() -> Task<Message> { Task::perform(Pokemon::search(), Message::PokemonFound) } @@ -63,10 +63,9 @@ impl Pokedex { } fn view(&self) -> Element<Message> { - let content = match self { + let content: Element<_> = match self { Pokedex::Loading => { - column![text("Searching for Pokémon...").size(40),] - .width(Length::Shrink) + text("Searching for Pokémon...").size(40).into() } Pokedex::Loaded { pokemon } => column![ pokemon.view(), @@ -74,13 +73,15 @@ impl Pokedex { ] .max_width(500) .spacing(20) - .align_items(Alignment::End), + .align_x(Right) + .into(), Pokedex::Errored => column![ text("Whoops! Something went wrong...").size(40), button("Try again").on_press(Message::Search) ] .spacing(20) - .align_items(Alignment::End), + .align_x(Right) + .into(), }; center(content).into() @@ -103,17 +104,17 @@ impl Pokemon { image::viewer(self.image.clone()), column![ row![ - text(&self.name).size(30).width(Length::Fill), + text(&self.name).size(30).width(Fill), text!("#{}", self.number).size(20).color([0.5, 0.5, 0.5]), ] - .align_items(Alignment::Center) + .align_y(Center) .spacing(20), self.description.as_ref(), ] .spacing(20), ] .spacing(20) - .align_items(Alignment::Center) + .align_y(Center) .into() } diff --git a/examples/qr_code/src/main.rs b/examples/qr_code/src/main.rs index b30ecf15..f1b654e0 100644 --- a/examples/qr_code/src/main.rs +++ b/examples/qr_code/src/main.rs @@ -1,5 +1,5 @@ use iced::widget::{center, column, pick_list, qr_code, row, text, text_input}; -use iced::{Alignment, Element, Theme}; +use iced::{Center, Element, Theme}; pub fn main() -> iced::Result { iced::application( @@ -58,7 +58,7 @@ impl QRGenerator { pick_list(Theme::ALL, Some(&self.theme), Message::ThemeChanged,) ] .spacing(10) - .align_items(Alignment::Center); + .align_y(Center); let content = column![title, input, choose_theme] .push_maybe( @@ -68,7 +68,7 @@ impl QRGenerator { ) .width(700) .spacing(20) - .align_items(Alignment::Center); + .align_x(Center); center(content).padding(20).into() } diff --git a/examples/screenshot/src/main.rs b/examples/screenshot/src/main.rs index acde8367..5c105f6c 100644 --- a/examples/screenshot/src/main.rs +++ b/examples/screenshot/src/main.rs @@ -1,10 +1,10 @@ -use iced::alignment; use iced::keyboard; use iced::widget::{button, column, container, image, row, text, text_input}; use iced::window; use iced::window::screenshot::{self, Screenshot}; use iced::{ - Alignment, ContentFit, Element, Length, Rectangle, Subscription, Task, + Center, ContentFit, Element, Fill, FillPortion, Rectangle, Subscription, + Task, }; use ::image as img; @@ -20,7 +20,7 @@ fn main() -> iced::Result { #[derive(Default)] struct Example { - screenshot: Option<Screenshot>, + screenshot: Option<(Screenshot, image::Handle)>, saved_png_path: Option<Result<String, PngError>>, png_saving: bool, crop_error: Option<screenshot::CropError>, @@ -52,10 +52,17 @@ impl Example { .map(Message::Screenshotted); } Message::Screenshotted(screenshot) => { - self.screenshot = Some(screenshot); + self.screenshot = Some(( + screenshot.clone(), + image::Handle::from_rgba( + screenshot.size.width, + screenshot.size.height, + screenshot.bytes, + ), + )); } Message::Png => { - if let Some(screenshot) = &self.screenshot { + if let Some((screenshot, _handle)) = &self.screenshot { self.png_saving = true; return Task::perform( @@ -81,7 +88,7 @@ impl Example { self.height_input_value = new_value; } Message::Crop => { - if let Some(screenshot) = &self.screenshot { + if let Some((screenshot, _handle)) = &self.screenshot { let cropped = screenshot.crop(Rectangle::<u32> { x: self.x_input_value.unwrap_or(0), y: self.y_input_value.unwrap_or(0), @@ -91,7 +98,14 @@ impl Example { match cropped { Ok(screenshot) => { - self.screenshot = Some(screenshot); + self.screenshot = Some(( + screenshot.clone(), + image::Handle::from_rgba( + screenshot.size.width, + screenshot.size.height, + screenshot.bytes, + ), + )); self.crop_error = None; } Err(crop_error) => { @@ -106,53 +120,41 @@ impl Example { } fn view(&self) -> Element<'_, Message> { - let image: Element<Message> = if let Some(screenshot) = &self.screenshot - { - image(image::Handle::from_rgba( - screenshot.size.width, - screenshot.size.height, - screenshot.clone(), - )) - .content_fit(ContentFit::Contain) - .width(Length::Fill) - .height(Length::Fill) - .into() - } else { - text("Press the button to take a screenshot!").into() - }; + let image: Element<Message> = + if let Some((_screenshot, handle)) = &self.screenshot { + image(handle) + .content_fit(ContentFit::Contain) + .width(Fill) + .height(Fill) + .into() + } else { + text("Press the button to take a screenshot!").into() + }; let image = container(image) - .center_y(Length::FillPortion(2)) + .center_y(FillPortion(2)) .padding(10) .style(container::rounded_box); let crop_origin_controls = row![ - text("X:") - .vertical_alignment(alignment::Vertical::Center) - .width(30), + text("X:").width(30), numeric_input("0", self.x_input_value).map(Message::XInputChanged), - text("Y:") - .vertical_alignment(alignment::Vertical::Center) - .width(30), + text("Y:").width(30), numeric_input("0", self.y_input_value).map(Message::YInputChanged) ] .spacing(10) - .align_items(Alignment::Center); + .align_y(Center); let crop_dimension_controls = row![ - text("W:") - .vertical_alignment(alignment::Vertical::Center) - .width(30), + text("W:").width(30), numeric_input("0", self.width_input_value) .map(Message::WidthInputChanged), - text("H:") - .vertical_alignment(alignment::Vertical::Center) - .width(30), + text("H:").width(30), numeric_input("0", self.height_input_value) .map(Message::HeightInputChanged) ] .spacing(10) - .align_items(Alignment::Center); + .align_y(Center); let crop_controls = column![crop_origin_controls, crop_dimension_controls] @@ -162,7 +164,7 @@ impl Example { .map(|error| text!("Crop error! \n{error}")), ) .spacing(10) - .align_items(Alignment::Center); + .align_x(Center); let controls = { let save_result = @@ -178,8 +180,8 @@ impl Example { column![ column![ button(centered_text("Screenshot!")) - .padding([10, 20, 10, 20]) - .width(Length::Fill) + .padding([10, 20]) + .width(Fill) .on_press(Message::Screenshot), if !self.png_saving { button(centered_text("Save as png")).on_press_maybe( @@ -190,8 +192,8 @@ impl Example { .style(button::secondary) } .style(button::secondary) - .padding([10, 20, 10, 20]) - .width(Length::Fill) + .padding([10, 20]) + .width(Fill) ] .spacing(10), column![ @@ -199,23 +201,23 @@ impl Example { button(centered_text("Crop")) .on_press(Message::Crop) .style(button::danger) - .padding([10, 20, 10, 20]) - .width(Length::Fill), + .padding([10, 20]) + .width(Fill), ] .spacing(10) - .align_items(Alignment::Center), + .align_x(Center), ] .push_maybe(save_result.map(text)) .spacing(40) }; - let side_content = container(controls).center_y(Length::Fill); + let side_content = container(controls).center_y(Fill); let content = row![side_content, image] .spacing(10) - .width(Length::Fill) - .height(Length::Fill) - .align_items(Alignment::Center); + .width(Fill) + .height(Fill) + .align_y(Center); container(content).padding(10).into() } @@ -276,8 +278,5 @@ fn numeric_input( } fn centered_text(content: &str) -> Element<'_, Message> { - text(content) - .width(Length::Fill) - .horizontal_alignment(alignment::Horizontal::Center) - .into() + text(content).width(Fill).align_x(Center).into() } diff --git a/examples/scrollable/src/main.rs b/examples/scrollable/src/main.rs index f2a853e1..de4f2f9a 100644 --- a/examples/scrollable/src/main.rs +++ b/examples/scrollable/src/main.rs @@ -1,9 +1,8 @@ -use iced::widget::scrollable::Properties; use iced::widget::{ button, column, container, horizontal_space, progress_bar, radio, row, - scrollable, slider, text, vertical_space, Scrollable, + scrollable, slider, text, vertical_space, }; -use iced::{Alignment, Border, Color, Element, Length, Task, Theme}; +use iced::{Border, Center, Color, Element, Fill, Task, Theme}; use once_cell::sync::Lazy; @@ -25,7 +24,7 @@ struct ScrollableDemo { scrollbar_margin: u16, scroller_width: u16, current_scroll_offset: scrollable::RelativeOffset, - alignment: scrollable::Alignment, + anchor: scrollable::Anchor, } #[derive(Debug, Clone, Eq, PartialEq, Copy)] @@ -38,7 +37,7 @@ enum Direction { #[derive(Debug, Clone)] enum Message { SwitchDirection(Direction), - AlignmentChanged(scrollable::Alignment), + AlignmentChanged(scrollable::Anchor), ScrollbarWidthChanged(u16), ScrollbarMarginChanged(u16), ScrollerWidthChanged(u16), @@ -55,7 +54,7 @@ impl ScrollableDemo { scrollbar_margin: 0, scroller_width: 10, current_scroll_offset: scrollable::RelativeOffset::START, - alignment: scrollable::Alignment::Start, + anchor: scrollable::Anchor::Start, } } @@ -72,7 +71,7 @@ impl ScrollableDemo { } Message::AlignmentChanged(alignment) => { self.current_scroll_offset = scrollable::RelativeOffset::START; - self.alignment = alignment; + self.anchor = alignment; scrollable::snap_to( SCROLLABLE_ID.clone(), @@ -169,14 +168,14 @@ impl ScrollableDemo { text("Scrollable alignment:"), radio( "Start", - scrollable::Alignment::Start, - Some(self.alignment), + scrollable::Anchor::Start, + Some(self.anchor), Message::AlignmentChanged, ), radio( "End", - scrollable::Alignment::End, - Some(self.alignment), + scrollable::Anchor::End, + Some(self.anchor), Message::AlignmentChanged, ) ] @@ -203,7 +202,7 @@ impl ScrollableDemo { let scrollable_content: Element<Message> = Element::from(match self.scrollable_direction { - Direction::Vertical => Scrollable::with_direction( + Direction::Vertical => scrollable( column![ scroll_to_end_button(), text("Beginning!"), @@ -213,22 +212,22 @@ impl ScrollableDemo { text("End!"), scroll_to_beginning_button(), ] - .align_items(Alignment::Center) - .padding([40, 0, 40, 0]) + .align_x(Center) + .padding([40, 0]) .spacing(40), - scrollable::Direction::Vertical( - Properties::new() - .width(self.scrollbar_width) - .margin(self.scrollbar_margin) - .scroller_width(self.scroller_width) - .alignment(self.alignment), - ), ) - .width(Length::Fill) - .height(Length::Fill) + .direction(scrollable::Direction::Vertical( + scrollable::Scrollbar::new() + .width(self.scrollbar_width) + .margin(self.scrollbar_margin) + .scroller_width(self.scroller_width) + .anchor(self.anchor), + )) + .width(Fill) + .height(Fill) .id(SCROLLABLE_ID.clone()) .on_scroll(Message::Scrolled), - Direction::Horizontal => Scrollable::with_direction( + Direction::Horizontal => scrollable( row![ scroll_to_end_button(), text("Beginning!"), @@ -239,22 +238,22 @@ impl ScrollableDemo { scroll_to_beginning_button(), ] .height(450) - .align_items(Alignment::Center) - .padding([0, 40, 0, 40]) + .align_y(Center) + .padding([0, 40]) .spacing(40), - scrollable::Direction::Horizontal( - Properties::new() - .width(self.scrollbar_width) - .margin(self.scrollbar_margin) - .scroller_width(self.scroller_width) - .alignment(self.alignment), - ), ) - .width(Length::Fill) - .height(Length::Fill) + .direction(scrollable::Direction::Horizontal( + scrollable::Scrollbar::new() + .width(self.scrollbar_width) + .margin(self.scrollbar_margin) + .scroller_width(self.scroller_width) + .anchor(self.anchor), + )) + .width(Fill) + .height(Fill) .id(SCROLLABLE_ID.clone()) .on_scroll(Message::Scrolled), - Direction::Multi => Scrollable::with_direction( + Direction::Multi => scrollable( //horizontal content row![ column![ @@ -281,24 +280,24 @@ impl ScrollableDemo { text("Horizontal - End!"), scroll_to_beginning_button(), ] - .align_items(Alignment::Center) - .padding([0, 40, 0, 40]) + .align_y(Center) + .padding([0, 40]) .spacing(40), - { - let properties = Properties::new() - .width(self.scrollbar_width) - .margin(self.scrollbar_margin) - .scroller_width(self.scroller_width) - .alignment(self.alignment); - - scrollable::Direction::Both { - horizontal: properties, - vertical: properties, - } - }, ) - .width(Length::Fill) - .height(Length::Fill) + .direction({ + let scrollbar = scrollable::Scrollbar::new() + .width(self.scrollbar_width) + .margin(self.scrollbar_margin) + .scroller_width(self.scroller_width) + .anchor(self.anchor); + + scrollable::Direction::Both { + horizontal: scrollbar, + vertical: scrollbar, + } + }) + .width(Fill) + .height(Fill) .id(SCROLLABLE_ID.clone()) .on_scroll(Message::Scrolled), }); @@ -323,7 +322,7 @@ impl ScrollableDemo { let content: Element<Message> = column![scroll_controls, scrollable_content, progress_bars] - .align_items(Alignment::Center) + .align_x(Center) .spacing(10) .into(); diff --git a/examples/sierpinski_triangle/src/main.rs b/examples/sierpinski_triangle/src/main.rs index 4c751937..99e7900a 100644 --- a/examples/sierpinski_triangle/src/main.rs +++ b/examples/sierpinski_triangle/src/main.rs @@ -2,7 +2,7 @@ use iced::mouse; use iced::widget::canvas::event::{self, Event}; use iced::widget::canvas::{self, Canvas, Geometry}; use iced::widget::{column, row, slider, text}; -use iced::{Color, Length, Point, Rectangle, Renderer, Size, Theme}; +use iced::{Center, Color, Fill, Point, Rectangle, Renderer, Size, Theme}; use rand::Rng; use std::fmt::Debug; @@ -50,9 +50,7 @@ impl SierpinskiEmulator { fn view(&self) -> iced::Element<'_, Message> { column![ - Canvas::new(&self.graph) - .width(Length::Fill) - .height(Length::Fill), + Canvas::new(&self.graph).width(Fill).height(Fill), row![ text!("Iteration: {:?}", self.graph.iteration), slider(0..=10000, self.graph.iteration, Message::IterationSet) @@ -60,7 +58,7 @@ impl SierpinskiEmulator { .padding(10) .spacing(20), ] - .align_items(iced::Alignment::Center) + .align_x(Center) .into() } } diff --git a/examples/slider/Cargo.toml b/examples/slider/Cargo.toml index fad8916e..05e74d2c 100644 --- a/examples/slider/Cargo.toml +++ b/examples/slider/Cargo.toml @@ -7,3 +7,4 @@ publish = false [dependencies] iced.workspace = true +iced.features = ["svg"] diff --git a/examples/slider/src/main.rs b/examples/slider/src/main.rs index 0b4c29aa..ffb5475f 100644 --- a/examples/slider/src/main.rs +++ b/examples/slider/src/main.rs @@ -1,5 +1,5 @@ -use iced::widget::{center, column, container, slider, text, vertical_slider}; -use iced::{Element, Length}; +use iced::widget::{column, container, iced, slider, text, vertical_slider}; +use iced::{Center, Element, Fill}; pub fn main() -> iced::Result { iced::run("Slider - Iced", Slider::update, Slider::view) @@ -12,19 +12,11 @@ pub enum Message { pub struct Slider { value: u8, - default: u8, - step: u8, - shift_step: u8, } impl Slider { fn new() -> Self { - Slider { - value: 50, - default: 50, - step: 5, - shift_step: 1, - } + Slider { value: 50 } } fn update(&mut self, message: Message) { @@ -37,32 +29,27 @@ impl Slider { fn view(&self) -> Element<Message> { let h_slider = container( - slider(0..=100, self.value, Message::SliderChanged) - .default(self.default) - .step(self.step) - .shift_step(self.shift_step), + slider(1..=100, self.value, Message::SliderChanged) + .default(50) + .shift_step(5), ) .width(250); let v_slider = container( - vertical_slider(0..=100, self.value, Message::SliderChanged) - .default(self.default) - .step(self.step) - .shift_step(self.shift_step), + vertical_slider(1..=100, self.value, Message::SliderChanged) + .default(50) + .shift_step(5), ) .height(200); let text = text(self.value); - center( - column![ - container(v_slider).center_x(Length::Fill), - container(h_slider).center_x(Length::Fill), - container(text).center_x(Length::Fill) - ] - .spacing(25), - ) - .into() + column![v_slider, h_slider, text, iced(self.value as f32),] + .width(Fill) + .align_x(Center) + .spacing(20) + .padding(20) + .into() } } diff --git a/examples/solar_system/Cargo.toml b/examples/solar_system/Cargo.toml index ca64da14..e2c18c50 100644 --- a/examples/solar_system/Cargo.toml +++ b/examples/solar_system/Cargo.toml @@ -7,7 +7,7 @@ publish = false [dependencies] iced.workspace = true -iced.features = ["debug", "canvas", "tokio"] +iced.features = ["debug", "canvas", "image", "tokio"] rand = "0.8.3" tracing-subscriber = "0.3" diff --git a/examples/solar_system/assets/earth.png b/examples/solar_system/assets/earth.png Binary files differnew file mode 100644 index 00000000..e81321d9 --- /dev/null +++ b/examples/solar_system/assets/earth.png diff --git a/examples/solar_system/assets/moon.png b/examples/solar_system/assets/moon.png Binary files differnew file mode 100644 index 00000000..03f10cb7 --- /dev/null +++ b/examples/solar_system/assets/moon.png diff --git a/examples/solar_system/assets/sun.png b/examples/solar_system/assets/sun.png Binary files differnew file mode 100644 index 00000000..29a928a7 --- /dev/null +++ b/examples/solar_system/assets/sun.png diff --git a/examples/solar_system/src/main.rs b/examples/solar_system/src/main.rs index 2a67e23e..1e74f2bd 100644 --- a/examples/solar_system/src/main.rs +++ b/examples/solar_system/src/main.rs @@ -7,13 +7,12 @@ //! //! [1]: https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API/Tutorial/Basic_animations#An_animated_solar_system use iced::mouse; -use iced::widget::canvas; -use iced::widget::canvas::gradient; use iced::widget::canvas::stroke::{self, Stroke}; use iced::widget::canvas::{Geometry, Path}; +use iced::widget::{canvas, image}; use iced::window; use iced::{ - Color, Element, Length, Point, Rectangle, Renderer, Size, Subscription, + Color, Element, Fill, Point, Rectangle, Renderer, Size, Subscription, Theme, Vector, }; @@ -52,10 +51,7 @@ impl SolarSystem { } fn view(&self) -> Element<Message> { - canvas(&self.state) - .width(Length::Fill) - .height(Length::Fill) - .into() + canvas(&self.state).width(Fill).height(Fill).into() } fn theme(&self) -> Theme { @@ -69,6 +65,9 @@ impl SolarSystem { #[derive(Debug)] struct State { + sun: image::Handle, + earth: image::Handle, + moon: image::Handle, space_cache: canvas::Cache, system_cache: canvas::Cache, start: Instant, @@ -88,6 +87,15 @@ impl State { let size = window::Settings::default().size; State { + sun: image::Handle::from_bytes( + include_bytes!("../assets/sun.png").as_slice(), + ), + earth: image::Handle::from_bytes( + include_bytes!("../assets/earth.png").as_slice(), + ), + moon: image::Handle::from_bytes( + include_bytes!("../assets/moon.png").as_slice(), + ), space_cache: canvas::Cache::default(), system_cache: canvas::Cache::default(), start: now, @@ -135,6 +143,8 @@ impl<Message> canvas::Program<Message> for State { let background = self.space_cache.draw(renderer, bounds.size(), |frame| { + frame.fill_rectangle(Point::ORIGIN, frame.size(), Color::BLACK); + let stars = Path::new(|path| { for (p, size) in &self.stars { path.rectangle(*p, Size::new(*size, *size)); @@ -147,17 +157,18 @@ impl<Message> canvas::Program<Message> for State { let system = self.system_cache.draw(renderer, bounds.size(), |frame| { let center = frame.center(); + frame.translate(Vector::new(center.x, center.y)); - let sun = Path::circle(center, Self::SUN_RADIUS); - let orbit = Path::circle(center, Self::ORBIT_RADIUS); + frame.draw_image( + Rectangle::with_radius(Self::SUN_RADIUS), + &self.sun, + ); - frame.fill(&sun, Color::from_rgb8(0xF9, 0xD7, 0x1C)); + let orbit = Path::circle(Point::ORIGIN, Self::ORBIT_RADIUS); frame.stroke( &orbit, Stroke { - style: stroke::Style::Solid(Color::from_rgba8( - 0, 153, 255, 0.1, - )), + style: stroke::Style::Solid(Color::WHITE.scale_alpha(0.1)), width: 1.0, line_dash: canvas::LineDash { offset: 0, @@ -171,30 +182,21 @@ impl<Message> canvas::Program<Message> for State { let rotation = (2.0 * PI / 60.0) * elapsed.as_secs() as f32 + (2.0 * PI / 60_000.0) * elapsed.subsec_millis() as f32; - frame.with_save(|frame| { - frame.translate(Vector::new(center.x, center.y)); - frame.rotate(rotation); - frame.translate(Vector::new(Self::ORBIT_RADIUS, 0.0)); - - let earth = Path::circle(Point::ORIGIN, Self::EARTH_RADIUS); - - let earth_fill = gradient::Linear::new( - Point::new(-Self::EARTH_RADIUS, 0.0), - Point::new(Self::EARTH_RADIUS, 0.0), - ) - .add_stop(0.2, Color::from_rgb(0.15, 0.50, 1.0)) - .add_stop(0.8, Color::from_rgb(0.0, 0.20, 0.47)); + frame.rotate(rotation); + frame.translate(Vector::new(Self::ORBIT_RADIUS, 0.0)); - frame.fill(&earth, earth_fill); + frame.draw_image( + Rectangle::with_radius(Self::EARTH_RADIUS), + canvas::Image::new(&self.earth).rotation(-rotation * 20.0), + ); - frame.with_save(|frame| { - frame.rotate(rotation * 10.0); - frame.translate(Vector::new(0.0, Self::MOON_DISTANCE)); + frame.rotate(rotation * 10.0); + frame.translate(Vector::new(0.0, Self::MOON_DISTANCE)); - let moon = Path::circle(Point::ORIGIN, Self::MOON_RADIUS); - frame.fill(&moon, Color::WHITE); - }); - }); + frame.draw_image( + Rectangle::with_radius(Self::MOON_RADIUS), + &self.moon, + ); }); vec![background, system] diff --git a/examples/stopwatch/src/main.rs b/examples/stopwatch/src/main.rs index bd56785a..0d824d36 100644 --- a/examples/stopwatch/src/main.rs +++ b/examples/stopwatch/src/main.rs @@ -1,8 +1,7 @@ -use iced::alignment; use iced::keyboard; use iced::time; use iced::widget::{button, center, column, row, text}; -use iced::{Alignment, Element, Subscription, Theme}; +use iced::{Center, Element, Subscription, Theme}; use std::time::{Duration, Instant}; @@ -101,13 +100,8 @@ impl Stopwatch { ) .size(40); - let button = |label| { - button( - text(label).horizontal_alignment(alignment::Horizontal::Center), - ) - .padding(10) - .width(80) - }; + let button = + |label| button(text(label).align_x(Center)).padding(10).width(80); let toggle_button = { let label = match self.state { @@ -124,9 +118,7 @@ impl Stopwatch { let controls = row![toggle_button, reset_button].spacing(20); - let content = column![duration, controls] - .align_items(Alignment::Center) - .spacing(20); + let content = column![duration, controls].align_x(Center).spacing(20); center(content).into() } diff --git a/examples/styling/src/main.rs b/examples/styling/src/main.rs index 3124493b..534f5e32 100644 --- a/examples/styling/src/main.rs +++ b/examples/styling/src/main.rs @@ -3,7 +3,7 @@ use iced::widget::{ row, scrollable, slider, text, text_input, toggler, vertical_rule, vertical_space, }; -use iced::{Alignment, Element, Length, Theme}; +use iced::{Center, Element, Fill, Theme}; pub fn main() -> iced::Result { iced::application("Styling - Iced", Styling::update, Styling::view) @@ -48,7 +48,7 @@ impl Styling { let choose_theme = column![ text("Theme:"), pick_list(Theme::ALL, Some(&self.theme), Message::ThemeChanged) - .width(Length::Fill), + .width(Fill), ] .spacing(10); @@ -71,26 +71,21 @@ impl Styling { vertical_space().height(800), "You did it!" ]) - .width(Length::Fill) + .width(Fill) .height(100); let checkbox = checkbox("Check me!", self.checkbox_value) .on_toggle(Message::CheckboxToggled); - let toggler = toggler( - String::from("Toggle me!"), - self.toggler_value, - Message::TogglerToggled, - ) - .width(Length::Shrink) - .spacing(10); + let toggler = toggler(self.toggler_value) + .label("Toggle me!") + .on_toggle(Message::TogglerToggled) + .spacing(10); let content = column![ choose_theme, horizontal_rule(38), - row![text_input, button] - .spacing(10) - .align_items(Alignment::Center), + row![text_input, button].spacing(10).align_y(Center), slider, progress_bar, row![ @@ -100,7 +95,7 @@ impl Styling { ] .spacing(10) .height(100) - .align_items(Alignment::Center), + .align_y(Center), ] .spacing(20) .padding(20) diff --git a/examples/svg/src/main.rs b/examples/svg/src/main.rs index e071c3af..02cb85cc 100644 --- a/examples/svg/src/main.rs +++ b/examples/svg/src/main.rs @@ -1,5 +1,5 @@ use iced::widget::{center, checkbox, column, container, svg}; -use iced::{color, Element, Length}; +use iced::{color, Element, Fill}; pub fn main() -> iced::Result { iced::run("SVG - Iced", Tiger::update, Tiger::view) @@ -30,24 +30,26 @@ impl Tiger { env!("CARGO_MANIFEST_DIR") )); - let svg = svg(handle).width(Length::Fill).height(Length::Fill).style( - |_theme, _status| svg::Style { - color: if self.apply_color_filter { - Some(color!(0x0000ff)) - } else { - None - }, - }, - ); + let svg = + svg(handle) + .width(Fill) + .height(Fill) + .style(|_theme, _status| svg::Style { + color: if self.apply_color_filter { + Some(color!(0x0000ff)) + } else { + None + }, + }); let apply_color_filter = checkbox("Apply a color filter", self.apply_color_filter) .on_toggle(Message::ToggleColorFilter); center( - column![svg, container(apply_color_filter).center_x(Length::Fill)] + column![svg, container(apply_color_filter).center_x(Fill)] .spacing(20) - .height(Length::Fill), + .height(Fill), ) .padding(20) .into() diff --git a/examples/the_matrix/src/main.rs b/examples/the_matrix/src/main.rs index 2ae1cc3a..0ed52dda 100644 --- a/examples/the_matrix/src/main.rs +++ b/examples/the_matrix/src/main.rs @@ -2,8 +2,7 @@ use iced::mouse; use iced::time::{self, Instant}; use iced::widget::canvas; use iced::{ - Color, Element, Font, Length, Point, Rectangle, Renderer, Subscription, - Theme, + Color, Element, Fill, Font, Point, Rectangle, Renderer, Subscription, Theme, }; use std::cell::RefCell; @@ -37,10 +36,7 @@ impl TheMatrix { } fn view(&self) -> Element<Message> { - canvas(self as &Self) - .width(Length::Fill) - .height(Length::Fill) - .into() + canvas(self as &Self).width(Fill).height(Fill).into() } fn subscription(&self) -> Subscription<Message> { diff --git a/examples/toast/src/main.rs b/examples/toast/src/main.rs index 232133b1..8f6a836e 100644 --- a/examples/toast/src/main.rs +++ b/examples/toast/src/main.rs @@ -4,7 +4,7 @@ use iced::keyboard::key; use iced::widget::{ self, button, center, column, pick_list, row, slider, text, text_input, }; -use iced::{Alignment, Element, Length, Subscription, Task}; +use iced::{Center, Element, Fill, Subscription, Task}; use toast::{Status, Toast}; @@ -125,7 +125,7 @@ impl App { Some(self.editing.status), Message::Status ) - .width(Length::Fill) + .width(Fill) .into() ), subtitle( @@ -142,7 +142,7 @@ impl App { .spacing(5) .into() ), - column![add_toast].align_items(Alignment::End) + column![add_toast].align_x(Center) ] .spacing(10) .max_width(200), @@ -177,8 +177,8 @@ mod toast { }; use iced::window; use iced::{ - Alignment, Element, Length, Point, Rectangle, Renderer, Size, Theme, - Vector, + Alignment, Center, Element, Fill, Length, Point, Rectangle, Renderer, + Size, Theme, Vector, }; pub const DEFAULT_TIMEOUT: u64 = 5; @@ -245,9 +245,9 @@ mod toast { .on_press((on_close)(index)) .padding(3), ] - .align_items(Alignment::Center) + .align_y(Center) ) - .width(Length::Fill) + .width(Fill) .padding(5) .style(match toast.status { Status::Primary => primary, @@ -257,7 +257,7 @@ mod toast { }), horizontal_rule(1), container(text(toast.body.as_str())) - .width(Length::Fill) + .width(Fill) .padding(5) .style(container::rounded_box), ]) @@ -347,7 +347,7 @@ mod toast { state: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn Operation<()>, + operation: &mut dyn Operation, ) { operation.container(None, layout.bounds(), &mut |operation| { self.content.as_widget().operate( @@ -479,8 +479,8 @@ mod toast { layout::flex::Axis::Vertical, renderer, &limits, - Length::Fill, - Length::Fill, + Fill, + Fill, 10.into(), 10.0, Alignment::End, @@ -589,7 +589,7 @@ mod toast { &mut self, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn widget::Operation<()>, + operation: &mut dyn widget::Operation, ) { operation.container(None, layout.bounds(), &mut |operation| { self.toasts diff --git a/examples/todos/src/main.rs b/examples/todos/src/main.rs index 6ed50d31..a5f7b36a 100644 --- a/examples/todos/src/main.rs +++ b/examples/todos/src/main.rs @@ -1,11 +1,10 @@ -use iced::alignment::{self, Alignment}; use iced::keyboard; use iced::widget::{ self, button, center, checkbox, column, container, keyed_column, row, scrollable, text, text_input, Text, }; use iced::window; -use iced::{Element, Font, Length, Subscription, Task as Command}; +use iced::{Center, Element, Fill, Font, Subscription, Task as Command}; use once_cell::sync::Lazy; use serde::{Deserialize, Serialize}; @@ -18,16 +17,14 @@ pub fn main() -> iced::Result { tracing_subscriber::fmt::init(); iced::application(Todos::title, Todos::update, Todos::view) - .load(Todos::load) .subscription(Todos::subscription) .font(include_bytes!("../fonts/icons.ttf").as_slice()) .window_size((500.0, 800.0)) - .run() + .run_with(Todos::new) } -#[derive(Default, Debug)] +#[derive(Debug)] enum Todos { - #[default] Loading, Loaded(State), } @@ -54,8 +51,11 @@ enum Message { } impl Todos { - fn load() -> Command<Message> { - Command::perform(SavedState::load(), Message::Loaded) + fn new() -> (Self, Command<Message>) { + ( + Self::Loading, + Command::perform(SavedState::load(), Message::Loaded), + ) } fn title(&self) -> String { @@ -192,17 +192,18 @@ impl Todos { .. }) => { let title = text("todos") - .width(Length::Fill) + .width(Fill) .size(100) .color([0.5, 0.5, 0.5]) - .horizontal_alignment(alignment::Horizontal::Center); + .align_x(Center); let input = text_input("What needs to be done?", input_value) .id(INPUT_ID.clone()) .on_input(Message::InputChanged) .on_submit(Message::CreateTask) .padding(15) - .size(30); + .size(30) + .align_x(Center); let controls = view_controls(tasks, *filter); let filtered_tasks = @@ -239,10 +240,7 @@ impl Todos { .spacing(20) .max_width(800); - scrollable( - container(content).center_x(Length::Fill).padding(40), - ) - .into() + scrollable(container(content).center_x(Fill).padding(40)).into() } } } @@ -342,7 +340,7 @@ impl Task { TaskState::Idle => { let checkbox = checkbox(&self.description, self.completed) .on_toggle(TaskMessage::Completed) - .width(Length::Fill) + .width(Fill) .size(17) .text_shaping(text::Shaping::Advanced); @@ -354,7 +352,7 @@ impl Task { .style(button::text), ] .spacing(20) - .align_items(Alignment::Center) + .align_y(Center) .into() } TaskState::Editing => { @@ -370,14 +368,14 @@ impl Task { button( row![delete_icon(), "Delete"] .spacing(10) - .align_items(Alignment::Center) + .align_y(Center) ) .on_press(TaskMessage::Delete) .padding(10) .style(button::danger) ] .spacing(20) - .align_items(Alignment::Center) + .align_y(Center) .into() } } @@ -404,17 +402,16 @@ fn view_controls(tasks: &[Task], current_filter: Filter) -> Element<Message> { "{tasks_left} {} left", if tasks_left == 1 { "task" } else { "tasks" } ) - .width(Length::Fill), + .width(Fill), row![ filter_button("All", Filter::All, current_filter), filter_button("Active", Filter::Active, current_filter), filter_button("Completed", Filter::Completed, current_filter,), ] - .width(Length::Shrink) .spacing(10) ] .spacing(20) - .align_items(Alignment::Center) + .align_y(Center) .into() } @@ -439,20 +436,15 @@ impl Filter { } fn loading_message<'a>() -> Element<'a, Message> { - center( - text("Loading...") - .horizontal_alignment(alignment::Horizontal::Center) - .size(50), - ) - .into() + center(text("Loading...").width(Fill).align_x(Center).size(50)).into() } fn empty_message(message: &str) -> Element<'_, Message> { center( text(message) - .width(Length::Fill) + .width(Fill) .size(25) - .horizontal_alignment(alignment::Horizontal::Center) + .align_x(Center) .color([0.7, 0.7, 0.7]), ) .height(200) @@ -466,7 +458,7 @@ fn icon(unicode: char) -> Text<'static> { text(unicode.to_string()) .font(ICONS) .width(20) - .horizontal_alignment(alignment::Horizontal::Center) + .align_x(Center) } fn edit_icon() -> Text<'static> { diff --git a/examples/tour/src/main.rs b/examples/tour/src/main.rs index 94ba78ee..d8c0b29a 100644 --- a/examples/tour/src/main.rs +++ b/examples/tour/src/main.rs @@ -1,10 +1,9 @@ -use iced::alignment::{self, Alignment}; use iced::widget::{ button, checkbox, column, container, horizontal_space, image, radio, row, scrollable, slider, text, text_input, toggler, vertical_space, }; use iced::widget::{Button, Column, Container, Slider}; -use iced::{Color, Element, Font, Length, Pixels}; +use iced::{Center, Color, Element, Fill, Font, Pixels}; pub fn main() -> iced::Result { #[cfg(target_arch = "wasm32")] @@ -173,10 +172,10 @@ impl Tour { } else { content }) - .center_x(Length::Fill), + .center_x(Fill), ); - container(scrollable).center_y(Length::Fill).into() + container(scrollable).center_y(Fill).into() } fn can_continue(&self) -> bool { @@ -235,11 +234,7 @@ impl Tour { 0 to 100:", ) .push(slider(0..=100, self.slider, Message::SliderChanged)) - .push( - text(self.slider.to_string()) - .width(Length::Fill) - .horizontal_alignment(alignment::Horizontal::Center), - ) + .push(text(self.slider.to_string()).width(Fill).align_x(Center)) } fn rows_and_columns(&self) -> Column<Message> { @@ -268,9 +263,7 @@ impl Tour { let spacing_section = column![ slider(0..=80, self.spacing, Message::SpacingChanged), - text!("{} px", self.spacing) - .width(Length::Fill) - .horizontal_alignment(alignment::Horizontal::Center), + text!("{} px", self.spacing).width(Fill).align_x(Center), ] .spacing(10); @@ -364,11 +357,11 @@ impl Tour { Self::container("Toggler") .push("A toggler is mostly used to enable or disable something.") .push( - Container::new(toggler( - "Toggle me to continue...".to_owned(), - self.toggler, - Message::TogglerChanged, - )) + Container::new( + toggler(self.toggler) + .label("Toggle me to continue...") + .on_toggle(Message::TogglerChanged), + ) .padding([0, 40]), ) } @@ -381,11 +374,7 @@ impl Tour { .push("An image that tries to keep its aspect ratio.") .push(ferris(width, filter_method)) .push(slider(100..=500, width, Message::ImageWidthChanged)) - .push( - text!("Width: {width} px") - .width(Length::Fill) - .horizontal_alignment(alignment::Horizontal::Center), - ) + .push(text!("Width: {width} px").width(Fill).align_x(Center)) .push( checkbox( "Use nearest interpolation", @@ -393,7 +382,7 @@ impl Tour { ) .on_toggle(Message::ImageUseNearestToggled), ) - .align_items(Alignment::Center) + .align_x(Center) } fn scrollable(&self) -> Column<Message> { @@ -409,18 +398,13 @@ impl Tour { .push(vertical_space().height(4096)) .push( text("You are halfway there!") - .width(Length::Fill) + .width(Fill) .size(30) - .horizontal_alignment(alignment::Horizontal::Center), + .align_x(Center), ) .push(vertical_space().height(4096)) .push(ferris(300, image::FilterMethod::Linear)) - .push( - text("You made it!") - .width(Length::Fill) - .size(50) - .horizontal_alignment(alignment::Horizontal::Center), - ) + .push(text("You made it!").width(Fill).size(50).align_x(Center)) } fn text_input(&self) -> Column<Message> { @@ -464,8 +448,8 @@ impl Tour { } else { value }) - .width(Length::Fill) - .horizontal_alignment(alignment::Horizontal::Center), + .width(Fill) + .align_x(Center), ) } @@ -570,7 +554,7 @@ fn ferris<'a>( .filter_method(filter_method) .width(width), ) - .center_x(Length::Fill) + .center_x(Fill) } fn padded_button<Message: Clone>(label: &str) -> Button<'_, Message> { diff --git a/examples/vectorial_text/src/main.rs b/examples/vectorial_text/src/main.rs index 6dd3273a..ce34d826 100644 --- a/examples/vectorial_text/src/main.rs +++ b/examples/vectorial_text/src/main.rs @@ -1,9 +1,9 @@ -use iced::alignment::{self, Alignment}; +use iced::alignment; use iced::mouse; use iced::widget::{ canvas, checkbox, column, horizontal_space, row, slider, text, }; -use iced::{Element, Length, Point, Rectangle, Renderer, Theme, Vector}; +use iced::{Center, Element, Fill, Point, Rectangle, Renderer, Theme, Vector}; pub fn main() -> iced::Result { iced::application( @@ -59,7 +59,7 @@ impl VectorialText { }; column![ - canvas(&self.state).width(Length::Fill).height(Length::Fill), + canvas(&self.state).width(Fill).height(Fill), column![ checkbox("Use Japanese", self.state.use_japanese,) .on_toggle(Message::ToggleJapanese), @@ -85,7 +85,7 @@ impl VectorialText { ] .spacing(20), ] - .align_items(Alignment::Center) + .align_x(Center) .spacing(10) ] .spacing(10) diff --git a/examples/visible_bounds/src/main.rs b/examples/visible_bounds/src/main.rs index e46d1ff0..77fec65e 100644 --- a/examples/visible_bounds/src/main.rs +++ b/examples/visible_bounds/src/main.rs @@ -5,8 +5,8 @@ use iced::widget::{ }; use iced::window; use iced::{ - Alignment, Color, Element, Font, Length, Point, Rectangle, Subscription, - Task, Theme, + Center, Color, Element, Fill, Font, Point, Rectangle, Subscription, Task, + Theme, }; pub fn main() -> iced::Result { @@ -70,7 +70,7 @@ impl Example { .color_maybe(color), ] .height(40) - .align_items(Alignment::Center) + .align_y(Center) }; let view_bounds = |label, bounds: Option<Rectangle>| { @@ -130,13 +130,13 @@ impl Example { .padding(20) ) .on_scroll(|_| Message::Scrolled) - .width(Length::Fill) + .width(Fill) .height(300), ] .padding(20) ) .on_scroll(|_| Message::Scrolled) - .width(Length::Fill) + .width(Fill) .height(300), ] .spacing(10) diff --git a/examples/websocket/src/echo.rs b/examples/websocket/src/echo.rs index cd32cb66..14652936 100644 --- a/examples/websocket/src/echo.rs +++ b/examples/websocket/src/echo.rs @@ -1,87 +1,79 @@ pub mod server; use iced::futures; -use iced::subscription::{self, Subscription}; +use iced::stream; use iced::widget::text; use futures::channel::mpsc; use futures::sink::SinkExt; -use futures::stream::StreamExt; +use futures::stream::{Stream, StreamExt}; use async_tungstenite::tungstenite; use std::fmt; -pub fn connect() -> Subscription<Event> { - struct Connect; +pub fn connect() -> impl Stream<Item = Event> { + stream::channel(100, |mut output| async move { + let mut state = State::Disconnected; - subscription::channel( - std::any::TypeId::of::<Connect>(), - 100, - |mut output| async move { - let mut state = State::Disconnected; + loop { + match &mut state { + State::Disconnected => { + const ECHO_SERVER: &str = "ws://127.0.0.1:3030"; - loop { - match &mut state { - State::Disconnected => { - const ECHO_SERVER: &str = "ws://127.0.0.1:3030"; - - match async_tungstenite::tokio::connect_async( - ECHO_SERVER, - ) + match async_tungstenite::tokio::connect_async(ECHO_SERVER) .await - { - Ok((websocket, _)) => { - let (sender, receiver) = mpsc::channel(100); - - let _ = output - .send(Event::Connected(Connection(sender))) - .await; + { + Ok((websocket, _)) => { + let (sender, receiver) = mpsc::channel(100); - state = State::Connected(websocket, receiver); - } - Err(_) => { - tokio::time::sleep( - tokio::time::Duration::from_secs(1), - ) + let _ = output + .send(Event::Connected(Connection(sender))) .await; - let _ = output.send(Event::Disconnected).await; - } + state = State::Connected(websocket, receiver); + } + Err(_) => { + tokio::time::sleep( + tokio::time::Duration::from_secs(1), + ) + .await; + + let _ = output.send(Event::Disconnected).await; } } - State::Connected(websocket, input) => { - let mut fused_websocket = websocket.by_ref().fuse(); - - futures::select! { - received = fused_websocket.select_next_some() => { - match received { - Ok(tungstenite::Message::Text(message)) => { - let _ = output.send(Event::MessageReceived(Message::User(message))).await; - } - Err(_) => { - let _ = output.send(Event::Disconnected).await; - - state = State::Disconnected; - } - Ok(_) => continue, + } + State::Connected(websocket, input) => { + let mut fused_websocket = websocket.by_ref().fuse(); + + futures::select! { + received = fused_websocket.select_next_some() => { + match received { + Ok(tungstenite::Message::Text(message)) => { + let _ = output.send(Event::MessageReceived(Message::User(message))).await; } - } - - message = input.select_next_some() => { - let result = websocket.send(tungstenite::Message::Text(message.to_string())).await; - - if result.is_err() { + Err(_) => { let _ = output.send(Event::Disconnected).await; state = State::Disconnected; } + Ok(_) => continue, + } + } + + message = input.select_next_some() => { + let result = websocket.send(tungstenite::Message::Text(message.to_string())).await; + + if result.is_err() { + let _ = output.send(Event::Disconnected).await; + + state = State::Disconnected; } } } } } - }, - ) + } + }) } #[derive(Debug)] diff --git a/examples/websocket/src/main.rs b/examples/websocket/src/main.rs index 8422ce16..8b1efb41 100644 --- a/examples/websocket/src/main.rs +++ b/examples/websocket/src/main.rs @@ -1,20 +1,17 @@ mod echo; -use iced::alignment::{self, Alignment}; use iced::widget::{ self, button, center, column, row, scrollable, text, text_input, }; -use iced::{color, Element, Length, Subscription, Task}; +use iced::{color, Center, Element, Fill, Subscription, Task}; use once_cell::sync::Lazy; pub fn main() -> iced::Result { iced::application("WebSocket - Iced", WebSocket::update, WebSocket::view) - .load(WebSocket::load) .subscription(WebSocket::subscription) - .run() + .run_with(WebSocket::new) } -#[derive(Default)] struct WebSocket { messages: Vec<echo::Message>, new_message: String, @@ -30,11 +27,18 @@ enum Message { } impl WebSocket { - fn load() -> Task<Message> { - Task::batch([ - Task::perform(echo::server::run(), |_| Message::Server), - widget::focus_next(), - ]) + fn new() -> (Self, Task<Message>) { + ( + Self { + messages: Vec::new(), + new_message: String::new(), + state: State::Disconnected, + }, + Task::batch([ + Task::perform(echo::server::run(), |_| Message::Server), + widget::focus_next(), + ]), + ) } fn update(&mut self, message: Message) -> Task<Message> { @@ -83,7 +87,7 @@ impl WebSocket { } fn subscription(&self) -> Subscription<Message> { - echo::connect().map(Message::Echo) + Subscription::run(echo::connect).map(Message::Echo) } fn view(&self) -> Element<Message> { @@ -99,7 +103,7 @@ impl WebSocket { .spacing(10), ) .id(MESSAGE_LOG.clone()) - .height(Length::Fill) + .height(Fill) .into() }; @@ -108,12 +112,8 @@ impl WebSocket { .on_input(Message::NewMessageChanged) .padding(10); - let mut button = button( - text("Send") - .height(40) - .vertical_alignment(alignment::Vertical::Center), - ) - .padding([0, 20]); + let mut button = button(text("Send").height(40).align_y(Center)) + .padding([0, 20]); if matches!(self.state, State::Connected(_)) { if let Some(message) = echo::Message::new(&self.new_message) { @@ -122,13 +122,11 @@ impl WebSocket { } } - row![input, button] - .spacing(10) - .align_items(Alignment::Center) + row![input, button].spacing(10).align_y(Center) }; column![message_log, new_message_input] - .height(Length::Fill) + .height(Fill) .padding(20) .spacing(10) .into() @@ -140,10 +138,4 @@ enum State { Connected(echo::Connection), } -impl Default for State { - fn default() -> Self { - Self::Disconnected - } -} - static MESSAGE_LOG: Lazy<scrollable::Id> = Lazy::new(scrollable::Id::unique); diff --git a/futures/src/backend/native/async_std.rs b/futures/src/backend/native/async_std.rs index b7da5e90..86714f45 100644 --- a/futures/src/backend/native/async_std.rs +++ b/futures/src/backend/native/async_std.rs @@ -27,7 +27,7 @@ pub mod time { pub fn every( duration: std::time::Duration, ) -> Subscription<std::time::Instant> { - Subscription::from_recipe(Every(duration)) + subscription::from_recipe(Every(duration)) } #[derive(Debug)] diff --git a/futures/src/backend/native/smol.rs b/futures/src/backend/native/smol.rs index aaf1518c..8d448e7f 100644 --- a/futures/src/backend/native/smol.rs +++ b/futures/src/backend/native/smol.rs @@ -26,7 +26,7 @@ pub mod time { pub fn every( duration: std::time::Duration, ) -> Subscription<std::time::Instant> { - Subscription::from_recipe(Every(duration)) + subscription::from_recipe(Every(duration)) } #[derive(Debug)] diff --git a/futures/src/backend/native/tokio.rs b/futures/src/backend/native/tokio.rs index df91d798..9dc3593d 100644 --- a/futures/src/backend/native/tokio.rs +++ b/futures/src/backend/native/tokio.rs @@ -31,7 +31,7 @@ pub mod time { pub fn every( duration: std::time::Duration, ) -> Subscription<std::time::Instant> { - Subscription::from_recipe(Every(duration)) + subscription::from_recipe(Every(duration)) } #[derive(Debug)] diff --git a/futures/src/backend/wasm/wasm_bindgen.rs b/futures/src/backend/wasm/wasm_bindgen.rs index 3228dd18..f7846c01 100644 --- a/futures/src/backend/wasm/wasm_bindgen.rs +++ b/futures/src/backend/wasm/wasm_bindgen.rs @@ -26,7 +26,7 @@ pub mod time { pub fn every( duration: std::time::Duration, ) -> Subscription<wasm_timer::Instant> { - Subscription::from_recipe(Every(duration)) + subscription::from_recipe(Every(duration)) } #[derive(Debug)] diff --git a/futures/src/keyboard.rs b/futures/src/keyboard.rs index f0d7d757..35f6b6fa 100644 --- a/futures/src/keyboard.rs +++ b/futures/src/keyboard.rs @@ -6,7 +6,7 @@ use crate::subscription::{self, Subscription}; use crate::MaybeSend; /// Listens to keyboard key presses and calls the given function -/// map them into actual messages. +/// to map them into actual messages. /// /// If the function returns `None`, the key press will be simply /// ignored. @@ -31,7 +31,7 @@ where } /// Listens to keyboard key releases and calls the given function -/// map them into actual messages. +/// to map them into actual messages. /// /// If the function returns `None`, the key release will be simply /// ignored. diff --git a/futures/src/lib.rs b/futures/src/lib.rs index a874a618..31738823 100644 --- a/futures/src/lib.rs +++ b/futures/src/lib.rs @@ -15,6 +15,7 @@ pub mod backend; pub mod event; pub mod executor; pub mod keyboard; +pub mod stream; pub mod subscription; pub use executor::Executor; diff --git a/futures/src/stream.rs b/futures/src/stream.rs new file mode 100644 index 00000000..af2f8c99 --- /dev/null +++ b/futures/src/stream.rs @@ -0,0 +1,46 @@ +//! Create asynchronous streams of data. +use futures::channel::mpsc; +use futures::stream::{self, Stream, StreamExt}; + +use std::future::Future; + +/// Creates a new [`Stream`] that produces the items sent from a [`Future`] +/// to the [`mpsc::Sender`] provided to the closure. +/// +/// This is a more ergonomic [`stream::unfold`], which allows you to go +/// from the "world of futures" to the "world of streams" by simply looping +/// and publishing to an async channel from inside a [`Future`]. +pub fn channel<T, F>( + size: usize, + f: impl FnOnce(mpsc::Sender<T>) -> F, +) -> impl Stream<Item = T> +where + F: Future<Output = ()>, +{ + let (sender, receiver) = mpsc::channel(size); + + let runner = stream::once(f(sender)).filter_map(|_| async { None }); + + stream::select(receiver, runner) +} + +/// Creates a new [`Stream`] that produces the items sent from a [`Future`] +/// that can fail to the [`mpsc::Sender`] provided to the closure. +pub fn try_channel<T, E, F>( + size: usize, + f: impl FnOnce(mpsc::Sender<T>) -> F, +) -> impl Stream<Item = Result<T, E>> +where + F: Future<Output = Result<(), E>>, +{ + let (sender, receiver) = mpsc::channel(size); + + let runner = stream::once(f(sender)).filter_map(|result| async { + match result { + Ok(()) => None, + Err(error) => Some(Err(error)), + } + }); + + stream::select(receiver.map(Ok), runner) +} diff --git a/futures/src/subscription.rs b/futures/src/subscription.rs index 316fc44d..d2a0c3f8 100644 --- a/futures/src/subscription.rs +++ b/futures/src/subscription.rs @@ -5,11 +5,9 @@ pub use tracker::Tracker; use crate::core::event; use crate::core::window; -use crate::futures::{Future, Stream}; +use crate::futures::Stream; use crate::{BoxStream, MaybeSend}; -use futures::channel::mpsc; -use futures::never::Never; use std::any::TypeId; use std::hash::Hash; @@ -61,20 +59,66 @@ pub type Hasher = rustc_hash::FxHasher; /// A request to listen to external events. /// -/// Besides performing async actions on demand with `Command`, most +/// Besides performing async actions on demand with `Task`, most /// applications also need to listen to external events passively. /// -/// A [`Subscription`] is normally provided to some runtime, like a `Command`, +/// A [`Subscription`] is normally provided to some runtime, like a `Task`, /// and it will generate events as long as the user keeps requesting it. /// /// For instance, you can use a [`Subscription`] to listen to a `WebSocket` /// connection, keyboard presses, mouse events, time ticks, etc. +/// +/// # The Lifetime of a [`Subscription`] +/// Much like a [`Future`] or a [`Stream`], a [`Subscription`] does not produce any effects +/// on its own. For a [`Subscription`] to run, it must be returned to the iced runtime—normally +/// in the `subscription` function of an `application` or a `daemon`. +/// +/// When a [`Subscription`] is provided to the runtime for the first time, the runtime will +/// start running it asynchronously. Running a [`Subscription`] consists in building its underlying +/// [`Stream`] and executing it in an async runtime. +/// +/// Therefore, you can think of a [`Subscription`] as a "stream builder". It simply represents a way +/// to build a certain [`Stream`] together with some way to _identify_ it. +/// +/// Identification is important because when a specific [`Subscription`] stops being returned to the +/// iced runtime, the runtime will kill its associated [`Stream`]. The runtime uses the identity of a +/// [`Subscription`] to keep track of it. +/// +/// This way, iced allows you to declaratively __subscribe__ to particular streams of data temporarily +/// and whenever necessary. +/// +/// ``` +/// # mod iced { +/// # pub mod time { +/// # pub use iced_futures::backend::default::time::every; +/// # pub use std::time::{Duration, Instant}; +/// # } +/// # +/// # pub use iced_futures::Subscription; +/// # } +/// use iced::time::{self, Duration, Instant}; +/// use iced::Subscription; +/// +/// struct State { +/// timer_enabled: bool, +/// } +/// +/// fn subscription(state: &State) -> Subscription<Instant> { +/// if state.timer_enabled { +/// time::every(Duration::from_secs(1)) +/// } else { +/// Subscription::none() +/// } +/// } +/// ``` +/// +/// [`Future`]: std::future::Future #[must_use = "`Subscription` must be returned to runtime to take effect"] -pub struct Subscription<Message> { - recipes: Vec<Box<dyn Recipe<Output = Message>>>, +pub struct Subscription<T> { + recipes: Vec<Box<dyn Recipe<Output = T>>>, } -impl<Message> Subscription<Message> { +impl<T> Subscription<T> { /// Returns an empty [`Subscription`] that will not produce any output. pub fn none() -> Self { Self { @@ -82,19 +126,102 @@ impl<Message> Subscription<Message> { } } - /// Creates a [`Subscription`] from a [`Recipe`] describing it. - pub fn from_recipe( - recipe: impl Recipe<Output = Message> + 'static, - ) -> Self { - Self { - recipes: vec![Box::new(recipe)], - } + /// Returns a [`Subscription`] that will call the given function to create and + /// asynchronously run the given [`Stream`]. + /// + /// # Creating an asynchronous worker with bidirectional communication + /// You can leverage this helper to create a [`Subscription`] that spawns + /// an asynchronous worker in the background and establish a channel of + /// communication with an `iced` application. + /// + /// You can achieve this by creating an `mpsc` channel inside the closure + /// and returning the `Sender` as a `Message` for the `Application`: + /// + /// ``` + /// use iced_futures::subscription::{self, Subscription}; + /// use iced_futures::stream; + /// use iced_futures::futures::channel::mpsc; + /// use iced_futures::futures::sink::SinkExt; + /// use iced_futures::futures::Stream; + /// + /// pub enum Event { + /// Ready(mpsc::Sender<Input>), + /// WorkFinished, + /// // ... + /// } + /// + /// enum Input { + /// DoSomeWork, + /// // ... + /// } + /// + /// fn some_worker() -> impl Stream<Item = Event> { + /// stream::channel(100, |mut output| async move { + /// // Create channel + /// let (sender, mut receiver) = mpsc::channel(100); + /// + /// // Send the sender back to the application + /// output.send(Event::Ready(sender)).await; + /// + /// loop { + /// use iced_futures::futures::StreamExt; + /// + /// // Read next input sent from `Application` + /// let input = receiver.select_next_some().await; + /// + /// match input { + /// Input::DoSomeWork => { + /// // Do some async work... + /// + /// // Finally, we can optionally produce a message to tell the + /// // `Application` the work is done + /// output.send(Event::WorkFinished).await; + /// } + /// } + /// } + /// }) + /// } + /// + /// fn subscription() -> Subscription<Event> { + /// Subscription::run(some_worker) + /// } + /// ``` + /// + /// Check out the [`websocket`] example, which showcases this pattern to maintain a `WebSocket` + /// connection open. + /// + /// [`websocket`]: https://github.com/iced-rs/iced/tree/0.12/examples/websocket + pub fn run<S>(builder: fn() -> S) -> Self + where + S: Stream<Item = T> + MaybeSend + 'static, + T: 'static, + { + from_recipe(Runner { + id: builder, + spawn: move |_| builder(), + }) + } + + /// Returns a [`Subscription`] that will create and asynchronously run the + /// given [`Stream`]. + /// + /// The `id` will be used to uniquely identify the [`Subscription`]. + pub fn run_with_id<I, S>(id: I, stream: S) -> Subscription<T> + where + I: Hash + 'static, + S: Stream<Item = T> + MaybeSend + 'static, + T: 'static, + { + from_recipe(Runner { + id, + spawn: move |_| stream, + }) } /// Batches all the provided subscriptions and returns the resulting /// [`Subscription`]. pub fn batch( - subscriptions: impl IntoIterator<Item = Subscription<Message>>, + subscriptions: impl IntoIterator<Item = Subscription<T>>, ) -> Self { Self { recipes: subscriptions @@ -104,18 +231,13 @@ impl<Message> Subscription<Message> { } } - /// Returns the different recipes of the [`Subscription`]. - pub fn into_recipes(self) -> Vec<Box<dyn Recipe<Output = Message>>> { - self.recipes - } - /// Adds a value to the [`Subscription`] context. /// /// The value will be part of the identity of a [`Subscription`]. - pub fn with<T>(mut self, value: T) -> Subscription<(T, Message)> + pub fn with<A>(mut self, value: A) -> Subscription<(A, T)> where - Message: 'static, - T: std::hash::Hash + Clone + Send + Sync + 'static, + T: 'static, + A: std::hash::Hash + Clone + Send + Sync + 'static, { Subscription { recipes: self @@ -123,7 +245,7 @@ impl<Message> Subscription<Message> { .drain(..) .map(|recipe| { Box::new(With::new(recipe, value.clone())) - as Box<dyn Recipe<Output = (T, Message)>> + as Box<dyn Recipe<Output = (A, T)>> }) .collect(), } @@ -136,8 +258,8 @@ impl<Message> Subscription<Message> { /// will panic in debug mode otherwise. pub fn map<F, A>(mut self, f: F) -> Subscription<A> where - Message: 'static, - F: Fn(Message) -> A + MaybeSend + Clone + 'static, + T: 'static, + F: Fn(T) -> A + MaybeSend + Clone + 'static, A: 'static, { debug_assert!( @@ -159,7 +281,23 @@ impl<Message> Subscription<Message> { } } -impl<Message> std::fmt::Debug for Subscription<Message> { +/// Creates a [`Subscription`] from a [`Recipe`] describing it. +pub fn from_recipe<T>( + recipe: impl Recipe<Output = T> + 'static, +) -> Subscription<T> { + Subscription { + recipes: vec![Box::new(recipe)], + } +} + +/// Returns the different recipes of the [`Subscription`]. +pub fn into_recipes<T>( + subscription: Subscription<T>, +) -> Vec<Box<dyn Recipe<Output = T>>> { + subscription.recipes +} + +impl<T> std::fmt::Debug for Subscription<T> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("Subscription").finish() } @@ -175,9 +313,9 @@ impl<Message> std::fmt::Debug for Subscription<Message> { /// The repository has a couple of [examples] that use a custom [`Recipe`]: /// /// - [`download_progress`], a basic application that asynchronously downloads -/// a dummy file of 100 MB and tracks the download progress. +/// a dummy file of 100 MB and tracks the download progress. /// - [`stopwatch`], a watch with start/stop and reset buttons showcasing how -/// to listen to time. +/// to listen to time. /// /// [examples]: https://github.com/iced-rs/iced/tree/0.12/examples /// [`download_progress`]: https://github.com/iced-rs/iced/tree/0.12/examples/download_progress @@ -273,65 +411,13 @@ where } } -/// Returns a [`Subscription`] that will call the given function to create and -/// asynchronously run the given [`Stream`]. -pub fn run<S, Message>(builder: fn() -> S) -> Subscription<Message> -where - S: Stream<Item = Message> + MaybeSend + 'static, - Message: 'static, -{ - Subscription::from_recipe(Runner { - id: builder, - spawn: move |_| builder(), - }) -} - -/// Returns a [`Subscription`] that will create and asynchronously run the -/// given [`Stream`]. -/// -/// The `id` will be used to uniquely identify the [`Subscription`]. -pub fn run_with_id<I, S, Message>(id: I, stream: S) -> Subscription<Message> -where - I: Hash + 'static, - S: Stream<Item = Message> + MaybeSend + 'static, - Message: 'static, -{ - Subscription::from_recipe(Runner { - id, - spawn: move |_| stream, - }) -} - -/// Returns a [`Subscription`] that will create and asynchronously run a -/// [`Stream`] that will call the provided closure to produce every `Message`. -/// -/// The `id` will be used to uniquely identify the [`Subscription`]. -pub fn unfold<I, T, Fut, Message>( - id: I, - initial: T, - mut f: impl FnMut(T) -> Fut + MaybeSend + Sync + 'static, -) -> Subscription<Message> +pub(crate) fn filter_map<I, F, T>(id: I, f: F) -> Subscription<T> where I: Hash + 'static, - T: MaybeSend + 'static, - Fut: Future<Output = (Message, T)> + MaybeSend + 'static, - Message: 'static + MaybeSend, + F: Fn(Event) -> Option<T> + MaybeSend + 'static, + T: 'static + MaybeSend, { - use futures::future::FutureExt; - - run_with_id( - id, - futures::stream::unfold(initial, move |state| f(state).map(Some)), - ) -} - -pub(crate) fn filter_map<I, F, Message>(id: I, f: F) -> Subscription<Message> -where - I: Hash + 'static, - F: Fn(Event) -> Option<Message> + MaybeSend + 'static, - Message: 'static + MaybeSend, -{ - Subscription::from_recipe(Runner { + from_recipe(Runner { id, spawn: |events| { use futures::future; @@ -342,122 +428,22 @@ where }) } -/// Creates a [`Subscription`] that publishes the events sent from a [`Future`] -/// to an [`mpsc::Sender`] with the given bounds. -/// -/// # Creating an asynchronous worker with bidirectional communication -/// You can leverage this helper to create a [`Subscription`] that spawns -/// an asynchronous worker in the background and establish a channel of -/// communication with an `iced` application. -/// -/// You can achieve this by creating an `mpsc` channel inside the closure -/// and returning the `Sender` as a `Message` for the `Application`: -/// -/// ``` -/// use iced_futures::subscription::{self, Subscription}; -/// use iced_futures::futures::channel::mpsc; -/// use iced_futures::futures::sink::SinkExt; -/// -/// pub enum Event { -/// Ready(mpsc::Sender<Input>), -/// WorkFinished, -/// // ... -/// } -/// -/// enum Input { -/// DoSomeWork, -/// // ... -/// } -/// -/// enum State { -/// Starting, -/// Ready(mpsc::Receiver<Input>), -/// } -/// -/// fn some_worker() -> Subscription<Event> { -/// struct SomeWorker; -/// -/// subscription::channel(std::any::TypeId::of::<SomeWorker>(), 100, |mut output| async move { -/// let mut state = State::Starting; -/// -/// loop { -/// match &mut state { -/// State::Starting => { -/// // Create channel -/// let (sender, receiver) = mpsc::channel(100); -/// -/// // Send the sender back to the application -/// output.send(Event::Ready(sender)).await; -/// -/// // We are ready to receive messages -/// state = State::Ready(receiver); -/// } -/// State::Ready(receiver) => { -/// use iced_futures::futures::StreamExt; -/// -/// // Read next input sent from `Application` -/// let input = receiver.select_next_some().await; -/// -/// match input { -/// Input::DoSomeWork => { -/// // Do some async work... -/// -/// // Finally, we can optionally produce a message to tell the -/// // `Application` the work is done -/// output.send(Event::WorkFinished).await; -/// } -/// } -/// } -/// } -/// } -/// }) -/// } -/// ``` -/// -/// Check out the [`websocket`] example, which showcases this pattern to maintain a `WebSocket` -/// connection open. -/// -/// [`websocket`]: https://github.com/iced-rs/iced/tree/0.12/examples/websocket -pub fn channel<I, Fut, Message>( - id: I, - size: usize, - f: impl FnOnce(mpsc::Sender<Message>) -> Fut + MaybeSend + 'static, -) -> Subscription<Message> -where - I: Hash + 'static, - Fut: Future<Output = Never> + MaybeSend + 'static, - Message: 'static + MaybeSend, -{ - use futures::stream::{self, StreamExt}; - - Subscription::from_recipe(Runner { - id, - spawn: move |_| { - let (sender, receiver) = mpsc::channel(size); - - let runner = stream::once(f(sender)).map(|_| unreachable!()); - - stream::select(receiver, runner) - }, - }) -} - -struct Runner<I, F, S, Message> +struct Runner<I, F, S, T> where F: FnOnce(EventStream) -> S, - S: Stream<Item = Message>, + S: Stream<Item = T>, { id: I, spawn: F, } -impl<I, S, F, Message> Recipe for Runner<I, F, S, Message> +impl<I, F, S, T> Recipe for Runner<I, F, S, T> where I: Hash + 'static, F: FnOnce(EventStream) -> S, - S: Stream<Item = Message> + MaybeSend + 'static, + S: Stream<Item = T> + MaybeSend + 'static, { - type Output = Message; + type Output = T; fn hash(&self, state: &mut Hasher) { std::any::TypeId::of::<I>().hash(state); diff --git a/futures/src/subscription/tracker.rs b/futures/src/subscription/tracker.rs index f17e3ea3..6daead24 100644 --- a/futures/src/subscription/tracker.rs +++ b/futures/src/subscription/tracker.rs @@ -42,10 +42,10 @@ impl Tracker { /// method: /// /// - If the provided [`Subscription`] contains a new [`Recipe`] that is - /// currently not being run, it will spawn a new stream and keep it alive. + /// currently not being run, it will spawn a new stream and keep it alive. /// - On the other hand, if a [`Recipe`] is currently in execution and the - /// provided [`Subscription`] does not contain it anymore, then the - /// [`Tracker`] will close and drop the relevant stream. + /// provided [`Subscription`] does not contain it anymore, then the + /// [`Tracker`] will close and drop the relevant stream. /// /// It returns a list of futures that need to be spawned to materialize /// the [`Tracker`] changes. diff --git a/graphics/Cargo.toml b/graphics/Cargo.toml index e8d27d07..7e2d767b 100644 --- a/graphics/Cargo.toml +++ b/graphics/Cargo.toml @@ -20,6 +20,7 @@ all-features = true [features] geometry = ["lyon_path"] image = ["dep:image", "kamadak-exif"] +svg = [] web-colors = [] fira-sans = [] diff --git a/graphics/src/geometry.rs b/graphics/src/geometry.rs index ab4a7a36..2b4b45a6 100644 --- a/graphics/src/geometry.rs +++ b/graphics/src/geometry.rs @@ -16,6 +16,7 @@ pub use stroke::{LineCap, LineDash, LineJoin, Stroke}; pub use style::Style; pub use text::Text; +pub use crate::core::{Image, Svg}; pub use crate::gradient::{self, Gradient}; use crate::cache::Cached; diff --git a/graphics/src/geometry/fill.rs b/graphics/src/geometry/fill.rs index 670fbc12..b79a2582 100644 --- a/graphics/src/geometry/fill.rs +++ b/graphics/src/geometry/fill.rs @@ -7,7 +7,7 @@ use crate::core::Color; use crate::gradient::{self, Gradient}; /// The style used to fill geometry. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Copy)] pub struct Fill { /// The color or gradient of the fill. /// diff --git a/graphics/src/geometry/frame.rs b/graphics/src/geometry/frame.rs index 377589d7..3dee7e75 100644 --- a/graphics/src/geometry/frame.rs +++ b/graphics/src/geometry/frame.rs @@ -1,6 +1,6 @@ //! Draw and generate geometry. use crate::core::{Point, Radians, Rectangle, Size, Vector}; -use crate::geometry::{self, Fill, Path, Stroke, Text}; +use crate::geometry::{self, Fill, Image, Path, Stroke, Svg, Text}; /// The region of a surface that can be used to draw geometry. #[allow(missing_debug_implementations)] @@ -65,6 +65,17 @@ where self.raw.stroke(path, stroke); } + /// Draws the stroke of an axis-aligned rectangle with the provided style + /// given its top-left corner coordinate and its `Size` on the [`Frame`] . + pub fn stroke_rectangle<'a>( + &mut self, + top_left: Point, + size: Size, + stroke: impl Into<Stroke<'a>>, + ) { + self.raw.stroke_rectangle(top_left, size, stroke); + } + /// Draws the characters of the given [`Text`] on the [`Frame`], filling /// them with the given color. /// @@ -75,6 +86,18 @@ where self.raw.fill_text(text); } + /// Draws the given [`Image`] on the [`Frame`] inside the given bounds. + #[cfg(feature = "image")] + pub fn draw_image(&mut self, bounds: Rectangle, image: impl Into<Image>) { + self.raw.draw_image(bounds, image); + } + + /// Draws the given [`Svg`] on the [`Frame`] inside the given bounds. + #[cfg(feature = "svg")] + pub fn draw_svg(&mut self, bounds: Rectangle, svg: impl Into<Svg>) { + self.raw.draw_svg(bounds, svg); + } + /// Stores the current transform of the [`Frame`] and executes the given /// drawing operations, restoring the transform afterwards. /// @@ -116,8 +139,7 @@ where let mut frame = self.draft(region); let result = f(&mut frame); - - self.paste(frame, Point::new(region.x, region.y)); + self.paste(frame); result } @@ -134,8 +156,8 @@ where } /// Draws the contents of the given [`Frame`] with origin at the given [`Point`]. - fn paste(&mut self, frame: Self, at: Point) { - self.raw.paste(frame.raw, at); + fn paste(&mut self, frame: Self) { + self.raw.paste(frame.raw); } /// Applies a translation to the current transform of the [`Frame`]. @@ -186,9 +208,15 @@ pub trait Backend: Sized { fn scale_nonuniform(&mut self, scale: impl Into<Vector>); fn draft(&mut self, clip_bounds: Rectangle) -> Self; - fn paste(&mut self, frame: Self, at: Point); + fn paste(&mut self, frame: Self); fn stroke<'a>(&mut self, path: &Path, stroke: impl Into<Stroke<'a>>); + fn stroke_rectangle<'a>( + &mut self, + top_left: Point, + size: Size, + stroke: impl Into<Stroke<'a>>, + ); fn fill(&mut self, path: &Path, fill: impl Into<Fill>); fn fill_text(&mut self, text: impl Into<Text>); @@ -199,6 +227,9 @@ pub trait Backend: Sized { fill: impl Into<Fill>, ); + fn draw_image(&mut self, bounds: Rectangle, image: impl Into<Image>); + fn draw_svg(&mut self, bounds: Rectangle, svg: impl Into<Svg>); + fn into_geometry(self) -> Self::Geometry; } @@ -231,9 +262,16 @@ impl Backend for () { fn scale_nonuniform(&mut self, _scale: impl Into<Vector>) {} fn draft(&mut self, _clip_bounds: Rectangle) -> Self {} - fn paste(&mut self, _frame: Self, _at: Point) {} + fn paste(&mut self, _frame: Self) {} fn stroke<'a>(&mut self, _path: &Path, _stroke: impl Into<Stroke<'a>>) {} + fn stroke_rectangle<'a>( + &mut self, + _top_left: Point, + _size: Size, + _stroke: impl Into<Stroke<'a>>, + ) { + } fn fill(&mut self, _path: &Path, _fill: impl Into<Fill>) {} fn fill_text(&mut self, _text: impl Into<Text>) {} @@ -245,5 +283,8 @@ impl Backend for () { ) { } + fn draw_image(&mut self, _bounds: Rectangle, _image: impl Into<Image>) {} + fn draw_svg(&mut self, _bounds: Rectangle, _svg: impl Into<Svg>) {} + fn into_geometry(self) -> Self::Geometry {} } diff --git a/graphics/src/geometry/stroke.rs b/graphics/src/geometry/stroke.rs index aff49ab3..b8f4515e 100644 --- a/graphics/src/geometry/stroke.rs +++ b/graphics/src/geometry/stroke.rs @@ -6,7 +6,7 @@ pub use crate::geometry::Style; use iced_core::Color; /// The style of a stroke. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Copy)] pub struct Stroke<'a> { /// The color or gradient of the stroke. /// diff --git a/graphics/src/geometry/style.rs b/graphics/src/geometry/style.rs index a0f4b08a..de77eccc 100644 --- a/graphics/src/geometry/style.rs +++ b/graphics/src/geometry/style.rs @@ -2,7 +2,7 @@ use crate::core::Color; use crate::geometry::Gradient; /// The coloring style of some drawing. -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, Copy, PartialEq)] pub enum Style { /// A solid [`Color`]. Solid(Color), diff --git a/graphics/src/geometry/text.rs b/graphics/src/geometry/text.rs index d314e85e..90147f87 100644 --- a/graphics/src/geometry/text.rs +++ b/graphics/src/geometry/text.rs @@ -43,6 +43,7 @@ impl Text { let mut buffer = cosmic_text::BufferLine::new( &self.content, + cosmic_text::LineEnding::default(), cosmic_text::AttrsList::new(text::to_attributes(self.font)), text::to_shaping(self.shaping), ); @@ -50,8 +51,10 @@ impl Text { let layout = buffer.layout( font_system.raw(), self.size.0, - f32::MAX, + None, cosmic_text::Wrap::None, + None, + 4, ); let translation_x = match self.horizontal_alignment { diff --git a/graphics/src/gradient.rs b/graphics/src/gradient.rs index 603f1b4a..54261721 100644 --- a/graphics/src/gradient.rs +++ b/graphics/src/gradient.rs @@ -9,7 +9,7 @@ use bytemuck::{Pod, Zeroable}; use half::f16; use std::cmp::Ordering; -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, Copy, PartialEq)] /// A fill which linearly interpolates colors along a direction. /// /// For a gradient which can be used as a fill for a background of a widget, see [`crate::core::Gradient`]. diff --git a/graphics/src/image.rs b/graphics/src/image.rs index 318592be..67a5e0cf 100644 --- a/graphics/src/image.rs +++ b/graphics/src/image.rs @@ -2,57 +2,26 @@ #[cfg(feature = "image")] pub use ::image as image_rs; -use crate::core::{image, svg, Color, Radians, Rectangle}; +use crate::core::image; +use crate::core::svg; +use crate::core::Rectangle; /// A raster or vector image. #[derive(Debug, Clone, PartialEq)] pub enum Image { /// A raster image. - Raster { - /// The handle of a raster image. - handle: image::Handle, + Raster(image::Image, Rectangle), - /// The filter method of a raster image. - filter_method: image::FilterMethod, - - /// The bounds of the image. - bounds: Rectangle, - - /// The rotation of the image. - rotation: Radians, - - /// The opacity of the image. - opacity: f32, - }, /// A vector image. - Vector { - /// The handle of a vector image. - handle: svg::Handle, - - /// The [`Color`] filter - color: Option<Color>, - - /// The bounds of the image. - bounds: Rectangle, - - /// The rotation of the image. - rotation: Radians, - - /// The opacity of the image. - opacity: f32, - }, + Vector(svg::Svg, Rectangle), } impl Image { /// Returns the bounds of the [`Image`]. pub fn bounds(&self) -> Rectangle { match self { - Image::Raster { - bounds, rotation, .. - } - | Image::Vector { - bounds, rotation, .. - } => bounds.rotate(*rotation), + Image::Raster(image, bounds) => bounds.rotate(image.rotation), + Image::Vector(svg, bounds) => bounds.rotate(svg.rotation), } } } diff --git a/graphics/src/text.rs b/graphics/src/text.rs index 30269e69..feb9932a 100644 --- a/graphics/src/text.rs +++ b/graphics/src/text.rs @@ -11,7 +11,7 @@ pub use cosmic_text; use crate::core::alignment; use crate::core::font::{self, Font}; -use crate::core::text::Shaping; +use crate::core::text::{Shaping, Wrapping}; use crate::core::{Color, Pixels, Point, Rectangle, Size, Transformation}; use once_cell::sync::OnceCell; @@ -232,13 +232,14 @@ impl PartialEq for Raw { /// Measures the dimensions of the given [`cosmic_text::Buffer`]. pub fn measure(buffer: &cosmic_text::Buffer) -> Size { - let (width, total_lines) = buffer - .layout_runs() - .fold((0.0, 0usize), |(width, total_lines), run| { - (run.line_w.max(width), total_lines + 1) - }); + let (width, height) = + buffer + .layout_runs() + .fold((0.0, 0.0), |(width, height), run| { + (run.line_w.max(width), height + run.line_height) + }); - Size::new(width, total_lines as f32 * buffer.metrics().line_height) + Size::new(width, height) } /// Returns the attributes of the given [`Font`]. @@ -305,6 +306,16 @@ pub fn to_shaping(shaping: Shaping) -> cosmic_text::Shaping { } } +/// Converts some [`Wrapping`] strategy to a [`cosmic_text::Wrap`] strategy. +pub fn to_wrap(wrapping: Wrapping) -> cosmic_text::Wrap { + match wrapping { + Wrapping::None => cosmic_text::Wrap::None, + Wrapping::Word => cosmic_text::Wrap::Word, + Wrapping::Glyph => cosmic_text::Wrap::Glyph, + Wrapping::WordOrGlyph => cosmic_text::Wrap::WordOrGlyph, + } +} + /// Converts some [`Color`] to a [`cosmic_text::Color`]. pub fn to_color(color: Color) -> cosmic_text::Color { let [r, g, b, a] = color.into_rgba8(); diff --git a/graphics/src/text/cache.rs b/graphics/src/text/cache.rs index 822b61c4..e64d93f1 100644 --- a/graphics/src/text/cache.rs +++ b/graphics/src/text/cache.rs @@ -48,8 +48,8 @@ impl Cache { buffer.set_size( font_system, - key.bounds.width, - key.bounds.height.max(key.line_height), + Some(key.bounds.width), + Some(key.bounds.height.max(key.line_height)), ); buffer.set_text( font_system, diff --git a/graphics/src/text/editor.rs b/graphics/src/text/editor.rs index c488a51c..1f1d0050 100644 --- a/graphics/src/text/editor.rs +++ b/graphics/src/text/editor.rs @@ -3,7 +3,7 @@ use crate::core::text::editor::{ self, Action, Cursor, Direction, Edit, Motion, }; use crate::core::text::highlighter::{self, Highlighter}; -use crate::core::text::LineHeight; +use crate::core::text::{LineHeight, Wrapping}; use crate::core::{Font, Pixels, Point, Rectangle, Size}; use crate::text; @@ -17,7 +17,7 @@ use std::sync::{self, Arc}; pub struct Editor(Option<Arc<Internal>>); struct Internal { - editor: cosmic_text::Editor, + editor: cosmic_text::Editor<'static>, font: Font, bounds: Size, topmost_line_changed: Option<usize>, @@ -32,7 +32,7 @@ impl Editor { /// Returns the buffer of the [`Editor`]. pub fn buffer(&self) -> &cosmic_text::Buffer { - self.internal().editor.buffer() + buffer_from_editor(&self.internal().editor) } /// Creates a [`Weak`] reference to the [`Editor`]. @@ -82,6 +82,13 @@ impl editor::Editor for Editor { }))) } + fn is_empty(&self) -> bool { + let buffer = self.buffer(); + + buffer.lines.is_empty() + || (buffer.lines.len() == 1 && buffer.lines[0].text().is_empty()) + } + fn line(&self, index: usize) -> Option<&str> { self.buffer() .lines @@ -101,16 +108,10 @@ impl editor::Editor for Editor { let internal = self.internal(); let cursor = internal.editor.cursor(); - let buffer = internal.editor.buffer(); - - match internal.editor.select_opt() { - Some(selection) => { - let (start, end) = if cursor < selection { - (cursor, selection) - } else { - (selection, cursor) - }; + let buffer = buffer_from_editor(&internal.editor); + match internal.editor.selection_bounds() { + Some((start, end)) => { let line_height = buffer.metrics().line_height; let selected_lines = end.line - start.line + 1; @@ -142,7 +143,8 @@ impl editor::Editor for Editor { width, y: (visual_line as i32 + visual_lines_offset) as f32 - * line_height, + * line_height + - buffer.scroll().vertical, height: line_height, }) } else { @@ -224,7 +226,8 @@ impl editor::Editor for Editor { Cursor::Caret(Point::new( offset, (visual_lines_offset + visual_line as i32) as f32 - * line_height, + * line_height + - buffer.scroll().vertical, )) } } @@ -252,16 +255,8 @@ impl editor::Editor for Editor { match action { // Motion events Action::Move(motion) => { - if let Some(selection) = editor.select_opt() { - let cursor = editor.cursor(); - - let (left, right) = if cursor < selection { - (cursor, selection) - } else { - (selection, cursor) - }; - - editor.set_select_opt(None); + if let Some((start, end)) = editor.selection_bounds() { + editor.set_selection(cosmic_text::Selection::None); match motion { // These motions are performed as-is even when a selection @@ -272,17 +267,20 @@ impl editor::Editor for Editor { | Motion::DocumentEnd => { editor.action( font_system.raw(), - motion_to_action(motion), + cosmic_text::Action::Motion(to_motion(motion)), ); } // Other motions simply move the cursor to one end of the selection _ => editor.set_cursor(match motion.direction() { - Direction::Left => left, - Direction::Right => right, + Direction::Left => start, + Direction::Right => end, }), } } else { - editor.action(font_system.raw(), motion_to_action(motion)); + editor.action( + font_system.raw(), + cosmic_text::Action::Motion(to_motion(motion)), + ); } } @@ -290,99 +288,58 @@ impl editor::Editor for Editor { Action::Select(motion) => { let cursor = editor.cursor(); - if editor.select_opt().is_none() { - editor.set_select_opt(Some(cursor)); + if editor.selection_bounds().is_none() { + editor + .set_selection(cosmic_text::Selection::Normal(cursor)); } - editor.action(font_system.raw(), motion_to_action(motion)); + editor.action( + font_system.raw(), + cosmic_text::Action::Motion(to_motion(motion)), + ); // Deselect if selection matches cursor position - if let Some(selection) = editor.select_opt() { - let cursor = editor.cursor(); - - if cursor.line == selection.line - && cursor.index == selection.index - { - editor.set_select_opt(None); + if let Some((start, end)) = editor.selection_bounds() { + if start.line == end.line && start.index == end.index { + editor.set_selection(cosmic_text::Selection::None); } } } Action::SelectWord => { - use unicode_segmentation::UnicodeSegmentation; - let cursor = editor.cursor(); - if let Some(line) = editor.buffer().lines.get(cursor.line) { - let (start, end) = - UnicodeSegmentation::unicode_word_indices(line.text()) - // Split words with dots - .flat_map(|(i, word)| { - word.split('.').scan(i, |current, word| { - let start = *current; - *current += word.len() + 1; - - Some((start, word)) - }) - }) - // Turn words into ranges - .map(|(i, word)| (i, i + word.len())) - // Find the word at cursor - .find(|&(start, end)| { - start <= cursor.index && cursor.index < end - }) - // Cursor is not in a word. Let's select its punctuation cluster. - .unwrap_or_else(|| { - let start = line.text()[..cursor.index] - .char_indices() - .rev() - .take_while(|(_, c)| { - c.is_ascii_punctuation() - }) - .map(|(i, _)| i) - .last() - .unwrap_or(cursor.index); - - let end = line.text()[cursor.index..] - .char_indices() - .skip_while(|(_, c)| { - c.is_ascii_punctuation() - }) - .map(|(i, _)| i + cursor.index) - .next() - .unwrap_or(cursor.index); - - (start, end) - }); - - if start != end { - editor.set_cursor(cosmic_text::Cursor { - index: start, - ..cursor - }); - - editor.set_select_opt(Some(cosmic_text::Cursor { - index: end, - ..cursor - })); - } - } + editor.set_selection(cosmic_text::Selection::Word(cursor)); } Action::SelectLine => { let cursor = editor.cursor(); - if let Some(line_length) = editor - .buffer() - .lines - .get(cursor.line) - .map(|line| line.text().len()) + editor.set_selection(cosmic_text::Selection::Line(cursor)); + } + Action::SelectAll => { + let buffer = buffer_from_editor(editor); + + if buffer.lines.len() > 1 + || buffer + .lines + .first() + .is_some_and(|line| !line.text().is_empty()) { - editor - .set_cursor(cosmic_text::Cursor { index: 0, ..cursor }); + let cursor = editor.cursor(); + + editor.set_selection(cosmic_text::Selection::Normal( + cosmic_text::Cursor { + line: 0, + index: 0, + ..cursor + }, + )); - editor.set_select_opt(Some(cosmic_text::Cursor { - index: line_length, - ..cursor - })); + editor.action( + font_system.raw(), + cosmic_text::Action::Motion( + cosmic_text::Motion::BufferEnd, + ), + ); } } @@ -419,10 +376,12 @@ impl editor::Editor for Editor { } let cursor = editor.cursor(); - let selection = editor.select_opt().unwrap_or(cursor); + let selection_start = editor + .selection_bounds() + .map(|(start, _)| start) + .unwrap_or(cursor); - internal.topmost_line_changed = - Some(cursor.min(selection).line); + internal.topmost_line_changed = Some(selection_start.line); } // Mouse events @@ -445,13 +404,9 @@ impl editor::Editor for Editor { ); // Deselect if selection matches cursor position - if let Some(selection) = editor.select_opt() { - let cursor = editor.cursor(); - - if cursor.line == selection.line - && cursor.index == selection.index - { - editor.set_select_opt(None); + if let Some((start, end)) = editor.selection_bounds() { + if start.line == end.line && start.index == end.index { + editor.set_selection(cosmic_text::Selection::None); } } } @@ -473,7 +428,7 @@ impl editor::Editor for Editor { fn min_bounds(&self) -> Size { let internal = self.internal(); - text::measure(internal.editor.buffer()) + text::measure(buffer_from_editor(&internal.editor)) } fn update( @@ -482,6 +437,7 @@ impl editor::Editor for Editor { new_font: Font, new_size: Pixels, new_line_height: LineHeight, + new_wrapping: Wrapping, new_highlighter: &mut impl Highlighter, ) { let editor = @@ -493,10 +449,12 @@ impl editor::Editor for Editor { let mut font_system = text::font_system().write().expect("Write font system"); + let buffer = buffer_mut_from_editor(&mut internal.editor); + if font_system.version() != internal.version { log::trace!("Updating `FontSystem` of `Editor`..."); - for line in internal.editor.buffer_mut().lines.iter_mut() { + for line in buffer.lines.iter_mut() { line.reset(); } @@ -507,7 +465,7 @@ impl editor::Editor for Editor { if new_font != internal.font { log::trace!("Updating font of `Editor`..."); - for line in internal.editor.buffer_mut().lines.iter_mut() { + for line in buffer.lines.iter_mut() { let _ = line.set_attrs_list(cosmic_text::AttrsList::new( text::to_attributes(new_font), )); @@ -517,7 +475,7 @@ impl editor::Editor for Editor { internal.topmost_line_changed = Some(0); } - let metrics = internal.editor.buffer().metrics(); + let metrics = buffer.metrics(); let new_line_height = new_line_height.to_absolute(new_size); if new_size.0 != metrics.font_size @@ -525,19 +483,27 @@ impl editor::Editor for Editor { { log::trace!("Updating `Metrics` of `Editor`..."); - internal.editor.buffer_mut().set_metrics( + buffer.set_metrics( font_system.raw(), cosmic_text::Metrics::new(new_size.0, new_line_height.0), ); } + let new_wrap = text::to_wrap(new_wrapping); + + if new_wrap != buffer.wrap() { + log::trace!("Updating `Wrap` strategy of `Editor`..."); + + buffer.set_wrap(font_system.raw(), new_wrap); + } + if new_bounds != internal.bounds { log::trace!("Updating size of `Editor`..."); - internal.editor.buffer_mut().set_size( + buffer.set_size( font_system.raw(), - new_bounds.width, - new_bounds.height, + Some(new_bounds.width), + Some(new_bounds.height), ); internal.bounds = new_bounds; @@ -552,7 +518,7 @@ impl editor::Editor for Editor { new_highlighter.change_line(topmost_line_changed); } - internal.editor.shape_as_needed(font_system.raw()); + internal.editor.shape_as_needed(font_system.raw(), false); self.0 = Some(Arc::new(internal)); } @@ -564,12 +530,13 @@ impl editor::Editor for Editor { format_highlight: impl Fn(&H::Highlight) -> highlighter::Format<Self::Font>, ) { let internal = self.internal(); - let buffer = internal.editor.buffer(); + let buffer = buffer_from_editor(&internal.editor); - let mut window = buffer.scroll() + buffer.visible_lines(); + let scroll = buffer.scroll(); + let mut window = (internal.bounds.height / buffer.metrics().line_height) + .ceil() as i32; - let last_visible_line = buffer - .lines + let last_visible_line = buffer.lines[scroll.line..] .iter() .enumerate() .find_map(|(i, line)| { @@ -583,7 +550,7 @@ impl editor::Editor for Editor { window -= visible_lines; None } else { - Some(i) + Some(scroll.line + i) } }) .unwrap_or(buffer.lines.len().saturating_sub(1)); @@ -605,7 +572,7 @@ impl editor::Editor for Editor { let attributes = text::to_attributes(font); - for line in &mut internal.editor.buffer_mut().lines + for line in &mut buffer_mut_from_editor(&mut internal.editor).lines [current_line..=last_visible_line] { let mut list = cosmic_text::AttrsList::new(attributes); @@ -631,7 +598,7 @@ impl editor::Editor for Editor { let _ = line.set_attrs_list(list); } - internal.editor.shape_as_needed(font_system.raw()); + internal.editor.shape_as_needed(font_system.raw(), false); self.0 = Some(Arc::new(internal)); } @@ -647,7 +614,8 @@ impl PartialEq for Internal { fn eq(&self, other: &Self) -> bool { self.font == other.font && self.bounds == other.bounds - && self.editor.buffer().metrics() == other.editor.buffer().metrics() + && buffer_from_editor(&self.editor).metrics() + == buffer_from_editor(&other.editor).metrics() } } @@ -709,7 +677,8 @@ fn highlight_line( let layout = line .layout_opt() .as_ref() - .expect("Line layout should be cached"); + .map(Vec::as_slice) + .unwrap_or_default(); layout.iter().map(move |visual_line| { let start = visual_line @@ -752,34 +721,61 @@ fn highlight_line( } fn visual_lines_offset(line: usize, buffer: &cosmic_text::Buffer) -> i32 { - let visual_lines_before_start: usize = buffer - .lines + let scroll = buffer.scroll(); + + let start = scroll.line.min(line); + let end = scroll.line.max(line); + + let visual_lines_offset: usize = buffer.lines[start..] .iter() - .take(line) + .take(end - start) .map(|line| { - line.layout_opt() - .as_ref() - .expect("Line layout should be cached") - .len() + line.layout_opt().as_ref().map(Vec::len).unwrap_or_default() }) .sum(); - visual_lines_before_start as i32 - buffer.scroll() + visual_lines_offset as i32 * if scroll.line < line { 1 } else { -1 } } -fn motion_to_action(motion: Motion) -> cosmic_text::Action { +fn to_motion(motion: Motion) -> cosmic_text::Motion { match motion { - Motion::Left => cosmic_text::Action::Left, - Motion::Right => cosmic_text::Action::Right, - Motion::Up => cosmic_text::Action::Up, - Motion::Down => cosmic_text::Action::Down, - Motion::WordLeft => cosmic_text::Action::LeftWord, - Motion::WordRight => cosmic_text::Action::RightWord, - Motion::Home => cosmic_text::Action::Home, - Motion::End => cosmic_text::Action::End, - Motion::PageUp => cosmic_text::Action::PageUp, - Motion::PageDown => cosmic_text::Action::PageDown, - Motion::DocumentStart => cosmic_text::Action::BufferStart, - Motion::DocumentEnd => cosmic_text::Action::BufferEnd, + Motion::Left => cosmic_text::Motion::Left, + Motion::Right => cosmic_text::Motion::Right, + Motion::Up => cosmic_text::Motion::Up, + Motion::Down => cosmic_text::Motion::Down, + Motion::WordLeft => cosmic_text::Motion::LeftWord, + Motion::WordRight => cosmic_text::Motion::RightWord, + Motion::Home => cosmic_text::Motion::Home, + Motion::End => cosmic_text::Motion::End, + Motion::PageUp => cosmic_text::Motion::PageUp, + Motion::PageDown => cosmic_text::Motion::PageDown, + Motion::DocumentStart => cosmic_text::Motion::BufferStart, + Motion::DocumentEnd => cosmic_text::Motion::BufferEnd, + } +} + +fn buffer_from_editor<'a, 'b>( + editor: &'a impl cosmic_text::Edit<'b>, +) -> &'a cosmic_text::Buffer +where + 'b: 'a, +{ + match editor.buffer_ref() { + cosmic_text::BufferRef::Owned(buffer) => buffer, + cosmic_text::BufferRef::Borrowed(buffer) => buffer, + cosmic_text::BufferRef::Arc(buffer) => buffer, + } +} + +fn buffer_mut_from_editor<'a, 'b>( + editor: &'a mut impl cosmic_text::Edit<'b>, +) -> &'a mut cosmic_text::Buffer +where + 'b: 'a, +{ + match editor.buffer_ref_mut() { + cosmic_text::BufferRef::Owned(buffer) => buffer, + cosmic_text::BufferRef::Borrowed(buffer) => buffer, + cosmic_text::BufferRef::Arc(_buffer) => unreachable!(), } } diff --git a/graphics/src/text/paragraph.rs b/graphics/src/text/paragraph.rs index 31a323ac..07ddbb82 100644 --- a/graphics/src/text/paragraph.rs +++ b/graphics/src/text/paragraph.rs @@ -1,8 +1,8 @@ //! Draw paragraphs. use crate::core; use crate::core::alignment; -use crate::core::text::{Hit, LineHeight, Shaping, Text}; -use crate::core::{Font, Pixels, Point, Size}; +use crate::core::text::{Hit, Shaping, Span, Text, Wrapping}; +use crate::core::{Font, Point, Rectangle, Size}; use crate::text; use std::fmt; @@ -10,13 +10,14 @@ use std::sync::{self, Arc}; /// A bunch of text. #[derive(Clone, PartialEq)] -pub struct Paragraph(Option<Arc<Internal>>); +pub struct Paragraph(Arc<Internal>); +#[derive(Clone)] struct Internal { buffer: cosmic_text::Buffer, - content: String, // TODO: Reuse from `buffer` (?) font: Font, shaping: Shaping, + wrapping: Wrapping, horizontal_alignment: alignment::Horizontal, vertical_alignment: alignment::Vertical, bounds: Size, @@ -52,9 +53,7 @@ impl Paragraph { } fn internal(&self) -> &Arc<Internal> { - self.0 - .as_ref() - .expect("paragraph should always be initialized") + &self.0 } } @@ -62,7 +61,7 @@ impl core::text::Paragraph for Paragraph { type Font = Font; fn with_text(text: Text<&str>) -> Self { - log::trace!("Allocating paragraph: {}", text.content); + log::trace!("Allocating plain paragraph: {}", text.content); let mut font_system = text::font_system().write().expect("Write font system"); @@ -77,8 +76,8 @@ impl core::text::Paragraph for Paragraph { buffer.set_size( font_system.raw(), - text.bounds.width, - text.bounds.height, + Some(text.bounds.width), + Some(text.bounds.height), ); buffer.set_text( @@ -90,73 +89,113 @@ impl core::text::Paragraph for Paragraph { let min_bounds = text::measure(&buffer); - Self(Some(Arc::new(Internal { + Self(Arc::new(Internal { buffer, - content: text.content.to_owned(), font: text.font, horizontal_alignment: text.horizontal_alignment, vertical_alignment: text.vertical_alignment, shaping: text.shaping, + wrapping: text.wrapping, bounds: text.bounds, min_bounds, version: font_system.version(), - }))) + })) + } + + fn with_spans<Link>(text: Text<&[Span<'_, Link>]>) -> Self { + log::trace!("Allocating rich paragraph: {} spans", text.content.len()); + + let mut font_system = + text::font_system().write().expect("Write font system"); + + let mut buffer = cosmic_text::Buffer::new( + font_system.raw(), + cosmic_text::Metrics::new( + text.size.into(), + text.line_height.to_absolute(text.size).into(), + ), + ); + + buffer.set_size( + font_system.raw(), + Some(text.bounds.width), + Some(text.bounds.height), + ); + + buffer.set_rich_text( + font_system.raw(), + text.content.iter().enumerate().map(|(i, span)| { + let attrs = text::to_attributes(span.font.unwrap_or(text.font)); + + let attrs = match (span.size, span.line_height) { + (None, None) => attrs, + _ => { + let size = span.size.unwrap_or(text.size); + + attrs.metrics(cosmic_text::Metrics::new( + size.into(), + span.line_height + .unwrap_or(text.line_height) + .to_absolute(size) + .into(), + )) + } + }; + + let attrs = if let Some(color) = span.color { + attrs.color(text::to_color(color)) + } else { + attrs + }; + + (span.text.as_ref(), attrs.metadata(i)) + }), + text::to_attributes(text.font), + text::to_shaping(text.shaping), + ); + + let min_bounds = text::measure(&buffer); + + Self(Arc::new(Internal { + buffer, + font: text.font, + horizontal_alignment: text.horizontal_alignment, + vertical_alignment: text.vertical_alignment, + shaping: text.shaping, + wrapping: text.wrapping, + bounds: text.bounds, + min_bounds, + version: font_system.version(), + })) } fn resize(&mut self, new_bounds: Size) { - let paragraph = self - .0 - .take() - .expect("paragraph should always be initialized"); - - match Arc::try_unwrap(paragraph) { - Ok(mut internal) => { - let mut font_system = - text::font_system().write().expect("Write font system"); - - internal.buffer.set_size( - font_system.raw(), - new_bounds.width, - new_bounds.height, - ); - - internal.bounds = new_bounds; - internal.min_bounds = text::measure(&internal.buffer); - - self.0 = Some(Arc::new(internal)); - } - Err(internal) => { - let metrics = internal.buffer.metrics(); - - // If there is a strong reference somewhere, we recompute the - // buffer from scratch - *self = Self::with_text(Text { - content: &internal.content, - bounds: internal.bounds, - size: Pixels(metrics.font_size), - line_height: LineHeight::Absolute(Pixels( - metrics.line_height, - )), - font: internal.font, - horizontal_alignment: internal.horizontal_alignment, - vertical_alignment: internal.vertical_alignment, - shaping: internal.shaping, - }); - } - } + let paragraph = Arc::make_mut(&mut self.0); + + let mut font_system = + text::font_system().write().expect("Write font system"); + + paragraph.buffer.set_size( + font_system.raw(), + Some(new_bounds.width), + Some(new_bounds.height), + ); + + paragraph.bounds = new_bounds; + paragraph.min_bounds = text::measure(¶graph.buffer); } - fn compare(&self, text: Text<&str>) -> core::text::Difference { + fn compare(&self, text: Text<()>) -> core::text::Difference { let font_system = text::font_system().read().expect("Read font system"); let paragraph = self.internal(); let metrics = paragraph.buffer.metrics(); if paragraph.version != font_system.version - || paragraph.content != text.content || metrics.font_size != text.size.0 || metrics.line_height != text.line_height.to_absolute(text.size).0 || paragraph.font != text.font || paragraph.shaping != text.shaping + || paragraph.wrapping != text.wrapping || paragraph.horizontal_alignment != text.horizontal_alignment || paragraph.vertical_alignment != text.vertical_alignment { @@ -186,6 +225,87 @@ impl core::text::Paragraph for Paragraph { Some(Hit::CharOffset(cursor.index)) } + fn hit_span(&self, point: Point) -> Option<usize> { + let internal = self.internal(); + + let cursor = internal.buffer.hit(point.x, point.y)?; + let line = internal.buffer.lines.get(cursor.line)?; + + let mut last_glyph = None; + let mut glyphs = line + .layout_opt() + .as_ref()? + .iter() + .flat_map(|line| line.glyphs.iter()) + .peekable(); + + while let Some(glyph) = glyphs.peek() { + if glyph.start <= cursor.index && cursor.index < glyph.end { + break; + } + + last_glyph = glyphs.next(); + } + + let glyph = match cursor.affinity { + cosmic_text::Affinity::Before => last_glyph, + cosmic_text::Affinity::After => glyphs.next(), + }?; + + Some(glyph.metadata) + } + + fn span_bounds(&self, index: usize) -> Vec<Rectangle> { + let internal = self.internal(); + + let mut bounds = Vec::new(); + let mut current_bounds = None; + + let glyphs = internal + .buffer + .layout_runs() + .flat_map(|run| { + let line_top = run.line_top; + let line_height = run.line_height; + + run.glyphs + .iter() + .map(move |glyph| (line_top, line_height, glyph)) + }) + .skip_while(|(_, _, glyph)| glyph.metadata != index) + .take_while(|(_, _, glyph)| glyph.metadata == index); + + for (line_top, line_height, glyph) in glyphs { + let y = line_top + glyph.y; + + let new_bounds = || { + Rectangle::new( + Point::new(glyph.x, y), + Size::new( + glyph.w, + glyph.line_height_opt.unwrap_or(line_height), + ), + ) + }; + + match current_bounds.as_mut() { + None => { + current_bounds = Some(new_bounds()); + } + Some(current_bounds) if y != current_bounds.y => { + bounds.push(*current_bounds); + *current_bounds = new_bounds(); + } + Some(current_bounds) => { + current_bounds.width += glyph.w; + } + } + } + + bounds.extend(current_bounds); + bounds + } + fn grapheme_position(&self, line: usize, index: usize) -> Option<Point> { use unicode_segmentation::UnicodeSegmentation; @@ -231,7 +351,7 @@ impl core::text::Paragraph for Paragraph { impl Default for Paragraph { fn default() -> Self { - Self(Some(Arc::new(Internal::default()))) + Self(Arc::new(Internal::default())) } } @@ -240,7 +360,6 @@ impl fmt::Debug for Paragraph { let paragraph = self.internal(); f.debug_struct("Paragraph") - .field("content", ¶graph.content) .field("font", ¶graph.font) .field("shaping", ¶graph.shaping) .field("horizontal_alignment", ¶graph.horizontal_alignment) @@ -253,8 +372,7 @@ impl fmt::Debug for Paragraph { impl PartialEq for Internal { fn eq(&self, other: &Self) -> bool { - self.content == other.content - && self.font == other.font + self.font == other.font && self.shaping == other.shaping && self.horizontal_alignment == other.horizontal_alignment && self.vertical_alignment == other.vertical_alignment @@ -271,9 +389,9 @@ impl Default for Internal { font_size: 1.0, line_height: 1.0, }), - content: String::new(), font: Font::default(), shaping: Shaping::default(), + wrapping: Wrapping::default(), horizontal_alignment: alignment::Horizontal::Left, vertical_alignment: alignment::Vertical::Top, bounds: Size::ZERO, @@ -298,7 +416,7 @@ pub struct Weak { impl Weak { /// Tries to update the reference into a [`Paragraph`]. pub fn upgrade(&self) -> Option<Paragraph> { - self.raw.upgrade().map(Some).map(Paragraph) + self.raw.upgrade().map(Paragraph) } } diff --git a/highlighter/src/lib.rs b/highlighter/src/lib.rs index 7636a712..83a15cb1 100644 --- a/highlighter/src/lib.rs +++ b/highlighter/src/lib.rs @@ -1,8 +1,9 @@ //! A syntax highlighter for iced. use iced_core as core; +use crate::core::font::{self, Font}; use crate::core::text::highlighter::{self, Format}; -use crate::core::{Color, Font}; +use crate::core::Color; use once_cell::sync::Lazy; use std::ops::Range; @@ -35,7 +36,7 @@ impl highlighter::Highlighter for Highlighter { fn new(settings: &Self::Settings) -> Self { let syntax = SYNTAXES - .find_syntax_by_token(&settings.extension) + .find_syntax_by_token(&settings.token) .unwrap_or_else(|| SYNTAXES.find_syntax_plain_text()); let highlighter = highlighting::Highlighter::new( @@ -55,7 +56,7 @@ impl highlighter::Highlighter for Highlighter { fn update(&mut self, new_settings: &Self::Settings) { self.syntax = SYNTAXES - .find_syntax_by_token(&new_settings.extension) + .find_syntax_by_token(&new_settings.token) .unwrap_or_else(|| SYNTAXES.find_syntax_plain_text()); self.highlighter = highlighting::Highlighter::new( @@ -141,11 +142,11 @@ pub struct Settings { /// /// It dictates the color scheme that will be used for highlighting. pub theme: Theme, - /// The extension of the file to highlight. + /// The extension of the file or the name of the language to highlight. /// - /// The [`Highlighter`] will use the extension to automatically determine + /// The [`Highlighter`] will use the token to automatically determine /// the grammar to use for highlighting. - pub extension: String, + pub token: String, } /// A highlight produced by a [`Highlighter`]. @@ -166,7 +167,28 @@ impl Highlight { /// /// If `None`, the original font should be unchanged. pub fn font(&self) -> Option<Font> { - None + self.0.font_style.and_then(|style| { + let bold = style.contains(highlighting::FontStyle::BOLD); + let italic = style.contains(highlighting::FontStyle::ITALIC); + + if bold || italic { + Some(Font { + weight: if bold { + font::Weight::Bold + } else { + font::Weight::Normal + }, + style: if italic { + font::Style::Italic + } else { + font::Style::Normal + }, + ..Font::MONOSPACE + }) + } else { + None + } + }) } /// Returns the [`Format`] of the [`Highlight`]. diff --git a/renderer/src/fallback.rs b/renderer/src/fallback.rs index 6a169692..8cb18bde 100644 --- a/renderer/src/fallback.rs +++ b/renderer/src/fallback.rs @@ -3,7 +3,7 @@ use crate::core::image; use crate::core::renderer; use crate::core::svg; use crate::core::{ - self, Background, Color, Point, Radians, Rectangle, Size, Transformation, + self, Background, Color, Image, Point, Rectangle, Size, Svg, Transformation, }; use crate::graphics; use crate::graphics::compositor; @@ -149,25 +149,8 @@ where delegate!(self, renderer, renderer.measure_image(handle)) } - fn draw_image( - &mut self, - handle: Self::Handle, - filter_method: image::FilterMethod, - bounds: Rectangle, - rotation: Radians, - opacity: f32, - ) { - delegate!( - self, - renderer, - renderer.draw_image( - handle, - filter_method, - bounds, - rotation, - opacity - ) - ); + fn draw_image(&mut self, image: Image<A::Handle>, bounds: Rectangle) { + delegate!(self, renderer, renderer.draw_image(image, bounds)); } } @@ -180,19 +163,8 @@ where delegate!(self, renderer, renderer.measure_svg(handle)) } - fn draw_svg( - &mut self, - handle: svg::Handle, - color: Option<Color>, - bounds: Rectangle, - rotation: Radians, - opacity: f32, - ) { - delegate!( - self, - renderer, - renderer.draw_svg(handle, color, bounds, rotation, opacity) - ); + fn draw_svg(&mut self, svg: Svg, bounds: Rectangle) { + delegate!(self, renderer, renderer.draw_svg(svg, bounds)); } } @@ -441,9 +413,9 @@ where #[cfg(feature = "geometry")] mod geometry { use super::Renderer; - use crate::core::{Point, Radians, Rectangle, Size, Vector}; + use crate::core::{Point, Radians, Rectangle, Size, Svg, Vector}; use crate::graphics::cache::{self, Cached}; - use crate::graphics::geometry::{self, Fill, Path, Stroke, Text}; + use crate::graphics::geometry::{self, Fill, Image, Path, Stroke, Text}; impl<A, B> geometry::Renderer for Renderer<A, B> where @@ -568,10 +540,31 @@ mod geometry { delegate!(self, frame, frame.stroke(path, stroke)); } + fn stroke_rectangle<'a>( + &mut self, + top_left: Point, + size: Size, + stroke: impl Into<Stroke<'a>>, + ) { + delegate!( + self, + frame, + frame.stroke_rectangle(top_left, size, stroke) + ); + } + fn fill_text(&mut self, text: impl Into<Text>) { delegate!(self, frame, frame.fill_text(text)); } + fn draw_image(&mut self, bounds: Rectangle, image: impl Into<Image>) { + delegate!(self, frame, frame.draw_image(bounds, image)); + } + + fn draw_svg(&mut self, bounds: Rectangle, svg: impl Into<Svg>) { + delegate!(self, frame, frame.draw_svg(bounds, svg)); + } + fn push_transform(&mut self) { delegate!(self, frame, frame.push_transform()); } @@ -587,13 +580,13 @@ mod geometry { } } - fn paste(&mut self, frame: Self, at: Point) { + fn paste(&mut self, frame: Self) { match (self, frame) { (Self::Primary(target), Self::Primary(source)) => { - target.paste(source, at); + target.paste(source); } (Self::Secondary(target), Self::Secondary(source)) => { - target.paste(source, at); + target.paste(source); } _ => unreachable!(), } diff --git a/runtime/src/clipboard.rs b/runtime/src/clipboard.rs index 19950d01..a02cc011 100644 --- a/runtime/src/clipboard.rs +++ b/runtime/src/clipboard.rs @@ -1,7 +1,7 @@ //! Access the clipboard. use crate::core::clipboard::Kind; use crate::futures::futures::channel::oneshot; -use crate::Task; +use crate::task::{self, Task}; /// A clipboard action to be performed by some [`Task`]. /// @@ -27,7 +27,7 @@ pub enum Action { /// Read the current contents of the clipboard. pub fn read() -> Task<Option<String>> { - Task::oneshot(|channel| { + task::oneshot(|channel| { crate::Action::Clipboard(Action::Read { target: Kind::Standard, channel, @@ -37,7 +37,7 @@ pub fn read() -> Task<Option<String>> { /// Read the current contents of the primary clipboard. pub fn read_primary() -> Task<Option<String>> { - Task::oneshot(|channel| { + task::oneshot(|channel| { crate::Action::Clipboard(Action::Read { target: Kind::Primary, channel, @@ -47,7 +47,7 @@ pub fn read_primary() -> Task<Option<String>> { /// Write the given contents to the clipboard. pub fn write<T>(contents: String) -> Task<T> { - Task::effect(crate::Action::Clipboard(Action::Write { + task::effect(crate::Action::Clipboard(Action::Write { target: Kind::Standard, contents, })) @@ -55,7 +55,7 @@ pub fn write<T>(contents: String) -> Task<T> { /// Write the given contents to the primary clipboard. pub fn write_primary<Message>(contents: String) -> Task<Message> { - Task::effect(crate::Action::Clipboard(Action::Write { + task::effect(crate::Action::Clipboard(Action::Write { target: Kind::Primary, contents, })) diff --git a/runtime/src/font.rs b/runtime/src/font.rs index d54eb6a8..75fdfc11 100644 --- a/runtime/src/font.rs +++ b/runtime/src/font.rs @@ -1,5 +1,6 @@ //! Load and use fonts. -use crate::{Action, Task}; +use crate::task::{self, Task}; +use crate::Action; use std::borrow::Cow; /// An error while loading a font. @@ -8,7 +9,7 @@ pub enum Error {} /// Load a font from its bytes. pub fn load(bytes: impl Into<Cow<'static, [u8]>>) -> Task<Result<(), Error>> { - Task::oneshot(|channel| Action::LoadFont { + task::oneshot(|channel| Action::LoadFont { bytes: bytes.into(), channel, }) diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index b4a5e819..7230fc73 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -15,14 +15,13 @@ pub mod keyboard; pub mod overlay; pub mod program; pub mod system; +pub mod task; pub mod user_interface; pub mod window; #[cfg(feature = "multi-window")] pub mod multi_window; -mod task; - // We disable debug capabilities on release builds unless the `debug` feature // is explicitly enabled. #[cfg(feature = "debug")] @@ -60,7 +59,7 @@ pub enum Action<T> { }, /// Run a widget operation. - Widget(Box<dyn widget::Operation<()> + Send>), + Widget(Box<dyn widget::Operation>), /// Run a clipboard action. Clipboard(clipboard::Action), @@ -80,7 +79,7 @@ pub enum Action<T> { impl<T> Action<T> { /// Creates a new [`Action::Widget`] with the given [`widget::Operation`]. - pub fn widget(operation: impl widget::Operation<()> + 'static) -> Self { + pub fn widget(operation: impl widget::Operation + 'static) -> Self { Self::Widget(Box::new(operation)) } @@ -127,5 +126,5 @@ where /// This will normally close any application windows and /// terminate the runtime loop. pub fn exit<T>() -> Task<T> { - Task::effect(Action::Exit) + task::effect(Action::Exit) } diff --git a/runtime/src/multi_window/state.rs b/runtime/src/multi_window/state.rs index 72ce6933..0bec555f 100644 --- a/runtime/src/multi_window/state.rs +++ b/runtime/src/multi_window/state.rs @@ -205,7 +205,7 @@ where pub fn operate( &mut self, renderer: &mut P::Renderer, - operations: impl Iterator<Item = Box<dyn Operation<()>>>, + operations: impl Iterator<Item = Box<dyn Operation>>, bounds: Size, debug: &mut Debug, ) { diff --git a/runtime/src/overlay/nested.rs b/runtime/src/overlay/nested.rs index 11eee41c..da3e6929 100644 --- a/runtime/src/overlay/nested.rs +++ b/runtime/src/overlay/nested.rs @@ -131,13 +131,13 @@ where &mut self, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn widget::Operation<()>, + operation: &mut dyn widget::Operation, ) { fn recurse<Message, Theme, Renderer>( element: &mut overlay::Element<'_, Message, Theme, Renderer>, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn widget::Operation<()>, + operation: &mut dyn widget::Operation, ) where Renderer: renderer::Renderer, { diff --git a/runtime/src/program/state.rs b/runtime/src/program/state.rs index e51ad0cb..c377814a 100644 --- a/runtime/src/program/state.rs +++ b/runtime/src/program/state.rs @@ -178,7 +178,7 @@ where pub fn operate( &mut self, renderer: &mut P::Renderer, - operations: impl Iterator<Item = Box<dyn Operation<()>>>, + operations: impl Iterator<Item = Box<dyn Operation>>, bounds: Size, debug: &mut Debug, ) { diff --git a/runtime/src/task.rs b/runtime/src/task.rs index b8a83d6d..ec8d7cc7 100644 --- a/runtime/src/task.rs +++ b/runtime/src/task.rs @@ -1,3 +1,4 @@ +//! Create runtime tasks. use crate::core::widget; use crate::futures::futures::channel::mpsc; use crate::futures::futures::channel::oneshot; @@ -29,24 +30,6 @@ impl<T> Task<T> { Self::future(future::ready(value)) } - /// Creates a new [`Task`] that runs the given [`Future`] and produces - /// its output. - pub fn future(future: impl Future<Output = T> + MaybeSend + 'static) -> Self - where - T: 'static, - { - Self::stream(stream::once(future)) - } - - /// Creates a new [`Task`] that runs the given [`Stream`] and produces - /// each of its items. - pub fn stream(stream: impl Stream<Item = T> + MaybeSend + 'static) -> Self - where - T: 'static, - { - Self(Some(boxed_stream(stream.map(Action::Output)))) - } - /// Creates a [`Task`] that runs the given [`Future`] to completion and maps its /// output with the given closure. pub fn perform<A>( @@ -83,66 +66,6 @@ impl<T> Task<T> { )))) } - /// Creates a new [`Task`] that runs the given [`widget::Operation`] and produces - /// its output. - pub fn widget(operation: impl widget::Operation<T> + 'static) -> Task<T> - where - T: Send + 'static, - { - Self::channel(move |sender| { - let operation = - widget::operation::map(Box::new(operation), move |value| { - let _ = sender.clone().try_send(value); - }); - - Action::Widget(Box::new(operation)) - }) - } - - /// Creates a new [`Task`] that executes the [`Action`] returned by the closure and - /// produces the value fed to the [`oneshot::Sender`]. - pub fn oneshot(f: impl FnOnce(oneshot::Sender<T>) -> Action<T>) -> Task<T> - where - T: MaybeSend + 'static, - { - let (sender, receiver) = oneshot::channel(); - - let action = f(sender); - - Self(Some(boxed_stream( - stream::once(async move { action }).chain( - receiver.into_stream().filter_map(|result| async move { - Some(Action::Output(result.ok()?)) - }), - ), - ))) - } - - /// Creates a new [`Task`] that executes the [`Action`] returned by the closure and - /// produces the values fed to the [`mpsc::Sender`]. - pub fn channel(f: impl FnOnce(mpsc::Sender<T>) -> Action<T>) -> Task<T> - where - T: MaybeSend + 'static, - { - let (sender, receiver) = mpsc::channel(1); - - let action = f(sender); - - Self(Some(boxed_stream( - stream::once(async move { action }) - .chain(receiver.map(|result| Action::Output(result))), - ))) - } - - /// Creates a new [`Task`] that executes the given [`Action`] and produces no output. - pub fn effect(action: impl Into<Action<Never>>) -> Self { - let action = action.into(); - - Self(Some(boxed_stream(stream::once(async move { - action.output().expect_err("no output") - })))) - } - /// Maps the output of a [`Task`] with the given closure. pub fn map<O>( self, @@ -236,9 +159,105 @@ impl<T> Task<T> { } } - /// Returns the underlying [`Stream`] of the [`Task`]. - pub fn into_stream(self) -> Option<BoxStream<Action<T>>> { - self.0 + /// Creates a new [`Task`] that discards the result of the current one. + /// + /// Useful if you only care about the side effects of a [`Task`]. + pub fn discard<O>(self) -> Task<O> + where + T: MaybeSend + 'static, + O: MaybeSend + 'static, + { + self.then(|_| Task::none()) + } + + /// Creates a new [`Task`] that can be aborted with the returned [`Handle`]. + pub fn abortable(self) -> (Self, Handle) + where + T: 'static, + { + match self.0 { + Some(stream) => { + let (stream, handle) = stream::abortable(stream); + + ( + Self(Some(boxed_stream(stream))), + Handle { + raw: Some(handle), + abort_on_drop: false, + }, + ) + } + None => ( + Self(None), + Handle { + raw: None, + abort_on_drop: false, + }, + ), + } + } + + /// Creates a new [`Task`] that runs the given [`Future`] and produces + /// its output. + pub fn future(future: impl Future<Output = T> + MaybeSend + 'static) -> Self + where + T: 'static, + { + Self::stream(stream::once(future)) + } + + /// Creates a new [`Task`] that runs the given [`Stream`] and produces + /// each of its items. + pub fn stream(stream: impl Stream<Item = T> + MaybeSend + 'static) -> Self + where + T: 'static, + { + Self(Some(boxed_stream(stream.map(Action::Output)))) + } +} + +/// A handle to a [`Task`] that can be used for aborting it. +#[derive(Debug, Clone)] +pub struct Handle { + raw: Option<stream::AbortHandle>, + abort_on_drop: bool, +} + +impl Handle { + /// Aborts the [`Task`] of this [`Handle`]. + pub fn abort(&self) { + if let Some(handle) = &self.raw { + handle.abort(); + } + } + + /// Returns a new [`Handle`] that will call [`Handle::abort`] whenever + /// it is dropped. + /// + /// This can be really useful if you do not want to worry about calling + /// [`Handle::abort`] yourself. + pub fn abort_on_drop(mut self) -> Self { + Self { + raw: self.raw.take(), + abort_on_drop: true, + } + } + + /// Returns `true` if the [`Task`] of this [`Handle`] has been aborted. + pub fn is_aborted(&self) -> bool { + if let Some(handle) = &self.raw { + handle.is_aborted() + } else { + true + } + } +} + +impl Drop for Handle { + fn drop(&mut self) { + if self.abort_on_drop { + self.abort(); + } } } @@ -275,11 +294,73 @@ impl<T, E> Task<Result<T, E>> { } } -impl<T> From<()> for Task<T> -where - T: MaybeSend + 'static, -{ +impl<T> From<()> for Task<T> { fn from(_value: ()) -> Self { Self::none() } } + +/// Creates a new [`Task`] that runs the given [`widget::Operation`] and produces +/// its output. +pub fn widget<T>(operation: impl widget::Operation<T> + 'static) -> Task<T> +where + T: Send + 'static, +{ + channel(move |sender| { + let operation = + widget::operation::map(Box::new(operation), move |value| { + let _ = sender.clone().try_send(value); + }); + + Action::Widget(Box::new(operation)) + }) +} + +/// Creates a new [`Task`] that executes the [`Action`] returned by the closure and +/// produces the value fed to the [`oneshot::Sender`]. +pub fn oneshot<T>(f: impl FnOnce(oneshot::Sender<T>) -> Action<T>) -> Task<T> +where + T: MaybeSend + 'static, +{ + let (sender, receiver) = oneshot::channel(); + + let action = f(sender); + + Task(Some(boxed_stream( + stream::once(async move { action }).chain( + receiver.into_stream().filter_map(|result| async move { + Some(Action::Output(result.ok()?)) + }), + ), + ))) +} + +/// Creates a new [`Task`] that executes the [`Action`] returned by the closure and +/// produces the values fed to the [`mpsc::Sender`]. +pub fn channel<T>(f: impl FnOnce(mpsc::Sender<T>) -> Action<T>) -> Task<T> +where + T: MaybeSend + 'static, +{ + let (sender, receiver) = mpsc::channel(1); + + let action = f(sender); + + Task(Some(boxed_stream( + stream::once(async move { action }) + .chain(receiver.map(|result| Action::Output(result))), + ))) +} + +/// Creates a new [`Task`] that executes the given [`Action`] and produces no output. +pub fn effect<T>(action: impl Into<Action<Never>>) -> Task<T> { + let action = action.into(); + + Task(Some(boxed_stream(stream::once(async move { + action.output().expect_err("no output") + })))) +} + +/// Returns the underlying [`Stream`] of the [`Task`]. +pub fn into_stream<T>(task: Task<T>) -> Option<BoxStream<Action<T>>> { + task.0 +} diff --git a/runtime/src/user_interface.rs b/runtime/src/user_interface.rs index 858b1a2d..11ebb381 100644 --- a/runtime/src/user_interface.rs +++ b/runtime/src/user_interface.rs @@ -566,7 +566,7 @@ where pub fn operate( &mut self, renderer: &Renderer, - operation: &mut dyn widget::Operation<()>, + operation: &mut dyn widget::Operation, ) { self.root.as_widget().operate( &mut self.state, diff --git a/runtime/src/window.rs b/runtime/src/window.rs index b04e5d59..cdf3d80a 100644 --- a/runtime/src/window.rs +++ b/runtime/src/window.rs @@ -11,7 +11,7 @@ use crate::core::{Point, Size}; use crate::futures::event; use crate::futures::futures::channel::oneshot; use crate::futures::Subscription; -use crate::Task; +use crate::task::{self, Task}; pub use raw_window_handle; @@ -63,6 +63,9 @@ pub enum Action { /// Get the current logical coordinates of the window. GetPosition(Id, oneshot::Sender<Option<Point>>), + /// Get the current scale factor (DPI) of the window. + GetScaleFactor(Id, oneshot::Sender<f32>), + /// Move the window to the given logical coordinates. /// /// Unsupported on Wayland. @@ -144,6 +147,18 @@ pub enum Action { /// Screenshot the viewport of the window. Screenshot(Id, oneshot::Sender<Screenshot>), + + /// Enables mouse passthrough for the given window. + /// + /// This disables mouse events for the window and passes mouse events + /// through to whatever window is underneath. + EnableMousePassthrough(Id), + + /// Disable mouse passthrough for the given window. + /// + /// This enables mouse events for the window and stops mouse events + /// from being passed to whatever is underneath. + DisableMousePassthrough(Id), } /// Subscribes to the frames of the window of the running application. @@ -175,7 +190,7 @@ pub fn events() -> Subscription<(Id, Event)> { /// Subscribes to all [`Event::Closed`] occurrences in the running application. pub fn open_events() -> Subscription<Id> { event::listen_with(|event, _status, id| { - if let crate::core::Event::Window(Event::Closed) = event { + if let crate::core::Event::Window(Event::Opened { .. }) = event { Some(id) } else { None @@ -194,6 +209,17 @@ pub fn close_events() -> Subscription<Id> { }) } +/// Subscribes to all [`Event::Resized`] occurrences in the running application. +pub fn resize_events() -> Subscription<(Id, Size)> { + event::listen_with(|event, _status, id| { + if let crate::core::Event::Window(Event::Resized(size)) = event { + Some((id, size)) + } else { + None + } + }) +} + /// Subscribes to all [`Event::CloseRequested`] occurences in the running application. pub fn close_requests() -> Subscription<Id> { event::listen_with(|event, _status, id| { @@ -207,102 +233,112 @@ pub fn close_requests() -> Subscription<Id> { /// Opens a new window with the given [`Settings`]; producing the [`Id`] /// of the new window on completion. -pub fn open(settings: Settings) -> Task<Id> { +pub fn open(settings: Settings) -> (Id, Task<Id>) { let id = Id::unique(); - Task::oneshot(|channel| { - crate::Action::Window(Action::Open(id, settings, channel)) - }) + ( + id, + task::oneshot(|channel| { + crate::Action::Window(Action::Open(id, settings, channel)) + }), + ) } /// Closes the window with `id`. pub fn close<T>(id: Id) -> Task<T> { - Task::effect(crate::Action::Window(Action::Close(id))) + task::effect(crate::Action::Window(Action::Close(id))) } /// Gets the window [`Id`] of the oldest window. pub fn get_oldest() -> Task<Option<Id>> { - Task::oneshot(|channel| crate::Action::Window(Action::GetOldest(channel))) + task::oneshot(|channel| crate::Action::Window(Action::GetOldest(channel))) } /// Gets the window [`Id`] of the latest window. pub fn get_latest() -> Task<Option<Id>> { - Task::oneshot(|channel| crate::Action::Window(Action::GetLatest(channel))) + task::oneshot(|channel| crate::Action::Window(Action::GetLatest(channel))) } /// Begins dragging the window while the left mouse button is held. pub fn drag<T>(id: Id) -> Task<T> { - Task::effect(crate::Action::Window(Action::Drag(id))) + task::effect(crate::Action::Window(Action::Drag(id))) } /// Resizes the window to the given logical dimensions. pub fn resize<T>(id: Id, new_size: Size) -> Task<T> { - Task::effect(crate::Action::Window(Action::Resize(id, new_size))) + task::effect(crate::Action::Window(Action::Resize(id, new_size))) } /// Get the window's size in logical dimensions. pub fn get_size(id: Id) -> Task<Size> { - Task::oneshot(move |channel| { + task::oneshot(move |channel| { crate::Action::Window(Action::GetSize(id, channel)) }) } /// Gets the maximized state of the window with the given [`Id`]. pub fn get_maximized(id: Id) -> Task<bool> { - Task::oneshot(move |channel| { + task::oneshot(move |channel| { crate::Action::Window(Action::GetMaximized(id, channel)) }) } /// Maximizes the window. pub fn maximize<T>(id: Id, maximized: bool) -> Task<T> { - Task::effect(crate::Action::Window(Action::Maximize(id, maximized))) + task::effect(crate::Action::Window(Action::Maximize(id, maximized))) } /// Gets the minimized state of the window with the given [`Id`]. pub fn get_minimized(id: Id) -> Task<Option<bool>> { - Task::oneshot(move |channel| { + task::oneshot(move |channel| { crate::Action::Window(Action::GetMinimized(id, channel)) }) } /// Minimizes the window. pub fn minimize<T>(id: Id, minimized: bool) -> Task<T> { - Task::effect(crate::Action::Window(Action::Minimize(id, minimized))) + task::effect(crate::Action::Window(Action::Minimize(id, minimized))) } /// Gets the position in logical coordinates of the window with the given [`Id`]. pub fn get_position(id: Id) -> Task<Option<Point>> { - Task::oneshot(move |channel| { + task::oneshot(move |channel| { crate::Action::Window(Action::GetPosition(id, channel)) }) } +/// Gets the scale factor of the window with the given [`Id`]. +pub fn get_scale_factor(id: Id) -> Task<f32> { + task::oneshot(move |channel| { + crate::Action::Window(Action::GetScaleFactor(id, channel)) + }) +} + /// Moves the window to the given logical coordinates. pub fn move_to<T>(id: Id, position: Point) -> Task<T> { - Task::effect(crate::Action::Window(Action::Move(id, position))) + task::effect(crate::Action::Window(Action::Move(id, position))) } /// Changes the [`Mode`] of the window. pub fn change_mode<T>(id: Id, mode: Mode) -> Task<T> { - Task::effect(crate::Action::Window(Action::ChangeMode(id, mode))) + task::effect(crate::Action::Window(Action::ChangeMode(id, mode))) } /// Gets the current [`Mode`] of the window. pub fn get_mode(id: Id) -> Task<Mode> { - Task::oneshot(move |channel| { + task::oneshot(move |channel| { crate::Action::Window(Action::GetMode(id, channel)) }) } /// Toggles the window to maximized or back. pub fn toggle_maximize<T>(id: Id) -> Task<T> { - Task::effect(crate::Action::Window(Action::ToggleMaximize(id))) + task::effect(crate::Action::Window(Action::ToggleMaximize(id))) } /// Toggles the window decorations. pub fn toggle_decorations<T>(id: Id) -> Task<T> { - Task::effect(crate::Action::Window(Action::ToggleDecorations(id))) + task::effect(crate::Action::Window(Action::ToggleDecorations(id))) } /// Request user attention to the window. This has no effect if the application @@ -315,7 +351,7 @@ pub fn request_user_attention<T>( id: Id, user_attention: Option<UserAttention>, ) -> Task<T> { - Task::effect(crate::Action::Window(Action::RequestUserAttention( + task::effect(crate::Action::Window(Action::RequestUserAttention( id, user_attention, ))) @@ -328,32 +364,32 @@ pub fn request_user_attention<T>( /// you are certain that's what the user wants. Focus stealing can cause an extremely disruptive /// user experience. pub fn gain_focus<T>(id: Id) -> Task<T> { - Task::effect(crate::Action::Window(Action::GainFocus(id))) + task::effect(crate::Action::Window(Action::GainFocus(id))) } /// Changes the window [`Level`]. pub fn change_level<T>(id: Id, level: Level) -> Task<T> { - Task::effect(crate::Action::Window(Action::ChangeLevel(id, level))) + task::effect(crate::Action::Window(Action::ChangeLevel(id, level))) } /// Show the [system menu] at cursor position. /// /// [system menu]: https://en.wikipedia.org/wiki/Common_menus_in_Microsoft_Windows#System_menu pub fn show_system_menu<T>(id: Id) -> Task<T> { - Task::effect(crate::Action::Window(Action::ShowSystemMenu(id))) + task::effect(crate::Action::Window(Action::ShowSystemMenu(id))) } /// Gets an identifier unique to the window, provided by the underlying windowing system. This is /// not to be confused with [`Id`]. pub fn get_raw_id<Message>(id: Id) -> Task<u64> { - Task::oneshot(|channel| { + task::oneshot(|channel| { crate::Action::Window(Action::GetRawId(id, channel)) }) } /// Changes the [`Icon`] of the window. pub fn change_icon<T>(id: Id, icon: Icon) -> Task<T> { - Task::effect(crate::Action::Window(Action::ChangeIcon(id, icon))) + task::effect(crate::Action::Window(Action::ChangeIcon(id, icon))) } /// Runs the given callback with the native window handle for the window with the given id. @@ -366,7 +402,7 @@ pub fn run_with_handle<T>( where T: Send + 'static, { - Task::oneshot(move |channel| { + task::oneshot(move |channel| { crate::Action::Window(Action::RunWithHandle( id, Box::new(move |handle| { @@ -378,7 +414,23 @@ where /// Captures a [`Screenshot`] from the window. pub fn screenshot(id: Id) -> Task<Screenshot> { - Task::oneshot(move |channel| { + task::oneshot(move |channel| { crate::Action::Window(Action::Screenshot(id, channel)) }) } + +/// Enables mouse passthrough for the given window. +/// +/// This disables mouse events for the window and passes mouse events +/// through to whatever window is underneath. +pub fn enable_mouse_passthrough<Message>(id: Id) -> Task<Message> { + task::effect(crate::Action::Window(Action::EnableMousePassthrough(id))) +} + +/// Disable mouse passthrough for the given window. +/// +/// This enables mouse events for the window and stops mouse events +/// from being passed to whatever is underneath. +pub fn disable_mouse_passthrough<Message>(id: Id) -> Task<Message> { + task::effect(crate::Action::Window(Action::DisableMousePassthrough(id))) +} diff --git a/src/advanced.rs b/src/advanced.rs index 8d06e805..843b381a 100644 --- a/src/advanced.rs +++ b/src/advanced.rs @@ -1,4 +1,18 @@ //! Leverage advanced concepts like custom widgets. +pub mod subscription { + //! Write your own subscriptions. + pub use crate::runtime::futures::subscription::{ + from_recipe, into_recipes, Event, EventStream, Hasher, MacOS, + PlatformSpecific, Recipe, + }; +} + +pub mod widget { + //! Create custom widgets and operate on them. + pub use crate::core::widget::*; + pub use crate::runtime::task::widget as operate; +} + pub use crate::core::clipboard::{self, Clipboard}; pub use crate::core::image; pub use crate::core::layout::{self, Layout}; @@ -7,13 +21,6 @@ pub use crate::core::overlay::{self, Overlay}; pub use crate::core::renderer::{self, Renderer}; pub use crate::core::svg; pub use crate::core::text::{self, Text}; -pub use crate::core::widget::{self, Widget}; pub use crate::core::Shell; pub use crate::renderer::graphics; - -pub mod subscription { - //! Write your own subscriptions. - pub use crate::runtime::futures::subscription::{ - EventStream, Hasher, Recipe, - }; -} +pub use widget::Widget; diff --git a/src/application.rs b/src/application.rs index 5d16b40f..d0f77304 100644 --- a/src/application.rs +++ b/src/application.rs @@ -103,10 +103,6 @@ where type Renderer = Renderer; type Executor = iced_futures::backend::default::Executor; - fn load(&self) -> Task<Self::Message> { - Task::none() - } - fn update( &self, state: &mut Self::State, @@ -166,14 +162,14 @@ impl<P: Program> Application<P> { Self: 'static, P::State: Default, { - self.run_with(P::State::default) + self.raw.run(self.settings, Some(self.window)) } /// Runs the [`Application`] with a closure that creates the initial state. pub fn run_with<I>(self, initialize: I) -> Result where Self: 'static, - I: Fn() -> P::State + Clone + 'static, + I: FnOnce() -> (P::State, Task<P::Message>) + 'static, { self.raw .run_with(self.settings, Some(self.window), initialize) @@ -323,20 +319,6 @@ impl<P: Program> Application<P> { } } - /// Runs the [`Task`] produced by the closure at startup. - pub fn load( - self, - f: impl Fn() -> Task<P::Message>, - ) -> Application< - impl Program<State = P::State, Message = P::Message, Theme = P::Theme>, - > { - Application { - raw: program::with_load(self.raw, f), - settings: self.settings, - window: self.window, - } - } - /// Sets the subscription logic of the [`Application`]. pub fn subscription( self, @@ -435,6 +417,15 @@ pub trait Update<State, Message> { ) -> impl Into<Task<Message>>; } +impl<State, Message> Update<State, Message> for () { + fn update( + &self, + _state: &mut State, + _message: Message, + ) -> impl Into<Task<Message>> { + } +} + impl<T, State, Message, C> Update<State, Message> for T where T: Fn(&mut State, Message) -> C, diff --git a/src/daemon.rs b/src/daemon.rs index 58293949..6a6ad133 100644 --- a/src/daemon.rs +++ b/src/daemon.rs @@ -55,10 +55,6 @@ where type Renderer = Renderer; type Executor = iced_futures::backend::default::Executor; - fn load(&self) -> Task<Self::Message> { - Task::none() - } - fn update( &self, state: &mut Self::State, @@ -116,14 +112,14 @@ impl<P: Program> Daemon<P> { Self: 'static, P::State: Default, { - self.run_with(P::State::default) + self.raw.run(self.settings, None) } /// Runs the [`Daemon`] with a closure that creates the initial state. pub fn run_with<I>(self, initialize: I) -> Result where Self: 'static, - I: Fn() -> P::State + Clone + 'static, + I: FnOnce() -> (P::State, Task<P::Message>) + 'static, { self.raw.run_with(self.settings, None, initialize) } @@ -176,19 +172,6 @@ impl<P: Program> Daemon<P> { } } - /// Runs the [`Task`] produced by the closure at startup. - pub fn load( - self, - f: impl Fn() -> Task<P::Message>, - ) -> Daemon< - impl Program<State = P::State, Message = P::Message, Theme = P::Theme>, - > { - Daemon { - raw: program::with_load(self.raw, f), - settings: self.settings, - } - } - /// Sets the subscription logic of the [`Daemon`]. pub fn subscription( self, @@ -1,170 +1,451 @@ -//! Iced is a cross-platform GUI library focused on simplicity and type-safety. +//! iced is a cross-platform GUI library focused on simplicity and type-safety. //! Inspired by [Elm]. //! -//! # Features -//! * Simple, easy-to-use, batteries-included API -//! * Type-safe, reactive programming model -//! * [Cross-platform support] (Windows, macOS, Linux, and the Web) -//! * Responsive layout -//! * Built-in widgets (including [text inputs], [scrollables], and more!) -//! * Custom widget support (create your own!) -//! * [Debug overlay with performance metrics] -//! * First-class support for async actions (use futures!) -//! * [Modular ecosystem] split into reusable parts: -//! * A [renderer-agnostic native runtime] enabling integration with existing -//! systems -//! * A [built-in renderer] supporting Vulkan, Metal, DX11, and DX12 -//! * A [windowing shell] -//! * A [web runtime] leveraging the DOM -//! -//! Check out the [repository] and the [examples] for more details! -//! -//! [Cross-platform support]: https://github.com/iced-rs/iced/blob/master/docs/images/todos_desktop.jpg?raw=true -//! [text inputs]: https://iced.rs/examples/text_input.mp4 -//! [scrollables]: https://iced.rs/examples/scrollable.mp4 -//! [Debug overlay with performance metrics]: https://iced.rs/examples/debug.mp4 -//! [Modular ecosystem]: https://github.com/iced-rs/iced/blob/master/ECOSYSTEM.md -//! [renderer-agnostic native runtime]: https://github.com/iced-rs/iced/tree/0.12/runtime -//! [`wgpu`]: https://github.com/gfx-rs/wgpu-rs -//! [built-in renderer]: https://github.com/iced-rs/iced/tree/0.12/wgpu -//! [windowing shell]: https://github.com/iced-rs/iced/tree/0.12/winit -//! [`dodrio`]: https://github.com/fitzgen/dodrio -//! [web runtime]: https://github.com/iced-rs/iced_web -//! [examples]: https://github.com/iced-rs/iced/tree/0.12/examples -//! [repository]: https://github.com/iced-rs/iced -//! -//! # Overview -//! Inspired by [The Elm Architecture], Iced expects you to split user -//! interfaces into four different concepts: -//! -//! * __State__ — the state of your application -//! * __Messages__ — user interactions or meaningful events that you care -//! about -//! * __View logic__ — a way to display your __state__ as widgets that -//! may produce __messages__ on user interaction -//! * __Update logic__ — a way to react to __messages__ and update your -//! __state__ -//! -//! We can build something to see how this works! Let's say we want a simple -//! counter that can be incremented and decremented using two buttons. -//! -//! We start by modelling the __state__ of our application: +//! [Elm]: https://elm-lang.org/ +//! +//! # Disclaimer +//! iced is __experimental__ software. If you expect the documentation to hold your hand +//! as you learn the ropes, you are in for a frustrating experience. +//! +//! The library leverages Rust to its full extent: ownership, borrowing, lifetimes, futures, +//! streams, first-class functions, trait bounds, closures, and more. This documentation +//! is not meant to teach you any of these. Far from it, it will assume you have __mastered__ +//! all of them. +//! +//! Furthermore—just like Rust—iced is very unforgiving. It will not let you easily cut corners. +//! The type signatures alone can be used to learn how to use most of the library. +//! Everything is connected. +//! +//! Therefore, iced is easy to learn for __advanced__ Rust programmers; but plenty of patient +//! beginners have learned it and had a good time with it. Since it leverages a lot of what +//! Rust has to offer in a type-safe way, it can be a great way to discover Rust itself. //! +//! If you don't like the sound of that, you expect to be spoonfed, or you feel frustrated +//! and struggle to use the library; then I recommend you to wait patiently until [the book] +//! is finished. +//! +//! [the book]: https://book.iced.rs +//! +//! # The Pocket Guide +//! Start by calling [`run`]: +//! +//! ```rust,no_run +//! pub fn main() -> iced::Result { +//! iced::run("A cool counter", update, view) +//! } +//! # fn update(state: &mut (), message: ()) {} +//! # fn view(state: &()) -> iced::Element<()> { iced::widget::text("").into() } //! ``` -//! #[derive(Default)] -//! struct Counter { -//! // The counter value -//! value: i32, +//! +//! Define an `update` function to __change__ your state: +//! +//! ```rust +//! fn update(counter: &mut u64, message: Message) { +//! match message { +//! Message::Increment => *counter += 1, +//! } //! } +//! # #[derive(Clone)] +//! # enum Message { Increment } //! ``` //! -//! Next, we need to define the possible user interactions of our counter: -//! the button presses. These interactions are our __messages__: +//! Define a `view` function to __display__ your state: +//! +//! ```rust +//! use iced::widget::{button, text}; +//! use iced::Element; //! +//! fn view(counter: &u64) -> Element<Message> { +//! button(text(counter)).on_press(Message::Increment).into() +//! } +//! # #[derive(Clone)] +//! # enum Message { Increment } //! ``` -//! #[derive(Debug, Clone, Copy)] -//! pub enum Message { +//! +//! And create a `Message` enum to __connect__ `view` and `update` together: +//! +//! ```rust +//! #[derive(Debug, Clone)] +//! enum Message { //! Increment, -//! Decrement, //! } //! ``` //! -//! Now, let's show the actual counter by putting it all together in our -//! __view logic__: +//! ## Custom State +//! You can define your own struct for your state: //! +//! ```rust +//! #[derive(Default)] +//! struct Counter { +//! value: u64, +//! } //! ``` -//! # struct Counter { -//! # // The counter value -//! # value: i32, -//! # } -//! # -//! # #[derive(Debug, Clone, Copy)] -//! # pub enum Message { -//! # Increment, -//! # Decrement, -//! # } -//! # -//! use iced::widget::{button, column, text, Column}; //! -//! impl Counter { -//! pub fn view(&self) -> Column<Message> { -//! // We use a column: a simple vertical layout -//! column![ -//! // The increment button. We tell it to produce an -//! // `Increment` message when pressed -//! button("+").on_press(Message::Increment), +//! But you have to change `update` and `view` accordingly: +//! +//! ```rust +//! # struct Counter { value: u64 } +//! # #[derive(Clone)] +//! # enum Message { Increment } +//! # use iced::widget::{button, text}; +//! # use iced::Element; +//! fn update(counter: &mut Counter, message: Message) { +//! match message { +//! Message::Increment => counter.value += 1, +//! } +//! } +//! +//! fn view(counter: &Counter) -> Element<Message> { +//! button(text(counter.value)).on_press(Message::Increment).into() +//! } +//! ``` +//! +//! ## Widgets and Elements +//! The `view` function must return an [`Element`]. An [`Element`] is just a generic [`widget`]. +//! +//! The [`widget`] module contains a bunch of functions to help you build +//! and use widgets. +//! +//! Widgets are configured using the builder pattern: //! -//! // We show the value of the counter here -//! text(self.value).size(50), +//! ```rust +//! # struct Counter { value: u64 } +//! # #[derive(Clone)] +//! # enum Message { Increment } +//! use iced::widget::{button, column, text}; +//! use iced::Element; //! -//! // The decrement button. We tell it to produce a -//! // `Decrement` message when pressed -//! button("-").on_press(Message::Decrement), +//! fn view(counter: &Counter) -> Element<Message> { +//! column![ +//! text(counter.value).size(20), +//! button("Increment").on_press(Message::Increment), +//! ] +//! .spacing(10) +//! .into() +//! } +//! ``` +//! +//! A widget can be turned into an [`Element`] by calling `into`. +//! +//! Widgets and elements are generic over the message type they produce. The +//! [`Element`] returned by `view` must have the same `Message` type as +//! your `update`. +//! +//! ## Layout +//! There is no unified layout system in iced. Instead, each widget implements +//! its own layout strategy. +//! +//! Building your layout will often consist in using a combination of +//! [rows], [columns], and [containers]: +//! +//! ```rust +//! # struct State; +//! # enum Message {} +//! use iced::widget::{column, container, row}; +//! use iced::{Fill, Element}; +//! +//! fn view(state: &State) -> Element<Message> { +//! container( +//! column![ +//! "Top", +//! row!["Left", "Right"].spacing(10), +//! "Bottom" //! ] -//! } +//! .spacing(10) +//! ) +//! .padding(10) +//! .center_x(Fill) +//! .center_y(Fill) +//! .into() //! } //! ``` //! -//! Finally, we need to be able to react to any produced __messages__ and change -//! our __state__ accordingly in our __update logic__: +//! Rows and columns lay out their children horizontally and vertically, +//! respectively. [Spacing] can be easily added between elements. +//! +//! Containers position or align a single widget inside their bounds. +//! +//! [rows]: widget::Row +//! [columns]: widget::Column +//! [containers]: widget::Container +//! [Spacing]: widget::Column::spacing +//! +//! ## Sizing +//! The width and height of widgets can generally be defined using a [`Length`]. +//! +//! - [`Fill`] will make the widget take all the available space in a given axis. +//! - [`Shrink`] will make the widget use its intrinsic size. +//! +//! Most widgets use a [`Shrink`] sizing strategy by default, but will inherit +//! a [`Fill`] strategy from their children. +//! +//! A fixed numeric [`Length`] in [`Pixels`] can also be used: +//! +//! ```rust +//! # struct State; +//! # enum Message {} +//! use iced::widget::container; +//! use iced::Element; //! +//! fn view(state: &State) -> Element<Message> { +//! container("I am 300px tall!").height(300).into() +//! } //! ``` -//! # struct Counter { -//! # // The counter value -//! # value: i32, -//! # } -//! # -//! # #[derive(Debug, Clone, Copy)] -//! # pub enum Message { -//! # Increment, -//! # Decrement, -//! # } -//! impl Counter { -//! // ... //! -//! pub fn update(&mut self, message: Message) { -//! match message { -//! Message::Increment => { -//! self.value += 1; -//! } -//! Message::Decrement => { -//! self.value -= 1; +//! ## Theming +//! The default [`Theme`] of an application can be changed by defining a `theme` +//! function and leveraging the [`Application`] builder, instead of directly +//! calling [`run`]: +//! +//! ```rust,no_run +//! # #[derive(Default)] +//! # struct State; +//! use iced::Theme; +//! +//! pub fn main() -> iced::Result { +//! iced::application("A cool application", update, view) +//! .theme(theme) +//! .run() +//! } +//! +//! fn theme(state: &State) -> Theme { +//! Theme::TokyoNight +//! } +//! # fn update(state: &mut State, message: ()) {} +//! # fn view(state: &State) -> iced::Element<()> { iced::widget::text("").into() } +//! ``` +//! +//! The `theme` function takes the current state of the application, allowing the +//! returned [`Theme`] to be completely dynamic—just like `view`. +//! +//! There are a bunch of built-in [`Theme`] variants at your disposal, but you can +//! also [create your own](Theme::custom). +//! +//! ## Styling +//! As with layout, iced does not have a unified styling system. However, all +//! of the built-in widgets follow the same styling approach. +//! +//! The appearance of a widget can be changed by calling its `style` method: +//! +//! ```rust +//! # struct State; +//! # enum Message {} +//! use iced::widget::container; +//! use iced::Element; +//! +//! fn view(state: &State) -> Element<Message> { +//! container("I am a rounded box!").style(container::rounded_box).into() +//! } +//! ``` +//! +//! The `style` method of a widget takes a closure that, given the current active +//! [`Theme`], returns the widget style: +//! +//! ```rust +//! # struct State; +//! # #[derive(Clone)] +//! # enum Message {} +//! use iced::widget::button; +//! use iced::{Element, Theme}; +//! +//! fn view(state: &State) -> Element<Message> { +//! button("I am a styled button!").style(|theme: &Theme, status| { +//! let palette = theme.extended_palette(); +//! +//! match status { +//! button::Status::Active => { +//! button::Style::default() +//! .with_background(palette.success.strong.color) //! } +//! _ => button::primary(theme, status), //! } +//! }) +//! .into() +//! } +//! ``` +//! +//! Widgets that can be in multiple different states will also provide the closure +//! with some [`Status`], allowing you to use a different style for each state. +//! +//! You can extract the [`Palette`] colors of a [`Theme`] with the [`palette`] or +//! [`extended_palette`] methods. +//! +//! Most widgets provide styling functions for your convenience in their respective modules; +//! like [`container::rounded_box`], [`button::primary`], or [`text::danger`]. +//! +//! [`Status`]: widget::button::Status +//! [`palette`]: Theme::palette +//! [`extended_palette`]: Theme::extended_palette +//! [`container::rounded_box`]: widget::container::rounded_box +//! [`button::primary`]: widget::button::primary +//! [`text::danger`]: widget::text::danger +//! +//! ## Concurrent Tasks +//! The `update` function can _optionally_ return a [`Task`]. +//! +//! A [`Task`] can be leveraged to perform asynchronous work, like running a +//! future or a stream: +//! +//! ```rust +//! # #[derive(Clone)] +//! # struct Weather; +//! use iced::Task; +//! +//! struct State { +//! weather: Option<Weather>, +//! } +//! +//! enum Message { +//! FetchWeather, +//! WeatherFetched(Weather), +//! } +//! +//! fn update(state: &mut State, message: Message) -> Task<Message> { +//! match message { +//! Message::FetchWeather => Task::perform( +//! fetch_weather(), +//! Message::WeatherFetched, +//! ), +//! Message::WeatherFetched(weather) => { +//! state.weather = Some(weather); +//! +//! Task::none() +//! } //! } //! } +//! +//! async fn fetch_weather() -> Weather { +//! // ... +//! # unimplemented!() +//! } //! ``` //! -//! And that's everything! We just wrote a whole user interface. Let's run it: +//! Tasks can also be used to interact with the iced runtime. Some modules +//! expose functions that create tasks for different purposes—like [changing +//! window settings](window#functions), [focusing a widget](widget::focus_next), or +//! [querying its visible bounds](widget::container::visible_bounds). +//! +//! Like futures and streams, tasks expose [a monadic interface](Task::then)—but they can also be +//! [mapped](Task::map), [chained](Task::chain), [batched](Task::batch), [canceled](Task::abortable), +//! and more. +//! +//! ## Passive Subscriptions +//! Applications can subscribe to passive sources of data—like time ticks or runtime events. +//! +//! You will need to define a `subscription` function and use the [`Application`] builder: //! -//! ```no_run +//! ```rust,no_run //! # #[derive(Default)] -//! # struct Counter; -//! # impl Counter { -//! # fn update(&mut self, _message: ()) {} -//! # fn view(&self) -> iced::Element<()> { unimplemented!() } -//! # } -//! # -//! fn main() -> iced::Result { -//! iced::run("A cool counter", Counter::update, Counter::view) +//! # struct State; +//! use iced::window; +//! use iced::{Size, Subscription}; +//! +//! #[derive(Debug)] +//! enum Message { +//! WindowResized(Size), +//! } +//! +//! pub fn main() -> iced::Result { +//! iced::application("A cool application", update, view) +//! .subscription(subscription) +//! .run() //! } +//! +//! fn subscription(state: &State) -> Subscription<Message> { +//! window::resize_events().map(|(_id, size)| Message::WindowResized(size)) +//! } +//! # fn update(state: &mut State, message: Message) {} +//! # fn view(state: &State) -> iced::Element<Message> { iced::widget::text("").into() } //! ``` //! -//! Iced will automatically: +//! A [`Subscription`] is [a _declarative_ builder of streams](Subscription#the-lifetime-of-a-subscription) +//! that are not allowed to end on their own. Only the `subscription` function +//! dictates the active subscriptions—just like `view` fully dictates the +//! visible widgets of your user interface, at every moment. //! -//! 1. Take the result of our __view logic__ and layout its widgets. -//! 1. Process events from our system and produce __messages__ for our -//! __update logic__. -//! 1. Draw the resulting user interface. +//! As with tasks, some modules expose convenient functions that build a [`Subscription`] for you—like +//! [`time::every`] which can be used to listen to time, or [`keyboard::on_key_press`] which will notify you +//! of any key presses. But you can also create your own with [`Subscription::run`] and [`run_with_id`]. //! -//! # Usage -//! Use [`run`] or the [`application`] builder. +//! [`run_with_id`]: Subscription::run_with_id //! -//! [Elm]: https://elm-lang.org/ -//! [The Elm Architecture]: https://guide.elm-lang.org/architecture/ -//! [`application`]: application() +//! ## Scaling Applications +//! The `update`, `view`, and `Message` triplet composes very nicely. +//! +//! A common pattern is to leverage this composability to split an +//! application into different screens: +//! +//! ```rust +//! # mod contacts { +//! # use iced::{Element, Task}; +//! # pub struct Contacts; +//! # impl Contacts { +//! # pub fn update(&mut self, message: Message) -> Task<Message> { unimplemented!() } +//! # pub fn view(&self) -> Element<Message> { unimplemented!() } +//! # } +//! # #[derive(Debug)] +//! # pub enum Message {} +//! # } +//! # mod conversation { +//! # use iced::{Element, Task}; +//! # pub struct Conversation; +//! # impl Conversation { +//! # pub fn update(&mut self, message: Message) -> Task<Message> { unimplemented!() } +//! # pub fn view(&self) -> Element<Message> { unimplemented!() } +//! # } +//! # #[derive(Debug)] +//! # pub enum Message {} +//! # } +//! use contacts::Contacts; +//! use conversation::Conversation; +//! +//! use iced::{Element, Task}; +//! +//! struct State { +//! screen: Screen, +//! } +//! +//! enum Screen { +//! Contacts(Contacts), +//! Conversation(Conversation), +//! } +//! +//! enum Message { +//! Contacts(contacts::Message), +//! Conversation(conversation::Message) +//! } +//! +//! fn update(state: &mut State, message: Message) -> Task<Message> { +//! match message { +//! Message::Contacts(message) => { +//! if let Screen::Contacts(contacts) = &mut state.screen { +//! contacts.update(message).map(Message::Contacts) +//! } else { +//! Task::none() +//! } +//! } +//! Message::Conversation(message) => { +//! if let Screen::Conversation(conversation) = &mut state.screen { +//! conversation.update(message).map(Message::Conversation) +//! } else { +//! Task::none() +//! } +//! } +//! } +//! } +//! +//! fn view(state: &State) -> Element<Message> { +//! match &state.screen { +//! Screen::Contacts(contacts) => contacts.view().map(Message::Contacts), +//! Screen::Conversation(conversation) => conversation.view().map(Message::Conversation), +//! } +//! } +//! ``` +//! +//! Functor methods like [`Task::map`], [`Element::map`], and [`Subscription::map`] make this +//! approach seamless. #![doc( - html_logo_url = "https://raw.githubusercontent.com/iced-rs/iced/9ab6923e943f784985e9ef9ca28b10278297225d/docs/logo.svg" + html_logo_url = "https://raw.githubusercontent.com/iced-rs/iced/bdf0430880f5c29443f5f0a0ae4895866dfef4c6/docs/logo.svg" )] #![cfg_attr(docsrs, feature(doc_auto_cfg))] #![cfg_attr(docsrs, feature(doc_cfg))] @@ -175,6 +456,7 @@ use iced_winit::core; use iced_winit::runtime; pub use iced_futures::futures; +pub use iced_futures::stream; #[cfg(feature = "highlighter")] pub use iced_highlighter as highlighter; @@ -195,13 +477,25 @@ pub use crate::core::alignment; pub use crate::core::border; pub use crate::core::color; pub use crate::core::gradient; +pub use crate::core::padding; pub use crate::core::theme; pub use crate::core::{ Alignment, Background, Border, Color, ContentFit, Degrees, Gradient, Length, Padding, Pixels, Point, Radians, Rectangle, Rotation, Shadow, Size, Theme, Transformation, Vector, }; -pub use crate::runtime::{exit, Task}; +pub use crate::runtime::exit; +pub use iced_futures::Subscription; + +pub use alignment::Horizontal::{Left, Right}; +pub use alignment::Vertical::{Bottom, Top}; +pub use Alignment::Center; +pub use Length::{Fill, FillPortion, Shrink}; + +pub mod task { + //! Create runtime tasks. + pub use crate::runtime::task::{Handle, Task}; +} pub mod clipboard { //! Access the clipboard. @@ -255,13 +549,6 @@ pub mod mouse { }; } -pub mod subscription { - //! Listen to external events in your application. - pub use iced_futures::subscription::{ - channel, run, run_with_id, unfold, Subscription, - }; -} - #[cfg(feature = "system")] pub mod system { //! Retrieve system information. @@ -314,7 +601,7 @@ pub use executor::Executor; pub use font::Font; pub use renderer::Renderer; pub use settings::Settings; -pub use subscription::Subscription; +pub use task::Task; #[doc(inline)] pub use application::application; @@ -339,8 +626,6 @@ pub type Result = std::result::Result<(), Error>; /// /// This is equivalent to chaining [`application()`] with [`Application::run`]. /// -/// [`program`]: program() -/// /// # Example /// ```no_run /// use iced::widget::{button, column, text, Column}; diff --git a/src/program.rs b/src/program.rs index 3f9d2d0c..2b697fbe 100644 --- a/src/program.rs +++ b/src/program.rs @@ -27,8 +27,6 @@ pub trait Program: Sized { /// The executor of the program. type Executor: Executor; - fn load(&self) -> Task<Self::Message>; - fn update( &self, state: &mut Self::State, @@ -80,7 +78,9 @@ pub trait Program: Sized { Self: 'static, Self::State: Default, { - self.run_with(settings, window_settings, Self::State::default) + self.run_with(settings, window_settings, || { + (Self::State::default(), Task::none()) + }) } /// Runs the [`Program`] with the given [`Settings`] and a closure that creates the initial state. @@ -92,7 +92,7 @@ pub trait Program: Sized { ) -> Result where Self: 'static, - I: Fn() -> Self::State + Clone + 'static, + I: FnOnce() -> (Self::State, Task<Self::Message>) + 'static, { use std::marker::PhantomData; @@ -102,7 +102,9 @@ pub trait Program: Sized { _initialize: PhantomData<I>, } - impl<P: Program, I: Fn() -> P::State> shell::Program for Instance<P, I> { + impl<P: Program, I: FnOnce() -> (P::State, Task<P::Message>)> + shell::Program for Instance<P, I> + { type Message = P::Message; type Theme = P::Theme; type Renderer = P::Renderer; @@ -112,8 +114,7 @@ pub trait Program: Sized { fn new( (program, initialize): Self::Flags, ) -> (Self, Task<Self::Message>) { - let state = initialize(); - let command = program.load(); + let (state, task) = initialize(); ( Self { @@ -121,7 +122,7 @@ pub trait Program: Sized { state, _initialize: PhantomData, }, - command, + task, ) } @@ -212,10 +213,6 @@ pub fn with_title<P: Program>( type Renderer = P::Renderer; type Executor = P::Executor; - fn load(&self) -> Task<Self::Message> { - self.program.load() - } - fn title(&self, state: &Self::State, window: window::Id) -> String { (self.title)(state, window) } @@ -267,80 +264,6 @@ pub fn with_title<P: Program>( WithTitle { program, title } } -pub fn with_load<P: Program>( - program: P, - f: impl Fn() -> Task<P::Message>, -) -> impl Program<State = P::State, Message = P::Message, Theme = P::Theme> { - struct WithLoad<P, F> { - program: P, - load: F, - } - - impl<P: Program, F> Program for WithLoad<P, F> - where - F: Fn() -> Task<P::Message>, - { - type State = P::State; - type Message = P::Message; - type Theme = P::Theme; - type Renderer = P::Renderer; - type Executor = P::Executor; - - fn load(&self) -> Task<Self::Message> { - Task::batch([self.program.load(), (self.load)()]) - } - - fn update( - &self, - state: &mut Self::State, - message: Self::Message, - ) -> Task<Self::Message> { - self.program.update(state, message) - } - - fn view<'a>( - &self, - state: &'a Self::State, - window: window::Id, - ) -> Element<'a, Self::Message, Self::Theme, Self::Renderer> { - self.program.view(state, window) - } - - fn title(&self, state: &Self::State, window: window::Id) -> String { - self.program.title(state, window) - } - - fn subscription( - &self, - state: &Self::State, - ) -> Subscription<Self::Message> { - self.program.subscription(state) - } - - fn theme( - &self, - state: &Self::State, - window: window::Id, - ) -> Self::Theme { - self.program.theme(state, window) - } - - fn style( - &self, - state: &Self::State, - theme: &Self::Theme, - ) -> Appearance { - self.program.style(state, theme) - } - - fn scale_factor(&self, state: &Self::State, window: window::Id) -> f64 { - self.program.scale_factor(state, window) - } - } - - WithLoad { program, load: f } -} - pub fn with_subscription<P: Program>( program: P, f: impl Fn(&P::State) -> Subscription<P::Message>, @@ -367,10 +290,6 @@ pub fn with_subscription<P: Program>( (self.subscription)(state) } - fn load(&self) -> Task<Self::Message> { - self.program.load() - } - fn update( &self, state: &mut Self::State, @@ -445,10 +364,6 @@ pub fn with_theme<P: Program>( (self.theme)(state, window) } - fn load(&self) -> Task<Self::Message> { - self.program.load() - } - fn title(&self, state: &Self::State, window: window::Id) -> String { self.program.title(state, window) } @@ -519,10 +434,6 @@ pub fn with_style<P: Program>( (self.style)(state, theme) } - fn load(&self) -> Task<Self::Message> { - self.program.load() - } - fn title(&self, state: &Self::State, window: window::Id) -> String { self.program.title(state, window) } @@ -585,10 +496,6 @@ pub fn with_scale_factor<P: Program>( type Renderer = P::Renderer; type Executor = P::Executor; - fn load(&self) -> Task<Self::Message> { - self.program.load() - } - fn title(&self, state: &Self::State, window: window::Id) -> String { self.program.title(state, window) } diff --git a/tiny_skia/Cargo.toml b/tiny_skia/Cargo.toml index 32ead3e0..323233f0 100644 --- a/tiny_skia/Cargo.toml +++ b/tiny_skia/Cargo.toml @@ -15,7 +15,7 @@ workspace = true [features] image = ["iced_graphics/image"] -svg = ["resvg"] +svg = ["iced_graphics/svg", "resvg"] geometry = ["iced_graphics/geometry"] [dependencies] diff --git a/tiny_skia/src/engine.rs b/tiny_skia/src/engine.rs index 028b304f..196c36cf 100644 --- a/tiny_skia/src/engine.rs +++ b/tiny_skia/src/engine.rs @@ -439,9 +439,13 @@ impl Engine { let transformation = transformation * *local_transformation; let (width, height) = buffer.size(); - let physical_bounds = - Rectangle::new(raw.position, Size::new(width, height)) - * transformation; + let physical_bounds = Rectangle::new( + raw.position, + Size::new( + width.unwrap_or(clip_bounds.width), + height.unwrap_or(clip_bounds.height), + ), + ) * transformation; if !clip_bounds.intersects(&physical_bounds) { return; @@ -546,13 +550,7 @@ impl Engine { ) { match image { #[cfg(feature = "image")] - Image::Raster { - handle, - filter_method, - bounds, - rotation, - opacity, - } => { + Image::Raster(raster, bounds) => { let physical_bounds = *bounds * _transformation; if !_clip_bounds.intersects(&physical_bounds) { @@ -563,7 +561,7 @@ impl Engine { .then_some(_clip_mask as &_); let center = physical_bounds.center(); - let radians = f32::from(*rotation); + let radians = f32::from(raster.rotation); let transform = into_transform(_transformation).post_rotate_at( radians.to_degrees(), @@ -572,23 +570,17 @@ impl Engine { ); self.raster_pipeline.draw( - handle, - *filter_method, + &raster.handle, + raster.filter_method, *bounds, - *opacity, + raster.opacity, _pixels, transform, clip_mask, ); } #[cfg(feature = "svg")] - Image::Vector { - handle, - color, - bounds, - rotation, - opacity, - } => { + Image::Vector(svg, bounds) => { let physical_bounds = *bounds * _transformation; if !_clip_bounds.intersects(&physical_bounds) { @@ -599,7 +591,7 @@ impl Engine { .then_some(_clip_mask as &_); let center = physical_bounds.center(); - let radians = f32::from(*rotation); + let radians = f32::from(svg.rotation); let transform = into_transform(_transformation).post_rotate_at( radians.to_degrees(), @@ -608,10 +600,10 @@ impl Engine { ); self.vector_pipeline.draw( - handle, - *color, + &svg.handle, + svg.color, physical_bounds, - *opacity, + svg.opacity, _pixels, transform, clip_mask, diff --git a/tiny_skia/src/geometry.rs b/tiny_skia/src/geometry.rs index 02b6e1b9..0d5fff62 100644 --- a/tiny_skia/src/geometry.rs +++ b/tiny_skia/src/geometry.rs @@ -1,10 +1,10 @@ use crate::core::text::LineHeight; -use crate::core::{Pixels, Point, Radians, Rectangle, Size, Vector}; +use crate::core::{self, Pixels, Point, Radians, Rectangle, Size, Svg, Vector}; use crate::graphics::cache::{self, Cached}; use crate::graphics::geometry::fill::{self, Fill}; use crate::graphics::geometry::stroke::{self, Stroke}; use crate::graphics::geometry::{self, Path, Style}; -use crate::graphics::{Gradient, Text}; +use crate::graphics::{self, Gradient, Image, Text}; use crate::Primitive; use std::rc::Rc; @@ -13,6 +13,7 @@ use std::rc::Rc; pub enum Geometry { Live { text: Vec<Text>, + images: Vec<graphics::Image>, primitives: Vec<Primitive>, clip_bounds: Rectangle, }, @@ -22,6 +23,7 @@ pub enum Geometry { #[derive(Debug, Clone)] pub struct Cache { pub text: Rc<[Text]>, + pub images: Rc<[graphics::Image]>, pub primitives: Rc<[Primitive]>, pub clip_bounds: Rectangle, } @@ -37,10 +39,12 @@ impl Cached for Geometry { match self { Self::Live { primitives, + images, text, clip_bounds, } => Cache { primitives: Rc::from(primitives), + images: Rc::from(images), text: Rc::from(text), clip_bounds, }, @@ -55,6 +59,7 @@ pub struct Frame { transform: tiny_skia::Transform, stack: Vec<tiny_skia::Transform>, primitives: Vec<Primitive>, + images: Vec<graphics::Image>, text: Vec<Text>, } @@ -68,6 +73,7 @@ impl Frame { clip_bounds, stack: Vec::new(), primitives: Vec::new(), + images: Vec::new(), text: Vec::new(), transform: tiny_skia::Transform::from_translate( clip_bounds.x, @@ -162,6 +168,15 @@ impl geometry::frame::Backend for Frame { }); } + fn stroke_rectangle<'a>( + &mut self, + top_left: Point, + size: Size, + stroke: impl Into<Stroke<'a>>, + ) { + self.stroke(&Path::rectangle(top_left, size), stroke); + } + fn fill_text(&mut self, text: impl Into<geometry::Text>) { let text = text.into(); @@ -238,7 +253,7 @@ impl geometry::frame::Backend for Frame { Self::with_clip(clip_bounds) } - fn paste(&mut self, frame: Self, _at: Point) { + fn paste(&mut self, frame: Self) { self.primitives.extend(frame.primitives); self.text.extend(frame.text); } @@ -269,10 +284,63 @@ impl geometry::frame::Backend for Frame { fn into_geometry(self) -> Geometry { Geometry::Live { primitives: self.primitives, + images: self.images, text: self.text, clip_bounds: self.clip_bounds, } } + + fn draw_image(&mut self, bounds: Rectangle, image: impl Into<core::Image>) { + let mut image = image.into(); + + let (bounds, external_rotation) = + transform_rectangle(bounds, self.transform); + + image.rotation += external_rotation; + + self.images.push(graphics::Image::Raster(image, bounds)); + } + + fn draw_svg(&mut self, bounds: Rectangle, svg: impl Into<Svg>) { + let mut svg = svg.into(); + + let (bounds, external_rotation) = + transform_rectangle(bounds, self.transform); + + svg.rotation += external_rotation; + + self.images.push(Image::Vector(svg, bounds)); + } +} + +fn transform_rectangle( + rectangle: Rectangle, + transform: tiny_skia::Transform, +) -> (Rectangle, Radians) { + let mut top_left = tiny_skia::Point { + x: rectangle.x, + y: rectangle.y, + }; + + let mut top_right = tiny_skia::Point { + x: rectangle.x + rectangle.width, + y: rectangle.y, + }; + + let mut bottom_left = tiny_skia::Point { + x: rectangle.x, + y: rectangle.y + rectangle.height, + }; + + transform.map_point(&mut top_left); + transform.map_point(&mut top_right); + transform.map_point(&mut bottom_left); + + Rectangle::with_vertices( + Point::new(top_left.x, top_left.y), + Point::new(top_right.x, top_right.y), + Point::new(bottom_left.x, bottom_left.y), + ) } fn convert_path(path: &Path) -> Option<tiny_skia::Path> { diff --git a/tiny_skia/src/layer.rs b/tiny_skia/src/layer.rs index 48fca1d8..bdfd4d38 100644 --- a/tiny_skia/src/layer.rs +++ b/tiny_skia/src/layer.rs @@ -1,6 +1,6 @@ +use crate::core::renderer::Quad; use crate::core::{ - image, renderer::Quad, svg, Background, Color, Point, Radians, Rectangle, - Transformation, + self, Background, Color, Point, Rectangle, Svg, Transformation, }; use crate::graphics::damage; use crate::graphics::layer; @@ -72,7 +72,7 @@ impl Layer { pub fn draw_text( &mut self, - text: crate::core::Text, + text: core::Text, position: Point, color: Color, clip_bounds: Rectangle, @@ -115,42 +115,35 @@ impl Layer { .push(Item::Cached(text, clip_bounds, transformation)); } - pub fn draw_image( + pub fn draw_image(&mut self, image: Image, transformation: Transformation) { + match image { + Image::Raster(raster, bounds) => { + self.draw_raster(raster, bounds, transformation); + } + Image::Vector(svg, bounds) => { + self.draw_svg(svg, bounds, transformation); + } + } + } + + pub fn draw_raster( &mut self, - handle: image::Handle, - filter_method: image::FilterMethod, + image: core::Image, bounds: Rectangle, transformation: Transformation, - rotation: Radians, - opacity: f32, ) { - let image = Image::Raster { - handle, - filter_method, - bounds: bounds * transformation, - rotation, - opacity, - }; + let image = Image::Raster(image, bounds * transformation); self.images.push(image); } pub fn draw_svg( &mut self, - handle: svg::Handle, - color: Option<Color>, + svg: Svg, bounds: Rectangle, transformation: Transformation, - rotation: Radians, - opacity: f32, ) { - let svg = Image::Vector { - handle, - color, - bounds: bounds * transformation, - rotation, - opacity, - }; + let svg = Image::Vector(svg, bounds * transformation); self.images.push(svg); } @@ -293,7 +286,7 @@ impl graphics::Layer for Layer { fn flush(&mut self) {} - fn resize(&mut self, bounds: graphics::core::Rectangle) { + fn resize(&mut self, bounds: Rectangle) { self.bounds = bounds; } diff --git a/tiny_skia/src/lib.rs b/tiny_skia/src/lib.rs index 1aabff00..758921d4 100644 --- a/tiny_skia/src/lib.rs +++ b/tiny_skia/src/lib.rs @@ -178,6 +178,16 @@ impl Renderer { engine::adjust_clip_mask(clip_mask, clip_bounds); } + for image in &layer.images { + self.engine.draw_image( + image, + Transformation::scale(scale_factor), + pixels, + clip_mask, + clip_bounds, + ); + } + for group in &layer.text { for text in group.as_slice() { self.engine.draw_text( @@ -190,16 +200,6 @@ impl Renderer { ); } } - - for image in &layer.images { - self.engine.draw_image( - image, - Transformation::scale(scale_factor), - pixels, - clip_mask, - clip_bounds, - ); - } } if !overlay.is_empty() { @@ -330,6 +330,7 @@ impl graphics::geometry::Renderer for Renderer { match geometry { Geometry::Live { primitives, + images, text, clip_bounds, } => { @@ -339,6 +340,10 @@ impl graphics::geometry::Renderer for Renderer { transformation, ); + for image in images { + layer.draw_image(image, transformation); + } + layer.draw_text_group(text, clip_bounds, transformation); } Geometry::Cache(cache) => { @@ -348,6 +353,10 @@ impl graphics::geometry::Renderer for Renderer { transformation, ); + for image in cache.images.iter() { + layer.draw_image(image.clone(), transformation); + } + layer.draw_text_cache( cache.text, cache.clip_bounds, @@ -372,23 +381,9 @@ impl core::image::Renderer for Renderer { self.engine.raster_pipeline.dimensions(handle) } - fn draw_image( - &mut self, - handle: Self::Handle, - filter_method: core::image::FilterMethod, - bounds: Rectangle, - rotation: core::Radians, - opacity: f32, - ) { + fn draw_image(&mut self, image: core::Image, bounds: Rectangle) { let (layer, transformation) = self.layers.current_mut(); - layer.draw_image( - handle, - filter_method, - bounds, - transformation, - rotation, - opacity, - ); + layer.draw_raster(image, bounds, transformation); } } @@ -401,23 +396,9 @@ impl core::svg::Renderer for Renderer { self.engine.vector_pipeline.viewport_dimensions(handle) } - fn draw_svg( - &mut self, - handle: core::svg::Handle, - color: Option<Color>, - bounds: Rectangle, - rotation: core::Radians, - opacity: f32, - ) { + fn draw_svg(&mut self, svg: core::Svg, bounds: Rectangle) { let (layer, transformation) = self.layers.current_mut(); - layer.draw_svg( - handle, - color, - bounds, - transformation, - rotation, - opacity, - ); + layer.draw_svg(svg, bounds, transformation); } } diff --git a/tiny_skia/src/text.rs b/tiny_skia/src/text.rs index c71deb10..0fc3d1f7 100644 --- a/tiny_skia/src/text.rs +++ b/tiny_skia/src/text.rs @@ -169,7 +169,13 @@ impl Pipeline { font_system.raw(), &mut self.glyph_cache, buffer, - Rectangle::new(position, Size::new(width, height)), + Rectangle::new( + position, + Size::new( + width.unwrap_or(pixels.width() as f32), + height.unwrap_or(pixels.height() as f32), + ), + ), color, alignment::Horizontal::Left, alignment::Vertical::Top, diff --git a/tiny_skia/src/vector.rs b/tiny_skia/src/vector.rs index bbe08cb8..8a15f47f 100644 --- a/tiny_skia/src/vector.rs +++ b/tiny_skia/src/vector.rs @@ -1,8 +1,7 @@ use crate::core::svg::{Data, Handle}; use crate::core::{Color, Rectangle, Size}; -use crate::graphics::text; -use resvg::usvg::{self, TreeTextToPath}; +use resvg::usvg; use rustc_hash::{FxHashMap, FxHashSet}; use tiny_skia::Transform; @@ -80,35 +79,28 @@ struct RasterKey { impl Cache { fn load(&mut self, handle: &Handle) -> Option<&usvg::Tree> { - use usvg::TreeParsing; - let id = handle.id(); if let hash_map::Entry::Vacant(entry) = self.trees.entry(id) { - let mut svg = match handle.data() { + let svg = match handle.data() { Data::Path(path) => { fs::read_to_string(path).ok().and_then(|contents| { usvg::Tree::from_str( &contents, - &usvg::Options::default(), + &usvg::Options::default(), // TODO: Set usvg::Options::fontdb ) .ok() }) } Data::Bytes(bytes) => { - usvg::Tree::from_data(bytes, &usvg::Options::default()).ok() + usvg::Tree::from_data( + bytes, + &usvg::Options::default(), // TODO: Set usvg::Options::fontdb + ) + .ok() } }; - if let Some(svg) = &mut svg { - if svg.has_text_nodes() { - let mut font_system = - text::font_system().write().expect("Write font system"); - - svg.convert_text(font_system.raw().db_mut()); - } - } - let _ = entry.insert(svg); } @@ -118,11 +110,9 @@ impl Cache { fn viewport_dimensions(&mut self, handle: &Handle) -> Option<Size<u32>> { let tree = self.load(handle)?; + let size = tree.size(); - Some(Size::new( - tree.size.width() as u32, - tree.size.height() as u32, - )) + Some(Size::new(size.width() as u32, size.height() as u32)) } fn draw( @@ -147,7 +137,7 @@ impl Cache { let mut image = tiny_skia::Pixmap::new(size.width, size.height)?; - let tree_size = tree.size.to_int_size(); + let tree_size = tree.size().to_int_size(); let target_size = if size.width > size.height { tree_size.scale_to_width(size.width) @@ -167,7 +157,7 @@ impl Cache { tiny_skia::Transform::default() }; - resvg::Tree::from_usvg(tree).render(transform, &mut image.as_mut()); + resvg::render(tree, transform, &mut image.as_mut()); if let Some([r, g, b, _]) = key.color { // Apply color filter diff --git a/wgpu/Cargo.toml b/wgpu/Cargo.toml index 30545fa2..b13ecb36 100644 --- a/wgpu/Cargo.toml +++ b/wgpu/Cargo.toml @@ -20,7 +20,7 @@ all-features = true [features] geometry = ["iced_graphics/geometry", "lyon"] image = ["iced_graphics/image"] -svg = ["resvg/text"] +svg = ["iced_graphics/svg", "resvg/text"] web-colors = ["iced_graphics/web-colors"] webgl = ["wgpu/webgl"] diff --git a/wgpu/src/geometry.rs b/wgpu/src/geometry.rs index f6213e1d..8e6f77d7 100644 --- a/wgpu/src/geometry.rs +++ b/wgpu/src/geometry.rs @@ -1,7 +1,7 @@ //! Build and draw geometry. use crate::core::text::LineHeight; use crate::core::{ - Pixels, Point, Radians, Rectangle, Size, Transformation, Vector, + self, Pixels, Point, Radians, Rectangle, Size, Svg, Transformation, Vector, }; use crate::graphics::cache::{self, Cached}; use crate::graphics::color; @@ -11,7 +11,7 @@ use crate::graphics::geometry::{ }; use crate::graphics::gradient::{self, Gradient}; use crate::graphics::mesh::{self, Mesh}; -use crate::graphics::{self, Text}; +use crate::graphics::{Image, Text}; use crate::text; use crate::triangle; @@ -19,16 +19,22 @@ use lyon::geom::euclid; use lyon::tessellation; use std::borrow::Cow; +use std::sync::Arc; #[derive(Debug)] pub enum Geometry { - Live { meshes: Vec<Mesh>, text: Vec<Text> }, + Live { + meshes: Vec<Mesh>, + images: Vec<Image>, + text: Vec<Text>, + }, Cached(Cache), } #[derive(Debug, Clone)] pub struct Cache { pub meshes: Option<triangle::Cache>, + pub images: Option<Arc<[Image]>>, pub text: Option<text::Cache>, } @@ -45,7 +51,17 @@ impl Cached for Geometry { previous: Option<Self::Cache>, ) -> Self::Cache { match self { - Self::Live { meshes, text } => { + Self::Live { + meshes, + images, + text, + } => { + let images = if images.is_empty() { + None + } else { + Some(Arc::from(images)) + }; + if let Some(mut previous) = previous { if let Some(cache) = &mut previous.meshes { cache.update(meshes); @@ -59,10 +75,13 @@ impl Cached for Geometry { previous.text = text::Cache::new(group, text); } + previous.images = images; + previous } else { Cache { meshes: triangle::Cache::new(meshes), + images, text: text::Cache::new(group, text), } } @@ -78,6 +97,7 @@ pub struct Frame { clip_bounds: Rectangle, buffers: BufferStack, meshes: Vec<Mesh>, + images: Vec<Image>, text: Vec<Text>, transforms: Transforms, fill_tessellator: tessellation::FillTessellator, @@ -96,6 +116,7 @@ impl Frame { clip_bounds: bounds, buffers: BufferStack::new(), meshes: Vec::new(), + images: Vec::new(), text: Vec::new(), transforms: Transforms { previous: Vec::new(), @@ -232,6 +253,44 @@ impl geometry::frame::Backend for Frame { .expect("Stroke path"); } + fn stroke_rectangle<'a>( + &mut self, + top_left: Point, + size: Size, + stroke: impl Into<Stroke<'a>>, + ) { + let stroke = stroke.into(); + + let mut buffer = self + .buffers + .get_stroke(&self.transforms.current.transform_style(stroke.style)); + + let top_left = self + .transforms + .current + .0 + .transform_point(lyon::math::Point::new(top_left.x, top_left.y)); + + let size = + self.transforms.current.0.transform_vector( + lyon::math::Vector::new(size.width, size.height), + ); + + let mut options = tessellation::StrokeOptions::default(); + options.line_width = stroke.width; + options.start_cap = into_line_cap(stroke.line_cap); + options.end_cap = into_line_cap(stroke.line_cap); + options.line_join = into_line_join(stroke.line_join); + + self.stroke_tessellator + .tessellate_rectangle( + &lyon::math::Box2D::new(top_left, top_left + size), + &options, + buffer.as_mut(), + ) + .expect("Stroke rectangle"); + } + fn fill_text(&mut self, text: impl Into<geometry::Text>) { let text = text.into(); @@ -270,7 +329,7 @@ impl geometry::frame::Backend for Frame { height: f32::INFINITY, }; - self.text.push(graphics::Text::Cached { + self.text.push(Text::Cached { content: text.content, bounds, color: text.color, @@ -335,10 +394,11 @@ impl geometry::frame::Backend for Frame { Frame::with_clip(clip_bounds) } - fn paste(&mut self, frame: Frame, _at: Point) { + fn paste(&mut self, frame: Frame) { self.meshes .extend(frame.buffers.into_meshes(frame.clip_bounds)); + self.images.extend(frame.images); self.text.extend(frame.text); } @@ -348,9 +408,32 @@ impl geometry::frame::Backend for Frame { Geometry::Live { meshes: self.meshes, + images: self.images, text: self.text, } } + + fn draw_image(&mut self, bounds: Rectangle, image: impl Into<core::Image>) { + let mut image = image.into(); + + let (bounds, external_rotation) = + self.transforms.current.transform_rectangle(bounds); + + image.rotation += external_rotation; + + self.images.push(Image::Raster(image, bounds)); + } + + fn draw_svg(&mut self, bounds: Rectangle, svg: impl Into<Svg>) { + let mut svg = svg.into(); + + let (bounds, external_rotation) = + self.transforms.current.transform_rectangle(bounds); + + svg.rotation += external_rotation; + + self.images.push(Image::Vector(svg, bounds)); + } } enum Buffer { @@ -518,6 +601,21 @@ impl Transform { gradient } + + fn transform_rectangle( + &self, + rectangle: Rectangle, + ) -> (Rectangle, Radians) { + let top_left = self.transform_point(rectangle.position()); + let top_right = self.transform_point( + rectangle.position() + Vector::new(rectangle.width, 0.0), + ); + let bottom_left = self.transform_point( + rectangle.position() + Vector::new(0.0, rectangle.height), + ); + + Rectangle::with_vertices(top_left, top_right, bottom_left) + } } struct GradientVertex2DBuilder { gradient: gradient::Packed, diff --git a/wgpu/src/image/mod.rs b/wgpu/src/image/mod.rs index daa2fe16..1b16022a 100644 --- a/wgpu/src/image/mod.rs +++ b/wgpu/src/image/mod.rs @@ -149,6 +149,8 @@ impl Pipeline { 6 => Float32x2, // Layer 7 => Sint32, + // Snap + 8 => Uint32, ), }], }, @@ -212,31 +214,24 @@ impl Pipeline { transformation: Transformation, scale: f32, ) { - let transformation = transformation * Transformation::scale(scale); - let nearest_instances: &mut Vec<Instance> = &mut Vec::new(); let linear_instances: &mut Vec<Instance> = &mut Vec::new(); for image in images { match &image { #[cfg(feature = "image")] - Image::Raster { - handle, - filter_method, - bounds, - rotation, - opacity, - } => { + Image::Raster(image, bounds) => { if let Some(atlas_entry) = - cache.upload_raster(device, encoder, handle) + cache.upload_raster(device, encoder, &image.handle) { add_instances( [bounds.x, bounds.y], [bounds.width, bounds.height], - f32::from(*rotation), - *opacity, + f32::from(image.rotation), + image.opacity, + image.snap, atlas_entry, - match filter_method { + match image.filter_method { crate::core::image::FilterMethod::Nearest => { nearest_instances } @@ -251,23 +246,23 @@ impl Pipeline { Image::Raster { .. } => {} #[cfg(feature = "svg")] - Image::Vector { - handle, - color, - bounds, - rotation, - opacity, - } => { + Image::Vector(svg, bounds) => { let size = [bounds.width, bounds.height]; if let Some(atlas_entry) = cache.upload_vector( - device, encoder, handle, *color, size, scale, + device, + encoder, + &svg.handle, + svg.color, + size, + scale, ) { add_instances( [bounds.x, bounds.y], size, - f32::from(*rotation), - *opacity, + f32::from(svg.rotation), + svg.opacity, + true, atlas_entry, nearest_instances, ); @@ -300,6 +295,7 @@ impl Pipeline { nearest_instances, linear_instances, transformation, + scale, ); self.prepare_layer += 1; @@ -375,9 +371,12 @@ impl Layer { nearest_instances: &[Instance], linear_instances: &[Instance], transformation: Transformation, + scale_factor: f32, ) { let uniforms = Uniforms { transform: transformation.into(), + scale_factor, + _padding: [0.0; 3], }; let bytes = bytemuck::bytes_of(&uniforms); @@ -492,6 +491,7 @@ struct Instance { _position_in_atlas: [f32; 2], _size_in_atlas: [f32; 2], _layer: u32, + _snap: u32, } impl Instance { @@ -502,6 +502,10 @@ impl Instance { #[derive(Debug, Clone, Copy, Zeroable, Pod)] struct Uniforms { transform: [f32; 16], + scale_factor: f32, + // Uniforms must be aligned to their largest member, + // this uses a mat4x4<f32> which aligns to 16, so align to that + _padding: [f32; 3], } fn add_instances( @@ -509,6 +513,7 @@ fn add_instances( image_size: [f32; 2], rotation: f32, opacity: f32, + snap: bool, entry: &atlas::Entry, instances: &mut Vec<Instance>, ) { @@ -525,6 +530,7 @@ fn add_instances( image_size, rotation, opacity, + snap, allocation, instances, ); @@ -554,8 +560,8 @@ fn add_instances( ]; add_instance( - position, center, size, rotation, opacity, allocation, - instances, + position, center, size, rotation, opacity, snap, + allocation, instances, ); } } @@ -569,6 +575,7 @@ fn add_instance( size: [f32; 2], rotation: f32, opacity: f32, + snap: bool, allocation: &atlas::Allocation, instances: &mut Vec<Instance>, ) { @@ -591,6 +598,7 @@ fn add_instance( (height as f32 - 1.0) / atlas::SIZE as f32, ], _layer: layer as u32, + _snap: snap as u32, }; instances.push(instance); diff --git a/wgpu/src/image/vector.rs b/wgpu/src/image/vector.rs index c6d829af..74e9924d 100644 --- a/wgpu/src/image/vector.rs +++ b/wgpu/src/image/vector.rs @@ -1,10 +1,9 @@ use crate::core::svg; use crate::core::{Color, Size}; -use crate::graphics::text; use crate::image::atlas::{self, Atlas}; use resvg::tiny_skia; -use resvg::usvg::{self, TreeTextToPath}; +use resvg::usvg; use rustc_hash::{FxHashMap, FxHashSet}; use std::fs; @@ -21,7 +20,7 @@ impl Svg { pub fn viewport_dimensions(&self) -> Size<u32> { match self { Svg::Loaded(tree) => { - let size = tree.size; + let size = tree.size(); Size::new(size.width() as u32, size.height() as u32) } @@ -45,38 +44,33 @@ type ColorFilter = Option<[u8; 4]>; impl Cache { /// Load svg pub fn load(&mut self, handle: &svg::Handle) -> &Svg { - use usvg::TreeParsing; - if self.svgs.contains_key(&handle.id()) { return self.svgs.get(&handle.id()).unwrap(); } - let mut svg = match handle.data() { + let svg = match handle.data() { svg::Data::Path(path) => fs::read_to_string(path) .ok() .and_then(|contents| { - usvg::Tree::from_str(&contents, &usvg::Options::default()) - .ok() + usvg::Tree::from_str( + &contents, + &usvg::Options::default(), // TODO: Set usvg::Options::fontdb + ) + .ok() }) .map(Svg::Loaded) .unwrap_or(Svg::NotFound), svg::Data::Bytes(bytes) => { - match usvg::Tree::from_data(bytes, &usvg::Options::default()) { + match usvg::Tree::from_data( + bytes, + &usvg::Options::default(), // TODO: Set usvg::Options::fontdb + ) { Ok(tree) => Svg::Loaded(tree), Err(_) => Svg::NotFound, } } }; - if let Svg::Loaded(svg) = &mut svg { - if svg.has_text_nodes() { - let mut font_system = - text::font_system().write().expect("Write font system"); - - svg.convert_text(font_system.raw().db_mut()); - } - } - self.should_trim = true; let _ = self.svgs.insert(handle.id(), svg); @@ -127,7 +121,7 @@ impl Cache { // It would be cool to be able to smooth resize the `svg` example. let mut img = tiny_skia::Pixmap::new(width, height)?; - let tree_size = tree.size.to_int_size(); + let tree_size = tree.size().to_int_size(); let target_size = if width > height { tree_size.scale_to_width(width) @@ -147,8 +141,7 @@ impl Cache { tiny_skia::Transform::default() }; - resvg::Tree::from_usvg(tree) - .render(transform, &mut img.as_mut()); + resvg::render(tree, transform, &mut img.as_mut()); let mut rgba = img.take(); diff --git a/wgpu/src/layer.rs b/wgpu/src/layer.rs index 9551311d..68d5a015 100644 --- a/wgpu/src/layer.rs +++ b/wgpu/src/layer.rs @@ -1,5 +1,5 @@ use crate::core::{ - renderer, Background, Color, Point, Radians, Rectangle, Transformation, + self, renderer, Background, Color, Point, Rectangle, Svg, Transformation, }; use crate::graphics; use crate::graphics::color; @@ -20,8 +20,8 @@ pub struct Layer { pub quads: quad::Batch, pub triangles: triangle::Batch, pub primitives: primitive::Batch, - pub text: text::Batch, pub images: image::Batch, + pub text: text::Batch, pending_meshes: Vec<Mesh>, pending_text: Vec<Text>, } @@ -112,42 +112,35 @@ impl Layer { self.pending_text.push(text); } - pub fn draw_image( + pub fn draw_image(&mut self, image: Image, transformation: Transformation) { + match image { + Image::Raster(image, bounds) => { + self.draw_raster(image, bounds, transformation); + } + Image::Vector(svg, bounds) => { + self.draw_svg(svg, bounds, transformation); + } + } + } + + pub fn draw_raster( &mut self, - handle: crate::core::image::Handle, - filter_method: crate::core::image::FilterMethod, + image: core::Image, bounds: Rectangle, transformation: Transformation, - rotation: Radians, - opacity: f32, ) { - let image = Image::Raster { - handle, - filter_method, - bounds: bounds * transformation, - rotation, - opacity, - }; + let image = Image::Raster(image, bounds * transformation); self.images.push(image); } pub fn draw_svg( &mut self, - handle: crate::core::svg::Handle, - color: Option<Color>, + svg: Svg, bounds: Rectangle, transformation: Transformation, - rotation: Radians, - opacity: f32, ) { - let svg = Image::Vector { - handle, - color, - bounds: bounds * transformation, - rotation, - opacity, - }; + let svg = Image::Vector(svg, bounds * transformation); self.images.push(svg); } diff --git a/wgpu/src/lib.rs b/wgpu/src/lib.rs index ad88ce3e..d79f0dc8 100644 --- a/wgpu/src/lib.rs +++ b/wgpu/src/lib.rs @@ -182,19 +182,6 @@ impl Renderer { } } - if !layer.text.is_empty() { - engine.text_pipeline.prepare( - device, - queue, - &self.text_viewport, - encoder, - &mut self.text_storage, - &layer.text, - layer.bounds, - Transformation::scale(scale_factor), - ); - } - #[cfg(any(feature = "svg", feature = "image"))] if !layer.images.is_empty() { engine.image_pipeline.prepare( @@ -207,6 +194,19 @@ impl Renderer { scale_factor, ); } + + if !layer.text.is_empty() { + engine.text_pipeline.prepare( + device, + queue, + &self.text_viewport, + encoder, + &mut self.text_storage, + &layer.text, + layer.bounds, + Transformation::scale(scale_factor), + ); + } } } @@ -359,17 +359,6 @@ impl Renderer { )); } - if !layer.text.is_empty() { - text_layer += engine.text_pipeline.render( - &self.text_viewport, - &self.text_storage, - text_layer, - &layer.text, - scissor_rect, - &mut render_pass, - ); - } - #[cfg(any(feature = "svg", feature = "image"))] if !layer.images.is_empty() { engine.image_pipeline.render( @@ -381,6 +370,17 @@ impl Renderer { image_layer += 1; } + + if !layer.text.is_empty() { + text_layer += engine.text_pipeline.render( + &self.text_viewport, + &self.text_storage, + text_layer, + &layer.text, + scissor_rect, + &mut render_pass, + ); + } } let _ = ManuallyDrop::into_inner(render_pass); @@ -408,6 +408,7 @@ impl Renderer { horizontal_alignment: alignment::Horizontal::Left, vertical_alignment: alignment::Vertical::Top, shaping: core::text::Shaping::Basic, + wrapping: core::text::Wrapping::Word, }; renderer.fill_text( @@ -527,23 +528,9 @@ impl core::image::Renderer for Renderer { self.image_cache.borrow_mut().measure_image(handle) } - fn draw_image( - &mut self, - handle: Self::Handle, - filter_method: core::image::FilterMethod, - bounds: Rectangle, - rotation: core::Radians, - opacity: f32, - ) { + fn draw_image(&mut self, image: core::Image, bounds: Rectangle) { let (layer, transformation) = self.layers.current_mut(); - layer.draw_image( - handle, - filter_method, - bounds, - transformation, - rotation, - opacity, - ); + layer.draw_raster(image, bounds, transformation); } } @@ -553,23 +540,9 @@ impl core::svg::Renderer for Renderer { self.image_cache.borrow_mut().measure_svg(handle) } - fn draw_svg( - &mut self, - handle: core::svg::Handle, - color_filter: Option<Color>, - bounds: Rectangle, - rotation: core::Radians, - opacity: f32, - ) { + fn draw_svg(&mut self, svg: core::Svg, bounds: Rectangle) { let (layer, transformation) = self.layers.current_mut(); - layer.draw_svg( - handle, - color_filter, - bounds, - transformation, - rotation, - opacity, - ); + layer.draw_svg(svg, bounds, transformation); } } @@ -593,8 +566,17 @@ impl graphics::geometry::Renderer for Renderer { let (layer, transformation) = self.layers.current_mut(); match geometry { - Geometry::Live { meshes, text } => { + Geometry::Live { + meshes, + images, + text, + } => { layer.draw_mesh_group(meshes, transformation); + + for image in images { + layer.draw_image(image, transformation); + } + layer.draw_text_group(text, transformation); } Geometry::Cached(cache) => { @@ -602,6 +584,12 @@ impl graphics::geometry::Renderer for Renderer { layer.draw_mesh_cache(meshes, transformation); } + if let Some(images) = cache.images { + for image in images.iter().cloned() { + layer.draw_image(image, transformation); + } + } + if let Some(text) = cache.text { layer.draw_text_cache(text, transformation); } diff --git a/wgpu/src/shader/image.wgsl b/wgpu/src/shader/image.wgsl index 0eeb100f..bc922838 100644 --- a/wgpu/src/shader/image.wgsl +++ b/wgpu/src/shader/image.wgsl @@ -1,5 +1,6 @@ struct Globals { transform: mat4x4<f32>, + scale_factor: f32, } @group(0) @binding(0) var<uniform> globals: Globals; @@ -16,6 +17,7 @@ struct VertexInput { @location(5) atlas_pos: vec2<f32>, @location(6) atlas_scale: vec2<f32>, @location(7) layer: i32, + @location(8) snap: u32, } struct VertexOutput { @@ -38,7 +40,7 @@ fn vs_main(input: VertexInput) -> VertexOutput { out.opacity = input.opacity; // Calculate the vertex position and move the center to the origin - v_pos = round(input.pos) + v_pos * input.scale - input.center; + v_pos = input.pos + v_pos * input.scale - input.center; // Apply the rotation around the center of the image let cos_rot = cos(input.rotation); @@ -51,7 +53,13 @@ fn vs_main(input: VertexInput) -> VertexOutput { ); // Calculate the final position of the vertex - out.position = globals.transform * (vec4<f32>(input.center, 0.0, 0.0) + rotate * vec4<f32>(v_pos, 0.0, 1.0)); + out.position = vec4(vec2(globals.scale_factor), 1.0, 1.0) * (vec4<f32>(input.center, 0.0, 0.0) + rotate * vec4<f32>(v_pos, 0.0, 1.0)); + + if bool(input.snap) { + out.position = round(out.position); + } + + out.position = globals.transform * out.position; return out; } diff --git a/wgpu/src/shader/quad/solid.wgsl b/wgpu/src/shader/quad/solid.wgsl index d908afbc..8eee16bb 100644 --- a/wgpu/src/shader/quad/solid.wgsl +++ b/wgpu/src/shader/quad/solid.wgsl @@ -30,6 +30,15 @@ fn solid_vs_main(input: SolidVertexInput) -> SolidVertexOutput { var pos: vec2<f32> = (input.pos + min(input.shadow_offset, vec2<f32>(0.0, 0.0)) - input.shadow_blur_radius) * globals.scale; var scale: vec2<f32> = (input.scale + vec2<f32>(abs(input.shadow_offset.x), abs(input.shadow_offset.y)) + input.shadow_blur_radius * 2.0) * globals.scale; + var snap: vec2<f32> = vec2<f32>(0.0, 0.0); + + if input.scale.x == 1.0 { + snap.x = round(pos.x) - pos.x; + } + + if input.scale.y == 1.0 { + snap.y = round(pos.y) - pos.y; + } var min_border_radius = min(input.scale.x, input.scale.y) * 0.5; var border_radius: vec4<f32> = vec4<f32>( @@ -43,13 +52,13 @@ fn solid_vs_main(input: SolidVertexInput) -> SolidVertexOutput { vec4<f32>(scale.x + 1.0, 0.0, 0.0, 0.0), vec4<f32>(0.0, scale.y + 1.0, 0.0, 0.0), vec4<f32>(0.0, 0.0, 1.0, 0.0), - vec4<f32>(pos - vec2<f32>(0.5, 0.5), 0.0, 1.0) + vec4<f32>(pos - vec2<f32>(0.5, 0.5) + snap, 0.0, 1.0) ); out.position = globals.transform * transform * vec4<f32>(vertex_position(input.vertex_index), 0.0, 1.0); out.color = input.color; out.border_color = input.border_color; - out.pos = input.pos * globals.scale; + out.pos = input.pos * globals.scale + snap; out.scale = input.scale * globals.scale; out.border_radius = border_radius * globals.scale; out.border_width = input.border_width * globals.scale; diff --git a/wgpu/src/text.rs b/wgpu/src/text.rs index 05db5f80..bf7eae18 100644 --- a/wgpu/src/text.rs +++ b/wgpu/src/text.rs @@ -585,7 +585,13 @@ fn prepare( ( buffer.as_ref(), - Rectangle::new(raw.position, Size::new(width, height)), + Rectangle::new( + raw.position, + Size::new( + width.unwrap_or(layer_bounds.width), + height.unwrap_or(layer_bounds.height), + ), + ), alignment::Horizontal::Left, alignment::Vertical::Top, raw.color, diff --git a/widget/Cargo.toml b/widget/Cargo.toml index 3c9f6a54..98a81145 100644 --- a/widget/Cargo.toml +++ b/widget/Cargo.toml @@ -22,8 +22,10 @@ lazy = ["ouroboros"] image = ["iced_renderer/image"] svg = ["iced_renderer/svg"] canvas = ["iced_renderer/geometry"] -qr_code = ["canvas", "qrcode"] +qr_code = ["canvas", "dep:qrcode"] wgpu = ["iced_renderer/wgpu"] +markdown = ["dep:pulldown-cmark", "dep:url"] +highlighter = ["dep:iced_highlighter"] advanced = [] [dependencies] @@ -31,6 +33,7 @@ iced_renderer.workspace = true iced_runtime.workspace = true num-traits.workspace = true +once_cell.workspace = true rustc-hash.workspace = true thiserror.workspace = true unicode-segmentation.workspace = true @@ -40,3 +43,12 @@ ouroboros.optional = true qrcode.workspace = true qrcode.optional = true + +pulldown-cmark.workspace = true +pulldown-cmark.optional = true + +iced_highlighter.workspace = true +iced_highlighter.optional = true + +url.workspace = true +url.optional = true diff --git a/widget/assets/iced-logo.svg b/widget/assets/iced-logo.svg new file mode 100644 index 00000000..459b7fbb --- /dev/null +++ b/widget/assets/iced-logo.svg @@ -0,0 +1,2 @@ +<?xml version="1.0" encoding="UTF-8"?> +<svg xmlns="http://www.w3.org/2000/svg" width="140" height="140" fill="none" version="1.1" viewBox="35 31 179 171"><rect x="42" y="31.001" width="169.9" height="169.9" rx="49.815" fill="url(#paint1_linear)"/><path d="m182.62 65.747-28.136 28.606-6.13-6.0291 28.136-28.606 6.13 6.0291zm-26.344 0.218-42.204 42.909-6.13-6.029 42.204-42.909 6.13 6.0291zm-61.648 23.913c5.3254-5.3831 10.65-10.765 21.569-21.867l6.13 6.0291c-10.927 11.11-16.258 16.498-21.587 21.885-4.4007 4.4488-8.8009 8.8968-16.359 16.573l31.977 8.358 25.968-26.402 6.13 6.0292-25.968 26.402 8.907 31.908 42.138-42.087 6.076 6.083-49.109 49.05-45.837-12.628-13.394-45.646 1.7714-1.801c10.928-11.111 16.258-16.499 21.588-21.886zm28.419 70.99-8.846-31.689-31.831-8.32 9.1945 31.335 31.482 8.674zm47.734-56.517 7.122-7.1221-6.08-6.0797-7.147 7.1474-30.171 30.674 6.13 6.029 30.146-30.649z" clip-rule="evenodd" fill="url(#paint2_linear)" fill-rule="evenodd"/><defs><filter id="filter0_f" x="55" y="47.001" width="144" height="168" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur" stdDeviation="2"/></filter><linearGradient id="paint0_linear" x1="127" x2="127" y1="51.001" y2="211" gradientUnits="userSpaceOnUse"><stop offset=".052083"/><stop stop-opacity=".08" offset="1"/></linearGradient><linearGradient id="paint1_linear" x1="212" x2="57.5" y1="31.001" y2="189" gradientUnits="userSpaceOnUse"><stop stop-color="#00A3FF" offset="0"/><stop stop-color="#30f" offset="1"/></linearGradient><linearGradient id="paint2_linear" x1="86.098" x2="206.01" y1="158.28" y2="35.327" gradientUnits="userSpaceOnUse"><stop stop-color="#fff" offset="0"/><stop stop-color="#fff" offset="1"/></linearGradient></defs></svg> diff --git a/widget/src/button.rs b/widget/src/button.rs index 5d446fea..eafa71b9 100644 --- a/widget/src/button.rs +++ b/widget/src/button.rs @@ -1,4 +1,5 @@ //! Allow your users to perform actions by pressing a button. +use crate::core::border::{self, Border}; use crate::core::event::{self, Event}; use crate::core::layout; use crate::core::mouse; @@ -9,8 +10,8 @@ use crate::core::touch; use crate::core::widget::tree::{self, Tree}; use crate::core::widget::Operation; use crate::core::{ - Background, Border, Clipboard, Color, Element, Layout, Length, Padding, - Rectangle, Shadow, Shell, Size, Theme, Vector, Widget, + Background, Clipboard, Color, Element, Layout, Length, Padding, Rectangle, + Shadow, Shell, Size, Theme, Vector, Widget, }; /// A generic widget that produces a message when pressed. @@ -52,7 +53,7 @@ where Theme: Catalog, { content: Element<'a, Message, Theme, Renderer>, - on_press: Option<Message>, + on_press: Option<OnPress<'a, Message>>, width: Length, height: Length, padding: Padding, @@ -60,6 +61,20 @@ where class: Theme::Class<'a>, } +enum OnPress<'a, Message> { + Direct(Message), + Closure(Box<dyn Fn() -> Message + 'a>), +} + +impl<'a, Message: Clone> OnPress<'a, Message> { + fn get(&self) -> Message { + match self { + OnPress::Direct(message) => message.clone(), + OnPress::Closure(f) => f(), + } + } +} + impl<'a, Message, Theme, Renderer> Button<'a, Message, Theme, Renderer> where Renderer: crate::core::Renderer, @@ -105,7 +120,23 @@ where /// /// Unless `on_press` is called, the [`Button`] will be disabled. pub fn on_press(mut self, on_press: Message) -> Self { - self.on_press = Some(on_press); + self.on_press = Some(OnPress::Direct(on_press)); + self + } + + /// Sets the message that will be produced when the [`Button`] is pressed. + /// + /// This is analogous to [`Button::on_press`], but using a closure to produce + /// the message. + /// + /// This closure will only be called when the [`Button`] is actually pressed and, + /// therefore, this method is useful to reduce overhead if creating the resulting + /// message is slow. + pub fn on_press_with( + mut self, + on_press: impl Fn() -> Message + 'a, + ) -> Self { + self.on_press = Some(OnPress::Closure(Box::new(on_press))); self } @@ -114,7 +145,7 @@ where /// /// If `None`, the [`Button`] will be disabled. pub fn on_press_maybe(mut self, on_press: Option<Message>) -> Self { - self.on_press = on_press; + self.on_press = on_press.map(OnPress::Direct); self } @@ -205,7 +236,7 @@ where tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn Operation<()>, + operation: &mut dyn Operation, ) { operation.container(None, layout.bounds(), &mut |operation| { self.content.as_widget().operate( @@ -258,7 +289,8 @@ where } Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) | Event::Touch(touch::Event::FingerLifted { .. }) => { - if let Some(on_press) = self.on_press.clone() { + if let Some(on_press) = self.on_press.as_ref().map(OnPress::get) + { let state = tree.state.downcast_mut::<State>(); if state.is_pressed { @@ -560,7 +592,7 @@ fn styled(pair: palette::Pair) -> Style { Style { background: Some(Background::Color(pair.color)), text_color: pair.text, - border: Border::rounded(2), + border: border::rounded(2), ..Style::default() } } diff --git a/widget/src/canvas.rs b/widget/src/canvas.rs index 73cef087..185fa082 100644 --- a/widget/src/canvas.rs +++ b/widget/src/canvas.rs @@ -8,8 +8,8 @@ pub use program::Program; pub use crate::graphics::cache::Group; pub use crate::graphics::geometry::{ - fill, gradient, path, stroke, Fill, Gradient, LineCap, LineDash, LineJoin, - Path, Stroke, Style, Text, + fill, gradient, path, stroke, Fill, Gradient, Image, LineCap, LineDash, + LineJoin, Path, Stroke, Style, Text, }; use crate::core; diff --git a/widget/src/checkbox.rs b/widget/src/checkbox.rs index 225c316d..32db5090 100644 --- a/widget/src/checkbox.rs +++ b/widget/src/checkbox.rs @@ -50,6 +50,7 @@ pub struct Checkbox< text_size: Option<Pixels>, text_line_height: text::LineHeight, text_shaping: text::Shaping, + text_wrapping: text::Wrapping, font: Option<Renderer::Font>, icon: Icon<Renderer::Font>, class: Theme::Class<'a>, @@ -81,7 +82,8 @@ where spacing: Self::DEFAULT_SPACING, text_size: None, text_line_height: text::LineHeight::default(), - text_shaping: text::Shaping::Basic, + text_shaping: text::Shaping::default(), + text_wrapping: text::Wrapping::default(), font: None, icon: Icon { font: Renderer::ICON_FONT, @@ -158,6 +160,12 @@ where self } + /// Sets the [`text::Wrapping`] strategy of the [`Checkbox`]. + pub fn text_wrapping(mut self, wrapping: text::Wrapping) -> Self { + self.text_wrapping = wrapping; + self + } + /// Sets the [`Renderer::Font`] of the text of the [`Checkbox`]. /// /// [`Renderer::Font`]: crate::core::text::Renderer @@ -240,6 +248,7 @@ where alignment::Horizontal::Left, alignment::Vertical::Top, self.text_shaping, + self.text_wrapping, ) }, ) @@ -348,6 +357,7 @@ where horizontal_alignment: alignment::Horizontal::Center, vertical_alignment: alignment::Vertical::Center, shaping: *shaping, + wrapping: text::Wrapping::default(), }, bounds.center(), style.icon_color, @@ -358,12 +368,14 @@ where { let label_layout = children.next().unwrap(); + let state: &widget::text::State<Renderer::Paragraph> = + tree.state.downcast_ref(); crate::text::draw( renderer, defaults, label_layout, - tree.state.downcast_ref(), + state.0.raw(), crate::text::Style { color: style.text_color, }, diff --git a/widget/src/column.rs b/widget/src/column.rs index 0b81c545..d3ea4cf7 100644 --- a/widget/src/column.rs +++ b/widget/src/column.rs @@ -1,4 +1,5 @@ //! Distribute content vertically. +use crate::core::alignment::{self, Alignment}; use crate::core::event::{self, Event}; use crate::core::layout; use crate::core::mouse; @@ -6,8 +7,8 @@ use crate::core::overlay; use crate::core::renderer; use crate::core::widget::{Operation, Tree}; use crate::core::{ - Alignment, Clipboard, Element, Layout, Length, Padding, Pixels, Rectangle, - Shell, Size, Vector, Widget, + Clipboard, Element, Layout, Length, Padding, Pixels, Rectangle, Shell, + Size, Vector, Widget, }; /// A container that distributes its contents vertically. @@ -19,7 +20,7 @@ pub struct Column<'a, Message, Theme = crate::Theme, Renderer = crate::Renderer> width: Length, height: Length, max_width: f32, - align_items: Alignment, + align: Alignment, clip: bool, children: Vec<Element<'a, Message, Theme, Renderer>>, } @@ -63,7 +64,7 @@ where width: Length::Shrink, height: Length::Shrink, max_width: f32::INFINITY, - align_items: Alignment::Start, + align: Alignment::Start, clip: false, children, } @@ -104,8 +105,8 @@ where } /// Sets the horizontal alignment of the contents of the [`Column`] . - pub fn align_items(mut self, align: Alignment) -> Self { - self.align_items = align; + pub fn align_x(mut self, align: impl Into<alignment::Horizontal>) -> Self { + self.align = Alignment::from(align.into()); self } @@ -210,7 +211,7 @@ where self.height, self.padding, self.spacing, - self.align_items, + self.align, &self.children, &mut tree.children, ) @@ -221,7 +222,7 @@ where tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn Operation<()>, + operation: &mut dyn Operation, ) { operation.container(None, layout.bounds(), &mut |operation| { self.children diff --git a/widget/src/combo_box.rs b/widget/src/combo_box.rs index 253850df..62785b2c 100644 --- a/widget/src/combo_box.rs +++ b/widget/src/combo_box.rs @@ -208,12 +208,14 @@ where /// The local state of a [`ComboBox`]. #[derive(Debug, Clone)] -pub struct State<T>(RefCell<Inner<T>>); +pub struct State<T> { + options: Vec<T>, + inner: RefCell<Inner<T>>, +} #[derive(Debug, Clone)] struct Inner<T> { value: String, - options: Vec<T>, option_matchers: Vec<String>, filtered_options: Filtered<T>, } @@ -247,39 +249,58 @@ where .collect(), ); - Self(RefCell::new(Inner { - value, + Self { options, - option_matchers, - filtered_options, - })) + inner: RefCell::new(Inner { + value, + option_matchers, + filtered_options, + }), + } + } + + /// Returns the options of the [`State`]. + /// + /// These are the options provided when the [`State`] + /// was constructed with [`State::new`]. + pub fn options(&self) -> &[T] { + &self.options } fn value(&self) -> String { - let inner = self.0.borrow(); + let inner = self.inner.borrow(); inner.value.clone() } fn with_inner<O>(&self, f: impl FnOnce(&Inner<T>) -> O) -> O { - let inner = self.0.borrow(); + let inner = self.inner.borrow(); f(&inner) } fn with_inner_mut(&self, f: impl FnOnce(&mut Inner<T>)) { - let mut inner = self.0.borrow_mut(); + let mut inner = self.inner.borrow_mut(); f(&mut inner); } fn sync_filtered_options(&self, options: &mut Filtered<T>) { - let inner = self.0.borrow(); + let inner = self.inner.borrow(); inner.filtered_options.sync(options); } } +impl<T> Default for State<T> +where + T: Display + Clone, +{ + fn default() -> Self { + Self::new(Vec::new()) + } +} + impl<T> Filtered<T> where T: Clone, @@ -431,7 +452,7 @@ where state.filtered_options.update( search( - &state.options, + &self.state.options, &state.option_matchers, &state.value, ) @@ -580,7 +601,7 @@ where if let Some(selection) = menu.new_selection.take() { // Clear the value and reset the options and menu state.value = String::new(); - state.filtered_options.update(state.options.clone()); + state.filtered_options.update(self.state.options.clone()); menu.menu = menu::State::default(); // Notify the selection diff --git a/widget/src/container.rs b/widget/src/container.rs index e917471f..3b794099 100644 --- a/widget/src/container.rs +++ b/widget/src/container.rs @@ -1,5 +1,6 @@ //! Decorate content and apply alignment. use crate::core::alignment::{self, Alignment}; +use crate::core::border::{self, Border}; use crate::core::event::{self, Event}; use crate::core::gradient::{self, Gradient}; use crate::core::layout; @@ -9,11 +10,11 @@ use crate::core::renderer; use crate::core::widget::tree::{self, Tree}; use crate::core::widget::{self, Operation}; use crate::core::{ - self, Background, Border, Clipboard, Color, Element, Layout, Length, + self, color, Background, Clipboard, Color, Element, Layout, Length, Padding, Pixels, Point, Rectangle, Shadow, Shell, Size, Theme, Vector, Widget, }; -use crate::runtime::Task; +use crate::runtime::task::{self, Task}; /// An element decorating some content. /// @@ -92,46 +93,6 @@ where self } - /// Sets the [`Container`] to fill the available space in the horizontal axis. - /// - /// This can be useful to quickly position content when chained with - /// alignment functions—like [`center_x`]. - /// - /// Calling this method is equivalent to calling [`width`] with a - /// [`Length::Fill`]. - /// - /// [`center_x`]: Self::center_x - /// [`width`]: Self::width - pub fn fill_x(self) -> Self { - self.width(Length::Fill) - } - - /// Sets the [`Container`] to fill the available space in the vetical axis. - /// - /// This can be useful to quickly position content when chained with - /// alignment functions—like [`center_y`]. - /// - /// Calling this method is equivalent to calling [`height`] with a - /// [`Length::Fill`]. - /// - /// [`center_y`]: Self::center_x - /// [`height`]: Self::height - pub fn fill_y(self) -> Self { - self.height(Length::Fill) - } - - /// Sets the [`Container`] to fill all the available space. - /// - /// Calling this method is equivalent to chaining [`fill_x`] and - /// [`fill_y`]. - /// - /// [`center`]: Self::center - /// [`fill_x`]: Self::fill_x - /// [`fill_y`]: Self::fill_y - pub fn fill(self) -> Self { - self.width(Length::Fill).height(Length::Fill) - } - /// Sets the maximum width of the [`Container`]. pub fn max_width(mut self, max_width: impl Into<Pixels>) -> Self { self.max_width = max_width.into().0; @@ -144,18 +105,6 @@ where self } - /// Sets the content alignment for the horizontal axis of the [`Container`]. - pub fn align_x(mut self, alignment: alignment::Horizontal) -> Self { - self.horizontal_alignment = alignment; - self - } - - /// Sets the content alignment for the vertical axis of the [`Container`]. - pub fn align_y(mut self, alignment: alignment::Vertical) -> Self { - self.vertical_alignment = alignment; - self - } - /// Sets the width of the [`Container`] and centers its contents horizontally. pub fn center_x(self, width: impl Into<Length>) -> Self { self.width(width).align_x(alignment::Horizontal::Center) @@ -179,6 +128,44 @@ where self.center_x(length).center_y(length) } + /// Aligns the contents of the [`Container`] to the left. + pub fn align_left(self, width: impl Into<Length>) -> Self { + self.width(width).align_x(alignment::Horizontal::Left) + } + + /// Aligns the contents of the [`Container`] to the right. + pub fn align_right(self, width: impl Into<Length>) -> Self { + self.width(width).align_x(alignment::Horizontal::Right) + } + + /// Aligns the contents of the [`Container`] to the top. + pub fn align_top(self, height: impl Into<Length>) -> Self { + self.height(height).align_y(alignment::Vertical::Top) + } + + /// Aligns the contents of the [`Container`] to the bottom. + pub fn align_bottom(self, height: impl Into<Length>) -> Self { + self.height(height).align_y(alignment::Vertical::Bottom) + } + + /// Sets the content alignment for the horizontal axis of the [`Container`]. + pub fn align_x( + mut self, + alignment: impl Into<alignment::Horizontal>, + ) -> Self { + self.horizontal_alignment = alignment.into(); + self + } + + /// Sets the content alignment for the vertical axis of the [`Container`]. + pub fn align_y( + mut self, + alignment: impl Into<alignment::Vertical>, + ) -> Self { + self.vertical_alignment = alignment.into(); + self + } + /// Sets whether the contents of the [`Container`] should be clipped on /// overflow. pub fn clip(mut self, clip: bool) -> Self { @@ -197,7 +184,6 @@ where } /// Sets the style class of the [`Container`]. - #[cfg(feature = "advanced")] #[must_use] pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self { self.class = class.into(); @@ -258,7 +244,7 @@ where tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn Operation<()>, + operation: &mut dyn Operation, ) { operation.container( self.id.as_ref().map(|id| &id.0), @@ -473,6 +459,7 @@ pub fn visible_bounds(id: Id) -> Task<Option<Rectangle>> { _state: &mut dyn widget::operation::Scrollable, _id: Option<&widget::Id>, bounds: Rectangle, + _content_bounds: Rectangle, translation: Vector, ) { match self.scrollables.last() { @@ -538,7 +525,7 @@ pub fn visible_bounds(id: Id) -> Task<Option<Rectangle>> { } } - Task::widget(VisibleBounds { + task::widget(VisibleBounds { target: id.into(), depth: 0, scrollables: Vec::new(), @@ -560,46 +547,54 @@ pub struct Style { } impl Style { - /// Updates the border of the [`Style`] with the given [`Color`] and `width`. - pub fn with_border( - self, - color: impl Into<Color>, - width: impl Into<Pixels>, - ) -> Self { + /// Updates the text color of the [`Style`]. + pub fn color(self, color: impl Into<Color>) -> Self { Self { - border: Border { - color: color.into(), - width: width.into().0, - ..Border::default() - }, + text_color: Some(color.into()), + ..self + } + } + + /// Updates the border of the [`Style`]. + pub fn border(self, border: impl Into<Border>) -> Self { + Self { + border: border.into(), ..self } } /// Updates the background of the [`Style`]. - pub fn with_background(self, background: impl Into<Background>) -> Self { + pub fn background(self, background: impl Into<Background>) -> Self { Self { background: Some(background.into()), ..self } } + + /// Updates the shadow of the [`Style`]. + pub fn shadow(self, shadow: impl Into<Shadow>) -> Self { + Self { + shadow: shadow.into(), + ..self + } + } } impl From<Color> for Style { fn from(color: Color) -> Self { - Self::default().with_background(color) + Self::default().background(color) } } impl From<Gradient> for Style { fn from(gradient: Gradient) -> Self { - Self::default().with_background(gradient) + Self::default().background(gradient) } } impl From<gradient::Linear> for Style { fn from(gradient: gradient::Linear) -> Self { - Self::default().with_background(gradient) + Self::default().background(gradient) } } @@ -618,6 +613,12 @@ pub trait Catalog { /// A styling function for a [`Container`]. pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme) -> Style + 'a>; +impl<'a, Theme> From<Style> for StyleFn<'a, Theme> { + fn from(style: Style) -> Self { + Box::new(move |_theme| style) + } +} + impl Catalog for Theme { type Class<'a> = StyleFn<'a, Self>; @@ -635,13 +636,18 @@ pub fn transparent<Theme>(_theme: &Theme) -> Style { Style::default() } +/// A [`Container`] with the given [`Background`]. +pub fn background(background: impl Into<Background>) -> Style { + Style::default().background(background) +} + /// A rounded [`Container`] with a background. pub fn rounded_box(theme: &Theme) -> Style { let palette = theme.extended_palette(); Style { background: Some(palette.background.weak.color.into()), - border: Border::rounded(2), + border: border::rounded(2), ..Style::default() } } @@ -660,3 +666,13 @@ pub fn bordered_box(theme: &Theme) -> Style { ..Style::default() } } + +/// A [`Container`] with a dark background and white text. +pub fn dark(_theme: &Theme) -> Style { + Style { + background: Some(color!(0x111111).into()), + text_color: Some(Color::WHITE), + border: border::rounded(2), + ..Style::default() + } +} diff --git a/widget/src/helpers.rs b/widget/src/helpers.rs index 62343a55..51978823 100644 --- a/widget/src/helpers.rs +++ b/widget/src/helpers.rs @@ -4,7 +4,8 @@ use crate::checkbox::{self, Checkbox}; use crate::combo_box::{self, ComboBox}; use crate::container::{self, Container}; use crate::core; -use crate::core::widget::operation; +use crate::core::widget::operation::{self, Operation}; +use crate::core::window; use crate::core::{Element, Length, Pixels, Widget}; use crate::keyed; use crate::overlay; @@ -12,7 +13,8 @@ use crate::pick_list::{self, PickList}; use crate::progress_bar::{self, ProgressBar}; use crate::radio::{self, Radio}; use crate::rule::{self, Rule}; -use crate::runtime::{Action, Task}; +use crate::runtime::task::{self, Task}; +use crate::runtime::Action; use crate::scrollable::{self, Scrollable}; use crate::slider::{self, Slider}; use crate::text::{self, Text}; @@ -111,6 +113,19 @@ macro_rules! text { }; } +/// Creates some [`Rich`] text with the given spans. +/// +/// [`Rich`]: text::Rich +#[macro_export] +macro_rules! rich_text { + () => ( + $crate::Column::new() + ); + ($($x:expr),+ $(,)?) => ( + $crate::text::Rich::from_iter([$($crate::text::Span::from($x)),+]) + ); +} + /// Creates a new [`Container`] with the provided content. /// /// [`Container`]: crate::Container @@ -275,7 +290,7 @@ where state: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn operation::Operation<()>, + operation: &mut dyn operation::Operation, ) { self.content .as_widget() @@ -383,6 +398,7 @@ where struct Hover<'a, Message, Theme, Renderer> { base: Element<'a, Message, Theme, Renderer>, top: Element<'a, Message, Theme, Renderer>, + is_top_focused: bool, is_top_overlay_active: bool, } @@ -458,7 +474,9 @@ where viewport, ); - if cursor.is_over(layout.bounds()) || self.is_top_overlay_active + if cursor.is_over(layout.bounds()) + || self.is_top_focused + || self.is_top_overlay_active { let (top_layout, top_tree) = children.next().unwrap(); @@ -477,7 +495,7 @@ where tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn operation::Operation<()>, + operation: &mut dyn operation::Operation, ) { let children = [&self.base, &self.top] .into_iter() @@ -501,6 +519,24 @@ where ) -> event::Status { let mut children = layout.children().zip(&mut tree.children); let (base_layout, base_tree) = children.next().unwrap(); + let (top_layout, top_tree) = children.next().unwrap(); + + if matches!(event, Event::Window(window::Event::RedrawRequested(_))) + { + let mut count_focused = operation::focusable::count(); + + self.top.as_widget_mut().operate( + top_tree, + top_layout, + renderer, + &mut operation::black_box(&mut count_focused), + ); + + self.is_top_focused = match count_focused.finish() { + operation::Outcome::Some(count) => count.focused.is_some(), + _ => false, + }; + } let top_status = if matches!( event, @@ -509,9 +545,9 @@ where | mouse::Event::ButtonReleased(_) ) ) || cursor.is_over(layout.bounds()) + || self.is_top_focused + || self.is_top_overlay_active { - let (top_layout, top_tree) = children.next().unwrap(); - self.top.as_widget_mut().on_event( top_tree, event.clone(), @@ -597,6 +633,7 @@ where Element::new(Hover { base: base.into(), top: top.into(), + is_top_focused: false, is_top_overlay_active: false, }) } @@ -645,8 +682,6 @@ where } /// Creates a new [`Text`] widget with the provided content. -/// -/// [`Text`]: core::widget::Text pub fn text<'a, Theme, Renderer>( text: impl text::IntoFragment<'a>, ) -> Text<'a, Theme, Renderer> @@ -658,8 +693,6 @@ where } /// Creates a new [`Text`] widget that displays the provided value. -/// -/// [`Text`]: core::widget::Text pub fn value<'a, Theme, Renderer>( value: impl ToString, ) -> Text<'a, Theme, Renderer> @@ -670,6 +703,34 @@ where Text::new(value.to_string()) } +/// Creates a new [`Rich`] text widget with the provided spans. +/// +/// [`Rich`]: text::Rich +pub fn rich_text<'a, Link, Theme, Renderer>( + spans: impl AsRef<[text::Span<'a, Link, Renderer::Font>]> + 'a, +) -> text::Rich<'a, Link, Theme, Renderer> +where + Link: Clone + 'static, + Theme: text::Catalog + 'a, + Renderer: core::text::Renderer, + Renderer::Font: 'a, +{ + text::Rich::with_spans(spans) +} + +/// Creates a new [`Span`] of text with the provided content. +/// +/// [`Span`]: text::Span +pub fn span<'a, Link, Font>( + text: impl text::IntoFragment<'a>, +) -> text::Span<'a, Link, Font> { + text::Span::new(text) +} + +#[cfg(feature = "markdown")] +#[doc(inline)] +pub use crate::markdown::view as markdown; + /// Creates a new [`Checkbox`]. /// /// [`Checkbox`]: crate::Checkbox @@ -706,15 +767,13 @@ where /// /// [`Toggler`]: crate::Toggler pub fn toggler<'a, Message, Theme, Renderer>( - label: impl Into<Option<String>>, is_checked: bool, - f: impl Fn(bool) -> Message + 'a, ) -> Toggler<'a, Message, Theme, Renderer> where Theme: toggler::Catalog + 'a, Renderer: core::text::Renderer, { - Toggler::new(label, is_checked, f) + Toggler::new(is_checked) } /// Creates a new [`TextInput`]. @@ -889,6 +948,41 @@ where crate::Svg::new(handle) } +/// Creates an [`Element`] that displays the iced logo with the given `text_size`. +/// +/// Useful for showing some love to your favorite GUI library in your "About" screen, +/// for instance. +#[cfg(feature = "svg")] +pub fn iced<'a, Message, Theme, Renderer>( + text_size: impl Into<Pixels>, +) -> Element<'a, Message, Theme, Renderer> +where + Message: 'a, + Renderer: core::Renderer + + core::text::Renderer<Font = core::Font> + + core::svg::Renderer + + 'a, + Theme: text::Catalog + crate::svg::Catalog + 'a, +{ + use crate::core::{Alignment, Font}; + use crate::svg; + use once_cell::sync::Lazy; + + static LOGO: Lazy<svg::Handle> = Lazy::new(|| { + svg::Handle::from_memory(include_bytes!("../assets/iced-logo.svg")) + }); + + let text_size = text_size.into(); + + row![ + svg(LOGO.clone()).width(text_size * 1.3), + text("iced").size(text_size).font(Font::MONOSPACE) + ] + .spacing(text_size.0 / 3.0) + .align_y(Alignment::Center) + .into() +} + /// Creates a new [`Canvas`]. /// /// [`Canvas`]: crate::Canvas @@ -930,12 +1024,12 @@ where /// Focuses the previous focusable widget. pub fn focus_previous<T>() -> Task<T> { - Task::effect(Action::widget(operation::focusable::focus_previous())) + task::effect(Action::widget(operation::focusable::focus_previous())) } /// Focuses the next focusable widget. pub fn focus_next<T>() -> Task<T> { - Task::effect(Action::widget(operation::focusable::focus_next())) + task::effect(Action::widget(operation::focusable::focus_next())) } /// A container intercepting mouse events. diff --git a/widget/src/image.rs b/widget/src/image.rs index 80e17263..e04f2d6f 100644 --- a/widget/src/image.rs +++ b/widget/src/image.rs @@ -43,7 +43,7 @@ pub struct Image<Handle> { impl<Handle> Image<Handle> { /// Creates a new [`Image`] with the given path. - pub fn new<T: Into<Handle>>(handle: T) -> Self { + pub fn new(handle: impl Into<Handle>) -> Self { Image { handle: handle.into(), width: Length::Shrink, @@ -181,11 +181,14 @@ pub fn draw<Renderer, Handle>( let render = |renderer: &mut Renderer| { renderer.draw_image( - handle.clone(), - filter_method, + image::Image { + handle: handle.clone(), + filter_method, + rotation: rotation.radians(), + opacity, + snap: true, + }, drawing_bounds, - rotation.radians(), - opacity, ); }; diff --git a/widget/src/image/viewer.rs b/widget/src/image/viewer.rs index b8b69b60..b1aad22c 100644 --- a/widget/src/image/viewer.rs +++ b/widget/src/image/viewer.rs @@ -6,8 +6,8 @@ use crate::core::mouse; use crate::core::renderer; use crate::core::widget::tree::{self, Tree}; use crate::core::{ - Clipboard, ContentFit, Element, Layout, Length, Pixels, Point, Radians, - Rectangle, Shell, Size, Vector, Widget, + Clipboard, ContentFit, Element, Image, Layout, Length, Pixels, Point, + Radians, Rectangle, Shell, Size, Vector, Widget, }; /// A frame that displays an image with the ability to zoom in/out and pan. @@ -349,11 +349,14 @@ where let render = |renderer: &mut Renderer| { renderer.with_translation(translation, |renderer| { renderer.draw_image( - self.handle.clone(), - self.filter_method, + Image { + handle: self.handle.clone(), + filter_method: self.filter_method, + rotation: Radians(0.0), + opacity: 1.0, + snap: true, + }, drawing_bounds, - Radians(0.0), - 1.0, ); }); }; diff --git a/widget/src/keyed/column.rs b/widget/src/keyed/column.rs index 69991d1f..2c56c605 100644 --- a/widget/src/keyed/column.rs +++ b/widget/src/keyed/column.rs @@ -265,7 +265,7 @@ where tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn Operation<()>, + operation: &mut dyn Operation, ) { operation.container(None, layout.bounds(), &mut |operation| { self.children diff --git a/widget/src/lazy.rs b/widget/src/lazy.rs index 606da22d..221f9de3 100644 --- a/widget/src/lazy.rs +++ b/widget/src/lazy.rs @@ -4,6 +4,7 @@ pub(crate) mod helpers; pub mod component; pub mod responsive; +#[allow(deprecated)] pub use component::Component; pub use responsive::Responsive; @@ -29,6 +30,7 @@ use std::hash::{Hash, Hasher as H}; use std::rc::Rc; /// A widget that only rebuilds its contents when necessary. +#[cfg(feature = "lazy")] #[allow(missing_debug_implementations)] pub struct Lazy<'a, Message, Theme, Renderer, Dependency, View> { dependency: Dependency, @@ -182,7 +184,7 @@ where tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn widget::Operation<()>, + operation: &mut dyn widget::Operation, ) { self.with_element(|element| { element.as_widget().operate( diff --git a/widget/src/lazy/component.rs b/widget/src/lazy/component.rs index f079c0df..659bc476 100644 --- a/widget/src/lazy/component.rs +++ b/widget/src/lazy/component.rs @@ -1,4 +1,5 @@ //! Build and reuse custom widgets using The Elm Architecture. +#![allow(deprecated)] use crate::core::event; use crate::core::layout::{self, Layout}; use crate::core::mouse; @@ -30,6 +31,12 @@ use std::rc::Rc; /// /// Additionally, a [`Component`] is capable of producing a `Message` to notify /// the parent application of any relevant interactions. +#[cfg(feature = "lazy")] +#[deprecated( + since = "0.13.0", + note = "components introduce encapsulated state and hamper the use of a single source of truth. \ + Instead, leverage the Elm Architecture directly, or implement a custom widget" +)] pub trait Component<Message, Theme = crate::Theme, Renderer = crate::Renderer> { /// The internal state of this [`Component`]. type State: Default; @@ -59,7 +66,7 @@ pub trait Component<Message, Theme = crate::Theme, Renderer = crate::Renderer> { fn operate( &self, _state: &mut Self::State, - _operation: &mut dyn widget::Operation<()>, + _operation: &mut dyn widget::Operation, ) { } @@ -172,7 +179,7 @@ where fn rebuild_element_with_operation( &self, - operation: &mut dyn widget::Operation<()>, + operation: &mut dyn widget::Operation, ) { let heads = self.state.borrow_mut().take().unwrap().into_heads(); @@ -358,7 +365,7 @@ where tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn widget::Operation<()>, + operation: &mut dyn widget::Operation, ) { self.rebuild_element_with_operation(operation); diff --git a/widget/src/lazy/helpers.rs b/widget/src/lazy/helpers.rs index 4d0776ca..52e690ff 100644 --- a/widget/src/lazy/helpers.rs +++ b/widget/src/lazy/helpers.rs @@ -1,9 +1,11 @@ use crate::core::{self, Element, Size}; -use crate::lazy::component::{self, Component}; -use crate::lazy::{Lazy, Responsive}; +use crate::lazy::component; use std::hash::Hash; +#[allow(deprecated)] +pub use crate::lazy::{Component, Lazy, Responsive}; + /// Creates a new [`Lazy`] widget with the given data `Dependency` and a /// closure that can turn this data into a widget tree. #[cfg(feature = "lazy")] @@ -21,6 +23,12 @@ where /// Turns an implementor of [`Component`] into an [`Element`] that can be /// embedded in any application. #[cfg(feature = "lazy")] +#[deprecated( + since = "0.13.0", + note = "components introduce encapsulated state and hamper the use of a single source of truth. \ + Instead, leverage the Elm Architecture directly, or implement a custom widget" +)] +#[allow(deprecated)] pub fn component<'a, C, Message, Theme, Renderer>( component: C, ) -> Element<'a, Message, Theme, Renderer> diff --git a/widget/src/lazy/responsive.rs b/widget/src/lazy/responsive.rs index 27f52617..dbf281f3 100644 --- a/widget/src/lazy/responsive.rs +++ b/widget/src/lazy/responsive.rs @@ -21,6 +21,7 @@ use std::ops::Deref; /// /// A [`Responsive`] widget will always try to fill all the available space of /// its parent. +#[cfg(feature = "lazy")] #[allow(missing_debug_implementations)] pub struct Responsive< 'a, @@ -161,7 +162,7 @@ where tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn widget::Operation<()>, + operation: &mut dyn widget::Operation, ) { let state = tree.state.downcast_mut::<State>(); let mut content = self.content.borrow_mut(); diff --git a/widget/src/lib.rs b/widget/src/lib.rs index 00e9aaa4..a68720d6 100644 --- a/widget/src/lib.rs +++ b/widget/src/lib.rs @@ -43,9 +43,6 @@ pub use helpers::*; mod lazy; #[cfg(feature = "lazy")] -pub use crate::lazy::{Component, Lazy, Responsive}; - -#[cfg(feature = "lazy")] pub use crate::lazy::helpers::*; #[doc(no_inline)] @@ -130,5 +127,8 @@ pub mod qr_code; #[doc(no_inline)] pub use qr_code::QRCode; +#[cfg(feature = "markdown")] +pub mod markdown; + pub use crate::core::theme::{self, Theme}; pub use renderer::Renderer; diff --git a/widget/src/markdown.rs b/widget/src/markdown.rs new file mode 100644 index 00000000..fa4ee6bf --- /dev/null +++ b/widget/src/markdown.rs @@ -0,0 +1,587 @@ +//! Parse and display Markdown. +//! +//! You can enable the `highlighter` feature for syntax highligting +//! in code blocks. +//! +//! Only the variants of [`Item`] are currently supported. +use crate::core::border; +use crate::core::font::{self, Font}; +use crate::core::padding; +use crate::core::theme; +use crate::core::{ + self, color, Color, Element, Length, Padding, Pixels, Theme, +}; +use crate::{column, container, rich_text, row, scrollable, span, text}; + +use std::cell::{Cell, RefCell}; +use std::rc::Rc; + +pub use core::text::Highlight; +pub use pulldown_cmark::HeadingLevel; +pub use url::Url; + +/// A Markdown item. +#[derive(Debug, Clone)] +pub enum Item { + /// A heading. + Heading(pulldown_cmark::HeadingLevel, Text), + /// A paragraph. + Paragraph(Text), + /// A code block. + /// + /// You can enable the `highlighter` feature for syntax highligting. + CodeBlock(Text), + /// A list. + List { + /// The first number of the list, if it is ordered. + start: Option<u64>, + /// The items of the list. + items: Vec<Vec<Item>>, + }, +} + +/// A bunch of parsed Markdown text. +#[derive(Debug, Clone)] +pub struct Text { + spans: Vec<Span>, + last_style: Cell<Option<Style>>, + last_styled_spans: RefCell<Rc<[text::Span<'static, Url>]>>, +} + +impl Text { + fn new(spans: Vec<Span>) -> Self { + Self { + spans, + last_style: Cell::default(), + last_styled_spans: RefCell::default(), + } + } + + /// Returns the [`rich_text()`] spans ready to be used for the given style. + /// + /// This method performs caching for you. It will only reallocate if the [`Style`] + /// provided changes. + pub fn spans(&self, style: Style) -> Rc<[text::Span<'static, Url>]> { + if Some(style) != self.last_style.get() { + *self.last_styled_spans.borrow_mut() = + self.spans.iter().map(|span| span.view(&style)).collect(); + + self.last_style.set(Some(style)); + } + + self.last_styled_spans.borrow().clone() + } +} + +#[derive(Debug, Clone)] +enum Span { + Standard { + text: String, + strikethrough: bool, + link: Option<Url>, + strong: bool, + emphasis: bool, + code: bool, + }, + #[cfg(feature = "highlighter")] + Highlight { + text: String, + color: Option<Color>, + font: Option<Font>, + }, +} + +impl Span { + fn view(&self, style: &Style) -> text::Span<'static, Url> { + match self { + Span::Standard { + text, + strikethrough, + link, + strong, + emphasis, + code, + } => { + let span = span(text.clone()).strikethrough(*strikethrough); + + let span = if *code { + span.font(Font::MONOSPACE) + .color(style.inline_code_color) + .background(style.inline_code_highlight.background) + .border(style.inline_code_highlight.border) + .padding(style.inline_code_padding) + } else if *strong || *emphasis { + span.font(Font { + weight: if *strong { + font::Weight::Bold + } else { + font::Weight::Normal + }, + style: if *emphasis { + font::Style::Italic + } else { + font::Style::Normal + }, + ..Font::default() + }) + } else { + span + }; + + let span = if let Some(link) = link.as_ref() { + span.color(style.link_color).link(link.clone()) + } else { + span + }; + + span + } + #[cfg(feature = "highlighter")] + Span::Highlight { text, color, font } => { + span(text.clone()).color_maybe(*color).font_maybe(*font) + } + } + } +} + +/// Parse the given Markdown content. +pub fn parse(markdown: &str) -> impl Iterator<Item = Item> + '_ { + struct List { + start: Option<u64>, + items: Vec<Vec<Item>>, + } + + let mut spans = Vec::new(); + let mut strong = false; + let mut emphasis = false; + let mut strikethrough = false; + let mut metadata = false; + let mut table = false; + let mut link = None; + let mut lists = Vec::new(); + + #[cfg(feature = "highlighter")] + let mut highlighter = None; + + let parser = pulldown_cmark::Parser::new_ext( + markdown, + pulldown_cmark::Options::ENABLE_YAML_STYLE_METADATA_BLOCKS + | pulldown_cmark::Options::ENABLE_PLUSES_DELIMITED_METADATA_BLOCKS + | pulldown_cmark::Options::ENABLE_TABLES + | pulldown_cmark::Options::ENABLE_STRIKETHROUGH, + ); + + let produce = |lists: &mut Vec<List>, item| { + if lists.is_empty() { + Some(item) + } else { + lists + .last_mut() + .expect("list context") + .items + .last_mut() + .expect("item context") + .push(item); + + None + } + }; + + // We want to keep the `spans` capacity + #[allow(clippy::drain_collect)] + parser.filter_map(move |event| match event { + pulldown_cmark::Event::Start(tag) => match tag { + pulldown_cmark::Tag::Strong if !metadata && !table => { + strong = true; + None + } + pulldown_cmark::Tag::Emphasis if !metadata && !table => { + emphasis = true; + None + } + pulldown_cmark::Tag::Strikethrough if !metadata && !table => { + strikethrough = true; + None + } + pulldown_cmark::Tag::Link { dest_url, .. } + if !metadata && !table => + { + match Url::parse(&dest_url) { + Ok(url) + if url.scheme() == "http" + || url.scheme() == "https" => + { + link = Some(url); + } + _ => {} + } + + None + } + pulldown_cmark::Tag::List(first_item) if !metadata && !table => { + lists.push(List { + start: first_item, + items: Vec::new(), + }); + + None + } + pulldown_cmark::Tag::Item => { + lists + .last_mut() + .expect("list context") + .items + .push(Vec::new()); + None + } + pulldown_cmark::Tag::CodeBlock( + pulldown_cmark::CodeBlockKind::Fenced(_language), + ) if !metadata && !table => { + #[cfg(feature = "highlighter")] + { + use iced_highlighter::{self, Highlighter}; + use text::Highlighter as _; + + highlighter = + Some(Highlighter::new(&iced_highlighter::Settings { + theme: iced_highlighter::Theme::Base16Ocean, + token: _language.to_string(), + })); + } + + None + } + pulldown_cmark::Tag::MetadataBlock(_) => { + metadata = true; + None + } + pulldown_cmark::Tag::Table(_) => { + table = true; + None + } + _ => None, + }, + pulldown_cmark::Event::End(tag) => match tag { + pulldown_cmark::TagEnd::Heading(level) if !metadata && !table => { + produce( + &mut lists, + Item::Heading(level, Text::new(spans.drain(..).collect())), + ) + } + pulldown_cmark::TagEnd::Strong if !metadata && !table => { + strong = false; + None + } + pulldown_cmark::TagEnd::Emphasis if !metadata && !table => { + emphasis = false; + None + } + pulldown_cmark::TagEnd::Strikethrough if !metadata && !table => { + strikethrough = false; + None + } + pulldown_cmark::TagEnd::Link if !metadata && !table => { + link = None; + None + } + pulldown_cmark::TagEnd::Paragraph if !metadata && !table => { + produce( + &mut lists, + Item::Paragraph(Text::new(spans.drain(..).collect())), + ) + } + pulldown_cmark::TagEnd::Item if !metadata && !table => { + if spans.is_empty() { + None + } else { + produce( + &mut lists, + Item::Paragraph(Text::new(spans.drain(..).collect())), + ) + } + } + pulldown_cmark::TagEnd::List(_) if !metadata && !table => { + let list = lists.pop().expect("list context"); + + produce( + &mut lists, + Item::List { + start: list.start, + items: list.items, + }, + ) + } + pulldown_cmark::TagEnd::CodeBlock if !metadata && !table => { + #[cfg(feature = "highlighter")] + { + highlighter = None; + } + + produce( + &mut lists, + Item::CodeBlock(Text::new(spans.drain(..).collect())), + ) + } + pulldown_cmark::TagEnd::MetadataBlock(_) => { + metadata = false; + None + } + pulldown_cmark::TagEnd::Table => { + table = false; + None + } + _ => None, + }, + pulldown_cmark::Event::Text(text) if !metadata && !table => { + #[cfg(feature = "highlighter")] + if let Some(highlighter) = &mut highlighter { + use text::Highlighter as _; + + for (range, highlight) in + highlighter.highlight_line(text.as_ref()) + { + let span = Span::Highlight { + text: text[range].to_owned(), + color: highlight.color(), + font: highlight.font(), + }; + + spans.push(span); + } + + return None; + } + + let span = Span::Standard { + text: text.into_string(), + strong, + emphasis, + strikethrough, + link: link.clone(), + code: false, + }; + + spans.push(span); + + None + } + pulldown_cmark::Event::Code(code) if !metadata && !table => { + let span = Span::Standard { + text: code.into_string(), + strong, + emphasis, + strikethrough, + link: link.clone(), + code: true, + }; + + spans.push(span); + None + } + pulldown_cmark::Event::SoftBreak if !metadata && !table => { + spans.push(Span::Standard { + text: String::from(" "), + strikethrough, + strong, + emphasis, + link: link.clone(), + code: false, + }); + None + } + pulldown_cmark::Event::HardBreak if !metadata && !table => { + spans.push(Span::Standard { + text: String::from("\n"), + strikethrough, + strong, + emphasis, + link: link.clone(), + code: false, + }); + None + } + _ => None, + }) +} + +/// Configuration controlling Markdown rendering in [`view`]. +#[derive(Debug, Clone, Copy)] +pub struct Settings { + /// The base text size. + pub text_size: Pixels, + /// The text size of level 1 heading. + pub h1_size: Pixels, + /// The text size of level 2 heading. + pub h2_size: Pixels, + /// The text size of level 3 heading. + pub h3_size: Pixels, + /// The text size of level 4 heading. + pub h4_size: Pixels, + /// The text size of level 5 heading. + pub h5_size: Pixels, + /// The text size of level 6 heading. + pub h6_size: Pixels, + /// The text size used in code blocks. + pub code_size: Pixels, +} + +impl Settings { + /// Creates new [`Settings`] with the given base text size in [`Pixels`]. + /// + /// Heading levels will be adjusted automatically. Specifically, + /// the first level will be twice the base size, and then every level + /// after that will be 25% smaller. + pub fn with_text_size(text_size: impl Into<Pixels>) -> Self { + let text_size = text_size.into(); + + Self { + text_size, + h1_size: text_size * 2.0, + h2_size: text_size * 1.75, + h3_size: text_size * 1.5, + h4_size: text_size * 1.25, + h5_size: text_size, + h6_size: text_size, + code_size: text_size * 0.75, + } + } +} + +impl Default for Settings { + fn default() -> Self { + Self::with_text_size(16) + } +} + +/// The text styling of some Markdown rendering in [`view`]. +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct Style { + /// The [`Highlight`] to be applied to the background of inline code. + pub inline_code_highlight: Highlight, + /// The [`Padding`] to be applied to the background of inline code. + pub inline_code_padding: Padding, + /// The [`Color`] to be applied to inline code. + pub inline_code_color: Color, + /// The [`Color`] to be applied to links. + pub link_color: Color, +} + +impl Style { + /// Creates a new [`Style`] from the given [`theme::Palette`]. + pub fn from_palette(palette: theme::Palette) -> Self { + Self { + inline_code_padding: padding::left(1).right(1), + inline_code_highlight: Highlight { + background: color!(0x111).into(), + border: border::rounded(2), + }, + inline_code_color: Color::WHITE, + link_color: palette.primary, + } + } +} + +/// Display a bunch of Markdown items. +/// +/// You can obtain the items with [`parse`]. +pub fn view<'a, Theme, Renderer>( + items: impl IntoIterator<Item = &'a Item>, + settings: Settings, + style: Style, +) -> Element<'a, Url, Theme, Renderer> +where + Theme: Catalog + 'a, + Renderer: core::text::Renderer<Font = Font> + 'a, +{ + let Settings { + text_size, + h1_size, + h2_size, + h3_size, + h4_size, + h5_size, + h6_size, + code_size, + } = settings; + + let spacing = text_size * 0.625; + + let blocks = items.into_iter().enumerate().map(|(i, item)| match item { + Item::Heading(level, heading) => { + container(rich_text(heading.spans(style)).size(match level { + pulldown_cmark::HeadingLevel::H1 => h1_size, + pulldown_cmark::HeadingLevel::H2 => h2_size, + pulldown_cmark::HeadingLevel::H3 => h3_size, + pulldown_cmark::HeadingLevel::H4 => h4_size, + pulldown_cmark::HeadingLevel::H5 => h5_size, + pulldown_cmark::HeadingLevel::H6 => h6_size, + })) + .padding(padding::top(if i > 0 { + text_size / 2.0 + } else { + Pixels::ZERO + })) + .into() + } + Item::Paragraph(paragraph) => { + rich_text(paragraph.spans(style)).size(text_size).into() + } + Item::List { start: None, items } => { + column(items.iter().map(|items| { + row![text("•").size(text_size), view(items, settings, style)] + .spacing(spacing) + .into() + })) + .spacing(spacing) + .into() + } + Item::List { + start: Some(start), + items, + } => column(items.iter().enumerate().map(|(i, items)| { + row![ + text!("{}.", i as u64 + *start).size(text_size), + view(items, settings, style) + ] + .spacing(spacing) + .into() + })) + .spacing(spacing) + .into(), + Item::CodeBlock(code) => container( + scrollable( + container( + rich_text(code.spans(style)) + .font(Font::MONOSPACE) + .size(code_size), + ) + .padding(spacing.0 / 2.0), + ) + .direction(scrollable::Direction::Horizontal( + scrollable::Scrollbar::default() + .width(spacing.0 / 2.0) + .scroller_width(spacing.0 / 2.0), + )), + ) + .width(Length::Fill) + .padding(spacing.0 / 2.0) + .class(Theme::code_block()) + .into(), + }); + + Element::new(column(blocks).width(Length::Fill).spacing(text_size)) +} + +/// The theme catalog of Markdown items. +pub trait Catalog: + container::Catalog + scrollable::Catalog + text::Catalog +{ + /// The styling class of a Markdown code block. + fn code_block<'a>() -> <Self as container::Catalog>::Class<'a>; +} + +impl Catalog for Theme { + fn code_block<'a>() -> <Self as container::Catalog>::Class<'a> { + Box::new(container::dark) + } +} diff --git a/widget/src/mouse_area.rs b/widget/src/mouse_area.rs index 17cae53b..d255ac99 100644 --- a/widget/src/mouse_area.rs +++ b/widget/src/mouse_area.rs @@ -1,7 +1,4 @@ //! A container for capturing mouse events. - -use iced_renderer::core::Point; - use crate::core::event::{self, Event}; use crate::core::layout; use crate::core::mouse; @@ -10,7 +7,8 @@ use crate::core::renderer; use crate::core::touch; use crate::core::widget::{tree, Operation, Tree}; use crate::core::{ - Clipboard, Element, Layout, Length, Rectangle, Shell, Size, Vector, Widget, + Clipboard, Element, Layout, Length, Point, Rectangle, Shell, Size, Vector, + Widget, }; /// Emit messages on mouse events. @@ -28,8 +26,9 @@ pub struct MouseArea< on_right_release: Option<Message>, on_middle_press: Option<Message>, on_middle_release: Option<Message>, + on_scroll: Option<Box<dyn Fn(mouse::ScrollDelta) -> Message + 'a>>, on_enter: Option<Message>, - on_move: Option<Box<dyn Fn(Point) -> Message>>, + on_move: Option<Box<dyn Fn(Point) -> Message + 'a>>, on_exit: Option<Message>, interaction: Option<mouse::Interaction>, } @@ -77,6 +76,16 @@ impl<'a, Message, Theme, Renderer> MouseArea<'a, Message, Theme, Renderer> { self } + /// The message to emit when scroll wheel is used + #[must_use] + pub fn on_scroll( + mut self, + on_scroll: impl Fn(mouse::ScrollDelta) -> Message + 'a, + ) -> Self { + self.on_scroll = Some(Box::new(on_scroll)); + self + } + /// The message to emit when the mouse enters the area. #[must_use] pub fn on_enter(mut self, message: Message) -> Self { @@ -86,11 +95,8 @@ impl<'a, Message, Theme, Renderer> MouseArea<'a, Message, Theme, Renderer> { /// The message to emit when the mouse moves in the area. #[must_use] - pub fn on_move<F>(mut self, build_message: F) -> Self - where - F: Fn(Point) -> Message + 'static, - { - self.on_move = Some(Box::new(build_message)); + pub fn on_move(mut self, on_move: impl Fn(Point) -> Message + 'a) -> Self { + self.on_move = Some(Box::new(on_move)); self } @@ -113,6 +119,8 @@ impl<'a, Message, Theme, Renderer> MouseArea<'a, Message, Theme, Renderer> { #[derive(Default)] struct State { is_hovered: bool, + bounds: Rectangle, + cursor_position: Option<Point>, } impl<'a, Message, Theme, Renderer> MouseArea<'a, Message, Theme, Renderer> { @@ -128,6 +136,7 @@ impl<'a, Message, Theme, Renderer> MouseArea<'a, Message, Theme, Renderer> { on_right_release: None, on_middle_press: None, on_middle_release: None, + on_scroll: None, on_enter: None, on_move: None, on_exit: None, @@ -178,7 +187,7 @@ where tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn Operation<()>, + operation: &mut dyn Operation, ) { self.content.as_widget().operate( &mut tree.children[0], @@ -302,13 +311,17 @@ fn update<Message: Clone, Theme, Renderer>( cursor: mouse::Cursor, shell: &mut Shell<'_, Message>, ) -> event::Status { - if let Event::Mouse(mouse::Event::CursorMoved { .. }) - | Event::Touch(touch::Event::FingerMoved { .. }) = event - { - let state: &mut State = tree.state.downcast_mut(); + let state: &mut State = tree.state.downcast_mut(); + let cursor_position = cursor.position(); + let bounds = layout.bounds(); + + if state.cursor_position != cursor_position && state.bounds != bounds { let was_hovered = state.is_hovered; + state.is_hovered = cursor.is_over(layout.bounds()); + state.cursor_position = cursor_position; + state.bounds = bounds; match ( widget.on_enter.as_ref(), @@ -397,5 +410,13 @@ fn update<Message: Clone, Theme, Renderer>( } } + if let Some(on_scroll) = widget.on_scroll.as_ref() { + if let Event::Mouse(mouse::Event::WheelScrolled { delta }) = event { + shell.publish(on_scroll(delta)); + + return event::Status::Captured; + } + } + event::Status::Ignored } diff --git a/widget/src/overlay/menu.rs b/widget/src/overlay/menu.rs index 98efe305..f05ae40a 100644 --- a/widget/src/overlay/menu.rs +++ b/widget/src/overlay/menu.rs @@ -1,5 +1,6 @@ //! Build and show dropdown menus. use crate::core::alignment; +use crate::core::border::{self, Border}; use crate::core::event::{self, Event}; use crate::core::layout::{self, Layout}; use crate::core::mouse; @@ -9,8 +10,8 @@ use crate::core::text::{self, Text}; use crate::core::touch; use crate::core::widget::Tree; use crate::core::{ - Background, Border, Clipboard, Color, Length, Padding, Pixels, Point, - Rectangle, Size, Theme, Vector, + Background, Clipboard, Color, Length, Padding, Pixels, Point, Rectangle, + Size, Theme, Vector, }; use crate::core::{Element, Shell, Widget}; use crate::scrollable::{self, Scrollable}; @@ -200,21 +201,18 @@ where class, } = menu; - let list = Scrollable::with_direction( - List { - options, - hovered_option, - on_selected, - on_option_hovered, - font, - text_size, - text_line_height, - text_shaping, - padding, - class, - }, - scrollable::Direction::default(), - ); + let list = Scrollable::new(List { + options, + hovered_option, + on_selected, + on_option_hovered, + font, + text_size, + text_line_height, + text_shaping, + padding, + class, + }); state.tree.diff(&list as &dyn Widget<_, _, _>); @@ -517,7 +515,7 @@ where width: bounds.width - style.border.width * 2.0, ..bounds }, - border: Border::rounded(style.border.radius), + border: border::rounded(style.border.radius), ..renderer::Quad::default() }, style.selected_background, @@ -534,6 +532,7 @@ where horizontal_alignment: alignment::Horizontal::Left, vertical_alignment: alignment::Vertical::Center, shaping: self.text_shaping, + wrapping: text::Wrapping::default(), }, Point::new(bounds.x + self.padding.left, bounds.center_y()), if is_selected { diff --git a/widget/src/pane_grid.rs b/widget/src/pane_grid.rs index c3da3879..710a5443 100644 --- a/widget/src/pane_grid.rs +++ b/widget/src/pane_grid.rs @@ -10,6 +10,7 @@ mod axis; mod configuration; mod content; +mod controls; mod direction; mod draggable; mod node; @@ -22,6 +23,7 @@ pub mod state; pub use axis::Axis; pub use configuration::Configuration; pub use content::Content; +pub use controls::Controls; pub use direction::Direction; pub use draggable::Draggable; pub use node::Node; @@ -324,7 +326,7 @@ where tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn widget::Operation<()>, + operation: &mut dyn widget::Operation, ) { operation.container(None, layout.bounds(), &mut |operation| { self.contents diff --git a/widget/src/pane_grid/content.rs b/widget/src/pane_grid/content.rs index d45fc0cd..ec0676b1 100644 --- a/widget/src/pane_grid/content.rs +++ b/widget/src/pane_grid/content.rs @@ -214,7 +214,7 @@ where tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn widget::Operation<()>, + operation: &mut dyn widget::Operation, ) { let body_layout = if let Some(title_bar) = &self.title_bar { let mut children = layout.children(); diff --git a/widget/src/pane_grid/controls.rs b/widget/src/pane_grid/controls.rs new file mode 100644 index 00000000..13b57acb --- /dev/null +++ b/widget/src/pane_grid/controls.rs @@ -0,0 +1,59 @@ +use crate::container; +use crate::core::{self, Element}; + +/// The controls of a [`Pane`]. +/// +/// [`Pane`]: super::Pane +#[allow(missing_debug_implementations)] +pub struct Controls< + 'a, + Message, + Theme = crate::Theme, + Renderer = crate::Renderer, +> where + Theme: container::Catalog, + Renderer: core::Renderer, +{ + pub(super) full: Element<'a, Message, Theme, Renderer>, + pub(super) compact: Option<Element<'a, Message, Theme, Renderer>>, +} + +impl<'a, Message, Theme, Renderer> Controls<'a, Message, Theme, Renderer> +where + Theme: container::Catalog, + Renderer: core::Renderer, +{ + /// Creates a new [`Controls`] with the given content. + pub fn new( + content: impl Into<Element<'a, Message, Theme, Renderer>>, + ) -> Self { + Self { + full: content.into(), + compact: None, + } + } + + /// Creates a new [`Controls`] with a full and compact variant. + /// If there is not enough room to show the full variant without overlap, + /// then the compact variant will be shown instead. + pub fn dynamic( + full: impl Into<Element<'a, Message, Theme, Renderer>>, + compact: impl Into<Element<'a, Message, Theme, Renderer>>, + ) -> Self { + Self { + full: full.into(), + compact: Some(compact.into()), + } + } +} + +impl<'a, Message, Theme, Renderer> From<Element<'a, Message, Theme, Renderer>> + for Controls<'a, Message, Theme, Renderer> +where + Theme: container::Catalog, + Renderer: core::Renderer, +{ + fn from(value: Element<'a, Message, Theme, Renderer>) -> Self { + Self::new(value) + } +} diff --git a/widget/src/pane_grid/title_bar.rs b/widget/src/pane_grid/title_bar.rs index c05f1252..5002b4f7 100644 --- a/widget/src/pane_grid/title_bar.rs +++ b/widget/src/pane_grid/title_bar.rs @@ -9,6 +9,7 @@ use crate::core::{ self, Clipboard, Element, Layout, Padding, Point, Rectangle, Shell, Size, Vector, }; +use crate::pane_grid::controls::Controls; /// The title bar of a [`Pane`]. /// @@ -24,7 +25,7 @@ pub struct TitleBar< Renderer: core::Renderer, { content: Element<'a, Message, Theme, Renderer>, - controls: Option<Element<'a, Message, Theme, Renderer>>, + controls: Option<Controls<'a, Message, Theme, Renderer>>, padding: Padding, always_show_controls: bool, class: Theme::Class<'a>, @@ -51,7 +52,7 @@ where /// Sets the controls of the [`TitleBar`]. pub fn controls( mut self, - controls: impl Into<Element<'a, Message, Theme, Renderer>>, + controls: impl Into<Controls<'a, Message, Theme, Renderer>>, ) -> Self { self.controls = Some(controls.into()); self @@ -104,10 +105,22 @@ where Renderer: core::Renderer, { pub(super) fn state(&self) -> Tree { - let children = if let Some(controls) = self.controls.as_ref() { - vec![Tree::new(&self.content), Tree::new(controls)] - } else { - vec![Tree::new(&self.content), Tree::empty()] + let children = match self.controls.as_ref() { + Some(controls) => match controls.compact.as_ref() { + Some(compact) => vec![ + Tree::new(&self.content), + Tree::new(&controls.full), + Tree::new(compact), + ], + None => vec![ + Tree::new(&self.content), + Tree::new(&controls.full), + Tree::empty(), + ], + }, + None => { + vec![Tree::new(&self.content), Tree::empty(), Tree::empty()] + } }; Tree { @@ -117,9 +130,13 @@ where } pub(super) fn diff(&self, tree: &mut Tree) { - if tree.children.len() == 2 { + if tree.children.len() == 3 { if let Some(controls) = self.controls.as_ref() { - tree.children[1].diff(controls); + if let Some(compact) = controls.compact.as_ref() { + tree.children[2].diff(compact); + } + + tree.children[1].diff(&controls.full); } tree.children[0].diff(&self.content); @@ -164,18 +181,42 @@ where if title_layout.bounds().width + controls_layout.bounds().width > padded.bounds().width { - show_title = false; + if let Some(compact) = controls.compact.as_ref() { + let compact_layout = children.next().unwrap(); + + compact.as_widget().draw( + &tree.children[2], + renderer, + theme, + &inherited_style, + compact_layout, + cursor, + viewport, + ); + } else { + show_title = false; + + controls.full.as_widget().draw( + &tree.children[1], + renderer, + theme, + &inherited_style, + controls_layout, + cursor, + viewport, + ); + } + } else { + controls.full.as_widget().draw( + &tree.children[1], + renderer, + theme, + &inherited_style, + controls_layout, + cursor, + viewport, + ); } - - controls.as_widget().draw( - &tree.children[1], - renderer, - theme, - &inherited_style, - controls_layout, - cursor, - viewport, - ); } } @@ -207,13 +248,20 @@ where let mut children = padded.children(); let title_layout = children.next().unwrap(); - if self.controls.is_some() { + if let Some(controls) = self.controls.as_ref() { let controls_layout = children.next().unwrap(); if title_layout.bounds().width + controls_layout.bounds().width > padded.bounds().width { - !controls_layout.bounds().contains(cursor_position) + if controls.compact.is_some() { + let compact_layout = children.next().unwrap(); + + !compact_layout.bounds().contains(cursor_position) + && !title_layout.bounds().contains(cursor_position) + } else { + !controls_layout.bounds().contains(cursor_position) + } } else { !controls_layout.bounds().contains(cursor_position) && !title_layout.bounds().contains(cursor_position) @@ -244,25 +292,73 @@ where let title_size = title_layout.size(); let node = if let Some(controls) = &self.controls { - let controls_layout = controls.as_widget().layout( + let controls_layout = controls.full.as_widget().layout( &mut tree.children[1], renderer, &layout::Limits::new(Size::ZERO, max_size), ); - let controls_size = controls_layout.size(); - let space_before_controls = max_size.width - controls_size.width; - - let height = title_size.height.max(controls_size.height); - - layout::Node::with_children( - Size::new(max_size.width, height), - vec![ - title_layout, - controls_layout - .move_to(Point::new(space_before_controls, 0.0)), - ], - ) + if title_layout.bounds().width + controls_layout.bounds().width + > max_size.width + { + if let Some(compact) = controls.compact.as_ref() { + let compact_layout = compact.as_widget().layout( + &mut tree.children[2], + renderer, + &layout::Limits::new(Size::ZERO, max_size), + ); + + let compact_size = compact_layout.size(); + let space_before_controls = + max_size.width - compact_size.width; + + let height = title_size.height.max(compact_size.height); + + layout::Node::with_children( + Size::new(max_size.width, height), + vec![ + title_layout, + controls_layout, + compact_layout.move_to(Point::new( + space_before_controls, + 0.0, + )), + ], + ) + } else { + let controls_size = controls_layout.size(); + let space_before_controls = + max_size.width - controls_size.width; + + let height = title_size.height.max(controls_size.height); + + layout::Node::with_children( + Size::new(max_size.width, height), + vec![ + title_layout, + controls_layout.move_to(Point::new( + space_before_controls, + 0.0, + )), + ], + ) + } + } else { + let controls_size = controls_layout.size(); + let space_before_controls = + max_size.width - controls_size.width; + + let height = title_size.height.max(controls_size.height); + + layout::Node::with_children( + Size::new(max_size.width, height), + vec![ + title_layout, + controls_layout + .move_to(Point::new(space_before_controls, 0.0)), + ], + ) + } } else { layout::Node::with_children( Size::new(max_size.width, title_size.height), @@ -278,7 +374,7 @@ where tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn widget::Operation<()>, + operation: &mut dyn widget::Operation, ) { let mut children = layout.children(); let padded = children.next().unwrap(); @@ -293,15 +389,33 @@ where if title_layout.bounds().width + controls_layout.bounds().width > padded.bounds().width { - show_title = false; - } + if let Some(compact) = controls.compact.as_ref() { + let compact_layout = children.next().unwrap(); - controls.as_widget().operate( - &mut tree.children[1], - controls_layout, - renderer, - operation, - ); + compact.as_widget().operate( + &mut tree.children[2], + compact_layout, + renderer, + operation, + ); + } else { + show_title = false; + + controls.full.as_widget().operate( + &mut tree.children[1], + controls_layout, + renderer, + operation, + ); + } + } else { + controls.full.as_widget().operate( + &mut tree.children[1], + controls_layout, + renderer, + operation, + ); + } }; if show_title { @@ -337,19 +451,45 @@ where if title_layout.bounds().width + controls_layout.bounds().width > padded.bounds().width { - show_title = false; - } + if let Some(compact) = controls.compact.as_mut() { + let compact_layout = children.next().unwrap(); + + compact.as_widget_mut().on_event( + &mut tree.children[2], + event.clone(), + compact_layout, + cursor, + renderer, + clipboard, + shell, + viewport, + ) + } else { + show_title = false; - controls.as_widget_mut().on_event( - &mut tree.children[1], - event.clone(), - controls_layout, - cursor, - renderer, - clipboard, - shell, - viewport, - ) + controls.full.as_widget_mut().on_event( + &mut tree.children[1], + event.clone(), + controls_layout, + cursor, + renderer, + clipboard, + shell, + viewport, + ) + } + } else { + controls.full.as_widget_mut().on_event( + &mut tree.children[1], + event.clone(), + controls_layout, + cursor, + renderer, + clipboard, + shell, + viewport, + ) + } } else { event::Status::Ignored }; @@ -396,18 +536,33 @@ where if let Some(controls) = &self.controls { let controls_layout = children.next().unwrap(); - let controls_interaction = controls.as_widget().mouse_interaction( - &tree.children[1], - controls_layout, - cursor, - viewport, - renderer, - ); + let controls_interaction = + controls.full.as_widget().mouse_interaction( + &tree.children[1], + controls_layout, + cursor, + viewport, + renderer, + ); if title_layout.bounds().width + controls_layout.bounds().width > padded.bounds().width { - controls_interaction + if let Some(compact) = controls.compact.as_ref() { + let compact_layout = children.next().unwrap(); + let compact_interaction = + compact.as_widget().mouse_interaction( + &tree.children[2], + compact_layout, + cursor, + viewport, + renderer, + ); + + compact_interaction.max(title_interaction) + } else { + controls_interaction + } } else { controls_interaction.max(title_interaction) } @@ -444,12 +599,36 @@ where controls.as_mut().and_then(|controls| { let controls_layout = children.next()?; - controls.as_widget_mut().overlay( - controls_state, - controls_layout, - renderer, - translation, - ) + if title_layout.bounds().width + + controls_layout.bounds().width + > padded.bounds().width + { + if let Some(compact) = controls.compact.as_mut() { + let compact_state = states.next().unwrap(); + let compact_layout = children.next()?; + + compact.as_widget_mut().overlay( + compact_state, + compact_layout, + renderer, + translation, + ) + } else { + controls.full.as_widget_mut().overlay( + controls_state, + controls_layout, + renderer, + translation, + ) + } + } else { + controls.full.as_widget_mut().overlay( + controls_state, + controls_layout, + renderer, + translation, + ) + } }) }) } diff --git a/widget/src/pick_list.rs b/widget/src/pick_list.rs index 97de5b48..1fc9951e 100644 --- a/widget/src/pick_list.rs +++ b/widget/src/pick_list.rs @@ -6,7 +6,8 @@ use crate::core::layout; use crate::core::mouse; use crate::core::overlay; use crate::core::renderer; -use crate::core::text::{self, Paragraph as _, Text}; +use crate::core::text::paragraph; +use crate::core::text::{self, Text}; use crate::core::touch; use crate::core::widget::tree::{self, Tree}; use crate::core::{ @@ -80,7 +81,7 @@ where padding: crate::button::DEFAULT_PADDING, text_size: None, text_line_height: text::LineHeight::default(), - text_shaping: text::Shaping::Basic, + text_shaping: text::Shaping::default(), font: None, handle: Handle::default(), class: <Theme as Catalog>::default(), @@ -249,6 +250,7 @@ where horizontal_alignment: alignment::Horizontal::Left, vertical_alignment: alignment::Vertical::Center, shaping: self.text_shaping, + wrapping: text::Wrapping::default(), }; for (option, paragraph) in options.iter().zip(state.options.iter_mut()) @@ -514,6 +516,7 @@ where horizontal_alignment: alignment::Horizontal::Right, vertical_alignment: alignment::Vertical::Center, shaping, + wrapping: text::Wrapping::default(), }, Point::new( bounds.x + bounds.width - self.padding.right, @@ -543,6 +546,7 @@ where horizontal_alignment: alignment::Horizontal::Left, vertical_alignment: alignment::Vertical::Center, shaping: self.text_shaping, + wrapping: text::Wrapping::default(), }, Point::new(bounds.x + self.padding.left, bounds.center_y()), if is_selected { @@ -622,8 +626,8 @@ struct State<P: text::Paragraph> { keyboard_modifiers: keyboard::Modifiers, is_open: bool, hovered_option: Option<usize>, - options: Vec<P>, - placeholder: P, + options: Vec<paragraph::Plain<P>>, + placeholder: paragraph::Plain<P>, } impl<P: text::Paragraph> State<P> { @@ -635,7 +639,7 @@ impl<P: text::Paragraph> State<P> { is_open: bool::default(), hovered_option: Option::default(), options: Vec::new(), - placeholder: P::default(), + placeholder: paragraph::Plain::default(), } } } diff --git a/widget/src/progress_bar.rs b/widget/src/progress_bar.rs index e7821b43..a10feea6 100644 --- a/widget/src/progress_bar.rs +++ b/widget/src/progress_bar.rs @@ -1,10 +1,11 @@ //! Provide progress feedback to your users. +use crate::core::border::{self, Border}; use crate::core::layout; use crate::core::mouse; use crate::core::renderer; use crate::core::widget::Tree; use crate::core::{ - self, Background, Border, Element, Layout, Length, Rectangle, Size, Theme, + self, Background, Color, Element, Layout, Length, Rectangle, Size, Theme, Widget, }; @@ -151,7 +152,10 @@ where width: active_progress_width, ..bounds }, - border: Border::rounded(style.border.radius), + border: Border { + color: Color::TRANSPARENT, + ..style.border + }, ..renderer::Quad::default() }, style.bar, @@ -255,6 +259,6 @@ fn styled( Style { background: background.into(), bar: bar.into(), - border: Border::rounded(2), + border: border::rounded(2), } } diff --git a/widget/src/radio.rs b/widget/src/radio.rs index 6b22961d..cfa961f3 100644 --- a/widget/src/radio.rs +++ b/widget/src/radio.rs @@ -1,5 +1,6 @@ //! Create choices using radio buttons. use crate::core::alignment; +use crate::core::border::{self, Border}; use crate::core::event::{self, Event}; use crate::core::layout; use crate::core::mouse; @@ -9,8 +10,8 @@ use crate::core::touch; use crate::core::widget; use crate::core::widget::tree::{self, Tree}; use crate::core::{ - Background, Border, Clipboard, Color, Element, Layout, Length, Pixels, - Rectangle, Shell, Size, Theme, Widget, + Background, Clipboard, Color, Element, Layout, Length, Pixels, Rectangle, + Shell, Size, Theme, Widget, }; /// A circular button representing a choice. @@ -81,6 +82,7 @@ where text_size: Option<Pixels>, text_line_height: text::LineHeight, text_shaping: text::Shaping, + text_wrapping: text::Wrapping, font: Option<Renderer::Font>, class: Theme::Class<'a>, } @@ -104,7 +106,7 @@ where /// * the label of the [`Radio`] button /// * the current selected value /// * a function that will be called when the [`Radio`] is selected. It - /// receives the value of the radio and must produce a `Message`. + /// receives the value of the radio and must produce a `Message`. pub fn new<F, V>( label: impl Into<String>, value: V, @@ -121,10 +123,11 @@ where label: label.into(), width: Length::Shrink, size: Self::DEFAULT_SIZE, - spacing: Self::DEFAULT_SPACING, //15 + spacing: Self::DEFAULT_SPACING, text_size: None, text_line_height: text::LineHeight::default(), - text_shaping: text::Shaping::Basic, + text_shaping: text::Shaping::default(), + text_wrapping: text::Wrapping::default(), font: None, class: Theme::default(), } @@ -169,6 +172,12 @@ where self } + /// Sets the [`text::Wrapping`] strategy of the [`Radio`] button. + pub fn text_wrapping(mut self, wrapping: text::Wrapping) -> Self { + self.text_wrapping = wrapping; + self + } + /// Sets the text font of the [`Radio`] button. pub fn font(mut self, font: impl Into<Renderer::Font>) -> Self { self.font = Some(font.into()); @@ -244,6 +253,7 @@ where alignment::Horizontal::Left, alignment::Vertical::Top, self.text_shaping, + self.text_wrapping, ) }, ) @@ -342,7 +352,7 @@ where width: bounds.width - dot_size, height: bounds.height - dot_size, }, - border: Border::rounded(dot_size / 2.0), + border: border::rounded(dot_size / 2.0), ..renderer::Quad::default() }, style.dot_color, @@ -352,12 +362,14 @@ where { let label_layout = children.next().unwrap(); + let state: &widget::text::State<Renderer::Paragraph> = + tree.state.downcast_ref(); crate::text::draw( renderer, defaults, label_layout, - tree.state.downcast_ref(), + state.0.raw(), crate::text::Style { color: style.text_color, }, diff --git a/widget/src/row.rs b/widget/src/row.rs index c8fcdb61..85af912f 100644 --- a/widget/src/row.rs +++ b/widget/src/row.rs @@ -1,4 +1,5 @@ //! Distribute content horizontally. +use crate::core::alignment::{self, Alignment}; use crate::core::event::{self, Event}; use crate::core::layout::{self, Layout}; use crate::core::mouse; @@ -6,8 +7,8 @@ use crate::core::overlay; use crate::core::renderer; use crate::core::widget::{Operation, Tree}; use crate::core::{ - Alignment, Clipboard, Element, Length, Padding, Pixels, Rectangle, Shell, - Size, Vector, Widget, + Clipboard, Element, Length, Padding, Pixels, Rectangle, Shell, Size, + Vector, Widget, }; /// A container that distributes its contents horizontally. @@ -17,7 +18,7 @@ pub struct Row<'a, Message, Theme = crate::Theme, Renderer = crate::Renderer> { padding: Padding, width: Length, height: Length, - align_items: Alignment, + align: Alignment, clip: bool, children: Vec<Element<'a, Message, Theme, Renderer>>, } @@ -60,7 +61,7 @@ where padding: Padding::ZERO, width: Length::Shrink, height: Length::Shrink, - align_items: Alignment::Start, + align: Alignment::Start, clip: false, children, } @@ -95,8 +96,8 @@ where } /// Sets the vertical alignment of the contents of the [`Row`] . - pub fn align_items(mut self, align: Alignment) -> Self { - self.align_items = align; + pub fn align_y(mut self, align: impl Into<alignment::Vertical>) -> Self { + self.align = Alignment::from(align.into()); self } @@ -141,6 +142,13 @@ where ) -> Self { children.into_iter().fold(self, Self::push) } + + /// Turns the [`Row`] into a [`Wrapping`] row. + /// + /// The original alignment of the [`Row`] is preserved per row wrapped. + pub fn wrap(self) -> Wrapping<'a, Message, Theme, Renderer> { + Wrapping { row: self } + } } impl<'a, Message, Renderer> Default for Row<'a, Message, Renderer> @@ -199,7 +207,7 @@ where self.height, self.padding, self.spacing, - self.align_items, + self.align, &self.children, &mut tree.children, ) @@ -210,7 +218,7 @@ where tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn Operation<()>, + operation: &mut dyn Operation, ) { operation.container(None, layout.bounds(), &mut |operation| { self.children @@ -338,3 +346,196 @@ where Self::new(row) } } + +/// A [`Row`] that wraps its contents. +/// +/// Create a [`Row`] first, and then call [`Row::wrap`] to +/// obtain a [`Row`] that wraps its contents. +/// +/// The original alignment of the [`Row`] is preserved per row wrapped. +#[allow(missing_debug_implementations)] +pub struct Wrapping< + 'a, + Message, + Theme = crate::Theme, + Renderer = crate::Renderer, +> { + row: Row<'a, Message, Theme, Renderer>, +} + +impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer> + for Wrapping<'a, Message, Theme, Renderer> +where + Renderer: crate::core::Renderer, +{ + fn children(&self) -> Vec<Tree> { + self.row.children() + } + + fn diff(&self, tree: &mut Tree) { + self.row.diff(tree); + } + + fn size(&self) -> Size<Length> { + self.row.size() + } + + fn layout( + &self, + tree: &mut Tree, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + let limits = limits + .width(self.row.width) + .height(self.row.height) + .shrink(self.row.padding); + + let spacing = self.row.spacing; + let max_width = limits.max().width; + + let mut children: Vec<layout::Node> = Vec::new(); + let mut intrinsic_size = Size::ZERO; + let mut row_start = 0; + let mut row_height = 0.0; + let mut x = 0.0; + let mut y = 0.0; + + let align_factor = match self.row.align { + Alignment::Start => 0.0, + Alignment::Center => 2.0, + Alignment::End => 1.0, + }; + + let align = |row_start: std::ops::Range<usize>, + row_height: f32, + children: &mut Vec<layout::Node>| { + if align_factor != 0.0 { + for node in &mut children[row_start] { + let height = node.size().height; + + node.translate_mut(Vector::new( + 0.0, + (row_height - height) / align_factor, + )); + } + } + }; + + for (i, child) in self.row.children.iter().enumerate() { + let node = child.as_widget().layout( + &mut tree.children[i], + renderer, + &limits, + ); + + let child_size = node.size(); + + if x != 0.0 && x + child_size.width > max_width { + intrinsic_size.width = intrinsic_size.width.max(x - spacing); + + align(row_start..i, row_height, &mut children); + + y += row_height + spacing; + x = 0.0; + row_start = i; + row_height = 0.0; + } + + row_height = row_height.max(child_size.height); + + children.push(node.move_to(( + x + self.row.padding.left, + y + self.row.padding.top, + ))); + + x += child_size.width + spacing; + } + + if x != 0.0 { + intrinsic_size.width = intrinsic_size.width.max(x - spacing); + } + + intrinsic_size.height = (y - spacing).max(0.0) + row_height; + align(row_start..children.len(), row_height, &mut children); + + let size = + limits.resolve(self.row.width, self.row.height, intrinsic_size); + + layout::Node::with_children(size.expand(self.row.padding), children) + } + + fn operate( + &self, + tree: &mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + operation: &mut dyn Operation, + ) { + self.row.operate(tree, layout, renderer, operation); + } + + fn on_event( + &mut self, + tree: &mut Tree, + event: Event, + layout: Layout<'_>, + cursor: mouse::Cursor, + renderer: &Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + viewport: &Rectangle, + ) -> event::Status { + self.row.on_event( + tree, event, layout, cursor, renderer, clipboard, shell, viewport, + ) + } + + fn mouse_interaction( + &self, + tree: &Tree, + layout: Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + renderer: &Renderer, + ) -> mouse::Interaction { + self.row + .mouse_interaction(tree, layout, cursor, viewport, renderer) + } + + fn draw( + &self, + tree: &Tree, + renderer: &mut Renderer, + theme: &Theme, + style: &renderer::Style, + layout: Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + ) { + self.row + .draw(tree, renderer, theme, style, layout, cursor, viewport); + } + + fn overlay<'b>( + &'b mut self, + tree: &'b mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + translation: Vector, + ) -> Option<overlay::Element<'b, Message, Theme, Renderer>> { + self.row.overlay(tree, layout, renderer, translation) + } +} + +impl<'a, Message, Theme, Renderer> From<Wrapping<'a, Message, Theme, Renderer>> + for Element<'a, Message, Theme, Renderer> +where + Message: 'a, + Theme: 'a, + Renderer: crate::core::Renderer + 'a, +{ + fn from(row: Wrapping<'a, Message, Theme, Renderer>) -> Self { + Self::new(row) + } +} diff --git a/widget/src/rule.rs b/widget/src/rule.rs index 1a536d2f..bbcd577e 100644 --- a/widget/src/rule.rs +++ b/widget/src/rule.rs @@ -1,6 +1,6 @@ //! Display a horizontal or vertical rule for dividing content. use crate::core; -use crate::core::border::{self, Border}; +use crate::core::border; use crate::core::layout; use crate::core::mouse; use crate::core::renderer; @@ -132,7 +132,7 @@ where renderer.fill_quad( renderer::Quad { bounds, - border: Border::rounded(style.radius), + border: border::rounded(style.radius), ..renderer::Quad::default() }, style.color, diff --git a/widget/src/scrollable.rs b/widget/src/scrollable.rs index c3d08223..af6a3945 100644 --- a/widget/src/scrollable.rs +++ b/widget/src/scrollable.rs @@ -1,21 +1,24 @@ //! Navigate an endless amount of content with a scrollbar. -// use crate::container; use crate::container; +use crate::core::border::{self, Border}; use crate::core::event::{self, Event}; use crate::core::keyboard; use crate::core::layout; use crate::core::mouse; use crate::core::overlay; use crate::core::renderer; +use crate::core::time::{Duration, Instant}; use crate::core::touch; use crate::core::widget; use crate::core::widget::operation::{self, Operation}; use crate::core::widget::tree::{self, Tree}; +use crate::core::window; use crate::core::{ - self, Background, Border, Clipboard, Color, Element, Layout, Length, + self, Background, Clipboard, Color, Element, Layout, Length, Padding, Pixels, Point, Rectangle, Shell, Size, Theme, Vector, Widget, }; -use crate::runtime::{Action, Task}; +use crate::runtime::task::{self, Task}; +use crate::runtime::Action; pub use operation::scrollable::{AbsoluteOffset, RelativeOffset}; @@ -52,34 +55,51 @@ where Self::with_direction(content, Direction::default()) } - /// Creates a new [`Scrollable`] with the given [`Direction`]. + /// Creates a new vertical [`Scrollable`]. pub fn with_direction( content: impl Into<Element<'a, Message, Theme, Renderer>>, - direction: Direction, + direction: impl Into<Direction>, ) -> Self { - let content = content.into(); + Scrollable { + id: None, + width: Length::Shrink, + height: Length::Shrink, + direction: direction.into(), + content: content.into(), + on_scroll: None, + class: Theme::default(), + } + .validate() + } + + fn validate(mut self) -> Self { + let size_hint = self.content.as_widget().size_hint(); debug_assert!( - direction.vertical().is_none() - || !content.as_widget().size_hint().height.is_fill(), + self.direction.vertical().is_none() || !size_hint.height.is_fill(), "scrollable content must not fill its vertical scrolling axis" ); debug_assert!( - direction.horizontal().is_none() - || !content.as_widget().size_hint().width.is_fill(), + self.direction.horizontal().is_none() || !size_hint.width.is_fill(), "scrollable content must not fill its horizontal scrolling axis" ); - Scrollable { - id: None, - width: Length::Shrink, - height: Length::Shrink, - direction, - content, - on_scroll: None, - class: Theme::default(), + if self.direction.horizontal().is_none() { + self.width = self.width.enclose(size_hint.width); } + + if self.direction.vertical().is_none() { + self.height = self.height.enclose(size_hint.height); + } + + self + } + + /// Creates a new [`Scrollable`] with the given [`Direction`]. + pub fn direction(mut self, direction: impl Into<Direction>) -> Self { + self.direction = direction.into(); + self.validate() } /// Sets the [`Id`] of the [`Scrollable`]. @@ -108,6 +128,69 @@ where self } + /// Anchors the vertical [`Scrollable`] direction to the top. + pub fn anchor_top(self) -> Self { + self.anchor_y(Anchor::Start) + } + + /// Anchors the vertical [`Scrollable`] direction to the bottom. + pub fn anchor_bottom(self) -> Self { + self.anchor_y(Anchor::End) + } + + /// Anchors the horizontal [`Scrollable`] direction to the left. + pub fn anchor_left(self) -> Self { + self.anchor_x(Anchor::Start) + } + + /// Anchors the horizontal [`Scrollable`] direction to the right. + pub fn anchor_right(self) -> Self { + self.anchor_x(Anchor::End) + } + + /// Sets the [`Anchor`] of the horizontal direction of the [`Scrollable`], if applicable. + pub fn anchor_x(mut self, alignment: Anchor) -> Self { + match &mut self.direction { + Direction::Horizontal(horizontal) + | Direction::Both { horizontal, .. } => { + horizontal.alignment = alignment; + } + Direction::Vertical { .. } => {} + } + + self + } + + /// Sets the [`Anchor`] of the vertical direction of the [`Scrollable`], if applicable. + pub fn anchor_y(mut self, alignment: Anchor) -> Self { + match &mut self.direction { + Direction::Vertical(vertical) + | Direction::Both { vertical, .. } => { + vertical.alignment = alignment; + } + Direction::Horizontal { .. } => {} + } + + self + } + + /// Embeds the [`Scrollbar`] into the [`Scrollable`], instead of floating on top of the + /// content. + /// + /// The `spacing` provided will be used as space between the [`Scrollbar`] and the contents + /// of the [`Scrollable`]. + pub fn spacing(mut self, new_spacing: impl Into<Pixels>) -> Self { + match &mut self.direction { + Direction::Horizontal(scrollbar) + | Direction::Vertical(scrollbar) => { + scrollbar.spacing = Some(new_spacing.into().0); + } + Direction::Both { .. } => {} + } + + self + } + /// Sets the style of this [`Scrollable`]. #[must_use] pub fn style(mut self, style: impl Fn(&Theme, Status) -> Style + 'a) -> Self @@ -131,102 +214,133 @@ where #[derive(Debug, Clone, Copy, PartialEq)] pub enum Direction { /// Vertical scrolling - Vertical(Properties), + Vertical(Scrollbar), /// Horizontal scrolling - Horizontal(Properties), + Horizontal(Scrollbar), /// Both vertical and horizontal scrolling Both { /// The properties of the vertical scrollbar. - vertical: Properties, + vertical: Scrollbar, /// The properties of the horizontal scrollbar. - horizontal: Properties, + horizontal: Scrollbar, }, } impl Direction { - /// Returns the [`Properties`] of the horizontal scrollbar, if any. - pub fn horizontal(&self) -> Option<&Properties> { + /// Returns the horizontal [`Scrollbar`], if any. + pub fn horizontal(&self) -> Option<&Scrollbar> { match self { - Self::Horizontal(properties) => Some(properties), + Self::Horizontal(scrollbar) => Some(scrollbar), Self::Both { horizontal, .. } => Some(horizontal), Self::Vertical(_) => None, } } - /// Returns the [`Properties`] of the vertical scrollbar, if any. - pub fn vertical(&self) -> Option<&Properties> { + /// Returns the vertical [`Scrollbar`], if any. + pub fn vertical(&self) -> Option<&Scrollbar> { match self { - Self::Vertical(properties) => Some(properties), + Self::Vertical(scrollbar) => Some(scrollbar), Self::Both { vertical, .. } => Some(vertical), Self::Horizontal(_) => None, } } + + fn align(&self, delta: Vector) -> Vector { + let horizontal_alignment = + self.horizontal().map(|p| p.alignment).unwrap_or_default(); + + let vertical_alignment = + self.vertical().map(|p| p.alignment).unwrap_or_default(); + + let align = |alignment: Anchor, delta: f32| match alignment { + Anchor::Start => delta, + Anchor::End => -delta, + }; + + Vector::new( + align(horizontal_alignment, delta.x), + align(vertical_alignment, delta.y), + ) + } } impl Default for Direction { fn default() -> Self { - Self::Vertical(Properties::default()) + Self::Vertical(Scrollbar::default()) } } -/// Properties of a scrollbar within a [`Scrollable`]. +/// A scrollbar within a [`Scrollable`]. #[derive(Debug, Clone, Copy, PartialEq)] -pub struct Properties { +pub struct Scrollbar { width: f32, margin: f32, scroller_width: f32, - alignment: Alignment, + alignment: Anchor, + spacing: Option<f32>, } -impl Default for Properties { +impl Default for Scrollbar { fn default() -> Self { Self { width: 10.0, margin: 0.0, scroller_width: 10.0, - alignment: Alignment::Start, + alignment: Anchor::Start, + spacing: None, } } } -impl Properties { - /// Creates new [`Properties`] for use in a [`Scrollable`]. +impl Scrollbar { + /// Creates new [`Scrollbar`] for use in a [`Scrollable`]. pub fn new() -> Self { Self::default() } - /// Sets the scrollbar width of the [`Scrollable`] . + /// Sets the scrollbar width of the [`Scrollbar`] . pub fn width(mut self, width: impl Into<Pixels>) -> Self { self.width = width.into().0.max(0.0); self } - /// Sets the scrollbar margin of the [`Scrollable`] . + /// Sets the scrollbar margin of the [`Scrollbar`] . pub fn margin(mut self, margin: impl Into<Pixels>) -> Self { self.margin = margin.into().0; self } - /// Sets the scroller width of the [`Scrollable`] . + /// Sets the scroller width of the [`Scrollbar`] . pub fn scroller_width(mut self, scroller_width: impl Into<Pixels>) -> Self { self.scroller_width = scroller_width.into().0.max(0.0); self } - /// Sets the alignment of the [`Scrollable`] . - pub fn alignment(mut self, alignment: Alignment) -> Self { + /// Sets the [`Anchor`] of the [`Scrollbar`] . + pub fn anchor(mut self, alignment: Anchor) -> Self { self.alignment = alignment; self } + + /// Sets whether the [`Scrollbar`] should be embedded in the [`Scrollable`], using + /// the given spacing between itself and the contents. + /// + /// An embedded [`Scrollbar`] will always be displayed, will take layout space, + /// and will not float over the contents. + pub fn spacing(mut self, spacing: impl Into<Pixels>) -> Self { + self.spacing = Some(spacing.into().0); + self + } } -/// Alignment of the scrollable's content relative to it's [`Viewport`] in one direction. +/// The anchor of the scroller of the [`Scrollable`] relative to its [`Viewport`] +/// on a given axis. #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] -pub enum Alignment { - /// Content is aligned to the start of the [`Viewport`]. +pub enum Anchor { + /// Scroller is anchoer to the start of the [`Viewport`]. #[default] Start, - /// Content is aligned to the end of the [`Viewport`] + /// Content is aligned to the end of the [`Viewport`]. End, } @@ -265,29 +379,55 @@ where renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { - layout::contained(limits, self.width, self.height, |limits| { - let child_limits = layout::Limits::new( - Size::new(limits.min().width, limits.min().height), - Size::new( - if self.direction.horizontal().is_some() { - f32::INFINITY - } else { - limits.max().width - }, - if self.direction.vertical().is_some() { - f32::MAX - } else { - limits.max().height - }, - ), - ); + let (right_padding, bottom_padding) = match self.direction { + Direction::Vertical(Scrollbar { + width, + margin, + spacing: Some(spacing), + .. + }) => (width + margin * 2.0 + spacing, 0.0), + Direction::Horizontal(Scrollbar { + width, + margin, + spacing: Some(spacing), + .. + }) => (0.0, width + margin * 2.0 + spacing), + _ => (0.0, 0.0), + }; - self.content.as_widget().layout( - &mut tree.children[0], - renderer, - &child_limits, - ) - }) + layout::padded( + limits, + self.width, + self.height, + Padding { + right: right_padding, + bottom: bottom_padding, + ..Padding::ZERO + }, + |limits| { + let child_limits = layout::Limits::new( + Size::new(limits.min().width, limits.min().height), + Size::new( + if self.direction.horizontal().is_some() { + f32::INFINITY + } else { + limits.max().width + }, + if self.direction.vertical().is_some() { + f32::MAX + } else { + limits.max().height + }, + ), + ); + + self.content.as_widget().layout( + &mut tree.children[0], + renderer, + &child_limits, + ) + }, + ) } fn operate( @@ -295,7 +435,7 @@ where tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn Operation<()>, + operation: &mut dyn Operation, ) { let state = tree.state.downcast_mut::<State>(); @@ -309,6 +449,7 @@ where state, self.id.as_ref().map(|id| &id.0), bounds, + content_bounds, translation, ); @@ -350,6 +491,24 @@ where let (mouse_over_y_scrollbar, mouse_over_x_scrollbar) = scrollbars.is_mouse_over(cursor); + if let Some(last_scrolled) = state.last_scrolled { + let clear_transaction = match event { + Event::Mouse( + mouse::Event::ButtonPressed(_) + | mouse::Event::ButtonReleased(_) + | mouse::Event::CursorLeft, + ) => true, + Event::Mouse(mouse::Event::CursorMoved { .. }) => { + last_scrolled.elapsed() > Duration::from_millis(100) + } + _ => last_scrolled.elapsed() > Duration::from_millis(1500), + }; + + if clear_transaction { + state.last_scrolled = None; + } + } + if let Some(scroller_grabbed_at) = state.y_scroller_grabbed_at { match event { Event::Mouse(mouse::Event::CursorMoved { .. }) @@ -368,7 +527,7 @@ where content_bounds, ); - let _ = notify_on_scroll( + let _ = notify_scroll( state, &self.on_scroll, bounds, @@ -406,7 +565,7 @@ where state.y_scroller_grabbed_at = Some(scroller_grabbed_at); - let _ = notify_on_scroll( + let _ = notify_scroll( state, &self.on_scroll, bounds, @@ -439,7 +598,7 @@ where content_bounds, ); - let _ = notify_on_scroll( + let _ = notify_scroll( state, &self.on_scroll, bounds, @@ -477,7 +636,7 @@ where state.x_scroller_grabbed_at = Some(scroller_grabbed_at); - let _ = notify_on_scroll( + let _ = notify_scroll( state, &self.on_scroll, bounds, @@ -492,7 +651,11 @@ where } } - let mut event_status = { + let content_status = if state.last_scrolled.is_some() + && matches!(event, Event::Mouse(mouse::Event::WheelScrolled { .. })) + { + event::Status::Ignored + } else { let cursor = match cursor_over_scrollable { Some(cursor_position) if !(mouse_over_x_scrollbar || mouse_over_y_scrollbar) => @@ -540,10 +703,10 @@ where state.x_scroller_grabbed_at = None; state.y_scroller_grabbed_at = None; - return event_status; + return content_status; } - if let event::Status::Captured = event_status { + if let event::Status::Captured = content_status { return event::Status::Captured; } @@ -563,23 +726,41 @@ where let delta = match delta { mouse::ScrollDelta::Lines { x, y } => { - // TODO: Configurable speed/friction (?) - let movement = if !cfg!(target_os = "macos") // macOS automatically inverts the axes when Shift is pressed - && state.keyboard_modifiers.shift() - { - Vector::new(y, x) - } else { + let is_shift_pressed = state.keyboard_modifiers.shift(); + + // macOS automatically inverts the axes when Shift is pressed + let (x, y) = + if cfg!(target_os = "macos") && is_shift_pressed { + (y, x) + } else { + (x, y) + }; + + let is_vertical = match self.direction { + Direction::Vertical(_) => true, + Direction::Horizontal(_) => false, + Direction::Both { .. } => !is_shift_pressed, + }; + + let movement = if is_vertical { Vector::new(x, y) + } else { + Vector::new(y, x) }; - movement * 60.0 + // TODO: Configurable speed/friction (?) + -movement * 60.0 } mouse::ScrollDelta::Pixels { x, y } => Vector::new(x, y), }; - state.scroll(delta, self.direction, bounds, content_bounds); + state.scroll( + self.direction.align(delta), + bounds, + content_bounds, + ); - event_status = if notify_on_scroll( + if notify_scroll( state, &self.on_scroll, bounds, @@ -589,7 +770,7 @@ where event::Status::Captured } else { event::Status::Ignored - }; + } } Event::Touch(event) if state.scroll_area_touched_at.is_some() @@ -613,13 +794,12 @@ where }; let delta = Vector::new( - cursor_position.x - scroll_box_touched_at.x, - cursor_position.y - scroll_box_touched_at.y, + scroll_box_touched_at.x - cursor_position.x, + scroll_box_touched_at.y - cursor_position.y, ); state.scroll( - delta, - self.direction, + self.direction.align(delta), bounds, content_bounds, ); @@ -628,7 +808,7 @@ where Some(cursor_position); // TODO: bubble up touch movements if not consumed. - let _ = notify_on_scroll( + let _ = notify_scroll( state, &self.on_scroll, bounds, @@ -640,12 +820,21 @@ where _ => {} } - event_status = event::Status::Captured; + event::Status::Captured } - _ => {} - } + Event::Window(window::Event::RedrawRequested(_)) => { + let _ = notify_viewport( + state, + &self.on_scroll, + bounds, + content_bounds, + shell, + ); - event_status + event::Status::Ignored + } + _ => event::Status::Ignored, + } } fn draw( @@ -736,7 +925,7 @@ where let draw_scrollbar = |renderer: &mut Renderer, - style: Scrollbar, + style: Rail, scrollbar: &internals::Scrollbar| { if scrollbar.bounds.width > 0.0 && scrollbar.bounds.height > 0.0 @@ -756,21 +945,23 @@ where ); } - if scrollbar.scroller.bounds.width > 0.0 - && scrollbar.scroller.bounds.height > 0.0 - && (style.scroller.color != Color::TRANSPARENT - || (style.scroller.border.color - != Color::TRANSPARENT - && style.scroller.border.width > 0.0)) - { - renderer.fill_quad( - renderer::Quad { - bounds: scrollbar.scroller.bounds, - border: style.scroller.border, - ..renderer::Quad::default() - }, - style.scroller.color, - ); + if let Some(scroller) = scrollbar.scroller { + if scroller.bounds.width > 0.0 + && scroller.bounds.height > 0.0 + && (style.scroller.color != Color::TRANSPARENT + || (style.scroller.border.color + != Color::TRANSPARENT + && style.scroller.border.width > 0.0)) + { + renderer.fill_quad( + renderer::Quad { + bounds: scroller.bounds, + border: style.scroller.border, + ..renderer::Quad::default() + }, + style.scroller.color, + ); + } } }; @@ -784,7 +975,7 @@ where if let Some(scrollbar) = scrollbars.y { draw_scrollbar( renderer, - style.vertical_scrollbar, + style.vertical_rail, &scrollbar, ); } @@ -792,7 +983,7 @@ where if let Some(scrollbar) = scrollbars.x { draw_scrollbar( renderer, - style.horizontal_scrollbar, + style.horizontal_rail, &scrollbar, ); } @@ -953,21 +1144,44 @@ impl From<Id> for widget::Id { } /// Produces a [`Task`] that snaps the [`Scrollable`] with the given [`Id`] -/// to the provided `percentage` along the x & y axis. +/// to the provided [`RelativeOffset`]. pub fn snap_to<T>(id: Id, offset: RelativeOffset) -> Task<T> { - Task::effect(Action::widget(operation::scrollable::snap_to(id.0, offset))) + task::effect(Action::widget(operation::scrollable::snap_to(id.0, offset))) } /// Produces a [`Task`] that scrolls the [`Scrollable`] with the given [`Id`] -/// to the provided [`AbsoluteOffset`] along the x & y axis. +/// to the provided [`AbsoluteOffset`]. pub fn scroll_to<T>(id: Id, offset: AbsoluteOffset) -> Task<T> { - Task::effect(Action::widget(operation::scrollable::scroll_to( + task::effect(Action::widget(operation::scrollable::scroll_to( id.0, offset, ))) } -/// Returns [`true`] if the viewport actually changed. -fn notify_on_scroll<Message>( +/// Produces a [`Task`] that scrolls the [`Scrollable`] with the given [`Id`] +/// by the provided [`AbsoluteOffset`]. +pub fn scroll_by<T>(id: Id, offset: AbsoluteOffset) -> Task<T> { + task::effect(Action::widget(operation::scrollable::scroll_by( + id.0, offset, + ))) +} + +fn notify_scroll<Message>( + state: &mut State, + on_scroll: &Option<Box<dyn Fn(Viewport) -> Message + '_>>, + bounds: Rectangle, + content_bounds: Rectangle, + shell: &mut Shell<'_, Message>, +) -> bool { + if notify_viewport(state, on_scroll, bounds, content_bounds, shell) { + state.last_scrolled = Some(Instant::now()); + + true + } else { + false + } +} + +fn notify_viewport<Message>( state: &mut State, on_scroll: &Option<Box<dyn Fn(Viewport) -> Message + '_>>, bounds: Rectangle, @@ -980,6 +1194,11 @@ fn notify_on_scroll<Message>( return false; } + let Some(on_scroll) = on_scroll else { + state.last_notified = None; + return false; + }; + let viewport = Viewport { offset_x: state.offset_x, offset_y: state.offset_y, @@ -999,7 +1218,9 @@ fn notify_on_scroll<Message>( (a - b).abs() <= f32::EPSILON || (a.is_nan() && b.is_nan()) }; - if unchanged(last_relative_offset.x, current_relative_offset.x) + if last_notified.bounds == bounds + && last_notified.content_bounds == content_bounds + && unchanged(last_relative_offset.x, current_relative_offset.x) && unchanged(last_relative_offset.y, current_relative_offset.y) && unchanged(last_absolute_offset.x, current_absolute_offset.x) && unchanged(last_absolute_offset.y, current_absolute_offset.y) @@ -1008,9 +1229,7 @@ fn notify_on_scroll<Message>( } } - if let Some(on_scroll) = on_scroll { - shell.publish(on_scroll(viewport)); - } + shell.publish(on_scroll(viewport)); state.last_notified = Some(viewport); true @@ -1025,6 +1244,7 @@ struct State { x_scroller_grabbed_at: Option<f32>, keyboard_modifiers: keyboard::Modifiers, last_notified: Option<Viewport>, + last_scrolled: Option<Instant>, } impl Default for State { @@ -1037,6 +1257,7 @@ impl Default for State { x_scroller_grabbed_at: None, keyboard_modifiers: keyboard::Modifiers::default(), last_notified: None, + last_scrolled: None, } } } @@ -1049,6 +1270,15 @@ impl operation::Scrollable for State { fn scroll_to(&mut self, offset: AbsoluteOffset) { State::scroll_to(self, offset); } + + fn scroll_by( + &mut self, + offset: AbsoluteOffset, + bounds: Rectangle, + content_bounds: Rectangle, + ) { + State::scroll_by(self, offset, bounds, content_bounds); + } } #[derive(Debug, Clone, Copy)] @@ -1073,13 +1303,13 @@ impl Offset { self, viewport: f32, content: f32, - alignment: Alignment, + alignment: Anchor, ) -> f32 { let offset = self.absolute(viewport, content); match alignment { - Alignment::Start => offset, - Alignment::End => ((content - viewport).max(0.0) - offset).max(0.0), + Anchor::Start => offset, + Anchor::End => ((content - viewport).max(0.0) - offset).max(0.0), } } } @@ -1152,34 +1382,13 @@ impl State { pub fn scroll( &mut self, delta: Vector<f32>, - direction: Direction, bounds: Rectangle, content_bounds: Rectangle, ) { - let horizontal_alignment = direction - .horizontal() - .map(|p| p.alignment) - .unwrap_or_default(); - - let vertical_alignment = direction - .vertical() - .map(|p| p.alignment) - .unwrap_or_default(); - - let align = |alignment: Alignment, delta: f32| match alignment { - Alignment::Start => delta, - Alignment::End => -delta, - }; - - let delta = Vector::new( - align(horizontal_alignment, delta.x), - align(vertical_alignment, delta.y), - ); - if bounds.height < content_bounds.height { self.offset_y = Offset::Absolute( (self.offset_y.absolute(bounds.height, content_bounds.height) - - delta.y) + + delta.y) .clamp(0.0, content_bounds.height - bounds.height), ); } @@ -1187,7 +1396,7 @@ impl State { if bounds.width < content_bounds.width { self.offset_x = Offset::Absolute( (self.offset_x.absolute(bounds.width, content_bounds.width) - - delta.x) + + delta.x) .clamp(0.0, content_bounds.width - bounds.width), ); } @@ -1233,6 +1442,16 @@ impl State { self.offset_y = Offset::Absolute(offset.y.max(0.0)); } + /// Scroll by the provided [`AbsoluteOffset`]. + pub fn scroll_by( + &mut self, + offset: AbsoluteOffset, + bounds: Rectangle, + content_bounds: Rectangle, + ) { + self.scroll(Vector::new(offset.x, offset.y), bounds, content_bounds); + } + /// Unsnaps the current scroll position, if snapped, given the bounds of the /// [`Scrollable`] and its contents. pub fn unsnap(&mut self, bounds: Rectangle, content_bounds: Rectangle) { @@ -1298,16 +1517,16 @@ impl Scrollbars { ) -> Self { let translation = state.translation(direction, bounds, content_bounds); - let show_scrollbar_x = direction - .horizontal() - .filter(|_| content_bounds.width > bounds.width); + let show_scrollbar_x = direction.horizontal().filter(|scrollbar| { + scrollbar.spacing.is_some() || content_bounds.width > bounds.width + }); - let show_scrollbar_y = direction - .vertical() - .filter(|_| content_bounds.height > bounds.height); + let show_scrollbar_y = direction.vertical().filter(|scrollbar| { + scrollbar.spacing.is_some() || content_bounds.height > bounds.height + }); let y_scrollbar = if let Some(vertical) = show_scrollbar_y { - let Properties { + let Scrollbar { width, margin, scroller_width, @@ -1341,26 +1560,35 @@ impl Scrollbars { }; let ratio = bounds.height / content_bounds.height; - // min height for easier grabbing with super tall content - let scroller_height = (scrollbar_bounds.height * ratio).max(2.0); - let scroller_offset = - translation.y * ratio * scrollbar_bounds.height / bounds.height; - let scroller_bounds = Rectangle { - x: bounds.x + bounds.width - - total_scrollbar_width / 2.0 - - scroller_width / 2.0, - y: (scrollbar_bounds.y + scroller_offset).max(0.0), - width: scroller_width, - height: scroller_height, + let scroller = if ratio >= 1.0 { + None + } else { + // min height for easier grabbing with super tall content + let scroller_height = + (scrollbar_bounds.height * ratio).max(2.0); + let scroller_offset = + translation.y * ratio * scrollbar_bounds.height + / bounds.height; + + let scroller_bounds = Rectangle { + x: bounds.x + bounds.width + - total_scrollbar_width / 2.0 + - scroller_width / 2.0, + y: (scrollbar_bounds.y + scroller_offset).max(0.0), + width: scroller_width, + height: scroller_height, + }; + + Some(internals::Scroller { + bounds: scroller_bounds, + }) }; Some(internals::Scrollbar { total_bounds: total_scrollbar_bounds, bounds: scrollbar_bounds, - scroller: internals::Scroller { - bounds: scroller_bounds, - }, + scroller, alignment: vertical.alignment, }) } else { @@ -1368,7 +1596,7 @@ impl Scrollbars { }; let x_scrollbar = if let Some(horizontal) = show_scrollbar_x { - let Properties { + let Scrollbar { width, margin, scroller_width, @@ -1402,26 +1630,34 @@ impl Scrollbars { }; let ratio = bounds.width / content_bounds.width; - // min width for easier grabbing with extra wide content - let scroller_length = (scrollbar_bounds.width * ratio).max(2.0); - let scroller_offset = - translation.x * ratio * scrollbar_bounds.width / bounds.width; - let scroller_bounds = Rectangle { - x: (scrollbar_bounds.x + scroller_offset).max(0.0), - y: bounds.y + bounds.height - - total_scrollbar_height / 2.0 - - scroller_width / 2.0, - width: scroller_length, - height: scroller_width, + let scroller = if ratio >= 1.0 { + None + } else { + // min width for easier grabbing with extra wide content + let scroller_length = (scrollbar_bounds.width * ratio).max(2.0); + let scroller_offset = + translation.x * ratio * scrollbar_bounds.width + / bounds.width; + + let scroller_bounds = Rectangle { + x: (scrollbar_bounds.x + scroller_offset).max(0.0), + y: bounds.y + bounds.height + - total_scrollbar_height / 2.0 + - scroller_width / 2.0, + width: scroller_length, + height: scroller_width, + }; + + Some(internals::Scroller { + bounds: scroller_bounds, + }) }; Some(internals::Scrollbar { total_bounds: total_scrollbar_bounds, bounds: scrollbar_bounds, - scroller: internals::Scroller { - bounds: scroller_bounds, - }, + scroller, alignment: horizontal.alignment, }) } else { @@ -1452,33 +1688,33 @@ impl Scrollbars { } fn grab_y_scroller(&self, cursor_position: Point) -> Option<f32> { - self.y.and_then(|scrollbar| { - if scrollbar.total_bounds.contains(cursor_position) { - Some(if scrollbar.scroller.bounds.contains(cursor_position) { - (cursor_position.y - scrollbar.scroller.bounds.y) - / scrollbar.scroller.bounds.height - } else { - 0.5 - }) + let scrollbar = self.y?; + let scroller = scrollbar.scroller?; + + if scrollbar.total_bounds.contains(cursor_position) { + Some(if scroller.bounds.contains(cursor_position) { + (cursor_position.y - scroller.bounds.y) / scroller.bounds.height } else { - None - } - }) + 0.5 + }) + } else { + None + } } fn grab_x_scroller(&self, cursor_position: Point) -> Option<f32> { - self.x.and_then(|scrollbar| { - if scrollbar.total_bounds.contains(cursor_position) { - Some(if scrollbar.scroller.bounds.contains(cursor_position) { - (cursor_position.x - scrollbar.scroller.bounds.x) - / scrollbar.scroller.bounds.width - } else { - 0.5 - }) + let scrollbar = self.x?; + let scroller = scrollbar.scroller?; + + if scrollbar.total_bounds.contains(cursor_position) { + Some(if scroller.bounds.contains(cursor_position) { + (cursor_position.x - scroller.bounds.x) / scroller.bounds.width } else { - None - } - }) + 0.5 + }) + } else { + None + } } fn active(&self) -> bool { @@ -1489,14 +1725,14 @@ impl Scrollbars { pub(super) mod internals { use crate::core::{Point, Rectangle}; - use super::Alignment; + use super::Anchor; #[derive(Debug, Copy, Clone)] pub struct Scrollbar { pub total_bounds: Rectangle, pub bounds: Rectangle, - pub scroller: Scroller, - pub alignment: Alignment, + pub scroller: Option<Scroller>, + pub alignment: Anchor, } impl Scrollbar { @@ -1511,14 +1747,18 @@ pub(super) mod internals { grabbed_at: f32, cursor_position: Point, ) -> f32 { - let percentage = (cursor_position.y - - self.bounds.y - - self.scroller.bounds.height * grabbed_at) - / (self.bounds.height - self.scroller.bounds.height); - - match self.alignment { - Alignment::Start => percentage, - Alignment::End => 1.0 - percentage, + if let Some(scroller) = self.scroller { + let percentage = (cursor_position.y + - self.bounds.y + - scroller.bounds.height * grabbed_at) + / (self.bounds.height - scroller.bounds.height); + + match self.alignment { + Anchor::Start => percentage, + Anchor::End => 1.0 - percentage, + } + } else { + 0.0 } } @@ -1528,14 +1768,18 @@ pub(super) mod internals { grabbed_at: f32, cursor_position: Point, ) -> f32 { - let percentage = (cursor_position.x - - self.bounds.x - - self.scroller.bounds.width * grabbed_at) - / (self.bounds.width - self.scroller.bounds.width); - - match self.alignment { - Alignment::Start => percentage, - Alignment::End => 1.0 - percentage, + if let Some(scroller) = self.scroller { + let percentage = (cursor_position.x + - self.bounds.x + - scroller.bounds.width * grabbed_at) + / (self.bounds.width - scroller.bounds.width); + + match self.alignment { + Anchor::Start => percentage, + Anchor::End => 1.0 - percentage, + } + } else { + 0.0 } } } @@ -1569,22 +1813,22 @@ pub enum Status { }, } -/// The appearance of a scrolable. +/// The appearance of a scrollable. #[derive(Debug, Clone, Copy)] pub struct Style { /// The [`container::Style`] of a scrollable. pub container: container::Style, - /// The vertical [`Scrollbar`] appearance. - pub vertical_scrollbar: Scrollbar, - /// The horizontal [`Scrollbar`] appearance. - pub horizontal_scrollbar: Scrollbar, + /// The vertical [`Rail`] appearance. + pub vertical_rail: Rail, + /// The horizontal [`Rail`] appearance. + pub horizontal_rail: Rail, /// The [`Background`] of the gap between a horizontal and vertical scrollbar. pub gap: Option<Background>, } /// The appearance of the scrollbar of a scrollable. #[derive(Debug, Clone, Copy)] -pub struct Scrollbar { +pub struct Rail { /// The [`Background`] of a scrollbar. pub background: Option<Background>, /// The [`Border`] of a scrollbar. @@ -1633,27 +1877,27 @@ impl Catalog for Theme { pub fn default(theme: &Theme, status: Status) -> Style { let palette = theme.extended_palette(); - let scrollbar = Scrollbar { + let scrollbar = Rail { background: Some(palette.background.weak.color.into()), - border: Border::rounded(2), + border: border::rounded(2), scroller: Scroller { color: palette.background.strong.color, - border: Border::rounded(2), + border: border::rounded(2), }, }; match status { Status::Active => Style { container: container::Style::default(), - vertical_scrollbar: scrollbar, - horizontal_scrollbar: scrollbar, + vertical_rail: scrollbar, + horizontal_rail: scrollbar, gap: None, }, Status::Hovered { is_horizontal_scrollbar_hovered, is_vertical_scrollbar_hovered, } => { - let hovered_scrollbar = Scrollbar { + let hovered_scrollbar = Rail { scroller: Scroller { color: palette.primary.strong.color, ..scrollbar.scroller @@ -1663,12 +1907,12 @@ pub fn default(theme: &Theme, status: Status) -> Style { Style { container: container::Style::default(), - vertical_scrollbar: if is_vertical_scrollbar_hovered { + vertical_rail: if is_vertical_scrollbar_hovered { hovered_scrollbar } else { scrollbar }, - horizontal_scrollbar: if is_horizontal_scrollbar_hovered { + horizontal_rail: if is_horizontal_scrollbar_hovered { hovered_scrollbar } else { scrollbar @@ -1680,7 +1924,7 @@ pub fn default(theme: &Theme, status: Status) -> Style { is_horizontal_scrollbar_dragged, is_vertical_scrollbar_dragged, } => { - let dragged_scrollbar = Scrollbar { + let dragged_scrollbar = Rail { scroller: Scroller { color: palette.primary.base.color, ..scrollbar.scroller @@ -1690,12 +1934,12 @@ pub fn default(theme: &Theme, status: Status) -> Style { Style { container: container::Style::default(), - vertical_scrollbar: if is_vertical_scrollbar_dragged { + vertical_rail: if is_vertical_scrollbar_dragged { dragged_scrollbar } else { scrollbar }, - horizontal_scrollbar: if is_horizontal_scrollbar_dragged { + horizontal_rail: if is_horizontal_scrollbar_dragged { dragged_scrollbar } else { scrollbar diff --git a/widget/src/slider.rs b/widget/src/slider.rs index a8f1d192..aebf68e2 100644 --- a/widget/src/slider.rs +++ b/widget/src/slider.rs @@ -1,5 +1,5 @@ //! Display an interactive selector of a single value from a range of values. -use crate::core::border; +use crate::core::border::{self, Border}; use crate::core::event::{self, Event}; use crate::core::keyboard; use crate::core::keyboard::key::{self, Key}; @@ -9,7 +9,7 @@ use crate::core::renderer; use crate::core::touch; use crate::core::widget::tree::{self, Tree}; use crate::core::{ - self, Border, Clipboard, Color, Element, Layout, Length, Pixels, Point, + self, Background, Clipboard, Color, Element, Layout, Length, Pixels, Point, Rectangle, Shell, Size, Theme, Widget, }; @@ -70,8 +70,8 @@ where /// * an inclusive range of possible values /// * the current value of the [`Slider`] /// * a function that will be called when the [`Slider`] is dragged. - /// It receives the new value of the [`Slider`] and must produce a - /// `Message`. + /// It receives the new value of the [`Slider`] and must produce a + /// `Message`. pub fn new<F>(range: RangeInclusive<T>, value: T, on_change: F) -> Self where F: 'a + Fn(T) -> Message, @@ -237,7 +237,7 @@ where let steps = (percent * (end - start) / step).round(); let value = steps * step + start; - T::from_f64(value) + T::from_f64(value.min(end)) }; new_value @@ -408,10 +408,10 @@ where width: offset + handle_width / 2.0, height: style.rail.width, }, - border: Border::rounded(style.rail.border_radius), + border: style.rail.border, ..renderer::Quad::default() }, - style.rail.colors.0, + style.rail.backgrounds.0, ); renderer.fill_quad( @@ -422,10 +422,10 @@ where width: bounds.width - offset - handle_width / 2.0, height: style.rail.width, }, - border: Border::rounded(style.rail.border_radius), + border: style.rail.border, ..renderer::Quad::default() }, - style.rail.colors.1, + style.rail.backgrounds.1, ); renderer.fill_quad( @@ -443,7 +443,7 @@ where }, ..renderer::Quad::default() }, - style.handle.color, + style.handle.background, ); } @@ -524,12 +524,12 @@ impl Style { /// The appearance of a slider rail #[derive(Debug, Clone, Copy)] pub struct Rail { - /// The colors of the rail of the slider. - pub colors: (Color, Color), + /// The backgrounds of the rail of the slider. + pub backgrounds: (Background, Background), /// The width of the stroke of a slider rail. pub width: f32, - /// The border radius of the corners of the rail. - pub border_radius: border::Radius, + /// The border of the rail. + pub border: Border, } /// The appearance of the handle of a slider. @@ -537,8 +537,8 @@ pub struct Rail { pub struct Handle { /// The shape of the handle. pub shape: HandleShape, - /// The [`Color`] of the handle. - pub color: Color, + /// The [`Background`] of the handle. + pub background: Background, /// The border width of the handle. pub border_width: f32, /// The border [`Color`] of the handle. @@ -601,13 +601,17 @@ pub fn default(theme: &Theme, status: Status) -> Style { Style { rail: Rail { - colors: (color, palette.secondary.base.color), + backgrounds: (color.into(), palette.secondary.base.color.into()), width: 4.0, - border_radius: 2.0.into(), + border: Border { + radius: 2.0.into(), + width: 0.0, + color: Color::TRANSPARENT, + }, }, handle: Handle { shape: HandleShape::Circle { radius: 7.0 }, - color, + background: color.into(), border_color: Color::TRANSPARENT, border_width: 0.0, }, diff --git a/widget/src/stack.rs b/widget/src/stack.rs index efa9711d..9ccaa274 100644 --- a/widget/src/stack.rs +++ b/widget/src/stack.rs @@ -189,7 +189,7 @@ where tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn Operation<()>, + operation: &mut dyn Operation, ) { operation.container(None, layout.bounds(), &mut |operation| { self.children @@ -209,19 +209,23 @@ where tree: &mut Tree, event: Event, layout: Layout<'_>, - cursor: mouse::Cursor, + mut cursor: mouse::Cursor, renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, viewport: &Rectangle, ) -> event::Status { + let is_over_scroll = + matches!(event, Event::Mouse(mouse::Event::WheelScrolled { .. })) + && cursor.is_over(layout.bounds()); + self.children .iter_mut() .rev() .zip(tree.children.iter_mut().rev()) .zip(layout.children().rev()) .map(|((child, state), layout)| { - child.as_widget_mut().on_event( + let status = child.as_widget_mut().on_event( state, event.clone(), layout, @@ -230,7 +234,19 @@ where clipboard, shell, viewport, - ) + ); + + if is_over_scroll && cursor != mouse::Cursor::Unavailable { + let interaction = child.as_widget().mouse_interaction( + state, layout, cursor, viewport, renderer, + ); + + if interaction != mouse::Interaction::None { + cursor = mouse::Cursor::Unavailable; + } + } + + status }) .find(|&status| status == event::Status::Captured) .unwrap_or(event::Status::Ignored) @@ -269,15 +285,53 @@ where viewport: &Rectangle, ) { if let Some(clipped_viewport) = layout.bounds().intersection(viewport) { - for (i, ((layer, state), layout)) in self + let layers_below = if cursor.is_over(layout.bounds()) { + self.children + .iter() + .rev() + .zip(tree.children.iter().rev()) + .zip(layout.children().rev()) + .position(|((layer, state), layout)| { + let interaction = layer.as_widget().mouse_interaction( + state, layout, cursor, viewport, renderer, + ); + + interaction != mouse::Interaction::None + }) + .map(|i| self.children.len() - i - 1) + .unwrap_or_default() + } else { + 0 + }; + + let mut layers = self .children .iter() .zip(&tree.children) .zip(layout.children()) - .enumerate() - { - if i > 0 { - renderer.with_layer(clipped_viewport, |renderer| { + .enumerate(); + + let layers = layers.by_ref(); + + let mut draw_layer = + |i, + layer: &Element<'a, Message, Theme, Renderer>, + state, + layout, + cursor| { + if i > 0 { + renderer.with_layer(clipped_viewport, |renderer| { + layer.as_widget().draw( + state, + renderer, + theme, + style, + layout, + cursor, + &clipped_viewport, + ); + }); + } else { layer.as_widget().draw( state, renderer, @@ -287,18 +341,15 @@ where cursor, &clipped_viewport, ); - }); - } else { - layer.as_widget().draw( - state, - renderer, - theme, - style, - layout, - cursor, - &clipped_viewport, - ); - } + } + }; + + for (i, ((layer, state), layout)) in layers.take(layers_below) { + draw_layer(i, layer, state, layout, mouse::Cursor::Unavailable); + } + + for (i, ((layer, state), layout)) in layers { + draw_layer(i, layer, state, layout, cursor); } } } diff --git a/widget/src/svg.rs b/widget/src/svg.rs index 4551bcad..bec0090f 100644 --- a/widget/src/svg.rs +++ b/widget/src/svg.rs @@ -211,11 +211,13 @@ where let render = |renderer: &mut Renderer| { renderer.draw_svg( - self.handle.clone(), - style.color, + svg::Svg { + handle: self.handle.clone(), + color: style.color, + rotation: self.rotation.radians(), + opacity: self.opacity, + }, drawing_bounds, - self.rotation.radians(), - self.opacity, ); }; diff --git a/widget/src/text.rs b/widget/src/text.rs index 0d689295..9bf7fce4 100644 --- a/widget/src/text.rs +++ b/widget/src/text.rs @@ -1,5 +1,9 @@ //! Draw and interact with text. +mod rich; + +pub use crate::core::text::{Fragment, Highlighter, IntoFragment, Span}; pub use crate::core::widget::text::*; +pub use rich::Rich; /// A paragraph. pub type Text<'a, Theme = crate::Theme, Renderer = crate::Renderer> = diff --git a/widget/src/text/rich.rs b/widget/src/text/rich.rs new file mode 100644 index 00000000..921c55a5 --- /dev/null +++ b/widget/src/text/rich.rs @@ -0,0 +1,538 @@ +use crate::core::alignment; +use crate::core::event; +use crate::core::layout; +use crate::core::mouse; +use crate::core::renderer; +use crate::core::text::{Paragraph, Span}; +use crate::core::widget::text::{ + self, Catalog, LineHeight, Shaping, Style, StyleFn, Wrapping, +}; +use crate::core::widget::tree::{self, Tree}; +use crate::core::{ + self, Clipboard, Color, Element, Event, Layout, Length, Pixels, Point, + Rectangle, Shell, Size, Vector, Widget, +}; + +/// A bunch of [`Rich`] text. +#[allow(missing_debug_implementations)] +pub struct Rich<'a, Link, Theme = crate::Theme, Renderer = crate::Renderer> +where + Link: Clone + 'static, + Theme: Catalog, + Renderer: core::text::Renderer, +{ + spans: Box<dyn AsRef<[Span<'a, Link, Renderer::Font>]> + 'a>, + size: Option<Pixels>, + line_height: LineHeight, + width: Length, + height: Length, + font: Option<Renderer::Font>, + align_x: alignment::Horizontal, + align_y: alignment::Vertical, + wrapping: Wrapping, + class: Theme::Class<'a>, +} + +impl<'a, Link, Theme, Renderer> Rich<'a, Link, Theme, Renderer> +where + Link: Clone + 'static, + Theme: Catalog, + Renderer: core::text::Renderer, + Renderer::Font: 'a, +{ + /// Creates a new empty [`Rich`] text. + pub fn new() -> Self { + Self { + spans: Box::new([]), + size: None, + line_height: LineHeight::default(), + width: Length::Shrink, + height: Length::Shrink, + font: None, + align_x: alignment::Horizontal::Left, + align_y: alignment::Vertical::Top, + wrapping: Wrapping::default(), + class: Theme::default(), + } + } + + /// Creates a new [`Rich`] text with the given text spans. + pub fn with_spans( + spans: impl AsRef<[Span<'a, Link, Renderer::Font>]> + 'a, + ) -> Self { + Self { + spans: Box::new(spans), + ..Self::new() + } + } + + /// Sets the default size of the [`Rich`] text. + pub fn size(mut self, size: impl Into<Pixels>) -> Self { + self.size = Some(size.into()); + self + } + + /// Sets the defualt [`LineHeight`] of the [`Rich`] text. + pub fn line_height(mut self, line_height: impl Into<LineHeight>) -> Self { + self.line_height = line_height.into(); + self + } + + /// Sets the default font of the [`Rich`] text. + pub fn font(mut self, font: impl Into<Renderer::Font>) -> Self { + self.font = Some(font.into()); + self + } + + /// Sets the width of the [`Rich`] text boundaries. + pub fn width(mut self, width: impl Into<Length>) -> Self { + self.width = width.into(); + self + } + + /// Sets the height of the [`Rich`] text boundaries. + pub fn height(mut self, height: impl Into<Length>) -> Self { + self.height = height.into(); + self + } + + /// Centers the [`Rich`] 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 [`Rich`] text. + pub fn align_x( + mut self, + alignment: impl Into<alignment::Horizontal>, + ) -> Self { + self.align_x = alignment.into(); + self + } + + /// Sets the [`alignment::Vertical`] of the [`Rich`] text. + pub fn align_y( + mut self, + alignment: impl Into<alignment::Vertical>, + ) -> Self { + self.align_y = alignment.into(); + self + } + + /// Sets the [`Wrapping`] strategy of the [`Rich`] text. + pub fn wrapping(mut self, wrapping: Wrapping) -> Self { + self.wrapping = wrapping; + self + } + + /// Sets the default style of the [`Rich`] text. + #[must_use] + pub fn style(mut self, style: impl Fn(&Theme) -> Style + 'a) -> Self + where + Theme::Class<'a>: From<StyleFn<'a, Theme>>, + { + self.class = (Box::new(style) as StyleFn<'a, Theme>).into(); + self + } + + /// Sets the default [`Color`] of the [`Rich`] text. + pub fn color(self, color: impl Into<Color>) -> Self + where + Theme::Class<'a>: From<StyleFn<'a, Theme>>, + { + self.color_maybe(Some(color)) + } + + /// Sets the default [`Color`] of the [`Rich`] text, if `Some`. + pub fn color_maybe(self, color: Option<impl Into<Color>>) -> Self + where + Theme::Class<'a>: From<StyleFn<'a, Theme>>, + { + let color = color.map(Into::into); + + self.style(move |_theme| Style { color }) + } + + /// Sets the default style class of the [`Rich`] text. + #[cfg(feature = "advanced")] + #[must_use] + pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self { + self.class = class.into(); + self + } +} + +impl<'a, Link, Theme, Renderer> Default for Rich<'a, Link, Theme, Renderer> +where + Link: Clone + 'a, + Theme: Catalog, + Renderer: core::text::Renderer, + Renderer::Font: 'a, +{ + fn default() -> Self { + Self::new() + } +} + +struct State<Link, P: Paragraph> { + spans: Vec<Span<'static, Link, P::Font>>, + span_pressed: Option<usize>, + paragraph: P, +} + +impl<'a, Link, Theme, Renderer> Widget<Link, Theme, Renderer> + for Rich<'a, Link, Theme, Renderer> +where + Link: Clone + 'static, + Theme: Catalog, + Renderer: core::text::Renderer, +{ + fn tag(&self) -> tree::Tag { + tree::Tag::of::<State<Link, Renderer::Paragraph>>() + } + + fn state(&self) -> tree::State { + tree::State::new(State::<Link, _> { + spans: Vec::new(), + span_pressed: None, + paragraph: Renderer::Paragraph::default(), + }) + } + + fn size(&self) -> Size<Length> { + Size { + width: self.width, + height: self.height, + } + } + + fn layout( + &self, + tree: &mut Tree, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + layout( + tree.state + .downcast_mut::<State<Link, Renderer::Paragraph>>(), + renderer, + limits, + self.width, + self.height, + self.spans.as_ref().as_ref(), + self.line_height, + self.size, + self.font, + self.align_x, + self.align_y, + self.wrapping, + ) + } + + fn draw( + &self, + tree: &Tree, + renderer: &mut Renderer, + theme: &Theme, + defaults: &renderer::Style, + layout: Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + ) { + let state = tree + .state + .downcast_ref::<State<Link, Renderer::Paragraph>>(); + + let style = theme.style(&self.class); + + let hovered_span = cursor + .position_in(layout.bounds()) + .and_then(|position| state.paragraph.hit_span(position)); + + for (index, span) in self.spans.as_ref().as_ref().iter().enumerate() { + let is_hovered_link = + span.link.is_some() && Some(index) == hovered_span; + + if span.highlight.is_some() + || span.underline + || span.strikethrough + || is_hovered_link + { + let translation = layout.position() - Point::ORIGIN; + let regions = state.paragraph.span_bounds(index); + + if let Some(highlight) = span.highlight { + for bounds in ®ions { + let bounds = Rectangle::new( + bounds.position() + - Vector::new( + span.padding.left, + span.padding.top, + ), + bounds.size() + + Size::new( + span.padding.horizontal(), + span.padding.vertical(), + ), + ); + + renderer.fill_quad( + renderer::Quad { + bounds: bounds + translation, + border: highlight.border, + ..Default::default() + }, + highlight.background, + ); + } + } + + if span.underline || span.strikethrough || is_hovered_link { + let size = span + .size + .or(self.size) + .unwrap_or(renderer.default_size()); + + let line_height = span + .line_height + .unwrap_or(self.line_height) + .to_absolute(size); + + let color = span + .color + .or(style.color) + .unwrap_or(defaults.text_color); + + let baseline = translation + + Vector::new( + 0.0, + size.0 + (line_height.0 - size.0) / 2.0, + ); + + if span.underline || is_hovered_link { + for bounds in ®ions { + renderer.fill_quad( + renderer::Quad { + bounds: Rectangle::new( + bounds.position() + baseline + - Vector::new(0.0, size.0 * 0.08), + Size::new(bounds.width, 1.0), + ), + ..Default::default() + }, + color, + ); + } + } + + if span.strikethrough { + for bounds in ®ions { + renderer.fill_quad( + renderer::Quad { + bounds: Rectangle::new( + bounds.position() + baseline + - Vector::new(0.0, size.0 / 2.0), + Size::new(bounds.width, 1.0), + ), + ..Default::default() + }, + color, + ); + } + } + } + } + } + + text::draw( + renderer, + defaults, + layout, + &state.paragraph, + style, + viewport, + ); + } + + fn on_event( + &mut self, + tree: &mut Tree, + event: Event, + layout: Layout<'_>, + cursor: mouse::Cursor, + _renderer: &Renderer, + _clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Link>, + _viewport: &Rectangle, + ) -> event::Status { + match event { + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => { + if let Some(position) = cursor.position_in(layout.bounds()) { + let state = tree + .state + .downcast_mut::<State<Link, Renderer::Paragraph>>(); + + if let Some(span) = state.paragraph.hit_span(position) { + state.span_pressed = Some(span); + + return event::Status::Captured; + } + } + } + Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) => { + let state = tree + .state + .downcast_mut::<State<Link, Renderer::Paragraph>>(); + + if let Some(span_pressed) = state.span_pressed { + state.span_pressed = None; + + if let Some(position) = cursor.position_in(layout.bounds()) + { + match state.paragraph.hit_span(position) { + Some(span) if span == span_pressed => { + if let Some(link) = self + .spans + .as_ref() + .as_ref() + .get(span) + .and_then(|span| span.link.clone()) + { + shell.publish(link); + } + } + _ => {} + } + } + } + } + _ => {} + } + + event::Status::Ignored + } + + fn mouse_interaction( + &self, + tree: &Tree, + layout: Layout<'_>, + cursor: mouse::Cursor, + _viewport: &Rectangle, + _renderer: &Renderer, + ) -> mouse::Interaction { + if let Some(position) = cursor.position_in(layout.bounds()) { + let state = tree + .state + .downcast_ref::<State<Link, Renderer::Paragraph>>(); + + if let Some(span) = state + .paragraph + .hit_span(position) + .and_then(|span| self.spans.as_ref().as_ref().get(span)) + { + if span.link.is_some() { + return mouse::Interaction::Pointer; + } + } + } + + mouse::Interaction::None + } +} + +fn layout<Link, Renderer>( + state: &mut State<Link, Renderer::Paragraph>, + renderer: &Renderer, + limits: &layout::Limits, + width: Length, + height: Length, + spans: &[Span<'_, Link, Renderer::Font>], + line_height: LineHeight, + size: Option<Pixels>, + font: Option<Renderer::Font>, + horizontal_alignment: alignment::Horizontal, + vertical_alignment: alignment::Vertical, + wrapping: Wrapping, +) -> layout::Node +where + Link: Clone, + Renderer: core::text::Renderer, +{ + layout::sized(limits, width, height, |limits| { + let bounds = limits.max(); + + let size = size.unwrap_or_else(|| renderer.default_size()); + let font = font.unwrap_or_else(|| renderer.default_font()); + + let text_with_spans = || core::Text { + content: spans, + bounds, + size, + line_height, + font, + horizontal_alignment, + vertical_alignment, + shaping: Shaping::Advanced, + wrapping, + }; + + if state.spans != spans { + state.paragraph = + Renderer::Paragraph::with_spans(text_with_spans()); + state.spans = spans.iter().cloned().map(Span::to_static).collect(); + } else { + match state.paragraph.compare(core::Text { + content: (), + bounds, + size, + line_height, + font, + horizontal_alignment, + vertical_alignment, + shaping: Shaping::Advanced, + wrapping, + }) { + core::text::Difference::None => {} + core::text::Difference::Bounds => { + state.paragraph.resize(bounds); + } + core::text::Difference::Shape => { + state.paragraph = + Renderer::Paragraph::with_spans(text_with_spans()); + } + } + } + + state.paragraph.min_bounds() + }) +} + +impl<'a, Link, Theme, Renderer> FromIterator<Span<'a, Link, Renderer::Font>> + for Rich<'a, Link, Theme, Renderer> +where + Link: Clone + 'a, + Theme: Catalog, + Renderer: core::text::Renderer, + Renderer::Font: 'a, +{ + fn from_iter<T: IntoIterator<Item = Span<'a, Link, Renderer::Font>>>( + spans: T, + ) -> Self { + Self::with_spans(spans.into_iter().collect::<Vec<_>>()) + } +} + +impl<'a, Link, Theme, Renderer> From<Rich<'a, Link, Theme, Renderer>> + for Element<'a, Link, Theme, Renderer> +where + Link: Clone + 'a, + Theme: Catalog + 'a, + Renderer: core::text::Renderer + 'a, +{ + fn from( + text: Rich<'a, Link, Theme, Renderer>, + ) -> Element<'a, Link, Theme, Renderer> { + Element::new(text) + } +} diff --git a/widget/src/text_editor.rs b/widget/src/text_editor.rs index fc2ade43..e0102656 100644 --- a/widget/src/text_editor.rs +++ b/widget/src/text_editor.rs @@ -1,4 +1,5 @@ //! Display a multi-line text input for text editing. +use crate::core::alignment; use crate::core::clipboard::{self, Clipboard}; use crate::core::event::{self, Event}; use crate::core::keyboard; @@ -8,11 +9,14 @@ use crate::core::mouse; use crate::core::renderer; use crate::core::text::editor::{Cursor, Editor as _}; use crate::core::text::highlighter::{self, Highlighter}; -use crate::core::text::{self, LineHeight}; +use crate::core::text::{self, LineHeight, Text, Wrapping}; +use crate::core::time::{Duration, Instant}; +use crate::core::widget::operation; use crate::core::widget::{self, Widget}; +use crate::core::window; use crate::core::{ - Background, Border, Color, Element, Length, Padding, Pixels, Rectangle, - Shell, Size, Theme, Vector, + Background, Border, Color, Element, Length, Padding, Pixels, Point, + Rectangle, Shell, Size, SmolStr, Theme, Vector, }; use std::cell::RefCell; @@ -36,13 +40,16 @@ pub struct TextEditor< Renderer: text::Renderer, { content: &'a Content<Renderer>, + placeholder: Option<text::Fragment<'a>>, font: Option<Renderer::Font>, text_size: Option<Pixels>, line_height: LineHeight, width: Length, height: Length, padding: Padding, + wrapping: Wrapping, class: Theme::Class<'a>, + key_binding: Option<Box<dyn Fn(KeyPress) -> Option<Binding<Message>> + 'a>>, on_edit: Option<Box<dyn Fn(Action) -> Message + 'a>>, highlighter_settings: Highlighter::Settings, highlighter_format: fn( @@ -61,13 +68,16 @@ where pub fn new(content: &'a Content<Renderer>) -> Self { Self { content, + placeholder: None, font: None, text_size: None, line_height: LineHeight::default(), width: Length::Fill, height: Length::Shrink, padding: Padding::new(5.0), + wrapping: Wrapping::default(), class: Theme::default(), + key_binding: None, on_edit: None, highlighter_settings: (), highlighter_format: |_highlight, _theme| { @@ -84,6 +94,15 @@ where Theme: Catalog, Renderer: text::Renderer, { + /// Sets the placeholder of the [`TextEditor`]. + pub fn placeholder( + mut self, + placeholder: impl text::IntoFragment<'a>, + ) -> Self { + self.placeholder = Some(placeholder.into_fragment()); + self + } + /// Sets the height of the [`TextEditor`]. pub fn height(mut self, height: impl Into<Length>) -> Self { self.height = height.into(); @@ -131,9 +150,34 @@ where self } + /// Sets the [`Wrapping`] strategy of the [`TextEditor`]. + pub fn wrapping(mut self, wrapping: Wrapping) -> Self { + self.wrapping = wrapping; + self + } + + /// Highlights the [`TextEditor`] using the given syntax and theme. + #[cfg(feature = "highlighter")] + pub fn highlight( + self, + syntax: &str, + theme: iced_highlighter::Theme, + ) -> TextEditor<'a, iced_highlighter::Highlighter, Message, Theme, Renderer> + where + Renderer: text::Renderer<Font = crate::core::Font>, + { + self.highlight_with::<iced_highlighter::Highlighter>( + iced_highlighter::Settings { + theme, + token: syntax.to_owned(), + }, + |highlight, _theme| highlight.to_format(), + ) + } + /// Highlights the [`TextEditor`] with the given [`Highlighter`] and /// a strategy to turn its highlights into some text format. - pub fn highlight<H: text::Highlighter>( + pub fn highlight_with<H: text::Highlighter>( self, settings: H::Settings, to_format: fn( @@ -143,19 +187,33 @@ where ) -> TextEditor<'a, H, Message, Theme, Renderer> { TextEditor { content: self.content, + placeholder: self.placeholder, font: self.font, text_size: self.text_size, line_height: self.line_height, width: self.width, height: self.height, padding: self.padding, + wrapping: self.wrapping, class: self.class, + key_binding: self.key_binding, on_edit: self.on_edit, highlighter_settings: settings, highlighter_format: to_format, } } + /// Sets the closure to produce key bindings on key presses. + /// + /// See [`Binding`] for the list of available bindings. + pub fn key_binding( + mut self, + key_binding: impl Fn(KeyPress) -> Option<Binding<Message>> + 'a, + ) -> Self { + self.key_binding = Some(Box::new(key_binding)); + self + } + /// Sets the style of the [`TextEditor`]. #[must_use] pub fn style(mut self, style: impl Fn(&Theme, Status) -> Style + 'a) -> Self @@ -322,7 +380,7 @@ where /// The state of a [`TextEditor`]. #[derive(Debug)] pub struct State<Highlighter: text::Highlighter> { - is_focused: bool, + focus: Option<Focus>, last_click: Option<mouse::Click>, drag_click: Option<mouse::click::Kind>, partial_scroll: f32, @@ -331,10 +389,55 @@ pub struct State<Highlighter: text::Highlighter> { highlighter_format_address: usize, } +#[derive(Debug, Clone, Copy)] +struct Focus { + updated_at: Instant, + now: Instant, + is_window_focused: bool, +} + +impl Focus { + const CURSOR_BLINK_INTERVAL_MILLIS: u128 = 500; + + fn now() -> Self { + let now = Instant::now(); + + Self { + updated_at: now, + now, + is_window_focused: true, + } + } + + fn is_cursor_visible(&self) -> bool { + self.is_window_focused + && ((self.now - self.updated_at).as_millis() + / Self::CURSOR_BLINK_INTERVAL_MILLIS) + % 2 + == 0 + } +} + impl<Highlighter: text::Highlighter> State<Highlighter> { /// Returns whether the [`TextEditor`] is currently focused or not. pub fn is_focused(&self) -> bool { - self.is_focused + self.focus.is_some() + } +} + +impl<Highlighter: text::Highlighter> operation::Focusable + for State<Highlighter> +{ + fn is_focused(&self) -> bool { + self.focus.is_some() + } + + fn focus(&mut self) { + self.focus = Some(Focus::now()); + } + + fn unfocus(&mut self) { + self.focus = None; } } @@ -351,7 +454,7 @@ where fn state(&self) -> widget::tree::State { widget::tree::State::new(State { - is_focused: false, + focus: None, last_click: None, drag_click: None, partial_scroll: 0.0, @@ -402,6 +505,7 @@ where self.font.unwrap_or_else(|| renderer.default_font()), self.text_size.unwrap_or_else(|| renderer.default_size()), self.line_height, + self.wrapping, state.highlighter.borrow_mut().deref_mut(), ); @@ -439,12 +543,48 @@ where let state = tree.state.downcast_mut::<State<Highlighter>>(); + match event { + Event::Window(window::Event::Unfocused) => { + if let Some(focus) = &mut state.focus { + focus.is_window_focused = false; + } + } + Event::Window(window::Event::Focused) => { + if let Some(focus) = &mut state.focus { + focus.is_window_focused = true; + focus.updated_at = Instant::now(); + + shell.request_redraw(window::RedrawRequest::NextFrame); + } + } + Event::Window(window::Event::RedrawRequested(now)) => { + if let Some(focus) = &mut state.focus { + if focus.is_window_focused { + focus.now = now; + + let millis_until_redraw = + Focus::CURSOR_BLINK_INTERVAL_MILLIS + - (now - focus.updated_at).as_millis() + % Focus::CURSOR_BLINK_INTERVAL_MILLIS; + + shell.request_redraw(window::RedrawRequest::At( + now + Duration::from_millis( + millis_until_redraw as u64, + ), + )); + } + } + } + _ => {} + } + let Some(update) = Update::from_event( event, state, layout.bounds(), self.padding, cursor, + self.key_binding.as_deref(), ) else { return event::Status::Ignored; }; @@ -459,12 +599,18 @@ where mouse::click::Kind::Triple => Action::SelectLine, }; - state.is_focused = true; + state.focus = Some(Focus::now()); state.last_click = Some(click); state.drag_click = Some(click.kind()); shell.publish(on_edit(action)); } + Update::Drag(position) => { + shell.publish(on_edit(Action::Drag(position))); + } + Update::Release => { + state.drag_click = None; + } Update::Scroll(lines) => { let bounds = self.content.0.borrow().editor.bounds(); @@ -479,34 +625,105 @@ where lines: lines as i32, })); } - Update::Unfocus => { - state.is_focused = false; - state.drag_click = None; - } - Update::Release => { - state.drag_click = None; - } - Update::Action(action) => { - shell.publish(on_edit(action)); - } - Update::Copy => { - if let Some(selection) = self.content.selection() { - clipboard.write(clipboard::Kind::Standard, selection); - } - } - Update::Cut => { - if let Some(selection) = self.content.selection() { - clipboard.write(clipboard::Kind::Standard, selection); - shell.publish(on_edit(Action::Edit(Edit::Delete))); + Update::Binding(binding) => { + fn apply_binding< + H: text::Highlighter, + R: text::Renderer, + Message, + >( + binding: Binding<Message>, + content: &Content<R>, + state: &mut State<H>, + on_edit: &dyn Fn(Action) -> Message, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + ) { + let mut publish = |action| shell.publish(on_edit(action)); + + match binding { + Binding::Unfocus => { + state.focus = None; + state.drag_click = None; + } + Binding::Copy => { + if let Some(selection) = content.selection() { + clipboard.write( + clipboard::Kind::Standard, + selection, + ); + } + } + Binding::Cut => { + if let Some(selection) = content.selection() { + clipboard.write( + clipboard::Kind::Standard, + selection, + ); + + publish(Action::Edit(Edit::Delete)); + } + } + Binding::Paste => { + if let Some(contents) = + clipboard.read(clipboard::Kind::Standard) + { + publish(Action::Edit(Edit::Paste(Arc::new( + contents, + )))); + } + } + Binding::Move(motion) => { + publish(Action::Move(motion)); + } + Binding::Select(motion) => { + publish(Action::Select(motion)); + } + Binding::SelectWord => { + publish(Action::SelectWord); + } + Binding::SelectLine => { + publish(Action::SelectLine); + } + Binding::SelectAll => { + publish(Action::SelectAll); + } + Binding::Insert(c) => { + publish(Action::Edit(Edit::Insert(c))); + } + Binding::Enter => { + publish(Action::Edit(Edit::Enter)); + } + Binding::Backspace => { + publish(Action::Edit(Edit::Backspace)); + } + Binding::Delete => { + publish(Action::Edit(Edit::Delete)); + } + Binding::Sequence(sequence) => { + for binding in sequence { + apply_binding( + binding, content, state, on_edit, + clipboard, shell, + ); + } + } + Binding::Custom(message) => { + shell.publish(message); + } + } } - } - Update::Paste => { - if let Some(contents) = - clipboard.read(clipboard::Kind::Standard) - { - shell.publish(on_edit(Action::Edit(Edit::Paste( - Arc::new(contents), - )))); + + apply_binding( + binding, + self.content, + state, + on_edit, + clipboard, + shell, + ); + + if let Some(focus) = &mut state.focus { + focus.updated_at = Instant::now(); } } } @@ -522,15 +739,17 @@ where defaults: &renderer::Style, layout: Layout<'_>, cursor: mouse::Cursor, - viewport: &Rectangle, + _viewport: &Rectangle, ) { let bounds = layout.bounds(); let mut internal = self.content.0.borrow_mut(); let state = tree.state.downcast_ref::<State<Highlighter>>(); + let font = self.font.unwrap_or_else(|| renderer.default_font()); + internal.editor.highlight( - self.font.unwrap_or_else(|| renderer.default_font()), + font, state.highlighter.borrow_mut().deref_mut(), |highlight| (self.highlighter_format)(highlight, theme), ); @@ -540,7 +759,7 @@ where let status = if is_disabled { Status::Disabled - } else if state.is_focused { + } else if state.focus.is_some() { Status::Focused } else if is_mouse_over { Status::Hovered @@ -559,22 +778,43 @@ where style.background, ); - renderer.fill_editor( - &internal.editor, - bounds.position() - + Vector::new(self.padding.left, self.padding.top), - defaults.text_color, - *viewport, - ); + let text_bounds = bounds.shrink(self.padding); + + if internal.editor.is_empty() { + if let Some(placeholder) = self.placeholder.clone() { + renderer.fill_text( + Text { + content: placeholder.into_owned(), + bounds: text_bounds.size(), + size: self + .text_size + .unwrap_or_else(|| renderer.default_size()), + line_height: self.line_height, + font, + horizontal_alignment: alignment::Horizontal::Left, + vertical_alignment: alignment::Vertical::Top, + shaping: text::Shaping::Advanced, + wrapping: self.wrapping, + }, + text_bounds.position(), + style.placeholder, + text_bounds, + ); + } + } else { + renderer.fill_editor( + &internal.editor, + text_bounds.position(), + defaults.text_color, + text_bounds, + ); + } - let translation = Vector::new( - bounds.x + self.padding.left, - bounds.y + self.padding.top, - ); + let translation = text_bounds.position() - Point::ORIGIN; - if state.is_focused { + if let Some(focus) = state.focus.as_ref() { match internal.editor.cursor() { - Cursor::Caret(position) => { + Cursor::Caret(position) if focus.is_cursor_visible() => { let cursor = Rectangle::new( position + translation, @@ -588,15 +828,12 @@ where ), ); - if let Some(clipped_cursor) = bounds.intersection(&cursor) { + if let Some(clipped_cursor) = + text_bounds.intersection(&cursor) + { renderer.fill_quad( renderer::Quad { - bounds: Rectangle { - x: clipped_cursor.x.floor(), - y: clipped_cursor.y, - width: clipped_cursor.width, - height: clipped_cursor.height, - }, + bounds: clipped_cursor, ..renderer::Quad::default() }, style.value, @@ -605,7 +842,7 @@ where } Cursor::Selection(ranges) => { for range in ranges.into_iter().filter_map(|range| { - bounds.intersection(&(range + translation)) + text_bounds.intersection(&(range + translation)) }) { renderer.fill_quad( renderer::Quad { @@ -616,6 +853,7 @@ where ); } } + Cursor::Caret(_) => {} } } } @@ -640,6 +878,18 @@ where mouse::Interaction::default() } } + + fn operate( + &self, + tree: &mut widget::Tree, + _layout: Layout<'_>, + _renderer: &Renderer, + operation: &mut dyn widget::Operation, + ) { + let state = tree.state.downcast_mut::<State<Highlighter>>(); + + operation.focusable(state, None); + } } impl<'a, Highlighter, Message, Theme, Renderer> @@ -658,27 +908,144 @@ where } } -enum Update { - Click(mouse::Click), - Scroll(f32), +/// A binding to an action in the [`TextEditor`]. +#[derive(Debug, Clone, PartialEq)] +pub enum Binding<Message> { + /// Unfocus the [`TextEditor`]. Unfocus, - Release, - Action(Action), + /// Copy the selection of the [`TextEditor`]. Copy, + /// Cut the selection of the [`TextEditor`]. Cut, + /// Paste the clipboard contents in the [`TextEditor`]. Paste, + /// Apply a [`Motion`]. + Move(Motion), + /// Select text with a given [`Motion`]. + Select(Motion), + /// Select the word at the current cursor. + SelectWord, + /// Select the line at the current cursor. + SelectLine, + /// Select the entire buffer. + SelectAll, + /// Insert the given character. + Insert(char), + /// Break the current line. + Enter, + /// Delete the previous character. + Backspace, + /// Delete the next character. + Delete, + /// A sequence of bindings to execute. + Sequence(Vec<Self>), + /// Produce the given message. + Custom(Message), +} + +/// A key press. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct KeyPress { + /// The key pressed. + pub key: keyboard::Key, + /// The state of the keyboard modifiers. + pub modifiers: keyboard::Modifiers, + /// The text produced by the key press. + pub text: Option<SmolStr>, + /// The current [`Status`] of the [`TextEditor`]. + pub status: Status, +} + +impl<Message> Binding<Message> { + /// Returns the default [`Binding`] for the given key press. + pub fn from_key_press(event: KeyPress) -> Option<Self> { + let KeyPress { + key, + modifiers, + text, + status, + } = event; + + if status != Status::Focused { + return None; + } + + match key.as_ref() { + keyboard::Key::Named(key::Named::Enter) => Some(Self::Enter), + keyboard::Key::Named(key::Named::Backspace) => { + Some(Self::Backspace) + } + keyboard::Key::Named(key::Named::Delete) => Some(Self::Delete), + keyboard::Key::Named(key::Named::Escape) => Some(Self::Unfocus), + keyboard::Key::Character("c") if modifiers.command() => { + Some(Self::Copy) + } + keyboard::Key::Character("x") if modifiers.command() => { + Some(Self::Cut) + } + keyboard::Key::Character("v") + if modifiers.command() && !modifiers.alt() => + { + Some(Self::Paste) + } + keyboard::Key::Character("a") if modifiers.command() => { + Some(Self::SelectAll) + } + _ => { + if let Some(text) = text { + let c = text.chars().find(|c| !c.is_control())?; + + Some(Self::Insert(c)) + } else if let keyboard::Key::Named(named_key) = key.as_ref() { + let motion = motion(named_key)?; + + let motion = if modifiers.macos_command() { + match motion { + Motion::Left => Motion::Home, + Motion::Right => Motion::End, + _ => motion, + } + } else { + motion + }; + + let motion = if modifiers.jump() { + motion.widen() + } else { + motion + }; + + Some(if modifiers.shift() { + Self::Select(motion) + } else { + Self::Move(motion) + }) + } else { + None + } + } + } + } +} + +enum Update<Message> { + Click(mouse::Click), + Drag(Point), + Release, + Scroll(f32), + Binding(Binding<Message>), } -impl Update { +impl<Message> Update<Message> { fn from_event<H: Highlighter>( event: Event, state: &State<H>, bounds: Rectangle, padding: Padding, cursor: mouse::Cursor, + key_binding: Option<&dyn Fn(KeyPress) -> Option<Binding<Message>>>, ) -> Option<Self> { - let action = |action| Some(Update::Action(action)); - let edit = |edit| action(Action::Edit(edit)); + let binding = |binding| Some(Update::Binding(binding)); match event { Event::Mouse(event) => match event { @@ -689,12 +1056,13 @@ impl Update { let click = mouse::Click::new( cursor_position, + mouse::Button::Left, state.last_click, ); Some(Update::Click(click)) - } else if state.is_focused { - Some(Update::Unfocus) + } else if state.focus.is_some() { + binding(Binding::Unfocus) } else { None } @@ -707,7 +1075,7 @@ impl Update { let cursor_position = cursor.position_in(bounds)? - Vector::new(padding.top, padding.left); - action(Action::Drag(cursor_position)) + Some(Update::Drag(cursor_position)) } _ => None, }, @@ -727,81 +1095,32 @@ impl Update { } _ => None, }, - Event::Keyboard(event) => match event { - keyboard::Event::KeyPressed { + Event::Keyboard(keyboard::Event::KeyPressed { + key, + modifiers, + text, + .. + }) => { + let status = if state.focus.is_some() { + Status::Focused + } else { + Status::Active + }; + + let key_press = KeyPress { key, modifiers, text, - .. - } if state.is_focused => { - match key.as_ref() { - keyboard::Key::Named(key::Named::Enter) => { - return edit(Edit::Enter); - } - keyboard::Key::Named(key::Named::Backspace) => { - return edit(Edit::Backspace); - } - keyboard::Key::Named(key::Named::Delete) => { - return edit(Edit::Delete); - } - keyboard::Key::Named(key::Named::Escape) => { - return Some(Self::Unfocus); - } - keyboard::Key::Character("c") - if modifiers.command() => - { - return Some(Self::Copy); - } - keyboard::Key::Character("x") - if modifiers.command() => - { - return Some(Self::Cut); - } - keyboard::Key::Character("v") - if modifiers.command() && !modifiers.alt() => - { - return Some(Self::Paste); - } - _ => {} - } - - if let Some(text) = text { - if let Some(c) = text.chars().find(|c| !c.is_control()) - { - return edit(Edit::Insert(c)); - } - } - - if let keyboard::Key::Named(named_key) = key.as_ref() { - if let Some(motion) = motion(named_key) { - let motion = if modifiers.macos_command() { - match motion { - Motion::Left => Motion::Home, - Motion::Right => Motion::End, - _ => motion, - } - } else { - motion - }; - - let motion = if modifiers.jump() { - motion.widen() - } else { - motion - }; - - return action(if modifiers.shift() { - Action::Select(motion) - } else { - Action::Move(motion) - }); - } - } + status, + }; - None + if let Some(key_binding) = key_binding { + key_binding(key_press) + } else { + Binding::from_key_press(key_press) } - _ => None, - }, + .map(Self::Binding) + } _ => None, } } diff --git a/widget/src/text_input.rs b/widget/src/text_input.rs index 4e89236b..d5ede524 100644 --- a/widget/src/text_input.rs +++ b/widget/src/text_input.rs @@ -19,7 +19,8 @@ use crate::core::keyboard::key; use crate::core::layout; use crate::core::mouse::{self, click}; use crate::core::renderer; -use crate::core::text::{self, Paragraph as _, Text}; +use crate::core::text::paragraph::{self, Paragraph as _}; +use crate::core::text::{self, Text}; use crate::core::time::{Duration, Instant}; use crate::core::touch; use crate::core::widget; @@ -30,7 +31,8 @@ use crate::core::{ Background, Border, Color, Element, Layout, Length, Padding, Pixels, Point, Rectangle, Shell, Size, Theme, Vector, Widget, }; -use crate::runtime::{Action, Task}; +use crate::runtime::task::{self, Task}; +use crate::runtime::Action; /// A field that can be filled with text. /// @@ -72,6 +74,7 @@ pub struct TextInput< padding: Padding, size: Option<Pixels>, line_height: text::LineHeight, + alignment: alignment::Horizontal, on_input: Option<Box<dyn Fn(String) -> Message + 'a>>, on_paste: Option<Box<dyn Fn(String) -> Message + 'a>>, on_submit: Option<Message>, @@ -101,6 +104,7 @@ where padding: DEFAULT_PADDING, size: None, line_height: text::LineHeight::default(), + alignment: alignment::Horizontal::Left, on_input: None, on_paste: None, on_submit: None, @@ -125,11 +129,23 @@ where /// the [`TextInput`]. /// /// If this method is not called, the [`TextInput`] will be disabled. - pub fn on_input<F>(mut self, callback: F) -> Self - where - F: 'a + Fn(String) -> Message, - { - self.on_input = Some(Box::new(callback)); + pub fn on_input( + mut self, + on_input: impl Fn(String) -> Message + 'a, + ) -> Self { + self.on_input = Some(Box::new(on_input)); + self + } + + /// Sets the message that should be produced when some text is typed into + /// the [`TextInput`], if `Some`. + /// + /// If `None`, the [`TextInput`] will be disabled. + pub fn on_input_maybe( + mut self, + on_input: Option<impl Fn(String) -> Message + 'a>, + ) -> Self { + self.on_input = on_input.map(|f| Box::new(f) as _); self } @@ -140,6 +156,13 @@ where self } + /// Sets the message that should be produced when the [`TextInput`] is + /// focused and the enter key is pressed, if `Some`. + pub fn on_submit_maybe(mut self, on_submit: Option<Message>) -> Self { + self.on_submit = on_submit; + self + } + /// Sets the message that should be produced when some text is pasted into /// the [`TextInput`]. pub fn on_paste( @@ -150,6 +173,16 @@ where self } + /// Sets the message that should be produced when some text is pasted into + /// the [`TextInput`], if `Some`. + pub fn on_paste_maybe( + mut self, + on_paste: Option<impl Fn(String) -> Message + 'a>, + ) -> Self { + self.on_paste = on_paste.map(|f| Box::new(f) as _); + self + } + /// Sets the [`Font`] of the [`TextInput`]. /// /// [`Font`]: text::Renderer::Font @@ -191,6 +224,15 @@ where self } + /// Sets the horizontal alignment of the [`TextInput`]. + pub fn align_x( + mut self, + alignment: impl Into<alignment::Horizontal>, + ) -> Self { + self.alignment = alignment.into(); + self + } + /// Sets the style of the [`TextInput`]. #[must_use] pub fn style(mut self, style: impl Fn(&Theme, Status) -> Style + 'a) -> Self @@ -238,6 +280,7 @@ where horizontal_alignment: alignment::Horizontal::Left, vertical_alignment: alignment::Vertical::Center, shaping: text::Shaping::Advanced, + wrapping: text::Wrapping::default(), }; state.placeholder.update(placeholder_text); @@ -262,6 +305,7 @@ where horizontal_alignment: alignment::Horizontal::Center, vertical_alignment: alignment::Vertical::Center, shaping: text::Shaping::Advanced, + wrapping: text::Wrapping::default(), }; state.icon.update(icon_text); @@ -359,7 +403,7 @@ where let icon_layout = children_layout.next().unwrap(); renderer.fill_paragraph( - &state.icon, + state.icon.raw(), icon_layout.bounds().center(), style.icon, *viewport, @@ -377,16 +421,16 @@ where cursor::State::Index(position) => { let (text_value_width, offset) = measure_cursor_and_scroll_offset( - &state.value, + state.value.raw(), text_bounds, position, ); - let is_cursor_visible = ((focus.now - focus.updated_at) - .as_millis() - / CURSOR_BLINK_INTERVAL_MILLIS) - % 2 - == 0; + let is_cursor_visible = !is_disabled + && ((focus.now - focus.updated_at).as_millis() + / CURSOR_BLINK_INTERVAL_MILLIS) + % 2 + == 0; let cursor = if is_cursor_visible { Some(( @@ -414,14 +458,14 @@ where let (left_position, left_offset) = measure_cursor_and_scroll_offset( - &state.value, + state.value.raw(), text_bounds, left, ); let (right_position, right_offset) = measure_cursor_and_scroll_offset( - &state.value, + state.value.raw(), text_bounds, right, ); @@ -455,9 +499,21 @@ where }; let draw = |renderer: &mut Renderer, viewport| { + let paragraph = if text.is_empty() { + state.placeholder.raw() + } else { + state.value.raw() + }; + + let alignment_offset = alignment_offset( + text_bounds.width, + paragraph.min_width(), + self.alignment, + ); + if let Some((cursor, color)) = cursor { renderer.with_translation( - Vector::new(-offset, 0.0), + Vector::new(alignment_offset - offset, 0.0), |renderer| { renderer.fill_quad(cursor, color); }, @@ -467,13 +523,9 @@ where } renderer.fill_paragraph( - if text.is_empty() { - &state.placeholder - } else { - &state.value - }, + paragraph, Point::new(text_bounds.x, text_bounds.center_y()) - - Vector::new(offset, 0.0), + + Vector::new(alignment_offset - offset, 0.0), if text.is_empty() { style.placeholder } else { @@ -510,12 +562,9 @@ where fn diff(&self, tree: &mut Tree) { let state = tree.state.downcast_mut::<State<Renderer::Paragraph>>(); - // Unfocus text input if it becomes disabled + // Stop pasting if input becomes disabled if self.on_input.is_none() { - state.last_click = None; - state.is_focused = None; state.is_pasting = None; - state.is_dragging = false; } } @@ -540,7 +589,7 @@ where tree: &mut Tree, _layout: Layout<'_>, _renderer: &Renderer, - operation: &mut dyn Operation<()>, + operation: &mut dyn Operation, ) { let state = tree.state.downcast_mut::<State<Renderer::Paragraph>>(); @@ -576,11 +625,7 @@ where | Event::Touch(touch::Event::FingerPressed { .. }) => { let state = state::<Renderer>(tree); - let click_position = if self.on_input.is_some() { - cursor.position_over(layout.bounds()) - } else { - None - }; + let click_position = cursor.position_over(layout.bounds()); state.is_focused = if click_position.is_some() { state.is_focused.or_else(|| { @@ -598,10 +643,24 @@ where if let Some(cursor_position) = click_position { let text_layout = layout.children().next().unwrap(); - let target = cursor_position.x - text_layout.bounds().x; - let click = - mouse::Click::new(cursor_position, state.last_click); + let target = { + let text_bounds = text_layout.bounds(); + + let alignment_offset = alignment_offset( + text_bounds.width, + state.value.raw().min_width(), + self.alignment, + ); + + cursor_position.x - text_bounds.x - alignment_offset + }; + + let click = mouse::Click::new( + cursor_position, + mouse::Button::Left, + state.last_click, + ); match click.kind() { click::Kind::Single => { @@ -675,7 +734,18 @@ where if state.is_dragging { let text_layout = layout.children().next().unwrap(); - let target = position.x - text_layout.bounds().x; + + let target = { + let text_bounds = text_layout.bounds(); + + let alignment_offset = alignment_offset( + text_bounds.width, + state.value.raw().min_width(), + self.alignment, + ); + + position.x - text_bounds.x - alignment_offset + }; let value = if self.is_secure { self.value.secure() @@ -704,10 +774,6 @@ where let state = state::<Renderer>(tree); if let Some(focus) = &mut state.is_focused { - let Some(on_input) = &self.on_input else { - return event::Status::Ignored; - }; - let modifiers = state.keyboard_modifiers; focus.updated_at = Instant::now(); @@ -731,6 +797,10 @@ where if state.keyboard_modifiers.command() && !self.is_secure => { + let Some(on_input) = &self.on_input else { + return event::Status::Ignored; + }; + if let Some((start, end)) = state.cursor.selection(&self.value) { @@ -755,6 +825,10 @@ where if state.keyboard_modifiers.command() && !state.keyboard_modifiers.alt() => { + let Some(on_input) = &self.on_input else { + return event::Status::Ignored; + }; + let content = match state.is_pasting.take() { Some(content) => content, None => { @@ -798,6 +872,10 @@ where } if let Some(text) = text { + let Some(on_input) = &self.on_input else { + return event::Status::Ignored; + }; + state.is_pasting = None; if let Some(c) = @@ -826,6 +904,10 @@ where } } keyboard::Key::Named(key::Named::Backspace) => { + let Some(on_input) = &self.on_input else { + return event::Status::Ignored; + }; + if modifiers.jump() && state.cursor.selection(&self.value).is_none() { @@ -850,6 +932,10 @@ where update_cache(state, &self.value); } keyboard::Key::Named(key::Named::Delete) => { + let Some(on_input) = &self.on_input else { + return event::Status::Ignored; + }; + if modifiers.jump() && state.cursor.selection(&self.value).is_none() { @@ -1068,7 +1154,7 @@ where ) -> mouse::Interaction { if cursor.is_over(layout.bounds()) { if self.on_input.is_none() { - mouse::Interaction::NotAllowed + mouse::Interaction::Idle } else { mouse::Interaction::Text } @@ -1142,13 +1228,13 @@ impl From<Id> for widget::Id { /// Produces a [`Task`] that focuses the [`TextInput`] with the given [`Id`]. pub fn focus<T>(id: Id) -> Task<T> { - Task::effect(Action::widget(operation::focusable::focus(id.0))) + task::effect(Action::widget(operation::focusable::focus(id.0))) } /// Produces a [`Task`] that moves the cursor of the [`TextInput`] with the given [`Id`] to the /// end. pub fn move_cursor_to_end<T>(id: Id) -> Task<T> { - Task::effect(Action::widget(operation::text_input::move_cursor_to_end( + task::effect(Action::widget(operation::text_input::move_cursor_to_end( id.0, ))) } @@ -1156,7 +1242,7 @@ pub fn move_cursor_to_end<T>(id: Id) -> Task<T> { /// Produces a [`Task`] that moves the cursor of the [`TextInput`] with the given [`Id`] to the /// front. pub fn move_cursor_to_front<T>(id: Id) -> Task<T> { - Task::effect(Action::widget(operation::text_input::move_cursor_to_front( + task::effect(Action::widget(operation::text_input::move_cursor_to_front( id.0, ))) } @@ -1164,22 +1250,22 @@ pub fn move_cursor_to_front<T>(id: Id) -> Task<T> { /// Produces a [`Task`] that moves the cursor of the [`TextInput`] with the given [`Id`] to the /// provided position. pub fn move_cursor_to<T>(id: Id, position: usize) -> Task<T> { - Task::effect(Action::widget(operation::text_input::move_cursor_to( + task::effect(Action::widget(operation::text_input::move_cursor_to( id.0, position, ))) } /// Produces a [`Task`] that selects all the content of the [`TextInput`] with the given [`Id`]. pub fn select_all<T>(id: Id) -> Task<T> { - Task::effect(Action::widget(operation::text_input::select_all(id.0))) + task::effect(Action::widget(operation::text_input::select_all(id.0))) } /// The state of a [`TextInput`]. #[derive(Debug, Default, Clone)] pub struct State<P: text::Paragraph> { - value: P, - placeholder: P, - icon: P, + value: paragraph::Plain<P>, + placeholder: paragraph::Plain<P>, + icon: paragraph::Plain<P>, is_focused: Option<Focus>, is_dragging: bool, is_pasting: Option<Value>, @@ -1208,21 +1294,6 @@ impl<P: text::Paragraph> State<P> { Self::default() } - /// Creates a new [`State`], representing a focused [`TextInput`]. - pub fn focused() -> Self { - Self { - value: P::default(), - placeholder: P::default(), - icon: P::default(), - is_focused: None, - is_dragging: false, - is_pasting: None, - last_click: None, - cursor: Cursor::default(), - keyboard_modifiers: keyboard::Modifiers::default(), - } - } - /// Returns whether the [`TextInput`] is currently focused or not. pub fn is_focused(&self) -> bool { self.is_focused.is_some() @@ -1318,7 +1389,7 @@ fn offset<P: text::Paragraph>( }; let (_, offset) = measure_cursor_and_scroll_offset( - &state.value, + state.value.raw(), text_bounds, focus_position, ); @@ -1356,6 +1427,7 @@ fn find_cursor_position<P: text::Paragraph>( let char_offset = state .value + .raw() .hit_test(Point::new(x + offset, text_bounds.height / 2.0)) .map(text::Hit::cursor)?; @@ -1385,7 +1457,7 @@ fn replace_paragraph<Renderer>( let mut children_layout = layout.children(); let text_bounds = children_layout.next().unwrap().bounds(); - state.value = Renderer::Paragraph::with_text(Text { + state.value = paragraph::Plain::new(Text { font, line_height, content: &value.to_string(), @@ -1394,6 +1466,7 @@ fn replace_paragraph<Renderer>( horizontal_alignment: alignment::Horizontal::Left, vertical_alignment: alignment::Vertical::Top, shaping: text::Shaping::Advanced, + wrapping: text::Wrapping::default(), }); } @@ -1498,3 +1571,21 @@ pub fn default(theme: &Theme, status: Status) -> Style { }, } } + +fn alignment_offset( + text_bounds_width: f32, + text_min_width: f32, + alignment: alignment::Horizontal, +) -> f32 { + if text_min_width > text_bounds_width { + 0.0 + } else { + match alignment { + alignment::Horizontal::Left => 0.0, + alignment::Horizontal::Center => { + (text_bounds_width - text_min_width) / 2.0 + } + alignment::Horizontal::Right => text_bounds_width - text_min_width, + } + } +} diff --git a/widget/src/themer.rs b/widget/src/themer.rs index 9eb47d84..499a9fe8 100644 --- a/widget/src/themer.rs +++ b/widget/src/themer.rs @@ -104,7 +104,7 @@ where tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn Operation<()>, + operation: &mut dyn Operation, ) { self.content .as_widget() @@ -236,7 +236,7 @@ where &mut self, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn Operation<()>, + operation: &mut dyn Operation, ) { self.content.operate(layout, renderer, operation); } diff --git a/widget/src/toggler.rs b/widget/src/toggler.rs index ca6e37b0..1c425dc1 100644 --- a/widget/src/toggler.rs +++ b/widget/src/toggler.rs @@ -26,7 +26,9 @@ use crate::core::{ /// /// let is_toggled = true; /// -/// Toggler::new(String::from("Toggle me!"), is_toggled, |b| Message::TogglerToggled(b)); +/// Toggler::new(is_toggled) +/// .label("Toggle me!") +/// .on_toggle(Message::TogglerToggled); /// ``` #[allow(missing_debug_implementations)] pub struct Toggler< @@ -39,14 +41,15 @@ pub struct Toggler< Renderer: text::Renderer, { is_toggled: bool, - on_toggle: Box<dyn Fn(bool) -> Message + 'a>, - label: Option<String>, + on_toggle: Option<Box<dyn Fn(bool) -> Message + 'a>>, + label: Option<text::Fragment<'a>>, width: Length, size: f32, text_size: Option<Pixels>, text_line_height: text::LineHeight, text_alignment: alignment::Horizontal, text_shaping: text::Shaping, + text_wrapping: text::Wrapping, spacing: f32, font: Option<Renderer::Font>, class: Theme::Class<'a>, @@ -68,30 +71,54 @@ where /// * a function that will be called when the [`Toggler`] is toggled. It /// will receive the new state of the [`Toggler`] and must produce a /// `Message`. - pub fn new<F>( - label: impl Into<Option<String>>, - is_toggled: bool, - f: F, - ) -> Self - where - F: 'a + Fn(bool) -> Message, - { + pub fn new(is_toggled: bool) -> Self { Toggler { is_toggled, - on_toggle: Box::new(f), - label: label.into(), - width: Length::Fill, + on_toggle: None, + label: None, + width: Length::Shrink, size: Self::DEFAULT_SIZE, text_size: None, text_line_height: text::LineHeight::default(), text_alignment: alignment::Horizontal::Left, - text_shaping: text::Shaping::Basic, + text_shaping: text::Shaping::default(), + text_wrapping: text::Wrapping::default(), spacing: Self::DEFAULT_SIZE / 2.0, font: None, class: Theme::default(), } } + /// Sets the label of the [`Toggler`]. + pub fn label(mut self, label: impl text::IntoFragment<'a>) -> Self { + self.label = Some(label.into_fragment()); + self + } + + /// Sets the message that should be produced when a user toggles + /// the [`Toggler`]. + /// + /// If this method is not called, the [`Toggler`] will be disabled. + pub fn on_toggle( + mut self, + on_toggle: impl Fn(bool) -> Message + 'a, + ) -> Self { + self.on_toggle = Some(Box::new(on_toggle)); + self + } + + /// Sets the message that should be produced when a user toggles + /// the [`Toggler`], if `Some`. + /// + /// If `None`, the [`Toggler`] will be disabled. + pub fn on_toggle_maybe( + mut self, + on_toggle: Option<impl Fn(bool) -> Message + 'a>, + ) -> Self { + self.on_toggle = on_toggle.map(|on_toggle| Box::new(on_toggle) as _); + self + } + /// Sets the size of the [`Toggler`]. pub fn size(mut self, size: impl Into<Pixels>) -> Self { self.size = size.into().0; @@ -131,6 +158,12 @@ where self } + /// Sets the [`text::Wrapping`] strategy of the [`Toggler`]. + pub fn text_wrapping(mut self, wrapping: text::Wrapping) -> Self { + self.text_wrapping = wrapping; + self + } + /// Sets the spacing between the [`Toggler`] and the text. pub fn spacing(mut self, spacing: impl Into<Pixels>) -> Self { self.spacing = spacing.into().0; @@ -216,6 +249,7 @@ where self.text_alignment, alignment::Vertical::Top, self.text_shaping, + self.text_wrapping, ) } else { layout::Node::new(Size::ZERO) @@ -235,13 +269,17 @@ where shell: &mut Shell<'_, Message>, _viewport: &Rectangle, ) -> event::Status { + let Some(on_toggle) = &self.on_toggle else { + return event::Status::Ignored; + }; + match event { Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) | Event::Touch(touch::Event::FingerPressed { .. }) => { let mouse_over = cursor.is_over(layout.bounds()); if mouse_over { - shell.publish((self.on_toggle)(!self.is_toggled)); + shell.publish(on_toggle(!self.is_toggled)); event::Status::Captured } else { @@ -261,7 +299,11 @@ where _renderer: &Renderer, ) -> mouse::Interaction { if cursor.is_over(layout.bounds()) { - mouse::Interaction::Pointer + if self.on_toggle.is_some() { + mouse::Interaction::Pointer + } else { + mouse::Interaction::NotAllowed + } } else { mouse::Interaction::default() } @@ -289,12 +331,14 @@ where if self.label.is_some() { let label_layout = children.next().unwrap(); + let state: &widget::text::State<Renderer::Paragraph> = + tree.state.downcast_ref(); crate::text::draw( renderer, style, label_layout, - tree.state.downcast_ref(), + state.0.raw(), crate::text::Style::default(), viewport, ); @@ -303,7 +347,9 @@ where let bounds = toggler_layout.bounds(); let is_mouse_over = cursor.is_over(layout.bounds()); - let status = if is_mouse_over { + let status = if self.on_toggle.is_none() { + Status::Disabled + } else if is_mouse_over { Status::Hovered { is_toggled: self.is_toggled, } @@ -392,6 +438,8 @@ pub enum Status { /// Indicates whether the [`Toggler`] is toggled. is_toggled: bool, }, + /// The [`Toggler`] is disabled. + Disabled, } /// The appearance of a toggler. @@ -452,6 +500,7 @@ pub fn default(theme: &Theme, status: Status) -> Style { palette.background.strong.color } } + Status::Disabled => palette.background.weak.color, }; let foreground = match status { @@ -472,6 +521,7 @@ pub fn default(theme: &Theme, status: Status) -> Style { palette.background.weak.color } } + Status::Disabled => palette.background.base.color, }; Style { diff --git a/widget/src/vertical_slider.rs b/widget/src/vertical_slider.rs index defb442f..03ec374c 100644 --- a/widget/src/vertical_slider.rs +++ b/widget/src/vertical_slider.rs @@ -5,6 +5,7 @@ pub use crate::slider::{ default, Catalog, Handle, HandleShape, Status, Style, StyleFn, }; +use crate::core::border::Border; use crate::core::event::{self, Event}; use crate::core::keyboard; use crate::core::keyboard::key::{self, Key}; @@ -14,8 +15,8 @@ use crate::core::renderer; use crate::core::touch; use crate::core::widget::tree::{self, Tree}; use crate::core::{ - self, Border, Clipboard, Element, Length, Pixels, Point, Rectangle, Shell, - Size, Widget, + self, Clipboard, Element, Length, Pixels, Point, Rectangle, Shell, Size, + Widget, }; /// An vertical bar and a handle that selects a single value from a range of @@ -71,8 +72,8 @@ where /// * an inclusive range of possible values /// * the current value of the [`VerticalSlider`] /// * a function that will be called when the [`VerticalSlider`] is dragged. - /// It receives the new value of the [`VerticalSlider`] and must produce a - /// `Message`. + /// It receives the new value of the [`VerticalSlider`] and must produce a + /// `Message`. pub fn new<F>(range: RangeInclusive<T>, value: T, on_change: F) -> Self where F: 'a + Fn(T) -> Message, @@ -239,7 +240,7 @@ where let steps = (percent * (end - start) / step).round(); let value = steps * step + start; - T::from_f64(value) + T::from_f64(value.min(end)) }; new_value @@ -412,10 +413,10 @@ where width: style.rail.width, height: offset + handle_width / 2.0, }, - border: Border::rounded(style.rail.border_radius), + border: style.rail.border, ..renderer::Quad::default() }, - style.rail.colors.1, + style.rail.backgrounds.1, ); renderer.fill_quad( @@ -426,10 +427,10 @@ where width: style.rail.width, height: bounds.height - offset - handle_width / 2.0, }, - border: Border::rounded(style.rail.border_radius), + border: style.rail.border, ..renderer::Quad::default() }, - style.rail.colors.0, + style.rail.backgrounds.0, ); renderer.fill_quad( @@ -447,7 +448,7 @@ where }, ..renderer::Quad::default() }, - style.handle.color, + style.handle.background, ); } diff --git a/winit/Cargo.toml b/winit/Cargo.toml index 68368aa1..f5a47952 100644 --- a/winit/Cargo.toml +++ b/winit/Cargo.toml @@ -33,7 +33,6 @@ log.workspace = true rustc-hash.workspace = true thiserror.workspace = true tracing.workspace = true -wasm-bindgen-futures.workspace = true window_clipboard.workspace = true winit.workspace = true @@ -46,4 +45,4 @@ winapi.workspace = true [target.'cfg(target_arch = "wasm32")'.dependencies] web-sys.workspace = true web-sys.features = ["Document", "Window"] - +wasm-bindgen-futures.workspace = true diff --git a/winit/src/clipboard.rs b/winit/src/clipboard.rs index 5237ca01..d54a1fe0 100644 --- a/winit/src/clipboard.rs +++ b/winit/src/clipboard.rs @@ -1,6 +1,8 @@ //! Access the clipboard. use crate::core::clipboard::Kind; +use std::sync::Arc; +use winit::window::{Window, WindowId}; /// A buffer for short-term storage and transfer within and between /// applications. @@ -10,18 +12,33 @@ pub struct Clipboard { } enum State { - Connected(window_clipboard::Clipboard), + Connected { + clipboard: window_clipboard::Clipboard, + // Held until drop to satisfy the safety invariants of + // `window_clipboard::Clipboard`. + // + // Note that the field ordering is load-bearing. + #[allow(dead_code)] + window: Arc<Window>, + }, Unavailable, } impl Clipboard { /// Creates a new [`Clipboard`] for the given window. - pub fn connect(window: &winit::window::Window) -> Clipboard { + pub fn connect(window: Arc<Window>) -> Clipboard { + // SAFETY: The window handle will stay alive throughout the entire + // lifetime of the `window_clipboard::Clipboard` because we hold + // the `Arc<Window>` together with `State`, and enum variant fields + // get dropped in declaration order. #[allow(unsafe_code)] - let state = unsafe { window_clipboard::Clipboard::connect(window) } - .ok() - .map(State::Connected) - .unwrap_or(State::Unavailable); + let clipboard = + unsafe { window_clipboard::Clipboard::connect(&window) }; + + let state = match clipboard { + Ok(clipboard) => State::Connected { clipboard, window }, + Err(_) => State::Unavailable, + }; Clipboard { state } } @@ -37,7 +54,7 @@ impl Clipboard { /// Reads the current content of the [`Clipboard`] as text. pub fn read(&self, kind: Kind) -> Option<String> { match &self.state { - State::Connected(clipboard) => match kind { + State::Connected { clipboard, .. } => match kind { Kind::Standard => clipboard.read().ok(), Kind::Primary => clipboard.read_primary().and_then(Result::ok), }, @@ -48,7 +65,7 @@ impl Clipboard { /// Writes the given text contents to the [`Clipboard`]. pub fn write(&mut self, kind: Kind, contents: String) { match &mut self.state { - State::Connected(clipboard) => { + State::Connected { clipboard, .. } => { let result = match kind { Kind::Standard => clipboard.write(contents), Kind::Primary => { @@ -66,6 +83,14 @@ impl Clipboard { State::Unavailable => {} } } + + /// Returns the identifier of the window used to create the [`Clipboard`], if any. + pub fn window_id(&self) -> Option<WindowId> { + match &self.state { + State::Connected { window, .. } => Some(window.id()), + State::Unavailable => None, + } + } } impl crate::core::Clipboard for Clipboard { diff --git a/winit/src/conversion.rs b/winit/src/conversion.rs index 0ed10c88..aaaca1a9 100644 --- a/winit/src/conversion.rs +++ b/winit/src/conversion.rs @@ -79,6 +79,10 @@ pub fn window_attributes( attributes = attributes .with_skip_taskbar(settings.platform_specific.skip_taskbar); + + attributes = attributes.with_undecorated_shadow( + settings.platform_specific.undecorated_shadow, + ); } #[cfg(target_os = "macos")] @@ -101,10 +105,14 @@ pub fn window_attributes( { use winit::platform::x11::WindowAttributesExtX11; - attributes = attributes.with_name( - &settings.platform_specific.application_id, - &settings.platform_specific.application_id, - ); + attributes = attributes + .with_override_redirect( + settings.platform_specific.override_redirect, + ) + .with_name( + &settings.platform_specific.application_id, + &settings.platform_specific.application_id, + ); } #[cfg(feature = "wayland")] { @@ -132,10 +140,10 @@ pub fn window_event( WindowEvent::Resized(new_size) => { let logical_size = new_size.to_logical(scale_factor); - Some(Event::Window(window::Event::Resized { + Some(Event::Window(window::Event::Resized(Size { width: logical_size.width, height: logical_size.height, - })) + }))) } WindowEvent::CloseRequested => { Some(Event::Window(window::Event::CloseRequested)) @@ -277,7 +285,7 @@ pub fn window_event( let winit::dpi::LogicalPosition { x, y } = position.to_logical(scale_factor); - Some(Event::Window(window::Event::Moved { x, y })) + Some(Event::Window(window::Event::Moved(Point::new(x, y)))) } _ => None, } diff --git a/winit/src/program.rs b/winit/src/program.rs index d55aedf1..52d8eb5f 100644 --- a/winit/src/program.rs +++ b/winit/src/program.rs @@ -202,12 +202,25 @@ where }; let (program, task) = runtime.enter(|| P::new(flags)); + let is_daemon = window_settings.is_none(); - if let Some(stream) = task.into_stream() { + let task = if let Some(window_settings) = window_settings { + let mut task = Some(task); + + let (_id, open) = runtime::window::open(window_settings); + + open.then(move |_| task.take().unwrap_or(Task::none())) + } else { + task + }; + + if let Some(stream) = runtime::task::into_stream(task) { runtime.run(stream); } - runtime.track(program.subscription().map(Action::Output).into_recipes()); + runtime.track(subscription::into_recipes( + program.subscription().map(Action::Output), + )); let (boot_sender, boot_receiver) = oneshot::channel(); let (event_sender, event_receiver) = mpsc::unbounded(); @@ -221,6 +234,7 @@ where boot_receiver, event_receiver, control_sender, + is_daemon, )); let context = task::Context::from_waker(task::noop_waker_ref()); @@ -229,7 +243,7 @@ where instance: std::pin::Pin<Box<F>>, context: task::Context<'static>, id: Option<String>, - boot: Option<BootConfig<Message, C>>, + boot: Option<BootConfig<C>>, sender: mpsc::UnboundedSender<Event<Action<Message>>>, receiver: mpsc::UnboundedReceiver<Control>, error: Option<Error>, @@ -240,11 +254,9 @@ where queued_events: Vec<Event<Action<Message>>>, } - struct BootConfig<Message: 'static, C> { - proxy: Proxy<Message>, + struct BootConfig<C> { sender: oneshot::Sender<Boot<C>>, fonts: Vec<Cow<'static, [u8]>>, - window_settings: Option<window::Settings>, graphics_settings: graphics::Settings, } @@ -253,10 +265,8 @@ where context, id: settings.id, boot: Some(BootConfig { - proxy, sender: boot_sender, fonts: settings.fonts, - window_settings, graphics_settings, }), sender: event_sender, @@ -278,10 +288,8 @@ where { fn resumed(&mut self, event_loop: &winit::event_loop::ActiveEventLoop) { let Some(BootConfig { - mut proxy, sender, fonts, - window_settings, graphics_settings, }) = self.boot.take() else { @@ -299,8 +307,6 @@ where } }; - let clipboard = Clipboard::connect(&window); - let finish_boot = async move { let mut compositor = C::new(graphics_settings, window.clone()).await?; @@ -310,27 +316,10 @@ where } sender - .send(Boot { - compositor, - clipboard, - window: window.id(), - is_daemon: window_settings.is_none(), - }) + .send(Boot { compositor }) .ok() .expect("Send boot event"); - if let Some(window_settings) = window_settings { - let (sender, _receiver) = oneshot::channel(); - - proxy.send_action(Action::Window( - runtime::window::Action::Open( - window::Id::unique(), - window_settings, - sender, - ), - )); - } - Ok::<_, graphics::Error>(()) }; @@ -420,6 +409,23 @@ where ); } + fn received_url( + &mut self, + event_loop: &winit::event_loop::ActiveEventLoop, + url: String, + ) { + self.process_event( + event_loop, + Event::EventLoopAwakened( + winit::event::Event::PlatformSpecific( + winit::event::PlatformSpecific::MacOS( + winit::event::MacOS::ReceivedUrl(url), + ), + ), + ), + ); + } + fn about_to_wait( &mut self, event_loop: &winit::event_loop::ActiveEventLoop, @@ -488,10 +494,13 @@ where settings, title, monitor, + on_open, } => { let exit_on_close_request = settings.exit_on_close_request; + let visible = settings.visible; + #[cfg(target_arch = "wasm32")] let target = settings.platform_specific.target.clone(); @@ -505,7 +514,8 @@ where .or(event_loop .primary_monitor()), self.id.clone(), - ), + ) + .with_visible(false), ) .expect("Create window"); @@ -561,6 +571,8 @@ where id, window, exit_on_close_request, + make_visible: visible, + on_open, }, ); } @@ -600,20 +612,21 @@ where struct Boot<C> { compositor: C, - clipboard: Clipboard, - window: winit::window::WindowId, - is_daemon: bool, } +#[derive(Debug)] enum Event<Message: 'static> { WindowCreated { id: window::Id, window: winit::window::Window, exit_on_close_request: bool, + make_visible: bool, + on_open: oneshot::Sender<window::Id>, }, EventLoopAwakened(winit::event::Event<Message>), } +#[derive(Debug)] enum Control { ChangeFlow(winit::event_loop::ControlFlow), Exit, @@ -622,6 +635,7 @@ enum Control { settings: window::Settings, title: String, monitor: Option<winit::monitor::MonitorHandle>, + on_open: oneshot::Sender<window::Id>, }, } @@ -630,9 +644,10 @@ async fn run_instance<P, C>( mut runtime: Runtime<P::Executor, Proxy<P::Message>, Action<P::Message>>, mut proxy: Proxy<P::Message>, mut debug: Debug, - mut boot: oneshot::Receiver<Boot<C>>, + boot: oneshot::Receiver<Boot<C>>, mut event_receiver: mpsc::UnboundedReceiver<Event<Action<P::Message>>>, mut control_sender: mpsc::UnboundedSender<Control>, + is_daemon: bool, ) where P: Program + 'static, C: Compositor<Renderer = P::Renderer> + 'static, @@ -641,14 +656,10 @@ async fn run_instance<P, C>( use winit::event; use winit::event_loop::ControlFlow; - let Boot { - mut compositor, - mut clipboard, - window: boot_window, - is_daemon, - } = boot.try_recv().ok().flatten().expect("Receive boot"); + let Boot { mut compositor } = boot.await.expect("Receive boot"); let mut window_manager = WindowManager::new(); + let mut is_window_opening = !is_daemon; let mut events = Vec::new(); let mut messages = Vec::new(); @@ -656,15 +667,29 @@ async fn run_instance<P, C>( let mut ui_caches = FxHashMap::default(); let mut user_interfaces = ManuallyDrop::new(FxHashMap::default()); + let mut clipboard = Clipboard::unconnected(); debug.startup_finished(); - 'main: while let Some(event) = event_receiver.next().await { + loop { + // Empty the queue if possible + let event = if let Ok(event) = event_receiver.try_next() { + event + } else { + event_receiver.next().await + }; + + let Some(event) = event else { + break; + }; + match event { Event::WindowCreated { id, window, exit_on_close_request, + make_visible, + on_open, } => { let window = window_manager.insert( id, @@ -689,6 +714,10 @@ async fn run_instance<P, C>( ); let _ = ui_caches.insert(id, user_interface::Cache::default()); + if make_visible { + window.raw.set_visible(true); + } + events.push(( id, core::Event::Window(window::Event::Opened { @@ -696,6 +725,13 @@ async fn run_instance<P, C>( size: window.size(), }), )); + + if clipboard.window_id().is_none() { + clipboard = Clipboard::connect(window.raw.clone()); + } + + let _ = on_open.send(id); + is_window_opening = false; } Event::EventLoopAwakened(event) => { match event { @@ -725,6 +761,7 @@ async fn run_instance<P, C>( action, &program, &mut compositor, + &mut events, &mut messages, &mut clipboard, &mut control_sender, @@ -732,6 +769,7 @@ async fn run_instance<P, C>( &mut user_interfaces, &mut window_manager, &mut ui_caches, + &mut is_window_opening, ); actions += 1; } @@ -916,10 +954,14 @@ async fn run_instance<P, C>( window_event, winit::event::WindowEvent::Destroyed ) - && window_id != boot_window + && !is_window_opening && window_manager.is_empty() { - break 'main; + control_sender + .start_send(Control::Exit) + .expect("Send control action"); + + continue; } let Some((id, window)) = @@ -933,14 +975,22 @@ async fn run_instance<P, C>( winit::event::WindowEvent::CloseRequested ) && window.exit_on_close_request { - let _ = window_manager.remove(id); - let _ = user_interfaces.remove(&id); - let _ = ui_caches.remove(&id); - - events.push(( - id, - core::Event::Window(window::Event::Closed), - )); + run_action( + Action::Window(runtime::window::Action::Close( + id, + )), + &program, + &mut compositor, + &mut events, + &mut messages, + &mut clipboard, + &mut control_sender, + &mut debug, + &mut user_interfaces, + &mut window_manager, + &mut ui_caches, + &mut is_window_opening, + ); } else { window.state.update( &window.raw, @@ -1114,19 +1164,20 @@ fn update<P: Program, E: Executor>( let task = runtime.enter(|| program.update(message)); debug.update_finished(); - if let Some(stream) = task.into_stream() { + if let Some(stream) = runtime::task::into_stream(task) { runtime.run(stream); } } let subscription = program.subscription(); - runtime.track(subscription.map(Action::Output).into_recipes()); + runtime.track(subscription::into_recipes(subscription.map(Action::Output))); } fn run_action<P, C>( action: Action<P::Message>, program: &P, compositor: &mut C, + events: &mut Vec<(window::Id, core::Event)>, messages: &mut Vec<P::Message>, clipboard: &mut Clipboard, control_sender: &mut mpsc::UnboundedSender<Control>, @@ -1137,6 +1188,7 @@ fn run_action<P, C>( >, window_manager: &mut WindowManager<P, C>, ui_caches: &mut FxHashMap<window::Id, user_interface::Cache>, + is_window_opening: &mut bool, ) where P: Program, C: Compositor<Renderer = P::Renderer> + 'static, @@ -1168,14 +1220,30 @@ fn run_action<P, C>( settings, title: program.title(id), monitor, + on_open: channel, }) .expect("Send control action"); - let _ = channel.send(id); + *is_window_opening = true; } window::Action::Close(id) => { - let _ = window_manager.remove(id); let _ = ui_caches.remove(&id); + let _ = interfaces.remove(&id); + + if let Some(window) = window_manager.remove(id) { + if clipboard.window_id() == Some(window.raw.id()) { + *clipboard = window_manager + .first() + .map(|window| window.raw.clone()) + .map(Clipboard::connect) + .unwrap_or_else(Clipboard::unconnected); + } + + events.push(( + id, + core::Event::Window(core::window::Event::Closed), + )); + } } window::Action::GetOldest(channel) => { let id = @@ -1235,7 +1303,7 @@ fn run_action<P, C>( } } window::Action::GetPosition(id, channel) => { - if let Some(window) = window_manager.get_mut(id) { + if let Some(window) = window_manager.get(id) { let position = window .raw .inner_position() @@ -1250,6 +1318,13 @@ fn run_action<P, C>( let _ = channel.send(position); } } + window::Action::GetScaleFactor(id, channel) => { + if let Some(window) = window_manager.get_mut(id) { + let scale_factor = window.raw.scale_factor(); + + let _ = channel.send(scale_factor as f32); + } + } window::Action::Move(id, position) => { if let Some(window) = window_manager.get_mut(id) { window.raw.set_outer_position( @@ -1360,6 +1435,16 @@ fn run_action<P, C>( )); } } + window::Action::EnableMousePassthrough(id) => { + if let Some(window) = window_manager.get_mut(id) { + let _ = window.raw.set_cursor_hittest(false); + } + } + window::Action::DisableMousePassthrough(id) => { + if let Some(window) = window_manager.get_mut(id) { + let _ = window.raw.set_cursor_hittest(true); + } + } }, Action::System(action) => match action { system::Action::QueryInformation(_channel) => { diff --git a/winit/src/program/window_manager.rs b/winit/src/program/window_manager.rs index fcbf79f6..3d22e155 100644 --- a/winit/src/program/window_manager.rs +++ b/winit/src/program/window_manager.rs @@ -74,12 +74,20 @@ where self.entries.is_empty() } + pub fn first(&self) -> Option<&Window<P, C>> { + self.entries.first_key_value().map(|(_id, window)| window) + } + pub fn iter_mut( &mut self, ) -> impl Iterator<Item = (Id, &mut Window<P, C>)> { self.entries.iter_mut().map(|(k, v)| (*k, v)) } + pub fn get(&self, id: Id) -> Option<&Window<P, C>> { + self.entries.get(&id) + } + pub fn get_mut(&mut self, id: Id) -> Option<&mut Window<P, C>> { self.entries.get_mut(&id) } diff --git a/winit/src/system.rs b/winit/src/system.rs index 7997f311..361135be 100644 --- a/winit/src/system.rs +++ b/winit/src/system.rs @@ -5,7 +5,7 @@ use crate::runtime::{self, Task}; /// Query for available system information. pub fn fetch_information() -> Task<Information> { - Task::oneshot(|channel| { + runtime::task::oneshot(|channel| { runtime::Action::System(Action::QueryInformation(channel)) }) } |