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.

["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.



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
# File 'lacci/lib/shoes/app.rb', line 39

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

  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
    self.destroy(send_event: false)
  end

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

    @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.



158
159
160
161
162
163
164
165
166
167
168
169
# File 'lacci/lib/shoes/app.rb', line 158

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.



35
36
37
# File 'lacci/lib/shoes/app.rb', line 35

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

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.



229
230
231
232
233
# File 'lacci/lib/shoes/app.rb', line 229

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

Instance Method Details

#all_drawablesObject



214
215
216
217
218
219
220
221
222
223
224
# File 'lacci/lib/shoes/app.rb', line 214

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



314
315
316
# File 'lacci/lib/shoes/app.rb', line 314

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

#borderObject

This is going to go away. See issue #498



319
320
321
# File 'lacci/lib/shoes/app.rb', line 319

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



174
175
176
# File 'lacci/lib/shoes/app.rb', line 174

def current_draw_context
  current_slot&.current_draw_context
end

#current_slotObject



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

def current_slot
  @slots[-1]
end

#destroy(send_event: true) ⇒ Object



209
210
211
212
# File 'lacci/lib/shoes/app.rb', line 209

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

#featuresObject

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

#find_drawables_by(*specs) ⇒ Object

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



237
238
239
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
# File 'lacci/lib/shoes/app.rb', line 237

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
        if s.start_with?("id:")
          find_id = Integer(s[3..-1])
          drawable = Shoes::Drawable.drawable_by_id(find_id)
          drawables &= [drawable]
        else
          raise Shoes::Errors::InvalidAttributeValueError, "Don't know how to find drawables by #{spec.inspect}!"
        end
      end
    else
      raise(Shoes::Errors::InvalidAttributeValueError, "Don't know how to find drawables by #{spec.inspect}!")
    end
  end
  drawables
end

#initObject



121
122
123
124
125
126
# File 'lacci/lib/shoes/app.rb', line 121

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

  with_slot(@document_root, &@app_code_body)
end

#line_to(x, y) ⇒ Object



340
341
342
343
344
345
346
# File 'lacci/lib/shoes/app.rb', line 340

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

  if current_slot.is_a?(::Shoes::Shape)
    current_slot.add_shape_command(["line_to", x, y])
  end
end

#move_to(x, y) ⇒ Object

Shape DSL methods



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

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

  if current_slot.is_a?(::Shoes::Shape)
    current_slot.add_shape_command(["move_to", x, y])
  end
end

#page(name, &block) ⇒ Object



279
280
281
282
283
284
285
286
# File 'lacci/lib/shoes/app.rb', line 279

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



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

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.



131
132
133
# File 'lacci/lib/shoes/app.rb', line 131

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.



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

def run
  if @do_shutdown
    $stderr.puts "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.
    until @do_shutdown
      Shoes::DisplayService.dispatch_event("heartbeat", nil)
    end
  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

#visit(name) ⇒ Object



288
289
290
291
292
293
294
295
296
# File 'lacci/lib/shoes/app.rb', line 288

def visit(name)
  if @pages && @pages[name]
    @document_root.clear do
      instance_eval(&@pages[name])
    end
  else
    puts "Error: URL '#{name}' not found"
  end
end

#with_slot(slot_item, &block) ⇒ Object



145
146
147
148
149
150
151
152
# File 'lacci/lib/shoes/app.rb', line 145

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

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