cyberarm_engine 0.23.0 → 0.24.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 (33) hide show
  1. checksums.yaml +4 -4
  2. data/assets/shaders/fragment/g_buffer.glsl +8 -8
  3. data/assets/shaders/fragment/lighting.glsl +15 -9
  4. data/assets/shaders/include/material_struct.glsl +16 -0
  5. data/assets/shaders/vertex/g_buffer.glsl +17 -17
  6. data/assets/shaders/vertex/lighting.glsl +16 -9
  7. data/lib/cyberarm_engine/builtin/intro_state.rb +1 -1
  8. data/lib/cyberarm_engine/common.rb +2 -2
  9. data/lib/cyberarm_engine/console.rb +10 -10
  10. data/lib/cyberarm_engine/game_object.rb +1 -1
  11. data/lib/cyberarm_engine/gosu_ext/draw_arc.rb +98 -0
  12. data/lib/cyberarm_engine/gosu_ext/draw_circle.rb +31 -0
  13. data/lib/cyberarm_engine/gosu_ext/draw_path.rb +17 -0
  14. data/lib/cyberarm_engine/model.rb +7 -6
  15. data/lib/cyberarm_engine/notification.rb +83 -0
  16. data/lib/cyberarm_engine/notification_manager.rb +242 -0
  17. data/lib/cyberarm_engine/opengl/renderer/opengl_renderer.rb +16 -10
  18. data/lib/cyberarm_engine/opengl/renderer/renderer.rb +12 -1
  19. data/lib/cyberarm_engine/opengl/shader.rb +2 -2
  20. data/lib/cyberarm_engine/stats.rb +181 -10
  21. data/lib/cyberarm_engine/text.rb +3 -0
  22. data/lib/cyberarm_engine/ui/element.rb +45 -14
  23. data/lib/cyberarm_engine/ui/elements/container.rb +86 -27
  24. data/lib/cyberarm_engine/ui/elements/slider.rb +4 -3
  25. data/lib/cyberarm_engine/ui/elements/text_block.rb +11 -1
  26. data/lib/cyberarm_engine/ui/gui_state.rb +28 -18
  27. data/lib/cyberarm_engine/ui/style.rb +2 -1
  28. data/lib/cyberarm_engine/vector.rb +35 -16
  29. data/lib/cyberarm_engine/version.rb +1 -1
  30. data/lib/cyberarm_engine/window.rb +35 -8
  31. data/lib/cyberarm_engine.rb +8 -2
  32. data/mrbgem.rake +29 -0
  33. metadata +10 -3
