diff --git a/include/SDL3/SDL_video.h b/include/SDL3/SDL_video.h index 76877b9bd0362..143ac14aee914 100644 --- a/include/SDL3/SDL_video.h +++ b/include/SDL3/SDL_video.h @@ -1362,11 +1362,20 @@ extern SDL_DECLSPEC SDL_Window * SDLCALL SDL_CreatePopupWindow(SDL_Window *paren * the compositor is told the frame is the window * (`xdg_surface.set_window_geometry`), so it places, snaps and tiles the * frame; the surface grows by the margins around the size it configures; - * and only the frame and a band of up to 8 units around it take input, so - * the rest of the shadow passes clicks through. Maximized, tiled or + * only the frame and a band around it take input (see + * `SDL_PROP_WINDOW_CREATE_WAYLAND_FRAME_INPUT_MARGIN_NUMBER`), so the rest + * of the shadow passes clicks through; and the opaque region of a window + * that isn't `SDL_WINDOW_TRANSPARENT` is the frame. Maximized, tiled or * fullscreen, the margins are zero (see * `SDL_PROP_WINDOW_WAYLAND_FRAME_INSET_LEFT_NUMBER`). xdg-shell windows - * only, not libdecor's. + * only, not libdecor's. The window keeps them if SDL has to make it again + * (as for an OpenGL renderer on a window made without `SDL_WINDOW_OPENGL`). + * - `SDL_PROP_WINDOW_CREATE_WAYLAND_FRAME_INPUT_MARGIN_NUMBER` - the width, in + * window coordinates, of the band around the frame that still takes input + * while the window has frame insets, where the application's hit test (see + * SDL_SetWindowHitTest()) can answer the edges it resizes from. Each side's + * band is at most that side's inset. Defaults to 8; 0 leaves only the + * frame taking input. * * These are additional supported properties on Windows: * @@ -1460,6 +1469,7 @@ extern SDL_DECLSPEC SDL_Window * SDLCALL SDL_CreateWindowWithProperties(SDL_Prop #define SDL_PROP_WINDOW_CREATE_WAYLAND_FRAME_INSET_TOP_NUMBER "SDL.window.create.wayland.frame_inset.top" #define SDL_PROP_WINDOW_CREATE_WAYLAND_FRAME_INSET_RIGHT_NUMBER "SDL.window.create.wayland.frame_inset.right" #define SDL_PROP_WINDOW_CREATE_WAYLAND_FRAME_INSET_BOTTOM_NUMBER "SDL.window.create.wayland.frame_inset.bottom" +#define SDL_PROP_WINDOW_CREATE_WAYLAND_FRAME_INPUT_MARGIN_NUMBER "SDL.window.create.wayland.frame_input_margin" #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" @@ -1624,8 +1634,10 @@ extern SDL_DECLSPEC SDL_Window * SDLCALL SDL_GetWindowParent(SDL_Window *window) * `SDL_PROP_WINDOW_WAYLAND_FRAME_INSET_RIGHT_NUMBER` and * `SDL_PROP_WINDOW_WAYLAND_FRAME_INSET_BOTTOM_NUMBER`: the frame insets in * effect now, in window coordinates — those asked for at creation while the - * window floats, zero while it is maximized, tiled or fullscreen. They - * change with the window's size; read them on `SDL_EVENT_WINDOW_RESIZED`. + * window floats, zero while it is maximized, tiled or fullscreen. Set from + * creation on: until the window is first shown, the ones it will float + * with. They change with the window's size; read them on + * `SDL_EVENT_WINDOW_RESIZED`. * * On X11: * diff --git a/src/video/wayland/SDL_waylandwindow.c b/src/video/wayland/SDL_waylandwindow.c index 4eabc97a7e541..e0a7313b18912 100644 --- a/src/video/wayland/SDL_waylandwindow.c +++ b/src/video/wayland/SDL_waylandwindow.c @@ -145,9 +145,10 @@ static void GetBufferSize(SDL_Window *window, int *width, int *height) } } -/* The band around a frame with insets that still takes input, in window coordinates: where an - * application's hit test answers the edges it resizes from, as a decorated window's border - * would, while the rest of its shadow passes clicks through. +/* The band around a frame with insets that still takes input by default, in window coordinates: + * where an application's hit test answers the edges it resizes from, as a decorated window's + * border would, while the rest of its shadow passes clicks through + * (SDL_PROP_WINDOW_CREATE_WAYLAND_FRAME_INPUT_MARGIN_NUMBER). */ #define WAYLAND_FRAME_INPUT_MARGIN 8 @@ -169,8 +170,38 @@ static bool HasFrameInsets(SDL_WindowData *wind) return wind->frame_insets.left || wind->frame_insets.top || wind->frame_insets.right || wind->frame_insets.bottom; } +/* The frame insets and input margin asked for at creation are kept in the window's properties + * as well, which outlive the window data: SDL_RecreateWindow makes the window again with no + * creation properties, and the window keeps its insets. + */ +#define WAYLAND_KEPT_FRAME_INSET_LEFT "SDL.internal.wayland.frame_inset.left" +#define WAYLAND_KEPT_FRAME_INSET_TOP "SDL.internal.wayland.frame_inset.top" +#define WAYLAND_KEPT_FRAME_INSET_RIGHT "SDL.internal.wayland.frame_inset.right" +#define WAYLAND_KEPT_FRAME_INSET_BOTTOM "SDL.internal.wayland.frame_inset.bottom" +#define WAYLAND_KEPT_FRAME_INPUT_MARGIN "SDL.internal.wayland.frame_input_margin" + +static int GetKeptCreateNumber(SDL_PropertiesID create_props, SDL_PropertiesID window_props, const char *create_name, const char *kept_name, int default_value) +{ + const Sint64 kept = SDL_GetNumberProperty(window_props, kept_name, default_value); + const int value = SDL_max((int)SDL_GetNumberProperty(create_props, create_name, kept), 0); + + SDL_SetNumberProperty(window_props, kept_name, value); + return value; +} + +// Publishes the frame insets in effect (SDL_PROP_WINDOW_WAYLAND_FRAME_INSET_*). +static void PublishFrameInsets(SDL_Window *window, int left, int top, int right, int bottom) +{ + const SDL_PropertiesID props = SDL_GetWindowProperties(window); + SDL_SetNumberProperty(props, SDL_PROP_WINDOW_WAYLAND_FRAME_INSET_LEFT_NUMBER, left); + SDL_SetNumberProperty(props, SDL_PROP_WINDOW_WAYLAND_FRAME_INSET_TOP_NUMBER, top); + SDL_SetNumberProperty(props, SDL_PROP_WINDOW_WAYLAND_FRAME_INSET_RIGHT_NUMBER, right); + SDL_SetNumberProperty(props, SDL_PROP_WINDOW_WAYLAND_FRAME_INSET_BOTTOM_NUMBER, bottom); +} + /* Tells the compositor the frame is the window, takes input on the frame and a band around it - * only, and publishes the insets in effect. Called whenever the size or the insets change. + * only, and publishes the insets in effect. Called whenever the size or the insets change. A + * hidden window has no xdg_surface to frame; the configure that shows it applies them. */ static void ApplyFrameInsets(SDL_Window *window, int left, int top, int right, int bottom) { @@ -178,15 +209,16 @@ static void ApplyFrameInsets(SDL_Window *window, int left, int top, int right, i const int frame_w = SDL_max(wind->current.logical_width - left - right, 1); const int frame_h = SDL_max(wind->current.logical_height - top - bottom, 1); - if (wind->shell_surface.xdg.surface) { - xdg_surface_set_window_geometry(wind->shell_surface.xdg.surface, left, top, frame_w, frame_h); + if (!wind->shell_surface.xdg.surface) { + return; } + xdg_surface_set_window_geometry(wind->shell_surface.xdg.surface, left, top, frame_w, frame_h); if (left || top || right || bottom) { - const int ml = SDL_min(left, WAYLAND_FRAME_INPUT_MARGIN); - const int mt = SDL_min(top, WAYLAND_FRAME_INPUT_MARGIN); - const int mr = SDL_min(right, WAYLAND_FRAME_INPUT_MARGIN); - const int mb = SDL_min(bottom, WAYLAND_FRAME_INPUT_MARGIN); + const int ml = SDL_min(left, wind->frame_input_margin); + const int mt = SDL_min(top, wind->frame_input_margin); + const int mr = SDL_min(right, wind->frame_input_margin); + const int mb = SDL_min(bottom, wind->frame_input_margin); struct wl_region *region = wl_compositor_create_region(wind->waylandData->compositor); wl_region_add(region, left - ml, top - mt, frame_w + ml + mr, frame_h + mt + mb); wl_surface_set_input_region(wind->surface, region); @@ -200,11 +232,7 @@ static void ApplyFrameInsets(SDL_Window *window, int left, int top, int right, i wind->applied_frame_insets.right = right; wind->applied_frame_insets.bottom = bottom; - const SDL_PropertiesID props = SDL_GetWindowProperties(window); - SDL_SetNumberProperty(props, SDL_PROP_WINDOW_WAYLAND_FRAME_INSET_LEFT_NUMBER, left); - SDL_SetNumberProperty(props, SDL_PROP_WINDOW_WAYLAND_FRAME_INSET_TOP_NUMBER, top); - SDL_SetNumberProperty(props, SDL_PROP_WINDOW_WAYLAND_FRAME_INSET_RIGHT_NUMBER, right); - SDL_SetNumberProperty(props, SDL_PROP_WINDOW_WAYLAND_FRAME_INSET_BOTTOM_NUMBER, bottom); + PublishFrameInsets(window, left, top, right, bottom); } static void SetMinMaxDimensions(SDL_Window *window) @@ -382,9 +410,13 @@ static void SetSurfaceOpaqueRegion(SDL_WindowData *wind, bool is_opaque) SDL_VideoData *viddata = wind->waylandData; if (is_opaque) { + // Only the frame: any frame insets around it hold a shadow, which is never opaque. + const int left = wind->applied_frame_insets.left; + const int top = wind->applied_frame_insets.top; + const int width = SDL_max(wind->current.logical_width - left - wind->applied_frame_insets.right, 0); + const int height = SDL_max(wind->current.logical_height - top - wind->applied_frame_insets.bottom, 0); 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_region_add(region, left, top, width, height); wl_surface_set_opaque_region(wind->surface, region); wl_region_destroy(region); } else { @@ -970,11 +1002,15 @@ static void handle_xdg_toplevel_configure(void *data, width = window->floating.w; height = window->floating.h; - // Clamp the window to the toplevel bounds, if any are set. + /* Clamp the window to the toplevel bounds, if any are set. They bound the + * frame, so the surface may be larger by its insets. + */ if (wind->shell_surface_status == WAYLAND_SHELL_SURFACE_STATUS_WAITING_FOR_CONFIGURE && wind->toplevel_bounds.width && wind->toplevel_bounds.height) { - width = SDL_min(wind->toplevel_bounds.width, width); - height = SDL_min(wind->toplevel_bounds.height, height); + int il, it, ir, ib; + GetFrameInsets(wind, floating, &il, &it, &ir, &ib); + width = SDL_min(wind->toplevel_bounds.width + il + ir, width); + height = SDL_min(wind->toplevel_bounds.height + it + ib, height); } } else { width = window->windowed.w; @@ -2396,6 +2432,11 @@ void Wayland_HideWindow(SDL_VideoDevice *_this, SDL_Window *window) wl_surface_attach(wind->surface, NULL, 0, 0); wl_surface_commit(wind->surface); + /* The window geometry went with the xdg_surface: forget the insets applied, so the next + * configure frames the window again even at the same size. + */ + SDL_zero(wind->applied_frame_insets); + SDL_zero(wind->shell_surface); wind->show_hide_sync_required = true; struct wl_callback *cb = wl_display_sync(_this->internal->display); @@ -2846,12 +2887,16 @@ bool Wayland_CreateWindow(SDL_VideoDevice *_this, SDL_Window *window, SDL_Proper // Default to all capabilities data->wm_caps = WAYLAND_WM_CAPS_ALL; - // Margins outside the frame for a shadow the application draws; toplevels only. + /* Margins outside the frame for a shadow the application draws; toplevels only. A window + * made again by SDL_RecreateWindow has no creation properties, and keeps those it had. + */ if (!SDL_WINDOW_IS_POPUP(window) && !custom_surface_role) { - data->frame_insets.left = SDL_max((int)SDL_GetNumberProperty(create_props, SDL_PROP_WINDOW_CREATE_WAYLAND_FRAME_INSET_LEFT_NUMBER, 0), 0); - data->frame_insets.top = SDL_max((int)SDL_GetNumberProperty(create_props, SDL_PROP_WINDOW_CREATE_WAYLAND_FRAME_INSET_TOP_NUMBER, 0), 0); - data->frame_insets.right = SDL_max((int)SDL_GetNumberProperty(create_props, SDL_PROP_WINDOW_CREATE_WAYLAND_FRAME_INSET_RIGHT_NUMBER, 0), 0); - data->frame_insets.bottom = SDL_max((int)SDL_GetNumberProperty(create_props, SDL_PROP_WINDOW_CREATE_WAYLAND_FRAME_INSET_BOTTOM_NUMBER, 0), 0); + const SDL_PropertiesID window_props = SDL_GetWindowProperties(window); + data->frame_insets.left = GetKeptCreateNumber(create_props, window_props, SDL_PROP_WINDOW_CREATE_WAYLAND_FRAME_INSET_LEFT_NUMBER, WAYLAND_KEPT_FRAME_INSET_LEFT, 0); + data->frame_insets.top = GetKeptCreateNumber(create_props, window_props, SDL_PROP_WINDOW_CREATE_WAYLAND_FRAME_INSET_TOP_NUMBER, WAYLAND_KEPT_FRAME_INSET_TOP, 0); + data->frame_insets.right = GetKeptCreateNumber(create_props, window_props, SDL_PROP_WINDOW_CREATE_WAYLAND_FRAME_INSET_RIGHT_NUMBER, WAYLAND_KEPT_FRAME_INSET_RIGHT, 0); + data->frame_insets.bottom = GetKeptCreateNumber(create_props, window_props, SDL_PROP_WINDOW_CREATE_WAYLAND_FRAME_INSET_BOTTOM_NUMBER, WAYLAND_KEPT_FRAME_INSET_BOTTOM, 0); + data->frame_input_margin = GetKeptCreateNumber(create_props, window_props, SDL_PROP_WINDOW_CREATE_WAYLAND_FRAME_INPUT_MARGIN_NUMBER, WAYLAND_KEPT_FRAME_INPUT_MARGIN, WAYLAND_FRAME_INPUT_MARGIN); } data->scale_factor = 1.0; @@ -3007,6 +3052,15 @@ bool Wayland_CreateWindow(SDL_VideoDevice *_this, SDL_Window *window, SDL_Proper SDL_SetPointerProperty(props, SDL_PROP_WINDOW_WAYLAND_VIEWPORT_POINTER, data->viewport); SDL_SetPointerProperty(props, SDL_PROP_WINDOW_WAYLAND_EGL_WINDOW_POINTER, data->egl_window); + /* Until the compositor first configures the window, the insets in effect are those it will + * float with, so an application can lay itself out before showing it. + */ + if (HasFrameInsets(data)) { + int il, it, ir, ib; + GetFrameInsets(data, true, &il, &it, &ir, &ib); + PublishFrameInsets(window, il, it, ir, ib); + } + data->hit_test_result = SDL_HITTEST_NORMAL; return true; diff --git a/src/video/wayland/SDL_waylandwindow.h b/src/video/wayland/SDL_waylandwindow.h index 025122e1e2a0a..e788be36055a4 100644 --- a/src/video/wayland/SDL_waylandwindow.h +++ b/src/video/wayland/SDL_waylandwindow.h @@ -209,6 +209,8 @@ struct SDL_WindowData { int left, top, right, bottom; } frame_insets, applied_frame_insets; + // The band round the frame that takes input (SDL_PROP_WINDOW_CREATE_WAYLAND_FRAME_INPUT_MARGIN_NUMBER). + int frame_input_margin; bool floating; bool suspended; diff --git a/test/testautomation_fizzy_wayland.c b/test/testautomation_fizzy_wayland.c index a16d567c59f59..c02bcb9cd738d 100644 --- a/test/testautomation_fizzy_wayland.c +++ b/test/testautomation_fizzy_wayland.c @@ -5,10 +5,13 @@ * decorations": SDL_PROP_WINDOW_CREATE_WAYLAND_FRAME_INSET_*_NUMBER give the * margins of an xdg-shell toplevel's surface outside its frame. While the * window floats, the window geometry is the frame, the input region is the - * frame and a band of at most 8 units round it, the min/max sizes are the - * frame's and popups anchor to the frame; maximized or fullscreen the insets - * are zero. The insets in effect are published as - * SDL_PROP_WINDOW_WAYLAND_FRAME_INSET_*_NUMBER. + * frame and a band round it (8 units, or + * SDL_PROP_WINDOW_CREATE_WAYLAND_FRAME_INPUT_MARGIN_NUMBER), an opaque + * window's opaque region is the frame, the min/max sizes are the frame's and + * popups anchor to the frame; maximized or fullscreen the insets are zero. The + * insets in effect are published as SDL_PROP_WINDOW_WAYLAND_FRAME_INSET_*_NUMBER + * from creation on. A window SDL makes again (SDL_RecreateWindow) or shows + * again keeps all of it. * * What SDL asks of the compositor is read from libwayland's own trace of the * requests it sends (WAYLAND_DEBUG=client), not from SDL's state: each test @@ -17,7 +20,9 @@ * objects (found by id through wl_proxy_get_id). * * The tests run only on the Wayland video driver, and skip elsewhere. They need - * a GPU driver to present frames with (lavapipe will do), so the windows map. + * a GPU driver to present frames with (lavapipe will do), so the windows map, + * and wayland_frameInsetsRecreated needs OpenGL ES through EGL (Mesa's + * llvmpipe will do). */ #include #include @@ -45,6 +50,9 @@ #define SDL_PROP_WINDOW_WAYLAND_FRAME_INSET_RIGHT_NUMBER "SDL.window.wayland.frame_inset.right" #define SDL_PROP_WINDOW_WAYLAND_FRAME_INSET_BOTTOM_NUMBER "SDL.window.wayland.frame_inset.bottom" #endif +#ifndef SDL_PROP_WINDOW_CREATE_WAYLAND_FRAME_INPUT_MARGIN_NUMBER +#define SDL_PROP_WINDOW_CREATE_WAYLAND_FRAME_INPUT_MARGIN_NUMBER "SDL.window.create.wayland.frame_input_margin" +#endif #ifdef FIZZY_WAYLAND_TRACE @@ -83,6 +91,12 @@ typedef struct bool input_subtracted; TraceRect input; /* the first rectangle added */ + bool opaque_set; + bool opaque_none; /* wl_surface.set_opaque_region(nil) */ + int opaque_rects; + bool opaque_subtracted; + TraceRect opaque; + bool min_set, max_set; int min_w, min_h, max_w, max_h; /* xdg_toplevel.set_{min,max}_size */ @@ -241,11 +255,19 @@ static int TraceStart(void) return TEST_COMPLETED; } -/* A line in the trace, which a test's checkpoint names. */ +/* A line in the trace, which a test's checkpoint names, and on the real stderr, + * so a test that hangs while the trace is on shows how far it got. */ static void TraceMark(const char *mark) { + char line[160]; + int length; + fprintf(stderr, "\nfizzy-mark: %s\n", mark); fflush(stderr); + length = SDL_snprintf(line, sizeof(line), "fizzy-mark: %s\n", mark); + if (saved_stderr >= 0 && length > 0) { + (void)!write(saved_stderr, line, (size_t)SDL_min(length, (int)sizeof(line) - 1)); + } } static const char *TraceFindMark(const char *mark) @@ -374,18 +396,20 @@ static TraceRegion *FindRegion(TraceRegion *regions, int *count, Uint32 id, bool /** * Reads the requests sent (and the toplevel configure events received) on a - * window's objects from the start of the trace up to `mark`, keeping the last - * of each. + * window's objects from the mark `from` (the start of the trace if NULL) up + * to `mark`, keeping the last of each. A window whose objects were made again + * (shown again, or recreated) is read from a mark set before that, as + * libwayland reuses the ids of destroyed objects. */ -static bool ParseTrace(const char *mark, const TraceIds *ids, TraceState *state) +static bool ParseTrace(const char *from, const char *mark, const TraceIds *ids, TraceState *state) { TraceRegion regions[MAX_REGIONS]; int region_count = 0; const char *end = TraceFindMark(mark); - const char *p = trace_text; + const char *p = from ? TraceFindMark(from) : trace_text; SDL_zerop(state); - if (!end) { + if (!end || !p || p > end) { return false; } @@ -492,6 +516,18 @@ static bool ParseTrace(const char *mark, const TraceIds *ids, TraceState *state) state->input_subtracted = region->subtracted; state->input = region->first; } + } else if (id == ids->surface && SDL_strcmp(iface, "wl_surface") == 0 && SDL_strcmp(name, "set_opaque_region") == 0) { + const Uint32 region_id = ParseObjectArg(args); + state->opaque_set = true; + state->opaque_none = region_id == 0; + state->opaque_rects = 0; + state->opaque_subtracted = false; + if (region_id) { + const TraceRegion *region = FindRegion(regions, ®ion_count, region_id, false); + state->opaque_rects = region->rects; + state->opaque_subtracted = region->subtracted; + state->opaque = region->first; + } } else if (id == ids->xdg_surface && SDL_strcmp(iface, "xdg_surface") == 0 && SDL_strcmp(name, "set_window_geometry") == 0) { int v[4]; if (ParseInts(args, v, 4) == 4) { @@ -559,7 +595,7 @@ static void LogTrace(const TraceIds *ids) if (ids->positioner) { SDL_snprintf(objects[count++], sizeof(objects[0]), "xdg_positioner#%" SDL_PRIu32 ".", ids->positioner); } - SDL_snprintf(objects[count++], sizeof(objects[0]), "wl_surface#%" SDL_PRIu32 ".set_input_region", ids->surface); + SDL_snprintf(objects[count++], sizeof(objects[0]), "wl_surface#%" SDL_PRIu32 ".set_", ids->surface); while (*p) { const char *eol = SDL_strchr(p, '\n'); @@ -656,16 +692,12 @@ typedef struct bool claimed; } TestWindow; -/* Makes a resizable, borderless window with the insets, as fizzy's Linux - * backend does, shows it and presents into it. Nothing is asserted here, as - * stderr is the trace: the caller checks after TraceStop. */ -static void CreateTestWindow(TestWindow *tw) +/* The properties of a resizable, borderless window with the insets, as fizzy's + * Linux backend makes, and an input margin unless it is negative. */ +static SDL_PropertiesID TestWindowProperties(int input_margin) { SDL_PropertiesID props = SDL_CreateProperties(); - SDL_zerop(tw); - tw->device = SDL_CreateGPUDevice(SDL_GPU_SHADERFORMAT_SPIRV | SDL_GPU_SHADERFORMAT_MSL | SDL_GPU_SHADERFORMAT_DXIL, false, NULL); - SDL_SetStringProperty(props, SDL_PROP_WINDOW_CREATE_TITLE_STRING, "testautomation_fizzy_wayland"); SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_WIDTH_NUMBER, SURFACE_W); SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_HEIGHT_NUMBER, SURFACE_H); @@ -675,6 +707,21 @@ static void CreateTestWindow(TestWindow *tw) SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_WAYLAND_FRAME_INSET_TOP_NUMBER, INSET_TOP); SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_WAYLAND_FRAME_INSET_RIGHT_NUMBER, INSET_RIGHT); SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_WAYLAND_FRAME_INSET_BOTTOM_NUMBER, INSET_BOTTOM); + if (input_margin >= 0) { + SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_WAYLAND_FRAME_INPUT_MARGIN_NUMBER, input_margin); + } + return props; +} + +/* Makes the test window (with an input margin unless it is negative), shows + * it and presents into it. Nothing is asserted here, as stderr is the trace: + * the caller checks after TraceStop. */ +static void CreateTestWindowWithMargin(TestWindow *tw, int input_margin) +{ + SDL_PropertiesID props = TestWindowProperties(input_margin); + + SDL_zerop(tw); + tw->device = SDL_CreateGPUDevice(SDL_GPU_SHADERFORMAT_SPIRV | SDL_GPU_SHADERFORMAT_MSL | SDL_GPU_SHADERFORMAT_DXIL, false, NULL); tw->window = SDL_CreateWindowWithProperties(props); SDL_DestroyProperties(props); @@ -686,17 +733,31 @@ static void CreateTestWindow(TestWindow *tw) } } -static void DestroyTestWindow(TestWindow *tw) +static void CreateTestWindow(TestWindow *tw) +{ + CreateTestWindowWithMargin(tw, -1); +} + +/* Releases the window from the GPU device and destroys the device, keeping the + * window. */ +static void ReleaseTestWindowDevice(TestWindow *tw) { if (tw->claimed) { SDL_WaitForGPUIdle(tw->device); SDL_ReleaseWindowFromGPUDevice(tw->device, tw->window); - } - if (tw->window) { - SDL_DestroyWindow(tw->window); + tw->claimed = false; } if (tw->device) { SDL_DestroyGPUDevice(tw->device); + tw->device = NULL; + } +} + +static void DestroyTestWindow(TestWindow *tw) +{ + ReleaseTestWindowDevice(tw); + if (tw->window) { + SDL_DestroyWindow(tw->window); } SDL_zerop(tw); } @@ -725,16 +786,31 @@ static void CheckPublishedInsets(const Checkpoint *checkpoint, const int expecte } } -/* While floating: the window geometry is the frame, inside the insets, and the - * input region the frame and a band of at most 8 units round it. */ -static void CheckFloatingFrame(const Checkpoint *checkpoint, const TraceState *state) +/* An opaque window's opaque region: one rectangle, the one given. */ +static void CheckOpaqueRegion(const Checkpoint *checkpoint, const TraceState *state, int x, int y, int w, int h, const char *what) +{ + SDLTest_AssertCheck(state->opaque_set && !state->opaque_none && state->opaque_rects == 1 && !state->opaque_subtracted, + "%s: validate that the opaque region set is one rectangle, got: %s, %d rectangles%s", + checkpoint->mark, !state->opaque_set ? "never set" : state->opaque_none ? "none (nil)" : "a region", + state->opaque_rects, state->opaque_subtracted ? ", with subtractions" : ""); + if (state->opaque_set && state->opaque_rects == 1) { + SDLTest_AssertCheck(state->opaque.x == x && state->opaque.y == y && state->opaque.w == w && state->opaque.h == h, + "%s: validate the opaque region is %s, expected: (%d, %d, %d, %d), got: (%d, %d, %d, %d)", + checkpoint->mark, what, x, y, w, h, state->opaque.x, state->opaque.y, state->opaque.w, state->opaque.h); + } +} + +/* While floating: the window geometry is the frame, inside the insets, the + * input region the frame and a band of at most `band` units round it, and the + * opaque region (the test window is opaque) the frame, not its shadow. */ +static void CheckFloatingFrameWithBand(const Checkpoint *checkpoint, const TraceState *state, int band) { const int frame_w = checkpoint->w - INSET_LEFT - INSET_RIGHT; const int frame_h = checkpoint->h - INSET_TOP - INSET_BOTTOM; - const int band_l = SDL_min(INSET_LEFT, INPUT_BAND); - const int band_t = SDL_min(INSET_TOP, INPUT_BAND); - const int band_r = SDL_min(INSET_RIGHT, INPUT_BAND); - const int band_b = SDL_min(INSET_BOTTOM, INPUT_BAND); + const int band_l = SDL_min(INSET_LEFT, band); + const int band_t = SDL_min(INSET_TOP, band); + const int band_r = SDL_min(INSET_RIGHT, band); + const int band_b = SDL_min(INSET_BOTTOM, band); CheckPublishedInsets(checkpoint, insets_asked); @@ -755,8 +831,15 @@ static void CheckFloatingFrame(const Checkpoint *checkpoint, const TraceState *s const int w = frame_w + band_l + band_r, h = frame_h + band_t + band_b; SDLTest_AssertCheck(state->input.x == x && state->input.y == y && state->input.w == w && state->input.h == h, "%s: validate the input region is the frame and a band of at most %d, expected: (%d, %d, %d, %d), got: (%d, %d, %d, %d)", - checkpoint->mark, INPUT_BAND, x, y, w, h, state->input.x, state->input.y, state->input.w, state->input.h); + checkpoint->mark, band, x, y, w, h, state->input.x, state->input.y, state->input.w, state->input.h); } + + CheckOpaqueRegion(checkpoint, state, INSET_LEFT, INSET_TOP, frame_w, frame_h, "the frame, without the shadow"); +} + +static void CheckFloatingFrame(const Checkpoint *checkpoint, const TraceState *state) +{ + CheckFloatingFrameWithBand(checkpoint, state, INPUT_BAND); } /* Maximized or fullscreen: no insets, the window geometry the whole surface @@ -781,11 +864,19 @@ static void CheckFilledFrame(const Checkpoint *checkpoint, const TraceState *sta SDLTest_AssertCheck(state->input_set && state->input_infinite, "%s: validate that the input region is the whole surface (nil), got: %s", checkpoint->mark, !state->input_set ? "never set" : state->input_infinite ? "nil" : "a region"); + CheckOpaqueRegion(checkpoint, state, 0, 0, checkpoint->w, checkpoint->h, "the whole surface"); } static bool ParseAt(const Checkpoint *checkpoint, const TraceIds *ids, TraceState *state) { - return SDLTest_AssertCheck(ParseTrace(checkpoint->mark, ids, state), "Validate that the trace holds the mark '%s'", checkpoint->mark); + return SDLTest_AssertCheck(ParseTrace(NULL, checkpoint->mark, ids, state), "Validate that the trace holds the mark '%s'", checkpoint->mark); +} + +/* Reads the trace from the mark `from`: for a window whose objects were made + * again after it. */ +static bool ParseSince(const char *from, const Checkpoint *checkpoint, const TraceIds *ids, TraceState *state) +{ + return SDLTest_AssertCheck(ParseTrace(from, checkpoint->mark, ids, state), "Validate that the trace holds the marks '%s' and then '%s'", from, checkpoint->mark); } /* Maximizes (or fullscreens) a floating window and restores it. */ @@ -857,6 +948,44 @@ static int frameInsetsFillAndRestore(bool fullscreen) return TEST_COMPLETED; } +/* A floating window made with the input margin given: the input region is the + * frame and a band of min(inset, margin) on each side. */ +static int frameInsetsInputMargin(int margin) +{ + TestWindow tw; + TraceIds ids; + Checkpoint shown = { 0 }; + TraceState state; + bool made, device, claimed; + int result = TraceStart(); + + if (result != TEST_COMPLETED) { + return result < 0 ? TEST_COMPLETED : result; + } + SDL_zero(state); + + CreateTestWindowWithMargin(&tw, margin); + made = tw.window != NULL; + device = tw.device != NULL; + claimed = tw.claimed; + SDL_zero(ids); + if (claimed) { + GetIds(tw.window, &ids); + TakeCheckpoint(tw.window, "shown", &shown); + } + DestroyTestWindow(&tw); + TraceStop(); + + if (CheckTestWindow(made, device, claimed, &ids)) { + if (ParseAt(&shown, &ids, &state)) { + CheckFloatingFrameWithBand(&shown, &state, margin); + } + } + LogTrace(&ids); + TraceFree(); + return TEST_COMPLETED; +} + #endif /* FIZZY_WAYLAND_TRACE */ /* Test case functions */ @@ -1044,6 +1173,241 @@ static int SDLCALL wayland_frameInsetsPopup(void *arg) #endif } +/** + * The insets are published from creation on: a window made hidden has them + * before it is ever shown, the ones it will float with. + */ +static int SDLCALL wayland_frameInsetsBeforeShow(void *arg) +{ +#ifdef FIZZY_WAYLAND_TRACE + SDL_PropertiesID props; + SDL_Window *window; + Checkpoint created = { 0 }; + int result = TraceStart(); + + if (result != TEST_COMPLETED) { + return result < 0 ? TEST_COMPLETED : result; + } + + props = TestWindowProperties(-1); + SDL_SetBooleanProperty(props, SDL_PROP_WINDOW_CREATE_HIDDEN_BOOLEAN, true); + window = SDL_CreateWindowWithProperties(props); + SDL_DestroyProperties(props); + if (window) { + TakeCheckpoint(window, "created hidden", &created); + SDL_DestroyWindow(window); + } + TraceStop(); + + if (SDLTest_AssertCheck(window != NULL, "Validate that the window was made")) { + SDLTest_AssertCheck((created.flags & SDL_WINDOW_HIDDEN) != 0, "%s: validate that the window is hidden", created.mark); + CheckPublishedInsets(&created, insets_asked); + } + TraceFree(); + return TEST_COMPLETED; +#else + SDLTest_Log("Wayland only, skipping test"); + return TEST_SKIPPED; +#endif +} + +/** + * A window SDL makes again keeps its insets. SDL_RecreateWindow destroys the + * native window and makes it again with no creation properties; attaching an + * OpenGL renderer to a shown window made without SDL_WINDOW_OPENGL does so. + * After that the new objects' window geometry, input region and opaque region + * are the frame's again. + */ +static int SDLCALL wayland_frameInsetsRecreated(void *arg) +{ +#ifdef FIZZY_WAYLAND_TRACE + TestWindow tw; + SDL_Renderer *renderer = NULL; + TraceIds ids, recreated_ids; + Checkpoint shown = { 0 }, recreated = { 0 }; + TraceState state; + bool made, device, claimed; + char renderer_error[256] = "none"; + int result = TraceStart(); + + if (result != TEST_COMPLETED) { + return result < 0 ? TEST_COMPLETED : result; + } + SDL_zero(state); + + CreateTestWindow(&tw); + made = tw.window != NULL; + device = tw.device != NULL; + claimed = tw.claimed; + SDL_zero(ids); + SDL_zero(recreated_ids); + if (claimed) { + int i; + + GetIds(tw.window, &ids); + TakeCheckpoint(tw.window, "shown", &shown); + + ReleaseTestWindowDevice(&tw); + TraceMark("recreating"); + renderer = SDL_CreateRenderer(tw.window, "opengles2"); + if (!renderer) { + SDL_strlcpy(renderer_error, SDL_GetError(), sizeof(renderer_error)); + } + if (renderer) { + for (i = 0; i < 10; ++i) { + SDL_Event event; + while (SDL_PollEvent(&event)) { + } + SDL_SetRenderDrawColor(renderer, 51, 76, 102, 255); + SDL_RenderClear(renderer); + SDL_RenderPresent(renderer); + SDL_Delay(5); + } + SDL_SyncWindow(tw.window); + GetIds(tw.window, &recreated_ids); + TakeCheckpoint(tw.window, "recreated", &recreated); + SDL_DestroyRenderer(renderer); + } + } + DestroyTestWindow(&tw); + TraceStop(); + + if (CheckTestWindow(made, device, claimed, &ids)) { + if (ParseAt(&shown, &ids, &state)) { + CheckFloatingFrame(&shown, &state); + } + + SDLTest_AssertCheck(renderer != NULL, "Validate that an OpenGL ES renderer was made for the window, got error: %s", renderer_error); + if (renderer) { + SDLTest_AssertCheck((recreated.flags & SDL_WINDOW_OPENGL) && !(recreated.flags & SDL_WINDOW_VULKAN), + "%s: validate that the window was made again for OpenGL, got flags: 0x%" SDL_PRIx64, + recreated.mark, (Uint64)recreated.flags); + SDLTest_AssertCheck(recreated_ids.xdg_surface != 0 && recreated_ids.toplevel != 0, + "%s: validate that the window made again is an xdg-shell toplevel, got xdg_surface %" SDL_PRIu32 ", xdg_toplevel %" SDL_PRIu32, + recreated.mark, recreated_ids.xdg_surface, recreated_ids.toplevel); + SDLTest_AssertCheck(recreated.w == SURFACE_W && recreated.h == SURFACE_H, + "%s: validate the surface is the size made, expected: %dx%d, got: %dx%d", + recreated.mark, SURFACE_W, SURFACE_H, recreated.w, recreated.h); + if (ParseSince("recreating", &recreated, &recreated_ids, &state)) { + CheckFloatingFrame(&recreated, &state); + } + } + } + LogTrace(&recreated_ids); + TraceFree(); + return TEST_COMPLETED; +#else + SDLTest_Log("Wayland only, skipping test"); + return TEST_SKIPPED; +#endif +} + +/** + * A window hidden and shown again gets a new xdg_surface, whose window + * geometry must be the frame again although the size and the insets have not + * changed. + */ +static int SDLCALL wayland_frameInsetsShownAgain(void *arg) +{ +#ifdef FIZZY_WAYLAND_TRACE + TestWindow tw; + TraceIds ids, again_ids; + Checkpoint shown = { 0 }, again = { 0 }; + TraceState state; + bool made, device, claimed, reclaimed = false; + int result = TraceStart(); + + if (result != TEST_COMPLETED) { + return result < 0 ? TEST_COMPLETED : result; + } + SDL_zero(state); + + CreateTestWindow(&tw); + made = tw.window != NULL; + device = tw.device != NULL; + claimed = tw.claimed; + SDL_zero(ids); + SDL_zero(again_ids); + if (claimed) { + SDL_Event event; + + GetIds(tw.window, &ids); + TakeCheckpoint(tw.window, "shown", &shown); + + /* Out of the GPU device while hidden: a swapchain waiting on frames of an unmapped + * surface can block for good. */ + SDL_WaitForGPUIdle(tw.device); + SDL_ReleaseWindowFromGPUDevice(tw.device, tw.window); + tw.claimed = false; + + SDL_HideWindow(tw.window); + SDL_SyncWindow(tw.window); + while (SDL_PollEvent(&event)) { + } + TraceMark("hidden"); + + SDL_ShowWindow(tw.window); + TraceMark("showing again"); + tw.claimed = SDL_ClaimWindowForGPUDevice(tw.device, tw.window); + reclaimed = tw.claimed; + if (reclaimed) { + Settle(tw.device, tw.window); + } + GetIds(tw.window, &again_ids); + TakeCheckpoint(tw.window, "shown again", &again); + } + DestroyTestWindow(&tw); + TraceStop(); + + if (CheckTestWindow(made, device, claimed, &ids)) { + if (ParseAt(&shown, &ids, &state)) { + CheckFloatingFrame(&shown, &state); + } + SDLTest_AssertCheck(reclaimed, "%s: validate that the window was claimed for the GPU device again", again.mark); + SDLTest_AssertCheck((again.flags & SDL_WINDOW_HIDDEN) == 0, "%s: validate that the window is shown", again.mark); + SDLTest_AssertCheck(again_ids.xdg_surface != 0 && again_ids.toplevel != 0, + "%s: validate that the window is an xdg-shell toplevel again, got xdg_surface %" SDL_PRIu32 ", xdg_toplevel %" SDL_PRIu32, + again.mark, again_ids.xdg_surface, again_ids.toplevel); + if (again_ids.xdg_surface && ParseSince("hidden", &again, &again_ids, &state)) { + CheckFloatingFrame(&again, &state); + } + } + LogTrace(&again_ids); + TraceFree(); + return TEST_COMPLETED; +#else + SDLTest_Log("Wayland only, skipping test"); + return TEST_SKIPPED; +#endif +} + +/** + * SDL_PROP_WINDOW_CREATE_WAYLAND_FRAME_INPUT_MARGIN_NUMBER widens the input + * band past the default 8, up to each inset. + */ +static int SDLCALL wayland_frameInsetsInputMarginWide(void *arg) +{ +#ifdef FIZZY_WAYLAND_TRACE + return frameInsetsInputMargin(12); +#else + SDLTest_Log("Wayland only, skipping test"); + return TEST_SKIPPED; +#endif +} + +/** + * An input margin of 0: only the frame takes input. + */ +static int SDLCALL wayland_frameInsetsInputMarginNone(void *arg) +{ +#ifdef FIZZY_WAYLAND_TRACE + return frameInsetsInputMargin(0); +#else + SDLTest_Log("Wayland only, skipping test"); + return TEST_SKIPPED; +#endif +} + /* ================= Test References ================== */ static const SDLTest_TestCaseReference waylandTestFrameInsetsFloating = { @@ -1062,11 +1426,36 @@ static const SDLTest_TestCaseReference waylandTestFrameInsetsPopup = { wayland_frameInsetsPopup, "wayland_frameInsetsPopup", "A popup is anchored to its parent's frame", TEST_ENABLED }; +static const SDLTest_TestCaseReference waylandTestFrameInsetsBeforeShow = { + wayland_frameInsetsBeforeShow, "wayland_frameInsetsBeforeShow", "A window's insets are published before it is shown", TEST_ENABLED +}; + +static const SDLTest_TestCaseReference waylandTestFrameInsetsRecreated = { + wayland_frameInsetsRecreated, "wayland_frameInsetsRecreated", "A window SDL makes again (SDL_RecreateWindow) keeps its insets", TEST_ENABLED +}; + +static const SDLTest_TestCaseReference waylandTestFrameInsetsShownAgain = { + wayland_frameInsetsShownAgain, "wayland_frameInsetsShownAgain", "A window hidden and shown again is framed again", TEST_ENABLED +}; + +static const SDLTest_TestCaseReference waylandTestFrameInsetsInputMarginWide = { + wayland_frameInsetsInputMarginWide, "wayland_frameInsetsInputMarginWide", "The input band follows a wider input margin", TEST_ENABLED +}; + +static const SDLTest_TestCaseReference waylandTestFrameInsetsInputMarginNone = { + wayland_frameInsetsInputMarginNone, "wayland_frameInsetsInputMarginNone", "With an input margin of 0 only the frame takes input", TEST_ENABLED +}; + static const SDLTest_TestCaseReference *waylandTests[] = { &waylandTestFrameInsetsFloating, &waylandTestFrameInsetsMaximized, &waylandTestFrameInsetsFullscreen, &waylandTestFrameInsetsPopup, + &waylandTestFrameInsetsBeforeShow, + &waylandTestFrameInsetsRecreated, + &waylandTestFrameInsetsShownAgain, + &waylandTestFrameInsetsInputMarginWide, + &waylandTestFrameInsetsInputMarginNone, NULL };