scarpe 0.2.1 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (347) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +4 -0
  3. data/.yardopts +12 -0
  4. data/CHANGELOG.md +16 -2
  5. data/Gemfile +3 -0
  6. data/Gemfile.lock +116 -0
  7. data/README.md +53 -30
  8. data/Rakefile +13 -1
  9. data/docs/yard/catscradle.md +44 -0
  10. data/docs/yard/template/default/fulldoc/html/setup.rb +13 -0
  11. data/docs/yard/template/default/layout/html/setup.rb +9 -0
  12. data/examples/animate.rb +20 -0
  13. data/examples/arrow.rb +10 -0
  14. data/examples/background_with_image.rb +16 -0
  15. data/examples/bloopsaphone/working/bronx_army_knife.rb +66 -0
  16. data/examples/bloopsaphone/working/morning_serenity.rb +21 -0
  17. data/examples/bloopsaphone/working/simpsons_theme_song_by_why.rb +6 -4
  18. data/examples/btn_tooltip.rb +7 -0
  19. data/examples/button_go_away.rb +1 -1
  20. data/examples/button_style_changed.rb +7 -0
  21. data/examples/button_styles_default.rb +6 -0
  22. data/examples/check.rb +18 -0
  23. data/examples/clear_and_append.rb +24 -0
  24. data/examples/download_and_show_image.rb +28 -0
  25. data/examples/edit_box.rb +3 -5
  26. data/examples/fonts.rb +2 -2
  27. data/examples/gen.rb +8 -8
  28. data/examples/get_headers.rb +10 -0
  29. data/examples/highlander.rb +4 -2
  30. data/examples/legacy/README.md +6 -0
  31. data/examples/legacy/not_checked/shoes-contrib/basic/shoes-notes.rb +1 -1
  32. data/examples/legacy/not_checked/simple/anim-shapes.rb +1 -1
  33. data/examples/legacy/not_checked/speedometer_app.rb +55 -0
  34. data/examples/legacy/working/simple/image-icon.rb +3 -0
  35. data/examples/legacy/{not_checked → working}/simple/image.rb +1 -1
  36. data/examples/link.rb +2 -2
  37. data/examples/list_box_choose.rb +17 -0
  38. data/examples/local_assets/local_file_server.rb +82 -0
  39. data/examples/local_assets/sample.gif +0 -0
  40. data/examples/local_assets/sample.mp4 +0 -0
  41. data/examples/local_fonts.rb +4 -0
  42. data/examples/local_images.rb +3 -0
  43. data/examples/motion_events.rb +20 -0
  44. data/examples/para/para_text.rb +14 -0
  45. data/examples/parse_xl_funnies.rb +58 -0
  46. data/examples/progress.rb +31 -0
  47. data/examples/radio/radio.rb +16 -0
  48. data/examples/radio/radio_groups.rb +18 -0
  49. data/examples/radio/radio_same_slot.rb +6 -0
  50. data/examples/rect.rb +4 -0
  51. data/examples/rotate_shapes.rb +17 -0
  52. data/examples/ruby_racer.rb +13 -15
  53. data/examples/selfitude.rb +18 -0
  54. data/examples/shapes/shapes_fill.rb +4 -3
  55. data/examples/shoes_school.rb +2 -4
  56. data/examples/show_hide.rb +6 -0
  57. data/examples/simpler-menu.rb +21 -0
  58. data/examples/skip_ci/change_my_audio_source.rb +21 -0
  59. data/examples/skip_ci/guitar_fretboard.rb +137 -0
  60. data/examples/video.rb +10 -0
  61. data/exe/scarpe +43 -66
  62. data/fonts/Pacifico.ttf +0 -0
  63. data/lacci/Gemfile +24 -0
  64. data/lacci/Gemfile.lock +79 -0
  65. data/lacci/Rakefile +12 -0
  66. data/lacci/lacci.gemspec +37 -0
  67. data/lacci/lib/lacci/scarpe_cli.rb +71 -0
  68. data/lacci/lib/lacci/scarpe_core.rb +22 -0
  69. data/lacci/lib/lacci/version.rb +13 -0
  70. data/lacci/lib/scarpe/niente/app.rb +23 -0
  71. data/lacci/lib/scarpe/niente/display_service.rb +62 -0
  72. data/lacci/lib/scarpe/niente/drawable.rb +57 -0
  73. data/lacci/lib/scarpe/niente/logger.rb +29 -0
  74. data/lacci/lib/scarpe/niente/shoes_spec.rb +87 -0
  75. data/lacci/lib/scarpe/niente.rb +20 -0
  76. data/lacci/lib/shoes/app.rb +309 -0
  77. data/{lib/scarpe → lacci/lib/shoes}/background.rb +2 -2
  78. data/{lib/scarpe → lacci/lib/shoes}/border.rb +2 -2
  79. data/lacci/lib/shoes/builtins.rb +63 -0
  80. data/lacci/lib/shoes/changelog.rb +52 -0
  81. data/{lib/scarpe → lacci/lib/shoes}/colors.rb +3 -1
  82. data/lacci/lib/shoes/constants.rb +47 -0
  83. data/{lib/scarpe → lacci/lib/shoes}/display_service.rb +71 -53
  84. data/lacci/lib/shoes/download.rb +123 -0
  85. data/lacci/lib/shoes/drawable.rb +380 -0
  86. data/lacci/lib/shoes/drawables/arc.rb +49 -0
  87. data/lacci/lib/shoes/drawables/arrow.rb +41 -0
  88. data/lacci/lib/shoes/drawables/button.rb +73 -0
  89. data/lacci/lib/shoes/drawables/check.rb +29 -0
  90. data/lacci/lib/shoes/drawables/document_root.rb +20 -0
  91. data/lacci/lib/shoes/drawables/edit_box.rb +29 -0
  92. data/{lib/scarpe → lacci/lib/shoes/drawables}/edit_line.rb +6 -6
  93. data/lacci/lib/shoes/drawables/flow.rb +22 -0
  94. data/{lib/scarpe → lacci/lib/shoes/drawables}/image.rb +7 -11
  95. data/lacci/lib/shoes/drawables/line.rb +20 -0
  96. data/lacci/lib/shoes/drawables/link.rb +34 -0
  97. data/lacci/lib/shoes/drawables/list_box.rb +56 -0
  98. data/lacci/lib/shoes/drawables/para.rb +118 -0
  99. data/lacci/lib/shoes/drawables/progress.rb +14 -0
  100. data/lacci/lib/shoes/drawables/radio.rb +33 -0
  101. data/lacci/lib/shoes/drawables/rect.rb +17 -0
  102. data/lacci/lib/shoes/drawables/shape.rb +36 -0
  103. data/lacci/lib/shoes/drawables/slot.rb +87 -0
  104. data/{lib/scarpe → lacci/lib/shoes/drawables}/span.rb +8 -7
  105. data/lacci/lib/shoes/drawables/stack.rb +26 -0
  106. data/lacci/lib/shoes/drawables/star.rb +50 -0
  107. data/lacci/lib/shoes/drawables/subscription_item.rb +93 -0
  108. data/lacci/lib/shoes/drawables/text_drawable.rb +63 -0
  109. data/lacci/lib/shoes/drawables/video.rb +16 -0
  110. data/lacci/lib/shoes/drawables/widget.rb +69 -0
  111. data/lacci/lib/shoes/drawables.rb +31 -0
  112. data/lacci/lib/shoes/errors.rb +28 -0
  113. data/lacci/lib/shoes/log.rb +71 -0
  114. data/lacci/lib/shoes/ruby_extensions.rb +15 -0
  115. data/lacci/lib/shoes/spacing.rb +9 -0
  116. data/lacci/lib/shoes-spec.rb +93 -0
  117. data/lacci/lib/shoes.rb +147 -0
  118. data/lacci/test/test_colors.rb +39 -0
  119. data/lacci/test/test_helper.rb +63 -0
  120. data/lacci/test/test_lacci.rb +18 -0
  121. data/lacci/test/test_shoes_errors.rb +49 -0
  122. data/lib/scarpe/cats_cradle.rb +271 -0
  123. data/lib/scarpe/errors.rb +77 -0
  124. data/lib/scarpe/evented_assertions.rb +121 -0
  125. data/lib/scarpe/shoes_spec.rb +181 -0
  126. data/lib/scarpe/version.rb +2 -2
  127. data/lib/scarpe/wv/app.rb +45 -23
  128. data/lib/scarpe/wv/arc.rb +4 -48
  129. data/lib/scarpe/wv/arrow.rb +9 -0
  130. data/lib/scarpe/wv/button.rb +7 -33
  131. data/lib/scarpe/wv/check.rb +27 -0
  132. data/lib/scarpe/wv/control_interface.rb +32 -40
  133. data/lib/scarpe/wv/document_root.rb +66 -31
  134. data/lib/scarpe/wv/drawable.rb +273 -0
  135. data/lib/scarpe/wv/edit_box.rb +4 -19
  136. data/lib/scarpe/wv/edit_line.rb +4 -18
  137. data/lib/scarpe/wv/flow.rb +2 -28
  138. data/lib/scarpe/wv/image.rb +10 -25
  139. data/lib/scarpe/wv/line.rb +3 -28
  140. data/lib/scarpe/wv/link.rb +3 -15
  141. data/lib/scarpe/wv/list_box.rb +6 -29
  142. data/lib/scarpe/wv/para.rb +11 -28
  143. data/lib/scarpe/wv/progress.rb +19 -0
  144. data/lib/scarpe/wv/radio.rb +33 -0
  145. data/lib/scarpe/wv/rect.rb +13 -0
  146. data/lib/scarpe/wv/shape.rb +41 -10
  147. data/lib/scarpe/wv/slot.rb +64 -0
  148. data/lib/scarpe/wv/span.rb +3 -25
  149. data/lib/scarpe/wv/stack.rb +2 -38
  150. data/lib/scarpe/wv/star.rb +3 -54
  151. data/lib/scarpe/wv/subscription_item.rb +84 -0
  152. data/lib/scarpe/wv/text_drawable.rb +32 -0
  153. data/lib/scarpe/wv/video.rb +34 -0
  154. data/lib/scarpe/wv/web_wrangler.rb +449 -299
  155. data/lib/scarpe/wv/webview_local_display.rb +63 -26
  156. data/lib/scarpe/wv/webview_relay_display.rb +24 -125
  157. data/lib/scarpe/wv/webview_relay_util.rb +140 -0
  158. data/lib/scarpe/wv/wv_display_worker.rb +19 -6
  159. data/lib/scarpe/wv.rb +76 -14
  160. data/lib/scarpe/wv_local.rb +1 -1
  161. data/lib/scarpe/wv_relay.rb +1 -1
  162. data/lib/scarpe.rb +4 -32
  163. data/logger/debug_web_wrangler.json +1 -1
  164. data/logger/scarpe_wv_test.json +1 -1
  165. data/scarpe-components/.gitignore +1 -0
  166. data/scarpe-components/Gemfile +22 -0
  167. data/scarpe-components/Gemfile.lock +86 -0
  168. data/scarpe-components/README.md +35 -0
  169. data/scarpe-components/Rakefile +12 -0
  170. data/scarpe-components/lib/scarpe/components/base64.rb +25 -0
  171. data/scarpe-components/lib/scarpe/components/calzini/alert.rb +49 -0
  172. data/scarpe-components/lib/scarpe/components/calzini/art_widgets.rb +203 -0
  173. data/scarpe-components/lib/scarpe/components/calzini/button.rb +39 -0
  174. data/scarpe-components/lib/scarpe/components/calzini/misc.rb +146 -0
  175. data/scarpe-components/lib/scarpe/components/calzini/para.rb +35 -0
  176. data/scarpe-components/lib/scarpe/components/calzini/slots.rb +155 -0
  177. data/scarpe-components/lib/scarpe/components/calzini/text_widgets.rb +65 -0
  178. data/scarpe-components/lib/scarpe/components/calzini.rb +149 -0
  179. data/scarpe-components/lib/scarpe/components/errors.rb +20 -0
  180. data/scarpe-components/lib/scarpe/components/file_helpers.rb +66 -0
  181. data/scarpe-components/lib/scarpe/components/html.rb +131 -0
  182. data/scarpe-components/lib/scarpe/components/minitest_export_reporter.rb +75 -0
  183. data/scarpe-components/lib/scarpe/components/minitest_import_runnable.rb +98 -0
  184. data/scarpe-components/lib/scarpe/components/minitest_result.rb +86 -0
  185. data/scarpe-components/lib/scarpe/components/modular_logger.rb +113 -0
  186. data/scarpe-components/lib/scarpe/components/print_logger.rb +47 -0
  187. data/{lib/scarpe → scarpe-components/lib/scarpe/components}/promises.rb +115 -48
  188. data/scarpe-components/lib/scarpe/components/segmented_file_loader.rb +189 -0
  189. data/scarpe-components/lib/scarpe/components/string_helpers.rb +10 -0
  190. data/scarpe-components/lib/scarpe/components/tiranti.rb +225 -0
  191. data/scarpe-components/lib/scarpe/components/unit_test_helpers.rb +257 -0
  192. data/scarpe-components/lib/scarpe/components/version.rb +7 -0
  193. data/scarpe-components/scarpe-components.gemspec +38 -0
  194. data/scarpe-components/test/calzini/test_calzini_alert.rb +30 -0
  195. data/scarpe-components/test/calzini/test_calzini_art_drawables.rb +105 -0
  196. data/scarpe-components/test/calzini/test_calzini_button.rb +52 -0
  197. data/scarpe-components/test/calzini/test_calzini_misc.rb +115 -0
  198. data/scarpe-components/test/calzini/test_calzini_para.rb +37 -0
  199. data/scarpe-components/test/calzini/test_calzini_slots.rb +130 -0
  200. data/scarpe-components/test/calzini/test_calzini_text_drawables.rb +41 -0
  201. data/scarpe-components/test/mtr_data/exception.json +1 -0
  202. data/scarpe-components/test/mtr_data/fail_with_message.json +1 -0
  203. data/scarpe-components/test/mtr_data/skipped_no_message.json +1 -0
  204. data/scarpe-components/test/mtr_data/skipped_w_msg.json +1 -0
  205. data/scarpe-components/test/mtr_data/succeed_2_asserts.json +1 -0
  206. data/scarpe-components/test/test_components.rb +9 -0
  207. data/scarpe-components/test/test_dimensions.rb +26 -0
  208. data/scarpe-components/test/test_helper.rb +43 -0
  209. data/scarpe-components/test/test_html.rb +65 -0
  210. data/scarpe-components/test/test_minitest_result.rb +61 -0
  211. data/scarpe-components/test/test_promises.rb +261 -0
  212. data/scarpe-components/test/test_segmented_app_files.rb +184 -0
  213. data/scarpegen.rb +14 -14
  214. data/sig/scarpe.rbs +1 -1
  215. data/{lib/scarpe → spikes}/glibui/widget.rb +2 -2
  216. data/{lib/scarpe → spikes}/glibui.rb +1 -1
  217. data/templates/basic_class_template.erb +13 -14
  218. data/templates/class_template_with_event_bind.erb +4 -4
  219. data/templates/class_template_with_shapes.erb +8 -17
  220. data/templates/example_template.erb +1 -1
  221. data/templates/module_template.erb +4 -4
  222. data/templates/webview_template.erb +3 -5
  223. metadata +236 -145
  224. data/examples/fill.rb +0 -25
  225. data/examples/legacy/not_checked/shoes-contrib/basic/class-book.yaml +0 -387
  226. data/examples/legacy/not_checked/shoes-contrib/elements/image-icon.rb +0 -3
  227. data/examples/legacy/not_checked/shoes-contrib/good/good-clock.rb +0 -51
  228. data/examples/legacy/not_checked/shoes-contrib/good/good-follow.rb +0 -26
  229. data/examples/legacy/not_checked/shoes-contrib/good/good-reminder.rb +0 -174
  230. data/examples/legacy/not_checked/shoes-contrib/good/good-vjot.rb +0 -56
  231. data/examples/legacy/not_checked/shoes-contrib/simple/simple-timer.rb +0 -13
  232. data/examples/legacy/not_checked/shoes-dep-samples/good-clock.rb +0 -51
  233. data/examples/legacy/not_checked/shoes-dep-samples/good-follow.rb +0 -26
  234. data/examples/legacy/not_checked/shoes-dep-samples/good-reminder.rb +0 -174
  235. data/examples/legacy/not_checked/shoes-dep-samples/good-vjot.rb +0 -56
  236. data/examples/legacy/not_checked/shoes-dep-samples/simple-accordion.rb +0 -75
  237. data/examples/legacy/not_checked/shoes-dep-samples/simple-anim-shapes.rb +0 -17
  238. data/examples/legacy/not_checked/shoes-dep-samples/simple-anim-text.rb +0 -13
  239. data/examples/legacy/not_checked/shoes-dep-samples/simple-arc.rb +0 -23
  240. data/examples/legacy/not_checked/shoes-dep-samples/simple-bounce.rb +0 -24
  241. data/examples/legacy/not_checked/shoes-dep-samples/simple-calc.rb +0 -70
  242. data/examples/legacy/not_checked/shoes-dep-samples/simple-chipmunk.rb +0 -26
  243. data/examples/legacy/not_checked/shoes-dep-samples/simple-control-sizes.rb +0 -24
  244. data/examples/legacy/not_checked/shoes-dep-samples/simple-curve.rb +0 -26
  245. data/examples/legacy/not_checked/shoes-dep-samples/simple-dialogs.rb +0 -29
  246. data/examples/legacy/not_checked/shoes-dep-samples/simple-draw.rb +0 -13
  247. data/examples/legacy/not_checked/shoes-dep-samples/simple-editor.rb +0 -28
  248. data/examples/legacy/not_checked/shoes-dep-samples/simple-form.rb +0 -28
  249. data/examples/legacy/not_checked/shoes-dep-samples/simple-form.shy +0 -0
  250. data/examples/legacy/not_checked/shoes-dep-samples/simple-mask.rb +0 -21
  251. data/examples/legacy/not_checked/shoes-dep-samples/simple-menu.rb +0 -31
  252. data/examples/legacy/not_checked/shoes-dep-samples/simple-menu1.rb +0 -35
  253. data/examples/legacy/not_checked/shoes-dep-samples/simple-rubygems.rb +0 -29
  254. data/examples/legacy/not_checked/shoes-dep-samples/simple-slide.rb +0 -45
  255. data/examples/legacy/not_checked/shoes-dep-samples/simple-sphere.rb +0 -28
  256. data/examples/legacy/not_checked/shoes-dep-samples/simple-sqlite3.rb +0 -13
  257. data/examples/legacy/not_checked/shoes-dep-samples/simple-timer.rb +0 -13
  258. data/examples/legacy/not_checked/shoes-dep-samples/simple-video.rb +0 -13
  259. data/examples/legacy/not_checked/simple/anim-text.rb +0 -13
  260. data/examples/legacy/not_checked/simple/arc.rb +0 -23
  261. data/examples/legacy/not_checked/simple/bounce.rb +0 -24
  262. data/examples/legacy/not_checked/simple/chipmunk.rb +0 -26
  263. data/examples/legacy/not_checked/simple/curve.rb +0 -26
  264. data/examples/legacy/not_checked/simple/dialogs.rb +0 -29
  265. data/examples/legacy/not_checked/simple/downloader.rb +0 -40
  266. data/examples/legacy/not_checked/simple/draw.rb +0 -13
  267. data/examples/legacy/not_checked/simple/mask.rb +0 -21
  268. data/examples/legacy/not_checked/simple/slide.rb +0 -45
  269. data/examples/legacy/not_checked/simple/sphere.rb +0 -28
  270. data/lib/constants.rb +0 -5
  271. data/lib/scarpe/alert.rb +0 -19
  272. data/lib/scarpe/app.rb +0 -78
  273. data/lib/scarpe/arc.rb +0 -49
  274. data/lib/scarpe/button.rb +0 -35
  275. data/lib/scarpe/document_root.rb +0 -20
  276. data/lib/scarpe/edit_box.rb +0 -24
  277. data/lib/scarpe/fill.rb +0 -23
  278. data/lib/scarpe/flow.rb +0 -19
  279. data/lib/scarpe/line.rb +0 -25
  280. data/lib/scarpe/link.rb +0 -25
  281. data/lib/scarpe/list_box.rb +0 -25
  282. data/lib/scarpe/logger.rb +0 -155
  283. data/lib/scarpe/para.rb +0 -90
  284. data/lib/scarpe/shape.rb +0 -19
  285. data/lib/scarpe/spacing.rb +0 -9
  286. data/lib/scarpe/stack.rb +0 -70
  287. data/lib/scarpe/star.rb +0 -47
  288. data/lib/scarpe/text_widget.rb +0 -42
  289. data/lib/scarpe/unit_test_helpers.rb +0 -163
  290. data/lib/scarpe/widget.rb +0 -198
  291. data/lib/scarpe/widgets.rb +0 -30
  292. data/lib/scarpe/wv/alert.rb +0 -65
  293. data/lib/scarpe/wv/background.rb +0 -18
  294. data/lib/scarpe/wv/border.rb +0 -22
  295. data/lib/scarpe/wv/control_interface_test.rb +0 -253
  296. data/lib/scarpe/wv/dimensions.rb +0 -22
  297. data/lib/scarpe/wv/fill.rb +0 -30
  298. data/lib/scarpe/wv/html.rb +0 -107
  299. data/lib/scarpe/wv/shape_helper.rb +0 -44
  300. data/lib/scarpe/wv/spacing.rb +0 -41
  301. data/lib/scarpe/wv/text_widget.rb +0 -30
  302. data/lib/scarpe/wv/widget.rb +0 -181
  303. data/scarpe-0.2.0.gem +0 -0
  304. /data/examples/legacy/not_checked/{expert → shoes-contrib/basic}/definr.rb +0 -0
  305. /data/examples/legacy/not_checked/{expert → shoes-contrib/basic}/funnies.rb +0 -0
  306. /data/examples/legacy/not_checked/shoes-contrib/{elements → basic}/list_box-select-class.rb +0 -0
  307. /data/examples/legacy/{not_checked/shoes-contrib/basic → working/simple}/basic-edit-box.rb +0 -0
  308. /data/examples/legacy/{not_checked/shoes-contrib/elements → working/simple}/basic-fps.rb +0 -0
  309. /data/examples/legacy/{not_checked/shoes-contrib/elements → working/simple}/border-cat.rb +0 -0
  310. /data/examples/legacy/{not_checked/shoes-contrib/elements → working/simple}/check-mate.rb +0 -0
  311. /data/examples/legacy/{not_checked/shoes-contrib/manipulation → working/simple}/clear-slot.rb +0 -0
  312. /data/examples/legacy/{not_checked/shoes-contrib/basic → working/simple}/clock.rb +0 -0
  313. /data/examples/legacy/{not_checked/shoes-contrib/basic → working/simple}/gradient-shoes.rb +0 -0
  314. /data/examples/legacy/{not_checked/shoes-contrib/basic → working/simple}/list_box-shape-report.rb +0 -0
  315. /data/examples/legacy/{not_checked/shoes-contrib/elements → working/simple}/list_box.rb +0 -0
  316. /data/examples/legacy/{not_checked/shoes-contrib/elements → working/simple}/phat-button.rb +0 -0
  317. /data/examples/legacy/{not_checked/shoes-contrib → working}/simple/simple-calc.rb +0 -0
  318. /data/examples/legacy/{not_checked/shoes-contrib/position → working/simple}/stack-width.rb +0 -0
  319. /data/examples/legacy/{not_checked/shoes-contrib/elements → working/simple}/width-introspec.rb +0 -0
  320. /data/{lib/scarpe → spikes}/glibui/README.md +0 -0
  321. /data/{lib/scarpe → spikes}/glibui/alert.rb +0 -0
  322. /data/{lib/scarpe → spikes}/glibui/app.rb +0 -0
  323. /data/{lib/scarpe → spikes}/glibui/background.rb +0 -0
  324. /data/{lib/scarpe → spikes}/glibui/border.rb +0 -0
  325. /data/{lib/scarpe → spikes}/glibui/button.rb +0 -0
  326. /data/{lib/scarpe → spikes}/glibui/dimensions.rb +0 -0
  327. /data/{lib/scarpe → spikes}/glibui/document_root.rb +0 -0
  328. /data/{lib/scarpe → spikes}/glibui/edit_box.rb +0 -0
  329. /data/{lib/scarpe → spikes}/glibui/edit_line.rb +0 -0
  330. /data/{lib/scarpe → spikes}/glibui/flow.rb +0 -0
  331. /data/{lib/scarpe → spikes}/glibui/html.rb +0 -0
  332. /data/{lib/scarpe → spikes}/glibui/image.rb +0 -0
  333. /data/{lib/scarpe → spikes}/glibui/link.rb +0 -0
  334. /data/{lib/scarpe → spikes}/glibui/local_display.rb +0 -0
  335. /data/{lib/scarpe → spikes}/glibui/para.rb +0 -0
  336. /data/{lib/scarpe → spikes}/glibui/spacing.rb +0 -0
  337. /data/{lib/scarpe → spikes}/glibui/stack.rb +0 -0
  338. /data/{lib/scarpe → spikes}/glibui/text_widget.rb +0 -0
  339. /data/{lib/scarpe → spikes}/libui/alert.rb +0 -0
  340. /data/{lib/scarpe → spikes}/libui/button.rb +0 -0
  341. /data/{lib/scarpe → spikes}/libui/colors.rb +0 -0
  342. /data/{lib/scarpe → spikes}/libui/core.rb +0 -0
  343. /data/{lib/scarpe → spikes}/libui/flow.rb +0 -0
  344. /data/{lib/scarpe → spikes}/libui/libui.rb +0 -0
  345. /data/{lib/scarpe → spikes}/libui/notepad.md +0 -0
  346. /data/{lib/scarpe → spikes}/libui/para.rb +0 -0
  347. /data/{lib/scarpe → spikes}/libui/stack.rb +0 -0