@@ -0,0 +1,242 @@
1
+ module CyberarmEngine
2
+ class NotificationManager
3
+ EDGE_TOP = :top
4
+ EDGE_BOTTOM = :bottom
5
+ EDGE_RIGHT = :right
6
+ EDGE_LEFT = :left
7
+
8
+ MODE_DEFAULT = :slide
9
+ MODE_CIRCLE = :circle
10
+
11
+ attr_reader :edge, :mode, :max_visible, :notifications
12
+ def initialize(edge: EDGE_RIGHT, mode: MODE_DEFAULT, window:, max_visible: 1)
13
+ @edge = edge
14
+ @mode = mode
15
+ @window = window
16
+ @max_visible = max_visible
17
+
18
+ @notifications = []
19
+ @drivers = []
20
+ @slots = Array.new(max_visible, nil)
21
+ end
22
+
23
+ def draw
24
+ @drivers.each do |driver|
25
+ case @edge
26
+ when :left, :right
27
+ x = @edge == :right ? @window.width + driver.x : -Notification::WIDTH + driver.x
28
+ y = driver.y + Notification::HEIGHT / 2
29
+
30
+ Gosu.translate(x, y + (Notification::HEIGHT + Notification::PADDING) * driver.slot) do
31
+ driver.draw
32
+ end
33
+
34
+ when :top, :bottom
35
+ x = @window.width / 2 - Notification::WIDTH / 2
36
+ y = @edge == :top ? driver.y - Notification::HEIGHT : @window.height + driver.y
37
+ slot_position = (Notification::HEIGHT + Notification::PADDING) * driver.slot
38
+ slot_position *= -1 if @edge == :bottom
39
+
40
+ Gosu.translate(x, y + slot_position) do
41
+ driver.draw
42
+ end
43
+ end
44
+ end
45
+ end
46
+
47
+ def update
48
+ show_next_notification if @drivers.size < @max_visible
49
+ @drivers.each do |driver|
50
+ if driver.done?
51
+ @slots[driver.slot] = nil
52
+ @drivers.delete(driver)
53
+ end
54
+ end
55
+
56
+ @drivers.each(&:update)
57
+ end
58
+
59
+ def show_next_notification
60
+ notification = @notifications.sort { |n| n.priority }.reverse.shift
61
+ return unless notification
62
+ return if available_slot_index < lowest_used_slot
63
+ @notifications.delete(notification)
64
+
65
+ @drivers << Driver.new(edge: @edge, mode: @mode, notification: notification, slot: available_slot_index)
66
+ slot = @slots[available_slot_index] = @drivers.last
67
+ end
68
+
69
+ def available_slot_index
70
+ @slots.each_with_index do |slot, i|
71
+ return i unless slot
72
+ end
73
+
74
+ return -1
75
+ end
76
+
77
+ def lowest_used_slot
78
+ @slots.each_with_index do |slot, i|
79
+ return i if slot
80
+ end
81
+
82
+ return -1
83
+ end
84
+
85
+ def highest_used_slot
86
+ _slot = -1
87
+ @slots.each_with_index do |slot, i|
88
+ _slot = i if slot
89
+ end
90
+
91
+ return _slot
92
+ end
93
+
94
+ def create_notification(**args)
95
+ notification = Notification.new(host: self, **args)
96
+ @notifications << notification
97
+ end
98
+
99
+ class Driver
100
+ attr_reader :x, :y, :notification, :slot
101
+ def initialize(edge:, mode:, notification:, slot:)
102
+ @edge = edge
103
+ @mode = mode
104
+ @notification = notification
105
+ @slot = slot
106
+
107
+ @x, @y = 0, 0
108
+ @delta = Gosu.milliseconds
109
+ @accumulator = 0.0
110
+
111
+ @born_at = Gosu.milliseconds
112
+ @duration_completed_at = Float::INFINITY
113
+ @transition_completed_at = Float::INFINITY
114
+ end
115
+
116
+ def transition_in_complete?
117
+ Gosu.milliseconds - @born_at >= @notification.transition_duration
118
+ end
119
+
120
+ def duration_completed?
121
+ Gosu.milliseconds - @transition_completed_at >= @notification.time_to_live
122
+ end
123
+
124
+ def done?
125
+ Gosu.milliseconds - @duration_completed_at >= @notification.transition_duration
126
+ end
127
+
128
+ def draw
129
+ ratio = 0.0
130
+
131
+ if not transition_in_complete?
132
+ ratio = animation_ratio
133
+ elsif transition_in_complete? and not duration_completed?
134
+ ratio = 1.0
135
+ elsif duration_completed?
136
+ ratio = 1.0 - animation_ratio
137
+ end
138
+
139
+ case @mode
140
+ when MODE_DEFAULT
141
+ Gosu.clip_to(0, 0, Notification::WIDTH, Notification::HEIGHT * ratio) do
142
+ @notification.draw
143
+ end
144
+ when MODE_CIRCLE
145
+ half = Notification::WIDTH / 2
146
+
147
+ Gosu.clip_to(half - (half * ratio), 0, Notification::WIDTH * ratio, Notification::HEIGHT) do
148
+ @notification.draw
149
+ end
150
+ end
151
+ end
152
+
153
+ def update
154
+ case @mode
155
+ when MODE_DEFAULT
156
+ update_default
157
+ when MODE_CIRCLE
158
+ update_circle
159
+ end
160
+
161
+ @accumulator += Gosu.milliseconds - @delta
162
+ @delta = Gosu.milliseconds
163
+ end
164
+
165
+
166
+ def update_default
167
+ case @edge
168
+ when :left, :right
169
+ if not transition_in_complete? # Slide In
170
+ @x = @edge == :right ? -x_offset : x_offset
171
+ elsif transition_in_complete? and not duration_completed?
172
+ @x = @edge == :right ? -Notification::WIDTH : Notification::WIDTH if @x.abs != Notification::WIDTH
173
+ @transition_completed_at = Gosu.milliseconds if @transition_completed_at == Float::INFINITY
174
+ @accumulator = 0.0
175
+ elsif duration_completed? # Slide Out
176
+ @x = @edge == :right ? x_offset - Notification::WIDTH : Notification::WIDTH - x_offset
177
+ @x = 0 if @edge == :left and @x <= 0
178
+ @x = 0 if @edge == :right and @x >= 0
179
+ @duration_completed_at = Gosu.milliseconds if @duration_completed_at == Float::INFINITY
180
+ end
181
+
182
+ when :top, :bottom
183
+ if not transition_in_complete? # Slide In
184
+ @y = @edge == :top ? y_offset : -y_offset
185
+ elsif transition_in_complete? and not duration_completed?
186
+ @y = @edge == :top ? Notification::HEIGHT : -Notification::HEIGHT if @x.abs != Notification::HEIGHT
187
+ @transition_completed_at = Gosu.milliseconds if @transition_completed_at == Float::INFINITY
188
+ @accumulator = 0.0
189
+ elsif duration_completed? # Slide Out
190
+ @y = @edge == :top ? Notification::HEIGHT - y_offset : y_offset - Notification::HEIGHT
191
+ @y = 0 if @edge == :top and @y <= 0
192
+ @y = 0 if @edge == :bottom and @y >= 0
193
+ @duration_completed_at = Gosu.milliseconds if @duration_completed_at == Float::INFINITY
194
+ end
195
+ end
196
+ end
197
+
198
+ def update_circle
199
+ case @edge
200
+ when :top, :bottom
201
+ @y = @edge == :top ? Notification::HEIGHT : -Notification::HEIGHT
202
+ when :left, :right
203
+ @x = @edge == :right ? -Notification::WIDTH : Notification::WIDTH
204
+ end
205
+
206
+ if transition_in_complete? and not duration_completed?
207
+ @transition_completed_at = Gosu.milliseconds if @transition_completed_at == Float::INFINITY
208
+ @accumulator = 0.0
209
+ elsif duration_completed?
210
+ @duration_completed_at = Gosu.milliseconds if @duration_completed_at == Float::INFINITY
211
+ end
212
+ end
213
+
214
+ def animation_ratio
215
+ x = (@accumulator / @notification.transition_duration)
216
+
217
+ case @notification.transition_type
218
+ when Notification::LINEAR_TRANSITION
219
+ x.clamp(0.0, 1.0)
220
+ when Notification::EASE_IN_OUT_TRANSITION # https://easings.net/#easeInOutQuint
221
+ (x < 0.5 ? 16 * x * x * x * x * x : 1 - ((-2 * x + 2) ** 5) / 2).clamp(0.0, 1.0)
222
+ end
223
+ end
224
+
225
+ def x_offset
226
+ if not transition_in_complete? or duration_completed?
227
+ Notification::WIDTH * animation_ratio
228
+ else
229
+ 0
230
+ end
231
+ end
232
+
233
+ def y_offset
234
+ if not transition_in_complete? or duration_completed?
235
+ Notification::HEIGHT * animation_ratio
236
+ else
237
+ 0
238
+ end
239
+ end
240
+ end
241
+ end
242
+ end
@@ -44,7 +44,7 @@ module CyberarmEngine
44
44
  shader.uniform_transform("projection", camera.projection_matrix)
