lacci 0.3.0 → 0.5.0

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 (48) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +0 -2
  3. data/Gemfile.lock +4 -32
  4. data/lib/lacci/scarpe_cli.rb +0 -1
  5. data/lib/lacci/version.rb +1 -1
  6. data/lib/scarpe/niente/app.rb +12 -1
  7. data/lib/scarpe/niente/display_service.rb +5 -1
  8. data/lib/scarpe/niente/drawable.rb +2 -0
  9. data/lib/scarpe/niente/shoes_spec.rb +10 -5
  10. data/lib/scarpe/niente.rb +15 -2
  11. data/lib/shoes/app.rb +204 -105
  12. data/lib/shoes/constants.rb +24 -2
  13. data/lib/shoes/display_service.rb +43 -4
  14. data/lib/shoes/drawable.rb +326 -36
  15. data/lib/shoes/drawables/arc.rb +4 -26
  16. data/lib/shoes/drawables/arrow.rb +3 -23
  17. data/lib/shoes/drawables/border.rb +28 -0
  18. data/lib/shoes/drawables/button.rb +5 -21
  19. data/lib/shoes/drawables/check.rb +7 -3
  20. data/lib/shoes/drawables/document_root.rb +4 -4
  21. data/lib/shoes/drawables/edit_box.rb +6 -5
  22. data/lib/shoes/drawables/edit_line.rb +5 -4
  23. data/lib/shoes/drawables/flow.rb +4 -6
  24. data/lib/shoes/drawables/font_helper.rb +62 -0
  25. data/lib/shoes/drawables/image.rb +2 -2
  26. data/lib/shoes/drawables/line.rb +3 -6
  27. data/lib/shoes/drawables/link.rb +16 -9
  28. data/lib/shoes/drawables/list_box.rb +8 -5
  29. data/lib/shoes/drawables/oval.rb +48 -0
  30. data/lib/shoes/drawables/para.rb +106 -18
  31. data/lib/shoes/drawables/progress.rb +2 -1
  32. data/lib/shoes/drawables/radio.rb +5 -3
  33. data/lib/shoes/drawables/rect.rb +7 -6
  34. data/lib/shoes/drawables/shape.rb +4 -3
  35. data/lib/shoes/drawables/slot.rb +102 -9
  36. data/lib/shoes/drawables/stack.rb +7 -12
  37. data/lib/shoes/drawables/star.rb +9 -31
  38. data/lib/shoes/drawables/text_drawable.rb +93 -34
  39. data/lib/shoes/drawables/video.rb +3 -2
  40. data/lib/shoes/drawables/widget.rb +9 -4
  41. data/lib/shoes/drawables.rb +2 -1
  42. data/lib/shoes/errors.rb +13 -3
  43. data/lib/shoes/margin_helper.rb +79 -0
  44. data/lib/shoes.rb +98 -20
  45. metadata +11 -15
  46. data/lib/scarpe/niente/logger.rb +0 -29
  47. data/lib/shoes/drawables/span.rb +0 -27
  48. data/lib/shoes/spacing.rb +0 -9
data/lib/shoes/app.rb CHANGED
@@ -4,66 +4,93 @@ class Shoes
4
4
  class App < Shoes::Drawable
5
5
  include Shoes::Log
6
6
 
7
- class << self
8
- attr_accessor :instance
9
- end
10
-
7
+ # The Shoes root of the drawable tree
11
8
  attr_reader :document_root
12
9
 
13
- shoes_styles :title, :width, :height, :resizable
10
+ # The application directory for this app. Often this will be the directory
11
+ # containing the launched application file.
12
+ attr_reader :dir
13
+
14
+ shoes_styles :title, :width, :height, :resizable, :features
15
+
16
+ # This is defined to avoid the linkable-id check in the Shoes-style method_missing def'n
17
+ attr_reader :features
14
18
 
