1/*
2 Simple DirectMedia Layer
3 Copyright (C) 1997-2025 Sam Lantinga <slouken@libsdl.org>
4
5 This software is provided 'as-is', without any express or implied
6 warranty. In no event will the authors be held liable for any damages
7 arising from the use of this software.
8
9 Permission is granted to anyone to use this software for any purpose,
10 including commercial applications, and to alter it and redistribute it
11 freely, subject to the following restrictions:
12
13 1. The origin of this software must not be misrepresented; you must not
14 claim that you wrote the original software. If you use this software
15 in a product, an acknowledgment in the product documentation would be
16 appreciated but is not required.
17 2. Altered source versions must be plainly marked as such, and must not be
18 misrepresented as being the original software.
19 3. This notice may not be removed or altered from any source distribution.
20*/
21
22#include "SDL_internal.h"
23
24#ifdef SDL_VIDEO_DRIVER_WAYLAND
25
26#include <sys/mman.h>
27
28#include "../SDL_sysvideo.h"
29#include "../../events/SDL_events_c.h"
30#include "../../core/unix/SDL_appid.h"
31#include "../SDL_egl_c.h"
32#include "SDL_waylandevents_c.h"
33#include "SDL_waylandwindow.h"
34#include "SDL_waylandvideo.h"
35#include "../../SDL_hints_c.h"
36#include "SDL_waylandcolor.h"
37
38#include "alpha-modifier-v1-client-protocol.h"
39#include "xdg-shell-client-protocol.h"
40#include "xdg-decoration-unstable-v1-client-protocol.h"
41#include "idle-inhibit-unstable-v1-client-protocol.h"
42#include "xdg-activation-v1-client-protocol.h"
43#include "viewporter-client-protocol.h"
44#include "fractional-scale-v1-client-protocol.h"
45#include "xdg-foreign-unstable-v2-client-protocol.h"
46#include "xdg-dialog-v1-client-protocol.h"
47#include "frog-color-management-v1-client-protocol.h"
48#include "xdg-toplevel-icon-v1-client-protocol.h"
49#include "color-management-v1-client-protocol.h"
50
51#ifdef HAVE_LIBDECOR_H
52#include <libdecor.h>
53#endif
54
55static double GetWindowScale(SDL_Window *window)
56{
57 return (window->flags & SDL_WINDOW_HIGH_PIXEL_DENSITY) || window->internal->scale_to_display ? window->internal->scale_factor : 1.0;
58}
59
60// These are point->pixel->point round trip safe; the inverse is not round trip safe due to rounding.
61static int PointToPixel(SDL_Window *window, int point)
62{
63 /* Rounds halfway away from zero as per the Wayland fractional scaling protocol spec.
64 * Wayland scale units are in units of 1/120, so the offset is required to correct for
65 * rounding errors when using certain scale values.
66 */
67 return point ? SDL_max((int)SDL_lround((double)point * GetWindowScale(window) + 1e-6), 1) : 0;
68}
69
70static int PixelToPoint(SDL_Window *window, int pixel)
71{
72 return pixel ? SDL_max((int)SDL_lround((double)pixel / GetWindowScale(window)), 1) : 0;
73}
74
75/* According to the Wayland spec:
76 *
77 * "If the [fullscreen] surface doesn't cover the whole output, the compositor will
78 * position the surface in the center of the output and compensate with border fill
79 * covering the rest of the output. The content of the border fill is undefined, but
80 * should be assumed to be in some way that attempts to blend into the surrounding area
81 * (e.g. solid black)."
82 *
83 * - KDE, as of 5.27, still doesn't do this
84 * - GNOME prior to 43 didn't do this (older versions are still found in many LTS distros)
85 *
86 * Default to 'stretch' for now, until things have moved forward enough that the default
87 * can be changed to 'aspect'.
88 */
89enum WaylandModeScale
90{
91 WAYLAND_MODE_SCALE_UNDEFINED,
92 WAYLAND_MODE_SCALE_ASPECT,
93 WAYLAND_MODE_SCALE_STRETCH,
94 WAYLAND_MODE_SCALE_NONE
95};
96
97static enum WaylandModeScale GetModeScaleMethod(void)
98{
99 static enum WaylandModeScale scale_mode = WAYLAND_MODE_SCALE_UNDEFINED;
100
101 if (scale_mode == WAYLAND_MODE_SCALE_UNDEFINED) {
102 const char *scale_hint = SDL_GetHint(SDL_HINT_VIDEO_WAYLAND_MODE_SCALING);
103
104 if (scale_hint) {
105 if (!SDL_strcasecmp(scale_hint, "aspect")) {
106 scale_mode = WAYLAND_MODE_SCALE_ASPECT;
107 } else if (!SDL_strcasecmp(scale_hint, "none")) {
108 scale_mode = WAYLAND_MODE_SCALE_NONE;
109 } else {
110 scale_mode = WAYLAND_MODE_SCALE_STRETCH;
111 }
112 } else {
113 scale_mode = WAYLAND_MODE_SCALE_STRETCH;
114 }
115 }
116
117 return scale_mode;
118}
119
120static void GetBufferSize(SDL_Window *window, int *width, int *height)
121{
122 SDL_WindowData *data = window->internal;
123 int buf_width;
124 int buf_height;
125
126 // Exclusive fullscreen modes always have a pixel density of 1
127 if (data->is_fullscreen && window->fullscreen_exclusive) {
128 buf_width = window->current_fullscreen_mode.w;
129 buf_height = window->current_fullscreen_mode.h;
130 } else if (!data->scale_to_display) {
131 // Round fractional backbuffer sizes halfway away from zero.
132 buf_width = PointToPixel(window, data->requested.logical_width);
133 buf_height = PointToPixel(window, data->requested.logical_height);
134 } else {
135 buf_width = data->requested.pixel_width;
136 buf_height = data->requested.pixel_height;
137 }
138
139 if (width) {
140 *width = buf_width;
141 }
142 if (height) {
143 *height = buf_height;
144 }
145}
146
147static void SetMinMaxDimensions(SDL_Window *window)
148{
149 SDL_WindowData *wind = window->internal;
150 int min_width, min_height, max_width, max_height;
151
152 if ((window->flags & SDL_WINDOW_FULLSCREEN) || wind->fullscreen_deadline_count) {
153 min_width = 0;
154 min_height = 0;
155 max_width = 0;
156 max_height = 0;
157 } else if (window->flags & SDL_WINDOW_RESIZABLE) {
158 int adj_w = SDL_max(window->min_w, wind->system_limits.min_width);
159 int adj_h = SDL_max(window->min_h, wind->system_limits.min_height);
160 if (wind->scale_to_display) {
161 adj_w = PixelToPoint(window, adj_w);
162 adj_h = PixelToPoint(window, adj_h);
163 }
164 min_width = adj_w;
165 min_height = adj_h;
166
167 adj_w = window->max_w ? SDL_max(window->max_w, wind->system_limits.min_width) : 0;
168 adj_h = window->max_h ? SDL_max(window->max_h, wind->system_limits.min_height) : 0;
169 if (wind->scale_to_display) {
170 adj_w = PixelToPoint(window, adj_w);
171 adj_h = PixelToPoint(window, adj_h);
172 }
173 max_width = adj_w;
174 max_height = adj_h;
175 } else {
176 min_width = wind->current.logical_width;
177 min_height = wind->current.logical_height;
178 max_width = wind->current.logical_width;
179 max_height = wind->current.logical_height;
180 }
181
182#ifdef HAVE_LIBDECOR_H
183 if (wind->shell_surface_type == WAYLAND_SHELL_SURFACE_TYPE_LIBDECOR) {
184 if (!wind->shell_surface.libdecor.initial_configure_seen || !wind->shell_surface.libdecor.frame) {
185 return; // Can't do anything yet, wait for ShowWindow
186 }
187 /* No need to change these values if the window is non-resizable,
188 * as libdecor will just overwrite them internally.
189 */
190 if (libdecor_frame_has_capability(wind->shell_surface.libdecor.frame, LIBDECOR_ACTION_RESIZE)) {
191 libdecor_frame_set_min_content_size(wind->shell_surface.libdecor.frame,
192 min_width,
193 min_height);
194 libdecor_frame_set_max_content_size(wind->shell_surface.libdecor.frame,
195 max_width,
196 max_height);
197 }
198 } else
199#endif
200 if (wind->shell_surface_type == WAYLAND_SHELL_SURFACE_TYPE_XDG_TOPLEVEL) {
201 if (wind->shell_surface.xdg.toplevel.xdg_toplevel == NULL) {
202 return; // Can't do anything yet, wait for ShowWindow
203 }
204 xdg_toplevel_set_min_size(wind->shell_surface.xdg.toplevel.xdg_toplevel,
205 min_width,
206 min_height);
207 xdg_toplevel_set_max_size(wind->shell_surface.xdg.toplevel.xdg_toplevel,
208 max_width,
209 max_height);
210 }
211}
212
213static void EnsurePopupPositionIsValid(SDL_Window *window, int *x, int *y)
214{
215 int adj_count = 0;
216
217 /* Per the xdg-positioner spec, child popup windows must intersect or at
218 * least be partially adjacent to the parent window.
219 *
220 * Failure to ensure this on a compositor that enforces this restriction
221 * can result in behavior ranging from the window being spuriously closed
222 * to a protocol violation.
223 */
224 if (*x + window->w < 0) {
225 *x = -window->w;
226 ++adj_count;
227 }
228 if (*y + window->h < 0) {
229 *y = -window->h;
230 ++adj_count;
231 }
232 if (*x > window->parent->w) {
233 *x = window->parent->w;
234 ++adj_count;
235 }
236 if (*y > window->parent->h) {
237 *y = window->parent->h;
238 ++adj_count;
239 }
240
241 /* If adjustment was required on the x and y axes, the popup is aligned with
242 * the parent corner-to-corner and is neither overlapping nor adjacent, so it
243 * must be nudged by 1 to be considered adjacent.
244 */
245 if (adj_count > 1) {
246 *x += *x < 0 ? 1 : -1;
247 }
248}
249
250static void AdjustPopupOffset(SDL_Window *popup, int *x, int *y)
251{
252 // Adjust the popup positioning, if necessary
253#ifdef HAVE_LIBDECOR_H
254 if (popup->parent->internal->shell_surface_type == WAYLAND_SHELL_SURFACE_TYPE_LIBDECOR) {
255 int adj_x, adj_y;
256 libdecor_frame_translate_coordinate(popup->parent->internal->shell_surface.libdecor.frame,
257 *x, *y, &adj_x, &adj_y);
258 *x = adj_x;
259 *y = adj_y;
260 }
261#endif
262}
263
264static void RepositionPopup(SDL_Window *window, bool use_current_position)
265{
266 SDL_WindowData *wind = window->internal;
267
268 if (wind->shell_surface_type == WAYLAND_SHELL_SURFACE_TYPE_XDG_POPUP &&
269 wind->shell_surface.xdg.popup.xdg_positioner &&
270 xdg_popup_get_version(wind->shell_surface.xdg.popup.xdg_popup) >= XDG_POPUP_REPOSITION_SINCE_VERSION) {
271 int x = use_current_position ? window->x : window->pending.x;
272 int y = use_current_position ? window->y : window->pending.y;
273
274 EnsurePopupPositionIsValid(window, &x, &y);
275 if (wind->scale_to_display) {
276 x = PixelToPoint(window->parent, x);
277 y = PixelToPoint(window->parent, y);
278 }
279 AdjustPopupOffset(window, &x, &y);
280 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);
281 xdg_positioner_set_size(wind->shell_surface.xdg.popup.xdg_positioner, wind->current.logical_width, wind->current.logical_height);
282 xdg_positioner_set_offset(wind->shell_surface.xdg.popup.xdg_positioner, x, y);
283 xdg_popup_reposition(wind->shell_surface.xdg.popup.xdg_popup,
284 wind->shell_surface.xdg.popup.xdg_positioner,
285 0);
286 }
287}
288
289static void SetSurfaceOpaqueRegion(SDL_WindowData *wind, bool is_opaque)
290{
291 SDL_VideoData *viddata = wind->waylandData;
292
293 if (is_opaque) {
294 struct wl_region *region = wl_compositor_create_region(viddata->compositor);
295 wl_region_add(region, 0, 0,
296 wind->current.logical_width, wind->current.logical_height);
297 wl_surface_set_opaque_region(wind->surface, region);
298 wl_region_destroy(region);
299 } else {
300 wl_surface_set_opaque_region(wind->surface, NULL);
301 }
302}
303
304static bool ConfigureWindowGeometry(SDL_Window *window)
305{
306 SDL_WindowData *data = window->internal;
307 const double scale_factor = GetWindowScale(window);
308 const int old_pixel_width = data->current.pixel_width;
309 const int old_pixel_height = data->current.pixel_height;
310 int window_width, window_height;
311 bool window_size_changed;
312
313 // Throttle interactive resize events to once per refresh cycle to prevent lag.
314 if (data->resizing) {
315 data->resizing = false;
316
317 if (data->drop_interactive_resizes) {
318 return false;
319 } else {
320 data->drop_interactive_resizes = true;
321 }
322 }
323
324 // Set the drawable backbuffer size.
325 GetBufferSize(window, &data->current.pixel_width, &data->current.pixel_height);
326 const bool buffer_size_changed = data->current.pixel_width != old_pixel_width ||
327 data->current.pixel_height != old_pixel_height;
328
329 if (data->egl_window && buffer_size_changed) {
330 WAYLAND_wl_egl_window_resize(data->egl_window,
331 data->current.pixel_width,
332 data->current.pixel_height,
333 0, 0);
334 }
335
336 if (data->is_fullscreen && window->fullscreen_exclusive) {
337 int output_width;
338 int output_height;
339 window_width = window->current_fullscreen_mode.w;
340 window_height = window->current_fullscreen_mode.h;
341
342 output_width = data->requested.logical_width;
343 output_height = data->requested.logical_height;
344
345 switch (GetModeScaleMethod()) {
346 case WAYLAND_MODE_SCALE_NONE:
347 /* The Wayland spec states that the advertised fullscreen dimensions are a maximum.
348 * Windows can request a smaller size, but exceeding these dimensions is a protocol violation,
349 * thus, modes that exceed the output size still need to be scaled with a viewport.
350 */
351 if (window_width <= output_width && window_height <= output_height) {
352 output_width = window_width;
353 output_height = window_height;
354
355 break;
356 }
357 SDL_FALLTHROUGH;
358 case WAYLAND_MODE_SCALE_ASPECT:
359 {
360 const float output_ratio = (float)output_width / (float)output_height;
361 const float mode_ratio = (float)window_width / (float)window_height;
362
363 if (output_ratio > mode_ratio) {
364 output_width = SDL_lroundf((float)window_width * ((float)output_height / (float)window_height));
365 } else if (output_ratio < mode_ratio) {
366 output_height = SDL_lroundf((float)window_height * ((float)output_width / (float)window_width));
367 }
368 } break;
369 default:
370 break;
371 }
372
373 window_size_changed = window_width != window->w || window_height != window->h ||
374 data->current.logical_width != output_width || data->current.logical_height != output_height;
375
376 if (window_size_changed || buffer_size_changed) {
377 if (data->viewport) {
378 wp_viewport_set_destination(data->viewport, output_width, output_height);
379
380 data->current.logical_width = output_width;
381 data->current.logical_height = output_height;
382 } else {
383 // Calculate the integer scale from the mode and output.
384 const int32_t int_scale = SDL_max(window->current_fullscreen_mode.w / output_width, 1);
385
386 wl_surface_set_buffer_scale(data->surface, int_scale);
387 data->current.logical_width = window->current_fullscreen_mode.w;
388 data->current.logical_height = window->current_fullscreen_mode.h;
389 }
390
391 data->pointer_scale.x = (double)window_width / (double)data->current.logical_width;
392 data->pointer_scale.y = (double)window_height / (double)data->current.logical_height;
393 }
394 } else {
395 window_width = data->requested.logical_width;
396 window_height = data->requested.logical_height;
397
398 window_size_changed = window_width != data->current.logical_width || window_height != data->current.logical_height;
399
400 if (window_size_changed || buffer_size_changed) {
401 if (data->viewport) {
402 wp_viewport_set_destination(data->viewport, window_width, window_height);
403 } else if (window->flags & SDL_WINDOW_HIGH_PIXEL_DENSITY) {
404 // Don't change this if the DPI awareness flag is unset, as an application may have set this manually on a custom or external surface.
405 wl_surface_set_buffer_scale(data->surface, (int32_t)scale_factor);
406 }
407
408 // Clamp the physical window size to the system minimum required size.
409 data->current.logical_width = SDL_max(window_width, data->system_limits.min_width);
410 data->current.logical_height = SDL_max(window_height, data->system_limits.min_height);
411
412 if (!data->scale_to_display) {
413 data->pointer_scale.x = 1.0;
414 data->pointer_scale.y = 1.0;
415 } else {
416 data->pointer_scale.x = scale_factor;
417 data->pointer_scale.y = scale_factor;
418 }
419 }
420 }
421
422 /*
423 * The surface geometry, opaque region and pointer confinement region only
424 * need to be recalculated if the output size has changed.
425 */
426 if (window_size_changed) {
427 /* XXX: This is a hack and only set on the xdg-toplevel path when viewports
428 * aren't supported to avoid a potential protocol violation if a buffer
429 * with an old size is committed.
430 */
431 if (!data->viewport && data->shell_surface_type == WAYLAND_SHELL_SURFACE_TYPE_XDG_TOPLEVEL && data->shell_surface.xdg.surface) {
432 xdg_surface_set_window_geometry(data->shell_surface.xdg.surface, 0, 0, data->current.logical_width, data->current.logical_height);
433 }
434
435 SetSurfaceOpaqueRegion(data, !(window->flags & SDL_WINDOW_TRANSPARENT) && window->opacity == 1.0f);
436
437 // Ensure that child popup windows are still in bounds.
438 for (SDL_Window *child = window->first_child; child; child = child->next_sibling) {
439 RepositionPopup(child, true);
440 }
441 }
442
443 /* Update the min/max dimensions, primarily if the state was changed, and for non-resizable
444 * xdg-toplevel windows where the limits should match the window size.
445 */
446 SetMinMaxDimensions(window);
447
448 // Unconditionally send the window and drawable size, the video core will deduplicate when required.
449 if (!data->scale_to_display) {
450 SDL_SendWindowEvent(window, SDL_EVENT_WINDOW_RESIZED, window_width, window_height);
451 } else {
452 SDL_SendWindowEvent(window, SDL_EVENT_WINDOW_RESIZED, data->current.pixel_width, data->current.pixel_height);
453 }
454 SDL_SendWindowEvent(window, SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED, data->current.pixel_width, data->current.pixel_height);
455
456 /* Send an exposure event if the window is in the shown state and the size has changed,
457 * even if the window is occluded, as the client needs to commit a new frame for the
458 * changes to take effect.
459 *
460 * The occlusion state is immediately set again afterward, if necessary.
461 */
462 if (data->shell_surface_status == WAYLAND_SHELL_SURFACE_STATUS_SHOWN) {
463 if ((buffer_size_changed || window_size_changed) ||
464 (!data->suspended && (window->flags & SDL_WINDOW_OCCLUDED))) {
465 SDL_SendWindowEvent(window, SDL_EVENT_WINDOW_EXPOSED, 0, 0);
466 }
467
468 if (data->suspended) {
469 SDL_SendWindowEvent(window, SDL_EVENT_WINDOW_OCCLUDED, 0, 0);
470 }
471 }
472
473 return true;
474}
475
476static void CommitLibdecorFrame(SDL_Window *window)
477{
478#ifdef HAVE_LIBDECOR_H
479 SDL_WindowData *wind = window->internal;
480
481 if (wind->shell_surface_type == WAYLAND_SHELL_SURFACE_TYPE_LIBDECOR && wind->shell_surface.libdecor.frame) {
482 struct libdecor_state *state = libdecor_state_new(wind->current.logical_width, wind->current.logical_height);
483 libdecor_frame_commit(wind->shell_surface.libdecor.frame, state, NULL);
484 libdecor_state_free(state);
485 }
486#endif
487}
488
489static void fullscreen_deadline_handler(void *data, struct wl_callback *callback, uint32_t callback_data)
490{
491 // Get the window from the ID as it may have been destroyed
492 SDL_WindowID windowID = (SDL_WindowID)((uintptr_t)data);
493 SDL_Window *window = SDL_GetWindowFromID(windowID);
494
495 if (window && window->internal) {
496 window->internal->fullscreen_deadline_count--;
497 }
498
499 wl_callback_destroy(callback);
500}
501
502static struct wl_callback_listener fullscreen_deadline_listener = {
503 fullscreen_deadline_handler
504};
505
506static void maximized_restored_deadline_handler(void *data, struct wl_callback *callback, uint32_t callback_data)
507{
508 // Get the window from the ID as it may have been destroyed
509 SDL_WindowID windowID = (SDL_WindowID)((uintptr_t)data);
510 SDL_Window *window = SDL_GetWindowFromID(windowID);
511
512 if (window && window->internal) {
513 window->internal->maximized_restored_deadline_count--;
514 }
515
516 wl_callback_destroy(callback);
517}
518
519static struct wl_callback_listener maximized_restored_deadline_listener = {
520 maximized_restored_deadline_handler
521};
522
523static void FlushPendingEvents(SDL_Window *window)
524{
525 // Serialize and restore the pending flags, as they may be overwritten while flushing.
526 const bool last_position_pending = window->last_position_pending;
527 const bool last_size_pending = window->last_size_pending;
528
529 while (window->internal->fullscreen_deadline_count || window->internal->maximized_restored_deadline_count) {
530 WAYLAND_wl_display_roundtrip(window->internal->waylandData->display);
531 }
532
533 window->last_position_pending = last_position_pending;
534 window->last_size_pending = last_size_pending;
535}
536
537/* While we can't get window position from the compositor, we do at least know
538 * what monitor we're on, so let's send move events that put the window at the
539 * center of the whatever display the wl_surface_listener events give us.
540 */
541static void Wayland_move_window(SDL_Window *window)
542{
543 SDL_WindowData *wind = window->internal;
544 SDL_DisplayData *display;
545 SDL_DisplayID *displays;
546
547 if (wind->outputs && wind->num_outputs) {
548 display = wind->outputs[wind->num_outputs - 1];
549 } else {
550 // A window may not be on any displays if minimized.
551 return;
552 }
553
554 displays = SDL_GetDisplays(NULL);
555 if (displays) {
556 for (int i = 0; displays[i]; ++i) {
557 if (SDL_GetDisplayDriverData(displays[i]) == display) {
558 /* We want to send a very very specific combination here:
559 *
560 * 1. A coordinate that tells the application what display we're on
561 * 2. Exactly (0, 0)
562 *
563 * Part 1 is useful information but is also really important for
564 * ensuring we end up on the right display for fullscreen, while
565 * part 2 is important because numerous applications use a specific
566 * combination of GetWindowPosition and GetGlobalMouseState, and of
567 * course neither are supported by Wayland. Since global mouse will
568 * fall back to just GetMouseState, we need the window position to
569 * be zero so the cursor math works without it going off in some
570 * random direction. See UE5 Editor for a notable example of this!
571 *
572 * This may be an issue some day if we're ever able to implement
573 * SDL_GetDisplayUsableBounds!
574 *
575 * -flibit
576 */
577
578 if (wind->last_displayID != displays[i]) {
579 wind->last_displayID = displays[i];
580 if (wind->shell_surface_type != WAYLAND_SHELL_SURFACE_TYPE_XDG_POPUP) {
581 SDL_SendWindowEvent(window, SDL_EVENT_WINDOW_MOVED, display->x, display->y);
582 SDL_SendWindowEvent(window, SDL_EVENT_WINDOW_DISPLAY_CHANGED, wind->last_displayID, 0);
583 }
584 }
585 break;
586 }
587 }
588 SDL_free(displays);
589 }
590}
591
592static void SetFullscreen(SDL_Window *window, struct wl_output *output)
593{
594 SDL_WindowData *wind = window->internal;
595 SDL_VideoData *viddata = wind->waylandData;
596
597#ifdef HAVE_LIBDECOR_H
598 if (wind->shell_surface_type == WAYLAND_SHELL_SURFACE_TYPE_LIBDECOR) {
599 if (!wind->shell_surface.libdecor.frame) {
600 return; // Can't do anything yet, wait for ShowWindow
601 }
602
603 wind->fullscreen_exclusive = output ? window->fullscreen_exclusive : false;
604 ++wind->fullscreen_deadline_count;
605 if (output) {
606 Wayland_SetWindowResizable(SDL_GetVideoDevice(), window, true);
607 wl_surface_commit(wind->surface);
608
609 libdecor_frame_set_fullscreen(wind->shell_surface.libdecor.frame, output);
610 } else {
611 libdecor_frame_unset_fullscreen(wind->shell_surface.libdecor.frame);
612 }
613 } else
614#endif
615 if (wind->shell_surface_type == WAYLAND_SHELL_SURFACE_TYPE_XDG_TOPLEVEL) {
616 if (wind->shell_surface.xdg.toplevel.xdg_toplevel == NULL) {
617 return; // Can't do anything yet, wait for ShowWindow
618 }
619
620 wind->fullscreen_exclusive = output ? window->fullscreen_exclusive : false;
621 ++wind->fullscreen_deadline_count;
622 if (output) {
623 Wayland_SetWindowResizable(SDL_GetVideoDevice(), window, true);
624 wl_surface_commit(wind->surface);
625
626 xdg_toplevel_set_fullscreen(wind->shell_surface.xdg.toplevel.xdg_toplevel, output);
627 } else {
628 xdg_toplevel_unset_fullscreen(wind->shell_surface.xdg.toplevel.xdg_toplevel);
629 }
630 }
631
632 // Queue a deadline event
633 struct wl_callback *cb = wl_display_sync(viddata->display);
634 wl_callback_add_listener(cb, &fullscreen_deadline_listener, (void *)((uintptr_t)window->id));
635}
636
637static void UpdateWindowFullscreen(SDL_Window *window, bool fullscreen)
638{
639 SDL_WindowData *wind = window->internal;
640
641 wind->is_fullscreen = fullscreen;
642
643 if (fullscreen) {
644 if (!(window->flags & SDL_WINDOW_FULLSCREEN)) {
645 SDL_copyp(&window->current_fullscreen_mode, &window->requested_fullscreen_mode);
646 SDL_SendWindowEvent(window, SDL_EVENT_WINDOW_ENTER_FULLSCREEN, 0, 0);
647 SDL_UpdateFullscreenMode(window, SDL_FULLSCREEN_OP_ENTER, false);
648
649 /* Set the output for exclusive fullscreen windows when entering fullscreen from a
650 * compositor event, or if the fullscreen parameters were changed between the initial
651 * fullscreen request and now, to ensure that the window is on the correct output,
652 * as requested by the client.
653 */
654 if (window->fullscreen_exclusive && (!wind->fullscreen_exclusive || !wind->fullscreen_was_positioned)) {
655 SDL_VideoDisplay *disp = SDL_GetVideoDisplay(window->current_fullscreen_mode.displayID);
656 if (disp) {
657 wind->fullscreen_was_positioned = true;
658 SetFullscreen(window, disp->internal->output);
659 }
660 }
661 }
662 } else {
663 // Don't change the fullscreen flags if the window is hidden or being hidden.
664 if ((window->flags & SDL_WINDOW_FULLSCREEN) && !window->is_hiding && !(window->flags & SDL_WINDOW_HIDDEN)) {
665 SDL_SendWindowEvent(window, SDL_EVENT_WINDOW_LEAVE_FULLSCREEN, 0, 0);
666 SDL_UpdateFullscreenMode(window, SDL_FULLSCREEN_OP_LEAVE, false);
667 wind->fullscreen_was_positioned = false;
668
669 /* Send a move event, in case it was deferred while the fullscreen window was moving and
670 * on multiple outputs.
671 */
672 Wayland_move_window(window);
673 }
674 }
675}
676
677static const struct wl_callback_listener surface_frame_listener;
678
679static void surface_frame_done(void *data, struct wl_callback *cb, uint32_t time)
680{
681 SDL_WindowData *wind = (SDL_WindowData *)data;
682
683 /* XXX: This is needed to work around an Nvidia egl-wayland bug due to buffer coordinates
684 * being used with wl_surface_damage, which causes part of the output to not be
685 * updated when using a viewport with an output region larger than the source region.
686 */
687 if (wl_compositor_get_version(wind->waylandData->compositor) >= WL_SURFACE_DAMAGE_BUFFER_SINCE_VERSION) {
688 wl_surface_damage_buffer(wind->surface, 0, 0, SDL_MAX_SINT32, SDL_MAX_SINT32);
689 } else {
690 wl_surface_damage(wind->surface, 0, 0, SDL_MAX_SINT32, SDL_MAX_SINT32);
691 }
692
693 wind->drop_interactive_resizes = false;
694
695 if (wind->shell_surface_status == WAYLAND_SHELL_SURFACE_STATUS_WAITING_FOR_FRAME) {
696 wind->shell_surface_status = WAYLAND_SHELL_SURFACE_STATUS_SHOWN;
697
698 // If any child windows are waiting on this window to be shown, show them now
699 for (SDL_Window *w = wind->sdlwindow->first_child; w; w = w->next_sibling) {
700 if (w->internal->shell_surface_status == WAYLAND_SHELL_SURFACE_STATUS_SHOW_PENDING) {
701 Wayland_ShowWindow(SDL_GetVideoDevice(), w);
702 } else if (w->internal->reparenting_required) {
703 Wayland_SetWindowParent(SDL_GetVideoDevice(), w, w->parent);
704 if (w->flags & SDL_WINDOW_MODAL) {
705 Wayland_SetWindowModal(SDL_GetVideoDevice(), w, true);
706 }
707 }
708 }
709
710 /* If the window was initially set to the suspended state, send the occluded event now,
711 * as we don't want to mark the window as occluded until at least one frame has been submitted.
712 */
713 if (wind->suspended) {
714 SDL_SendWindowEvent(wind->sdlwindow, SDL_EVENT_WINDOW_OCCLUDED, 0, 0);
715 }
716 }
717
718 wl_callback_destroy(cb);
719 wind->surface_frame_callback = wl_surface_frame(wind->surface);
720 wl_callback_add_listener(wind->surface_frame_callback, &surface_frame_listener, data);
721}
722
723static const struct wl_callback_listener surface_frame_listener = {
724 surface_frame_done
725};
726
727static const struct wl_callback_listener gles_swap_frame_listener;
728
729static void gles_swap_frame_done(void *data, struct wl_callback *cb, uint32_t time)
730{
731 SDL_WindowData *wind = (SDL_WindowData *)data;
732 SDL_SetAtomicInt(&wind->swap_interval_ready, 1); // mark window as ready to present again.
733
734 // reset this callback to fire again once a new frame was presented and compositor wants the next one.
735 wind->gles_swap_frame_callback = wl_surface_frame(wind->gles_swap_frame_surface_wrapper);
736 wl_callback_destroy(cb);
737 wl_callback_add_listener(wind->gles_swap_frame_callback, &gles_swap_frame_listener, data);
738}
739
740static const struct wl_callback_listener gles_swap_frame_listener = {
741 gles_swap_frame_done
742};
743
744static void handle_configure_xdg_shell_surface(void *data, struct xdg_surface *xdg, uint32_t serial)
745{
746 SDL_WindowData *wind = (SDL_WindowData *)data;
747 SDL_Window *window = wind->sdlwindow;
748
749 if (ConfigureWindowGeometry(window)) {
750 xdg_surface_ack_configure(xdg, serial);
751 }
752
753 wind->shell_surface.xdg.initial_configure_seen = true;
754}
755
756static const struct xdg_surface_listener shell_surface_listener_xdg = {
757 handle_configure_xdg_shell_surface
758};
759
760static void handle_configure_xdg_toplevel(void *data,
761 struct xdg_toplevel *xdg_toplevel,
762 int32_t width,
763 int32_t height,
764 struct wl_array *states)
765{
766 SDL_WindowData *wind = (SDL_WindowData *)data;
767 SDL_Window *window = wind->sdlwindow;
768
769 enum xdg_toplevel_state *state;
770 bool fullscreen = false;
771 bool maximized = false;
772 bool floating = true;
773 bool tiled = false;
774 bool active = false;
775 bool resizing = false;
776 bool suspended = false;
777 wl_array_for_each (state, states) {
778 switch (*state) {
779 case XDG_TOPLEVEL_STATE_FULLSCREEN:
780 fullscreen = true;
781 floating = false;
782 break;
783 case XDG_TOPLEVEL_STATE_MAXIMIZED:
784 maximized = true;
785 floating = false;
786 break;
787 case XDG_TOPLEVEL_STATE_RESIZING:
788 resizing = true;
789 break;
790 case XDG_TOPLEVEL_STATE_ACTIVATED:
791 active = true;
792 break;
793 case XDG_TOPLEVEL_STATE_TILED_LEFT:
794 case XDG_TOPLEVEL_STATE_TILED_RIGHT:
795 case XDG_TOPLEVEL_STATE_TILED_TOP:
796 case XDG_TOPLEVEL_STATE_TILED_BOTTOM:
797 tiled = true;
798 floating = false;
799 break;
800 case XDG_TOPLEVEL_STATE_SUSPENDED:
801 suspended = true;
802 break;
803 default:
804 break;
805 }
806 }
807
808 UpdateWindowFullscreen(window, fullscreen);
809
810 /* Always send a maximized/restore event; if the event is redundant it will
811 * automatically be discarded (see src/events/SDL_windowevents.c)
812 *
813 * No, we do not get minimize events from xdg-shell, however, the minimized
814 * state can be programmatically set. The meaning of 'minimized' is compositor
815 * dependent, but in general, we can assume that the flag should remain set until
816 * the next focused configure event occurs.
817 */
818 if (active || !(window->flags & SDL_WINDOW_MINIMIZED)) {
819 if (window->flags & SDL_WINDOW_MINIMIZED) {
820 // If we were minimized, send a restored event before possibly sending maximized.
821 SDL_SendWindowEvent(window, SDL_EVENT_WINDOW_RESTORED, 0, 0);
822 }
823 SDL_SendWindowEvent(window,
824 (maximized && !fullscreen) ? SDL_EVENT_WINDOW_MAXIMIZED : SDL_EVENT_WINDOW_RESTORED,
825 0, 0);
826 }
827
828 if (!fullscreen) {
829 /* xdg_toplevel spec states that this is a suggestion.
830 * Ignore if less than or greater than max/min size.
831 */
832 if (window->flags & SDL_WINDOW_RESIZABLE) {
833 if (width == 0 || height == 0) {
834 /* This happens when the compositor indicates that the size is
835 * up to the client, so use the cached window size here.
836 */
837 if (floating) {
838 width = window->floating.w;
839 height = window->floating.h;
840
841 // Clamp the window to the toplevel bounds, if any are set.
842 if (wind->shell_surface_status == WAYLAND_SHELL_SURFACE_STATUS_WAITING_FOR_CONFIGURE &&
843 wind->toplevel_bounds.width && wind->toplevel_bounds.height) {
844 width = SDL_min(wind->toplevel_bounds.width, width);
845 height = SDL_min(wind->toplevel_bounds.height, height);
846 }
847 } else {
848 width = window->windowed.w;
849 height = window->windowed.h;
850 }
851
852 if (!wind->scale_to_display) {
853 wind->requested.logical_width = width;
854 wind->requested.logical_height = height;
855 } else {
856 wind->requested.pixel_width = width;
857 wind->requested.pixel_height = height;
858 width = wind->requested.logical_width = PixelToPoint(window, width);
859 height = wind->requested.logical_height = PixelToPoint(window, height);
860 }
861 } else {
862 /* Don't apply the supplied dimensions if they haven't changed from the last configuration
863 * event, or a newer size set programmatically can be overwritten by old data.
864 */
865 if (width != wind->last_configure.width || height != wind->last_configure.height) {
866 wind->requested.logical_width = width;
867 wind->requested.logical_height = height;
868
869 if (wind->scale_to_display) {
870 wind->requested.pixel_width = PointToPixel(window, width);
871 wind->requested.pixel_height = PointToPixel(window, height);
872 }
873 }
874 }
875 } else {
876 /* If we're a fixed-size window, we know our size for sure.
877 * Always assume the configure is wrong.
878 */
879 if (!wind->scale_to_display) {
880 width = wind->requested.logical_width = window->floating.w;
881 height = wind->requested.logical_height = window->floating.h;
882 } else {
883 wind->requested.pixel_width = window->floating.w;
884 wind->requested.pixel_height = window->floating.h;
885 width = wind->requested.logical_width = PixelToPoint(window, window->floating.w);
886 height = wind->requested.logical_height = PixelToPoint(window, window->floating.h);
887 }
888 }
889
890 /* Notes on the spec:
891 *
892 * - The content limits are only a hint, which the compositor is free to ignore,
893 * so apply them manually when appropriate.
894 *
895 * - Maximized windows must have their exact dimensions respected, thus they must
896 * not be resized, or a protocol violation can occur.
897 *
898 * - When resizing a window, the width/height are maximum values, so aspect ratio
899 * correction can't resize beyond the existing dimensions, or a protocol violation
900 * can occur. In practice, nothing seems to kill clients that do this, but doing
901 * so causes GNOME to glitch out.
902 */
903 if (!maximized) {
904 if (!wind->scale_to_display) {
905 if (window->max_w > 0) {
906 wind->requested.logical_width = SDL_min(wind->requested.logical_width, window->max_w);
907 }
908 wind->requested.logical_width = SDL_max(wind->requested.logical_width, window->min_w);
909
910 if (window->max_h > 0) {
911 wind->requested.logical_height = SDL_min(wind->requested.logical_height, window->max_h);
912 }
913 wind->requested.logical_height = SDL_max(wind->requested.logical_height, window->min_h);
914
915 // Aspect correction.
916 const float aspect = (float)wind->requested.logical_width / (float)wind->requested.logical_height;
917
918 if (window->min_aspect != 0.f && aspect < window->min_aspect) {
919 wind->requested.logical_height = SDL_lroundf((float)wind->requested.logical_width / window->min_aspect);
920 } else if (window->max_aspect != 0.f && aspect > window->max_aspect) {
921 wind->requested.logical_width = SDL_lroundf((float)wind->requested.logical_height * window->max_aspect);
922 }
923 } else {
924 if (window->max_w > 0) {
925 wind->requested.pixel_width = SDL_min(wind->requested.pixel_width, window->max_w);
926 }
927 wind->requested.pixel_width = SDL_max(wind->requested.pixel_width, window->min_w);
928
929 if (window->max_h > 0) {
930 wind->requested.pixel_height = SDL_min(wind->requested.pixel_height, window->max_h);
931 }
932 wind->requested.pixel_height = SDL_max(wind->requested.pixel_height, window->min_h);
933
934 // Aspect correction.
935 const float aspect = (float)wind->requested.pixel_width / (float)wind->requested.pixel_height;
936
937 if (window->min_aspect != 0.f && aspect < window->min_aspect) {
938 wind->requested.pixel_height = SDL_lroundf((float)wind->requested.pixel_width / window->min_aspect);
939 } else if (window->max_aspect != 0.f && aspect > window->max_aspect) {
940 wind->requested.pixel_width = SDL_lroundf((float)wind->requested.pixel_height * window->max_aspect);
941 }
942
943 wind->requested.logical_width = PixelToPoint(window, wind->requested.pixel_width);
944 wind->requested.logical_height = PixelToPoint(window, wind->requested.pixel_height);
945 }
946 }
947 } else {
948 // Fullscreen windows know their exact size.
949 if (width == 0 || height == 0) {
950 width = wind->requested.logical_width;
951 height = wind->requested.logical_height;
952 } else {
953 wind->requested.logical_width = width;
954 wind->requested.logical_height = height;
955 }
956
957 if (wind->scale_to_display) {
958 wind->requested.pixel_width = PointToPixel(window, width);
959 wind->requested.pixel_height = PointToPixel(window, height);
960 }
961 }
962
963 wind->last_configure.width = width;
964 wind->last_configure.height = height;
965 wind->floating = floating;
966 wind->suspended = suspended;
967 wind->active = active;
968 window->tiled = tiled;
969 wind->resizing = resizing;
970
971 if (wind->shell_surface_status == WAYLAND_SHELL_SURFACE_STATUS_WAITING_FOR_CONFIGURE) {
972 wind->shell_surface_status = WAYLAND_SHELL_SURFACE_STATUS_WAITING_FOR_FRAME;
973 }
974}
975
976static void handle_close_xdg_toplevel(void *data, struct xdg_toplevel *xdg_toplevel)
977{
978 SDL_WindowData *window = (SDL_WindowData *)data;
979 SDL_SendWindowEvent(window->sdlwindow, SDL_EVENT_WINDOW_CLOSE_REQUESTED, 0, 0);
980}
981
982static void handle_xdg_configure_toplevel_bounds(void *data,
983 struct xdg_toplevel *xdg_toplevel,
984 int32_t width, int32_t height)
985{
986 SDL_WindowData *window = (SDL_WindowData *)data;
987 window->toplevel_bounds.width = width;
988 window->toplevel_bounds.height = height;
989}
990
991static void handle_xdg_toplevel_wm_capabilities(void *data,
992 struct xdg_toplevel *xdg_toplevel,
993 struct wl_array *capabilities)
994{
995 SDL_WindowData *wind = (SDL_WindowData *)data;
996 enum xdg_toplevel_wm_capabilities *wm_cap;
997
998 wind->wm_caps = 0;
999
1000 wl_array_for_each (wm_cap, capabilities) {
1001 switch (*wm_cap) {
1002 case XDG_TOPLEVEL_WM_CAPABILITIES_WINDOW_MENU:
1003 wind->wm_caps |= WAYLAND_WM_CAPS_WINDOW_MENU;
1004 break;
1005 case XDG_TOPLEVEL_WM_CAPABILITIES_MAXIMIZE:
1006 wind->wm_caps |= WAYLAND_WM_CAPS_MAXIMIZE;
1007 break;
1008 case XDG_TOPLEVEL_WM_CAPABILITIES_FULLSCREEN:
1009 wind->wm_caps |= WAYLAND_WM_CAPS_FULLSCREEN;
1010 break;
1011 case XDG_TOPLEVEL_WM_CAPABILITIES_MINIMIZE:
1012 wind->wm_caps |= WAYLAND_WM_CAPS_MINIMIZE;
1013 break;
1014 default:
1015 break;
1016 }
1017 }
1018}
1019
1020static const struct xdg_toplevel_listener toplevel_listener_xdg = {
1021 handle_configure_xdg_toplevel,
1022 handle_close_xdg_toplevel,
1023 handle_xdg_configure_toplevel_bounds, // Version 4
1024 handle_xdg_toplevel_wm_capabilities // Version 5
1025};
1026
1027static void handle_configure_xdg_popup(void *data,
1028 struct xdg_popup *xdg_popup,
1029 int32_t x,
1030 int32_t y,
1031 int32_t width,
1032 int32_t height)
1033{
1034 SDL_WindowData *wind = (SDL_WindowData *)data;
1035 int offset_x = 0, offset_y = 0;
1036
1037 // Adjust the position if it was offset for libdecor
1038 AdjustPopupOffset(wind->sdlwindow, &offset_x, &offset_y);
1039 x -= offset_x;
1040 y -= offset_y;
1041
1042 /* This happens when the compositor indicates that the size is
1043 * up to the client, so use the cached window size here.
1044 */
1045 if (width == 0 || height == 0) {
1046 width = wind->sdlwindow->floating.w;
1047 height = wind->sdlwindow->floating.h;
1048 }
1049
1050 /* Don't apply the supplied dimensions if they haven't changed from the last configuration
1051 * event, or a newer size set programmatically can be overwritten by old data.
1052 */
1053 if (width != wind->last_configure.width || height != wind->last_configure.height) {
1054 wind->requested.logical_width = width;
1055 wind->requested.logical_height = height;
1056
1057 if (wind->scale_to_display) {
1058 wind->requested.pixel_width = PointToPixel(wind->sdlwindow, width);
1059 wind->requested.pixel_height = PointToPixel(wind->sdlwindow, height);
1060 }
1061 }
1062
1063 if (wind->scale_to_display) {
1064 x = PointToPixel(wind->sdlwindow->parent, x);
1065 y = PointToPixel(wind->sdlwindow->parent, y);
1066 }
1067
1068 SDL_SendWindowEvent(wind->sdlwindow, SDL_EVENT_WINDOW_MOVED, x, y);
1069
1070 wind->last_configure.width = width;
1071 wind->last_configure.height = height;
1072
1073 if (wind->shell_surface_status == WAYLAND_SHELL_SURFACE_STATUS_WAITING_FOR_CONFIGURE) {
1074 wind->shell_surface_status = WAYLAND_SHELL_SURFACE_STATUS_WAITING_FOR_FRAME;
1075 }
1076}
1077
1078static void handle_done_xdg_popup(void *data, struct xdg_popup *xdg_popup)
1079{
1080 SDL_WindowData *window = (SDL_WindowData *)data;
1081 SDL_SendWindowEvent(window->sdlwindow, SDL_EVENT_WINDOW_CLOSE_REQUESTED, 0, 0);
1082}
1083
1084static void handle_repositioned_xdg_popup(void *data,
1085 struct xdg_popup *xdg_popup,
1086 uint32_t token)
1087{
1088 // No-op, configure does all the work we care about
1089}
1090
1091static const struct xdg_popup_listener popup_listener_xdg = {
1092 handle_configure_xdg_popup,
1093 handle_done_xdg_popup,
1094 handle_repositioned_xdg_popup
1095};
1096
1097static void handle_configure_zxdg_decoration(void *data,
1098 struct zxdg_toplevel_decoration_v1 *zxdg_toplevel_decoration_v1,
1099 uint32_t mode)
1100{
1101 SDL_Window *window = (SDL_Window *)data;
1102 SDL_WindowData *internal = window->internal;
1103 SDL_VideoDevice *device = SDL_GetVideoDevice();
1104
1105 /* If the compositor tries to force CSD anyway, bail on direct XDG support
1106 * and fall back to libdecor, it will handle these events from then on.
1107 *
1108 * To do this we have to fully unmap, then map with libdecor loaded.
1109 */
1110 if (mode == ZXDG_TOPLEVEL_DECORATION_V1_MODE_CLIENT_SIDE) {
1111 if (window->flags & SDL_WINDOW_BORDERLESS) {
1112 // borderless windows do request CSD, so we got what we wanted
1113 return;
1114 }
1115 if (!Wayland_LoadLibdecor(internal->waylandData, true)) {
1116 // libdecor isn't available, so no borders for you... oh well
1117 return;
1118 }
1119 WAYLAND_wl_display_roundtrip(internal->waylandData->display);
1120
1121 Wayland_HideWindow(device, window);
1122 SDL_zero(internal->shell_surface);
1123 internal->shell_surface_type = WAYLAND_SHELL_SURFACE_TYPE_LIBDECOR;
1124
1125 Wayland_ShowWindow(device, window);
1126 }
1127}
1128
1129static const struct zxdg_toplevel_decoration_v1_listener decoration_listener = {
1130 handle_configure_zxdg_decoration
1131};
1132
1133#ifdef HAVE_LIBDECOR_H
1134/*
1135 * XXX: Hack for older versions of libdecor that lack the function to query the
1136 * minimum content size limit. The internal limits must always be overridden
1137 * to ensure that very small windows don't cause errors or crashes.
1138 *
1139 * On libdecor >= 0.1.2, which exposes the function to get the minimum content
1140 * size limit, this function is a no-op.
1141 *
1142 * Can be removed if the minimum required version of libdecor is raised to
1143 * 0.1.2 or higher.
1144 */
1145static void OverrideLibdecorLimits(SDL_Window *window)
1146{
1147#ifdef SDL_VIDEO_DRIVER_WAYLAND_DYNAMIC_LIBDECOR
1148 if (!libdecor_frame_get_min_content_size) {
1149 libdecor_frame_set_min_content_size(window->internal->shell_surface.libdecor.frame, window->min_w, window->min_h);
1150 }
1151#elif !SDL_LIBDECOR_CHECK_VERSION(0, 2, 0)
1152 libdecor_frame_set_min_content_size(window->internal->shell_surface.libdecor.frame, window->min_w, window->min_h);
1153#endif
1154}
1155
1156/*
1157 * NOTE: Retrieves the minimum content size limits, if the function for doing so is available.
1158 * On versions of libdecor that lack the minimum content size retrieval function, this
1159 * function is a no-op.
1160 *
1161 * Can be replaced with a direct call if the minimum required version of libdecor is raised
1162 * to 0.1.2 or higher.
1163 */
1164static void LibdecorGetMinContentSize(struct libdecor_frame *frame, int *min_w, int *min_h)
1165{
1166#ifdef SDL_VIDEO_DRIVER_WAYLAND_DYNAMIC_LIBDECOR
1167 if (libdecor_frame_get_min_content_size != NULL) {
1168 libdecor_frame_get_min_content_size(frame, min_w, min_h);
1169 }
1170#elif SDL_LIBDECOR_CHECK_VERSION(0, 2, 0)
1171 libdecor_frame_get_min_content_size(frame, min_w, min_h);
1172#endif
1173}
1174
1175static void decoration_frame_configure(struct libdecor_frame *frame,
1176 struct libdecor_configuration *configuration,
1177 void *user_data)
1178{
1179 SDL_WindowData *wind = (SDL_WindowData *)user_data;
1180 SDL_Window *window = wind->sdlwindow;
1181
1182 enum libdecor_window_state window_state;
1183 int width, height;
1184
1185 bool prev_fullscreen = wind->is_fullscreen;
1186 bool active = false;
1187 bool fullscreen = false;
1188 bool maximized = false;
1189 bool tiled = false;
1190 bool suspended = false;
1191 bool resizing = false;
1192
1193 static const enum libdecor_window_state tiled_states = (LIBDECOR_WINDOW_STATE_TILED_LEFT | LIBDECOR_WINDOW_STATE_TILED_RIGHT |
1194 LIBDECOR_WINDOW_STATE_TILED_TOP | LIBDECOR_WINDOW_STATE_TILED_BOTTOM);
1195
1196 // Window State
1197 if (libdecor_configuration_get_window_state(configuration, &window_state)) {
1198 fullscreen = (window_state & LIBDECOR_WINDOW_STATE_FULLSCREEN) != 0;
1199 maximized = (window_state & LIBDECOR_WINDOW_STATE_MAXIMIZED) != 0;
1200 active = (window_state & LIBDECOR_WINDOW_STATE_ACTIVE) != 0;
1201 tiled = (window_state & tiled_states) != 0;
1202#if SDL_LIBDECOR_CHECK_VERSION(0, 2, 0)
1203 suspended = (window_state & LIBDECOR_WINDOW_STATE_SUSPENDED) != 0;
1204#endif
1205#if SDL_LIBDECOR_CHECK_VERSION(0, 3, 0)
1206 resizing = (window_state & LIBDECOR_WINDOW_STATE_RESIZING) != 0;
1207#endif
1208 }
1209 const bool floating = !(fullscreen || maximized || tiled);
1210
1211 UpdateWindowFullscreen(window, fullscreen);
1212
1213 /* Always send a maximized/restore event; if the event is redundant it will
1214 * automatically be discarded (see src/events/SDL_windowevents.c)
1215 *
1216 * No, we do not get minimize events from libdecor, however, the minimized
1217 * state can be programmatically set. The meaning of 'minimized' is compositor
1218 * dependent, but in general, we can assume that the flag should remain set until
1219 * the next focused configure event occurs.
1220 */
1221 if (active || !(window->flags & SDL_WINDOW_MINIMIZED)) {
1222 if (window->flags & SDL_WINDOW_MINIMIZED) {
1223 // If we were minimized, send a restored event before possibly sending maximized.
1224 SDL_SendWindowEvent(window, SDL_EVENT_WINDOW_RESTORED, 0, 0);
1225 }
1226 SDL_SendWindowEvent(window,
1227 (maximized && !fullscreen) ? SDL_EVENT_WINDOW_MAXIMIZED : SDL_EVENT_WINDOW_RESTORED,
1228 0, 0);
1229 }
1230
1231 /* For fullscreen or fixed-size windows we know our size.
1232 * Always assume the configure is wrong.
1233 */
1234 if (fullscreen) {
1235 if (!libdecor_configuration_get_content_size(configuration, frame, &width, &height)) {
1236 width = wind->requested.logical_width;
1237 height = wind->requested.logical_height;
1238 } else {
1239 // Fullscreen windows know their exact size.
1240 wind->requested.logical_width = width;
1241 wind->requested.logical_height = height;
1242
1243 if (wind->scale_to_display) {
1244 wind->requested.pixel_width = PointToPixel(window, width);
1245 wind->requested.pixel_height = PointToPixel(window, height);
1246 }
1247 }
1248 } else {
1249 if (!(window->flags & SDL_WINDOW_RESIZABLE)) {
1250 /* If we're a fixed-size window, we know our size for sure.
1251 * Always assume the configure is wrong.
1252 */
1253 if (!wind->scale_to_display) {
1254 width = wind->requested.logical_width = window->floating.w;
1255 height = wind->requested.logical_height = window->floating.h;
1256 } else {
1257 wind->requested.pixel_width = window->floating.w;
1258 wind->requested.pixel_height = window->floating.h;
1259 width = wind->requested.logical_width = PixelToPoint(window, window->floating.w);
1260 height = wind->requested.logical_height = PixelToPoint(window, window->floating.h);
1261 }
1262
1263 OverrideLibdecorLimits(window);
1264 } else {
1265 /* XXX: The libdecor cairo plugin sends bogus content sizes that add the
1266 * height of the title bar when transitioning from a fixed-size to
1267 * floating state. Ignore the sent window dimensions in this case,
1268 * in favor of the cached value to avoid the window increasing in
1269 * size after every state transition.
1270 *
1271 * https://gitlab.freedesktop.org/libdecor/libdecor/-/issues/34
1272 */
1273 if ((floating && (!wind->floating && !(window->flags & SDL_WINDOW_BORDERLESS))) ||
1274 !libdecor_configuration_get_content_size(configuration, frame, &width, &height)) {
1275 /* This happens when we're being restored from a non-floating state,
1276 * or the compositor indicates that the size is up to the client, so
1277 * used the cached window size here.
1278 */
1279 if (floating) {
1280 width = window->floating.w;
1281 height = window->floating.h;
1282 } else {
1283 width = window->windowed.w;
1284 height = window->windowed.h;
1285 }
1286
1287 if (!wind->scale_to_display) {
1288 wind->requested.logical_width = width;
1289 wind->requested.logical_height = height;
1290 } else {
1291 wind->requested.pixel_width = width;
1292 wind->requested.pixel_height = height;
1293 width = wind->requested.logical_width = PixelToPoint(window, width);
1294 height = wind->requested.logical_height = PixelToPoint(window, height);
1295 }
1296 } else {
1297 /* Don't apply the supplied dimensions if they haven't changed from the last configuration
1298 * event, or a newer size set programmatically can be overwritten by old data.
1299 */
1300 if (width != wind->last_configure.width || height != wind->last_configure.height) {
1301 wind->requested.logical_width = width;
1302 wind->requested.logical_height = height;
1303
1304 if (wind->scale_to_display) {
1305 wind->requested.pixel_width = PointToPixel(window, width);
1306 wind->requested.pixel_height = PointToPixel(window, height);
1307 }
1308 }
1309 }
1310 }
1311
1312 /* Notes on the spec:
1313 *
1314 * - The content limits are only a hint, which the compositor is free to ignore,
1315 * so apply them manually when appropriate.
1316 *
1317 * - Maximized windows must have their exact dimensions respected, thus they must
1318 * not be resized, or a protocol violation can occur.
1319 *
1320 * - When resizing a window, the width/height are maximum values, so aspect ratio
1321 * correction can't resize beyond the existing dimensions, or a protocol violation
1322 * can occur. In practice, nothing seems to kill clients that do this, but doing
1323 * so causes GNOME to glitch out.
1324 */
1325 if (!maximized) {
1326 if (!wind->scale_to_display) {
1327 if (window->max_w > 0) {
1328 wind->requested.logical_width = SDL_min(wind->requested.logical_width, window->max_w);
1329 }
1330 wind->requested.logical_width = SDL_max(wind->requested.logical_width, window->min_w);
1331
1332 if (window->max_h > 0) {
1333 wind->requested.logical_height = SDL_min(wind->requested.logical_height, window->max_h);
1334 }
1335 wind->requested.logical_height = SDL_max(wind->requested.logical_height, window->min_h);
1336
1337 // Aspect correction.
1338 const float aspect = (float)wind->requested.logical_width / (float)wind->requested.logical_height;
1339
1340 if (window->min_aspect != 0.f && aspect < window->min_aspect) {
1341 wind->requested.logical_height = SDL_lroundf((float)wind->requested.logical_width / window->min_aspect);
1342 } else if (window->max_aspect != 0.f && aspect > window->max_aspect) {
1343 wind->requested.logical_width = SDL_lroundf((float)wind->requested.logical_height * window->max_aspect);
1344 }
1345 } else {
1346 if (window->max_w > 0) {
1347 wind->requested.pixel_width = SDL_min(wind->requested.pixel_width, window->max_w);
1348 }
1349 wind->requested.pixel_width = SDL_max(wind->requested.pixel_width, window->min_w);
1350
1351 if (window->max_h > 0) {
1352 wind->requested.pixel_height = SDL_min(wind->requested.pixel_height, window->max_h);
1353 }
1354 wind->requested.pixel_height = SDL_max(wind->requested.pixel_height, window->min_h);
1355
1356 // Aspect correction.
1357 const float aspect = (float)wind->requested.pixel_width / (float)wind->requested.pixel_height;
1358
1359 if (window->min_aspect != 0.f && aspect < window->min_aspect) {
1360 wind->requested.pixel_height = SDL_lroundf((float)wind->requested.pixel_width / window->min_aspect);
1361 } else if (window->max_aspect != 0.f && aspect > window->max_aspect) {
1362 wind->requested.pixel_width = SDL_lroundf((float)wind->requested.pixel_height * window->max_aspect);
1363 }
1364
1365 wind->requested.logical_width = PixelToPoint(window, wind->requested.pixel_width);
1366 wind->requested.logical_height = PixelToPoint(window, wind->requested.pixel_height);
1367 }
1368 }
1369 }
1370
1371 // Store the new state.
1372 wind->last_configure.width = width;
1373 wind->last_configure.height = height;
1374 wind->floating = floating;
1375 wind->suspended = suspended;
1376 wind->active = active;
1377 window->tiled = tiled;
1378 wind->resizing = resizing;
1379
1380 // Update the window manager capabilities.
1381#if SDL_LIBDECOR_CHECK_VERSION(0, 3, 0)
1382 enum libdecor_wm_capabilities caps;
1383#ifdef SDL_VIDEO_DRIVER_WAYLAND_DYNAMIC_LIBDECOR
1384 if (libdecor_frame_get_wm_capabilities) {
1385 caps = libdecor_frame_get_wm_capabilities(wind->shell_surface.libdecor.frame);
1386#else
1387 caps = libdecor_frame_get_wm_capabilities(wind->shell_surface.libdecor.frame);
1388 {
1389#endif
1390 wind->wm_caps = 0;
1391 wind->wm_caps |= caps & LIBDECOR_WM_CAPABILITIES_WINDOW_MENU ? WAYLAND_WM_CAPS_WINDOW_MENU : 0;
1392 wind->wm_caps |= caps & LIBDECOR_WM_CAPABILITIES_MAXIMIZE ? WAYLAND_WM_CAPS_MAXIMIZE : 0;
1393 wind->wm_caps |= caps & LIBDECOR_WM_CAPABILITIES_FULLSCREEN ? WAYLAND_WM_CAPS_FULLSCREEN : 0;
1394 wind->wm_caps |= caps & LIBDECOR_WM_CAPABILITIES_MINIMIZE ? WAYLAND_WM_CAPS_MINIMIZE : 0;
1395 }
1396#endif
1397
1398 // Calculate the new window geometry
1399 if (ConfigureWindowGeometry(window)) {
1400 // ... then commit the changes on the libdecor side.
1401 struct libdecor_state *state = libdecor_state_new(wind->current.logical_width, wind->current.logical_height);
1402 libdecor_frame_commit(frame, state, configuration);
1403 libdecor_state_free(state);
1404 }
1405
1406 if (!wind->shell_surface.libdecor.initial_configure_seen) {
1407 LibdecorGetMinContentSize(frame, &wind->system_limits.min_width, &wind->system_limits.min_height);
1408 wind->shell_surface.libdecor.initial_configure_seen = true;
1409 }
1410 if (wind->shell_surface_status == WAYLAND_SHELL_SURFACE_STATUS_WAITING_FOR_CONFIGURE) {
1411 wind->shell_surface_status = WAYLAND_SHELL_SURFACE_STATUS_WAITING_FOR_FRAME;
1412 }
1413
1414 /* Update the resize capability if this config event was the result of the
1415 * compositor taking a window out of fullscreen. Since this will change the
1416 * capabilities and commit a new frame state with the last known content
1417 * dimension, this has to be called after the new state has been committed
1418 * and the new content dimensions were updated.
1419 */
1420 if (prev_fullscreen && !wind->is_fullscreen) {
1421 Wayland_SetWindowResizable(SDL_GetVideoDevice(), window,
1422 !!(window->flags & SDL_WINDOW_RESIZABLE));
1423 }
1424}
1425
1426static void decoration_frame_close(struct libdecor_frame *frame, void *user_data)
1427{
1428 SDL_SendWindowEvent(((SDL_WindowData *)user_data)->sdlwindow, SDL_EVENT_WINDOW_CLOSE_REQUESTED, 0, 0);
1429}
1430
1431static void decoration_frame_commit(struct libdecor_frame *frame, void *user_data)
1432{
1433 /* libdecor decoration subsurfaces are synchronous, so the client needs to
1434 * commit a frame to trigger an update of the decoration surfaces.
1435 */
1436 SDL_WindowData *wind = (SDL_WindowData *)user_data;
1437 if (!wind->suspended && wind->shell_surface_status == WAYLAND_SHELL_SURFACE_STATUS_SHOWN) {
1438 SDL_SendWindowEvent(wind->sdlwindow, SDL_EVENT_WINDOW_EXPOSED, 0, 0);
1439 }
1440}
1441
1442static void decoration_dismiss_popup(struct libdecor_frame *frame, const char *seat_name, void *user_data)
1443{
1444 // NOP
1445}
1446
1447static struct libdecor_frame_interface libdecor_frame_interface = {
1448 decoration_frame_configure,
1449 decoration_frame_close,
1450 decoration_frame_commit,
1451 decoration_dismiss_popup
1452};
1453#endif
1454
1455static void Wayland_HandlePreferredScaleChanged(SDL_WindowData *window_data, double factor)
1456{
1457 const double old_factor = window_data->scale_factor;
1458
1459 // Round the scale factor if viewports aren't available.
1460 if (!window_data->viewport) {
1461 factor = SDL_ceil(factor);
1462 }
1463
1464 if (factor != old_factor) {
1465 window_data->scale_factor = factor;
1466
1467 if (window_data->scale_to_display) {
1468 /* If the window is in the floating state with a user/application specified size, calculate the new
1469 * logical size from the backbuffer size. Otherwise, use the fixed underlying logical size to calculate
1470 * the new backbuffer dimensions.
1471 */
1472 if (window_data->floating) {
1473 window_data->requested.logical_width = PixelToPoint(window_data->sdlwindow, window_data->requested.pixel_width);
1474 window_data->requested.logical_height = PixelToPoint(window_data->sdlwindow, window_data->requested.pixel_height);
1475 } else {
1476 window_data->requested.pixel_width = PointToPixel(window_data->sdlwindow, window_data->requested.logical_width);
1477 window_data->requested.pixel_height = PointToPixel(window_data->sdlwindow, window_data->requested.logical_height);
1478 }
1479 }
1480
1481 if (window_data->sdlwindow->flags & SDL_WINDOW_HIGH_PIXEL_DENSITY || window_data->scale_to_display) {
1482 ConfigureWindowGeometry(window_data->sdlwindow);
1483 CommitLibdecorFrame(window_data->sdlwindow);
1484 }
1485 }
1486}
1487
1488static void Wayland_MaybeUpdateScaleFactor(SDL_WindowData *window)
1489{
1490 double factor;
1491 int i;
1492
1493 /* If the fractional scale protocol is present or the core protocol supports the
1494 * preferred buffer scale event, the compositor will explicitly tell the application
1495 * what scale it wants via these events, so don't try to determine the scale factor
1496 * from which displays the surface has entered.
1497 */
1498 if (window->fractional_scale || wl_surface_get_version(window->surface) >= WL_SURFACE_PREFERRED_BUFFER_SCALE_SINCE_VERSION) {
1499 return;
1500 }
1501
1502 if (window->num_outputs != 0) {
1503 // Check every display's factor, use the highest
1504 factor = 0.0;
1505 for (i = 0; i < window->num_outputs; i++) {
1506 SDL_DisplayData *internal = window->outputs[i];
1507 factor = SDL_max(factor, internal->scale_factor);
1508 }
1509 } else {
1510 // All outputs removed, just fall back.
1511 factor = window->scale_factor;
1512 }
1513
1514 Wayland_HandlePreferredScaleChanged(window, factor);
1515}
1516
1517void Wayland_RemoveOutputFromWindow(SDL_WindowData *window, SDL_DisplayData *display_data)
1518{
1519 for (int i = 0; i < window->num_outputs; i++) {
1520 if (window->outputs[i] == display_data) { // remove this one
1521 if (i == (window->num_outputs - 1)) {
1522 window->outputs[i] = NULL;
1523 } else {
1524 SDL_memmove(&window->outputs[i],
1525 &window->outputs[i + 1],
1526 sizeof(SDL_DisplayData *) * ((window->num_outputs - i) - 1));
1527 }
1528 window->num_outputs--;
1529 i--;
1530 }
1531 }
1532
1533 if (window->num_outputs == 0) {
1534 SDL_free(window->outputs);
1535 window->outputs = NULL;
1536 } else if (!window->is_fullscreen || window->num_outputs == 1) {
1537 Wayland_move_window(window->sdlwindow);
1538 Wayland_MaybeUpdateScaleFactor(window);
1539 }
1540}
1541
1542static void handle_surface_enter(void *data, struct wl_surface *surface, struct wl_output *output)
1543{
1544 SDL_WindowData *window = data;
1545 SDL_DisplayData *internal = wl_output_get_user_data(output);
1546 SDL_DisplayData **new_outputs;
1547
1548 if (!SDL_WAYLAND_own_output(output) || !SDL_WAYLAND_own_surface(surface)) {
1549 return;
1550 }
1551
1552 new_outputs = SDL_realloc(window->outputs,
1553 sizeof(SDL_DisplayData *) * (window->num_outputs + 1));
1554 if (!new_outputs) {
1555 return;
1556 }
1557 window->outputs = new_outputs;
1558 window->outputs[window->num_outputs++] = internal;
1559
1560 // Update the scale factor after the move so that fullscreen outputs are updated.
1561 if (!window->is_fullscreen || window->num_outputs == 1) {
1562 Wayland_move_window(window->sdlwindow);
1563 Wayland_MaybeUpdateScaleFactor(window);
1564 }
1565}
1566
1567static void handle_surface_leave(void *data, struct wl_surface *surface, struct wl_output *output)
1568{
1569 SDL_WindowData *window = (SDL_WindowData *)data;
1570
1571 if (!SDL_WAYLAND_own_output(output) || !SDL_WAYLAND_own_surface(surface)) {
1572 return;
1573 }
1574
1575 Wayland_RemoveOutputFromWindow(window, (SDL_DisplayData *)wl_output_get_user_data(output));
1576}
1577
1578static void handle_preferred_buffer_scale(void *data, struct wl_surface *wl_surface, int32_t factor)
1579{
1580 SDL_WindowData *wind = data;
1581
1582 /* The spec is unclear on how this interacts with the fractional scaling protocol,
1583 * so, for now, assume that the fractional scaling protocol takes priority and
1584 * only listen to this event if the fractional scaling protocol is not present.
1585 */
1586 if (!wind->fractional_scale) {
1587 Wayland_HandlePreferredScaleChanged(data, (double)factor);
1588 }
1589}
1590
1591static void handle_preferred_buffer_transform(void *data, struct wl_surface *wl_surface, uint32_t transform)
1592{
1593 // Nothing to do here.
1594}
1595
1596static const struct wl_surface_listener surface_listener = {
1597 handle_surface_enter,
1598 handle_surface_leave,
1599 handle_preferred_buffer_scale,
1600 handle_preferred_buffer_transform
1601};
1602
1603static void handle_preferred_fractional_scale(void *data, struct wp_fractional_scale_v1 *wp_fractional_scale_v1, uint32_t scale)
1604{
1605 const double factor = (double)scale / 120.; // 120 is a magic number defined in the spec as a common denominator
1606 Wayland_HandlePreferredScaleChanged(data, factor);
1607}
1608
1609static const struct wp_fractional_scale_v1_listener fractional_scale_listener = {
1610 handle_preferred_fractional_scale
1611};
1612
1613static void frog_preferred_metadata_handler(void *data, struct frog_color_managed_surface *frog_color_managed_surface, uint32_t transfer_function,
1614 uint32_t output_display_primary_red_x, uint32_t output_display_primary_red_y,
1615 uint32_t output_display_primary_green_x, uint32_t output_display_primary_green_y,
1616 uint32_t output_display_primary_blue_x, uint32_t output_display_primary_blue_y,
1617 uint32_t output_white_point_x, uint32_t output_white_point_y,
1618 uint32_t max_luminance, uint32_t min_luminance,
1619 uint32_t max_full_frame_luminance)
1620{
1621 SDL_WindowData *wind = (SDL_WindowData *)data;
1622 SDL_HDROutputProperties HDR;
1623
1624 SDL_zero(HDR);
1625
1626 switch (transfer_function) {
1627 case FROG_COLOR_MANAGED_SURFACE_TRANSFER_FUNCTION_ST2084_PQ:
1628 /* ITU-R BT.2408-7 (Sept 2023) has the reference PQ white level at 203 nits,
1629 * while older Dolby documentation claims a reference level of 100 nits.
1630 *
1631 * Use 203 nits for now.
1632 */
1633 HDR.HDR_headroom = max_luminance / 203.0f;
1634 break;
1635 case FROG_COLOR_MANAGED_SURFACE_TRANSFER_FUNCTION_SCRGB_LINEAR:
1636 HDR.HDR_headroom = max_luminance / 80.0f;
1637 break;
1638 case FROG_COLOR_MANAGED_SURFACE_TRANSFER_FUNCTION_UNDEFINED:
1639 case FROG_COLOR_MANAGED_SURFACE_TRANSFER_FUNCTION_SRGB:
1640 case FROG_COLOR_MANAGED_SURFACE_TRANSFER_FUNCTION_GAMMA_22:
1641 default:
1642 HDR.HDR_headroom = 1.0f;
1643 break;
1644 }
1645
1646 HDR.SDR_white_level = 1.0f;
1647 SDL_SetWindowHDRProperties(wind->sdlwindow, &HDR, true);
1648}
1649
1650static const struct frog_color_managed_surface_listener frog_surface_listener = {
1651 frog_preferred_metadata_handler
1652};
1653
1654static void feedback_surface_preferred_changed(void *data,
1655 struct wp_color_management_surface_feedback_v1 *wp_color_management_surface_feedback_v1,
1656 uint32_t identity)
1657{
1658 SDL_WindowData *wind = (SDL_WindowData *)data;
1659 Wayland_GetColorInfoForWindow(wind, false);
1660}
1661
1662static const struct wp_color_management_surface_feedback_v1_listener color_management_surface_feedback_listener = {
1663 feedback_surface_preferred_changed
1664};
1665
1666static void SetKeyboardFocus(SDL_Window *window, bool set_focus)
1667{
1668 SDL_Window *toplevel = window;
1669
1670 // Find the toplevel parent
1671 while (SDL_WINDOW_IS_POPUP(toplevel)) {
1672 toplevel = toplevel->parent;
1673 }
1674
1675 toplevel->internal->keyboard_focus = window;
1676
1677 if (set_focus && !window->is_hiding && !window->is_destroying) {
1678 SDL_SetKeyboardFocus(window);
1679 }
1680}
1681
1682bool Wayland_SetWindowHitTest(SDL_Window *window, bool enabled)
1683{
1684 return true; // just succeed, the real work is done elsewhere.
1685}
1686
1687static struct xdg_toplevel *GetToplevelForWindow(SDL_WindowData *wind)
1688{
1689 if (wind) {
1690 /* Libdecor crashes on attempts to unset the parent by passing null, which is allowed by the
1691 * toplevel spec, so just use the raw xdg-toplevel instead (that's what libdecor does
1692 * internally anyways).
1693 */
1694#ifdef HAVE_LIBDECOR_H
1695 if (wind->shell_surface_type == WAYLAND_SHELL_SURFACE_TYPE_LIBDECOR && wind->shell_surface.libdecor.frame) {
1696 return libdecor_frame_get_xdg_toplevel(wind->shell_surface.libdecor.frame);
1697 } else
1698#endif
1699 if (wind->shell_surface_type == WAYLAND_SHELL_SURFACE_TYPE_XDG_TOPLEVEL && wind->shell_surface.xdg.toplevel.xdg_toplevel) {
1700 return wind->shell_surface.xdg.toplevel.xdg_toplevel;
1701 }
1702 }
1703
1704 return NULL;
1705}
1706
1707bool Wayland_SetWindowParent(SDL_VideoDevice *_this, SDL_Window *window, SDL_Window *parent_window)
1708{
1709 SDL_WindowData *child_data = window->internal;
1710 SDL_WindowData *parent_data = parent_window ? parent_window->internal : NULL;
1711
1712 child_data->reparenting_required = false;
1713
1714 if (parent_data && parent_data->shell_surface_status != WAYLAND_SHELL_SURFACE_STATUS_SHOWN) {
1715 // Need to wait for the parent to become mapped, or it's the same as setting a null parent.
1716 child_data->reparenting_required = true;
1717 return true;
1718 }
1719
1720 struct xdg_toplevel *child_toplevel = GetToplevelForWindow(child_data);
1721 struct xdg_toplevel *parent_toplevel = GetToplevelForWindow(parent_data);
1722
1723 if (child_toplevel) {
1724 xdg_toplevel_set_parent(child_toplevel, parent_toplevel);
1725 }
1726
1727 return true;
1728}
1729
1730bool Wayland_SetWindowModal(SDL_VideoDevice *_this, SDL_Window *window, bool modal)
1731{
1732 SDL_VideoData *viddata = _this->internal;
1733 SDL_WindowData *data = window->internal;
1734 SDL_WindowData *parent_data = window->parent->internal;
1735
1736 if (parent_data->shell_surface_status != WAYLAND_SHELL_SURFACE_STATUS_SHOWN) {
1737 // Need to wait for the parent to become mapped before changing modal status.
1738 data->reparenting_required = true;
1739 return true;
1740 } else {
1741 data->reparenting_required = false;
1742 }
1743
1744 struct xdg_toplevel *toplevel = GetToplevelForWindow(data);
1745
1746 if (toplevel) {
1747 if (viddata->xdg_wm_dialog_v1) {
1748 if (modal) {
1749 if (!data->xdg_dialog_v1) {
1750 data->xdg_dialog_v1 = xdg_wm_dialog_v1_get_xdg_dialog(viddata->xdg_wm_dialog_v1, toplevel);
1751 }
1752
1753 xdg_dialog_v1_set_modal(data->xdg_dialog_v1);
1754 } else if (data->xdg_dialog_v1) {
1755 xdg_dialog_v1_unset_modal(data->xdg_dialog_v1);
1756 }
1757 }
1758 }
1759
1760 return true;
1761}
1762
1763static void show_hide_sync_handler(void *data, struct wl_callback *callback, uint32_t callback_data)
1764{
1765 // Get the window from the ID as it may have been destroyed
1766 SDL_WindowID windowID = (SDL_WindowID)((uintptr_t)data);
1767 SDL_Window *window = SDL_GetWindowFromID(windowID);
1768
1769 if (window && window->internal) {
1770 SDL_WindowData *wind = window->internal;
1771 wind->show_hide_sync_required = false;
1772 }
1773
1774 wl_callback_destroy(callback);
1775}
1776
1777static struct wl_callback_listener show_hide_sync_listener = {
1778 show_hide_sync_handler
1779};
1780
1781static void exported_handle_handler(void *data, struct zxdg_exported_v2 *zxdg_exported_v2, const char *handle)
1782{
1783 SDL_WindowData *wind = (SDL_WindowData*)data;
1784 SDL_PropertiesID props = SDL_GetWindowProperties(wind->sdlwindow);
1785
1786 SDL_SetStringProperty(props, SDL_PROP_WINDOW_WAYLAND_XDG_TOPLEVEL_EXPORT_HANDLE_STRING, handle);
1787}
1788
1789static struct zxdg_exported_v2_listener exported_v2_listener = {
1790 exported_handle_handler
1791};
1792
1793void Wayland_ShowWindow(SDL_VideoDevice *_this, SDL_Window *window)
1794{
1795 SDL_VideoData *c = _this->internal;
1796 SDL_WindowData *data = window->internal;
1797 SDL_PropertiesID props = SDL_GetWindowProperties(window);
1798
1799 // Custom surfaces don't get toplevels and are always considered 'shown'; nothing to do here.
1800 if (data->shell_surface_type == WAYLAND_SHELL_SURFACE_TYPE_CUSTOM) {
1801 return;
1802 }
1803
1804 /* If this is a child window, the parent *must* be in the final shown state,
1805 * meaning that it has received a configure event, followed by a frame callback.
1806 * If not, a race condition can result, with effects ranging from the child
1807 * window to spuriously closing to protocol errors.
1808 *
1809 * If waiting on the parent window, set the pending status and the window will
1810 * be shown when the parent is in the shown state.
1811 */
1812 if (window->parent) {
1813 if (window->parent->internal->shell_surface_status != WAYLAND_SHELL_SURFACE_STATUS_SHOWN) {
1814 data->shell_surface_status = WAYLAND_SHELL_SURFACE_STATUS_SHOW_PENDING;
1815 return;
1816 }
1817 }
1818
1819 /* The window was hidden, but the sync point hasn't yet been reached.
1820 * Pump events to avoid a possible protocol violation.
1821 */
1822 if (data->show_hide_sync_required) {
1823 WAYLAND_wl_display_roundtrip(c->display);
1824 }
1825
1826 data->shell_surface_status = WAYLAND_SHELL_SURFACE_STATUS_WAITING_FOR_CONFIGURE;
1827
1828 /* Detach any previous buffers before resetting everything, otherwise when
1829 * calling this a second time you'll get an annoying protocol error!
1830 *
1831 * FIXME: This was originally moved to HideWindow, which _should_ make
1832 * sense, but for whatever reason UE5's popups require that this actually
1833 * be in both places at once? Possibly from renderers making commits? I can't
1834 * fully remember if this location caused crashes or if I was fixing a pair
1835 * of Hide/Show calls. In any case, UE gives us a pretty good test and having
1836 * both detach calls passes. This bug may be relevant if I'm wrong:
1837 *
1838 * https://bugs.kde.org/show_bug.cgi?id=448856
1839 *
1840 * -flibit
1841 */
1842 wl_surface_attach(data->surface, NULL, 0, 0);
1843 wl_surface_commit(data->surface);
1844
1845 // Create the shell surface and map the toplevel/popup
1846#ifdef HAVE_LIBDECOR_H
1847 if (data->shell_surface_type == WAYLAND_SHELL_SURFACE_TYPE_LIBDECOR) {
1848 data->shell_surface.libdecor.frame = libdecor_decorate(c->shell.libdecor,
1849 data->surface,
1850 &libdecor_frame_interface,
1851 data);
1852 if (!data->shell_surface.libdecor.frame) {
1853 SDL_LogError(SDL_LOG_CATEGORY_VIDEO, "Failed to create libdecor frame!");
1854 } else {
1855 libdecor_frame_set_app_id(data->shell_surface.libdecor.frame, data->app_id);
1856 libdecor_frame_map(data->shell_surface.libdecor.frame);
1857 if (window->flags & SDL_WINDOW_BORDERLESS) {
1858 // Note: Calling this with 'true' immediately after mapping will cause the libdecor Cairo plugin to crash.
1859 libdecor_frame_set_visibility(data->shell_surface.libdecor.frame, false);
1860 }
1861
1862 if (c->zxdg_exporter_v2) {
1863 data->exported = zxdg_exporter_v2_export_toplevel(c->zxdg_exporter_v2, data->surface);
1864 zxdg_exported_v2_add_listener(data->exported, &exported_v2_listener, data);
1865 }
1866
1867 if (c->xdg_toplevel_icon_manager_v1 && data->xdg_toplevel_icon_v1) {
1868 xdg_toplevel_icon_manager_v1_set_icon(_this->internal->xdg_toplevel_icon_manager_v1,
1869 libdecor_frame_get_xdg_toplevel(data->shell_surface.libdecor.frame),
1870 data->xdg_toplevel_icon_v1);
1871 }
1872
1873 SDL_SetPointerProperty(props, SDL_PROP_WINDOW_WAYLAND_XDG_SURFACE_POINTER, libdecor_frame_get_xdg_surface(data->shell_surface.libdecor.frame));
1874 SDL_SetPointerProperty(props, SDL_PROP_WINDOW_WAYLAND_XDG_TOPLEVEL_POINTER, libdecor_frame_get_xdg_toplevel(data->shell_surface.libdecor.frame));
1875 }
1876 } else
1877#endif
1878 if (data->shell_surface_type == WAYLAND_SHELL_SURFACE_TYPE_XDG_TOPLEVEL || data->shell_surface_type == WAYLAND_SHELL_SURFACE_TYPE_XDG_POPUP) {
1879 data->shell_surface.xdg.surface = xdg_wm_base_get_xdg_surface(c->shell.xdg, data->surface);
1880 xdg_surface_set_user_data(data->shell_surface.xdg.surface, data);
1881 xdg_surface_add_listener(data->shell_surface.xdg.surface, &shell_surface_listener_xdg, data);
1882 SDL_SetPointerProperty(SDL_GetWindowProperties(window), SDL_PROP_WINDOW_WAYLAND_XDG_SURFACE_POINTER, data->shell_surface.xdg.surface);
1883
1884 if (data->shell_surface_type == WAYLAND_SHELL_SURFACE_TYPE_XDG_POPUP) {
1885 SDL_Window *parent = window->parent;
1886 SDL_WindowData *parent_data = parent->internal;
1887 struct xdg_surface *parent_xdg_surface = NULL;
1888 int position_x = 0, position_y = 0;
1889
1890 // Configure the popup parameters
1891#ifdef HAVE_LIBDECOR_H
1892 if (parent_data->shell_surface_type == WAYLAND_SHELL_SURFACE_TYPE_LIBDECOR) {
1893 parent_xdg_surface = libdecor_frame_get_xdg_surface(parent_data->shell_surface.libdecor.frame);
1894 } else
1895#endif
1896 if (parent_data->shell_surface_type == WAYLAND_SHELL_SURFACE_TYPE_XDG_TOPLEVEL ||
1897 parent_data->shell_surface_type == WAYLAND_SHELL_SURFACE_TYPE_XDG_POPUP) {
1898 parent_xdg_surface = parent_data->shell_surface.xdg.surface;
1899 }
1900
1901 // Set up the positioner for the popup and configure the constraints
1902 data->shell_surface.xdg.popup.xdg_positioner = xdg_wm_base_create_positioner(c->shell.xdg);
1903 xdg_positioner_set_anchor(data->shell_surface.xdg.popup.xdg_positioner, XDG_POSITIONER_ANCHOR_TOP_LEFT);
1904 xdg_positioner_set_anchor_rect(data->shell_surface.xdg.popup.xdg_positioner, 0, 0, parent->internal->current.logical_width, parent->internal->current.logical_width);
1905 xdg_positioner_set_constraint_adjustment(data->shell_surface.xdg.popup.xdg_positioner,
1906 XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_SLIDE_X | XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_SLIDE_Y);
1907 xdg_positioner_set_gravity(data->shell_surface.xdg.popup.xdg_positioner, XDG_POSITIONER_GRAVITY_BOTTOM_RIGHT);
1908 xdg_positioner_set_size(data->shell_surface.xdg.popup.xdg_positioner, data->current.logical_width, data->current.logical_height);
1909
1910 // Set the popup initial position
1911 position_x = window->last_position_pending ? window->pending.x : window->x;
1912 position_y = window->last_position_pending ? window->pending.y : window->y;
1913 EnsurePopupPositionIsValid(window, &position_x, &position_y);
1914 if (data->scale_to_display) {
1915 position_x = PixelToPoint(window->parent, position_x);
1916 position_y = PixelToPoint(window->parent, position_y);
1917 }
1918 AdjustPopupOffset(window, &position_x, &position_y);
1919 xdg_positioner_set_offset(data->shell_surface.xdg.popup.xdg_positioner, position_x, position_y);
1920
1921 // Assign the popup role
1922 data->shell_surface.xdg.popup.xdg_popup = xdg_surface_get_popup(data->shell_surface.xdg.surface,
1923 parent_xdg_surface,
1924 data->shell_surface.xdg.popup.xdg_positioner);
1925 xdg_popup_add_listener(data->shell_surface.xdg.popup.xdg_popup, &popup_listener_xdg, data);
1926
1927 if (window->flags & SDL_WINDOW_TOOLTIP) {
1928 struct wl_region *region;
1929
1930 // Tooltips can't be interacted with, so turn off the input region to avoid blocking anything behind them
1931 region = wl_compositor_create_region(c->compositor);
1932 wl_region_add(region, 0, 0, 0, 0);
1933 wl_surface_set_input_region(data->surface, region);
1934 wl_region_destroy(region);
1935 } else if (window->flags & SDL_WINDOW_POPUP_MENU) {
1936 SetKeyboardFocus(window, window->parent == SDL_GetKeyboardFocus());
1937 }
1938
1939 SDL_SetPointerProperty(props, SDL_PROP_WINDOW_WAYLAND_XDG_POPUP_POINTER, data->shell_surface.xdg.popup.xdg_popup);
1940 SDL_SetPointerProperty(props, SDL_PROP_WINDOW_WAYLAND_XDG_POSITIONER_POINTER, data->shell_surface.xdg.popup.xdg_positioner);
1941 } else {
1942 data->shell_surface.xdg.toplevel.xdg_toplevel = xdg_surface_get_toplevel(data->shell_surface.xdg.surface);
1943 xdg_toplevel_set_app_id(data->shell_surface.xdg.toplevel.xdg_toplevel, data->app_id);
1944 xdg_toplevel_add_listener(data->shell_surface.xdg.toplevel.xdg_toplevel, &toplevel_listener_xdg, data);
1945
1946 // Create the window decorations
1947 if (c->decoration_manager) {
1948 data->server_decoration = zxdg_decoration_manager_v1_get_toplevel_decoration(c->decoration_manager, data->shell_surface.xdg.toplevel.xdg_toplevel);
1949 zxdg_toplevel_decoration_v1_add_listener(data->server_decoration, &decoration_listener, window);
1950 const enum zxdg_toplevel_decoration_v1_mode mode = !(window->flags & SDL_WINDOW_BORDERLESS) ? ZXDG_TOPLEVEL_DECORATION_V1_MODE_SERVER_SIDE : ZXDG_TOPLEVEL_DECORATION_V1_MODE_CLIENT_SIDE;
1951 zxdg_toplevel_decoration_v1_set_mode(data->server_decoration, mode);
1952 }
1953
1954 if (c->zxdg_exporter_v2) {
1955 data->exported = zxdg_exporter_v2_export_toplevel(c->zxdg_exporter_v2, data->surface);
1956 zxdg_exported_v2_add_listener(data->exported, &exported_v2_listener, data);
1957 }
1958
1959 if (c->xdg_toplevel_icon_manager_v1 && data->xdg_toplevel_icon_v1) {
1960 xdg_toplevel_icon_manager_v1_set_icon(_this->internal->xdg_toplevel_icon_manager_v1,
1961 data->shell_surface.xdg.toplevel.xdg_toplevel,
1962 data->xdg_toplevel_icon_v1);
1963 }
1964
1965 SDL_SetPointerProperty(props, SDL_PROP_WINDOW_WAYLAND_XDG_TOPLEVEL_POINTER, data->shell_surface.xdg.toplevel.xdg_toplevel);
1966 }
1967 }
1968
1969 // Restore state that was set prior to this call
1970 Wayland_SetWindowParent(_this, window, window->parent);
1971
1972 if (window->flags & SDL_WINDOW_MODAL) {
1973 Wayland_SetWindowModal(_this, window, true);
1974 }
1975
1976 Wayland_SetWindowTitle(_this, window);
1977
1978 /* We have to wait until the surface gets a "configure" event, or use of
1979 * this surface will fail. This is a new rule for xdg_shell.
1980 */
1981#ifdef HAVE_LIBDECOR_H
1982 if (data->shell_surface_type == WAYLAND_SHELL_SURFACE_TYPE_LIBDECOR) {
1983 if (data->shell_surface.libdecor.frame) {
1984 while (!data->shell_surface.libdecor.initial_configure_seen) {
1985 WAYLAND_wl_display_flush(c->display);
1986 WAYLAND_wl_display_dispatch(c->display);
1987 }
1988 }
1989 } else
1990#endif
1991 if (data->shell_surface_type == WAYLAND_SHELL_SURFACE_TYPE_XDG_POPUP || data->shell_surface_type == WAYLAND_SHELL_SURFACE_TYPE_XDG_TOPLEVEL) {
1992 /* Unlike libdecor we need to call this explicitly to prevent a deadlock.
1993 * libdecor will call this as part of their configure event!
1994 * -flibit
1995 */
1996 wl_surface_commit(data->surface);
1997 if (data->shell_surface.xdg.surface) {
1998 while (!data->shell_surface.xdg.initial_configure_seen) {
1999 WAYLAND_wl_display_flush(c->display);
2000 WAYLAND_wl_display_dispatch(c->display);
2001 }
2002 }
2003 } else {
2004 // Nothing to see here, just commit.
2005 wl_surface_commit(data->surface);
2006 }
2007
2008 // Make sure the window can't be resized to 0 or it can be spuriously closed by the window manager.
2009 data->system_limits.min_width = SDL_max(data->system_limits.min_width, 1);
2010 data->system_limits.min_height = SDL_max(data->system_limits.min_height, 1);
2011
2012 /* Unlike the rest of window state we have to set this _after_ flushing the
2013 * display, because we need to create the decorations before possibly hiding
2014 * them immediately afterward.
2015 */
2016#ifdef HAVE_LIBDECOR_H
2017 if (data->shell_surface_type == WAYLAND_SHELL_SURFACE_TYPE_LIBDECOR) {
2018 // Libdecor plugins can enforce minimum window sizes, so adjust if the initial window size is too small.
2019 if (window->windowed.w < data->system_limits.min_width ||
2020 window->windowed.h < data->system_limits.min_height) {
2021
2022 // Warn if the window frame will be larger than the content surface.
2023 SDL_LogWarn(SDL_LOG_CATEGORY_VIDEO,
2024 "Window dimensions (%i, %i) are smaller than the system enforced minimum (%i, %i); window borders will be larger than the content surface.",
2025 window->windowed.w, window->windowed.h, data->system_limits.min_width, data->system_limits.min_height);
2026
2027 data->current.logical_width = SDL_max(window->windowed.w, data->system_limits.min_width);
2028 data->current.logical_height = SDL_max(window->windowed.h, data->system_limits.min_height);
2029 CommitLibdecorFrame(window);
2030 }
2031 }
2032#endif
2033 Wayland_SetWindowResizable(_this, window, !!(window->flags & SDL_WINDOW_RESIZABLE));
2034
2035 // We're finally done putting the window together, raise if possible
2036 if (c->activation_manager) {
2037 /* Note that we don't check for empty strings, as that is still
2038 * considered a valid activation token!
2039 */
2040 const char *activation_token = SDL_getenv("XDG_ACTIVATION_TOKEN");
2041 if (activation_token) {
2042 xdg_activation_v1_activate(c->activation_manager,
2043 activation_token,
2044 data->surface);
2045
2046 // Clear this variable, per the protocol's request
2047 SDL_unsetenv_unsafe("XDG_ACTIVATION_TOKEN");
2048 }
2049 }
2050
2051 data->show_hide_sync_required = true;
2052 struct wl_callback *cb = wl_display_sync(_this->internal->display);
2053 wl_callback_add_listener(cb, &show_hide_sync_listener, (void*)((uintptr_t)window->id));
2054
2055 // Send an exposure event to signal that the client should draw.
2056 if (data->shell_surface_status == WAYLAND_SHELL_SURFACE_STATUS_WAITING_FOR_FRAME) {
2057 SDL_SendWindowEvent(window, SDL_EVENT_WINDOW_EXPOSED, 0, 0);
2058 }
2059}
2060
2061static void Wayland_ReleasePopup(SDL_VideoDevice *_this, SDL_Window *popup)
2062{
2063 SDL_WindowData *popupdata;
2064
2065 // Basic sanity checks to weed out the weird popup closures
2066 if (!SDL_ObjectValid(popup, SDL_OBJECT_TYPE_WINDOW)) {
2067 return;
2068 }
2069 popupdata = popup->internal;
2070 if (!popupdata) {
2071 return;
2072 }
2073
2074 // This may already be freed by a parent popup!
2075 if (popupdata->shell_surface.xdg.popup.xdg_popup == NULL) {
2076 return;
2077 }
2078
2079 if (popup->flags & SDL_WINDOW_POPUP_MENU) {
2080 SDL_Window *new_focus = popup->parent;
2081 bool set_focus = popup == SDL_GetKeyboardFocus();
2082
2083 // Find the highest level window, up to the toplevel parent, that isn't being hidden or destroyed.
2084 while (SDL_WINDOW_IS_POPUP(new_focus) && (new_focus->is_hiding || new_focus->is_destroying)) {
2085 new_focus = new_focus->parent;
2086
2087 // If some window in the chain currently had focus, set it to the new lowest-level window.
2088 if (!set_focus) {
2089 set_focus = new_focus == SDL_GetKeyboardFocus();
2090 }
2091 }
2092
2093 SetKeyboardFocus(new_focus, set_focus);
2094 }
2095
2096 xdg_popup_destroy(popupdata->shell_surface.xdg.popup.xdg_popup);
2097 xdg_positioner_destroy(popupdata->shell_surface.xdg.popup.xdg_positioner);
2098 popupdata->shell_surface.xdg.popup.xdg_popup = NULL;
2099 popupdata->shell_surface.xdg.popup.xdg_positioner = NULL;
2100
2101 SDL_PropertiesID props = SDL_GetWindowProperties(popup);
2102 SDL_SetPointerProperty(props, SDL_PROP_WINDOW_WAYLAND_XDG_POPUP_POINTER, NULL);
2103 SDL_SetPointerProperty(props, SDL_PROP_WINDOW_WAYLAND_XDG_POSITIONER_POINTER, NULL);
2104}
2105
2106void Wayland_HideWindow(SDL_VideoDevice *_this, SDL_Window *window)
2107{
2108 SDL_VideoData *data = _this->internal;
2109 SDL_WindowData *wind = window->internal;
2110 SDL_PropertiesID props = SDL_GetWindowProperties(window);
2111
2112 // Custom surfaces have nothing to destroy and are always considered to be 'shown'; nothing to do here.
2113 if (wind->shell_surface_type == WAYLAND_SHELL_SURFACE_TYPE_CUSTOM) {
2114 return;
2115 }
2116
2117 /* The window was shown, but the sync point hasn't yet been reached.
2118 * Pump events to avoid a possible protocol violation.
2119 */
2120 if (wind->show_hide_sync_required) {
2121 WAYLAND_wl_display_roundtrip(data->display);
2122 }
2123
2124 wind->shell_surface_status = WAYLAND_SHELL_SURFACE_STATUS_HIDDEN;
2125
2126 if (wind->server_decoration) {
2127 zxdg_toplevel_decoration_v1_destroy(wind->server_decoration);
2128 wind->server_decoration = NULL;
2129 }
2130
2131 // Be sure to detach after this is done, otherwise ShowWindow crashes!
2132 if (wind->shell_surface_type != WAYLAND_SHELL_SURFACE_TYPE_XDG_POPUP) {
2133 wl_surface_attach(wind->surface, NULL, 0, 0);
2134 wl_surface_commit(wind->surface);
2135 }
2136
2137 // Clean up the export handle.
2138 if (wind->exported) {
2139 zxdg_exported_v2_destroy(wind->exported);
2140 wind->exported = NULL;
2141
2142 SDL_SetStringProperty(props, SDL_PROP_WINDOW_WAYLAND_XDG_TOPLEVEL_EXPORT_HANDLE_STRING, NULL);
2143 }
2144
2145 if (wind->xdg_dialog_v1) {
2146 xdg_dialog_v1_destroy(wind->xdg_dialog_v1);
2147 wind->xdg_dialog_v1 = NULL;
2148 }
2149
2150#ifdef HAVE_LIBDECOR_H
2151 if (wind->shell_surface_type == WAYLAND_SHELL_SURFACE_TYPE_LIBDECOR) {
2152 if (wind->shell_surface.libdecor.frame) {
2153 libdecor_frame_unref(wind->shell_surface.libdecor.frame);
2154 wind->shell_surface.libdecor.frame = NULL;
2155
2156 SDL_SetPointerProperty(props, SDL_PROP_WINDOW_WAYLAND_XDG_SURFACE_POINTER, NULL);
2157 SDL_SetPointerProperty(props, SDL_PROP_WINDOW_WAYLAND_XDG_TOPLEVEL_POINTER, NULL);
2158 }
2159 } else
2160#endif
2161 if (wind->shell_surface_type == WAYLAND_SHELL_SURFACE_TYPE_XDG_POPUP) {
2162 Wayland_ReleasePopup(_this, window);
2163 } else if (wind->shell_surface.xdg.toplevel.xdg_toplevel) {
2164 xdg_toplevel_destroy(wind->shell_surface.xdg.toplevel.xdg_toplevel);
2165 wind->shell_surface.xdg.toplevel.xdg_toplevel = NULL;
2166 SDL_SetPointerProperty(props, SDL_PROP_WINDOW_WAYLAND_XDG_TOPLEVEL_POINTER, NULL);
2167 }
2168 if (wind->shell_surface.xdg.surface) {
2169 xdg_surface_destroy(wind->shell_surface.xdg.surface);
2170 wind->shell_surface.xdg.surface = NULL;
2171 SDL_SetPointerProperty(props, SDL_PROP_WINDOW_WAYLAND_XDG_SURFACE_POINTER, NULL);
2172 }
2173
2174 wind->show_hide_sync_required = true;
2175 struct wl_callback *cb = wl_display_sync(_this->internal->display);
2176 wl_callback_add_listener(cb, &show_hide_sync_listener, (void*)((uintptr_t)window->id));
2177}
2178
2179static void handle_xdg_activation_done(void *data,
2180 struct xdg_activation_token_v1 *xdg_activation_token_v1,
2181 const char *token)
2182{
2183 SDL_WindowData *window = data;
2184 if (xdg_activation_token_v1 == window->activation_token) {
2185 xdg_activation_v1_activate(window->waylandData->activation_manager,
2186 token,
2187 window->surface);
2188 xdg_activation_token_v1_destroy(window->activation_token);
2189 window->activation_token = NULL;
2190 }
2191}
2192
2193static const struct xdg_activation_token_v1_listener activation_listener_xdg = {
2194 handle_xdg_activation_done
2195};
2196
2197/* The xdg-activation protocol considers "activation" to be one of two things:
2198 *
2199 * 1: Raising a window to the top and flashing the titlebar
2200 * 2: Flashing the titlebar while keeping the window where it is
2201 *
2202 * As you might expect from Wayland, the general policy is to go with #2 unless
2203 * the client can prove to the compositor beyond a reasonable doubt that raising
2204 * the window will not be malicuous behavior.
2205 *
2206 * For SDL this means RaiseWindow and FlashWindow both use the same protocol,
2207 * but in different ways: RaiseWindow will provide as _much_ information as
2208 * possible while FlashWindow will provide as _little_ information as possible,
2209 * to nudge the compositor into doing what we want.
2210 *
2211 * This isn't _strictly_ what the protocol says will happen, but this is what
2212 * current implementations are doing (as of writing, YMMV in the far distant
2213 * future).
2214 *
2215 * -flibit
2216 */
2217static void Wayland_activate_window(SDL_VideoData *data, SDL_WindowData *target_wind, bool set_serial)
2218{
2219 struct SDL_WaylandInput * input = data->input;
2220 SDL_Window *focus = SDL_GetKeyboardFocus();
2221 struct wl_surface *requesting_surface = focus ? focus->internal->surface : NULL;
2222
2223 if (data->activation_manager) {
2224 if (target_wind->activation_token) {
2225 // We're about to overwrite this with a new request
2226 xdg_activation_token_v1_destroy(target_wind->activation_token);
2227 }
2228
2229 target_wind->activation_token = xdg_activation_v1_get_activation_token(data->activation_manager);
2230 xdg_activation_token_v1_add_listener(target_wind->activation_token,
2231 &activation_listener_xdg,
2232 target_wind);
2233
2234 /* Note that we are not setting the app_id here.
2235 *
2236 * Hypothetically we could set the app_id from data->classname, but
2237 * that part of the API is for _external_ programs, not ourselves.
2238 *
2239 * -flibit
2240 */
2241 if (requesting_surface) {
2242 // This specifies the surface from which the activation request is originating, not the activation target surface.
2243 xdg_activation_token_v1_set_surface(target_wind->activation_token, requesting_surface);
2244 }
2245 if (set_serial && input && input->seat) {
2246 xdg_activation_token_v1_set_serial(target_wind->activation_token, input->last_implicit_grab_serial, input->seat);
2247 }
2248 xdg_activation_token_v1_commit(target_wind->activation_token);
2249 }
2250}
2251
2252void Wayland_RaiseWindow(SDL_VideoDevice *_this, SDL_Window *window)
2253{
2254 Wayland_activate_window(_this->internal, window->internal, true);
2255}
2256
2257bool Wayland_FlashWindow(SDL_VideoDevice *_this, SDL_Window *window, SDL_FlashOperation operation)
2258{
2259 /* Not setting the serial will specify 'urgency' without switching focus as per
2260 * https://gitlab.freedesktop.org/wayland/wayland-protocols/-/merge_requests/9#note_854977
2261 */
2262 Wayland_activate_window(_this->internal, window->internal, false);
2263 return true;
2264}
2265
2266SDL_FullscreenResult Wayland_SetWindowFullscreen(SDL_VideoDevice *_this, SDL_Window *window,
2267 SDL_VideoDisplay *display, SDL_FullscreenOp fullscreen)
2268{
2269 SDL_WindowData *wind = window->internal;
2270 struct wl_output *output = display->internal->output;
2271
2272 // Custom surfaces have no toplevel to make fullscreen.
2273 if (wind->shell_surface_type == WAYLAND_SHELL_SURFACE_TYPE_CUSTOM) {
2274 return SDL_FULLSCREEN_FAILED;
2275 }
2276
2277 if (wind->show_hide_sync_required) {
2278 WAYLAND_wl_display_roundtrip(_this->internal->display);
2279 }
2280
2281 // Flushing old events pending a new one, ignore this request.
2282 if (wind->drop_fullscreen_requests) {
2283 return SDL_FULLSCREEN_SUCCEEDED;
2284 }
2285
2286 wind->drop_fullscreen_requests = true;
2287 FlushPendingEvents(window);
2288 wind->drop_fullscreen_requests = false;
2289
2290 // Nothing to do if the window is not fullscreen, and this isn't an explicit enter request.
2291 if (!wind->is_fullscreen) {
2292 if (fullscreen == SDL_FULLSCREEN_OP_UPDATE) {
2293 // Request was out of date; signal the video core not to update any state.
2294 return SDL_FULLSCREEN_PENDING;
2295 } else if (fullscreen == SDL_FULLSCREEN_OP_LEAVE) {
2296 // Already not fullscreen; nothing to do.
2297 return SDL_FULLSCREEN_SUCCEEDED;
2298 }
2299 }
2300
2301 // Don't send redundant fullscreen set/unset events.
2302 if (!!fullscreen != wind->is_fullscreen) {
2303 wind->fullscreen_was_positioned = !!fullscreen;
2304 SetFullscreen(window, fullscreen ? output : NULL);
2305 } else if (wind->is_fullscreen) {
2306 /*
2307 * If the window is already fullscreen, this is likely a request to switch between
2308 * fullscreen and fullscreen desktop, change outputs, or change the video mode.
2309 *
2310 * If the window is already positioned on the target output, just update the
2311 * window geometry.
2312 */
2313 if (wind->last_displayID != display->id) {
2314 wind->fullscreen_was_positioned = true;
2315 SetFullscreen(window, output);
2316 } else {
2317 ConfigureWindowGeometry(window);
2318 CommitLibdecorFrame(window);
2319
2320 return SDL_FULLSCREEN_SUCCEEDED;
2321 }
2322 }
2323
2324 return SDL_FULLSCREEN_PENDING;
2325}
2326
2327void Wayland_RestoreWindow(SDL_VideoDevice *_this, SDL_Window *window)
2328{
2329 SDL_WindowData *wind = window->internal;
2330
2331 // Not currently fullscreen or maximized, and no state pending; nothing to do.
2332 if (!(window->flags & (SDL_WINDOW_FULLSCREEN | SDL_WINDOW_MAXIMIZED)) &&
2333 !wind->fullscreen_deadline_count && !wind->maximized_restored_deadline_count) {
2334 return;
2335 }
2336
2337#ifdef HAVE_LIBDECOR_H
2338 if (wind->shell_surface_type == WAYLAND_SHELL_SURFACE_TYPE_LIBDECOR) {
2339 if (!wind->shell_surface.libdecor.frame) {
2340 return; // Can't do anything yet, wait for ShowWindow
2341 }
2342 libdecor_frame_unset_maximized(wind->shell_surface.libdecor.frame);
2343
2344 ++wind->maximized_restored_deadline_count;
2345 struct wl_callback *cb = wl_display_sync(_this->internal->display);
2346 wl_callback_add_listener(cb, &maximized_restored_deadline_listener, (void *)((uintptr_t)window->id));
2347 } else
2348#endif
2349 // Note that xdg-shell does NOT provide a way to unset minimize!
2350 if (wind->shell_surface_type == WAYLAND_SHELL_SURFACE_TYPE_XDG_TOPLEVEL) {
2351 if (wind->shell_surface.xdg.toplevel.xdg_toplevel == NULL) {
2352 return; // Can't do anything yet, wait for ShowWindow
2353 }
2354 xdg_toplevel_unset_maximized(wind->shell_surface.xdg.toplevel.xdg_toplevel);
2355
2356 ++wind->maximized_restored_deadline_count;
2357 struct wl_callback *cb = wl_display_sync(_this->internal->display);
2358 wl_callback_add_listener(cb, &maximized_restored_deadline_listener, (void *)((uintptr_t)window->id));
2359 }
2360}
2361
2362void Wayland_SetWindowBordered(SDL_VideoDevice *_this, SDL_Window *window, bool bordered)
2363{
2364 SDL_WindowData *wind = window->internal;
2365 const SDL_VideoData *viddata = (const SDL_VideoData *)_this->internal;
2366
2367#ifdef HAVE_LIBDECOR_H
2368 if (wind->shell_surface_type == WAYLAND_SHELL_SURFACE_TYPE_LIBDECOR) {
2369 if (wind->shell_surface.libdecor.frame) {
2370 libdecor_frame_set_visibility(wind->shell_surface.libdecor.frame, bordered);
2371 }
2372 } else
2373#endif
2374 if (wind->shell_surface_type == WAYLAND_SHELL_SURFACE_TYPE_XDG_TOPLEVEL) {
2375 if ((viddata->decoration_manager) && (wind->server_decoration)) {
2376 const enum zxdg_toplevel_decoration_v1_mode mode = bordered ? ZXDG_TOPLEVEL_DECORATION_V1_MODE_SERVER_SIDE : ZXDG_TOPLEVEL_DECORATION_V1_MODE_CLIENT_SIDE;
2377 zxdg_toplevel_decoration_v1_set_mode(wind->server_decoration, mode);
2378 }
2379 }
2380}
2381
2382void Wayland_SetWindowResizable(SDL_VideoDevice *_this, SDL_Window *window, bool resizable)
2383{
2384#ifdef HAVE_LIBDECOR_H
2385 const SDL_WindowData *wind = window->internal;
2386
2387 if (wind->shell_surface_type == WAYLAND_SHELL_SURFACE_TYPE_LIBDECOR) {
2388 if (!wind->shell_surface.libdecor.frame) {
2389 return; // Can't do anything yet, wait for ShowWindow
2390 }
2391 if (libdecor_frame_has_capability(wind->shell_surface.libdecor.frame, LIBDECOR_ACTION_RESIZE)) {
2392 if (!resizable) {
2393 libdecor_frame_unset_capabilities(wind->shell_surface.libdecor.frame, LIBDECOR_ACTION_RESIZE);
2394 }
2395 } else if (resizable) {
2396 libdecor_frame_set_capabilities(wind->shell_surface.libdecor.frame, LIBDECOR_ACTION_RESIZE);
2397 }
2398 }
2399#endif
2400
2401 /* When changing the resize capability on libdecor windows, the limits must always
2402 * be reapplied, as when libdecor changes states, it overwrites the values internally.
2403 */
2404 SetMinMaxDimensions(window);
2405 CommitLibdecorFrame(window);
2406}
2407
2408void Wayland_MaximizeWindow(SDL_VideoDevice *_this, SDL_Window *window)
2409{
2410 SDL_VideoData *viddata = _this->internal;
2411 SDL_WindowData *wind = window->internal;
2412
2413 if (wind->show_hide_sync_required) {
2414 WAYLAND_wl_display_roundtrip(_this->internal->display);
2415 }
2416
2417 // Not fullscreen, already maximized, and no state pending; nothing to do.
2418 if (!(window->flags & SDL_WINDOW_FULLSCREEN) && (window->flags & SDL_WINDOW_MAXIMIZED) &&
2419 !wind->fullscreen_deadline_count && !wind->maximized_restored_deadline_count) {
2420 return;
2421 }
2422
2423#ifdef HAVE_LIBDECOR_H
2424 if (wind->shell_surface_type == WAYLAND_SHELL_SURFACE_TYPE_LIBDECOR) {
2425 if (!wind->shell_surface.libdecor.frame) {
2426 return; // Can't do anything yet, wait for ShowWindow
2427 }
2428
2429 // Commit to preserve any pending size data.
2430 wl_surface_commit(wind->surface);
2431 libdecor_frame_set_maximized(wind->shell_surface.libdecor.frame);
2432
2433 ++wind->maximized_restored_deadline_count;
2434 struct wl_callback *cb = wl_display_sync(viddata->display);
2435 wl_callback_add_listener(cb, &maximized_restored_deadline_listener, (void *)((uintptr_t)window->id));
2436 } else
2437#endif
2438 if (wind->shell_surface_type == WAYLAND_SHELL_SURFACE_TYPE_XDG_TOPLEVEL) {
2439 if (wind->shell_surface.xdg.toplevel.xdg_toplevel == NULL) {
2440 return; // Can't do anything yet, wait for ShowWindow
2441 }
2442
2443 // Commit to preserve any pending size data.
2444 wl_surface_commit(wind->surface);
2445 xdg_toplevel_set_maximized(wind->shell_surface.xdg.toplevel.xdg_toplevel);
2446
2447 ++wind->maximized_restored_deadline_count;
2448 struct wl_callback *cb = wl_display_sync(viddata->display);
2449 wl_callback_add_listener(cb, &maximized_restored_deadline_listener, (void *)((uintptr_t)window->id));
2450 }
2451}
2452
2453void Wayland_MinimizeWindow(SDL_VideoDevice *_this, SDL_Window *window)
2454{
2455 SDL_WindowData *wind = window->internal;
2456
2457 if (!(wind->wm_caps & WAYLAND_WM_CAPS_MINIMIZE)) {
2458 return;
2459 }
2460
2461#ifdef HAVE_LIBDECOR_H
2462 if (wind->shell_surface_type == WAYLAND_SHELL_SURFACE_TYPE_LIBDECOR) {
2463 if (!wind->shell_surface.libdecor.frame) {
2464 return; // Can't do anything yet, wait for ShowWindow
2465 }
2466 libdecor_frame_set_minimized(wind->shell_surface.libdecor.frame);
2467 SDL_SendWindowEvent(window, SDL_EVENT_WINDOW_MINIMIZED, 0, 0);
2468 } else
2469#endif
2470 if (wind->shell_surface_type == WAYLAND_SHELL_SURFACE_TYPE_XDG_TOPLEVEL) {
2471 if (wind->shell_surface.xdg.toplevel.xdg_toplevel == NULL) {
2472 return; // Can't do anything yet, wait for ShowWindow
2473 }
2474 xdg_toplevel_set_minimized(wind->shell_surface.xdg.toplevel.xdg_toplevel);
2475 SDL_SendWindowEvent(window, SDL_EVENT_WINDOW_MINIMIZED, 0, 0);
2476 }
2477}
2478
2479bool Wayland_SetWindowMouseRect(SDL_VideoDevice *_this, SDL_Window *window)
2480{
2481 SDL_VideoData *data = _this->internal;
2482
2483 /* This may look suspiciously like SetWindowGrab, despite SetMouseRect not
2484 * implicitly doing a grab. And you're right! Wayland doesn't let us mess
2485 * around with mouse focus whatsoever, so it just happens to be that the
2486 * work that we can do in these two functions ends up being the same.
2487 *
2488 * Just know that this call lets you confine with a rect, SetWindowGrab
2489 * lets you confine without a rect.
2490 */
2491 if (SDL_RectEmpty(&window->mouse_rect) && !(window->flags & SDL_WINDOW_MOUSE_GRABBED)) {
2492 return Wayland_input_unconfine_pointer(data->input, window);
2493 } else {
2494 return Wayland_input_confine_pointer(data->input, window);
2495 }
2496}
2497
2498bool Wayland_SetWindowMouseGrab(SDL_VideoDevice *_this, SDL_Window *window, bool grabbed)
2499{
2500 SDL_VideoData *data = _this->internal;
2501
2502 if (grabbed) {
2503 return Wayland_input_confine_pointer(data->input, window);
2504 } else if (SDL_RectEmpty(&window->mouse_rect)) {
2505 return Wayland_input_unconfine_pointer(data->input, window);
2506 }
2507
2508 return true;
2509}
2510
2511bool Wayland_SetWindowKeyboardGrab(SDL_VideoDevice *_this, SDL_Window *window, bool grabbed)
2512{
2513 SDL_VideoData *data = _this->internal;
2514
2515 if (grabbed) {
2516 return Wayland_input_grab_keyboard(window, data->input);
2517 } else {
2518 return Wayland_input_ungrab_keyboard(window);
2519 }
2520}
2521
2522bool Wayland_CreateWindow(SDL_VideoDevice *_this, SDL_Window *window, SDL_PropertiesID create_props)
2523{
2524 SDL_WindowData *data;
2525 SDL_VideoData *c = _this->internal;
2526 struct wl_surface *external_surface = (struct wl_surface *)SDL_GetPointerProperty(create_props, SDL_PROP_WINDOW_CREATE_WAYLAND_WL_SURFACE_POINTER,
2527 (struct wl_surface *)SDL_GetPointerProperty(create_props, "sdl2-compat.external_window", NULL));
2528 const bool custom_surface_role = (external_surface != NULL) || SDL_GetBooleanProperty(create_props, SDL_PROP_WINDOW_CREATE_WAYLAND_SURFACE_ROLE_CUSTOM_BOOLEAN, false);
2529 const bool create_egl_window = !!(window->flags & SDL_WINDOW_OPENGL) ||
2530 SDL_GetBooleanProperty(create_props, SDL_PROP_WINDOW_CREATE_WAYLAND_CREATE_EGL_WINDOW_BOOLEAN, false);
2531
2532 data = SDL_calloc(1, sizeof(*data));
2533 if (!data) {
2534 return false;
2535 }
2536
2537 window->internal = data;
2538
2539 if (window->x == SDL_WINDOWPOS_UNDEFINED) {
2540 window->x = 0;
2541 }
2542 if (window->y == SDL_WINDOWPOS_UNDEFINED) {
2543 window->y = 0;
2544 }
2545
2546 data->waylandData = c;
2547 data->sdlwindow = window;
2548
2549 // Default to all capabilities
2550 data->wm_caps = WAYLAND_WM_CAPS_ALL;
2551
2552 data->scale_factor = 1.0;
2553
2554 if (SDL_WINDOW_IS_POPUP(window)) {
2555 data->scale_to_display = window->parent->internal->scale_to_display;
2556 data->scale_factor = window->parent->internal->scale_factor;
2557 EnsurePopupPositionIsValid(window, &window->x, &window->y);
2558 } else {
2559 for (int i = 0; i < _this->num_displays; i++) {
2560 data->scale_factor = SDL_max(data->scale_factor, _this->displays[i]->internal->scale_factor);
2561 }
2562 }
2563
2564 data->outputs = NULL;
2565 data->num_outputs = 0;
2566 data->scale_to_display = c->scale_to_display_enabled;
2567
2568 // Cache the app_id at creation time, as it may change before the window is mapped.
2569 data->app_id = SDL_strdup(SDL_GetAppID());
2570
2571 if (!data->scale_to_display) {
2572 data->requested.logical_width = window->floating.w;
2573 data->requested.logical_height = window->floating.h;
2574 } else {
2575 data->requested.logical_width = PixelToPoint(window, window->floating.w);
2576 data->requested.logical_height = PixelToPoint(window, window->floating.h);
2577 data->requested.pixel_width = window->floating.w;
2578 data->requested.pixel_height = window->floating.h;
2579 }
2580
2581 if (!external_surface) {
2582 data->surface = wl_compositor_create_surface(c->compositor);
2583 wl_surface_add_listener(data->surface, &surface_listener, data);
2584 wl_surface_set_user_data(data->surface, data);
2585 SDL_WAYLAND_register_surface(data->surface);
2586 } else {
2587 window->flags |= SDL_WINDOW_EXTERNAL;
2588 data->surface = external_surface;
2589
2590 /* External surfaces are registered by being put in a list, as changing tags or userdata
2591 * can cause problems with external toolkits.
2592 */
2593 Wayland_AddWindowDataToExternalList(data);
2594 }
2595
2596 /* Always attach a viewport and fractional scale manager if available and the surface is not custom/external,
2597 * or the custom/external surface was explicitly flagged as high pixel density aware, which signals that the
2598 * application wants SDL to handle scaling.
2599 */
2600 if (!custom_surface_role || (window->flags & SDL_WINDOW_HIGH_PIXEL_DENSITY)) {
2601 if (c->viewporter) {
2602 data->viewport = wp_viewporter_get_viewport(c->viewporter, data->surface);
2603
2604 // The viewport always uses the entire buffer.
2605 wp_viewport_set_source(data->viewport,
2606 wl_fixed_from_int(-1), wl_fixed_from_int(-1),
2607 wl_fixed_from_int(-1), wl_fixed_from_int(-1));
2608 }
2609 if (c->fractional_scale_manager) {
2610 data->fractional_scale = wp_fractional_scale_manager_v1_get_fractional_scale(c->fractional_scale_manager, data->surface);
2611 wp_fractional_scale_v1_add_listener(data->fractional_scale, &fractional_scale_listener, data);
2612 }
2613 }
2614
2615 if (!custom_surface_role) {
2616 if (c->wp_color_manager_v1) {
2617 data->wp_color_management_surface_feedback = wp_color_manager_v1_get_surface_feedback(c->wp_color_manager_v1, data->surface);
2618 wp_color_management_surface_feedback_v1_add_listener(data->wp_color_management_surface_feedback, &color_management_surface_feedback_listener, data);
2619 Wayland_GetColorInfoForWindow(data, true);
2620 } else if (c->frog_color_management_factory_v1) {
2621 data->frog_color_managed_surface = frog_color_management_factory_v1_get_color_managed_surface(c->frog_color_management_factory_v1, data->surface);
2622 frog_color_managed_surface_add_listener(data->frog_color_managed_surface, &frog_surface_listener, data);
2623 }
2624
2625 if (c->wp_alpha_modifier_v1) {
2626 data->wp_alpha_modifier_surface_v1 = wp_alpha_modifier_v1_get_surface(c->wp_alpha_modifier_v1, data->surface);
2627 wp_alpha_modifier_surface_v1_set_multiplier(data->wp_alpha_modifier_surface_v1, SDL_MAX_UINT32);
2628 }
2629 }
2630
2631 // Must be called before EGL configuration to set the drawable backbuffer size.
2632 ConfigureWindowGeometry(window);
2633
2634 /* Fire a callback when the compositor wants a new frame rendered.
2635 * Right now this only matters for OpenGL; we use this callback to add a
2636 * wait timeout that avoids getting deadlocked by the compositor when the
2637 * window isn't visible.
2638 */
2639 if (window->flags & SDL_WINDOW_OPENGL) {
2640 data->gles_swap_frame_event_queue = WAYLAND_wl_display_create_queue(data->waylandData->display);
2641 data->gles_swap_frame_surface_wrapper = WAYLAND_wl_proxy_create_wrapper(data->surface);
2642 WAYLAND_wl_proxy_set_queue((struct wl_proxy *)data->gles_swap_frame_surface_wrapper, data->gles_swap_frame_event_queue);
2643 data->gles_swap_frame_callback = wl_surface_frame(data->gles_swap_frame_surface_wrapper);
2644 wl_callback_add_listener(data->gles_swap_frame_callback, &gles_swap_frame_listener, data);
2645 }
2646
2647 // No frame callback on external surfaces as it may already have one attached.
2648 if (!external_surface) {
2649 // Fire a callback when the compositor wants a new frame to set the surface damage region.
2650 data->surface_frame_callback = wl_surface_frame(data->surface);
2651 wl_callback_add_listener(data->surface_frame_callback, &surface_frame_listener, data);
2652 }
2653
2654 if (window->flags & SDL_WINDOW_TRANSPARENT) {
2655 if (_this->gl_config.alpha_size == 0) {
2656 _this->gl_config.alpha_size = 8;
2657 }
2658 }
2659
2660 if (create_egl_window) {
2661 data->egl_window = WAYLAND_wl_egl_window_create(data->surface, data->current.pixel_width, data->current.pixel_height);
2662 }
2663
2664#ifdef SDL_VIDEO_OPENGL_EGL
2665 if (window->flags & SDL_WINDOW_OPENGL) {
2666 // Create the GLES window surface
2667 data->egl_surface = SDL_EGL_CreateSurface(_this, window, (NativeWindowType)data->egl_window);
2668
2669 if (data->egl_surface == EGL_NO_SURFACE) {
2670 return false; // SDL_EGL_CreateSurface should have set error
2671 }
2672 }
2673#endif
2674
2675 if (c->relative_mouse_mode) {
2676 Wayland_input_enable_relative_pointer(c->input);
2677 }
2678
2679 // We may need to create an idle inhibitor for this new window
2680 Wayland_SuspendScreenSaver(_this);
2681
2682 if (!custom_surface_role) {
2683#ifdef HAVE_LIBDECOR_H
2684 if (c->shell.libdecor && !SDL_WINDOW_IS_POPUP(window)) {
2685 data->shell_surface_type = WAYLAND_SHELL_SURFACE_TYPE_LIBDECOR;
2686 } else
2687#endif
2688 if (c->shell.xdg) {
2689 if (SDL_WINDOW_IS_POPUP(window)) {
2690 data->shell_surface_type = WAYLAND_SHELL_SURFACE_TYPE_XDG_POPUP;
2691 } else {
2692 data->shell_surface_type = WAYLAND_SHELL_SURFACE_TYPE_XDG_TOPLEVEL;
2693 }
2694 } // All other cases will be WAYLAND_SURFACE_UNKNOWN
2695 } else {
2696 // Roleless and external surfaces are always considered to be in the shown state by the backend.
2697 data->shell_surface_type = WAYLAND_SHELL_SURFACE_TYPE_CUSTOM;
2698 data->shell_surface_status = WAYLAND_SHELL_SURFACE_STATUS_SHOWN;
2699 }
2700
2701 if (SDL_GetHintBoolean(SDL_HINT_VIDEO_DOUBLE_BUFFER, false)) {
2702 data->double_buffer = true;
2703 }
2704
2705 SDL_PropertiesID props = SDL_GetWindowProperties(window);
2706 SDL_SetPointerProperty(props, SDL_PROP_WINDOW_WAYLAND_DISPLAY_POINTER, data->waylandData->display);
2707 SDL_SetPointerProperty(props, SDL_PROP_WINDOW_WAYLAND_SURFACE_POINTER, data->surface);
2708 SDL_SetPointerProperty(props, SDL_PROP_WINDOW_WAYLAND_VIEWPORT_POINTER, data->viewport);
2709 SDL_SetPointerProperty(props, SDL_PROP_WINDOW_WAYLAND_EGL_WINDOW_POINTER, data->egl_window);
2710
2711 data->hit_test_result = SDL_HITTEST_NORMAL;
2712
2713 return true;
2714}
2715
2716void Wayland_SetWindowMinimumSize(SDL_VideoDevice *_this, SDL_Window *window)
2717{
2718 // Will be committed when Wayland_SetWindowSize() is called by the video core.
2719 SetMinMaxDimensions(window);
2720}
2721
2722void Wayland_SetWindowMaximumSize(SDL_VideoDevice *_this, SDL_Window *window)
2723{
2724 // Will be committed when Wayland_SetWindowSize() is called by the video core.
2725 SetMinMaxDimensions(window);
2726}
2727
2728bool Wayland_SetWindowPosition(SDL_VideoDevice *_this, SDL_Window *window)
2729{
2730 SDL_WindowData *wind = window->internal;
2731
2732 // Only popup windows can be positioned relative to the parent.
2733 if (wind->shell_surface_type == WAYLAND_SHELL_SURFACE_TYPE_XDG_POPUP) {
2734 if (wind->shell_surface.xdg.popup.xdg_popup &&
2735 xdg_popup_get_version(wind->shell_surface.xdg.popup.xdg_popup) < XDG_POPUP_REPOSITION_SINCE_VERSION) {
2736 return SDL_Unsupported();
2737 }
2738
2739 RepositionPopup(window, false);
2740 return true;
2741 } else if (wind->shell_surface_type == WAYLAND_SHELL_SURFACE_TYPE_LIBDECOR || wind->shell_surface_type == WAYLAND_SHELL_SURFACE_TYPE_XDG_TOPLEVEL) {
2742 /* Catch up on any pending state before attempting to change the fullscreen window
2743 * display via a set fullscreen call to make sure the window doesn't have a pending
2744 * leave fullscreen event that it might override.
2745 */
2746 FlushPendingEvents(window);
2747
2748 if (wind->is_fullscreen) {
2749 SDL_VideoDisplay *display = SDL_GetVideoDisplayForFullscreenWindow(window);
2750 if (display && wind->last_displayID != display->id) {
2751 struct wl_output *output = display->internal->output;
2752 SetFullscreen(window, output);
2753
2754 return true;
2755 }
2756 }
2757 }
2758 return SDL_SetError("wayland cannot position non-popup windows");
2759}
2760
2761void Wayland_SetWindowSize(SDL_VideoDevice *_this, SDL_Window *window)
2762{
2763 SDL_WindowData *wind = window->internal;
2764
2765 /* Flush any pending state operations, as fullscreen windows do not get
2766 * explicitly resized, not strictly obeying the size of a maximized window
2767 * is a protocol violation, and pending restore events might result in a
2768 * configure event overwriting the requested size.
2769 *
2770 * Calling this on a custom surface is informative, so the size must
2771 * always be passed through.
2772 */
2773 FlushPendingEvents(window);
2774
2775 // Maximized and fullscreen windows don't get resized.
2776 if (!(window->flags & (SDL_WINDOW_FULLSCREEN | SDL_WINDOW_MAXIMIZED)) ||
2777 wind->shell_surface_type == WAYLAND_SHELL_SURFACE_TYPE_CUSTOM) {
2778 if (!wind->scale_to_display) {
2779 wind->requested.logical_width = window->pending.w;
2780 wind->requested.logical_height = window->pending.h;
2781 } else {
2782 wind->requested.logical_width = PixelToPoint(window, window->pending.w);
2783 wind->requested.logical_height = PixelToPoint(window, window->pending.h);
2784 wind->requested.pixel_width = window->pending.w;
2785 wind->requested.pixel_height = window->pending.h;
2786 }
2787
2788 ConfigureWindowGeometry(window);
2789 } else {
2790 // Can't resize the window.
2791 window->last_size_pending = false;
2792 }
2793
2794 // Always commit, as this may be in response to a min/max limit change.
2795 CommitLibdecorFrame(window);
2796}
2797
2798void Wayland_GetWindowSizeInPixels(SDL_VideoDevice *_this, SDL_Window *window, int *w, int *h)
2799{
2800 SDL_WindowData *data = window->internal;
2801
2802 *w = data->current.pixel_width;
2803 *h = data->current.pixel_height;
2804}
2805
2806float Wayland_GetWindowContentScale(SDL_VideoDevice *_this, SDL_Window *window)
2807{
2808 SDL_WindowData *wind = window->internal;
2809
2810 if (window->flags & SDL_WINDOW_HIGH_PIXEL_DENSITY || wind->scale_to_display || wind->fullscreen_exclusive) {
2811 return (float)wind->scale_factor;
2812 }
2813
2814 return 1.0f;
2815}
2816
2817SDL_DisplayID Wayland_GetDisplayForWindow(SDL_VideoDevice *_this, SDL_Window *window)
2818{
2819 SDL_WindowData *wind = window->internal;
2820
2821 if (wind) {
2822 return wind->last_displayID;
2823 }
2824
2825 return 0;
2826}
2827
2828bool Wayland_SetWindowOpacity(SDL_VideoDevice *_this, SDL_Window *window, float opacity)
2829{
2830 SDL_WindowData *wind = window->internal;
2831
2832 if (wind->wp_alpha_modifier_surface_v1) {
2833 SetSurfaceOpaqueRegion(wind, !(window->flags & SDL_WINDOW_TRANSPARENT) && opacity == 1.0f);
2834 wp_alpha_modifier_surface_v1_set_multiplier(wind->wp_alpha_modifier_surface_v1, (Uint32)((double)SDL_MAX_UINT32 * (double)opacity));
2835
2836 return true;
2837 }
2838
2839 return SDL_SetError("wayland: set window opacity failed; compositor lacks support for the required wp_alpha_modifier_v1 protocol");
2840}
2841
2842void Wayland_SetWindowTitle(SDL_VideoDevice *_this, SDL_Window *window)
2843{
2844 SDL_WindowData *wind = window->internal;
2845 const char *title = window->title ? window->title : "";
2846
2847#ifdef HAVE_LIBDECOR_H
2848 if (wind->shell_surface_type == WAYLAND_SHELL_SURFACE_TYPE_LIBDECOR && wind->shell_surface.libdecor.frame) {
2849 libdecor_frame_set_title(wind->shell_surface.libdecor.frame, title);
2850 } else
2851#endif
2852 if (wind->shell_surface_type == WAYLAND_SHELL_SURFACE_TYPE_XDG_TOPLEVEL && wind->shell_surface.xdg.toplevel.xdg_toplevel) {
2853 xdg_toplevel_set_title(wind->shell_surface.xdg.toplevel.xdg_toplevel, title);
2854 }
2855}
2856
2857bool Wayland_SetWindowIcon(SDL_VideoDevice *_this, SDL_Window *window, SDL_Surface *icon)
2858{
2859 SDL_WindowData *wind = window->internal;
2860 struct xdg_toplevel *toplevel = NULL;
2861
2862 if (!_this->internal->xdg_toplevel_icon_manager_v1) {
2863 return SDL_SetError("wayland: cannot set icon; required xdg_toplevel_icon_v1 protocol not supported");
2864 }
2865
2866 if (icon->w != icon->h) {
2867 return SDL_SetError("wayland: icon width and height must be equal, got %ix%i", icon->w, icon->h);
2868 }
2869
2870 int image_count = 0;
2871 SDL_Surface **images = SDL_GetSurfaceImages(icon, &image_count);
2872 if (!images || !image_count) {
2873 return false;
2874 }
2875
2876 // Release the old icon resources.
2877 if (wind->xdg_toplevel_icon_v1) {
2878 xdg_toplevel_icon_v1_destroy(wind->xdg_toplevel_icon_v1);
2879 wind->xdg_toplevel_icon_v1 = NULL;
2880 }
2881
2882 for (int i = 0; i < wind->icon_buffer_count; ++i) {
2883 Wayland_ReleaseSHMBuffer(&wind->icon_buffers[i]);
2884 }
2885 SDL_free(wind->icon_buffers);
2886 wind->icon_buffer_count = 0;
2887
2888 wind->xdg_toplevel_icon_v1 = xdg_toplevel_icon_manager_v1_create_icon(_this->internal->xdg_toplevel_icon_manager_v1);
2889 wind->icon_buffers = SDL_calloc(image_count, sizeof(struct Wayland_SHMBuffer));
2890 if (!wind->icon_buffers) {
2891 goto failure_cleanup;
2892 }
2893
2894 for (int i = 0; i < image_count; ++i) {
2895 if (images[i]->w == images[i]->h) {
2896 struct Wayland_SHMBuffer *buffer = &wind->icon_buffers[wind->icon_buffer_count];
2897
2898 if (!Wayland_AllocSHMBuffer(images[i]->w, images[i]->h, buffer)) {
2899 SDL_SetError("wayland: failed to allocate SHM buffer for the icon");
2900 goto failure_cleanup;
2901 }
2902
2903 SDL_PremultiplyAlpha(images[i]->w, images[i]->h, images[i]->format, images[i]->pixels, images[i]->pitch, SDL_PIXELFORMAT_ARGB8888, buffer->shm_data, images[i]->w * 4, true);
2904 const int scale = (int)SDL_ceil((double)images[i]->w / (double)icon->w);
2905 xdg_toplevel_icon_v1_add_buffer(wind->xdg_toplevel_icon_v1, buffer->wl_buffer, scale);
2906 wind->icon_buffer_count++;
2907 } else {
2908 SDL_LogWarn(SDL_LOG_CATEGORY_VIDEO, "wayland: icon width and height must be equal, got %ix%i for image level %i; skipping", images[i]->w, images[i]->h, i);
2909 }
2910 }
2911
2912 SDL_free(images);
2913
2914#ifdef HAVE_LIBDECOR_H
2915 if (wind->shell_surface_type == WAYLAND_SHELL_SURFACE_TYPE_LIBDECOR && wind->shell_surface.libdecor.frame) {
2916 toplevel = libdecor_frame_get_xdg_toplevel(wind->shell_surface.libdecor.frame);
2917 } else
2918#endif
2919 if (wind->shell_surface_type == WAYLAND_SHELL_SURFACE_TYPE_XDG_TOPLEVEL && wind->shell_surface.xdg.toplevel.xdg_toplevel) {
2920 toplevel = wind->shell_surface.xdg.toplevel.xdg_toplevel;
2921 }
2922
2923 if (toplevel) {
2924 xdg_toplevel_icon_manager_v1_set_icon(_this->internal->xdg_toplevel_icon_manager_v1, toplevel, wind->xdg_toplevel_icon_v1);
2925 }
2926
2927 return true;
2928
2929failure_cleanup:
2930
2931 if (wind->xdg_toplevel_icon_v1) {
2932 xdg_toplevel_icon_v1_destroy(wind->xdg_toplevel_icon_v1);
2933 wind->xdg_toplevel_icon_v1 = NULL;
2934 }
2935
2936 for (int i = 0; i < wind->icon_buffer_count; ++i) {
2937 Wayland_ReleaseSHMBuffer(&wind->icon_buffers[i]);
2938 }
2939 SDL_free(wind->icon_buffers);
2940 wind->icon_buffers = NULL;
2941 wind->icon_buffer_count = 0;
2942
2943 return false;
2944}
2945
2946void *Wayland_GetWindowICCProfile(SDL_VideoDevice *_this, SDL_Window *window, size_t *size)
2947{
2948 SDL_WindowData *wind = window->internal;
2949 void *ret = NULL;
2950
2951 if (wind->icc_size > 0) {
2952 void *icc_map = mmap(NULL, wind->icc_size, PROT_READ, MAP_PRIVATE, wind->icc_fd, 0);
2953 if (icc_map != MAP_FAILED) {
2954 ret = SDL_malloc(wind->icc_size);
2955 if (ret) {
2956 *size = wind->icc_size;
2957 SDL_memcpy(ret, icc_map, *size);
2958 }
2959 munmap(icc_map, wind->icc_size);
2960 }
2961 }
2962
2963 return ret;
2964}
2965
2966bool Wayland_SyncWindow(SDL_VideoDevice *_this, SDL_Window *window)
2967{
2968 SDL_WindowData *wind = window->internal;
2969
2970 do {
2971 WAYLAND_wl_display_roundtrip(_this->internal->display);
2972 } while (wind->fullscreen_deadline_count || wind->maximized_restored_deadline_count);
2973
2974 return true;
2975}
2976
2977void Wayland_ShowWindowSystemMenu(SDL_Window *window, int x, int y)
2978{
2979 SDL_WindowData *wind = window->internal;
2980
2981 if (wind->scale_to_display) {
2982 x = PixelToPoint(window, x);
2983 y = PixelToPoint(window, y);
2984 }
2985
2986#ifdef HAVE_LIBDECOR_H
2987 if (wind->shell_surface_type == WAYLAND_SHELL_SURFACE_TYPE_LIBDECOR) {
2988 if (wind->shell_surface.libdecor.frame) {
2989 libdecor_frame_show_window_menu(wind->shell_surface.libdecor.frame, wind->waylandData->input->seat, wind->waylandData->input->last_implicit_grab_serial, x, y);
2990 }
2991 } else
2992#endif
2993 if (wind->shell_surface_type == WAYLAND_SHELL_SURFACE_TYPE_XDG_TOPLEVEL) {
2994 if (wind->shell_surface.xdg.toplevel.xdg_toplevel) {
2995 xdg_toplevel_show_window_menu(wind->shell_surface.xdg.toplevel.xdg_toplevel, wind->waylandData->input->seat, wind->waylandData->input->last_implicit_grab_serial, x, y);
2996 }
2997 }
2998}
2999
3000bool Wayland_SuspendScreenSaver(SDL_VideoDevice *_this)
3001{
3002 SDL_VideoData *data = _this->internal;
3003
3004#ifdef SDL_USE_LIBDBUS
3005 if (SDL_DBus_ScreensaverInhibit(_this->suspend_screensaver)) {
3006 return true;
3007 }
3008#endif
3009
3010 /* The idle_inhibit_unstable_v1 protocol suspends the screensaver
3011 on a per wl_surface basis, but SDL assumes that suspending
3012 the screensaver can be done independently of any window.
3013
3014 To reconcile these differences, we propagate the idle inhibit
3015 state to each window. If there is no window active, we will
3016 be able to inhibit idle once the first window is created.
3017 */
3018 if (data->idle_inhibit_manager) {
3019 SDL_Window *window = _this->windows;
3020 while (window) {
3021 SDL_WindowData *win_data = window->internal;
3022
3023 if (_this->suspend_screensaver && !win_data->idle_inhibitor) {
3024 win_data->idle_inhibitor =
3025 zwp_idle_inhibit_manager_v1_create_inhibitor(data->idle_inhibit_manager,
3026 win_data->surface);
3027 } else if (!_this->suspend_screensaver && win_data->idle_inhibitor) {
3028 zwp_idle_inhibitor_v1_destroy(win_data->idle_inhibitor);
3029 win_data->idle_inhibitor = NULL;
3030 }
3031
3032 window = window->next;
3033 }
3034 }
3035
3036 return true;
3037}
3038
3039void Wayland_DestroyWindow(SDL_VideoDevice *_this, SDL_Window *window)
3040{
3041 SDL_VideoData *data = _this->internal;
3042 SDL_WindowData *wind = window->internal;
3043
3044 if (data && wind) {
3045 /* Roundtrip before destroying the window to make sure that it has received input leave events, so that
3046 * no internal structures are left pointing to the destroyed window.
3047 */
3048 if (wind->show_hide_sync_required) {
3049 WAYLAND_wl_display_roundtrip(data->display);
3050 }
3051
3052#ifdef SDL_VIDEO_OPENGL_EGL
3053 if (wind->egl_surface) {
3054 SDL_EGL_DestroySurface(_this, wind->egl_surface);
3055 }
3056#endif
3057 if (wind->egl_window) {
3058 WAYLAND_wl_egl_window_destroy(wind->egl_window);
3059 }
3060
3061 if (wind->idle_inhibitor) {
3062 zwp_idle_inhibitor_v1_destroy(wind->idle_inhibitor);
3063 }
3064
3065 if (wind->activation_token) {
3066 xdg_activation_token_v1_destroy(wind->activation_token);
3067 }
3068
3069 if (wind->viewport) {
3070 wp_viewport_destroy(wind->viewport);
3071 }
3072
3073 if (wind->fractional_scale) {
3074 wp_fractional_scale_v1_destroy(wind->fractional_scale);
3075 }
3076
3077 if (wind->wp_alpha_modifier_surface_v1) {
3078 wp_alpha_modifier_surface_v1_destroy(wind->wp_alpha_modifier_surface_v1);
3079 }
3080
3081 if (wind->frog_color_managed_surface) {
3082 frog_color_managed_surface_destroy(wind->frog_color_managed_surface);
3083 }
3084
3085 if (wind->wp_color_management_surface_feedback) {
3086 Wayland_FreeColorInfoState(wind->color_info_state);
3087 wp_color_management_surface_feedback_v1_destroy(wind->wp_color_management_surface_feedback);
3088 }
3089
3090 SDL_free(wind->outputs);
3091 SDL_free(wind->app_id);
3092
3093 if (wind->gles_swap_frame_callback) {
3094 wl_callback_destroy(wind->gles_swap_frame_callback);
3095 WAYLAND_wl_proxy_wrapper_destroy(wind->gles_swap_frame_surface_wrapper);
3096 WAYLAND_wl_event_queue_destroy(wind->gles_swap_frame_event_queue);
3097 }
3098
3099 if (wind->surface_frame_callback) {
3100 wl_callback_destroy(wind->surface_frame_callback);
3101 }
3102
3103 if (!(window->flags & SDL_WINDOW_EXTERNAL)) {
3104 wl_surface_destroy(wind->surface);
3105 } else {
3106 Wayland_RemoveWindowDataFromExternalList(wind);
3107 }
3108
3109 if (wind->xdg_toplevel_icon_v1) {
3110 xdg_toplevel_icon_v1_destroy(wind->xdg_toplevel_icon_v1);
3111 }
3112
3113 for (int i = 0; i < wind->icon_buffer_count; ++i) {
3114 Wayland_ReleaseSHMBuffer(&wind->icon_buffers[i]);
3115 }
3116 SDL_free(wind->icon_buffers);
3117 wind->icon_buffer_count = 0;
3118
3119 SDL_free(wind);
3120 WAYLAND_wl_display_flush(data->display);
3121 }
3122 window->internal = NULL;
3123}
3124
3125#endif // SDL_VIDEO_DRIVER_WAYLAND
3126