45
45
  shader.uniform_transform("view", camera.view_matrix)
46
46
  shader.uniform_transform("model", entity.model_matrix)
47
- shader.uniform_vec3("cameraPosition", camera.position)
47
+ shader.uniform_vector3("camera_position", camera.position)
48
48
 
49
49
  gl_error?
50
50
  draw_model(entity.model, shader)
@@ -154,15 +154,21 @@ module CyberarmEngine
154
154
  glBindTexture(GL_TEXTURE_2D, @g_buffer.texture(:depth))
155
155
  shader.uniform_integer("depth", 4)
156
156
 
157
- lights.each_with_index do |light, _i|
158
- shader.uniform_integer("light[0].type", light.type)
159
- shader.uniform_vec3("light[0].direction", light.direction)
160
- shader.uniform_vec3("light[0].position", light.position)
161
- shader.uniform_vec3("light[0].diffuse", light.diffuse)
162
- shader.uniform_vec3("light[0].ambient", light.ambient)
163
- shader.uniform_vec3("light[0].specular", light.specular)
157
+ # FIXME: Try to figure out how to up this to 32 and/or beyond
158
+ # (currently fails with more then 7 lights passed in to shader)
159
+ lights.each_slice(7).each do |light_group|
160
+ light_group.each_with_index do |light, _i|
161
+ shader.uniform_integer("light_count", light_group.size)
164
162
 
