Skip to content

Commit 858b630

Browse files
committed
Add tests for SDL.
1 parent 20a190f commit 858b630

File tree

3 files changed

+155
-42
lines changed

3 files changed

+155
-42
lines changed

tcod/sdl/render.py

Lines changed: 51 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import annotations
22

3+
import enum
34
from typing import Any, Optional, Tuple
45

56
import numpy as np
@@ -10,6 +11,17 @@
1011
from tcod.sdl import _check, _check_p
1112

1213

14+
class TextureAccess(enum.IntEnum):
15+
"""Determines how a texture is expected to be used."""
16+
17+
STATIC = lib.SDL_TEXTUREACCESS_STATIC or 0
18+
"""Texture rarely changes."""
19+
STREAMING = lib.SDL_TEXTUREACCESS_STREAMING or 0
20+
"""Texture frequently changes."""
21+
TARGET = lib.SDL_TEXTUREACCESS_TARGET or 0
22+
"""Texture will be used as a render target."""
23+
24+
1325
class Texture:
1426
def __init__(self, sdl_texture_p: Any, sdl_renderer_p: Any = None) -> None:
1527
self.p = sdl_texture_p
@@ -56,7 +68,9 @@ def height(self) -> int:
5668
@property
5769
def alpha_mod(self) -> int:
5870
"""Texture alpha modulate value, can be set to: 0 - 255."""
59-
return int(lib.SDL_GetTextureAlphaMod(self.p))
71+
out = ffi.new("uint8_t*")
72+
_check(lib.SDL_GetTextureAlphaMod(self.p, out))
73+
return int(out[0])
6074

6175
@alpha_mod.setter
6276
def alpha_mod(self, value: int) -> None:
@@ -65,7 +79,9 @@ def alpha_mod(self, value: int) -> None:
6579
@property
6680
def blend_mode(self) -> int:
6781
"""Texture blend mode, can be set."""
68-
return int(lib.SDL_GetTextureBlendMode(self.p))
82+
out = ffi.new("SDL_BlendMode*")
83+
_check(lib.SDL_GetTextureBlendMode(self.p, out))
84+
return int(out[0])
6985

7086
@blend_mode.setter
7187
def blend_mode(self, value: int) -> None:
@@ -101,6 +117,8 @@ class Renderer:
101117
def __init__(self, sdl_renderer_p: Any) -> None:
102118
if ffi.typeof(sdl_renderer_p) is not ffi.typeof("struct SDL_Renderer*"):
103119
raise TypeError(f"Expected a {ffi.typeof('struct SDL_Window*')} type (was {ffi.typeof(sdl_renderer_p)}).")
120+
if not sdl_renderer_p:
121+
raise TypeError("C pointer must not be null.")
104122
self.p = sdl_renderer_p
105123

106124
def __eq__(self, other: Any) -> bool:
@@ -124,34 +142,49 @@ def present(self) -> None:
124142
"""Present the currently rendered image to the screen."""
125143
lib.SDL_RenderPresent(self.p)
126144

145+
def set_render_target(self, texture: Texture) -> _RestoreTargetContext:
146+
"""Change the render target to `texture`, returns a context that will restore the original target when exited."""
147+
restore = _RestoreTargetContext(self)
148+
_check(lib.SDL_SetRenderTarget(self.p, texture.p))
149+
return restore
150+
127151
def new_texture(
128152
self, width: int, height: int, *, format: Optional[int] = None, access: Optional[int] = None
129153
) -> Texture:
130-
"""Allocate and return a new Texture for this renderer."""
154+
"""Allocate and return a new Texture for this renderer.
155+
156+
Args:
157+
width: The pixel width of the new texture.
158+
height: The pixel height of the new texture.
159+
format: The format the new texture.
160+
access: The access mode of the texture. Defaults to :any:`TextureAccess.STATIC`.
161+
See :any:`TextureAccess` for more options.
162+
"""
131163
if format is None:
132164
format = 0
133165
if access is None:
134166
access = int(lib.SDL_TEXTUREACCESS_STATIC)
135167
texture_p = ffi.gc(lib.SDL_CreateTexture(self.p, format, access, width, height), lib.SDL_DestroyTexture)
136168
return Texture(texture_p, self.p)
137169

138-
def set_render_target(self, texture: Texture) -> _RestoreTargetContext:
139-
"""Change the render target to `texture`, returns a context that will restore the original target when exited."""
140-
restore = _RestoreTargetContext(self)
141-
_check(lib.SDL_SetRenderTarget(self.p, texture.p))
142-
return restore
143-
144170
def upload_texture(
145171
self, pixels: NDArray[Any], *, format: Optional[int] = None, access: Optional[int] = None
146172
) -> Texture:
147-
"""Return a new Texture from an array of pixels."""
173+
"""Return a new Texture from an array of pixels.
174+
175+
Args:
176+
pixels: An RGB or RGBA array of pixels in row-major order.
177+
format: The format of `pixels` when it isn't a simple RGB or RGBA array.
178+
access: The access mode of the texture. Defaults to :any:`TextureAccess.STATIC`.
179+
See :any:`TextureAccess` for more options.
180+
"""
148181
if format is None:
149182
assert len(pixels.shape) == 3
150183
assert pixels.dtype == np.uint8
151184
if pixels.shape[2] == 4:
152185
format = int(lib.SDL_PIXELFORMAT_RGBA32)
153186
elif pixels.shape[2] == 3:
154-
format = int(lib.SDL_PIXELFORMAT_RGB32)
187+
format = int(lib.SDL_PIXELFORMAT_RGB24)
155188
else:
156189
raise TypeError(f"Can't determine the format required for an array of shape {pixels.shape}.")
157190

