diff --git a/docs/README-wayland.md b/docs/README-wayland.md index 720807032c7ec..3400c7e54cae0 100644 --- a/docs/README-wayland.md +++ b/docs/README-wayland.md @@ -6,6 +6,22 @@ encounter limitations or behavior that is different from other windowing systems ## Common issues: +### ```SDL_SetWindowPosition()``` doesn't work on non-popup windows + +- Wayland requires the `ext-zones-v1` extension to position windows programmatically. Otherwise, toplevel windows may + not be positioned programmatically.\ + \ + Enabling this protocol requires setting the `SDL_VIDEO_WAYLAND_ENABLE_ZONES` hint to `1`. After initializing the video + subsystem, compositor support for the required protocol may be queried via the + `SDL_PROP_GLOBAL_VIDEO_WAYLAND_HAS_ZONES_BOOLEAN` global video property.\ + \ + This protocol allows for positioning windows within the boundaries of desktop zones, the coordinates of which may not + correspond 1:1 to output display coordinates. This is primarily intended for clients with multi-window interfaces that + need to position windows relative to one another, development environments/workflows, and embedded scenarios where + positioning is desired and the underlying environment and its capabilities are known. Single window clients should + _not_ enable this by default, as it can override compositor window positioning, and tiling window managers in + particular may demonstrate undesirable behavior with it. + ### Legacy, DPI-unaware applications are blurry - Wayland handles high-DPI displays by scaling the desktop, which causes applications that are not designed to be @@ -34,10 +50,6 @@ encounter limitations or behavior that is different from other windowing systems system settings, and falling back to a selection algorithm if this fails. If it is incorrect, it can be manually overridden by setting the ```SDL_VIDEO_DISPLAY_PRIORITY``` hint. -### ```SDL_SetWindowPosition()``` doesn't work on non-popup windows - -- Wayland does not allow toplevel windows to position themselves programmatically. - ### Retrieving the global mouse cursor position when the cursor is outside a window doesn't work - Wayland only provides applications with the cursor position within the borders of the application windows. Querying diff --git a/include/SDL3/SDL_hints.h b/include/SDL3/SDL_hints.h index b5565b5496101..0e155f2b98ece 100644 --- a/include/SDL3/SDL_hints.h +++ b/include/SDL3/SDL_hints.h @@ -3784,6 +3784,34 @@ extern "C" { */ #define SDL_HINT_VIDEO_WAYLAND_ALLOW_LIBDECOR "SDL_VIDEO_WAYLAND_ALLOW_LIBDECOR" +/** + * A variable controlling whether to allow positioning of windows via the + * `ext-zones-v1` protocol. + * + * This hint requires that the compositor supports the `ext-zones-v1` protocol. + * Support for this protocol can be checked via the global + * `SDL_PROP_GLOBAL_VIDEO_WAYLAND_HAS_ZONES_BOOLEAN` property after initializing + * the video subsystem with this hint set. + * + * If the compositor lacks support for the required protocol, this hint does + * nothing. + * + * Zones are arbitrary regions that allow for limited window placement within a + * logical space, and should not be presumed to correlate 1:1 to display output + * coordinates, so care must be taken when enabling this. See + * docs/README-wayland.md and wayland-protocols/ext-zones-v1.xml for more details. + * + * The variable can be set to the following values: + * + * - "0": positioning with ext-zones is disabled. (default) + * - "1": positioning with ext-zones is enabled. + * + * This hint should be set before SDL is initialized. + * + * \since This hint is available since SDL 3.1.6. + */ +#define SDL_HINT_VIDEO_WAYLAND_ENABLE_ZONES "SDL_VIDEO_WAYLAND_ENABLE_ZONES" + /** * A variable controlling whether video mode emulation is enabled under * Wayland. diff --git a/include/SDL3/SDL_video.h b/include/SDL3/SDL_video.h index d727457191aab..a4eeeaee5882e 100644 --- a/include/SDL3/SDL_video.h +++ b/include/SDL3/SDL_video.h @@ -100,6 +100,26 @@ typedef Uint32 SDL_WindowID; */ #define SDL_PROP_GLOBAL_VIDEO_WAYLAND_WL_DISPLAY_POINTER "SDL.video.wayland.wl_display" +/** + * A boolean set to true if the windowing system supports the optional + * ext-zones protocol for positioning windows in Wayland. Requires that the + * `SDL_VIDEO_WAYLAND_ENABLE_ZONES` hint be set to enable. See + * docs/README-wayland.md for more information. + * + * Can be queried after video subsystem initialization. + */ +#define SDL_PROP_GLOBAL_VIDEO_WAYLAND_HAS_ZONES_BOOLEAN "SDL.video.wayland.has_zones" + +/** + * A semicolon-separated list containing the mappings of outputs to zone handles, with + * list items in the form =. + * + * Can be set before the video subsystem is initialized to import a list of existing + * zone handles for outputs, or read after initialization to retrieve the current list of + * zone handles for outputs. + */ +#define SDL_PROP_GLOBAL_VIDEO_WAYLAND_ZONE_MAPPING_STRING "SDL.video.wayland.zone_mapping" + /** * System theme. * @@ -1337,6 +1357,9 @@ extern SDL_DECLSPEC SDL_Window * SDLCALL SDL_CreatePopupWindow(SDL_Window *paren * - `SDL_PROP_WINDOW_CREATE_WAYLAND_WL_SURFACE_POINTER` - the wl_surface * associated with the window, if you want to wrap an existing window. See * [README-wayland](README-wayland) for more information. + * - `SDL_PROP_WINDOW_CREATE_WAYLAND_ZONE_LAYER_NUMBER` - the layer for the + * window when the ext-zones protocol is in use. See + * [README-wayland](README-wayland) for more information. * * These are additional supported properties on Windows: * @@ -1434,6 +1457,7 @@ extern SDL_DECLSPEC SDL_Window * SDLCALL SDL_CreateWindowWithProperties(SDL_Prop #define SDL_PROP_WINDOW_CREATE_WAYLAND_SURFACE_ROLE_CUSTOM_BOOLEAN "SDL.window.create.wayland.surface_role_custom" #define SDL_PROP_WINDOW_CREATE_WAYLAND_CREATE_EGL_WINDOW_BOOLEAN "SDL.window.create.wayland.create_egl_window" #define SDL_PROP_WINDOW_CREATE_WAYLAND_WL_SURFACE_POINTER "SDL.window.create.wayland.wl_surface" +#define SDL_PROP_WINDOW_CREATE_WAYLAND_ZONE_LAYER_NUMBER "SDL.window.create.wayland.zone_layer" #define SDL_PROP_WINDOW_CREATE_WIN32_HWND_POINTER "SDL.window.create.win32.hwnd" #define SDL_PROP_WINDOW_CREATE_WIN32_PIXEL_FORMAT_HWND_POINTER "SDL.window.create.win32.pixel_format_hwnd" #define SDL_PROP_WINDOW_CREATE_X11_WINDOW_NUMBER "SDL.window.create.x11.window" diff --git a/src/video/wayland/SDL_waylandvideo.c b/src/video/wayland/SDL_waylandvideo.c index e266c5f2fd06d..36e289307448a 100644 --- a/src/video/wayland/SDL_waylandvideo.c +++ b/src/video/wayland/SDL_waylandvideo.c @@ -47,6 +47,7 @@ #include "alpha-modifier-v1-client-protocol.h" #include "cursor-shape-v1-client-protocol.h" +#include "ext-zones-v1-client-protocol.h" #include "fractional-scale-v1-client-protocol.h" #include "frog-color-management-v1-client-protocol.h" #include "idle-inhibit-unstable-v1-client-protocol.h" @@ -400,6 +401,7 @@ static void handle_wl_output_done(void *data, struct wl_output *output); // Initialization/Query functions static bool Wayland_VideoInit(SDL_VideoDevice *_this); static bool Wayland_GetDisplayBounds(SDL_VideoDevice *_this, SDL_VideoDisplay *display, SDL_Rect *rect); +static bool Wayland_GetDisplayUsableBounds(SDL_VideoDevice *_this, SDL_VideoDisplay *display, SDL_Rect *rect); static void Wayland_VideoQuit(SDL_VideoDevice *_this); static const char *SDL_WAYLAND_surface_tag = "sdl-window"; @@ -617,6 +619,7 @@ static SDL_VideoDevice *Wayland_CreateDevice(bool require_preferred_protocols) device->VideoInit = Wayland_VideoInit; device->VideoQuit = Wayland_VideoQuit; device->GetDisplayBounds = Wayland_GetDisplayBounds; + device->GetDisplayUsableBounds = Wayland_GetDisplayUsableBounds; device->SuspendScreenSaver = Wayland_SuspendScreenSaver; device->PumpEvents = Wayland_PumpEvents; @@ -661,6 +664,7 @@ static SDL_VideoDevice *Wayland_CreateDevice(bool require_preferred_protocols) device->GetWindowSizeInPixels = Wayland_GetWindowSizeInPixels; device->GetWindowContentScale = Wayland_GetWindowContentScale; device->GetWindowICCProfile = Wayland_GetWindowICCProfile; + device->GetWindowBordersSize = Wayland_GetWindowBorderSize; device->GetDisplayForWindow = Wayland_GetDisplayForWindow; device->DestroyWindow = Wayland_DestroyWindow; device->SetWindowHitTest = Wayland_SetWindowHitTest; @@ -673,6 +677,7 @@ static SDL_VideoDevice *Wayland_CreateDevice(bool require_preferred_protocols) device->SyncWindow = Wayland_SyncWindow; device->SetWindowFocusable = Wayland_SetWindowFocusable; device->ReconfigureWindow = Wayland_ReconfigureWindow; + device->SetWindowAlwaysOnTop = Wayland_SetWindowAlwaysOnTop; #ifdef SDL_USE_LIBDBUS if (SDL_SystemTheme_Init()) @@ -731,7 +736,88 @@ VideoBootStrap Wayland_bootstrap = { false }; -static void handle_xdg_output_logical_position(void *data, struct zxdg_output_v1 *xdg_output, int32_t x, int32_t y) +static void handle_zone_size(void *data, struct ext_zone_v1 *ext_zone_v1, int32_t width, int32_t height) +{ + SDL_DisplayData *disp = (SDL_DisplayData *)data; + + // Negative size values mean that zone creation on this output was denied. + if (width < 0 || height < 0) { + ext_zone_v1_destroy(disp->ext_zone_v1); + disp->ext_zone_v1 = NULL; + return; + } + + disp->zone_width = width ? width : SDL_MAX_SINT32; + disp->zone_height = height ? height : SDL_MAX_SINT32; + + if (disp->display) { + SDL_VideoDisplay *display = SDL_GetVideoDisplay(disp->display); + if (display) { + SDL_SendDisplayEvent(display, SDL_EVENT_DISPLAY_USABLE_BOUNDS_CHANGED, width, height); + } + } +} + +static void handle_zone_handle(void *data, struct ext_zone_v1 *ext_zone_v1, const char *handle) +{ + SDL_DisplayData *disp = (SDL_DisplayData *)data; + SDL_free(disp->zone_handle); + disp->zone_handle = SDL_strdup(handle); +} + +static void handle_zone_done(void *data, struct ext_zone_v1 *ext_zone_v1) +{ + // NOP +} + +static void handle_zone_item_blocked(void *data, struct ext_zone_v1 *ext_zone_v1, struct ext_zone_item_v1 *item) +{ + SDL_WindowData *wind = (SDL_WindowData *)ext_zone_item_v1_get_user_data(item); + wind->entering_new_zone = false; +} + +static void handle_zone_item_entered(void *data, struct ext_zone_v1 *ext_zone_v1, struct ext_zone_item_v1 *item) +{ + if (item) { + SDL_WindowData *wind = (SDL_WindowData *)ext_zone_item_v1_get_user_data(item); + wind->current_ext_zone_v1 = ext_zone_v1; + + if (wind->entering_new_zone) { + Wayland_ApplyZoneItemPosition(wind); + } + + wind->entering_new_zone = false; + } +} + +static void handle_zone_item_left(void *data, struct ext_zone_v1 *ext_zone_v1, struct ext_zone_item_v1 *item) +{ + if (item) { + SDL_WindowData *wind = (SDL_WindowData *)ext_zone_item_v1_get_user_data(item); + wind->current_ext_zone_v1 = NULL; + + if (!wind->entering_new_zone) { + // If leaving a zone without a new join pending, find a new zone to join. + const SDL_DisplayData *display = Wayland_GetDisplayForWindowZone(wind); + + if (display) { + ext_zone_v1_add_item(display->ext_zone_v1, item); + } + } + } +} + +static const struct ext_zone_v1_listener zone_listener = { + handle_zone_size, + handle_zone_handle, + handle_zone_done, + handle_zone_item_blocked, + handle_zone_item_entered, + handle_zone_item_left +}; + +static void handle_xdg_output_logical_position(void *data, struct zxdg_output_v1 *xdg_output, + int32_t x, int32_t y) { SDL_DisplayData *internal = (SDL_DisplayData *)data; @@ -795,6 +881,40 @@ static const struct zxdg_output_v1_listener xdg_output_listener = { handle_xdg_output_description, }; +static void Wayland_SetOutputZone(SDL_DisplayData *disp, bool run_queue) +{ + SDL_PropertiesID props = SDL_GetGlobalProperties(); + const char *zone_map = SDL_GetStringProperty(props, SDL_PROP_GLOBAL_VIDEO_WAYLAND_ZONE_MAPPING_STRING, NULL); + + if (zone_map) { + char *map = SDL_strdup(zone_map); + char *saveptr; + char *entry = SDL_strtok_r(map, ";", &saveptr); + + while (entry) { + char *end = SDL_strchr(entry, '='); + if (end) { + *end = '\0'; + const bool cmp = SDL_strcmp(entry, disp->wl_output_name) == 0; + *end = '='; + + if (cmp) { + disp->ext_zone_v1 = ext_zone_manager_v1_get_zone_from_handle(disp->videodata->ext_zone_manager_v1, end); + break; + } + } + entry = SDL_strtok_r(NULL, "=", &saveptr); + } + } + + if (!disp->ext_zone_v1) { + disp->ext_zone_v1 = ext_zone_manager_v1_get_zone(disp->videodata->ext_zone_manager_v1, disp->output); + } + + ext_zone_v1_set_user_data(disp->ext_zone_v1, disp); + ext_zone_v1_add_listener(disp->ext_zone_v1, &zone_listener, disp); +} + static void AddEmulatedModes(SDL_DisplayData *dispdata, int native_width, int native_height) { struct EmulatedMode @@ -1071,6 +1191,11 @@ static void handle_wl_output_done(void *data, struct wl_output *output) SDL_SetDisplayHDRProperties(dpy, &internal->HDR); + // Get the zone for this output, if the extension is available. + if (video->ext_zone_manager_v1 && !internal->ext_zone_v1) { + Wayland_SetOutputZone(internal, true); + } + if (internal->display == 0) { // First time getting display info, initialize the VideoDisplay if (internal->physical_width_mm >= internal->physical_height_mm) { @@ -1191,12 +1316,17 @@ static void Wayland_free_display(SDL_VideoDisplay *display, bool send_event) } SDL_free(display_data->wl_output_name); + SDL_free(display_data->zone_handle); if (display_data->wp_color_management_output) { Wayland_FreeColorInfoState(display_data->color_info_state); wp_color_management_output_v1_destroy(display_data->wp_color_management_output); } + if (display_data->ext_zone_v1) { + ext_zone_v1_destroy(display_data->ext_zone_v1); + } + if (display_data->xdg_output) { zxdg_output_v1_destroy(display_data->xdg_output); } @@ -1211,6 +1341,49 @@ static void Wayland_free_display(SDL_VideoDisplay *display, bool send_event) } } +static void Wayland_UpdateZoneMapProperty(SDL_VideoData *vid) +{ + int strlen = 0; + char *str = NULL; + + if (!vid->ext_zone_manager_v1) { + return; + } + + for(int i = 0; i < vid->output_count; ++i) { + SDL_DisplayData *d = vid->output_list[i]; + if (!d->wl_output_name || !d->zone_handle) { + continue; + } + + strlen += SDL_strlen(d->wl_output_name); + strlen += SDL_strlen(d->zone_handle); + strlen += 2; + } + + if (strlen) { + str = SDL_calloc(strlen, sizeof(char)); + + for(int i = 0, p = 0; i < vid->output_count; ++i) { + SDL_DisplayData *d = vid->output_list[i]; + if (!d->wl_output_name || !d->zone_handle) { + continue; + } + if (p) { + str[p++] = ';'; + } + + p += SDL_strlcpy(str + p, d->wl_output_name, strlen - p); + str[p++] = '='; + p += SDL_strlcpy(str + p, d->zone_handle, strlen - p); + } + } + + SDL_PropertiesID props = SDL_GetGlobalProperties(); + SDL_SetStringProperty(props, SDL_PROP_GLOBAL_VIDEO_WAYLAND_ZONE_MAPPING_STRING, str); + SDL_free(str); +} + static void Wayland_FinalizeDisplays(SDL_VideoData *vid) { Wayland_SortOutputs(vid); @@ -1220,6 +1393,8 @@ static void Wayland_FinalizeDisplays(SDL_VideoData *vid) SDL_free(d->placeholder.name); SDL_zero(d->placeholder); } + + Wayland_UpdateZoneMapProperty(vid); } static void Wayland_init_xdg_output(SDL_VideoData *d) @@ -1327,6 +1502,10 @@ static void handle_registry_global(void *data, struct wl_registry *registry, uin d->wp_alpha_modifier_v1 = wl_registry_bind(d->registry, id, &wp_alpha_modifier_v1_interface, 1); } else if (SDL_strcmp(interface, "xdg_toplevel_icon_manager_v1") == 0) { d->xdg_toplevel_icon_manager_v1 = wl_registry_bind(d->registry, id, &xdg_toplevel_icon_manager_v1_interface, 1); + } else if (SDL_strcmp(interface, "ext_zone_manager_v1") == 0) { + if (SDL_GetHintBoolean(SDL_HINT_VIDEO_WAYLAND_ENABLE_ZONES, false)) { + d->ext_zone_manager_v1 = wl_registry_bind(d->registry, id, &ext_zone_manager_v1_interface, 1); + } } else if (SDL_strcmp(interface, "frog_color_management_factory_v1") == 0) { d->frog_color_management_factory_v1 = wl_registry_bind(d->registry, id, &frog_color_management_factory_v1_interface, 1); } else if (SDL_strcmp(interface, "wp_color_manager_v1") == 0) { @@ -1462,11 +1641,18 @@ bool Wayland_VideoInit(SDL_VideoDevice *_this) // Second roundtrip to receive all output events. WAYLAND_wl_display_roundtrip(data->display); + // Third roundtrip, if necessary, to retrieve zone info. + if (data->ext_zone_manager_v1) { + WAYLAND_wl_display_roundtrip(data->display); + } + Wayland_FinalizeDisplays(data); Wayland_InitMouse(data); Wayland_InitKeyboard(_this); + SDL_SetBooleanProperty(SDL_GetGlobalProperties(), SDL_PROP_GLOBAL_VIDEO_WAYLAND_HAS_ZONES_BOOLEAN, !!data->ext_zone_manager_v1); + if (data->primary_selection_device_manager) { _this->SetPrimarySelectionText = Wayland_SetPrimarySelectionText; _this->GetPrimarySelectionText = Wayland_GetPrimarySelectionText; @@ -1508,6 +1694,22 @@ static bool Wayland_GetDisplayBounds(SDL_VideoDevice *_this, SDL_VideoDisplay *d return true; } +static bool Wayland_GetDisplayUsableBounds(SDL_VideoDevice *_this, SDL_VideoDisplay *display, SDL_Rect *rect) +{ + SDL_DisplayData *internal = display->internal; + + if (internal->ext_zone_v1) { + rect->x = internal->x; + rect->y = internal->y; + rect->w = internal->zone_width; + rect->h = internal->zone_height; + + return true; + } + + return Wayland_GetDisplayBounds(_this, display, rect); +} + static void Wayland_VideoCleanup(SDL_VideoDevice *_this) { SDL_VideoData *data = _this->internal; @@ -1642,6 +1844,11 @@ static void Wayland_VideoCleanup(SDL_VideoDevice *_this) data->xdg_toplevel_icon_manager_v1 = NULL; } + if (data->ext_zone_manager_v1) { + ext_zone_manager_v1_destroy(data->ext_zone_manager_v1); + data->ext_zone_manager_v1 = NULL; + } + if (data->frog_color_management_factory_v1) { frog_color_management_factory_v1_destroy(data->frog_color_management_factory_v1); data->frog_color_management_factory_v1 = NULL; diff --git a/src/video/wayland/SDL_waylandvideo.h b/src/video/wayland/SDL_waylandvideo.h index 72e9c0718d227..65d45e139eaca 100644 --- a/src/video/wayland/SDL_waylandvideo.h +++ b/src/video/wayland/SDL_waylandvideo.h @@ -87,6 +87,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 ext_zone_manager_v1 *ext_zone_manager_v1; struct xkb_context *xkb_context; @@ -109,16 +110,19 @@ struct SDL_DisplayData struct wl_output *output; struct zxdg_output_v1 *xdg_output; struct wp_color_management_output_v1 *wp_color_management_output; + struct ext_zone_v1 *ext_zone_v1; char *wl_output_name; + char *zone_handle; double scale_factor; uint32_t registry_id; int logical_width, logical_height; int pixel_width, pixel_height; int x, y, refresh, transform; + int zone_width, zone_height; SDL_DisplayOrientation orientation; int physical_width_mm, physical_height_mm; bool has_logical_position, has_logical_size; - bool running_colorspace_event_queue; + //bool running_colorspace_event_queue; SDL_HDROutputProperties HDR; SDL_DisplayID display; SDL_VideoDisplay placeholder; diff --git a/src/video/wayland/SDL_waylandwindow.c b/src/video/wayland/SDL_waylandwindow.c index c400760a46008..445319004aa8b 100644 --- a/src/video/wayland/SDL_waylandwindow.c +++ b/src/video/wayland/SDL_waylandwindow.c @@ -47,6 +47,7 @@ #include "frog-color-management-v1-client-protocol.h" #include "xdg-toplevel-icon-v1-client-protocol.h" #include "color-management-v1-client-protocol.h" +#include "ext-zones-v1-client-protocol.h" #ifdef HAVE_LIBDECOR_H #include @@ -80,8 +81,8 @@ static int PixelToPoint(SDL_Window *window, int pixel) * should be assumed to be in some way that attempts to blend into the surrounding area * (e.g. solid black)." * - * - KDE, as of 5.27, still doesn't do this - * - GNOME prior to 43 didn't do this (older versions are still found in many LTS distros) + * - KDE, as of 6.2, still doesn't do this. + * - GNOME prior to 43 didn't do this (older versions are still found in many LTS distros). * * Default to 'stretch' for now, until things have moved forward enough that the default * can be changed to 'aspect'. @@ -473,6 +474,65 @@ static bool ConfigureWindowGeometry(SDL_Window *window) return true; } +SDL_DisplayData *Wayland_GetDisplayForWindowZone(SDL_WindowData *wind) +{ + SDL_VideoData *vid = wind->waylandData; + + if (!vid->ext_zone_manager_v1) { + return NULL; + } + + SDL_DisplayData *dst_display = NULL; + SDL_DisplayData *overlapped_display = NULL; + SDL_DisplayData *closest_display = NULL; + const int x = wind->sdlwindow->floating.x - wind->borders.left; + const int y = wind->sdlwindow->floating.y - wind->borders.top; + const SDL_Rect window_rect = { x, y, + wind->current.logical_width + wind->borders.left + wind->borders.right, + wind->current.logical_height + wind->borders.top + wind->borders.bottom }; + int closest_distance = SDL_MAX_SINT32; + int overlapped_area = 0; + + /* Find the best zone for window placement: + * - If part of the window overlaps one or more zones, the zone most overlapped by the window is used. + * - If the window is completely out of bounds, the closest zone is used. + */ + for (int i = 0; i < vid->output_count; i++) { + SDL_DisplayData *disp = vid->output_list[i]; + + // Find the zone with the most window overlap. + const SDL_Rect display_rect = { disp->x, disp->y, disp->zone_width, disp->zone_height }; + SDL_Rect overlap; + if (SDL_GetRectUnion(&window_rect, &display_rect, &overlap)) { + const int area = overlap.w * overlap.h; + if (overlapped_area < area) { + overlapped_area = area; + overlapped_display = disp; + } + } + + // Find the closest zone as a fallback if the window is completely out of bounds. + const int dx = disp->x - x; + const int dy = disp->y - y; + const int distance = SDL_abs((dx * dx) + (dy * dy)); + + if (distance < closest_distance) { + closest_distance = distance; + closest_display = disp; + } + } + + if (!dst_display) { + if (overlapped_display) { + dst_display = overlapped_display; + } else { + dst_display = closest_display; + } + } + + return dst_display; +} + static void CommitLibdecorFrame(SDL_Window *window) { #ifdef HAVE_LIBDECOR_H @@ -520,7 +580,24 @@ static struct wl_callback_listener maximized_restored_deadline_listener = { maximized_restored_deadline_handler }; -static void FlushPendingEvents(SDL_Window *window) +static void position_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); + SDL_Window *window = SDL_GetWindowFromID(windowID); + + if (window && window->internal) { + window->internal->position_deadline_count--; + } + + wl_callback_destroy(callback); +} + +static struct wl_callback_listener position_deadline_listener = { + position_deadline_handler +}; + +static void FlushPendingMaximizeRestoreEvents(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; @@ -578,9 +655,15 @@ static void Wayland_move_window(SDL_Window *window) if (wind->last_displayID != displays[i]) { wind->last_displayID = displays[i]; if (wind->shell_surface_type != WAYLAND_SHELL_SURFACE_TYPE_XDG_POPUP) { - SDL_SendWindowEvent(window, SDL_EVENT_WINDOW_MOVED, display->x, display->y); + if (!wind->ext_zone_item_v1) { + SDL_SendWindowEvent(window, SDL_EVENT_WINDOW_MOVED, display->x, display->y); + } SDL_SendWindowEvent(window, SDL_EVENT_WINDOW_DISPLAY_CHANGED, wind->last_displayID, 0); } + + if (display->ext_zone_v1 && wind->ext_zone_item_v1 && !wind->current_ext_zone_v1) { + ext_zone_v1_add_item(display->ext_zone_v1, wind->ext_zone_item_v1); + } } break; } @@ -691,10 +774,18 @@ static void surface_frame_done(void *data, struct wl_callback *cb, uint32_t time } wind->drop_interactive_resizes = false; + wind->pending_commit_flags = WAYLAND_PENDING_COMMIT_NONE; if (wind->shell_surface_status == WAYLAND_SHELL_SURFACE_STATUS_WAITING_FOR_FRAME) { wind->shell_surface_status = WAYLAND_SHELL_SURFACE_STATUS_SHOWN; + wind->adjust_initial_position = false; + + if (wind->ext_zone_item_v1 && wind->current_ext_zone_v1) { + // If the window is in a zone, send the initial coordinates after mapping. + SDL_SendWindowEvent(wind->sdlwindow, SDL_EVENT_WINDOW_MOVED, wind->last_configure.x, wind->last_configure.y); + } + // If any child windows are waiting on this window to be shown, show them now for (SDL_Window *w = wind->sdlwindow->first_child; w; w = w->next_sibling) { if (w->internal->shell_surface_status == WAYLAND_SHELL_SURFACE_STATUS_SHOW_PENDING) { @@ -1576,6 +1667,10 @@ void Wayland_RemoveOutputFromWindow(SDL_WindowData *window, SDL_DisplayData *dis } } + if (window->current_ext_zone_v1 == display_data->ext_zone_v1) { + window->current_ext_zone_v1 = NULL; + } + if (window->num_outputs == 0) { SDL_free(window->outputs); window->outputs = NULL; @@ -1836,6 +1931,101 @@ static struct zxdg_exported_v2_listener exported_v2_listener = { exported_handle_handler }; +void Wayland_ApplyZoneItemPosition(SDL_WindowData *wind) +{ + if (wind->current_ext_zone_v1) { + SDL_DisplayData *dst_display = ext_zone_v1_get_user_data(wind->current_ext_zone_v1); + + // Part of the window must be within zone bounds (a size of 0 means that a zone is infinite in that direction). + const int min_x = -(wind->sdlwindow->w + wind->borders.left + wind->borders.right - 1); + const int min_y = -(wind->sdlwindow->h + wind->borders.top + wind->borders.bottom - 1); + int x = wind->requested.x - wind->borders.left - dst_display->x; + int y = wind->requested.y - wind->borders.top - dst_display->y; + x = SDL_clamp(x, min_x, dst_display->zone_width - 1); + y = SDL_clamp(y, min_y, dst_display->zone_height - 1); + + ext_zone_item_v1_set_position(wind->ext_zone_item_v1, x, y); + ++wind->position_deadline_count; + struct wl_callback *cb = wl_display_sync(wind->waylandData->display); + wl_callback_add_listener(cb, &position_deadline_listener, (void *)((uintptr_t)wind->sdlwindow->id)); + wind->pending_commit_flags = WAYLAND_PENDING_COMMIT_POSITION; + } +} + +static bool Wayland_PositionWindowInZone(SDL_WindowData *wind) +{ + SDL_DisplayData *dst_display = Wayland_GetDisplayForWindowZone(wind); + if (dst_display) { + struct ext_zone_v1 *dst_zone = dst_display->ext_zone_v1; + + /* If the new position is in the current zone, just change the position, + * otherwise, select a new zone, and the position will be set upon entry. + */ + if (wind->current_ext_zone_v1 != dst_zone) { + wind->entering_new_zone = true; + ext_zone_v1_add_item(dst_zone, wind->ext_zone_item_v1); + } else { + Wayland_ApplyZoneItemPosition(wind); + } + + return true; + } + + return SDL_SetError("cannot position window: no zone available"); +} + +static void handle_ext_zone_item_v1_frame_extents(void *data, struct ext_zone_item_v1 *ext_zone_item_v1, + int32_t top, int32_t bottom, int32_t left, int32_t right) +{ + SDL_WindowData *wind = (SDL_WindowData *)data; + + wind->borders.top = top; + wind->borders.bottom = bottom; + wind->borders.left = left; + wind->borders.right = right; + wind->borders_changed = true; +} + +static void handle_ext_zone_item_v1_position(void *data, struct ext_zone_item_v1 *item, int32_t x, int32_t y) +{ + if (!item) { + return; + } + + SDL_WindowData *wind = (SDL_WindowData *)data; + SDL_DisplayData *disp = (SDL_DisplayData *)ext_zone_v1_get_user_data(wind->current_ext_zone_v1); + + const int w_x = disp->x + x + wind->borders.left; + const int w_y = disp->y + y + wind->borders.top; + wind->last_configure.x = w_x; + wind->last_configure.y = w_y; + + if (wind->shell_surface_status == WAYLAND_SHELL_SURFACE_STATUS_SHOWN) { + SDL_SendWindowEvent(wind->sdlwindow, SDL_EVENT_WINDOW_MOVED, w_x, w_y); + } else if (wind->shell_surface_status == WAYLAND_SHELL_SURFACE_STATUS_WAITING_FOR_FRAME) { + /* If the window is not yet mapped, adjust the initial client area position + * to compensate for the borders. + */ + if (wind->adjust_initial_position && wind->borders_changed) { + Wayland_PositionWindowInZone(wind); + } + } + + wind->have_configure_position = true; + wind->borders_changed = false; +} + +static void handle_ext_zone_item_v1_position_failed(void *data, struct ext_zone_item_v1 *item) +{ + // NOP +} + +static const struct ext_zone_item_v1_listener zone_item_listener = { + handle_ext_zone_item_v1_frame_extents, + handle_ext_zone_item_v1_position, + handle_ext_zone_item_v1_position_failed +}; + void Wayland_ShowWindow(SDL_VideoDevice *_this, SDL_Window *window) { SDL_VideoData *c = _this->internal; @@ -2007,6 +2197,19 @@ void Wayland_ShowWindow(SDL_VideoDevice *_this, SDL_Window *window) data->xdg_toplevel_icon_v1); } + if (c->ext_zone_manager_v1) { + data->ext_zone_item_v1 = ext_zone_manager_v1_get_zone_item(c->ext_zone_manager_v1, data->shell_surface.xdg.toplevel.xdg_toplevel); + ext_zone_item_v1_set_user_data(data->ext_zone_item_v1, data); + ext_zone_item_v1_add_listener(data->ext_zone_item_v1, &zone_item_listener, data); + + if (!window->undefined_x && !window->undefined_y) { + data->adjust_initial_position = true; + data->requested.x = window->x; + data->requested.y = window->y; + Wayland_PositionWindowInZone(data); + } + } + SDL_SetPointerProperty(props, SDL_PROP_WINDOW_WAYLAND_XDG_TOPLEVEL_POINTER, data->shell_surface.xdg.toplevel.xdg_toplevel); } } @@ -2160,6 +2363,12 @@ void Wayland_HideWindow(SDL_VideoDevice *_this, SDL_Window *window) wind->shell_surface_status = WAYLAND_SHELL_SURFACE_STATUS_HIDDEN; + if (wind->ext_zone_item_v1) { + ext_zone_item_v1_destroy(wind->ext_zone_item_v1); + wind->ext_zone_item_v1 = NULL; + wind->current_ext_zone_v1 = NULL; + } + if (wind->server_decoration) { zxdg_toplevel_decoration_v1_destroy(wind->server_decoration); wind->server_decoration = NULL; @@ -2333,7 +2542,7 @@ SDL_FullscreenResult Wayland_SetWindowFullscreen(SDL_VideoDevice *_this, SDL_Win } wind->drop_fullscreen_requests = true; - FlushPendingEvents(window); + FlushPendingMaximizeRestoreEvents(window); wind->drop_fullscreen_requests = false; // Nothing to do if the window is not fullscreen, and this isn't an explicit enter request. @@ -2492,8 +2701,11 @@ 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 preserve any pending size or position data. + if (wind->pending_commit_flags) { + wl_surface_commit(wind->surface); + wind->pending_commit_flags = WAYLAND_PENDING_COMMIT_NONE; + } libdecor_frame_set_maximized(wind->shell_surface.libdecor.frame); ++wind->maximized_restored_deadline_count; @@ -2507,7 +2719,10 @@ void Wayland_MaximizeWindow(SDL_VideoDevice *_this, SDL_Window *window) } // Commit to preserve any pending size data. - wl_surface_commit(wind->surface); + if (wind->pending_commit_flags) { + wl_surface_commit(wind->surface); + wind->pending_commit_flags = WAYLAND_PENDING_COMMIT_NONE; + } xdg_toplevel_set_maximized(wind->shell_surface.xdg.toplevel.xdg_toplevel); ++wind->maximized_restored_deadline_count; @@ -2656,6 +2871,11 @@ bool Wayland_CreateWindow(SDL_VideoDevice *_this, SDL_Window *window, SDL_Proper data->scale_factor = 1.0; + data->zone_layer = SDL_GetNumberProperty(create_props, SDL_PROP_WINDOW_CREATE_WAYLAND_ZONE_LAYER_NUMBER, 1); + if (data->zone_layer <= 0) { + data->zone_layer = 1; + } + if (SDL_WINDOW_IS_POPUP(window)) { data->scale_to_display = window->parent->internal->scale_to_display; data->scale_factor = window->parent->internal->scale_factor; @@ -2828,6 +3048,7 @@ void Wayland_SetWindowMaximumSize(SDL_VideoDevice *_this, SDL_Window *window) bool Wayland_SetWindowPosition(SDL_VideoDevice *_this, SDL_Window *window) { + SDL_VideoData *vid = _this->internal; SDL_WindowData *wind = window->internal; // Only popup windows can be positioned relative to the parent. @@ -2840,11 +3061,10 @@ bool Wayland_SetWindowPosition(SDL_VideoDevice *_this, SDL_Window *window) RepositionPopup(window, false); return true; } else if (wind->shell_surface_type == WAYLAND_SHELL_SURFACE_TYPE_LIBDECOR || wind->shell_surface_type == WAYLAND_SHELL_SURFACE_TYPE_XDG_TOPLEVEL) { - /* Catch up on any pending state before attempting to change the fullscreen window - * display via a set fullscreen call to make sure the window doesn't have a pending - * leave fullscreen event that it might override. + /* Catch up on any pending state before attempting to change the position + * doesn't have pending state that doing so might override. */ - FlushPendingEvents(window); + FlushPendingMaximizeRestoreEvents(window); if (wind->is_fullscreen) { SDL_VideoDisplay *display = SDL_GetVideoDisplayForFullscreenWindow(window); @@ -2854,9 +3074,20 @@ bool Wayland_SetWindowPosition(SDL_VideoDevice *_this, SDL_Window *window) return true; } + } else if (vid->ext_zone_manager_v1) { + if (wind->floating) { + wind->adjust_initial_position = wind->shell_surface_status == WAYLAND_SHELL_SURFACE_STATUS_WAITING_FOR_FRAME; + wind->requested.x = window->pending.x; + wind->requested.y = window->pending.y; + return Wayland_PositionWindowInZone(wind); + } + + // Can't move the window in its current state. + window->last_position_pending = false; + return true; } } - return SDL_SetError("wayland cannot position non-popup windows"); + return SDL_SetError("cannot position non-popup windows; compositor lacks support for the ext-zone-manager-v1 protocol"); } void Wayland_SetWindowSize(SDL_VideoDevice *_this, SDL_Window *window) @@ -2871,7 +3102,7 @@ void Wayland_SetWindowSize(SDL_VideoDevice *_this, SDL_Window *window) * Calling this on a custom surface is informative, so the size must * always be passed through. */ - FlushPendingEvents(window); + FlushPendingMaximizeRestoreEvents(window); // Maximized and fullscreen windows don't get resized. if (!(window->flags & (SDL_WINDOW_FULLSCREEN | SDL_WINDOW_MAXIMIZED)) || @@ -2915,6 +3146,22 @@ float Wayland_GetWindowContentScale(SDL_VideoDevice *_this, SDL_Window *window) return 1.0f; } +bool Wayland_GetWindowBorderSize(SDL_VideoDevice *_this, SDL_Window *window, int *top, int *left, int *bottom, int *right) +{ + SDL_WindowData *wind = window->internal; + + if (wind->ext_zone_item_v1) { + *top = wind->borders.top; + *bottom = wind->borders.bottom; + *left = wind->borders.left; + *right = wind->borders.right; + + return true; + } + + return SDL_SetError("window border sizes require the ext_zones_v1 protocol"); +} + SDL_DisplayID Wayland_GetDisplayForWindow(SDL_VideoDevice *_this, SDL_Window *window) { SDL_WindowData *wind = window->internal; @@ -3084,8 +3331,13 @@ bool Wayland_SyncWindow(SDL_VideoDevice *_this, SDL_Window *window) SDL_WindowData *wind = window->internal; do { + // Viewport commits are only needed when entering a fixed-size state, and are unnecessary here. + if (wind->pending_commit_flags & WAYLAND_PENDING_COMMIT_POSITION) { + wl_surface_commit(wind->surface); + wind->pending_commit_flags = WAYLAND_PENDING_COMMIT_NONE; + } WAYLAND_wl_display_roundtrip(_this->internal->display); - } while (wind->fullscreen_deadline_count || wind->maximized_restored_deadline_count); + } while (wind->fullscreen_deadline_count || wind->maximized_restored_deadline_count || wind->position_deadline_count); return true; } @@ -3111,6 +3363,18 @@ bool Wayland_SetWindowFocusable(SDL_VideoDevice *_this, SDL_Window *window, bool return SDL_SetError("wayland: focus can only be toggled on popup menu windows"); } +void Wayland_SetWindowAlwaysOnTop(SDL_VideoDevice *_this, SDL_Window *window, bool on_top) +{ + SDL_WindowData *wind = window->internal; + + if (wind->ext_zone_item_v1) { + const Sint32 item_layer = on_top ? wind->zone_layer : 0; + ext_zone_item_v1_set_layer(wind->ext_zone_item_v1, item_layer); + } else { + SDL_SetError("wayland: setting always on top requires that the ext_zones_v1 protocol is supported and enabled"); + } +} + void Wayland_ShowWindowSystemMenu(SDL_Window *window, int x, int y) { SDL_WindowData *wind = window->internal; diff --git a/src/video/wayland/SDL_waylandwindow.h b/src/video/wayland/SDL_waylandwindow.h index 50f8c528d0d6b..c683f24c796c4 100644 --- a/src/video/wayland/SDL_waylandwindow.h +++ b/src/video/wayland/SDL_waylandwindow.h @@ -114,11 +114,14 @@ struct SDL_WindowData struct xdg_dialog_v1 *xdg_dialog_v1; struct wp_alpha_modifier_surface_v1 *wp_alpha_modifier_surface_v1; struct xdg_toplevel_icon_v1 *xdg_toplevel_icon_v1; + struct ext_zone_item_v1 *ext_zone_item_v1; struct frog_color_managed_surface *frog_color_managed_surface; struct wp_color_management_surface_feedback_v1 *wp_color_management_surface_feedback; struct Wayland_ColorInfoState *color_info_state; + struct ext_zone_v1 *current_ext_zone_v1; + SDL_AtomicInt swap_interval_ready; SDL_DisplayData **outputs; @@ -151,6 +154,9 @@ struct SDL_WindowData // The size of the window in pixels, when using screen space scaling. int pixel_width; int pixel_height; + + int x; + int y; } requested; // The current size of the window and drawable backing store. @@ -170,6 +176,9 @@ struct SDL_WindowData { int width; int height; + + int x; + int y; } last_configure; // System enforced window size limits. @@ -193,9 +202,26 @@ struct SDL_WindowData bool active; } text_input_props; + struct + { + int top; + int bottom; + int left; + int right; + } borders; + + enum + { + WAYLAND_PENDING_COMMIT_NONE = 0x00, + WAYLAND_PENDING_COMMIT_VIEWPORT = 0x01, + WAYLAND_PENDING_COMMIT_POSITION = 0x02 + } pending_commit_flags; + SDL_DisplayID last_displayID; int fullscreen_deadline_count; int maximized_restored_deadline_count; + int position_deadline_count; + int zone_layer; Uint64 last_focus_event_time_ns; int icc_fd; Uint32 icc_size; @@ -213,6 +239,10 @@ struct SDL_WindowData bool scale_to_display; bool reparenting_required; bool double_buffer; + bool entering_new_zone; + bool borders_changed; + bool adjust_initial_position; + bool have_configure_position; SDL_HitTestResult hit_test_result; @@ -249,6 +279,8 @@ extern bool Wayland_SetWindowIcon(SDL_VideoDevice *_this, SDL_Window *window, SD extern bool Wayland_SetWindowFocusable(SDL_VideoDevice *_this, SDL_Window *window, bool focusable); extern float Wayland_GetWindowContentScale(SDL_VideoDevice *_this, SDL_Window *window); extern void *Wayland_GetWindowICCProfile(SDL_VideoDevice *_this, SDL_Window *window, size_t *size); +extern bool Wayland_GetWindowBorderSize(SDL_VideoDevice *_this, SDL_Window *window, int *top, int *left, int *bottom, int *right); +extern void Wayland_SetWindowAlwaysOnTop(SDL_VideoDevice *_this, SDL_Window *window, bool on_top); extern bool Wayland_SetWindowHitTest(SDL_Window *window, bool enabled); extern bool Wayland_FlashWindow(SDL_VideoDevice *_this, SDL_Window *window, SDL_FlashOperation operation); @@ -256,5 +288,7 @@ extern bool Wayland_SyncWindow(SDL_VideoDevice *_this, SDL_Window *window); extern bool Wayland_ReconfigureWindow(SDL_VideoDevice *_this, SDL_Window *window, SDL_WindowFlags flags); extern void Wayland_RemoveOutputFromWindow(SDL_WindowData *window, SDL_DisplayData *display_data); +extern SDL_DisplayData *Wayland_GetDisplayForWindowZone(SDL_WindowData *wind); +extern void Wayland_ApplyZoneItemPosition(SDL_WindowData *wind); #endif // SDL_waylandwindow_h_ diff --git a/wayland-protocols/ext-zones-v1.xml b/wayland-protocols/ext-zones-v1.xml new file mode 100644 index 0000000000000..b5f55abcd3ca5 --- /dev/null +++ b/wayland-protocols/ext-zones-v1.xml @@ -0,0 +1,499 @@ + + + + + Copyright © 2023-2025 Matthias Klumpp + Copyright © 2024-2025 Frank Praznik + Copyright © 2024 Victoria Brekenfeld + + 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 provides a way for clients to create and add toplevel windows + to "zones". + + A zone is a isolated environment with its own coordinate space where + clients can add and arrange windows that logically belong and relate to + each other. + It provides means for, among other things, requesting that windows are + placed at specific coordinates within the zone coordinate space. + See the description of "ext_zone_v1" for more details. + + This document adheres to RFC 2119 when using words like "must", + "should", "may", etc. + + 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 'ext_zone_manager' interface defines base requests for obtaining and + managing zones for a client. + + + + + This has no effect other than to destroy the ext_zone_manager object. + + + + + + Create a new positionable zone item from an 'xdg_toplevel'. + The resulting wrapper object can then be used to position the + toplevel window in a zone. + + + + + + + + Create a new zone. While the zone object exists, the compositor + must consider it "used" and keep track of it. + + A zone is represented by a string 'handle'. + + The compositor must keep zone handles valid while any client is + using the corresponding zone and has items associated with it. + The compositor may always give a client the same zone for a given + output, and remember its position and size for the client, but + clients should not rely on this behavior. + + A client can request a zone to be placed on a specific + output by passing a wl_output as 'output'. If a valid output + is set, the compositor should place the zone on that output. + If NULL is passed, the compositor decides the output. + + The compositor should provide the biggest reasonable zone space + for the client, governed by its own policy. + + If the compositor wants to deny zone creation (e.g. on a specific + output), the returned zone must be "invalid". A zone is invalid + if it has a negative size, in which case the client is forbidden + to place items in it. + + + + + + + + Create a new zone object using the zone's handle. + For the returned zone, the same rules as described in + 'get_zone' apply. + + This requests returns a reference to an existing or remembered zone + that is represented by 'handle'. + The zone may potentially have been created by a different client. + + This allows cooperating clients to share the same coordinate space, + but prevents other clients from e.g. layering their items on top of + items of other clients. + + If the zone handle was invalid or unknown, a new zone must + be created and returned instead, following the rules outlined + in 'get_zone' and assuming no output preference. + + Every new zone object created by this request emits its initial event + sequence, including the 'handle' event, which must return a different + handle from the one passed to this request in case the existing zone + could not be joined. + + + + + + + + + The zone item object is an opaque descriptor for a positionable + element, such as a toplevel window. + It currently can only be created from an 'xdg_toplevel' via the + 'get_zone_item' request on a 'ext_zone_manager'. + + + + + Destroys the zone item. This request may be sent at any time by the + client. + By destroying the object, the respective item surface remains at its + last position, but its association with its zone is lost. + This will also cause it to loose any other attached state, like its + layer index. + + + + + + The 'frame_extents' event describes the current extents of the frame + bordering the item's content area. + + This event is sent immediately after the item joins a zone, or if + the item frame extents have been changed by other means (e.g. toggled + by a client request, or compositor involvement). The dimensions are in + the same coordinate space as the item's zone (the surface coordinate + space). + + If the item has no associated frame, the event should still be sent, + but extents must be set to zero. + + This event can only be emitted if the item is currently associated + with a zone. + + + + + + + + + + Request a preferred position (x, y) for the specified item + surface to be placed at, relative to its associated zone. + This state is double-buffered and is applied on the next + wl_surface.commit of the surface represented by 'item'. + + X and Y coordinates are relative to the zone this item is associated + with, and must not be larger than the dimensions set by the zone size. + They may be smaller than zero, if the item's top-left edge is to be + placed beyond the zone's top-left sides, but clients should expect the + compositor to more aggressively sanitize the coordinate values in that + case. + If a coordinate exceeds the zone's maximum bounds, the compositor must + sanitize it to more appropriate values (e.g. by clamping the values to + the maximum size). + For infinite zones, the client may pick any coordinate. + + Compositors implementing this protocol should try to place an item + at the requested coordinates relative to the item's zone, unless doing + so is not allowed by compositor policy (because e.g. the user has set + custom rules for the surface represented by the respective item, the + surface overlaps with a protected shell component, session management + has loaded previous surface positions or the placement request would + send the item out of bounds). + + Clients should be aware that their placement preferences might not + always be followed and must be prepared to handle the case where the + item is placed at a different position by the compositor. + + Once an item has been mapped, a change to its preferred placement can + still be requested and should be applied, but must not be followed + by the compositor while the user is interacting with the affected item + surface (e.g. clicking & dragging within the window, or resizing it). + + After a call to this request, a 'position' event must be emitted with the + item's new actual position. + If the current item has no zone associated with it, a 'position_failed' + event must be emitted. + If the compositor did not move the item at all, not even with sanitized + values, a 'position_failed' event must be emitted as well. + + + + + + + + This event notifies the client of the current position (x, y) of + the item relative to its zone. + Coordinates are relative to the zone this item belongs to, and only + valid within it. + Negative coordinates are possible, if the user has moved an item + surface beyone the zone's top-left boundary. + + This event is sent in response to a 'set_position' request, + or if the item position has been changed by other means + (e.g. user interaction or compositor involvement). + + This event can only be emitted if the item is currently associated + with a zone. + + + + + + + + The compositor was unable to set the position of this item entirely, + and could not even find sanitized coordinates to place the item at + instead. + + This event will also be emitted if 'set_position' was called while the + item had no zone associated with it. + + + + + + Request a preferred permanent Z position for this item relative to + other item surfaces in the same zone this item is associated with. + This state is double-buffered and is applied on the next + wl_surface.commit of the surface represented by 'item'. + + This function associates a "layer index" with the item, with all + item surfaces assumed to be positioned at a layer with index 0 by + default. + Item surfaces that are positioned in a layer with a higher index + permanently float above items with a lower index. Items with a + lower layer index sink below items with a higher index. + + Upon user interaction, sunken item surfaces are not raised on top of + items in a layer with an index higher than theirs, and floating items + do not sink below items in a lower layer, even if these items are + selected. + + Items with the same layer index are subject to compositor policy, + which usually means they will obey user interaction and raise above or + sink below each other depending on which surface is currently activated. + + The layer index only affects the stacking order of items within the + same zone. The compositor is allowed to move items of one zone + (or no zone) above or below any item in a different zone, regardless + of their zone-specific layer index. + + Compositors without support for stacking windows or with other conflicting + policy may ignore this request. + + + + + + + + An 'ext_zone' describes a display area provided by the compositor in + which a client can place windows and move them around. + + A zone's area could for example correspond to the space usable for + placing windows on a specific output (space without panels or other + restricted elements) or it could be an area of the output the compositor + has specifically chosen for a client to place its surfaces in. + + Windows are added to a zone as 'ext_zone_item' objects. + + All item surface position coordinates (x, y) are relative to the selected + zone. + They are using the 'size' of the respective zone as coordinate system, + with (0, 0) being in the top left corner. + + If a zone item is moved out of the top/left boundaries of the zone by + user interaction, its coordinates must become negative, relative to the + zones top-left coordinate origin. A client may position an item at negative + coordinates. + + The compositor must ensure that any item positioned by the client is + visible and accessible to the user, and is not moved into invisible space + outside of a zone. + Positioning requests may be rejected or altered by the compositor, depending + on its policy. + + The absolute position of the zone within the compositor's coordinate space + is opaque to the client and the compositor may move the entire zone without + the client noticing it. A zone may also be arbitrarily resized, in which + case the respective 'size' event must be emitted again to notify the client. + + Zone items are allowed to have a "layer" attribute, to keep them permanently + above or below other surfaces in the same zone. + Using layers, clients can overlay their own windows permanently, but not + the ones of other clients not sharing the same zone. + + A zone is always tied to an output and does not extend beyond it. + + A zone may be "invalid". An invalid zone is created with a negative + 'size' and must not be used for item arrangement. + + Upon creation the compositor must emit 'size' and 'handle' events for the + newly created 'ext_zone', followed by 'done'. + + + + + Using this request a client can tell the compositor that it is not + going to use the 'ext_zone' object anymore. + The zone itself must only be destroyed if no other client + is currently using it, so this request may only destroy the object + reference owned by the client. + + + + + + The 'size' event describes the size of this zone. + + It is a rectangle with its origin in the top-left corner, using + the surface coordinate space (device pixels divided by the scaling + factor of the output this zone is attached to). + + If a width or height value is zero, the zone is infinite + in that direction. + + If the width and height values are negative, the zone is considered + "invalid" and must not be used. + A size event declaring the zone invalid may only be emitted immediately + after the zone was created. + A zone must not become invalid at a later time by sending a negative + 'size' after the zone has been established. + + The 'size' event is sent immediately after creating an 'ext_zone_v1', + and whenever the size of the zone changes. A zone size can change at + any time, for any reason, for example due to output size or scaling + changes, or by compositor policy. + + Upon subsequent emissions of 'size' after 'ext_zone' has already + been created, the 'done' event does not have to be sent again. + + + + + + + + The handle event provides the unique handle of this zone. + The handle may be shared with any client, which then can use it to + join this client's zone by calling + 'ext_zone_manager.get_zone'. + + This event must only be emitted once after the zone was created. + If this zone is invalid, the handle must be an empty string. + + + + + + + This event is sent after all other properties (size, handle) of an + 'ext_zone' have been sent. + + This allows changes to the ext_zone properties to be seen as + atomic, even if they happen via multiple events. + + + + + + + + + + Make 'item' a member of this zone. + This state is double-buffered and is applied on the next + 'wl_surface.commit' of the surface represented by 'item'. + + This request associates an item with this zone. + If this request is called on an item that already has a zone + association with a different zone, the item should leave its old zone + (with 'item_left' being emitted on its old zone) and will instead + be associated with this zone. + + Upon receiving this request and if the target zone is allowed for 'item', + a compositor must emit 'item_entered' to confirm the zone association. + It must even emit this event if the item was already associated with this + zone before. + + The compositor must move the surface represented by 'item' into the + boundary of this zone upon receiving this request and accepting it + (either by extending the zone size, or by moving the item surface). + + If the compositor does not allow the item to switch zone associations, + and wants it to remain in its previous zone, it must emit + 'item_blocked' instead. + Compositors might want to prevent zone associations if they + perform specialized window management (e.g. autotiling) that would + make clients moving items between certain zones undesirable. + + Once the 'item' is added to its zone, the compositor must first send + a 'frame_extents' event on the item, followed by an initial 'position' + event with the item's current position. + The compositor must then send 'position' events when the position + of the item in its zone is changed, for as long as the item is + associated with a zone. + + If the zone is invalid, an 'invalid' error must be raised and the item + must not be associated with the invalid zone. + + + + + + + Remove 'item' as a member of this zone. + This state is double-buffered and is applied on the next + 'wl_surface.commit' of the surface represented by 'item'. + + This request removes the item from this zone explicitly, + making the client unable to retrieve coordinates again. + + Upon receiving this request, the compositor should not change the + item surface position on screen, and must emit 'item_left' to confirm + the item's removal. It must even emit this event if the + item was never associated with this zone. + + + + + + + This event notifies the client that an item was prevented from + joining this zone. + + It is emitted as a response to 'add_item' if the compositor did not + allow the item to join this particular zone. + + + + + + + This event notifies the client of an item joining this zone. + + It is emitted as a response to 'add_item' or if the compositor + automatically had the item surface (re)join an existing zone. + + + + + + + This event notifies the client of an item leaving this zone, and + therefore the client being unable to retrieve its coordinates in + future. + If the client still wishes to adjust the item surface coordinates, it + may associate the item with a zone again by calling 'add_item'. + + This event is emitted for example if the user moved an item surface out + of a smaller zone's boundaries, or onto a different screen where the + previous zone can not expand to. It is also emitted in response to + explicitly removing an item via 'remove_item'. + + + + + + +