Class: Shoes::App

Inherits:
Drawable show all
Includes:
Log
Defined in:
lacci/lib/shoes/app.rb,
lacci/lib/shoes/app.rb

Overview

These methods will need to be defined on Slots too, but probably need a rework in general.

Constant Summary collapse

CUSTOM_EVENT_LOOP_TYPES =

These are the allowed values for custom_event_loop events.

  • displaylib means the display library is not going to return from running the app
  • return means the display library will return and the loop will be handled outside Lacci's control
  • wait means Lacci should busy-wait and send eternal heartbeats from the "run" event

If the display service grabs control and keeps it, Webview-style, that means "displaylib" should be the value. A Scarpe-Wasm-style "return" is appropriate if the code can finish without Ruby ending the process at the end of the source file. A "wait" can prevent Ruby from finishing early, but also prevents multiple applications. Only "return" will normally allow multiple Shoes applications.

%w[displaylib return wait]

Constants included from Log

Log::DEFAULT_COMPONENT, Log::DEFAULT_DEBUG_LOG_CONFIG, Log::DEFAULT_LOG_CONFIG

Constants inherited from Drawable

Drawable::DRAW_CONTEXT_STYLES

Class Attribute Summary collapse

Instance Attribute Summary collapse

Attributes inherited from Drawable

#debug_id, #destroyed, #parent

Attributes inherited from Linkable

#linkable_id

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Log

configure_logger, #log_init, logger

Methods inherited from Drawable

allocate_drawable_id, #app, #banner, #caption, convert_to_float, convert_to_integer, #download, drawable_by_id, drawable_class_by_name, dsl_name, #event, expects_parent?, feature_for_shoes_style, get_shoes_events, #hide, #hover, init_args, #inscription, #inspect, is_widget_class?, #leave, #motion, opt_init_args, optional_init_args, register_drawable_id, registered_shoes_events?, required_init_args, #respond_to_missing?, #set_parent, shoes_events, shoes_style, shoes_style_hashes, shoes_style_name?, shoes_style_names, #shoes_style_values, shoes_styles, #show, #style, #subtitle, #tagline, #title, #toggle, unregister_drawable_id, use_current_app, validate_as, with_current_app

Methods included from MarginHelper

#margin_parse

Methods included from Colors

#gray, #rgb, #to_rgb

Methods inherited from Linkable

#bind_shoes_event, #send_self_event, #send_shoes_event, #unsub_all_shoes_events, #unsub_shoes_event

Constructor Details

#initialize(title: 'Shoes!', width: 480, height: 420, resizable: true, features: [], &app_code_body) ⇒ App

Returns a new instance of App.



37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
# File 'lacci/lib/shoes/app.rb', line 37

