lacci 0.4.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3bf9fa7d8c25eece259eb65e02ac680c3004762fa86951597ae1721010a8af52
4
- data.tar.gz: b997f33fc1f63480f38144f89c54af16a3f99c7eafaf960b53770ea4894b51e6
3
+ metadata.gz: e4b8a6a22600196c15c5ed8abbb7710bcc2d24fd2842aaa9190af738ef1e1802
4
+ data.tar.gz: 9bc49da62c2eea40133ca5d0cb01a3af7bec21d0d605478da5c235ce4a34c7a4
5
5
  SHA512:
6
- metadata.gz: 7eb784a865dfd9add9ade9861a62cfa2192e407d1371e49cdd23eb6205c55c36f67d6a31347775ee6b3d43c6e025ea1788e6c51dedf4251a562dcb62f2cda1e5
7
- data.tar.gz: ea5b4aef895ee41ea78699bac827f8404ebbef97f0d7e8ed93b41382076c3188538e70aa32016b5ff90bd81b05feca8effc977f5452efe30744f25a7d3fe845f
6
+ metadata.gz: 77de9f93d6495c6d0092eaf39bc36d46492a4f32725c3d5b2eb2dc07992cb31c21a10489eea8e77d29144dbbdd1daad8b12b8ccfb42bee1124579a8c0c34dbbb
7
+ data.tar.gz: fa322933a6da6fd7f831ac8490ec5b519f4e93f740fb35dcddf973562abfd81efa7f5cc9c89c4dd4d23f33dbdc35b59b90f9c217708d0c3e168d0658419ac85e
data/Gemfile CHANGED
@@ -17,8 +17,6 @@ group :development do
17
17
  gem "yard"
18
18
  gem "redcarpet"
19
19
  gem "debug"
20
- gem "rubocop", "~> 1.21"
21
- gem "rubocop-shopify"
22
20
  #gem "commonmarker"
23
21
  #gem "github-markup"
24
22
  end
data/Gemfile.lock CHANGED
@@ -1,19 +1,18 @@
1
1
  PATH
2
2
  remote: ../scarpe-components
3
3
  specs:
4
- scarpe-components (0.3.0)
4
+ scarpe-components (0.4.0)
5
5
 
6
6
  PATH
7
7
  remote: .
8
8
  specs:
9
- lacci (0.3.0)
10
- scarpe-components
9
+ lacci (0.4.0)
10
+ scarpe-components (~> 0.4.0)
11
11
 
12
12
  GEM
13
13
  remote: https://rubygems.org/
14
14
  specs:
15
15
  ansi (1.5.0)
16
- ast (2.4.2)
17
16
  builder (3.2.4)
18
17
  debug (1.8.0)
19
18
  irb (>= 1.5.0)
@@ -21,46 +20,21 @@ GEM
21
20
  io-console (0.6.0)
22
21
  irb (1.7.2)
23
22
  reline (>= 0.3.6)
24
- json (2.6.3)
25
- language_server-protocol (3.17.0.3)
26
23
  minitest (5.18.1)
27
24
  minitest-reporters (1.6.0)
28
25
  ansi
29
26
  builder
30
27
  minitest (>= 5.0)
31
28
  ruby-progressbar
32
- parallel (1.23.0)
33
- parser (3.2.2.3)
34
- ast (~> 2.4.1)
35
- racc
36
- racc (1.7.1)
37
- rainbow (3.1.1)
38
29
  rake (13.0.6)
39
30
  redcarpet (3.6.0)
40
- regexp_parser (2.8.1)
41
31
  reline (0.3.6)
42
32
  io-console (~> 0.5)
43
- rexml (3.2.5)
44
- rubocop (1.54.1)
45
- json (~> 2.3)
46
- language_server-protocol (>= 3.17.0)
47
- parallel (~> 1.10)
48
- parser (>= 3.2.2.3)
49
- rainbow (>= 2.2.2, < 4.0)
50
- regexp_parser (>= 1.8, < 3.0)
51
- rexml (>= 3.2.5, < 4.0)
52
- rubocop-ast (>= 1.28.0, < 2.0)
53
- ruby-progressbar (~> 1.7)
54
- unicode-display_width (>= 2.4.0, < 3.0)
55
- rubocop-ast (1.29.0)
56
- parser (>= 3.2.1.0)
57
- rubocop-shopify (2.14.0)
58
- rubocop (~> 1.51)
59
33
  ruby-progressbar (1.13.0)
60
- unicode-display_width (2.4.2)
61
34
  yard (0.9.34)
62
35
 
63
36
  PLATFORMS
37
+ arm64-darwin-21
64
38
  x86_64-darwin-22
65
39
 
66
40
  DEPENDENCIES
@@ -70,8 +44,6 @@ DEPENDENCIES
70
44
  minitest-reporters
71
45
  rake (~> 13.0)
72
46
  redcarpet
73
- rubocop (~> 1.21)
74
- rubocop-shopify
75
47
  scarpe-components!
76
48
  yard
77
49
 
data/lib/lacci/version.rb CHANGED
@@ -9,5 +9,5 @@
9
9
  # mostly invisible. Instead, look at the {Shoes} module
10
10
  # to see what's in Lacci.
11
11
  module Lacci
