glimmer-dsl-swt 4.18.2.3 → 4.18.3.2

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 (44) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +59 -0
  3. data/README.md +267 -39
  4. data/VERSION +1 -1
  5. data/glimmer-dsl-swt.gemspec +14 -6
  6. data/lib/ext/glimmer/config.rb +24 -7
  7. data/lib/glimmer/data_binding/widget_binding.rb +14 -4
  8. data/lib/glimmer/dsl/swt/color_expression.rb +4 -4
  9. data/lib/glimmer/dsl/swt/data_binding_expression.rb +3 -3
  10. data/lib/glimmer/dsl/swt/dsl.rb +1 -0
  11. data/lib/glimmer/dsl/swt/multiply_expression.rb +53 -0
  12. data/lib/glimmer/dsl/swt/property_expression.rb +4 -2
  13. data/lib/glimmer/dsl/swt/shape_expression.rb +2 -4
  14. data/lib/glimmer/dsl/swt/transform_expression.rb +55 -0
  15. data/lib/glimmer/dsl/swt/widget_expression.rb +2 -1
  16. data/lib/glimmer/swt/color_proxy.rb +28 -6
  17. data/lib/glimmer/swt/custom/drawable.rb +8 -0
  18. data/lib/glimmer/swt/custom/shape.rb +66 -26
  19. data/lib/glimmer/swt/directory_dialog_proxy.rb +3 -3
  20. data/lib/glimmer/swt/display_proxy.rb +25 -4
  21. data/lib/glimmer/swt/file_dialog_proxy.rb +3 -3
  22. data/lib/glimmer/swt/layout_data_proxy.rb +3 -3
  23. data/lib/glimmer/swt/shell_proxy.rb +20 -5
  24. data/lib/glimmer/swt/table_proxy.rb +19 -4
  25. data/lib/glimmer/swt/transform_proxy.rb +109 -0
  26. data/lib/glimmer/swt/widget_listener_proxy.rb +14 -5
  27. data/lib/glimmer/swt/widget_proxy.rb +31 -20
  28. data/lib/glimmer/ui/custom_shell.rb +13 -11
  29. data/lib/glimmer/ui/custom_widget.rb +68 -44
  30. data/samples/elaborate/meta_sample.rb +81 -24
  31. data/samples/elaborate/tetris.rb +102 -47
  32. data/samples/elaborate/tetris/model/block.rb +2 -2
  33. data/samples/elaborate/tetris/model/game.rb +236 -74
  34. data/samples/elaborate/tetris/model/past_game.rb +26 -0
  35. data/samples/elaborate/tetris/model/tetromino.rb +123 -35
  36. data/samples/elaborate/tetris/view/block.rb +34 -9
  37. data/samples/elaborate/tetris/view/high_score_dialog.rb +114 -0
  38. data/samples/elaborate/tetris/view/playfield.rb +12 -5
  39. data/samples/elaborate/tetris/view/score_lane.rb +87 -0
  40. data/samples/elaborate/tetris/view/tetris_menu_bar.rb +123 -0
  41. data/samples/elaborate/tic_tac_toe.rb +4 -4
  42. data/samples/hello/hello_canvas_transform.rb +40 -0
  43. data/samples/hello/hello_link.rb +1 -1
  44. metadata +12 -4
@@ -24,79 +24,134 @@
24
24
  require_relative 'tetris/model/game'
25
25
 
26
26
  require_relative 'tetris/view/playfield'
27
+ require_relative 'tetris/view/score_lane'
28
+ require_relative 'tetris/view/high_score_dialog'
29
+ require_relative 'tetris/view/tetris_menu_bar'
27
30
 
28
31
  class Tetris
29
32
  include Glimmer::UI::CustomShell
30
33
 
31
34
  BLOCK_SIZE = 25
32
- PLAYFIELD_WIDTH = 10
33
- PLAYFIELD_HEIGHT = 20
35
+ FONT_NAME = 'Menlo'
36
+ FONT_TITLE_HEIGHT = 32
37
+ FONT_TITLE_STYLE = :bold
38
+
39
+ option :playfield_width, default: Model::Game::PLAYFIELD_WIDTH
40
+ option :playfield_height, default: Model::Game::PLAYFIELD_HEIGHT
41
+
42
+ attr_reader :game
34
43
 