165
- glDrawArrays(GL_TRIANGLES, 0, @g_buffer.vertices.size)
163
+ shader.uniform_integer("lights[#{_i}].type", light.type)
164
+ shader.uniform_vector3("lights[#{_i}].direction", light.direction)
165
+ shader.uniform_vector3("lights[#{_i}].position", light.position)
166
+ shader.uniform_vector3("lights[#{_i}].diffuse", light.diffuse)
167
+ shader.uniform_vector3("lights[#{_i}].ambient", light.ambient)
168
+ shader.uniform_vector3("lights[#{_i}].specular", light.specular)
169
+
170
+ glDrawArrays(GL_TRIANGLES, 0, @g_buffer.vertices.size)
171
+ end
166
172
  end
167
173
 
168
174
  glBindVertexArray(0)
@@ -215,7 +221,7 @@ module CyberarmEngine
215
221
 
216
222
  offset = 0
217
223
  model.objects.each do |object|
218
- shader.uniform_boolean("hasTexture", object.has_texture?)
224
+ shader.uniform_boolean("has_texture", object.has_texture?)
219
225
 
220
226
  if object.has_texture?
221
227
  glBindTexture(GL_TEXTURE_2D, object.materials.find { |mat| mat.texture_id }.texture_id)
@@ -8,8 +8,19 @@ module CyberarmEngine
8
8
  end
9
9
 
10
10
  def draw(camera, lights, entities)
11
+ Stats.frame.start_timing(:opengl_renderer)
12
+
13
+ Stats.frame.start_timing(:opengl_model_renderer)
11
14
  @opengl_renderer.render(camera, lights, entities)
12
- @bounding_box_renderer.render(entities) if @show_bounding_boxes
15
+ Stats.frame.end_timing(:opengl_model_renderer)
16
+
17
+ if @show_bounding_boxes
18
+ Stats.frame.start_timing(:opengl_boundingbox_renderer)
19
+ @bounding_box_renderer.render(entities)
20
+ Stats.frame.end_timing(:opengl_boundingbox_renderer)
21
+ end
22
+
23
+ Stats.frame.end_timing(:opengl_renderer)
13
24
  end
14
25
 
15
26
  def canvas_size_changed
@@ -385,7 +385,7 @@ module CyberarmEngine
385
385
  # @param value [Vector]
