scarpe 0.2.1 → 0.2.2

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 (240) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +4 -0
  3. data/.yardopts +11 -0
  4. data/Gemfile +3 -0
  5. data/Gemfile.lock +112 -0
  6. data/README.md +31 -24
  7. data/Rakefile +13 -1
  8. data/docs/yard/catscradle.md +44 -0
  9. data/docs/yard/template/default/fulldoc/html/setup.rb +13 -0
  10. data/docs/yard/template/default/layout/html/setup.rb +9 -0
  11. data/examples/background_with_image.rb +16 -0
  12. data/examples/bloopsaphone/working/bronx_army_knife.rb +66 -0
  13. data/examples/bloopsaphone/working/morning_serenity.rb +21 -0
  14. data/examples/bloopsaphone/working/simpsons_theme_song_by_why.rb +6 -4
  15. data/examples/button_go_away.rb +1 -1
  16. data/examples/check.rb +18 -0
  17. data/examples/clear_and_append.rb +24 -0
  18. data/examples/download_and_show_image.rb +28 -0
  19. data/examples/edit_box.rb +3 -5
  20. data/examples/fonts.rb +2 -2
  21. data/examples/get_headers.rb +10 -0
  22. data/examples/highlander.rb +2 -0
  23. data/examples/link.rb +2 -2
  24. data/examples/local_fonts.rb +4 -0
  25. data/examples/local_images.rb +4 -0
  26. data/examples/motion_events.rb +20 -0
  27. data/examples/parse_xl_funnies.rb +58 -0
  28. data/examples/radio/radio.rb +16 -0
  29. data/examples/radio/radio_groups.rb +18 -0
  30. data/examples/radio/radio_same_slot.rb +6 -0
  31. data/examples/ruby_racer.rb +13 -15
  32. data/examples/selfitude.rb +18 -0
  33. data/examples/shapes/shapes_fill.rb +4 -3
  34. data/examples/shoes_school.rb +2 -4
  35. data/examples/show_hide.rb +6 -0
  36. data/examples/skip_ci/change_my_audio_source.rb +21 -0
  37. data/examples/skip_ci/guitar_fretboard.rb +137 -0
  38. data/examples/video.rb +10 -0
  39. data/exe/scarpe +42 -66
  40. data/fonts/Pacifico.ttf +0 -0
  41. data/lacci/Gemfile +22 -0
  42. data/lacci/Gemfile.lock +72 -0
  43. data/lacci/Rakefile +12 -0
  44. data/lacci/lacci.gemspec +37 -0
  45. data/lacci/lib/lacci/scarpe_cli.rb +70 -0
  46. data/lacci/lib/lacci/scarpe_core.rb +21 -0
  47. data/lacci/lib/lacci/version.rb +13 -0
  48. data/lacci/lib/shoes/app.rb +264 -0
  49. data/{lib/scarpe → lacci/lib/shoes}/background.rb +1 -1
  50. data/{lib/scarpe → lacci/lib/shoes}/border.rb +1 -1
  51. data/{lib/scarpe → lacci/lib/shoes}/colors.rb +1 -1
  52. data/lacci/lib/shoes/constants.rb +29 -0
  53. data/{lib/scarpe → lacci/lib/shoes}/display_service.rb +40 -45
  54. data/lacci/lib/shoes/download.rb +123 -0
  55. data/lacci/lib/shoes/log.rb +71 -0
  56. data/lacci/lib/shoes/spacing.rb +9 -0
  57. data/{lib/scarpe → lacci/lib/shoes}/widget.rb +63 -43
  58. data/{lib/scarpe → lacci/lib/shoes/widgets}/alert.rb +3 -3
  59. data/{lib/scarpe → lacci/lib/shoes/widgets}/arc.rb +7 -5
  60. data/{lib/scarpe → lacci/lib/shoes/widgets}/button.rb +3 -3
  61. data/lacci/lib/shoes/widgets/check.rb +28 -0
  62. data/lacci/lib/shoes/widgets/document_root.rb +20 -0
  63. data/{lib/scarpe → lacci/lib/shoes/widgets}/edit_box.rb +10 -5
  64. data/{lib/scarpe → lacci/lib/shoes/widgets}/edit_line.rb +2 -2
  65. data/lacci/lib/shoes/widgets/flow.rb +22 -0
  66. data/lacci/lib/shoes/widgets/font.rb +14 -0
  67. data/{lib/scarpe → lacci/lib/shoes/widgets}/image.rb +3 -7
  68. data/lacci/lib/shoes/widgets/line.rb +18 -0
  69. data/{lib/scarpe → lacci/lib/shoes/widgets}/link.rb +2 -2
  70. data/{lib/scarpe → lacci/lib/shoes/widgets}/list_box.rb +2 -2
  71. data/{lib/scarpe → lacci/lib/shoes/widgets}/para.rb +4 -26
  72. data/lacci/lib/shoes/widgets/radio.rb +35 -0
  73. data/lacci/lib/shoes/widgets/shape.rb +37 -0
  74. data/lacci/lib/shoes/widgets/slot.rb +75 -0
  75. data/{lib/scarpe → lacci/lib/shoes/widgets}/span.rb +2 -2
  76. data/lacci/lib/shoes/widgets/stack.rb +24 -0
  77. data/{lib/scarpe → lacci/lib/shoes/widgets}/star.rb +6 -9
  78. data/lacci/lib/shoes/widgets/subscription_item.rb +60 -0
  79. data/lacci/lib/shoes/widgets/text_widget.rb +51 -0
  80. data/lacci/lib/shoes/widgets/video.rb +15 -0
  81. data/lacci/lib/shoes/widgets.rb +29 -0
  82. data/lacci/lib/shoes.rb +127 -0
  83. data/lacci/test/test_colors.rb +39 -0
  84. data/lacci/test/test_helper.rb +9 -0
  85. data/lacci/test/test_lacci.rb +9 -0
  86. data/lib/scarpe/cats_cradle.rb +249 -0
  87. data/lib/scarpe/evented_assertions.rb +88 -0
  88. data/lib/scarpe/version.rb +1 -1
  89. data/lib/scarpe/wv/alert.rb +3 -2
  90. data/lib/scarpe/wv/app.rb +30 -8
  91. data/lib/scarpe/wv/arc.rb +5 -6
  92. data/lib/scarpe/wv/background.rb +10 -1
  93. data/lib/scarpe/wv/border.rb +5 -3
  94. data/lib/scarpe/wv/button.rb +11 -9
  95. data/lib/scarpe/wv/check.rb +29 -0
  96. data/lib/scarpe/wv/control_interface.rb +14 -20
  97. data/lib/scarpe/wv/control_interface_test.rb +13 -28
  98. data/lib/scarpe/wv/document_root.rb +3 -45
  99. data/lib/scarpe/wv/edit_box.rb +5 -7
  100. data/lib/scarpe/wv/edit_line.rb +2 -2
  101. data/lib/scarpe/wv/flow.rb +10 -20
  102. data/lib/scarpe/wv/font.rb +36 -0
  103. data/lib/scarpe/wv/html.rb +3 -2
  104. data/lib/scarpe/wv/image.rb +7 -2
  105. data/lib/scarpe/wv/line.rb +4 -7
  106. data/lib/scarpe/wv/link.rb +1 -0
  107. data/lib/scarpe/wv/list_box.rb +3 -3
  108. data/lib/scarpe/wv/para.rb +16 -14
  109. data/lib/scarpe/wv/radio.rb +34 -0
  110. data/lib/scarpe/wv/shape.rb +44 -8
  111. data/lib/scarpe/wv/slot.rb +81 -0
  112. data/lib/scarpe/wv/spacing.rb +1 -1
  113. data/lib/scarpe/wv/span.rb +10 -8
  114. data/lib/scarpe/wv/stack.rb +10 -30
  115. data/lib/scarpe/wv/star.rb +11 -12
  116. data/lib/scarpe/wv/subscription_item.rb +50 -0
  117. data/lib/scarpe/wv/video.rb +34 -0
  118. data/lib/scarpe/wv/web_wrangler.rb +238 -58
  119. data/lib/scarpe/wv/webview_local_display.rb +27 -5
  120. data/lib/scarpe/wv/webview_relay_display.rb +18 -119
  121. data/lib/scarpe/wv/webview_relay_util.rb +143 -0
  122. data/lib/scarpe/wv/widget.rb +80 -11
  123. data/lib/scarpe/wv/wv_display_worker.rb +17 -4
  124. data/lib/scarpe/wv.rb +33 -4
  125. data/lib/scarpe/wv_local.rb +1 -1
  126. data/lib/scarpe/wv_relay.rb +1 -1
  127. data/lib/scarpe.rb +3 -32
  128. data/scarpe-components/.gitignore +1 -0
  129. data/scarpe-components/Gemfile +22 -0
  130. data/scarpe-components/README.md +35 -0
  131. data/scarpe-components/Rakefile +12 -0
  132. data/scarpe-components/lib/scarpe/components/base64.rb +29 -0
  133. data/scarpe-components/lib/scarpe/components/file_helpers.rb +65 -0
  134. data/scarpe-components/lib/scarpe/components/modular_logger.rb +113 -0
  135. data/scarpe-components/lib/scarpe/components/print_logger.rb +43 -0
  136. data/{lib/scarpe → scarpe-components/lib/scarpe/components}/promises.rb +102 -35
  137. data/scarpe-components/lib/scarpe/components/segmented_file_loader.rb +170 -0
  138. data/scarpe-components/lib/scarpe/components/unit_test_helpers.rb +217 -0
  139. data/scarpe-components/lib/scarpe/components/version.rb +7 -0
  140. data/scarpe-components/scarpe-components.gemspec +38 -0
  141. data/scarpe-components/test/test_components.rb +9 -0
  142. data/scarpe-components/test/test_helper.rb +23 -0
  143. data/scarpe-components/test/test_promises.rb +260 -0
  144. data/scarpe-components/test/test_segmented_app_files.rb +182 -0
  145. data/{lib/scarpe → spikes}/glibui/widget.rb +2 -2
  146. data/{lib/scarpe → spikes}/glibui.rb +1 -1
  147. data/templates/basic_class_template.erb +1 -1
  148. data/templates/class_template_with_event_bind.erb +1 -1
  149. data/templates/class_template_with_shapes.erb +1 -1
  150. data/templates/webview_template.erb +0 -3
  151. metadata +151 -118
  152. data/examples/fill.rb +0 -25
  153. data/examples/legacy/not_checked/shoes-contrib/basic/class-book.yaml +0 -387
  154. data/examples/legacy/not_checked/shoes-contrib/good/good-clock.rb +0 -51
  155. data/examples/legacy/not_checked/shoes-contrib/good/good-follow.rb +0 -26
  156. data/examples/legacy/not_checked/shoes-contrib/good/good-reminder.rb +0 -174
  157. data/examples/legacy/not_checked/shoes-contrib/good/good-vjot.rb +0 -56
  158. data/examples/legacy/not_checked/shoes-contrib/simple/simple-timer.rb +0 -13
  159. data/examples/legacy/not_checked/shoes-dep-samples/good-clock.rb +0 -51
  160. data/examples/legacy/not_checked/shoes-dep-samples/good-follow.rb +0 -26
  161. data/examples/legacy/not_checked/shoes-dep-samples/good-reminder.rb +0 -174
  162. data/examples/legacy/not_checked/shoes-dep-samples/good-vjot.rb +0 -56
  163. data/examples/legacy/not_checked/shoes-dep-samples/simple-accordion.rb +0 -75
  164. data/examples/legacy/not_checked/shoes-dep-samples/simple-anim-shapes.rb +0 -17
  165. data/examples/legacy/not_checked/shoes-dep-samples/simple-anim-text.rb +0 -13
  166. data/examples/legacy/not_checked/shoes-dep-samples/simple-arc.rb +0 -23
  167. data/examples/legacy/not_checked/shoes-dep-samples/simple-bounce.rb +0 -24
  168. data/examples/legacy/not_checked/shoes-dep-samples/simple-calc.rb +0 -70
  169. data/examples/legacy/not_checked/shoes-dep-samples/simple-chipmunk.rb +0 -26
  170. data/examples/legacy/not_checked/shoes-dep-samples/simple-control-sizes.rb +0 -24
  171. data/examples/legacy/not_checked/shoes-dep-samples/simple-curve.rb +0 -26
  172. data/examples/legacy/not_checked/shoes-dep-samples/simple-dialogs.rb +0 -29
  173. data/examples/legacy/not_checked/shoes-dep-samples/simple-draw.rb +0 -13
  174. data/examples/legacy/not_checked/shoes-dep-samples/simple-editor.rb +0 -28
  175. data/examples/legacy/not_checked/shoes-dep-samples/simple-form.rb +0 -28
  176. data/examples/legacy/not_checked/shoes-dep-samples/simple-form.shy +0 -0
  177. data/examples/legacy/not_checked/shoes-dep-samples/simple-mask.rb +0 -21
  178. data/examples/legacy/not_checked/shoes-dep-samples/simple-menu.rb +0 -31
  179. data/examples/legacy/not_checked/shoes-dep-samples/simple-menu1.rb +0 -35
  180. data/examples/legacy/not_checked/shoes-dep-samples/simple-rubygems.rb +0 -29
  181. data/examples/legacy/not_checked/shoes-dep-samples/simple-slide.rb +0 -45
  182. data/examples/legacy/not_checked/shoes-dep-samples/simple-sphere.rb +0 -28
  183. data/examples/legacy/not_checked/shoes-dep-samples/simple-sqlite3.rb +0 -13
  184. data/examples/legacy/not_checked/shoes-dep-samples/simple-timer.rb +0 -13
  185. data/examples/legacy/not_checked/shoes-dep-samples/simple-video.rb +0 -13
  186. data/examples/legacy/not_checked/simple/anim-text.rb +0 -13
  187. data/examples/legacy/not_checked/simple/arc.rb +0 -23
  188. data/examples/legacy/not_checked/simple/bounce.rb +0 -24
  189. data/examples/legacy/not_checked/simple/chipmunk.rb +0 -26
  190. data/examples/legacy/not_checked/simple/curve.rb +0 -26
  191. data/examples/legacy/not_checked/simple/dialogs.rb +0 -29
  192. data/examples/legacy/not_checked/simple/downloader.rb +0 -40
  193. data/examples/legacy/not_checked/simple/draw.rb +0 -13
  194. data/examples/legacy/not_checked/simple/mask.rb +0 -21
  195. data/examples/legacy/not_checked/simple/slide.rb +0 -45
  196. data/examples/legacy/not_checked/simple/sphere.rb +0 -28
  197. data/lib/constants.rb +0 -5
  198. data/lib/scarpe/app.rb +0 -78
  199. data/lib/scarpe/document_root.rb +0 -20
  200. data/lib/scarpe/fill.rb +0 -23
  201. data/lib/scarpe/flow.rb +0 -19
  202. data/lib/scarpe/line.rb +0 -25
  203. data/lib/scarpe/logger.rb +0 -155
  204. data/lib/scarpe/shape.rb +0 -19
  205. data/lib/scarpe/spacing.rb +0 -9
  206. data/lib/scarpe/stack.rb +0 -70
  207. data/lib/scarpe/text_widget.rb +0 -42
  208. data/lib/scarpe/unit_test_helpers.rb +0 -163
  209. data/lib/scarpe/widgets.rb +0 -30
  210. data/lib/scarpe/wv/fill.rb +0 -30
  211. data/lib/scarpe/wv/shape_helper.rb +0 -44
  212. data/scarpe-0.2.0.gem +0 -0
  213. /data/{lib/scarpe → spikes}/glibui/README.md +0 -0
  214. /data/{lib/scarpe → spikes}/glibui/alert.rb +0 -0
  215. /data/{lib/scarpe → spikes}/glibui/app.rb +0 -0
  216. /data/{lib/scarpe → spikes}/glibui/background.rb +0 -0
  217. /data/{lib/scarpe → spikes}/glibui/border.rb +0 -0
  218. /data/{lib/scarpe → spikes}/glibui/button.rb +0 -0
  219. /data/{lib/scarpe → spikes}/glibui/dimensions.rb +0 -0
  220. /data/{lib/scarpe → spikes}/glibui/document_root.rb +0 -0
  221. /data/{lib/scarpe → spikes}/glibui/edit_box.rb +0 -0
  222. /data/{lib/scarpe → spikes}/glibui/edit_line.rb +0 -0
  223. /data/{lib/scarpe → spikes}/glibui/flow.rb +0 -0
  224. /data/{lib/scarpe → spikes}/glibui/html.rb +0 -0
  225. /data/{lib/scarpe → spikes}/glibui/image.rb +0 -0
  226. /data/{lib/scarpe → spikes}/glibui/link.rb +0 -0
  227. /data/{lib/scarpe → spikes}/glibui/local_display.rb +0 -0
  228. /data/{lib/scarpe → spikes}/glibui/para.rb +0 -0
  229. /data/{lib/scarpe → spikes}/glibui/spacing.rb +0 -0
  230. /data/{lib/scarpe → spikes}/glibui/stack.rb +0 -0
  231. /data/{lib/scarpe → spikes}/glibui/text_widget.rb +0 -0
  232. /data/{lib/scarpe → spikes}/libui/alert.rb +0 -0
  233. /data/{lib/scarpe → spikes}/libui/button.rb +0 -0
  234. /data/{lib/scarpe → spikes}/libui/colors.rb +0 -0
  235. /data/{lib/scarpe → spikes}/libui/core.rb +0 -0
  236. /data/{lib/scarpe → spikes}/libui/flow.rb +0 -0
  237. /data/{lib/scarpe → spikes}/libui/libui.rb +0 -0
  238. /data/{lib/scarpe → spikes}/libui/notepad.md +0 -0
  239. /data/{lib/scarpe → spikes}/libui/para.rb +0 -0
  240. /data/{lib/scarpe → spikes}/libui/stack.rb +0 -0
