ruflet_core 0.0.21 → 0.0.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. checksums.yaml +4 -4
  2. data/lib/ruflet/version.rb +1 -1
  3. data/lib/ruflet_ui/ruflet/dsl.rb +76 -0
  4. data/lib/ruflet_ui/ruflet/event.rb +3 -2
  5. data/lib/ruflet_ui/ruflet/events/gesture_events.rb +2 -1
  6. data/lib/ruflet_ui/ruflet/page.rb +242 -4
  7. data/lib/ruflet_ui/ruflet/ui/control_factory.rb +2 -0
  8. data/lib/ruflet_ui/ruflet/ui/control_methods.rb +4 -0
  9. data/lib/ruflet_ui/ruflet/ui/control_registry.rb +20 -1
  10. data/lib/ruflet_ui/ruflet/ui/controls/cupertinos/cupertinoactionsheet_control.rb +1 -1
  11. data/lib/ruflet_ui/ruflet/ui/controls/cupertinos/cupertinoactionsheetaction_control.rb +1 -1
  12. data/lib/ruflet_ui/ruflet/ui/controls/cupertinos/cupertinoactivityindicator_control.rb +1 -1
  13. data/lib/ruflet_ui/ruflet/ui/controls/cupertinos/cupertinoalertdialog_control.rb +1 -1
  14. data/lib/ruflet_ui/ruflet/ui/controls/cupertinos/cupertinoappbar_control.rb +1 -1
  15. data/lib/ruflet_ui/ruflet/ui/controls/cupertinos/cupertinobottomsheet_control.rb +1 -1
  16. data/lib/ruflet_ui/ruflet/ui/controls/cupertinos/cupertinobutton_control.rb +1 -1
  17. data/lib/ruflet_ui/ruflet/ui/controls/cupertinos/cupertinocheckbox_control.rb +1 -1
  18. data/lib/ruflet_ui/ruflet/ui/controls/cupertinos/cupertinocontextmenu_control.rb +1 -1
  19. data/lib/ruflet_ui/ruflet/ui/controls/cupertinos/cupertinocontextmenuaction_control.rb +1 -1
  20. data/lib/ruflet_ui/ruflet/ui/controls/cupertinos/cupertinodatepicker_control.rb +1 -1
  21. data/lib/ruflet_ui/ruflet/ui/controls/cupertinos/cupertinodialogaction_control.rb +1 -1
  22. data/lib/ruflet_ui/ruflet/ui/controls/cupertinos/cupertinofilledbutton_control.rb +1 -1
  23. data/lib/ruflet_ui/ruflet/ui/controls/cupertinos/cupertinolisttile_control.rb +1 -1
  24. data/lib/ruflet_ui/ruflet/ui/controls/cupertinos/cupertinonavigationbar_control.rb +1 -1
  25. data/lib/ruflet_ui/ruflet/ui/controls/cupertinos/cupertinopicker_control.rb +1 -1
  26. data/lib/ruflet_ui/ruflet/ui/controls/cupertinos/cupertinoradio_control.rb +1 -1
  27. data/lib/ruflet_ui/ruflet/ui/controls/cupertinos/cupertinosegmentedbutton_control.rb +1 -1
  28. data/lib/ruflet_ui/ruflet/ui/controls/cupertinos/cupertinoslider_control.rb +1 -1
  29. data/lib/ruflet_ui/ruflet/ui/controls/cupertinos/cupertinoslidingsegmentedbutton_control.rb +1 -1
  30. data/lib/ruflet_ui/ruflet/ui/controls/cupertinos/cupertinoswitch_control.rb +1 -1
  31. data/lib/ruflet_ui/ruflet/ui/controls/cupertinos/cupertinotextfield_control.rb +1 -1
  32. data/lib/ruflet_ui/ruflet/ui/controls/cupertinos/cupertinotimerpicker_control.rb +1 -1
  33. data/lib/ruflet_ui/ruflet/ui/controls/cupertinos/cupertinotintedbutton_control.rb +1 -1
  34. data/lib/ruflet_ui/ruflet/ui/controls/materials/ad_controls.rb +45 -0
  35. data/lib/ruflet_ui/ruflet/ui/controls/materials/animation_extension_controls.rb +29 -0
  36. data/lib/ruflet_ui/ruflet/ui/controls/materials/chart_controls.rb +56 -279
  37. data/lib/ruflet_ui/ruflet/ui/controls/materials/code_editor_control.rb +42 -0
  38. data/lib/ruflet_ui/ruflet/ui/controls/materials/color_picker_controls.rb +34 -0
  39. data/lib/ruflet_ui/ruflet/ui/controls/materials/datatable2_controls.rb +41 -0
  40. data/lib/ruflet_ui/ruflet/ui/controls/materials/dropdownm2_control.rb +1 -1
  41. data/lib/ruflet_ui/ruflet/ui/controls/materials/map_control.rb +80 -0
  42. data/lib/ruflet_ui/ruflet/ui/controls/materials/ruflet_controls.rb +62 -0
  43. data/lib/ruflet_ui/ruflet/ui/controls/materials/video_control.rb +1 -2
  44. data/lib/ruflet_ui/ruflet/ui/controls/materials/webview_control.rb +5 -30
  45. data/lib/ruflet_ui/ruflet/ui/controls/ruflet_controls.rb +60 -0
  46. data/lib/ruflet_ui/ruflet/ui/controls/shared/browsercontextmenu_control.rb +17 -0
  47. data/lib/ruflet_ui/ruflet/ui/controls/shared/ruflet_app_control.rb +3 -3
  48. data/lib/ruflet_ui/ruflet/ui/controls/shared/serviceregistry_control.rb +5 -1
  49. data/lib/ruflet_ui/ruflet/ui/cupertino_control_methods.rb +2 -0
  50. data/lib/ruflet_ui/ruflet/ui/material_control_methods.rb +110 -6
  51. data/lib/ruflet_ui/ruflet/ui/material_control_registry.rb +23 -8
  52. data/lib/ruflet_ui/ruflet/ui/platform_control_contracts.rb +392 -0
  53. data/lib/ruflet_ui/ruflet/ui/platform_control_methods.rb +35 -0
  54. data/lib/ruflet_ui/ruflet/ui/services/current_service_methods.rb +220 -0
  55. data/lib/ruflet_ui/ruflet/ui/services/ruflet/camera_control.rb +88 -0
  56. data/lib/ruflet_ui/ruflet/ui/services/ruflet_services.rb +4 -0
  57. data/lib/ruflet_ui/ruflet/ui/shared_control_forwarders.rb +75 -0
  58. metadata +9 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 96131a85e131d05e9d2bbe07eb2bb3ceba97aeb2112301d4d6137d7067e81c3c
