Detailed changes
@@ -107,10 +107,11 @@ use gpui::{
AvailableSpace, Background, Bounds, ClickEvent, ClipboardEntry, ClipboardItem, Context,
DispatchPhase, Edges, Entity, EntityInputHandler, EventEmitter, FocusHandle, FocusOutEvent,
Focusable, FontId, FontWeight, Global, HighlightStyle, Hsla, KeyContext, Modifiers,
- MouseButton, MouseDownEvent, MouseMoveEvent, PaintQuad, ParentElement, Pixels, Render,
- ScrollHandle, SharedString, Size, Stateful, Styled, Subscription, Task, TextRun, TextStyle,
- TextStyleRefinement, UTF16Selection, UnderlineStyle, UniformListScrollHandle, WeakEntity,
- WeakFocusHandle, Window, div, point, prelude::*, pulsating_between, px, relative, size,
+ MouseButton, MouseDownEvent, MouseMoveEvent, PaintQuad, ParentElement, Pixels, PressureStage,
+ Render, ScrollHandle, SharedString, Size, Stateful, Styled, Subscription, Task, TextRun,
+ TextStyle, TextStyleRefinement, UTF16Selection, UnderlineStyle, UniformListScrollHandle,
+ WeakEntity, WeakFocusHandle, Window, div, point, prelude::*, pulsating_between, px, relative,
+ size,
};
use hover_links::{HoverLink, HoveredLinkState, find_file};
use hover_popover::{HoverState, hide_hover};
@@ -1121,6 +1122,7 @@ pub struct Editor {
remote_id: Option<ViewId>,
pub hover_state: HoverState,
pending_mouse_down: Option<Rc<RefCell<Option<MouseDownEvent>>>>,
+ prev_pressure_stage: Option<PressureStage>,
gutter_hovered: bool,
hovered_link_state: Option<HoveredLinkState>,
edit_prediction_provider: Option<RegisteredEditPredictionDelegate>,
@@ -2300,6 +2302,7 @@ impl Editor {
remote_id: None,
hover_state: HoverState::default(),
pending_mouse_down: None,
+ prev_pressure_stage: None,
hovered_link_state: None,
edit_prediction_provider: None,
active_edit_prediction: None,
@@ -48,11 +48,11 @@ use gpui::{
DispatchPhase, Edges, Element, ElementInputHandler, Entity, Focusable as _, FontId,
GlobalElementId, Hitbox, HitboxBehavior, Hsla, InteractiveElement, IntoElement, IsZero,
KeybindingKeystroke, Length, Modifiers, ModifiersChangedEvent, MouseButton, MouseClickEvent,
- MouseDownEvent, MouseMoveEvent, MouseUpEvent, PaintQuad, ParentElement, Pixels, ScrollDelta,
- ScrollHandle, ScrollWheelEvent, ShapedLine, SharedString, Size, StatefulInteractiveElement,
- Style, Styled, TextRun, TextStyleRefinement, WeakEntity, Window, anchored, deferred, div, fill,
- linear_color_stop, linear_gradient, outline, point, px, quad, relative, size, solid_background,
- transparent_black,
+ MouseDownEvent, MouseMoveEvent, MousePressureEvent, MouseUpEvent, PaintQuad, ParentElement,
+ Pixels, PressureStage, ScrollDelta, ScrollHandle, ScrollWheelEvent, ShapedLine, SharedString,
+ Size, StatefulInteractiveElement, Style, Styled, TextRun, TextStyleRefinement, WeakEntity,
+ Window, anchored, deferred, div, fill, linear_color_stop, linear_gradient, outline, point, px,
+ quad, relative, size, solid_background, transparent_black,
};
use itertools::Itertools;
use language::{IndentGuideSettings, language_settings::ShowWhitespaceSetting};
@@ -1035,6 +1035,28 @@ impl EditorElement {
}
}
+ fn pressure_click(
+ editor: &mut Editor,
+ event: &MousePressureEvent,
+ position_map: &PositionMap,
+ window: &mut Window,
+ cx: &mut Context<Editor>,
+ ) {
+ let text_hitbox = &position_map.text_hitbox;
+ let force_click_possible =
+ matches!(editor.prev_pressure_stage, Some(PressureStage::Normal))
+ && event.stage == PressureStage::Force;
+
+ editor.prev_pressure_stage = Some(event.stage);
+
+ if force_click_possible && text_hitbox.is_hovered(window) {
+ let point = position_map.point_for_position(event.position);
+ editor.handle_click_hovered_link(point, event.modifiers, window, cx);
+ editor.selection_drag_state = SelectionDragState::None;
+ cx.stop_propagation();
+ }
+ }
+
fn mouse_dragged(
editor: &mut Editor,
event: &MouseMoveEvent,
@@ -7769,6 +7791,19 @@ impl EditorElement {
}
});
+ window.on_mouse_event({
+ let position_map = layout.position_map.clone();
+ let editor = self.editor.clone();
+
+ move |event: &MousePressureEvent, phase, window, cx| {
+ if phase == DispatchPhase::Bubble {
+ editor.update(cx, |editor, cx| {
+ Self::pressure_click(editor, &event, &position_map, window, cx);
+ })
+ }
+ }
+ });
+
window.on_mouse_event({
let position_map = layout.position_map.clone();
let editor = self.editor.clone();
@@ -735,7 +735,7 @@ mod tests {
test::editor_lsp_test_context::EditorLspTestContext,
};
use futures::StreamExt;
- use gpui::Modifiers;
+ use gpui::{Modifiers, MousePressureEvent, PressureStage};
use indoc::indoc;
use lsp::request::{GotoDefinition, GotoTypeDefinition};
use multi_buffer::MultiBufferOffset;
@@ -1706,4 +1706,77 @@ mod tests {
cx.simulate_click(screen_coord, Modifiers::secondary_key());
cx.update_workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 1));
}
+
+ #[gpui::test]
+ async fn test_pressure_links(cx: &mut gpui::TestAppContext) {
+ init_test(cx, |_| {});
+
+ let mut cx = EditorLspTestContext::new_rust(
+ lsp::ServerCapabilities {
+ hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
+ definition_provider: Some(lsp::OneOf::Left(true)),
+ ..Default::default()
+ },
+ cx,
+ )
+ .await;
+
+ cx.set_state(indoc! {"
+ fn ˇtest() { do_work(); }
+ fn do_work() { test(); }
+ "});
+
+ // Position the mouse over a symbol that has a definition
+ let hover_point = cx.pixel_position(indoc! {"
+ fn test() { do_wˇork(); }
+ fn do_work() { test(); }
+ "});
+ let symbol_range = cx.lsp_range(indoc! {"
+ fn test() { «do_work»(); }
+ fn do_work() { test(); }
+ "});
+ let target_range = cx.lsp_range(indoc! {"
+ fn test() { do_work(); }
+ fn «do_work»() { test(); }
+ "});
+
+ let mut requests =
+ cx.set_request_handler::<GotoDefinition, _, _>(move |url, _, _| async move {
+ Ok(Some(lsp::GotoDefinitionResponse::Link(vec![
+ lsp::LocationLink {
+ origin_selection_range: Some(symbol_range),
+ target_uri: url.clone(),
+ target_range,
+ target_selection_range: target_range,
+ },
+ ])))
+ });
+
+ cx.simulate_mouse_move(hover_point, None, Modifiers::none());
+
+ // First simulate Normal pressure to set up the previous stage
+ cx.simulate_event(MousePressureEvent {
+ pressure: 0.5,
+ stage: PressureStage::Normal,
+ position: hover_point,
+ modifiers: Modifiers::none(),
+ });
+ cx.background_executor.run_until_parked();
+
+ // Now simulate Force pressure to trigger the force click and go-to definition
+ cx.simulate_event(MousePressureEvent {
+ pressure: 1.0,
+ stage: PressureStage::Force,
+ position: hover_point,
+ modifiers: Modifiers::none(),
+ });
+ requests.next().await;
+ cx.background_executor.run_until_parked();
+
+ // Assert that we navigated to the definition
+ cx.assert_editor_state(indoc! {"
+ fn test() { do_work(); }
+ fn «do_workˇ»() { test(); }
+ "});
+ }
}
@@ -330,3 +330,7 @@ path = "examples/window_shadow.rs"
[[example]]
name = "grid_layout"
path = "examples/grid_layout.rs"
+
+[[example]]
+name = "mouse_pressure"
+path = "examples/mouse_pressure.rs"
@@ -0,0 +1,66 @@
+use gpui::{
+ App, Application, Bounds, Context, MousePressureEvent, PressureStage, Window, WindowBounds,
+ WindowOptions, div, prelude::*, px, rgb, size,
+};
+
+struct MousePressureExample {
+ pressure_stage: PressureStage,
+ pressure_amount: f32,
+}
+
+impl Render for MousePressureExample {
+ fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
+ div()
+ .flex()
+ .flex_col()
+ .gap_3()
+ .bg(rgb(0x505050))
+ .size(px(500.0))
+ .justify_center()
+ .items_center()
+ .shadow_lg()
+ .border_1()
+ .border_color(rgb(0x0000ff))
+ .text_xl()
+ .text_color(rgb(0xffffff))
+ .child(format!("Pressure stage: {:?}", &self.pressure_stage))
+ .child(format!("Pressure amount: {:.2}", &self.pressure_amount))
+ .on_mouse_pressure(cx.listener(Self::on_mouse_pressure))
+ }
+}
+
+impl MousePressureExample {
+ fn on_mouse_pressure(
+ &mut self,
+ pressure_event: &MousePressureEvent,
+ _window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ self.pressure_amount = pressure_event.pressure;
+ self.pressure_stage = pressure_event.stage;
+
+ cx.notify();
+ }
+}
+
+fn main() {
+ Application::new().run(|cx: &mut App| {
+ let bounds = Bounds::centered(None, size(px(500.), px(500.0)), cx);
+
+ cx.open_window(
+ WindowOptions {
+ window_bounds: Some(WindowBounds::Windowed(bounds)),
+ ..Default::default()
+ },
+ |_, cx| {
+ cx.new(|_| MousePressureExample {
+ pressure_stage: PressureStage::Zero,
+ pressure_amount: 0.0,
+ })
+ },
+ )
+ .unwrap();
+
+ cx.activate(true);
+ });
+}
@@ -20,8 +20,8 @@ use crate::{
DispatchPhase, Display, Element, ElementId, Entity, FocusHandle, Global, GlobalElementId,
Hitbox, HitboxBehavior, HitboxId, InspectorElementId, IntoElement, IsZero, KeyContext,
KeyDownEvent, KeyUpEvent, KeyboardButton, KeyboardClickEvent, LayoutId, ModifiersChangedEvent,
- MouseButton, MouseClickEvent, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Overflow,
- ParentElement, Pixels, Point, Render, ScrollWheelEvent, SharedString, Size, Style,
+ MouseButton, MouseClickEvent, MouseDownEvent, MouseMoveEvent, MousePressureEvent, MouseUpEvent,
+ Overflow, ParentElement, Pixels, Point, Render, ScrollWheelEvent, SharedString, Size, Style,
StyleRefinement, Styled, Task, TooltipId, Visibility, Window, WindowControlArea, point, px,
size,
};
@@ -166,6 +166,38 @@ impl Interactivity {
}));
}
+ /// Bind the given callback to the mouse pressure event, during the bubble phase
+ /// the imperative API equivalent to [`InteractiveElement::on_mouse_pressure`].
+ ///
+ /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback.
+ pub fn on_mouse_pressure(
+ &mut self,
+ listener: impl Fn(&MousePressureEvent, &mut Window, &mut App) + 'static,
+ ) {
+ self.mouse_pressure_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 the mouse pressure event, during the capture phase
+ /// the imperative API equivalent to [`InteractiveElement::on_mouse_pressure`].
+ ///
+ /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback.
+ pub fn capture_mouse_pressure(
+ &mut self,
+ listener: impl Fn(&MousePressureEvent, &mut Window, &mut App) + 'static,
+ ) {
+ self.mouse_pressure_listeners
+ .push(Box::new(move |event, phase, hitbox, window, cx| {
+ if phase == DispatchPhase::Capture && hitbox.is_hovered(window) {
+ (listener)(event, window, cx)
+ }
+ }));
+ }
+
/// Bind the given callback to the mouse up event for the given button, during the bubble phase.
/// The imperative API equivalent to [`InteractiveElement::on_mouse_up`].
///
@@ -769,6 +801,30 @@ pub trait InteractiveElement: Sized {
self
}
+ /// Bind the given callback to the mouse pressure event, during the bubble phase
+ /// the fluent API equivalent to [`Interactivity::on_mouse_pressure`]
+ ///
+ /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback.
+ fn on_mouse_pressure(
+ mut self,
+ listener: impl Fn(&MousePressureEvent, &mut Window, &mut App) + 'static,
+ ) -> Self {
+ self.interactivity().on_mouse_pressure(listener);
+ self
+ }
+
+ /// Bind the given callback to the mouse pressure event, during the capture phase
+ /// the fluent API equivalent to [`Interactivity::on_mouse_pressure`]
+ ///
+ /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback.
+ fn capture_mouse_pressure(
+ mut self,
+ listener: impl Fn(&MousePressureEvent, &mut Window, &mut App) + 'static,
+ ) -> Self {
+ self.interactivity().capture_mouse_pressure(listener);
+ self
+ }
+
/// Bind the given callback to the mouse down event, on any button, during the capture phase,
/// when the mouse is outside of the bounds of this element.
/// The fluent API equivalent to [`Interactivity::on_mouse_down_out`].
@@ -1197,7 +1253,8 @@ pub(crate) type MouseDownListener =
Box<dyn Fn(&MouseDownEvent, DispatchPhase, &Hitbox, &mut Window, &mut App) + 'static>;
pub(crate) type MouseUpListener =
Box<dyn Fn(&MouseUpEvent, DispatchPhase, &Hitbox, &mut Window, &mut App) + 'static>;
-
+pub(crate) type MousePressureListener =
+ Box<dyn Fn(&MousePressureEvent, DispatchPhase, &Hitbox, &mut Window, &mut App) + 'static>;
pub(crate) type MouseMoveListener =
Box<dyn Fn(&MouseMoveEvent, DispatchPhase, &Hitbox, &mut Window, &mut App) + 'static>;
@@ -1521,6 +1578,7 @@ pub struct Interactivity {
pub(crate) group_drag_over_styles: Vec<(TypeId, GroupStyle)>,
pub(crate) mouse_down_listeners: Vec<MouseDownListener>,
pub(crate) mouse_up_listeners: Vec<MouseUpListener>,
+ pub(crate) mouse_pressure_listeners: Vec<MousePressureListener>,
pub(crate) mouse_move_listeners: Vec<MouseMoveListener>,
pub(crate) scroll_wheel_listeners: Vec<ScrollWheelListener>,
pub(crate) key_down_listeners: Vec<KeyDownListener>,
@@ -1714,6 +1772,7 @@ impl Interactivity {
|| self.group_hover_style.is_some()
|| self.hover_listener.is_some()
|| !self.mouse_up_listeners.is_empty()
+ || !self.mouse_pressure_listeners.is_empty()
|| !self.mouse_down_listeners.is_empty()
|| !self.mouse_move_listeners.is_empty()
|| !self.click_listeners.is_empty()
@@ -2064,6 +2123,13 @@ impl Interactivity {
})
}
+ for listener in self.mouse_pressure_listeners.drain(..) {
+ let hitbox = hitbox.clone();
+ window.on_mouse_event(move |event: &MousePressureEvent, phase, window, cx| {
+ listener(event, phase, &hitbox, window, cx);
+ })
+ }
+
for listener in self.mouse_move_listeners.drain(..) {
let hitbox = hitbox.clone();
window.on_mouse_event(move |event: &MouseMoveEvent, phase, window, cx| {
@@ -174,6 +174,40 @@ pub struct MouseClickEvent {
pub up: MouseUpEvent,
}
+/// The stage of a pressure click event.
+#[derive(Clone, Copy, Debug, Default, PartialEq)]
+pub enum PressureStage {
+ /// No pressure.
+ #[default]
+ Zero,
+ /// Normal click pressure.
+ Normal,
+ /// High pressure, enough to trigger a force click.
+ Force,
+}
+
+/// A mouse pressure event from the platform. Generated when a force-sensitive trackpad is pressed hard.
+/// Currently only implemented for macOS trackpads.
+#[derive(Debug, Clone, Default)]
+pub struct MousePressureEvent {
+ /// Pressure of the current stage as a float between 0 and 1
+ pub pressure: f32,
+ /// The pressure stage of the event.
+ pub stage: PressureStage,
+ /// The position of the mouse on the window.
+ pub position: Point<Pixels>,
+ /// The modifiers that were held down when the mouse pressure changed.
+ pub modifiers: Modifiers,
+}
+
+impl Sealed for MousePressureEvent {}
+impl InputEvent for MousePressureEvent {
+ fn to_platform_input(self) -> PlatformInput {
+ PlatformInput::MousePressure(self)
+ }
+}
+impl MouseEvent for MousePressureEvent {}
+
/// A click event that was generated by a keyboard button being pressed and released.
#[derive(Clone, Debug, Default)]
pub struct KeyboardClickEvent {
@@ -571,6 +605,8 @@ pub enum PlatformInput {
MouseDown(MouseDownEvent),
/// The mouse was released.
MouseUp(MouseUpEvent),
+ /// Mouse pressure.
+ MousePressure(MousePressureEvent),
/// The mouse was moved.
MouseMove(MouseMoveEvent),
/// The mouse exited the window.
@@ -590,6 +626,7 @@ impl PlatformInput {
PlatformInput::MouseDown(event) => Some(event),
PlatformInput::MouseUp(event) => Some(event),
PlatformInput::MouseMove(event) => Some(event),
+ PlatformInput::MousePressure(event) => Some(event),
PlatformInput::MouseExited(event) => Some(event),
PlatformInput::ScrollWheel(event) => Some(event),
PlatformInput::FileDrop(event) => Some(event),
@@ -604,6 +641,7 @@ impl PlatformInput {
PlatformInput::MouseDown(_) => None,
PlatformInput::MouseUp(_) => None,
PlatformInput::MouseMove(_) => None,
+ PlatformInput::MousePressure(_) => None,
PlatformInput::MouseExited(_) => None,
PlatformInput::ScrollWheel(_) => None,
PlatformInput::FileDrop(_) => None,
@@ -1,7 +1,8 @@
use crate::{
Capslock, KeyDownEvent, KeyUpEvent, Keystroke, Modifiers, ModifiersChangedEvent, MouseButton,
- MouseDownEvent, MouseExitEvent, MouseMoveEvent, MouseUpEvent, NavigationDirection, Pixels,
- PlatformInput, ScrollDelta, ScrollWheelEvent, TouchPhase,
+ MouseDownEvent, MouseExitEvent, MouseMoveEvent, MousePressureEvent, MouseUpEvent,
+ NavigationDirection, Pixels, PlatformInput, PressureStage, ScrollDelta, ScrollWheelEvent,
+ TouchPhase,
platform::mac::{
LMGetKbdType, NSStringExt, TISCopyCurrentKeyboardLayoutInputSource,
TISGetInputSourceProperty, UCKeyTranslate, kTISPropertyUnicodeKeyLayoutData,
@@ -187,6 +188,26 @@ impl PlatformInput {
})
})
}
+ NSEventType::NSEventTypePressure => {
+ let stage = native_event.stage();
+ let pressure = native_event.pressure();
+
+ window_height.map(|window_height| {
+ Self::MousePressure(MousePressureEvent {
+ stage: match stage {
+ 1 => PressureStage::Normal,
+ 2 => PressureStage::Force,
+ _ => PressureStage::Zero,
+ },
+ pressure,
+ modifiers: read_modifiers(native_event),
+ position: point(
+ px(native_event.locationInWindow().x as f32),
+ window_height - px(native_event.locationInWindow().y as f32),
+ ),
+ })
+ })
+ }
// Some mice (like Logitech MX Master) send navigation buttons as swipe events
NSEventType::NSEventTypeSwipe => {
let navigation_direction = match native_event.phase() {
@@ -153,6 +153,10 @@ unsafe fn build_classes() {
sel!(mouseMoved:),
handle_view_event as extern "C" fn(&Object, Sel, id),
);
+ decl.add_method(
+ sel!(pressureChangeWithEvent:),
+ handle_view_event as extern "C" fn(&Object, Sel, id),
+ );
decl.add_method(
sel!(mouseExited:),
handle_view_event as extern "C" fn(&Object, Sel, id),
@@ -3705,6 +3705,9 @@ impl Window {
self.modifiers = mouse_up.modifiers;
PlatformInput::MouseUp(mouse_up)
}
+ PlatformInput::MousePressure(mouse_pressure) => {
+ PlatformInput::MousePressure(mouse_pressure)
+ }
PlatformInput::MouseExited(mouse_exited) => {
self.modifiers = mouse_exited.modifiers;
PlatformInput::MouseExited(mouse_exited)