@@ -1,34 +1,37 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "shape_helper"
4
-
5
3
  class Scarpe
6
4
  class WebviewStar < Scarpe::WebviewWidget
7
- include ShapeHelper
8
5
  def initialize(properties)
9
6
  super(properties)
10
7
  end
11
8
 
12
9
  def element(&block)
10
+ fill = @draw_context["fill"]
11
+ stroke = @draw_context["stroke"]
12
+ fill = "black" if fill == ""
13
+ stroke = "black" if stroke == ""
13
14
  HTML.render do |h|
14
15
  h.div(id: html_id, style: style) do
15
- h.svg(width: @outer, height: @outer, style: "fill:#{@color};") do
16
- h.polygon(points: star_points, style: "stroke:#{stroke_color};stroke-width:2")
16
+ h.svg(width: @outer, height: @outer, style: "fill:#{fill};") do
17
+ h.polygon(points: star_points, style: "stroke:#{stroke};stroke-width:2")
17
18
  end
18
19
  block.call(h) if block_given?
19
20
  end
20
21
  end
21
22
  end
22
23
 
23
- private
24
+ protected
24
25
 
25
26
  def style
26
- {
27
+ super.merge({
27
28
  width: Dimensions.length(@width),
28
29
  height: Dimensions.length(@height),
29
- }
30
+ })
30
31
  end
