diff --git a/buildconfig/stubs/pygame/_sdl2/video.pyi b/buildconfig/stubs/pygame/_sdl2/video.pyi index fabf97f309..95fb212e02 100644 --- a/buildconfig/stubs/pygame/_sdl2/video.pyi +++ b/buildconfig/stubs/pygame/_sdl2/video.pyi @@ -1,10 +1,10 @@ -from typing import Any, Generator, Iterable, Optional, Tuple, Union, final +from typing import Any, Generator, Iterable, Optional, Tuple, Union from pygame.color import Color from pygame.rect import Rect from pygame.surface import Surface -from .._common import RectValue, ColorValue +from .._common import ColorValue, RectValue WINDOWPOS_UNDEFINED: int WINDOWPOS_CENTERED: int diff --git a/buildconfig/stubs/pygame/_window.pyi b/buildconfig/stubs/pygame/_window.pyi index 44e2b7a780..0fcb31467b 100644 --- a/buildconfig/stubs/pygame/_window.pyi +++ b/buildconfig/stubs/pygame/_window.pyi @@ -25,6 +25,8 @@ class Window: def minimize(self) -> None: ... def set_modal_for(self, parent: Window) -> None: ... def set_icon(self, icon: Surface) -> None: ... + def get_surface(self) -> Surface: ... + def update_from_surface(self) -> None: ... grab_mouse: bool grab_keyboard: bool diff --git a/docs/reST/ref/sdl2_video.rst b/docs/reST/ref/sdl2_video.rst index 842855db48..be127057da 100644 --- a/docs/reST/ref/sdl2_video.rst +++ b/docs/reST/ref/sdl2_video.rst @@ -294,6 +294,43 @@ Create a Window object that uses the same window data from the :mod:`pygame.display` module, created upon calling :func:`pygame.display.set_mode`. + .. method:: get_surface + + | :sl:`Get the window surface` + | :sg:`get_surface() -> Surface` + + Return a reference to the surface associated with the window. + + The size of surface will automatically change to fit the window size. + + The window surface become invalid when the window is destroyed. + + .. seealso:: :func:`update_from_surface` + + .. versionadded:: 2.4.0 + + .. method:: update_from_surface + + | :sl:`Update the window surface to the window.` + | :sg:`update_from_surface() -> None` + + Update content from the window surface to the window. + + Here is an example of using ``get_surface`` and ``update_from_surface``: + + .. code-block:: python + + win = video.Window() + surf = win.get_surface() # get the window surface + + # draw something on the surface + surf.fill((255,0,0)) + + win.update_from_surface() # update the surface to the window + + + .. versionadded:: 2.4.0 + .. method:: set_windowed | :sl:`Enable windowed mode (exit fullscreen)` diff --git a/src_c/display.c b/src_c/display.c index b991368ec2..e64e73c4db 100644 --- a/src_c/display.c +++ b/src_c/display.c @@ -196,6 +196,7 @@ pg_display_quit(PyObject *self, PyObject *_null) pg_mod_autoquit(IMPPREFIX "event"); pg_mod_autoquit(IMPPREFIX "time"); + pg_mod_autoquit(IMPPREFIX "_window"); if (SDL_WasInit(SDL_INIT_VIDEO)) { SDL_QuitSubSystem(SDL_INIT_VIDEO); @@ -251,6 +252,8 @@ pg_display_init(PyObject *self, PyObject *_null) return NULL; if (!pg_mod_autoinit(IMPPREFIX "event")) return NULL; + if (!pg_mod_autoinit(IMPPREFIX "_window")) + return NULL; Py_RETURN_NONE; } diff --git a/src_c/doc/sdl2_video_doc.h b/src_c/doc/sdl2_video_doc.h index e335cb946e..434a39fd12 100644 --- a/src_c/doc/sdl2_video_doc.h +++ b/src_c/doc/sdl2_video_doc.h @@ -22,6 +22,8 @@ #define DOC_SDL2_VIDEO_WINDOW_OPACITY "opacity -> float\nGet or set the window opacity, between 0.0 (fully transparent) and 1.0 (fully opaque)" #define DOC_SDL2_VIDEO_WINDOW_DISPLAYINDEX "get_display_index -> int\nGet the index of the display that owns the window (**read-only**)" #define DOC_SDL2_VIDEO_WINDOW_FROMDISPLAYMODULE "from_display_module() -> Window\nCreate a Window object using window data from display module" +#define DOC_SDL2_VIDEO_WINDOW_GETSURFACE "get_surface() -> Surface\nGet the window surface" +#define DOC_SDL2_VIDEO_WINDOW_UPDATEFROMSURFACE "update_from_surface() -> None\nUpdate the window surface to the window." #define DOC_SDL2_VIDEO_WINDOW_SETWINDOWED "set_windowed() -> None\nEnable windowed mode (exit fullscreen)" #define DOC_SDL2_VIDEO_WINDOW_SETFULLSCREEN "set_fullscreen(desktop=False) -> None\nEnter fullscreen" #define DOC_SDL2_VIDEO_WINDOW_DESTROY "destroy() -> None\nDestroy the window" diff --git a/src_c/include/_pygame.h b/src_c/include/_pygame.h index a5ae05138c..68bb5ae1a2 100644 --- a/src_c/include/_pygame.h +++ b/src_c/include/_pygame.h @@ -486,6 +486,7 @@ typedef struct pgColorObject pgColorObject; typedef struct { PyObject_HEAD SDL_Window *_win; SDL_bool _is_borrowed; + pgSurfaceObject *surf; } pgWindowObject; #ifndef PYGAMEAPI_WINDOW_INTERNAL diff --git a/src_c/window.c b/src_c/window.c index cd99c19117..d13a5de4ff 100644 --- a/src_c/window.c +++ b/src_c/window.c @@ -119,9 +119,89 @@ window_destroy(pgWindowObject *self, PyObject *_null) SDL_DestroyWindow(self->_win); self->_win = NULL; } + if (self->surf) { + // Set the internal surface to NULL to make pygame surface invalid + // since this surface will be deallocated by SDL when the window is + // destroyed. + self->surf->surf = NULL; + + Py_DECREF(self->surf); + self->surf = NULL; + } + Py_RETURN_NONE; +} + +static PyObject * +window_get_surface(pgWindowObject *self) +{ + PyObject *surf = NULL; + SDL_Surface *_surf; + + if (self->_is_borrowed) { + surf = (PyObject *)pg_GetDefaultWindowSurface(); + if (!surf) { + return RAISE(pgExc_SDLError, + "display.set_mode has not been called yet."); + } + Py_INCREF(surf); + return surf; + } + + _surf = SDL_GetWindowSurface(self->_win); + if (!_surf) { + return RAISE(pgExc_SDLError, SDL_GetError()); + } + if (self->surf == NULL) { + self->surf = pgSurface_New2(_surf, SDL_FALSE); + if (!self->surf) + return NULL; + } + self->surf->surf = _surf; + Py_INCREF(self->surf); + return (PyObject *)self->surf; +} + +static PyObject * +window_update_from_surface(pgWindowObject *self) +{ + int result; + + Py_BEGIN_ALLOW_THREADS; + result = SDL_UpdateWindowSurface(self->_win); + Py_END_ALLOW_THREADS; + if (result) + return RAISE(pgExc_SDLError, SDL_GetError()); Py_RETURN_NONE; } +// Callback function for surface auto resize +static int SDLCALL +_resize_event_watch(void *userdata, SDL_Event *event) +{ + pgWindowObject *event_window_pg; + SDL_Window *event_window; + if ((event->type != SDL_WINDOWEVENT)) + return 0; + if (event->window.event != SDL_WINDOWEVENT_SIZE_CHANGED) + return 0; + event_window = SDL_GetWindowFromID(event->window.windowID); + event_window_pg = SDL_GetWindowData(event_window, "pg_window"); + + if (!event_window_pg) + return 0; + + if (event_window_pg->_is_borrowed) { + // have been handled by event watch in display.c + return 0; + } + + if (!event_window_pg->surf) + return 0; + + event_window_pg->surf->surf = SDL_GetWindowSurface(event_window); + return 0; +} + static PyObject * window_set_windowed(pgWindowObject *self, PyObject *_null) { @@ -615,6 +695,15 @@ window_dealloc(pgWindowObject *self, PyObject *_null) SDL_SetWindowData(self->_win, "pg_window", NULL); } } + if (self->surf) { + // Set the internal surface to NULL to make pygame surface invalid + // since this surface will be deallocated by SDL when the window is + // destroyed. + self->surf->surf = NULL; + + Py_DECREF(self->surf); + } + Py_TYPE(self)->tp_free(self); } @@ -815,6 +904,7 @@ window_init(pgWindowObject *self, PyObject *args, PyObject *kwargs) } self->_win = _win; self->_is_borrowed = SDL_FALSE; + self->surf = NULL; SDL_SetWindowData(_win, "pg_window", self); @@ -880,6 +970,20 @@ window_repr(pgWindowObject *self) return PyUnicode_FromFormat("", title, win_id); } +static PyObject * +_window_internal_mod_init(PyObject *self, PyObject *_null) +{ + SDL_AddEventWatch(_resize_event_watch, NULL); + Py_RETURN_NONE; +} + +static PyObject * +_window_internal_mod_quit(PyObject *self, PyObject *_null) +{ + SDL_DelEventWatch(_resize_event_watch, NULL); + Py_RETURN_NONE; +} + static PyMethodDef window_methods[] = { {"destroy", (PyCFunction)window_destroy, METH_NOARGS, DOC_SDL2_VIDEO_WINDOW_DESTROY}, @@ -903,6 +1007,10 @@ static PyMethodDef window_methods[] = { DOC_SDL2_VIDEO_WINDOW_SETMODALFOR}, {"set_icon", (PyCFunction)window_set_icon, METH_O, DOC_SDL2_VIDEO_WINDOW_SETICON}, + {"update_from_surface", (PyCFunction)window_update_from_surface, + METH_NOARGS, DOC_SDL2_VIDEO_WINDOW_UPDATEFROMSURFACE}, + {"get_surface", (PyCFunction)window_get_surface, METH_NOARGS, + DOC_SDL2_VIDEO_WINDOW_GETSURFACE}, {"from_display_module", (PyCFunction)window_from_display_module, METH_CLASS | METH_NOARGS, DOC_SDL2_VIDEO_WINDOW_FROMDISPLAYMODULE}, {NULL, NULL, 0, NULL}}; @@ -959,6 +1067,10 @@ static PyTypeObject pgWindow_Type = { static PyMethodDef _window_methods[] = { {"get_grabbed_window", (PyCFunction)get_grabbed_window, METH_NOARGS, DOC_SDL2_VIDEO_GETGRABBEDWINDOW}, + {"_internal_mod_init", (PyCFunction)_window_internal_mod_init, METH_NOARGS, + "auto initialize for _window module"}, + {"_internal_mod_quit", (PyCFunction)_window_internal_mod_quit, METH_NOARGS, + "auto quit for _window module"}, {NULL, NULL, 0, NULL}}; MODINIT_DEFINE(_window) diff --git a/test/window_test.py b/test/window_test.py index e1b7e3e1c4..473d6faa8d 100644 --- a/test/window_test.py +++ b/test/window_test.py @@ -299,6 +299,47 @@ def test_from_display_module(self): pygame.display.quit() pygame.init() + def test_window_surface(self): + win = Window(size=(640, 480)) + surf = win.get_surface() + + self.assertIsInstance(surf, pygame.Surface) + + # test auto resize + self.assertTupleEqual(win.size, surf.get_size()) + win.size = (100, 100) + self.assertTupleEqual(win.size, surf.get_size()) + win.size = (1280, 720) + self.assertTupleEqual(win.size, surf.get_size()) + + # window surface should be invalid after the window is destroyed + win.destroy() + self.assertRaises(pygame.error, lambda: surf.fill((0, 0, 0))) + + def test_window_surface_with_display_module(self): + # get_surface() should raise an error if the set_mode() is not called. + pygame.display.set_mode((640, 480)) + win1 = Window.from_display_module() + pygame.display.quit() + pygame.init() + self.assertRaises(pygame.error, lambda: win1.get_surface()) + + # the surface returned by get_surface() should be + # the surface returned by set_mode() + surf1 = pygame.display.set_mode((640, 480)) + win2 = Window.from_display_module() + surf2 = win2.get_surface() + self.assertIs(surf1, surf2) + + def test_window_update_from_surface(self): + win = Window(size=(640, 480)) + surf = win.get_surface() + surf.fill((255, 0, 0)) + + self.assertRaises(TypeError, lambda: win.update_from_surface("an argument")) + self.assertIs(win.update_from_surface(), None) + win.destroy() + def tearDown(self): self.win.destroy()