Detailed changes
@@ -15,6 +15,8 @@
//! and Tailwind-like styling that you can use to build your own custom elements. Div is
//! constructed by combining these two systems into an all-in-one element.
+#[cfg(any(target_os = "linux", target_os = "macos"))]
+use crate::PinchEvent;
use crate::{
AbsoluteLength, Action, AnyDrag, AnyElement, AnyTooltip, AnyView, App, Bounds, ClickEvent,
DispatchPhase, Display, Element, ElementId, Entity, FocusHandle, Global, GlobalElementId,
@@ -353,6 +355,43 @@ impl Interactivity {
}));
}
+ /// Bind the given callback to pinch gesture events during the bubble phase.
+ ///
+ /// Note: This event is only available on macOS and Wayland (Linux).
+ /// On Windows, pinch gestures are simulated as scroll wheel events with Ctrl held.
+ ///
+ /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback.
+ #[cfg(any(target_os = "linux", target_os = "macos"))]
+ pub fn on_pinch(&mut self, listener: impl Fn(&PinchEvent, &mut Window, &mut App) + 'static) {
+ self.pinch_listeners
+ .push(Box::new(move |event, phase, hitbox, window, cx| {
+ if phase == DispatchPhase::Bubble && hitbox.is_hovered(window) {
+ (listener)(event, window, cx);
+ }
+ }));
+ }
+
+ /// Bind the given callback to pinch gesture events during the capture phase.
+ ///
+ /// Note: This event is only available on macOS and Wayland (Linux).
+ /// On Windows, pinch gestures are simulated as scroll wheel events with Ctrl held.
+ ///
+ /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback.
+ #[cfg(any(target_os = "linux", target_os = "macos"))]
+ pub fn capture_pinch(
+ &mut self,
+ listener: impl Fn(&PinchEvent, &mut Window, &mut App) + 'static,
+ ) {
+ self.pinch_listeners
+ .push(Box::new(move |event, phase, _hitbox, window, cx| {
+ if phase == DispatchPhase::Capture {
+ (listener)(event, window, cx);
+ } else {
+ cx.propagate();
+ }
+ }));
+ }
+
/// Bind the given callback to an action dispatch during the capture phase.
/// The imperative API equivalent to [`InteractiveElement::capture_action`].
///
@@ -635,6 +674,16 @@ impl Interactivity {
pub fn block_mouse_except_scroll(&mut self) {
self.hitbox_behavior = HitboxBehavior::BlockMouseExceptScroll;
}
+
+ #[cfg(any(target_os = "linux", target_os = "macos"))]
+ fn has_pinch_listeners(&self) -> bool {
+ !self.pinch_listeners.is_empty()
+ }
+
+ #[cfg(not(any(target_os = "linux", target_os = "macos")))]
+ fn has_pinch_listeners(&self) -> bool {
+ false
+ }
}
/// A trait for elements that want to use the standard GPUI event handlers that don't
@@ -905,6 +954,34 @@ pub trait InteractiveElement: Sized {
self
}
+ /// Bind the given callback to pinch gesture events during the bubble phase.
+ /// The fluent API equivalent to [`Interactivity::on_pinch`].
+ ///
+ /// Note: This event is only available on macOS and Wayland (Linux).
+ /// On Windows, pinch gestures are simulated as scroll wheel events with Ctrl held.
+ ///
+ /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback.
+ #[cfg(any(target_os = "linux", target_os = "macos"))]
+ fn on_pinch(mut self, listener: impl Fn(&PinchEvent, &mut Window, &mut App) + 'static) -> Self {
+ self.interactivity().on_pinch(listener);
+ self
+ }
+
+ /// Bind the given callback to pinch gesture events during the capture phase.
+ /// The fluent API equivalent to [`Interactivity::capture_pinch`].
+ ///
+ /// Note: This event is only available on macOS and Wayland (Linux).
+ /// On Windows, pinch gestures are simulated as scroll wheel events with Ctrl held.
+ ///
+ /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback.
+ #[cfg(any(target_os = "linux", target_os = "macos"))]
+ fn capture_pinch(
+ mut self,
+ listener: impl Fn(&PinchEvent, &mut Window, &mut App) + 'static,
+ ) -> Self {
+ self.interactivity().capture_pinch(listener);
+ self
+ }
/// Capture the given action, before normal action dispatch can fire.
/// The fluent API equivalent to [`Interactivity::capture_action`].
///
@@ -1290,6 +1367,10 @@ pub(crate) type MouseMoveListener =
pub(crate) type ScrollWheelListener =
Box<dyn Fn(&ScrollWheelEvent, DispatchPhase, &Hitbox, &mut Window, &mut App) + 'static>;
+#[cfg(any(target_os = "linux", target_os = "macos"))]
+pub(crate) type PinchListener =
+ Box<dyn Fn(&PinchEvent, DispatchPhase, &Hitbox, &mut Window, &mut App) + 'static>;
+
pub(crate) type ClickListener = Rc<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>;
pub(crate) type DragListener =
@@ -1644,6 +1725,8 @@ pub struct Interactivity {
pub(crate) mouse_pressure_listeners: Vec<MousePressureListener>,
pub(crate) mouse_move_listeners: Vec<MouseMoveListener>,
pub(crate) scroll_wheel_listeners: Vec<ScrollWheelListener>,
+ #[cfg(any(target_os = "linux", target_os = "macos"))]
+ pub(crate) pinch_listeners: Vec<PinchListener>,
pub(crate) key_down_listeners: Vec<KeyDownListener>,
pub(crate) key_up_listeners: Vec<KeyUpListener>,
pub(crate) modifiers_changed_listeners: Vec<ModifiersChangedListener>,
@@ -1847,6 +1930,7 @@ impl Interactivity {
|| !self.click_listeners.is_empty()
|| !self.aux_click_listeners.is_empty()
|| !self.scroll_wheel_listeners.is_empty()
+ || self.has_pinch_listeners()
|| self.drag_listener.is_some()
|| !self.drop_listeners.is_empty()
|| self.tooltip_builder.is_some()
@@ -2213,6 +2297,14 @@ impl Interactivity {
})
}
+ #[cfg(any(target_os = "linux", target_os = "macos"))]
+ for listener in self.pinch_listeners.drain(..) {
+ let hitbox = hitbox.clone();
+ window.on_mouse_event(move |event: &PinchEvent, 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()
@@ -17,6 +17,9 @@ pub trait KeyEvent: InputEvent {}
/// A mouse event from the platform.
pub trait MouseEvent: InputEvent {}
+/// A gesture event from the platform.
+pub trait GestureEvent: InputEvent {}
+
/// The key down event equivalent for the platform.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct KeyDownEvent {
@@ -467,6 +470,51 @@ impl Default for ScrollDelta {
}
}
+/// A pinch gesture event from the platform, generated when the user performs
+/// a pinch-to-zoom gesture (typically on a trackpad).
+///
+/// Note: This event is only available on macOS and Wayland (Linux).
+/// On Windows, pinch gestures are simulated as scroll wheel events with Ctrl held.
+#[derive(Clone, Debug, Default)]
+#[cfg(any(target_os = "linux", target_os = "macos"))]
+pub struct PinchEvent {
+ /// The position of the pinch center on the window.
+ pub position: Point<Pixels>,
+
+ /// The zoom delta for this event.
+ /// Positive values indicate zooming in, negative values indicate zooming out.
+ /// For example, 0.1 represents a 10% zoom increase.
+ pub delta: f32,
+
+ /// The modifiers that were held down during the pinch gesture.
+ pub modifiers: Modifiers,
+
+ /// The phase of the pinch gesture.
+ pub phase: TouchPhase,
+}
+
+#[cfg(any(target_os = "linux", target_os = "macos"))]
+impl Sealed for PinchEvent {}
+#[cfg(any(target_os = "linux", target_os = "macos"))]
+impl InputEvent for PinchEvent {
+ fn to_platform_input(self) -> PlatformInput {
+ PlatformInput::Pinch(self)
+ }
+}
+#[cfg(any(target_os = "linux", target_os = "macos"))]
+impl GestureEvent for PinchEvent {}
+#[cfg(any(target_os = "linux", target_os = "macos"))]
+impl MouseEvent for PinchEvent {}
+
+#[cfg(any(target_os = "linux", target_os = "macos"))]
+impl Deref for PinchEvent {
+ type Target = Modifiers;
+
+ fn deref(&self) -> &Self::Target {
+ &self.modifiers
+ }
+}
+
impl ScrollDelta {
/// Returns true if this is a precise scroll delta in pixels.
pub fn precise(&self) -> bool {
@@ -626,6 +674,9 @@ pub enum PlatformInput {
MouseExited(MouseExitEvent),
/// The scroll wheel was used.
ScrollWheel(ScrollWheelEvent),
+ /// A pinch gesture was performed.
+ #[cfg(any(target_os = "linux", target_os = "macos"))]
+ Pinch(PinchEvent),
/// Files were dragged and dropped onto the window.
FileDrop(FileDropEvent),
}
@@ -642,6 +693,8 @@ impl PlatformInput {
PlatformInput::MousePressure(event) => Some(event),
PlatformInput::MouseExited(event) => Some(event),
PlatformInput::ScrollWheel(event) => Some(event),
+ #[cfg(any(target_os = "linux", target_os = "macos"))]
+ PlatformInput::Pinch(event) => Some(event),
PlatformInput::FileDrop(event) => Some(event),
}
}
@@ -657,6 +710,8 @@ impl PlatformInput {
PlatformInput::MousePressure(_) => None,
PlatformInput::MouseExited(_) => None,
PlatformInput::ScrollWheel(_) => None,
+ #[cfg(any(target_os = "linux", target_os = "macos"))]
+ PlatformInput::Pinch(_) => None,
PlatformInput::FileDrop(_) => None,
}
}
@@ -3945,6 +3945,12 @@ impl Window {
self.modifiers = scroll_wheel.modifiers;
PlatformInput::ScrollWheel(scroll_wheel)
}
+ #[cfg(any(target_os = "linux", target_os = "macos"))]
+ PlatformInput::Pinch(pinch) => {
+ self.mouse_position = pinch.position;
+ self.modifiers = pinch.modifiers;
+ PlatformInput::Pinch(pinch)
+ }
// Translate dragging and dropping of external files from the operating system
// to internal drag and drop events.
PlatformInput::FileDrop(file_drop) => match file_drop {
@@ -36,6 +36,9 @@ use wayland_client::{
wl_shm_pool, wl_surface,
},
};
+use wayland_protocols::wp::pointer_gestures::zv1::client::{
+ zwp_pointer_gesture_pinch_v1, zwp_pointer_gestures_v1,
+};
use wayland_protocols::wp::primary_selection::zv1::client::zwp_primary_selection_offer_v1::{
self, ZwpPrimarySelectionOfferV1,
};
@@ -124,6 +127,7 @@ pub struct Globals {
pub layer_shell: Option<zwlr_layer_shell_v1::ZwlrLayerShellV1>,
pub blur_manager: Option<org_kde_kwin_blur_manager::OrgKdeKwinBlurManager>,
pub text_input_manager: Option<zwp_text_input_manager_v3::ZwpTextInputManagerV3>,
+ pub gesture_manager: Option<zwp_pointer_gestures_v1::ZwpPointerGesturesV1>,
pub dialog: Option<xdg_wm_dialog_v1::XdgWmDialogV1>,
pub executor: ForegroundExecutor,
}
@@ -164,6 +168,7 @@ impl Globals {
layer_shell: globals.bind(&qh, 1..=5, ()).ok(),
blur_manager: globals.bind(&qh, 1..=1, ()).ok(),
text_input_manager: globals.bind(&qh, 1..=1, ()).ok(),
+ gesture_manager: globals.bind(&qh, 1..=3, ()).ok(),
dialog: globals.bind(&qh, dialog_v..=dialog_v, ()).ok(),
executor,
qh,
@@ -208,6 +213,8 @@ pub(crate) struct WaylandClientState {
pub compositor_gpu: Option<CompositorGpuHint>,
wl_seat: wl_seat::WlSeat, // TODO: Multi seat support
wl_pointer: Option<wl_pointer::WlPointer>,
+ pinch_gesture: Option<zwp_pointer_gesture_pinch_v1::ZwpPointerGesturePinchV1>,
+ pinch_scale: f32,
wl_keyboard: Option<wl_keyboard::WlKeyboard>,
cursor_shape_device: Option<wp_cursor_shape_device_v1::WpCursorShapeDeviceV1>,
data_device: Option<wl_data_device::WlDataDevice>,
@@ -584,6 +591,8 @@ impl WaylandClient {
wl_seat: seat,
wl_pointer: None,
wl_keyboard: None,
+ pinch_gesture: None,
+ pinch_scale: 1.0,
cursor_shape_device: None,
data_device,
primary_selection,
@@ -1325,6 +1334,12 @@ impl Dispatch<wl_seat::WlSeat, ()> for WaylandClientStatePtr {
.as_ref()
.map(|cursor_shape_manager| cursor_shape_manager.get_pointer(&pointer, qh, ()));
+ state.pinch_gesture = state.globals.gesture_manager.as_ref().map(
+ |gesture_manager: &zwp_pointer_gestures_v1::ZwpPointerGesturesV1| {
+ gesture_manager.get_pinch_gesture(&pointer, qh, ())
+ },
+ );
+
if let Some(wl_pointer) = &state.wl_pointer {
wl_pointer.release();
}
@@ -1998,6 +2013,91 @@ impl Dispatch<wl_pointer::WlPointer, ()> for WaylandClientStatePtr {
}
}
+impl Dispatch<zwp_pointer_gestures_v1::ZwpPointerGesturesV1, ()> for WaylandClientStatePtr {
+ fn event(
+ _this: &mut Self,
+ _: &zwp_pointer_gestures_v1::ZwpPointerGesturesV1,
+ _: <zwp_pointer_gestures_v1::ZwpPointerGesturesV1 as Proxy>::Event,
+ _: &(),
+ _: &Connection,
+ _: &QueueHandle<Self>,
+ ) {
+ // The gesture manager doesn't generate events
+ }
+}
+
+impl Dispatch<zwp_pointer_gesture_pinch_v1::ZwpPointerGesturePinchV1, ()>
+ for WaylandClientStatePtr
+{
+ fn event(
+ this: &mut Self,
+ _: &zwp_pointer_gesture_pinch_v1::ZwpPointerGesturePinchV1,
+ event: <zwp_pointer_gesture_pinch_v1::ZwpPointerGesturePinchV1 as Proxy>::Event,
+ _: &(),
+ _: &Connection,
+ _: &QueueHandle<Self>,
+ ) {
+ use gpui::PinchEvent;
+
+ let client = this.get_client();
+ let mut state = client.borrow_mut();
+
+ let Some(window) = state.mouse_focused_window.clone() else {
+ return;
+ };
+
+ match event {
+ zwp_pointer_gesture_pinch_v1::Event::Begin {
+ serial: _,
+ time: _,
+ surface: _,
+ fingers: _,
+ } => {
+ state.pinch_scale = 1.0;
+ let input = PlatformInput::Pinch(PinchEvent {
+ position: state.mouse_location.unwrap_or(point(px(0.0), px(0.0))),
+ delta: 0.0,
+ modifiers: state.modifiers,
+ phase: TouchPhase::Started,
+ });
+ drop(state);
+ window.handle_input(input);
+ }
+ zwp_pointer_gesture_pinch_v1::Event::Update { time: _, scale, .. } => {
+ let new_absolute_scale = scale as f32;
+ let previous_scale = state.pinch_scale;
+ let zoom_delta = new_absolute_scale - previous_scale;
+ state.pinch_scale = new_absolute_scale;
+
+ let input = PlatformInput::Pinch(PinchEvent {
+ position: state.mouse_location.unwrap_or(point(px(0.0), px(0.0))),
+ delta: zoom_delta,
+ modifiers: state.modifiers,
+ phase: TouchPhase::Moved,
+ });
+ drop(state);
+ window.handle_input(input);
+ }
+ zwp_pointer_gesture_pinch_v1::Event::End {
+ serial: _,
+ time: _,
+ cancelled: _,
+ } => {
+ state.pinch_scale = 1.0;
+ let input = PlatformInput::Pinch(PinchEvent {
+ position: state.mouse_location.unwrap_or(point(px(0.0), px(0.0))),
+ delta: 0.0,
+ modifiers: state.modifiers,
+ phase: TouchPhase::Ended,
+ });
+ drop(state);
+ window.handle_input(input);
+ }
+ _ => {}
+ }
+ }
+}
+
impl Dispatch<wp_fractional_scale_v1::WpFractionalScaleV1, ObjectId> for WaylandClientStatePtr {
fn event(
this: &mut Self,
@@ -1,8 +1,8 @@
use gpui::{
Capslock, KeyDownEvent, KeyUpEvent, Keystroke, Modifiers, ModifiersChangedEvent, MouseButton,
MouseDownEvent, MouseExitEvent, MouseMoveEvent, MousePressureEvent, MouseUpEvent,
- NavigationDirection, Pixels, PlatformInput, PressureStage, ScrollDelta, ScrollWheelEvent,
- TouchPhase, point, px,
+ NavigationDirection, PinchEvent, Pixels, PlatformInput, PressureStage, ScrollDelta,
+ ScrollWheelEvent, TouchPhase, point, px,
};
use crate::{
@@ -234,6 +234,27 @@ pub(crate) unsafe fn platform_input_from_native(
_ => None,
}
}
+ 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,
+ };
+
+ let magnification = native_event.magnification() as f32;
+
+ PlatformInput::Pinch(PinchEvent {
+ position: point(
+ px(native_event.locationInWindow().x as f32),
+ window_height - px(native_event.locationInWindow().y as f32),
+ ),
+ delta: magnification,
+ modifiers: read_modifiers(native_event),
+ phase,
+ })
+ }),
NSEventType::NSScrollWheel => window_height.map(|window_height| {
let phase = match native_event.phase() {
NSEventPhase::NSEventPhaseMayBegin | NSEventPhase::NSEventPhaseBegan => {
@@ -172,6 +172,10 @@ unsafe fn build_classes() {
sel!(mouseExited:),
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!(mouseDragged:),
handle_view_event as extern "C" fn(&Object, Sel, id),
@@ -6,6 +6,8 @@ use std::path::Path;
use anyhow::Context as _;
use editor::{EditorSettings, items::entry_git_aware_label_color};
use file_icons::FileIcons;
+#[cfg(any(target_os = "linux", target_os = "macos"))]
+use gpui::PinchEvent;
use gpui::{
AnyElement, App, Bounds, Context, DispatchPhase, Element, ElementId, Entity, EventEmitter,
FocusHandle, Focusable, GlobalElementId, InspectorElementId, InteractiveElement, IntoElement,
@@ -260,6 +262,12 @@ impl ImageView {
cx.notify();
}
}
+
+ #[cfg(any(target_os = "linux", target_os = "macos"))]
+ fn handle_pinch(&mut self, event: &PinchEvent, _window: &mut Window, cx: &mut Context<Self>) {
+ let zoom_factor = 1.0 + event.delta;
+ self.set_zoom(self.zoom_level * zoom_factor, Some(event.position), cx);
+ }
}
struct ImageContentElement {
@@ -679,8 +687,9 @@ impl Render for ImageView {
.size_full()
.relative()
.bg(cx.theme().colors().editor_background)
- .child(
- div()
+ .child({
+ #[cfg(any(target_os = "linux", target_os = "macos"))]
+ let container = div()
.id("image-container")
.size_full()
.overflow_hidden()
@@ -690,13 +699,34 @@ impl Render for ImageView {
gpui::CursorStyle::OpenHand
})
.on_scroll_wheel(cx.listener(Self::handle_scroll_wheel))
+ .on_pinch(cx.listener(Self::handle_pinch))
.on_mouse_down(MouseButton::Left, cx.listener(Self::handle_mouse_down))
.on_mouse_down(MouseButton::Middle, cx.listener(Self::handle_mouse_down))
.on_mouse_up(MouseButton::Left, cx.listener(Self::handle_mouse_up))
.on_mouse_up(MouseButton::Middle, cx.listener(Self::handle_mouse_up))
.on_mouse_move(cx.listener(Self::handle_mouse_move))
- .child(ImageContentElement::new(cx.entity())),
- )
+ .child(ImageContentElement::new(cx.entity()));
+
+ #[cfg(not(any(target_os = "linux", target_os = "macos")))]
+ let container = div()
+ .id("image-container")
+ .size_full()
+ .overflow_hidden()
+ .cursor(if self.is_dragging() {
+ gpui::CursorStyle::ClosedHand
+ } else {
+ gpui::CursorStyle::OpenHand
+ })
+ .on_scroll_wheel(cx.listener(Self::handle_scroll_wheel))
+ .on_mouse_down(MouseButton::Left, cx.listener(Self::handle_mouse_down))
+ .on_mouse_down(MouseButton::Middle, cx.listener(Self::handle_mouse_down))
+ .on_mouse_up(MouseButton::Left, cx.listener(Self::handle_mouse_up))
+ .on_mouse_up(MouseButton::Middle, cx.listener(Self::handle_mouse_up))
+ .on_mouse_move(cx.listener(Self::handle_mouse_move))
+ .child(ImageContentElement::new(cx.entity()));
+
+ container
+ })
}
}