15
- CUSTOM_EVENT_LOOP_TYPES = ["displaylib", "return", "wait"]
19
+ # These are the allowed values for custom_event_loop events.
20
+ #
21
+ # * displaylib means the display library is not going to return from running the app
22
+ # * return means the display library will return and the loop will be handled outside Lacci's control
23
+ # * wait means Lacci should busy-wait and send eternal heartbeats from the "run" event
24
+ #
25
+ # If the display service grabs control and keeps it, Webview-style, that means "displaylib"
26
+ # should be the value. A Scarpe-Wasm-style "return" is appropriate if the code can finish
27
+ # without Ruby ending the process at the end of the source file. A "wait" can prevent Ruby
28
+ # from finishing early, but also prevents multiple applications. Only "return" will normally
29
+ # allow multiple Shoes applications.
30
+ CUSTOM_EVENT_LOOP_TYPES = %w[displaylib return wait]
16
31
 
32
+ class << self
33
+ attr_accessor :set_test_code
34
+ end
35
+
36
+ init_args
17
37
  def initialize(
18
- title: "Shoes!",
38
+ title: 'Shoes!',
19
39
  width: 480,
20
40
  height: 420,
21
41
  resizable: true,
42
+ features: [],
22
43
  &app_code_body
23
44
  )
24
- log_init("Shoes::App")
45
+ log_init('Shoes::App')
25
46
 
26
- if Shoes::App.instance
27
- @log.error("Trying to create a second Shoes::App in the same process! Fail!")
28
- raise Shoes::Errors::TooManyInstancesError, "Cannot create multiple Shoes::App objects!"
47
+ if Shoes::FEATURES.include?(:multi_app) || Shoes.APPS.empty?
48
+ Shoes.APPS.push self
29
49
  else
30
- Shoes::App.instance = self
50
+ @log.error('Trying to create a second Shoes::App in the same process! Fail!')
51
+ raise Shoes::Errors::TooManyInstancesError, 'Cannot create multiple Shoes::App objects!'
31
52
  end
32
53
 
33
- @do_shutdown = false
34
- @event_loop_type = "displaylib" # the default
54
+ # We cd to the app's containing dir when running the app
55
+ @dir = Dir.pwd
35
56
 
36
- super
57
+ @do_shutdown = false
58
+ @event_loop_type = 'displaylib' # the default
37
59
 
38
- # The draw context tracks current settings like fill and stroke,
39
- # plus potentially other current state that changes from drawable
40
- # to drawable and slot to slot.
41
- @draw_context = {
42
- "fill" => "",
43
- "stroke" => "",
44
- "rotate" => 0,
45
- }
60
+ @features = features
46
61
 
47
- # This creates the DocumentRoot, including its corresponding display drawable
48
- @document_root = Shoes::DocumentRoot.new
62
+ unknown_ext = features - Shoes::FEATURES - Shoes::EXTENSIONS
63
+ unsupported_features = unknown_ext & Shoes::KNOWN_FEATURES
64
+ unless unsupported_features.empty?
65
+ @log.error("Shoes app requires feature(s) not supported by this display service: #{unsupported_features.inspect}!")
66
+ raise Shoes::Errors::UnsupportedFeatureError, "Shoes app needs features: #{unsupported_features.inspect}"
67
+ end
68
+ unless unknown_ext.empty?
69
+ @log.warn("Shoes app requested unknown features #{unknown_ext.inspect}! Known: #{(Shoes::FEATURES + Shoes::EXTENSIONS).inspect}")
70
+ end
49
71
 
50
72
  @slots = []
51
73
 
74
+ @content_container = nil
75
+
76
+ @routes = {}
77
+
78
+ super
79
+
80
+ # This creates the DocumentRoot, including its corresponding display drawable
81
+ Drawable.with_current_app(self) do
82
+ @document_root = Shoes::DocumentRoot.new
83
+ end
84
+
52
85
  # Now create the App display drawable
53
86
  create_display_drawable
54
87
 
55
- # Set up testing events *after* Display Service basic objects exist
56
- if ENV["SCARPE_APP_TEST"]
57
- test_code = File.read ENV["SCARPE_APP_TEST"]
58
- if test_code != ""
59
- @test_obj = Object.new
60
- @test_obj.instance_eval test_code
61
- end
62
- end
88
+ # Set up testing *after* Display Service basic objects exist
63
89
 
64
- if ENV["SHOES_SPEC_TEST"]
65
- test_code = File.read ENV["SHOES_SPEC_TEST"]
90
+ if ENV['SHOES_SPEC_TEST'] && !Shoes::App.set_test_code
91
+ test_code = File.read ENV['SHOES_SPEC_TEST']
66
92
  unless test_code.empty?