4
- data.tar.gz: 21245edeb2dc44b0cfe457c34678a552929ef813b2309494453582b717558209
3
+ metadata.gz: f3e8b0865b88790b85de1ad455a9b262603722e1919b25af614994765794d704
4
+ data.tar.gz: 8f16bbf7b16594ef9a4ff31d91e7fac5b67d36edd26021f297be09a110da95cf
5
5
  SHA512:
6
- metadata.gz: 1889c987529cb0915da64b09e534d8d132c34c1ec1553d099f3a81340744d57a5a48a8325ae77f6a4b19f7537440dae1a6c77a5dc7d632548b7dfe297de857f7
7
- data.tar.gz: ce99b6ffcae8ccc7c6837df5f53fe936bd1df521c73642a60957a3552a53500b5626a2d0611d571cc069efca0c597194ff4ec627916b73d07081e6046cf96611
6
+ metadata.gz: e97396572fb70e02900174cc177dc435159083d844f4f44a6dd9c854543af13eb866cbfd17265ac6c4239f260bc78e171eb02cfd16c564bf1e2d3203241d8661
7
+ data.tar.gz: e1db85c6c73756534c68018ee32803beda36d50237511e259ba693c269a0c671e0432e4417083b99bad0feb50b984244c16f33eab36d755930c833496fff27fc
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Ruflet
4
- VERSION = "0.0.21" unless const_defined?(:VERSION)
4
+ VERSION = "0.0.22" unless const_defined?(:VERSION)
5
5
  end
@@ -51,6 +51,16 @@ module Ruflet
51
51
  def control(type, **props, &block) = _pending_app.control(type, **props, &block)
52
52
  def widget(type, **props, &block) = _pending_app.widget(type, **props, &block)
53
53
  def service(type, **props, &block) = _pending_app.service(type, **props, &block)
54
+ def view(children = nil, **props, &block) = _pending_app.view(children, **props, &block)
55
+ def base_page(**props, &block) = _pending_app.base_page(**props, &block)
56
+ def basepage(**props, &block) = _pending_app.basepage(**props, &block)
57
+ def dialogs(controls = nil, **props) = _pending_app.dialogs(controls, **props)
58
+ def pagelet(content = nil, **props, &block) = _pending_app.pagelet(content, **props, &block)
59
+ def ruflet_app(**props) = _pending_app.ruflet_app(**props)
60
+ def rufletapp(**props) = _pending_app.rufletapp(**props)
61
+ def service_registry(services = nil, **props) = _pending_app.service_registry(services, **props)
62
+ def serviceregistry(services = nil, **props) = _pending_app.serviceregistry(services, **props)
63
+ def window(**props) = _pending_app.window(**props)
54
64
  def column(children = nil, **props, &block) = _pending_app.column(children, **props, &block)
55
65
  def center(**props, &block) = _pending_app.center(**props, &block)
56
66
  def row(children = nil, **props, &block) = _pending_app.row(children, **props, &block)
@@ -62,7 +72,31 @@ module Ruflet
62
72
  def animatedswitcher(content = nil, **props) = _pending_app.animatedswitcher(content, **props)
63
73
  def animation(duration = nil, **props) = _pending_app.animation(duration, **props)
64
74
  def animation_style(**props) = _pending_app.animation_style(**props)
75
+ def accelerometer(**props) = _pending_app.accelerometer(**props)
65
76
  def audio(**props) = _pending_app.audio(**props)
77
+ def audio_recorder(**props) = _pending_app.audio_recorder(**props)
78
+ def barometer(**props) = _pending_app.barometer(**props)
79
+ def battery(**props) = _pending_app.battery(**props)
80
+ def browser_context_menu(**props) = _pending_app.browser_context_menu(**props)
81
+ def camera(**props) = _pending_app.camera(**props)
82
+ def connectivity(**props) = _pending_app.connectivity(**props)
83
+ def file_picker(**props) = _pending_app.file_picker(**props)
84
+ def flashlight(**props) = _pending_app.flashlight(**props)
85
+ def geolocator(**props) = _pending_app.geolocator(**props)
86
+ def gyroscope(**props) = _pending_app.gyroscope(**props)
87
+ def haptic_feedback(**props) = _pending_app.haptic_feedback(**props)
88
+ def magnetometer(**props) = _pending_app.magnetometer(**props)
89
+ def permission_handler(**props) = _pending_app.permission_handler(**props)
90
+ def screen_brightness(**props) = _pending_app.screen_brightness(**props)
91
+ def secure_storage(**props) = _pending_app.secure_storage(**props)
92
+ def semantics_service(**props) = _pending_app.semantics_service(**props)
93
+ def shake_detector(**props) = _pending_app.shake_detector(**props)
94
+ def share(**props) = _pending_app.share(**props)
95
+ def shared_preferences(**props) = _pending_app.shared_preferences(**props)
96
+ def storage_paths(**props) = _pending_app.storage_paths(**props)
97
+ def url_launcher(**props) = _pending_app.url_launcher(**props)
98
+ def user_accelerometer(**props) = _pending_app.user_accelerometer(**props)
99
+ def wakelock(**props) = _pending_app.wakelock(**props)
66
100
  def auto_complete(suggestions = nil, **props) = _pending_app.auto_complete(suggestions, **props)
