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