93
+ Shoes::App.set_test_code = true
67
94
  Shoes::Spec.instance.run_shoes_spec_test_code test_code
68
95
  end
69
96
  end
@@ -72,30 +99,34 @@ class Shoes
72
99
 
73
100
  # Try to de-dup as much as possible and not send repeat or multiple
74
101
  # destroy events
75
- @watch_for_destroy = bind_shoes_event(event_name: "destroy") do
102
+ @watch_for_destroy = bind_shoes_event(event_name: 'destroy') do
76
103
  Shoes::DisplayService.unsub_from_events(@watch_for_destroy) if @watch_for_destroy
77
104
  @watch_for_destroy = nil
78
- self.destroy(send_event: false)
105
+ destroy(send_event: false)
79
106
  end
80
107
 
81
- @watch_for_event_loop = bind_shoes_event(event_name: "custom_event_loop") do |loop_type|
82
- raise(Shoes::Errors::InvalidAttributeValueError, "Unknown event loop type: #{loop_type.inspect}!") unless CUSTOM_EVENT_LOOP_TYPES.include?(loop_type)
108
+ @watch_for_event_loop = bind_shoes_event(event_name: 'custom_event_loop') do |loop_type|
109
+ unless CUSTOM_EVENT_LOOP_TYPES.include?(loop_type)
110
+ raise(Shoes::Errors::InvalidAttributeValueError,
111
+ "Unknown event loop type: #{loop_type.inspect}!")
112
+ end
83
113
 
84
114
  @event_loop_type = loop_type
85
115
  end
86
116
 
87
- Signal.trap("INT") do
88
- @log.warn("App interrupted by signal, stopping...")
117
+ Signal.trap('INT') do
118
+ @log.warn('App interrupted by signal, stopping...')
89
119
  puts "\nStopping Shoes app..."
90
120
  destroy
91
121
  end
92
122
  end
93
123
 
94
124
  def init
95
- send_shoes_event(event_name: "init")
125
+ send_shoes_event(event_name: 'init')
96
126
  return if @do_shutdown
97
127
 
98
- ::Shoes::App.instance.with_slot(@document_root, &@app_code_body)
128
+ with_slot(@document_root, &@app_code_body)
129
+ render_index_if_defined_on_first_boot
99
130
  end
100
131
 
101
132
  # "Container" drawables like flows, stacks, masks and the document root
@@ -119,7 +150,7 @@ class Shoes
119
150
  return unless block_given?
120
151
 
121
152
  push_slot(slot_item)
122
- Shoes::App.instance.instance_eval(&block)
153
+ instance_eval(&block)
123
154
  ensure
124
155
  pop_slot
125
156
  end
@@ -133,22 +164,19 @@ class Shoes
133
164
  return super unless klass
134
165
 
135
166
  ::Shoes::App.define_method(name) do |*args, **kwargs, &block|
136
- # Look up the Shoes drawable and create it...
137
- drawable_instance = klass.new(*args, **kwargs, &block)
138
-
139
- unless klass.ancestors.include?(::Shoes::TextDrawable)
140
- # Create this drawable in the current app slot
141
- drawable_instance.set_parent ::Shoes::App.instance.current_slot
167
+ Drawable.with_current_app(self) do
168
+ klass.new(*args, **kwargs, &block)
142
169
  end
143
-
144
- drawable_instance
145
170
  end
146
171
 
147
172
  send(name, *args, **kwargs, &block)
148
173
  end
149
174
 
175
+ # Get the current draw context for the current slot
176
+ #
177
+ # @return [Hash] a hash of Shoes styles for the current draw context
150
178
  def current_draw_context
151
- @draw_context.dup
179
+ current_slot&.current_draw_context
152
180
  end
153
181
 
154
182
  # This usually doesn't return. The display service may take control
@@ -157,40 +185,39 @@ class Shoes
157
185
  # want to (and/or can't) take control of the event loop.
158
186
  def run
159
187
  if @do_shutdown
160
- $stderr.puts "Destroy has already been signaled, but we just called Shoes::App.run!"
188
+ warn 'Destroy has already been signaled, but we just called Shoes::App.run!'
161
189
  return
162
190
  end
163
191
 
164
192
  # The display lib can send us an event to customise the event loop handling.
165
193
  # But it must do so before the "run" event returns.