12
- VERSION = "0.4.0"
12
+ VERSION = "0.5.0"
13
13
  end
@@ -14,7 +14,18 @@ module Niente
14
14
  end
15
15
 
16
16
  def run
17
- send_shoes_event("wait", event_name: "custom_event_loop")
17
+ send_shoes_event("return", event_name: "custom_event_loop")
18
+
19
+ @do_shutdown = false
20
+ bind_shoes_event(event_name: "destroy") do
21
+ @do_shutdown = true
22
+ end
23
+
24
+ at_exit do
25
+ until @do_shutdown
26
+ Shoes::DisplayService.dispatch_event("heartbeat", nil)
27
+ end
28
+ end
18
29
  end
19
30
 
20
31
  def destroy
@@ -8,7 +8,7 @@ module Niente; end
8
8
  class Niente::Test
9
9
  def self.run_shoes_spec_test_code(code, class_name: nil, test_name: nil)
10
10
  if @shoes_spec_init
11
- raise Shoes::Errors::MultipleShoesSpecRunsError, "Scarpe-Webview can only run a single Shoes spec per process!"
11
+ raise Shoes::Errors::MultipleShoesSpecRunsError, "Niente can only run a single Shoes spec per process!"
12
12
  end
13
13
  @shoes_spec_init = true
14
14
 
@@ -21,7 +21,7 @@ class Niente::Test
21
21
  Shoes::DisplayService.subscribe_to_event("heartbeat", nil) do
22
22
  unless @hb_init
23
23
  Minitest.run []
24
- Shoes::App.instance.destroy
24
+ Shoes.APPS.each(&:destroy)
25
25
  end
26
26
  @hb_init = true
27
27
  end
@@ -40,9 +40,8 @@ class Niente::ShoesSpecTest < Minitest::Test
40
40
  finder_name = drawable_class.dsl_name
41
41
 
42
42
  define_method(finder_name) do |*args|
43
- app = Shoes::App.instance
43
+ drawables = Shoes::App.find_drawables_by(drawable_class, *args)
44
44
 
45
- drawables = app.find_drawables_by(drawable_class, *args)
46
45
  raise Shoes::Errors::MultipleDrawablesFoundError, "Found more than one #{finder_name} matching #{args.inspect}!" if drawables.size > 1
47
46
  raise Shoes::Errors::NoDrawablesFoundError, "Found no #{finder_name} matching #{args.inspect}!" if drawables.empty?
48
47
 
@@ -51,7 +50,7 @@ class Niente::ShoesSpecTest < Minitest::Test
51
50
  end
52
51
 
53
52
  def drawable(*specs)
54
- drawables = Shoes::App.instance.find_drawables_by(*specs)
53
+ drawables = Shoes::App.find_drawables_by(*specs)
55
54
  raise Shoes::Errors::MultipleDrawablesFoundError, "Found more than one #{finder_name} matching #{args.inspect}!" if drawables.size > 1
56
55
  raise Shoes::Errors::NoDrawablesFoundError, "Found no #{finder_name} matching #{args.inspect}!" if drawables.empty?
57
56
  Niente::ShoesSpecProxy.new(drawables[0])
data/lib/scarpe/niente.rb CHANGED
@@ -30,3 +30,4 @@ Shoes.add_file_loader loader
30
30
 
31
31
  Shoes::DisplayService.set_display_service_class(Niente::DisplayService)
32
32
 
33
+ Shoes::FEATURES.push(:multi_app)
data/lib/shoes/app.rb CHANGED
@@ -4,10 +4,6 @@ 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
-
11
7
  # The Shoes root of the drawable tree
12
8
  attr_reader :document_root
13
9
 
@@ -18,35 +14,48 @@ class Shoes
18
14
  shoes_styles :title, :width, :height, :resizable, :features
19
15
 
20
16
  # This is defined to avoid the linkable-id check in the Shoes-style method_missing def'n
21
- def features
22
- @features
23
- end
17
+ attr_reader :features
24
18
 
25
- 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]
31
+
32
+ class << self
33
+ attr_accessor :set_test_code
34
+ end
26
35
 
27
36
  init_args
28
37
  def initialize(
29
- title: "Shoes!",
38
+ title: 'Shoes!',
30
39
  width: 480,
31
40
  height: 420,
32
41
  resizable: true,
33
42
  features: [],
34
43
  &app_code_body
35
44
  )
36
- log_init("Shoes::App")
45
+ log_init('Shoes::App')
37
46
 
38
- if Shoes::App.instance
39
- @log.error("Trying to create a second Shoes::App in the same process! Fail!")
40
- 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
41
49
  else
42
- 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!'
43
52
  end
44
53
 
45
54
  # We cd to the app's containing dir when running the app
46
55
  @dir = Dir.pwd
47
56
 
48
57
  @do_shutdown = false
49
- @event_loop_type = "displaylib" # the default
58
+ @event_loop_type = 'displaylib' # the default
50
59
 
51
60
  @features = features
52
61
 
@@ -62,19 +71,26 @@ class Shoes
62
71
 
63
72
  @slots = []
64
73
 
74
+ @content_container = nil
75
+
76
+ @routes = {}
77
+
65
78
  super
66
79
 
67
80
  # This creates the DocumentRoot, including its corresponding display drawable
