diff --git a/kitty/child-monitor.c b/kitty/child-monitor.c index 0417614ce..64d5f83be 100644 --- a/kitty/child-monitor.c +++ b/kitty/child-monitor.c @@ -717,9 +717,9 @@ prepare_to_render_os_window(OSWindow *os_window, monotonic_t now, unsigned int * } if (send_cell_data_to_gpu(TD.vao_idx, TD.xstart, TD.ystart, TD.dx, TD.dy, TD.screen, os_window)) needs_render = true; } - if (OPT(mouse_hide_wait) > 0 && !is_mouse_hidden(os_window)) { - if (now - os_window->last_mouse_activity_at >= OPT(mouse_hide_wait)) hide_mouse(os_window); - else set_maximum_wait(OPT(mouse_hide_wait) - now + os_window->last_mouse_activity_at); + if (OPT(mouse_hide.hide_wait) > 0 && !is_mouse_hidden(os_window)) { + if (now - os_window->last_mouse_activity_at >= OPT(mouse_hide.hide_wait)) hide_mouse(os_window); + else set_maximum_wait(OPT(mouse_hide.hide_wait) - now + os_window->last_mouse_activity_at); } Tab *tab = os_window->tabs + os_window->active_tab; *active_window_bg = OPT(background); diff --git a/kitty/glfw.c b/kitty/glfw.c index 05426d581..8b3377056 100644 --- a/kitty/glfw.c +++ b/kitty/glfw.c @@ -217,6 +217,30 @@ show_mouse_cursor(GLFWwindow *w) { glfwSetInputMode(w, GLFW_CURSOR, GLFW_CURSOR_NORMAL); } +void +cursor_active_callback(GLFWwindow *w, monotonic_t now) { + if (OPT(mouse_hide.unhide_wait) == 0) { + show_mouse_cursor(w); + } else if (OPT(mouse_hide.unhide_wait) > 0) { + if (global_state.callback_os_window->mouse_activate_deadline == -1) { + global_state.callback_os_window->mouse_activate_deadline = OPT(mouse_hide.unhide_wait) + now; + global_state.callback_os_window->mouse_show_threshold = (int) (OPT(mouse_hide.unhide_wait) / 1e9 * OPT(mouse_hide.unhide_threshold)); + } else if (now < global_state.callback_os_window->mouse_activate_deadline) { + if (global_state.callback_os_window->mouse_show_threshold > 0) { + global_state.callback_os_window->mouse_show_threshold--; + } + } else { + if ( + now < global_state.callback_os_window->mouse_activate_deadline + s_double_to_monotonic_t(0.5) && + global_state.callback_os_window->mouse_show_threshold == 0 + ) { + show_mouse_cursor(w); + } + global_state.callback_os_window->mouse_activate_deadline = -1; + } + } +} + void blank_os_window(OSWindow *osw) { color_type color = OPT(background); @@ -451,8 +475,8 @@ cursor_enter_callback(GLFWwindow *w, int entered) { double x, y; glfwGetCursorPos(w, &x, &y); debug_input("Mouse cursor entered window: %llu at %fx%f\n", global_state.callback_os_window->id, x, y); - show_mouse_cursor(w); monotonic_t now = monotonic(); + cursor_active_callback(w, now); global_state.callback_os_window->last_mouse_activity_at = now; global_state.callback_os_window->mouse_x = x * global_state.callback_os_window->viewport_x_ratio; global_state.callback_os_window->mouse_y = y * global_state.callback_os_window->viewport_y_ratio; @@ -465,9 +489,9 @@ cursor_enter_callback(GLFWwindow *w, int entered) { static void mouse_button_callback(GLFWwindow *w, int button, int action, int mods) { if (!set_callback_window(w)) return; - show_mouse_cursor(w); - mods_at_last_key_or_button_event = mods; monotonic_t now = monotonic(); + cursor_active_callback(w, now); + mods_at_last_key_or_button_event = mods; OSWindow *window = global_state.callback_os_window; window->last_mouse_activity_at = now; if (button >= 0 && (unsigned int)button < arraysz(global_state.callback_os_window->mouse_button_pressed)) { @@ -489,8 +513,8 @@ mouse_button_callback(GLFWwindow *w, int button, int action, int mods) { static void cursor_pos_callback(GLFWwindow *w, double x, double y) { if (!set_callback_window(w)) return; - show_mouse_cursor(w); monotonic_t now = monotonic(); + cursor_active_callback(w, now); global_state.callback_os_window->last_mouse_activity_at = now; global_state.callback_os_window->cursor_blink_zero_time = now; global_state.callback_os_window->mouse_x = x * global_state.callback_os_window->viewport_x_ratio; @@ -504,8 +528,10 @@ cursor_pos_callback(GLFWwindow *w, double x, double y) { static void scroll_callback(GLFWwindow *w, double xoffset, double yoffset, int flags, int mods) { if (!set_callback_window(w)) return; - show_mouse_cursor(w); monotonic_t now = monotonic(); + if (OPT(mouse_hide.scroll_unhide)) { + cursor_active_callback(w, now); + } global_state.callback_os_window->last_mouse_activity_at = now; if (is_window_ready_for_callbacks()) scroll_event(xoffset, yoffset, flags, mods); request_tick_callback(); @@ -519,13 +545,13 @@ window_focus_callback(GLFWwindow *w, int focused) { if (!set_callback_window(w)) return; debug_input("\x1b[35mon_focus_change\x1b[m: window id: 0x%llu focused: %d\n", global_state.callback_os_window->id, focused); global_state.callback_os_window->is_focused = focused ? true : false; + monotonic_t now = monotonic(); if (focused) { - show_mouse_cursor(w); + cursor_active_callback(w, now); focus_in_event(); global_state.callback_os_window->last_focused_counter = ++focus_counter; global_state.check_for_active_animated_images = true; } - monotonic_t now = monotonic(); global_state.callback_os_window->last_mouse_activity_at = now; global_state.callback_os_window->cursor_blink_zero_time = now; if (is_window_ready_for_callbacks()) { @@ -1371,6 +1397,8 @@ create_os_window(PyObject UNUSED *self, PyObject *args, PyObject *kw) { w->is_focused = true; w->cursor_blink_zero_time = now; w->last_mouse_activity_at = now; + w->mouse_activate_deadline = -1; + w->mouse_show_threshold = 0; w->is_semi_transparent = is_semi_transparent; if (want_semi_transparent && !w->is_semi_transparent) { static bool warned = false; diff --git a/kitty/keys.c b/kitty/keys.c index 2481ba14c..68175df8f 100644 --- a/kitty/keys.c +++ b/kitty/keys.c @@ -235,7 +235,7 @@ on_key_input(const GLFWkeyevent *ev) { } if (!w) { debug("no active window, ignoring\n"); return; } send_pending_click_to_window(w, -1); - if (OPT(mouse_hide_wait) < 0 && !is_no_action_key(key, native_key)) hide_mouse(global_state.callback_os_window); + if (OPT(mouse_hide.hide_wait) < 0 && !is_no_action_key(key, native_key)) hide_mouse(global_state.callback_os_window); Screen *screen = w->render_data.screen; id_type active_window_id = w->id; diff --git a/kitty/options/definition.py b/kitty/options/definition.py index 660984dcd..10380a9e5 100644 --- a/kitty/options/definition.py +++ b/kitty/options/definition.py @@ -511,13 +511,51 @@ agr('mouse', 'Mouse') opt('mouse_hide_wait', '3.0', macos_default='0.0', - option_type='float', ctype='time', + option_type='mouse_hide_wait', ctype='!mouse_hide_wait', long_text=''' Hide mouse cursor after the specified number of seconds of the mouse not being used. Set to zero to disable mouse cursor hiding. Set to a negative value to hide the mouse cursor immediately when typing text. Disabled by default on macOS as getting it to work robustly with the ever-changing sea of bugs that is Cocoa is too much effort. + +By default, once the cursor is hidden, it is immediately unhidden on any +further mouse events. + +Two formats are supported: + - "" + - " " + +To change the unhide behavior, the optional parameters , +, and may be set. + +: +Waits for the specified number of seconds after mouse events before unhiding the +mouse cursor. Set to zero to unhide mouse cursor immediately on mouse activity. +This is useful to prevent the mouse cursor from unhiding on accidental swipes on +the trackpad. + +: +Sets the threshold of mouse activity required to unhide the mouse cursor, when +the option is non-zero. When is zero, this has no +effect. + +For example, if is 40 and is 2.5, when kitty +detects a mouse event, it records the number of mouse events in the next 2.5 +seconds, and checks if that exceeds 40 * 2.5 = 100. If it does, then the mouse +cursor is unhidden, otherwise nothing happens. + +: +Controls what mouse events may unhide the mouse cursor. If enabled, both scroll +and movement events may unhide the cursor. If disabled, only mouse movements can +unhide the cursor. + + +Examples of valid values: + - 0.0 + - 1.0 + - -1.0 + - 0.1 3.0 40 yes ''' ) diff --git a/kitty/options/parse.py b/kitty/options/parse.py index 7b046bc36..bc11eddc9 100644 --- a/kitty/options/parse.py +++ b/kitty/options/parse.py @@ -14,7 +14,7 @@ from kitty.options.utils import ( cursor_trail_decay, deprecated_adjust_line_height, deprecated_hide_window_decorations_aliases, deprecated_macos_show_window_title_in_menubar_alias, deprecated_send_text, disable_ligatures, edge_width, env, filter_notification, font_features, hide_window_decorations, macos_option_as_alt, - macos_titlebar_color, menu_map, modify_font, narrow_symbols, notify_on_cmd_finish, + macos_titlebar_color, menu_map, modify_font, mouse_hide_wait, narrow_symbols, notify_on_cmd_finish, optional_edge_width, parse_font_spec, parse_map, parse_mouse_map, paste_actions, pointer_shape_when_dragging, remote_control_password, resize_debounce_time, scrollback_lines, scrollback_pager_history_size, shell_integration, store_multiple, symbol_map, tab_activity_symbol, @@ -1137,7 +1137,7 @@ class Parser: ans["modify_font"][k] = v def mouse_hide_wait(self, val: str, ans: dict[str, typing.Any]) -> None: - ans['mouse_hide_wait'] = float(val) + ans['mouse_hide_wait'] = mouse_hide_wait(val) def narrow_symbols(self, val: str, ans: dict[str, typing.Any]) -> None: for k, v in narrow_symbols(val): diff --git a/kitty/options/to-c-generated.h b/kitty/options/to-c-generated.h index b1ac92ef1..e2164ae2c 100644 --- a/kitty/options/to-c-generated.h +++ b/kitty/options/to-c-generated.h @@ -319,7 +319,7 @@ convert_from_opts_touch_scroll_multiplier(PyObject *py_opts, Options *opts) { static void convert_from_python_mouse_hide_wait(PyObject *val, Options *opts) { - opts->mouse_hide_wait = parse_s_double_to_monotonic_t(val); + mouse_hide_wait(val, opts); } static void diff --git a/kitty/options/to-c.h b/kitty/options/to-c.h index bc959f19f..ecfa0c552 100644 --- a/kitty/options/to-c.h +++ b/kitty/options/to-c.h @@ -197,6 +197,18 @@ visual_bell_duration(PyObject *src, Options *opts) { #undef parse_animation +static inline void +mouse_hide_wait(PyObject *val, Options *opts) { + if (!PyTuple_Check(val) || PyTuple_GET_SIZE(val) != 4) { + PyErr_SetString(PyExc_TypeError, "mouse_hide_wait is not a 4-item tuple"); + return; + } + opts->mouse_hide.hide_wait = parse_s_double_to_monotonic_t(PyTuple_GET_ITEM(val, 0)); + opts->mouse_hide.unhide_wait = parse_s_double_to_monotonic_t(PyTuple_GET_ITEM(val, 1)); + opts->mouse_hide.unhide_threshold = PyLong_AsLong(PyTuple_GET_ITEM(val, 2)); + opts->mouse_hide.scroll_unhide = PyObject_IsTrue(PyTuple_GET_ITEM(val, 3)); +} + static inline void cursor_trail_decay(PyObject *src, Options *opts) { opts->cursor_trail_decay_fast = PyFloat_AsFloat(PyTuple_GET_ITEM(src, 0)); diff --git a/kitty/options/types.py b/kitty/options/types.py index 0949ccc83..8e6a75446 100644 --- a/kitty/options/types.py +++ b/kitty/options/types.py @@ -11,7 +11,7 @@ import kitty.fast_data_types from kitty.fonts import FontSpec import kitty.fonts from kitty.options.utils import ( - AliasMap, KeyDefinition, KeyboardModeMap, MouseMap, MouseMapping, NotifyOnCmdFinish, + AliasMap, KeyDefinition, KeyboardModeMap, MouseHideWait, MouseMap, MouseMapping, NotifyOnCmdFinish, TabBarMarginHeight ) import kitty.options.utils @@ -568,7 +568,7 @@ class Options: mark2_foreground: Color = Color(0, 0, 0) mark3_background: Color = Color(242, 116, 188) mark3_foreground: Color = Color(0, 0, 0) - mouse_hide_wait: float = 0.0 if is_macos else 3.0 + mouse_hide_wait: MouseHideWait = MouseHideWait(hide_wait=0.0, show_wait=0.0, show_threshold=40, scroll_show=True) if is_macos else MouseHideWait(hide_wait=3.0, show_wait=0.0, show_threshold=40, scroll_show=True) notify_on_cmd_finish: NotifyOnCmdFinish = NotifyOnCmdFinish(when='never', duration=5.0, action='notify', cmdline=(), clear_on=('focus', 'next')) open_url_with: list[str] = ['default'] paste_actions: frozenset[str] = frozenset({'confirm', 'quote-urls-at-prompt'}) diff --git a/kitty/options/utils.py b/kitty/options/utils.py index 5dab6906b..a8fccd252 100644 --- a/kitty/options/utils.py +++ b/kitty/options/utils.py @@ -1590,6 +1590,24 @@ def cursor_blink_interval(spec: str) -> tuple[float, EasingFunction, EasingFunct return parse_animation(spec) +class MouseHideWait(NamedTuple): + hide_wait: float + show_wait: float + show_threshold: int + scroll_show: bool + + +def mouse_hide_wait(x: str) -> MouseHideWait: + parts = x.split(maxsplit=3) + if len(parts) != 1 and len(parts) != 4: + log_error(f'Invalid mouse_hide_wait: {x}, ignoring') + return MouseHideWait(3.0, 0.0, 40, True) + if len(parts) == 1: + return MouseHideWait(float(parts[0]), 0.0, 40, True) + else: + return MouseHideWait(float(parts[0]), float(parts[1]), int(parts[2]), to_bool(parts[3])) + + def visual_bell_duration(spec: str) -> tuple[float, EasingFunction, EasingFunction]: return parse_animation(spec, interval=0.) diff --git a/kitty/state.h b/kitty/state.h index 2b11c6777..b192aed2a 100644 --- a/kitty/state.h +++ b/kitty/state.h @@ -39,7 +39,12 @@ struct MenuItem { }; typedef struct { - monotonic_t visual_bell_duration, cursor_blink_interval, cursor_stop_blinking_after, mouse_hide_wait, click_interval; + monotonic_t visual_bell_duration, cursor_blink_interval, cursor_stop_blinking_after, click_interval; + struct { + monotonic_t hide_wait, unhide_wait; + int unhide_threshold; + bool scroll_unhide; + } mouse_hide; double wheel_scroll_multiplier, touch_scroll_multiplier; int wheel_scroll_min_lines; bool enable_audio_bell; @@ -286,7 +291,8 @@ typedef struct { } tab_bar_edge_color; bool tab_bar_data_updated; bool is_focused; - monotonic_t cursor_blink_zero_time, last_mouse_activity_at; + monotonic_t cursor_blink_zero_time, last_mouse_activity_at, mouse_activate_deadline; + int mouse_show_threshold; bool has_received_cursor_pos_event; double mouse_x, mouse_y; bool mouse_button_pressed[32]; diff --git a/kitty_tests/options.py b/kitty_tests/options.py index 1eba937d8..b717d9ac0 100644 --- a/kitty_tests/options.py +++ b/kitty_tests/options.py @@ -56,7 +56,10 @@ def conf_parsing(self): opts = p('font_size 11.37', 'clear_all_shortcuts y', 'color23 red') self.ae(opts.font_size, 11.37) - self.ae(opts.mouse_hide_wait, 0 if is_macos else 3) + self.ae(opts.mouse_hide_wait[0], 0 if is_macos else 3) + self.ae(opts.mouse_hide_wait[1], 0) + self.ae(opts.mouse_hide_wait[2], 40) + self.ae(opts.mouse_hide_wait[3], True) self.ae(opts.color23, Color(255, 0, 0)) self.assertFalse(opts.keyboard_modes[''].keymap) opts = p('clear_all_shortcuts y', 'map f1 next_window')