diff --git a/crates/gpui/src/elements/div.rs b/crates/gpui/src/elements/div.rs index efc931f05ffbed..be3b61932942c0 100644 --- a/crates/gpui/src/elements/div.rs +++ b/crates/gpui/src/elements/div.rs @@ -22,8 +22,8 @@ use crate::{ KeyDownEvent, KeyUpEvent, KeyboardButton, KeyboardClickEvent, LayoutId, ModifiersChangedEvent, MouseButton, MouseClickEvent, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Overflow, ParentElement, Pixels, Point, Render, ScrollWheelEvent, SharedString, Size, Style, - StyleRefinement, Styled, Task, TooltipId, Visibility, Window, WindowControlArea, point, px, - size, + StyleRefinement, Styled, Task, TooltipId, Visibility, Window, WindowControlArea, ZoomEvent, + point, px, size, }; use collections::HashMap; use refineable::Refineable; @@ -321,6 +321,19 @@ impl Interactivity { })); } + /// Bind the given callback to zoom events (e.g. from pinch-to-zoom) during the bubble phase. + /// The imperative API equivalent to [`InteractiveElement::on_zoom`]. + /// + /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. + pub fn on_zoom(&mut self, listener: impl Fn(&ZoomEvent, &mut Window, &mut App) + 'static) { + self.zoom_listeners + .push(Box::new(move |event, phase, hitbox, window, cx| { + if phase == DispatchPhase::Bubble && hitbox.should_handle_scroll(window) { + (listener)(event, window, cx); + } + })); + } + /// Bind the given callback to an action dispatch during the capture phase /// The imperative API equivalent to [`InteractiveElement::capture_action`] /// @@ -582,8 +595,8 @@ impl Interactivity { self.window_control = Some(area); } - /// Block non-scroll mouse interactions with elements behind this element's hitbox. See - /// [`Hitbox::is_hovered`] for details. + /// Block all mouse interactions with elements behind this element's hitbox except for scrolling + /// and zooming (e.g. pinch-to-zoom). See [`Hitbox::is_hovered`] for details. /// /// The imperative API equivalent to [`InteractiveElement::block_mouse_except_scroll`] pub fn block_mouse_except_scroll(&mut self) { @@ -835,6 +848,15 @@ pub trait InteractiveElement: Sized { self } + /// Bind the given callback to zoom events (e.g. from pinch-to-zoom) during the bubble phase. + /// The fluent API equivalent to [`Interactivity::on_zoom`]. + /// + /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. + fn on_zoom(mut self, listener: impl Fn(&ZoomEvent, &mut Window, &mut App) + 'static) -> Self { + self.interactivity().on_zoom(listener); + self + } + /// Capture the given action, before normal action dispatch can fire /// The fluent API equivalent to [`Interactivity::on_scroll_wheel`] /// @@ -1006,8 +1028,8 @@ pub trait InteractiveElement: Sized { self } - /// Block non-scroll mouse interactions with elements behind this element's hitbox. See - /// [`Hitbox::is_hovered`] for details. + /// Block all mouse interactions with elements behind this element's hitbox except for scrolling + /// and zooming (e.g. pinch-to-zoom). See [`Hitbox::is_hovered`] for details. /// /// The fluent API equivalent to [`Interactivity::block_mouse_except_scroll`] fn block_mouse_except_scroll(mut self) -> Self { @@ -1204,6 +1226,9 @@ pub(crate) type MouseMoveListener = pub(crate) type ScrollWheelListener = Box; +pub(crate) type ZoomListener = + Box; + pub(crate) type ClickListener = Rc; pub(crate) type DragListener = @@ -1523,6 +1548,7 @@ pub struct Interactivity { pub(crate) mouse_up_listeners: Vec, pub(crate) mouse_move_listeners: Vec, pub(crate) scroll_wheel_listeners: Vec, + pub(crate) zoom_listeners: Vec, pub(crate) key_down_listeners: Vec, pub(crate) key_up_listeners: Vec, pub(crate) modifiers_changed_listeners: Vec, @@ -1718,6 +1744,7 @@ impl Interactivity { || !self.mouse_move_listeners.is_empty() || !self.click_listeners.is_empty() || !self.scroll_wheel_listeners.is_empty() + || !self.zoom_listeners.is_empty() || self.drag_listener.is_some() || !self.drop_listeners.is_empty() || self.tooltip_builder.is_some() @@ -2078,6 +2105,13 @@ impl Interactivity { }) } + for listener in self.zoom_listeners.drain(..) { + let hitbox = hitbox.clone(); + window.on_mouse_event(move |event: &ZoomEvent, phase, window, cx| { + listener(event, phase, &hitbox, window, cx); + }) + } + if self.hover_style.is_some() || self.base_style.mouse_cursor.is_some() || cx.active_drag.is_some() && !self.drag_over_styles.is_empty() diff --git a/crates/gpui/src/interactive.rs b/crates/gpui/src/interactive.rs index dafe623dfada7b..cb07c509dab50e 100644 --- a/crates/gpui/src/interactive.rs +++ b/crates/gpui/src/interactive.rs @@ -465,6 +465,55 @@ impl ScrollDelta { } } +/// A zoom event from the platform (e.g. pinch-to-zoom). +#[derive(Clone, Debug, Default)] +pub struct ZoomEvent { + /// The position of the mouse on the window. + pub position: Point, + + /// The change in zoom amount for this event. + pub delta: ZoomDelta, + + /// The modifiers that were held down when the zoom amount changed. + pub modifiers: Modifiers, + + /// The phase of the touch event. + pub touch_phase: TouchPhase, +} + +impl Sealed for ZoomEvent {} +impl InputEvent for ZoomEvent { + fn to_platform_input(self) -> PlatformInput { + PlatformInput::Zoom(self) + } +} +impl MouseEvent for ZoomEvent {} + +impl Deref for ZoomEvent { + type Target = Modifiers; + + fn deref(&self) -> &Self::Target { + &self.modifiers + } +} + +/// The zoom delta for a zoom event. +/// +/// Marked non-exhaustive until pinch-to-zoom is implemented for Windows/Linux, which +/// may represent zoom amount with a pixel distance instead. +#[derive(Clone, Copy, Debug)] +#[non_exhaustive] +pub enum ZoomDelta { + /// How much the zoom amount (1 = no zoom) would change. + ZoomAmount(f32), +} + +impl Default for ZoomDelta { + fn default() -> Self { + Self::ZoomAmount(Default::default()) + } +} + /// A mouse exit event from the platform, generated when the mouse leaves the window. #[derive(Clone, Debug, Default)] pub struct MouseExitEvent { @@ -561,6 +610,8 @@ pub enum PlatformInput { MouseExited(MouseExitEvent), /// The scroll wheel was used. ScrollWheel(ScrollWheelEvent), + /// The window was zoomed in or out (e.g. with pinch-to-zoom). + Zoom(ZoomEvent), /// Files were dragged and dropped onto the window. FileDrop(FileDropEvent), } @@ -576,6 +627,7 @@ impl PlatformInput { PlatformInput::MouseMove(event) => Some(event), PlatformInput::MouseExited(event) => Some(event), PlatformInput::ScrollWheel(event) => Some(event), + PlatformInput::Zoom(event) => Some(event), PlatformInput::FileDrop(event) => Some(event), } } @@ -590,6 +642,7 @@ impl PlatformInput { PlatformInput::MouseMove(_) => None, PlatformInput::MouseExited(_) => None, PlatformInput::ScrollWheel(_) => None, + PlatformInput::Zoom(_) => None, PlatformInput::FileDrop(_) => None, } } diff --git a/crates/gpui/src/platform/mac/events.rs b/crates/gpui/src/platform/mac/events.rs index 938db4b76205ee..a12165a25abce4 100644 --- a/crates/gpui/src/platform/mac/events.rs +++ b/crates/gpui/src/platform/mac/events.rs @@ -1,7 +1,7 @@ use crate::{ Capslock, KeyDownEvent, KeyUpEvent, Keystroke, Modifiers, ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseExitEvent, MouseMoveEvent, MouseUpEvent, NavigationDirection, Pixels, - PlatformInput, ScrollDelta, ScrollWheelEvent, TouchPhase, + PlatformInput, ScrollDelta, ScrollWheelEvent, TouchPhase, ZoomDelta, ZoomEvent, platform::mac::{ LMGetKbdType, NSStringExt, TISCopyCurrentKeyboardLayoutInputSource, TISGetInputSourceProperty, UCKeyTranslate, kTISPropertyUnicodeKeyLayoutData, @@ -243,6 +243,25 @@ impl PlatformInput { modifiers: read_modifiers(native_event), }) }), + NSEventType::NSEventTypeMagnify => window_height.map(|window_height| { + let phase = match native_event.phase() { + NSEventPhase::NSEventPhaseMayBegin | NSEventPhase::NSEventPhaseBegan => { + TouchPhase::Started + } + NSEventPhase::NSEventPhaseEnded => TouchPhase::Ended, + _ => TouchPhase::Moved, + }; + + Self::Zoom(ZoomEvent { + position: point( + px(native_event.locationInWindow().x as f32), + window_height - px(native_event.locationInWindow().y as f32), + ), + delta: ZoomDelta::ZoomAmount(native_event.magnification() as f32), + modifiers: read_modifiers(native_event), + touch_phase: phase, + }) + }), NSEventType::NSLeftMouseDragged | NSEventType::NSRightMouseDragged | NSEventType::NSOtherMouseDragged => { diff --git a/crates/gpui/src/platform/mac/window.rs b/crates/gpui/src/platform/mac/window.rs index 95efffa3e77cdb..410d643f48ab98 100644 --- a/crates/gpui/src/platform/mac/window.rs +++ b/crates/gpui/src/platform/mac/window.rs @@ -169,6 +169,10 @@ unsafe fn build_classes() { sel!(swipeWithEvent:), handle_view_event as extern "C" fn(&Object, Sel, id), ); + decl.add_method( + sel!(magnifyWithEvent:), + handle_view_event as extern "C" fn(&Object, Sel, id), + ); decl.add_method( sel!(flagsChanged:), handle_view_event as extern "C" fn(&Object, Sel, id), diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index 0610ea96cb5150..717e1c9f8e109d 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -500,8 +500,8 @@ pub struct HitboxId(u64); impl HitboxId { /// Checks if the hitbox with this ID is currently hovered. Except when handling - /// `ScrollWheelEvent`, this is typically what you want when determining whether to handle mouse - /// events or paint hover styles. + /// `ScrollWheelEvent` and `ZoomEvent`, this is typically what you want when determining + /// whether to handle mouse events or paint hover styles. /// /// See [`Hitbox::is_hovered`] for details. pub fn is_hovered(self, window: &Window) -> bool { @@ -514,10 +514,11 @@ impl HitboxId { false } - /// Checks if the hitbox with this ID contains the mouse and should handle scroll events. - /// Typically this should only be used when handling `ScrollWheelEvent`, and otherwise - /// `is_hovered` should be used. See the documentation of `Hitbox::is_hovered` for details about - /// this distinction. + /// Checks if the hitbox with this ID contains the mouse. + /// + /// Typically this should only be used when handling `ScrollWheelEvent` and `ZoomEvent`, and + /// otherwise `is_hovered` should be used. See the documentation of `Hitbox::is_hovered` for + /// details about this distinction. pub fn should_handle_scroll(self, window: &Window) -> bool { window.mouse_hit_test.ids.contains(&self) } @@ -543,17 +544,17 @@ pub struct Hitbox { } impl Hitbox { - /// Checks if the hitbox is currently hovered. Except when handling `ScrollWheelEvent`, this is - /// typically what you want when determining whether to handle mouse events or paint hover - /// styles. + /// Checks if the hitbox is currently hovered. Except when handling `ScrollWheelEvent` and + /// `ZoomEvent`, this is typically what you want when determining whether to handle mouse events + /// or paint hover styles. /// /// This can return `false` even when the hitbox contains the mouse, if a hitbox in front of /// this sets `HitboxBehavior::BlockMouse` (`InteractiveElement::occlude`) or /// `HitboxBehavior::BlockMouseExceptScroll` (`InteractiveElement::block_mouse_except_scroll`). /// - /// Handling of `ScrollWheelEvent` should typically use `should_handle_scroll` instead. - /// Concretely, this is due to use-cases like overlays that cause the elements under to be - /// non-interactive while still allowing scrolling. More abstractly, this is because + /// Handling of `ScrollWheelEvent` and `ZoomEvent` should typically use `should_handle_scroll` + /// instead. Concretely, this is due to use-cases like overlays that cause the elements under to + /// be non-interactive while still allowing scrolling. More abstractly, this is because /// `is_hovered` is about element interactions directly under the mouse - mouse moves, clicks, /// hover styling, etc. In contrast, scrolling is about finding the current outer scrollable /// container. @@ -562,8 +563,8 @@ impl Hitbox { } /// Checks if the hitbox contains the mouse and should handle scroll events. Typically this - /// should only be used when handling `ScrollWheelEvent`, and otherwise `is_hovered` should be - /// used. See the documentation of `Hitbox::is_hovered` for details about this distinction. + /// should only be used when handling `ScrollWheelEvent` and `ZoomEvent`, and otherwise `is_hovered` + /// should be used. See the documentation of `Hitbox::is_hovered` for details about this distinction. /// /// This can return `false` even when the hitbox contains the mouse, if a hitbox in front of /// this sets `HitboxBehavior::BlockMouse` (`InteractiveElement::occlude`). @@ -604,12 +605,13 @@ pub enum HitboxBehavior { /// All hitboxes behind this hitbox will have `hitbox.is_hovered() == false`, even when /// `hitbox.should_handle_scroll() == true`. Typically for elements this causes all mouse - /// interaction except scroll events to be ignored - see the documentation of - /// [`Hitbox::is_hovered`] for details. This flag is set by + /// interaction except scroll and zoom (e.g. pinch-to-zoom) events to be ignored - see the + /// documentation of [`Hitbox::is_hovered`] for details. This flag is set by /// [`InteractiveElement::block_mouse_except_scroll`]. /// /// For mouse handlers that check those hitboxes, this behaves the same as registering a - /// bubble-phase handler for every mouse event type **except** `ScrollWheelEvent`: + /// bubble-phase handler for every mouse event type **except** `ScrollWheelEvent` and + /// `ZoomEvent`: /// /// ```ignore /// window.on_mouse_event(move |_: &EveryMouseEventTypeExceptScroll, phase, window, cx| { @@ -619,9 +621,10 @@ pub enum HitboxBehavior { /// }) /// ``` /// - /// See the documentation of [`Hitbox::is_hovered`] for details of why `ScrollWheelEvent` is - /// handled differently than other mouse events. If also blocking these scroll events is - /// desired, then a `cx.stop_propagation()` handler like the one above can be used. + /// See the documentation of [`Hitbox::is_hovered`] for details of why `ScrollWheelEvent` + /// and `ZoomEvent` are handled differently than other mouse events. If also blocking these + /// scroll and zoom events is desired, then a `cx.stop_propagation()` handler like the one + /// above can be used. /// /// This has effects beyond event handling - this affects any use of `is_hovered`, such as /// hover styles and tooltops. These other behaviors are the main point of this mechanism. @@ -3638,6 +3641,11 @@ impl Window { self.modifiers = scroll_wheel.modifiers; PlatformInput::ScrollWheel(scroll_wheel) } + PlatformInput::Zoom(zoom) => { + self.mouse_position = zoom.position; + self.modifiers = zoom.modifiers; + PlatformInput::Zoom(zoom) + } // Translate dragging and dropping of external files from the operating system // to internal drag and drop events. PlatformInput::FileDrop(file_drop) => match file_drop {