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 89506a4702..2f4b264639 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 70c541c736..6f5432ccd2 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 dbf0bb5cd0..575e183b10 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 f2ebbea8c2..89f830b646 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 e47907d22e..8fd461d1d1 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 695e9798f2..a313bff2f1 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 f178e4b643..cf73832b11 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 5c5ab5a392..075edac55c 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 4a4521b35d..4952843148 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 a678c77896..a672db2d2e 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 2b24403846..20a4bae3a6 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 5423dfcbc7..e538b16c19 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 57a2d50ca6..6240c023b7 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 d85762680a..797b4b5d95 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 b3afcdb63d..6acc2db09f 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 b6f8806c05..545b9ff291 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 b863425b94..cf8f04f6fe 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.