scarpe 0.2.1 → 0.2.2

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