67
101
  def autocomplete(suggestions = nil, **props) = _pending_app.autocomplete(suggestions, **props)
68
102
  def auto_complete_suggestion(key = nil, **props) = _pending_app.auto_complete_suggestion(key, **props)
@@ -70,6 +104,8 @@ module Ruflet
70
104
  def autocompletesuggestion(key = nil, **props) = _pending_app.autocompletesuggestion(key, **props)
71
105
  def context_menu(content = nil, **props) = _pending_app.context_menu(content, **props)
72
106
  def contextmenu(content = nil, **props) = _pending_app.contextmenu(content, **props)
107
+ def context_menu_action(content = nil, **props) = _pending_app.context_menu_action(content, **props)
108
+ def contextmenuaction(content = nil, **props) = _pending_app.contextmenuaction(content, **props)
73
109
  def autofill_group(content = nil, **props) = _pending_app.autofill_group(content, **props)
74
110
  def autofillgroup(content = nil, **props) = _pending_app.autofillgroup(content, **props)
75
111
  def hero(content = nil, **props) = _pending_app.hero(content, **props)
@@ -133,6 +169,14 @@ module Ruflet
133
169
  def polygonmarker(**props) = _pending_app.polygonmarker(**props)
134
170
  def simple_attribution(**props) = _pending_app.simple_attribution(**props)
135
171
  def simpleattribution(**props) = _pending_app.simpleattribution(**props)
172
+ def map_layer(**props) = _pending_app.map_layer(**props)
173
+ def maplayer(**props) = _pending_app.maplayer(**props)
174
+ def rich_attribution(attributions = nil, **props) = _pending_app.rich_attribution(attributions, **props)
175
+ def richattribution(attributions = nil, **props) = _pending_app.richattribution(attributions, **props)
176
+ def image_source_attribution(image = nil, **props) = _pending_app.image_source_attribution(image, **props)
177
+ def imagesourceattribution(image = nil, **props) = _pending_app.imagesourceattribution(image, **props)
178
+ def text_source_attribution(text = nil, **props) = _pending_app.text_source_attribution(text, **props)
179
+ def textsourceattribution(text = nil, **props) = _pending_app.textsourceattribution(text, **props)
136
180
  def list_view(children = nil, **props) = _pending_app.list_view(children, **props)
137
181
  def listview(children = nil, **props) = _pending_app.listview(children, **props)
138
182
  def menu_bar(children = nil, **props) = _pending_app.menu_bar(children, **props)
@@ -160,6 +204,13 @@ module Ruflet
160
204
  def datarow(cells = nil, **props) = _pending_app.datarow(cells, **props)
161
205
  def data_cell(content = nil, **props) = _pending_app.data_cell(content, **props)
162
206
  def datacell(content = nil, **props) = _pending_app.datacell(content, **props)
207
+ def option(key = nil, **props) = _pending_app.option(key, **props)
208
+ def data_table2(columns = nil, **props) = _pending_app.data_table2(columns, **props)
209
+ def datatable2(columns = nil, **props) = _pending_app.datatable2(columns, **props)
210
+ def data_column2(label = nil, **props) = _pending_app.data_column2(label, **props)
211
+ def datacolumn2(label = nil, **props) = _pending_app.datacolumn2(label, **props)
212
+ def data_row2(cells = nil, **props) = _pending_app.data_row2(cells, **props)
213
+ def datarow2(cells = nil, **props) = _pending_app.datarow2(cells, **props)
163
214
  def expansion_tile(children = nil, **props) = _pending_app.expansion_tile(children, **props)
164
215
  def expansiontile(children = nil, **props) = _pending_app.expansiontile(children, **props)
165
216
  def expansion_panel(**props) = _pending_app.expansion_panel(**props)
@@ -178,6 +229,9 @@ module Ruflet
178
229
  def pageview(children = nil, **props) = _pending_app.pageview(children, **props)
179
230
  def progress_ring(**props) = _pending_app.progress_ring(**props)
180
231
  def progressring(**props) = _pending_app.progressring(**props)
232
+ def picker(children = nil, **props) = _pending_app.picker(children, **props)
233
+ def timer_picker(**props) = _pending_app.timer_picker(**props)
234
+ def timerpicker(**props) = _pending_app.timerpicker(**props)
181
235
  def spinkit(**props) = _pending_app.spinkit(**props)
182
236
  def range_slider(**props) = _pending_app.range_slider(**props)
183
237
  def rangeslider(**props) = _pending_app.rangeslider(**props)
@@ -257,6 +311,10 @@ module Ruflet
257
311
  def snackbaraction(label = nil, **props) = _pending_app.snackbaraction(label, **props)
258
312
  def bottom_sheet(content = nil, **props) = _pending_app.bottom_sheet(content, **props)
259
313
  def bottomsheet(content = nil, **props) = _pending_app.bottomsheet(content, **props)