166
- send_shoes_event(event_name: "run")
194
+ send_shoes_event(event_name: 'run')
167
195
 
168
196
  case @event_loop_type
169
- when "wait"
197
+ when 'wait'
170
198
  # Display lib wants us to busy-wait instead of it.
171
- until @do_shutdown
172
- Shoes::DisplayService.dispatch_event("heartbeat", nil)
173
- end
174
- when "displaylib"
199
+ Shoes::DisplayService.dispatch_event('heartbeat', nil) until @do_shutdown
200
+ when 'displaylib'
175
201
  # If run event returned, that means we're done.
176
202
  destroy
177
- when "return"
203
+ when 'return'
178
204
  # We can just return to the main event loop. But we shouldn't call destroy.
179
205
  # Presumably some event loop *outside* our event loop is handling things.
180
206
  else
181
- raise Shoes::Errors::InvalidAttributeValueError, "Internal error! Incorrect event loop type: #{@event_loop_type.inspect}!"
207
+ raise Shoes::Errors::InvalidAttributeValueError,
208
+ "Internal error! Incorrect event loop type: #{@event_loop_type.inspect}!"
182
209
  end
183
210
  end
184
211
 
185
212
  def destroy(send_event: true)
186
213
  @do_shutdown = true
187
- send_shoes_event(event_name: "destroy") if send_event
214
+ send_shoes_event(event_name: 'destroy') if send_event
188
215
  end
189
216
 
190
217
  def all_drawables
191
218
  out = []
192
219
 
193
- to_add = @document_root.children
220
+ to_add = [@document_root, @document_root.children]
194
221
  until to_add.empty?
195
222
  out.concat(to_add)
196
223
  to_add = to_add.flat_map { |w| w.respond_to?(:children) ? w.children : [] }.compact
@@ -199,40 +226,52 @@ class Shoes
199
226
  out
200
227
  end
201
228
 
229
+ # We can add various ways to find drawables here.
230
+ # These are sort of like Shoes selectors, used for testing.
231
+ # This method finds a drawable across all active Shoes apps.
232
+ def self.find_drawables_by(*specs)
233
+ Shoes.APPS.flat_map do |app|
234
+ app.find_drawables_by(*specs)
235
+ end
236
+ end
237
+
202
238
  # We can add various ways to find drawables here.
203
239
  # These are sort of like Shoes selectors, used for testing.
204
240
  def find_drawables_by(*specs)
205
241
  drawables = all_drawables
206
242
  specs.each do |spec|
207
243
  if spec == Shoes::App
208
- drawables = [Shoes::App.instance]
244
+ drawables = [@app]
209
245
  elsif spec.is_a?(Class)
210
246
  drawables.select! { |w| spec === w }
211
247
  elsif spec.is_a?(Symbol) || spec.is_a?(String)
212
248
  s = spec.to_s
213
249
  case s[0]
214
- when "$"
250
+ when '$'
215
251
  begin
216
252
  # I'm not finding a global_variable_get or similar...
217
253
  global_value = eval s
218
254
  drawables &= [global_value]
219
255
  rescue
220
- raise Shoes::Errors::InvalidAttributeValueError, "Error getting global variable: #{spec.inspect}"
256
+ # raise Shoes::Errors::InvalidAttributeValueError, "Error getting global variable: #{spec.inspect}"
257
+ drawables = []
221
258
  end
222
- when "@"
223
- if Shoes::App.instance.instance_variables.include?(spec.to_sym)
224
- drawables &= [self.instance_variable_get(spec)]
259
+ when '@'
260
+ if @app.instance_variables.include?(spec.to_sym)
261
+ drawables &= [@app.instance_variable_get(spec)]
225
262
  else
226
- raise Shoes::Errors::InvalidAttributeValueError, "Can't find top-level instance variable: #{spec.inspect}!"
263
+ # raise Shoes::Errors::InvalidAttributeValueError, "Can't find top-level instance variable: #{spec.inspect}!"
264
+ drawables = []
227
265
  end
228
266
  else
229
- if s.start_with?("id:")
230
- find_id = Integer(s[3..-1])
231
- drawable = Shoes::Drawable.drawable_by_id(find_id)
232
- drawables &= [drawable]
233
- else
267
+ unless s.start_with?('id:')
234
268
  raise Shoes::Errors::InvalidAttributeValueError, "Don't know how to find drawables by #{spec.inspect}!"
