From 7a1bdb4ff1eec2d369ce40f56c3dc9c5e4583bd8 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 27 Jun 2023 08:48:16 +0530 Subject: [PATCH] macOS: Implement background blurring Uses a private API that allows us to control the amount of blurring. While using a private API is obviously not ideal, it is used by both iTerm.app and Apple's own Terminal.app, so hopefully it should stick around. Fixes #6135 --- glfw/cocoa_platform.h | 1 + glfw/cocoa_window.m | 15 +++++++++++++++ glfw/glfw.py | 1 + glfw/glfw3.h | 4 ++++ glfw/internal.h | 1 + glfw/window.c | 5 +++++ kitty/glfw-wrapper.c | 3 +++ kitty/glfw-wrapper.h | 8 ++++++++ kitty/glfw.c | 16 ++++++++++++++++ kitty/options/definition.py | 11 +++++++++++ kitty/options/parse.py | 3 +++ kitty/options/to-c-generated.h | 15 +++++++++++++++ kitty/options/types.py | 4 +++- kitty/state.c | 1 + kitty/state.h | 2 ++ 15 files changed, 89 insertions(+), 1 deletion(-) diff --git a/glfw/cocoa_platform.h b/glfw/cocoa_platform.h index 34e6a515e..51167cd7d 100644 --- a/glfw/cocoa_platform.h +++ b/glfw/cocoa_platform.h @@ -139,6 +139,7 @@ typedef struct _GLFWwindowNS int width, height; int fbWidth, fbHeight; float xscale, yscale; + int blur_radius; // The total sum of the distances the cursor has been warped // since the last cursor motion event was processed diff --git a/glfw/cocoa_window.m b/glfw/cocoa_window.m index 3e8c4b39d..de8607ad4 100644 --- a/glfw/cocoa_window.m +++ b/glfw/cocoa_window.m @@ -36,6 +36,8 @@ #include +GLFWAPI int glfwCocoaSetBackgroundBlur(GLFWwindow *w, int radius); + static const char* polymorphic_string_as_utf8(id string) { if (string == nil) return "(nil)"; @@ -1835,6 +1837,7 @@ static bool createNativeWindow(_GLFWwindow* window, _glfwPlatformGetWindowSize(window, &window->ns.width, &window->ns.height); _glfwPlatformGetFramebufferSize(window, &window->ns.fbWidth, &window->ns.fbHeight); + if (wndconfig->ns.blur_radius > 0) glfwCocoaSetBackgroundBlur((GLFWwindow*)window, wndconfig->ns.blur_radius); return true; } @@ -2969,6 +2972,18 @@ GLFWAPI void glfwCocoaRequestRenderFrame(GLFWwindow *w, GLFWcocoarenderframefun requestRenderFrame((_GLFWwindow*)w, callback); } +GLFWAPI int glfwCocoaSetBackgroundBlur(GLFWwindow *w, int radius) { + _GLFWwindow* window = (_GLFWwindow*)w; + int orig = window->ns.blur_radius; + if (radius > -1 && radius != window->ns.blur_radius) { + extern OSStatus CGSSetWindowBackgroundBlurRadius(void* connection, NSInteger windowNumber, int radius); + extern void* CGSDefaultConnectionForThread(void); + CGSSetWindowBackgroundBlurRadius(CGSDefaultConnectionForThread(), [window->ns.object windowNumber], radius); + window->ns.blur_radius = radius; + } + return orig; +} + GLFWAPI int glfwGetCurrentSystemColorTheme(void) { int theme_type = 0; NSAppearance *changedAppearance = NSApp.effectiveAppearance; diff --git a/glfw/glfw.py b/glfw/glfw.py index d00b068dc..d8d4888b8 100755 --- a/glfw/glfw.py +++ b/glfw/glfw.py @@ -249,6 +249,7 @@ def generate_wrappers(glfw_header: str) -> None: GLFWapplicationwillfinishlaunchingfun glfwSetApplicationWillFinishLaunching(GLFWapplicationwillfinishlaunchingfun callback) uint32_t glfwGetCocoaKeyEquivalent(uint32_t glfw_key, int glfw_mods, int* cocoa_mods) void glfwCocoaRequestRenderFrame(GLFWwindow *w, GLFWcocoarenderframefun callback) + int glfwCocoaSetBackgroundBlur(GLFWwindow *w, int blur_radius) void* glfwGetX11Display(void) int32_t glfwGetX11Window(GLFWwindow* window) void glfwSetPrimarySelectionString(GLFWwindow* window, const char* string) diff --git a/glfw/glfw3.h b/glfw/glfw3.h index 9e60ed4af..6ad185df9 100644 --- a/glfw/glfw3.h +++ b/glfw/glfw3.h @@ -1034,6 +1034,10 @@ typedef enum { SRGB_COLORSPACE = 1, DISPLAY_P3_COLORSPACE = 2, } GlfwCocoaColorSpaces; +/*! @brief macOS specific + * [window hint](@ref GLFW_COCOA_BLUR_RADIUS_hint). + */ +#define GLFW_COCOA_BLUR_RADIUS 0x00023005 /*! @brief X11 specific * [window hint](@ref GLFW_X11_CLASS_NAME_hint). diff --git a/glfw/internal.h b/glfw/internal.h index d7729ecef..53c777748 100644 --- a/glfw/internal.h +++ b/glfw/internal.h @@ -313,6 +313,7 @@ struct _GLFWwndconfig struct { bool retina; int color_space; + int blur_radius; char frameName[256]; } ns; struct { diff --git a/glfw/window.c b/glfw/window.c index 93f116b53..18eb5c723 100644 --- a/glfw/window.c +++ b/glfw/window.c @@ -334,6 +334,8 @@ void glfwDefaultWindowHints(void) _glfw.hints.window.ns.retina = true; // use the default colorspace assigned by the system _glfw.hints.window.ns.color_space = 0; + // no blur + _glfw.hints.window.ns.blur_radius = 0; } GLFWAPI void glfwWindowHint(int hint, int value) @@ -417,6 +419,9 @@ GLFWAPI void glfwWindowHint(int hint, int value) case GLFW_COCOA_COLOR_SPACE: _glfw.hints.window.ns.color_space = value; return; + case GLFW_COCOA_BLUR_RADIUS: + _glfw.hints.window.ns.blur_radius = value; + return; case GLFW_COCOA_GRAPHICS_SWITCHING: _glfw.hints.context.nsgl.offline = value ? true : false; return; diff --git a/kitty/glfw-wrapper.c b/kitty/glfw-wrapper.c index 4753b0753..90227fdc4 100644 --- a/kitty/glfw-wrapper.c +++ b/kitty/glfw-wrapper.c @@ -443,6 +443,9 @@ load_glfw(const char* path) { *(void **) (&glfwCocoaRequestRenderFrame_impl) = dlsym(handle, "glfwCocoaRequestRenderFrame"); if (glfwCocoaRequestRenderFrame_impl == NULL) dlerror(); // clear error indicator + *(void **) (&glfwCocoaSetBackgroundBlur_impl) = dlsym(handle, "glfwCocoaSetBackgroundBlur"); + if (glfwCocoaSetBackgroundBlur_impl == NULL) dlerror(); // clear error indicator + *(void **) (&glfwGetX11Display_impl) = dlsym(handle, "glfwGetX11Display"); if (glfwGetX11Display_impl == NULL) dlerror(); // clear error indicator diff --git a/kitty/glfw-wrapper.h b/kitty/glfw-wrapper.h index 678a79187..ccaa5ed1a 100644 --- a/kitty/glfw-wrapper.h +++ b/kitty/glfw-wrapper.h @@ -772,6 +772,10 @@ typedef enum { SRGB_COLORSPACE = 1, DISPLAY_P3_COLORSPACE = 2, } GlfwCocoaColorSpaces; +/*! @brief macOS specific + * [window hint](@ref GLFW_COCOA_BLUR_RADIUS_hint). + */ +#define GLFW_COCOA_BLUR_RADIUS 0x00023005 /*! @brief X11 specific * [window hint](@ref GLFW_X11_CLASS_NAME_hint). @@ -2216,6 +2220,10 @@ typedef void (*glfwCocoaRequestRenderFrame_func)(GLFWwindow*, GLFWcocoarenderfra GFW_EXTERN glfwCocoaRequestRenderFrame_func glfwCocoaRequestRenderFrame_impl; #define glfwCocoaRequestRenderFrame glfwCocoaRequestRenderFrame_impl +typedef int (*glfwCocoaSetBackgroundBlur_func)(GLFWwindow*, int); +GFW_EXTERN glfwCocoaSetBackgroundBlur_func glfwCocoaSetBackgroundBlur_impl; +#define glfwCocoaSetBackgroundBlur glfwCocoaSetBackgroundBlur_impl + typedef void* (*glfwGetX11Display_func)(void); GFW_EXTERN glfwGetX11Display_func glfwGetX11Display_impl; #define glfwGetX11Display glfwGetX11Display_impl diff --git a/kitty/glfw.c b/kitty/glfw.c index af7f25a5d..4b79881ac 100644 --- a/kitty/glfw.c +++ b/kitty/glfw.c @@ -902,6 +902,11 @@ create_os_window(PyObject UNUSED *self, PyObject *args, PyObject *kw) { #ifdef __APPLE__ glfwWindowHint(GLFW_COCOA_COLOR_SPACE, OPT(macos_colorspace)); + if (OPT(background_blur) > 0 && OPT(background_opacity) < 1.f) { + glfwWindowHint(GLFW_COCOA_BLUR_RADIUS, MIN(OPT(background_blur), 128)); + } else { + glfwWindowHint(GLFW_COCOA_BLUR_RADIUS, 0); + } #else glfwWindowHintString(GLFW_X11_INSTANCE_NAME, wm_class_name); glfwWindowHintString(GLFW_X11_CLASS_NAME, wm_class_class); @@ -1081,6 +1086,17 @@ os_window_update_size_increments(OSWindow *window) { } } +void +update_background_blur(OSWindow *os_window) { +#ifdef __APPLE__ + int new_blur_radius = 0; + if (os_window->background_opacity < 1.f && OPT(background_blur) > -1) new_blur_radius = OPT(background_blur); + glfwCocoaSetBackgroundBlur(os_window->handle, new_blur_radius); +#else + (void)os_window; +#endif +} + #ifdef __APPLE__ static bool window_in_same_cocoa_workspace(void *w, size_t *source_workspaces, size_t source_workspace_count) { diff --git a/kitty/options/definition.py b/kitty/options/definition.py index 45ab74980..5a152d210 100644 --- a/kitty/options/definition.py +++ b/kitty/options/definition.py @@ -1371,6 +1371,17 @@ config. ''' ) +opt('background_blur', '0', option_type='int', ctype='int', + long_text=''' +Set to a positive value to enable background blur (blurring of the visuals +behind a transparent window) on platforms that support it. Only takes effect +when :opt:`background_opacity` is less than one. On macOS, this will also +control the :italic:`blur radius` (amount of blurring). Setting it to too high +a value will cause severe performance issues and/or rendering artifacts. +Usually, values up to 64 work well. Note that this might cause performance issues, +depending on how the platform implements it, so use with care. +''') + opt('background_image', 'none', option_type='config_or_absolute_path', ctype='!background_image', long_text='Path to a background image. Must be in PNG format.' diff --git a/kitty/options/parse.py b/kitty/options/parse.py index fc0972e45..d008ab7a8 100644 --- a/kitty/options/parse.py +++ b/kitty/options/parse.py @@ -65,6 +65,9 @@ class Parser: def background(self, val: str, ans: typing.Dict[str, typing.Any]) -> None: ans['background'] = to_color(val) + def background_blur(self, val: str, ans: typing.Dict[str, typing.Any]) -> None: + ans['background_blur'] = int(val) + def background_image(self, val: str, ans: typing.Dict[str, typing.Any]) -> None: ans['background_image'] = config_or_absolute_path(val) diff --git a/kitty/options/to-c-generated.h b/kitty/options/to-c-generated.h index cf57edf7a..aebbb451f 100644 --- a/kitty/options/to-c-generated.h +++ b/kitty/options/to-c-generated.h @@ -733,6 +733,19 @@ convert_from_opts_background_opacity(PyObject *py_opts, Options *opts) { Py_DECREF(ret); } +static void +convert_from_python_background_blur(PyObject *val, Options *opts) { + opts->background_blur = PyLong_AsLong(val); +} + +static void +convert_from_opts_background_blur(PyObject *py_opts, Options *opts) { + PyObject *ret = PyObject_GetAttrString(py_opts, "background_blur"); + if (ret == NULL) return; + convert_from_python_background_blur(ret, opts); + Py_DECREF(ret); +} + static void convert_from_python_background_image(PyObject *val, Options *opts) { background_image(val, opts); @@ -1159,6 +1172,8 @@ convert_opts_from_python_opts(PyObject *py_opts, Options *opts) { if (PyErr_Occurred()) return false; convert_from_opts_background_opacity(py_opts, opts); if (PyErr_Occurred()) return false; + convert_from_opts_background_blur(py_opts, opts); + if (PyErr_Occurred()) return false; convert_from_opts_background_image(py_opts, opts); if (PyErr_Occurred()) return false; convert_from_opts_background_image_layout(py_opts, opts); diff --git a/kitty/options/types.py b/kitty/options/types.py index f750aff02..e7e53f96a 100644 --- a/kitty/options/types.py +++ b/kitty/options/types.py @@ -61,6 +61,7 @@ option_names = ( # {{{ 'allow_hyperlinks', 'allow_remote_control', 'background', + 'background_blur', 'background_image', 'background_image_layout', 'background_image_linear', @@ -480,6 +481,7 @@ class Options: allow_hyperlinks: int = 1 allow_remote_control: choices_for_allow_remote_control = 'no' background: Color = Color(0, 0, 0) + background_blur: int = 0 background_image: typing.Optional[str] = None background_image_layout: choices_for_background_image_layout = 'tiled' background_image_linear: bool = False @@ -511,7 +513,7 @@ class Options: cursor_underline_thickness: float = 2.0 default_pointer_shape: choices_for_default_pointer_shape = 'beam' detect_urls: bool = True - dim_opacity: float = 0.75 + dim_opacity: float = 0.4 disable_ligatures: int = 0 draw_minimal_borders: bool = True dynamic_background_opacity: bool = False diff --git a/kitty/state.c b/kitty/state.c index 92cd35ce8..d14a489c6 100644 --- a/kitty/state.c +++ b/kitty/state.c @@ -1093,6 +1093,7 @@ PYWRAP0(apply_options_update) { get_platform_dependent_config_values(os_window->handle); os_window->background_opacity = OPT(background_opacity); os_window->is_damaged = true; + update_background_blur(os_window); for (size_t t = 0; t < os_window->num_tabs; t++) { Tab *tab = os_window->tabs + t; for (size_t w = 0; w < tab->num_windows; w++) { diff --git a/kitty/state.h b/kitty/state.h index 1d5c00431..dd126347c 100644 --- a/kitty/state.h +++ b/kitty/state.h @@ -89,6 +89,7 @@ typedef struct { float val; AdjustmentUnit unit; } underline_position, underline_thickness, strikethrough_position, strikethrough_thickness, cell_width, cell_height, baseline; bool show_hyperlink_targets; + int background_blur; } Options; typedef struct WindowLogoRenderData { @@ -365,3 +366,4 @@ void update_ime_position(Window* w, Screen *screen); bool update_ime_position_for_window(id_type window_id, bool force, int update_focus); void set_ignore_os_keyboard_processing(bool enabled); void update_menu_bar_title(PyObject *title UNUSED); +void update_background_blur(OSWindow *);