def initialize(
  title: 'Shoes!',
  width: 480,
  height: 420,
  resizable: true,
  features: [],
  &app_code_body
)
  log_init('Shoes::App')

  if Shoes::FEATURES.include?(:multi_app) || Shoes.APPS.empty?
    Shoes.APPS.push self
  else
    @log.error('Trying to create a second Shoes::App in the same process! Fail!')
    raise Shoes::Errors::TooManyInstancesError, 'Cannot create multiple Shoes::App objects!'
  end

  # We cd to the app's containing dir when running the app
  @dir = Dir.pwd

  @do_shutdown = false
  @event_loop_type = 'displaylib' # the default

  @features = features

  unknown_ext = features - Shoes::FEATURES - Shoes::EXTENSIONS
  unsupported_features = unknown_ext & Shoes::KNOWN_FEATURES
  unless unsupported_features.empty?
    @log.error("Shoes app requires feature(s) not supported by this display service: #{unsupported_features.inspect}!")
    raise Shoes::Errors::UnsupportedFeatureError, "Shoes app needs features: #{unsupported_features.inspect}"
  end
  unless unknown_ext.empty?
    @log.warn("Shoes app requested unknown features #{unknown_ext.inspect}! Known: #{(Shoes::FEATURES + Shoes::EXTENSIONS).inspect}")
  end

  @slots = []

  @content_container = nil

  @routes = {}

  super

  # This creates the DocumentRoot, including its corresponding display drawable
  Drawable.with_current_app(self) do
    @document_root = Shoes::DocumentRoot.new
  end

  # Now create the App display drawable
  create_display_drawable

  # Set up testing *after* Display Service basic objects exist

  if ENV['SHOES_SPEC_TEST'] && !Shoes::App.set_test_code
    test_code = File.read ENV['SHOES_SPEC_TEST']
    unless test_code.empty?
      Shoes::App.set_test_code = true
      Shoes::Spec.instance.run_shoes_spec_test_code test_code
    end
  end

  @app_code_body = app_code_body

  # Try to de-dup as much as possible and not send repeat or multiple
  # destroy events
  @watch_for_destroy = bind_shoes_event(event_name: 'destroy') do
    Shoes::DisplayService.unsub_from_events(@watch_for_destroy) if @watch_for_destroy
    @watch_for_destroy = nil
    destroy(send_event: false)
  end

  @watch_for_event_loop = bind_shoes_event(event_name: 'custom_event_loop') do |loop_type|
    unless CUSTOM_EVENT_LOOP_TYPES.include?(loop_type)
      raise(Shoes::Errors::InvalidAttributeValueError,
            "Unknown event loop type: #{loop_type.inspect}!")
    end

    @event_loop_type = loop_type
  end

  Signal.trap('INT') do
    @log.warn('App interrupted by signal, stopping...')
    puts "\nStopping Shoes app..."
    destroy
  end
end

Dynamic Method Handling

This class handles dynamic methods through the method_missing method

#method_missing(name, *args, **kwargs, &block) ⇒ Object

We use method_missing for drawable-creating methods like "button". The parent's method_missing will auto-create Shoes style getters and setters. This is similar to the method_missing in Shoes::Slot, but different in where the new drawable appears.



162
163
164
165
166
167
168
169
170
171
172
173
# File 'lacci/lib/shoes/app.rb', line 162

def method_missing(name, *args, **kwargs, &block)
  klass = ::Shoes::Drawable.drawable_class_by_name(name)
  return super unless klass

  ::Shoes::App.define_method(name) do |*args, **kwargs, &block|
    Drawable.with_current_app(self) do
      klass.new(*args, **kwargs, &block)
    end
  end

  send(name, *args, **kwargs, &block)
end

Class Attribute Details

.set_test_codeObject

Returns the value of attribute set_test_code.



33
34
35
# File 'lacci/lib/shoes/app.rb', line 33

def set_test_code
  @set_test_code
end

Instance Attribute Details

#dirObject (readonly)

The application directory for this app. Often this will be the directory containing the launched application file.



12
13
14
# File 'lacci/lib/shoes/app.rb', line 12

def dir
  @dir
end

#document_rootObject (readonly)

The Shoes root of the drawable tree



8
9
10
# File 'lacci/lib/shoes/app.rb', line 8

def document_root
  @document_root
end

#featuresObject (readonly)

This is defined to avoid the linkable-id check in the Shoes-style method_missing def'n



17
18
19
# File 'lacci/lib/shoes/app.rb', line 17

def features
  @features
end

Class Method Details

.find_drawables_by(*specs) ⇒ Object

We can add various ways to find drawables here. These are sort of like Shoes selectors, used for testing. This method finds a drawable across all active Shoes apps.



232
233
234
235
236
# File 'lacci/lib/shoes/app.rb', line 232

def self.find_drawables_by(*specs)
  Shoes.APPS.flat_map do |app|
    app.find_drawables_by(*specs)
  end
end

Instance Method Details

#all_drawablesObject



217
218
219
220
221
222
223
224
225
226
227
# File 'lacci/lib/shoes/app.rb', line 217

