Skip to content

Add support for non-constrained and non-grabbing popups #12840

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions include/SDL3/SDL_video.h
Original file line number Diff line number Diff line change
Expand Up @@ -1188,6 +1188,15 @@ extern SDL_DECLSPEC SDL_Window * SDLCALL SDL_CreateWindow(const char *title, int
* Popup windows implicitly do not have a border/decorations and do not appear
* on the taskbar/dock or in lists of windows such as alt-tab menus.
*
* By default, popup window positions will automatically be constrained to keep
* the entire window within display bounds. This can be overridden with the
* `SDL_PROP_WINDOW_CREATE_CONSTRAIN_POPUP_BOOLEAN` property.
*
* By default, popup menus will automatically grab keyboard focus from the parent
* when shown. This behavior can be overridden by setting the `SDL_WINDOW_NOT_FOCUSABLE`
* flag, setting the `SDL_PROP_WINDOW_CREATE_FOCUSABLE_BOOLEAN` property to false, or
* toggling it after creation via the `SDL_SetWindowFocusable()` function.
*
* If a parent window is hidden or destroyed, any child popup windows will be
* recursively hidden or destroyed as well. Child popup windows not explicitly
* hidden will be restored when the parent is shown.
Expand Down Expand Up @@ -1228,6 +1237,9 @@ extern SDL_DECLSPEC SDL_Window * SDLCALL SDL_CreatePopupWindow(SDL_Window *paren
* be always on top
* - `SDL_PROP_WINDOW_CREATE_BORDERLESS_BOOLEAN`: true if the window has no
* window decoration
* - `SDL_PROP_WINDOW_CREATE_CONSTRAIN_POPUP_BOOLEAN`: true if the "tooltip" and
* "menu" window types should be automatically constrained to be entirely within
* display bounds (default), false if no constraints on the position are desired.
* - `SDL_PROP_WINDOW_CREATE_EXTERNAL_GRAPHICS_CONTEXT_BOOLEAN`: true if the
* window will be used with an externally managed graphics context.
* - `SDL_PROP_WINDOW_CREATE_FOCUSABLE_BOOLEAN`: true if the window should
Expand Down Expand Up @@ -1356,6 +1368,7 @@ extern SDL_DECLSPEC SDL_Window * SDLCALL SDL_CreateWindowWithProperties(SDL_Prop

#define SDL_PROP_WINDOW_CREATE_ALWAYS_ON_TOP_BOOLEAN "SDL.window.create.always_on_top"
#define SDL_PROP_WINDOW_CREATE_BORDERLESS_BOOLEAN "SDL.window.create.borderless"
#define SDL_PROP_WINDOW_CREATE_CONSTRAIN_POPUP_BOOLEAN "SDL.window.create.constrain_popup"
#define SDL_PROP_WINDOW_CREATE_FOCUSABLE_BOOLEAN "SDL.window.create.focusable"
#define SDL_PROP_WINDOW_CREATE_EXTERNAL_GRAPHICS_CONTEXT_BOOLEAN "SDL.window.create.external_graphics_context"
#define SDL_PROP_WINDOW_CREATE_FLAGS_NUMBER "SDL.window.create.flags"
Expand Down
6 changes: 6 additions & 0 deletions src/video/SDL_sysvideo.h
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ struct SDL_Window
bool last_position_pending; // This should NOT be cleared by the backend, as it is used for fullscreen positioning.
bool last_size_pending; // This should be cleared by the backend if the new size cannot be applied.
bool update_fullscreen_on_display_changed;
bool constrain_popup;
bool is_destroying;
bool is_dropping; // drag/drop in progress, expecting SDL_SendDropComplete().

Expand Down Expand Up @@ -133,6 +134,9 @@ struct SDL_Window

SDL_WindowData *internal;

// If a toplevel window, holds the current keyboard focus for grabbing popups.
SDL_Window *keyboard_focus;

SDL_Window *prev;
SDL_Window *next;

Expand Down Expand Up @@ -571,6 +575,8 @@ extern bool SDL_RecreateWindow(SDL_Window *window, SDL_WindowFlags flags);
extern bool SDL_HasWindows(void);
extern void SDL_RelativeToGlobalForWindow(SDL_Window *window, int rel_x, int rel_y, int *abs_x, int *abs_y);
extern void SDL_GlobalToRelativeForWindow(SDL_Window *window, int abs_x, int abs_y, int *rel_x, int *rel_y);
extern bool SDL_ShouldFocusPopup(SDL_Window *window);
extern bool SDL_ShouldRelinquishPopupFocus(SDL_Window *window, SDL_Window **new_focus);

extern void SDL_OnDisplayAdded(SDL_VideoDisplay *display);
extern void SDL_OnDisplayMoved(SDL_VideoDisplay *display);
Expand Down
43 changes: 43 additions & 0 deletions src/video/SDL_video.c
Original file line number Diff line number Diff line change
Expand Up @@ -2491,6 +2491,7 @@ SDL_Window *SDL_CreateWindowWithProperties(SDL_PropertiesID props)
window->is_destroying = false;
window->last_displayID = SDL_GetDisplayForWindow(window);
window->external_graphics_context = external_graphics_context;
window->constrain_popup = SDL_GetBooleanProperty(props, SDL_PROP_WINDOW_CREATE_CONSTRAIN_POPUP_BOOLEAN, true);

if (_this->windows) {
_this->windows->prev = window;
Expand Down Expand Up @@ -3692,6 +3693,48 @@ bool SDL_SetWindowModal(SDL_Window *window, bool modal)
return _this->SetWindowModal(_this, window, modal);
}

bool SDL_ShouldRelinquishPopupFocus(SDL_Window *window, SDL_Window **new_focus)
{
SDL_Window *focus = window->parent;
bool set_focus = !!(window->flags & SDL_WINDOW_INPUT_FOCUS);

// Find the highest level window, up to the toplevel parent, that isn't being hidden or destroyed, and can grab the keyboard focus.
while (SDL_WINDOW_IS_POPUP(focus) && ((focus->flags & SDL_WINDOW_NOT_FOCUSABLE) || focus->is_hiding || focus->is_destroying)) {
focus = focus->parent;

// If some window in the chain currently had focus, set it to the new lowest-level window.
if (!set_focus) {
set_focus = !!(focus->flags & SDL_WINDOW_INPUT_FOCUS);
}
}

*new_focus = focus;
return set_focus;
}

bool SDL_ShouldFocusPopup(SDL_Window *window)
{
SDL_Window *toplevel_parent;
for (toplevel_parent = window->parent; SDL_WINDOW_IS_POPUP(toplevel_parent); toplevel_parent = toplevel_parent->parent) {
}

SDL_Window *current_focus = toplevel_parent->keyboard_focus;
bool found_higher_focus = false;

/* Traverse the window tree from the currently focused window to the toplevel parent and see if we encounter
* the new focus request. If the new window is found, a higher-level window already has focus.
*/
SDL_Window *w;
for (w = current_focus; w != toplevel_parent; w = w->parent) {
if (w == window) {
found_higher_focus = true;
break;
}
}

return !found_higher_focus || w == toplevel_parent;
}

bool SDL_SetWindowFocusable(SDL_Window *window, bool focusable)
{
CHECK_WINDOW_MAGIC(window, false);
Expand Down
1 change: 0 additions & 1 deletion src/video/cocoa/SDL_cocoawindow.h
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,6 @@ typedef enum
@property(nonatomic) BOOL was_zoomed;
@property(nonatomic) NSInteger window_number;
@property(nonatomic) NSInteger flash_request;
@property(nonatomic) SDL_Window *keyboard_focus;
@property(nonatomic) SDL3Cocoa_WindowListener *listener;
@property(nonatomic) NSModalSession modal_session;
@property(nonatomic) SDL_CocoaVideoData *videodata;
Expand Down
51 changes: 25 additions & 26 deletions src/video/cocoa/SDL_cocoawindow.m
Original file line number Diff line number Diff line change
Expand Up @@ -707,10 +707,7 @@ static void Cocoa_UpdateClipCursor(SDL_Window *window)
static void Cocoa_SetKeyboardFocus(SDL_Window *window, bool set_active_focus)
{
SDL_Window *toplevel = GetParentToplevelWindow(window);
SDL_CocoaWindowData *toplevel_data;

toplevel_data = (__bridge SDL_CocoaWindowData *)toplevel->internal;
toplevel_data.keyboard_focus = window;
toplevel->keyboard_focus = window;

if (set_active_focus && !window->is_hiding && !window->is_destroying) {
SDL_SetKeyboardFocus(window);
Expand Down Expand Up @@ -1252,7 +1249,7 @@ - (void)windowDidBecomeKey:(NSNotification *)aNotification

// We're going to get keyboard events, since we're key.
// This needs to be done before restoring the relative mouse mode.
Cocoa_SetKeyboardFocus(_data.keyboard_focus ? _data.keyboard_focus : window, true);
Cocoa_SetKeyboardFocus(window->keyboard_focus ? window->keyboard_focus : window, true);

// If we just gained focus we need the updated mouse position
if (!(window->flags & SDL_WINDOW_MOUSE_RELATIVE_MODE)) {
Expand Down Expand Up @@ -2211,8 +2208,8 @@ then immediately ordering out (removing) the window does work. */
if (window->flags & SDL_WINDOW_TOOLTIP) {
[nswindow setIgnoresMouseEvents:YES];
[nswindow setAcceptsMouseMovedEvents:NO];
} else if (window->flags & SDL_WINDOW_POPUP_MENU) {
Cocoa_SetKeyboardFocus(window, window->parent == SDL_GetKeyboardFocus());
} else if ((window->flags & SDL_WINDOW_POPUP_MENU) && !(window->flags & SDL_WINDOW_NOT_FOCUSABLE)) {
Cocoa_SetKeyboardFocus(window, true);
}
}

Expand Down Expand Up @@ -2301,7 +2298,7 @@ bool Cocoa_CreateWindow(SDL_VideoDevice *_this, SDL_Window *window, SDL_Properti
rect.origin.y -= screenRect.origin.y;

// Constrain the popup
if (SDL_WINDOW_IS_POPUP(window)) {
if (SDL_WINDOW_IS_POPUP(window) && window->constrain_popup) {
if (rect.origin.x + rect.size.width > screenRect.origin.x + screenRect.size.width) {
rect.origin.x -= (rect.origin.x + rect.size.width) - (screenRect.origin.x + screenRect.size.width);
}
Expand Down Expand Up @@ -2457,7 +2454,7 @@ bool Cocoa_SetWindowPosition(SDL_VideoDevice *_this, SDL_Window *window)
ConvertNSRect(&rect);

// Position and constrain the popup
if (SDL_WINDOW_IS_POPUP(window)) {
if (SDL_WINDOW_IS_POPUP(window) && window->constrain_popup) {
NSRect screenRect = [ScreenForRect(&rect) frame];

if (rect.origin.x + rect.size.width > screenRect.origin.x + screenRect.size.width) {
Expand Down Expand Up @@ -2629,20 +2626,9 @@ void Cocoa_HideWindow(SDL_VideoDevice *_this, SDL_Window *window)
Cocoa_SetWindowModal(_this, window, false);

// Transfer keyboard focus back to the parent when closing a popup menu
if (window->flags & SDL_WINDOW_POPUP_MENU) {
SDL_Window *new_focus = window->parent;
bool set_focus = window == SDL_GetKeyboardFocus();

// Find the highest level window, up to the toplevel parent, that isn't being hidden or destroyed.
while (SDL_WINDOW_IS_POPUP(new_focus) && (new_focus->is_hiding || new_focus->is_destroying)) {
new_focus = new_focus->parent;

// If some window in the chain currently had focus, set it to the new lowest-level window.
if (!set_focus) {
set_focus = new_focus == SDL_GetKeyboardFocus();
}
}

if ((window->flags & SDL_WINDOW_POPUP_MENU) && !(window->flags & SDL_WINDOW_NOT_FOCUSABLE)) {
SDL_Window *new_focus;
const bool set_focus = SDL_ShouldRelinquishPopupFocus(window, &new_focus);
Cocoa_SetKeyboardFocus(new_focus, set_focus);
} else if (window->parent && waskey) {
/* Key status is not automatically set on the parent when a child is hidden. Check if the
Expand Down Expand Up @@ -3068,20 +3054,19 @@ void Cocoa_DestroyWindow(SDL_VideoDevice *_this, SDL_Window *window)

#endif // SDL_VIDEO_OPENGL
SDL_Window *topmost = GetParentToplevelWindow(window);
SDL_CocoaWindowData *topmost_data = (__bridge SDL_CocoaWindowData *)topmost->internal;

/* Reset the input focus of the root window if this window is still set as keyboard focus.
* SDL_DestroyWindow will have already taken care of reassigning focus if this is the SDL
* keyboard focus, this ensures that an inactive window with this window set as input focus
* does not try to reference it the next time it gains focus.
*/
if (topmost_data.keyboard_focus == window) {
if (topmost->keyboard_focus == window) {
SDL_Window *new_focus = window;
while (SDL_WINDOW_IS_POPUP(new_focus) && (new_focus->is_hiding || new_focus->is_destroying)) {
new_focus = new_focus->parent;
}

topmost_data.keyboard_focus = new_focus;
topmost->keyboard_focus = new_focus;
}

if ([data.listener isInFullscreenSpace]) {
Expand Down Expand Up @@ -3246,6 +3231,20 @@ bool Cocoa_FlashWindow(SDL_VideoDevice *_this, SDL_Window *window, SDL_FlashOper

bool Cocoa_SetWindowFocusable(SDL_VideoDevice *_this, SDL_Window *window, bool focusable)
{
if (window->flags & SDL_WINDOW_POPUP_MENU) {
if (!(window->flags & SDL_WINDOW_HIDDEN)) {
if (!focusable && (window->flags & SDL_WINDOW_INPUT_FOCUS)) {
SDL_Window *new_focus;
const bool set_focus = SDL_ShouldRelinquishPopupFocus(window, &new_focus);
Cocoa_SetKeyboardFocus(new_focus, set_focus);
} else if (focusable) {
if (SDL_ShouldFocusPopup(window)) {
Cocoa_SetKeyboardFocus(window, true);
}
}
}
}

return true; // just succeed, the real work is done elsewhere.
}

Expand Down
2 changes: 1 addition & 1 deletion src/video/wayland/SDL_waylandevents.c
Original file line number Diff line number Diff line change
Expand Up @@ -1824,7 +1824,7 @@ static void keyboard_handle_enter(void *data, struct wl_keyboard *keyboard,
seat->keyboard.focus = window;

// Restore the keyboard focus to the child popup that was holding it
SDL_SetKeyboardFocus(window->keyboard_focus ? window->keyboard_focus : window->sdlwindow);
SDL_SetKeyboardFocus(window->sdlwindow->keyboard_focus ? window->sdlwindow->keyboard_focus : window->sdlwindow);

// Update the keyboard grab and any relative pointer grabs related to this keyboard focus.
Wayland_SeatUpdateKeyboardGrab(seat);
Expand Down
1 change: 1 addition & 0 deletions src/video/wayland/SDL_waylandvideo.c
Original file line number Diff line number Diff line change
Expand Up @@ -629,6 +629,7 @@ static SDL_VideoDevice *Wayland_CreateDevice(bool require_preferred_protocols)
device->HasScreenKeyboardSupport = Wayland_HasScreenKeyboardSupport;
device->ShowWindowSystemMenu = Wayland_ShowWindowSystemMenu;
device->SyncWindow = Wayland_SyncWindow;
device->SetWindowFocusable = Wayland_SetWindowFocusable;

#ifdef SDL_USE_LIBDBUS
if (SDL_SystemTheme_Init())
Expand Down
53 changes: 32 additions & 21 deletions src/video/wayland/SDL_waylandwindow.c
Original file line number Diff line number Diff line change
Expand Up @@ -1677,7 +1677,7 @@ static const struct wp_color_management_surface_feedback_v1_listener color_manag
feedback_surface_preferred_changed
};

static void SetKeyboardFocus(SDL_Window *window, bool set_focus)
static void Wayland_SetKeyboardFocus(SDL_Window *window, bool set_focus)
{
SDL_Window *toplevel = window;

Expand All @@ -1686,7 +1686,7 @@ static void SetKeyboardFocus(SDL_Window *window, bool set_focus)
toplevel = toplevel->parent;
}

toplevel->internal->keyboard_focus = window;
toplevel->keyboard_focus = window;

if (set_focus && !window->is_hiding && !window->is_destroying) {
SDL_SetKeyboardFocus(window);
Expand Down Expand Up @@ -1916,8 +1916,9 @@ void Wayland_ShowWindow(SDL_VideoDevice *_this, SDL_Window *window)
data->shell_surface.xdg.popup.xdg_positioner = xdg_wm_base_create_positioner(c->shell.xdg);
xdg_positioner_set_anchor(data->shell_surface.xdg.popup.xdg_positioner, XDG_POSITIONER_ANCHOR_TOP_LEFT);
xdg_positioner_set_anchor_rect(data->shell_surface.xdg.popup.xdg_positioner, 0, 0, parent->internal->current.logical_width, parent->internal->current.logical_width);
xdg_positioner_set_constraint_adjustment(data->shell_surface.xdg.popup.xdg_positioner,
XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_SLIDE_X | XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_SLIDE_Y);

const Uint32 constraint = window->constrain_popup ? (XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_SLIDE_X | XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_SLIDE_Y) : XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_NONE;
xdg_positioner_set_constraint_adjustment(data->shell_surface.xdg.popup.xdg_positioner, constraint);
xdg_positioner_set_gravity(data->shell_surface.xdg.popup.xdg_positioner, XDG_POSITIONER_GRAVITY_BOTTOM_RIGHT);
xdg_positioner_set_size(data->shell_surface.xdg.popup.xdg_positioner, data->current.logical_width, data->current.logical_height);

Expand Down Expand Up @@ -1946,8 +1947,8 @@ void Wayland_ShowWindow(SDL_VideoDevice *_this, SDL_Window *window)
wl_region_add(region, 0, 0, 0, 0);
wl_surface_set_input_region(data->surface, region);
wl_region_destroy(region);
} else if (window->flags & SDL_WINDOW_POPUP_MENU) {
SetKeyboardFocus(window, window->parent == SDL_GetKeyboardFocus());
} else if ((window->flags & SDL_WINDOW_POPUP_MENU) && !(window->flags & SDL_WINDOW_NOT_FOCUSABLE)) {
Wayland_SetKeyboardFocus(window, true);
}

SDL_SetPointerProperty(props, SDL_PROP_WINDOW_WAYLAND_XDG_POPUP_POINTER, data->shell_surface.xdg.popup.xdg_popup);
Expand Down Expand Up @@ -2094,21 +2095,10 @@ static void Wayland_ReleasePopup(SDL_VideoDevice *_this, SDL_Window *popup)
return;
}

if (popup->flags & SDL_WINDOW_POPUP_MENU) {
SDL_Window *new_focus = popup->parent;
bool set_focus = popup == SDL_GetKeyboardFocus();

// Find the highest level window, up to the toplevel parent, that isn't being hidden or destroyed.
while (SDL_WINDOW_IS_POPUP(new_focus) && (new_focus->is_hiding || new_focus->is_destroying)) {
new_focus = new_focus->parent;

// If some window in the chain currently had focus, set it to the new lowest-level window.
if (!set_focus) {
set_focus = new_focus == SDL_GetKeyboardFocus();
}
}

SetKeyboardFocus(new_focus, set_focus);
if ((popup->flags & SDL_WINDOW_POPUP_MENU) && !(popup->flags & SDL_WINDOW_NOT_FOCUSABLE)) {
SDL_Window *new_focus;
const bool set_focus = SDL_ShouldRelinquishPopupFocus(popup, &new_focus);
Wayland_SetKeyboardFocus(new_focus, set_focus);
}

xdg_popup_destroy(popupdata->shell_surface.xdg.popup.xdg_popup);
Expand Down Expand Up @@ -3002,6 +2992,27 @@ bool Wayland_SyncWindow(SDL_VideoDevice *_this, SDL_Window *window)
return true;
}

bool Wayland_SetWindowFocusable(SDL_VideoDevice *_this, SDL_Window *window, bool focusable)
{
if (window->flags & SDL_WINDOW_POPUP_MENU) {
if (!(window->flags & SDL_WINDOW_HIDDEN)) {
if (!focusable && (window->flags & SDL_WINDOW_INPUT_FOCUS)) {
SDL_Window *new_focus;
const bool set_focus = SDL_ShouldRelinquishPopupFocus(window, &new_focus);
Wayland_SetKeyboardFocus(new_focus, set_focus);
} else if (focusable) {
if (SDL_ShouldFocusPopup(window)) {
Wayland_SetKeyboardFocus(window, true);
}
}
}

return true;
}

return SDL_SetError("wayland: focus can only be toggled on popup menu windows");
}

void Wayland_ShowWindowSystemMenu(SDL_Window *window, int x, int y)
{
SDL_WindowData *wind = window->internal;
Expand Down
3 changes: 1 addition & 2 deletions src/video/wayland/SDL_waylandwindow.h
Original file line number Diff line number Diff line change
Expand Up @@ -126,8 +126,6 @@ struct SDL_WindowData
SDL_DisplayData **outputs;
int num_outputs;

SDL_Window *keyboard_focus;

char *app_id;
double scale_factor;

Expand Down Expand Up @@ -249,6 +247,7 @@ extern void Wayland_ShowWindowSystemMenu(SDL_Window *window, int x, int y);
extern void Wayland_DestroyWindow(SDL_VideoDevice *_this, SDL_Window *window);
extern bool Wayland_SuspendScreenSaver(SDL_VideoDevice *_this);
extern bool Wayland_SetWindowIcon(SDL_VideoDevice *_this, SDL_Window *window, SDL_Surface *icon);
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);

Expand Down
2 changes: 1 addition & 1 deletion src/video/windows/SDL_windowsevents.c
Original file line number Diff line number Diff line change
Expand Up @@ -389,7 +389,7 @@ static void WIN_UpdateFocus(SDL_Window *window, bool expect_focus)
}
}

SDL_SetKeyboardFocus(data->keyboard_focus ? data->keyboard_focus : window);
SDL_SetKeyboardFocus(window->keyboard_focus ? window->keyboard_focus : window);

// In relative mode we are guaranteed to have mouse focus if we have keyboard focus
if (!SDL_GetMouse()->relative_mode) {
Expand Down
Loading
Loading