386
386
  # @param location [Integer]
387
387
  # @return [void]
388
- def uniform_vec3(variable, value, location = nil)
388
+ def uniform_vector3(variable, value, location = nil)
389
389
  attr_loc = location || attribute_location(variable)
390
390
 
391
391
  glUniform3f(attr_loc, *value.to_a[0..2])
@@ -397,7 +397,7 @@ module CyberarmEngine
397
397
  # @param value [Vector]
398
398
  # @param location [Integer]
399
399
  # @return [void]
400
- def uniform_vec4(variable, value, location = nil)
400
+ def uniform_vector4(variable, value, location = nil)
401
401
  attr_loc = location || attribute_location(variable)
402
402
 
403
403
  glUniform4f(attr_loc, *value.to_a)
@@ -1,20 +1,191 @@
1
1
  module CyberarmEngine
2
2
  class Stats
3
- @@hash = {
4
- gui_recalculations_last_frame: 0
5
- }
3
+ @frames = []
4
+ @frame_index = -1
5
+ @max_frame_history = 1024
6
6
 
7
- def self.get(key)
8
- @@hash.dig(key)
7
+ def self.new_frame
8
+ if @frames.size < @max_frame_history
9
+ @frames << Frame.new
10
+ else
11
+ @frames[@frame_index] = Frame.new
12
+ end
13
+ end
14
+
15
+ def self.frame
16
+ @frames[@frame_index]
17
+ end
18
+
19
+ def self.end_frame
20
+ frame&.complete
21
+
22
+ @frame_index += 1
23
+ @frame_index %= @max_frame_history
24
+ end
25
+
26
+ def self.frames
27
+ if @frames.size < @max_frame_history
28
+ @frames
29
+ else
30
+ @frames.rotate(@frame_index - (@max_frame_history - (@frames.size - 1)))
31
+ end
32
+ end
33
+
34
+ def self.frame_index
35
+ @frame_index
9
36
  end
10
37
 
11
- def self.increment(key, n)
12
- @@hash[key] += n
38
+ def self.max_frame_history
39
+ @max_frame_history
13
40
  end
14
41
 
