Wayland: map first window at correct size with fractional scaling

The first OS window on Wayland is created at scale 1 because the
compositor only sends the fractional scale after the surface exists.
The initial size in cells is therefore computed with scale-1 cell
metrics and the window is mapped a few cells too small. The existing
post-map recompute calls glfwSetWindowSize(), but on Wayland that loses
to the compositor's authoritative configure, which re-asserts the size
it recorded at map time. So a window that receives focus (the normal
interactive case) stays too small; a background window that never gets
an activated configure happens to keep the resized value, which is why
it can look fixed.

Correct the size while the window is still unmapped instead. GLFW gains
a hook (glfwWaylandSetInitialWindowSizeCallback) that it invokes when
the fractional scale is applied during creation -- mutter, kwin and niri
send the scale before the first configure -- letting the embedder
recompute the cell-based logical size for the real scale before the
window is mapped. The compositor then records the correct size and its
activation configure agrees, so there is nothing to race.

The existing post-map recompute is kept as a fallback for compositors
that only send the scale after the first configure (sway, Hyprland).

Also add glfwCocoaSetWindowLevel to the glfw.py wrapper generator list:
it was present in the committed glfw-wrapper but missing from the
generator, so regenerating the wrapper (required to add the hook above)
would otherwise drop it and break the macOS build.
This commit is contained in:
Dan Egnor 2026-07-18 20:27:59 -07:00
parent 5b4ea1bbba
commit db14d7c220
6 changed files with 86 additions and 7 deletions

View file

@ -318,6 +318,7 @@ def generate_wrappers(glfw_header: str) -> None:
void glfwCocoaSetWindowChrome(GLFWwindow* window, unsigned int color, bool use_system_color, unsigned int system_color,\
int background_blur, unsigned int hide_window_decorations, bool show_text_in_titlebar, int color_space, float background_opacity, bool resizable)
void glfwCocoaRegisterMIMETypes(GLFWwindow *window, const char **mimes, size_t count)
void glfwCocoaSetWindowLevel(GLFWwindow *window, const char *level_spec)
const char* glfwGetPrimarySelectionString(GLFWwindow* window, void)
int glfwGetNativeKeyForName(const char* key_name, int case_sensitive)
void glfwRequestWaylandFrameEvent(GLFWwindow *handle, unsigned long long id, GLFWwaylandframecallbackfunc callback)
@ -326,6 +327,7 @@ def generate_wrappers(glfw_header: str) -> None:
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 glfwWaylandSetInitialWindowSizeCallback(GLFWwaylandinitialsizefun callback)
void glfwWaylandRedrawCSDWindowTitle(GLFWwindow *handle)
bool glfwWaylandIsWindowFullyCreated(GLFWwindow *handle)
bool glfwWaylandBeep(GLFWwindow *handle)

1
glfw/glfw3.h vendored
View file

@ -1933,6 +1933,7 @@ typedef void (* GLFWjoystickfun)(int,int);
typedef void (* GLFWuserdatafun)(unsigned long long, void*);
typedef void (* GLFWtickcallback)(void*);
typedef void (* GLFWactivationcallback)(GLFWwindow *window, const char *token, void *data);
typedef void (* GLFWwaylandinitialsizefun)(GLFWwindow *window, float xscale, float yscale, int *width, int *height);
typedef bool (* GLFWdrawtextfun)(GLFWwindow *window, const char *text, uint32_t fg, uint32_t bg, uint8_t *output_buf, size_t width, size_t height, float x_offset, float y_offset, size_t right_margin, bool is_single_glyph);
typedef char* (* GLFWcurrentselectionfun)(void);
typedef bool (* GLFWhascurrentselectionfun)(void);

33
glfw/wl_window.c vendored
View file