314
+ def action_sheet(**props) = _pending_app.action_sheet(**props)
315
+ def actionsheet(**props) = _pending_app.actionsheet(**props)
316
+ def action_sheet_action(content = nil, **props) = _pending_app.action_sheet_action(content, **props)
317
+ def actionsheetaction(content = nil, **props) = _pending_app.actionsheetaction(content, **props)
260
318
  def markdown(value = nil, **props) = _pending_app.markdown(value, **props)
261
319
  def floating_action_button(**props) = _pending_app.floating_action_button(**props)
262
320
  def floatingactionbutton(**props) = _pending_app.floatingactionbutton(**props)
@@ -319,6 +377,24 @@ module Ruflet
319
377
  def web_view(**props) = _pending_app.web_view(**props)
320
378
  def webview(**props) = _pending_app.webview(**props)
321
379
  def video(**props) = _pending_app.video(**props)
380
+ def block_picker(**props) = _pending_app.block_picker(**props)
381
+ def blockpicker(**props) = _pending_app.blockpicker(**props)
382
+ def color_picker(**props) = _pending_app.color_picker(**props)
383
+ def colorpicker(**props) = _pending_app.colorpicker(**props)
384
+ def hue_ring_picker(**props) = _pending_app.hue_ring_picker(**props)
385
+ def hueringpicker(**props) = _pending_app.hueringpicker(**props)
386
+ def material_picker(**props) = _pending_app.material_picker(**props)
387
+ def materialpicker(**props) = _pending_app.materialpicker(**props)
388
+ def multiple_choice_block_picker(**props) = _pending_app.multiple_choice_block_picker(**props)
389
+ def multiplechoiceblockpicker(**props) = _pending_app.multiplechoiceblockpicker(**props)
390
+ def slide_picker(**props) = _pending_app.slide_picker(**props)
391
+ def slidepicker(**props) = _pending_app.slidepicker(**props)
392
+ def banner_ad(**props) = _pending_app.banner_ad(**props)
393
+ def bannerad(**props) = _pending_app.bannerad(**props)
394
+ def interstitial_ad(**props) = _pending_app.interstitial_ad(**props)
395
+ def interstitialad(**props) = _pending_app.interstitialad(**props)
396
+ def native_ad(**props) = _pending_app.native_ad(**props)
397
+ def nativead(**props) = _pending_app.nativead(**props)
322
398
  def code_editor(value = nil, **props) = _pending_app.code_editor(value, **props)
323
399
  def codeeditor(value = nil, **props) = _pending_app.codeeditor(value, **props)
324
400
  def lottie(src = nil, **props) = _pending_app.lottie(src, **props)
@@ -30,9 +30,10 @@ module Ruflet
30
30
 
31
31
  def parse_data(raw)
32
32
  return raw unless raw.is_a?(String)
33
+ return raw unless Object.const_defined?(:JSON)
33
34
 
34
- JSON.parse(raw)
35
- rescue JSON::ParserError
35
+ Object.const_get(:JSON).parse(raw)
36
+ rescue StandardError
36
37
  raw
37
38
  end
38
39
 
@@ -48,7 +48,8 @@ module Ruflet
48
48
  raw = data.is_a?(Hash) ? stringify_keys(data) : data
49
49
  value =
50
50
  if raw.is_a?(Hash)
51
- raw["value"] || raw["v"] || raw["data"] || raw["state"] || raw["route"]
51
+ key = %w[value v data state route].find { |candidate| raw.key?(candidate) }
52
+ key.nil? ? nil : raw[key]
52
53
  else
53
54
  raw
54
55
  end
@@ -178,6 +178,8 @@ module Ruflet
178
178
  @page_event_handlers = {}
179
179
  @view_props = {}
180
180
  @page_props = { "route" => (client_details["route"] || "/") }
181
+ @published_control_states = {}
182
+ @published_shell_state = nil
181
183
  @window = build_client_window(client_details["window"])
182
184
  @page_props["window"] = @window
183
185
  @overlay_container = Ruflet::Control.new(
@@ -229,6 +231,18 @@ module Ruflet
229
231
  @page_props["route"] = value
230
232
  end
231
233
 
234
+ # A resumed Page keeps its Ruby controls and event handlers, but a newly
235
+ # connected client has not mounted any of that state yet. Invalidate only
236
+ # the client-side publication snapshot so the next update sends the full
237
+ # page shell, including the internal overlay, dialog, and service roots.
238
+ def prepare_for_reconnect!
239
+ @overlay_container_mounted = false
240
+ @dialogs_container_mounted = false
241
+ @services_container_mounted = false
242
+ @published_shell_state = nil
243
+ self
244
+ end
245
+
232
246
  def vertical_alignment
233
247
  @page_props["vertical_alignment"] || @view_props["vertical_alignment"]
234
248
  end
@@ -313,6 +327,7 @@ module Ruflet
313
327
  @overlay_container_mounted = true if @overlay_container.wire_id
314
328
  @dialogs_container_mounted = true if @dialogs_container.wire_id
315
329
  @services_container_mounted = true if @services_container.wire_id
330
+ capture_published_wire_state!
316
331
  patch
317
332
  end
318
333
 
@@ -1281,7 +1296,7 @@ module Ruflet
1281
1296
 
1282
1297
  def update(control_or_id = nil, **props)
1283
1298
  if control_or_id.nil? && props.empty?
1284
- send_view_patch
1299
+ send_changed_control_patches
1285
1300
  return self
1286
1301
  end
1287
1302
 
@@ -1310,8 +1325,10 @@ module Ruflet
1310
1325
 
1311
1326
  # Keep runtime control tree aligned with incremental patches.
1312
1327
  if patch.key?("controls")
1328
+ replacement_controls = Array(patch["controls"]).dup
1313
1329
  control.children.clear
1314
- Array(patch["controls"]).each { |child| control.children << child if child.is_a?(Control) }
1330
+ replacement_controls.each { |child| control.children << child if child.is_a?(Control) }
1331
+ patch["controls"] = replacement_controls
1315
1332
  end
1316
1333
 
1317
1334
  visited = Set.new
@@ -1323,6 +1340,7 @@ module Ruflet
1323
1340
  "id" => wire_id,
1324
1341
  "patch" => [[0], *patch_ops]
1325
1342
  })
