guindilla_gui 0.1.1

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.
data/README.md ADDED
@@ -0,0 +1,89 @@
1
+
2
+ # GuindillaGUI
3
+
4
+
5
+ ## Description
6
+ Libray for creating browser-based GUIs in Ruby
7
+
8
+ This library is still in a bare-bones **wicked-alpha** state, and is subject to fires, floods, and radical changes!<br>
9
+ Tested on Linux (Arch 5.15.x-lts) with Ruby 3.0.x
10
+
11
+ ## Installation
12
+ Should be as easy as:
13
+ `gem install guindilla_gui`
14
+
15
+ ## Usage
16
+
17
+ Take a look at the [wiki](https://gitlab.com/lljk/guindilla_gui/-/wikis/home) and the examples. Basic usage goes something like this:
18
+ ```
19
+ require 'guindilla_gui'
20
+ include GuindillaGUI
21
+
22
+ # your regular old ruby logic goes here #
23
+
24
+ Guindilla.new do
25
+
26
+ # your nifty GuindillaGUI methods go here #
27
+
28
+ end
29
+ ```
30
+ Normally `Guindilla` will be the only class you need to explicitly instantiate.
31
+ Other classes are instantiated by methods called inside the `Guindilla` block, e.g.:
32
+ ```
33
+ image('my_image.jpg')
34
+ button("push me") do
35
+ ...
36
+ end
37
+ ```
38
+ Here's a look at `demo1.rb` from the examples for a better idea of how this all works...
39
+ ```
40
+ require 'guindilla_gui'
41
+ include GuindillaGUI
42
+
43
+ images = []
44
+ (2..6).each do |n|
45
+ images << "http://poignant.guide/images/chapter.poignant.guide-#{n}.jpg"
46
+ end
47
+
48
+ def rand_color
49
+ "rgb(#{rand(200)}, #{rand(200)}, #{rand(200)})"
50
+ end
51
+
52
+
53
+ Guindilla.new do
54
+ font_family('sans')
55
+ text_align('center')
56
+ align('center')
57
+
58
+ banner = heading("GuindillaGUI",
59
+ width: '90%',
60
+ border_radius: '10px',
61
+ background: 'crimson',
62
+ color: 'white'
63
+ )
64
+ banner.transition('background', 1, "ease-out")
65
+
66
+ pic = image('http://poignant.guide/images/chapter.poignant.guide-2.jpg')
67
+ pic.transition("rotate", 1)
68
+
69
+ button("go ahead, push me.", width: '40%', margin: '20px') do
70
+ banner.background = rand_color
71
+ pic.source = images[rand(5)]
72
+ pic.rotate(360)
73
+ end
74
+
75
+ end
76
+ ```
77
+
78
+ ## Support
79
+
80
+ ## Contributing
81
+
82
+ ## Acknowledgments
83
+ thanks to Matz, _why, and especially The Dude for being thoughtful, thought-provoking, and just generally awesome.<br>
84
+ thanks to ashbb and the Shoes gang for help way back when.<br>
85
+ thanks and praises to the most high.
86
+
87
+ ## License
88
+ yeah, well since we've got to do this business -<br>
89
+ GPL-3.0-or-later
@@ -0,0 +1,605 @@
1
+ #------------------------------------------------------------------------------#
2
+ # Copyleft 2022
3
+ # This file is part of GuindillaGUI.
4
+ # GuindillaGUI is free software: you can redistribute it and/or modify it under
5
+ # the terms of the GNU General Public License as published by the Free Software
6
+ # Foundation, either version 3 of the License, or (at your option) any later
7
+ # version.
8
+ # GuindillaGUI is distributed in the hope that it will be useful, but WITHOUT
9
+ # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
10
+ # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
11
+ # You should have received a copy of the GNU General Public License along with
12
+ # GuindillaGUI. If not, see <https://www.gnu.org/licenses/>.
13
+ #------------------------------------------------------------------------------#
14
+
15
+ module GuindillaGUI
16
+ ##
17
+ # Normally `Guindilla` will be the only class you explicitly instantiate.
18
+ # Other classes are instantiated by methods called inside the `Guindilla` block.
19
+ # Option keys may include `browser:` , `host:` , and/or `port:`
20
+ class Guindilla
21
+ attr_accessor :socket, :active_id, :elements, :blocks, :inputs
22
+
23
+ def initialize(options={}, &block)
24
+ @@gui = self
25
+ @active_id = 'body'
26
+ @elements = {}
27
+ @blocks = {}
28
+ @inputs = {}
29
+ html_file = File.expand_path File.dirname(__FILE__) + "/resources/guindilla.html"
30
+ options[:host] ? host = options[:host] : host = 'localhost'
31
+ options[:port] ? port = options[:port] : port = 8181
32
+
33
+ # initialize socket client #
34
+ Launchy.open(html_file) do |exception|
35
+ puts "Attempted to open #{html_file} and failed because #{exception}"
36
+ end
37
+
38
+ # initialize socket server #
39
+ EM.run do
40
+ EM::WebSocket.start(:host => host, :port => port) do |socket|
41
+ @socket = socket
42
+
43
+ socket.onopen do |handshake|
44
+ puts "GuindillaGUI WebSocket connection open on: #{host}, port #{port}"
45
+ # make the main box #
46
+ v_box do
47
+ # pass off to user #
48
+ self.instance_eval &block if block
49
+ end
50
+ end
51
+
52
+ # handle events #
53
+ socket.onmessage do |msg|
54
+ puts "Recieved message: #{msg}" if options[:verbose] == true
55
+ ##
56
+ # not sure how necessary this is, but think it handles funky closes
57
+ socket.close if msg == "UI closed."
58
+
59
+ message = msg.split(":!!:")
60
+ id = message[0]
61
+
62
+ if message[1]
63
+ case message[1]
64
+ when "position"
65
+ element = @elements[:"#{id}"]
66
+ message[2].chop!.split(",").each do |pair|
67
+ keyval = pair.split(":")
68
+ element.position[:"#{keyval[0]}"] = keyval[1].to_f
69
+ end
70
+ @blocks[:"#{id}_pos"].call(element.position) if @blocks[:"#{id}_pos"]
71
+ when "input"
72
+ message[2] = "" unless message[2]
73
+ begin
74
+ message[2] = Integer(message[2])
75
+ rescue ArgumentError => e
76
+ end
77
+ @inputs[:"#{id}"].value = message[2]
78
+ @blocks[:"#{id}"].call(message[2]) if @blocks[:"#{id}"]
79
+ when "mousemove"
80
+ xy = message[2].split(',')
81
+ @blocks[:"#{id}_move"].call(xy[0].to_i, xy[1].to_i) if @blocks[:"#{id}_move"]
82
+ end
83
+ else
84
+ @blocks[:"#{id}"].call if @blocks[:"#{id}"]
85
+ end
86
+ end #socket.onmessage
87
+
88
+ socket.onclose do
89
+ #send_js(%Q~ window.stop(); process.exit(1); ~) # maybe not necessary
90
+ puts "GuindillaGUI WebSocket connection closed"
91
+ #abort "exiting GuindillaGUI..."
92
+ puts "exiting GuindillaGUI..."
93
+ exit!
94
+ end
95
+
96
+ end #EM::WebSocket.run
97
+ end #EM.run
98
+ end #initialize
99
+
100
+ def attributes # REVIEW: not sure this is needed (it's only for stand-alone) #
101
+ @@gui.elements[:"#{caller_id(self)}"].attributes
102
+ end
103
+
104
+ private
105
+ ##
106
+ # `caller_id(caller)` allows for stand-alone style methods
107
+ # from within an element's block, eg:
108
+ # ```
109
+ # h_box do
110
+ # background('yellow')
111
+ # end
112
+ # ```
113
+ def caller_id(caller)
114
+ if caller.class.to_s == "GuindillaGUI::Guindilla"
115
+ id = @@gui.active_id
116
+ else
117
+ id = caller.id
118
+ end
119
+ end
120
+
121
+ def send_js(script)
122
+ @@gui.socket.send(%Q~ <script>#{script}</script> ~)
123
+ end
124
+ end #class Guindilla
125
+
126
+
127
+ ##############################################################################
128
+ #----------------------------------------------------------------------------#
129
+ # Element #
130
+ #----------------------------------------------------------------------------#
131
+ ##############################################################################
132
+ ##
133
+ # `Element` parent class that Guindilla methods use to create HTML elements.
134
+ class Element < Guindilla
135
+ attr_reader :id
136
+ attr_accessor :attributes, :position
137
+
138
+ def initialize(type, attributes={})
139
+ @id = "#{type}_#{Time.now.hash.to_s.gsub('-', 'x')}"
140
+ send_js(%Q~
141
+ const #{@id} = document.createElement("#{type}");
142
+ #{@id}.id = "#{@id}";
143
+ ~)
144
+
145
+ hidden = attributes.delete(:hidden)
146
+ self.hide if hidden == true
147
+ send_js(%Q~ #{@@gui.active_id}.append(#{@id}); ~)
148
+ if attributes.has_key?(:size)
149
+ size = attributes[:size].split(",")
150
+ attributes[:width] = size[0].to_i
151
+ attributes[:height] = size[1].to_i
152
+ end
153
+
154
+ @attributes = attributes
155
+ @position = {}
156
+ @@gui.elements[:"#{@id}"] = self
157
+ self.style(attributes)
158
+ end
159
+
160
+ def get_position(&block)
161
+ @@gui.blocks[:"#{self.id}_pos"] = block if block_given?
162
+ send_js(%Q~
163
+ var rect = #{@id}.getBoundingClientRect();
164
+ var rect_string = ""
165
+ for (var key in rect) {
166
+ if(typeof rect[key] !== 'function') {
167
+ rect_string += `${key}:${rect[key]},`;
168
+ }
169
+ }
170
+ socket.send("#{id}:!!:position:!!:" + rect_string)
171
+ ~)
172
+ end
173
+ end #class Element
174
+
175
+
176
+ ##############################################################################
177
+ #----------------------------------------------------------------------------#
178
+ # AudioVideo #
179
+ #----------------------------------------------------------------------------#
180
+ ##############################################################################
181
+ class AudioVideo < Element
182
+ attr_accessor :state
183
+
184
+ def initialize(type, attributes)
185
+ super("#{type}")
186
+ @state = "stopped"
187
+ if attributes[:controls] == true
188
+ send_js(%Q~ #{self.id}.controls = true; ~)
189
+ end
190
+ self.width = attributes[:width] if attributes[:width]
191
+ self.height = attributes[:height] if attributes[:height]
192
+ self.source = attributes[:source] if attributes[:source]
193
+ end
194
+
195
+ def pause
196
+ send_js(%Q~ #{self.id}.pause(); ~)
197
+ @state = "paused"
198
+ end
199
+
200
+ def play
201
+ send_js(%Q~ #{self.id}.play(); ~)
202
+ @state = "playing"
203
+ end
204
+
205
+ def volume=(vol)
206
+ send_js(%Q~ #{self.id}.volume = #{vol}; ~)
207
+ end
208
+ end #class AudioVideo
209
+
210
+
211
+ ##############################################################################
212
+ #----------------------------------------------------------------------------#
213
+ # Box #
214
+ #----------------------------------------------------------------------------#
215
+ ##############################################################################
216
+ class Box < Element
217
+ attr_reader :direction
218
+
219
+ def initialize(direction, attributes, &block)
220
+ attributes[:justify_content] = attributes[:justify] if attributes[:justify]
221
+ attributes[:align_items] = attributes[:align] if attributes [:align]
222
+ super("div", attributes)
223
+
224
+ unless direction == nil
225
+ send_js(%Q~
226
+ #{self.id}.style.display = 'flex';
227
+ #{self.id}.style.flexFlow = '#{direction} wrap';
228
+ ~)
229
+ end
230
+
231
+ last_active_id = @@gui.active_id
232
+ @@gui.active_id = self.id
233
+ if block_given?
234
+ block.call
235
+ end
236
+ @@gui.active_id = last_active_id
237
+ # HACK: return self (?) # hmm, here?, in methods? nowhere?
238
+ end #initialize
239
+ end #class Box
240
+
241
+
242
+ ##############################################################################
243
+ #----------------------------------------------------------------------------#
244
+ # Canvas #
245
+ #----------------------------------------------------------------------------#
246
+ ##############################################################################
247
+ class Canvas < Element
248
+ def initialize(attributes, &block)
249
+ super("canvas", attributes)
250
+ send_js(%Q~ const #{self.id}_ctx = #{self.id}.getContext("2d"); ~)
251
+ self.instance_eval &block if block_given?
252
+ end
253
+
254
+ ##
255
+ # attributes may include `type: fill`, and/or `reverse: true`
256
+ def arc(x, y, radius, start_angle, end_angle, attributes={})
257
+ attributes[:type] ? type = attributes[:type] : type = 'stroke'
258
+ attributes[:reverse] ? reverse = attributes[:reverse] : reverse = false
259
+ s_ang = (start_angle) * Math::PI / 180
260
+ e_ang = (end_angle) * Math::PI / 180
261
+ send_js(%Q~
262
+ #{self.id}_ctx.arc(#{x}, #{y}, #{radius}, #{s_ang}, #{e_ang}, #{reverse});
263
+ #{self.id}_ctx.#{type.to_s}();
264
+ ~)
265
+ end
266
+
267
+ def arc_to(x1, y1, x2, y2, radius) ## HACK: works, but wtf is this? ##
268
+ send_js(%Q~
269
+ #{self.id}_ctx.arc(#{x1}, #{y1}, #{x2}, #{y2}, #{radius});
270
+ #{self.id}_ctx.stroke();
271
+ ~)
272
+ end
273
+
274
+ def canvas_circle(x, y, radius, type="stroke")
275
+ arc(x, y, radius, 0, 360, type: "#{type}")
276
+ send_js(%Q~ #{self.id}_ctx.fill; ~) if type.to_s == "fill"
277
+ end
278
+
279
+ def canvas_rectangle(x, y, width, height, type="stroke")
280
+ send_js(%Q~
281
+ #{self.id}_ctx.#{type.to_s}Rect(#{x}, #{y}, #{width}, #{height});
282
+ ~)
283
+ end
284
+
285
+ def draw(type="stroke", &block)
286
+ send_js(%Q~ #{self.id}_ctx.beginPath(); ~)
287
+ block.call
288
+ send_js(%Q~ #{self.id}_ctx.#{type.to_s}(); ~)
289
+ end
290
+
291
+ def fill
292
+ send_js(%Q~ #{self.id}_ctx.fill(); ~)
293
+ end
294
+
295
+ def fill_color(clr)
296
+ send_js(%Q~ #{self.id}_ctx.fillStyle = "#{clr}"; ~)
297
+ end
298
+
299
+ def line_color(clr)
300
+ send_js(%Q~
301
+ #{self.id}_ctx.strokeStyle = "#{clr}";
302
+ ~)
303
+ end
304
+
305
+ def line_to(x, y)
306
+ send_js(%Q~
307
+ #{self.id}_ctx.lineTo(#{x}, #{y});
308
+ #{self.id}_ctx.stroke();
309
+ ~)
310
+ end
311
+
312
+ def move_to(x, y)
313
+ send_js(%Q~
314
+ #{self.id}_ctx.moveTo(#{x}, #{y});
315
+ ~)
316
+ end
317
+
318
+ def stroke
319
+ send_js(%Q~ #{self.id}_ctx.stroke(); ~)
320
+ end
321
+ end #class Canvas
322
+
323
+
324
+ ##############################################################################
325
+ #----------------------------------------------------------------------------#
326
+ # Chart #
327
+ #----------------------------------------------------------------------------#
328
+ ##############################################################################
329
+ class Chart < Element
330
+ attr_accessor :data
331
+
332
+ def initialize(attributes, &block)
333
+ title = attributes.delete(:title)
334
+ attributes[:type] = 'scatter' unless attributes.has_key?(:type)
335
+ @type = attributes.delete(:type)
336
+ attributes[:showlegend] = true unless attributes.has_key?(:showlegend)
337
+ showlegend = attributes.delete(:showlegend)
338
+ div = container(attributes)
339
+ @data = []
340
+ @x_axis = {}
341
+ @y_axis = {}
342
+ @legend = {}
343
+
344
+ instance_eval &block if block
345
+
346
+ data_string = "["
347
+ @data.each{|hash| data_string += to_plotly(hash) + ", "}
348
+ data_string.delete_suffix!(", ")
349
+ data_string += "]"
350
+
351
+ layout_string = %Q~{title: "#{title}", showlegend: #{showlegend}, ~
352
+ layout_string += "xaxis: " + to_plotly(@x_axis) + ", " unless @x_axis.empty?
353
+ layout_string += "yaxis: " + to_plotly(@y_axis) + ", " unless @y_axis.empty?
354
+ layout_string += "legend: " + to_plotly(@legend) + ", " unless @legend.empty?
355
+ layout_string.delete_suffix!(", ")
356
+ layout_string += "}"
357
+
358
+ send_js(%Q~ Plotly.newPlot(#{div.id}, #{data_string}, #{layout_string}); ~)
359
+ end #initialize
360
+
361
+ def plot(plot_hash)
362
+ plot_hash[:type] = @type
363
+ @data << plot_hash
364
+ end
365
+
366
+ def legend(legend_hash)
367
+ @legend = legend_hash
368
+ end
369
+
370
+ def x_axis(x_hash)
371
+ @x_axis = x_hash
372
+ end
373
+
374
+ def y_axis(y_hash)
375
+ @y_axis = y_hash
376
+ end
377
+
378
+ private
379
+
380
+ def to_plotly(hash)
381
+ string = "{"
382
+ hash.each do |key, value|
383
+ if value.is_a?(String)
384
+ string += %Q~#{key}: "#{value}", ~
385
+ elsif value.is_a?(Hash)
386
+ string += "#{key}: "
387
+ string += to_plotly(value)
388
+ string.delete_suffix!(", ")
389
+ string += ", "
390
+ else
391
+ string += "#{key}: #{value}, "
392
+ end
393
+ end
394
+ string.delete_suffix!(", ")
395
+ string += "}"
396
+ return string
397
+ end
398
+ end # class Chart
399
+
400
+
401
+ ##############################################################################
402
+ #----------------------------------------------------------------------------#
403
+ # Input #
404
+ #----------------------------------------------------------------------------#
405
+ ##############################################################################
406
+ class Input < Element
407
+ attr_accessor :label, :value
408
+
409
+ def initialize(type, label, attributes, &block)
410
+ attributes[:hidden] = true
411
+ min = attributes.delete(:min) if attributes.has_key?(:min)
412
+ max = attributes.delete(:max) if attributes.has_key?(:max)
413
+ step = attributes.delete(:step) if attributes.has_key?(:step)
414
+ value = attributes.delete(:value) if attributes.has_key?(:value)
415
+ super("input", attributes)
416
+
417
+ send_js(%Q~ #{self.id}.type = "#{type}"; ~)
418
+ @@gui.inputs[:"#{self.id}"] = self
419
+ @@gui.blocks[:"#{self.id}"] = block if block_given?
420
+ group = attributes[:group] #####
421
+
422
+ case type
423
+ when "checkbox"
424
+ send_js(%Q~
425
+ #{self.id}.oninput = function(event){
426
+ socket.send("#{self.id}:!!:input:!!:" + #{self.id}.checked);
427
+ event.stopPropagation();
428
+ };
429
+ ~)
430
+ self.check if attributes[:checked]
431
+ when "color"
432
+ send_js(%Q~
433
+ #{self.id}.oninput = function(event){
434
+ socket.send("#{self.id}:!!:input:!!:" + #{self.id}.value);
435
+ event.stopPropagation();
436
+ };
437
+ ~)
438
+ when "file"
439
+ send_js(%Q~
440
+ #{self.id}.oninput = function(event){
441
+ socket.send("#{self.id}:!!:input:!!:" + URL.createObjectURL(#{self.id}.files[0]));
442
+ event.stopPropagation();
443
+ };
444
+ #{self.id}.setAttribute('multiple', '');
445
+ ~)
446
+ when "number"
447
+ send_js(%Q~
448
+ #{self.id}.setAttribute('min', '#{min}') ;
449
+ #{self.id}.setAttribute('max', '#{max}') ;
450
+ ~)
451
+ send_js(%Q~
452
+ #{self.id}.onchange = function(event){
453
+ socket.send("#{self.id}:!!:input:!!:" + #{self.id}.value);
454
+ event.stopPropagation();
455
+ };
456
+ ~)
457
+ when "radio"
458
+ send_js(%Q~ #{self.id}.name = "#{group}"; ~)
459
+ send_js(%Q~
460
+ #{self.id}.oninput = function(event){
461
+ socket.send("#{self.id}:!!:input:!!:" + #{self.id}.checked);
462
+ event.stopPropagation();
463
+ };
464
+ ~)
465
+ self.check if attributes[:checked]
466
+ when "range"
467
+ send_js(%Q~
468
+ #{self.id}.setAttribute('min', '#{min}') ;
469
+ #{self.id}.setAttribute('max', '#{max}') ;
470
+ #{self.id}.setAttribute('step', '#{step}') ;
471
+ #{self.id}.setAttribute('value', '#{value}') ;
472
+ ~)
473
+ send_js(%Q~
474
+ #{self.id}.onchange = function(event){
475
+ socket.send("#{self.id}:!!:input:!!:" + #{self.id}.value);
476
+ event.stopPropagation();
477
+ };
478
+ ~)
479
+ when "text"
480
+ send_js(%Q~
481
+ #{self.id}.oninput = function(event){
482
+ socket.send("#{self.id}:!!:input:!!:" + #{self.id}.value);
483
+ event.stopPropagation();
484
+ };
485
+ ~)
486
+ when "url"
487
+ send_js(%Q~
488
+ #{self.id}.oninput = function(event){
489
+ socket.send("#{self.id}:!!:input:!!:" + #{self.id}.value);
490
+ event.stopPropagation();
491
+ };
492
+ ~)
493
+ end #case type
494
+
495
+ @label = Element.new("label")
496
+ send_js(%Q~ #{@label.id}.innerHTML = "#{label}"; ~)
497
+ @label.append(self)
498
+ @label.show
499
+ return self ## ?
500
+ end #initialize
501
+
502
+ def check
503
+ send_js(%Q~ #{self.id}.checked = true; ~)
504
+ end
505
+
506
+ def uncheck
507
+ send_js(%Q~ #{self.id}.checked = false; ~)
508
+ end
509
+ end #class Input
510
+
511
+
512
+ ##############################################################################
513
+ #----------------------------------------------------------------------------#
514
+ # Svg #
515
+ #----------------------------------------------------------------------------#
516
+ ##############################################################################
517
+ class Svg < Element
518
+ attr_reader :type
519
+
520
+ def initialize(type, attributes, &block)
521
+ attributes[:fill] = attributes.delete(:color) if attributes.has_key?(:color)
522
+ if attributes.has_key?(:r)
523
+ attributes[:width] = attributes[:r] * 2
524
+ attributes[:height] = attributes[:r] * 2
525
+ end
526
+ @type = type
527
+ hidden = attributes.delete(:hidden)
528
+ @id = "svg_#{Time.now.hash.to_s.gsub('-', 'x')}"
529
+ svg_ns = "http://www.w3.org/2000/svg"
530
+
531
+ send_js(%Q~
532
+ const #{@id} = document.createElementNS("#{svg_ns}", "svg");
533
+ #{@id}.id = "#{@id}";
534
+ #{@id}.setAttribute("width", #{attributes[:width]});
535
+ #{@id}.setAttribute("height", #{attributes[:height]});
536
+ const #{type}_#{@id} = document.createElementNS("#{svg_ns}", "#{type}");
537
+ #{type}_#{@id}.id = "#{type}_#{@id}";
538
+ ~)
539
+
540
+ attributes.each do |key, value|
541
+ send_js(%Q~
542
+ #{type}_#{@id}.setAttribute("#{key}", "#{value}");
543
+ ~)
544
+ end
545
+
546
+ send_js(%Q~ #{self.id}.appendChild(#{type}_#{@id}); ~)
547
+ send_js(%Q~ #{@@gui.active_id}.append(#{@id}); ~)
548
+ self.hide if hidden == true
549
+
550
+ @attributes = attributes
551
+ @@gui.elements[:"#{@id}"] = self
552
+ return self ###
553
+ end #initialize
554
+ end #class Svg
555
+
556
+
557
+ ##############################################################################
558
+ #----------------------------------------------------------------------------#
559
+ # Timer #
560
+ #----------------------------------------------------------------------------#
561
+ ##############################################################################
562
+ class Timer < Guindilla
563
+ attr_reader :id, :running, :secs
564
+
565
+ def initialize(type, seconds, &block)
566
+ @id = "timer_#{Time.now.hash.to_s.gsub('-', 'x')}"
567
+ @secs = seconds
568
+ @@gui.blocks[:"#{@id}"] = block if block_given?
569
+
570
+ if type == "interval"
571
+ send_js(%Q~
572
+ let #{@id} = setInterval(function(){
573
+ socket.send("#{@id}:!!:");
574
+ }, #{@secs * 1000});
575
+ ~)
576
+ elsif type == "timeout"
577
+ send_js(%Q~
578
+ let #{@id} = setTimeout(function(){
579
+ socket.send("#{@id}:!!:");
580
+ }, #{@secs * 1000});
581
+ ~)
582
+ end
583
+ @running = true
584
+ end
585
+
586
+ def start
587
+ send_js(%Q~
588
+ #{@id} = setInterval(function(){
589
+ socket.send("#{@id}:!!:");
590
+ }, #{@secs * 1000});
591
+ ~)
592
+ @running = true
593
+ end
594
+
595
+ def stop
596
+ send_js(%Q~ clearInterval(#{self.id}) ~)
597
+ @running = false
598
+ end
599
+
600
+ def toggle
601
+ self.running ? self.stop : self.start
602
+ end
603
+ end #class Timer
604
+
605
+ end #module GuindillaGUI