glimr 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.
Files changed (90) hide show
  1. data/lib/glimr.rb +2 -0
  2. data/lib/glimr/configurable.rb +37 -0
  3. data/lib/glimr/default_theme/button_bg.png +0 -0
  4. data/lib/glimr/default_theme/button_bg_down.png +0 -0
  5. data/lib/glimr/default_theme/button_cover.png +0 -0
  6. data/lib/glimr/default_theme/button_cover_down.png +0 -0
  7. data/lib/glimr/default_theme/button_focus.png +0 -0
  8. data/lib/glimr/default_theme/checkbox_bg.png +0 -0
  9. data/lib/glimr/default_theme/checkbox_checked_bg.png +0 -0
  10. data/lib/glimr/default_theme/font.ttf +0 -0
  11. data/lib/glimr/default_theme/hscroller_bg.png +0 -0
  12. data/lib/glimr/default_theme/radiobutton_bg.png +0 -0
  13. data/lib/glimr/default_theme/radiobutton_checked_bg.png +0 -0
  14. data/lib/glimr/default_theme/resizer_down.png +0 -0
  15. data/lib/glimr/default_theme/resizer_up.png +0 -0
  16. data/lib/glimr/default_theme/scroll_down_down.png +0 -0
  17. data/lib/glimr/default_theme/scroll_down_up.png +0 -0
  18. data/lib/glimr/default_theme/scroll_hknob_down.png +0 -0
  19. data/lib/glimr/default_theme/scroll_hknob_up.png +0 -0
  20. data/lib/glimr/default_theme/scroll_left_down.png +0 -0
  21. data/lib/glimr/default_theme/scroll_left_up.png +0 -0
  22. data/lib/glimr/default_theme/scroll_right_down.png +0 -0
  23. data/lib/glimr/default_theme/scroll_right_up.png +0 -0
  24. data/lib/glimr/default_theme/scroll_up_down.png +0 -0
  25. data/lib/glimr/default_theme/scroll_up_up.png +0 -0
  26. data/lib/glimr/default_theme/scroll_vknob_down.png +0 -0
  27. data/lib/glimr/default_theme/scroll_vknob_up.png +0 -0
  28. data/lib/glimr/default_theme/text_cursor.png +0 -0
  29. data/lib/glimr/default_theme/text_cursor_insert.png +0 -0
  30. data/lib/glimr/default_theme/text_input_bg.png +0 -0
  31. data/lib/glimr/default_theme/vscroller_bg.png +0 -0
  32. data/lib/glimr/event.rb +41 -0
  33. data/lib/glimr/eventlistener.rb +209 -0
  34. data/lib/glimr/layoutable.rb +520 -0
  35. data/lib/glimr/renderer.rb +2 -0
  36. data/lib/glimr/renderer/camera.rb +63 -0
  37. data/lib/glimr/renderer/geometry.rb +194 -0
  38. data/lib/glimr/renderer/glutwindow.rb +387 -0
  39. data/lib/glimr/renderer/light.rb +43 -0
  40. data/lib/glimr/renderer/material.rb +66 -0
  41. data/lib/glimr/renderer/model.rb +103 -0
  42. data/lib/glimr/renderer/orthoprojection.rb +21 -0
  43. data/lib/glimr/renderer/raw.rb +34 -0
  44. data/lib/glimr/renderer/sceneobject.rb +279 -0
  45. data/lib/glimr/renderer/shader.rb +14 -0
  46. data/lib/glimr/renderer/texture.rb +280 -0
  47. data/lib/glimr/renderer/transform.rb +322 -0
  48. data/lib/glimr/renderer/viewport.rb +349 -0
  49. data/lib/glimr/renderer_core.rb +10 -0
  50. data/lib/glimr/util.rb +247 -0
  51. data/lib/glimr/widget.rb +87 -0
  52. data/lib/glimr/widgets.rb +37 -0
  53. data/lib/glimr/widgets/button.rb +277 -0
  54. data/lib/glimr/widgets/checkbox.rb +82 -0
  55. data/lib/glimr/widgets/container.rb +84 -0
  56. data/lib/glimr/widgets/image.rb +82 -0
  57. data/lib/glimr/widgets/label.rb +91 -0
  58. data/lib/glimr/widgets/layout.rb +227 -0
  59. data/lib/glimr/widgets/list.rb +28 -0
  60. data/lib/glimr/widgets/radiogroup.rb +118 -0
  61. data/lib/glimr/widgets/resizer.rb +31 -0
  62. data/lib/glimr/widgets/scrollable_container.rb +67 -0
  63. data/lib/glimr/widgets/scrollbar.rb +496 -0
  64. data/lib/glimr/widgets/stretchable_image.rb +135 -0
  65. data/lib/glimr/widgets/text_editor.rb +349 -0
  66. data/tests/assets/datatowers_crop.jpg +0 -0
  67. data/tests/assets/download_progress_meter.png +0 -0
  68. data/tests/assets/metalwing2.png +0 -0
  69. data/tests/assets/redhairgreeneyes3.jpg +0 -0
  70. data/tests/demo_apps/spinning_ruby.rb +37 -0
  71. data/tests/integration_tests/run_all.rb +8 -0
  72. data/tests/integration_tests/test_button.rb +22 -0
  73. data/tests/integration_tests/test_checkbox.rb +21 -0
  74. data/tests/integration_tests/test_container.rb +22 -0
  75. data/tests/integration_tests/test_label.rb +12 -0
  76. data/tests/integration_tests/test_layout.rb +43 -0
  77. data/tests/integration_tests/test_radiogroup.rb +16 -0
  78. data/tests/integration_tests/test_renderer.rb +44 -0
  79. data/tests/integration_tests/test_renderer2.rb +36 -0
  80. data/tests/integration_tests/test_scrollable_container.rb +34 -0
  81. data/tests/integration_tests/test_scrollbar.rb +20 -0
  82. data/tests/integration_tests/test_stretchable_image.rb +34 -0
  83. data/tests/integration_tests/test_text_input.rb +20 -0
  84. data/tests/integration_tests/test_zsort.rb +18 -0
  85. data/tests/unit_tests/test_button.rb +93 -0
  86. data/tests/unit_tests/test_checkbox.rb +35 -0
  87. data/tests/unit_tests/test_label.rb +36 -0
  88. data/tests/unit_tests/test_layout.rb +229 -0
  89. data/tests/unit_tests/test_widget.rb +3 -0
  90. metadata +139 -0