68
- @document_root = Shoes::DocumentRoot.new
81
+ Drawable.with_current_app(self) do
82
+ @document_root = Shoes::DocumentRoot.new
83
+ end
69
84
 
70
85
  # Now create the App display drawable
71
86
  create_display_drawable
72
87
 
73
88
  # Set up testing *after* Display Service basic objects exist
74
89
 
75
- if ENV["SHOES_SPEC_TEST"]
76
- 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']
77
92
  unless test_code.empty?
93
+ Shoes::App.set_test_code = true
78
94
  Shoes::Spec.instance.run_shoes_spec_test_code test_code
79
95
  end
80
96
  end
@@ -83,30 +99,34 @@ class Shoes
83
99
 
84
100
  # Try to de-dup as much as possible and not send repeat or multiple
85
101
  # destroy events
86
- @watch_for_destroy = bind_shoes_event(event_name: "destroy") do
102
+ @watch_for_destroy = bind_shoes_event(event_name: 'destroy') do
87
103
  Shoes::DisplayService.unsub_from_events(@watch_for_destroy) if @watch_for_destroy
88
104
  @watch_for_destroy = nil
89
- self.destroy(send_event: false)
105
+ destroy(send_event: false)
90
106
  end
91
107
 
92
- @watch_for_event_loop = bind_shoes_event(event_name: "custom_event_loop") do |loop_type|
93
- 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
94
113
 
95
114
  @event_loop_type = loop_type
96
115
  end
97
116
 
98
- Signal.trap("INT") do
99
- @log.warn("App interrupted by signal, stopping...")
117
+ Signal.trap('INT') do
118
+ @log.warn('App interrupted by signal, stopping...')
100
119
  puts "\nStopping Shoes app..."
101
120
  destroy
102
121
  end
103
122
  end
104
123
 
105
124
  def init
106
- send_shoes_event(event_name: "init")
125
+ send_shoes_event(event_name: 'init')
107
126
  return if @do_shutdown
108
127
 
109
- ::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
110
130
  end
111
131
 
112
132
  # "Container" drawables like flows, stacks, masks and the document root
@@ -130,7 +150,7 @@ class Shoes
130
150
  return unless block_given?
131
151
 
132
152
  push_slot(slot_item)
133
- Shoes::App.instance.instance_eval(&block)
153
+ instance_eval(&block)
134
154
  ensure
135
155
  pop_slot
136
156
  end
@@ -144,7 +164,9 @@ class Shoes
144
164
  return super unless klass
145
165
 
146
166
  ::Shoes::App.define_method(name) do |*args, **kwargs, &block|
147
- klass.new(*args, **kwargs, &block)
167
+ Drawable.with_current_app(self) do
168
+ klass.new(*args, **kwargs, &block)
169
+ end
148
170
  end
149
171
 
150
172
  send(name, *args, **kwargs, &block)
@@ -163,34 +185,33 @@ class Shoes
163
185
  # want to (and/or can't) take control of the event loop.
164
186
  def run
165
187
  if @do_shutdown
166
- $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!'
167
189
  return
168
190
  end
169
191
 
170
192
  # The display lib can send us an event to customise the event loop handling.
171
193
  # But it must do so before the "run" event returns.
172
- send_shoes_event(event_name: "run")
194
+ send_shoes_event(event_name: 'run')
173
195
 
174
196
  case @event_loop_type
175
- when "wait"
197
+ when 'wait'
176
198
  # Display lib wants us to busy-wait instead of it.
177
- until @do_shutdown
178
- Shoes::DisplayService.dispatch_event("heartbeat", nil)
179
- end
180
- when "displaylib"
199
+ Shoes::DisplayService.dispatch_event('heartbeat', nil) until @do_shutdown
200
+ when 'displaylib'
181
201
  # If run event returned, that means we're done.
182
202
  destroy
183
- when "return"
203
+ when 'return'
184
204
  # We can just return to the main event loop. But we shouldn't call destroy.
185
205
  # Presumably some event loop *outside* our event loop is handling things.
186
206
  else
187
- 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}!"
188
209
  end
189
210
  end
190
211
 
191
212
  def destroy(send_event: true)
192
213
  @do_shutdown = true
193
- send_shoes_event(event_name: "destroy") if send_event
214
+ send_shoes_event(event_name: 'destroy') if send_event
194
215
  end
195
216
 
196
217
  def all_drawables
@@ -205,40 +226,52 @@ class Shoes
205
226
  out
206
227
  end
207
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
+
208
238
  # We can add various ways to find drawables here.
209
239
  # These are sort of like Shoes selectors, used for testing.
210
240
  def find_drawables_by(*specs)
211
241
  drawables = all_drawables
212
242
  specs.each do |spec|
213
243
  if spec == Shoes::App
214
- drawables = [Shoes::App.instance]
244
+ drawables = [@app]
215
245
  elsif spec.is_a?(Class)
216
246
  drawables.select! { |w| spec === w }
217
247
  elsif spec.is_a?(Symbol) || spec.is_a?(String)
218
248
  s = spec.to_s
219
249
  case s[0]
220
- when "$"
250
+ when '$'
221
251
  begin
222
252
  # I'm not finding a global_variable_get or similar...
223
253
  global_value = eval s
