diff --git a/buildconfig/stubs/pygame/_window.pyi b/buildconfig/stubs/pygame/_window.pyi index 9697f5e566..dd3eeb97f4 100644 --- a/buildconfig/stubs/pygame/_window.pyi +++ b/buildconfig/stubs/pygame/_window.pyi @@ -1,4 +1,4 @@ -from typing import Optional, Tuple, Union, final +from typing import Literal, Optional, Tuple, Union, final from pygame._common import Coordinate, RectValue from pygame.locals import WINDOWPOS_UNDEFINED @@ -28,6 +28,22 @@ class Window: def set_icon(self, icon: Surface) -> None: ... def get_surface(self) -> Surface: ... def flip(self) -> None: ... + def add_draggable_hit_test(self, hit_rect: RectValue) -> None: ... + def add_resize_hit_test( + self, + hit_rect: RectValue, + orientation: Literal[ + 'topleft', + 'left', + 'bottomleft', + 'bottom', + 'bottomright', + 'right', + 'topright', + 'top' + ] + ) -> None: ... + def clear_hit_test(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 729404d6a5..47aa33c32d 100644 --- a/docs/reST/ref/sdl2_video.rst +++ b/docs/reST/ref/sdl2_video.rst @@ -349,6 +349,76 @@ .. versionadded:: 2.4.0 + .. method:: add_draggable_hit_test + + | :sl:`Add a draggable hit test` + | :sg:`add_draggable_hit_test(hit_rect) -> None` + + :param rect hit_rect: The rect for hit test. + :raises pygame.error: If platform don't support this functionality. + + Normally windows are dragged and resized by decorations provided by the + system window manager (a title bar, borders, etc), but for some apps, it + makes sense to drag them from somewhere else inside the window itself; for + example, one might have a borderless window that wants to be draggable from + any part, or simulate its own title bar, etc. + + This function allows you to create a "draggable hit test" rect on the window. + That means when user drags this rect, the whole window is dragged and moved. + + Multiple hit tests can be added by calling this function multiple times. + Use :func:`clear_hit_test` to remove all hit tests. + + A ``WINDOWHITTEST`` event will be pushed to the event queue when a hit test + is triggered. + + Mouse input may not be delivered to your application if it is within a + special area; the OS will often apply that input to moving the window or + resizing the window and not deliver it to the application. + + .. seealso:: :func:`add_resize_hit_test` + .. seealso:: :func:`clear_hit_test`. + .. versionadded:: 2.4.0 + + .. method:: add_resize_hit_test + + | :sl:`Add a resize hit test` + | :sg:`add_resize_hit_test(hit_rect, orientation) -> None` + + :param rect hit_rect: The rect for hit test. + :param str orientation: The orientation of resie. + :raises pygame.error: If platform don't support this functionality. + + This function allows you to create a "resize hit test" rect. By dragging + this rect, user can resize the window (the window should be resizable). + Use ``orientation`` parameter to specify the orientation of resize. + + The ``orientation`` can be ``"topleft"``, ``"left"``, ``"bottomleft"``, + ``"bottom"``, ``"bottomright"``, ``"right"``, ``"topright"`` or ``"top"``. + + Multiple hit tests can be added by calling this function multiple times. + Use :func:`clear_hit_test` to remove all hit tests. + + For details about hit test, see :func:`add_draggable_hit_test` + + .. seealso:: :func:`add_draggable_hit_test` + .. seealso:: :func:`clear_hit_test` + .. versionadded:: 2.4.0 + + .. method:: clear_hit_test + + | :sl:`Clear all hit tests` + | :sg:`clear_hit_test() -> None` + + :raises pygame.error: If platform don't support this functionality. + + Clear all hit tests created by :func:`add_draggable_hit_test` + and :func:`add_resize_hit_test` on this window. + + .. seealso:: :func:`add_draggable_hit_test` + .. seealso:: :func:`add_resize_hit_test` + .. versionadded:: 2.4.0 + .. method:: set_windowed | :sl:`Enable windowed mode (exit fullscreen)` diff --git a/src_c/doc/sdl2_video_doc.h b/src_c/doc/sdl2_video_doc.h index a6a8f0229a..4ebe55cc36 100644 --- a/src_c/doc/sdl2_video_doc.h +++ b/src_c/doc/sdl2_video_doc.h @@ -24,6 +24,9 @@ #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_FLIP "flip() -> None\nUpdate the display surface to the window." +#define DOC_SDL2_VIDEO_WINDOW_ADDDRAGGABLEHITTEST "add_draggable_hit_test(hit_rect) -> None\nAdd a draggable hit test" +#define DOC_SDL2_VIDEO_WINDOW_ADDRESIZEHITTEST "add_resize_hit_test(hit_rect, orientation) -> None\nAdd a resize hit test" +#define DOC_SDL2_VIDEO_WINDOW_CLEARHITTEST "clear_hit_test() -> None\nClear all hit tests" #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 4199119271..3ee62f2e20 100644 --- a/src_c/include/_pygame.h +++ b/src_c/include/_pygame.h @@ -490,10 +490,18 @@ typedef struct pgColorObject pgColorObject; /* * Window module */ + +typedef struct { + SDL_Rect hit_area; + SDL_HitTestResult hit_type; +} pgWindowHitTestData; + typedef struct { PyObject_HEAD SDL_Window *_win; SDL_bool _is_borrowed; pgSurfaceObject *surf; + pgWindowHitTestData *hit_test_data; + int num_hit_test_data; } pgWindowObject; #ifndef PYGAMEAPI_WINDOW_INTERNAL diff --git a/src_c/window.c b/src_c/window.c index 0f8536abe6..3c7381a5a1 100644 --- a/src_c/window.c +++ b/src_c/window.c @@ -96,6 +96,11 @@ static PyTypeObject pgWindow_Type; #define pgWindow_Check(x) \ (PyObject_IsInstance((x), (PyObject *)&pgWindow_Type)) +#define WINDOW_FREE_HIT_TEST_DATA(pg_window) \ + free(pg_window->hit_test_data); \ + pg_window->hit_test_data = NULL; \ + pg_window->num_hit_test_data = 0; + static PyObject * get_grabbed_window(PyObject *self, PyObject *_null) { @@ -711,6 +716,117 @@ window_get_display_index(pgWindowObject *self, PyObject *_null) return PyLong_FromLong(index); } +static SDL_HitTestResult +_window_hit_test_callback(SDL_Window *win, const SDL_Point *point, void *data) +{ + pgWindowObject *pg_win = SDL_GetWindowData(win, "pg_window"); + if (pg_win == NULL) + return SDL_HITTEST_NORMAL; + + pgWindowHitTestData *hit_test_data = pg_win->hit_test_data; + if (hit_test_data == NULL) + return SDL_HITTEST_NORMAL; + + for (int i = 0; i < pg_win->num_hit_test_data; i++) { + if (SDL_PointInRect(point, &(hit_test_data[i].hit_area))) + return hit_test_data[i].hit_type; + } + + return SDL_HITTEST_NORMAL; +} + +static PyObject * +_window_add_hit_test(pgWindowObject *self, PyObject *hit_pg_rect, + SDL_HitTestResult hit_type) +{ + SDL_Rect tmp_rect; + SDL_Rect *hit_rect; + SDL_bool need_set_hittest = SDL_FALSE; + + if (self->num_hit_test_data == 0) + need_set_hittest = SDL_TRUE; + + hit_rect = pgRect_FromObject(hit_pg_rect, &tmp_rect); + if (!hit_rect) + return RAISE(PyExc_TypeError, "area should be a rect-like object."); + + pgWindowHitTestData *tmp = + realloc(self->hit_test_data, + (self->num_hit_test_data + 1) * sizeof(pgWindowHitTestData)); + if (!tmp) + return PyErr_NoMemory(); + + self->hit_test_data = tmp; + self->num_hit_test_data++; + self->hit_test_data[self->num_hit_test_data - 1].hit_area = *hit_rect; + self->hit_test_data[self->num_hit_test_data - 1].hit_type = hit_type; + + if (need_set_hittest) { + int result = + SDL_SetWindowHitTest(self->_win, _window_hit_test_callback, NULL); + if (result != 0) + return RAISE(pgExc_SDLError, SDL_GetError()); + } + + Py_RETURN_NONE; +} + +static PyObject * +window_add_resize_hit_test(pgWindowObject *self, PyObject *args, + PyObject *kwargs) +{ + PyObject *hit_rect = NULL; + SDL_HitTestResult hit_type; + char *orientation_str; + char *keywords[] = {"hit_rect", "orientation", NULL}; + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "Os", keywords, &hit_rect, + &orientation_str)) + return NULL; + + if (!strcmp(orientation_str, "topleft")) + hit_type = SDL_HITTEST_RESIZE_TOPLEFT; + else if (!strcmp(orientation_str, "left")) + hit_type = SDL_HITTEST_RESIZE_LEFT; + else if (!strcmp(orientation_str, "bottomleft")) + hit_type = SDL_HITTEST_RESIZE_BOTTOMLEFT; + else if (!strcmp(orientation_str, "bottom")) + hit_type = SDL_HITTEST_RESIZE_BOTTOM; + else if (!strcmp(orientation_str, "bottomright")) + hit_type = SDL_HITTEST_RESIZE_BOTTOMRIGHT; + else if (!strcmp(orientation_str, "right")) + hit_type = SDL_HITTEST_RESIZE_RIGHT; + else if (!strcmp(orientation_str, "topright")) + hit_type = SDL_HITTEST_RESIZE_TOPRIGHT; + else if (!strcmp(orientation_str, "top")) + hit_type = SDL_HITTEST_RESIZE_TOP; + else + return RAISE(PyExc_TypeError, + "orientation should be 'topleft', 'left', 'bottomleft', " + "'bottom', 'bottomright', 'right', 'topright' or 'top'"); + return _window_add_hit_test(self, hit_rect, hit_type); +} + +static PyObject * +window_add_draggable_hit_test(pgWindowObject *self, PyObject *args, + PyObject *kwargs) +{ + PyObject *hit_rect = NULL; + char *keywords[] = {"hit_rect", NULL}; + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O", keywords, &hit_rect)) + return NULL; + return _window_add_hit_test(self, hit_rect, SDL_HITTEST_DRAGGABLE); +} + +static PyObject * +window_clear_hit_test(pgWindowObject *self) +{ + int result = SDL_SetWindowHitTest(self->_win, NULL, NULL); + if (result != 0) + return RAISE(pgExc_SDLError, SDL_GetError()); + WINDOW_FREE_HIT_TEST_DATA(self); + Py_RETURN_NONE; +} + static void window_dealloc(pgWindowObject *self, PyObject *_null) { @@ -731,6 +847,8 @@ window_dealloc(pgWindowObject *self, PyObject *_null) Py_DECREF(self->surf); } + WINDOW_FREE_HIT_TEST_DATA(self); + Py_TYPE(self)->tp_free(self); } @@ -932,6 +1050,8 @@ window_init(pgWindowObject *self, PyObject *args, PyObject *kwargs) self->_win = _win; self->_is_borrowed = SDL_FALSE; self->surf = NULL; + self->hit_test_data = NULL; + self->num_hit_test_data = 0; SDL_SetWindowData(_win, "pg_window", self); @@ -971,6 +1091,8 @@ window_from_display_module(PyTypeObject *cls, PyObject *_null) self = (pgWindowObject *)(cls->tp_new(cls, NULL, NULL)); self->_win = window; self->_is_borrowed = SDL_TRUE; + self->hit_test_data = NULL; + self->num_hit_test_data = 0; SDL_SetWindowData(window, "pg_window", self); return (PyObject *)self; } @@ -1038,6 +1160,12 @@ static PyMethodDef window_methods[] = { DOC_SDL2_VIDEO_WINDOW_FLIP}, {"get_surface", (PyCFunction)window_get_surface, METH_NOARGS, DOC_SDL2_VIDEO_WINDOW_GETSURFACE}, + {"add_draggable_hit_test", (PyCFunction)window_add_draggable_hit_test, + METH_VARARGS | METH_KEYWORDS, DOC_SDL2_VIDEO_WINDOW_ADDDRAGGABLEHITTEST}, + {"add_resize_hit_test", (PyCFunction)window_add_resize_hit_test, + METH_VARARGS | METH_KEYWORDS, DOC_SDL2_VIDEO_WINDOW_ADDRESIZEHITTEST}, + {"clear_hit_test", (PyCFunction)window_clear_hit_test, METH_NOARGS, + DOC_SDL2_VIDEO_WINDOW_CLEARHITTEST}, {"from_display_module", (PyCFunction)window_from_display_module, METH_CLASS | METH_NOARGS, DOC_SDL2_VIDEO_WINDOW_FROMDISPLAYMODULE}, {NULL, NULL, 0, NULL}};