@@ -8,49 +8,70 @@ require "cgi"
8
8
  # After creation, it starts in setup mode, and you can
9
9
  # use setup-mode callbacks.
10
10
 
11
- class Scarpe
11
+ module Scarpe::Webview
12
+ # The Scarpe WebWrangler, for Webview, manages a lot of Webviews quirks. It provides
13
+ # a simpler underlying abstraction for DOMWrangler and the Webview drawables.
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
16
- attr_reader :is_terminated
17
- attr_reader :heartbeat # This is the heartbeat duration in seconds, usually fractional
18
- attr_reader :control_interface
19
43
 
20
- # This error indicates a problem when running ConfirmedEval
21
- class JSEvalError < Scarpe::Error
22
- def initialize(data)
23
- @data = data
24
- super(data[:msg] || (self.class.name + "!"))
25
- end
26
- end
27
-
28
- # We got an error running the supplied JS code string in confirmed_eval
29
- class JSRuntimeError < JSEvalError
30
- end
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.
46
+ attr_reader :is_terminated
31
47
 
32
- # The code timed out for some reason
33
- class JSTimeoutError < JSEvalError
34
- end
48
+ # This is the time between heartbeats in seconds, usually fractional
49
+ attr_reader :heartbeat
35
50
 
36
- # We got weird or nonsensical results that seem like an error on WebWrangler's part
37
- class InternalError < JSEvalError
38
- end
51
+ # A reference to the control_interface that manages internal Scarpe Webview events.
52
+ attr_reader :control_interface
39
53
 