@@ -0,0 +1,2 @@
1
+ require 'glimr/renderer'
2
+ require 'glimr/widgets'
@@ -0,0 +1,37 @@
1
+ module GlimR
2
+
3
+ # Hash-configurable object.
4
+ # Takes a hash and sets the setters named
5
+ # by the keys to the values.
6
+ # Use #default_config to define the default config.
7
+ #
8
+ # class MyConfigurable
9
+ # include Configurable
10
+ # attr_accessor :x, :y
11
+ # def default_config
12
+ # { :x => 'default x', :y => 'default y' }
13
+ # end
14
+ # end
15
+ #
16
+ # mc = MyConfigurable.new :y => 6
17
+ # mc.x
18
+ # # => 'default x'
19
+ # mc.y
20
+ # # => 6
21
+ #
22
+ module Configurable
23
+
24
+ def initialize(config_hash = {}, &optional_config_block)
25
+ config = default_config.merge(config_hash)
26
+ config.each{|k,v| instance_variable_set("@#{k}", v) }
27
+ super
28
+ yield(self) if block_given?
29
+ end
30
+
31
+ def default_config
32
+ {}
33
+ end
34
+
35
+ end
36
+
37
+ end
@@ -0,0 +1,41 @@
1
+ require 'ostruct'
2
+
3
+ module GlimR
4
+
5
+ # Implements the <a href="http://www.w3.org/TR/DOM-Level-2-Events">W3C DOM Event model</a>
6
+ # event interface.
7
+ #
8
+ # Much like the ECMAScript API.
9
+ #
10
+ class Event < OpenStruct
11
+
12
+ attr_reader :type, :cancelled, :stopped
13
+ attr_accessor :phase, :params
14
+
15
+ # Creates a new event of the given type, sets params to param hash.
16
+ # By default, the event bubbles and is not cancelable.
17
+ def initialize(type, params={:bubbles => true, :cancelable => false})
18
+ params = params.clone
19
+ params[:bubbles] = true if params[:bubbles].nil?
20
+ params[:cancelable] = false if params[:cancelable].nil?
21
+ params[:time] ||= Time.now.to_f
22
+ super(params)
23
+ @type = type.to_sym
24
+ @params = params
25
+ @phase = :capture
26
+ end
27
+
28
+ # Stops the handling of the event.
29
+ def stop_propagation
30
+ @stopped = true
31
+ end
32
+
33
+ # Cancels the event's possible default behaviour.
34
+ def prevent_default
35
+ @cancelled = true if cancelable
36
+ end
37
+
38
+ end
39
+
40
+
41
+ end
@@ -0,0 +1,209 @@
1
+ require 'glimr/event'
2
+
3
+
4
+ class Array
5
+
6
+ def delete_first(item)
7
+ i = index(item)
8
+ delete_at i if i
9
+ end
10
+
11
+ end
12
+
13
+
14
+
15
+ module GlimR
16
+
17
+
18
+ # The EventListener mixin implements an event listening interface
19
+ # similar to <a href="http://www.w3.org/TR/DOM-Level-2-Events/">
20
+ # W3C's ECMAScript DOM event API</a>, with the addition of event multicasting.
21
+ #
22
+ # EventListener provides the following methods:
23
+ # #add_event_listener, #remove_event_listener,
24
+ # #dispatch_event and #multicast_event.
25
+ #
26
+ # The list of event listeners can be modified with #add_event_listener and #remove_event_listener.
27
+ #
28
+ # E.g.
29
+ # l = event_listener.add_event_listener(:hello){|listener, event|
30
+ # puts "Goodbye!"
31
+ # }
32
+ # event_listener.remove_event_listener(:hello, l)
33
+ # event_listener.add_event_listener(:hello){|listener, event|
34
+ # puts "Hello, World!"
35
+ # }
36
+ # another_hello_listener.add_event_listener(:hello){|l, e|
37
+ # puts "Hello from me too!"
38
+ # }
39
+ # event_listener.attach another_hello_listener
40
+ #
41
+ # Also provides a method_missing that captures method calls starting with on_ and
42
+ # parses them as requests to add event listener for the event type.
43
+ #
44
+ # E.g. to add a bubbling event listener for the event type :frame, it's possible to do
45
+ # obj.on_frame{|o,e| puts 'got frame event'}
46
+ #
47
+ # And for a capturing listener
48
+ # obj.on_frame(true){|o,e| puts 'got it first!'}
49
+ #
50
+ # To send a targeted event to the EventListener, use #dispatch_event.
51
+ #
52
+ # E.g.
53
+ # event_listener.dispatch_event(Event.new(:hello))
54
+ # # Hello, World!
55
+ #
56
+ # To broadcast an event to all listeners for the event type in the scene graph, use #multicast_event.
57
+ #
58
+ # E.g.
59
+ # event_listener.multicast_event(Event.new(:hello))
60
+ # # Hello from me too!
61
+ # # Hello, World!
62
+ #
63
+ module EventListener
64
+
65
+ attr_accessor :event_listeners
66
+ attr_reader :listener_count
67
+
68
+ def initialize(*a,&b)
69
+ @event_listeners = Hash.new{|h,k| h[k] = Hash.new{|h,k| h[k] = []} }
70
+ @listener_count = Hash.new{|h,k| h[k] = 0 }
71
+ end
72
+
73
+ # Adds an event listener for the given event type. If use_capture is true, listens on capture phase.
74
+ # Otherwise listens on bubble phase. If listener is nil or false, the given block is used as the listener
75
+ # instead.
76
+ def add_event_listener(type, listener=nil, use_capture=false, &block)
77
+ listener ||= block
78
+ type = type.to_sym
79
+ @event_listeners[type][(use_capture ? :capture : :bubble)] << listener
80
+ increment_listener_count type
81
+ end
82
+
83
+ # Removes the given event listener from the event type. If no listener is given, removes all event listeners
84
+ # from the event type. If use_capture is true, removes capturing event listener(s), if false, removes bubbling
85
+ # event listener(s).
86
+ def remove_event_listener(type, listener=nil, use_capture=false)
87
+ type = type.to_sym
88
+ if listener
89
+ success = @event_listeners[type][(use_capture ? :capture : :bubble)].delete_first(listener)
90
+ decrement_listener_count type if success
91
+ else
92
+ listeners = @event_listeners[type][(use_capture ? :capture : :bubble)]
93
+ sz = listeners.size
94
+ if sz > 0
95
+ listeners.clear
96
+ decrement_listener_count type, sz
97
+ end
98
+ end
99
+ end
100
+
101
+ def method_missing(mn, *a, &b)
102
+ if mn.to_s[0,3] == "on_"
103
+ add_event_listener(mn.to_s[3..-1].gsub(/=$/,''), *a, &b)
104
+ else
105
+ super
106
+ end
107
+ end
108
+
109
+ # Sends the Event evt to first the capture listeners,
110
+ # then down to children, then to bubbling listeners.
111
+ #
112
+ # Returns immediately if there are no listeners for
113
+ # the event type in or below this node.
114
+ #
115
+ def multicast_event(evt)
116
+ return !evt.cancelled if @listener_count[evt.type] <= 0
117
+ evt.phase = :capture
118
+ process_event(evt)
119
+ return !evt.cancelled if evt.stopped
120
+ if evt.phase == :capture # still going down
121
+ children.each{|c|
122
+ c.multicast_event(evt) if c.is_a? EventListener
123
+ return !evt.cancelled if evt.stopped
124
+ }
125
+ end
126
+ return !evt.cancelled if evt.stopped
127
+ evt.phase = :bubble
128
+ process_event(evt)
129
+ return !evt.cancelled
130
+ end
131
+
132
+ # Sends the Event evt down to self and bubbles it up.
133
+ def dispatch_event(evt)
134
+ evt.target = self
135
+ ancestors = [self]
136
+ o = self
137
+ while o.parent and !o.parent.event_root and o.parent != evt.sender
138
+ ancestors << o.parent
139
+ o = o.parent
140
+ end
141
+ i = ancestors.size
142
+ ancestors.reverse.each{|a|
143
+ a.process_event(evt)
144
+ break if evt.stopped
145
+ i -= 1
146
+ break if evt.phase == :bubble
147
+ }
148
+ # Now either the event was turned around at i or reached the bottom (i==1).
149
+ # Continue by setting evt.phase to :bubble and sending it up from i.
150
+ i += 1 if evt.target == self # bubble already called for self
151
+ evt.phase = :bubble
152
+ (ancestors[i..-1] || []).each{|a|
153
+ break if evt.stopped
154
+ a.process_event(evt)
155
+ }
156
+ return !evt.cancelled
157
+ end
158
+
159
+ def event_root
160
+ false
161
+ end
162
+
163
+ # Processes the Event evt by calling all listeners on
164
+ # this object registered to handle the event.
165
+ def process_event(evt)
166
+ listeners_for(evt).each{|l|
167
+ break if evt.stopped
168
+ case l
169
+ when Symbol
170
+ __send__(l, self, evt)
171
+ else
172
+ l.call(self, evt)
173
+ end
174
+ }
175
+ !evt.cancelled
176
+ end
177
+
178
+ # Adds count to the subtree total listener count for type.
179
+ # Sends changes to parent to keep its subtree total listener count correct.
180
+ def increment_listener_count type, count=1
181
+ c = @listener_count[type]
182
+ raise ArgumentError, "Trying to decrement listener count too much." if -count > c
183
+ @listener_count[type] += count
184
+ parent.increment_listener_count type, count if parent.is_a? EventListener
185
+ end
186
+
187
+ # Subtracts count from the subtree total listener count for type.
188
+ # Sends changes to parent to keep its subtree total listener count correct.
189
+ def decrement_listener_count type, count=1
190
+ increment_listener_count type, -count
191
+ end
192
+
193
+ private
194
+
195
+ def listeners_for(evt)
196
+ if self == evt.target
197
+ evt.phase = :at
198
+ @event_listeners[evt.type].values.flatten
199
+ elsif evt.phase == :capture
200
+ @event_listeners[evt.type][:capture]
201
+ else
202
+ @event_listeners[evt.type][:bubble]
203
+ end
204
+ end
205
+
206
+ end
207
+
208
+
209
+ end
@@ -0,0 +1,520 @@
1
+ require 'ostruct'
2
+ require 'glimr/util'
3
+
4
+
5
+ module GlimR
6
+
7
+
8
+ # Layoutable is a mixin for creating GlimR nodes that
9
+ # are suspectible to layout rules.
10
+ #
11
+ # Layoutable objects have dimensions, margins, and padding.
12
+ #
13
+ # The margins define how much space should be left around the
14
+ # Layoutable. Padding defines how much distance should be left
15
+ # between the borders of the Layoutable and its child nodes.
16
+ #
17
+ # They also have the following attributes that control their
18
+ # dynamic resizing behaviour: expand_width, expand_height,
19
+ # fit_children, min_width, min_height, max_width and max_height.
20
+ #
21
+ # Dynamic layout rules:
22
+ #
23
+ # An object expands in a dimension if either its corresponding expand
24
+ # attribute is set to true, or if its fit_children is set to true and one of
25
+ # its children expands in the dimension.
26
+ #
27
+ # If an object neither fits its children nor expands, it is a constant size
28
+ # object.
29
+ #
30
+ # When a layoutable object is attached to the scene graph
31
+ # it expands to its maximum dimensions if it is an expanding object.
32
+ #
33
+ # When a layoutable object changes size, it informs its parent
34
+ # if the parent is a layoutable. If the object was a constant size object,
35
+ # or the size change resulted from changing the object's
36
+ # dimension constraints (min_width, max_width, etc.), the expanding
37
+ # children of the object are also informed of the size change.
38
+ #
39
+ # When computing the dimensions of a Layoutable, dimension
40
+ # constraints take predecence over expanding, which takes
41
+ # predecence over fit_children.
42
+ #
43
+ # E.g. the final width of a layoutable node is computed as follows:
44
+ #
45
+ # 1. set width to max child width if fit_children
46
+ # 2. set width to the inner width of the parent node if expand_width
47
+ # 3. clamp the width between min_width and max_width if they're set
48
+ #
49
+ module Layoutable
50
+
51
+ def self.size_changing_accessor(*mnames)
52
+ attr_reader *mnames
53
+ mnames.each{|mn|
54
+ ivar_name = :"@#{mn}"
55
+ define_method("#{mn}=") do |value|
56
+ size_changing{ instance_variable_set(ivar_name, value) }
57
+ end
58
+ }
59
+ end
60
+
61
+ size_changing_accessor :width, :height, :depth
62
+ size_changing_accessor :margin_left, :margin_right,
63
+ :margin_top, :margin_bottom,
64
+ :margin_front, :margin_back
65
+ size_changing_accessor :padding_left, :padding_right,
66
+ :padding_top, :padding_bottom,
67
+ :padding_front, :padding_back
68
+
69
+ size_changing_accessor :expand_width, :expand_height, :fit_children,
70
+ :min_width, :min_height, :max_width, :max_height
71
+
72
+ attr_accessor :align, :valign
73
+
74
+ alias_method :fit_children?, :fit_children
75
+
76
+ DIMENSIONS = [:left, :right, :top, :bottom] #, :front, :back]
77
+
78
+
79
+ # horiz margin setters modify full_width
80
+ # vert margin setters modify full_height
81
+ # horiz padding setters modify full_width
82
+ # vert padding setters modify full_height
83
+
84
+
85
+ def default_config
86
+ super.merge(
87
+ :expand_width => false, :expand_height => false,
88
+ :fit_children => true, :align => :left, :valign => :top,
89
+ :min_width => 0, :min_height => 0,
90
+ :max_width => 1.0/0.0, :max_height => 1.0/0.0,
91
+ :width => 0, :height => 0, :depth => 0,
92
+ :margin_left => 0, :margin_right => 0,
93
+ :margin_top => 0, :margin_bottom => 0,
94
+ :margin_front => 0, :margin_back => 0,
95
+ :padding_left => 0, :padding_right => 0,
96
+ :padding_top => 0, :padding_bottom => 0,
97
+ :padding_front => 0, :padding_back => 0
98
+ )
99
+ end
100
+
101
+ def initialize(*a, &b)
102
+ super
103
+ @__resize_handler = lambda{|o,e| o.expand! }
104
+ self.margin = @margin if @margin
105
+ self.padding = @padding if @padding
106
+ end
107
+
108
+ # Sets x to the given integer value.
109
+ def x= nx
110
+ super nx.to_i
111
+ end
112
+
113
+ # Sets y to the given integer value.
114
+ def y= ny
115
+ super ny.to_i
116
+ end
117
+
118
+ # Sets align to a and relayouts.
119
+ def align= a
120
+ if @align != a
121
+ @align = a
122
+ layout
123
+ end
124
+ end
125
+
126
+ # Sets valign to v and relayouts.
127
+ def valign= v
128
+ if @valign != v
129
+ @valign = v
130
+ layout
131
+ end
132
+ end
133
+
134
+ # Sets min_width to mw and sets width to min_width
135
+ # if it's smaller than the new min_width.
136
+ def min_width= mw
137
+ @min_width = mw
138
+ self.width = mw if self.width < mw
139
+ fit_to_children! if fit_children?
140
+ end
141
+
142
+ # Sets min_height to mw and sets height to min_height
143
+ # if it's smaller than the new min_height.
144
+ def min_height= mh
145
+ @min_height = mh
146
+ self.height = mh if self.height < mh
147
+ fit_to_children! if fit_children?
148
+ end
149
+
150
+ # Sets max_width to mw and sets width to max_width
151
+ # if it's larger than the new max_width.
152
+ def max_width= mw
153
+ @max_width = mw
154
+ self.width = mw if self.width > mw
155
+ end
156
+
157
+ # Sets max_height to mw and sets height to max_height
158
+ # if it's larger than the new max_height.
159
+ def max_height= mh
160
+ @max_height = mh
161
+ self.height = mh if self.height > mh
162
+ end
163
+
164
+ # Returns current margins as an OpenStruct with #left, #right, #top, and #bottom.
165
+ def margin
166
+ OpenStruct.new(
167
+ :left => margin_left, :right => margin_right,
168
+ :top => margin_top, :bottom => margin_bottom,
169
+ :front => margin_front, :back => margin_back
170
+ )
171
+ end
172
+
173
+ # Sets margins to new_margin.
174
+ # If given a Numeric, sets all margins to the given value.
175
+ # If given a hash, sets the margins to the corresponding values in the hash.
176
+ # If given any other object, acts as if it is an OpenStruct returned by #margin.
177
+ #
178
+ def margin= new_margin
179
+ size_changing do
180
+ case new_margin
181
+ when Numeric
182
+ @margin_left = @margin_right = @margin_top = @margin_bottom = new_margin
183
+ touch!
184
+ when Hash
185
+ new_margin.each{|k,v|
186
+ __send__("margin_#{k}=", v)
187
+ }
188
+ else
189
+ DIMENSIONS.each{|d|
190
+ __send__("margin_#{d}=", new_margin.__send__(d))
191
+ }
192
+ end
193
+ end
194
+ end
195
+
196
+ # Returns current padding as an OpenStruct with #left, #right, #top, and #bottom.
197
+ def padding
198
+ OpenStruct.new(
199
+ :left => padding_left, :right => padding_right,
200
+ :top => padding_top, :bottom => padding_bottom,
201
+ :front => padding_front, :back => padding_back
202
+ )
203
+ end
204
+
205
+ # Sets padding to new_padding.
206
+ # If given a Numeric, sets all paddings to the given value.
207
+ # If given a hash, sets the paddings to the corresponding values in the hash.
208
+ # If given any other object, acts as if it is an OpenStruct returned by #padding.
209
+ #
210
+ def padding= new_padding
211
+ size_changing do
212
+ case new_padding
213
+ when Numeric
214
+ @padding_left = @padding_right = @padding_top = @padding_bottom = new_padding
215
+ touch!
216
+ when Hash
217
+ new_padding.each{|k,v|
218
+ __send__("padding_#{k}=", v)
219
+ }
220
+ else
221
+ DIMENSIONS.each{|d|
222
+ __send__("padding_#{d}=", new_padding.__send__(d))
223
+ }
224
+ end
225
+ end
226
+ end
227
+
228
+ # Returns an Array of all children that respond true to #is_a?(Layoutable).
229
+ def layoutable_children
230
+ children.find_all{|c| c.is_a? Layoutable }
231
+ end
232
+
233
+ # Whether the object expands to fill its parent's width or not.
234
+ #
235
+ # Returns true if either @expand_width is true or fit_children is true
236
+ # and any of the layoutable children returns true to #expand_width.
237
+ def expand_width
238
+ @expand_width or (fit_children and layoutable_children.any?{|c| c.expand_width })
239
+ end
240
+
241
+ def parent= p
242
+ super
243
+ ew = expand_width
244
+ eh = expand_height
245
+ viewport_expand = p.is_a?(Viewport) and (ew or eh)
246
+ constant_expand = (p.is_a?(Layoutable) and !p.fit_children?) and
247
+ ((ew and !parent.expand_width) or (eh and !parent.expand_height))
248
+ expand! if (viewport_expand or constant_expand)
249
+ end
250
+
251
+ # Whether the object expands to fill its parent's height or not.
252
+ #
253
+ # Returns true if either @expand_height is true or fit_children is true
254
+ # and any of the layoutable children returns true to #expand_height.
255
+ def expand_height
256
+ @expand_height or (fit_children and
257
+ layoutable_children.any?{|c| c.expand_height })
258
+ end
259
+
260
+ # Whether the Layoutable object expands to fill its parent's width or height.
261
+ #
262
+ def expand?
263
+ expand_width or expand_height
264
+ end
265
+
266
+ # Returns true if the object's size is constant, i.e it does not expand and
267
+ # does not fit its children.
268
+ def constant_size?
269
+ not (expand? or fit_children?)
270
+ end
271
+
272
+ def attach(*a, &b)
273
+ size_changing{
274
+ super
275
+ children_change
276
+ }
277
+ end
278
+
279
+ def detach(*a, &b)
280
+ size_changing{
281
+ super
282
+ children_change
283
+ }
284
+ end
285
+
286
+ # Sets dimensions equal to the largest child + padding.
287
+ def fit_to_children!
288
+ size_changing do
289
+ fit_width! unless parent and expand_width
290
+ fit_height! unless parent and expand_height
291
+ end
292
+ end
293
+
294
+ # Sets width equal to the largest child + padding.
295
+ def fit_width!
296
+ self.width = layoutable_children.map{|c|
297
+ c.full_width + padding_left + padding_right
298
+ }.max unless layoutable_children.empty?
299
+ end
300
+
301
+ # Sets height equal to the largest child + padding.
302
+ def fit_height!
303
+ self.height = layoutable_children.map{|c|
304
+ c.full_height + padding_top + padding_bottom
305
+ }.max unless layoutable_children.empty?
306
+ end
307
+
308
+ # Yields to block and informs relevant parties if size changed during it.
309
+ #
310
+ # In detail:
311
+ #
312
+ # If the node turns into an expanding one, it is expanded. This may cause a size change.
313
+ #
314
+ # If the inner dimensions (caused by padding changes) are altered,
315
+ # children are told and relayouted.
316
+ #
317
+ # If the outer dimensions (caused by dimension changes and margin changes)
318
+ # are altered, children are told and relayouted and possible Layoutable parent is told
319
+ # (unless the node is an expanding one, in which case the parent already knows.)
320
+ #
321
+ def size_changing(&block)
322
+ return yield if @__doing_size_changing_operation
323
+ @__doing_size_changing_operation = true
324
+ ow, oh, iw, ih = full_width, full_height, inner_width, inner_height
325
+ cs = constant_size?
326
+ fc = fit_children?
327
+ ew = expand_width
328
+ eh = expand_height
329
+ rv = yield
330
+ fit_to_children! if fit_children? and ((!fc and fit_children?) or (ew and !expand_width) or (eh and !expand_height))
331
+ expand! if (!ew and expand_width) or (!eh and expand_height)
332
+ @width = [[min_width, width].max, max_width].min
333
+ @height = [[min_height, height].max, max_height].min
334
+ dw = (ow != full_width)
335
+ dh = (oh != full_height)
336
+ diw = (iw != inner_width)
337
+ dih = (ih != inner_height)
338
+ if dw or dh or diw or dih
339
+ tell_children_of_size_change if diw or dih
340
+ parent.children_change if parent.is_a?(Layoutable) and (dw or dh)
341
+ layout
342
+ dispatch_event(Event.new(:layout)) if respond_to? :dispatch_event
343
+ @width = [[min_width, width].max, max_width].min
344
+ @height = [[min_height, height].max, max_height].min
345
+ end
346
+ @__doing_size_changing_operation = false
347
+ rv
348
+ end
349
+
350
+ # Children changed, let's accommodate.
351
+ def children_change
352
+ fit_to_children! if fit_children?
353
+ layout
354
+ end
355
+
356
+ # Expands all expanding children.
357
+ def tell_children_of_size_change
358
+ layoutable_children.each{|c| c.expand! if c.expand? }
359
+ end
360
+
361
+ # Expands self to maximum available dimensions.
362
+ # Brought on by parent size change.
363
+ #
364
+ def expand!(given_width=nil,given_height=nil)
365
+ size_changing{
366
+ expand_to_max_width!(given_width) if expand_width
367
+ expand_to_max_height!(given_width) if expand_height
368
+ }
369
+ end
370
+
371
+ # Expands self, parent and children to
372
+ # maximum available width if expand_width is true.
373
+ #
374
+ # If expand_width is true, expands to
375
+ # either the given_width, the result of
376
+ # calling @parent.expand_to_max_width! or
377
+ # @viewport.width (in this order.)
378
+ #
379
+ # If width changes as a result of this
380
+ # call, calls each Layoutable child's
381
+ # expand_to_max_width! with the new width.
382
+ #
383
+ # Returns free_width.
384
+ #
385
+ def expand_to_max_width!(given_width=nil)
386
+ if expand_width
387
+ remove_event_listener(:resize, @__resize_handler)
388
+ if given_width
389
+ @width = given_width
390
+ elsif @parent.is_a? Layoutable
391
+ @width = @parent.expand_to_max_width!
392
+ elsif @viewport ||= (@parent && @parent.viewport)
393
+ on_resize(@__resize_handler)
394
+ @width = @viewport.width
395
+ end
396
+ end
397
+ free_width
398
+ end
399
+
400
+ # Expands self, parent and children to
401
+ # maximum available height if expand_height is true.
402
+ #
403
+ # If expand_height is true, expands to
404
+ # either the given_height, the result of
405
+ # calling @parent.expand_to_max_height! or
406
+ # @viewport.height (in this order.)
407
+ #
408
+ # If height changes as a result of this
409
+ # call, calls each Layoutable child's
410
+ # expand_to_max_height! with the new height.
411
+ #
412
+ # Returns free_height.
413
+ #
414
+ def expand_to_max_height!(given_height=nil)
415
+ if expand_height
416
+ remove_event_listener(:resize, @__resize_handler)
417
+ if given_height
418
+ @height = given_height
419
+ elsif @parent.is_a? Layoutable
420
+ @height = @parent.expand_to_max_height!
421
+ elsif @viewport ||= (@parent && @parent.viewport)
422
+ on_resize(@__resize_handler)
423
+ @height = @viewport.height
424
+ end
425
+ end
426
+ free_height
427
+ end
428
+
429
+ def layout
430
+ @layouted_width = @width
431
+ @layouted_height = @height
432
+ layoutable_children.each{|c|
433
+ case align
434
+ when :left
435
+ c.x = padding_left + c.margin_left
436
+ when :right
437
+ c.x = width - padding_right - c.full_width
438
+ when :center
439
+ c.x = padding_left + c.margin_left + ((inner_width - c.margin_left - c.margin_right) - c.width) / 2.0
440
+ end
441
+ case valign
442
+ when :top
443
+ c.y = padding_top + c.margin_top
444
+ when :bottom
445
+ c.y = height - padding_bottom - c.full_height
446
+ when :middle
447
+ c.y = padding_top + c.margin_top + ((inner_height - c.margin_top - c.margin_bottom) - c.height) / 2.0
448
+ end
449
+ }
450
+ end
451
+
452
+ # Returns the maximum width an expanding child can expand to.
453
+ def free_width
454
+ inner_width
455
+ end
456
+
457
+ # Returns the maximum height an expanding child can expand to.
458
+ def free_height
459
+ inner_height
460
+ end
461
+
462
+ # Width sans padding.
463
+ def inner_width
464
+ @width - @padding_left - @padding_right
465
+ end
466
+
467
+ # Height sans padding.
468
+ def inner_height
469
+ @height - @padding_bottom - @padding_top
470
+ end
471
+
472
+ # Depth sans padding.
473
+ def inner_depth
474
+ @depth - @padding_front - @padding_back
475
+ end
476
+
477
+ # Width, scaled, plus margins.
478
+ def full_width
479
+ case scale
480
+ when Numeric
481
+ width * scale
482
+ when Array
483
+ width * scale[0]
484
+ else
485
+ width
486
+ end + @margin_left + @margin_right
487
+ end
488
+
489
+ # Height, scaled, plus margins.
490
+ def full_height
491
+ case scale
492
+ when Numeric
493
+ height * scale
494
+ when Array
495
+ height * scale[1]
496
+ else
497
+ height
498
+ end + @margin_top + @margin_bottom
499
+ end
500
+
501
+ def full_depth
502
+ case scale
503
+ when Numeric
504
+ depth * scale
505
+ when Array
506
+ depth * scale[2]
507
+ else
508
+ depth
509
+ end + @margin_front + @margin_back
510
+ end
511
+
512
+ def inspect
513
+ super[0..-2] + "::w#@width:h#@height>"
514
+ end
515
+
516
+ end
517
+
518
+
519
+ end
520
+