diff options
Diffstat (limited to 'native')
43 files changed, 1948 insertions, 511 deletions
diff --git a/native/Cargo.toml b/native/Cargo.toml index 2c99638a..a3134ef4 100644 --- a/native/Cargo.toml +++ b/native/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "iced_native" -version = "0.3.0" +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.3" +version = "0.4" path = "../core" [dependencies.iced_futures] -version = "0.2" +version = "0.3" path = "../futures" features = ["thread-pool"] diff --git a/native/README.md b/native/README.md index 6323dd4f..0d79690a 100644 --- a/native/README.md +++ b/native/README.md @@ -28,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.3" +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 ecdccabf..081b4004 100644 --- a/native/src/clipboard.rs +++ b/native/src/clipboard.rs @@ -1,6 +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. - 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/element.rs b/native/src/element.rs index d6e9639a..5c84a388 100644 --- a/native/src/element.rs +++ b/native/src/element.rs @@ -223,17 +223,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.widget.on_event( event, layout, cursor_position, - messages, renderer, clipboard, + messages, ) } @@ -311,9 +311,9 @@ 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(); @@ -321,9 +321,9 @@ where event, layout, cursor_position, - &mut original_messages, renderer, clipboard, + &mut original_messages, ); original_messages @@ -401,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, ) } diff --git a/native/src/event.rs b/native/src/event.rs index 0e86171e..1c26b5f2 100644 --- a/native/src/event.rs +++ b/native/src/event.rs @@ -1,5 +1,8 @@ //! Handle events of a user interface. -use crate::{keyboard, mouse, window}; +use crate::keyboard; +use crate::mouse; +use crate::touch; +use crate::window; /// A user interface event. /// @@ -17,6 +20,30 @@ 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. diff --git a/native/src/layout.rs b/native/src/layout.rs index 6d144902..b4b4a021 100644 --- a/native/src/layout.rs +++ b/native/src/layout.rs @@ -19,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 { diff --git a/native/src/layout/flex.rs b/native/src/layout/flex.rs index 4f6523fb..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. @@ -62,7 +63,7 @@ pub fn resolve<Message, Renderer>( axis: Axis, renderer: &Renderer, limits: &Limits, - padding: f32, + padding: Padding, spacing: f32, align_items: Align, items: &[Element<'_, Message, Renderer>], @@ -141,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)); @@ -166,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 a7bb5c9c..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)] @@ -117,8 +117,11 @@ impl Limits { } /// Shrinks the current [`Limits`] to account for the given padding. - 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`]. diff --git a/native/src/lib.rs b/native/src/lib.rs index f9a99c48..cbb02506 100644 --- a/native/src/lib.rs +++ b/native/src/lib.rs @@ -33,6 +33,7 @@ #![deny(unused_results)] #![forbid(unsafe_code)] #![forbid(rust_2018_idioms)] +pub mod clipboard; pub mod event; pub mod keyboard; pub mod layout; @@ -41,10 +42,10 @@ 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 hasher; mod runtime; @@ -60,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/overlay.rs b/native/src/overlay.rs index ea8bb384..84145e7f 100644 --- a/native/src/overlay.rs +++ b/native/src/overlay.rs @@ -67,9 +67,9 @@ 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 { event::Status::Ignored } diff --git a/native/src/overlay/element.rs b/native/src/overlay/element.rs index 0f44a781..e4819037 100644 --- a/native/src/overlay/element.rs +++ b/native/src/overlay/element.rs @@ -53,17 +53,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.overlay.on_event( event, layout, cursor_position, - messages, renderer, clipboard, + messages, ) } @@ -117,9 +117,9 @@ 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(); @@ -127,9 +127,9 @@ where event, layout, cursor_position, - &mut original_messages, renderer, clipboard, + &mut original_messages, ); original_messages diff --git a/native/src/overlay/menu.rs b/native/src/overlay/menu.rs index abac849f..f62dcb46 100644 --- a/native/src/overlay/menu.rs +++ b/native/src/overlay/menu.rs @@ -6,9 +6,10 @@ use crate::mouse; use crate::overlay; use crate::scrollable; use crate::text; +use crate::touch; use crate::{ - Clipboard, Container, Element, 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. @@ -19,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, @@ -44,7 +45,7 @@ where hovered_option, last_selection, width: 0, - padding: 0, + padding: Padding::ZERO, text_size: None, font: Default::default(), style: Default::default(), @@ -57,9 +58,9 @@ where self } - /// Sets the padding of the [`Menu`]. - 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 } @@ -218,17 +219,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.container.on_event( event.clone(), layout, cursor_position, - messages, renderer, clipboard, + messages, ) } @@ -260,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, @@ -293,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, ); @@ -319,9 +320,9 @@ 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)) => { @@ -337,17 +338,38 @@ 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()); + } + } + } + } _ => {} } @@ -408,7 +430,7 @@ pub trait Renderer: 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 9ee72703..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; @@ -11,7 +11,10 @@ pub trait Program: Sized { type Renderer: Renderer; /// The type of __messages__ your [`Program`] will produce. - 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`]. /// @@ -21,7 +24,11 @@ pub trait Program: Sized { /// /// Any [`Command`] returned will be executed immediately in the /// background by shells. - 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`]. /// diff --git a/native/src/program/state.rs b/native/src/program/state.rs index e630890a..fd1f2b52 100644 --- a/native/src/program/state.rs +++ b/native/src/program/state.rs @@ -1,6 +1,5 @@ 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 @@ -91,8 +90,8 @@ where &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( @@ -109,8 +108,8 @@ where let _ = user_interface.update( &self.queued_events, cursor_position, - clipboard, renderer, + clipboard, &mut messages, ); @@ -136,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/null.rs b/native/src/renderer/null.rs index 91ee9a28..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, }; @@ -145,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 = (); @@ -246,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, ) { } @@ -261,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, ) { } @@ -275,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/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 7a64ac59..8e0d7d1c 100644 --- a/native/src/user_interface.rs +++ b/native/src/user_interface.rs @@ -16,7 +16,7 @@ use std::hash::Hasher; /// The [`integration` example] uses a [`UserInterface`] to integrate Iced in /// an existing graphical application. /// -/// [`integration` example]: https://github.com/hecrj/iced/tree/0.2/examples/integration +/// [`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>, @@ -134,7 +134,7 @@ where /// 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 { @@ -157,6 +157,7 @@ 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(); @@ -176,8 +177,8 @@ where /// let event_statuses = user_interface.update( /// &events, /// cursor_position, - /// None, /// &renderer, + /// &mut clipboard, /// &mut messages /// ); /// @@ -193,8 +194,8 @@ where &mut self, events: &[Event], cursor_position: Point, - clipboard: Option<&dyn Clipboard>, renderer: &Renderer, + clipboard: &mut dyn Clipboard, messages: &mut Vec<Message>, ) -> Vec<event::Status> { let (base_cursor, overlay_statuses) = if let Some(mut overlay) = @@ -215,9 +216,9 @@ where event, Layout::new(&layer.layout), cursor_position, - messages, renderer, clipboard, + messages, ) }) .collect(); @@ -246,9 +247,9 @@ where event, Layout::new(&self.base.layout), base_cursor, - messages, renderer, clipboard, + messages, ); event_status.merge(overlay_status) @@ -269,7 +270,7 @@ where /// [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 { @@ -292,6 +293,7 @@ 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(); /// @@ -309,8 +311,8 @@ where /// let event_statuses = user_interface.update( /// &events, /// cursor_position, - /// None, /// &renderer, + /// &mut clipboard, /// &mut messages /// ); /// diff --git a/native/src/widget.rs b/native/src/widget.rs index 3677713a..43c1b023 100644 --- a/native/src/widget.rs +++ b/native/src/widget.rs @@ -36,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; @@ -71,6 +73,10 @@ 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::event::{self, Event}; use crate::layout; @@ -93,12 +99,12 @@ use crate::{Clipboard, Hasher, Layout, Length, Point, Rectangle}; /// - [`geometry`], a custom widget showcasing how to draw geometry with the /// `Mesh2D` primitive in [`iced_wgpu`]. /// -/// [examples]: https://github.com/hecrj/iced/tree/0.2/examples -/// [`bezier_tool`]: https://github.com/hecrj/iced/tree/0.2/examples/bezier_tool -/// [`custom_widget`]: https://github.com/hecrj/iced/tree/0.2/examples/custom_widget -/// [`geometry`]: https://github.com/hecrj/iced/tree/0.2/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.2/wgpu +/// [`iced_wgpu`]: https://github.com/hecrj/iced/tree/0.3/wgpu pub trait Widget<Message, Renderer> where Renderer: crate::Renderer, @@ -161,9 +167,9 @@ 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 { event::Status::Ignored } diff --git a/native/src/widget/button.rs b/native/src/widget/button.rs index dca20e13..c469a0e5 100644 --- a/native/src/widget/button.rs +++ b/native/src/widget/button.rs @@ -4,8 +4,11 @@ use crate::event::{self, Event}; use crate::layout; use crate::mouse; +use crate::overlay; +use crate::touch; use crate::{ - Clipboard, Element, Hasher, Layout, Length, Point, Rectangle, Widget, + Clipboard, Element, Hasher, Layout, Length, Padding, Point, Rectangle, + Widget, }; use std::hash::Hash; @@ -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,7 +61,7 @@ pub struct Button<'a, Message, Renderer: self::Renderer> { height: Length, min_width: u32, min_height: u32, - padding: u16, + padding: Padding, style: Renderer::Style, } @@ -87,13 +113,14 @@ where self } - /// Sets the padding of the [`Button`]. - 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. + /// 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 @@ -138,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]) } @@ -159,12 +188,24 @@ 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(); @@ -175,7 +216,8 @@ where } } } - 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(); @@ -190,6 +232,9 @@ where } } } + Event::Touch(touch::Event::FingerLost { .. }) => { + self.state.is_pressed = false; + } _ => {} } @@ -223,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`]. @@ -233,7 +285,7 @@ where /// [renderer]: crate::renderer pub trait Renderer: crate::Renderer + Sized { /// The default padding of a [`Button`]. - const DEFAULT_PADDING: u16; + const DEFAULT_PADDING: Padding; /// The style supported by this renderer. type Style: Default; diff --git a/native/src/widget/checkbox.rs b/native/src/widget/checkbox.rs index 81420458..0f21c873 100644 --- a/native/src/widget/checkbox.rs +++ b/native/src/widget/checkbox.rs @@ -6,9 +6,10 @@ use crate::layout; use crate::mouse; use crate::row; use crate::text; +use crate::touch; use crate::{ - Align, Clipboard, Element, 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. @@ -38,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, } @@ -65,6 +67,7 @@ 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(), } } @@ -101,6 +104,12 @@ impl<Message, Renderer: self::Renderer + text::Renderer> 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`]. pub fn style(mut self, style: impl Into<Renderer::Style>) -> Self { self.style = style.into(); @@ -149,12 +158,13 @@ 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 { @@ -191,7 +201,7 @@ where &self.label, self.text_size.unwrap_or(renderer.default_size()), self.font, - None, + self.text_color, HorizontalAlignment::Left, VerticalAlignment::Center, ); diff --git a/native/src/widget/column.rs b/native/src/widget/column.rs index e0e88d31..52a2e80c 100644 --- a/native/src/widget/column.rs +++ b/native/src/widget/column.rs @@ -5,7 +5,8 @@ use crate::event::{self, Event}; use crate::layout; use crate::overlay; use crate::{ - Align, Clipboard, Element, Hasher, Layout, Length, Point, Rectangle, Widget, + Align, Clipboard, Element, Hasher, Layout, Length, Padding, Point, + Rectangle, Widget, }; use std::u32; @@ -14,7 +15,7 @@ use std::u32; #[allow(missing_debug_implementations)] pub struct Column<'a, Message, Renderer> { spacing: u16, - padding: u16, + padding: Padding, width: Length, height: Length, max_width: u32, @@ -35,7 +36,7 @@ impl<'a, Message, Renderer> Column<'a, Message, Renderer> { ) -> Self { Column { spacing: 0, - padding: 0, + padding: Padding::ZERO, width: Length::Shrink, height: Length::Shrink, max_width: u32::MAX, @@ -55,9 +56,9 @@ impl<'a, Message, Renderer> Column<'a, Message, Renderer> { self } - /// Sets the padding of the [`Column`]. - 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 } @@ -129,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, @@ -141,9 +142,9 @@ 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.children .iter_mut() @@ -153,9 +154,9 @@ where event.clone(), layout, cursor_position, - messages, renderer, clipboard, + messages, ) }) .fold(event::Status::Ignored, event::Status::merge) diff --git a/native/src/widget/container.rs b/native/src/widget/container.rs index 65764148..69aee64d 100644 --- a/native/src/widget/container.rs +++ b/native/src/widget/container.rs @@ -5,7 +5,8 @@ use crate::event::{self, Event}; use crate::layout; use crate::overlay; use crate::{ - Align, Clipboard, Element, Hasher, Layout, Length, Point, Rectangle, Widget, + Align, Clipboard, Element, Hasher, Layout, Length, Padding, Point, + Rectangle, Widget, }; use std::u32; @@ -15,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, @@ -36,7 +37,7 @@ where T: Into<Element<'a, Message, Renderer>>, { Container { - padding: 0, + padding: Padding::ZERO, width: Length::Shrink, height: Length::Shrink, max_width: u32::MAX, @@ -48,9 +49,9 @@ where } } - /// Sets the padding of the [`Container`]. - 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 } @@ -127,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( @@ -151,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, ) } diff --git a/native/src/widget/image.rs b/native/src/widget/image.rs index 51d7ba26..4d8e0a3f 100644 --- a/native/src/widget/image.rs +++ b/native/src/widget/image.rs @@ -1,4 +1,7 @@ //! Display images in your user interface. +pub mod viewer; +pub use viewer::Viewer; + use crate::layout; use crate::{Element, Hasher, Layout, Length, Point, Rectangle, Size, Widget}; 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 ff19cbc2..b72172cc 100644 --- a/native/src/widget/pane_grid.rs +++ b/native/src/widget/pane_grid.rs @@ -6,7 +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.2/examples/pane_grid +//! [`pane_grid` example]: https://github.com/hecrj/iced/tree/0.3/examples/pane_grid mod axis; mod configuration; mod content; @@ -33,7 +33,7 @@ use crate::layout; use crate::mouse; use crate::overlay; use crate::row; -use crate::text; +use crate::touch; use crate::{ Clipboard, Element, Hasher, Layout, Length, Point, Rectangle, Size, Vector, Widget, @@ -98,6 +98,7 @@ pub struct PaneGrid<'a, Message, Renderer: self::Renderer> { 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>)>, + style: <Renderer as self::Renderer>::Style, } impl<'a, Message, Renderer> PaneGrid<'a, Message, Renderer> @@ -129,6 +130,7 @@ where on_click: None, on_drag: None, on_resize: None, + style: Default::default(), } } @@ -186,6 +188,15 @@ where self.on_resize = Some((leeway, Box::new(f))); self } + + /// 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 + } } impl<'a, Message, Renderer> PaneGrid<'a, Message, Renderer> @@ -351,49 +362,41 @@ 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 event_status = event::Status::Ignored; match event { - Event::Mouse(mouse_event) => match mouse_event { - mouse::Event::ButtonPressed(mouse::Button::Left) => { - let bounds = layout.bounds(); - - 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, - ); + 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, @@ -401,47 +404,51 @@ where ); } } + None => { + self.click_pane(layout, cursor_position, messages); + } } } - mouse::Event::ButtonReleased(mouse::Button::Left) => { - 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)| { + } + 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)); - } + messages.push(on_drag(event)); + } - self.state.idle(); + self.state.idle(); - event_status = event::Status::Captured; - } else if self.state.picked_split().is_some() { - 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_status = - self.trigger_resize(layout, cursor_position, messages); + event_status = event::Status::Captured; } - _ => {} - }, + } + Event::Mouse(mouse::Event::CursorMoved { .. }) + | Event::Touch(touch::Event::FingerMoved { .. }) => { + event_status = + self.trigger_resize(layout, cursor_position, messages); + } _ => {} } @@ -454,9 +461,9 @@ where event.clone(), layout, cursor_position, - messages, renderer, clipboard, + messages, ) }) .fold(event_status, event::Status::merge) @@ -471,11 +478,28 @@ where defaults: &Renderer::Defaults, layout: Layout<'_>, cursor_position: Point, - _viewport: &Rectangle, + 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(); @@ -489,15 +513,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, @@ -506,7 +535,9 @@ where self.state.picked_pane(), picked_split, layout, + &self.style, cursor_position, + viewport, ) } @@ -543,9 +574,10 @@ where /// able to use a [`PaneGrid`] in your user interface. /// /// [renderer]: crate::renderer -pub trait Renderer: - crate::Renderer + container::Renderer + text::Renderer + Sized -{ +pub trait Renderer: crate::Renderer + container::Renderer + Sized { + /// The style supported by this renderer. + type Style: Default; + /// Draws a [`PaneGrid`]. /// /// It receives: @@ -559,9 +591,11 @@ pub trait Renderer: 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`]. @@ -575,10 +609,11 @@ pub trait Renderer: &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`]. @@ -586,20 +621,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 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; } @@ -623,14 +656,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/content.rs b/native/src/widget/pane_grid/content.rs index c9981903..b0110393 100644 --- a/native/src/widget/pane_grid/content.rs +++ b/native/src/widget/pane_grid/content.rs @@ -3,7 +3,7 @@ use crate::event::{self, Event}; use crate::layout; use crate::overlay; use crate::pane_grid::{self, TitleBar}; -use crate::{Clipboard, Element, Hasher, Layout, Point, Size}; +use crate::{Clipboard, Element, Hasher, Layout, Point, Rectangle, Size}; /// The content of a [`Pane`]. /// @@ -12,7 +12,7 @@ use crate::{Clipboard, Element, Hasher, Layout, Point, Size}; 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> @@ -24,7 +24,7 @@ where Self { title_bar: None, body: body.into(), - style: Renderer::Style::default(), + style: Default::default(), } } @@ -38,7 +38,10 @@ where } /// Sets the style of the [`Content`]. - 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 } @@ -57,6 +60,7 @@ where 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(); @@ -70,6 +74,7 @@ where Some((title_bar, title_bar_layout)), (&self.body, body_layout), cursor_position, + viewport, ) } else { renderer.draw_pane( @@ -79,6 +84,7 @@ where None, (&self.body, layout), cursor_position, + viewport, ) } } @@ -140,9 +146,9 @@ 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 event_status = event::Status::Ignored; @@ -153,9 +159,9 @@ where event.clone(), children.next().unwrap(), cursor_position, - messages, renderer, clipboard, + messages, ); children.next().unwrap() @@ -167,9 +173,9 @@ where event, body_layout, cursor_position, - messages, renderer, clipboard, + messages, ); event_status.merge(body_status) @@ -187,18 +193,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 319936fc..84714e00 100644 --- a/native/src/widget/pane_grid/node.rs +++ b/native/src/widget/pane_grid/node.rs @@ -3,7 +3,7 @@ use crate::{ Rectangle, Size, }; -use std::collections::HashMap; +use std::collections::BTreeMap; /// A layout node of a [`PaneGrid`]. /// @@ -59,8 +59,8 @@ impl Node { &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, @@ -83,8 +83,8 @@ impl Node { &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, @@ -191,7 +191,7 @@ impl Node { &self, spacing: f32, current: &Rectangle, - regions: &mut HashMap<Pane, Rectangle>, + regions: &mut BTreeMap<Pane, Rectangle>, ) { match self { Node::Split { @@ -212,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 39d9f3ef..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`]: crate::widget::PaneGrid -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[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 16975abc..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`]: crate::widget::PaneGrid -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[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 666e1ca0..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`]. /// @@ -257,7 +257,7 @@ impl Internal { &self, spacing: f32, size: Size, - ) -> HashMap<Pane, Rectangle> { + ) -> BTreeMap<Pane, Rectangle> { self.layout.pane_regions(spacing, size) } @@ -265,7 +265,7 @@ impl Internal { &self, spacing: f32, size: Size, - ) -> HashMap<Split, (Axis, Rectangle, f32)> { + ) -> BTreeMap<Split, (Axis, Rectangle, f32)> { self.layout.split_regions(spacing, size) } diff --git a/native/src/widget/pane_grid/title_bar.rs b/native/src/widget/pane_grid/title_bar.rs index 475cb9ae..070010f8 100644 --- a/native/src/widget/pane_grid/title_bar.rs +++ b/native/src/widget/pane_grid/title_bar.rs @@ -1,43 +1,42 @@ +use crate::container; use crate::event::{self, Event}; use crate::layout; +use crate::overlay; use crate::pane_grid; -use crate::{Clipboard, Element, Hasher, Layout, Point, Rectangle, Size}; +use crate::{ + Clipboard, Element, Hasher, Layout, Padding, Point, Rectangle, Size, +}; /// The title bar of a [`Pane`]. /// /// [`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. - 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`]. - pub fn title_size(mut self, size: u16) -> Self { - self.title_size = Some(size); - self - } - /// Sets the controls of the [`TitleBar`]. pub fn controls( mut self, @@ -47,14 +46,17 @@ where self } - /// Sets the padding of the [`TitleBar`]. - 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`]. - 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 } @@ -86,53 +88,36 @@ where 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 @@ -147,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 @@ -165,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( @@ -175,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 @@ -196,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( @@ -213,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( @@ -226,28 +207,63 @@ 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 { - if let Some(controls) = &mut self.controls { - let mut children = layout.children(); - let padded = children.next().unwrap(); + 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 58c0dfe1..f4b60fc4 100644 --- a/native/src/widget/pick_list.rs +++ b/native/src/widget/pick_list.rs @@ -6,8 +6,10 @@ use crate::overlay; use crate::overlay::menu::{self, Menu}; use crate::scrollable; use crate::text; +use crate::touch; use crate::{ - Clipboard, Element, Hasher, Layout, Length, Point, Rectangle, Size, Widget, + Clipboard, Element, Hasher, Layout, Length, Padding, Point, Rectangle, + Size, Widget, }; use std::borrow::Cow; @@ -23,9 +25,10 @@ where 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, @@ -80,6 +83,7 @@ where last_selection, on_selected: Box::new(on_selected), options: options.into(), + placeholder: None, selected, width: Length::Shrink, text_size: None, @@ -89,15 +93,21 @@ 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`]. pub fn width(mut self, width: Length) -> Self { self.width = width; self } - /// Sets the padding of the [`PickList`]. - 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 } @@ -132,7 +142,7 @@ where Renderer: self::Renderer + scrollable::Renderer + 'a, { fn width(&self) -> Length { - Length::Shrink + self.width } fn height(&self) -> Length { @@ -149,27 +159,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, }; @@ -178,11 +198,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) @@ -193,6 +213,8 @@ where match self.width { Length::Shrink => { + self.placeholder.hash(state); + self.options .iter() .map(ToString::to_string) @@ -209,12 +231,13 @@ 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 event_status = if *self.is_open { // TODO: Encode cursor availability in the type system *self.is_open = @@ -245,6 +268,43 @@ where event_status } } + Event::Mouse(mouse::Event::WheelScrolled { + delta: mouse::ScrollDelta::Lines { y, .. }, + }) if 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())); + } + + return event::Status::Captured; + } + _ => event::Status::Ignored, } } @@ -262,6 +322,7 @@ where 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, @@ -306,7 +367,7 @@ where /// [renderer]: crate::renderer pub trait Renderer: text::Renderer + menu::Renderer { /// The default padding of a [`PickList`]. - const DEFAULT_PADDING: u16; + const DEFAULT_PADDING: Padding; /// The [`PickList`] style supported by this renderer. type Style: Default; @@ -322,7 +383,8 @@ pub trait Renderer: text::Renderer + menu::Renderer { 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/radio.rs b/native/src/widget/radio.rs index 4935569f..dee82d1f 100644 --- a/native/src/widget/radio.rs +++ b/native/src/widget/radio.rs @@ -1,16 +1,17 @@ //! Create choices using radio buttons. +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, Color}; use crate::{ Align, Clipboard, Element, Hasher, HorizontalAlignment, Layout, Length, Point, Rectangle, Row, Text, VerticalAlignment, Widget, }; -use std::hash::Hash; - /// A circular button representing a choice. /// /// # Example @@ -46,6 +47,8 @@ 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, } @@ -80,6 +83,8 @@ where 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(), } } @@ -108,6 +113,18 @@ where 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. pub fn style(mut self, style: impl Into<Renderer::Style>) -> Self { self.style = style.into(); @@ -155,12 +172,13 @@ 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()); @@ -194,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, ); diff --git a/native/src/widget/row.rs b/native/src/widget/row.rs index b71663bd..9ebc9145 100644 --- a/native/src/widget/row.rs +++ b/native/src/widget/row.rs @@ -3,7 +3,8 @@ use crate::event::{self, Event}; use crate::layout; use crate::overlay; use crate::{ - Align, Clipboard, Element, Hasher, Layout, Length, Point, Rectangle, Widget, + Align, Clipboard, Element, Hasher, Layout, Length, Padding, Point, + Rectangle, Widget, }; use std::hash::Hash; @@ -13,7 +14,7 @@ use std::u32; #[allow(missing_debug_implementations)] pub struct Row<'a, Message, Renderer> { spacing: u16, - padding: u16, + padding: Padding, width: Length, height: Length, max_width: u32, @@ -34,7 +35,7 @@ impl<'a, Message, Renderer> Row<'a, Message, Renderer> { ) -> Self { Row { spacing: 0, - padding: 0, + padding: Padding::ZERO, width: Length::Shrink, height: Length::Shrink, max_width: u32::MAX, @@ -54,9 +55,9 @@ impl<'a, Message, Renderer> Row<'a, Message, Renderer> { self } - /// Sets the padding of the [`Row`]. - 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 } @@ -128,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, @@ -140,9 +141,9 @@ 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.children .iter_mut() @@ -152,9 +153,9 @@ where event.clone(), layout, cursor_position, - messages, renderer, clipboard, + messages, ) }) .fold(event::Status::Ignored, event::Status::merge) diff --git a/native/src/widget/scrollable.rs b/native/src/widget/scrollable.rs index e23ab06a..68da2e67 100644 --- a/native/src/widget/scrollable.rs +++ b/native/src/widget/scrollable.rs @@ -4,8 +4,9 @@ use crate::event::{self, Event}; use crate::layout; use crate::mouse; use crate::overlay; +use crate::touch; use crate::{ - Align, Clipboard, Column, Element, Hasher, Layout, Length, Point, + Align, Clipboard, Column, Element, Hasher, Layout, Length, Padding, Point, Rectangle, Size, Vector, Widget, }; @@ -22,6 +23,7 @@ pub struct Scrollable<'a, Message, Renderer: self::Renderer> { scrollbar_margin: u16, scroller_width: u16, content: Column<'a, Message, Renderer>, + on_scroll: Option<Box<dyn Fn(f32) -> Message>>, style: Renderer::Style, } @@ -36,6 +38,7 @@ impl<'a, Message, Renderer: self::Renderer> Scrollable<'a, Message, Renderer> { scrollbar_margin: 0, scroller_width: 10, content: Column::new(), + on_scroll: None, style: Renderer::Style::default(), } } @@ -50,9 +53,9 @@ impl<'a, Message, Renderer: self::Renderer> Scrollable<'a, Message, Renderer> { self } - /// Sets the padding of the [`Scrollable`]. - 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 } @@ -100,12 +103,22 @@ impl<'a, Message, Renderer: self::Renderer> Scrollable<'a, Message, Renderer> { } /// Sets the scroller width of the [`Scrollable`] . - /// Silently enforces a minimum value of 1. + /// + /// 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(); @@ -120,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> @@ -161,9 +192,9 @@ 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); @@ -204,9 +235,9 @@ where event.clone(), content, cursor_position, - messages, renderer, clipboard, + messages, ) }; @@ -227,6 +258,45 @@ where } } + 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; } _ => {} @@ -237,12 +307,15 @@ where 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) { @@ -255,6 +328,8 @@ where content_bounds, ); + self.notify_on_scroll(bounds, content_bounds, messages); + return event::Status::Captured; } } @@ -264,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) @@ -281,6 +357,12 @@ where self.state.scroller_grabbed_at = Some(scroller_grabbed_at); + self.notify_on_scroll( + bounds, + content_bounds, + messages, + ); + return event::Status::Captured; } } @@ -382,10 +464,44 @@ where } /// The local state of a [`Scrollable`]. -#[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 { @@ -406,13 +522,14 @@ 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. @@ -422,23 +539,40 @@ impl State { 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. 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`]. diff --git a/native/src/widget/slider.rs b/native/src/widget/slider.rs index ff39b816..2a74d5a3 100644 --- a/native/src/widget/slider.rs +++ b/native/src/widget/slider.rs @@ -4,6 +4,7 @@ use crate::event::{self, Event}; use crate::layout; use crate::mouse; +use crate::touch; use crate::{ Clipboard, Element, Hasher, Layout, Length, Point, Rectangle, Size, Widget, }; @@ -179,9 +180,9 @@ 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(); @@ -207,34 +208,35 @@ 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; - } + 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; - - return event::Status::Captured; + } + 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; - } + return event::Status::Captured; } - _ => {} - }, + } _ => {} } diff --git a/native/src/widget/text_input.rs b/native/src/widget/text_input.rs index 3e637e97..cec1e485 100644 --- a/native/src/widget/text_input.rs +++ b/native/src/widget/text_input.rs @@ -16,8 +16,10 @@ use crate::keyboard; use crate::layout; use crate::mouse::{self, click}; use crate::text; +use crate::touch; use crate::{ - Clipboard, Element, Hasher, Layout, Length, Point, Rectangle, Size, Widget, + Clipboard, Element, Hasher, Layout, Length, Padding, Point, Rectangle, + Size, Widget, }; use std::u32; @@ -55,7 +57,7 @@ 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>, @@ -91,7 +93,7 @@ where 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, @@ -125,9 +127,9 @@ where self } - /// Sets the padding of the [`TextInput`]. - 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 } @@ -222,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( @@ -242,12 +246,13 @@ 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; @@ -318,13 +323,16 @@ where 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 { @@ -354,7 +362,7 @@ where Event::Keyboard(keyboard::Event::CharacterReceived(c)) if self.state.is_focused && self.state.is_pasting.is_none() - && !self.state.keyboard_modifiers.is_command_pressed() + && !self.state.keyboard_modifiers.command() && !c.is_control() => { let mut editor = @@ -442,7 +450,7 @@ where if platform::is_jump_modifier_pressed(modifiers) && !self.is_secure { - if modifiers.shift { + if modifiers.shift() { self.state .cursor .select_left_by_words(&self.value); @@ -451,7 +459,7 @@ where .cursor .move_left_by_words(&self.value); } - } else if modifiers.shift { + } else if modifiers.shift() { self.state.cursor.select_left(&self.value) } else { self.state.cursor.move_left(&self.value); @@ -461,7 +469,7 @@ where if platform::is_jump_modifier_pressed(modifiers) && !self.is_secure { - if modifiers.shift { + if modifiers.shift() { self.state .cursor .select_right_by_words(&self.value); @@ -470,14 +478,14 @@ where .cursor .move_right_by_words(&self.value); } - } else if modifiers.shift { + } 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 { + if modifiers.shift() { self.state.cursor.select_range( self.state.cursor.start(&self.value), 0, @@ -487,7 +495,7 @@ where } } keyboard::KeyCode::End => { - if modifiers.shift { + if modifiers.shift() { self.state.cursor.select_range( self.state.cursor.start(&self.value), self.value.len(), @@ -496,45 +504,75 @@ where self.state.cursor.move_to(self.value.len()); } } - keyboard::KeyCode::V => { - if self.state.keyboard_modifiers.is_command_pressed() { - if let Some(clipboard) = clipboard { - let content = match self.state.is_pasting.take() - { - Some(content) => content, - None => { - let content: String = clipboard - .content() - .unwrap_or(String::new()) - .chars() - .filter(|c| !c.is_control()) - .collect(); - - Value::new(&content) - } - }; - - let mut editor = Editor::new( - &mut self.value, - &mut self.state.cursor, + 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 => {} + } - editor.paste(content.clone()); + let mut editor = Editor::new( + &mut self.value, + &mut self.state.cursor, + ); - let message = - (self.on_change)(editor.contents()); - messages.push(message); + editor.delete(); - self.state.is_pasting = Some(content); - } + let message = (self.on_change)(editor.contents()); + messages.push(message); + } + 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 + .read() + .unwrap_or(String::new()) + .chars() + .filter(|c| !c.is_control()) + .collect(); + + Value::new(&content) + } + }; + + let mut editor = Editor::new( + &mut self.value, + &mut self.state.cursor, + ); + + editor.paste(content.clone()); + + let message = (self.on_change)(editor.contents()); + messages.push(message); + + self.state.is_pasting = Some(content); } else { self.state.is_pasting = None; } } - keyboard::KeyCode::A => { - if self.state.keyboard_modifiers.is_command_pressed() { - self.state.cursor.select_all(&self.value); - } + keyboard::KeyCode::A + if self.state.keyboard_modifiers.command() => + { + self.state.cursor.select_all(&self.value); } keyboard::KeyCode::Escape => { self.state.is_focused = false; @@ -748,6 +786,11 @@ impl State { 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 @@ -811,9 +854,9 @@ mod platform { pub fn is_jump_modifier_pressed(modifiers: keyboard::Modifiers) -> bool { if cfg!(target_os = "macos") { - modifiers.alt + 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 e630e293..4f3b159b 100644 --- a/native/src/widget/text_input/cursor.rs +++ b/native/src/widget/text_input/cursor.rs @@ -48,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); } @@ -101,7 +113,7 @@ impl Cursor { State::Selection { start, end } if end > 0 => { self.select_range(start, end - 1) } - _ => (), + _ => {} } } @@ -113,7 +125,7 @@ impl Cursor { State::Selection { start, end } if end < value.len() => { self.select_range(start, end + 1) } - _ => (), + _ => {} } } @@ -161,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 86be2790..2034cca4 100644 --- a/native/src/widget/text_input/value.rs +++ b/native/src/widget/text_input/value.rs @@ -73,6 +73,15 @@ 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`. pub fn until(&self, index: usize) -> Self { 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..3aa1ab0b 100644 --- a/native/src/window/event.rs +++ b/native/src/window/event.rs @@ -3,7 +3,7 @@ use std::path::PathBuf; /// A window-related event. #[derive(PartialEq, Clone, Debug)] pub enum Event { - /// A window was resized + /// A window was resized. Resized { /// The new width of the window (in units) width: u32, @@ -12,6 +12,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 |