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.
This commit is contained in:
Kovid Goyal 2018-07-07 10:39:30 +05:30
parent 1faddeb402
commit 4313531432
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
15 changed files with 101 additions and 38 deletions

View File

@ -15,6 +15,9 @@ Changelog
- macOS: Add an option :opt:`macos_window_resizable` to control if kitty - macOS: Add an option :opt:`macos_window_resizable` to control if kitty
top-level windows are resizable using the mouse or not (:iss:`698`) 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 - Fix triple-click to select line not working when the entire line is filled
(:iss:`703`) (:iss:`703`)

View File

@ -1886,44 +1886,44 @@ int _glfwPlatformGetKeyScancode(int key)
int _glfwPlatformCreateCursor(_GLFWcursor* cursor, int _glfwPlatformCreateCursor(_GLFWcursor* cursor,
const GLFWimage* image, const GLFWimage* image,
int xhot, int yhot) int xhot, int yhot, int count)
{ {
NSImage* native; NSImage* native;
NSBitmapImageRep* rep; NSBitmapImageRep* rep;
if (!initializeAppKit()) if (!initializeAppKit())
return GLFW_FALSE; return GLFW_FALSE;
native = [[NSImage alloc] initWithSize:NSMakeSize(image->width, image->height)];
rep = [[NSBitmapImageRep alloc] if (native == nil)
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)
return GLFW_FALSE; 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)]; memcpy([rep bitmapData], src->pixels, src->width * src->height * 4);
[native addRepresentation:rep]; [native addRepresentation:rep];
[rep release];
}
cursor->ns.object = [[NSCursor alloc] initWithImage:native cursor->ns.object = [[NSCursor alloc] initWithImage:native
hotSpot:NSMakePoint(xhot, yhot)]; hotSpot:NSMakePoint(xhot, yhot)];
[native release]; [native release];
[rep release];
if (cursor->ns.object == nil) if (cursor->ns.object == nil)
return GLFW_FALSE; return GLFW_FALSE;
return GLFW_TRUE; return GLFW_TRUE;
} }

5
glfw/glfw3.h vendored
View File

