From a8610fbd13a5999149a426ed169f2c322f8e54bd Mon Sep 17 00:00:00 2001 From: Thomas Mickley-Doyle Date: Thu, 20 Feb 2025 12:54:01 -0600 Subject: [PATCH] Hide the mouse when the user is typing in the editor (#25040) Closes https://github.com/zed-industries/zed/issues/4461 This PR improves the coding experience by hiding the mouse while the user is typing so it does not accidentally get in their way, making it challenging to ready characters in the editor. Release Notes: - The following PR hides the cursor when the user is typing by adding a new cursor style called `None`. - Assuming the user does not move the mouse, it will stay hidden until it is moved again. https://github.com/user-attachments/assets/6ba9f2ee-b9f3-4595-81e4-e9d986da4a39 --------- Co-authored-by: Agus Co-authored-by: Peter Tripp Co-authored-by: Kirill Bulatov --- assets/settings/default.json | 2 ++ crates/editor/src/editor.rs | 13 +++++++++ crates/editor/src/editor_settings.rs | 5 ++++ crates/editor/src/element.rs | 9 ++++-- crates/gpui/src/platform.rs | 3 ++ crates/gpui/src/platform/linux/platform.rs | 6 ++++ crates/gpui/src/platform/linux/wayland.rs | 6 ++++ .../gpui/src/platform/linux/wayland/client.rs | 8 ++++- crates/gpui/src/platform/linux/x11/client.rs | 29 +++++++++++++++---- crates/gpui/src/platform/mac/platform.rs | 1 + crates/gpui/src/platform/windows/events.rs | 18 ++++++++++-- crates/gpui/src/platform/windows/platform.rs | 8 ++--- crates/gpui/src/platform/windows/util.rs | 23 ++++++++------- crates/gpui/src/platform/windows/window.rs | 6 ++-- crates/gpui/src/window.rs | 1 + crates/gpui_macros/src/styles.rs | 7 +++++ docs/src/configuring-zed.md | 10 +++++++ 17 files changed, 127 insertions(+), 28 deletions(-) diff --git a/assets/settings/default.json b/assets/settings/default.json index 89506a4702b7d5e348629121e2e89c0e01e7d6ac..2f4b2646399f3fc77deac91f187e193f1683a8b7 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -150,6 +150,8 @@ // // Default: not set, defaults to "bar" "cursor_shape": null, + // Determines whether the mouse cursor is hidden when typing in an editor or input box. + "hide_mouse_while_typing": true, // How to highlight the current line in the editor. // // 1. Don't highlight the current line: diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 70c541c736cfd9727b8534bb3ece11a20c379f5c..6f5432ccd2c3ea2cb94df31951c51d8986430445 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -716,6 +716,8 @@ pub struct Editor { toggle_fold_multiple_buffers: Task<()>, _scroll_cursor_center_top_bottom_task: Task<()>, serialize_selections: Task<()>, + mouse_cursor_hidden: bool, + hide_mouse_while_typing: bool, } #[derive(Copy, Clone, Debug, PartialEq, Eq, Default)] @@ -1432,6 +1434,10 @@ impl Editor { serialize_selections: Task::ready(()), text_style_refinement: None, load_diff_task: load_uncommitted_diff, + mouse_cursor_hidden: false, + hide_mouse_while_typing: EditorSettings::get_global(cx) + .hide_mouse_while_typing + .unwrap_or(true), }; this.tasks_update_task = Some(this.refresh_runnables(window, cx)); this._subscriptions.extend(project_subscriptions); @@ -2764,6 +2770,8 @@ impl Editor { return; } + self.mouse_cursor_hidden = self.hide_mouse_while_typing; + let selections = self.selections.all_adjusted(cx); let mut bracket_inserted = false; let mut edits = Vec::new(); @@ -14355,6 +14363,11 @@ impl Editor { self.scroll_manager.vertical_scroll_margin = editor_settings.vertical_scroll_margin; self.show_breadcrumbs = editor_settings.toolbar.breadcrumbs; self.cursor_shape = editor_settings.cursor_shape.unwrap_or_default(); + self.hide_mouse_while_typing = editor_settings.hide_mouse_while_typing.unwrap_or(true); + + if !self.hide_mouse_while_typing { + self.mouse_cursor_hidden = false; + } } if old_cursor_shape != self.cursor_shape { diff --git a/crates/editor/src/editor_settings.rs b/crates/editor/src/editor_settings.rs index dbf0bb5cd0d3a8726b8d937d51c14e5503218760..575e183b105aaed905a994ae5ec8713c395bb801 100644 --- a/crates/editor/src/editor_settings.rs +++ b/crates/editor/src/editor_settings.rs @@ -37,6 +37,7 @@ pub struct EditorSettings { pub auto_signature_help: bool, pub show_signature_help_after_edits: bool, pub jupyter: Jupyter, + pub hide_mouse_while_typing: Option, } #[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] @@ -270,6 +271,10 @@ pub struct EditorSettingsContent { /// /// Default: None pub cursor_shape: Option, + /// Determines whether the mouse cursor should be hidden while typing in an editor or input box. + /// + /// Default: true + pub hide_mouse_while_typing: Option, /// How to highlight the current line in the editor. /// /// Default: all diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index f2ebbea8c21f77b5d797550d98fdecbb38a0c6ca..89f830b646341b25ed827d7900bb213fe109c37b 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -848,6 +848,7 @@ impl EditorElement { let modifiers = event.modifiers; let gutter_hovered = gutter_hitbox.is_hovered(window); editor.set_gutter_hovered(gutter_hovered, cx); + editor.mouse_cursor_hidden = false; // Don't trigger hover popover if mouse is hovering over context menu if text_hitbox.is_hovered(window) { @@ -4813,9 +4814,10 @@ impl EditorElement { bounds: layout.position_map.text_hitbox.bounds, }), |window| { - let cursor_style = if self - .editor - .read(cx) + let editor = self.editor.read(cx); + let cursor_style = if editor.mouse_cursor_hidden { + CursorStyle::None + } else if editor .hovered_link_state .as_ref() .is_some_and(|hovered_link_state| !hovered_link_state.links.is_empty()) @@ -6815,6 +6817,7 @@ impl Element for EditorElement { }, false, ); + // Offset the content_bounds from the text_bounds by the gutter margin (which // is roughly half a character wide) to make hit testing work more like how we want. let content_origin = diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs index e47907d22ebf172ba17231fb98ec35bd280692df..8fd461d1d155ecb6018aad336df01915043f042b 100644 --- a/crates/gpui/src/platform.rs +++ b/crates/gpui/src/platform.rs @@ -1227,6 +1227,9 @@ pub enum CursorStyle { /// A cursor indicating that the operation will result in a context menu /// corresponds to the CSS cursor value `context-menu` ContextualMenu, + + /// Hide the cursor + None, } impl Default for CursorStyle { diff --git a/crates/gpui/src/platform/linux/platform.rs b/crates/gpui/src/platform/linux/platform.rs index 695e9798f278ef5bde8901dc3c9873a1a2d98c4d..a313bff2f154de7f63c6b91ae238343ea7671c9c 100644 --- a/crates/gpui/src/platform/linux/platform.rs +++ b/crates/gpui/src/platform/linux/platform.rs @@ -666,6 +666,12 @@ impl CursorStyle { CursorStyle::DragLink => "alias", CursorStyle::DragCopy => "copy", CursorStyle::ContextualMenu => "context-menu", + CursorStyle::None => { + #[cfg(debug_assertions)] + panic!("CursorStyle::None should be handled separately in the client"); + #[cfg(not(debug_assertions))] + "default" + } } .to_string() } diff --git a/crates/gpui/src/platform/linux/wayland.rs b/crates/gpui/src/platform/linux/wayland.rs index f178e4b643f18d5fbbeaa4f8d88b604f9673b124..cf73832b11fb1baad08bf5ee3142e461876fbe92 100644 --- a/crates/gpui/src/platform/linux/wayland.rs +++ b/crates/gpui/src/platform/linux/wayland.rs @@ -35,6 +35,12 @@ impl CursorStyle { CursorStyle::DragLink => Shape::Alias, CursorStyle::DragCopy => Shape::Copy, CursorStyle::ContextualMenu => Shape::ContextMenu, + CursorStyle::None => { + #[cfg(debug_assertions)] + panic!("CursorStyle::None should be handled separately in the client"); + #[cfg(not(debug_assertions))] + Shape::Default + } } } } diff --git a/crates/gpui/src/platform/linux/wayland/client.rs b/crates/gpui/src/platform/linux/wayland/client.rs index 5c5ab5a3929ab5a6396abdc781a8ce1f44f90b1f..075edac55c1ca66bed1463767b50846db6385bf6 100644 --- a/crates/gpui/src/platform/linux/wayland/client.rs +++ b/crates/gpui/src/platform/linux/wayland/client.rs @@ -667,7 +667,13 @@ impl LinuxClient for WaylandClient { let serial = state.serial_tracker.get(SerialKind::MouseEnter); state.cursor_style = Some(style); - if let Some(cursor_shape_device) = &state.cursor_shape_device { + if let CursorStyle::None = style { + let wl_pointer = state + .wl_pointer + .clone() + .expect("window is focused by pointer"); + wl_pointer.set_cursor(serial, None, 0, 0); + } else if let Some(cursor_shape_device) = &state.cursor_shape_device { cursor_shape_device.set_shape(serial, style.to_shape()); } else if let Some(focused_window) = &state.mouse_focused_window { // cursor-shape-v1 isn't supported, set the cursor using a surface. diff --git a/crates/gpui/src/platform/linux/x11/client.rs b/crates/gpui/src/platform/linux/x11/client.rs index 4a4521b35d05cac960442c394ae01df831572660..49528431486be227dffb5e31008d945320b78087 100644 --- a/crates/gpui/src/platform/linux/x11/client.rs +++ b/crates/gpui/src/platform/linux/x11/client.rs @@ -1438,13 +1438,16 @@ impl LinuxClient for X11Client { let cursor = match state.cursor_cache.get(&style) { Some(cursor) => *cursor, None => { - let Some(cursor) = state - .cursor_handle - .load_cursor(&state.xcb_connection, &style.to_icon_name()) - .log_err() - else { + let Some(cursor) = (match style { + CursorStyle::None => create_invisible_cursor(&state.xcb_connection).log_err(), + _ => state + .cursor_handle + .load_cursor(&state.xcb_connection, &style.to_icon_name()) + .log_err(), + }) else { return; }; + state.cursor_cache.insert(style, cursor); cursor } @@ -1938,3 +1941,19 @@ fn make_scroll_wheel_event( touch_phase: TouchPhase::default(), } } + +fn create_invisible_cursor( + connection: &XCBConnection, +) -> anyhow::Result { + let empty_pixmap = connection.generate_id()?; + let root = connection.setup().roots[0].root; + connection.create_pixmap(1, empty_pixmap, root, 1, 1)?; + + let cursor = connection.generate_id()?; + connection.create_cursor(cursor, empty_pixmap, empty_pixmap, 0, 0, 0, 0, 0, 0, 0, 0)?; + + connection.free_pixmap(empty_pixmap)?; + + connection.flush()?; + Ok(cursor) +} diff --git a/crates/gpui/src/platform/mac/platform.rs b/crates/gpui/src/platform/mac/platform.rs index a678c778961817cc41e13e8e7e8c42da15a3f4bd..a672db2d2e828bd1d9b562d12298ce39d46a0d03 100644 --- a/crates/gpui/src/platform/mac/platform.rs +++ b/crates/gpui/src/platform/mac/platform.rs @@ -938,6 +938,7 @@ impl Platform for MacPlatform { CursorStyle::DragLink => msg_send![class!(NSCursor), dragLinkCursor], CursorStyle::DragCopy => msg_send![class!(NSCursor), dragCopyCursor], CursorStyle::ContextualMenu => msg_send![class!(NSCursor), contextualMenuCursor], + CursorStyle::None => msg_send![class!(NSCursor), setHiddenUntilMouseMoves:YES], }; let old_cursor: id = msg_send![class!(NSCursor), currentCursor]; diff --git a/crates/gpui/src/platform/windows/events.rs b/crates/gpui/src/platform/windows/events.rs index 2b24403846eb4ab37fac9161db54c5ab9ce2852a..20a4bae3a6aa460b151ce631f64e6fc97acb19f3 100644 --- a/crates/gpui/src/platform/windows/events.rs +++ b/crates/gpui/src/platform/windows/events.rs @@ -1121,7 +1121,19 @@ fn handle_nc_mouse_up_msg( } fn handle_cursor_changed(lparam: LPARAM, state_ptr: Rc) -> Option { - state_ptr.state.borrow_mut().current_cursor = HCURSOR(lparam.0 as _); + let mut state = state_ptr.state.borrow_mut(); + let had_cursor = state.current_cursor.is_some(); + + state.current_cursor = if lparam.0 == 0 { + None + } else { + Some(HCURSOR(lparam.0 as _)) + }; + + if had_cursor != state.current_cursor.is_some() { + unsafe { SetCursor(state.current_cursor.as_ref()) }; + } + Some(0) } @@ -1132,7 +1144,9 @@ fn handle_set_cursor(lparam: LPARAM, state_ptr: Rc) -> Op ) { return None; } - unsafe { SetCursor(state_ptr.state.borrow().current_cursor) }; + unsafe { + SetCursor(state_ptr.state.borrow().current_cursor.as_ref()); + }; Some(1) } diff --git a/crates/gpui/src/platform/windows/platform.rs b/crates/gpui/src/platform/windows/platform.rs index 5423dfcbc775e037722441bfe9e4f0062ced77cb..e538b16c1977ef8db2232610bac4432f60f6aaa4 100644 --- a/crates/gpui/src/platform/windows/platform.rs +++ b/crates/gpui/src/platform/windows/platform.rs @@ -50,7 +50,7 @@ pub(crate) struct WindowsPlatformState { callbacks: PlatformCallbacks, menus: Vec, // NOTE: standard cursor handles don't need to close. - pub(crate) current_cursor: HCURSOR, + pub(crate) current_cursor: Option, } #[derive(Default)] @@ -506,11 +506,11 @@ impl Platform for WindowsPlatform { fn set_cursor_style(&self, style: CursorStyle) { let hcursor = load_cursor(style); let mut lock = self.state.borrow_mut(); - if lock.current_cursor.0 != hcursor.0 { + if lock.current_cursor.map(|c| c.0) != hcursor.map(|c| c.0) { self.post_message( WM_GPUI_CURSOR_STYLE_CHANGED, WPARAM(0), - LPARAM(hcursor.0 as isize), + LPARAM(hcursor.map_or(0, |c| c.0 as isize)), ); lock.current_cursor = hcursor; } @@ -613,7 +613,7 @@ impl Drop for WindowsPlatform { pub(crate) struct WindowCreationInfo { pub(crate) icon: HICON, pub(crate) executor: ForegroundExecutor, - pub(crate) current_cursor: HCURSOR, + pub(crate) current_cursor: Option, pub(crate) windows_version: WindowsVersion, pub(crate) validation_number: usize, pub(crate) main_receiver: flume::Receiver, diff --git a/crates/gpui/src/platform/windows/util.rs b/crates/gpui/src/platform/windows/util.rs index 57a2d50ca67cca3b6d45b730de8641a1bd54e679..6240c023b787630dacab5895362238339aaf5a13 100644 --- a/crates/gpui/src/platform/windows/util.rs +++ b/crates/gpui/src/platform/windows/util.rs @@ -105,7 +105,7 @@ pub(crate) fn windows_credentials_target_name(url: &str) -> String { format!("zed:url={}", url) } -pub(crate) fn load_cursor(style: CursorStyle) -> HCURSOR { +pub(crate) fn load_cursor(style: CursorStyle) -> Option { static ARROW: OnceLock = OnceLock::new(); static IBEAM: OnceLock = OnceLock::new(); static CROSS: OnceLock = OnceLock::new(); @@ -126,17 +126,20 @@ pub(crate) fn load_cursor(style: CursorStyle) -> HCURSOR { | CursorStyle::ResizeUpDown | CursorStyle::ResizeRow => (&SIZENS, IDC_SIZENS), CursorStyle::OperationNotAllowed => (&NO, IDC_NO), + CursorStyle::None => return None, _ => (&ARROW, IDC_ARROW), }; - *(*lock.get_or_init(|| { - HCURSOR( - unsafe { LoadImageW(None, name, IMAGE_CURSOR, 0, 0, LR_DEFAULTSIZE | LR_SHARED) } - .log_err() - .unwrap_or_default() - .0, - ) - .into() - })) + Some( + *(*lock.get_or_init(|| { + HCURSOR( + unsafe { LoadImageW(None, name, IMAGE_CURSOR, 0, 0, LR_DEFAULTSIZE | LR_SHARED) } + .log_err() + .unwrap_or_default() + .0, + ) + .into() + })), + ) } /// This function is used to configure the dark mode for the window built-in title bar. diff --git a/crates/gpui/src/platform/windows/window.rs b/crates/gpui/src/platform/windows/window.rs index d85762680adc0d83428e982dbf0cf3d53f2ada00..797b4b5d9566b4623485af0c0e27f6a26ffdf383 100644 --- a/crates/gpui/src/platform/windows/window.rs +++ b/crates/gpui/src/platform/windows/window.rs @@ -49,7 +49,7 @@ pub struct WindowsWindowState { pub click_state: ClickState, pub system_settings: WindowsSystemSettings, - pub current_cursor: HCURSOR, + pub current_cursor: Option, pub nc_button_pressed: Option, pub display: WindowsDisplay, @@ -77,7 +77,7 @@ impl WindowsWindowState { hwnd: HWND, transparent: bool, cs: &CREATESTRUCTW, - current_cursor: HCURSOR, + current_cursor: Option, display: WindowsDisplay, gpu_context: &BladeContext, ) -> Result { @@ -352,7 +352,7 @@ struct WindowCreateContext<'a> { transparent: bool, is_movable: bool, executor: ForegroundExecutor, - current_cursor: HCURSOR, + current_cursor: Option, windows_version: WindowsVersion, validation_number: usize, main_receiver: flume::Receiver, diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index b3afcdb63d30822901f88d5b4dff2e21f9f721cc..6acc2db09f06cd9735c3cb43b3c29de8e31f0d96 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -3233,6 +3233,7 @@ impl Window { keystroke, &dispatch_path, ); + if !match_result.to_replay.is_empty() { self.replay_pending_input(match_result.to_replay, cx) } diff --git a/crates/gpui_macros/src/styles.rs b/crates/gpui_macros/src/styles.rs index b6f8806c059317467fecd4511b28a8860e548b48..545b9ff2919fff517fb2d9a21271cee430e30d88 100644 --- a/crates/gpui_macros/src/styles.rs +++ b/crates/gpui_macros/src/styles.rs @@ -326,6 +326,13 @@ pub fn cursor_style_methods(input: TokenStream) -> TokenStream { self.style().mouse_cursor = Some(gpui::CursorStyle::ResizeLeft); self } + + /// Sets cursor style when hovering over an element to `none`. + /// [Docs](https://tailwindcss.com/docs/cursor) + #visibility fn cursor_none(mut self, cursor: CursorStyle) -> Self { + self.style().mouse_cursor = Some(gpui::CursorStyle::None); + self + } }; output.into() diff --git a/docs/src/configuring-zed.md b/docs/src/configuring-zed.md index b863425b942b4bb6adb43b0ee5a8f77fa75c38d9..cf8f04f6febe5a85aff0757562e3e4fe5a1f04f1 100644 --- a/docs/src/configuring-zed.md +++ b/docs/src/configuring-zed.md @@ -533,6 +533,16 @@ List of `string` values "cursor_shape": "hollow" ``` +## Hide Mouse While Typing + +- Description: Determines whether the mouse cursor should be hidden while typing in an editor or input box. +- Setting: `hide_mouse_while_typing` +- Default: `true` + +**Options** + +`boolean` values + ## Editor Scrollbar - Description: Whether or not to show the editor scrollbar and various elements in it.