diff options
Diffstat (limited to 'native')
57 files changed, 2819 insertions, 1945 deletions
diff --git a/native/Cargo.toml b/native/Cargo.toml index 13052a93..a3134ef4 100644 --- a/native/Cargo.toml +++ b/native/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "iced_native" -version = "0.2.2" +version = "0.4.0" authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"] edition = "2018" description = "A renderer-agnostic library for native GUIs" @@ -16,10 +16,10 @@ unicode-segmentation = "1.6" num-traits = "0.2" [dependencies.iced_core] -version = "0.2" +version = "0.4" path = "../core" [dependencies.iced_futures] -version = "0.1" +version = "0.3" path = "../futures" features = ["thread-pool"] diff --git a/native/README.md b/native/README.md index 31c7db88..0d79690a 100644 --- a/native/README.md +++ b/native/README.md @@ -14,7 +14,9 @@ To achieve this, it introduces a bunch of reusable interfaces: - A bunch of `Renderer` traits, meant to keep the crate renderer-agnostic. - A `Windowed` trait, leveraging [`raw-window-handle`], which can be implemented by graphical renderers that target _windows_. Window-based shells (like [`iced_winit`]) can use this trait to stay renderer-agnostic. - +<p align="center"> + <img alt="The native target" src="../docs/graphs/native.png" width="80%"> +</p> [documentation]: https://docs.rs/iced_native [`iced_core`]: ../core @@ -26,7 +28,7 @@ To achieve this, it introduces a bunch of reusable interfaces: Add `iced_native` as a dependency in your `Cargo.toml`: ```toml -iced_native = "0.2" +iced_native = "0.4" ``` __Iced moves fast and the `master` branch can contain breaking changes!__ If diff --git a/native/src/clipboard.rs b/native/src/clipboard.rs index 4c574590..081b4004 100644 --- a/native/src/clipboard.rs +++ b/native/src/clipboard.rs @@ -1,8 +1,23 @@ +//! Access the clipboard. + /// A buffer for short-term storage and transfer within and between /// applications. pub trait Clipboard { - /// Returns the current content of the [`Clipboard`] as text. - /// - /// [`Clipboard`]: trait.Clipboard.html - fn content(&self) -> Option<String>; + /// Reads the current content of the [`Clipboard`] as text. + fn read(&self) -> Option<String>; + + /// Writes the given text contents to the [`Clipboard`]. + fn write(&mut self, contents: String); +} + +/// A null implementation of the [`Clipboard`] trait. +#[derive(Debug, Clone, Copy)] +pub struct Null; + +impl Clipboard for Null { + fn read(&self) -> Option<String> { + None + } + + fn write(&mut self, _contents: String) {} } diff --git a/native/src/debug/basic.rs b/native/src/debug/basic.rs index 8a712038..a42f66ea 100644 --- a/native/src/debug/basic.rs +++ b/native/src/debug/basic.rs @@ -32,9 +32,7 @@ pub struct Debug { } impl Debug { - /// Creates a new [`Debug`]. - /// - /// [`Debug`]: struct.Debug.html + /// Creates a new [`struct@Debug`]. pub fn new() -> Self { let now = time::Instant::now(); diff --git a/native/src/element.rs b/native/src/element.rs index 514a135b..5c84a388 100644 --- a/native/src/element.rs +++ b/native/src/element.rs @@ -1,6 +1,8 @@ +use crate::event::{self, Event}; +use crate::layout; +use crate::overlay; use crate::{ - layout, overlay, Clipboard, Color, Event, Hasher, Layout, Length, Point, - Widget, + Clipboard, Color, Hasher, Layout, Length, Point, Rectangle, Widget, }; /// A generic [`Widget`]. @@ -12,8 +14,6 @@ use crate::{ /// to turn it into an [`Element`]. /// /// [built-in widget]: widget/index.html#built-in-widgets -/// [`Widget`]: widget/trait.Widget.html -/// [`Element`]: struct.Element.html #[allow(missing_debug_implementations)] pub struct Element<'a, Message, Renderer> { pub(crate) widget: Box<dyn Widget<Message, Renderer> + 'a>, @@ -24,9 +24,6 @@ where Renderer: crate::Renderer, { /// Creates a new [`Element`] containing the given [`Widget`]. - /// - /// [`Element`]: struct.Element.html - /// [`Widget`]: widget/trait.Widget.html pub fn new( widget: impl Widget<Message, Renderer> + 'a, ) -> Element<'a, Message, Renderer> { @@ -40,8 +37,6 @@ where /// This method is useful when you want to decouple different parts of your /// UI and make them __composable__. /// - /// [`Element`]: struct.Element.html - /// /// # Example /// Imagine we want to use [our counter](index.html#usage). But instead of /// showing a single counter, we want to display many of them. We can reuse @@ -187,8 +182,7 @@ where /// The [`Renderer`] will explain the layout of the [`Element`] graphically. /// This can be very useful for debugging your layout! /// - /// [`Element`]: struct.Element.html - /// [`Renderer`]: trait.Renderer.html + /// [`Renderer`]: crate::Renderer pub fn explain<C: Into<Color>>( self, color: C, @@ -203,23 +197,18 @@ where } /// Returns the width of the [`Element`]. - /// - /// [`Element`]: struct.Element.html pub fn width(&self) -> Length { self.widget.width() } /// Returns the height of the [`Element`]. - /// - /// [`Element`]: struct.Element.html pub fn height(&self) -> Length { self.widget.height() } /// Computes the layout of the [`Element`] in the given [`Limits`]. /// - /// [`Element`]: struct.Element.html - /// [`Limits`]: layout/struct.Limits.html + /// [`Limits`]: layout::Limits pub fn layout( &self, renderer: &Renderer, @@ -229,52 +218,44 @@ where } /// Processes a runtime [`Event`]. - /// - /// [`Event`]: enum.Event.html pub fn on_event( &mut self, event: Event, layout: Layout<'_>, cursor_position: Point, - messages: &mut Vec<Message>, renderer: &Renderer, - clipboard: Option<&dyn Clipboard>, - ) { + clipboard: &mut dyn Clipboard, + messages: &mut Vec<Message>, + ) -> event::Status { self.widget.on_event( event, layout, cursor_position, - messages, renderer, clipboard, - ); + messages, + ) } /// Draws the [`Element`] and its children using the given [`Layout`]. - /// - /// [`Element`]: struct.Element.html - /// [`Layout`]: layout/struct.Layout.html pub fn draw( &self, renderer: &mut Renderer, defaults: &Renderer::Defaults, layout: Layout<'_>, cursor_position: Point, + viewport: &Rectangle, ) -> Renderer::Output { self.widget - .draw(renderer, defaults, layout, cursor_position) + .draw(renderer, defaults, layout, cursor_position, viewport) } /// Computes the _layout_ hash of the [`Element`]. - /// - /// [`Element`]: struct.Element.html pub fn hash_layout(&self, state: &mut Hasher) { self.widget.hash_layout(state); } /// Returns the overlay of the [`Element`], if there is any. - /// - /// [`Element`]: struct.Element.html pub fn overlay<'b>( &'b mut self, layout: Layout<'_>, @@ -330,24 +311,26 @@ where event: Event, layout: Layout<'_>, cursor_position: Point, - messages: &mut Vec<B>, renderer: &Renderer, - clipboard: Option<&dyn Clipboard>, - ) { + clipboard: &mut dyn Clipboard, + messages: &mut Vec<B>, + ) -> event::Status { let mut original_messages = Vec::new(); - self.widget.on_event( + let status = self.widget.on_event( event, layout, cursor_position, - &mut original_messages, renderer, clipboard, + &mut original_messages, ); original_messages .drain(..) .for_each(|message| messages.push((self.mapper)(message))); + + status } fn draw( @@ -356,9 +339,10 @@ where defaults: &Renderer::Defaults, layout: Layout<'_>, cursor_position: Point, + viewport: &Rectangle, ) -> Renderer::Output { self.widget - .draw(renderer, defaults, layout, cursor_position) + .draw(renderer, defaults, layout, cursor_position, viewport) } fn hash_layout(&self, state: &mut Hasher) { @@ -417,17 +401,17 @@ where event: Event, layout: Layout<'_>, cursor_position: Point, - messages: &mut Vec<Message>, renderer: &Renderer, - clipboard: Option<&dyn Clipboard>, - ) { + clipboard: &mut dyn Clipboard, + messages: &mut Vec<Message>, + ) -> event::Status { self.element.widget.on_event( event, layout, cursor_position, - messages, renderer, clipboard, + messages, ) } @@ -437,12 +421,14 @@ where defaults: &Renderer::Defaults, layout: Layout<'_>, cursor_position: Point, + viewport: &Rectangle, ) -> Renderer::Output { renderer.explain( defaults, self.element.widget.as_ref(), layout, cursor_position, + viewport, self.color, ) } diff --git a/native/src/event.rs b/native/src/event.rs index 606a71d6..1c26b5f2 100644 --- a/native/src/event.rs +++ b/native/src/event.rs @@ -1,4 +1,8 @@ -use crate::{keyboard, mouse, window}; +//! Handle events of a user interface. +use crate::keyboard; +use crate::mouse; +use crate::touch; +use crate::window; /// A user interface event. /// @@ -6,7 +10,7 @@ use crate::{keyboard, mouse, window}; /// additional events, feel free to [open an issue] and share your use case!_ /// /// [open an issue]: https://github.com/hecrj/iced/issues -#[derive(PartialEq, Clone, Debug)] +#[derive(Debug, Clone, PartialEq)] pub enum Event { /// A keyboard event Keyboard(keyboard::Event), @@ -16,4 +20,59 @@ pub enum Event { /// A window event Window(window::Event), + + /// A touch event + Touch(touch::Event), + + /// A platform specific event + PlatformSpecific(PlatformSpecific), +} + +/// A platform specific event +#[derive(Debug, Clone, PartialEq)] +pub enum PlatformSpecific { + /// A MacOS specific event + MacOS(MacOS), +} + +/// Describes an event specific to MacOS +#[derive(Debug, Clone, PartialEq)] +pub enum MacOS { + /// Triggered when the app receives an URL from the system + /// + /// _**Note:** For this event to be triggered, the executable needs to be properly [bundled]!_ + /// + /// [bundled]: https://developer.apple.com/library/archive/documentation/CoreFoundation/Conceptual/CFBundles/BundleTypes/BundleTypes.html#//apple_ref/doc/uid/10000123i-CH101-SW19 + ReceivedUrl(String), +} + +/// The status of an [`Event`] after being processed. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Status { + /// The [`Event`] was **NOT** handled by any widget. + Ignored, + + /// The [`Event`] was handled and processed by a widget. + Captured, +} + +impl Status { + /// Merges two [`Status`] into one. + /// + /// `Captured` takes precedence over `Ignored`: + /// + /// ``` + /// use iced_native::event::Status; + /// + /// assert_eq!(Status::Ignored.merge(Status::Ignored), Status::Ignored); + /// assert_eq!(Status::Ignored.merge(Status::Captured), Status::Captured); + /// assert_eq!(Status::Captured.merge(Status::Ignored), Status::Captured); + /// assert_eq!(Status::Captured.merge(Status::Captured), Status::Captured); + /// ``` + pub fn merge(self, b: Self) -> Self { + match self { + Status::Ignored => b, + Status::Captured => Status::Captured, + } + } } diff --git a/native/src/layout.rs b/native/src/layout.rs index 93d95e17..b4b4a021 100644 --- a/native/src/layout.rs +++ b/native/src/layout.rs @@ -12,8 +12,6 @@ pub use node::Node; use crate::{Point, Rectangle, Vector}; /// The bounds of a [`Node`] and its children, using absolute coordinates. -/// -/// [`Node`]: struct.Node.html #[derive(Debug, Clone, Copy)] pub struct Layout<'a> { position: Point, @@ -21,11 +19,14 @@ pub struct Layout<'a> { } impl<'a> Layout<'a> { - pub(crate) fn new(node: &'a Node) -> Self { + /// Creates a new [`Layout`] for the given [`Node`] at the origin. + pub fn new(node: &'a Node) -> Self { Self::with_offset(Vector::new(0.0, 0.0), node) } - pub(crate) fn with_offset(offset: Vector, node: &'a Node) -> Self { + /// Creates a new [`Layout`] for the given [`Node`] with the provided offset + /// from the origin. + pub fn with_offset(offset: Vector, node: &'a Node) -> Self { let bounds = node.bounds(); Self { @@ -35,8 +36,6 @@ impl<'a> Layout<'a> { } /// Returns the position of the [`Layout`]. - /// - /// [`Layout`]: struct.Layout.html pub fn position(&self) -> Point { self.position } @@ -45,10 +44,6 @@ impl<'a> Layout<'a> { /// /// The returned [`Rectangle`] describes the position and size of a /// [`Node`]. - /// - /// [`Layout`]: struct.Layout.html - /// [`Rectangle`]: struct.Rectangle.html - /// [`Node`]: struct.Node.html pub fn bounds(&self) -> Rectangle { let bounds = self.node.bounds(); @@ -61,10 +56,7 @@ impl<'a> Layout<'a> { } /// Returns an iterator over the [`Layout`] of the children of a [`Node`]. - /// - /// [`Layout`]: struct.Layout.html - /// [`Node`]: struct.Node.html - pub fn children(&'a self) -> impl Iterator<Item = Layout<'a>> { + pub fn children(self) -> impl Iterator<Item = Layout<'a>> { self.node.children().iter().map(move |node| { Layout::with_offset( Vector::new(self.position.x, self.position.y), diff --git a/native/src/layout/debugger.rs b/native/src/layout/debugger.rs index e4b21609..0759613f 100644 --- a/native/src/layout/debugger.rs +++ b/native/src/layout/debugger.rs @@ -1,8 +1,6 @@ -use crate::{Color, Layout, Point, Renderer, Widget}; +use crate::{Color, Layout, Point, Rectangle, Renderer, Widget}; /// A renderer able to graphically explain a [`Layout`]. -/// -/// [`Layout`]: struct.Layout.html pub trait Debugger: Renderer { /// Explains the [`Layout`] of an [`Element`] for debugging purposes. /// @@ -12,15 +10,15 @@ pub trait Debugger: Renderer { /// A common approach consists in recursively rendering the bounds of the /// [`Layout`] and its children. /// - /// [`Layout`]: struct.Layout.html - /// [`Element`]: ../struct.Element.html - /// [`Element::explain`]: ../struct.Element.html#method.explain + /// [`Element`]: crate::Element + /// [`Element::explain`]: crate::Element::explain fn explain<Message>( &mut self, defaults: &Self::Defaults, widget: &dyn Widget<Message, Self>, layout: Layout<'_>, cursor_position: Point, + viewport: &Rectangle, color: Color, ) -> Self::Output; } diff --git a/native/src/layout/flex.rs b/native/src/layout/flex.rs index 9da75a21..3d3ff82c 100644 --- a/native/src/layout/flex.rs +++ b/native/src/layout/flex.rs @@ -16,9 +16,10 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. + use crate::{ layout::{Limits, Node}, - Align, Element, Point, Size, + Align, Element, Padding, Point, Size, }; /// The main axis of a flex layout. @@ -58,13 +59,11 @@ impl Axis { /// padding and alignment to the items as needed. /// /// It returns a new layout [`Node`]. -/// -/// [`Node`]: ../struct.Node.html pub fn resolve<Message, Renderer>( axis: Axis, renderer: &Renderer, limits: &Limits, - padding: f32, + padding: Padding, spacing: f32, align_items: Align, items: &[Element<'_, Message, Renderer>], @@ -143,14 +142,15 @@ where } } - let mut main = padding; + let pad = axis.pack(padding.left as f32, padding.top as f32); + let mut main = pad.0; for (i, node) in nodes.iter_mut().enumerate() { if i > 0 { main += spacing; } - let (x, y) = axis.pack(main, padding); + let (x, y) = axis.pack(main, pad.1); node.move_to(Point::new(x, y)); @@ -168,7 +168,7 @@ where main += axis.main(size); } - let (width, height) = axis.pack(main - padding, cross); + let (width, height) = axis.pack(main - pad.0, cross); let size = limits.resolve(Size::new(width, height)); Node::with_children(size.pad(padding), nodes) diff --git a/native/src/layout/limits.rs b/native/src/layout/limits.rs index 664c881a..6d5f6563 100644 --- a/native/src/layout/limits.rs +++ b/native/src/layout/limits.rs @@ -1,4 +1,4 @@ -use crate::{Length, Size}; +use crate::{Length, Padding, Size}; /// A set of size constraints for layouting. #[derive(Debug, Clone, Copy)] @@ -17,9 +17,6 @@ impl Limits { }; /// Creates new [`Limits`] with the given minimum and maximum [`Size`]. - /// - /// [`Limits`]: struct.Limits.html - /// [`Size`]: ../struct.Size.html pub const fn new(min: Size, max: Size) -> Limits { Limits { min, @@ -29,32 +26,21 @@ impl Limits { } /// Returns the minimum [`Size`] of the [`Limits`]. - /// - /// [`Limits`]: struct.Limits.html - /// [`Size`]: ../struct.Size.html pub fn min(&self) -> Size { self.min } /// Returns the maximum [`Size`] of the [`Limits`]. - /// - /// [`Limits`]: struct.Limits.html - /// [`Size`]: ../struct.Size.html pub fn max(&self) -> Size { self.max } /// Returns the fill [`Size`] of the [`Limits`]. - /// - /// [`Limits`]: struct.Limits.html - /// [`Size`]: ../struct.Size.html pub fn fill(&self) -> Size { self.fill } /// Applies a width constraint to the current [`Limits`]. - /// - /// [`Limits`]: struct.Limits.html pub fn width(mut self, width: Length) -> Limits { match width { Length::Shrink => { @@ -77,8 +63,6 @@ impl Limits { } /// Applies a height constraint to the current [`Limits`]. - /// - /// [`Limits`]: struct.Limits.html pub fn height(mut self, height: Length) -> Limits { match height { Length::Shrink => { @@ -101,8 +85,6 @@ impl Limits { } /// Applies a minimum width constraint to the current [`Limits`]. - /// - /// [`Limits`]: struct.Limits.html pub fn min_width(mut self, min_width: u32) -> Limits { self.min.width = self.min.width.max(min_width as f32).min(self.max.width); @@ -111,8 +93,6 @@ impl Limits { } /// Applies a maximum width constraint to the current [`Limits`]. - /// - /// [`Limits`]: struct.Limits.html pub fn max_width(mut self, max_width: u32) -> Limits { self.max.width = self.max.width.min(max_width as f32).max(self.min.width); @@ -121,8 +101,6 @@ impl Limits { } /// Applies a minimum height constraint to the current [`Limits`]. - /// - /// [`Limits`]: struct.Limits.html pub fn min_height(mut self, min_height: u32) -> Limits { self.min.height = self.min.height.max(min_height as f32).min(self.max.height); @@ -131,8 +109,6 @@ impl Limits { } /// Applies a maximum height constraint to the current [`Limits`]. - /// - /// [`Limits`]: struct.Limits.html pub fn max_height(mut self, max_height: u32) -> Limits { self.max.height = self.max.height.min(max_height as f32).max(self.min.height); @@ -141,16 +117,14 @@ impl Limits { } /// Shrinks the current [`Limits`] to account for the given padding. - /// - /// [`Limits`]: struct.Limits.html - pub fn pad(&self, padding: f32) -> Limits { - self.shrink(Size::new(padding * 2.0, padding * 2.0)) + pub fn pad(&self, padding: Padding) -> Limits { + self.shrink(Size::new( + padding.horizontal() as f32, + padding.vertical() as f32, + )) } /// Shrinks the current [`Limits`] by the given [`Size`]. - /// - /// [`Limits`]: struct.Limits.html - /// [`Size`]: ../struct.Size.html pub fn shrink(&self, size: Size) -> Limits { let min = Size::new( (self.min().width - size.width).max(0.0), @@ -171,8 +145,6 @@ impl Limits { } /// Removes the minimum width constraint for the current [`Limits`]. - /// - /// [`Limits`]: struct.Limits.html pub fn loose(&self) -> Limits { Limits { min: Size::ZERO, @@ -183,8 +155,6 @@ impl Limits { /// Computes the resulting [`Size`] that fits the [`Limits`] given the /// intrinsic size of some content. - /// - /// [`Limits`]: struct.Limits.html pub fn resolve(&self, intrinsic_size: Size) -> Size { Size::new( intrinsic_size diff --git a/native/src/layout/node.rs b/native/src/layout/node.rs index a265c46a..d7666f31 100644 --- a/native/src/layout/node.rs +++ b/native/src/layout/node.rs @@ -9,17 +9,11 @@ pub struct Node { impl Node { /// Creates a new [`Node`] with the given [`Size`]. - /// - /// [`Node`]: struct.Node.html - /// [`Size`]: ../struct.Size.html pub const fn new(size: Size) -> Self { Self::with_children(size, Vec::new()) } /// Creates a new [`Node`] with the given [`Size`] and children. - /// - /// [`Node`]: struct.Node.html - /// [`Size`]: ../struct.Size.html pub const fn with_children(size: Size, children: Vec<Node>) -> Self { Node { bounds: Rectangle { @@ -33,30 +27,21 @@ impl Node { } /// Returns the [`Size`] of the [`Node`]. - /// - /// [`Node`]: struct.Node.html - /// [`Size`]: ../struct.Size.html pub fn size(&self) -> Size { Size::new(self.bounds.width, self.bounds.height) } /// Returns the bounds of the [`Node`]. - /// - /// [`Node`]: struct.Node.html pub fn bounds(&self) -> Rectangle { self.bounds } /// Returns the children of the [`Node`]. - /// - /// [`Node`]: struct.Node.html pub fn children(&self) -> &[Node] { &self.children } /// Aligns the [`Node`] in the given space. - /// - /// [`Node`]: struct.Node.html pub fn align( &mut self, horizontal_alignment: Align, @@ -85,8 +70,6 @@ impl Node { } /// Moves the [`Node`] to the given position. - /// - /// [`Node`]: struct.Node.html pub fn move_to(&mut self, position: Point) { self.bounds.x = position.x; self.bounds.y = position.y; diff --git a/native/src/lib.rs b/native/src/lib.rs index 067e3c0a..cbb02506 100644 --- a/native/src/lib.rs +++ b/native/src/lib.rs @@ -1,6 +1,6 @@ //! A renderer-agnostic native GUI runtime. //! -//!  +//!  //! //! `iced_native` takes [`iced_core`] and builds a native runtime on top of it, //! featuring: @@ -27,14 +27,14 @@ //! [`iced_winit`]: https://github.com/hecrj/iced/tree/master/winit //! [`druid`]: https://github.com/xi-editor/druid //! [`raw-window-handle`]: https://github.com/rust-windowing/raw-window-handle -//! [`Widget`]: widget/trait.Widget.html -//! [`UserInterface`]: struct.UserInterface.html -//! [renderer]: renderer/index.html +//! [renderer]: crate::renderer #![deny(missing_docs)] #![deny(missing_debug_implementations)] #![deny(unused_results)] #![forbid(unsafe_code)] #![forbid(rust_2018_idioms)] +pub mod clipboard; +pub mod event; pub mod keyboard; pub mod layout; pub mod mouse; @@ -42,12 +42,11 @@ pub mod overlay; pub mod program; pub mod renderer; pub mod subscription; +pub mod touch; pub mod widget; pub mod window; -mod clipboard; mod element; -mod event; mod hasher; mod runtime; mod user_interface; @@ -62,8 +61,8 @@ mod debug; mod debug; pub use iced_core::{ - Align, Background, Color, Font, HorizontalAlignment, Length, Point, - Rectangle, Size, Vector, VerticalAlignment, + menu, Align, Background, Color, Font, HorizontalAlignment, Length, Menu, + Padding, Point, Rectangle, Size, Vector, VerticalAlignment, }; pub use iced_futures::{executor, futures, Command}; diff --git a/native/src/mouse/click.rs b/native/src/mouse/click.rs index d27bc67e..6c8b61a5 100644 --- a/native/src/mouse/click.rs +++ b/native/src/mouse/click.rs @@ -36,8 +36,6 @@ impl Kind { impl Click { /// Creates a new [`Click`] with the given position and previous last /// [`Click`]. - /// - /// [`Click`]: struct.Click.html pub fn new(position: Point, previous: Option<Click>) -> Click { let time = Instant::now(); @@ -59,9 +57,6 @@ impl Click { } /// Returns the [`Kind`] of [`Click`]. - /// - /// [`Kind`]: enum.Kind.html - /// [`Click`]: struct.Click.html pub fn kind(&self) -> Kind { self.kind } diff --git a/native/src/overlay.rs b/native/src/overlay.rs index 7c3bec32..84145e7f 100644 --- a/native/src/overlay.rs +++ b/native/src/overlay.rs @@ -6,7 +6,9 @@ pub mod menu; pub use element::Element; pub use menu::Menu; -use crate::{layout, Clipboard, Event, Hasher, Layout, Point, Size}; +use crate::event::{self, Event}; +use crate::layout; +use crate::{Clipboard, Hasher, Layout, Point, Size}; /// An interactive component that can be displayed on top of other widgets. pub trait Overlay<Message, Renderer> @@ -18,9 +20,7 @@ where /// This [`Node`] is used by the runtime to compute the [`Layout`] of the /// user interface. /// - /// [`Node`]: ../layout/struct.Node.html - /// [`Widget`]: trait.Overlay.html - /// [`Layout`]: ../layout/struct.Layout.html + /// [`Node`]: layout::Node fn layout( &self, renderer: &Renderer, @@ -29,8 +29,6 @@ where ) -> layout::Node; /// Draws the [`Overlay`] using the associated `Renderer`. - /// - /// [`Overlay`]: trait.Overlay.html fn draw( &self, renderer: &mut Renderer, @@ -49,9 +47,7 @@ where /// For example, the [`Text`] widget does not hash its color property, as /// its value cannot affect the overall [`Layout`] of the user interface. /// - /// [`Overlay`]: trait.Overlay.html - /// [`Layout`]: ../layout/struct.Layout.html - /// [`Text`]: text/struct.Text.html + /// [`Text`]: crate::widget::Text fn hash_layout(&self, state: &mut Hasher, position: Point); /// Processes a runtime [`Event`]. @@ -66,19 +62,15 @@ where /// * a [`Clipboard`], if available /// /// By default, it does nothing. - /// - /// [`Event`]: ../enum.Event.html - /// [`Overlay`]: trait.Widget.html - /// [`Layout`]: ../layout/struct.Layout.html - /// [`Clipboard`]: ../trait.Clipboard.html fn on_event( &mut self, _event: Event, _layout: Layout<'_>, _cursor_position: Point, - _messages: &mut Vec<Message>, _renderer: &Renderer, - _clipboard: Option<&dyn Clipboard>, - ) { + _clipboard: &mut dyn Clipboard, + _messages: &mut Vec<Message>, + ) -> event::Status { + event::Status::Ignored } } diff --git a/native/src/overlay/element.rs b/native/src/overlay/element.rs index e1fd9b88..e4819037 100644 --- a/native/src/overlay/element.rs +++ b/native/src/overlay/element.rs @@ -1,10 +1,10 @@ pub use crate::Overlay; -use crate::{layout, Clipboard, Event, Hasher, Layout, Point, Size, Vector}; +use crate::event::{self, Event}; +use crate::layout; +use crate::{Clipboard, Hasher, Layout, Point, Size, Vector}; /// A generic [`Overlay`]. -/// -/// [`Overlay`]: trait.Overlay.html #[allow(missing_debug_implementations)] pub struct Element<'a, Message, Renderer> { position: Point, @@ -16,9 +16,6 @@ where Renderer: crate::Renderer, { /// Creates a new [`Element`] containing the given [`Overlay`]. - /// - /// [`Element`]: struct.Element.html - /// [`Overlay`]: trait.Overlay.html pub fn new( position: Point, overlay: Box<dyn Overlay<Message, Renderer> + 'a>, @@ -27,16 +24,12 @@ where } /// Translates the [`Element`]. - /// - /// [`Element`]: struct.Element.html pub fn translate(mut self, translation: Vector) -> Self { self.position = self.position + translation; self } /// Applies a transformation to the produced message of the [`Element`]. - /// - /// [`Element`]: struct.Element.html pub fn map<B>(self, f: &'a dyn Fn(Message) -> B) -> Element<'a, B, Renderer> where Message: 'a, @@ -50,38 +43,31 @@ where } /// Computes the layout of the [`Element`] in the given bounds. - /// - /// [`Element`]: struct.Element.html pub fn layout(&self, renderer: &Renderer, bounds: Size) -> layout::Node { self.overlay.layout(renderer, bounds, self.position) } /// Processes a runtime [`Event`]. - /// - /// [`Event`]: enum.Event.html pub fn on_event( &mut self, event: Event, layout: Layout<'_>, cursor_position: Point, - messages: &mut Vec<Message>, renderer: &Renderer, - clipboard: Option<&dyn Clipboard>, - ) { + clipboard: &mut dyn Clipboard, + messages: &mut Vec<Message>, + ) -> event::Status { self.overlay.on_event( event, layout, cursor_position, - messages, renderer, clipboard, + messages, ) } /// Draws the [`Element`] and its children using the given [`Layout`]. - /// - /// [`Element`]: struct.Element.html - /// [`Layout`]: layout/struct.Layout.html pub fn draw( &self, renderer: &mut Renderer, @@ -94,8 +80,6 @@ where } /// Computes the _layout_ hash of the [`Element`]. - /// - /// [`Element`]: struct.Element.html pub fn hash_layout(&self, state: &mut Hasher) { self.overlay.hash_layout(state, self.position); } @@ -133,24 +117,26 @@ where event: Event, layout: Layout<'_>, cursor_position: Point, - messages: &mut Vec<B>, renderer: &Renderer, - clipboard: Option<&dyn Clipboard>, - ) { + clipboard: &mut dyn Clipboard, + messages: &mut Vec<B>, + ) -> event::Status { let mut original_messages = Vec::new(); - self.content.on_event( + let event_status = self.content.on_event( event, layout, cursor_position, - &mut original_messages, renderer, clipboard, + &mut original_messages, ); original_messages .drain(..) .for_each(|message| messages.push((self.mapper)(message))); + + event_status } fn draw( diff --git a/native/src/overlay/menu.rs b/native/src/overlay/menu.rs index c2df468e..f62dcb46 100644 --- a/native/src/overlay/menu.rs +++ b/native/src/overlay/menu.rs @@ -1,8 +1,15 @@ //! Build and show dropdown menus. +use crate::container; +use crate::event::{self, Event}; +use crate::layout; +use crate::mouse; +use crate::overlay; +use crate::scrollable; +use crate::text; +use crate::touch; use crate::{ - container, layout, mouse, overlay, scrollable, text, Clipboard, Container, - Element, Event, Hasher, Layout, Length, Point, Rectangle, Scrollable, Size, - Vector, Widget, + Clipboard, Container, Element, Hasher, Layout, Length, Padding, Point, + Rectangle, Scrollable, Size, Vector, Widget, }; /// A list of selectable options. @@ -13,7 +20,7 @@ pub struct Menu<'a, T, Renderer: self::Renderer> { hovered_option: &'a mut Option<usize>, last_selection: &'a mut Option<T>, width: u16, - padding: u16, + padding: Padding, text_size: Option<u16>, font: Renderer::Font, style: <Renderer as self::Renderer>::Style, @@ -26,9 +33,6 @@ where { /// Creates a new [`Menu`] with the given [`State`], a list of options, and /// the message to produced when an option is selected. - /// - /// [`Menu`]: struct.Menu.html - /// [`State`]: struct.State.html pub fn new( state: &'a mut State, options: &'a [T], @@ -41,7 +45,7 @@ where hovered_option, last_selection, width: 0, - padding: 0, + padding: Padding::ZERO, text_size: None, font: Default::default(), style: Default::default(), @@ -49,40 +53,30 @@ where } /// Sets the width of the [`Menu`]. - /// - /// [`Menu`]: struct.Menu.html pub fn width(mut self, width: u16) -> Self { self.width = width; self } - /// Sets the padding of the [`Menu`]. - /// - /// [`Menu`]: struct.Menu.html - pub fn padding(mut self, padding: u16) -> Self { - self.padding = padding; + /// Sets the [`Padding`] of the [`Menu`]. + pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self { + self.padding = padding.into(); self } /// Sets the text size of the [`Menu`]. - /// - /// [`Menu`]: struct.Menu.html pub fn text_size(mut self, text_size: u16) -> Self { self.text_size = Some(text_size); self } /// Sets the font of the [`Menu`]. - /// - /// [`Menu`]: struct.Menu.html pub fn font(mut self, font: Renderer::Font) -> Self { self.font = font; self } /// Sets the style of the [`Menu`]. - /// - /// [`Menu`]: struct.Menu.html pub fn style( mut self, style: impl Into<<Renderer as self::Renderer>::Style>, @@ -97,8 +91,6 @@ where /// The `target_height` will be used to display the menu either on top /// of the target or under it, depending on the screen position and the /// dimensions of the [`Menu`]. - /// - /// [`Menu`]: struct.Menu.html pub fn overlay<Message: 'a>( self, position: Point, @@ -112,8 +104,6 @@ where } /// The local state of a [`Menu`]. -/// -/// [`Menu`]: struct.Menu.html #[derive(Debug, Clone, Default)] pub struct State { scrollable: scrollable::State, @@ -121,9 +111,6 @@ pub struct State { impl State { /// Creates a new [`State`] for a [`Menu`]. - /// - /// [`State`]: struct.State.html - /// [`Menu`]: struct.Menu.html pub fn new() -> Self { Self::default() } @@ -232,18 +219,18 @@ where event: Event, layout: Layout<'_>, cursor_position: Point, - messages: &mut Vec<Message>, renderer: &Renderer, - clipboard: Option<&dyn Clipboard>, - ) { + clipboard: &mut dyn Clipboard, + messages: &mut Vec<Message>, + ) -> event::Status { self.container.on_event( event.clone(), layout, cursor_position, - messages, renderer, clipboard, - ); + messages, + ) } fn draw( @@ -253,9 +240,13 @@ where layout: Layout<'_>, cursor_position: Point, ) -> Renderer::Output { - let primitives = - self.container - .draw(renderer, defaults, layout, cursor_position); + let primitives = self.container.draw( + renderer, + defaults, + layout, + cursor_position, + &layout.bounds(), + ); renderer.decorate( layout.bounds(), @@ -270,7 +261,7 @@ struct List<'a, T, Renderer: self::Renderer> { options: &'a [T], hovered_option: &'a mut Option<usize>, last_selection: &'a mut Option<T>, - padding: u16, + padding: Padding, text_size: Option<u16>, font: Renderer::Font, style: <Renderer as self::Renderer>::Style, @@ -303,7 +294,7 @@ where let size = { let intrinsic = Size::new( 0.0, - f32::from(text_size + self.padding * 2) + f32::from(text_size + self.padding.vertical()) * self.options.len() as f32, ); @@ -329,10 +320,10 @@ where event: Event, layout: Layout<'_>, cursor_position: Point, - _messages: &mut Vec<Message>, renderer: &Renderer, - _clipboard: Option<&dyn Clipboard>, - ) { + _clipboard: &mut dyn Clipboard, + _messages: &mut Vec<Message>, + ) -> event::Status { match event { Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => { let bounds = layout.bounds(); @@ -347,19 +338,42 @@ where } Event::Mouse(mouse::Event::CursorMoved { .. }) => { let bounds = layout.bounds(); - let text_size = - self.text_size.unwrap_or(renderer.default_size()); if bounds.contains(cursor_position) { + let text_size = + self.text_size.unwrap_or(renderer.default_size()); + *self.hovered_option = Some( ((cursor_position.y - bounds.y) - / f32::from(text_size + self.padding * 2)) + / f32::from(text_size + self.padding.vertical())) as usize, ); } } + Event::Touch(touch::Event::FingerPressed { .. }) => { + let bounds = layout.bounds(); + + if bounds.contains(cursor_position) { + let text_size = + self.text_size.unwrap_or(renderer.default_size()); + + *self.hovered_option = Some( + ((cursor_position.y - bounds.y) + / f32::from(text_size + self.padding.vertical())) + as usize, + ); + + if let Some(index) = *self.hovered_option { + if let Some(option) = self.options.get(index) { + *self.last_selection = Some(option.clone()); + } + } + } + } _ => {} } + + event::Status::Ignored } fn draw( @@ -368,11 +382,13 @@ where _defaults: &Renderer::Defaults, layout: Layout<'_>, cursor_position: Point, + viewport: &Rectangle, ) -> Renderer::Output { self::Renderer::draw( renderer, layout.bounds(), cursor_position, + viewport, self.options, *self.hovered_option, self.padding, @@ -388,21 +404,16 @@ where /// Your [renderer] will need to implement this trait before being /// able to use a [`Menu`] in your user interface. /// -/// [`Menu`]: struct.Menu.html -/// [renderer]: ../../renderer/index.html +/// [renderer]: crate::renderer pub trait Renderer: scrollable::Renderer + container::Renderer + text::Renderer { /// The [`Menu`] style supported by this renderer. - /// - /// [`Menu`]: struct.Menu.html type Style: Default + Clone; /// Decorates a the list of options of a [`Menu`]. /// /// This method can be used to draw a background for the [`Menu`]. - /// - /// [`Menu`]: struct.Menu.html fn decorate( &mut self, bounds: Rectangle, @@ -412,15 +423,14 @@ pub trait Renderer: ) -> Self::Output; /// Draws the list of options of a [`Menu`]. - /// - /// [`Menu`]: struct.Menu.html fn draw<T: ToString>( &mut self, bounds: Rectangle, cursor_position: Point, + viewport: &Rectangle, options: &[T], hovered_option: Option<usize>, - padding: u16, + padding: Padding, text_size: u16, font: Self::Font, style: &<Self as Renderer>::Style, diff --git a/native/src/program.rs b/native/src/program.rs index 14afcd84..75fab094 100644 --- a/native/src/program.rs +++ b/native/src/program.rs @@ -1,5 +1,5 @@ //! Build interactive programs using The Elm Architecture. -use crate::{Command, Element, Renderer}; +use crate::{Clipboard, Command, Element, Renderer}; mod state; @@ -8,14 +8,13 @@ pub use state::State; /// The core of a user interface application following The Elm Architecture. pub trait Program: Sized { /// The graphics backend to use to draw the [`Program`]. - /// - /// [`Program`]: trait.Program.html type Renderer: Renderer; /// The type of __messages__ your [`Program`] will produce. - /// - /// [`Program`]: trait.Program.html - type Message: std::fmt::Debug + Send; + type Message: std::fmt::Debug + Clone + Send; + + /// The type of [`Clipboard`] your [`Program`] will use. + type Clipboard: Clipboard; /// Handles a __message__ and updates the state of the [`Program`]. /// @@ -25,15 +24,14 @@ pub trait Program: Sized { /// /// Any [`Command`] returned will be executed immediately in the /// background by shells. - /// - /// [`Program`]: trait.Application.html - /// [`Command`]: struct.Command.html - fn update(&mut self, message: Self::Message) -> Command<Self::Message>; + fn update( + &mut self, + message: Self::Message, + clipboard: &mut Self::Clipboard, + ) -> Command<Self::Message>; /// Returns the widgets to display in the [`Program`]. /// /// These widgets can produce __messages__ based on user interaction. - /// - /// [`Program`]: trait.Program.html fn view(&mut self) -> Element<'_, Self::Message, Self::Renderer>; } diff --git a/native/src/program/state.rs b/native/src/program/state.rs index 95e6b60c..fd1f2b52 100644 --- a/native/src/program/state.rs +++ b/native/src/program/state.rs @@ -1,12 +1,9 @@ use crate::{ - Cache, Clipboard, Command, Debug, Event, Point, Program, Renderer, Size, - UserInterface, + Cache, Command, Debug, Event, Point, Program, Renderer, Size, UserInterface, }; /// The execution state of a [`Program`]. It leverages caching, event /// processing, and rendering primitive storage. -/// -/// [`Program`]: trait.Program.html #[allow(missing_debug_implementations)] pub struct State<P> where @@ -25,9 +22,6 @@ where { /// Creates a new [`State`] with the provided [`Program`], initializing its /// primitive with the given logical bounds and renderer. - /// - /// [`State`]: struct.State.html - /// [`Program`]: trait.Program.html pub fn new( mut program: P, bounds: Size, @@ -59,39 +53,30 @@ where } /// Returns a reference to the [`Program`] of the [`State`]. - /// - /// [`Program`]: trait.Program.html - /// [`State`]: struct.State.html pub fn program(&self) -> &P { &self.program } /// Returns a reference to the current rendering primitive of the [`State`]. - /// - /// [`State`]: struct.State.html pub fn primitive(&self) -> &<P::Renderer as Renderer>::Output { &self.primitive } /// Queues an event in the [`State`] for processing during an [`update`]. /// - /// [`State`]: struct.State.html - /// [`update`]: #method.update + /// [`update`]: Self::update pub fn queue_event(&mut self, event: Event) { self.queued_events.push(event); } /// Queues a message in the [`State`] for processing during an [`update`]. /// - /// [`State`]: struct.State.html - /// [`update`]: #method.update + /// [`update`]: Self::update pub fn queue_message(&mut self, message: P::Message) { self.queued_messages.push(message); } /// Returns whether the event queue of the [`State`] is empty or not. - /// - /// [`State`]: struct.State.html pub fn is_queue_empty(&self) -> bool { self.queued_events.is_empty() && self.queued_messages.is_empty() } @@ -101,14 +86,12 @@ where /// /// Returns the [`Command`] obtained from [`Program`] after updating it, /// only if an update was necessary. - /// - /// [`Program`]: trait.Program.html pub fn update( &mut self, bounds: Size, cursor_position: Point, - clipboard: Option<&dyn Clipboard>, renderer: &mut P::Renderer, + clipboard: &mut P::Clipboard, debug: &mut Debug, ) -> Option<Command<P::Message>> { let mut user_interface = build_user_interface( @@ -120,14 +103,17 @@ where ); debug.event_processing_started(); - let mut messages = user_interface.update( + let mut messages = Vec::new(); + + let _ = user_interface.update( &self.queued_events, cursor_position, - clipboard, renderer, + clipboard, + &mut messages, ); - messages.extend(self.queued_messages.drain(..)); + messages.extend(self.queued_messages.drain(..)); self.queued_events.clear(); debug.event_processing_finished(); @@ -149,7 +135,7 @@ where debug.log_message(&message); debug.update_started(); - let command = self.program.update(message); + let command = self.program.update(message, clipboard); debug.update_finished(); command diff --git a/native/src/renderer.rs b/native/src/renderer.rs index d986141f..39a6cff1 100644 --- a/native/src/renderer.rs +++ b/native/src/renderer.rs @@ -13,12 +13,12 @@ //! In the end, a __renderer__ satisfying all the constraints is //! needed to build a [`UserInterface`]. //! -//! [`Widget`]: ../widget/trait.Widget.html -//! [`UserInterface`]: ../struct.UserInterface.html -//! [`Text`]: ../widget/text/struct.Text.html -//! [`text::Renderer`]: ../widget/text/trait.Renderer.html -//! [`Checkbox`]: ../widget/checkbox/struct.Checkbox.html -//! [`checkbox::Renderer`]: ../widget/checkbox/trait.Renderer.html +//! [`Widget`]: crate::Widget +//! [`UserInterface`]: crate::UserInterface +//! [`Text`]: crate::widget::Text +//! [`text::Renderer`]: crate::widget::text::Renderer +//! [`Checkbox`]: crate::widget::Checkbox +//! [`checkbox::Renderer`]: crate::widget::checkbox::Renderer #[cfg(debug_assertions)] mod null; @@ -34,15 +34,11 @@ pub trait Renderer: Sized { /// /// If you are implementing a graphical renderer, your output will most /// likely be a tree of visual primitives. - /// - /// [`Renderer`]: trait.Renderer.html type Output; /// The default styling attributes of the [`Renderer`]. /// /// This type can be leveraged to implement style inheritance. - /// - /// [`Renderer`]: trait.Renderer.html type Defaults: Default; /// Lays out the elements of a user interface. diff --git a/native/src/renderer/null.rs b/native/src/renderer/null.rs index 2aee0da1..bb57c163 100644 --- a/native/src/renderer/null.rs +++ b/native/src/renderer/null.rs @@ -1,7 +1,7 @@ use crate::{ button, checkbox, column, container, pane_grid, progress_bar, radio, row, - scrollable, slider, text, text_input, Color, Element, Font, - HorizontalAlignment, Layout, Point, Rectangle, Renderer, Size, + scrollable, slider, text, text_input, toggler, Color, Element, Font, + HorizontalAlignment, Layout, Padding, Point, Rectangle, Renderer, Size, VerticalAlignment, }; @@ -13,8 +13,6 @@ pub struct Null; impl Null { /// Creates a new [`Null`] renderer. - /// - /// [`Null`]: struct.Null.html pub fn new() -> Null { Null } @@ -35,6 +33,7 @@ impl column::Renderer for Null { _content: &[Element<'_, Message, Self>], _layout: Layout<'_>, _cursor_position: Point, + _viewport: &Rectangle, ) { } } @@ -46,6 +45,7 @@ impl row::Renderer for Null { _content: &[Element<'_, Message, Self>], _layout: Layout<'_>, _cursor_position: Point, + _viewport: &Rectangle, ) { } } @@ -89,6 +89,9 @@ impl scrollable::Renderer for Null { _bounds: Rectangle, _content_bounds: Rectangle, _offset: u32, + _scrollbar_width: u16, + _scrollbar_margin: u16, + _scroller_width: u16, ) -> Option<scrollable::Scrollbar> { None } @@ -142,7 +145,7 @@ impl text_input::Renderer for Null { } impl button::Renderer for Null { - const DEFAULT_PADDING: u16 = 0; + const DEFAULT_PADDING: Padding = Padding::ZERO; type Style = (); @@ -234,6 +237,7 @@ impl container::Renderer for Null { _defaults: &Self::Defaults, _bounds: Rectangle, _cursor_position: Point, + _viewport: &Rectangle, _style: &Self::Style, _content: &Element<'_, Message, Self>, _content_layout: Layout<'_>, @@ -242,14 +246,18 @@ impl container::Renderer for Null { } impl pane_grid::Renderer for Null { + type Style = (); + fn draw<Message>( &mut self, _defaults: &Self::Defaults, _content: &[(pane_grid::Pane, pane_grid::Content<'_, Message, Self>)], _dragging: Option<(pane_grid::Pane, Point)>, - _resizing: Option<pane_grid::Axis>, + _resizing: Option<(pane_grid::Axis, Rectangle, bool)>, _layout: Layout<'_>, + _style: &<Self as pane_grid::Renderer>::Style, _cursor_position: Point, + _viewport: &Rectangle, ) { } @@ -257,13 +265,14 @@ impl pane_grid::Renderer for Null { &mut self, _defaults: &Self::Defaults, _bounds: Rectangle, - _style: &Self::Style, + _style: &<Self as container::Renderer>::Style, _title_bar: Option<( &pane_grid::TitleBar<'_, Message, Self>, Layout<'_>, )>, _body: (&Element<'_, Message, Self>, Layout<'_>), _cursor_position: Point, + _viewport: &Rectangle, ) { } @@ -271,13 +280,27 @@ impl pane_grid::Renderer for Null { &mut self, _defaults: &Self::Defaults, _bounds: Rectangle, - _style: &Self::Style, - _title: &str, - _title_size: u16, - _title_font: Self::Font, - _title_bounds: Rectangle, + _style: &<Self as container::Renderer>::Style, + _content: (&Element<'_, Message, Self>, Layout<'_>), _controls: Option<(&Element<'_, Message, Self>, Layout<'_>)>, _cursor_position: Point, + _viewport: &Rectangle, + ) { + } +} + +impl toggler::Renderer for Null { + type Style = (); + + const DEFAULT_SIZE: u16 = 20; + + fn draw( + &mut self, + _bounds: Rectangle, + _is_checked: bool, + _is_mouse_over: bool, + _label: Option<Self::Output>, + _style: &Self::Style, ) { } } diff --git a/native/src/runtime.rs b/native/src/runtime.rs index 9fa031f4..5b0a6925 100644 --- a/native/src/runtime.rs +++ b/native/src/runtime.rs @@ -1,12 +1,18 @@ //! Run commands and subscriptions. -use crate::{Event, Hasher}; +use crate::event::{self, Event}; +use crate::Hasher; /// A native runtime with a generic executor and receiver of results. /// /// It can be used by shells to easily spawn a [`Command`] or track a /// [`Subscription`]. /// -/// [`Command`]: ../struct.Command.html -/// [`Subscription`]: ../struct.Subscription.html -pub type Runtime<Executor, Receiver, Message> = - iced_futures::Runtime<Hasher, Event, Executor, Receiver, Message>; +/// [`Command`]: crate::Command +/// [`Subscription`]: crate::Subscription +pub type Runtime<Executor, Receiver, Message> = iced_futures::Runtime< + Hasher, + (Event, event::Status), + Executor, + Receiver, + Message, +>; diff --git a/native/src/subscription.rs b/native/src/subscription.rs index 0d002c6c..ff954382 100644 --- a/native/src/subscription.rs +++ b/native/src/subscription.rs @@ -1,5 +1,6 @@ //! Listen to external events in your application. -use crate::{Event, Hasher}; +use crate::event::{self, Event}; +use crate::Hasher; use iced_futures::futures::stream::BoxStream; /// A request to listen to external events. @@ -13,21 +14,18 @@ use iced_futures::futures::stream::BoxStream; /// For instance, you can use a [`Subscription`] to listen to a WebSocket /// connection, keyboard presses, mouse events, time ticks, etc. /// -/// [`Command`]: ../struct.Command.html -/// [`Subscription`]: struct.Subscription.html -pub type Subscription<T> = iced_futures::Subscription<Hasher, Event, T>; +/// [`Command`]: crate::Command +pub type Subscription<T> = + iced_futures::Subscription<Hasher, (Event, event::Status), T>; /// A stream of runtime events. /// /// It is the input of a [`Subscription`] in the native runtime. -/// -/// [`Subscription`]: type.Subscription.html -pub type EventStream = BoxStream<'static, Event>; +pub type EventStream = BoxStream<'static, (Event, event::Status)>; /// A native [`Subscription`] tracker. -/// -/// [`Subscription`]: type.Subscription.html -pub type Tracker = iced_futures::subscription::Tracker<Hasher, Event>; +pub type Tracker = + iced_futures::subscription::Tracker<Hasher, (Event, event::Status)>; pub use iced_futures::subscription::Recipe; @@ -37,11 +35,30 @@ use events::Events; /// Returns a [`Subscription`] to all the runtime events. /// -/// This subscription will notify your application of any [`Event`] handled by -/// the runtime. -/// -/// [`Subscription`]: type.Subscription.html -/// [`Event`]: ../enum.Event.html +/// This subscription will notify your application of any [`Event`] that was +/// not captured by any widget. pub fn events() -> Subscription<Event> { - Subscription::from_recipe(Events) + Subscription::from_recipe(Events { + f: |event, status| match status { + event::Status::Ignored => Some(event), + event::Status::Captured => None, + }, + }) +} + +/// Returns a [`Subscription`] that filters all the runtime events with the +/// provided function, producing messages accordingly. +/// +/// This subscription will call the provided function for every [`Event`] +/// handled by the runtime. If the function: +/// +/// - Returns `None`, the [`Event`] will be discarded. +/// - Returns `Some` message, the `Message` will be produced. +pub fn events_with<Message>( + f: fn(Event, event::Status) -> Option<Message>, +) -> Subscription<Message> +where + Message: 'static + Send, +{ + Subscription::from_recipe(Events { f }) } diff --git a/native/src/subscription/events.rs b/native/src/subscription/events.rs index ceae467d..f689f3af 100644 --- a/native/src/subscription/events.rs +++ b/native/src/subscription/events.rs @@ -1,18 +1,26 @@ -use crate::{ - subscription::{EventStream, Recipe}, - Event, Hasher, -}; +use crate::event::{self, Event}; +use crate::subscription::{EventStream, Recipe}; +use crate::Hasher; +use iced_futures::futures::future; +use iced_futures::futures::StreamExt; use iced_futures::BoxStream; -pub struct Events; +pub struct Events<Message> { + pub(super) f: fn(Event, event::Status) -> Option<Message>, +} -impl Recipe<Hasher, Event> for Events { - type Output = Event; +impl<Message> Recipe<Hasher, (Event, event::Status)> for Events<Message> +where + Message: 'static + Send, +{ + type Output = Message; fn hash(&self, state: &mut Hasher) { use std::hash::Hash; - std::any::TypeId::of::<Self>().hash(state); + struct Marker; + std::any::TypeId::of::<Marker>().hash(state); + self.f.hash(state); } fn stream( @@ -20,5 +28,9 @@ impl Recipe<Hasher, Event> for Events { event_stream: EventStream, ) -> BoxStream<Self::Output> { event_stream + .filter_map(move |(event, status)| { + future::ready((self.f)(event, status)) + }) + .boxed() } } diff --git a/native/src/touch.rs b/native/src/touch.rs new file mode 100644 index 00000000..18120644 --- /dev/null +++ b/native/src/touch.rs @@ -0,0 +1,23 @@ +//! Build touch events. +use crate::Point; + +/// A touch interaction. +#[derive(Debug, Clone, Copy, PartialEq)] +#[allow(missing_docs)] +pub enum Event { + /// A touch interaction was started. + FingerPressed { id: Finger, position: Point }, + + /// An on-going touch interaction was moved. + FingerMoved { id: Finger, position: Point }, + + /// A touch interaction was ended. + FingerLifted { id: Finger, position: Point }, + + /// A touch interaction was canceled. + FingerLost { id: Finger, position: Point }, +} + +/// A unique identifier representing a finger on a touch interaction. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct Finger(pub u64); diff --git a/native/src/user_interface.rs b/native/src/user_interface.rs index 00a290f1..8e0d7d1c 100644 --- a/native/src/user_interface.rs +++ b/native/src/user_interface.rs @@ -1,4 +1,7 @@ -use crate::{layout, overlay, Clipboard, Element, Event, Layout, Point, Size}; +use crate::event::{self, Event}; +use crate::layout; +use crate::overlay; +use crate::{Clipboard, Element, Layout, Point, Rectangle, Size}; use std::hash::Hasher; @@ -9,14 +12,11 @@ use std::hash::Hasher; /// Iced tries to avoid dictating how to write your event loop. You are in /// charge of using this type in your system in any way you want. /// -/// [`Layout`]: struct.Layout.html -/// /// # Example /// The [`integration` example] uses a [`UserInterface`] to integrate Iced in /// an existing graphical application. /// -/// [`integration` example]: https://github.com/hecrj/iced/tree/0.1/examples/integration -/// [`UserInterface`]: struct.UserInterface.html +/// [`integration` example]: https://github.com/hecrj/iced/tree/0.3/examples/integration #[allow(missing_debug_implementations)] pub struct UserInterface<'a, Message, Renderer> { root: Element<'a, Message, Renderer>, @@ -34,10 +34,6 @@ where /// It is able to avoid expensive computations when using a [`Cache`] /// obtained from a previous instance of a [`UserInterface`]. /// - /// [`Element`]: struct.Element.html - /// [`Cache`]: struct.Cache.html - /// [`UserInterface`]: struct.UserInterface.html - /// /// # Example /// Imagine we want to build a [`UserInterface`] for /// [the counter example that we previously wrote](index.html#usage). Here @@ -133,15 +129,12 @@ where /// It returns __messages__ that may have been produced as a result of user /// interactions. You should feed these to your __update logic__. /// - /// [`UserInterface`]: struct.UserInterface.html - /// [`Event`]: enum.Event.html - /// /// # Example /// Let's allow our [counter](index.html#usage) to change state by /// completing [the previous example](#example): /// /// ```no_run - /// use iced_native::{UserInterface, Cache, Size, Point}; + /// use iced_native::{clipboard, UserInterface, Cache, Size, Point}; /// use iced_wgpu::Renderer; /// /// # mod iced_wgpu { @@ -164,12 +157,14 @@ where /// let mut renderer = Renderer::new(); /// let mut window_size = Size::new(1024.0, 768.0); /// let mut cursor_position = Point::default(); + /// let mut clipboard = clipboard::Null; /// /// // Initialize our event storage /// let mut events = Vec::new(); + /// let mut messages = Vec::new(); /// /// loop { - /// // Process system events... + /// // Obtain system events... /// /// let mut user_interface = UserInterface::build( /// counter.view(), @@ -179,17 +174,18 @@ where /// ); /// /// // Update the user interface - /// let messages = user_interface.update( + /// let event_statuses = user_interface.update( /// &events, /// cursor_position, - /// None, /// &renderer, + /// &mut clipboard, + /// &mut messages /// ); /// /// cache = user_interface.into_cache(); /// /// // Process the produced messages - /// for message in messages { + /// for message in messages.drain(..) { /// counter.update(message); /// } /// } @@ -198,12 +194,11 @@ where &mut self, events: &[Event], cursor_position: Point, - clipboard: Option<&dyn Clipboard>, renderer: &Renderer, - ) -> Vec<Message> { - let mut messages = Vec::new(); - - let base_cursor = if let Some(mut overlay) = + clipboard: &mut dyn Clipboard, + messages: &mut Vec<Message>, + ) -> Vec<event::Status> { + let (base_cursor, overlay_statuses) = if let Some(mut overlay) = self.root.overlay(Layout::new(&self.base.layout)) { let layer = Self::overlay_layer( @@ -213,16 +208,20 @@ where renderer, ); - for event in events { - overlay.on_event( - event.clone(), - Layout::new(&layer.layout), - cursor_position, - &mut messages, - renderer, - clipboard, - ); - } + let event_statuses = events + .iter() + .cloned() + .map(|event| { + overlay.on_event( + event, + Layout::new(&layer.layout), + cursor_position, + renderer, + clipboard, + messages, + ) + }) + .collect(); let base_cursor = if layer.layout.bounds().contains(cursor_position) { @@ -234,40 +233,44 @@ where self.overlay = Some(layer); - base_cursor + (base_cursor, event_statuses) } else { - cursor_position + (cursor_position, vec![event::Status::Ignored; events.len()]) }; - for event in events { - self.root.widget.on_event( - event.clone(), - Layout::new(&self.base.layout), - base_cursor, - &mut messages, - renderer, - clipboard, - ); - } + events + .iter() + .cloned() + .zip(overlay_statuses.into_iter()) + .map(|(event, overlay_status)| { + let event_status = self.root.widget.on_event( + event, + Layout::new(&self.base.layout), + base_cursor, + renderer, + clipboard, + messages, + ); - messages + event_status.merge(overlay_status) + }) + .collect() } /// Draws the [`UserInterface`] with the provided [`Renderer`]. /// - /// It returns the current state of the [`MouseCursor`]. You should update - /// the icon of the mouse cursor accordingly in your system. + /// It returns the some [`Renderer::Output`]. You should update the icon of + /// the mouse cursor accordingly in your system. /// - /// [`UserInterface`]: struct.UserInterface.html - /// [`Renderer`]: trait.Renderer.html - /// [`MouseCursor`]: enum.MouseCursor.html + /// [`Renderer`]: crate::Renderer + /// [`Renderer::Output`]: crate::Renderer::Output /// /// # Example /// We can finally draw our [counter](index.html#usage) by /// [completing the last example](#example-1): /// /// ```no_run - /// use iced_native::{UserInterface, Cache, Size, Point}; + /// use iced_native::{clipboard, UserInterface, Cache, Size, Point}; /// use iced_wgpu::Renderer; /// /// # mod iced_wgpu { @@ -290,10 +293,12 @@ where /// let mut renderer = Renderer::new(); /// let mut window_size = Size::new(1024.0, 768.0); /// let mut cursor_position = Point::default(); + /// let mut clipboard = clipboard::Null; /// let mut events = Vec::new(); + /// let mut messages = Vec::new(); /// /// loop { - /// // Process system events... + /// // Obtain system events... /// /// let mut user_interface = UserInterface::build( /// counter.view(), @@ -302,11 +307,13 @@ where /// &mut renderer, /// ); /// - /// let messages = user_interface.update( + /// // Update the user interface + /// let event_statuses = user_interface.update( /// &events, /// cursor_position, - /// None, /// &renderer, + /// &mut clipboard, + /// &mut messages /// ); /// /// // Draw the user interface @@ -314,7 +321,7 @@ where /// /// cache = user_interface.into_cache(); /// - /// for message in messages { + /// for message in messages.drain(..) { /// counter.update(message); /// } /// @@ -327,6 +334,8 @@ where renderer: &mut Renderer, cursor_position: Point, ) -> Renderer::Output { + let viewport = Rectangle::with_size(self.bounds); + let overlay = if let Some(mut overlay) = self.root.overlay(Layout::new(&self.base.layout)) { @@ -365,6 +374,7 @@ where &Renderer::Defaults::default(), Layout::new(&self.base.layout), base_cursor, + &viewport, ); renderer.overlay( @@ -378,15 +388,28 @@ where &Renderer::Defaults::default(), Layout::new(&self.base.layout), cursor_position, + &viewport, ) } } + /// Relayouts and returns a new [`UserInterface`] using the provided + /// bounds. + pub fn relayout(self, bounds: Size, renderer: &mut Renderer) -> Self { + Self::build( + self.root, + bounds, + Cache { + base: self.base, + overlay: self.overlay, + bounds: self.bounds, + }, + renderer, + ) + } + /// Extract the [`Cache`] of the [`UserInterface`], consuming it in the /// process. - /// - /// [`Cache`]: struct.Cache.html - /// [`UserInterface`]: struct.UserInterface.html pub fn into_cache(self) -> Cache { Cache { base: self.base, @@ -427,8 +450,6 @@ struct Layer { } /// Reusable data of a specific [`UserInterface`]. -/// -/// [`UserInterface`]: struct.UserInterface.html #[derive(Debug, Clone)] pub struct Cache { base: Layer, @@ -441,9 +462,6 @@ impl Cache { /// /// You should use this to initialize a [`Cache`] before building your first /// [`UserInterface`]. - /// - /// [`Cache`]: struct.Cache.html - /// [`UserInterface`]: struct.UserInterface.html pub fn new() -> Cache { Cache { base: Layer { diff --git a/native/src/widget.rs b/native/src/widget.rs index a10281df..43c1b023 100644 --- a/native/src/widget.rs +++ b/native/src/widget.rs @@ -18,8 +18,7 @@ //! use iced_native::{button, Button, Widget}; //! ``` //! -//! [`Widget`]: trait.Widget.html -//! [renderer]: ../renderer/index.html +//! [renderer]: crate::renderer pub mod button; pub mod checkbox; pub mod column; @@ -37,6 +36,8 @@ pub mod space; pub mod svg; pub mod text; pub mod text_input; +pub mod toggler; +pub mod tooltip; #[doc(no_inline)] pub use button::Button; @@ -72,17 +73,21 @@ pub use svg::Svg; pub use text::Text; #[doc(no_inline)] pub use text_input::TextInput; +#[doc(no_inline)] +pub use toggler::Toggler; +#[doc(no_inline)] +pub use tooltip::Tooltip; -use crate::{layout, overlay, Clipboard, Event, Hasher, Layout, Length, Point}; +use crate::event::{self, Event}; +use crate::layout; +use crate::overlay; +use crate::{Clipboard, Hasher, Layout, Length, Point, Rectangle}; /// A component that displays information and allows interaction. /// /// If you want to build your own widgets, you will need to implement this /// trait. /// -/// [`Widget`]: trait.Widget.html -/// [`Element`]: ../struct.Element.html -/// /// # Examples /// The repository has some [examples] showcasing how to implement a custom /// widget: @@ -94,24 +99,20 @@ use crate::{layout, overlay, Clipboard, Event, Hasher, Layout, Length, Point}; /// - [`geometry`], a custom widget showcasing how to draw geometry with the /// `Mesh2D` primitive in [`iced_wgpu`]. /// -/// [examples]: https://github.com/hecrj/iced/tree/0.1/examples -/// [`bezier_tool`]: https://github.com/hecrj/iced/tree/0.1/examples/bezier_tool -/// [`custom_widget`]: https://github.com/hecrj/iced/tree/0.1/examples/custom_widget -/// [`geometry`]: https://github.com/hecrj/iced/tree/0.1/examples/geometry +/// [examples]: https://github.com/hecrj/iced/tree/0.3/examples +/// [`bezier_tool`]: https://github.com/hecrj/iced/tree/0.3/examples/bezier_tool +/// [`custom_widget`]: https://github.com/hecrj/iced/tree/0.3/examples/custom_widget +/// [`geometry`]: https://github.com/hecrj/iced/tree/0.3/examples/geometry /// [`lyon`]: https://github.com/nical/lyon -/// [`iced_wgpu`]: https://github.com/hecrj/iced/tree/0.1/wgpu +/// [`iced_wgpu`]: https://github.com/hecrj/iced/tree/0.3/wgpu pub trait Widget<Message, Renderer> where Renderer: crate::Renderer, { /// Returns the width of the [`Widget`]. - /// - /// [`Widget`]: trait.Widget.html fn width(&self) -> Length; /// Returns the height of the [`Widget`]. - /// - /// [`Widget`]: trait.Widget.html fn height(&self) -> Length; /// Returns the [`Node`] of the [`Widget`]. @@ -119,9 +120,7 @@ where /// This [`Node`] is used by the runtime to compute the [`Layout`] of the /// user interface. /// - /// [`Node`]: ../layout/struct.Node.html - /// [`Widget`]: trait.Widget.html - /// [`Layout`]: ../layout/struct.Layout.html + /// [`Node`]: layout::Node fn layout( &self, renderer: &Renderer, @@ -129,14 +128,13 @@ where ) -> layout::Node; /// Draws the [`Widget`] using the associated `Renderer`. - /// - /// [`Widget`]: trait.Widget.html fn draw( &self, renderer: &mut Renderer, defaults: &Renderer::Defaults, layout: Layout<'_>, cursor_position: Point, + viewport: &Rectangle, ) -> Renderer::Output; /// Computes the _layout_ hash of the [`Widget`]. @@ -149,9 +147,7 @@ where /// For example, the [`Text`] widget does not hash its color property, as /// its value cannot affect the overall [`Layout`] of the user interface. /// - /// [`Widget`]: trait.Widget.html - /// [`Layout`]: ../layout/struct.Layout.html - /// [`Text`]: text/struct.Text.html + /// [`Text`]: crate::widget::Text fn hash_layout(&self, state: &mut Hasher); /// Processes a runtime [`Event`]. @@ -166,25 +162,19 @@ where /// * a [`Clipboard`], if available /// /// By default, it does nothing. - /// - /// [`Event`]: ../enum.Event.html - /// [`Widget`]: trait.Widget.html - /// [`Layout`]: ../layout/struct.Layout.html - /// [`Clipboard`]: ../trait.Clipboard.html fn on_event( &mut self, _event: Event, _layout: Layout<'_>, _cursor_position: Point, - _messages: &mut Vec<Message>, _renderer: &Renderer, - _clipboard: Option<&dyn Clipboard>, - ) { + _clipboard: &mut dyn Clipboard, + _messages: &mut Vec<Message>, + ) -> event::Status { + event::Status::Ignored } - /// Returns the overlay of the [`Element`], if there is any. - /// - /// [`Element`]: struct.Element.html + /// Returns the overlay of the [`Widget`], if there is any. fn overlay( &mut self, _layout: Layout<'_>, diff --git a/native/src/widget/button.rs b/native/src/widget/button.rs index c932da2b..c469a0e5 100644 --- a/native/src/widget/button.rs +++ b/native/src/widget/button.rs @@ -1,12 +1,14 @@ //! Allow your users to perform actions by pressing a button. //! //! A [`Button`] has some local [`State`]. -//! -//! [`Button`]: struct.Button.html -//! [`State`]: struct.State.html +use crate::event::{self, Event}; +use crate::layout; +use crate::mouse; +use crate::overlay; +use crate::touch; use crate::{ - layout, mouse, Clipboard, Element, Event, Hasher, Layout, Length, Point, - Rectangle, Widget, + Clipboard, Element, Hasher, Layout, Length, Padding, Point, Rectangle, + Widget, }; use std::hash::Hash; @@ -18,6 +20,7 @@ use std::hash::Hash; /// # type Button<'a, Message> = /// # iced_native::Button<'a, Message, iced_native::renderer::Null>; /// # +/// #[derive(Clone)] /// enum Message { /// ButtonPressed, /// } @@ -26,6 +29,29 @@ use std::hash::Hash; /// let button = Button::new(&mut state, Text::new("Press me!")) /// .on_press(Message::ButtonPressed); /// ``` +/// +/// If a [`Button::on_press`] handler is not set, the resulting [`Button`] will +/// be disabled: +/// +/// ``` +/// # use iced_native::{button, Text}; +/// # +/// # type Button<'a, Message> = +/// # iced_native::Button<'a, Message, iced_native::renderer::Null>; +/// # +/// #[derive(Clone)] +/// enum Message { +/// ButtonPressed, +/// } +/// +/// fn disabled_button(state: &mut button::State) -> Button<'_, Message> { +/// Button::new(state, Text::new("I'm disabled!")) +/// } +/// +/// fn enabled_button(state: &mut button::State) -> Button<'_, Message> { +/// disabled_button(state).on_press(Message::ButtonPressed) +/// } +/// ``` #[allow(missing_debug_implementations)] pub struct Button<'a, Message, Renderer: self::Renderer> { state: &'a mut State, @@ -35,19 +61,17 @@ pub struct Button<'a, Message, Renderer: self::Renderer> { height: Length, min_width: u32, min_height: u32, - padding: u16, + padding: Padding, style: Renderer::Style, } impl<'a, Message, Renderer> Button<'a, Message, Renderer> where + Message: Clone, Renderer: self::Renderer, { /// Creates a new [`Button`] with some local [`State`] and the given /// content. - /// - /// [`Button`]: struct.Button.html - /// [`State`]: struct.State.html pub fn new<E>(state: &'a mut State, content: E) -> Self where E: Into<Element<'a, Message, Renderer>>, @@ -66,56 +90,43 @@ where } /// Sets the width of the [`Button`]. - /// - /// [`Button`]: struct.Button.html pub fn width(mut self, width: Length) -> Self { self.width = width; self } /// Sets the height of the [`Button`]. - /// - /// [`Button`]: struct.Button.html pub fn height(mut self, height: Length) -> Self { self.height = height; self } /// Sets the minimum width of the [`Button`]. - /// - /// [`Button`]: struct.Button.html pub fn min_width(mut self, min_width: u32) -> Self { self.min_width = min_width; self } /// Sets the minimum height of the [`Button`]. - /// - /// [`Button`]: struct.Button.html pub fn min_height(mut self, min_height: u32) -> Self { self.min_height = min_height; self } - /// Sets the padding of the [`Button`]. - /// - /// [`Button`]: struct.Button.html - pub fn padding(mut self, padding: u16) -> Self { - self.padding = padding; + /// Sets the [`Padding`] of the [`Button`]. + pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self { + self.padding = padding.into(); self } /// Sets the message that will be produced when the [`Button`] is pressed. - /// - /// [`Button`]: struct.Button.html + /// If on_press isn't set, button will be disabled. pub fn on_press(mut self, msg: Message) -> Self { self.on_press = Some(msg); self } /// Sets the style of the [`Button`]. - /// - /// [`Button`]: struct.Button.html pub fn style(mut self, style: impl Into<Renderer::Style>) -> Self { self.style = style.into(); self @@ -123,8 +134,6 @@ where } /// The local state of a [`Button`]. -/// -/// [`Button`]: struct.Button.html #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub struct State { is_pressed: bool, @@ -132,8 +141,6 @@ pub struct State { impl State { /// Creates a new [`State`]. - /// - /// [`State`]: struct.State.html pub fn new() -> State { State::default() } @@ -142,8 +149,8 @@ impl State { impl<'a, Message, Renderer> Widget<Message, Renderer> for Button<'a, Message, Renderer> where - Renderer: self::Renderer, Message: Clone, + Renderer: self::Renderer, { fn width(&self) -> Length { self.width @@ -158,18 +165,20 @@ where renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { - let padding = f32::from(self.padding); let limits = limits .min_width(self.min_width) .min_height(self.min_height) .width(self.width) .height(self.height) - .pad(padding); + .pad(self.padding); let mut content = self.content.layout(renderer, &limits); - content.move_to(Point::new(padding, padding)); + content.move_to(Point::new( + self.padding.left.into(), + self.padding.top.into(), + )); - let size = limits.resolve(content.size()).pad(padding); + let size = limits.resolve(content.size()).pad(self.padding); layout::Node::with_children(size, vec![content]) } @@ -179,34 +188,57 @@ where event: Event, layout: Layout<'_>, cursor_position: Point, + renderer: &Renderer, + clipboard: &mut dyn Clipboard, messages: &mut Vec<Message>, - _renderer: &Renderer, - _clipboard: Option<&dyn Clipboard>, - ) { + ) -> event::Status { + if let event::Status::Captured = self.content.on_event( + event.clone(), + layout.children().next().unwrap(), + cursor_position, + renderer, + clipboard, + messages, + ) { + return event::Status::Captured; + } + match event { - Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => { + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerPressed { .. }) => { if self.on_press.is_some() { let bounds = layout.bounds(); - self.state.is_pressed = bounds.contains(cursor_position); + if bounds.contains(cursor_position) { + self.state.is_pressed = true; + + return event::Status::Captured; + } } } - Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) => { + Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerLifted { .. }) => { if let Some(on_press) = self.on_press.clone() { let bounds = layout.bounds(); - let is_clicked = self.state.is_pressed - && bounds.contains(cursor_position); + if self.state.is_pressed { + self.state.is_pressed = false; - self.state.is_pressed = false; + if bounds.contains(cursor_position) { + messages.push(on_press); + } - if is_clicked { - messages.push(on_press); + return event::Status::Captured; } } } + Event::Touch(touch::Event::FingerLost { .. }) => { + self.state.is_pressed = false; + } _ => {} } + + event::Status::Ignored } fn draw( @@ -215,6 +247,7 @@ where defaults: &Renderer::Defaults, layout: Layout<'_>, cursor_position: Point, + _viewport: &Rectangle, ) -> Renderer::Output { renderer.draw( defaults, @@ -235,6 +268,13 @@ where self.width.hash(state); self.content.hash_layout(state); } + + fn overlay( + &mut self, + layout: Layout<'_>, + ) -> Option<overlay::Element<'_, Message, Renderer>> { + self.content.overlay(layout.children().next().unwrap()) + } } /// The renderer of a [`Button`]. @@ -242,20 +282,15 @@ where /// Your [renderer] will need to implement this trait before being /// able to use a [`Button`] in your user interface. /// -/// [`Button`]: struct.Button.html -/// [renderer]: ../../renderer/index.html +/// [renderer]: crate::renderer pub trait Renderer: crate::Renderer + Sized { /// The default padding of a [`Button`]. - /// - /// [`Button`]: struct.Button.html - const DEFAULT_PADDING: u16; + const DEFAULT_PADDING: Padding; /// The style supported by this renderer. type Style: Default; /// Draws a [`Button`]. - /// - /// [`Button`]: struct.Button.html fn draw<Message>( &mut self, defaults: &Self::Defaults, @@ -272,8 +307,8 @@ pub trait Renderer: crate::Renderer + Sized { impl<'a, Message, Renderer> From<Button<'a, Message, Renderer>> for Element<'a, Message, Renderer> where - Renderer: 'a + self::Renderer, Message: 'a + Clone, + Renderer: 'a + self::Renderer, { fn from( button: Button<'a, Message, Renderer>, diff --git a/native/src/widget/checkbox.rs b/native/src/widget/checkbox.rs index 99178aae..0f21c873 100644 --- a/native/src/widget/checkbox.rs +++ b/native/src/widget/checkbox.rs @@ -1,10 +1,15 @@ //! Show toggle controls using checkboxes. use std::hash::Hash; +use crate::event::{self, Event}; +use crate::layout; +use crate::mouse; +use crate::row; +use crate::text; +use crate::touch; use crate::{ - layout, mouse, row, text, Align, Clipboard, Element, Event, Hasher, - HorizontalAlignment, Layout, Length, Point, Rectangle, Row, Text, - VerticalAlignment, Widget, + Align, Clipboard, Color, Element, Hasher, HorizontalAlignment, Layout, + Length, Point, Rectangle, Row, Text, VerticalAlignment, Widget, }; /// A box that can be checked. @@ -34,6 +39,7 @@ pub struct Checkbox<Message, Renderer: self::Renderer + text::Renderer> { spacing: u16, text_size: Option<u16>, font: Renderer::Font, + text_color: Option<Color>, style: Renderer::Style, } @@ -48,8 +54,6 @@ impl<Message, Renderer: self::Renderer + text::Renderer> /// * a function that will be called when the [`Checkbox`] is toggled. It /// will receive the new state of the [`Checkbox`] and must produce a /// `Message`. - /// - /// [`Checkbox`]: struct.Checkbox.html pub fn new<F>(is_checked: bool, label: impl Into<String>, f: F) -> Self where F: 'static + Fn(bool) -> Message, @@ -63,37 +67,30 @@ impl<Message, Renderer: self::Renderer + text::Renderer> spacing: Renderer::DEFAULT_SPACING, text_size: None, font: Renderer::Font::default(), + text_color: None, style: Renderer::Style::default(), } } /// Sets the size of the [`Checkbox`]. - /// - /// [`Checkbox`]: struct.Checkbox.html pub fn size(mut self, size: u16) -> Self { self.size = size; self } /// Sets the width of the [`Checkbox`]. - /// - /// [`Checkbox`]: struct.Checkbox.html pub fn width(mut self, width: Length) -> Self { self.width = width; self } /// Sets the spacing between the [`Checkbox`] and the text. - /// - /// [`Checkbox`]: struct.Checkbox.html pub fn spacing(mut self, spacing: u16) -> Self { self.spacing = spacing; self } /// Sets the text size of the [`Checkbox`]. - /// - /// [`Checkbox`]: struct.Checkbox.html pub fn text_size(mut self, text_size: u16) -> Self { self.text_size = Some(text_size); self @@ -101,16 +98,19 @@ impl<Message, Renderer: self::Renderer + text::Renderer> /// Sets the [`Font`] of the text of the [`Checkbox`]. /// - /// [`Checkbox`]: struct.Checkbox.html - /// [`Font`]: ../../struct.Font.html + /// [`Font`]: crate::widget::text::Renderer::Font pub fn font(mut self, font: Renderer::Font) -> Self { self.font = font; self } + /// Sets the text color of the [`Checkbox`] button. + pub fn text_color(mut self, color: Color) -> Self { + self.text_color = Some(color); + self + } + /// Sets the style of the [`Checkbox`]. - /// - /// [`Checkbox`]: struct.Checkbox.html pub fn style(mut self, style: impl Into<Renderer::Style>) -> Self { self.style = style.into(); self @@ -158,20 +158,25 @@ where event: Event, layout: Layout<'_>, cursor_position: Point, - messages: &mut Vec<Message>, _renderer: &Renderer, - _clipboard: Option<&dyn Clipboard>, - ) { + _clipboard: &mut dyn Clipboard, + messages: &mut Vec<Message>, + ) -> event::Status { match event { - Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => { + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerPressed { .. }) => { let mouse_over = layout.bounds().contains(cursor_position); if mouse_over { messages.push((self.on_toggle)(!self.is_checked)); + + return event::Status::Captured; } } _ => {} } + + event::Status::Ignored } fn draw( @@ -180,6 +185,7 @@ where defaults: &Renderer::Defaults, layout: Layout<'_>, cursor_position: Point, + _viewport: &Rectangle, ) -> Renderer::Output { let bounds = layout.bounds(); let mut children = layout.children(); @@ -195,7 +201,7 @@ where &self.label, self.text_size.unwrap_or(renderer.default_size()), self.font, - None, + self.text_color, HorizontalAlignment::Left, VerticalAlignment::Center, ); @@ -225,20 +231,15 @@ where /// Your [renderer] will need to implement this trait before being /// able to use a [`Checkbox`] in your user interface. /// -/// [`Checkbox`]: struct.Checkbox.html -/// [renderer]: ../../renderer/index.html +/// [renderer]: crate::Renderer pub trait Renderer: crate::Renderer { /// The style supported by this renderer. type Style: Default; /// The default size of a [`Checkbox`]. - /// - /// [`Checkbox`]: struct.Checkbox.html const DEFAULT_SIZE: u16; /// The default spacing of a [`Checkbox`]. - /// - /// [`Checkbox`]: struct.Checkbox.html const DEFAULT_SPACING: u16; /// Draws a [`Checkbox`]. @@ -248,8 +249,6 @@ pub trait Renderer: crate::Renderer { /// * whether the [`Checkbox`] is selected or not /// * whether the mouse is over the [`Checkbox`] or not /// * the drawn label of the [`Checkbox`] - /// - /// [`Checkbox`]: struct.Checkbox.html fn draw( &mut self, bounds: Rectangle, diff --git a/native/src/widget/column.rs b/native/src/widget/column.rs index 42cfe9b9..52a2e80c 100644 --- a/native/src/widget/column.rs +++ b/native/src/widget/column.rs @@ -1,22 +1,21 @@ //! Distribute content vertically. use std::hash::Hash; +use crate::event::{self, Event}; +use crate::layout; +use crate::overlay; use crate::{ - layout, overlay, Align, Clipboard, Element, Event, Hasher, Layout, Length, - Point, Widget, + Align, Clipboard, Element, Hasher, Layout, Length, Padding, Point, + Rectangle, Widget, }; use std::u32; /// A container that distributes its contents vertically. -/// -/// A [`Column`] will try to fill the horizontal space of its container. -/// -/// [`Column`]: struct.Column.html #[allow(missing_debug_implementations)] pub struct Column<'a, Message, Renderer> { spacing: u16, - padding: u16, + padding: Padding, width: Length, height: Length, max_width: u32, @@ -27,21 +26,17 @@ pub struct Column<'a, Message, Renderer> { impl<'a, Message, Renderer> Column<'a, Message, Renderer> { /// Creates an empty [`Column`]. - /// - /// [`Column`]: struct.Column.html pub fn new() -> Self { Self::with_children(Vec::new()) } /// Creates a [`Column`] with the given elements. - /// - /// [`Column`]: struct.Column.html pub fn with_children( children: Vec<Element<'a, Message, Renderer>>, ) -> Self { Column { spacing: 0, - padding: 0, + padding: Padding::ZERO, width: Length::Shrink, height: Length::Shrink, max_width: u32::MAX, @@ -61,57 +56,43 @@ impl<'a, Message, Renderer> Column<'a, Message, Renderer> { self } - /// Sets the padding of the [`Column`]. - /// - /// [`Column`]: struct.Column.html - pub fn padding(mut self, units: u16) -> Self { - self.padding = units; + /// Sets the [`Padding`] of the [`Column`]. + pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self { + self.padding = padding.into(); self } /// Sets the width of the [`Column`]. - /// - /// [`Column`]: struct.Column.html pub fn width(mut self, width: Length) -> Self { self.width = width; self } /// Sets the height of the [`Column`]. - /// - /// [`Column`]: struct.Column.html pub fn height(mut self, height: Length) -> Self { self.height = height; self } /// Sets the maximum width of the [`Column`]. - /// - /// [`Column`]: struct.Column.html pub fn max_width(mut self, max_width: u32) -> Self { self.max_width = max_width; self } /// Sets the maximum height of the [`Column`] in pixels. - /// - /// [`Column`]: struct.Column.html pub fn max_height(mut self, max_height: u32) -> Self { self.max_height = max_height; self } /// Sets the horizontal alignment of the contents of the [`Column`] . - /// - /// [`Column`]: struct.Column.html pub fn align_items(mut self, align: Align) -> Self { self.align_items = align; self } /// Adds an element to the [`Column`]. - /// - /// [`Column`]: struct.Column.html pub fn push<E>(mut self, child: E) -> Self where E: Into<Element<'a, Message, Renderer>>, @@ -149,7 +130,7 @@ where layout::flex::Axis::Vertical, renderer, &limits, - self.padding as f32, + self.padding, self.spacing as f32, self.align_items, &self.children, @@ -161,22 +142,24 @@ where event: Event, layout: Layout<'_>, cursor_position: Point, - messages: &mut Vec<Message>, renderer: &Renderer, - clipboard: Option<&dyn Clipboard>, - ) { - self.children.iter_mut().zip(layout.children()).for_each( - |(child, layout)| { + clipboard: &mut dyn Clipboard, + messages: &mut Vec<Message>, + ) -> event::Status { + self.children + .iter_mut() + .zip(layout.children()) + .map(|(child, layout)| { child.widget.on_event( event.clone(), layout, cursor_position, - messages, renderer, clipboard, + messages, ) - }, - ); + }) + .fold(event::Status::Ignored, event::Status::merge) } fn draw( @@ -185,8 +168,15 @@ where defaults: &Renderer::Defaults, layout: Layout<'_>, cursor_position: Point, + viewport: &Rectangle, ) -> Renderer::Output { - renderer.draw(defaults, &self.children, layout, cursor_position) + renderer.draw( + defaults, + &self.children, + layout, + cursor_position, + viewport, + ) } fn hash_layout(&self, state: &mut Hasher) { @@ -199,6 +189,7 @@ where self.max_height.hash(state); self.align_items.hash(state); self.spacing.hash(state); + self.padding.hash(state); for child in &self.children { child.widget.hash_layout(state); @@ -222,8 +213,7 @@ where /// Your [renderer] will need to implement this trait before being /// able to use a [`Column`] in your user interface. /// -/// [`Column`]: struct.Column.html -/// [renderer]: ../../renderer/index.html +/// [renderer]: crate::renderer pub trait Renderer: crate::Renderer + Sized { /// Draws a [`Column`]. /// @@ -231,15 +221,13 @@ pub trait Renderer: crate::Renderer + Sized { /// - the children of the [`Column`] /// - the [`Layout`] of the [`Column`] and its children /// - the cursor position - /// - /// [`Column`]: struct.Column.html - /// [`Layout`]: ../layout/struct.Layout.html fn draw<Message>( &mut self, defaults: &Self::Defaults, content: &[Element<'_, Message, Self>], layout: Layout<'_>, cursor_position: Point, + viewport: &Rectangle, ) -> Self::Output; } diff --git a/native/src/widget/container.rs b/native/src/widget/container.rs index b8316e62..69aee64d 100644 --- a/native/src/widget/container.rs +++ b/native/src/widget/container.rs @@ -1,9 +1,12 @@ //! Decorate content and apply alignment. use std::hash::Hash; +use crate::event::{self, Event}; +use crate::layout; +use crate::overlay; use crate::{ - layout, overlay, Align, Clipboard, Element, Event, Hasher, Layout, Length, - Point, Rectangle, Widget, + Align, Clipboard, Element, Hasher, Layout, Length, Padding, Point, + Rectangle, Widget, }; use std::u32; @@ -13,7 +16,7 @@ use std::u32; /// It is normally used for alignment purposes. #[allow(missing_debug_implementations)] pub struct Container<'a, Message, Renderer: self::Renderer> { - padding: u16, + padding: Padding, width: Length, height: Length, max_width: u32, @@ -29,14 +32,12 @@ where Renderer: self::Renderer, { /// Creates an empty [`Container`]. - /// - /// [`Container`]: struct.Container.html pub fn new<T>(content: T) -> Self where T: Into<Element<'a, Message, Renderer>>, { Container { - padding: 0, + padding: Padding::ZERO, width: Length::Shrink, height: Length::Shrink, max_width: u32::MAX, @@ -48,81 +49,61 @@ where } } - /// Sets the padding of the [`Container`]. - /// - /// [`Container`]: struct.Column.html - pub fn padding(mut self, units: u16) -> Self { - self.padding = units; + /// Sets the [`Padding`] of the [`Container`]. + pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self { + self.padding = padding.into(); self } /// Sets the width of the [`Container`]. - /// - /// [`Container`]: struct.Container.html pub fn width(mut self, width: Length) -> Self { self.width = width; self } /// Sets the height of the [`Container`]. - /// - /// [`Container`]: struct.Container.html pub fn height(mut self, height: Length) -> Self { self.height = height; self } /// Sets the maximum width of the [`Container`]. - /// - /// [`Container`]: struct.Container.html pub fn max_width(mut self, max_width: u32) -> Self { self.max_width = max_width; self } /// Sets the maximum height of the [`Container`] in pixels. - /// - /// [`Container`]: struct.Container.html pub fn max_height(mut self, max_height: u32) -> Self { self.max_height = max_height; self } /// Sets the content alignment for the horizontal axis of the [`Container`]. - /// - /// [`Container`]: struct.Container.html pub fn align_x(mut self, alignment: Align) -> Self { self.horizontal_alignment = alignment; self } /// Sets the content alignment for the vertical axis of the [`Container`]. - /// - /// [`Container`]: struct.Container.html pub fn align_y(mut self, alignment: Align) -> Self { self.vertical_alignment = alignment; self } /// Centers the contents in the horizontal axis of the [`Container`]. - /// - /// [`Container`]: struct.Container.html pub fn center_x(mut self) -> Self { self.horizontal_alignment = Align::Center; self } /// Centers the contents in the vertical axis of the [`Container`]. - /// - /// [`Container`]: struct.Container.html pub fn center_y(mut self) -> Self { self.vertical_alignment = Align::Center; self } /// Sets the style of the [`Container`]. - /// - /// [`Container`]: struct.Container.html pub fn style(mut self, style: impl Into<Renderer::Style>) -> Self { self.style = style.into(); self @@ -147,23 +128,24 @@ where renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { - let padding = f32::from(self.padding); - let limits = limits .loose() .max_width(self.max_width) .max_height(self.max_height) .width(self.width) .height(self.height) - .pad(padding); + .pad(self.padding); let mut content = self.content.layout(renderer, &limits.loose()); let size = limits.resolve(content.size()); - content.move_to(Point::new(padding, padding)); + content.move_to(Point::new( + self.padding.left.into(), + self.padding.top.into(), + )); content.align(self.horizontal_alignment, self.vertical_alignment, size); - layout::Node::with_children(size.pad(padding), vec![content]) + layout::Node::with_children(size.pad(self.padding), vec![content]) } fn on_event( @@ -171,17 +153,17 @@ where event: Event, layout: Layout<'_>, cursor_position: Point, - messages: &mut Vec<Message>, renderer: &Renderer, - clipboard: Option<&dyn Clipboard>, - ) { + clipboard: &mut dyn Clipboard, + messages: &mut Vec<Message>, + ) -> event::Status { self.content.widget.on_event( event, layout.children().next().unwrap(), cursor_position, - messages, renderer, clipboard, + messages, ) } @@ -191,11 +173,13 @@ where defaults: &Renderer::Defaults, layout: Layout<'_>, cursor_position: Point, + viewport: &Rectangle, ) -> Renderer::Output { renderer.draw( defaults, layout.bounds(), cursor_position, + viewport, &self.style, &self.content, layout.children().next().unwrap(), @@ -228,20 +212,18 @@ where /// Your [renderer] will need to implement this trait before being /// able to use a [`Container`] in your user interface. /// -/// [`Container`]: struct.Container.html -/// [renderer]: ../../renderer/index.html +/// [renderer]: crate::renderer pub trait Renderer: crate::Renderer { /// The style supported by this renderer. type Style: Default; /// Draws a [`Container`]. - /// - /// [`Container`]: struct.Container.html fn draw<Message>( &mut self, defaults: &Self::Defaults, bounds: Rectangle, cursor_position: Point, + viewport: &Rectangle, style: &Self::Style, content: &Element<'_, Message, Self>, content_layout: Layout<'_>, diff --git a/native/src/widget/image.rs b/native/src/widget/image.rs index 132f249d..4d8e0a3f 100644 --- a/native/src/widget/image.rs +++ b/native/src/widget/image.rs @@ -1,5 +1,9 @@ //! Display images in your user interface. -use crate::{layout, Element, Hasher, Layout, Length, Point, Size, Widget}; +pub mod viewer; +pub use viewer::Viewer; + +use crate::layout; +use crate::{Element, Hasher, Layout, Length, Point, Rectangle, Size, Widget}; use std::{ hash::{Hash, Hasher as _}, @@ -27,8 +31,6 @@ pub struct Image { impl Image { /// Creates a new [`Image`] with the given path. - /// - /// [`Image`]: struct.Image.html pub fn new<T: Into<Handle>>(handle: T) -> Self { Image { handle: handle.into(), @@ -38,16 +40,12 @@ impl Image { } /// Sets the width of the [`Image`] boundaries. - /// - /// [`Image`]: struct.Image.html pub fn width(mut self, width: Length) -> Self { self.width = width; self } /// Sets the height of the [`Image`] boundaries. - /// - /// [`Image`]: struct.Image.html pub fn height(mut self, height: Length) -> Self { self.height = height; self @@ -97,6 +95,7 @@ where _defaults: &Renderer::Defaults, layout: Layout<'_>, _cursor_position: Point, + _viewport: &Rectangle, ) -> Renderer::Output { renderer.draw(self.handle.clone(), layout) } @@ -112,8 +111,6 @@ where } /// An [`Image`] handle. -/// -/// [`Image`]: struct.Image.html #[derive(Debug, Clone)] pub struct Handle { id: u64, @@ -124,8 +121,6 @@ impl Handle { /// Creates an image [`Handle`] pointing to the image of the given path. /// /// Makes an educated guess about the image format by examining the data in the file. - /// - /// [`Handle`]: struct.Handle.html pub fn from_path<T: Into<PathBuf>>(path: T) -> Handle { Self::from_data(Data::Path(path.into())) } @@ -135,8 +130,6 @@ impl Handle { /// pixels. /// /// This is useful if you have already decoded your image. - /// - /// [`Handle`]: struct.Handle.html pub fn from_pixels(width: u32, height: u32, pixels: Vec<u8>) -> Handle { Self::from_data(Data::Pixels { width, @@ -151,8 +144,6 @@ impl Handle { /// /// This is useful if you already have your image loaded in-memory, maybe /// because you downloaded or generated it procedurally. - /// - /// [`Handle`]: struct.Handle.html pub fn from_memory(bytes: Vec<u8>) -> Handle { Self::from_data(Data::Bytes(bytes)) } @@ -168,15 +159,11 @@ impl Handle { } /// Returns the unique identifier of the [`Handle`]. - /// - /// [`Handle`]: struct.Handle.html pub fn id(&self) -> u64 { self.id } /// Returns a reference to the image [`Data`]. - /// - /// [`Data`]: enum.Data.html pub fn data(&self) -> &Data { &self.data } @@ -198,8 +185,6 @@ impl Hash for Handle { } /// The data of an [`Image`]. -/// -/// [`Image`]: struct.Image.html #[derive(Clone, Hash)] pub enum Data { /// File data @@ -236,17 +221,12 @@ impl std::fmt::Debug for Data { /// Your [renderer] will need to implement this trait before being able to use /// an [`Image`] in your user interface. /// -/// [`Image`]: struct.Image.html -/// [renderer]: ../../renderer/index.html +/// [renderer]: crate::renderer pub trait Renderer: crate::Renderer { /// Returns the dimensions of an [`Image`] located on the given path. - /// - /// [`Image`]: struct.Image.html fn dimensions(&self, handle: &Handle) -> (u32, u32); /// Draws an [`Image`]. - /// - /// [`Image`]: struct.Image.html fn draw(&mut self, handle: Handle, layout: Layout<'_>) -> Self::Output; } diff --git a/native/src/widget/image/viewer.rs b/native/src/widget/image/viewer.rs new file mode 100644 index 00000000..405daf00 --- /dev/null +++ b/native/src/widget/image/viewer.rs @@ -0,0 +1,413 @@ +//! Zoom and pan on an image. +use crate::event::{self, Event}; +use crate::image; +use crate::layout; +use crate::mouse; +use crate::{ + Clipboard, Element, Hasher, Layout, Length, Point, Rectangle, Size, Vector, + Widget, +}; + +use std::hash::Hash; + +/// A frame that displays an image with the ability to zoom in/out and pan. +#[allow(missing_debug_implementations)] +pub struct Viewer<'a> { + state: &'a mut State, + padding: u16, + width: Length, + height: Length, + min_scale: f32, + max_scale: f32, + scale_step: f32, + handle: image::Handle, +} + +impl<'a> Viewer<'a> { + /// Creates a new [`Viewer`] with the given [`State`] and [`Handle`]. + /// + /// [`Handle`]: image::Handle + pub fn new(state: &'a mut State, handle: image::Handle) -> Self { + Viewer { + state, + padding: 0, + width: Length::Shrink, + height: Length::Shrink, + min_scale: 0.25, + max_scale: 10.0, + scale_step: 0.10, + handle, + } + } + + /// Sets the padding of the [`Viewer`]. + pub fn padding(mut self, units: u16) -> Self { + self.padding = units; + self + } + + /// Sets the width of the [`Viewer`]. + pub fn width(mut self, width: Length) -> Self { + self.width = width; + self + } + + /// Sets the height of the [`Viewer`]. + pub fn height(mut self, height: Length) -> Self { + self.height = height; + self + } + + /// Sets the max scale applied to the image of the [`Viewer`]. + /// + /// Default is `10.0` + pub fn max_scale(mut self, max_scale: f32) -> Self { + self.max_scale = max_scale; + self + } + + /// Sets the min scale applied to the image of the [`Viewer`]. + /// + /// Default is `0.25` + pub fn min_scale(mut self, min_scale: f32) -> Self { + self.min_scale = min_scale; + self + } + + /// Sets the percentage the image of the [`Viewer`] will be scaled by + /// when zoomed in / out. + /// + /// Default is `0.10` + pub fn scale_step(mut self, scale_step: f32) -> Self { + self.scale_step = scale_step; + self + } + + /// Returns the bounds of the underlying image, given the bounds of + /// the [`Viewer`]. Scaling will be applied and original aspect ratio + /// will be respected. + fn image_size<Renderer>(&self, renderer: &Renderer, bounds: Size) -> Size + where + Renderer: self::Renderer + image::Renderer, + { + let (width, height) = renderer.dimensions(&self.handle); + + let (width, height) = { + let dimensions = (width as f32, height as f32); + + let width_ratio = bounds.width / dimensions.0; + let height_ratio = bounds.height / dimensions.1; + + let ratio = width_ratio.min(height_ratio); + + let scale = self.state.scale; + + if ratio < 1.0 { + (dimensions.0 * ratio * scale, dimensions.1 * ratio * scale) + } else { + (dimensions.0 * scale, dimensions.1 * scale) + } + }; + + Size::new(width, height) + } +} + +impl<'a, Message, Renderer> Widget<Message, Renderer> for Viewer<'a> +where + Renderer: self::Renderer + image::Renderer, +{ + fn width(&self) -> Length { + self.width + } + + fn height(&self) -> Length { + self.height + } + + fn layout( + &self, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + let (width, height) = renderer.dimensions(&self.handle); + + let mut size = limits + .width(self.width) + .height(self.height) + .resolve(Size::new(width as f32, height as f32)); + + let expansion_size = if height > width { + self.width + } else { + self.height + }; + + // Only calculate viewport sizes if the images are constrained to a limited space. + // If they are Fill|Portion let them expand within their alotted space. + match expansion_size { + Length::Shrink | Length::Units(_) => { + let aspect_ratio = width as f32 / height as f32; + let viewport_aspect_ratio = size.width / size.height; + if viewport_aspect_ratio > aspect_ratio { + size.width = width as f32 * size.height / height as f32; + } else { + size.height = height as f32 * size.width / width as f32; + } + } + Length::Fill | Length::FillPortion(_) => {} + } + + layout::Node::new(size) + } + + fn on_event( + &mut self, + event: Event, + layout: Layout<'_>, + cursor_position: Point, + renderer: &Renderer, + _clipboard: &mut dyn Clipboard, + _messages: &mut Vec<Message>, + ) -> event::Status { + let bounds = layout.bounds(); + let is_mouse_over = bounds.contains(cursor_position); + + match event { + Event::Mouse(mouse::Event::WheelScrolled { delta }) + if is_mouse_over => + { + match delta { + mouse::ScrollDelta::Lines { y, .. } + | mouse::ScrollDelta::Pixels { y, .. } => { + let previous_scale = self.state.scale; + + if y < 0.0 && previous_scale > self.min_scale + || y > 0.0 && previous_scale < self.max_scale + { + self.state.scale = (if y > 0.0 { + self.state.scale * (1.0 + self.scale_step) + } else { + self.state.scale / (1.0 + self.scale_step) + }) + .max(self.min_scale) + .min(self.max_scale); + + let image_size = + self.image_size(renderer, bounds.size()); + + let factor = + self.state.scale / previous_scale - 1.0; + + let cursor_to_center = + cursor_position - bounds.center(); + + let adjustment = cursor_to_center * factor + + self.state.current_offset * factor; + + self.state.current_offset = Vector::new( + if image_size.width > bounds.width { + self.state.current_offset.x + adjustment.x + } else { + 0.0 + }, + if image_size.height > bounds.height { + self.state.current_offset.y + adjustment.y + } else { + 0.0 + }, + ); + } + } + } + + event::Status::Captured + } + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) + if is_mouse_over => + { + self.state.cursor_grabbed_at = Some(cursor_position); + self.state.starting_offset = self.state.current_offset; + + event::Status::Captured + } + Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) + if self.state.cursor_grabbed_at.is_some() => + { + self.state.cursor_grabbed_at = None; + + event::Status::Captured + } + Event::Mouse(mouse::Event::CursorMoved { position }) => { + if let Some(origin) = self.state.cursor_grabbed_at { + let image_size = self.image_size(renderer, bounds.size()); + + let hidden_width = (image_size.width - bounds.width / 2.0) + .max(0.0) + .round(); + + let hidden_height = (image_size.height + - bounds.height / 2.0) + .max(0.0) + .round(); + + let delta = position - origin; + + let x = if bounds.width < image_size.width { + (self.state.starting_offset.x - delta.x) + .min(hidden_width) + .max(-hidden_width) + } else { + 0.0 + }; + + let y = if bounds.height < image_size.height { + (self.state.starting_offset.y - delta.y) + .min(hidden_height) + .max(-hidden_height) + } else { + 0.0 + }; + + self.state.current_offset = Vector::new(x, y); + + event::Status::Captured + } else { + event::Status::Ignored + } + } + _ => event::Status::Ignored, + } + } + + fn draw( + &self, + renderer: &mut Renderer, + _defaults: &Renderer::Defaults, + layout: Layout<'_>, + cursor_position: Point, + _viewport: &Rectangle, + ) -> Renderer::Output { + let bounds = layout.bounds(); + + let image_size = self.image_size(renderer, bounds.size()); + + let translation = { + let image_top_left = Vector::new( + bounds.width / 2.0 - image_size.width / 2.0, + bounds.height / 2.0 - image_size.height / 2.0, + ); + + image_top_left - self.state.offset(bounds, image_size) + }; + + let is_mouse_over = bounds.contains(cursor_position); + + self::Renderer::draw( + renderer, + &self.state, + bounds, + image_size, + translation, + self.handle.clone(), + is_mouse_over, + ) + } + + fn hash_layout(&self, state: &mut Hasher) { + struct Marker; + std::any::TypeId::of::<Marker>().hash(state); + + self.width.hash(state); + self.height.hash(state); + self.padding.hash(state); + + self.handle.hash(state); + } +} + +/// The local state of a [`Viewer`]. +#[derive(Debug, Clone, Copy)] +pub struct State { + scale: f32, + starting_offset: Vector, + current_offset: Vector, + cursor_grabbed_at: Option<Point>, +} + +impl Default for State { + fn default() -> Self { + Self { + scale: 1.0, + starting_offset: Vector::default(), + current_offset: Vector::default(), + cursor_grabbed_at: None, + } + } +} + +impl State { + /// Creates a new [`State`]. + pub fn new() -> Self { + State::default() + } + + /// Returns the current offset of the [`State`], given the bounds + /// of the [`Viewer`] and its image. + fn offset(&self, bounds: Rectangle, image_size: Size) -> Vector { + let hidden_width = + (image_size.width - bounds.width / 2.0).max(0.0).round(); + + let hidden_height = + (image_size.height - bounds.height / 2.0).max(0.0).round(); + + Vector::new( + self.current_offset.x.min(hidden_width).max(-hidden_width), + self.current_offset.y.min(hidden_height).max(-hidden_height), + ) + } + + /// Returns if the cursor is currently grabbed by the [`Viewer`]. + pub fn is_cursor_grabbed(&self) -> bool { + self.cursor_grabbed_at.is_some() + } +} + +/// The renderer of an [`Viewer`]. +/// +/// Your [renderer] will need to implement this trait before being +/// able to use a [`Viewer`] in your user interface. +/// +/// [renderer]: crate::renderer +pub trait Renderer: crate::Renderer + Sized { + /// Draws the [`Viewer`]. + /// + /// It receives: + /// - the [`State`] of the [`Viewer`] + /// - the bounds of the [`Viewer`] widget + /// - the [`Size`] of the scaled [`Viewer`] image + /// - the translation of the clipped image + /// - the [`Handle`] to the underlying image + /// - whether the mouse is over the [`Viewer`] or not + /// + /// [`Handle`]: image::Handle + fn draw( + &mut self, + state: &State, + bounds: Rectangle, + image_size: Size, + translation: Vector, + handle: image::Handle, + is_mouse_over: bool, + ) -> Self::Output; +} + +impl<'a, Message, Renderer> From<Viewer<'a>> for Element<'a, Message, Renderer> +where + Renderer: 'a + self::Renderer + image::Renderer, + Message: 'a, +{ + fn from(viewer: Viewer<'a>) -> Element<'a, Message, Renderer> { + Element::new(viewer) + } +} diff --git a/native/src/widget/pane_grid.rs b/native/src/widget/pane_grid.rs index 5180fd3b..26a72409 100644 --- a/native/src/widget/pane_grid.rs +++ b/native/src/widget/pane_grid.rs @@ -6,8 +6,7 @@ //! The [`pane_grid` example] showcases how to use a [`PaneGrid`] with resizing, //! drag and drop, and hotkey support. //! -//! [`pane_grid` example]: https://github.com/hecrj/iced/tree/0.1/examples/pane_grid -//! [`PaneGrid`]: struct.PaneGrid.html +//! [`pane_grid` example]: https://github.com/hecrj/iced/tree/0.3/examples/pane_grid mod axis; mod configuration; mod content; @@ -25,12 +24,19 @@ pub use direction::Direction; pub use node::Node; pub use pane::Pane; pub use split::Split; -pub use state::{Focus, State}; +pub use state::State; pub use title_bar::TitleBar; +use crate::container; +use crate::event::{self, Event}; +use crate::layout; +use crate::mouse; +use crate::overlay; +use crate::row; +use crate::touch; use crate::{ - container, keyboard, layout, mouse, overlay, row, text, Clipboard, Element, - Event, Hasher, Layout, Length, Point, Rectangle, Size, Vector, Widget, + Clipboard, Element, Hasher, Layout, Length, Point, Rectangle, Size, Vector, + Widget, }; /// A collection of panes distributed using either vertical or horizontal splits @@ -73,7 +79,7 @@ use crate::{ /// let (mut state, _) = pane_grid::State::new(PaneState::SomePane); /// /// let pane_grid = -/// PaneGrid::new(&mut state, |pane, state, focus| { +/// PaneGrid::new(&mut state, |pane, state| { /// pane_grid::Content::new(match state { /// PaneState::SomePane => Text::new("This is some pane"), /// PaneState::AnotherKindOfPane => Text::new("This is another kind of pane"), @@ -82,9 +88,6 @@ use crate::{ /// .on_drag(Message::PaneDragged) /// .on_resize(10, Message::PaneResized); /// ``` -/// -/// [`PaneGrid`]: struct.PaneGrid.html -/// [`State`]: struct.State.html #[allow(missing_debug_implementations)] pub struct PaneGrid<'a, Message, Renderer: self::Renderer> { state: &'a mut state::Internal, @@ -92,10 +95,10 @@ pub struct PaneGrid<'a, Message, Renderer: self::Renderer> { width: Length, height: Length, spacing: u16, - modifier_keys: keyboard::ModifiersState, + on_click: Option<Box<dyn Fn(Pane) -> Message + 'a>>, on_drag: Option<Box<dyn Fn(DragEvent) -> Message + 'a>>, on_resize: Option<(u16, Box<dyn Fn(ResizeEvent) -> Message + 'a>)>, - on_key_press: Option<Box<dyn Fn(KeyPressEvent) -> Option<Message> + 'a>>, + style: <Renderer as self::Renderer>::Style, } impl<'a, Message, Renderer> PaneGrid<'a, Message, Renderer> @@ -106,37 +109,15 @@ where /// /// The view function will be called to display each [`Pane`] present in the /// [`State`]. - /// - /// [`PaneGrid`]: struct.PaneGrid.html - /// [`State`]: struct.State.html - /// [`Pane`]: struct.Pane.html pub fn new<T>( state: &'a mut State<T>, - view: impl Fn( - Pane, - &'a mut T, - Option<Focus>, - ) -> Content<'a, Message, Renderer>, + view: impl Fn(Pane, &'a mut T) -> Content<'a, Message, Renderer>, ) -> Self { let elements = { - let action = state.internal.action(); - let current_focus = action.focus(); - state .panes .iter_mut() - .map(move |(pane, pane_state)| { - let focus = match current_focus { - Some((focused_pane, focus)) - if *pane == focused_pane => - { - Some(focus) - } - _ => None, - }; - - (*pane, view(*pane, pane_state, focus)) - }) + .map(|(pane, pane_state)| (*pane, view(*pane, pane_state))) .collect() }; @@ -146,59 +127,43 @@ where width: Length::Fill, height: Length::Fill, spacing: 0, - modifier_keys: keyboard::ModifiersState { - control: true, - ..Default::default() - }, + on_click: None, on_drag: None, on_resize: None, - on_key_press: None, + style: Default::default(), } } /// Sets the width of the [`PaneGrid`]. - /// - /// [`PaneGrid`]: struct.PaneGrid.html pub fn width(mut self, width: Length) -> Self { self.width = width; self } /// Sets the height of the [`PaneGrid`]. - /// - /// [`PaneGrid`]: struct.PaneGrid.html pub fn height(mut self, height: Length) -> Self { self.height = height; self } /// Sets the spacing _between_ the panes of the [`PaneGrid`]. - /// - /// [`PaneGrid`]: struct.PaneGrid.html pub fn spacing(mut self, units: u16) -> Self { self.spacing = units; self } - /// Sets the modifier keys of the [`PaneGrid`]. - /// - /// The modifier keys will need to be pressed to trigger key events. - /// - /// The default modifier key is `Ctrl`. - /// - /// [`PaneGrid`]: struct.PaneGrid.html - pub fn modifier_keys( - mut self, - modifier_keys: keyboard::ModifiersState, - ) -> Self { - self.modifier_keys = modifier_keys; + /// Sets the message that will be produced when a [`Pane`] of the + /// [`PaneGrid`] is clicked. + pub fn on_click<F>(mut self, f: F) -> Self + where + F: 'a + Fn(Pane) -> Message, + { + self.on_click = Some(Box::new(f)); self } /// Enables the drag and drop interactions of the [`PaneGrid`], which will /// use the provided function to produce messages. - /// - /// [`PaneGrid`]: struct.PaneGrid.html pub fn on_drag<F>(mut self, f: F) -> Self where F: 'a + Fn(DragEvent) -> Message, @@ -216,8 +181,6 @@ where /// The grabbable area of a split will have a length of `spacing + leeway`, /// properly centered. In other words, a length of /// `(spacing + leeway) / 2.0` on either side of the split line. - /// - /// [`PaneGrid`]: struct.PaneGrid.html pub fn on_resize<F>(mut self, leeway: u16, f: F) -> Self where F: 'a + Fn(ResizeEvent) -> Message, @@ -226,28 +189,12 @@ where self } - /// Captures hotkey interactions with the [`PaneGrid`], using the provided - /// function to produce messages. - /// - /// The function will be called when: - /// - a [`Pane`] is focused - /// - a key is pressed - /// - all the modifier keys are pressed - /// - /// If the function returns `None`, the key press event will be discarded - /// without producing any message. - /// - /// This method is particularly useful to implement hotkey interactions. - /// For instance, you can use it to enable splitting, swapping, or resizing - /// panes by pressing combinations of keys. - /// - /// [`PaneGrid`]: struct.PaneGrid.html - /// [`Pane`]: struct.Pane.html - pub fn on_key_press<F>(mut self, f: F) -> Self - where - F: 'a + Fn(KeyPressEvent) -> Option<Message>, - { - self.on_key_press = Some(Box::new(f)); + /// Sets the style of the [`PaneGrid`]. + pub fn style( + mut self, + style: impl Into<<Renderer as self::Renderer>::Style>, + ) -> Self { + self.style = style.into(); self } } @@ -268,24 +215,20 @@ where ); if let Some(((pane, content), layout)) = clicked_region.next() { - match &self.on_drag { - Some(on_drag) => { - if content.can_be_picked_at(layout, cursor_position) { - let pane_position = layout.position(); + if let Some(on_click) = &self.on_click { + messages.push(on_click(*pane)); + } - let origin = cursor_position - - Vector::new(pane_position.x, pane_position.y); + if let Some(on_drag) = &self.on_drag { + if content.can_be_picked_at(layout, cursor_position) { + let pane_position = layout.position(); - self.state.pick_pane(pane, origin); + let origin = cursor_position + - Vector::new(pane_position.x, pane_position.y); - messages - .push(on_drag(DragEvent::Picked { pane: *pane })); - } else { - self.state.focus(pane); - } - } - None => { - self.state.focus(pane); + self.state.pick_pane(pane, origin); + + messages.push(on_drag(DragEvent::Picked { pane: *pane })); } } } @@ -296,7 +239,7 @@ where layout: Layout<'_>, cursor_position: Point, messages: &mut Vec<Message>, - ) { + ) -> event::Status { if let Some((_, on_resize)) = &self.on_resize { if let Some((split, _)) = self.state.picked_split() { let bounds = layout.bounds(); @@ -323,85 +266,55 @@ where }; messages.push(on_resize(ResizeEvent { split, ratio })); + + return event::Status::Captured; } } } + + event::Status::Ignored } } /// An event produced during a drag and drop interaction of a [`PaneGrid`]. -/// -/// [`PaneGrid`]: struct.PaneGrid.html #[derive(Debug, Clone, Copy)] pub enum DragEvent { /// A [`Pane`] was picked for dragging. - /// - /// [`Pane`]: struct.Pane.html Picked { /// The picked [`Pane`]. - /// - /// [`Pane`]: struct.Pane.html pane: Pane, }, /// A [`Pane`] was dropped on top of another [`Pane`]. - /// - /// [`Pane`]: struct.Pane.html Dropped { /// The picked [`Pane`]. - /// - /// [`Pane`]: struct.Pane.html pane: Pane, /// The [`Pane`] where the picked one was dropped on. - /// - /// [`Pane`]: struct.Pane.html target: Pane, }, /// A [`Pane`] was picked and then dropped outside of other [`Pane`] /// boundaries. - /// - /// [`Pane`]: struct.Pane.html Canceled { /// The picked [`Pane`]. - /// - /// [`Pane`]: struct.Pane.html pane: Pane, }, } /// An event produced during a resize interaction of a [`PaneGrid`]. -/// -/// [`PaneGrid`]: struct.PaneGrid.html #[derive(Debug, Clone, Copy)] pub struct ResizeEvent { /// The [`Split`] that is being dragged for resizing. - /// - /// [`Split`]: struct.Split.html pub split: Split, /// The new ratio of the [`Split`]. /// /// The ratio is a value in [0, 1], representing the exact position of a /// [`Split`] between two panes. - /// - /// [`Split`]: struct.Split.html pub ratio: f32, } -/// An event produced during a key press interaction of a [`PaneGrid`]. -/// -/// [`PaneGrid`]: struct.PaneGrid.html -#[derive(Debug, Clone, Copy)] -pub struct KeyPressEvent { - /// The key that was pressed. - pub key_code: keyboard::KeyCode, - - /// The state of the modifier keys when the key was pressed. - pub modifiers: keyboard::ModifiersState, -} - impl<'a, Message, Renderer> Widget<Message, Renderer> for PaneGrid<'a, Message, Renderer> where @@ -449,45 +362,41 @@ where event: Event, layout: Layout<'_>, cursor_position: Point, - messages: &mut Vec<Message>, renderer: &Renderer, - clipboard: Option<&dyn Clipboard>, - ) { - match event { - Event::Mouse(mouse_event) => match mouse_event { - mouse::Event::ButtonPressed(mouse::Button::Left) => { - let bounds = layout.bounds(); - - if bounds.contains(cursor_position) { - match self.on_resize { - Some((leeway, _)) => { - let relative_cursor = Point::new( - cursor_position.x - bounds.x, - cursor_position.y - bounds.y, - ); - - let splits = self.state.split_regions( - f32::from(self.spacing), - Size::new(bounds.width, bounds.height), - ); + clipboard: &mut dyn Clipboard, + messages: &mut Vec<Message>, + ) -> event::Status { + let mut event_status = event::Status::Ignored; - let clicked_split = hovered_split( - splits.iter(), - f32::from(self.spacing + leeway), - relative_cursor, - ); + match event { + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerPressed { .. }) => { + let bounds = layout.bounds(); - if let Some((split, axis)) = clicked_split { - self.state.pick_split(&split, axis); - } else { - self.click_pane( - layout, - cursor_position, - messages, - ); - } - } - None => { + if bounds.contains(cursor_position) { + event_status = event::Status::Captured; + + match self.on_resize { + Some((leeway, _)) => { + let relative_cursor = Point::new( + cursor_position.x - bounds.x, + cursor_position.y - bounds.y, + ); + + let splits = self.state.split_regions( + f32::from(self.spacing), + Size::new(bounds.width, bounds.height), + ); + + let clicked_split = hovered_split( + splits.iter(), + f32::from(self.spacing + leeway), + relative_cursor, + ); + + if let Some((split, axis, _)) = clicked_split { + self.state.pick_split(&split, axis); + } else { self.click_pane( layout, cursor_position, @@ -495,91 +404,73 @@ where ); } } - } else { - // TODO: Encode cursor availability in the type system - if cursor_position.x > 0.0 && cursor_position.y > 0.0 { - self.state.unfocus(); + None => { + self.click_pane(layout, cursor_position, messages); } } } - mouse::Event::ButtonReleased(mouse::Button::Left) => { - if let Some((pane, _)) = self.state.picked_pane() { - self.state.focus(&pane); - - if let Some(on_drag) = &self.on_drag { - let mut dropped_region = self - .elements - .iter() - .zip(layout.children()) - .filter(|(_, layout)| { + } + Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerLifted { .. }) + | Event::Touch(touch::Event::FingerLost { .. }) => { + if let Some((pane, _)) = self.state.picked_pane() { + if let Some(on_drag) = &self.on_drag { + let mut dropped_region = + self.elements.iter().zip(layout.children()).filter( + |(_, layout)| { layout.bounds().contains(cursor_position) - }); - - let event = match dropped_region.next() { - Some(((target, _), _)) if pane != *target => { - DragEvent::Dropped { - pane, - target: *target, - } + }, + ); + + let event = match dropped_region.next() { + Some(((target, _), _)) if pane != *target => { + DragEvent::Dropped { + pane, + target: *target, } - _ => DragEvent::Canceled { pane }, - }; + } + _ => DragEvent::Canceled { pane }, + }; - messages.push(on_drag(event)); - } - } else if self.state.picked_split().is_some() { - self.state.drop_split(); + messages.push(on_drag(event)); } + + self.state.idle(); + + event_status = event::Status::Captured; + } else if self.state.picked_split().is_some() { + self.state.idle(); + + event_status = event::Status::Captured; } - mouse::Event::CursorMoved { .. } => { + } + Event::Mouse(mouse::Event::CursorMoved { .. }) + | Event::Touch(touch::Event::FingerMoved { .. }) => { + event_status = self.trigger_resize(layout, cursor_position, messages); - } - _ => {} - }, - Event::Keyboard(keyboard_event) => { - match keyboard_event { - keyboard::Event::KeyPressed { - modifiers, - key_code, - } => { - if let Some(on_key_press) = &self.on_key_press { - // TODO: Discard when event is captured - if let Some(_) = self.state.active_pane() { - if modifiers.matches(self.modifier_keys) { - if let Some(message) = - on_key_press(KeyPressEvent { - key_code, - modifiers, - }) - { - messages.push(message); - } - } - } - } - } - _ => {} - } } _ => {} } - if self.state.picked_pane().is_none() { - { - self.elements.iter_mut().zip(layout.children()).for_each( - |((_, pane), layout)| { - pane.on_event( - event.clone(), - layout, - cursor_position, - messages, - renderer, - clipboard, - ) - }, - ); - } - } + let picked_pane = self.state.picked_pane().map(|(pane, _)| pane); + + self.elements + .iter_mut() + .zip(layout.children()) + .map(|((pane, content), layout)| { + let is_picked = picked_pane == Some(*pane); + + content.on_event( + event.clone(), + layout, + cursor_position, + renderer, + clipboard, + messages, + is_picked, + ) + }) + .fold(event_status, event::Status::merge) } fn draw( @@ -588,10 +479,28 @@ where defaults: &Renderer::Defaults, layout: Layout<'_>, cursor_position: Point, + viewport: &Rectangle, ) -> Renderer::Output { let picked_split = self .state .picked_split() + .and_then(|(split, axis)| { + let bounds = layout.bounds(); + + let splits = self + .state + .split_regions(f32::from(self.spacing), bounds.size()); + + let (_axis, region, ratio) = splits.get(&split)?; + + let region = axis.split_line_bounds( + *region, + *ratio, + f32::from(self.spacing), + ); + + Some((axis, region + Vector::new(bounds.x, bounds.y), true)) + }) .or_else(|| match self.on_resize { Some((leeway, _)) => { let bounds = layout.bounds(); @@ -605,15 +514,20 @@ where .state .split_regions(f32::from(self.spacing), bounds.size()); - hovered_split( + let (_split, axis, region) = hovered_split( splits.iter(), f32::from(self.spacing + leeway), relative_cursor, - ) + )?; + + Some(( + axis, + region + Vector::new(bounds.x, bounds.y), + false, + )) } None => None, - }) - .map(|(_, axis)| axis); + }); self::Renderer::draw( renderer, @@ -622,7 +536,9 @@ where self.state.picked_pane(), picked_split, layout, + &self.style, cursor_position, + viewport, ) } @@ -658,11 +574,11 @@ where /// Your [renderer] will need to implement this trait before being /// able to use a [`PaneGrid`] in your user interface. /// -/// [`PaneGrid`]: struct.PaneGrid.html -/// [renderer]: ../../renderer/index.html -pub trait Renderer: - crate::Renderer + container::Renderer + text::Renderer + Sized -{ +/// [renderer]: crate::renderer +pub trait Renderer: crate::Renderer + container::Renderer + Sized { + /// The style supported by this renderer. + type Style: Default; + /// Draws a [`PaneGrid`]. /// /// It receives: @@ -671,18 +587,16 @@ pub trait Renderer: /// - the [`Axis`] that is currently being resized /// - the [`Layout`] of the [`PaneGrid`] and its elements /// - the cursor position - /// - /// [`PaneGrid`]: struct.PaneGrid.html - /// [`Pane`]: struct.Pane.html - /// [`Layout`]: ../layout/struct.Layout.html fn draw<Message>( &mut self, defaults: &Self::Defaults, content: &[(Pane, Content<'_, Message, Self>)], dragging: Option<(Pane, Point)>, - resizing: Option<Axis>, + resizing: Option<(Axis, Rectangle, bool)>, layout: Layout<'_>, + style: &<Self as self::Renderer>::Style, cursor_position: Point, + viewport: &Rectangle, ) -> Self::Output; /// Draws a [`Pane`]. @@ -692,17 +606,15 @@ pub trait Renderer: /// - the [`Content`] of the [`Pane`] /// - the [`Layout`] of the [`Pane`] and its elements /// - the cursor position - /// - /// [`Pane`]: struct.Pane.html - /// [`Layout`]: ../layout/struct.Layout.html fn draw_pane<Message>( &mut self, defaults: &Self::Defaults, bounds: Rectangle, - style: &Self::Style, + style: &<Self as container::Renderer>::Style, title_bar: Option<(&TitleBar<'_, Message, Self>, Layout<'_>)>, body: (&Element<'_, Message, Self>, Layout<'_>), cursor_position: Point, + viewport: &Rectangle, ) -> Self::Output; /// Draws a [`TitleBar`]. @@ -710,23 +622,18 @@ pub trait Renderer: /// It receives: /// - the bounds, style of the [`TitleBar`] /// - the style of the [`TitleBar`] - /// - the title of the [`TitleBar`] with its size, font, and bounds - /// - the controls of the [`TitleBar`] with their [`Layout`+, if any + /// - the content of the [`TitleBar`] with its layout + /// - the controls of the [`TitleBar`] with their [`Layout`], if any /// - the cursor position - /// - /// [`TitleBar`]: struct.TitleBar.html - /// [`Layout`]: ../layout/struct.Layout.html fn draw_title_bar<Message>( &mut self, defaults: &Self::Defaults, bounds: Rectangle, - style: &Self::Style, - title: &str, - title_size: u16, - title_font: Self::Font, - title_bounds: Rectangle, + style: &<Self as container::Renderer>::Style, + content: (&Element<'_, Message, Self>, Layout<'_>), controls: Option<(&Element<'_, Message, Self>, Layout<'_>)>, cursor_position: Point, + viewport: &Rectangle, ) -> Self::Output; } @@ -750,14 +657,14 @@ fn hovered_split<'a>( splits: impl Iterator<Item = (&'a Split, &'a (Axis, Rectangle, f32))>, spacing: f32, cursor_position: Point, -) -> Option<(Split, Axis)> { +) -> Option<(Split, Axis, Rectangle)> { splits .filter_map(|(split, (axis, region, ratio))| { let bounds = axis.split_line_bounds(*region, *ratio, f32::from(spacing)); if bounds.contains(cursor_position) { - Some((*split, *axis)) + Some((*split, *axis, bounds)) } else { None } diff --git a/native/src/widget/pane_grid/configuration.rs b/native/src/widget/pane_grid/configuration.rs index 1fed98b7..4c43826e 100644 --- a/native/src/widget/pane_grid/configuration.rs +++ b/native/src/widget/pane_grid/configuration.rs @@ -2,7 +2,7 @@ use crate::pane_grid::Axis; /// The arrangement of a [`PaneGrid`]. /// -/// [`PaneGrid`]: struct.PaneGrid.html +/// [`PaneGrid`]: crate::pane_grid::PaneGrid #[derive(Debug, Clone)] pub enum Configuration<T> { /// A split of the available space. @@ -13,18 +13,14 @@ pub enum Configuration<T> { /// The ratio of the split in [0.0, 1.0]. ratio: f32, - /// The left/top [`Content`] of the split. - /// - /// [`Configuration`]: enum.Node.html + /// The left/top [`Configuration`] of the split. a: Box<Configuration<T>>, - /// The right/bottom [`Content`] of the split. - /// - /// [`Configuration`]: enum.Node.html + /// The right/bottom [`Configuration`] of the split. b: Box<Configuration<T>>, }, /// A [`Pane`]. /// - /// [`Pane`]: struct.Pane.html + /// [`Pane`]: crate::pane_grid::Pane Pane(T), } diff --git a/native/src/widget/pane_grid/content.rs b/native/src/widget/pane_grid/content.rs index 1d339b75..bac9fdd4 100644 --- a/native/src/widget/pane_grid/content.rs +++ b/native/src/widget/pane_grid/content.rs @@ -1,17 +1,18 @@ use crate::container; +use crate::event::{self, Event}; use crate::layout; use crate::overlay; use crate::pane_grid::{self, TitleBar}; -use crate::{Clipboard, Element, Event, Hasher, Layout, Point, Size}; +use crate::{Clipboard, Element, Hasher, Layout, Point, Rectangle, Size}; /// The content of a [`Pane`]. /// -/// [`Pane`]: struct.Pane.html +/// [`Pane`]: crate::widget::pane_grid::Pane #[allow(missing_debug_implementations)] pub struct Content<'a, Message, Renderer: pane_grid::Renderer> { title_bar: Option<TitleBar<'a, Message, Renderer>>, body: Element<'a, Message, Renderer>, - style: Renderer::Style, + style: <Renderer as container::Renderer>::Style, } impl<'a, Message, Renderer> Content<'a, Message, Renderer> @@ -19,20 +20,15 @@ where Renderer: pane_grid::Renderer, { /// Creates a new [`Content`] with the provided body. - /// - /// [`Content`]: struct.Content.html pub fn new(body: impl Into<Element<'a, Message, Renderer>>) -> Self { Self { title_bar: None, body: body.into(), - style: Renderer::Style::default(), + style: Default::default(), } } /// Sets the [`TitleBar`] of this [`Content`]. - /// - /// [`TitleBar`]: struct.TitleBar.html - /// [`Content`]: struct.Content.html pub fn title_bar( mut self, title_bar: TitleBar<'a, Message, Renderer>, @@ -41,10 +37,11 @@ where self } - /// Sets the style of the [`TitleBar`]. - /// - /// [`TitleBar`]: struct.TitleBar.html - pub fn style(mut self, style: impl Into<Renderer::Style>) -> Self { + /// Sets the style of the [`Content`]. + pub fn style( + mut self, + style: impl Into<<Renderer as container::Renderer>::Style>, + ) -> Self { self.style = style.into(); self } @@ -56,15 +53,14 @@ where { /// Draws the [`Content`] with the provided [`Renderer`] and [`Layout`]. /// - /// [`Content`]: struct.Content.html - /// [`Renderer`]: trait.Renderer.html - /// [`Layout`]: ../layout/struct.Layout.html + /// [`Renderer`]: crate::widget::pane_grid::Renderer pub fn draw( &self, renderer: &mut Renderer, defaults: &Renderer::Defaults, layout: Layout<'_>, cursor_position: Point, + viewport: &Rectangle, ) -> Renderer::Output { if let Some(title_bar) = &self.title_bar { let mut children = layout.children(); @@ -78,6 +74,7 @@ where Some((title_bar, title_bar_layout)), (&self.body, body_layout), cursor_position, + viewport, ) } else { renderer.draw_pane( @@ -87,15 +84,13 @@ where None, (&self.body, layout), cursor_position, + viewport, ) } } /// Returns whether the [`Content`] with the given [`Layout`] can be picked /// at the provided cursor position. - /// - /// [`Content`]: struct.Content.html - /// [`Layout`]: ../layout/struct.Layout.html pub fn can_be_picked_at( &self, layout: Layout<'_>, @@ -151,20 +146,23 @@ where event: Event, layout: Layout<'_>, cursor_position: Point, - messages: &mut Vec<Message>, renderer: &Renderer, - clipboard: Option<&dyn Clipboard>, - ) { + clipboard: &mut dyn Clipboard, + messages: &mut Vec<Message>, + is_picked: bool, + ) -> event::Status { + let mut event_status = event::Status::Ignored; + let body_layout = if let Some(title_bar) = &mut self.title_bar { let mut children = layout.children(); - title_bar.on_event( + event_status = title_bar.on_event( event.clone(), children.next().unwrap(), cursor_position, - messages, renderer, clipboard, + messages, ); children.next().unwrap() @@ -172,14 +170,20 @@ where layout }; - self.body.on_event( - event, - body_layout, - cursor_position, - messages, - renderer, - clipboard, - ); + let body_status = if is_picked { + event::Status::Ignored + } else { + self.body.on_event( + event, + body_layout, + cursor_position, + renderer, + clipboard, + messages, + ) + }; + + event_status.merge(body_status) } pub(crate) fn hash_layout(&self, state: &mut Hasher) { @@ -194,18 +198,17 @@ where &mut self, layout: Layout<'_>, ) -> Option<overlay::Element<'_, Message, Renderer>> { - let body_layout = if self.title_bar.is_some() { + if let Some(title_bar) = self.title_bar.as_mut() { let mut children = layout.children(); + let title_bar_layout = children.next()?; - // Overlays only allowed in the pane body, for now at least. - let _title_bar_layout = children.next(); - - children.next()? + match title_bar.overlay(title_bar_layout) { + Some(overlay) => Some(overlay), + None => self.body.overlay(children.next()?), + } } else { - layout - }; - - self.body.overlay(body_layout) + self.body.overlay(layout) + } } } diff --git a/native/src/widget/pane_grid/node.rs b/native/src/widget/pane_grid/node.rs index cbfd8a43..84714e00 100644 --- a/native/src/widget/pane_grid/node.rs +++ b/native/src/widget/pane_grid/node.rs @@ -3,21 +3,16 @@ use crate::{ Rectangle, Size, }; -use std::collections::HashMap; +use std::collections::BTreeMap; /// A layout node of a [`PaneGrid`]. /// -/// [`PaneGrid`]: struct.PaneGrid.html +/// [`PaneGrid`]: crate::widget::PaneGrid #[derive(Debug, Clone)] pub enum Node { /// The region of this [`Node`] is split into two. - /// - /// [`Node`]: enum.Node.html Split { /// The [`Split`] of this [`Node`]. - /// - /// [`Split`]: struct.Split.html - /// [`Node`]: enum.Node.html id: Split, /// The direction of the split. @@ -27,26 +22,17 @@ pub enum Node { ratio: f32, /// The left/top [`Node`] of the split. - /// - /// [`Node`]: enum.Node.html a: Box<Node>, /// The right/bottom [`Node`] of the split. - /// - /// [`Node`]: enum.Node.html b: Box<Node>, }, /// The region of this [`Node`] is taken by a [`Pane`]. - /// - /// [`Pane`]: struct.Pane.html Pane(Pane), } impl Node { /// Returns an iterator over each [`Split`] in this [`Node`]. - /// - /// [`Split`]: struct.Split.html - /// [`Node`]: enum.Node.html pub fn splits(&self) -> impl Iterator<Item = &Split> { let mut unvisited_nodes = vec![self]; @@ -69,15 +55,12 @@ impl Node { /// Returns the rectangular region for each [`Pane`] in the [`Node`] given /// the spacing between panes and the total available space. - /// - /// [`Pane`]: struct.Pane.html - /// [`Node`]: enum.Node.html pub fn pane_regions( &self, spacing: f32, size: Size, - ) -> HashMap<Pane, Rectangle> { - let mut regions = HashMap::new(); + ) -> BTreeMap<Pane, Rectangle> { + let mut regions = BTreeMap::new(); self.compute_regions( spacing, @@ -96,15 +79,12 @@ impl Node { /// Returns the axis, rectangular region, and ratio for each [`Split`] in /// the [`Node`] given the spacing between panes and the total available /// space. - /// - /// [`Split`]: struct.Split.html - /// [`Node`]: enum.Node.html pub fn split_regions( &self, spacing: f32, size: Size, - ) -> HashMap<Split, (Axis, Rectangle, f32)> { - let mut splits = HashMap::new(); + ) -> BTreeMap<Split, (Axis, Rectangle, f32)> { + let mut splits = BTreeMap::new(); self.compute_splits( spacing, @@ -211,7 +191,7 @@ impl Node { &self, spacing: f32, current: &Rectangle, - regions: &mut HashMap<Pane, Rectangle>, + regions: &mut BTreeMap<Pane, Rectangle>, ) { match self { Node::Split { @@ -232,7 +212,7 @@ impl Node { &self, spacing: f32, current: &Rectangle, - splits: &mut HashMap<Split, (Axis, Rectangle, f32)>, + splits: &mut BTreeMap<Split, (Axis, Rectangle, f32)>, ) { match self { Node::Split { diff --git a/native/src/widget/pane_grid/pane.rs b/native/src/widget/pane_grid/pane.rs index f9866407..d6fbab83 100644 --- a/native/src/widget/pane_grid/pane.rs +++ b/native/src/widget/pane_grid/pane.rs @@ -1,5 +1,5 @@ /// A rectangular region in a [`PaneGrid`] used to display widgets. /// -/// [`PaneGrid`]: struct.PaneGrid.html -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +/// [`PaneGrid`]: crate::widget::PaneGrid +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct Pane(pub(super) usize); diff --git a/native/src/widget/pane_grid/split.rs b/native/src/widget/pane_grid/split.rs index d020c510..8132272a 100644 --- a/native/src/widget/pane_grid/split.rs +++ b/native/src/widget/pane_grid/split.rs @@ -1,5 +1,5 @@ /// A divider that splits a region in a [`PaneGrid`] into two different panes. /// -/// [`PaneGrid`]: struct.PaneGrid.html -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +/// [`PaneGrid`]: crate::widget::PaneGrid +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct Split(pub(super) usize); diff --git a/native/src/widget/pane_grid/state.rs b/native/src/widget/pane_grid/state.rs index fb59c846..fb96f89f 100644 --- a/native/src/widget/pane_grid/state.rs +++ b/native/src/widget/pane_grid/state.rs @@ -3,7 +3,7 @@ use crate::{ Hasher, Point, Rectangle, Size, }; -use std::collections::HashMap; +use std::collections::{BTreeMap, HashMap}; /// The state of a [`PaneGrid`]. /// @@ -15,41 +15,19 @@ use std::collections::HashMap; /// provided to the view function of [`PaneGrid::new`] for displaying each /// [`Pane`]. /// -/// [`PaneGrid`]: struct.PaneGrid.html -/// [`PaneGrid::new`]: struct.PaneGrid.html#method.new -/// [`Pane`]: struct.Pane.html -/// [`Split`]: struct.Split.html -/// [`State`]: struct.State.html +/// [`PaneGrid`]: crate::widget::PaneGrid +/// [`PaneGrid::new`]: crate::widget::PaneGrid::new #[derive(Debug, Clone)] pub struct State<T> { pub(super) panes: HashMap<Pane, T>, pub(super) internal: Internal, } -/// The current focus of a [`Pane`]. -/// -/// [`Pane`]: struct.Pane.html -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum Focus { - /// The [`Pane`] is just focused. - /// - /// [`Pane`]: struct.Pane.html - Idle, - - /// The [`Pane`] is being dragged. - /// - /// [`Pane`]: struct.Pane.html - Dragging, -} - impl<T> State<T> { /// Creates a new [`State`], initializing the first pane with the provided /// state. /// /// Alongside the [`State`], it returns the first [`Pane`] identifier. - /// - /// [`State`]: struct.State.html - /// [`Pane`]: struct.Pane.html pub fn new(first_pane_state: T) -> (Self, Pane) { ( Self::with_configuration(Configuration::Pane(first_pane_state)), @@ -58,9 +36,6 @@ impl<T> State<T> { } /// Creates a new [`State`] with the given [`Configuration`]. - /// - /// [`State`]: struct.State.html - /// [`Configuration`]: enum.Configuration.html pub fn with_configuration(config: impl Into<Configuration<T>>) -> Self { let mut panes = HashMap::new(); @@ -72,95 +47,46 @@ impl<T> State<T> { internal: Internal { layout, last_id, - action: Action::Idle { focus: None }, + action: Action::Idle, }, } } /// Returns the total amount of panes in the [`State`]. - /// - /// [`State`]: struct.State.html pub fn len(&self) -> usize { self.panes.len() } /// Returns the internal state of the given [`Pane`], if it exists. - /// - /// [`Pane`]: struct.Pane.html pub fn get(&self, pane: &Pane) -> Option<&T> { self.panes.get(pane) } /// Returns the internal state of the given [`Pane`] with mutability, if it /// exists. - /// - /// [`Pane`]: struct.Pane.html pub fn get_mut(&mut self, pane: &Pane) -> Option<&mut T> { self.panes.get_mut(pane) } /// Returns an iterator over all the panes of the [`State`], alongside its /// internal state. - /// - /// [`State`]: struct.State.html pub fn iter(&self) -> impl Iterator<Item = (&Pane, &T)> { self.panes.iter() } /// Returns a mutable iterator over all the panes of the [`State`], /// alongside its internal state. - /// - /// [`State`]: struct.State.html pub fn iter_mut(&mut self) -> impl Iterator<Item = (&Pane, &mut T)> { self.panes.iter_mut() } /// Returns the layout of the [`State`]. - /// - /// [`State`]: struct.State.html pub fn layout(&self) -> &Node { &self.internal.layout } - /// Returns the focused [`Pane`] of the [`State`], if there is one. - /// - /// [`Pane`]: struct.Pane.html - /// [`State`]: struct.State.html - pub fn focused(&self) -> Option<Pane> { - self.internal.focused_pane() - } - - /// Returns the active [`Pane`] of the [`State`], if there is one. - /// - /// A [`Pane`] is active if it is focused and is __not__ being dragged. - /// - /// [`Pane`]: struct.Pane.html - /// [`State`]: struct.State.html - pub fn active(&self) -> Option<Pane> { - self.internal.active_pane() - } - /// Returns the adjacent [`Pane`] of another [`Pane`] in the given /// direction, if there is one. - /// - /// ## Example - /// You can combine this with [`State::active`] to find the pane that is - /// adjacent to the current active one, and then swap them. For instance: - /// - /// ``` - /// # use iced_native::pane_grid; - /// # - /// # let (mut state, _) = pane_grid::State::new(()); - /// # - /// if let Some(active) = state.active() { - /// if let Some(adjacent) = state.adjacent(&active, pane_grid::Direction::Right) { - /// state.swap(&active, &adjacent); - /// } - /// } - /// ``` - /// - /// [`Pane`]: struct.Pane.html - /// [`State::active`]: struct.State.html#method.active pub fn adjacent(&self, pane: &Pane, direction: Direction) -> Option<Pane> { let regions = self .internal @@ -194,25 +120,8 @@ impl<T> State<T> { Some(*pane) } - /// Focuses the given [`Pane`]. - /// - /// [`Pane`]: struct.Pane.html - pub fn focus(&mut self, pane: &Pane) { - self.internal.focus(pane); - } - - /// Unfocuses the current focused [`Pane`]. - /// - /// [`Pane`]: struct.Pane.html - pub fn unfocus(&mut self) { - self.internal.unfocus(); - } - /// Splits the given [`Pane`] into two in the given [`Axis`] and /// initializing the new [`Pane`] with the provided internal state. - /// - /// [`Pane`]: struct.Pane.html - /// [`Axis`]: enum.Axis.html pub fn split( &mut self, axis: Axis, @@ -236,7 +145,6 @@ impl<T> State<T> { node.split(new_split, axis, new_pane); let _ = self.panes.insert(new_pane, state); - self.focus(&new_pane); Some((new_pane, new_split)) } @@ -246,9 +154,8 @@ impl<T> State<T> { /// If you want to swap panes on drag and drop in your [`PaneGrid`], you /// will need to call this method when handling a [`DragEvent`]. /// - /// [`State`]: struct.State.html - /// [`PaneGrid`]: struct.PaneGrid.html - /// [`DragEvent`]: struct.DragEvent.html + /// [`PaneGrid`]: crate::widget::PaneGrid + /// [`DragEvent`]: crate::widget::pane_grid::DragEvent pub fn swap(&mut self, a: &Pane, b: &Pane) { self.internal.layout.update(&|node| match node { Node::Split { .. } => {} @@ -270,20 +177,17 @@ impl<T> State<T> { /// If you want to enable resize interactions in your [`PaneGrid`], you will /// need to call this method when handling a [`ResizeEvent`]. /// - /// [`Split`]: struct.Split.html - /// [`PaneGrid`]: struct.PaneGrid.html - /// [`ResizeEvent`]: struct.ResizeEvent.html + /// [`PaneGrid`]: crate::widget::PaneGrid + /// [`ResizeEvent`]: crate::widget::pane_grid::ResizeEvent pub fn resize(&mut self, split: &Split, ratio: f32) { let _ = self.internal.layout.resize(split, ratio); } - /// Closes the given [`Pane`] and returns its internal state, if it exists. - /// - /// [`Pane`]: struct.Pane.html - pub fn close(&mut self, pane: &Pane) -> Option<T> { + /// Closes the given [`Pane`] and returns its internal state and its closest + /// sibling, if it exists. + pub fn close(&mut self, pane: &Pane) -> Option<(T, Pane)> { if let Some(sibling) = self.internal.layout.remove(pane) { - self.focus(&sibling); - self.panes.remove(pane) + self.panes.remove(pane).map(|state| (state, sibling)) } else { None } @@ -329,52 +233,12 @@ pub struct Internal { #[derive(Debug, Clone, Copy, PartialEq)] pub enum Action { - Idle { - focus: Option<Pane>, - }, - Dragging { - pane: Pane, - origin: Point, - focus: Option<Pane>, - }, - Resizing { - split: Split, - axis: Axis, - focus: Option<Pane>, - }, -} - -impl Action { - pub fn focus(&self) -> Option<(Pane, Focus)> { - match self { - Action::Idle { focus } | Action::Resizing { focus, .. } => { - focus.map(|pane| (pane, Focus::Idle)) - } - Action::Dragging { pane, .. } => Some((*pane, Focus::Dragging)), - } - } + Idle, + Dragging { pane: Pane, origin: Point }, + Resizing { split: Split, axis: Axis }, } impl Internal { - pub fn action(&self) -> Action { - self.action - } - - pub fn focused_pane(&self) -> Option<Pane> { - match self.action { - Action::Idle { focus } => focus, - Action::Dragging { focus, .. } => focus, - Action::Resizing { focus, .. } => focus, - } - } - - pub fn active_pane(&self) -> Option<Pane> { - match self.action { - Action::Idle { focus } => focus, - _ => None, - } - } - pub fn picked_pane(&self) -> Option<(Pane, Point)> { match self.action { Action::Dragging { pane, origin, .. } => Some((pane, origin)), @@ -393,7 +257,7 @@ impl Internal { &self, spacing: f32, size: Size, - ) -> HashMap<Pane, Rectangle> { + ) -> BTreeMap<Pane, Rectangle> { self.layout.pane_regions(spacing, size) } @@ -401,21 +265,14 @@ impl Internal { &self, spacing: f32, size: Size, - ) -> HashMap<Split, (Axis, Rectangle, f32)> { + ) -> BTreeMap<Split, (Axis, Rectangle, f32)> { self.layout.split_regions(spacing, size) } - pub fn focus(&mut self, pane: &Pane) { - self.action = Action::Idle { focus: Some(*pane) }; - } - pub fn pick_pane(&mut self, pane: &Pane, origin: Point) { - let focus = self.focused_pane(); - self.action = Action::Dragging { pane: *pane, origin, - focus, }; } @@ -426,26 +283,14 @@ impl Internal { return; } - let focus = self.action.focus().map(|(pane, _)| pane); - self.action = Action::Resizing { split: *split, axis, - focus, }; } - pub fn drop_split(&mut self) { - match self.action { - Action::Resizing { focus, .. } => { - self.action = Action::Idle { focus }; - } - _ => {} - } - } - - pub fn unfocus(&mut self) { - self.action = Action::Idle { focus: None }; + pub fn idle(&mut self) { + self.action = Action::Idle; } pub fn hash_layout(&self, hasher: &mut Hasher) { diff --git a/native/src/widget/pane_grid/title_bar.rs b/native/src/widget/pane_grid/title_bar.rs index 9dfb9ae4..070010f8 100644 --- a/native/src/widget/pane_grid/title_bar.rs +++ b/native/src/widget/pane_grid/title_bar.rs @@ -1,51 +1,43 @@ +use crate::container; +use crate::event::{self, Event}; use crate::layout; +use crate::overlay; use crate::pane_grid; use crate::{ - Clipboard, Element, Event, Hasher, Layout, Point, Rectangle, Size, + Clipboard, Element, Hasher, Layout, Padding, Point, Rectangle, Size, }; /// The title bar of a [`Pane`]. /// -/// [`Pane`]: struct.Pane.html +/// [`Pane`]: crate::widget::pane_grid::Pane #[allow(missing_debug_implementations)] pub struct TitleBar<'a, Message, Renderer: pane_grid::Renderer> { - title: String, - title_size: Option<u16>, + content: Element<'a, Message, Renderer>, controls: Option<Element<'a, Message, Renderer>>, - padding: u16, + padding: Padding, always_show_controls: bool, - style: Renderer::Style, + style: <Renderer as container::Renderer>::Style, } impl<'a, Message, Renderer> TitleBar<'a, Message, Renderer> where Renderer: pane_grid::Renderer, { - /// Creates a new [`TitleBar`] with the given title. - /// - /// [`TitleBar`]: struct.TitleBar.html - pub fn new(title: impl Into<String>) -> Self { + /// Creates a new [`TitleBar`] with the given content. + pub fn new<E>(content: E) -> Self + where + E: Into<Element<'a, Message, Renderer>>, + { Self { - title: title.into(), - title_size: None, + content: content.into(), controls: None, - padding: 0, + padding: Padding::ZERO, always_show_controls: false, - style: Renderer::Style::default(), + style: Default::default(), } } - /// Sets the size of the title of the [`TitleBar`]. - /// - /// [`TitleBar`]: struct.TitleBar.html - pub fn title_size(mut self, size: u16) -> Self { - self.title_size = Some(size); - self - } - /// Sets the controls of the [`TitleBar`]. - /// - /// [`TitleBar`]: struct.TitleBar.html pub fn controls( mut self, controls: impl Into<Element<'a, Message, Renderer>>, @@ -54,18 +46,17 @@ where self } - /// Sets the padding of the [`TitleBar`]. - /// - /// [`TitleBar`]: struct.TitleBar.html - pub fn padding(mut self, units: u16) -> Self { - self.padding = units; + /// Sets the [`Padding`] of the [`TitleBar`]. + pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self { + self.padding = padding.into(); self } /// Sets the style of the [`TitleBar`]. - /// - /// [`TitleBar`]: struct.TitleBar.html - pub fn style(mut self, style: impl Into<Renderer::Style>) -> Self { + pub fn style( + mut self, + style: impl Into<<Renderer as container::Renderer>::Style>, + ) -> Self { self.style = style.into(); self } @@ -76,9 +67,8 @@ where /// By default, the controls are only visible when the [`Pane`] of this /// [`TitleBar`] is hovered. /// - /// [`TitleBar`]: struct.TitleBar.html - /// [`controls`]: struct.TitleBar.html#method.controls - /// [`Pane`]: struct.Pane.html + /// [`controls`]: Self::controls + /// [`Pane`]: crate::widget::pane_grid::Pane pub fn always_show_controls(mut self) -> Self { self.always_show_controls = true; self @@ -91,70 +81,49 @@ where { /// Draws the [`TitleBar`] with the provided [`Renderer`] and [`Layout`]. /// - /// [`TitleBar`]: struct.TitleBar.html - /// [`Renderer`]: trait.Renderer.html - /// [`Layout`]: ../layout/struct.Layout.html + /// [`Renderer`]: crate::widget::pane_grid::Renderer pub fn draw( &self, renderer: &mut Renderer, defaults: &Renderer::Defaults, layout: Layout<'_>, cursor_position: Point, + viewport: &Rectangle, show_controls: bool, ) -> Renderer::Output { let mut children = layout.children(); let padded = children.next().unwrap(); - if let Some(controls) = &self.controls { - let mut children = padded.children(); - let title_layout = children.next().unwrap(); + let mut children = padded.children(); + let title_layout = children.next().unwrap(); + + let controls = if let Some(controls) = &self.controls { let controls_layout = children.next().unwrap(); - let (title_bounds, controls) = - if show_controls || self.always_show_controls { - (title_layout.bounds(), Some((controls, controls_layout))) - } else { - ( - Rectangle { - width: padded.bounds().width, - ..title_layout.bounds() - }, - None, - ) - }; - - renderer.draw_title_bar( - defaults, - layout.bounds(), - &self.style, - &self.title, - self.title_size.unwrap_or(renderer.default_size()), - Renderer::Font::default(), - title_bounds, - controls, - cursor_position, - ) + if show_controls || self.always_show_controls { + Some((controls, controls_layout)) + } else { + None + } } else { - renderer.draw_title_bar::<()>( - defaults, - layout.bounds(), - &self.style, - &self.title, - self.title_size.unwrap_or(renderer.default_size()), - Renderer::Font::default(), - padded.bounds(), - None, - cursor_position, - ) - } + None + }; + + renderer.draw_title_bar( + defaults, + layout.bounds(), + &self.style, + (&self.content, title_layout), + controls, + cursor_position, + viewport, + ) } /// Returns whether the mouse cursor is over the pick area of the /// [`TitleBar`] or not. /// /// The whole [`TitleBar`] is a pick area, except its controls. - /// - /// [`TitleBar`]: struct.TitleBar.html pub fn is_over_pick_area( &self, layout: Layout<'_>, @@ -163,15 +132,16 @@ where if layout.bounds().contains(cursor_position) { let mut children = layout.children(); let padded = children.next().unwrap(); + let mut children = padded.children(); + let title_layout = children.next().unwrap(); if self.controls.is_some() { - let mut children = padded.children(); - let _ = children.next().unwrap(); let controls_layout = children.next().unwrap(); !controls_layout.bounds().contains(cursor_position) + && !title_layout.bounds().contains(cursor_position) } else { - true + !title_layout.bounds().contains(cursor_position) } } else { false @@ -181,9 +151,12 @@ where pub(crate) fn hash_layout(&self, hasher: &mut Hasher) { use std::hash::Hash; - self.title.hash(hasher); - self.title_size.hash(hasher); + self.content.hash_layout(hasher); self.padding.hash(hasher); + + if let Some(controls) = &self.controls { + controls.hash_layout(hasher); + } } pub(crate) fn layout( @@ -191,19 +164,13 @@ where renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { - let padding = f32::from(self.padding); - let limits = limits.pad(padding); + let limits = limits.pad(self.padding); let max_size = limits.max(); - let title_size = self.title_size.unwrap_or(renderer.default_size()); - let title_font = Renderer::Font::default(); - - let (title_width, title_height) = renderer.measure( - &self.title, - title_size, - title_font, - Size::new(f32::INFINITY, max_size.height), - ); + let title_layout = self + .content + .layout(renderer, &layout::Limits::new(Size::ZERO, max_size)); + let title_size = title_layout.size(); let mut node = if let Some(controls) = &self.controls { let mut controls_layout = controls @@ -212,16 +179,8 @@ where let controls_size = controls_layout.size(); let space_before_controls = max_size.width - controls_size.width; - let mut title_layout = layout::Node::new(Size::new( - title_width.min(space_before_controls), - title_height, - )); - - let title_size = title_layout.size(); let height = title_size.height.max(controls_size.height); - title_layout - .move_to(Point::new(0.0, (height - title_size.height) / 2.0)); controls_layout.move_to(Point::new(space_before_controls, 0.0)); layout::Node::with_children( @@ -229,12 +188,18 @@ where vec![title_layout, controls_layout], ) } else { - layout::Node::new(Size::new(max_size.width, title_height)) + layout::Node::with_children( + Size::new(max_size.width, title_size.height), + vec![title_layout], + ) }; - node.move_to(Point::new(padding, padding)); + node.move_to(Point::new( + self.padding.left.into(), + self.padding.top.into(), + )); - layout::Node::with_children(node.size().pad(padding), vec![node]) + layout::Node::with_children(node.size().pad(self.padding), vec![node]) } pub(crate) fn on_event( @@ -242,26 +207,63 @@ where event: Event, layout: Layout<'_>, cursor_position: Point, - messages: &mut Vec<Message>, renderer: &Renderer, - clipboard: Option<&dyn Clipboard>, - ) { - if let Some(controls) = &mut self.controls { - let mut children = layout.children(); - let padded = children.next().unwrap(); + clipboard: &mut dyn Clipboard, + messages: &mut Vec<Message>, + ) -> event::Status { + let mut children = layout.children(); + let padded = children.next().unwrap(); - let mut children = padded.children(); - let _ = children.next(); + let mut children = padded.children(); + let title_layout = children.next().unwrap(); + + let control_status = if let Some(controls) = &mut self.controls { let controls_layout = children.next().unwrap(); controls.on_event( - event, + event.clone(), controls_layout, cursor_position, - messages, renderer, clipboard, - ); - } + messages, + ) + } else { + event::Status::Ignored + }; + + let title_status = self.content.on_event( + event, + title_layout, + cursor_position, + renderer, + clipboard, + messages, + ); + + control_status.merge(title_status) + } + + pub(crate) fn overlay( + &mut self, + layout: Layout<'_>, + ) -> Option<overlay::Element<'_, Message, Renderer>> { + let mut children = layout.children(); + let padded = children.next()?; + + let mut children = padded.children(); + let title_layout = children.next()?; + + let Self { + content, controls, .. + } = self; + + content.overlay(title_layout).or_else(move || { + controls.as_mut().and_then(|controls| { + let controls_layout = children.next()?; + + controls.overlay(controls_layout) + }) + }) } } diff --git a/native/src/widget/pick_list.rs b/native/src/widget/pick_list.rs index 04478225..d7792000 100644 --- a/native/src/widget/pick_list.rs +++ b/native/src/widget/pick_list.rs @@ -1,9 +1,16 @@ //! Display a dropdown list of selectable values. +use crate::event::{self, Event}; +use crate::keyboard; +use crate::layout; +use crate::mouse; +use crate::overlay; +use crate::overlay::menu::{self, Menu}; +use crate::scrollable; +use crate::text; +use crate::touch; use crate::{ - layout, mouse, overlay, - overlay::menu::{self, Menu}, - scrollable, text, Clipboard, Element, Event, Hasher, Layout, Length, Point, - Rectangle, Size, Widget, + Clipboard, Element, Hasher, Layout, Length, Padding, Point, Rectangle, + Size, Widget, }; use std::borrow::Cow; @@ -14,25 +21,26 @@ where [T]: ToOwned<Owned = Vec<T>>, { menu: &'a mut menu::State, + keyboard_modifiers: &'a mut keyboard::Modifiers, is_open: &'a mut bool, hovered_option: &'a mut Option<usize>, last_selection: &'a mut Option<T>, on_selected: Box<dyn Fn(T) -> Message>, options: Cow<'a, [T]>, + placeholder: Option<String>, selected: Option<T>, width: Length, - padding: u16, + padding: Padding, text_size: Option<u16>, font: Renderer::Font, style: <Renderer as self::Renderer>::Style, } /// The local state of a [`PickList`]. -/// -/// [`PickList`]: struct.PickList.html #[derive(Debug, Clone)] pub struct State<T> { menu: menu::State, + keyboard_modifiers: keyboard::Modifiers, is_open: bool, hovered_option: Option<usize>, last_selection: Option<T>, @@ -42,6 +50,7 @@ impl<T> Default for State<T> { fn default() -> Self { Self { menu: menu::State::default(), + keyboard_modifiers: keyboard::Modifiers::default(), is_open: bool::default(), hovered_option: Option::default(), last_selection: Option::default(), @@ -52,15 +61,12 @@ impl<T> Default for State<T> { impl<'a, T: 'a, Message, Renderer: self::Renderer> PickList<'a, T, Message, Renderer> where - T: ToString, + T: ToString + Eq, [T]: ToOwned<Owned = Vec<T>>, { /// Creates a new [`PickList`] with the given [`State`], a list of options, /// the current selected value, and the message to produce when an option is /// selected. - /// - /// [`PickList`]: struct.PickList.html - /// [`State`]: struct.State.html pub fn new( state: &'a mut State<T>, options: impl Into<Cow<'a, [T]>>, @@ -69,6 +75,7 @@ where ) -> Self { let State { menu, + keyboard_modifiers, is_open, hovered_option, last_selection, @@ -76,11 +83,13 @@ where Self { menu, + keyboard_modifiers, is_open, hovered_option, last_selection, on_selected: Box::new(on_selected), options: options.into(), + placeholder: None, selected, width: Length::Shrink, text_size: None, @@ -90,41 +99,37 @@ where } } + /// Sets the placeholder of the [`PickList`]. + pub fn placeholder(mut self, placeholder: impl Into<String>) -> Self { + self.placeholder = Some(placeholder.into()); + self + } + /// Sets the width of the [`PickList`]. - /// - /// [`PickList`]: struct.PickList.html pub fn width(mut self, width: Length) -> Self { self.width = width; self } - /// Sets the padding of the [`PickList`]. - /// - /// [`PickList`]: struct.PickList.html - pub fn padding(mut self, padding: u16) -> Self { - self.padding = padding; + /// Sets the [`Padding`] of the [`PickList`]. + pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self { + self.padding = padding.into(); self } /// Sets the text size of the [`PickList`]. - /// - /// [`PickList`]: struct.PickList.html pub fn text_size(mut self, size: u16) -> Self { self.text_size = Some(size); self } /// Sets the font of the [`PickList`]. - /// - /// [`PickList`]: struct.PickList.html pub fn font(mut self, font: Renderer::Font) -> Self { self.font = font; self } /// Sets the style of the [`PickList`]. - /// - /// [`PickList`]: struct.PickList.html pub fn style( mut self, style: impl Into<<Renderer as self::Renderer>::Style>, @@ -143,7 +148,7 @@ where Renderer: self::Renderer + scrollable::Renderer + 'a, { fn width(&self) -> Length { - Length::Shrink + self.width } fn height(&self) -> Length { @@ -160,27 +165,37 @@ where let limits = limits .width(self.width) .height(Length::Shrink) - .pad(f32::from(self.padding)); + .pad(self.padding); let text_size = self.text_size.unwrap_or(renderer.default_size()); + let font = self.font; let max_width = match self.width { Length::Shrink => { + let measure = |label: &str| -> u32 { + let (width, _) = renderer.measure( + label, + text_size, + font, + Size::new(f32::INFINITY, f32::INFINITY), + ); + + width.round() as u32 + }; + let labels = self.options.iter().map(ToString::to_string); - labels - .map(|label| { - let (width, _) = renderer.measure( - &label, - text_size, - Renderer::Font::default(), - Size::new(f32::INFINITY, f32::INFINITY), - ); - - width.round() as u32 - }) - .max() - .unwrap_or(100) + let labels_width = + labels.map(|label| measure(&label)).max().unwrap_or(100); + + let placeholder_width = self + .placeholder + .as_ref() + .map(String::as_str) + .map(measure) + .unwrap_or(100); + + labels_width.max(placeholder_width) } _ => 0, }; @@ -189,11 +204,11 @@ where let intrinsic = Size::new( max_width as f32 + f32::from(text_size) - + f32::from(self.padding), + + f32::from(self.padding.left), f32::from(text_size), ); - limits.resolve(intrinsic).pad(f32::from(self.padding)) + limits.resolve(intrinsic).pad(self.padding) }; layout::Node::new(size) @@ -204,6 +219,8 @@ where match self.width { Length::Shrink => { + self.placeholder.hash(state); + self.options .iter() .map(ToString::to_string) @@ -220,16 +237,19 @@ where event: Event, layout: Layout<'_>, cursor_position: Point, - messages: &mut Vec<Message>, _renderer: &Renderer, - _clipboard: Option<&dyn Clipboard>, - ) { + _clipboard: &mut dyn Clipboard, + messages: &mut Vec<Message>, + ) -> event::Status { match event { - Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => { - if *self.is_open { + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerPressed { .. }) => { + let event_status = if *self.is_open { // TODO: Encode cursor availability in the type system *self.is_open = cursor_position.x < 0.0 || cursor_position.y < 0.0; + + event::Status::Captured } else if layout.bounds().contains(cursor_position) { let selected = self.selected.as_ref(); @@ -238,15 +258,65 @@ where .options .iter() .position(|option| Some(option) == selected); - } + + event::Status::Captured + } else { + event::Status::Ignored + }; if let Some(last_selection) = self.last_selection.take() { messages.push((self.on_selected)(last_selection)); *self.is_open = false; + + event::Status::Captured + } else { + event_status + } + } + Event::Mouse(mouse::Event::WheelScrolled { + delta: mouse::ScrollDelta::Lines { y, .. }, + }) if self.keyboard_modifiers.command() + && layout.bounds().contains(cursor_position) + && !*self.is_open => + { + fn find_next<'a, T: PartialEq>( + selected: &'a T, + mut options: impl Iterator<Item = &'a T>, + ) -> Option<&'a T> { + let _ = options.find(|&option| option == selected); + + options.next() } + + let next_option = if y < 0.0 { + if let Some(selected) = self.selected.as_ref() { + find_next(selected, self.options.iter()) + } else { + self.options.first() + } + } else if y > 0.0 { + if let Some(selected) = self.selected.as_ref() { + find_next(selected, self.options.iter().rev()) + } else { + self.options.last() + } + } else { + None + }; + + if let Some(next_option) = next_option { + messages.push((self.on_selected)(next_option.clone())); + } + + event::Status::Captured + } + Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => { + *self.keyboard_modifiers = modifiers; + + event::Status::Ignored } - _ => {} + _ => event::Status::Ignored, } } @@ -256,12 +326,14 @@ where _defaults: &Renderer::Defaults, layout: Layout<'_>, cursor_position: Point, + _viewport: &Rectangle, ) -> Renderer::Output { self::Renderer::draw( renderer, layout.bounds(), cursor_position, self.selected.as_ref().map(ToString::to_string), + self.placeholder.as_ref().map(String::as_str), self.padding, self.text_size.unwrap_or(renderer.default_size()), self.font, @@ -303,36 +375,27 @@ where /// Your [renderer] will need to implement this trait before being /// able to use a [`PickList`] in your user interface. /// -/// [`PickList`]: struct.PickList.html -/// [renderer]: ../../renderer/index.html +/// [renderer]: crate::renderer pub trait Renderer: text::Renderer + menu::Renderer { /// The default padding of a [`PickList`]. - /// - /// [`PickList`]: struct.PickList.html - const DEFAULT_PADDING: u16; + const DEFAULT_PADDING: Padding; /// The [`PickList`] style supported by this renderer. - /// - /// [`PickList`]: struct.PickList.html type Style: Default; /// Returns the style of the [`Menu`] of the [`PickList`]. - /// - /// [`Menu`]: ../../overlay/menu/struct.Menu.html - /// [`PickList`]: struct.PickList.html fn menu_style( style: &<Self as Renderer>::Style, ) -> <Self as menu::Renderer>::Style; /// Draws a [`PickList`]. - /// - /// [`PickList`]: struct.PickList.html fn draw( &mut self, bounds: Rectangle, cursor_position: Point, selected: Option<String>, - padding: u16, + placeholder: Option<&str>, + padding: Padding, text_size: u16, font: Self::Font, style: &<Self as Renderer>::Style, diff --git a/native/src/widget/progress_bar.rs b/native/src/widget/progress_bar.rs index 5ab76d47..d294f198 100644 --- a/native/src/widget/progress_bar.rs +++ b/native/src/widget/progress_bar.rs @@ -33,8 +33,6 @@ impl<Renderer: self::Renderer> ProgressBar<Renderer> { /// It expects: /// * an inclusive range of possible values /// * the current value of the [`ProgressBar`] - /// - /// [`ProgressBar`]: struct.ProgressBar.html pub fn new(range: RangeInclusive<f32>, value: f32) -> Self { ProgressBar { value: value.max(*range.start()).min(*range.end()), @@ -46,24 +44,18 @@ impl<Renderer: self::Renderer> ProgressBar<Renderer> { } /// Sets the width of the [`ProgressBar`]. - /// - /// [`ProgressBar`]: struct.ProgressBar.html pub fn width(mut self, width: Length) -> Self { self.width = width; self } /// Sets the height of the [`ProgressBar`]. - /// - /// [`ProgressBar`]: struct.ProgressBar.html pub fn height(mut self, height: Length) -> Self { self.height = Some(height); self } /// Sets the style of the [`ProgressBar`]. - /// - /// [`ProgressBar`]: struct.ProgressBar.html pub fn style(mut self, style: impl Into<Renderer::Style>) -> Self { self.style = style.into(); self @@ -104,6 +96,7 @@ where _defaults: &Renderer::Defaults, layout: Layout<'_>, _cursor_position: Point, + _viewport: &Rectangle, ) -> Renderer::Output { renderer.draw( layout.bounds(), @@ -127,15 +120,12 @@ where /// Your [renderer] will need to implement this trait before being /// able to use a [`ProgressBar`] in your user interface. /// -/// [`ProgressBar`]: struct.ProgressBar.html -/// [renderer]: ../../renderer/index.html +/// [renderer]: crate::renderer pub trait Renderer: crate::Renderer { /// The style supported by this renderer. type Style: Default; /// The default height of a [`ProgressBar`]. - /// - /// [`ProgressBar`]: struct.ProgressBar.html const DEFAULT_HEIGHT: u16; /// Draws a [`ProgressBar`]. @@ -146,8 +136,6 @@ pub trait Renderer: crate::Renderer { /// * the current value of the [`ProgressBar`] /// * maybe a specific background of the [`ProgressBar`] /// * maybe a specific active color of the [`ProgressBar`] - /// - /// [`ProgressBar`]: struct.ProgressBar.html fn draw( &self, bounds: Rectangle, diff --git a/native/src/widget/radio.rs b/native/src/widget/radio.rs index 5b8d00e9..dee82d1f 100644 --- a/native/src/widget/radio.rs +++ b/native/src/widget/radio.rs @@ -1,12 +1,17 @@ //! Create choices using radio buttons. +use std::hash::Hash; + +use crate::event::{self, Event}; +use crate::mouse; +use crate::row; +use crate::text; +use crate::touch; +use crate::{layout, Color}; use crate::{ - layout, mouse, row, text, Align, Clipboard, Element, Event, Hasher, - HorizontalAlignment, Layout, Length, Point, Rectangle, Row, Text, - VerticalAlignment, Widget, + Align, Clipboard, Element, Hasher, HorizontalAlignment, Layout, Length, + Point, Rectangle, Row, Text, VerticalAlignment, Widget, }; -use std::hash::Hash; - /// A circular button representing a choice. /// /// # Example @@ -42,11 +47,15 @@ pub struct Radio<Message, Renderer: self::Renderer + text::Renderer> { size: u16, spacing: u16, text_size: Option<u16>, + text_color: Option<Color>, + font: Renderer::Font, style: Renderer::Style, } impl<Message, Renderer: self::Renderer + text::Renderer> Radio<Message, Renderer> +where + Message: Clone, { /// Creates a new [`Radio`] button. /// @@ -56,8 +65,6 @@ impl<Message, Renderer: self::Renderer + text::Renderer> /// * 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`. - /// - /// [`Radio`]: struct.Radio.html pub fn new<F, V>( value: V, label: impl Into<String>, @@ -76,45 +83,49 @@ impl<Message, Renderer: self::Renderer + text::Renderer> size: <Renderer as self::Renderer>::DEFAULT_SIZE, spacing: Renderer::DEFAULT_SPACING, //15 text_size: None, + text_color: None, + font: Default::default(), style: Renderer::Style::default(), } } /// Sets the size of the [`Radio`] button. - /// - /// [`Radio`]: struct.Radio.html pub fn size(mut self, size: u16) -> Self { self.size = size; self } /// Sets the width of the [`Radio`] button. - /// - /// [`Radio`]: struct.Radio.html pub fn width(mut self, width: Length) -> Self { self.width = width; self } /// Sets the spacing between the [`Radio`] button and the text. - /// - /// [`Radio`]: struct.Radio.html pub fn spacing(mut self, spacing: u16) -> Self { self.spacing = spacing; self } /// Sets the text size of the [`Radio`] button. - /// - /// [`Radio`]: struct.Radio.html pub fn text_size(mut self, text_size: u16) -> Self { self.text_size = Some(text_size); self } + /// Sets the text color of the [`Radio`] button. + pub fn text_color(mut self, color: Color) -> Self { + self.text_color = Some(color); + self + } + + /// Sets the text font of the [`Radio`] button. + pub fn font(mut self, font: Renderer::Font) -> Self { + self.font = font; + self + } + /// Sets the style of the [`Radio`] button. - /// - /// [`Radio`]: struct.Radio.html pub fn style(mut self, style: impl Into<Renderer::Style>) -> Self { self.style = style.into(); self @@ -123,8 +134,8 @@ impl<Message, Renderer: self::Renderer + text::Renderer> impl<Message, Renderer> Widget<Message, Renderer> for Radio<Message, Renderer> where - Renderer: self::Renderer + text::Renderer + row::Renderer, Message: Clone, + Renderer: self::Renderer + text::Renderer + row::Renderer, { fn width(&self) -> Length { self.width @@ -161,18 +172,23 @@ where event: Event, layout: Layout<'_>, cursor_position: Point, - messages: &mut Vec<Message>, _renderer: &Renderer, - _clipboard: Option<&dyn Clipboard>, - ) { + _clipboard: &mut dyn Clipboard, + messages: &mut Vec<Message>, + ) -> event::Status { match event { - Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => { + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerPressed { .. }) => { if layout.bounds().contains(cursor_position) { messages.push(self.on_click.clone()); + + return event::Status::Captured; } } _ => {} } + + event::Status::Ignored } fn draw( @@ -181,6 +197,7 @@ where defaults: &Renderer::Defaults, layout: Layout<'_>, cursor_position: Point, + _viewport: &Rectangle, ) -> Renderer::Output { let bounds = layout.bounds(); let mut children = layout.children(); @@ -195,8 +212,8 @@ where label_layout.bounds(), &self.label, self.text_size.unwrap_or(renderer.default_size()), - Default::default(), - None, + self.font, + self.text_color, HorizontalAlignment::Left, VerticalAlignment::Center, ); @@ -226,20 +243,15 @@ where /// Your [renderer] will need to implement this trait before being /// able to use a [`Radio`] button in your user interface. /// -/// [`Radio`]: struct.Radio.html -/// [renderer]: ../../renderer/index.html +/// [renderer]: crate::renderer pub trait Renderer: crate::Renderer { /// The style supported by this renderer. type Style: Default; /// The default size of a [`Radio`] button. - /// - /// [`Radio`]: struct.Radio.html const DEFAULT_SIZE: u16; /// The default spacing of a [`Radio`] button. - /// - /// [`Radio`]: struct.Radio.html const DEFAULT_SPACING: u16; /// Draws a [`Radio`] button. @@ -249,8 +261,6 @@ pub trait Renderer: crate::Renderer { /// * whether the [`Radio`] is selected or not /// * whether the mouse is over the [`Radio`] or not /// * the drawn label of the [`Radio`] - /// - /// [`Radio`]: struct.Radio.html fn draw( &mut self, bounds: Rectangle, @@ -264,8 +274,8 @@ pub trait Renderer: crate::Renderer { impl<'a, Message, Renderer> From<Radio<Message, Renderer>> for Element<'a, Message, Renderer> where - Renderer: 'a + self::Renderer + row::Renderer + text::Renderer, Message: 'a + Clone, + Renderer: 'a + self::Renderer + row::Renderer + text::Renderer, { fn from(radio: Radio<Message, Renderer>) -> Element<'a, Message, Renderer> { Element::new(radio) diff --git a/native/src/widget/row.rs b/native/src/widget/row.rs index 2b6db224..9ebc9145 100644 --- a/native/src/widget/row.rs +++ b/native/src/widget/row.rs @@ -1,22 +1,20 @@ //! Distribute content horizontally. -use std::hash::Hash; - +use crate::event::{self, Event}; +use crate::layout; +use crate::overlay; use crate::{ - layout, overlay, Align, Clipboard, Element, Event, Hasher, Layout, Length, - Point, Widget, + Align, Clipboard, Element, Hasher, Layout, Length, Padding, Point, + Rectangle, Widget, }; +use std::hash::Hash; use std::u32; /// A container that distributes its contents horizontally. -/// -/// A [`Row`] will try to fill the horizontal space of its container. -/// -/// [`Row`]: struct.Row.html #[allow(missing_debug_implementations)] pub struct Row<'a, Message, Renderer> { spacing: u16, - padding: u16, + padding: Padding, width: Length, height: Length, max_width: u32, @@ -27,21 +25,17 @@ pub struct Row<'a, Message, Renderer> { impl<'a, Message, Renderer> Row<'a, Message, Renderer> { /// Creates an empty [`Row`]. - /// - /// [`Row`]: struct.Row.html pub fn new() -> Self { Self::with_children(Vec::new()) } /// Creates a [`Row`] with the given elements. - /// - /// [`Row`]: struct.Row.html pub fn with_children( children: Vec<Element<'a, Message, Renderer>>, ) -> Self { Row { spacing: 0, - padding: 0, + padding: Padding::ZERO, width: Length::Shrink, height: Length::Shrink, max_width: u32::MAX, @@ -61,58 +55,43 @@ impl<'a, Message, Renderer> Row<'a, Message, Renderer> { self } - /// Sets the padding of the [`Row`]. - /// - /// [`Row`]: struct.Row.html - pub fn padding(mut self, units: u16) -> Self { - self.padding = units; + /// Sets the [`Padding`] of the [`Row`]. + pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self { + self.padding = padding.into(); self } /// Sets the width of the [`Row`]. - /// - /// [`Row`]: struct.Row.html pub fn width(mut self, width: Length) -> Self { self.width = width; self } /// Sets the height of the [`Row`]. - /// - /// [`Row`]: struct.Row.html pub fn height(mut self, height: Length) -> Self { self.height = height; self } /// Sets the maximum width of the [`Row`]. - /// - /// [`Row`]: struct.Row.html pub fn max_width(mut self, max_width: u32) -> Self { self.max_width = max_width; self } /// Sets the maximum height of the [`Row`]. - /// - /// [`Row`]: struct.Row.html pub fn max_height(mut self, max_height: u32) -> Self { self.max_height = max_height; self } /// Sets the vertical alignment of the contents of the [`Row`] . - /// - /// [`Row`]: struct.Row.html pub fn align_items(mut self, align: Align) -> Self { self.align_items = align; self } /// Adds an [`Element`] to the [`Row`]. - /// - /// [`Element`]: ../struct.Element.html - /// [`Row`]: struct.Row.html pub fn push<E>(mut self, child: E) -> Self where E: Into<Element<'a, Message, Renderer>>, @@ -150,7 +129,7 @@ where layout::flex::Axis::Horizontal, renderer, &limits, - self.padding as f32, + self.padding, self.spacing as f32, self.align_items, &self.children, @@ -162,22 +141,24 @@ where event: Event, layout: Layout<'_>, cursor_position: Point, - messages: &mut Vec<Message>, renderer: &Renderer, - clipboard: Option<&dyn Clipboard>, - ) { - self.children.iter_mut().zip(layout.children()).for_each( - |(child, layout)| { + clipboard: &mut dyn Clipboard, + messages: &mut Vec<Message>, + ) -> event::Status { + self.children + .iter_mut() + .zip(layout.children()) + .map(|(child, layout)| { child.widget.on_event( event.clone(), layout, cursor_position, - messages, renderer, clipboard, + messages, ) - }, - ); + }) + .fold(event::Status::Ignored, event::Status::merge) } fn draw( @@ -186,8 +167,15 @@ where defaults: &Renderer::Defaults, layout: Layout<'_>, cursor_position: Point, + viewport: &Rectangle, ) -> Renderer::Output { - renderer.draw(defaults, &self.children, layout, cursor_position) + renderer.draw( + defaults, + &self.children, + layout, + cursor_position, + viewport, + ) } fn hash_layout(&self, state: &mut Hasher) { @@ -200,7 +188,7 @@ where self.max_height.hash(state); self.align_items.hash(state); self.spacing.hash(state); - self.spacing.hash(state); + self.padding.hash(state); for child in &self.children { child.widget.hash_layout(state); @@ -224,8 +212,7 @@ where /// Your [renderer] will need to implement this trait before being /// able to use a [`Row`] in your user interface. /// -/// [`Row`]: struct.Row.html -/// [renderer]: ../../renderer/index.html +/// [renderer]: crate::renderer pub trait Renderer: crate::Renderer + Sized { /// Draws a [`Row`]. /// @@ -233,15 +220,13 @@ pub trait Renderer: crate::Renderer + Sized { /// - the children of the [`Row`] /// - the [`Layout`] of the [`Row`] and its children /// - the cursor position - /// - /// [`Row`]: struct.Row.html - /// [`Layout`]: ../layout/struct.Layout.html fn draw<Message>( &mut self, defaults: &Self::Defaults, children: &[Element<'_, Message, Self>], layout: Layout<'_>, cursor_position: Point, + viewport: &Rectangle, ) -> Self::Output; } diff --git a/native/src/widget/rule.rs b/native/src/widget/rule.rs index 25cec53b..18c88658 100644 --- a/native/src/widget/rule.rs +++ b/native/src/widget/rule.rs @@ -17,8 +17,6 @@ pub struct Rule<Renderer: self::Renderer> { impl<Renderer: self::Renderer> Rule<Renderer> { /// Creates a horizontal [`Rule`] for dividing content by the given vertical spacing. - /// - /// [`Rule`]: struct.Rule.html pub fn horizontal(spacing: u16) -> Self { Rule { width: Length::Fill, @@ -29,8 +27,6 @@ impl<Renderer: self::Renderer> Rule<Renderer> { } /// Creates a vertical [`Rule`] for dividing content by the given horizontal spacing. - /// - /// [`Rule`]: struct.Rule.html pub fn vertical(spacing: u16) -> Self { Rule { width: Length::from(Length::Units(spacing)), @@ -41,8 +37,6 @@ impl<Renderer: self::Renderer> Rule<Renderer> { } /// Sets the style of the [`Rule`]. - /// - /// [`Rule`]: struct.Rule.html pub fn style(mut self, style: impl Into<Renderer::Style>) -> Self { self.style = style.into(); self @@ -77,6 +71,7 @@ where _defaults: &Renderer::Defaults, layout: Layout<'_>, _cursor_position: Point, + _viewport: &Rectangle, ) -> Renderer::Output { renderer.draw(layout.bounds(), &self.style, self.is_horizontal) } @@ -91,8 +86,6 @@ where } /// The renderer of a [`Rule`]. -/// -/// [`Rule`]: struct.Rule.html pub trait Renderer: crate::Renderer { /// The style supported by this renderer. type Style: Default; @@ -103,8 +96,6 @@ pub trait Renderer: crate::Renderer { /// * the bounds of the [`Rule`] /// * the style of the [`Rule`] /// * whether the [`Rule`] is horizontal (true) or vertical (false) - /// - /// [`Rule`]: struct.Rule.html fn draw( &mut self, bounds: Rectangle, diff --git a/native/src/widget/scrollable.rs b/native/src/widget/scrollable.rs index 75e97027..68da2e67 100644 --- a/native/src/widget/scrollable.rs +++ b/native/src/widget/scrollable.rs @@ -1,7 +1,13 @@ //! Navigate an endless amount of content with a scrollbar. +use crate::column; +use crate::event::{self, Event}; +use crate::layout; +use crate::mouse; +use crate::overlay; +use crate::touch; use crate::{ - column, layout, mouse, overlay, Align, Clipboard, Column, Element, Event, - Hasher, Layout, Length, Point, Rectangle, Size, Vector, Widget, + Align, Clipboard, Column, Element, Hasher, Layout, Length, Padding, Point, + Rectangle, Size, Vector, Widget, }; use std::{f32, hash::Hash, u32}; @@ -13,21 +19,26 @@ pub struct Scrollable<'a, Message, Renderer: self::Renderer> { state: &'a mut State, height: Length, max_height: u32, + scrollbar_width: u16, + scrollbar_margin: u16, + scroller_width: u16, content: Column<'a, Message, Renderer>, + on_scroll: Option<Box<dyn Fn(f32) -> Message>>, style: Renderer::Style, } impl<'a, Message, Renderer: self::Renderer> Scrollable<'a, Message, Renderer> { /// Creates a new [`Scrollable`] with the given [`State`]. - /// - /// [`Scrollable`]: struct.Scrollable.html - /// [`State`]: struct.State.html pub fn new(state: &'a mut State) -> Self { Scrollable { state, height: Length::Shrink, max_height: u32::MAX, + scrollbar_width: 10, + scrollbar_margin: 0, + scroller_width: 10, content: Column::new(), + on_scroll: None, style: Renderer::Style::default(), } } @@ -42,65 +53,79 @@ impl<'a, Message, Renderer: self::Renderer> Scrollable<'a, Message, Renderer> { self } - /// Sets the padding of the [`Scrollable`]. - /// - /// [`Scrollable`]: struct.Scrollable.html - pub fn padding(mut self, units: u16) -> Self { - self.content = self.content.padding(units); + /// Sets the [`Padding`] of the [`Scrollable`]. + pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self { + self.content = self.content.padding(padding); self } /// Sets the width of the [`Scrollable`]. - /// - /// [`Scrollable`]: struct.Scrollable.html pub fn width(mut self, width: Length) -> Self { self.content = self.content.width(width); self } /// Sets the height of the [`Scrollable`]. - /// - /// [`Scrollable`]: struct.Scrollable.html pub fn height(mut self, height: Length) -> Self { self.height = height; self } /// Sets the maximum width of the [`Scrollable`]. - /// - /// [`Scrollable`]: struct.Scrollable.html pub fn max_width(mut self, max_width: u32) -> Self { self.content = self.content.max_width(max_width); self } /// Sets the maximum height of the [`Scrollable`] in pixels. - /// - /// [`Scrollable`]: struct.Scrollable.html pub fn max_height(mut self, max_height: u32) -> Self { self.max_height = max_height; self } /// Sets the horizontal alignment of the contents of the [`Scrollable`] . - /// - /// [`Scrollable`]: struct.Scrollable.html pub fn align_items(mut self, align_items: Align) -> Self { self.content = self.content.align_items(align_items); self } - /// Sets the style of the [`Scrollable`] . + /// Sets the scrollbar width of the [`Scrollable`] . + /// Silently enforces a minimum value of 1. + pub fn scrollbar_width(mut self, scrollbar_width: u16) -> Self { + self.scrollbar_width = scrollbar_width.max(1); + self + } + + /// Sets the scrollbar margin of the [`Scrollable`] . + pub fn scrollbar_margin(mut self, scrollbar_margin: u16) -> Self { + self.scrollbar_margin = scrollbar_margin; + self + } + + /// Sets the scroller width of the [`Scrollable`] . /// - /// [`Scrollable`]: struct.Scrollable.html + /// It silently enforces a minimum value of 1. + pub fn scroller_width(mut self, scroller_width: u16) -> Self { + self.scroller_width = scroller_width.max(1); + self + } + + /// Sets a function to call when the [`Scrollable`] is scrolled. + /// + /// The function takes the new relative offset of the [`Scrollable`] + /// (e.g. `0` means top, while `1` means bottom). + pub fn on_scroll(mut self, f: impl Fn(f32) -> Message + 'static) -> Self { + self.on_scroll = Some(Box::new(f)); + self + } + + /// Sets the style of the [`Scrollable`] . pub fn style(mut self, style: impl Into<Renderer::Style>) -> Self { self.style = style.into(); self } /// Adds an element to the [`Scrollable`]. - /// - /// [`Scrollable`]: struct.Scrollable.html pub fn push<E>(mut self, child: E) -> Self where E: Into<Element<'a, Message, Renderer>>, @@ -108,6 +133,24 @@ impl<'a, Message, Renderer: self::Renderer> Scrollable<'a, Message, Renderer> { self.content = self.content.push(child); self } + + fn notify_on_scroll( + &self, + bounds: Rectangle, + content_bounds: Rectangle, + messages: &mut Vec<Message>, + ) { + if content_bounds.height <= bounds.height { + return; + } + + if let Some(on_scroll) = &self.on_scroll { + messages.push(on_scroll( + self.state.offset.absolute(bounds, content_bounds) + / (content_bounds.height - bounds.height), + )); + } + } } impl<'a, Message, Renderer> Widget<Message, Renderer> @@ -149,17 +192,59 @@ where event: Event, layout: Layout<'_>, cursor_position: Point, - messages: &mut Vec<Message>, renderer: &Renderer, - clipboard: Option<&dyn Clipboard>, - ) { + clipboard: &mut dyn Clipboard, + messages: &mut Vec<Message>, + ) -> event::Status { let bounds = layout.bounds(); let is_mouse_over = bounds.contains(cursor_position); let content = layout.children().next().unwrap(); let content_bounds = content.bounds(); - // TODO: Event capture. Nested scrollables should capture scroll events. + let offset = self.state.offset(bounds, content_bounds); + let scrollbar = renderer.scrollbar( + bounds, + content_bounds, + offset, + self.scrollbar_width, + self.scrollbar_margin, + self.scroller_width, + ); + let is_mouse_over_scrollbar = scrollbar + .as_ref() + .map(|scrollbar| scrollbar.is_mouse_over(cursor_position)) + .unwrap_or(false); + + let event_status = { + let cursor_position = if is_mouse_over && !is_mouse_over_scrollbar { + Point::new( + cursor_position.x, + cursor_position.y + + self.state.offset(bounds, content_bounds) as f32, + ) + } else { + // TODO: Make `cursor_position` an `Option<Point>` so we can encode + // cursor availability. + // This will probably happen naturally once we add multi-window + // support. + Point::new(cursor_position.x, -1.0) + }; + + self.content.on_event( + event.clone(), + content, + cursor_position, + renderer, + clipboard, + messages, + ) + }; + + if let event::Status::Captured = event_status { + return event::Status::Captured; + } + if is_mouse_over { match event { Event::Mouse(mouse::Event::WheelScrolled { delta }) => { @@ -172,26 +257,65 @@ where self.state.scroll(y, bounds, content_bounds); } } + + self.notify_on_scroll(bounds, content_bounds, messages); + + return event::Status::Captured; + } + Event::Touch(event) => { + match event { + touch::Event::FingerPressed { .. } => { + self.state.scroll_box_touched_at = + Some(cursor_position); + } + touch::Event::FingerMoved { .. } => { + if let Some(scroll_box_touched_at) = + self.state.scroll_box_touched_at + { + let delta = + cursor_position.y - scroll_box_touched_at.y; + + self.state.scroll( + delta, + bounds, + content_bounds, + ); + + self.state.scroll_box_touched_at = + Some(cursor_position); + + self.notify_on_scroll( + bounds, + content_bounds, + messages, + ); + } + } + touch::Event::FingerLifted { .. } + | touch::Event::FingerLost { .. } => { + self.state.scroll_box_touched_at = None; + } + } + + return event::Status::Captured; } _ => {} } } - let offset = self.state.offset(bounds, content_bounds); - let scrollbar = renderer.scrollbar(bounds, content_bounds, offset); - let is_mouse_over_scrollbar = scrollbar - .as_ref() - .map(|scrollbar| scrollbar.is_mouse_over(cursor_position)) - .unwrap_or(false); - if self.state.is_scroller_grabbed() { match event { Event::Mouse(mouse::Event::ButtonReleased( mouse::Button::Left, - )) => { + )) + | Event::Touch(touch::Event::FingerLifted { .. }) + | Event::Touch(touch::Event::FingerLost { .. }) => { self.state.scroller_grabbed_at = None; + + return event::Status::Captured; } - Event::Mouse(mouse::Event::CursorMoved { .. }) => { + Event::Mouse(mouse::Event::CursorMoved { .. }) + | Event::Touch(touch::Event::FingerMoved { .. }) => { if let (Some(scrollbar), Some(scroller_grabbed_at)) = (scrollbar, self.state.scroller_grabbed_at) { @@ -203,6 +327,10 @@ where bounds, content_bounds, ); + + self.notify_on_scroll(bounds, content_bounds, messages); + + return event::Status::Captured; } } _ => {} @@ -211,7 +339,8 @@ where match event { Event::Mouse(mouse::Event::ButtonPressed( mouse::Button::Left, - )) => { + )) + | Event::Touch(touch::Event::FingerPressed { .. }) => { if let Some(scrollbar) = scrollbar { if let Some(scroller_grabbed_at) = scrollbar.grab_scroller(cursor_position) @@ -227,6 +356,14 @@ where self.state.scroller_grabbed_at = Some(scroller_grabbed_at); + + self.notify_on_scroll( + bounds, + content_bounds, + messages, + ); + + return event::Status::Captured; } } } @@ -234,28 +371,7 @@ where } } - let cursor_position = if is_mouse_over && !is_mouse_over_scrollbar { - Point::new( - cursor_position.x, - cursor_position.y - + self.state.offset(bounds, content_bounds) as f32, - ) - } else { - // TODO: Make `cursor_position` an `Option<Point>` so we can encode - // cursor availability. - // This will probably happen naturally once we add multi-window - // support. - Point::new(cursor_position.x, -1.0) - }; - - self.content.on_event( - event, - content, - cursor_position, - messages, - renderer, - clipboard, - ) + event::Status::Ignored } fn draw( @@ -264,12 +380,20 @@ where defaults: &Renderer::Defaults, layout: Layout<'_>, cursor_position: Point, + _viewport: &Rectangle, ) -> Renderer::Output { let bounds = layout.bounds(); let content_layout = layout.children().next().unwrap(); let content_bounds = content_layout.bounds(); let offset = self.state.offset(bounds, content_bounds); - let scrollbar = renderer.scrollbar(bounds, content_bounds, offset); + let scrollbar = renderer.scrollbar( + bounds, + content_bounds, + offset, + self.scrollbar_width, + self.scrollbar_margin, + self.scroller_width, + ); let is_mouse_over = bounds.contains(cursor_position); let is_mouse_over_scrollbar = scrollbar @@ -289,6 +413,10 @@ where defaults, content_layout, cursor_position, + &Rectangle { + y: bounds.y + offset as f32, + ..bounds + }, ) }; @@ -336,27 +464,54 @@ where } /// The local state of a [`Scrollable`]. -/// -/// [`Scrollable`]: struct.Scrollable.html -#[derive(Debug, Clone, Copy, Default)] +#[derive(Debug, Clone, Copy)] pub struct State { scroller_grabbed_at: Option<f32>, - offset: f32, + scroll_box_touched_at: Option<Point>, + offset: Offset, +} + +impl Default for State { + fn default() -> Self { + Self { + scroller_grabbed_at: None, + scroll_box_touched_at: None, + offset: Offset::Absolute(0.0), + } + } +} + +/// The local state of a [`Scrollable`]. +#[derive(Debug, Clone, Copy)] +enum Offset { + Absolute(f32), + Relative(f32), +} + +impl Offset { + fn absolute(self, bounds: Rectangle, content_bounds: Rectangle) -> f32 { + match self { + Self::Absolute(absolute) => { + let hidden_content = + (content_bounds.height - bounds.height).max(0.0); + + absolute.min(hidden_content) + } + Self::Relative(percentage) => { + ((content_bounds.height - bounds.height) * percentage).max(0.0) + } + } + } } impl State { /// Creates a new [`State`] with the scrollbar located at the top. - /// - /// [`State`]: struct.State.html pub fn new() -> Self { State::default() } /// Apply a scrolling offset to the current [`State`], given the bounds of /// the [`Scrollable`] and its contents. - /// - /// [`Scrollable`]: struct.Scrollable.html - /// [`State`]: struct.State.html pub fn scroll( &mut self, delta_y: f32, @@ -367,70 +522,83 @@ impl State { return; } - self.offset = (self.offset - delta_y) - .max(0.0) - .min((content_bounds.height - bounds.height) as f32); + self.offset = Offset::Absolute( + (self.offset.absolute(bounds, content_bounds) - delta_y) + .max(0.0) + .min((content_bounds.height - bounds.height) as f32), + ); } - /// Moves the scroll position to a relative amount, given the bounds of - /// the [`Scrollable`] and its contents. + /// Scrolls the [`Scrollable`] to a relative amount. /// /// `0` represents scrollbar at the top, while `1` represents scrollbar at /// the bottom. - /// - /// [`Scrollable`]: struct.Scrollable.html - /// [`State`]: struct.State.html pub fn scroll_to( &mut self, percentage: f32, bounds: Rectangle, content_bounds: Rectangle, ) { + self.snap_to(percentage); + self.unsnap(bounds, content_bounds); + } + + /// Snaps the scroll position to a relative amount. + /// + /// `0` represents scrollbar at the top, while `1` represents scrollbar at + /// the bottom. + pub fn snap_to(&mut self, percentage: f32) { + self.offset = Offset::Relative(percentage.max(0.0).min(1.0)); + } + + /// 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) { self.offset = - ((content_bounds.height - bounds.height) * percentage).max(0.0); + Offset::Absolute(self.offset.absolute(bounds, content_bounds)); } /// Returns the current scrolling offset of the [`State`], given the bounds /// of the [`Scrollable`] and its contents. - /// - /// [`Scrollable`]: struct.Scrollable.html - /// [`State`]: struct.State.html pub fn offset(&self, bounds: Rectangle, content_bounds: Rectangle) -> u32 { - let hidden_content = - (content_bounds.height - bounds.height).max(0.0).round() as u32; - - self.offset.min(hidden_content as f32) as u32 + self.offset.absolute(bounds, content_bounds) as u32 } /// Returns whether the scroller is currently grabbed or not. pub fn is_scroller_grabbed(&self) -> bool { self.scroller_grabbed_at.is_some() } + + /// Returns whether the scroll box is currently touched or not. + pub fn is_scroll_box_touched(&self) -> bool { + self.scroll_box_touched_at.is_some() + } } /// The scrollbar of a [`Scrollable`]. -/// -/// [`Scrollable`]: struct.Scrollable.html #[derive(Debug)] pub struct Scrollbar { + /// The outer bounds of the scrollable, including the [`Scrollbar`] and + /// [`Scroller`]. + pub outer_bounds: Rectangle, + /// The bounds of the [`Scrollbar`]. - /// - /// [`Scrollbar`]: struct.Scrollbar.html pub bounds: Rectangle, + /// The margin within the [`Scrollbar`]. + pub margin: u16, + /// The bounds of the [`Scroller`]. - /// - /// [`Scroller`]: struct.Scroller.html pub scroller: Scroller, } impl Scrollbar { fn is_mouse_over(&self, cursor_position: Point) -> bool { - self.bounds.contains(cursor_position) + self.outer_bounds.contains(cursor_position) } fn grab_scroller(&self, cursor_position: Point) -> Option<f32> { - if self.bounds.contains(cursor_position) { + if self.outer_bounds.contains(cursor_position) { Some(if self.scroller.bounds.contains(cursor_position) { (cursor_position.y - self.scroller.bounds.y) / self.scroller.bounds.height @@ -455,13 +623,9 @@ impl Scrollbar { } /// The handle of a [`Scrollbar`]. -/// -/// [`Scrollbar`]: struct.Scrollbar.html #[derive(Debug, Clone, Copy)] pub struct Scroller { /// The bounds of the [`Scroller`]. - /// - /// [`Scroller`]: struct.Scrollbar.html pub bounds: Rectangle, } @@ -470,22 +634,21 @@ pub struct Scroller { /// Your [renderer] will need to implement this trait before being /// able to use a [`Scrollable`] in your user interface. /// -/// [`Scrollable`]: struct.Scrollable.html -/// [renderer]: ../../renderer/index.html +/// [renderer]: crate::renderer pub trait Renderer: column::Renderer + Sized { /// The style supported by this renderer. type Style: Default; /// Returns the [`Scrollbar`] given the bounds and content bounds of a /// [`Scrollable`]. - /// - /// [`Scrollbar`]: struct.Scrollbar.html - /// [`Scrollable`]: struct.Scrollable.html fn scrollbar( &self, bounds: Rectangle, content_bounds: Rectangle, offset: u32, + scrollbar_width: u16, + scrollbar_margin: u16, + scroller_width: u16, ) -> Option<Scrollbar>; /// Draws the [`Scrollable`]. @@ -499,10 +662,6 @@ pub trait Renderer: column::Renderer + Sized { /// - a optional [`Scrollbar`] to be rendered /// - the scrolling offset /// - the drawn content - /// - /// [`Scrollbar`]: struct.Scrollbar.html - /// [`Scrollable`]: struct.Scrollable.html - /// [`State`]: struct.State.html fn draw( &mut self, scrollable: &State, diff --git a/native/src/widget/slider.rs b/native/src/widget/slider.rs index c49053f1..2a74d5a3 100644 --- a/native/src/widget/slider.rs +++ b/native/src/widget/slider.rs @@ -1,12 +1,12 @@ //! Display an interactive selector of a single value from a range of values. //! //! A [`Slider`] has some local [`State`]. -//! -//! [`Slider`]: struct.Slider.html -//! [`State`]: struct.State.html +use crate::event::{self, Event}; +use crate::layout; +use crate::mouse; +use crate::touch; use crate::{ - layout, mouse, Clipboard, Element, Event, Hasher, Layout, Length, Point, - Rectangle, Size, Widget, + Clipboard, Element, Hasher, Layout, Length, Point, Rectangle, Size, Widget, }; use std::{hash::Hash, ops::RangeInclusive}; @@ -19,13 +19,12 @@ use std::{hash::Hash, ops::RangeInclusive}; /// The [`Slider`] range of numeric values is generic and its step size defaults /// to 1 unit. /// -/// [`Slider`]: struct.Slider.html -/// /// # Example /// ``` /// # use iced_native::{slider, renderer::Null}; /// # /// # pub type Slider<'a, T, Message> = iced_native::Slider<'a, T, Message, Null>; +/// #[derive(Clone)] /// pub enum Message { /// SliderChanged(f32), /// } @@ -53,6 +52,7 @@ pub struct Slider<'a, T, Message, Renderer: self::Renderer> { impl<'a, T, Message, Renderer> Slider<'a, T, Message, Renderer> where T: Copy + From<u8> + std::cmp::PartialOrd, + Message: Clone, Renderer: self::Renderer, { /// Creates a new [`Slider`]. @@ -64,9 +64,6 @@ where /// * a function that will be called when the [`Slider`] is dragged. /// It receives the new value of the [`Slider`] and must produce a /// `Message`. - /// - /// [`Slider`]: struct.Slider.html - /// [`State`]: struct.State.html pub fn new<F>( state: &'a mut State, range: RangeInclusive<T>, @@ -107,40 +104,30 @@ where /// Typically, the user's interaction with the slider is finished when this message is produced. /// This is useful if you need to spawn a long-running task from the slider's result, where /// the default on_change message could create too many events. - /// - /// [`Slider`]: struct.Slider.html pub fn on_release(mut self, on_release: Message) -> Self { self.on_release = Some(on_release); self } /// Sets the width of the [`Slider`]. - /// - /// [`Slider`]: struct.Slider.html pub fn width(mut self, width: Length) -> Self { self.width = width; self } /// Sets the height of the [`Slider`]. - /// - /// [`Slider`]: struct.Slider.html pub fn height(mut self, height: u16) -> Self { self.height = height; self } /// Sets the style of the [`Slider`]. - /// - /// [`Slider`]: struct.Slider.html pub fn style(mut self, style: impl Into<Renderer::Style>) -> Self { self.style = style.into(); self } /// Sets the step size of the [`Slider`]. - /// - /// [`Slider`]: struct.Slider.html pub fn step(mut self, step: T) -> Self { self.step = step; self @@ -148,8 +135,6 @@ where } /// The local state of a [`Slider`]. -/// -/// [`Slider`]: struct.Slider.html #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub struct State { is_dragging: bool, @@ -157,8 +142,6 @@ pub struct State { impl State { /// Creates a new [`State`]. - /// - /// [`State`]: struct.State.html pub fn new() -> State { State::default() } @@ -168,8 +151,8 @@ impl<'a, T, Message, Renderer> Widget<Message, Renderer> for Slider<'a, T, Message, Renderer> where T: Copy + Into<f64> + num_traits::FromPrimitive, - Renderer: self::Renderer, Message: Clone, + Renderer: self::Renderer, { fn width(&self) -> Length { self.width @@ -197,10 +180,10 @@ where event: Event, layout: Layout<'_>, cursor_position: Point, - messages: &mut Vec<Message>, _renderer: &Renderer, - _clipboard: Option<&dyn Clipboard>, - ) { + _clipboard: &mut dyn Clipboard, + messages: &mut Vec<Message>, + ) -> event::Status { let mut change = || { let bounds = layout.bounds(); if cursor_position.x <= bounds.x { @@ -225,30 +208,39 @@ where }; match event { - Event::Mouse(mouse_event) => match mouse_event { - mouse::Event::ButtonPressed(mouse::Button::Left) => { - if layout.bounds().contains(cursor_position) { - change(); - self.state.is_dragging = true; - } + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerPressed { .. }) => { + if layout.bounds().contains(cursor_position) { + change(); + self.state.is_dragging = true; + + return event::Status::Captured; } - mouse::Event::ButtonReleased(mouse::Button::Left) => { - if self.state.is_dragging { - if let Some(on_release) = self.on_release.clone() { - messages.push(on_release); - } - self.state.is_dragging = false; + } + Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerLifted { .. }) + | Event::Touch(touch::Event::FingerLost { .. }) => { + if self.state.is_dragging { + if let Some(on_release) = self.on_release.clone() { + messages.push(on_release); } + self.state.is_dragging = false; + + return event::Status::Captured; } - mouse::Event::CursorMoved { .. } => { - if self.state.is_dragging { - change(); - } + } + Event::Mouse(mouse::Event::CursorMoved { .. }) + | Event::Touch(touch::Event::FingerMoved { .. }) => { + if self.state.is_dragging { + change(); + + return event::Status::Captured; } - _ => {} - }, + } _ => {} } + + event::Status::Ignored } fn draw( @@ -257,6 +249,7 @@ where _defaults: &Renderer::Defaults, layout: Layout<'_>, cursor_position: Point, + _viewport: &Rectangle, ) -> Renderer::Output { let start = *self.range.start(); let end = *self.range.end(); @@ -284,15 +277,12 @@ where /// Your [renderer] will need to implement this trait before being /// able to use a [`Slider`] in your user interface. /// -/// [`Slider`]: struct.Slider.html -/// [renderer]: ../../renderer/index.html +/// [renderer]: crate::renderer pub trait Renderer: crate::Renderer { /// The style supported by this renderer. type Style: Default; /// The default height of a [`Slider`]. - /// - /// [`Slider`]: struct.Slider.html const DEFAULT_HEIGHT: u16; /// Draws a [`Slider`]. @@ -303,10 +293,6 @@ pub trait Renderer: crate::Renderer { /// * the local state of the [`Slider`] /// * the range of values of the [`Slider`] /// * the current value of the [`Slider`] - /// - /// [`Slider`]: struct.Slider.html - /// [`State`]: struct.State.html - /// [`Class`]: enum.Class.html fn draw( &mut self, bounds: Rectangle, @@ -322,8 +308,8 @@ impl<'a, T, Message, Renderer> From<Slider<'a, T, Message, Renderer>> for Element<'a, Message, Renderer> where T: 'a + Copy + Into<f64> + num_traits::FromPrimitive, - Renderer: 'a + self::Renderer, Message: 'a + Clone, + Renderer: 'a + self::Renderer, { fn from( slider: Slider<'a, T, Message, Renderer>, diff --git a/native/src/widget/space.rs b/native/src/widget/space.rs index f1576ffb..6b34ece8 100644 --- a/native/src/widget/space.rs +++ b/native/src/widget/space.rs @@ -16,15 +16,11 @@ pub struct Space { impl Space { /// Creates an amount of empty [`Space`] with the given width and height. - /// - /// [`Space`]: struct.Space.html pub fn new(width: Length, height: Length) -> Self { Space { width, height } } /// Creates an amount of horizontal [`Space`]. - /// - /// [`Space`]: struct.Space.html pub fn with_width(width: Length) -> Self { Space { width, @@ -33,8 +29,6 @@ impl Space { } /// Creates an amount of vertical [`Space`]. - /// - /// [`Space`]: struct.Space.html pub fn with_height(height: Length) -> Self { Space { width: Length::Shrink, @@ -71,6 +65,7 @@ where _defaults: &Renderer::Defaults, layout: Layout<'_>, _cursor_position: Point, + _viewport: &Rectangle, ) -> Renderer::Output { renderer.draw(layout.bounds()) } @@ -84,14 +79,10 @@ where } /// The renderer of an amount of [`Space`]. -/// -/// [`Space`]: struct.Space.html pub trait Renderer: crate::Renderer { /// Draws an amount of empty [`Space`]. /// /// You should most likely return an empty primitive here. - /// - /// [`Space`]: struct.Space.html fn draw(&mut self, bounds: Rectangle) -> Self::Output; } diff --git a/native/src/widget/svg.rs b/native/src/widget/svg.rs index 114d5e41..9cd61918 100644 --- a/native/src/widget/svg.rs +++ b/native/src/widget/svg.rs @@ -1,5 +1,6 @@ //! Display vector graphics in your application. -use crate::{layout, Element, Hasher, Layout, Length, Point, Size, Widget}; +use crate::layout; +use crate::{Element, Hasher, Layout, Length, Point, Rectangle, Size, Widget}; use std::{ hash::{Hash, Hasher as _}, @@ -13,8 +14,6 @@ use std::{ /// /// [`Svg`] images can have a considerable rendering cost when resized, /// specially when they are complex. -/// -/// [`Svg`]: struct.Svg.html #[derive(Debug, Clone)] pub struct Svg { handle: Handle, @@ -24,9 +23,6 @@ pub struct Svg { impl Svg { /// Creates a new [`Svg`] from the given [`Handle`]. - /// - /// [`Svg`]: struct.Svg.html - /// [`Handle`]: struct.Handle.html pub fn new(handle: impl Into<Handle>) -> Self { Svg { handle: handle.into(), @@ -37,23 +33,17 @@ impl Svg { /// Creates a new [`Svg`] that will display the contents of the file at the /// provided path. - /// - /// [`Svg`]: struct.Svg.html pub fn from_path(path: impl Into<PathBuf>) -> Self { Self::new(Handle::from_path(path)) } /// Sets the width of the [`Svg`]. - /// - /// [`Svg`]: struct.Svg.html pub fn width(mut self, width: Length) -> Self { self.width = width; self } /// Sets the height of the [`Svg`]. - /// - /// [`Svg`]: struct.Svg.html pub fn height(mut self, height: Length) -> Self { self.height = height; self @@ -103,6 +93,7 @@ where _defaults: &Renderer::Defaults, layout: Layout<'_>, _cursor_position: Point, + _viewport: &Rectangle, ) -> Renderer::Output { renderer.draw(self.handle.clone(), layout) } @@ -117,8 +108,6 @@ where } /// An [`Svg`] handle. -/// -/// [`Svg`]: struct.Svg.html #[derive(Debug, Clone)] pub struct Handle { id: u64, @@ -128,8 +117,6 @@ pub struct Handle { impl Handle { /// Creates an SVG [`Handle`] pointing to the vector image of the given /// path. - /// - /// [`Handle`]: struct.Handle.html pub fn from_path(path: impl Into<PathBuf>) -> Handle { Self::from_data(Data::Path(path.into())) } @@ -139,8 +126,6 @@ impl Handle { /// /// This is useful if you already have your SVG data in-memory, maybe /// because you downloaded or generated it procedurally. - /// - /// [`Handle`]: struct.Handle.html pub fn from_memory(bytes: impl Into<Vec<u8>>) -> Handle { Self::from_data(Data::Bytes(bytes.into())) } @@ -156,15 +141,11 @@ impl Handle { } /// Returns the unique identifier of the [`Handle`]. - /// - /// [`Handle`]: struct.Handle.html pub fn id(&self) -> u64 { self.id } /// Returns a reference to the SVG [`Data`]. - /// - /// [`Data`]: enum.Data.html pub fn data(&self) -> &Data { &self.data } @@ -177,8 +158,6 @@ impl Hash for Handle { } /// The data of an [`Svg`]. -/// -/// [`Svg`]: struct.Svg.html #[derive(Clone, Hash)] pub enum Data { /// File data @@ -204,18 +183,12 @@ impl std::fmt::Debug for Data { /// Your [renderer] will need to implement this trait before being able to use /// an [`Svg`] in your user interface. /// -/// [`Svg`]: struct.Svg.html -/// [renderer]: ../../renderer/index.html +/// [renderer]: crate::renderer pub trait Renderer: crate::Renderer { /// Returns the default dimensions of an [`Svg`] for the given [`Handle`]. - /// - /// [`Svg`]: struct.Svg.html - /// [`Handle`]: struct.Handle.html fn dimensions(&self, handle: &Handle) -> (u32, u32); /// Draws an [`Svg`]. - /// - /// [`Svg`]: struct.Svg.html fn draw(&mut self, handle: Handle, layout: Layout<'_>) -> Self::Output; } diff --git a/native/src/widget/text.rs b/native/src/widget/text.rs index 48a69e34..6cc18e6c 100644 --- a/native/src/widget/text.rs +++ b/native/src/widget/text.rs @@ -33,8 +33,6 @@ pub struct Text<Renderer: self::Renderer> { impl<Renderer: self::Renderer> Text<Renderer> { /// Create a new fragment of [`Text`] with the given contents. - /// - /// [`Text`]: struct.Text.html pub fn new<T: Into<String>>(label: T) -> Self { Text { content: label.into(), @@ -49,17 +47,12 @@ impl<Renderer: self::Renderer> Text<Renderer> { } /// Sets the size of the [`Text`]. - /// - /// [`Text`]: struct.Text.html pub fn size(mut self, size: u16) -> Self { self.size = Some(size); self } /// Sets the [`Color`] of the [`Text`]. - /// - /// [`Text`]: struct.Text.html - /// [`Color`]: ../../struct.Color.html pub fn color<C: Into<Color>>(mut self, color: C) -> Self { self.color = Some(color.into()); self @@ -67,33 +60,25 @@ impl<Renderer: self::Renderer> Text<Renderer> { /// Sets the [`Font`] of the [`Text`]. /// - /// [`Text`]: struct.Text.html - /// [`Font`]: ../../struct.Font.html + /// [`Font`]: Renderer::Font pub fn font(mut self, font: impl Into<Renderer::Font>) -> Self { self.font = font.into(); self } /// Sets the width of the [`Text`] boundaries. - /// - /// [`Text`]: struct.Text.html pub fn width(mut self, width: Length) -> Self { self.width = width; self } /// Sets the height of the [`Text`] boundaries. - /// - /// [`Text`]: struct.Text.html pub fn height(mut self, height: Length) -> Self { self.height = height; self } /// Sets the [`HorizontalAlignment`] of the [`Text`]. - /// - /// [`Text`]: struct.Text.html - /// [`HorizontalAlignment`]: enum.HorizontalAlignment.html pub fn horizontal_alignment( mut self, alignment: HorizontalAlignment, @@ -103,9 +88,6 @@ impl<Renderer: self::Renderer> Text<Renderer> { } /// Sets the [`VerticalAlignment`] of the [`Text`]. - /// - /// [`Text`]: struct.Text.html - /// [`VerticalAlignment`]: enum.VerticalAlignment.html pub fn vertical_alignment(mut self, alignment: VerticalAlignment) -> Self { self.vertical_alignment = alignment; self @@ -149,6 +131,7 @@ where defaults: &Renderer::Defaults, layout: Layout<'_>, _cursor_position: Point, + _viewport: &Rectangle, ) -> Renderer::Output { renderer.draw( defaults, @@ -176,26 +159,18 @@ where /// The renderer of a [`Text`] fragment. /// /// Your [renderer] will need to implement this trait before being -/// able to use [`Text`] in your [`UserInterface`]. +/// able to use [`Text`] in your user interface. /// -/// [`Text`]: struct.Text.html -/// [renderer]: ../../renderer/index.html -/// [`UserInterface`]: ../../struct.UserInterface.html +/// [renderer]: crate::Renderer pub trait Renderer: crate::Renderer { /// The font type used for [`Text`]. - /// - /// [`Text`]: struct.Text.html type Font: Default + Copy; /// Returns the default size of [`Text`]. - /// - /// [`Text`]: struct.Text.html fn default_size(&self) -> u16; /// Measures the [`Text`] in the given bounds and returns the minimum /// boundaries that can fit the contents. - /// - /// [`Text`]: struct.Text.html fn measure( &self, content: &str, @@ -213,10 +188,6 @@ pub trait Renderer: crate::Renderer { /// * the color of the [`Text`] /// * the [`HorizontalAlignment`] of the [`Text`] /// * the [`VerticalAlignment`] of the [`Text`] - /// - /// [`Text`]: struct.Text.html - /// [`HorizontalAlignment`]: enum.HorizontalAlignment.html - /// [`VerticalAlignment`]: enum.VerticalAlignment.html fn draw( &mut self, defaults: &Self::Defaults, diff --git a/native/src/widget/text_input.rs b/native/src/widget/text_input.rs index 9e15f4be..cec1e485 100644 --- a/native/src/widget/text_input.rs +++ b/native/src/widget/text_input.rs @@ -1,9 +1,6 @@ //! Display fields that can be filled with text. //! //! A [`TextInput`] has some local [`State`]. -//! -//! [`TextInput`]: struct.TextInput.html -//! [`State`]: struct.State.html mod editor; mod value; @@ -14,10 +11,14 @@ pub use value::Value; use editor::Editor; +use crate::event::{self, Event}; +use crate::keyboard; +use crate::layout; +use crate::mouse::{self, click}; +use crate::text; +use crate::touch; use crate::{ - keyboard, layout, - mouse::{self, click}, - text, Clipboard, Element, Event, Hasher, Layout, Length, Point, Rectangle, + Clipboard, Element, Hasher, Layout, Length, Padding, Point, Rectangle, Size, Widget, }; @@ -56,14 +57,18 @@ pub struct TextInput<'a, Message, Renderer: self::Renderer> { font: Renderer::Font, width: Length, max_width: u32, - padding: u16, + padding: Padding, size: Option<u16>, on_change: Box<dyn Fn(String) -> Message>, on_submit: Option<Message>, style: Renderer::Style, } -impl<'a, Message, Renderer: self::Renderer> TextInput<'a, Message, Renderer> { +impl<'a, Message, Renderer> TextInput<'a, Message, Renderer> +where + Message: Clone, + Renderer: self::Renderer, +{ /// Creates a new [`TextInput`]. /// /// It expects: @@ -71,9 +76,6 @@ impl<'a, Message, Renderer: self::Renderer> TextInput<'a, Message, Renderer> { /// - a placeholder /// - the current value /// - a function that produces a message when the [`TextInput`] changes - /// - /// [`TextInput`]: struct.TextInput.html - /// [`State`]: struct.State.html pub fn new<F>( state: &'a mut State, placeholder: &str, @@ -91,7 +93,7 @@ impl<'a, Message, Renderer: self::Renderer> TextInput<'a, Message, Renderer> { font: Default::default(), width: Length::Fill, max_width: u32::MAX, - padding: 0, + padding: Padding::ZERO, size: None, on_change: Box::new(on_change), on_submit: None, @@ -100,8 +102,6 @@ impl<'a, Message, Renderer: self::Renderer> TextInput<'a, Message, Renderer> { } /// Converts the [`TextInput`] into a secure password input. - /// - /// [`TextInput`]: struct.TextInput.html pub fn password(mut self) -> Self { self.is_secure = true; self @@ -109,39 +109,31 @@ impl<'a, Message, Renderer: self::Renderer> TextInput<'a, Message, Renderer> { /// Sets the [`Font`] of the [`Text`]. /// - /// [`Text`]: struct.Text.html - /// [`Font`]: ../../struct.Font.html + /// [`Font`]: crate::widget::text::Renderer::Font + /// [`Text`]: crate::widget::Text pub fn font(mut self, font: Renderer::Font) -> Self { self.font = font; self } /// Sets the width of the [`TextInput`]. - /// - /// [`TextInput`]: struct.TextInput.html pub fn width(mut self, width: Length) -> Self { self.width = width; self } /// Sets the maximum width of the [`TextInput`]. - /// - /// [`TextInput`]: struct.TextInput.html pub fn max_width(mut self, max_width: u32) -> Self { self.max_width = max_width; self } - /// Sets the padding of the [`TextInput`]. - /// - /// [`TextInput`]: struct.TextInput.html - pub fn padding(mut self, units: u16) -> Self { - self.padding = units; + /// Sets the [`Padding`] of the [`TextInput`]. + pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self { + self.padding = padding.into(); self } /// Sets the text size of the [`TextInput`]. - /// - /// [`TextInput`]: struct.TextInput.html pub fn size(mut self, size: u16) -> Self { self.size = Some(size); self @@ -149,34 +141,75 @@ impl<'a, Message, Renderer: self::Renderer> TextInput<'a, Message, Renderer> { /// Sets the message that should be produced when the [`TextInput`] is /// focused and the enter key is pressed. - /// - /// [`TextInput`]: struct.TextInput.html pub fn on_submit(mut self, message: Message) -> Self { self.on_submit = Some(message); self } /// Sets the style of the [`TextInput`]. - /// - /// [`TextInput`]: struct.TextInput.html pub fn style(mut self, style: impl Into<Renderer::Style>) -> Self { self.style = style.into(); self } /// Returns the current [`State`] of the [`TextInput`]. - /// - /// [`TextInput`]: struct.TextInput.html pub fn state(&self) -> &State { self.state } } +impl<'a, Message, Renderer> TextInput<'a, Message, Renderer> +where + Renderer: self::Renderer, +{ + /// Draws the [`TextInput`] with the given [`Renderer`], overriding its + /// [`Value`] if provided. + pub fn draw( + &self, + renderer: &mut Renderer, + layout: Layout<'_>, + cursor_position: Point, + value: Option<&Value>, + ) -> Renderer::Output { + let value = value.unwrap_or(&self.value); + let bounds = layout.bounds(); + let text_bounds = layout.children().next().unwrap().bounds(); + + if self.is_secure { + self::Renderer::draw( + renderer, + bounds, + text_bounds, + cursor_position, + self.font, + self.size.unwrap_or(renderer.default_size()), + &self.placeholder, + &value.secure(), + &self.state, + &self.style, + ) + } else { + self::Renderer::draw( + renderer, + bounds, + text_bounds, + cursor_position, + self.font, + self.size.unwrap_or(renderer.default_size()), + &self.placeholder, + value, + &self.state, + &self.style, + ) + } + } +} + impl<'a, Message, Renderer> Widget<Message, Renderer> for TextInput<'a, Message, Renderer> where - Renderer: self::Renderer, Message: Clone, + Renderer: self::Renderer, { fn width(&self) -> Length { self.width @@ -191,19 +224,21 @@ where renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { - let padding = self.padding as f32; let text_size = self.size.unwrap_or(renderer.default_size()); let limits = limits - .pad(padding) + .pad(self.padding) .width(self.width) .max_width(self.max_width) .height(Length::Units(text_size)); let mut text = layout::Node::new(limits.resolve(Size::ZERO)); - text.move_to(Point::new(padding, padding)); + text.move_to(Point::new( + self.padding.left.into(), + self.padding.top.into(), + )); - layout::Node::with_children(text.size().pad(padding), vec![text]) + layout::Node::with_children(text.size().pad(self.padding), vec![text]) } fn on_event( @@ -211,14 +246,17 @@ where event: Event, layout: Layout<'_>, cursor_position: Point, - messages: &mut Vec<Message>, renderer: &Renderer, - clipboard: Option<&dyn Clipboard>, - ) { + clipboard: &mut dyn Clipboard, + messages: &mut Vec<Message>, + ) -> event::Status { match event { - Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => { + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerPressed { .. }) => { let is_clicked = layout.bounds().contains(cursor_position); + self.state.is_focused = is_clicked; + if is_clicked { let text_layout = layout.children().next().unwrap(); let target = cursor_position.x - text_layout.bounds().x; @@ -250,6 +288,8 @@ where } else { self.state.cursor.move_to(0); } + + self.state.is_dragging = true; } click::Kind::Double => { if self.is_secure { @@ -269,25 +309,30 @@ where self.value.next_end_of_word(position), ); } + + self.state.is_dragging = false; } click::Kind::Triple => { self.state.cursor.select_all(&self.value); + self.state.is_dragging = false; } } self.state.last_click = Some(click); - } - self.state.is_dragging = is_clicked; - self.state.is_focused = is_clicked; + return event::Status::Captured; + } } - Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) => { + Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerLifted { .. }) + | Event::Touch(touch::Event::FingerLost { .. }) => { self.state.is_dragging = false; } - Event::Mouse(mouse::Event::CursorMoved { x, .. }) => { + Event::Mouse(mouse::Event::CursorMoved { position }) + | Event::Touch(touch::Event::FingerMoved { position, .. }) => { if self.state.is_dragging { let text_layout = layout.children().next().unwrap(); - let target = x - text_layout.bounds().x; + let target = position.x - text_layout.bounds().x; if target > 0.0 { let value = if self.is_secure { @@ -310,11 +355,14 @@ where position, ); } + + return event::Status::Captured; } } Event::Keyboard(keyboard::Event::CharacterReceived(c)) if self.state.is_focused && self.state.is_pasting.is_none() + && !self.state.keyboard_modifiers.command() && !c.is_control() => { let mut editor = @@ -324,120 +372,179 @@ where let message = (self.on_change)(editor.contents()); messages.push(message); + + return event::Status::Captured; } Event::Keyboard(keyboard::Event::KeyPressed { - key_code, - modifiers, - }) if self.state.is_focused => match key_code { - keyboard::KeyCode::Enter => { - if let Some(on_submit) = self.on_submit.clone() { - messages.push(on_submit); - } - } - keyboard::KeyCode::Backspace => { - if platform::is_jump_modifier_pressed(modifiers) - && self.state.cursor.selection(&self.value).is_none() - { - if self.is_secure { - let cursor_pos = self.state.cursor.end(&self.value); - self.state.cursor.select_range(0, cursor_pos); - } else { - self.state.cursor.select_left_by_words(&self.value); + key_code, .. + }) if self.state.is_focused => { + let modifiers = self.state.keyboard_modifiers; + + match key_code { + keyboard::KeyCode::Enter => { + if let Some(on_submit) = self.on_submit.clone() { + messages.push(on_submit); } } + keyboard::KeyCode::Backspace => { + if platform::is_jump_modifier_pressed(modifiers) + && self + .state + .cursor + .selection(&self.value) + .is_none() + { + if self.is_secure { + let cursor_pos = + self.state.cursor.end(&self.value); + self.state.cursor.select_range(0, cursor_pos); + } else { + self.state + .cursor + .select_left_by_words(&self.value); + } + } - let mut editor = - Editor::new(&mut self.value, &mut self.state.cursor); + let mut editor = Editor::new( + &mut self.value, + &mut self.state.cursor, + ); - editor.backspace(); + editor.backspace(); - let message = (self.on_change)(editor.contents()); - messages.push(message); - } - keyboard::KeyCode::Delete => { - if platform::is_jump_modifier_pressed(modifiers) - && self.state.cursor.selection(&self.value).is_none() - { - if self.is_secure { - let cursor_pos = self.state.cursor.end(&self.value); - self.state - .cursor - .select_range(cursor_pos, self.value.len()); - } else { - self.state + let message = (self.on_change)(editor.contents()); + messages.push(message); + } + keyboard::KeyCode::Delete => { + if platform::is_jump_modifier_pressed(modifiers) + && self + .state .cursor - .select_right_by_words(&self.value); + .selection(&self.value) + .is_none() + { + if self.is_secure { + let cursor_pos = + self.state.cursor.end(&self.value); + self.state + .cursor + .select_range(cursor_pos, self.value.len()); + } else { + self.state + .cursor + .select_right_by_words(&self.value); + } } - } - let mut editor = - Editor::new(&mut self.value, &mut self.state.cursor); + let mut editor = Editor::new( + &mut self.value, + &mut self.state.cursor, + ); - editor.delete(); + editor.delete(); - let message = (self.on_change)(editor.contents()); - messages.push(message); - } - keyboard::KeyCode::Left => { - if platform::is_jump_modifier_pressed(modifiers) - && !self.is_secure - { - if modifiers.shift { - self.state.cursor.select_left_by_words(&self.value); + let message = (self.on_change)(editor.contents()); + messages.push(message); + } + keyboard::KeyCode::Left => { + if platform::is_jump_modifier_pressed(modifiers) + && !self.is_secure + { + if modifiers.shift() { + self.state + .cursor + .select_left_by_words(&self.value); + } else { + self.state + .cursor + .move_left_by_words(&self.value); + } + } else if modifiers.shift() { + self.state.cursor.select_left(&self.value) } else { - self.state.cursor.move_left_by_words(&self.value); + self.state.cursor.move_left(&self.value); } - } else if modifiers.shift { - self.state.cursor.select_left(&self.value) - } else { - self.state.cursor.move_left(&self.value); } - } - keyboard::KeyCode::Right => { - if platform::is_jump_modifier_pressed(modifiers) - && !self.is_secure - { - if modifiers.shift { - self.state - .cursor - .select_right_by_words(&self.value); + keyboard::KeyCode::Right => { + if platform::is_jump_modifier_pressed(modifiers) + && !self.is_secure + { + if modifiers.shift() { + self.state + .cursor + .select_right_by_words(&self.value); + } else { + self.state + .cursor + .move_right_by_words(&self.value); + } + } else if modifiers.shift() { + self.state.cursor.select_right(&self.value) } else { - self.state.cursor.move_right_by_words(&self.value); + self.state.cursor.move_right(&self.value); } - } else if modifiers.shift { - self.state.cursor.select_right(&self.value) - } else { - self.state.cursor.move_right(&self.value); } - } - keyboard::KeyCode::Home => { - if modifiers.shift { - self.state.cursor.select_range( - self.state.cursor.start(&self.value), - 0, - ); - } else { - self.state.cursor.move_to(0); + keyboard::KeyCode::Home => { + if modifiers.shift() { + self.state.cursor.select_range( + self.state.cursor.start(&self.value), + 0, + ); + } else { + self.state.cursor.move_to(0); + } } - } - keyboard::KeyCode::End => { - if modifiers.shift { - self.state.cursor.select_range( - self.state.cursor.start(&self.value), - self.value.len(), + keyboard::KeyCode::End => { + if modifiers.shift() { + self.state.cursor.select_range( + self.state.cursor.start(&self.value), + self.value.len(), + ); + } else { + self.state.cursor.move_to(self.value.len()); + } + } + keyboard::KeyCode::C + if self.state.keyboard_modifiers.command() => + { + match self.state.cursor.selection(&self.value) { + Some((start, end)) => { + clipboard.write( + self.value.select(start, end).to_string(), + ); + } + None => {} + } + } + keyboard::KeyCode::X + if self.state.keyboard_modifiers.command() => + { + match self.state.cursor.selection(&self.value) { + Some((start, end)) => { + clipboard.write( + self.value.select(start, end).to_string(), + ); + } + None => {} + } + + let mut editor = Editor::new( + &mut self.value, + &mut self.state.cursor, ); - } else { - self.state.cursor.move_to(self.value.len()); + + editor.delete(); + + let message = (self.on_change)(editor.contents()); + messages.push(message); } - } - keyboard::KeyCode::V => { - if platform::is_copy_paste_modifier_pressed(modifiers) { - if let Some(clipboard) = clipboard { + keyboard::KeyCode::V => { + if self.state.keyboard_modifiers.command() { let content = match self.state.is_pasting.take() { Some(content) => content, None => { let content: String = clipboard - .content() + .read() .unwrap_or(String::new()) .chars() .filter(|c| !c.is_control()) @@ -458,33 +565,49 @@ where messages.push(message); self.state.is_pasting = Some(content); + } else { + self.state.is_pasting = None; } - } else { - self.state.is_pasting = None; } - } - keyboard::KeyCode::A => { - if platform::is_copy_paste_modifier_pressed(modifiers) { + keyboard::KeyCode::A + if self.state.keyboard_modifiers.command() => + { self.state.cursor.select_all(&self.value); } + keyboard::KeyCode::Escape => { + self.state.is_focused = false; + self.state.is_dragging = false; + self.state.is_pasting = None; + + self.state.keyboard_modifiers = + keyboard::Modifiers::default(); + } + _ => {} } - keyboard::KeyCode::Escape => { - self.state.is_focused = false; - self.state.is_dragging = false; - self.state.is_pasting = None; - } - _ => {} - }, + + return event::Status::Captured; + } Event::Keyboard(keyboard::Event::KeyReleased { key_code, .. - }) => match key_code { - keyboard::KeyCode::V => { - self.state.is_pasting = None; + }) if self.state.is_focused => { + match key_code { + keyboard::KeyCode::V => { + self.state.is_pasting = None; + } + _ => {} } - _ => {} - }, + + return event::Status::Captured; + } + Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) + if self.state.is_focused => + { + self.state.keyboard_modifiers = modifiers; + } _ => {} } + + event::Status::Ignored } fn draw( @@ -493,37 +616,9 @@ where _defaults: &Renderer::Defaults, layout: Layout<'_>, cursor_position: Point, + _viewport: &Rectangle, ) -> Renderer::Output { - let bounds = layout.bounds(); - let text_bounds = layout.children().next().unwrap().bounds(); - - if self.is_secure { - self::Renderer::draw( - renderer, - bounds, - text_bounds, - cursor_position, - self.font, - self.size.unwrap_or(renderer.default_size()), - &self.placeholder, - &self.value.secure(), - &self.state, - &self.style, - ) - } else { - self::Renderer::draw( - renderer, - bounds, - text_bounds, - cursor_position, - self.font, - self.size.unwrap_or(renderer.default_size()), - &self.placeholder, - &self.value, - &self.state, - &self.style, - ) - } + self.draw(renderer, layout, cursor_position, None) } fn hash_layout(&self, state: &mut Hasher) { @@ -543,15 +638,12 @@ where /// Your [renderer] will need to implement this trait before being /// able to use a [`TextInput`] in your user interface. /// -/// [`TextInput`]: struct.TextInput.html -/// [renderer]: ../../renderer/index.html +/// [renderer]: crate::renderer pub trait Renderer: text::Renderer + Sized { /// The style supported by this renderer. type Style: Default; /// Returns the width of the value of the [`TextInput`]. - /// - /// [`TextInput`]: struct.TextInput.html fn measure_value(&self, value: &str, size: u16, font: Self::Font) -> f32; /// Returns the current horizontal offset of the value of the @@ -559,9 +651,6 @@ pub trait Renderer: text::Renderer + Sized { /// /// This is the amount of horizontal scrolling applied when the [`Value`] /// does not fit the [`TextInput`]. - /// - /// [`TextInput`]: struct.TextInput.html - /// [`Value`]: struct.Value.html fn offset( &self, text_bounds: Rectangle, @@ -580,10 +669,6 @@ pub trait Renderer: text::Renderer + Sized { /// - the placeholder to show when the value is empty /// - the current [`Value`] /// - the current [`State`] - /// - /// [`TextInput`]: struct.TextInput.html - /// [`Value`]: struct.Value.html - /// [`State`]: struct.State.html fn draw( &mut self, bounds: Rectangle, @@ -599,8 +684,6 @@ pub trait Renderer: text::Renderer + Sized { /// Computes the position of the text cursor at the given X coordinate of /// a [`TextInput`]. - /// - /// [`TextInput`]: struct.TextInput.html fn find_cursor_position( &self, text_bounds: Rectangle, @@ -629,8 +712,8 @@ pub trait Renderer: text::Renderer + Sized { impl<'a, Message, Renderer> From<TextInput<'a, Message, Renderer>> for Element<'a, Message, Renderer> where - Renderer: 'a + self::Renderer, Message: 'a + Clone, + Renderer: 'a + self::Renderer, { fn from( text_input: TextInput<'a, Message, Renderer>, @@ -640,8 +723,6 @@ where } /// The state of a [`TextInput`]. -/// -/// [`TextInput`]: struct.TextInput.html #[derive(Debug, Default, Clone)] pub struct State { is_focused: bool, @@ -649,20 +730,17 @@ pub struct State { is_pasting: Option<Value>, last_click: Option<mouse::Click>, cursor: Cursor, + keyboard_modifiers: keyboard::Modifiers, // TODO: Add stateful horizontal scrolling offset } impl State { /// Creates a new [`State`], representing an unfocused [`TextInput`]. - /// - /// [`State`]: struct.State.html pub fn new() -> Self { Self::default() } /// Creates a new [`State`], representing a focused [`TextInput`]. - /// - /// [`State`]: struct.State.html pub fn focused() -> Self { Self { is_focused: true, @@ -670,47 +748,49 @@ impl State { is_pasting: None, last_click: None, cursor: Cursor::default(), + keyboard_modifiers: keyboard::Modifiers::default(), } } /// Returns whether the [`TextInput`] is currently focused or not. - /// - /// [`TextInput`]: struct.TextInput.html pub fn is_focused(&self) -> bool { self.is_focused } /// Returns the [`Cursor`] of the [`TextInput`]. - /// - /// [`Cursor`]: struct.Cursor.html - /// [`TextInput`]: struct.TextInput.html pub fn cursor(&self) -> Cursor { self.cursor } + /// Focuses the [`TextInput`]. + pub fn focus(&mut self) { + self.is_focused = true; + } + + /// Unfocuses the [`TextInput`]. + pub fn unfocus(&mut self) { + self.is_focused = false; + } + /// Moves the [`Cursor`] of the [`TextInput`] to the front of the input text. - /// - /// [`Cursor`]: struct.Cursor.html - /// [`TextInput`]: struct.TextInput.html pub fn move_cursor_to_front(&mut self) { self.cursor.move_to(0); } /// Moves the [`Cursor`] of the [`TextInput`] to the end of the input text. - /// - /// [`Cursor`]: struct.Cursor.html - /// [`TextInput`]: struct.TextInput.html pub fn move_cursor_to_end(&mut self) { self.cursor.move_to(usize::MAX); } /// Moves the [`Cursor`] of the [`TextInput`] to an arbitrary location. - /// - /// [`Cursor`]: struct.Cursor.html - /// [`TextInput`]: struct.TextInput.html pub fn move_cursor_to(&mut self, position: usize) { self.cursor.move_to(position); } + + /// Selects all the content of the [`TextInput`]. + pub fn select_all(&mut self) { + self.cursor.select_range(0, usize::MAX); + } } // TODO: Reduce allocations @@ -772,23 +852,11 @@ fn find_cursor_position<Renderer: self::Renderer>( mod platform { use crate::keyboard; - pub fn is_jump_modifier_pressed( - modifiers: keyboard::ModifiersState, - ) -> bool { - if cfg!(target_os = "macos") { - modifiers.alt - } else { - modifiers.control - } - } - - pub fn is_copy_paste_modifier_pressed( - modifiers: keyboard::ModifiersState, - ) -> bool { + pub fn is_jump_modifier_pressed(modifiers: keyboard::Modifiers) -> bool { if cfg!(target_os = "macos") { - modifiers.logo + modifiers.alt() } else { - modifiers.control + modifiers.control() } } } diff --git a/native/src/widget/text_input/cursor.rs b/native/src/widget/text_input/cursor.rs index aa03bb74..4f3b159b 100644 --- a/native/src/widget/text_input/cursor.rs +++ b/native/src/widget/text_input/cursor.rs @@ -8,8 +8,6 @@ pub struct Cursor { } /// The state of a [`Cursor`]. -/// -/// [`Cursor`]: struct.Cursor.html #[derive(Debug, Copy, Clone)] pub enum State { /// Cursor without a selection @@ -34,9 +32,6 @@ impl Default for Cursor { impl Cursor { /// Returns the [`State`] of the [`Cursor`]. - /// - /// [`State`]: struct.State.html - /// [`Cursor`]: struct.Cursor.html pub fn state(&self, value: &Value) -> State { match self.state { State::Index(index) => State::Index(index.min(value.len())), @@ -53,6 +48,18 @@ impl Cursor { } } + /// Returns the current selection of the [`Cursor`] for the given [`Value`]. + /// + /// `start` is guaranteed to be <= than `end`. + pub fn selection(&self, value: &Value) -> Option<(usize, usize)> { + match self.state(value) { + State::Selection { start, end } => { + Some((start.min(end), start.max(end))) + } + _ => None, + } + } + pub(crate) fn move_to(&mut self, position: usize) { self.state = State::Index(position); } @@ -106,7 +113,7 @@ impl Cursor { State::Selection { start, end } if end > 0 => { self.select_range(start, end - 1) } - _ => (), + _ => {} } } @@ -118,7 +125,7 @@ impl Cursor { State::Selection { start, end } if end < value.len() => { self.select_range(start, end + 1) } - _ => (), + _ => {} } } @@ -166,15 +173,6 @@ impl Cursor { end.min(value.len()) } - pub(crate) fn selection(&self, value: &Value) -> Option<(usize, usize)> { - match self.state(value) { - State::Selection { start, end } => { - Some((start.min(end), start.max(end))) - } - _ => None, - } - } - fn left(&self, value: &Value) -> usize { match self.state(value) { State::Index(index) => index, diff --git a/native/src/widget/text_input/editor.rs b/native/src/widget/text_input/editor.rs index 20e42567..0b50a382 100644 --- a/native/src/widget/text_input/editor.rs +++ b/native/src/widget/text_input/editor.rs @@ -20,7 +20,7 @@ impl<'a> Editor<'a> { self.cursor.move_left(self.value); self.value.remove_many(left, right); } - _ => (), + _ => {} } self.value.insert(self.cursor.end(self.value), character); @@ -35,7 +35,7 @@ impl<'a> Editor<'a> { self.cursor.move_left(self.value); self.value.remove_many(left, right); } - _ => (), + _ => {} } self.value.insert_many(self.cursor.end(self.value), content); diff --git a/native/src/widget/text_input/value.rs b/native/src/widget/text_input/value.rs index 1e9ba45b..2034cca4 100644 --- a/native/src/widget/text_input/value.rs +++ b/native/src/widget/text_input/value.rs @@ -2,7 +2,7 @@ use unicode_segmentation::UnicodeSegmentation; /// The value of a [`TextInput`]. /// -/// [`TextInput`]: struct.TextInput.html +/// [`TextInput`]: crate::widget::TextInput // TODO: Reduce allocations, cache results (?) #[derive(Debug, Clone)] pub struct Value { @@ -11,8 +11,6 @@ pub struct Value { impl Value { /// Creates a new [`Value`] from a string slice. - /// - /// [`Value`]: struct.Value.html pub fn new(string: &str) -> Self { let graphemes = UnicodeSegmentation::graphemes(string, true) .map(String::from) @@ -21,17 +19,20 @@ impl Value { Self { graphemes } } - /// Returns the total amount of graphemes in the [`Value`]. + /// Returns whether the [`Value`] is empty or not. /// - /// [`Value`]: struct.Value.html + /// A [`Value`] is empty when it contains no graphemes. + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + + /// Returns the total amount of graphemes in the [`Value`]. pub fn len(&self) -> usize { self.graphemes.len() } /// Returns the position of the previous start of a word from the given /// grapheme `index`. - /// - /// [`Value`]: struct.Value.html pub fn previous_start_of_word(&self, index: usize) -> usize { let previous_string = &self.graphemes[..index.min(self.graphemes.len())].concat(); @@ -54,8 +55,6 @@ impl Value { /// Returns the position of the next end of a word from the given grapheme /// `index`. - /// - /// [`Value`]: struct.Value.html pub fn next_end_of_word(&self, index: usize) -> usize { let next_string = &self.graphemes[index..].concat(); @@ -74,10 +73,17 @@ impl Value { .unwrap_or(self.len()) } + /// Returns a new [`Value`] containing the graphemes from `start` until the + /// given `end`. + pub fn select(&self, start: usize, end: usize) -> Self { + let graphemes = + self.graphemes[start.min(self.len())..end.min(self.len())].to_vec(); + + Self { graphemes } + } + /// Returns a new [`Value`] containing the graphemes until the given /// `index`. - /// - /// [`Value`]: struct.Value.html pub fn until(&self, index: usize) -> Self { let graphemes = self.graphemes[..index.min(self.len())].to_vec(); @@ -85,8 +91,6 @@ impl Value { } /// Converts the [`Value`] into a `String`. - /// - /// [`Value`]: struct.Value.html pub fn to_string(&self) -> String { self.graphemes.concat() } @@ -109,8 +113,6 @@ impl Value { } /// Removes the grapheme at the given `index`. - /// - /// [`Value`]: struct.Value.html pub fn remove(&mut self, index: usize) { let _ = self.graphemes.remove(index); } @@ -122,8 +124,6 @@ impl Value { /// Returns a new [`Value`] with all its graphemes replaced with the /// dot ('•') character. - /// - /// [`Value`]: struct.Value.html pub fn secure(&self) -> Self { Self { graphemes: std::iter::repeat(String::from("•")) diff --git a/native/src/widget/toggler.rs b/native/src/widget/toggler.rs new file mode 100644 index 00000000..4035276c --- /dev/null +++ b/native/src/widget/toggler.rs @@ -0,0 +1,277 @@ +//! Show toggle controls using togglers. +use std::hash::Hash; + +use crate::{ + event, layout, mouse, row, text, Align, Clipboard, Element, Event, Hasher, + HorizontalAlignment, Layout, Length, Point, Rectangle, Row, Text, + VerticalAlignment, Widget, +}; + +/// A toggler widget +/// +/// # Example +/// +/// ``` +/// # type Toggler<Message> = iced_native::Toggler<Message, iced_native::renderer::Null>; +/// # +/// pub enum Message { +/// TogglerToggled(bool), +/// } +/// +/// let is_active = true; +/// +/// Toggler::new(is_active, String::from("Toggle me!"), |b| Message::TogglerToggled(b)); +/// ``` +#[allow(missing_debug_implementations)] +pub struct Toggler<Message, Renderer: self::Renderer + text::Renderer> { + is_active: bool, + on_toggle: Box<dyn Fn(bool) -> Message>, + label: Option<String>, + width: Length, + size: u16, + text_size: Option<u16>, + text_alignment: HorizontalAlignment, + spacing: u16, + font: Renderer::Font, + style: Renderer::Style, +} + +impl<Message, Renderer: self::Renderer + text::Renderer> + Toggler<Message, Renderer> +{ + /// Creates a new [`Toggler`]. + /// + /// It expects: + /// * a boolean describing whether the [`Toggler`] is checked or not + /// * An optional label for the [`Toggler`] + /// * 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>( + is_active: bool, + label: impl Into<Option<String>>, + f: F, + ) -> Self + where + F: 'static + Fn(bool) -> Message, + { + Toggler { + is_active, + on_toggle: Box::new(f), + label: label.into(), + width: Length::Fill, + size: <Renderer as self::Renderer>::DEFAULT_SIZE, + text_size: None, + text_alignment: HorizontalAlignment::Left, + spacing: 0, + font: Renderer::Font::default(), + style: Renderer::Style::default(), + } + } + + /// Sets the size of the [`Toggler`]. + pub fn size(mut self, size: u16) -> Self { + self.size = size; + self + } + + /// Sets the width of the [`Toggler`]. + pub fn width(mut self, width: Length) -> Self { + self.width = width; + self + } + + /// Sets the text size o the [`Toggler`]. + pub fn text_size(mut self, text_size: u16) -> Self { + self.text_size = Some(text_size); + self + } + + /// Sets the horizontal alignment of the text of the [`Toggler`] + pub fn text_alignment(mut self, alignment: HorizontalAlignment) -> Self { + self.text_alignment = alignment; + self + } + + /// Sets the spacing between the [`Toggler`] and the text. + pub fn spacing(mut self, spacing: u16) -> Self { + self.spacing = spacing; + self + } + + /// Sets the [`Font`] of the text of the [`Toggler`] + pub fn font(mut self, font: Renderer::Font) -> Self { + self.font = font; + self + } + + /// Sets the style of the [`Toggler`]. + pub fn style(mut self, style: impl Into<Renderer::Style>) -> Self { + self.style = style.into(); + self + } +} + +impl<Message, Renderer> Widget<Message, Renderer> for Toggler<Message, Renderer> +where + Renderer: self::Renderer + text::Renderer + row::Renderer, +{ + fn width(&self) -> Length { + self.width + } + + fn height(&self) -> Length { + Length::Shrink + } + + fn layout( + &self, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + let mut row = Row::<(), Renderer>::new() + .width(self.width) + .spacing(self.spacing) + .align_items(Align::Center); + + if let Some(label) = &self.label { + row = row.push( + Text::new(label) + .horizontal_alignment(self.text_alignment) + .font(self.font) + .width(self.width) + .size(self.text_size.unwrap_or(renderer.default_size())), + ); + } + + row = row.push( + Row::new() + .width(Length::Units(2 * self.size)) + .height(Length::Units(self.size)), + ); + + row.layout(renderer, limits) + } + + fn on_event( + &mut self, + event: Event, + layout: Layout<'_>, + cursor_position: Point, + _renderer: &Renderer, + _clipboard: &mut dyn Clipboard, + messages: &mut Vec<Message>, + ) -> event::Status { + match event { + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => { + let mouse_over = layout.bounds().contains(cursor_position); + + if mouse_over { + messages.push((self.on_toggle)(!self.is_active)); + + event::Status::Captured + } else { + event::Status::Ignored + } + } + _ => event::Status::Ignored, + } + } + + fn draw( + &self, + renderer: &mut Renderer, + defaults: &Renderer::Defaults, + layout: Layout<'_>, + cursor_position: Point, + _viewport: &Rectangle, + ) -> Renderer::Output { + let bounds = layout.bounds(); + let mut children = layout.children(); + + let label = match &self.label { + Some(label) => { + let label_layout = children.next().unwrap(); + + Some(text::Renderer::draw( + renderer, + defaults, + label_layout.bounds(), + &label, + self.text_size.unwrap_or(renderer.default_size()), + self.font, + None, + self.text_alignment, + VerticalAlignment::Center, + )) + } + + None => None, + }; + + let toggler_layout = children.next().unwrap(); + let toggler_bounds = toggler_layout.bounds(); + + let is_mouse_over = bounds.contains(cursor_position); + + self::Renderer::draw( + renderer, + toggler_bounds, + self.is_active, + is_mouse_over, + label, + &self.style, + ) + } + + fn hash_layout(&self, state: &mut Hasher) { + struct Marker; + std::any::TypeId::of::<Marker>().hash(state); + + self.label.hash(state) + } +} + +/// The renderer of a [`Toggler`]. +/// +/// Your [renderer] will need to implement this trait before being +/// able to use a [`Toggler`] in your user interface. +/// +/// [renderer]: ../../renderer/index.html +pub trait Renderer: crate::Renderer { + /// The style supported by this renderer. + type Style: Default; + + /// The default size of a [`Toggler`]. + const DEFAULT_SIZE: u16; + + /// Draws a [`Toggler`]. + /// + /// It receives: + /// * the bounds of the [`Toggler`] + /// * whether the [`Toggler`] is activated or not + /// * whether the mouse is over the [`Toggler`] or not + /// * the drawn label of the [`Toggler`] + /// * the style of the [`Toggler`] + fn draw( + &mut self, + bounds: Rectangle, + is_active: bool, + is_mouse_over: bool, + label: Option<Self::Output>, + style: &Self::Style, + ) -> Self::Output; +} + +impl<'a, Message, Renderer> From<Toggler<Message, Renderer>> + for Element<'a, Message, Renderer> +where + Renderer: 'a + self::Renderer + text::Renderer + row::Renderer, + Message: 'a, +{ + fn from( + toggler: Toggler<Message, Renderer>, + ) -> Element<'a, Message, Renderer> { + Element::new(toggler) + } +} diff --git a/native/src/widget/tooltip.rs b/native/src/widget/tooltip.rs new file mode 100644 index 00000000..276afd41 --- /dev/null +++ b/native/src/widget/tooltip.rs @@ -0,0 +1,210 @@ +//! Display a widget over another. +use std::hash::Hash; + +use iced_core::Rectangle; + +use crate::widget::container; +use crate::widget::text::{self, Text}; +use crate::{ + event, layout, Clipboard, Element, Event, Hasher, Layout, Length, Point, + Widget, +}; + +/// An element to display a widget over another. +#[allow(missing_debug_implementations)] +pub struct Tooltip<'a, Message, Renderer: self::Renderer> { + content: Element<'a, Message, Renderer>, + tooltip: Text<Renderer>, + position: Position, + style: <Renderer as container::Renderer>::Style, + gap: u16, + padding: u16, +} + +impl<'a, Message, Renderer> Tooltip<'a, Message, Renderer> +where + Renderer: self::Renderer, +{ + /// Creates an empty [`Tooltip`]. + /// + /// [`Tooltip`]: struct.Tooltip.html + pub fn new( + content: impl Into<Element<'a, Message, Renderer>>, + tooltip: impl ToString, + position: Position, + ) -> Self { + Tooltip { + content: content.into(), + tooltip: Text::new(tooltip.to_string()), + position, + style: Default::default(), + gap: 0, + padding: Renderer::DEFAULT_PADDING, + } + } + + /// Sets the size of the text of the [`Tooltip`]. + pub fn size(mut self, size: u16) -> Self { + self.tooltip = self.tooltip.size(size); + self + } + + /// Sets the font of the [`Tooltip`]. + /// + /// [`Font`]: Renderer::Font + pub fn font(mut self, font: impl Into<Renderer::Font>) -> Self { + self.tooltip = self.tooltip.font(font); + self + } + + /// Sets the gap between the content and its [`Tooltip`]. + pub fn gap(mut self, gap: u16) -> Self { + self.gap = gap; + self + } + + /// Sets the padding of the [`Tooltip`]. + pub fn padding(mut self, padding: u16) -> Self { + self.padding = padding; + self + } + + /// Sets the style of the [`Tooltip`]. + pub fn style( + mut self, + style: impl Into<<Renderer as container::Renderer>::Style>, + ) -> Self { + self.style = style.into(); + self + } +} + +/// The position of the tooltip. Defaults to following the cursor. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Position { + /// The tooltip will follow the cursor. + FollowCursor, + /// The tooltip will appear on the top of the widget. + Top, + /// The tooltip will appear on the bottom of the widget. + Bottom, + /// The tooltip will appear on the left of the widget. + Left, + /// The tooltip will appear on the right of the widget. + Right, +} + +impl<'a, Message, Renderer> Widget<Message, Renderer> + for Tooltip<'a, Message, Renderer> +where + Renderer: self::Renderer, +{ + fn width(&self) -> Length { + self.content.width() + } + + fn height(&self) -> Length { + self.content.height() + } + + fn layout( + &self, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + self.content.layout(renderer, limits) + } + + fn on_event( + &mut self, + event: Event, + layout: Layout<'_>, + cursor_position: Point, + renderer: &Renderer, + clipboard: &mut dyn Clipboard, + messages: &mut Vec<Message>, + ) -> event::Status { + self.content.widget.on_event( + event, + layout, + cursor_position, + renderer, + clipboard, + messages, + ) + } + + fn draw( + &self, + renderer: &mut Renderer, + defaults: &Renderer::Defaults, + layout: Layout<'_>, + cursor_position: Point, + viewport: &Rectangle, + ) -> Renderer::Output { + self::Renderer::draw( + renderer, + defaults, + cursor_position, + layout, + viewport, + &self.content, + &self.tooltip, + self.position, + &self.style, + self.gap, + self.padding, + ) + } + + fn hash_layout(&self, state: &mut Hasher) { + struct Marker; + std::any::TypeId::of::<Marker>().hash(state); + + self.content.hash_layout(state); + } +} + +/// The renderer of a [`Tooltip`]. +/// +/// Your [renderer] will need to implement this trait before being +/// able to use a [`Tooltip`] in your user interface. +/// +/// [`Tooltip`]: struct.Tooltip.html +/// [renderer]: ../../renderer/index.html +pub trait Renderer: + crate::Renderer + text::Renderer + container::Renderer +{ + /// The default padding of a [`Tooltip`] drawn by this renderer. + const DEFAULT_PADDING: u16; + + /// Draws a [`Tooltip`]. + /// + /// [`Tooltip`]: struct.Tooltip.html + fn draw<Message>( + &mut self, + defaults: &Self::Defaults, + cursor_position: Point, + content_layout: Layout<'_>, + viewport: &Rectangle, + content: &Element<'_, Message, Self>, + tooltip: &Text<Self>, + position: Position, + style: &<Self as container::Renderer>::Style, + gap: u16, + padding: u16, + ) -> Self::Output; +} + +impl<'a, Message, Renderer> From<Tooltip<'a, Message, Renderer>> + for Element<'a, Message, Renderer> +where + Renderer: 'a + self::Renderer, + Message: 'a, +{ + fn from( + column: Tooltip<'a, Message, Renderer>, + ) -> Element<'a, Message, Renderer> { + Element::new(column) + } +} diff --git a/native/src/window/event.rs b/native/src/window/event.rs index b177141a..64f2b8d8 100644 --- a/native/src/window/event.rs +++ b/native/src/window/event.rs @@ -3,7 +3,15 @@ use std::path::PathBuf; /// A window-related event. #[derive(PartialEq, Clone, Debug)] pub enum Event { - /// A window was resized + /// A window was moved. + Moved { + /// The new logical x location of the window + x: i32, + /// The new logical y location of the window + y: i32, + }, + + /// A window was resized. Resized { /// The new width of the window (in units) width: u32, @@ -12,6 +20,18 @@ pub enum Event { height: u32, }, + /// The user has requested for the window to close. + /// + /// Usually, you will want to terminate the execution whenever this event + /// occurs. + CloseRequested, + + /// A window was focused. + Focused, + + /// A window was unfocused. + Unfocused, + /// A file is being hovered over the window. /// /// When the user hovers multiple files at once, this event will be emitted |