Skip to content

Add pygame.display.get_desktop_usable_bounds #3530

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 2 commits into
base: main
Choose a base branch
from

Conversation

damusss
Copy link
Member

@damusss damusss commented Jul 13, 2025

As I said on discord, pygame.display.get_desktop_sizes exist but doesn't take in consideration the taskbar or any other OS reserved space. The SDL function for it exists, so it makes sense to me to add it to display. It would be very tedious to do this in python cross platform without it. It's a differnt function from the sizes because it returns rects, but the return list uses the same logic so with the same index you get the full size and usable area for the same desktop. Also, it's alredy SDL3 compatible, so there will be no problems when display is ported. There is no conflict with window because this is one of the features that make sense for display, and doesn't concert any windows (display should only hold this functions)

Summary by CodeRabbit

  • New Features

    • Added get_desktop_usable_bounds() to retrieve usable desktop areas (excluding taskbars/docks) as a list of Rects.
    • Supports multi-monitor setups and mirroring; count may differ from attached monitors.
  • Documentation

    • Added API docs for get_desktop_usable_bounds(), including signature and behavior.
  • Tests

    • Added tests ensuring get_desktop_usable_bounds() returns Rects and that each bound does not exceed corresponding desktop sizes.

@damusss damusss requested a review from a team as a code owner July 13, 2025 07:49
@damusss damusss added New API This pull request may need extra debate as it adds a new class or function to pygame display pygame.display labels Jul 13, 2025
@ankith26
Copy link
Member

@coderabbitai full review

Copy link

coderabbitai bot commented Aug 13, 2025

✅ Actions performed

Full review triggered.

Copy link

coderabbitai bot commented Aug 13, 2025

Walkthrough

Added a new display API get_desktop_usable_bounds returning a list of Rects for usable desktop areas. Implemented in C with SDL2/SDL3 paths, exposed in the display module, documented via a new doc macro, typed in stubs, and tested to validate types and size constraints against get_desktop_sizes.

Changes

Cohort / File(s) Summary
Stub typings
buildconfig/stubs/pygame/display.pyi
Declares get_desktop_usable_bounds() -> list[Rect]; imports Rect; removes Union from typing imports; places declaration after get_desktop_sizes.
C display module
src_c/display.c
Implements pg_get_desktop_usable_bounds using SDL APIs (SDL3 and pre-SDL3); creates Python list of Rects; registers method in _pg_display_methods with DOC_DISPLAY_GETDESKTOPUSABLEBOUNDS.
Docs macro
src_c/doc/display_doc.h
Adds DOC_DISPLAY_GETDESKTOPUSABLEBOUNDS documenting the new function signature and purpose.
Tests
test/display_test.py
Adds test_get_desktop_usable_bounds verifying list of pygame.Rect and bounds not exceeding get_desktop_sizes per index.

Sequence Diagram(s)

sequenceDiagram
    participant Py as Python user
    participant Mod as pygame.display
    participant C as pg_get_desktop_usable_bounds (C)
    participant SDL as SDL Video API

    Py->>Mod: get_desktop_usable_bounds()
    Mod->>C: call (no args)
    alt SDL3
        C->>SDL: SDL_GetDisplays() / SDL_GetDisplayUsableBounds()
    else pre-SDL3
        C->>SDL: SDL_GetNumVideoDisplays() / SDL_GetDisplayUsableBounds()
    end
    SDL-->>C: usable bounds per display
    C-->>Mod: list[Rect]
    Mod-->>Py: list[Rect]
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

I mapped the screens where windows hop,
Found cozy bounds where menus stop.
Rects in a row, neat as a line—
Usable lands where pixels shine.
I thump my paw, compile, then cheer:
“New API, so crystal clear!” 🐇✨

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
test/display_test.py (1)

700-709: Avoid potential IndexError and assert list alignment

If, for any reason, the two lists differ in length, this loop will raise IndexError. Assert the lengths match and iterate with zip to keep the test robust.

Apply this diff:

 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])
+    self.assertEqual(len(bounds), len(sizes))
+    for bound, size in zip(bounds, sizes):
+        self.assertIsInstance(bound, pygame.Rect)
+        self.assertLessEqual(bound.w, size[0])
+        self.assertLessEqual(bound.h, size[1])
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a6db5c7 and f1f8177.