235
269
  end
270
+
271
+ find_id = Integer(s[3..-1])
272
+ drawable = Shoes::Drawable.drawable_by_id(find_id)
273
+ drawables &= [drawable]
274
+
236
275
  end
237
276
  else
238
277
  raise(Shoes::Errors::InvalidAttributeValueError, "Don't know how to find drawables by #{spec.inspect}!")
@@ -240,11 +279,67 @@ class Shoes
240
279
  end
241
280
  drawables
242
281
  end
282
+
283
+ def page(name, &block)
284
+ @pages ||= {}
285
+ @pages[name] = proc do
286
+ stack(width: 1.0, height: 1.0) do
287
+ instance_eval(&block)
288
+ end
289
+ end
290
+ end
291
+
292
+ def visit(name_or_path)
293
+ # First, check for exact page match (symbol)
294
+ if @pages && @pages[name_or_path]
295
+ @document_root.clear do
296
+ instance_eval(&@pages[name_or_path])
297
+ end
298
+ return
299
+ end
300
+
301
+ # Second, check URL routes
302
+ route, method_name = @routes.find { |r, _| r === name_or_path }
303
+ if route
304
+ @document_root.clear do
305
+ if route.is_a?(Regexp)
306
+ match_data = route.match(name_or_path)
307
+ send(method_name, *match_data.captures)
308
+ else
309
+ send(method_name)
310
+ end
311
+ end
312
+ return
313
+ end
314
+
315
+ # Third, if it's a string path like "/page2", try matching page :page2
316
+ if name_or_path.is_a?(String) && name_or_path.start_with?("/")
317
+ page_name = name_or_path[1..-1].to_sym # "/page2" -> :page2
318
+ if @pages && @pages[page_name]
319
+ @document_root.clear do
320
+ instance_eval(&@pages[page_name])
321
+ end
322
+ return
323
+ end
324
+ end
325
+
326
+ puts "Error: URL '#{name_or_path}' not found"
327
+ end
328
+
329
+ def url(path, method_name)
330
+ if path.is_a?(String) && path.include?('(')
331
+ # Convert string patterns to regex
332
+ regex = Regexp.new("^#{path.gsub(/\(.*?\)/, '(.*?)')}$")
333
+ @routes[regex] = method_name
334
+ else
335
+ @routes[path] = method_name
336
+ end
337
+ end
243
338
  end
244
339
  end
245
340
 
246
341
  # Event handler DSLs get defined in both App and Slot - same code, slightly different results
247
- events = [:motion, :hover, :leave, :click, :release, :keypress, :animate, :every, :timer]
342
+ events = %i[motion hover leave click release keypress animate every timer]
248
343
  events.each do |event|
249
344
  Shoes::App.define_method(event) do |*args, &block|
250
345
  subscription_item(args:, shoes_api_name: event.to_s, &block)
@@ -256,54 +351,58 @@ end
256
351
 
257
352
  # These methods will need to be defined on Slots too, but probably need a rework in general.
258
353
  class Shoes::App < Shoes::Drawable
354
+ # This is going to go away. See issue #496
259
355
  def background(...)
260
356
  current_slot.background(...)
261
357
  end
262
358
 
359
+ # This is going to go away. See issue #498
263
360
  def border(...)
264
361
  current_slot.border(...)
265
362
  end
266
363
 
267
- # Draw context methods
268
-
269
- def fill(color)
270
- @draw_context["fill"] = color
271
- end
272
-
273
- def nofill
274
- @draw_context["fill"] = ""
275
- end
276
-
277
- def stroke(color)
278
- @draw_context["stroke"] = color
279
- end
280
-
281
- def nostroke
282
- @draw_context["stroke"] = ""
364
+ # Draw Context methods -- forward to the current slot
365
+ %i[fill nofill stroke strokewidth nostroke rotate].each do |dc_method|
366
+ define_method(dc_method) do |*args|
367
+ current_slot.send(dc_method, *args)
368
+ end
283
369
  end
284
370
 
285
371
  # Shape DSL methods
286
372
 
287
373
  def move_to(x, y)