40
- # This is the JS function name for eval results
54
+ # This is the JS function name for eval results (internal-only)
41
55
  EVAL_RESULT = "scarpeAsyncEvalResult"
42
56
 
43
- # Allow a half-second for Webview to finish our JS eval before we decide it's not going to
57
+ # Allow this many seconds for Webview to finish our JS eval before we decide it's not going to
44
58
  EVAL_DEFAULT_TIMEOUT = 0.5
45
59
 
46
- def initialize(title:, width:, height:, resizable: false, debug: false, heartbeat: 0.1)
47
- log_init("WV::WebWrangler")
60
+ # Create a new WebWrangler.
61
+ #
62
+ # @param title [String] window title
63
+ # @param width [Integer] window width in pixels
64
+ # @param height [Integer] window height in pixels
65
+ # @param resizable [Boolean] whether the window should be resizable by the user
66
+ # @param heartbeat [Float] time between heartbeats in seconds
67
+ def initialize(title:, width:, height:, resizable: false, heartbeat: 0.1)
68
+ log_init("Webview::WebWrangler")
48
69
 
49
70
  @log.debug("Creating WebWrangler...")
50
71
 
51
- # For now, always allow inspect element
72
+ # For now, always allow inspect element, so pass debug: true
52
73
  @webview = WebviewRuby::Webview.new debug: true
