ruflet 0.0.1

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 (71) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +3 -0
  3. data/lib/ruflet/version.rb +5 -0
  4. data/lib/ruflet.rb +40 -0
  5. data/lib/ruflet_protocol/ruflet/protocol.rb +62 -0
  6. data/lib/ruflet_protocol.rb +4 -0
  7. data/lib/ruflet_ui/ruflet/app.rb +20 -0
  8. data/lib/ruflet_ui/ruflet/colors.rb +200 -0
  9. data/lib/ruflet_ui/ruflet/control.rb +154 -0
  10. data/lib/ruflet_ui/ruflet/dsl.rb +182 -0
  11. data/lib/ruflet_ui/ruflet/event.rb +28 -0
  12. data/lib/ruflet_ui/ruflet/icon_data.rb +57 -0
  13. data/lib/ruflet_ui/ruflet/icons/cupertino/cupertino_icons.rb +54 -0
  14. data/lib/ruflet_ui/ruflet/icons/cupertino_icon_lookup.rb +112 -0
  15. data/lib/ruflet_ui/ruflet/icons/material_icon_lookup.rb +112 -0
  16. data/lib/ruflet_ui/ruflet/icons/material_icons.rb +55 -0
  17. data/lib/ruflet_ui/ruflet/page.rb +550 -0
  18. data/lib/ruflet_ui/ruflet/ui/control_factory.rb +22 -0
  19. data/lib/ruflet_ui/ruflet/ui/control_methods.rb +16 -0
  20. data/lib/ruflet_ui/ruflet/ui/control_registry.rb +13 -0
  21. data/lib/ruflet_ui/ruflet/ui/controls/cupertino/cupertino_action_sheet_control.rb +13 -0
  22. data/lib/ruflet_ui/ruflet/ui/controls/cupertino/cupertino_alert_dialog_control.rb +13 -0
  23. data/lib/ruflet_ui/ruflet/ui/controls/cupertino/cupertino_button_control.rb +13 -0
  24. data/lib/ruflet_ui/ruflet/ui/controls/cupertino/cupertino_dialog_action_control.rb +13 -0
  25. data/lib/ruflet_ui/ruflet/ui/controls/cupertino/cupertino_filled_button_control.rb +13 -0
  26. data/lib/ruflet_ui/ruflet/ui/controls/cupertino/cupertino_navigation_bar_control.rb +13 -0
  27. data/lib/ruflet_ui/ruflet/ui/controls/cupertino/cupertino_slider_control.rb +13 -0
  28. data/lib/ruflet_ui/ruflet/ui/controls/cupertino/cupertino_switch_control.rb +13 -0
  29. data/lib/ruflet_ui/ruflet/ui/controls/cupertino/cupertino_text_field_control.rb +13 -0
  30. data/lib/ruflet_ui/ruflet/ui/controls/material/alert_dialog_control.rb +13 -0
  31. data/lib/ruflet_ui/ruflet/ui/controls/material/app_bar_control.rb +13 -0
  32. data/lib/ruflet_ui/ruflet/ui/controls/material/bottom_sheet_control.rb +13 -0
  33. data/lib/ruflet_ui/ruflet/ui/controls/material/button_control.rb +24 -0
  34. data/lib/ruflet_ui/ruflet/ui/controls/material/checkbox_control.rb +13 -0
  35. data/lib/ruflet_ui/ruflet/ui/controls/material/column_control.rb +13 -0
  36. data/lib/ruflet_ui/ruflet/ui/controls/material/container_control.rb +13 -0
  37. data/lib/ruflet_ui/ruflet/ui/controls/material/drag_target_control.rb +13 -0
  38. data/lib/ruflet_ui/ruflet/ui/controls/material/draggable_control.rb +13 -0
  39. data/lib/ruflet_ui/ruflet/ui/controls/material/elevated_button_control.rb +13 -0
  40. data/lib/ruflet_ui/ruflet/ui/controls/material/filled_button_control.rb +13 -0
  41. data/lib/ruflet_ui/ruflet/ui/controls/material/floating_action_button_control.rb +28 -0
  42. data/lib/ruflet_ui/ruflet/ui/controls/material/gesture_detector_control.rb +13 -0
  43. data/lib/ruflet_ui/ruflet/ui/controls/material/icon_button_control.rb +13 -0
  44. data/lib/ruflet_ui/ruflet/ui/controls/material/icon_control.rb +24 -0
  45. data/lib/ruflet_ui/ruflet/ui/controls/material/image_control.rb +13 -0
  46. data/lib/ruflet_ui/ruflet/ui/controls/material/markdown_control.rb +13 -0
  47. data/lib/ruflet_ui/ruflet/ui/controls/material/navigation_bar_control.rb +13 -0
  48. data/lib/ruflet_ui/ruflet/ui/controls/material/navigation_bar_destination_control.rb +13 -0
  49. data/lib/ruflet_ui/ruflet/ui/controls/material/radio_control.rb +13 -0
  50. data/lib/ruflet_ui/ruflet/ui/controls/material/radio_group_control.rb +13 -0
  51. data/lib/ruflet_ui/ruflet/ui/controls/material/row_control.rb +13 -0
  52. data/lib/ruflet_ui/ruflet/ui/controls/material/snack_bar_control.rb +13 -0
  53. data/lib/ruflet_ui/ruflet/ui/controls/material/stack_control.rb +13 -0
  54. data/lib/ruflet_ui/ruflet/ui/controls/material/tab_bar_control.rb +13 -0
  55. data/lib/ruflet_ui/ruflet/ui/controls/material/tab_bar_view_control.rb +13 -0
  56. data/lib/ruflet_ui/ruflet/ui/controls/material/tab_control.rb +13 -0
  57. data/lib/ruflet_ui/ruflet/ui/controls/material/tabs_control.rb +63 -0
  58. data/lib/ruflet_ui/ruflet/ui/controls/material/text_button_control.rb +13 -0
  59. data/lib/ruflet_ui/ruflet/ui/controls/material/text_control.rb +13 -0
  60. data/lib/ruflet_ui/ruflet/ui/controls/material/text_field_control.rb +13 -0
  61. data/lib/ruflet_ui/ruflet/ui/controls/material/view_control.rb +13 -0
  62. data/lib/ruflet_ui/ruflet/ui/cupertino_control_factory.rb +40 -0
  63. data/lib/ruflet_ui/ruflet/ui/cupertino_control_methods.rb +26 -0
  64. data/lib/ruflet_ui/ruflet/ui/cupertino_control_registry.rb +35 -0
  65. data/lib/ruflet_ui/ruflet/ui/material_control_factory.rb +94 -0
  66. data/lib/ruflet_ui/ruflet/ui/material_control_methods.rb +116 -0
  67. data/lib/ruflet_ui/ruflet/ui/material_control_registry.rb +88 -0
  68. data/lib/ruflet_ui/ruflet/ui/shared_control_forwarders.rb +85 -0
  69. data/lib/ruflet_ui/ruflet/ui/widget_builder.rb +48 -0
  70. data/lib/ruflet_ui.rb +112 -0
  71. metadata +109 -0
