Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 40 additions & 6 deletions crates/gpui/src/elements/div.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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`]
///
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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`]
///
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -1204,6 +1226,9 @@ pub(crate) type MouseMoveListener =
pub(crate) type ScrollWheelListener =
Box<dyn Fn(&ScrollWheelEvent, DispatchPhase, &Hitbox, &mut Window, &mut App) + 'static>;

pub(crate) type ZoomListener =
Box<dyn Fn(&ZoomEvent, DispatchPhase, &Hitbox, &mut Window, &mut App) + 'static>;

pub(crate) type ClickListener = Rc<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>;

pub(crate) type DragListener =
Expand Down Expand Up @@ -1523,6 +1548,7 @@ pub struct Interactivity {
pub(crate) mouse_up_listeners: Vec<MouseUpListener>,
pub(crate) mouse_move_listeners: Vec<MouseMoveListener>,
pub(crate) scroll_wheel_listeners: Vec<ScrollWheelListener>,
pub(crate) zoom_listeners: Vec<ZoomListener>,
pub(crate) key_down_listeners: Vec<KeyDownListener>,
pub(crate) key_up_listeners: Vec<KeyUpListener>,
pub(crate) modifiers_changed_listeners: Vec<ModifiersChangedListener>,
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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()
Expand Down
53 changes: 53 additions & 0 deletions crates/gpui/src/interactive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Pixels>,

/// 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 {
Expand Down Expand Up @@ -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),
}
Expand All @@ -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),
}
}
Expand All @@ -590,6 +642,7 @@ impl PlatformInput {
PlatformInput::MouseMove(_) => None,
PlatformInput::MouseExited(_) => None,
PlatformInput::ScrollWheel(_) => None,
PlatformInput::Zoom(_) => None,
PlatformInput::FileDrop(_) => None,
}
}
Expand Down
21 changes: 20 additions & 1 deletion crates/gpui/src/platform/mac/events.rs
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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 => {
Expand Down
4 changes: 4 additions & 0 deletions crates/gpui/src/platform/mac/window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
48 changes: 28 additions & 20 deletions crates/gpui/src/window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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)
}
Expand All @@ -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.
Expand All @@ -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`).
Expand Down Expand Up @@ -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| {
Expand All @@ -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.
Expand Down Expand Up @@ -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 {
Expand Down