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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +1 -0
- data/LICENSE.md +675 -0
- data/README.md +89 -0
- data/lib/guindilla_gui/guindilla_classes.rb +605 -0
- data/lib/guindilla_gui/guindilla_methods.rb +614 -0
- data/lib/guindilla_gui/resources/guindilla.html +35 -0
- data/lib/guindilla_gui/resources/guindilla.js +46 -0
- data/lib/guindilla_gui/resources/guindilla_gui.png +0 -0
- data/lib/guindilla_gui/resources/jquery/jquery-3.6.0.min.js +2 -0
- data/lib/guindilla_gui/resources/plotly/plotly-2.9.0.min.js +65 -0
- data/lib/guindilla_gui/version.rb +5 -0
- data/lib/guindilla_gui.rb +20 -0
- metadata +98 -0
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
|