Skip to content

Commit

Permalink
macOS: Use a custom mouse cursor that shows up well on both light and…
Browse files Browse the repository at this point in the history
… dark backgrounds

Fixes #359. Also, update GLFW from upstream for retina cursor setting
support.
  • Loading branch information
kovidgoyal committed Jul 7, 2018
1 parent 1faddeb commit 4313531
Show file tree
Hide file tree
Showing 15 changed files with 102 additions and 39 deletions.
3 changes: 3 additions & 0 deletions docs/changelog.rst
Expand Up @@ -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`)

Expand Down
48 changes: 24 additions & 24 deletions glfw/cocoa_window.m
Expand Up @@ -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);

native = [[NSImage alloc] initWithSize:NSMakeSize(image->width, image->height)];
[native addRepresentation:rep];
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;

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;
}

Expand Down
5 changes: 3 additions & 2 deletions glfw/glfw3.h
Expand Up @@ -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.
*
Expand All @@ -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.
*
Expand Down
5 changes: 3 additions & 2 deletions glfw/input.c
Expand Up @@ -799,19 +799,20 @@ 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);

cursor = calloc(1, sizeof(_GLFWcursor));
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;
Expand Down
2 changes: 1 addition & 1 deletion glfw/internal.h
Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion glfw/null_window.c
Expand Up @@ -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;
}
Expand Down
2 changes: 1 addition & 1 deletion glfw/win32_window.c
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion glfw/wl_window.c
Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion glfw/x11_window.c
Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions kitty/constants.py
Expand Up @@ -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:
Expand Down
41 changes: 39 additions & 2 deletions kitty/glfw.c
Expand Up @@ -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;
Expand Down Expand Up @@ -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) {
Expand All @@ -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),
Expand Down Expand Up @@ -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 --------------------------------------------------------------------

Expand Down
26 changes: 22 additions & 4 deletions kitty/main.py
Expand Up @@ -12,12 +12,13 @@
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
Expand All @@ -28,6 +29,21 @@
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
Expand Down Expand Up @@ -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(
Expand Down
Binary file added logo/beam-cursor.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added logo/beam-cursor@2x.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions setup.py
Expand Up @@ -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 [
Expand Down

0 comments on commit 4313531

Please sign in to comment.