15
- def self.clear
16
- @@hash.each do |key, _value|
17
- @@hash[key] = 0
42
+ class Frame
43
+ Timing = Struct.new(:start_time, :end_time, :duration)
44
+
45
+ attr_reader :frame_timing, :counters, :timings, :multitimings
46
+ def initialize
47
+ @frame_timing = Timing.new(Gosu.milliseconds, -1, -1)
48
+ @attempted_multitiming = false
49
+
50
+ @counters = {
51
+ gui_recalculations: 0
52
+ }
53
+
54
+ @timings = {}
55
+ @multitimings = {}
56
+ end
57
+
58
+ def increment(key, number = 1)
59
+ @counters[key] ||= 0
60
+ @counters[key] += number
61
+ end
62
+
63
+ def start_timing(key)
64
+ raise "key must be a symbol!" unless key.is_a?(Symbol)
65
+ if @timings[key]
66
+ # FIXME: Make it not spammy...
67
+ # warn "Only one timing per key per frame. (Timing for #{key.inspect} already exists!)"
68
+ @attempted_multitiming = true
69
+ @multitimings[key] = true
70
+
71
+ return
72
+ end
73
+
74
+ @timings[key] = Timing.new(Gosu.milliseconds, -1, -1)
75
+ end
76
+
77
+ def end_timing(key)
78
+ timing = @timings[key]
79
+
80
+ # FIXME: Make it not spammy...
81
+ # warn "Timing #{key.inspect} already ended!" if timing.end_time != -1
82
+
83
+ timing.end_time = Gosu.milliseconds
84
+ timing.duration = timing.end_time - timing.start_time
85
+ end
86
+
87
+ def complete
88
+ @frame_timing.end_time = Gosu.milliseconds
89
+ @frame_timing.duration = @frame_timing.end_time - @frame_timing.start_time
90
+
91
+ # Lock data structures
92
+ @frame_timing.freeze
93
+ @counters.freeze
94
+ @timings.freeze
95
+ @multitimings.freeze
96
+ end
97
+
98
+ def complete?
99
+ @frame_timing.duration != -1
100
+ end
101
+
102
+ def attempted_multitiming?
103
+ @attempted_multitiming
104
+ end
105
+ end
106
+
107
+ class StatsPlotter
108
+ attr_reader :position
109
+
110
+ def initialize(x, y, z = Float::INFINITY, width = 128, height = 128)
111
+ @position = Vector.new(x, y, z)
112
+ @width = width
113
+ @height = height
114
+
115
+ @padding = 2
116
+ @text_size = 16
117
+
118
+ @max_timing_label = CyberarmEngine::Text.new("", x: x + @padding + 1, y: y + @padding, z: z, size: @text_size, border: true)
119
+ @avg_timing_label = CyberarmEngine::Text.new("", x: x + @padding + 1, y: y + @padding + @height / 2 - @text_size / 2, z: z, size: @text_size, border: true)
120
+ @min_timing_label = CyberarmEngine::Text.new("", x: x + @padding + 1, y: y + @height - (@text_size + @padding / 2), z: z, size: @text_size, border: true)
121
+
122
+ @timings_label = CyberarmEngine::Text.new("", x: x + @padding + @width + @padding, y: y + @padding, z: z, size: @text_size, border: true)
123
+
124
+ @frame_stats = []
125
+ @graphs = {
126
+ frame_timings: []
127
+ }
128
+ end
129
+
130
+ def calculate_graphs
131
+ calculate_frame_timings_graph
132
+ end
133
+
134
+ def calculate_frame_timings_graph
135
+ @graphs[:frame_timings].clear
136
+
137
+ samples = @width - @padding
138
+ nodes = Array.new(samples.ceil) { [] }
139
+
140
+ slice = 0
141
+ @frame_stats.each_slice((CyberarmEngine::Stats.max_frame_history / samples.to_f).ceil) do |bucket|
142
+ bucket.each do |frame|
143
+ nodes[slice] << frame.frame_timing.duration
144
+ end
145
+
146
+ slice += 1
147
+ end
148
+
149
+ nodes.each_with_index do |cluster, i|
150
+ break if cluster.empty?
151
+
152
+ @graphs[:frame_timings] << CyberarmEngine::Vector.new(@position.x + @padding + 1 * i, (@position.y + @height - @padding) - cluster.max)
153
+ end
154
+ end
155
+
156
+ def draw
157
+ @frame_stats = CyberarmEngine::Stats.frames.select(&:complete?)
158
+ return if @frame_stats.empty?
159
+
160
+ calculate_graphs
161
+
162
+ @max_timing_label.text = "Max: #{@frame_stats.map { |f| f.frame_timing.duration }.max.to_s.rjust(3, " ")}ms"
163
+ @avg_timing_label.text = "Avg: #{(@frame_stats.map { |f| f.frame_timing.duration }.sum / @frame_stats.size).to_s.rjust(3, " ")}ms"
164
+ @min_timing_label.text = "Min: #{@frame_stats.map { |f| f.frame_timing.duration }.min.to_s.rjust(3, " ")}ms"
165
+
166
+ Gosu.draw_rect(@position.x, @position.y, @width, @height, 0xaa_222222, @position.z)
167
+ Gosu.draw_rect(@position.x + @padding, @position.y + @padding, @width - @padding * 2, @height - @padding * 2, 0xaa_222222, @position.z)
168
+
169
+ draw_graphs
170
+
171
+ @max_timing_label.draw
172
+ @avg_timing_label.draw
173
+ @min_timing_label.draw
174
+
175
+ # TODO: Make this optional
176
+ draw_timings
177
+ end
178
+
179
+ def draw_graphs
180
+ Gosu.draw_path(@graphs[:frame_timings], Gosu::Color::WHITE, Float::INFINITY)
181
+ end
182
+
183
+ def draw_timings
184
+ frame = @frame_stats.last
185
+
186
+ @timings_label.text = "#{frame.attempted_multitiming? ? "<c=d00>Attempted Multitiming!\nTimings may be inaccurate for:\n#{frame.multitimings.map { |m, _| m}.join("\n") }</c>\n\n" : ''}#{frame.timings.map { |t, v| "#{t}: #{v.duration}ms" }.join("\n")}"
187
+ Gosu.draw_rect(@timings_label.x - @padding, @timings_label.y - @padding, @timings_label.width + @padding * 2, @timings_label.height + @padding * 2, 0xdd_222222, @position.z)
188
+ @timings_label.draw
18
189
  end
