From 5d7167cbc98d1a47a940b4092b3505ebd2b95a95 Mon Sep 17 00:00:00 2001
From: svncibrahim <[EMAIL REDACTED]>
Date: Sun, 26 Jul 2026 09:56:01 +0300
Subject: [PATCH] wayland: Add opt-in border inset window properties
Applications drawing client-side decorations (shadows, invisible resize borders) can declare their insets via four new window properties. SDL sets the xdg geometry to the surface minus the insets and translates configure sizes, size limits and popup positioning accordingly. All-zero insets (the default) leave protocol traffic unchanged; the configure-size dedup becomes state-aware so a maximize/fullscreen toggle at unchanged dimensions is re-adopted rather than skipped.
---
docs/README-wayland.md | 10 ++
include/SDL3/SDL_video.h | 20 ++++
src/video/wayland/SDL_waylandwindow.c | 148 +++++++++++++++++++++-----
src/video/wayland/SDL_waylandwindow.h | 3 +
4 files changed, 157 insertions(+), 24 deletions(-)
diff --git a/docs/README-wayland.md b/docs/README-wayland.md
index 78fe380336e62..f52499acf0732 100644
--- a/docs/README-wayland.md
+++ b/docs/README-wayland.md
@@ -22,6 +22,16 @@ encounter limitations or behavior that is different from other windowing systems
if a plugin to generate native-looking decorations is not installed (i.e. the GTK plugin), the decorations will not
appear to be 'native'.
+### Border insets for client-side decorations
+
+- Applications drawing client-side decorations (shadows or invisible resize borders) can declare their insets via the
+ `SDL_PROP_WINDOW_WAYLAND_BORDER_INSET_*` window properties when the window is created with the `SDL_PROP_WINDOW_CREATE_WAYLAND_ENABLE_INSETS_BOOLEAN` property set to `true`. SDL then sets the xdg window geometry to the surface minus
+ the insets (in points, regardless of pixel density) and translates configure sizes and size limits accordingly.
+ Maximized and fullscreen windows are always sized exactly as configured, while tiled windows keep their insets.
+ Changes take effect on the next configure. Only xdg-toplevel windows are affected; popup and custom-role surfaces
+ ignore the properties, as does libdecor, which manages the window geometry itself
+ (see `SDL_HINT_VIDEO_WAYLAND_ALLOW_LIBDECOR`).
+
### Windows do not appear immediately after creation
- Wayland requires that the application initially present a buffer before the window becomes visible. Additionally,
diff --git a/include/SDL3/SDL_video.h b/include/SDL3/SDL_video.h
index c62b294728ebb..9259421848577 100644
--- a/include/SDL3/SDL_video.h
+++ b/include/SDL3/SDL_video.h
@@ -1392,6 +1392,9 @@ extern SDL_DECLSPEC SDL_Window * SDLCALL SDL_CreatePopupWindow(SDL_Window *paren
* - `SDL_PROP_WINDOW_CREATE_WAYLAND_WL_SURFACE_POINTER` - the wl_surface
* associated with the window, if you want to wrap an existing window. See
* [README-wayland](README-wayland) for more information.
+ * - `SDL_PROP_WINDOW_CREATE_WAYLAND_ENABLE_INSETS_BOOLEAN` - true if the
+ * application wants to enable custom border inset window properties. See
+ * [README-wayland](README-wayland) for more information.
*
* These are additional supported properties on Windows:
*
@@ -1494,6 +1497,7 @@ extern SDL_DECLSPEC SDL_Window * SDLCALL SDL_CreateWindowWithProperties(SDL_Prop
#define SDL_PROP_WINDOW_CREATE_WAYLAND_CREATE_EGL_WINDOW_BOOLEAN "SDL.window.create.wayland.create_egl_window"
#define SDL_PROP_WINDOW_CREATE_WAYLAND_WINDOW_ID_STRING "SDL.window.create.wayland.window_id"
#define SDL_PROP_WINDOW_CREATE_WAYLAND_WL_SURFACE_POINTER "SDL.window.create.wayland.wl_surface"
+#define SDL_PROP_WINDOW_CREATE_WAYLAND_ENABLE_INSETS_BOOLEAN "SDL.window.create.wayland.enable_insets"
#define SDL_PROP_WINDOW_CREATE_WIN32_HWND_POINTER "SDL.window.create.win32.hwnd"
#define SDL_PROP_WINDOW_CREATE_WIN32_PIXEL_FORMAT_HWND_POINTER "SDL.window.create.win32.pixel_format_hwnd"
#define SDL_PROP_WINDOW_CREATE_WIN32_STYLE_EX_NUMBER "SDL.window.create.win32.style_ex"
@@ -1653,6 +1657,10 @@ extern SDL_DECLSPEC SDL_Window * SDLCALL SDL_GetWindowParent(SDL_Window *window)
* show/hide calls. They will be null if the window is hidden and must be
* queried each time it is shown.
*
+ * Note: The `border_inset_*` properties can be set by the application when
+ * client-side decorations such as shadows or invisible resize borders
+ * extend beyond the visible frame (see docs/README-wayland.md for details).
+ *
* - `SDL_PROP_WINDOW_WAYLAND_DISPLAY_POINTER`: the wl_display associated with
* the window
* - `SDL_PROP_WINDOW_WAYLAND_SURFACE_POINTER`: the wl_surface associated with
@@ -1677,6 +1685,14 @@ extern SDL_DECLSPEC SDL_Window * SDLCALL SDL_GetWindowParent(SDL_Window *window)
* associated with the window
* - `SDL_PROP_WINDOW_WAYLAND_XDG_POSITIONER_POINTER`: the xdg_positioner
* associated with the window, in popup mode
+ * - `SDL_PROP_WINDOW_WAYLAND_BORDER_INSET_LEFT_NUMBER`: the left border inset
+ * of the visible window
+ * - `SDL_PROP_WINDOW_WAYLAND_BORDER_INSET_TOP_NUMBER`: the top border inset
+ * of the visible window
+ * - `SDL_PROP_WINDOW_WAYLAND_BORDER_INSET_RIGHT_NUMBER`: the right border
+ * inset of the visible window
+ * - `SDL_PROP_WINDOW_WAYLAND_BORDER_INSET_BOTTOM_NUMBER`: the bottom border
+ * inset of the visible window
*
* On X11:
*
@@ -1748,6 +1764,10 @@ extern SDL_DECLSPEC SDL_PropertiesID SDLCALL SDL_GetWindowProperties(SDL_Window
#define SDL_PROP_WINDOW_WAYLAND_XDG_TOPLEVEL_EXPORT_HANDLE_STRING "SDL.window.wayland.xdg_toplevel_export_handle"
#define SDL_PROP_WINDOW_WAYLAND_XDG_POPUP_POINTER "SDL.window.wayland.xdg_popup"
#define SDL_PROP_WINDOW_WAYLAND_XDG_POSITIONER_POINTER "SDL.window.wayland.xdg_positioner"
+#define SDL_PROP_WINDOW_WAYLAND_BORDER_INSET_LEFT_NUMBER "SDL.window.wayland.border_inset_left"
+#define SDL_PROP_WINDOW_WAYLAND_BORDER_INSET_TOP_NUMBER "SDL.window.wayland.border_inset_top"
+#define SDL_PROP_WINDOW_WAYLAND_BORDER_INSET_RIGHT_NUMBER "SDL.window.wayland.border_inset_right"
+#define SDL_PROP_WINDOW_WAYLAND_BORDER_INSET_BOTTOM_NUMBER "SDL.window.wayland.border_inset_bottom"
#define SDL_PROP_WINDOW_X11_DISPLAY_POINTER "SDL.window.x11.display"
#define SDL_PROP_WINDOW_X11_SCREEN_NUMBER "SDL.window.x11.screen"
#define SDL_PROP_WINDOW_X11_WINDOW_NUMBER "SDL.window.x11.window"
diff --git a/src/video/wayland/SDL_waylandwindow.c b/src/video/wayland/SDL_waylandwindow.c
index adfd7a1e7b4fe..e3b0ebafec622 100644
--- a/src/video/wayland/SDL_waylandwindow.c
+++ b/src/video/wayland/SDL_waylandwindow.c
@@ -142,6 +142,30 @@ static enum WaylandModeScale GetModeScaleMethod(void)
return scale_mode;
}
+typedef struct BorderInsets
+{
+ int left;
+ int top;
+ int right;
+ int bottom;
+} BorderInsets;
+
+// The border insets from the window properties; they only apply to xdg-toplevel windows.
+static void GetBorderInsets(SDL_Window *window, BorderInsets *insets)
+{
+ SDL_zerop(insets);
+ if (!window->internal->enable_insets) {
+ return;
+ }
+ if (window->internal->shell_surface_type == WAYLAND_SHELL_SURFACE_TYPE_XDG_TOPLEVEL) {
+ const SDL_PropertiesID props = SDL_GetWindowProperties(window);
+ insets->left = (int)SDL_clamp(SDL_GetNumberProperty(props, SDL_PROP_WINDOW_WAYLAND_BORDER_INSET_LEFT_NUMBER, 0), 0, SDL_MAX_SINT16);
+ insets->top = (int)SDL_clamp(SDL_GetNumberProperty(props, SDL_PROP_WINDOW_WAYLAND_BORDER_INSET_TOP_NUMBER, 0), 0, SDL_MAX_SINT16);
+ insets->right = (int)SDL_clamp(SDL_GetNumberProperty(props, SDL_PROP_WINDOW_WAYLAND_BORDER_INSET_RIGHT_NUMBER, 0), 0, SDL_MAX_SINT16);
+ insets->bottom = (int)SDL_clamp(SDL_GetNumberProperty(props, SDL_PROP_WINDOW_WAYLAND_BORDER_INSET_BOTTOM_NUMBER, 0), 0, SDL_MAX_SINT16);
+ }
+}
+
static void SetMinMaxDimensions(SDL_Window *window)
{
SDL_WindowData *wind = window->internal;
@@ -209,6 +233,19 @@ static void SetMinMaxDimensions(SDL_Window *window)
if (!wind->shell_surface.xdg.toplevel.xdg_toplevel) {
return; // Can't do anything yet, wait for ShowWindow
}
+ /* The limits are in window geometry space; keep an existing minimum at 1 or more,
+ * or the window can be spuriously closed.
+ */
+ BorderInsets insets;
+ GetBorderInsets(window, &insets);
+ min_width = SDL_max(min_width - (insets.left + insets.right), min_width ? 1 : 0);
+ min_height = SDL_max(min_height - (insets.top + insets.bottom), min_height ? 1 : 0);
+ if (max_width) {
+ max_width = SDL_max(max_width - (insets.left + insets.right), 1);
+ }
+ if (max_height) {
+ max_height = SDL_max(max_height - (insets.top + insets.bottom), 1);
+ }
xdg_toplevel_set_min_size(wind->shell_surface.xdg.toplevel.xdg_toplevel,
min_width,
min_height);
@@ -255,9 +292,21 @@ static void EnsurePopupPositionIsValid(SDL_Window *window, int *x, int *y)
}
}
+// The window geometry: the explicitly set one, or the full surface if SDL has never set it.
+static void GetWindowGeometry(SDL_WindowData *wind, SDL_Rect *geometry)
+{
+ *geometry = wind->explicit_geometry;
+ if (SDL_RectEmpty(geometry)) {
+ geometry->x = 0;
+ geometry->y = 0;
+ geometry->w = wind->current.logical_width;
+ geometry->h = wind->current.logical_height;
+ }
+}
+
static void AdjustPopupOffset(SDL_Window *popup, int *x, int *y)
{
- // Adjust the popup positioning, if necessary
+ // Positioner offsets are relative to the origin of the parent's window geometry.
#ifdef HAVE_LIBDECOR_H
if (popup->parent->internal->shell_surface_type == WAYLAND_SHELL_SURFACE_TYPE_LIBDECOR) {
int adj_x, adj_y;
@@ -265,8 +314,12 @@ static void AdjustPopupOffset(SDL_Window *popup, int *x, int *y)
*x, *y, &adj_x, &adj_y);
*x = adj_x;
*y = adj_y;
- }
+ } else
#endif
+ {
+ *x -= popup->parent->internal->explicit_geometry.x;
+ *y -= popup->parent->internal->explicit_geometry.y;
+ }
}
static void RepositionPopup(SDL_Window *window, bool use_current_position)
@@ -285,7 +338,9 @@ static void RepositionPopup(SDL_Window *window, bool use_current_position)
y = PixelToPoint(window->parent, y);
}
AdjustPopupOffset(window, &x, &y);
- xdg_positioner_set_anchor_rect(wind->shell_surface.xdg.popup.xdg_positioner, 0, 0, window->parent->internal->current.logical_width, window->parent->internal->current.logical_height);
+ SDL_Rect parent_geometry;
+ GetWindowGeometry(window->parent->internal, &parent_geometry);
+ xdg_positioner_set_anchor_rect(wind->shell_surface.xdg.popup.xdg_positioner, 0, 0, parent_geometry.w, parent_geometry.h);
xdg_positioner_set_size(wind->shell_surface.xdg.popup.xdg_positioner, wind->current.logical_width, wind->current.logical_height);
xdg_positioner_set_offset(wind->shell_surface.xdg.popup.xdg_positioner, x, y);
xdg_popup_reposition(wind->shell_surface.xdg.popup.xdg_popup,
@@ -398,7 +453,8 @@ static void ConfigureWindowGeometry(SDL_Window *window)
viewport_height = data->requested.pixel_height;
}
- if (data->shell_surface_status != WAYLAND_SHELL_SURFACE_STATUS_HIDDEN &&
+ if (!data->enable_insets &&
+ data->shell_surface_status != WAYLAND_SHELL_SURFACE_STATUS_HIDDEN &&
data->viewport && data->waylandData->subcompositor && !data->floating && !data->is_fullscreen) {
int min_width, min_height, max_width, max_height;
if (window->flags & SDL_WINDOW_RESIZABLE) {
@@ -523,7 +579,7 @@ static void ConfigureWindowGeometry(SDL_Window *window)
/* Calculate the mask size and offset.
* Fullscreen windows are centered and masked automatically by the compositor, unless it lacks the capability.
*/
- if (data->viewport && data->waylandData->subcompositor && (!data->is_fullscreen || ShouldMaskFullscreen()) &&
+ if (!data->enable_insets && data->viewport && data->waylandData->subcompositor && (!data->is_fullscreen || ShouldMaskFullscreen()) &&
(viewport_width != data->current.logical_width || viewport_height != data->current.logical_height)) {
struct wl_buffer *old_buffer = NULL;
@@ -588,19 +644,37 @@ static void ConfigureWindowGeometry(SDL_Window *window)
data->mask.mapped = false;
}
- /*
- * The surface geometry, opaque region and pointer confinement region only
- * need to be recalculated if the output size has changed.
- */
- if (window_size_changed) {
- /* XXX: This is a hack and only set on the xdg-toplevel path when viewports
- * aren't supported to avoid a potential protocol violation if a buffer
- * with an old size is committed.
+ if (data->shell_surface_type == WAYLAND_SHELL_SURFACE_TYPE_XDG_TOPLEVEL && data->shell_surface.xdg.surface) {
+ /* The window geometry is the surface minus the declared border insets. They don't apply
+ * when an exact size is required (maximized/fullscreen).
+ */
+ BorderInsets insets;
+ GetBorderInsets(window, &insets);
+ const bool use_insets = (insets.left || insets.top || insets.right || insets.bottom) &&
+ !(window->flags & (SDL_WINDOW_MAXIMIZED | SDL_WINDOW_FULLSCREEN));
+ SDL_Rect geometry = { 0, 0, data->current.logical_width, data->current.logical_height };
+ if (use_insets) {
+ geometry.x = insets.left;
+ geometry.y = insets.top;
+ geometry.w = SDL_max(geometry.w - (insets.left + insets.right), 1);
+ geometry.h = SDL_max(geometry.h - (insets.top + insets.bottom), 1);
+ }
+ /* An explicitly set geometry does not track the surface, so it must be kept in sync
+ * once set. Otherwise this is only done when viewports aren't supported and the size
+ * has changed (XXX: a hack) to avoid a potential protocol violation if a buffer with
+ * an old size is committed.
*/
- if (!data->viewport && data->shell_surface_type == WAYLAND_SHELL_SURFACE_TYPE_XDG_TOPLEVEL && data->shell_surface.xdg.surface) {
- xdg_surface_set_window_geometry(data->shell_surface.xdg.surface, 0, 0, data->current.logical_width, data->current.logical_height);
+ if ((use_insets || !SDL_RectEmpty(&data->explicit_geometry) || (!data->viewport && window_size_changed)) &&
+ !SDL_RectsEqual(&geometry, &data->explicit_geometry)) {
+ xdg_surface_set_window_geometry(data->shell_surface.xdg.surface, geometry.x, geometry.y, geometry.w, geometry.h);
+ data->explicit_geometry = geometry;
}
+ }
+ /* The opaque region and pointer confinement region only need to be
+ * recalculated if the output size has changed.
+ */
+ if (window_size_changed) {
if (is_opaque) {
SetSurfaceOpaqueRegion(data->surface, viewport_width, viewport_height);
} else {
@@ -1109,8 +1183,22 @@ static void handle_xdg_toplevel_configure(void *data,
}
}
+ /* Configure sizes are in window geometry space; add the border insets, if any, to get
+ * the surface size. Maximized and fullscreen windows must use the configured size as-is,
+ * so a change in inset applicability requires re-adopting even an unchanged size, and
+ * sizes adopted from the cached window size are converted back to geometry space for
+ * the last_configure store.
+ */
+ const bool insets_apply = !fullscreen && !maximized;
+
// When resizing, dimensions other than 0 are a maximum.
- const bool new_configure_size = width != wind->last_configure.width || height != wind->last_configure.height;
+ const bool new_configure_size = width != wind->last_configure.width || height != wind->last_configure.height ||
+ insets_apply != wind->last_configure.insets_apply;
+
+ BorderInsets insets = { 0, 0, 0, 0 };
+ if (insets_apply) {
+ GetBorderInsets(window, &insets);
+ }
DetermineResizeAxis(wind, width, height, resizing);
UpdateWindowFullscreen(window, fullscreen);
@@ -1159,15 +1247,16 @@ static void handle_xdg_toplevel_configure(void *data,
wind->requested.pixel_width = width;
width = wind->requested.logical_width = PixelToPoint(window, width);
}
+ width = SDL_max(width - (insets.left + insets.right), 1);
} else if (new_configure_size) {
/* Don't apply the supplied dimensions if they haven't changed from the last configuration
* event, or a newer size set programmatically can be overwritten by old data.
*/
- wind->requested.logical_width = width;
+ wind->requested.logical_width = width + (insets.left + insets.right);
if (wind->scale_to_display) {
- wind->requested.pixel_width = PointToPixel(window, width);
+ wind->requested.pixel_width = PointToPixel(window, wind->requested.logical_width);
}
}
if (!height) {
@@ -1191,14 +1280,15 @@ static void handle_xdg_toplevel_configure(void *data,
wind->requested.pixel_height = height;
height = wind->requested.logical_height = PixelToPoint(window, height);
}
+ height = SDL_max(height - (insets.top + insets.bottom), 1);
} else if (new_configure_size) {
/* Don't apply the supplied dimensions if they haven't changed from the last configuration
* event, or a newer size set programmatically can be overwritten by old data.
*/
- wind->requested.logical_height = height;
+ wind->requested.logical_height = height + (insets.top + insets.bottom);
if (wind->scale_to_display) {
- wind->requested.pixel_height = PointToPixel(window, height);
+ wind->requested.pixel_height = PointToPixel(window, wind->requested.logical_height);
}
}
} else {
@@ -1214,6 +1304,8 @@ static void handle_xdg_toplevel_configure(void *data,
width = wind->requested.logical_width = PixelToPoint(window, window->floating.w);
height = wind->requested.logical_height = PixelToPoint(window, window->floating.h);
}
+ width = SDL_max(width - (insets.left + insets.right), 1);
+ height = SDL_max(height - (insets.top + insets.bottom), 1);
}
/* Notes on the spec and implementations:
@@ -1256,6 +1348,7 @@ static void handle_xdg_toplevel_configure(void *data,
wind->last_configure.width = width;
wind->last_configure.height = height;
+ wind->last_configure.insets_apply = insets_apply;
wind->floating = floating;
wind->suspended = suspended;
wind->active = active;
@@ -1804,10 +1897,13 @@ static void Wayland_HandlePreferredScaleChanged(SDL_WindowData *window_data, dou
* incorrectly change size when moved between displays with differing scale factors.
*
* Store the last requested logical size as the last configure size, so a configure event
- * with the old size will be ignored.
+ * with the old size will be ignored. Configure sizes are in window geometry space,
+ * so subtract any border insets.
*/
- window_data->last_configure.width = window_data->requested.logical_width;
- window_data->last_configure.height = window_data->requested.logical_height;
+ BorderInsets insets;
+ GetBorderInsets(window_data->sdlwindow, &insets);
+ window_data->last_configure.width = SDL_max(window_data->requested.logical_width - (insets.left + insets.right), 1);
+ window_data->last_configure.height = SDL_max(window_data->requested.logical_height - (insets.top + insets.bottom), 1);
window_data->requested.logical_width = PixelToPoint(window_data->sdlwindow, window_data->requested.pixel_width);
window_data->requested.logical_height = PixelToPoint(window_data->sdlwindow, window_data->requested.pixel_height);
@@ -2305,7 +2401,9 @@ void Wayland_ShowWindow(SDL_VideoDevice *_this, SDL_Window *window)
// Set up the positioner for the popup and configure the constraints
data->shell_surface.xdg.popup.xdg_positioner = xdg_wm_base_create_positioner(c->shell.xdg);
xdg_positioner_set_anchor(data->shell_surface.xdg.popup.xdg_positioner, XDG_POSITIONER_ANCHOR_TOP_LEFT);
- xdg_positioner_set_anchor_rect(data->shell_surface.xdg.popup.xdg_positioner, 0, 0, parent->internal->current.logical_width, parent->internal->current.logical_height);
+ SDL_Rect parent_geometry;
+ GetWindowGeometry(parent->internal, &parent_geometry);
+ xdg_positioner_set_anchor_rect(data->shell_surface.xdg.popup.xdg_positioner, 0, 0, parent_geometry.w, parent_geometry.h);
const Uint32 constraint = window->constrain_popup ? (XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_SLIDE_X | XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_SLIDE_Y) : XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_NONE;
xdg_positioner_set_constraint_adjustment(data->shell_surface.xdg.popup.xdg_positioner, constraint);
@@ -2606,6 +2704,7 @@ void Wayland_HideWindow(SDL_VideoDevice *_this, SDL_Window *window)
Wayland_DestroyToplevelSession(wind);
SDL_zero(wind->shell_surface);
+ SDL_zero(wind->explicit_geometry);
wind->show_hide_sync_required = true;
struct wl_callback *cb = wl_display_sync(_this->internal->display);
wl_callback_add_listener(cb, &show_hide_sync_listener, (void *)((uintptr_t)window->id));
@@ -3068,6 +3167,7 @@ bool Wayland_CreateWindow(SDL_VideoDevice *_this, SDL_Window *window, SDL_Proper
}
window->internal = data;
+ data->enable_insets = SDL_GetBooleanProperty(create_props, SDL_PROP_WINDOW_CREATE_WAYLAND_ENABLE_INSETS_BOOLEAN, false);
if (window->x == SDL_WINDOWPOS_UNDEFINED) {
window->x = 0;
diff --git a/src/video/wayland/SDL_waylandwindow.h b/src/video/wayland/SDL_waylandwindow.h
index 427c42713e568..1df119ac64930 100644
--- a/src/video/wayland/SDL_waylandwindow.h
+++ b/src/video/wayland/SDL_waylandwindow.h
@@ -185,6 +185,7 @@ struct SDL_WindowData
{
int width;
int height;
+ bool insets_apply;
} last_configure;
// System enforced window size limits.
@@ -222,6 +223,7 @@ struct SDL_WindowData
bool active;
} text_input_props;
+ SDL_Rect explicit_geometry;
SDL_DisplayID last_displayID;
int pending_state_deadline_count;
Uint64 last_focus_event_time_ns;
@@ -245,6 +247,7 @@ struct SDL_WindowData
bool reparenting_required;
bool double_buffer;
bool accepts_drag_and_drop;
+ bool enable_insets;
SDL_HitTestResult hit_test_result;