@ -4123,6 +4123,7 @@ GLFWAPI void glfwSetCursorPos(GLFWwindow* window, double xpos, double ypos);
* @param[in] image The desired cursor image. * @param[in] image The desired cursor image.
* @param[in] xhot The desired x-coordinate, in pixels, of the cursor hotspot. * @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] 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 * @return The handle of the created cursor, or `NULL` if an
* [error](@ref error_handling) occurred. * [error](@ref error_handling) occurred.
* *
@ -4138,11 +4139,11 @@ GLFWAPI void glfwSetCursorPos(GLFWwindow* window, double xpos, double ypos);
* @sa @ref glfwDestroyCursor * @sa @ref glfwDestroyCursor
* @sa @ref glfwCreateStandardCursor * @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 * @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. /*! @brief Creates a cursor with a standard shape.
* *

5
glfw/input.c vendored
View File

@ -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; _GLFWcursor* cursor;
assert(image != NULL); assert(image != NULL);
assert(count > 0);
_GLFW_REQUIRE_INIT_OR_RETURN(NULL); _GLFW_REQUIRE_INIT_OR_RETURN(NULL);
@ -811,7 +812,7 @@ GLFWAPI GLFWcursor* glfwCreateCursor(const GLFWimage* image, int xhot, int yhot)
cursor->next = _glfw.cursorListHead; cursor->next = _glfw.cursorListHead;
_glfw.cursorListHead = cursor; _glfw.cursorListHead = cursor;
if (!_glfwPlatformCreateCursor(cursor, image, xhot, yhot)) if (!_glfwPlatformCreateCursor(cursor, image, xhot, yhot, count))
{ {
glfwDestroyCursor((GLFWcursor*) cursor); glfwDestroyCursor((GLFWcursor*) cursor);
return NULL; return NULL;

2
glfw/internal.h vendored
View File

@ -600,7 +600,7 @@ void _glfwPlatformGetCursorPos(_GLFWwindow* window, double* xpos, double* ypos);
void _glfwPlatformSetCursorPos(_GLFWwindow* window, double xpos, double ypos); void _glfwPlatformSetCursorPos(_GLFWwindow* window, double xpos, double ypos);
void _glfwPlatformSetCursorMode(_GLFWwindow* window, int mode); void _glfwPlatformSetCursorMode(_GLFWwindow* window, int mode);
int _glfwPlatformCreateCursor(_GLFWcursor* cursor, 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); int _glfwPlatformCreateStandardCursor(_GLFWcursor* cursor, int shape);
void _glfwPlatformDestroyCursor(_GLFWcursor* cursor); void _glfwPlatformDestroyCursor(_GLFWcursor* cursor);
void _glfwPlatformSetCursor(_GLFWwindow* window, _GLFWcursor* cursor); void _glfwPlatformSetCursor(_GLFWwindow* window, _GLFWcursor* cursor);

2
glfw/null_window.c vendored
View File

@ -267,7 +267,7 @@ void _glfwPlatformSetCursorMode(_GLFWwindow* window, int mode)
int _glfwPlatformCreateCursor(_GLFWcursor* cursor, int _glfwPlatformCreateCursor(_GLFWcursor* cursor,
const GLFWimage* image, const GLFWimage* image,
int xhot, int yhot) int xhot, int yhot, int count)
{ {
return GLFW_TRUE; return GLFW_TRUE;
} }

2
glfw/win32_window.c vendored
View File

@ -1839,7 +1839,7 @@ int _glfwPlatformGetKeyScancode(int key)
int _glfwPlatformCreateCursor(_GLFWcursor* cursor, int _glfwPlatformCreateCursor(_GLFWcursor* cursor,
const GLFWimage* image, const GLFWimage* image,
int xhot, int yhot) int xhot, int yhot, int count)
{ {
cursor->win32.handle = (HCURSOR) createIcon(image, xhot, yhot, GLFW_FALSE); cursor->win32.handle = (HCURSOR) createIcon(image, xhot, yhot, GLFW_FALSE);
if (!cursor->win32.handle) if (!cursor->win32.handle)

2
glfw/wl_window.c vendored
View File

@ -1257,7 +1257,7 @@ int _glfwPlatformGetKeyScancode(int key)
int _glfwPlatformCreateCursor(_GLFWcursor* cursor, int _glfwPlatformCreateCursor(_GLFWcursor* cursor,
const GLFWimage* image, const GLFWimage* image,
int xhot, int yhot) int xhot, int yhot, int count)
{ {
cursor->wl.buffer = createShmBuffer(image); cursor->wl.buffer = createShmBuffer(image);
cursor->wl.width = image->width; cursor->wl.width = image->width;

2
glfw/x11_window.c vendored
View File

@ -2684,7 +2684,7 @@ int _glfwPlatformGetKeyScancode(int key)
int _glfwPlatformCreateCursor(_GLFWcursor* cursor, int _glfwPlatformCreateCursor(_GLFWcursor* cursor,
const GLFWimage* image, const GLFWimage* image,
int xhot, int yhot) int xhot, int yhot, int count)
{ {
cursor->x11.handle = _glfwCreateCursorX11(image, xhot, yhot); cursor->x11.handle = _glfwCreateCursorX11(image, xhot, yhot);
if (!cursor->x11.handle) if (!cursor->x11.handle)

View File

@ -105,6 +105,7 @@ def wakeup():
base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
terminfo_dir = os.path.join(base_dir, 'terminfo') terminfo_dir = os.path.join(base_dir, 'terminfo')
logo_data_file = os.path.join(base_dir, 'logo', 'kitty.rgba') 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: try:
shell_path = pwd.getpwuid(os.geteuid()).pw_shell or '/bin/sh' shell_path = pwd.getpwuid(os.geteuid()).pw_shell or '/bin/sh'
except KeyError: except KeyError:

View File

@ -456,8 +456,10 @@ create_os_window(PyObject UNUSED *self, PyObject *args) {
if (OPT(macos_hide_from_tasks)) cocoa_set_hide_from_tasks(); if (OPT(macos_hide_from_tasks)) cocoa_set_hide_from_tasks();
#endif #endif
#define CC(dest, shape) {\ #define CC(dest, shape) {\
dest##_cursor = glfwCreateStandardCursor(GLFW_##shape##_CURSOR); \ if (!dest##_cursor) { \
if (dest##_cursor == NULL) { log_error("Failed to create the %s mouse cursor, using default cursor.", #shape); }} 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); CC(standard, IBEAM); CC(click, HAND); CC(arrow, ARROW);
#undef CC #undef CC
is_first_window = false; is_first_window = false;
@ -839,6 +841,39 @@ set_smallest_allowed_resize(PyObject *self UNUSED, PyObject *args) {
Py_RETURN_NONE; 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__ #ifdef __APPLE__
void void
get_cocoa_key_equivalent(int key, int mods, unsigned short *cocoa_key, int *cocoa_mods) { 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 {{{ // Boilerplate {{{
static PyMethodDef module_methods[] = { static PyMethodDef module_methods[] = {
METHODB(set_custom_cursor, METH_VARARGS),
METHODB(set_smallest_allowed_resize, METH_VARARGS), METHODB(set_smallest_allowed_resize, METH_VARARGS),
METHODB(create_os_window, METH_VARARGS), METHODB(create_os_window, METH_VARARGS),
METHODB(set_default_window_icon, METH_VARARGS), METHODB(set_default_window_icon, METH_VARARGS),
@ -891,6 +927,7 @@ init_glfw(PyObject *m) {
ADDC(GLFW_PRESS); ADDC(GLFW_PRESS);
ADDC(GLFW_REPEAT); ADDC(GLFW_REPEAT);
ADDC(GLFW_TRUE); ADDC(GLFW_FALSE); ADDC(GLFW_TRUE); ADDC(GLFW_FALSE);
ADDC(GLFW_IBEAM_CURSOR); ADDC(GLFW_HAND_CURSOR); ADDC(GLFW_ARROW_CURSOR);
// --- Keys -------------------------------------------------------------------- // --- Keys --------------------------------------------------------------------

View File

@ -12,12 +12,13 @@ from .boss import Boss
from .cli import create_opts, parse_args from .cli import create_opts, parse_args
from .config import cached_values_for, initial_window_size_func from .config import cached_values_for, initial_window_size_func
from .constants import ( from .constants import (
appname, config_dir, glfw_path, is_macos, is_wayland, kitty_exe, appname, beam_cursor_data_file, config_dir, glfw_path, is_macos,
logo_data_file is_wayland, kitty_exe, logo_data_file
) )
from .fast_data_types import ( from .fast_data_types import (
GLFW_MOD_SUPER, create_os_window, free_font_data, glfw_init, GLFW_IBEAM_CURSOR, GLFW_MOD_SUPER, create_os_window, free_font_data,
glfw_terminate, set_default_window_icon, set_options 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.box_drawing import set_scale
from .fonts.render import set_font_family from .fonts.render import set_font_family
@ -28,6 +29,21 @@ from .utils import (
from .window import load_shader_programs 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): def talk_to_instance(args):
import json import json
import socket import socket
@ -100,6 +116,8 @@ def get_new_os_window_trigger(opts):
def _run_app(opts, args): def _run_app(opts, args):
new_os_window_trigger = get_new_os_window_trigger(opts) 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 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: with startup_notification_handler(extra_callback=run_app.first_window_callback) as pre_show_callback:
window_id = create_os_window( window_id = create_os_window(

BIN
logo/beam-cursor.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 217 B

BIN
logo/beam-cursor@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -584,6 +584,8 @@ def package(args, for_bundle=False, sh_launcher=False):
subprocess.check_call(['tic', '-x', '-o' + odir, 'terminfo/kitty.terminfo']) subprocess.check_call(['tic', '-x', '-o' + odir, 'terminfo/kitty.terminfo'])
shutil.copy2('__main__.py', libdir) shutil.copy2('__main__.py', libdir)
shutil.copy2('logo/kitty.rgba', os.path.join(libdir, 'logo')) 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): def src_ignore(parent, entries):
return [ return [