def all_drawables
  out = []

  to_add = [@document_root, @document_root.children]
  until to_add.empty?
    out.concat(to_add)
    to_add = to_add.flat_map { |w| w.respond_to?(:children) ? w.children : [] }.compact
  end

  out
end

#backgroundObject

This is going to go away. See issue #496



355
356
357
# File 'lacci/lib/shoes/app.rb', line 355

def background(...)
  current_slot.background(...)
end

#borderObject

This is going to go away. See issue #498



360
361
362
# File 'lacci/lib/shoes/app.rb', line 360

def border(...)
  current_slot.border(...)
end

#current_draw_contextHash

Get the current draw context for the current slot

Returns:

  • (Hash)

    a hash of Shoes styles for the current draw context



178
179
180
# File 'lacci/lib/shoes/app.rb', line 178

def current_draw_context
  current_slot&.current_draw_context
end

#current_slotObject



145
146
147
# File 'lacci/lib/shoes/app.rb', line 145

def current_slot
  @slots[-1]
end

#destroy(send_event: true) ⇒ Object



212
213
214
215
# File 'lacci/lib/shoes/app.rb', line 212

def destroy(send_event: true)
  @do_shutdown = true
  send_shoes_event(event_name: 'destroy') if send_event
end

#find_drawables_by(*specs) ⇒ Object

We can add various ways to find drawables here. These are sort of like Shoes selectors, used for testing.



240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
# File 'lacci/lib/shoes/app.rb', line 240

def find_drawables_by(*specs)
  drawables = all_drawables
  specs.each do |spec|
    if spec == Shoes::App
      drawables = [@app]
    elsif spec.is_a?(Class)
      drawables.select! { |w| spec === w }
    elsif spec.is_a?(Symbol) || spec.is_a?(String)
      s = spec.to_s
      case s[0]
      when '$'
        begin
          # I'm not finding a global_variable_get or similar...
          global_value = eval s
          drawables &= [global_value]
        rescue
          # raise Shoes::Errors::InvalidAttributeValueError, "Error getting global variable: #{spec.inspect}"
          drawables = []
        end
      when '@'
        if @app.instance_variables.include?(spec.to_sym)
          drawables &= [@app.instance_variable_get(spec)]
        else
          # raise Shoes::Errors::InvalidAttributeValueError, "Can't find top-level instance variable: #{spec.inspect}!"
          drawables = []
        end
      else
        unless s.start_with?('id:')
          raise Shoes::Errors::InvalidAttributeValueError, "Don't know how to find drawables by #{spec.inspect}!"
        end

        find_id = Integer(s[3..-1])
        drawable = Shoes::Drawable.drawable_by_id(find_id)
        drawables &= [drawable]

      end
    else
      raise(Shoes::Errors::InvalidAttributeValueError, "Don't know how to find drawables by #{spec.inspect}!")
    end
  end
  drawables
end

#initObject



124
125
126
127
128
129
130
# File 'lacci/lib/shoes/app.rb', line 124

def init
  send_shoes_event(event_name: 'init')
  return if @do_shutdown

  with_slot(@document_root, &@app_code_body)
  render_index_if_defined_on_first_boot
end

#line_to(x, y) ⇒ Object



384
385
386
387
388
389
390
391
392
393
# File 'lacci/lib/shoes/app.rb', line 384

def line_to(x, y)
  unless x.is_a?(Numeric) && y.is_a?(Numeric)
    raise(Shoes::Errors::InvalidAttributeValueError,
          'Pass only Numeric arguments to line_to!')
  end

  return unless current_slot.is_a?(::Shoes::Shape)

  current_slot.add_shape_command(['line_to', x, y])
end

#move_to(x, y) ⇒ Object

Shape DSL methods



373
374
375
376
377
378
379
380
381
382
# File 'lacci/lib/shoes/app.rb', line 373

