Skip to content

Commit 76c3786

Browse files
committed
Add better mouse coordinate conversion functions
Update logical_size property to return None instead of (0, 0), None is a more expected return value for this state. Update samples to highlight the tile under the mouse
1 parent 57cb18f commit 76c3786

File tree

4 files changed

+137
-28
lines changed

4 files changed

+137
-28
lines changed

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,15 @@ This project adheres to [Semantic Versioning](https://semver.org/) since version
66

77
## [Unreleased]
88

9+
### Added
10+
11+
- Added methods: `Renderer.coordinates_from_window` and `Renderer.coordinates_to_window`
12+
- Added `tcod.event.convert_coordinates_from_window`.
13+
14+
### Changed
15+
16+
- `Renderer.logical_size` now returns `None` instead of `(0, 0)` when logical size is unset.
17+
918
## [19.6.3] - 2026-01-12
1019

1120
Fix missing deployment

examples/samples_tcod.py

Lines changed: 14 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1379,6 +1379,8 @@ def redraw_display() -> None:
13791379
SAMPLES[cur_sample].on_draw()
13801380
sample_console.blit(root_console, SAMPLE_SCREEN_X, SAMPLE_SCREEN_Y)
13811381
draw_stats()
1382+
if 0 <= mouse_tile_xy[0] < root_console.width and 0 <= mouse_tile_xy[1] < root_console.height:
1383+
root_console.rgb[["fg", "bg"]].T[mouse_tile_xy] = (0, 0, 0), (255, 255, 255) # Highlight mouse tile
13821384
if context.sdl_renderer:
13831385
# Clear the screen to ensure no garbage data outside of the logical area is displayed
13841386
context.sdl_renderer.draw_color = (0, 0, 0, 255)
@@ -1410,25 +1412,20 @@ def handle_time() -> None:
14101412
frame_length.append(frame_times[-1] - frame_times[-2])
14111413

14121414

1415+
mouse_tile_xy = (-1, -1)
1416+
"""Last known mouse tile position."""
1417+
1418+
14131419
def handle_events() -> None:
1420+
global mouse_tile_xy
14141421
for event in tcod.event.get():
1415-
if context.sdl_renderer: # Manual handing of tile coordinates since context.present is skipped
1416-
assert context.sdl_window
1417-
tile_width = context.sdl_window.size[0] / root_console.width
1418-
tile_height = context.sdl_window.size[1] / root_console.height
1419-
1420-
if isinstance(event, (tcod.event.MouseState, tcod.event.MouseMotion)):
1421-
event.tile = tcod.event.Point(event.position.x // tile_width, event.position.y // tile_height)
1422-
if isinstance(event, tcod.event.MouseMotion):
1423-
prev_tile = (
1424-
(event.position[0] - event.motion[0]) // tile_width,
1425-
(event.position[1] - event.motion[1]) // tile_height,
1426-
)
1427-
event.tile_motion = tcod.event.Point(event.tile[0] - prev_tile[0], event.tile[1] - prev_tile[1])
1428-
else:
1429-
context.convert_event(event)
1430-
1431-
SAMPLES[cur_sample].on_event(event)
1422+
tile_event = tcod.event.convert_coordinates_from_window(event, context, root_console)
1423+
SAMPLES[cur_sample].on_event(tile_event)
1424+
match tile_event:
1425+
case tcod.event.MouseMotion(position=(x, y)):
1426+
mouse_tile_xy = int(x), int(y)
1427+
case tcod.event.WindowEvent(type="WindowLeave"):
1428+
mouse_tile_xy = -1, -1
14321429

14331430

14341431
def draw_samples_menu() -> None:

tcod/event.py

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,13 +87,15 @@
8787
import sys
8888
import warnings
8989
from collections.abc import Callable, Iterator, Mapping
90-
from typing import TYPE_CHECKING, Any, Final, Generic, Literal, NamedTuple, TypeVar
90+
from typing import TYPE_CHECKING, Any, Final, Generic, Literal, NamedTuple, TypeVar, overload
9191

9292
import numpy as np
9393
from typing_extensions import deprecated
9494

95+
import tcod.context
9596
import tcod.event_constants
9697
import tcod.sdl.joystick
98+
import tcod.sdl.render
9799
import tcod.sdl.sys
98100
from tcod.cffi import ffi, lib
99101
from tcod.event_constants import * # noqa: F403
@@ -103,6 +105,7 @@
103105
from numpy.typing import NDArray
104106

105107
T = TypeVar("T")
108+
_EventType = TypeVar("_EventType", bound="Event")
106109

107110

108111
class _ConstantsWithPrefix(Mapping[int, str]):
@@ -1564,6 +1567,78 @@ def get_mouse_state() -> MouseState:
15641567
return MouseState((xy[0], xy[1]), (int(tile[0]), int(tile[1])), buttons)
15651568

15661569

1570+
@overload
1571+
def convert_coordinates_from_window(
1572+
event: _EventType,
1573+
/,
1574+
context: tcod.context.Context | tcod.sdl.render.Renderer,
1575+
console: tcod.console.Console | tuple[int, int],
1576+
dest_rect: tuple[int, int, int, int] | None = None,
1577+
) -> _EventType: ...
1578+
@overload
1579+
def convert_coordinates_from_window(
1580+
xy: tuple[float, float],
1581+
/,
1582+
context: tcod.context.Context | tcod.sdl.render.Renderer,
1583+
console: tcod.console.Console | tuple[int, int],
1584+
dest_rect: tuple[int, int, int, int] | None = None,
1585+
) -> tuple[float, float]: ...
1586+
def convert_coordinates_from_window(
1587+
event: _EventType | tuple[float, float],
1588+
/,
1589+
context: tcod.context.Context | tcod.sdl.render.Renderer,
1590+
console: tcod.console.Console | tuple[int, int],
1591+
dest_rect: tuple[int, int, int, int] | None = None,
1592+
) -> _EventType | tuple[float, float]:
1593+
"""Return an event or position with window mouse coordinates converted into console tile coordinates.
1594+
1595+
Args:
1596+
event: :any:`Event` to convert, or the `(x, y)` coordinates to convert.
1597+
context: Context or Renderer to fetch the SDL renderer from for reference with conversions.
1598+
console: A console used as a size reference.
1599+
Otherwise the `(columns, rows)` can be given directly as a tuple.
1600+
dest_rect: The consoles rendering destination as `(x, y, width, height)`.
1601+
If None is given then the whole rendering target is assumed.
1602+
1603+
.. versionadded:: Unreleased
1604+
"""
1605+
if isinstance(context, tcod.context.Context):
1606+
maybe_renderer: Final = context.sdl_renderer
1607+
if maybe_renderer is None:
1608+
return event
1609+
context = maybe_renderer
1610+
1611+
if isinstance(console, tcod.console.Console):
1612+
console = console.width, console.height
1613+
1614+
if dest_rect is None:
1615+
dest_rect = (0, 0, *(context.logical_size or context.output_size))
1616+
1617+
x_scale: Final = console[0] / dest_rect[2]
1618+
y_scale: Final = console[1] / dest_rect[3]
1619+
x_offset: Final = dest_rect[0]
1620+
y_offset: Final = dest_rect[1]
1621+
1622+
if not isinstance(event, Event):
1623+
x, y = context.coordinates_from_window(event)
1624+
return (x - x_offset) * x_scale, (y - y_offset) * y_scale
1625+
1626+
if isinstance(event, MouseMotion):
1627+
previous_position = convert_coordinates_from_window(
1628+
((event.position[0] - event.motion[0]), (event.position[1] - event.motion[1])), context, console, dest_rect
1629+
)
1630+
position = convert_coordinates_from_window(event.position, context, console, dest_rect)
1631+
event.motion = tcod.event.Point(position[0] - previous_position[0], position[1] - previous_position[1])
1632+
event._tile_motion = tcod.event.Point(
1633+
int(position[0]) - int(previous_position[0]), int(position[1]) - int(previous_position[1])
1634+
)
1635+
if isinstance(event, (MouseState, MouseMotion)):
1636+
event.position = event._tile = tcod.event.Point(
1637+
*convert_coordinates_from_window(event.position, context, console, dest_rect)
1638+
)
1639+
return event
1640+
1641+
15671642
@ffi.def_extern() # type: ignore[untyped-decorator]
15681643
def _sdl_event_watcher(userdata: Any, sdl_event: Any) -> int:
15691644
callback: Callable[[Event], None] = ffi.from_handle(userdata)

tcod/sdl/render.py

Lines changed: 38 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -439,16 +439,16 @@ def draw_blend_mode(self, value: int) -> None:
439439

440440
@property
441441
def output_size(self) -> tuple[int, int]:
442-
"""Get the (width, height) pixel resolution of the rendering context.
442+
"""Get the (width, height) pixel resolution of the current rendering context.
443443
444444
.. seealso::
445-
https://wiki.libsdl.org/SDL_GetRendererOutputSize
445+
https://wiki.libsdl.org/SDL3/SDL_GetCurrentRenderOutputSize
446446
447447
.. versionadded:: 13.5
448448
"""
449449
out = ffi.new("int[2]")
450450
_check(lib.SDL_GetCurrentRenderOutputSize(self.p, out, out + 1))
451-
return out[0], out[1]
451+
return tuple(out)
452452

453453
@property
454454
def clip_rect(self) -> tuple[int, int, int, int] | None:
@@ -481,10 +481,8 @@ def set_logical_presentation(self, resolution: tuple[int, int], mode: LogicalPre
481481
_check(lib.SDL_SetRenderLogicalPresentation(self.p, width, height, mode))
482482

483483
@property
484-
def logical_size(self) -> tuple[int, int]:
485-
"""Get current independent (width, height) resolution.
486-
487-
Might be (0, 0) if a resolution was never assigned.
484+
def logical_size(self) -> tuple[int, int] | None:
485+
"""Get current independent (width, height) resolution, or None if logical size is unset.
488486
489487
.. seealso::
490488
https://wiki.libsdl.org/SDL3/SDL_GetRenderLogicalPresentation
@@ -493,10 +491,14 @@ def logical_size(self) -> tuple[int, int]:
493491
494492
.. versionchanged:: 19.0
495493
Setter is deprecated, use :any:`set_logical_presentation` instead.
494+
495+
.. versionchanged:: Unreleased
496+
Return ``None`` instead of ``(0, 0)`` when logical size is disabled.
496497
"""
497498
out = ffi.new("int[2]")
498499
lib.SDL_GetRenderLogicalPresentation(self.p, out, out + 1, ffi.NULL)
499-
return out[0], out[1]
500+
out_tuple = tuple(out)
501+
return None if out_tuple == (0, 0) else out_tuple
500502

501503
@logical_size.setter
502504
@deprecated("Use set_logical_presentation method to correctly setup logical size.")
@@ -509,7 +511,7 @@ def scale(self) -> tuple[float, float]:
509511
"""Get or set an (x_scale, y_scale) multiplier for drawing.
510512
511513
.. seealso::
512-
https://wiki.libsdl.org/SDL_RenderSetScale
514+
https://wiki.libsdl.org/SDL3/SDL_SetRenderScale
513515
514516
.. versionadded:: 13.5
515517
"""
@@ -526,7 +528,7 @@ def viewport(self) -> tuple[int, int, int, int] | None:
526528
"""Get or set the drawing area for the current rendering target.
527529
528530
.. seealso::
529-
https://wiki.libsdl.org/SDL_RenderSetViewport
531+
https://wiki.libsdl.org/SDL3/SDL_SetRenderViewport
530532
531533
.. versionadded:: 13.5
532534
"""
@@ -753,6 +755,32 @@ def geometry(
753755
)
754756
)
755757

758+
def coordinates_from_window(self, xy: tuple[float, float], /) -> tuple[float, float]:
759+
"""Return the renderer coordinates from the given windows coordinates.
760+
761+
.. seealso::
762+
https://wiki.libsdl.org/SDL3/SDL_RenderCoordinatesFromWindow
763+
764+
.. versionadded:: Unreleased
765+
"""
766+
x, y = xy
767+
out_xy = ffi.new("float[2]")
768+
_check(lib.SDL_RenderCoordinatesFromWindow(self.p, x, y, out_xy, out_xy + 1))
769+
return tuple(out_xy)
770+
771+
def coordinates_to_window(self, xy: tuple[float, float], /) -> tuple[float, float]:
772+
"""Return the window coordinates from the given render coordinates.
773+
774+
.. seealso::
775+
https://wiki.libsdl.org/SDL3/SDL_RenderCoordinatesToWindow
776+
777+
.. versionadded:: Unreleased
778+
"""
779+
x, y = xy
780+
out_xy = ffi.new("float[2]")
781+
_check(lib.SDL_RenderCoordinatesToWindow(self.p, x, y, out_xy, out_xy + 1))
782+
return tuple(out_xy)
783+
756784

757785
def new_renderer(
758786
window: tcod.sdl.video.Window,

0 commit comments

Comments
 (0)