ruflet 0.0.7 → 0.0.9

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 (82) hide show
  1. checksums.yaml +4 -4
  2. data/bin/ruflet +6 -0
  3. data/lib/ruflet/cli/build_command.rb +372 -0
  4. data/lib/ruflet/cli/extra_command.rb +146 -0
  5. data/lib/ruflet/cli/flutter_sdk.rb +359 -0
  6. data/lib/ruflet/cli/new_command.rb +221 -0
  7. data/lib/ruflet/cli/run_command.rb +699 -0
  8. data/lib/ruflet/cli/templates.rb +68 -0
  9. data/lib/ruflet/cli/update_command.rb +111 -0
  10. data/lib/ruflet/cli.rb +85 -0
  11. data/lib/ruflet/version.rb +1 -1
  12. data/lib/ruflet_cli.rb +3 -0
  13. metadata +59 -75
  14. data/lib/ruflet/manifest_compiler.rb +0 -62
  15. data/lib/ruflet.rb +0 -40
  16. data/lib/ruflet_protocol/ruflet/protocol.rb +0 -62
  17. data/lib/ruflet_protocol.rb +0 -4
  18. data/lib/ruflet_ui/ruflet/app.rb +0 -31
  19. data/lib/ruflet_ui/ruflet/colors.rb +0 -234
  20. data/lib/ruflet_ui/ruflet/control.rb +0 -168
  21. data/lib/ruflet_ui/ruflet/dsl.rb +0 -227
  22. data/lib/ruflet_ui/ruflet/event.rb +0 -28
  23. data/lib/ruflet_ui/ruflet/icon_data.rb +0 -62
  24. data/lib/ruflet_ui/ruflet/icons/cupertino/cupertino_icons.rb +0 -54
  25. data/lib/ruflet_ui/ruflet/icons/cupertino_icon_lookup.rb +0 -112
  26. data/lib/ruflet_ui/ruflet/icons/material_icon_lookup.rb +0 -112
  27. data/lib/ruflet_ui/ruflet/icons/material_icons.rb +0 -55
  28. data/lib/ruflet_ui/ruflet/page.rb +0 -741
  29. data/lib/ruflet_ui/ruflet/ui/control_factory.rb +0 -23
  30. data/lib/ruflet_ui/ruflet/ui/control_methods.rb +0 -16
  31. data/lib/ruflet_ui/ruflet/ui/control_registry.rb +0 -13
  32. data/lib/ruflet_ui/ruflet/ui/controls/cupertino/cupertino_action_sheet_control.rb +0 -13
  33. data/lib/ruflet_ui/ruflet/ui/controls/cupertino/cupertino_alert_dialog_control.rb +0 -13
  34. data/lib/ruflet_ui/ruflet/ui/controls/cupertino/cupertino_button_control.rb +0 -13
  35. data/lib/ruflet_ui/ruflet/ui/controls/cupertino/cupertino_dialog_action_control.rb +0 -24
  36. data/lib/ruflet_ui/ruflet/ui/controls/cupertino/cupertino_filled_button_control.rb +0 -13
  37. data/lib/ruflet_ui/ruflet/ui/controls/cupertino/cupertino_navigation_bar_control.rb +0 -13
  38. data/lib/ruflet_ui/ruflet/ui/controls/cupertino/cupertino_slider_control.rb +0 -13
  39. data/lib/ruflet_ui/ruflet/ui/controls/cupertino/cupertino_switch_control.rb +0 -13
  40. data/lib/ruflet_ui/ruflet/ui/controls/cupertino/cupertino_text_field_control.rb +0 -13
  41. data/lib/ruflet_ui/ruflet/ui/controls/material/alert_dialog_control.rb +0 -13
  42. data/lib/ruflet_ui/ruflet/ui/controls/material/app_bar_control.rb +0 -13
  43. data/lib/ruflet_ui/ruflet/ui/controls/material/bottom_sheet_control.rb +0 -13
  44. data/lib/ruflet_ui/ruflet/ui/controls/material/button_control.rb +0 -24
  45. data/lib/ruflet_ui/ruflet/ui/controls/material/checkbox_control.rb +0 -13
  46. data/lib/ruflet_ui/ruflet/ui/controls/material/column_control.rb +0 -13
  47. data/lib/ruflet_ui/ruflet/ui/controls/material/container_control.rb +0 -13
  48. data/lib/ruflet_ui/ruflet/ui/controls/material/drag_target_control.rb +0 -13
  49. data/lib/ruflet_ui/ruflet/ui/controls/material/draggable_control.rb +0 -13
  50. data/lib/ruflet_ui/ruflet/ui/controls/material/elevated_button_control.rb +0 -13
  51. data/lib/ruflet_ui/ruflet/ui/controls/material/filled_button_control.rb +0 -13
  52. data/lib/ruflet_ui/ruflet/ui/controls/material/floating_action_button_control.rb +0 -28
  53. data/lib/ruflet_ui/ruflet/ui/controls/material/gesture_detector_control.rb +0 -13
  54. data/lib/ruflet_ui/ruflet/ui/controls/material/grid_view_control.rb +0 -13
  55. data/lib/ruflet_ui/ruflet/ui/controls/material/icon_button_control.rb +0 -13
  56. data/lib/ruflet_ui/ruflet/ui/controls/material/icon_control.rb +0 -24
  57. data/lib/ruflet_ui/ruflet/ui/controls/material/image_control.rb +0 -13
  58. data/lib/ruflet_ui/ruflet/ui/controls/material/markdown_control.rb +0 -13
  59. data/lib/ruflet_ui/ruflet/ui/controls/material/navigation_bar_control.rb +0 -13
  60. data/lib/ruflet_ui/ruflet/ui/controls/material/navigation_bar_destination_control.rb +0 -13
  61. data/lib/ruflet_ui/ruflet/ui/controls/material/radio_control.rb +0 -13
  62. data/lib/ruflet_ui/ruflet/ui/controls/material/radio_group_control.rb +0 -13
  63. data/lib/ruflet_ui/ruflet/ui/controls/material/row_control.rb +0 -13
  64. data/lib/ruflet_ui/ruflet/ui/controls/material/snack_bar_control.rb +0 -68
  65. data/lib/ruflet_ui/ruflet/ui/controls/material/stack_control.rb +0 -13
  66. data/lib/ruflet_ui/ruflet/ui/controls/material/tab_bar_control.rb +0 -13
  67. data/lib/ruflet_ui/ruflet/ui/controls/material/tab_bar_view_control.rb +0 -13
  68. data/lib/ruflet_ui/ruflet/ui/controls/material/tab_control.rb +0 -13
  69. data/lib/ruflet_ui/ruflet/ui/controls/material/tabs_control.rb +0 -63
  70. data/lib/ruflet_ui/ruflet/ui/controls/material/text_button_control.rb +0 -13
  71. data/lib/ruflet_ui/ruflet/ui/controls/material/text_control.rb +0 -13
  72. data/lib/ruflet_ui/ruflet/ui/controls/material/text_field_control.rb +0 -13
  73. data/lib/ruflet_ui/ruflet/ui/controls/material/view_control.rb +0 -13
  74. data/lib/ruflet_ui/ruflet/ui/cupertino_control_factory.rb +0 -40
  75. data/lib/ruflet_ui/ruflet/ui/cupertino_control_methods.rb +0 -26
  76. data/lib/ruflet_ui/ruflet/ui/cupertino_control_registry.rb +0 -49
  77. data/lib/ruflet_ui/ruflet/ui/material_control_factory.rb +0 -97
  78. data/lib/ruflet_ui/ruflet/ui/material_control_methods.rb +0 -128
  79. data/lib/ruflet_ui/ruflet/ui/material_control_registry.rb +0 -154
  80. data/lib/ruflet_ui/ruflet/ui/shared_control_forwarders.rb +0 -89
  81. data/lib/ruflet_ui/ruflet/ui/widget_builder.rb +0 -55
  82. data/lib/ruflet_ui.rb +0 -111
