glimr 0.1.0 → 0.1.1

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.
data/README ADDED
@@ -0,0 +1,222 @@
1
+ GlimR 0.1
2
+ Ruby OpenGL Widget Kit
3
+ 2006-07-14
4
+ -----------
5
+ http://glimr.rubyforge.org
6
+
7
+ Under development. Watch your head.
8
+
9
+ Funded as one of Ruby Central Inc.'s
10
+ Google Summer of Code 2006 projects.
11
+
12
+ http://code.google.com/soc/
13
+
14
+
15
+ Installation:
16
+ -------------
17
+
18
+ Requirements:
19
+ * Ruby OpenGL bindings and GLUT
20
+ * Ruby/SDL with SDL::TTF support
21
+
22
+ To install:
23
+ (sudo) ruby setup.rb
24
+
25
+ Should install and run on Windows, Linux and OS X.
26
+
27
+
28
+
29
+ How to use:
30
+ -----------
31
+ GlimR is an event-driven GUI toolkit.
32
+
33
+ You write programs by creating widgets,
34
+ attaching them to a parent widget and setting
35
+ event listeners to respond to user input.
36
+
37
+ GlimR can be used as either a stand-alone
38
+ GUI toolkit or embedded as a part of an
39
+ OpenGL application.
40
+
41
+ Currently GlimR has simple low-level widgets
42
+ and you need to build dialogs and such from
43
+ smaller bits.
44
+
45
+ Here's a small GlimR program:
46
+
47
+ require 'glimr'
48
+ include GlimR
49
+
50
+ name_form = VLayout.new(
51
+ :expand_height => true, :expand_width => true,
52
+ :align => :center, :valign => :middle)
53
+
54
+ label = Label.new(:text => "Enter your name:")
55
+ name_input = TextInput.new(:text => "")
56
+ submit = Button.new(:label => "OK"){|o,e|
57
+ name_form.attach Label.new(:text => "Hi, #{name_input.text}!")
58
+ }
59
+ name_form.attach label, name_input, submit
60
+
61
+ window = GLUTWindow.new(400, 200, "My App")
62
+ window.attach name_form
63
+ window.run
64
+
65
+
66
+ See tests/integration_tests/ for more sample code.
67
+
68
+
69
+
70
+ Embedding:
71
+ ----------
72
+ To embed GlimR in an OpenGL application, do this:
73
+ 1. Create a GlimR::Viewport as the root of your GlimR scene.
74
+ 2. Set the viewport's width and height to your window's width and height.
75
+ 3. Send user input events, window reshape events and a frame event to
76
+ the viewport. See an example on how to do this in
77
+ lib/glimr/renderer/glutwindow.rb.
78
+ 4. After drawing your OpenGL frame -- but before swapping buffers --
79
+ draw the viewport by calling viewport.render
80
+
81
+ To avoid loading GLUT, set $glimr_no_glut = true before loading GlimR.
82
+
83
+ To draw the GUI only when it has changed, render the viewport to a
84
+ texture and poll viewport.changed? to figure out if anything
85
+ redraw-worthy has taken place.
86
+
87
+ E.g. hypothetical frame loop:
88
+
89
+ input_events.each{|ev| viewport.multicast_event(ev) }
90
+ if reshape_event
91
+ viewport.multicast_event(reshape_event)
92
+ reshape_event = false
93
+ end
94
+ viewport.multicast_event(frame_event)
95
+ if viewport.changed?
96
+ gui_texture.render_into(w,h){ viewport.render }
97
+ end
98
+
99
+ draw_frame
100
+
101
+ gui_texture.draw_on_screen
102
+
103
+ Implementation of gui_texture left as an exercise for the reader.
104
+
105
+
106
+
107
+ Code structure:
108
+ ---------------
109
+
110
+ --- EVENTS
111
+
112
+ + lib/glimr/event.rb - GlimR::Event
113
+ - Events are glorified OpenStructs
114
+ - conforms to the DOM2 Event interface (but no subclasses)
115
+
116
+ + lib/glimr/eventlistener.rb - GlimR::EventListener
117
+ - EventListener mixin
118
+ - conforms to the DOM2 EventListener interface
119
+ (plus event multicasting)
120
+ - if you're ever used JavaScript, you should feel comfy
121
+
122
+ --- UTILITIES
123
+
124
+ + lib/glimr/configurable.rb - Configurable
125
+ - Configurable mixin for Hash-based initialization that works
126
+ with inheritance
127
+
128
+ + lib/glimr/layoutable.rb - GlimR::Layoutable
129
+ - Layoutable mixin
130
+ - used to make objects get layouted
131
+ - Widgets and Layouts are Layoutable
132
+
133
+ + lib/glimr/util.rb
134
+ - utility classes and core class extensions
135
+ (metaprogramming helpers mostly)
136
+
137
+ --- WIDGETS
138
+
139
+ + lib/glimr/widget.rb - GlimR::Widget
140
+ - base GlimR widget class
141
+ - inherits from GlimR::Model (see below)
142
+ - is Layoutable and EventListener
143
+
144
+ + lib/glimr/widgets.rb
145
+ - loads widgets from widgets/
146
+
147
+ + lib/glimr/default_theme/
148
+ - the theme dir contains the images and fonts used by the widgets
149
+
150
+ + lib/glimr/widgets/
151
+ - all different widgets
152
+
153
+ + lib/glimr/layout.rb - GlimR::Layout, GlimR::VLayout, GlimR::HLayout
154
+ - layout classes, inherit from GlimR::Widget
155
+
156
+ + button.rb - Button
157
+ + checkbox.rb - CheckBox
158
+ + radiogroup.rb - RadioGroup
159
+
160
+ + container.rb - Container
161
+ - viewport wrapper
162
+ + scrollable_container.rb - ScrollableContainer
163
+ + resizer.rb - Resizer
164
+ - resizing knob
165
+ + scrollbar.rb - VScrollBar, HScrollBar
166
+
167
+ + image.rb - Image
168
+ + stretchable_image.rb - StretchableImage
169
+
170
+ + label.rb - Label
171
+ + text_editor.rb - TextInput
172
+
173
+ --- RENDERER
174
+
175
+ + lib/glimr/renderer.rb
176
+ - loads renderer core classes
177
+ - loads standalone renderer if $glimr_no_glut is nil or false
178
+
179
+ + lib/glimr/renderer_core.rb
180
+ - loads renderer core classes
181
+
182
+ + lib/glimr/renderer/
183
+ - rendering-related classes
184
+
185
+ + glutwindow.rb - Glimr::GLUTWindow
186
+ - stand-alone GLUT window
187
+ - creates GL context
188
+ - passes input events to the viewport
189
+ - ctrl-q quits
190
+
191
+ + sceneobject.rb - GlimR::SceneObject
192
+ - class that other rendering classes inherit from
193
+ - defines child-parent relationships and state collapsing defaults
194
+
195
+ + viewport.rb - GlimR::Viewport
196
+ - the root of the GlimR drawing hierarchy
197
+ - captures input events, munges them to fit its view of the world
198
+ and passes them to the scene below
199
+ - handles clearing screen, setting up camera, clipping to viewport,
200
+ turning mouse_down & mouse_up into click
201
+
202
+ + model.rb - GlimR::Model
203
+ - convenience class for managing a transform with attached
204
+ material, texture, shader and geometry
205
+
206
+ + geometry.rb - GlimR::Geometry
207
+ - drawable geometry
208
+ - uses vertex arrays
209
+
210
+ + transform.rb - GlimR::Transform, GlimR::AngleAxis, GlimR::Quaternion
211
+ - does OpenGL transforms: translation, scaling, rotation
212
+ (with angle-axis or quaternion), custom transform matrix
213
+
214
+ + texture.rb - GlimR::Texture
215
+ - manages OpenGL textures
216
+ - handles loading image files to textures
217
+
218
+
219
+
220
+ Author:
221
+ -------
222
+ Ilmari Heikkinen <ilmari.heikkinen@gmail.com>
@@ -0,0 +1,21 @@
1
+ require 'rake'
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = 'glimr'
5
+ s.version = '0.1.1'
6
+ s.date = '2006-07-22'
7
+ s.summary = 'Ruby OpenGL GUI Widget Kit'
8
+ s.email = 'ilmari.heikkinen@gmail.com'
9
+ s.homepage = 'glimr.rubyforge.org'
10
+ s.rubyforge_project = 'glimr'
11
+ s.authors = ['Ilmari Heikkinen']
12
+ s.files = FileList[
13
+ '*',
14
+ 'lib/**/*.rb',
15
+ 'lib/**/*.png',
16
+ 'lib/**/*.ttf',
17
+ 'tests/**/*'
18
+ ].to_a
19
+ s.required_ruby_version = '>= 1.8.1'
20
+ s.requirements = ['Ruby-OpenGL >= 0.32g', 'Ruby/SDL >= 1.0.0']
21
+ end
@@ -1,387 +1,387 @@
1
- require 'glimr/renderer/viewport'
2
- require 'glimr/renderer/camera'
3
- require 'glimr/util'
4
- GC.disable
5
- require 'glut'
6
- GLUT.Init
7
-
8
-
9
- module GlimR
10
-
11
- # The GLUTWindow provides a stand-alone OpenGL context for GlimR.
12
- #
13
- # When rendering, a GLUTWindow first sends a frame event to its viewport,
14
- # then clears the buffer and tells its viewport to draw itself.
15
- #
16
- # The GLUTWindow is also responsible for mapping GLUT events into GlimR
17
- # events and sending them to the viewport.
18
- #
19
- class GLUTWindow
20
-
21
- @frame_time = 0
22
-
23
- # Index of the current window.
24
- def self.window_index
25
- @window_index ||= 0
26
- end
27
-
28
- # Array of windows created.
29
- def self.windows
30
- @windows ||= []
31
- end
32
-
33
- # Id of current GLUT window.
34
- def self.current_window
35
- windows[window_index]
36
- end
37
-
38
- # Sets the active GLUT window to the next window in order.
39
- def self.next_window
40
- if @window_index == 0
41
- slp = 0.033 - (Time.now.to_f - @frame_time)
42
- sleep(slp) if slp > 0
43
- @frame_time = Time.now.to_f
44
- end
45
- @window_index = (window_index + 1)%windows.size
46
- GLUT.SetWindow(current_window.window_id)
47
- current_window.heartbeat
48
- end
49
-
50
- # Creates a GLUT window with the title and adds it to windows list.
51
- def self.create_window(window)
52
- GLUT.InitDisplayMode(window.display_mode)
53
- GLUT.InitWindowSize(window.width, window.height)
54
- GLUT.InitWindowPosition(window.x, window.y) if window.x and window.y
55
- window.window_id = GLUT.CreateWindow window.title
56
- windows << window
57
- window.window_id
58
- end
59
-
60
- # Destroys the GLUT window w and deletes it from the windows list.
61
- # Exits if the window in question was the last remaining window.
62
- def self.destroy_window(w)
63
- windows.delete w
64
- GLUT.DestroyWindow w.window_id
65
- exit if windows.empty?
66
- end
67
-
68
- # GLUT idle function is to show the next window.
69
- GLUT.IdleFunc(lambda { next_window })
70
-
71
- attr_accessor :viewport
72
- attr_accessor :window_id
73
- attr_reader :width, :height, :title, :frame, :frames_drawn, :x, :y, :display_mode
74
-
75
- delegate :viewport, :<<, :attach, :detach, :camera=, :camera, :background, :background=
76
-
77
- # Creates a window with width w and height h.
78
- def initialize(w=400,h=300,title="GlimR - #{$0}",x=nil,y=nil)
79
- @viewport = Viewport.new
80
- @viewport.background = [1,1,1,1]
81
- @viewport.clear_depth_buffer = true
82
- @visible = true
83
- @display_mode = GLUT::DOUBLE | GLUT::RGB | GLUT::DEPTH
84
- @x = x
85
- @y = y
86
- @viewport.x = 0
87
- @viewport.y = 0
88
- @frame_event = Event.new(:frame)
89
- @reshape_event = Event.new(:resize)
90
- @frame_time = @last_frame_time = Time.now.to_f
91
- @frame = @frames_drawn = 0
92
- @title = title
93
- @event_queue = {}
94
- @mouse_buttons_down = {}
95
- @mouse_properties = {}
96
- @modifiers = {}
97
- reshape(w,h)
98
- init_glut
99
- end
100
-
101
- # Initialize GLUT state for the window, then ask GLUTWindow class to
102
- # create the window.
103
- def init_glut
104
- self.class.create_window(self)
105
- set_callback_funcs
106
- end
107
-
108
- # GLUT ReshapeFunc.
109
- # Sets viewport width and height to w and h, respectively,
110
- # and sends a resize event to the viewport.
111
- def reshape(w,h)
112
- @width = w
113
- @height = h
114
- @reshape_event.time = Time.now.to_f
115
- @reshape_event.bubbles = false
116
- @reshape_event.width = w
117
- @reshape_event.height = h
118
- viewport.multicast_event(@reshape_event)
119
- end
120
-
121
- # Enters GLUT.MainLoop.
122
- def run
123
- GLUT.MainLoop
124
- end
125
-
126
- def need_redraw?
127
- @visible and (changed? or damaged?)
128
- end
129
-
130
- def damaged?
131
- GLUT.LayerGet(GLUT::NORMAL_DAMAGED) == GL::TRUE
132
- end
133
-
134
- def changed?
135
- viewport.changed?
136
- end
137
-
138
- def heartbeat
139
- GC.disable
140
- @frame_time = Time.now.to_f
141
- @frame_event.time = @frame_time
142
- @frame_event.bubbles = false
143
- @event_queue.values.each{|evt|
144
- viewport.dispatch_event(evt)
145
- }
146
- @event_queue.clear
147
- viewport.multicast_event(@frame_event)
148
- GLUT.PostRedisplay if need_redraw?
149
- GC.enable
150
- @frame += 1
151
- end
152
-
153
- def queue_event evt
154
- if evt.type == :mouse_move and @event_queue[:mouse_move]
155
- evt.dx += @event_queue[:mouse_move].dx
156
- evt.dy += @event_queue[:mouse_move].dy
157
- evt.params[:dx] = evt.dx
158
- evt.params[:dy] = evt.dy
159
- evt.params[:time] = evt.time = Time.now.to_f
160
- end
161
- @event_queue[evt.type] = evt
162
- end
163
-
164
- # GLUT DisplayFunc.
165
- #
166
- # Displays the GLUTWindow. Sleeps to maintain a maximum
167
- # framerate of 33fps, then displays previously rendered frame.
168
- # Then sends a frame event to the viewport.
169
- #
170
- # If the GLUTWindow is visible, clears the buffer and tells viewport to draw itself.
171
- # Finally increments the frame counter.
172
- #
173
- def display
174
- GC.disable
175
- viewport.render
176
- GLUT.SwapBuffers
177
- @frames_drawn += 1
178
- end
179
-
180
- # Asks GLUTWindow class to destroy the window.
181
- def close(*a)
182
- self.class.destroy_window(self)
183
- end
184
-
185
- # Toggles fullscreen mode.
186
- def fullscreen(*a)
187
- if not @fullscreen
188
- @fullscreen = %w(X Y WIDTH HEIGHT).map{|cn|
189
- GLUT.Get(GLUT.const_get("WINDOW_#{cn}"))
190
- }
191
- GLUT.FullScreen
192
- else
193
- x,y,w,h = *@fullscreen
194
- GLUT.ReshapeWindow(w,h)
195
- GLUT.PositionWindow(x,y)
196
- @fullscreen = nil
197
- end
198
- end
199
-
200
- # Takes screenshot (not implemented.)
201
- def take_screenshot(*a)
202
- #screenshot
203
- end
204
-
205
- # Enters the profiler, lets run for 5 seconds, then
206
- # exits.
207
- def profiler(*a)
208
- Thread.new{
209
- puts "Entering profiler"
210
- require 'profile'
211
- sleep 5
212
- exit
213
- }
214
- end
215
-
216
- # Set GLUT callback functions to the object's methods that have
217
- # the rubyized name of the corresponding callback function.
218
- # That's not very clear, is it. Maybe this example sheds some light on it.
219
- #
220
- # GLUT::ReshapeFunc is set to "reshape"-method
221
- # GLUT::SpaceballMotionFunc is set to "spaceball_motion"-method
222
- # GLUT::MouseFunc is set to "mouse"-method
223
- # ... and so on for every GLUT method that ends with Func.
224
- #
225
- # If there is no corresponding method (e.g. there's no "idle"-method), the
226
- # callback won't be set.
227
- #
228
- # List of callback names (avoid these in non-callback method names):
229
- #
230
- # ButtonBoxFunc => button_box
231
- # DialsFunc => dials
232
- # DisplayFunc => display
233
- # EntryFunc => entry
234
- # IdleFunc => idle
235
- # KeyboardFunc => keyboard
236
- # MotionFunc => motion
237
- # MouseFunc => mouse
238
- # OverlayDisplayFunc => overlay_display
239
- # PassiveMotionFunc => passive_motion
240
- # ReshapeFunc => reshape
241
- # SpaceballButtonFunc => spaceball_button
242
- # SpaceballMotionFunc => spaceball_motion
243
- # SpaceballRotateFunc => spaceball_rotate
244
- # SpecialFunc => special
245
- # TabletButtonFunc => tablet_button
246
- # TabletMotionFunc => tablet_motion
247
- # TimerFunc => timer
248
- # VisibilityFunc => visibility
249
- # WindowStatusFunc => window_status
250
- #
251
- def set_callback_funcs
252
- GLUT.methods.grep(/Func$/).sort.each{|callback_setter_name|
253
- # Change FooBarFunc into foo_bar
254
- callback_method_name = callback_setter_name[0,1].downcase +
255
- callback_setter_name[1..-5].
256
- gsub(/[A-Z]/){|cap| "_" + cap.downcase}
257
- if respond_to?(callback_method_name.to_sym)
258
- callback_method = method(callback_method_name).to_proc
259
- GLUT.__send__(callback_setter_name, callback_method)
260
- end
261
- }
262
- @mouse_over_window = true
263
- end
264
-
265
- MOUSE_BUTTONS = [:left, :middle, :right, :wheel_up, :wheel_down]
266
-
267
- # GLUT EntryFunc.
268
- def entry(enter)
269
- @mouse_over_window = (enter == 1)
270
- if @mouse_over_window
271
- @mouse_x, @mouse_y = @prev_mouse_x, @prev_mouse_y
272
- else
273
- @mouse_x = @mouse_y = nil
274
- end
275
- end
276
-
277
- # Updates current mouse coordinates.
278
- def update_coords(x,y,update_modifier_keys=true)
279
- props = {
280
- :target => @target,
281
- :x => x, :y => y,
282
- :dx => (@prev_mouse_x ? x-@prev_mouse_x : 0),
283
- :dy => (@prev_mouse_y ? y-@prev_mouse_y : 0),
284
- :buttons => @mouse_buttons_down
285
- }
286
- @prev_mouse_x, @prev_mouse_y = x, y
287
- @mouse_x, @mouse_y = x, y if @mouse_over_window
288
- update_modifiers if update_modifier_keys
289
- @mouse_properties = @mouse_properties.merge(props).merge(@modifiers)
290
- end
291
-
292
- # GLUT PassiveMotionFunc. Moving mouse without buttons depressed
293
- def passive_motion(x,y)
294
- event_properties = update_coords(x,y,false)
295
- queue_event Event.new(:mouse_move, event_properties)
296
- end
297
-
298
- # GLUT MotionFunc. Moving mouse with mouse button depressed.
299
- def motion(x,y)
300
- event_properties = update_coords(x,y,false)
301
- queue_event Event.new(:mouse_move, event_properties)
302
- end
303
-
304
- # GLUT ButtonBoxFunc.
305
- def button_box(*a)
306
- end
307
-
308
- # GLUT VisibilityFunc. Toggles window visibility variable based on the argument.
309
- def visibility(visible)
310
- @visible = (visible == GLUT::VISIBLE)
311
- end
312
-
313
- # GLUT WindowStatusFunc.
314
- def window_status(*a)
315
- end
316
-
317
- # GLUT MouseFunc. Fired when mouse button pressed or released.
318
- def mouse(button, down, x, y)
319
- down = (down == 0)
320
- button = MOUSE_BUTTONS[button]
321
- @mouse_buttons_down[button] = down
322
- event_type = (down ? :mouse_down : :mouse_up)
323
- event_properties = update_coords(x,y)
324
- event_properties[:button] = button
325
- queue_event Event.new(event_type, event_properties)
326
- end
327
-
328
- # Gets modifier key states and assigns them to
329
- # @modifiers-Hash and returns the Hash.
330
- def update_modifiers
331
- modifier_bitfield = GLUT.GetModifiers
332
- GLUT.constants.grep(/^ACTIVE_/).each{|cn|
333
- @modifiers[cn[7..-1].downcase.to_sym] = (modifier_bitfield & GLUT.const_get(cn) > 0)
334
- }
335
- @modifiers
336
- end
337
-
338
- # GLUT KeyboardFunc. Fired when key pressed.
339
- # Multicasts key_down and key_up.
340
- #
341
- # Note: there is no "Hold button down"-functionality in GLUT.
342
- #
343
- def keyboard(key, x, y)
344
- event_properties = update_coords(x,y).merge(:key => key)
345
- ev = Event.new(:key_down, event_properties)
346
- @viewport.multicast_event(ev)
347
- ev = Event.new(:key_up, event_properties)
348
- @viewport.multicast_event(ev)
349
- handler = keyhandlers[key] if @modifiers[:ctrl] and not (@modifiers[:shift] or @modifiers[:alt])
350
- if handler and not (ev.stopped or ev.cancelled)
351
- if handler.respond_to? :call
352
- handler.call(key, x, y)
353
- else
354
- __send__(handler, key, x, y)
355
- end
356
- end
357
- end
358
-
359
- # Handles special key presses.
360
- #
361
- # Converts the special key to a symbol by stripping KEY_
362
- # off the front and downcasing the remains, e.g. :f1, :left, :right.
363
- #
364
- # Delegates the symbol to #keyboard.
365
- #
366
- def special(key, x, y)
367
- cn = GLUT.constants.grep(/^KEY_/).find{|k| GLUT.const_get(k) == key}
368
- key = cn.sub(/^KEY_/, '').downcase.to_sym
369
- keyboard(key, x, y)
370
- end
371
-
372
- # Default key handlers.
373
- #
374
- # ESC closes window, f toggles fullscreen, p enters profiler, s takes screenshot.
375
- #
376
- def keyhandlers
377
- @keyhandlers ||= {
378
- 17 => :close,
379
- 6 => :fullscreen,
380
- 16 => :profiler,
381
- 20 => :take_screenshot,
382
- }
383
- end
384
-
385
- end
386
-
387
- end
1
+ require 'glimr/renderer/viewport'
2
+ require 'glimr/renderer/camera'
3
+ require 'glimr/util'
4
+ GC.disable
5
+ require 'glut'
6
+ GLUT.Init
7
+
8
+
9
+ module GlimR
10
+
11
+ # The GLUTWindow provides a stand-alone OpenGL context for GlimR.
12
+ #
13
+ # When rendering, a GLUTWindow first sends a frame event to its viewport,
14
+ # then clears the buffer and tells its viewport to draw itself.
15
+ #
16
+ # The GLUTWindow is also responsible for mapping GLUT events into GlimR
17
+ # events and sending them to the viewport.
18
+ #
19
+ class GLUTWindow
20
+
21
+ @frame_time = 0
22
+
23
+ # Index of the current window.
24
+ def self.window_index
25
+ @window_index ||= 0
26
+ end
27
+
28
+ # Array of windows created.
29
+ def self.windows
30
+ @windows ||= []
31
+ end
32
+
33
+ # Id of current GLUT window.
34
+ def self.current_window
35
+ windows[window_index]
36
+ end
37
+
38
+ # Sets the active GLUT window to the next window in order.
39
+ def self.next_window
40
+ GC.enable
41
+ if @window_index == 0
42
+ slp = 0.033 - (Time.now.to_f - @frame_time)
43
+ sleep(slp) if slp > 0
44
+ @frame_time = Time.now.to_f
45
+ end
46
+ @window_index = (window_index + 1)%windows.size
47
+ GC.disable
48
+ GLUT.SetWindow(current_window.window_id)
49
+ current_window.heartbeat
50
+ end
51
+
52
+ # Creates a GLUT window with the title and adds it to windows list.
53
+ def self.create_window(window)
54
+ GLUT.InitDisplayMode(window.display_mode)
55
+ GLUT.InitWindowSize(window.width, window.height)
56
+ GLUT.InitWindowPosition(window.x, window.y) if window.x and window.y
57
+ window.window_id = GLUT.CreateWindow window.title
58
+ windows << window
59
+ window.window_id
60
+ end
61
+
62
+ # Destroys the GLUT window w and deletes it from the windows list.
63
+ # Exits if the window in question was the last remaining window.
64
+ def self.destroy_window(w)
65
+ windows.delete w
66
+ GLUT.DestroyWindow w.window_id
67
+ exit if windows.empty?
68
+ end
69
+
70
+ # GLUT idle function is to show the next window.
71
+ GLUT.IdleFunc(lambda { next_window })
72
+
73
+ attr_accessor :viewport
74
+ attr_accessor :window_id
75
+ attr_reader :width, :height, :title, :frame, :frames_drawn, :x, :y, :display_mode
76
+
77
+ delegate :viewport, :<<, :attach, :detach, :camera=, :camera, :background, :background=
78
+
79
+ # Creates a window with width w and height h.
80
+ def initialize(w=400,h=300,title="GlimR - #{$0}",x=nil,y=nil)
81
+ @viewport = Viewport.new
82
+ @viewport.background = [1,1,1,1]
83
+ @viewport.clear_depth_buffer = true
84
+ @visible = true
85
+ @display_mode = GLUT::DOUBLE | GLUT::RGB | GLUT::DEPTH
86
+ @x = x
87
+ @y = y
88
+ @viewport.x = 0
89
+ @viewport.y = 0
90
+ @frame_event = Event.new(:frame)
91
+ @reshape_event = Event.new(:window_resize)
92
+ @frame_time = @last_frame_time = Time.now.to_f
93
+ @frame = @frames_drawn = 0
94
+ @title = title
95
+ @event_queue = {}
96
+ @mouse_buttons_down = {}
97
+ @mouse_properties = {}
98
+ @modifiers = {}
99
+ reshape(w,h)
100
+ init_glut
101
+ end
102
+
103
+ # Initialize GLUT state for the window, then ask GLUTWindow class to
104
+ # create the window.
105
+ def init_glut
106
+ self.class.create_window(self)
107
+ set_callback_funcs
108
+ end
109
+
110
+ # GLUT ReshapeFunc.
111
+ # Sets viewport width and height to w and h, respectively,
112
+ # and sends a window_resize event to the viewport.
113
+ def reshape(w,h)
114
+ @width = w
115
+ @height = h
116
+ @reshape_event.time = Time.now.to_f
117
+ @reshape_event.bubbles = false
118
+ @reshape_event.width = w
119
+ @reshape_event.height = h
120
+ viewport.multicast_event(@reshape_event)
121
+ end
122
+
123
+ # Enters GLUT.MainLoop.
124
+ def run
125
+ GLUT.MainLoop
126
+ end
127
+
128
+ def need_redraw?
129
+ @visible and (changed? or damaged?)
130
+ end
131
+
132
+ def damaged?
133
+ GLUT.LayerGet(GLUT::NORMAL_DAMAGED) == GL::TRUE
134
+ end
135
+
136
+ def changed?
137
+ viewport.changed?
138
+ end
139
+
140
+ def heartbeat
141
+ @frame_time = Time.now.to_f
142
+ @frame_event.time = @frame_time
143
+ @frame_event.bubbles = false
144
+ @event_queue.values.each{|evt|
145
+ viewport.dispatch_event(evt)
146
+ }
147
+ @event_queue.clear
148
+ viewport.multicast_event(@frame_event)
149
+ GLUT.PostRedisplay if need_redraw?
150
+ @frame += 1
151
+ end
152
+
153
+ def queue_event evt
154
+ if evt.type == :mouse_move and @event_queue[:mouse_move]
155
+ evt.dx += @event_queue[:mouse_move].dx
156
+ evt.dy += @event_queue[:mouse_move].dy
157
+ evt.params[:dx] = evt.dx
158
+ evt.params[:dy] = evt.dy
159
+ evt.params[:time] = evt.time = Time.now.to_f
160
+ end
161
+ @event_queue[evt.type] = evt
162
+ end
163
+
164
+ # GLUT DisplayFunc.
165
+ #
166
+ # Displays the GLUTWindow. Sleeps to maintain a maximum
167
+ # framerate of 33fps, then displays previously rendered frame.
168
+ # Then sends a frame event to the viewport.
169
+ #
170
+ # If the GLUTWindow is visible, clears the buffer and tells viewport to draw itself.
171
+ # Finally increments the frame counter.
172
+ #
173
+ def display
174
+ GC.disable
175
+ viewport.render
176
+ GLUT.SwapBuffers
177
+ @frames_drawn += 1
178
+ end
179
+
180
+ # Asks GLUTWindow class to destroy the window.
181
+ def close(*a)
182
+ self.class.destroy_window(self)
183
+ end
184
+
185
+ # Toggles fullscreen mode.
186
+ def fullscreen(*a)
187
+ if not @fullscreen
188
+ @fullscreen = %w(X Y WIDTH HEIGHT).map{|cn|
189
+ GLUT.Get(GLUT.const_get("WINDOW_#{cn}"))
190
+ }
191
+ GLUT.FullScreen
192
+ else
193
+ x,y,w,h = *@fullscreen
194
+ GLUT.ReshapeWindow(w,h)
195
+ GLUT.PositionWindow(x,y)
196
+ @fullscreen = nil
197
+ end
198
+ end
199
+
200
+ # Takes screenshot (not implemented.)
201
+ def take_screenshot(*a)
202
+ #screenshot
203
+ end
204
+
205
+ # Enters the profiler, lets run for 5 seconds, then
206
+ # exits.
207
+ def profiler(*a)
208
+ Thread.new{
209
+ puts "Entering profiler"
210
+ require 'profile'
211
+ sleep 5
212
+ exit
213
+ }
214
+ end
215
+
216
+ # Set GLUT callback functions to the object's methods that have
217
+ # the rubyized name of the corresponding callback function.
218
+ # That's not very clear, is it. Maybe this example sheds some light on it.
219
+ #
220
+ # GLUT::ReshapeFunc is set to "reshape"-method
221
+ # GLUT::SpaceballMotionFunc is set to "spaceball_motion"-method
222
+ # GLUT::MouseFunc is set to "mouse"-method
223
+ # ... and so on for every GLUT method that ends with Func.
224
+ #
225
+ # If there is no corresponding method (e.g. there's no "idle"-method), the
226
+ # callback won't be set.
227
+ #
228
+ # List of callback names (avoid these in non-callback method names):
229
+ #
230
+ # ButtonBoxFunc => button_box
231
+ # DialsFunc => dials
232
+ # DisplayFunc => display
233
+ # EntryFunc => entry
234
+ # IdleFunc => idle
235
+ # KeyboardFunc => keyboard
236
+ # MotionFunc => motion
237
+ # MouseFunc => mouse
238
+ # OverlayDisplayFunc => overlay_display
239
+ # PassiveMotionFunc => passive_motion
240
+ # ReshapeFunc => reshape
241
+ # SpaceballButtonFunc => spaceball_button
242
+ # SpaceballMotionFunc => spaceball_motion
243
+ # SpaceballRotateFunc => spaceball_rotate
244
+ # SpecialFunc => special
245
+ # TabletButtonFunc => tablet_button
246
+ # TabletMotionFunc => tablet_motion
247
+ # TimerFunc => timer
248
+ # VisibilityFunc => visibility
249
+ # WindowStatusFunc => window_status
250
+ #
251
+ def set_callback_funcs
252
+ GLUT.methods.grep(/Func$/).sort.each{|callback_setter_name|
253
+ # Change FooBarFunc into foo_bar
254
+ callback_method_name = callback_setter_name[0,1].downcase +
255
+ callback_setter_name[1..-5].
256
+ gsub(/[A-Z]/){|cap| "_" + cap.downcase}
257
+ if respond_to?(callback_method_name.to_sym)
258
+ callback_method = method(callback_method_name).to_proc
259
+ GLUT.__send__(callback_setter_name, callback_method)
260
+ end
261
+ }
262
+ @mouse_over_window = true
263
+ end
264
+
265
+ MOUSE_BUTTONS = [:left, :middle, :right, :wheel_up, :wheel_down]
266
+
267
+ # GLUT EntryFunc.
268
+ def entry(enter)
269
+ @mouse_over_window = (enter == 1)
270
+ if @mouse_over_window
271
+ @mouse_x, @mouse_y = @prev_mouse_x, @prev_mouse_y
272
+ else
273
+ @mouse_x = @mouse_y = nil
274
+ end
275
+ end
276
+
277
+ # Updates current mouse coordinates.
278
+ def update_coords(x,y,update_modifier_keys=true)
279
+ props = {
280
+ :target => @target,
281
+ :x => x, :y => y,
282
+ :dx => (@prev_mouse_x ? x-@prev_mouse_x : 0),
283
+ :dy => (@prev_mouse_y ? y-@prev_mouse_y : 0),
284
+ :buttons => @mouse_buttons_down
285
+ }
286
+ @prev_mouse_x, @prev_mouse_y = x, y
287
+ @mouse_x, @mouse_y = x, y if @mouse_over_window
288
+ update_modifiers if update_modifier_keys
289
+ @mouse_properties = @mouse_properties.merge(props).merge(@modifiers)
290
+ end
291
+
292
+ # GLUT PassiveMotionFunc. Moving mouse without buttons depressed
293
+ def passive_motion(x,y)
294
+ event_properties = update_coords(x,y,false)
295
+ queue_event Event.new(:mouse_move, event_properties)
296
+ end
297
+
298
+ # GLUT MotionFunc. Moving mouse with mouse button depressed.
299
+ def motion(x,y)
300
+ event_properties = update_coords(x,y,false)
301
+ queue_event Event.new(:mouse_move, event_properties)
302
+ end
303
+
304
+ # GLUT ButtonBoxFunc.
305
+ def button_box(*a)
306
+ end
307
+
308
+ # GLUT VisibilityFunc. Toggles window visibility variable based on the argument.
309
+ def visibility(visible)
310
+ @visible = (visible == GLUT::VISIBLE)
311
+ end
312
+
313
+ # GLUT WindowStatusFunc.
314
+ def window_status(*a)
315
+ end
316
+
317
+ # GLUT MouseFunc. Fired when mouse button pressed or released.
318
+ def mouse(button, down, x, y)
319
+ down = (down == 0)
320
+ button = MOUSE_BUTTONS[button]
321
+ @mouse_buttons_down[button] = down
322
+ event_type = (down ? :mouse_down : :mouse_up)
323
+ event_properties = update_coords(x,y)
324
+ event_properties[:button] = button
325
+ queue_event Event.new(event_type, event_properties)
326
+ end
327
+
328
+ # Gets modifier key states and assigns them to
329
+ # @modifiers-Hash and returns the Hash.
330
+ def update_modifiers
331
+ modifier_bitfield = GLUT.GetModifiers
332
+ GLUT.constants.grep(/^ACTIVE_/).each{|cn|
333
+ @modifiers[cn[7..-1].downcase.to_sym] = (modifier_bitfield & GLUT.const_get(cn) > 0)
334
+ }
335
+ @modifiers
336
+ end
337
+
338
+ # GLUT KeyboardFunc. Fired when key pressed.
339
+ # Multicasts key_down and key_up.
340
+ #
341
+ # Note: there is no "Hold button down"-functionality in GLUT.
342
+ #
343
+ def keyboard(key, x, y)
344
+ event_properties = update_coords(x,y).merge(:key => key)
345
+ ev = Event.new(:key_down, event_properties)
346
+ @viewport.multicast_event(ev)
347
+ ev = Event.new(:key_up, event_properties)
348
+ @viewport.multicast_event(ev)
349
+ handler = keyhandlers[key] if @modifiers[:ctrl] and not (@modifiers[:shift] or @modifiers[:alt])
350
+ if handler and not (ev.stopped or ev.cancelled)
351
+ if handler.respond_to? :call
352
+ handler.call(key, x, y)
353
+ else
354
+ __send__(handler, key, x, y)
355
+ end
356
+ end
357
+ end
358
+
359
+ # Handles special key presses.
360
+ #
361
+ # Converts the special key to a symbol by stripping KEY_
362
+ # off the front and downcasing the remains, e.g. :f1, :left, :right.
363
+ #
364
+ # Delegates the symbol to #keyboard.
365
+ #
366
+ def special(key, x, y)
367
+ cn = GLUT.constants.grep(/^KEY_/).find{|k| GLUT.const_get(k) == key}
368
+ key = cn.sub(/^KEY_/, '').downcase.to_sym
369
+ keyboard(key, x, y)
370
+ end
371
+
372
+ # Default key handlers.
373
+ #
374
+ # ESC closes window, f toggles fullscreen, p enters profiler, s takes screenshot.
375
+ #
376
+ def keyhandlers
377
+ @keyhandlers ||= {
378
+ 17 => :close,
379
+ 6 => :fullscreen,
380
+ 16 => :profiler,
381
+ 20 => :take_screenshot,
382
+ }
383
+ end
384
+
385
+ end
386
+
387
+ end