diff options
Diffstat (limited to 'native/src/widget')
29 files changed, 3880 insertions, 973 deletions
diff --git a/native/src/widget/button.rs b/native/src/widget/button.rs index 81dbe7c5..7d5eb30c 100644 --- a/native/src/widget/button.rs +++ b/native/src/widget/button.rs @@ -1,13 +1,12 @@ //! Allow your users to perform actions by pressing a button. //! //! A [`Button`] has some local [`State`]. -//! -//! [`Button`]: struct.Button.html -//! [`State`]: struct.State.html +use crate::event::{self, Event}; +use crate::layout; +use crate::mouse; +use crate::touch::{self, Touch}; use crate::{ - input::{mouse, touch, ButtonState, Touch}, - layout, Clipboard, Element, Event, Hasher, Layout, Length, Point, - Rectangle, Widget, + Clipboard, Element, Hasher, Layout, Length, Point, Rectangle, Widget, }; use std::hash::Hash; @@ -19,6 +18,7 @@ use std::hash::Hash; /// # type Button<'a, Message> = /// # iced_native::Button<'a, Message, iced_native::renderer::Null>; /// # +/// #[derive(Clone)] /// enum Message { /// ButtonPressed, /// } @@ -42,13 +42,11 @@ pub struct Button<'a, Message, Renderer: self::Renderer> { impl<'a, Message, Renderer> Button<'a, Message, Renderer> where + Message: Clone, Renderer: self::Renderer, { /// Creates a new [`Button`] with some local [`State`] and the given /// content. - /// - /// [`Button`]: struct.Button.html - /// [`State`]: struct.State.html pub fn new<E>(state: &'a mut State, content: E) -> Self where E: Into<Element<'a, Message, Renderer>>, @@ -67,56 +65,42 @@ where } /// Sets the width of the [`Button`]. - /// - /// [`Button`]: struct.Button.html pub fn width(mut self, width: Length) -> Self { self.width = width; self } /// Sets the height of the [`Button`]. - /// - /// [`Button`]: struct.Button.html pub fn height(mut self, height: Length) -> Self { self.height = height; self } /// Sets the minimum width of the [`Button`]. - /// - /// [`Button`]: struct.Button.html pub fn min_width(mut self, min_width: u32) -> Self { self.min_width = min_width; self } /// Sets the minimum height of the [`Button`]. - /// - /// [`Button`]: struct.Button.html pub fn min_height(mut self, min_height: u32) -> Self { self.min_height = min_height; self } /// Sets the padding of the [`Button`]. - /// - /// [`Button`]: struct.Button.html pub fn padding(mut self, padding: u16) -> Self { self.padding = padding; self } /// Sets the message that will be produced when the [`Button`] is pressed. - /// - /// [`Button`]: struct.Button.html pub fn on_press(mut self, msg: Message) -> Self { self.on_press = Some(msg); self } /// Sets the style of the [`Button`]. - /// - /// [`Button`]: struct.Button.html pub fn style(mut self, style: impl Into<Renderer::Style>) -> Self { self.style = style.into(); self @@ -124,8 +108,6 @@ where } /// The local state of a [`Button`]. -/// -/// [`Button`]: struct.Button.html #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub struct State { is_pressed: bool, @@ -133,8 +115,6 @@ pub struct State { impl State { /// Creates a new [`State`]. - /// - /// [`State`]: struct.State.html pub fn new() -> State { State::default() } @@ -143,8 +123,8 @@ impl State { impl<'a, Message, Renderer> Widget<Message, Renderer> for Button<'a, Message, Renderer> where - Renderer: self::Renderer, Message: Clone, + Renderer: self::Renderer, { fn width(&self) -> Length { self.width @@ -183,12 +163,9 @@ where messages: &mut Vec<Message>, _renderer: &Renderer, _clipboard: Option<&dyn Clipboard>, - ) { + ) -> event::Status { match event { - Event::Mouse(mouse::Event::Input { - button: mouse::Button::Left, - state: ButtonState::Pressed, - }) + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) | Event::Touch(Touch { phase: touch::Phase::Started, .. @@ -196,26 +173,29 @@ where 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::Input { - button: mouse::Button::Left, - state: ButtonState::Released, - }) + Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) | Event::Touch(Touch { phase: touch::Phase::Ended, .. }) => { if let Some(on_press) = self.on_press.clone() { let bounds = layout.bounds(); - let is_clicked = self.state.is_pressed - && bounds.contains(cursor_position); - self.state.is_pressed = false; + if self.state.is_pressed { + 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; } } } @@ -227,6 +207,8 @@ where } _ => {} } + + event::Status::Ignored } fn draw( @@ -235,6 +217,7 @@ where defaults: &Renderer::Defaults, layout: Layout<'_>, cursor_position: Point, + _viewport: &Rectangle, ) -> Renderer::Output { renderer.draw( defaults, @@ -249,6 +232,9 @@ where } fn hash_layout(&self, state: &mut Hasher) { + struct Marker; + std::any::TypeId::of::<Marker>().hash(state); + self.width.hash(state); self.content.hash_layout(state); } @@ -259,20 +245,15 @@ where /// Your [renderer] will need to implement this trait before being /// able to use a [`Button`] in your user interface. /// -/// [`Button`]: struct.Button.html -/// [renderer]: ../../renderer/index.html +/// [renderer]: crate::renderer pub trait Renderer: crate::Renderer + Sized { /// The default padding of a [`Button`]. - /// - /// [`Button`]: struct.Button.html const DEFAULT_PADDING: u16; /// The style supported by this renderer. type Style: Default; /// Draws a [`Button`]. - /// - /// [`Button`]: struct.Button.html fn draw<Message>( &mut self, defaults: &Self::Defaults, @@ -289,8 +270,8 @@ pub trait Renderer: crate::Renderer + Sized { impl<'a, Message, Renderer> From<Button<'a, Message, Renderer>> for Element<'a, Message, Renderer> where - Renderer: 'static + self::Renderer, - Message: 'static + Clone, + 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 7b2345de..92175b25 100644 --- a/native/src/widget/checkbox.rs +++ b/native/src/widget/checkbox.rs @@ -1,11 +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::{self, Touch}; use crate::{ - input::{mouse, touch, ButtonState, Touch}, - layout, row, text, Align, Clipboard, Element, Event, Font, Hasher, - HorizontalAlignment, Layout, Length, Point, Rectangle, Row, Text, - VerticalAlignment, Widget, + Align, Clipboard, Element, Hasher, HorizontalAlignment, Layout, Length, + Point, Rectangle, Row, Text, VerticalAlignment, Widget, }; /// A box that can be checked. @@ -33,7 +37,8 @@ pub struct Checkbox<Message, Renderer: self::Renderer + text::Renderer> { width: Length, size: u16, spacing: u16, - text_size: u16, + text_size: Option<u16>, + font: Renderer::Font, style: Renderer::Style, } @@ -48,59 +53,56 @@ 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: &str, f: F) -> Self + pub fn new<F>(is_checked: bool, label: impl Into<String>, f: F) -> Self where F: 'static + Fn(bool) -> Message, { Checkbox { is_checked, on_toggle: Box::new(f), - label: String::from(label), + label: label.into(), width: Length::Shrink, size: <Renderer as self::Renderer>::DEFAULT_SIZE, spacing: Renderer::DEFAULT_SPACING, - text_size: <Renderer as text::Renderer>::DEFAULT_SIZE, + text_size: None, + font: Renderer::Font::default(), style: Renderer::Style::default(), } } /// Sets the size of the [`Checkbox`]. - /// - /// [`Checkbox`]: struct.Checkbox.html pub fn size(mut self, size: u16) -> Self { self.size = size; self } /// Sets the width of the [`Checkbox`]. - /// - /// [`Checkbox`]: struct.Checkbox.html pub fn width(mut self, width: Length) -> Self { self.width = width; self } /// Sets the spacing between the [`Checkbox`] and the text. - /// - /// [`Checkbox`]: struct.Checkbox.html pub fn spacing(mut self, spacing: u16) -> Self { self.spacing = spacing; self } /// Sets the text size of the [`Checkbox`]. - /// - /// [`Checkbox`]: struct.Checkbox.html pub fn text_size(mut self, text_size: u16) -> Self { - self.text_size = text_size; + self.text_size = Some(text_size); self } - /// Sets the style of the [`Checkbox`]. + /// Sets the [`Font`] of the text of the [`Checkbox`]. /// - /// [`Checkbox`]: struct.Checkbox.html + /// [`Font`]: crate::widget::text::Renderer::Font + pub fn font(mut self, font: Renderer::Font) -> Self { + self.font = font; + self + } + + /// Sets the style of the [`Checkbox`]. pub fn style(mut self, style: impl Into<Renderer::Style>) -> Self { self.style = style.into(); self @@ -136,8 +138,9 @@ where ) .push( Text::new(&self.label) + .font(self.font) .width(self.width) - .size(self.text_size), + .size(self.text_size.unwrap_or(renderer.default_size())), ) .layout(renderer, limits) } @@ -150,12 +153,9 @@ where messages: &mut Vec<Message>, _renderer: &Renderer, _clipboard: Option<&dyn Clipboard>, - ) { + ) -> event::Status { match event { - Event::Mouse(mouse::Event::Input { - button: mouse::Button::Left, - state: ButtonState::Pressed, - }) + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) | Event::Touch(Touch { phase: touch::Phase::Started, .. @@ -164,10 +164,14 @@ where if mouse_over { messages.push((self.on_toggle)(!self.is_checked)); + + return event::Status::Captured; } } _ => {} } + + event::Status::Ignored } fn draw( @@ -176,6 +180,7 @@ where defaults: &Renderer::Defaults, layout: Layout<'_>, cursor_position: Point, + _viewport: &Rectangle, ) -> Renderer::Output { let bounds = layout.bounds(); let mut children = layout.children(); @@ -189,8 +194,8 @@ where defaults, label_layout.bounds(), &self.label, - self.text_size, - Font::Default, + self.text_size.unwrap_or(renderer.default_size()), + self.font, None, HorizontalAlignment::Left, VerticalAlignment::Center, @@ -209,6 +214,9 @@ where } fn hash_layout(&self, state: &mut Hasher) { + struct Marker; + std::any::TypeId::of::<Marker>().hash(state); + self.label.hash(state); } } @@ -218,20 +226,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`]. @@ -241,8 +244,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, @@ -256,8 +257,8 @@ pub trait Renderer: crate::Renderer { impl<'a, Message, Renderer> From<Checkbox<Message, Renderer>> for Element<'a, Message, Renderer> where - Renderer: 'static + self::Renderer + text::Renderer + row::Renderer, - Message: 'static, + Renderer: 'a + self::Renderer + text::Renderer + row::Renderer, + Message: 'a, { fn from( checkbox: Checkbox<Message, Renderer>, diff --git a/native/src/widget/column.rs b/native/src/widget/column.rs index a7a6f242..e0e88d31 100644 --- a/native/src/widget/column.rs +++ b/native/src/widget/column.rs @@ -1,18 +1,16 @@ //! Distribute content vertically. use std::hash::Hash; +use crate::event::{self, Event}; +use crate::layout; +use crate::overlay; use crate::{ - layout, Align, Clipboard, Element, Event, Hasher, Layout, Length, Point, - Widget, + Align, Clipboard, Element, Hasher, Layout, Length, Point, Rectangle, Widget, }; use std::u32; /// A container that distributes its contents vertically. -/// -/// A [`Column`] will try to fill the horizontal space of its container. -/// -/// [`Column`]: struct.Column.html #[allow(missing_debug_implementations)] pub struct Column<'a, Message, Renderer> { spacing: u16, @@ -27,15 +25,11 @@ pub struct Column<'a, Message, Renderer> { impl<'a, Message, Renderer> Column<'a, Message, Renderer> { /// Creates an empty [`Column`]. - /// - /// [`Column`]: struct.Column.html pub fn new() -> Self { Self::with_children(Vec::new()) } /// Creates a [`Column`] with the given elements. - /// - /// [`Column`]: struct.Column.html pub fn with_children( children: Vec<Element<'a, Message, Renderer>>, ) -> Self { @@ -62,56 +56,42 @@ impl<'a, Message, Renderer> Column<'a, Message, Renderer> { } /// Sets the padding of the [`Column`]. - /// - /// [`Column`]: struct.Column.html pub fn padding(mut self, units: u16) -> Self { self.padding = units; self } /// Sets the width of the [`Column`]. - /// - /// [`Column`]: struct.Column.html pub fn width(mut self, width: Length) -> Self { self.width = width; self } /// Sets the height of the [`Column`]. - /// - /// [`Column`]: struct.Column.html pub fn height(mut self, height: Length) -> Self { self.height = height; self } /// Sets the maximum width of the [`Column`]. - /// - /// [`Column`]: struct.Column.html pub fn max_width(mut self, max_width: u32) -> Self { self.max_width = max_width; self } /// Sets the maximum height of the [`Column`] in pixels. - /// - /// [`Column`]: struct.Column.html pub fn max_height(mut self, max_height: u32) -> Self { self.max_height = max_height; self } /// Sets the horizontal alignment of the contents of the [`Column`] . - /// - /// [`Column`]: struct.Column.html pub fn align_items(mut self, align: Align) -> Self { self.align_items = align; self } /// Adds an element to the [`Column`]. - /// - /// [`Column`]: struct.Column.html pub fn push<E>(mut self, child: E) -> Self where E: Into<Element<'a, Message, Renderer>>, @@ -164,9 +144,11 @@ where messages: &mut Vec<Message>, renderer: &Renderer, clipboard: Option<&dyn Clipboard>, - ) { - self.children.iter_mut().zip(layout.children()).for_each( - |(child, layout)| { + ) -> event::Status { + self.children + .iter_mut() + .zip(layout.children()) + .map(|(child, layout)| { child.widget.on_event( event.clone(), layout, @@ -175,8 +157,8 @@ where renderer, clipboard, ) - }, - ); + }) + .fold(event::Status::Ignored, event::Status::merge) } fn draw( @@ -185,23 +167,44 @@ 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) { - 0.hash(state); + struct Marker; + std::any::TypeId::of::<Marker>().hash(state); + self.width.hash(state); self.height.hash(state); self.max_width.hash(state); self.max_height.hash(state); self.align_items.hash(state); self.spacing.hash(state); + self.padding.hash(state); for child in &self.children { child.widget.hash_layout(state); } } + + fn overlay( + &mut self, + layout: Layout<'_>, + ) -> Option<overlay::Element<'_, Message, Renderer>> { + self.children + .iter_mut() + .zip(layout.children()) + .filter_map(|(child, layout)| child.widget.overlay(layout)) + .next() + } } /// The renderer of a [`Column`]. @@ -209,8 +212,7 @@ where /// Your [renderer] will need to implement this trait before being /// able to use a [`Column`] in your user interface. /// -/// [`Column`]: struct.Column.html -/// [renderer]: ../../renderer/index.html +/// [renderer]: crate::renderer pub trait Renderer: crate::Renderer + Sized { /// Draws a [`Column`]. /// @@ -218,15 +220,13 @@ pub trait Renderer: crate::Renderer + Sized { /// - the children of the [`Column`] /// - the [`Layout`] of the [`Column`] and its children /// - the cursor position - /// - /// [`Column`]: struct.Row.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; } @@ -234,7 +234,7 @@ impl<'a, Message, Renderer> From<Column<'a, Message, Renderer>> for Element<'a, Message, Renderer> where Renderer: 'a + self::Renderer, - Message: 'static, + Message: 'a, { fn from( column: Column<'a, Message, Renderer>, diff --git a/native/src/widget/container.rs b/native/src/widget/container.rs index 3459a832..65764148 100644 --- a/native/src/widget/container.rs +++ b/native/src/widget/container.rs @@ -1,9 +1,11 @@ //! Decorate content and apply alignment. use std::hash::Hash; +use crate::event::{self, Event}; +use crate::layout; +use crate::overlay; use crate::{ - layout, Align, Clipboard, Element, Event, Hasher, Layout, Length, Point, - Rectangle, Widget, + Align, Clipboard, Element, Hasher, Layout, Length, Point, Rectangle, Widget, }; use std::u32; @@ -13,6 +15,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, width: Length, height: Length, max_width: u32, @@ -28,13 +31,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, width: Length::Shrink, height: Length::Shrink, max_width: u32::MAX, @@ -46,73 +48,61 @@ where } } + /// Sets the padding of the [`Container`]. + pub fn padding(mut self, units: u16) -> Self { + self.padding = units; + self + } + /// Sets the width of the [`Container`]. - /// - /// [`Container`]: struct.Container.html pub fn width(mut self, width: Length) -> Self { self.width = width; self } /// Sets the height of the [`Container`]. - /// - /// [`Container`]: struct.Container.html pub fn height(mut self, height: Length) -> Self { self.height = height; self } /// Sets the maximum width of the [`Container`]. - /// - /// [`Container`]: struct.Container.html pub fn max_width(mut self, max_width: u32) -> Self { self.max_width = max_width; self } /// Sets the maximum height of the [`Container`] in pixels. - /// - /// [`Container`]: struct.Container.html pub fn max_height(mut self, max_height: u32) -> Self { self.max_height = max_height; self } - + /// Sets the content alignment for the horizontal axis of the [`Container`]. - /// - /// [`Container`]: struct.Container.html pub fn align_x(mut self, alignment: Align) -> Self { self.horizontal_alignment = alignment; self } /// Sets the content alignment for the vertical axis of the [`Container`]. - /// - /// [`Container`]: struct.Container.html pub fn align_y(mut self, alignment: Align) -> Self { self.vertical_alignment = alignment; self } /// Centers the contents in the horizontal axis of the [`Container`]. - /// - /// [`Container`]: struct.Container.html pub fn center_x(mut self) -> Self { self.horizontal_alignment = Align::Center; self } /// Centers the contents in the vertical axis of the [`Container`]. - /// - /// [`Container`]: struct.Container.html pub fn center_y(mut self) -> Self { self.vertical_alignment = Align::Center; self } /// Sets the style of the [`Container`]. - /// - /// [`Container`]: struct.Container.html pub fn style(mut self, style: impl Into<Renderer::Style>) -> Self { self.style = style.into(); self @@ -137,19 +127,23 @@ 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); + .height(self.height) + .pad(padding); let mut content = self.content.layout(renderer, &limits.loose()); let size = limits.resolve(content.size()); + content.move_to(Point::new(padding, padding)); content.align(self.horizontal_alignment, self.vertical_alignment, size); - layout::Node::with_children(size, vec![content]) + layout::Node::with_children(size.pad(padding), vec![content]) } fn on_event( @@ -160,7 +154,7 @@ where messages: &mut Vec<Message>, renderer: &Renderer, clipboard: Option<&dyn Clipboard>, - ) { + ) -> event::Status { self.content.widget.on_event( event, layout.children().next().unwrap(), @@ -177,11 +171,13 @@ where defaults: &Renderer::Defaults, layout: Layout<'_>, cursor_position: Point, + viewport: &Rectangle, ) -> Renderer::Output { renderer.draw( defaults, layout.bounds(), cursor_position, + viewport, &self.style, &self.content, layout.children().next().unwrap(), @@ -189,7 +185,10 @@ where } fn hash_layout(&self, state: &mut Hasher) { - 0.hash(state); + struct Marker; + std::any::TypeId::of::<Marker>().hash(state); + + self.padding.hash(state); self.width.hash(state); self.height.hash(state); self.max_width.hash(state); @@ -197,6 +196,13 @@ where self.content.hash_layout(state); } + + fn overlay( + &mut self, + layout: Layout<'_>, + ) -> Option<overlay::Element<'_, Message, Renderer>> { + self.content.overlay(layout.children().next().unwrap()) + } } /// The renderer of a [`Container`]. @@ -204,20 +210,18 @@ where /// Your [renderer] will need to implement this trait before being /// able to use a [`Container`] in your user interface. /// -/// [`Container`]: struct.Container.html -/// [renderer]: ../../renderer/index.html +/// [renderer]: crate::renderer pub trait Renderer: crate::Renderer { /// The style supported by this renderer. type Style: Default; /// Draws a [`Container`]. - /// - /// [`Container`]: struct.Container.html fn draw<Message>( &mut self, defaults: &Self::Defaults, bounds: Rectangle, cursor_position: Point, + viewport: &Rectangle, style: &Self::Style, content: &Element<'_, Message, Self>, content_layout: Layout<'_>, @@ -228,7 +232,7 @@ impl<'a, Message, Renderer> From<Container<'a, Message, Renderer>> for Element<'a, Message, Renderer> where Renderer: 'a + self::Renderer, - Message: 'static, + Message: 'a, { fn from( column: Container<'a, Message, Renderer>, diff --git a/native/src/widget/image.rs b/native/src/widget/image.rs index fbe38bfc..51d7ba26 100644 --- a/native/src/widget/image.rs +++ b/native/src/widget/image.rs @@ -1,5 +1,6 @@ //! Display images in your user interface. -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 _}, @@ -27,8 +28,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 +37,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,11 +92,15 @@ where _defaults: &Renderer::Defaults, layout: Layout<'_>, _cursor_position: Point, + _viewport: &Rectangle, ) -> Renderer::Output { renderer.draw(self.handle.clone(), layout) } fn hash_layout(&self, state: &mut Hasher) { + struct Marker; + std::any::TypeId::of::<Marker>().hash(state); + self.handle.hash(state); self.width.hash(state); self.height.hash(state); @@ -109,8 +108,6 @@ where } /// An [`Image`] handle. -/// -/// [`Image`]: struct.Image.html #[derive(Debug, Clone)] pub struct Handle { id: u64, @@ -120,18 +117,16 @@ pub struct Handle { impl Handle { /// Creates an image [`Handle`] pointing to the image of the given path. /// - /// [`Handle`]: struct.Handle.html + /// Makes an educated guess about the image format by examining the data in the file. pub fn from_path<T: Into<PathBuf>>(path: T) -> Handle { Self::from_data(Data::Path(path.into())) } /// Creates an image [`Handle`] containing the image pixels directly. This - /// function expects the input data to be provided as a `Vec<u8>` of BGRA + /// function expects the input data to be provided as a `Vec<u8>` of BGRA /// 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, @@ -142,10 +137,10 @@ impl Handle { /// Creates an image [`Handle`] containing the image data directly. /// + /// Makes an educated guess about the image format by examining the given data. + /// /// 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)) } @@ -161,29 +156,22 @@ 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 } } -impl From<String> for Handle { - fn from(path: String) -> Handle { - Handle::from_path(path) - } -} - -impl From<&str> for Handle { - fn from(path: &str) -> Handle { - Handle::from_path(path) +impl<T> From<T> for Handle +where + T: Into<PathBuf>, +{ + fn from(path: T) -> Handle { + Handle::from_path(path.into()) } } @@ -194,8 +182,6 @@ impl Hash for Handle { } /// The data of an [`Image`]. -/// -/// [`Image`]: struct.Image.html #[derive(Clone, Hash)] pub enum Data { /// File data @@ -232,17 +218,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/pane_grid.rs b/native/src/widget/pane_grid.rs new file mode 100644 index 00000000..ff19cbc2 --- /dev/null +++ b/native/src/widget/pane_grid.rs @@ -0,0 +1,639 @@ +//! Let your users split regions of your application and organize layout dynamically. +//! +//! [](https://gfycat.com/mixedflatjellyfish) +//! +//! # Example +//! The [`pane_grid` example] showcases how to use a [`PaneGrid`] with resizing, +//! drag and drop, and hotkey support. +//! +//! [`pane_grid` example]: https://github.com/hecrj/iced/tree/0.2/examples/pane_grid +mod axis; +mod configuration; +mod content; +mod direction; +mod node; +mod pane; +mod split; +mod state; +mod title_bar; + +pub use axis::Axis; +pub use configuration::Configuration; +pub use content::Content; +pub use direction::Direction; +pub use node::Node; +pub use pane::Pane; +pub use split::Split; +pub use state::State; +pub use title_bar::TitleBar; + +use crate::container; +use crate::event::{self, Event}; +use crate::layout; +use crate::mouse; +use crate::overlay; +use crate::row; +use crate::text; +use crate::{ + Clipboard, Element, Hasher, Layout, Length, Point, Rectangle, Size, Vector, + Widget, +}; + +/// A collection of panes distributed using either vertical or horizontal splits +/// to completely fill the space available. +/// +/// [](https://gfycat.com/frailfreshairedaleterrier) +/// +/// This distribution of space is common in tiling window managers (like +/// [`awesome`](https://awesomewm.org/), [`i3`](https://i3wm.org/), or even +/// [`tmux`](https://github.com/tmux/tmux)). +/// +/// A [`PaneGrid`] supports: +/// +/// * Vertical and horizontal splits +/// * Tracking of the last active pane +/// * Mouse-based resizing +/// * Drag and drop to reorganize panes +/// * Hotkey support +/// * Configurable modifier keys +/// * [`State`] API to perform actions programmatically (`split`, `swap`, `resize`, etc.) +/// +/// ## Example +/// +/// ``` +/// # use iced_native::{pane_grid, Text}; +/// # +/// # type PaneGrid<'a, Message> = +/// # iced_native::PaneGrid<'a, Message, iced_native::renderer::Null>; +/// # +/// enum PaneState { +/// SomePane, +/// AnotherKindOfPane, +/// } +/// +/// enum Message { +/// PaneDragged(pane_grid::DragEvent), +/// PaneResized(pane_grid::ResizeEvent), +/// } +/// +/// let (mut state, _) = pane_grid::State::new(PaneState::SomePane); +/// +/// let pane_grid = +/// 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"), +/// }) +/// }) +/// .on_drag(Message::PaneDragged) +/// .on_resize(10, Message::PaneResized); +/// ``` +#[allow(missing_debug_implementations)] +pub struct PaneGrid<'a, Message, Renderer: self::Renderer> { + state: &'a mut state::Internal, + elements: Vec<(Pane, Content<'a, Message, Renderer>)>, + width: Length, + height: Length, + spacing: u16, + 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>)>, +} + +impl<'a, Message, Renderer> PaneGrid<'a, Message, Renderer> +where + Renderer: self::Renderer, +{ + /// Creates a [`PaneGrid`] with the given [`State`] and view function. + /// + /// The view function will be called to display each [`Pane`] present in the + /// [`State`]. + pub fn new<T>( + state: &'a mut State<T>, + view: impl Fn(Pane, &'a mut T) -> Content<'a, Message, Renderer>, + ) -> Self { + let elements = { + state + .panes + .iter_mut() + .map(|(pane, pane_state)| (*pane, view(*pane, pane_state))) + .collect() + }; + + Self { + state: &mut state.internal, + elements, + width: Length::Fill, + height: Length::Fill, + spacing: 0, + on_click: None, + on_drag: None, + on_resize: None, + } + } + + /// Sets the width of the [`PaneGrid`]. + pub fn width(mut self, width: Length) -> Self { + self.width = width; + self + } + + /// Sets the height of the [`PaneGrid`]. + pub fn height(mut self, height: Length) -> Self { + self.height = height; + self + } + + /// Sets the spacing _between_ the panes of the [`PaneGrid`]. + pub fn spacing(mut self, units: u16) -> Self { + self.spacing = units; + self + } + + /// 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. + pub fn on_drag<F>(mut self, f: F) -> Self + where + F: 'a + Fn(DragEvent) -> Message, + { + self.on_drag = Some(Box::new(f)); + self + } + + /// Enables the resize interactions of the [`PaneGrid`], which will + /// use the provided function to produce messages. + /// + /// The `leeway` describes the amount of space around a split that can be + /// used to grab it. + /// + /// The grabbable area of a split will have a length of `spacing + leeway`, + /// properly centered. In other words, a length of + /// `(spacing + leeway) / 2.0` on either side of the split line. + pub fn on_resize<F>(mut self, leeway: u16, f: F) -> Self + where + F: 'a + Fn(ResizeEvent) -> Message, + { + self.on_resize = Some((leeway, Box::new(f))); + self + } +} + +impl<'a, Message, Renderer> PaneGrid<'a, Message, Renderer> +where + Renderer: self::Renderer, +{ + fn click_pane( + &mut self, + layout: Layout<'_>, + cursor_position: Point, + messages: &mut Vec<Message>, + ) { + let mut clicked_region = + self.elements.iter().zip(layout.children()).filter( + |(_, layout)| layout.bounds().contains(cursor_position), + ); + + if let Some(((pane, content), layout)) = clicked_region.next() { + if let Some(on_click) = &self.on_click { + messages.push(on_click(*pane)); + } + + if let Some(on_drag) = &self.on_drag { + if content.can_be_picked_at(layout, cursor_position) { + let pane_position = layout.position(); + + let origin = cursor_position + - Vector::new(pane_position.x, pane_position.y); + + self.state.pick_pane(pane, origin); + + messages.push(on_drag(DragEvent::Picked { pane: *pane })); + } + } + } + } + + fn trigger_resize( + &mut self, + 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(); + + let splits = self.state.split_regions( + f32::from(self.spacing), + Size::new(bounds.width, bounds.height), + ); + + if let Some((axis, rectangle, _)) = splits.get(&split) { + let ratio = match axis { + Axis::Horizontal => { + let position = + cursor_position.y - bounds.y - rectangle.y; + + (position / rectangle.height).max(0.1).min(0.9) + } + Axis::Vertical => { + let position = + cursor_position.x - bounds.x - rectangle.x; + + (position / rectangle.width).max(0.1).min(0.9) + } + }; + + 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`]. +#[derive(Debug, Clone, Copy)] +pub enum DragEvent { + /// A [`Pane`] was picked for dragging. + Picked { + /// The picked [`Pane`]. + pane: Pane, + }, + + /// A [`Pane`] was dropped on top of another [`Pane`]. + Dropped { + /// The picked [`Pane`]. + pane: Pane, + + /// The [`Pane`] where the picked one was dropped on. + target: Pane, + }, + + /// A [`Pane`] was picked and then dropped outside of other [`Pane`] + /// boundaries. + Canceled { + /// The picked [`Pane`]. + pane: Pane, + }, +} + +/// An event produced during a resize interaction of a [`PaneGrid`]. +#[derive(Debug, Clone, Copy)] +pub struct ResizeEvent { + /// The [`Split`] that is being dragged for resizing. + 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. + pub ratio: f32, +} + +impl<'a, Message, Renderer> Widget<Message, Renderer> + for PaneGrid<'a, Message, Renderer> +where + Renderer: self::Renderer + container::Renderer, +{ + fn width(&self) -> Length { + self.width + } + + fn height(&self) -> Length { + self.height + } + + fn layout( + &self, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + let limits = limits.width(self.width).height(self.height); + let size = limits.resolve(Size::ZERO); + + let regions = self.state.pane_regions(f32::from(self.spacing), size); + + let children = self + .elements + .iter() + .filter_map(|(pane, element)| { + let region = regions.get(pane)?; + let size = Size::new(region.width, region.height); + + let mut node = + element.layout(renderer, &layout::Limits::new(size, size)); + + node.move_to(Point::new(region.x, region.y)); + + Some(node) + }) + .collect(); + + layout::Node::with_children(size, children) + } + + fn on_event( + &mut self, + event: Event, + layout: Layout<'_>, + cursor_position: Point, + messages: &mut Vec<Message>, + renderer: &Renderer, + clipboard: Option<&dyn Clipboard>, + ) -> event::Status { + let mut event_status = event::Status::Ignored; + + match event { + Event::Mouse(mouse_event) => match mouse_event { + mouse::Event::ButtonPressed(mouse::Button::Left) => { + let bounds = layout.bounds(); + + if bounds.contains(cursor_position) { + event_status = event::Status::Captured; + + match self.on_resize { + Some((leeway, _)) => { + let relative_cursor = Point::new( + cursor_position.x - bounds.x, + cursor_position.y - bounds.y, + ); + + let splits = self.state.split_regions( + f32::from(self.spacing), + Size::new(bounds.width, bounds.height), + ); + + let clicked_split = hovered_split( + splits.iter(), + f32::from(self.spacing + leeway), + relative_cursor, + ); + + if let Some((split, axis)) = clicked_split { + self.state.pick_split(&split, axis); + } else { + self.click_pane( + layout, + cursor_position, + messages, + ); + } + } + None => { + self.click_pane( + layout, + cursor_position, + messages, + ); + } + } + } + } + mouse::Event::ButtonReleased(mouse::Button::Left) => { + if let Some((pane, _)) = self.state.picked_pane() { + if let Some(on_drag) = &self.on_drag { + let mut dropped_region = self + .elements + .iter() + .zip(layout.children()) + .filter(|(_, layout)| { + layout.bounds().contains(cursor_position) + }); + + let event = match dropped_region.next() { + Some(((target, _), _)) if pane != *target => { + DragEvent::Dropped { + pane, + target: *target, + } + } + _ => DragEvent::Canceled { pane }, + }; + + 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_status = + self.trigger_resize(layout, cursor_position, messages); + } + _ => {} + }, + _ => {} + } + + if self.state.picked_pane().is_none() { + self.elements + .iter_mut() + .zip(layout.children()) + .map(|((_, pane), layout)| { + pane.on_event( + event.clone(), + layout, + cursor_position, + messages, + renderer, + clipboard, + ) + }) + .fold(event_status, event::Status::merge) + } else { + event::Status::Captured + } + } + + fn draw( + &self, + renderer: &mut Renderer, + defaults: &Renderer::Defaults, + layout: Layout<'_>, + cursor_position: Point, + _viewport: &Rectangle, + ) -> Renderer::Output { + let picked_split = self + .state + .picked_split() + .or_else(|| match self.on_resize { + Some((leeway, _)) => { + let bounds = layout.bounds(); + + let relative_cursor = Point::new( + cursor_position.x - bounds.x, + cursor_position.y - bounds.y, + ); + + let splits = self + .state + .split_regions(f32::from(self.spacing), bounds.size()); + + hovered_split( + splits.iter(), + f32::from(self.spacing + leeway), + relative_cursor, + ) + } + None => None, + }) + .map(|(_, axis)| axis); + + self::Renderer::draw( + renderer, + defaults, + &self.elements, + self.state.picked_pane(), + picked_split, + layout, + cursor_position, + ) + } + + fn hash_layout(&self, state: &mut Hasher) { + use std::hash::Hash; + + struct Marker; + std::any::TypeId::of::<Marker>().hash(state); + + self.width.hash(state); + self.height.hash(state); + self.state.hash_layout(state); + + for (_, element) in &self.elements { + element.hash_layout(state); + } + } + + fn overlay( + &mut self, + layout: Layout<'_>, + ) -> Option<overlay::Element<'_, Message, Renderer>> { + self.elements + .iter_mut() + .zip(layout.children()) + .filter_map(|((_, pane), layout)| pane.overlay(layout)) + .next() + } +} + +/// The renderer of a [`PaneGrid`]. +/// +/// Your [renderer] will need to implement this trait before being +/// able to use a [`PaneGrid`] in your user interface. +/// +/// [renderer]: crate::renderer +pub trait Renderer: + crate::Renderer + container::Renderer + text::Renderer + Sized +{ + /// Draws a [`PaneGrid`]. + /// + /// It receives: + /// - the elements of the [`PaneGrid`] + /// - the [`Pane`] that is currently being dragged + /// - the [`Axis`] that is currently being resized + /// - the [`Layout`] of the [`PaneGrid`] and its elements + /// - the cursor position + fn draw<Message>( + &mut self, + defaults: &Self::Defaults, + content: &[(Pane, Content<'_, Message, Self>)], + dragging: Option<(Pane, Point)>, + resizing: Option<Axis>, + layout: Layout<'_>, + cursor_position: Point, + ) -> Self::Output; + + /// Draws a [`Pane`]. + /// + /// It receives: + /// - the [`TitleBar`] of the [`Pane`], if any + /// - the [`Content`] of the [`Pane`] + /// - the [`Layout`] of the [`Pane`] and its elements + /// - the cursor position + fn draw_pane<Message>( + &mut self, + defaults: &Self::Defaults, + bounds: Rectangle, + style: &Self::Style, + title_bar: Option<(&TitleBar<'_, Message, Self>, Layout<'_>)>, + body: (&Element<'_, Message, Self>, Layout<'_>), + cursor_position: Point, + ) -> Self::Output; + + /// Draws a [`TitleBar`]. + /// + /// It receives: + /// - the bounds, style of the [`TitleBar`] + /// - the style of the [`TitleBar`] + /// - the title of the [`TitleBar`] with its size, font, and bounds + /// - the controls of the [`TitleBar`] with their [`Layout`+, if any + /// - the cursor position + fn draw_title_bar<Message>( + &mut self, + defaults: &Self::Defaults, + bounds: Rectangle, + style: &Self::Style, + title: &str, + title_size: u16, + title_font: Self::Font, + title_bounds: Rectangle, + controls: Option<(&Element<'_, Message, Self>, Layout<'_>)>, + cursor_position: Point, + ) -> Self::Output; +} + +impl<'a, Message, Renderer> From<PaneGrid<'a, Message, Renderer>> + for Element<'a, Message, Renderer> +where + Renderer: 'a + self::Renderer + row::Renderer, + Message: 'a, +{ + fn from( + pane_grid: PaneGrid<'a, Message, Renderer>, + ) -> Element<'a, Message, Renderer> { + Element::new(pane_grid) + } +} + +/* + * Helpers + */ +fn hovered_split<'a>( + splits: impl Iterator<Item = (&'a Split, &'a (Axis, Rectangle, f32))>, + spacing: f32, + cursor_position: Point, +) -> Option<(Split, Axis)> { + splits + .filter_map(|(split, (axis, region, ratio))| { + let bounds = + axis.split_line_bounds(*region, *ratio, f32::from(spacing)); + + if bounds.contains(cursor_position) { + Some((*split, *axis)) + } else { + None + } + }) + .next() +} diff --git a/native/src/widget/pane_grid/axis.rs b/native/src/widget/pane_grid/axis.rs new file mode 100644 index 00000000..2320cb7c --- /dev/null +++ b/native/src/widget/pane_grid/axis.rs @@ -0,0 +1,238 @@ +use crate::Rectangle; + +/// A fixed reference line for the measurement of coordinates. +#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)] +pub enum Axis { + /// The horizontal axis: — + Horizontal, + /// The vertical axis: | + Vertical, +} + +impl Axis { + pub(super) fn split( + &self, + rectangle: &Rectangle, + ratio: f32, + spacing: f32, + ) -> (Rectangle, Rectangle) { + match self { + Axis::Horizontal => { + let height_top = + (rectangle.height * ratio - spacing / 2.0).round(); + let height_bottom = rectangle.height - height_top - spacing; + + ( + Rectangle { + height: height_top, + ..*rectangle + }, + Rectangle { + y: rectangle.y + height_top + spacing, + height: height_bottom, + ..*rectangle + }, + ) + } + Axis::Vertical => { + let width_left = + (rectangle.width * ratio - spacing / 2.0).round(); + let width_right = rectangle.width - width_left - spacing; + + ( + Rectangle { + width: width_left, + ..*rectangle + }, + Rectangle { + x: rectangle.x + width_left + spacing, + width: width_right, + ..*rectangle + }, + ) + } + } + } + + pub(super) fn split_line_bounds( + &self, + rectangle: Rectangle, + ratio: f32, + spacing: f32, + ) -> Rectangle { + match self { + Axis::Horizontal => Rectangle { + x: rectangle.x, + y: (rectangle.y + rectangle.height * ratio - spacing / 2.0) + .round(), + width: rectangle.width, + height: spacing, + }, + Axis::Vertical => Rectangle { + x: (rectangle.x + rectangle.width * ratio - spacing / 2.0) + .round(), + y: rectangle.y, + width: spacing, + height: rectangle.height, + }, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + enum Case { + Horizontal { + overall_height: f32, + spacing: f32, + top_height: f32, + bottom_y: f32, + bottom_height: f32, + }, + Vertical { + overall_width: f32, + spacing: f32, + left_width: f32, + right_x: f32, + right_width: f32, + }, + } + + #[test] + fn split() { + let cases = vec![ + // Even height, even spacing + Case::Horizontal { + overall_height: 10.0, + spacing: 2.0, + top_height: 4.0, + bottom_y: 6.0, + bottom_height: 4.0, + }, + // Odd height, even spacing + Case::Horizontal { + overall_height: 9.0, + spacing: 2.0, + top_height: 4.0, + bottom_y: 6.0, + bottom_height: 3.0, + }, + // Even height, odd spacing + Case::Horizontal { + overall_height: 10.0, + spacing: 1.0, + top_height: 5.0, + bottom_y: 6.0, + bottom_height: 4.0, + }, + // Odd height, odd spacing + Case::Horizontal { + overall_height: 9.0, + spacing: 1.0, + top_height: 4.0, + bottom_y: 5.0, + bottom_height: 4.0, + }, + // Even width, even spacing + Case::Vertical { + overall_width: 10.0, + spacing: 2.0, + left_width: 4.0, + right_x: 6.0, + right_width: 4.0, + }, + // Odd width, even spacing + Case::Vertical { + overall_width: 9.0, + spacing: 2.0, + left_width: 4.0, + right_x: 6.0, + right_width: 3.0, + }, + // Even width, odd spacing + Case::Vertical { + overall_width: 10.0, + spacing: 1.0, + left_width: 5.0, + right_x: 6.0, + right_width: 4.0, + }, + // Odd width, odd spacing + Case::Vertical { + overall_width: 9.0, + spacing: 1.0, + left_width: 4.0, + right_x: 5.0, + right_width: 4.0, + }, + ]; + for case in cases { + match case { + Case::Horizontal { + overall_height, + spacing, + top_height, + bottom_y, + bottom_height, + } => { + let a = Axis::Horizontal; + let r = Rectangle { + x: 0.0, + y: 0.0, + width: 10.0, + height: overall_height, + }; + let (top, bottom) = a.split(&r, 0.5, spacing); + assert_eq!( + top, + Rectangle { + height: top_height, + ..r + } + ); + assert_eq!( + bottom, + Rectangle { + y: bottom_y, + height: bottom_height, + ..r + } + ); + } + Case::Vertical { + overall_width, + spacing, + left_width, + right_x, + right_width, + } => { + let a = Axis::Vertical; + let r = Rectangle { + x: 0.0, + y: 0.0, + width: overall_width, + height: 10.0, + }; + let (left, right) = a.split(&r, 0.5, spacing); + assert_eq!( + left, + Rectangle { + width: left_width, + ..r + } + ); + assert_eq!( + right, + Rectangle { + x: right_x, + width: right_width, + ..r + } + ); + } + } + } + } +} diff --git a/native/src/widget/pane_grid/configuration.rs b/native/src/widget/pane_grid/configuration.rs new file mode 100644 index 00000000..4c43826e --- /dev/null +++ b/native/src/widget/pane_grid/configuration.rs @@ -0,0 +1,26 @@ +use crate::pane_grid::Axis; + +/// The arrangement of a [`PaneGrid`]. +/// +/// [`PaneGrid`]: crate::pane_grid::PaneGrid +#[derive(Debug, Clone)] +pub enum Configuration<T> { + /// A split of the available space. + Split { + /// The direction of the split. + axis: Axis, + + /// The ratio of the split in [0.0, 1.0]. + ratio: f32, + + /// The left/top [`Configuration`] of the split. + a: Box<Configuration<T>>, + + /// The right/bottom [`Configuration`] of the split. + b: Box<Configuration<T>>, + }, + /// A [`Pane`]. + /// + /// [`Pane`]: crate::pane_grid::Pane + Pane(T), +} diff --git a/native/src/widget/pane_grid/content.rs b/native/src/widget/pane_grid/content.rs new file mode 100644 index 00000000..c9981903 --- /dev/null +++ b/native/src/widget/pane_grid/content.rs @@ -0,0 +1,213 @@ +use crate::container; +use crate::event::{self, Event}; +use crate::layout; +use crate::overlay; +use crate::pane_grid::{self, TitleBar}; +use crate::{Clipboard, Element, Hasher, Layout, Point, Size}; + +/// The content of a [`Pane`]. +/// +/// [`Pane`]: crate::widget::pane_grid::Pane +#[allow(missing_debug_implementations)] +pub struct Content<'a, Message, Renderer: pane_grid::Renderer> { + title_bar: Option<TitleBar<'a, Message, Renderer>>, + body: Element<'a, Message, Renderer>, + style: Renderer::Style, +} + +impl<'a, Message, Renderer> Content<'a, Message, Renderer> +where + Renderer: pane_grid::Renderer, +{ + /// Creates a new [`Content`] with the provided body. + pub fn new(body: impl Into<Element<'a, Message, Renderer>>) -> Self { + Self { + title_bar: None, + body: body.into(), + style: Renderer::Style::default(), + } + } + + /// Sets the [`TitleBar`] of this [`Content`]. + pub fn title_bar( + mut self, + title_bar: TitleBar<'a, Message, Renderer>, + ) -> Self { + self.title_bar = Some(title_bar); + self + } + + /// Sets the style of the [`Content`]. + pub fn style(mut self, style: impl Into<Renderer::Style>) -> Self { + self.style = style.into(); + self + } +} + +impl<'a, Message, Renderer> Content<'a, Message, Renderer> +where + Renderer: pane_grid::Renderer, +{ + /// Draws the [`Content`] with the provided [`Renderer`] and [`Layout`]. + /// + /// [`Renderer`]: crate::widget::pane_grid::Renderer + pub fn draw( + &self, + renderer: &mut Renderer, + defaults: &Renderer::Defaults, + layout: Layout<'_>, + cursor_position: Point, + ) -> Renderer::Output { + if let Some(title_bar) = &self.title_bar { + let mut children = layout.children(); + let title_bar_layout = children.next().unwrap(); + let body_layout = children.next().unwrap(); + + renderer.draw_pane( + defaults, + layout.bounds(), + &self.style, + Some((title_bar, title_bar_layout)), + (&self.body, body_layout), + cursor_position, + ) + } else { + renderer.draw_pane( + defaults, + layout.bounds(), + &self.style, + None, + (&self.body, layout), + cursor_position, + ) + } + } + + /// Returns whether the [`Content`] with the given [`Layout`] can be picked + /// at the provided cursor position. + pub fn can_be_picked_at( + &self, + layout: Layout<'_>, + cursor_position: Point, + ) -> bool { + if let Some(title_bar) = &self.title_bar { + let mut children = layout.children(); + let title_bar_layout = children.next().unwrap(); + + title_bar.is_over_pick_area(title_bar_layout, cursor_position) + } else { + false + } + } + + pub(crate) fn layout( + &self, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + if let Some(title_bar) = &self.title_bar { + let max_size = limits.max(); + + let title_bar_layout = title_bar + .layout(renderer, &layout::Limits::new(Size::ZERO, max_size)); + + let title_bar_size = title_bar_layout.size(); + + let mut body_layout = self.body.layout( + renderer, + &layout::Limits::new( + Size::ZERO, + Size::new( + max_size.width, + max_size.height - title_bar_size.height, + ), + ), + ); + + body_layout.move_to(Point::new(0.0, title_bar_size.height)); + + layout::Node::with_children( + max_size, + vec![title_bar_layout, body_layout], + ) + } else { + self.body.layout(renderer, limits) + } + } + + pub(crate) fn on_event( + &mut self, + event: Event, + layout: Layout<'_>, + cursor_position: Point, + messages: &mut Vec<Message>, + renderer: &Renderer, + clipboard: Option<&dyn Clipboard>, + ) -> event::Status { + let mut event_status = event::Status::Ignored; + + let body_layout = if let Some(title_bar) = &mut self.title_bar { + let mut children = layout.children(); + + event_status = title_bar.on_event( + event.clone(), + children.next().unwrap(), + cursor_position, + messages, + renderer, + clipboard, + ); + + children.next().unwrap() + } else { + layout + }; + + let body_status = self.body.on_event( + event, + body_layout, + cursor_position, + messages, + renderer, + clipboard, + ); + + event_status.merge(body_status) + } + + pub(crate) fn hash_layout(&self, state: &mut Hasher) { + if let Some(title_bar) = &self.title_bar { + title_bar.hash_layout(state); + } + + self.body.hash_layout(state); + } + + pub(crate) fn overlay( + &mut self, + layout: Layout<'_>, + ) -> Option<overlay::Element<'_, Message, Renderer>> { + let body_layout = if self.title_bar.is_some() { + let mut children = layout.children(); + + // Overlays only allowed in the pane body, for now at least. + let _title_bar_layout = children.next(); + + children.next()? + } else { + layout + }; + + self.body.overlay(body_layout) + } +} + +impl<'a, T, Message, Renderer> From<T> for Content<'a, Message, Renderer> +where + T: Into<Element<'a, Message, Renderer>>, + Renderer: pane_grid::Renderer + container::Renderer, +{ + fn from(element: T) -> Self { + Self::new(element) + } +} diff --git a/native/src/widget/pane_grid/direction.rs b/native/src/widget/pane_grid/direction.rs new file mode 100644 index 00000000..b31a8737 --- /dev/null +++ b/native/src/widget/pane_grid/direction.rs @@ -0,0 +1,12 @@ +/// A four cardinal direction. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Direction { + /// ↑ + Up, + /// ↓ + Down, + /// ← + Left, + /// → + Right, +} diff --git a/native/src/widget/pane_grid/node.rs b/native/src/widget/pane_grid/node.rs new file mode 100644 index 00000000..319936fc --- /dev/null +++ b/native/src/widget/pane_grid/node.rs @@ -0,0 +1,258 @@ +use crate::{ + pane_grid::{Axis, Pane, Split}, + Rectangle, Size, +}; + +use std::collections::HashMap; + +/// A layout node of a [`PaneGrid`]. +/// +/// [`PaneGrid`]: crate::widget::PaneGrid +#[derive(Debug, Clone)] +pub enum Node { + /// The region of this [`Node`] is split into two. + Split { + /// The [`Split`] of this [`Node`]. + id: Split, + + /// The direction of the split. + axis: Axis, + + /// The ratio of the split in [0.0, 1.0]. + ratio: f32, + + /// The left/top [`Node`] of the split. + a: Box<Node>, + + /// The right/bottom [`Node`] of the split. + b: Box<Node>, + }, + /// The region of this [`Node`] is taken by a [`Pane`]. + Pane(Pane), +} + +impl Node { + /// Returns an iterator over each [`Split`] in this [`Node`]. + pub fn splits(&self) -> impl Iterator<Item = &Split> { + let mut unvisited_nodes = vec![self]; + + std::iter::from_fn(move || { + while let Some(node) = unvisited_nodes.pop() { + match node { + Node::Split { id, a, b, .. } => { + unvisited_nodes.push(a); + unvisited_nodes.push(b); + + return Some(id); + } + _ => {} + } + } + + None + }) + } + + /// Returns the rectangular region for each [`Pane`] in the [`Node`] given + /// the spacing between panes and the total available space. + pub fn pane_regions( + &self, + spacing: f32, + size: Size, + ) -> HashMap<Pane, Rectangle> { + let mut regions = HashMap::new(); + + self.compute_regions( + spacing, + &Rectangle { + x: 0.0, + y: 0.0, + width: size.width, + height: size.height, + }, + &mut regions, + ); + + regions + } + + /// Returns the axis, rectangular region, and ratio for each [`Split`] in + /// the [`Node`] given the spacing between panes and the total available + /// space. + pub fn split_regions( + &self, + spacing: f32, + size: Size, + ) -> HashMap<Split, (Axis, Rectangle, f32)> { + let mut splits = HashMap::new(); + + self.compute_splits( + spacing, + &Rectangle { + x: 0.0, + y: 0.0, + width: size.width, + height: size.height, + }, + &mut splits, + ); + + splits + } + + pub(crate) fn find(&mut self, pane: &Pane) -> Option<&mut Node> { + match self { + Node::Split { a, b, .. } => { + a.find(pane).or_else(move || b.find(pane)) + } + Node::Pane(p) => { + if p == pane { + Some(self) + } else { + None + } + } + } + } + + pub(crate) fn split(&mut self, id: Split, axis: Axis, new_pane: Pane) { + *self = Node::Split { + id, + axis, + ratio: 0.5, + a: Box::new(self.clone()), + b: Box::new(Node::Pane(new_pane)), + }; + } + + pub(crate) fn update(&mut self, f: &impl Fn(&mut Node)) { + match self { + Node::Split { a, b, .. } => { + a.update(f); + b.update(f); + } + _ => {} + } + + f(self); + } + + pub(crate) fn resize(&mut self, split: &Split, percentage: f32) -> bool { + match self { + Node::Split { + id, ratio, a, b, .. + } => { + if id == split { + *ratio = percentage; + + true + } else if a.resize(split, percentage) { + true + } else { + b.resize(split, percentage) + } + } + Node::Pane(_) => false, + } + } + + pub(crate) fn remove(&mut self, pane: &Pane) -> Option<Pane> { + match self { + Node::Split { a, b, .. } => { + if a.pane() == Some(*pane) { + *self = *b.clone(); + Some(self.first_pane()) + } else if b.pane() == Some(*pane) { + *self = *a.clone(); + Some(self.first_pane()) + } else { + a.remove(pane).or_else(|| b.remove(pane)) + } + } + Node::Pane(_) => None, + } + } + + fn pane(&self) -> Option<Pane> { + match self { + Node::Split { .. } => None, + Node::Pane(pane) => Some(*pane), + } + } + + fn first_pane(&self) -> Pane { + match self { + Node::Split { a, .. } => a.first_pane(), + Node::Pane(pane) => *pane, + } + } + + fn compute_regions( + &self, + spacing: f32, + current: &Rectangle, + regions: &mut HashMap<Pane, Rectangle>, + ) { + match self { + Node::Split { + axis, ratio, a, b, .. + } => { + let (region_a, region_b) = axis.split(current, *ratio, spacing); + + a.compute_regions(spacing, ®ion_a, regions); + b.compute_regions(spacing, ®ion_b, regions); + } + Node::Pane(pane) => { + let _ = regions.insert(*pane, *current); + } + } + } + + fn compute_splits( + &self, + spacing: f32, + current: &Rectangle, + splits: &mut HashMap<Split, (Axis, Rectangle, f32)>, + ) { + match self { + Node::Split { + axis, + ratio, + a, + b, + id, + } => { + let (region_a, region_b) = axis.split(current, *ratio, spacing); + + let _ = splits.insert(*id, (*axis, *current, *ratio)); + + a.compute_splits(spacing, ®ion_a, splits); + b.compute_splits(spacing, ®ion_b, splits); + } + Node::Pane(_) => {} + } + } +} + +impl std::hash::Hash for Node { + fn hash<H: std::hash::Hasher>(&self, state: &mut H) { + match self { + Node::Split { + id, + axis, + ratio, + a, + b, + } => { + id.hash(state); + axis.hash(state); + ((ratio * 100_000.0) as u32).hash(state); + a.hash(state); + b.hash(state); + } + Node::Pane(pane) => { + pane.hash(state); + } + } + } +} diff --git a/native/src/widget/pane_grid/pane.rs b/native/src/widget/pane_grid/pane.rs new file mode 100644 index 00000000..39d9f3ef --- /dev/null +++ b/native/src/widget/pane_grid/pane.rs @@ -0,0 +1,5 @@ +/// A rectangular region in a [`PaneGrid`] used to display widgets. +/// +/// [`PaneGrid`]: crate::widget::PaneGrid +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct Pane(pub(super) usize); diff --git a/native/src/widget/pane_grid/split.rs b/native/src/widget/pane_grid/split.rs new file mode 100644 index 00000000..16975abc --- /dev/null +++ b/native/src/widget/pane_grid/split.rs @@ -0,0 +1,5 @@ +/// A divider that splits a region in a [`PaneGrid`] into two different panes. +/// +/// [`PaneGrid`]: crate::widget::PaneGrid +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct Split(pub(super) usize); diff --git a/native/src/widget/pane_grid/state.rs b/native/src/widget/pane_grid/state.rs new file mode 100644 index 00000000..666e1ca0 --- /dev/null +++ b/native/src/widget/pane_grid/state.rs @@ -0,0 +1,301 @@ +use crate::{ + pane_grid::{Axis, Configuration, Direction, Node, Pane, Split}, + Hasher, Point, Rectangle, Size, +}; + +use std::collections::HashMap; + +/// The state of a [`PaneGrid`]. +/// +/// It keeps track of the state of each [`Pane`] and the position of each +/// [`Split`]. +/// +/// The [`State`] needs to own any mutable contents a [`Pane`] may need. This is +/// why this struct is generic over the type `T`. Values of this type are +/// provided to the view function of [`PaneGrid::new`] for displaying each +/// [`Pane`]. +/// +/// [`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, +} + +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. + pub fn new(first_pane_state: T) -> (Self, Pane) { + ( + Self::with_configuration(Configuration::Pane(first_pane_state)), + Pane(0), + ) + } + + /// Creates a new [`State`] with the given [`Configuration`]. + pub fn with_configuration(config: impl Into<Configuration<T>>) -> Self { + let mut panes = HashMap::new(); + + let (layout, last_id) = + Self::distribute_content(&mut panes, config.into(), 0); + + State { + panes, + internal: Internal { + layout, + last_id, + action: Action::Idle, + }, + } + } + + /// Returns the total amount of panes in the [`State`]. + pub fn len(&self) -> usize { + self.panes.len() + } + + /// Returns the internal state of the given [`Pane`], if it exists. + 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. + 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. + 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. + pub fn iter_mut(&mut self) -> impl Iterator<Item = (&Pane, &mut T)> { + self.panes.iter_mut() + } + + /// Returns the layout of the [`State`]. + pub fn layout(&self) -> &Node { + &self.internal.layout + } + + /// Returns the adjacent [`Pane`] of another [`Pane`] in the given + /// direction, if there is one. + pub fn adjacent(&self, pane: &Pane, direction: Direction) -> Option<Pane> { + let regions = self + .internal + .layout + .pane_regions(0.0, Size::new(4096.0, 4096.0)); + + let current_region = regions.get(pane)?; + + let target = match direction { + Direction::Left => { + Point::new(current_region.x - 1.0, current_region.y + 1.0) + } + Direction::Right => Point::new( + current_region.x + current_region.width + 1.0, + current_region.y + 1.0, + ), + Direction::Up => { + Point::new(current_region.x + 1.0, current_region.y - 1.0) + } + Direction::Down => Point::new( + current_region.x + 1.0, + current_region.y + current_region.height + 1.0, + ), + }; + + let mut colliding_regions = + regions.iter().filter(|(_, region)| region.contains(target)); + + let (pane, _) = colliding_regions.next()?; + + Some(*pane) + } + + /// Splits the given [`Pane`] into two in the given [`Axis`] and + /// initializing the new [`Pane`] with the provided internal state. + pub fn split( + &mut self, + axis: Axis, + pane: &Pane, + state: T, + ) -> Option<(Pane, Split)> { + let node = self.internal.layout.find(pane)?; + + let new_pane = { + self.internal.last_id = self.internal.last_id.checked_add(1)?; + + Pane(self.internal.last_id) + }; + + let new_split = { + self.internal.last_id = self.internal.last_id.checked_add(1)?; + + Split(self.internal.last_id) + }; + + node.split(new_split, axis, new_pane); + + let _ = self.panes.insert(new_pane, state); + + Some((new_pane, new_split)) + } + + /// Swaps the position of the provided panes in the [`State`]. + /// + /// If you want to swap panes on drag and drop in your [`PaneGrid`], you + /// will need to call this method when handling a [`DragEvent`]. + /// + /// [`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 { .. } => {} + Node::Pane(pane) => { + if pane == a { + *node = Node::Pane(*b); + } else if pane == b { + *node = Node::Pane(*a); + } + } + }); + } + + /// Resizes two panes by setting the position of the provided [`Split`]. + /// + /// The ratio is a value in [0, 1], representing the exact position of a + /// [`Split`] between two panes. + /// + /// If you want to enable resize interactions in your [`PaneGrid`], you will + /// need to call this method when handling a [`ResizeEvent`]. + /// + /// [`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 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.panes.remove(pane).map(|state| (state, sibling)) + } else { + None + } + } + + fn distribute_content( + panes: &mut HashMap<Pane, T>, + content: Configuration<T>, + next_id: usize, + ) -> (Node, usize) { + match content { + Configuration::Split { axis, ratio, a, b } => { + let (a, next_id) = Self::distribute_content(panes, *a, next_id); + let (b, next_id) = Self::distribute_content(panes, *b, next_id); + + ( + Node::Split { + id: Split(next_id), + axis, + ratio, + a: Box::new(a), + b: Box::new(b), + }, + next_id + 1, + ) + } + Configuration::Pane(state) => { + let id = Pane(next_id); + let _ = panes.insert(id, state); + + (Node::Pane(id), next_id + 1) + } + } + } +} + +#[derive(Debug, Clone)] +pub struct Internal { + layout: Node, + last_id: usize, + action: Action, +} + +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum Action { + Idle, + Dragging { pane: Pane, origin: Point }, + Resizing { split: Split, axis: Axis }, +} + +impl Internal { + pub fn picked_pane(&self) -> Option<(Pane, Point)> { + match self.action { + Action::Dragging { pane, origin, .. } => Some((pane, origin)), + _ => None, + } + } + + pub fn picked_split(&self) -> Option<(Split, Axis)> { + match self.action { + Action::Resizing { split, axis, .. } => Some((split, axis)), + _ => None, + } + } + + pub fn pane_regions( + &self, + spacing: f32, + size: Size, + ) -> HashMap<Pane, Rectangle> { + self.layout.pane_regions(spacing, size) + } + + pub fn split_regions( + &self, + spacing: f32, + size: Size, + ) -> HashMap<Split, (Axis, Rectangle, f32)> { + self.layout.split_regions(spacing, size) + } + + pub fn pick_pane(&mut self, pane: &Pane, origin: Point) { + self.action = Action::Dragging { + pane: *pane, + origin, + }; + } + + pub fn pick_split(&mut self, split: &Split, axis: Axis) { + // TODO: Obtain `axis` from layout itself. Maybe we should implement + // `Node::find_split` + if self.picked_pane().is_some() { + return; + } + + self.action = Action::Resizing { + split: *split, + axis, + }; + } + + pub fn idle(&mut self) { + self.action = Action::Idle; + } + + pub fn hash_layout(&self, hasher: &mut Hasher) { + use std::hash::Hash; + + self.layout.hash(hasher); + } +} diff --git a/native/src/widget/pane_grid/title_bar.rs b/native/src/widget/pane_grid/title_bar.rs new file mode 100644 index 00000000..475cb9ae --- /dev/null +++ b/native/src/widget/pane_grid/title_bar.rs @@ -0,0 +1,253 @@ +use crate::event::{self, Event}; +use crate::layout; +use crate::pane_grid; +use crate::{Clipboard, Element, Hasher, Layout, Point, Rectangle, Size}; + +/// The title bar of a [`Pane`]. +/// +/// [`Pane`]: crate::widget::pane_grid::Pane +#[allow(missing_debug_implementations)] +pub struct TitleBar<'a, Message, Renderer: pane_grid::Renderer> { + title: String, + title_size: Option<u16>, + controls: Option<Element<'a, Message, Renderer>>, + padding: u16, + always_show_controls: bool, + style: Renderer::Style, +} + +impl<'a, Message, Renderer> TitleBar<'a, Message, Renderer> +where + Renderer: pane_grid::Renderer, +{ + /// Creates a new [`TitleBar`] with the given title. + pub fn new(title: impl Into<String>) -> Self { + Self { + title: title.into(), + title_size: None, + controls: None, + padding: 0, + always_show_controls: false, + style: Renderer::Style::default(), + } + } + + /// Sets the size of the title of the [`TitleBar`]. + pub fn title_size(mut self, size: u16) -> Self { + self.title_size = Some(size); + self + } + + /// Sets the controls of the [`TitleBar`]. + pub fn controls( + mut self, + controls: impl Into<Element<'a, Message, Renderer>>, + ) -> Self { + self.controls = Some(controls.into()); + self + } + + /// Sets the padding of the [`TitleBar`]. + pub fn padding(mut self, units: u16) -> Self { + self.padding = units; + self + } + + /// Sets the style of the [`TitleBar`]. + pub fn style(mut self, style: impl Into<Renderer::Style>) -> Self { + self.style = style.into(); + self + } + + /// Sets whether or not the [`controls`] attached to this [`TitleBar`] are + /// always visible. + /// + /// By default, the controls are only visible when the [`Pane`] of this + /// [`TitleBar`] is hovered. + /// + /// [`controls`]: Self::controls + /// [`Pane`]: crate::widget::pane_grid::Pane + pub fn always_show_controls(mut self) -> Self { + self.always_show_controls = true; + self + } +} + +impl<'a, Message, Renderer> TitleBar<'a, Message, Renderer> +where + Renderer: pane_grid::Renderer, +{ + /// Draws the [`TitleBar`] with the provided [`Renderer`] and [`Layout`]. + /// + /// [`Renderer`]: crate::widget::pane_grid::Renderer + pub fn draw( + &self, + renderer: &mut Renderer, + defaults: &Renderer::Defaults, + layout: Layout<'_>, + cursor_position: Point, + show_controls: bool, + ) -> Renderer::Output { + let mut children = layout.children(); + let padded = children.next().unwrap(); + + if let Some(controls) = &self.controls { + let mut children = padded.children(); + let title_layout = children.next().unwrap(); + let controls_layout = children.next().unwrap(); + + let (title_bounds, controls) = + if show_controls || self.always_show_controls { + (title_layout.bounds(), Some((controls, controls_layout))) + } else { + ( + Rectangle { + width: padded.bounds().width, + ..title_layout.bounds() + }, + None, + ) + }; + + renderer.draw_title_bar( + defaults, + layout.bounds(), + &self.style, + &self.title, + self.title_size.unwrap_or(renderer.default_size()), + Renderer::Font::default(), + title_bounds, + controls, + cursor_position, + ) + } else { + renderer.draw_title_bar::<()>( + defaults, + layout.bounds(), + &self.style, + &self.title, + self.title_size.unwrap_or(renderer.default_size()), + Renderer::Font::default(), + padded.bounds(), + None, + cursor_position, + ) + } + } + + /// Returns whether the mouse cursor is over the pick area of the + /// [`TitleBar`] or not. + /// + /// The whole [`TitleBar`] is a pick area, except its controls. + pub fn is_over_pick_area( + &self, + layout: Layout<'_>, + cursor_position: Point, + ) -> bool { + if layout.bounds().contains(cursor_position) { + let mut children = layout.children(); + let padded = children.next().unwrap(); + + if self.controls.is_some() { + let mut children = padded.children(); + let _ = children.next().unwrap(); + let controls_layout = children.next().unwrap(); + + !controls_layout.bounds().contains(cursor_position) + } else { + true + } + } else { + false + } + } + + pub(crate) fn hash_layout(&self, hasher: &mut Hasher) { + use std::hash::Hash; + + self.title.hash(hasher); + self.title_size.hash(hasher); + self.padding.hash(hasher); + } + + pub(crate) fn layout( + &self, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + let padding = f32::from(self.padding); + let limits = limits.pad(padding); + let max_size = limits.max(); + + let title_size = self.title_size.unwrap_or(renderer.default_size()); + let title_font = Renderer::Font::default(); + + let (title_width, title_height) = renderer.measure( + &self.title, + title_size, + title_font, + Size::new(f32::INFINITY, max_size.height), + ); + + let mut node = if let Some(controls) = &self.controls { + let mut controls_layout = controls + .layout(renderer, &layout::Limits::new(Size::ZERO, max_size)); + + let controls_size = controls_layout.size(); + let space_before_controls = max_size.width - controls_size.width; + + let mut title_layout = layout::Node::new(Size::new( + title_width.min(space_before_controls), + title_height, + )); + + let title_size = title_layout.size(); + let height = title_size.height.max(controls_size.height); + + title_layout + .move_to(Point::new(0.0, (height - title_size.height) / 2.0)); + controls_layout.move_to(Point::new(space_before_controls, 0.0)); + + layout::Node::with_children( + Size::new(max_size.width, height), + vec![title_layout, controls_layout], + ) + } else { + layout::Node::new(Size::new(max_size.width, title_height)) + }; + + node.move_to(Point::new(padding, padding)); + + layout::Node::with_children(node.size().pad(padding), vec![node]) + } + + pub(crate) fn on_event( + &mut self, + event: Event, + layout: Layout<'_>, + cursor_position: Point, + messages: &mut Vec<Message>, + renderer: &Renderer, + clipboard: Option<&dyn Clipboard>, + ) -> event::Status { + if let Some(controls) = &mut self.controls { + let mut children = layout.children(); + let padded = children.next().unwrap(); + + let mut children = padded.children(); + let _ = children.next(); + let controls_layout = children.next().unwrap(); + + controls.on_event( + event, + controls_layout, + cursor_position, + messages, + renderer, + clipboard, + ) + } else { + event::Status::Ignored + } + } +} diff --git a/native/src/widget/pick_list.rs b/native/src/widget/pick_list.rs new file mode 100644 index 00000000..58c0dfe1 --- /dev/null +++ b/native/src/widget/pick_list.rs @@ -0,0 +1,343 @@ +//! Display a dropdown list of selectable values. +use crate::event::{self, Event}; +use crate::layout; +use crate::mouse; +use crate::overlay; +use crate::overlay::menu::{self, Menu}; +use crate::scrollable; +use crate::text; +use crate::{ + Clipboard, Element, Hasher, Layout, Length, Point, Rectangle, Size, Widget, +}; +use std::borrow::Cow; + +/// A widget for selecting a single value from a list of options. +#[allow(missing_debug_implementations)] +pub struct PickList<'a, T, Message, Renderer: self::Renderer> +where + [T]: ToOwned<Owned = Vec<T>>, +{ + menu: &'a mut menu::State, + is_open: &'a mut bool, + hovered_option: &'a mut Option<usize>, + last_selection: &'a mut Option<T>, + on_selected: Box<dyn Fn(T) -> Message>, + options: Cow<'a, [T]>, + selected: Option<T>, + width: Length, + padding: u16, + text_size: Option<u16>, + font: Renderer::Font, + style: <Renderer as self::Renderer>::Style, +} + +/// The local state of a [`PickList`]. +#[derive(Debug, Clone)] +pub struct State<T> { + menu: menu::State, + is_open: bool, + hovered_option: Option<usize>, + last_selection: Option<T>, +} + +impl<T> Default for State<T> { + fn default() -> Self { + Self { + menu: menu::State::default(), + is_open: bool::default(), + hovered_option: Option::default(), + last_selection: Option::default(), + } + } +} + +impl<'a, T: 'a, Message, Renderer: self::Renderer> + PickList<'a, T, Message, Renderer> +where + T: ToString + Eq, + [T]: ToOwned<Owned = Vec<T>>, +{ + /// Creates a new [`PickList`] with the given [`State`], a list of options, + /// the current selected value, and the message to produce when an option is + /// selected. + pub fn new( + state: &'a mut State<T>, + options: impl Into<Cow<'a, [T]>>, + selected: Option<T>, + on_selected: impl Fn(T) -> Message + 'static, + ) -> Self { + let State { + menu, + is_open, + hovered_option, + last_selection, + } = state; + + Self { + menu, + is_open, + hovered_option, + last_selection, + on_selected: Box::new(on_selected), + options: options.into(), + selected, + width: Length::Shrink, + text_size: None, + padding: Renderer::DEFAULT_PADDING, + font: Default::default(), + style: Default::default(), + } + } + + /// Sets the width of the [`PickList`]. + pub fn width(mut self, width: Length) -> Self { + self.width = width; + self + } + + /// Sets the padding of the [`PickList`]. + pub fn padding(mut self, padding: u16) -> Self { + self.padding = padding; + self + } + + /// Sets the text size of the [`PickList`]. + pub fn text_size(mut self, size: u16) -> Self { + self.text_size = Some(size); + self + } + + /// Sets the font of the [`PickList`]. + pub fn font(mut self, font: Renderer::Font) -> Self { + self.font = font; + self + } + + /// Sets the style of the [`PickList`]. + pub fn style( + mut self, + style: impl Into<<Renderer as self::Renderer>::Style>, + ) -> Self { + self.style = style.into(); + self + } +} + +impl<'a, T: 'a, Message, Renderer> Widget<Message, Renderer> + for PickList<'a, T, Message, Renderer> +where + T: Clone + ToString + Eq, + [T]: ToOwned<Owned = Vec<T>>, + Message: 'static, + Renderer: self::Renderer + scrollable::Renderer + 'a, +{ + fn width(&self) -> Length { + Length::Shrink + } + + fn height(&self) -> Length { + Length::Shrink + } + + fn layout( + &self, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + use std::f32; + + let limits = limits + .width(self.width) + .height(Length::Shrink) + .pad(f32::from(self.padding)); + + let text_size = self.text_size.unwrap_or(renderer.default_size()); + + let max_width = match self.width { + Length::Shrink => { + let labels = self.options.iter().map(ToString::to_string); + + labels + .map(|label| { + let (width, _) = renderer.measure( + &label, + text_size, + Renderer::Font::default(), + Size::new(f32::INFINITY, f32::INFINITY), + ); + + width.round() as u32 + }) + .max() + .unwrap_or(100) + } + _ => 0, + }; + + let size = { + let intrinsic = Size::new( + max_width as f32 + + f32::from(text_size) + + f32::from(self.padding), + f32::from(text_size), + ); + + limits.resolve(intrinsic).pad(f32::from(self.padding)) + }; + + layout::Node::new(size) + } + + fn hash_layout(&self, state: &mut Hasher) { + use std::hash::Hash as _; + + match self.width { + Length::Shrink => { + self.options + .iter() + .map(ToString::to_string) + .for_each(|label| label.hash(state)); + } + _ => { + self.width.hash(state); + } + } + } + + fn on_event( + &mut self, + event: Event, + layout: Layout<'_>, + cursor_position: Point, + messages: &mut Vec<Message>, + _renderer: &Renderer, + _clipboard: Option<&dyn Clipboard>, + ) -> event::Status { + match event { + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => { + let event_status = if *self.is_open { + // TODO: Encode cursor availability in the type system + *self.is_open = + cursor_position.x < 0.0 || cursor_position.y < 0.0; + + event::Status::Captured + } else if layout.bounds().contains(cursor_position) { + let selected = self.selected.as_ref(); + + *self.is_open = true; + *self.hovered_option = self + .options + .iter() + .position(|option| Some(option) == selected); + + event::Status::Captured + } else { + event::Status::Ignored + }; + + if let Some(last_selection) = self.last_selection.take() { + messages.push((self.on_selected)(last_selection)); + + *self.is_open = false; + + event::Status::Captured + } else { + event_status + } + } + _ => event::Status::Ignored, + } + } + + fn draw( + &self, + renderer: &mut Renderer, + _defaults: &Renderer::Defaults, + layout: Layout<'_>, + cursor_position: Point, + _viewport: &Rectangle, + ) -> Renderer::Output { + self::Renderer::draw( + renderer, + layout.bounds(), + cursor_position, + self.selected.as_ref().map(ToString::to_string), + self.padding, + self.text_size.unwrap_or(renderer.default_size()), + self.font, + &self.style, + ) + } + + fn overlay( + &mut self, + layout: Layout<'_>, + ) -> Option<overlay::Element<'_, Message, Renderer>> { + if *self.is_open { + let bounds = layout.bounds(); + + let mut menu = Menu::new( + &mut self.menu, + &self.options, + &mut self.hovered_option, + &mut self.last_selection, + ) + .width(bounds.width.round() as u16) + .padding(self.padding) + .font(self.font) + .style(Renderer::menu_style(&self.style)); + + if let Some(text_size) = self.text_size { + menu = menu.text_size(text_size); + } + + Some(menu.overlay(layout.position(), bounds.height)) + } else { + None + } + } +} + +/// The renderer of a [`PickList`]. +/// +/// Your [renderer] will need to implement this trait before being +/// able to use a [`PickList`] in your user interface. +/// +/// [renderer]: crate::renderer +pub trait Renderer: text::Renderer + menu::Renderer { + /// The default padding of a [`PickList`]. + const DEFAULT_PADDING: u16; + + /// The [`PickList`] style supported by this renderer. + type Style: Default; + + /// Returns the style of the [`Menu`] of the [`PickList`]. + fn menu_style( + style: &<Self as Renderer>::Style, + ) -> <Self as menu::Renderer>::Style; + + /// Draws a [`PickList`]. + fn draw( + &mut self, + bounds: Rectangle, + cursor_position: Point, + selected: Option<String>, + padding: u16, + text_size: u16, + font: Self::Font, + style: &<Self as Renderer>::Style, + ) -> Self::Output; +} + +impl<'a, T: 'a, Message, Renderer> Into<Element<'a, Message, Renderer>> + for PickList<'a, T, Message, Renderer> +where + T: Clone + ToString + Eq, + [T]: ToOwned<Owned = Vec<T>>, + Renderer: self::Renderer + 'a, + Message: 'static, +{ + fn into(self) -> Element<'a, Message, Renderer> { + Element::new(self) + } +} diff --git a/native/src/widget/progress_bar.rs b/native/src/widget/progress_bar.rs index 67d1ab83..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(), @@ -114,6 +107,9 @@ where } fn hash_layout(&self, state: &mut Hasher) { + struct Marker; + std::any::TypeId::of::<Marker>().hash(state); + self.width.hash(state); self.height.hash(state); } @@ -124,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`]. @@ -143,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, @@ -157,8 +148,8 @@ pub trait Renderer: crate::Renderer { impl<'a, Message, Renderer> From<ProgressBar<Renderer>> for Element<'a, Message, Renderer> where - Renderer: 'static + self::Renderer, - Message: 'static, + Renderer: 'a + self::Renderer, + Message: 'a, { fn from( progress_bar: ProgressBar<Renderer>, diff --git a/native/src/widget/radio.rs b/native/src/widget/radio.rs index 46983db3..3a1dd386 100644 --- a/native/src/widget/radio.rs +++ b/native/src/widget/radio.rs @@ -1,9 +1,13 @@ //! Create choices using radio buttons. +use crate::event::{self, Event}; +use crate::layout; +use crate::mouse; +use crate::row; +use crate::text; +use crate::touch::{self, Touch}; use crate::{ - input::{mouse, touch, ButtonState, Touch}, - layout, row, text, Align, Clipboard, Element, Event, Font, 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; @@ -35,14 +39,22 @@ use std::hash::Hash; /// ///  #[allow(missing_debug_implementations)] -pub struct Radio<Message, Renderer: self::Renderer> { +pub struct Radio<Message, Renderer: self::Renderer + text::Renderer> { is_selected: bool, on_click: Message, label: String, + width: Length, + size: u16, + spacing: u16, + text_size: Option<u16>, style: Renderer::Style, } -impl<Message, Renderer: self::Renderer> Radio<Message, Renderer> { +impl<Message, Renderer: self::Renderer + text::Renderer> + Radio<Message, Renderer> +where + Message: Clone, +{ /// Creates a new [`Radio`] button. /// /// It expects: @@ -51,9 +63,12 @@ impl<Message, Renderer: self::Renderer> Radio<Message, 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: &str, selected: Option<V>, f: F) -> Self + pub fn new<F, V>( + value: V, + label: impl Into<String>, + selected: Option<V>, + f: F, + ) -> Self where V: Eq + Copy, F: 'static + Fn(V) -> Message, @@ -61,14 +76,40 @@ impl<Message, Renderer: self::Renderer> Radio<Message, Renderer> { Radio { is_selected: Some(value) == selected, on_click: f(value), - label: String::from(label), + label: label.into(), + width: Length::Shrink, + size: <Renderer as self::Renderer>::DEFAULT_SIZE, + spacing: Renderer::DEFAULT_SPACING, //15 + text_size: None, style: Renderer::Style::default(), } } + /// Sets the size of the [`Radio`] button. + pub fn size(mut self, size: u16) -> Self { + self.size = size; + self + } + + /// Sets the width of the [`Radio`] button. + pub fn width(mut self, width: Length) -> Self { + self.width = width; + self + } + + /// Sets the spacing between the [`Radio`] button and the text. + pub fn spacing(mut self, spacing: u16) -> Self { + self.spacing = spacing; + self + } + + /// Sets the text size of the [`Radio`] button. + pub fn text_size(mut self, text_size: u16) -> Self { + self.text_size = Some(text_size); + self + } + /// Sets the style of the [`Radio`] button. - /// - /// [`Radio`]: struct.Radio.html pub fn style(mut self, style: impl Into<Renderer::Style>) -> Self { self.style = style.into(); self @@ -77,11 +118,11 @@ impl<Message, Renderer: self::Renderer> Radio<Message, 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 { - Length::Fill + self.width } fn height(&self) -> Length { @@ -93,18 +134,20 @@ where renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { - let size = self::Renderer::default_size(renderer); - Row::<(), Renderer>::new() - .width(Length::Fill) - .spacing(15) + .width(self.width) + .spacing(self.spacing) .align_items(Align::Center) .push( Row::new() - .width(Length::Units(size as u16)) - .height(Length::Units(size as u16)), + .width(Length::Units(self.size)) + .height(Length::Units(self.size)), + ) + .push( + Text::new(&self.label) + .width(self.width) + .size(self.text_size.unwrap_or(renderer.default_size())), ) - .push(Text::new(&self.label)) .layout(renderer, limits) } @@ -116,22 +159,23 @@ where messages: &mut Vec<Message>, _renderer: &Renderer, _clipboard: Option<&dyn Clipboard>, - ) { + ) -> event::Status { match event { - Event::Mouse(mouse::Event::Input { - button: mouse::Button::Left, - state: ButtonState::Pressed, - }) + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) | Event::Touch(Touch { phase: touch::Phase::Started, .. }) => { if layout.bounds().contains(cursor_position) { messages.push(self.on_click.clone()); + + return event::Status::Captured; } } _ => {} } + + event::Status::Ignored } fn draw( @@ -140,6 +184,7 @@ where defaults: &Renderer::Defaults, layout: Layout<'_>, cursor_position: Point, + _viewport: &Rectangle, ) -> Renderer::Output { let bounds = layout.bounds(); let mut children = layout.children(); @@ -153,8 +198,8 @@ where defaults, label_layout.bounds(), &self.label, - <Renderer as text::Renderer>::DEFAULT_SIZE, - Font::Default, + self.text_size.unwrap_or(renderer.default_size()), + Default::default(), None, HorizontalAlignment::Left, VerticalAlignment::Center, @@ -173,6 +218,9 @@ where } fn hash_layout(&self, state: &mut Hasher) { + struct Marker; + std::any::TypeId::of::<Marker>().hash(state); + self.label.hash(state); } } @@ -182,16 +230,16 @@ 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; - /// Returns the default size of a [`Radio`] button. - /// - /// [`Radio`]: struct.Radio.html - fn default_size(&self) -> u32; + /// The default size of a [`Radio`] button. + const DEFAULT_SIZE: u16; + + /// The default spacing of a [`Radio`] button. + const DEFAULT_SPACING: u16; /// Draws a [`Radio`] button. /// @@ -200,8 +248,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, @@ -215,8 +261,8 @@ pub trait Renderer: crate::Renderer { impl<'a, Message, Renderer> From<Radio<Message, Renderer>> for Element<'a, Message, Renderer> where - Renderer: 'static + self::Renderer + row::Renderer + text::Renderer, - Message: 'static + Clone, + 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 c8812ea2..b71663bd 100644 --- a/native/src/widget/row.rs +++ b/native/src/widget/row.rs @@ -1,18 +1,15 @@ //! Distribute content horizontally. -use std::hash::Hash; - +use crate::event::{self, Event}; +use crate::layout; +use crate::overlay; use crate::{ - layout, Align, Clipboard, Element, Event, Hasher, Layout, Length, Point, - Widget, + Align, Clipboard, Element, Hasher, Layout, Length, Point, Rectangle, Widget, }; +use std::hash::Hash; use std::u32; /// A container that distributes its contents horizontally. -/// -/// A [`Row`] will try to fill the horizontal space of its container. -/// -/// [`Row`]: struct.Row.html #[allow(missing_debug_implementations)] pub struct Row<'a, Message, Renderer> { spacing: u16, @@ -27,15 +24,11 @@ pub struct Row<'a, Message, Renderer> { impl<'a, Message, Renderer> Row<'a, Message, Renderer> { /// Creates an empty [`Row`]. - /// - /// [`Row`]: struct.Row.html pub fn new() -> Self { Self::with_children(Vec::new()) } /// Creates a [`Row`] with the given elements. - /// - /// [`Row`]: struct.Row.html pub fn with_children( children: Vec<Element<'a, Message, Renderer>>, ) -> Self { @@ -62,57 +55,42 @@ impl<'a, Message, Renderer> Row<'a, Message, Renderer> { } /// Sets the padding of the [`Row`]. - /// - /// [`Row`]: struct.Row.html pub fn padding(mut self, units: u16) -> Self { self.padding = units; self } /// Sets the width of the [`Row`]. - /// - /// [`Row`]: struct.Row.html pub fn width(mut self, width: Length) -> Self { self.width = width; self } /// Sets the height of the [`Row`]. - /// - /// [`Row`]: struct.Row.html pub fn height(mut self, height: Length) -> Self { self.height = height; self } /// Sets the maximum width of the [`Row`]. - /// - /// [`Row`]: struct.Row.html pub fn max_width(mut self, max_width: u32) -> Self { self.max_width = max_width; self } /// Sets the maximum height of the [`Row`]. - /// - /// [`Row`]: struct.Row.html pub fn max_height(mut self, max_height: u32) -> Self { self.max_height = max_height; self } /// Sets the vertical alignment of the contents of the [`Row`] . - /// - /// [`Row`]: struct.Row.html pub fn align_items(mut self, align: Align) -> Self { self.align_items = align; self } /// Adds an [`Element`] to the [`Row`]. - /// - /// [`Element`]: ../struct.Element.html - /// [`Row`]: struct.Row.html pub fn push<E>(mut self, child: E) -> Self where E: Into<Element<'a, Message, Renderer>>, @@ -165,9 +143,11 @@ where messages: &mut Vec<Message>, renderer: &Renderer, clipboard: Option<&dyn Clipboard>, - ) { - self.children.iter_mut().zip(layout.children()).for_each( - |(child, layout)| { + ) -> event::Status { + self.children + .iter_mut() + .zip(layout.children()) + .map(|(child, layout)| { child.widget.on_event( event.clone(), layout, @@ -176,8 +156,8 @@ where renderer, clipboard, ) - }, - ); + }) + .fold(event::Status::Ignored, event::Status::merge) } fn draw( @@ -186,24 +166,44 @@ 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) { - 1.hash(state); + struct Marker; + std::any::TypeId::of::<Marker>().hash(state); + self.width.hash(state); self.height.hash(state); self.max_width.hash(state); self.max_height.hash(state); self.align_items.hash(state); self.spacing.hash(state); - self.spacing.hash(state); + self.padding.hash(state); for child in &self.children { child.widget.hash_layout(state); } } + + fn overlay( + &mut self, + layout: Layout<'_>, + ) -> Option<overlay::Element<'_, Message, Renderer>> { + self.children + .iter_mut() + .zip(layout.children()) + .filter_map(|(child, layout)| child.widget.overlay(layout)) + .next() + } } /// The renderer of a [`Row`]. @@ -211,8 +211,7 @@ where /// Your [renderer] will need to implement this trait before being /// able to use a [`Row`] in your user interface. /// -/// [`Row`]: struct.Row.html -/// [renderer]: ../../renderer/index.html +/// [renderer]: crate::renderer pub trait Renderer: crate::Renderer + Sized { /// Draws a [`Row`]. /// @@ -220,15 +219,13 @@ pub trait Renderer: crate::Renderer + Sized { /// - the children of the [`Row`] /// - the [`Layout`] of the [`Row`] and its children /// - the cursor position - /// - /// [`Row`]: struct.Row.html - /// [`Layout`]: ../layout/struct.Layout.html fn draw<Message>( &mut self, defaults: &Self::Defaults, children: &[Element<'_, Message, Self>], layout: Layout<'_>, cursor_position: Point, + viewport: &Rectangle, ) -> Self::Output; } @@ -236,7 +233,7 @@ impl<'a, Message, Renderer> From<Row<'a, Message, Renderer>> for Element<'a, Message, Renderer> where Renderer: 'a + self::Renderer, - Message: 'static, + Message: 'a, { fn from(row: Row<'a, Message, Renderer>) -> Element<'a, Message, Renderer> { Element::new(row) diff --git a/native/src/widget/rule.rs b/native/src/widget/rule.rs new file mode 100644 index 00000000..18c88658 --- /dev/null +++ b/native/src/widget/rule.rs @@ -0,0 +1,116 @@ +//! Display a horizontal or vertical rule for dividing content. + +use std::hash::Hash; + +use crate::{ + layout, Element, Hasher, Layout, Length, Point, Rectangle, Size, Widget, +}; + +/// Display a horizontal or vertical rule for dividing content. +#[derive(Debug, Copy, Clone)] +pub struct Rule<Renderer: self::Renderer> { + width: Length, + height: Length, + style: Renderer::Style, + is_horizontal: bool, +} + +impl<Renderer: self::Renderer> Rule<Renderer> { + /// Creates a horizontal [`Rule`] for dividing content by the given vertical spacing. + pub fn horizontal(spacing: u16) -> Self { + Rule { + width: Length::Fill, + height: Length::from(Length::Units(spacing)), + style: Renderer::Style::default(), + is_horizontal: true, + } + } + + /// Creates a vertical [`Rule`] for dividing content by the given horizontal spacing. + pub fn vertical(spacing: u16) -> Self { + Rule { + width: Length::from(Length::Units(spacing)), + height: Length::Fill, + style: Renderer::Style::default(), + is_horizontal: false, + } + } + + /// Sets the style of the [`Rule`]. + pub fn style(mut self, style: impl Into<Renderer::Style>) -> Self { + self.style = style.into(); + self + } +} + +impl<Message, Renderer> Widget<Message, Renderer> for Rule<Renderer> +where + Renderer: self::Renderer, +{ + fn width(&self) -> Length { + self.width + } + + fn height(&self) -> Length { + self.height + } + + fn layout( + &self, + _renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + let limits = limits.width(self.width).height(self.height); + + layout::Node::new(limits.resolve(Size::ZERO)) + } + + fn draw( + &self, + renderer: &mut Renderer, + _defaults: &Renderer::Defaults, + layout: Layout<'_>, + _cursor_position: Point, + _viewport: &Rectangle, + ) -> Renderer::Output { + renderer.draw(layout.bounds(), &self.style, self.is_horizontal) + } + + fn hash_layout(&self, state: &mut Hasher) { + struct Marker; + std::any::TypeId::of::<Marker>().hash(state); + + self.width.hash(state); + self.height.hash(state); + } +} + +/// The renderer of a [`Rule`]. +pub trait Renderer: crate::Renderer { + /// The style supported by this renderer. + type Style: Default; + + /// Draws a [`Rule`]. + /// + /// It receives: + /// * the bounds of the [`Rule`] + /// * the style of the [`Rule`] + /// * whether the [`Rule`] is horizontal (true) or vertical (false) + fn draw( + &mut self, + bounds: Rectangle, + style: &Self::Style, + is_horizontal: bool, + ) -> Self::Output; +} + +impl<'a, Message, Renderer> From<Rule<Renderer>> + for Element<'a, Message, Renderer> +where + Renderer: 'a + self::Renderer, + Message: 'a, +{ + fn from(rule: Rule<Renderer>) -> Element<'a, Message, Renderer> { + Element::new(rule) + } +} diff --git a/native/src/widget/scrollable.rs b/native/src/widget/scrollable.rs index eb1722ed..8c321ee5 100644 --- a/native/src/widget/scrollable.rs +++ b/native/src/widget/scrollable.rs @@ -1,9 +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::{self, Touch}; use crate::{ - column, - input::{mouse, touch, ButtonState, Touch}, - layout, Align, Clipboard, Column, Element, Event, Hasher, Layout, Length, - Point, Rectangle, Size, Widget, + Align, Clipboard, Column, Element, Hasher, Layout, Length, Point, + Rectangle, Size, Vector, Widget, }; use std::{f32, hash::Hash, u32}; @@ -15,20 +19,23 @@ pub struct Scrollable<'a, Message, Renderer: self::Renderer> { state: &'a mut State, height: Length, max_height: u32, + scrollbar_width: u16, + scrollbar_margin: u16, + scroller_width: u16, content: Column<'a, Message, Renderer>, style: Renderer::Style, } impl<'a, Message, Renderer: self::Renderer> Scrollable<'a, Message, Renderer> { /// Creates a new [`Scrollable`] with the given [`State`]. - /// - /// [`Scrollable`]: struct.Scrollable.html - /// [`State`]: struct.State.html pub fn new(state: &'a mut State) -> Self { Scrollable { state, height: Length::Shrink, max_height: u32::MAX, + scrollbar_width: 10, + scrollbar_margin: 0, + scroller_width: 10, content: Column::new(), style: Renderer::Style::default(), } @@ -45,64 +52,68 @@ impl<'a, Message, Renderer: self::Renderer> Scrollable<'a, Message, Renderer> { } /// Sets the padding of the [`Scrollable`]. - /// - /// [`Scrollable`]: struct.Scrollable.html pub fn padding(mut self, units: u16) -> Self { self.content = self.content.padding(units); self } /// Sets the width of the [`Scrollable`]. - /// - /// [`Scrollable`]: struct.Scrollable.html pub fn width(mut self, width: Length) -> Self { self.content = self.content.width(width); self } /// Sets the height of the [`Scrollable`]. - /// - /// [`Scrollable`]: struct.Scrollable.html pub fn height(mut self, height: Length) -> Self { self.height = height; self } /// Sets the maximum width of the [`Scrollable`]. - /// - /// [`Scrollable`]: struct.Scrollable.html pub fn max_width(mut self, max_width: u32) -> Self { self.content = self.content.max_width(max_width); self } /// Sets the maximum height of the [`Scrollable`] in pixels. - /// - /// [`Scrollable`]: struct.Scrollable.html pub fn max_height(mut self, max_height: u32) -> Self { self.max_height = max_height; self } /// Sets the horizontal alignment of the contents of the [`Scrollable`] . - /// - /// [`Scrollable`]: struct.Scrollable.html pub fn align_items(mut self, align_items: Align) -> Self { self.content = self.content.align_items(align_items); self } + /// Sets the scrollbar width of the [`Scrollable`] . + /// Silently enforces a minimum value of 1. + pub fn scrollbar_width(mut self, scrollbar_width: u16) -> Self { + self.scrollbar_width = scrollbar_width.max(1); + self + } + + /// Sets the scrollbar margin of the [`Scrollable`] . + pub fn scrollbar_margin(mut self, scrollbar_margin: u16) -> Self { + self.scrollbar_margin = scrollbar_margin; + self + } + + /// Sets the scroller width of the [`Scrollable`] . + /// Silently enforces a minimum value of 1. + pub fn scroller_width(mut self, scroller_width: u16) -> Self { + self.scroller_width = scroller_width.max(1); + self + } + /// Sets the style of the [`Scrollable`] . - /// - /// [`Scrollable`]: struct.Scrollable.html pub fn style(mut self, style: impl Into<Renderer::Style>) -> Self { self.style = style.into(); self } /// Adds an element to the [`Scrollable`]. - /// - /// [`Scrollable`]: struct.Scrollable.html pub fn push<E>(mut self, child: E) -> Self where E: Into<Element<'a, Message, Renderer>>, @@ -115,7 +126,7 @@ impl<'a, Message, Renderer: self::Renderer> Scrollable<'a, Message, Renderer> { impl<'a, Message, Renderer> Widget<Message, Renderer> for Scrollable<'a, Message, Renderer> where - Renderer: 'static + self::Renderer + column::Renderer, + Renderer: self::Renderer, { fn width(&self) -> Length { Widget::<Message, Renderer>::width(&self.content) @@ -154,14 +165,56 @@ where messages: &mut Vec<Message>, renderer: &Renderer, clipboard: Option<&dyn Clipboard>, - ) { + ) -> event::Status { let bounds = layout.bounds(); let is_mouse_over = bounds.contains(cursor_position); let content = layout.children().next().unwrap(); let content_bounds = content.bounds(); - // TODO: Event capture. Nested scrollables should capture scroll events. + let offset = self.state.offset(bounds, content_bounds); + let scrollbar = renderer.scrollbar( + bounds, + content_bounds, + offset, + self.scrollbar_width, + self.scrollbar_margin, + self.scroller_width, + ); + let is_mouse_over_scrollbar = scrollbar + .as_ref() + .map(|scrollbar| scrollbar.is_mouse_over(cursor_position)) + .unwrap_or(false); + + let event_status = { + let cursor_position = if is_mouse_over && !is_mouse_over_scrollbar { + Point::new( + cursor_position.x, + cursor_position.y + + self.state.offset(bounds, content_bounds) as f32, + ) + } else { + // TODO: Make `cursor_position` an `Option<Point>` so we can encode + // cursor availability. + // This will probably happen naturally once we add multi-window + // support. + Point::new(cursor_position.x, -1.0) + }; + + self.content.on_event( + event.clone(), + content, + cursor_position, + messages, + renderer, + clipboard, + ) + }; + + if let event::Status::Captured = event_status { + return event::Status::Captured; + } + if is_mouse_over { match event { Event::Mouse(mouse::Event::WheelScrolled { delta }) => { @@ -174,6 +227,8 @@ where self.state.scroll(y, bounds, content_bounds); } } + + return event::Status::Captured; } Event::Touch(Touch { phase, .. }) => match phase { touch::Phase::Started => { @@ -199,19 +254,11 @@ where } } - 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::Input { - button: mouse::Button::Left, - state: ButtonState::Released, - }) + Event::Mouse(mouse::Event::ButtonReleased( + mouse::Button::Left, + )) | Event::Touch(Touch { phase: touch::Phase::Ended, .. @@ -221,6 +268,8 @@ where .. }) => { self.state.scroller_grabbed_at = None; + + return event::Status::Captured; } Event::Mouse(mouse::Event::CursorMoved { .. }) | Event::Touch(Touch { @@ -238,16 +287,17 @@ where bounds, content_bounds, ); + + return event::Status::Captured; } } _ => {} } } else if is_mouse_over_scrollbar { match event { - Event::Mouse(mouse::Event::Input { - button: mouse::Button::Left, - state: ButtonState::Pressed, - }) + Event::Mouse(mouse::Event::ButtonPressed( + mouse::Button::Left, + )) | Event::Touch(Touch { phase: touch::Phase::Started, .. @@ -267,6 +317,8 @@ where self.state.scroller_grabbed_at = Some(scroller_grabbed_at); + + return event::Status::Captured; } } } @@ -274,28 +326,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( @@ -304,12 +335,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 @@ -329,6 +368,10 @@ where defaults, content_layout, cursor_position, + &Rectangle { + y: bounds.y + offset as f32, + ..bounds + }, ) }; @@ -347,18 +390,35 @@ where } fn hash_layout(&self, state: &mut Hasher) { - std::any::TypeId::of::<Scrollable<'static, (), Renderer>>().hash(state); + struct Marker; + std::any::TypeId::of::<Marker>().hash(state); self.height.hash(state); self.max_height.hash(state); self.content.hash_layout(state) } + + fn overlay( + &mut self, + layout: Layout<'_>, + ) -> Option<overlay::Element<'_, Message, Renderer>> { + let Self { content, state, .. } = self; + + content + .overlay(layout.children().next().unwrap()) + .map(|overlay| { + let bounds = layout.bounds(); + let content_layout = layout.children().next().unwrap(); + let content_bounds = content_layout.bounds(); + let offset = state.offset(bounds, content_bounds); + + overlay.translate(Vector::new(0.0, -(offset as f32))) + }) + } } /// The local state of a [`Scrollable`]. -/// -/// [`Scrollable`]: struct.Scrollable.html #[derive(Debug, Clone, Copy, Default)] pub struct State { scroller_grabbed_at: Option<f32>, @@ -368,17 +428,12 @@ pub struct State { 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, @@ -399,9 +454,6 @@ impl State { /// /// `0` represents scrollbar at the top, while `1` represents scrollbar at /// the bottom. - /// - /// [`Scrollable`]: struct.Scrollable.html - /// [`State`]: struct.State.html pub fn scroll_to( &mut self, percentage: f32, @@ -414,9 +466,6 @@ impl State { /// Returns the current scrolling offset of the [`State`], given the bounds /// of the [`Scrollable`] and its contents. - /// - /// [`Scrollable`]: struct.Scrollable.html - /// [`State`]: struct.State.html pub fn offset(&self, bounds: Rectangle, content_bounds: Rectangle) -> u32 { let hidden_content = (content_bounds.height - bounds.height).max(0.0).round() as u32; @@ -436,28 +485,29 @@ impl State { } /// 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 @@ -482,13 +532,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, } @@ -497,22 +543,21 @@ pub struct Scroller { /// Your [renderer] will need to implement this trait before being /// able to use a [`Scrollable`] in your user interface. /// -/// [`Scrollable`]: struct.Scrollable.html -/// [renderer]: ../../renderer/index.html -pub trait Renderer: crate::Renderer + Sized { +/// [renderer]: crate::renderer +pub trait Renderer: column::Renderer + Sized { /// The style supported by this renderer. type Style: Default; /// Returns the [`Scrollbar`] given the bounds and content bounds of a /// [`Scrollable`]. - /// - /// [`Scrollbar`]: struct.Scrollbar.html - /// [`Scrollable`]: struct.Scrollable.html fn scrollbar( &self, bounds: Rectangle, content_bounds: Rectangle, offset: u32, + scrollbar_width: u16, + scrollbar_margin: u16, + scroller_width: u16, ) -> Option<Scrollbar>; /// Draws the [`Scrollable`]. @@ -526,10 +571,6 @@ pub trait Renderer: crate::Renderer + Sized { /// - a optional [`Scrollbar`] to be rendered /// - the scrolling offset /// - the drawn content - /// - /// [`Scrollbar`]: struct.Scrollbar.html - /// [`Scrollable`]: struct.Scrollable.html - /// [`State`]: struct.State.html fn draw( &mut self, scrollable: &State, @@ -547,8 +588,8 @@ pub trait Renderer: crate::Renderer + Sized { impl<'a, Message, Renderer> From<Scrollable<'a, Message, Renderer>> for Element<'a, Message, Renderer> where - Renderer: 'static + self::Renderer + column::Renderer, - Message: 'static, + Renderer: 'a + self::Renderer, + Message: 'a, { fn from( scrollable: Scrollable<'a, Message, Renderer>, diff --git a/native/src/widget/slider.rs b/native/src/widget/slider.rs index c98cebb6..755e6b2b 100644 --- a/native/src/widget/slider.rs +++ b/native/src/widget/slider.rs @@ -1,13 +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::{self, Touch}; use crate::{ - input::{mouse, touch, ButtonState, Touch}, - layout, 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}; @@ -17,13 +16,15 @@ use std::{hash::Hash, ops::RangeInclusive}; /// /// A [`Slider`] will try to fill the horizontal space of its container. /// -/// [`Slider`]: struct.Slider.html +/// The [`Slider`] range of numeric values is generic and its step size defaults +/// to 1 unit. /// /// # Example /// ``` /// # use iced_native::{slider, renderer::Null}; /// # -/// # pub type Slider<'a, Message> = iced_native::Slider<'a, Message, Null>; +/// # pub type Slider<'a, T, Message> = iced_native::Slider<'a, T, Message, Null>; +/// #[derive(Clone)] /// pub enum Message { /// SliderChanged(f32), /// } @@ -36,16 +37,24 @@ use std::{hash::Hash, ops::RangeInclusive}; /// ///  #[allow(missing_debug_implementations)] -pub struct Slider<'a, Message, Renderer: self::Renderer> { +pub struct Slider<'a, T, Message, Renderer: self::Renderer> { state: &'a mut State, - range: RangeInclusive<f32>, - value: f32, - on_change: Box<dyn Fn(f32) -> Message>, + range: RangeInclusive<T>, + step: T, + value: T, + on_change: Box<dyn Fn(T) -> Message>, + on_release: Option<Message>, width: Length, + height: u16, style: Renderer::Style, } -impl<'a, Message, Renderer: self::Renderer> Slider<'a, Message, Renderer> { +impl<'a, T, Message, Renderer> Slider<'a, T, Message, Renderer> +where + T: Copy + From<u8> + std::cmp::PartialOrd, + Message: Clone, + Renderer: self::Renderer, +{ /// Creates a new [`Slider`]. /// /// It expects: @@ -55,48 +64,77 @@ impl<'a, Message, Renderer: self::Renderer> Slider<'a, Message, Renderer> { /// * a function that will be called when the [`Slider`] is dragged. /// It receives the new value of the [`Slider`] and must produce a /// `Message`. - /// - /// [`Slider`]: struct.Slider.html - /// [`State`]: struct.State.html pub fn new<F>( state: &'a mut State, - range: RangeInclusive<f32>, - value: f32, + range: RangeInclusive<T>, + value: T, on_change: F, ) -> Self where - F: 'static + Fn(f32) -> Message, + F: 'static + Fn(T) -> Message, { + let value = if value >= *range.start() { + value + } else { + *range.start() + }; + + let value = if value <= *range.end() { + value + } else { + *range.end() + }; + Slider { state, - value: value.max(*range.start()).min(*range.end()), + value, range, + step: T::from(1), on_change: Box::new(on_change), + on_release: None, width: Length::Fill, + height: Renderer::DEFAULT_HEIGHT, style: Renderer::Style::default(), } } - /// Sets the width of the [`Slider`]. + /// Sets the release message of the [`Slider`]. + /// This is called when the mouse is released from the slider. /// - /// [`Slider`]: struct.Slider.html + /// Typically, the user's interaction with the slider is finished when this message is produced. + /// This is useful if you need to spawn a long-running task from the slider's result, where + /// the default on_change message could create too many events. + pub fn on_release(mut self, on_release: Message) -> Self { + self.on_release = Some(on_release); + self + } + + /// Sets the width of the [`Slider`]. pub fn width(mut self, width: Length) -> Self { self.width = width; self } + /// Sets the height of the [`Slider`]. + pub fn height(mut self, height: u16) -> Self { + self.height = height; + self + } + /// Sets the style of the [`Slider`]. - /// - /// [`Slider`]: struct.Slider.html pub fn style(mut self, style: impl Into<Renderer::Style>) -> Self { self.style = style.into(); self } + + /// Sets the step size of the [`Slider`]. + pub fn step(mut self, step: T) -> Self { + self.step = step; + self + } } /// The local state of a [`Slider`]. -/// -/// [`Slider`]: struct.Slider.html #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub struct State { is_dragging: bool, @@ -104,16 +142,16 @@ pub struct State { impl State { /// Creates a new [`State`]. - /// - /// [`State`]: struct.State.html pub fn new() -> State { State::default() } } -impl<'a, Message, Renderer> Widget<Message, Renderer> - for Slider<'a, Message, Renderer> +impl<'a, T, Message, Renderer> Widget<Message, Renderer> + for Slider<'a, T, Message, Renderer> where + T: Copy + Into<f64> + num_traits::FromPrimitive, + Message: Clone, Renderer: self::Renderer, { fn width(&self) -> Length { @@ -126,12 +164,11 @@ where fn layout( &self, - renderer: &Renderer, + _renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { - let limits = limits - .width(self.width) - .height(Length::Units(renderer.height() as u16)); + let limits = + limits.width(self.width).height(Length::Units(self.height)); let size = limits.resolve(Size::ZERO); @@ -146,28 +183,32 @@ where messages: &mut Vec<Message>, _renderer: &Renderer, _clipboard: Option<&dyn Clipboard>, - ) { + ) -> event::Status { let mut change = || { let bounds = layout.bounds(); - if cursor_position.x <= bounds.x { messages.push((self.on_change)(*self.range.start())); } else if cursor_position.x >= bounds.x + bounds.width { messages.push((self.on_change)(*self.range.end())); } else { - let percent = (cursor_position.x - bounds.x) / bounds.width; - let value = (self.range.end() - self.range.start()) * percent - + self.range.start(); + let step = self.step.into(); + let start = (*self.range.start()).into(); + let end = (*self.range.end()).into(); + + let percent = f64::from(cursor_position.x - bounds.x) + / f64::from(bounds.width); - messages.push((self.on_change)(value)); + let steps = (percent * (end - start) / step).round(); + let value = steps * step + start; + + if let Some(value) = T::from_f64(value) { + messages.push((self.on_change)(value)); + } } }; match event { - Event::Mouse(mouse::Event::Input { - button: mouse::Button::Left, - state: ButtonState::Pressed, - }) + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) | Event::Touch(Touch { phase: touch::Phase::Started, .. @@ -175,17 +216,23 @@ where if layout.bounds().contains(cursor_position) { change(); self.state.is_dragging = true; + + return event::Status::Captured; } } - Event::Mouse(mouse::Event::Input { - button: mouse::Button::Left, - state: ButtonState::Released, - }) + Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) | Event::Touch(Touch { phase: touch::Phase::Ended, .. }) => { - self.state.is_dragging = false; + if self.state.is_dragging { + if let Some(on_release) = self.on_release.clone() { + messages.push(on_release); + } + self.state.is_dragging = false; + + return event::Status::Captured; + } } Event::Mouse(mouse::Event::CursorMoved { .. }) | Event::Touch(Touch { @@ -194,10 +241,14 @@ where }) => { if self.state.is_dragging { change(); + + return event::Status::Captured; } } _ => {} } + + event::Status::Ignored } fn draw( @@ -206,18 +257,25 @@ where _defaults: &Renderer::Defaults, layout: Layout<'_>, cursor_position: Point, + _viewport: &Rectangle, ) -> Renderer::Output { + let start = *self.range.start(); + let end = *self.range.end(); + renderer.draw( layout.bounds(), cursor_position, - self.range.clone(), - self.value, + start.into() as f32..=end.into() as f32, + self.value.into() as f32, self.state.is_dragging, &self.style, ) } fn hash_layout(&self, state: &mut Hasher) { + struct Marker; + std::any::TypeId::of::<Marker>().hash(state); + self.width.hash(state); } } @@ -227,16 +285,13 @@ where /// Your [renderer] will need to implement this trait before being /// able to use a [`Slider`] in your user interface. /// -/// [`Slider`]: struct.Slider.html -/// [renderer]: ../../renderer/index.html +/// [renderer]: crate::renderer pub trait Renderer: crate::Renderer { /// The style supported by this renderer. type Style: Default; - /// Returns the height of the [`Slider`]. - /// - /// [`Slider`]: struct.Slider.html - fn height(&self) -> u32; + /// The default height of a [`Slider`]. + const DEFAULT_HEIGHT: u16; /// Draws a [`Slider`]. /// @@ -246,10 +301,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, @@ -261,14 +312,15 @@ pub trait Renderer: crate::Renderer { ) -> Self::Output; } -impl<'a, Message, Renderer> From<Slider<'a, Message, Renderer>> +impl<'a, T, Message, Renderer> From<Slider<'a, T, Message, Renderer>> for Element<'a, Message, Renderer> where - Renderer: 'static + self::Renderer, - Message: 'static, + T: 'a + Copy + Into<f64> + num_traits::FromPrimitive, + Message: 'a + Clone, + Renderer: 'a + self::Renderer, { fn from( - slider: Slider<'a, Message, Renderer>, + slider: Slider<'a, T, Message, Renderer>, ) -> Element<'a, Message, Renderer> { Element::new(slider) } diff --git a/native/src/widget/space.rs b/native/src/widget/space.rs index 24c94bf6..6b34ece8 100644 --- a/native/src/widget/space.rs +++ b/native/src/widget/space.rs @@ -16,15 +16,11 @@ pub struct Space { impl Space { /// Creates an amount of empty [`Space`] with the given width and height. - /// - /// [`Space`]: struct.Space.html pub fn new(width: Length, height: Length) -> Self { Space { width, height } } /// Creates an amount of horizontal [`Space`]. - /// - /// [`Space`]: struct.Space.html pub fn with_width(width: Length) -> Self { Space { width, @@ -33,8 +29,6 @@ impl Space { } /// Creates an amount of vertical [`Space`]. - /// - /// [`Space`]: struct.Space.html pub fn with_height(height: Length) -> Self { Space { width: Length::Shrink, @@ -43,7 +37,7 @@ impl Space { } } -impl<'a, Message, Renderer> Widget<Message, Renderer> for Space +impl<Message, Renderer> Widget<Message, Renderer> for Space where Renderer: self::Renderer, { @@ -71,33 +65,31 @@ where _defaults: &Renderer::Defaults, layout: Layout<'_>, _cursor_position: Point, + _viewport: &Rectangle, ) -> Renderer::Output { renderer.draw(layout.bounds()) } fn hash_layout(&self, state: &mut Hasher) { std::any::TypeId::of::<Space>().hash(state); + self.width.hash(state); self.height.hash(state); } } /// 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; } impl<'a, Message, Renderer> From<Space> for Element<'a, Message, Renderer> where Renderer: self::Renderer, - Message: 'static, + Message: 'a, { fn from(space: Space) -> Element<'a, Message, Renderer> { Element::new(space) diff --git a/native/src/widget/svg.rs b/native/src/widget/svg.rs index 063730bb..9cd61918 100644 --- a/native/src/widget/svg.rs +++ b/native/src/widget/svg.rs @@ -1,9 +1,11 @@ //! 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, - path::{Path, PathBuf}, + hash::{Hash, Hasher as _}, + path::PathBuf, + sync::Arc, }; /// A vector graphics image. @@ -12,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, @@ -23,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(), @@ -34,17 +31,19 @@ impl Svg { } } + /// Creates a new [`Svg`] that will display the contents of the file at the + /// provided path. + 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 @@ -94,68 +93,88 @@ where _defaults: &Renderer::Defaults, layout: Layout<'_>, _cursor_position: Point, + _viewport: &Rectangle, ) -> Renderer::Output { renderer.draw(self.handle.clone(), layout) } fn hash_layout(&self, state: &mut Hasher) { + std::any::TypeId::of::<Svg>().hash(state); + + self.handle.hash(state); self.width.hash(state); self.height.hash(state); } } /// An [`Svg`] handle. -/// -/// [`Svg`]: struct.Svg.html #[derive(Debug, Clone)] pub struct Handle { id: u64, - path: PathBuf, + data: Arc<Data>, } impl Handle { /// Creates an SVG [`Handle`] pointing to the vector image of the given /// path. - /// - /// [`Handle`]: struct.Handle.html - pub fn from_path<T: Into<PathBuf>>(path: T) -> Handle { - use std::hash::Hasher as _; + pub fn from_path(path: impl Into<PathBuf>) -> Handle { + Self::from_data(Data::Path(path.into())) + } - let path = path.into(); + /// Creates an SVG [`Handle`] from raw bytes containing either an SVG string + /// or gzip compressed data. + /// + /// This is useful if you already have your SVG data in-memory, maybe + /// because you downloaded or generated it procedurally. + pub fn from_memory(bytes: impl Into<Vec<u8>>) -> Handle { + Self::from_data(Data::Bytes(bytes.into())) + } + fn from_data(data: Data) -> Handle { let mut hasher = Hasher::default(); - path.hash(&mut hasher); + data.hash(&mut hasher); Handle { id: hasher.finish(), - path, + data: Arc::new(data), } } /// Returns the unique identifier of the [`Handle`]. - /// - /// [`Handle`]: struct.Handle.html pub fn id(&self) -> u64 { self.id } - /// Returns a reference to the path of the [`Handle`]. - /// - /// [`Handle`]: enum.Handle.html - pub fn path(&self) -> &Path { - &self.path + /// Returns a reference to the SVG [`Data`]. + pub fn data(&self) -> &Data { + &self.data } } -impl From<String> for Handle { - fn from(path: String) -> Handle { - Handle::from_path(path) +impl Hash for Handle { + fn hash<H: std::hash::Hasher>(&self, state: &mut H) { + self.id.hash(state); } } -impl From<&str> for Handle { - fn from(path: &str) -> Handle { - Handle::from_path(path) +/// The data of an [`Svg`]. +#[derive(Clone, Hash)] +pub enum Data { + /// File data + Path(PathBuf), + + /// In-memory data + /// + /// Can contain an SVG string or a gzip compressed data. + Bytes(Vec<u8>), +} + +impl std::fmt::Debug for Data { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Data::Path(path) => write!(f, "Path({:?})", path), + Data::Bytes(_) => write!(f, "Bytes(...)"), + } } } @@ -164,17 +183,12 @@ impl From<&str> for Handle { /// 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`] located on the given path. - /// - /// [`Svg`]: struct.Svg.html + /// Returns the default dimensions of an [`Svg`] for the given [`Handle`]. 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 7d8cad6e..6cc18e6c 100644 --- a/native/src/widget/text.rs +++ b/native/src/widget/text.rs @@ -1,7 +1,7 @@ //! Write some text for your users to read. use crate::{ - layout, Color, Element, Font, Hasher, HorizontalAlignment, Layout, Length, - Point, Rectangle, Size, VerticalAlignment, Widget, + layout, Color, Element, Hasher, HorizontalAlignment, Layout, Length, Point, + Rectangle, Size, VerticalAlignment, Widget, }; use std::hash::Hash; @@ -11,7 +11,7 @@ use std::hash::Hash; /// # Example /// /// ``` -/// # use iced_native::Text; +/// # type Text = iced_native::Text<iced_native::renderer::Null>; /// # /// Text::new("I <3 iced!") /// .color([0.0, 0.0, 1.0]) @@ -19,28 +19,26 @@ use std::hash::Hash; /// ``` /// ///  -#[derive(Debug, Clone)] -pub struct Text { +#[derive(Debug)] +pub struct Text<Renderer: self::Renderer> { content: String, size: Option<u16>, color: Option<Color>, - font: Font, + font: Renderer::Font, width: Length, height: Length, horizontal_alignment: HorizontalAlignment, vertical_alignment: VerticalAlignment, } -impl Text { +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(), size: None, color: None, - font: Font::Default, + font: Default::default(), width: Length::Shrink, height: Length::Shrink, horizontal_alignment: HorizontalAlignment::Left, @@ -49,17 +47,12 @@ impl Text { } /// 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 Text { /// Sets the [`Font`] of the [`Text`]. /// - /// [`Text`]: struct.Text.html - /// [`Font`]: ../../struct.Font.html - pub fn font(mut self, font: Font) -> Self { - self.font = font; + /// [`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,16 +88,13 @@ impl Text { } /// 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 } } -impl<Message, Renderer> Widget<Message, Renderer> for Text +impl<Message, Renderer> Widget<Message, Renderer> for Text<Renderer> where Renderer: self::Renderer, { @@ -131,7 +113,7 @@ where ) -> layout::Node { let limits = limits.width(self.width).height(self.height); - let size = self.size.unwrap_or(Renderer::DEFAULT_SIZE); + let size = self.size.unwrap_or(renderer.default_size()); let bounds = limits.max(); @@ -149,12 +131,13 @@ where defaults: &Renderer::Defaults, layout: Layout<'_>, _cursor_position: Point, + _viewport: &Rectangle, ) -> Renderer::Output { renderer.draw( defaults, layout.bounds(), &self.content, - self.size.unwrap_or(Renderer::DEFAULT_SIZE), + self.size.unwrap_or(renderer.default_size()), self.font, self.color, self.horizontal_alignment, @@ -163,6 +146,9 @@ where } fn hash_layout(&self, state: &mut Hasher) { + struct Marker; + std::any::TypeId::of::<Marker>().hash(state); + self.content.hash(state); self.size.hash(state); self.width.hash(state); @@ -173,26 +159,23 @@ 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 default size of [`Text`]. - /// - /// [`Text`]: struct.Text.html - const DEFAULT_SIZE: u16; + /// The font type used for [`Text`]. + type Font: Default + Copy; + + /// Returns the default size of [`Text`]. + fn default_size(&self) -> u16; /// Measures the [`Text`] in the given bounds and returns the minimum /// boundaries that can fit the contents. - /// - /// [`Text`]: struct.Text.html fn measure( &self, content: &str, size: u16, - font: Font, + font: Self::Font, bounds: Size, ) -> (f32, f32); @@ -205,28 +188,40 @@ 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, bounds: Rectangle, content: &str, size: u16, - font: Font, + font: Self::Font, color: Option<Color>, horizontal_alignment: HorizontalAlignment, vertical_alignment: VerticalAlignment, ) -> Self::Output; } -impl<'a, Message, Renderer> From<Text> for Element<'a, Message, Renderer> +impl<'a, Message, Renderer> From<Text<Renderer>> + for Element<'a, Message, Renderer> where - Renderer: self::Renderer, + Renderer: self::Renderer + 'a, { - fn from(text: Text) -> Element<'a, Message, Renderer> { + fn from(text: Text<Renderer>) -> Element<'a, Message, Renderer> { Element::new(text) } } + +impl<Renderer: self::Renderer> Clone for Text<Renderer> { + fn clone(&self) -> Self { + Self { + content: self.content.clone(), + size: self.size, + color: self.color, + font: self.font, + width: self.width, + height: self.height, + horizontal_alignment: self.horizontal_alignment, + vertical_alignment: self.vertical_alignment, + } + } +} diff --git a/native/src/widget/text_input.rs b/native/src/widget/text_input.rs index c06a8cce..ca71c20c 100644 --- a/native/src/widget/text_input.rs +++ b/native/src/widget/text_input.rs @@ -1,17 +1,27 @@ //! 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; + +pub mod cursor; + +pub use cursor::Cursor; +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::{self, Touch}; use crate::{ - input::{keyboard, mouse, touch, ButtonState, Touch}, - layout, Clipboard, Element, Event, Font, Hasher, Layout, Length, Point, - Rectangle, Size, Widget, + Clipboard, Element, Hasher, Layout, Length, Point, Rectangle, Size, Widget, }; use std::u32; -use unicode_segmentation::UnicodeSegmentation; /// A field that can be filled with text. /// @@ -43,7 +53,7 @@ pub struct TextInput<'a, Message, Renderer: self::Renderer> { placeholder: String, value: Value, is_secure: bool, - font: Font, + font: Renderer::Font, width: Length, max_width: u32, padding: u16, @@ -53,7 +63,11 @@ pub struct TextInput<'a, Message, Renderer: self::Renderer> { style: Renderer::Style, } -impl<'a, Message, Renderer: self::Renderer> TextInput<'a, Message, Renderer> { +impl<'a, Message, Renderer> TextInput<'a, Message, Renderer> +where + Message: Clone, + Renderer: self::Renderer, +{ /// Creates a new [`TextInput`]. /// /// It expects: @@ -61,9 +75,6 @@ impl<'a, Message, Renderer: self::Renderer> TextInput<'a, Message, Renderer> { /// - a placeholder /// - the current value /// - a function that produces a message when the [`TextInput`] changes - /// - /// [`TextInput`]: struct.TextInput.html - /// [`State`]: struct.State.html pub fn new<F>( state: &'a mut State, placeholder: &str, @@ -78,7 +89,7 @@ impl<'a, Message, Renderer: self::Renderer> TextInput<'a, Message, Renderer> { placeholder: String::from(placeholder), value: Value::new(value), is_secure: false, - font: Font::Default, + font: Default::default(), width: Length::Fill, max_width: u32::MAX, padding: 0, @@ -90,8 +101,6 @@ impl<'a, Message, Renderer: self::Renderer> TextInput<'a, Message, Renderer> { } /// Converts the [`TextInput`] into a secure password input. - /// - /// [`TextInput`]: struct.TextInput.html pub fn password(mut self) -> Self { self.is_secure = true; self @@ -99,39 +108,31 @@ impl<'a, Message, Renderer: self::Renderer> TextInput<'a, Message, Renderer> { /// Sets the [`Font`] of the [`Text`]. /// - /// [`Text`]: struct.Text.html - /// [`Font`]: ../../struct.Font.html - pub fn font(mut self, font: Font) -> Self { + /// [`Font`]: crate::widget::text::Renderer::Font + /// [`Text`]: crate::widget::Text + pub fn font(mut self, font: Renderer::Font) -> Self { self.font = font; self } /// Sets the width of the [`TextInput`]. - /// - /// [`TextInput`]: struct.TextInput.html pub fn width(mut self, width: Length) -> Self { self.width = width; self } /// Sets the maximum width of the [`TextInput`]. - /// - /// [`TextInput`]: struct.TextInput.html pub fn max_width(mut self, max_width: u32) -> Self { self.max_width = max_width; self } /// Sets the padding of the [`TextInput`]. - /// - /// [`TextInput`]: struct.TextInput.html pub fn padding(mut self, units: u16) -> Self { self.padding = units; self } /// Sets the text size of the [`TextInput`]. - /// - /// [`TextInput`]: struct.TextInput.html pub fn size(mut self, size: u16) -> Self { self.size = Some(size); self @@ -139,27 +140,75 @@ impl<'a, Message, Renderer: self::Renderer> TextInput<'a, Message, Renderer> { /// Sets the message that should be produced when the [`TextInput`] is /// focused and the enter key is pressed. - /// - /// [`TextInput`]: struct.TextInput.html pub fn on_submit(mut self, message: Message) -> Self { self.on_submit = Some(message); self } /// Sets the style of the [`TextInput`]. - /// - /// [`TextInput`]: struct.TextInput.html pub fn style(mut self, style: impl Into<Renderer::Style>) -> Self { self.style = style.into(); self } + + /// Returns the current [`State`] of the [`TextInput`]. + pub fn state(&self) -> &State { + self.state + } +} + +impl<'a, Message, Renderer> TextInput<'a, Message, Renderer> +where + Renderer: self::Renderer, +{ + /// Draws the [`TextInput`] with the given [`Renderer`], overriding its + /// [`Value`] if provided. + pub fn draw( + &self, + renderer: &mut Renderer, + layout: Layout<'_>, + cursor_position: Point, + value: Option<&Value>, + ) -> Renderer::Output { + let value = value.unwrap_or(&self.value); + let bounds = layout.bounds(); + let text_bounds = layout.children().next().unwrap().bounds(); + + if self.is_secure { + self::Renderer::draw( + renderer, + bounds, + text_bounds, + cursor_position, + self.font, + self.size.unwrap_or(renderer.default_size()), + &self.placeholder, + &value.secure(), + &self.state, + &self.style, + ) + } else { + self::Renderer::draw( + renderer, + bounds, + text_bounds, + cursor_position, + self.font, + self.size.unwrap_or(renderer.default_size()), + &self.placeholder, + value, + &self.state, + &self.style, + ) + } + } } impl<'a, Message, Renderer> Widget<Message, Renderer> for TextInput<'a, Message, Renderer> where - Renderer: 'static + self::Renderer, - Message: Clone + std::fmt::Debug, + Message: Clone, + Renderer: self::Renderer, { fn width(&self) -> Length { self.width @@ -197,22 +246,91 @@ where messages: &mut Vec<Message>, renderer: &Renderer, clipboard: Option<&dyn Clipboard>, - ) { + ) -> event::Status { match event { - Event::Mouse(mouse::Event::Input { - button: mouse::Button::Left, - state: ButtonState::Pressed, - }) + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) | Event::Touch(Touch { phase: touch::Phase::Started, .. }) => { 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; + let click = mouse::Click::new( + cursor_position, + self.state.last_click, + ); + + match click.kind() { + click::Kind::Single => { + if target > 0.0 { + let value = if self.is_secure { + self.value.secure() + } else { + self.value.clone() + }; + + let position = renderer.find_cursor_position( + text_layout.bounds(), + self.font, + self.size, + &value, + &self.state, + target, + ); + + self.state.cursor.move_to(position); + } else { + self.state.cursor.move_to(0); + } + + self.state.is_dragging = true; + } + click::Kind::Double => { + if self.is_secure { + self.state.cursor.select_all(&self.value); + } else { + let position = renderer.find_cursor_position( + text_layout.bounds(), + self.font, + self.size, + &self.value, + &self.state, + target, + ); + + self.state.cursor.select_range( + self.value.previous_start_of_word(position), + 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); + + return event::Status::Captured; + } + } + Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) => { + self.state.is_dragging = false; + } + Event::Mouse(mouse::Event::CursorMoved { position }) => { + if self.state.is_dragging { + let text_layout = layout.children().next().unwrap(); + let target = position.x - text_layout.bounds().x; + if target > 0.0 { let value = if self.is_secure { self.value.secure() @@ -220,154 +338,243 @@ where self.value.clone() }; - let size = self.size.unwrap_or(renderer.default_size()); - - let offset = renderer.offset( + let position = renderer.find_cursor_position( text_layout.bounds(), - size, + self.font, + self.size, &value, &self.state, - self.font, + target, ); - self.state.cursor_position = find_cursor_position( - renderer, - target + offset, - &value, - size, - 0, - self.value.len(), - self.font, + self.state.cursor.select_range( + self.state.cursor.start(&value), + position, ); - } else { - self.state.cursor_position = 0; } - } - self.state.is_focused = is_clicked; + return event::Status::Captured; + } } Event::Keyboard(keyboard::Event::CharacterReceived(c)) if self.state.is_focused && self.state.is_pasting.is_none() + && !self.state.keyboard_modifiers.is_command_pressed() && !c.is_control() => { - let cursor_position = self.state.cursor_position(&self.value); + let mut editor = + Editor::new(&mut self.value, &mut self.state.cursor); - self.value.insert(cursor_position, c); - self.state.move_cursor_right(&self.value); + editor.insert(c); - let message = (self.on_change)(self.value.to_string()); + let message = (self.on_change)(editor.contents()); messages.push(message); + + return event::Status::Captured; } - Event::Keyboard(keyboard::Event::Input { - key_code, - state: ButtonState::Pressed, - 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); + Event::Keyboard(keyboard::Event::KeyPressed { + 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 => { - let cursor_position = - self.state.cursor_position(&self.value); + 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); + } + } - if cursor_position > 0 { - self.state.move_cursor_left(&self.value); + let mut editor = Editor::new( + &mut self.value, + &mut self.state.cursor, + ); - let _ = self.value.remove(cursor_position - 1); + editor.backspace(); - let message = (self.on_change)(self.value.to_string()); + let message = (self.on_change)(editor.contents()); messages.push(message); } - } - keyboard::KeyCode::Delete => { - let cursor_position = - self.state.cursor_position(&self.value); + 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 + .cursor + .select_right_by_words(&self.value); + } + } + + let mut editor = Editor::new( + &mut self.value, + &mut self.state.cursor, + ); - if cursor_position < self.value.len() { - let _ = self.value.remove(cursor_position); + editor.delete(); - let message = (self.on_change)(self.value.to_string()); + let message = (self.on_change)(editor.contents()); messages.push(message); } - } - keyboard::KeyCode::Left => { - if platform::is_jump_modifier_pressed(modifiers) - && !self.is_secure - { - self.state.move_cursor_left_by_words(&self.value); - } else { - self.state.move_cursor_left(&self.value); + 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(&self.value); + } } - } - keyboard::KeyCode::Right => { - if platform::is_jump_modifier_pressed(modifiers) - && !self.is_secure - { - self.state.move_cursor_right_by_words(&self.value); - } else { - self.state.move_cursor_right(&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(&self.value); + } } - } - keyboard::KeyCode::Home => { - self.state.cursor_position = 0; - } - keyboard::KeyCode::End => { - self.state.move_cursor_to_end(&self.value); - } - keyboard::KeyCode::V => { - if platform::is_copy_paste_modifier_pressed(modifiers) { - if let Some(clipboard) = clipboard { - let content = match self.state.is_pasting.take() { - Some(content) => content, - None => { - let content: String = clipboard - .content() - .unwrap_or(String::new()) - .chars() - .filter(|c| !c.is_control()) - .collect(); - - Value::new(&content) - } - }; - - let cursor_position = - self.state.cursor_position(&self.value); - - self.value - .insert_many(cursor_position, content.clone()); - - self.state.move_cursor_right_by_amount( - &self.value, - content.len(), + keyboard::KeyCode::Home => { + if modifiers.shift { + self.state.cursor.select_range( + self.state.cursor.start(&self.value), + 0, ); - self.state.is_pasting = Some(content); - - let message = - (self.on_change)(self.value.to_string()); - messages.push(message); + } else { + self.state.cursor.move_to(0); + } + } + keyboard::KeyCode::End => { + if modifiers.shift { + self.state.cursor.select_range( + self.state.cursor.start(&self.value), + self.value.len(), + ); + } else { + self.state.cursor.move_to(self.value.len()); + } + } + keyboard::KeyCode::V => { + if self.state.keyboard_modifiers.is_command_pressed() { + if let Some(clipboard) = clipboard { + let content = match self.state.is_pasting.take() + { + Some(content) => content, + None => { + let content: String = clipboard + .content() + .unwrap_or(String::new()) + .chars() + .filter(|c| !c.is_control()) + .collect(); + + Value::new(&content) + } + }; + + let mut editor = Editor::new( + &mut self.value, + &mut self.state.cursor, + ); + + editor.paste(content.clone()); + + let message = + (self.on_change)(editor.contents()); + messages.push(message); + + self.state.is_pasting = Some(content); + } + } else { + self.state.is_pasting = None; + } + } + keyboard::KeyCode::A => { + if self.state.keyboard_modifiers.is_command_pressed() { + self.state.cursor.select_all(&self.value); } - } else { + } + 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(); } + _ => {} } - _ => {} - }, - Event::Keyboard(keyboard::Event::Input { - key_code, - state: ButtonState::Released, - .. - }) => match key_code { - keyboard::KeyCode::V => { - self.state.is_pasting = None; + + return event::Status::Captured; + } + Event::Keyboard(keyboard::Event::KeyReleased { + key_code, .. + }) 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( @@ -376,41 +583,15 @@ where _defaults: &Renderer::Defaults, layout: Layout<'_>, cursor_position: Point, + _viewport: &Rectangle, ) -> Renderer::Output { - let bounds = layout.bounds(); - let text_bounds = layout.children().next().unwrap().bounds(); - - if self.is_secure { - renderer.draw( - bounds, - text_bounds, - cursor_position, - self.size.unwrap_or(renderer.default_size()), - self.font, - &self.placeholder, - &self.value.secure(), - &self.state, - &self.style, - ) - } else { - renderer.draw( - bounds, - text_bounds, - cursor_position, - self.size.unwrap_or(renderer.default_size()), - self.font, - &self.placeholder, - &self.value, - &self.state, - &self.style, - ) - } + self.draw(renderer, layout, cursor_position, None) } fn hash_layout(&self, state: &mut Hasher) { use std::{any::TypeId, hash::Hash}; - - TypeId::of::<TextInput<'static, (), Renderer>>().hash(state); + struct Marker; + TypeId::of::<Marker>().hash(state); self.width.hash(state); self.max_width.hash(state); @@ -424,71 +605,82 @@ where /// Your [renderer] will need to implement this trait before being /// able to use a [`TextInput`] in your user interface. /// -/// [`TextInput`]: struct.TextInput.html -/// [renderer]: ../../renderer/index.html -pub trait Renderer: crate::Renderer + Sized { +/// [renderer]: crate::renderer +pub trait Renderer: text::Renderer + Sized { /// The style supported by this renderer. type Style: Default; - /// Returns the default size of the text of the [`TextInput`]. - /// - /// [`TextInput`]: struct.TextInput.html - fn default_size(&self) -> u16; - /// Returns the width of the value of the [`TextInput`]. - /// - /// [`TextInput`]: struct.TextInput.html - fn measure_value(&self, value: &str, size: u16, font: Font) -> f32; + fn measure_value(&self, value: &str, size: u16, font: Self::Font) -> f32; /// Returns the current horizontal offset of the value of the /// [`TextInput`]. /// /// This is the amount of horizontal scrolling applied when the [`Value`] /// does not fit the [`TextInput`]. - /// - /// [`TextInput`]: struct.TextInput.html - /// [`Value`]: struct.Value.html fn offset( &self, text_bounds: Rectangle, + font: Self::Font, size: u16, value: &Value, state: &State, - font: Font, ) -> f32; /// Draws a [`TextInput`]. /// /// It receives: - /// - its bounds of the [`TextInput`] + /// - the bounds of the [`TextInput`] /// - the bounds of the text (i.e. the current value) /// - the cursor position /// - the placeholder to show when the value is empty /// - the current [`Value`] /// - the current [`State`] - /// - /// [`TextInput`]: struct.TextInput.html - /// [`Value`]: struct.Value.html - /// [`State`]: struct.State.html fn draw( &mut self, bounds: Rectangle, text_bounds: Rectangle, cursor_position: Point, + font: Self::Font, size: u16, - font: Font, placeholder: &str, value: &Value, state: &State, style: &Self::Style, ) -> Self::Output; + + /// Computes the position of the text cursor at the given X coordinate of + /// a [`TextInput`]. + fn find_cursor_position( + &self, + text_bounds: Rectangle, + font: Self::Font, + size: Option<u16>, + value: &Value, + state: &State, + x: f32, + ) -> usize { + let size = size.unwrap_or(self.default_size()); + + let offset = self.offset(text_bounds, font, size, &value, &state); + + find_cursor_position( + self, + &value, + font, + size, + x + offset, + 0, + value.len(), + ) + } } impl<'a, Message, Renderer> From<TextInput<'a, Message, Renderer>> for Element<'a, Message, Renderer> where - Renderer: 'static + self::Renderer, - Message: 'static + Clone + std::fmt::Debug, + Message: 'a + Clone, + Renderer: 'a + self::Renderer, { fn from( text_input: TextInput<'a, Message, Renderer>, @@ -498,245 +690,80 @@ where } /// The state of a [`TextInput`]. -/// -/// [`TextInput`]: struct.TextInput.html #[derive(Debug, Default, Clone)] pub struct State { is_focused: bool, + is_dragging: bool, is_pasting: Option<Value>, - cursor_position: usize, + 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 { - use std::usize; - Self { is_focused: true, + is_dragging: false, is_pasting: None, - cursor_position: usize::MAX, + 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 position of a [`TextInput`]. - /// - /// [`TextInput`]: struct.TextInput.html - pub fn cursor_position(&self, value: &Value) -> usize { - self.cursor_position.min(value.len()) - } - - /// Moves the cursor of a [`TextInput`] to the left. - /// - /// [`TextInput`]: struct.TextInput.html - pub(crate) fn move_cursor_left(&mut self, value: &Value) { - let current = self.cursor_position(value); - - if current > 0 { - self.cursor_position = current - 1; - } - } - - /// Moves the cursor of a [`TextInput`] to the right. - /// - /// [`TextInput`]: struct.TextInput.html - pub(crate) fn move_cursor_right(&mut self, value: &Value) { - self.move_cursor_right_by_amount(value, 1) - } - - pub(crate) fn move_cursor_right_by_amount( - &mut self, - value: &Value, - amount: usize, - ) { - let current = self.cursor_position(value); - let new_position = current.saturating_add(amount); - - if new_position < value.len() + 1 { - self.cursor_position = new_position; - } - } - - /// Moves the cursor of a [`TextInput`] to the previous start of a word. - /// - /// [`TextInput`]: struct.TextInput.html - pub(crate) fn move_cursor_left_by_words(&mut self, value: &Value) { - let current = self.cursor_position(value); - - self.cursor_position = value.previous_start_of_word(current); + /// Returns the [`Cursor`] of the [`TextInput`]. + pub fn cursor(&self) -> Cursor { + self.cursor } - /// Moves the cursor of a [`TextInput`] to the next end of a word. - /// - /// [`TextInput`]: struct.TextInput.html - pub(crate) fn move_cursor_right_by_words(&mut self, value: &Value) { - let current = self.cursor_position(value); - - self.cursor_position = value.next_end_of_word(current); + /// Focuses the [`TextInput`]. + pub fn focus(&mut self) { + self.is_focused = true; } - /// Moves the cursor of a [`TextInput`] to the end. - /// - /// [`TextInput`]: struct.TextInput.html - pub(crate) fn move_cursor_to_end(&mut self, value: &Value) { - self.cursor_position = value.len(); + /// Unfocuses the [`TextInput`]. + pub fn unfocus(&mut self) { + self.is_focused = false; } -} -/// The value of a [`TextInput`]. -/// -/// [`TextInput`]: struct.TextInput.html -// TODO: Reduce allocations, cache results (?) -#[derive(Debug, Clone)] -pub struct Value { - graphemes: Vec<String>, -} - -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) - .collect(); - - Self { graphemes } - } - - /// Returns the total amount of graphemes in the [`Value`]. - /// - /// [`Value`]: struct.Value.html - 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(); - - UnicodeSegmentation::split_word_bound_indices(&previous_string as &str) - .filter(|(_, word)| !word.trim_start().is_empty()) - .next_back() - .map(|(i, previous_word)| { - index - - UnicodeSegmentation::graphemes(previous_word, true) - .count() - - UnicodeSegmentation::graphemes( - &previous_string[i + previous_word.len()..] as &str, - true, - ) - .count() - }) - .unwrap_or(0) + /// Moves the [`Cursor`] of the [`TextInput`] to the front of the input text. + pub fn move_cursor_to_front(&mut self) { + self.cursor.move_to(0); } - /// 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(); - - UnicodeSegmentation::split_word_bound_indices(&next_string as &str) - .filter(|(_, word)| !word.trim_start().is_empty()) - .next() - .map(|(i, next_word)| { - index - + UnicodeSegmentation::graphemes(next_word, true).count() - + UnicodeSegmentation::graphemes( - &next_string[..i] as &str, - true, - ) - .count() - }) - .unwrap_or(self.len()) + /// Moves the [`Cursor`] of the [`TextInput`] to the end of the input text. + pub fn move_cursor_to_end(&mut self) { + self.cursor.move_to(usize::MAX); } - /// 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(); - - Self { graphemes } - } - - /// Converts the [`Value`] into a `String`. - /// - /// [`Value`]: struct.Value.html - pub fn to_string(&self) -> String { - self.graphemes.concat() - } - - /// Inserts a new `char` at the given grapheme `index`. - pub fn insert(&mut self, index: usize, c: char) { - self.graphemes.insert(index, c.to_string()); - - self.graphemes = - UnicodeSegmentation::graphemes(&self.to_string() as &str, true) - .map(String::from) - .collect(); - } - - /// Inserts a bunch of graphemes at the given grapheme `index`. - pub fn insert_many(&mut self, index: usize, mut value: Value) { - let _ = self - .graphemes - .splice(index..index, value.graphemes.drain(..)); - } - - /// Removes the grapheme at the given `index`. - /// - /// [`Value`]: struct.Value.html - pub fn remove(&mut self, index: usize) { - let _ = self.graphemes.remove(index); - } - - /// 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("•")) - .take(self.graphemes.len()) - .collect(), - } + /// Moves the [`Cursor`] of the [`TextInput`] to an arbitrary location. + pub fn move_cursor_to(&mut self, position: usize) { + self.cursor.move_to(position); } } // TODO: Reduce allocations fn find_cursor_position<Renderer: self::Renderer>( renderer: &Renderer, - target: f32, value: &Value, + font: Renderer::Font, size: u16, + target: f32, start: usize, end: usize, - font: Font, ) -> usize { if start >= end { if start == 0 { @@ -764,46 +791,34 @@ fn find_cursor_position<Renderer: self::Renderer>( if width > target { find_cursor_position( renderer, - target, value, + font, size, + target, start, start + index, - font, ) } else { find_cursor_position( renderer, - target, value, + font, size, + target, start + index + 1, end, - font, ) } } mod platform { - use crate::input::keyboard; + use crate::keyboard; - pub fn is_jump_modifier_pressed( - modifiers: keyboard::ModifiersState, - ) -> bool { + pub fn is_jump_modifier_pressed(modifiers: keyboard::Modifiers) -> bool { if cfg!(target_os = "macos") { modifiers.alt } else { modifiers.control } } - - pub fn is_copy_paste_modifier_pressed( - modifiers: keyboard::ModifiersState, - ) -> bool { - if cfg!(target_os = "macos") { - modifiers.logo - } else { - modifiers.control - } - } } diff --git a/native/src/widget/text_input/cursor.rs b/native/src/widget/text_input/cursor.rs new file mode 100644 index 00000000..e630e293 --- /dev/null +++ b/native/src/widget/text_input/cursor.rs @@ -0,0 +1,186 @@ +//! Track the cursor of a text input. +use crate::widget::text_input::Value; + +/// The cursor of a text input. +#[derive(Debug, Copy, Clone)] +pub struct Cursor { + state: State, +} + +/// The state of a [`Cursor`]. +#[derive(Debug, Copy, Clone)] +pub enum State { + /// Cursor without a selection + Index(usize), + + /// Cursor selecting a range of text + Selection { + /// The start of the selection + start: usize, + /// The end of the selection + end: usize, + }, +} + +impl Default for Cursor { + fn default() -> Self { + Cursor { + state: State::Index(0), + } + } +} + +impl Cursor { + /// Returns the [`State`] of the [`Cursor`]. + pub fn state(&self, value: &Value) -> State { + match self.state { + State::Index(index) => State::Index(index.min(value.len())), + State::Selection { start, end } => { + let start = start.min(value.len()); + let end = end.min(value.len()); + + if start == end { + State::Index(start) + } else { + State::Selection { start, end } + } + } + } + } + + pub(crate) fn move_to(&mut self, position: usize) { + self.state = State::Index(position); + } + + pub(crate) fn move_right(&mut self, value: &Value) { + self.move_right_by_amount(value, 1) + } + + pub(crate) fn move_right_by_words(&mut self, value: &Value) { + self.move_to(value.next_end_of_word(self.right(value))) + } + + pub(crate) fn move_right_by_amount( + &mut self, + value: &Value, + amount: usize, + ) { + match self.state(value) { + State::Index(index) => { + self.move_to(index.saturating_add(amount).min(value.len())) + } + State::Selection { start, end } => self.move_to(end.max(start)), + } + } + + pub(crate) fn move_left(&mut self, value: &Value) { + match self.state(value) { + State::Index(index) if index > 0 => self.move_to(index - 1), + State::Selection { start, end } => self.move_to(start.min(end)), + _ => self.move_to(0), + } + } + + pub(crate) fn move_left_by_words(&mut self, value: &Value) { + self.move_to(value.previous_start_of_word(self.left(value))); + } + + pub(crate) fn select_range(&mut self, start: usize, end: usize) { + if start == end { + self.state = State::Index(start); + } else { + self.state = State::Selection { start, end }; + } + } + + pub(crate) fn select_left(&mut self, value: &Value) { + match self.state(value) { + State::Index(index) if index > 0 => { + self.select_range(index, index - 1) + } + State::Selection { start, end } if end > 0 => { + self.select_range(start, end - 1) + } + _ => (), + } + } + + pub(crate) fn select_right(&mut self, value: &Value) { + match self.state(value) { + State::Index(index) if index < value.len() => { + self.select_range(index, index + 1) + } + State::Selection { start, end } if end < value.len() => { + self.select_range(start, end + 1) + } + _ => (), + } + } + + pub(crate) fn select_left_by_words(&mut self, value: &Value) { + match self.state(value) { + State::Index(index) => { + self.select_range(index, value.previous_start_of_word(index)) + } + State::Selection { start, end } => { + self.select_range(start, value.previous_start_of_word(end)) + } + } + } + + pub(crate) fn select_right_by_words(&mut self, value: &Value) { + match self.state(value) { + State::Index(index) => { + self.select_range(index, value.next_end_of_word(index)) + } + State::Selection { start, end } => { + self.select_range(start, value.next_end_of_word(end)) + } + } + } + + pub(crate) fn select_all(&mut self, value: &Value) { + self.select_range(0, value.len()); + } + + pub(crate) fn start(&self, value: &Value) -> usize { + let start = match self.state { + State::Index(index) => index, + State::Selection { start, .. } => start, + }; + + start.min(value.len()) + } + + pub(crate) fn end(&self, value: &Value) -> usize { + let end = match self.state { + State::Index(index) => index, + State::Selection { end, .. } => end, + }; + + 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, + State::Selection { start, end } => start.min(end), + } + } + + fn right(&self, value: &Value) -> usize { + match self.state(value) { + State::Index(index) => index, + State::Selection { start, end } => start.max(end), + } + } +} diff --git a/native/src/widget/text_input/editor.rs b/native/src/widget/text_input/editor.rs new file mode 100644 index 00000000..20e42567 --- /dev/null +++ b/native/src/widget/text_input/editor.rs @@ -0,0 +1,77 @@ +use crate::text_input::{Cursor, Value}; + +pub struct Editor<'a> { + value: &'a mut Value, + cursor: &'a mut Cursor, +} + +impl<'a> Editor<'a> { + pub fn new(value: &'a mut Value, cursor: &'a mut Cursor) -> Editor<'a> { + Editor { value, cursor } + } + + pub fn contents(&self) -> String { + self.value.to_string() + } + + pub fn insert(&mut self, character: char) { + match self.cursor.selection(self.value) { + Some((left, right)) => { + self.cursor.move_left(self.value); + self.value.remove_many(left, right); + } + _ => (), + } + + self.value.insert(self.cursor.end(self.value), character); + self.cursor.move_right(self.value); + } + + pub fn paste(&mut self, content: Value) { + let length = content.len(); + + match self.cursor.selection(self.value) { + Some((left, right)) => { + self.cursor.move_left(self.value); + self.value.remove_many(left, right); + } + _ => (), + } + + self.value.insert_many(self.cursor.end(self.value), content); + + self.cursor.move_right_by_amount(self.value, length); + } + + pub fn backspace(&mut self) { + match self.cursor.selection(self.value) { + Some((start, end)) => { + self.cursor.move_left(self.value); + self.value.remove_many(start, end); + } + None => { + let start = self.cursor.start(self.value); + + if start > 0 { + self.cursor.move_left(self.value); + self.value.remove(start - 1); + } + } + } + } + + pub fn delete(&mut self) { + match self.cursor.selection(self.value) { + Some(_) => { + self.backspace(); + } + None => { + let end = self.cursor.end(self.value); + + if end < self.value.len() { + self.value.remove(end); + } + } + } + } +} diff --git a/native/src/widget/text_input/value.rs b/native/src/widget/text_input/value.rs new file mode 100644 index 00000000..86be2790 --- /dev/null +++ b/native/src/widget/text_input/value.rs @@ -0,0 +1,125 @@ +use unicode_segmentation::UnicodeSegmentation; + +/// The value of a [`TextInput`]. +/// +/// [`TextInput`]: crate::widget::TextInput +// TODO: Reduce allocations, cache results (?) +#[derive(Debug, Clone)] +pub struct Value { + graphemes: Vec<String>, +} + +impl Value { + /// Creates a new [`Value`] from a string slice. + pub fn new(string: &str) -> Self { + let graphemes = UnicodeSegmentation::graphemes(string, true) + .map(String::from) + .collect(); + + Self { graphemes } + } + + /// Returns whether the [`Value`] is empty or not. + /// + /// 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`. + pub fn previous_start_of_word(&self, index: usize) -> usize { + let previous_string = + &self.graphemes[..index.min(self.graphemes.len())].concat(); + + UnicodeSegmentation::split_word_bound_indices(&previous_string as &str) + .filter(|(_, word)| !word.trim_start().is_empty()) + .next_back() + .map(|(i, previous_word)| { + index + - UnicodeSegmentation::graphemes(previous_word, true) + .count() + - UnicodeSegmentation::graphemes( + &previous_string[i + previous_word.len()..] as &str, + true, + ) + .count() + }) + .unwrap_or(0) + } + + /// Returns the position of the next end of a word from the given grapheme + /// `index`. + pub fn next_end_of_word(&self, index: usize) -> usize { + let next_string = &self.graphemes[index..].concat(); + + UnicodeSegmentation::split_word_bound_indices(&next_string as &str) + .filter(|(_, word)| !word.trim_start().is_empty()) + .next() + .map(|(i, next_word)| { + index + + UnicodeSegmentation::graphemes(next_word, true).count() + + UnicodeSegmentation::graphemes( + &next_string[..i] as &str, + true, + ) + .count() + }) + .unwrap_or(self.len()) + } + + /// Returns a new [`Value`] containing the graphemes until the given + /// `index`. + pub fn until(&self, index: usize) -> Self { + let graphemes = self.graphemes[..index.min(self.len())].to_vec(); + + Self { graphemes } + } + + /// Converts the [`Value`] into a `String`. + pub fn to_string(&self) -> String { + self.graphemes.concat() + } + + /// Inserts a new `char` at the given grapheme `index`. + pub fn insert(&mut self, index: usize, c: char) { + self.graphemes.insert(index, c.to_string()); + + self.graphemes = + UnicodeSegmentation::graphemes(&self.to_string() as &str, true) + .map(String::from) + .collect(); + } + + /// Inserts a bunch of graphemes at the given grapheme `index`. + pub fn insert_many(&mut self, index: usize, mut value: Value) { + let _ = self + .graphemes + .splice(index..index, value.graphemes.drain(..)); + } + + /// Removes the grapheme at the given `index`. + pub fn remove(&mut self, index: usize) { + let _ = self.graphemes.remove(index); + } + + /// Removes the graphemes from `start` to `end`. + pub fn remove_many(&mut self, start: usize, end: usize) { + let _ = self.graphemes.splice(start..end, std::iter::empty()); + } + + /// Returns a new [`Value`] with all its graphemes replaced with the + /// dot ('•') character. + pub fn secure(&self) -> Self { + Self { + graphemes: std::iter::repeat(String::from("•")) + .take(self.graphemes.len()) + .collect(), + } + } +} |