diff --git a/docs/changelog.rst b/docs/changelog.rst index fda3311ba57..07e97ea6301 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -214,6 +214,11 @@ Detailed list of changes - edit-in-kitty: Handle connection drop more gracefully (:pull:`9480`) +- Wayland: Add support for :code:`titlebar-only` in + :opt:`hide_window_decorations` to hide the titlebar while keeping shadow + borders for resizing. On compositors that use server-side decorations (such as + GNOME), this forces client-side decoration mode (:pull:`9486`) + 0.45.0 [2025-12-24] ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/glfw/glfw.py b/glfw/glfw.py index b720d2d0d03..9a631c6b69d 100755 --- a/glfw/glfw.py +++ b/glfw/glfw.py @@ -323,6 +323,7 @@ def generate_wrappers(glfw_header: str) -> None: const char* glfwWaylandMissingCapabilities(void) void glfwWaylandRunWithActivationToken(GLFWwindow *handle, GLFWactivationcallback cb, void *cb_data) bool glfwWaylandSetTitlebarColor(GLFWwindow *handle, uint32_t color, bool use_system_color) + void glfwWaylandSetTitlebarHidden(GLFWwindow *handle, bool hidden) void glfwWaylandRedrawCSDWindowTitle(GLFWwindow *handle) bool glfwWaylandIsWindowFullyCreated(GLFWwindow *handle) bool glfwWaylandBeep(GLFWwindow *handle) diff --git a/glfw/wl_client_side_decorations.c b/glfw/wl_client_side_decorations.c index cc7b5cf18c5..481d359d819 100644 --- a/glfw/wl_client_side_decorations.c +++ b/glfw/wl_client_side_decorations.c @@ -469,12 +469,14 @@ render_shadows(_GLFWwindow *window) { static bool create_shm_buffers(_GLFWwindow* window) { decs.mapping.size = 0; + const bool has_titlebar = !decs.titlebar_hidden; + const int side_height = window->wl.height + (has_titlebar ? decs.metrics.visible_titlebar_height : 0); #define bp(which, width, height) decs.mapping.size += init_buffer_pair(&decs.which.buffer, width, height, decs.for_window_state.fscale); - bp(titlebar, window->wl.width, decs.metrics.visible_titlebar_height); + if (has_titlebar) bp(titlebar, window->wl.width, decs.metrics.visible_titlebar_height); bp(shadow_top, window->wl.width, decs.metrics.width); bp(shadow_bottom, window->wl.width, decs.metrics.width); - bp(shadow_left, decs.metrics.width, window->wl.height + decs.metrics.visible_titlebar_height); - bp(shadow_right, decs.metrics.width, window->wl.height + decs.metrics.visible_titlebar_height); + bp(shadow_left, decs.metrics.width, side_height); + bp(shadow_right, decs.metrics.width, side_height); bp(shadow_upper_left, decs.metrics.width, decs.metrics.width); bp(shadow_upper_right, decs.metrics.width, decs.metrics.width); bp(shadow_lower_left, decs.metrics.width, decs.metrics.width); @@ -497,10 +499,11 @@ create_shm_buffers(_GLFWwindow* window) { close(fd); size_t offset = 0; #define Q(which) alloc_buffer_pair(window->id, &decs.which.buffer, pool, decs.mapping.data, &offset) - all_surfaces(Q); + if (has_titlebar) Q(titlebar); + all_shadow_surfaces(Q); #undef Q wl_shm_pool_destroy(pool); - render_title_bar(window, true); + if (has_titlebar) render_title_bar(window, true); render_shadows(window); debug("Created decoration buffers at scale: %f\n", decs.for_window_state.fscale); return true; @@ -578,6 +581,7 @@ csd_should_window_be_decorated(_GLFWwindow *window) { static bool ensure_csd_resources(_GLFWwindow *window) { if (!window_is_csd_capable(window)) return false; + const bool has_titlebar = !decs.titlebar_hidden; const bool is_focused = window->id == _glfw.focusedWindowId; const bool focus_changed = is_focused != decs.for_window_state.focused; const double current_scale = _glfwWaylandWindowScale(window); @@ -588,34 +592,47 @@ ensure_csd_resources(_GLFWwindow *window) { !decs.mapping.data ); const bool state_changed = decs.for_window_state.toplevel_states != window->wl.current.toplevel_states; - const bool needs_update = focus_changed || size_changed || !decs.titlebar.surface || decs.buffer_destroyed || state_changed; + const bool titlebar_state_changed = (has_titlebar && !decs.titlebar.surface) || (!has_titlebar && decs.titlebar.surface); + const bool needs_update = focus_changed || size_changed || titlebar_state_changed || decs.buffer_destroyed || state_changed; debug("CSD: old.size: %dx%d new.size: %dx%d needs_update: %d size_changed: %d state_changed: %d buffer_destroyed: %d\n", decs.for_window_state.width, decs.for_window_state.height, window->wl.width, window->wl.height, needs_update, size_changed, state_changed, decs.buffer_destroyed); if (!needs_update) return false; decs.for_window_state.fscale = current_scale; // used in create_shm_buffers - if (size_changed || decs.buffer_destroyed) { + if (size_changed || decs.buffer_destroyed || titlebar_state_changed) { free_csd_buffers(window); if (!create_shm_buffers(window)) return false; decs.buffer_destroyed = false; } + const int top_y = has_titlebar ? -(int)decs.metrics.visible_titlebar_height : 0; + #define setup_surface(which, x, y) \ if (!decs.which.surface) create_csd_surfaces(window, &decs.which); \ position_csd_surface(&decs.which, x, y); - setup_surface(titlebar, 0, -decs.metrics.visible_titlebar_height); - setup_surface(shadow_top, decs.titlebar.x, decs.titlebar.y - decs.metrics.width); - setup_surface(shadow_bottom, decs.titlebar.x, window->wl.height); - setup_surface(shadow_left, -decs.metrics.width, decs.titlebar.y); + if (has_titlebar) { + setup_surface(titlebar, 0, -decs.metrics.visible_titlebar_height); + } else { + free_csd_surface(&decs.titlebar); + if (decs.focus == CSD_titlebar) { + decs.focus = CENTRAL_WINDOW; + decs.dragging = false; + } + } + setup_surface(shadow_top, 0, top_y - decs.metrics.width); + setup_surface(shadow_bottom, 0, window->wl.height); + setup_surface(shadow_left, -decs.metrics.width, top_y); setup_surface(shadow_right, window->wl.width, decs.shadow_left.y); setup_surface(shadow_upper_left, decs.shadow_left.x, decs.shadow_top.y); setup_surface(shadow_upper_right, decs.shadow_right.x, decs.shadow_top.y); setup_surface(shadow_lower_left, decs.shadow_left.x, decs.shadow_bottom.y); setup_surface(shadow_lower_right, decs.shadow_right.x, decs.shadow_bottom.y); - if (focus_changed || state_changed) update_title_bar(window); - damage_csd(titlebar, decs.titlebar.buffer.front); + if (has_titlebar) { + if (focus_changed || state_changed) update_title_bar(window); + damage_csd(titlebar, decs.titlebar.buffer.front); + } #define d(which) damage_csd(which, is_focused ? decs.which.buffer.front : decs.which.buffer.back); d(shadow_left); d(shadow_right); d(shadow_top); d(shadow_bottom); d(shadow_upper_left); d(shadow_upper_right); d(shadow_lower_left); d(shadow_lower_right); @@ -659,17 +676,18 @@ csd_change_title(_GLFWwindow *window) { void csd_set_window_geometry(_GLFWwindow *window, int32_t *width, int32_t *height) { const bool include_space_for_csd = csd_should_window_be_decorated(window); + const bool has_titlebar = include_space_for_csd && !decs.titlebar_hidden; bool size_specified_by_compositor = *width > 0 && *height > 0; if (!size_specified_by_compositor) { *width = window->wl.user_requested_content_size.width; *height = window->wl.user_requested_content_size.height; if (window->wl.xdg.top_level_bounds.width > 0) *width = MIN(*width, window->wl.xdg.top_level_bounds.width); if (window->wl.xdg.top_level_bounds.height > 0) *height = MIN(*height, window->wl.xdg.top_level_bounds.height); - if (include_space_for_csd) *height += decs.metrics.visible_titlebar_height; + if (has_titlebar) *height += decs.metrics.visible_titlebar_height; } decs.geometry.x = 0; decs.geometry.y = 0; decs.geometry.width = *width; decs.geometry.height = *height; - if (include_space_for_csd) { + if (has_titlebar) { decs.geometry.y = -decs.metrics.visible_titlebar_height; *height -= decs.metrics.visible_titlebar_height; } diff --git a/glfw/wl_platform.h b/glfw/wl_platform.h index c2a83a0e8d1..2b532ec7224 100644 --- a/glfw/wl_platform.h +++ b/glfw/wl_platform.h @@ -231,7 +231,7 @@ typedef struct _GLFWwindowWayland } pointerLock; struct { - bool serverSide, buffer_destroyed, titlebar_needs_update, dragging; + bool serverSide, buffer_destroyed, titlebar_needs_update, dragging, titlebar_hidden; _GLFWCSDSurface focus; _GLFWWaylandCSDSurface titlebar, shadow_left, shadow_right, shadow_top, shadow_bottom, shadow_upper_left, shadow_upper_right, shadow_lower_left, shadow_lower_right; diff --git a/glfw/wl_window.c b/glfw/wl_window.c index 238b2296db0..20f834925a3 100644 --- a/glfw/wl_window.c +++ b/glfw/wl_window.c @@ -813,6 +813,8 @@ apply_xdg_configure_changes(_GLFWwindow *window) { if (window->wl.pending_state & PENDING_STATE_DECORATION) { uint32_t mode = window->wl.pending.decoration_mode; bool has_server_side_decorations = (mode == ZXDG_TOPLEVEL_DECORATION_V1_MODE_SERVER_SIDE); + // Force CSD when decorations are hidden or titlebar is hidden + if (!window->decorated || window->wl.decorations.titlebar_hidden) has_server_side_decorations = false; window->wl.decorations.serverSide = has_server_side_decorations; window->wl.current.decoration_mode = mode; } @@ -960,8 +962,14 @@ static void setXdgDecorations(_GLFWwindow* window) { if (window->wl.xdg.decoration) { - window->wl.decorations.serverSide = true; - zxdg_toplevel_decoration_v1_set_mode(window->wl.xdg.decoration, window->decorated ? ZXDG_TOPLEVEL_DECORATION_V1_MODE_SERVER_SIDE: ZXDG_TOPLEVEL_DECORATION_V1_MODE_CLIENT_SIDE); + if (window->wl.decorations.titlebar_hidden) { + window->wl.decorations.serverSide = false; + zxdg_toplevel_decoration_v1_set_mode(window->wl.xdg.decoration, ZXDG_TOPLEVEL_DECORATION_V1_MODE_CLIENT_SIDE); + csd_set_visible(window, csd_should_window_be_decorated(window)); + } else { + window->wl.decorations.serverSide = true; + zxdg_toplevel_decoration_v1_set_mode(window->wl.xdg.decoration, window->decorated ? ZXDG_TOPLEVEL_DECORATION_V1_MODE_SERVER_SIDE: ZXDG_TOPLEVEL_DECORATION_V1_MODE_CLIENT_SIDE); + } } else { window->wl.decorations.serverSide = false; csd_set_visible(window, csd_should_window_be_decorated(window)); @@ -1707,7 +1715,8 @@ void _glfwPlatformGetWindowFrameSize(_GLFWwindow* window, if (window->decorated && !window->monitor && !window->wl.decorations.serverSide) { if (top) - *top = window->wl.decorations.metrics.top - window->wl.decorations.metrics.visible_titlebar_height; + *top = window->wl.decorations.titlebar_hidden ? 0 : + window->wl.decorations.metrics.top - window->wl.decorations.metrics.visible_titlebar_height; if (left) *left = window->wl.decorations.metrics.width; if (right) @@ -3034,6 +3043,16 @@ GLFWAPI void glfwWaylandRedrawCSDWindowTitle(GLFWwindow *handle) { if (csd_change_title(window)) commit_window_surface_if_safe(window); } +GLFWAPI void glfwWaylandSetTitlebarHidden(GLFWwindow *handle, bool hidden) { + _GLFWwindow* window = (_GLFWwindow*) handle; + if (window->wl.decorations.titlebar_hidden != hidden) { + window->wl.decorations.titlebar_hidden = hidden; + setXdgDecorations(window); + inform_compositor_of_window_geometry(window, "SetTitlebarHidden"); + commit_window_surface_if_safe(window); + } +} + const GLFWLayerShellConfig* _glfwPlatformGetLayerShellConfig(_GLFWwindow *window) { return &window->wl.layer_shell.config; diff --git a/kitty/glfw-wrapper.c b/kitty/glfw-wrapper.c index 79a05696bcd..f16d61b00bb 100644 --- a/kitty/glfw-wrapper.c +++ b/kitty/glfw-wrapper.c @@ -515,6 +515,9 @@ load_glfw(const char* path) { *(void **) (&glfwWaylandSetTitlebarColor_impl) = dlsym(handle, "glfwWaylandSetTitlebarColor"); if (glfwWaylandSetTitlebarColor_impl == NULL) dlerror(); // clear error indicator + *(void **) (&glfwWaylandSetTitlebarHidden_impl) = dlsym(handle, "glfwWaylandSetTitlebarHidden"); + if (glfwWaylandSetTitlebarHidden_impl == NULL) dlerror(); // clear error indicator + *(void **) (&glfwWaylandRedrawCSDWindowTitle_impl) = dlsym(handle, "glfwWaylandRedrawCSDWindowTitle"); if (glfwWaylandRedrawCSDWindowTitle_impl == NULL) dlerror(); // clear error indicator diff --git a/kitty/glfw-wrapper.h b/kitty/glfw-wrapper.h index bdbc37be133..10e18341a12 100644 --- a/kitty/glfw-wrapper.h +++ b/kitty/glfw-wrapper.h @@ -2516,6 +2516,10 @@ typedef bool (*glfwWaylandSetTitlebarColor_func)(GLFWwindow*, uint32_t, bool); GFW_EXTERN glfwWaylandSetTitlebarColor_func glfwWaylandSetTitlebarColor_impl; #define glfwWaylandSetTitlebarColor glfwWaylandSetTitlebarColor_impl +typedef void (*glfwWaylandSetTitlebarHidden_func)(GLFWwindow*, bool); +GFW_EXTERN glfwWaylandSetTitlebarHidden_func glfwWaylandSetTitlebarHidden_impl; +#define glfwWaylandSetTitlebarHidden glfwWaylandSetTitlebarHidden_impl + typedef void (*glfwWaylandRedrawCSDWindowTitle_func)(GLFWwindow*); GFW_EXTERN glfwWaylandRedrawCSDWindowTitle_func glfwWaylandRedrawCSDWindowTitle_impl; #define glfwWaylandRedrawCSDWindowTitle glfwWaylandRedrawCSDWindowTitle_impl diff --git a/kitty/glfw.c b/kitty/glfw.c index 0358c8bf193..ddf9f10d1ad 100644 --- a/kitty/glfw.c +++ b/kitty/glfw.c @@ -1308,6 +1308,10 @@ apply_window_chrome_state(GLFWwindow *w, WindowChromeState new_state, int width, // Need to resize the window again after hiding decorations or title bar to take up screen space if (window_decorations_changed) glfwSetWindowSize(w, width, height); #else + if (global_state.is_wayland && glfwWaylandSetTitlebarHidden) { + bool titlebar_only = (new_state.hide_window_decorations & 2) != 0; + glfwWaylandSetTitlebarHidden(w, titlebar_only); + } if (window_decorations_changed) { bool hide_window_decorations = new_state.hide_window_decorations & 1; glfwSetWindowAttrib(w, GLFW_DECORATED, !hide_window_decorations); @@ -1587,6 +1591,10 @@ create_os_window(PyObject UNUSED *self, PyObject *args, PyObject *kw) { if (temp_window) { glfwDestroyWindow(temp_window); temp_window = NULL; } if (glfw_window == NULL) glfw_failure; #undef glfw_failure + // Set titlebar-only mode before the window becomes visible + if (global_state.is_wayland && (OPT(hide_window_decorations) & 2) && glfwWaylandSetTitlebarHidden) { + glfwWaylandSetTitlebarHidden(glfw_window, true); + } glfwMakeContextCurrent(glfw_window); if (is_first_window) gl_init(); bool is_semi_transparent = glfwGetWindowAttrib(glfw_window, GLFW_TRANSPARENT_FRAMEBUFFER); diff --git a/kitty/options/definition.py b/kitty/options/definition.py index f8b4c159a36..566d879970b 100644 --- a/kitty/options/definition.py +++ b/kitty/options/definition.py @@ -1341,9 +1341,13 @@ long_text=''' Hide the window decorations (title-bar and window borders) with :code:`yes`. On macOS, :code:`titlebar-only` and :code:`titlebar-and-corners` can be used to only hide the titlebar and the rounded corners. +On Wayland, :code:`titlebar-only` can be used to hide the titlebar while keeping +the window shadow borders for resizing. On compositors that use server-side +decorations (such as GNOME), both :code:`yes` and :code:`titlebar-only` force +client-side decoration mode. Whether this works and exactly what effect it has depends on the window manager/operating system. Note that the effects of changing this option when reloading config -are undefined. When using :code:`titlebar-only`, it is useful to also set +are undefined. When using :code:`titlebar-only` on macOS, it is useful to also set :opt:`window_margin_width` and :opt:`placement_strategy` to prevent the rounded corners from clipping text. Or use :code:`titlebar-and-corners`. '''