glimmer-dsl-libui 0.5.3 → 0.5.6

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