gosu 0.7.39-x86-mingw32

Sign up to get free protection for your applications and to get access to all the features.
Files changed (66) hide show
  1. data/COPYING +34 -0
  2. data/Gosu/Async.hpp +50 -0
  3. data/Gosu/Audio.hpp +163 -0
  4. data/Gosu/AutoLink.hpp +16 -0
  5. data/Gosu/Bitmap.hpp +96 -0
  6. data/Gosu/ButtonsMac.hpp +140 -0
  7. data/Gosu/ButtonsWin.hpp +140 -0
  8. data/Gosu/ButtonsX.hpp +141 -0
  9. data/Gosu/Color.hpp +204 -0
  10. data/Gosu/Directories.hpp +36 -0
  11. data/Gosu/Font.hpp +83 -0
  12. data/Gosu/Fwd.hpp +31 -0
  13. data/Gosu/Gosu.hpp +34 -0
  14. data/Gosu/Graphics.hpp +120 -0
  15. data/Gosu/GraphicsBase.hpp +66 -0
  16. data/Gosu/IO.hpp +259 -0
  17. data/Gosu/Image.hpp +138 -0
  18. data/Gosu/ImageData.hpp +58 -0
  19. data/Gosu/Input.hpp +161 -0
  20. data/Gosu/Inspection.hpp +14 -0
  21. data/Gosu/Math.hpp +135 -0
  22. data/Gosu/Platform.hpp +73 -0
  23. data/Gosu/Sockets.hpp +137 -0
  24. data/Gosu/TR1.hpp +44 -0
  25. data/Gosu/Text.hpp +71 -0
  26. data/Gosu/TextInput.hpp +70 -0
  27. data/Gosu/Timing.hpp +16 -0
  28. data/Gosu/Utility.hpp +28 -0
  29. data/Gosu/Version.hpp +526 -0
  30. data/Gosu/WinUtility.hpp +75 -0
  31. data/Gosu/Window.hpp +124 -0
  32. data/README.txt +25 -0
  33. data/examples/ChipmunkIntegration.rb +275 -0
  34. data/examples/CptnRuby.rb +223 -0
  35. data/examples/MoreChipmunkAndRMagick.rb +155 -0
  36. data/examples/OpenGLIntegration.rb +226 -0
  37. data/examples/RMagickIntegration.rb +417 -0
  38. data/examples/TextInput.rb +154 -0
  39. data/examples/Tutorial.rb +131 -0
  40. data/examples/media/Beep.wav +0 -0
  41. data/examples/media/CptnRuby Gem.png +0 -0
  42. data/examples/media/CptnRuby Map.txt +25 -0
  43. data/examples/media/CptnRuby Tileset.png +0 -0
  44. data/examples/media/CptnRuby.png +0 -0
  45. data/examples/media/Cursor.png +0 -0
  46. data/examples/media/Earth.png +0 -0
  47. data/examples/media/Explosion.wav +0 -0
  48. data/examples/media/Landscape.svg +10 -0
  49. data/examples/media/LargeStar.png +0 -0
  50. data/examples/media/Smoke.png +0 -0
  51. data/examples/media/Soldier.png +0 -0
  52. data/examples/media/Space.png +0 -0
  53. data/examples/media/Star.png +0 -0
  54. data/examples/media/Starfighter.bmp +0 -0
  55. data/lib/FreeImage.dll +0 -0
  56. data/lib/OpenAL32.dll +0 -0
  57. data/lib/gosu.for_1_8.so +0 -0
  58. data/lib/gosu.for_1_9.so +0 -0
  59. data/lib/gosu.rb +17 -0
  60. data/lib/gosu/patches.rb +75 -0
  61. data/lib/gosu/preview.rb +121 -0
  62. data/lib/gosu/run.rb +11 -0
  63. data/lib/gosu/swig_patches.rb +48 -0
  64. data/lib/gosu/zen.rb +28 -0
  65. data/lib/libsndfile.dll +0 -0
  66. metadata +138 -0
