glimr 0.1.0 → 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
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