@ -440,8 +440,41 @@ clipboard_mime(void) {
return buf;
}
static GLFWwaylandinitialsizefun initial_window_size_callback = NULL;
GLFWAPI void
glfwWaylandSetInitialWindowSizeCallback(GLFWwaylandinitialsizefun callback) {
initial_window_size_callback = callback;
}
static void
maybe_recompute_initial_window_size(_GLFWwindow *window) {
// Before the initial window is mapped, the compositor tells us the real
// (possibly fractional) scale. For sizes specified in cells this changes
// the correct logical window size, because cell metrics are not a linear
// function of scale (integer-pixel rounding). Ask the embedder to recompute
// the logical size now, while the window is still unmapped, so it is mapped
// at the right size and the compositor's authoritative configure agrees.
if (window->wl.window_fully_created) return;
if (!initial_window_size_callback) return;
if (!window->wl.xdg.toplevel) return; // only ordinary toplevels have cell-based sizes
if (window->wl.layer_shell.zwlr_layer_surface_v1) return; // layer-shell surfaces size themselves
double scale = _glfwWaylandWindowScale(window);
int w = window->wl.width, h = window->wl.height;
initial_window_size_callback((GLFWwindow*)window, (float)scale, (float)scale, &w, &h);
if (w > 0 && h > 0 && (w != window->wl.width || h != window->wl.height)) {
debug("Recomputed initial size of window %llu for scale %.3f: %dx%d -> %dx%d\n",
window->id, scale, window->wl.width, window->wl.height, w, h);
window->wl.width = w; window->wl.height = h;
window->wl.user_requested_content_size.width = w;
window->wl.user_requested_content_size.height = h;
update_regions(window);
}
}
static void
apply_scale_changes(_GLFWwindow *window, bool resize_framebuffer, bool update_csd) {
maybe_recompute_initial_window_size(window);
double scale = _glfwWaylandWindowScale(window);
if (resize_framebuffer) resizeFramebuffer(window);
_glfwInputWindowContentScale(window, (float)scale, (float)scale);

9
kitty/glfw-wrapper.c generated
View file

@ -497,15 +497,15 @@ load_glfw(const char* path) {
*(void **) (&glfwCocoaCycleThroughOSWindows_impl) = dlsym(handle, "glfwCocoaCycleThroughOSWindows");
if (glfwCocoaCycleThroughOSWindows_impl == NULL) dlerror(); // clear error indicator
*(void **) (&glfwCocoaSetWindowLevel_impl) = dlsym(handle, "glfwCocoaSetWindowLevel");
if (glfwCocoaSetWindowLevel_impl == NULL) dlerror(); // clear error indicator
*(void **) (&glfwCocoaSetWindowChrome_impl) = dlsym(handle, "glfwCocoaSetWindowChrome");
if (glfwCocoaSetWindowChrome_impl == NULL) dlerror(); // clear error indicator
*(void **) (&glfwCocoaRegisterMIMETypes_impl) = dlsym(handle, "glfwCocoaRegisterMIMETypes");
if (glfwCocoaRegisterMIMETypes_impl == NULL) dlerror(); // clear error indicator
*(void **) (&glfwCocoaSetWindowLevel_impl) = dlsym(handle, "glfwCocoaSetWindowLevel");
if (glfwCocoaSetWindowLevel_impl == NULL) dlerror(); // clear error indicator
*(void **) (&glfwGetPrimarySelectionString_impl) = dlsym(handle, "glfwGetPrimarySelectionString");
if (glfwGetPrimarySelectionString_impl == NULL) dlerror(); // clear error indicator
@ -530,6 +530,9 @@ load_glfw(const char* path) {
*(void **) (&glfwWaylandSetTitlebarHidden_impl) = dlsym(handle, "glfwWaylandSetTitlebarHidden");
if (glfwWaylandSetTitlebarHidden_impl == NULL) dlerror(); // clear error indicator
*(void **) (&glfwWaylandSetInitialWindowSizeCallback_impl) = dlsym(handle, "glfwWaylandSetInitialWindowSizeCallback");
if (glfwWaylandSetInitialWindowSizeCallback_impl == NULL) dlerror(); // clear error indicator
*(void **) (&glfwWaylandRedrawCSDWindowTitle_impl) = dlsym(handle, "glfwWaylandRedrawCSDWindowTitle");
if (glfwWaylandRedrawCSDWindowTitle_impl == NULL) dlerror(); // clear error indicator

13
kitty/glfw-wrapper.h generated
View file

@ -1661,6 +1661,7 @@ typedef void (* GLFWjoystickfun)(int,int);
typedef void (* GLFWuserdatafun)(unsigned long long, void*);
typedef void (* GLFWtickcallback)(void*);
typedef void (* GLFWactivationcallback)(GLFWwindow *window, const char *token, void *data);
typedef void (* GLFWwaylandinitialsizefun)(GLFWwindow *window, float xscale, float yscale, int *width, int *height);
typedef bool (* GLFWdrawtextfun)(GLFWwindow *window, const char *text, uint32_t fg, uint32_t bg, uint8_t *output_buf, size_t width, size_t height, float x_offset, float y_offset, size_t right_margin, bool is_single_glyph);
typedef char* (* GLFWcurrentselectionfun)(void);
typedef bool (* GLFWhascurrentselectionfun)(void);
@ -2479,10 +2480,6 @@ typedef void (*glfwCocoaCycleThroughOSWindows_func)(bool);
GFW_EXTERN glfwCocoaCycleThroughOSWindows_func glfwCocoaCycleThroughOSWindows_impl;
#define glfwCocoaCycleThroughOSWindows glfwCocoaCycleThroughOSWindows_impl
typedef void (*glfwCocoaSetWindowLevel_func)(GLFWwindow*, const char*);
GFW_EXTERN glfwCocoaSetWindowLevel_func glfwCocoaSetWindowLevel_impl;
#define glfwCocoaSetWindowLevel glfwCocoaSetWindowLevel_impl
typedef void (*glfwCocoaSetWindowChrome_func)(GLFWwindow*, unsigned int, bool, unsigned int, int, unsigned int, bool, int, float, bool);
GFW_EXTERN glfwCocoaSetWindowChrome_func glfwCocoaSetWindowChrome_impl;
#define glfwCocoaSetWindowChrome glfwCocoaSetWindowChrome_impl
@ -2491,6 +2488,10 @@ typedef void (*glfwCocoaRegisterMIMETypes_func)(GLFWwindow*, const char**, size_
GFW_EXTERN glfwCocoaRegisterMIMETypes_func glfwCocoaRegisterMIMETypes_impl;
#define glfwCocoaRegisterMIMETypes glfwCocoaRegisterMIMETypes_impl
typedef void (*glfwCocoaSetWindowLevel_func)(GLFWwindow*, const char*);
GFW_EXTERN glfwCocoaSetWindowLevel_func glfwCocoaSetWindowLevel_impl;
#define glfwCocoaSetWindowLevel glfwCocoaSetWindowLevel_impl
typedef const char* (*glfwGetPrimarySelectionString_func)(GLFWwindow*);
GFW_EXTERN glfwGetPrimarySelectionString_func glfwGetPrimarySelectionString_impl;
#define glfwGetPrimarySelectionString glfwGetPrimarySelectionString_impl
@ -2523,6 +2524,10 @@ typedef void (*glfwWaylandSetTitlebarHidden_func)(GLFWwindow*, bool);
GFW_EXTERN glfwWaylandSetTitlebarHidden_func glfwWaylandSetTitlebarHidden_impl;
#define glfwWaylandSetTitlebarHidden glfwWaylandSetTitlebarHidden_impl
typedef void (*glfwWaylandSetInitialWindowSizeCallback_func)(GLFWwaylandinitialsizefun);
GFW_EXTERN glfwWaylandSetInitialWindowSizeCallback_func glfwWaylandSetInitialWindowSizeCallback_impl;
#define glfwWaylandSetInitialWindowSizeCallback glfwWaylandSetInitialWindowSizeCallback_impl
typedef void (*glfwWaylandRedrawCSDWindowTitle_func)(GLFWwindow*);
GFW_EXTERN glfwWaylandRedrawCSDWindowTitle_func glfwWaylandRedrawCSDWindowTitle_impl;
#define glfwWaylandRedrawCSDWindowTitle glfwWaylandRedrawCSDWindowTitle_impl

View file

@ -1705,6 +1705,32 @@ os_window_update_size_increments(OSWindow *window) {
}
// Borrowed reference to the get_window_size callable, valid only for the
// duration of a create_os_window() call. Used by the Wayland initial-size hook
// below, which fires synchronously from inside glfwCreateWindow().
static PyObject *initial_window_size_py_callback = NULL;
static void
wayland_initial_size_callback(GLFWwindow *window UNUSED, float xscale, float yscale, int *width, int *height) {
// The compositor has told us the real scale before the window is mapped;
// recompute the cell-based logical size for that scale so the window is
// mapped at the correct size in the first place.
if (!initial_window_size_py_callback) return;
double xdpi, ydpi;
dpi_from_scale(xscale, yscale, &xdpi, &ydpi);
FONTS_DATA_HANDLE fonts_data = load_fonts_data(OPT(font_size), xdpi, ydpi);
if (!fonts_data) return;
PyObject *ret = PyObject_CallFunction(initial_window_size_py_callback, "IIddff",
fonts_data->fcm.cell_width, fonts_data->fcm.cell_height,
fonts_data->logical_dpi_x, fonts_data->logical_dpi_y, xscale, yscale);
if (ret) {
int w = PyLong_AsLong(PyTuple_GET_ITEM(ret, 0)), h = PyLong_AsLong(PyTuple_GET_ITEM(ret, 1));
if (!PyErr_Occurred() && w > 0 && h > 0) { *width = w; *height = h; }
else PyErr_Clear();
Py_DECREF(ret);
} else PyErr_Clear();
}
static PyObject*
create_os_window(PyObject UNUSED *self, PyObject *args, PyObject *kw) {
int x = INT_MIN, y = INT_MIN, window_state = WINDOW_NORMAL, disallow_override_title = 0;
@ -1822,7 +1848,16 @@ create_os_window(PyObject UNUSED *self, PyObject *args, PyObject *kw) {
if (!layer_shell_config_from_python(layer_shell_config, lsc)) return NULL;
lsc->expected.xscale = xscale; lsc->expected.yscale = yscale;
}
// On Wayland the true (fractional) scale is only known after the surface
// exists. Register a hook so the window is mapped at the correct cell-based
// size once the compositor reveals the scale, rather than being resized
// afterwards (which loses to the compositor's authoritative configure).
if (global_state.is_wayland && glfwWaylandSetInitialWindowSizeCallback) {
glfwWaylandSetInitialWindowSizeCallback(wayland_initial_size_callback);
initial_window_size_py_callback = get_window_size;
}
GLFWwindow *glfw_window = glfwCreateWindow(width, height, title, NULL, temp_window ? temp_window : common_context, lsc);
initial_window_size_py_callback = NULL;
if (temp_window) { glfwDestroyWindow(temp_window); temp_window = NULL; }
if (glfw_window == NULL) glfw_failure;
#undef glfw_failure