31
32
 
33
+ private
34
+
32
35
  def star_points
33
36
  get_star_points.join(",")
34
37
  end
@@ -56,9 +59,5 @@ class Scarpe
56
59
 
57
60
  [outer_x, outer_y, inner_x, inner_y]
58
61
  end
59
-
60
- def stroke_color
61
- "black"
62
- end
63
62
  end
64
63
  end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Scarpe::WebviewSubscriptionItem < Scarpe::WebviewWidget
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,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Scarpe
4
+ class WebviewVideo < Scarpe::WebviewWidget
5
+ SUPPORTED_FORMATS = {
6
+ "video/mp4" => [".mp4"],
7
+ "video/webp" => [".webp"],
8
+ "video/quicktime" => [".mov"],
9
+ "video/x-matroska" => [".mkv"],
10
+ # Add more formats and their associated file extensions if needed
11
+ }.freeze
12
+
13
+ def initialize(properties)
14
+ @url = properties[:url]
15
+ super
16
+ end
17
+
18
+ def element
19
+ HTML.render do |h|
20
+ h.video(id: html_id, style: style, controls: true) do
21
+ supported_formats.each do |format|
22
+ h.source(src: @url, type: format)
23
+ end
24
+ end
25
+ end
26
+ end
27
+
28
+ private
29
+
30
+ def supported_formats
31
+ SUPPORTED_FORMATS.select { |_format, extensions| extensions.include?(File.extname(@url)) }.keys
32
+ end
33
+ end
34
+ end
@@ -9,12 +9,46 @@ require "cgi"
9
9
  # use setup-mode callbacks.