53
- @webview = Scarpe::LoggedWrapper.new(@webview, "WebviewAPI") if debug
74
+ @webview = Shoes::LoggedWrapper.new(@webview, "WebviewAPI") if ENV["SCARPE_DEBUG"]
54
75
  @init_refs = {} # Inits don't go away so keep a reference to them to prevent GC
55
76
 
56
77
  @title = title
@@ -59,8 +80,8 @@ class Scarpe
59
80
  @resizable = resizable
60
81
  @heartbeat = heartbeat
61
82
 
62
- # Better to have a single setInterval than many when we don't care too much
63
- # about the timing.
83
+ # JS setInterval uses RPC and is quite expensive. For many periodic operations
84
+ # we can group them under a single heartbeat handler and avoid extra JS calls or RPC.
64
85
  @heartbeat_handlers = []
65
86
 
66
87
  # Need to keep track of which WebView Javascript evals are still pending,
@@ -100,16 +121,28 @@ class Scarpe
100
121
 
101
122
  ### Setup-mode Callbacks
102
123
 
124
+ # Bind a Javascript-callable function by name. When JS calls the function,
125
+ # an async message is sent to Ruby via RPC and will eventually cause the
126
+ # block to be called. This method only works in setup mode, before the
127
+ # underlying Webview has been told to run.
128
+ #
129
+ # @param name [String] the Javascript name for the new function
130
+ # @yield The Ruby block to be invoked when JS calls the function
103
131
  def bind(name, &block)
104
- raise "App is running, javascript binding no longer works because it uses WebView init!" if @is_running
132
+ raise Scarpe::JSBindingError, "App is running, javascript binding no longer works because it uses WebView init!" if @is_running
105
133
 
106
134
  @webview.bind(name, &block)
107
135
  end
108
136
 
137
+ # Request that this block of code be run initially when the Webview is run.
138
+ # This operates via #init and will not work if Webview is already running.
139
+ #
140
+ # @param name [String] the Javascript name for the init function
141
+ # @yield The Ruby block to be invoked when Webview runs
109
142
  def init_code(name, &block)
110
- raise "App is running, javascript init no longer works!" if @is_running
143
+ raise Scarpe::JSInitError, "App is running, javascript init no longer works!" if @is_running
111
144
 
112
- # Save a reference to the init string so that it goesn't get GC'd
145
+ # Save a reference to the init string so that it doesn't get GC'd
113
146
  code_str = "#{name}();"
114
147
  @init_refs[name] = code_str
115
148
 
@@ -118,8 +151,16 @@ class Scarpe
118
151
  end
119
152
 
120
153
  # Run the specified code periodically, every "interval" seconds.
121
- # If interface is unspecified, run per-heartbeat, which is very
122
- # slightly more efficient.
154
+ # If interval is unspecified, run per-heartbeat. This avoids extra
155
+ # RPC and Javascript overhead. This may use the #init mechanism,
156
+ # so it should be invoked when the WebWrangler is in setup mode,
157
+ # before the Webview is running.
158
+ #
159
+ # TODO: add a way to stop this loop and unsubscribe.
160
+ #
161
+ # @param name [String] the name of the Javascript init function, if needed
162
+ # @param interval [Float] the duration between invoking this block
163
+ # @yield the Ruby block to invoke periodically
123
164
  def periodic_code(name, interval = heartbeat, &block)
124
165
  if interval == heartbeat
125
166
  @heartbeat_handlers << block
@@ -129,7 +170,7 @@ class Scarpe
129
170
  # new window. But will there ever be a new page/window? Can we just
130
171
  # use eval instead of init to set up a periodic handler and call it
131
172
  # good?
132
- raise "App is running, can't set up new periodic handlers with init!"
173
+ raise Scarpe::PeriodicHandlerSetupError, "App is running, can't set up new periodic handlers with init!"
133
174
  end
134
175
 
135
176
  js_interval = (interval.to_f * 1_000.0).to_i
@@ -143,7 +184,7 @@ class Scarpe
143
184
 
144
185
  # Running callbacks
145
186
 
146
- # js_eventually is a simple JS evaluation. On syntax error, nothing happens.
187
+ # js_eventually is a native Webview JS evaluation. On syntax error, nothing happens.
147
188
  # On runtime error, execution stops at the error with no further
148
189
  # effect or notification. This is rarely what you want.
149
190
  # The js_eventually code is run asynchronously, returning neither error
@@ -151,10 +192,13 @@ class Scarpe
151
192
  #
152
193
  # This method does *not* return a promise, and there is no way to track
153
194
  # its progress or its success or failure.
195
+ #
196
+ # @param code [String] the Javascript code to attempt to execute
197
+ # @return [void]
154
198
  def js_eventually(code)
155
- raise "WebWrangler isn't running, eval doesn't work!" unless @is_running
199
+ raise Scarpe::WebWranglerNotRunningError, "WebWrangler isn't running, eval doesn't work!" unless @is_running
156
200
 
157
- @log.warning "Deprecated: please do NOT use js_eventually, it's basically never what you want!" unless ENV["CI"]
201
+ @log.warn "Deprecated: please do NOT use js_eventually, it's basically never what you want!" unless ENV["CI"]
158
202
 
159
203
  @webview.eval(code)
160
204
  end
@@ -163,21 +207,27 @@ class Scarpe
163
207
  # promise which will be fulfilled or rejected after the JS executes
164
208
  # or times out.
165
209
  #
166
- # Note that we *both* care whether the JS has finished after it was
210
+ # We *both* care whether the JS has finished after it was
167
211
  # 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.
212
+ # depends on tasks that never fulfill or reject then it will
213
+ # raise a timed-out exception.
170
214
  #
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
-
215
+ # Right now we can't/don't pass arguments through from previous fulfilled
216
+ # promises. To do that, you can schedule the JS to run after the
217
+ # other promises succeed.
218
+ #
219
+ # Webview does not allow interacting with a JS eval once it has
220
+ # been scheduled. So there is no way to guarantee that a piece of JS has
221
+ # not executed, or will not execute in the future. A timeout exception
222
+ # only means that WebWrangler will no longer wait for confirmation or
223
+ # fulfill the promise if the JS later completes.
224
+ #
225
+ # @param code [String] the Javascript code to execute
226
+ # @param timeout [Float] how long to allow before raising a timeout exception
227
+ # @param wait_for [Array<Scarpe::Promise>] promises that must complete successfully before this JS is scheduled
228
+ def eval_js_async(code, timeout: EVAL_DEFAULT_TIMEOUT, wait_for: [])
179
229
  unless @is_running
