glimmer-dsl-libui 0.5.3 → 0.5.6

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.
@@ -0,0 +1,113 @@
1
+ require 'glimmer-dsl-libui'
2
+ require 'facets'
3
+
4
+ include Glimmer
5
+
6
+ Address = Struct.new(:street, :p_o_box, :city, :state, :zip_code)
7
+
8
+ class FormField
9
+ include Glimmer::LibUI::CustomControl
10
+
11
+ options :model, :attribute
12
+
13
+ body {
14
+ entry { |e|
15
+ label attribute.to_s.underscore.split('_').map(&:capitalize).join(' ')
16
+ text <=> [model, attribute]
17
+ }
18
+ }
19
+ end
20
+
21
+ class AddressForm
22
+ include Glimmer::LibUI::CustomControl
23
+
24
+ options :address
25
+
26
+ body {
27
+ form {
28
+ form_field(model: address, attribute: :street)
29
+ form_field(model: address, attribute: :p_o_box)
30
+ form_field(model: address, attribute: :city)
31
+ form_field(model: address, attribute: :state)
32
+ form_field(model: address, attribute: :zip_code)
33
+ }
34
+ }
35
+ end
36
+
37
+ class LabelPair
38
+ include Glimmer::LibUI::CustomControl
39
+
40
+ options :model, :attribute, :value
41
+
42
+ body {
43
+ horizontal_box {
44
+ label(attribute.to_s.underscore.split('_').map(&:capitalize).join(' '))
45
+ label(value.to_s) {
46
+ text <= [model, attribute]
47
+ }
48
+ }
49
+ }
50
+ end
51
+
52
+ class AddressView
53
+ include Glimmer::LibUI::CustomControl
54
+
55
+ options :address
56
+
57
+ body {
58
+ vertical_box {
59
+ address.each_pair do |attribute, value|
60
+ label_pair(model: address, attribute: attribute, value: value)
61
+ end
62
+ }
63
+ }
64
+ end
65
+
66
+ address1 = Address.new('123 Main St', '23923', 'Denver', 'Colorado', '80014')
67
+ address2 = Address.new('2038 Park Ave', '83272', 'Boston', 'Massachusetts', '02101')
68
+
69
+ window('Class-Based Custom Keyword') {
70
+ margined true
71
+
72
+ horizontal_box {
73
+ vertical_box {
74
+ label('Address 1') {
75
+ stretchy false
76
+ }
77
+
78
+ address_form(address: address1)
79
+
80
+ horizontal_separator {
81
+ stretchy false
82
+ }
83
+
84
+ label('Address 1 (Saved)') {
85
+ stretchy false
86
+ }
87
+
88
+ address_view(address: address1)
89
+ }
90
+
91
+ vertical_separator {
92
+ stretchy false
93
+ }
94
+
95
+ vertical_box {
96
+ label('Address 2') {
97
+ stretchy false
98
+ }
99
+
100
+ address_form(address: address2)
101
+
102
+ horizontal_separator {
103
+ stretchy false
104
+ }
105
+
106
+ label('Address 2 (Saved)') {
107
+ stretchy false
108
+ }
109
+
110
+ address_view(address: address2)
111
+ }
112
+ }
113
+ }.show
@@ -32,7 +32,7 @@ def label_pair(model, attribute, value)
32
32
  }
33
33
  end
34
34
 