10
10
 
11
11
  class Scarpe
12
+ # The Scarpe WebWrangler, for Webview, manages a lot of Webviews quirks. It provides
13
+ # a simpler underlying abstraction for DOMWrangler and the Webview widgets.
14
+ # Webview can be picky - if you send it too many messages, it can crash. If the
15
+ # messages you send it are too large, it can crash. If you don't return control
16
+ # to its event loop, it can crash. It doesn't save references to all event handlers,
17
+ # so if you don't save references to them, garbage collection will cause it to
18
+ # crash.
19
+ #
20
+ # As well, Webview only supports asynchronous JS code evaluation with no value
21
+ # being returned. One of WebWrangler's responsibilities is to make asynchronous
22
+ # JS calls, detect when they return a value or time out, and make the result clear
23
+ # to other Scarpe code.
24
+ #
25
+ # Some Webview API functions will crash on some platforms if called from a
26
+ # background thread. Webview will halt all background threads when it runs its
27
+ # event loop. So it's best to assume no Ruby background threads will be available
28
+ # while Webview is running. If a Ruby app wants ongoing work to occur, that work
29
+ # should be registered via a heartbeat handler on the Webview.
30
+ #
31
+ # A WebWrangler is initially in Setup mode, where the underlying Webview exists
32
+ # but does not yet control the event loop. In Setup mode you can bind JS functions,
33
+ # set up initialization code, but nothing is yet running.
34
+ #
35
+ # Once run() is called on WebWrangler, we will hand control of the event loop to
36
+ # the Webview. This will also stop any background threads in Ruby.
12
37
  class WebWrangler
13
- include Scarpe::Log
38
+ include Shoes::Log
14
39
 
40
+ # Whether Webview has been started. Once Webview is running you can't add new
41
+ # Javascript bindings. Until it is running, you can't use eval to run Javascript.
15
42
  attr_reader :is_running
43
+
44
+ # Once Webview is marked terminated, it's attempting to shut down. If we get
45
+ # events (e.g. heartbeats) after that, we should ignore them.
16
46
  attr_reader :is_terminated
17
- attr_reader :heartbeat # This is the heartbeat duration in seconds, usually fractional
47
+
48
+ # This is the time between heartbeats in seconds, usually fractional
49
+ attr_reader :heartbeat
50
+
51
+ # A reference to the control_interface that manages internal Scarpe Webview events.
18
52
  attr_reader :control_interface