180
- raise "WebWrangler isn't running, so evaluating JS won't work!"
230
+ raise Scarpe::WebWranglerNotRunningError, "WebWrangler isn't running, so evaluating JS won't work!"
181
231
  end
182
232
 
183
233
  this_eval_serial = @eval_counter
@@ -192,9 +242,8 @@ class Scarpe
192
242
 
193
243
  # We'll need this inside the promise-scheduling block
194
244
  pending_evals = @pending_evals
195
- timeout = opts[:timeout] || EVAL_DEFAULT_TIMEOUT
196
245
 
197
- promise = Scarpe::Promise.new(parents: (opts[:wait_for] || [])) do
246
+ promise = Scarpe::Promise.new(parents: wait_for) do
198
247
  # Are we mid-shutdown?
199
248
  if @webview
200
249
  wrapped_code = WebWrangler.js_wrapped_code(code, this_eval_serial)
@@ -220,6 +269,16 @@ class Scarpe
220
269
  promise
221
270
  end
222
271
 
272
+ # This method takes a piece of Javascript code and wraps it in the WebWrangler
273
+ # boilerplate to see if it parses successfully, run it, and see if it succeeds.
274
+ # This function would normally be used by testing code, to mock Webview and
275
+ # watch for code being run. Javascript code containing backticks
276
+ # could potentially break this abstraction layer, which would cause the resulting
277
+ # code to fail to parse and Webview would return no error. This should not be
278
+ # used for random or untrusted code.
279
+ #
280
+ # @param code [String] the Javascript code to be wrapped
281
+ # @param eval_id [Integer] the tracking code to use when calling EVAL_RESULT
223
282
  def self.js_wrapped_code(code, eval_id)
224
283
  <<~JS_CODE