224
254
  drawables &= [global_value]
225
255
  rescue
226
- raise Shoes::Errors::InvalidAttributeValueError, "Error getting global variable: #{spec.inspect}"
256
+ # raise Shoes::Errors::InvalidAttributeValueError, "Error getting global variable: #{spec.inspect}"
257
+ drawables = []
227
258
  end
228
- when "@"
229
- if Shoes::App.instance.instance_variables.include?(spec.to_sym)
230
- drawables &= [self.instance_variable_get(spec)]
259
+ when '@'
260
+ if @app.instance_variables.include?(spec.to_sym)
261
+ drawables &= [@app.instance_variable_get(spec)]
231
262
  else
232
- 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 = []
233
265
  end
234
266
  else
235
- if s.start_with?("id:")
236
- find_id = Integer(s[3..-1])
237
- drawable = Shoes::Drawable.drawable_by_id(find_id)
238
- drawables &= [drawable]
239
- else
267
+ unless s.start_with?('id:')
240
268
  raise Shoes::Errors::InvalidAttributeValueError, "Don't know how to find drawables by #{spec.inspect}!"
241
269
  end
270
+
271
+ find_id = Integer(s[3..-1])
272
+ drawable = Shoes::Drawable.drawable_by_id(find_id)
273
+ drawables &= [drawable]
274
+
242
275
  end
243
276
  else
244
277
  raise(Shoes::Errors::InvalidAttributeValueError, "Don't know how to find drawables by #{spec.inspect}!")
@@ -246,11 +279,67 @@ class Shoes
246
279
  end
247
280
  drawables
248
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
249
338
  end
250
339
  end
251
340
 
252
341
  # Event handler DSLs get defined in both App and Slot - same code, slightly different results
253
- events = [:motion, :hover, :leave, :click, :release, :keypress, :animate, :every, :timer]
342
+ events = %i[motion hover leave click release keypress animate every timer]
254
343
  events.each do |event|
255
344
  Shoes::App.define_method(event) do |*args, &block|
256
345
  subscription_item(args:, shoes_api_name: event.to_s, &block)
@@ -273,7 +362,7 @@ class Shoes::App < Shoes::Drawable
273
362
  end
274
363
 
275
364
  # Draw Context methods -- forward to the current slot
276
- [:fill, :nofill, :stroke, :strokewidth, :nostroke, :rotate].each do |dc_method|
365
+ %i[fill nofill stroke strokewidth nostroke rotate].each do |dc_method|
277
366
  define_method(dc_method) do |*args|
278
367
  current_slot.send(dc_method, *args)
279
368
  end
@@ -282,22 +371,38 @@ class Shoes::App < Shoes::Drawable
282
371
  # Shape DSL methods
283
372
 
284
373
  def move_to(x, y)
285
- raise(Shoes::Errors::InvalidAttributeValueError, "Pass only Numeric arguments to move_to!") unless x.is_a?(Numeric) && y.is_a?(Numeric)
286
-
287
- if current_slot.is_a?(::Shoes::Shape)
288
- 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!')
289
377
  end
378
+
379
+ return unless current_slot.is_a?(::Shoes::Shape)
380
+
381
+ current_slot.add_shape_command(['move_to', x, y])
290
382
  end
291
383
 
292
384
  def line_to(x, y)
293
- raise(Shoes::Errors::InvalidAttributeValueError, "Pass only Numeric arguments to line_to!") unless x.is_a?(Numeric) && y.is_a?(Numeric)
294
-
295
- if current_slot.is_a?(::Shoes::Shape)
296
- 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!')
297
388
  end
389
+
390
+ return unless current_slot.is_a?(::Shoes::Shape)
391
+
392
+ current_slot.add_shape_command(['line_to', x, y])
298
393
  end
299
394
 
300
395
  # Not implemented yet: curve_to, arc_to
301
396
 
302
- 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
303
408
  end
@@ -54,6 +54,7 @@ class Shoes
54
54
  # goes in EXTENSIONS and is nonstandard.
55
55
  KNOWN_FEATURES = [
56
56
  :html, # Supports .to_html on display objects, HTML classes on drawables, etc.
57
+ :multi_app, # Supports multiple applications at once
57
58
  ].freeze
58
59
  end
59
60
 
@@ -54,12 +54,12 @@ class Shoes
54
54
  def validate_as(prop_name, value)
55
55
  prop_name = prop_name.to_s
56
56
  hashes = shoes_style_hashes
57
-
57
+
58
58
  h = hashes.detect { |hash| hash[:name] == prop_name }
59
59
  raise(Shoes::Errors::NoSuchStyleError, "Can't find property #{prop_name.inspect} in #{self} property list: #{hashes.inspect}!") unless h
60
-
60
+
61
61
  return value if h[:validator].nil?
62
-
62
+
63
63
  # Pass both the property name and value to the validator block
64
64
  h[:validator].call(value,prop_name)
65
65
  end
@@ -215,7 +215,7 @@ class Shoes
215
215
  # styles available with no features requested, pass nil to with_features.
216
216
  def shoes_style_names(with_features: nil)
217
217
  # No with_features given? Use the ones requested by this Shoes::App