1343
+ capture_published_control_tree!(control)
1326
1344
 
1327
1345
  self
1328
1346
  end
@@ -1336,11 +1354,24 @@ module Ruflet
1336
1354
  end
1337
1355
 
1338
1356
  def apply_client_update(control_or_id, props)
1357
+ if page_control_target?(control_or_id)
1358
+ patch = normalize_props(props || {})
1359
+ @client_details.merge!(patch)
1360
+ # These are live client measurements. Keep the page getters current as
1361
+ # well, without consuming route changes before their event is dispatched.
1362
+ %w[width height platform_brightness media].each do |key|
1363
+ @page_props[key] = patch[key] if patch.key?(key)
1364
+ end
1365
+ capture_published_shell_state!
1366
+ return self
1367
+ end
1368
+
1339
1369
  control = resolve_control(control_or_id)
1340
1370
  return self unless control
1341
1371
 
1342
1372
  patch = normalize_props(props || {})
1343
1373
  patch.each { |k, v| control.props[k] = v }
1374
+ capture_published_control_tree!(control)
1344
1375
 
1345
1376
  remove_dialog_tracking(control) if patch.key?("open") && patch["open"] == false
1346
1377
 
@@ -1353,6 +1384,7 @@ module Ruflet
1353
1384
  route_from_event = extract_route(data)
1354
1385
  return if route_from_event && route_from_event == @page_props["route"]
1355
1386
  @page_props["route"] = route_from_event if route_from_event
1387
+ capture_published_shell_state!
1356
1388
  end
1357
1389
  dispatch_page_event(name: name, data: data)
1358
1390
  return
@@ -1362,7 +1394,13 @@ module Ruflet
1362
1394
  return unless control
1363
1395
 
1364
1396
  event = Event.new(name: name, target: target, raw_data: data, page: self, control: control)
1365
- apply_event_value_to_control(control, event) if %w[change select select_change].include?(name.to_s)
1397
+ if %w[change select select_change].include?(name.to_s)
1398
+ apply_event_value_to_control(control, event)
1399
+ # The client already owns this value. Treat it as published before the
1400
+ # handler runs so a later bare page.update only sends application-side
1401
+ # mutations, not the value that originated on the client.
1402
+ capture_published_control_tree!(control)
1403
+ end
1366
1404
  control.emit(name, event)
1367
1405
 
1368
1406
  if name.to_s == "dismiss" && remove_dialog_tracking(control)
@@ -1533,6 +1571,202 @@ module Ruflet
1533
1571
  @sender.call(action, payload)
1534
1572
  end
1535
1573
 
