Skip to content

Commit 7455ff9

Browse files
committed
Fix mouse tile regressions and improve integer coordinate handling
Refactor Point to be generic to better track what uses int and float types Fix wrong C type in get_mouse_state, added function to samples Added separate integer attributes to mouse events
1 parent 1ab50f7 commit 7455ff9

File tree

4 files changed

+110
-73
lines changed

4 files changed

+110
-73
lines changed

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,19 +9,30 @@ This project adheres to [Semantic Versioning](https://semver.org/) since version
99
### Added
1010

1111
- `tcod.sdl.video.Window` now accepts an SDL WindowID.
12+
- `MouseState.integer_position` and `MouseMotion.integer_motion` to handle cases where integer values are preferred.
1213

1314
### Changed
1415

1516
- Event classes are now more strict with attribute types
1617
- Event class initializers are keyword-only and no longer take a type parameter, with exceptions.
1718
Generally event class initialization is an internal process.
1819
- `MouseButtonEvent` no longer a subclass of `MouseState`.
20+
- `tcod.event.Point` is now a generic type containing `int` or `float` values depending on the context.
21+
- When converting mouse events to tiles:
22+
`MouseState.position` and `MouseMotion.motion` refers to sub-tile coordinates.
23+
`MouseState.integer_position` and `MouseMotion.integer_motion` refers to integer tile coordinates.
1924

2025
### Deprecated
2126

2227
- `Event.type` is deprecated except for special cases such as `ControllerDevice`, `WindowEvent`, etc.
2328
- `MouseButtonEvent.state` is deprecated, replaced by the existing `.button` attribute.
2429

30+
### Fixed
31+
32+
- Fixed incorrect C FFI types inside `tcod.event.get_mouse_state`.
33+
- Fixed regression in mouse event tile coordinates being `float` instead of `int`.
34+
`convert_coordinates_from_window` can be used if sub-tile coordinates were desired.
35+
2536
## [20.1.0] - 2026-02-25
2637

2738
### Added

examples/samples_tcod.py

Lines changed: 11 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1011,7 +1011,6 @@ class MouseSample(Sample):
10111011

10121012
def __init__(self) -> None:
10131013
self.motion = tcod.event.MouseMotion()
1014-
self.mouse_left = self.mouse_middle = self.mouse_right = 0
10151014
self.log: list[str] = []
10161015

10171016
def on_enter(self) -> None:
@@ -1022,15 +1021,18 @@ def on_enter(self) -> None:
10221021

10231022
def on_draw(self) -> None:
10241023
sample_console.clear(bg=GREY)
1024+
mouse_state = tcod.event.get_mouse_state()
10251025
sample_console.print(
10261026
1,
10271027
1,
1028-
f"Pixel position : {self.motion.position.x:4.0f}x{self.motion.position.y:4.0f}\n"
1029-
f"Tile position : {self.motion.tile.x:4.0f}x{self.motion.tile.y:4.0f}\n"
1030-
f"Tile movement : {self.motion.tile_motion.x:4.0f}x{self.motion.tile_motion.y:4.0f}\n"
1031-
f"Left button : {'ON' if self.mouse_left else 'OFF'}\n"
1032-
f"Right button : {'ON' if self.mouse_right else 'OFF'}\n"
1033-
f"Middle button : {'ON' if self.mouse_middle else 'OFF'}\n",
1028+
f"Pixel position : {mouse_state.position.x:4.0f}x{mouse_state.position.y:4.0f}\n"
1029+
f"Tile position : {self.motion.tile.x:4d}x{self.motion.tile.y:4d}\n"
1030+
f"Tile movement : {self.motion.tile_motion.x:4d}x{self.motion.tile_motion.y:4d}\n"
1031+
f"Left button : {'ON' if mouse_state.state & tcod.event.MouseButtonMask.LEFT else 'OFF'}\n"
1032+
f"Middle button : {'ON' if mouse_state.state & tcod.event.MouseButtonMask.MIDDLE else 'OFF'}\n"
1033+
f"Right button : {'ON' if mouse_state.state & tcod.event.MouseButtonMask.RIGHT else 'OFF'}\n"
1034+
f"X1 button : {'ON' if mouse_state.state & tcod.event.MouseButtonMask.X1 else 'OFF'}\n"
1035+
f"X2 button : {'ON' if mouse_state.state & tcod.event.MouseButtonMask.X2 else 'OFF'}\n",
10341036
fg=LIGHT_YELLOW,
10351037
bg=None,
10361038
)
@@ -1046,18 +1048,6 @@ def on_event(self, event: tcod.event.Event) -> None:
10461048
match event:
10471049
case tcod.event.MouseMotion():
10481050
self.motion = event
1049-
case tcod.event.MouseButtonDown(button=tcod.event.MouseButton.LEFT):
1050-
self.mouse_left = True
1051-
case tcod.event.MouseButtonDown(button=tcod.event.MouseButton.MIDDLE):
1052-
self.mouse_middle = True
1053-
case tcod.event.MouseButtonDown(button=tcod.event.MouseButton.RIGHT):
1054-
self.mouse_right = True
1055-
case tcod.event.MouseButtonUp(button=tcod.event.MouseButton.LEFT):
1056-
self.mouse_left = False
1057-
case tcod.event.MouseButtonUp(button=tcod.event.MouseButton.MIDDLE):
1058-
self.mouse_middle = False
1059-
case tcod.event.MouseButtonUp(button=tcod.event.MouseButton.RIGHT):
1060-
self.mouse_right = False
10611051
case tcod.event.KeyDown(sym=KeySym.N1):
10621052
tcod.sdl.mouse.show(visible=False)
10631053
case tcod.event.KeyDown(sym=KeySym.N2):
@@ -1422,8 +1412,8 @@ def handle_events() -> None:
14221412
tile_event = tcod.event.convert_coordinates_from_window(event, context, root_console)
14231413
SAMPLES[cur_sample].on_event(tile_event)
14241414
match tile_event:
1425-
case tcod.event.MouseMotion(position=(x, y)):
1426-
mouse_tile_xy = int(x), int(y)
1415+
case tcod.event.MouseMotion(integer_position=(x, y)):
1416+
mouse_tile_xy = x, y
14271417
case tcod.event.WindowEvent(type="WindowLeave"):
14281418
mouse_tile_xy = -1, -1
14291419

tcod/context.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import pickle
3030
import sys
3131
import warnings
32+
from math import floor
3233
from pathlib import Path
3334
from typing import TYPE_CHECKING, Any, Literal, NoReturn, TypeVar
3435

@@ -256,16 +257,22 @@ def convert_event(self, event: _Event) -> _Event:
256257
event_copy = copy.copy(event)
257258
if isinstance(event, (tcod.event.MouseState, tcod.event.MouseMotion)):
258259
assert isinstance(event_copy, (tcod.event.MouseState, tcod.event.MouseMotion))
259-
event_copy.position = event._tile = tcod.event.Point(*self.pixel_to_tile(*event.position))
260+
event_copy.position = tcod.event.Point(*self.pixel_to_tile(event.position[0], event.position[1]))
261+
event._tile = tcod.event.Point(floor(event_copy.position[0]), floor(event_copy.position[1]))
260262
if isinstance(event, tcod.event.MouseMotion):
261263
assert isinstance(event_copy, tcod.event.MouseMotion)
262264
assert event._tile is not None
263265
prev_tile = self.pixel_to_tile(
264266
event.position[0] - event.motion[0],
265267
event.position[1] - event.motion[1],
266268
)
267-
event_copy.motion = event._tile_motion = tcod.event.Point(
268-
int(event._tile[0]) - int(prev_tile[0]), int(event._tile[1]) - int(prev_tile[1])
269+
event_copy.motion = tcod.event.Point(
270+
event_copy.position[0] - prev_tile[0],
271+
event_copy.position[1] - prev_tile[1],
272+
)
273+
event._tile_motion = tcod.event.Point(
274+
event._tile[0] - floor(prev_tile[0]),
275+
event._tile[1] - floor(prev_tile[1]),
269276
)
270277
return event_copy
271278

tcod/event.py

Lines changed: 78 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@
8787
import sys
8888
import warnings
8989
from collections.abc import Callable, Iterator, Mapping
90+
from math import floor
9091
from typing import TYPE_CHECKING, Any, Final, Generic, Literal, NamedTuple, TypeAlias, TypeVar, overload
9192

9293
import attrs
@@ -149,16 +150,16 @@ def _describe_bitmask(bits: int, table: Mapping[int, str], default: str = "0") -
149150
return "|".join(result)
150151

151152

152-
def _pixel_to_tile(x: float, y: float) -> tuple[float, float] | None:
153+
def _pixel_to_tile(xy: tuple[float, float], /) -> Point[float] | None:
153154
"""Convert pixel coordinates to tile coordinates."""
154155
if not lib.TCOD_ctx.engine:
155156
return None
156-
xy = ffi.new("double[2]", (x, y))
157-
lib.TCOD_sys_pixel_to_tile(xy, xy + 1)
158-
return xy[0], xy[1]
157+
xy_out = ffi.new("double[2]", xy)
158+
lib.TCOD_sys_pixel_to_tile(xy_out, xy_out + 1)
159+
return Point(float(xy_out[0]), float(xy_out[1]))
159160

160161

161-
class Point(NamedTuple):
162+
class Point(NamedTuple, Generic[T]):
162163
"""A 2D position used for events with mouse coordinates.
163164
164165
.. seealso::
@@ -168,13 +169,13 @@ class Point(NamedTuple):
168169
Now uses floating point coordinates due to the port to SDL3.
169170
"""
170171

171-
x: float
172+
x: T
172173
"""A pixel or tile coordinate starting with zero as the left-most position."""
173-
y: float
174+
y: T
174175
"""A pixel or tile coordinate starting with zero as the top-most position."""
175176

176177

177-
def _verify_tile_coordinates(xy: Point | None) -> Point:
178+
def _verify_tile_coordinates(xy: Point[int] | None) -> Point[int]:
178179
"""Check if an events tile coordinate is initialized and warn if not.
179180
180181
Always returns a valid Point object for backwards compatibility.
@@ -399,36 +400,50 @@ class MouseState(Event):
399400
Renamed `pixel` attribute to `position`.
400401
"""
401402

402-
position: Point = attrs.field(default=Point(0, 0))
403+
position: Point[float] = attrs.field(default=Point(0.0, 0.0))
403404
"""The position coordinates of the mouse."""
404-
_tile: Point | None = attrs.field(default=Point(0, 0), alias="tile")
405-
"""The integer tile coordinates of the mouse on the screen."""
405+
_tile: Point[int] | None = attrs.field(default=Point(0, 0), alias="tile")
406+
406407
state: MouseButtonMask = attrs.field(default=MouseButtonMask(0))
407408
"""A bitmask of which mouse buttons are currently held."""
408409

410+
@property
411+
def integer_position(self) -> Point[int]:
412+
"""Integer coordinates of this event.
413+
414+
.. versionadded:: Unreleased
415+
"""
416+
x, y = self.position
417+
return Point(floor(x), floor(y))
418+
409419
@property
410420
@deprecated("The mouse.pixel attribute is deprecated. Use mouse.position instead.")
411-
def pixel(self) -> Point:
421+
def pixel(self) -> Point[float]:
412422
return self.position
413423

414424
@pixel.setter
415-
def pixel(self, value: Point) -> None:
425+
def pixel(self, value: Point[float]) -> None:
416426
self.position = value
417427

418428
@property
419429
@deprecated(
420430
"The mouse.tile attribute is deprecated."
421-
" Use mouse.position of the event returned by context.convert_event instead."
431+
" Use mouse.integer_position of the event returned by context.convert_event instead."
422432
)
423-
def tile(self) -> Point:
433+
def tile(self) -> Point[int]:
434+
"""The integer tile coordinates of the mouse on the screen.
435+
436+
.. deprecated:: Unreleased
437+
Use :any:`integer_position` of the event returned by :any:`Context.convert_event` instead.
438+
"""
424439
return _verify_tile_coordinates(self._tile)
425440

426441
@tile.setter
427442
@deprecated(
428443
"The mouse.tile attribute is deprecated."
429-
" Use mouse.position of the event returned by context.convert_event instead."
444+
" Use mouse.integer_position of the event returned by context.convert_event instead."
430445
)
431-
def tile(self, xy: tuple[float, float]) -> None:
446+
def tile(self, xy: tuple[int, int]) -> None:
432447
self._tile = Point(*xy)
433448

434449

@@ -444,52 +459,67 @@ class MouseMotion(MouseState):
444459
`position` and `motion` now use floating point coordinates.
445460
"""
446461

447-
motion: Point = attrs.field(default=Point(0, 0))
462+
motion: Point[float] = attrs.field(default=Point(0.0, 0.0))
448463
"""The pixel delta."""
449-
_tile_motion: Point | None = attrs.field(default=Point(0, 0), alias="tile_motion")
450-
"""The tile delta."""
464+
_tile_motion: Point[int] | None = attrs.field(default=Point(0, 0), alias="tile_motion")
465+
466+
@property
467+
def integer_motion(self) -> Point[int]:
468+
"""Integer motion of this event.
469+
470+
.. versionadded:: Unreleased
471+
"""
472+
x, y = self.position
473+
dx, dy = self.motion
474+
prev_x, prev_y = x - dx, y - dy
475+
return Point(floor(x) - floor(prev_x), floor(y) - floor(prev_y))
451476

452477
@property
453478
@deprecated("The mouse.pixel_motion attribute is deprecated. Use mouse.motion instead.")
454-
def pixel_motion(self) -> Point:
479+
def pixel_motion(self) -> Point[float]:
455480
return self.motion
456481

457482
@pixel_motion.setter
458483
@deprecated("The mouse.pixel_motion attribute is deprecated. Use mouse.motion instead.")
459-
def pixel_motion(self, value: Point) -> None:
484+
def pixel_motion(self, value: Point[float]) -> None:
460485
self.motion = value
461486

462487
@property
463488
@deprecated(
464489
"The mouse.tile_motion attribute is deprecated."
465-
" Use mouse.motion of the event returned by context.convert_event instead."
490+
" Use mouse.integer_motion of the event returned by context.convert_event instead."
466491
)
467-
def tile_motion(self) -> Point:
492+
def tile_motion(self) -> Point[int]:
493+
"""The tile delta.
494+
495+
.. deprecated:: Unreleased
496+
Use :any:`integer_motion` of the event returned by :any:`Context.convert_event` instead.
497+
"""
468498
return _verify_tile_coordinates(self._tile_motion)
469499

470500
@tile_motion.setter
471501
@deprecated(
472502
"The mouse.tile_motion attribute is deprecated."
473-
" Use mouse.motion of the event returned by context.convert_event instead."
503+
" Use mouse.integer_motion of the event returned by context.convert_event instead."
474504
)
475-
def tile_motion(self, xy: tuple[float, float]) -> None:
505+
def tile_motion(self, xy: tuple[int, int]) -> None:
476506
self._tile_motion = Point(*xy)
477507

478508
@classmethod
479509
def _from_sdl_event(cls, sdl_event: _C_SDL_Event) -> Self:
480510
motion = sdl_event.motion
481511
state = MouseButtonMask(motion.state)
482512

483-
pixel = Point(motion.x, motion.y)
484-
pixel_motion = Point(motion.xrel, motion.yrel)
485-
subtile = _pixel_to_tile(*pixel)
513+
pixel = Point(float(motion.x), float(motion.y))
514+
pixel_motion = Point(float(motion.xrel), float(motion.yrel))
515+
subtile = _pixel_to_tile(pixel)
486516
if subtile is None:
487517
self = cls(position=pixel, motion=pixel_motion, tile=None, tile_motion=None, state=state)
488518
else:
489-
tile = Point(int(subtile[0]), int(subtile[1]))
490-
prev_pixel = pixel[0] - pixel_motion[0], pixel[1] - pixel_motion[1]
491-
prev_subtile = _pixel_to_tile(*prev_pixel) or (0, 0)
492-
prev_tile = int(prev_subtile[0]), int(prev_subtile[1])
519+
tile = Point(floor(subtile[0]), floor(subtile[1]))
520+
prev_pixel = (pixel[0] - pixel_motion[0], pixel[1] - pixel_motion[1])
521+
prev_subtile = _pixel_to_tile(prev_pixel) or (0, 0)
522+
prev_tile = floor(prev_subtile[0]), floor(prev_subtile[1])
493523
tile_motion = Point(tile[0] - prev_tile[0], tile[1] - prev_tile[1])
494524
self = cls(position=pixel, motion=pixel_motion, tile=tile, tile_motion=tile_motion, state=state)
495525
self.sdl_event = sdl_event
@@ -507,10 +537,10 @@ class MouseButtonEvent(Event):
507537
No longer a subclass of :any:`MouseState`.
508538
"""
509539

510-
position: Point = attrs.field(default=Point(0, 0))
540+
position: Point[float] = attrs.field(default=Point(0.0, 0.0))
511541
"""The pixel coordinates of the mouse."""
512-
_tile: Point | None = attrs.field(default=Point(0, 0), alias="tile")
513-
"""The tile coordinates of the mouse on the screen."""
542+
_tile: Point[int] | None = attrs.field(default=Point(0, 0), alias="tile")
543+
"""The tile integer coordinates of the mouse on the screen. Deprecated."""
514544
button: MouseButton
515545
"""Which mouse button index was pressed or released in this event.
516546
@@ -521,12 +551,12 @@ class MouseButtonEvent(Event):
521551
@classmethod
522552
def _from_sdl_event(cls, sdl_event: _C_SDL_Event) -> Self:
523553
button = sdl_event.button
524-
pixel = Point(button.x, button.y)
525-
subtile = _pixel_to_tile(*pixel)
554+
pixel = Point(float(button.x), float(button.y))
555+
subtile = _pixel_to_tile(pixel)
526556
if subtile is None:
527-
tile: Point | None = None
557+
tile: Point[int] | None = None
528558
else:
529-
tile = Point(float(subtile[0]), float(subtile[1]))
559+
tile = Point(floor(subtile[0]), floor(subtile[1]))
530560
self = cls(position=pixel, tile=tile, button=MouseButton(button.button))
531561
self.sdl_event = sdl_event
532562
return self
@@ -1362,12 +1392,12 @@ def get_mouse_state() -> MouseState:
13621392
13631393
.. versionadded:: 9.3
13641394
"""
1365-
xy = ffi.new("int[2]")
1395+
xy = ffi.new("float[2]")
13661396
buttons = lib.SDL_GetMouseState(xy, xy + 1)
1367-
tile = _pixel_to_tile(*xy)
1397+
tile = _pixel_to_tile(tuple(xy))
13681398
if tile is None:
13691399
return MouseState(position=Point(xy[0], xy[1]), tile=None, state=buttons)
1370-
return MouseState(position=Point(xy[0], xy[1]), tile=Point(int(tile[0]), int(tile[1])), state=buttons)
1400+
return MouseState(position=Point(xy[0], xy[1]), tile=Point(floor(tile[0]), floor(tile[1])), state=buttons)
13711401

13721402

13731403
@overload
@@ -1431,14 +1461,13 @@ def convert_coordinates_from_window(
14311461
((event.position[0] - event.motion[0]), (event.position[1] - event.motion[1])), context, console, dest_rect
14321462
)
14331463
position = convert_coordinates_from_window(event.position, context, console, dest_rect)
1434-
event.motion = tcod.event.Point(position[0] - previous_position[0], position[1] - previous_position[1])
1435-
event._tile_motion = tcod.event.Point(
1436-
int(position[0]) - int(previous_position[0]), int(position[1]) - int(previous_position[1])
1464+
event.motion = Point(position[0] - previous_position[0], position[1] - previous_position[1])
1465+
event._tile_motion = Point(
1466+
floor(position[0]) - floor(previous_position[0]), floor(position[1]) - floor(previous_position[1])
14371467
)
14381468
if isinstance(event, (MouseState, MouseMotion)):
1439-
event.position = event._tile = tcod.event.Point(
1440-
*convert_coordinates_from_window(event.position, context, console, dest_rect)
1441-
)
1469+
event.position = Point(*convert_coordinates_from_window(event.position, context, console, dest_rect))
1470+
event._tile = Point(floor(event.position[0]), floor(event.position[1]))
14421471
return event
14431472

14441473

0 commit comments

Comments
 (0)