35
- def address(address_model)
35
+ def address_view(address_model)
36
36
  vertical_box {
37
37
  address_model.each_pair do |attribute, value|
38
38
  label_pair(address_model, attribute, value)
@@ -43,7 +43,7 @@ end
43
43
  address1 = Address.new('123 Main St', '23923', 'Denver', 'Colorado', '80014')
44
44
  address2 = Address.new('2038 Park Ave', '83272', 'Boston', 'Massachusetts', '02101')
45
45
 
46
- window('Method-Based Custom Keyword') {
46
+ window('Method-Based Custom Controls') {
47
47
  margined true
48
48
 
49
49
  horizontal_box {
@@ -62,7 +62,7 @@ window('Method-Based Custom Keyword') {
62
62
  stretchy false
63
63
  }
64
64
 
65
- address(address1)
65
+ address_view(address1)
66
66
  }
67
67
 
68
68
  vertical_separator {
@@ -84,7 +84,7 @@ window('Method-Based Custom Keyword') {
84
84
  stretchy false
85
85
  }
86
86
 
87
- address(address2)
87
+ address_view(address2)
88
88
  }
89
89
  }
90
90
  }.show
@@ -39,7 +39,7 @@ def label_pair(model, attribute, value)
39
39
  end
40
40
  end
41
41
 
42
- def address(address_model)
42
+ def address_view(address_model)
43
43
  vertical_box {
44
44
  address_model.each_pair do |attribute, value|
45
45
  label_pair(address_model, attribute, value)
@@ -50,7 +50,7 @@ end
50
50
  address1 = Address.new('123 Main St', '23923', 'Denver', 'Colorado', '80014')
51
51
  address2 = Address.new('2038 Park Ave', '83272', 'Boston', 'Massachusetts', '02101')
52
52
 
53
- window('Method-Based Custom Keyword') {
53
+ window('Method-Based Custom Controls') {
54
54
  margined true
55
55
 
56
56
  horizontal_box {
@@ -69,7 +69,7 @@ window('Method-Based Custom Keyword') {
69
69
  stretchy false
70
70
  }
71
71
 
72
- address(address1)
72
+ address_view(address1)
73
73
  }
74
74
 
75
75
  vertical_separator {
@@ -91,7 +91,7 @@ window('Method-Based Custom Keyword') {
91
91
  stretchy false
92
92
  }
93
93
 
94
- address(address2)
94
+ address_view(address2)
95
95
  }
96
96
  }
97
97
  }.show
Binary file
@@ -0,0 +1,58 @@
1
+ # Copyright (c) 2021-2022 Andy Maleh
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining
4
+ # a copy of this software and associated documentation files (the
5
+ # "Software"), to deal in the Software without restriction, including
6
+ # without limitation the rights to use, copy, modify, merge, publish,
7
+ # distribute, sublicense, and/or sell copies of the Software, and to
8
+ # permit persons to whom the Software is furnished to do so, subject to
9
+ # the following conditions:
10
+ #
11
+ # The above copyright notice and this permission notice shall be
12
+ # included in all copies or substantial portions of the Software.
13
+ #
14
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21
+
22
+ require 'glimmer'
23
+ require 'glimmer/dsl/expression'
24
+ require 'glimmer/dsl/parent_expression'
25
+ require 'glimmer/dsl/top_level_expression'
26
+ require 'glimmer/libui/custom_control'
27
+
28
+ module Glimmer
29
+ module DSL
30
+ module Libui
31
+ class CustomControlExpression < Expression
32
+ # TODO Consider making custom controls automatically generate static expressions
33
+ include ParentExpression
34
+ include TopLevelExpression
35
+
36
+ def can_interpret?(parent, keyword, *args, &block)
37
+ LibUI::CustomControl.for(keyword)
38
+ end
39
+
40
+ def interpret(parent, keyword, *args, &block)
41
+ options = args.last.is_a?(Hash) ? args.pop : {}
42
+ LibUI::CustomControl.for(keyword).new(keyword, parent, args, options, &block)
43
+ end
44
+
45
+ def add_content(custom_control, keyword, *args, &block)
46
+ options = args.last.is_a?(Hash) ? args.last : {post_add_content: true}
47
+ # TODO consider avoiding source_location
48
+ if block.source_location == custom_control.content&.__getobj__&.source_location
49
+ custom_control.content.call(custom_control) unless custom_control.content.called?
50
+ else
51
+ super
52
+ end
53
+ custom_control.post_add_content if options[:post_add_content]
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -43,6 +43,7 @@ module Glimmer
43
43
  string
44
44
  operation
45
45
  control
46
+ custom_control
46
47
  shape
47
48
  ]
48
49
  )
@@ -32,7 +32,8 @@ module Glimmer
32
32
  (
33
33
  parent.is_a?(Glimmer::LibUI::ControlProxy) or
34
34
  parent.is_a?(Glimmer::LibUI::Shape) or
35
- parent.is_a?(Glimmer::LibUI::AttributedString)
35
+ parent.is_a?(Glimmer::LibUI::AttributedString) or
36
+ parent.is_a?(Glimmer::LibUI::CustomControl)
36
37
  ) and
37
38
  block.nil? and
38
39
  parent.respond_to?("#{keyword}=", *args)
@@ -60,6 +60,10 @@ module Glimmer
60
60
  end
61
61
 
62
62
  def draw_fill_mode
63
+ @args[0].is_a?(Integer) ? (@args[0] == 0 ? :winding : :alternate ) : ((@args[0].is_a?(String) || @args[0].is_a?(Symbol)) ? @args[0].to_sym : :winding)
64
+ end
65
+
66
+ def draw_fill_mode_value
63
67
  @args[0].is_a?(Integer) ? @args[0] : @args[0].to_s == 'alternate' ? 1 : 0
64
68
  end
65
69
 