@@ -1,741 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "event"
4
- require "ruflet_protocol"
5
- require_relative "control"
6
- require_relative "ui/widget_builder"
7
- require_relative "icons/material_icon_lookup"
8
- require_relative "icons/cupertino_icon_lookup"
9
- require "set"
10
- require "cgi"
11
- require "thread"
12
- require "timeout"
13
-
14
- module Ruflet
15
- class Page
16
- PAGE_PROP_KEYS = %w[route title vertical_alignment horizontal_alignment scroll].freeze
17
- DIALOG_PROP_KEYS = %w[dialog snack_bar bottom_sheet].freeze
18
- BUTTON_TEXT_TYPES = %w[button elevatedbutton textbutton filledbutton].freeze
19
- DEPRECATED_PAGE_WIDGET_METHODS = %i[
20
- control widget view column center row stack container gesture_detector gesturedetector draggable
21
- drag_target dragtarget text button elevated_button elevatedbutton text_button textbutton filled_button
22
- filledbutton icon_button iconbutton text_field textfield checkbox radio radio_group radiogroup
23
- alert_dialog alertdialog markdown icon image app_bar appbar floating_action_button snack_bar snackbar
24
- bottom_sheet bottomsheet tabs tab tab_bar tabbar tab_bar_view tabbarview navigation_bar navigationbar
25
- navigation_bar_destination navigationbardestination fab cupertino_button
26
- cupertinobutton cupertino_filled_button cupertinofilledbutton cupertino_text_field cupertinotextfield
27
- cupertino_switch cupertinoswitch cupertino_slider cupertinoslider cupertino_alert_dialog
28
- cupertinoalertdialog cupertino_action_sheet cupertinoactionsheet cupertino_dialog_action
29
- cupertinodialogaction cupertino_navigation_bar cupertinonavigationbar
30
- ].freeze
31
-
32
- attr_reader :session_id, :client_details, :views
33
-
34
- def initialize(session_id:, client_details:, sender:)
35
- @session_id = session_id
36
- @client_details = client_details
37
- @sender = sender
38
- @control_index = {}
39
- @wire_index = {}
40
- @next_wire_id = 100
41
- @view_id = 20
42
- @root_controls = []
43
- @views = []
44
- @dialogs = []
45
- @page_event_handlers = {}
46
- @view_props = {}
47
- @page_props = { "route" => (client_details["route"] || "/") }
48
- @overlay_container = Ruflet::Control.new(
49
- type: "overlay",
50
- id: "_overlay",
51
- controls: []
52
- )
53
- @services_container = Ruflet::Control.new(
54
- type: "service_registry",
55
- id: "_services",
56
- "_services": [],
57
- "_internals": { "uid" => Ruflet::Control.generate_id }
58
- )
59
- @dialogs_container = Ruflet::Control.new(
60
- type: "dialogs",
61
- id: "_dialogs",
62
- controls: []
63
- )
64
- @invoke_waiters = {}
65
- @invoke_waiters_mutex = Mutex.new
66
- refresh_overlay_container!
67
- refresh_services_container!
68
- refresh_dialogs_container!
69
- end
70
-
71
- def set_view_props(props)
72
- split_props(normalize_props(props || {}))
73
- self
74
- end
75
-
76
- def title
77
- @page_props["title"]
78
- end
79
-
80
- def title=(value)
81
- @page_props["title"] = value
82
- end
83
-
84
- def route
85
- @page_props["route"]
86
- end
87
-
88
- def route=(value)
89
- @page_props["route"] = value
90
- end
91
-
92
- def vertical_alignment
93
- @page_props["vertical_alignment"] || @view_props["vertical_alignment"]
94
- end
95
-
96
- def vertical_alignment=(value)
97
- v = normalize_value("vertical_alignment", value)
98
- @page_props["vertical_alignment"] = v
99
- @view_props["vertical_alignment"] = v
100
- end
101
-
102
- def horizontal_alignment
103
- @page_props["horizontal_alignment"] || @view_props["horizontal_alignment"]
104
- end
105
-
106
- def horizontal_alignment=(value)
107
- v = normalize_value("horizontal_alignment", value)
108
- @page_props["horizontal_alignment"] = v
109
- @view_props["horizontal_alignment"] = v
110
- end
111
-
112
- def bgcolor
113
- @view_props["bgcolor"]
114
- end
115
-
116
- def bgcolor=(value)
117
- @view_props["bgcolor"] = normalize_value("bgcolor", value)
118
- end
119
-
120
- def add(*controls, appbar: nil, floating_action_button: nil, navigation_bar: nil, dialog: nil, snack_bar: nil, bottom_sheet: nil)
121
- controls = controls.flatten
122
- visited = Set.new
123
- controls.each { |c| register_control_tree(c, visited) }
124
- @root_controls = controls
125
-
126
- @view_props["appbar"] = appbar if appbar
127
- @view_props["floating_action_button"] = floating_action_button if floating_action_button
128
- @view_props["navigation_bar"] = navigation_bar if navigation_bar
129
- @dialog = dialog if dialog
130
- @snack_bar = snack_bar if snack_bar
131
- @bottom_sheet = bottom_sheet if bottom_sheet
132
-
133
- refresh_dialogs_container!
134
- @view_props.each_value { |value| register_embedded_value(value, visited) }
135
-
136
- send_view_patch
137
-
138
- self
139
- end
140
-
141
- def views=(value)
142
- @views = Array(value).compact
143
- self
144
- end
145
-
146
- def services
147
- @services_container.props["_services"] ||= []
148
- end
149
-
150
- def services=(value)
151
- @services_container.props["_services"] = Array(value).compact
152
- refresh_services_container!
153
- push_services_update!
154
- self
155
- end
156
-
157
- def add_service(*value)
158
- @services_container.props["_services"] = services + value.flatten.compact
159
- refresh_services_container!
160
- push_services_update!
161
- self
162
- end
163
-
164
- def go(route, **query_params)
165
- @page_props["route"] = build_route(route, query_params)
166
- dispatch_page_event(name: "route_change", data: @page_props["route"])
167
- send_view_patch
168
- self
169
- end
170
-
171
- def on_route_change=(handler)
172
- @page_event_handlers["route_change"] = handler
173
- end
174
-
175
- def on_view_pop=(handler)
176
- @page_event_handlers["view_pop"] = handler
177
- end
178
-
179
- def on(event_name, &block)
180
- @page_event_handlers[event_name.to_s.sub(/\Aon_/, "")] = block
181
- self
182
- end
183
-
184
- def mount(&block)
185
- builder = WidgetBuilder.new
186
- builder.instance_eval(&block)
187
- add(*builder.children)
188
- end
189
-
190
- def appbar=(value)
191
- @view_props["appbar"] = value
192
- end
193
-
194
- def floating_action_button=(value)
195
- @view_props["floating_action_button"] = value
196
- end
197
-
198
- def dialog = @dialog
199
-
200
- def dialog=(value)
201
- @dialog = value
202
- refresh_dialogs_container!
203
- end
204
-
205
- def snack_bar=(value)
206
- @snack_bar = value
207
- refresh_dialogs_container!
208
- end
209
-
210
- def snackbar=(value)
211
- self.snack_bar = value
212
- end
213
-
214
- def bottom_sheet=(value)
215
- @bottom_sheet = value
216
- refresh_dialogs_container!
217
- end
218
-
219
- def bottomsheet=(value)
220
- self.bottom_sheet = value
221
- end
222
-
223
- def show_dialog(dialog_control)
224
- return self unless dialog_control
225
-
226
- return self if dialog_open?(dialog_control)
227
-
228
- dialog_control.props["open"] = true
229
- @dialogs << dialog_control unless @dialogs.include?(dialog_control)
230
- refresh_dialogs_container!
231
- send_view_patch unless @dialogs_container.wire_id
232
- push_dialogs_update!
233
- self
234
- end
235
-
236
- def invoke(control_or_id, method_name, args: nil, timeout: 10)
237
- control = resolve_control(control_or_id)
238
- return nil unless control
239
-
240
- call_id = "call_#{Ruflet::Control.generate_id}"
241
- send_message(Protocol::ACTIONS[:invoke_control_method], {
242
- "control_id" => control.wire_id,
243
- "call_id" => call_id,
244
- "name" => method_name.to_s,
245
- "args" => args,
246
- "timeout" => timeout
247
- })
248
-
249
- call_id
250
- end
251
-
252
- def launch_url(url, mode: "external_application", web_view_configuration: nil, browser_configuration: nil, web_only_window_name: nil, timeout: 10)
253
- launcher = ensure_url_launcher_service
254
- invoke(
255
- launcher,
256
- "launch_url",
257
- args: {
258
- "url" => url,
259
- "mode" => mode,
260
- "web_view_configuration" => web_view_configuration,
261
- "browser_configuration" => browser_configuration,
262
- "web_only_window_name" => web_only_window_name
263
- }.compact,
264
- timeout: timeout
265
- )
266
- end
267
-
268
- def can_launch_url(url, timeout: 10)
269
- launcher = ensure_url_launcher_service
270
- invoke(launcher, "can_launch_url", args: { "url" => url }, timeout: timeout)
271
- end
272
-
273
- def set_clipboard(value, timeout: 10)
274
- clipboard = ensure_clipboard_service
275
- invoke(clipboard, "set", args: { "data" => value.to_s }, timeout: timeout)
276
- end
277
-
278
- def get_clipboard(timeout: 10)
279
- clipboard = ensure_clipboard_service
280
- invoke(clipboard, "get", timeout: timeout)
281
- end
282
-
283
- def set_clipboard_files(files, timeout: 10)
284
- clipboard = ensure_clipboard_service
285
- invoke(clipboard, "set_files", args: { "files" => Array(files).map(&:to_s) }, timeout: timeout)
286
- end
287
-
288
- def get_clipboard_files(timeout: 10)
289
- clipboard = ensure_clipboard_service
290
- invoke(clipboard, "get_files", timeout: timeout)
291
- end
292
-
293
- def set_clipboard_image(value, timeout: 10)
294
- clipboard = ensure_clipboard_service
295
- invoke(clipboard, "set_image", args: { "data" => value }, timeout: timeout)
296
- end
297
-
298
- def get_clipboard_image(timeout: 10)
299
- clipboard = ensure_clipboard_service
300
- invoke(clipboard, "get_image", timeout: timeout)
301
- end
302
-
303
- def handle_invoke_method_result(payload)
304
- call_id = payload["call_id"].to_s
305
- waiter = @invoke_waiters_mutex.synchronize { @invoke_waiters[call_id] }
306
- return false unless waiter
307
-
308
- waiter << payload
309
- true
310
- end
311
-
312
- def pop_dialog
313
- dialog_control = latest_open_dialog
314
- return nil unless dialog_control
315
-
316
- dialog_control.props["open"] = false
317
- refresh_dialogs_container!
318
- push_dialogs_update!
319
- dialog_control
320
- end
321
-
322
- def update(control_or_id = nil, **props)
323
- if control_or_id.nil? && props.empty?
324
- send_view_patch
325
- return self
326
- end
327
-
328
- if page_control_target?(control_or_id)
329
- split_props(normalize_props(props))
330
- send_view_patch
331
- return self
332
- end
333
-
334
- control = resolve_control(control_or_id)
335
- return self unless control
336
-
337
- patch = normalize_props(props)
338
- if BUTTON_TEXT_TYPES.include?(control.type) && patch.key?("text")
339
- patch["content"] = patch.delete("text")
340
- end
341
-
342
- # Keep runtime control tree aligned with incremental patches.
343
- if patch.key?("controls")
344
- control.children.clear
345
- Array(patch["controls"]).each { |child| control.children << child if child.is_a?(Control) }
346
- end
347
-
348
- visited = Set.new
349
- patch.each_value { |value| register_embedded_value(value, visited) }
350
-
351
- patch_ops = patch.map { |k, v| [0, 0, k, serialize_patch_value(v)] }
352
-
353
- send_message(Protocol::ACTIONS[:patch_control], {
354
- "id" => control.wire_id,
355
- "patch" => [[0], *patch_ops]
356
- })
357
-
358
- self
359
- end
360
-
361
- def patch_page(control_id, **props)
362
- update(control_id, **props)
363
- end
364
-
365
- def apply_client_update(control_or_id, props)
366
- control = resolve_control(control_or_id)
367
- return self unless control
368
-
369
- patch = normalize_props(props || {})
370
- patch.each { |k, v| control.props[k] = v }
371
-
372
- remove_dialog_tracking(control) if patch.key?("open") && patch["open"] == false
373
-
374
- self
375
- end
376
-
377
- def dispatch_event(target:, name:, data:)
378
- if page_control_target?(target)
379
- if name.to_s == "route_change"
380
- route_from_event = extract_route(data)
381
- @page_props["route"] = route_from_event if route_from_event
382
- end
383
- dispatch_page_event(name: name, data: data)
384
- return
385
- end
386
-
387
- control = @wire_index[target.to_i] || @control_index[target.to_s]
388
- return unless control
389
-
390
- event = Event.new(name: name, target: target, raw_data: data, page: self, control: control)
391
- control.emit(name, event)
392
-
393
- if name.to_s == "dismiss" && remove_dialog_tracking(control)
394
- push_dialogs_update!
395
- end
396
- end
397
-
398
- def method_missing(name, *args, &block)
399
- method_name = name.to_s
400
- prop_name = method_name.delete_suffix("=")
401
-
402
- if method_name.end_with?("=")
403
- if DEPRECATED_PAGE_WIDGET_METHODS.include?(prop_name.to_sym)
404
- Kernel.warn("[DEPRECATION] `page.#{prop_name}(...)` is no longer supported.")
405
- raise NoMethodError, "Use `#{prop_name}(...)` as a free widget helper, then attach with `page.add(...)`."
406
- end
407
- assign_split_prop(prop_name, normalize_value(prop_name, args.first))
408
- return args.first
409
- end
410
-
411
- if args.empty? && !block
412
- return @page_props[method_name] if @page_props.key?(method_name)
413
- return @view_props[method_name] if @view_props.key?(method_name)
414
- return instance_variable_get("@#{method_name}") if DIALOG_PROP_KEYS.include?(method_name)
415
- end
416
-
417
- if DEPRECATED_PAGE_WIDGET_METHODS.include?(name.to_sym)
418
- Kernel.warn("[DEPRECATION] `page.#{name}(...)` is no longer supported.")
419
- raise NoMethodError, "Use `#{name}(...)` as a free widget helper, then attach with `page.add(...)`."
420
- end
421
-
422
- super
423
- end
424
-
425
- def respond_to_missing?(name, include_private = false)
426
- method_name = name.to_s
427
- prop_name = method_name.delete_suffix("=")
428
- DEPRECATED_PAGE_WIDGET_METHODS.include?(name.to_sym) ||
429
- DEPRECATED_PAGE_WIDGET_METHODS.include?(prop_name.to_sym) ||
430
- method_name.end_with?("=") ||
431
- @page_props.key?(method_name) ||
432
- @view_props.key?(method_name) ||
433
- DIALOG_PROP_KEYS.include?(method_name) ||
434
- super
435
- end
436
-
437
- private
438
-
439
- def invoke_and_wait(control_or_id, method_name, args: nil, timeout: 10)
440
- control = resolve_control(control_or_id)
441
- return nil unless control
442
-
443
- call_id = "call_#{Ruflet::Control.generate_id}"
444
- waiter = Queue.new
445
- @invoke_waiters_mutex.synchronize { @invoke_waiters[call_id] = waiter }
446
-
447
- send_message(Protocol::ACTIONS[:invoke_control_method], {
448
- "control_id" => control.wire_id,
449
- "call_id" => call_id,
450
- "name" => method_name.to_s,
451
- "args" => args,
452
- "timeout" => timeout
453
- })
454
-
455
- response = Timeout.timeout(timeout.to_f) { waiter.pop }
456
- error = response["error"]
457
- raise RuntimeError, error if error && !error.to_s.empty?
458
-
459
- response["result"]
460
- ensure
461
- @invoke_waiters_mutex.synchronize { @invoke_waiters.delete(call_id) } if call_id
462
- end
463
-
464
- def build_widget(type, **props, &block) = WidgetBuilder.new.control(type, **props, &block)
465
-
466
- def split_props(props)
467
- props.each do |k, v|
468
- assign_split_prop(k, v)
469
- end
470
- end
471
-
472
- def send_message(action, payload)
473
- @sender.call(action, payload)
474
- end
475
-
476
- def send_view_patch
477
- refresh_control_indexes!
478
- view_patches = build_view_patches
479
- page_patch_ops = build_page_patch_ops
480
-
481
- send_message(Protocol::ACTIONS[:patch_control], {
482
- "id" => 1,
483
- "patch" => [
484
- [0],
485
- [0, 0, "views", view_patches],
486
- *page_patch_ops
487
- ]
488
- })
489
- end
490
-
491
- def register_control_tree(control, visited = Set.new)
492
- return unless control
493
- return if visited.include?(control.object_id)
494
-
495
- visited << control.object_id
496
- assign_wire_id(control)
497
- control.runtime_page = self if control.respond_to?(:runtime_page=)
498
- @control_index[control.id.to_s] = control
499
- @wire_index[control.wire_id] = control
500
- control.children.each { |child| register_control_tree(child, visited) }
501
- control.props.each_value { |value| register_embedded_value(value, visited) }
502
- end
503
-
504
- def implicit_view_patch
505
- view_patch = {
506
- "_c" => "View",
507
- "_i" => @view_id,
508
- "route" => (@page_props["route"] || @client_details["route"] || "/"),
509
- # Required by Flet layout engine so children with `expand` inside View
510
- # are wrapped with Expanded/Flexible on the Flutter side.
511
- "_internals" => { "host_expanded" => true }
512
- }
513
- @view_props.each { |k, v| view_patch[k] = serialize_patch_value(v) }
514
- view_patch["controls"] = @root_controls.map(&:to_patch)
515
- view_patch
516
- end
517
-
518
- def refresh_control_indexes!
519
- @control_index.clear
520
- @wire_index.clear
521
- visited = Set.new
522
-
523
- if @views.any?
524
- @views.each { |view| register_control_tree(view, visited) }
525
- else
526
- @root_controls.each { |control| register_control_tree(control, visited) }
527
- @view_props.each_value { |value| register_embedded_value(value, visited) }
528
- end
529
- @page_props.each_value { |value| register_embedded_value(value, visited) }
530
- end
531
-
532
- def register_embedded_value(value, visited)
533
- case value
534
- when Control
535
- register_control_tree(value, visited)
536
- when Array
537
- value.each { |v| register_embedded_value(v, visited) }
538
- when Hash
539
- value.each_value { |v| register_embedded_value(v, visited) }
540
- end
541
- end
542
-
543
- def assign_wire_id(control)
544
- return if control.wire_id
545
-
546
- control.wire_id = @next_wire_id
547
- @next_wire_id += 1
548
- end
549
-
550
- def resolve_control(control_or_id)
551
- if control_or_id.respond_to?(:wire_id)
552
- control_or_id
553
- elsif control_or_id.to_s.match?(/^\d+$/)
554
- @wire_index[control_or_id.to_i]
555
- else
556
- @control_index[control_or_id.to_s]
557
- end
558
- end
559
-
560
- def normalize_props(hash)
561
- hash.each_with_object({}) do |(k, v), result|
562
- key = k.to_s
563
- key = "controls" if key == "children"
564
- result[key] = normalize_value(key, v)
565
- end
566
- end
567
-
568
- def normalize_value(key, value)
569
- if icon_prop_key?(key) && (value.is_a?(String) || value.is_a?(Symbol) || value.is_a?(Integer))
570
- codepoint = resolve_icon_codepoint(value)
571
- return codepoint unless codepoint.nil?
572
- end
573
-
574
- return value.value if value.is_a?(Ruflet::IconData)
575
- value.is_a?(Symbol) ? value.to_s : value
576
- end
577
-
578
- def build_route(route, query_params = {})
579
- base = route.to_s
580
- return base if query_params.nil? || query_params.empty?
581
-
582
- query = query_params.map { |k, v| "#{CGI.escape(k.to_s)}=#{CGI.escape(v.to_s)}" }.join("&")
583
- separator = base.include?("?") ? "&" : "?"
584
- "#{base}#{separator}#{query}"
585
- end
586
-
587
- def extract_route(data)
588
- case data
589
- when String
590
- data
591
- when Hash
592
- data["route"] || data[:route]
593
- else
594
- nil
595
- end
596
- end
597
-
598
- def dispatch_page_event(name:, data:)
599
- handler = @page_event_handlers[name.to_s.sub(/\Aon_/, "")]
600
- return unless handler.respond_to?(:call)
601
-
602
- event = Event.new(name: name.to_s, target: 1, raw_data: data, page: self, control: nil)
603
- handler.call(event)
604
- end
605
-
606
- def page_control_target?(control_or_id)
607
- control_or_id == 1 || control_or_id.to_s == "1" || control_or_id.to_s == "page"
608
- end
609
-
610
- def serialize_patch_value(value)
611
- case value
612
- when Control
613
- value.to_patch
614
- when Ruflet::IconData
615
- value.value
616
- when Array
617
- value.map { |v| serialize_patch_value(v) }
618
- when Hash
619
- value.transform_values { |v| serialize_patch_value(v) }
620
- else
621
- value
622
- end
623
- end
624
-
625
- def icon_prop_key?(key)
626
- key == "icon" || key.end_with?("_icon")
627
- end
628
-
629
- def refresh_dialogs_container!
630
- dialog_controls = (@dialogs + dialog_slots).uniq
631
- @dialogs_container.props["controls"] = dialog_controls
632
- @page_props["_dialogs"] = @dialogs_container
633
- end
634
-
635
- def refresh_overlay_container!
636
- @page_props["_overlay"] = @overlay_container
637
- end
638
-
639
- def refresh_services_container!
640
- @page_props["_services"] = @services_container
641
- end
642
-
643
- def push_services_update!
644
- refresh_control_indexes!
645
-
646
- if @services_container.wire_id
647
- send_message(Protocol::ACTIONS[:patch_control], {
648
- "id" => @services_container.wire_id,
649
- "patch" => [[0], [0, 0, "_services", serialize_patch_value(@services_container.props["_services"])]]
650
- })
651
- else
652
- send_view_patch
653
- end
654
- end
655
-
656
- def push_dialogs_update!
657
- refresh_control_indexes!
658
-
659
- if @dialogs_container.wire_id
660
- send_message(Protocol::ACTIONS[:patch_control], {
661
- "id" => @dialogs_container.wire_id,
662
- "patch" => [[0], [0, 0, "controls", serialize_patch_value(@dialogs_container.props["controls"])]]
663
- })
664
- else
665
- send_view_patch
666
- end
667
- end
668
-
669
- def dialog_slots
670
- [@dialog, @snack_bar, @bottom_sheet].compact
671
- end
672
-
673
- def latest_open_dialog
674
- @dialogs.reverse.find { |d| d.props["open"] != false }
675
- end
676
-
677
- def dialog_open?(dialog_control)
678
- @dialogs.include?(dialog_control) && dialog_control.props["open"] == true
679
- end
680
-
681
- def remove_dialog_tracking(control)
682
- return false unless @dialogs.include?(control)
683
-
684
- @dialogs.delete(control)
685
- refresh_dialogs_container!
686
- true
687
- end
688
-
689
- def assign_split_prop(key, value)
690
- if key == "vertical_alignment" || key == "horizontal_alignment"
691
- @page_props[key] = value
692
- @view_props[key] = value
693
- elsif DIALOG_PROP_KEYS.include?(key)
694
- instance_variable_set("@#{key}", value)
695
- refresh_dialogs_container!
696
- elsif PAGE_PROP_KEYS.include?(key)
697
- @page_props[key] = value
698
- else
699
- @view_props[key] = value
700
- end
701
- end
702
-
703
- def build_view_patches
704
- if @views.any?
705
- @views.map(&:to_patch)
706
- else
707
- [implicit_view_patch]
708
- end
709
- end
710
-
711
- def build_page_patch_ops
712
- @page_props.map { |k, v| [0, 0, k, serialize_patch_value(v)] }
713
- end
714
-
715
- def resolve_icon_codepoint(value)
716
- codepoint = Ruflet::MaterialIconLookup.codepoint_for(value)
717
- if codepoint.nil? || codepoint == value
718
- codepoint = Ruflet::CupertinoIconLookup.codepoint_for(value)
719
- end
720
- codepoint
721
- end
722
-
723
- def ensure_url_launcher_service
724
- launcher = services.find { |service| service.is_a?(Control) && service.type == "url_launcher" }
725
- return launcher if launcher
726
-
727
- launcher = build_widget(:url_launcher)
728
- add_service(launcher)
729
- launcher
730
- end
731
-
732
- def ensure_clipboard_service
733
- clipboard = services.find { |service| service.is_a?(Control) && service.type == "clipboard" }
734
- return clipboard if clipboard
735
-
736
- clipboard = build_widget(:clipboard)
737
- add_service(clipboard)
738
- clipboard
739
- end
740
- end
741
- end