guindilla_gui 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
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