teek-sdl2 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/ext/teek_sdl2/extconf.rb +112 -0
- data/ext/teek_sdl2/sdl2bridge.c +142 -0
- data/ext/teek_sdl2/sdl2image.c +83 -0
- data/ext/teek_sdl2/sdl2pixels.c +166 -0
- data/ext/teek_sdl2/sdl2surface.c +534 -0
- data/ext/teek_sdl2/sdl2text.c +234 -0
- data/ext/teek_sdl2/teek_sdl2.c +65 -0
- data/ext/teek_sdl2/teek_sdl2.h +63 -0
- data/lib/teek/sdl2/font.rb +94 -0
- data/lib/teek/sdl2/renderer.rb +227 -0
- data/lib/teek/sdl2/texture.rb +91 -0
- data/lib/teek/sdl2/version.rb +7 -0
- data/lib/teek/sdl2/viewport.rb +230 -0
- data/lib/teek/sdl2.rb +69 -0
- data/teek-sdl2.gemspec +28 -0
- data/test/test_callback_teardown.rb +35 -0
- data/test/test_helper.rb +19 -0
- data/test/test_image.rb +87 -0
- data/test/test_input.rb +185 -0
- data/test/test_renderer.rb +138 -0
- data/test/test_sdl2.rb +24 -0
- data/test/test_viewport.rb +89 -0
- metadata +120 -0
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Teek
|
|
4
|
+
module SDL2
|
|
5
|
+
# GPU-resident pixel buffer backed by an +SDL_Texture+.
|
|
6
|
+
#
|
|
7
|
+
# Textures are created through a {Renderer}, not directly instantiated.
|
|
8
|
+
# Use the convenience constructors {.streaming} and {.target}, or call
|
|
9
|
+
# {Renderer#create_texture} directly.
|
|
10
|
+
#
|
|
11
|
+
# ## C-defined methods
|
|
12
|
+
#
|
|
13
|
+
# These are defined in the C extension (+sdl2surface.c+):
|
|
14
|
+
#
|
|
15
|
+
# - {#update} — upload pixel data from a String
|
|
16
|
+
# - {#width} — texture width in pixels
|
|
17
|
+
# - {#height} — texture height in pixels
|
|
18
|
+
# - {#destroy} — free GPU resources
|
|
19
|
+
# - {#destroyed?} — check if the texture has been destroyed
|
|
20
|
+
#
|
|
21
|
+
# @example Create and update a streaming texture
|
|
22
|
+
# tex = Teek::SDL2::Texture.streaming(renderer, 256, 224)
|
|
23
|
+
# tex.update(pixel_data_string)
|
|
24
|
+
# renderer.copy(tex)
|
|
25
|
+
#
|
|
26
|
+
# @see Renderer#create_texture
|
|
27
|
+
class Texture
|
|
28
|
+
|
|
29
|
+
# @!method update(pixel_data)
|
|
30
|
+
# Upload pixel data to the texture. The data must be a binary String
|
|
31
|
+
# of ARGB8888 pixels (4 bytes per pixel, width * height * 4 total).
|
|
32
|
+
# @param pixel_data [String] raw pixel bytes
|
|
33
|
+
# @return [self]
|
|
34
|
+
|
|
35
|
+
# @!method width
|
|
36
|
+
# @return [Integer] texture width in pixels
|
|
37
|
+
|
|
38
|
+
# @!method height
|
|
39
|
+
# @return [Integer] texture height in pixels
|
|
40
|
+
|
|
41
|
+
# @!method destroy
|
|
42
|
+
# Free this texture's GPU resources.
|
|
43
|
+
# @return [void]
|
|
44
|
+
|
|
45
|
+
# @!method destroyed?
|
|
46
|
+
# @return [Boolean] whether this texture has been destroyed
|
|
47
|
+
|
|
48
|
+
# Load an image file into a GPU texture via SDL2_image.
|
|
49
|
+
#
|
|
50
|
+
# @param renderer [Renderer] the renderer that owns this texture
|
|
51
|
+
# @param path [String] path to an image file (PNG, JPG, BMP, etc.)
|
|
52
|
+
# @return [Texture]
|
|
53
|
+
#
|
|
54
|
+
# @example
|
|
55
|
+
# sprite = Teek::SDL2::Texture.from_file(renderer, "assets/player.png")
|
|
56
|
+
# renderer.copy(sprite)
|
|
57
|
+
def self.from_file(renderer, path)
|
|
58
|
+
renderer.load_image(path)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Create a streaming texture (lockable, CPU-updatable).
|
|
62
|
+
#
|
|
63
|
+
# @param renderer [Renderer] the renderer that owns this texture
|
|
64
|
+
# @param width [Integer] width in pixels
|
|
65
|
+
# @param height [Integer] height in pixels
|
|
66
|
+
# @return [Texture]
|
|
67
|
+
#
|
|
68
|
+
# @example
|
|
69
|
+
# tex = Teek::SDL2::Texture.streaming(renderer, 256, 224)
|
|
70
|
+
# tex.update(rgba_string)
|
|
71
|
+
def self.streaming(renderer, width, height)
|
|
72
|
+
renderer.create_texture(width, height, :streaming)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Create a target texture (can be rendered to via +SDL_SetRenderTarget+).
|
|
76
|
+
#
|
|
77
|
+
# @param renderer [Renderer] the renderer that owns this texture
|
|
78
|
+
# @param width [Integer] width in pixels
|
|
79
|
+
# @param height [Integer] height in pixels
|
|
80
|
+
# @return [Texture]
|
|
81
|
+
def self.target(renderer, width, height)
|
|
82
|
+
renderer.create_texture(width, height, :target)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# @return [Array(Integer, Integer)] +[width, height]+
|
|
86
|
+
def size
|
|
87
|
+
[width, height]
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'set'
|
|
4
|
+
|
|
5
|
+
module Teek
|
|
6
|
+
module SDL2
|
|
7
|
+
# An SDL2-accelerated rendering surface embedded in a Tk frame.
|
|
8
|
+
#
|
|
9
|
+
# Viewport creates a Tk frame, obtains its native window handle via
|
|
10
|
+
# the Tk C API, then embeds an SDL2 renderer inside it using
|
|
11
|
+
# +SDL_CreateWindowFrom+. All drawing goes through SDL2 with GPU
|
|
12
|
+
# acceleration — no Tk involvement in the rendering path.
|
|
13
|
+
#
|
|
14
|
+
# Keyboard input is tracked automatically via Tk bindings so you can
|
|
15
|
+
# poll key state with {#key_down?} in a game loop.
|
|
16
|
+
#
|
|
17
|
+
# @example Create a viewport and draw a red rectangle
|
|
18
|
+
# viewport = Teek::SDL2::Viewport.new(app, width: 800, height: 600)
|
|
19
|
+
# viewport.pack(fill: :both, expand: true)
|
|
20
|
+
#
|
|
21
|
+
# viewport.render do |r|
|
|
22
|
+
# r.clear(0, 0, 0)
|
|
23
|
+
# r.fill_rect(10, 10, 100, 50, 255, 0, 0)
|
|
24
|
+
# end
|
|
25
|
+
#
|
|
26
|
+
# @example Poll keyboard input
|
|
27
|
+
# if viewport.key_down?('left')
|
|
28
|
+
# player_x -= speed
|
|
29
|
+
# end
|
|
30
|
+
#
|
|
31
|
+
# @see Renderer
|
|
32
|
+
class Viewport
|
|
33
|
+
# @return [Teek::App] the Teek application
|
|
34
|
+
attr_reader :app
|
|
35
|
+
|
|
36
|
+
# @return [Teek::Widget] the underlying Tk frame
|
|
37
|
+
attr_reader :frame
|
|
38
|
+
|
|
39
|
+
# @return [Teek::SDL2::Renderer] the SDL2 renderer
|
|
40
|
+
attr_reader :renderer
|
|
41
|
+
|
|
42
|
+
# @return [Set<String>] currently held key names (lowercase keysyms)
|
|
43
|
+
attr_reader :keys_down
|
|
44
|
+
|
|
45
|
+
# @param app [Teek::App] the Teek application
|
|
46
|
+
# @param parent [Teek::Widget, String, nil] parent widget (nil for root)
|
|
47
|
+
# @param width [Integer] initial width in pixels
|
|
48
|
+
# @param height [Integer] initial height in pixels
|
|
49
|
+
def initialize(app, parent: nil, width: 640, height: 480)
|
|
50
|
+
@app = app
|
|
51
|
+
@destroyed = false
|
|
52
|
+
|
|
53
|
+
# Create a Tk frame to host the SDL2 window
|
|
54
|
+
@frame = app.create_widget('frame', parent: parent,
|
|
55
|
+
width: width, height: height)
|
|
56
|
+
|
|
57
|
+
# Pack with fixed size so the frame is managed, then force a
|
|
58
|
+
# full update. On X11, update_idletasks alone isn't enough —
|
|
59
|
+
# the window must process MapNotify to be usable by SDL2.
|
|
60
|
+
@frame.pack
|
|
61
|
+
app.tcl_eval('update')
|
|
62
|
+
|
|
63
|
+
# Get platform-native window handle via Tk C API
|
|
64
|
+
# (macOS: NSWindow*, X11: Window ID, Windows: HWND)
|
|
65
|
+
#
|
|
66
|
+
# NOTE: On macOS, Tk_MacOSXGetNSWindowForDrawable returns the NSWindow
|
|
67
|
+
# for the entire Tk toplevel, not just this frame. SDL_CreateWindowFrom
|
|
68
|
+
# therefore creates a renderer that covers the whole window. Tk widgets
|
|
69
|
+
# packed alongside the viewport will be painted over by SDL2 rendering.
|
|
70
|
+
# On X11 each frame has its own X Window, so embedding is frame-scoped.
|
|
71
|
+
# Workaround on macOS: use SDL2_ttf to draw overlay text on the surface
|
|
72
|
+
# rather than Tk widgets.
|
|
73
|
+
handle = app.interp.native_window_handle(@frame.path)
|
|
74
|
+
|
|
75
|
+
# Create SDL2 renderer embedded in the frame (Layer 2 → Layer 1)
|
|
76
|
+
@renderer = Teek::SDL2.create_renderer_from_handle(handle)
|
|
77
|
+
|
|
78
|
+
# Register SDL2 event source if this is the first viewport
|
|
79
|
+
Teek::SDL2.register_event_source
|
|
80
|
+
|
|
81
|
+
# Key state tracking for game-loop polling
|
|
82
|
+
@keys_down = Set.new
|
|
83
|
+
@frame.bind('KeyPress', :keysym) { |k| @keys_down.add(k.downcase) }
|
|
84
|
+
@frame.bind('KeyRelease', :keysym) { |k| @keys_down.delete(k.downcase) }
|
|
85
|
+
|
|
86
|
+
# Click-to-focus: Tk frames must have focus to receive key events
|
|
87
|
+
@frame.bind('ButtonPress-1') { focus }
|
|
88
|
+
|
|
89
|
+
# Bind cleanup on frame destroy
|
|
90
|
+
@frame.bind('<Destroy>') { _on_destroy }
|
|
91
|
+
|
|
92
|
+
# Track viewport count for event source lifecycle
|
|
93
|
+
Teek::SDL2._viewports << self
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Draw with the renderer in a block, auto-presenting at the end.
|
|
97
|
+
#
|
|
98
|
+
# @yield [renderer] the SDL2 renderer for this viewport
|
|
99
|
+
# @yieldparam renderer [Teek::SDL2::Renderer]
|
|
100
|
+
# @return [self]
|
|
101
|
+
# @raise [Teek::SDL2::Error] if the viewport has been destroyed
|
|
102
|
+
#
|
|
103
|
+
# @example
|
|
104
|
+
# viewport.render do |r|
|
|
105
|
+
# r.clear(0, 0, 0)
|
|
106
|
+
# r.fill_rect(10, 10, 100, 50, 255, 0, 0)
|
|
107
|
+
# end
|
|
108
|
+
def render(&block)
|
|
109
|
+
raise Teek::SDL2::Error, "viewport has been destroyed" if @destroyed
|
|
110
|
+
@renderer.render(&block)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Pack the viewport into its parent using Tk's pack geometry manager.
|
|
114
|
+
#
|
|
115
|
+
# @param kwargs options passed to the Tk +pack+ command
|
|
116
|
+
# @return [self]
|
|
117
|
+
def pack(**kwargs)
|
|
118
|
+
@frame.pack(**kwargs)
|
|
119
|
+
self
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Grid the viewport into its parent using Tk's grid geometry manager.
|
|
123
|
+
#
|
|
124
|
+
# @param kwargs options passed to the Tk +grid+ command
|
|
125
|
+
# @return [self]
|
|
126
|
+
def grid(**kwargs)
|
|
127
|
+
@frame.grid(**kwargs)
|
|
128
|
+
self
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Check if a key is currently held down. Uses Tk keysym names (lowercase).
|
|
132
|
+
#
|
|
133
|
+
# @param keysym [String, Symbol] Tk keysym name (e.g. +'left'+, +'space'+, +'a'+)
|
|
134
|
+
# @return [Boolean]
|
|
135
|
+
#
|
|
136
|
+
# @example
|
|
137
|
+
# viewport.key_down?('left') # arrow key
|
|
138
|
+
# viewport.key_down?('space') # spacebar
|
|
139
|
+
# viewport.key_down?('a') # letter key
|
|
140
|
+
def key_down?(keysym)
|
|
141
|
+
@keys_down.include?(keysym.to_s.downcase)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Give this viewport keyboard focus so it receives key events.
|
|
145
|
+
#
|
|
146
|
+
# @return [void]
|
|
147
|
+
def focus
|
|
148
|
+
@app.tcl_eval("focus #{@frame.path}")
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# Bind a Tk event on the viewport frame.
|
|
152
|
+
#
|
|
153
|
+
# Automatically chains with internal behavior (key tracking,
|
|
154
|
+
# click-to-focus) so user callbacks don't clobber {#key_down?}.
|
|
155
|
+
#
|
|
156
|
+
# @param event [String] Tk event name (e.g. +'KeyPress'+, +'ButtonPress-1'+)
|
|
157
|
+
# @param subs [Array<Symbol, String>] Tk substitution codes
|
|
158
|
+
# @yield called when the event fires
|
|
159
|
+
# @return [void]
|
|
160
|
+
def bind(event, *subs, &block)
|
|
161
|
+
case event.to_s
|
|
162
|
+
when 'KeyPress'
|
|
163
|
+
@frame.bind(event, *subs) do |*args|
|
|
164
|
+
@keys_down.add(args.first.to_s.downcase) if args.first
|
|
165
|
+
block&.call(*args)
|
|
166
|
+
end
|
|
167
|
+
when 'KeyRelease'
|
|
168
|
+
@frame.bind(event, *subs) do |*args|
|
|
169
|
+
@keys_down.delete(args.first.to_s.downcase) if args.first
|
|
170
|
+
block&.call(*args)
|
|
171
|
+
end
|
|
172
|
+
when /ButtonPress/
|
|
173
|
+
@frame.bind(event, *subs) do |*args|
|
|
174
|
+
focus
|
|
175
|
+
block&.call(*args)
|
|
176
|
+
end
|
|
177
|
+
else
|
|
178
|
+
@frame.bind(event, *subs, &block)
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# Destroy the viewport, its SDL2 renderer, and the Tk frame.
|
|
183
|
+
#
|
|
184
|
+
# @return [void]
|
|
185
|
+
def destroy
|
|
186
|
+
return if @destroyed
|
|
187
|
+
@renderer.destroy unless @renderer.destroyed?
|
|
188
|
+
@frame.destroy if @frame.exist?
|
|
189
|
+
_cleanup
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# @return [Boolean] whether this viewport has been destroyed
|
|
193
|
+
def destroyed?
|
|
194
|
+
@destroyed
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def inspect
|
|
198
|
+
"#<Teek::SDL2::Viewport #{@frame.path} #{destroyed? ? 'DESTROYED' : 'active'}>"
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
private
|
|
202
|
+
|
|
203
|
+
def _on_destroy
|
|
204
|
+
return if @destroyed
|
|
205
|
+
@renderer.destroy unless @renderer.destroyed?
|
|
206
|
+
_cleanup
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def _cleanup
|
|
210
|
+
@destroyed = true
|
|
211
|
+
Teek::SDL2._viewports.delete(self)
|
|
212
|
+
|
|
213
|
+
# Unregister event source when last viewport is gone
|
|
214
|
+
if Teek::SDL2._viewports.empty?
|
|
215
|
+
Teek::SDL2.unregister_event_source
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
# Internal viewport tracking for event source lifecycle
|
|
221
|
+
@_viewports = []
|
|
222
|
+
|
|
223
|
+
class << self
|
|
224
|
+
# @api private
|
|
225
|
+
def _viewports
|
|
226
|
+
@_viewports
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
end
|
data/lib/teek/sdl2.rb
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "teek"
|
|
4
|
+
require_relative "sdl2/version"
|
|
5
|
+
require "teek_sdl2"
|
|
6
|
+
|
|
7
|
+
module Teek
|
|
8
|
+
# GPU-accelerated 2D rendering via SDL2, embedded inside Tk windows.
|
|
9
|
+
#
|
|
10
|
+
# Teek::SDL2 lets you drop an SDL2 hardware-accelerated surface into any
|
|
11
|
+
# Tk application. The surface lives inside a Tk frame so it coexists with
|
|
12
|
+
# normal Tk widgets (buttons, labels, menus) while all pixel work is
|
|
13
|
+
# GPU-driven.
|
|
14
|
+
#
|
|
15
|
+
# The main entry point is {Viewport}, which creates a Tk frame, obtains
|
|
16
|
+
# its native window handle, and hands it to SDL2 for rendering.
|
|
17
|
+
#
|
|
18
|
+
# @example Basic usage
|
|
19
|
+
# require 'teek'
|
|
20
|
+
# require 'teek/sdl2'
|
|
21
|
+
#
|
|
22
|
+
# app = Teek::App.new
|
|
23
|
+
# vp = Teek::SDL2::Viewport.new(app, width: 800, height: 600)
|
|
24
|
+
# vp.render do |r|
|
|
25
|
+
# r.clear(0, 0, 0)
|
|
26
|
+
# r.fill_rect(10, 10, 100, 50, 255, 0, 0)
|
|
27
|
+
# end
|
|
28
|
+
# app.mainloop
|
|
29
|
+
#
|
|
30
|
+
# @see Viewport
|
|
31
|
+
# @see Renderer
|
|
32
|
+
# @see Texture
|
|
33
|
+
# @see Font
|
|
34
|
+
module SDL2
|
|
35
|
+
@event_source = nil
|
|
36
|
+
|
|
37
|
+
# Register SDL2 as a Tcl event source. Called automatically when the
|
|
38
|
+
# first {Viewport} is created. Uses a C function pointer for the hot
|
|
39
|
+
# path — no Ruby in the poll loop.
|
|
40
|
+
#
|
|
41
|
+
# @param interval_ms [Integer] polling interval in milliseconds
|
|
42
|
+
# @return [void]
|
|
43
|
+
# @api private
|
|
44
|
+
def self.register_event_source(interval_ms: 16)
|
|
45
|
+
return if @event_source&.registered?
|
|
46
|
+
|
|
47
|
+
fn_ptr = _event_check_fn_ptr # C function address from sdl2bridge.c
|
|
48
|
+
@event_source = Teek._register_event_source(fn_ptr, 0, interval_ms)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Remove SDL2 from Tcl's event loop. Called automatically when the last
|
|
52
|
+
# {Viewport} is destroyed.
|
|
53
|
+
#
|
|
54
|
+
# @return [void]
|
|
55
|
+
# @api private
|
|
56
|
+
def self.unregister_event_source
|
|
57
|
+
@event_source&.unregister
|
|
58
|
+
@event_source = nil
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Ruby convenience layers (reopen C-defined classes)
|
|
64
|
+
require_relative "sdl2/renderer"
|
|
65
|
+
require_relative "sdl2/texture"
|
|
66
|
+
require_relative "sdl2/font"
|
|
67
|
+
|
|
68
|
+
# Tk bridge (embeds SDL2 surface into a Tk frame)
|
|
69
|
+
require_relative "sdl2/viewport"
|
data/teek-sdl2.gemspec
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
require_relative "lib/teek/sdl2/version"
|
|
2
|
+
|
|
3
|
+
Gem::Specification.new do |spec|
|
|
4
|
+
spec.name = "teek-sdl2"
|
|
5
|
+
spec.version = Teek::SDL2::VERSION
|
|
6
|
+
spec.authors = ["James Cook"]
|
|
7
|
+
spec.email = ["jcook.rubyist@gmail.com"]
|
|
8
|
+
|
|
9
|
+
spec.summary = "GPU-accelerated SDL2 rendering for teek (Tk)"
|
|
10
|
+
spec.description = "Embeds an SDL2 renderer inside a Tk frame for GPU-accelerated drawing"
|
|
11
|
+
spec.homepage = "https://github.com/jamescook/teek"
|
|
12
|
+
spec.licenses = ["MIT"]
|
|
13
|
+
|
|
14
|
+
spec.files = Dir.glob("{lib,ext,test}/**/*").select { |f|
|
|
15
|
+
File.file?(f) && f !~ /\.(bundle|so|o|log)$/
|
|
16
|
+
} + %w[teek-sdl2.gemspec]
|
|
17
|
+
spec.require_paths = ["lib"]
|
|
18
|
+
spec.extensions = ["ext/teek_sdl2/extconf.rb"]
|
|
19
|
+
spec.required_ruby_version = ">= 3.2"
|
|
20
|
+
|
|
21
|
+
spec.add_dependency "teek", ">= 0.1.2"
|
|
22
|
+
|
|
23
|
+
spec.add_development_dependency "rake", "~> 13.0"
|
|
24
|
+
spec.add_development_dependency "rake-compiler", "~> 1.0"
|
|
25
|
+
spec.add_development_dependency "minitest", "~> 6.0"
|
|
26
|
+
|
|
27
|
+
spec.requirements << "SDL2 development headers (libsdl2-dev or sdl2 via Homebrew)"
|
|
28
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "minitest/autorun"
|
|
4
|
+
require_relative "../../test/tk_test_helper"
|
|
5
|
+
|
|
6
|
+
class TestCallbackTeardown < Minitest::Test
|
|
7
|
+
include TeekTestHelper
|
|
8
|
+
|
|
9
|
+
def test_viewport_destroy_no_bgerror
|
|
10
|
+
assert_tk_app("destroying viewport does not trigger bgerror") do
|
|
11
|
+
require "teek/sdl2"
|
|
12
|
+
app.show
|
|
13
|
+
app.update
|
|
14
|
+
|
|
15
|
+
# Capture any bgerror that Tcl would normally show in a dialog
|
|
16
|
+
app.set_variable("_bgerror_msg", "")
|
|
17
|
+
app.tcl_eval('proc bgerror {msg} { set ::_bgerror_msg $msg }')
|
|
18
|
+
|
|
19
|
+
vp = Teek::SDL2::Viewport.new(app, width: 200, height: 200)
|
|
20
|
+
vp.pack
|
|
21
|
+
app.update
|
|
22
|
+
|
|
23
|
+
vp.render do |r|
|
|
24
|
+
r.clear(30, 30, 30)
|
|
25
|
+
r.fill(20, 20, 80, 60, r: 200, g: 50, b: 50)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
vp.destroy
|
|
29
|
+
app.update
|
|
30
|
+
|
|
31
|
+
err = app.get_variable("_bgerror_msg")
|
|
32
|
+
assert_equal "", err, "bgerror fired during viewport destroy: #{err}"
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
data/test/test_helper.rb
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
if ENV['COVERAGE']
|
|
4
|
+
require 'simplecov'
|
|
5
|
+
require_relative '../../test/simplecov_config'
|
|
6
|
+
|
|
7
|
+
coverage_name = ENV['COVERAGE_NAME'] || 'sdl2'
|
|
8
|
+
SimpleCov.coverage_dir "#{SimpleCovConfig::PROJECT_ROOT}/coverage/results/#{coverage_name}"
|
|
9
|
+
SimpleCov.command_name "sdl2:#{coverage_name}"
|
|
10
|
+
SimpleCov.print_error_status = false
|
|
11
|
+
SimpleCov.formatter SimpleCov::Formatter::SimpleFormatter
|
|
12
|
+
|
|
13
|
+
SimpleCov.start do
|
|
14
|
+
SimpleCovConfig.apply_filters(self)
|
|
15
|
+
track_files "#{SimpleCovConfig::PROJECT_ROOT}/lib/**/*.rb"
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
require "minitest/autorun"
|
data/test/test_image.rb
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "minitest/autorun"
|
|
4
|
+
require_relative "../../test/tk_test_helper"
|
|
5
|
+
|
|
6
|
+
class TestImage < Minitest::Test
|
|
7
|
+
include TeekTestHelper
|
|
8
|
+
|
|
9
|
+
def test_load_image
|
|
10
|
+
assert_tk_app("load_image returns a texture with correct dimensions") do
|
|
11
|
+
require "teek/sdl2"
|
|
12
|
+
|
|
13
|
+
png = fixture_path("teek-sdl2/assets/test_red_8x8.png")
|
|
14
|
+
app.show
|
|
15
|
+
app.update
|
|
16
|
+
viewport = Teek::SDL2::Viewport.new(app, width: 200, height: 200)
|
|
17
|
+
|
|
18
|
+
tex = viewport.renderer.load_image(png)
|
|
19
|
+
assert_kind_of Teek::SDL2::Texture, tex
|
|
20
|
+
assert_equal 8, tex.width
|
|
21
|
+
assert_equal 8, tex.height
|
|
22
|
+
assert_equal [8, 8], tex.size
|
|
23
|
+
refute tex.destroyed?
|
|
24
|
+
|
|
25
|
+
tex.destroy
|
|
26
|
+
assert tex.destroyed?
|
|
27
|
+
viewport.destroy
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def test_load_image_and_render
|
|
32
|
+
assert_tk_app("load_image texture can be rendered") do
|
|
33
|
+
require "teek/sdl2"
|
|
34
|
+
|
|
35
|
+
png = fixture_path("teek-sdl2/assets/test_red_8x8.png")
|
|
36
|
+
app.show
|
|
37
|
+
app.update
|
|
38
|
+
viewport = Teek::SDL2::Viewport.new(app, width: 200, height: 200)
|
|
39
|
+
viewport.pack
|
|
40
|
+
|
|
41
|
+
tex = viewport.renderer.load_image(png)
|
|
42
|
+
|
|
43
|
+
viewport.render do |r|
|
|
44
|
+
r.clear(0, 0, 0)
|
|
45
|
+
r.copy(tex, nil, [0, 0, tex.width * 4, tex.height * 4])
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
tex.destroy
|
|
49
|
+
viewport.destroy
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def test_texture_from_file
|
|
54
|
+
assert_tk_app("Texture.from_file convenience works") do
|
|
55
|
+
require "teek/sdl2"
|
|
56
|
+
|
|
57
|
+
png = fixture_path("teek-sdl2/assets/test_red_8x8.png")
|
|
58
|
+
app.show
|
|
59
|
+
app.update
|
|
60
|
+
viewport = Teek::SDL2::Viewport.new(app, width: 200, height: 200)
|
|
61
|
+
|
|
62
|
+
tex = Teek::SDL2::Texture.from_file(viewport.renderer, png)
|
|
63
|
+
assert_kind_of Teek::SDL2::Texture, tex
|
|
64
|
+
assert_equal 8, tex.width
|
|
65
|
+
assert_equal 8, tex.height
|
|
66
|
+
|
|
67
|
+
tex.destroy
|
|
68
|
+
viewport.destroy
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def test_load_image_bad_path
|
|
73
|
+
assert_tk_app("load_image raises on missing file") do
|
|
74
|
+
require "teek/sdl2"
|
|
75
|
+
|
|
76
|
+
app.show
|
|
77
|
+
app.update
|
|
78
|
+
viewport = Teek::SDL2::Viewport.new(app, width: 200, height: 200)
|
|
79
|
+
|
|
80
|
+
assert_raises(RuntimeError) do
|
|
81
|
+
viewport.renderer.load_image("/nonexistent/path/to/image.png")
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
viewport.destroy
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|