288
- raise(Shoes::Errors::InvalidAttributeValueError, "Pass only Numeric arguments to move_to!") unless x.is_a?(Numeric) && y.is_a?(Numeric)
289
-
290
- if current_slot.is_a?(::Shoes::Shape)
291
- current_slot.add_shape_command(["move_to", x, y])
374
+ unless x.is_a?(Numeric) && y.is_a?(Numeric)
375
+ raise(Shoes::Errors::InvalidAttributeValueError,
376
+ 'Pass only Numeric arguments to move_to!')
292
377
  end
378
+
379
+ return unless current_slot.is_a?(::Shoes::Shape)
380
+
381
+ current_slot.add_shape_command(['move_to', x, y])
293
382
  end
294
383
 
295
384
  def line_to(x, y)
296
- raise(Shoes::Errors::InvalidAttributeValueError, "Pass only Numeric arguments to line_to!") unless x.is_a?(Numeric) && y.is_a?(Numeric)
297
-
298
- if current_slot.is_a?(::Shoes::Shape)
299
- current_slot.add_shape_command(["line_to", x, y])
385
+ unless x.is_a?(Numeric) && y.is_a?(Numeric)
386
+ raise(Shoes::Errors::InvalidAttributeValueError,
387
+ 'Pass only Numeric arguments to line_to!')
300
388
  end
301
- end
302
389
 
303
- def rotate(angle)
304
- @draw_context["rotate"] = angle
390
+ return unless current_slot.is_a?(::Shoes::Shape)
391
+
392
+ current_slot.add_shape_command(['line_to', x, y])
305
393
  end
394
+
306
395
  # Not implemented yet: curve_to, arc_to
307
396
 
308
- alias_method :info, :puts
397
+ alias info puts
398
+
399
+ private
400
+
401
+ def render_index_if_defined_on_first_boot
402
+ return if @first_boot_finished
403
+
404
+ visit('/') if @routes['/'] == :index
405
+
406
+ @first_boot_finished = true
407
+ end
309
408
  end
@@ -13,10 +13,10 @@ class Shoes
13
13
  [ENV["LOCALAPPDATA"], "Shoes"],
14
14
  [ENV["APPDATA"], "Shoes"],
15
15
  [ENV["HOME"], ".shoes"],
16
- [Dir.tmpdir, "shoes"],
17
16
  ]
18
17
 
19
18
  top, file = homes.detect { |home_top, _| home_top && File.exist?(home_top) }
19
+ return nil if top.nil?
20
20
  File.join(top, file)
21
21
  end
22
22
 
@@ -32,8 +32,30 @@ class Shoes
32
32
  HALF_PI = 1.57079632679489661923
33
33
  PI = 3.14159265358979323846
34
34
 
35
- # This should be set up by the Display Service when it loads
35
+ # These should be set up by the Display Service when it loads. They are intentionally
36
+ # *not* frozen so that the Display Service can add to them (and then optionally
37
+ # freeze them.)
38
+
39
+ # Fonts currently loaded and available
36
40
  FONTS = []
41
+
42
+ # Standard features available in this display service - see KNOWN_FEATURES.
43
+ # These may or may not require the Shoes.app requesting them per-app.
44
+ FEATURES = []
45
+
46
+ # Nonstandard extensions, e.g. Scarpe extensions, supported by this display lib.
47
+ # An application may have to request the extensions for them to be available so
48
+ # that a casual reader can see Shoes.app(features: :scarpe) and realize why
49
+ # there are nonstandard styles or drawables.
50
+ EXTENSIONS = []
51
+
52
+ # These are all known features supported by this version of Lacci.
53
+ # Features on this list are allowed to be in FEATURES. Anything else
54
+ # goes in EXTENSIONS and is nonstandard.
55
+ KNOWN_FEATURES = [
56
+ :html, # Supports .to_html on display objects, HTML classes on drawables, etc.
57
+ :multi_app, # Supports multiple applications at once
58
+ ].freeze
37
59
  end
38
60
 
39
61
  # Access and assign the release constants
@@ -32,7 +32,14 @@ class Shoes
32
32
  # This is in the eigenclass/metaclass, *not* instances of DisplayService
33
33
  include Shoes::Log
34
34
 
35
+ # Send a Shoes event to all subscribers.
35
36
  # An event_target may be nil, to indicate there is no target.
