diff --git a/buildconfig/stubs/pygame/display.pyi b/buildconfig/stubs/pygame/display.pyi index c46189eadc..b48361ce23 100644 --- a/buildconfig/stubs/pygame/display.pyi +++ b/buildconfig/stubs/pygame/display.pyi @@ -52,10 +52,11 @@ required). """ from collections.abc import Iterable -from typing import Literal, Optional, Union, overload +from typing import Literal, Optional, overload from pygame._sdl2 import Window from pygame.constants import FULLSCREEN +from pygame.rect import Rect from pygame.surface import Surface from pygame.typing import ( ColorLike, @@ -421,6 +422,24 @@ def get_desktop_sizes() -> list[tuple[int, int]]: .. versionaddedold:: 2.0.0 """ +def get_desktop_usable_bounds() -> list[Rect]: + """Get the bounding rects of the usable area of active desktops. + + Returns a list of :class:`pygame.Rect` representing the bounding rectangles + of the usable area of the currently configured virtual desktops. + + The usable area is the desktop area minus the areas that are used by + the operating system. This includes, for example, taskbars. The location + and size of the unusable areas depend on the operating system and its + configuration. The usable area is usually the area that maximized windows + fill. + + The length of the list is not the same as the number of attached monitors, + as a desktop can be mirrored across multiple monitors. + + .. versionadded:: 2.5.6 + """ + def list_modes( depth: int = 0, flags: int = FULLSCREEN, diff --git a/src_c/display.c b/src_c/display.c index 6934ca808d..49a90536cd 100644 --- a/src_c/display.c +++ b/src_c/display.c @@ -2296,6 +2296,65 @@ pg_get_desktop_screen_sizes(PyObject *self, PyObject *_null) return result; } +static PyObject * +pg_get_desktop_usable_bounds(PyObject *self, PyObject *_null) +{ + int display_count, i; + PyObject *result = NULL; + + VIDEO_INIT_CHECK(); + +#if PG_SDL3 + SDL_DisplayID *displays = SDL_GetDisplays(&display_count); + if (displays == NULL) { + return RAISE(pgExc_SDLError, SDL_GetError()); + } +#else + display_count = SDL_GetNumVideoDisplays(); + if (display_count < 0) { + return RAISE(pgExc_SDLError, SDL_GetError()); + } +#endif + + result = PyList_New(display_count); + if (!result) { +#if PG_SDL3 + SDL_free(displays); +#endif + return NULL; + } + + for (i = 0; i < display_count; i++) { + SDL_Rect bounds; +#if PG_SDL3 + SDL_DisplayID display_id = displays[i]; + if (SDL_GetDisplayUsableBounds(display_id, &bounds) == SDL_FALSE) { + Py_DECREF(result); + SDL_free(displays); + return RAISE(pgExc_SDLError, SDL_GetError()); + } +#else + if (SDL_GetDisplayUsableBounds(i, &bounds) < 0) { + Py_DECREF(result); + return RAISE(pgExc_SDLError, SDL_GetError()); + } +#endif + PyObject *pg_rect = pgRect_New(&bounds); + if (pg_rect == NULL) { +#if PG_SDL3 + SDL_free(displays); +#endif + Py_DECREF(result); + return NULL; /* exception already set */ + } + PyList_SET_ITEM(result, i, pg_rect); + } + +#if PG_SDL3 + SDL_free(displays); +#endif + return result; +} static PyObject * pg_is_fullscreen(PyObject *self, PyObject *_null) { @@ -3130,6 +3189,8 @@ static PyMethodDef _pg_display_methods[] = { METH_NOARGS, "provisional API, subject to change"}, {"get_desktop_sizes", (PyCFunction)pg_get_desktop_screen_sizes, METH_NOARGS, DOC_DISPLAY_GETDESKTOPSIZES}, + {"get_desktop_usable_bounds", (PyCFunction)pg_get_desktop_usable_bounds, + METH_NOARGS, DOC_DISPLAY_GETDESKTOPUSABLEBOUNDS}, {"is_fullscreen", (PyCFunction)pg_is_fullscreen, METH_NOARGS, DOC_DISPLAY_ISFULLSCREEN}, {"is_vsync", (PyCFunction)pg_is_vsync, METH_NOARGS, DOC_DISPLAY_ISVSYNC}, diff --git a/src_c/doc/display_doc.h b/src_c/doc/display_doc.h index f449077645..3b0b732f8c 100644 --- a/src_c/doc/display_doc.h +++ b/src_c/doc/display_doc.h @@ -11,6 +11,7 @@ #define DOC_DISPLAY_INFO "Info() -> _VidInfo\nCreate a video display information object." #define DOC_DISPLAY_GETWMINFO "get_wm_info() -> dict[str, int]\nGet information about the current windowing system." #define DOC_DISPLAY_GETDESKTOPSIZES "get_desktop_sizes() -> list[tuple[int, int]]\nGet sizes of active desktops." +#define DOC_DISPLAY_GETDESKTOPUSABLEBOUNDS "get_desktop_usable_bounds() -> list[Rect]\nGet the bounding rects of the usable area of active desktops." #define DOC_DISPLAY_LISTMODES "list_modes(depth=0, flags=FULLSCREEN, display=0) -> list[tuple[int, int]]\nGet list of available fullscreen modes." #define DOC_DISPLAY_MODEOK "mode_ok(size, flags=0, depth=0, display=0) -> int\nPick the best color depth for a display mode." #define DOC_DISPLAY_GLGETATTRIBUTE "gl_get_attribute(flag, /) -> int\nGet the value for an OpenGL flag for the current display." diff --git a/test/display_test.py b/test/display_test.py index 3d7b91641f..f03baa55e6 100644 --- a/test/display_test.py +++ b/test/display_test.py @@ -697,6 +697,16 @@ def test_get_set_window_position(self): self.assertEqual(position[0], 420) self.assertEqual(position[1], 360) + def test_get_desktop_usable_bounds(self): + bounds = pygame.display.get_desktop_usable_bounds() + sizes = pygame.display.get_desktop_sizes() + self.assertIsInstance(bounds, list) + for i, bound in enumerate(bounds): + self.assertIsInstance(bound, pygame.Rect) + size = sizes[i] + self.assertLessEqual(bound.w, size[0]) + self.assertLessEqual(bound.h, size[1]) + class DisplayUpdateTest(unittest.TestCase): def question(self, qstr):