@@ -174,6 +207,13 @@ def new_renderer(
174207
) -> Renderer:
175208
"""Initialize and return a new SDL Renderer.
176209
210+
Args:
211+
window: The window that this renderer will be attached to.
212+
driver: Force SDL to use a specific video driver.
213+
software: If True then a software renderer will be forced. By default a hardware renderer is used.
214+
vsync: If True then Vsync will be enabled.
215+
target_textures: If True then target textures can be used by the renderer.
216+
177217
Example::
178218
179219
# Start by creating a window.

tcod/sdl/video.py

Lines changed: 30 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
"Window",
2222
"new_window",
2323
"get_grabbed_window",
24+
"screen_saver_allowed",
2425
)
2526

2627

@@ -66,10 +67,10 @@ class _TempSurface:
6667

6768
def __init__(self, pixels: ArrayLike) -> None:
6869
self._array: NDArray[np.uint8] = np.ascontiguousarray(pixels, dtype=np.uint8)
69-
if len(self._array) != 3:
70-
raise TypeError("NumPy shape must be 3D [y, x, ch] (got %r)" % (self._array.shape,))
71-
if 3 <= self._array.shape[2] <= 4:
72-
raise TypeError("NumPy array must have RGB or RGBA channels. (got %r)" % (self._array.shape,))
70+
if len(self._array.shape) != 3:
71+
raise TypeError(f"NumPy shape must be 3D [y, x, ch] (got {self._array.shape})")
72+
if not (3 <= self._array.shape[2] <= 4):
73+
raise TypeError(f"NumPy array must have RGB or RGBA channels. (got {self._array.shape})")
7374
self.p = ffi.gc(
7475
lib.SDL_CreateRGBSurfaceFrom(
7576
ffi.from_buffer("void*", self._array),
@@ -95,32 +96,21 @@ def __init__(self, sdl_window_p: Any) -> None:
9596
"sdl_window_p must be %r type (was %r)." % (ffi.typeof("struct SDL_Window*"), ffi.typeof(sdl_window_p))
9697
)
9798
if not sdl_window_p:
98-
raise ValueError("sdl_window_p can not be a null pointer.")
99+
raise TypeError("sdl_window_p can not be a null pointer.")
99100
self.p = sdl_window_p
100101

101102
def __eq__(self, other: Any) -> bool:
102103
return bool(self.p == other.p)
103104

104-
def set_icon(self, image: ArrayLike) -> None:
105+
def set_icon(self, pixels: ArrayLike) -> None:
105106
"""Set the window icon from an image.
106107
107-
`image` is a C memory order RGB or RGBA NumPy array.
108+
Args:
109+
pixels: A row-major array of RGB or RGBA pixel values.
108110
"""
109-
surface = _TempSurface(image)
111+
surface = _TempSurface(pixels)
110112
lib.SDL_SetWindowIcon(self.p, surface.p)
111113

112-
@property
113-
def allow_screen_saver(self) -> bool:
114-
"""Get or set if the operating system is allowed to display a screen saver."""
115-
return bool(lib.SDL_IsScreenSaverEnabled(self.p))
116-
117-
@allow_screen_saver.setter
118-
def allow_screen_saver(self, value: bool) -> None:
119-
if value:
120-
lib.SDL_EnableScreenSaver(self.p)
121-
else:
122-
lib.SDL_DisableScreenSaver(self.p)
123-
124114
@property
125115
def position(self) -> Tuple[int, int]:
126116
"""Get or set the (x, y) position of the window.
@@ -180,11 +170,11 @@ def max_size(self, xy: Tuple[int, int]) -> None:
180170
@property
181171
def title(self) -> str:
182172
"""Get or set the title of the window."""
183-
return str(ffi.string(lib.SDL_GetWindowtitle(self.p)), encoding="utf-8")
173+
return str(ffi.string(lib.SDL_GetWindowTitle(self.p)), encoding="utf-8")
184174

185175
@title.setter
186176
def title(self, value: str) -> None:
187-
lib.SDL_SetWindowtitle(self.p, value.encode("utf-8"))
177+
lib.SDL_SetWindowTitle(self.p, value.encode("utf-8"))
188178