225
284
  (function() {
@@ -243,7 +302,7 @@ class Scarpe
243
302
  def receive_eval_result(r_type, id, val)
244
303
  entry = @pending_evals.delete(id)
245
304
  unless entry
246
- raise "Received an eval result for a nonexistent ID #{id.inspect}!"
305
+ raise Scarpe::NonexistentEvalResultError, "Received an eval result for a nonexistent ID #{id.inspect}!"
247
306
  end
248
307
 
249
308
  @log.debug("Got JS value: #{r_type} / #{id} / #{val.inspect}")
@@ -254,13 +313,13 @@ class Scarpe
254
313
  when "success"
255
314
  promise.fulfilled!(val)
256
315
  when "error"
257
- promise.rejected! JSRuntimeError.new(
316
+ promise.rejected! Scarpe::JSRuntimeError.new(
258
317
  msg: "JS runtime error: #{val.inspect}!",
259
318
  code: entry[:code],
260
319
  ret_value: val,
261
320
  )
262
321
  else
263
- promise.rejected! InternalError.new(
322
+ promise.rejected! Scarpe::JSInternalError.new(
264
323
  msg: "JS eval internal error! r_type: #{r_type.inspect}",
265
324
  code: entry[:code],
266
325
  ret_value: val,
@@ -268,11 +327,11 @@ class Scarpe
268
327
  end
269
328
  end
270
329
 
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.
330
+ # @todo would be good to keep 'tombstone' results for awhile after timeout, maybe up to around a minute,
331
+ # so we can detect if we're timing things out and then having them return successfully after a delay.
332
+ # Then we could adjust the timeouts. We could also check if later serial numbers have returned, and time
333
+ # out earlier serial numbers... *if* we're sure Webview will always execute JS evals in order.
334
+ # This all adds complexity, though. For now, do timeouts on a simple max duration.
276
335
  def time_out_eval_results
277
336
  t_now = Time.now
278
337
  timed_out_from_scheduling = @pending_evals.keys.select do |id|
@@ -296,29 +355,34 @@ class Scarpe
296
355
  timed_out_ids.each do |id|
297
356
  @log.error "Timing out JS eval! #{@pending_evals[id][:code]}"
298
357
  entry = @pending_evals.delete(id)
299
- err = JSTimeoutError.new(msg: "JS timeout error!", code: entry[:code], ret_value: nil)
358
+ err = Scarpe::JSTimeoutError.new(msg: "JS timeout error!", code: entry[:code], ret_value: nil)
300
359
  entry[:promise].rejected!(err)
301
360
  end
302
361
  end
303
362
 
304
363
  public
305
364
 
306
- # After setup, we call run to go to "running" mode.
307
- # No more setup callbacks, only running callbacks.
365
+ attr_writer :empty_page
308
366
 
367
+ # After setup, we call run to go to "running" mode.
368
+ # No more setup callbacks should be called, only running callbacks.
309
369
  def run
310
370
  @log.debug("Run...")
311
371
 
312
372
  # From webview:
313
373
  # 0 - Width and height are default size
314
- # 1 - Width and height are minimum bonds
315
- # 2 - Width and height are maximum bonds
374
+ # 1 - Width and height are minimum bounds
375
+ # 2 - Width and height are maximum bounds
316
376
  # 3 - Window size can not be changed by a user
317
377
  hint = @resizable ? 0 : 3
318
378
 
319
379
  @webview.set_title(@title)
320
380
  @webview.set_size(@width, @height, hint)
321
- @webview.navigate("data:text/html, #{empty}")
381
+ unless @empty_page
382
+ raise Scarpe::EmptyPageNotSetError, "No empty page markup was set!"
383
+ end
384
+
385
+ @webview.navigate("data:text/html, #{CGI.escape @empty_page}")
322
386
 
323
387
  monkey_patch_console(@webview)
324
388
 
@@ -329,6 +393,8 @@ class Scarpe
329
393
  @webview = nil
330
394
  end
331
395
 
396
+ # Request destruction of WebWrangler, including terminating the underlying
397
+ # Webview and (when possible) destroying it.
332
398
  def destroy
333
399
  @log.debug("Destroying WebWrangler...")
334
400
  @log.debug(" (WebWrangler was already terminated)") if @is_terminated
@@ -362,318 +428,402 @@ class Scarpe
362
428
  end
363
429
 
364
430
  def empty
365
- html = <<~HTML
366
- <html>
367
- <head id='head-wvroot'>
368
- <style id='style-wvroot'>
369
- /** Style resets **/
370
- body {
371
- font-family: arial, Helvetica, sans-serif;
372
- margin: 0;
373
- height: 100%;
374
- overflow: hidden;
375
- }
376
- p {
377
- margin: 0;
378
- }
379
- </style>
380
- </head>
381
- <body id='body-wvroot'>
382
- <div id='wrapper-wvroot'></div>
383
- </body>
384
- </html>
385
- HTML
386
-
387
- CGI.escape(html)
431
+ Scarpe::Components::Calzini.empty_page_element
388
432
  end
389
433
 
390
434
  public
391
435
 
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
436
  # Replace the entire DOM - return a promise for when this has been done.
397
437
  # This will often get rid of smaller changes in the queue, which is
398
438
  # a good thing since they won't have to be run.
439
+ #
440
+ # @param html_text [String] The new HTML for the new full DOM
441
+ # @return [Scarpe::Promise] a promise that will be fulfilled when the update is complete
399
442
  def replace(html_text)
400
443
  @dom_wrangler.request_replace(html_text)
401
444
  end
402
445
 
403
446
  # Request a DOM change - return a promise for when this has been done.
447
+ # If a full replacement (see #replace) is requested, this change may
448
+ # be lost. Only use it for changes that are preserved by a full update.
449
+ #
450
+ # @param js [String] the JS to execute to alter the DOM
451
+ # @return [Scarpe::Promise] a promise that will be fulfilled when the update is complete
404
452
  def dom_change(js)
405
453
  @dom_wrangler.request_change(js)
406
454
  end
407
455
 
408
456
  # Return whether the DOM is, right this moment, confirmed to be fully
409
457
  # up to date or not.
458
+ #
459
+ # @return [Boolean] true if the window is fully updated, false if changes are pending
410
460
  def dom_fully_updated?
411
461
  @dom_wrangler.fully_updated?
412
462
  end
413
463
 
414
464
  # Return a promise that will be fulfilled when all current DOM changes
415
- # have committed (but not necessarily any future DOM changes.)
465
+ # have committed. If other changes are requested before these
466
+ # complete, the promise will ***not*** wait for them. If you wish to
467
+ # wait until all changes from all sources have completed, use
468
+ # #promise_dom_fully_updated.
469
+ #
470
+ # @return [Scarpe::Promise] a promise that will be fulfilled when all current changes complete
416
471
  def dom_promise_redraw
417
472
  @dom_wrangler.promise_redraw
418
473
  end
419
474
 
420
475
  # 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.
476
+ # fully up to date. A slow trickle of changes can make this
477
+ # take a long time, since it includes all current and future changes,
478
+ # not just changes before this call.
479
+ #
480
+ # If you want to know that some specific individual change is done, it's often
481
+ # easiest to use the promise returned by #dom_change, which will
482
+ # be fulfilled when that specific change is verified complete.
483
+ #
484
+ # If no changes are pending, promise_dom_fully_updated will
485
+ # return a promise that is already fulfilled.
486
+ #
487
+ # @return [Scarpe::Promise] a promise that will be fulfilled when all changes are complete
426
488
  def promise_dom_fully_updated
427
489
  @dom_wrangler.promise_fully_updated
428
490
  end
429
491
 
492
+ # DOMWrangler will frequently schedule and confirm small JS updates.
493
+ # A handler registered with on_every_redraw will be called after each
494
+ # small update.
495
+ #
496
+ # @yield Called after each update or batch of updates is verified complete
497
+ # @return [void]
430
498
  def on_every_redraw(&block)
431
499
  @dom_wrangler.on_every_redraw(&block)
432
500
  end
433
501
  end
434
502
  end
435
503
 
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
- class Scarpe
446
- class WebWrangler
447
- class DOMWrangler
448
- include Scarpe::Log
504
+ class Scarpe::Webview::WebWrangler
505
+ # Leaving DOM changes as "meh, async, we'll see when it happens" is terrible for testing.
506
+ # Instead, we need to track whether particular changes have committed yet or not.
507
+ # So we add a single gateway for all DOM changes, and we make sure its work is done
508
+ # before we consider a redraw complete.
509
+ #
510
+ # DOMWrangler batches up changes into fewer RPC calls. It's fine to have a redraw
511
+ # "in flight" and have changes waiting to catch the next bus. But we don't want more
512
+ # than one in flight, since it seems like having too many pending RPC requests can
513
+ # crash Webview. So we allow one redraw scheduled and one redraw promise waiting,
514
+ # at maximum.
515
+ #
516
+ # A WebWrangler will create and wrap a DOMWrangler, serving as the interface
517
+ # for all DOM operations.
518
+ #
519
+ # A batch of DOMWrangler changes may be removed if a full update is scheduled. That
520
+ # update is considered to replace the previous incremental changes. Any changes that
521
+ # need to execute even if a full update happens should be scheduled through
522
+ # WebWrangler#eval_js_async, not DOMWrangler.
523
+ class DOMWrangler
524
+ include Shoes::Log
525
+
526
+ # Changes that have not yet been executed
527
+ attr_reader :waiting_changes
528
+
529
+ # A Scarpe::Promise for JS that has been scheduled to execute but is not yet verified complete
530
+ attr_reader :pending_redraw_promise
531
+
532
+ # A Scarpe::Promise for waiting changes - it will be fulfilled when all waiting changes
533
+ # have been verified complete, or when a full redraw that removed them has been
534
+ # verified complete. If many small changes are scheduled, the same promise will be
535
+ # returned for many of them.
536
+ attr_reader :waiting_redraw_promise
537
+
538
+ # Create a DOMWrangler that is paired with a WebWrangler. The WebWrangler is
539
+ # treated as an underlying abstraction for reliable JS evaluation.
540
+ def initialize(web_wrangler)
541
+ log_init("Webview::WebWrangler::DOMWrangler")
542
+
543
+ @wrangler = web_wrangler
544
+
545
+ @waiting_changes = []
546
+ @pending_redraw_promise = nil
547
+ @waiting_redraw_promise = nil
548
+
549
+ @fully_up_to_date_promise = nil
550
+
551
+ # Initially we're waiting for a full replacement to happen.
552
+ # It's possible to request updates/changes before we have
553
+ # a DOM in place and before Webview is running. If we do
554
+ # that, we should discard those updates.
555
+ @first_draw_requested = false
556
+
557
+ @redraw_handlers = []
558
+
559
+ # The "fully up to date" logic is complicated and not
560
+ # as well tested as I'd like. This makes it far less
561
+ # likely that the event simply won't fire.
562
+ # With more comprehensive testing, this should be
563
+ # removable.
564
+ web_wrangler.periodic_code("scarpeDOMWranglerHeartbeat") do
565
+ if @fully_up_to_date_promise && fully_updated?
566
+ @log.info("Fulfilling up-to-date promise on heartbeat")
567
+ @fully_up_to_date_promise.fulfilled!
568
+ @fully_up_to_date_promise = nil
569
+ end
570
+ end
571
+ end
449
572
 
450
- attr_reader :waiting_changes
451
- attr_reader :pending_redraw_promise
452
- attr_reader :waiting_redraw_promise
573
+ def request_change(js_code)
574
+ # No updates until there's something to update
575
+ return unless @first_draw_requested
453
576
 
454
- def initialize(web_wrangler, debug: false)
455
- log_init("WV::WebWrangler::DOMWrangler")
577
+ @waiting_changes << js_code
456
578
 
457
- @wrangler = web_wrangler
579
+ promise_redraw
580
+ end
458
581
 
459
- @waiting_changes = []
460
- @pending_redraw_promise = nil
461
- @waiting_redraw_promise = nil
462
-
463
- @fully_up_to_date_promise = nil
464
-
465
- # Initially we're waiting for a full replacement to happen.
466
- # It's possible to request updates/changes before we have
467
- # a DOM in place and before Webview is running. If we do
468
- # that, we should discard those updates.
469
- @first_draw_requested = false
470
-
471
- @redraw_handlers = []
472
-
473
- # The "fully up to date" logic is complicated and not
474
- # as well tested as I'd like. This makes it far less
475
- # likely that the event simply won't fire.
476
- # With more comprehensive testing, this should be
477
- # removable.
478
- web_wrangler.periodic_code("scarpeDOMWranglerHeartbeat") do
479
- if @fully_up_to_date_promise && fully_updated?
480
- @log.info("Fulfilling up-to-date promise on heartbeat")
481
- @fully_up_to_date_promise.fulfilled!
482
- @fully_up_to_date_promise = nil
483
- end
484
- end
485
- end
582
+ def self.replacement_code(html_text)
583
+ "document.getElementById('wrapper-wvroot').innerHTML = `#{html_text}`; true"
584
+ end
486
585
 
487
- def request_change(js_code)
488
- # No updates until there's something to update
489
- return unless @first_draw_requested
586
+ def request_replace(html_text)
587
+ # Replace other pending changes, they're not needed any more
588
+ @waiting_changes = [DOMWrangler.replacement_code(html_text)]
589
+ @first_draw_requested = true
490
590
 
491
- @waiting_changes << js_code
591
+ @log.debug("Requesting DOM replacement...")
592
+ promise_redraw
593
+ end
492
594
 
493
- promise_redraw
494
- end
595
+ def on_every_redraw(&block)
596
+ @redraw_handlers << block
597
+ end
495
598
 
496
- def self.replacement_code(html_text)
497
- "document.getElementById('wrapper-wvroot').innerHTML = `#{html_text}`; true"
599
+ # promise_redraw returns a Scarpe::Promise which will be fulfilled after all current
600
+ # pending or waiting changes have completed. This may require creating a new
601
+ # promise.
602
+ #
603
+ # What are the states of redraw?
604
+ # "empty" - no waiting promise, no pending-redraw promise, no pending changes
605
+ # "pending only" - no waiting promise, but we have a pending redraw with some changes; it hasn't committed yet
606
+ # "pending and waiting" - we have a waiting promise for our unscheduled changes; we can add more unscheduled
607
+ # changes since we haven't scheduled them yet.
608
+ #
609
+ # This is often called after adding a new waiting change or replacing them, so the state may have just changed.
610
+ # It can also be called when no changes have been made and no updates need to happen.
611
+ def promise_redraw
612
+ if fully_updated?
613
+ # No changes to make, nothing in-process or waiting, so just return a pre-fulfilled promise
614
+ @log.debug("Requesting redraw but there are no pending changes or promises, return pre-fulfilled")
615
+ return ::Scarpe::Promise.fulfilled
498
616
  end
499
617
 
500
- def request_replace(html_text)
501
- # Replace other pending changes, they're not needed any more
502
- @waiting_changes = [DOMWrangler.replacement_code(html_text)]
503
- @first_draw_requested = true
504
-
505
- @log.debug("Requesting DOM replacement...")
506
- promise_redraw
618
+ # Already have a redraw requested *and* one on deck? Then all current changes will have committed
619
+ # when we (eventually) fulfill the waiting_redraw_promise.
620
+ if @waiting_redraw_promise
621
+ @log.debug("Promising eventual redraw of #{@waiting_changes.size} waiting unscheduled changes.")
622
+ return @waiting_redraw_promise
507
623
  end
508
624
 
509
- def on_every_redraw(&block)
510
- @redraw_handlers << block
625
+ if @waiting_changes.empty?
626
+ # There's no waiting_redraw_promise. There are no waiting changes. But we're not fully updated.
627
+ # So there must be a redraw in flight, and we don't need to schedule a new waiting_redraw_promise.
628
+ @log.debug("Returning in-flight redraw promise")
629
+ return @pending_redraw_promise
511
630
  end
512
631
 
513
- # What are the states of redraw?
514
- # "empty" - no waiting promise, no pending-redraw promise, no pending changes
515
- # "pending only" - no waiting promise, but we have a pending redraw with some changes; it hasn't committed yet
516
- # "pending and waiting" - we have a waiting promise for our unscheduled changes; we can add more unscheduled
517
- # changes since we haven't scheduled them yet.
518
- #
519
- # This is often called after adding a new waiting change or replacing them, so the state may have just changed.
520
- # It can also be called when no changes have been made and no updates need to happen.
521
- def promise_redraw
522
- if fully_updated?
523
- # No changes to make, nothing in-process or waiting, so just return a pre-fulfilled promise
524
- @log.debug("Requesting redraw but there are no pending changes or promises, return pre-fulfilled")
525
- return Promise.fulfilled
526
- end
527
-
528
- # Already have a redraw requested *and* one on deck? Then all current changes will have committed
529
- # when we (eventually) fulfill the waiting_redraw_promise.
530
- if @waiting_redraw_promise
531
- @log.debug("Promising eventual redraw of #{@waiting_changes.size} waiting unscheduled changes.")
532
- return @waiting_redraw_promise
533
- end
534
-
535
- if @waiting_changes.empty?
536
- # There's no waiting_redraw_promise. There are no waiting changes. But we're not fully updated.
537
- # So there must be a redraw in flight, and we don't need to schedule a new waiting_redraw_promise.
538
- @log.debug("Returning in-flight redraw promise")
539
- return @pending_redraw_promise
540
- end
541
-
542
- @log.debug("Requesting redraw with #{@waiting_changes.size} waiting changes and no waiting promise - need to schedule something!")
543
-
544
- # We have at least one waiting change, possibly newly-added. We have no waiting_redraw_promise.
545
- # Do we already have a redraw in-flight?
546
- if @pending_redraw_promise
547
- # Yes we do. Schedule a new waiting promise. When it turns into the pending_redraw_promise it will
548
- # grab all waiting changes. In the mean time, it sits here and waits.
549
- #
550
- # We *could* do a fancy promise thing and have it update @waiting_changes for itself, etc, when it
551
- # schedules itself. But we should always be calling promise_redraw or having a redraw fulfilled (see below)
552
- # when these things change. I'd rather keep the logic in this method. It's easier to reason through
553
- # all the cases.
554
- @waiting_redraw_promise = Promise.new
555
-
556
- @log.debug("Creating a new waiting promise since a pending promise is already in place")
557
- return @waiting_redraw_promise
558
- end
632
+ @log.debug("Requesting redraw with #{@waiting_changes.size} waiting changes and no waiting promise - need to schedule something!")
633
+
634
+ # We have at least one waiting change, possibly newly-added. We have no waiting_redraw_promise.
635
+ # Do we already have a redraw in-flight?
636
+ if @pending_redraw_promise
637
+ # Yes we do. Schedule a new waiting promise. When it turns into the pending_redraw_promise it will
638
+ # grab all waiting changes. In the mean time, it sits here and waits.
639
+ #
640
+ # We *could* do a fancy promise thing and have it update @waiting_changes for itself, etc, when it
641
+ # schedules itself. But we should always be calling promise_redraw or having a redraw fulfilled (see below)
642
+ # when these things change. I'd rather keep the logic in this method. It's easier to reason through
643
+ # all the cases.
644
+ @waiting_redraw_promise = ::Scarpe::Promise.new
645
+
646
+ @log.debug("Creating a new waiting promise since a pending promise is already in place")
647
+ return @waiting_redraw_promise
648
+ end
559
649
 
560
- # We have no redraw in-flight and no pre-existing waiting line. The new change(s) are presumably right
561
- # after things were fully up-to-date. We can schedule them for immediate redraw.
650
+ # We have no redraw in-flight and no pre-existing waiting line. The new change(s) are presumably right
651
+ # after things were fully up-to-date. We can schedule them for immediate redraw.
562
652
 
563
- @log.debug("Requesting redraw with #{@waiting_changes.size} waiting changes - scheduling a new redraw for them!")
564
- promise = schedule_waiting_changes # This clears the waiting changes
565
- @pending_redraw_promise = promise
653
+ @log.debug("Requesting redraw with #{@waiting_changes.size} waiting changes - scheduling a new redraw for them!")
654
+ promise = schedule_waiting_changes # This clears the waiting changes
655
+ @pending_redraw_promise = promise
566
656
 
567
- promise.on_fulfilled do
568
- @redraw_handlers.each(&:call)
569
- @pending_redraw_promise = nil
657
+ promise.on_fulfilled do
658
+ @redraw_handlers.each(&:call)
659
+ @pending_redraw_promise = nil
570
660
 
571
- if @waiting_redraw_promise
572
- # While this redraw was in flight, more waiting changes got added and we made a promise
573
- # about when they'd complete. Now they get scheduled, and we'll fulfill the waiting
574
- # promise when that redraw finishes. Clear the old waiting promise. We'll add a new one
575
- # when/if more changes are scheduled during this redraw.
576
- old_waiting_promise = @waiting_redraw_promise
577
- @waiting_redraw_promise = nil
661
+ if @waiting_redraw_promise
662
+ # While this redraw was in flight, more waiting changes got added and we made a promise
663
+ # about when they'd complete. Now they get scheduled, and we'll fulfill the waiting
664
+ # promise when that redraw finishes. Clear the old waiting promise. We'll add a new one
665
+ # when/if more changes are scheduled during this redraw.
666
+ old_waiting_promise = @waiting_redraw_promise
667
+ @waiting_redraw_promise = nil
578
668
 
579
- @log.debug "Fulfilled redraw with #{@waiting_changes.size} waiting changes - scheduling a new redraw for them!"
669
+ @log.debug "Fulfilled redraw with #{@waiting_changes.size} waiting changes - scheduling a new redraw for them!"
580
670
 
581
- new_promise = promise_redraw
582
- new_promise.on_fulfilled { old_waiting_promise.fulfilled! }
583
- else
584
- # The in-flight redraw completed, and there's still no waiting promise. Good! That means
585
- # we should be fully up-to-date.
586
- @log.debug "Fulfilled redraw with no waiting changes - marking us as up to date!"
587
- if @waiting_changes.empty?
588
- # We're fully up to date! Fulfill the promise. Now we don't need it again until somebody asks
589
- # us for another.
590
- if @fully_up_to_date_promise
591
- @fully_up_to_date_promise.fulfilled!
592
- @fully_up_to_date_promise = nil
593
- end
594
- else
595
- @log.error "WHOAH, WHAT? My logic must be wrong, because there's " +
596
- "no waiting promise, but waiting changes!"
671
+ new_promise = promise_redraw
672
+ new_promise.on_fulfilled { old_waiting_promise.fulfilled! }
673
+ else
674
+ # The in-flight redraw completed, and there's still no waiting promise. Good! That means
675
+ # we should be fully up-to-date.
676
+ @log.debug "Fulfilled redraw with no waiting changes - marking us as up to date!"
677
+ if @waiting_changes.empty?
678
+ # We're fully up to date! Fulfill the promise. Now we don't need it again until somebody asks
679
+ # us for another.
680
+ if @fully_up_to_date_promise
681
+ @fully_up_to_date_promise.fulfilled!
682
+ @fully_up_to_date_promise = nil
597
683
  end
684
+ else
685
+ @log.error "WHOAH, WHAT? My logic must be wrong, because there's " +
686
+ "no waiting promise, but waiting changes!"
598
687
  end
688
+ end
599
689
 
600
- @log.debug("Redraw is now fully up-to-date") if fully_updated?
601
- end.on_rejected do
602
- @log.error "Could not complete JS redraw! #{promise.reason.full_message}"
603
- @log.debug("REDRAW FULLY UP TO DATE BUT JS FAILED") if fully_updated?
604
-
605
- raise "JS Redraw failed! Bailing!"
690
+ @log.debug("Redraw is now fully up-to-date") if fully_updated?
691
+ end.on_rejected do
692
+ @log.error "Could not complete JS redraw! #{promise.reason.full_message}"
693
+ @log.debug("REDRAW FULLY UP TO DATE BUT JS FAILED") if fully_updated?
606
694
 
607
- # Later we should figure out how to handle this. Clear the promises and queues and request another redraw?
608
- end
609
- end
695
+ raise Scarpe::JSRedrawError, "JS Redraw failed! Bailing!"
610
696
 
611
- def fully_updated?
612
- @pending_redraw_promise.nil? && @waiting_redraw_promise.nil? && @waiting_changes.empty?
697
+ # Later we should figure out how to handle this. Clear the promises and queues and request another redraw?
613
698
  end
699
+ end
614
700
 
615
- # Return a promise which will be fulfilled when the DOM is fully up-to-date
616
- def promise_fully_updated
617
- if fully_updated?
618
- # No changes to make, nothing in-process or waiting, so just return a pre-fulfilled promise
619
- return Promise.fulfilled
620
- end
701
+ def fully_updated?
702
+ @pending_redraw_promise.nil? && @waiting_redraw_promise.nil? && @waiting_changes.empty?
703
+ end
621
704
 
622
- # Do we already have a promise for this? Return it. Everybody can share one.
623
- if @fully_up_to_date_promise
624
- return @fully_up_to_date_promise
625
- end
705
+ # Return a promise which will be fulfilled when the DOM is fully up-to-date
706
+ def promise_fully_updated
707
+ if fully_updated?
708
+ # No changes to make, nothing in-process or waiting, so just return a pre-fulfilled promise
709
+ return ::Scarpe::Promise.fulfilled
710
+ end
626
711
 
627
- # We're not fully updated, so we need a promise. Create it, return it.
628
- @fully_up_to_date_promise = Promise.new
712
+ # Do we already have a promise for this? Return it. Everybody can share one.
713
+ if @fully_up_to_date_promise
714
+ return @fully_up_to_date_promise
629
715
  end
630
716
 
631
- private
717
+ # We're not fully updated, so we need a promise. Create it, return it.
718
+ @fully_up_to_date_promise = ::Scarpe::Promise.new
719
+ end
632
720
 
633
- # Put together the waiting changes into a new in-flight redraw request.
634
- # Return it as a promise.
635
- def schedule_waiting_changes
636
- return if @waiting_changes.empty?
721
+ private
637
722
 
638
- js_code = @waiting_changes.join(";")
639
- @waiting_changes = [] # They're not waiting any more!
640
- @wrangler.eval_js_async(js_code)
641
- end
723
+ # Put together the waiting changes into a new in-flight redraw request.
724
+ # Return it as a promise.
725
+ def schedule_waiting_changes
726
+ return if @waiting_changes.empty?
727
+
728
+ js_code = @waiting_changes.join(";")
729
+ @waiting_changes = [] # They're not waiting any more!
730
+ @wrangler.eval_js_async(js_code)
642
731
  end
643
732
  end
644
- end
645
733
 
646
- # For now we don't need one of these to add DOM elements, just to manipulate them
647
- # after initial render.
648
- class Scarpe
649
- class WebWrangler
650
- class ElementWrangler
651
- attr_reader :html_id
734
+ # An ElementWrangler provides a way for a Drawable to manipulate is DOM element(s)
735
+ # via their HTML IDs. The most straightforward Drawables can have a single HTML ID
736
+ # and use a single ElementWrangler to make any needed changes.
737
+ #
738
+ # For now we don't need an ElementWrangler to add DOM elements, just to manipulate them
739
+ # after initial render. New DOM objects for Drawables are normally added via full
740
+ # redraws rather than incremental updates.
741
+ #
742
+ # Any changes made via ElementWrangler may be cancelled if a full redraw occurs,
743
+ # since it is assumed that small DOM manipulations are no longer needed. If a
744
+ # change would need to be made even if a full redraw occurred, it should be
745
+ # scheduled via WebWrangler#eval_js_async, not via an ElementWrangler.
746
+ class ElementWrangler
747
+ attr_reader :html_id
748
+
749
+ # Create an ElementWrangler for the given HTML ID
750
+ #
751
+ # @param html_id [String] the HTML ID for the DOM element
752
+ def initialize(html_id)
753
+ @webwrangler = ::Scarpe::Webview::DisplayService.instance.wrangler
754
+ raise Scarpe::MissingWranglerError, "Can't get WebWrangler!" unless @webwrangler
652
755
 
653
- def initialize(html_id)
654
- @webwrangler = WebviewDisplayService.instance.wrangler
655
- @html_id = html_id
656
- end
756
+ @html_id = html_id
757
+ end
657
758
 
658
- def promise_update
659
- @webwrangler.dom_promise_redraw
660
- end
759
+ # Return a promise that will be fulfilled when all changes scheduled via
760
+ # this ElementWrangler are verified complete.
761
+ #
762
+ # @return [Scarpe::Promise] a promise that will be fulfilled when scheduled changes are complete
763
+ def promise_update
764
+ @webwrangler.dom_promise_redraw
765
+ end
661
766
 
662
- def value=(new_value)
663
- @webwrangler.dom_change("document.getElementById('" + html_id + "').value = `" + new_value + "`; true")
664
- end
767
+ # Update the JS DOM element's value. The given Ruby value will be converted to string and assigned in backquotes.
768
+ #
769
+ # @param new_value [String] the new value
770
+ # @return [Scarpe::Promise] a promise that will be fulfilled when the change is complete
771
+ def value=(new_value)
772
+ @webwrangler.dom_change("document.getElementById('" + html_id + "').value = `" + new_value + "`; true")
773
+ end
665
774
 
666
- def inner_text=(new_text)
667
- @webwrangler.dom_change("document.getElementById('" + html_id + "').innerText = '" + new_text + "'; true")
668
- end
775
+ # Update the JS DOM element's inner_text. The given Ruby value will be converted to string and assigned in single-quotes.
776
+ #
777
+ # @param new_text [String] the new inner_text
778
+ # @return [Scarpe::Promise] a promise that will be fulfilled when the change is complete
779
+ def inner_text=(new_text)
780
+ @webwrangler.dom_change("document.getElementById('" + html_id + "').innerText = '" + new_text + "'; true")
781
+ end
669
782
 
670
- def inner_html=(new_html)
671
- @webwrangler.dom_change("document.getElementById(\"" + html_id + "\").innerHTML = `" + new_html + "`; true")
672
- end
783
+ # Update the JS DOM element's inner_html. The given Ruby value will be converted to string and assigned in backquotes.
784
+ #
785
+ # @param new_html [String] the new inner_html
786
+ # @return [Scarpe::Promise] a promise that will be fulfilled when the change is complete
787
+ def inner_html=(new_html)
788
+ @webwrangler.dom_change("document.getElementById(\"" + html_id + "\").innerHTML = `" + new_html + "`; true")
789
+ end
673
790
 
674
- def remove
675
- @webwrangler.dom_change("document.getElementById('" + html_id + "').remove(); true")
676
- end
791
+ # Update the JS DOM element's outer_html. The given Ruby value will be converted to string and assigned in backquotes.
792
+ #
793
+ # @param new_html [String] the new outer_html
794
+ # @return [Scarpe::Promise] a promise that will be fulfilled when the change is complete
795
+ def outer_html=(new_html)
796
+ @webwrangler.dom_change("document.getElementById(\"" + html_id + "\").outerHTML = `" + new_html + "`; true")
797
+ end
798
+
799
+ # Update the JS DOM element's attribute. The given Ruby value will be inspected and assigned.
800
+ #
801
+ # @param attribute [String] the attribute name
802
+ # @param value [String] the new attribute value
803
+ # @return [Scarpe::Promise] a promise that will be fulfilled when the change is complete
804
+ def set_attribute(attribute, value)
805
+ @webwrangler.dom_change("document.getElementById(\"" + html_id + "\").setAttribute(" + attribute.inspect + "," + value.inspect + "); true")
806
+ end
807
+
808
+ # Update an attribute of the JS DOM element's style. The given Ruby value will be inspected and assigned.
809
+ #
810
+ # @param style_attr [String] the style attribute name
811
+ # @param value [String] the new style attribute value
812
+ # @return [Scarpe::Promise] a promise that will be fulfilled when the change is complete
813
+ def set_style(style_attr, value)
814
+ @webwrangler.dom_change("document.getElementById(\"" + html_id + "\").style.#{style_attr} = " + value.inspect + "; true")
815
+ end
816
+
817
+ # Remove the specified DOM element
818
+ #
819
+ # @return [Scarpe::Promise] a promise that wil be fulfilled when the element is removed
820
+ def remove
821
+ @webwrangler.dom_change("document.getElementById('" + html_id + "').remove(); true")
822
+ end
823
+
824
+ def toggle_input_button(mark)
825
+ checked_value = mark ? "true" : "false"
826
+ @webwrangler.dom_change("document.getElementById('#{html_id}').checked = #{checked_value};")
677
827
  end
678
828
  end
679
829
  end