@@ -159,7 +163,7 @@ module Glimmer
159
163
  private
160
164
 
161
165
  def build_control
162
- @libui = ::LibUI.draw_new_path(draw_fill_mode)
166
+ @libui = ::LibUI.draw_new_path(draw_fill_mode_value)
163
167
  end
164
168
 
165
169
  def init_draw_brush(draw_brush, draw_brush_args)
@@ -0,0 +1,249 @@
1
+ # Copyright (c) 2021-2022 Andy Maleh
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining
4
+ # a copy of this software and associated documentation files (the
5
+ # "Software"), to deal in the Software without restriction, including
6
+ # without limitation the rights to use, copy, modify, merge, publish,
7
+ # distribute, sublicense, and/or sell copies of the Software, and to
8
+ # permit persons to whom the Software is furnished to do so, subject to
9
+ # the following conditions:
10
+ #
11
+ # The above copyright notice and this permission notice shall be
12
+ # included in all copies or substantial portions of the Software.
13
+ #
14
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21
+
22
+ require 'super_module'
23
+ require 'glimmer'
24
+ require 'glimmer/proc_tracker'
25
+ require 'glimmer/data_binding/observer'
26
+ require 'glimmer/data_binding/observable_model'
27
+
28
+ module Glimmer
29
+ module LibUI
30
+ module CustomControl
31
+ include SuperModule
32
+ include DataBinding::ObservableModel
33
+
34
+ super_module_included do |klass|
35
+ # TODO clear memoization of WidgetProxy.libui_class_for for a keyword if a custom control was defined with that keyword
36
+ klass.include(Glimmer)
37
+ Glimmer::LibUI::CustomControl.add_custom_control_namespaces_for(klass)
38
+ end
39
+
40
+ class << self
41
+ def for(keyword)
42
+ unless flyweight_custom_control_classes.keys.include?(keyword)
43
+ begin
44
+ extracted_namespaces = keyword.
45
+ to_s.
46
+ split(/__/).map do |namespace|
47
+ namespace.camelcase(:upper)
48
+ end
49
+ custom_control_namespaces.each do |base|
50
+ extracted_namespaces.reduce(base) do |result, namespace|
51
+ if !result.constants.include?(namespace)
52
+ namespace = result.constants.detect {|c| c.to_s.upcase == namespace.to_s.upcase } || namespace
53
+ end
54
+ begin
55
+ flyweight_custom_control_classes[keyword] = constant = result.const_get(namespace)
56
+ return constant if constant.ancestors.include?(Glimmer::LibUI::CustomControl)
57
+ flyweight_custom_control_classes[keyword] = constant
58
+ rescue => e
59
+ # Glimmer::Config.logger.debug {"#{e.message}\n#{e.backtrace.join("\n")}"}
60
+ flyweight_custom_control_classes[keyword] = result
61
+ end
62
+ end
63
+ end
64
+ raise "#{keyword} has no custom control class!"
65
+ rescue => e
66
+ Glimmer::Config.logger.debug {e.message}
67
+ Glimmer::Config.logger.debug {"#{e.message}\n#{e.backtrace.join("\n")}"}
68
+ flyweight_custom_control_classes[keyword] = nil
69
+ end
70
+ end
71
+ flyweight_custom_control_classes[keyword]
72
+ end
73
+
74
+ # Flyweight Design Pattern memoization cache. Can be cleared if memory is needed.
75
+ def flyweight_custom_control_classes
76
+ @flyweight_custom_control_classes ||= {}
77
+ end
78
+
79
+ # Returns keyword to use for this custom control
80
+ def keyword
81
+ self.name.underscore.gsub('::', '__')
82
+ end
83
+
84
+ # Returns shortcut keyword to use for this custom control (keyword minus namespace)
85
+ def shortcut_keyword
86
+ self.name.underscore.gsub('::', '__').split('__').last
87
+ end
88
+
89
+ def add_custom_control_namespaces_for(klass)
90
+ Glimmer::LibUI::CustomControl.namespaces_for_class(klass).drop(1).each do |namespace|
91
+ Glimmer::LibUI::CustomControl.custom_control_namespaces << namespace
92
+ end
93
+ end
94
+
95
+ def namespaces_for_class(m)
96
+ return [m] if m.name.nil?
97
+ namespace_constants = m.name.split(/::/).map(&:to_sym)
98
+ namespace_constants.reduce([Object]) do |output, namespace_constant|
99
+ output += [output.last.const_get(namespace_constant)]
100
+ end[1..-1].uniq.reverse
101
+ end
102
+
103
+ def custom_control_namespaces
104
+ @custom_control_namespaces ||= reset_custom_control_namespaces
105
+ end
106
+
107
+ def reset_custom_control_namespaces
108
+ @custom_control_namespaces = Set[Object, Glimmer::LibUI]
109
+ end
110
+
111
+ # Allows defining convenience option accessors for an array of option names
112
+ # Example: `options :color1, :color2` defines `#color1` and `#color2`
113
+ # where they return the instance values `options[:color1]` and `options[:color2]`
114
+ # respectively.
115
+ # Can be called multiple times to set more options additively.
116
+ # When passed no arguments, it returns list of all option names captured so far
117
+ def options(*new_options)
118
+ new_options = new_options.compact.map(&:to_s).map(&:to_sym)
119
+ if new_options.empty?
120
+ @options ||= {} # maps options to defaults
121
+ else
122
+ new_options = new_options.reduce({}) {|new_options_hash, new_option| new_options_hash.merge(new_option => nil)}
123
+ @options = options.merge(new_options)
124
+ def_option_attr_accessors(new_options)
125
+ end
126
+ end
127
+
128
+ def option(new_option, default: nil)
129
+ new_option = new_option.to_s.to_sym
130
+ new_options = {new_option => default}
131
+ @options = options.merge(new_options)
132
+ def_option_attr_accessors(new_options)
133
+ end
134
+
135
+ def def_option_attr_accessors(new_options)
136
+ new_options.each do |option, default|
137
+ class_eval <<-end_eval, __FILE__, __LINE__
138
+ def #{option}
139
+ options[:#{option}]
140
+ end
141
+
142
+ def #{option}=(option_value)
143
+ self.options[:#{option}] = option_value
144
+ end
145
+ end_eval
146
+ end
147
+ end
148
+
149
+ def before_body(&block)
150
+ @before_body_block = block
151
+ end
152
+
153
+ def body(&block)
154
+ @body_block = block
155
+ end
156
+
157
+ def after_body(&block)
158
+ @after_body_block = block
159
+ end
160
+ end
161
+
162
+ attr_reader :body_root, :libui, :parent, :parent_proxy, :args, :keyword, :content, :options
163
+
164
+ def initialize(keyword, parent, args, options, &content)
165
+ @parent_proxy = @parent = parent
166
+ options ||= {}
167
+ @options = self.class.options.merge(options)
168
+ @content = ProcTracker.new(content) if content
169
+ execute_hook('before_body')
170
+ body_block = self.class.instance_variable_get("@body_block")
171
+ raise Glimmer::Error, 'Invalid custom control for having no body! Please define body block!' if body_block.nil?
172
+ @body_root = instance_exec(&body_block)
173
+ raise Glimmer::Error, 'Invalid custom control for having an empty body! Please fill body block!' if @body_root.nil?
174
+ @libui = @body_root.libui
175
+ execute_hook('after_body')
176
+ # TODO deregister all observer_registrations on destroy of the control once that listener is supported
177
+ # (on_destroy) unless it is the last window closing, in which case exit faster
178
+ post_add_content if content.nil?
179
+ end
180
+
181
+ # Subclasses may override to perform post initialization work on an added child
182
+ def post_initialize_child(child)
183
+ # No Op by default
184
+ end
185
+
186
+ def post_add_content
187
+ # No Op by default
188
+ end
189
+
190
+ def observer_registrations
191
+ @observer_registrations ||= []
192
+ end
193
+
194
+ def can_handle_listener?(listener)
195
+ body_root&.can_handle_listener?(listener.to_s)
196
+ end
197
+
198
+ def handle_listener(listener, &block)
199
+ body_root.handle_listener(listener.to_s, &block)
200
+ end
201
+
202
+ # This method ensures it has an instance method not coming from Glimmer DSL
203
+ def has_instance_method?(method_name)
204
+ respond_to?(method_name) and
205
+ !@body_root.respond_to_libui?(method_name) and
206
+ (method(method_name) rescue nil) and
207
+ !method(method_name)&.source_location&.first&.include?('glimmer/dsl/engine.rb') and
208
+ !method(method_name)&.source_location&.first&.include?('glimmer/libui/control_proxy.rb')
209
+ end
210
+
211
+ # Returns content block if used as an attribute reader (no args)
212
+ # Otherwise, if a block is passed, it adds it as content to this custom control
213
+ def content(&block)
214
+ if block_given?
215
+ Glimmer::DSL::Engine.add_content(self, Glimmer::DSL::Libui::CustomControlExpression.new, self.class.keyword, &block)
216
+ else
217
+ @content
218
+ end
219
+ end
220
+
221
+ def method_missing(method_name, *args, &block)
222
+ # TODO Consider supporting a glimmer error silencing option for methods defined here
223
+ # but fail the glimmer DSL for the right reason to avoid seeing noise in the log output
224
+ if block && can_handle_listener?(method_name)
225
+ handle_listener(method_name, &block)
226
+ else
227
+ @body_root.send(method_name, *args, &block)
228
+ end
229
+ end
230
+
231
+ def respond_to?(method_name, *args, &block)
232
+ super or
233
+ can_handle_listener?(method_name) or
234
+ @body_root.respond_to?(method_name, *args, &block)
235
+ end
236
+
237
+ private
238
+
239
+ def execute_hook(hook_name)
240
+ hook_block = self.class.instance_variable_get("@#{hook_name}_block")
241
+ return if hook_block.nil?
242
+ temp_method_name = "#{hook_name}_block_#{hook_block.hash.abs}_#{(Time.now.to_f * 1_000_000).to_i}"
243
+ singleton_class.define_method(temp_method_name, &hook_block)
244
+ send(temp_method_name)
245
+ singleton_class.send(:remove_method, temp_method_name)
246
+ end
247
+ end
248
+ end
249
+ end
@@ -54,7 +54,7 @@ module Glimmer
54
54
  perfect_shape_dependencies = [x, y, c1_x, c1_y, c2_x, c2_y, end_x, end_y]
55
55
  if perfect_shape_dependencies != @perfect_shape_dependencies
56
56
  x, y, c1_x, c1_y, c2_x, c2_y, end_x, end_y = @perfect_shape_dependencies = perfect_shape_dependencies
57
- @perfect_shape = PerfectShape::CubicBezierCurve.new(points: [[x, y], [c1_x, c1_y], [c2_x, c2_y], [end_x, end_y]])
57
+ @perfect_shape = PerfectShape::CubicBezierCurve.new(points: [x, y, c1_x, c1_y, c2_x, c2_y, end_x, end_y].compact)
58
58
  end
59
59
  @perfect_shape
60
60
  end
@@ -50,6 +50,18 @@ module Glimmer
50
50
  alias closed= closed
51
51
  alias set_closed closed
52
52
  alias closed? closed
53
+
54
+ def perfect_shape
55
+ perfect_shape_dependencies = [x, y, closed, parent.draw_fill_mode, children]
56
+ if perfect_shape_dependencies != @perfect_shape_dependencies
57
+ x, y, closed, draw_fill_mode, children = @perfect_shape_dependencies = perfect_shape_dependencies
58
+ path_shapes = [[x, y]]
59
+ path_shapes += children.map(&:perfect_shape)
60
+ winding_rule = draw_fill_mode == :winding ? :wind_non_zero : :wind_even_odd
61
+ @perfect_shape = PerfectShape::Path.new(closed: closed, winding_rule: winding_rule, shapes: path_shapes)
62
+ end
63
+ @perfect_shape
64
+ end
53
65
  end
54
66
  end
55
67
  end
@@ -57,7 +57,7 @@ module Glimmer
57
57
  perfect_shape_dependencies = [x, y, end_x, end_y]
58
58
  if perfect_shape_dependencies != @perfect_shape_dependencies
59
59
  x, y, end_x, end_y = @perfect_shape_dependencies = perfect_shape_dependencies
60
- @perfect_shape = PerfectShape::Line.new(points: [[x, y], [end_x, end_y]])
60
+ @perfect_shape = PerfectShape::Line.new(points: [x, y, end_x, end_y].compact)
61
61
  end
62
62
  @perfect_shape
63
63
  end
@@ -0,0 +1,39 @@
1
+ # Copyright (c) 2007-2022 Andy Maleh
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining
4
+ # a copy of this software and associated documentation files (the
5
+ # "Software"), to deal in the Software without restriction, including
6
+ # without limitation the rights to use, copy, modify, merge, publish,
7
+ # distribute, sublicense, and/or sell copies of the Software, and to
8
+ # permit persons to whom the Software is furnished to do so, subject to
9
+ # the following conditions:
10
+ #
11
+ # The above copyright notice and this permission notice shall be
12
+ # included in all copies or substantial portions of the Software.
13
+ #
14
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21
+
22
+ require 'delegate'
23
+
24
+ module Glimmer
25
+ class ProcTracker < DelegateClass(Proc)
26
+ def initialize(proc)
27
+ super(proc)
28
+ end
29
+
30
+ def call(*args)
31
+ __getobj__.call(*args)
32
+ @called = true
33
+ end
34
+
35
+ def called?
36
+ !!@called
37
+ end
38
+ end
39
+ end