19
53
 
20
54
  # This error indicates a problem when running ConfirmedEval
@@ -25,7 +59,7 @@ class Scarpe
25
59
  end
26
60
  end
27
61
 
28
- # We got an error running the supplied JS code string in confirmed_eval
62
+ # An error running the supplied JS code string in confirmed_eval
29
63
  class JSRuntimeError < JSEvalError
30
64
  end
31
65
 
@@ -37,20 +71,27 @@ class Scarpe
37
71
  class InternalError < JSEvalError
38
72
  end
39
73
 
40
- # This is the JS function name for eval results
74
+ # This is the JS function name for eval results (internal-only)
41
75
  EVAL_RESULT = "scarpeAsyncEvalResult"
42
76
 
43
- # Allow a half-second for Webview to finish our JS eval before we decide it's not going to
77
+ # Allow this many seconds for Webview to finish our JS eval before we decide it's not going to
44
78
  EVAL_DEFAULT_TIMEOUT = 0.5
45
79
 
46
- def initialize(title:, width:, height:, resizable: false, debug: false, heartbeat: 0.1)
80
+ # Create a new WebWrangler.
81
+ #
82
+ # @param title [String] window title
83
+ # @param width [Integer] window width in pixels
84
+ # @param height [Integer] window height in pixels
85
+ # @param resizable [Boolean] whether the window should be resizable by the user
86
+ # @param heartbeat [Float] time between heartbeats in seconds
87
+ def initialize(title:, width:, height:, resizable: false, heartbeat: 0.1)
47
88
  log_init("WV::WebWrangler")
48
89
 
49
90
  @log.debug("Creating WebWrangler...")
50
91
 
51
- # For now, always allow inspect element
92
+ # For now, always allow inspect element, so pass debug: true
52
93
  @webview = WebviewRuby::Webview.new debug: true
53
- @webview = Scarpe::LoggedWrapper.new(@webview, "WebviewAPI") if debug
94
+ @webview = Shoes::LoggedWrapper.new(@webview, "WebviewAPI") if ENV["SCARPE_DEBUG"]
54
95
  @init_refs = {} # Inits don't go away so keep a reference to them to prevent GC
55
96
 
56
97
  @title = title
@@ -59,8 +100,8 @@ class Scarpe
59
100
  @resizable = resizable
60
101
  @heartbeat = heartbeat
61
102
 
62
- # Better to have a single setInterval than many when we don't care too much
63
- # about the timing.
103
+ # JS setInterval uses RPC and is quite expensive. For many periodic operations
104
+ # we can group them under a single heartbeat handler and avoid extra JS calls or RPC.
64
105
  @heartbeat_handlers = []
65
106
 
66
107
  # Need to keep track of which WebView Javascript evals are still pending,
@@ -100,16 +141,28 @@ class Scarpe
100
141
 
101
142
  ### Setup-mode Callbacks
102
143
 
144
+ # Bind a Javascript-callable function by name. When JS calls the function,
145
+ # an async message is sent to Ruby via RPC and will eventually cause the
146
+ # block to be called. This method only works in setup mode, before the
147
+ # underlying Webview has been told to run.
148
+ #
149
+ # @param name [String] the Javascript name for the new function
150
+ # @yield The Ruby block to be invoked when JS calls the function
103
151
  def bind(name, &block)
104
152
  raise "App is running, javascript binding no longer works because it uses WebView init!" if @is_running
105
153
 
106
154
  @webview.bind(name, &block)
107
155
  end
108
156
 
157
+ # Request that this block of code be run initially when the Webview is run.
158
+ # This operates via #init and will not work if Webview is already running.
159
+ #
160
+ # @param name [String] the Javascript name for the init function
161
+ # @yield The Ruby block to be invoked when Webview runs
109
162
  def init_code(name, &block)
110
163
  raise "App is running, javascript init no longer works!" if @is_running
111
164
 
112
- # Save a reference to the init string so that it goesn't get GC'd
165
+ # Save a reference to the init string so that it doesn't get GC'd
113
166
  code_str = "#{name}();"
114
167
  @init_refs[name] = code_str
115
168
 
@@ -118,8 +171,14 @@ class Scarpe
118
171
  end
119
172
 
120
173
  # Run the specified code periodically, every "interval" seconds.
121
- # If interface is unspecified, run per-heartbeat, which is very
122
- # slightly more efficient.
174
+ # If interval is unspecified, run per-heartbeat. This avoids extra
175
+ # RPC and Javascript overhead. This may use the #init mechanism,
176
+ # so it should be invoked when the WebWrangler is in setup mode,
177
+ # before the Webview is running.
178
+ #
179
+ # @param name [String] the name of the Javascript init function, if needed
180
+ # @param interval [Float] the duration between invoking this block
181
+ # @yield the Ruby block to invoke periodically
123
182
  def periodic_code(name, interval = heartbeat, &block)
124
183
  if interval == heartbeat
125
184
  @heartbeat_handlers << block
@@ -143,7 +202,7 @@ class Scarpe
143
202
 
144
203
  # Running callbacks
145
204
 
146
- # js_eventually is a simple JS evaluation. On syntax error, nothing happens.
205
+ # js_eventually is a native Webview JS evaluation. On syntax error, nothing happens.
147
206
  # On runtime error, execution stops at the error with no further
148
207
  # effect or notification. This is rarely what you want.
149
208
  # The js_eventually code is run asynchronously, returning neither error
@@ -151,10 +210,13 @@ class Scarpe
151
210
  #
152
211
  # This method does *not* return a promise, and there is no way to track
153
212
  # its progress or its success or failure.
213
+ #
214
+ # @param code [String] the Javascript code to attempt to execute
215
+ # @return [void]
154
216
  def js_eventually(code)
155
217
  raise "WebWrangler isn't running, eval doesn't work!" unless @is_running
