diff options
Diffstat (limited to 'native/src/widget')
29 files changed, 2333 insertions, 1292 deletions
diff --git a/native/src/widget/button.rs b/native/src/widget/button.rs index c932da2b..8e2450de 100644 --- a/native/src/widget/button.rs +++ b/native/src/widget/button.rs @@ -1,12 +1,12 @@ //! Allow your users to perform actions by pressing a button. //! //! A [`Button`] has some local [`State`]. -//! -//! [`Button`]: struct.Button.html -//! [`State`]: struct.State.html +use crate::event::{self, Event}; +use crate::layout; +use crate::mouse; +use crate::touch; use crate::{ - layout, mouse, Clipboard, Element, Event, Hasher, Layout, Length, Point, - Rectangle, Widget, + Clipboard, Element, Hasher, Layout, Length, Point, Rectangle, Widget, }; use std::hash::Hash; @@ -18,6 +18,7 @@ use std::hash::Hash; /// # type Button<'a, Message> = /// # iced_native::Button<'a, Message, iced_native::renderer::Null>; /// # +/// #[derive(Clone)] /// enum Message { /// ButtonPressed, /// } @@ -41,13 +42,11 @@ pub struct Button<'a, Message, Renderer: self::Renderer> { impl<'a, Message, Renderer> Button<'a, Message, Renderer> where + Message: Clone, Renderer: self::Renderer, { /// Creates a new [`Button`] with some local [`State`] and the given /// content. - /// - /// [`Button`]: struct.Button.html - /// [`State`]: struct.State.html pub fn new<E>(state: &'a mut State, content: E) -> Self where E: Into<Element<'a, Message, Renderer>>, @@ -66,56 +65,42 @@ where } /// Sets the width of the [`Button`]. - /// - /// [`Button`]: struct.Button.html pub fn width(mut self, width: Length) -> Self { self.width = width; self } /// Sets the height of the [`Button`]. - /// - /// [`Button`]: struct.Button.html pub fn height(mut self, height: Length) -> Self { self.height = height; self } /// Sets the minimum width of the [`Button`]. - /// - /// [`Button`]: struct.Button.html pub fn min_width(mut self, min_width: u32) -> Self { self.min_width = min_width; self } /// Sets the minimum height of the [`Button`]. - /// - /// [`Button`]: struct.Button.html pub fn min_height(mut self, min_height: u32) -> Self { self.min_height = min_height; self } /// Sets the padding of the [`Button`]. - /// - /// [`Button`]: struct.Button.html pub fn padding(mut self, padding: u16) -> Self { self.padding = padding; self } /// Sets the message that will be produced when the [`Button`] is pressed. - /// - /// [`Button`]: struct.Button.html pub fn on_press(mut self, msg: Message) -> Self { self.on_press = Some(msg); self } /// Sets the style of the [`Button`]. - /// - /// [`Button`]: struct.Button.html pub fn style(mut self, style: impl Into<Renderer::Style>) -> Self { self.style = style.into(); self @@ -123,8 +108,6 @@ where } /// The local state of a [`Button`]. -/// -/// [`Button`]: struct.Button.html #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub struct State { is_pressed: bool, @@ -132,8 +115,6 @@ pub struct State { impl State { /// Creates a new [`State`]. - /// - /// [`State`]: struct.State.html pub fn new() -> State { State::default() } @@ -142,8 +123,8 @@ impl State { impl<'a, Message, Renderer> Widget<Message, Renderer> for Button<'a, Message, Renderer> where - Renderer: self::Renderer, Message: Clone, + Renderer: self::Renderer, { fn width(&self) -> Length { self.width @@ -182,31 +163,43 @@ where messages: &mut Vec<Message>, _renderer: &Renderer, _clipboard: Option<&dyn Clipboard>, - ) { + ) -> 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 self.on_press.is_some() { let bounds = layout.bounds(); - self.state.is_pressed = bounds.contains(cursor_position); + if bounds.contains(cursor_position) { + self.state.is_pressed = true; + + return event::Status::Captured; + } } } - Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) => { + Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerLifted { .. }) => { if let Some(on_press) = self.on_press.clone() { let bounds = layout.bounds(); - let is_clicked = self.state.is_pressed - && bounds.contains(cursor_position); + if self.state.is_pressed { + self.state.is_pressed = false; - self.state.is_pressed = false; + if bounds.contains(cursor_position) { + messages.push(on_press); + } - if is_clicked { - messages.push(on_press); + return event::Status::Captured; } } } + Event::Touch(touch::Event::FingerLost { .. }) => { + self.state.is_pressed = false; + } _ => {} } + + event::Status::Ignored } fn draw( @@ -215,6 +208,7 @@ where defaults: &Renderer::Defaults, layout: Layout<'_>, cursor_position: Point, + _viewport: &Rectangle, ) -> Renderer::Output { renderer.draw( defaults, @@ -242,20 +236,15 @@ where /// Your [renderer] will need to implement this trait before being /// able to use a [`Button`] in your user interface. /// -/// [`Button`]: struct.Button.html -/// [renderer]: ../../renderer/index.html +/// [renderer]: crate::renderer pub trait Renderer: crate::Renderer + Sized { /// The default padding of a [`Button`]. - /// - /// [`Button`]: struct.Button.html const DEFAULT_PADDING: u16; /// The style supported by this renderer. type Style: Default; /// Draws a [`Button`]. - /// - /// [`Button`]: struct.Button.html fn draw<Message>( &mut self, defaults: &Self::Defaults, @@ -272,8 +261,8 @@ pub trait Renderer: crate::Renderer + Sized { impl<'a, Message, Renderer> From<Button<'a, Message, Renderer>> for Element<'a, Message, Renderer> where - Renderer: 'a + self::Renderer, Message: 'a + Clone, + Renderer: 'a + self::Renderer, { fn from( button: Button<'a, Message, Renderer>, diff --git a/native/src/widget/checkbox.rs b/native/src/widget/checkbox.rs index 5fb13290..77a82fad 100644 --- a/native/src/widget/checkbox.rs +++ b/native/src/widget/checkbox.rs @@ -1,10 +1,15 @@ //! Show toggle controls using checkboxes. use std::hash::Hash; +use crate::event::{self, Event}; +use crate::layout; +use crate::mouse; +use crate::row; +use crate::text; +use crate::touch; use crate::{ - layout, mouse, row, text, Align, Clipboard, Element, Event, Hasher, - HorizontalAlignment, Layout, Length, Point, Rectangle, Row, Text, - VerticalAlignment, Widget, + Align, Clipboard, Element, Hasher, HorizontalAlignment, Layout, Length, + Point, Rectangle, Row, Text, VerticalAlignment, Widget, }; /// A box that can be checked. @@ -32,7 +37,8 @@ pub struct Checkbox<Message, Renderer: self::Renderer + text::Renderer> { width: Length, size: u16, spacing: u16, - text_size: u16, + text_size: Option<u16>, + font: Renderer::Font, style: Renderer::Style, } @@ -47,8 +53,6 @@ impl<Message, Renderer: self::Renderer + text::Renderer> /// * a function that will be called when the [`Checkbox`] is toggled. It /// will receive the new state of the [`Checkbox`] and must produce a /// `Message`. - /// - /// [`Checkbox`]: struct.Checkbox.html pub fn new<F>(is_checked: bool, label: impl Into<String>, f: F) -> Self where F: 'static + Fn(bool) -> Message, @@ -60,46 +64,45 @@ impl<Message, Renderer: self::Renderer + text::Renderer> width: Length::Shrink, size: <Renderer as self::Renderer>::DEFAULT_SIZE, spacing: Renderer::DEFAULT_SPACING, - text_size: <Renderer as text::Renderer>::DEFAULT_SIZE, + text_size: None, + font: Renderer::Font::default(), style: Renderer::Style::default(), } } /// Sets the size of the [`Checkbox`]. - /// - /// [`Checkbox`]: struct.Checkbox.html pub fn size(mut self, size: u16) -> Self { self.size = size; self } /// Sets the width of the [`Checkbox`]. - /// - /// [`Checkbox`]: struct.Checkbox.html pub fn width(mut self, width: Length) -> Self { self.width = width; self } /// Sets the spacing between the [`Checkbox`] and the text. - /// - /// [`Checkbox`]: struct.Checkbox.html pub fn spacing(mut self, spacing: u16) -> Self { self.spacing = spacing; self } /// Sets the text size of the [`Checkbox`]. - /// - /// [`Checkbox`]: struct.Checkbox.html pub fn text_size(mut self, text_size: u16) -> Self { - self.text_size = text_size; + self.text_size = Some(text_size); self } - /// Sets the style of the [`Checkbox`]. + /// Sets the [`Font`] of the text of the [`Checkbox`]. /// - /// [`Checkbox`]: struct.Checkbox.html + /// [`Font`]: crate::widget::text::Renderer::Font + pub fn font(mut self, font: Renderer::Font) -> Self { + self.font = font; + self + } + + /// Sets the style of the [`Checkbox`]. pub fn style(mut self, style: impl Into<Renderer::Style>) -> Self { self.style = style.into(); self @@ -135,8 +138,9 @@ where ) .push( Text::new(&self.label) + .font(self.font) .width(self.width) - .size(self.text_size), + .size(self.text_size.unwrap_or(renderer.default_size())), ) .layout(renderer, limits) } @@ -149,17 +153,22 @@ where messages: &mut Vec<Message>, _renderer: &Renderer, _clipboard: Option<&dyn Clipboard>, - ) { + ) -> event::Status { match event { - Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => { + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerPressed { .. }) => { let mouse_over = layout.bounds().contains(cursor_position); if mouse_over { messages.push((self.on_toggle)(!self.is_checked)); + + return event::Status::Captured; } } _ => {} } + + event::Status::Ignored } fn draw( @@ -168,6 +177,7 @@ where defaults: &Renderer::Defaults, layout: Layout<'_>, cursor_position: Point, + _viewport: &Rectangle, ) -> Renderer::Output { let bounds = layout.bounds(); let mut children = layout.children(); @@ -181,8 +191,8 @@ where defaults, label_layout.bounds(), &self.label, - self.text_size, - Default::default(), + self.text_size.unwrap_or(renderer.default_size()), + self.font, None, HorizontalAlignment::Left, VerticalAlignment::Center, @@ -213,20 +223,15 @@ where /// Your [renderer] will need to implement this trait before being /// able to use a [`Checkbox`] in your user interface. /// -/// [`Checkbox`]: struct.Checkbox.html -/// [renderer]: ../../renderer/index.html +/// [renderer]: crate::Renderer pub trait Renderer: crate::Renderer { /// The style supported by this renderer. type Style: Default; /// The default size of a [`Checkbox`]. - /// - /// [`Checkbox`]: struct.Checkbox.html const DEFAULT_SIZE: u16; /// The default spacing of a [`Checkbox`]. - /// - /// [`Checkbox`]: struct.Checkbox.html const DEFAULT_SPACING: u16; /// Draws a [`Checkbox`]. @@ -236,8 +241,6 @@ pub trait Renderer: crate::Renderer { /// * whether the [`Checkbox`] is selected or not /// * whether the mouse is over the [`Checkbox`] or not /// * the drawn label of the [`Checkbox`] - /// - /// [`Checkbox`]: struct.Checkbox.html fn draw( &mut self, bounds: Rectangle, diff --git a/native/src/widget/column.rs b/native/src/widget/column.rs index 259a7e6e..e0e88d31 100644 --- a/native/src/widget/column.rs +++ b/native/src/widget/column.rs @@ -1,18 +1,16 @@ //! Distribute content vertically. use std::hash::Hash; +use crate::event::{self, Event}; +use crate::layout; +use crate::overlay; use crate::{ - layout, Align, Clipboard, Element, Event, Hasher, Layout, Length, Point, - Widget, + Align, Clipboard, Element, Hasher, Layout, Length, Point, Rectangle, Widget, }; use std::u32; /// A container that distributes its contents vertically. -/// -/// A [`Column`] will try to fill the horizontal space of its container. -/// -/// [`Column`]: struct.Column.html #[allow(missing_debug_implementations)] pub struct Column<'a, Message, Renderer> { spacing: u16, @@ -27,15 +25,11 @@ pub struct Column<'a, Message, Renderer> { impl<'a, Message, Renderer> Column<'a, Message, Renderer> { /// Creates an empty [`Column`]. - /// - /// [`Column`]: struct.Column.html pub fn new() -> Self { Self::with_children(Vec::new()) } /// Creates a [`Column`] with the given elements. - /// - /// [`Column`]: struct.Column.html pub fn with_children( children: Vec<Element<'a, Message, Renderer>>, ) -> Self { @@ -62,56 +56,42 @@ impl<'a, Message, Renderer> Column<'a, Message, Renderer> { } /// Sets the padding of the [`Column`]. - /// - /// [`Column`]: struct.Column.html pub fn padding(mut self, units: u16) -> Self { self.padding = units; self } /// Sets the width of the [`Column`]. - /// - /// [`Column`]: struct.Column.html pub fn width(mut self, width: Length) -> Self { self.width = width; self } /// Sets the height of the [`Column`]. - /// - /// [`Column`]: struct.Column.html pub fn height(mut self, height: Length) -> Self { self.height = height; self } /// Sets the maximum width of the [`Column`]. - /// - /// [`Column`]: struct.Column.html pub fn max_width(mut self, max_width: u32) -> Self { self.max_width = max_width; self } /// Sets the maximum height of the [`Column`] in pixels. - /// - /// [`Column`]: struct.Column.html pub fn max_height(mut self, max_height: u32) -> Self { self.max_height = max_height; self } /// Sets the horizontal alignment of the contents of the [`Column`] . - /// - /// [`Column`]: struct.Column.html pub fn align_items(mut self, align: Align) -> Self { self.align_items = align; self } /// Adds an element to the [`Column`]. - /// - /// [`Column`]: struct.Column.html pub fn push<E>(mut self, child: E) -> Self where E: Into<Element<'a, Message, Renderer>>, @@ -164,9 +144,11 @@ where messages: &mut Vec<Message>, renderer: &Renderer, clipboard: Option<&dyn Clipboard>, - ) { - self.children.iter_mut().zip(layout.children()).for_each( - |(child, layout)| { + ) -> event::Status { + self.children + .iter_mut() + .zip(layout.children()) + .map(|(child, layout)| { child.widget.on_event( event.clone(), layout, @@ -175,8 +157,8 @@ where renderer, clipboard, ) - }, - ); + }) + .fold(event::Status::Ignored, event::Status::merge) } fn draw( @@ -185,8 +167,15 @@ where defaults: &Renderer::Defaults, layout: Layout<'_>, cursor_position: Point, + viewport: &Rectangle, ) -> Renderer::Output { - renderer.draw(defaults, &self.children, layout, cursor_position) + renderer.draw( + defaults, + &self.children, + layout, + cursor_position, + viewport, + ) } fn hash_layout(&self, state: &mut Hasher) { @@ -199,11 +188,23 @@ where self.max_height.hash(state); self.align_items.hash(state); self.spacing.hash(state); + self.padding.hash(state); for child in &self.children { child.widget.hash_layout(state); } } + + fn overlay( + &mut self, + layout: Layout<'_>, + ) -> Option<overlay::Element<'_, Message, Renderer>> { + self.children + .iter_mut() + .zip(layout.children()) + .filter_map(|(child, layout)| child.widget.overlay(layout)) + .next() + } } /// The renderer of a [`Column`]. @@ -211,8 +212,7 @@ where /// Your [renderer] will need to implement this trait before being /// able to use a [`Column`] in your user interface. /// -/// [`Column`]: struct.Column.html -/// [renderer]: ../../renderer/index.html +/// [renderer]: crate::renderer pub trait Renderer: crate::Renderer + Sized { /// Draws a [`Column`]. /// @@ -220,15 +220,13 @@ pub trait Renderer: crate::Renderer + Sized { /// - the children of the [`Column`] /// - the [`Layout`] of the [`Column`] and its children /// - the cursor position - /// - /// [`Column`]: struct.Column.html - /// [`Layout`]: ../layout/struct.Layout.html fn draw<Message>( &mut self, defaults: &Self::Defaults, content: &[Element<'_, Message, Self>], layout: Layout<'_>, cursor_position: Point, + viewport: &Rectangle, ) -> Self::Output; } diff --git a/native/src/widget/container.rs b/native/src/widget/container.rs index 2590fe3b..65764148 100644 --- a/native/src/widget/container.rs +++ b/native/src/widget/container.rs @@ -1,9 +1,11 @@ //! Decorate content and apply alignment. use std::hash::Hash; +use crate::event::{self, Event}; +use crate::layout; +use crate::overlay; use crate::{ - layout, Align, Clipboard, Element, Event, Hasher, Layout, Length, Point, - Rectangle, Widget, + Align, Clipboard, Element, Hasher, Layout, Length, Point, Rectangle, Widget, }; use std::u32; @@ -29,8 +31,6 @@ where Renderer: self::Renderer, { /// Creates an empty [`Container`]. - /// - /// [`Container`]: struct.Container.html pub fn new<T>(content: T) -> Self where T: Into<Element<'a, Message, Renderer>>, @@ -49,80 +49,60 @@ where } /// Sets the padding of the [`Container`]. - /// - /// [`Container`]: struct.Column.html pub fn padding(mut self, units: u16) -> Self { self.padding = units; self } /// Sets the width of the [`Container`]. - /// - /// [`Container`]: struct.Container.html pub fn width(mut self, width: Length) -> Self { self.width = width; self } /// Sets the height of the [`Container`]. - /// - /// [`Container`]: struct.Container.html pub fn height(mut self, height: Length) -> Self { self.height = height; self } /// Sets the maximum width of the [`Container`]. - /// - /// [`Container`]: struct.Container.html pub fn max_width(mut self, max_width: u32) -> Self { self.max_width = max_width; self } /// Sets the maximum height of the [`Container`] in pixels. - /// - /// [`Container`]: struct.Container.html pub fn max_height(mut self, max_height: u32) -> Self { self.max_height = max_height; self } /// Sets the content alignment for the horizontal axis of the [`Container`]. - /// - /// [`Container`]: struct.Container.html pub fn align_x(mut self, alignment: Align) -> Self { self.horizontal_alignment = alignment; self } /// Sets the content alignment for the vertical axis of the [`Container`]. - /// - /// [`Container`]: struct.Container.html pub fn align_y(mut self, alignment: Align) -> Self { self.vertical_alignment = alignment; self } /// Centers the contents in the horizontal axis of the [`Container`]. - /// - /// [`Container`]: struct.Container.html pub fn center_x(mut self) -> Self { self.horizontal_alignment = Align::Center; self } /// Centers the contents in the vertical axis of the [`Container`]. - /// - /// [`Container`]: struct.Container.html pub fn center_y(mut self) -> Self { self.vertical_alignment = Align::Center; self } /// Sets the style of the [`Container`]. - /// - /// [`Container`]: struct.Container.html pub fn style(mut self, style: impl Into<Renderer::Style>) -> Self { self.style = style.into(); self @@ -174,7 +154,7 @@ where messages: &mut Vec<Message>, renderer: &Renderer, clipboard: Option<&dyn Clipboard>, - ) { + ) -> event::Status { self.content.widget.on_event( event, layout.children().next().unwrap(), @@ -191,11 +171,13 @@ where defaults: &Renderer::Defaults, layout: Layout<'_>, cursor_position: Point, + viewport: &Rectangle, ) -> Renderer::Output { renderer.draw( defaults, layout.bounds(), cursor_position, + viewport, &self.style, &self.content, layout.children().next().unwrap(), @@ -214,6 +196,13 @@ where 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 [`Container`]. @@ -221,20 +210,18 @@ where /// Your [renderer] will need to implement this trait before being /// able to use a [`Container`] in your user interface. /// -/// [`Container`]: struct.Container.html -/// [renderer]: ../../renderer/index.html +/// [renderer]: crate::renderer pub trait Renderer: crate::Renderer { /// The style supported by this renderer. type Style: Default; /// Draws a [`Container`]. - /// - /// [`Container`]: struct.Container.html fn draw<Message>( &mut self, defaults: &Self::Defaults, bounds: Rectangle, cursor_position: Point, + viewport: &Rectangle, style: &Self::Style, content: &Element<'_, Message, Self>, content_layout: Layout<'_>, diff --git a/native/src/widget/image.rs b/native/src/widget/image.rs index 49905830..4d8e0a3f 100644 --- a/native/src/widget/image.rs +++ b/native/src/widget/image.rs @@ -2,7 +2,8 @@ pub mod viewer; pub use viewer::Viewer; -use crate::{layout, Element, Hasher, Layout, Length, Point, Size, Widget}; +use crate::layout; +use crate::{Element, Hasher, Layout, Length, Point, Rectangle, Size, Widget}; use std::{ hash::{Hash, Hasher as _}, @@ -30,8 +31,6 @@ pub struct Image { impl Image { /// Creates a new [`Image`] with the given path. - /// - /// [`Image`]: struct.Image.html pub fn new<T: Into<Handle>>(handle: T) -> Self { Image { handle: handle.into(), @@ -41,16 +40,12 @@ impl Image { } /// Sets the width of the [`Image`] boundaries. - /// - /// [`Image`]: struct.Image.html pub fn width(mut self, width: Length) -> Self { self.width = width; self } /// Sets the height of the [`Image`] boundaries. - /// - /// [`Image`]: struct.Image.html pub fn height(mut self, height: Length) -> Self { self.height = height; self @@ -100,6 +95,7 @@ where _defaults: &Renderer::Defaults, layout: Layout<'_>, _cursor_position: Point, + _viewport: &Rectangle, ) -> Renderer::Output { renderer.draw(self.handle.clone(), layout) } @@ -115,8 +111,6 @@ where } /// An [`Image`] handle. -/// -/// [`Image`]: struct.Image.html #[derive(Debug, Clone)] pub struct Handle { id: u64, @@ -127,8 +121,6 @@ impl Handle { /// Creates an image [`Handle`] pointing to the image of the given path. /// /// Makes an educated guess about the image format by examining the data in the file. - /// - /// [`Handle`]: struct.Handle.html pub fn from_path<T: Into<PathBuf>>(path: T) -> Handle { Self::from_data(Data::Path(path.into())) } @@ -138,8 +130,6 @@ impl Handle { /// pixels. /// /// This is useful if you have already decoded your image. - /// - /// [`Handle`]: struct.Handle.html pub fn from_pixels(width: u32, height: u32, pixels: Vec<u8>) -> Handle { Self::from_data(Data::Pixels { width, @@ -154,8 +144,6 @@ impl Handle { /// /// This is useful if you already have your image loaded in-memory, maybe /// because you downloaded or generated it procedurally. - /// - /// [`Handle`]: struct.Handle.html pub fn from_memory(bytes: Vec<u8>) -> Handle { Self::from_data(Data::Bytes(bytes)) } @@ -171,15 +159,11 @@ impl Handle { } /// Returns the unique identifier of the [`Handle`]. - /// - /// [`Handle`]: struct.Handle.html pub fn id(&self) -> u64 { self.id } /// Returns a reference to the image [`Data`]. - /// - /// [`Data`]: enum.Data.html pub fn data(&self) -> &Data { &self.data } @@ -201,8 +185,6 @@ impl Hash for Handle { } /// The data of an [`Image`]. -/// -/// [`Image`]: struct.Image.html #[derive(Clone, Hash)] pub enum Data { /// File data @@ -239,17 +221,12 @@ impl std::fmt::Debug for Data { /// Your [renderer] will need to implement this trait before being able to use /// an [`Image`] in your user interface. /// -/// [`Image`]: struct.Image.html -/// [renderer]: ../../renderer/index.html +/// [renderer]: crate::renderer pub trait Renderer: crate::Renderer { /// Returns the dimensions of an [`Image`] located on the given path. - /// - /// [`Image`]: struct.Image.html fn dimensions(&self, handle: &Handle) -> (u32, u32); /// Draws an [`Image`]. - /// - /// [`Image`]: struct.Image.html fn draw(&mut self, handle: Handle, layout: Layout<'_>) -> Self::Output; } diff --git a/native/src/widget/image/viewer.rs b/native/src/widget/image/viewer.rs index b129924b..9544beab 100644 --- a/native/src/widget/image/viewer.rs +++ b/native/src/widget/image/viewer.rs @@ -1,7 +1,11 @@ //! Zoom and pan on an image. +use crate::event::{self, Event}; +use crate::image; +use crate::layout; +use crate::mouse; use crate::{ - image, layout, mouse, Clipboard, Element, Event, Hasher, Layout, Length, - Point, Rectangle, Size, Vector, Widget, + Clipboard, Element, Hasher, Layout, Length, Point, Rectangle, Size, Vector, + Widget, }; use std::{f32, hash::Hash, u32}; @@ -216,7 +220,7 @@ where _messages: &mut Vec<Message>, renderer: &Renderer, _clipboard: Option<&dyn Clipboard>, - ) { + ) -> event::Status { let bounds = layout.bounds(); let is_mouse_over = bounds.contains(cursor_position); @@ -290,11 +294,16 @@ where self.state.starting_cursor_pos = None } } - Event::Mouse(mouse::Event::CursorMoved { x, y }) => { + Event::Mouse(mouse::Event::CursorMoved { position }) => { if self.state.is_cursor_clicked() { let image_bounds = self.image_bounds(renderer, bounds); - self.state.pan(x, y, bounds, image_bounds); + self.state.pan( + position.x, + position.y, + bounds, + image_bounds, + ); } } _ => {} @@ -305,6 +314,8 @@ where self.state.starting_cursor_pos = None; } } + + event::Status::Ignored } fn draw( @@ -313,6 +324,7 @@ where _defaults: &Renderer::Defaults, layout: Layout<'_>, cursor_position: Point, + _viewport: &Rectangle, ) -> Renderer::Output { let bounds = layout.bounds(); diff --git a/native/src/widget/pane_grid.rs b/native/src/widget/pane_grid.rs index 076ae76f..ff19cbc2 100644 --- a/native/src/widget/pane_grid.rs +++ b/native/src/widget/pane_grid.rs @@ -6,33 +6,43 @@ //! The [`pane_grid` example] showcases how to use a [`PaneGrid`] with resizing, //! drag and drop, and hotkey support. //! -//! [`pane_grid` example]: https://github.com/hecrj/iced/tree/0.1/examples/pane_grid -//! [`PaneGrid`]: struct.PaneGrid.html +//! [`pane_grid` example]: https://github.com/hecrj/iced/tree/0.2/examples/pane_grid mod axis; +mod configuration; mod content; mod direction; mod node; mod pane; mod split; mod state; +mod title_bar; pub use axis::Axis; +pub use configuration::Configuration; pub use content::Content; pub use direction::Direction; pub use node::Node; pub use pane::Pane; pub use split::Split; -pub use state::{Focus, State}; - +pub use state::State; +pub use title_bar::TitleBar; + +use crate::container; +use crate::event::{self, Event}; +use crate::layout; +use crate::mouse; +use crate::overlay; +use crate::row; +use crate::text; use crate::{ - keyboard, layout, mouse, Clipboard, Element, Event, Hasher, Layout, Length, - Point, Size, Widget, + Clipboard, Element, Hasher, Layout, Length, Point, Rectangle, Size, Vector, + Widget, }; /// A collection of panes distributed using either vertical or horizontal splits /// to completely fill the space available. /// -/// [](https://gfycat.com/mixedflatjellyfish) +/// [](https://gfycat.com/frailfreshairedaleterrier) /// /// This distribution of space is common in tiling window managers (like /// [`awesome`](https://awesomewm.org/), [`i3`](https://i3wm.org/), or even @@ -69,134 +79,89 @@ use crate::{ /// let (mut state, _) = pane_grid::State::new(PaneState::SomePane); /// /// let pane_grid = -/// PaneGrid::new(&mut state, |pane, state, focus| { -/// match state { +/// PaneGrid::new(&mut state, |pane, state| { +/// pane_grid::Content::new(match state { /// PaneState::SomePane => Text::new("This is some pane"), /// PaneState::AnotherKindOfPane => Text::new("This is another kind of pane"), -/// }.into() +/// }) /// }) /// .on_drag(Message::PaneDragged) -/// .on_resize(Message::PaneResized); +/// .on_resize(10, Message::PaneResized); /// ``` -/// -/// [`PaneGrid`]: struct.PaneGrid.html -/// [`State`]: struct.State.html #[allow(missing_debug_implementations)] -pub struct PaneGrid<'a, Message, Renderer> { +pub struct PaneGrid<'a, Message, Renderer: self::Renderer> { state: &'a mut state::Internal, - pressed_modifiers: &'a mut keyboard::ModifiersState, - elements: Vec<(Pane, Element<'a, Message, Renderer>)>, + elements: Vec<(Pane, Content<'a, Message, Renderer>)>, width: Length, height: Length, spacing: u16, - modifier_keys: keyboard::ModifiersState, + on_click: Option<Box<dyn Fn(Pane) -> Message + 'a>>, on_drag: Option<Box<dyn Fn(DragEvent) -> Message + 'a>>, - on_resize: Option<Box<dyn Fn(ResizeEvent) -> Message + 'a>>, - on_key_press: Option<Box<dyn Fn(KeyPressEvent) -> Option<Message> + 'a>>, + on_resize: Option<(u16, Box<dyn Fn(ResizeEvent) -> Message + 'a>)>, } -impl<'a, Message, Renderer> PaneGrid<'a, Message, Renderer> { +impl<'a, Message, Renderer> PaneGrid<'a, Message, Renderer> +where + Renderer: self::Renderer, +{ /// Creates a [`PaneGrid`] with the given [`State`] and view function. /// /// The view function will be called to display each [`Pane`] present in the /// [`State`]. - /// - /// [`PaneGrid`]: struct.PaneGrid.html - /// [`State`]: struct.State.html - /// [`Pane`]: struct.Pane.html pub fn new<T>( state: &'a mut State<T>, - view: impl Fn( - Pane, - &'a mut T, - Option<Focus>, - ) -> Element<'a, Message, Renderer>, + view: impl Fn(Pane, &'a mut T) -> Content<'a, Message, Renderer>, ) -> Self { let elements = { - let action = state.internal.action(); - let current_focus = action.focus(); - state .panes .iter_mut() - .map(move |(pane, pane_state)| { - let focus = match current_focus { - Some((focused_pane, focus)) - if *pane == focused_pane => - { - Some(focus) - } - _ => None, - }; - - (*pane, view(*pane, pane_state, focus)) - }) + .map(|(pane, pane_state)| (*pane, view(*pane, pane_state))) .collect() }; Self { state: &mut state.internal, - pressed_modifiers: &mut state.modifiers, elements, width: Length::Fill, height: Length::Fill, spacing: 0, - modifier_keys: keyboard::ModifiersState { - control: true, - ..Default::default() - }, + on_click: None, on_drag: None, on_resize: None, - on_key_press: None, } } /// Sets the width of the [`PaneGrid`]. - /// - /// [`PaneGrid`]: struct.PaneGrid.html pub fn width(mut self, width: Length) -> Self { self.width = width; self } /// Sets the height of the [`PaneGrid`]. - /// - /// [`PaneGrid`]: struct.PaneGrid.html pub fn height(mut self, height: Length) -> Self { self.height = height; self } /// Sets the spacing _between_ the panes of the [`PaneGrid`]. - /// - /// [`PaneGrid`]: struct.PaneGrid.html pub fn spacing(mut self, units: u16) -> Self { self.spacing = units; self } - /// Sets the modifier keys of the [`PaneGrid`]. - /// - /// The modifier keys will need to be pressed to trigger dragging, resizing, - /// and key events. - /// - /// The default modifier key is `Ctrl`. - /// - /// [`PaneGrid`]: struct.PaneGrid.html - pub fn modifier_keys( - mut self, - modifier_keys: keyboard::ModifiersState, - ) -> Self { - self.modifier_keys = modifier_keys; + /// Sets the message that will be produced when a [`Pane`] of the + /// [`PaneGrid`] is clicked. + pub fn on_click<F>(mut self, f: F) -> Self + where + F: 'a + Fn(Pane) -> Message, + { + self.on_click = Some(Box::new(f)); self } /// Enables the drag and drop interactions of the [`PaneGrid`], which will /// use the provided function to produce messages. - /// - /// Panes can be dragged using `Modifier keys + Left click`. - /// - /// [`PaneGrid`]: struct.PaneGrid.html pub fn on_drag<F>(mut self, f: F) -> Self where F: 'a + Fn(DragEvent) -> Message, @@ -208,40 +173,54 @@ impl<'a, Message, Renderer> PaneGrid<'a, Message, Renderer> { /// Enables the resize interactions of the [`PaneGrid`], which will /// use the provided function to produce messages. /// - /// Panes can be resized using `Modifier keys + Right click`. + /// The `leeway` describes the amount of space around a split that can be + /// used to grab it. /// - /// [`PaneGrid`]: struct.PaneGrid.html - pub fn on_resize<F>(mut self, f: F) -> Self + /// The grabbable area of a split will have a length of `spacing + leeway`, + /// properly centered. In other words, a length of + /// `(spacing + leeway) / 2.0` on either side of the split line. + pub fn on_resize<F>(mut self, leeway: u16, f: F) -> Self where F: 'a + Fn(ResizeEvent) -> Message, { - self.on_resize = Some(Box::new(f)); + self.on_resize = Some((leeway, Box::new(f))); self } +} - /// Captures hotkey interactions with the [`PaneGrid`], using the provided - /// function to produce messages. - /// - /// The function will be called when: - /// - a [`Pane`] is focused - /// - a key is pressed - /// - all the modifier keys are pressed - /// - /// If the function returns `None`, the key press event will be discarded - /// without producing any message. - /// - /// This method is particularly useful to implement hotkey interactions. - /// For instance, you can use it to enable splitting, swapping, or resizing - /// panes by pressing combinations of keys. - /// - /// [`PaneGrid`]: struct.PaneGrid.html - /// [`Pane`]: struct.Pane.html - pub fn on_key_press<F>(mut self, f: F) -> Self - where - F: 'a + Fn(KeyPressEvent) -> Option<Message>, - { - self.on_key_press = Some(Box::new(f)); - self +impl<'a, Message, Renderer> PaneGrid<'a, Message, Renderer> +where + Renderer: self::Renderer, +{ + fn click_pane( + &mut self, + layout: Layout<'_>, + cursor_position: Point, + messages: &mut Vec<Message>, + ) { + let mut clicked_region = + self.elements.iter().zip(layout.children()).filter( + |(_, layout)| layout.bounds().contains(cursor_position), + ); + + if let Some(((pane, content), layout)) = clicked_region.next() { + if let Some(on_click) = &self.on_click { + messages.push(on_click(*pane)); + } + + if let Some(on_drag) = &self.on_drag { + if content.can_be_picked_at(layout, cursor_position) { + let pane_position = layout.position(); + + let origin = cursor_position + - Vector::new(pane_position.x, pane_position.y); + + self.state.pick_pane(pane, origin); + + messages.push(on_drag(DragEvent::Picked { pane: *pane })); + } + } + } } fn trigger_resize( @@ -249,12 +228,12 @@ impl<'a, Message, Renderer> PaneGrid<'a, Message, Renderer> { layout: Layout<'_>, cursor_position: Point, messages: &mut Vec<Message>, - ) { - if let Some(on_resize) = &self.on_resize { + ) -> event::Status { + if let Some((_, on_resize)) = &self.on_resize { if let Some((split, _)) = self.state.picked_split() { let bounds = layout.bounds(); - let splits = self.state.splits( + let splits = self.state.split_regions( f32::from(self.spacing), Size::new(bounds.width, bounds.height), ); @@ -276,89 +255,59 @@ impl<'a, Message, Renderer> PaneGrid<'a, Message, Renderer> { }; messages.push(on_resize(ResizeEvent { split, ratio })); + + return event::Status::Captured; } } } + + event::Status::Ignored } } /// An event produced during a drag and drop interaction of a [`PaneGrid`]. -/// -/// [`PaneGrid`]: struct.PaneGrid.html #[derive(Debug, Clone, Copy)] pub enum DragEvent { /// A [`Pane`] was picked for dragging. - /// - /// [`Pane`]: struct.Pane.html Picked { /// The picked [`Pane`]. - /// - /// [`Pane`]: struct.Pane.html pane: Pane, }, /// A [`Pane`] was dropped on top of another [`Pane`]. - /// - /// [`Pane`]: struct.Pane.html Dropped { /// The picked [`Pane`]. - /// - /// [`Pane`]: struct.Pane.html pane: Pane, /// The [`Pane`] where the picked one was dropped on. - /// - /// [`Pane`]: struct.Pane.html target: Pane, }, /// A [`Pane`] was picked and then dropped outside of other [`Pane`] /// boundaries. - /// - /// [`Pane`]: struct.Pane.html Canceled { /// The picked [`Pane`]. - /// - /// [`Pane`]: struct.Pane.html pane: Pane, }, } /// An event produced during a resize interaction of a [`PaneGrid`]. -/// -/// [`PaneGrid`]: struct.PaneGrid.html #[derive(Debug, Clone, Copy)] pub struct ResizeEvent { /// The [`Split`] that is being dragged for resizing. - /// - /// [`Split`]: struct.Split.html pub split: Split, /// The new ratio of the [`Split`]. /// /// The ratio is a value in [0, 1], representing the exact position of a /// [`Split`] between two panes. - /// - /// [`Split`]: struct.Split.html pub ratio: f32, } -/// An event produced during a key press interaction of a [`PaneGrid`]. -/// -/// [`PaneGrid`]: struct.PaneGrid.html -#[derive(Debug, Clone, Copy)] -pub struct KeyPressEvent { - /// The key that was pressed. - pub key_code: keyboard::KeyCode, - - /// The state of the modifier keys when the key was pressed. - pub modifiers: keyboard::ModifiersState, -} - impl<'a, Message, Renderer> Widget<Message, Renderer> for PaneGrid<'a, Message, Renderer> where - Renderer: self::Renderer, + Renderer: self::Renderer + container::Renderer, { fn width(&self) -> Length { self.width @@ -376,7 +325,7 @@ where let limits = limits.width(self.width).height(self.height); let size = limits.resolve(Size::ZERO); - let regions = self.state.regions(f32::from(self.spacing), size); + let regions = self.state.pane_regions(f32::from(self.spacing), size); let children = self .elements @@ -405,42 +354,57 @@ where messages: &mut Vec<Message>, renderer: &Renderer, clipboard: Option<&dyn Clipboard>, - ) { + ) -> 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 mut clicked_region = - self.elements.iter().zip(layout.children()).filter( - |(_, layout)| { - layout.bounds().contains(cursor_position) - }, - ); - - if let Some(((pane, _), _)) = clicked_region.next() { - match &self.on_drag { - Some(on_drag) - if self - .pressed_modifiers - .matches(self.modifier_keys) => - { - self.state.pick_pane(pane); - - messages.push(on_drag(DragEvent::Picked { - pane: *pane, - })); + 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, + ); + + if let Some((split, axis)) = clicked_split { + self.state.pick_split(&split, axis); + } else { + self.click_pane( + layout, + cursor_position, + messages, + ); + } } - _ => { - self.state.focus(pane); + None => { + self.click_pane( + layout, + cursor_position, + messages, + ); } } - } else { - self.state.unfocus(); } } mouse::Event::ButtonReleased(mouse::Button::Left) => { - if let Some(pane) = self.state.picked_pane() { - self.state.focus(&pane); - + if let Some((pane, _)) = self.state.picked_pane() { if let Some(on_drag) = &self.on_drag { let mut dropped_region = self .elements @@ -462,131 +426,42 @@ where messages.push(on_drag(event)); } - } - } - mouse::Event::ButtonPressed(mouse::Button::Right) - if self.on_resize.is_some() - && self.state.picked_pane().is_none() - && self - .pressed_modifiers - .matches(self.modifier_keys) => - { - let bounds = layout.bounds(); - if bounds.contains(cursor_position) { - let relative_cursor = Point::new( - cursor_position.x - bounds.x, - cursor_position.y - bounds.y, - ); - - let splits = self.state.splits( - f32::from(self.spacing), - Size::new(bounds.width, bounds.height), - ); - - let mut sorted_splits: Vec<_> = splits - .iter() - .filter(|(_, (axis, rectangle, _))| match axis { - Axis::Horizontal => { - relative_cursor.x > rectangle.x - && relative_cursor.x - < rectangle.x + rectangle.width - } - Axis::Vertical => { - relative_cursor.y > rectangle.y - && relative_cursor.y - < rectangle.y + rectangle.height - } - }) - .collect(); - - sorted_splits.sort_by_key( - |(_, (axis, rectangle, ratio))| { - let distance = match axis { - Axis::Horizontal => (relative_cursor.y - - (rectangle.y - + rectangle.height * ratio)) - .abs(), - Axis::Vertical => (relative_cursor.x - - (rectangle.x - + rectangle.width * ratio)) - .abs(), - }; - - distance.round() as u32 - }, - ); - - if let Some((split, (axis, _, _))) = - sorted_splits.first() - { - self.state.pick_split(split, *axis); - self.trigger_resize( - layout, - cursor_position, - messages, - ); - } + 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::ButtonReleased(mouse::Button::Right) - if self.state.picked_split().is_some() => - { - self.state.drop_split(); - } mouse::Event::CursorMoved { .. } => { - self.trigger_resize(layout, cursor_position, messages); + event_status = + self.trigger_resize(layout, cursor_position, messages); } _ => {} }, - Event::Keyboard(keyboard_event) => { - match keyboard_event { - keyboard::Event::KeyPressed { - modifiers, - key_code, - } => { - if let Some(on_key_press) = &self.on_key_press { - // TODO: Discard when event is captured - if let Some(_) = self.state.active_pane() { - if modifiers.matches(self.modifier_keys) { - if let Some(message) = - on_key_press(KeyPressEvent { - key_code, - modifiers, - }) - { - messages.push(message); - } - } - } - } - - *self.pressed_modifiers = modifiers; - } - keyboard::Event::KeyReleased { modifiers, .. } => { - *self.pressed_modifiers = modifiers; - } - _ => {} - } - } _ => {} } if self.state.picked_pane().is_none() { - { - self.elements.iter_mut().zip(layout.children()).for_each( - |((_, pane), layout)| { - pane.widget.on_event( - event.clone(), - layout, - cursor_position, - messages, - renderer, - clipboard, - ) - }, - ); - } + self.elements + .iter_mut() + .zip(layout.children()) + .map(|((_, pane), layout)| { + pane.on_event( + event.clone(), + layout, + cursor_position, + messages, + renderer, + clipboard, + ) + }) + .fold(event_status, event::Status::merge) + } else { + event::Status::Captured } } @@ -596,12 +471,40 @@ where defaults: &Renderer::Defaults, layout: Layout<'_>, cursor_position: Point, + _viewport: &Rectangle, ) -> Renderer::Output { - renderer.draw( + let picked_split = self + .state + .picked_split() + .or_else(|| match self.on_resize { + Some((leeway, _)) => { + let bounds = layout.bounds(); + + 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), bounds.size()); + + hovered_split( + splits.iter(), + f32::from(self.spacing + leeway), + relative_cursor, + ) + } + None => None, + }) + .map(|(_, axis)| axis); + + self::Renderer::draw( + renderer, defaults, &self.elements, self.state.picked_pane(), - self.state.picked_split().map(|(_, axis)| axis), + picked_split, layout, cursor_position, ) @@ -609,6 +512,7 @@ where fn hash_layout(&self, state: &mut Hasher) { use std::hash::Hash; + struct Marker; std::any::TypeId::of::<Marker>().hash(state); @@ -620,6 +524,17 @@ where element.hash_layout(state); } } + + fn overlay( + &mut self, + layout: Layout<'_>, + ) -> Option<overlay::Element<'_, Message, Renderer>> { + self.elements + .iter_mut() + .zip(layout.children()) + .filter_map(|((_, pane), layout)| pane.overlay(layout)) + .next() + } } /// The renderer of a [`PaneGrid`]. @@ -627,9 +542,10 @@ where /// Your [renderer] will need to implement this trait before being /// able to use a [`PaneGrid`] in your user interface. /// -/// [`PaneGrid`]: struct.PaneGrid.html -/// [renderer]: ../../renderer/index.html -pub trait Renderer: crate::Renderer + Sized { +/// [renderer]: crate::renderer +pub trait Renderer: + crate::Renderer + container::Renderer + text::Renderer + Sized +{ /// Draws a [`PaneGrid`]. /// /// It receives: @@ -638,25 +554,59 @@ pub trait Renderer: crate::Renderer + Sized { /// - the [`Axis`] that is currently being resized /// - the [`Layout`] of the [`PaneGrid`] and its elements /// - the cursor position - /// - /// [`PaneGrid`]: struct.PaneGrid.html - /// [`Pane`]: struct.Pane.html - /// [`Layout`]: ../layout/struct.Layout.html fn draw<Message>( &mut self, defaults: &Self::Defaults, - content: &[(Pane, Element<'_, Message, Self>)], - dragging: Option<Pane>, + content: &[(Pane, Content<'_, Message, Self>)], + dragging: Option<(Pane, Point)>, resizing: Option<Axis>, layout: Layout<'_>, cursor_position: Point, ) -> Self::Output; + + /// Draws a [`Pane`]. + /// + /// It receives: + /// - the [`TitleBar`] of the [`Pane`], if any + /// - the [`Content`] of the [`Pane`] + /// - the [`Layout`] of the [`Pane`] and its elements + /// - the cursor position + fn draw_pane<Message>( + &mut self, + defaults: &Self::Defaults, + bounds: Rectangle, + style: &Self::Style, + title_bar: Option<(&TitleBar<'_, Message, Self>, Layout<'_>)>, + body: (&Element<'_, Message, Self>, Layout<'_>), + cursor_position: Point, + ) -> Self::Output; + + /// Draws a [`TitleBar`]. + /// + /// 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 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, + controls: Option<(&Element<'_, Message, Self>, Layout<'_>)>, + cursor_position: Point, + ) -> Self::Output; } impl<'a, Message, Renderer> From<PaneGrid<'a, Message, Renderer>> for Element<'a, Message, Renderer> where - Renderer: 'a + self::Renderer, + Renderer: 'a + self::Renderer + row::Renderer, Message: 'a, { fn from( @@ -665,3 +615,25 @@ where Element::new(pane_grid) } } + +/* + * Helpers + */ +fn hovered_split<'a>( + splits: impl Iterator<Item = (&'a Split, &'a (Axis, Rectangle, f32))>, + spacing: f32, + cursor_position: Point, +) -> Option<(Split, Axis)> { + 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)) + } else { + None + } + }) + .next() +} diff --git a/native/src/widget/pane_grid/axis.rs b/native/src/widget/pane_grid/axis.rs index f0e3f362..2320cb7c 100644 --- a/native/src/widget/pane_grid/axis.rs +++ b/native/src/widget/pane_grid/axis.rs @@ -14,41 +14,225 @@ impl Axis { &self, rectangle: &Rectangle, ratio: f32, - halved_spacing: f32, + spacing: f32, ) -> (Rectangle, Rectangle) { match self { Axis::Horizontal => { - let height_top = (rectangle.height * ratio).round(); - let height_bottom = rectangle.height - height_top; + let height_top = + (rectangle.height * ratio - spacing / 2.0).round(); + let height_bottom = rectangle.height - height_top - spacing; ( Rectangle { - height: height_top - halved_spacing, + height: height_top, ..*rectangle }, Rectangle { - y: rectangle.y + height_top + halved_spacing, - height: height_bottom - halved_spacing, + y: rectangle.y + height_top + spacing, + height: height_bottom, ..*rectangle }, ) } Axis::Vertical => { - let width_left = (rectangle.width * ratio).round(); - let width_right = rectangle.width - width_left; + let width_left = + (rectangle.width * ratio - spacing / 2.0).round(); + let width_right = rectangle.width - width_left - spacing; ( Rectangle { - width: width_left - halved_spacing, + width: width_left, ..*rectangle }, Rectangle { - x: rectangle.x + width_left + halved_spacing, - width: width_right - halved_spacing, + x: rectangle.x + width_left + spacing, + width: width_right, ..*rectangle }, ) } } } + + pub(super) fn split_line_bounds( + &self, + rectangle: Rectangle, + ratio: f32, + spacing: f32, + ) -> Rectangle { + match self { + Axis::Horizontal => Rectangle { + x: rectangle.x, + y: (rectangle.y + rectangle.height * ratio - spacing / 2.0) + .round(), + width: rectangle.width, + height: spacing, + }, + Axis::Vertical => Rectangle { + x: (rectangle.x + rectangle.width * ratio - spacing / 2.0) + .round(), + y: rectangle.y, + width: spacing, + height: rectangle.height, + }, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + enum Case { + Horizontal { + overall_height: f32, + spacing: f32, + top_height: f32, + bottom_y: f32, + bottom_height: f32, + }, + Vertical { + overall_width: f32, + spacing: f32, + left_width: f32, + right_x: f32, + right_width: f32, + }, + } + + #[test] + fn split() { + let cases = vec![ + // Even height, even spacing + Case::Horizontal { + overall_height: 10.0, + spacing: 2.0, + top_height: 4.0, + bottom_y: 6.0, + bottom_height: 4.0, + }, + // Odd height, even spacing + Case::Horizontal { + overall_height: 9.0, + spacing: 2.0, + top_height: 4.0, + bottom_y: 6.0, + bottom_height: 3.0, + }, + // Even height, odd spacing + Case::Horizontal { + overall_height: 10.0, + spacing: 1.0, + top_height: 5.0, + bottom_y: 6.0, + bottom_height: 4.0, + }, + // Odd height, odd spacing + Case::Horizontal { + overall_height: 9.0, + spacing: 1.0, + top_height: 4.0, + bottom_y: 5.0, + bottom_height: 4.0, + }, + // Even width, even spacing + Case::Vertical { + overall_width: 10.0, + spacing: 2.0, + left_width: 4.0, + right_x: 6.0, + right_width: 4.0, + }, + // Odd width, even spacing + Case::Vertical { + overall_width: 9.0, + spacing: 2.0, + left_width: 4.0, + right_x: 6.0, + right_width: 3.0, + }, + // Even width, odd spacing + Case::Vertical { + overall_width: 10.0, + spacing: 1.0, + left_width: 5.0, + right_x: 6.0, + right_width: 4.0, + }, + // Odd width, odd spacing + Case::Vertical { + overall_width: 9.0, + spacing: 1.0, + left_width: 4.0, + right_x: 5.0, + right_width: 4.0, + }, + ]; + for case in cases { + match case { + Case::Horizontal { + overall_height, + spacing, + top_height, + bottom_y, + bottom_height, + } => { + let a = Axis::Horizontal; + let r = Rectangle { + x: 0.0, + y: 0.0, + width: 10.0, + height: overall_height, + }; + let (top, bottom) = a.split(&r, 0.5, spacing); + assert_eq!( + top, + Rectangle { + height: top_height, + ..r + } + ); + assert_eq!( + bottom, + Rectangle { + y: bottom_y, + height: bottom_height, + ..r + } + ); + } + Case::Vertical { + overall_width, + spacing, + left_width, + right_x, + right_width, + } => { + let a = Axis::Vertical; + let r = Rectangle { + x: 0.0, + y: 0.0, + width: overall_width, + height: 10.0, + }; + let (left, right) = a.split(&r, 0.5, spacing); + assert_eq!( + left, + Rectangle { + width: left_width, + ..r + } + ); + assert_eq!( + right, + Rectangle { + x: right_x, + width: right_width, + ..r + } + ); + } + } + } + } } diff --git a/native/src/widget/pane_grid/configuration.rs b/native/src/widget/pane_grid/configuration.rs new file mode 100644 index 00000000..4c43826e --- /dev/null +++ b/native/src/widget/pane_grid/configuration.rs @@ -0,0 +1,26 @@ +use crate::pane_grid::Axis; + +/// The arrangement of a [`PaneGrid`]. +/// +/// [`PaneGrid`]: crate::pane_grid::PaneGrid +#[derive(Debug, Clone)] +pub enum Configuration<T> { + /// A split of the available space. + Split { + /// The direction of the split. + axis: Axis, + + /// The ratio of the split in [0.0, 1.0]. + ratio: f32, + + /// The left/top [`Configuration`] of the split. + a: Box<Configuration<T>>, + + /// The right/bottom [`Configuration`] of the split. + b: Box<Configuration<T>>, + }, + /// A [`Pane`]. + /// + /// [`Pane`]: crate::pane_grid::Pane + Pane(T), +} diff --git a/native/src/widget/pane_grid/content.rs b/native/src/widget/pane_grid/content.rs index 8822083e..c9981903 100644 --- a/native/src/widget/pane_grid/content.rs +++ b/native/src/widget/pane_grid/content.rs @@ -1,30 +1,213 @@ -use crate::pane_grid::Axis; +use crate::container; +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}; -/// The content of a [`PaneGrid`]. +/// The content of a [`Pane`]. /// -/// [`PaneGrid`]: struct.PaneGrid.html -#[derive(Debug, Clone)] -pub enum Content<T> { - /// A split of the available space. - Split { - /// The direction of the split. - axis: Axis, - - /// The ratio of the split in [0.0, 1.0]. - ratio: f32, - - /// The left/top [`Content`] of the split. - /// - /// [`Content`]: enum.Node.html - a: Box<Content<T>>, - - /// The right/bottom [`Content`] of the split. - /// - /// [`Content`]: enum.Node.html - b: Box<Content<T>>, - }, - /// A [`Pane`]. +/// [`Pane`]: crate::widget::pane_grid::Pane +#[allow(missing_debug_implementations)] +pub struct Content<'a, Message, Renderer: pane_grid::Renderer> { + title_bar: Option<TitleBar<'a, Message, Renderer>>, + body: Element<'a, Message, Renderer>, + style: Renderer::Style, +} + +impl<'a, Message, Renderer> Content<'a, Message, Renderer> +where + Renderer: pane_grid::Renderer, +{ + /// Creates a new [`Content`] with the provided body. + pub fn new(body: impl Into<Element<'a, Message, Renderer>>) -> Self { + Self { + title_bar: None, + body: body.into(), + style: Renderer::Style::default(), + } + } + + /// Sets the [`TitleBar`] of this [`Content`]. + pub fn title_bar( + mut self, + title_bar: TitleBar<'a, Message, Renderer>, + ) -> Self { + self.title_bar = Some(title_bar); + self + } + + /// Sets the style of the [`Content`]. + pub fn style(mut self, style: impl Into<Renderer::Style>) -> Self { + self.style = style.into(); + self + } +} + +impl<'a, Message, Renderer> Content<'a, Message, Renderer> +where + Renderer: pane_grid::Renderer, +{ + /// Draws the [`Content`] with the provided [`Renderer`] and [`Layout`]. /// - /// [`Pane`]: struct.Pane.html - Pane(T), + /// [`Renderer`]: crate::widget::pane_grid::Renderer + pub fn draw( + &self, + renderer: &mut Renderer, + defaults: &Renderer::Defaults, + layout: Layout<'_>, + cursor_position: Point, + ) -> Renderer::Output { + if let Some(title_bar) = &self.title_bar { + let mut children = layout.children(); + let title_bar_layout = children.next().unwrap(); + let body_layout = children.next().unwrap(); + + renderer.draw_pane( + defaults, + layout.bounds(), + &self.style, + Some((title_bar, title_bar_layout)), + (&self.body, body_layout), + cursor_position, + ) + } else { + renderer.draw_pane( + defaults, + layout.bounds(), + &self.style, + None, + (&self.body, layout), + cursor_position, + ) + } + } + + /// Returns whether the [`Content`] with the given [`Layout`] can be picked + /// at the provided cursor position. + pub fn can_be_picked_at( + &self, + layout: Layout<'_>, + cursor_position: Point, + ) -> bool { + if let Some(title_bar) = &self.title_bar { + let mut children = layout.children(); + let title_bar_layout = children.next().unwrap(); + + title_bar.is_over_pick_area(title_bar_layout, cursor_position) + } else { + false + } + } + + pub(crate) fn layout( + &self, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + if let Some(title_bar) = &self.title_bar { + let max_size = limits.max(); + + let title_bar_layout = title_bar + .layout(renderer, &layout::Limits::new(Size::ZERO, max_size)); + + let title_bar_size = title_bar_layout.size(); + + let mut body_layout = self.body.layout( + renderer, + &layout::Limits::new( + Size::ZERO, + Size::new( + max_size.width, + max_size.height - title_bar_size.height, + ), + ), + ); + + body_layout.move_to(Point::new(0.0, title_bar_size.height)); + + layout::Node::with_children( + max_size, + vec![title_bar_layout, body_layout], + ) + } else { + self.body.layout(renderer, limits) + } + } + + pub(crate) fn on_event( + &mut self, + event: Event, + layout: Layout<'_>, + cursor_position: Point, + messages: &mut Vec<Message>, + renderer: &Renderer, + clipboard: Option<&dyn Clipboard>, + ) -> event::Status { + let mut event_status = event::Status::Ignored; + + let body_layout = if let Some(title_bar) = &mut self.title_bar { + let mut children = layout.children(); + + event_status = title_bar.on_event( + event.clone(), + children.next().unwrap(), + cursor_position, + messages, + renderer, + clipboard, + ); + + children.next().unwrap() + } else { + layout + }; + + let body_status = self.body.on_event( + event, + body_layout, + cursor_position, + messages, + renderer, + clipboard, + ); + + event_status.merge(body_status) + } + + pub(crate) fn hash_layout(&self, state: &mut Hasher) { + if let Some(title_bar) = &self.title_bar { + title_bar.hash_layout(state); + } + + self.body.hash_layout(state); + } + + pub(crate) fn overlay( + &mut self, + layout: Layout<'_>, + ) -> Option<overlay::Element<'_, Message, Renderer>> { + let body_layout = if self.title_bar.is_some() { + let mut children = layout.children(); + + // Overlays only allowed in the pane body, for now at least. + let _title_bar_layout = children.next(); + + children.next()? + } else { + layout + }; + + self.body.overlay(body_layout) + } +} + +impl<'a, T, Message, Renderer> From<T> for Content<'a, Message, Renderer> +where + T: Into<Element<'a, Message, Renderer>>, + Renderer: pane_grid::Renderer + container::Renderer, +{ + fn from(element: T) -> Self { + Self::new(element) + } } diff --git a/native/src/widget/pane_grid/node.rs b/native/src/widget/pane_grid/node.rs index 723ec393..319936fc 100644 --- a/native/src/widget/pane_grid/node.rs +++ b/native/src/widget/pane_grid/node.rs @@ -7,17 +7,12 @@ use std::collections::HashMap; /// A layout node of a [`PaneGrid`]. /// -/// [`PaneGrid`]: struct.PaneGrid.html +/// [`PaneGrid`]: crate::widget::PaneGrid #[derive(Debug, Clone)] pub enum Node { /// The region of this [`Node`] is split into two. - /// - /// [`Node`]: enum.Node.html Split { /// The [`Split`] of this [`Node`]. - /// - /// [`Split`]: struct.Split.html - /// [`Node`]: enum.Node.html id: Split, /// The direction of the split. @@ -27,28 +22,40 @@ pub enum Node { ratio: f32, /// The left/top [`Node`] of the split. - /// - /// [`Node`]: enum.Node.html a: Box<Node>, /// The right/bottom [`Node`] of the split. - /// - /// [`Node`]: enum.Node.html b: Box<Node>, }, /// The region of this [`Node`] is taken by a [`Pane`]. - /// - /// [`Pane`]: struct.Pane.html Pane(Pane), } impl Node { + /// Returns an iterator over each [`Split`] in this [`Node`]. + pub fn splits(&self) -> impl Iterator<Item = &Split> { + let mut unvisited_nodes = vec![self]; + + std::iter::from_fn(move || { + while let Some(node) = unvisited_nodes.pop() { + match node { + Node::Split { id, a, b, .. } => { + unvisited_nodes.push(a); + unvisited_nodes.push(b); + + return Some(id); + } + _ => {} + } + } + + None + }) + } + /// Returns the rectangular region for each [`Pane`] in the [`Node`] given /// the spacing between panes and the total available space. - /// - /// [`Pane`]: struct.Pane.html - /// [`Node`]: enum.Node.html - pub fn regions( + pub fn pane_regions( &self, spacing: f32, size: Size, @@ -56,7 +63,7 @@ impl Node { let mut regions = HashMap::new(); self.compute_regions( - spacing / 2.0, + spacing, &Rectangle { x: 0.0, y: 0.0, @@ -72,10 +79,7 @@ impl Node { /// Returns the axis, rectangular region, and ratio for each [`Split`] in /// the [`Node`] given the spacing between panes and the total available /// space. - /// - /// [`Split`]: struct.Split.html - /// [`Node`]: enum.Node.html - pub fn splits( + pub fn split_regions( &self, spacing: f32, size: Size, @@ -83,7 +87,7 @@ impl Node { let mut splits = HashMap::new(); self.compute_splits( - spacing / 2.0, + spacing, &Rectangle { x: 0.0, y: 0.0, @@ -185,7 +189,7 @@ impl Node { fn compute_regions( &self, - halved_spacing: f32, + spacing: f32, current: &Rectangle, regions: &mut HashMap<Pane, Rectangle>, ) { @@ -193,11 +197,10 @@ impl Node { Node::Split { axis, ratio, a, b, .. } => { - let (region_a, region_b) = - axis.split(current, *ratio, halved_spacing); + let (region_a, region_b) = axis.split(current, *ratio, spacing); - a.compute_regions(halved_spacing, ®ion_a, regions); - b.compute_regions(halved_spacing, ®ion_b, regions); + a.compute_regions(spacing, ®ion_a, regions); + b.compute_regions(spacing, ®ion_b, regions); } Node::Pane(pane) => { let _ = regions.insert(*pane, *current); @@ -207,7 +210,7 @@ impl Node { fn compute_splits( &self, - halved_spacing: f32, + spacing: f32, current: &Rectangle, splits: &mut HashMap<Split, (Axis, Rectangle, f32)>, ) { @@ -219,13 +222,12 @@ impl Node { b, id, } => { - let (region_a, region_b) = - axis.split(current, *ratio, halved_spacing); + let (region_a, region_b) = axis.split(current, *ratio, spacing); let _ = splits.insert(*id, (*axis, *current, *ratio)); - a.compute_splits(halved_spacing, ®ion_a, splits); - b.compute_splits(halved_spacing, ®ion_b, splits); + a.compute_splits(spacing, ®ion_a, splits); + b.compute_splits(spacing, ®ion_b, splits); } Node::Pane(_) => {} } diff --git a/native/src/widget/pane_grid/pane.rs b/native/src/widget/pane_grid/pane.rs index f9866407..39d9f3ef 100644 --- a/native/src/widget/pane_grid/pane.rs +++ b/native/src/widget/pane_grid/pane.rs @@ -1,5 +1,5 @@ /// A rectangular region in a [`PaneGrid`] used to display widgets. /// -/// [`PaneGrid`]: struct.PaneGrid.html +/// [`PaneGrid`]: crate::widget::PaneGrid #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct Pane(pub(super) usize); diff --git a/native/src/widget/pane_grid/split.rs b/native/src/widget/pane_grid/split.rs index d020c510..16975abc 100644 --- a/native/src/widget/pane_grid/split.rs +++ b/native/src/widget/pane_grid/split.rs @@ -1,5 +1,5 @@ /// A divider that splits a region in a [`PaneGrid`] into two different panes. /// -/// [`PaneGrid`]: struct.PaneGrid.html +/// [`PaneGrid`]: crate::widget::PaneGrid #[derive(Debug, Clone, Copy, PartialEq, Eq, 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 4b13fb8e..666e1ca0 100644 --- a/native/src/widget/pane_grid/state.rs +++ b/native/src/widget/pane_grid/state.rs @@ -1,6 +1,5 @@ use crate::{ - keyboard, - pane_grid::{Axis, Content, Direction, Node, Pane, Split}, + pane_grid::{Axis, Configuration, Direction, Node, Pane, Split}, Hasher, Point, Rectangle, Size, }; @@ -16,32 +15,12 @@ use std::collections::HashMap; /// provided to the view function of [`PaneGrid::new`] for displaying each /// [`Pane`]. /// -/// [`PaneGrid`]: struct.PaneGrid.html -/// [`PaneGrid::new`]: struct.PaneGrid.html#method.new -/// [`Pane`]: struct.Pane.html -/// [`Split`]: struct.Split.html -/// [`State`]: struct.State.html +/// [`PaneGrid`]: crate::widget::PaneGrid +/// [`PaneGrid::new`]: crate::widget::PaneGrid::new #[derive(Debug, Clone)] pub struct State<T> { pub(super) panes: HashMap<Pane, T>, pub(super) internal: Internal, - pub(super) modifiers: keyboard::ModifiersState, -} - -/// The current focus of a [`Pane`]. -/// -/// [`Pane`]: struct.Pane.html -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum Focus { - /// The [`Pane`] is just focused. - /// - /// [`Pane`]: struct.Pane.html - Idle, - - /// The [`Pane`] is being dragged. - /// - /// [`Pane`]: struct.Pane.html - Dragging, } impl<T> State<T> { @@ -49,113 +28,70 @@ impl<T> State<T> { /// state. /// /// Alongside the [`State`], it returns the first [`Pane`] identifier. - /// - /// [`State`]: struct.State.html - /// [`Pane`]: struct.Pane.html pub fn new(first_pane_state: T) -> (Self, Pane) { - (Self::with_content(Content::Pane(first_pane_state)), Pane(0)) + ( + Self::with_configuration(Configuration::Pane(first_pane_state)), + Pane(0), + ) } - /// Creates a new [`State`] with the given [`Content`]. - /// - /// [`State`]: struct.State.html - /// [`Content`]: enum.Content.html - pub fn with_content(content: impl Into<Content<T>>) -> Self { + /// Creates a new [`State`] with the given [`Configuration`]. + pub fn with_configuration(config: impl Into<Configuration<T>>) -> Self { let mut panes = HashMap::new(); let (layout, last_id) = - Self::distribute_content(&mut panes, content.into(), 0); + Self::distribute_content(&mut panes, config.into(), 0); State { panes, internal: Internal { layout, last_id, - action: Action::Idle { focus: None }, + action: Action::Idle, }, - modifiers: keyboard::ModifiersState::default(), } } /// Returns the total amount of panes in the [`State`]. - /// - /// [`State`]: struct.State.html pub fn len(&self) -> usize { self.panes.len() } /// Returns the internal state of the given [`Pane`], if it exists. - /// - /// [`Pane`]: struct.Pane.html pub fn get(&self, pane: &Pane) -> Option<&T> { self.panes.get(pane) } /// Returns the internal state of the given [`Pane`] with mutability, if it /// exists. - /// - /// [`Pane`]: struct.Pane.html pub fn get_mut(&mut self, pane: &Pane) -> Option<&mut T> { self.panes.get_mut(pane) } /// Returns an iterator over all the panes of the [`State`], alongside its /// internal state. - /// - /// [`State`]: struct.State.html pub fn iter(&self) -> impl Iterator<Item = (&Pane, &T)> { self.panes.iter() } /// Returns a mutable iterator over all the panes of the [`State`], /// alongside its internal state. - /// - /// [`State`]: struct.State.html pub fn iter_mut(&mut self) -> impl Iterator<Item = (&Pane, &mut T)> { self.panes.iter_mut() } /// Returns the layout of the [`State`]. - /// - /// [`State`]: struct.State.html pub fn layout(&self) -> &Node { &self.internal.layout } - /// Returns the active [`Pane`] of the [`State`], if there is one. - /// - /// A [`Pane`] is active if it is focused and is __not__ being dragged. - /// - /// [`Pane`]: struct.Pane.html - /// [`State`]: struct.State.html - pub fn active(&self) -> Option<Pane> { - self.internal.active_pane() - } - /// Returns the adjacent [`Pane`] of another [`Pane`] in the given /// direction, if there is one. - /// - /// ## Example - /// You can combine this with [`State::active`] to find the pane that is - /// adjacent to the current active one, and then swap them. For instance: - /// - /// ``` - /// # use iced_native::pane_grid; - /// # - /// # let (mut state, _) = pane_grid::State::new(()); - /// # - /// if let Some(active) = state.active() { - /// if let Some(adjacent) = state.adjacent(&active, pane_grid::Direction::Right) { - /// state.swap(&active, &adjacent); - /// } - /// } - /// ``` - /// - /// [`Pane`]: struct.Pane.html - /// [`State::active`]: struct.State.html#method.active pub fn adjacent(&self, pane: &Pane, direction: Direction) -> Option<Pane> { - let regions = - self.internal.layout.regions(0.0, Size::new(4096.0, 4096.0)); + let regions = self + .internal + .layout + .pane_regions(0.0, Size::new(4096.0, 4096.0)); let current_region = regions.get(pane)?; @@ -184,18 +120,8 @@ impl<T> State<T> { Some(*pane) } - /// Focuses the given [`Pane`]. - /// - /// [`Pane`]: struct.Pane.html - pub fn focus(&mut self, pane: &Pane) { - self.internal.focus(pane); - } - /// Splits the given [`Pane`] into two in the given [`Axis`] and /// initializing the new [`Pane`] with the provided internal state. - /// - /// [`Pane`]: struct.Pane.html - /// [`Axis`]: enum.Axis.html pub fn split( &mut self, axis: Axis, @@ -219,7 +145,6 @@ impl<T> State<T> { node.split(new_split, axis, new_pane); let _ = self.panes.insert(new_pane, state); - self.focus(&new_pane); Some((new_pane, new_split)) } @@ -229,9 +154,8 @@ impl<T> State<T> { /// If you want to swap panes on drag and drop in your [`PaneGrid`], you /// will need to call this method when handling a [`DragEvent`]. /// - /// [`State`]: struct.State.html - /// [`PaneGrid`]: struct.PaneGrid.html - /// [`DragEvent`]: struct.DragEvent.html + /// [`PaneGrid`]: crate::widget::PaneGrid + /// [`DragEvent`]: crate::widget::pane_grid::DragEvent pub fn swap(&mut self, a: &Pane, b: &Pane) { self.internal.layout.update(&|node| match node { Node::Split { .. } => {} @@ -253,20 +177,17 @@ impl<T> State<T> { /// If you want to enable resize interactions in your [`PaneGrid`], you will /// need to call this method when handling a [`ResizeEvent`]. /// - /// [`Split`]: struct.Split.html - /// [`PaneGrid`]: struct.PaneGrid.html - /// [`ResizeEvent`]: struct.ResizeEvent.html + /// [`PaneGrid`]: crate::widget::PaneGrid + /// [`ResizeEvent`]: crate::widget::pane_grid::ResizeEvent pub fn resize(&mut self, split: &Split, ratio: f32) { let _ = self.internal.layout.resize(split, ratio); } - /// Closes the given [`Pane`] and returns its internal state, if it exists. - /// - /// [`Pane`]: struct.Pane.html - pub fn close(&mut self, pane: &Pane) -> Option<T> { + /// Closes the given [`Pane`] and returns its internal state and its closest + /// sibling, if it exists. + pub fn close(&mut self, pane: &Pane) -> Option<(T, Pane)> { if let Some(sibling) = self.internal.layout.remove(pane) { - self.focus(&sibling); - self.panes.remove(pane) + self.panes.remove(pane).map(|state| (state, sibling)) } else { None } @@ -274,11 +195,11 @@ impl<T> State<T> { fn distribute_content( panes: &mut HashMap<Pane, T>, - content: Content<T>, + content: Configuration<T>, next_id: usize, ) -> (Node, usize) { match content { - Content::Split { axis, ratio, a, b } => { + Configuration::Split { axis, ratio, a, b } => { let (a, next_id) = Self::distribute_content(panes, *a, next_id); let (b, next_id) = Self::distribute_content(panes, *b, next_id); @@ -293,7 +214,7 @@ impl<T> State<T> { next_id + 1, ) } - Content::Pane(state) => { + Configuration::Pane(state) => { let id = Pane(next_id); let _ = panes.insert(id, state); @@ -310,47 +231,17 @@ pub struct Internal { action: Action, } -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, PartialEq)] pub enum Action { - Idle { - focus: Option<Pane>, - }, - Dragging { - pane: Pane, - }, - Resizing { - split: Split, - axis: Axis, - focus: Option<Pane>, - }, -} - -impl Action { - pub fn focus(&self) -> Option<(Pane, Focus)> { - match self { - Action::Idle { focus } | Action::Resizing { focus, .. } => { - focus.map(|pane| (pane, Focus::Idle)) - } - Action::Dragging { pane } => Some((*pane, Focus::Dragging)), - } - } + Idle, + Dragging { pane: Pane, origin: Point }, + Resizing { split: Split, axis: Axis }, } impl Internal { - pub fn action(&self) -> Action { - self.action - } - - pub fn active_pane(&self) -> Option<Pane> { + pub fn picked_pane(&self) -> Option<(Pane, Point)> { match self.action { - Action::Idle { focus } => focus, - _ => None, - } - } - - pub fn picked_pane(&self) -> Option<Pane> { - match self.action { - Action::Dragging { pane } => Some(pane), + Action::Dragging { pane, origin, .. } => Some((pane, origin)), _ => None, } } @@ -362,28 +253,27 @@ impl Internal { } } - pub fn regions( + pub fn pane_regions( &self, spacing: f32, size: Size, ) -> HashMap<Pane, Rectangle> { - self.layout.regions(spacing, size) + self.layout.pane_regions(spacing, size) } - pub fn splits( + pub fn split_regions( &self, spacing: f32, size: Size, ) -> HashMap<Split, (Axis, Rectangle, f32)> { - self.layout.splits(spacing, size) + self.layout.split_regions(spacing, size) } - pub fn focus(&mut self, pane: &Pane) { - self.action = Action::Idle { focus: Some(*pane) }; - } - - pub fn pick_pane(&mut self, pane: &Pane) { - self.action = Action::Dragging { pane: *pane }; + pub fn pick_pane(&mut self, pane: &Pane, origin: Point) { + self.action = Action::Dragging { + pane: *pane, + origin, + }; } pub fn pick_split(&mut self, split: &Split, axis: Axis) { @@ -393,26 +283,14 @@ impl Internal { return; } - let focus = self.action.focus().map(|(pane, _)| pane); - self.action = Action::Resizing { split: *split, axis, - focus, }; } - pub fn drop_split(&mut self) { - match self.action { - Action::Resizing { focus, .. } => { - self.action = Action::Idle { focus }; - } - _ => {} - } - } - - pub fn unfocus(&mut self) { - self.action = Action::Idle { focus: None }; + pub fn idle(&mut self) { + self.action = Action::Idle; } pub fn hash_layout(&self, hasher: &mut Hasher) { diff --git a/native/src/widget/pane_grid/title_bar.rs b/native/src/widget/pane_grid/title_bar.rs new file mode 100644 index 00000000..475cb9ae --- /dev/null +++ b/native/src/widget/pane_grid/title_bar.rs @@ -0,0 +1,253 @@ +use crate::event::{self, Event}; +use crate::layout; +use crate::pane_grid; +use crate::{Clipboard, Element, Hasher, Layout, 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>, + controls: Option<Element<'a, Message, Renderer>>, + padding: u16, + always_show_controls: bool, + style: 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 { + Self { + title: title.into(), + title_size: None, + controls: None, + padding: 0, + always_show_controls: false, + style: Renderer::Style::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, + controls: impl Into<Element<'a, Message, Renderer>>, + ) -> Self { + self.controls = Some(controls.into()); + self + } + + /// Sets the padding of the [`TitleBar`]. + pub fn padding(mut self, units: u16) -> Self { + self.padding = units; + self + } + + /// Sets the style of the [`TitleBar`]. + pub fn style(mut self, style: impl Into<Renderer::Style>) -> Self { + self.style = style.into(); + self + } + + /// Sets whether or not the [`controls`] attached to this [`TitleBar`] are + /// always visible. + /// + /// By default, the controls are only visible when the [`Pane`] of this + /// [`TitleBar`] is hovered. + /// + /// [`controls`]: Self::controls + /// [`Pane`]: crate::widget::pane_grid::Pane + pub fn always_show_controls(mut self) -> Self { + self.always_show_controls = true; + self + } +} + +impl<'a, Message, Renderer> TitleBar<'a, Message, Renderer> +where + Renderer: pane_grid::Renderer, +{ + /// Draws the [`TitleBar`] with the provided [`Renderer`] and [`Layout`]. + /// + /// [`Renderer`]: crate::widget::pane_grid::Renderer + pub fn draw( + &self, + renderer: &mut Renderer, + defaults: &Renderer::Defaults, + layout: Layout<'_>, + cursor_position: Point, + 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 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, + ) + } 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, + ) + } + } + + /// Returns whether the mouse cursor is over the pick area of the + /// [`TitleBar`] or not. + /// + /// The whole [`TitleBar`] is a pick area, except its controls. + pub fn is_over_pick_area( + &self, + layout: Layout<'_>, + cursor_position: Point, + ) -> bool { + if layout.bounds().contains(cursor_position) { + let mut children = layout.children(); + let padded = 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) + } else { + true + } + } else { + false + } + } + + pub(crate) fn hash_layout(&self, hasher: &mut Hasher) { + use std::hash::Hash; + + self.title.hash(hasher); + self.title_size.hash(hasher); + self.padding.hash(hasher); + } + + pub(crate) fn layout( + &self, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + let padding = f32::from(self.padding); + let limits = limits.pad(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 mut node = if let Some(controls) = &self.controls { + let mut controls_layout = controls + .layout(renderer, &layout::Limits::new(Size::ZERO, max_size)); + + let controls_size = controls_layout.size(); + let space_before_controls = max_size.width - controls_size.width; + + let 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( + Size::new(max_size.width, height), + vec![title_layout, controls_layout], + ) + } else { + layout::Node::new(Size::new(max_size.width, title_height)) + }; + + node.move_to(Point::new(padding, padding)); + + layout::Node::with_children(node.size().pad(padding), vec![node]) + } + + pub(crate) fn on_event( + &mut self, + event: Event, + layout: Layout<'_>, + cursor_position: Point, + messages: &mut Vec<Message>, + renderer: &Renderer, + clipboard: Option<&dyn Clipboard>, + ) -> event::Status { + if let Some(controls) = &mut self.controls { + let mut children = layout.children(); + let padded = children.next().unwrap(); + + let mut children = padded.children(); + let _ = children.next(); + let controls_layout = children.next().unwrap(); + + controls.on_event( + event, + controls_layout, + cursor_position, + messages, + renderer, + clipboard, + ) + } else { + event::Status::Ignored + } + } +} diff --git a/native/src/widget/pick_list.rs b/native/src/widget/pick_list.rs new file mode 100644 index 00000000..58c0dfe1 --- /dev/null +++ b/native/src/widget/pick_list.rs @@ -0,0 +1,343 @@ +//! Display a dropdown list of selectable values. +use crate::event::{self, Event}; +use crate::layout; +use crate::mouse; +use crate::overlay; +use crate::overlay::menu::{self, Menu}; +use crate::scrollable; +use crate::text; +use crate::{ + Clipboard, Element, Hasher, Layout, Length, Point, Rectangle, Size, Widget, +}; +use std::borrow::Cow; + +/// A widget for selecting a single value from a list of options. +#[allow(missing_debug_implementations)] +pub struct PickList<'a, T, Message, Renderer: self::Renderer> +where + [T]: ToOwned<Owned = Vec<T>>, +{ + menu: &'a mut menu::State, + is_open: &'a mut bool, + hovered_option: &'a mut Option<usize>, + last_selection: &'a mut Option<T>, + on_selected: Box<dyn Fn(T) -> Message>, + options: Cow<'a, [T]>, + selected: Option<T>, + width: Length, + padding: u16, + text_size: Option<u16>, + font: Renderer::Font, + style: <Renderer as self::Renderer>::Style, +} + +/// The local state of a [`PickList`]. +#[derive(Debug, Clone)] +pub struct State<T> { + menu: menu::State, + is_open: bool, + hovered_option: Option<usize>, + last_selection: Option<T>, +} + +impl<T> Default for State<T> { + fn default() -> Self { + Self { + menu: menu::State::default(), + is_open: bool::default(), + hovered_option: Option::default(), + last_selection: Option::default(), + } + } +} + +impl<'a, T: 'a, Message, Renderer: self::Renderer> + PickList<'a, T, Message, Renderer> +where + T: ToString + Eq, + [T]: ToOwned<Owned = Vec<T>>, +{ + /// Creates a new [`PickList`] with the given [`State`], a list of options, + /// the current selected value, and the message to produce when an option is + /// selected. + pub fn new( + state: &'a mut State<T>, + options: impl Into<Cow<'a, [T]>>, + selected: Option<T>, + on_selected: impl Fn(T) -> Message + 'static, + ) -> Self { + let State { + menu, + is_open, + hovered_option, + last_selection, + } = state; + + Self { + menu, + is_open, + hovered_option, + last_selection, + on_selected: Box::new(on_selected), + options: options.into(), + selected, + width: Length::Shrink, + text_size: None, + padding: Renderer::DEFAULT_PADDING, + font: Default::default(), + style: Default::default(), + } + } + + /// 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; + self + } + + /// Sets the text size of the [`PickList`]. + pub fn text_size(mut self, size: u16) -> Self { + self.text_size = Some(size); + self + } + + /// Sets the font of the [`PickList`]. + pub fn font(mut self, font: Renderer::Font) -> Self { + self.font = font; + self + } + + /// Sets the style of the [`PickList`]. + pub fn style( + mut self, + style: impl Into<<Renderer as self::Renderer>::Style>, + ) -> Self { + self.style = style.into(); + self + } +} + +impl<'a, T: 'a, Message, Renderer> Widget<Message, Renderer> + for PickList<'a, T, Message, Renderer> +where + T: Clone + ToString + Eq, + [T]: ToOwned<Owned = Vec<T>>, + Message: 'static, + Renderer: self::Renderer + scrollable::Renderer + 'a, +{ + fn width(&self) -> Length { + Length::Shrink + } + + fn height(&self) -> Length { + Length::Shrink + } + + fn layout( + &self, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + use std::f32; + + let limits = limits + .width(self.width) + .height(Length::Shrink) + .pad(f32::from(self.padding)); + + let text_size = self.text_size.unwrap_or(renderer.default_size()); + + let max_width = match self.width { + Length::Shrink => { + 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) + } + _ => 0, + }; + + let size = { + let intrinsic = Size::new( + max_width as f32 + + f32::from(text_size) + + f32::from(self.padding), + f32::from(text_size), + ); + + limits.resolve(intrinsic).pad(f32::from(self.padding)) + }; + + layout::Node::new(size) + } + + fn hash_layout(&self, state: &mut Hasher) { + use std::hash::Hash as _; + + match self.width { + Length::Shrink => { + self.options + .iter() + .map(ToString::to_string) + .for_each(|label| label.hash(state)); + } + _ => { + self.width.hash(state); + } + } + } + + fn on_event( + &mut self, + event: Event, + layout: Layout<'_>, + cursor_position: Point, + messages: &mut Vec<Message>, + _renderer: &Renderer, + _clipboard: Option<&dyn Clipboard>, + ) -> event::Status { + match event { + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => { + let event_status = if *self.is_open { + // TODO: Encode cursor availability in the type system + *self.is_open = + cursor_position.x < 0.0 || cursor_position.y < 0.0; + + event::Status::Captured + } else if layout.bounds().contains(cursor_position) { + let selected = self.selected.as_ref(); + + *self.is_open = true; + *self.hovered_option = self + .options + .iter() + .position(|option| Some(option) == selected); + + event::Status::Captured + } else { + event::Status::Ignored + }; + + if let Some(last_selection) = self.last_selection.take() { + messages.push((self.on_selected)(last_selection)); + + *self.is_open = false; + + event::Status::Captured + } else { + event_status + } + } + _ => event::Status::Ignored, + } + } + + fn draw( + &self, + renderer: &mut Renderer, + _defaults: &Renderer::Defaults, + layout: Layout<'_>, + cursor_position: Point, + _viewport: &Rectangle, + ) -> Renderer::Output { + self::Renderer::draw( + renderer, + layout.bounds(), + cursor_position, + self.selected.as_ref().map(ToString::to_string), + self.padding, + self.text_size.unwrap_or(renderer.default_size()), + self.font, + &self.style, + ) + } + + fn overlay( + &mut self, + layout: Layout<'_>, + ) -> Option<overlay::Element<'_, Message, Renderer>> { + if *self.is_open { + let bounds = layout.bounds(); + + let mut menu = Menu::new( + &mut self.menu, + &self.options, + &mut self.hovered_option, + &mut self.last_selection, + ) + .width(bounds.width.round() as u16) + .padding(self.padding) + .font(self.font) + .style(Renderer::menu_style(&self.style)); + + if let Some(text_size) = self.text_size { + menu = menu.text_size(text_size); + } + + Some(menu.overlay(layout.position(), bounds.height)) + } else { + None + } + } +} + +/// The renderer of a [`PickList`]. +/// +/// Your [renderer] will need to implement this trait before being +/// able to use a [`PickList`] in your user interface. +/// +/// [renderer]: crate::renderer +pub trait Renderer: text::Renderer + menu::Renderer { + /// The default padding of a [`PickList`]. + const DEFAULT_PADDING: u16; + + /// The [`PickList`] style supported by this renderer. + type Style: Default; + + /// Returns the style of the [`Menu`] of the [`PickList`]. + fn menu_style( + style: &<Self as Renderer>::Style, + ) -> <Self as menu::Renderer>::Style; + + /// Draws a [`PickList`]. + fn draw( + &mut self, + bounds: Rectangle, + cursor_position: Point, + selected: Option<String>, + padding: u16, + text_size: u16, + font: Self::Font, + style: &<Self as Renderer>::Style, + ) -> Self::Output; +} + +impl<'a, T: 'a, Message, Renderer> Into<Element<'a, Message, Renderer>> + for PickList<'a, T, Message, Renderer> +where + T: Clone + ToString + Eq, + [T]: ToOwned<Owned = Vec<T>>, + Renderer: self::Renderer + 'a, + Message: 'static, +{ + fn into(self) -> Element<'a, Message, Renderer> { + Element::new(self) + } +} diff --git a/native/src/widget/progress_bar.rs b/native/src/widget/progress_bar.rs index 5ab76d47..d294f198 100644 --- a/native/src/widget/progress_bar.rs +++ b/native/src/widget/progress_bar.rs @@ -33,8 +33,6 @@ impl<Renderer: self::Renderer> ProgressBar<Renderer> { /// It expects: /// * an inclusive range of possible values /// * the current value of the [`ProgressBar`] - /// - /// [`ProgressBar`]: struct.ProgressBar.html pub fn new(range: RangeInclusive<f32>, value: f32) -> Self { ProgressBar { value: value.max(*range.start()).min(*range.end()), @@ -46,24 +44,18 @@ impl<Renderer: self::Renderer> ProgressBar<Renderer> { } /// Sets the width of the [`ProgressBar`]. - /// - /// [`ProgressBar`]: struct.ProgressBar.html pub fn width(mut self, width: Length) -> Self { self.width = width; self } /// Sets the height of the [`ProgressBar`]. - /// - /// [`ProgressBar`]: struct.ProgressBar.html pub fn height(mut self, height: Length) -> Self { self.height = Some(height); self } /// Sets the style of the [`ProgressBar`]. - /// - /// [`ProgressBar`]: struct.ProgressBar.html pub fn style(mut self, style: impl Into<Renderer::Style>) -> Self { self.style = style.into(); self @@ -104,6 +96,7 @@ where _defaults: &Renderer::Defaults, layout: Layout<'_>, _cursor_position: Point, + _viewport: &Rectangle, ) -> Renderer::Output { renderer.draw( layout.bounds(), @@ -127,15 +120,12 @@ where /// Your [renderer] will need to implement this trait before being /// able to use a [`ProgressBar`] in your user interface. /// -/// [`ProgressBar`]: struct.ProgressBar.html -/// [renderer]: ../../renderer/index.html +/// [renderer]: crate::renderer pub trait Renderer: crate::Renderer { /// The style supported by this renderer. type Style: Default; /// The default height of a [`ProgressBar`]. - /// - /// [`ProgressBar`]: struct.ProgressBar.html const DEFAULT_HEIGHT: u16; /// Draws a [`ProgressBar`]. @@ -146,8 +136,6 @@ pub trait Renderer: crate::Renderer { /// * the current value of the [`ProgressBar`] /// * maybe a specific background of the [`ProgressBar`] /// * maybe a specific active color of the [`ProgressBar`] - /// - /// [`ProgressBar`]: struct.ProgressBar.html fn draw( &self, bounds: Rectangle, diff --git a/native/src/widget/radio.rs b/native/src/widget/radio.rs index d07a9012..69952345 100644 --- a/native/src/widget/radio.rs +++ b/native/src/widget/radio.rs @@ -1,8 +1,13 @@ //! Create choices using radio buttons. +use crate::event::{self, Event}; +use crate::layout; +use crate::mouse; +use crate::row; +use crate::text; +use crate::touch; use crate::{ - layout, mouse, row, text, Align, Clipboard, Element, Event, Hasher, - HorizontalAlignment, Layout, Length, Point, Rectangle, Row, Text, - VerticalAlignment, Widget, + Align, Clipboard, Element, Hasher, HorizontalAlignment, Layout, Length, + Point, Rectangle, Row, Text, VerticalAlignment, Widget, }; use std::hash::Hash; @@ -41,12 +46,14 @@ pub struct Radio<Message, Renderer: self::Renderer + text::Renderer> { width: Length, size: u16, spacing: u16, - text_size: u16, + text_size: Option<u16>, style: Renderer::Style, } impl<Message, Renderer: self::Renderer + text::Renderer> Radio<Message, Renderer> +where + Message: Clone, { /// Creates a new [`Radio`] button. /// @@ -56,8 +63,6 @@ impl<Message, Renderer: self::Renderer + text::Renderer> /// * the current selected value /// * a function that will be called when the [`Radio`] is selected. It /// receives the value of the radio and must produce a `Message`. - /// - /// [`Radio`]: struct.Radio.html pub fn new<F, V>( value: V, label: impl Into<String>, @@ -75,46 +80,36 @@ impl<Message, Renderer: self::Renderer + text::Renderer> width: Length::Shrink, size: <Renderer as self::Renderer>::DEFAULT_SIZE, spacing: Renderer::DEFAULT_SPACING, //15 - text_size: <Renderer as text::Renderer>::DEFAULT_SIZE, + text_size: None, style: Renderer::Style::default(), } } /// Sets the size of the [`Radio`] button. - /// - /// [`Radio`]: struct.Radio.html pub fn size(mut self, size: u16) -> Self { self.size = size; self } /// Sets the width of the [`Radio`] button. - /// - /// [`Radio`]: struct.Radio.html pub fn width(mut self, width: Length) -> Self { self.width = width; self } /// Sets the spacing between the [`Radio`] button and the text. - /// - /// [`Radio`]: struct.Radio.html pub fn spacing(mut self, spacing: u16) -> Self { self.spacing = spacing; self } /// Sets the text size of the [`Radio`] button. - /// - /// [`Radio`]: struct.Radio.html pub fn text_size(mut self, text_size: u16) -> Self { - self.text_size = text_size; + self.text_size = Some(text_size); self } /// Sets the style of the [`Radio`] button. - /// - /// [`Radio`]: struct.Radio.html pub fn style(mut self, style: impl Into<Renderer::Style>) -> Self { self.style = style.into(); self @@ -123,8 +118,8 @@ impl<Message, Renderer: self::Renderer + text::Renderer> impl<Message, Renderer> Widget<Message, Renderer> for Radio<Message, Renderer> where - Renderer: self::Renderer + text::Renderer + row::Renderer, Message: Clone, + Renderer: self::Renderer + text::Renderer + row::Renderer, { fn width(&self) -> Length { self.width @@ -151,7 +146,7 @@ where .push( Text::new(&self.label) .width(self.width) - .size(self.text_size), + .size(self.text_size.unwrap_or(renderer.default_size())), ) .layout(renderer, limits) } @@ -164,15 +159,20 @@ where messages: &mut Vec<Message>, _renderer: &Renderer, _clipboard: Option<&dyn Clipboard>, - ) { + ) -> event::Status { match event { - Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => { + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerPressed { .. }) => { if layout.bounds().contains(cursor_position) { messages.push(self.on_click.clone()); + + return event::Status::Captured; } } _ => {} } + + event::Status::Ignored } fn draw( @@ -181,6 +181,7 @@ where defaults: &Renderer::Defaults, layout: Layout<'_>, cursor_position: Point, + _viewport: &Rectangle, ) -> Renderer::Output { let bounds = layout.bounds(); let mut children = layout.children(); @@ -194,7 +195,7 @@ where defaults, label_layout.bounds(), &self.label, - self.text_size, + self.text_size.unwrap_or(renderer.default_size()), Default::default(), None, HorizontalAlignment::Left, @@ -226,20 +227,15 @@ where /// Your [renderer] will need to implement this trait before being /// able to use a [`Radio`] button in your user interface. /// -/// [`Radio`]: struct.Radio.html -/// [renderer]: ../../renderer/index.html +/// [renderer]: crate::renderer pub trait Renderer: crate::Renderer { /// The style supported by this renderer. type Style: Default; /// The default size of a [`Radio`] button. - /// - /// [`Radio`]: struct.Radio.html const DEFAULT_SIZE: u16; /// The default spacing of a [`Radio`] button. - /// - /// [`Radio`]: struct.Radio.html const DEFAULT_SPACING: u16; /// Draws a [`Radio`] button. @@ -249,8 +245,6 @@ pub trait Renderer: crate::Renderer { /// * whether the [`Radio`] is selected or not /// * whether the mouse is over the [`Radio`] or not /// * the drawn label of the [`Radio`] - /// - /// [`Radio`]: struct.Radio.html fn draw( &mut self, bounds: Rectangle, @@ -264,8 +258,8 @@ pub trait Renderer: crate::Renderer { impl<'a, Message, Renderer> From<Radio<Message, Renderer>> for Element<'a, Message, Renderer> where - Renderer: 'a + self::Renderer + row::Renderer + text::Renderer, Message: 'a + Clone, + Renderer: 'a + self::Renderer + row::Renderer + text::Renderer, { fn from(radio: Radio<Message, Renderer>) -> Element<'a, Message, Renderer> { Element::new(radio) diff --git a/native/src/widget/row.rs b/native/src/widget/row.rs index 31f7472f..b71663bd 100644 --- a/native/src/widget/row.rs +++ b/native/src/widget/row.rs @@ -1,18 +1,15 @@ //! Distribute content horizontally. -use std::hash::Hash; - +use crate::event::{self, Event}; +use crate::layout; +use crate::overlay; use crate::{ - layout, Align, Clipboard, Element, Event, Hasher, Layout, Length, Point, - Widget, + Align, Clipboard, Element, Hasher, Layout, Length, Point, Rectangle, Widget, }; +use std::hash::Hash; use std::u32; /// A container that distributes its contents horizontally. -/// -/// A [`Row`] will try to fill the horizontal space of its container. -/// -/// [`Row`]: struct.Row.html #[allow(missing_debug_implementations)] pub struct Row<'a, Message, Renderer> { spacing: u16, @@ -27,15 +24,11 @@ pub struct Row<'a, Message, Renderer> { impl<'a, Message, Renderer> Row<'a, Message, Renderer> { /// Creates an empty [`Row`]. - /// - /// [`Row`]: struct.Row.html pub fn new() -> Self { Self::with_children(Vec::new()) } /// Creates a [`Row`] with the given elements. - /// - /// [`Row`]: struct.Row.html pub fn with_children( children: Vec<Element<'a, Message, Renderer>>, ) -> Self { @@ -62,57 +55,42 @@ impl<'a, Message, Renderer> Row<'a, Message, Renderer> { } /// Sets the padding of the [`Row`]. - /// - /// [`Row`]: struct.Row.html pub fn padding(mut self, units: u16) -> Self { self.padding = units; self } /// Sets the width of the [`Row`]. - /// - /// [`Row`]: struct.Row.html pub fn width(mut self, width: Length) -> Self { self.width = width; self } /// Sets the height of the [`Row`]. - /// - /// [`Row`]: struct.Row.html pub fn height(mut self, height: Length) -> Self { self.height = height; self } /// Sets the maximum width of the [`Row`]. - /// - /// [`Row`]: struct.Row.html pub fn max_width(mut self, max_width: u32) -> Self { self.max_width = max_width; self } /// Sets the maximum height of the [`Row`]. - /// - /// [`Row`]: struct.Row.html pub fn max_height(mut self, max_height: u32) -> Self { self.max_height = max_height; self } /// Sets the vertical alignment of the contents of the [`Row`] . - /// - /// [`Row`]: struct.Row.html pub fn align_items(mut self, align: Align) -> Self { self.align_items = align; self } /// Adds an [`Element`] to the [`Row`]. - /// - /// [`Element`]: ../struct.Element.html - /// [`Row`]: struct.Row.html pub fn push<E>(mut self, child: E) -> Self where E: Into<Element<'a, Message, Renderer>>, @@ -165,9 +143,11 @@ where messages: &mut Vec<Message>, renderer: &Renderer, clipboard: Option<&dyn Clipboard>, - ) { - self.children.iter_mut().zip(layout.children()).for_each( - |(child, layout)| { + ) -> event::Status { + self.children + .iter_mut() + .zip(layout.children()) + .map(|(child, layout)| { child.widget.on_event( event.clone(), layout, @@ -176,8 +156,8 @@ where renderer, clipboard, ) - }, - ); + }) + .fold(event::Status::Ignored, event::Status::merge) } fn draw( @@ -186,8 +166,15 @@ where defaults: &Renderer::Defaults, layout: Layout<'_>, cursor_position: Point, + viewport: &Rectangle, ) -> Renderer::Output { - renderer.draw(defaults, &self.children, layout, cursor_position) + renderer.draw( + defaults, + &self.children, + layout, + cursor_position, + viewport, + ) } fn hash_layout(&self, state: &mut Hasher) { @@ -200,12 +187,23 @@ where self.max_height.hash(state); self.align_items.hash(state); self.spacing.hash(state); - self.spacing.hash(state); + self.padding.hash(state); for child in &self.children { child.widget.hash_layout(state); } } + + fn overlay( + &mut self, + layout: Layout<'_>, + ) -> Option<overlay::Element<'_, Message, Renderer>> { + self.children + .iter_mut() + .zip(layout.children()) + .filter_map(|(child, layout)| child.widget.overlay(layout)) + .next() + } } /// The renderer of a [`Row`]. @@ -213,8 +211,7 @@ where /// Your [renderer] will need to implement this trait before being /// able to use a [`Row`] in your user interface. /// -/// [`Row`]: struct.Row.html -/// [renderer]: ../../renderer/index.html +/// [renderer]: crate::renderer pub trait Renderer: crate::Renderer + Sized { /// Draws a [`Row`]. /// @@ -222,15 +219,13 @@ pub trait Renderer: crate::Renderer + Sized { /// - the children of the [`Row`] /// - the [`Layout`] of the [`Row`] and its children /// - the cursor position - /// - /// [`Row`]: struct.Row.html - /// [`Layout`]: ../layout/struct.Layout.html fn draw<Message>( &mut self, defaults: &Self::Defaults, children: &[Element<'_, Message, Self>], layout: Layout<'_>, cursor_position: Point, + viewport: &Rectangle, ) -> Self::Output; } diff --git a/native/src/widget/rule.rs b/native/src/widget/rule.rs new file mode 100644 index 00000000..18c88658 --- /dev/null +++ b/native/src/widget/rule.rs @@ -0,0 +1,116 @@ +//! Display a horizontal or vertical rule for dividing content. + +use std::hash::Hash; + +use crate::{ + layout, Element, Hasher, Layout, Length, Point, Rectangle, Size, Widget, +}; + +/// Display a horizontal or vertical rule for dividing content. +#[derive(Debug, Copy, Clone)] +pub struct Rule<Renderer: self::Renderer> { + width: Length, + height: Length, + style: Renderer::Style, + is_horizontal: bool, +} + +impl<Renderer: self::Renderer> Rule<Renderer> { + /// Creates a horizontal [`Rule`] for dividing content by the given vertical spacing. + pub fn horizontal(spacing: u16) -> Self { + Rule { + width: Length::Fill, + height: Length::from(Length::Units(spacing)), + style: Renderer::Style::default(), + is_horizontal: true, + } + } + + /// Creates a vertical [`Rule`] for dividing content by the given horizontal spacing. + pub fn vertical(spacing: u16) -> Self { + Rule { + width: Length::from(Length::Units(spacing)), + height: Length::Fill, + style: Renderer::Style::default(), + is_horizontal: false, + } + } + + /// Sets the style of the [`Rule`]. + pub fn style(mut self, style: impl Into<Renderer::Style>) -> Self { + self.style = style.into(); + self + } +} + +impl<Message, Renderer> Widget<Message, Renderer> for Rule<Renderer> +where + Renderer: self::Renderer, +{ + fn width(&self) -> Length { + self.width + } + + fn height(&self) -> Length { + self.height + } + + fn layout( + &self, + _renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + let limits = limits.width(self.width).height(self.height); + + layout::Node::new(limits.resolve(Size::ZERO)) + } + + fn draw( + &self, + renderer: &mut Renderer, + _defaults: &Renderer::Defaults, + layout: Layout<'_>, + _cursor_position: Point, + _viewport: &Rectangle, + ) -> Renderer::Output { + renderer.draw(layout.bounds(), &self.style, self.is_horizontal) + } + + fn hash_layout(&self, state: &mut Hasher) { + struct Marker; + std::any::TypeId::of::<Marker>().hash(state); + + self.width.hash(state); + self.height.hash(state); + } +} + +/// The renderer of a [`Rule`]. +pub trait Renderer: crate::Renderer { + /// The style supported by this renderer. + type Style: Default; + + /// Draws a [`Rule`]. + /// + /// It receives: + /// * the bounds of the [`Rule`] + /// * the style of the [`Rule`] + /// * whether the [`Rule`] is horizontal (true) or vertical (false) + fn draw( + &mut self, + bounds: Rectangle, + style: &Self::Style, + is_horizontal: bool, + ) -> Self::Output; +} + +impl<'a, Message, Renderer> From<Rule<Renderer>> + for Element<'a, Message, Renderer> +where + Renderer: 'a + self::Renderer, + Message: 'a, +{ + fn from(rule: Rule<Renderer>) -> Element<'a, Message, Renderer> { + Element::new(rule) + } +} diff --git a/native/src/widget/scrollable.rs b/native/src/widget/scrollable.rs index 3c8e5e5b..18cdf169 100644 --- a/native/src/widget/scrollable.rs +++ b/native/src/widget/scrollable.rs @@ -1,7 +1,13 @@ //! Navigate an endless amount of content with a scrollbar. +use crate::column; +use crate::event::{self, Event}; +use crate::layout; +use crate::mouse; +use crate::overlay; +use crate::touch; use crate::{ - column, layout, mouse, Align, Clipboard, Column, Element, Event, Hasher, - Layout, Length, Point, Rectangle, Size, Widget, + Align, Clipboard, Column, Element, Hasher, Layout, Length, Point, + Rectangle, Size, Vector, Widget, }; use std::{f32, hash::Hash, u32}; @@ -13,20 +19,23 @@ pub struct Scrollable<'a, Message, Renderer: self::Renderer> { state: &'a mut State, height: Length, max_height: u32, + scrollbar_width: u16, + scrollbar_margin: u16, + scroller_width: u16, content: Column<'a, Message, Renderer>, style: Renderer::Style, } impl<'a, Message, Renderer: self::Renderer> Scrollable<'a, Message, Renderer> { /// Creates a new [`Scrollable`] with the given [`State`]. - /// - /// [`Scrollable`]: struct.Scrollable.html - /// [`State`]: struct.State.html pub fn new(state: &'a mut State) -> Self { Scrollable { state, height: Length::Shrink, max_height: u32::MAX, + scrollbar_width: 10, + scrollbar_margin: 0, + scroller_width: 10, content: Column::new(), style: Renderer::Style::default(), } @@ -43,64 +52,68 @@ impl<'a, Message, Renderer: self::Renderer> Scrollable<'a, Message, Renderer> { } /// Sets the padding of the [`Scrollable`]. - /// - /// [`Scrollable`]: struct.Scrollable.html pub fn padding(mut self, units: u16) -> Self { self.content = self.content.padding(units); self } /// Sets the width of the [`Scrollable`]. - /// - /// [`Scrollable`]: struct.Scrollable.html pub fn width(mut self, width: Length) -> Self { self.content = self.content.width(width); self } /// Sets the height of the [`Scrollable`]. - /// - /// [`Scrollable`]: struct.Scrollable.html pub fn height(mut self, height: Length) -> Self { self.height = height; self } /// Sets the maximum width of the [`Scrollable`]. - /// - /// [`Scrollable`]: struct.Scrollable.html pub fn max_width(mut self, max_width: u32) -> Self { self.content = self.content.max_width(max_width); self } /// Sets the maximum height of the [`Scrollable`] in pixels. - /// - /// [`Scrollable`]: struct.Scrollable.html pub fn max_height(mut self, max_height: u32) -> Self { self.max_height = max_height; self } /// Sets the horizontal alignment of the contents of the [`Scrollable`] . - /// - /// [`Scrollable`]: struct.Scrollable.html pub fn align_items(mut self, align_items: Align) -> Self { self.content = self.content.align_items(align_items); self } + /// Sets the scrollbar width of the [`Scrollable`] . + /// Silently enforces a minimum value of 1. + pub fn scrollbar_width(mut self, scrollbar_width: u16) -> Self { + self.scrollbar_width = scrollbar_width.max(1); + self + } + + /// Sets the scrollbar margin of the [`Scrollable`] . + pub fn scrollbar_margin(mut self, scrollbar_margin: u16) -> Self { + self.scrollbar_margin = scrollbar_margin; + self + } + + /// Sets the scroller width of the [`Scrollable`] . + /// 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 the style of the [`Scrollable`] . - /// - /// [`Scrollable`]: struct.Scrollable.html pub fn style(mut self, style: impl Into<Renderer::Style>) -> Self { self.style = style.into(); self } /// Adds an element to the [`Scrollable`]. - /// - /// [`Scrollable`]: struct.Scrollable.html pub fn push<E>(mut self, child: E) -> Self where E: Into<Element<'a, Message, Renderer>>, @@ -113,7 +126,7 @@ impl<'a, Message, Renderer: self::Renderer> Scrollable<'a, Message, Renderer> { impl<'a, Message, Renderer> Widget<Message, Renderer> for Scrollable<'a, Message, Renderer> where - Renderer: self::Renderer + column::Renderer, + Renderer: self::Renderer, { fn width(&self) -> Length { Widget::<Message, Renderer>::width(&self.content) @@ -152,14 +165,56 @@ where messages: &mut Vec<Message>, renderer: &Renderer, clipboard: Option<&dyn Clipboard>, - ) { + ) -> event::Status { let bounds = layout.bounds(); let is_mouse_over = bounds.contains(cursor_position); let content = layout.children().next().unwrap(); let content_bounds = content.bounds(); - // TODO: Event capture. Nested scrollables should capture scroll events. + let offset = self.state.offset(bounds, content_bounds); + let scrollbar = renderer.scrollbar( + bounds, + content_bounds, + offset, + self.scrollbar_width, + self.scrollbar_margin, + self.scroller_width, + ); + let is_mouse_over_scrollbar = scrollbar + .as_ref() + .map(|scrollbar| scrollbar.is_mouse_over(cursor_position)) + .unwrap_or(false); + + let event_status = { + let cursor_position = if is_mouse_over && !is_mouse_over_scrollbar { + Point::new( + cursor_position.x, + cursor_position.y + + self.state.offset(bounds, content_bounds) as f32, + ) + } else { + // TODO: Make `cursor_position` an `Option<Point>` so we can encode + // cursor availability. + // This will probably happen naturally once we add multi-window + // support. + Point::new(cursor_position.x, -1.0) + }; + + self.content.on_event( + event.clone(), + content, + cursor_position, + messages, + renderer, + clipboard, + ) + }; + + if let event::Status::Captured = event_status { + return event::Status::Captured; + } + if is_mouse_over { match event { Event::Mouse(mouse::Event::WheelScrolled { delta }) => { @@ -172,26 +227,57 @@ where self.state.scroll(y, bounds, content_bounds); } } + + 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); + } + } + touch::Event::FingerLifted { .. } + | touch::Event::FingerLost { .. } => { + self.state.scroll_box_touched_at = None; + } + } + + return event::Status::Captured; } _ => {} } } - let offset = self.state.offset(bounds, content_bounds); - let scrollbar = renderer.scrollbar(bounds, content_bounds, offset); - let is_mouse_over_scrollbar = scrollbar - .as_ref() - .map(|scrollbar| scrollbar.is_mouse_over(cursor_position)) - .unwrap_or(false); - if self.state.is_scroller_grabbed() { match event { Event::Mouse(mouse::Event::ButtonReleased( mouse::Button::Left, - )) => { + )) + | Event::Touch(touch::Event::FingerLifted { .. }) + | Event::Touch(touch::Event::FingerLost { .. }) => { self.state.scroller_grabbed_at = None; + + return event::Status::Captured; } - Event::Mouse(mouse::Event::CursorMoved { .. }) => { + Event::Mouse(mouse::Event::CursorMoved { .. }) + | Event::Touch(touch::Event::FingerMoved { .. }) => { if let (Some(scrollbar), Some(scroller_grabbed_at)) = (scrollbar, self.state.scroller_grabbed_at) { @@ -203,6 +289,8 @@ where bounds, content_bounds, ); + + return event::Status::Captured; } } _ => {} @@ -211,7 +299,8 @@ where match event { Event::Mouse(mouse::Event::ButtonPressed( mouse::Button::Left, - )) => { + )) + | Event::Touch(touch::Event::FingerPressed { .. }) => { if let Some(scrollbar) = scrollbar { if let Some(scroller_grabbed_at) = scrollbar.grab_scroller(cursor_position) @@ -227,6 +316,8 @@ where self.state.scroller_grabbed_at = Some(scroller_grabbed_at); + + return event::Status::Captured; } } } @@ -234,28 +325,7 @@ where } } - let cursor_position = if is_mouse_over && !is_mouse_over_scrollbar { - Point::new( - cursor_position.x, - cursor_position.y - + self.state.offset(bounds, content_bounds) as f32, - ) - } else { - // TODO: Make `cursor_position` an `Option<Point>` so we can encode - // cursor availability. - // This will probably happen naturally once we add multi-window - // support. - Point::new(cursor_position.x, -1.0) - }; - - self.content.on_event( - event, - content, - cursor_position, - messages, - renderer, - clipboard, - ) + event::Status::Ignored } fn draw( @@ -264,12 +334,20 @@ where defaults: &Renderer::Defaults, layout: Layout<'_>, cursor_position: Point, + _viewport: &Rectangle, ) -> Renderer::Output { let bounds = layout.bounds(); let content_layout = layout.children().next().unwrap(); let content_bounds = content_layout.bounds(); let offset = self.state.offset(bounds, content_bounds); - let scrollbar = renderer.scrollbar(bounds, content_bounds, offset); + let scrollbar = renderer.scrollbar( + bounds, + content_bounds, + offset, + self.scrollbar_width, + self.scrollbar_margin, + self.scroller_width, + ); let is_mouse_over = bounds.contains(cursor_position); let is_mouse_over_scrollbar = scrollbar @@ -289,6 +367,10 @@ where defaults, content_layout, cursor_position, + &Rectangle { + y: bounds.y + offset as f32, + ..bounds + }, ) }; @@ -315,30 +397,42 @@ where self.content.hash_layout(state) } + + fn overlay( + &mut self, + layout: Layout<'_>, + ) -> Option<overlay::Element<'_, Message, Renderer>> { + let Self { content, state, .. } = self; + + content + .overlay(layout.children().next().unwrap()) + .map(|overlay| { + let bounds = layout.bounds(); + let content_layout = layout.children().next().unwrap(); + let content_bounds = content_layout.bounds(); + let offset = state.offset(bounds, content_bounds); + + overlay.translate(Vector::new(0.0, -(offset as f32))) + }) + } } /// The local state of a [`Scrollable`]. -/// -/// [`Scrollable`]: struct.Scrollable.html #[derive(Debug, Clone, Copy, Default)] pub struct State { scroller_grabbed_at: Option<f32>, + scroll_box_touched_at: Option<Point>, offset: f32, } impl State { /// Creates a new [`State`] with the scrollbar located at the top. - /// - /// [`State`]: struct.State.html pub fn new() -> Self { State::default() } /// Apply a scrolling offset to the current [`State`], given the bounds of /// the [`Scrollable`] and its contents. - /// - /// [`Scrollable`]: struct.Scrollable.html - /// [`State`]: struct.State.html pub fn scroll( &mut self, delta_y: f32, @@ -359,9 +453,6 @@ impl State { /// /// `0` represents scrollbar at the top, while `1` represents scrollbar at /// the bottom. - /// - /// [`Scrollable`]: struct.Scrollable.html - /// [`State`]: struct.State.html pub fn scroll_to( &mut self, percentage: f32, @@ -374,9 +465,6 @@ impl State { /// Returns the current scrolling offset of the [`State`], given the bounds /// of the [`Scrollable`] and its contents. - /// - /// [`Scrollable`]: struct.Scrollable.html - /// [`State`]: struct.State.html pub fn offset(&self, bounds: Rectangle, content_bounds: Rectangle) -> u32 { let hidden_content = (content_bounds.height - bounds.height).max(0.0).round() as u32; @@ -388,31 +476,37 @@ impl State { pub fn is_scroller_grabbed(&self) -> bool { self.scroller_grabbed_at.is_some() } + + /// Returns whether the scroll box is currently touched or not. + pub fn is_scroll_box_touched(&self) -> bool { + self.scroll_box_touched_at.is_some() + } } /// The scrollbar of a [`Scrollable`]. -/// -/// [`Scrollable`]: struct.Scrollable.html #[derive(Debug)] pub struct Scrollbar { + /// The outer bounds of the scrollable, including the [`Scrollbar`] and + /// [`Scroller`]. + pub outer_bounds: Rectangle, + /// The bounds of the [`Scrollbar`]. - /// - /// [`Scrollbar`]: struct.Scrollbar.html pub bounds: Rectangle, + /// The margin within the [`Scrollbar`]. + pub margin: u16, + /// The bounds of the [`Scroller`]. - /// - /// [`Scroller`]: struct.Scroller.html pub scroller: Scroller, } impl Scrollbar { fn is_mouse_over(&self, cursor_position: Point) -> bool { - self.bounds.contains(cursor_position) + self.outer_bounds.contains(cursor_position) } fn grab_scroller(&self, cursor_position: Point) -> Option<f32> { - if self.bounds.contains(cursor_position) { + if self.outer_bounds.contains(cursor_position) { Some(if self.scroller.bounds.contains(cursor_position) { (cursor_position.y - self.scroller.bounds.y) / self.scroller.bounds.height @@ -437,13 +531,9 @@ impl Scrollbar { } /// The handle of a [`Scrollbar`]. -/// -/// [`Scrollbar`]: struct.Scrollbar.html #[derive(Debug, Clone, Copy)] pub struct Scroller { /// The bounds of the [`Scroller`]. - /// - /// [`Scroller`]: struct.Scrollbar.html pub bounds: Rectangle, } @@ -452,22 +542,21 @@ pub struct Scroller { /// Your [renderer] will need to implement this trait before being /// able to use a [`Scrollable`] in your user interface. /// -/// [`Scrollable`]: struct.Scrollable.html -/// [renderer]: ../../renderer/index.html -pub trait Renderer: crate::Renderer + Sized { +/// [renderer]: crate::renderer +pub trait Renderer: column::Renderer + Sized { /// The style supported by this renderer. type Style: Default; /// Returns the [`Scrollbar`] given the bounds and content bounds of a /// [`Scrollable`]. - /// - /// [`Scrollbar`]: struct.Scrollbar.html - /// [`Scrollable`]: struct.Scrollable.html fn scrollbar( &self, bounds: Rectangle, content_bounds: Rectangle, offset: u32, + scrollbar_width: u16, + scrollbar_margin: u16, + scroller_width: u16, ) -> Option<Scrollbar>; /// Draws the [`Scrollable`]. @@ -481,10 +570,6 @@ pub trait Renderer: crate::Renderer + Sized { /// - a optional [`Scrollbar`] to be rendered /// - the scrolling offset /// - the drawn content - /// - /// [`Scrollbar`]: struct.Scrollbar.html - /// [`Scrollable`]: struct.Scrollable.html - /// [`State`]: struct.State.html fn draw( &mut self, scrollable: &State, @@ -502,7 +587,7 @@ pub trait Renderer: crate::Renderer + Sized { impl<'a, Message, Renderer> From<Scrollable<'a, Message, Renderer>> for Element<'a, Message, Renderer> where - Renderer: 'a + self::Renderer + column::Renderer, + Renderer: 'a + self::Renderer, Message: 'a, { fn from( diff --git a/native/src/widget/slider.rs b/native/src/widget/slider.rs index 8cdfc3de..010c6e53 100644 --- a/native/src/widget/slider.rs +++ b/native/src/widget/slider.rs @@ -1,12 +1,12 @@ //! Display an interactive selector of a single value from a range of values. //! //! A [`Slider`] has some local [`State`]. -//! -//! [`Slider`]: struct.Slider.html -//! [`State`]: struct.State.html +use crate::event::{self, Event}; +use crate::layout; +use crate::mouse; +use crate::touch; use crate::{ - layout, mouse, Clipboard, Element, Event, Hasher, Layout, Length, Point, - Rectangle, Size, Widget, + Clipboard, Element, Hasher, Layout, Length, Point, Rectangle, Size, Widget, }; use std::{hash::Hash, ops::RangeInclusive}; @@ -16,13 +16,15 @@ use std::{hash::Hash, ops::RangeInclusive}; /// /// A [`Slider`] will try to fill the horizontal space of its container. /// -/// [`Slider`]: struct.Slider.html +/// The [`Slider`] range of numeric values is generic and its step size defaults +/// to 1 unit. /// /// # Example /// ``` /// # use iced_native::{slider, renderer::Null}; /// # -/// # pub type Slider<'a, Message> = iced_native::Slider<'a, Message, Null>; +/// # pub type Slider<'a, T, Message> = iced_native::Slider<'a, T, Message, Null>; +/// #[derive(Clone)] /// pub enum Message { /// SliderChanged(f32), /// } @@ -35,16 +37,24 @@ use std::{hash::Hash, ops::RangeInclusive}; /// ///  #[allow(missing_debug_implementations)] -pub struct Slider<'a, Message, Renderer: self::Renderer> { +pub struct Slider<'a, T, Message, Renderer: self::Renderer> { state: &'a mut State, - range: RangeInclusive<f32>, - value: f32, - on_change: Box<dyn Fn(f32) -> Message>, + range: RangeInclusive<T>, + step: T, + value: T, + on_change: Box<dyn Fn(T) -> Message>, + on_release: Option<Message>, width: Length, + height: u16, style: Renderer::Style, } -impl<'a, Message, Renderer: self::Renderer> Slider<'a, Message, Renderer> { +impl<'a, T, Message, Renderer> Slider<'a, T, Message, Renderer> +where + T: Copy + From<u8> + std::cmp::PartialOrd, + Message: Clone, + Renderer: self::Renderer, +{ /// Creates a new [`Slider`]. /// /// It expects: @@ -54,48 +64,77 @@ impl<'a, Message, Renderer: self::Renderer> Slider<'a, Message, Renderer> { /// * a function that will be called when the [`Slider`] is dragged. /// It receives the new value of the [`Slider`] and must produce a /// `Message`. - /// - /// [`Slider`]: struct.Slider.html - /// [`State`]: struct.State.html pub fn new<F>( state: &'a mut State, - range: RangeInclusive<f32>, - value: f32, + range: RangeInclusive<T>, + value: T, on_change: F, ) -> Self where - F: 'static + Fn(f32) -> Message, + F: 'static + Fn(T) -> Message, { + let value = if value >= *range.start() { + value + } else { + *range.start() + }; + + let value = if value <= *range.end() { + value + } else { + *range.end() + }; + Slider { state, - value: value.max(*range.start()).min(*range.end()), + value, range, + step: T::from(1), on_change: Box::new(on_change), + on_release: None, width: Length::Fill, + height: Renderer::DEFAULT_HEIGHT, style: Renderer::Style::default(), } } - /// Sets the width of the [`Slider`]. + /// Sets the release message of the [`Slider`]. + /// This is called when the mouse is released from the slider. /// - /// [`Slider`]: struct.Slider.html + /// Typically, the user's interaction with the slider is finished when this message is produced. + /// This is useful if you need to spawn a long-running task from the slider's result, where + /// the default on_change message could create too many events. + pub fn on_release(mut self, on_release: Message) -> Self { + self.on_release = Some(on_release); + self + } + + /// Sets the width of the [`Slider`]. pub fn width(mut self, width: Length) -> Self { self.width = width; self } + /// Sets the height of the [`Slider`]. + pub fn height(mut self, height: u16) -> Self { + self.height = height; + self + } + /// Sets the style of the [`Slider`]. - /// - /// [`Slider`]: struct.Slider.html pub fn style(mut self, style: impl Into<Renderer::Style>) -> Self { self.style = style.into(); self } + + /// Sets the step size of the [`Slider`]. + pub fn step(mut self, step: T) -> Self { + self.step = step; + self + } } /// The local state of a [`Slider`]. -/// -/// [`Slider`]: struct.Slider.html #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub struct State { is_dragging: bool, @@ -103,16 +142,16 @@ pub struct State { impl State { /// Creates a new [`State`]. - /// - /// [`State`]: struct.State.html pub fn new() -> State { State::default() } } -impl<'a, Message, Renderer> Widget<Message, Renderer> - for Slider<'a, Message, Renderer> +impl<'a, T, Message, Renderer> Widget<Message, Renderer> + for Slider<'a, T, Message, Renderer> where + T: Copy + Into<f64> + num_traits::FromPrimitive, + Message: Clone, Renderer: self::Renderer, { fn width(&self) -> Length { @@ -125,12 +164,11 @@ where fn layout( &self, - renderer: &Renderer, + _renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { - let limits = limits - .width(self.width) - .height(Length::Units(renderer.height() as u16)); + let limits = + limits.width(self.width).height(Length::Units(self.height)); let size = limits.resolve(Size::ZERO); @@ -145,43 +183,64 @@ where messages: &mut Vec<Message>, _renderer: &Renderer, _clipboard: Option<&dyn Clipboard>, - ) { + ) -> event::Status { let mut change = || { let bounds = layout.bounds(); - if cursor_position.x <= bounds.x { messages.push((self.on_change)(*self.range.start())); } else if cursor_position.x >= bounds.x + bounds.width { messages.push((self.on_change)(*self.range.end())); } else { - let percent = (cursor_position.x - bounds.x) / bounds.width; - let value = (self.range.end() - self.range.start()) * percent - + self.range.start(); + let step = self.step.into(); + let start = (*self.range.start()).into(); + let end = (*self.range.end()).into(); + + let percent = f64::from(cursor_position.x - bounds.x) + / f64::from(bounds.width); - messages.push((self.on_change)(value)); + let steps = (percent * (end - start) / step).round(); + let value = steps * step + start; + + if let Some(value) = T::from_f64(value) { + messages.push((self.on_change)(value)); + } } }; match event { - Event::Mouse(mouse_event) => match mouse_event { - mouse::Event::ButtonPressed(mouse::Button::Left) => { - if layout.bounds().contains(cursor_position) { - change(); - self.state.is_dragging = true; - } + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerPressed { .. }) => { + if layout.bounds().contains(cursor_position) { + change(); + self.state.is_dragging = true; + + return event::Status::Captured; } - mouse::Event::ButtonReleased(mouse::Button::Left) => { + } + Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerLifted { .. }) + | Event::Touch(touch::Event::FingerLost { .. }) => { + if self.state.is_dragging { + if let Some(on_release) = self.on_release.clone() { + messages.push(on_release); + } self.state.is_dragging = false; + + return event::Status::Captured; } - mouse::Event::CursorMoved { .. } => { - if self.state.is_dragging { - change(); - } + } + Event::Mouse(mouse::Event::CursorMoved { .. }) + | Event::Touch(touch::Event::FingerMoved { .. }) => { + if self.state.is_dragging { + change(); + + return event::Status::Captured; } - _ => {} - }, + } _ => {} } + + event::Status::Ignored } fn draw( @@ -190,12 +249,16 @@ where _defaults: &Renderer::Defaults, layout: Layout<'_>, cursor_position: Point, + _viewport: &Rectangle, ) -> Renderer::Output { + let start = *self.range.start(); + let end = *self.range.end(); + renderer.draw( layout.bounds(), cursor_position, - self.range.clone(), - self.value, + start.into() as f32..=end.into() as f32, + self.value.into() as f32, self.state.is_dragging, &self.style, ) @@ -214,16 +277,13 @@ where /// Your [renderer] will need to implement this trait before being /// able to use a [`Slider`] in your user interface. /// -/// [`Slider`]: struct.Slider.html -/// [renderer]: ../../renderer/index.html +/// [renderer]: crate::renderer pub trait Renderer: crate::Renderer { /// The style supported by this renderer. type Style: Default; - /// Returns the height of the [`Slider`]. - /// - /// [`Slider`]: struct.Slider.html - fn height(&self) -> u32; + /// The default height of a [`Slider`]. + const DEFAULT_HEIGHT: u16; /// Draws a [`Slider`]. /// @@ -233,10 +293,6 @@ pub trait Renderer: crate::Renderer { /// * the local state of the [`Slider`] /// * the range of values of the [`Slider`] /// * the current value of the [`Slider`] - /// - /// [`Slider`]: struct.Slider.html - /// [`State`]: struct.State.html - /// [`Class`]: enum.Class.html fn draw( &mut self, bounds: Rectangle, @@ -248,14 +304,15 @@ pub trait Renderer: crate::Renderer { ) -> Self::Output; } -impl<'a, Message, Renderer> From<Slider<'a, Message, Renderer>> +impl<'a, T, Message, Renderer> From<Slider<'a, T, Message, Renderer>> for Element<'a, Message, Renderer> where + T: 'a + Copy + Into<f64> + num_traits::FromPrimitive, + Message: 'a + Clone, Renderer: 'a + self::Renderer, - Message: 'a, { fn from( - slider: Slider<'a, Message, Renderer>, + slider: Slider<'a, T, Message, Renderer>, ) -> Element<'a, Message, Renderer> { Element::new(slider) } diff --git a/native/src/widget/space.rs b/native/src/widget/space.rs index e56a8fe1..6b34ece8 100644 --- a/native/src/widget/space.rs +++ b/native/src/widget/space.rs @@ -16,15 +16,11 @@ pub struct Space { impl Space { /// Creates an amount of empty [`Space`] with the given width and height. - /// - /// [`Space`]: struct.Space.html pub fn new(width: Length, height: Length) -> Self { Space { width, height } } /// Creates an amount of horizontal [`Space`]. - /// - /// [`Space`]: struct.Space.html pub fn with_width(width: Length) -> Self { Space { width, @@ -33,8 +29,6 @@ impl Space { } /// Creates an amount of vertical [`Space`]. - /// - /// [`Space`]: struct.Space.html pub fn with_height(height: Length) -> Self { Space { width: Length::Shrink, @@ -43,7 +37,7 @@ impl Space { } } -impl<'a, Message, Renderer> Widget<Message, Renderer> for Space +impl<Message, Renderer> Widget<Message, Renderer> for Space where Renderer: self::Renderer, { @@ -71,6 +65,7 @@ where _defaults: &Renderer::Defaults, layout: Layout<'_>, _cursor_position: Point, + _viewport: &Rectangle, ) -> Renderer::Output { renderer.draw(layout.bounds()) } @@ -84,14 +79,10 @@ where } /// The renderer of an amount of [`Space`]. -/// -/// [`Space`]: struct.Space.html pub trait Renderer: crate::Renderer { /// Draws an amount of empty [`Space`]. /// /// You should most likely return an empty primitive here. - /// - /// [`Space`]: struct.Space.html fn draw(&mut self, bounds: Rectangle) -> Self::Output; } diff --git a/native/src/widget/svg.rs b/native/src/widget/svg.rs index 114d5e41..9cd61918 100644 --- a/native/src/widget/svg.rs +++ b/native/src/widget/svg.rs @@ -1,5 +1,6 @@ //! Display vector graphics in your application. -use crate::{layout, Element, Hasher, Layout, Length, Point, Size, Widget}; +use crate::layout; +use crate::{Element, Hasher, Layout, Length, Point, Rectangle, Size, Widget}; use std::{ hash::{Hash, Hasher as _}, @@ -13,8 +14,6 @@ use std::{ /// /// [`Svg`] images can have a considerable rendering cost when resized, /// specially when they are complex. -/// -/// [`Svg`]: struct.Svg.html #[derive(Debug, Clone)] pub struct Svg { handle: Handle, @@ -24,9 +23,6 @@ pub struct Svg { impl Svg { /// Creates a new [`Svg`] from the given [`Handle`]. - /// - /// [`Svg`]: struct.Svg.html - /// [`Handle`]: struct.Handle.html pub fn new(handle: impl Into<Handle>) -> Self { Svg { handle: handle.into(), @@ -37,23 +33,17 @@ impl Svg { /// Creates a new [`Svg`] that will display the contents of the file at the /// provided path. - /// - /// [`Svg`]: struct.Svg.html pub fn from_path(path: impl Into<PathBuf>) -> Self { Self::new(Handle::from_path(path)) } /// Sets the width of the [`Svg`]. - /// - /// [`Svg`]: struct.Svg.html pub fn width(mut self, width: Length) -> Self { self.width = width; self } /// Sets the height of the [`Svg`]. - /// - /// [`Svg`]: struct.Svg.html pub fn height(mut self, height: Length) -> Self { self.height = height; self @@ -103,6 +93,7 @@ where _defaults: &Renderer::Defaults, layout: Layout<'_>, _cursor_position: Point, + _viewport: &Rectangle, ) -> Renderer::Output { renderer.draw(self.handle.clone(), layout) } @@ -117,8 +108,6 @@ where } /// An [`Svg`] handle. -/// -/// [`Svg`]: struct.Svg.html #[derive(Debug, Clone)] pub struct Handle { id: u64, @@ -128,8 +117,6 @@ pub struct Handle { impl Handle { /// Creates an SVG [`Handle`] pointing to the vector image of the given /// path. - /// - /// [`Handle`]: struct.Handle.html pub fn from_path(path: impl Into<PathBuf>) -> Handle { Self::from_data(Data::Path(path.into())) } @@ -139,8 +126,6 @@ impl Handle { /// /// This is useful if you already have your SVG data in-memory, maybe /// because you downloaded or generated it procedurally. - /// - /// [`Handle`]: struct.Handle.html pub fn from_memory(bytes: impl Into<Vec<u8>>) -> Handle { Self::from_data(Data::Bytes(bytes.into())) } @@ -156,15 +141,11 @@ impl Handle { } /// Returns the unique identifier of the [`Handle`]. - /// - /// [`Handle`]: struct.Handle.html pub fn id(&self) -> u64 { self.id } /// Returns a reference to the SVG [`Data`]. - /// - /// [`Data`]: enum.Data.html pub fn data(&self) -> &Data { &self.data } @@ -177,8 +158,6 @@ impl Hash for Handle { } /// The data of an [`Svg`]. -/// -/// [`Svg`]: struct.Svg.html #[derive(Clone, Hash)] pub enum Data { /// File data @@ -204,18 +183,12 @@ impl std::fmt::Debug for Data { /// Your [renderer] will need to implement this trait before being able to use /// an [`Svg`] in your user interface. /// -/// [`Svg`]: struct.Svg.html -/// [renderer]: ../../renderer/index.html +/// [renderer]: crate::renderer pub trait Renderer: crate::Renderer { /// Returns the default dimensions of an [`Svg`] for the given [`Handle`]. - /// - /// [`Svg`]: struct.Svg.html - /// [`Handle`]: struct.Handle.html fn dimensions(&self, handle: &Handle) -> (u32, u32); /// Draws an [`Svg`]. - /// - /// [`Svg`]: struct.Svg.html fn draw(&mut self, handle: Handle, layout: Layout<'_>) -> Self::Output; } diff --git a/native/src/widget/text.rs b/native/src/widget/text.rs index d60aa468..6cc18e6c 100644 --- a/native/src/widget/text.rs +++ b/native/src/widget/text.rs @@ -19,7 +19,7 @@ use std::hash::Hash; /// ``` /// ///  -#[derive(Debug, Clone)] +#[derive(Debug)] pub struct Text<Renderer: self::Renderer> { content: String, size: Option<u16>, @@ -33,8 +33,6 @@ pub struct Text<Renderer: self::Renderer> { impl<Renderer: self::Renderer> Text<Renderer> { /// Create a new fragment of [`Text`] with the given contents. - /// - /// [`Text`]: struct.Text.html pub fn new<T: Into<String>>(label: T) -> Self { Text { content: label.into(), @@ -49,17 +47,12 @@ impl<Renderer: self::Renderer> Text<Renderer> { } /// Sets the size of the [`Text`]. - /// - /// [`Text`]: struct.Text.html pub fn size(mut self, size: u16) -> Self { self.size = Some(size); self } /// Sets the [`Color`] of the [`Text`]. - /// - /// [`Text`]: struct.Text.html - /// [`Color`]: ../../struct.Color.html pub fn color<C: Into<Color>>(mut self, color: C) -> Self { self.color = Some(color.into()); self @@ -67,33 +60,25 @@ impl<Renderer: self::Renderer> Text<Renderer> { /// Sets the [`Font`] of the [`Text`]. /// - /// [`Text`]: struct.Text.html - /// [`Font`]: ../../struct.Font.html + /// [`Font`]: Renderer::Font pub fn font(mut self, font: impl Into<Renderer::Font>) -> Self { self.font = font.into(); self } /// Sets the width of the [`Text`] boundaries. - /// - /// [`Text`]: struct.Text.html pub fn width(mut self, width: Length) -> Self { self.width = width; self } /// Sets the height of the [`Text`] boundaries. - /// - /// [`Text`]: struct.Text.html pub fn height(mut self, height: Length) -> Self { self.height = height; self } /// Sets the [`HorizontalAlignment`] of the [`Text`]. - /// - /// [`Text`]: struct.Text.html - /// [`HorizontalAlignment`]: enum.HorizontalAlignment.html pub fn horizontal_alignment( mut self, alignment: HorizontalAlignment, @@ -103,9 +88,6 @@ impl<Renderer: self::Renderer> Text<Renderer> { } /// Sets the [`VerticalAlignment`] of the [`Text`]. - /// - /// [`Text`]: struct.Text.html - /// [`VerticalAlignment`]: enum.VerticalAlignment.html pub fn vertical_alignment(mut self, alignment: VerticalAlignment) -> Self { self.vertical_alignment = alignment; self @@ -131,7 +113,7 @@ where ) -> layout::Node { let limits = limits.width(self.width).height(self.height); - let size = self.size.unwrap_or(Renderer::DEFAULT_SIZE); + let size = self.size.unwrap_or(renderer.default_size()); let bounds = limits.max(); @@ -149,12 +131,13 @@ where defaults: &Renderer::Defaults, layout: Layout<'_>, _cursor_position: Point, + _viewport: &Rectangle, ) -> Renderer::Output { renderer.draw( defaults, layout.bounds(), &self.content, - self.size.unwrap_or(Renderer::DEFAULT_SIZE), + self.size.unwrap_or(renderer.default_size()), self.font, self.color, self.horizontal_alignment, @@ -176,26 +159,18 @@ where /// The renderer of a [`Text`] fragment. /// /// Your [renderer] will need to implement this trait before being -/// able to use [`Text`] in your [`UserInterface`]. +/// able to use [`Text`] in your user interface. /// -/// [`Text`]: struct.Text.html -/// [renderer]: ../../renderer/index.html -/// [`UserInterface`]: ../../struct.UserInterface.html +/// [renderer]: crate::Renderer pub trait Renderer: crate::Renderer { /// The font type used for [`Text`]. - /// - /// [`Text`]: struct.Text.html type Font: Default + Copy; - /// The default size of [`Text`]. - /// - /// [`Text`]: struct.Text.html - const DEFAULT_SIZE: u16; + /// Returns the default size of [`Text`]. + fn default_size(&self) -> u16; /// Measures the [`Text`] in the given bounds and returns the minimum /// boundaries that can fit the contents. - /// - /// [`Text`]: struct.Text.html fn measure( &self, content: &str, @@ -213,10 +188,6 @@ pub trait Renderer: crate::Renderer { /// * the color of the [`Text`] /// * the [`HorizontalAlignment`] of the [`Text`] /// * the [`VerticalAlignment`] of the [`Text`] - /// - /// [`Text`]: struct.Text.html - /// [`HorizontalAlignment`]: enum.HorizontalAlignment.html - /// [`VerticalAlignment`]: enum.VerticalAlignment.html fn draw( &mut self, defaults: &Self::Defaults, @@ -239,3 +210,18 @@ where Element::new(text) } } + +impl<Renderer: self::Renderer> Clone for Text<Renderer> { + fn clone(&self) -> Self { + Self { + content: self.content.clone(), + size: self.size, + color: self.color, + font: self.font, + width: self.width, + height: self.height, + horizontal_alignment: self.horizontal_alignment, + vertical_alignment: self.vertical_alignment, + } + } +} diff --git a/native/src/widget/text_input.rs b/native/src/widget/text_input.rs index 1cdbe007..2fd9cec1 100644 --- a/native/src/widget/text_input.rs +++ b/native/src/widget/text_input.rs @@ -1,9 +1,6 @@ //! Display fields that can be filled with text. //! //! A [`TextInput`] has some local [`State`]. -//! -//! [`TextInput`]: struct.TextInput.html -//! [`State`]: struct.State.html mod editor; mod value; @@ -14,11 +11,14 @@ pub use value::Value; use editor::Editor; +use crate::event::{self, Event}; +use crate::keyboard; +use crate::layout; +use crate::mouse::{self, click}; +use crate::text; +use crate::touch; use crate::{ - keyboard, layout, - mouse::{self, click}, - Clipboard, Element, Event, Font, Hasher, Layout, Length, Point, Rectangle, - Size, Widget, + Clipboard, Element, Hasher, Layout, Length, Point, Rectangle, Size, Widget, }; use std::u32; @@ -53,7 +53,7 @@ pub struct TextInput<'a, Message, Renderer: self::Renderer> { placeholder: String, value: Value, is_secure: bool, - font: Font, + font: Renderer::Font, width: Length, max_width: u32, padding: u16, @@ -63,7 +63,11 @@ pub struct TextInput<'a, Message, Renderer: self::Renderer> { style: Renderer::Style, } -impl<'a, Message, Renderer: self::Renderer> TextInput<'a, Message, Renderer> { +impl<'a, Message, Renderer> TextInput<'a, Message, Renderer> +where + Message: Clone, + Renderer: self::Renderer, +{ /// Creates a new [`TextInput`]. /// /// It expects: @@ -71,9 +75,6 @@ impl<'a, Message, Renderer: self::Renderer> TextInput<'a, Message, Renderer> { /// - a placeholder /// - the current value /// - a function that produces a message when the [`TextInput`] changes - /// - /// [`TextInput`]: struct.TextInput.html - /// [`State`]: struct.State.html pub fn new<F>( state: &'a mut State, placeholder: &str, @@ -88,7 +89,7 @@ impl<'a, Message, Renderer: self::Renderer> TextInput<'a, Message, Renderer> { placeholder: String::from(placeholder), value: Value::new(value), is_secure: false, - font: Font::Default, + font: Default::default(), width: Length::Fill, max_width: u32::MAX, padding: 0, @@ -100,8 +101,6 @@ impl<'a, Message, Renderer: self::Renderer> TextInput<'a, Message, Renderer> { } /// Converts the [`TextInput`] into a secure password input. - /// - /// [`TextInput`]: struct.TextInput.html pub fn password(mut self) -> Self { self.is_secure = true; self @@ -109,39 +108,31 @@ impl<'a, Message, Renderer: self::Renderer> TextInput<'a, Message, Renderer> { /// Sets the [`Font`] of the [`Text`]. /// - /// [`Text`]: struct.Text.html - /// [`Font`]: ../../struct.Font.html - pub fn font(mut self, font: Font) -> Self { + /// [`Font`]: crate::widget::text::Renderer::Font + /// [`Text`]: crate::widget::Text + pub fn font(mut self, font: Renderer::Font) -> Self { self.font = font; self } /// Sets the width of the [`TextInput`]. - /// - /// [`TextInput`]: struct.TextInput.html pub fn width(mut self, width: Length) -> Self { self.width = width; self } /// Sets the maximum width of the [`TextInput`]. - /// - /// [`TextInput`]: struct.TextInput.html pub fn max_width(mut self, max_width: u32) -> Self { self.max_width = max_width; self } /// Sets the padding of the [`TextInput`]. - /// - /// [`TextInput`]: struct.TextInput.html pub fn padding(mut self, units: u16) -> Self { self.padding = units; self } /// Sets the text size of the [`TextInput`]. - /// - /// [`TextInput`]: struct.TextInput.html pub fn size(mut self, size: u16) -> Self { self.size = Some(size); self @@ -149,27 +140,75 @@ impl<'a, Message, Renderer: self::Renderer> TextInput<'a, Message, Renderer> { /// Sets the message that should be produced when the [`TextInput`] is /// focused and the enter key is pressed. - /// - /// [`TextInput`]: struct.TextInput.html pub fn on_submit(mut self, message: Message) -> Self { self.on_submit = Some(message); self } /// Sets the style of the [`TextInput`]. - /// - /// [`TextInput`]: struct.TextInput.html pub fn style(mut self, style: impl Into<Renderer::Style>) -> Self { self.style = style.into(); self } + + /// Returns the current [`State`] of the [`TextInput`]. + pub fn state(&self) -> &State { + self.state + } +} + +impl<'a, Message, Renderer> TextInput<'a, Message, Renderer> +where + Renderer: self::Renderer, +{ + /// Draws the [`TextInput`] with the given [`Renderer`], overriding its + /// [`Value`] if provided. + pub fn draw( + &self, + renderer: &mut Renderer, + layout: Layout<'_>, + cursor_position: Point, + value: Option<&Value>, + ) -> Renderer::Output { + let value = value.unwrap_or(&self.value); + let bounds = layout.bounds(); + let text_bounds = layout.children().next().unwrap().bounds(); + + if self.is_secure { + self::Renderer::draw( + renderer, + bounds, + text_bounds, + cursor_position, + self.font, + self.size.unwrap_or(renderer.default_size()), + &self.placeholder, + &value.secure(), + &self.state, + &self.style, + ) + } else { + self::Renderer::draw( + renderer, + bounds, + text_bounds, + cursor_position, + self.font, + self.size.unwrap_or(renderer.default_size()), + &self.placeholder, + value, + &self.state, + &self.style, + ) + } + } } impl<'a, Message, Renderer> Widget<Message, Renderer> for TextInput<'a, Message, Renderer> where - Renderer: self::Renderer, Message: Clone, + Renderer: self::Renderer, { fn width(&self) -> Length { self.width @@ -207,11 +246,14 @@ where messages: &mut Vec<Message>, renderer: &Renderer, clipboard: Option<&dyn Clipboard>, - ) { + ) -> event::Status { match event { - Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => { + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerPressed { .. }) => { let is_clicked = layout.bounds().contains(cursor_position); + self.state.is_focused = is_clicked; + if is_clicked { let text_layout = layout.children().next().unwrap(); let target = cursor_position.x - text_layout.bounds().x; @@ -243,6 +285,8 @@ where } else { self.state.cursor.move_to(0); } + + self.state.is_dragging = true; } click::Kind::Double => { if self.is_secure { @@ -262,25 +306,30 @@ where self.value.next_end_of_word(position), ); } + + self.state.is_dragging = false; } click::Kind::Triple => { self.state.cursor.select_all(&self.value); + self.state.is_dragging = false; } } self.state.last_click = Some(click); - } - self.state.is_dragging = is_clicked; - self.state.is_focused = is_clicked; + return event::Status::Captured; + } } - Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) => { + Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerLifted { .. }) + | Event::Touch(touch::Event::FingerLost { .. }) => { self.state.is_dragging = false; } - Event::Mouse(mouse::Event::CursorMoved { x, .. }) => { + Event::Mouse(mouse::Event::CursorMoved { position }) + | Event::Touch(touch::Event::FingerMoved { position, .. }) => { if self.state.is_dragging { let text_layout = layout.children().next().unwrap(); - let target = x - text_layout.bounds().x; + let target = position.x - text_layout.bounds().x; if target > 0.0 { let value = if self.is_secure { @@ -303,11 +352,14 @@ where position, ); } + + return event::Status::Captured; } } Event::Keyboard(keyboard::Event::CharacterReceived(c)) if self.state.is_focused && self.state.is_pasting.is_none() + && !self.state.keyboard_modifiers.is_command_pressed() && !c.is_control() => { let mut editor = @@ -317,162 +369,212 @@ where let message = (self.on_change)(editor.contents()); messages.push(message); + + return event::Status::Captured; } Event::Keyboard(keyboard::Event::KeyPressed { - key_code, - modifiers, - }) if self.state.is_focused => match key_code { - keyboard::KeyCode::Enter => { - if let Some(on_submit) = self.on_submit.clone() { - messages.push(on_submit); - } - } - keyboard::KeyCode::Backspace => { - if platform::is_jump_modifier_pressed(modifiers) - && self.state.cursor.selection().is_none() - { - if self.is_secure { - let cursor_pos = self.state.cursor.end(&self.value); - self.state.cursor.select_range(0, cursor_pos); - } else { - self.state.cursor.select_left_by_words(&self.value); + key_code, .. + }) if self.state.is_focused => { + let modifiers = self.state.keyboard_modifiers; + + match key_code { + keyboard::KeyCode::Enter => { + if let Some(on_submit) = self.on_submit.clone() { + messages.push(on_submit); } } + keyboard::KeyCode::Backspace => { + if platform::is_jump_modifier_pressed(modifiers) + && self + .state + .cursor + .selection(&self.value) + .is_none() + { + if self.is_secure { + let cursor_pos = + self.state.cursor.end(&self.value); + self.state.cursor.select_range(0, cursor_pos); + } else { + self.state + .cursor + .select_left_by_words(&self.value); + } + } - let mut editor = - Editor::new(&mut self.value, &mut self.state.cursor); + let mut editor = Editor::new( + &mut self.value, + &mut self.state.cursor, + ); - editor.backspace(); + editor.backspace(); - let message = (self.on_change)(editor.contents()); - messages.push(message); - } - keyboard::KeyCode::Delete => { - if platform::is_jump_modifier_pressed(modifiers) - && self.state.cursor.selection().is_none() - { - if self.is_secure { - let cursor_pos = self.state.cursor.end(&self.value); - self.state - .cursor - .select_range(cursor_pos, self.value.len()); - } else { - self.state + let message = (self.on_change)(editor.contents()); + messages.push(message); + } + keyboard::KeyCode::Delete => { + if platform::is_jump_modifier_pressed(modifiers) + && self + .state .cursor - .select_right_by_words(&self.value); + .selection(&self.value) + .is_none() + { + if self.is_secure { + let cursor_pos = + self.state.cursor.end(&self.value); + self.state + .cursor + .select_range(cursor_pos, self.value.len()); + } else { + self.state + .cursor + .select_right_by_words(&self.value); + } } - } - let mut editor = - Editor::new(&mut self.value, &mut self.state.cursor); + let mut editor = Editor::new( + &mut self.value, + &mut self.state.cursor, + ); - editor.delete(); + editor.delete(); - let message = (self.on_change)(editor.contents()); - messages.push(message); - } - keyboard::KeyCode::Left => { - if platform::is_jump_modifier_pressed(modifiers) - && !self.is_secure - { - if modifiers.shift { - self.state.cursor.select_left_by_words(&self.value); + let message = (self.on_change)(editor.contents()); + messages.push(message); + } + keyboard::KeyCode::Left => { + if platform::is_jump_modifier_pressed(modifiers) + && !self.is_secure + { + if modifiers.shift { + self.state + .cursor + .select_left_by_words(&self.value); + } else { + self.state + .cursor + .move_left_by_words(&self.value); + } + } else if modifiers.shift { + self.state.cursor.select_left(&self.value) } else { - self.state.cursor.move_left_by_words(&self.value); + self.state.cursor.move_left(&self.value); } - } else if modifiers.shift { - self.state.cursor.select_left(&self.value) - } else { - self.state.cursor.move_left(&self.value); } - } - keyboard::KeyCode::Right => { - if platform::is_jump_modifier_pressed(modifiers) - && !self.is_secure - { - if modifiers.shift { - self.state - .cursor - .select_right_by_words(&self.value); + keyboard::KeyCode::Right => { + if platform::is_jump_modifier_pressed(modifiers) + && !self.is_secure + { + if modifiers.shift { + self.state + .cursor + .select_right_by_words(&self.value); + } else { + self.state + .cursor + .move_right_by_words(&self.value); + } + } else if modifiers.shift { + self.state.cursor.select_right(&self.value) } else { - self.state.cursor.move_right_by_words(&self.value); + self.state.cursor.move_right(&self.value); } - } else if modifiers.shift { - self.state.cursor.select_right(&self.value) - } else { - self.state.cursor.move_right(&self.value); } - } - keyboard::KeyCode::Home => { - if modifiers.shift { - self.state.cursor.select_range( - self.state.cursor.start(&self.value), - 0, - ); - } else { - self.state.cursor.move_to(0); - } - } - keyboard::KeyCode::End => { - if modifiers.shift { - self.state.cursor.select_range( - self.state.cursor.start(&self.value), - self.value.len(), - ); - } else { - self.state.cursor.move_to(self.value.len()); + keyboard::KeyCode::Home => { + if modifiers.shift { + self.state.cursor.select_range( + self.state.cursor.start(&self.value), + 0, + ); + } else { + self.state.cursor.move_to(0); + } } - } - keyboard::KeyCode::V => { - if platform::is_copy_paste_modifier_pressed(modifiers) { - 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::End => { + if modifiers.shift { + self.state.cursor.select_range( + self.state.cursor.start(&self.value), + self.value.len(), ); + } else { + self.state.cursor.move_to(self.value.len()); + } + } + keyboard::KeyCode::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) + } + }; - 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.paste(content.clone()); - self.state.is_pasting = Some(content); + let message = + (self.on_change)(editor.contents()); + messages.push(message); + + self.state.is_pasting = Some(content); + } + } else { + self.state.is_pasting = None; } - } else { - self.state.is_pasting = None; } - } - keyboard::KeyCode::A => { - if platform::is_copy_paste_modifier_pressed(modifiers) { - self.state.cursor.select_all(&self.value); + keyboard::KeyCode::A => { + if self.state.keyboard_modifiers.is_command_pressed() { + self.state.cursor.select_all(&self.value); + } + } + keyboard::KeyCode::Escape => { + self.state.is_focused = false; + self.state.is_dragging = false; + self.state.is_pasting = None; + + self.state.keyboard_modifiers = + keyboard::Modifiers::default(); } + _ => {} } - _ => {} - }, + + return event::Status::Captured; + } Event::Keyboard(keyboard::Event::KeyReleased { key_code, .. - }) => match key_code { - keyboard::KeyCode::V => { - self.state.is_pasting = None; + }) if self.state.is_focused => { + match key_code { + keyboard::KeyCode::V => { + self.state.is_pasting = None; + } + _ => {} } - _ => {} - }, + + return event::Status::Captured; + } + Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) + if self.state.is_focused => + { + self.state.keyboard_modifiers = modifiers; + } _ => {} } + + event::Status::Ignored } fn draw( @@ -481,35 +583,9 @@ where _defaults: &Renderer::Defaults, layout: Layout<'_>, cursor_position: Point, + _viewport: &Rectangle, ) -> Renderer::Output { - let bounds = layout.bounds(); - let text_bounds = layout.children().next().unwrap().bounds(); - - if self.is_secure { - renderer.draw( - bounds, - text_bounds, - cursor_position, - self.font, - self.size.unwrap_or(renderer.default_size()), - &self.placeholder, - &self.value.secure(), - &self.state, - &self.style, - ) - } else { - renderer.draw( - bounds, - text_bounds, - cursor_position, - self.font, - self.size.unwrap_or(renderer.default_size()), - &self.placeholder, - &self.value, - &self.state, - &self.style, - ) - } + self.draw(renderer, layout, cursor_position, None) } fn hash_layout(&self, state: &mut Hasher) { @@ -529,34 +605,23 @@ where /// Your [renderer] will need to implement this trait before being /// able to use a [`TextInput`] in your user interface. /// -/// [`TextInput`]: struct.TextInput.html -/// [renderer]: ../../renderer/index.html -pub trait Renderer: crate::Renderer + Sized { +/// [renderer]: crate::renderer +pub trait Renderer: text::Renderer + Sized { /// The style supported by this renderer. type Style: Default; - /// Returns the default size of the text of the [`TextInput`]. - /// - /// [`TextInput`]: struct.TextInput.html - fn default_size(&self) -> u16; - /// Returns the width of the value of the [`TextInput`]. - /// - /// [`TextInput`]: struct.TextInput.html - fn measure_value(&self, value: &str, size: u16, font: Font) -> f32; + fn measure_value(&self, value: &str, size: u16, font: Self::Font) -> f32; /// Returns the current horizontal offset of the value of the /// [`TextInput`]. /// /// This is the amount of horizontal scrolling applied when the [`Value`] /// does not fit the [`TextInput`]. - /// - /// [`TextInput`]: struct.TextInput.html - /// [`Value`]: struct.Value.html fn offset( &self, text_bounds: Rectangle, - font: Font, + font: Self::Font, size: u16, value: &Value, state: &State, @@ -571,16 +636,12 @@ pub trait Renderer: crate::Renderer + Sized { /// - the placeholder to show when the value is empty /// - the current [`Value`] /// - the current [`State`] - /// - /// [`TextInput`]: struct.TextInput.html - /// [`Value`]: struct.Value.html - /// [`State`]: struct.State.html fn draw( &mut self, bounds: Rectangle, text_bounds: Rectangle, cursor_position: Point, - font: Font, + font: Self::Font, size: u16, placeholder: &str, value: &Value, @@ -590,12 +651,10 @@ pub trait Renderer: crate::Renderer + Sized { /// Computes the position of the text cursor at the given X coordinate of /// a [`TextInput`]. - /// - /// [`TextInput`]: struct.TextInput.html fn find_cursor_position( &self, text_bounds: Rectangle, - font: Font, + font: Self::Font, size: Option<u16>, value: &Value, state: &State, @@ -620,8 +679,8 @@ pub trait Renderer: crate::Renderer + Sized { impl<'a, Message, Renderer> From<TextInput<'a, Message, Renderer>> for Element<'a, Message, Renderer> where - Renderer: 'a + self::Renderer, Message: 'a + Clone, + Renderer: 'a + self::Renderer, { fn from( text_input: TextInput<'a, Message, Renderer>, @@ -631,8 +690,6 @@ where } /// The state of a [`TextInput`]. -/// -/// [`TextInput`]: struct.TextInput.html #[derive(Debug, Default, Clone)] pub struct State { is_focused: bool, @@ -640,20 +697,17 @@ pub struct State { is_pasting: Option<Value>, last_click: Option<mouse::Click>, cursor: Cursor, + keyboard_modifiers: keyboard::Modifiers, // TODO: Add stateful horizontal scrolling offset } impl State { /// Creates a new [`State`], representing an unfocused [`TextInput`]. - /// - /// [`State`]: struct.State.html pub fn new() -> Self { Self::default() } /// Creates a new [`State`], representing a focused [`TextInput`]. - /// - /// [`State`]: struct.State.html pub fn focused() -> Self { Self { is_focused: true, @@ -661,30 +715,51 @@ impl State { is_pasting: None, last_click: None, cursor: Cursor::default(), + keyboard_modifiers: keyboard::Modifiers::default(), } } /// Returns whether the [`TextInput`] is currently focused or not. - /// - /// [`TextInput`]: struct.TextInput.html pub fn is_focused(&self) -> bool { self.is_focused } /// Returns the [`Cursor`] of the [`TextInput`]. - /// - /// [`Cursor`]: struct.Cursor.html - /// [`TextInput`]: struct.TextInput.html pub fn cursor(&self) -> Cursor { self.cursor } + + /// Focuses the [`TextInput`]. + pub fn focus(&mut self) { + self.is_focused = true; + } + + /// Unfocuses the [`TextInput`]. + pub fn unfocus(&mut self) { + self.is_focused = false; + } + + /// Moves the [`Cursor`] of the [`TextInput`] to the front of the input text. + pub fn move_cursor_to_front(&mut self) { + self.cursor.move_to(0); + } + + /// Moves the [`Cursor`] of the [`TextInput`] to the end of the input text. + pub fn move_cursor_to_end(&mut self) { + self.cursor.move_to(usize::MAX); + } + + /// Moves the [`Cursor`] of the [`TextInput`] to an arbitrary location. + pub fn move_cursor_to(&mut self, position: usize) { + self.cursor.move_to(position); + } } // TODO: Reduce allocations fn find_cursor_position<Renderer: self::Renderer>( renderer: &Renderer, value: &Value, - font: Font, + font: Renderer::Font, size: u16, target: f32, start: usize, @@ -739,23 +814,11 @@ fn find_cursor_position<Renderer: self::Renderer>( mod platform { use crate::keyboard; - pub fn is_jump_modifier_pressed( - modifiers: keyboard::ModifiersState, - ) -> bool { + pub fn is_jump_modifier_pressed(modifiers: keyboard::Modifiers) -> bool { if cfg!(target_os = "macos") { modifiers.alt } else { modifiers.control } } - - pub fn is_copy_paste_modifier_pressed( - modifiers: keyboard::ModifiersState, - ) -> bool { - if cfg!(target_os = "macos") { - modifiers.logo - } else { - modifiers.control - } - } } diff --git a/native/src/widget/text_input/cursor.rs b/native/src/widget/text_input/cursor.rs index 16e7a01b..e630e293 100644 --- a/native/src/widget/text_input/cursor.rs +++ b/native/src/widget/text_input/cursor.rs @@ -8,8 +8,6 @@ pub struct Cursor { } /// The state of a [`Cursor`]. -/// -/// [`Cursor`]: struct.Cursor.html #[derive(Debug, Copy, Clone)] pub enum State { /// Cursor without a selection @@ -34,9 +32,6 @@ impl Default for Cursor { impl Cursor { /// Returns the [`State`] of the [`Cursor`]. - /// - /// [`State`]: struct.State.html - /// [`Cursor`]: struct.Cursor.html pub fn state(&self, value: &Value) -> State { match self.state { State::Index(index) => State::Index(index.min(value.len())), @@ -166,8 +161,8 @@ impl Cursor { end.min(value.len()) } - pub(crate) fn selection(&self) -> Option<(usize, usize)> { - match self.state { + 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))) } diff --git a/native/src/widget/text_input/editor.rs b/native/src/widget/text_input/editor.rs index c9b9795d..20e42567 100644 --- a/native/src/widget/text_input/editor.rs +++ b/native/src/widget/text_input/editor.rs @@ -15,7 +15,7 @@ impl<'a> Editor<'a> { } pub fn insert(&mut self, character: char) { - match self.cursor.selection() { + match self.cursor.selection(self.value) { Some((left, right)) => { self.cursor.move_left(self.value); self.value.remove_many(left, right); @@ -30,7 +30,7 @@ impl<'a> Editor<'a> { pub fn paste(&mut self, content: Value) { let length = content.len(); - match self.cursor.selection() { + match self.cursor.selection(self.value) { Some((left, right)) => { self.cursor.move_left(self.value); self.value.remove_many(left, right); @@ -44,7 +44,7 @@ impl<'a> Editor<'a> { } pub fn backspace(&mut self) { - match self.cursor.selection() { + match self.cursor.selection(self.value) { Some((start, end)) => { self.cursor.move_left(self.value); self.value.remove_many(start, end); @@ -61,7 +61,7 @@ impl<'a> Editor<'a> { } pub fn delete(&mut self) { - match self.cursor.selection() { + match self.cursor.selection(self.value) { Some(_) => { self.backspace(); } diff --git a/native/src/widget/text_input/value.rs b/native/src/widget/text_input/value.rs index 1e9ba45b..86be2790 100644 --- a/native/src/widget/text_input/value.rs +++ b/native/src/widget/text_input/value.rs @@ -2,7 +2,7 @@ use unicode_segmentation::UnicodeSegmentation; /// The value of a [`TextInput`]. /// -/// [`TextInput`]: struct.TextInput.html +/// [`TextInput`]: crate::widget::TextInput // TODO: Reduce allocations, cache results (?) #[derive(Debug, Clone)] pub struct Value { @@ -11,8 +11,6 @@ pub struct Value { impl Value { /// Creates a new [`Value`] from a string slice. - /// - /// [`Value`]: struct.Value.html pub fn new(string: &str) -> Self { let graphemes = UnicodeSegmentation::graphemes(string, true) .map(String::from) @@ -21,17 +19,20 @@ impl Value { Self { graphemes } } - /// Returns the total amount of graphemes in the [`Value`]. + /// Returns whether the [`Value`] is empty or not. /// - /// [`Value`]: struct.Value.html + /// A [`Value`] is empty when it contains no graphemes. + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + + /// Returns the total amount of graphemes in the [`Value`]. pub fn len(&self) -> usize { self.graphemes.len() } /// Returns the position of the previous start of a word from the given /// grapheme `index`. - /// - /// [`Value`]: struct.Value.html pub fn previous_start_of_word(&self, index: usize) -> usize { let previous_string = &self.graphemes[..index.min(self.graphemes.len())].concat(); @@ -54,8 +55,6 @@ impl Value { /// Returns the position of the next end of a word from the given grapheme /// `index`. - /// - /// [`Value`]: struct.Value.html pub fn next_end_of_word(&self, index: usize) -> usize { let next_string = &self.graphemes[index..].concat(); @@ -76,8 +75,6 @@ impl Value { /// Returns a new [`Value`] containing the graphemes until the given /// `index`. - /// - /// [`Value`]: struct.Value.html pub fn until(&self, index: usize) -> Self { let graphemes = self.graphemes[..index.min(self.len())].to_vec(); @@ -85,8 +82,6 @@ impl Value { } /// Converts the [`Value`] into a `String`. - /// - /// [`Value`]: struct.Value.html pub fn to_string(&self) -> String { self.graphemes.concat() } @@ -109,8 +104,6 @@ impl Value { } /// Removes the grapheme at the given `index`. - /// - /// [`Value`]: struct.Value.html pub fn remove(&mut self, index: usize) { let _ = self.graphemes.remove(index); } @@ -122,8 +115,6 @@ impl Value { /// Returns a new [`Value`] with all its graphemes replaced with the /// dot ('•') character. - /// - /// [`Value`]: struct.Value.html pub fn secure(&self) -> Self { Self { graphemes: std::iter::repeat(String::from("•")) |