Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions docs/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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]
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Expand Down
1 change: 1 addition & 0 deletions glfw/glfw.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
48 changes: 33 additions & 15 deletions glfw/wl_client_side_decorations.c
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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;
Expand Down Expand Up @@ -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);
Expand All @@ -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);
Expand Down Expand Up @@ -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;
}
Expand Down
2 changes: 1 addition & 1 deletion glfw/wl_platform.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
25 changes: 22 additions & 3 deletions glfw/wl_window.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -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));
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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;
Expand Down
3 changes: 3 additions & 0 deletions kitty/glfw-wrapper.c

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions kitty/glfw-wrapper.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions kitty/glfw.c
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down
6 changes: 5 additions & 1 deletion kitty/options/definition.py
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
'''
Expand Down