diff options
Diffstat (limited to 'native/src')
-rw-r--r-- | native/src/event.rs | 8 | ||||
-rw-r--r-- | native/src/lib.rs | 1 | ||||
-rw-r--r-- | native/src/touch.rs | 23 | ||||
-rw-r--r-- | native/src/widget/button.rs | 25 | ||||
-rw-r--r-- | native/src/widget/checkbox.rs | 4 | ||||
-rw-r--r-- | native/src/widget/image.rs | 3 | ||||
-rw-r--r-- | native/src/widget/image/viewer.rs | 392 | ||||
-rw-r--r-- | native/src/widget/radio.rs | 4 | ||||
-rw-r--r-- | native/src/widget/scrollable.rs | 48 | ||||
-rw-r--r-- | native/src/widget/slider.rs | 46 | ||||
-rw-r--r-- | native/src/widget/text_input.rs | 13 |
11 files changed, 531 insertions, 36 deletions
diff --git a/native/src/event.rs b/native/src/event.rs index 0e86171e..205bb797 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,9 @@ pub enum Event { /// A window event Window(window::Event), + + /// A touch event + Touch(touch::Event), } /// The status of an [`Event`] after being processed. diff --git a/native/src/lib.rs b/native/src/lib.rs index f9a99c48..0890785b 100644 --- a/native/src/lib.rs +++ b/native/src/lib.rs @@ -41,6 +41,7 @@ pub mod overlay; pub mod program; pub mod renderer; pub mod subscription; +pub mod touch; pub mod widget; pub mod window; 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/widget/button.rs b/native/src/widget/button.rs index dca20e13..b8c14634 100644 --- a/native/src/widget/button.rs +++ b/native/src/widget/button.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, Widget, }; @@ -160,11 +161,23 @@ where layout: Layout<'_>, cursor_position: Point, messages: &mut Vec<Message>, - _renderer: &Renderer, - _clipboard: Option<&dyn Clipboard>, + 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, + messages, + renderer, + clipboard, + ) { + 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 +188,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 +204,9 @@ where } } } + Event::Touch(touch::Event::FingerLost { .. }) => { + self.state.is_pressed = false; + } _ => {} } diff --git a/native/src/widget/checkbox.rs b/native/src/widget/checkbox.rs index 81420458..77a82fad 100644 --- a/native/src/widget/checkbox.rs +++ b/native/src/widget/checkbox.rs @@ -6,6 +6,7 @@ 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, @@ -154,7 +155,8 @@ where _clipboard: Option<&dyn Clipboard>, ) -> event::Status { match event { - Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => { + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerPressed { .. }) => { let mouse_over = layout.bounds().contains(cursor_position); if mouse_over { 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..4ec3faf6 --- /dev/null +++ b/native/src/widget/image/viewer.rs @@ -0,0 +1,392 @@ +//! 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 aspect_ratio = width as f32 / height as f32; + + let mut size = limits + .width(self.width) + .height(self.height) + .resolve(Size::new(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; + } + + layout::Node::new(size) + } + + fn on_event( + &mut self, + event: Event, + layout: Layout<'_>, + cursor_position: Point, + _messages: &mut Vec<Message>, + renderer: &Renderer, + _clipboard: Option<&dyn Clipboard>, + ) -> event::Status { + let 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::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::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) => { + self.state.cursor_grabbed_at = None + } + 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::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/radio.rs b/native/src/widget/radio.rs index 4935569f..69952345 100644 --- a/native/src/widget/radio.rs +++ b/native/src/widget/radio.rs @@ -4,6 +4,7 @@ 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, @@ -160,7 +161,8 @@ where _clipboard: Option<&dyn Clipboard>, ) -> event::Status { match event { - Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => { + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerPressed { .. }) => { if layout.bounds().contains(cursor_position) { messages.push(self.on_click.clone()); diff --git a/native/src/widget/scrollable.rs b/native/src/widget/scrollable.rs index e23ab06a..18cdf169 100644 --- a/native/src/widget/scrollable.rs +++ b/native/src/widget/scrollable.rs @@ -4,6 +4,7 @@ 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, Rectangle, Size, Vector, Widget, @@ -229,6 +230,37 @@ where return event::Status::Captured; } + Event::Touch(event) => { + match event { + touch::Event::FingerPressed { .. } => { + self.state.scroll_box_touched_at = + Some(cursor_position); + } + touch::Event::FingerMoved { .. } => { + if let Some(scroll_box_touched_at) = + self.state.scroll_box_touched_at + { + let delta = + cursor_position.y - scroll_box_touched_at.y; + + self.state.scroll( + delta, + bounds, + content_bounds, + ); + + self.state.scroll_box_touched_at = + Some(cursor_position); + } + } + touch::Event::FingerLifted { .. } + | touch::Event::FingerLost { .. } => { + self.state.scroll_box_touched_at = None; + } + } + + return event::Status::Captured; + } _ => {} } } @@ -237,12 +269,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) { @@ -264,7 +299,8 @@ where match event { Event::Mouse(mouse::Event::ButtonPressed( mouse::Button::Left, - )) => { + )) + | Event::Touch(touch::Event::FingerPressed { .. }) => { if let Some(scrollbar) = scrollbar { if let Some(scroller_grabbed_at) = scrollbar.grab_scroller(cursor_position) @@ -385,6 +421,7 @@ where #[derive(Debug, Clone, Copy, Default)] pub struct State { scroller_grabbed_at: Option<f32>, + scroll_box_touched_at: Option<Point>, offset: f32, } @@ -439,6 +476,11 @@ impl State { pub fn is_scroller_grabbed(&self) -> bool { self.scroller_grabbed_at.is_some() } + + /// Returns whether the scroll box is currently touched or not. + pub fn is_scroll_box_touched(&self) -> bool { + self.scroll_box_touched_at.is_some() + } } /// The scrollbar of a [`Scrollable`]. diff --git a/native/src/widget/slider.rs b/native/src/widget/slider.rs index ff39b816..010c6e53 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, }; @@ -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..2fd9cec1 100644 --- a/native/src/widget/text_input.rs +++ b/native/src/widget/text_input.rs @@ -16,6 +16,7 @@ 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, }; @@ -247,7 +248,8 @@ where clipboard: Option<&dyn Clipboard>, ) -> event::Status { match event { - Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => { + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerPressed { .. }) => { let is_clicked = layout.bounds().contains(cursor_position); self.state.is_focused = is_clicked; @@ -318,13 +320,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 { |