218
- with_features ||= Shoes::App.instance.features
218
+ with_features ||= @app.features
219
219
  parent_prop_names = self != Shoes::Drawable ? self.superclass.shoes_style_names(with_features:) : []
220
220
 
221
221
  if with_features == :all
@@ -236,6 +236,24 @@ class Shoes
236
236
  linkable_properties_hash[name.to_s] ||
237
237
  (self != Shoes::Drawable && superclass.shoes_style_name?(name))
238
238
  end
239
+
240
+ # Current_app is set every time a drawable is created - we don't want to keep a default
241
+ # long because it's possible for apps to alternate who is creating. So make sure it's
242
+ # not kept long, and used up when used once.
243
+
244
+ def with_current_app(app)
245
+ old_cur_app = @current_app
246
+ @current_app = app
247
+ ret = yield
248
+ @current_app = old_cur_app
249
+ ret
250
+ end
251
+
252
+ def use_current_app
253
+ cur_app = @current_app
254
+ @current_app = nil
255
+ cur_app
256
+ end
239
257
  end
240
258
 
241
259
  # Every Shoes drawable has positioning properties
@@ -256,17 +274,20 @@ class Shoes
256
274
  # Their value is set at drawable-create time.
257
275
  DRAW_CONTEXT_STYLES = [:fill, :stroke, :strokewidth, :rotate, :transform, :translate]
258
276
 
259
- include MarginHelper
277
+ include MarginHelper
260
278
 
261
279
  def initialize(*args, **kwargs)
262
280
  kwargs = margin_parse(kwargs)
263
281
  log_init("Shoes::#{self.class.name}") unless @log
264
282
 
283
+ # Grab the current app, mark it as used
284
+ @app = self.is_a?(Shoes::App) ? self : Drawable.use_current_app
285
+
265
286
  # First, get the list of allowed and disallowed styles for the given features
266
287
  # and make sure no disallowed styles were given.
267
288
 
268
- app_features = Shoes::App.instance.features
269
- this_app_styles = self.class.shoes_style_names.map(&:to_sym)
289
+ app_features = @app.features
290
+ this_app_styles = self.class.shoes_style_names(with_features: @app.features).map(&:to_sym)
270
291
  not_this_app_styles = self.class.shoes_style_names(with_features: :all).map(&:to_sym) - this_app_styles
271
292
 
272
293
  bad_styles = kwargs.keys & not_this_app_styles
@@ -309,8 +330,8 @@ class Shoes
309
330
  end
310
331
  end
311
332
 
312
- this_drawable_styles = self.class.shoes_style_names.map(&:to_sym)
313
- dc = Shoes::App.instance.current_draw_context || {}
333
+ this_drawable_styles = self.class.shoes_style_names(with_features: @app.features).map(&:to_sym)
334
+ dc = @app.current_draw_context || {}
314
335
 
315
336
  # Styles not passed as arguments can come from the draw context
316
337
 
@@ -318,11 +339,6 @@ class Shoes
318
339
  # given as positional or keyword arguments?
319
340
  draw_context_styles = (DRAW_CONTEXT_STYLES & this_drawable_styles) - supplied_args
320
341
  unless draw_context_styles.empty?
321
- # When we first call this, there is no parent. We don't want to set the parent
322
- # yet because that will send a notification, and *that* should wait until after
323
- # we've told the display service that this drawable was created. So instead
324
- # we'll query the parent object's draw context directly.
325
-
326
342
  draw_context_styles.each do |style|
327
343
  dc_val = dc[style.to_s]
328
344
  next if dc_val.nil?
@@ -363,7 +379,7 @@ class Shoes
363
379
 
364
380
  generate_debug_id
365
381
 
366
- parent = ::Shoes::App.instance.current_slot
382
+ parent = @app.current_slot
367
383
  if self.class.expects_parent?
368
384
  set_parent(parent, notify: false)
369
385
  end
@@ -416,8 +432,8 @@ class Shoes
416
432
  # @return [Shoes::App] the Shoes app
417
433
  # @yield the block to call with the Shoes App as self
418
434
  def app(&block)
419
- Shoes::App.instance.with_slot(self, &block) if block_given?
420
- Shoes::App.instance
435
+ @app.with_slot(self, &block) if block_given?
436
+ @app
421
437
  end
422
438
 
423
439
  private
@@ -471,8 +487,8 @@ class Shoes
471
487
  send_shoes_event(*args, **kwargs, event_name:, target: linkable_id)
472
488
  end
473
489
 
474
- def shoes_style_values
475
- all_property_names = self.class.shoes_style_names
490
+ def shoes_style_values(with_features: @app.features)
491
+ all_property_names = self.class.shoes_style_names(with_features:)
476
492
 
477
493
  properties = {}
478
494
  all_property_names.each do |prop|
@@ -15,10 +15,10 @@ class Shoes
15
15
 
16
16
  init_args :left, :top, :width, :height, :angle1, :angle2
17
17
  def initialize(*args, **kwargs)
18
- @draw_context = Shoes::App.instance.current_draw_context
19
-
20
18
  super
21
19
 
20
+ @draw_context = @app.current_draw_context
21
+
22
22
  create_display_drawable
23
23
  end
24
24
 
@@ -11,10 +11,10 @@ class Shoes
11
11
 
12
12
  init_args :left, :top, :width
13
13
  def initialize(*args, **kwargs)
