cyberarm_engine 0.23.0 → 0.24.1

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