diff --git a/assets/settings/default.json b/assets/settings/default.json index 464587e6290b2f..9148800af8874d 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 8c007c3440ceb2..c30fa9e95f0849 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)); @@ -1344,9 +1339,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, @@ -2614,9 +2607,6 @@ impl Editor { blink_manager.disable(cx); } }); - if active { - editor.show_mouse_cursor(cx); - } }), ] }) @@ -2648,11 +2638,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, @@ -2994,31 +2980,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, @@ -4820,8 +4781,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)); @@ -5323,7 +5282,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 @@ -5562,8 +5520,6 @@ impl Editor { return; } - self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx); - let buffer = self.buffer.read(cx); let snapshot = buffer.snapshot(cx); @@ -5633,8 +5589,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; @@ -6689,7 +6643,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) } @@ -6702,7 +6655,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) } @@ -6715,7 +6667,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) } @@ -6725,7 +6676,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) } @@ -7212,7 +7162,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)? { @@ -11276,7 +11225,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); @@ -11329,7 +11277,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| { @@ -11355,7 +11302,6 @@ impl Editor { return; } - self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx); if self.move_to_prev_snippet_tabstop(window, cx) { return; } @@ -11374,7 +11320,6 @@ impl Editor { } if self.move_to_next_snippet_tabstop(window, cx) { - self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx); return; } cx.propagate(); @@ -11392,7 +11337,6 @@ impl Editor { } if self.move_to_prev_snippet_tabstop(window, cx) { - self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx); return; } cx.propagate(); @@ -11405,13 +11349,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); @@ -11543,7 +11485,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; @@ -11651,7 +11592,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(); @@ -11730,7 +11670,6 @@ impl Editor { return; } - self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx); let selections = self .selections .all::(&self.display_snapshot(cx)) @@ -11752,7 +11691,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); @@ -11966,7 +11904,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); } @@ -12048,7 +11985,6 @@ impl Editor { if self.read_only(cx) { return; } - self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx); let snapshot = self.buffer.read(cx).snapshot(cx); @@ -12151,7 +12087,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 @@ -12173,7 +12108,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)) @@ -12202,7 +12136,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(); @@ -12605,7 +12538,6 @@ impl Editor { if self.read_only(cx) { return; } - self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx); let display_snapshot = self.display_snapshot(cx); @@ -12846,7 +12778,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); @@ -13000,7 +12931,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); @@ -13422,7 +13352,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(); @@ -13522,7 +13451,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(); @@ -13670,7 +13598,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; @@ -13784,7 +13711,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; @@ -13881,7 +13807,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| { @@ -13946,7 +13871,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; @@ -14409,7 +14333,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); } @@ -14418,7 +14341,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() { @@ -14439,7 +14361,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()) @@ -14787,7 +14708,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), @@ -14811,8 +14731,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() @@ -14841,8 +14759,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() @@ -14876,7 +14792,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() { @@ -14890,14 +14805,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() { @@ -14911,7 +14824,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) @@ -14929,8 +14841,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(); @@ -14972,8 +14882,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| { @@ -15009,8 +14917,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| { @@ -15037,7 +14943,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| { @@ -15052,7 +14957,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| { @@ -15071,8 +14975,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| { @@ -15111,8 +15013,6 @@ impl Editor { return; }; - self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx); - let effects = if action.center_cursor { SelectionEffects::scroll(Autoscroll::center()) } else { @@ -15140,7 +15040,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| { @@ -15157,8 +15056,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(); @@ -15195,8 +15092,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| { @@ -15235,8 +15130,6 @@ impl Editor { return; }; - self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx); - let effects = if action.center_cursor { SelectionEffects::scroll(Autoscroll::center()) } else { @@ -15263,7 +15156,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| { @@ -15354,7 +15246,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, _| { ( @@ -15371,7 +15262,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 +15278,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, _| { ( @@ -15405,7 +15294,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, _| { ( @@ -15425,7 +15313,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| { @@ -15459,7 +15346,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| { @@ -15490,7 +15376,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) @@ -15504,7 +15389,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) @@ -15518,7 +15402,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) @@ -15532,7 +15415,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) @@ -15549,7 +15431,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| { @@ -15582,7 +15463,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| { @@ -15613,7 +15493,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, _| { ( @@ -15636,7 +15515,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, _| { ( @@ -15661,7 +15539,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| { @@ -15687,7 +15564,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, _| { ( @@ -15704,7 +15580,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, _| { ( @@ -15724,7 +15599,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 { @@ -15746,7 +15620,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 { @@ -15764,7 +15637,6 @@ impl Editor { }); }); } - this.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx); let item = this.cut_common(false, window, cx); cx.write_to_clipboard(item); }); @@ -15780,7 +15652,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( @@ -15801,7 +15672,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( @@ -15822,7 +15692,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, _| { ( @@ -15843,7 +15712,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, _| { ( @@ -15864,7 +15732,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( @@ -15914,7 +15781,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( @@ -15939,7 +15805,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( @@ -15964,7 +15829,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, _| { ( @@ -15985,7 +15849,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, _| { ( @@ -16006,7 +15869,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, _| { ( @@ -16027,7 +15889,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, _| { ( @@ -16048,7 +15909,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]); }); @@ -16062,7 +15922,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]); }); @@ -16073,7 +15932,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]) @@ -16162,7 +16020,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 @@ -16174,14 +16031,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(); @@ -16280,8 +16135,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); @@ -16703,8 +16556,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)?; @@ -16770,7 +16621,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, @@ -16787,7 +16637,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); @@ -16994,7 +16843,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 @@ -17185,7 +17033,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 @@ -17488,8 +17335,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 @@ -17557,8 +17402,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); @@ -17674,8 +17517,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() { @@ -17718,7 +17559,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 @@ -17800,8 +17640,6 @@ impl Editor { return; } - self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx); - let buffer = self.buffer.read(cx).snapshot(cx); let mut selected_sibling = false; @@ -17860,8 +17698,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; @@ -18000,8 +17836,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); @@ -18065,8 +17899,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); @@ -18098,7 +17930,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) = @@ -18155,7 +17986,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| { @@ -18181,7 +18011,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| { @@ -18365,7 +18194,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) } @@ -18378,7 +18206,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) } @@ -18476,7 +18303,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( @@ -18545,7 +18371,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( @@ -18639,7 +18464,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 @@ -20149,7 +19973,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(), @@ -20174,7 +19997,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(), @@ -20296,7 +20118,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, @@ -21880,7 +21701,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 { @@ -21913,7 +21733,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 @@ -24253,7 +24072,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 @@ -25332,7 +25150,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 { @@ -25990,8 +25807,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 1999d0e537e525..0c39196062b08a 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 4b719b1474d38d..cb122e4d18f270 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 e00fd20ed5abdc..1877d8704f6a7b 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 f19c780caa671d..a9255898daa526 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 6cd8e9cd8d4d9d..8799d9195f0090 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; @@ -1814,9 +1822,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 a59b21f038a01b..cc8c5749bd4696 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 8b9bec7edd170b..3719a3ee24f9eb 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 60ffe1411b4444..d56c5db9b66387 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(); @@ -4470,6 +4467,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 { @@ -4735,6 +4740,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 e3c947bcb9d333..343600cd862f45 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 aa1e7974043419..3e90688d1bd98b 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 3b8ca0f45025b7..14c4cc2e1c2205 100644 --- a/crates/gpui_linux/src/linux/wayland/client.rs +++ b/crates/gpui_linux/src/linux/wayland/client.rs @@ -253,6 +253,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>, @@ -407,6 +408,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, + ); } } @@ -662,6 +722,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, @@ -782,35 +843,44 @@ 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) { let mut state = self.0.borrow_mut(); if let (Some(activation), Some(window)) = ( @@ -1426,6 +1496,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 { @@ -1726,14 +1797,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(); @@ -1759,6 +1825,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); @@ -1774,6 +1841,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 07d1596a2f7b68..565c493e37e31e 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(); @@ -1662,11 +1672,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( @@ -1681,6 +1697,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( @@ -1981,42 +2005,34 @@ 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); } } - } - 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)); + Err(err) => { + errors.push_str(&err.to_string()); + errors.push('\n'); + } } } + 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)) + } }; let cursor = match result { @@ -2047,6 +2063,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 fefe957bb25bad..77cf85a8d1d340 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 6931102acba0d9..cad3f85b59f9ea 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, @@ -1846,26 +1824,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], @@ -1901,11 +1860,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]; } @@ -2136,6 +2092,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 { @@ -2361,6 +2331,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 133c9fdebec06b..fdbc64f623a474 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 4d78b71aa05b74..290ef33e5f1faa 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 23b0c078b8ae62..370582e83b5a5f 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 4c2898256212a7..7b76c4d12a8980 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 fe5093dede385c..15dd4c69984d14 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 f655c1989e2c69..2fd7c3c6461dd5 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 8fa8907a16cc48..394b26f57ab3f0 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 00000000000000..a2a705e03f42e7 --- /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 ac7e4e337ed946..e1b15d98ea528d 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 64f00bc5d6501b..db7f847b23417f 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 18a7bd5fd497ce..c1e3f755d1ab41 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 @@ -833,34 +829,6 @@ pub enum GoToDefinitionScrollStrategy { Top, } -/// 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 251718d146506f..02fe409c6325af 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 99dd77b84aebb4..6d9a252d3c418f 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 eebad4d4382b70..d61b0547aef5ce 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 a76846fd33dd89..6c0c3d0201b490 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 6d1a9c176f1193..7cc8d6dfbc66fb 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -378,6 +378,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; @@ -1846,6 +1848,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 c6c33b8b5edd0d..4bfabc88f31bc3 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 6140475eb71294..7c5b9f000e66ff 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,