14
- @draw_context = Shoes::App.instance.current_draw_context
15
-
16
14
  super
17
15
 
16
+ @draw_context = @app.current_draw_context
17
+
18
18
  create_display_drawable
19
19
  end
20
20
  end
@@ -17,7 +17,7 @@ class Shoes
17
17
  opt_init_args :stroke, :strokewidth, :curve
18
18
  def initialize(*args, **kwargs)
19
19
  super
20
- @draw_context = Shoes::App.instance.current_draw_context
20
+ @draw_context = @app.current_draw_context
21
21
 
22
22
  create_display_drawable
23
23
  end
@@ -41,7 +41,7 @@ class Shoes
41
41
  # Bind block to a handler named "click"
42
42
  bind_self_event("click") do
43
43
  @log.debug("Button clicked, calling handler") if @block
44
- @block&.call
44
+ @block&.call if @block
45
45
  end
46
46
 
47
47
  create_display_drawable
@@ -2,7 +2,7 @@
2
2
 
3
3
  class Shoes
4
4
  class EditLine < Shoes::Drawable
5
- shoes_styles :text, :width, :font, :tooltip, :stroke
5
+ shoes_styles :text, :width, :font, :tooltip, :stroke, :secret
6
6
  shoes_events :change
7
7
 
8
8
  init_args
@@ -14,7 +14,7 @@ class Shoes
14
14
  # Create the display-side drawable *before* instance_eval, which will add child drawables with their display drawables
15
15
  create_display_drawable
16
16
 
17
- Shoes::App.instance.with_slot(self, &block) if block_given?
17
+ @app.with_slot(self, &block) if block_given?
18
18
  end
19
19
  end
20
20
  end
@@ -7,10 +7,10 @@ class Shoes
7
7
 
8
8
  init_args :left, :top, :x2, :y2
9
9
  def initialize(*args, **kwargs)
10
- @draw_context = Shoes::App.instance.current_draw_context
11
-
12
10
  super
13
11
 
12
+ @draw_context = @app.current_draw_context
13
+
14
14
  create_display_drawable
15
15
  end
16
16
  end
@@ -10,12 +10,22 @@ class Shoes
10
10
  init_args # Empty by the time it reaches Drawable#initialize
11
11
  def initialize(*args, **kwargs, &block)
12
12
  @block = block
13
+
14
+ # Check if click is an internal route (starts with /)
15
+ click_value = kwargs[:click]
16
+ @internal_route = click_value.is_a?(String) && click_value.start_with?("/")
17
+
13
18
  # We can't send a block to the display drawable, but we can send a boolean
14
- @has_block = !block.nil?
19
+ # Also set has_block if we have an internal route (so display uses onclick, not href)
20
+ @has_block = !block.nil? || @internal_route
15
21
 
16
22
  super
17
23
 
18
24
  bind_self_event("click") do
25
+ if @internal_route
26
+ # Navigate to the internal route
27
+ app.visit(@click)
28
+ end
19
29
  @block&.call
20
30
  end
21
31
  end
@@ -18,10 +18,10 @@ class Shoes
18
18
  init_args :left, :top
19
19
  opt_init_args :radius, :height
20
20
  def initialize(*args, **options)
21
- @draw_context = Shoes::App.instance.current_draw_context
22
-
23
21
  super # Parse any positional or keyword args
24
22
 
23
+ @draw_context = @app.current_draw_context
24
+
25
25
  unless @left && @top && (@width || @height || @radius)
26
26
  raise Shoes::Errors::InvalidAttributeValueError, "Oval requires left, top and one of (width, height, radius) to be specified!"
27
27
  end
@@ -8,10 +8,10 @@ class Shoes
8
8
  init_args :left, :top, :width, :height
9
9
  opt_init_args :curve
10
10
  def initialize(*args, **kwargs)
11
- @draw_context = Shoes::App.instance.current_draw_context
12
-
13
11
  super
14
12
 
13
+ @draw_context = @app.current_draw_context
14
+
15
15
  create_display_drawable
16
16
  end
17
17
  end
@@ -16,12 +16,12 @@ class Shoes
16
16
  init_args # No positional args
17
17
  def initialize(**kwargs, &block)
18
18
  @shape_commands = []
19
- @draw_context = Shoes::App.instance.current_draw_context
20
19
 
21
20
  super
21
+ @draw_context = @app.current_draw_context
22
22
  create_display_drawable
23
23
 
24
- Shoes::App.instance.with_slot(self, &block) if block_given?
24
+ @app.with_slot(self, &block) if block_given?
25
25
  end
26
26
 
27
27
  # The cmd should be an array of the form:
@@ -62,8 +62,10 @@ class Shoes::Slot < Shoes::Drawable
62
62
  # Look up the Shoes drawable and create it. But first set
63
63
  # this slot as the current one so that draw context
64
64
  # is handled properly.
65
- Shoes::App.instance.with_slot(self) do
66
- instance = klass.new(*args, **kwargs, &block)
65
+ @app.with_slot(self) do
66
+ Shoes::Drawable.with_current_app(self.app) do
67
+ instance = klass.new(*args, **kwargs, &block)
68
+ end
67
69
  end
68
70
 
69
71
  instance
