Class: Clogs::App
Overview
The display side of Shoes::App: one libui window, one canvas, and the
plumbing that turns libui's callbacks into Shoes events.
Constant Summary
collapse
- BACKGROUND =
---- painting -----------------------------------------------------
[255, 255, 255, 255].freeze
Class Attribute Summary collapse
-
.builtin_response ⇒ Object
Set synchronously while a builtin event is being handled, so that ask/confirm can return a value even on Lacci versions that have no response channel of their own.
-
.instance ⇒ Object
Returns the value of attribute instance.
Instance Attribute Summary collapse
Attributes inherited from Drawable
#abs_x, #abs_y, #children, #height, #parent, #shoes_linkable_id, #styles, #width, #x, #y
Instance Method Summary
collapse
-
#add_timer(interval_ms, repeat: true, &block) ⇒ Object
libui's timer callback returns nonzero to keep firing.
-
#app ⇒ Object
-
#app_height ⇒ Object
-
#app_width ⇒ Object
-
#arm_timer(interval_ms, repeat, block) ⇒ Object
The libui binding builds the callback itself when given a block, which is the only way to get the int (*)(void *) signature right; a hand-rolled closure with the wrong arity corrupts the stack.
-
#arm_timers ⇒ Object
-
#builtin(cmd, args) ⇒ Object
---- builtins -----------------------------------------------------.
-
#capture_screenshot(path) ⇒ Object
-
#check_test_deadline ⇒ Object
A busy app -- an animation that cannot keep up on a slow machine, say -- can starve a one-shot timer indefinitely.
-
#destroy ⇒ Object
-
#finish_test_run ⇒ Object
-
#handle_press(event) ⇒ Object
-
#handle_release(event) ⇒ Object
-
#init ⇒ Object
-
#initialize(properties) ⇒ App
constructor
-
#install_test_hooks ⇒ Object
Hooks for automated testing: run the app for a fixed time, optionally capture the screen, then quit.
-
#key_name(event) ⇒ Object
Shoes reports keys as single characters, names like "up", or "control_x" style combinations.
-
#needs_layout! ⇒ Object
-
#notify_subscribers(api_name, *args) ⇒ Object
-
#on_crossed(left) ⇒ Object
-
#on_draw(painter, _params) ⇒ Object
-
#on_key(event) ⇒ Object
-
#on_mouse(event) ⇒ Object
-
#open_list_boxes ⇒ Object
-
#peers_at(x, y) ⇒ Object
---- input --------------------------------------------------------.
-
#quit ⇒ Object
-
#redraw! ⇒ Object
-
#report_error(error) ⇒ Object
-
#run ⇒ Object
-
#set_focus(peer) ⇒ Object
-
#subscriptions ⇒ Object
---- subscriptions and timers -------------------------------------.
-
#title ⇒ Object
-
#topmost_clickable(x, y) ⇒ Object
-
#update_hover(event) ⇒ Object
Methods inherited from Drawable
#add_child, #clickable?, #contains?, #destroy_self, #draw, #each_peer, #focus_gained, #focus_lost, #focusable?, for_shoes_class, #hidden?, #margin, #measure, #notify, #on_click, #on_mouse_move, #on_release, #paint, #positioned?, #properties_changed, #remove_child, #requested_height, #requested_width, #set_parent, #style
Constructor Details
#initialize(properties) ⇒ App
Returns a new instance of App.
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
|
# File 'lib/clogs/app.rb', line 23
def initialize(properties)
super
App.instance = self
@timers = []
@needs_layout = true
@focused = nil
@hovered = nil
@mouse_state = [0, 0, 0]
bind_shoes_event(event_name: "init") { init }
bind_shoes_event(event_name: "run") { run }
bind_shoes_event(event_name: "destroy") { destroy }
Shoes::DisplayService.subscribe_to_event("builtin", nil) do |cmd, args|
App.builtin_response = builtin(cmd, args)
end
end
|
Class Attribute Details
.builtin_response ⇒ Object
Set synchronously while a builtin event is being handled, so that
ask/confirm can return a value even on Lacci versions that have no
response channel of their own.
18
19
20
|
# File 'lib/clogs/app.rb', line 18
def builtin_response
@builtin_response
end
|
.instance ⇒ Object
Returns the value of attribute instance.
13
14
15
|
# File 'lib/clogs/app.rb', line 13
def instance
@instance
end
|
Instance Attribute Details
#canvas ⇒ Object
Returns the value of attribute canvas.
21
22
23
|
# File 'lib/clogs/app.rb', line 21
def canvas
@canvas
end
|
#document_root ⇒ Object
Returns the value of attribute document_root.
21
22
23
|
# File 'lib/clogs/app.rb', line 21
def document_root
@document_root
end
|
#mouse_state ⇒ Object
Returns the value of attribute mouse_state.
21
22
23
|
# File 'lib/clogs/app.rb', line 21
def mouse_state
@mouse_state
end
|
#window ⇒ Object
Returns the value of attribute window.
21
22
23
|
# File 'lib/clogs/app.rb', line 21
def window
@window
end
|
Instance Method Details
#add_timer(interval_ms, repeat: true, &block) ⇒ Object
libui's timer callback returns nonzero to keep firing.
Timers may only be armed once the main loop is running: Shoes programs
routinely call animate or every while the app is still being built,
and handing libui a timer before then crashes it. Queue those and arm
them when the loop starts.
337
338
339
340
341
342
343
344
345
346
347
348
|
# File 'lib/clogs/app.rb', line 337
def add_timer(interval_ms, repeat: true, &block)
interval_ms = 1 if interval_ms.to_i < 1
unless @running
(@pending_timers ||= []) << [interval_ms, repeat, block]
return
end
UI::L.queue_main { arm_timer(interval_ms, repeat, block) }
end
|
#app ⇒ Object
40
41
42
|
# File 'lib/clogs/app.rb', line 40
def app
self
end
|
#app_height ⇒ Object
52
53
54
|
# File 'lib/clogs/app.rb', line 52
def app_height
(style(:height) || 420).to_i
end
|
#app_width ⇒ Object
48
49
50
|
# File 'lib/clogs/app.rb', line 48
def app_width
(style(:width) || 480).to_i
end
|
#arm_timer(interval_ms, repeat, block) ⇒ Object
The libui binding builds the callback itself when given a block, which
is the only way to get the int (*)(void *) signature right; a
hand-rolled closure with the wrong arity corrupts the stack.
Returning nonzero keeps the timer running.
360
361
362
363
364
365
366
367
368
369
370
371
372
373
|
# File 'lib/clogs/app.rb', line 360
def arm_timer(interval_ms, repeat, block)
UI::L.timer(interval_ms) do
if @destroyed
0
else
begin
block.call
rescue StandardError => e
report_error(e)
end
repeat ? 1 : 0
end
end
end
|
#arm_timers ⇒ Object
350
351
352
353
354
|
# File 'lib/clogs/app.rb', line 350
def arm_timers
pending = @pending_timers || []
@pending_timers = nil
pending.each { |interval, repeat, block| arm_timer(interval, repeat, block) }
end
|
#builtin(cmd, args) ⇒ Object
---- builtins -----------------------------------------------------
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
|
# File 'lib/clogs/app.rb', line 377
def builtin(cmd, args)
case cmd.to_s
when "alert" then Dialogs.alert(@window, args[0])
when "confirm" then Dialogs.confirm(@window, args[0])
when "ask" then Dialogs.ask(@window, args[0])
when "ask_open_file" then Dialogs.open_file(@window)
when "ask_save_file" then Dialogs.save_file(@window)
when "ask_open_folder" then Dialogs.open_folder(@window)
when "ask_color" then nil
when "font" then nil
end
rescue StandardError => e
report_error(e)
nil
end
|
#capture_screenshot(path) ⇒ Object
128
129
130
131
132
|
# File 'lib/clogs/app.rb', line 128
def capture_screenshot(path)
system("import", "-window", "root", path, err: File::NULL)
end
|
#check_test_deadline ⇒ Object
A busy app -- an animation that cannot keep up on a slow machine, say --
can starve a one-shot timer indefinitely. The deadline is therefore also
checked from the paint callback, so the run ends as long as anything is
still happening at all.
113
114
115
116
117
118
|
# File 'lib/clogs/app.rb', line 113
def check_test_deadline
return unless @exit_deadline
return if Process.clock_gettime(Process::CLOCK_MONOTONIC) < @exit_deadline
finish_test_run
end
|
#destroy ⇒ Object
134
135
136
137
138
139
|
# File 'lib/clogs/app.rb', line 134
def destroy
return if @destroyed
@destroyed = true
UI::L.quit
end
|
#finish_test_run ⇒ Object
120
121
122
123
124
125
126
|
# File 'lib/clogs/app.rb', line 120
def finish_test_run
return if @exit_deadline.nil?
@exit_deadline = nil
capture_screenshot(ENV["CLOGS_SCREENSHOT"]) if ENV["CLOGS_SCREENSHOT"]
quit
end
|
#handle_press(event) ⇒ Object
222
223
224
225
226
227
228
229
230
231
232
|
# File 'lib/clogs/app.rb', line 222
def handle_press(event)
open_list_boxes.each { |lb| lb.close unless lb.contains?(event.x, event.y) || lb.overlay_contains?(event.x, event.y) }
target = open_list_boxes.find { |lb| lb.overlay_contains?(event.x, event.y) } ||
topmost_clickable(event.x, event.y)
set_focus(target&.focusable? ? target : nil)
target&.on_click(event.x, event.y, event.down)
@pressed = target
notify_subscribers("click", event.down, event.x.round, event.y.round)
end
|
#handle_release(event) ⇒ Object
234
235
236
237
238
|
# File 'lib/clogs/app.rb', line 234
def handle_release(event)
@pressed&.on_release(event.x, event.y, event.up)
@pressed = nil
notify_subscribers("release", event.up, event.x.round, event.y.round)
end
|
#init ⇒ Object
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
|
# File 'lib/clogs/app.rb', line 56
def init
UI::L.init unless @initialized
@initialized = true
@window = UI::L.new_window(title, app_width, app_height, 0)
UI::L.window_set_margined(@window, 0)
UI::L.window_on_closing(@window) do
quit
0
end
@canvas = Canvas.new
@canvas.on_draw = method(:on_draw)
@canvas.on_mouse = method(:on_mouse)
@canvas.on_key = method(:on_key)
@canvas.on_crossed = method(:on_crossed)
box = UI::L.new_vertical_box
UI::L.box_append(box, @canvas.area, 1)
UI::L.window_set_child(@window, box)
end
|
#install_test_hooks ⇒ Object
Hooks for automated testing: run the app for a fixed time, optionally
capture the screen, then quit. Used by Clogs' own visual tests and handy
for any Shoes app under CI.
CLOGS_EXIT_AFTER_MS=800 CLOGS_SCREENSHOT=out.png ruby app.rb
101
102
103
104
105
106
107
|
# File 'lib/clogs/app.rb', line 101
def install_test_hooks
ms = ENV["CLOGS_EXIT_AFTER_MS"]&.to_i
return if ms.nil? || ms <= 0
@exit_deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + ms / 1000.0
add_timer(ms, repeat: false) { finish_test_run }
end
|
#key_name(event) ⇒ Object
Shoes reports keys as single characters, names like "up", or
"control_x" style combinations.
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
|
# File 'lib/clogs/app.rb', line 288
def key_name(event)
base = if event.ext
event.ext.to_s
elsif event.char
case event.char
when "\b", "\x7F" then "backspace"
when "\r", "\n" then "\n"
when "\t" then "tab"
when " " then " "
else event.char
end
end
return nil unless base
mods = []
mods << "control" if event.ctrl?
mods << "alt" if event.alt?
mods << "shift" if event.shift? && base.length > 1
mods.empty? ? base : (mods + [base]).join("_")
end
|
#needs_layout! ⇒ Object
152
153
154
155
|
# File 'lib/clogs/app.rb', line 152
def needs_layout!
@needs_layout = true
redraw!
end
|
#notify_subscribers(api_name, *args) ⇒ Object
325
326
327
328
329
|
# File 'lib/clogs/app.rb', line 325
def notify_subscribers(api_name, *args)
subscriptions.each do |sub|
sub.notify(api_name, *args) if sub.api_name == api_name
end
end
|
#on_crossed(left) ⇒ Object
309
310
311
312
313
314
315
|
# File 'lib/clogs/app.rb', line 309
def on_crossed(left)
return unless left
@hovered&.on_mouse_leave if @hovered.respond_to?(:on_mouse_leave)
@hovered = nil
redraw!
end
|
#on_draw(painter, _params) ⇒ Object
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
|
# File 'lib/clogs/app.rb', line 167
def on_draw(painter, _params)
check_test_deadline
painter.fill_rect(0, 0, painter.width, painter.height, BACKGROUND)
return unless @document_root
if @needs_layout || @last_width != painter.width
@document_root.measure(painter.width)
@last_width = painter.width
@needs_layout = false
end
@document_root.paint(painter, 0, 0)
open_list_boxes.each { |lb| lb.draw_overlay(painter) }
rescue StandardError => e
report_error(e)
end
|
#on_key(event) ⇒ Object
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
|
# File 'lib/clogs/app.rb', line 269
def on_key(event)
return false if @destroyed
if @focused&.on_key(event)
redraw!
return true
end
name = key_name(event)
return false unless name && !event.up
notify_subscribers("keypress", name)
true
rescue StandardError => e
report_error(e)
false
end
|
#on_mouse(event) ⇒ Object
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
|
# File 'lib/clogs/app.rb', line 205
def on_mouse(event)
@mouse_state = [event.button_down? ? 1 : 0, event.x.round, event.y.round]
Shoes::DisplayService.mouse_state = @mouse_state if Shoes::DisplayService.respond_to?(:mouse_state=)
update_hover(event)
notify_subscribers("motion", event.x.round, event.y.round,
event.modifiers.anybits?(UI::MOD_CTRL), event.modifiers.anybits?(UI::MOD_SHIFT))
if event.down.positive?
handle_press(event)
elsif event.up.positive?
handle_release(event)
end
rescue StandardError => e
report_error(e)
end
|
#open_list_boxes ⇒ Object
183
184
185
186
187
188
189
|
# File 'lib/clogs/app.rb', line 183
def open_list_boxes
return [] unless @document_root
found = []
@document_root.each_peer { |peer| found << peer if peer.is_a?(ListBox) && peer.open? }
found
end
|
#peers_at(x, y) ⇒ Object
---- input --------------------------------------------------------
193
194
195
196
197
198
199
|
# File 'lib/clogs/app.rb', line 193
def peers_at(x, y)
return [] unless @document_root
hits = []
@document_root.each_peer { |peer| hits << peer if peer.contains?(x, y) }
hits
end
|
#quit ⇒ Object
141
142
143
144
|
# File 'lib/clogs/app.rb', line 141
def quit
notify("destroy")
destroy
end
|
#redraw! ⇒ Object
157
158
159
160
161
|
# File 'lib/clogs/app.rb', line 157
def redraw!
return if @destroyed
@canvas&.redraw
end
|
#report_error(error) ⇒ Object
393
394
395
396
|
# File 'lib/clogs/app.rb', line 393
def report_error(error)
warn "Clogs error: #{error.class}: #{error.message}"
warn error.backtrace.first(12).join("\n") if error.backtrace
end
|
#run ⇒ Object
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
|
# File 'lib/clogs/app.rb', line 79
def run
UI::L.control_show(@window)
@running = true
arm_timers
install_test_hooks
UI::L.main
UI::L.control_destroy(@window) if @window
@window = nil
@canvas = nil
UI::L.uninit if @initialized
end
|
#set_focus(peer) ⇒ Object
257
258
259
260
261
262
263
264
265
266
267
|
# File 'lib/clogs/app.rb', line 257
def set_focus(peer)
return if peer == @focused
@focused&.focused = false
@focused&.focus_lost
@focused = peer
return unless peer
peer.focused = true
peer.focus_gained
end
|
#subscriptions ⇒ Object
---- subscriptions and timers -------------------------------------
319
320
321
322
323
|
# File 'lib/clogs/app.rb', line 319
def subscriptions
found = []
@document_root&.each_peer { |peer| found << peer if peer.is_a?(SubscriptionItem) }
found
end
|
#title ⇒ Object
44
45
46
|
# File 'lib/clogs/app.rb', line 44
def title
style(:title) || "Shoes"
end
|
#topmost_clickable(x, y) ⇒ Object
201
202
203
|
# File 'lib/clogs/app.rb', line 201
def topmost_clickable(x, y)
peers_at(x, y).reverse.find(&:clickable?)
end
|
#update_hover(event) ⇒ Object
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
|
# File 'lib/clogs/app.rb', line 240
def update_hover(event)
target = peers_at(event.x, event.y).reverse.find { |p| p.clickable? || p.is_a?(Control) }
return if target == @hovered
if @hovered
@hovered.on_mouse_leave if @hovered.respond_to?(:on_mouse_leave)
@hovered.notify("leave")
end
@hovered = target
if target
target.on_mouse_enter if target.respond_to?(:on_mouse_enter)
target.notify("hover")
end
notify_subscribers("hover") if target
notify_subscribers("leave") unless target
end
|