1574
+ # Bare page.update is the public commit point for direct mutations such as
1575
+ # `list.children.replace(...)`. Keep a shallow, immutable wire snapshot of
1576
+ # every mounted control so the commit can target only controls whose own
1577
+ # properties or child references changed. Descendant contents are tracked
1578
+ # independently: changing a Text value patches the Text, while replacing a
1579
+ # Column's children patches the Column.
1580
+ def send_changed_control_patches
1581
+ refresh_control_indexes!
1582
+ current = build_wire_state_snapshot
1583
+
1584
+ # Navigation, root controls, and page/view chrome are page-owned rather
1585
+ # than regular mounted controls. Retain the complete view patch as the
1586
+ # safe fallback whenever that shell changes.
1587
+ if @published_shell_state.nil? || current[:shell] != @published_shell_state
1588
+ send_view_patch
1589
+ return
1590
+ end
1591
+
1592
+ dirty_ids = current[:controls].each_with_object(Set.new) do |(wire_id, entry), dirty|
1593
+ prior = @published_control_states[wire_id]
1594
+ dirty << wire_id if prior && prior != entry[:state]
1595
+ end
1596
+
1597
+ # A changed parent patch serializes its newly referenced children, so a
1598
+ # second patch for a dirty descendant would be redundant and can race the
1599
+ # parent's replacement on the client.
1600
+ targets = dirty_ids.reject do |wire_id|
1601
+ ancestor = current[:parents][wire_id]
1602
+ found = false
1603
+ while ancestor
1604
+ if dirty_ids.include?(ancestor)
1605
+ found = true
1606
+ break
1607
+ end
1608
+ ancestor = current[:parents][ancestor]
1609
+ end
1610
+ found
1611
+ end
1612
+
1613
+ targets.each do |wire_id|
1614
+ entry = current[:controls][wire_id]
1615
+ patch_ops = changed_control_patch_ops(
1616
+ entry[:control],
1617
+ @published_control_states.fetch(wire_id),
1618
+ entry[:state]
1619
+ )
1620
+ next if patch_ops.empty?
1621
+
1622
+ send_message(Protocol::ACTIONS[:patch_control], {
1623
+ "id" => wire_id,
1624
+ "patch" => [[0], *patch_ops]
1625
+ })
1626
+ end
1627
+
1628
+ publish_wire_state_snapshot!(current)
1629
+ end
1630
+
1631
+ def changed_control_patch_ops(control, prior, current)
1632
+ operations = []
1633
+ prior_props = prior.fetch("props")
1634
+ current_props = current.fetch("props")
1635
+
1636
+ (prior_props.keys | current_props.keys).each do |key|
1637
+ next if prior_props[key] == current_props[key]
1638
+
1639
+ value = control.props.key?(key) ? control.props[key] : nil
1640
+ operations << [0, 0, key, serialize_patch_value(value)]
1641
+ end
1642
+
1643
+ if prior.fetch("children") != current.fetch("children")
1644
+ operations << [0, 0, "controls", serialize_patch_value(control.children)]
1645
+ end
1646
+ operations
1647
+ end
1648
+
1649
+ def build_wire_state_snapshot
1650
+ controls = {}
1651
+ parents = {}
1652
+ visited = Set.new
1653
+
1654
+ roots = @views.any? ? @views : @root_controls
1655
+ roots.each { |control| collect_control_wire_state(control, nil, controls, parents, visited) }
1656
+ unless @views.any?
1657
+ @view_props.each_value do |value|
1658
+ collect_embedded_wire_state(value, nil, controls, parents, visited)
1659
+ end
1660
+ end
1661
+ @page_props.each_value do |value|
1662
+ collect_embedded_wire_state(value, nil, controls, parents, visited)
1663
+ end
1664
+
1665
+ {
1666
+ controls: controls,
1667
+ parents: parents,
1668
+ shell: current_shell_wire_state
1669
+ }
1670
+ end
1671
+
1672
+ def collect_control_wire_state(control, parent_id, controls, parents, visited)
1673
+ return unless control
1674
+ return if visited.include?(control.object_id)
1675
+
1676
+ visited << control.object_id
1677
+ wire_id = control.wire_id
1678
+ return unless wire_id
1679
+
1680
+ parents[wire_id] = parent_id if parent_id
1681
+ controls[wire_id] = {
1682
+ control: control,
1683
+ state: control_local_wire_state(control)
1684
+ }
1685
+ control.children.each do |child|
1686
+ collect_control_wire_state(child, wire_id, controls, parents, visited)
1687
+ end
1688
+ control.props.each_value do |value|
1689
+ collect_embedded_wire_state(value, wire_id, controls, parents, visited)
1690
+ end
1691
+ end
1692
+
1693
+ def collect_embedded_wire_state(value, parent_id, controls, parents, visited)
1694
+ case value
1695
+ when Control
1696
+ collect_control_wire_state(value, parent_id, controls, parents, visited)
1697
+ when Array
1698
+ value.each do |entry|
1699
+ collect_embedded_wire_state(entry, parent_id, controls, parents, visited)
1700
+ end
1701
+ when Hash
1702
+ value.each_value do |entry|
1703
+ collect_embedded_wire_state(entry, parent_id, controls, parents, visited)
1704
+ end
1705
+ end
1706
+ end
1707
+
1708
+ def control_local_wire_state(control)
1709
+ {
1710
+ "type" => control.type,
1711
+ "props" => comparison_wire_value(control.props),
1712
+ "children" => control.children.map(&:wire_id)
1713
+ }
1714
+ end
1715
+
1716
+ def current_shell_wire_state
1717
+ {
1718
+ "mode" => (@views.any? ? "views" : "implicit_view"),
1719
+ "roots" => (@views.any? ? @views : @root_controls).map(&:wire_id),
1720
+ "view_props" => comparison_wire_value(@view_props),
1721
+ "page_props" => comparison_wire_value(@page_props)
1722
+ }
1723
+ end
1724
+
1725
+ # Controls are references in their parent's local state. Their contents
1726
+ # are deliberately excluded and tracked under their own wire IDs.
1727
+ def comparison_wire_value(value)
1728
+ case value
1729
+ when Control
1730
+ [Control, value.wire_id]
1731
+ when Array
1732
+ value.map { |entry| comparison_wire_value(entry) }
1733
+ when Hash
1734
+ value.each_with_object({}) do |(key, entry), result|
1735
+ result[key] = comparison_wire_value(entry)
1736
+ end
1737
+ when String
1738
+ value.dup
1739
+ else
1740
+ begin
1741
+ value.dup
1742
+ rescue TypeError
1743
+ value
1744
+ end
1745
+ end
1746
+ end
1747
+
1748
+ def capture_published_wire_state!
1749
+ publish_wire_state_snapshot!(build_wire_state_snapshot)
1750
+ end
1751
+
1752
+ def publish_wire_state_snapshot!(snapshot)
1753
+ @published_control_states = snapshot[:controls].transform_values { |entry| entry[:state] }
1754
+ @published_shell_state = snapshot[:shell]
1755
+ end
1756
+
1757
+ def capture_published_control_tree!(control)
1758
+ controls = {}
1759
+ parents = {}
1760
+ collect_control_wire_state(control, nil, controls, parents, Set.new)
1761
+ controls.each do |wire_id, entry|
1762
+ @published_control_states[wire_id] = entry[:state]
1763
+ end
1764
+ end
1765
+
1766
+ def capture_published_shell_state!
1767
+ @published_shell_state = current_shell_wire_state if @published_shell_state
1768
+ end
1769
+
1536
1770
  def replace_root_controls(controls)
1537
1771
  visited = Set.new
1538
1772
  controls.each { |control| register_control_tree(control, visited) }
@@ -1559,6 +1793,7 @@ module Ruflet
1559
1793
  @overlay_container_mounted = true if @overlay_container.wire_id
1560
1794
  @dialogs_container_mounted = true if @dialogs_container.wire_id
