diff options
Diffstat (limited to 'native')
71 files changed, 5688 insertions, 1747 deletions
diff --git a/native/CHANGELOG.md b/native/CHANGELOG.md deleted file mode 100644 index df8852b7..00000000 --- a/native/CHANGELOG.md +++ /dev/null @@ -1,38 +0,0 @@ -# Changelog -All notable changes to this project will be documented in this file. - -The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), -and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - -## [Unreleased] -### Added -- `image::Handle` type with `from_path` and `from_memory` methods. [#90] -- `image::Data` enum representing different kinds of image data. [#90] -- `text_input::Renderer::measure_value` required method to measure the width of a `TextInput` value. [#108] -- Click-based cursor positioning for `TextInput`. [#108] -- `Home` and `End` keys support for `TextInput`. [#108] -- `Ctrl+Left` and `Ctrl+Right` cursor word jump for `TextInput`. [#108] -- `keyboard::ModifiersState` struct which contains the state of the keyboard modifiers. [#108] -- `TextInput::password` method to enable secure password input mode. [#113] -- `Button::height` and `Button::min_height` methods to control the height of a button. - -### Changed -- `Image::new` takes an `Into<image::Handle>` now instead of an `Into<String>`. [#90] -- `Button::background` takes an `Into<Background>` now instead of a `Background`. -- `keyboard::Event::Input` now contains key modifiers state. [#108] - -### Fixed -- `Image` widget not keeping aspect ratio consistently. [#90] -- `TextInput` not taking grapheme clusters into account. [#108] - -[#90]: https://github.com/hecrj/iced/pull/90 -[#108]: https://github.com/hecrj/iced/pull/108 -[#113]: https://github.com/hecrj/iced/pull/113 - - -## [0.1.0] - 2019-11-25 -### Added -- First release! :tada: - -[Unreleased]: https://github.com/hecrj/iced/compare/native-0.1.0...HEAD -[0.1.0]: https://github.com/hecrj/iced/releases/tag/native-0.1.0 diff --git a/native/Cargo.toml b/native/Cargo.toml index 6276535e..2c99638a 100644 --- a/native/Cargo.toml +++ b/native/Cargo.toml @@ -1,22 +1,25 @@ [package] name = "iced_native" -version = "0.1.0" +version = "0.3.0" authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"] edition = "2018" description = "A renderer-agnostic library for native GUIs" license = "MIT" repository = "https://github.com/hecrj/iced" +[features] +debug = [] + [dependencies] twox-hash = "1.5" -raw-window-handle = "0.3" unicode-segmentation = "1.6" +num-traits = "0.2" [dependencies.iced_core] -version = "0.1.0" +version = "0.3" path = "../core" [dependencies.iced_futures] -version = "0.1.0-alpha" +version = "0.2" path = "../futures" features = ["thread-pool"] diff --git a/native/README.md b/native/README.md index 59ec5424..6323dd4f 100644 --- a/native/README.md +++ b/native/README.md @@ -14,7 +14,9 @@ To achieve this, it introduces a bunch of reusable interfaces: - A bunch of `Renderer` traits, meant to keep the crate renderer-agnostic. - A `Windowed` trait, leveraging [`raw-window-handle`], which can be implemented by graphical renderers that target _windows_. Window-based shells (like [`iced_winit`]) can use this trait to stay renderer-agnostic. - +<p align="center"> + <img alt="The native target" src="../docs/graphs/native.png" width="80%"> +</p> [documentation]: https://docs.rs/iced_native [`iced_core`]: ../core @@ -26,7 +28,7 @@ To achieve this, it introduces a bunch of reusable interfaces: Add `iced_native` as a dependency in your `Cargo.toml`: ```toml -iced_native = "0.1.0" +iced_native = "0.3" ``` __Iced moves fast and the `master` branch can contain breaking changes!__ If diff --git a/native/src/clipboard.rs b/native/src/clipboard.rs index 4c574590..ecdccabf 100644 --- a/native/src/clipboard.rs +++ b/native/src/clipboard.rs @@ -2,7 +2,5 @@ /// applications. pub trait Clipboard { /// Returns the current content of the [`Clipboard`] as text. - /// - /// [`Clipboard`]: trait.Clipboard.html fn content(&self) -> Option<String>; } diff --git a/native/src/debug/basic.rs b/native/src/debug/basic.rs new file mode 100644 index 00000000..a42f66ea --- /dev/null +++ b/native/src/debug/basic.rs @@ -0,0 +1,218 @@ +#![allow(missing_docs)] +use std::{collections::VecDeque, time}; + +/// A bunch of time measurements for debugging purposes. +#[derive(Debug)] +pub struct Debug { + is_enabled: bool, + + startup_start: time::Instant, + startup_duration: time::Duration, + + update_start: time::Instant, + update_durations: TimeBuffer, + + view_start: time::Instant, + view_durations: TimeBuffer, + + layout_start: time::Instant, + layout_durations: TimeBuffer, + + event_start: time::Instant, + event_durations: TimeBuffer, + + draw_start: time::Instant, + draw_durations: TimeBuffer, + + render_start: time::Instant, + render_durations: TimeBuffer, + + message_count: usize, + last_messages: VecDeque<String>, +} + +impl Debug { + /// Creates a new [`struct@Debug`]. + pub fn new() -> Self { + let now = time::Instant::now(); + + Self { + is_enabled: false, + startup_start: now, + startup_duration: time::Duration::from_secs(0), + + update_start: now, + update_durations: TimeBuffer::new(200), + + view_start: now, + view_durations: TimeBuffer::new(200), + + layout_start: now, + layout_durations: TimeBuffer::new(200), + + event_start: now, + event_durations: TimeBuffer::new(200), + + draw_start: now, + draw_durations: TimeBuffer::new(200), + + render_start: now, + render_durations: TimeBuffer::new(50), + + message_count: 0, + last_messages: VecDeque::new(), + } + } + + pub fn toggle(&mut self) { + self.is_enabled = !self.is_enabled; + } + + pub fn startup_started(&mut self) { + self.startup_start = time::Instant::now(); + } + + pub fn startup_finished(&mut self) { + self.startup_duration = time::Instant::now() - self.startup_start; + } + + pub fn update_started(&mut self) { + self.update_start = time::Instant::now(); + } + + pub fn update_finished(&mut self) { + self.update_durations + .push(time::Instant::now() - self.update_start); + } + + pub fn view_started(&mut self) { + self.view_start = time::Instant::now(); + } + + pub fn view_finished(&mut self) { + self.view_durations + .push(time::Instant::now() - self.view_start); + } + + pub fn layout_started(&mut self) { + self.layout_start = time::Instant::now(); + } + + pub fn layout_finished(&mut self) { + self.layout_durations + .push(time::Instant::now() - self.layout_start); + } + + pub fn event_processing_started(&mut self) { + self.event_start = time::Instant::now(); + } + + pub fn event_processing_finished(&mut self) { + self.event_durations + .push(time::Instant::now() - self.event_start); + } + + pub fn draw_started(&mut self) { + self.draw_start = time::Instant::now(); + } + + pub fn draw_finished(&mut self) { + self.draw_durations + .push(time::Instant::now() - self.draw_start); + } + + pub fn render_started(&mut self) { + self.render_start = time::Instant::now(); + } + + pub fn render_finished(&mut self) { + self.render_durations + .push(time::Instant::now() - self.render_start); + } + + pub fn log_message<Message: std::fmt::Debug>(&mut self, message: &Message) { + self.last_messages.push_back(format!("{:?}", message)); + + if self.last_messages.len() > 10 { + let _ = self.last_messages.pop_front(); + } + + self.message_count += 1; + } + + pub fn overlay(&self) -> Vec<String> { + if !self.is_enabled { + return Vec::new(); + } + + let mut lines = Vec::new(); + + fn key_value<T: std::fmt::Debug>(key: &str, value: T) -> String { + format!("{} {:?}", key, value) + } + + lines.push(format!( + "{} {} - {}", + env!("CARGO_PKG_NAME"), + env!("CARGO_PKG_VERSION"), + env!("CARGO_PKG_REPOSITORY"), + )); + lines.push(key_value("Startup:", self.startup_duration)); + lines.push(key_value("Update:", self.update_durations.average())); + lines.push(key_value("View:", self.view_durations.average())); + lines.push(key_value("Layout:", self.layout_durations.average())); + lines.push(key_value( + "Event processing:", + self.event_durations.average(), + )); + lines.push(key_value( + "Primitive generation:", + self.draw_durations.average(), + )); + lines.push(key_value("Render:", self.render_durations.average())); + lines.push(key_value("Message count:", self.message_count)); + lines.push(String::from("Last messages:")); + lines.extend(self.last_messages.iter().map(|msg| { + if msg.len() <= 100 { + format!(" {}", msg) + } else { + format!(" {:.100}...", msg) + } + })); + + lines + } +} + +#[derive(Debug)] +struct TimeBuffer { + head: usize, + size: usize, + contents: Vec<time::Duration>, +} + +impl TimeBuffer { + fn new(capacity: usize) -> TimeBuffer { + TimeBuffer { + head: 0, + size: 0, + contents: vec![time::Duration::from_secs(0); capacity], + } + } + + fn push(&mut self, duration: time::Duration) { + self.head = (self.head + 1) % self.contents.len(); + self.contents[self.head] = duration; + self.size = (self.size + 1).min(self.contents.len()); + } + + fn average(&self) -> time::Duration { + let sum: time::Duration = if self.size == self.contents.len() { + self.contents[..].iter().sum() + } else { + self.contents[..self.size].iter().sum() + }; + + sum / self.size.max(1) as u32 + } +} diff --git a/native/src/debug/null.rs b/native/src/debug/null.rs new file mode 100644 index 00000000..60e6122d --- /dev/null +++ b/native/src/debug/null.rs @@ -0,0 +1,47 @@ +#![allow(missing_docs)] +#[derive(Debug)] +pub struct Debug; + +impl Debug { + pub fn new() -> Self { + Self + } + + pub fn startup_started(&mut self) {} + + pub fn startup_finished(&mut self) {} + + pub fn update_started(&mut self) {} + + pub fn update_finished(&mut self) {} + + pub fn view_started(&mut self) {} + + pub fn view_finished(&mut self) {} + + pub fn layout_started(&mut self) {} + + pub fn layout_finished(&mut self) {} + + pub fn event_processing_started(&mut self) {} + + pub fn event_processing_finished(&mut self) {} + + pub fn draw_started(&mut self) {} + + pub fn draw_finished(&mut self) {} + + pub fn render_started(&mut self) {} + + pub fn render_finished(&mut self) {} + + pub fn log_message<Message: std::fmt::Debug>( + &mut self, + _message: &Message, + ) { + } + + pub fn overlay(&self) -> Vec<String> { + Vec::new() + } +} diff --git a/native/src/element.rs b/native/src/element.rs index 276f7614..d6e9639a 100644 --- a/native/src/element.rs +++ b/native/src/element.rs @@ -1,5 +1,8 @@ +use crate::event::{self, Event}; +use crate::layout; +use crate::overlay; use crate::{ - layout, Clipboard, Color, Event, Hasher, Layout, Length, Point, Widget, + Clipboard, Color, Hasher, Layout, Length, Point, Rectangle, Widget, }; /// A generic [`Widget`]. @@ -11,8 +14,6 @@ use crate::{ /// to turn it into an [`Element`]. /// /// [built-in widget]: widget/index.html#built-in-widgets -/// [`Widget`]: widget/trait.Widget.html -/// [`Element`]: struct.Element.html #[allow(missing_debug_implementations)] pub struct Element<'a, Message, Renderer> { pub(crate) widget: Box<dyn Widget<Message, Renderer> + 'a>, @@ -22,10 +23,7 @@ impl<'a, Message, Renderer> Element<'a, Message, Renderer> where Renderer: crate::Renderer, { - /// Create a new [`Element`] containing the given [`Widget`]. - /// - /// [`Element`]: struct.Element.html - /// [`Widget`]: widget/trait.Widget.html + /// Creates a new [`Element`] containing the given [`Widget`]. pub fn new( widget: impl Widget<Message, Renderer> + 'a, ) -> Element<'a, Message, Renderer> { @@ -39,8 +37,6 @@ where /// This method is useful when you want to decouple different parts of your /// UI and make them __composable__. /// - /// [`Element`]: struct.Element.html - /// /// # Example /// Imagine we want to use [our counter](index.html#usage). But instead of /// showing a single counter, we want to display many of them. We can reuse @@ -81,7 +77,7 @@ where /// /// ``` /// # mod counter { - /// # use iced_native::{text, Text}; + /// # type Text = iced_native::Text<iced_native::renderer::Null>; /// # /// # #[derive(Debug, Clone, Copy)] /// # pub enum Message {} @@ -186,8 +182,7 @@ where /// The [`Renderer`] will explain the layout of the [`Element`] graphically. /// This can be very useful for debugging your layout! /// - /// [`Element`]: struct.Element.html - /// [`Renderer`]: trait.Renderer.html + /// [`Renderer`]: crate::Renderer pub fn explain<C: Into<Color>>( self, color: C, @@ -202,23 +197,18 @@ where } /// Returns the width of the [`Element`]. - /// - /// [`Element`]: struct.Element.html pub fn width(&self) -> Length { self.widget.width() } /// Returns the height of the [`Element`]. - /// - /// [`Element`]: struct.Element.html pub fn height(&self) -> Length { self.widget.height() } /// Computes the layout of the [`Element`] in the given [`Limits`]. /// - /// [`Element`]: struct.Element.html - /// [`Limits`]: layout/struct.Limits.html + /// [`Limits`]: layout::Limits pub fn layout( &self, renderer: &Renderer, @@ -227,27 +217,51 @@ where self.widget.layout(renderer, limits) } + /// Processes a runtime [`Event`]. + pub fn on_event( + &mut self, + event: Event, + layout: Layout<'_>, + cursor_position: Point, + messages: &mut Vec<Message>, + renderer: &Renderer, + clipboard: Option<&dyn Clipboard>, + ) -> event::Status { + self.widget.on_event( + event, + layout, + cursor_position, + messages, + renderer, + clipboard, + ) + } + /// Draws the [`Element`] and its children using the given [`Layout`]. - /// - /// [`Element`]: struct.Element.html - /// [`Layout`]: layout/struct.Layout.html pub fn draw( &self, renderer: &mut Renderer, defaults: &Renderer::Defaults, layout: Layout<'_>, cursor_position: Point, + viewport: &Rectangle, ) -> Renderer::Output { self.widget - .draw(renderer, defaults, layout, cursor_position) + .draw(renderer, defaults, layout, cursor_position, viewport) } /// Computes the _layout_ hash of the [`Element`]. - /// - /// [`Element`]: struct.Element.html pub fn hash_layout(&self, state: &mut Hasher) { self.widget.hash_layout(state); } + + /// Returns the overlay of the [`Element`], if there is any. + pub fn overlay<'b>( + &'b mut self, + layout: Layout<'_>, + ) -> Option<overlay::Element<'b, Message, Renderer>> { + self.widget.overlay(layout) + } } struct Map<'a, A, B, Renderer> { @@ -272,7 +286,9 @@ impl<'a, A, B, Renderer> Map<'a, A, B, Renderer> { impl<'a, A, B, Renderer> Widget<B, Renderer> for Map<'a, A, B, Renderer> where - Renderer: crate::Renderer, + Renderer: crate::Renderer + 'a, + A: 'static, + B: 'static, { fn width(&self) -> Length { self.widget.width() @@ -298,10 +314,10 @@ where messages: &mut Vec<B>, renderer: &Renderer, clipboard: Option<&dyn Clipboard>, - ) { + ) -> event::Status { let mut original_messages = Vec::new(); - self.widget.on_event( + let status = self.widget.on_event( event, layout, cursor_position, @@ -313,6 +329,8 @@ where original_messages .drain(..) .for_each(|message| messages.push((self.mapper)(message))); + + status } fn draw( @@ -321,14 +339,26 @@ where defaults: &Renderer::Defaults, layout: Layout<'_>, cursor_position: Point, + viewport: &Rectangle, ) -> Renderer::Output { self.widget - .draw(renderer, defaults, layout, cursor_position) + .draw(renderer, defaults, layout, cursor_position, viewport) } fn hash_layout(&self, state: &mut Hasher) { self.widget.hash_layout(state); } + + fn overlay( + &mut self, + layout: Layout<'_>, + ) -> Option<overlay::Element<'_, B, Renderer>> { + let mapper = &self.mapper; + + self.widget + .overlay(layout) + .map(move |overlay| overlay.map(mapper)) + } } struct Explain<'a, Message, Renderer: crate::Renderer> { @@ -374,7 +404,7 @@ where messages: &mut Vec<Message>, renderer: &Renderer, clipboard: Option<&dyn Clipboard>, - ) { + ) -> event::Status { self.element.widget.on_event( event, layout, @@ -391,12 +421,14 @@ where defaults: &Renderer::Defaults, layout: Layout<'_>, cursor_position: Point, + viewport: &Rectangle, ) -> Renderer::Output { renderer.explain( defaults, self.element.widget.as_ref(), layout, cursor_position, + viewport, self.color, ) } @@ -404,4 +436,11 @@ where fn hash_layout(&self, state: &mut Hasher) { self.element.widget.hash_layout(state); } + + fn overlay( + &mut self, + layout: Layout<'_>, + ) -> Option<overlay::Element<'_, Message, Renderer>> { + self.element.overlay(layout) + } } diff --git a/native/src/event.rs b/native/src/event.rs index 99a8e880..f3c260c0 100644 --- a/native/src/event.rs +++ b/native/src/event.rs @@ -1,7 +1,8 @@ -use crate::{ - input::{keyboard, mouse, Touch}, - window, -}; +//! Handle events of a user interface. +use crate::keyboard; +use crate::mouse; +use crate::touch; +use crate::window; /// A user interface event. /// @@ -21,5 +22,36 @@ pub enum Event { Window(window::Event), /// A touch event - Touch(Touch), + Touch(touch::Touch), +} + +/// The status of an [`Event`] after being processed. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Status { + /// The [`Event`] was **NOT** handled by any widget. + Ignored, + + /// The [`Event`] was handled and processed by a widget. + Captured, +} + +impl Status { + /// Merges two [`Status`] into one. + /// + /// `Captured` takes precedence over `Ignored`: + /// + /// ``` + /// use iced_native::event::Status; + /// + /// assert_eq!(Status::Ignored.merge(Status::Ignored), Status::Ignored); + /// assert_eq!(Status::Ignored.merge(Status::Captured), Status::Captured); + /// assert_eq!(Status::Captured.merge(Status::Ignored), Status::Captured); + /// assert_eq!(Status::Captured.merge(Status::Captured), Status::Captured); + /// ``` + pub fn merge(self, b: Self) -> Self { + match self { + Status::Ignored => b, + Status::Captured => Status::Captured, + } + } } diff --git a/native/src/input.rs b/native/src/input.rs deleted file mode 100644 index 514a98ea..00000000 --- a/native/src/input.rs +++ /dev/null @@ -1,9 +0,0 @@ -//! Map your system events into input events that the runtime can understand. -pub mod keyboard; -pub mod mouse; -pub mod touch; - -mod button_state; - -pub use button_state::ButtonState; -pub use touch::Touch; diff --git a/native/src/input/button_state.rs b/native/src/input/button_state.rs deleted file mode 100644 index 988043ba..00000000 --- a/native/src/input/button_state.rs +++ /dev/null @@ -1,9 +0,0 @@ -/// The state of a button. -#[derive(Debug, Hash, Ord, PartialOrd, PartialEq, Eq, Clone, Copy)] -pub enum ButtonState { - /// The button is pressed. - Pressed, - - /// The button is __not__ pressed. - Released, -} diff --git a/native/src/input/keyboard.rs b/native/src/input/keyboard.rs deleted file mode 100644 index 432e75ba..00000000 --- a/native/src/input/keyboard.rs +++ /dev/null @@ -1,8 +0,0 @@ -//! Build keyboard events. -mod event; -mod key_code; -mod modifiers_state; - -pub use event::Event; -pub use key_code::KeyCode; -pub use modifiers_state::ModifiersState; diff --git a/native/src/input/keyboard/event.rs b/native/src/input/keyboard/event.rs deleted file mode 100644 index 862f30c4..00000000 --- a/native/src/input/keyboard/event.rs +++ /dev/null @@ -1,26 +0,0 @@ -use super::{KeyCode, ModifiersState}; -use crate::input::ButtonState; - -/// A keyboard event. -/// -/// _**Note:** This type is largely incomplete! If you need to track -/// additional events, feel free to [open an issue] and share your use case!_ -/// -/// [open an issue]: https://github.com/hecrj/iced/issues -#[derive(Debug, Clone, Copy, PartialEq)] -pub enum Event { - /// A keyboard key was pressed or released. - Input { - /// The state of the key - state: ButtonState, - - /// The key identifier - key_code: KeyCode, - - /// The state of the modifier keys - modifiers: ModifiersState, - }, - - /// A unicode character was received. - CharacterReceived(char), -} diff --git a/native/src/input/keyboard/key_code.rs b/native/src/input/keyboard/key_code.rs deleted file mode 100644 index 26020a57..00000000 --- a/native/src/input/keyboard/key_code.rs +++ /dev/null @@ -1,198 +0,0 @@ -/// The symbolic name of a keyboard key. -/// -/// This is mostly the `KeyCode` type found in [`winit`]. -/// -/// [`winit`]: https://docs.rs/winit/0.20.0-alpha3/winit/ -#[derive(Debug, Hash, Ord, PartialOrd, PartialEq, Eq, Clone, Copy)] -#[repr(u32)] -#[allow(missing_docs)] -pub enum KeyCode { - /// The '1' key over the letters. - Key1, - /// The '2' key over the letters. - Key2, - /// The '3' key over the letters. - Key3, - /// The '4' key over the letters. - Key4, - /// The '5' key over the letters. - Key5, - /// The '6' key over the letters. - Key6, - /// The '7' key over the letters. - Key7, - /// The '8' key over the letters. - Key8, - /// The '9' key over the letters. - Key9, - /// The '0' key over the 'O' and 'P' keys. - Key0, - - A, - B, - C, - D, - E, - F, - G, - H, - I, - J, - K, - L, - M, - N, - O, - P, - Q, - R, - S, - T, - U, - V, - W, - X, - Y, - Z, - - /// The Escape key, next to F1 - Escape, - - F1, - F2, - F3, - F4, - F5, - F6, - F7, - F8, - F9, - F10, - F11, - F12, - F13, - F14, - F15, - F16, - F17, - F18, - F19, - F20, - F21, - F22, - F23, - F24, - - /// Print Screen/SysRq - Snapshot, - /// Scroll Lock - Scroll, - /// Pause/Break key, next to Scroll lock - Pause, - - /// `Insert`, next to Backspace - Insert, - Home, - Delete, - End, - PageDown, - PageUp, - - Left, - Up, - Right, - Down, - - Backspace, - Enter, - Space, - - /// The "Compose" key on Linux - Compose, - - Caret, - - Numlock, - Numpad0, - Numpad1, - Numpad2, - Numpad3, - Numpad4, - Numpad5, - Numpad6, - Numpad7, - Numpad8, - Numpad9, - - AbntC1, - AbntC2, - Add, - Apostrophe, - Apps, - At, - Ax, - Backslash, - Calculator, - Capital, - Colon, - Comma, - Convert, - Decimal, - Divide, - Equals, - Grave, - Kana, - Kanji, - LAlt, - LBracket, - LControl, - LShift, - LWin, - Mail, - MediaSelect, - MediaStop, - Minus, - Multiply, - Mute, - MyComputer, - NavigateForward, // also called "Prior" - NavigateBackward, // also called "Next" - NextTrack, - NoConvert, - NumpadComma, - NumpadEnter, - NumpadEquals, - OEM102, - Period, - PlayPause, - Power, - PrevTrack, - RAlt, - RBracket, - RControl, - RShift, - RWin, - Semicolon, - Slash, - Sleep, - Stop, - Subtract, - Sysrq, - Tab, - Underline, - Unlabeled, - VolumeDown, - VolumeUp, - Wake, - WebBack, - WebFavorites, - WebForward, - WebHome, - WebRefresh, - WebSearch, - WebStop, - Yen, - Copy, - Paste, - Cut, -} diff --git a/native/src/input/keyboard/modifiers_state.rs b/native/src/input/keyboard/modifiers_state.rs deleted file mode 100644 index 4e3794b3..00000000 --- a/native/src/input/keyboard/modifiers_state.rs +++ /dev/null @@ -1,15 +0,0 @@ -/// The current state of the keyboard modifiers. -#[derive(Debug, Clone, Copy, PartialEq)] -pub struct ModifiersState { - /// Whether a shift key is pressed - pub shift: bool, - - /// Whether a control key is pressed - pub control: bool, - - /// Whether an alt key is pressed - pub alt: bool, - - /// Whether a logo key is pressed (e.g. windows key, command key...) - pub logo: bool, -} diff --git a/native/src/input/mouse.rs b/native/src/input/mouse.rs deleted file mode 100644 index 69dc6b4c..00000000 --- a/native/src/input/mouse.rs +++ /dev/null @@ -1,6 +0,0 @@ -//! Build mouse events. -mod button; -mod event; - -pub use button::Button; -pub use event::{Event, ScrollDelta}; diff --git a/native/src/input/mouse/button.rs b/native/src/input/mouse/button.rs deleted file mode 100644 index aeb8a55d..00000000 --- a/native/src/input/mouse/button.rs +++ /dev/null @@ -1,15 +0,0 @@ -/// The button of a mouse. -#[derive(Debug, Hash, PartialEq, Eq, Clone, Copy)] -pub enum Button { - /// The left mouse button. - Left, - - /// The right mouse button. - Right, - - /// The middle (wheel) button. - Middle, - - /// Some other button. - Other(u8), -} diff --git a/native/src/input/mouse/event.rs b/native/src/input/mouse/event.rs deleted file mode 100644 index 5068d634..00000000 --- a/native/src/input/mouse/event.rs +++ /dev/null @@ -1,58 +0,0 @@ -use super::Button; -use crate::{input::ButtonState, Point}; - -/// A mouse event. -/// -/// _**Note:** This type is largely incomplete! If you need to track -/// additional events, feel free to [open an issue] and share your use case!_ -/// -/// [open an issue]: https://github.com/hecrj/iced/issues -#[derive(Debug, Clone, Copy, PartialEq)] -pub enum Event { - /// The mouse cursor entered the window. - CursorEntered, - - /// The mouse cursor left the window. - CursorLeft, - - /// The mouse cursor was moved - CursorMoved { - /// The new position of the mouse cursor - position: Point, - }, - - /// A mouse button was pressed or released. - Input { - /// The state of the button - state: ButtonState, - - /// The button identifier - button: Button, - }, - - /// The mouse wheel was scrolled. - WheelScrolled { - /// The scroll movement. - delta: ScrollDelta, - }, -} - -/// A scroll movement. -#[derive(Debug, Clone, Copy, PartialEq)] -pub enum ScrollDelta { - /// A line-based scroll movement - Lines { - /// The number of horizontal lines scrolled - x: f32, - - /// The number of vertical lines scrolled - y: f32, - }, - /// A pixel-based scroll movement - Pixels { - /// The number of horizontal pixels scrolled - x: f32, - /// The number of vertical pixels scrolled - y: f32, - }, -} diff --git a/native/src/keyboard.rs b/native/src/keyboard.rs new file mode 100644 index 00000000..012538e3 --- /dev/null +++ b/native/src/keyboard.rs @@ -0,0 +1,2 @@ +//! Track keyboard events. +pub use iced_core::keyboard::*; diff --git a/native/src/layout.rs b/native/src/layout.rs index 4a3ab94a..6d144902 100644 --- a/native/src/layout.rs +++ b/native/src/layout.rs @@ -12,8 +12,6 @@ pub use node::Node; use crate::{Point, Rectangle, Vector}; /// The bounds of a [`Node`] and its children, using absolute coordinates. -/// -/// [`Node`]: struct.Node.html #[derive(Debug, Clone, Copy)] pub struct Layout<'a> { position: Point, @@ -34,14 +32,15 @@ impl<'a> Layout<'a> { } } - /// Gets the bounds of the [`Layout`]. + /// Returns the position of the [`Layout`]. + pub fn position(&self) -> Point { + self.position + } + + /// Returns the bounds of the [`Layout`]. /// /// The returned [`Rectangle`] describes the position and size of a /// [`Node`]. - /// - /// [`Layout`]: struct.Layout.html - /// [`Rectangle`]: struct.Rectangle.html - /// [`Node`]: struct.Node.html pub fn bounds(&self) -> Rectangle { let bounds = self.node.bounds(); @@ -54,10 +53,7 @@ impl<'a> Layout<'a> { } /// Returns an iterator over the [`Layout`] of the children of a [`Node`]. - /// - /// [`Layout`]: struct.Layout.html - /// [`Node`]: struct.Node.html - pub fn children(&'a self) -> impl Iterator<Item = Layout<'a>> { + pub fn children(self) -> impl Iterator<Item = Layout<'a>> { self.node.children().iter().map(move |node| { Layout::with_offset( Vector::new(self.position.x, self.position.y), diff --git a/native/src/layout/debugger.rs b/native/src/layout/debugger.rs index e4b21609..0759613f 100644 --- a/native/src/layout/debugger.rs +++ b/native/src/layout/debugger.rs @@ -1,8 +1,6 @@ -use crate::{Color, Layout, Point, Renderer, Widget}; +use crate::{Color, Layout, Point, Rectangle, Renderer, Widget}; /// A renderer able to graphically explain a [`Layout`]. -/// -/// [`Layout`]: struct.Layout.html pub trait Debugger: Renderer { /// Explains the [`Layout`] of an [`Element`] for debugging purposes. /// @@ -12,15 +10,15 @@ pub trait Debugger: Renderer { /// A common approach consists in recursively rendering the bounds of the /// [`Layout`] and its children. /// - /// [`Layout`]: struct.Layout.html - /// [`Element`]: ../struct.Element.html - /// [`Element::explain`]: ../struct.Element.html#method.explain + /// [`Element`]: crate::Element + /// [`Element::explain`]: crate::Element::explain fn explain<Message>( &mut self, defaults: &Self::Defaults, widget: &dyn Widget<Message, Self>, layout: Layout<'_>, cursor_position: Point, + viewport: &Rectangle, color: Color, ) -> Self::Output; } diff --git a/native/src/layout/flex.rs b/native/src/layout/flex.rs index 2f65f1c1..4f6523fb 100644 --- a/native/src/layout/flex.rs +++ b/native/src/layout/flex.rs @@ -58,8 +58,6 @@ impl Axis { /// padding and alignment to the items as needed. /// /// It returns a new layout [`Node`]. -/// -/// [`Node`]: ../struct.Node.html pub fn resolve<Message, Renderer>( axis: Axis, renderer: &Renderer, @@ -171,8 +169,5 @@ where let (width, height) = axis.pack(main - padding, cross); let size = limits.resolve(Size::new(width, height)); - Node::with_children( - Size::new(size.width + padding * 2.0, size.height + padding * 2.0), - nodes, - ) + Node::with_children(size.pad(padding), nodes) } diff --git a/native/src/layout/limits.rs b/native/src/layout/limits.rs index 664c881a..a7bb5c9c 100644 --- a/native/src/layout/limits.rs +++ b/native/src/layout/limits.rs @@ -17,9 +17,6 @@ impl Limits { }; /// Creates new [`Limits`] with the given minimum and maximum [`Size`]. - /// - /// [`Limits`]: struct.Limits.html - /// [`Size`]: ../struct.Size.html pub const fn new(min: Size, max: Size) -> Limits { Limits { min, @@ -29,32 +26,21 @@ impl Limits { } /// Returns the minimum [`Size`] of the [`Limits`]. - /// - /// [`Limits`]: struct.Limits.html - /// [`Size`]: ../struct.Size.html pub fn min(&self) -> Size { self.min } /// Returns the maximum [`Size`] of the [`Limits`]. - /// - /// [`Limits`]: struct.Limits.html - /// [`Size`]: ../struct.Size.html pub fn max(&self) -> Size { self.max } /// Returns the fill [`Size`] of the [`Limits`]. - /// - /// [`Limits`]: struct.Limits.html - /// [`Size`]: ../struct.Size.html pub fn fill(&self) -> Size { self.fill } /// Applies a width constraint to the current [`Limits`]. - /// - /// [`Limits`]: struct.Limits.html pub fn width(mut self, width: Length) -> Limits { match width { Length::Shrink => { @@ -77,8 +63,6 @@ impl Limits { } /// Applies a height constraint to the current [`Limits`]. - /// - /// [`Limits`]: struct.Limits.html pub fn height(mut self, height: Length) -> Limits { match height { Length::Shrink => { @@ -101,8 +85,6 @@ impl Limits { } /// Applies a minimum width constraint to the current [`Limits`]. - /// - /// [`Limits`]: struct.Limits.html pub fn min_width(mut self, min_width: u32) -> Limits { self.min.width = self.min.width.max(min_width as f32).min(self.max.width); @@ -111,8 +93,6 @@ impl Limits { } /// Applies a maximum width constraint to the current [`Limits`]. - /// - /// [`Limits`]: struct.Limits.html pub fn max_width(mut self, max_width: u32) -> Limits { self.max.width = self.max.width.min(max_width as f32).max(self.min.width); @@ -121,8 +101,6 @@ impl Limits { } /// Applies a minimum height constraint to the current [`Limits`]. - /// - /// [`Limits`]: struct.Limits.html pub fn min_height(mut self, min_height: u32) -> Limits { self.min.height = self.min.height.max(min_height as f32).min(self.max.height); @@ -131,8 +109,6 @@ impl Limits { } /// Applies a maximum height constraint to the current [`Limits`]. - /// - /// [`Limits`]: struct.Limits.html pub fn max_height(mut self, max_height: u32) -> Limits { self.max.height = self.max.height.min(max_height as f32).max(self.min.height); @@ -141,16 +117,11 @@ impl Limits { } /// Shrinks the current [`Limits`] to account for the given padding. - /// - /// [`Limits`]: struct.Limits.html pub fn pad(&self, padding: f32) -> Limits { self.shrink(Size::new(padding * 2.0, padding * 2.0)) } /// Shrinks the current [`Limits`] by the given [`Size`]. - /// - /// [`Limits`]: struct.Limits.html - /// [`Size`]: ../struct.Size.html pub fn shrink(&self, size: Size) -> Limits { let min = Size::new( (self.min().width - size.width).max(0.0), @@ -171,8 +142,6 @@ impl Limits { } /// Removes the minimum width constraint for the current [`Limits`]. - /// - /// [`Limits`]: struct.Limits.html pub fn loose(&self) -> Limits { Limits { min: Size::ZERO, @@ -183,8 +152,6 @@ impl Limits { /// Computes the resulting [`Size`] that fits the [`Limits`] given the /// intrinsic size of some content. - /// - /// [`Limits`]: struct.Limits.html pub fn resolve(&self, intrinsic_size: Size) -> Size { Size::new( intrinsic_size diff --git a/native/src/layout/node.rs b/native/src/layout/node.rs index a265c46a..d7666f31 100644 --- a/native/src/layout/node.rs +++ b/native/src/layout/node.rs @@ -9,17 +9,11 @@ pub struct Node { impl Node { /// Creates a new [`Node`] with the given [`Size`]. - /// - /// [`Node`]: struct.Node.html - /// [`Size`]: ../struct.Size.html pub const fn new(size: Size) -> Self { Self::with_children(size, Vec::new()) } /// Creates a new [`Node`] with the given [`Size`] and children. - /// - /// [`Node`]: struct.Node.html - /// [`Size`]: ../struct.Size.html pub const fn with_children(size: Size, children: Vec<Node>) -> Self { Node { bounds: Rectangle { @@ -33,30 +27,21 @@ impl Node { } /// Returns the [`Size`] of the [`Node`]. - /// - /// [`Node`]: struct.Node.html - /// [`Size`]: ../struct.Size.html pub fn size(&self) -> Size { Size::new(self.bounds.width, self.bounds.height) } /// Returns the bounds of the [`Node`]. - /// - /// [`Node`]: struct.Node.html pub fn bounds(&self) -> Rectangle { self.bounds } /// Returns the children of the [`Node`]. - /// - /// [`Node`]: struct.Node.html pub fn children(&self) -> &[Node] { &self.children } /// Aligns the [`Node`] in the given space. - /// - /// [`Node`]: struct.Node.html pub fn align( &mut self, horizontal_alignment: Align, @@ -85,8 +70,6 @@ impl Node { } /// Moves the [`Node`] to the given position. - /// - /// [`Node`]: struct.Node.html pub fn move_to(&mut self, position: Point) { self.bounds.x = position.x; self.bounds.y = position.y; diff --git a/native/src/lib.rs b/native/src/lib.rs index e4e7baee..0890785b 100644 --- a/native/src/lib.rs +++ b/native/src/lib.rs @@ -1,6 +1,6 @@ //! A renderer-agnostic native GUI runtime. //! -//!  +//!  //! //! `iced_native` takes [`iced_core`] and builds a native runtime on top of it, //! featuring: @@ -9,20 +9,17 @@ //! - Event handling for all the built-in widgets //! - A renderer-agnostic API //! -//! To achieve this, it introduces a bunch of reusable interfaces: +//! To achieve this, it introduces a couple of reusable interfaces: //! //! - A [`Widget`] trait, which is used to implement new widgets: from layout //! requirements to event and drawing logic. //! - A bunch of `Renderer` traits, meant to keep the crate renderer-agnostic. -//! - A [`window::Renderer`] trait, leveraging [`raw-window-handle`], which can be -//! implemented by graphical renderers that target _windows_. Window-based -//! shells (like [`iced_winit`]) can use this trait to stay renderer-agnostic. //! //! # Usage //! The strategy to use this crate depends on your particular use case. If you //! want to: -//! - Implement a custom shell or integrate it in your own system, you should -//! check out the [`UserInterface`] type. +//! - Implement a custom shell or integrate it in your own system, check out the +//! [`UserInterface`] type. //! - Build a new renderer, see the [renderer] module. //! - Build a custom widget, start at the [`Widget`] trait. //! @@ -30,30 +27,39 @@ //! [`iced_winit`]: https://github.com/hecrj/iced/tree/master/winit //! [`druid`]: https://github.com/xi-editor/druid //! [`raw-window-handle`]: https://github.com/rust-windowing/raw-window-handle -//! [`Widget`]: widget/trait.Widget.html -//! [`window::Renderer`]: window/trait.Renderer.html -//! [`UserInterface`]: struct.UserInterface.html -//! [renderer]: renderer/index.html +//! [renderer]: crate::renderer #![deny(missing_docs)] #![deny(missing_debug_implementations)] #![deny(unused_results)] #![forbid(unsafe_code)] #![forbid(rust_2018_idioms)] -pub mod input; +pub mod event; +pub mod keyboard; pub mod layout; +pub mod mouse; +pub mod overlay; +pub mod program; pub mod renderer; pub mod subscription; +pub mod touch; pub mod widget; pub mod window; mod clipboard; mod element; -mod event; mod hasher; -mod mouse_cursor; mod runtime; mod user_interface; +// We disable debug capabilities on release builds unless the `debug` feature +// is explicitly enabled. +#[cfg(feature = "debug")] +#[path = "debug/basic.rs"] +mod debug; +#[cfg(not(feature = "debug"))] +#[path = "debug/null.rs"] +mod debug; + pub use iced_core::{ Align, Background, Color, Font, HorizontalAlignment, Length, Point, Rectangle, Size, Vector, VerticalAlignment, @@ -64,11 +70,13 @@ pub use iced_futures::{executor, futures, Command}; pub use executor::Executor; pub use clipboard::Clipboard; +pub use debug::Debug; pub use element::Element; pub use event::Event; pub use hasher::Hasher; pub use layout::Layout; -pub use mouse_cursor::MouseCursor; +pub use overlay::Overlay; +pub use program::Program; pub use renderer::Renderer; pub use runtime::Runtime; pub use subscription::Subscription; diff --git a/native/src/mouse.rs b/native/src/mouse.rs new file mode 100644 index 00000000..9ee406cf --- /dev/null +++ b/native/src/mouse.rs @@ -0,0 +1,6 @@ +//! Track mouse events. + +pub mod click; + +pub use click::Click; +pub use iced_core::mouse::*; diff --git a/native/src/mouse/click.rs b/native/src/mouse/click.rs new file mode 100644 index 00000000..6c8b61a5 --- /dev/null +++ b/native/src/mouse/click.rs @@ -0,0 +1,71 @@ +//! Track mouse clicks. +use crate::Point; +use std::time::Instant; + +/// A mouse click. +#[derive(Debug, Clone, Copy)] +pub struct Click { + kind: Kind, + position: Point, + time: Instant, +} + +/// The kind of mouse click. +#[derive(Debug, Clone, Copy)] +pub enum Kind { + /// A single click + Single, + + /// A double click + Double, + + /// A triple click + Triple, +} + +impl Kind { + fn next(&self) -> Kind { + match self { + Kind::Single => Kind::Double, + Kind::Double => Kind::Triple, + Kind::Triple => Kind::Double, + } + } +} + +impl Click { + /// Creates a new [`Click`] with the given position and previous last + /// [`Click`]. + pub fn new(position: Point, previous: Option<Click>) -> Click { + let time = Instant::now(); + + let kind = if let Some(previous) = previous { + if previous.is_consecutive(position, time) { + previous.kind.next() + } else { + Kind::Single + } + } else { + Kind::Single + }; + + Click { + kind, + position, + time, + } + } + + /// Returns the [`Kind`] of [`Click`]. + pub fn kind(&self) -> Kind { + self.kind + } + + fn is_consecutive(&self, new_position: Point, time: Instant) -> bool { + self.position == new_position + && time + .checked_duration_since(self.time) + .map(|duration| duration.as_millis() <= 300) + .unwrap_or(false) + } +} diff --git a/native/src/mouse_cursor.rs b/native/src/mouse_cursor.rs deleted file mode 100644 index c7297e0e..00000000 --- a/native/src/mouse_cursor.rs +++ /dev/null @@ -1,30 +0,0 @@ -/// The state of the mouse cursor. -#[derive(Debug, Eq, PartialEq, Clone, Copy, PartialOrd, Ord)] -pub enum MouseCursor { - /// The cursor is out of the bounds of the user interface. - OutOfBounds, - - /// The cursor is over a non-interactive widget. - Idle, - - /// The cursor is over a clickable widget. - Pointer, - - /// The cursor is over a busy widget. - Working, - - /// The cursor is over a grabbable widget. - Grab, - - /// The cursor is grabbing a widget. - Grabbing, - - /// The cursor is over a text widget. - Text, -} - -impl Default for MouseCursor { - fn default() -> MouseCursor { - MouseCursor::OutOfBounds - } -} diff --git a/native/src/overlay.rs b/native/src/overlay.rs new file mode 100644 index 00000000..ea8bb384 --- /dev/null +++ b/native/src/overlay.rs @@ -0,0 +1,76 @@ +//! Display interactive elements on top of other widgets. +mod element; + +pub mod menu; + +pub use element::Element; +pub use menu::Menu; + +use crate::event::{self, Event}; +use crate::layout; +use crate::{Clipboard, Hasher, Layout, Point, Size}; + +/// An interactive component that can be displayed on top of other widgets. +pub trait Overlay<Message, Renderer> +where + Renderer: crate::Renderer, +{ + /// Returns the layout [`Node`] of the [`Overlay`]. + /// + /// This [`Node`] is used by the runtime to compute the [`Layout`] of the + /// user interface. + /// + /// [`Node`]: layout::Node + fn layout( + &self, + renderer: &Renderer, + bounds: Size, + position: Point, + ) -> layout::Node; + + /// Draws the [`Overlay`] using the associated `Renderer`. + fn draw( + &self, + renderer: &mut Renderer, + defaults: &Renderer::Defaults, + layout: Layout<'_>, + cursor_position: Point, + ) -> Renderer::Output; + + /// Computes the _layout_ hash of the [`Overlay`]. + /// + /// The produced hash is used by the runtime to decide if the [`Layout`] + /// needs to be recomputed between frames. Therefore, to ensure maximum + /// efficiency, the hash should only be affected by the properties of the + /// [`Overlay`] that can affect layouting. + /// + /// For example, the [`Text`] widget does not hash its color property, as + /// its value cannot affect the overall [`Layout`] of the user interface. + /// + /// [`Text`]: crate::widget::Text + fn hash_layout(&self, state: &mut Hasher, position: Point); + + /// Processes a runtime [`Event`]. + /// + /// It receives: + /// * an [`Event`] describing user interaction + /// * the computed [`Layout`] of the [`Overlay`] + /// * the current cursor position + /// * a mutable `Message` list, allowing the [`Overlay`] to produce + /// new messages based on user interaction. + /// * the `Renderer` + /// * a [`Clipboard`], if available + /// + /// By default, it does nothing. + fn on_event( + &mut self, + _event: Event, + _layout: Layout<'_>, + _cursor_position: Point, + _messages: &mut Vec<Message>, + _renderer: &Renderer, + _clipboard: Option<&dyn Clipboard>, + ) -> event::Status { + event::Status::Ignored + } +} diff --git a/native/src/overlay/element.rs b/native/src/overlay/element.rs new file mode 100644 index 00000000..0f44a781 --- /dev/null +++ b/native/src/overlay/element.rs @@ -0,0 +1,156 @@ +pub use crate::Overlay; + +use crate::event::{self, Event}; +use crate::layout; +use crate::{Clipboard, Hasher, Layout, Point, Size, Vector}; + +/// A generic [`Overlay`]. +#[allow(missing_debug_implementations)] +pub struct Element<'a, Message, Renderer> { + position: Point, + overlay: Box<dyn Overlay<Message, Renderer> + 'a>, +} + +impl<'a, Message, Renderer> Element<'a, Message, Renderer> +where + Renderer: crate::Renderer, +{ + /// Creates a new [`Element`] containing the given [`Overlay`]. + pub fn new( + position: Point, + overlay: Box<dyn Overlay<Message, Renderer> + 'a>, + ) -> Self { + Self { position, overlay } + } + + /// Translates the [`Element`]. + pub fn translate(mut self, translation: Vector) -> Self { + self.position = self.position + translation; + self + } + + /// Applies a transformation to the produced message of the [`Element`]. + pub fn map<B>(self, f: &'a dyn Fn(Message) -> B) -> Element<'a, B, Renderer> + where + Message: 'a, + Renderer: 'a, + B: 'static, + { + Element { + position: self.position, + overlay: Box::new(Map::new(self.overlay, f)), + } + } + + /// Computes the layout of the [`Element`] in the given bounds. + pub fn layout(&self, renderer: &Renderer, bounds: Size) -> layout::Node { + self.overlay.layout(renderer, bounds, self.position) + } + + /// Processes a runtime [`Event`]. + pub fn on_event( + &mut self, + event: Event, + layout: Layout<'_>, + cursor_position: Point, + messages: &mut Vec<Message>, + renderer: &Renderer, + clipboard: Option<&dyn Clipboard>, + ) -> event::Status { + self.overlay.on_event( + event, + layout, + cursor_position, + messages, + renderer, + clipboard, + ) + } + + /// Draws the [`Element`] and its children using the given [`Layout`]. + pub fn draw( + &self, + renderer: &mut Renderer, + defaults: &Renderer::Defaults, + layout: Layout<'_>, + cursor_position: Point, + ) -> Renderer::Output { + self.overlay + .draw(renderer, defaults, layout, cursor_position) + } + + /// Computes the _layout_ hash of the [`Element`]. + pub fn hash_layout(&self, state: &mut Hasher) { + self.overlay.hash_layout(state, self.position); + } +} + +struct Map<'a, A, B, Renderer> { + content: Box<dyn Overlay<A, Renderer> + 'a>, + mapper: &'a dyn Fn(A) -> B, +} + +impl<'a, A, B, Renderer> Map<'a, A, B, Renderer> { + pub fn new( + content: Box<dyn Overlay<A, Renderer> + 'a>, + mapper: &'a dyn Fn(A) -> B, + ) -> Map<'a, A, B, Renderer> { + Map { content, mapper } + } +} + +impl<'a, A, B, Renderer> Overlay<B, Renderer> for Map<'a, A, B, Renderer> +where + Renderer: crate::Renderer, +{ + fn layout( + &self, + renderer: &Renderer, + bounds: Size, + position: Point, + ) -> layout::Node { + self.content.layout(renderer, bounds, position) + } + + fn on_event( + &mut self, + event: Event, + layout: Layout<'_>, + cursor_position: Point, + messages: &mut Vec<B>, + renderer: &Renderer, + clipboard: Option<&dyn Clipboard>, + ) -> event::Status { + let mut original_messages = Vec::new(); + + let event_status = self.content.on_event( + event, + layout, + cursor_position, + &mut original_messages, + renderer, + clipboard, + ); + + original_messages + .drain(..) + .for_each(|message| messages.push((self.mapper)(message))); + + event_status + } + + fn draw( + &self, + renderer: &mut Renderer, + defaults: &Renderer::Defaults, + layout: Layout<'_>, + cursor_position: Point, + ) -> Renderer::Output { + self.content + .draw(renderer, defaults, layout, cursor_position) + } + + fn hash_layout(&self, state: &mut Hasher, position: Point) { + self.content.hash_layout(state, position); + } +} diff --git a/native/src/overlay/menu.rs b/native/src/overlay/menu.rs new file mode 100644 index 00000000..abac849f --- /dev/null +++ b/native/src/overlay/menu.rs @@ -0,0 +1,428 @@ +//! Build and show dropdown menus. +use crate::container; +use crate::event::{self, Event}; +use crate::layout; +use crate::mouse; +use crate::overlay; +use crate::scrollable; +use crate::text; +use crate::{ + Clipboard, Container, Element, Hasher, Layout, Length, Point, Rectangle, + Scrollable, Size, Vector, Widget, +}; + +/// A list of selectable options. +#[allow(missing_debug_implementations)] +pub struct Menu<'a, T, Renderer: self::Renderer> { + state: &'a mut State, + options: &'a [T], + hovered_option: &'a mut Option<usize>, + last_selection: &'a mut Option<T>, + width: u16, + padding: u16, + text_size: Option<u16>, + font: Renderer::Font, + style: <Renderer as self::Renderer>::Style, +} + +impl<'a, T, Renderer> Menu<'a, T, Renderer> +where + T: ToString + Clone, + Renderer: self::Renderer + 'a, +{ + /// Creates a new [`Menu`] with the given [`State`], a list of options, and + /// the message to produced when an option is selected. + pub fn new( + state: &'a mut State, + options: &'a [T], + hovered_option: &'a mut Option<usize>, + last_selection: &'a mut Option<T>, + ) -> Self { + Menu { + state, + options, + hovered_option, + last_selection, + width: 0, + padding: 0, + text_size: None, + font: Default::default(), + style: Default::default(), + } + } + + /// Sets the width of the [`Menu`]. + pub fn width(mut self, width: u16) -> Self { + self.width = width; + self + } + + /// Sets the padding of the [`Menu`]. + pub fn padding(mut self, padding: u16) -> Self { + self.padding = padding; + self + } + + /// Sets the text size of the [`Menu`]. + pub fn text_size(mut self, text_size: u16) -> Self { + self.text_size = Some(text_size); + self + } + + /// Sets the font of the [`Menu`]. + pub fn font(mut self, font: Renderer::Font) -> Self { + self.font = font; + self + } + + /// Sets the style of the [`Menu`]. + pub fn style( + mut self, + style: impl Into<<Renderer as self::Renderer>::Style>, + ) -> Self { + self.style = style.into(); + self + } + + /// Turns the [`Menu`] into an overlay [`Element`] at the given target + /// position. + /// + /// The `target_height` will be used to display the menu either on top + /// of the target or under it, depending on the screen position and the + /// dimensions of the [`Menu`]. + pub fn overlay<Message: 'a>( + self, + position: Point, + target_height: f32, + ) -> overlay::Element<'a, Message, Renderer> { + overlay::Element::new( + position, + Box::new(Overlay::new(self, target_height)), + ) + } +} + +/// The local state of a [`Menu`]. +#[derive(Debug, Clone, Default)] +pub struct State { + scrollable: scrollable::State, +} + +impl State { + /// Creates a new [`State`] for a [`Menu`]. + pub fn new() -> Self { + Self::default() + } +} + +struct Overlay<'a, Message, Renderer: self::Renderer> { + container: Container<'a, Message, Renderer>, + width: u16, + target_height: f32, + style: <Renderer as self::Renderer>::Style, +} + +impl<'a, Message, Renderer: self::Renderer> Overlay<'a, Message, Renderer> +where + Message: 'a, + Renderer: 'a, +{ + pub fn new<T>(menu: Menu<'a, T, Renderer>, target_height: f32) -> Self + where + T: Clone + ToString, + { + let Menu { + state, + options, + hovered_option, + last_selection, + width, + padding, + font, + text_size, + style, + } = menu; + + let container = + Container::new(Scrollable::new(&mut state.scrollable).push(List { + options, + hovered_option, + last_selection, + font, + text_size, + padding, + style: style.clone(), + })) + .padding(1); + + Self { + container, + width: width, + target_height, + style: style, + } + } +} + +impl<'a, Message, Renderer> crate::Overlay<Message, Renderer> + for Overlay<'a, Message, Renderer> +where + Renderer: self::Renderer, +{ + fn layout( + &self, + renderer: &Renderer, + bounds: Size, + position: Point, + ) -> layout::Node { + let space_below = bounds.height - (position.y + self.target_height); + let space_above = position.y; + + let limits = layout::Limits::new( + Size::ZERO, + Size::new( + bounds.width - position.x, + if space_below > space_above { + space_below + } else { + space_above + }, + ), + ) + .width(Length::Units(self.width)); + + let mut node = self.container.layout(renderer, &limits); + + node.move_to(if space_below > space_above { + position + Vector::new(0.0, self.target_height) + } else { + position - Vector::new(0.0, node.size().height) + }); + + node + } + + fn hash_layout(&self, state: &mut Hasher, position: Point) { + use std::hash::Hash; + + struct Marker; + std::any::TypeId::of::<Marker>().hash(state); + + (position.x as u32).hash(state); + (position.y as u32).hash(state); + self.container.hash_layout(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 { + self.container.on_event( + event.clone(), + layout, + cursor_position, + messages, + renderer, + clipboard, + ) + } + + fn draw( + &self, + renderer: &mut Renderer, + defaults: &Renderer::Defaults, + layout: Layout<'_>, + cursor_position: Point, + ) -> Renderer::Output { + let primitives = self.container.draw( + renderer, + defaults, + layout, + cursor_position, + &layout.bounds(), + ); + + renderer.decorate( + layout.bounds(), + cursor_position, + &self.style, + primitives, + ) + } +} + +struct List<'a, T, Renderer: self::Renderer> { + options: &'a [T], + hovered_option: &'a mut Option<usize>, + last_selection: &'a mut Option<T>, + padding: u16, + text_size: Option<u16>, + font: Renderer::Font, + style: <Renderer as self::Renderer>::Style, +} + +impl<'a, T, Message, Renderer: self::Renderer> Widget<Message, Renderer> + for List<'a, T, Renderer> +where + T: Clone + ToString, + Renderer: self::Renderer, +{ + fn width(&self) -> Length { + Length::Fill + } + + fn height(&self) -> Length { + Length::Shrink + } + + fn layout( + &self, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + use std::f32; + + let limits = limits.width(Length::Fill).height(Length::Shrink); + let text_size = self.text_size.unwrap_or(renderer.default_size()); + + let size = { + let intrinsic = Size::new( + 0.0, + f32::from(text_size + self.padding * 2) + * self.options.len() as f32, + ); + + limits.resolve(intrinsic) + }; + + layout::Node::new(size) + } + + fn hash_layout(&self, state: &mut Hasher) { + use std::hash::Hash as _; + + struct Marker; + std::any::TypeId::of::<Marker>().hash(state); + + self.options.len().hash(state); + self.text_size.hash(state); + self.padding.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 bounds = layout.bounds(); + + if bounds.contains(cursor_position) { + if let Some(index) = *self.hovered_option { + if let Some(option) = self.options.get(index) { + *self.last_selection = Some(option.clone()); + } + } + } + } + Event::Mouse(mouse::Event::CursorMoved { .. }) => { + let bounds = layout.bounds(); + let text_size = + self.text_size.unwrap_or(renderer.default_size()); + + if bounds.contains(cursor_position) { + *self.hovered_option = Some( + ((cursor_position.y - bounds.y) + / f32::from(text_size + self.padding * 2)) + as usize, + ); + } + } + _ => {} + } + + 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, + viewport, + self.options, + *self.hovered_option, + self.padding, + self.text_size.unwrap_or(renderer.default_size()), + self.font, + &self.style, + ) + } +} + +/// The renderer of a [`Menu`]. +/// +/// Your [renderer] will need to implement this trait before being +/// able to use a [`Menu`] in your user interface. +/// +/// [renderer]: crate::renderer +pub trait Renderer: + scrollable::Renderer + container::Renderer + text::Renderer +{ + /// The [`Menu`] style supported by this renderer. + type Style: Default + Clone; + + /// Decorates a the list of options of a [`Menu`]. + /// + /// This method can be used to draw a background for the [`Menu`]. + fn decorate( + &mut self, + bounds: Rectangle, + cursor_position: Point, + style: &<Self as Renderer>::Style, + primitive: Self::Output, + ) -> Self::Output; + + /// Draws the list of options of a [`Menu`]. + fn draw<T: ToString>( + &mut self, + bounds: Rectangle, + cursor_position: Point, + viewport: &Rectangle, + options: &[T], + hovered_option: Option<usize>, + padding: u16, + text_size: u16, + font: Self::Font, + style: &<Self as Renderer>::Style, + ) -> Self::Output; +} + +impl<'a, T, Message, Renderer> Into<Element<'a, Message, Renderer>> + for List<'a, T, Renderer> +where + T: ToString + Clone, + Message: 'a, + Renderer: 'a + self::Renderer, +{ + fn into(self) -> Element<'a, Message, Renderer> { + Element::new(self) + } +} diff --git a/native/src/program.rs b/native/src/program.rs new file mode 100644 index 00000000..9ee72703 --- /dev/null +++ b/native/src/program.rs @@ -0,0 +1,30 @@ +//! Build interactive programs using The Elm Architecture. +use crate::{Command, Element, Renderer}; + +mod state; + +pub use state::State; + +/// The core of a user interface application following The Elm Architecture. +pub trait Program: Sized { + /// The graphics backend to use to draw the [`Program`]. + type Renderer: Renderer; + + /// The type of __messages__ your [`Program`] will produce. + type Message: std::fmt::Debug + Send; + + /// Handles a __message__ and updates the state of the [`Program`]. + /// + /// This is where you define your __update logic__. All the __messages__, + /// produced by either user interactions or commands, will be handled by + /// this method. + /// + /// Any [`Command`] returned will be executed immediately in the + /// background by shells. + fn update(&mut self, message: Self::Message) -> Command<Self::Message>; + + /// Returns the widgets to display in the [`Program`]. + /// + /// These widgets can produce __messages__ based on user interaction. + fn view(&mut self) -> Element<'_, Self::Message, Self::Renderer>; +} diff --git a/native/src/program/state.rs b/native/src/program/state.rs new file mode 100644 index 00000000..e630890a --- /dev/null +++ b/native/src/program/state.rs @@ -0,0 +1,180 @@ +use crate::{ + Cache, Clipboard, Command, Debug, Event, Point, Program, Renderer, Size, + UserInterface, +}; + +/// The execution state of a [`Program`]. It leverages caching, event +/// processing, and rendering primitive storage. +#[allow(missing_debug_implementations)] +pub struct State<P> +where + P: Program + 'static, +{ + program: P, + cache: Option<Cache>, + primitive: <P::Renderer as Renderer>::Output, + queued_events: Vec<Event>, + queued_messages: Vec<P::Message>, +} + +impl<P> State<P> +where + P: Program + 'static, +{ + /// Creates a new [`State`] with the provided [`Program`], initializing its + /// primitive with the given logical bounds and renderer. + pub fn new( + mut program: P, + bounds: Size, + cursor_position: Point, + renderer: &mut P::Renderer, + debug: &mut Debug, + ) -> Self { + let mut user_interface = build_user_interface( + &mut program, + Cache::default(), + renderer, + bounds, + debug, + ); + + debug.draw_started(); + let primitive = user_interface.draw(renderer, cursor_position); + debug.draw_finished(); + + let cache = Some(user_interface.into_cache()); + + State { + program, + cache, + primitive, + queued_events: Vec::new(), + queued_messages: Vec::new(), + } + } + + /// Returns a reference to the [`Program`] of the [`State`]. + pub fn program(&self) -> &P { + &self.program + } + + /// Returns a reference to the current rendering primitive of the [`State`]. + pub fn primitive(&self) -> &<P::Renderer as Renderer>::Output { + &self.primitive + } + + /// Queues an event in the [`State`] for processing during an [`update`]. + /// + /// [`update`]: Self::update + pub fn queue_event(&mut self, event: Event) { + self.queued_events.push(event); + } + + /// Queues a message in the [`State`] for processing during an [`update`]. + /// + /// [`update`]: Self::update + pub fn queue_message(&mut self, message: P::Message) { + self.queued_messages.push(message); + } + + /// Returns whether the event queue of the [`State`] is empty or not. + pub fn is_queue_empty(&self) -> bool { + self.queued_events.is_empty() && self.queued_messages.is_empty() + } + + /// Processes all the queued events and messages, rebuilding and redrawing + /// the widgets of the linked [`Program`] if necessary. + /// + /// Returns the [`Command`] obtained from [`Program`] after updating it, + /// only if an update was necessary. + pub fn update( + &mut self, + bounds: Size, + cursor_position: Point, + clipboard: Option<&dyn Clipboard>, + renderer: &mut P::Renderer, + debug: &mut Debug, + ) -> Option<Command<P::Message>> { + let mut user_interface = build_user_interface( + &mut self.program, + self.cache.take().unwrap(), + renderer, + bounds, + debug, + ); + + debug.event_processing_started(); + let mut messages = Vec::new(); + + let _ = user_interface.update( + &self.queued_events, + cursor_position, + clipboard, + renderer, + &mut messages, + ); + + messages.extend(self.queued_messages.drain(..)); + self.queued_events.clear(); + debug.event_processing_finished(); + + if messages.is_empty() { + debug.draw_started(); + self.primitive = user_interface.draw(renderer, cursor_position); + debug.draw_finished(); + + self.cache = Some(user_interface.into_cache()); + + None + } else { + // When there are messages, we are forced to rebuild twice + // for now :^) + let temp_cache = user_interface.into_cache(); + + let commands = + Command::batch(messages.into_iter().map(|message| { + debug.log_message(&message); + + debug.update_started(); + let command = self.program.update(message); + debug.update_finished(); + + command + })); + + let mut user_interface = build_user_interface( + &mut self.program, + temp_cache, + renderer, + bounds, + debug, + ); + + debug.draw_started(); + self.primitive = user_interface.draw(renderer, cursor_position); + debug.draw_finished(); + + self.cache = Some(user_interface.into_cache()); + + Some(commands) + } + } +} + +fn build_user_interface<'a, P: Program>( + program: &'a mut P, + cache: Cache, + renderer: &mut P::Renderer, + size: Size, + debug: &mut Debug, +) -> UserInterface<'a, P::Message, P::Renderer> { + debug.view_started(); + let view = program.view(); + debug.view_finished(); + + debug.layout_started(); + let user_interface = UserInterface::build(view, size, cache, renderer); + debug.layout_finished(); + + user_interface +} diff --git a/native/src/renderer.rs b/native/src/renderer.rs index a16df72b..39a6cff1 100644 --- a/native/src/renderer.rs +++ b/native/src/renderer.rs @@ -13,19 +13,19 @@ //! In the end, a __renderer__ satisfying all the constraints is //! needed to build a [`UserInterface`]. //! -//! [`Widget`]: ../widget/trait.Widget.html -//! [`UserInterface`]: ../struct.UserInterface.html -//! [`Text`]: ../widget/text/struct.Text.html -//! [`text::Renderer`]: ../widget/text/trait.Renderer.html -//! [`Checkbox`]: ../widget/checkbox/struct.Checkbox.html -//! [`checkbox::Renderer`]: ../widget/checkbox/trait.Renderer.html +//! [`Widget`]: crate::Widget +//! [`UserInterface`]: crate::UserInterface +//! [`Text`]: crate::widget::Text +//! [`text::Renderer`]: crate::widget::text::Renderer +//! [`Checkbox`]: crate::widget::Checkbox +//! [`checkbox::Renderer`]: crate::widget::checkbox::Renderer #[cfg(debug_assertions)] mod null; #[cfg(debug_assertions)] pub use null::Null; -use crate::{layout, Element}; +use crate::{layout, Element, Rectangle}; /// A component that can take the state of a user interface and produce an /// output for its users. @@ -34,15 +34,11 @@ pub trait Renderer: Sized { /// /// If you are implementing a graphical renderer, your output will most /// likely be a tree of visual primitives. - /// - /// [`Renderer`]: trait.Renderer.html type Output; /// The default styling attributes of the [`Renderer`]. /// /// This type can be leveraged to implement style inheritance. - /// - /// [`Renderer`]: trait.Renderer.html type Defaults: Default; /// Lays out the elements of a user interface. @@ -56,4 +52,13 @@ pub trait Renderer: Sized { ) -> layout::Node { element.layout(self, limits) } + + /// Overlays the `overlay` output with the given bounds on top of the `base` + /// output. + fn overlay( + &mut self, + base: Self::Output, + overlay: Self::Output, + overlay_bounds: Rectangle, + ) -> Self::Output; } diff --git a/native/src/renderer/null.rs b/native/src/renderer/null.rs index 0fcce5ad..91ee9a28 100644 --- a/native/src/renderer/null.rs +++ b/native/src/renderer/null.rs @@ -1,7 +1,8 @@ use crate::{ - button, checkbox, column, progress_bar, radio, row, scrollable, slider, - text, text_input, Color, Element, Font, HorizontalAlignment, Layout, Point, - Rectangle, Renderer, Size, VerticalAlignment, + button, checkbox, column, container, pane_grid, progress_bar, radio, row, + scrollable, slider, text, text_input, Color, Element, Font, + HorizontalAlignment, Layout, Point, Rectangle, Renderer, Size, + VerticalAlignment, }; /// A renderer that does nothing. @@ -12,8 +13,6 @@ pub struct Null; impl Null { /// Creates a new [`Null`] renderer. - /// - /// [`Null`]: struct.Null.html pub fn new() -> Null { Null } @@ -22,6 +21,9 @@ impl Null { impl Renderer for Null { type Output = (); type Defaults = (); + + fn overlay(&mut self, _base: (), _overlay: (), _overlay_bounds: Rectangle) { + } } impl column::Renderer for Null { @@ -31,6 +33,7 @@ impl column::Renderer for Null { _content: &[Element<'_, Message, Self>], _layout: Layout<'_>, _cursor_position: Point, + _viewport: &Rectangle, ) { } } @@ -42,12 +45,17 @@ impl row::Renderer for Null { _content: &[Element<'_, Message, Self>], _layout: Layout<'_>, _cursor_position: Point, + _viewport: &Rectangle, ) { } } impl text::Renderer for Null { - const DEFAULT_SIZE: u16 = 20; + type Font = Font; + + fn default_size(&self) -> u16 { + 20 + } fn measure( &self, @@ -81,6 +89,9 @@ impl scrollable::Renderer for Null { _bounds: Rectangle, _content_bounds: Rectangle, _offset: u32, + _scrollbar_width: u16, + _scrollbar_margin: u16, + _scroller_width: u16, ) -> Option<scrollable::Scrollbar> { None } @@ -103,10 +114,6 @@ impl scrollable::Renderer for Null { impl text_input::Renderer for Null { type Style = (); - fn default_size(&self) -> u16 { - 20 - } - fn measure_value(&self, _value: &str, _size: u16, _font: Font) -> f32 { 0.0 } @@ -114,10 +121,10 @@ impl text_input::Renderer for Null { fn offset( &self, _text_bounds: Rectangle, + _font: Font, _size: u16, _value: &text_input::Value, _state: &text_input::State, - _font: Font, ) -> f32 { 0.0 } @@ -127,8 +134,8 @@ impl text_input::Renderer for Null { _bounds: Rectangle, _text_bounds: Rectangle, _cursor_position: Point, - _size: u16, _font: Font, + _size: u16, _placeholder: &str, _value: &text_input::Value, _state: &text_input::State, @@ -159,9 +166,8 @@ impl button::Renderer for Null { impl radio::Renderer for Null { type Style = (); - fn default_size(&self) -> u32 { - 20 - } + const DEFAULT_SIZE: u16 = 20; + const DEFAULT_SPACING: u16 = 15; fn draw( &mut self, @@ -194,9 +200,7 @@ impl checkbox::Renderer for Null { impl slider::Renderer for Null { type Style = (); - fn height(&self) -> u32 { - 30 - } + const DEFAULT_HEIGHT: u16 = 30; fn draw( &mut self, @@ -224,3 +228,60 @@ impl progress_bar::Renderer for Null { ) { } } + +impl container::Renderer for Null { + type Style = (); + + 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<'_>, + ) { + } +} + +impl pane_grid::Renderer for Null { + fn draw<Message>( + &mut self, + _defaults: &Self::Defaults, + _content: &[(pane_grid::Pane, pane_grid::Content<'_, Message, Self>)], + _dragging: Option<(pane_grid::Pane, Point)>, + _resizing: Option<pane_grid::Axis>, + _layout: Layout<'_>, + _cursor_position: Point, + ) { + } + + fn draw_pane<Message>( + &mut self, + _defaults: &Self::Defaults, + _bounds: Rectangle, + _style: &Self::Style, + _title_bar: Option<( + &pane_grid::TitleBar<'_, Message, Self>, + Layout<'_>, + )>, + _body: (&Element<'_, Message, Self>, Layout<'_>), + _cursor_position: Point, + ) { + } + + 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, + ) { + } +} diff --git a/native/src/runtime.rs b/native/src/runtime.rs index 9fa031f4..5b0a6925 100644 --- a/native/src/runtime.rs +++ b/native/src/runtime.rs @@ -1,12 +1,18 @@ //! Run commands and subscriptions. -use crate::{Event, Hasher}; +use crate::event::{self, Event}; +use crate::Hasher; /// A native runtime with a generic executor and receiver of results. /// /// It can be used by shells to easily spawn a [`Command`] or track a /// [`Subscription`]. /// -/// [`Command`]: ../struct.Command.html -/// [`Subscription`]: ../struct.Subscription.html -pub type Runtime<Executor, Receiver, Message> = - iced_futures::Runtime<Hasher, Event, Executor, Receiver, Message>; +/// [`Command`]: crate::Command +/// [`Subscription`]: crate::Subscription +pub type Runtime<Executor, Receiver, Message> = iced_futures::Runtime< + Hasher, + (Event, event::Status), + Executor, + Receiver, + Message, +>; diff --git a/native/src/subscription.rs b/native/src/subscription.rs index 0d002c6c..ff954382 100644 --- a/native/src/subscription.rs +++ b/native/src/subscription.rs @@ -1,5 +1,6 @@ //! Listen to external events in your application. -use crate::{Event, Hasher}; +use crate::event::{self, Event}; +use crate::Hasher; use iced_futures::futures::stream::BoxStream; /// A request to listen to external events. @@ -13,21 +14,18 @@ use iced_futures::futures::stream::BoxStream; /// For instance, you can use a [`Subscription`] to listen to a WebSocket /// connection, keyboard presses, mouse events, time ticks, etc. /// -/// [`Command`]: ../struct.Command.html -/// [`Subscription`]: struct.Subscription.html -pub type Subscription<T> = iced_futures::Subscription<Hasher, Event, T>; +/// [`Command`]: crate::Command +pub type Subscription<T> = + iced_futures::Subscription<Hasher, (Event, event::Status), T>; /// A stream of runtime events. /// /// It is the input of a [`Subscription`] in the native runtime. -/// -/// [`Subscription`]: type.Subscription.html -pub type EventStream = BoxStream<'static, Event>; +pub type EventStream = BoxStream<'static, (Event, event::Status)>; /// A native [`Subscription`] tracker. -/// -/// [`Subscription`]: type.Subscription.html -pub type Tracker = iced_futures::subscription::Tracker<Hasher, Event>; +pub type Tracker = + iced_futures::subscription::Tracker<Hasher, (Event, event::Status)>; pub use iced_futures::subscription::Recipe; @@ -37,11 +35,30 @@ use events::Events; /// Returns a [`Subscription`] to all the runtime events. /// -/// This subscription will notify your application of any [`Event`] handled by -/// the runtime. -/// -/// [`Subscription`]: type.Subscription.html -/// [`Event`]: ../enum.Event.html +/// This subscription will notify your application of any [`Event`] that was +/// not captured by any widget. pub fn events() -> Subscription<Event> { - Subscription::from_recipe(Events) + Subscription::from_recipe(Events { + f: |event, status| match status { + event::Status::Ignored => Some(event), + event::Status::Captured => None, + }, + }) +} + +/// Returns a [`Subscription`] that filters all the runtime events with the +/// provided function, producing messages accordingly. +/// +/// This subscription will call the provided function for every [`Event`] +/// handled by the runtime. If the function: +/// +/// - Returns `None`, the [`Event`] will be discarded. +/// - Returns `Some` message, the `Message` will be produced. +pub fn events_with<Message>( + f: fn(Event, event::Status) -> Option<Message>, +) -> Subscription<Message> +where + Message: 'static + Send, +{ + Subscription::from_recipe(Events { f }) } diff --git a/native/src/subscription/events.rs b/native/src/subscription/events.rs index 7d33166e..f689f3af 100644 --- a/native/src/subscription/events.rs +++ b/native/src/subscription/events.rs @@ -1,24 +1,36 @@ -use crate::{ - subscription::{EventStream, Recipe}, - Event, Hasher, -}; -use iced_futures::futures::stream::BoxStream; +use crate::event::{self, Event}; +use crate::subscription::{EventStream, Recipe}; +use crate::Hasher; +use iced_futures::futures::future; +use iced_futures::futures::StreamExt; +use iced_futures::BoxStream; -pub struct Events; +pub struct Events<Message> { + pub(super) f: fn(Event, event::Status) -> Option<Message>, +} -impl Recipe<Hasher, Event> for Events { - type Output = Event; +impl<Message> Recipe<Hasher, (Event, event::Status)> for Events<Message> +where + Message: 'static + Send, +{ + type Output = Message; fn hash(&self, state: &mut Hasher) { use std::hash::Hash; - std::any::TypeId::of::<Self>().hash(state); + struct Marker; + std::any::TypeId::of::<Marker>().hash(state); + self.f.hash(state); } fn stream( self: Box<Self>, event_stream: EventStream, - ) -> BoxStream<'static, Self::Output> { + ) -> BoxStream<Self::Output> { event_stream + .filter_map(move |(event, status)| { + future::ready((self.f)(event, status)) + }) + .boxed() } } diff --git a/native/src/input/touch.rs b/native/src/touch.rs index ea879427..88bd83bb 100644 --- a/native/src/input/touch.rs +++ b/native/src/touch.rs @@ -1,5 +1,4 @@ //! Build touch events. - use crate::Point; /// A touch interaction. diff --git a/native/src/user_interface.rs b/native/src/user_interface.rs index b71b9003..7a64ac59 100644 --- a/native/src/user_interface.rs +++ b/native/src/user_interface.rs @@ -1,7 +1,7 @@ -use crate::{ - input::{mouse, Touch}, - layout, Clipboard, Element, Event, Layout, Point, Size, -}; +use crate::event::{self, Event}; +use crate::layout; +use crate::overlay; +use crate::{Clipboard, Element, Layout, Point, Rectangle, Size}; use std::hash::Hasher; @@ -12,14 +12,17 @@ use std::hash::Hasher; /// Iced tries to avoid dictating how to write your event loop. You are in /// charge of using this type in your system in any way you want. /// -/// [`Layout`]: struct.Layout.html +/// # Example +/// The [`integration` example] uses a [`UserInterface`] to integrate Iced in +/// an existing graphical application. +/// +/// [`integration` example]: https://github.com/hecrj/iced/tree/0.2/examples/integration #[allow(missing_debug_implementations)] pub struct UserInterface<'a, Message, Renderer> { - hash: u64, root: Element<'a, Message, Renderer>, - layout: layout::Node, + base: Layer, + overlay: Option<Layer>, bounds: Size, - cursor_position: Point, } impl<'a, Message, Renderer> UserInterface<'a, Message, Renderer> @@ -31,10 +34,6 @@ where /// It is able to avoid expensive computations when using a [`Cache`] /// obtained from a previous instance of a [`UserInterface`]. /// - /// [`Element`]: struct.Element.html - /// [`Cache`]: struct.Cache.html - /// [`UserInterface`]: struct.UserInterface.html - /// /// # Example /// Imagine we want to build a [`UserInterface`] for /// [the counter example that we previously wrote](index.html#usage). Here @@ -91,25 +90,37 @@ where ) -> Self { let root = root.into(); - let hash = { - let hasher = &mut crate::Hasher::default(); - root.hash_layout(hasher); + let (base, overlay) = { + let hash = { + let hasher = &mut crate::Hasher::default(); + root.hash_layout(hasher); - hasher.finish() - }; + hasher.finish() + }; - let layout = if hash == cache.hash && bounds == cache.bounds { - cache.layout - } else { - renderer.layout(&root, &layout::Limits::new(Size::ZERO, bounds)) + let layout_is_cached = + hash == cache.base.hash && bounds == cache.bounds; + + let (layout, overlay) = if layout_is_cached { + (cache.base.layout, cache.overlay) + } else { + ( + renderer.layout( + &root, + &layout::Limits::new(Size::ZERO, bounds), + ), + None, + ) + }; + + (Layer { layout, hash }, overlay) }; UserInterface { - hash, root, - layout, + base, + overlay, bounds, - cursor_position: cache.cursor_position, } } @@ -118,15 +129,12 @@ where /// It returns __messages__ that may have been produced as a result of user /// interactions. You should feed these to your __update logic__. /// - /// [`UserInterface`]: struct.UserInterface.html - /// [`Event`]: enum.Event.html - /// /// # Example /// Let's allow our [counter](index.html#usage) to change state by /// completing [the previous example](#example): /// /// ```no_run - /// use iced_native::{UserInterface, Cache, Size}; + /// use iced_native::{UserInterface, Cache, Size, Point}; /// use iced_wgpu::Renderer; /// /// # mod iced_wgpu { @@ -148,12 +156,14 @@ where /// let mut cache = Cache::new(); /// let mut renderer = Renderer::new(); /// let mut window_size = Size::new(1024.0, 768.0); + /// let mut cursor_position = Point::default(); /// /// // Initialize our event storage /// let mut events = Vec::new(); + /// let mut messages = Vec::new(); /// /// loop { - /// // Process system events... + /// // Obtain system events... /// /// let mut user_interface = UserInterface::build( /// counter.view(), @@ -163,61 +173,103 @@ where /// ); /// /// // Update the user interface - /// let messages = user_interface.update(events.drain(..), None, &renderer); + /// let event_statuses = user_interface.update( + /// &events, + /// cursor_position, + /// None, + /// &renderer, + /// &mut messages + /// ); /// /// cache = user_interface.into_cache(); /// /// // Process the produced messages - /// for message in messages { + /// for message in messages.drain(..) { /// counter.update(message); /// } /// } /// ``` pub fn update( &mut self, - events: impl IntoIterator<Item = Event>, + events: &[Event], + cursor_position: Point, clipboard: Option<&dyn Clipboard>, renderer: &Renderer, - ) -> Vec<Message> { - let mut messages = Vec::new(); - - for event in events { - match event { - Event::Mouse(mouse::Event::CursorMoved { position }) - | Event::Touch(Touch { position, .. }) => { - self.cursor_position = position; - } - _ => {} - } - - self.root.widget.on_event( - event, - Layout::new(&self.layout), - self.cursor_position, - &mut messages, + messages: &mut Vec<Message>, + ) -> Vec<event::Status> { + let (base_cursor, overlay_statuses) = if let Some(mut overlay) = + self.root.overlay(Layout::new(&self.base.layout)) + { + let layer = Self::overlay_layer( + self.overlay.take(), + self.bounds, + &mut overlay, renderer, - clipboard, ); - } - messages + let event_statuses = events + .iter() + .cloned() + .map(|event| { + overlay.on_event( + event, + Layout::new(&layer.layout), + cursor_position, + messages, + renderer, + clipboard, + ) + }) + .collect(); + + let base_cursor = if layer.layout.bounds().contains(cursor_position) + { + // TODO: Type-safe cursor availability + Point::new(-1.0, -1.0) + } else { + cursor_position + }; + + self.overlay = Some(layer); + + (base_cursor, event_statuses) + } else { + (cursor_position, vec![event::Status::Ignored; events.len()]) + }; + + events + .iter() + .cloned() + .zip(overlay_statuses.into_iter()) + .map(|(event, overlay_status)| { + let event_status = self.root.widget.on_event( + event, + Layout::new(&self.base.layout), + base_cursor, + messages, + renderer, + clipboard, + ); + + event_status.merge(overlay_status) + }) + .collect() } /// Draws the [`UserInterface`] with the provided [`Renderer`]. /// - /// It returns the current state of the [`MouseCursor`]. You should update - /// the icon of the mouse cursor accordingly in your system. + /// It returns the some [`Renderer::Output`]. You should update the icon of + /// the mouse cursor accordingly in your system. /// - /// [`UserInterface`]: struct.UserInterface.html - /// [`Renderer`]: trait.Renderer.html - /// [`MouseCursor`]: enum.MouseCursor.html + /// [`Renderer`]: crate::Renderer + /// [`Renderer::Output`]: crate::Renderer::Output /// /// # Example /// We can finally draw our [counter](index.html#usage) by /// [completing the last example](#example-1): /// /// ```no_run - /// use iced_native::{UserInterface, Cache, Size}; + /// use iced_native::{UserInterface, Cache, Size, Point}; /// use iced_wgpu::Renderer; /// /// # mod iced_wgpu { @@ -239,10 +291,12 @@ where /// let mut cache = Cache::new(); /// let mut renderer = Renderer::new(); /// let mut window_size = Size::new(1024.0, 768.0); + /// let mut cursor_position = Point::default(); /// let mut events = Vec::new(); + /// let mut messages = Vec::new(); /// /// loop { - /// // Process system events... + /// // Obtain system events... /// /// let mut user_interface = UserInterface::build( /// counter.view(), @@ -251,14 +305,21 @@ where /// &mut renderer, /// ); /// - /// let messages = user_interface.update(events.drain(..), None, &renderer); + /// // Update the user interface + /// let event_statuses = user_interface.update( + /// &events, + /// cursor_position, + /// None, + /// &renderer, + /// &mut messages + /// ); /// /// // Draw the user interface - /// let mouse_cursor = user_interface.draw(&mut renderer); + /// let mouse_cursor = user_interface.draw(&mut renderer, cursor_position); /// /// cache = user_interface.into_cache(); /// - /// for message in messages { + /// for message in messages.drain(..) { /// counter.update(message); /// } /// @@ -266,39 +327,132 @@ where /// // Flush rendering operations... /// } /// ``` - pub fn draw(&self, renderer: &mut Renderer) -> Renderer::Output { - self.root.widget.draw( + pub fn draw( + &mut self, + renderer: &mut Renderer, + cursor_position: Point, + ) -> Renderer::Output { + let viewport = Rectangle::with_size(self.bounds); + + let overlay = if let Some(mut overlay) = + self.root.overlay(Layout::new(&self.base.layout)) + { + let layer = Self::overlay_layer( + self.overlay.take(), + self.bounds, + &mut overlay, + renderer, + ); + + let overlay_bounds = layer.layout.bounds(); + + let overlay_primitives = overlay.draw( + renderer, + &Renderer::Defaults::default(), + Layout::new(&layer.layout), + cursor_position, + ); + + self.overlay = Some(layer); + + Some((overlay_primitives, overlay_bounds)) + } else { + None + }; + + if let Some((overlay_primitives, overlay_bounds)) = overlay { + let base_cursor = if overlay_bounds.contains(cursor_position) { + Point::new(-1.0, -1.0) + } else { + cursor_position + }; + + let base_primitives = self.root.widget.draw( + renderer, + &Renderer::Defaults::default(), + Layout::new(&self.base.layout), + base_cursor, + &viewport, + ); + + renderer.overlay( + base_primitives, + overlay_primitives, + overlay_bounds, + ) + } else { + self.root.widget.draw( + renderer, + &Renderer::Defaults::default(), + Layout::new(&self.base.layout), + cursor_position, + &viewport, + ) + } + } + + /// Relayouts and returns a new [`UserInterface`] using the provided + /// bounds. + pub fn relayout(self, bounds: Size, renderer: &mut Renderer) -> Self { + Self::build( + self.root, + bounds, + Cache { + base: self.base, + overlay: self.overlay, + bounds: self.bounds, + }, renderer, - &Renderer::Defaults::default(), - Layout::new(&self.layout), - self.cursor_position, ) } /// Extract the [`Cache`] of the [`UserInterface`], consuming it in the /// process. - /// - /// [`Cache`]: struct.Cache.html - /// [`UserInterface`]: struct.UserInterface.html pub fn into_cache(self) -> Cache { Cache { - hash: self.hash, - layout: self.layout, + base: self.base, + overlay: self.overlay, bounds: self.bounds, - cursor_position: self.cursor_position, + } + } + + fn overlay_layer( + cache: Option<Layer>, + bounds: Size, + overlay: &mut overlay::Element<'_, Message, Renderer>, + renderer: &Renderer, + ) -> Layer { + let new_hash = { + let hasher = &mut crate::Hasher::default(); + overlay.hash_layout(hasher); + + hasher.finish() + }; + + let layout = match cache { + Some(Layer { hash, layout }) if new_hash == hash => layout, + _ => overlay.layout(renderer, bounds), + }; + + Layer { + layout, + hash: new_hash, } } } +#[derive(Debug, Clone)] +struct Layer { + layout: layout::Node, + hash: u64, +} + /// Reusable data of a specific [`UserInterface`]. -/// -/// [`UserInterface`]: struct.UserInterface.html #[derive(Debug, Clone)] pub struct Cache { - hash: u64, - layout: layout::Node, + base: Layer, + overlay: Option<Layer>, bounds: Size, - cursor_position: Point, } impl Cache { @@ -306,15 +460,14 @@ impl Cache { /// /// You should use this to initialize a [`Cache`] before building your first /// [`UserInterface`]. - /// - /// [`Cache`]: struct.Cache.html - /// [`UserInterface`]: struct.UserInterface.html pub fn new() -> Cache { Cache { - hash: 0, - layout: layout::Node::new(Size::new(0.0, 0.0)), + base: Layer { + layout: layout::Node::new(Size::new(0.0, 0.0)), + hash: 0, + }, + overlay: None, bounds: Size::ZERO, - cursor_position: Point::new(-1.0, -1.0), } } } @@ -324,11 +477,3 @@ impl Default for Cache { Cache::new() } } - -impl PartialEq for Cache { - fn eq(&self, other: &Cache) -> bool { - self.hash == other.hash && self.cursor_position == other.cursor_position - } -} - -impl Eq for Cache {} diff --git a/native/src/widget.rs b/native/src/widget.rs index f9424b02..3677713a 100644 --- a/native/src/widget.rs +++ b/native/src/widget.rs @@ -18,16 +18,18 @@ //! use iced_native::{button, Button, Widget}; //! ``` //! -//! [`Widget`]: trait.Widget.html -//! [renderer]: ../renderer/index.html +//! [renderer]: crate::renderer pub mod button; pub mod checkbox; pub mod column; pub mod container; pub mod image; +pub mod pane_grid; +pub mod pick_list; pub mod progress_bar; pub mod radio; pub mod row; +pub mod rule; pub mod scrollable; pub mod slider; pub mod space; @@ -46,12 +48,18 @@ pub use container::Container; #[doc(no_inline)] pub use image::Image; #[doc(no_inline)] +pub use pane_grid::PaneGrid; +#[doc(no_inline)] +pub use pick_list::PickList; +#[doc(no_inline)] pub use progress_bar::ProgressBar; #[doc(no_inline)] pub use radio::Radio; #[doc(no_inline)] pub use row::Row; #[doc(no_inline)] +pub use rule::Rule; +#[doc(no_inline)] pub use scrollable::Scrollable; #[doc(no_inline)] pub use slider::Slider; @@ -64,27 +72,41 @@ pub use text::Text; #[doc(no_inline)] pub use text_input::TextInput; -use crate::{layout, Clipboard, Event, Hasher, Layout, Length, Point}; +use crate::event::{self, Event}; +use crate::layout; +use crate::overlay; +use crate::{Clipboard, Hasher, Layout, Length, Point, Rectangle}; /// A component that displays information and allows interaction. /// /// If you want to build your own widgets, you will need to implement this /// trait. /// -/// [`Widget`]: trait.Widget.html -/// [`Element`]: ../struct.Element.html +/// # Examples +/// The repository has some [examples] showcasing how to implement a custom +/// widget: +/// +/// - [`bezier_tool`], a Paint-like tool for drawing Bézier curves using +/// [`lyon`]. +/// - [`custom_widget`], a demonstration of how to build a custom widget that +/// draws a circle. +/// - [`geometry`], a custom widget showcasing how to draw geometry with the +/// `Mesh2D` primitive in [`iced_wgpu`]. +/// +/// [examples]: https://github.com/hecrj/iced/tree/0.2/examples +/// [`bezier_tool`]: https://github.com/hecrj/iced/tree/0.2/examples/bezier_tool +/// [`custom_widget`]: https://github.com/hecrj/iced/tree/0.2/examples/custom_widget +/// [`geometry`]: https://github.com/hecrj/iced/tree/0.2/examples/geometry +/// [`lyon`]: https://github.com/nical/lyon +/// [`iced_wgpu`]: https://github.com/hecrj/iced/tree/0.2/wgpu pub trait Widget<Message, Renderer> where Renderer: crate::Renderer, { /// Returns the width of the [`Widget`]. - /// - /// [`Widget`]: trait.Widget.html fn width(&self) -> Length; /// Returns the height of the [`Widget`]. - /// - /// [`Widget`]: trait.Widget.html fn height(&self) -> Length; /// Returns the [`Node`] of the [`Widget`]. @@ -92,9 +114,7 @@ where /// This [`Node`] is used by the runtime to compute the [`Layout`] of the /// user interface. /// - /// [`Node`]: ../layout/struct.Node.html - /// [`Widget`]: trait.Widget.html - /// [`Layout`]: ../layout/struct.Layout.html + /// [`Node`]: layout::Node fn layout( &self, renderer: &Renderer, @@ -102,14 +122,13 @@ where ) -> layout::Node; /// Draws the [`Widget`] using the associated `Renderer`. - /// - /// [`Widget`]: trait.Widget.html fn draw( &self, renderer: &mut Renderer, defaults: &Renderer::Defaults, layout: Layout<'_>, cursor_position: Point, + viewport: &Rectangle, ) -> Renderer::Output; /// Computes the _layout_ hash of the [`Widget`]. @@ -122,9 +141,7 @@ where /// For example, the [`Text`] widget does not hash its color property, as /// its value cannot affect the overall [`Layout`] of the user interface. /// - /// [`Widget`]: trait.Widget.html - /// [`Layout`]: ../layout/struct.Layout.html - /// [`Text`]: text/struct.Text.html + /// [`Text`]: crate::widget::Text fn hash_layout(&self, state: &mut Hasher); /// Processes a runtime [`Event`]. @@ -136,12 +153,9 @@ where /// * a mutable `Message` list, allowing the [`Widget`] to produce /// new messages based on user interaction. /// * the `Renderer` + /// * a [`Clipboard`], if available /// /// By default, it does nothing. - /// - /// [`Event`]: ../enum.Event.html - /// [`Widget`]: trait.Widget.html - /// [`Layout`]: ../layout/struct.Layout.html fn on_event( &mut self, _event: Event, @@ -150,6 +164,15 @@ where _messages: &mut Vec<Message>, _renderer: &Renderer, _clipboard: Option<&dyn Clipboard>, - ) { + ) -> event::Status { + event::Status::Ignored + } + + /// Returns the overlay of the [`Widget`], if there is any. + fn overlay( + &mut self, + _layout: Layout<'_>, + ) -> Option<overlay::Element<'_, Message, Renderer>> { + None } } 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(), + } + } +} diff --git a/native/src/window.rs b/native/src/window.rs index 4dcae62f..220bb3be 100644 --- a/native/src/window.rs +++ b/native/src/window.rs @@ -1,6 +1,4 @@ //! Build window-based GUI applications. -mod backend; mod event; -pub use backend::Backend; pub use event::Event; diff --git a/native/src/window/backend.rs b/native/src/window/backend.rs deleted file mode 100644 index 3bc691cd..00000000 --- a/native/src/window/backend.rs +++ /dev/null @@ -1,55 +0,0 @@ -use crate::MouseCursor; - -use raw_window_handle::HasRawWindowHandle; - -/// A graphics backend that can render to windows. -pub trait Backend: Sized { - /// The settings of the backend. - type Settings: Default; - - /// The iced renderer of the backend. - type Renderer: crate::Renderer; - - /// The surface of the backend. - type Surface; - - /// The swap chain of the backend. - type SwapChain; - - /// Creates a new [`Backend`] and an associated iced renderer. - /// - /// [`Backend`]: trait.Backend.html - fn new(settings: Self::Settings) -> (Self, Self::Renderer); - - /// Crates a new [`Surface`] for the given window. - /// - /// [`Surface`]: #associatedtype.Surface - fn create_surface<W: HasRawWindowHandle>( - &mut self, - window: &W, - ) -> Self::Surface; - - /// Crates a new [`SwapChain`] for the given [`Surface`]. - /// - /// [`SwapChain`]: #associatedtype.SwapChain - /// [`Surface`]: #associatedtype.Surface - fn create_swap_chain( - &mut self, - surface: &Self::Surface, - width: u32, - height: u32, - ) -> Self::SwapChain; - - /// Draws the output primitives to the next frame of the given [`SwapChain`]. - /// - /// [`SwapChain`]: #associatedtype.SwapChain - /// [`Surface`]: #associatedtype.Surface - fn draw<T: AsRef<str>>( - &mut self, - renderer: &mut Self::Renderer, - swap_chain: &mut Self::SwapChain, - output: &<Self::Renderer as crate::Renderer>::Output, - scale_factor: f64, - overlay: &[T], - ) -> MouseCursor; -} |