scarpe-wasm 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. checksums.yaml +7 -0
  2. data/CODE_OF_CONDUCT.md +84 -0
  3. data/Gemfile +13 -0
  4. data/LICENSE.txt +21 -0
  5. data/README.md +39 -0
  6. data/Rakefile +12 -0
  7. data/lib/scarpe/wasm/alert.rb +65 -0
  8. data/lib/scarpe/wasm/app.rb +107 -0
  9. data/lib/scarpe/wasm/arc.rb +54 -0
  10. data/lib/scarpe/wasm/background.rb +27 -0
  11. data/lib/scarpe/wasm/border.rb +24 -0
  12. data/lib/scarpe/wasm/button.rb +50 -0
  13. data/lib/scarpe/wasm/check.rb +29 -0
  14. data/lib/scarpe/wasm/control_interface.rb +147 -0
  15. data/lib/scarpe/wasm/control_interface_test.rb +234 -0
  16. data/lib/scarpe/wasm/dimensions.rb +22 -0
  17. data/lib/scarpe/wasm/document_root.rb +8 -0
  18. data/lib/scarpe/wasm/edit_box.rb +44 -0
  19. data/lib/scarpe/wasm/edit_line.rb +43 -0
  20. data/lib/scarpe/wasm/flow.rb +24 -0
  21. data/lib/scarpe/wasm/font.rb +36 -0
  22. data/lib/scarpe/wasm/html.rb +108 -0
  23. data/lib/scarpe/wasm/image.rb +41 -0
  24. data/lib/scarpe/wasm/line.rb +35 -0
  25. data/lib/scarpe/wasm/link.rb +29 -0
  26. data/lib/scarpe/wasm/list_box.rb +50 -0
  27. data/lib/scarpe/wasm/para.rb +90 -0
  28. data/lib/scarpe/wasm/radio.rb +34 -0
  29. data/lib/scarpe/wasm/shape.rb +68 -0
  30. data/lib/scarpe/wasm/slot.rb +81 -0
  31. data/lib/scarpe/wasm/spacing.rb +41 -0
  32. data/lib/scarpe/wasm/span.rb +66 -0
  33. data/lib/scarpe/wasm/stack.rb +24 -0
  34. data/lib/scarpe/wasm/star.rb +61 -0
  35. data/lib/scarpe/wasm/subscription_item.rb +50 -0
  36. data/lib/scarpe/wasm/text_widget.rb +30 -0
  37. data/lib/scarpe/wasm/version.rb +7 -0
  38. data/lib/scarpe/wasm/video.rb +42 -0
  39. data/lib/scarpe/wasm/wasm_calls.rb +118 -0
  40. data/lib/scarpe/wasm/wasm_local_display.rb +94 -0
  41. data/lib/scarpe/wasm/web_wrangler.rb +679 -0
  42. data/lib/scarpe/wasm/webview_relay_display.rb +220 -0
  43. data/lib/scarpe/wasm/widget.rb +228 -0
  44. data/lib/scarpe/wasm/wv_display_worker.rb +75 -0
  45. data/lib/scarpe/wasm.rb +46 -0
  46. data/scarpe-wasm.gemspec +39 -0
  47. data/sig/scarpe/wasm.rbs +6 -0
  48. metadata +92 -0
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "scarpe/base64"
4
+
5
+ class Scarpe
6
+ class WASMImage < WASMWidget
7
+ include Base64
8
+ def initialize(properties)
9
+ super
10
+
11
+ @url = valid_url?(@url) ? @url : "data:image/png;base64,#{encode_file_to_base64(@url)}"
12
+ end
13
+
14
+ def element
15
+ if @click
16
+ HTML.render do |h|
17
+ h.a(id: html_id, href: @click) { h.img(id: html_id, src: @url, style:) }
18
+ end
19
+ else
20
+ HTML.render do |h|
21
+ h.img(id: html_id, src: @url, style:)
22
+ end
23
+ end
24
+ end
25
+
26
+ private
27
+
28
+ def style
29
+ styles = {}
30
+
31
+ styles[:width] = Dimensions.length(@width) if @width
32
+ styles[:height] = Dimensions.length(@height) if @height
33
+
34
+ styles[:top] = Dimensions.length(@top) if @top
35
+ styles[:left] = Dimensions.length(@left) if @left
36
+ styles[:position] = "absolute" if @top || @left
37
+
38
+ styles
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Scarpe
4
+ class WASMLine < Scarpe::WASMWidget
5
+ def initialize(properties)
6
+ super(properties)
7
+ end
8
+
9
+ def element
10
+ HTML.render do |h|
11
+ h.div(id: html_id, style: style) do
12
+ h.svg(width: @x2, height: @y2) do
13
+ h.line(x1: @left, y1: @top, x2: @x2, y2: @y2, style: line_style)
14
+ end
15
+ end
16
+ end
17
+ end
18
+
19
+ private
20
+
21
+ def style
22
+ {
23
+ left: "#{@left}px",
24
+ top: "#{@top}px",
25
+ }
26
+ end
27
+
28
+ def line_style
29
+ {
30
+ stroke: @draw_context["stroke"],
31
+ "stroke-width": "4",
32
+ }
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Scarpe
4
+ class WASMLink < WASMWidget
5
+ def initialize(properties)
6
+ super
7
+
8
+ bind("click") do
9
+ send_self_event(event_name: "click")
10
+ end
11
+ end
12
+
13
+ def element
14
+ HTML.render do |h|
15
+ h.a(**attributes) do
16
+ @text
17
+ end
18
+ end
19
+ end
20
+
21
+ def attributes
22
+ {
23
+ id: html_id,
24
+ href: @click,
25
+ onclick: (handler_js_code("click") if @has_block),
26
+ }.compact
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Scarpe
4
+ class WASMListBox < Scarpe::WASMWidget
5
+ attr_reader :selected_item, :items, :height, :width
6
+
7
+ def initialize(properties)
8
+ super(properties)
9
+
10
+ # The JS handler sends a "change" event, which we forward to the Shoes widget tree
11
+ bind("change") do |new_item|
12
+ send_self_event(new_item, event_name: "change")
13
+ end
14
+ end
15
+
16
+ def properties_changed(changes)
17
+ selected = changes.delete("selected_item")
18
+ if selected
19
+ html_element.value = selected
20
+ end
21
+ super
22
+ end
23
+
24
+ def element
25
+ onchange = handler_js_code("change", "this.options[this.selectedIndex].value")
26
+
27
+ select_attrs = { id: html_id, onchange: onchange, style: style }
28
+ option_attrs = { value: nil, selected: false }
29
+
30
+ HTML.render do |h|
31
+ h.select(**select_attrs) do
32
+ items.each do |item|
33
+ h.option(**option_attrs, value: item, selected: (item == selected_item)) { item }
34
+ end
35
+ end
36
+ end
37
+ end
38
+
39
+ private
40
+
41
+ def style
42
+ styles = {}
43
+
44
+ styles[:height] = Dimensions.length(height) if height
45
+ styles[:width] = Dimensions.length(width) if width
46
+
47
+ styles.compact
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Scarpe
4
+ class WASMPara < WASMWidget
5
+ SIZES = {
6
+ inscription: 10,
7
+ ins: 10,
8
+ para: 12,
9
+ caption: 14,
10
+ tagline: 18,
11
+ subtitle: 26,
12
+ title: 34,
13
+ banner: 48,
14
+ }.freeze
15
+ private_constant :SIZES
16
+
17
+ def initialize(properties)
18
+ super
19
+ end
20
+
21
+ def properties_changed(changes)
22
+ items = changes.delete("text_items")
23
+ if items
24
+ html_element.inner_html = to_html
25
+ return
26
+ end
27
+
28
+ # Not deleting, so this will re-render
29
+ if changes["size"] && SIZES[@size.to_sym]
30
+ @size = @size.to_sym
31
+ end
32
+
33
+ super
34
+ end
35
+
36
+ def items_to_display_children(items)
37
+ return [] if items.nil?
38
+
39
+ items.map do |item|
40
+ if item.is_a?(String)
41
+ item
42
+ else
43
+ WASMDisplayService.instance.query_display_widget_for(item)
44
+ end
45
+ end
46
+ end
47
+
48
+ def element(&block)
49
+ HTML.render do |h|
50
+ h.p(**options, &block)
51
+ end
52
+ end
53
+
54
+ def to_html
55
+ @children ||= []
56
+
57
+ element { child_markup }
58
+ end
59
+
60
+ private
61
+
62
+ def child_markup
63
+ items_to_display_children(@text_items).map do |child|
64
+ if child.respond_to?(:to_html)
65
+ child.to_html
66
+ else
67
+ child.gsub("\n", "<br>")
68
+ end
69
+ end.join
70
+ end
71
+
72
+ def options
73
+ @html_attributes.merge(id: html_id, style: style)
74
+ end
75
+
76
+ def style
77
+ {
78
+ "color" => rgb_to_hex(@stroke),
79
+ "font-size" => font_size,
80
+ "font-family" => @font,
81
+ }.compact
82
+ end
83
+
84
+ def font_size
85
+ font_size = @size.is_a?(Symbol) ? SIZES[@size] : @size
86
+
87
+ Dimensions.length(font_size)
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Scarpe
4
+ class WASMRadio < Scarpe::WASMWidget
5
+ attr_reader :text
6
+
7
+ def initialize(properties)
8
+ super
9
+
10
+ bind("click") do
11
+ send_self_event(event_name: "click", target: shoes_linkable_id)
12
+ end
13
+ end
14
+
15
+ def properties_changed(changes)
16
+ items = changes.delete("checked")
17
+ html_element.toggle_input_button(items)
18
+
19
+ super
20
+ end
21
+
22
+ def element
23
+ HTML.render do |h|
24
+ h.input(type: :radio, id: html_id, onclick: handler_js_code("click"), name: group_name, value: "hmm #{text}", checked: @checked)
25
+ end
26
+ end
27
+
28
+ private
29
+
30
+ def group_name
31
+ @group || @parent
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Scarpe
4
+ # Should inherit from Slot?
5
+ class WASMShape < Scarpe::WASMWidget
6
+ def initialize(properties)
7
+ super(properties)
8
+ end
9
+
10
+ def to_html
11
+ @children ||= []
12
+ child_markup = @children.map(&:to_html).join
13
+
14
+ color = @draw_context["fill"] || "black"
15
+ self_markup = HTML.render do |h|
16
+ h.div(id: html_id, style: style) do
17
+ h.svg(width: "400", height: "500") do
18
+ h.path(d: path_from_shape_commands, style: "fill:#{color};stroke-width:2;")
19
+ end
20
+ end
21
+ end
22
+
23
+ # Put child markup first for backward compatibility, but I'm pretty sure this is wrong.
24
+ child_markup + self_markup
25
+ end
26
+
27
+ def element(&block)
28
+ color = @draw_context["fill"] || "black"
29
+ HTML.render do |h|
30
+ h.div(id: html_id, style: style) do
31
+ h.svg(width: "400", height: "500") do
32
+ h.path(d: path_from_shape_commands, style: "fill:#{color};stroke-width:2;")
33
+ end
34
+ block.call(h) if block_given?
35
+ end
36
+ end
37
+ end
38
+
39
+ private
40
+
41
+ # We have a set of Shoes shape commands, but we need SVG objects like paths.
42
+ def path_from_shape_commands
43
+ current_path = ""
44
+
45
+ @shape_commands.each do |cmd, *args|
46
+ case cmd
47
+ when "move_to"
48
+ x, y = *args
49
+ current_path += "M #{x} #{y} "
50
+ when "line_to"
51
+ x, y = *args
52
+ current_path += "L #{x} #{y} "
53
+ else
54
+ raise "Unknown shape command! #{cmd.inspect}"
55
+ end
56
+ end
57
+
58
+ current_path
59
+ end
60
+
61
+ def style
62
+ {
63
+ width: "400",
64
+ height: "900",
65
+ }
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Scarpe
4
+ class WASMSlot < Scarpe::WASMWidget
5
+ include Scarpe::WASMBackground
6
+ include Scarpe::WASMBorder
7
+ include Scarpe::WASMSpacing
8
+
9
+ def initialize(properties)
10
+ @event_callbacks = {}
11
+
12
+ super
13
+ end
14
+
15
+ def element(&block)
16
+ HTML.render do |h|
17
+ h.div(attributes.merge(id: html_id, style: style), &block)
18
+ end
19
+ end
20
+
21
+ def set_event_callback(obj, event_name, js_code)
22
+ event_name = event_name.to_s
23
+ @event_callbacks[event_name] ||= {}
24
+ if @event_callbacks[event_name][obj]
25
+ raise "Can't have two callbacks on the same event, from the same object, on the same parent!"
26
+ end
27
+
28
+ @event_callbacks[event_name][obj] = js_code
29
+
30
+ update_dom_event(event_name)
31
+ end
32
+
33
+ def remove_event_callback(obj, event_name)
34
+ event_name = event_name.to_s
35
+ @event_callbacks[event_name] ||= {}
36
+ @event_callbacks[event_name].delete(obj)
37
+
38
+ update_dom_event(event_name)
39
+ end
40
+
41
+ def remove_event_callbacks(obj)
42
+ changed = []
43
+
44
+ @event_callbacks.each do |event_name, items|
45
+ changed << event_name if items.delete(obj)
46
+ end
47
+
48
+ changed.each { |event_name| update_dom_event(event_name) }
49
+ end
50
+
51
+ protected
52
+
53
+ def update_dom_event(event_name)
54
+ html_element.set_attribute(event_name, @event_callbacks[event_name].values.join(";"))
55
+ end
56
+
57
+ def attributes
58
+ attr = {}
59
+
60
+ @event_callbacks.each do |event_name, handlers|
61
+ attr[event_name] = handlers.values.join(";")
62
+ end
63
+
64
+ attr
65
+ end
66
+
67
+ def style
68
+ styles = super
69
+
70
+ styles["margin-top"] = @margin_top if @margin_top
71
+ styles["margin-bottom"] = @margin_bottom if @margin_bottom
72
+ styles["margin-left"] = @margin_left if @margin_left
73
+ styles["margin-right"] = @margin_right if @margin_right
74
+
75
+ styles[:width] = Dimensions.length(@width) if @width
76
+ styles[:height] = Dimensions.length(@height) if @height
77
+
78
+ styles
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Scarpe
4
+ module WASMSpacing
5
+ SPACING_DIRECTIONS = [:left, :right, :top, :bottom]
6
+
7
+ def style
8
+ styles = defined?(super) ? super : {}
9
+
10
+ extract_spacing_styles_for(:margin, styles, @margin)
11
+ extract_spacing_styles_for(:padding, styles, @padding)
12
+
13
+ styles
14
+ end
15
+
16
+ def extract_spacing_styles_for(attribute, styles, values)
17
+ values ||= spacing_values_from_options(attribute)
18
+
19
+ case values
20
+ when Hash
21
+ values.each do |direction, value|
22
+ styles["#{attribute}-#{direction}"] = Dimensions.length(value)
23
+ end
24
+ when Array
25
+ SPACING_DIRECTIONS.zip(values).to_h.compact.each do |direction, value|
26
+ styles["#{attribute}-#{direction}"] = Dimensions.length(value)
27
+ end
28
+ else
29
+ styles[attribute] = Dimensions.length(values)
30
+ end
31
+
32
+ styles.compact!
33
+ end
34
+
35
+ def spacing_values_from_options(attribute)
36
+ SPACING_DIRECTIONS.map do |direction|
37
+ @options.delete("#{attribute}_#{direction}".to_sym)
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Scarpe
4
+ class WASMSpan < Scarpe::WASMWidget
5
+ SIZES = {
6
+ inscription: 10,
7
+ ins: 10,
8
+ span: 12,
9
+ caption: 14,
10
+ tagline: 18,
11
+ subtitle: 26,
12
+ title: 34,
13
+ banner: 48,
14
+ }.freeze
15
+ private_constant :SIZES
16
+
17
+ def initialize(properties)
18
+ super
19
+ end
20
+
21
+ def properties_changed(changes)
22
+ text = changes.delete("text")
23
+ if text
24
+ html_element.inner_html = text
25
+ return
26
+ end
27
+
28
+ # Not deleting, so this will re-render
29
+ if changes["size"] && SIZES[@size.to_sym]
30
+ @size = @size.to_sym
31
+ end
32
+
33
+ super
34
+ end
35
+
36
+ def element(&block)
37
+ HTML.render do |h|
38
+ h.span(**options, &block)
39
+ end
40
+ end
41
+
42
+ def to_html
43
+ element { @text }
44
+ end
45
+
46
+ private
47
+
48
+ def options
49
+ @html_attributes.merge(id: html_id, style: style)
50
+ end
51
+
52
+ def style
53
+ {
54
+ "color" => @stroke,
55
+ "font-size" => font_size,
56
+ "font-family" => @font,
57
+ }.compact
58
+ end
59
+
60
+ def font_size
61
+ font_size = @size.is_a?(Symbol) ? SIZES[@size] : @size
62
+
63
+ Dimensions.length(font_size)
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Scarpe
4
+ class WASMStack < Scarpe::WASMSlot
5
+ def get_style
6
+ style
7
+ end
8
+
9
+ protected
10
+
11
+ def style
12
+ styles = super
13
+
14
+ styles[:display] = "flex"
15
+ styles["flex-direction"] = "column"
16
+ styles["align-content"] = "flex-start"
17
+ styles["justify-content"] = "flex-start"
18
+ styles["align-items"] = "flex-start"
19
+ styles["overflow"] = "auto" if @scroll
20
+
21
+ styles
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Scarpe
4
+ class WASMStar < Scarpe::WASMWidget
5
+ def initialize(properties)
6
+ super(properties)
7
+ end
8
+
9
+ def element(&block)
10
+ fill = @draw_context["fill"]
11
+ stroke = @draw_context["stroke"]
12
+ fill = "black" if fill == ""
13
+ stroke = "black" if stroke == ""
14
+ HTML.render do |h|
15
+ h.div(id: html_id, style: style) do
16
+ h.svg(width: @outer, height: @outer, style: "fill:#{fill};") do
17
+ h.polygon(points: star_points, style: "stroke:#{stroke};stroke-width:2")
18
+ end
19
+ block.call(h) if block_given?
20
+ end
21
+ end
22
+ end
23
+
24
+ private
25
+
26
+ def style
27
+ {
28
+ width: Dimensions.length(@width),
29
+ height: Dimensions.length(@height),
30
+ }
31
+ end
32
+
33
+ def star_points
34
+ get_star_points.join(",")
35
+ end
36
+
37
+ def get_star_points
38
+ angle = 2 * Math::PI / @points
39
+ coordinates = []
40
+
41
+ @points.times do |i|
42
+ outer_angle = i * angle
43
+ inner_angle = outer_angle + angle / 2
44
+
45
+ coordinates.concat(get_coordinates(outer_angle, inner_angle))
46
+ end
47
+
48
+ coordinates
49
+ end
50
+
51
+ def get_coordinates(outer_angle, inner_angle)
52
+ outer_x = @outer / 2 + Math.cos(outer_angle) * @outer / 2
53
+ outer_y = @outer / 2 + Math.sin(outer_angle) * @outer / 2
54
+
55
+ inner_x = @outer / 2 + Math.cos(inner_angle) * @inner / 2
56
+ inner_y = @outer / 2 + Math.sin(inner_angle) * @inner / 2
57
+
58
+ [outer_x, outer_y, inner_x, inner_y]
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Scarpe::WASMSubscriptionItem < Scarpe::WASMWidget
4
+ def initialize(properties)
5
+ super
6
+
7
+ bind(@shoes_api_name) do |*args|
8
+ send_self_event(*args, event_name: @shoes_api_name)
9
+ end
10
+ end
11
+
12
+ def element
13
+ ""
14
+ end
15
+
16
+ # This will get called once we know the parent, which is useful for events
17
+ # like hover, where our subscription is likely to depend on what our parent is.
18
+ def set_parent(new_parent)
19
+ super
20
+
21
+ case @shoes_api_name
22
+ when "motion"
23
+ # TODO: what do we do for whole-screen mousemove outside the window?
24
+ # Those should be set on body, which right now doesn't have a widget.
25
+ # TODO: figure out how to handle alt and meta keys - does Shoes3 recognise those?
26
+ new_parent.set_event_callback(
27
+ self,
28
+ "onmousemove",
29
+ handler_js_code(
30
+ @shoes_api_name,
31
+ "arguments[0].x",
32
+ "arguments[0].y",
33
+ "arguments[0].ctrlKey",
34
+ "arguments[0].shiftKey",
35
+ ),
36
+ )
37
+ when "hover"
38
+ new_parent.set_event_callback(self, "onmouseenter", handler_js_code(@shoes_api_name))
39
+ when "click"
40
+ new_parent.set_event_callback(self, "onclick", handler_js_code(@shoes_api_name, "arguments[0].button", "arguments[0].x", "arguments[0].y"))
41
+ else
42
+ raise "Unknown Shoes event API: #{@shoes_api_name}!"
43
+ end
44
+ end
45
+
46
+ def destroy_self
47
+ @parent.remove_event_callbacks(self)
48
+ super
49
+ end
50
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Scarpe
4
+ class WASMTextWidget < Scarpe::WASMWidget
5
+ end
6
+
7
+ class << self
8
+ def default_wasm_text_widget_with(element)
9
+ wasm_class_name = "WASM#{element.capitalize}"
10
+ wasm_widget_class = Class.new(Scarpe::WASMTextWidget) do
11
+ def initialize(properties)
12
+ class_name = self.class.name.split("::")[-1]
13
+ @html_tag = class_name.delete_prefix("WASM").downcase
14
+ super
15
+ end
16
+
17
+ def element
18
+ HTML.render do |h|
19
+ h.send(@html_tag) { @content.to_s }
20
+ end
21
+ end
22
+ end
23
+ Scarpe.const_set wasm_class_name, wasm_widget_class
24
+ end
25
+ end
26
+ end
27
+
28
+ Scarpe.default_wasm_text_widget_with(:code)
29
+ Scarpe.default_wasm_text_widget_with(:em)
30
+ Scarpe.default_wasm_text_widget_with(:strong)