diff --git a/src/video/wayland/SDL_waylandevents.c b/src/video/wayland/SDL_waylandevents.c index b8fb8e0a42..30b4002701 100644 --- a/src/video/wayland/SDL_waylandevents.c +++ b/src/video/wayland/SDL_waylandevents.c @@ -802,7 +802,20 @@ static void pointer_handle_motion(void *data, struct wl_pointer *pointer, static void pointer_dispatch_enter(SDL_WaylandSeat *seat) { - SDL_WindowData *window = seat->pointer.pending_frame.enter_window; + SDL_WindowData *window = Wayland_GetWindowDataForOwnedSurface(seat->pointer.pending_frame.enter_surface); + if (!window) { + // Entering a surface not managed by SDL; just set the cursor reset flag. + Wayland_SeatResetCursor(seat); + return; + } + + if (window->surface != seat->pointer.pending_frame.enter_surface) { + /* This surface is part of the window managed by SDL, but it is not the main content + * surface and doesn't get focus. Just set the default cursor and leave. + */ + Wayland_SeatSetDefaultCursor(seat); + return; + } seat->pointer.focus = window; ++window->pointer_focus_count; @@ -834,14 +847,8 @@ static void pointer_handle_enter(void *data, struct wl_pointer *pointer, return; } - SDL_WindowData *window = Wayland_GetWindowDataForOwnedSurface(surface); - if (!window) { - // Not a surface owned by SDL. - return; - } - SDL_WaylandSeat *seat = (SDL_WaylandSeat *)data; - seat->pointer.pending_frame.enter_window = window; + seat->pointer.pending_frame.enter_surface = surface; seat->pointer.enter_serial = serial; /* In the case of e.g. a pointer confine warp, we may receive an enter @@ -860,32 +867,39 @@ static void pointer_handle_enter(void *data, struct wl_pointer *pointer, static void pointer_dispatch_leave(SDL_WaylandSeat *seat, bool update_pointer) { - SDL_WindowData *window = seat->pointer.pending_frame.leave_window; + SDL_WindowData *window = Wayland_GetWindowDataForOwnedSurface(seat->pointer.pending_frame.leave_surface); if (window) { - // Clear the capture flag and raise all buttons - window->sdlwindow->flags &= ~SDL_WINDOW_MOUSE_CAPTURE; + if (seat->pointer.focus) { + if (seat->pointer.focus->surface == seat->pointer.pending_frame.leave_surface) { + // Clear the capture flag and raise all buttons + window->sdlwindow->flags &= ~SDL_WINDOW_MOUSE_CAPTURE; - seat->pointer.focus = NULL; - for (Uint8 i = 1; seat->pointer.buttons_pressed; ++i) { - if (seat->pointer.buttons_pressed & SDL_BUTTON_MASK(i)) { - SDL_SendMouseButton(0, window->sdlwindow, seat->pointer.sdl_id, i, false); - seat->pointer.buttons_pressed &= ~SDL_BUTTON_MASK(i); + seat->pointer.focus = NULL; + for (Uint8 i = 1; seat->pointer.buttons_pressed; ++i) { + if (seat->pointer.buttons_pressed & SDL_BUTTON_MASK(i)) { + SDL_SendMouseButton(0, window->sdlwindow, seat->pointer.sdl_id, i, false); + seat->pointer.buttons_pressed &= ~SDL_BUTTON_MASK(i); + } + } + + /* A pointer leave event may be emitted if the compositor hides the pointer in response to receiving a touch event. + * Don't relinquish focus if the surface has active touches, as the compositor is just transitioning from mouse to touch mode. + */ + SDL_Window *mouse_focus = SDL_GetMouseFocus(); + const bool had_focus = mouse_focus && window->sdlwindow == mouse_focus; + if (!--window->pointer_focus_count && had_focus && !window->active_touch_count) { + SDL_SetMouseFocus(NULL); + } + + if (update_pointer) { + Wayland_SeatUpdatePointerGrab(seat); + Wayland_SeatUpdatePointerCursor(seat); + } } - } - - /* A pointer leave event may be emitted if the compositor hides the pointer in response to receiving a touch event. - * Don't relinquish focus if the surface has active touches, as the compositor is just transitioning from mouse to touch mode. - */ - SDL_Window *mouse_focus = SDL_GetMouseFocus(); - const bool had_focus = mouse_focus && window->sdlwindow == mouse_focus; - if (!--window->pointer_focus_count && had_focus && !window->active_touch_count) { - SDL_SetMouseFocus(NULL); - } - - if (update_pointer) { - Wayland_SeatUpdatePointerGrab(seat); - Wayland_SeatUpdatePointerCursor(seat); + } else if (update_pointer) { + // Leaving a non-content surface managed by SDL; just set the cursor reset flag. + Wayland_SeatResetCursor(seat); } } } @@ -898,15 +912,9 @@ static void pointer_handle_leave(void *data, struct wl_pointer *pointer, return; } - SDL_WindowData *window = Wayland_GetWindowDataForOwnedSurface(surface); - if (!window) { - // Not a surface owned by SDL. - return; - } - SDL_WaylandSeat *seat = (SDL_WaylandSeat *)data; - seat->pointer.pending_frame.leave_window = window; - if (wl_pointer_get_version(seat->pointer.wl_pointer) < WL_POINTER_FRAME_SINCE_VERSION && window == seat->pointer.focus) { + seat->pointer.pending_frame.leave_surface = surface; + if (wl_pointer_get_version(seat->pointer.wl_pointer) < WL_POINTER_FRAME_SINCE_VERSION) { pointer_dispatch_leave(seat, true); } } @@ -1277,11 +1285,13 @@ static void pointer_handle_frame(void *data, struct wl_pointer *pointer) { SDL_WaylandSeat *seat = data; - if (seat->pointer.pending_frame.enter_window) { - if (seat->pointer.focus && seat->pointer.pending_frame.leave_window == seat->pointer.focus) { + if (seat->pointer.pending_frame.enter_surface) { + if (seat->pointer.pending_frame.leave_surface) { // Leaving the previous surface before entering a new surface. pointer_dispatch_leave(seat, false); + seat->pointer.pending_frame.leave_surface = NULL; } + pointer_dispatch_enter(seat); } @@ -1309,7 +1319,7 @@ static void pointer_handle_frame(void *data, struct wl_pointer *pointer) pointer_dispatch_axis(seat); } - if (seat->pointer.focus && seat->pointer.pending_frame.leave_window == seat->pointer.focus) { + if (seat->pointer.pending_frame.leave_surface) { pointer_dispatch_leave(seat, true); } @@ -1435,7 +1445,7 @@ static void touch_handler_down(void *data, struct wl_touch *touch, uint32_t seri Wayland_UpdateImplicitGrabSerial(seat, serial); window_data = Wayland_GetWindowDataForOwnedSurface(surface); - if (window_data) { + if (window_data && window_data->surface == surface) { float x, y; if (window_data->current.logical_width <= 1) { @@ -1457,8 +1467,7 @@ static void touch_handler_down(void *data, struct wl_touch *touch, uint32_t seri } } -static void touch_handler_up(void *data, struct wl_touch *touch, uint32_t serial, - uint32_t timestamp, int id) +static void touch_handler_up(void *data, struct wl_touch *touch, uint32_t serial, uint32_t timestamp, int id) { SDL_WaylandSeat *seat = (SDL_WaylandSeat *)data; wl_fixed_t fx = 0, fy = 0; @@ -1469,7 +1478,7 @@ static void touch_handler_up(void *data, struct wl_touch *touch, uint32_t serial if (surface) { SDL_WindowData *window_data = Wayland_GetWindowDataForOwnedSurface(surface); - if (window_data) { + if (window_data && window_data->surface == surface) { const float x = (float)wl_fixed_to_double(fx) / window_data->current.logical_width; const float y = (float)wl_fixed_to_double(fy) / window_data->current.logical_height; @@ -1489,8 +1498,7 @@ static void touch_handler_up(void *data, struct wl_touch *touch, uint32_t serial } } -static void touch_handler_motion(void *data, struct wl_touch *touch, uint32_t timestamp, - int id, wl_fixed_t fx, wl_fixed_t fy) +static void touch_handler_motion(void *data, struct wl_touch *touch, uint32_t timestamp, int id, wl_fixed_t fx, wl_fixed_t fy) { SDL_WaylandSeat *seat = (SDL_WaylandSeat *)data; struct wl_surface *surface = NULL; @@ -1500,7 +1508,7 @@ static void touch_handler_motion(void *data, struct wl_touch *touch, uint32_t ti if (surface) { SDL_WindowData *window_data = Wayland_GetWindowDataForOwnedSurface(surface); - if (window_data) { + if (window_data && window_data->surface == surface) { const float x = (float)wl_fixed_to_double(fx) / window_data->current.logical_width; const float y = (float)wl_fixed_to_double(fy) / window_data->current.logical_height; @@ -2395,9 +2403,9 @@ static void Wayland_SeatDestroyPointer(SDL_WaylandSeat *seat) // Make sure focus is removed from a surface before the pointer is destroyed. if (seat->pointer.focus) { - seat->pointer.pending_frame.leave_window = seat->pointer.focus; + seat->pointer.pending_frame.leave_surface = seat->pointer.focus->surface; pointer_dispatch_leave(seat, false); - seat->pointer.pending_frame.leave_window = NULL; + seat->pointer.pending_frame.leave_surface = NULL; } SDL_RemoveMouse(seat->pointer.sdl_id); @@ -3349,7 +3357,7 @@ static void tablet_tool_handle_proximity_in(void *data, struct zwp_tablet_tool_v { SDL_WaylandPenTool *sdltool = (SDL_WaylandPenTool *) data; SDL_WindowData *windowdata = surface ? Wayland_GetWindowDataForOwnedSurface(surface) : NULL; - sdltool->focus = windowdata; + sdltool->focus = windowdata && windowdata->surface == surface ? windowdata : NULL; sdltool->proximity_serial = serial; sdltool->frame.have_proximity = true; sdltool->frame.in_proximity = true; @@ -3664,9 +3672,9 @@ void Wayland_DisplayRemoveWindowReferencesFromSeats(SDL_VideoData *display, SDL_ } if (seat->pointer.focus == window) { - seat->pointer.pending_frame.leave_window = seat->pointer.focus; + seat->pointer.pending_frame.leave_surface = seat->pointer.focus->surface; pointer_dispatch_leave(seat, true); - seat->pointer.pending_frame.leave_window = NULL; + seat->pointer.pending_frame.leave_surface = NULL; } // Need the safe loop variant here as cancelling a touch point removes it from the list. diff --git a/src/video/wayland/SDL_waylandevents_c.h b/src/video/wayland/SDL_waylandevents_c.h index 4e98e6f69f..1a597a35f8 100644 --- a/src/video/wayland/SDL_waylandevents_c.h +++ b/src/video/wayland/SDL_waylandevents_c.h @@ -236,8 +236,8 @@ typedef struct SDL_WaylandSeat SDL_MouseWheelDirection direction; } axis; - SDL_WindowData *enter_window; - SDL_WindowData *leave_window; + struct wl_surface *enter_surface; + struct wl_surface *leave_surface; // Event timestamp in nanoseconds Uint64 timestamp_ns; diff --git a/src/video/wayland/SDL_waylandmouse.c b/src/video/wayland/SDL_waylandmouse.c index ec54994d87..72b9d14d5e 100644 --- a/src/video/wayland/SDL_waylandmouse.c +++ b/src/video/wayland/SDL_waylandmouse.c @@ -1523,6 +1523,23 @@ void Wayland_FiniMouse(SDL_VideoData *data) #endif } +void Wayland_SeatResetCursor(SDL_WaylandSeat *seat) +{ + Wayland_CursorStateResetCursor(&seat->pointer.cursor_state); +} + +void Wayland_SeatSetDefaultCursor(SDL_WaylandSeat *seat) +{ + SDL_Mouse *mouse = SDL_GetMouse(); + SDL_WindowData *pointer_focus = seat->pointer.focus; + const Wayland_PointerObject obj = { + .wl_pointer = seat->pointer.wl_pointer, + .is_pointer = true + }; + + Wayland_CursorStateSetCursor(&seat->pointer.cursor_state, &obj, pointer_focus, seat->pointer.enter_serial, mouse->def_cursor); +} + void Wayland_SeatUpdatePointerCursor(SDL_WaylandSeat *seat) { SDL_Mouse *mouse = SDL_GetMouse(); diff --git a/src/video/wayland/SDL_waylandmouse.h b/src/video/wayland/SDL_waylandmouse.h index 481cae1073..85ba5eef24 100644 --- a/src/video/wayland/SDL_waylandmouse.h +++ b/src/video/wayland/SDL_waylandmouse.h @@ -27,6 +27,8 @@ extern void Wayland_InitMouse(SDL_VideoData *data); extern void Wayland_FiniMouse(SDL_VideoData *data); extern void Wayland_SeatUpdatePointerCursor(SDL_WaylandSeat *seat); +extern void Wayland_SeatSetDefaultCursor(SDL_WaylandSeat *seat); +extern void Wayland_SeatResetCursor(SDL_WaylandSeat *seat); extern void Wayland_TabletToolUpdateCursor(SDL_WaylandPenTool *tool); extern void Wayland_SeatWarpMouse(SDL_WaylandSeat *seat, SDL_WindowData *window, float x, float y); extern void Wayland_CursorStateSetFrameCallback(SDL_WaylandCursorState *state, void *userdata); diff --git a/src/video/wayland/SDL_waylandshmbuffer.c b/src/video/wayland/SDL_waylandshmbuffer.c index 88da2e9324..9e355fd77a 100644 --- a/src/video/wayland/SDL_waylandshmbuffer.c +++ b/src/video/wayland/SDL_waylandshmbuffer.c @@ -32,6 +32,7 @@ #include "SDL_waylandshmbuffer.h" #include "SDL_waylandvideo.h" +#include "single-pixel-buffer-v1-client-protocol.h" static bool SetTempFileSize(int fd, off_t size) { @@ -186,4 +187,28 @@ void Wayland_ReleaseSHMPool(Wayland_SHMPool *shmPool) } } +struct wl_buffer *Wayland_CreateSinglePixelBuffer(Uint32 r, Uint32 g, Uint32 b, Uint32 a) +{ + SDL_VideoData *viddata = SDL_GetVideoDevice()->internal; + + // The single-pixel buffer protocol is preferred, as the compositor can choose an optimal format. + if (viddata->single_pixel_buffer_manager) { + return wp_single_pixel_buffer_manager_v1_create_u32_rgba_buffer(viddata->single_pixel_buffer_manager, r, g, b, a); + } else { + Wayland_SHMPool *pool = Wayland_AllocSHMPool(4); + if (!pool) { + return NULL; + } + + void *mem; + struct wl_buffer *wl_buffer = Wayland_AllocBufferFromPool(pool, 1, 1, &mem); + + const Uint8 pixel[4] = { r >> 24, g >> 24, b >> 24, a >> 24 }; + SDL_memcpy(mem, pixel, sizeof(pixel)); + + Wayland_ReleaseSHMPool(pool); + return wl_buffer; + } +} + #endif diff --git a/src/video/wayland/SDL_waylandshmbuffer.h b/src/video/wayland/SDL_waylandshmbuffer.h index 019793e7ed..44caa6ed09 100644 --- a/src/video/wayland/SDL_waylandshmbuffer.h +++ b/src/video/wayland/SDL_waylandshmbuffer.h @@ -30,4 +30,6 @@ extern Wayland_SHMPool *Wayland_AllocSHMPool(int size); extern struct wl_buffer *Wayland_AllocBufferFromPool(Wayland_SHMPool *shmPool, int width, int height, void **data); extern void Wayland_ReleaseSHMPool(Wayland_SHMPool *shmPool); +extern struct wl_buffer *Wayland_CreateSinglePixelBuffer(Uint32 r, Uint32 g, Uint32 b, Uint32 a); + #endif diff --git a/src/video/wayland/SDL_waylandvideo.c b/src/video/wayland/SDL_waylandvideo.c index 2b0cfad3d3..bcefeb74dd 100644 --- a/src/video/wayland/SDL_waylandvideo.c +++ b/src/video/wayland/SDL_waylandvideo.c @@ -69,6 +69,7 @@ #include "color-management-v1-client-protocol.h" #include "pointer-warp-v1-client-protocol.h" #include "pointer-gestures-unstable-v1-client-protocol.h" +#include "single-pixel-buffer-v1-client-protocol.h" #ifdef HAVE_LIBDECOR_H #include @@ -653,6 +654,7 @@ static SDL_VideoDevice *Wayland_CreateDevice(bool require_preferred_protocols) device->SetWindowResizable = Wayland_SetWindowResizable; device->SetWindowPosition = Wayland_SetWindowPosition; device->SetWindowSize = Wayland_SetWindowSize; + device->SetWindowAspectRatio = Wayland_SetWindowAspectRatio; device->SetWindowMinimumSize = Wayland_SetWindowMinimumSize; device->SetWindowMaximumSize = Wayland_SetWindowMaximumSize; device->SetWindowParent = Wayland_SetWindowParent; @@ -1278,6 +1280,8 @@ static void handle_registry_global(void *data, struct wl_registry *registry, uin if (SDL_strcmp(interface, "wl_compositor") == 0) { d->compositor = wl_registry_bind(d->registry, id, &wl_compositor_interface, SDL_min(SDL_WL_COMPOSITOR_VERSION, version)); + } else if (SDL_strcmp(interface, "wl_subcompositor") == 0) { + d->subcompositor = wl_registry_bind(d->registry, id, &wl_subcompositor_interface, 1); } else if (SDL_strcmp(interface, "wl_output") == 0) { Wayland_add_display(d, id, SDL_min(version, SDL_WL_OUTPUT_VERSION)); } else if (SDL_strcmp(interface, "wl_seat") == 0) { @@ -1344,6 +1348,8 @@ static void handle_registry_global(void *data, struct wl_registry *registry, uin } else if (SDL_strcmp(interface, "zwp_pointer_gestures_v1") == 0) { d->zwp_pointer_gestures = wl_registry_bind(d->registry, id, &zwp_pointer_gestures_v1_interface, SDL_min(version, 3)); Wayland_DisplayInitPointerGestureManager(d); + } else if (SDL_strcmp(interface, "wp_single_pixel_buffer_manager_v1") == 0) { + d->single_pixel_buffer_manager = wl_registry_bind(d->registry, id, &wp_single_pixel_buffer_manager_v1_interface, 1); } #ifdef SDL_WL_FIXES_VERSION else if (SDL_strcmp(interface, "wl_fixes") == 0) { @@ -1692,6 +1698,16 @@ static void Wayland_VideoCleanup(SDL_VideoDevice *_this) data->zwp_pointer_gestures = NULL; } + if (data->single_pixel_buffer_manager) { + wp_single_pixel_buffer_manager_v1_destroy(data->single_pixel_buffer_manager); + data->single_pixel_buffer_manager = NULL; + } + + if (data->subcompositor) { + wl_subcompositor_destroy(data->subcompositor); + data->subcompositor = NULL; + } + if (data->compositor) { wl_compositor_destroy(data->compositor); data->compositor = NULL; diff --git a/src/video/wayland/SDL_waylandvideo.h b/src/video/wayland/SDL_waylandvideo.h index 3542b48afa..8578aa7650 100644 --- a/src/video/wayland/SDL_waylandvideo.h +++ b/src/video/wayland/SDL_waylandvideo.h @@ -61,6 +61,7 @@ struct SDL_VideoData struct libdecor *libdecor; #endif } shell; + struct wl_subcompositor *subcompositor; struct zwp_relative_pointer_manager_v1 *relative_pointer_manager; struct zwp_pointer_constraints_v1 *pointer_constraints; struct wp_pointer_warp_v1 *wp_pointer_warp_v1; @@ -85,6 +86,7 @@ struct SDL_VideoData struct zwp_tablet_manager_v2 *tablet_manager; struct wl_fixes *wl_fixes; struct zwp_pointer_gestures_v1 *zwp_pointer_gestures; + struct wp_single_pixel_buffer_manager_v1 *single_pixel_buffer_manager; struct xkb_context *xkb_context; diff --git a/src/video/wayland/SDL_waylandwindow.c b/src/video/wayland/SDL_waylandwindow.c index 5dac6f22a8..7fdaa52b83 100644 --- a/src/video/wayland/SDL_waylandwindow.c +++ b/src/video/wayland/SDL_waylandwindow.c @@ -117,39 +117,15 @@ static enum WaylandModeScale GetModeScaleMethod(void) return scale_mode; } -static void GetBufferSize(SDL_Window *window, int *width, int *height) -{ - SDL_WindowData *data = window->internal; - int buf_width; - int buf_height; - - // Exclusive fullscreen modes always have a pixel density of 1 - if (data->is_fullscreen && window->fullscreen_exclusive) { - buf_width = window->current_fullscreen_mode.w; - buf_height = window->current_fullscreen_mode.h; - } else if (!data->scale_to_display) { - // Round fractional backbuffer sizes halfway away from zero. - buf_width = PointToPixel(window, data->requested.logical_width); - buf_height = PointToPixel(window, data->requested.logical_height); - } else { - buf_width = data->requested.pixel_width; - buf_height = data->requested.pixel_height; - } - - if (width) { - *width = buf_width; - } - if (height) { - *height = buf_height; - } -} - static void SetMinMaxDimensions(SDL_Window *window) { SDL_WindowData *wind = window->internal; int min_width, min_height, max_width, max_height; - if ((window->flags & SDL_WINDOW_FULLSCREEN) || wind->fullscreen_deadline_count) { + /* Keep the limits off while the window is in a fixed-size state, or the controls + * to exit that state may be disabled. + */ + if (window->flags & (SDL_WINDOW_FULLSCREEN | SDL_WINDOW_MAXIMIZED)) { min_width = 0; min_height = 0; max_width = 0; @@ -184,6 +160,13 @@ static void SetMinMaxDimensions(SDL_Window *window) if (!wind->shell_surface.libdecor.frame) { return; // Can't do anything yet, wait for ShowWindow } + + if (min_width && min_height && min_width == max_width && min_height == max_height) { + libdecor_frame_unset_capabilities(wind->shell_surface.libdecor.frame, LIBDECOR_ACTION_RESIZE); + } else { + libdecor_frame_set_capabilities(wind->shell_surface.libdecor.frame, LIBDECOR_ACTION_RESIZE); + } + /* No need to change these values if the window is non-resizable, * as libdecor will just overwrite them internally. */ @@ -286,18 +269,17 @@ static void RepositionPopup(SDL_Window *window, bool use_current_position) } } -static void SetSurfaceOpaqueRegion(SDL_WindowData *wind, bool is_opaque) +static void SetSurfaceOpaqueRegion(struct wl_surface *surface, int width, int height) { - SDL_VideoData *viddata = wind->waylandData; + SDL_VideoData *viddata = SDL_GetVideoDevice()->internal; - if (is_opaque) { + if (width && height) { struct wl_region *region = wl_compositor_create_region(viddata->compositor); - wl_region_add(region, 0, 0, - wind->current.logical_width, wind->current.logical_height); - wl_surface_set_opaque_region(wind->surface, region); + wl_region_add(region, 0, 0, width, height); + wl_surface_set_opaque_region(surface, region); wl_region_destroy(region); } else { - wl_surface_set_opaque_region(wind->surface, NULL); + wl_surface_set_opaque_region(surface, NULL); } } @@ -307,29 +289,19 @@ static void ConfigureWindowGeometry(SDL_Window *window) const double scale_factor = GetWindowScale(window); const int old_pixel_width = data->current.pixel_width; const int old_pixel_height = data->current.pixel_height; - int window_width, window_height; + int window_width = 0; + int window_height = 0; + int viewport_width, viewport_height; bool window_size_changed; - - // Set the drawable backbuffer size. - GetBufferSize(window, &data->current.pixel_width, &data->current.pixel_height); - const bool buffer_size_changed = data->current.pixel_width != old_pixel_width || - data->current.pixel_height != old_pixel_height; - - if (data->egl_window && buffer_size_changed) { - WAYLAND_wl_egl_window_resize(data->egl_window, - data->current.pixel_width, - data->current.pixel_height, - 0, 0); - } + bool buffer_size_changed; + const bool is_opaque = !(window->flags & SDL_WINDOW_TRANSPARENT) && window->opacity == 1.0f; if (data->is_fullscreen && window->fullscreen_exclusive) { - int output_width; - int output_height; window_width = window->current_fullscreen_mode.w; window_height = window->current_fullscreen_mode.h; - output_width = data->requested.logical_width; - output_height = data->requested.logical_height; + viewport_width = data->requested.logical_width; + viewport_height = data->requested.logical_height; switch (GetModeScaleMethod()) { case WAYLAND_MODE_SCALE_NONE: @@ -337,40 +309,50 @@ static void ConfigureWindowGeometry(SDL_Window *window) * Windows can request a smaller size, but exceeding these dimensions is a protocol violation, * thus, modes that exceed the output size still need to be scaled with a viewport. */ - if (window_width <= output_width && window_height <= output_height) { - output_width = window_width; - output_height = window_height; + if (window_width <= viewport_width && window_height <= viewport_height) { + viewport_width = window_width; + viewport_height = window_height; break; } SDL_FALLTHROUGH; case WAYLAND_MODE_SCALE_ASPECT: { - const float output_ratio = (float)output_width / (float)output_height; + const float output_ratio = (float)viewport_width / (float)viewport_height; const float mode_ratio = (float)window_width / (float)window_height; if (output_ratio > mode_ratio) { - output_width = SDL_lroundf((float)window_width * ((float)output_height / (float)window_height)); + viewport_width = SDL_lroundf((float)window_width * ((float)viewport_height / (float)window_height)); } else if (output_ratio < mode_ratio) { - output_height = SDL_lroundf((float)window_height * ((float)output_width / (float)window_width)); + viewport_height = SDL_lroundf((float)window_height * ((float)viewport_width / (float)window_width)); } } break; default: break; } - window_size_changed = window_width != window->w || window_height != window->h || - data->current.logical_width != output_width || data->current.logical_height != output_height; + window_size_changed = window_width != window->w || + window_height != window->h || + data->current.viewport_width != viewport_width || + data->current.viewport_height != viewport_height; + + // Exclusive fullscreen window sizes are always in pixel units. + data->current.pixel_width = window_width; + data->current.pixel_height = window_height; + buffer_size_changed = data->current.pixel_width != old_pixel_width || + data->current.pixel_height != old_pixel_height; if (window_size_changed || buffer_size_changed) { if (data->viewport) { - wp_viewport_set_destination(data->viewport, output_width, output_height); + wp_viewport_set_destination(data->viewport, viewport_width, viewport_height); - data->current.logical_width = output_width; - data->current.logical_height = output_height; + data->current.logical_width = data->requested.logical_width; + data->current.logical_height = data->requested.logical_height; + data->current.viewport_width = viewport_width; + data->current.viewport_height = viewport_height; } else { // Calculate the integer scale from the mode and output. - const int32_t int_scale = SDL_max(window->current_fullscreen_mode.w / output_width, 1); + const int32_t int_scale = SDL_max(window->current_fullscreen_mode.w / viewport_width, 1); wl_surface_set_buffer_scale(data->surface, int_scale); data->current.logical_width = window->current_fullscreen_mode.w; @@ -381,33 +363,184 @@ static void ConfigureWindowGeometry(SDL_Window *window) data->pointer_scale.y = (double)window_height / (double)data->current.logical_height; } } else { - window_width = data->requested.logical_width; - window_height = data->requested.logical_height; + if (!data->scale_to_display) { + viewport_width = data->requested.logical_width; + viewport_height = data->requested.logical_height; + } else { + viewport_width = data->requested.pixel_width; + viewport_height = data->requested.pixel_height; + } - window_size_changed = window_width != data->current.logical_width || window_height != data->current.logical_height; + if (data->viewport && data->waylandData->subcompositor && !data->is_fullscreen) { + if (window->min_w) { + window_width = viewport_width = SDL_max(viewport_width, window->min_w); + } + if (window->min_h) { + window_height = viewport_height = SDL_max(viewport_height, window->min_h); + } + if (window->max_w) { + window_width = viewport_width = SDL_min(viewport_width, window->max_w); + } + if (window->max_h) { + window_height = viewport_height = SDL_min(viewport_height, window->max_h); + } + + float aspect = (float)viewport_width / (float)viewport_height; + if (window->min_aspect != 0.f && aspect < window->min_aspect) { + viewport_height = SDL_lroundf((float)viewport_width / window->min_aspect); + } else if (window->max_aspect != 0.f && aspect > window->max_aspect) { + viewport_width = SDL_lroundf((float)viewport_height * window->max_aspect); + } + + // At this point, the viewport matches the window dimensions, but the viewport might be clamped to window dimensions beyond here. + window_width = viewport_width; + window_height = viewport_height; + + // If the viewport bounds exceed the window size, scale them while maintaining the aspect ratio. + if (!data->scale_to_display) { + if (viewport_width > data->requested.logical_width || viewport_height > data->requested.logical_height) { + aspect = (float)viewport_width / (float)viewport_height; + const float window_ratio = (float)data->requested.logical_width / (float)data->requested.logical_height; + if (aspect >= window_ratio) { + viewport_width = data->requested.logical_width; + viewport_height = SDL_lroundf((float)viewport_width / aspect); + } else if (aspect < window_ratio) { + viewport_height = data->requested.logical_height; + viewport_width = SDL_lroundf((float)viewport_height * aspect); + } + } + } else { + if (viewport_width > data->requested.pixel_width || viewport_height > data->requested.pixel_height) { + aspect = (float)viewport_width / (float)viewport_height; + const float window_ratio = (float)data->requested.pixel_width / (float)data->requested.pixel_height; + if (aspect >= window_ratio) { + viewport_width = data->requested.pixel_width; + viewport_height = SDL_lroundf((float)viewport_width / aspect); + } else if (aspect < window_ratio) { + viewport_height = data->requested.pixel_height; + viewport_width = SDL_lroundf((float)viewport_height * aspect); + } + } + } + } else { + window_width = viewport_width; + window_height = viewport_height; + } + + if (!data->scale_to_display) { + data->current.pixel_width = PointToPixel(window, window_width); + data->current.pixel_height = PointToPixel(window, window_height); + } else { + // The viewport size is in pixels at this point; convert it to logical units. + data->current.pixel_width = window_width; + data->current.pixel_height = window_height; + viewport_width = PixelToPoint(window, viewport_width); + viewport_height = PixelToPoint(window, viewport_height); + } + + // Clamp the physical window size to the system minimum required size. + data->requested.logical_width = SDL_max(data->requested.logical_width, data->system_limits.min_width); + data->requested.logical_height = SDL_max(data->requested.logical_height, data->system_limits.min_height); + + window_size_changed = data->requested.logical_width != data->current.logical_width || + data->requested.logical_height != data->current.logical_height || + viewport_width != data->current.viewport_width || + viewport_height != data->current.viewport_height; + + buffer_size_changed = data->current.pixel_width != old_pixel_width || + data->current.pixel_height != old_pixel_height; if (window_size_changed || buffer_size_changed) { if (data->viewport) { - wp_viewport_set_destination(data->viewport, window_width, window_height); + wp_viewport_set_destination(data->viewport, viewport_width, viewport_height); } else if (window->flags & SDL_WINDOW_HIGH_PIXEL_DENSITY) { // 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. wl_surface_set_buffer_scale(data->surface, (int32_t)scale_factor); } - // Clamp the physical window size to the system minimum required size. - data->current.logical_width = SDL_max(window_width, data->system_limits.min_width); - data->current.logical_height = SDL_max(window_height, data->system_limits.min_height); + data->current.logical_width = data->requested.logical_width; + data->current.logical_height = data->requested.logical_height; + data->current.viewport_width = viewport_width; + data->current.viewport_height = viewport_height; - if (!data->scale_to_display) { - data->pointer_scale.x = 1.0; - data->pointer_scale.y = 1.0; - } else { - data->pointer_scale.x = scale_factor; - data->pointer_scale.y = scale_factor; - } + data->pointer_scale.x = (double)window_width / (double)viewport_width; + data->pointer_scale.y = (double)window_height / (double)viewport_height; } } + if (data->egl_window && buffer_size_changed) { + WAYLAND_wl_egl_window_resize(data->egl_window, + data->current.pixel_width, + data->current.pixel_height, + 0, 0); + } + + /* Calculate the mask size and offset. + * Fullscreen windows are centered and masked automatically by the compositor. + */ + if (data->viewport && data->waylandData->subcompositor && !data->is_fullscreen && + (viewport_width != data->current.logical_width || viewport_height != data->current.logical_height)) { + struct wl_buffer *old_buffer = NULL; + + if (!data->mask.surface) { + data->mask.surface = wl_compositor_create_surface(data->waylandData->compositor); + SDL_WAYLAND_register_surface(data->mask.surface); + wl_surface_set_user_data(data->mask.surface, data); + } + if (!data->mask.subsurface) { + data->mask.subsurface = wl_subcompositor_get_subsurface(data->waylandData->subcompositor, data->mask.surface, data->surface); + } + if (!data->mask.viewport) { + data->mask.viewport = wp_viewporter_get_viewport(data->waylandData->viewporter, data->mask.surface); + } + if (!data->mask.buffer || data->mask.opaque != is_opaque) { + old_buffer = data->mask.buffer; + data->mask.opaque = is_opaque; + data->mask.buffer = Wayland_CreateSinglePixelBuffer(0, 0, 0, is_opaque ? SDL_MAX_UINT32 : 0); + } + + wl_surface_attach(data->mask.surface, data->mask.buffer, 0, 0); + + wl_subsurface_place_below(data->mask.subsurface, data->surface); + wp_viewport_set_destination(data->mask.viewport, data->current.logical_width, data->current.logical_height); + + if (wl_surface_get_version(data->mask.surface) >= WL_SURFACE_DAMAGE_BUFFER_SINCE_VERSION) { + wl_surface_damage_buffer(data->mask.surface, 0, 0, SDL_MAX_SINT32, SDL_MAX_SINT32); + } else { + wl_surface_damage(data->mask.surface, 0, 0, SDL_MAX_SINT32, SDL_MAX_SINT32); + } + + if (is_opaque) { + SetSurfaceOpaqueRegion(data->mask.surface, data->current.logical_width, data->current.logical_height); + } else { + SetSurfaceOpaqueRegion(data->mask.surface, 0, 0); + } + + data->mask.offset_x = -(data->current.logical_width - viewport_width) / 2; + data->mask.offset_y = -(data->current.logical_height - viewport_height) / 2; + + // Can't use an offset subsurface with libdecor (yet), or the decorations won't line up properly. + if (data->shell_surface_type != WAYLAND_SHELL_SURFACE_TYPE_LIBDECOR) { + wl_subsurface_set_position(data->mask.subsurface, data->mask.offset_x, data->mask.offset_y); + } + + wl_surface_commit(data->mask.surface); + + if (old_buffer) { + wl_buffer_destroy(old_buffer); + } + + data->mask.mapped = true; + } else if (data->mask.mapped) { + wl_subsurface_set_position(data->mask.subsurface, 0, 0); + wl_surface_attach(data->mask.surface, NULL, 0, 0); + wl_surface_commit(data->mask.surface); + + data->mask.offset_x = 0; + data->mask.offset_y = 0; + data->mask.mapped = false; + } + /* * The surface geometry, opaque region and pointer confinement region only * need to be recalculated if the output size has changed. @@ -421,7 +554,11 @@ static void ConfigureWindowGeometry(SDL_Window *window) xdg_surface_set_window_geometry(data->shell_surface.xdg.surface, 0, 0, data->current.logical_width, data->current.logical_height); } - SetSurfaceOpaqueRegion(data, !(window->flags & SDL_WINDOW_TRANSPARENT) && window->opacity == 1.0f); + if (is_opaque) { + SetSurfaceOpaqueRegion(data->surface, viewport_width, viewport_height); + } else { + SetSurfaceOpaqueRegion(data->surface, 0, 0); + } // Ensure that child popup windows are still in bounds. for (SDL_Window *child = window->first_child; child; child = child->next_sibling) { @@ -473,47 +610,40 @@ static void CommitLibdecorFrame(SDL_Window *window) #endif } -static void fullscreen_deadline_handler(void *data, struct wl_callback *callback, uint32_t callback_data) +static void pending_state_deadline_handler(void *data, struct wl_callback *callback, uint32_t callback_data) { - // Get the window from the ID as it may have been destroyed - SDL_WindowID windowID = (SDL_WindowID)((uintptr_t)data); + // Get the window from the ID, as it may have been destroyed. + SDL_WindowID windowID = (SDL_WindowID)(uintptr_t)data; SDL_Window *window = SDL_GetWindowFromID(windowID); if (window && window->internal) { - window->internal->fullscreen_deadline_count--; + --window->internal->pending_state_deadline_count; } wl_callback_destroy(callback); } -static struct wl_callback_listener fullscreen_deadline_listener = { - fullscreen_deadline_handler +static struct wl_callback_listener pending_state_deadline_listener = { + pending_state_deadline_handler }; -static void maximized_restored_deadline_handler(void *data, struct wl_callback *callback, uint32_t callback_data) +static void AddPendingStateSync(SDL_WindowData *window_data) { - // Get the window from the ID as it may have been destroyed - SDL_WindowID windowID = (SDL_WindowID)((uintptr_t)data); - SDL_Window *window = SDL_GetWindowFromID(windowID); + SDL_VideoData *video_data = window_data->waylandData; + void *cb_data = (void *)(uintptr_t)window_data->sdlwindow->id; - if (window && window->internal) { - window->internal->maximized_restored_deadline_count--; - } - - wl_callback_destroy(callback); + ++window_data->pending_state_deadline_count; + struct wl_callback *cb = wl_display_sync(video_data->display); + wl_callback_add_listener(cb, &pending_state_deadline_listener, cb_data); } -static struct wl_callback_listener maximized_restored_deadline_listener = { - maximized_restored_deadline_handler -}; - static void FlushPendingEvents(SDL_Window *window) { // Serialize and restore the pending flags, as they may be overwritten while flushing. const bool last_position_pending = window->last_position_pending; const bool last_size_pending = window->last_size_pending; - while (window->internal->fullscreen_deadline_count || window->internal->maximized_restored_deadline_count) { + while (window->internal->pending_state_deadline_count) { WAYLAND_wl_display_roundtrip(window->internal->waylandData->display); } @@ -579,7 +709,6 @@ static void Wayland_move_window(SDL_Window *window) static void SetFullscreen(SDL_Window *window, struct wl_output *output, bool fullscreen) { SDL_WindowData *wind = window->internal; - SDL_VideoData *viddata = wind->waylandData; #ifdef HAVE_LIBDECOR_H if (wind->shell_surface_type == WAYLAND_SHELL_SURFACE_TYPE_LIBDECOR) { @@ -588,11 +717,7 @@ static void SetFullscreen(SDL_Window *window, struct wl_output *output, bool ful } wind->fullscreen_exclusive = output ? window->fullscreen_exclusive : false; - ++wind->fullscreen_deadline_count; if (fullscreen) { - Wayland_SetWindowResizable(SDL_GetVideoDevice(), window, true); - wl_surface_commit(wind->surface); - libdecor_frame_set_fullscreen(wind->shell_surface.libdecor.frame, output); } else { libdecor_frame_unset_fullscreen(wind->shell_surface.libdecor.frame); @@ -605,20 +730,14 @@ static void SetFullscreen(SDL_Window *window, struct wl_output *output, bool ful } wind->fullscreen_exclusive = output ? window->fullscreen_exclusive : false; - ++wind->fullscreen_deadline_count; if (fullscreen) { - Wayland_SetWindowResizable(SDL_GetVideoDevice(), window, true); - wl_surface_commit(wind->surface); - xdg_toplevel_set_fullscreen(wind->shell_surface.xdg.toplevel.xdg_toplevel, output); } else { xdg_toplevel_unset_fullscreen(wind->shell_surface.xdg.toplevel.xdg_toplevel); } } - // Queue a deadline event - struct wl_callback *cb = wl_display_sync(viddata->display); - wl_callback_add_listener(cb, &fullscreen_deadline_listener, (void *)((uintptr_t)window->id)); + AddPendingStateSync(wind); } static void UpdateWindowFullscreen(SDL_Window *window, bool fullscreen) @@ -677,6 +796,8 @@ static void surface_frame_done(void *data, struct wl_callback *cb, uint32_t time wl_surface_damage(wind->surface, 0, 0, SDL_MAX_SINT32, SDL_MAX_SINT32); } + wind->pending_state_commit = false; + if (wind->shell_surface_type == WAYLAND_SHELL_SURFACE_TYPE_XDG_TOPLEVEL) { if (wind->pending_config_ack) { wind->pending_config_ack = false; @@ -827,6 +948,9 @@ static void handle_xdg_toplevel_configure(void *data, } } + // When resizing, dimensions other than 0 are a maximum. + const bool new_configure_size = width != wind->last_configure.width || height != wind->last_configure.height; + UpdateWindowFullscreen(window, fullscreen); /* Always send a maximized/restore event; if the event is redundant it will @@ -851,51 +975,72 @@ static void handle_xdg_toplevel_configure(void *data, /* xdg_toplevel spec states that this is a suggestion. * Ignore if less than or greater than max/min size. */ - if (window->flags & SDL_WINDOW_RESIZABLE) { - if (width == 0 || height == 0) { + if ((window->flags & SDL_WINDOW_RESIZABLE) || maximized) { + if (!width) { /* This happens when the compositor indicates that the size is * up to the client, so use the cached window size here. */ if (floating) { width = window->floating.w; - height = window->floating.h; // Clamp the window to the toplevel bounds, if any are set. - if (wind->shell_surface_status == WAYLAND_SHELL_SURFACE_STATUS_WAITING_FOR_CONFIGURE && - wind->toplevel_bounds.width && wind->toplevel_bounds.height) { + if (wind->shell_surface_status == WAYLAND_SHELL_SURFACE_STATUS_WAITING_FOR_CONFIGURE && wind->toplevel_bounds.width) { width = SDL_min(wind->toplevel_bounds.width, width); - height = SDL_min(wind->toplevel_bounds.height, height); } } else { width = window->windowed.w; - height = window->windowed.h; } if (!wind->scale_to_display) { wind->requested.logical_width = width; - wind->requested.logical_height = height; } else { wind->requested.pixel_width = width; - wind->requested.pixel_height = height; width = wind->requested.logical_width = PixelToPoint(window, width); - height = wind->requested.logical_height = PixelToPoint(window, height); } - } else { + } else if (new_configure_size) { /* Don't apply the supplied dimensions if they haven't changed from the last configuration * event, or a newer size set programmatically can be overwritten by old data. */ - if (width != wind->last_configure.width || height != wind->last_configure.height) { - wind->requested.logical_width = width; - wind->requested.logical_height = height; - if (wind->scale_to_display) { - wind->requested.pixel_width = PointToPixel(window, width); - wind->requested.pixel_height = PointToPixel(window, height); + wind->requested.logical_width = width; + + if (wind->scale_to_display) { + wind->requested.pixel_width = PointToPixel(window, width); + } + } + if (!height) { + /* This happens when the compositor indicates that the size is + * up to the client, so use the cached window size here. + */ + if (floating) { + height = window->floating.h; + + // Clamp the window to the toplevel bounds, if any are set. + if (wind->shell_surface_status == WAYLAND_SHELL_SURFACE_STATUS_WAITING_FOR_CONFIGURE && wind->toplevel_bounds.height) { + height = SDL_min(wind->toplevel_bounds.height, height); } + } else { + height = window->windowed.h; + } + + if (!wind->scale_to_display) { + wind->requested.logical_height = height; + } else { + wind->requested.pixel_height = height; + height = wind->requested.logical_height = PixelToPoint(window, height); + } + } else if (new_configure_size) { + /* Don't apply the supplied dimensions if they haven't changed from the last configuration + * event, or a newer size set programmatically can be overwritten by old data. + */ + wind->requested.logical_height = height; + + if (wind->scale_to_display) { + wind->requested.pixel_height = PointToPixel(window, height); } } } else { - /* If we're a fixed-size window, we know our size for sure. + /* If we're a fixed-size, non-maximized window, we know our size for sure. * Always assume the configure is wrong. */ if (!wind->scale_to_display) { @@ -909,20 +1054,24 @@ static void handle_xdg_toplevel_configure(void *data, } } - /* Notes on the spec: + /* Notes on the spec and implementations: * * - The content limits are only a hint, which the compositor is free to ignore, * so apply them manually when appropriate. * - * - Maximized windows must have their exact dimensions respected, thus they must - * not be resized, or a protocol violation can occur. + * - Only floating windows are truly safe to resize: maximized windows must have + * their exact dimensions respected, or a protocol violation can occur, and tiled + * windows can technically use dimensions smaller than the ones supplied by the + * compositor, but doing so can cause odd behavior. In these cases it's best to use + * the supplied dimensions and use a viewport + mask to enforce the size limits and/or + * aspect ratio. * * - When resizing a window, the width/height are maximum values, so aspect ratio * correction can't resize beyond the existing dimensions, or a protocol violation - * can occur. In practice, nothing seems to kill clients that do this, but doing - * so causes GNOME to glitch out. + * can occur. However, in practice, nothing seems to kill clients that do this, but + * doing so can cause certain compositors to glitch out. */ - if (!maximized) { + if (floating) { if (!wind->scale_to_display) { if (window->max_w > 0) { wind->requested.logical_width = SDL_min(wind->requested.logical_width, window->max_w); @@ -1198,7 +1347,6 @@ static void decoration_frame_configure(struct libdecor_frame *frame, enum libdecor_window_state window_state; int width, height; - bool prev_fullscreen = wind->is_fullscreen; bool active = false; bool fullscreen = false; bool maximized = false; @@ -1210,6 +1358,10 @@ static void decoration_frame_configure(struct libdecor_frame *frame, static const enum libdecor_window_state tiled_states = (LIBDECOR_WINDOW_STATE_TILED_LEFT | LIBDECOR_WINDOW_STATE_TILED_RIGHT | LIBDECOR_WINDOW_STATE_TILED_TOP | LIBDECOR_WINDOW_STATE_TILED_BOTTOM); + if (wind->shell_surface_status == WAYLAND_SHELL_SURFACE_STATUS_WAITING_FOR_CONFIGURE) { + LibdecorGetMinContentSize(frame, &wind->system_limits.min_width, &wind->system_limits.min_height); + } + // Window State if (libdecor_configuration_get_window_state(configuration, &window_state)) { fullscreen = (window_state & LIBDECOR_WINDOW_STATE_FULLSCREEN) != 0; @@ -1276,8 +1428,8 @@ static void decoration_frame_configure(struct libdecor_frame *frame, } } } else { - if (!(window->flags & SDL_WINDOW_RESIZABLE)) { - /* If we're a fixed-size window, we know our size for sure. + if (!(window->flags & SDL_WINDOW_RESIZABLE) && !maximized) { + /* If we're a fixed-size, non-maximized window, we know our size for sure. * Always assume the configure is wrong. */ if (!wind->scale_to_display) { @@ -1302,32 +1454,72 @@ static void decoration_frame_configure(struct libdecor_frame *frame, */ if ((floating && (!wind->floating && !(window->flags & SDL_WINDOW_BORDERLESS))) || !libdecor_configuration_get_content_size(configuration, frame, &width, &height)) { + width = 0; + height = 0; + } + + const bool new_configure_size = width != wind->last_configure.width || height != wind->last_configure.height; + + if (!width) { /* This happens when we're being restored from a non-floating state, * or the compositor indicates that the size is up to the client, so * used the cached window size here. */ if (floating) { width = window->floating.w; - height = window->floating.h; // Clamp the window to the toplevel bounds, if any are set. - if (wind->shell_surface_status == WAYLAND_SHELL_SURFACE_STATUS_WAITING_FOR_CONFIGURE && - wind->toplevel_bounds.width && wind->toplevel_bounds.height) { + if (wind->shell_surface_status == WAYLAND_SHELL_SURFACE_STATUS_WAITING_FOR_CONFIGURE && wind->toplevel_bounds.width) { width = SDL_min(wind->toplevel_bounds.width, width); - height = SDL_min(wind->toplevel_bounds.height, height); } } else { width = window->windowed.w; - height = window->windowed.h; } if (!wind->scale_to_display) { wind->requested.logical_width = width; - wind->requested.logical_height = height; } else { wind->requested.pixel_width = width; - wind->requested.pixel_height = height; width = wind->requested.logical_width = PixelToPoint(window, width); + } + } else { + /* Don't apply the supplied dimensions if they haven't changed from the last configuration + * event, or a newer size set programmatically can be overwritten by old data. + * + * If a client takes a long time to present the first frame after creating the window, a + * configure event to set the suspended state may arrive with the content size increased + * by the decoration dimensions, which should also be ignored. + */ + if (new_configure_size && + !(wind->shell_surface_status == WAYLAND_SHELL_SURFACE_STATUS_WAITING_FOR_FRAME && wind->suspended != suspended)) { + wind->requested.logical_width = width; + + if (wind->scale_to_display) { + wind->requested.pixel_width = PointToPixel(window, width); + } + } + } + + if (!height) { + /* This happens when we're being restored from a non-floating state, + * or the compositor indicates that the size is up to the client, so + * used the cached window size here. + */ + if (floating) { + height = window->floating.h; + + // Clamp the window to the toplevel bounds, if any are set. + if (wind->shell_surface_status == WAYLAND_SHELL_SURFACE_STATUS_WAITING_FOR_CONFIGURE && wind->toplevel_bounds.height) { + height = SDL_min(wind->toplevel_bounds.height, height); + } + } else { + height = window->windowed.h; + } + + if (!wind->scale_to_display) { + wind->requested.logical_height = height; + } else { + wind->requested.pixel_height = height; height = wind->requested.logical_height = PixelToPoint(window, height); } } else { @@ -1338,33 +1530,35 @@ static void decoration_frame_configure(struct libdecor_frame *frame, * configure event to set the suspended state may arrive with the content size increased * by the decoration dimensions, which should also be ignored. */ - if ((width != wind->last_configure.width || height != wind->last_configure.height) && + if (new_configure_size && !(wind->shell_surface_status == WAYLAND_SHELL_SURFACE_STATUS_WAITING_FOR_FRAME && wind->suspended != suspended)) { - wind->requested.logical_width = width; wind->requested.logical_height = height; if (wind->scale_to_display) { - wind->requested.pixel_width = PointToPixel(window, width); wind->requested.pixel_height = PointToPixel(window, height); } } } } - /* Notes on the spec: + /* Notes on the spec and implementations: * * - The content limits are only a hint, which the compositor is free to ignore, * so apply them manually when appropriate. * - * - Maximized windows must have their exact dimensions respected, thus they must - * not be resized, or a protocol violation can occur. + * - Only floating windows are truly safe to resize: maximized windows must have + * their exact dimensions respected, or a protocol violation can occur, and tiled + * windows can technically use dimensions smaller than the ones supplied by the + * compositor, but doing so can cause odd behavior. In these cases it's best to use + * the supplied dimensions and use a viewport + mask to enforce the size limits and/or + * aspect ratio. * * - When resizing a window, the width/height are maximum values, so aspect ratio * correction can't resize beyond the existing dimensions, or a protocol violation - * can occur. In practice, nothing seems to kill clients that do this, but doing - * so causes GNOME to glitch out. + * can occur. However, in practice, nothing seems to kill clients that do this, but + * doing so can cause certain compositors to glitch out. */ - if (!maximized) { + if (floating) { if (!wind->scale_to_display) { if (window->max_w > 0) { wind->requested.logical_width = SDL_min(wind->requested.logical_width, window->max_w); @@ -1453,20 +1647,8 @@ static void decoration_frame_configure(struct libdecor_frame *frame, } if (wind->shell_surface_status == WAYLAND_SHELL_SURFACE_STATUS_WAITING_FOR_CONFIGURE) { - LibdecorGetMinContentSize(frame, &wind->system_limits.min_width, &wind->system_limits.min_height); wind->shell_surface_status = WAYLAND_SHELL_SURFACE_STATUS_WAITING_FOR_FRAME; } - - /* Update the resize capability if this config event was the result of the - * compositor taking a window out of fullscreen. Since this will change the - * capabilities and commit a new frame state with the last known content - * dimension, this has to be called after the new state has been committed - * and the new content dimensions were updated. - */ - if (prev_fullscreen && !wind->is_fullscreen) { - Wayland_SetWindowResizable(SDL_GetVideoDevice(), window, - !!(window->flags & SDL_WINDOW_RESIZABLE)); - } } static void decoration_frame_close(struct libdecor_frame *frame, void *user_data) @@ -2236,6 +2418,10 @@ void Wayland_HideWindow(SDL_VideoDevice *_this, SDL_Window *window) } // Attach a null buffer to unmap the surface. + if (wind->mask.surface) { + wl_surface_attach(wind->mask.surface, NULL, 0, 0); + wl_surface_commit(wind->mask.surface); + } wl_surface_attach(wind->surface, NULL, 0, 0); wl_surface_commit(wind->surface); @@ -2395,6 +2581,11 @@ SDL_FullscreenResult Wayland_SetWindowFullscreen(SDL_VideoDevice *_this, SDL_Win output = NULL; } } + + // Commit to set any pending size or limit data. + if (fullscreen && wind->pending_state_commit) { + wl_surface_commit(wind->surface); + } SetFullscreen(window, output, !!fullscreen); } else if (wind->is_fullscreen) { /* @@ -2429,7 +2620,7 @@ void Wayland_RestoreWindow(SDL_VideoDevice *_this, SDL_Window *window) // Not currently fullscreen or maximized, and no state pending; nothing to do. if (!(window->flags & (SDL_WINDOW_FULLSCREEN | SDL_WINDOW_MAXIMIZED)) && - !wind->fullscreen_deadline_count && !wind->maximized_restored_deadline_count) { + !wind->pending_state_deadline_count) { return; } @@ -2439,10 +2630,7 @@ void Wayland_RestoreWindow(SDL_VideoDevice *_this, SDL_Window *window) return; // Can't do anything yet, wait for ShowWindow } libdecor_frame_unset_maximized(wind->shell_surface.libdecor.frame); - - ++wind->maximized_restored_deadline_count; - struct wl_callback *cb = wl_display_sync(_this->internal->display); - wl_callback_add_listener(cb, &maximized_restored_deadline_listener, (void *)((uintptr_t)window->id)); + AddPendingStateSync(wind); } else #endif // Note that xdg-shell does NOT provide a way to unset minimize! @@ -2451,10 +2639,7 @@ void Wayland_RestoreWindow(SDL_VideoDevice *_this, SDL_Window *window) return; // Can't do anything yet, wait for ShowWindow } xdg_toplevel_unset_maximized(wind->shell_surface.xdg.toplevel.xdg_toplevel); - - ++wind->maximized_restored_deadline_count; - struct wl_callback *cb = wl_display_sync(_this->internal->display); - wl_callback_add_listener(cb, &maximized_restored_deadline_listener, (void *)((uintptr_t)window->id)); + AddPendingStateSync(wind); } } @@ -2480,33 +2665,21 @@ void Wayland_SetWindowBordered(SDL_VideoDevice *_this, SDL_Window *window, bool void Wayland_SetWindowResizable(SDL_VideoDevice *_this, SDL_Window *window, bool resizable) { -#ifdef HAVE_LIBDECOR_H - const SDL_WindowData *wind = window->internal; - - if (wind->shell_surface_type == WAYLAND_SHELL_SURFACE_TYPE_LIBDECOR) { - if (!wind->shell_surface.libdecor.frame) { - return; // Can't do anything yet, wait for ShowWindow - } - if (libdecor_frame_has_capability(wind->shell_surface.libdecor.frame, LIBDECOR_ACTION_RESIZE)) { - if (!resizable) { - libdecor_frame_unset_capabilities(wind->shell_surface.libdecor.frame, LIBDECOR_ACTION_RESIZE); - } - } else if (resizable) { - libdecor_frame_set_capabilities(wind->shell_surface.libdecor.frame, LIBDECOR_ACTION_RESIZE); - } - } -#endif + SDL_WindowData *wind = window->internal; /* When changing the resize capability on libdecor windows, the limits must always * be reapplied, as when libdecor changes states, it overwrites the values internally. */ SetMinMaxDimensions(window); CommitLibdecorFrame(window); + + if (wind->shell_surface_status == WAYLAND_SHELL_SURFACE_STATUS_SHOWN) { + wind->pending_state_commit = true; + } } void Wayland_MaximizeWindow(SDL_VideoDevice *_this, SDL_Window *window) { - SDL_VideoData *viddata = _this->internal; SDL_WindowData *wind = window->internal; if (wind->show_hide_sync_required) { @@ -2515,7 +2688,7 @@ void Wayland_MaximizeWindow(SDL_VideoDevice *_this, SDL_Window *window) // Not fullscreen, already maximized, and no state pending; nothing to do. if (!(window->flags & SDL_WINDOW_FULLSCREEN) && (window->flags & SDL_WINDOW_MAXIMIZED) && - !wind->fullscreen_deadline_count && !wind->maximized_restored_deadline_count) { + !wind->pending_state_deadline_count) { return; } @@ -2525,13 +2698,12 @@ void Wayland_MaximizeWindow(SDL_VideoDevice *_this, SDL_Window *window) return; // Can't do anything yet, wait for ShowWindow } - // Commit to preserve any pending size data. - wl_surface_commit(wind->surface); + // Commit to set any pending size or limit data. + if (wind->pending_state_commit) { + wl_surface_commit(wind->surface); + } libdecor_frame_set_maximized(wind->shell_surface.libdecor.frame); - - ++wind->maximized_restored_deadline_count; - struct wl_callback *cb = wl_display_sync(viddata->display); - wl_callback_add_listener(cb, &maximized_restored_deadline_listener, (void *)((uintptr_t)window->id)); + AddPendingStateSync(wind); } else #endif if (wind->shell_surface_type == WAYLAND_SHELL_SURFACE_TYPE_XDG_TOPLEVEL) { @@ -2539,13 +2711,12 @@ void Wayland_MaximizeWindow(SDL_VideoDevice *_this, SDL_Window *window) return; // Can't do anything yet, wait for ShowWindow } - // Commit to preserve any pending size data. - wl_surface_commit(wind->surface); + // Commit to set any pending size or limit data. + if (wind->pending_state_commit) { + wl_surface_commit(wind->surface); + } xdg_toplevel_set_maximized(wind->shell_surface.xdg.toplevel.xdg_toplevel); - - ++wind->maximized_restored_deadline_count; - struct wl_callback *cb = wl_display_sync(viddata->display); - wl_callback_add_listener(cb, &maximized_restored_deadline_listener, (void *)((uintptr_t)window->id)); + AddPendingStateSync(wind); } } @@ -2850,13 +3021,19 @@ bool Wayland_CreateWindow(SDL_VideoDevice *_this, SDL_Window *window, SDL_Proper void Wayland_SetWindowMinimumSize(SDL_VideoDevice *_this, SDL_Window *window) { // Will be committed when Wayland_SetWindowSize() is called by the video core. - SetMinMaxDimensions(window); + window->internal->limits_changed = true; } void Wayland_SetWindowMaximumSize(SDL_VideoDevice *_this, SDL_Window *window) { // Will be committed when Wayland_SetWindowSize() is called by the video core. - SetMinMaxDimensions(window); + window->internal->limits_changed = true; +} + +void Wayland_SetWindowAspectRatio(SDL_VideoDevice *_this, SDL_Window *window) +{ + // Will be committed when Wayland_SetWindowSize() is called by the video core. + window->internal->limits_changed = true; } bool Wayland_SetWindowPosition(SDL_VideoDevice *_this, SDL_Window *window) @@ -2906,8 +3083,10 @@ void Wayland_SetWindowSize(SDL_VideoDevice *_this, SDL_Window *window) */ FlushPendingEvents(window); - // Maximized and fullscreen windows don't get resized. - if (!(window->flags & (SDL_WINDOW_FULLSCREEN | SDL_WINDOW_MAXIMIZED)) || + /* Maximized and fullscreen windows don't get resized, and the new size is ignored + * if this is just to recalculate the min/max or aspect limits on a tiled window. + */ + if (wind->floating || (window->tiled && !wind->limits_changed) || wind->shell_surface_type == WAYLAND_SHELL_SURFACE_TYPE_CUSTOM) { if (!wind->scale_to_display) { wind->requested.logical_width = window->pending.w; @@ -2918,14 +3097,18 @@ void Wayland_SetWindowSize(SDL_VideoDevice *_this, SDL_Window *window) wind->requested.pixel_width = window->pending.w; wind->requested.pixel_height = window->pending.h; } - - ConfigureWindowGeometry(window); } else { // Can't resize the window. window->last_size_pending = false; } - // Always commit, as this may be in response to a min/max limit change. + wind->limits_changed = false; + if (wind->shell_surface_status == WAYLAND_SHELL_SURFACE_STATUS_SHOWN) { + wind->pending_state_commit = true; + } + + // Always recalculate the geometry, as this may be in response to a min/max limit change. + ConfigureWindowGeometry(window); CommitLibdecorFrame(window); } @@ -2964,7 +3147,39 @@ bool Wayland_SetWindowOpacity(SDL_VideoDevice *_this, SDL_Window *window, float SDL_WindowData *wind = window->internal; if (wind->wp_alpha_modifier_surface_v1) { - SetSurfaceOpaqueRegion(wind, !(window->flags & SDL_WINDOW_TRANSPARENT) && opacity == 1.0f); + const bool is_opaque = !(window->flags & SDL_WINDOW_TRANSPARENT) && opacity == 1.0f; + + if (wind->mask.mapped && wind->mask.opaque != is_opaque) { + struct wl_buffer *old_buffer = wind->mask.buffer; + wind->mask.opaque = is_opaque; + wind->mask.buffer = Wayland_CreateSinglePixelBuffer(0, 0, 0, is_opaque ? SDL_MAX_UINT32 : 0); + + wl_surface_attach(wind->mask.surface, wind->mask.buffer, 0, 0); + if (wl_surface_get_version(wind->mask.surface) >= WL_SURFACE_DAMAGE_BUFFER_SINCE_VERSION) { + wl_surface_damage_buffer(wind->mask.surface, 0, 0, SDL_MAX_SINT32, SDL_MAX_SINT32); + } else { + wl_surface_damage(wind->mask.surface, 0, 0, SDL_MAX_SINT32, SDL_MAX_SINT32); + } + + if (is_opaque) { + SetSurfaceOpaqueRegion(wind->mask.surface, wind->current.logical_width, wind->current.logical_height); + } else { + SetSurfaceOpaqueRegion(wind->mask.surface, 0, 0); + } + + wl_surface_commit(wind->mask.surface); + + if (old_buffer) { + wl_buffer_destroy(old_buffer); + } + } + + if (is_opaque) { + SetSurfaceOpaqueRegion(wind->surface, wind->current.viewport_width, wind->current.viewport_height); + } else { + SetSurfaceOpaqueRegion(wind->surface, 0, 0); + } + wp_alpha_modifier_surface_v1_set_multiplier(wind->wp_alpha_modifier_surface_v1, (Uint32)((double)SDL_MAX_UINT32 * (double)opacity)); return true; @@ -3163,7 +3378,7 @@ bool Wayland_SyncWindow(SDL_VideoDevice *_this, SDL_Window *window) do { WAYLAND_wl_display_roundtrip(_this->internal->display); - } while (wind->fullscreen_deadline_count || wind->maximized_restored_deadline_count); + } while (wind->pending_state_deadline_count); return true; } @@ -3275,6 +3490,18 @@ void Wayland_DestroyWindow(SDL_VideoDevice *_this, SDL_Window *window) */ Wayland_DisplayRemoveWindowReferencesFromSeats(data, wind); + if (wind->mask.viewport) { + wp_viewport_destroy(wind->mask.viewport); + } + if (wind->mask.buffer) { + wl_buffer_destroy(wind->mask.buffer); + } + if (wind->mask.subsurface) { + wl_subsurface_destroy(wind->mask.subsurface); + } + if (wind->mask.surface) { + wl_surface_destroy(wind->mask.surface); + } #ifdef SDL_VIDEO_OPENGL_EGL if (wind->egl_surface) { SDL_EGL_DestroySurface(_this, wind->egl_surface); diff --git a/src/video/wayland/SDL_waylandwindow.h b/src/video/wayland/SDL_waylandwindow.h index a82000c2bf..0f8f22f767 100644 --- a/src/video/wayland/SDL_waylandwindow.h +++ b/src/video/wayland/SDL_waylandwindow.h @@ -165,6 +165,10 @@ struct SDL_WindowData // The size of the window backbuffer in pixels. int pixel_width; int pixel_height; + + // The dimensions of the active viewport, in logical units. + int viewport_width; + int viewport_height; } current; // The last compositor requested parameters; used for deduplication of window geometry configuration. @@ -188,6 +192,20 @@ struct SDL_WindowData int height; } toplevel_bounds; + struct + { + struct wl_surface *surface; + struct wl_subsurface *subsurface; + struct wl_buffer *buffer; + struct wp_viewport *viewport; + + int offset_x; + int offset_y; + + bool mapped; + bool opaque; + } mask; + struct { int hint; @@ -196,8 +214,7 @@ struct SDL_WindowData } text_input_props; SDL_DisplayID last_displayID; - int fullscreen_deadline_count; - int maximized_restored_deadline_count; + int pending_state_deadline_count; Uint64 last_focus_event_time_ns; int icc_fd; Uint32 icc_size; @@ -206,6 +223,8 @@ struct SDL_WindowData bool resizing; bool active; bool pending_config_ack; + bool pending_state_commit; + bool limits_changed; bool is_fullscreen; bool fullscreen_exclusive; bool drop_fullscreen_requests; @@ -236,6 +255,7 @@ extern void Wayland_SetWindowResizable(SDL_VideoDevice *_this, SDL_Window *windo extern bool Wayland_CreateWindow(SDL_VideoDevice *_this, SDL_Window *window, SDL_PropertiesID create_props); extern bool Wayland_SetWindowPosition(SDL_VideoDevice *_this, SDL_Window *window); extern void Wayland_SetWindowSize(SDL_VideoDevice *_this, SDL_Window *window); +extern void Wayland_SetWindowAspectRatio(SDL_VideoDevice *_this, SDL_Window *window); extern void Wayland_SetWindowMinimumSize(SDL_VideoDevice *_this, SDL_Window *window); extern void Wayland_SetWindowMaximumSize(SDL_VideoDevice *_this, SDL_Window *window); extern void Wayland_GetWindowSizeInPixels(SDL_VideoDevice *_this, SDL_Window *window, int *w, int *h); diff --git a/wayland-protocols/single-pixel-buffer-v1.xml b/wayland-protocols/single-pixel-buffer-v1.xml new file mode 100644 index 0000000000..bcbd0382eb --- /dev/null +++ b/wayland-protocols/single-pixel-buffer-v1.xml @@ -0,0 +1,76 @@ + + + + Copyright © 2022 Simon Ser + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice (including the next + paragraph) shall be included in all copies or substantial portions of the + Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. + + + + This protocol extension allows clients to create single-pixel buffers. + + Compositors supporting this protocol extension should also support the + viewporter protocol extension. Clients may use viewporter to scale a + single-pixel buffer to a desired size. + + Warning! The protocol described in this file is currently in the testing + phase. Backward compatible changes may be added together with the + corresponding interface version bump. Backward incompatible changes can + only be done by creating a new major version of the extension. + + + + + The wp_single_pixel_buffer_manager_v1 interface is a factory for + single-pixel buffers. + + + + + Destroy the wp_single_pixel_buffer_manager_v1 object. + + The child objects created via this interface are unaffected. + + + + + + Create a single-pixel buffer from four 32-bit RGBA values. + + Unless specified in another protocol extension, the RGBA values use + pre-multiplied alpha. + + The width and height of the buffer are 1. + + The r, g, b and a arguments valid range is from UINT32_MIN (0) + to UINT32_MAX (0xffffffff). + + These arguments should be interpreted as a percentage, i.e. + - UINT32_MIN = 0% of the given color component + - UINT32_MAX = 100% of the given color component + + + + + + + + +