37
+ #
38
+ # @param event_name [String] the name of the event
39
+ # @param event_target [String] the specific target, if any
40
+ # @param args [Array] arguments to pass to the subscribing block
41
+ # @param args [Array] keyword arguments to pass to the subscribing block
42
+ # @return [void]
36
43
  def dispatch_event(event_name, event_target, *args, **kwargs)
37
44
  @@display_event_handlers ||= {}
38
45
 
@@ -69,10 +76,20 @@ class Shoes
69
76
  kwargs[:event_name] = event_name
70
77
  kwargs[:event_target] = event_target if event_target
71
78
  handlers.each { |h| h[:handler].call(*args, **kwargs) }
79
+ nil
72
80
  end
73
81
 
74
- # It's permitted to subscribe to event_name :any for all event names, and event_target :any for all targets.
75
- # An event_target of nil means "no target", and only matches events dispatched with a nil target.
82
+ # Subscribe to the given event name and target.
83
+ # It's permitted to subscribe to event_name :any for all event names,
84
+ # and event_target :any for all targets. An event_target of nil means
85
+ # "no target", and only matches events dispatched with a nil target.
86
+ # The subscription will return an unsubscribe ID, which can be used
87
+ # later to unsubscribe from the notification.
88
+ #
89
+ # @param event_name [String,Symbol] the event name to subscribe to, or :any for all event names
90
+ # @param event_target [String,Symbol,NilClass] the event target to subscribe to, or :any for all targets - nil is a valid target
91
+ # @block the block to call when the event occurs - it will receive arguments from the event-dispatch call
92
+ # @return [Integer] an unsubscription ID which can be used later to cancel the subscription
76
93
  def subscribe_to_event(event_name, event_target, &handler)
77
94
  @@display_event_handlers ||= {}
78
95
  @@display_event_unsub_id ||= 0
@@ -96,6 +113,10 @@ class Shoes
96
113
  id
97
114
  end
98
115
 
116
+ # Unsubscribe from any event subscriptions matching the unsub ID.
117
+ #
118
+ # @param unsub_id [Integer] the unsub ID returned when subscribing
119
+ # @return [void]
99
120
  def unsub_from_events(unsub_id)
100
121
  raise "Must provide an unsubscribe ID!" if unsub_id.nil?
101
122
 
@@ -106,17 +127,32 @@ class Shoes
106
127
  end
107
128
  end
108
129
 
130
+ # Reset the display service, for instance between unit tests.
131
+ # This destroys all existing subscriptions.
132
+ #
133
+ # @return [void]
109
134
  def full_reset!
110
135
  @@display_event_handlers = {}
111
136
  @json_debug_serialize = nil
112
137
  end
113
138
 
139
+ # Set the Display Service class which will handle display service functions
140
+ # for this process. This can only be set once. The display service can be
141
+ # a subclass of Shoes::DisplayService, but isn't required to be.
142
+ #
143
+ # Shoes will create an instance of this class with no arguments passed to
144
+ # initialize, and use it as the display service for the lifetime of the
145
+ # process.
146
+ #
147
+ # @param klass [Class] the class for the display service
114
148
  def set_display_service_class(klass)
115
149
  raise "Can only set a single display service class!" if @display_service_klass
116
150
 
117
151
  @display_service_klass = klass
118
152
  end
119
153
 
154
+ # Get the current display service instance. This requires a display service
155
+ # class having been set first. @see set_display_service_class
120
156
  def display_service
121
157
  return @service if @service
122
158
 
@@ -126,9 +162,13 @@ class Shoes
126
162
  end
127
163
  end
128
164
 
165
+ def initialize
166
+ @display_drawable_for = {}
167
+ end
168
+
129
169
  # These methods are an interface to DisplayService objects.
130
170
 
131
- def create_display_drawable_for(drawable_class_name, drawable_id, properties, is_widget:)
171
+ def create_display_drawable_for(drawable_class_name, drawable_id, properties, parent_id:, is_widget:)
132
172
  raise "Override in DisplayService implementation!"
133
173
  end
134
174
 
@@ -174,7 +214,6 @@ class Shoes
174
214
  def initialize(linkable_id: object_id)
175
215
  @linkable_id = linkable_id
176
216
  @subscriptions = {}
177
- @display_drawable_for ||= {}
178
217
  end
179
218
 
180
219
  def send_self_event(*args, event_name:, **kwargs)