glimmer-dsl-libui 0.4.6 → 0.4.10

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 (34) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +29 -0
  3. data/README.md +601 -323
  4. data/VERSION +1 -1
  5. data/examples/button_counter.rb +2 -1
  6. data/examples/cpu_percentage.rb +36 -0
  7. data/examples/date_time_picker.rb +19 -14
  8. data/examples/date_time_picker2.rb +20 -0
  9. data/examples/meta_example.rb +17 -6
  10. data/examples/midi_player.rb +5 -6
  11. data/examples/midi_player2.rb +83 -0
  12. data/examples/midi_player3.rb +84 -0
  13. data/examples/snake.rb +19 -10
  14. data/examples/snake2.rb +97 -0
  15. data/examples/tetris.rb +15 -18
  16. data/examples/tic_tac_toe.rb +1 -0
  17. data/examples/tic_tac_toe2.rb +84 -0
  18. data/glimmer-dsl-libui.gemspec +0 -0
  19. data/lib/glimmer/libui/control_proxy/checkbox_proxy.rb +4 -0
  20. data/lib/glimmer/libui/control_proxy/color_button_proxy.rb +1 -2
  21. data/lib/glimmer/libui/control_proxy/combobox_proxy.rb +17 -2
  22. data/lib/glimmer/libui/control_proxy/date_time_picker_proxy.rb +4 -0
  23. data/lib/glimmer/libui/control_proxy/editable_combobox_proxy.rb +4 -0
  24. data/lib/glimmer/libui/control_proxy/entry_proxy.rb +1 -2
  25. data/lib/glimmer/libui/control_proxy/font_button_proxy.rb +5 -1
  26. data/lib/glimmer/libui/control_proxy/menu_item_proxy/check_menu_item_proxy.rb +4 -0
  27. data/lib/glimmer/libui/control_proxy/menu_item_proxy/radio_menu_item_proxy.rb +16 -4
  28. data/lib/glimmer/libui/control_proxy/multiline_entry_proxy.rb +1 -2
  29. data/lib/glimmer/libui/control_proxy/radio_buttons_proxy.rb +19 -0
  30. data/lib/glimmer/libui/control_proxy/slider_proxy.rb +1 -2
  31. data/lib/glimmer/libui/control_proxy/spinbox_proxy.rb +1 -2
  32. data/lib/glimmer/libui/control_proxy.rb +2 -2
  33. data/lib/glimmer/libui/data_bindable.rb +27 -2
  34. metadata +8 -2
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.4.6
1
+ 0.4.10
@@ -13,7 +13,8 @@ class ButtonCounter
13
13
  window('Hello, Button!', 190, 20) {
14
14
  vertical_box {
15
15
  button {
16
- text <= [self, :count, on_read: ->(count) {"Count: #{count}"}] # data-bind button text to self count, converting to string on read.
16
+ # data-bind button text to self count, converting to string on read.
17
+ text <= [self, :count, on_read: ->(count) {"Count: #{count}"}]
17
18
 
18
19
  on_clicked do
19
20
  self.count += 1
@@ -0,0 +1,36 @@
1
+ require 'glimmer-dsl-libui'
2
+ require 'bigdecimal'
3
+
4
+ include Glimmer
5
+
6
+ data = [
7
+ ['CPU', '0%', 0],
8
+ ]
9
+
10
+ Glimmer::LibUI.timer(1) do
11
+ cpu_percentage_value = nil
12
+ if OS.windows?
13
+ cpu_percentage_raw_value = `wmic cpu get loadpercentage`
14
+ cpu_percentage_value = cpu_percentage_raw_value.split("\n")[2].to_i
15
+ elsif OS.mac?
16
+ cpu_percentage_value = `ps -A -o %cpu | awk '{s+=$1} END {print s}'`.to_i
17
+ elsif OS.linux?
18
+ stats = `top -n 1`
19
+ idle_percentage = stats.split("\n")[2].match(/ni,.* (.*) .*id/)[1]
20
+ cpu_percentage_value = (BigDecimal(100) - BigDecimal(idle_percentage)).to_i
21
+ end
22
+ data[0][1] = "#{cpu_percentage_value}%"
23
+ data[0][2] = cpu_percentage_value
24
+ end
25
+
26
+ window('CPU Percentage', 400, 200) {
27
+ vertical_box {
28
+ table {
29
+ text_column('Name')
30
+ text_column('Value')
31
+ progress_bar_column('Percentage')
32
+
33
+ cell_rows data # implicit data-binding
34
+ }
35
+ }
36
+ }.show
@@ -2,19 +2,24 @@
2
2
 
3
3
  require 'glimmer-dsl-libui'
4
4
 
5
- include Glimmer
6
-
7
- window('Date Time Pickers', 300, 200) {
8
- vertical_box {
9
- date_time_picker { |dtp|
10
- on_changed do
11
- time = dtp.time
12
- p time
13
- end
14
- }
15
- }
5
+ class DateTimePicker
6
+ include Glimmer
7
+
8
+ attr_accessor :picked_time
16
9
 
17
- on_closing do
18
- puts 'Bye Bye'
10
+ def launch
11
+ window('Date Time Pickers', 300, 200) {
12
+ vertical_box {
13
+ date_time_picker {
14
+ time <=> [self, :picked_time, after_write: ->(time) { p time }]
15
+ }
16
+ }
17
+
18
+ on_closing do
19
+ puts 'Bye Bye'
20
+ end
21
+ }.show
19
22
  end
20
- }.show
23
+ end
24
+
25
+ DateTimePicker.new.launch
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'glimmer-dsl-libui'
4
+
5
+ include Glimmer
6
+
7
+ window('Date Time Pickers', 300, 200) {
8
+ vertical_box {
9
+ date_time_picker { |dtp|
10
+ on_changed do
11
+ time = dtp.time
12
+ p time
13
+ end
14
+ }
15
+ }
16
+
17
+ on_closing do
18
+ puts 'Bye Bye'
19
+ end
20
+ }.show
@@ -24,18 +24,28 @@ class MetaExample
24
24
  @examples
25
25
  end
26
26
 
27
+ def basic_examples
28
+ examples.select {|example| example.start_with?('Basic') || ADDITIONAL_BASIC_EXAMPLES.include?(example) }
29
+ end
30
+
31
+ def advanced_examples
32
+ examples - basic_examples
33
+ end
34
+
27
35
  def examples_with_versions
28
- examples.map do |example|
29
- version_count_for(example) > 1 ? "#{example} (#{version_count_for(example)} versions)" : example
30
- end
36
+ append_versions(examples)
31
37
  end
32
38
 
33
39
  def basic_examples_with_versions
34
- examples_with_versions.select {|example| example.start_with?('Basic') || ADDITIONAL_BASIC_EXAMPLES.include?(example) }
40
+ append_versions(basic_examples)
35
41
  end
36
42
 
37
43
  def advanced_examples_with_versions
38
- examples_with_versions - basic_examples_with_versions
44
+ append_versions(advanced_examples)
45
+ end
46
+
47
+ def append_versions(examples)
48
+ examples.map { |example| version_count_for(example) > 1 ? "#{example} (#{version_count_for(example)} versions)" : example }
39
49
  end
40
50
 
41
51
  def file_path_for(example)
@@ -72,7 +82,7 @@ class MetaExample
72
82
  end
73
83
 
74
84
  def launch
75
- window('Meta-Example', 700, 500) {
85
+ window('Meta-Example', 1000, 500) {
76
86
  margined true
77
87
 
78
88
  horizontal_box {
@@ -107,6 +117,7 @@ class MetaExample
107
117
  @advanced_example_radio_buttons = radio_buttons {
108
118
  stretchy false
109
119
  items advanced_examples_with_versions
120
+ selected -1
110
121
 
111
122
  on_selected do
112
123
  @selected_example_index = examples_with_versions.index(advanced_examples_with_versions[@advanced_example_radio_buttons.selected])
@@ -6,6 +6,8 @@ class TinyMidiPlayer
6
6
  include Glimmer
7
7
 
8
8
  VERSION = '0.0.1'
9
+
10
+ attr_accessor :selected_file
9
11
 
10
12
  def initialize
11
13
  @pid = nil
@@ -68,13 +70,10 @@ class TinyMidiPlayer
68
70
  }
69
71
  }
70
72
 
71
- combobox { |c|
73
+ combobox {
72
74
  items @midi_files.map { |path| File.basename(path) }
73
-
74
- on_selected do
75
- @selected_file = @midi_files[c.selected]
76
- play_midi if @th&.alive?
77
- end
75
+ # data-bind selected item (String) to self.selected_file with on-read/on-write converters and after_write operation
76
+ selected_item <=> [self, :selected_file, on_read: ->(f) {File.basename(f.to_s)}, on_write: ->(f) {File.join(@music_directory, f)}, after_write: -> { play_midi if @th&.alive? }]
78
77
  }
79
78
  }
80
79
  }.show
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'glimmer-dsl-libui'
4
+
5
+ class TinyMidiPlayer
6
+ include Glimmer
7
+
8
+ VERSION = '0.0.1'
9
+
10
+ attr_accessor :selected_file
11
+
12
+ def initialize
13
+ @pid = nil
14
+ @music_directory = File.expand_path('../sounds', __dir__)
15
+ @midi_files = Dir.glob(File.join(@music_directory, '**/*.mid'))
16
+ .sort_by { |path| File.basename(path) }
17
+ at_exit { stop_midi }
18
+ create_gui
19
+ end
20
+
21
+ def stop_midi
22
+ if @pid
23
+ Process.kill(:SIGKILL, @pid) if @th.alive?
24
+ @pid = nil
25
+ end
26
+ end
27
+
28
+ def play_midi
29
+ stop_midi
30
+ if @pid.nil? && @selected_file
31
+ begin
32
+ @pid = spawn "timidity #{@selected_file}"
33
+ @th = Process.detach @pid
34
+ rescue Errno::ENOENT
35
+ warn 'Timidty++ not found. Please install Timidity++.'
36
+ warn 'https://sourceforge.net/projects/timidity/'
37
+ end
38
+ end
39
+ end
40
+
41
+ def show_version
42
+ msg_box('Tiny Midi Player',
43
+ "Written in Ruby\n" \
44
+ "https://github.com/kojix2/libui\n" \
45
+ "Version #{VERSION}")
46
+ end
47
+
48
+ def create_gui
49
+ menu('Help') {
50
+ menu_item('Version') {
51
+ on_clicked do
52
+ show_version
53
+ end
54
+ }
55
+ }
56
+ window('Tiny Midi Player', 200, 50) {
57
+ horizontal_box {
58
+ vertical_box {
59
+ stretchy false
60
+
61
+ button('▶') {
62
+ on_clicked do
63
+ play_midi
64
+ end
65
+ }
66
+ button('■') {
67
+ on_clicked do
68
+ stop_midi
69
+ end
70
+ }
71
+ }
72
+
73
+ combobox { |c|
74
+ items @midi_files.map { |path| File.basename(path) }
75
+ # data-bind selected index (Integer) to self.selected_file with on-read/on-write converters and after_write operation
76
+ selected <=> [self, :selected_file, on_read: ->(f) {@midi_files.index(f)}, on_write: ->(i) {@midi_files[i]}, after_write: -> { play_midi if @th&.alive? }]
77
+ }
78
+ }
79
+ }.show
80
+ end
81
+ end
82
+
83
+ TinyMidiPlayer.new
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'glimmer-dsl-libui'
4
+
5
+ class TinyMidiPlayer
6
+ include Glimmer
7
+
8
+ VERSION = '0.0.1'
9
+
10
+ def initialize
11
+ @pid = nil
12
+ @music_directory = File.expand_path('../sounds', __dir__)
13
+ @midi_files = Dir.glob(File.join(@music_directory, '**/*.mid'))
14
+ .sort_by { |path| File.basename(path) }
15
+ at_exit { stop_midi }
16
+ create_gui
17
+ end
18
+
19
+ def stop_midi
20
+ if @pid
21
+ Process.kill(:SIGKILL, @pid) if @th.alive?
22
+ @pid = nil
23
+ end
24
+ end
25
+
26
+ def play_midi
27
+ stop_midi
28
+ if @pid.nil? && @selected_file
29
+ begin
30
+ @pid = spawn "timidity #{@selected_file}"
31
+ @th = Process.detach @pid
32
+ rescue Errno::ENOENT
33
+ warn 'Timidty++ not found. Please install Timidity++.'
34
+ warn 'https://sourceforge.net/projects/timidity/'
35
+ end
36
+ end
37
+ end
38
+
39
+ def show_version
40
+ msg_box('Tiny Midi Player',
41
+ "Written in Ruby\n" \
42
+ "https://github.com/kojix2/libui\n" \
43
+ "Version #{VERSION}")
44
+ end
45
+
46
+ def create_gui
47
+ menu('Help') {
48
+ menu_item('Version') {
49
+ on_clicked do
50
+ show_version
51
+ end
52
+ }
53
+ }
54
+ window('Tiny Midi Player', 200, 50) {
55
+ horizontal_box {
56
+ vertical_box {
57
+ stretchy false
58
+
59
+ button('▶') {
60
+ on_clicked do
61
+ play_midi
62
+ end
63
+ }
64
+ button('■') {
65
+ on_clicked do
66
+ stop_midi
67
+ end
68
+ }
69
+ }
70
+
71
+ combobox { |c|
72
+ items @midi_files.map { |path| File.basename(path) }
73
+
74
+ on_selected do
75
+ @selected_file = @midi_files[c.selected]
76
+ play_midi if @th&.alive?
77
+ end
78
+ }
79
+ }
80
+ }.show
81
+ end
82
+ end
83
+
84
+ TinyMidiPlayer.new
data/examples/snake.rb CHANGED
@@ -12,6 +12,7 @@ class Snake
12
12
  @game = Model::Game.new
13
13
  @grid = Presenter::Grid.new(@game)
14
14
  @game.start
15
+ @keypress_queue = []
15
16
  create_gui
16
17
  register_observers
17
18
  end
@@ -31,7 +32,23 @@ class Snake
31
32
  end
32
33
 
33
34
  Glimmer::LibUI.timer(SNAKE_MOVE_DELAY) do
34
- @game.snake.move unless @game.over?
35
+ unless @game.over?
36
+ process_queued_keypress
37
+ @game.snake.move
38
+ end
39
+ end
40
+ end
41
+
42
+ def process_queued_keypress
43
+ # key press queue ensures one turn per snake move to avoid a double-turn resulting in instant death (due to snake illogically going back against itself)
44
+ key = @keypress_queue.shift
45
+ case [@game.snake.head.orientation, key]
46
+ in [:north, :right] | [:east, :down] | [:south, :left] | [:west, :up]
47
+ @game.snake.turn_right
48
+ in [:north, :left] | [:west, :down] | [:south, :right] | [:east, :up]
49
+ @game.snake.turn_left
50
+ else
51
+ # No Op
35
52
  end
36
53
  end
37
54
 
@@ -56,15 +73,7 @@ class Snake
56
73
  }
57
74
 
58
75
  on_key_up do |area_key_event|
59
- orientation_and_key = [@game.snake.head.orientation, area_key_event[:ext_key]]
60
- case orientation_and_key
61
- in [:north, :right] | [:east, :down] | [:south, :left] | [:west, :up]
62
- @game.snake.turn_right
63
- in [:north, :left] | [:west, :down] | [:south, :right] | [:east, :up]
64
- @game.snake.turn_left
65
- else
66
- # No Op
67
- end
76
+ @keypress_queue << area_key_event[:ext_key]
68
77
  end
69
78
  }
70
79
  end
@@ -0,0 +1,97 @@
1
+ require 'glimmer-dsl-libui'
2
+
3
+ require_relative 'snake/presenter/grid'
4
+
5
+ class Snake
6
+ include Glimmer
7
+
8
+ CELL_SIZE = 15
9
+ SNAKE_MOVE_DELAY = 0.1
10
+
11
+ def initialize
12
+ @game = Model::Game.new
13
+ @grid = Presenter::Grid.new(@game)
14
+ @game.start
15
+ @keypress_queue = []
16
+ create_gui
17
+ register_observers
18
+ end
19
+
20
+ def launch
21
+ @main_window.show
22
+ end
23
+
24
+ def register_observers
25
+ @game.height.times do |row|
26
+ @game.width.times do |column|
27
+ observe(@grid.cells[row][column], :color) do |new_color|
28
+ @cell_grid[row][column].fill = new_color
29
+ end
30
+ end
31
+ end
32
+
33
+ observe(@game, :over) do |game_over|
34
+ Glimmer::LibUI.queue_main do
35
+ if game_over
36
+ msg_box('Game Over!', "Score: #{@game.score} | High Score: #{@game.high_score}")
37
+ @game.start
38
+ end
39
+ end
40
+ end
41
+
42
+ Glimmer::LibUI.timer(SNAKE_MOVE_DELAY) do
43
+ unless @game.over?
44
+ process_queued_keypress
45
+ @game.snake.move
46
+ end
47
+ end
48
+ end
49
+
50
+ def process_queued_keypress
51
+ # key press queue ensures one turn per snake move to avoid a double-turn resulting in instant death (due to snake illogically going back against itself)
52
+ key = @keypress_queue.shift
53
+ case [@game.snake.head.orientation, key]
54
+ in [:north, :right] | [:east, :down] | [:south, :left] | [:west, :up]
55
+ @game.snake.turn_right
56
+ in [:north, :left] | [:west, :down] | [:south, :right] | [:east, :up]
57
+ @game.snake.turn_left
58
+ else
59
+ # No Op
60
+ end
61
+ end
62
+
63
+ def create_gui
64
+ @cell_grid = []
65
+ @main_window = window {
66
+ # data-bind window title to game score, converting it to a title string on read from the model
67
+ title <= [@game, :score, on_read: -> (score) {"Snake (Score: #{@game.score})"}]
68
+ content_size @game.width * CELL_SIZE, @game.height * CELL_SIZE
69
+ resizable false
70
+
71
+ vertical_box {
72
+ padded false
73
+
74
+ @game.height.times do |row|
75
+ @cell_grid << []
76
+ horizontal_box {
77
+ padded false
78
+
79
+ @game.width.times do |column|
80
+ area {
81
+ @cell_grid.last << square(0, 0, CELL_SIZE) {
82
+ fill Presenter::Cell::COLOR_CLEAR
83
+ }
84
+
85
+ on_key_up do |area_key_event|
86
+ @keypress_queue << area_key_event[:ext_key]
87
+ end
88
+ }
89
+ end
90
+ }
91
+ end
92
+ }
93
+ }
94
+ end
95
+ end
96
+
97
+ Snake.new.launch
data/examples/tetris.rb CHANGED
@@ -109,22 +109,23 @@ class Tetris
109
109
  menu('Game') {
110
110
  @pause_menu_item = check_menu_item('Pause') {
111
111
  enabled false
112
-
113
- on_clicked do
114
- @game.paused = @pause_menu_item.checked?
115
- end
112
+ checked <=> [@game, :paused]
116
113
  }
114
+
117
115
  menu_item('Restart') {
118
116
  on_clicked do
119
117
  @game.restart!
120
118
  end
121
119
  }
120
+
122
121
  separator_menu_item
122
+
123
123
  menu_item('Exit') {
124
124
  on_clicked do
125
125
  exit(0)
126
126
  end
127
127
  }
128
+
128
129
  quit_menu_item if OS.mac?
129
130
  }
130
131
 
@@ -134,6 +135,7 @@ class Tetris
134
135
  show_high_scores
135
136
  end
136
137
  }
138
+
137
139
  menu_item('Clear High Scores') {
138
140
  on_clicked {
139
141
  @game.clear_high_scores!
@@ -142,22 +144,16 @@ class Tetris
142
144
  }
143
145
 
144
146
  menu('Options') {
145
- radio_menu_item('Instant Down on Up Arrow') {
146
- on_clicked do
147
- @game.instant_down_on_up = true
148
- end
147
+ radio_menu_item('Instant Down on Up Arrow') { |r|
148
+ checked <=> [@game, :instant_down_on_up]
149
149
  }
150
- radio_menu_item('Rotate Right on Up Arrow') {
151
- on_clicked do
152
- @game.rotate_right_on_up = true
153
- end
150
+
151
+ radio_menu_item('Rotate Right on Up Arrow') { |r|
152
+ checked <=> [@game, :rotate_right_on_up]
154
153
  }
155
- radio_menu_item('Rotate Left on Up Arrow') {
156
- checked true
157
-
158
- on_clicked do
159
- @game.rotate_left_on_up = true
160
- end
154
+
155
+ radio_menu_item('Rotate Left on Up Arrow') { |r|
156
+ checked <=> [@game, :rotate_left_on_up]
161
157
  }
162
158
  }
163
159
 
@@ -169,6 +165,7 @@ class Tetris
169
165
  end
170
166
  }
171
167
  end
168
+
172
169
  menu_item('About') {
173
170
  on_clicked do
174
171
  show_about_dialog
@@ -41,6 +41,7 @@ class TicTacToe
41
41
  text(23, 19) {
42
42
  string {
43
43
  font family: 'Arial', size: OS.mac? ? 20 : 16
44
+ # data-bind string property of area text attributed string to tic tac toe board cell sign
44
45
  string <= [@tic_tac_toe_board[row + 1, column + 1], :sign] # board model is 1-based
45
46
  }
46
47
  }
@@ -0,0 +1,84 @@
1
+
2
+ require 'glimmer-dsl-libui'
3
+
4
+ require_relative "tic_tac_toe/board"
5
+
6
+ class TicTacToe
7
+ include Glimmer
8
+
9
+ def initialize
10
+ @tic_tac_toe_board = Board.new
11
+ end
12
+
13
+ def launch
14
+ create_gui
15
+ register_observers
16
+ @main_window.show
17
+ end
18
+
19
+ def register_observers
20
+ observe(@tic_tac_toe_board, :game_status) do |game_status|
21
+ display_win_message if game_status == Board::WIN
22
+ display_draw_message if game_status == Board::DRAW
23
+ end
24
+
25
+ 3.times.map do |row|
26
+ 3.times.map do |column|
27
+ observe(@tic_tac_toe_board[row + 1, column + 1], :sign) do |sign| # board model is 1-based
28
+ @cells[row][column].string = sign
29
+ end
30
+ end
31
+ end
32
+ end
33
+
34
+ def create_gui
35
+ @main_window = window('Tic-Tac-Toe', 180, 180) {
36
+ resizable false
37
+
38
+ @cells = []
39
+ vertical_box {
40
+ padded false
41
+
42
+ 3.times.map do |row|
43
+ @cells << []
44
+ horizontal_box {
45
+ padded false
46
+
47
+ 3.times.map do |column|
48
+ area {
49
+ square(0, 0, 60) {
50
+ stroke :black, thickness: 2
51
+ }
52
+ text(23, 19) {
53
+ @cells[row] << string('') {
54
+ font family: 'Arial', size: OS.mac? ? 20 : 16
55
+ }
56
+ }
57
+ on_mouse_up do
58
+ @tic_tac_toe_board.mark(row + 1, column + 1) # board model is 1-based
59
+ end
60
+ }
61
+ end
62
+ }
63
+ end
64
+ }
65
+ }
66
+ end
67
+
68
+ def display_win_message
69
+ display_game_over_message("Player #{@tic_tac_toe_board.winning_sign} has won!")
70
+ end
71
+
72
+ def display_draw_message
73
+ display_game_over_message("Draw!")
74
+ end
75
+
76
+ def display_game_over_message(message_text)
77
+ Glimmer::LibUI.queue_main do
78
+ msg_box('Game Over', message_text)
79
+ @tic_tac_toe_board.reset!
80
+ end
81
+ end
82
+ end
83
+
84
+ TicTacToe.new.launch
Binary file
@@ -29,6 +29,10 @@ module Glimmer
29
29
  # Follows the Proxy Design Pattern
30
30
  class CheckboxProxy < ControlProxy
31
31
  DEFAULT_TEXT = ''
32
+
33
+ def data_bind_write(property, model_binding)
34
+ handle_listener('on_toggled') { model_binding.call(checked) } if property == 'checked'
35
+ end
32
36
 
33
37
  private
34
38
 
@@ -114,8 +114,7 @@ module Glimmer
114
114
  super
115
115
  end
116
116
 
117
- def data_bind(property, model_binding)
118
- super
117
+ def data_bind_write(property, model_binding)
119
118
  handle_listener('on_changed') { model_binding.call(color) } if property == 'color'
120
119
  end
121
120
  end