1561
1795
  @services_container_mounted = true if @services_container.wire_id
1796
+ capture_published_wire_state!
1562
1797
  end
1563
1798
 
1564
1799
  def register_control_tree(control, visited = Set.new)
@@ -1778,6 +2013,7 @@ module Ruflet
1778
2013
  "id" => @overlay_container.wire_id,
1779
2014
  "patch" => [[0], [0, 0, "controls", serialize_patch_value(@overlay_container.children)]]
1780
2015
  })
2016
+ capture_published_control_tree!(@overlay_container)
1781
2017
  else
1782
2018
  send_view_patch
1783
2019
  end
@@ -1795,6 +2031,7 @@ module Ruflet
1795
2031
  "id" => @services_container.wire_id,
1796
2032
  "patch" => [[0], [0, 0, "_services", serialize_patch_value(@services_container.props["_services"])]]
1797
2033
  })
2034
+ capture_published_control_tree!(@services_container)
1798
2035
  else
1799
2036
  send_view_patch
1800
2037
  end
@@ -1808,6 +2045,7 @@ module Ruflet
1808
2045
  "id" => @dialogs_container.wire_id,
1809
2046
  "patch" => [[0], [0, 0, "controls", serialize_patch_value(@dialogs_container.props["controls"])]]
1810
2047
  })
2048
+ capture_published_control_tree!(@dialogs_container)
1811
2049
  else
1812
2050
  send_view_patch
1813
2051
  end
@@ -1841,7 +2079,7 @@ module Ruflet
1841
2079
  end
1842
2080
 
1843
2081
  def assign_split_prop(key, value)
1844
- if key == "vertical_alignment" || key == "horizontal_alignment"
2082
+ if key == "vertical_alignment" || key == "horizontal_alignment" || key == "scroll"
1845
2083
  @page_props[key] = value
1846
2084
  @view_props[key] = value
1847
2085
  elsif DIALOG_PROP_KEYS.include?(key)
@@ -2,6 +2,7 @@
2
2
 
3
3
  require_relative "controls/ruflet_controls"
4
4
  require_relative "services/ruflet_services"
5
+ require_relative "platform_control_contracts"
5
6
  require_relative "../control"
6
7
 
7
8
  module Ruflet
@@ -12,6 +13,7 @@ module Ruflet
12
13
  CLASS_MAP =
13
14
  Services::RufletServices::CLASS_MAP
14
15
  .merge(Controls::RufletControls::CLASS_MAP)
16
+ .merge(PlatformControlContracts::CLASS_MAP)
15
17
  .freeze
16
18
  EXTENSION_CLASS_MAP = {}
17
19
  CONSTRUCTOR_KEYWORDS_CACHE = {}
@@ -1,12 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "material_control_methods"
4
+ require_relative "platform_control_methods"
4
5
  require_relative "cupertino_control_methods"
5
6
 
6
7
  module Ruflet
7
8
  module UI
8
9
  module ControlMethods
9
10
  include MaterialControlMethods
11
+ include PlatformControlMethods
12
+ # Compatibility aliases only. They emit the same canonical wire types as
13
+ # the methods above and never select the visual renderer.
10
14
  include CupertinoControlMethods
11
15
 
12
16
  def control(type, **props, &block) = build_widget(type, **props, &block)
@@ -7,7 +7,26 @@ module Ruflet
7
7
  require_relative "cupertino_control_registry"
8
8
  require_relative "controls/ruflet_controls"
9
9
  require_relative "services/ruflet_services"
10
- TYPE_MAP = MaterialControlRegistry::TYPE_MAP.merge(CupertinoControlRegistry::TYPE_MAP).freeze
10
+ require_relative "platform_control_contracts"
11
+ # Controls whose Ruby name and wire name differ on purpose. The client
12
+ # Native Ruflet clients use RufletApp as the canonical wire type while
13
+ # the Ruby API remains ruflet_app.
14
+ #
15
+ # RufletAppControl::WIRE already says this, but that is only read when the
16
+ # control resolves to its own class. Serialization otherwise derives the
17
+ # wire name from the type and must still send the canonical RufletApp.
18
+ # Map it here so both paths agree. to_patch also looks the type up with
19
+ # underscores removed, so register that spelling too.
20
+ SHARED_TYPE_MAP = {
21
+ "ruflet_app" => "RufletApp",
22
+ "rufletapp" => "RufletApp"
23
+ }.freeze
24
+
25
+ TYPE_MAP = MaterialControlRegistry::TYPE_MAP
26
+ .merge(CupertinoControlRegistry::TYPE_MAP)
27
+ .merge(SHARED_TYPE_MAP)
28
+ .merge(PlatformControlContracts::TYPE_MAP)
29
+ .freeze
11
30
  SCHEMA_EVENT_PROPS =
12
31
  Controls::RufletControls::CLASS_MAP
13
32
  .merge(Services::RufletServices::CLASS_MAP)
@@ -6,7 +6,7 @@ module Ruflet
6
6
  module RufletComponents
7
7
  class CupertinoActionSheetControl < Ruflet::Control
8
8
  TYPE = "cupertinoactionsheet".freeze
9
- WIRE = "CupertinoActionSheet".freeze
9
+ WIRE = "ActionSheet".freeze
10
10
 
11
11
  KEYWORDS = [:actions, :align, :animate_align, :animate_margin, :animate_offset, :animate_opacity, :animate_position, :animate_rotation, :animate_scale, :animate_size, :aspect_ratio, :badge, :bottom, :cancel, :col, :data, :disabled, :expand, :expand_loose, :height, :key, :left, :margin, :message, :offset, :opacity, :right, :rotate, :rtl, :scale, :size_change_interval, :title, :tooltip, :top, :visible, :width, :on_animation_end, :on_size_change].freeze