@@ -173,6 +175,6 @@ class Shoes::Slot < Shoes::Drawable
173
175
  raise(Shoes::Errors::InvalidAttributeValueError, "append requires a block!") unless block_given?
174
176
  raise(Shoes::Errors::InvalidAttributeValueError, "Don't append to something that isn't a slot!") unless self.is_a?(Shoes::Slot)
175
177
 
176
- Shoes::App.instance.with_slot(self, &block)
178
+ @app.with_slot(self, &block)
177
179
  end
178
180
  end
@@ -15,7 +15,7 @@ class Shoes
15
15
 
16
16
  # Create the display-side drawable *before* running the block.
17
17
  # Then child drawables have a parent to add themselves to.
18
- Shoes::App.instance.with_slot(self, &block) if block_given?
18
+ @app.with_slot(self, &block) if block_given?
19
19
  end
20
20
  end
21
21
  end
@@ -19,7 +19,7 @@ class Shoes
19
19
  def initialize(*args, **kwargs)
20
20
  super
21
21
 
22
- @draw_context = Shoes::App.instance.current_draw_context
22
+ @draw_context = @app.current_draw_context
23
23
 
24
24
  create_display_drawable
25
25
  end
@@ -67,7 +67,7 @@ class Shoes::Widget < Shoes::Slot
67
67
  __widget_initialize(*args, **kwargs, &block)
68
68
 
69
69
  # Do Widgets do this?
70
- Shoes::App.instance.with_slot(self, &block) if block
70
+ @app.with_slot(self, &block) if block
71
71
  end
72
72
  @midway_through_adding_initialize = false
73
73
  end
data/lib/shoes.rb CHANGED
@@ -7,47 +7,47 @@
7
7
  # to handle the DSL and command-line parts of Shoes without knowing anything about how the
8
8
  # display side works at all.
9
9
 
10
- if RUBY_VERSION[0..2] < "3.2"
11
- Shoes::Log.logger("Shoes").error("Lacci (Scarpe, Shoes) requires Ruby 3.2 or higher!")
10
+ if RUBY_VERSION[0..2] < '3.2'
11
+ Shoes::Log.logger('Shoes').error('Lacci (Scarpe, Shoes) requires Ruby 3.2 or higher!')
12
12
  exit(-1)
13
13
  end
14
14
 
15
15
  class Shoes; end
16
16
  class Shoes::Error < StandardError; end
17
- require_relative "shoes/errors"
17
+ require_relative 'shoes/errors'
18
18
 
19
- require_relative "shoes/constants"
20
- require_relative "shoes/ruby_extensions"
19
+ require_relative 'shoes/constants'
20
+ require_relative 'shoes/ruby_extensions'
21
21
 
22
22
  # Shoes adds some top-level methods and constants that can be used everywhere. Kernel is where they go.
23
23
  module Kernel
24
24
  include Shoes::Constants
25
25
  end
26
26
 
27
- require_relative "shoes/display_service"
27
+ require_relative 'shoes/display_service'
28
28
 
29
29
  # Pre-declare classes that get referenced outside their own require file
30
30
  class Shoes::Drawable < Shoes::Linkable; end
31
31
  class Shoes::Slot < Shoes::Drawable; end
32
32
  class Shoes::Widget < Shoes::Slot; end
33
33
 
34
- require_relative "shoes/log"
35
- require_relative "shoes/colors"
34
+ require_relative 'shoes/log'
35
+ require_relative 'shoes/colors'
36
36
 
37
- require_relative "shoes/builtins"
37
+ require_relative 'shoes/builtins'
38
38
 
39
- require_relative "shoes/background"
39
+ require_relative 'shoes/background'
40
40
 
41
- require_relative "shoes/drawable"
42
- require_relative "shoes/app"
43
- require_relative "shoes/drawables"
41
+ require_relative 'shoes/drawable'
42
+ require_relative 'shoes/app'
43
+ require_relative 'shoes/drawables'
44
44
 
45
- require_relative "shoes/download"
45
+ require_relative 'shoes/download'
46
46
 
47
47
  # No easy way to tell at this point whether
48
48
  # we will later load Shoes-Spec code, e.g.
49
49
  # by running a segmented app with test code.
50
- require_relative "shoes-spec"
50
+ require_relative 'shoes-spec'
51
51
 
52
52
  # The module containing Shoes in all its glory.
53
53
  # Shoes is a platform-independent GUI library, designed to create
@@ -55,6 +55,40 @@ require_relative "shoes-spec"
55
55
  #
56
56
  class Shoes
57
57
  class << self
58
+ attr_accessor :APPS
59
+
60
+ # Track the most recently defined Shoes subclass for the inheritance pattern
61
+ # e.g., class Book < Shoes; end; Shoes.app
62
+ attr_accessor :pending_app_class
63
+
64
+ # When someone does `class MyApp < Shoes`, track it
65
+ def inherited(subclass)
66
+ # Only track direct subclasses of Shoes, not Shoes::App, Shoes::Drawable, etc.
67
+ # Those have their own inheritance tracking
68
+ if self == ::Shoes
69
+ Shoes.pending_app_class = subclass
70
+ end
71
+ super
72
+ end
73
+
74
+ # Class-level url method for defining routes in Shoes subclasses
75
+ # e.g., class Book < Shoes; url '/', :index; end
76
+ def url(path, method_name)
77
+ @class_routes ||= {}
78
+ if path.is_a?(String) && path.include?('(')
79
+ # Convert string patterns like '/page/(\d+)' to regex
80
+ regex = Regexp.new("^#{path.gsub(/\(.*?\)/, '(.*?)')}$")
81
+ @class_routes[regex] = method_name
82
+ else
83
+ @class_routes[path] = method_name
84
+ end
85
+ end
86
+
87
+ # Get the routes defined on this class
88
+ def class_routes
89
+ @class_routes ||= {}
90
+ end
91
+
58
92
  # Creates a Shoes app with a new window. The block parameter is used to create