35
44
  before_body {
36
- Model::Game.configure_beeper do
45
+ @mutex = Mutex.new
46
+ @game = Model::Game.new(playfield_width, playfield_height)
47
+
48
+ @game.configure_beeper do
37
49
  display.beep
38
50
  end
39
51
 
40
- Model::Game.start
41
-
52
+ Display.app_name = 'Glimmer Tetris'
42
53
  display {
43
- on_swt_keydown { |key_event|
44
- unless Model::Game.current_tetromino.stopped?
45
- case key_event.keyCode
46
- when swt(:arrow_down)
47
- Model::Game.current_tetromino.down
48
- when swt(:arrow_left)
49
- Model::Game.current_tetromino.left
50
- when swt(:arrow_right)
51
- Model::Game.current_tetromino.right
52
- when swt(:shift)
53
- if key_event.keyLocation == swt(:right) # right shift key
54
- Model::Game.current_tetromino.rotate(:right)
55
- elsif key_event.keyLocation == swt(:left) # left shift key
56
- Model::Game.current_tetromino.rotate(:left)
57
- end
58
- when 'd'.bytes.first, swt(:arrow_up)
59
- Model::Game.current_tetromino.rotate(:right)
60
- when 'a'.bytes.first
61
- Model::Game.current_tetromino.rotate(:left)
54
+ @keyboard_listener = on_swt_keydown { |key_event|
55
+ case key_event.keyCode
56
+ when swt(:arrow_down), 's'.bytes.first
57
+ game.down!
58
+ when swt(:arrow_left), 'a'.bytes.first
59
+ game.left!
60
+ when swt(:arrow_right), 'd'.bytes.first
61
+ game.right!
62
+ when swt(:shift), swt(:alt)
63
+ if key_event.keyLocation == swt(:right) # right shift key
64
+ game.rotate!(:right)
65
+ elsif key_event.keyLocation == swt(:left) # left shift key
66
+ game.rotate!(:left)
62
67
  end
68
+ when swt(:arrow_up)
69
+ game.rotate!(:right)
70
+ when swt(:ctrl)
71
+ game.rotate!(:left)
63
72
  end
64
73
  }
74
+
75
+ # if running in app mode, set the Mac app about dialog (ignored in platforms)
76
+ @about_observer = on_about {
77
+ show_about_dialog
78
+ }
65
79
  }
66
80
  }
67
81
 
68
82
  after_body {
69
- Thread.new {
70
- loop {
71
- sleep(0.9)
72
- sync_exec {
73
- unless @game_over
74
- Model::Game.current_tetromino.down
75
- if Model::Game.current_tetromino.stopped? && Model::Game.current_tetromino.row <= 0
76
- @game_over = true
77
- display.beep
78
- message_box(:icon_error) {
79
- text 'Tetris'
80
- message 'Game Over!'
81
- }.open
82
- Model::Game.restart
83
- @game_over = false
84
- end
85
- Model::Game.consider_adding_tetromino
86
- end
87
- }
88
- }
89
- }
83
+ @game_over_observer = observe(@game, :game_over) do |game_over|
84
+ if game_over
85
+ show_high_score_dialog
86
+ else
87
+ start_moving_tetrominos_down
88
+ end
89
+ end
90
+ @game.start!
90
91
  }
91
92
 
92
93
  body {
93
94
  shell(:no_resize) {
95
+ grid_layout {
96
+ num_columns 2
97
+ make_columns_equal_width false
98
+ margin_width 0
99
+ margin_height 0
100
+ horizontal_spacing 0
101
+ }
102
+
94
103
  text 'Glimmer Tetris'
104
+ minimum_size 475, 500
95
105
  background :gray
96
-
97
- playfield(playfield_width: PLAYFIELD_WIDTH, playfield_height: PLAYFIELD_HEIGHT, block_size: BLOCK_SIZE)
106
+
107
+ tetris_menu_bar(game: game)
108
+
109
+ playfield(game_playfield: game.playfield, playfield_width: playfield_width, playfield_height: playfield_height, block_size: BLOCK_SIZE)
110
+
111
+ score_lane(game: game, block_size: BLOCK_SIZE) {
112
+ layout_data(:fill, :fill, true, true)
113
+ }
114
+
115
+ on_widget_disposed {
116
+ deregister_observers
117
+ }
98
118
  }
99
119
  }
120
+
121
+ def start_moving_tetrominos_down
122
+ Thread.new do
123
+ @mutex.synchronize do
124
+ loop do
125
+ time = Time.now
126
+ sleep @game.delay
127
+ break if @game.game_over? || body_root.disposed?
128
+ sync_exec {
129
+ @game.down! unless @game.paused?
130
+ }
131
+ end
132
+ end
133
+ end
134
+ end
135
+
136
+ def show_high_score_dialog
137
+ return if @high_score_dialog&.visible?
138
+ @high_score_dialog = high_score_dialog(parent_shell: body_root, game: @game) if @high_score_dialog.nil? || @high_score_dialog.disposed?
139
+ @high_score_dialog.show
140
+ end
141
+
142
+ def show_about_dialog
143
+ message_box {
144
+ text 'Glimmer Tetris'
145
+ message "Glimmer Tetris\n\nGlimmer DSL for SWT Sample\n\nCopyright (c) 2007-2021 Andy Maleh"
146
+ }.open
147
+ end
148
+
149
+ def deregister_observers
150
+ @show_high_scores_observer.deregister
151
+ @game_over_observer.deregister
152
+ @keyboard_listener.deregister
153
+ @about_observer&.deregister
154
+ end
100
155
  end
101
156
 
102
157
  Tetris.launch
@@ -31,8 +31,9 @@ class Tetris
31
31
  @color = color
32
32
  end
33
33
 
34
+ # Clears block color. `quietly` option indicates if it should not notify observers by setting value quietly via variable not attribute writer.
34
35
  def clear
35
- self.color = COLOR_CLEAR
36
+ self.color = COLOR_CLEAR unless self.color == COLOR_CLEAR
36
37
  end
37
38
 
38
39
  def clear?
@@ -45,4 +46,3 @@ class Tetris
45
46
  end
46
47
  end
47
48
  end
48
-
@@ -19,98 +19,260 @@
19
19
  # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
20
  # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21
21
 
22
+ require 'fileutils'
23
+ require 'etc'
24
+ require 'glimmer/data_binding/observer'
25
+ require 'glimmer/config'
26
+
22
27
  require_relative 'block'
23
28
  require_relative 'tetromino'
29
+ require_relative 'past_game'
24
30
 
25
31
  class Tetris
26
32
  module Model
27
33
  class Game
28
- class << self
29
- def consider_adding_tetromino
30
- if tetrominoes.empty? || Game.current_tetromino.stopped?
31
- tetrominoes << Tetromino.new
32
- end
33
- end
34
-
35
- def current_tetromino
36
- tetrominoes.last
37
- end
34
+ PLAYFIELD_WIDTH = 10
35
+ PLAYFIELD_HEIGHT = 20
36
+ PREVIEW_PLAYFIELD_WIDTH = 4
37
+ PREVIEW_PLAYFIELD_HEIGHT = 2
38
+ SCORE_MULTIPLIER = {1 => 40, 2 => 100, 3 => 300, 4 => 1200}
39
+
40
+ attr_reader :playfield_width, :playfield_height
41
+ attr_accessor :game_over, :paused, :preview_tetromino, :lines, :score, :level, :high_scores, :beeping, :added_high_score
42
+ alias game_over? game_over
43
+ alias paused? paused
44
+ alias beeping? beeping
45
+ alias added_high_score? added_high_score
46
+
47
+ def initialize(playfield_width = PLAYFIELD_WIDTH, playfield_height = PLAYFIELD_HEIGHT)
48
+ @playfield_width = playfield_width
49
+ @playfield_height = playfield_height
50
+ @high_scores = []
51
+ @beeping = true
52
+ load_high_scores!
53
+ end
54
+
55
+ def configure_beeper(&beeper)
56
+ @beeper = beeper
57
+ end
58
+
59
+ def game_in_progress?
60
+ !game_over? && !paused?
61
+ end
62
+
63
+ def start!
64
+ self.paused = false
65
+ self.level = 1
66
+ self.score = 0
67
+ self.lines = 0
68
+ reset_playfield
69
+ reset_preview_playfield
70
+ reset_tetrominoes
71
+ preview_next_tetromino!
72
+ consider_adding_tetromino
73
+ self.game_over = false
74
+ end
75
+ alias restart! start!
76
+
77
+ def game_over!
78
+ add_high_score!
79
+ beep
80
+ self.game_over = true
81
+ end
82
+
83
+ def clear_high_scores!
84
+ high_scores.clear
85
+ end
86
+
87
+ def add_high_score!
88
+ self.added_high_score = true
89
+ high_scores.prepend(PastGame.new("Player #{high_scores.count + 1}", score))
90
+ end
91
+
92
+ def save_high_scores!
93
+ high_score_file_content = @high_scores.map {|past_game| past_game.to_a.join("\t") }.join("\n")
94
+ FileUtils.mkdir_p(tetris_dir)
95
+ File.write(tetris_high_score_file, high_score_file_content)
96
+ rescue => e
97
+ # Fail safely by keeping high scores in memory if unable to access disk
98
+ Glimmer::Config.logger.error {"Failed to save high scores in: #{tetris_high_score_file}\n#{e.full_message}"}
99
+ end
38
100
 
39
- def tetrominoes
40
- @tetrominoes ||= reset_tetrominoes
101
+ def load_high_scores!
102
+ if File.exist?(tetris_high_score_file)
103
+ self.high_scores = File.read(tetris_high_score_file).split("\n").map {|line| PastGame.new(*line.split("\t")) }
41
104
  end
42
-
43
- # Returns blocks in the playfield
44
- def playfield
45
- @playfield ||= PLAYFIELD_HEIGHT.times.map {
46
- PLAYFIELD_WIDTH.times.map {
47
- Block.new
48
- }
105
+ rescue => e
106
+ # Fail safely by keeping high scores in memory if unable to access disk
107
+ Glimmer::Config.logger.error {"Failed to load high scores from: #{tetris_high_score_file}\n#{e.full_message}"}
108
+ end
109
+
110
+ def tetris_dir
111
+ @tetris_dir ||= File.join(Etc.getpwuid.dir, '.glimmer-tetris')
112
+ end
113
+
114
+ def tetris_high_score_file
115
+ File.join(tetris_dir, "high_scores.txt")
116
+ end
117
+
118
+ def down!
119
+ return unless game_in_progress?
120
+ current_tetromino.down!
121
+ game_over! if current_tetromino.row <= 0 && current_tetromino.stopped?
122
+ end
123
+
124
+ def right!
125
+ return unless game_in_progress?
126
+ current_tetromino.right!
127
+ end
128
+
129
+ def left!
130
+ return unless game_in_progress?
131
+ current_tetromino.left!
132
+ end
133
+
134
+ def rotate!(direction)
135
+ return unless game_in_progress?
136
+ current_tetromino.rotate!(direction)
137
+ end
138
+
139
+ def current_tetromino
140
+ tetrominoes.last
141
+ end
142
+
143
+ def tetrominoes
144
+ @tetrominoes ||= reset_tetrominoes
145
+ end
146
+
147
+ # Returns blocks in the playfield
148
+ def playfield
149
+ @playfield ||= @original_playfield = @playfield_height.times.map {
150
+ @playfield_width.times.map {
151
+ Block.new
49
152
  }
50
- end
51
-
52
- def consider_eliminating_lines
53
- cleared_line = false
54
- playfield.each_with_index do |row, playfield_row|
55
- if row.all? {|block| !block.clear?}
56
- cleared_line = true
57
- shift_blocks_down_above_row(playfield_row)
58
- end
153
+ }
154
+ end
155
+
156
+ # Executes a hypothetical scenario without truly changing playfield permanently
157
+ def hypothetical(&block)
158
+ @playfield = hypothetical_playfield
159
+ block.call
160
+ @playfield = @original_playfield
161
+ end
162
+
163
+ # Returns whether currently executing a hypothetical scenario
164
+ def hypothetical?
165
+ @playfield != @original_playfield
166
+ end
167
+
168
+ def hypothetical_playfield
169
+ @playfield_height.times.map { |row|
170
+ @playfield_width.times.map { |column|
171
+ playfield[row][column].clone
172
+ }
173
+ }
174
+ end
175
+
176
+ def preview_playfield
177
+ @preview_playfield ||= PREVIEW_PLAYFIELD_HEIGHT.times.map {|row|
178
+ PREVIEW_PLAYFIELD_WIDTH.times.map {|column|
179
+ Block.new
180
+ }
181
+ }
182
+ end
183
+
184
+ def preview_next_tetromino!
185
+ self.preview_tetromino = Tetromino.new(self)
186
+ end
187
+
188
+ def calculate_score!(eliminated_lines)
189
+ new_score = SCORE_MULTIPLIER[eliminated_lines] * (level + 1)
190
+ self.score += new_score
191
+ end
192
+
193
+ def level_up!
194
+ self.level += 1 if lines >= self.level*10
195
+ end
196
+
197
+ def delay
198
+ [1.1 - (level.to_i * 0.1), 0.001].max
199
+ end
200
+
201
+ def beep
202
+ @beeper&.call if beeping
203
+ end
204
+
205
+ def reset_tetrominoes
206
+ @tetrominoes = []
207
+ end
208
+
209
+ def reset_playfield
210
+ playfield.each do |row|
211
+ row.each do |block|
212
+ block.clear
59
213
  end
60
- beep if cleared_line
61
- end
62
-
63
- def beep
64
- @beeper&.call
65
- end
66
-
67
- def configure_beeper(&beeper)
68
- @beeper = beeper
69
214
  end
70
-
71
- def shift_blocks_down_above_row(row)
72
- row.downto(0) do |playfield_row|
73
- playfield[playfield_row].each_with_index do |block, playfield_column|
74
- previous_row = playfield[playfield_row - 1]
75
- previous_block = previous_row[playfield_column]
76
- block.color = previous_block.color
77
- end
215
+ end
216
+
217
+ def reset_preview_playfield
218
+ preview_playfield.each do |row|
219
+ row.each do |block|
220
+ block.clear
78
221
  end
79
- playfield[0].each(&:clear)
80
- end
81
-
82
- def restart
83
- reset_playfield
84
- reset_tetrominoes
85
222
  end
86
- alias start restart
87
-
88
- def reset_tetrominoes
89
- @tetrominoes = [Tetromino.new]
223
+ end
224
+
225
+ def consider_adding_tetromino
226
+ if tetrominoes.empty? || current_tetromino.stopped?
227
+ preview_tetromino.launch!
228
+ preview_next_tetromino!
90
229
  end
91
-
92
- def reset_playfield
93
- playfield.each do |row|
94
- row.each do |block|
95
- block.clear
96
- end
230
+ end
231
+
232
+ def consider_eliminating_lines
233
+ eliminated_lines = 0
234
+ playfield.each_with_index do |row, playfield_row|
235
+ if row.all? {|block| !block.clear?}
236
+ eliminated_lines += 1
237
+ shift_blocks_down_above_row(playfield_row)
97
238
  end
98
239
  end
99
-
100
- def playfield_remaining_heights(tetromino = nil)
101
- PLAYFIELD_WIDTH.times.map do |playfield_column|
102
- (playfield.each_with_index.detect do |row, playfield_row|
103
- !row[playfield_column].clear? &&
104
- (
105
- tetromino.nil? ||
106
- tetromino.bottom_block_for_column(playfield_column).nil? ||
107
- (playfield_row > tetromino.row + tetromino.bottom_block_for_column(playfield_column)[:row])
108
- )
109
- end || [nil, PLAYFIELD_HEIGHT])[1]
110
- end.to_a
240
+ if eliminated_lines > 0
241
+ beep
242
+ self.lines += eliminated_lines
243
+ level_up!
244
+ calculate_score!(eliminated_lines)
245
+ end
246
+ end
247
+
248
+ def playfield_remaining_heights(tetromino = nil)
249
+ @playfield_width.times.map do |playfield_column|
250
+ (playfield.each_with_index.detect do |row, playfield_row|
251
+ !row[playfield_column].clear? &&
252
+ (
253
+ tetromino.nil? ||
254
+ (bottom_most_block = tetromino.bottom_most_block_for_column(playfield_column)).nil? ||
255
+ (playfield_row > tetromino.row + bottom_most_block[:row])
256
+ )
257
+ end || [nil, @playfield_height])[1]
258
+ end.to_a
259
+ end
260
+
261
+ private
262
+
263
+ def shift_blocks_down_above_row(row)
264
+ row.downto(0) do |playfield_row|
265
+ playfield[playfield_row].each_with_index do |block, playfield_column|
266
+ previous_row = playfield[playfield_row - 1]
267
+ previous_block = previous_row[playfield_column]
268
+ block.color = previous_block.color unless block.color == previous_block.color
269
+ end
111
270
  end
271
+ playfield[0].each(&:clear)
112
272
  end
273
+
113
274
  end
275
+
114
276
  end
277
+
115
278
  end
116
-