156
218
 
157
- @log.warning "Deprecated: please do NOT use js_eventually, it's basically never what you want!" unless ENV["CI"]
219
+ @log.warn "Deprecated: please do NOT use js_eventually, it's basically never what you want!" unless ENV["CI"]
158
220
 
159
221
  @webview.eval(code)
160
222
  end
@@ -163,19 +225,25 @@ class Scarpe
163
225
  # promise which will be fulfilled or rejected after the JS executes
164
226
  # or times out.
165
227
  #
166
- # Note that we *both* care whether the JS has finished after it was
228
+ # We *both* care whether the JS has finished after it was
167
229
  # scheduled *and* whether it ever got scheduled at all. If it
168
- # depends on tasks that never fulfill or reject then it may wait
169
- # in limbo, potentially forever.
230
+ # depends on tasks that never fulfill or reject then it will
231
+ # raise a timed-out exception.
170
232
  #
171
- # Right now we can't/don't handle arguments from previous fulfilled
172
- # promises. To do that, we'd probably need to know we were passing
173
- # in a JS function.
174
- EVAL_OPTS = [:timeout, :wait_for]
175
- def eval_js_async(code, opts = {})
176
- bad_opts = opts.keys - EVAL_OPTS
177
- raise("Bad options given to eval_with_handler! #{bad_opts.inspect}") unless bad_opts.empty?
178
-
233
+ # Right now we can't/don't pass arguments through from previous fulfilled
234
+ # promises. To do that, you can schedule the JS to run after the
235
+ # other promises succeed.
236
+ #
237
+ # Webview does not allow interacting with a JS eval once it has
238
+ # been scheduled. So there is no way to guarantee that a piece of JS has
239
+ # not executed, or will not execute in the future. A timeout exception
240
+ # only means that WebWrangler will no longer wait for confirmation or
241
+ # fulfill the promise if the JS later completes.
242
+ #
243
+ # @param code [String] the Javascript code to execute
244
+ # @param timeout [Float] how long to allow before raising a timeout exception
245
+ # @param wait_for [Array<Promise>] promises that must complete successfully before this JS is scheduled
246
+ def eval_js_async(code, timeout: EVAL_DEFAULT_TIMEOUT, wait_for: [])
179
247
  unless @is_running
180
248
  raise "WebWrangler isn't running, so evaluating JS won't work!"
181
249
  end
@@ -192,9 +260,8 @@ class Scarpe
192
260
 
193
261
  # We'll need this inside the promise-scheduling block
194
262
  pending_evals = @pending_evals
195
- timeout = opts[:timeout] || EVAL_DEFAULT_TIMEOUT
196
263
 
197
- promise = Scarpe::Promise.new(parents: (opts[:wait_for] || [])) do
264
+ promise = Scarpe::Promise.new(parents: wait_for) do
198
265
  # Are we mid-shutdown?
199
266
  if @webview
200
267
  wrapped_code = WebWrangler.js_wrapped_code(code, this_eval_serial)
@@ -220,6 +287,16 @@ class Scarpe
220
287
  promise
221
288
  end
222
289
 
290
+ # This method takes a piece of Javascript code and wraps it in the WebWrangler
291
+ # boilerplate to see if it parses successfully, run it, and see if it succeeds.
292
+ # This function would normally be used by testing code, to mock Webview and
293
+ # watch for code being run. Javascript code containing backticks
294
+ # could potentially break this abstraction layer, which would cause the resulting
295
+ # code to fail to parse and Webview would return no error. This should not be
296
+ # used for random or untrusted code.
297
+ #
298
+ # @param code [String] the Javascript code to be wrapped
299
+ # @param eval_id [Integer] the tracking code to use when calling EVAL_RESULT
223
300
  def self.js_wrapped_code(code, eval_id)
224
301
  <<~JS_CODE
