diff options
Diffstat (limited to 'native/src/widget')
30 files changed, 2312 insertions, 1512 deletions
diff --git a/native/src/widget/button.rs b/native/src/widget/button.rs index c932da2b..c469a0e5 100644 --- a/native/src/widget/button.rs +++ b/native/src/widget/button.rs @@ -1,12 +1,14 @@ //! Allow your users to perform actions by pressing a button. //! //! A [`Button`] has some local [`State`]. -//! -//! [`Button`]: struct.Button.html -//! [`State`]: struct.State.html +use crate::event::{self, Event}; +use crate::layout; +use crate::mouse; +use crate::overlay; +use crate::touch; use crate::{ - layout, mouse, Clipboard, Element, Event, Hasher, Layout, Length, Point, - Rectangle, Widget, + Clipboard, Element, Hasher, Layout, Length, Padding, Point, Rectangle, + Widget, }; use std::hash::Hash; @@ -18,6 +20,7 @@ use std::hash::Hash; /// # type Button<'a, Message> = /// # iced_native::Button<'a, Message, iced_native::renderer::Null>; /// # +/// #[derive(Clone)] /// enum Message { /// ButtonPressed, /// } @@ -26,6 +29,29 @@ use std::hash::Hash; /// let button = Button::new(&mut state, Text::new("Press me!")) /// .on_press(Message::ButtonPressed); /// ``` +/// +/// If a [`Button::on_press`] handler is not set, the resulting [`Button`] will +/// be disabled: +/// +/// ``` +/// # use iced_native::{button, Text}; +/// # +/// # type Button<'a, Message> = +/// # iced_native::Button<'a, Message, iced_native::renderer::Null>; +/// # +/// #[derive(Clone)] +/// enum Message { +/// ButtonPressed, +/// } +/// +/// fn disabled_button(state: &mut button::State) -> Button<'_, Message> { +/// Button::new(state, Text::new("I'm disabled!")) +/// } +/// +/// fn enabled_button(state: &mut button::State) -> Button<'_, Message> { +/// disabled_button(state).on_press(Message::ButtonPressed) +/// } +/// ``` #[allow(missing_debug_implementations)] pub struct Button<'a, Message, Renderer: self::Renderer> { state: &'a mut State, @@ -35,19 +61,17 @@ pub struct Button<'a, Message, Renderer: self::Renderer> { height: Length, min_width: u32, min_height: u32, - padding: u16, + padding: Padding, style: Renderer::Style, } impl<'a, Message, Renderer> Button<'a, Message, Renderer> where + Message: Clone, Renderer: self::Renderer, { /// Creates a new [`Button`] with some local [`State`] and the given /// content. - /// - /// [`Button`]: struct.Button.html - /// [`State`]: struct.State.html pub fn new<E>(state: &'a mut State, content: E) -> Self where E: Into<Element<'a, Message, Renderer>>, @@ -66,56 +90,43 @@ where } /// Sets the width of the [`Button`]. - /// - /// [`Button`]: struct.Button.html pub fn width(mut self, width: Length) -> Self { self.width = width; self } /// Sets the height of the [`Button`]. - /// - /// [`Button`]: struct.Button.html pub fn height(mut self, height: Length) -> Self { self.height = height; self } /// Sets the minimum width of the [`Button`]. - /// - /// [`Button`]: struct.Button.html pub fn min_width(mut self, min_width: u32) -> Self { self.min_width = min_width; self } /// Sets the minimum height of the [`Button`]. - /// - /// [`Button`]: struct.Button.html pub fn min_height(mut self, min_height: u32) -> Self { self.min_height = min_height; self } - /// Sets the padding of the [`Button`]. - /// - /// [`Button`]: struct.Button.html - pub fn padding(mut self, padding: u16) -> Self { - self.padding = padding; + /// Sets the [`Padding`] of the [`Button`]. + pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self { + self.padding = padding.into(); self } /// Sets the message that will be produced when the [`Button`] is pressed. - /// - /// [`Button`]: struct.Button.html + /// If on_press isn't set, button will be disabled. pub fn on_press(mut self, msg: Message) -> Self { self.on_press = Some(msg); self } /// Sets the style of the [`Button`]. - /// - /// [`Button`]: struct.Button.html pub fn style(mut self, style: impl Into<Renderer::Style>) -> Self { self.style = style.into(); self @@ -123,8 +134,6 @@ where } /// The local state of a [`Button`]. -/// -/// [`Button`]: struct.Button.html #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub struct State { is_pressed: bool, @@ -132,8 +141,6 @@ pub struct State { impl State { /// Creates a new [`State`]. - /// - /// [`State`]: struct.State.html pub fn new() -> State { State::default() } @@ -142,8 +149,8 @@ impl State { impl<'a, Message, Renderer> Widget<Message, Renderer> for Button<'a, Message, Renderer> where - Renderer: self::Renderer, Message: Clone, + Renderer: self::Renderer, { fn width(&self) -> Length { self.width @@ -158,18 +165,20 @@ where renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { - let padding = f32::from(self.padding); let limits = limits .min_width(self.min_width) .min_height(self.min_height) .width(self.width) .height(self.height) - .pad(padding); + .pad(self.padding); let mut content = self.content.layout(renderer, &limits); - content.move_to(Point::new(padding, padding)); + content.move_to(Point::new( + self.padding.left.into(), + self.padding.top.into(), + )); - let size = limits.resolve(content.size()).pad(padding); + let size = limits.resolve(content.size()).pad(self.padding); layout::Node::with_children(size, vec![content]) } @@ -179,34 +188,57 @@ where event: Event, layout: Layout<'_>, cursor_position: Point, + renderer: &Renderer, + clipboard: &mut dyn Clipboard, messages: &mut Vec<Message>, - _renderer: &Renderer, - _clipboard: Option<&dyn Clipboard>, - ) { + ) -> event::Status { + if let event::Status::Captured = self.content.on_event( + event.clone(), + layout.children().next().unwrap(), + cursor_position, + renderer, + clipboard, + messages, + ) { + return event::Status::Captured; + } + match event { - Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => { + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerPressed { .. }) => { if self.on_press.is_some() { let bounds = layout.bounds(); - self.state.is_pressed = bounds.contains(cursor_position); + if bounds.contains(cursor_position) { + self.state.is_pressed = true; + + return event::Status::Captured; + } } } - Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) => { + Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerLifted { .. }) => { if let Some(on_press) = self.on_press.clone() { let bounds = layout.bounds(); - let is_clicked = self.state.is_pressed - && bounds.contains(cursor_position); + if self.state.is_pressed { + self.state.is_pressed = false; - self.state.is_pressed = false; + if bounds.contains(cursor_position) { + messages.push(on_press); + } - if is_clicked { - messages.push(on_press); + return event::Status::Captured; } } } + Event::Touch(touch::Event::FingerLost { .. }) => { + self.state.is_pressed = false; + } _ => {} } + + event::Status::Ignored } fn draw( @@ -215,6 +247,7 @@ where defaults: &Renderer::Defaults, layout: Layout<'_>, cursor_position: Point, + _viewport: &Rectangle, ) -> Renderer::Output { renderer.draw( defaults, @@ -235,6 +268,13 @@ where self.width.hash(state); self.content.hash_layout(state); } + + fn overlay( + &mut self, + layout: Layout<'_>, + ) -> Option<overlay::Element<'_, Message, Renderer>> { + self.content.overlay(layout.children().next().unwrap()) + } } /// The renderer of a [`Button`]. @@ -242,20 +282,15 @@ where /// Your [renderer] will need to implement this trait before being /// able to use a [`Button`] in your user interface. /// -/// [`Button`]: struct.Button.html -/// [renderer]: ../../renderer/index.html +/// [renderer]: crate::renderer pub trait Renderer: crate::Renderer + Sized { /// The default padding of a [`Button`]. - /// - /// [`Button`]: struct.Button.html - const DEFAULT_PADDING: u16; + const DEFAULT_PADDING: Padding; /// The style supported by this renderer. type Style: Default; /// Draws a [`Button`]. - /// - /// [`Button`]: struct.Button.html fn draw<Message>( &mut self, defaults: &Self::Defaults, @@ -272,8 +307,8 @@ pub trait Renderer: crate::Renderer + Sized { impl<'a, Message, Renderer> From<Button<'a, Message, Renderer>> for Element<'a, Message, Renderer> where - Renderer: 'a + self::Renderer, Message: 'a + Clone, + Renderer: 'a + self::Renderer, { fn from( button: Button<'a, Message, Renderer>, diff --git a/native/src/widget/checkbox.rs b/native/src/widget/checkbox.rs index 99178aae..0f21c873 100644 --- a/native/src/widget/checkbox.rs +++ b/native/src/widget/checkbox.rs @@ -1,10 +1,15 @@ //! Show toggle controls using checkboxes. use std::hash::Hash; +use crate::event::{self, Event}; +use crate::layout; +use crate::mouse; +use crate::row; +use crate::text; +use crate::touch; use crate::{ - layout, mouse, row, text, Align, Clipboard, Element, Event, Hasher, - HorizontalAlignment, Layout, Length, Point, Rectangle, Row, Text, - VerticalAlignment, Widget, + Align, Clipboard, Color, Element, Hasher, HorizontalAlignment, Layout, + Length, Point, Rectangle, Row, Text, VerticalAlignment, Widget, }; /// A box that can be checked. @@ -34,6 +39,7 @@ pub struct Checkbox<Message, Renderer: self::Renderer + text::Renderer> { spacing: u16, text_size: Option<u16>, font: Renderer::Font, + text_color: Option<Color>, style: Renderer::Style, } @@ -48,8 +54,6 @@ impl<Message, Renderer: self::Renderer + text::Renderer> /// * a function that will be called when the [`Checkbox`] is toggled. It /// will receive the new state of the [`Checkbox`] and must produce a /// `Message`. - /// - /// [`Checkbox`]: struct.Checkbox.html pub fn new<F>(is_checked: bool, label: impl Into<String>, f: F) -> Self where F: 'static + Fn(bool) -> Message, @@ -63,37 +67,30 @@ impl<Message, Renderer: self::Renderer + text::Renderer> spacing: Renderer::DEFAULT_SPACING, text_size: None, font: Renderer::Font::default(), + text_color: None, style: Renderer::Style::default(), } } /// Sets the size of the [`Checkbox`]. - /// - /// [`Checkbox`]: struct.Checkbox.html pub fn size(mut self, size: u16) -> Self { self.size = size; self } /// Sets the width of the [`Checkbox`]. - /// - /// [`Checkbox`]: struct.Checkbox.html pub fn width(mut self, width: Length) -> Self { self.width = width; self } /// Sets the spacing between the [`Checkbox`] and the text. - /// - /// [`Checkbox`]: struct.Checkbox.html pub fn spacing(mut self, spacing: u16) -> Self { self.spacing = spacing; self } /// Sets the text size of the [`Checkbox`]. - /// - /// [`Checkbox`]: struct.Checkbox.html pub fn text_size(mut self, text_size: u16) -> Self { self.text_size = Some(text_size); self @@ -101,16 +98,19 @@ impl<Message, Renderer: self::Renderer + text::Renderer> /// Sets the [`Font`] of the text of the [`Checkbox`]. /// - /// [`Checkbox`]: struct.Checkbox.html - /// [`Font`]: ../../struct.Font.html + /// [`Font`]: crate::widget::text::Renderer::Font pub fn font(mut self, font: Renderer::Font) -> Self { self.font = font; self } + /// Sets the text color of the [`Checkbox`] button. + pub fn text_color(mut self, color: Color) -> Self { + self.text_color = Some(color); + self + } + /// Sets the style of the [`Checkbox`]. - /// - /// [`Checkbox`]: struct.Checkbox.html pub fn style(mut self, style: impl Into<Renderer::Style>) -> Self { self.style = style.into(); self @@ -158,20 +158,25 @@ where event: Event, layout: Layout<'_>, cursor_position: Point, - messages: &mut Vec<Message>, _renderer: &Renderer, - _clipboard: Option<&dyn Clipboard>, - ) { + _clipboard: &mut dyn Clipboard, + messages: &mut Vec<Message>, + ) -> event::Status { match event { - Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => { + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerPressed { .. }) => { let mouse_over = layout.bounds().contains(cursor_position); if mouse_over { messages.push((self.on_toggle)(!self.is_checked)); + + return event::Status::Captured; } } _ => {} } + + event::Status::Ignored } fn draw( @@ -180,6 +185,7 @@ where defaults: &Renderer::Defaults, layout: Layout<'_>, cursor_position: Point, + _viewport: &Rectangle, ) -> Renderer::Output { let bounds = layout.bounds(); let mut children = layout.children(); @@ -195,7 +201,7 @@ where &self.label, self.text_size.unwrap_or(renderer.default_size()), self.font, - None, + self.text_color, HorizontalAlignment::Left, VerticalAlignment::Center, ); @@ -225,20 +231,15 @@ where /// Your [renderer] will need to implement this trait before being /// able to use a [`Checkbox`] in your user interface. /// -/// [`Checkbox`]: struct.Checkbox.html -/// [renderer]: ../../renderer/index.html +/// [renderer]: crate::Renderer pub trait Renderer: crate::Renderer { /// The style supported by this renderer. type Style: Default; /// The default size of a [`Checkbox`]. - /// - /// [`Checkbox`]: struct.Checkbox.html const DEFAULT_SIZE: u16; /// The default spacing of a [`Checkbox`]. - /// - /// [`Checkbox`]: struct.Checkbox.html const DEFAULT_SPACING: u16; /// Draws a [`Checkbox`]. @@ -248,8 +249,6 @@ pub trait Renderer: crate::Renderer { /// * whether the [`Checkbox`] is selected or not /// * whether the mouse is over the [`Checkbox`] or not /// * the drawn label of the [`Checkbox`] - /// - /// [`Checkbox`]: struct.Checkbox.html fn draw( &mut self, bounds: Rectangle, diff --git a/native/src/widget/column.rs b/native/src/widget/column.rs index 42cfe9b9..52a2e80c 100644 --- a/native/src/widget/column.rs +++ b/native/src/widget/column.rs @@ -1,22 +1,21 @@ //! Distribute content vertically. use std::hash::Hash; +use crate::event::{self, Event}; +use crate::layout; +use crate::overlay; use crate::{ - layout, overlay, Align, Clipboard, Element, Event, Hasher, Layout, Length, - Point, Widget, + Align, Clipboard, Element, Hasher, Layout, Length, Padding, Point, + Rectangle, Widget, }; use std::u32; /// A container that distributes its contents vertically. -/// -/// A [`Column`] will try to fill the horizontal space of its container. -/// -/// [`Column`]: struct.Column.html #[allow(missing_debug_implementations)] pub struct Column<'a, Message, Renderer> { spacing: u16, - padding: u16, + padding: Padding, width: Length, height: Length, max_width: u32, @@ -27,21 +26,17 @@ pub struct Column<'a, Message, Renderer> { impl<'a, Message, Renderer> Column<'a, Message, Renderer> { /// Creates an empty [`Column`]. - /// - /// [`Column`]: struct.Column.html pub fn new() -> Self { Self::with_children(Vec::new()) } /// Creates a [`Column`] with the given elements. - /// - /// [`Column`]: struct.Column.html pub fn with_children( children: Vec<Element<'a, Message, Renderer>>, ) -> Self { Column { spacing: 0, - padding: 0, + padding: Padding::ZERO, width: Length::Shrink, height: Length::Shrink, max_width: u32::MAX, @@ -61,57 +56,43 @@ impl<'a, Message, Renderer> Column<'a, Message, Renderer> { self } - /// Sets the padding of the [`Column`]. - /// - /// [`Column`]: struct.Column.html - pub fn padding(mut self, units: u16) -> Self { - self.padding = units; + /// Sets the [`Padding`] of the [`Column`]. + pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self { + self.padding = padding.into(); self } /// Sets the width of the [`Column`]. - /// - /// [`Column`]: struct.Column.html pub fn width(mut self, width: Length) -> Self { self.width = width; self } /// Sets the height of the [`Column`]. - /// - /// [`Column`]: struct.Column.html pub fn height(mut self, height: Length) -> Self { self.height = height; self } /// Sets the maximum width of the [`Column`]. - /// - /// [`Column`]: struct.Column.html pub fn max_width(mut self, max_width: u32) -> Self { self.max_width = max_width; self } /// Sets the maximum height of the [`Column`] in pixels. - /// - /// [`Column`]: struct.Column.html pub fn max_height(mut self, max_height: u32) -> Self { self.max_height = max_height; self } /// Sets the horizontal alignment of the contents of the [`Column`] . - /// - /// [`Column`]: struct.Column.html pub fn align_items(mut self, align: Align) -> Self { self.align_items = align; self } /// Adds an element to the [`Column`]. - /// - /// [`Column`]: struct.Column.html pub fn push<E>(mut self, child: E) -> Self where E: Into<Element<'a, Message, Renderer>>, @@ -149,7 +130,7 @@ where layout::flex::Axis::Vertical, renderer, &limits, - self.padding as f32, + self.padding, self.spacing as f32, self.align_items, &self.children, @@ -161,22 +142,24 @@ where event: Event, layout: Layout<'_>, cursor_position: Point, - messages: &mut Vec<Message>, renderer: &Renderer, - clipboard: Option<&dyn Clipboard>, - ) { - self.children.iter_mut().zip(layout.children()).for_each( - |(child, layout)| { + clipboard: &mut dyn Clipboard, + messages: &mut Vec<Message>, + ) -> event::Status { + self.children + .iter_mut() + .zip(layout.children()) + .map(|(child, layout)| { child.widget.on_event( event.clone(), layout, cursor_position, - messages, renderer, clipboard, + messages, ) - }, - ); + }) + .fold(event::Status::Ignored, event::Status::merge) } fn draw( @@ -185,8 +168,15 @@ where defaults: &Renderer::Defaults, layout: Layout<'_>, cursor_position: Point, + viewport: &Rectangle, ) -> Renderer::Output { - renderer.draw(defaults, &self.children, layout, cursor_position) + renderer.draw( + defaults, + &self.children, + layout, + cursor_position, + viewport, + ) } fn hash_layout(&self, state: &mut Hasher) { @@ -199,6 +189,7 @@ where self.max_height.hash(state); self.align_items.hash(state); self.spacing.hash(state); + self.padding.hash(state); for child in &self.children { child.widget.hash_layout(state); @@ -222,8 +213,7 @@ where /// Your [renderer] will need to implement this trait before being /// able to use a [`Column`] in your user interface. /// -/// [`Column`]: struct.Column.html -/// [renderer]: ../../renderer/index.html +/// [renderer]: crate::renderer pub trait Renderer: crate::Renderer + Sized { /// Draws a [`Column`]. /// @@ -231,15 +221,13 @@ pub trait Renderer: crate::Renderer + Sized { /// - the children of the [`Column`] /// - the [`Layout`] of the [`Column`] and its children /// - the cursor position - /// - /// [`Column`]: struct.Column.html - /// [`Layout`]: ../layout/struct.Layout.html fn draw<Message>( &mut self, defaults: &Self::Defaults, content: &[Element<'_, Message, Self>], layout: Layout<'_>, cursor_position: Point, + viewport: &Rectangle, ) -> Self::Output; } diff --git a/native/src/widget/container.rs b/native/src/widget/container.rs index b8316e62..69aee64d 100644 --- a/native/src/widget/container.rs +++ b/native/src/widget/container.rs @@ -1,9 +1,12 @@ //! Decorate content and apply alignment. use std::hash::Hash; +use crate::event::{self, Event}; +use crate::layout; +use crate::overlay; use crate::{ - layout, overlay, Align, Clipboard, Element, Event, Hasher, Layout, Length, - Point, Rectangle, Widget, + Align, Clipboard, Element, Hasher, Layout, Length, Padding, Point, + Rectangle, Widget, }; use std::u32; @@ -13,7 +16,7 @@ use std::u32; /// It is normally used for alignment purposes. #[allow(missing_debug_implementations)] pub struct Container<'a, Message, Renderer: self::Renderer> { - padding: u16, + padding: Padding, width: Length, height: Length, max_width: u32, @@ -29,14 +32,12 @@ where Renderer: self::Renderer, { /// Creates an empty [`Container`]. - /// - /// [`Container`]: struct.Container.html pub fn new<T>(content: T) -> Self where T: Into<Element<'a, Message, Renderer>>, { Container { - padding: 0, + padding: Padding::ZERO, width: Length::Shrink, height: Length::Shrink, max_width: u32::MAX, @@ -48,81 +49,61 @@ where } } - /// Sets the padding of the [`Container`]. - /// - /// [`Container`]: struct.Column.html - pub fn padding(mut self, units: u16) -> Self { - self.padding = units; + /// Sets the [`Padding`] of the [`Container`]. + pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self { + self.padding = padding.into(); self } /// Sets the width of the [`Container`]. - /// - /// [`Container`]: struct.Container.html pub fn width(mut self, width: Length) -> Self { self.width = width; self } /// Sets the height of the [`Container`]. - /// - /// [`Container`]: struct.Container.html pub fn height(mut self, height: Length) -> Self { self.height = height; self } /// Sets the maximum width of the [`Container`]. - /// - /// [`Container`]: struct.Container.html pub fn max_width(mut self, max_width: u32) -> Self { self.max_width = max_width; self } /// Sets the maximum height of the [`Container`] in pixels. - /// - /// [`Container`]: struct.Container.html pub fn max_height(mut self, max_height: u32) -> Self { self.max_height = max_height; self } /// Sets the content alignment for the horizontal axis of the [`Container`]. - /// - /// [`Container`]: struct.Container.html pub fn align_x(mut self, alignment: Align) -> Self { self.horizontal_alignment = alignment; self } /// Sets the content alignment for the vertical axis of the [`Container`]. - /// - /// [`Container`]: struct.Container.html pub fn align_y(mut self, alignment: Align) -> Self { self.vertical_alignment = alignment; self } /// Centers the contents in the horizontal axis of the [`Container`]. - /// - /// [`Container`]: struct.Container.html pub fn center_x(mut self) -> Self { self.horizontal_alignment = Align::Center; self } /// Centers the contents in the vertical axis of the [`Container`]. - /// - /// [`Container`]: struct.Container.html pub fn center_y(mut self) -> Self { self.vertical_alignment = Align::Center; self } /// Sets the style of the [`Container`]. - /// - /// [`Container`]: struct.Container.html pub fn style(mut self, style: impl Into<Renderer::Style>) -> Self { self.style = style.into(); self @@ -147,23 +128,24 @@ where renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { - let padding = f32::from(self.padding); - let limits = limits .loose() .max_width(self.max_width) .max_height(self.max_height) .width(self.width) .height(self.height) - .pad(padding); + .pad(self.padding); let mut content = self.content.layout(renderer, &limits.loose()); let size = limits.resolve(content.size()); - content.move_to(Point::new(padding, padding)); + content.move_to(Point::new( + self.padding.left.into(), + self.padding.top.into(), + )); content.align(self.horizontal_alignment, self.vertical_alignment, size); - layout::Node::with_children(size.pad(padding), vec![content]) + layout::Node::with_children(size.pad(self.padding), vec![content]) } fn on_event( @@ -171,17 +153,17 @@ where event: Event, layout: Layout<'_>, cursor_position: Point, - messages: &mut Vec<Message>, renderer: &Renderer, - clipboard: Option<&dyn Clipboard>, - ) { + clipboard: &mut dyn Clipboard, + messages: &mut Vec<Message>, + ) -> event::Status { self.content.widget.on_event( event, layout.children().next().unwrap(), cursor_position, - messages, renderer, clipboard, + messages, ) } @@ -191,11 +173,13 @@ where defaults: &Renderer::Defaults, layout: Layout<'_>, cursor_position: Point, + viewport: &Rectangle, ) -> Renderer::Output { renderer.draw( defaults, layout.bounds(), cursor_position, + viewport, &self.style, &self.content, layout.children().next().unwrap(), @@ -228,20 +212,18 @@ where /// Your [renderer] will need to implement this trait before being /// able to use a [`Container`] in your user interface. /// -/// [`Container`]: struct.Container.html -/// [renderer]: ../../renderer/index.html +/// [renderer]: crate::renderer pub trait Renderer: crate::Renderer { /// The style supported by this renderer. type Style: Default; /// Draws a [`Container`]. - /// - /// [`Container`]: struct.Container.html fn draw<Message>( &mut self, defaults: &Self::Defaults, bounds: Rectangle, cursor_position: Point, + viewport: &Rectangle, style: &Self::Style, content: &Element<'_, Message, Self>, content_layout: Layout<'_>, diff --git a/native/src/widget/image.rs b/native/src/widget/image.rs index 132f249d..4d8e0a3f 100644 --- a/native/src/widget/image.rs +++ b/native/src/widget/image.rs @@ -1,5 +1,9 @@ //! Display images in your user interface. -use crate::{layout, Element, Hasher, Layout, Length, Point, Size, Widget}; +pub mod viewer; +pub use viewer::Viewer; + +use crate::layout; +use crate::{Element, Hasher, Layout, Length, Point, Rectangle, Size, Widget}; use std::{ hash::{Hash, Hasher as _}, @@ -27,8 +31,6 @@ pub struct Image { impl Image { /// Creates a new [`Image`] with the given path. - /// - /// [`Image`]: struct.Image.html pub fn new<T: Into<Handle>>(handle: T) -> Self { Image { handle: handle.into(), @@ -38,16 +40,12 @@ impl Image { } /// Sets the width of the [`Image`] boundaries. - /// - /// [`Image`]: struct.Image.html pub fn width(mut self, width: Length) -> Self { self.width = width; self } /// Sets the height of the [`Image`] boundaries. - /// - /// [`Image`]: struct.Image.html pub fn height(mut self, height: Length) -> Self { self.height = height; self @@ -97,6 +95,7 @@ where _defaults: &Renderer::Defaults, layout: Layout<'_>, _cursor_position: Point, + _viewport: &Rectangle, ) -> Renderer::Output { renderer.draw(self.handle.clone(), layout) } @@ -112,8 +111,6 @@ where } /// An [`Image`] handle. -/// -/// [`Image`]: struct.Image.html #[derive(Debug, Clone)] pub struct Handle { id: u64, @@ -124,8 +121,6 @@ impl Handle { /// Creates an image [`Handle`] pointing to the image of the given path. /// /// Makes an educated guess about the image format by examining the data in the file. - /// - /// [`Handle`]: struct.Handle.html pub fn from_path<T: Into<PathBuf>>(path: T) -> Handle { Self::from_data(Data::Path(path.into())) } @@ -135,8 +130,6 @@ impl Handle { /// pixels. /// /// This is useful if you have already decoded your image. - /// - /// [`Handle`]: struct.Handle.html pub fn from_pixels(width: u32, height: u32, pixels: Vec<u8>) -> Handle { Self::from_data(Data::Pixels { width, @@ -151,8 +144,6 @@ impl Handle { /// /// This is useful if you already have your image loaded in-memory, maybe /// because you downloaded or generated it procedurally. - /// - /// [`Handle`]: struct.Handle.html pub fn from_memory(bytes: Vec<u8>) -> Handle { Self::from_data(Data::Bytes(bytes)) } @@ -168,15 +159,11 @@ impl Handle { } /// Returns the unique identifier of the [`Handle`]. - /// - /// [`Handle`]: struct.Handle.html pub fn id(&self) -> u64 { self.id } /// Returns a reference to the image [`Data`]. - /// - /// [`Data`]: enum.Data.html pub fn data(&self) -> &Data { &self.data } @@ -198,8 +185,6 @@ impl Hash for Handle { } /// The data of an [`Image`]. -/// -/// [`Image`]: struct.Image.html #[derive(Clone, Hash)] pub enum Data { /// File data @@ -236,17 +221,12 @@ impl std::fmt::Debug for Data { /// Your [renderer] will need to implement this trait before being able to use /// an [`Image`] in your user interface. /// -/// [`Image`]: struct.Image.html -/// [renderer]: ../../renderer/index.html +/// [renderer]: crate::renderer pub trait Renderer: crate::Renderer { /// Returns the dimensions of an [`Image`] located on the given path. - /// - /// [`Image`]: struct.Image.html fn dimensions(&self, handle: &Handle) -> (u32, u32); /// Draws an [`Image`]. - /// - /// [`Image`]: struct.Image.html fn draw(&mut self, handle: Handle, layout: Layout<'_>) -> Self::Output; } diff --git a/native/src/widget/image/viewer.rs b/native/src/widget/image/viewer.rs new file mode 100644 index 00000000..405daf00 --- /dev/null +++ b/native/src/widget/image/viewer.rs @@ -0,0 +1,413 @@ +//! Zoom and pan on an image. +use crate::event::{self, Event}; +use crate::image; +use crate::layout; +use crate::mouse; +use crate::{ + Clipboard, Element, Hasher, Layout, Length, Point, Rectangle, Size, Vector, + Widget, +}; + +use std::hash::Hash; + +/// A frame that displays an image with the ability to zoom in/out and pan. +#[allow(missing_debug_implementations)] +pub struct Viewer<'a> { + state: &'a mut State, + padding: u16, + width: Length, + height: Length, + min_scale: f32, + max_scale: f32, + scale_step: f32, + handle: image::Handle, +} + +impl<'a> Viewer<'a> { + /// Creates a new [`Viewer`] with the given [`State`] and [`Handle`]. + /// + /// [`Handle`]: image::Handle + pub fn new(state: &'a mut State, handle: image::Handle) -> Self { + Viewer { + state, + padding: 0, + width: Length::Shrink, + height: Length::Shrink, + min_scale: 0.25, + max_scale: 10.0, + scale_step: 0.10, + handle, + } + } + + /// Sets the padding of the [`Viewer`]. + pub fn padding(mut self, units: u16) -> Self { + self.padding = units; + self + } + + /// Sets the width of the [`Viewer`]. + pub fn width(mut self, width: Length) -> Self { + self.width = width; + self + } + + /// Sets the height of the [`Viewer`]. + pub fn height(mut self, height: Length) -> Self { + self.height = height; + self + } + + /// Sets the max scale applied to the image of the [`Viewer`]. + /// + /// Default is `10.0` + pub fn max_scale(mut self, max_scale: f32) -> Self { + self.max_scale = max_scale; + self + } + + /// Sets the min scale applied to the image of the [`Viewer`]. + /// + /// Default is `0.25` + pub fn min_scale(mut self, min_scale: f32) -> Self { + self.min_scale = min_scale; + self + } + + /// Sets the percentage the image of the [`Viewer`] will be scaled by + /// when zoomed in / out. + /// + /// Default is `0.10` + pub fn scale_step(mut self, scale_step: f32) -> Self { + self.scale_step = scale_step; + self + } + + /// Returns the bounds of the underlying image, given the bounds of + /// the [`Viewer`]. Scaling will be applied and original aspect ratio + /// will be respected. + fn image_size<Renderer>(&self, renderer: &Renderer, bounds: Size) -> Size + where + Renderer: self::Renderer + image::Renderer, + { + let (width, height) = renderer.dimensions(&self.handle); + + let (width, height) = { + let dimensions = (width as f32, height as f32); + + let width_ratio = bounds.width / dimensions.0; + let height_ratio = bounds.height / dimensions.1; + + let ratio = width_ratio.min(height_ratio); + + let scale = self.state.scale; + + if ratio < 1.0 { + (dimensions.0 * ratio * scale, dimensions.1 * ratio * scale) + } else { + (dimensions.0 * scale, dimensions.1 * scale) + } + }; + + Size::new(width, height) + } +} + +impl<'a, Message, Renderer> Widget<Message, Renderer> for Viewer<'a> +where + Renderer: self::Renderer + image::Renderer, +{ + fn width(&self) -> Length { + self.width + } + + fn height(&self) -> Length { + self.height + } + + fn layout( + &self, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + let (width, height) = renderer.dimensions(&self.handle); + + let mut size = limits + .width(self.width) + .height(self.height) + .resolve(Size::new(width as f32, height as f32)); + + let expansion_size = if height > width { + self.width + } else { + self.height + }; + + // Only calculate viewport sizes if the images are constrained to a limited space. + // If they are Fill|Portion let them expand within their alotted space. + match expansion_size { + Length::Shrink | Length::Units(_) => { + let aspect_ratio = width as f32 / height as f32; + let viewport_aspect_ratio = size.width / size.height; + if viewport_aspect_ratio > aspect_ratio { + size.width = width as f32 * size.height / height as f32; + } else { + size.height = height as f32 * size.width / width as f32; + } + } + Length::Fill | Length::FillPortion(_) => {} + } + + layout::Node::new(size) + } + + fn on_event( + &mut self, + event: Event, + layout: Layout<'_>, + cursor_position: Point, + renderer: &Renderer, + _clipboard: &mut dyn Clipboard, + _messages: &mut Vec<Message>, + ) -> event::Status { + let bounds = layout.bounds(); + let is_mouse_over = bounds.contains(cursor_position); + + match event { + Event::Mouse(mouse::Event::WheelScrolled { delta }) + if is_mouse_over => + { + match delta { + mouse::ScrollDelta::Lines { y, .. } + | mouse::ScrollDelta::Pixels { y, .. } => { + let previous_scale = self.state.scale; + + if y < 0.0 && previous_scale > self.min_scale + || y > 0.0 && previous_scale < self.max_scale + { + self.state.scale = (if y > 0.0 { + self.state.scale * (1.0 + self.scale_step) + } else { + self.state.scale / (1.0 + self.scale_step) + }) + .max(self.min_scale) + .min(self.max_scale); + + let image_size = + self.image_size(renderer, bounds.size()); + + let factor = + self.state.scale / previous_scale - 1.0; + + let cursor_to_center = + cursor_position - bounds.center(); + + let adjustment = cursor_to_center * factor + + self.state.current_offset * factor; + + self.state.current_offset = Vector::new( + if image_size.width > bounds.width { + self.state.current_offset.x + adjustment.x + } else { + 0.0 + }, + if image_size.height > bounds.height { + self.state.current_offset.y + adjustment.y + } else { + 0.0 + }, + ); + } + } + } + + event::Status::Captured + } + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) + if is_mouse_over => + { + self.state.cursor_grabbed_at = Some(cursor_position); + self.state.starting_offset = self.state.current_offset; + + event::Status::Captured + } + Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) + if self.state.cursor_grabbed_at.is_some() => + { + self.state.cursor_grabbed_at = None; + + event::Status::Captured + } + Event::Mouse(mouse::Event::CursorMoved { position }) => { + if let Some(origin) = self.state.cursor_grabbed_at { + let image_size = self.image_size(renderer, bounds.size()); + + let hidden_width = (image_size.width - bounds.width / 2.0) + .max(0.0) + .round(); + + let hidden_height = (image_size.height + - bounds.height / 2.0) + .max(0.0) + .round(); + + let delta = position - origin; + + let x = if bounds.width < image_size.width { + (self.state.starting_offset.x - delta.x) + .min(hidden_width) + .max(-hidden_width) + } else { + 0.0 + }; + + let y = if bounds.height < image_size.height { + (self.state.starting_offset.y - delta.y) + .min(hidden_height) + .max(-hidden_height) + } else { + 0.0 + }; + + self.state.current_offset = Vector::new(x, y); + + event::Status::Captured + } else { + event::Status::Ignored + } + } + _ => event::Status::Ignored, + } + } + + fn draw( + &self, + renderer: &mut Renderer, + _defaults: &Renderer::Defaults, + layout: Layout<'_>, + cursor_position: Point, + _viewport: &Rectangle, + ) -> Renderer::Output { + let bounds = layout.bounds(); + + let image_size = self.image_size(renderer, bounds.size()); + + let translation = { + let image_top_left = Vector::new( + bounds.width / 2.0 - image_size.width / 2.0, + bounds.height / 2.0 - image_size.height / 2.0, + ); + + image_top_left - self.state.offset(bounds, image_size) + }; + + let is_mouse_over = bounds.contains(cursor_position); + + self::Renderer::draw( + renderer, + &self.state, + bounds, + image_size, + translation, + self.handle.clone(), + is_mouse_over, + ) + } + + fn hash_layout(&self, state: &mut Hasher) { + struct Marker; + std::any::TypeId::of::<Marker>().hash(state); + + self.width.hash(state); + self.height.hash(state); + self.padding.hash(state); + + self.handle.hash(state); + } +} + +/// The local state of a [`Viewer`]. +#[derive(Debug, Clone, Copy)] +pub struct State { + scale: f32, + starting_offset: Vector, + current_offset: Vector, + cursor_grabbed_at: Option<Point>, +} + +impl Default for State { + fn default() -> Self { + Self { + scale: 1.0, + starting_offset: Vector::default(), + current_offset: Vector::default(), + cursor_grabbed_at: None, + } + } +} + +impl State { + /// Creates a new [`State`]. + pub fn new() -> Self { + State::default() + } + + /// Returns the current offset of the [`State`], given the bounds + /// of the [`Viewer`] and its image. + fn offset(&self, bounds: Rectangle, image_size: Size) -> Vector { + let hidden_width = + (image_size.width - bounds.width / 2.0).max(0.0).round(); + + let hidden_height = + (image_size.height - bounds.height / 2.0).max(0.0).round(); + + Vector::new( + self.current_offset.x.min(hidden_width).max(-hidden_width), + self.current_offset.y.min(hidden_height).max(-hidden_height), + ) + } + + /// Returns if the cursor is currently grabbed by the [`Viewer`]. + pub fn is_cursor_grabbed(&self) -> bool { + self.cursor_grabbed_at.is_some() + } +} + +/// The renderer of an [`Viewer`]. +/// +/// Your [renderer] will need to implement this trait before being +/// able to use a [`Viewer`] in your user interface. +/// +/// [renderer]: crate::renderer +pub trait Renderer: crate::Renderer + Sized { + /// Draws the [`Viewer`]. + /// + /// It receives: + /// - the [`State`] of the [`Viewer`] + /// - the bounds of the [`Viewer`] widget + /// - the [`Size`] of the scaled [`Viewer`] image + /// - the translation of the clipped image + /// - the [`Handle`] to the underlying image + /// - whether the mouse is over the [`Viewer`] or not + /// + /// [`Handle`]: image::Handle + fn draw( + &mut self, + state: &State, + bounds: Rectangle, + image_size: Size, + translation: Vector, + handle: image::Handle, + is_mouse_over: bool, + ) -> Self::Output; +} + +impl<'a, Message, Renderer> From<Viewer<'a>> for Element<'a, Message, Renderer> +where + Renderer: 'a + self::Renderer + image::Renderer, + Message: 'a, +{ + fn from(viewer: Viewer<'a>) -> Element<'a, Message, Renderer> { + Element::new(viewer) + } +} diff --git a/native/src/widget/pane_grid.rs b/native/src/widget/pane_grid.rs index 5180fd3b..26a72409 100644 --- a/native/src/widget/pane_grid.rs +++ b/native/src/widget/pane_grid.rs @@ -6,8 +6,7 @@ //! The [`pane_grid` example] showcases how to use a [`PaneGrid`] with resizing, //! drag and drop, and hotkey support. //! -//! [`pane_grid` example]: https://github.com/hecrj/iced/tree/0.1/examples/pane_grid -//! [`PaneGrid`]: struct.PaneGrid.html +//! [`pane_grid` example]: https://github.com/hecrj/iced/tree/0.3/examples/pane_grid mod axis; mod configuration; mod content; @@ -25,12 +24,19 @@ pub use direction::Direction; pub use node::Node; pub use pane::Pane; pub use split::Split; -pub use state::{Focus, State}; +pub use state::State; pub use title_bar::TitleBar; +use crate::container; +use crate::event::{self, Event}; +use crate::layout; +use crate::mouse; +use crate::overlay; +use crate::row; +use crate::touch; use crate::{ - container, keyboard, layout, mouse, overlay, row, text, Clipboard, Element, - Event, Hasher, Layout, Length, Point, Rectangle, Size, Vector, Widget, + Clipboard, Element, Hasher, Layout, Length, Point, Rectangle, Size, Vector, + Widget, }; /// A collection of panes distributed using either vertical or horizontal splits @@ -73,7 +79,7 @@ use crate::{ /// let (mut state, _) = pane_grid::State::new(PaneState::SomePane); /// /// let pane_grid = -/// PaneGrid::new(&mut state, |pane, state, focus| { +/// PaneGrid::new(&mut state, |pane, state| { /// pane_grid::Content::new(match state { /// PaneState::SomePane => Text::new("This is some pane"), /// PaneState::AnotherKindOfPane => Text::new("This is another kind of pane"), @@ -82,9 +88,6 @@ use crate::{ /// .on_drag(Message::PaneDragged) /// .on_resize(10, Message::PaneResized); /// ``` -/// -/// [`PaneGrid`]: struct.PaneGrid.html -/// [`State`]: struct.State.html #[allow(missing_debug_implementations)] pub struct PaneGrid<'a, Message, Renderer: self::Renderer> { state: &'a mut state::Internal, @@ -92,10 +95,10 @@ pub struct PaneGrid<'a, Message, Renderer: self::Renderer> { width: Length, height: Length, spacing: u16, - modifier_keys: keyboard::ModifiersState, + on_click: Option<Box<dyn Fn(Pane) -> Message + 'a>>, on_drag: Option<Box<dyn Fn(DragEvent) -> Message + 'a>>, on_resize: Option<(u16, Box<dyn Fn(ResizeEvent) -> Message + 'a>)>, - on_key_press: Option<Box<dyn Fn(KeyPressEvent) -> Option<Message> + 'a>>, + style: <Renderer as self::Renderer>::Style, } impl<'a, Message, Renderer> PaneGrid<'a, Message, Renderer> @@ -106,37 +109,15 @@ where /// /// The view function will be called to display each [`Pane`] present in the /// [`State`]. - /// - /// [`PaneGrid`]: struct.PaneGrid.html - /// [`State`]: struct.State.html - /// [`Pane`]: struct.Pane.html pub fn new<T>( state: &'a mut State<T>, - view: impl Fn( - Pane, - &'a mut T, - Option<Focus>, - ) -> Content<'a, Message, Renderer>, + view: impl Fn(Pane, &'a mut T) -> Content<'a, Message, Renderer>, ) -> Self { let elements = { - let action = state.internal.action(); - let current_focus = action.focus(); - state .panes .iter_mut() - .map(move |(pane, pane_state)| { - let focus = match current_focus { - Some((focused_pane, focus)) - if *pane == focused_pane => - { - Some(focus) - } - _ => None, - }; - - (*pane, view(*pane, pane_state, focus)) - }) + .map(|(pane, pane_state)| (*pane, view(*pane, pane_state))) .collect() }; @@ -146,59 +127,43 @@ where width: Length::Fill, height: Length::Fill, spacing: 0, - modifier_keys: keyboard::ModifiersState { - control: true, - ..Default::default() - }, + on_click: None, on_drag: None, on_resize: None, - on_key_press: None, + style: Default::default(), } } /// Sets the width of the [`PaneGrid`]. - /// - /// [`PaneGrid`]: struct.PaneGrid.html pub fn width(mut self, width: Length) -> Self { self.width = width; self } /// Sets the height of the [`PaneGrid`]. - /// - /// [`PaneGrid`]: struct.PaneGrid.html pub fn height(mut self, height: Length) -> Self { self.height = height; self } /// Sets the spacing _between_ the panes of the [`PaneGrid`]. - /// - /// [`PaneGrid`]: struct.PaneGrid.html pub fn spacing(mut self, units: u16) -> Self { self.spacing = units; self } - /// Sets the modifier keys of the [`PaneGrid`]. - /// - /// The modifier keys will need to be pressed to trigger key events. - /// - /// The default modifier key is `Ctrl`. - /// - /// [`PaneGrid`]: struct.PaneGrid.html - pub fn modifier_keys( - mut self, - modifier_keys: keyboard::ModifiersState, - ) -> Self { - self.modifier_keys = modifier_keys; + /// Sets the message that will be produced when a [`Pane`] of the + /// [`PaneGrid`] is clicked. + pub fn on_click<F>(mut self, f: F) -> Self + where + F: 'a + Fn(Pane) -> Message, + { + self.on_click = Some(Box::new(f)); self } /// Enables the drag and drop interactions of the [`PaneGrid`], which will /// use the provided function to produce messages. - /// - /// [`PaneGrid`]: struct.PaneGrid.html pub fn on_drag<F>(mut self, f: F) -> Self where F: 'a + Fn(DragEvent) -> Message, @@ -216,8 +181,6 @@ where /// The grabbable area of a split will have a length of `spacing + leeway`, /// properly centered. In other words, a length of /// `(spacing + leeway) / 2.0` on either side of the split line. - /// - /// [`PaneGrid`]: struct.PaneGrid.html pub fn on_resize<F>(mut self, leeway: u16, f: F) -> Self where F: 'a + Fn(ResizeEvent) -> Message, @@ -226,28 +189,12 @@ where self } - /// Captures hotkey interactions with the [`PaneGrid`], using the provided - /// function to produce messages. - /// - /// The function will be called when: - /// - a [`Pane`] is focused - /// - a key is pressed - /// - all the modifier keys are pressed - /// - /// If the function returns `None`, the key press event will be discarded - /// without producing any message. - /// - /// This method is particularly useful to implement hotkey interactions. - /// For instance, you can use it to enable splitting, swapping, or resizing - /// panes by pressing combinations of keys. - /// - /// [`PaneGrid`]: struct.PaneGrid.html - /// [`Pane`]: struct.Pane.html - pub fn on_key_press<F>(mut self, f: F) -> Self - where - F: 'a + Fn(KeyPressEvent) -> Option<Message>, - { - self.on_key_press = Some(Box::new(f)); + /// Sets the style of the [`PaneGrid`]. + pub fn style( + mut self, + style: impl Into<<Renderer as self::Renderer>::Style>, + ) -> Self { + self.style = style.into(); self } } @@ -268,24 +215,20 @@ where ); if let Some(((pane, content), layout)) = clicked_region.next() { - match &self.on_drag { - Some(on_drag) => { - if content.can_be_picked_at(layout, cursor_position) { - let pane_position = layout.position(); + if let Some(on_click) = &self.on_click { + messages.push(on_click(*pane)); + } - let origin = cursor_position - - Vector::new(pane_position.x, pane_position.y); + if let Some(on_drag) = &self.on_drag { + if content.can_be_picked_at(layout, cursor_position) { + let pane_position = layout.position(); - self.state.pick_pane(pane, origin); + let origin = cursor_position + - Vector::new(pane_position.x, pane_position.y); - messages - .push(on_drag(DragEvent::Picked { pane: *pane })); - } else { - self.state.focus(pane); - } - } - None => { - self.state.focus(pane); + self.state.pick_pane(pane, origin); + + messages.push(on_drag(DragEvent::Picked { pane: *pane })); } } } @@ -296,7 +239,7 @@ where layout: Layout<'_>, cursor_position: Point, messages: &mut Vec<Message>, - ) { + ) -> event::Status { if let Some((_, on_resize)) = &self.on_resize { if let Some((split, _)) = self.state.picked_split() { let bounds = layout.bounds(); @@ -323,85 +266,55 @@ where }; messages.push(on_resize(ResizeEvent { split, ratio })); + + return event::Status::Captured; } } } + + event::Status::Ignored } } /// An event produced during a drag and drop interaction of a [`PaneGrid`]. -/// -/// [`PaneGrid`]: struct.PaneGrid.html #[derive(Debug, Clone, Copy)] pub enum DragEvent { /// A [`Pane`] was picked for dragging. - /// - /// [`Pane`]: struct.Pane.html Picked { /// The picked [`Pane`]. - /// - /// [`Pane`]: struct.Pane.html pane: Pane, }, /// A [`Pane`] was dropped on top of another [`Pane`]. - /// - /// [`Pane`]: struct.Pane.html Dropped { /// The picked [`Pane`]. - /// - /// [`Pane`]: struct.Pane.html pane: Pane, /// The [`Pane`] where the picked one was dropped on. - /// - /// [`Pane`]: struct.Pane.html target: Pane, }, /// A [`Pane`] was picked and then dropped outside of other [`Pane`] /// boundaries. - /// - /// [`Pane`]: struct.Pane.html Canceled { /// The picked [`Pane`]. - /// - /// [`Pane`]: struct.Pane.html pane: Pane, }, } /// An event produced during a resize interaction of a [`PaneGrid`]. -/// -/// [`PaneGrid`]: struct.PaneGrid.html #[derive(Debug, Clone, Copy)] pub struct ResizeEvent { /// The [`Split`] that is being dragged for resizing. - /// - /// [`Split`]: struct.Split.html pub split: Split, /// The new ratio of the [`Split`]. /// /// The ratio is a value in [0, 1], representing the exact position of a /// [`Split`] between two panes. - /// - /// [`Split`]: struct.Split.html pub ratio: f32, } -/// An event produced during a key press interaction of a [`PaneGrid`]. -/// -/// [`PaneGrid`]: struct.PaneGrid.html -#[derive(Debug, Clone, Copy)] -pub struct KeyPressEvent { - /// The key that was pressed. - pub key_code: keyboard::KeyCode, - - /// The state of the modifier keys when the key was pressed. - pub modifiers: keyboard::ModifiersState, -} - impl<'a, Message, Renderer> Widget<Message, Renderer> for PaneGrid<'a, Message, Renderer> where @@ -449,45 +362,41 @@ where event: Event, layout: Layout<'_>, cursor_position: Point, - messages: &mut Vec<Message>, renderer: &Renderer, - clipboard: Option<&dyn Clipboard>, - ) { - match event { - Event::Mouse(mouse_event) => match mouse_event { - mouse::Event::ButtonPressed(mouse::Button::Left) => { - let bounds = layout.bounds(); - - if bounds.contains(cursor_position) { - match self.on_resize { - Some((leeway, _)) => { - let relative_cursor = Point::new( - cursor_position.x - bounds.x, - cursor_position.y - bounds.y, - ); - - let splits = self.state.split_regions( - f32::from(self.spacing), - Size::new(bounds.width, bounds.height), - ); + clipboard: &mut dyn Clipboard, + messages: &mut Vec<Message>, + ) -> event::Status { + let mut event_status = event::Status::Ignored; - let clicked_split = hovered_split( - splits.iter(), - f32::from(self.spacing + leeway), - relative_cursor, - ); + match event { + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerPressed { .. }) => { + let bounds = layout.bounds(); - if let Some((split, axis)) = clicked_split { - self.state.pick_split(&split, axis); - } else { - self.click_pane( - layout, - cursor_position, - messages, - ); - } - } - None => { + if bounds.contains(cursor_position) { + event_status = event::Status::Captured; + + match self.on_resize { + Some((leeway, _)) => { + let relative_cursor = Point::new( + cursor_position.x - bounds.x, + cursor_position.y - bounds.y, + ); + + let splits = self.state.split_regions( + f32::from(self.spacing), + Size::new(bounds.width, bounds.height), + ); + + let clicked_split = hovered_split( + splits.iter(), + f32::from(self.spacing + leeway), + relative_cursor, + ); + + if let Some((split, axis, _)) = clicked_split { + self.state.pick_split(&split, axis); + } else { self.click_pane( layout, cursor_position, @@ -495,91 +404,73 @@ where ); } } - } else { - // TODO: Encode cursor availability in the type system - if cursor_position.x > 0.0 && cursor_position.y > 0.0 { - self.state.unfocus(); + None => { + self.click_pane(layout, cursor_position, messages); } } } - mouse::Event::ButtonReleased(mouse::Button::Left) => { - if let Some((pane, _)) = self.state.picked_pane() { - self.state.focus(&pane); - - if let Some(on_drag) = &self.on_drag { - let mut dropped_region = self - .elements - .iter() - .zip(layout.children()) - .filter(|(_, layout)| { + } + Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerLifted { .. }) + | Event::Touch(touch::Event::FingerLost { .. }) => { + if let Some((pane, _)) = self.state.picked_pane() { + if let Some(on_drag) = &self.on_drag { + let mut dropped_region = + self.elements.iter().zip(layout.children()).filter( + |(_, layout)| { layout.bounds().contains(cursor_position) - }); - - let event = match dropped_region.next() { - Some(((target, _), _)) if pane != *target => { - DragEvent::Dropped { - pane, - target: *target, - } + }, + ); + + let event = match dropped_region.next() { + Some(((target, _), _)) if pane != *target => { + DragEvent::Dropped { + pane, + target: *target, } - _ => DragEvent::Canceled { pane }, - }; + } + _ => DragEvent::Canceled { pane }, + }; - messages.push(on_drag(event)); - } - } else if self.state.picked_split().is_some() { - self.state.drop_split(); + messages.push(on_drag(event)); } + + self.state.idle(); + + event_status = event::Status::Captured; + } else if self.state.picked_split().is_some() { + self.state.idle(); + + event_status = event::Status::Captured; } - mouse::Event::CursorMoved { .. } => { + } + Event::Mouse(mouse::Event::CursorMoved { .. }) + | Event::Touch(touch::Event::FingerMoved { .. }) => { + event_status = self.trigger_resize(layout, cursor_position, messages); - } - _ => {} - }, - Event::Keyboard(keyboard_event) => { - match keyboard_event { - keyboard::Event::KeyPressed { - modifiers, - key_code, - } => { - if let Some(on_key_press) = &self.on_key_press { - // TODO: Discard when event is captured - if let Some(_) = self.state.active_pane() { - if modifiers.matches(self.modifier_keys) { - if let Some(message) = - on_key_press(KeyPressEvent { - key_code, - modifiers, - }) - { - messages.push(message); - } - } - } - } - } - _ => {} - } } _ => {} } - if self.state.picked_pane().is_none() { - { - self.elements.iter_mut().zip(layout.children()).for_each( - |((_, pane), layout)| { - pane.on_event( - event.clone(), - layout, - cursor_position, - messages, - renderer, - clipboard, - ) - }, - ); - } - } + let picked_pane = self.state.picked_pane().map(|(pane, _)| pane); + + self.elements + .iter_mut() + .zip(layout.children()) + .map(|((pane, content), layout)| { + let is_picked = picked_pane == Some(*pane); + + content.on_event( + event.clone(), + layout, + cursor_position, + renderer, + clipboard, + messages, + is_picked, + ) + }) + .fold(event_status, event::Status::merge) } fn draw( @@ -588,10 +479,28 @@ where defaults: &Renderer::Defaults, layout: Layout<'_>, cursor_position: Point, + viewport: &Rectangle, ) -> Renderer::Output { let picked_split = self .state .picked_split() + .and_then(|(split, axis)| { + let bounds = layout.bounds(); + + let splits = self + .state + .split_regions(f32::from(self.spacing), bounds.size()); + + let (_axis, region, ratio) = splits.get(&split)?; + + let region = axis.split_line_bounds( + *region, + *ratio, + f32::from(self.spacing), + ); + + Some((axis, region + Vector::new(bounds.x, bounds.y), true)) + }) .or_else(|| match self.on_resize { Some((leeway, _)) => { let bounds = layout.bounds(); @@ -605,15 +514,20 @@ where .state .split_regions(f32::from(self.spacing), bounds.size()); - hovered_split( + let (_split, axis, region) = hovered_split( splits.iter(), f32::from(self.spacing + leeway), relative_cursor, - ) + )?; + + Some(( + axis, + region + Vector::new(bounds.x, bounds.y), + false, + )) } None => None, - }) - .map(|(_, axis)| axis); + }); self::Renderer::draw( renderer, @@ -622,7 +536,9 @@ where self.state.picked_pane(), picked_split, layout, + &self.style, cursor_position, + viewport, ) } @@ -658,11 +574,11 @@ where /// Your [renderer] will need to implement this trait before being /// able to use a [`PaneGrid`] in your user interface. /// -/// [`PaneGrid`]: struct.PaneGrid.html -/// [renderer]: ../../renderer/index.html -pub trait Renderer: - crate::Renderer + container::Renderer + text::Renderer + Sized -{ +/// [renderer]: crate::renderer +pub trait Renderer: crate::Renderer + container::Renderer + Sized { + /// The style supported by this renderer. + type Style: Default; + /// Draws a [`PaneGrid`]. /// /// It receives: @@ -671,18 +587,16 @@ pub trait Renderer: /// - the [`Axis`] that is currently being resized /// - the [`Layout`] of the [`PaneGrid`] and its elements /// - the cursor position - /// - /// [`PaneGrid`]: struct.PaneGrid.html - /// [`Pane`]: struct.Pane.html - /// [`Layout`]: ../layout/struct.Layout.html fn draw<Message>( &mut self, defaults: &Self::Defaults, content: &[(Pane, Content<'_, Message, Self>)], dragging: Option<(Pane, Point)>, - resizing: Option<Axis>, + resizing: Option<(Axis, Rectangle, bool)>, layout: Layout<'_>, + style: &<Self as self::Renderer>::Style, cursor_position: Point, + viewport: &Rectangle, ) -> Self::Output; /// Draws a [`Pane`]. @@ -692,17 +606,15 @@ pub trait Renderer: /// - the [`Content`] of the [`Pane`] /// - the [`Layout`] of the [`Pane`] and its elements /// - the cursor position - /// - /// [`Pane`]: struct.Pane.html - /// [`Layout`]: ../layout/struct.Layout.html fn draw_pane<Message>( &mut self, defaults: &Self::Defaults, bounds: Rectangle, - style: &Self::Style, + style: &<Self as container::Renderer>::Style, title_bar: Option<(&TitleBar<'_, Message, Self>, Layout<'_>)>, body: (&Element<'_, Message, Self>, Layout<'_>), cursor_position: Point, + viewport: &Rectangle, ) -> Self::Output; /// Draws a [`TitleBar`]. @@ -710,23 +622,18 @@ pub trait Renderer: /// It receives: /// - the bounds, style of the [`TitleBar`] /// - the style of the [`TitleBar`] - /// - the title of the [`TitleBar`] with its size, font, and bounds - /// - the controls of the [`TitleBar`] with their [`Layout`+, if any + /// - the content of the [`TitleBar`] with its layout + /// - the controls of the [`TitleBar`] with their [`Layout`], if any /// - the cursor position - /// - /// [`TitleBar`]: struct.TitleBar.html - /// [`Layout`]: ../layout/struct.Layout.html fn draw_title_bar<Message>( &mut self, defaults: &Self::Defaults, bounds: Rectangle, - style: &Self::Style, - title: &str, - title_size: u16, - title_font: Self::Font, - title_bounds: Rectangle, + style: &<Self as container::Renderer>::Style, + content: (&Element<'_, Message, Self>, Layout<'_>), controls: Option<(&Element<'_, Message, Self>, Layout<'_>)>, cursor_position: Point, + viewport: &Rectangle, ) -> Self::Output; } @@ -750,14 +657,14 @@ fn hovered_split<'a>( splits: impl Iterator<Item = (&'a Split, &'a (Axis, Rectangle, f32))>, spacing: f32, cursor_position: Point, -) -> Option<(Split, Axis)> { +) -> Option<(Split, Axis, Rectangle)> { splits .filter_map(|(split, (axis, region, ratio))| { let bounds = axis.split_line_bounds(*region, *ratio, f32::from(spacing)); if bounds.contains(cursor_position) { - Some((*split, *axis)) + Some((*split, *axis, bounds)) } else { None } diff --git a/native/src/widget/pane_grid/configuration.rs b/native/src/widget/pane_grid/configuration.rs index 1fed98b7..4c43826e 100644 --- a/native/src/widget/pane_grid/configuration.rs +++ b/native/src/widget/pane_grid/configuration.rs @@ -2,7 +2,7 @@ use crate::pane_grid::Axis; /// The arrangement of a [`PaneGrid`]. /// -/// [`PaneGrid`]: struct.PaneGrid.html +/// [`PaneGrid`]: crate::pane_grid::PaneGrid #[derive(Debug, Clone)] pub enum Configuration<T> { /// A split of the available space. @@ -13,18 +13,14 @@ pub enum Configuration<T> { /// The ratio of the split in [0.0, 1.0]. ratio: f32, - /// The left/top [`Content`] of the split. - /// - /// [`Configuration`]: enum.Node.html + /// The left/top [`Configuration`] of the split. a: Box<Configuration<T>>, - /// The right/bottom [`Content`] of the split. - /// - /// [`Configuration`]: enum.Node.html + /// The right/bottom [`Configuration`] of the split. b: Box<Configuration<T>>, }, /// A [`Pane`]. /// - /// [`Pane`]: struct.Pane.html + /// [`Pane`]: crate::pane_grid::Pane Pane(T), } diff --git a/native/src/widget/pane_grid/content.rs b/native/src/widget/pane_grid/content.rs index 1d339b75..bac9fdd4 100644 --- a/native/src/widget/pane_grid/content.rs +++ b/native/src/widget/pane_grid/content.rs @@ -1,17 +1,18 @@ use crate::container; +use crate::event::{self, Event}; use crate::layout; use crate::overlay; use crate::pane_grid::{self, TitleBar}; -use crate::{Clipboard, Element, Event, Hasher, Layout, Point, Size}; +use crate::{Clipboard, Element, Hasher, Layout, Point, Rectangle, Size}; /// The content of a [`Pane`]. /// -/// [`Pane`]: struct.Pane.html +/// [`Pane`]: crate::widget::pane_grid::Pane #[allow(missing_debug_implementations)] pub struct Content<'a, Message, Renderer: pane_grid::Renderer> { title_bar: Option<TitleBar<'a, Message, Renderer>>, body: Element<'a, Message, Renderer>, - style: Renderer::Style, + style: <Renderer as container::Renderer>::Style, } impl<'a, Message, Renderer> Content<'a, Message, Renderer> @@ -19,20 +20,15 @@ where Renderer: pane_grid::Renderer, { /// Creates a new [`Content`] with the provided body. - /// - /// [`Content`]: struct.Content.html pub fn new(body: impl Into<Element<'a, Message, Renderer>>) -> Self { Self { title_bar: None, body: body.into(), - style: Renderer::Style::default(), + style: Default::default(), } } /// Sets the [`TitleBar`] of this [`Content`]. - /// - /// [`TitleBar`]: struct.TitleBar.html - /// [`Content`]: struct.Content.html pub fn title_bar( mut self, title_bar: TitleBar<'a, Message, Renderer>, @@ -41,10 +37,11 @@ where self } - /// Sets the style of the [`TitleBar`]. - /// - /// [`TitleBar`]: struct.TitleBar.html - pub fn style(mut self, style: impl Into<Renderer::Style>) -> Self { + /// Sets the style of the [`Content`]. + pub fn style( + mut self, + style: impl Into<<Renderer as container::Renderer>::Style>, + ) -> Self { self.style = style.into(); self } @@ -56,15 +53,14 @@ where { /// Draws the [`Content`] with the provided [`Renderer`] and [`Layout`]. /// - /// [`Content`]: struct.Content.html - /// [`Renderer`]: trait.Renderer.html - /// [`Layout`]: ../layout/struct.Layout.html + /// [`Renderer`]: crate::widget::pane_grid::Renderer pub fn draw( &self, renderer: &mut Renderer, defaults: &Renderer::Defaults, layout: Layout<'_>, cursor_position: Point, + viewport: &Rectangle, ) -> Renderer::Output { if let Some(title_bar) = &self.title_bar { let mut children = layout.children(); @@ -78,6 +74,7 @@ where Some((title_bar, title_bar_layout)), (&self.body, body_layout), cursor_position, + viewport, ) } else { renderer.draw_pane( @@ -87,15 +84,13 @@ where None, (&self.body, layout), cursor_position, + viewport, ) } } /// Returns whether the [`Content`] with the given [`Layout`] can be picked /// at the provided cursor position. - /// - /// [`Content`]: struct.Content.html - /// [`Layout`]: ../layout/struct.Layout.html pub fn can_be_picked_at( &self, layout: Layout<'_>, @@ -151,20 +146,23 @@ where event: Event, layout: Layout<'_>, cursor_position: Point, - messages: &mut Vec<Message>, renderer: &Renderer, - clipboard: Option<&dyn Clipboard>, - ) { + clipboard: &mut dyn Clipboard, + messages: &mut Vec<Message>, + is_picked: bool, + ) -> event::Status { + let mut event_status = event::Status::Ignored; + let body_layout = if let Some(title_bar) = &mut self.title_bar { let mut children = layout.children(); - title_bar.on_event( + event_status = title_bar.on_event( event.clone(), children.next().unwrap(), cursor_position, - messages, renderer, clipboard, + messages, ); children.next().unwrap() @@ -172,14 +170,20 @@ where layout }; - self.body.on_event( - event, - body_layout, - cursor_position, - messages, - renderer, - clipboard, - ); + let body_status = if is_picked { + event::Status::Ignored + } else { + self.body.on_event( + event, + body_layout, + cursor_position, + renderer, + clipboard, + messages, + ) + }; + + event_status.merge(body_status) } pub(crate) fn hash_layout(&self, state: &mut Hasher) { @@ -194,18 +198,17 @@ where &mut self, layout: Layout<'_>, ) -> Option<overlay::Element<'_, Message, Renderer>> { - let body_layout = if self.title_bar.is_some() { + if let Some(title_bar) = self.title_bar.as_mut() { let mut children = layout.children(); + let title_bar_layout = children.next()?; - // Overlays only allowed in the pane body, for now at least. - let _title_bar_layout = children.next(); - - children.next()? + match title_bar.overlay(title_bar_layout) { + Some(overlay) => Some(overlay), + None => self.body.overlay(children.next()?), + } } else { - layout - }; - - self.body.overlay(body_layout) + self.body.overlay(layout) + } } } diff --git a/native/src/widget/pane_grid/node.rs b/native/src/widget/pane_grid/node.rs index cbfd8a43..84714e00 100644 --- a/native/src/widget/pane_grid/node.rs +++ b/native/src/widget/pane_grid/node.rs @@ -3,21 +3,16 @@ use crate::{ Rectangle, Size, }; -use std::collections::HashMap; +use std::collections::BTreeMap; /// A layout node of a [`PaneGrid`]. /// -/// [`PaneGrid`]: struct.PaneGrid.html +/// [`PaneGrid`]: crate::widget::PaneGrid #[derive(Debug, Clone)] pub enum Node { /// The region of this [`Node`] is split into two. - /// - /// [`Node`]: enum.Node.html Split { /// The [`Split`] of this [`Node`]. - /// - /// [`Split`]: struct.Split.html - /// [`Node`]: enum.Node.html id: Split, /// The direction of the split. @@ -27,26 +22,17 @@ pub enum Node { ratio: f32, /// The left/top [`Node`] of the split. - /// - /// [`Node`]: enum.Node.html a: Box<Node>, /// The right/bottom [`Node`] of the split. - /// - /// [`Node`]: enum.Node.html b: Box<Node>, }, /// The region of this [`Node`] is taken by a [`Pane`]. - /// - /// [`Pane`]: struct.Pane.html Pane(Pane), } impl Node { /// Returns an iterator over each [`Split`] in this [`Node`]. - /// - /// [`Split`]: struct.Split.html - /// [`Node`]: enum.Node.html pub fn splits(&self) -> impl Iterator<Item = &Split> { let mut unvisited_nodes = vec![self]; @@ -69,15 +55,12 @@ impl Node { /// Returns the rectangular region for each [`Pane`] in the [`Node`] given /// the spacing between panes and the total available space. - /// - /// [`Pane`]: struct.Pane.html - /// [`Node`]: enum.Node.html pub fn pane_regions( &self, spacing: f32, size: Size, - ) -> HashMap<Pane, Rectangle> { - let mut regions = HashMap::new(); + ) -> BTreeMap<Pane, Rectangle> { + let mut regions = BTreeMap::new(); self.compute_regions( spacing, @@ -96,15 +79,12 @@ impl Node { /// Returns the axis, rectangular region, and ratio for each [`Split`] in /// the [`Node`] given the spacing between panes and the total available /// space. - /// - /// [`Split`]: struct.Split.html - /// [`Node`]: enum.Node.html pub fn split_regions( &self, spacing: f32, size: Size, - ) -> HashMap<Split, (Axis, Rectangle, f32)> { - let mut splits = HashMap::new(); + ) -> BTreeMap<Split, (Axis, Rectangle, f32)> { + let mut splits = BTreeMap::new(); self.compute_splits( spacing, @@ -211,7 +191,7 @@ impl Node { &self, spacing: f32, current: &Rectangle, - regions: &mut HashMap<Pane, Rectangle>, + regions: &mut BTreeMap<Pane, Rectangle>, ) { match self { Node::Split { @@ -232,7 +212,7 @@ impl Node { &self, spacing: f32, current: &Rectangle, - splits: &mut HashMap<Split, (Axis, Rectangle, f32)>, + splits: &mut BTreeMap<Split, (Axis, Rectangle, f32)>, ) { match self { Node::Split { diff --git a/native/src/widget/pane_grid/pane.rs b/native/src/widget/pane_grid/pane.rs index f9866407..d6fbab83 100644 --- a/native/src/widget/pane_grid/pane.rs +++ b/native/src/widget/pane_grid/pane.rs @@ -1,5 +1,5 @@ /// A rectangular region in a [`PaneGrid`] used to display widgets. /// -/// [`PaneGrid`]: struct.PaneGrid.html -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +/// [`PaneGrid`]: crate::widget::PaneGrid +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct Pane(pub(super) usize); diff --git a/native/src/widget/pane_grid/split.rs b/native/src/widget/pane_grid/split.rs index d020c510..8132272a 100644 --- a/native/src/widget/pane_grid/split.rs +++ b/native/src/widget/pane_grid/split.rs @@ -1,5 +1,5 @@ /// A divider that splits a region in a [`PaneGrid`] into two different panes. /// -/// [`PaneGrid`]: struct.PaneGrid.html -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +/// [`PaneGrid`]: crate::widget::PaneGrid +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct Split(pub(super) usize); diff --git a/native/src/widget/pane_grid/state.rs b/native/src/widget/pane_grid/state.rs index fb59c846..fb96f89f 100644 --- a/native/src/widget/pane_grid/state.rs +++ b/native/src/widget/pane_grid/state.rs @@ -3,7 +3,7 @@ use crate::{ Hasher, Point, Rectangle, Size, }; -use std::collections::HashMap; +use std::collections::{BTreeMap, HashMap}; /// The state of a [`PaneGrid`]. /// @@ -15,41 +15,19 @@ use std::collections::HashMap; /// provided to the view function of [`PaneGrid::new`] for displaying each /// [`Pane`]. /// -/// [`PaneGrid`]: struct.PaneGrid.html -/// [`PaneGrid::new`]: struct.PaneGrid.html#method.new -/// [`Pane`]: struct.Pane.html -/// [`Split`]: struct.Split.html -/// [`State`]: struct.State.html +/// [`PaneGrid`]: crate::widget::PaneGrid +/// [`PaneGrid::new`]: crate::widget::PaneGrid::new #[derive(Debug, Clone)] pub struct State<T> { pub(super) panes: HashMap<Pane, T>, pub(super) internal: Internal, } -/// The current focus of a [`Pane`]. -/// -/// [`Pane`]: struct.Pane.html -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum Focus { - /// The [`Pane`] is just focused. - /// - /// [`Pane`]: struct.Pane.html - Idle, - - /// The [`Pane`] is being dragged. - /// - /// [`Pane`]: struct.Pane.html - Dragging, -} - impl<T> State<T> { /// Creates a new [`State`], initializing the first pane with the provided /// state. /// /// Alongside the [`State`], it returns the first [`Pane`] identifier. - /// - /// [`State`]: struct.State.html - /// [`Pane`]: struct.Pane.html pub fn new(first_pane_state: T) -> (Self, Pane) { ( Self::with_configuration(Configuration::Pane(first_pane_state)), @@ -58,9 +36,6 @@ impl<T> State<T> { } /// Creates a new [`State`] with the given [`Configuration`]. - /// - /// [`State`]: struct.State.html - /// [`Configuration`]: enum.Configuration.html pub fn with_configuration(config: impl Into<Configuration<T>>) -> Self { let mut panes = HashMap::new(); @@ -72,95 +47,46 @@ impl<T> State<T> { internal: Internal { layout, last_id, - action: Action::Idle { focus: None }, + action: Action::Idle, }, } } /// Returns the total amount of panes in the [`State`]. - /// - /// [`State`]: struct.State.html pub fn len(&self) -> usize { self.panes.len() } /// Returns the internal state of the given [`Pane`], if it exists. - /// - /// [`Pane`]: struct.Pane.html pub fn get(&self, pane: &Pane) -> Option<&T> { self.panes.get(pane) } /// Returns the internal state of the given [`Pane`] with mutability, if it /// exists. - /// - /// [`Pane`]: struct.Pane.html pub fn get_mut(&mut self, pane: &Pane) -> Option<&mut T> { self.panes.get_mut(pane) } /// Returns an iterator over all the panes of the [`State`], alongside its /// internal state. - /// - /// [`State`]: struct.State.html pub fn iter(&self) -> impl Iterator<Item = (&Pane, &T)> { self.panes.iter() } /// Returns a mutable iterator over all the panes of the [`State`], /// alongside its internal state. - /// - /// [`State`]: struct.State.html pub fn iter_mut(&mut self) -> impl Iterator<Item = (&Pane, &mut T)> { self.panes.iter_mut() } /// Returns the layout of the [`State`]. - /// - /// [`State`]: struct.State.html pub fn layout(&self) -> &Node { &self.internal.layout } - /// Returns the focused [`Pane`] of the [`State`], if there is one. - /// - /// [`Pane`]: struct.Pane.html - /// [`State`]: struct.State.html - pub fn focused(&self) -> Option<Pane> { - self.internal.focused_pane() - } - - /// Returns the active [`Pane`] of the [`State`], if there is one. - /// - /// A [`Pane`] is active if it is focused and is __not__ being dragged. - /// - /// [`Pane`]: struct.Pane.html - /// [`State`]: struct.State.html - pub fn active(&self) -> Option<Pane> { - self.internal.active_pane() - } - /// Returns the adjacent [`Pane`] of another [`Pane`] in the given /// direction, if there is one. - /// - /// ## Example - /// You can combine this with [`State::active`] to find the pane that is - /// adjacent to the current active one, and then swap them. For instance: - /// - /// ``` - /// # use iced_native::pane_grid; - /// # - /// # let (mut state, _) = pane_grid::State::new(()); - /// # - /// if let Some(active) = state.active() { - /// if let Some(adjacent) = state.adjacent(&active, pane_grid::Direction::Right) { - /// state.swap(&active, &adjacent); - /// } - /// } - /// ``` - /// - /// [`Pane`]: struct.Pane.html - /// [`State::active`]: struct.State.html#method.active pub fn adjacent(&self, pane: &Pane, direction: Direction) -> Option<Pane> { let regions = self .internal @@ -194,25 +120,8 @@ impl<T> State<T> { Some(*pane) } - /// Focuses the given [`Pane`]. - /// - /// [`Pane`]: struct.Pane.html - pub fn focus(&mut self, pane: &Pane) { - self.internal.focus(pane); - } - - /// Unfocuses the current focused [`Pane`]. - /// - /// [`Pane`]: struct.Pane.html - pub fn unfocus(&mut self) { - self.internal.unfocus(); - } - /// Splits the given [`Pane`] into two in the given [`Axis`] and /// initializing the new [`Pane`] with the provided internal state. - /// - /// [`Pane`]: struct.Pane.html - /// [`Axis`]: enum.Axis.html pub fn split( &mut self, axis: Axis, @@ -236,7 +145,6 @@ impl<T> State<T> { node.split(new_split, axis, new_pane); let _ = self.panes.insert(new_pane, state); - self.focus(&new_pane); Some((new_pane, new_split)) } @@ -246,9 +154,8 @@ impl<T> State<T> { /// If you want to swap panes on drag and drop in your [`PaneGrid`], you /// will need to call this method when handling a [`DragEvent`]. /// - /// [`State`]: struct.State.html - /// [`PaneGrid`]: struct.PaneGrid.html - /// [`DragEvent`]: struct.DragEvent.html + /// [`PaneGrid`]: crate::widget::PaneGrid + /// [`DragEvent`]: crate::widget::pane_grid::DragEvent pub fn swap(&mut self, a: &Pane, b: &Pane) { self.internal.layout.update(&|node| match node { Node::Split { .. } => {} @@ -270,20 +177,17 @@ impl<T> State<T> { /// If you want to enable resize interactions in your [`PaneGrid`], you will /// need to call this method when handling a [`ResizeEvent`]. /// - /// [`Split`]: struct.Split.html - /// [`PaneGrid`]: struct.PaneGrid.html - /// [`ResizeEvent`]: struct.ResizeEvent.html + /// [`PaneGrid`]: crate::widget::PaneGrid + /// [`ResizeEvent`]: crate::widget::pane_grid::ResizeEvent pub fn resize(&mut self, split: &Split, ratio: f32) { let _ = self.internal.layout.resize(split, ratio); } - /// Closes the given [`Pane`] and returns its internal state, if it exists. - /// - /// [`Pane`]: struct.Pane.html - pub fn close(&mut self, pane: &Pane) -> Option<T> { + /// Closes the given [`Pane`] and returns its internal state and its closest + /// sibling, if it exists. + pub fn close(&mut self, pane: &Pane) -> Option<(T, Pane)> { if let Some(sibling) = self.internal.layout.remove(pane) { - self.focus(&sibling); - self.panes.remove(pane) + self.panes.remove(pane).map(|state| (state, sibling)) } else { None } @@ -329,52 +233,12 @@ pub struct Internal { #[derive(Debug, Clone, Copy, PartialEq)] pub enum Action { - Idle { - focus: Option<Pane>, - }, - Dragging { - pane: Pane, - origin: Point, - focus: Option<Pane>, - }, - Resizing { - split: Split, - axis: Axis, - focus: Option<Pane>, - }, -} - -impl Action { - pub fn focus(&self) -> Option<(Pane, Focus)> { - match self { - Action::Idle { focus } | Action::Resizing { focus, .. } => { - focus.map(|pane| (pane, Focus::Idle)) - } - Action::Dragging { pane, .. } => Some((*pane, Focus::Dragging)), - } - } + Idle, + Dragging { pane: Pane, origin: Point }, + Resizing { split: Split, axis: Axis }, } impl Internal { - pub fn action(&self) -> Action { - self.action - } - - pub fn focused_pane(&self) -> Option<Pane> { - match self.action { - Action::Idle { focus } => focus, - Action::Dragging { focus, .. } => focus, - Action::Resizing { focus, .. } => focus, - } - } - - pub fn active_pane(&self) -> Option<Pane> { - match self.action { - Action::Idle { focus } => focus, - _ => None, - } - } - pub fn picked_pane(&self) -> Option<(Pane, Point)> { match self.action { Action::Dragging { pane, origin, .. } => Some((pane, origin)), @@ -393,7 +257,7 @@ impl Internal { &self, spacing: f32, size: Size, - ) -> HashMap<Pane, Rectangle> { + ) -> BTreeMap<Pane, Rectangle> { self.layout.pane_regions(spacing, size) } @@ -401,21 +265,14 @@ impl Internal { &self, spacing: f32, size: Size, - ) -> HashMap<Split, (Axis, Rectangle, f32)> { + ) -> BTreeMap<Split, (Axis, Rectangle, f32)> { self.layout.split_regions(spacing, size) } - pub fn focus(&mut self, pane: &Pane) { - self.action = Action::Idle { focus: Some(*pane) }; - } - pub fn pick_pane(&mut self, pane: &Pane, origin: Point) { - let focus = self.focused_pane(); - self.action = Action::Dragging { pane: *pane, origin, - focus, }; } @@ -426,26 +283,14 @@ impl Internal { return; } - let focus = self.action.focus().map(|(pane, _)| pane); - self.action = Action::Resizing { split: *split, axis, - focus, }; } - pub fn drop_split(&mut self) { - match self.action { - Action::Resizing { focus, .. } => { - self.action = Action::Idle { focus }; - } - _ => {} - } - } - - pub fn unfocus(&mut self) { - self.action = Action::Idle { focus: None }; + pub fn idle(&mut self) { + self.action = Action::Idle; } pub fn hash_layout(&self, hasher: &mut Hasher) { diff --git a/native/src/widget/pane_grid/title_bar.rs b/native/src/widget/pane_grid/title_bar.rs index 9dfb9ae4..070010f8 100644 --- a/native/src/widget/pane_grid/title_bar.rs +++ b/native/src/widget/pane_grid/title_bar.rs @@ -1,51 +1,43 @@ +use crate::container; +use crate::event::{self, Event}; use crate::layout; +use crate::overlay; use crate::pane_grid; use crate::{ - Clipboard, Element, Event, Hasher, Layout, Point, Rectangle, Size, + Clipboard, Element, Hasher, Layout, Padding, Point, Rectangle, Size, }; /// The title bar of a [`Pane`]. /// -/// [`Pane`]: struct.Pane.html +/// [`Pane`]: crate::widget::pane_grid::Pane #[allow(missing_debug_implementations)] pub struct TitleBar<'a, Message, Renderer: pane_grid::Renderer> { - title: String, - title_size: Option<u16>, + content: Element<'a, Message, Renderer>, controls: Option<Element<'a, Message, Renderer>>, - padding: u16, + padding: Padding, always_show_controls: bool, - style: Renderer::Style, + style: <Renderer as container::Renderer>::Style, } impl<'a, Message, Renderer> TitleBar<'a, Message, Renderer> where Renderer: pane_grid::Renderer, { - /// Creates a new [`TitleBar`] with the given title. - /// - /// [`TitleBar`]: struct.TitleBar.html - pub fn new(title: impl Into<String>) -> Self { + /// Creates a new [`TitleBar`] with the given content. + pub fn new<E>(content: E) -> Self + where + E: Into<Element<'a, Message, Renderer>>, + { Self { - title: title.into(), - title_size: None, + content: content.into(), controls: None, - padding: 0, + padding: Padding::ZERO, always_show_controls: false, - style: Renderer::Style::default(), + style: Default::default(), } } - /// Sets the size of the title of the [`TitleBar`]. - /// - /// [`TitleBar`]: struct.TitleBar.html - pub fn title_size(mut self, size: u16) -> Self { - self.title_size = Some(size); - self - } - /// Sets the controls of the [`TitleBar`]. - /// - /// [`TitleBar`]: struct.TitleBar.html pub fn controls( mut self, controls: impl Into<Element<'a, Message, Renderer>>, @@ -54,18 +46,17 @@ where self } - /// Sets the padding of the [`TitleBar`]. - /// - /// [`TitleBar`]: struct.TitleBar.html - pub fn padding(mut self, units: u16) -> Self { - self.padding = units; + /// Sets the [`Padding`] of the [`TitleBar`]. + pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self { + self.padding = padding.into(); self } /// Sets the style of the [`TitleBar`]. - /// - /// [`TitleBar`]: struct.TitleBar.html - pub fn style(mut self, style: impl Into<Renderer::Style>) -> Self { + pub fn style( + mut self, + style: impl Into<<Renderer as container::Renderer>::Style>, + ) -> Self { self.style = style.into(); self } @@ -76,9 +67,8 @@ where /// By default, the controls are only visible when the [`Pane`] of this /// [`TitleBar`] is hovered. /// - /// [`TitleBar`]: struct.TitleBar.html - /// [`controls`]: struct.TitleBar.html#method.controls - /// [`Pane`]: struct.Pane.html + /// [`controls`]: Self::controls + /// [`Pane`]: crate::widget::pane_grid::Pane pub fn always_show_controls(mut self) -> Self { self.always_show_controls = true; self @@ -91,70 +81,49 @@ where { /// Draws the [`TitleBar`] with the provided [`Renderer`] and [`Layout`]. /// - /// [`TitleBar`]: struct.TitleBar.html - /// [`Renderer`]: trait.Renderer.html - /// [`Layout`]: ../layout/struct.Layout.html + /// [`Renderer`]: crate::widget::pane_grid::Renderer pub fn draw( &self, renderer: &mut Renderer, defaults: &Renderer::Defaults, layout: Layout<'_>, cursor_position: Point, + viewport: &Rectangle, show_controls: bool, ) -> Renderer::Output { let mut children = layout.children(); let padded = children.next().unwrap(); - if let Some(controls) = &self.controls { - let mut children = padded.children(); - let title_layout = children.next().unwrap(); + let mut children = padded.children(); + let title_layout = children.next().unwrap(); + + let controls = if let Some(controls) = &self.controls { let controls_layout = children.next().unwrap(); - let (title_bounds, controls) = - if show_controls || self.always_show_controls { - (title_layout.bounds(), Some((controls, controls_layout))) - } else { - ( - Rectangle { - width: padded.bounds().width, - ..title_layout.bounds() - }, - None, - ) - }; - - renderer.draw_title_bar( - defaults, - layout.bounds(), - &self.style, - &self.title, - self.title_size.unwrap_or(renderer.default_size()), - Renderer::Font::default(), - title_bounds, - controls, - cursor_position, - ) + if show_controls || self.always_show_controls { + Some((controls, controls_layout)) + } else { + None + } } else { - renderer.draw_title_bar::<()>( - defaults, - layout.bounds(), - &self.style, - &self.title, - self.title_size.unwrap_or(renderer.default_size()), - Renderer::Font::default(), - padded.bounds(), - None, - cursor_position, - ) - } + None + }; + + renderer.draw_title_bar( + defaults, + layout.bounds(), + &self.style, + (&self.content, title_layout), + controls, + cursor_position, + viewport, + ) } /// Returns whether the mouse cursor is over the pick area of the /// [`TitleBar`] or not. /// /// The whole [`TitleBar`] is a pick area, except its controls. - /// - /// [`TitleBar`]: struct.TitleBar.html pub fn is_over_pick_area( &self, layout: Layout<'_>, @@ -163,15 +132,16 @@ where if layout.bounds().contains(cursor_position) { let mut children = layout.children(); let padded = children.next().unwrap(); + let mut children = padded.children(); + let title_layout = children.next().unwrap(); if self.controls.is_some() { - let mut children = padded.children(); - let _ = children.next().unwrap(); let controls_layout = children.next().unwrap(); !controls_layout.bounds().contains(cursor_position) + && !title_layout.bounds().contains(cursor_position) } else { - true + !title_layout.bounds().contains(cursor_position) } } else { false @@ -181,9 +151,12 @@ where pub(crate) fn hash_layout(&self, hasher: &mut Hasher) { use std::hash::Hash; - self.title.hash(hasher); - self.title_size.hash(hasher); + self.content.hash_layout(hasher); self.padding.hash(hasher); + + if let Some(controls) = &self.controls { + controls.hash_layout(hasher); + } } pub(crate) fn layout( @@ -191,19 +164,13 @@ where renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { - let padding = f32::from(self.padding); - let limits = limits.pad(padding); + let limits = limits.pad(self.padding); let max_size = limits.max(); - let title_size = self.title_size.unwrap_or(renderer.default_size()); - let title_font = Renderer::Font::default(); - - let (title_width, title_height) = renderer.measure( - &self.title, - title_size, - title_font, - Size::new(f32::INFINITY, max_size.height), - ); + let title_layout = self + .content + .layout(renderer, &layout::Limits::new(Size::ZERO, max_size)); + let title_size = title_layout.size(); let mut node = if let Some(controls) = &self.controls { let mut controls_layout = controls @@ -212,16 +179,8 @@ where let controls_size = controls_layout.size(); let space_before_controls = max_size.width - controls_size.width; - let mut title_layout = layout::Node::new(Size::new( - title_width.min(space_before_controls), - title_height, - )); - - let title_size = title_layout.size(); let height = title_size.height.max(controls_size.height); - title_layout - .move_to(Point::new(0.0, (height - title_size.height) / 2.0)); controls_layout.move_to(Point::new(space_before_controls, 0.0)); layout::Node::with_children( @@ -229,12 +188,18 @@ where vec![title_layout, controls_layout], ) } else { - layout::Node::new(Size::new(max_size.width, title_height)) + layout::Node::with_children( + Size::new(max_size.width, title_size.height), + vec![title_layout], + ) }; - node.move_to(Point::new(padding, padding)); + node.move_to(Point::new( + self.padding.left.into(), + self.padding.top.into(), + )); - layout::Node::with_children(node.size().pad(padding), vec![node]) + layout::Node::with_children(node.size().pad(self.padding), vec![node]) } pub(crate) fn on_event( @@ -242,26 +207,63 @@ where event: Event, layout: Layout<'_>, cursor_position: Point, - messages: &mut Vec<Message>, renderer: &Renderer, - clipboard: Option<&dyn Clipboard>, - ) { - if let Some(controls) = &mut self.controls { - let mut children = layout.children(); - let padded = children.next().unwrap(); + clipboard: &mut dyn Clipboard, + messages: &mut Vec<Message>, + ) -> event::Status { + let mut children = layout.children(); + let padded = children.next().unwrap(); - let mut children = padded.children(); - let _ = children.next(); + let mut children = padded.children(); + let title_layout = children.next().unwrap(); + + let control_status = if let Some(controls) = &mut self.controls { let controls_layout = children.next().unwrap(); controls.on_event( - event, + event.clone(), controls_layout, cursor_position, - messages, renderer, clipboard, - ); - } + messages, + ) + } else { + event::Status::Ignored + }; + + let title_status = self.content.on_event( + event, + title_layout, + cursor_position, + renderer, + clipboard, + messages, + ); + + control_status.merge(title_status) + } + + pub(crate) fn overlay( + &mut self, + layout: Layout<'_>, + ) -> Option<overlay::Element<'_, Message, Renderer>> { + let mut children = layout.children(); + let padded = children.next()?; + + let mut children = padded.children(); + let title_layout = children.next()?; + + let Self { + content, controls, .. + } = self; + + content.overlay(title_layout).or_else(move || { + controls.as_mut().and_then(|controls| { + let controls_layout = children.next()?; + + controls.overlay(controls_layout) + }) + }) } } diff --git a/native/src/widget/pick_list.rs b/native/src/widget/pick_list.rs index 04478225..d7792000 100644 --- a/native/src/widget/pick_list.rs +++ b/native/src/widget/pick_list.rs @@ -1,9 +1,16 @@ //! Display a dropdown list of selectable values. +use crate::event::{self, Event}; +use crate::keyboard; +use crate::layout; +use crate::mouse; +use crate::overlay; +use crate::overlay::menu::{self, Menu}; +use crate::scrollable; +use crate::text; +use crate::touch; use crate::{ - layout, mouse, overlay, - overlay::menu::{self, Menu}, - scrollable, text, Clipboard, Element, Event, Hasher, Layout, Length, Point, - Rectangle, Size, Widget, + Clipboard, Element, Hasher, Layout, Length, Padding, Point, Rectangle, + Size, Widget, }; use std::borrow::Cow; @@ -14,25 +21,26 @@ where [T]: ToOwned<Owned = Vec<T>>, { menu: &'a mut menu::State, + keyboard_modifiers: &'a mut keyboard::Modifiers, is_open: &'a mut bool, hovered_option: &'a mut Option<usize>, last_selection: &'a mut Option<T>, on_selected: Box<dyn Fn(T) -> Message>, options: Cow<'a, [T]>, + placeholder: Option<String>, selected: Option<T>, width: Length, - padding: u16, + padding: Padding, text_size: Option<u16>, font: Renderer::Font, style: <Renderer as self::Renderer>::Style, } /// The local state of a [`PickList`]. -/// -/// [`PickList`]: struct.PickList.html #[derive(Debug, Clone)] pub struct State<T> { menu: menu::State, + keyboard_modifiers: keyboard::Modifiers, is_open: bool, hovered_option: Option<usize>, last_selection: Option<T>, @@ -42,6 +50,7 @@ impl<T> Default for State<T> { fn default() -> Self { Self { menu: menu::State::default(), + keyboard_modifiers: keyboard::Modifiers::default(), is_open: bool::default(), hovered_option: Option::default(), last_selection: Option::default(), @@ -52,15 +61,12 @@ impl<T> Default for State<T> { impl<'a, T: 'a, Message, Renderer: self::Renderer> PickList<'a, T, Message, Renderer> where - T: ToString, + T: ToString + Eq, [T]: ToOwned<Owned = Vec<T>>, { /// Creates a new [`PickList`] with the given [`State`], a list of options, /// the current selected value, and the message to produce when an option is /// selected. - /// - /// [`PickList`]: struct.PickList.html - /// [`State`]: struct.State.html pub fn new( state: &'a mut State<T>, options: impl Into<Cow<'a, [T]>>, @@ -69,6 +75,7 @@ where ) -> Self { let State { menu, + keyboard_modifiers, is_open, hovered_option, last_selection, @@ -76,11 +83,13 @@ where Self { menu, + keyboard_modifiers, is_open, hovered_option, last_selection, on_selected: Box::new(on_selected), options: options.into(), + placeholder: None, selected, width: Length::Shrink, text_size: None, @@ -90,41 +99,37 @@ where } } + /// Sets the placeholder of the [`PickList`]. + pub fn placeholder(mut self, placeholder: impl Into<String>) -> Self { + self.placeholder = Some(placeholder.into()); + self + } + /// Sets the width of the [`PickList`]. - /// - /// [`PickList`]: struct.PickList.html pub fn width(mut self, width: Length) -> Self { self.width = width; self } - /// Sets the padding of the [`PickList`]. - /// - /// [`PickList`]: struct.PickList.html - pub fn padding(mut self, padding: u16) -> Self { - self.padding = padding; + /// Sets the [`Padding`] of the [`PickList`]. + pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self { + self.padding = padding.into(); self } /// Sets the text size of the [`PickList`]. - /// - /// [`PickList`]: struct.PickList.html pub fn text_size(mut self, size: u16) -> Self { self.text_size = Some(size); self } /// Sets the font of the [`PickList`]. - /// - /// [`PickList`]: struct.PickList.html pub fn font(mut self, font: Renderer::Font) -> Self { self.font = font; self } /// Sets the style of the [`PickList`]. - /// - /// [`PickList`]: struct.PickList.html pub fn style( mut self, style: impl Into<<Renderer as self::Renderer>::Style>, @@ -143,7 +148,7 @@ where Renderer: self::Renderer + scrollable::Renderer + 'a, { fn width(&self) -> Length { - Length::Shrink + self.width } fn height(&self) -> Length { @@ -160,27 +165,37 @@ where let limits = limits .width(self.width) .height(Length::Shrink) - .pad(f32::from(self.padding)); + .pad(self.padding); let text_size = self.text_size.unwrap_or(renderer.default_size()); + let font = self.font; let max_width = match self.width { Length::Shrink => { + let measure = |label: &str| -> u32 { + let (width, _) = renderer.measure( + label, + text_size, + font, + Size::new(f32::INFINITY, f32::INFINITY), + ); + + width.round() as u32 + }; + let labels = self.options.iter().map(ToString::to_string); - labels - .map(|label| { - let (width, _) = renderer.measure( - &label, - text_size, - Renderer::Font::default(), - Size::new(f32::INFINITY, f32::INFINITY), - ); - - width.round() as u32 - }) - .max() - .unwrap_or(100) + let labels_width = + labels.map(|label| measure(&label)).max().unwrap_or(100); + + let placeholder_width = self + .placeholder + .as_ref() + .map(String::as_str) + .map(measure) + .unwrap_or(100); + + labels_width.max(placeholder_width) } _ => 0, }; @@ -189,11 +204,11 @@ where let intrinsic = Size::new( max_width as f32 + f32::from(text_size) - + f32::from(self.padding), + + f32::from(self.padding.left), f32::from(text_size), ); - limits.resolve(intrinsic).pad(f32::from(self.padding)) + limits.resolve(intrinsic).pad(self.padding) }; layout::Node::new(size) @@ -204,6 +219,8 @@ where match self.width { Length::Shrink => { + self.placeholder.hash(state); + self.options .iter() .map(ToString::to_string) @@ -220,16 +237,19 @@ where event: Event, layout: Layout<'_>, cursor_position: Point, - messages: &mut Vec<Message>, _renderer: &Renderer, - _clipboard: Option<&dyn Clipboard>, - ) { + _clipboard: &mut dyn Clipboard, + messages: &mut Vec<Message>, + ) -> event::Status { match event { - Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => { - if *self.is_open { + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerPressed { .. }) => { + let event_status = if *self.is_open { // TODO: Encode cursor availability in the type system *self.is_open = cursor_position.x < 0.0 || cursor_position.y < 0.0; + + event::Status::Captured } else if layout.bounds().contains(cursor_position) { let selected = self.selected.as_ref(); @@ -238,15 +258,65 @@ where .options .iter() .position(|option| Some(option) == selected); - } + + event::Status::Captured + } else { + event::Status::Ignored + }; if let Some(last_selection) = self.last_selection.take() { messages.push((self.on_selected)(last_selection)); *self.is_open = false; + + event::Status::Captured + } else { + event_status + } + } + Event::Mouse(mouse::Event::WheelScrolled { + delta: mouse::ScrollDelta::Lines { y, .. }, + }) if self.keyboard_modifiers.command() + && layout.bounds().contains(cursor_position) + && !*self.is_open => + { + fn find_next<'a, T: PartialEq>( + selected: &'a T, + mut options: impl Iterator<Item = &'a T>, + ) -> Option<&'a T> { + let _ = options.find(|&option| option == selected); + + options.next() } + + let next_option = if y < 0.0 { + if let Some(selected) = self.selected.as_ref() { + find_next(selected, self.options.iter()) + } else { + self.options.first() + } + } else if y > 0.0 { + if let Some(selected) = self.selected.as_ref() { + find_next(selected, self.options.iter().rev()) + } else { + self.options.last() + } + } else { + None + }; + + if let Some(next_option) = next_option { + messages.push((self.on_selected)(next_option.clone())); + } + + event::Status::Captured + } + Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => { + *self.keyboard_modifiers = modifiers; + + event::Status::Ignored } - _ => {} + _ => event::Status::Ignored, } } @@ -256,12 +326,14 @@ where _defaults: &Renderer::Defaults, layout: Layout<'_>, cursor_position: Point, + _viewport: &Rectangle, ) -> Renderer::Output { self::Renderer::draw( renderer, layout.bounds(), cursor_position, self.selected.as_ref().map(ToString::to_string), + self.placeholder.as_ref().map(String::as_str), self.padding, self.text_size.unwrap_or(renderer.default_size()), self.font, @@ -303,36 +375,27 @@ where /// Your [renderer] will need to implement this trait before being /// able to use a [`PickList`] in your user interface. /// -/// [`PickList`]: struct.PickList.html -/// [renderer]: ../../renderer/index.html +/// [renderer]: crate::renderer pub trait Renderer: text::Renderer + menu::Renderer { /// The default padding of a [`PickList`]. - /// - /// [`PickList`]: struct.PickList.html - const DEFAULT_PADDING: u16; + const DEFAULT_PADDING: Padding; /// The [`PickList`] style supported by this renderer. - /// - /// [`PickList`]: struct.PickList.html type Style: Default; /// Returns the style of the [`Menu`] of the [`PickList`]. - /// - /// [`Menu`]: ../../overlay/menu/struct.Menu.html - /// [`PickList`]: struct.PickList.html fn menu_style( style: &<Self as Renderer>::Style, ) -> <Self as menu::Renderer>::Style; /// Draws a [`PickList`]. - /// - /// [`PickList`]: struct.PickList.html fn draw( &mut self, bounds: Rectangle, cursor_position: Point, selected: Option<String>, - padding: u16, + placeholder: Option<&str>, + padding: Padding, text_size: u16, font: Self::Font, style: &<Self as Renderer>::Style, diff --git a/native/src/widget/progress_bar.rs b/native/src/widget/progress_bar.rs index 5ab76d47..d294f198 100644 --- a/native/src/widget/progress_bar.rs +++ b/native/src/widget/progress_bar.rs @@ -33,8 +33,6 @@ impl<Renderer: self::Renderer> ProgressBar<Renderer> { /// It expects: /// * an inclusive range of possible values /// * the current value of the [`ProgressBar`] - /// - /// [`ProgressBar`]: struct.ProgressBar.html pub fn new(range: RangeInclusive<f32>, value: f32) -> Self { ProgressBar { value: value.max(*range.start()).min(*range.end()), @@ -46,24 +44,18 @@ impl<Renderer: self::Renderer> ProgressBar<Renderer> { } /// Sets the width of the [`ProgressBar`]. - /// - /// [`ProgressBar`]: struct.ProgressBar.html pub fn width(mut self, width: Length) -> Self { self.width = width; self } /// Sets the height of the [`ProgressBar`]. - /// - /// [`ProgressBar`]: struct.ProgressBar.html pub fn height(mut self, height: Length) -> Self { self.height = Some(height); self } /// Sets the style of the [`ProgressBar`]. - /// - /// [`ProgressBar`]: struct.ProgressBar.html pub fn style(mut self, style: impl Into<Renderer::Style>) -> Self { self.style = style.into(); self @@ -104,6 +96,7 @@ where _defaults: &Renderer::Defaults, layout: Layout<'_>, _cursor_position: Point, + _viewport: &Rectangle, ) -> Renderer::Output { renderer.draw( layout.bounds(), @@ -127,15 +120,12 @@ where /// Your [renderer] will need to implement this trait before being /// able to use a [`ProgressBar`] in your user interface. /// -/// [`ProgressBar`]: struct.ProgressBar.html -/// [renderer]: ../../renderer/index.html +/// [renderer]: crate::renderer pub trait Renderer: crate::Renderer { /// The style supported by this renderer. type Style: Default; /// The default height of a [`ProgressBar`]. - /// - /// [`ProgressBar`]: struct.ProgressBar.html const DEFAULT_HEIGHT: u16; /// Draws a [`ProgressBar`]. @@ -146,8 +136,6 @@ pub trait Renderer: crate::Renderer { /// * the current value of the [`ProgressBar`] /// * maybe a specific background of the [`ProgressBar`] /// * maybe a specific active color of the [`ProgressBar`] - /// - /// [`ProgressBar`]: struct.ProgressBar.html fn draw( &self, bounds: Rectangle, diff --git a/native/src/widget/radio.rs b/native/src/widget/radio.rs index 5b8d00e9..dee82d1f 100644 --- a/native/src/widget/radio.rs +++ b/native/src/widget/radio.rs @@ -1,12 +1,17 @@ //! Create choices using radio buttons. +use std::hash::Hash; + +use crate::event::{self, Event}; +use crate::mouse; +use crate::row; +use crate::text; +use crate::touch; +use crate::{layout, Color}; use crate::{ - layout, mouse, row, text, Align, Clipboard, Element, Event, Hasher, - HorizontalAlignment, Layout, Length, Point, Rectangle, Row, Text, - VerticalAlignment, Widget, + Align, Clipboard, Element, Hasher, HorizontalAlignment, Layout, Length, + Point, Rectangle, Row, Text, VerticalAlignment, Widget, }; -use std::hash::Hash; - /// A circular button representing a choice. /// /// # Example @@ -42,11 +47,15 @@ pub struct Radio<Message, Renderer: self::Renderer + text::Renderer> { size: u16, spacing: u16, text_size: Option<u16>, + text_color: Option<Color>, + font: Renderer::Font, style: Renderer::Style, } impl<Message, Renderer: self::Renderer + text::Renderer> Radio<Message, Renderer> +where + Message: Clone, { /// Creates a new [`Radio`] button. /// @@ -56,8 +65,6 @@ impl<Message, Renderer: self::Renderer + text::Renderer> /// * the current selected value /// * a function that will be called when the [`Radio`] is selected. It /// receives the value of the radio and must produce a `Message`. - /// - /// [`Radio`]: struct.Radio.html pub fn new<F, V>( value: V, label: impl Into<String>, @@ -76,45 +83,49 @@ impl<Message, Renderer: self::Renderer + text::Renderer> size: <Renderer as self::Renderer>::DEFAULT_SIZE, spacing: Renderer::DEFAULT_SPACING, //15 text_size: None, + text_color: None, + font: Default::default(), style: Renderer::Style::default(), } } /// Sets the size of the [`Radio`] button. - /// - /// [`Radio`]: struct.Radio.html pub fn size(mut self, size: u16) -> Self { self.size = size; self } /// Sets the width of the [`Radio`] button. - /// - /// [`Radio`]: struct.Radio.html pub fn width(mut self, width: Length) -> Self { self.width = width; self } /// Sets the spacing between the [`Radio`] button and the text. - /// - /// [`Radio`]: struct.Radio.html pub fn spacing(mut self, spacing: u16) -> Self { self.spacing = spacing; self } /// Sets the text size of the [`Radio`] button. - /// - /// [`Radio`]: struct.Radio.html pub fn text_size(mut self, text_size: u16) -> Self { self.text_size = Some(text_size); self } + /// Sets the text color of the [`Radio`] button. + pub fn text_color(mut self, color: Color) -> Self { + self.text_color = Some(color); + self + } + + /// Sets the text font of the [`Radio`] button. + pub fn font(mut self, font: Renderer::Font) -> Self { + self.font = font; + self + } + /// Sets the style of the [`Radio`] button. - /// - /// [`Radio`]: struct.Radio.html pub fn style(mut self, style: impl Into<Renderer::Style>) -> Self { self.style = style.into(); self @@ -123,8 +134,8 @@ impl<Message, Renderer: self::Renderer + text::Renderer> impl<Message, Renderer> Widget<Message, Renderer> for Radio<Message, Renderer> where - Renderer: self::Renderer + text::Renderer + row::Renderer, Message: Clone, + Renderer: self::Renderer + text::Renderer + row::Renderer, { fn width(&self) -> Length { self.width @@ -161,18 +172,23 @@ where event: Event, layout: Layout<'_>, cursor_position: Point, - messages: &mut Vec<Message>, _renderer: &Renderer, - _clipboard: Option<&dyn Clipboard>, - ) { + _clipboard: &mut dyn Clipboard, + messages: &mut Vec<Message>, + ) -> event::Status { match event { - Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => { + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerPressed { .. }) => { if layout.bounds().contains(cursor_position) { messages.push(self.on_click.clone()); + + return event::Status::Captured; } } _ => {} } + + event::Status::Ignored } fn draw( @@ -181,6 +197,7 @@ where defaults: &Renderer::Defaults, layout: Layout<'_>, cursor_position: Point, + _viewport: &Rectangle, ) -> Renderer::Output { let bounds = layout.bounds(); let mut children = layout.children(); @@ -195,8 +212,8 @@ where label_layout.bounds(), &self.label, self.text_size.unwrap_or(renderer.default_size()), - Default::default(), - None, + self.font, + self.text_color, HorizontalAlignment::Left, VerticalAlignment::Center, ); @@ -226,20 +243,15 @@ where /// Your [renderer] will need to implement this trait before being /// able to use a [`Radio`] button in your user interface. /// -/// [`Radio`]: struct.Radio.html -/// [renderer]: ../../renderer/index.html +/// [renderer]: crate::renderer pub trait Renderer: crate::Renderer { /// The style supported by this renderer. type Style: Default; /// The default size of a [`Radio`] button. - /// - /// [`Radio`]: struct.Radio.html const DEFAULT_SIZE: u16; /// The default spacing of a [`Radio`] button. - /// - /// [`Radio`]: struct.Radio.html const DEFAULT_SPACING: u16; /// Draws a [`Radio`] button. @@ -249,8 +261,6 @@ pub trait Renderer: crate::Renderer { /// * whether the [`Radio`] is selected or not /// * whether the mouse is over the [`Radio`] or not /// * the drawn label of the [`Radio`] - /// - /// [`Radio`]: struct.Radio.html fn draw( &mut self, bounds: Rectangle, @@ -264,8 +274,8 @@ pub trait Renderer: crate::Renderer { impl<'a, Message, Renderer> From<Radio<Message, Renderer>> for Element<'a, Message, Renderer> where - Renderer: 'a + self::Renderer + row::Renderer + text::Renderer, Message: 'a + Clone, + Renderer: 'a + self::Renderer + row::Renderer + text::Renderer, { fn from(radio: Radio<Message, Renderer>) -> Element<'a, Message, Renderer> { Element::new(radio) diff --git a/native/src/widget/row.rs b/native/src/widget/row.rs index 2b6db224..9ebc9145 100644 --- a/native/src/widget/row.rs +++ b/native/src/widget/row.rs @@ -1,22 +1,20 @@ //! Distribute content horizontally. -use std::hash::Hash; - +use crate::event::{self, Event}; +use crate::layout; +use crate::overlay; use crate::{ - layout, overlay, Align, Clipboard, Element, Event, Hasher, Layout, Length, - Point, Widget, + Align, Clipboard, Element, Hasher, Layout, Length, Padding, Point, + Rectangle, Widget, }; +use std::hash::Hash; use std::u32; /// A container that distributes its contents horizontally. -/// -/// A [`Row`] will try to fill the horizontal space of its container. -/// -/// [`Row`]: struct.Row.html #[allow(missing_debug_implementations)] pub struct Row<'a, Message, Renderer> { spacing: u16, - padding: u16, + padding: Padding, width: Length, height: Length, max_width: u32, @@ -27,21 +25,17 @@ pub struct Row<'a, Message, Renderer> { impl<'a, Message, Renderer> Row<'a, Message, Renderer> { /// Creates an empty [`Row`]. - /// - /// [`Row`]: struct.Row.html pub fn new() -> Self { Self::with_children(Vec::new()) } /// Creates a [`Row`] with the given elements. - /// - /// [`Row`]: struct.Row.html pub fn with_children( children: Vec<Element<'a, Message, Renderer>>, ) -> Self { Row { spacing: 0, - padding: 0, + padding: Padding::ZERO, width: Length::Shrink, height: Length::Shrink, max_width: u32::MAX, @@ -61,58 +55,43 @@ impl<'a, Message, Renderer> Row<'a, Message, Renderer> { self } - /// Sets the padding of the [`Row`]. - /// - /// [`Row`]: struct.Row.html - pub fn padding(mut self, units: u16) -> Self { - self.padding = units; + /// Sets the [`Padding`] of the [`Row`]. + pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self { + self.padding = padding.into(); self } /// Sets the width of the [`Row`]. - /// - /// [`Row`]: struct.Row.html pub fn width(mut self, width: Length) -> Self { self.width = width; self } /// Sets the height of the [`Row`]. - /// - /// [`Row`]: struct.Row.html pub fn height(mut self, height: Length) -> Self { self.height = height; self } /// Sets the maximum width of the [`Row`]. - /// - /// [`Row`]: struct.Row.html pub fn max_width(mut self, max_width: u32) -> Self { self.max_width = max_width; self } /// Sets the maximum height of the [`Row`]. - /// - /// [`Row`]: struct.Row.html pub fn max_height(mut self, max_height: u32) -> Self { self.max_height = max_height; self } /// Sets the vertical alignment of the contents of the [`Row`] . - /// - /// [`Row`]: struct.Row.html pub fn align_items(mut self, align: Align) -> Self { self.align_items = align; self } /// Adds an [`Element`] to the [`Row`]. - /// - /// [`Element`]: ../struct.Element.html - /// [`Row`]: struct.Row.html pub fn push<E>(mut self, child: E) -> Self where E: Into<Element<'a, Message, Renderer>>, @@ -150,7 +129,7 @@ where layout::flex::Axis::Horizontal, renderer, &limits, - self.padding as f32, + self.padding, self.spacing as f32, self.align_items, &self.children, @@ -162,22 +141,24 @@ where event: Event, layout: Layout<'_>, cursor_position: Point, - messages: &mut Vec<Message>, renderer: &Renderer, - clipboard: Option<&dyn Clipboard>, - ) { - self.children.iter_mut().zip(layout.children()).for_each( - |(child, layout)| { + clipboard: &mut dyn Clipboard, + messages: &mut Vec<Message>, + ) -> event::Status { + self.children + .iter_mut() + .zip(layout.children()) + .map(|(child, layout)| { child.widget.on_event( event.clone(), layout, cursor_position, - messages, renderer, clipboard, + messages, ) - }, - ); + }) + .fold(event::Status::Ignored, event::Status::merge) } fn draw( @@ -186,8 +167,15 @@ where defaults: &Renderer::Defaults, layout: Layout<'_>, cursor_position: Point, + viewport: &Rectangle, ) -> Renderer::Output { - renderer.draw(defaults, &self.children, layout, cursor_position) + renderer.draw( + defaults, + &self.children, + layout, + cursor_position, + viewport, + ) } fn hash_layout(&self, state: &mut Hasher) { @@ -200,7 +188,7 @@ where self.max_height.hash(state); self.align_items.hash(state); self.spacing.hash(state); - self.spacing.hash(state); + self.padding.hash(state); for child in &self.children { child.widget.hash_layout(state); @@ -224,8 +212,7 @@ where /// Your [renderer] will need to implement this trait before being /// able to use a [`Row`] in your user interface. /// -/// [`Row`]: struct.Row.html -/// [renderer]: ../../renderer/index.html +/// [renderer]: crate::renderer pub trait Renderer: crate::Renderer + Sized { /// Draws a [`Row`]. /// @@ -233,15 +220,13 @@ pub trait Renderer: crate::Renderer + Sized { /// - the children of the [`Row`] /// - the [`Layout`] of the [`Row`] and its children /// - the cursor position - /// - /// [`Row`]: struct.Row.html - /// [`Layout`]: ../layout/struct.Layout.html fn draw<Message>( &mut self, defaults: &Self::Defaults, children: &[Element<'_, Message, Self>], layout: Layout<'_>, cursor_position: Point, + viewport: &Rectangle, ) -> Self::Output; } diff --git a/native/src/widget/rule.rs b/native/src/widget/rule.rs index 25cec53b..18c88658 100644 --- a/native/src/widget/rule.rs +++ b/native/src/widget/rule.rs @@ -17,8 +17,6 @@ pub struct Rule<Renderer: self::Renderer> { impl<Renderer: self::Renderer> Rule<Renderer> { /// Creates a horizontal [`Rule`] for dividing content by the given vertical spacing. - /// - /// [`Rule`]: struct.Rule.html pub fn horizontal(spacing: u16) -> Self { Rule { width: Length::Fill, @@ -29,8 +27,6 @@ impl<Renderer: self::Renderer> Rule<Renderer> { } /// Creates a vertical [`Rule`] for dividing content by the given horizontal spacing. - /// - /// [`Rule`]: struct.Rule.html pub fn vertical(spacing: u16) -> Self { Rule { width: Length::from(Length::Units(spacing)), @@ -41,8 +37,6 @@ impl<Renderer: self::Renderer> Rule<Renderer> { } /// Sets the style of the [`Rule`]. - /// - /// [`Rule`]: struct.Rule.html pub fn style(mut self, style: impl Into<Renderer::Style>) -> Self { self.style = style.into(); self @@ -77,6 +71,7 @@ where _defaults: &Renderer::Defaults, layout: Layout<'_>, _cursor_position: Point, + _viewport: &Rectangle, ) -> Renderer::Output { renderer.draw(layout.bounds(), &self.style, self.is_horizontal) } @@ -91,8 +86,6 @@ where } /// The renderer of a [`Rule`]. -/// -/// [`Rule`]: struct.Rule.html pub trait Renderer: crate::Renderer { /// The style supported by this renderer. type Style: Default; @@ -103,8 +96,6 @@ pub trait Renderer: crate::Renderer { /// * the bounds of the [`Rule`] /// * the style of the [`Rule`] /// * whether the [`Rule`] is horizontal (true) or vertical (false) - /// - /// [`Rule`]: struct.Rule.html fn draw( &mut self, bounds: Rectangle, diff --git a/native/src/widget/scrollable.rs b/native/src/widget/scrollable.rs index 75e97027..68da2e67 100644 --- a/native/src/widget/scrollable.rs +++ b/native/src/widget/scrollable.rs @@ -1,7 +1,13 @@ //! Navigate an endless amount of content with a scrollbar. +use crate::column; +use crate::event::{self, Event}; +use crate::layout; +use crate::mouse; +use crate::overlay; +use crate::touch; use crate::{ - column, layout, mouse, overlay, Align, Clipboard, Column, Element, Event, - Hasher, Layout, Length, Point, Rectangle, Size, Vector, Widget, + Align, Clipboard, Column, Element, Hasher, Layout, Length, Padding, Point, + Rectangle, Size, Vector, Widget, }; use std::{f32, hash::Hash, u32}; @@ -13,21 +19,26 @@ pub struct Scrollable<'a, Message, Renderer: self::Renderer> { state: &'a mut State, height: Length, max_height: u32, + scrollbar_width: u16, + scrollbar_margin: u16, + scroller_width: u16, content: Column<'a, Message, Renderer>, + on_scroll: Option<Box<dyn Fn(f32) -> Message>>, style: Renderer::Style, } impl<'a, Message, Renderer: self::Renderer> Scrollable<'a, Message, Renderer> { /// Creates a new [`Scrollable`] with the given [`State`]. - /// - /// [`Scrollable`]: struct.Scrollable.html - /// [`State`]: struct.State.html pub fn new(state: &'a mut State) -> Self { Scrollable { state, height: Length::Shrink, max_height: u32::MAX, + scrollbar_width: 10, + scrollbar_margin: 0, + scroller_width: 10, content: Column::new(), + on_scroll: None, style: Renderer::Style::default(), } } @@ -42,65 +53,79 @@ impl<'a, Message, Renderer: self::Renderer> Scrollable<'a, Message, Renderer> { self } - /// Sets the padding of the [`Scrollable`]. - /// - /// [`Scrollable`]: struct.Scrollable.html - pub fn padding(mut self, units: u16) -> Self { - self.content = self.content.padding(units); + /// Sets the [`Padding`] of the [`Scrollable`]. + pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self { + self.content = self.content.padding(padding); self } /// Sets the width of the [`Scrollable`]. - /// - /// [`Scrollable`]: struct.Scrollable.html pub fn width(mut self, width: Length) -> Self { self.content = self.content.width(width); self } /// Sets the height of the [`Scrollable`]. - /// - /// [`Scrollable`]: struct.Scrollable.html pub fn height(mut self, height: Length) -> Self { self.height = height; self } /// Sets the maximum width of the [`Scrollable`]. - /// - /// [`Scrollable`]: struct.Scrollable.html pub fn max_width(mut self, max_width: u32) -> Self { self.content = self.content.max_width(max_width); self } /// Sets the maximum height of the [`Scrollable`] in pixels. - /// - /// [`Scrollable`]: struct.Scrollable.html pub fn max_height(mut self, max_height: u32) -> Self { self.max_height = max_height; self } /// Sets the horizontal alignment of the contents of the [`Scrollable`] . - /// - /// [`Scrollable`]: struct.Scrollable.html pub fn align_items(mut self, align_items: Align) -> Self { self.content = self.content.align_items(align_items); self } - /// Sets the style of the [`Scrollable`] . + /// Sets the scrollbar width of the [`Scrollable`] . + /// Silently enforces a minimum value of 1. + pub fn scrollbar_width(mut self, scrollbar_width: u16) -> Self { + self.scrollbar_width = scrollbar_width.max(1); + self + } + + /// Sets the scrollbar margin of the [`Scrollable`] . + pub fn scrollbar_margin(mut self, scrollbar_margin: u16) -> Self { + self.scrollbar_margin = scrollbar_margin; + self + } + + /// Sets the scroller width of the [`Scrollable`] . /// - /// [`Scrollable`]: struct.Scrollable.html + /// It silently enforces a minimum value of 1. + pub fn scroller_width(mut self, scroller_width: u16) -> Self { + self.scroller_width = scroller_width.max(1); + self + } + + /// Sets a function to call when the [`Scrollable`] is scrolled. + /// + /// The function takes the new relative offset of the [`Scrollable`] + /// (e.g. `0` means top, while `1` means bottom). + pub fn on_scroll(mut self, f: impl Fn(f32) -> Message + 'static) -> Self { + self.on_scroll = Some(Box::new(f)); + self + } + + /// Sets the style of the [`Scrollable`] . pub fn style(mut self, style: impl Into<Renderer::Style>) -> Self { self.style = style.into(); self } /// Adds an element to the [`Scrollable`]. - /// - /// [`Scrollable`]: struct.Scrollable.html pub fn push<E>(mut self, child: E) -> Self where E: Into<Element<'a, Message, Renderer>>, @@ -108,6 +133,24 @@ impl<'a, Message, Renderer: self::Renderer> Scrollable<'a, Message, Renderer> { self.content = self.content.push(child); self } + + fn notify_on_scroll( + &self, + bounds: Rectangle, + content_bounds: Rectangle, + messages: &mut Vec<Message>, + ) { + if content_bounds.height <= bounds.height { + return; + } + + if let Some(on_scroll) = &self.on_scroll { + messages.push(on_scroll( + self.state.offset.absolute(bounds, content_bounds) + / (content_bounds.height - bounds.height), + )); + } + } } impl<'a, Message, Renderer> Widget<Message, Renderer> @@ -149,17 +192,59 @@ where event: Event, layout: Layout<'_>, cursor_position: Point, - messages: &mut Vec<Message>, renderer: &Renderer, - clipboard: Option<&dyn Clipboard>, - ) { + clipboard: &mut dyn Clipboard, + messages: &mut Vec<Message>, + ) -> event::Status { let bounds = layout.bounds(); let is_mouse_over = bounds.contains(cursor_position); let content = layout.children().next().unwrap(); let content_bounds = content.bounds(); - // TODO: Event capture. Nested scrollables should capture scroll events. + let offset = self.state.offset(bounds, content_bounds); + let scrollbar = renderer.scrollbar( + bounds, + content_bounds, + offset, + self.scrollbar_width, + self.scrollbar_margin, + self.scroller_width, + ); + let is_mouse_over_scrollbar = scrollbar + .as_ref() + .map(|scrollbar| scrollbar.is_mouse_over(cursor_position)) + .unwrap_or(false); + + let event_status = { + let cursor_position = if is_mouse_over && !is_mouse_over_scrollbar { + Point::new( + cursor_position.x, + cursor_position.y + + self.state.offset(bounds, content_bounds) as f32, + ) + } else { + // TODO: Make `cursor_position` an `Option<Point>` so we can encode + // cursor availability. + // This will probably happen naturally once we add multi-window + // support. + Point::new(cursor_position.x, -1.0) + }; + + self.content.on_event( + event.clone(), + content, + cursor_position, + renderer, + clipboard, + messages, + ) + }; + + if let event::Status::Captured = event_status { + return event::Status::Captured; + } + if is_mouse_over { match event { Event::Mouse(mouse::Event::WheelScrolled { delta }) => { @@ -172,26 +257,65 @@ where self.state.scroll(y, bounds, content_bounds); } } + + self.notify_on_scroll(bounds, content_bounds, messages); + + return event::Status::Captured; + } + Event::Touch(event) => { + match event { + touch::Event::FingerPressed { .. } => { + self.state.scroll_box_touched_at = + Some(cursor_position); + } + touch::Event::FingerMoved { .. } => { + if let Some(scroll_box_touched_at) = + self.state.scroll_box_touched_at + { + let delta = + cursor_position.y - scroll_box_touched_at.y; + + self.state.scroll( + delta, + bounds, + content_bounds, + ); + + self.state.scroll_box_touched_at = + Some(cursor_position); + + self.notify_on_scroll( + bounds, + content_bounds, + messages, + ); + } + } + touch::Event::FingerLifted { .. } + | touch::Event::FingerLost { .. } => { + self.state.scroll_box_touched_at = None; + } + } + + return event::Status::Captured; } _ => {} } } - let offset = self.state.offset(bounds, content_bounds); - let scrollbar = renderer.scrollbar(bounds, content_bounds, offset); - let is_mouse_over_scrollbar = scrollbar - .as_ref() - .map(|scrollbar| scrollbar.is_mouse_over(cursor_position)) - .unwrap_or(false); - if self.state.is_scroller_grabbed() { match event { Event::Mouse(mouse::Event::ButtonReleased( mouse::Button::Left, - )) => { + )) + | Event::Touch(touch::Event::FingerLifted { .. }) + | Event::Touch(touch::Event::FingerLost { .. }) => { self.state.scroller_grabbed_at = None; + + return event::Status::Captured; } - Event::Mouse(mouse::Event::CursorMoved { .. }) => { + Event::Mouse(mouse::Event::CursorMoved { .. }) + | Event::Touch(touch::Event::FingerMoved { .. }) => { if let (Some(scrollbar), Some(scroller_grabbed_at)) = (scrollbar, self.state.scroller_grabbed_at) { @@ -203,6 +327,10 @@ where bounds, content_bounds, ); + + self.notify_on_scroll(bounds, content_bounds, messages); + + return event::Status::Captured; } } _ => {} @@ -211,7 +339,8 @@ where match event { Event::Mouse(mouse::Event::ButtonPressed( mouse::Button::Left, - )) => { + )) + | Event::Touch(touch::Event::FingerPressed { .. }) => { if let Some(scrollbar) = scrollbar { if let Some(scroller_grabbed_at) = scrollbar.grab_scroller(cursor_position) @@ -227,6 +356,14 @@ where self.state.scroller_grabbed_at = Some(scroller_grabbed_at); + + self.notify_on_scroll( + bounds, + content_bounds, + messages, + ); + + return event::Status::Captured; } } } @@ -234,28 +371,7 @@ where } } - let cursor_position = if is_mouse_over && !is_mouse_over_scrollbar { - Point::new( - cursor_position.x, - cursor_position.y - + self.state.offset(bounds, content_bounds) as f32, - ) - } else { - // TODO: Make `cursor_position` an `Option<Point>` so we can encode - // cursor availability. - // This will probably happen naturally once we add multi-window - // support. - Point::new(cursor_position.x, -1.0) - }; - - self.content.on_event( - event, - content, - cursor_position, - messages, - renderer, - clipboard, - ) + event::Status::Ignored } fn draw( @@ -264,12 +380,20 @@ where defaults: &Renderer::Defaults, layout: Layout<'_>, cursor_position: Point, + _viewport: &Rectangle, ) -> Renderer::Output { let bounds = layout.bounds(); let content_layout = layout.children().next().unwrap(); let content_bounds = content_layout.bounds(); let offset = self.state.offset(bounds, content_bounds); - let scrollbar = renderer.scrollbar(bounds, content_bounds, offset); + let scrollbar = renderer.scrollbar( + bounds, + content_bounds, + offset, + self.scrollbar_width, + self.scrollbar_margin, + self.scroller_width, + ); let is_mouse_over = bounds.contains(cursor_position); let is_mouse_over_scrollbar = scrollbar @@ -289,6 +413,10 @@ where defaults, content_layout, cursor_position, + &Rectangle { + y: bounds.y + offset as f32, + ..bounds + }, ) }; @@ -336,27 +464,54 @@ where } /// The local state of a [`Scrollable`]. -/// -/// [`Scrollable`]: struct.Scrollable.html -#[derive(Debug, Clone, Copy, Default)] +#[derive(Debug, Clone, Copy)] pub struct State { scroller_grabbed_at: Option<f32>, - offset: f32, + scroll_box_touched_at: Option<Point>, + offset: Offset, +} + +impl Default for State { + fn default() -> Self { + Self { + scroller_grabbed_at: None, + scroll_box_touched_at: None, + offset: Offset::Absolute(0.0), + } + } +} + +/// The local state of a [`Scrollable`]. +#[derive(Debug, Clone, Copy)] +enum Offset { + Absolute(f32), + Relative(f32), +} + +impl Offset { + fn absolute(self, bounds: Rectangle, content_bounds: Rectangle) -> f32 { + match self { + Self::Absolute(absolute) => { + let hidden_content = + (content_bounds.height - bounds.height).max(0.0); + + absolute.min(hidden_content) + } + Self::Relative(percentage) => { + ((content_bounds.height - bounds.height) * percentage).max(0.0) + } + } + } } impl State { /// Creates a new [`State`] with the scrollbar located at the top. - /// - /// [`State`]: struct.State.html pub fn new() -> Self { State::default() } /// Apply a scrolling offset to the current [`State`], given the bounds of /// the [`Scrollable`] and its contents. - /// - /// [`Scrollable`]: struct.Scrollable.html - /// [`State`]: struct.State.html pub fn scroll( &mut self, delta_y: f32, @@ -367,70 +522,83 @@ impl State { return; } - self.offset = (self.offset - delta_y) - .max(0.0) - .min((content_bounds.height - bounds.height) as f32); + self.offset = Offset::Absolute( + (self.offset.absolute(bounds, content_bounds) - delta_y) + .max(0.0) + .min((content_bounds.height - bounds.height) as f32), + ); } - /// Moves the scroll position to a relative amount, given the bounds of - /// the [`Scrollable`] and its contents. + /// Scrolls the [`Scrollable`] to a relative amount. /// /// `0` represents scrollbar at the top, while `1` represents scrollbar at /// the bottom. - /// - /// [`Scrollable`]: struct.Scrollable.html - /// [`State`]: struct.State.html pub fn scroll_to( &mut self, percentage: f32, bounds: Rectangle, content_bounds: Rectangle, ) { + self.snap_to(percentage); + self.unsnap(bounds, content_bounds); + } + + /// Snaps the scroll position to a relative amount. + /// + /// `0` represents scrollbar at the top, while `1` represents scrollbar at + /// the bottom. + pub fn snap_to(&mut self, percentage: f32) { + self.offset = Offset::Relative(percentage.max(0.0).min(1.0)); + } + + /// Unsnaps the current scroll position, if snapped, given the bounds of the + /// [`Scrollable`] and its contents. + pub fn unsnap(&mut self, bounds: Rectangle, content_bounds: Rectangle) { self.offset = - ((content_bounds.height - bounds.height) * percentage).max(0.0); + Offset::Absolute(self.offset.absolute(bounds, content_bounds)); } /// Returns the current scrolling offset of the [`State`], given the bounds /// of the [`Scrollable`] and its contents. - /// - /// [`Scrollable`]: struct.Scrollable.html - /// [`State`]: struct.State.html pub fn offset(&self, bounds: Rectangle, content_bounds: Rectangle) -> u32 { - let hidden_content = - (content_bounds.height - bounds.height).max(0.0).round() as u32; - - self.offset.min(hidden_content as f32) as u32 + self.offset.absolute(bounds, content_bounds) as u32 } /// Returns whether the scroller is currently grabbed or not. pub fn is_scroller_grabbed(&self) -> bool { self.scroller_grabbed_at.is_some() } + + /// Returns whether the scroll box is currently touched or not. + pub fn is_scroll_box_touched(&self) -> bool { + self.scroll_box_touched_at.is_some() + } } /// The scrollbar of a [`Scrollable`]. -/// -/// [`Scrollable`]: struct.Scrollable.html #[derive(Debug)] pub struct Scrollbar { + /// The outer bounds of the scrollable, including the [`Scrollbar`] and + /// [`Scroller`]. + pub outer_bounds: Rectangle, + /// The bounds of the [`Scrollbar`]. - /// - /// [`Scrollbar`]: struct.Scrollbar.html pub bounds: Rectangle, + /// The margin within the [`Scrollbar`]. + pub margin: u16, + /// The bounds of the [`Scroller`]. - /// - /// [`Scroller`]: struct.Scroller.html pub scroller: Scroller, } impl Scrollbar { fn is_mouse_over(&self, cursor_position: Point) -> bool { - self.bounds.contains(cursor_position) + self.outer_bounds.contains(cursor_position) } fn grab_scroller(&self, cursor_position: Point) -> Option<f32> { - if self.bounds.contains(cursor_position) { + if self.outer_bounds.contains(cursor_position) { Some(if self.scroller.bounds.contains(cursor_position) { (cursor_position.y - self.scroller.bounds.y) / self.scroller.bounds.height @@ -455,13 +623,9 @@ impl Scrollbar { } /// The handle of a [`Scrollbar`]. -/// -/// [`Scrollbar`]: struct.Scrollbar.html #[derive(Debug, Clone, Copy)] pub struct Scroller { /// The bounds of the [`Scroller`]. - /// - /// [`Scroller`]: struct.Scrollbar.html pub bounds: Rectangle, } @@ -470,22 +634,21 @@ pub struct Scroller { /// Your [renderer] will need to implement this trait before being /// able to use a [`Scrollable`] in your user interface. /// -/// [`Scrollable`]: struct.Scrollable.html -/// [renderer]: ../../renderer/index.html +/// [renderer]: crate::renderer pub trait Renderer: column::Renderer + Sized { /// The style supported by this renderer. type Style: Default; /// Returns the [`Scrollbar`] given the bounds and content bounds of a /// [`Scrollable`]. - /// - /// [`Scrollbar`]: struct.Scrollbar.html - /// [`Scrollable`]: struct.Scrollable.html fn scrollbar( &self, bounds: Rectangle, content_bounds: Rectangle, offset: u32, + scrollbar_width: u16, + scrollbar_margin: u16, + scroller_width: u16, ) -> Option<Scrollbar>; /// Draws the [`Scrollable`]. @@ -499,10 +662,6 @@ pub trait Renderer: column::Renderer + Sized { /// - a optional [`Scrollbar`] to be rendered /// - the scrolling offset /// - the drawn content - /// - /// [`Scrollbar`]: struct.Scrollbar.html - /// [`Scrollable`]: struct.Scrollable.html - /// [`State`]: struct.State.html fn draw( &mut self, scrollable: &State, diff --git a/native/src/widget/slider.rs b/native/src/widget/slider.rs index c49053f1..2a74d5a3 100644 --- a/native/src/widget/slider.rs +++ b/native/src/widget/slider.rs @@ -1,12 +1,12 @@ //! Display an interactive selector of a single value from a range of values. //! //! A [`Slider`] has some local [`State`]. -//! -//! [`Slider`]: struct.Slider.html -//! [`State`]: struct.State.html +use crate::event::{self, Event}; +use crate::layout; +use crate::mouse; +use crate::touch; use crate::{ - layout, mouse, Clipboard, Element, Event, Hasher, Layout, Length, Point, - Rectangle, Size, Widget, + Clipboard, Element, Hasher, Layout, Length, Point, Rectangle, Size, Widget, }; use std::{hash::Hash, ops::RangeInclusive}; @@ -19,13 +19,12 @@ use std::{hash::Hash, ops::RangeInclusive}; /// The [`Slider`] range of numeric values is generic and its step size defaults /// to 1 unit. /// -/// [`Slider`]: struct.Slider.html -/// /// # Example /// ``` /// # use iced_native::{slider, renderer::Null}; /// # /// # pub type Slider<'a, T, Message> = iced_native::Slider<'a, T, Message, Null>; +/// #[derive(Clone)] /// pub enum Message { /// SliderChanged(f32), /// } @@ -53,6 +52,7 @@ pub struct Slider<'a, T, Message, Renderer: self::Renderer> { impl<'a, T, Message, Renderer> Slider<'a, T, Message, Renderer> where T: Copy + From<u8> + std::cmp::PartialOrd, + Message: Clone, Renderer: self::Renderer, { /// Creates a new [`Slider`]. @@ -64,9 +64,6 @@ where /// * a function that will be called when the [`Slider`] is dragged. /// It receives the new value of the [`Slider`] and must produce a /// `Message`. - /// - /// [`Slider`]: struct.Slider.html - /// [`State`]: struct.State.html pub fn new<F>( state: &'a mut State, range: RangeInclusive<T>, @@ -107,40 +104,30 @@ where /// Typically, the user's interaction with the slider is finished when this message is produced. /// This is useful if you need to spawn a long-running task from the slider's result, where /// the default on_change message could create too many events. - /// - /// [`Slider`]: struct.Slider.html pub fn on_release(mut self, on_release: Message) -> Self { self.on_release = Some(on_release); self } /// Sets the width of the [`Slider`]. - /// - /// [`Slider`]: struct.Slider.html pub fn width(mut self, width: Length) -> Self { self.width = width; self } /// Sets the height of the [`Slider`]. - /// - /// [`Slider`]: struct.Slider.html pub fn height(mut self, height: u16) -> Self { self.height = height; self } /// Sets the style of the [`Slider`]. - /// - /// [`Slider`]: struct.Slider.html pub fn style(mut self, style: impl Into<Renderer::Style>) -> Self { self.style = style.into(); self } /// Sets the step size of the [`Slider`]. - /// - /// [`Slider`]: struct.Slider.html pub fn step(mut self, step: T) -> Self { self.step = step; self @@ -148,8 +135,6 @@ where } /// The local state of a [`Slider`]. -/// -/// [`Slider`]: struct.Slider.html #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub struct State { is_dragging: bool, @@ -157,8 +142,6 @@ pub struct State { impl State { /// Creates a new [`State`]. - /// - /// [`State`]: struct.State.html pub fn new() -> State { State::default() } @@ -168,8 +151,8 @@ impl<'a, T, Message, Renderer> Widget<Message, Renderer> for Slider<'a, T, Message, Renderer> where T: Copy + Into<f64> + num_traits::FromPrimitive, - Renderer: self::Renderer, Message: Clone, + Renderer: self::Renderer, { fn width(&self) -> Length { self.width @@ -197,10 +180,10 @@ where event: Event, layout: Layout<'_>, cursor_position: Point, - messages: &mut Vec<Message>, _renderer: &Renderer, - _clipboard: Option<&dyn Clipboard>, - ) { + _clipboard: &mut dyn Clipboard, + messages: &mut Vec<Message>, + ) -> event::Status { let mut change = || { let bounds = layout.bounds(); if cursor_position.x <= bounds.x { @@ -225,30 +208,39 @@ where }; match event { - Event::Mouse(mouse_event) => match mouse_event { - mouse::Event::ButtonPressed(mouse::Button::Left) => { - if layout.bounds().contains(cursor_position) { - change(); - self.state.is_dragging = true; - } + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerPressed { .. }) => { + if layout.bounds().contains(cursor_position) { + change(); + self.state.is_dragging = true; + + return event::Status::Captured; } - mouse::Event::ButtonReleased(mouse::Button::Left) => { - if self.state.is_dragging { - if let Some(on_release) = self.on_release.clone() { - messages.push(on_release); - } - self.state.is_dragging = false; + } + Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerLifted { .. }) + | Event::Touch(touch::Event::FingerLost { .. }) => { + if self.state.is_dragging { + if let Some(on_release) = self.on_release.clone() { + messages.push(on_release); } + self.state.is_dragging = false; + + return event::Status::Captured; } - mouse::Event::CursorMoved { .. } => { - if self.state.is_dragging { - change(); - } + } + Event::Mouse(mouse::Event::CursorMoved { .. }) + | Event::Touch(touch::Event::FingerMoved { .. }) => { + if self.state.is_dragging { + change(); + + return event::Status::Captured; } - _ => {} - }, + } _ => {} } + + event::Status::Ignored } fn draw( @@ -257,6 +249,7 @@ where _defaults: &Renderer::Defaults, layout: Layout<'_>, cursor_position: Point, + _viewport: &Rectangle, ) -> Renderer::Output { let start = *self.range.start(); let end = *self.range.end(); @@ -284,15 +277,12 @@ where /// Your [renderer] will need to implement this trait before being /// able to use a [`Slider`] in your user interface. /// -/// [`Slider`]: struct.Slider.html -/// [renderer]: ../../renderer/index.html +/// [renderer]: crate::renderer pub trait Renderer: crate::Renderer { /// The style supported by this renderer. type Style: Default; /// The default height of a [`Slider`]. - /// - /// [`Slider`]: struct.Slider.html const DEFAULT_HEIGHT: u16; /// Draws a [`Slider`]. @@ -303,10 +293,6 @@ pub trait Renderer: crate::Renderer { /// * the local state of the [`Slider`] /// * the range of values of the [`Slider`] /// * the current value of the [`Slider`] - /// - /// [`Slider`]: struct.Slider.html - /// [`State`]: struct.State.html - /// [`Class`]: enum.Class.html fn draw( &mut self, bounds: Rectangle, @@ -322,8 +308,8 @@ impl<'a, T, Message, Renderer> From<Slider<'a, T, Message, Renderer>> for Element<'a, Message, Renderer> where T: 'a + Copy + Into<f64> + num_traits::FromPrimitive, - Renderer: 'a + self::Renderer, Message: 'a + Clone, + Renderer: 'a + self::Renderer, { fn from( slider: Slider<'a, T, Message, Renderer>, diff --git a/native/src/widget/space.rs b/native/src/widget/space.rs index f1576ffb..6b34ece8 100644 --- a/native/src/widget/space.rs +++ b/native/src/widget/space.rs @@ -16,15 +16,11 @@ pub struct Space { impl Space { /// Creates an amount of empty [`Space`] with the given width and height. - /// - /// [`Space`]: struct.Space.html pub fn new(width: Length, height: Length) -> Self { Space { width, height } } /// Creates an amount of horizontal [`Space`]. - /// - /// [`Space`]: struct.Space.html pub fn with_width(width: Length) -> Self { Space { width, @@ -33,8 +29,6 @@ impl Space { } /// Creates an amount of vertical [`Space`]. - /// - /// [`Space`]: struct.Space.html pub fn with_height(height: Length) -> Self { Space { width: Length::Shrink, @@ -71,6 +65,7 @@ where _defaults: &Renderer::Defaults, layout: Layout<'_>, _cursor_position: Point, + _viewport: &Rectangle, ) -> Renderer::Output { renderer.draw(layout.bounds()) } @@ -84,14 +79,10 @@ where } /// The renderer of an amount of [`Space`]. -/// -/// [`Space`]: struct.Space.html pub trait Renderer: crate::Renderer { /// Draws an amount of empty [`Space`]. /// /// You should most likely return an empty primitive here. - /// - /// [`Space`]: struct.Space.html fn draw(&mut self, bounds: Rectangle) -> Self::Output; } diff --git a/native/src/widget/svg.rs b/native/src/widget/svg.rs index 114d5e41..9cd61918 100644 --- a/native/src/widget/svg.rs +++ b/native/src/widget/svg.rs @@ -1,5 +1,6 @@ //! Display vector graphics in your application. -use crate::{layout, Element, Hasher, Layout, Length, Point, Size, Widget}; +use crate::layout; +use crate::{Element, Hasher, Layout, Length, Point, Rectangle, Size, Widget}; use std::{ hash::{Hash, Hasher as _}, @@ -13,8 +14,6 @@ use std::{ /// /// [`Svg`] images can have a considerable rendering cost when resized, /// specially when they are complex. -/// -/// [`Svg`]: struct.Svg.html #[derive(Debug, Clone)] pub struct Svg { handle: Handle, @@ -24,9 +23,6 @@ pub struct Svg { impl Svg { /// Creates a new [`Svg`] from the given [`Handle`]. - /// - /// [`Svg`]: struct.Svg.html - /// [`Handle`]: struct.Handle.html pub fn new(handle: impl Into<Handle>) -> Self { Svg { handle: handle.into(), @@ -37,23 +33,17 @@ impl Svg { /// Creates a new [`Svg`] that will display the contents of the file at the /// provided path. - /// - /// [`Svg`]: struct.Svg.html pub fn from_path(path: impl Into<PathBuf>) -> Self { Self::new(Handle::from_path(path)) } /// Sets the width of the [`Svg`]. - /// - /// [`Svg`]: struct.Svg.html pub fn width(mut self, width: Length) -> Self { self.width = width; self } /// Sets the height of the [`Svg`]. - /// - /// [`Svg`]: struct.Svg.html pub fn height(mut self, height: Length) -> Self { self.height = height; self @@ -103,6 +93,7 @@ where _defaults: &Renderer::Defaults, layout: Layout<'_>, _cursor_position: Point, + _viewport: &Rectangle, ) -> Renderer::Output { renderer.draw(self.handle.clone(), layout) } @@ -117,8 +108,6 @@ where } /// An [`Svg`] handle. -/// -/// [`Svg`]: struct.Svg.html #[derive(Debug, Clone)] pub struct Handle { id: u64, @@ -128,8 +117,6 @@ pub struct Handle { impl Handle { /// Creates an SVG [`Handle`] pointing to the vector image of the given /// path. - /// - /// [`Handle`]: struct.Handle.html pub fn from_path(path: impl Into<PathBuf>) -> Handle { Self::from_data(Data::Path(path.into())) } @@ -139,8 +126,6 @@ impl Handle { /// /// This is useful if you already have your SVG data in-memory, maybe /// because you downloaded or generated it procedurally. - /// - /// [`Handle`]: struct.Handle.html pub fn from_memory(bytes: impl Into<Vec<u8>>) -> Handle { Self::from_data(Data::Bytes(bytes.into())) } @@ -156,15 +141,11 @@ impl Handle { } /// Returns the unique identifier of the [`Handle`]. - /// - /// [`Handle`]: struct.Handle.html pub fn id(&self) -> u64 { self.id } /// Returns a reference to the SVG [`Data`]. - /// - /// [`Data`]: enum.Data.html pub fn data(&self) -> &Data { &self.data } @@ -177,8 +158,6 @@ impl Hash for Handle { } /// The data of an [`Svg`]. -/// -/// [`Svg`]: struct.Svg.html #[derive(Clone, Hash)] pub enum Data { /// File data @@ -204,18 +183,12 @@ impl std::fmt::Debug for Data { /// Your [renderer] will need to implement this trait before being able to use /// an [`Svg`] in your user interface. /// -/// [`Svg`]: struct.Svg.html -/// [renderer]: ../../renderer/index.html +/// [renderer]: crate::renderer pub trait Renderer: crate::Renderer { /// Returns the default dimensions of an [`Svg`] for the given [`Handle`]. - /// - /// [`Svg`]: struct.Svg.html - /// [`Handle`]: struct.Handle.html fn dimensions(&self, handle: &Handle) -> (u32, u32); /// Draws an [`Svg`]. - /// - /// [`Svg`]: struct.Svg.html fn draw(&mut self, handle: Handle, layout: Layout<'_>) -> Self::Output; } diff --git a/native/src/widget/text.rs b/native/src/widget/text.rs index 48a69e34..6cc18e6c 100644 --- a/native/src/widget/text.rs +++ b/native/src/widget/text.rs @@ -33,8 +33,6 @@ pub struct Text<Renderer: self::Renderer> { impl<Renderer: self::Renderer> Text<Renderer> { /// Create a new fragment of [`Text`] with the given contents. - /// - /// [`Text`]: struct.Text.html pub fn new<T: Into<String>>(label: T) -> Self { Text { content: label.into(), @@ -49,17 +47,12 @@ impl<Renderer: self::Renderer> Text<Renderer> { } /// Sets the size of the [`Text`]. - /// - /// [`Text`]: struct.Text.html pub fn size(mut self, size: u16) -> Self { self.size = Some(size); self } /// Sets the [`Color`] of the [`Text`]. - /// - /// [`Text`]: struct.Text.html - /// [`Color`]: ../../struct.Color.html pub fn color<C: Into<Color>>(mut self, color: C) -> Self { self.color = Some(color.into()); self @@ -67,33 +60,25 @@ impl<Renderer: self::Renderer> Text<Renderer> { /// Sets the [`Font`] of the [`Text`]. /// - /// [`Text`]: struct.Text.html - /// [`Font`]: ../../struct.Font.html + /// [`Font`]: Renderer::Font pub fn font(mut self, font: impl Into<Renderer::Font>) -> Self { self.font = font.into(); self } /// Sets the width of the [`Text`] boundaries. - /// - /// [`Text`]: struct.Text.html pub fn width(mut self, width: Length) -> Self { self.width = width; self } /// Sets the height of the [`Text`] boundaries. - /// - /// [`Text`]: struct.Text.html pub fn height(mut self, height: Length) -> Self { self.height = height; self } /// Sets the [`HorizontalAlignment`] of the [`Text`]. - /// - /// [`Text`]: struct.Text.html - /// [`HorizontalAlignment`]: enum.HorizontalAlignment.html pub fn horizontal_alignment( mut self, alignment: HorizontalAlignment, @@ -103,9 +88,6 @@ impl<Renderer: self::Renderer> Text<Renderer> { } /// Sets the [`VerticalAlignment`] of the [`Text`]. - /// - /// [`Text`]: struct.Text.html - /// [`VerticalAlignment`]: enum.VerticalAlignment.html pub fn vertical_alignment(mut self, alignment: VerticalAlignment) -> Self { self.vertical_alignment = alignment; self @@ -149,6 +131,7 @@ where defaults: &Renderer::Defaults, layout: Layout<'_>, _cursor_position: Point, + _viewport: &Rectangle, ) -> Renderer::Output { renderer.draw( defaults, @@ -176,26 +159,18 @@ where /// The renderer of a [`Text`] fragment. /// /// Your [renderer] will need to implement this trait before being -/// able to use [`Text`] in your [`UserInterface`]. +/// able to use [`Text`] in your user interface. /// -/// [`Text`]: struct.Text.html -/// [renderer]: ../../renderer/index.html -/// [`UserInterface`]: ../../struct.UserInterface.html +/// [renderer]: crate::Renderer pub trait Renderer: crate::Renderer { /// The font type used for [`Text`]. - /// - /// [`Text`]: struct.Text.html type Font: Default + Copy; /// Returns the default size of [`Text`]. - /// - /// [`Text`]: struct.Text.html fn default_size(&self) -> u16; /// Measures the [`Text`] in the given bounds and returns the minimum /// boundaries that can fit the contents. - /// - /// [`Text`]: struct.Text.html fn measure( &self, content: &str, @@ -213,10 +188,6 @@ pub trait Renderer: crate::Renderer { /// * the color of the [`Text`] /// * the [`HorizontalAlignment`] of the [`Text`] /// * the [`VerticalAlignment`] of the [`Text`] - /// - /// [`Text`]: struct.Text.html - /// [`HorizontalAlignment`]: enum.HorizontalAlignment.html - /// [`VerticalAlignment`]: enum.VerticalAlignment.html fn draw( &mut self, defaults: &Self::Defaults, diff --git a/native/src/widget/text_input.rs b/native/src/widget/text_input.rs index 9e15f4be..cec1e485 100644 --- a/native/src/widget/text_input.rs +++ b/native/src/widget/text_input.rs @@ -1,9 +1,6 @@ //! Display fields that can be filled with text. //! //! A [`TextInput`] has some local [`State`]. -//! -//! [`TextInput`]: struct.TextInput.html -//! [`State`]: struct.State.html mod editor; mod value; @@ -14,10 +11,14 @@ pub use value::Value; use editor::Editor; +use crate::event::{self, Event}; +use crate::keyboard; +use crate::layout; +use crate::mouse::{self, click}; +use crate::text; +use crate::touch; use crate::{ - keyboard, layout, - mouse::{self, click}, - text, Clipboard, Element, Event, Hasher, Layout, Length, Point, Rectangle, + Clipboard, Element, Hasher, Layout, Length, Padding, Point, Rectangle, Size, Widget, }; @@ -56,14 +57,18 @@ pub struct TextInput<'a, Message, Renderer: self::Renderer> { font: Renderer::Font, width: Length, max_width: u32, - padding: u16, + padding: Padding, size: Option<u16>, on_change: Box<dyn Fn(String) -> Message>, on_submit: Option<Message>, style: Renderer::Style, } -impl<'a, Message, Renderer: self::Renderer> TextInput<'a, Message, Renderer> { +impl<'a, Message, Renderer> TextInput<'a, Message, Renderer> +where + Message: Clone, + Renderer: self::Renderer, +{ /// Creates a new [`TextInput`]. /// /// It expects: @@ -71,9 +76,6 @@ impl<'a, Message, Renderer: self::Renderer> TextInput<'a, Message, Renderer> { /// - a placeholder /// - the current value /// - a function that produces a message when the [`TextInput`] changes - /// - /// [`TextInput`]: struct.TextInput.html - /// [`State`]: struct.State.html pub fn new<F>( state: &'a mut State, placeholder: &str, @@ -91,7 +93,7 @@ impl<'a, Message, Renderer: self::Renderer> TextInput<'a, Message, Renderer> { font: Default::default(), width: Length::Fill, max_width: u32::MAX, - padding: 0, + padding: Padding::ZERO, size: None, on_change: Box::new(on_change), on_submit: None, @@ -100,8 +102,6 @@ impl<'a, Message, Renderer: self::Renderer> TextInput<'a, Message, Renderer> { } /// Converts the [`TextInput`] into a secure password input. - /// - /// [`TextInput`]: struct.TextInput.html pub fn password(mut self) -> Self { self.is_secure = true; self @@ -109,39 +109,31 @@ impl<'a, Message, Renderer: self::Renderer> TextInput<'a, Message, Renderer> { /// Sets the [`Font`] of the [`Text`]. /// - /// [`Text`]: struct.Text.html - /// [`Font`]: ../../struct.Font.html + /// [`Font`]: crate::widget::text::Renderer::Font + /// [`Text`]: crate::widget::Text pub fn font(mut self, font: Renderer::Font) -> Self { self.font = font; self } /// Sets the width of the [`TextInput`]. - /// - /// [`TextInput`]: struct.TextInput.html pub fn width(mut self, width: Length) -> Self { self.width = width; self } /// Sets the maximum width of the [`TextInput`]. - /// - /// [`TextInput`]: struct.TextInput.html pub fn max_width(mut self, max_width: u32) -> Self { self.max_width = max_width; self } - /// Sets the padding of the [`TextInput`]. - /// - /// [`TextInput`]: struct.TextInput.html - pub fn padding(mut self, units: u16) -> Self { - self.padding = units; + /// Sets the [`Padding`] of the [`TextInput`]. + pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self { + self.padding = padding.into(); self } /// Sets the text size of the [`TextInput`]. - /// - /// [`TextInput`]: struct.TextInput.html pub fn size(mut self, size: u16) -> Self { self.size = Some(size); self @@ -149,34 +141,75 @@ impl<'a, Message, Renderer: self::Renderer> TextInput<'a, Message, Renderer> { /// Sets the message that should be produced when the [`TextInput`] is /// focused and the enter key is pressed. - /// - /// [`TextInput`]: struct.TextInput.html pub fn on_submit(mut self, message: Message) -> Self { self.on_submit = Some(message); self } /// Sets the style of the [`TextInput`]. - /// - /// [`TextInput`]: struct.TextInput.html pub fn style(mut self, style: impl Into<Renderer::Style>) -> Self { self.style = style.into(); self } /// Returns the current [`State`] of the [`TextInput`]. - /// - /// [`TextInput`]: struct.TextInput.html pub fn state(&self) -> &State { self.state } } +impl<'a, Message, Renderer> TextInput<'a, Message, Renderer> +where + Renderer: self::Renderer, +{ + /// Draws the [`TextInput`] with the given [`Renderer`], overriding its + /// [`Value`] if provided. + pub fn draw( + &self, + renderer: &mut Renderer, + layout: Layout<'_>, + cursor_position: Point, + value: Option<&Value>, + ) -> Renderer::Output { + let value = value.unwrap_or(&self.value); + let bounds = layout.bounds(); + let text_bounds = layout.children().next().unwrap().bounds(); + + if self.is_secure { + self::Renderer::draw( + renderer, + bounds, + text_bounds, + cursor_position, + self.font, + self.size.unwrap_or(renderer.default_size()), + &self.placeholder, + &value.secure(), + &self.state, + &self.style, + ) + } else { + self::Renderer::draw( + renderer, + bounds, + text_bounds, + cursor_position, + self.font, + self.size.unwrap_or(renderer.default_size()), + &self.placeholder, + value, + &self.state, + &self.style, + ) + } + } +} + impl<'a, Message, Renderer> Widget<Message, Renderer> for TextInput<'a, Message, Renderer> where - Renderer: self::Renderer, Message: Clone, + Renderer: self::Renderer, { fn width(&self) -> Length { self.width @@ -191,19 +224,21 @@ where renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { - let padding = self.padding as f32; let text_size = self.size.unwrap_or(renderer.default_size()); let limits = limits - .pad(padding) + .pad(self.padding) .width(self.width) .max_width(self.max_width) .height(Length::Units(text_size)); let mut text = layout::Node::new(limits.resolve(Size::ZERO)); - text.move_to(Point::new(padding, padding)); + text.move_to(Point::new( + self.padding.left.into(), + self.padding.top.into(), + )); - layout::Node::with_children(text.size().pad(padding), vec![text]) + layout::Node::with_children(text.size().pad(self.padding), vec![text]) } fn on_event( @@ -211,14 +246,17 @@ where event: Event, layout: Layout<'_>, cursor_position: Point, - messages: &mut Vec<Message>, renderer: &Renderer, - clipboard: Option<&dyn Clipboard>, - ) { + clipboard: &mut dyn Clipboard, + messages: &mut Vec<Message>, + ) -> event::Status { match event { - Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => { + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerPressed { .. }) => { let is_clicked = layout.bounds().contains(cursor_position); + self.state.is_focused = is_clicked; + if is_clicked { let text_layout = layout.children().next().unwrap(); let target = cursor_position.x - text_layout.bounds().x; @@ -250,6 +288,8 @@ where } else { self.state.cursor.move_to(0); } + + self.state.is_dragging = true; } click::Kind::Double => { if self.is_secure { @@ -269,25 +309,30 @@ where self.value.next_end_of_word(position), ); } + + self.state.is_dragging = false; } click::Kind::Triple => { self.state.cursor.select_all(&self.value); + self.state.is_dragging = false; } } self.state.last_click = Some(click); - } - self.state.is_dragging = is_clicked; - self.state.is_focused = is_clicked; + return event::Status::Captured; + } } - Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) => { + Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerLifted { .. }) + | Event::Touch(touch::Event::FingerLost { .. }) => { self.state.is_dragging = false; } - Event::Mouse(mouse::Event::CursorMoved { x, .. }) => { + Event::Mouse(mouse::Event::CursorMoved { position }) + | Event::Touch(touch::Event::FingerMoved { position, .. }) => { if self.state.is_dragging { let text_layout = layout.children().next().unwrap(); - let target = x - text_layout.bounds().x; + let target = position.x - text_layout.bounds().x; if target > 0.0 { let value = if self.is_secure { @@ -310,11 +355,14 @@ where position, ); } + + return event::Status::Captured; } } Event::Keyboard(keyboard::Event::CharacterReceived(c)) if self.state.is_focused && self.state.is_pasting.is_none() + && !self.state.keyboard_modifiers.command() && !c.is_control() => { let mut editor = @@ -324,120 +372,179 @@ where let message = (self.on_change)(editor.contents()); messages.push(message); + + return event::Status::Captured; } Event::Keyboard(keyboard::Event::KeyPressed { - key_code, - modifiers, - }) if self.state.is_focused => match key_code { - keyboard::KeyCode::Enter => { - if let Some(on_submit) = self.on_submit.clone() { - messages.push(on_submit); - } - } - keyboard::KeyCode::Backspace => { - if platform::is_jump_modifier_pressed(modifiers) - && self.state.cursor.selection(&self.value).is_none() - { - if self.is_secure { - let cursor_pos = self.state.cursor.end(&self.value); - self.state.cursor.select_range(0, cursor_pos); - } else { - self.state.cursor.select_left_by_words(&self.value); + key_code, .. + }) if self.state.is_focused => { + let modifiers = self.state.keyboard_modifiers; + + match key_code { + keyboard::KeyCode::Enter => { + if let Some(on_submit) = self.on_submit.clone() { + messages.push(on_submit); } } + keyboard::KeyCode::Backspace => { + if platform::is_jump_modifier_pressed(modifiers) + && self + .state + .cursor + .selection(&self.value) + .is_none() + { + if self.is_secure { + let cursor_pos = + self.state.cursor.end(&self.value); + self.state.cursor.select_range(0, cursor_pos); + } else { + self.state + .cursor + .select_left_by_words(&self.value); + } + } - let mut editor = - Editor::new(&mut self.value, &mut self.state.cursor); + let mut editor = Editor::new( + &mut self.value, + &mut self.state.cursor, + ); - editor.backspace(); + editor.backspace(); - let message = (self.on_change)(editor.contents()); - messages.push(message); - } - keyboard::KeyCode::Delete => { - if platform::is_jump_modifier_pressed(modifiers) - && self.state.cursor.selection(&self.value).is_none() - { - if self.is_secure { - let cursor_pos = self.state.cursor.end(&self.value); - self.state - .cursor - .select_range(cursor_pos, self.value.len()); - } else { - self.state + let message = (self.on_change)(editor.contents()); + messages.push(message); + } + keyboard::KeyCode::Delete => { + if platform::is_jump_modifier_pressed(modifiers) + && self + .state .cursor - .select_right_by_words(&self.value); + .selection(&self.value) + .is_none() + { + if self.is_secure { + let cursor_pos = + self.state.cursor.end(&self.value); + self.state + .cursor + .select_range(cursor_pos, self.value.len()); + } else { + self.state + .cursor + .select_right_by_words(&self.value); + } } - } - let mut editor = - Editor::new(&mut self.value, &mut self.state.cursor); + let mut editor = Editor::new( + &mut self.value, + &mut self.state.cursor, + ); - editor.delete(); + editor.delete(); - let message = (self.on_change)(editor.contents()); - messages.push(message); - } - keyboard::KeyCode::Left => { - if platform::is_jump_modifier_pressed(modifiers) - && !self.is_secure - { - if modifiers.shift { - self.state.cursor.select_left_by_words(&self.value); + let message = (self.on_change)(editor.contents()); + messages.push(message); + } + keyboard::KeyCode::Left => { + if platform::is_jump_modifier_pressed(modifiers) + && !self.is_secure + { + if modifiers.shift() { + self.state + .cursor + .select_left_by_words(&self.value); + } else { + self.state + .cursor + .move_left_by_words(&self.value); + } + } else if modifiers.shift() { + self.state.cursor.select_left(&self.value) } else { - self.state.cursor.move_left_by_words(&self.value); + self.state.cursor.move_left(&self.value); } - } else if modifiers.shift { - self.state.cursor.select_left(&self.value) - } else { - self.state.cursor.move_left(&self.value); } - } - keyboard::KeyCode::Right => { - if platform::is_jump_modifier_pressed(modifiers) - && !self.is_secure - { - if modifiers.shift { - self.state - .cursor - .select_right_by_words(&self.value); + keyboard::KeyCode::Right => { + if platform::is_jump_modifier_pressed(modifiers) + && !self.is_secure + { + if modifiers.shift() { + self.state + .cursor + .select_right_by_words(&self.value); + } else { + self.state + .cursor + .move_right_by_words(&self.value); + } + } else if modifiers.shift() { + self.state.cursor.select_right(&self.value) } else { - self.state.cursor.move_right_by_words(&self.value); + self.state.cursor.move_right(&self.value); } - } else if modifiers.shift { - self.state.cursor.select_right(&self.value) - } else { - self.state.cursor.move_right(&self.value); } - } - keyboard::KeyCode::Home => { - if modifiers.shift { - self.state.cursor.select_range( - self.state.cursor.start(&self.value), - 0, - ); - } else { - self.state.cursor.move_to(0); + keyboard::KeyCode::Home => { + if modifiers.shift() { + self.state.cursor.select_range( + self.state.cursor.start(&self.value), + 0, + ); + } else { + self.state.cursor.move_to(0); + } } - } - keyboard::KeyCode::End => { - if modifiers.shift { - self.state.cursor.select_range( - self.state.cursor.start(&self.value), - self.value.len(), + keyboard::KeyCode::End => { + if modifiers.shift() { + self.state.cursor.select_range( + self.state.cursor.start(&self.value), + self.value.len(), + ); + } else { + self.state.cursor.move_to(self.value.len()); + } + } + keyboard::KeyCode::C + if self.state.keyboard_modifiers.command() => + { + match self.state.cursor.selection(&self.value) { + Some((start, end)) => { + clipboard.write( + self.value.select(start, end).to_string(), + ); + } + None => {} + } + } + keyboard::KeyCode::X + if self.state.keyboard_modifiers.command() => + { + match self.state.cursor.selection(&self.value) { + Some((start, end)) => { + clipboard.write( + self.value.select(start, end).to_string(), + ); + } + None => {} + } + + let mut editor = Editor::new( + &mut self.value, + &mut self.state.cursor, ); - } else { - self.state.cursor.move_to(self.value.len()); + + editor.delete(); + + let message = (self.on_change)(editor.contents()); + messages.push(message); } - } - keyboard::KeyCode::V => { - if platform::is_copy_paste_modifier_pressed(modifiers) { - if let Some(clipboard) = clipboard { + keyboard::KeyCode::V => { + if self.state.keyboard_modifiers.command() { let content = match self.state.is_pasting.take() { Some(content) => content, None => { let content: String = clipboard - .content() + .read() .unwrap_or(String::new()) .chars() .filter(|c| !c.is_control()) @@ -458,33 +565,49 @@ where messages.push(message); self.state.is_pasting = Some(content); + } else { + self.state.is_pasting = None; } - } else { - self.state.is_pasting = None; } - } - keyboard::KeyCode::A => { - if platform::is_copy_paste_modifier_pressed(modifiers) { + keyboard::KeyCode::A + if self.state.keyboard_modifiers.command() => + { self.state.cursor.select_all(&self.value); } + keyboard::KeyCode::Escape => { + self.state.is_focused = false; + self.state.is_dragging = false; + self.state.is_pasting = None; + + self.state.keyboard_modifiers = + keyboard::Modifiers::default(); + } + _ => {} } - keyboard::KeyCode::Escape => { - self.state.is_focused = false; - self.state.is_dragging = false; - self.state.is_pasting = None; - } - _ => {} - }, + + return event::Status::Captured; + } Event::Keyboard(keyboard::Event::KeyReleased { key_code, .. - }) => match key_code { - keyboard::KeyCode::V => { - self.state.is_pasting = None; + }) if self.state.is_focused => { + match key_code { + keyboard::KeyCode::V => { + self.state.is_pasting = None; + } + _ => {} } - _ => {} - }, + + return event::Status::Captured; + } + Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) + if self.state.is_focused => + { + self.state.keyboard_modifiers = modifiers; + } _ => {} } + + event::Status::Ignored } fn draw( @@ -493,37 +616,9 @@ where _defaults: &Renderer::Defaults, layout: Layout<'_>, cursor_position: Point, + _viewport: &Rectangle, ) -> Renderer::Output { - let bounds = layout.bounds(); - let text_bounds = layout.children().next().unwrap().bounds(); - - if self.is_secure { - self::Renderer::draw( - renderer, - bounds, - text_bounds, - cursor_position, - self.font, - self.size.unwrap_or(renderer.default_size()), - &self.placeholder, - &self.value.secure(), - &self.state, - &self.style, - ) - } else { - self::Renderer::draw( - renderer, - bounds, - text_bounds, - cursor_position, - self.font, - self.size.unwrap_or(renderer.default_size()), - &self.placeholder, - &self.value, - &self.state, - &self.style, - ) - } + self.draw(renderer, layout, cursor_position, None) } fn hash_layout(&self, state: &mut Hasher) { @@ -543,15 +638,12 @@ where /// Your [renderer] will need to implement this trait before being /// able to use a [`TextInput`] in your user interface. /// -/// [`TextInput`]: struct.TextInput.html -/// [renderer]: ../../renderer/index.html +/// [renderer]: crate::renderer pub trait Renderer: text::Renderer + Sized { /// The style supported by this renderer. type Style: Default; /// Returns the width of the value of the [`TextInput`]. - /// - /// [`TextInput`]: struct.TextInput.html fn measure_value(&self, value: &str, size: u16, font: Self::Font) -> f32; /// Returns the current horizontal offset of the value of the @@ -559,9 +651,6 @@ pub trait Renderer: text::Renderer + Sized { /// /// This is the amount of horizontal scrolling applied when the [`Value`] /// does not fit the [`TextInput`]. - /// - /// [`TextInput`]: struct.TextInput.html - /// [`Value`]: struct.Value.html fn offset( &self, text_bounds: Rectangle, @@ -580,10 +669,6 @@ pub trait Renderer: text::Renderer + Sized { /// - the placeholder to show when the value is empty /// - the current [`Value`] /// - the current [`State`] - /// - /// [`TextInput`]: struct.TextInput.html - /// [`Value`]: struct.Value.html - /// [`State`]: struct.State.html fn draw( &mut self, bounds: Rectangle, @@ -599,8 +684,6 @@ pub trait Renderer: text::Renderer + Sized { /// Computes the position of the text cursor at the given X coordinate of /// a [`TextInput`]. - /// - /// [`TextInput`]: struct.TextInput.html fn find_cursor_position( &self, text_bounds: Rectangle, @@ -629,8 +712,8 @@ pub trait Renderer: text::Renderer + Sized { impl<'a, Message, Renderer> From<TextInput<'a, Message, Renderer>> for Element<'a, Message, Renderer> where - Renderer: 'a + self::Renderer, Message: 'a + Clone, + Renderer: 'a + self::Renderer, { fn from( text_input: TextInput<'a, Message, Renderer>, @@ -640,8 +723,6 @@ where } /// The state of a [`TextInput`]. -/// -/// [`TextInput`]: struct.TextInput.html #[derive(Debug, Default, Clone)] pub struct State { is_focused: bool, @@ -649,20 +730,17 @@ pub struct State { is_pasting: Option<Value>, last_click: Option<mouse::Click>, cursor: Cursor, + keyboard_modifiers: keyboard::Modifiers, // TODO: Add stateful horizontal scrolling offset } impl State { /// Creates a new [`State`], representing an unfocused [`TextInput`]. - /// - /// [`State`]: struct.State.html pub fn new() -> Self { Self::default() } /// Creates a new [`State`], representing a focused [`TextInput`]. - /// - /// [`State`]: struct.State.html pub fn focused() -> Self { Self { is_focused: true, @@ -670,47 +748,49 @@ impl State { is_pasting: None, last_click: None, cursor: Cursor::default(), + keyboard_modifiers: keyboard::Modifiers::default(), } } /// Returns whether the [`TextInput`] is currently focused or not. - /// - /// [`TextInput`]: struct.TextInput.html pub fn is_focused(&self) -> bool { self.is_focused } /// Returns the [`Cursor`] of the [`TextInput`]. - /// - /// [`Cursor`]: struct.Cursor.html - /// [`TextInput`]: struct.TextInput.html pub fn cursor(&self) -> Cursor { self.cursor } + /// Focuses the [`TextInput`]. + pub fn focus(&mut self) { + self.is_focused = true; + } + + /// Unfocuses the [`TextInput`]. + pub fn unfocus(&mut self) { + self.is_focused = false; + } + /// Moves the [`Cursor`] of the [`TextInput`] to the front of the input text. - /// - /// [`Cursor`]: struct.Cursor.html - /// [`TextInput`]: struct.TextInput.html pub fn move_cursor_to_front(&mut self) { self.cursor.move_to(0); } /// Moves the [`Cursor`] of the [`TextInput`] to the end of the input text. - /// - /// [`Cursor`]: struct.Cursor.html - /// [`TextInput`]: struct.TextInput.html pub fn move_cursor_to_end(&mut self) { self.cursor.move_to(usize::MAX); } /// Moves the [`Cursor`] of the [`TextInput`] to an arbitrary location. - /// - /// [`Cursor`]: struct.Cursor.html - /// [`TextInput`]: struct.TextInput.html pub fn move_cursor_to(&mut self, position: usize) { self.cursor.move_to(position); } + + /// Selects all the content of the [`TextInput`]. + pub fn select_all(&mut self) { + self.cursor.select_range(0, usize::MAX); + } } // TODO: Reduce allocations @@ -772,23 +852,11 @@ fn find_cursor_position<Renderer: self::Renderer>( mod platform { use crate::keyboard; - pub fn is_jump_modifier_pressed( - modifiers: keyboard::ModifiersState, - ) -> bool { - if cfg!(target_os = "macos") { - modifiers.alt - } else { - modifiers.control - } - } - - pub fn is_copy_paste_modifier_pressed( - modifiers: keyboard::ModifiersState, - ) -> bool { + pub fn is_jump_modifier_pressed(modifiers: keyboard::Modifiers) -> bool { if cfg!(target_os = "macos") { - modifiers.logo + modifiers.alt() } else { - modifiers.control + modifiers.control() } } } diff --git a/native/src/widget/text_input/cursor.rs b/native/src/widget/text_input/cursor.rs index aa03bb74..4f3b159b 100644 --- a/native/src/widget/text_input/cursor.rs +++ b/native/src/widget/text_input/cursor.rs @@ -8,8 +8,6 @@ pub struct Cursor { } /// The state of a [`Cursor`]. -/// -/// [`Cursor`]: struct.Cursor.html #[derive(Debug, Copy, Clone)] pub enum State { /// Cursor without a selection @@ -34,9 +32,6 @@ impl Default for Cursor { impl Cursor { /// Returns the [`State`] of the [`Cursor`]. - /// - /// [`State`]: struct.State.html - /// [`Cursor`]: struct.Cursor.html pub fn state(&self, value: &Value) -> State { match self.state { State::Index(index) => State::Index(index.min(value.len())), @@ -53,6 +48,18 @@ impl Cursor { } } + /// Returns the current selection of the [`Cursor`] for the given [`Value`]. + /// + /// `start` is guaranteed to be <= than `end`. + pub fn selection(&self, value: &Value) -> Option<(usize, usize)> { + match self.state(value) { + State::Selection { start, end } => { + Some((start.min(end), start.max(end))) + } + _ => None, + } + } + pub(crate) fn move_to(&mut self, position: usize) { self.state = State::Index(position); } @@ -106,7 +113,7 @@ impl Cursor { State::Selection { start, end } if end > 0 => { self.select_range(start, end - 1) } - _ => (), + _ => {} } } @@ -118,7 +125,7 @@ impl Cursor { State::Selection { start, end } if end < value.len() => { self.select_range(start, end + 1) } - _ => (), + _ => {} } } @@ -166,15 +173,6 @@ impl Cursor { end.min(value.len()) } - pub(crate) fn selection(&self, value: &Value) -> Option<(usize, usize)> { - match self.state(value) { - State::Selection { start, end } => { - Some((start.min(end), start.max(end))) - } - _ => None, - } - } - fn left(&self, value: &Value) -> usize { match self.state(value) { State::Index(index) => index, diff --git a/native/src/widget/text_input/editor.rs b/native/src/widget/text_input/editor.rs index 20e42567..0b50a382 100644 --- a/native/src/widget/text_input/editor.rs +++ b/native/src/widget/text_input/editor.rs @@ -20,7 +20,7 @@ impl<'a> Editor<'a> { self.cursor.move_left(self.value); self.value.remove_many(left, right); } - _ => (), + _ => {} } self.value.insert(self.cursor.end(self.value), character); @@ -35,7 +35,7 @@ impl<'a> Editor<'a> { self.cursor.move_left(self.value); self.value.remove_many(left, right); } - _ => (), + _ => {} } self.value.insert_many(self.cursor.end(self.value), content); diff --git a/native/src/widget/text_input/value.rs b/native/src/widget/text_input/value.rs index 1e9ba45b..2034cca4 100644 --- a/native/src/widget/text_input/value.rs +++ b/native/src/widget/text_input/value.rs @@ -2,7 +2,7 @@ use unicode_segmentation::UnicodeSegmentation; /// The value of a [`TextInput`]. /// -/// [`TextInput`]: struct.TextInput.html +/// [`TextInput`]: crate::widget::TextInput // TODO: Reduce allocations, cache results (?) #[derive(Debug, Clone)] pub struct Value { @@ -11,8 +11,6 @@ pub struct Value { impl Value { /// Creates a new [`Value`] from a string slice. - /// - /// [`Value`]: struct.Value.html pub fn new(string: &str) -> Self { let graphemes = UnicodeSegmentation::graphemes(string, true) .map(String::from) @@ -21,17 +19,20 @@ impl Value { Self { graphemes } } - /// Returns the total amount of graphemes in the [`Value`]. + /// Returns whether the [`Value`] is empty or not. /// - /// [`Value`]: struct.Value.html + /// A [`Value`] is empty when it contains no graphemes. + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + + /// Returns the total amount of graphemes in the [`Value`]. pub fn len(&self) -> usize { self.graphemes.len() } /// Returns the position of the previous start of a word from the given /// grapheme `index`. - /// - /// [`Value`]: struct.Value.html pub fn previous_start_of_word(&self, index: usize) -> usize { let previous_string = &self.graphemes[..index.min(self.graphemes.len())].concat(); @@ -54,8 +55,6 @@ impl Value { /// Returns the position of the next end of a word from the given grapheme /// `index`. - /// - /// [`Value`]: struct.Value.html pub fn next_end_of_word(&self, index: usize) -> usize { let next_string = &self.graphemes[index..].concat(); @@ -74,10 +73,17 @@ impl Value { .unwrap_or(self.len()) } + /// Returns a new [`Value`] containing the graphemes from `start` until the + /// given `end`. + pub fn select(&self, start: usize, end: usize) -> Self { + let graphemes = + self.graphemes[start.min(self.len())..end.min(self.len())].to_vec(); + + Self { graphemes } + } + /// Returns a new [`Value`] containing the graphemes until the given /// `index`. - /// - /// [`Value`]: struct.Value.html pub fn until(&self, index: usize) -> Self { let graphemes = self.graphemes[..index.min(self.len())].to_vec(); @@ -85,8 +91,6 @@ impl Value { } /// Converts the [`Value`] into a `String`. - /// - /// [`Value`]: struct.Value.html pub fn to_string(&self) -> String { self.graphemes.concat() } @@ -109,8 +113,6 @@ impl Value { } /// Removes the grapheme at the given `index`. - /// - /// [`Value`]: struct.Value.html pub fn remove(&mut self, index: usize) { let _ = self.graphemes.remove(index); } @@ -122,8 +124,6 @@ impl Value { /// Returns a new [`Value`] with all its graphemes replaced with the /// dot ('•') character. - /// - /// [`Value`]: struct.Value.html pub fn secure(&self) -> Self { Self { graphemes: std::iter::repeat(String::from("•")) diff --git a/native/src/widget/toggler.rs b/native/src/widget/toggler.rs new file mode 100644 index 00000000..4035276c --- /dev/null +++ b/native/src/widget/toggler.rs @@ -0,0 +1,277 @@ +//! Show toggle controls using togglers. +use std::hash::Hash; + +use crate::{ + event, layout, mouse, row, text, Align, Clipboard, Element, Event, Hasher, + HorizontalAlignment, Layout, Length, Point, Rectangle, Row, Text, + VerticalAlignment, Widget, +}; + +/// A toggler widget +/// +/// # Example +/// +/// ``` +/// # type Toggler<Message> = iced_native::Toggler<Message, iced_native::renderer::Null>; +/// # +/// pub enum Message { +/// TogglerToggled(bool), +/// } +/// +/// let is_active = true; +/// +/// Toggler::new(is_active, String::from("Toggle me!"), |b| Message::TogglerToggled(b)); +/// ``` +#[allow(missing_debug_implementations)] +pub struct Toggler<Message, Renderer: self::Renderer + text::Renderer> { + is_active: bool, + on_toggle: Box<dyn Fn(bool) -> Message>, + label: Option<String>, + width: Length, + size: u16, + text_size: Option<u16>, + text_alignment: HorizontalAlignment, + spacing: u16, + font: Renderer::Font, + style: Renderer::Style, +} + +impl<Message, Renderer: self::Renderer + text::Renderer> + Toggler<Message, Renderer> +{ + /// Creates a new [`Toggler`]. + /// + /// It expects: + /// * a boolean describing whether the [`Toggler`] is checked or not + /// * An optional label for the [`Toggler`] + /// * a function that will be called when the [`Toggler`] is toggled. It + /// will receive the new state of the [`Toggler`] and must produce a + /// `Message`. + pub fn new<F>( + is_active: bool, + label: impl Into<Option<String>>, + f: F, + ) -> Self + where + F: 'static + Fn(bool) -> Message, + { + Toggler { + is_active, + on_toggle: Box::new(f), + label: label.into(), + width: Length::Fill, + size: <Renderer as self::Renderer>::DEFAULT_SIZE, + text_size: None, + text_alignment: HorizontalAlignment::Left, + spacing: 0, + font: Renderer::Font::default(), + style: Renderer::Style::default(), + } + } + + /// Sets the size of the [`Toggler`]. + pub fn size(mut self, size: u16) -> Self { + self.size = size; + self + } + + /// Sets the width of the [`Toggler`]. + pub fn width(mut self, width: Length) -> Self { + self.width = width; + self + } + + /// Sets the text size o the [`Toggler`]. + pub fn text_size(mut self, text_size: u16) -> Self { + self.text_size = Some(text_size); + self + } + + /// Sets the horizontal alignment of the text of the [`Toggler`] + pub fn text_alignment(mut self, alignment: HorizontalAlignment) -> Self { + self.text_alignment = alignment; + self + } + + /// Sets the spacing between the [`Toggler`] and the text. + pub fn spacing(mut self, spacing: u16) -> Self { + self.spacing = spacing; + self + } + + /// Sets the [`Font`] of the text of the [`Toggler`] + pub fn font(mut self, font: Renderer::Font) -> Self { + self.font = font; + self + } + + /// Sets the style of the [`Toggler`]. + pub fn style(mut self, style: impl Into<Renderer::Style>) -> Self { + self.style = style.into(); + self + } +} + +impl<Message, Renderer> Widget<Message, Renderer> for Toggler<Message, Renderer> +where + Renderer: self::Renderer + text::Renderer + row::Renderer, +{ + fn width(&self) -> Length { + self.width + } + + fn height(&self) -> Length { + Length::Shrink + } + + fn layout( + &self, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + let mut row = Row::<(), Renderer>::new() + .width(self.width) + .spacing(self.spacing) + .align_items(Align::Center); + + if let Some(label) = &self.label { + row = row.push( + Text::new(label) + .horizontal_alignment(self.text_alignment) + .font(self.font) + .width(self.width) + .size(self.text_size.unwrap_or(renderer.default_size())), + ); + } + + row = row.push( + Row::new() + .width(Length::Units(2 * self.size)) + .height(Length::Units(self.size)), + ); + + row.layout(renderer, limits) + } + + fn on_event( + &mut self, + event: Event, + layout: Layout<'_>, + cursor_position: Point, + _renderer: &Renderer, + _clipboard: &mut dyn Clipboard, + messages: &mut Vec<Message>, + ) -> event::Status { + match event { + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => { + let mouse_over = layout.bounds().contains(cursor_position); + + if mouse_over { + messages.push((self.on_toggle)(!self.is_active)); + + event::Status::Captured + } else { + event::Status::Ignored + } + } + _ => event::Status::Ignored, + } + } + + fn draw( + &self, + renderer: &mut Renderer, + defaults: &Renderer::Defaults, + layout: Layout<'_>, + cursor_position: Point, + _viewport: &Rectangle, + ) -> Renderer::Output { + let bounds = layout.bounds(); + let mut children = layout.children(); + + let label = match &self.label { + Some(label) => { + let label_layout = children.next().unwrap(); + + Some(text::Renderer::draw( + renderer, + defaults, + label_layout.bounds(), + &label, + self.text_size.unwrap_or(renderer.default_size()), + self.font, + None, + self.text_alignment, + VerticalAlignment::Center, + )) + } + + None => None, + }; + + let toggler_layout = children.next().unwrap(); + let toggler_bounds = toggler_layout.bounds(); + + let is_mouse_over = bounds.contains(cursor_position); + + self::Renderer::draw( + renderer, + toggler_bounds, + self.is_active, + is_mouse_over, + label, + &self.style, + ) + } + + fn hash_layout(&self, state: &mut Hasher) { + struct Marker; + std::any::TypeId::of::<Marker>().hash(state); + + self.label.hash(state) + } +} + +/// The renderer of a [`Toggler`]. +/// +/// Your [renderer] will need to implement this trait before being +/// able to use a [`Toggler`] in your user interface. +/// +/// [renderer]: ../../renderer/index.html +pub trait Renderer: crate::Renderer { + /// The style supported by this renderer. + type Style: Default; + + /// The default size of a [`Toggler`]. + const DEFAULT_SIZE: u16; + + /// Draws a [`Toggler`]. + /// + /// It receives: + /// * the bounds of the [`Toggler`] + /// * whether the [`Toggler`] is activated or not + /// * whether the mouse is over the [`Toggler`] or not + /// * the drawn label of the [`Toggler`] + /// * the style of the [`Toggler`] + fn draw( + &mut self, + bounds: Rectangle, + is_active: bool, + is_mouse_over: bool, + label: Option<Self::Output>, + style: &Self::Style, + ) -> Self::Output; +} + +impl<'a, Message, Renderer> From<Toggler<Message, Renderer>> + for Element<'a, Message, Renderer> +where + Renderer: 'a + self::Renderer + text::Renderer + row::Renderer, + Message: 'a, +{ + fn from( + toggler: Toggler<Message, Renderer>, + ) -> Element<'a, Message, Renderer> { + Element::new(toggler) + } +} diff --git a/native/src/widget/tooltip.rs b/native/src/widget/tooltip.rs new file mode 100644 index 00000000..276afd41 --- /dev/null +++ b/native/src/widget/tooltip.rs @@ -0,0 +1,210 @@ +//! Display a widget over another. +use std::hash::Hash; + +use iced_core::Rectangle; + +use crate::widget::container; +use crate::widget::text::{self, Text}; +use crate::{ + event, layout, Clipboard, Element, Event, Hasher, Layout, Length, Point, + Widget, +}; + +/// An element to display a widget over another. +#[allow(missing_debug_implementations)] +pub struct Tooltip<'a, Message, Renderer: self::Renderer> { + content: Element<'a, Message, Renderer>, + tooltip: Text<Renderer>, + position: Position, + style: <Renderer as container::Renderer>::Style, + gap: u16, + padding: u16, +} + +impl<'a, Message, Renderer> Tooltip<'a, Message, Renderer> +where + Renderer: self::Renderer, +{ + /// Creates an empty [`Tooltip`]. + /// + /// [`Tooltip`]: struct.Tooltip.html + pub fn new( + content: impl Into<Element<'a, Message, Renderer>>, + tooltip: impl ToString, + position: Position, + ) -> Self { + Tooltip { + content: content.into(), + tooltip: Text::new(tooltip.to_string()), + position, + style: Default::default(), + gap: 0, + padding: Renderer::DEFAULT_PADDING, + } + } + + /// Sets the size of the text of the [`Tooltip`]. + pub fn size(mut self, size: u16) -> Self { + self.tooltip = self.tooltip.size(size); + self + } + + /// Sets the font of the [`Tooltip`]. + /// + /// [`Font`]: Renderer::Font + pub fn font(mut self, font: impl Into<Renderer::Font>) -> Self { + self.tooltip = self.tooltip.font(font); + self + } + + /// Sets the gap between the content and its [`Tooltip`]. + pub fn gap(mut self, gap: u16) -> Self { + self.gap = gap; + self + } + + /// Sets the padding of the [`Tooltip`]. + pub fn padding(mut self, padding: u16) -> Self { + self.padding = padding; + self + } + + /// Sets the style of the [`Tooltip`]. + pub fn style( + mut self, + style: impl Into<<Renderer as container::Renderer>::Style>, + ) -> Self { + self.style = style.into(); + self + } +} + +/// The position of the tooltip. Defaults to following the cursor. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Position { + /// The tooltip will follow the cursor. + FollowCursor, + /// The tooltip will appear on the top of the widget. + Top, + /// The tooltip will appear on the bottom of the widget. + Bottom, + /// The tooltip will appear on the left of the widget. + Left, + /// The tooltip will appear on the right of the widget. + Right, +} + +impl<'a, Message, Renderer> Widget<Message, Renderer> + for Tooltip<'a, Message, Renderer> +where + Renderer: self::Renderer, +{ + fn width(&self) -> Length { + self.content.width() + } + + fn height(&self) -> Length { + self.content.height() + } + + fn layout( + &self, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + self.content.layout(renderer, limits) + } + + fn on_event( + &mut self, + event: Event, + layout: Layout<'_>, + cursor_position: Point, + renderer: &Renderer, + clipboard: &mut dyn Clipboard, + messages: &mut Vec<Message>, + ) -> event::Status { + self.content.widget.on_event( + event, + layout, + cursor_position, + renderer, + clipboard, + messages, + ) + } + + fn draw( + &self, + renderer: &mut Renderer, + defaults: &Renderer::Defaults, + layout: Layout<'_>, + cursor_position: Point, + viewport: &Rectangle, + ) -> Renderer::Output { + self::Renderer::draw( + renderer, + defaults, + cursor_position, + layout, + viewport, + &self.content, + &self.tooltip, + self.position, + &self.style, + self.gap, + self.padding, + ) + } + + fn hash_layout(&self, state: &mut Hasher) { + struct Marker; + std::any::TypeId::of::<Marker>().hash(state); + + self.content.hash_layout(state); + } +} + +/// The renderer of a [`Tooltip`]. +/// +/// Your [renderer] will need to implement this trait before being +/// able to use a [`Tooltip`] in your user interface. +/// +/// [`Tooltip`]: struct.Tooltip.html +/// [renderer]: ../../renderer/index.html +pub trait Renderer: + crate::Renderer + text::Renderer + container::Renderer +{ + /// The default padding of a [`Tooltip`] drawn by this renderer. + const DEFAULT_PADDING: u16; + + /// Draws a [`Tooltip`]. + /// + /// [`Tooltip`]: struct.Tooltip.html + fn draw<Message>( + &mut self, + defaults: &Self::Defaults, + cursor_position: Point, + content_layout: Layout<'_>, + viewport: &Rectangle, + content: &Element<'_, Message, Self>, + tooltip: &Text<Self>, + position: Position, + style: &<Self as container::Renderer>::Style, + gap: u16, + padding: u16, + ) -> Self::Output; +} + +impl<'a, Message, Renderer> From<Tooltip<'a, Message, Renderer>> + for Element<'a, Message, Renderer> +where + Renderer: 'a + self::Renderer, + Message: 'a, +{ + fn from( + column: Tooltip<'a, Message, Renderer>, + ) -> Element<'a, Message, Renderer> { + Element::new(column) + } +} |