59
93
  # drawables and set up handlers. Arguments are passed to Shoes::App.new internally.
60
94
  #
@@ -78,7 +112,7 @@ class Shoes
78
112
  # @return [void]
79
113
  # @see Shoes::App#new
80
114
  def app(
81
- title: "Shoes!",
115
+ title: 'Shoes!',
82
116
  width: 480,
83
117
  height: 420,
84
118
  resizable: true,
@@ -87,6 +121,47 @@ class Shoes
87
121
  )
88
122
  f = [features].flatten # Make sure this is a list, not a single symbol
89
123
  app = Shoes::App.new(title:, width:, height:, resizable:, features: f, &app_code_body)
124
+
125
+ # If there's a pending Shoes subclass (e.g., class Book < Shoes), use it
126
+ if Shoes.pending_app_class
127
+ subclass = Shoes.pending_app_class
128
+ Shoes.pending_app_class = nil # Clear it so it doesn't affect future apps
129
+
130
+ # Include the subclass as a module to get its instance methods
131
+ # This works because we're extending the singleton class
132
+ methods_to_copy = subclass.instance_methods(false)
133
+
134
+ methods_to_copy.each do |method_name|
135
+ # Get source location and use eval to redefine - but that's fragile
136
+ # Instead, let's use a delegation pattern with the app as context
137
+
138
+ # Read the method's arity and create a proper wrapper
139
+ um = subclass.instance_method(method_name)
140
+
141
+ # Define a wrapper that will eval the original method body in app's context
142
+ # This is a bit hacky but works: we store the subclass and call via instance_eval
143
+ app.define_singleton_method(method_name) do |*args, &block|
144
+ # Create a temporary subclass instance that delegates to app for Shoes methods
145
+ temp = subclass.allocate
146
+ temp.instance_variable_set(:@__shoes_app__, self)
147
+
148
+ # Define method_missing on the temp to delegate Shoes DSL calls to the app
149
+ temp.define_singleton_method(:method_missing) do |name, *a, **kw, &b|
150
+ @__shoes_app__.send(name, *a, **kw, &b)
151
+ end
152
+ temp.define_singleton_method(:respond_to_missing?) { |*| true }
153
+
154
+ # Call the original method on temp (which delegates DSL calls to app)
155
+ temp.send(method_name, *args, &block)
156
+ end
157
+ end
158
+
159
+ # Copy routes from the subclass to the app
160
+ subclass.class_routes.each do |path, method_name|
161
+ app.url(path, method_name)
162
+ end
163
+ end
164
+
90
165
  app.init
91
166
  app.run
92
167
  nil
@@ -125,7 +200,7 @@ class Shoes
125
200
  proc do |path|
126
201
  load path
127
202
  true
128
- end,
203
+ end
129
204
  ]
130
205
  end
131
206
 
@@ -145,4 +220,6 @@ class Shoes
145
220
  @file_loaders = loaders
146
221
  end
147
222
  end
223
+
224
+ Shoes.APPS ||= []
148
225
  end
metadata CHANGED
@@ -1,15 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lacci
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Marco Concetto Rudilosso
8
8
  - Noah Gibbs
9
- autorequire:
10
9
  bindir: exe
11
10
  cert_chain: []
12
- date: 2024-05-06 00:00:00.000000000 Z
11
+ date: 1980-01-02 00:00:00.000000000 Z
13
12
  dependencies:
14
13
  - !ruby/object:Gem::Dependency
15
14
  name: scarpe-components
@@ -25,7 +24,6 @@ dependencies:
25
24
  - - "~>"
26
25
  - !ruby/object:Gem::Version
27
26
  version: 0.4.0
28
- description:
29
27
  email:
30
28
  - marcoc.r@outlook.com
31
29
  - the.codefolio.guy@gmail.com
@@ -94,7 +92,6 @@ licenses:
94
92
  metadata:
95
93
  homepage_uri: https://github.com/scarpe-team/scarpe
96
94
  changelog_uri: https://github.com/scarpe-team/scarpe/blob/main/CHANGELOG.md
97
- post_install_message:
98
95
  rdoc_options: []
99
96
  require_paths:
100
97
  - lib
@@ -109,8 +106,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
109
106
  - !ruby/object:Gem::Version
110
107
  version: '0'
111
108
  requirements: []
112
- rubygems_version: 3.5.3
113
- signing_key:
109
+ rubygems_version: 3.6.9
114
110
  specification_version: 4
115
111
  summary: Lacci - a portable Shoes DSL with switchable display backends, part of Scarpe
116
112
  test_files: []