diff options
Diffstat (limited to '')
41 files changed, 1944 insertions, 507 deletions
diff --git a/native/src/clipboard.rs b/native/src/clipboard.rs index ecdccabf..081b4004 100644 --- a/native/src/clipboard.rs +++ b/native/src/clipboard.rs @@ -1,6 +1,23 @@ +//! Access the clipboard. +  /// A buffer for short-term storage and transfer within and between  /// applications.  pub trait Clipboard { -    /// Returns the current content of the [`Clipboard`] as text. -    fn content(&self) -> Option<String>; +    /// Reads the current content of the [`Clipboard`] as text. +    fn read(&self) -> Option<String>; + +    /// Writes the given text contents to the [`Clipboard`]. +    fn write(&mut self, contents: String); +} + +/// A null implementation of the [`Clipboard`] trait. +#[derive(Debug, Clone, Copy)] +pub struct Null; + +impl Clipboard for Null { +    fn read(&self) -> Option<String> { +        None +    } + +    fn write(&mut self, _contents: String) {}  } diff --git a/native/src/element.rs b/native/src/element.rs index d6e9639a..5c84a388 100644 --- a/native/src/element.rs +++ b/native/src/element.rs @@ -223,17 +223,17 @@ where          event: Event,          layout: Layout<'_>,          cursor_position: Point, -        messages: &mut Vec<Message>,          renderer: &Renderer, -        clipboard: Option<&dyn Clipboard>, +        clipboard: &mut dyn Clipboard, +        messages: &mut Vec<Message>,      ) -> event::Status {          self.widget.on_event(              event,              layout,              cursor_position, -            messages,              renderer,              clipboard, +            messages,          )      } @@ -311,9 +311,9 @@ where          event: Event,          layout: Layout<'_>,          cursor_position: Point, -        messages: &mut Vec<B>,          renderer: &Renderer, -        clipboard: Option<&dyn Clipboard>, +        clipboard: &mut dyn Clipboard, +        messages: &mut Vec<B>,      ) -> event::Status {          let mut original_messages = Vec::new(); @@ -321,9 +321,9 @@ where              event,              layout,              cursor_position, -            &mut original_messages,              renderer,              clipboard, +            &mut original_messages,          );          original_messages @@ -401,17 +401,17 @@ where          event: Event,          layout: Layout<'_>,          cursor_position: Point, -        messages: &mut Vec<Message>,          renderer: &Renderer, -        clipboard: Option<&dyn Clipboard>, +        clipboard: &mut dyn Clipboard, +        messages: &mut Vec<Message>,      ) -> event::Status {          self.element.widget.on_event(              event,              layout,              cursor_position, -            messages,              renderer,              clipboard, +            messages,          )      } diff --git a/native/src/event.rs b/native/src/event.rs index 0e86171e..1c26b5f2 100644 --- a/native/src/event.rs +++ b/native/src/event.rs @@ -1,5 +1,8 @@  //! Handle events of a user interface. -use crate::{keyboard, mouse, window}; +use crate::keyboard; +use crate::mouse; +use crate::touch; +use crate::window;  /// A user interface event.  /// @@ -17,6 +20,30 @@ pub enum Event {      /// A window event      Window(window::Event), + +    /// A touch event +    Touch(touch::Event), + +    /// A platform specific event +    PlatformSpecific(PlatformSpecific), +} + +/// A platform specific event +#[derive(Debug, Clone, PartialEq)] +pub enum PlatformSpecific { +    /// A MacOS specific event +    MacOS(MacOS), +} + +/// Describes an event specific to MacOS +#[derive(Debug, Clone, PartialEq)] +pub enum MacOS { +    /// Triggered when the app receives an URL from the system +    /// +    /// _**Note:** For this event to be triggered, the executable needs to be properly [bundled]!_ +    /// +    /// [bundled]: https://developer.apple.com/library/archive/documentation/CoreFoundation/Conceptual/CFBundles/BundleTypes/BundleTypes.html#//apple_ref/doc/uid/10000123i-CH101-SW19 +    ReceivedUrl(String),  }  /// The status of an [`Event`] after being processed. diff --git a/native/src/layout.rs b/native/src/layout.rs index 6d144902..b4b4a021 100644 --- a/native/src/layout.rs +++ b/native/src/layout.rs @@ -19,11 +19,14 @@ pub struct Layout<'a> {  }  impl<'a> Layout<'a> { -    pub(crate) fn new(node: &'a Node) -> Self { +    /// Creates a new [`Layout`] for the given [`Node`] at the origin. +    pub fn new(node: &'a Node) -> Self {          Self::with_offset(Vector::new(0.0, 0.0), node)      } -    pub(crate) fn with_offset(offset: Vector, node: &'a Node) -> Self { +    /// Creates a new [`Layout`] for the given [`Node`] with the provided offset +    /// from the origin. +    pub fn with_offset(offset: Vector, node: &'a Node) -> Self {          let bounds = node.bounds();          Self { diff --git a/native/src/layout/flex.rs b/native/src/layout/flex.rs index 4f6523fb..3d3ff82c 100644 --- a/native/src/layout/flex.rs +++ b/native/src/layout/flex.rs @@ -16,9 +16,10 @@  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  // See the License for the specific language governing permissions and  // limitations under the License. +  use crate::{      layout::{Limits, Node}, -    Align, Element, Point, Size, +    Align, Element, Padding, Point, Size,  };  /// The main axis of a flex layout. @@ -62,7 +63,7 @@ pub fn resolve<Message, Renderer>(      axis: Axis,      renderer: &Renderer,      limits: &Limits, -    padding: f32, +    padding: Padding,      spacing: f32,      align_items: Align,      items: &[Element<'_, Message, Renderer>], @@ -141,14 +142,15 @@ where          }      } -    let mut main = padding; +    let pad = axis.pack(padding.left as f32, padding.top as f32); +    let mut main = pad.0;      for (i, node) in nodes.iter_mut().enumerate() {          if i > 0 {              main += spacing;          } -        let (x, y) = axis.pack(main, padding); +        let (x, y) = axis.pack(main, pad.1);          node.move_to(Point::new(x, y)); @@ -166,7 +168,7 @@ where          main += axis.main(size);      } -    let (width, height) = axis.pack(main - padding, cross); +    let (width, height) = axis.pack(main - pad.0, cross);      let size = limits.resolve(Size::new(width, height));      Node::with_children(size.pad(padding), nodes) diff --git a/native/src/layout/limits.rs b/native/src/layout/limits.rs index a7bb5c9c..6d5f6563 100644 --- a/native/src/layout/limits.rs +++ b/native/src/layout/limits.rs @@ -1,4 +1,4 @@ -use crate::{Length, Size}; +use crate::{Length, Padding, Size};  /// A set of size constraints for layouting.  #[derive(Debug, Clone, Copy)] @@ -117,8 +117,11 @@ impl Limits {      }      /// Shrinks the current [`Limits`] to account for the given padding. -    pub fn pad(&self, padding: f32) -> Limits { -        self.shrink(Size::new(padding * 2.0, padding * 2.0)) +    pub fn pad(&self, padding: Padding) -> Limits { +        self.shrink(Size::new( +            padding.horizontal() as f32, +            padding.vertical() as f32, +        ))      }      /// Shrinks the current [`Limits`] by the given [`Size`]. diff --git a/native/src/lib.rs b/native/src/lib.rs index f9a99c48..cbb02506 100644 --- a/native/src/lib.rs +++ b/native/src/lib.rs @@ -33,6 +33,7 @@  #![deny(unused_results)]  #![forbid(unsafe_code)]  #![forbid(rust_2018_idioms)] +pub mod clipboard;  pub mod event;  pub mod keyboard;  pub mod layout; @@ -41,10 +42,10 @@ pub mod overlay;  pub mod program;  pub mod renderer;  pub mod subscription; +pub mod touch;  pub mod widget;  pub mod window; -mod clipboard;  mod element;  mod hasher;  mod runtime; @@ -60,8 +61,8 @@ mod debug;  mod debug;  pub use iced_core::{ -    Align, Background, Color, Font, HorizontalAlignment, Length, Point, -    Rectangle, Size, Vector, VerticalAlignment, +    menu, Align, Background, Color, Font, HorizontalAlignment, Length, Menu, +    Padding, Point, Rectangle, Size, Vector, VerticalAlignment,  };  pub use iced_futures::{executor, futures, Command}; diff --git a/native/src/overlay.rs b/native/src/overlay.rs index ea8bb384..84145e7f 100644 --- a/native/src/overlay.rs +++ b/native/src/overlay.rs @@ -67,9 +67,9 @@ where          _event: Event,          _layout: Layout<'_>,          _cursor_position: Point, -        _messages: &mut Vec<Message>,          _renderer: &Renderer, -        _clipboard: Option<&dyn Clipboard>, +        _clipboard: &mut dyn Clipboard, +        _messages: &mut Vec<Message>,      ) -> event::Status {          event::Status::Ignored      } diff --git a/native/src/overlay/element.rs b/native/src/overlay/element.rs index 0f44a781..e4819037 100644 --- a/native/src/overlay/element.rs +++ b/native/src/overlay/element.rs @@ -53,17 +53,17 @@ where          event: Event,          layout: Layout<'_>,          cursor_position: Point, -        messages: &mut Vec<Message>,          renderer: &Renderer, -        clipboard: Option<&dyn Clipboard>, +        clipboard: &mut dyn Clipboard, +        messages: &mut Vec<Message>,      ) -> event::Status {          self.overlay.on_event(              event,              layout,              cursor_position, -            messages,              renderer,              clipboard, +            messages,          )      } @@ -117,9 +117,9 @@ where          event: Event,          layout: Layout<'_>,          cursor_position: Point, -        messages: &mut Vec<B>,          renderer: &Renderer, -        clipboard: Option<&dyn Clipboard>, +        clipboard: &mut dyn Clipboard, +        messages: &mut Vec<B>,      ) -> event::Status {          let mut original_messages = Vec::new(); @@ -127,9 +127,9 @@ where              event,              layout,              cursor_position, -            &mut original_messages,              renderer,              clipboard, +            &mut original_messages,          );          original_messages diff --git a/native/src/overlay/menu.rs b/native/src/overlay/menu.rs index abac849f..f62dcb46 100644 --- a/native/src/overlay/menu.rs +++ b/native/src/overlay/menu.rs @@ -6,9 +6,10 @@ use crate::mouse;  use crate::overlay;  use crate::scrollable;  use crate::text; +use crate::touch;  use crate::{ -    Clipboard, Container, Element, Hasher, Layout, Length, Point, Rectangle, -    Scrollable, Size, Vector, Widget, +    Clipboard, Container, Element, Hasher, Layout, Length, Padding, Point, +    Rectangle, Scrollable, Size, Vector, Widget,  };  /// A list of selectable options. @@ -19,7 +20,7 @@ pub struct Menu<'a, T, Renderer: self::Renderer> {      hovered_option: &'a mut Option<usize>,      last_selection: &'a mut Option<T>,      width: u16, -    padding: u16, +    padding: Padding,      text_size: Option<u16>,      font: Renderer::Font,      style: <Renderer as self::Renderer>::Style, @@ -44,7 +45,7 @@ where              hovered_option,              last_selection,              width: 0, -            padding: 0, +            padding: Padding::ZERO,              text_size: None,              font: Default::default(),              style: Default::default(), @@ -57,9 +58,9 @@ where          self      } -    /// Sets the padding of the [`Menu`]. -    pub fn padding(mut self, padding: u16) -> Self { -        self.padding = padding; +    /// Sets the [`Padding`] of the [`Menu`]. +    pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self { +        self.padding = padding.into();          self      } @@ -218,17 +219,17 @@ where          event: Event,          layout: Layout<'_>,          cursor_position: Point, -        messages: &mut Vec<Message>,          renderer: &Renderer, -        clipboard: Option<&dyn Clipboard>, +        clipboard: &mut dyn Clipboard, +        messages: &mut Vec<Message>,      ) -> event::Status {          self.container.on_event(              event.clone(),              layout,              cursor_position, -            messages,              renderer,              clipboard, +            messages,          )      } @@ -260,7 +261,7 @@ struct List<'a, T, Renderer: self::Renderer> {      options: &'a [T],      hovered_option: &'a mut Option<usize>,      last_selection: &'a mut Option<T>, -    padding: u16, +    padding: Padding,      text_size: Option<u16>,      font: Renderer::Font,      style: <Renderer as self::Renderer>::Style, @@ -293,7 +294,7 @@ where          let size = {              let intrinsic = Size::new(                  0.0, -                f32::from(text_size + self.padding * 2) +                f32::from(text_size + self.padding.vertical())                      * self.options.len() as f32,              ); @@ -319,9 +320,9 @@ where          event: Event,          layout: Layout<'_>,          cursor_position: Point, -        _messages: &mut Vec<Message>,          renderer: &Renderer, -        _clipboard: Option<&dyn Clipboard>, +        _clipboard: &mut dyn Clipboard, +        _messages: &mut Vec<Message>,      ) -> event::Status {          match event {              Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => { @@ -337,17 +338,38 @@ where              }              Event::Mouse(mouse::Event::CursorMoved { .. }) => {                  let bounds = layout.bounds(); -                let text_size = -                    self.text_size.unwrap_or(renderer.default_size());                  if bounds.contains(cursor_position) { +                    let text_size = +                        self.text_size.unwrap_or(renderer.default_size()); +                      *self.hovered_option = Some(                          ((cursor_position.y - bounds.y) -                            / f32::from(text_size + self.padding * 2)) +                            / f32::from(text_size + self.padding.vertical()))                              as usize,                      );                  }              } +            Event::Touch(touch::Event::FingerPressed { .. }) => { +                let bounds = layout.bounds(); + +                if bounds.contains(cursor_position) { +                    let text_size = +                        self.text_size.unwrap_or(renderer.default_size()); + +                    *self.hovered_option = Some( +                        ((cursor_position.y - bounds.y) +                            / f32::from(text_size + self.padding.vertical())) +                            as usize, +                    ); + +                    if let Some(index) = *self.hovered_option { +                        if let Some(option) = self.options.get(index) { +                            *self.last_selection = Some(option.clone()); +                        } +                    } +                } +            }              _ => {}          } @@ -408,7 +430,7 @@ pub trait Renderer:          viewport: &Rectangle,          options: &[T],          hovered_option: Option<usize>, -        padding: u16, +        padding: Padding,          text_size: u16,          font: Self::Font,          style: &<Self as Renderer>::Style, diff --git a/native/src/program.rs b/native/src/program.rs index 9ee72703..75fab094 100644 --- a/native/src/program.rs +++ b/native/src/program.rs @@ -1,5 +1,5 @@  //! Build interactive programs using The Elm Architecture. -use crate::{Command, Element, Renderer}; +use crate::{Clipboard, Command, Element, Renderer};  mod state; @@ -11,7 +11,10 @@ pub trait Program: Sized {      type Renderer: Renderer;      /// The type of __messages__ your [`Program`] will produce. -    type Message: std::fmt::Debug + Send; +    type Message: std::fmt::Debug + Clone + Send; + +    /// The type of [`Clipboard`] your [`Program`] will use. +    type Clipboard: Clipboard;      /// Handles a __message__ and updates the state of the [`Program`].      /// @@ -21,7 +24,11 @@ pub trait Program: Sized {      ///      /// Any [`Command`] returned will be executed immediately in the      /// background by shells. -    fn update(&mut self, message: Self::Message) -> Command<Self::Message>; +    fn update( +        &mut self, +        message: Self::Message, +        clipboard: &mut Self::Clipboard, +    ) -> Command<Self::Message>;      /// Returns the widgets to display in the [`Program`].      /// diff --git a/native/src/program/state.rs b/native/src/program/state.rs index e630890a..fd1f2b52 100644 --- a/native/src/program/state.rs +++ b/native/src/program/state.rs @@ -1,6 +1,5 @@  use crate::{ -    Cache, Clipboard, Command, Debug, Event, Point, Program, Renderer, Size, -    UserInterface, +    Cache, Command, Debug, Event, Point, Program, Renderer, Size, UserInterface,  };  /// The execution state of a [`Program`]. It leverages caching, event @@ -91,8 +90,8 @@ where          &mut self,          bounds: Size,          cursor_position: Point, -        clipboard: Option<&dyn Clipboard>,          renderer: &mut P::Renderer, +        clipboard: &mut P::Clipboard,          debug: &mut Debug,      ) -> Option<Command<P::Message>> {          let mut user_interface = build_user_interface( @@ -109,8 +108,8 @@ where          let _ = user_interface.update(              &self.queued_events,              cursor_position, -            clipboard,              renderer, +            clipboard,              &mut messages,          ); @@ -136,7 +135,7 @@ where                      debug.log_message(&message);                      debug.update_started(); -                    let command = self.program.update(message); +                    let command = self.program.update(message, clipboard);                      debug.update_finished();                      command diff --git a/native/src/renderer/null.rs b/native/src/renderer/null.rs index 91ee9a28..bb57c163 100644 --- a/native/src/renderer/null.rs +++ b/native/src/renderer/null.rs @@ -1,7 +1,7 @@  use crate::{      button, checkbox, column, container, pane_grid, progress_bar, radio, row, -    scrollable, slider, text, text_input, Color, Element, Font, -    HorizontalAlignment, Layout, Point, Rectangle, Renderer, Size, +    scrollable, slider, text, text_input, toggler, Color, Element, Font, +    HorizontalAlignment, Layout, Padding, Point, Rectangle, Renderer, Size,      VerticalAlignment,  }; @@ -145,7 +145,7 @@ impl text_input::Renderer for Null {  }  impl button::Renderer for Null { -    const DEFAULT_PADDING: u16 = 0; +    const DEFAULT_PADDING: Padding = Padding::ZERO;      type Style = (); @@ -246,14 +246,18 @@ impl container::Renderer for Null {  }  impl pane_grid::Renderer for Null { +    type Style = (); +      fn draw<Message>(          &mut self,          _defaults: &Self::Defaults,          _content: &[(pane_grid::Pane, pane_grid::Content<'_, Message, Self>)],          _dragging: Option<(pane_grid::Pane, Point)>, -        _resizing: Option<pane_grid::Axis>, +        _resizing: Option<(pane_grid::Axis, Rectangle, bool)>,          _layout: Layout<'_>, +        _style: &<Self as pane_grid::Renderer>::Style,          _cursor_position: Point, +        _viewport: &Rectangle,      ) {      } @@ -261,13 +265,14 @@ impl pane_grid::Renderer for Null {          &mut self,          _defaults: &Self::Defaults,          _bounds: Rectangle, -        _style: &Self::Style, +        _style: &<Self as container::Renderer>::Style,          _title_bar: Option<(              &pane_grid::TitleBar<'_, Message, Self>,              Layout<'_>,          )>,          _body: (&Element<'_, Message, Self>, Layout<'_>),          _cursor_position: Point, +        _viewport: &Rectangle,      ) {      } @@ -275,13 +280,27 @@ impl pane_grid::Renderer for Null {          &mut self,          _defaults: &Self::Defaults,          _bounds: Rectangle, -        _style: &Self::Style, -        _title: &str, -        _title_size: u16, -        _title_font: Self::Font, -        _title_bounds: Rectangle, +        _style: &<Self as container::Renderer>::Style, +        _content: (&Element<'_, Message, Self>, Layout<'_>),          _controls: Option<(&Element<'_, Message, Self>, Layout<'_>)>,          _cursor_position: Point, +        _viewport: &Rectangle, +    ) { +    } +} + +impl toggler::Renderer for Null { +    type Style = (); + +    const DEFAULT_SIZE: u16 = 20; + +    fn draw( +        &mut self, +        _bounds: Rectangle, +        _is_checked: bool, +        _is_mouse_over: bool, +        _label: Option<Self::Output>, +        _style: &Self::Style,      ) {      }  } diff --git a/native/src/touch.rs b/native/src/touch.rs new file mode 100644 index 00000000..18120644 --- /dev/null +++ b/native/src/touch.rs @@ -0,0 +1,23 @@ +//! Build touch events. +use crate::Point; + +/// A touch interaction. +#[derive(Debug, Clone, Copy, PartialEq)] +#[allow(missing_docs)] +pub enum Event { +    /// A touch interaction was started. +    FingerPressed { id: Finger, position: Point }, + +    /// An on-going touch interaction was moved. +    FingerMoved { id: Finger, position: Point }, + +    /// A touch interaction was ended. +    FingerLifted { id: Finger, position: Point }, + +    /// A touch interaction was canceled. +    FingerLost { id: Finger, position: Point }, +} + +/// A unique identifier representing a finger on a touch interaction. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct Finger(pub u64); diff --git a/native/src/user_interface.rs b/native/src/user_interface.rs index 7a64ac59..8e0d7d1c 100644 --- a/native/src/user_interface.rs +++ b/native/src/user_interface.rs @@ -16,7 +16,7 @@ use std::hash::Hasher;  /// The [`integration` example] uses a [`UserInterface`] to integrate Iced in  /// an existing graphical application.  /// -/// [`integration` example]: https://github.com/hecrj/iced/tree/0.2/examples/integration +/// [`integration` example]: https://github.com/hecrj/iced/tree/0.3/examples/integration  #[allow(missing_debug_implementations)]  pub struct UserInterface<'a, Message, Renderer> {      root: Element<'a, Message, Renderer>, @@ -134,7 +134,7 @@ where      /// completing [the previous example](#example):      ///      /// ```no_run -    /// use iced_native::{UserInterface, Cache, Size, Point}; +    /// use iced_native::{clipboard, UserInterface, Cache, Size, Point};      /// use iced_wgpu::Renderer;      ///      /// # mod iced_wgpu { @@ -157,6 +157,7 @@ where      /// let mut renderer = Renderer::new();      /// let mut window_size = Size::new(1024.0, 768.0);      /// let mut cursor_position = Point::default(); +    /// let mut clipboard = clipboard::Null;      ///      /// // Initialize our event storage      /// let mut events = Vec::new(); @@ -176,8 +177,8 @@ where      ///     let event_statuses = user_interface.update(      ///         &events,      ///         cursor_position, -    ///         None,      ///         &renderer, +    ///         &mut clipboard,      ///         &mut messages      ///     );      /// @@ -193,8 +194,8 @@ where          &mut self,          events: &[Event],          cursor_position: Point, -        clipboard: Option<&dyn Clipboard>,          renderer: &Renderer, +        clipboard: &mut dyn Clipboard,          messages: &mut Vec<Message>,      ) -> Vec<event::Status> {          let (base_cursor, overlay_statuses) = if let Some(mut overlay) = @@ -215,9 +216,9 @@ where                          event,                          Layout::new(&layer.layout),                          cursor_position, -                        messages,                          renderer,                          clipboard, +                        messages,                      )                  })                  .collect(); @@ -246,9 +247,9 @@ where                      event,                      Layout::new(&self.base.layout),                      base_cursor, -                    messages,                      renderer,                      clipboard, +                    messages,                  );                  event_status.merge(overlay_status) @@ -269,7 +270,7 @@ where      /// [completing the last example](#example-1):      ///      /// ```no_run -    /// use iced_native::{UserInterface, Cache, Size, Point}; +    /// use iced_native::{clipboard, UserInterface, Cache, Size, Point};      /// use iced_wgpu::Renderer;      ///      /// # mod iced_wgpu { @@ -292,6 +293,7 @@ where      /// let mut renderer = Renderer::new();      /// let mut window_size = Size::new(1024.0, 768.0);      /// let mut cursor_position = Point::default(); +    /// let mut clipboard = clipboard::Null;      /// let mut events = Vec::new();      /// let mut messages = Vec::new();      /// @@ -309,8 +311,8 @@ where      ///     let event_statuses = user_interface.update(      ///         &events,      ///         cursor_position, -    ///         None,      ///         &renderer, +    ///         &mut clipboard,      ///         &mut messages      ///     );      /// diff --git a/native/src/widget.rs b/native/src/widget.rs index 3677713a..43c1b023 100644 --- a/native/src/widget.rs +++ b/native/src/widget.rs @@ -36,6 +36,8 @@ pub mod space;  pub mod svg;  pub mod text;  pub mod text_input; +pub mod toggler; +pub mod tooltip;  #[doc(no_inline)]  pub use button::Button; @@ -71,6 +73,10 @@ pub use svg::Svg;  pub use text::Text;  #[doc(no_inline)]  pub use text_input::TextInput; +#[doc(no_inline)] +pub use toggler::Toggler; +#[doc(no_inline)] +pub use tooltip::Tooltip;  use crate::event::{self, Event};  use crate::layout; @@ -93,12 +99,12 @@ use crate::{Clipboard, Hasher, Layout, Length, Point, Rectangle};  /// - [`geometry`], a custom widget showcasing how to draw geometry with the  /// `Mesh2D` primitive in [`iced_wgpu`].  /// -/// [examples]: https://github.com/hecrj/iced/tree/0.2/examples -/// [`bezier_tool`]: https://github.com/hecrj/iced/tree/0.2/examples/bezier_tool -/// [`custom_widget`]: https://github.com/hecrj/iced/tree/0.2/examples/custom_widget -/// [`geometry`]: https://github.com/hecrj/iced/tree/0.2/examples/geometry +/// [examples]: https://github.com/hecrj/iced/tree/0.3/examples +/// [`bezier_tool`]: https://github.com/hecrj/iced/tree/0.3/examples/bezier_tool +/// [`custom_widget`]: https://github.com/hecrj/iced/tree/0.3/examples/custom_widget +/// [`geometry`]: https://github.com/hecrj/iced/tree/0.3/examples/geometry  /// [`lyon`]: https://github.com/nical/lyon -/// [`iced_wgpu`]: https://github.com/hecrj/iced/tree/0.2/wgpu +/// [`iced_wgpu`]: https://github.com/hecrj/iced/tree/0.3/wgpu  pub trait Widget<Message, Renderer>  where      Renderer: crate::Renderer, @@ -161,9 +167,9 @@ where          _event: Event,          _layout: Layout<'_>,          _cursor_position: Point, -        _messages: &mut Vec<Message>,          _renderer: &Renderer, -        _clipboard: Option<&dyn Clipboard>, +        _clipboard: &mut dyn Clipboard, +        _messages: &mut Vec<Message>,      ) -> event::Status {          event::Status::Ignored      } diff --git a/native/src/widget/button.rs b/native/src/widget/button.rs index dca20e13..c469a0e5 100644 --- a/native/src/widget/button.rs +++ b/native/src/widget/button.rs @@ -4,8 +4,11 @@  use crate::event::{self, Event};  use crate::layout;  use crate::mouse; +use crate::overlay; +use crate::touch;  use crate::{ -    Clipboard, Element, Hasher, Layout, Length, Point, Rectangle, Widget, +    Clipboard, Element, Hasher, Layout, Length, Padding, Point, Rectangle, +    Widget,  };  use std::hash::Hash; @@ -26,6 +29,29 @@ use std::hash::Hash;  /// let button = Button::new(&mut state, Text::new("Press me!"))  ///     .on_press(Message::ButtonPressed);  /// ``` +/// +/// If a [`Button::on_press`] handler is not set, the resulting [`Button`] will +/// be disabled: +/// +/// ``` +/// # use iced_native::{button, Text}; +/// # +/// # type Button<'a, Message> = +/// #     iced_native::Button<'a, Message, iced_native::renderer::Null>; +/// # +/// #[derive(Clone)] +/// enum Message { +///     ButtonPressed, +/// } +/// +/// fn disabled_button(state: &mut button::State) -> Button<'_, Message> { +///     Button::new(state, Text::new("I'm disabled!")) +/// } +/// +/// fn enabled_button(state: &mut button::State) -> Button<'_, Message> { +///     disabled_button(state).on_press(Message::ButtonPressed) +/// } +/// ```  #[allow(missing_debug_implementations)]  pub struct Button<'a, Message, Renderer: self::Renderer> {      state: &'a mut State, @@ -35,7 +61,7 @@ pub struct Button<'a, Message, Renderer: self::Renderer> {      height: Length,      min_width: u32,      min_height: u32, -    padding: u16, +    padding: Padding,      style: Renderer::Style,  } @@ -87,13 +113,14 @@ where          self      } -    /// Sets the padding of the [`Button`]. -    pub fn padding(mut self, padding: u16) -> Self { -        self.padding = padding; +    /// Sets the [`Padding`] of the [`Button`]. +    pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self { +        self.padding = padding.into();          self      }      /// Sets the message that will be produced when the [`Button`] is pressed. +    /// If on_press isn't set, button will be disabled.      pub fn on_press(mut self, msg: Message) -> Self {          self.on_press = Some(msg);          self @@ -138,18 +165,20 @@ where          renderer: &Renderer,          limits: &layout::Limits,      ) -> layout::Node { -        let padding = f32::from(self.padding);          let limits = limits              .min_width(self.min_width)              .min_height(self.min_height)              .width(self.width)              .height(self.height) -            .pad(padding); +            .pad(self.padding);          let mut content = self.content.layout(renderer, &limits); -        content.move_to(Point::new(padding, padding)); +        content.move_to(Point::new( +            self.padding.left.into(), +            self.padding.top.into(), +        )); -        let size = limits.resolve(content.size()).pad(padding); +        let size = limits.resolve(content.size()).pad(self.padding);          layout::Node::with_children(size, vec![content])      } @@ -159,12 +188,24 @@ where          event: Event,          layout: Layout<'_>,          cursor_position: Point, +        renderer: &Renderer, +        clipboard: &mut dyn Clipboard,          messages: &mut Vec<Message>, -        _renderer: &Renderer, -        _clipboard: Option<&dyn Clipboard>,      ) -> event::Status { +        if let event::Status::Captured = self.content.on_event( +            event.clone(), +            layout.children().next().unwrap(), +            cursor_position, +            renderer, +            clipboard, +            messages, +        ) { +            return event::Status::Captured; +        } +          match event { -            Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => { +            Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) +            | Event::Touch(touch::Event::FingerPressed { .. }) => {                  if self.on_press.is_some() {                      let bounds = layout.bounds(); @@ -175,7 +216,8 @@ where                      }                  }              } -            Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) => { +            Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) +            | Event::Touch(touch::Event::FingerLifted { .. }) => {                  if let Some(on_press) = self.on_press.clone() {                      let bounds = layout.bounds(); @@ -190,6 +232,9 @@ where                      }                  }              } +            Event::Touch(touch::Event::FingerLost { .. }) => { +                self.state.is_pressed = false; +            }              _ => {}          } @@ -223,6 +268,13 @@ where          self.width.hash(state);          self.content.hash_layout(state);      } + +    fn overlay( +        &mut self, +        layout: Layout<'_>, +    ) -> Option<overlay::Element<'_, Message, Renderer>> { +        self.content.overlay(layout.children().next().unwrap()) +    }  }  /// The renderer of a [`Button`]. @@ -233,7 +285,7 @@ where  /// [renderer]: crate::renderer  pub trait Renderer: crate::Renderer + Sized {      /// The default padding of a [`Button`]. -    const DEFAULT_PADDING: u16; +    const DEFAULT_PADDING: Padding;      /// The style supported by this renderer.      type Style: Default; diff --git a/native/src/widget/checkbox.rs b/native/src/widget/checkbox.rs index 81420458..0f21c873 100644 --- a/native/src/widget/checkbox.rs +++ b/native/src/widget/checkbox.rs @@ -6,9 +6,10 @@ use crate::layout;  use crate::mouse;  use crate::row;  use crate::text; +use crate::touch;  use crate::{ -    Align, Clipboard, Element, Hasher, HorizontalAlignment, Layout, Length, -    Point, Rectangle, Row, Text, VerticalAlignment, Widget, +    Align, Clipboard, Color, Element, Hasher, HorizontalAlignment, Layout, +    Length, Point, Rectangle, Row, Text, VerticalAlignment, Widget,  };  /// A box that can be checked. @@ -38,6 +39,7 @@ pub struct Checkbox<Message, Renderer: self::Renderer + text::Renderer> {      spacing: u16,      text_size: Option<u16>,      font: Renderer::Font, +    text_color: Option<Color>,      style: Renderer::Style,  } @@ -65,6 +67,7 @@ impl<Message, Renderer: self::Renderer + text::Renderer>              spacing: Renderer::DEFAULT_SPACING,              text_size: None,              font: Renderer::Font::default(), +            text_color: None,              style: Renderer::Style::default(),          }      } @@ -101,6 +104,12 @@ impl<Message, Renderer: self::Renderer + text::Renderer>          self      } +    /// Sets the text color of the [`Checkbox`] button. +    pub fn text_color(mut self, color: Color) -> Self { +        self.text_color = Some(color); +        self +    } +      /// Sets the style of the [`Checkbox`].      pub fn style(mut self, style: impl Into<Renderer::Style>) -> Self {          self.style = style.into(); @@ -149,12 +158,13 @@ where          event: Event,          layout: Layout<'_>,          cursor_position: Point, -        messages: &mut Vec<Message>,          _renderer: &Renderer, -        _clipboard: Option<&dyn Clipboard>, +        _clipboard: &mut dyn Clipboard, +        messages: &mut Vec<Message>,      ) -> event::Status {          match event { -            Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => { +            Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) +            | Event::Touch(touch::Event::FingerPressed { .. }) => {                  let mouse_over = layout.bounds().contains(cursor_position);                  if mouse_over { @@ -191,7 +201,7 @@ where              &self.label,              self.text_size.unwrap_or(renderer.default_size()),              self.font, -            None, +            self.text_color,              HorizontalAlignment::Left,              VerticalAlignment::Center,          ); diff --git a/native/src/widget/column.rs b/native/src/widget/column.rs index e0e88d31..52a2e80c 100644 --- a/native/src/widget/column.rs +++ b/native/src/widget/column.rs @@ -5,7 +5,8 @@ use crate::event::{self, Event};  use crate::layout;  use crate::overlay;  use crate::{ -    Align, Clipboard, Element, Hasher, Layout, Length, Point, Rectangle, Widget, +    Align, Clipboard, Element, Hasher, Layout, Length, Padding, Point, +    Rectangle, Widget,  };  use std::u32; @@ -14,7 +15,7 @@ use std::u32;  #[allow(missing_debug_implementations)]  pub struct Column<'a, Message, Renderer> {      spacing: u16, -    padding: u16, +    padding: Padding,      width: Length,      height: Length,      max_width: u32, @@ -35,7 +36,7 @@ impl<'a, Message, Renderer> Column<'a, Message, Renderer> {      ) -> Self {          Column {              spacing: 0, -            padding: 0, +            padding: Padding::ZERO,              width: Length::Shrink,              height: Length::Shrink,              max_width: u32::MAX, @@ -55,9 +56,9 @@ impl<'a, Message, Renderer> Column<'a, Message, Renderer> {          self      } -    /// Sets the padding of the [`Column`]. -    pub fn padding(mut self, units: u16) -> Self { -        self.padding = units; +    /// Sets the [`Padding`] of the [`Column`]. +    pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self { +        self.padding = padding.into();          self      } @@ -129,7 +130,7 @@ where              layout::flex::Axis::Vertical,              renderer,              &limits, -            self.padding as f32, +            self.padding,              self.spacing as f32,              self.align_items,              &self.children, @@ -141,9 +142,9 @@ where          event: Event,          layout: Layout<'_>,          cursor_position: Point, -        messages: &mut Vec<Message>,          renderer: &Renderer, -        clipboard: Option<&dyn Clipboard>, +        clipboard: &mut dyn Clipboard, +        messages: &mut Vec<Message>,      ) -> event::Status {          self.children              .iter_mut() @@ -153,9 +154,9 @@ where                      event.clone(),                      layout,                      cursor_position, -                    messages,                      renderer,                      clipboard, +                    messages,                  )              })              .fold(event::Status::Ignored, event::Status::merge) diff --git a/native/src/widget/container.rs b/native/src/widget/container.rs index 65764148..69aee64d 100644 --- a/native/src/widget/container.rs +++ b/native/src/widget/container.rs @@ -5,7 +5,8 @@ use crate::event::{self, Event};  use crate::layout;  use crate::overlay;  use crate::{ -    Align, Clipboard, Element, Hasher, Layout, Length, Point, Rectangle, Widget, +    Align, Clipboard, Element, Hasher, Layout, Length, Padding, Point, +    Rectangle, Widget,  };  use std::u32; @@ -15,7 +16,7 @@ use std::u32;  /// It is normally used for alignment purposes.  #[allow(missing_debug_implementations)]  pub struct Container<'a, Message, Renderer: self::Renderer> { -    padding: u16, +    padding: Padding,      width: Length,      height: Length,      max_width: u32, @@ -36,7 +37,7 @@ where          T: Into<Element<'a, Message, Renderer>>,      {          Container { -            padding: 0, +            padding: Padding::ZERO,              width: Length::Shrink,              height: Length::Shrink,              max_width: u32::MAX, @@ -48,9 +49,9 @@ where          }      } -    /// Sets the padding of the [`Container`]. -    pub fn padding(mut self, units: u16) -> Self { -        self.padding = units; +    /// Sets the [`Padding`] of the [`Container`]. +    pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self { +        self.padding = padding.into();          self      } @@ -127,23 +128,24 @@ where          renderer: &Renderer,          limits: &layout::Limits,      ) -> layout::Node { -        let padding = f32::from(self.padding); -          let limits = limits              .loose()              .max_width(self.max_width)              .max_height(self.max_height)              .width(self.width)              .height(self.height) -            .pad(padding); +            .pad(self.padding);          let mut content = self.content.layout(renderer, &limits.loose());          let size = limits.resolve(content.size()); -        content.move_to(Point::new(padding, padding)); +        content.move_to(Point::new( +            self.padding.left.into(), +            self.padding.top.into(), +        ));          content.align(self.horizontal_alignment, self.vertical_alignment, size); -        layout::Node::with_children(size.pad(padding), vec![content]) +        layout::Node::with_children(size.pad(self.padding), vec![content])      }      fn on_event( @@ -151,17 +153,17 @@ where          event: Event,          layout: Layout<'_>,          cursor_position: Point, -        messages: &mut Vec<Message>,          renderer: &Renderer, -        clipboard: Option<&dyn Clipboard>, +        clipboard: &mut dyn Clipboard, +        messages: &mut Vec<Message>,      ) -> event::Status {          self.content.widget.on_event(              event,              layout.children().next().unwrap(),              cursor_position, -            messages,              renderer,              clipboard, +            messages,          )      } diff --git a/native/src/widget/image.rs b/native/src/widget/image.rs index 51d7ba26..4d8e0a3f 100644 --- a/native/src/widget/image.rs +++ b/native/src/widget/image.rs @@ -1,4 +1,7 @@  //! Display images in your user interface. +pub mod viewer; +pub use viewer::Viewer; +  use crate::layout;  use crate::{Element, Hasher, Layout, Length, Point, Rectangle, Size, Widget}; diff --git a/native/src/widget/image/viewer.rs b/native/src/widget/image/viewer.rs new file mode 100644 index 00000000..405daf00 --- /dev/null +++ b/native/src/widget/image/viewer.rs @@ -0,0 +1,413 @@ +//! Zoom and pan on an image. +use crate::event::{self, Event}; +use crate::image; +use crate::layout; +use crate::mouse; +use crate::{ +    Clipboard, Element, Hasher, Layout, Length, Point, Rectangle, Size, Vector, +    Widget, +}; + +use std::hash::Hash; + +/// A frame that displays an image with the ability to zoom in/out and pan. +#[allow(missing_debug_implementations)] +pub struct Viewer<'a> { +    state: &'a mut State, +    padding: u16, +    width: Length, +    height: Length, +    min_scale: f32, +    max_scale: f32, +    scale_step: f32, +    handle: image::Handle, +} + +impl<'a> Viewer<'a> { +    /// Creates a new [`Viewer`] with the given [`State`] and [`Handle`]. +    /// +    /// [`Handle`]: image::Handle +    pub fn new(state: &'a mut State, handle: image::Handle) -> Self { +        Viewer { +            state, +            padding: 0, +            width: Length::Shrink, +            height: Length::Shrink, +            min_scale: 0.25, +            max_scale: 10.0, +            scale_step: 0.10, +            handle, +        } +    } + +    /// Sets the padding of the [`Viewer`]. +    pub fn padding(mut self, units: u16) -> Self { +        self.padding = units; +        self +    } + +    /// Sets the width of the [`Viewer`]. +    pub fn width(mut self, width: Length) -> Self { +        self.width = width; +        self +    } + +    /// Sets the height of the [`Viewer`]. +    pub fn height(mut self, height: Length) -> Self { +        self.height = height; +        self +    } + +    /// Sets the max scale applied to the image of the [`Viewer`]. +    /// +    /// Default is `10.0` +    pub fn max_scale(mut self, max_scale: f32) -> Self { +        self.max_scale = max_scale; +        self +    } + +    /// Sets the min scale applied to the image of the [`Viewer`]. +    /// +    /// Default is `0.25` +    pub fn min_scale(mut self, min_scale: f32) -> Self { +        self.min_scale = min_scale; +        self +    } + +    /// Sets the percentage the image of the [`Viewer`] will be scaled by +    /// when zoomed in / out. +    /// +    /// Default is `0.10` +    pub fn scale_step(mut self, scale_step: f32) -> Self { +        self.scale_step = scale_step; +        self +    } + +    /// Returns the bounds of the underlying image, given the bounds of +    /// the [`Viewer`]. Scaling will be applied and original aspect ratio +    /// will be respected. +    fn image_size<Renderer>(&self, renderer: &Renderer, bounds: Size) -> Size +    where +        Renderer: self::Renderer + image::Renderer, +    { +        let (width, height) = renderer.dimensions(&self.handle); + +        let (width, height) = { +            let dimensions = (width as f32, height as f32); + +            let width_ratio = bounds.width / dimensions.0; +            let height_ratio = bounds.height / dimensions.1; + +            let ratio = width_ratio.min(height_ratio); + +            let scale = self.state.scale; + +            if ratio < 1.0 { +                (dimensions.0 * ratio * scale, dimensions.1 * ratio * scale) +            } else { +                (dimensions.0 * scale, dimensions.1 * scale) +            } +        }; + +        Size::new(width, height) +    } +} + +impl<'a, Message, Renderer> Widget<Message, Renderer> for Viewer<'a> +where +    Renderer: self::Renderer + image::Renderer, +{ +    fn width(&self) -> Length { +        self.width +    } + +    fn height(&self) -> Length { +        self.height +    } + +    fn layout( +        &self, +        renderer: &Renderer, +        limits: &layout::Limits, +    ) -> layout::Node { +        let (width, height) = renderer.dimensions(&self.handle); + +        let mut size = limits +            .width(self.width) +            .height(self.height) +            .resolve(Size::new(width as f32, height as f32)); + +        let expansion_size = if height > width { +            self.width +        } else { +            self.height +        }; + +        // Only calculate viewport sizes if the images are constrained to a limited space. +        // If they are Fill|Portion let them expand within their alotted space. +        match expansion_size { +            Length::Shrink | Length::Units(_) => { +                let aspect_ratio = width as f32 / height as f32; +                let viewport_aspect_ratio = size.width / size.height; +                if viewport_aspect_ratio > aspect_ratio { +                    size.width = width as f32 * size.height / height as f32; +                } else { +                    size.height = height as f32 * size.width / width as f32; +                } +            } +            Length::Fill | Length::FillPortion(_) => {} +        } + +        layout::Node::new(size) +    } + +    fn on_event( +        &mut self, +        event: Event, +        layout: Layout<'_>, +        cursor_position: Point, +        renderer: &Renderer, +        _clipboard: &mut dyn Clipboard, +        _messages: &mut Vec<Message>, +    ) -> event::Status { +        let bounds = layout.bounds(); +        let is_mouse_over = bounds.contains(cursor_position); + +        match event { +            Event::Mouse(mouse::Event::WheelScrolled { delta }) +                if is_mouse_over => +            { +                match delta { +                    mouse::ScrollDelta::Lines { y, .. } +                    | mouse::ScrollDelta::Pixels { y, .. } => { +                        let previous_scale = self.state.scale; + +                        if y < 0.0 && previous_scale > self.min_scale +                            || y > 0.0 && previous_scale < self.max_scale +                        { +                            self.state.scale = (if y > 0.0 { +                                self.state.scale * (1.0 + self.scale_step) +                            } else { +                                self.state.scale / (1.0 + self.scale_step) +                            }) +                            .max(self.min_scale) +                            .min(self.max_scale); + +                            let image_size = +                                self.image_size(renderer, bounds.size()); + +                            let factor = +                                self.state.scale / previous_scale - 1.0; + +                            let cursor_to_center = +                                cursor_position - bounds.center(); + +                            let adjustment = cursor_to_center * factor +                                + self.state.current_offset * factor; + +                            self.state.current_offset = Vector::new( +                                if image_size.width > bounds.width { +                                    self.state.current_offset.x + adjustment.x +                                } else { +                                    0.0 +                                }, +                                if image_size.height > bounds.height { +                                    self.state.current_offset.y + adjustment.y +                                } else { +                                    0.0 +                                }, +                            ); +                        } +                    } +                } + +                event::Status::Captured +            } +            Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) +                if is_mouse_over => +            { +                self.state.cursor_grabbed_at = Some(cursor_position); +                self.state.starting_offset = self.state.current_offset; + +                event::Status::Captured +            } +            Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) +                if self.state.cursor_grabbed_at.is_some() => +            { +                self.state.cursor_grabbed_at = None; + +                event::Status::Captured +            } +            Event::Mouse(mouse::Event::CursorMoved { position }) => { +                if let Some(origin) = self.state.cursor_grabbed_at { +                    let image_size = self.image_size(renderer, bounds.size()); + +                    let hidden_width = (image_size.width - bounds.width / 2.0) +                        .max(0.0) +                        .round(); + +                    let hidden_height = (image_size.height +                        - bounds.height / 2.0) +                        .max(0.0) +                        .round(); + +                    let delta = position - origin; + +                    let x = if bounds.width < image_size.width { +                        (self.state.starting_offset.x - delta.x) +                            .min(hidden_width) +                            .max(-hidden_width) +                    } else { +                        0.0 +                    }; + +                    let y = if bounds.height < image_size.height { +                        (self.state.starting_offset.y - delta.y) +                            .min(hidden_height) +                            .max(-hidden_height) +                    } else { +                        0.0 +                    }; + +                    self.state.current_offset = Vector::new(x, y); + +                    event::Status::Captured +                } else { +                    event::Status::Ignored +                } +            } +            _ => event::Status::Ignored, +        } +    } + +    fn draw( +        &self, +        renderer: &mut Renderer, +        _defaults: &Renderer::Defaults, +        layout: Layout<'_>, +        cursor_position: Point, +        _viewport: &Rectangle, +    ) -> Renderer::Output { +        let bounds = layout.bounds(); + +        let image_size = self.image_size(renderer, bounds.size()); + +        let translation = { +            let image_top_left = Vector::new( +                bounds.width / 2.0 - image_size.width / 2.0, +                bounds.height / 2.0 - image_size.height / 2.0, +            ); + +            image_top_left - self.state.offset(bounds, image_size) +        }; + +        let is_mouse_over = bounds.contains(cursor_position); + +        self::Renderer::draw( +            renderer, +            &self.state, +            bounds, +            image_size, +            translation, +            self.handle.clone(), +            is_mouse_over, +        ) +    } + +    fn hash_layout(&self, state: &mut Hasher) { +        struct Marker; +        std::any::TypeId::of::<Marker>().hash(state); + +        self.width.hash(state); +        self.height.hash(state); +        self.padding.hash(state); + +        self.handle.hash(state); +    } +} + +/// The local state of a [`Viewer`]. +#[derive(Debug, Clone, Copy)] +pub struct State { +    scale: f32, +    starting_offset: Vector, +    current_offset: Vector, +    cursor_grabbed_at: Option<Point>, +} + +impl Default for State { +    fn default() -> Self { +        Self { +            scale: 1.0, +            starting_offset: Vector::default(), +            current_offset: Vector::default(), +            cursor_grabbed_at: None, +        } +    } +} + +impl State { +    /// Creates a new [`State`]. +    pub fn new() -> Self { +        State::default() +    } + +    /// Returns the current offset of the [`State`], given the bounds +    /// of the [`Viewer`] and its image. +    fn offset(&self, bounds: Rectangle, image_size: Size) -> Vector { +        let hidden_width = +            (image_size.width - bounds.width / 2.0).max(0.0).round(); + +        let hidden_height = +            (image_size.height - bounds.height / 2.0).max(0.0).round(); + +        Vector::new( +            self.current_offset.x.min(hidden_width).max(-hidden_width), +            self.current_offset.y.min(hidden_height).max(-hidden_height), +        ) +    } + +    /// Returns if the cursor is currently grabbed by the [`Viewer`]. +    pub fn is_cursor_grabbed(&self) -> bool { +        self.cursor_grabbed_at.is_some() +    } +} + +/// The renderer of an [`Viewer`]. +/// +/// Your [renderer] will need to implement this trait before being +/// able to use a [`Viewer`] in your user interface. +/// +/// [renderer]: crate::renderer +pub trait Renderer: crate::Renderer + Sized { +    /// Draws the [`Viewer`]. +    /// +    /// It receives: +    /// - the [`State`] of the [`Viewer`] +    /// - the bounds of the [`Viewer`] widget +    /// - the [`Size`] of the scaled [`Viewer`] image +    /// - the translation of the clipped image +    /// - the [`Handle`] to the underlying image +    /// - whether the mouse is over the [`Viewer`] or not +    /// +    /// [`Handle`]: image::Handle +    fn draw( +        &mut self, +        state: &State, +        bounds: Rectangle, +        image_size: Size, +        translation: Vector, +        handle: image::Handle, +        is_mouse_over: bool, +    ) -> Self::Output; +} + +impl<'a, Message, Renderer> From<Viewer<'a>> for Element<'a, Message, Renderer> +where +    Renderer: 'a + self::Renderer + image::Renderer, +    Message: 'a, +{ +    fn from(viewer: Viewer<'a>) -> Element<'a, Message, Renderer> { +        Element::new(viewer) +    } +} diff --git a/native/src/widget/pane_grid.rs b/native/src/widget/pane_grid.rs index ff19cbc2..b72172cc 100644 --- a/native/src/widget/pane_grid.rs +++ b/native/src/widget/pane_grid.rs @@ -6,7 +6,7 @@  //! The [`pane_grid` example] showcases how to use a [`PaneGrid`] with resizing,  //! drag and drop, and hotkey support.  //! -//! [`pane_grid` example]: https://github.com/hecrj/iced/tree/0.2/examples/pane_grid +//! [`pane_grid` example]: https://github.com/hecrj/iced/tree/0.3/examples/pane_grid  mod axis;  mod configuration;  mod content; @@ -33,7 +33,7 @@ use crate::layout;  use crate::mouse;  use crate::overlay;  use crate::row; -use crate::text; +use crate::touch;  use crate::{      Clipboard, Element, Hasher, Layout, Length, Point, Rectangle, Size, Vector,      Widget, @@ -98,6 +98,7 @@ pub struct PaneGrid<'a, Message, Renderer: self::Renderer> {      on_click: Option<Box<dyn Fn(Pane) -> Message + 'a>>,      on_drag: Option<Box<dyn Fn(DragEvent) -> Message + 'a>>,      on_resize: Option<(u16, Box<dyn Fn(ResizeEvent) -> Message + 'a>)>, +    style: <Renderer as self::Renderer>::Style,  }  impl<'a, Message, Renderer> PaneGrid<'a, Message, Renderer> @@ -129,6 +130,7 @@ where              on_click: None,              on_drag: None,              on_resize: None, +            style: Default::default(),          }      } @@ -186,6 +188,15 @@ where          self.on_resize = Some((leeway, Box::new(f)));          self      } + +    /// Sets the style of the [`PaneGrid`]. +    pub fn style( +        mut self, +        style: impl Into<<Renderer as self::Renderer>::Style>, +    ) -> Self { +        self.style = style.into(); +        self +    }  }  impl<'a, Message, Renderer> PaneGrid<'a, Message, Renderer> @@ -351,49 +362,41 @@ where          event: Event,          layout: Layout<'_>,          cursor_position: Point, -        messages: &mut Vec<Message>,          renderer: &Renderer, -        clipboard: Option<&dyn Clipboard>, +        clipboard: &mut dyn Clipboard, +        messages: &mut Vec<Message>,      ) -> event::Status {          let mut event_status = event::Status::Ignored;          match event { -            Event::Mouse(mouse_event) => match mouse_event { -                mouse::Event::ButtonPressed(mouse::Button::Left) => { -                    let bounds = layout.bounds(); - -                    if bounds.contains(cursor_position) { -                        event_status = event::Status::Captured; - -                        match self.on_resize { -                            Some((leeway, _)) => { -                                let relative_cursor = Point::new( -                                    cursor_position.x - bounds.x, -                                    cursor_position.y - bounds.y, -                                ); - -                                let splits = self.state.split_regions( -                                    f32::from(self.spacing), -                                    Size::new(bounds.width, bounds.height), -                                ); - -                                let clicked_split = hovered_split( -                                    splits.iter(), -                                    f32::from(self.spacing + leeway), -                                    relative_cursor, -                                ); +            Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) +            | Event::Touch(touch::Event::FingerPressed { .. }) => { +                let bounds = layout.bounds(); -                                if let Some((split, axis)) = clicked_split { -                                    self.state.pick_split(&split, axis); -                                } else { -                                    self.click_pane( -                                        layout, -                                        cursor_position, -                                        messages, -                                    ); -                                } -                            } -                            None => { +                if bounds.contains(cursor_position) { +                    event_status = event::Status::Captured; + +                    match self.on_resize { +                        Some((leeway, _)) => { +                            let relative_cursor = Point::new( +                                cursor_position.x - bounds.x, +                                cursor_position.y - bounds.y, +                            ); + +                            let splits = self.state.split_regions( +                                f32::from(self.spacing), +                                Size::new(bounds.width, bounds.height), +                            ); + +                            let clicked_split = hovered_split( +                                splits.iter(), +                                f32::from(self.spacing + leeway), +                                relative_cursor, +                            ); + +                            if let Some((split, axis, _)) = clicked_split { +                                self.state.pick_split(&split, axis); +                            } else {                                  self.click_pane(                                      layout,                                      cursor_position, @@ -401,47 +404,51 @@ where                                  );                              }                          } +                        None => { +                            self.click_pane(layout, cursor_position, messages); +                        }                      }                  } -                mouse::Event::ButtonReleased(mouse::Button::Left) => { -                    if let Some((pane, _)) = self.state.picked_pane() { -                        if let Some(on_drag) = &self.on_drag { -                            let mut dropped_region = self -                                .elements -                                .iter() -                                .zip(layout.children()) -                                .filter(|(_, layout)| { +            } +            Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) +            | Event::Touch(touch::Event::FingerLifted { .. }) +            | Event::Touch(touch::Event::FingerLost { .. }) => { +                if let Some((pane, _)) = self.state.picked_pane() { +                    if let Some(on_drag) = &self.on_drag { +                        let mut dropped_region = +                            self.elements.iter().zip(layout.children()).filter( +                                |(_, layout)| {                                      layout.bounds().contains(cursor_position) -                                }); - -                            let event = match dropped_region.next() { -                                Some(((target, _), _)) if pane != *target => { -                                    DragEvent::Dropped { -                                        pane, -                                        target: *target, -                                    } +                                }, +                            ); + +                        let event = match dropped_region.next() { +                            Some(((target, _), _)) if pane != *target => { +                                DragEvent::Dropped { +                                    pane, +                                    target: *target,                                  } -                                _ => DragEvent::Canceled { pane }, -                            }; +                            } +                            _ => DragEvent::Canceled { pane }, +                        }; -                            messages.push(on_drag(event)); -                        } +                        messages.push(on_drag(event)); +                    } -                        self.state.idle(); +                    self.state.idle(); -                        event_status = event::Status::Captured; -                    } else if self.state.picked_split().is_some() { -                        self.state.idle(); +                    event_status = event::Status::Captured; +                } else if self.state.picked_split().is_some() { +                    self.state.idle(); -                        event_status = event::Status::Captured; -                    } -                } -                mouse::Event::CursorMoved { .. } => { -                    event_status = -                        self.trigger_resize(layout, cursor_position, messages); +                    event_status = event::Status::Captured;                  } -                _ => {} -            }, +            } +            Event::Mouse(mouse::Event::CursorMoved { .. }) +            | Event::Touch(touch::Event::FingerMoved { .. }) => { +                event_status = +                    self.trigger_resize(layout, cursor_position, messages); +            }              _ => {}          } @@ -454,9 +461,9 @@ where                          event.clone(),                          layout,                          cursor_position, -                        messages,                          renderer,                          clipboard, +                        messages,                      )                  })                  .fold(event_status, event::Status::merge) @@ -471,11 +478,28 @@ where          defaults: &Renderer::Defaults,          layout: Layout<'_>,          cursor_position: Point, -        _viewport: &Rectangle, +        viewport: &Rectangle,      ) -> Renderer::Output {          let picked_split = self              .state              .picked_split() +            .and_then(|(split, axis)| { +                let bounds = layout.bounds(); + +                let splits = self +                    .state +                    .split_regions(f32::from(self.spacing), bounds.size()); + +                let (_axis, region, ratio) = splits.get(&split)?; + +                let region = axis.split_line_bounds( +                    *region, +                    *ratio, +                    f32::from(self.spacing), +                ); + +                Some((axis, region + Vector::new(bounds.x, bounds.y), true)) +            })              .or_else(|| match self.on_resize {                  Some((leeway, _)) => {                      let bounds = layout.bounds(); @@ -489,15 +513,20 @@ where                          .state                          .split_regions(f32::from(self.spacing), bounds.size()); -                    hovered_split( +                    let (_split, axis, region) = hovered_split(                          splits.iter(),                          f32::from(self.spacing + leeway),                          relative_cursor, -                    ) +                    )?; + +                    Some(( +                        axis, +                        region + Vector::new(bounds.x, bounds.y), +                        false, +                    ))                  }                  None => None, -            }) -            .map(|(_, axis)| axis); +            });          self::Renderer::draw(              renderer, @@ -506,7 +535,9 @@ where              self.state.picked_pane(),              picked_split,              layout, +            &self.style,              cursor_position, +            viewport,          )      } @@ -543,9 +574,10 @@ where  /// able to use a [`PaneGrid`] in your user interface.  ///  /// [renderer]: crate::renderer -pub trait Renderer: -    crate::Renderer + container::Renderer + text::Renderer + Sized -{ +pub trait Renderer: crate::Renderer + container::Renderer + Sized { +    /// The style supported by this renderer. +    type Style: Default; +      /// Draws a [`PaneGrid`].      ///      /// It receives: @@ -559,9 +591,11 @@ pub trait Renderer:          defaults: &Self::Defaults,          content: &[(Pane, Content<'_, Message, Self>)],          dragging: Option<(Pane, Point)>, -        resizing: Option<Axis>, +        resizing: Option<(Axis, Rectangle, bool)>,          layout: Layout<'_>, +        style: &<Self as self::Renderer>::Style,          cursor_position: Point, +        viewport: &Rectangle,      ) -> Self::Output;      /// Draws a [`Pane`]. @@ -575,10 +609,11 @@ pub trait Renderer:          &mut self,          defaults: &Self::Defaults,          bounds: Rectangle, -        style: &Self::Style, +        style: &<Self as container::Renderer>::Style,          title_bar: Option<(&TitleBar<'_, Message, Self>, Layout<'_>)>,          body: (&Element<'_, Message, Self>, Layout<'_>),          cursor_position: Point, +        viewport: &Rectangle,      ) -> Self::Output;      /// Draws a [`TitleBar`]. @@ -586,20 +621,18 @@ pub trait Renderer:      /// It receives:      /// - the bounds, style of the [`TitleBar`]      /// - the style of the [`TitleBar`] -    /// - the title of the [`TitleBar`] with its size, font, and bounds -    /// - the controls of the [`TitleBar`] with their [`Layout`+, if any +    /// - the content of the [`TitleBar`] with its layout +    /// - the controls of the [`TitleBar`] with their [`Layout`], if any      /// - the cursor position      fn draw_title_bar<Message>(          &mut self,          defaults: &Self::Defaults,          bounds: Rectangle, -        style: &Self::Style, -        title: &str, -        title_size: u16, -        title_font: Self::Font, -        title_bounds: Rectangle, +        style: &<Self as container::Renderer>::Style, +        content: (&Element<'_, Message, Self>, Layout<'_>),          controls: Option<(&Element<'_, Message, Self>, Layout<'_>)>,          cursor_position: Point, +        viewport: &Rectangle,      ) -> Self::Output;  } @@ -623,14 +656,14 @@ fn hovered_split<'a>(      splits: impl Iterator<Item = (&'a Split, &'a (Axis, Rectangle, f32))>,      spacing: f32,      cursor_position: Point, -) -> Option<(Split, Axis)> { +) -> Option<(Split, Axis, Rectangle)> {      splits          .filter_map(|(split, (axis, region, ratio))| {              let bounds =                  axis.split_line_bounds(*region, *ratio, f32::from(spacing));              if bounds.contains(cursor_position) { -                Some((*split, *axis)) +                Some((*split, *axis, bounds))              } else {                  None              } diff --git a/native/src/widget/pane_grid/content.rs b/native/src/widget/pane_grid/content.rs index c9981903..b0110393 100644 --- a/native/src/widget/pane_grid/content.rs +++ b/native/src/widget/pane_grid/content.rs @@ -3,7 +3,7 @@ use crate::event::{self, Event};  use crate::layout;  use crate::overlay;  use crate::pane_grid::{self, TitleBar}; -use crate::{Clipboard, Element, Hasher, Layout, Point, Size}; +use crate::{Clipboard, Element, Hasher, Layout, Point, Rectangle, Size};  /// The content of a [`Pane`].  /// @@ -12,7 +12,7 @@ use crate::{Clipboard, Element, Hasher, Layout, Point, Size};  pub struct Content<'a, Message, Renderer: pane_grid::Renderer> {      title_bar: Option<TitleBar<'a, Message, Renderer>>,      body: Element<'a, Message, Renderer>, -    style: Renderer::Style, +    style: <Renderer as container::Renderer>::Style,  }  impl<'a, Message, Renderer> Content<'a, Message, Renderer> @@ -24,7 +24,7 @@ where          Self {              title_bar: None,              body: body.into(), -            style: Renderer::Style::default(), +            style: Default::default(),          }      } @@ -38,7 +38,10 @@ where      }      /// Sets the style of the [`Content`]. -    pub fn style(mut self, style: impl Into<Renderer::Style>) -> Self { +    pub fn style( +        mut self, +        style: impl Into<<Renderer as container::Renderer>::Style>, +    ) -> Self {          self.style = style.into();          self      } @@ -57,6 +60,7 @@ where          defaults: &Renderer::Defaults,          layout: Layout<'_>,          cursor_position: Point, +        viewport: &Rectangle,      ) -> Renderer::Output {          if let Some(title_bar) = &self.title_bar {              let mut children = layout.children(); @@ -70,6 +74,7 @@ where                  Some((title_bar, title_bar_layout)),                  (&self.body, body_layout),                  cursor_position, +                viewport,              )          } else {              renderer.draw_pane( @@ -79,6 +84,7 @@ where                  None,                  (&self.body, layout),                  cursor_position, +                viewport,              )          }      } @@ -140,9 +146,9 @@ where          event: Event,          layout: Layout<'_>,          cursor_position: Point, -        messages: &mut Vec<Message>,          renderer: &Renderer, -        clipboard: Option<&dyn Clipboard>, +        clipboard: &mut dyn Clipboard, +        messages: &mut Vec<Message>,      ) -> event::Status {          let mut event_status = event::Status::Ignored; @@ -153,9 +159,9 @@ where                  event.clone(),                  children.next().unwrap(),                  cursor_position, -                messages,                  renderer,                  clipboard, +                messages,              );              children.next().unwrap() @@ -167,9 +173,9 @@ where              event,              body_layout,              cursor_position, -            messages,              renderer,              clipboard, +            messages,          );          event_status.merge(body_status) @@ -187,18 +193,17 @@ where          &mut self,          layout: Layout<'_>,      ) -> Option<overlay::Element<'_, Message, Renderer>> { -        let body_layout = if self.title_bar.is_some() { +        if let Some(title_bar) = self.title_bar.as_mut() {              let mut children = layout.children(); +            let title_bar_layout = children.next()?; -            // Overlays only allowed in the pane body, for now at least. -            let _title_bar_layout = children.next(); - -            children.next()? +            match title_bar.overlay(title_bar_layout) { +                Some(overlay) => Some(overlay), +                None => self.body.overlay(children.next()?), +            }          } else { -            layout -        }; - -        self.body.overlay(body_layout) +            self.body.overlay(layout) +        }      }  } diff --git a/native/src/widget/pane_grid/node.rs b/native/src/widget/pane_grid/node.rs index 319936fc..84714e00 100644 --- a/native/src/widget/pane_grid/node.rs +++ b/native/src/widget/pane_grid/node.rs @@ -3,7 +3,7 @@ use crate::{      Rectangle, Size,  }; -use std::collections::HashMap; +use std::collections::BTreeMap;  /// A layout node of a [`PaneGrid`].  /// @@ -59,8 +59,8 @@ impl Node {          &self,          spacing: f32,          size: Size, -    ) -> HashMap<Pane, Rectangle> { -        let mut regions = HashMap::new(); +    ) -> BTreeMap<Pane, Rectangle> { +        let mut regions = BTreeMap::new();          self.compute_regions(              spacing, @@ -83,8 +83,8 @@ impl Node {          &self,          spacing: f32,          size: Size, -    ) -> HashMap<Split, (Axis, Rectangle, f32)> { -        let mut splits = HashMap::new(); +    ) -> BTreeMap<Split, (Axis, Rectangle, f32)> { +        let mut splits = BTreeMap::new();          self.compute_splits(              spacing, @@ -191,7 +191,7 @@ impl Node {          &self,          spacing: f32,          current: &Rectangle, -        regions: &mut HashMap<Pane, Rectangle>, +        regions: &mut BTreeMap<Pane, Rectangle>,      ) {          match self {              Node::Split { @@ -212,7 +212,7 @@ impl Node {          &self,          spacing: f32,          current: &Rectangle, -        splits: &mut HashMap<Split, (Axis, Rectangle, f32)>, +        splits: &mut BTreeMap<Split, (Axis, Rectangle, f32)>,      ) {          match self {              Node::Split { diff --git a/native/src/widget/pane_grid/pane.rs b/native/src/widget/pane_grid/pane.rs index 39d9f3ef..d6fbab83 100644 --- a/native/src/widget/pane_grid/pane.rs +++ b/native/src/widget/pane_grid/pane.rs @@ -1,5 +1,5 @@  /// A rectangular region in a [`PaneGrid`] used to display widgets.  ///  /// [`PaneGrid`]: crate::widget::PaneGrid -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]  pub struct Pane(pub(super) usize); diff --git a/native/src/widget/pane_grid/split.rs b/native/src/widget/pane_grid/split.rs index 16975abc..8132272a 100644 --- a/native/src/widget/pane_grid/split.rs +++ b/native/src/widget/pane_grid/split.rs @@ -1,5 +1,5 @@  /// A divider that splits a region in a [`PaneGrid`] into two different panes.  ///  /// [`PaneGrid`]: crate::widget::PaneGrid -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]  pub struct Split(pub(super) usize); diff --git a/native/src/widget/pane_grid/state.rs b/native/src/widget/pane_grid/state.rs index 666e1ca0..fb96f89f 100644 --- a/native/src/widget/pane_grid/state.rs +++ b/native/src/widget/pane_grid/state.rs @@ -3,7 +3,7 @@ use crate::{      Hasher, Point, Rectangle, Size,  }; -use std::collections::HashMap; +use std::collections::{BTreeMap, HashMap};  /// The state of a [`PaneGrid`].  /// @@ -257,7 +257,7 @@ impl Internal {          &self,          spacing: f32,          size: Size, -    ) -> HashMap<Pane, Rectangle> { +    ) -> BTreeMap<Pane, Rectangle> {          self.layout.pane_regions(spacing, size)      } @@ -265,7 +265,7 @@ impl Internal {          &self,          spacing: f32,          size: Size, -    ) -> HashMap<Split, (Axis, Rectangle, f32)> { +    ) -> BTreeMap<Split, (Axis, Rectangle, f32)> {          self.layout.split_regions(spacing, size)      } diff --git a/native/src/widget/pane_grid/title_bar.rs b/native/src/widget/pane_grid/title_bar.rs index 475cb9ae..070010f8 100644 --- a/native/src/widget/pane_grid/title_bar.rs +++ b/native/src/widget/pane_grid/title_bar.rs @@ -1,43 +1,42 @@ +use crate::container;  use crate::event::{self, Event};  use crate::layout; +use crate::overlay;  use crate::pane_grid; -use crate::{Clipboard, Element, Hasher, Layout, Point, Rectangle, Size}; +use crate::{ +    Clipboard, Element, Hasher, Layout, Padding, Point, Rectangle, Size, +};  /// The title bar of a [`Pane`].  ///  /// [`Pane`]: crate::widget::pane_grid::Pane  #[allow(missing_debug_implementations)]  pub struct TitleBar<'a, Message, Renderer: pane_grid::Renderer> { -    title: String, -    title_size: Option<u16>, +    content: Element<'a, Message, Renderer>,      controls: Option<Element<'a, Message, Renderer>>, -    padding: u16, +    padding: Padding,      always_show_controls: bool, -    style: Renderer::Style, +    style: <Renderer as container::Renderer>::Style,  }  impl<'a, Message, Renderer> TitleBar<'a, Message, Renderer>  where      Renderer: pane_grid::Renderer,  { -    /// Creates a new [`TitleBar`] with the given title. -    pub fn new(title: impl Into<String>) -> Self { +    /// Creates a new [`TitleBar`] with the given content. +    pub fn new<E>(content: E) -> Self +    where +        E: Into<Element<'a, Message, Renderer>>, +    {          Self { -            title: title.into(), -            title_size: None, +            content: content.into(),              controls: None, -            padding: 0, +            padding: Padding::ZERO,              always_show_controls: false, -            style: Renderer::Style::default(), +            style: Default::default(),          }      } -    /// Sets the size of the title of the [`TitleBar`]. -    pub fn title_size(mut self, size: u16) -> Self { -        self.title_size = Some(size); -        self -    } -      /// Sets the controls of the [`TitleBar`].      pub fn controls(          mut self, @@ -47,14 +46,17 @@ where          self      } -    /// Sets the padding of the [`TitleBar`]. -    pub fn padding(mut self, units: u16) -> Self { -        self.padding = units; +    /// Sets the [`Padding`] of the [`TitleBar`]. +    pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self { +        self.padding = padding.into();          self      }      /// Sets the style of the [`TitleBar`]. -    pub fn style(mut self, style: impl Into<Renderer::Style>) -> Self { +    pub fn style( +        mut self, +        style: impl Into<<Renderer as container::Renderer>::Style>, +    ) -> Self {          self.style = style.into();          self      } @@ -86,53 +88,36 @@ where          defaults: &Renderer::Defaults,          layout: Layout<'_>,          cursor_position: Point, +        viewport: &Rectangle,          show_controls: bool,      ) -> Renderer::Output {          let mut children = layout.children();          let padded = children.next().unwrap(); -        if let Some(controls) = &self.controls { -            let mut children = padded.children(); -            let title_layout = children.next().unwrap(); +        let mut children = padded.children(); +        let title_layout = children.next().unwrap(); + +        let controls = if let Some(controls) = &self.controls {              let controls_layout = children.next().unwrap(); -            let (title_bounds, controls) = -                if show_controls || self.always_show_controls { -                    (title_layout.bounds(), Some((controls, controls_layout))) -                } else { -                    ( -                        Rectangle { -                            width: padded.bounds().width, -                            ..title_layout.bounds() -                        }, -                        None, -                    ) -                }; - -            renderer.draw_title_bar( -                defaults, -                layout.bounds(), -                &self.style, -                &self.title, -                self.title_size.unwrap_or(renderer.default_size()), -                Renderer::Font::default(), -                title_bounds, -                controls, -                cursor_position, -            ) +            if show_controls || self.always_show_controls { +                Some((controls, controls_layout)) +            } else { +                None +            }          } else { -            renderer.draw_title_bar::<()>( -                defaults, -                layout.bounds(), -                &self.style, -                &self.title, -                self.title_size.unwrap_or(renderer.default_size()), -                Renderer::Font::default(), -                padded.bounds(), -                None, -                cursor_position, -            ) -        } +            None +        }; + +        renderer.draw_title_bar( +            defaults, +            layout.bounds(), +            &self.style, +            (&self.content, title_layout), +            controls, +            cursor_position, +            viewport, +        )      }      /// Returns whether the mouse cursor is over the pick area of the @@ -147,15 +132,16 @@ where          if layout.bounds().contains(cursor_position) {              let mut children = layout.children();              let padded = children.next().unwrap(); +            let mut children = padded.children(); +            let title_layout = children.next().unwrap();              if self.controls.is_some() { -                let mut children = padded.children(); -                let _ = children.next().unwrap();                  let controls_layout = children.next().unwrap();                  !controls_layout.bounds().contains(cursor_position) +                    && !title_layout.bounds().contains(cursor_position)              } else { -                true +                !title_layout.bounds().contains(cursor_position)              }          } else {              false @@ -165,9 +151,12 @@ where      pub(crate) fn hash_layout(&self, hasher: &mut Hasher) {          use std::hash::Hash; -        self.title.hash(hasher); -        self.title_size.hash(hasher); +        self.content.hash_layout(hasher);          self.padding.hash(hasher); + +        if let Some(controls) = &self.controls { +            controls.hash_layout(hasher); +        }      }      pub(crate) fn layout( @@ -175,19 +164,13 @@ where          renderer: &Renderer,          limits: &layout::Limits,      ) -> layout::Node { -        let padding = f32::from(self.padding); -        let limits = limits.pad(padding); +        let limits = limits.pad(self.padding);          let max_size = limits.max(); -        let title_size = self.title_size.unwrap_or(renderer.default_size()); -        let title_font = Renderer::Font::default(); - -        let (title_width, title_height) = renderer.measure( -            &self.title, -            title_size, -            title_font, -            Size::new(f32::INFINITY, max_size.height), -        ); +        let title_layout = self +            .content +            .layout(renderer, &layout::Limits::new(Size::ZERO, max_size)); +        let title_size = title_layout.size();          let mut node = if let Some(controls) = &self.controls {              let mut controls_layout = controls @@ -196,16 +179,8 @@ where              let controls_size = controls_layout.size();              let space_before_controls = max_size.width - controls_size.width; -            let mut title_layout = layout::Node::new(Size::new( -                title_width.min(space_before_controls), -                title_height, -            )); - -            let title_size = title_layout.size();              let height = title_size.height.max(controls_size.height); -            title_layout -                .move_to(Point::new(0.0, (height - title_size.height) / 2.0));              controls_layout.move_to(Point::new(space_before_controls, 0.0));              layout::Node::with_children( @@ -213,12 +188,18 @@ where                  vec![title_layout, controls_layout],              )          } else { -            layout::Node::new(Size::new(max_size.width, title_height)) +            layout::Node::with_children( +                Size::new(max_size.width, title_size.height), +                vec![title_layout], +            )          }; -        node.move_to(Point::new(padding, padding)); +        node.move_to(Point::new( +            self.padding.left.into(), +            self.padding.top.into(), +        )); -        layout::Node::with_children(node.size().pad(padding), vec![node]) +        layout::Node::with_children(node.size().pad(self.padding), vec![node])      }      pub(crate) fn on_event( @@ -226,28 +207,63 @@ where          event: Event,          layout: Layout<'_>,          cursor_position: Point, -        messages: &mut Vec<Message>,          renderer: &Renderer, -        clipboard: Option<&dyn Clipboard>, +        clipboard: &mut dyn Clipboard, +        messages: &mut Vec<Message>,      ) -> event::Status { -        if let Some(controls) = &mut self.controls { -            let mut children = layout.children(); -            let padded = children.next().unwrap(); +        let mut children = layout.children(); +        let padded = children.next().unwrap(); -            let mut children = padded.children(); -            let _ = children.next(); +        let mut children = padded.children(); +        let title_layout = children.next().unwrap(); + +        let control_status = if let Some(controls) = &mut self.controls {              let controls_layout = children.next().unwrap();              controls.on_event( -                event, +                event.clone(),                  controls_layout,                  cursor_position, -                messages,                  renderer,                  clipboard, +                messages,              )          } else {              event::Status::Ignored -        } +        }; + +        let title_status = self.content.on_event( +            event, +            title_layout, +            cursor_position, +            renderer, +            clipboard, +            messages, +        ); + +        control_status.merge(title_status) +    } + +    pub(crate) fn overlay( +        &mut self, +        layout: Layout<'_>, +    ) -> Option<overlay::Element<'_, Message, Renderer>> { +        let mut children = layout.children(); +        let padded = children.next()?; + +        let mut children = padded.children(); +        let title_layout = children.next()?; + +        let Self { +            content, controls, .. +        } = self; + +        content.overlay(title_layout).or_else(move || { +            controls.as_mut().and_then(|controls| { +                let controls_layout = children.next()?; + +                controls.overlay(controls_layout) +            }) +        })      }  } diff --git a/native/src/widget/pick_list.rs b/native/src/widget/pick_list.rs index 58c0dfe1..f4b60fc4 100644 --- a/native/src/widget/pick_list.rs +++ b/native/src/widget/pick_list.rs @@ -6,8 +6,10 @@ use crate::overlay;  use crate::overlay::menu::{self, Menu};  use crate::scrollable;  use crate::text; +use crate::touch;  use crate::{ -    Clipboard, Element, Hasher, Layout, Length, Point, Rectangle, Size, Widget, +    Clipboard, Element, Hasher, Layout, Length, Padding, Point, Rectangle, +    Size, Widget,  };  use std::borrow::Cow; @@ -23,9 +25,10 @@ where      last_selection: &'a mut Option<T>,      on_selected: Box<dyn Fn(T) -> Message>,      options: Cow<'a, [T]>, +    placeholder: Option<String>,      selected: Option<T>,      width: Length, -    padding: u16, +    padding: Padding,      text_size: Option<u16>,      font: Renderer::Font,      style: <Renderer as self::Renderer>::Style, @@ -80,6 +83,7 @@ where              last_selection,              on_selected: Box::new(on_selected),              options: options.into(), +            placeholder: None,              selected,              width: Length::Shrink,              text_size: None, @@ -89,15 +93,21 @@ where          }      } +    /// Sets the placeholder of the [`PickList`]. +    pub fn placeholder(mut self, placeholder: impl Into<String>) -> Self { +        self.placeholder = Some(placeholder.into()); +        self +    } +      /// Sets the width of the [`PickList`].      pub fn width(mut self, width: Length) -> Self {          self.width = width;          self      } -    /// Sets the padding of the [`PickList`]. -    pub fn padding(mut self, padding: u16) -> Self { -        self.padding = padding; +    /// Sets the [`Padding`] of the [`PickList`]. +    pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self { +        self.padding = padding.into();          self      } @@ -132,7 +142,7 @@ where      Renderer: self::Renderer + scrollable::Renderer + 'a,  {      fn width(&self) -> Length { -        Length::Shrink +        self.width      }      fn height(&self) -> Length { @@ -149,27 +159,37 @@ where          let limits = limits              .width(self.width)              .height(Length::Shrink) -            .pad(f32::from(self.padding)); +            .pad(self.padding);          let text_size = self.text_size.unwrap_or(renderer.default_size()); +        let font = self.font;          let max_width = match self.width {              Length::Shrink => { +                let measure = |label: &str| -> u32 { +                    let (width, _) = renderer.measure( +                        label, +                        text_size, +                        font, +                        Size::new(f32::INFINITY, f32::INFINITY), +                    ); + +                    width.round() as u32 +                }; +                  let labels = self.options.iter().map(ToString::to_string); -                labels -                    .map(|label| { -                        let (width, _) = renderer.measure( -                            &label, -                            text_size, -                            Renderer::Font::default(), -                            Size::new(f32::INFINITY, f32::INFINITY), -                        ); - -                        width.round() as u32 -                    }) -                    .max() -                    .unwrap_or(100) +                let labels_width = +                    labels.map(|label| measure(&label)).max().unwrap_or(100); + +                let placeholder_width = self +                    .placeholder +                    .as_ref() +                    .map(String::as_str) +                    .map(measure) +                    .unwrap_or(100); + +                labels_width.max(placeholder_width)              }              _ => 0,          }; @@ -178,11 +198,11 @@ where              let intrinsic = Size::new(                  max_width as f32                      + f32::from(text_size) -                    + f32::from(self.padding), +                    + f32::from(self.padding.left),                  f32::from(text_size),              ); -            limits.resolve(intrinsic).pad(f32::from(self.padding)) +            limits.resolve(intrinsic).pad(self.padding)          };          layout::Node::new(size) @@ -193,6 +213,8 @@ where          match self.width {              Length::Shrink => { +                self.placeholder.hash(state); +                  self.options                      .iter()                      .map(ToString::to_string) @@ -209,12 +231,13 @@ where          event: Event,          layout: Layout<'_>,          cursor_position: Point, -        messages: &mut Vec<Message>,          _renderer: &Renderer, -        _clipboard: Option<&dyn Clipboard>, +        _clipboard: &mut dyn Clipboard, +        messages: &mut Vec<Message>,      ) -> event::Status {          match event { -            Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => { +            Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) +            | Event::Touch(touch::Event::FingerPressed { .. }) => {                  let event_status = if *self.is_open {                      // TODO: Encode cursor availability in the type system                      *self.is_open = @@ -245,6 +268,43 @@ where                      event_status                  }              } +            Event::Mouse(mouse::Event::WheelScrolled { +                delta: mouse::ScrollDelta::Lines { y, .. }, +            }) if layout.bounds().contains(cursor_position) +                && !*self.is_open => +            { +                fn find_next<'a, T: PartialEq>( +                    selected: &'a T, +                    mut options: impl Iterator<Item = &'a T>, +                ) -> Option<&'a T> { +                    let _ = options.find(|&option| option == selected); + +                    options.next() +                } + +                let next_option = if y < 0.0 { +                    if let Some(selected) = self.selected.as_ref() { +                        find_next(selected, self.options.iter()) +                    } else { +                        self.options.first() +                    } +                } else if y > 0.0 { +                    if let Some(selected) = self.selected.as_ref() { +                        find_next(selected, self.options.iter().rev()) +                    } else { +                        self.options.last() +                    } +                } else { +                    None +                }; + +                if let Some(next_option) = next_option { +                    messages.push((self.on_selected)(next_option.clone())); +                } + +                return event::Status::Captured; +            } +              _ => event::Status::Ignored,          }      } @@ -262,6 +322,7 @@ where              layout.bounds(),              cursor_position,              self.selected.as_ref().map(ToString::to_string), +            self.placeholder.as_ref().map(String::as_str),              self.padding,              self.text_size.unwrap_or(renderer.default_size()),              self.font, @@ -306,7 +367,7 @@ where  /// [renderer]: crate::renderer  pub trait Renderer: text::Renderer + menu::Renderer {      /// The default padding of a [`PickList`]. -    const DEFAULT_PADDING: u16; +    const DEFAULT_PADDING: Padding;      /// The [`PickList`] style supported by this renderer.      type Style: Default; @@ -322,7 +383,8 @@ pub trait Renderer: text::Renderer + menu::Renderer {          bounds: Rectangle,          cursor_position: Point,          selected: Option<String>, -        padding: u16, +        placeholder: Option<&str>, +        padding: Padding,          text_size: u16,          font: Self::Font,          style: &<Self as Renderer>::Style, diff --git a/native/src/widget/radio.rs b/native/src/widget/radio.rs index 4935569f..dee82d1f 100644 --- a/native/src/widget/radio.rs +++ b/native/src/widget/radio.rs @@ -1,16 +1,17 @@  //! Create choices using radio buttons. +use std::hash::Hash; +  use crate::event::{self, Event}; -use crate::layout;  use crate::mouse;  use crate::row;  use crate::text; +use crate::touch; +use crate::{layout, Color};  use crate::{      Align, Clipboard, Element, Hasher, HorizontalAlignment, Layout, Length,      Point, Rectangle, Row, Text, VerticalAlignment, Widget,  }; -use std::hash::Hash; -  /// A circular button representing a choice.  ///  /// # Example @@ -46,6 +47,8 @@ pub struct Radio<Message, Renderer: self::Renderer + text::Renderer> {      size: u16,      spacing: u16,      text_size: Option<u16>, +    text_color: Option<Color>, +    font: Renderer::Font,      style: Renderer::Style,  } @@ -80,6 +83,8 @@ where              size: <Renderer as self::Renderer>::DEFAULT_SIZE,              spacing: Renderer::DEFAULT_SPACING, //15              text_size: None, +            text_color: None, +            font: Default::default(),              style: Renderer::Style::default(),          }      } @@ -108,6 +113,18 @@ where          self      } +    /// Sets the text color of the [`Radio`] button. +    pub fn text_color(mut self, color: Color) -> Self { +        self.text_color = Some(color); +        self +    } + +    /// Sets the text font of the [`Radio`] button. +    pub fn font(mut self, font: Renderer::Font) -> Self { +        self.font = font; +        self +    } +      /// Sets the style of the [`Radio`] button.      pub fn style(mut self, style: impl Into<Renderer::Style>) -> Self {          self.style = style.into(); @@ -155,12 +172,13 @@ where          event: Event,          layout: Layout<'_>,          cursor_position: Point, -        messages: &mut Vec<Message>,          _renderer: &Renderer, -        _clipboard: Option<&dyn Clipboard>, +        _clipboard: &mut dyn Clipboard, +        messages: &mut Vec<Message>,      ) -> event::Status {          match event { -            Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => { +            Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) +            | Event::Touch(touch::Event::FingerPressed { .. }) => {                  if layout.bounds().contains(cursor_position) {                      messages.push(self.on_click.clone()); @@ -194,8 +212,8 @@ where              label_layout.bounds(),              &self.label,              self.text_size.unwrap_or(renderer.default_size()), -            Default::default(), -            None, +            self.font, +            self.text_color,              HorizontalAlignment::Left,              VerticalAlignment::Center,          ); diff --git a/native/src/widget/row.rs b/native/src/widget/row.rs index b71663bd..9ebc9145 100644 --- a/native/src/widget/row.rs +++ b/native/src/widget/row.rs @@ -3,7 +3,8 @@ use crate::event::{self, Event};  use crate::layout;  use crate::overlay;  use crate::{ -    Align, Clipboard, Element, Hasher, Layout, Length, Point, Rectangle, Widget, +    Align, Clipboard, Element, Hasher, Layout, Length, Padding, Point, +    Rectangle, Widget,  };  use std::hash::Hash; @@ -13,7 +14,7 @@ use std::u32;  #[allow(missing_debug_implementations)]  pub struct Row<'a, Message, Renderer> {      spacing: u16, -    padding: u16, +    padding: Padding,      width: Length,      height: Length,      max_width: u32, @@ -34,7 +35,7 @@ impl<'a, Message, Renderer> Row<'a, Message, Renderer> {      ) -> Self {          Row {              spacing: 0, -            padding: 0, +            padding: Padding::ZERO,              width: Length::Shrink,              height: Length::Shrink,              max_width: u32::MAX, @@ -54,9 +55,9 @@ impl<'a, Message, Renderer> Row<'a, Message, Renderer> {          self      } -    /// Sets the padding of the [`Row`]. -    pub fn padding(mut self, units: u16) -> Self { -        self.padding = units; +    /// Sets the [`Padding`] of the [`Row`]. +    pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self { +        self.padding = padding.into();          self      } @@ -128,7 +129,7 @@ where              layout::flex::Axis::Horizontal,              renderer,              &limits, -            self.padding as f32, +            self.padding,              self.spacing as f32,              self.align_items,              &self.children, @@ -140,9 +141,9 @@ where          event: Event,          layout: Layout<'_>,          cursor_position: Point, -        messages: &mut Vec<Message>,          renderer: &Renderer, -        clipboard: Option<&dyn Clipboard>, +        clipboard: &mut dyn Clipboard, +        messages: &mut Vec<Message>,      ) -> event::Status {          self.children              .iter_mut() @@ -152,9 +153,9 @@ where                      event.clone(),                      layout,                      cursor_position, -                    messages,                      renderer,                      clipboard, +                    messages,                  )              })              .fold(event::Status::Ignored, event::Status::merge) diff --git a/native/src/widget/scrollable.rs b/native/src/widget/scrollable.rs index e23ab06a..68da2e67 100644 --- a/native/src/widget/scrollable.rs +++ b/native/src/widget/scrollable.rs @@ -4,8 +4,9 @@ use crate::event::{self, Event};  use crate::layout;  use crate::mouse;  use crate::overlay; +use crate::touch;  use crate::{ -    Align, Clipboard, Column, Element, Hasher, Layout, Length, Point, +    Align, Clipboard, Column, Element, Hasher, Layout, Length, Padding, Point,      Rectangle, Size, Vector, Widget,  }; @@ -22,6 +23,7 @@ pub struct Scrollable<'a, Message, Renderer: self::Renderer> {      scrollbar_margin: u16,      scroller_width: u16,      content: Column<'a, Message, Renderer>, +    on_scroll: Option<Box<dyn Fn(f32) -> Message>>,      style: Renderer::Style,  } @@ -36,6 +38,7 @@ impl<'a, Message, Renderer: self::Renderer> Scrollable<'a, Message, Renderer> {              scrollbar_margin: 0,              scroller_width: 10,              content: Column::new(), +            on_scroll: None,              style: Renderer::Style::default(),          }      } @@ -50,9 +53,9 @@ impl<'a, Message, Renderer: self::Renderer> Scrollable<'a, Message, Renderer> {          self      } -    /// Sets the padding of the [`Scrollable`]. -    pub fn padding(mut self, units: u16) -> Self { -        self.content = self.content.padding(units); +    /// Sets the [`Padding`] of the [`Scrollable`]. +    pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self { +        self.content = self.content.padding(padding);          self      } @@ -100,12 +103,22 @@ impl<'a, Message, Renderer: self::Renderer> Scrollable<'a, Message, Renderer> {      }      /// Sets the scroller width of the [`Scrollable`] . -    /// Silently enforces a minimum value of 1. +    /// +    /// It silently enforces a minimum value of 1.      pub fn scroller_width(mut self, scroller_width: u16) -> Self {          self.scroller_width = scroller_width.max(1);          self      } +    /// Sets a function to call when the [`Scrollable`] is scrolled. +    /// +    /// The function takes the new relative offset of the [`Scrollable`] +    /// (e.g. `0` means top, while `1` means bottom). +    pub fn on_scroll(mut self, f: impl Fn(f32) -> Message + 'static) -> Self { +        self.on_scroll = Some(Box::new(f)); +        self +    } +      /// Sets the style of the [`Scrollable`] .      pub fn style(mut self, style: impl Into<Renderer::Style>) -> Self {          self.style = style.into(); @@ -120,6 +133,24 @@ impl<'a, Message, Renderer: self::Renderer> Scrollable<'a, Message, Renderer> {          self.content = self.content.push(child);          self      } + +    fn notify_on_scroll( +        &self, +        bounds: Rectangle, +        content_bounds: Rectangle, +        messages: &mut Vec<Message>, +    ) { +        if content_bounds.height <= bounds.height { +            return; +        } + +        if let Some(on_scroll) = &self.on_scroll { +            messages.push(on_scroll( +                self.state.offset.absolute(bounds, content_bounds) +                    / (content_bounds.height - bounds.height), +            )); +        } +    }  }  impl<'a, Message, Renderer> Widget<Message, Renderer> @@ -161,9 +192,9 @@ where          event: Event,          layout: Layout<'_>,          cursor_position: Point, -        messages: &mut Vec<Message>,          renderer: &Renderer, -        clipboard: Option<&dyn Clipboard>, +        clipboard: &mut dyn Clipboard, +        messages: &mut Vec<Message>,      ) -> event::Status {          let bounds = layout.bounds();          let is_mouse_over = bounds.contains(cursor_position); @@ -204,9 +235,9 @@ where                  event.clone(),                  content,                  cursor_position, -                messages,                  renderer,                  clipboard, +                messages,              )          }; @@ -227,6 +258,45 @@ where                          }                      } +                    self.notify_on_scroll(bounds, content_bounds, messages); + +                    return event::Status::Captured; +                } +                Event::Touch(event) => { +                    match event { +                        touch::Event::FingerPressed { .. } => { +                            self.state.scroll_box_touched_at = +                                Some(cursor_position); +                        } +                        touch::Event::FingerMoved { .. } => { +                            if let Some(scroll_box_touched_at) = +                                self.state.scroll_box_touched_at +                            { +                                let delta = +                                    cursor_position.y - scroll_box_touched_at.y; + +                                self.state.scroll( +                                    delta, +                                    bounds, +                                    content_bounds, +                                ); + +                                self.state.scroll_box_touched_at = +                                    Some(cursor_position); + +                                self.notify_on_scroll( +                                    bounds, +                                    content_bounds, +                                    messages, +                                ); +                            } +                        } +                        touch::Event::FingerLifted { .. } +                        | touch::Event::FingerLost { .. } => { +                            self.state.scroll_box_touched_at = None; +                        } +                    } +                      return event::Status::Captured;                  }                  _ => {} @@ -237,12 +307,15 @@ where              match event {                  Event::Mouse(mouse::Event::ButtonReleased(                      mouse::Button::Left, -                )) => { +                )) +                | Event::Touch(touch::Event::FingerLifted { .. }) +                | Event::Touch(touch::Event::FingerLost { .. }) => {                      self.state.scroller_grabbed_at = None;                      return event::Status::Captured;                  } -                Event::Mouse(mouse::Event::CursorMoved { .. }) => { +                Event::Mouse(mouse::Event::CursorMoved { .. }) +                | Event::Touch(touch::Event::FingerMoved { .. }) => {                      if let (Some(scrollbar), Some(scroller_grabbed_at)) =                          (scrollbar, self.state.scroller_grabbed_at)                      { @@ -255,6 +328,8 @@ where                              content_bounds,                          ); +                        self.notify_on_scroll(bounds, content_bounds, messages); +                          return event::Status::Captured;                      }                  } @@ -264,7 +339,8 @@ where              match event {                  Event::Mouse(mouse::Event::ButtonPressed(                      mouse::Button::Left, -                )) => { +                )) +                | Event::Touch(touch::Event::FingerPressed { .. }) => {                      if let Some(scrollbar) = scrollbar {                          if let Some(scroller_grabbed_at) =                              scrollbar.grab_scroller(cursor_position) @@ -281,6 +357,12 @@ where                              self.state.scroller_grabbed_at =                                  Some(scroller_grabbed_at); +                            self.notify_on_scroll( +                                bounds, +                                content_bounds, +                                messages, +                            ); +                              return event::Status::Captured;                          }                      } @@ -382,10 +464,44 @@ where  }  /// The local state of a [`Scrollable`]. -#[derive(Debug, Clone, Copy, Default)] +#[derive(Debug, Clone, Copy)]  pub struct State {      scroller_grabbed_at: Option<f32>, -    offset: f32, +    scroll_box_touched_at: Option<Point>, +    offset: Offset, +} + +impl Default for State { +    fn default() -> Self { +        Self { +            scroller_grabbed_at: None, +            scroll_box_touched_at: None, +            offset: Offset::Absolute(0.0), +        } +    } +} + +/// The local state of a [`Scrollable`]. +#[derive(Debug, Clone, Copy)] +enum Offset { +    Absolute(f32), +    Relative(f32), +} + +impl Offset { +    fn absolute(self, bounds: Rectangle, content_bounds: Rectangle) -> f32 { +        match self { +            Self::Absolute(absolute) => { +                let hidden_content = +                    (content_bounds.height - bounds.height).max(0.0); + +                absolute.min(hidden_content) +            } +            Self::Relative(percentage) => { +                ((content_bounds.height - bounds.height) * percentage).max(0.0) +            } +        } +    }  }  impl State { @@ -406,13 +522,14 @@ impl State {              return;          } -        self.offset = (self.offset - delta_y) -            .max(0.0) -            .min((content_bounds.height - bounds.height) as f32); +        self.offset = Offset::Absolute( +            (self.offset.absolute(bounds, content_bounds) - delta_y) +                .max(0.0) +                .min((content_bounds.height - bounds.height) as f32), +        );      } -    /// Moves the scroll position to a relative amount, given the bounds of -    /// the [`Scrollable`] and its contents. +    /// Scrolls the [`Scrollable`] to a relative amount.      ///      /// `0` represents scrollbar at the top, while `1` represents scrollbar at      /// the bottom. @@ -422,23 +539,40 @@ impl State {          bounds: Rectangle,          content_bounds: Rectangle,      ) { +        self.snap_to(percentage); +        self.unsnap(bounds, content_bounds); +    } + +    /// Snaps the scroll position to a relative amount. +    /// +    /// `0` represents scrollbar at the top, while `1` represents scrollbar at +    /// the bottom. +    pub fn snap_to(&mut self, percentage: f32) { +        self.offset = Offset::Relative(percentage.max(0.0).min(1.0)); +    } + +    /// Unsnaps the current scroll position, if snapped, given the bounds of the +    /// [`Scrollable`] and its contents. +    pub fn unsnap(&mut self, bounds: Rectangle, content_bounds: Rectangle) {          self.offset = -            ((content_bounds.height - bounds.height) * percentage).max(0.0); +            Offset::Absolute(self.offset.absolute(bounds, content_bounds));      }      /// Returns the current scrolling offset of the [`State`], given the bounds      /// of the [`Scrollable`] and its contents.      pub fn offset(&self, bounds: Rectangle, content_bounds: Rectangle) -> u32 { -        let hidden_content = -            (content_bounds.height - bounds.height).max(0.0).round() as u32; - -        self.offset.min(hidden_content as f32) as u32 +        self.offset.absolute(bounds, content_bounds) as u32      }      /// Returns whether the scroller is currently grabbed or not.      pub fn is_scroller_grabbed(&self) -> bool {          self.scroller_grabbed_at.is_some()      } + +    /// Returns whether the scroll box is currently touched or not. +    pub fn is_scroll_box_touched(&self) -> bool { +        self.scroll_box_touched_at.is_some() +    }  }  /// The scrollbar of a [`Scrollable`]. diff --git a/native/src/widget/slider.rs b/native/src/widget/slider.rs index ff39b816..2a74d5a3 100644 --- a/native/src/widget/slider.rs +++ b/native/src/widget/slider.rs @@ -4,6 +4,7 @@  use crate::event::{self, Event};  use crate::layout;  use crate::mouse; +use crate::touch;  use crate::{      Clipboard, Element, Hasher, Layout, Length, Point, Rectangle, Size, Widget,  }; @@ -179,9 +180,9 @@ where          event: Event,          layout: Layout<'_>,          cursor_position: Point, -        messages: &mut Vec<Message>,          _renderer: &Renderer, -        _clipboard: Option<&dyn Clipboard>, +        _clipboard: &mut dyn Clipboard, +        messages: &mut Vec<Message>,      ) -> event::Status {          let mut change = || {              let bounds = layout.bounds(); @@ -207,34 +208,35 @@ where          };          match event { -            Event::Mouse(mouse_event) => match mouse_event { -                mouse::Event::ButtonPressed(mouse::Button::Left) => { -                    if layout.bounds().contains(cursor_position) { -                        change(); -                        self.state.is_dragging = true; +            Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) +            | Event::Touch(touch::Event::FingerPressed { .. }) => { +                if layout.bounds().contains(cursor_position) { +                    change(); +                    self.state.is_dragging = true; -                        return event::Status::Captured; -                    } +                    return event::Status::Captured;                  } -                mouse::Event::ButtonReleased(mouse::Button::Left) => { -                    if self.state.is_dragging { -                        if let Some(on_release) = self.on_release.clone() { -                            messages.push(on_release); -                        } -                        self.state.is_dragging = false; - -                        return event::Status::Captured; +            } +            Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) +            | Event::Touch(touch::Event::FingerLifted { .. }) +            | Event::Touch(touch::Event::FingerLost { .. }) => { +                if self.state.is_dragging { +                    if let Some(on_release) = self.on_release.clone() { +                        messages.push(on_release);                      } +                    self.state.is_dragging = false; + +                    return event::Status::Captured;                  } -                mouse::Event::CursorMoved { .. } => { -                    if self.state.is_dragging { -                        change(); +            } +            Event::Mouse(mouse::Event::CursorMoved { .. }) +            | Event::Touch(touch::Event::FingerMoved { .. }) => { +                if self.state.is_dragging { +                    change(); -                        return event::Status::Captured; -                    } +                    return event::Status::Captured;                  } -                _ => {} -            }, +            }              _ => {}          } diff --git a/native/src/widget/text_input.rs b/native/src/widget/text_input.rs index 3e637e97..cec1e485 100644 --- a/native/src/widget/text_input.rs +++ b/native/src/widget/text_input.rs @@ -16,8 +16,10 @@ use crate::keyboard;  use crate::layout;  use crate::mouse::{self, click};  use crate::text; +use crate::touch;  use crate::{ -    Clipboard, Element, Hasher, Layout, Length, Point, Rectangle, Size, Widget, +    Clipboard, Element, Hasher, Layout, Length, Padding, Point, Rectangle, +    Size, Widget,  };  use std::u32; @@ -55,7 +57,7 @@ pub struct TextInput<'a, Message, Renderer: self::Renderer> {      font: Renderer::Font,      width: Length,      max_width: u32, -    padding: u16, +    padding: Padding,      size: Option<u16>,      on_change: Box<dyn Fn(String) -> Message>,      on_submit: Option<Message>, @@ -91,7 +93,7 @@ where              font: Default::default(),              width: Length::Fill,              max_width: u32::MAX, -            padding: 0, +            padding: Padding::ZERO,              size: None,              on_change: Box::new(on_change),              on_submit: None, @@ -125,9 +127,9 @@ where          self      } -    /// Sets the padding of the [`TextInput`]. -    pub fn padding(mut self, units: u16) -> Self { -        self.padding = units; +    /// Sets the [`Padding`] of the [`TextInput`]. +    pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self { +        self.padding = padding.into();          self      } @@ -222,19 +224,21 @@ where          renderer: &Renderer,          limits: &layout::Limits,      ) -> layout::Node { -        let padding = self.padding as f32;          let text_size = self.size.unwrap_or(renderer.default_size());          let limits = limits -            .pad(padding) +            .pad(self.padding)              .width(self.width)              .max_width(self.max_width)              .height(Length::Units(text_size));          let mut text = layout::Node::new(limits.resolve(Size::ZERO)); -        text.move_to(Point::new(padding, padding)); +        text.move_to(Point::new( +            self.padding.left.into(), +            self.padding.top.into(), +        )); -        layout::Node::with_children(text.size().pad(padding), vec![text]) +        layout::Node::with_children(text.size().pad(self.padding), vec![text])      }      fn on_event( @@ -242,12 +246,13 @@ where          event: Event,          layout: Layout<'_>,          cursor_position: Point, -        messages: &mut Vec<Message>,          renderer: &Renderer, -        clipboard: Option<&dyn Clipboard>, +        clipboard: &mut dyn Clipboard, +        messages: &mut Vec<Message>,      ) -> event::Status {          match event { -            Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => { +            Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) +            | Event::Touch(touch::Event::FingerPressed { .. }) => {                  let is_clicked = layout.bounds().contains(cursor_position);                  self.state.is_focused = is_clicked; @@ -318,13 +323,16 @@ where                      return event::Status::Captured;                  }              } -            Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) => { +            Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) +            | Event::Touch(touch::Event::FingerLifted { .. }) +            | Event::Touch(touch::Event::FingerLost { .. }) => {                  self.state.is_dragging = false;              } -            Event::Mouse(mouse::Event::CursorMoved { x, .. }) => { +            Event::Mouse(mouse::Event::CursorMoved { position }) +            | Event::Touch(touch::Event::FingerMoved { position, .. }) => {                  if self.state.is_dragging {                      let text_layout = layout.children().next().unwrap(); -                    let target = x - text_layout.bounds().x; +                    let target = position.x - text_layout.bounds().x;                      if target > 0.0 {                          let value = if self.is_secure { @@ -354,7 +362,7 @@ where              Event::Keyboard(keyboard::Event::CharacterReceived(c))                  if self.state.is_focused                      && self.state.is_pasting.is_none() -                    && !self.state.keyboard_modifiers.is_command_pressed() +                    && !self.state.keyboard_modifiers.command()                      && !c.is_control() =>              {                  let mut editor = @@ -442,7 +450,7 @@ where                          if platform::is_jump_modifier_pressed(modifiers)                              && !self.is_secure                          { -                            if modifiers.shift { +                            if modifiers.shift() {                                  self.state                                      .cursor                                      .select_left_by_words(&self.value); @@ -451,7 +459,7 @@ where                                      .cursor                                      .move_left_by_words(&self.value);                              } -                        } else if modifiers.shift { +                        } else if modifiers.shift() {                              self.state.cursor.select_left(&self.value)                          } else {                              self.state.cursor.move_left(&self.value); @@ -461,7 +469,7 @@ where                          if platform::is_jump_modifier_pressed(modifiers)                              && !self.is_secure                          { -                            if modifiers.shift { +                            if modifiers.shift() {                                  self.state                                      .cursor                                      .select_right_by_words(&self.value); @@ -470,14 +478,14 @@ where                                      .cursor                                      .move_right_by_words(&self.value);                              } -                        } else if modifiers.shift { +                        } else if modifiers.shift() {                              self.state.cursor.select_right(&self.value)                          } else {                              self.state.cursor.move_right(&self.value);                          }                      }                      keyboard::KeyCode::Home => { -                        if modifiers.shift { +                        if modifiers.shift() {                              self.state.cursor.select_range(                                  self.state.cursor.start(&self.value),                                  0, @@ -487,7 +495,7 @@ where                          }                      }                      keyboard::KeyCode::End => { -                        if modifiers.shift { +                        if modifiers.shift() {                              self.state.cursor.select_range(                                  self.state.cursor.start(&self.value),                                  self.value.len(), @@ -496,45 +504,75 @@ where                              self.state.cursor.move_to(self.value.len());                          }                      } -                    keyboard::KeyCode::V => { -                        if self.state.keyboard_modifiers.is_command_pressed() { -                            if let Some(clipboard) = clipboard { -                                let content = match self.state.is_pasting.take() -                                { -                                    Some(content) => content, -                                    None => { -                                        let content: String = clipboard -                                            .content() -                                            .unwrap_or(String::new()) -                                            .chars() -                                            .filter(|c| !c.is_control()) -                                            .collect(); - -                                        Value::new(&content) -                                    } -                                }; - -                                let mut editor = Editor::new( -                                    &mut self.value, -                                    &mut self.state.cursor, +                    keyboard::KeyCode::C +                        if self.state.keyboard_modifiers.command() => +                    { +                        match self.state.cursor.selection(&self.value) { +                            Some((start, end)) => { +                                clipboard.write( +                                    self.value.select(start, end).to_string(),                                  ); +                            } +                            None => {} +                        } +                    } +                    keyboard::KeyCode::X +                        if self.state.keyboard_modifiers.command() => +                    { +                        match self.state.cursor.selection(&self.value) { +                            Some((start, end)) => { +                                clipboard.write( +                                    self.value.select(start, end).to_string(), +                                ); +                            } +                            None => {} +                        } -                                editor.paste(content.clone()); +                        let mut editor = Editor::new( +                            &mut self.value, +                            &mut self.state.cursor, +                        ); -                                let message = -                                    (self.on_change)(editor.contents()); -                                messages.push(message); +                        editor.delete(); -                                self.state.is_pasting = Some(content); -                            } +                        let message = (self.on_change)(editor.contents()); +                        messages.push(message); +                    } +                    keyboard::KeyCode::V => { +                        if self.state.keyboard_modifiers.command() { +                            let content = match self.state.is_pasting.take() { +                                Some(content) => content, +                                None => { +                                    let content: String = clipboard +                                        .read() +                                        .unwrap_or(String::new()) +                                        .chars() +                                        .filter(|c| !c.is_control()) +                                        .collect(); + +                                    Value::new(&content) +                                } +                            }; + +                            let mut editor = Editor::new( +                                &mut self.value, +                                &mut self.state.cursor, +                            ); + +                            editor.paste(content.clone()); + +                            let message = (self.on_change)(editor.contents()); +                            messages.push(message); + +                            self.state.is_pasting = Some(content);                          } else {                              self.state.is_pasting = None;                          }                      } -                    keyboard::KeyCode::A => { -                        if self.state.keyboard_modifiers.is_command_pressed() { -                            self.state.cursor.select_all(&self.value); -                        } +                    keyboard::KeyCode::A +                        if self.state.keyboard_modifiers.command() => +                    { +                        self.state.cursor.select_all(&self.value);                      }                      keyboard::KeyCode::Escape => {                          self.state.is_focused = false; @@ -748,6 +786,11 @@ impl State {      pub fn move_cursor_to(&mut self, position: usize) {          self.cursor.move_to(position);      } + +    /// Selects all the content of the [`TextInput`]. +    pub fn select_all(&mut self) { +        self.cursor.select_range(0, usize::MAX); +    }  }  // TODO: Reduce allocations @@ -811,9 +854,9 @@ mod platform {      pub fn is_jump_modifier_pressed(modifiers: keyboard::Modifiers) -> bool {          if cfg!(target_os = "macos") { -            modifiers.alt +            modifiers.alt()          } else { -            modifiers.control +            modifiers.control()          }      }  } diff --git a/native/src/widget/text_input/cursor.rs b/native/src/widget/text_input/cursor.rs index e630e293..4f3b159b 100644 --- a/native/src/widget/text_input/cursor.rs +++ b/native/src/widget/text_input/cursor.rs @@ -48,6 +48,18 @@ impl Cursor {          }      } +    /// Returns the current selection of the [`Cursor`] for the given [`Value`]. +    /// +    /// `start` is guaranteed to be <= than `end`. +    pub fn selection(&self, value: &Value) -> Option<(usize, usize)> { +        match self.state(value) { +            State::Selection { start, end } => { +                Some((start.min(end), start.max(end))) +            } +            _ => None, +        } +    } +      pub(crate) fn move_to(&mut self, position: usize) {          self.state = State::Index(position);      } @@ -101,7 +113,7 @@ impl Cursor {              State::Selection { start, end } if end > 0 => {                  self.select_range(start, end - 1)              } -            _ => (), +            _ => {}          }      } @@ -113,7 +125,7 @@ impl Cursor {              State::Selection { start, end } if end < value.len() => {                  self.select_range(start, end + 1)              } -            _ => (), +            _ => {}          }      } @@ -161,15 +173,6 @@ impl Cursor {          end.min(value.len())      } -    pub(crate) fn selection(&self, value: &Value) -> Option<(usize, usize)> { -        match self.state(value) { -            State::Selection { start, end } => { -                Some((start.min(end), start.max(end))) -            } -            _ => None, -        } -    } -      fn left(&self, value: &Value) -> usize {          match self.state(value) {              State::Index(index) => index, diff --git a/native/src/widget/text_input/editor.rs b/native/src/widget/text_input/editor.rs index 20e42567..0b50a382 100644 --- a/native/src/widget/text_input/editor.rs +++ b/native/src/widget/text_input/editor.rs @@ -20,7 +20,7 @@ impl<'a> Editor<'a> {                  self.cursor.move_left(self.value);                  self.value.remove_many(left, right);              } -            _ => (), +            _ => {}          }          self.value.insert(self.cursor.end(self.value), character); @@ -35,7 +35,7 @@ impl<'a> Editor<'a> {                  self.cursor.move_left(self.value);                  self.value.remove_many(left, right);              } -            _ => (), +            _ => {}          }          self.value.insert_many(self.cursor.end(self.value), content); diff --git a/native/src/widget/text_input/value.rs b/native/src/widget/text_input/value.rs index 86be2790..2034cca4 100644 --- a/native/src/widget/text_input/value.rs +++ b/native/src/widget/text_input/value.rs @@ -73,6 +73,15 @@ impl Value {              .unwrap_or(self.len())      } +    /// Returns a new [`Value`] containing the graphemes from `start` until the +    /// given `end`. +    pub fn select(&self, start: usize, end: usize) -> Self { +        let graphemes = +            self.graphemes[start.min(self.len())..end.min(self.len())].to_vec(); + +        Self { graphemes } +    } +      /// Returns a new [`Value`] containing the graphemes until the given      /// `index`.      pub fn until(&self, index: usize) -> Self { diff --git a/native/src/widget/toggler.rs b/native/src/widget/toggler.rs new file mode 100644 index 00000000..4035276c --- /dev/null +++ b/native/src/widget/toggler.rs @@ -0,0 +1,277 @@ +//! Show toggle controls using togglers. +use std::hash::Hash; + +use crate::{ +    event, layout, mouse, row, text, Align, Clipboard, Element, Event, Hasher, +    HorizontalAlignment, Layout, Length, Point, Rectangle, Row, Text, +    VerticalAlignment, Widget, +}; + +/// A toggler widget +/// +/// # Example +/// +/// ``` +/// # type Toggler<Message> = iced_native::Toggler<Message, iced_native::renderer::Null>; +/// # +/// pub enum Message { +///     TogglerToggled(bool), +/// } +/// +/// let is_active = true; +/// +/// Toggler::new(is_active, String::from("Toggle me!"), |b| Message::TogglerToggled(b)); +/// ``` +#[allow(missing_debug_implementations)] +pub struct Toggler<Message, Renderer: self::Renderer + text::Renderer> { +    is_active: bool, +    on_toggle: Box<dyn Fn(bool) -> Message>, +    label: Option<String>, +    width: Length, +    size: u16, +    text_size: Option<u16>, +    text_alignment: HorizontalAlignment, +    spacing: u16, +    font: Renderer::Font, +    style: Renderer::Style, +} + +impl<Message, Renderer: self::Renderer + text::Renderer> +    Toggler<Message, Renderer> +{ +    /// Creates a new [`Toggler`]. +    /// +    /// It expects: +    ///   * a boolean describing whether the [`Toggler`] is checked or not +    ///   * An optional label for the [`Toggler`] +    ///   * a function that will be called when the [`Toggler`] is toggled. It +    ///     will receive the new state of the [`Toggler`] and must produce a +    ///     `Message`. +    pub fn new<F>( +        is_active: bool, +        label: impl Into<Option<String>>, +        f: F, +    ) -> Self +    where +        F: 'static + Fn(bool) -> Message, +    { +        Toggler { +            is_active, +            on_toggle: Box::new(f), +            label: label.into(), +            width: Length::Fill, +            size: <Renderer as self::Renderer>::DEFAULT_SIZE, +            text_size: None, +            text_alignment: HorizontalAlignment::Left, +            spacing: 0, +            font: Renderer::Font::default(), +            style: Renderer::Style::default(), +        } +    } + +    /// Sets the size of the [`Toggler`]. +    pub fn size(mut self, size: u16) -> Self { +        self.size = size; +        self +    } + +    /// Sets the width of the [`Toggler`]. +    pub fn width(mut self, width: Length) -> Self { +        self.width = width; +        self +    } + +    /// Sets the text size o the [`Toggler`]. +    pub fn text_size(mut self, text_size: u16) -> Self { +        self.text_size = Some(text_size); +        self +    } + +    /// Sets the horizontal alignment of the text of the [`Toggler`] +    pub fn text_alignment(mut self, alignment: HorizontalAlignment) -> Self { +        self.text_alignment = alignment; +        self +    } + +    /// Sets the spacing between the [`Toggler`] and the text. +    pub fn spacing(mut self, spacing: u16) -> Self { +        self.spacing = spacing; +        self +    } + +    /// Sets the [`Font`] of the text of the [`Toggler`] +    pub fn font(mut self, font: Renderer::Font) -> Self { +        self.font = font; +        self +    } + +    /// Sets the style of the [`Toggler`]. +    pub fn style(mut self, style: impl Into<Renderer::Style>) -> Self { +        self.style = style.into(); +        self +    } +} + +impl<Message, Renderer> Widget<Message, Renderer> for Toggler<Message, Renderer> +where +    Renderer: self::Renderer + text::Renderer + row::Renderer, +{ +    fn width(&self) -> Length { +        self.width +    } + +    fn height(&self) -> Length { +        Length::Shrink +    } + +    fn layout( +        &self, +        renderer: &Renderer, +        limits: &layout::Limits, +    ) -> layout::Node { +        let mut row = Row::<(), Renderer>::new() +            .width(self.width) +            .spacing(self.spacing) +            .align_items(Align::Center); + +        if let Some(label) = &self.label { +            row = row.push( +                Text::new(label) +                    .horizontal_alignment(self.text_alignment) +                    .font(self.font) +                    .width(self.width) +                    .size(self.text_size.unwrap_or(renderer.default_size())), +            ); +        } + +        row = row.push( +            Row::new() +                .width(Length::Units(2 * self.size)) +                .height(Length::Units(self.size)), +        ); + +        row.layout(renderer, limits) +    } + +    fn on_event( +        &mut self, +        event: Event, +        layout: Layout<'_>, +        cursor_position: Point, +        _renderer: &Renderer, +        _clipboard: &mut dyn Clipboard, +        messages: &mut Vec<Message>, +    ) -> event::Status { +        match event { +            Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => { +                let mouse_over = layout.bounds().contains(cursor_position); + +                if mouse_over { +                    messages.push((self.on_toggle)(!self.is_active)); + +                    event::Status::Captured +                } else { +                    event::Status::Ignored +                } +            } +            _ => event::Status::Ignored, +        } +    } + +    fn draw( +        &self, +        renderer: &mut Renderer, +        defaults: &Renderer::Defaults, +        layout: Layout<'_>, +        cursor_position: Point, +        _viewport: &Rectangle, +    ) -> Renderer::Output { +        let bounds = layout.bounds(); +        let mut children = layout.children(); + +        let label = match &self.label { +            Some(label) => { +                let label_layout = children.next().unwrap(); + +                Some(text::Renderer::draw( +                    renderer, +                    defaults, +                    label_layout.bounds(), +                    &label, +                    self.text_size.unwrap_or(renderer.default_size()), +                    self.font, +                    None, +                    self.text_alignment, +                    VerticalAlignment::Center, +                )) +            } + +            None => None, +        }; + +        let toggler_layout = children.next().unwrap(); +        let toggler_bounds = toggler_layout.bounds(); + +        let is_mouse_over = bounds.contains(cursor_position); + +        self::Renderer::draw( +            renderer, +            toggler_bounds, +            self.is_active, +            is_mouse_over, +            label, +            &self.style, +        ) +    } + +    fn hash_layout(&self, state: &mut Hasher) { +        struct Marker; +        std::any::TypeId::of::<Marker>().hash(state); + +        self.label.hash(state) +    } +} + +/// The renderer of a [`Toggler`]. +/// +/// Your [renderer] will need to implement this trait before being +/// able to use a [`Toggler`] in your user interface. +/// +/// [renderer]: ../../renderer/index.html +pub trait Renderer: crate::Renderer { +    /// The style supported by this renderer. +    type Style: Default; + +    /// The default size of a [`Toggler`]. +    const DEFAULT_SIZE: u16; + +    /// Draws a [`Toggler`]. +    /// +    /// It receives: +    ///   * the bounds of the [`Toggler`] +    ///   * whether the [`Toggler`] is activated or not +    ///   * whether the mouse is over the [`Toggler`] or not +    ///   * the drawn label of the [`Toggler`] +    ///   * the style of the [`Toggler`] +    fn draw( +        &mut self, +        bounds: Rectangle, +        is_active: bool, +        is_mouse_over: bool, +        label: Option<Self::Output>, +        style: &Self::Style, +    ) -> Self::Output; +} + +impl<'a, Message, Renderer> From<Toggler<Message, Renderer>> +    for Element<'a, Message, Renderer> +where +    Renderer: 'a + self::Renderer + text::Renderer + row::Renderer, +    Message: 'a, +{ +    fn from( +        toggler: Toggler<Message, Renderer>, +    ) -> Element<'a, Message, Renderer> { +        Element::new(toggler) +    } +} diff --git a/native/src/widget/tooltip.rs b/native/src/widget/tooltip.rs new file mode 100644 index 00000000..276afd41 --- /dev/null +++ b/native/src/widget/tooltip.rs @@ -0,0 +1,210 @@ +//! Display a widget over another. +use std::hash::Hash; + +use iced_core::Rectangle; + +use crate::widget::container; +use crate::widget::text::{self, Text}; +use crate::{ +    event, layout, Clipboard, Element, Event, Hasher, Layout, Length, Point, +    Widget, +}; + +/// An element to display a widget over another. +#[allow(missing_debug_implementations)] +pub struct Tooltip<'a, Message, Renderer: self::Renderer> { +    content: Element<'a, Message, Renderer>, +    tooltip: Text<Renderer>, +    position: Position, +    style: <Renderer as container::Renderer>::Style, +    gap: u16, +    padding: u16, +} + +impl<'a, Message, Renderer> Tooltip<'a, Message, Renderer> +where +    Renderer: self::Renderer, +{ +    /// Creates an empty [`Tooltip`]. +    /// +    /// [`Tooltip`]: struct.Tooltip.html +    pub fn new( +        content: impl Into<Element<'a, Message, Renderer>>, +        tooltip: impl ToString, +        position: Position, +    ) -> Self { +        Tooltip { +            content: content.into(), +            tooltip: Text::new(tooltip.to_string()), +            position, +            style: Default::default(), +            gap: 0, +            padding: Renderer::DEFAULT_PADDING, +        } +    } + +    /// Sets the size of the text of the [`Tooltip`]. +    pub fn size(mut self, size: u16) -> Self { +        self.tooltip = self.tooltip.size(size); +        self +    } + +    /// Sets the font of the [`Tooltip`]. +    /// +    /// [`Font`]: Renderer::Font +    pub fn font(mut self, font: impl Into<Renderer::Font>) -> Self { +        self.tooltip = self.tooltip.font(font); +        self +    } + +    /// Sets the gap between the content and its [`Tooltip`]. +    pub fn gap(mut self, gap: u16) -> Self { +        self.gap = gap; +        self +    } + +    /// Sets the padding of the [`Tooltip`]. +    pub fn padding(mut self, padding: u16) -> Self { +        self.padding = padding; +        self +    } + +    /// Sets the style of the [`Tooltip`]. +    pub fn style( +        mut self, +        style: impl Into<<Renderer as container::Renderer>::Style>, +    ) -> Self { +        self.style = style.into(); +        self +    } +} + +/// The position of the tooltip. Defaults to following the cursor. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Position { +    /// The tooltip will follow the cursor. +    FollowCursor, +    /// The tooltip will appear on the top of the widget. +    Top, +    /// The tooltip will appear on the bottom of the widget. +    Bottom, +    /// The tooltip will appear on the left of the widget. +    Left, +    /// The tooltip will appear on the right of the widget. +    Right, +} + +impl<'a, Message, Renderer> Widget<Message, Renderer> +    for Tooltip<'a, Message, Renderer> +where +    Renderer: self::Renderer, +{ +    fn width(&self) -> Length { +        self.content.width() +    } + +    fn height(&self) -> Length { +        self.content.height() +    } + +    fn layout( +        &self, +        renderer: &Renderer, +        limits: &layout::Limits, +    ) -> layout::Node { +        self.content.layout(renderer, limits) +    } + +    fn on_event( +        &mut self, +        event: Event, +        layout: Layout<'_>, +        cursor_position: Point, +        renderer: &Renderer, +        clipboard: &mut dyn Clipboard, +        messages: &mut Vec<Message>, +    ) -> event::Status { +        self.content.widget.on_event( +            event, +            layout, +            cursor_position, +            renderer, +            clipboard, +            messages, +        ) +    } + +    fn draw( +        &self, +        renderer: &mut Renderer, +        defaults: &Renderer::Defaults, +        layout: Layout<'_>, +        cursor_position: Point, +        viewport: &Rectangle, +    ) -> Renderer::Output { +        self::Renderer::draw( +            renderer, +            defaults, +            cursor_position, +            layout, +            viewport, +            &self.content, +            &self.tooltip, +            self.position, +            &self.style, +            self.gap, +            self.padding, +        ) +    } + +    fn hash_layout(&self, state: &mut Hasher) { +        struct Marker; +        std::any::TypeId::of::<Marker>().hash(state); + +        self.content.hash_layout(state); +    } +} + +/// The renderer of a [`Tooltip`]. +/// +/// Your [renderer] will need to implement this trait before being +/// able to use a [`Tooltip`] in your user interface. +/// +/// [`Tooltip`]: struct.Tooltip.html +/// [renderer]: ../../renderer/index.html +pub trait Renderer: +    crate::Renderer + text::Renderer + container::Renderer +{ +    /// The default padding of a [`Tooltip`] drawn by this renderer. +    const DEFAULT_PADDING: u16; + +    /// Draws a [`Tooltip`]. +    /// +    /// [`Tooltip`]: struct.Tooltip.html +    fn draw<Message>( +        &mut self, +        defaults: &Self::Defaults, +        cursor_position: Point, +        content_layout: Layout<'_>, +        viewport: &Rectangle, +        content: &Element<'_, Message, Self>, +        tooltip: &Text<Self>, +        position: Position, +        style: &<Self as container::Renderer>::Style, +        gap: u16, +        padding: u16, +    ) -> Self::Output; +} + +impl<'a, Message, Renderer> From<Tooltip<'a, Message, Renderer>> +    for Element<'a, Message, Renderer> +where +    Renderer: 'a + self::Renderer, +    Message: 'a, +{ +    fn from( +        column: Tooltip<'a, Message, Renderer>, +    ) -> Element<'a, Message, Renderer> { +        Element::new(column) +    } +} diff --git a/native/src/window/event.rs b/native/src/window/event.rs index b177141a..3aa1ab0b 100644 --- a/native/src/window/event.rs +++ b/native/src/window/event.rs @@ -3,7 +3,7 @@ use std::path::PathBuf;  /// A window-related event.  #[derive(PartialEq, Clone, Debug)]  pub enum Event { -    /// A window was resized +    /// A window was resized.      Resized {          /// The new width of the window (in units)          width: u32, @@ -12,6 +12,18 @@ pub enum Event {          height: u32,      }, +    /// The user has requested for the window to close. +    /// +    /// Usually, you will want to terminate the execution whenever this event +    /// occurs. +    CloseRequested, + +    /// A window was focused. +    Focused, + +    /// A window was unfocused. +    Unfocused, +      /// A file is being hovered over the window.      ///      /// When the user hovers multiple files at once, this event will be emitted  | 
