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:
parent
1faddeb402
commit
4313531432
@ -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`)
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
5
glfw/glfw3.h
vendored
5
glfw/glfw3.h
vendored
@ -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.
|
||||
*
|
||||
|
||||
5
glfw/input.c
vendored
5
glfw/input.c
vendored
@ -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;
|
||||
|
||||
2
glfw/internal.h
vendored
2
glfw/internal.h
vendored
@ -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);
|
||||
|
||||
2
glfw/null_window.c
vendored
2
glfw/null_window.c
vendored
@ -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;
|
||||
}
|
||||
|
||||
2
glfw/win32_window.c
vendored
2
glfw/win32_window.c
vendored
@ -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)
|
||||
|
||||
2
glfw/wl_window.c
vendored
2
glfw/wl_window.c
vendored
@ -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;
|
||||
|
||||
2
glfw/x11_window.c
vendored
2
glfw/x11_window.c
vendored
@ -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)
|
||||
|
||||
@ -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:
|
||||
|
||||
41
kitty/glfw.c
41
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 --------------------------------------------------------------------
|
||||
|
||||
|
||||
@ -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(
|
||||
|
||||
BIN
logo/beam-cursor.png
Normal file
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
BIN
logo/beam-cursor@2x.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.5 KiB |
2
setup.py
2
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 [
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user