@@ -0,0 +1,417 @@
1
+ # A (too) simple Gorilla-style shooter for two players.
2
+ # Shows how Gosu and RMagick can be used together to generate a map, implement
3
+ # a dynamic landscape and generally look great.
4
+ # Also shows a very minimal, yet effective way of designing a game's object system.
5
+
6
+ # Doesn't make use of Gosu's Z-ordering. Not many different things to draw, it's
7
+ # easy to get the order right without it.
8
+
9
+ # Known issues:
10
+ # * Collision detection of the missiles is lazy, allows shooting through thin walls.
11
+ # * The look of dead soldiers is, err, by accident. Soldier.png needs to be
12
+ # designed in a less obfuscated way :)
13
+
14
+ require 'rubygems'
15
+ require 'gosu'
16
+ require 'RMagick'
17
+
18
+ NULL_PIXEL = Magick::Pixel.from_color('none')
19
+
20
+ # The class for this game's map.
21
+ # Design:
22
+ # * Dynamic map creation at startup, holding it as RMagick Image in @image
23
+ # * Testing for solidity by testing @image's pixel values
24
+ # * Drawing from a Gosu::Image instance
25
+ # * Blasting holes into the map is implemented by drawing and erasing portions
26
+ # of @image, then recreating the corresponding area in the Gosu::Image
27
+
28
+ class Map
29
+ WIDTH, HEIGHT = 800, 600
30
+
31
+ def initialize window
32
+ # We'll need the window later for re-creating Gosu images.
33
+ @window = window
34
+
35
+ # Let's start with something simple and load the sky via RMagick.
36
+ # Loading SVG files isn't possible with Gosu, so say wow!
37
+ # (Seems to take a while though)
38
+ sky = Magick::Image.read("media/Landscape.svg").first
39
+ @sky = Gosu::Image.new(window, sky, true)
40
+
41
+ # Create the map an stores the RMagick image in @image
42
+ create_rmagick_map
43
+
44
+ # Copy the RMagick Image to a Gosu Image (still unchanged)
45
+ @gosu_image = Gosu::Image.new(window, @image, true)
46
+ end
47
+
48
+ def solid? x, y
49
+ # Map is open at the top.
50
+ return false if y < 0
51
+ # Map is closed on all other sides.
52
+ return true if x < 0 or x >= 800 or y >= 600
53
+ # Inside of the map, determine solidity from the map image.
54
+ @image.pixel_color(x, y) != NULL_PIXEL
55
+ end
56
+
57
+ def draw
58
+ # Sky background.
59
+ @sky.draw(0, 0, 0)
60
+ # The landscape.
61
+ @gosu_image.draw 0, 0, 0
62
+ end
63
+
64
+ # Radius of a crater.
65
+ RADIUS = 25
66
+ # Radius of a crater, Shadow included.
67
+ SH_RADIUS = 45
68
+
69
+ # Create the crater image (basically a circle shape that is used to erase
70
+ # parts of the map) and the crater shadow image.
71
+ CRATER_IMAGE = begin
72
+ crater = Magick::Image.new(2 * RADIUS, 2 * RADIUS) { self.background_color = 'none' }
73
+ gc = Magick::Draw.new
74
+ gc.fill('black').circle(RADIUS, RADIUS, RADIUS, 0)
75
+ gc.draw crater
76
+ crater
77
+ end
78
+ CRATER_SHADOW = CRATER_IMAGE.shadow(0, 0, (SH_RADIUS - RADIUS) / 2, 1)
79
+
80
+ def blast x, y
81
+ # Draw the shadow (twice for more intensity), then erase a circle from the map.
82
+ @image.composite!(CRATER_SHADOW, x - SH_RADIUS, y - SH_RADIUS, Magick::AtopCompositeOp)
83
+ @image.composite!(CRATER_SHADOW, x - SH_RADIUS, y - SH_RADIUS, Magick::AtopCompositeOp)
84
+ @image.composite!(CRATER_IMAGE, x - RADIUS, y - RADIUS, Magick::DstOutCompositeOp)
85
+
86
+ # Isolate the affected portion of the RMagick image.
87
+ dirty_portion = @image.crop(x - SH_RADIUS, y - SH_RADIUS, SH_RADIUS * 2, SH_RADIUS * 2)
88
+ # Overwrite this part of the Gosu image. If the crater begins outside of the map, still
89
+ # just update the inner part.
90
+ @gosu_image.insert dirty_portion, [x - SH_RADIUS, 0].max, [y - SH_RADIUS, 0].max
91
+ end
92
+
93
+ private
94
+
95
+ def create_rmagick_map
96
+ # This is the one large RMagick image that represents the map.
97
+ @image = Magick::Image.new(WIDTH, HEIGHT) { self.background_color = 'none' }
98
+
99
+ # Set up a Draw object that fills with an earth texture.
100
+ earth = Magick::Image.read('media/Earth.png').first.resize(1.5)
101
+ gc = Magick::Draw.new
102
+ gc.pattern('earth', 0, 0, earth.columns, earth.rows) { gc.composite(0, 0, 0, 0, earth) }
103
+ gc.fill('earth')
104
+ gc.stroke('#603000').stroke_width(1.5)
105
+ # Draw a smooth bezier island onto the map!
106
+ polypoints = [0, HEIGHT]
107
+ 0.upto(8) do |x|
108
+ polypoints += [x * 100, HEIGHT * 0.2 + rand(HEIGHT * 0.8)]
109
+ end
110
+ polypoints += [WIDTH, HEIGHT]
111
+ gc.bezier(*polypoints)
112
+ gc.draw(@image)
113
+
114
+ # Create a bright-dark gradient fill, an image from it and change the map's
115
+ # brightness with it.
116
+ fill = Magick::GradientFill.new(0, HEIGHT * 0.4, WIDTH, HEIGHT * 0.4, '#fff', '#666')
117
+ gradient = Magick::Image.new(WIDTH, HEIGHT, fill)
118
+ gradient = @image.composite(gradient, 0, 0, Magick::InCompositeOp)
119
+ @image.composite!(gradient, 0, 0, Magick::MultiplyCompositeOp)
120
+
121
+ # Finally, place the star in the middle of the map, just onto the ground.
122
+ star = Magick::Image.read('media/LargeStar.png').first
123
+ star_y = 0
124
+ star_y += 20 until solid?(WIDTH / 2, star_y)
125
+ @image.composite!(star, (WIDTH - star.columns) / 2, star_y - star.rows * 0.85,
126
+ Magick::DstOverCompositeOp)
127
+ end
128
+ end
129
+
130
+ # Player class.
131
+ # Note that applies to the whole game:
132
+ # All objects implement an informal interface.
133
+ # draw: Draws the object (obviously)
134
+ # update: Moves the object etc., returns false if the object is to be deleted
135
+ # hit_by?(missile): Returns true if an object is hit by the missile, causing
136
+ # it to explode on this object.
137
+
138
+ class Player
139
+ # Magic numbers considered harmful! This is the height of the
140
+ # player as used for collision detection.
141
+ HEIGHT = 14
142
+
143
+ attr_reader :x, :y, :dead
144
+
145
+ def initialize(window, x, y, color)
146
+ # Only load the images once for all instances of this class.
147
+ @@images ||= Gosu::Image.load_tiles(window, "media/Soldier.png", 40, 50, false)
148
+
149
+ @window, @x, @y, @color = window, x, y, color
150
+ @vy = 0
151
+
152
+ # -1: left, +1: right
153
+ @dir = -1
154
+
155
+ # Aiming angle.
156
+ @angle = 90
157
+ end
158
+
159
+ def draw
160
+ if dead then
161
+ # Poor, broken soldier.
162
+ @@images[0].draw_rot(x, y, 0, 290 * @dir, 0.5, 0.65, @dir * 0.5, 0.5, @color)
163
+ @@images[2].draw_rot(x, y, 0, 160 * @dir, 0.95, 0.5, 0.5, @dir * 0.5, @color)
164
+ else
165
+ # Was moved last frame?
166
+ if @show_walk_anim
167
+ # Yes: Display walking animation.
168
+ frame = Gosu::milliseconds / 200 % 2
169
+ else
170
+ # No: Stand around (boring).
171
+ frame = 0
172
+ end
173
+
174
+ # Draw feet, then chest.
175
+ @@images[frame].draw(x - 10 * @dir, y - 20, 0, @dir * 0.5, 0.5, @color)
176
+ angle = @angle
177
+ angle = 180 - angle if @dir == -1
178
+ @@images[2].draw_rot(x, y - 5, 0, angle, 1, 0.5, 0.5, @dir * 0.5, @color)
179
+ end
180
+ end
181
+
182
+ def update
183
+ # First, assume that no walking happened this frame.
184
+ @show_walk_anim = false
185
+
186
+ # Gravity.
187
+ @vy += 1
188
+
189
+ if @vy > 1 then
190
+ # Move upwards until hitting something.
191
+ @vy.times do
192
+ if @window.map.solid?(x, y + 1)
193
+ @vy = 0
194
+ break
195
+ else
196
+ @y += 1
197
+ end
198
+ end
199
+ else
200
+ # Move downwards until hitting something.
201
+ (-@vy).times do
202
+ if @window.map.solid?(x, y - HEIGHT - 1)
203
+ @vy = 0
204
+ break
205
+ else
206
+ @y -= 1
207
+ end
208
+ end
209
+ end
210
+
211
+ # Soldiers are never deleted (they may die, though, but that is a different
212
+ # concept).
213
+ true
214
+ end
215
+
216
+ def aim_up
217
+ @angle -= 2 unless @angle < 10
218
+ end
219
+
220
+ def aim_down
221
+ @angle += 2 unless @angle > 170
222
+ end
223
+
224
+ def try_walk(dir)
225
+ @show_walk_anim = true
226
+ @dir = dir
227
+ # First, magically move up (so soldiers can run up hills)
228
+ 2.times { @y -= 1 unless @window.map.solid?(x, y - HEIGHT - 1) }
229
+ # Now move into the desired direction.
230
+ @x += dir unless @window.map.solid?(x + dir, y) or
231
+ @window.map.solid?(x + dir, y - HEIGHT)
232
+ # To make up for unnecessary movement upwards, sink downward again.
233
+ 2.times { @y += 1 unless @window.map.solid?(x, y + 1) }
234
+ end
235
+
236
+ def try_jump
237
+ @vy = -12 if @window.map.solid?(x, y + 1)
238
+ end
239
+
240
+ def shoot
241
+ @window.objects << Missile.new(@window, x + 10 * @dir, y - 10, @angle * @dir)
242
+ end
243
+
244
+ def hit_by? missile
245
+ if Gosu::distance(missile.x, missile.y, x, y) < 30 then
246
+ # Was hit :(
247
+ @dead = true
248
+ return true
249
+ else
250
+ return false
251
+ end
252
+ end
253
+ end
254
+
255
+ # Implements the same interface as Player, except it'S a missile!
256
+
257
+ class Missile
258
+ attr_reader :x, :y, :vx, :vy
259
+
260
+ # All missile instances use the same sound.
261
+ EXPLOSION = Gosu::Sample.new("media/Explosion.wav")
262
+
263
+ def initialize(window, x, y, angle)
264
+ # Horizontal/vertical velocity.
265
+ @vx, @vy = Gosu::offset_x(angle, 20).to_i, Gosu::offset_y(angle, 20).to_i
266
+
267
+ @window, @x, @y = window, x + @vx, y + @vy
268
+ end
269
+
270
+ def update
271
+ # Movement, gravity
272
+ @x += @vx
273
+ @y += @vy
274
+ @vy += 1
275
+ # Hit anything?
276
+ if @window.map.solid?(x, y) or @window.objects.any? { |o| o.hit_by?(self) } then
277
+ # Create great particles.
278
+ 5.times { @window.objects << Particle.new(@window, x - 25 + rand(51), y - 25 + rand(51)) }
279
+ @window.map.blast(x, y)
280
+ # Weeee, stereo sound!
281
+ EXPLOSION.play_pan((2 * @x - Map::WIDTH) / Map::WIDTH)
282
+ return false
283
+ else
284
+ return true
285
+ end
286
+ end
287
+
288
+ def draw
289
+ # Just draw a small quad.
290
+ @window.draw_quad(x-2, y-2, 0xff800000, x+2, y-2, 0xff800000,
291
+ x-2, y+2, 0xff800000, x+2, y+2, 0xff800000, 0)
292
+ end
293
+
294
+ def hit_by?(missile)
295
+ # Missiles can't be hit by other missiles!
296
+ false
297
+ end
298
+ end
299
+
300
+ # Very minimal object that just draws a fading particle.
301
+
302
+ class Particle
303
+ def initialize(window, x, y)
304
+ # All Particle instances use the same image
305
+ @@image ||= Gosu::Image.new(window, 'media/Smoke.png', false)
306
+
307
+ @x, @y = x, y
308
+ @color = Gosu::Color.new(255, 255, 255, 255)
309
+ end
310
+
311
+ def update
312
+ @y -= 5
313
+ @x = @x - 1 + rand(3)
314
+ @color.alpha -= 5
315
+
316
+ # Remove if faded completely.
317
+ @color.alpha > 0
318
+ end
319
+
320
+ def draw
321
+ @@image.draw(@x - 25, @y - 25, 0, 1, 1, @color)
322
+ end
323
+
324
+ def hit_by?(missile)
325
+ # Smoke can't be hit!
326
+ false
327
+ end
328
+ end
329
+
330
+ # Finally, the class that ties it all together.
331
+ # Very straightforward implementation.
332
+
333
+ class GameWindow < Gosu::Window
334
+ attr_reader :map, :objects
335
+
336
+ def initialize()
337
+ super(800, 600, false)
338
+ self.caption = "Gosu & RMagick Integration Demo"
339
+
340
+ # Texts to display in the appropriate situations.
341
+ @player_instructions = []
342
+ @player_won_messages = []
343
+ 2.times do |plr|
344
+ @player_instructions << Gosu::Image.from_text(self,
345
+ "It is the #{ plr == 0 ? 'green' : 'red' } toy soldier's turn.\n" +
346
+ "(Arrow keys to walk and aim, Return to jump, Space to shoot)",
347
+ Gosu::default_font_name, 30, 0, width, :center)
348
+ @player_won_messages << Gosu::Image.from_text(self,
349
+ "The #{ plr == 0 ? 'green' : 'red' } toy soldier has won!",
350
+ Gosu::default_font_name, 30, 5, width, :center)
351
+ end
352
+
353
+ # Create everything!
354
+ @map = Map.new(self)
355
+ @players = [Player.new(self, 200, 40, 0xff308000), Player.new(self, 600, 40, 0xff803000)]
356
+ @objects = @players.dup
357
+
358
+ # Let any player start.
359
+ @current_player = rand(2)
360
+ # Currently not waiting for a missile to hit something.
361
+ @waiting = false
362
+ end
363
+
364
+ def draw
365
+ # Draw the main game.
366
+ @map.draw
367
+ @objects.each { |o| o.draw }
368
+
369
+ # If any text should be displayed, draw it - and add a nice black border around it
370
+ # by drawing it four times, with a little offset in each direction.
371
+
372
+ cur_text = @player_instructions[@current_player] if not @waiting
373
+ cur_text = @player_won_messages[1 - @current_player] if @players[@current_player].dead
374
+
375
+ if cur_text then
376
+ x, y = 0, 30
377
+ cur_text.draw(x - 1, y, 0, 1, 1, 0xff000000)
378
+ cur_text.draw(x + 1, y, 0, 1, 1, 0xff000000)
379
+ cur_text.draw(x, y - 1, 0, 1, 1, 0xff000000)
380
+ cur_text.draw(x, y + 1, 0, 1, 1, 0xff000000)
381
+ cur_text.draw(x, y, 0, 1, 1, 0xffffffff)
382
+ end
383
+ end
384
+
385
+ def update
386
+ # if waiting for the next player's turn, continue to do so until the missile has
387
+ # hit something.
388
+ @waiting &&= !@objects.grep(Missile).empty?
389
+
390
+ # Remove all objects whose update method returns false.
391
+ @objects.reject! { |o| o.update == false }
392
+
393
+ # If it's a player's turn, forward controls.
394
+ if not @waiting and not @players[@current_player].dead then
395
+ player = @players[@current_player]
396
+ player.aim_up if button_down? Gosu::KbUp
397
+ player.aim_down if button_down? Gosu::KbDown
398
+ player.try_walk(-1) if button_down? Gosu::KbLeft
399
+ player.try_walk(+1) if button_down? Gosu::KbRight
400
+ player.try_jump if button_down? Gosu::KbReturn
401
+ end
402
+ end
403
+
404
+ def button_down(id)
405
+ if id == Gosu::KbSpace and not @waiting and not @players[@current_player].dead then
406
+ # Shoot! This is handled in button_down because holding space shouldn't auto-fire.
407
+ @players[@current_player].shoot
408
+ @current_player = 1 - @current_player
409
+ @waiting = true
410
+ end
411
+ # Very important feature! ;)
412
+ close if id == Gosu::KbEscape
413
+ end
414
+ end
415
+
416
+ # So far we have only defined how everything *should* work - now set it up and run it!
417
+ GameWindow.new.show
@@ -0,0 +1,154 @@
1
+ # This example demonstrates the use of the TextInput functionality.
2
+ # One can tab through, or click into the text fields and change it's contents.
3
+
4
+ # At its most basic form, you only need to create a new TextInput instance and
5
+ # set the text_input attribute of your window to it. Until you set this
6
+ # attribute to nil again, the TextInput object will build a text that can be
7
+ # accessed via TextInput#text.
8
+
9
+ # The TextInput object also maintains the position of the caret as the index
10
+ # of the character that it's left to via the caret_pos attribute. Furthermore,
11
+ # if there is a selection, the selection_start attribute yields its beginning,
12
+ # using the same indexing scheme. If there is no selection, selection_start
13
+ # is equal to caret_pos.
14
+
15
+ # A TextInput object is purely abstract, though; drawing the input field is left
16
+ # to the user. In this case, we are subclassing TextInput to add this code.
17
+ # As with most of Gosu, how this is handled is completely left open; the scheme
18
+ # presented here is not mandatory! Gosu only aims to provide enough code for
19
+ # games (or intermediate UI toolkits) to be built upon it.
20
+
21
+ require 'rubygems'
22
+ require 'gosu'
23
+
24
+ class TextField < Gosu::TextInput
25
+ # Some constants that define our appearance.
26
+ INACTIVE_COLOR = 0xcc666666
27
+ ACTIVE_COLOR = 0xccff6666
28
+ SELECTION_COLOR = 0xcc0000ff
29
+ CARET_COLOR = 0xffffffff
30
+ PADDING = 5
31
+
32
+ attr_reader :x, :y
33
+
34
+ def initialize(window, font, x, y)
35
+ # TextInput's constructor doesn't expect any arguments.
36
+ super()
37
+
38
+ @window, @font, @x, @y = window, font, x, y
39
+
40
+ # Start with a self-explanatory text in each field.
41
+ self.text = "Click to change text"
42
+ end
43
+
44
+ # Example filter method. You can truncate the text to employ a length limit (watch out
45
+ # with Ruby 1.8 and UTF-8!), limit the text to certain characters etc.
46
+ def filter text
47
+ text.upcase
48
+ end
49
+
50
+ def draw
51
+ # Depending on whether this is the currently selected input or not, change the
52
+ # background's color.
53
+ if @window.text_input == self then
54
+ background_color = ACTIVE_COLOR
55
+ else
56
+ background_color = INACTIVE_COLOR
57
+ end
58
+ @window.draw_quad(x - PADDING, y - PADDING, background_color,
59
+ x + width + PADDING, y - PADDING, background_color,
60
+ x - PADDING, y + height + PADDING, background_color,
61
+ x + width + PADDING, y + height + PADDING, background_color, 0)
62
+
63
+ # Calculate the position of the caret and the selection start.
64
+ pos_x = x + @font.text_width(self.text[0...self.caret_pos])
65
+ sel_x = x + @font.text_width(self.text[0...self.selection_start])
66
+
67
+ # Draw the selection background, if any; if not, sel_x and pos_x will be
68
+ # the same value, making this quad empty.
69
+ @window.draw_quad(sel_x, y, SELECTION_COLOR,
70
+ pos_x, y, SELECTION_COLOR,
71
+ sel_x, y + height, SELECTION_COLOR,
72
+ pos_x, y + height, SELECTION_COLOR, 0)
73
+
74
+ # Draw the caret; again, only if this is the currently selected field.
75
+ if @window.text_input == self then
76
+ @window.draw_line(pos_x, y, CARET_COLOR,
77
+ pos_x, y + height, CARET_COLOR, 0)
78
+ end
79
+
80
+ # Finally, draw the text itself!
81
+ @font.draw(self.text, x, y, 0)
82
+ end
83
+
84
+ # This text field grows with the text that's being entered.
85
+ # (Usually one would use clip_to and scroll around on the text field.)
86
+ def width
87
+ @font.text_width(self.text)
88
+ end
89
+
90
+ def height
91
+ @font.height
92
+ end
93
+
94
+ # Hit-test for selecting a text field with the mouse.
95
+ def under_point?(mouse_x, mouse_y)
96
+ mouse_x > x - PADDING and mouse_x < x + width + PADDING and
97
+ mouse_y > y - PADDING and mouse_y < y + height + PADDING
98
+ end
99
+
100
+ # Tries to move the caret to the position specifies by mouse_x
101
+ def move_caret(mouse_x)
102
+ # Test character by character
103
+ 1.upto(self.text.length) do |i|
104
+ if mouse_x < x + @font.text_width(text[0...i]) then
105
+ self.caret_pos = self.selection_start = i - 1;
106
+ return
107
+ end
108
+ end
109
+ # Default case: user must have clicked the right edge
110
+ self.caret_pos = self.selection_start = self.text.length
111
+ end
112
+ end
113
+
114
+ class TextInputWindow < Gosu::Window
115
+ def initialize
116
+ super(300, 200, false)
117
+ self.caption = "Text Input Example"
118
+
119
+ font = Gosu::Font.new(self, Gosu::default_font_name, 20)
120
+
121
+ # Set up an array of three text fields.
122
+ @text_fields = Array.new(3) { |index| TextField.new(self, font, 50, 30 + index * 50) }
123
+
124
+ @cursor = Gosu::Image.new(self, "media/Cursor.png", false)
125
+ end
126
+
127
+ def draw
128
+ @text_fields.each { |tf| tf.draw }
129
+ @cursor.draw(mouse_x, mouse_y, 0)
130
+ end
131
+
132
+ def button_down(id)
133
+ if id == Gosu::KbTab then
134
+ # Tab key will not be 'eaten' by text fields; use for switching through
135
+ # text fields.
136
+ index = @text_fields.index(self.text_input) || -1
137
+ self.text_input = @text_fields[(index + 1) % @text_fields.size]
138
+ elsif id == Gosu::KbEscape then
139
+ # Escape key will not be 'eaten' by text fields; use for deselecting.
140
+ if self.text_input then
141
+ self.text_input = nil
142
+ else
143
+ close
144
+ end
145
+ elsif id == Gosu::MsLeft then
146
+ # Mouse click: Select text field based on mouse position.
147
+ self.text_input = @text_fields.find { |tf| tf.under_point?(mouse_x, mouse_y) }
148
+ # Advanced: Move caret to clicked position
149
+ self.text_input.move_caret(mouse_x) unless self.text_input.nil?
150
+ end
151
+ end
152
+ end
153
+
154
+ TextInputWindow.new.show