225
302
  (function() {
@@ -268,11 +345,11 @@ class Scarpe
268
345
  end
269
346
  end
270
347
 
271
- # TODO: would be good to keep 'tombstone' results for awhile after timeout, maybe up to around a minute,
272
- # so we can detect if we're timing things out and then having them return successfully after a delay.
273
- # Then we could adjust the timeouts. We could also check if later serial numbers have returned, and time
274
- # out earlier serial numbers... *if* we're sure Webview will always execute JS evals in order.
275
- # This all adds complexity, though. For now, do timeouts on a simple max duration.
348
+ # @todo would be good to keep 'tombstone' results for awhile after timeout, maybe up to around a minute,
349
+ # so we can detect if we're timing things out and then having them return successfully after a delay.
350
+ # Then we could adjust the timeouts. We could also check if later serial numbers have returned, and time
351
+ # out earlier serial numbers... *if* we're sure Webview will always execute JS evals in order.
352
+ # This all adds complexity, though. For now, do timeouts on a simple max duration.
276
353
  def time_out_eval_results
277
354
  t_now = Time.now
278
355
  timed_out_from_scheduling = @pending_evals.keys.select do |id|
@@ -304,8 +381,7 @@ class Scarpe
304
381
  public
305
382
 
306
383
  # After setup, we call run to go to "running" mode.
307
- # No more setup callbacks, only running callbacks.
308
-
384
+ # No more setup callbacks should be called, only running callbacks.
309
385
  def run
310
386
  @log.debug("Run...")
311
387
 
@@ -329,6 +405,8 @@ class Scarpe
329
405
  @webview = nil
330
406
  end
331
407
 
408
+ # Request destruction of WebWrangler, including terminating the underlying
409
+ # Webview and (when possible) destroying it.
332
410
  def destroy
333
411
  @log.debug("Destroying WebWrangler...")
334
412
  @log.debug(" (WebWrangler was already terminated)") if @is_terminated
@@ -389,69 +467,112 @@ class Scarpe
389
467
 
390
468
  public
391
469
 
392
- # For now, the WebWrangler gets a bunch of fairly low-level requests
393
- # to mess with the HTML DOM. This needs to be turned into a nicer API,
394
- # but first we'll get it all into one place and see what we're doing.
395
-
396
470
  # Replace the entire DOM - return a promise for when this has been done.
397
471
  # This will often get rid of smaller changes in the queue, which is
398
472
  # a good thing since they won't have to be run.
473
+ #
474
+ # @param html_text [String] The new HTML for the new full DOM
475
+ # @return [Scarpe::Promise] a promise that will be fulfilled when the update is complete
399
476
  def replace(html_text)
400
477
  @dom_wrangler.request_replace(html_text)
401
478
  end
402
479
 
403
480
  # Request a DOM change - return a promise for when this has been done.
481
+ # If a full replacement (see #replace) is requested, this change may
482
+ # be lost. Only use it for changes that are preserved by a full update.
483
+ #
484
+ # @param js [String] the JS to execute to alter the DOM
485
+ # @return [Scarpe::Promise] a promise that will be fulfilled when the update is complete
404
486
  def dom_change(js)
405
487
  @dom_wrangler.request_change(js)
406
488
  end
407
489
 
408
490
  # Return whether the DOM is, right this moment, confirmed to be fully
409
491
  # up to date or not.
492
+ #
493
+ # @return [Boolean] true if the window is fully updated, false if changes are pending
410
494
  def dom_fully_updated?
411
495
  @dom_wrangler.fully_updated?
412
496
  end
413
497
 
414
498
  # Return a promise that will be fulfilled when all current DOM changes
415
- # have committed (but not necessarily any future DOM changes.)
499
+ # have committed. If other changes are requested before these
500
+ # complete, the promise will ***not*** wait for them. If you wish to
501
+ # wait until all changes from all sources have completed, use
502
+ # #promise_dom_fully_updated.
503
+ #
504
+ # @return [Scarpe::Promise] a promise that will be fulfilled when all current changes complete
416
505
  def dom_promise_redraw
417
506
  @dom_wrangler.promise_redraw
418
507
  end
419
508
 
420
509
  # Return a promise which will be fulfilled the next time the DOM is
421
- # fully up to date. Note that a slow trickle of changes can make this
422
- # take a long time, since it is *not* only changes up to this point.
423
- # If you want to know that some specific change is done, it's often
424
- # easiest to use the promise returned by dom_change(), which will
425
- # be fulfilled when that specific change commits.
510
+ # fully up to date. A slow trickle of changes can make this
511
+ # take a long time, since it includes all current and future changes,
512
+ # not just changes before this call.
513
+ #
514
+ # If you want to know that some specific individual change is done, it's often
515
+ # easiest to use the promise returned by #dom_change, which will
516
+ # be fulfilled when that specific change is verified complete.
517
+ #
518
+ # If no changes are pending, promise_dom_fully_updated will
519
+ # return a promise that is already fulfilled.
520
+ #
521
+ # @return [Scarpe::Promise] a promise that will be fulfilled when all changes are complete
426
522
  def promise_dom_fully_updated
427
523
  @dom_wrangler.promise_fully_updated
428
524
  end
429
525
 
526
+ # DOMWrangler will frequently schedule and confirm small JS updates.
527
+ # A handler registered with on_every_redraw will be called after each
528
+ # small update.
529
+ #
530
+ # @yield Called after each update or batch of updates is verified complete
531
+ # @return [void]
430
532
  def on_every_redraw(&block)
431
533
  @dom_wrangler.on_every_redraw(&block)
432
534
  end
433
535
  end
434
536
  end
435
537
 
436
- # Leaving DOM changes as "meh, async, we'll see when it happens" is terrible for testing.
437
- # Instead, we need to track whether particular changes have committed yet or not.
438
- # So we add a single gateway for all DOM changes, and we make sure its work is done
439
- # before we consider a redraw complete.
440
- #
441
- # DOMWrangler batches up changes - it's fine to have a redraw "in flight" and have
442
- # changes waiting to catch the next bus. But we don't want more than one in flight,
443
- # since it seems like having too many pending RPC requests can crash Webview. So:
444
- # one redraw scheduled and one redraw promise waiting around, at maximum.
445
538
  class Scarpe
446
539
  class WebWrangler
540
+ # Leaving DOM changes as "meh, async, we'll see when it happens" is terrible for testing.
541
+ # Instead, we need to track whether particular changes have committed yet or not.
542
+ # So we add a single gateway for all DOM changes, and we make sure its work is done
543
+ # before we consider a redraw complete.
544
+ #
545
+ # DOMWrangler batches up changes into fewer RPC calls. It's fine to have a redraw
546
+ # "in flight" and have changes waiting to catch the next bus. But we don't want more
547
+ # than one in flight, since it seems like having too many pending RPC requests can
548
+ # crash Webview. So we allow one redraw scheduled and one redraw promise waiting,
549
+ # at maximum.
550
+ #
551
+ # A WebWrangler will create and wrap a DOMWrangler, serving as the interface
552
+ # for all DOM operations.
553
+ #
554
+ # A batch of DOMWrangler changes may be removed if a full update is scheduled. That
555
+ # update is considered to replace the previous incremental changes. Any changes that
556
+ # need to execute even if a full update happens should be scheduled through
557
+ # WebWrangler#eval_js_async, not DOMWrangler.
447
558
  class DOMWrangler
448
- include Scarpe::Log
559
+ include Shoes::Log
449
560
 
561
+ # Changes that have not yet been executed
450
562
  attr_reader :waiting_changes
563
+
564
+ # A Scarpe::Promise for JS that has been scheduled to execute but is not yet verified complete
451
565
  attr_reader :pending_redraw_promise
566
+
567
+ # A Scarpe::Promise for waiting changes - it will be fulfilled when all waiting changes
568
+ # have been verified complete, or when a full redraw that removed them has been
569
+ # verified complete. If many small changes are scheduled, the same promise will be
570
+ # returned for many of them.
452
571
  attr_reader :waiting_redraw_promise
453
572
 
454
- def initialize(web_wrangler, debug: false)
573
+ # Create a DOMWrangler that is paired with a WebWrangler. The WebWrangler is
574
+ # treated as an underlying abstraction for reliable JS evaluation.
575
+ def initialize(web_wrangler)
455
576
  log_init("WV::WebWrangler::DOMWrangler")
456
577
 
457
578
  @wrangler = web_wrangler
@@ -510,6 +631,10 @@ class Scarpe
510
631
  @redraw_handlers << block
511
632
  end
512
633
 
634
+ # promise_redraw returns a Scarpe::Promise which will be fulfilled after all current
635
+ # pending or waiting changes have completed. This may require creating a new
636
+ # promise.
637
+ #
513
638
  # What are the states of redraw?
514
639
  # "empty" - no waiting promise, no pending-redraw promise, no pending changes
515
640
  # "pending only" - no waiting promise, but we have a pending redraw with some changes; it hasn't committed yet
@@ -643,37 +768,92 @@ class Scarpe
643
768
  end
644
769
  end
645
770
 
646
- # For now we don't need one of these to add DOM elements, just to manipulate them
647
- # after initial render.
648
771
  class Scarpe
649
772
  class WebWrangler
773
+ # An ElementWrangler provides a way for a Widget to manipulate is DOM element(s)
774
+ # via their HTML IDs. The most straightforward Widgets can have a single HTML ID
775
+ # and use a single ElementWrangler to make any needed changes.
776
+ #
777
+ # For now we don't need an ElementWrangler to add DOM elements, just to manipulate them
778
+ # after initial render. New DOM objects for Widgets are normally added via full
779
+ # redraws rather than incremental updates.
780
+ #
781
+ # Any changes made via ElementWrangler may be cancelled if a full redraw occurs,
782
+ # since it is assumed that small DOM manipulations are no longer needed. If a
783
+ # change would need to be made even if a full redraw occurred, it should be
784
+ # scheduled via WebWrangler#eval_js_async, not via an ElementWrangler.
650
785
  class ElementWrangler
651
786
  attr_reader :html_id
652
787
 
788
+ # Create an ElementWrangler for the given HTML ID
789
+ #
790
+ # @param html_id [String] the HTML ID for the DOM element
653
791
  def initialize(html_id)
654
792
  @webwrangler = WebviewDisplayService.instance.wrangler
655
793
  @html_id = html_id
656
794
  end
657
795
 
796
+ # Return a promise that will be fulfilled when all changes scheduled via
797
+ # this ElementWrangler are verified complete.
798
+ #
799
+ # @return [Scarpe::Promise] a promise that will be fulfilled when scheduled changes are complete
658
800
  def promise_update
659
801
  @webwrangler.dom_promise_redraw
660
802
  end
661
803
 
804
+ # Update the JS DOM element's value. The given Ruby value will be converted to string and assigned in backquotes.
805
+ #
806
+ # @param new_value [String] the new value
807
+ # @return [Scarpe::Promise] a promise that will be fulfilled when the change is complete
662
808
  def value=(new_value)
663
809
  @webwrangler.dom_change("document.getElementById('" + html_id + "').value = `" + new_value + "`; true")
664
810
  end
665
811
 
812
+ # Update the JS DOM element's inner_text. The given Ruby value will be converted to string and assigned in single-quotes.
813
+ #
814
+ # @param new_text [String] the new inner_text
815
+ # @return [Scarpe::Promise] a promise that will be fulfilled when the change is complete
666
816
  def inner_text=(new_text)
667
817
  @webwrangler.dom_change("document.getElementById('" + html_id + "').innerText = '" + new_text + "'; true")
668
818
  end
669
819
 
820
+ # Update the JS DOM element's inner_html. The given Ruby value will be converted to string and assigned in backquotes.
821
+ #
822
+ # @param new_html [String] the new inner_html
823
+ # @return [Scarpe::Promise] a promise that will be fulfilled when the change is complete
670
824
  def inner_html=(new_html)
671
825
  @webwrangler.dom_change("document.getElementById(\"" + html_id + "\").innerHTML = `" + new_html + "`; true")
672
826
  end
673
827
 
828
+ # Update the JS DOM element's inner_html. The given Ruby value will be inspected and assigned.
829
+ #
830
+ # @param attribute [String] the attribute name
831
+ # @param value [String] the new attribute value
832
+ # @return [Scarpe::Promise] a promise that will be fulfilled when the change is complete
833
+ def set_attribute(attribute, value)
834
+ @webwrangler.dom_change("document.getElementById(\"" + html_id + "\").setAttribute(" + attribute.inspect + "," + value.inspect + "); true")
835
+ end
836
+
837
+ # Update an attribute of the JS DOM element's style. The given Ruby value will be inspected and assigned.
838
+ #
839
+ # @param style_attr [String] the style attribute name
840
+ # @param value [String] the new style attribute value
841
+ # @return [Scarpe::Promise] a promise that will be fulfilled when the change is complete
842
+ def set_style(style_attr, value)
843
+ @webwrangler.dom_change("document.getElementById(\"" + html_id + "\").style.#{style_attr} = " + value.inspect + "; true")
844
+ end
845
+
846
+ # Remove the specified DOM element
847
+ #
848
+ # @return [Scarpe::Promise] a promise that wil be fulfilled when the element is removed
674
849
  def remove
675
850
  @webwrangler.dom_change("document.getElementById('" + html_id + "').remove(); true")
676
851
  end
852
+
853
+ def toggle_input_button(mark)
854
+ checked_value = mark ? "true" : "false"
855
+ @webwrangler.dom_change("document.getElementById('#{html_id}').checked = #{checked_value};")
856
+ end
677
857
  end
678
858
  end
679
859
  end