diff --git a/core/src/mouse/event.rs b/core/src/mouse/event.rs index 7f5585ada0..65914de91d 100644 --- a/core/src/mouse/event.rs +++ b/core/src/mouse/event.rs @@ -1,4 +1,4 @@ -use crate::Point; +use crate::{Point, Vector}; use super::Button; @@ -22,6 +22,21 @@ pub enum Event { position: Point, }, + /// The mouse was moved. + /// + /// This will fire in situations where [`CursorMoved`] might not, + /// such as the mouse being outside of the window or hitting the edge + /// of the monitor, and can be used to get the correct motion when + /// [`CursorGrab`] is set to something other than [`None`]. + /// + /// [`CursorMoved`]: Event::CursorMoved + /// [`CursorGrab`]: super::super::window::CursorGrab + /// [`None`]: super::super::window::CursorGrab::None + MouseMotion { + /// The change in position of the mouse cursor + delta: Vector, + }, + /// A mouse button was pressed. ButtonPressed(Button), diff --git a/core/src/point.rs b/core/src/point.rs index cea57518e9..c7d725fe5b 100644 --- a/core/src/point.rs +++ b/core/src/point.rs @@ -74,6 +74,16 @@ where } } +impl std::ops::AddAssign> for Point +where + T: std::ops::AddAssign, +{ + fn add_assign(&mut self, vector: Vector) { + self.x += vector.x; + self.y += vector.y; + } +} + impl std::ops::Sub> for Point where T: std::ops::Sub, @@ -88,6 +98,16 @@ where } } +impl std::ops::SubAssign> for Point +where + T: std::ops::SubAssign, +{ + fn sub_assign(&mut self, vector: Vector) { + self.x -= vector.x; + self.y -= vector.y; + } +} + impl std::ops::Sub> for Point where T: std::ops::Sub, @@ -99,6 +119,16 @@ where } } +impl std::ops::SubAssign> for Point +where + T: std::ops::SubAssign, +{ + fn sub_assign(&mut self, point: Point) { + self.x -= point.x; + self.y -= point.y; + } +} + impl fmt::Display for Point where T: fmt::Display, diff --git a/core/src/vector.rs b/core/src/vector.rs index ff848c4f37..7639ce1d63 100644 --- a/core/src/vector.rs +++ b/core/src/vector.rs @@ -37,8 +37,18 @@ where { type Output = Self; - fn add(self, b: Self) -> Self { - Self::new(self.x + b.x, self.y + b.y) + fn add(self, rhs: Self) -> Self { + Self::new(self.x + rhs.x, self.y + rhs.y) + } +} + +impl std::ops::AddAssign for Vector +where + T: std::ops::AddAssign, +{ + fn add_assign(&mut self, rhs: Self) { + self.x += rhs.x; + self.y += rhs.y; } } @@ -48,8 +58,18 @@ where { type Output = Self; - fn sub(self, b: Self) -> Self { - Self::new(self.x - b.x, self.y - b.y) + fn sub(self, rhs: Self) -> Self { + Self::new(self.x - rhs.x, self.y - rhs.y) + } +} + +impl std::ops::SubAssign for Vector +where + T: std::ops::SubAssign, +{ + fn sub_assign(&mut self, rhs: Self) { + self.x -= rhs.x; + self.y -= rhs.y; } } @@ -64,6 +84,37 @@ where } } +impl std::ops::MulAssign for Vector +where + T: std::ops::MulAssign + Copy, +{ + fn mul_assign(&mut self, scale: T) { + self.x *= scale; + self.y *= scale; + } +} + +impl std::ops::Div for Vector +where + T: std::ops::Div + Copy, +{ + type Output = Self; + + fn div(self, scale: T) -> Self { + Self::new(self.x / scale, self.y / scale) + } +} + +impl std::ops::DivAssign for Vector +where + T: std::ops::DivAssign + Copy, +{ + fn div_assign(&mut self, scale: T) { + self.x /= scale; + self.y /= scale; + } +} + impl Default for Vector where T: Default, diff --git a/core/src/window.rs b/core/src/window.rs index 448ffc4503..703a81a500 100644 --- a/core/src/window.rs +++ b/core/src/window.rs @@ -2,6 +2,7 @@ pub mod icon; pub mod settings; +mod cursor_grab; mod event; mod id; mod level; @@ -10,6 +11,7 @@ mod position; mod redraw_request; mod user_attention; +pub use cursor_grab::CursorGrab; pub use event::Event; pub use icon::Icon; pub use id::Id; diff --git a/core/src/window/cursor_grab.rs b/core/src/window/cursor_grab.rs new file mode 100644 index 0000000000..01b872405a --- /dev/null +++ b/core/src/window/cursor_grab.rs @@ -0,0 +1,29 @@ +/// The behavior of cursor grabbing. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub enum CursorGrab { + /// No grabbing of the cursor is performed. + #[default] + None, + + /// The cursor is confined to the window area. + /// + /// There's no guarantee that the cursor will be hidden. You should hide it by yourself if you + /// want to do so. + /// + /// ## Platform-specific + /// + /// - **macOS:** Not implemented. + /// - **iOS / Android / Web:** Unsupported. + Confined, + + /// The cursor is locked inside the window area to the certain position. + /// + /// There's no guarantee that the cursor will be hidden. You should hide it by yourself if you + /// want to do so. + /// + /// ## Platform-specific + /// + /// - **X11 / Windows:** Not implemented. + /// - **iOS / Android:** Unsupported. + Locked, +} diff --git a/examples/game_of_life/src/main.rs b/examples/game_of_life/src/main.rs index 9dcebecc4b..7e6a5cfbd5 100644 --- a/examples/game_of_life/src/main.rs +++ b/examples/game_of_life/src/main.rs @@ -64,7 +64,7 @@ impl GameOfLife { match message { Message::Grid(message, version) => { if version == self.version { - self.grid.update(message); + return self.grid.update(message).discard(); } } Message::Tick | Message::Next => { @@ -195,6 +195,9 @@ mod grid { use iced::widget::canvas; use iced::widget::canvas::event::{self, Event}; use iced::widget::canvas::{Cache, Canvas, Frame, Geometry, Path, Text}; + use iced::window; + use iced::window::CursorGrab; + use iced::Task; use iced::{ Color, Element, Fill, Point, Rectangle, Renderer, Size, Theme, Vector, }; @@ -219,6 +222,7 @@ mod grid { pub enum Message { Populate(Cell), Unpopulate(Cell), + Panning(bool), Translated(Vector), Scaled(f32, Option), Ticked { @@ -282,7 +286,7 @@ mod grid { }) } - pub fn update(&mut self, message: Message) { + pub fn update(&mut self, message: Message) -> Task { match message { Message::Populate(cell) => { self.state.populate(cell); @@ -296,6 +300,22 @@ mod grid { self.preset = Preset::Custom; } + Message::Panning(panning) => { + #[cfg(any(target_os = "linux", target_os = "windows"))] + let cursor_grab = CursorGrab::Confined; + #[cfg(any(target_os = "macos", target_arch = "wasm32"))] + let cursor_grab = CursorGrab::Locked; + return window::get_oldest().then(move |id| { + window::cursor_grab( + id.expect("there is only a single window so it must be the oldest"), + if panning { + cursor_grab + } else { + CursorGrab::None + }, + ) + }); + } Message::Translated(translation) => { self.translation = translation; @@ -327,6 +347,8 @@ mod grid { dbg!(error); } } + + Task::none() } pub fn view(&self) -> Element { @@ -379,13 +401,17 @@ mod grid { fn update( &self, - interaction: &mut Interaction, + mut interaction: &mut Interaction, event: Event, bounds: Rectangle, cursor: mouse::Cursor, ) -> (event::Status, Option) { if let Event::Mouse(mouse::Event::ButtonReleased(_)) = event { *interaction = Interaction::None; + return ( + event::Status::Captured, + Some(Message::Panning(false)), + ); } let Some(cursor_position) = cursor.position_in(bounds) else { @@ -430,36 +456,35 @@ mod grid { mouse::Button::Right => { *interaction = Interaction::Panning { translation: self.translation, - start: cursor_position, }; - None + Some(Message::Panning(true)) } _ => None, }; (event::Status::Captured, message) } - mouse::Event::CursorMoved { .. } => { - let message = match *interaction { - Interaction::Drawing => populate, - Interaction::Erasing => unpopulate, - Interaction::Panning { translation, start } => { - Some(Message::Translated( - translation - + (cursor_position - start) - * (1.0 / self.scaling), - )) + mouse::Event::CursorMoved { .. } => match *interaction { + Interaction::Drawing => { + (event::Status::Captured, populate) + } + Interaction::Erasing => { + (event::Status::Captured, unpopulate) + } + _ => (event::Status::Ignored, None), + }, + mouse::Event::MouseMotion { delta } => { + match &mut interaction { + Interaction::Panning { translation } => { + *translation += delta / self.scaling; + ( + event::Status::Captured, + Some(Message::Translated(*translation)), + ) } - Interaction::None => None, - }; - - let event_status = match interaction { - Interaction::None => event::Status::Ignored, - _ => event::Status::Captured, - }; - - (event_status, message) + _ => (event::Status::Ignored, None), + } } mouse::Event::WheelScrolled { delta } => match delta { mouse::ScrollDelta::Lines { y, .. } @@ -881,7 +906,7 @@ mod grid { None, Drawing, Erasing, - Panning { translation: Vector, start: Point }, + Panning { translation: Vector }, } impl Default for Interaction { diff --git a/runtime/src/window.rs b/runtime/src/window.rs index 27035d7de1..7c9b46e7d3 100644 --- a/runtime/src/window.rs +++ b/runtime/src/window.rs @@ -1,6 +1,7 @@ //! Build window-based GUI applications. pub mod screenshot; +use iced_core::window::CursorGrab; pub use screenshot::Screenshot; use crate::core::time::Instant; @@ -159,6 +160,13 @@ pub enum Action { /// This enables mouse events for the window and stops mouse events /// from being passed to whatever is underneath. DisableMousePassthrough(Id), + + /// Constraints the cursor in a window. + /// + /// ## Platform-specific + /// -- **Web / macOS:** [`CursorGrab::Confined`] unsupported. + /// -- **Windows / X11:** [`CursorGrab::Locked`] unsupported. + CursorGrab(Id, CursorGrab), } /// Subscribes to the frames of the window of the running application. @@ -434,3 +442,8 @@ pub fn enable_mouse_passthrough(id: Id) -> Task { pub fn disable_mouse_passthrough(id: Id) -> Task { task::effect(crate::Action::Window(Action::DisableMousePassthrough(id))) } + +/// Constraints the cursor in a window. +pub fn cursor_grab(id: Id, cursor_grab: CursorGrab) -> Task { + task::effect(crate::Action::Window(Action::CursorGrab(id, cursor_grab))) +} diff --git a/winit/src/conversion.rs b/winit/src/conversion.rs index 43e1848b4a..da49e56646 100644 --- a/winit/src/conversion.rs +++ b/winit/src/conversion.rs @@ -6,7 +6,7 @@ use crate::core::keyboard; use crate::core::mouse; use crate::core::touch; use crate::core::window; -use crate::core::{Event, Point, Size}; +use crate::core::{Event, Point, Size, Vector}; /// Converts some [`window::Settings`] into some `WindowAttributes` from `winit`. pub fn window_attributes( @@ -300,6 +300,25 @@ pub fn window_event( } } +/// Converts a winit device event into an iced event. +pub fn device_event( + event: winit::event::DeviceEvent, + scale_factor: f64, +) -> Option { + use winit::event::DeviceEvent; + + match event { + DeviceEvent::MouseMotion { delta: (x, y) } => { + let x = (x / scale_factor) as f32; + let y = (y / scale_factor) as f32; + Some(Event::Mouse(mouse::Event::MouseMotion { + delta: Vector { x, y }, + })) + } + _ => None, + } +} + /// Converts a [`window::Level`] to a [`winit`] window level. /// /// [`winit`]: https://github.com/rust-windowing/winit @@ -1135,6 +1154,19 @@ pub fn icon(icon: window::Icon) -> Option { winit::window::Icon::from_rgba(pixels, size.width, size.height).ok() } +/// Converts some [`CursorGrab`] into it's `winit` counterpart. +/// +/// [`CursorGrab`]: window::CursorGrab +pub fn cursor_grab( + cursor_grab: window::CursorGrab, +) -> winit::window::CursorGrabMode { + match cursor_grab { + window::CursorGrab::None => winit::window::CursorGrabMode::None, + window::CursorGrab::Confined => winit::window::CursorGrabMode::Confined, + window::CursorGrab::Locked => winit::window::CursorGrabMode::Locked, + } +} + // See: https://en.wikipedia.org/wiki/Private_Use_Areas fn is_private_use(c: char) -> bool { ('\u{E000}'..='\u{F8FF}').contains(&c) diff --git a/winit/src/program.rs b/winit/src/program.rs index 8d1eec3af9..405b5be367 100644 --- a/winit/src/program.rs +++ b/winit/src/program.rs @@ -418,6 +418,21 @@ where } } + fn device_event( + &mut self, + event_loop: &winit::event_loop::ActiveEventLoop, + device_id: winit::event::DeviceId, + event: winit::event::DeviceEvent, + ) { + self.process_event( + event_loop, + Event::EventLoopAwakened(winit::event::Event::DeviceEvent { + device_id, + event, + }), + ); + } + fn user_event( &mut self, event_loop: &winit::event_loop::ActiveEventLoop, @@ -1030,6 +1045,21 @@ async fn run_instance( } } } + event::Event::DeviceEvent { + device_id: _, + event, + } => { + for (id, window) in window_manager.iter_mut() { + let scale_factor = window.raw.scale_factor(); + + if let Some(event) = conversion::device_event( + event.clone(), + scale_factor, + ) { + events.push((id, event)); + } + } + } event::Event::AboutToWait => { if events.is_empty() && messages.is_empty() { continue; @@ -1468,6 +1498,13 @@ fn run_action( let _ = window.raw.set_cursor_hittest(true); } } + window::Action::CursorGrab(id, cursor_grab) => { + if let Some(window) = window_manager.get_mut(id) { + let _ = window + .raw + .set_cursor_grab(conversion::cursor_grab(cursor_grab)); + } + } }, Action::System(action) => match action { system::Action::QueryInformation(_channel) => {