19
190
  end
20
191
  end
@@ -81,6 +81,7 @@ module CyberarmEngine
81
81
  @size = size
82
82
  @font = font_name
83
83
 
84
+ invalidate_cache!
84
85
  @textobject = check_cache(size, font_name)
85
86
  end
86
87
  end
@@ -149,6 +150,8 @@ module CyberarmEngine
149
150
  end
150
151
 
151
152
  def markup_width(text = @text)
153
+ text = text.to_s
154
+
152
155
  spacing = 0
153
156
  spacing += @border_size if @border
154
157
  spacing += @shadow_size if @shadow
@@ -30,6 +30,8 @@ module CyberarmEngine
30
30
  @y = @style.y
31
31
  @z = @style.z
32
32
 
33
+ @old_width = 0
34
+ @old_height = 0
33
35
  @width = 0
34
36
  @height = 0
35
37
 
@@ -196,7 +198,7 @@ module CyberarmEngine
196
198
  end
197
199
 
198
200
  def enter(_sender)
199
- @focus = false unless window.button_down?(Gosu::MsLeft)
201
+ @focus = false unless Gosu.button_down?(Gosu::MS_LEFT)
200
202
 
201
203
  if !@enabled
202
204
  update_styles(:disabled)
@@ -316,7 +318,8 @@ module CyberarmEngine
316
318
  end
317
319
 
318
320
  def debug_draw
319
- return if defined?(GUI_DEBUG_ONLY_ELEMENT) && self.class == GUI_DEBUG_ONLY_ELEMENT
321
+ # FIXME
322
+ return# if const_defined?(GUI_DEBUG_ONLY_ELEMENT) && self.class == GUI_DEBUG_ONLY_ELEMENT
320
323
 
