Skip to content

Port SDL_SetWindowHitTest #2582

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

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
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
18 changes: 17 additions & 1 deletion buildconfig/stubs/pygame/_window.pyi
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
Expand Down
70 changes: 70 additions & 0 deletions docs/reST/ref/sdl2_video.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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)`
Expand Down
3 changes: 3 additions & 0 deletions src_c/doc/sdl2_video_doc.h
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
8 changes: 8 additions & 0 deletions src_c/include/_pygame.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
128 changes: 128 additions & 0 deletions src_c/window.c
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down Expand Up @@ -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)
{
Expand All @@ -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);
}

Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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}};
Expand Down