def move_to(x, y)
  unless x.is_a?(Numeric) && y.is_a?(Numeric)
    raise(Shoes::Errors::InvalidAttributeValueError,
          'Pass only Numeric arguments to move_to!')
  end

  return unless current_slot.is_a?(::Shoes::Shape)

  current_slot.add_shape_command(['move_to', x, y])
end

#page(name, &block) ⇒ Object



283
284
285
286
287
288
289
290
# File 'lacci/lib/shoes/app.rb', line 283

def page(name, &block)
  @pages ||= {}
  @pages[name] = proc do
    stack(width: 1.0, height: 1.0) do
      instance_eval(&block)
    end
  end
end

#pop_slotObject



139
140
141
142
143
# File 'lacci/lib/shoes/app.rb', line 139

def pop_slot
  return if @slots.size <= 1

  @slots.pop
end

#push_slot(slot) ⇒ Object

"Container" drawables like flows, stacks, masks and the document root are considered "slots" in Shoes parlance. When a new slot is created, we push it here in order to track what drawables are found in that slot.



135
136
137
# File 'lacci/lib/shoes/app.rb', line 135

def push_slot(slot)
  @slots.push(slot)
end

#runObject

This usually doesn't return. The display service may take control of the main thread. Local Webview even stops any background threads. However, some display libraries don't want to shut down and don't want to (and/or can't) take control of the event loop.



186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
# File 'lacci/lib/shoes/app.rb', line 186

def run
  if @do_shutdown
    warn 'Destroy has already been signaled, but we just called Shoes::App.run!'
    return
  end

  # The display lib can send us an event to customise the event loop handling.
  # But it must do so before the "run" event returns.
  send_shoes_event(event_name: 'run')

  case @event_loop_type
  when 'wait'
    # Display lib wants us to busy-wait instead of it.
    Shoes::DisplayService.dispatch_event('heartbeat', nil) until @do_shutdown
  when 'displaylib'
    # If run event returned, that means we're done.
    destroy
  when 'return'
    # We can just return to the main event loop. But we shouldn't call destroy.
    # Presumably some event loop *outside* our event loop is handling things.
  else
    raise Shoes::Errors::InvalidAttributeValueError,
          "Internal error! Incorrect event loop type: #{@event_loop_type.inspect}!"
  end
end

#url(path, method_name) ⇒ Object



329
330
331
332
333
334
335
336
337
# File 'lacci/lib/shoes/app.rb', line 329

def url(path, method_name)
  if path.is_a?(String) && path.include?('(')
    # Convert string patterns to regex
    regex = Regexp.new("^#{path.gsub(/\(.*?\)/, '(.*?)')}$")
    @routes[regex] = method_name
  else
    @routes[path] = method_name
  end
end

#visit(name_or_path) ⇒ Object



292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
# File 'lacci/lib/shoes/app.rb', line 292

def visit(name_or_path)
  # First, check for exact page match (symbol)
  if @pages && @pages[name_or_path]
    @document_root.clear do
      instance_eval(&@pages[name_or_path])
    end
    return
  end

  # Second, check URL routes
  route, method_name = @routes.find { |r, _| r === name_or_path }
  if route
    @document_root.clear do
      if route.is_a?(Regexp)
        match_data = route.match(name_or_path)
        send(method_name, *match_data.captures)
      else
        send(method_name)
      end
    end
    return
  end

  # Third, if it's a string path like "/page2", try matching page :page2
  if name_or_path.is_a?(String) && name_or_path.start_with?("/")
    page_name = name_or_path[1..-1].to_sym  # "/page2" -> :page2
    if @pages && @pages[page_name]
      @document_root.clear do
        instance_eval(&@pages[page_name])
      end
      return
    end
  end

  puts "Error: URL '#{name_or_path}' not found"
end

#with_slot(slot_item, &block) ⇒ Object



149
150
151
152
153
154
155
156
# File 'lacci/lib/shoes/app.rb', line 149

def with_slot(slot_item, &block)
  return unless block_given?

  push_slot(slot_item)
  instance_eval(&block)
ensure
  pop_slot
end