321
324
  Gosu.draw_line(
322
325
  x, y, @debug_color,
@@ -341,6 +344,7 @@ module CyberarmEngine
341
344
  end
342
345
 
343
346
  def update
347
+ recalculate_if_size_changed
344
348
  end
345
349
 
346
350
  def button_down(id)
@@ -410,10 +414,14 @@ module CyberarmEngine
410
414
  end
411
415
 
412
416
  def scroll_width
413
- @children.sum(&:outer_width)
417
+ return @cached_scroll_width if @cached_scroll_width && is_a?(Container)
418
+
419
+ @cached_scroll_width = @children.sum(&:outer_width)
414
420
  end
415
421
 
416
422
  def scroll_height
423
+ return @cached_scroll_height if @cached_scroll_height && is_a?(Container)
424
+
417
425
  if is_a?(CyberarmEngine::Element::Flow)
418
426
  return 0 if @children.size.zero?
419
427
 
@@ -434,18 +442,18 @@ module CyberarmEngine
434
442
 
435
443
  pairs_ << a_ unless pairs_.last == a_
436
444
 
437
- pairs_.sum { |pair| + @style.padding_top + @style.border_thickness_top + pair.map(&:outer_height).max } + @style.padding_bottom + @style.border_thickness_bottom
445
+ @cached_scroll_height = pairs_.sum { |pair| + @style.padding_top + @style.border_thickness_top + pair.map(&:outer_height).max } + @style.padding_bottom + @style.border_thickness_bottom
438
446
  else
439
- @style.padding_top + @style.border_thickness_top + @children.sum(&:outer_height) + @style.padding_bottom + @style.border_thickness_bottom
447
+ @cached_scroll_height = @style.padding_top + @style.border_thickness_top + @children.sum(&:outer_height) + @style.padding_bottom + @style.border_thickness_bottom
440
448
  end
441
449
  end
442
450
 
443
451
  def max_scroll_width
444
- scroll_width - outer_width
452
+ (scroll_width - outer_width).positive? ? scroll_width - outer_width : scroll_width
445
453
  end
446
454
 
447
455
  def max_scroll_height
448
- scroll_height - outer_height
456
+ (scroll_height - outer_height).positive? ? scroll_height - outer_height : scroll_height
449
457
  end
450
458
 
451
459
  def dimensional_size(size, dimension)
@@ -461,15 +469,13 @@ module CyberarmEngine
461
469
  if @parent && @style.fill &&
462
470
  (dimension == :width && @parent.is_a?(Flow) ||
463
471
  dimension == :height && @parent.is_a?(Stack))
464
- return space_available_width - noncontent_width if dimension == :width && @parent.is_a?(Flow)
465
- return space_available_height - noncontent_height if dimension == :height && @parent.is_a?(Stack)
466
-
467
- # Handle min_width/height and max_width/height
468
- else
469
- return @style.send(:"min_#{dimension}") if @style.send(:"min_#{dimension}") && new_size.to_f < @style.send(:"min_#{dimension}")
470
- return @style.send(:"max_#{dimension}") if @style.send(:"max_#{dimension}") && new_size.to_f > @style.send(:"max_#{dimension}")
472
+ new_size = space_available_width - noncontent_width if dimension == :width && @parent.is_a?(Flow)
473
+ new_size = space_available_height - noncontent_height if dimension == :height && @parent.is_a?(Stack)
471
474
  end
472
475
 
476
+ return @style.send(:"min_#{dimension}") if @style.send(:"min_#{dimension}") && new_size.to_f < @style.send(:"min_#{dimension}")
477
+ return @style.send(:"max_#{dimension}") if @style.send(:"max_#{dimension}") && new_size.to_f > @style.send(:"max_#{dimension}")
478
+
473
479
  new_size
474
480
  end
475
481
 
@@ -555,6 +561,15 @@ module CyberarmEngine
555
561
  @style.background_image_canvas.image = @style.background_image
556
562
  end
557
563
 
564
+ def recalculate_if_size_changed
565
+ if !is_a?(ToolTip) && (@old_width != width || @old_height != height)
566
+ root.gui_state.request_recalculate
567
+
568
+ @old_width = width
569
+ @old_height = height
570
+ end
571
+ end
572
+
558
573
  def root
559
574
  return self if is_root?
560
575
 
@@ -575,6 +590,22 @@ module CyberarmEngine
575
590
  @gui_state != nil
576
591
  end
577
592
 
593
+ def child_of?(element)
594
+ return element == self if is_root?
595
+ return false unless element.is_a?(Container)
596
+ return true if element.children.find { |child| child == self }
597
+
598
+ element.children.find { |child| child.child_of?(element) if child.is_a?(Container) }
599
+ end
600
+
601
+ def parent_of?(element)
602
+ return false if element == self
603
+ return false unless is_a?(Container)
604
+ return true if @children.find { |child| child == element }
605
+
606
+ @children.find { |child| child.parent_of?(element) if child.is_a?(Container) }
607
+ end
608
+
578
609
  def focus(_)
579
610
  warn "#{self.class}#focus was not overridden!"
580
611