scarpe-wasm 0.1.0

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.
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)