@@ -0,0 +1,550 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "event"
4
+ require "ruflet_protocol"
5
+ require_relative "control"
6
+ require_relative "ui/control_methods"
7
+ require_relative "ui/widget_builder"
8
+ require_relative "icons/material_icon_lookup"
9
+ require_relative "icons/cupertino_icon_lookup"
10
+ require "set"
11
+ require "cgi"
12
+
13
+ module Ruflet
14
+ class Page
15
+ include UI::ControlMethods
16
+
17
+ PAGE_PROP_KEYS = %w[route title vertical_alignment horizontal_alignment].freeze
18
+ DIALOG_PROP_KEYS = %w[dialog snack_bar bottom_sheet].freeze
19
+ BUTTON_TEXT_TYPES = %w[button elevatedbutton textbutton filledbutton].freeze
20
+
21
+ attr_reader :session_id, :client_details, :views
22
+
23
+ def initialize(session_id:, client_details:, sender:)
24
+ @session_id = session_id
25
+ @client_details = client_details
26
+ @sender = sender
27
+ @control_index = {}
28
+ @wire_index = {}
29
+ @next_wire_id = 100
30
+ @view_id = 20
31
+ @root_controls = []
32
+ @views = []
33
+ @dialogs = []
34
+ @page_event_handlers = {}
35
+ @view_props = {}
36
+ @page_props = { "route" => (client_details["route"] || "/") }
37
+ @overlay_container = Ruflet::Control.new(
38
+ type: "overlay",
39
+ id: "_overlay",
40
+ controls: []
41
+ )
42
+ @dialogs_container = Ruflet::Control.new(
43
+ type: "dialogs",
44
+ id: "_dialogs",
45
+ controls: []
46
+ )
47
+ refresh_overlay_container!
48
+ refresh_dialogs_container!
49
+ end
50
+
51
+ def set_view_props(props)
52
+ split_props(normalize_props(props || {}))
53
+ self
54
+ end
55
+
56
+ def title
57
+ @page_props["title"]
58
+ end
59
+
60
+ def title=(value)
61
+ @page_props["title"] = value
62
+ end
63
+
64
+ def route
65
+ @page_props["route"]
66
+ end
67
+
68
+ def route=(value)
69
+ @page_props["route"] = value
70
+ end
71
+
72
+ def vertical_alignment
73
+ @page_props["vertical_alignment"] || @view_props["vertical_alignment"]
74
+ end
75
+
76
+ def vertical_alignment=(value)
77
+ v = normalize_value("vertical_alignment", value)
78
+ @page_props["vertical_alignment"] = v
79
+ @view_props["vertical_alignment"] = v
80
+ end
81
+
82
+ def horizontal_alignment
83
+ @page_props["horizontal_alignment"] || @view_props["horizontal_alignment"]
84
+ end
85
+
86
+ def horizontal_alignment=(value)
87
+ v = normalize_value("horizontal_alignment", value)
88
+ @page_props["horizontal_alignment"] = v
89
+ @view_props["horizontal_alignment"] = v
90
+ end
91
+
92
+ def bgcolor
93
+ @view_props["bgcolor"]
94
+ end
95
+
96
+ def bgcolor=(value)
97
+ @view_props["bgcolor"] = normalize_value("bgcolor", value)
98
+ end
99
+
100
+ def add(*controls, appbar: nil, floating_action_button: nil, navigation_bar: nil, dialog: nil, snack_bar: nil, bottom_sheet: nil)
101
+ controls = controls.flatten
102
+ visited = Set.new
103
+ controls.each { |c| register_control_tree(c, visited) }
104
+ @root_controls = controls
105
+
106
+ @view_props["appbar"] = appbar if appbar
107
+ @view_props["floating_action_button"] = floating_action_button if floating_action_button
108
+ @view_props["navigation_bar"] = navigation_bar if navigation_bar
109
+ @dialog = dialog if dialog
110
+ @snack_bar = snack_bar if snack_bar
111
+ @bottom_sheet = bottom_sheet if bottom_sheet
112
+
113
+ refresh_dialogs_container!
114
+ @view_props.each_value { |value| register_embedded_value(value, visited) }
115
+
116
+ send_view_patch
117
+
118
+ self
119
+ end
120
+
121
+ def views=(value)
122
+ @views = Array(value).compact
123
+ self
124
+ end
125
+
126
+ def go(route, **query_params)
127
+ @page_props["route"] = build_route(route, query_params)
128
+ dispatch_page_event(name: "route_change", data: @page_props["route"])
129
+ send_view_patch
130
+ self
131
+ end
132
+
133
+ def on_route_change=(handler)
134
+ @page_event_handlers["route_change"] = handler
135
+ end
136
+
137
+ def on_view_pop=(handler)
138
+ @page_event_handlers["view_pop"] = handler
139
+ end
140
+
141
+ def on(event_name, &block)
142
+ @page_event_handlers[event_name.to_s.sub(/\Aon_/, "")] = block
143
+ self
144
+ end
145
+
146
+ def mount(&block)
147
+ builder = WidgetBuilder.new
148
+ builder.instance_eval(&block)
149
+ add(*builder.children)
150
+ end
151
+
152
+ def appbar(**props, &block)
153
+ return @view_props["appbar"] if props.empty? && !block
154
+
155
+ WidgetBuilder.new.appbar(**props, &block)
156
+ end
157
+
158
+ def appbar=(value)
159
+ @view_props["appbar"] = value
160
+ end
161
+
162
+ def floating_action_button(**props, &block)
163
+ return @view_props["floating_action_button"] if props.empty? && !block
164
+
165
+ WidgetBuilder.new.floating_action_button(**props, &block)
166
+ end
167
+
168
+ def floating_action_button=(value)
169
+ @view_props["floating_action_button"] = value
170
+ end
171
+
172
+ def dialog = @dialog
173
+
174
+ def dialog=(value)
175
+ @dialog = value
176
+ refresh_dialogs_container!
177
+ end
178
+
179
+ def snack_bar(**props, &block)
180
+ return @snack_bar if props.empty? && !block
181
+
182
+ super
183
+ end
184
+
185
+ def snack_bar=(value)
186
+ @snack_bar = value
187
+ refresh_dialogs_container!
188
+ end
189
+
190
+ def snackbar(**props, &block)
191
+ snack_bar(**props, &block)
192
+ end
193
+
194
+ def snackbar=(value)
195
+ self.snack_bar = value
196
+ end
197
+
198
+ def bottom_sheet(**props, &block)
199
+ return @bottom_sheet if props.empty? && !block
200
+
201
+ super
202
+ end
203
+
204
+ def bottom_sheet=(value)
205
+ @bottom_sheet = value
206
+ refresh_dialogs_container!
207
+ end
208
+
209
+ def bottomsheet(**props, &block)
210
+ bottom_sheet(**props, &block)
211
+ end
212
+
213
+ def bottomsheet=(value)
214
+ self.bottom_sheet = value
215
+ end
216
+
217
+ def show_dialog(dialog_control)
218
+ return self unless dialog_control
219
+
220
+ return self if dialog_open?(dialog_control)
221
+
222
+ dialog_control.props["open"] = true
223
+ @dialogs << dialog_control unless @dialogs.include?(dialog_control)
224
+ refresh_dialogs_container!
225
+ send_view_patch unless @dialogs_container.wire_id
226
+ push_dialogs_update!
227
+ self
228
+ end
229
+
230
+ def pop_dialog
231
+ dialog_control = latest_open_dialog
232
+ return nil unless dialog_control
233
+
234
+ dialog_control.props["open"] = false
235
+ refresh_dialogs_container!
236
+ push_dialogs_update!
237
+ dialog_control
238
+ end
239
+
240
+ def update(control_or_id = nil, **props)
241
+ if control_or_id.nil? && props.empty?
242
+ send_view_patch
243
+ return self
244
+ end
245
+
246
+ if page_control_target?(control_or_id)
247
+ split_props(normalize_props(props))
248
+ send_view_patch
249
+ return self
250
+ end
251
+
252
+ control = resolve_control(control_or_id)
253
+ return self unless control
254
+
255
+ patch = normalize_props(props)
256
+ if BUTTON_TEXT_TYPES.include?(control.type) && patch.key?("text")
257
+ patch["content"] = patch.delete("text")
258
+ end
259
+
260
+ patch_ops = patch.map { |k, v| [0, 0, k, v] }
261
+
262
+ send_message(Protocol::ACTIONS[:patch_control], {
263
+ "id" => control.wire_id,
264
+ "patch" => [[0], *patch_ops]
265
+ })
266
+
267
+ self
268
+ end
269
+
270
+ def patch_page(control_id, **props)
271
+ update(control_id, **props)
272
+ end
273
+
274
+ def apply_client_update(control_or_id, props)
275
+ control = resolve_control(control_or_id)
276
+ return self unless control
277
+
278
+ patch = normalize_props(props || {})
279
+ patch.each { |k, v| control.props[k] = v }
280
+
281
+ remove_dialog_tracking(control) if patch.key?("open") && patch["open"] == false
282
+
283
+ self
284
+ end
285
+
286
+ def dispatch_event(target:, name:, data:)
287
+ if page_control_target?(target)
288
+ if name.to_s == "route_change"
289
+ route_from_event = extract_route(data)
290
+ @page_props["route"] = route_from_event if route_from_event
291
+ end
292
+ dispatch_page_event(name: name, data: data)
293
+ return
294
+ end
295
+
296
+ control = @wire_index[target.to_i] || @control_index[target.to_s]
297
+ return unless control
298
+
299
+ event = Event.new(name: name, target: target, raw_data: data, page: self, control: control)
300
+ control.emit(name, event)
301
+
302
+ if name.to_s == "dismiss" && remove_dialog_tracking(control)
303
+ push_dialogs_update!
304
+ end
305
+ end
306
+
307
+ private
308
+
309
+ def build_widget(type, **props, &block) = WidgetBuilder.new.control(type, **props, &block)
310
+
311
+ def split_props(props)
312
+ props.each do |k, v|
313
+ assign_split_prop(k, v)
314
+ end
315
+ end
316
+
317
+ def send_message(action, payload)
318
+ @sender.call(action, payload)
319
+ end
320
+
321
+ def send_view_patch
322
+ refresh_control_indexes!
323
+ view_patches = build_view_patches
324
+ page_patch_ops = build_page_patch_ops
325
+
326
+ send_message(Protocol::ACTIONS[:patch_control], {
327
+ "id" => 1,
328
+ "patch" => [
329
+ [0],
330
+ [0, 0, "views", view_patches],
331
+ *page_patch_ops
332
+ ]
333
+ })
334
+ end
335
+
336
+ def register_control_tree(control, visited = Set.new)
337
+ return unless control
338
+ return if visited.include?(control.object_id)
339
+
340
+ visited << control.object_id
341
+ assign_wire_id(control)
342
+ control.runtime_page = self if control.respond_to?(:runtime_page=)
343
+ @control_index[control.id.to_s] = control
344
+ @wire_index[control.wire_id] = control
345
+ control.children.each { |child| register_control_tree(child, visited) }
346
+ control.props.each_value { |value| register_embedded_value(value, visited) }
347
+ end
348
+
349
+ def implicit_view_patch
350
+ view_patch = {
351
+ "_c" => "View",
352
+ "_i" => @view_id,
353
+ "route" => (@page_props["route"] || @client_details["route"] || "/"),
354
+ # Required by Flet layout engine so children with `expand` inside View
355
+ # are wrapped with Expanded/Flexible on the Flutter side.
356
+ "_internals" => { "host_expanded" => true }
357
+ }
358
+ @view_props.each { |k, v| view_patch[k] = serialize_patch_value(v) }
359
+ view_patch["controls"] = @root_controls.map(&:to_patch)
360
+ view_patch
361
+ end
362
+
363
+ def refresh_control_indexes!
364
+ @control_index.clear
365
+ @wire_index.clear
366
+ visited = Set.new
367
+
368
+ if @views.any?
369
+ @views.each { |view| register_control_tree(view, visited) }
370
+ else
371
+ @root_controls.each { |control| register_control_tree(control, visited) }
372
+ @view_props.each_value { |value| register_embedded_value(value, visited) }
373
+ end
374
+ @page_props.each_value { |value| register_embedded_value(value, visited) }
375
+ end
376
+
377
+ def register_embedded_value(value, visited)
378
+ case value
379
+ when Control
380
+ register_control_tree(value, visited)
381
+ when Array
382
+ value.each { |v| register_embedded_value(v, visited) }
383
+ when Hash
384
+ value.each_value { |v| register_embedded_value(v, visited) }
385
+ end
386
+ end
387
+
388
+ def assign_wire_id(control)
389
+ return if control.wire_id
390
+
391
+ control.wire_id = @next_wire_id
392
+ @next_wire_id += 1
393
+ end
394
+
395
+ def resolve_control(control_or_id)
396
+ if control_or_id.respond_to?(:wire_id)
397
+ control_or_id
398
+ elsif control_or_id.to_s.match?(/^\d+$/)
399
+ @wire_index[control_or_id.to_i]
400
+ else
401
+ @control_index[control_or_id.to_s]
402
+ end
403
+ end
404
+
405
+ def normalize_props(hash)
406
+ hash.each_with_object({}) do |(k, v), result|
407
+ key = k.to_s
408
+ result[key] = normalize_value(key, v)
409
+ end
410
+ end
411
+
412
+ def normalize_value(key, value)
413
+ if icon_prop_key?(key) && (value.is_a?(String) || value.is_a?(Symbol) || value.is_a?(Integer))
414
+ codepoint = resolve_icon_codepoint(value)
415
+ return codepoint unless codepoint.nil?
416
+ end
417
+
418
+ return value.value if value.is_a?(Ruflet::IconData)
419
+ value.is_a?(Symbol) ? value.to_s : value
420
+ end
421
+
422
+ def build_route(route, query_params = {})
423
+ base = route.to_s
424
+ return base if query_params.nil? || query_params.empty?
425
+
426
+ query = query_params.map { |k, v| "#{CGI.escape(k.to_s)}=#{CGI.escape(v.to_s)}" }.join("&")
427
+ separator = base.include?("?") ? "&" : "?"
428
+ "#{base}#{separator}#{query}"
429
+ end
430
+
431
+ def extract_route(data)
432
+ case data
433
+ when String
434
+ data
435
+ when Hash
436
+ data["route"] || data[:route]
437
+ else
438
+ nil
439
+ end
440
+ end
441
+
442
+ def dispatch_page_event(name:, data:)
443
+ handler = @page_event_handlers[name.to_s.sub(/\Aon_/, "")]
444
+ return unless handler.respond_to?(:call)
445
+
446
+ event = Event.new(name: name.to_s, target: 1, raw_data: data, page: self, control: nil)
447
+ handler.call(event)
448
+ end
449
+
450
+ def page_control_target?(control_or_id)
451
+ control_or_id == 1 || control_or_id.to_s == "1" || control_or_id.to_s == "page"
452
+ end
453
+
454
+ def serialize_patch_value(value)
455
+ case value
456
+ when Control
457
+ value.to_patch
458
+ when Ruflet::IconData
459
+ value.value
460
+ when Array
461
+ value.map { |v| serialize_patch_value(v) }
462
+ when Hash
463
+ value.transform_values { |v| serialize_patch_value(v) }
464
+ else
465
+ value
466
+ end
467
+ end
468
+
469
+ def icon_prop_key?(key)
470
+ key == "icon" || key.end_with?("_icon")
471
+ end
472
+
473
+ def refresh_dialogs_container!
474
+ dialog_controls = (@dialogs + dialog_slots).uniq
475
+ @dialogs_container.props["controls"] = dialog_controls
476
+ @page_props["_dialogs"] = @dialogs_container
477
+ end
478
+
479
+ def refresh_overlay_container!
480
+ @page_props["_overlay"] = @overlay_container
481
+ end
482
+
483
+ def push_dialogs_update!
484
+ refresh_control_indexes!
485
+
486
+ if @dialogs_container.wire_id
487
+ send_message(Protocol::ACTIONS[:patch_control], {
488
+ "id" => @dialogs_container.wire_id,
489
+ "patch" => [[0], [0, 0, "controls", serialize_patch_value(@dialogs_container.props["controls"])]]
490
+ })
491
+ else
492
+ send_view_patch
493
+ end
494
+ end
495
+
496
+ def dialog_slots
497
+ [@dialog, @snack_bar, @bottom_sheet].compact
498
+ end
499
+
500
+ def latest_open_dialog
501
+ @dialogs.reverse.find { |d| d.props["open"] != false }
502
+ end
503
+
504
+ def dialog_open?(dialog_control)
505
+ @dialogs.include?(dialog_control) && dialog_control.props["open"] == true
506
+ end
507
+
508
+ def remove_dialog_tracking(control)
509
+ return false unless @dialogs.include?(control)
510
+
511
+ @dialogs.delete(control)
512
+ refresh_dialogs_container!
513
+ true
514
+ end
515
+
516
+ def assign_split_prop(key, value)
517
+ if key == "vertical_alignment" || key == "horizontal_alignment"
518
+ @page_props[key] = value
519
+ @view_props[key] = value
520
+ elsif DIALOG_PROP_KEYS.include?(key)
521
+ instance_variable_set("@#{key}", value)
522
+ refresh_dialogs_container!
523
+ elsif PAGE_PROP_KEYS.include?(key)
524
+ @page_props[key] = value
525
+ else
526
+ @view_props[key] = value
527
+ end
528
+ end
529
+
530
+ def build_view_patches
531
+ if @views.any?
532
+ @views.map(&:to_patch)
533
+ else
534
+ [implicit_view_patch]
535
+ end
536
+ end
537
+
538
+ def build_page_patch_ops
539
+ @page_props.map { |k, v| [0, 0, k, serialize_patch_value(v)] }
540
+ end
541
+
542
+ def resolve_icon_codepoint(value)
543
+ codepoint = Ruflet::MaterialIconLookup.codepoint_for(value)
544
+ if codepoint.nil? || codepoint == value
545
+ codepoint = Ruflet::CupertinoIconLookup.codepoint_for(value)
546
+ end
547
+ codepoint
548
+ end
549
+ end
550
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "material_control_factory"
4
+ require_relative "cupertino_control_factory"
5
+
6
+ module Ruflet
7
+ module UI
8
+ module ControlFactory
9
+ module_function
10
+
11
+ CLASS_MAP = MaterialControlFactory::CLASS_MAP.merge(CupertinoControlFactory::CLASS_MAP).freeze
12
+
13
+ def build(type, id: nil, **props)
14
+ normalized_type = type.to_s.downcase
15
+ klass = CLASS_MAP[normalized_type]
16
+ raise ArgumentError, "Unsupported control type: #{normalized_type}" unless klass
17
+
18
+ klass.new(id: id, **props)
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "material_control_methods"
4
+ require_relative "cupertino_control_methods"
5
+
6
+ module Ruflet
7
+ module UI
8
+ module ControlMethods
9
+ include MaterialControlMethods
10
+ include CupertinoControlMethods
11
+
12
+ def control(type, **props, &block) = build_widget(type, **props, &block)
13
+ def widget(type, **props, &block) = build_widget(type, **props, &block)
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ruflet
4
+ module UI
5
+ module ControlRegistry
6
+ require_relative "material_control_registry"
7
+ require_relative "cupertino_control_registry"
8
+
9
+ TYPE_MAP = MaterialControlRegistry::TYPE_MAP.merge(CupertinoControlRegistry::TYPE_MAP).freeze
10
+ EVENT_PROPS = MaterialControlRegistry::EVENT_PROPS.merge(CupertinoControlRegistry::EVENT_PROPS).freeze
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ruflet
4
+ module UI
5
+ module Controls
6
+ class CupertinoActionSheetControl < Ruflet::Control
7
+ def initialize(id: nil, **props)
8
+ super(type: "cupertino_action_sheet", id: id, **props)
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ruflet
4
+ module UI
5
+ module Controls
6
+ class CupertinoAlertDialogControl < Ruflet::Control
7
+ def initialize(id: nil, **props)
8
+ super(type: "cupertino_alert_dialog", id: id, **props)
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ruflet
4
+ module UI
5
+ module Controls
6
+ class CupertinoButtonControl < Ruflet::Control
7
+ def initialize(id: nil, **props)
8
+ super(type: "cupertino_button", id: id, **props)
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ruflet
4
+ module UI
5
+ module Controls
6
+ class CupertinoDialogActionControl < Ruflet::Control
7
+ def initialize(id: nil, **props)
8
+ super(type: "cupertino_dialog_action", id: id, **props)
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ruflet
4
+ module UI
5
+ module Controls
6
+ class CupertinoFilledButtonControl < Ruflet::Control
7
+ def initialize(id: nil, **props)
8
+ super(type: "cupertino_filled_button", id: id, **props)
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ruflet
4
+ module UI
5
+ module Controls
6
+ class CupertinoNavigationBarControl < Ruflet::Control
7
+ def initialize(id: nil, **props)
8
+ super(type: "cupertino_navigation_bar", id: id, **props)
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ruflet
4
+ module UI
5
+ module Controls
6
+ class CupertinoSliderControl < Ruflet::Control
7
+ def initialize(id: nil, **props)
8
+ super(type: "cupertino_slider", id: id, **props)
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end