From 4313531432740b6d66779ce7109e9d0be6b85ad3 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 7 Jul 2018 10:39:30 +0530 Subject: [PATCH] macOS: Use a custom mouse cursor that shows up well on both light and dark backgrounds Fixes #359. Also, update GLFW from upstream for retina cursor setting support. --- docs/changelog.rst | 3 +++ glfw/cocoa_window.m | 46 ++++++++++++++++++++-------------------- glfw/glfw3.h | 5 +++-- glfw/input.c | 5 +++-- glfw/internal.h | 2 +- glfw/null_window.c | 2 +- glfw/win32_window.c | 2 +- glfw/wl_window.c | 2 +- glfw/x11_window.c | 2 +- kitty/constants.py | 1 + kitty/glfw.c | 41 +++++++++++++++++++++++++++++++++-- kitty/main.py | 26 +++++++++++++++++++---- logo/beam-cursor.png | Bin 0 -> 217 bytes logo/beam-cursor@2x.png | Bin 0 -> 1503 bytes setup.py | 2 ++ 15 files changed, 101 insertions(+), 38 deletions(-) create mode 100644 logo/beam-cursor.png create mode 100644 logo/beam-cursor@2x.png diff --git a/docs/changelog.rst b/docs/changelog.rst index ef1a00f31..e467dbc41 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -15,6 +15,9 @@ Changelog - macOS: Add an option :opt:`macos_window_resizable` to control if kitty top-level windows are resizable using the mouse or not (:iss:`698`) +- macOS: Use a custom mouse cursor that shows up well on both light and dark backgrounds + (:iss:`359`) + - Fix triple-click to select line not working when the entire line is filled (:iss:`703`) diff --git a/glfw/cocoa_window.m b/glfw/cocoa_window.m index c521c1585..860b1677d 100644 --- a/glfw/cocoa_window.m +++ b/glfw/cocoa_window.m @@ -1886,44 +1886,44 @@ int _glfwPlatformGetKeyScancode(int key) int _glfwPlatformCreateCursor(_GLFWcursor* cursor, const GLFWimage* image, - int xhot, int yhot) + int xhot, int yhot, int count) { NSImage* native; NSBitmapImageRep* rep; if (!initializeAppKit()) return GLFW_FALSE; - - rep = [[NSBitmapImageRep alloc] - initWithBitmapDataPlanes:NULL - pixelsWide:image->width - pixelsHigh:image->height - bitsPerSample:8 - samplesPerPixel:4 - hasAlpha:YES - isPlanar:NO - colorSpaceName:NSCalibratedRGBColorSpace - bitmapFormat:NSAlphaNonpremultipliedBitmapFormat - bytesPerRow:image->width * 4 - bitsPerPixel:32]; - - if (rep == nil) + native = [[NSImage alloc] initWithSize:NSMakeSize(image->width, image->height)]; + if (native == nil) return GLFW_FALSE; - memcpy([rep bitmapData], image->pixels, image->width * image->height * 4); + for (int i = 0; i < count; i++) { + const GLFWimage *src = image + i; + rep = [[NSBitmapImageRep alloc] + initWithBitmapDataPlanes:NULL + pixelsWide:src->width + pixelsHigh:src->height + bitsPerSample:8 + samplesPerPixel:4 + hasAlpha:YES + isPlanar:NO + colorSpaceName:NSCalibratedRGBColorSpace + bitmapFormat:NSAlphaNonpremultipliedBitmapFormat + bytesPerRow:src->width * 4 + bitsPerPixel:32]; + if (rep == nil) + return GLFW_FALSE; - native = [[NSImage alloc] initWithSize:NSMakeSize(image->width, image->height)]; - [native addRepresentation:rep]; + memcpy([rep bitmapData], src->pixels, src->width * src->height * 4); + [native addRepresentation:rep]; + [rep release]; + } cursor->ns.object = [[NSCursor alloc] initWithImage:native hotSpot:NSMakePoint(xhot, yhot)]; - [native release]; - [rep release]; - if (cursor->ns.object == nil) return GLFW_FALSE; - return GLFW_TRUE; } diff --git a/glfw/glfw3.h b/glfw/glfw3.h index edf0745cf..eba72a141 100644 --- a/glfw/glfw3.h +++ b/glfw/glfw3.h @@ -4123,6 +4123,7 @@ GLFWAPI void glfwSetCursorPos(GLFWwindow* window, double xpos, double ypos); * @param[in] image The desired cursor image. * @param[in] xhot The desired x-coordinate, in pixels, of the cursor hotspot. * @param[in] yhot The desired y-coordinate, in pixels, of the cursor hotspot. + * @param[in] count The number of images. Used on Cocoa for retina cursors. The first image should be the 1:1 scale image. * @return The handle of the created cursor, or `NULL` if an * [error](@ref error_handling) occurred. * @@ -4138,11 +4139,11 @@ GLFWAPI void glfwSetCursorPos(GLFWwindow* window, double xpos, double ypos); * @sa @ref glfwDestroyCursor * @sa @ref glfwCreateStandardCursor * - * @since Added in version 3.1. + * @since Added in version 3.1. Changed in 4.0 to add the count parameter. * * @ingroup input */ -GLFWAPI GLFWcursor* glfwCreateCursor(const GLFWimage* image, int xhot, int yhot); +GLFWAPI GLFWcursor* glfwCreateCursor(const GLFWimage* image, int xhot, int yhot, int count); /*! @brief Creates a cursor with a standard shape. * diff --git a/glfw/input.c b/glfw/input.c index d2704a07a..9161c0a1d 100644 --- a/glfw/input.c +++ b/glfw/input.c @@ -799,11 +799,12 @@ GLFWAPI void glfwSetCursorPos(GLFWwindow* handle, double xpos, double ypos) } } -GLFWAPI GLFWcursor* glfwCreateCursor(const GLFWimage* image, int xhot, int yhot) +GLFWAPI GLFWcursor* glfwCreateCursor(const GLFWimage* image, int xhot, int yhot, int count) { _GLFWcursor* cursor; assert(image != NULL); + assert(count > 0); _GLFW_REQUIRE_INIT_OR_RETURN(NULL); @@ -811,7 +812,7 @@ GLFWAPI GLFWcursor* glfwCreateCursor(const GLFWimage* image, int xhot, int yhot) cursor->next = _glfw.cursorListHead; _glfw.cursorListHead = cursor; - if (!_glfwPlatformCreateCursor(cursor, image, xhot, yhot)) + if (!_glfwPlatformCreateCursor(cursor, image, xhot, yhot, count)) { glfwDestroyCursor((GLFWcursor*) cursor); return NULL; diff --git a/glfw/internal.h b/glfw/internal.h index 782c0f2cf..f6a792d6a 100644 --- a/glfw/internal.h +++ b/glfw/internal.h @@ -600,7 +600,7 @@ void _glfwPlatformGetCursorPos(_GLFWwindow* window, double* xpos, double* ypos); void _glfwPlatformSetCursorPos(_GLFWwindow* window, double xpos, double ypos); void _glfwPlatformSetCursorMode(_GLFWwindow* window, int mode); int _glfwPlatformCreateCursor(_GLFWcursor* cursor, - const GLFWimage* image, int xhot, int yhot); + const GLFWimage* image, int xhot, int yhot, int count); int _glfwPlatformCreateStandardCursor(_GLFWcursor* cursor, int shape); void _glfwPlatformDestroyCursor(_GLFWcursor* cursor); void _glfwPlatformSetCursor(_GLFWwindow* window, _GLFWcursor* cursor); diff --git a/glfw/null_window.c b/glfw/null_window.c index 0e380c58d..82f414934 100644 --- a/glfw/null_window.c +++ b/glfw/null_window.c @@ -267,7 +267,7 @@ void _glfwPlatformSetCursorMode(_GLFWwindow* window, int mode) int _glfwPlatformCreateCursor(_GLFWcursor* cursor, const GLFWimage* image, - int xhot, int yhot) + int xhot, int yhot, int count) { return GLFW_TRUE; } diff --git a/glfw/win32_window.c b/glfw/win32_window.c index 3480d1149..a4bda1ebc 100644 --- a/glfw/win32_window.c +++ b/glfw/win32_window.c @@ -1839,7 +1839,7 @@ int _glfwPlatformGetKeyScancode(int key) int _glfwPlatformCreateCursor(_GLFWcursor* cursor, const GLFWimage* image, - int xhot, int yhot) + int xhot, int yhot, int count) { cursor->win32.handle = (HCURSOR) createIcon(image, xhot, yhot, GLFW_FALSE); if (!cursor->win32.handle) diff --git a/glfw/wl_window.c b/glfw/wl_window.c index 3ea921d92..10c9bf5c1 100644 --- a/glfw/wl_window.c +++ b/glfw/wl_window.c @@ -1257,7 +1257,7 @@ int _glfwPlatformGetKeyScancode(int key) int _glfwPlatformCreateCursor(_GLFWcursor* cursor, const GLFWimage* image, - int xhot, int yhot) + int xhot, int yhot, int count) { cursor->wl.buffer = createShmBuffer(image); cursor->wl.width = image->width; diff --git a/glfw/x11_window.c b/glfw/x11_window.c index 1eecd1d2e..ab3fcb4d7 100644 --- a/glfw/x11_window.c +++ b/glfw/x11_window.c @@ -2684,7 +2684,7 @@ int _glfwPlatformGetKeyScancode(int key) int _glfwPlatformCreateCursor(_GLFWcursor* cursor, const GLFWimage* image, - int xhot, int yhot) + int xhot, int yhot, int count) { cursor->x11.handle = _glfwCreateCursorX11(image, xhot, yhot); if (!cursor->x11.handle) diff --git a/kitty/constants.py b/kitty/constants.py index fd377ec63..63381e46c 100644 --- a/kitty/constants.py +++ b/kitty/constants.py @@ -105,6 +105,7 @@ def wakeup(): base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) terminfo_dir = os.path.join(base_dir, 'terminfo') logo_data_file = os.path.join(base_dir, 'logo', 'kitty.rgba') +beam_cursor_data_file = os.path.join(base_dir, 'logo', 'beam-cursor.png') try: shell_path = pwd.getpwuid(os.geteuid()).pw_shell or '/bin/sh' except KeyError: diff --git a/kitty/glfw.c b/kitty/glfw.c index d43e816d3..a3bf42677 100644 --- a/kitty/glfw.c +++ b/kitty/glfw.c @@ -456,8 +456,10 @@ create_os_window(PyObject UNUSED *self, PyObject *args) { if (OPT(macos_hide_from_tasks)) cocoa_set_hide_from_tasks(); #endif #define CC(dest, shape) {\ - dest##_cursor = glfwCreateStandardCursor(GLFW_##shape##_CURSOR); \ - if (dest##_cursor == NULL) { log_error("Failed to create the %s mouse cursor, using default cursor.", #shape); }} + if (!dest##_cursor) { \ + dest##_cursor = glfwCreateStandardCursor(GLFW_##shape##_CURSOR); \ + if (dest##_cursor == NULL) { log_error("Failed to create the %s mouse cursor, using default cursor.", #shape); } \ +}} CC(standard, IBEAM); CC(click, HAND); CC(arrow, ARROW); #undef CC is_first_window = false; @@ -839,6 +841,39 @@ set_smallest_allowed_resize(PyObject *self UNUSED, PyObject *args) { Py_RETURN_NONE; } +static PyObject* +set_custom_cursor(PyObject *self UNUSED, PyObject *args) { + int shape; + int x=0, y=0; + Py_ssize_t sz; + PyObject *images; + if (!PyArg_ParseTuple(args, "iO!|ii", &shape, &PyTuple_Type, &images, &x, &y)) return NULL; + static GLFWimage gimages[16] = {{0}}; + size_t count = MIN((size_t)PyTuple_GET_SIZE(images), arraysz(gimages)); + for (size_t i = 0; i < count; i++) { + if (!PyArg_ParseTuple(PyTuple_GET_ITEM(images, i), "s#ii", &gimages[i].pixels, &sz, &gimages[i].width, &gimages[i].height)) return NULL; + if (gimages[i].width * gimages[i].height * 4 != sz) { + PyErr_SetString(PyExc_ValueError, "The image data size does not match its width and height"); + return NULL; + } + } +#define CASE(which, dest) {\ + case which: \ + standard_cursor = glfwCreateCursor(gimages, x, y, count); \ + if (standard_cursor == NULL) { PyErr_SetString(PyExc_ValueError, "Failed to create custom cursor"); return NULL; } \ + break; \ +} + switch(shape) { + CASE(GLFW_IBEAM_CURSOR, standard_cursor); + CASE(GLFW_HAND_CURSOR, click_cursor); + CASE(GLFW_ARROW_CURSOR, arrow_cursor); + default: + PyErr_SetString(PyExc_ValueError, "Unknown cursor shape"); + return NULL; + } + Py_RETURN_NONE; +} + #ifdef __APPLE__ void get_cocoa_key_equivalent(int key, int mods, unsigned short *cocoa_key, int *cocoa_mods) { @@ -848,6 +883,7 @@ get_cocoa_key_equivalent(int key, int mods, unsigned short *cocoa_key, int *coco // Boilerplate {{{ static PyMethodDef module_methods[] = { + METHODB(set_custom_cursor, METH_VARARGS), METHODB(set_smallest_allowed_resize, METH_VARARGS), METHODB(create_os_window, METH_VARARGS), METHODB(set_default_window_icon, METH_VARARGS), @@ -891,6 +927,7 @@ init_glfw(PyObject *m) { ADDC(GLFW_PRESS); ADDC(GLFW_REPEAT); ADDC(GLFW_TRUE); ADDC(GLFW_FALSE); + ADDC(GLFW_IBEAM_CURSOR); ADDC(GLFW_HAND_CURSOR); ADDC(GLFW_ARROW_CURSOR); // --- Keys -------------------------------------------------------------------- diff --git a/kitty/main.py b/kitty/main.py index 5fe738ab9..c16998168 100644 --- a/kitty/main.py +++ b/kitty/main.py @@ -12,12 +12,13 @@ from .boss import Boss from .cli import create_opts, parse_args from .config import cached_values_for, initial_window_size_func from .constants import ( - appname, config_dir, glfw_path, is_macos, is_wayland, kitty_exe, - logo_data_file + appname, beam_cursor_data_file, config_dir, glfw_path, is_macos, + is_wayland, kitty_exe, logo_data_file ) from .fast_data_types import ( - GLFW_MOD_SUPER, create_os_window, free_font_data, glfw_init, - glfw_terminate, set_default_window_icon, set_options + GLFW_IBEAM_CURSOR, GLFW_MOD_SUPER, create_os_window, free_font_data, + glfw_init, glfw_terminate, load_png_data, set_custom_cursor, + set_default_window_icon, set_options ) from .fonts.box_drawing import set_scale from .fonts.render import set_font_family @@ -28,6 +29,21 @@ from .utils import ( from .window import load_shader_programs +def set_custom_ibeam_cursor(): + with open(beam_cursor_data_file, 'rb') as f: + data = f.read() + rgba_data, width, height = load_png_data(data) + c2x = os.path.splitext(beam_cursor_data_file) + with open(c2x[0] + '@2x' + c2x[1], 'rb') as f: + data = f.read() + rgba_data2, width2, height2 = load_png_data(data) + images = (rgba_data, width, height), (rgba_data2, width2, height2) + try: + set_custom_cursor(GLFW_IBEAM_CURSOR, images, 4, 8) + except Exception as e: + log_error('Failed to set custom beam cursor with error: {}'.format(e)) + + def talk_to_instance(args): import json import socket @@ -100,6 +116,8 @@ def get_new_os_window_trigger(opts): def _run_app(opts, args): new_os_window_trigger = get_new_os_window_trigger(opts) + if is_macos: + set_custom_ibeam_cursor() with cached_values_for(run_app.cached_values_name) as cached_values: with startup_notification_handler(extra_callback=run_app.first_window_callback) as pre_show_callback: window_id = create_os_window( diff --git a/logo/beam-cursor.png b/logo/beam-cursor.png new file mode 100644 index 0000000000000000000000000000000000000000..849145a6aa2b51dfe11c878b2c2e933f415f4db7 GIT binary patch literal 217 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!60wlNoGJgf6I14-?iy0WWg+Z8+Vb&Z8prAss zN02WALzOB6LqjtI!_WUf`XvKHsR0ASs{{rHs~HRo;`x)}kGcWX#(TOrhE&|zGw~#E zg98uq@n|8|i_*MHmp){E6myW-@TW`BlOtSDRhXuzp4up-*kt9PH7CQsNhQX$fq^6K z^?APTOA9|9aA&TnzJE@!EWdWU(*GqJrZ1YqqcZX9$LeQ{40jl{LX(?S6Ml3^w)^1&PVosU-?Y zsp*+{wo31J?^jaDOtDo8H}y5}EpSfF$n>ZxN)4{^3rViZPPR-@vbR&PsjvbXkegbP zs8ErclUHn2VXFi-*9yo63F|8YQc=cL27o{eau&IJ?Vsd64h!2V$h;Lm|i!#enQ{0O3a}~hhYn6$`egh*y{)XsHAt%iAfsWA! zB@U#-0uutJKoAq2^no0B=1I*1=9nU2))1W0_mzQxiQm)3F{I+wo9X+#nGz+A$JdLL zT8LU$+*tDBfr!uztsN|urWQL|fAFiUaDFh$k5|d#R#Lai!&QQUJ6N2Q7FkGyybzd} z&>WtBzWB$n+l3b&#EL&;j*ox8^K+%$_r2G5SNBc{5_Dm2Zj|VC(^P!IxJ8<;om0NR zBgIC}{Lx1XQI?0Y!3&H}Opa`kUq7WgZ@agy7`NDdMqLN4(z`E9R$R?mdij`-dKQC0 zZc`P<#jLGIGMmkE;9MaflCdX3W%_Bs85#3k6BpfmXd%_x@vKpVuiaVGfA-mB-}qXb6q)|letQ1sy3B>& zF3%Z?85SQ}vv1KQ+fUvzcDR37dyS97{!dzRMc>UF+{K04My>ZXG-)Zidl9$QV;I%Z!!s_Ss zPdR3%8QwXbp2gH)le0ELM@)UDk6Y7+pEW#-Sfj3TDI7YtD`@40w$K|ka(wMq^0seY zdQ-T^I*Vz~hiT$mtk?cb$S!L5&D7XXQ&_cxts%v74Yyj|e);aW2D3>gm;8xhceq-# m(`T-qdi0)o=ea&89$<+6%CNZRY-cK{$n|vfb6Mw<&;$TlTp*AD literal 0 HcmV?d00001 diff --git a/setup.py b/setup.py index 69daa58ee..a05c25396 100755 --- a/setup.py +++ b/setup.py @@ -584,6 +584,8 @@ def package(args, for_bundle=False, sh_launcher=False): subprocess.check_call(['tic', '-x', '-o' + odir, 'terminfo/kitty.terminfo']) shutil.copy2('__main__.py', libdir) shutil.copy2('logo/kitty.rgba', os.path.join(libdir, 'logo')) + shutil.copy2('logo/beam-cursor.png', os.path.join(libdir, 'logo')) + shutil.copy2('logo/beam-cursor@2x.png', os.path.join(libdir, 'logo')) def src_ignore(parent, entries): return [