189179
@property
190180
def flags(self) -> WindowFlags:
@@ -303,6 +293,15 @@ def new_window(
303293
) -> Window:
304294
"""Initialize and return a new SDL Window.
305295
296+
Args:
297+
width: The requested pixel width of the window.
298+
height: The requested pixel height of the window.
299+
x: The left-most position of the window.
300+
y: The top-most position of the window.
301+
title: The title text of the new window. If no option is given then `sys.arg[0]` will be used as the title.
302+
flags: The SDL flags to use for this window, such as `tcod.sdl.video.WindowFlags.RESIZABLE`.
303+
See :any:`WindowFlags` for more options.
304+
306305
Example::
307306
308307
# Create a new resizable window with a custom title.
@@ -325,12 +324,12 @@ def get_grabbed_window() -> Optional[Window]:
325324
return Window(sdl_window_p) if sdl_window_p else None
326325

327326

328-
def _get_active_window() -> Window:
329-
"""Return the SDL2 window current managed by libtcod.
330-
331-
Will raise an error if libtcod does not currently have a window.
332-
"""
333-
sdl_window = lib.TCOD_sys_get_window()
334-
if not sdl_window:
335-
raise RuntimeError("TCOD does not have an active window.")
336-
return Window(sdl_window)
327+
def screen_saver_allowed(allow: Optional[bool] = None) -> bool:
328+
"""Allow or prevent a screen saver from being displayed and return the current allowed status."""
329+
if allow is None:
330+
pass
331+
elif allow:
332+
lib.SDL_EnableScreenSaver()
333+
else:
334+
lib.SDL_DisableScreenSaver()
335+
return bool(lib.SDL_IsScreenSaverEnabled())

tests/test_sdl.py

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import sys
2+
3+
import numpy as np
4+
import pytest
5+
6+
import tcod.sdl.render
7+
import tcod.sdl.video
8+
9+
10+
def test_sdl_window() -> None:
11+
assert tcod.sdl.video.get_grabbed_window() is None
12+
window = tcod.sdl.video.new_window(1, 1)
13+
window.raise_window()
14+
window.maximize()
15+
window.restore()
16+
window.minimize()
17+
window.hide()
18+
window.show()
19+
assert window.title == sys.argv[0]
20+
window.title = "Title"
21+
assert window.title == "Title"
22+
assert window.opacity == 1.0
23+
window.position = window.position
24+
window.fullscreen = window.fullscreen
25+
window.resizable = window.resizable
26+
window.size = window.size
27+
window.min_size = window.min_size
28+
window.max_size = window.max_size
29+
window.border_size
30+
window.set_icon(np.zeros((32, 32, 3), dtype=np.uint8))
31+
with pytest.raises(TypeError):
32+
window.set_icon(np.zeros((32, 32, 5), dtype=np.uint8))
33+
with pytest.raises(TypeError):
34+
window.set_icon(np.zeros((32, 32), dtype=np.uint8))
35+
window.opacity = window.opacity
36+
window.grab = window.grab
37+
38+
39+
def test_sdl_window_bad_types() -> None:
40+
with pytest.raises(TypeError):
41+
tcod.sdl.video.Window(tcod.ffi.cast("SDL_Window*", tcod.ffi.NULL))
42+
with pytest.raises(TypeError):
43+
tcod.sdl.video.Window(tcod.ffi.new("SDL_Rect*"))
44+
45+
46+
def test_sdl_screen_saver() -> None:
47+
assert tcod.sdl.video.screen_saver_allowed(False) is False
48+
assert tcod.sdl.video.screen_saver_allowed(True) is True
49+
assert tcod.sdl.video.screen_saver_allowed() is True
50+
51+
52+
def test_sdl_render() -> None:
53+
window = tcod.sdl.video.new_window(1, 1)
54+
render = tcod.sdl.render.new_renderer(window, software=True, vsync=False, target_textures=True)
55+
render.present()
56+
rgb = render.upload_texture(np.zeros((8, 8, 3), np.uint8))
57+
assert (rgb.width, rgb.height) == (8, 8)
58+
assert rgb.access == tcod.sdl.render.TextureAccess.STATIC
59+
assert rgb.format == tcod.lib.SDL_PIXELFORMAT_RGB24
60+
rgb.alpha_mod = rgb.alpha_mod
61+
rgb.blend_mode = rgb.blend_mode
62+
rgb.rgb_mod = rgb.rgb_mod
63+
rgba = render.upload_texture(np.zeros((8, 8, 4), np.uint8), access=tcod.sdl.render.TextureAccess.TARGET)
64+
with render.set_render_target(rgba):
65+
render.copy(rgb)
66+
with pytest.raises(TypeError):
67+
render.upload_texture(np.zeros((8, 8, 5), np.uint8))
68+
69+
70+
def test_sdl_render_bad_types() -> None:
71+
with pytest.raises(TypeError):
72+
tcod.sdl.render.Renderer(tcod.ffi.cast("SDL_Renderer*", tcod.ffi.NULL))
73+
with pytest.raises(TypeError):
74+
tcod.sdl.render.Renderer(tcod.ffi.new("SDL_Rect*"))

0 commit comments

Comments
 (0)