📒 Files selected for processing (4)
  • buildconfig/stubs/pygame/display.pyi (2 hunks)
  • src_c/display.c (2 hunks)
  • src_c/doc/display_doc.h (1 hunks)
  • test/display_test.py (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (2)
test/display_test.py (2)
buildconfig/stubs/pygame/display.pyi (2)
  • get_desktop_usable_bounds (425-441)
  • get_desktop_sizes (406-423)
buildconfig/stubs/pygame/rect.pyi (1)
  • Rect (303-303)
buildconfig/stubs/pygame/display.pyi (4)
buildconfig/stubs/pygame/typing.pyi (1)
  • rect (57-57)
src_py/typing.py (1)
  • rect (57-57)
buildconfig/stubs/typing_sample_app.py (1)
  • rect (100-101)
buildconfig/stubs/pygame/rect.pyi (1)
  • Rect (303-303)
🔇 Additional comments (4)
src_c/doc/display_doc.h (1)

14-14: Doc macro addition LGTM

The new macro name and summary align with existing patterns and the API behavior.

buildconfig/stubs/pygame/display.pyi (2)

425-442: Stub signature and docstring LGTM

Return type list[Rect] matches runtime objects. The docstring mirrors the new API semantics and version tag is present.


55-55: No Union usage detected – import removal is safe

  • buildconfig/stubs/pygame/display.pyi: scanned for “Union” with ripgrep; no occurrences found.
src_c/display.c (1)

3179-3181: Method registration LGTM

Correctly registered with METH_NOARGS and linked to the doc macro.

Comment on lines +2299 to +2344
static PyObject *
pg_get_desktop_usable_bounds(PyObject *self, PyObject *_null)
{
int display_count, i;
PyObject *result;

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) {
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) {
return RAISE(pgExc_SDLError, SDL_GetError());
}
#else
if (SDL_GetDisplayUsableBounds(i, &bounds) < 0) {
return RAISE(pgExc_SDLError, SDL_GetError());
}
#endif
PyObject *pg_rect = pgRect_New(&bounds);
if (pg_rect == NULL) {
return NULL; /* exception already set */
}
PyList_SET_ITEM(result, i, pg_rect);
}
return result;
}

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Memory leaks and missing DECREF on error paths in SDL3 and SDL2 branches

  • SDL3 path: SDL_GetDisplays allocates an array that must be freed with SDL_free(). It is never freed.
  • Both paths: After allocating result = PyList_New(...), early returns on errors leak the Python list (missing Py_DECREF(result)).
  • On pgRect_New failure, result and (in SDL3) displays are leaked.

Fix by freeing displays (SDL3) and DECREF-ing result on all error paths.

Apply this diff:

 static PyObject *
 pg_get_desktop_usable_bounds(PyObject *self, PyObject *_null)
 {
-    int display_count, i;
-    PyObject *result;
+    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());
-    }
+    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) {
-        return NULL;
-    }
+    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) {
-            return RAISE(pgExc_SDLError, SDL_GetError());
-        }
+        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) {
-            return RAISE(pgExc_SDLError, SDL_GetError());
-        }
+        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) {
-            return NULL; /* exception already set */
-        }
+        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);
     }
-    return result;
+#if PG_SDL3
+    SDL_free(displays);
+#endif
+    return result;
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
static PyObject *
pg_get_desktop_usable_bounds(PyObject *self, PyObject *_null)
{
int display_count, i;
PyObject *result;
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) {
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) {
return RAISE(pgExc_SDLError, SDL_GetError());
}
#else
if (SDL_GetDisplayUsableBounds(i, &bounds) < 0) {
return RAISE(pgExc_SDLError, SDL_GetError());
}
#endif
PyObject *pg_rect = pgRect_New(&bounds);
if (pg_rect == NULL) {
return NULL; /* exception already set */
}
PyList_SET_ITEM(result, i, pg_rect);
}
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;
}
🤖 Prompt for AI Agents
In src_c/display.c around lines 2299 to 2344, the SDL3 branch leaks the displays
array returned by SDL_GetDisplays and both branches leak the Python list and (in
SDL3) displays on early error paths; ensure you free resources on every error
path: after creating result = PyList_New(display_count) call, on any subsequent
error return you must Py_DECREF(result) before returning; in the SDL3 branch
free the displays array with SDL_free(displays) before every return (both on
SDL_GetDisplayUsableBounds failure and on pgRect_New failure), and also free
displays just before the successful return; do not change behavior otherwise.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
display pygame.display New API This pull request may need extra debate as it adds a new class or function to pygame
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants