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.
- checksums.yaml +7 -0
- data/README.md +3 -0
- data/lib/ruflet/version.rb +5 -0
- data/lib/ruflet.rb +40 -0
- data/lib/ruflet_protocol/ruflet/protocol.rb +62 -0
- data/lib/ruflet_protocol.rb +4 -0
- data/lib/ruflet_ui/ruflet/app.rb +20 -0
- data/lib/ruflet_ui/ruflet/colors.rb +200 -0
- data/lib/ruflet_ui/ruflet/control.rb +154 -0
- data/lib/ruflet_ui/ruflet/dsl.rb +182 -0
- data/lib/ruflet_ui/ruflet/event.rb +28 -0
- data/lib/ruflet_ui/ruflet/icon_data.rb +57 -0
- data/lib/ruflet_ui/ruflet/icons/cupertino/cupertino_icons.rb +54 -0
- data/lib/ruflet_ui/ruflet/icons/cupertino_icon_lookup.rb +112 -0
- data/lib/ruflet_ui/ruflet/icons/material_icon_lookup.rb +112 -0
- data/lib/ruflet_ui/ruflet/icons/material_icons.rb +55 -0
- data/lib/ruflet_ui/ruflet/page.rb +550 -0
- data/lib/ruflet_ui/ruflet/ui/control_factory.rb +22 -0
- data/lib/ruflet_ui/ruflet/ui/control_methods.rb +16 -0
- data/lib/ruflet_ui/ruflet/ui/control_registry.rb +13 -0
- data/lib/ruflet_ui/ruflet/ui/controls/cupertino/cupertino_action_sheet_control.rb +13 -0
- data/lib/ruflet_ui/ruflet/ui/controls/cupertino/cupertino_alert_dialog_control.rb +13 -0
- data/lib/ruflet_ui/ruflet/ui/controls/cupertino/cupertino_button_control.rb +13 -0
- data/lib/ruflet_ui/ruflet/ui/controls/cupertino/cupertino_dialog_action_control.rb +13 -0
- data/lib/ruflet_ui/ruflet/ui/controls/cupertino/cupertino_filled_button_control.rb +13 -0
- data/lib/ruflet_ui/ruflet/ui/controls/cupertino/cupertino_navigation_bar_control.rb +13 -0
- data/lib/ruflet_ui/ruflet/ui/controls/cupertino/cupertino_slider_control.rb +13 -0
- data/lib/ruflet_ui/ruflet/ui/controls/cupertino/cupertino_switch_control.rb +13 -0
- data/lib/ruflet_ui/ruflet/ui/controls/cupertino/cupertino_text_field_control.rb +13 -0
- data/lib/ruflet_ui/ruflet/ui/controls/material/alert_dialog_control.rb +13 -0
- data/lib/ruflet_ui/ruflet/ui/controls/material/app_bar_control.rb +13 -0
- data/lib/ruflet_ui/ruflet/ui/controls/material/bottom_sheet_control.rb +13 -0
- data/lib/ruflet_ui/ruflet/ui/controls/material/button_control.rb +24 -0
- data/lib/ruflet_ui/ruflet/ui/controls/material/checkbox_control.rb +13 -0
- data/lib/ruflet_ui/ruflet/ui/controls/material/column_control.rb +13 -0
- data/lib/ruflet_ui/ruflet/ui/controls/material/container_control.rb +13 -0
- data/lib/ruflet_ui/ruflet/ui/controls/material/drag_target_control.rb +13 -0
- data/lib/ruflet_ui/ruflet/ui/controls/material/draggable_control.rb +13 -0
- data/lib/ruflet_ui/ruflet/ui/controls/material/elevated_button_control.rb +13 -0
- data/lib/ruflet_ui/ruflet/ui/controls/material/filled_button_control.rb +13 -0
- data/lib/ruflet_ui/ruflet/ui/controls/material/floating_action_button_control.rb +28 -0
- data/lib/ruflet_ui/ruflet/ui/controls/material/gesture_detector_control.rb +13 -0
- data/lib/ruflet_ui/ruflet/ui/controls/material/icon_button_control.rb +13 -0
- data/lib/ruflet_ui/ruflet/ui/controls/material/icon_control.rb +24 -0
- data/lib/ruflet_ui/ruflet/ui/controls/material/image_control.rb +13 -0
- data/lib/ruflet_ui/ruflet/ui/controls/material/markdown_control.rb +13 -0
- data/lib/ruflet_ui/ruflet/ui/controls/material/navigation_bar_control.rb +13 -0
- data/lib/ruflet_ui/ruflet/ui/controls/material/navigation_bar_destination_control.rb +13 -0
- data/lib/ruflet_ui/ruflet/ui/controls/material/radio_control.rb +13 -0
- data/lib/ruflet_ui/ruflet/ui/controls/material/radio_group_control.rb +13 -0
- data/lib/ruflet_ui/ruflet/ui/controls/material/row_control.rb +13 -0
- data/lib/ruflet_ui/ruflet/ui/controls/material/snack_bar_control.rb +13 -0
- data/lib/ruflet_ui/ruflet/ui/controls/material/stack_control.rb +13 -0
- data/lib/ruflet_ui/ruflet/ui/controls/material/tab_bar_control.rb +13 -0
- data/lib/ruflet_ui/ruflet/ui/controls/material/tab_bar_view_control.rb +13 -0
- data/lib/ruflet_ui/ruflet/ui/controls/material/tab_control.rb +13 -0
- data/lib/ruflet_ui/ruflet/ui/controls/material/tabs_control.rb +63 -0
- data/lib/ruflet_ui/ruflet/ui/controls/material/text_button_control.rb +13 -0
- data/lib/ruflet_ui/ruflet/ui/controls/material/text_control.rb +13 -0
- data/lib/ruflet_ui/ruflet/ui/controls/material/text_field_control.rb +13 -0
- data/lib/ruflet_ui/ruflet/ui/controls/material/view_control.rb +13 -0
- data/lib/ruflet_ui/ruflet/ui/cupertino_control_factory.rb +40 -0
- data/lib/ruflet_ui/ruflet/ui/cupertino_control_methods.rb +26 -0
- data/lib/ruflet_ui/ruflet/ui/cupertino_control_registry.rb +35 -0
- data/lib/ruflet_ui/ruflet/ui/material_control_factory.rb +94 -0
- data/lib/ruflet_ui/ruflet/ui/material_control_methods.rb +116 -0
- data/lib/ruflet_ui/ruflet/ui/material_control_registry.rb +88 -0
- data/lib/ruflet_ui/ruflet/ui/shared_control_forwarders.rb +85 -0
- data/lib/ruflet_ui/ruflet/ui/widget_builder.rb +48 -0
- data/lib/ruflet_ui.rb +112 -0
- 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 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
|