12
12
 
@@ -6,7 +6,7 @@ module Ruflet
6
6
  module RufletComponents
7
7
  class CupertinoActionSheetActionControl < Ruflet::Control
8
8
  TYPE = "cupertinoactionsheetaction".freeze
9
- WIRE = "CupertinoActionSheetAction".freeze
9
+ WIRE = "ActionSheetAction".freeze
10
10
 
11
11
  KEYWORDS = [:align, :animate_align, :animate_margin, :animate_offset, :animate_opacity, :animate_position, :animate_rotation, :animate_scale, :animate_size, :aspect_ratio, :badge, :bottom, :col, :content, :data, :default, :destructive, :disabled, :expand, :expand_loose, :height, :key, :left, :margin, :mouse_cursor, :offset, :opacity, :right, :rotate, :rtl, :scale, :size_change_interval, :tooltip, :top, :visible, :width, :on_animation_end, :on_click, :on_size_change].freeze
12
12
 
@@ -6,7 +6,7 @@ module Ruflet
6
6
  module RufletComponents
7
7
  class CupertinoActivityIndicatorControl < Ruflet::Control
8
8
  TYPE = "cupertinoactivityindicator".freeze
9
- WIRE = "CupertinoActivityIndicator".freeze
9
+ WIRE = "ProgressRing".freeze
10
10
 
11
11
  KEYWORDS = [:align, :animate_align, :animate_margin, :animate_offset, :animate_opacity, :animate_position, :animate_rotation, :animate_scale, :animate_size, :animating, :aspect_ratio, :badge, :bottom, :col, :color, :data, :disabled, :expand, :expand_loose, :height, :key, :left, :margin, :offset, :opacity, :progress, :radius, :right, :rotate, :rtl, :scale, :size_change_interval, :tooltip, :top, :visible, :width, :on_animation_end, :on_size_change].freeze
12
12
 
@@ -6,7 +6,7 @@ module Ruflet
6
6
  module RufletComponents
7
7
  class CupertinoAlertDialogControl < Ruflet::Control
8
8
  TYPE = "cupertinoalertdialog".freeze
9
- WIRE = "CupertinoAlertDialog".freeze
9
+ WIRE = "AlertDialog".freeze
10
10
 
11
11
  def initialize(id: nil, actions: nil, adaptive: nil, badge: nil, barrier_color: nil, col: nil, content: nil, data: nil, disabled: nil, expand: nil, expand_loose: nil, inset_animation: nil, key: nil, modal: nil, opacity: nil, open: nil, rtl: nil, title: nil, tooltip: nil, visible: nil, on_dismiss: nil)
12
12
  actions = [] if actions.nil?
@@ -6,7 +6,7 @@ module Ruflet
6
6
  module RufletComponents
7
7
  class CupertinoAppBarControl < Ruflet::Control
8
8
  TYPE = "cupertinoappbar".freeze
9
- WIRE = "CupertinoAppBar".freeze
9
+ WIRE = "AppBar".freeze
10
10
 
11
11
  def initialize(id: nil, automatic_background_visibility: nil, automatically_imply_leading: nil, automatically_imply_title: nil, badge: nil, bgcolor: nil, border: nil, brightness: nil, col: nil, data: nil, disabled: nil, enable_background_filter_blur: nil, expand: nil, expand_loose: nil, key: nil, large: nil, leading: nil, opacity: nil, padding: nil, previous_page_title: nil, rtl: nil, title: nil, tooltip: nil, trailing: nil, transition_between_routes: nil, visible: nil)
12
12
  large = false if large.nil?
@@ -6,7 +6,7 @@ module Ruflet
6
6
  module RufletComponents
7
7
  class CupertinoBottomSheetControl < Ruflet::Control
8
8
  TYPE = "cupertinobottomsheet".freeze
9
- WIRE = "CupertinoBottomSheet".freeze
9
+ WIRE = "BottomSheet".freeze
10
10
 
11
11
  def initialize(id: nil, adaptive: nil, badge: nil, bgcolor: nil, col: nil, content: nil, data: nil, disabled: nil, expand: nil, expand_loose: nil, height: nil, key: nil, modal: nil, opacity: nil, open: nil, padding: nil, rtl: nil, tooltip: nil, visible: nil, on_dismiss: nil)
12
12
  raise ArgumentError, "cupertino_bottom_sheet content is required" if content.nil?
@@ -6,7 +6,7 @@ module Ruflet
6
6
  module RufletComponents
7
7
  class CupertinoButtonControl < Ruflet::Control
8
8
  TYPE = "cupertinobutton".freeze
9
- WIRE = "CupertinoButton".freeze
9
+ WIRE = "Button".freeze
10
10
 
11
11
  KEYWORDS = [:align, :alignment, :animate_align, :animate_margin, :animate_offset, :animate_opacity, :animate_position, :animate_rotation, :animate_scale, :animate_size, :aspect_ratio, :autofocus, :badge, :bgcolor, :border_radius, :bottom, :col, :color, :content, :data, :disabled, :disabled_bgcolor, :expand, :expand_loose, :focus_color, :height, :icon, :icon_color, :key, :left, :margin, :min_size, :mouse_cursor, :offset, :opacity, :opacity_on_click, :padding, :right, :rotate, :rtl, :scale, :size, :size_change_interval, :tooltip, :top, :url, :visible, :width, :on_animation_end, :on_blur, :on_click, :on_focus, :on_long_press, :on_size_change].freeze
12
12