From ba6d258b0f1ae5b5bf29503a8699eaaa2a31106f Mon Sep 17 00:00:00 2001 From: Agus Zubiaga Date: Mon, 4 May 2026 19:51:56 -0300 Subject: [PATCH] Handle hiding cursor on keyboard input at GPUI level (#55664) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Instead of manually handing hiding the cursor on keyboard input at the editor level, GPUI will now take care of it. This makes it significantly easier to handle the edge cases, and allows delegating the cursor restoration to the platform itself in the macOS case. On Linux and Windows, we still have to restore the cursor on movement ourselves, but this now happens at the platform-specific level. Bugs fixed by this change: - No cursor when "Unsaved edits" prompt appears - Cursor disappears when clicking a panel button if it contains a search bar (e.g. collab panel) ### Setting rename The `hide_mouse` setting value `"on_typing_and_movement"` has been renamed to `"on_typing_and_action"` to better reflect what it actually does — it hides the cursor when a keystroke resolves to an action (e.g. cursor movement, deletion). Existing settings are migrated automatically. ### Tested platforms - [x] macOS - [x] Wayland - [x] X11 - [x] Windows - [x] Web Self-Review Checklist: - [x] I've reviewed my own diff for quality, security, and reliability - [x] Unsafe blocks (if any) have justifying comments - [x] The content is consistent with the [UI/UX checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist) - [ ] Tests cover the new/changed behavior - [x] Performance impact has been considered and is acceptable Release Notes: - Renamed the `hide_mouse` setting value `on_typing_and_movement` to `on_typing_and_action` to better describe its behavior (existing settings are auto-migrated) - Fixed a few situations where the mouse cursor would be incorrectly hidden --- assets/settings/default.json | 9 +- crates/editor/src/editor.rs | 191 +----------------- crates/editor/src/editor_settings.rs | 7 +- crates/editor/src/element.rs | 5 +- crates/editor/src/hover_links.rs | 32 +-- crates/gpui/src/app.rs | 39 ++++ crates/gpui/src/platform.rs | 11 +- crates/gpui/src/platform/test/platform.rs | 6 + crates/gpui/src/platform/visual_test.rs | 8 + crates/gpui/src/window.rs | 59 ++++-- crates/gpui_linux/src/linux/platform.rs | 18 +- crates/gpui_linux/src/linux/wayland.rs | 6 - crates/gpui_linux/src/linux/wayland/client.rs | 134 +++++++++--- crates/gpui_linux/src/linux/x11/client.rs | 149 +++++++++++--- crates/gpui_macos/src/platform.rs | 33 ++- crates/gpui_macos/src/window.rs | 71 ++----- crates/gpui_macros/src/styles.rs | 6 - crates/gpui_web/src/platform.rs | 107 +++++++++- crates/gpui_windows/src/events.rs | 27 ++- crates/gpui_windows/src/platform.rs | 30 +++ crates/gpui_windows/src/util.rs | 1 - crates/gpui_windows/src/window.rs | 8 + crates/migrator/src/migrations.rs | 6 + .../src/migrations/m_2026_05_04/settings.rs | 38 ++++ crates/migrator/src/migrator.rs | 44 ++++ crates/settings/src/vscode_import.rs | 2 +- crates/settings_content/src/editor.rs | 32 --- .../settings_content/src/settings_content.rs | 36 ++++ crates/settings_ui/src/page_data.rs | 4 +- crates/vim/src/helix.rs | 5 +- crates/vim/src/vim.rs | 8 +- crates/zed/src/zed.rs | 21 ++ docs/src/reference/all-settings.md | 9 +- docs/src/visual-customization.md | 4 +- 34 files changed, 716 insertions(+), 450 deletions(-) create mode 100644 crates/migrator/src/migrations/m_2026_05_04/settings.rs diff --git a/assets/settings/default.json b/assets/settings/default.json index 54b9070da2..cd2e164dfb 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -230,15 +230,16 @@ // // Default: "bar" "cursor_shape": "bar", - // Determines when the mouse cursor should be hidden in an editor or input box. + // Determines when the mouse cursor should be hidden in response to keyboard + // input. // // 1. Never hide the mouse cursor: // "never" // 2. Hide only when typing: // "on_typing" - // 3. Hide on both typing and cursor movement: - // "on_typing_and_movement" - "hide_mouse": "on_typing_and_movement", + // 3. Hide on typing and on key bindings that resolve to an action: + // "on_typing_and_action" + "hide_mouse": "on_typing_and_action", // Determines whether the focused panel follows the mouse location. "focus_follows_mouse": { "enabled": false, diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index a05876d2d9..ea38200cfe 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -65,8 +65,8 @@ pub use display_map::{ pub use edit_prediction_types::Direction; pub use editor_settings::{ CompletionDetailAlignment, CurrentLineHighlight, DiffViewStyle, DocumentColorsRenderMode, - EditorSettings, EditorSettingsScrollbarProxy, HideMouseMode, ScrollBeyondLastLine, - ScrollbarAxes, SearchSettings, ShowMinimap, ui_scrollbar_settings_from_raw, + EditorSettings, EditorSettingsScrollbarProxy, ScrollBeyondLastLine, ScrollbarAxes, + SearchSettings, ShowMinimap, ui_scrollbar_settings_from_raw, }; pub use element::{ CursorLayout, EditorElement, HighlightedRange, HighlightedRangeLine, PointForPosition, @@ -334,11 +334,6 @@ enum DisplayDiffHunk { }, } -pub enum HideMouseCursorOrigin { - TypingAction, - MovementAction, -} - pub fn init(cx: &mut App) { cx.set_global(GlobalBlameRenderer(Arc::new(()))); cx.set_global(breadcrumbs::RenderBreadcrumbText(render_breadcrumb_text)); @@ -1354,9 +1349,7 @@ pub struct Editor { _scroll_cursor_center_top_bottom_task: Task<()>, serialize_selections: Task<()>, serialize_folds: Task<()>, - mouse_cursor_hidden: bool, minimap: Option>, - hide_mouse_mode: HideMouseMode, pub change_list: ChangeList, inline_value_cache: InlineValueCache, number_deleted_lines: bool, @@ -2624,9 +2617,6 @@ impl Editor { blink_manager.disable(cx); } }); - if active { - editor.show_mouse_cursor(cx); - } }), ] }) @@ -2658,11 +2648,7 @@ impl Editor { text_style_refinement: None, load_diff_task: load_uncommitted_diff, temporary_diff_override: false, - mouse_cursor_hidden: false, minimap: None, - hide_mouse_mode: EditorSettings::get_global(cx) - .hide_mouse - .unwrap_or_default(), change_list: ChangeList::new(), mode, selection_drag_state: SelectionDragState::None, @@ -3004,31 +2990,6 @@ impl Editor { self.last_bounds.as_ref() } - fn show_mouse_cursor(&mut self, cx: &mut Context) { - if self.mouse_cursor_hidden { - self.mouse_cursor_hidden = false; - cx.notify(); - } - } - - pub fn hide_mouse_cursor(&mut self, origin: HideMouseCursorOrigin, cx: &mut Context) { - let hide_mouse_cursor = match origin { - HideMouseCursorOrigin::TypingAction => { - matches!( - self.hide_mouse_mode, - HideMouseMode::OnTyping | HideMouseMode::OnTypingAndMovement - ) - } - HideMouseCursorOrigin::MovementAction => { - matches!(self.hide_mouse_mode, HideMouseMode::OnTypingAndMovement) - } - }; - if self.mouse_cursor_hidden != hide_mouse_cursor { - self.mouse_cursor_hidden = hide_mouse_cursor; - cx.notify(); - } - } - fn accept_edit_prediction_keystroke( &self, granularity: EditPredictionGranularity, @@ -4830,8 +4791,6 @@ impl Editor { return; } - self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx); - self.unfold_buffers_with_selections(cx); let selections = self.selections.all_adjusted(&self.display_snapshot(cx)); @@ -5333,7 +5292,6 @@ impl Editor { return; } - self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx); self.transact(window, cx, |this, window, cx| { let (edits_with_flags, selection_info): (Vec<_>, Vec<_>) = { let selections = this @@ -5572,8 +5530,6 @@ impl Editor { return; } - self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx); - let buffer = self.buffer.read(cx); let snapshot = buffer.snapshot(cx); @@ -5643,8 +5599,6 @@ impl Editor { return; } - self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx); - let mut buffer_edits: HashMap, Vec)> = HashMap::default(); let mut rows = Vec::new(); let mut rows_inserted = 0; @@ -6699,7 +6653,6 @@ impl Editor { if self.read_only(cx) { return None; } - self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx); self.do_completion(action.item_ix, CompletionIntent::Complete, window, cx) } @@ -6712,7 +6665,6 @@ impl Editor { if self.read_only(cx) { return None; } - self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx); self.do_completion(None, CompletionIntent::CompleteWithInsert, window, cx) } @@ -6725,7 +6677,6 @@ impl Editor { if self.read_only(cx) { return None; } - self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx); self.do_completion(None, CompletionIntent::CompleteWithReplace, window, cx) } @@ -6735,7 +6686,6 @@ impl Editor { window: &mut Window, cx: &mut Context, ) -> Option>> { - self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx); self.do_completion(action.item_ix, CompletionIntent::Compose, window, cx) } @@ -7229,7 +7179,6 @@ impl Editor { if self.read_only(cx) { return None; } - self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx); let actions_menu = if let CodeContextMenu::CodeActions(menu) = self.hide_context_menu(window, cx)? { @@ -11293,7 +11242,6 @@ impl Editor { if self.read_only(cx) { return; } - self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx); self.transact(window, cx, |this, window, cx| { this.select_autoclose_pair(window, cx); @@ -11346,7 +11294,6 @@ impl Editor { if self.read_only(cx) { return; } - self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx); self.transact(window, cx, |this, window, cx| { this.change_selections(Default::default(), window, cx, |s| { s.move_with(&mut |map, selection| { @@ -11372,7 +11319,6 @@ impl Editor { return; } - self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx); if self.move_to_prev_snippet_tabstop(window, cx) { return; } @@ -11391,7 +11337,6 @@ impl Editor { } if self.move_to_next_snippet_tabstop(window, cx) { - self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx); return; } cx.propagate(); @@ -11409,7 +11354,6 @@ impl Editor { } if self.move_to_prev_snippet_tabstop(window, cx) { - self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx); return; } cx.propagate(); @@ -11422,13 +11366,11 @@ impl Editor { } if self.move_to_next_snippet_tabstop(window, cx) { - self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx); return; } if self.read_only(cx) { return; } - self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx); let mut selections = self.selections.all_adjusted(&self.display_snapshot(cx)); let buffer = self.buffer.read(cx); let snapshot = buffer.snapshot(cx); @@ -11560,7 +11502,6 @@ impl Editor { return; } - self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx); let mut selections = self.selections.all::(&self.display_snapshot(cx)); let mut prev_edited_row = 0; let mut row_delta = 0; @@ -11668,7 +11609,6 @@ impl Editor { return; } - self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx); let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); let selections = self.selections.all::(&display_map); let mut deletion_ranges = Vec::new(); @@ -11747,7 +11687,6 @@ impl Editor { return; } - self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx); let selections = self .selections .all::(&self.display_snapshot(cx)) @@ -11769,7 +11708,6 @@ impl Editor { if self.read_only(cx) { return; } - self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx); let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); let selections = self.selections.all::(&display_map); @@ -11983,7 +11921,6 @@ impl Editor { } pub fn join_lines(&mut self, _: &JoinLines, window: &mut Window, cx: &mut Context) { - self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx); self.join_lines_impl(true, window, cx); } @@ -12065,7 +12002,6 @@ impl Editor { if self.read_only(cx) { return; } - self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx); let snapshot = self.buffer.read(cx).snapshot(cx); @@ -12168,7 +12104,6 @@ impl Editor { if self.read_only(cx) { return; } - self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx); let mut buffer_ids = HashSet::default(); let snapshot = self.buffer().read(cx).snapshot(cx); for selection in self @@ -12190,7 +12125,6 @@ impl Editor { if self.read_only(cx) { return; } - self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx); let selections = self .selections .all(&self.display_snapshot(cx)) @@ -12219,7 +12153,6 @@ impl Editor { .map(|selection| selection.range()) .collect(); - self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx); self.restore_hunks_in_ranges(selections, window, cx); let all_diff_hunks_expanded = self.buffer().read(cx).all_diff_hunks_expanded(); @@ -12622,7 +12555,6 @@ impl Editor { if self.read_only(cx) { return; } - self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx); let display_snapshot = self.display_snapshot(cx); @@ -12863,7 +12795,6 @@ impl Editor { if self.read_only(cx) { return; } - self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx); let display_snapshot = self.display_snapshot(cx); let selections = self.selections.all::(&display_snapshot); @@ -13017,7 +12948,6 @@ impl Editor { if self.read_only(cx) { return; } - self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx); let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); let buffer = self.buffer.read(cx).snapshot(cx); @@ -13439,7 +13369,6 @@ impl Editor { if self.read_only(cx) { return; } - self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx); let buffer = self.buffer.read(cx).snapshot(cx); let mut new_selections = Vec::new(); @@ -13539,7 +13468,6 @@ impl Editor { if self.read_only(cx) { return; } - self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx); let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); let buffer = display_map.buffer_snapshot(); @@ -13687,7 +13615,6 @@ impl Editor { if self.read_only(cx) { return; } - self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx); if self.mode.is_single_line() { cx.propagate(); return; @@ -13801,7 +13728,6 @@ impl Editor { if self.read_only(cx) { return; } - self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx); if self.mode.is_single_line() { cx.propagate(); return; @@ -13898,7 +13824,6 @@ impl Editor { if self.read_only(cx) { return; } - self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx); let text_layout_details = &self.text_layout_details(window, cx); self.transact(window, cx, |this, window, cx| { let edits = this.change_selections(Default::default(), window, cx, |s| { @@ -13963,7 +13888,6 @@ impl Editor { if self.read_only(cx) { return; } - self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx); if self.mode.is_single_line() { cx.propagate(); return; @@ -14426,7 +14350,6 @@ impl Editor { if self.read_only(cx) { return; } - self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx); let item = self.cut_common(true, window, cx); cx.write_to_clipboard(item); } @@ -14435,7 +14358,6 @@ impl Editor { if self.read_only(cx) { return; } - self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx); self.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { s.move_with(&mut |snapshot, sel| { if sel.is_empty() { @@ -14456,7 +14378,6 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { - self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx); let (text, metadata) = if let Some(KillRing(item)) = cx.try_global() { if let Some(ClipboardEntry::String(kill_ring)) = item.entries().first() { (kill_ring.text().to_string(), kill_ring.metadata_json()) @@ -14804,7 +14725,6 @@ impl Editor { if self.read_only(cx) { return; } - self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx); if let Some(item) = cx.read_from_clipboard() { let clipboard_string = item.entries().iter().find_map(|entry| match entry { ClipboardEntry::String(s) => Some(s), @@ -14828,8 +14748,6 @@ impl Editor { return; } - self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx); - if let Some(transaction_id) = self.buffer.update(cx, |buffer, cx| buffer.undo(cx)) { if let Some((selections, _)) = self.selection_history.transaction(transaction_id).cloned() @@ -14858,8 +14776,6 @@ impl Editor { return; } - self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx); - if let Some(transaction_id) = self.buffer.update(cx, |buffer, cx| buffer.redo(cx)) { if let Some((_, Some(selections))) = self.selection_history.transaction(transaction_id).cloned() @@ -14893,7 +14809,6 @@ impl Editor { } pub fn move_left(&mut self, _: &MoveLeft, window: &mut Window, cx: &mut Context) { - self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx); self.change_selections(Default::default(), window, cx, |s| { s.move_with(&mut |map, selection| { let cursor = if selection.is_empty() { @@ -14907,14 +14822,12 @@ impl Editor { } pub fn select_left(&mut self, _: &SelectLeft, window: &mut Window, cx: &mut Context) { - self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx); self.change_selections(Default::default(), window, cx, |s| { s.move_heads_with(&mut |map, head, _| (movement::left(map, head), SelectionGoal::None)); }) } pub fn move_right(&mut self, _: &MoveRight, window: &mut Window, cx: &mut Context) { - self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx); self.change_selections(Default::default(), window, cx, |s| { s.move_with(&mut |map, selection| { let cursor = if selection.is_empty() { @@ -14928,7 +14841,6 @@ impl Editor { } pub fn select_right(&mut self, _: &SelectRight, window: &mut Window, cx: &mut Context) { - self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx); self.change_selections(Default::default(), window, cx, |s| { s.move_heads_with(&mut |map, head, _| { (movement::right(map, head), SelectionGoal::None) @@ -14946,8 +14858,6 @@ impl Editor { return; } - self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx); - let text_layout_details = &self.text_layout_details(window, cx); let selection_count = self.selections.count(); let first_selection = self.selections.first_anchor(); @@ -14989,8 +14899,6 @@ impl Editor { return; } - self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx); - let text_layout_details = &self.text_layout_details(window, cx); self.change_selections(Default::default(), window, cx, |s| { @@ -15026,8 +14934,6 @@ impl Editor { return; } - self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx); - let text_layout_details = &self.text_layout_details(window, cx); self.change_selections(Default::default(), window, cx, |s| { @@ -15054,7 +14960,6 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { - self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx); let text_layout_details = &self.text_layout_details(window, cx); self.change_selections(Default::default(), window, cx, |s| { s.move_heads_with(&mut |map, head, goal| { @@ -15069,7 +14974,6 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { - self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx); let text_layout_details = &self.text_layout_details(window, cx); self.change_selections(Default::default(), window, cx, |s| { s.move_heads_with(&mut |map, head, goal| { @@ -15088,8 +14992,6 @@ impl Editor { return; }; - self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx); - let text_layout_details = &self.text_layout_details(window, cx); self.change_selections(Default::default(), window, cx, |s| { @@ -15128,8 +15030,6 @@ impl Editor { return; }; - self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx); - let effects = if action.center_cursor { SelectionEffects::scroll(Autoscroll::center()) } else { @@ -15157,7 +15057,6 @@ impl Editor { } pub fn select_up(&mut self, _: &SelectUp, window: &mut Window, cx: &mut Context) { - self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx); let text_layout_details = &self.text_layout_details(window, cx); self.change_selections(Default::default(), window, cx, |s| { s.move_heads_with(&mut |map, head, goal| { @@ -15174,8 +15073,6 @@ impl Editor { return; } - self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx); - let text_layout_details = &self.text_layout_details(window, cx); let selection_count = self.selections.count(); let first_selection = self.selections.first_anchor(); @@ -15212,8 +15109,6 @@ impl Editor { return; }; - self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx); - let text_layout_details = &self.text_layout_details(window, cx); self.change_selections(Default::default(), window, cx, |s| { @@ -15252,8 +15147,6 @@ impl Editor { return; }; - self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx); - let effects = if action.center_cursor { SelectionEffects::scroll(Autoscroll::center()) } else { @@ -15280,7 +15173,6 @@ impl Editor { } pub fn select_down(&mut self, _: &SelectDown, window: &mut Window, cx: &mut Context) { - self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx); let text_layout_details = &self.text_layout_details(window, cx); self.change_selections(Default::default(), window, cx, |s| { s.move_heads_with(&mut |map, head, goal| { @@ -15371,7 +15263,6 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { - self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx); self.change_selections(Default::default(), window, cx, |s| { s.move_cursors_with(&mut |map, head, _| { ( @@ -15388,7 +15279,6 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { - self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx); self.change_selections(Default::default(), window, cx, |s| { s.move_cursors_with(&mut |map, head, _| { ( @@ -15405,7 +15295,6 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { - self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx); self.change_selections(Default::default(), window, cx, |s| { s.move_heads_with(&mut |map, head, _| { ( @@ -15422,7 +15311,6 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { - self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx); self.change_selections(Default::default(), window, cx, |s| { s.move_heads_with(&mut |map, head, _| { ( @@ -15442,7 +15330,6 @@ impl Editor { if self.read_only(cx) { return; } - self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx); self.transact(window, cx, |this, window, cx| { this.select_autoclose_pair(window, cx); this.change_selections(Default::default(), window, cx, |s| { @@ -15476,7 +15363,6 @@ impl Editor { if self.read_only(cx) { return; } - self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx); self.transact(window, cx, |this, window, cx| { this.select_autoclose_pair(window, cx); this.change_selections(Default::default(), window, cx, |s| { @@ -15507,7 +15393,6 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { - self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx); self.change_selections(Default::default(), window, cx, |s| { s.move_cursors_with(&mut |map, head, _| { (movement::next_word_end(map, head), SelectionGoal::None) @@ -15521,7 +15406,6 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { - self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx); self.change_selections(Default::default(), window, cx, |s| { s.move_cursors_with(&mut |map, head, _| { (movement::next_subword_end(map, head), SelectionGoal::None) @@ -15535,7 +15419,6 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { - self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx); self.change_selections(Default::default(), window, cx, |s| { s.move_heads_with(&mut |map, head, _| { (movement::next_word_end(map, head), SelectionGoal::None) @@ -15549,7 +15432,6 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { - self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx); self.change_selections(Default::default(), window, cx, |s| { s.move_heads_with(&mut |map, head, _| { (movement::next_subword_end(map, head), SelectionGoal::None) @@ -15566,7 +15448,6 @@ impl Editor { if self.read_only(cx) { return; } - self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx); self.transact(window, cx, |this, window, cx| { this.change_selections(Default::default(), window, cx, |s| { s.move_with(&mut |map, selection| { @@ -15599,7 +15480,6 @@ impl Editor { if self.read_only(cx) { return; } - self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx); self.transact(window, cx, |this, window, cx| { this.change_selections(Default::default(), window, cx, |s| { s.move_with(&mut |map, selection| { @@ -15630,7 +15510,6 @@ impl Editor { cx: &mut Context, ) { let stop_at_indent = action.stop_at_indent && !self.mode.is_single_line(); - self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx); self.change_selections(Default::default(), window, cx, |s| { s.move_cursors_with(&mut |map, head, _| { ( @@ -15653,7 +15532,6 @@ impl Editor { cx: &mut Context, ) { let stop_at_indent = action.stop_at_indent && !self.mode.is_single_line(); - self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx); self.change_selections(Default::default(), window, cx, |s| { s.move_heads_with(&mut |map, head, _| { ( @@ -15678,7 +15556,6 @@ impl Editor { if self.read_only(cx) { return; } - self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx); self.transact(window, cx, |this, window, cx| { this.change_selections(Default::default(), window, cx, |s| { s.move_with(&mut |_, selection| { @@ -15704,7 +15581,6 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { - self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx); self.change_selections(Default::default(), window, cx, |s| { s.move_cursors_with(&mut |map, head, _| { ( @@ -15721,7 +15597,6 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { - self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx); self.change_selections(Default::default(), window, cx, |s| { s.move_heads_with(&mut |map, head, _| { ( @@ -15741,7 +15616,6 @@ impl Editor { if self.read_only(cx) { return; } - self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx); self.transact(window, cx, |this, window, cx| { this.select_to_end_of_line( &SelectToEndOfLine { @@ -15763,7 +15637,6 @@ impl Editor { if self.read_only(cx) { return; } - self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx); self.transact(window, cx, |this, window, cx| { this.select_to_end_of_line( &SelectToEndOfLine { @@ -15781,7 +15654,6 @@ impl Editor { }); }); } - this.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx); let item = this.cut_common(false, window, cx); cx.write_to_clipboard(item); }); @@ -15797,7 +15669,6 @@ impl Editor { cx.propagate(); return; } - self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx); self.change_selections(Default::default(), window, cx, |s| { s.move_with(&mut |map, selection| { selection.collapse_to( @@ -15818,7 +15689,6 @@ impl Editor { cx.propagate(); return; } - self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx); self.change_selections(Default::default(), window, cx, |s| { s.move_with(&mut |map, selection| { selection.collapse_to( @@ -15839,7 +15709,6 @@ impl Editor { cx.propagate(); return; } - self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx); self.change_selections(Default::default(), window, cx, |s| { s.move_heads_with(&mut |map, head, _| { ( @@ -15860,7 +15729,6 @@ impl Editor { cx.propagate(); return; } - self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx); self.change_selections(Default::default(), window, cx, |s| { s.move_heads_with(&mut |map, head, _| { ( @@ -15881,7 +15749,6 @@ impl Editor { cx.propagate(); return; } - self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx); self.change_selections(Default::default(), window, cx, |s| { s.move_with(&mut |map, selection| { selection.collapse_to( @@ -15931,7 +15798,6 @@ impl Editor { cx.propagate(); return; } - self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx); self.change_selections(Default::default(), window, cx, |s| { s.move_with(&mut |map, selection| { selection.collapse_to( @@ -15956,7 +15822,6 @@ impl Editor { cx.propagate(); return; } - self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx); self.change_selections(Default::default(), window, cx, |s| { s.move_with(&mut |map, selection| { selection.collapse_to( @@ -15981,7 +15846,6 @@ impl Editor { cx.propagate(); return; } - self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx); self.change_selections(Default::default(), window, cx, |s| { s.move_heads_with(&mut |map, head, _| { ( @@ -16002,7 +15866,6 @@ impl Editor { cx.propagate(); return; } - self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx); self.change_selections(Default::default(), window, cx, |s| { s.move_heads_with(&mut |map, head, _| { ( @@ -16023,7 +15886,6 @@ impl Editor { cx.propagate(); return; } - self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx); self.change_selections(Default::default(), window, cx, |s| { s.move_heads_with(&mut |map, head, _| { ( @@ -16044,7 +15906,6 @@ impl Editor { cx.propagate(); return; } - self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx); self.change_selections(Default::default(), window, cx, |s| { s.move_heads_with(&mut |map, head, _| { ( @@ -16065,7 +15926,6 @@ impl Editor { cx.propagate(); return; } - self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx); self.change_selections(Default::default(), window, cx, |s| { s.select_ranges(vec![Anchor::Min..Anchor::Min]); }); @@ -16079,7 +15939,6 @@ impl Editor { ) { let mut selection = self.selections.last::(&self.display_snapshot(cx)); selection.set_head(Point::zero(), SelectionGoal::None); - self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx); self.change_selections(Default::default(), window, cx, |s| { s.select(vec![selection]); }); @@ -16090,7 +15949,6 @@ impl Editor { cx.propagate(); return; } - self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx); let cursor = self.buffer.read(cx).read(cx).len(); self.change_selections(Default::default(), window, cx, |s| { s.select_ranges(vec![cursor..cursor]) @@ -16179,7 +16037,6 @@ impl Editor { } pub fn select_to_end(&mut self, _: &SelectToEnd, window: &mut Window, cx: &mut Context) { - self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx); let buffer = self.buffer.read(cx).snapshot(cx); let mut selection = self .selections @@ -16191,14 +16048,12 @@ impl Editor { } pub fn select_all(&mut self, _: &SelectAll, window: &mut Window, cx: &mut Context) { - self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx); self.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { s.select_ranges(vec![Anchor::Min..Anchor::Max]); }); } pub fn select_line(&mut self, _: &SelectLine, window: &mut Window, cx: &mut Context) { - self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx); let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); let mut selections = self.selections.all::(&display_map); let max_point = display_map.buffer_snapshot().max_point(); @@ -16297,8 +16152,6 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { - self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx); - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); let all_selections = self.selections.all::(&display_map); let text_layout_details = self.text_layout_details(window, cx); @@ -16720,8 +16573,6 @@ impl Editor { window: &mut Window, cx: &mut Context, ) -> Result<()> { - self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx); - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); self.select_next_match_internal(&display_map, false, None, window, cx)?; @@ -16787,7 +16638,6 @@ impl Editor { window: &mut Window, cx: &mut Context, ) -> Result<()> { - self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx); let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); self.select_next_match_internal( &display_map, @@ -16804,7 +16654,6 @@ impl Editor { window: &mut Window, cx: &mut Context, ) -> Result<()> { - self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx); let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); let buffer = display_map.buffer_snapshot(); let mut selections = self.selections.all::(&display_map); @@ -17011,7 +16860,6 @@ impl Editor { if self.read_only(cx) { return; } - self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx); self.transact(window, cx, |this, _window, cx| { let mut selections = this .selections @@ -17202,7 +17050,6 @@ impl Editor { if self.read_only(cx) { return; } - self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx); let text_layout_details = &self.text_layout_details(window, cx); self.transact(window, cx, |this, window, cx| { let mut selections = this @@ -17505,8 +17352,6 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { - self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx); - let buffer = self.buffer.read(cx).snapshot(cx); let old_selections = self .selections @@ -17574,8 +17419,6 @@ impl Editor { return; } - self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx); - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); let buffer = self.buffer.read(cx).snapshot(cx); @@ -17691,8 +17534,6 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { - self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx); - if let Some((mut selections, scroll_behavior, is_selection_reversed)) = self.select_syntax_node_history.pop() { @@ -17735,7 +17576,6 @@ impl Editor { if self.read_only(cx) { return; } - self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx); let buffer = self.buffer.read(cx).snapshot(cx); let selections = self @@ -17817,8 +17657,6 @@ impl Editor { return; } - self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx); - let buffer = self.buffer.read(cx).snapshot(cx); let mut selected_sibling = false; @@ -17877,8 +17715,6 @@ impl Editor { ) { let old_selections: Arc<[_]> = self.selections.all_anchors(&self.display_snapshot(cx)); - self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx); - let multibuffer_snapshot = self.buffer.read(cx).snapshot(cx); let mut selected_sibling = false; @@ -18017,8 +17853,6 @@ impl Editor { return false; } - self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx); - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); let buffer = self.buffer.read(cx).snapshot(cx); @@ -18082,8 +17916,6 @@ impl Editor { cx: &mut Context, move_to_end: bool, ) { - self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx); - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); let buffer = self.buffer.read(cx).snapshot(cx); let old_selections = self.selections.all::(&display_map); @@ -18115,7 +17947,6 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { - self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx); self.change_selections(Default::default(), window, cx, |s| { s.move_offsets_with(&mut |snapshot, selection| { let Some(enclosing_bracket_ranges) = @@ -18172,7 +18003,6 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { - self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx); if let Some(entry) = self.selection_history.undo_stack.pop_back() { self.selection_history.mode = SelectionHistoryMode::Undoing; self.with_selection_effects_deferred(window, cx, |this, window, cx| { @@ -18198,7 +18028,6 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { - self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx); if let Some(entry) = self.selection_history.redo_stack.pop_back() { self.selection_history.mode = SelectionHistoryMode::Redoing; self.with_selection_effects_deferred(window, cx, |this, window, cx| { @@ -18386,7 +18215,6 @@ impl Editor { if !self.diagnostics_enabled() { return; } - self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx); self.go_to_diagnostic_impl(Direction::Next, action.severity, window, cx) } @@ -18399,7 +18227,6 @@ impl Editor { if !self.diagnostics_enabled() { return; } - self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx); self.go_to_diagnostic_impl(Direction::Prev, action.severity, window, cx) } @@ -18497,7 +18324,6 @@ impl Editor { } pub fn go_to_next_hunk(&mut self, _: &GoToHunk, window: &mut Window, cx: &mut Context) { - self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx); let snapshot = self.snapshot(window, cx); let selection = self.selections.newest::(&self.display_snapshot(cx)); self.go_to_hunk_before_or_after_position( @@ -18566,7 +18392,6 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { - self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx); let snapshot = self.snapshot(window, cx); let selection = self.selections.newest::(&snapshot.display_snapshot); self.go_to_hunk_before_or_after_position( @@ -18660,7 +18485,6 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { - self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx); let snapshot = self.snapshot(window, cx); let buffer = &snapshot.buffer_snapshot(); let position = self @@ -20180,7 +20004,6 @@ impl Editor { if self.read_only(cx) { return None; } - self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx); let project = match &self.project { Some(project) => project.clone(), @@ -20205,7 +20028,6 @@ impl Editor { if self.read_only(cx) { return None; } - self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx); let project = match &self.project { Some(project) => project.clone(), @@ -20327,7 +20149,6 @@ impl Editor { if self.read_only(cx) { return None; } - self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx); let project = match &self.project { Some(project) => project.clone(), None => return None, @@ -21911,7 +21732,6 @@ impl Editor { if self.read_only(cx) { return; } - self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx); let buffers = self.buffer.read(cx).all_buffers(); for branch_buffer in buffers { @@ -21944,7 +21764,6 @@ impl Editor { if self.read_only(cx) { return; } - self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx); let snapshot = self.snapshot(window, cx); let hunks = snapshot.hunks_for_ranges( self.selections @@ -24284,7 +24103,6 @@ impl Editor { if self.read_only(cx) { return; } - self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx); self.transact(window, cx, |this, window, cx| { let edits = this .selections @@ -25363,7 +25181,6 @@ 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_mode = editor_settings.hide_mouse.unwrap_or_default(); } if old_cursor_shape != self.cursor_shape { @@ -26021,8 +25838,8 @@ impl Editor { } }); - if let Some(position_map) = self.last_position_map.clone() - && !self.mouse_cursor_hidden + if cx.is_cursor_visible() + && let Some(position_map) = self.last_position_map.clone() { EditorElement::mouse_moved( self, diff --git a/crates/editor/src/editor_settings.rs b/crates/editor/src/editor_settings.rs index 1999d0e537..0c39196062 100644 --- a/crates/editor/src/editor_settings.rs +++ b/crates/editor/src/editor_settings.rs @@ -6,9 +6,8 @@ use project::project_settings::DiagnosticSeverity; pub use settings::{ CodeLens, CompletionDetailAlignment, CurrentLineHighlight, DelayMs, DiffViewStyle, DisplayIn, DocumentColorsRenderMode, DoubleClickInMultibuffer, GoToDefinitionFallback, - GoToDefinitionScrollStrategy, HideMouseMode, MinimapThumb, MinimapThumbBorder, - MultiCursorModifier, ScrollBeyondLastLine, ScrollbarDiagnostics, SeedQuerySetting, ShowMinimap, - SnippetSortOrder, + GoToDefinitionScrollStrategy, MinimapThumb, MinimapThumbBorder, MultiCursorModifier, + ScrollBeyondLastLine, ScrollbarDiagnostics, SeedQuerySetting, ShowMinimap, SnippetSortOrder, }; use settings::{RegisterSetting, RelativeLineNumbers, Settings}; use ui::scrollbars::ShowScrollbar; @@ -55,7 +54,6 @@ pub struct EditorSettings { pub go_to_definition_fallback: GoToDefinitionFallback, pub go_to_definition_scroll_strategy: GoToDefinitionScrollStrategy, pub jupyter: Jupyter, - pub hide_mouse: Option, pub snippet_sort_order: SnippetSortOrder, pub diagnostics_max_severity: Option, pub inline_code_actions: bool, @@ -291,7 +289,6 @@ impl Settings for EditorSettings { jupyter: Jupyter { enabled: editor.jupyter.unwrap().enabled.unwrap(), }, - hide_mouse: editor.hide_mouse, snippet_sort_order: editor.snippet_sort_order.unwrap(), diagnostics_max_severity: editor.diagnostics_max_severity.map(Into::into), inline_code_actions: editor.inline_code_actions.unwrap(), diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 69a829e578..a8c0d443d7 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -1262,7 +1262,6 @@ impl EditorElement { let text_hovered = text_hitbox.is_hovered(window); let gutter_hovered = gutter_hitbox.is_hovered(window); editor.set_gutter_hovered(gutter_hovered, cx); - editor.show_mouse_cursor(cx); let point_for_position = position_map.point_for_position(event.position); let valid_point = point_for_position.nearest_valid; @@ -6607,9 +6606,7 @@ impl EditorElement { }), |window| { let editor = self.editor.read(cx); - if editor.mouse_cursor_hidden { - window.set_window_cursor_style(CursorStyle::None); - } else if let SelectionDragState::ReadyToDrag { + if let SelectionDragState::ReadyToDrag { mouse_down_time, .. } = &editor.selection_drag_state { diff --git a/crates/editor/src/hover_links.rs b/crates/editor/src/hover_links.rs index e00fd20ed5..1877d8704f 100644 --- a/crates/editor/src/hover_links.rs +++ b/crates/editor/src/hover_links.rs @@ -120,7 +120,12 @@ impl Editor { cx: &mut Context, ) { let hovered_link_modifier = Editor::is_cmd_or_ctrl_pressed(&modifiers, cx); - if !hovered_link_modifier || self.has_pending_selection() || self.mouse_cursor_hidden { + if !hovered_link_modifier || self.has_pending_selection() { + self.hide_hovered_link(cx); + return; + } + + if !cx.is_cursor_visible() { self.hide_hovered_link(cx); return; } @@ -782,7 +787,7 @@ fn surrounding_filename( mod tests { use super::*; use crate::{ - DisplayPoint, HideMouseCursorOrigin, + DisplayPoint, display_map::ToDisplayPoint, editor_tests::init_test, inlays::inlay_hints::tests::{cached_hint_labels, visible_hint_labels}, @@ -1413,29 +1418,6 @@ mod tests { Let's test a [complex](«https://zed.dev/channel/ˇ») case. "}, ); - - // Cursor hidden with secondary key - let screen_coord = cx.pixel_position(indoc! {" - Let's test a [complex](https://zed.dev/ˇchannel/) case. - "}); - cx.simulate_mouse_move(screen_coord, None, Modifiers::none()); - cx.update_editor(|editor, _, cx| { - editor.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx); - }); - cx.simulate_modifiers_change(Modifiers::secondary_key()); - assert_no_highlight!(cx); - - // Cursor active again - let screen_coord = cx.pixel_position(indoc! {" - Let's test a [complex](https://ˇzed.dev/channel/) case. - "}); - cx.simulate_mouse_move(screen_coord, None, Modifiers::secondary_key()); - cx.assert_editor_text_highlights( - HighlightKey::HoveredLinkState, - indoc! {" - Let's test a [complex](«https://zed.dev/channel/ˇ») case. - "}, - ); } #[gpui::test] diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index f19c780caa..a9255898da 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -257,6 +257,22 @@ pub enum QuitMode { Explicit, } +/// Controls when GPUI hides the mouse cursor in response to keyboard input. +/// +/// Restoration on mouse motion is handled by the platform layer; this enum +/// only describes the policy for *triggering* a hide. +#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] +pub enum CursorHideMode { + /// Never hide the cursor automatically. + Never, + /// Hide on character-producing key presses (typing). + OnTyping, + /// Hide on character-producing key presses, *and* when a key binding + /// resolves to an action that consumes the keystroke. + #[default] + OnTypingAndAction, +} + #[doc(hidden)] #[derive(Clone, PartialEq, Eq)] pub struct SystemWindowTab { @@ -649,6 +665,7 @@ pub struct App { pub(crate) window_update_stack: Vec, pub(crate) mode: GpuiMode, + pub(crate) cursor_hide_mode: CursorHideMode, flushing_effects: bool, pending_updates: usize, quit_mode: QuitMode, @@ -737,6 +754,7 @@ impl App { inspector_element_registry: InspectorElementRegistry::default(), quit_mode: QuitMode::default(), quitting: false, + cursor_hide_mode: CursorHideMode::default(), #[cfg(any(test, feature = "test-support", debug_assertions))] name: None, @@ -877,6 +895,27 @@ impl App { self.platform.quit(); } + /// Returns the current policy for hiding the cursor in response to + /// keyboard input. + pub fn cursor_hide_mode(&self) -> CursorHideMode { + self.cursor_hide_mode + } + + /// Sets the policy controlling when GPUI hides the cursor in response + /// to keyboard input. + pub fn set_cursor_hide_mode(&mut self, mode: CursorHideMode) { + self.cursor_hide_mode = mode; + } + + /// Returns whether the cursor is currently visible according to the + /// platform. This will report `false` after a keyboard input has hidden + /// the cursor and the user has not yet moved the mouse to restore it. + /// + /// See [`App::set_cursor_hide_mode`]. + pub fn is_cursor_visible(&self) -> bool { + self.platform.is_cursor_visible() + } + /// Schedules all windows in the application to be redrawn. This can be called /// multiple times in an update cycle and still result in a single redraw. pub fn refresh_windows(&mut self) { diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs index ac36f05c42..a00d158bc5 100644 --- a/crates/gpui/src/platform.rs +++ b/crates/gpui/src/platform.rs @@ -212,6 +212,14 @@ pub trait Platform: 'static { fn path_for_auxiliary_executable(&self, name: &str) -> Result; fn set_cursor_style(&self, style: CursorStyle); + + /// Hides the mouse cursor until the user moves the mouse over one of + /// this application's windows. + fn hide_cursor_until_mouse_moves(&self); + + /// Returns whether the mouse cursor is currently visible. + fn is_cursor_visible(&self) -> bool; + fn should_auto_hide_scrollbars(&self) -> bool; fn read_from_clipboard(&self) -> Option; @@ -1818,9 +1826,6 @@ 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, } /// A clipboard item that should be copied to the clipboard diff --git a/crates/gpui/src/platform/test/platform.rs b/crates/gpui/src/platform/test/platform.rs index a59b21f038..cc8c5749bd 100644 --- a/crates/gpui/src/platform/test/platform.rs +++ b/crates/gpui/src/platform/test/platform.rs @@ -403,6 +403,12 @@ impl Platform for TestPlatform { *self.active_cursor.lock() = style; } + fn hide_cursor_until_mouse_moves(&self) {} + + fn is_cursor_visible(&self) -> bool { + true + } + fn should_auto_hide_scrollbars(&self) -> bool { false } diff --git a/crates/gpui/src/platform/visual_test.rs b/crates/gpui/src/platform/visual_test.rs index 8b9bec7edd..3719a3ee24 100644 --- a/crates/gpui/src/platform/visual_test.rs +++ b/crates/gpui/src/platform/visual_test.rs @@ -202,6 +202,14 @@ impl Platform for VisualTestPlatform { self.platform.set_cursor_style(style) } + fn hide_cursor_until_mouse_moves(&self) { + self.platform.hide_cursor_until_mouse_moves(); + } + + fn is_cursor_visible(&self) -> bool { + self.platform.is_cursor_visible() + } + fn should_auto_hide_scrollbars(&self) -> bool { self.platform.should_auto_hide_scrollbars() } diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index ad8bb0f735..dc387c67f3 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -3,21 +3,22 @@ use crate::Inspector; use crate::{ Action, AnyDrag, AnyElement, AnyImageCache, AnyTooltip, AnyView, App, AppContext, Arena, Asset, AsyncWindowContext, AvailableSpace, Background, BorderStyle, Bounds, BoxShadow, Capslock, - Context, Corners, CursorStyle, Decorations, DevicePixels, DispatchActionListener, - DispatchNodeId, DispatchTree, DisplayId, Edges, Effect, Entity, EntityId, EventEmitter, - FileDropEvent, FontId, Global, GlobalElementId, GlyphId, GpuSpecs, Hsla, InputHandler, IsZero, - KeyBinding, KeyContext, KeyDownEvent, KeyEvent, Keystroke, KeystrokeEvent, LayoutId, - LineLayoutIndex, Modifiers, ModifiersChangedEvent, MonochromeSprite, MouseButton, MouseEvent, - MouseMoveEvent, MouseUpEvent, Path, Pixels, PlatformAtlas, PlatformDisplay, PlatformInput, - PlatformInputHandler, PlatformWindow, Point, PolychromeSprite, Priority, PromptButton, - PromptLevel, Quad, Render, RenderGlyphParams, RenderImage, RenderImageParams, RenderSvgParams, - Replay, ResizeEdge, SMOOTH_SVG_SCALE_FACTOR, SUBPIXEL_VARIANTS_X, SUBPIXEL_VARIANTS_Y, - ScaledPixels, Scene, Shadow, SharedString, Size, StrikethroughStyle, Style, SubpixelSprite, - SubscriberSet, Subscription, SystemWindowTab, SystemWindowTabController, TabStopMap, - TaffyLayoutEngine, Task, TextRenderingMode, TextStyle, TextStyleRefinement, ThermalState, - TransformationMatrix, Underline, UnderlineStyle, WindowAppearance, WindowBackgroundAppearance, - WindowBounds, WindowControls, WindowDecorations, WindowOptions, WindowParams, WindowTextSystem, - point, prelude::*, px, rems, size, transparent_black, + Context, Corners, CursorHideMode, CursorStyle, Decorations, DevicePixels, + DispatchActionListener, DispatchNodeId, DispatchTree, DisplayId, Edges, Effect, Entity, + EntityId, EventEmitter, FileDropEvent, FontId, Global, GlobalElementId, GlyphId, GpuSpecs, + Hsla, InputHandler, IsZero, KeyBinding, KeyContext, KeyDownEvent, KeyEvent, Keystroke, + KeystrokeEvent, LayoutId, LineLayoutIndex, Modifiers, ModifiersChangedEvent, MonochromeSprite, + MouseButton, MouseEvent, MouseMoveEvent, MouseUpEvent, Path, Pixels, PlatformAtlas, + PlatformDisplay, PlatformInput, PlatformInputHandler, PlatformWindow, Point, PolychromeSprite, + Priority, PromptButton, PromptLevel, Quad, Render, RenderGlyphParams, RenderImage, + RenderImageParams, RenderSvgParams, Replay, ResizeEdge, SMOOTH_SVG_SCALE_FACTOR, + SUBPIXEL_VARIANTS_X, SUBPIXEL_VARIANTS_Y, ScaledPixels, Scene, Shadow, SharedString, Size, + StrikethroughStyle, Style, SubpixelSprite, SubscriberSet, Subscription, SystemWindowTab, + SystemWindowTabController, TabStopMap, TaffyLayoutEngine, Task, TextRenderingMode, TextStyle, + TextStyleRefinement, ThermalState, TransformationMatrix, Underline, UnderlineStyle, + WindowAppearance, WindowBackgroundAppearance, WindowBounds, WindowControls, WindowDecorations, + WindowOptions, WindowParams, WindowTextSystem, point, prelude::*, px, rems, size, + transparent_black, }; use anyhow::{Context as _, Result, anyhow}; use collections::{FxHashMap, FxHashSet}; @@ -1457,10 +1458,6 @@ impl Window { move |active| { handle .update(&mut cx, |_, window, cx| { - if !active { - cx.platform.set_cursor_style(CursorStyle::Arrow); - } - window.active.set(active); window.modifiers = window.platform_window.modifiers(); window.capslock = window.platform_window.capslock(); @@ -4473,6 +4470,14 @@ impl Window { } else if let Some(key_down_event) = event.downcast_ref::() { self.pending_modifier.saw_keystroke = true; keystroke = Some(key_down_event.keystroke.clone()); + if key_down_event.keystroke.key_char.is_some() + && matches!( + cx.cursor_hide_mode, + CursorHideMode::OnTyping | CursorHideMode::OnTypingAndAction + ) + { + cx.platform.hide_cursor_until_mouse_moves(); + } } let Some(keystroke) = keystroke else { @@ -4738,6 +4743,22 @@ impl Window { node_id: DispatchNodeId, action: &dyn Action, cx: &mut App, + ) { + self.dispatch_action_on_node_inner(node_id, action, cx); + + if !cx.propagate_event + && cx.cursor_hide_mode == CursorHideMode::OnTypingAndAction + && self.last_input_was_keyboard() + { + cx.platform.hide_cursor_until_mouse_moves(); + } + } + + fn dispatch_action_on_node_inner( + &mut self, + node_id: DispatchNodeId, + action: &dyn Action, + cx: &mut App, ) { let dispatch_path = self.rendered_frame.dispatch_tree.dispatch_path(node_id); diff --git a/crates/gpui_linux/src/linux/platform.rs b/crates/gpui_linux/src/linux/platform.rs index e3c947bcb9..343600cd86 100644 --- a/crates/gpui_linux/src/linux/platform.rs +++ b/crates/gpui_linux/src/linux/platform.rs @@ -80,6 +80,10 @@ pub(crate) trait LinuxClient { options: WindowParams, ) -> anyhow::Result>; fn set_cursor_style(&self, style: CursorStyle); + fn hide_cursor_until_mouse_moves(&self) {} + fn is_cursor_visible(&self) -> bool { + true + } fn open_uri(&self, uri: &str); fn reveal_path(&self, path: PathBuf); fn write_to_primary(&self, item: ClipboardItem); @@ -530,6 +534,14 @@ impl Platform for LinuxPlatform

{ self.inner.set_cursor_style(style) } + fn hide_cursor_until_mouse_moves(&self) { + self.inner.hide_cursor_until_mouse_moves() + } + + fn is_cursor_visible(&self) -> bool { + self.inner.is_cursor_visible() + } + fn should_auto_hide_scrollbars(&self) -> bool { self.inner.with_common(|common| common.auto_hide_scrollbars) } @@ -776,12 +788,6 @@ pub(super) fn cursor_style_to_icon_names(style: CursorStyle) -> &'static [&'stat 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_CURSOR_ICON_NAME] - } } } diff --git a/crates/gpui_linux/src/linux/wayland.rs b/crates/gpui_linux/src/linux/wayland.rs index aa1e797404..3e90688d1b 100644 --- a/crates/gpui_linux/src/linux/wayland.rs +++ b/crates/gpui_linux/src/linux/wayland.rs @@ -37,11 +37,5 @@ pub(super) fn to_shape(style: CursorStyle) -> Shape { 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_linux/src/linux/wayland/client.rs b/crates/gpui_linux/src/linux/wayland/client.rs index f91efa53e8..5edd9477b3 100644 --- a/crates/gpui_linux/src/linux/wayland/client.rs +++ b/crates/gpui_linux/src/linux/wayland/client.rs @@ -256,6 +256,7 @@ pub(crate) struct WaylandClientState { keyboard_focused_window: Option, loop_handle: LoopHandle<'static, WaylandClientStatePtr>, cursor_style: Option, + cursor_hidden_window: Option, clipboard: Clipboard, data_offers: Vec>, primary_data_offer: Option>, @@ -410,6 +411,65 @@ impl WaylandClientStatePtr { { state.keyboard_focused_window = Some(window); } + if let Some(window) = state.cursor_hidden_window.take() + && !window.ptr_eq(&closed_window) + { + state.cursor_hidden_window = Some(window); + } + } +} + +impl WaylandClientState { + fn hide_cursor_until_mouse_moves(&mut self) { + if self.cursor_hidden_window.is_some() { + return; + } + let Some(focused_window) = self.mouse_focused_window.clone() else { + // No surface to apply the hidden cursor to. + return; + }; + let Some(wl_pointer) = self.wl_pointer.clone() else { + // Seat lost its pointer capability; nothing to hide. + return; + }; + let serial = self.serial_tracker.get(SerialKind::MouseEnter); + wl_pointer.set_cursor(serial, None, 0, 0); + self.cursor_hidden_window = Some(focused_window); + } + + fn restore_cursor_after_hide(&mut self) { + if self.cursor_hidden_window.take().is_none() { + return; + } + let Some(style) = self.cursor_style else { + return; + }; + let serial = self.serial_tracker.get(SerialKind::MouseEnter); + if let Some(cursor_shape_device) = &self.cursor_shape_device { + cursor_shape_device.set_shape(serial, to_shape(style)); + return; + } + let Some(focused_window) = self.mouse_focused_window.clone() else { + log::warn!( + "wayland: no focused surface to restore cursor style {:?} after hide; cursor may stay invisible", + style + ); + return; + }; + let Some(wl_pointer) = self.wl_pointer.clone() else { + log::warn!( + "wayland: no wl_pointer to restore cursor style {:?} after hide; cursor may stay invisible", + style + ); + return; + }; + let scale = focused_window.primary_output_scale(); + self.cursor.set_icon( + &wl_pointer, + serial, + cursor_style_to_icon_names(style), + scale, + ); } } @@ -665,6 +725,7 @@ impl WaylandClient { loop_handle: handle.clone(), enter_token: None, cursor_style: None, + cursor_hidden_window: None, clipboard: Clipboard::new(conn.clone(), handle.clone()), data_offers: Vec::new(), primary_data_offer: None, @@ -785,33 +846,42 @@ impl LinuxClient for WaylandClient { .as_ref() .is_some_and(|w| !w.is_blocked())); - if need_update { - let serial = state.serial_tracker.get(SerialKind::MouseEnter); - state.cursor_style = Some(style); - - 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, to_shape(style)); - } else if let Some(focused_window) = &state.mouse_focused_window { - // cursor-shape-v1 isn't supported, set the cursor using a surface. - let wl_pointer = state - .wl_pointer - .clone() - .expect("window is focused by pointer"); - let scale = focused_window.primary_output_scale(); - state.cursor.set_icon( - &wl_pointer, - serial, - cursor_style_to_icon_names(style), - scale, - ); - } + if !need_update { + return; } + + state.cursor_style = Some(style); + + // Don't clobber the invisible cursor; restore reads back from `cursor_style`. + if state.cursor_hidden_window.is_some() { + return; + } + + let serial = state.serial_tracker.get(SerialKind::MouseEnter); + if let Some(cursor_shape_device) = &state.cursor_shape_device { + cursor_shape_device.set_shape(serial, to_shape(style)); + } else if let Some(focused_window) = &state.mouse_focused_window { + // cursor-shape-v1 isn't supported, set the cursor using a surface. + let wl_pointer = state + .wl_pointer + .clone() + .expect("window is focused by pointer"); + let scale = focused_window.primary_output_scale(); + state.cursor.set_icon( + &wl_pointer, + serial, + cursor_style_to_icon_names(style), + scale, + ); + } + } + + fn hide_cursor_until_mouse_moves(&self) { + self.0.borrow_mut().hide_cursor_until_mouse_moves(); + } + + fn is_cursor_visible(&self) -> bool { + self.0.borrow().cursor_hidden_window.is_none() } fn open_uri(&self, uri: &str) { @@ -1432,6 +1502,7 @@ impl Dispatch for WaylandClientStatePtr { state.enter_token.take(); // Prevent keyboard events from repeating after opening e.g. a file chooser and closing it quickly state.repeat.current_id += 1; + state.restore_cursor_after_hide(); if let Some(window) = keyboard_focused_window { if let Some(ref mut compose) = state.compose_state { @@ -1732,14 +1803,9 @@ impl Dispatch for WaylandClientStatePtr { if state.enter_token.is_some() { state.enter_token = None; } + state.restore_cursor_after_hide(); if let Some(style) = state.cursor_style { - 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 { + if let Some(cursor_shape_device) = &state.cursor_shape_device { cursor_shape_device.set_shape(serial, to_shape(style)); } else { let scale = window.primary_output_scale(); @@ -1765,6 +1831,7 @@ impl Dispatch for WaylandClientStatePtr { state.mouse_focused_window = None; state.mouse_location = None; state.button_pressed = None; + state.cursor_hidden_window = None; drop(state); focused_window.handle_input(input); @@ -1780,6 +1847,7 @@ impl Dispatch for WaylandClientStatePtr { return; } state.mouse_location = Some(point(px(surface_x as f32), px(surface_y as f32))); + state.restore_cursor_after_hide(); if let Some(window) = state.mouse_focused_window.clone() { if window.is_blocked() { diff --git a/crates/gpui_linux/src/linux/x11/client.rs b/crates/gpui_linux/src/linux/x11/client.rs index 03272a9905..bedd3c3e29 100644 --- a/crates/gpui_linux/src/linux/x11/client.rs +++ b/crates/gpui_linux/src/linux/x11/client.rs @@ -211,6 +211,8 @@ pub struct X11ClientState { pub(crate) cursor_handle: cursor::Handle, pub(crate) cursor_styles: HashMap, pub(crate) cursor_cache: HashMap>, + pub(crate) invisible_cursor_cache: Option, + pub(crate) cursor_hidden_window: Option, pointer_device_states: BTreeMap, @@ -249,6 +251,9 @@ impl X11ClientStatePtr { if state.keyboard_focused_window == Some(x_window) { state.keyboard_focused_window = None; } + if state.cursor_hidden_window == Some(x_window) { + state.cursor_hidden_window = None; + } state.cursor_styles.remove(&x_window); } @@ -543,6 +548,8 @@ impl X11Client { cursor_handle, cursor_styles: HashMap::default(), cursor_cache: HashMap::default(), + cursor_hidden_window: None, + invisible_cursor_cache: None, pointer_device_states, @@ -971,6 +978,7 @@ impl X11Client { compose_state.reset(); } state.pre_edit_text.take(); + state.restore_cursor_after_hide(); drop(state); self.reset_ime(); window.handle_ime_delete(); @@ -1224,6 +1232,7 @@ impl X11Client { Event::XinputMotion(event) => { let window = self.get_window(event.event)?; let mut state = self.0.borrow_mut(); + state.restore_cursor_after_hide(); if window.is_blocked() { // We want to set the cursor to the default arrow // when the window is blocked @@ -1286,6 +1295,7 @@ impl X11Client { window.set_hovered(true); let mut state = self.0.borrow_mut(); state.mouse_focused_window = Some(event.event); + state.restore_cursor_after_hide(); } Event::XinputLeave(event) if event.mode == xinput::NotifyMode::NORMAL => { let mut state = self.0.borrow_mut(); @@ -1667,11 +1677,17 @@ impl LinuxClient for X11Client { return; } + state.cursor_styles.insert(focused_window, style); + + // Don't clobber the invisible cursor; restore reads back from `cursor_styles`. + if state.cursor_hidden_window == Some(focused_window) { + return; + } + let Some(cursor) = state.get_cursor_icon(style) else { return; }; - state.cursor_styles.insert(focused_window, style); check_reply( || "Failed to set cursor style", state.xcb_connection.change_window_attributes( @@ -1686,6 +1702,14 @@ impl LinuxClient for X11Client { state.xcb_connection.flush().log_err(); } + fn hide_cursor_until_mouse_moves(&self) { + self.0.borrow_mut().hide_cursor_until_mouse_moves(); + } + + fn is_cursor_visible(&self) -> bool { + self.0.borrow().cursor_hidden_window.is_none() + } + fn open_uri(&self, uri: &str) { #[cfg(any(feature = "wayland", feature = "x11"))] open_uri_internal( @@ -1986,41 +2010,33 @@ impl X11ClientState { return *cursor; } - let result; - match style { - CursorStyle::None => match create_invisible_cursor(&self.xcb_connection) { - Ok(loaded_cursor) => result = Ok(loaded_cursor), - Err(err) => result = Err(err.context("X11: error while creating invisible cursor")), - }, - _ => 'outer: { - let mut errors = String::new(); - let cursor_icon_names = cursor_style_to_icon_names(style); - for cursor_icon_name in cursor_icon_names { - match self - .cursor_handle - .load_cursor(&self.xcb_connection, cursor_icon_name) - { - Ok(loaded_cursor) => { - if loaded_cursor != x11rb::NONE { - result = Ok(loaded_cursor); - break 'outer; - } - } - Err(err) => { - errors.push_str(&err.to_string()); - errors.push('\n'); + let result = 'outer: { + let mut errors = String::new(); + let cursor_icon_names = cursor_style_to_icon_names(style); + for cursor_icon_name in cursor_icon_names { + match self + .cursor_handle + .load_cursor(&self.xcb_connection, cursor_icon_name) + { + Ok(loaded_cursor) => { + if loaded_cursor != x11rb::NONE { + break 'outer Ok(loaded_cursor); } } + Err(err) => { + errors.push_str(&err.to_string()); + errors.push('\n'); + } } - if errors.is_empty() { - result = Err(anyhow!( - "errors while loading cursor icons {:?}:\n{}", - cursor_icon_names, - errors - )); - } else { - result = Err(anyhow!("did not find cursor icons {:?}", cursor_icon_names)); - } + } + if errors.is_empty() { + Err(anyhow!( + "errors while loading cursor icons {:?}:\n{}", + cursor_icon_names, + errors + )) + } else { + Err(anyhow!("did not find cursor icons {:?}", cursor_icon_names)) } }; @@ -2052,6 +2068,73 @@ impl X11ClientState { self.cursor_cache.insert(style, cursor); cursor } + + fn get_or_create_invisible_cursor(&mut self) -> Option { + if let Some(cursor) = self.invisible_cursor_cache { + return Some(cursor); + } + let cursor = create_invisible_cursor(&self.xcb_connection) + .context("X11: error while creating invisible cursor") + .log_err()?; + self.invisible_cursor_cache = Some(cursor); + Some(cursor) + } + + fn hide_cursor_until_mouse_moves(&mut self) { + if self.cursor_hidden_window.is_some() { + return; + } + let Some(focused_window) = self.mouse_focused_window else { + // No window to apply the per-window invisible cursor to. + return; + }; + let Some(invisible_cursor) = self.get_or_create_invisible_cursor() else { + return; + }; + check_reply( + || "Failed to hide cursor", + self.xcb_connection.change_window_attributes( + focused_window, + &ChangeWindowAttributesAux { + cursor: Some(invisible_cursor), + ..Default::default() + }, + ), + ) + .log_err(); + self.xcb_connection.flush().log_err(); + self.cursor_hidden_window = Some(focused_window); + } + + fn restore_cursor_after_hide(&mut self) { + let Some(hidden_window) = self.cursor_hidden_window.take() else { + return; + }; + let style = self + .cursor_styles + .get(&hidden_window) + .copied() + .unwrap_or(CursorStyle::Arrow); + let Some(cursor) = self.get_cursor_icon(style) else { + log::warn!( + "X11: no cursor icon available to restore {:?} after hide; cursor may stay invisible", + style + ); + return; + }; + check_reply( + || "Failed to restore cursor style after hide", + self.xcb_connection.change_window_attributes( + hidden_window, + &ChangeWindowAttributesAux { + cursor: Some(cursor), + ..Default::default() + }, + ), + ) + .log_err(); + self.xcb_connection.flush().log_err(); + } } // Adapted from: diff --git a/crates/gpui_macos/src/platform.rs b/crates/gpui_macos/src/platform.rs index fefe957bb2..77cf85a8d1 100644 --- a/crates/gpui_macos/src/platform.rs +++ b/crates/gpui_macos/src/platform.rs @@ -52,7 +52,10 @@ use std::{ ptr, rc::Rc, slice, str, - sync::{Arc, OnceLock, atomic::AtomicBool}, + sync::{ + Arc, OnceLock, + atomic::{AtomicBool, Ordering}, + }, }; use util::{ ResultExt, @@ -179,7 +182,8 @@ pub(crate) struct MacPlatformState { dock_menu: Option, menus: Option>, keyboard_mapper: Rc, - cursor_hidden: Arc, + /// Mirrors `[NSCursor setHiddenUntilMouseMoves:]` state, which AppKit doesn't expose. + cursor_visible: Arc, } impl MacPlatform { @@ -216,7 +220,7 @@ impl MacPlatform { on_thermal_state_change: None, menus: None, keyboard_mapper, - cursor_hidden: Arc::new(AtomicBool::new(false)), + cursor_visible: Arc::new(AtomicBool::new(true)), })) } @@ -621,10 +625,10 @@ impl Platform for MacPlatform { handle: AnyWindowHandle, options: WindowParams, ) -> Result> { - let (cursor_hidden, foreground_executor, background_executor, renderer_context) = { + let (cursor_visible, foreground_executor, background_executor, renderer_context) = { let guard = self.0.lock(); ( - guard.cursor_hidden.clone(), + guard.cursor_visible.clone(), guard.foreground_executor.clone(), guard.background_executor.clone(), guard.renderer_context.clone(), @@ -634,7 +638,7 @@ impl Platform for MacPlatform { Ok(Box::new(MacWindow::open( handle, options, - cursor_hidden, + cursor_visible, foreground_executor, background_executor, renderer_context, @@ -991,12 +995,25 @@ impl Platform for MacPlatform { /// Match cursor style to one of the styles available /// in macOS's [NSCursor](https://developer.apple.com/documentation/appkit/nscursor). fn set_cursor_style(&self, style: CursorStyle) { - let cursor_hidden = self.0.lock().cursor_hidden.clone(); unsafe { - set_active_window_cursor_style(style, &cursor_hidden); + set_active_window_cursor_style(style); } } + fn hide_cursor_until_mouse_moves(&self) { + let cursor_visible = self.0.lock().cursor_visible.clone(); + if !cursor_visible.swap(false, Ordering::Relaxed) { + return; + } + unsafe { + let _: () = msg_send![class!(NSCursor), setHiddenUntilMouseMoves: YES]; + } + } + + fn is_cursor_visible(&self) -> bool { + self.0.lock().cursor_visible.load(Ordering::Relaxed) + } + fn should_auto_hide_scrollbars(&self) -> bool { #[allow(non_upper_case_globals)] const NSScrollerStyleOverlay: NSInteger = 1; diff --git a/crates/gpui_macos/src/window.rs b/crates/gpui_macos/src/window.rs index 2ab4b52d8d..8e7fcb154c 100644 --- a/crates/gpui_macos/src/window.rs +++ b/crates/gpui_macos/src/window.rs @@ -324,10 +324,7 @@ pub(crate) fn convert_mouse_position(position: NSPoint, window_height: Pixels) - /// This function is not thread safe. Callers must ensure this is called on the AppKit main /// thread because it reads the active AppKit window and updates GPUI window state associated /// with Objective-C objects. -pub(crate) unsafe fn set_active_window_cursor_style( - style: CursorStyle, - cursor_hidden: &AtomicBool, -) { +pub(crate) unsafe fn set_active_window_cursor_style(style: CursorStyle) { // SAFETY: The caller guarantees AppKit main-thread access. The class check ensures the // window has our WINDOW_STATE_IVAR before reading it. unsafe { @@ -345,9 +342,6 @@ pub(crate) unsafe fn set_active_window_cursor_style( }; let Some(active_window) = active_window else { - if !matches!(style, CursorStyle::None) { - unhide_cursor(cursor_hidden); - } return; }; @@ -363,22 +357,6 @@ pub(crate) unsafe fn set_active_window_cursor_style( } } -/// Unhides the cursor if this GPUI platform instance has hidden it. -/// -/// # Safety -/// -/// Must be called on the AppKit main thread. -unsafe fn unhide_cursor(cursor_hidden: &AtomicBool) { - unsafe { - if cursor_hidden - .compare_exchange(true, false, Ordering::Relaxed, Ordering::Relaxed) - .is_ok() - { - let _: () = msg_send![class!(NSCursor), unhide]; - } - } -} - unsafe fn build_window_class(name: &'static str, superclass: &Class) -> *const Class { unsafe { let mut decl = ClassDecl::new(name, superclass).unwrap(); @@ -496,7 +474,7 @@ struct MacWindowState { blurred_view: Option, background_appearance: WindowBackgroundAppearance, cursor_style: CursorStyle, - cursor_hidden: Arc, + cursor_visible: Arc, display_link: Option, renderer: renderer::Renderer, request_frame_callback: Option>, @@ -698,7 +676,7 @@ impl MacWindow { tabbing_identifier, .. }: WindowParams, - cursor_hidden: Arc, + cursor_visible: Arc, foreground_executor: ForegroundExecutor, background_executor: BackgroundExecutor, renderer_context: renderer::Context, @@ -816,7 +794,7 @@ impl MacWindow { blurred_view: None, background_appearance: WindowBackgroundAppearance::Opaque, cursor_style: CursorStyle::Arrow, - cursor_hidden, + cursor_visible, display_link: None, renderer: renderer::new_renderer( renderer_context, @@ -1850,26 +1828,7 @@ extern "C" fn reset_cursor_rects(this: &Object, _: Sel) { let _: () = msg_send![super(this, class!(NSView)), resetCursorRects]; let window_state = get_window_state(this); - let cursor_style; - let cursor_hidden; - - { - let window_state = window_state.lock(); - - if matches!(window_state.cursor_style, CursorStyle::None) { - if window_state - .cursor_hidden - .compare_exchange(false, true, Ordering::Relaxed, Ordering::Relaxed) - .is_ok() - { - let _: () = msg_send![class!(NSCursor), hide]; - } - return; - } - - cursor_style = window_state.cursor_style; - cursor_hidden = window_state.cursor_hidden.clone(); - }; + let cursor_style = window_state.lock().cursor_style; let cursor: id = match cursor_style { CursorStyle::Arrow => msg_send![class!(NSCursor), arrowCursor], @@ -1905,11 +1864,8 @@ extern "C" fn reset_cursor_rects(this: &Object, _: Sel) { CursorStyle::DragLink => msg_send![class!(NSCursor), dragLinkCursor], CursorStyle::DragCopy => msg_send![class!(NSCursor), dragCopyCursor], CursorStyle::ContextualMenu => msg_send![class!(NSCursor), contextualMenuCursor], - CursorStyle::None => unreachable!(), }; - unhide_cursor(&cursor_hidden); - let bounds = NSView::bounds(this as *const Object as id); let _: () = msg_send![this, addCursorRect: bounds cursor: cursor]; } @@ -2140,6 +2096,20 @@ extern "C" fn handle_view_event(this: &Object, _: Sel, native_event: id) { let event = unsafe { platform_input_from_native(native_event, Some(window_height)) }; if let Some(mut event) = event { + // AppKit unhides the cursor on the next mouse movement; mirror that here. + if matches!( + event, + PlatformInput::MouseMove(_) + | PlatformInput::MouseDown(_) + | PlatformInput::MouseUp(_) + | PlatformInput::MousePressure(_) + | PlatformInput::MouseExited(_) + | PlatformInput::ScrollWheel(_) + | PlatformInput::Pinch(_) + ) { + lock.cursor_visible.store(true, Ordering::Relaxed); + } + match &mut event { PlatformInput::MouseDown( event @ MouseDownEvent { @@ -2365,6 +2335,9 @@ extern "C" fn window_did_change_key_status(this: &Object, selector: Sel, _: id) let lock = window_state.lock(); let is_active = unsafe { lock.native_window.isKeyWindow() == YES }; + // AppKit also unhides the cursor on activation changes, so mirror that here. + lock.cursor_visible.store(true, Ordering::Relaxed); + // When opening a pop-up while the application isn't active, Cocoa sends a spurious // `windowDidBecomeKey` message to the previous key window even though that window // isn't actually key. This causes a bug if the application is later activated while diff --git a/crates/gpui_macros/src/styles.rs b/crates/gpui_macros/src/styles.rs index 133c9fdebe..fdbc64f623 100644 --- a/crates/gpui_macros/src/styles.rs +++ b/crates/gpui_macros/src/styles.rs @@ -327,12 +327,6 @@ pub fn cursor_style_methods(input: TokenStream) -> TokenStream { 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/crates/gpui_web/src/platform.rs b/crates/gpui_web/src/platform.rs index 4d78b71aa0..290ef33e5f 100644 --- a/crates/gpui_web/src/platform.rs +++ b/crates/gpui_web/src/platform.rs @@ -13,11 +13,12 @@ use gpui::{ use gpui_wgpu::WgpuContext; use std::{ borrow::Cow, - cell::RefCell, + cell::{Cell, RefCell}, path::{Path, PathBuf}, rc::Rc, sync::Arc, }; +use wasm_bindgen::prelude::*; static BUNDLED_FONTS: &[&[u8]] = &[ include_bytes!("../../../assets/fonts/ibm-plex-sans/IBMPlexSans-Regular.ttf"), @@ -39,6 +40,9 @@ pub struct WebPlatform { active_display: Rc, callbacks: RefCell, wgpu_context: Rc>>, + cursor_visible: Rc>, + last_cursor_css: Rc>, + _cursor_restore_listeners: Vec, } #[derive(Default)] @@ -77,6 +81,14 @@ impl WebPlatform { let active_display: Rc = Rc::new(WebDisplay::new(browser_window.clone())); + let cursor_visible = Rc::new(Cell::new(true)); + let last_cursor_css = Rc::new(Cell::new("default")); + let cursor_restore_listeners = cursor_restore_listeners( + &browser_window, + cursor_visible.clone(), + last_cursor_css.clone(), + ); + Self { browser_window, background_executor, @@ -86,6 +98,9 @@ impl WebPlatform { active_display, callbacks: RefCell::new(WebPlatformCallbacks::default()), wgpu_context: Rc::new(RefCell::new(None)), + cursor_visible, + last_cursor_css, + _cursor_restore_listeners: cursor_restore_listeners, } } } @@ -292,18 +307,25 @@ impl Platform for WebPlatform { CursorStyle::DragLink => "alias", CursorStyle::DragCopy => "copy", CursorStyle::ContextualMenu => "context-menu", - CursorStyle::None => "none", }; - if let Some(document) = self.browser_window.document() { - if let Some(body) = document.body() { - if let Err(error) = body.style().set_property("cursor", css_cursor) { - log::warn!("Failed to set cursor style: {error:?}"); - } - } + self.last_cursor_css.set(css_cursor); + if self.cursor_visible.get() { + set_body_cursor(&self.browser_window, css_cursor); } } + fn hide_cursor_until_mouse_moves(&self) { + if !self.cursor_visible.replace(false) { + return; + } + set_body_cursor(&self.browser_window, "none"); + } + + fn is_cursor_visible(&self) -> bool { + self.cursor_visible.get() + } + fn should_auto_hide_scrollbars(&self) -> bool { true } @@ -342,3 +364,72 @@ impl Platform for WebPlatform { self.callbacks.borrow_mut().keyboard_layout_change = Some(callback); } } + +struct EventListenerHandle { + target: web_sys::EventTarget, + event_name: &'static str, + closure: Closure, +} + +impl Drop for EventListenerHandle { + fn drop(&mut self) { + self.target + .remove_event_listener_with_callback( + self.event_name, + self.closure.as_ref().unchecked_ref(), + ) + .ok(); + } +} + +fn cursor_restore_listeners( + browser_window: &web_sys::Window, + cursor_visible: Rc>, + last_cursor_css: Rc>, +) -> Vec { + let mut handles = Vec::new(); + let Some(document) = browser_window.document() else { + return handles; + }; + + let make_restore_handler = |browser_window: web_sys::Window| { + let cursor_visible = cursor_visible.clone(); + let last_cursor_css = last_cursor_css.clone(); + Closure::::new(move |_event: JsValue| { + if !cursor_visible.replace(true) { + set_body_cursor(&browser_window, last_cursor_css.get()); + } + }) + }; + + let mut add_listener = |target: &web_sys::EventTarget, event_name: &'static str| { + let closure = make_restore_handler(browser_window.clone()); + target + .add_event_listener_with_callback(event_name, closure.as_ref().unchecked_ref()) + .ok(); + handles.push(EventListenerHandle { + target: target.clone(), + event_name, + closure, + }); + }; + + let document_target: &web_sys::EventTarget = document.as_ref(); + let window_target: &web_sys::EventTarget = browser_window.as_ref(); + + add_listener(document_target, "mousemove"); + add_listener(document_target, "mouseenter"); + add_listener(window_target, "blur"); + add_listener(document_target, "visibilitychange"); + + handles +} + +fn set_body_cursor(browser_window: &web_sys::Window, css_cursor: &str) { + if let Some(document) = browser_window.document() + && let Some(body) = document.body() + && let Err(error) = body.style().set_property("cursor", css_cursor) + { + log::warn!("Failed to set cursor style: {error:?}"); + } +} diff --git a/crates/gpui_windows/src/events.rs b/crates/gpui_windows/src/events.rs index 23b0c078b8..370582e83b 100644 --- a/crates/gpui_windows/src/events.rs +++ b/crates/gpui_windows/src/events.rs @@ -1,4 +1,4 @@ -use std::rc::Rc; +use std::{rc::Rc, sync::atomic::Ordering}; use ::util::ResultExt; use anyhow::Context as _; @@ -297,6 +297,7 @@ impl WindowsWindowInner { fn handle_mouse_move_msg(&self, handle: HWND, lparam: LPARAM, wparam: WPARAM) -> Option { self.start_tracking_mouse(handle, TME_LEAVE); + self.restore_cursor_after_hide(); let Some(mut func) = self.state.callbacks.input.take() else { return Some(1); @@ -330,6 +331,9 @@ impl WindowsWindowInner { fn handle_mouse_leave_msg(&self) -> Option { self.state.hovered.set(false); + // The next window's `WM_SETCURSOR` picks its own cursor, so we just clear + // the flag for tight `is_cursor_visible()` semantics. + self.state.cursor_visible.store(true, Ordering::Relaxed); if let Some(mut callback) = self.state.callbacks.hovered_status_change.take() { callback(false); self.state @@ -726,6 +730,10 @@ impl WindowsWindowInner { let activated = wparam.loword() > 0; let this = self.clone(); + if !activated { + this.state.cursor_visible.store(true, Ordering::Relaxed); + } + // When the window is activated (gains focus), reset the modifier tracking state. // This fixes the issue where Alt-Tab away and back leaves stale modifier state // (especially the Alt key) because Windows doesn't always send key-up events to @@ -915,6 +923,7 @@ impl WindowsWindowInner { fn handle_nc_mouse_move_msg(&self, handle: HWND, lparam: LPARAM) -> Option { self.start_tracking_mouse(handle, TME_LEAVE | TME_NONCLIENT); + self.restore_cursor_after_hide(); let mut func = self.state.callbacks.input.take()?; let scale_factor = self.state.scale_factor.get(); @@ -1078,8 +1087,13 @@ impl WindowsWindowInner { { return None; } + let cursor = if self.state.cursor_visible.load(Ordering::Relaxed) { + self.state.current_cursor.get() + } else { + None + }; unsafe { - SetCursor(self.state.current_cursor.get()); + SetCursor(cursor); }; Some(0) } @@ -1233,6 +1247,15 @@ impl WindowsWindowInner { } } + /// Clear the hidden flag and restore the cursor immediately + fn restore_cursor_after_hide(&self) { + if !self.state.cursor_visible.swap(true, Ordering::Relaxed) { + unsafe { + SetCursor(self.state.current_cursor.get()); + } + } + } + fn start_tracking_mouse(&self, handle: HWND, flags: TRACKMOUSEEVENT_FLAGS) { if !self.state.hovered.get() { self.state.hovered.set(true); diff --git a/crates/gpui_windows/src/platform.rs b/crates/gpui_windows/src/platform.rs index 4c28982562..7b76c4d12a 100644 --- a/crates/gpui_windows/src/platform.rs +++ b/crates/gpui_windows/src/platform.rs @@ -63,6 +63,8 @@ pub(crate) struct WindowsPlatformState { jump_list: RefCell, // NOTE: standard cursor handles don't need to close. pub(crate) current_cursor: Cell>, + /// Shared with each window so `WM_SETCURSOR` can read it directly. + pub(crate) cursor_visible: Arc, directx_devices: RefCell>, } @@ -87,6 +89,7 @@ impl WindowsPlatformState { callbacks, jump_list: RefCell::new(jump_list), current_cursor: Cell::new(current_cursor), + cursor_visible: Arc::new(AtomicBool::new(true)), directx_devices: RefCell::new(directx_devices), menus: RefCell::new(Vec::new()), } @@ -219,6 +222,7 @@ impl WindowsPlatform { icon: self.icon, executor: self.foreground_executor.clone(), current_cursor: self.inner.state.current_cursor.get(), + cursor_visible: self.inner.state.cursor_visible.clone(), drop_target_helper: self.drop_target_helper.clone().unwrap(), validation_number: self.inner.validation_number, main_receiver: self.inner.main_receiver.clone(), @@ -682,6 +686,31 @@ impl Platform for WindowsPlatform { } } + fn hide_cursor_until_mouse_moves(&self) { + if !self + .inner + .state + .cursor_visible + .swap(false, Ordering::Relaxed) + { + return; + } + + for handle in self.raw_window_handles.read().iter() { + let Some(window) = window_from_hwnd(handle.as_raw()) else { + continue; + }; + if window.state.hovered.get() { + unsafe { SetCursor(None) }; + break; + } + } + } + + fn is_cursor_visible(&self) -> bool { + self.inner.state.cursor_visible.load(Ordering::Relaxed) + } + fn should_auto_hide_scrollbars(&self) -> bool { should_auto_hide_scrollbars().log_err().unwrap_or(false) } @@ -1015,6 +1044,7 @@ pub(crate) struct WindowCreationInfo { pub(crate) icon: HICON, pub(crate) executor: ForegroundExecutor, pub(crate) current_cursor: Option, + pub(crate) cursor_visible: Arc, pub(crate) drop_target_helper: IDropTargetHelper, pub(crate) validation_number: usize, pub(crate) main_receiver: PriorityQueueReceiver, diff --git a/crates/gpui_windows/src/util.rs b/crates/gpui_windows/src/util.rs index fe5093dede..15dd4c6998 100644 --- a/crates/gpui_windows/src/util.rs +++ b/crates/gpui_windows/src/util.rs @@ -115,7 +115,6 @@ pub(crate) fn load_cursor(style: CursorStyle) -> Option { CursorStyle::ResizeUpLeftDownRight => (&SIZENWSE, IDC_SIZENWSE), CursorStyle::ResizeUpRightDownLeft => (&SIZENESW, IDC_SIZENESW), CursorStyle::OperationNotAllowed => (&NO, IDC_NO), - CursorStyle::None => return None, _ => (&ARROW, IDC_ARROW), }; Some( diff --git a/crates/gpui_windows/src/window.rs b/crates/gpui_windows/src/window.rs index f655c1989e..2fd7c3c646 100644 --- a/crates/gpui_windows/src/window.rs +++ b/crates/gpui_windows/src/window.rs @@ -66,6 +66,8 @@ pub struct WindowsWindowState { pub click_state: ClickState, pub current_cursor: Cell>, + /// Shared with [`WindowsPlatformState::cursor_visible`]. + pub cursor_visible: Arc, pub nc_button_pressed: Cell>, pub display: Cell, @@ -98,6 +100,7 @@ impl WindowsWindowState { directx_devices: &DirectXDevices, window_params: &CREATESTRUCTW, current_cursor: Option, + cursor_visible: Arc, display: WindowsDisplay, min_size: Option>, appearance: WindowAppearance, @@ -158,6 +161,7 @@ impl WindowsWindowState { renderer: RefCell::new(renderer), click_state, current_cursor: Cell::new(current_cursor), + cursor_visible, nc_button_pressed: Cell::new(nc_button_pressed), display: Cell::new(display), fullscreen: Cell::new(fullscreen), @@ -234,6 +238,7 @@ impl WindowsWindowInner { &context.directx_devices, cs, context.current_cursor, + context.cursor_visible.clone(), context.display, context.min_size, context.appearance, @@ -373,6 +378,7 @@ struct WindowCreateContext { min_size: Option>, executor: ForegroundExecutor, current_cursor: Option, + cursor_visible: Arc, drop_target_helper: IDropTargetHelper, validation_number: usize, main_receiver: PriorityQueueReceiver, @@ -394,6 +400,7 @@ impl WindowsWindow { icon, executor, current_cursor, + cursor_visible, drop_target_helper, validation_number, main_receiver, @@ -473,6 +480,7 @@ impl WindowsWindow { min_size: params.window_min_size, executor, current_cursor, + cursor_visible, drop_target_helper, validation_number, main_receiver, diff --git a/crates/migrator/src/migrations.rs b/crates/migrator/src/migrations.rs index 8fa8907a16..394b26f57a 100644 --- a/crates/migrator/src/migrations.rs +++ b/crates/migrator/src/migrations.rs @@ -358,3 +358,9 @@ pub(crate) mod m_2026_04_17 { pub(crate) use settings::promote_show_branch_icon_true_to_show_branch_status_icon; } + +pub(crate) mod m_2026_05_04 { + mod settings; + + pub(crate) use settings::SETTINGS_PATTERNS; +} diff --git a/crates/migrator/src/migrations/m_2026_05_04/settings.rs b/crates/migrator/src/migrations/m_2026_05_04/settings.rs new file mode 100644 index 0000000000..a2a705e03f --- /dev/null +++ b/crates/migrator/src/migrations/m_2026_05_04/settings.rs @@ -0,0 +1,38 @@ +use std::ops::Range; +use tree_sitter::{Query, QueryMatch}; + +use crate::MigrationPatterns; +use crate::patterns::SETTINGS_ROOT_KEY_VALUE_PATTERN; + +pub const SETTINGS_PATTERNS: MigrationPatterns = + &[(SETTINGS_ROOT_KEY_VALUE_PATTERN, replace_setting_value)]; + +fn replace_setting_value( + contents: &str, + mat: &QueryMatch, + query: &Query, +) -> Option<(Range, String)> { + let setting_capture_ix = query.capture_index_for_name("name")?; + let setting_name_range = mat + .nodes_for_capture_index(setting_capture_ix) + .next()? + .byte_range(); + let setting_name = contents.get(setting_name_range)?; + + if setting_name != "hide_mouse" { + return None; + } + + let value_capture_ix = query.capture_index_for_name("value")?; + let value_range = mat + .nodes_for_capture_index(value_capture_ix) + .next()? + .byte_range(); + let value = contents.get(value_range.clone())?; + + if value.trim() != "\"on_typing_and_movement\"" { + return None; + } + + Some((value_range, "\"on_typing_and_action\"".to_string())) +} diff --git a/crates/migrator/src/migrator.rs b/crates/migrator/src/migrator.rs index ac7e4e337e..e1b15d98ea 100644 --- a/crates/migrator/src/migrator.rs +++ b/crates/migrator/src/migrator.rs @@ -253,6 +253,10 @@ pub fn migrate_settings(text: &str) -> Result> { MigrationType::Json( migrations::m_2026_04_17::promote_show_branch_icon_true_to_show_branch_status_icon, ), + MigrationType::TreeSitter( + migrations::m_2026_05_04::SETTINGS_PATTERNS, + &SETTINGS_QUERY_2026_05_04, + ), ]; run_migrations(text, migrations) } @@ -395,6 +399,10 @@ define_query!( KEYMAP_QUERY_2026_03_23, migrations::m_2026_03_23::KEYMAP_PATTERNS ); +define_query!( + SETTINGS_QUERY_2026_05_04, + migrations::m_2026_05_04::SETTINGS_PATTERNS +); // custom query static EDIT_PREDICTION_SETTINGS_MIGRATION_QUERY: LazyLock = LazyLock::new(|| { @@ -5346,6 +5354,42 @@ mod tests { ); } + #[test] + fn test_rename_hide_mouse_on_typing_and_movement_to_on_typing_and_action() { + assert_migrate_settings( + r#" + { + "hide_mouse": "on_typing_and_movement" + } + "#, + Some( + r#" + { + "hide_mouse": "on_typing_and_action" + } + "#, + ), + ); + } + + #[test] + fn test_chain_hide_mouse_while_typing_to_on_typing_and_action() { + assert_migrate_settings( + r#" + { + "hide_mouse_while_typing": true + } + "#, + Some( + r#" + { + "hide_mouse": "on_typing_and_action" + } + "#, + ), + ); + } + #[test] fn test_promote_show_branch_icon_true_to_show_branch_status_icon_no_change_when_already_migrated() { diff --git a/crates/settings/src/vscode_import.rs b/crates/settings/src/vscode_import.rs index 64f00bc5d6..db7f847b23 100644 --- a/crates/settings/src/vscode_import.rs +++ b/crates/settings/src/vscode_import.rs @@ -192,6 +192,7 @@ impl VsCodeSettings { ..GlobalLspSettingsContent::default() }), helix_mode: None, + hide_mouse: None, image_viewer: None, journal: None, language_models: None, @@ -267,7 +268,6 @@ impl VsCodeSettings { go_to_definition_fallback: None, go_to_definition_scroll_strategy: None, gutter: self.gutter_content(), - hide_mouse: None, horizontal_scroll_margin: None, hover_popover_delay: self.read_u64("editor.hover.delay").map(Into::into), hover_popover_enabled: self.read_bool("editor.hover.enabled"), diff --git a/crates/settings_content/src/editor.rs b/crates/settings_content/src/editor.rs index 5c0dd93968..6a36d0c524 100644 --- a/crates/settings_content/src/editor.rs +++ b/crates/settings_content/src/editor.rs @@ -22,10 +22,6 @@ pub struct EditorSettingsContent { /// /// Default: bar pub cursor_shape: Option, - /// Determines when the mouse cursor should be hidden in an editor or input box. - /// - /// Default: on_typing_and_movement - pub hide_mouse: Option, /// Determines how snippets are sorted relative to other completion items. /// /// Default: inline @@ -836,34 +832,6 @@ pub enum GoToDefinitionScrollStrategy { Preserve, } -/// Determines when the mouse cursor should be hidden in an editor or input box. -/// -/// Default: on_typing_and_movement -#[derive( - Copy, - Clone, - Debug, - Default, - Serialize, - Deserialize, - PartialEq, - Eq, - JsonSchema, - MergeFrom, - strum::VariantArray, - strum::VariantNames, -)] -#[serde(rename_all = "snake_case")] -pub enum HideMouseMode { - /// Never hide the mouse cursor - Never, - /// Hide only when typing - OnTyping, - /// Hide on both typing and cursor movement - #[default] - OnTypingAndMovement, -} - /// Determines how snippets are sorted relative to other completion items. /// /// Default: inline diff --git a/crates/settings_content/src/settings_content.rs b/crates/settings_content/src/settings_content.rs index 251718d146..02fe409c63 100644 --- a/crates/settings_content/src/settings_content.rs +++ b/crates/settings_content/src/settings_content.rs @@ -82,6 +82,35 @@ pub enum ParseStatus { Failed { error: String }, } +/// Determines when the mouse cursor should be hidden in response to keyboard +/// input. +/// +/// Default: on_typing_and_action +#[derive( + Copy, + Clone, + Debug, + Default, + Serialize, + Deserialize, + PartialEq, + Eq, + JsonSchema, + MergeFrom, + strum::VariantArray, + strum::VariantNames, +)] +#[serde(rename_all = "snake_case")] +pub enum HideMouseMode { + /// Never hide the mouse cursor + Never, + /// Hide only when typing + OnTyping, + /// Hide on typing and on key bindings that resolve to an action + #[default] + OnTypingAndAction, +} + #[with_fallible_options] #[derive(Debug, PartialEq, Default, Clone, Serialize, Deserialize, JsonSchema, MergeFrom)] pub struct SettingsContent { @@ -156,6 +185,13 @@ pub struct SettingsContent { /// Default: false pub helix_mode: Option, + /// Determines when the mouse cursor should be hidden in response to + /// keyboard input. Applies globally across all input surfaces (editors, + /// terminals, palettes, etc.). + /// + /// Default: on_typing_and_action + pub hide_mouse: Option, + pub journal: Option, /// A map of log scopes to the desired log level. diff --git a/crates/settings_ui/src/page_data.rs b/crates/settings_ui/src/page_data.rs index a43c566d1a..ce0c53b382 100644 --- a/crates/settings_ui/src/page_data.rs +++ b/crates/settings_ui/src/page_data.rs @@ -1145,9 +1145,9 @@ fn appearance_page() -> SettingsPage { description: "When to hide the mouse cursor.", field: Box::new(SettingField { json_path: Some("hide_mouse"), - pick: |settings_content| settings_content.editor.hide_mouse.as_ref(), + pick: |settings_content| settings_content.hide_mouse.as_ref(), write: |settings_content, value, _| { - settings_content.editor.hide_mouse = value; + settings_content.hide_mouse = value; }, }), metadata: None, diff --git a/crates/vim/src/helix.rs b/crates/vim/src/helix.rs index eebad4d438..d61b0547ae 100644 --- a/crates/vim/src/helix.rs +++ b/crates/vim/src/helix.rs @@ -7,8 +7,8 @@ mod surround; use editor::display_map::{DisplayRow, DisplaySnapshot}; use editor::{ - DisplayPoint, Editor, EditorSettings, HideMouseCursorOrigin, MultiBufferOffset, - NavigationOverlayLabel, NavigationTargetOverlay, SelectionEffects, ToOffset, ToPoint, movement, + DisplayPoint, Editor, EditorSettings, MultiBufferOffset, NavigationOverlayLabel, + NavigationTargetOverlay, SelectionEffects, ToOffset, ToPoint, movement, }; use gpui::actions; use gpui::{App, Context, Font, Hsla, Pixels, Window, WindowTextSystem}; @@ -807,7 +807,6 @@ impl Vim { ) { let count = Vim::take_count(cx).unwrap_or(1); self.update_editor(cx, |_, editor, cx| { - editor.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx); let display_map = editor.display_map.update(cx, |map, cx| map.snapshot(cx)); let mut selections = editor.selections.all::(&display_map); let max_point = display_map.buffer_snapshot().max_point(); diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index a76846fd33..6c0c3d0201 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -22,8 +22,8 @@ mod visual; use crate::normal::paste::Paste as VimPaste; use collections::HashMap; use editor::{ - Anchor, Bias, Editor, EditorEvent, EditorSettings, HideMouseCursorOrigin, MultiBufferOffset, - NavigationOverlayKey, NavigationTargetOverlay, SelectionEffects, + Anchor, Bias, Editor, EditorEvent, EditorSettings, MultiBufferOffset, NavigationOverlayKey, + NavigationTargetOverlay, SelectionEffects, actions::Paste, display_map::ToDisplayPoint, movement::{self, FindRange}, @@ -1089,10 +1089,6 @@ impl Vim { if let Some(action) = keystroke_event.action.as_ref() { // Keystroke is handled by the vim system, so continue forward if action.name().starts_with("vim::") { - self.update_editor(cx, |_, editor, cx| { - editor.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx) - }); - return; } } else if window.has_pending_keystrokes() || keystroke_event.keystroke.is_ime_in_progress() diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 9461c89b7a..e9da593960 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -382,6 +382,8 @@ pub fn initialize_workspace(app_state: Arc, cx: &mut App) { }) .detach(); + init_cursor_hide_mode(cx); + cx.observe_new(|_multi_workspace: &mut MultiWorkspace, window, cx| { let Some(window) = window else { return; @@ -1852,6 +1854,25 @@ fn notify_settings_errors(result: settings::SettingsParseResult, is_user: bool, }; } +#[derive(Copy, Clone, Debug, settings::RegisterSetting)] +struct CursorHideModeSetting(gpui::CursorHideMode); + +impl Settings for CursorHideModeSetting { + fn from_settings(content: &settings::SettingsContent) -> Self { + Self(match content.hide_mouse.unwrap_or_default() { + settings::HideMouseMode::Never => gpui::CursorHideMode::Never, + settings::HideMouseMode::OnTyping => gpui::CursorHideMode::OnTyping, + settings::HideMouseMode::OnTypingAndAction => gpui::CursorHideMode::OnTypingAndAction, + }) + } +} + +fn init_cursor_hide_mode(cx: &mut App) { + let apply = |cx: &mut App| cx.set_cursor_hide_mode(CursorHideModeSetting::get_global(cx).0); + apply(cx); + cx.observe_global::(apply).detach(); +} + pub fn watch_settings_files(fs: Arc, cx: &mut App) { MigrationNotification::set_global(cx.new(|_| MigrationNotification), cx); diff --git a/docs/src/reference/all-settings.md b/docs/src/reference/all-settings.md index eb4ccd1259..bc20f1cf57 100644 --- a/docs/src/reference/all-settings.md +++ b/docs/src/reference/all-settings.md @@ -801,9 +801,8 @@ List of `string` values ## Hide Mouse -- Description: Determines when the mouse cursor should be hidden in an editor or input box. -- Setting: `hide_mouse` -- Default: `on_typing_and_movement` +- Description: Determines when the mouse cursor should be hidden in response to keyboard input. +- Default: `on_typing_and_action` **Options** @@ -823,11 +822,11 @@ List of `string` values } ``` -3. Hide on both typing and cursor movement: +3. Hide on typing and on key bindings that resolve to an action: ```json [settings] { - "hide_mouse": "on_typing_and_movement" + "hide_mouse": "on_typing_and_action" } ``` diff --git a/docs/src/visual-customization.md b/docs/src/visual-customization.md index 6140475eb7..7c5b9f000e 100644 --- a/docs/src/visual-customization.md +++ b/docs/src/visual-customization.md @@ -183,8 +183,8 @@ TBD: Centered layout related settings // Highlight the current line in the editor: none, gutter, line, all "current_line_highlight": "all", - // When does the mouse cursor hide: never, on_typing, on_typing_and_movement - "hide_mouse": "on_typing_and_movement", + // When does the mouse cursor hide: never, on_typing, on_typing_and_action + "hide_mouse": "on_typing_and_action", // Whether to highlight all occurrences of the selected text in an editor. "selection_highlight": true,