ruflet_core 0.0.13 → 0.0.15
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 +4 -4
- data/lib/ruflet/version.rb +1 -1
- data/lib/ruflet_protocol/ruflet/protocol.rb +8 -1
- data/lib/ruflet_ui/ruflet/control.rb +31 -0
- data/lib/ruflet_ui/ruflet/page.rb +112 -19
- data/lib/ruflet_ui/ruflet/ui/controls/materials/audio_control.rb +2 -4
- data/lib/ruflet_ui/ruflet/ui/controls/materials/map_controls.rb +80 -9
- data/lib/ruflet_ui/ruflet/ui/controls/materials/webview_control.rb +114 -1
- data/lib/ruflet_ui/ruflet/ui/controls/shared/window_control.rb +28 -4
- data/lib/ruflet_ui/ruflet/ui/services/ruflet/audio_recorder_control.rb +2 -0
- data/lib/ruflet_ui/ruflet/ui/services/ruflet/permissionhandler_control.rb +2 -0
- data/lib/ruflet_ui/ruflet/ui/services/ruflet/securestorage_control.rb +2 -0
- data/lib/ruflet_ui.rb +6 -2
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c1823b100c3e97ee941849763509b25179554464310eb640d7954bb48a30d821
|
|
4
|
+
data.tar.gz: 80d3f6891f3418bf0b2b3aa2007e08e6578ca336b72bb48b9d84c54cac2397b5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e37e04ea218fbc50da5d3a456ecdfe376f12d00fc802c0869da7dff977bafb6d0abab8eecff92e223b42002f94652e40f7d91d2eb7a62f04fbec72bf1c7dd600
|
|
7
|
+
data.tar.gz: 42458a85edccff3f66fd88cdc71724a7a94dbf1853fbf080d0751da7b57e783694e0ba65091c908bb4b1f203d6a8e8725e9e594df762f7deb1996e29e1bc88ed
|
data/lib/ruflet/version.rb
CHANGED
|
@@ -39,6 +39,12 @@ module Ruflet
|
|
|
39
39
|
"height" => page["height"],
|
|
40
40
|
"platform" => page["platform"],
|
|
41
41
|
"platform_brightness" => page["platform_brightness"],
|
|
42
|
+
# The Flutter client reports the host OS in "platform" even inside a
|
|
43
|
+
# browser, so "web" is the only reliable way to tell a web client from a
|
|
44
|
+
# native one. (pwa/wasm passed through for completeness.)
|
|
45
|
+
"web" => page["web"],
|
|
46
|
+
"pwa" => page["pwa"],
|
|
47
|
+
"wasm" => page["wasm"],
|
|
42
48
|
"media" => page["media"] || {}
|
|
43
49
|
}
|
|
44
50
|
end
|
|
@@ -97,7 +103,8 @@ module Ruflet
|
|
|
97
103
|
}
|
|
98
104
|
end
|
|
99
105
|
|
|
100
|
-
def register_response(session_id:, page_patch:
|
|
106
|
+
def register_response(session_id:, page_patch: nil, error: nil)
|
|
107
|
+
page_patch ||= {}
|
|
101
108
|
{
|
|
102
109
|
"session_id" => session_id,
|
|
103
110
|
"page_patch" => page_patch,
|
|
@@ -47,6 +47,37 @@ module Ruflet
|
|
|
47
47
|
@handlers.key?(normalized_event_name(event_name))
|
|
48
48
|
end
|
|
49
49
|
|
|
50
|
+
# Read a prop by name: control["value"] or control[:value].
|
|
51
|
+
def [](key)
|
|
52
|
+
@props[key.to_s]
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Write a prop by name: control["value"] = "x".
|
|
56
|
+
def []=(key, value)
|
|
57
|
+
@props[key.to_s] = value
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Convenience dot access to props, mirroring Flet-style controls:
|
|
61
|
+
# control.value # => @props["value"] (reads an existing prop)
|
|
62
|
+
# control.value = "hello" # => sets @props["value"]
|
|
63
|
+
# Reads only resolve props that exist, so typos still raise NoMethodError
|
|
64
|
+
# instead of silently returning nil. Defined methods (type, id, props,
|
|
65
|
+
# children, on, emit, to_patch, …) are never shadowed.
|
|
66
|
+
def method_missing(name, *args, &block)
|
|
67
|
+
key = name.to_s
|
|
68
|
+
if key.end_with?("=")
|
|
69
|
+
return @props[key[0..-2]] = args.first
|
|
70
|
+
end
|
|
71
|
+
return @props[key] if args.empty? && @props.key?(key)
|
|
72
|
+
|
|
73
|
+
super
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def respond_to_missing?(name, include_private = false)
|
|
77
|
+
key = name.to_s
|
|
78
|
+
key.end_with?("=") || @props.key?(key) || super
|
|
79
|
+
end
|
|
80
|
+
|
|
50
81
|
def to_patch
|
|
51
82
|
wire_type = schema_wire_type_for_class
|
|
52
83
|
if wire_type.nil?
|
|
@@ -160,6 +160,8 @@ module Ruflet
|
|
|
160
160
|
@root_controls = []
|
|
161
161
|
@views = []
|
|
162
162
|
@dialogs = []
|
|
163
|
+
@overlay_container_mounted = false
|
|
164
|
+
@dialogs_container_mounted = false
|
|
163
165
|
@services_container_mounted = false
|
|
164
166
|
@visual_service_controls = {}
|
|
165
167
|
@page_event_handlers = {}
|
|
@@ -248,10 +250,10 @@ module Ruflet
|
|
|
248
250
|
controls.each { |c| register_control_tree(c, visited) }
|
|
249
251
|
@root_controls = controls
|
|
250
252
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
253
|
+
update_view_slot("appbar", appbar)
|
|
254
|
+
update_view_slot("bottom_appbar", bottom_appbar)
|
|
255
|
+
update_view_slot("floating_action_button", floating_action_button)
|
|
256
|
+
update_view_slot("navigation_bar", navigation_bar)
|
|
255
257
|
@dialog = dialog if dialog
|
|
256
258
|
@snack_bar = snack_bar if snack_bar
|
|
257
259
|
@bottom_sheet = bottom_sheet if bottom_sheet
|
|
@@ -482,11 +484,13 @@ module Ruflet
|
|
|
482
484
|
def dialog=(value)
|
|
483
485
|
@dialog = value
|
|
484
486
|
refresh_dialogs_container!
|
|
487
|
+
push_dialogs_update! if @dialogs_container_mounted
|
|
485
488
|
end
|
|
486
489
|
|
|
487
490
|
def snack_bar=(value)
|
|
488
491
|
@snack_bar = value
|
|
489
492
|
refresh_dialogs_container!
|
|
493
|
+
push_dialogs_update! if @dialogs_container_mounted
|
|
490
494
|
end
|
|
491
495
|
|
|
492
496
|
def snackbar=(value)
|
|
@@ -508,6 +512,7 @@ module Ruflet
|
|
|
508
512
|
return self if dialog_open?(dialog_control)
|
|
509
513
|
|
|
510
514
|
dialog_control.props["open"] = true
|
|
515
|
+
remove_existing_singleton_dialogs(dialog_control)
|
|
511
516
|
@dialogs << dialog_control unless @dialogs.include?(dialog_control)
|
|
512
517
|
refresh_dialogs_container!
|
|
513
518
|
send_view_patch unless @dialogs_container.wire_id
|
|
@@ -528,7 +533,7 @@ module Ruflet
|
|
|
528
533
|
call_id = "call_#{Ruflet::Control.generate_id}"
|
|
529
534
|
if on_result.respond_to?(:call)
|
|
530
535
|
@invoke_waiters_mutex.synchronize { @invoke_callbacks[call_id] = on_result }
|
|
531
|
-
|
|
536
|
+
if embedded_async_timeout_available? && !timeout.nil?
|
|
532
537
|
Thread.new(call_id, timeout.to_f) do |pending_call_id, invoke_timeout|
|
|
533
538
|
sleep([invoke_timeout, 0.0].max + 0.1)
|
|
534
539
|
callback = @invoke_waiters_mutex.synchronize { @invoke_callbacks.delete(pending_call_id) }
|
|
@@ -1145,9 +1150,23 @@ module Ruflet
|
|
|
1145
1150
|
return nil unless dialog_control
|
|
1146
1151
|
|
|
1147
1152
|
dialog_control.props["open"] = false
|
|
1153
|
+
update(dialog_control, open: false)
|
|
1154
|
+
dialog_control
|
|
1155
|
+
end
|
|
1156
|
+
|
|
1157
|
+
def close_dialog(dialog_control)
|
|
1158
|
+
return self unless dialog_control
|
|
1159
|
+
|
|
1160
|
+
dialog_control.props["open"] = false
|
|
1161
|
+
@dialog = nil if @dialog.equal?(dialog_control)
|
|
1162
|
+
remove_dialog_tracking(dialog_control)
|
|
1148
1163
|
refresh_dialogs_container!
|
|
1164
|
+
# Patch the dialogs container in place. Forcing a full view re-render
|
|
1165
|
+
# here would remount the whole overlay — fatal while another dialog
|
|
1166
|
+
# (e.g. the form behind a nested picker) is still open. The empty case
|
|
1167
|
+
# is handled inside push_dialogs_update!.
|
|
1149
1168
|
push_dialogs_update!
|
|
1150
|
-
|
|
1169
|
+
self
|
|
1151
1170
|
end
|
|
1152
1171
|
|
|
1153
1172
|
def update(control_or_id = nil, **props)
|
|
@@ -1185,6 +1204,7 @@ module Ruflet
|
|
|
1185
1204
|
|
|
1186
1205
|
visited = Set.new
|
|
1187
1206
|
patch.each_value { |value| register_embedded_value(value, visited) }
|
|
1207
|
+
patch.each { |k, v| control.props[k] = v }
|
|
1188
1208
|
|
|
1189
1209
|
patch_ops = patch.map { |k, v| [0, 0, k, serialize_patch_value(v)] }
|
|
1190
1210
|
|
|
@@ -1207,8 +1227,6 @@ module Ruflet
|
|
|
1207
1227
|
patch = normalize_props(props || {})
|
|
1208
1228
|
patch.each { |k, v| control.props[k] = v }
|
|
1209
1229
|
|
|
1210
|
-
remove_dialog_tracking(control) if patch.key?("open") && patch["open"] == false
|
|
1211
|
-
|
|
1212
1230
|
self
|
|
1213
1231
|
end
|
|
1214
1232
|
|
|
@@ -1216,6 +1234,10 @@ module Ruflet
|
|
|
1216
1234
|
if page_control_target?(target)
|
|
1217
1235
|
if name.to_s == "route_change"
|
|
1218
1236
|
route_from_event = extract_route(data)
|
|
1237
|
+
# Dialogs (including pickers) belong to the view that opened them.
|
|
1238
|
+
# Navigating away must dismiss them, or they ghost onto the next
|
|
1239
|
+
# view — the picker that "reappears after going home".
|
|
1240
|
+
dismiss_tracked_dialogs! if route_from_event && route_from_event != @page_props["route"]
|
|
1219
1241
|
@page_props["route"] = route_from_event if route_from_event
|
|
1220
1242
|
end
|
|
1221
1243
|
dispatch_page_event(name: name, data: data)
|
|
@@ -1225,13 +1247,19 @@ module Ruflet
|
|
|
1225
1247
|
control = @wire_index[target.to_i] || @control_index[target.to_s]
|
|
1226
1248
|
return unless control
|
|
1227
1249
|
|
|
1228
|
-
event = Event.new(name: name, target: target, raw_data: data, page: self, control: control)
|
|
1250
|
+
event = Ruflet::Event.new(name: name, target: target, raw_data: data, page: self, control: control)
|
|
1229
1251
|
apply_event_value_to_control(control, event) if %w[change select select_change].include?(name.to_s)
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1252
|
+
# Material/Cupertino pickers dismiss themselves on the client once a
|
|
1253
|
+
# value is confirmed, but only send a value event — never a close. Mark
|
|
1254
|
+
# the dialog closed here so show_dialog can reopen it next time.
|
|
1255
|
+
mark_picker_dialog_closed(control, name)
|
|
1256
|
+
if dialog_close_event?(control, name) && remove_dialog_tracking(control)
|
|
1257
|
+
# Patch the container in place; never force a full view re-render that
|
|
1258
|
+
# would remount a still-open parent dialog (the nested-picker case).
|
|
1233
1259
|
push_dialogs_update!
|
|
1234
1260
|
end
|
|
1261
|
+
|
|
1262
|
+
control.emit(name, event)
|
|
1235
1263
|
end
|
|
1236
1264
|
|
|
1237
1265
|
def method_missing(name, *args, &block)
|
|
@@ -1273,6 +1301,10 @@ module Ruflet
|
|
|
1273
1301
|
|
|
1274
1302
|
private
|
|
1275
1303
|
|
|
1304
|
+
def embedded_async_timeout_available?
|
|
1305
|
+
!Object.const_defined?(:RUFLET_EMBEDDED_FAKE_THREAD)
|
|
1306
|
+
end
|
|
1307
|
+
|
|
1276
1308
|
def invoke_and_wait(control_or_id, method_name, args: nil, timeout: 10)
|
|
1277
1309
|
control_id =
|
|
1278
1310
|
if page_control_target?(control_or_id)
|
|
@@ -1370,6 +1402,14 @@ module Ruflet
|
|
|
1370
1402
|
@sender.call(action, payload)
|
|
1371
1403
|
end
|
|
1372
1404
|
|
|
1405
|
+
def update_view_slot(name, value)
|
|
1406
|
+
if value.nil?
|
|
1407
|
+
@view_props.delete(name)
|
|
1408
|
+
else
|
|
1409
|
+
@view_props[name] = value
|
|
1410
|
+
end
|
|
1411
|
+
end
|
|
1412
|
+
|
|
1373
1413
|
def send_view_patch
|
|
1374
1414
|
refresh_control_indexes!
|
|
1375
1415
|
view_patches = build_view_patches
|
|
@@ -1379,10 +1419,12 @@ module Ruflet
|
|
|
1379
1419
|
"id" => 1,
|
|
1380
1420
|
"patch" => [
|
|
1381
1421
|
[0],
|
|
1382
|
-
|
|
1383
|
-
|
|
1422
|
+
*page_patch_ops,
|
|
1423
|
+
[0, 0, "views", view_patches]
|
|
1384
1424
|
]
|
|
1385
1425
|
})
|
|
1426
|
+
@overlay_container_mounted = true if @overlay_container.wire_id
|
|
1427
|
+
@dialogs_container_mounted = true if @dialogs_container.wire_id
|
|
1386
1428
|
@services_container_mounted = true if @services_container.wire_id
|
|
1387
1429
|
end
|
|
1388
1430
|
|
|
@@ -1534,8 +1576,8 @@ module Ruflet
|
|
|
1534
1576
|
end_value = range_value["end_value"] || range_value[:end_value]
|
|
1535
1577
|
control.props["start_value"] = start_value unless start_value.nil?
|
|
1536
1578
|
control.props["end_value"] = end_value unless end_value.nil?
|
|
1537
|
-
return
|
|
1538
1579
|
end
|
|
1580
|
+
return
|
|
1539
1581
|
end
|
|
1540
1582
|
|
|
1541
1583
|
return if value.nil?
|
|
@@ -1582,7 +1624,7 @@ module Ruflet
|
|
|
1582
1624
|
end
|
|
1583
1625
|
|
|
1584
1626
|
def refresh_dialogs_container!
|
|
1585
|
-
dialog_controls = (
|
|
1627
|
+
dialog_controls = (dialog_slots + @dialogs).uniq
|
|
1586
1628
|
@dialogs_container.props["controls"] = dialog_controls
|
|
1587
1629
|
@page_props["_dialogs"] = @dialogs_container
|
|
1588
1630
|
end
|
|
@@ -1611,6 +1653,12 @@ module Ruflet
|
|
|
1611
1653
|
def push_dialogs_update!
|
|
1612
1654
|
refresh_control_indexes!
|
|
1613
1655
|
|
|
1656
|
+
# Once the dialogs container is mounted, every change — opening, closing,
|
|
1657
|
+
# even down to no dialogs at all — is an in-place patch of its controls
|
|
1658
|
+
# list. Re-sending the view (or the container as a whole object) would
|
|
1659
|
+
# replace the live container instance on the Flutter side, detaching its
|
|
1660
|
+
# listeners and breaking any other dialog still open. Only the very first
|
|
1661
|
+
# dialog, before the container has a wire id, needs a view patch to mount.
|
|
1614
1662
|
if @dialogs_container.wire_id
|
|
1615
1663
|
send_message(Protocol::ACTIONS[:patch_control], {
|
|
1616
1664
|
"id" => @dialogs_container.wire_id,
|
|
@@ -1633,6 +1681,41 @@ module Ruflet
|
|
|
1633
1681
|
@dialogs.include?(dialog_control) && dialog_control.props["open"] == true
|
|
1634
1682
|
end
|
|
1635
1683
|
|
|
1684
|
+
def dialog_close_event?(control, name)
|
|
1685
|
+
name = name.to_s
|
|
1686
|
+
name == "dismiss" || (%w[change select select_change].include?(name) && @dialogs.include?(control) && control.props["open"] == false)
|
|
1687
|
+
end
|
|
1688
|
+
|
|
1689
|
+
# Picker dialogs that auto-dismiss on the client after a selection. Their
|
|
1690
|
+
# confirm sends a value event (change/select), not a close, so the server
|
|
1691
|
+
# must flip `open` to false or show_dialog's open-guard blocks reopening.
|
|
1692
|
+
PICKER_DIALOG_TYPES = %w[
|
|
1693
|
+
datepicker daterangepicker timepicker
|
|
1694
|
+
cupertinodatepicker cupertinotimerpicker
|
|
1695
|
+
].freeze
|
|
1696
|
+
|
|
1697
|
+
def picker_dialog?(control)
|
|
1698
|
+
PICKER_DIALOG_TYPES.include?(control.type.to_s.tr("_", "").downcase)
|
|
1699
|
+
end
|
|
1700
|
+
|
|
1701
|
+
def mark_picker_dialog_closed(control, name)
|
|
1702
|
+
return unless picker_dialog?(control)
|
|
1703
|
+
return unless %w[change select select_change dismiss].include?(name.to_s)
|
|
1704
|
+
|
|
1705
|
+
control.props["open"] = false
|
|
1706
|
+
end
|
|
1707
|
+
|
|
1708
|
+
# Close and untrack every dialog currently shown. Called on navigation so
|
|
1709
|
+
# a dialog opened in one view does not linger as an overlay on the next.
|
|
1710
|
+
def dismiss_tracked_dialogs!
|
|
1711
|
+
return if @dialogs.empty?
|
|
1712
|
+
|
|
1713
|
+
@dialogs.each { |dialog| dialog.props["open"] = false }
|
|
1714
|
+
@dialogs.clear
|
|
1715
|
+
refresh_dialogs_container!
|
|
1716
|
+
push_dialogs_update! if @dialogs_container_mounted
|
|
1717
|
+
end
|
|
1718
|
+
|
|
1636
1719
|
def remove_dialog_tracking(control)
|
|
1637
1720
|
return false unless @dialogs.include?(control)
|
|
1638
1721
|
|
|
@@ -1641,6 +1724,16 @@ module Ruflet
|
|
|
1641
1724
|
true
|
|
1642
1725
|
end
|
|
1643
1726
|
|
|
1727
|
+
def remove_existing_singleton_dialogs(control)
|
|
1728
|
+
return unless singleton_dialog_control?(control)
|
|
1729
|
+
|
|
1730
|
+
@dialogs.delete_if { |dialog| dialog != control && singleton_dialog_control?(dialog) }
|
|
1731
|
+
end
|
|
1732
|
+
|
|
1733
|
+
def singleton_dialog_control?(control)
|
|
1734
|
+
control.type.to_s.tr("_", "").downcase == "snackbar"
|
|
1735
|
+
end
|
|
1736
|
+
|
|
1644
1737
|
def assign_split_prop(key, value)
|
|
1645
1738
|
if key == "vertical_alignment" || key == "horizontal_alignment"
|
|
1646
1739
|
@page_props[key] = value
|
|
@@ -1668,8 +1761,8 @@ module Ruflet
|
|
|
1668
1761
|
# Keep internal containers stable after initial mount.
|
|
1669
1762
|
# Re-sending them as full objects can replace Control instances with
|
|
1670
1763
|
# same IDs and detach service invoke listeners on the Flutter side.
|
|
1671
|
-
next nil if k == "_overlay" && @
|
|
1672
|
-
next nil if k == "_dialogs" && @
|
|
1764
|
+
next nil if k == "_overlay" && @overlay_container_mounted
|
|
1765
|
+
next nil if k == "_dialogs" && @dialogs_container_mounted
|
|
1673
1766
|
next nil if k == "_services" && @services_container_mounted
|
|
1674
1767
|
|
|
1675
1768
|
[0, 0, k, serialize_patch_value(v)]
|
|
@@ -1774,7 +1867,7 @@ module Ruflet
|
|
|
1774
1867
|
call_id = "call_#{Ruflet::Control.generate_id}"
|
|
1775
1868
|
if on_result.respond_to?(:call)
|
|
1776
1869
|
@invoke_waiters_mutex.synchronize { @invoke_callbacks[call_id] = on_result }
|
|
1777
|
-
|
|
1870
|
+
if embedded_async_timeout_available? && !timeout.nil?
|
|
1778
1871
|
Thread.new(call_id, timeout.to_f) do |pending_call_id, invoke_timeout|
|
|
1779
1872
|
sleep([invoke_timeout, 0.0].max + 0.1)
|
|
1780
1873
|
callback = @invoke_waiters_mutex.synchronize { @invoke_callbacks.delete(pending_call_id) }
|
|
@@ -44,10 +44,8 @@ module Ruflet
|
|
|
44
44
|
runtime_page&.invoke(self, "pause", timeout: timeout, on_result: on_result)
|
|
45
45
|
end
|
|
46
46
|
|
|
47
|
-
def play(position:
|
|
48
|
-
args
|
|
49
|
-
args["position"] = position unless position.nil?
|
|
50
|
-
runtime_page&.invoke(self, "play", args: args.empty? ? nil : args, timeout: timeout, on_result: on_result)
|
|
47
|
+
def play(position: 0, timeout: 10, on_result: nil)
|
|
48
|
+
runtime_page&.invoke(self, "play", args: { "position" => position }, timeout: timeout, on_result: on_result)
|
|
51
49
|
end
|
|
52
50
|
|
|
53
51
|
def release(timeout: 10, on_result: nil)
|
|
@@ -4,14 +4,37 @@ module Ruflet
|
|
|
4
4
|
module UI
|
|
5
5
|
module Controls
|
|
6
6
|
module RufletComponents
|
|
7
|
+
module MapValueNormalizer
|
|
8
|
+
private
|
|
9
|
+
|
|
10
|
+
def normalize_map_coordinate(value)
|
|
11
|
+
case value
|
|
12
|
+
when Array
|
|
13
|
+
if value.length == 2 && value.all? { |item| item.is_a?(Numeric) }
|
|
14
|
+
{ "latitude" => value[0], "longitude" => value[1] }
|
|
15
|
+
else
|
|
16
|
+
value.map { |item| normalize_map_coordinate(item) }
|
|
17
|
+
end
|
|
18
|
+
when Hash
|
|
19
|
+
value.transform_keys(&:to_s).each_with_object({}) do |(key, item), result|
|
|
20
|
+
result[key] = normalize_map_coordinate(item)
|
|
21
|
+
end
|
|
22
|
+
else
|
|
23
|
+
value.respond_to?(:to_h) ? normalize_map_coordinate(value.to_h) : value
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
7
28
|
class MapControl < Ruflet::Control
|
|
29
|
+
include MapValueNormalizer
|
|
30
|
+
|
|
8
31
|
TYPE = "map".freeze
|
|
9
32
|
WIRE = "Map".freeze
|
|
10
33
|
|
|
11
34
|
def initialize(id: nil, layers: nil, initial_center: nil, initial_zoom: nil, min_zoom: nil, max_zoom: nil, interaction_configuration: nil, keep_alive: nil, bgcolor: nil, data: nil, expand: nil, height: nil, key: nil, visible: nil, width: nil, on_init: nil, on_long_press: nil, on_position_change: nil, on_secondary_tap: nil, on_tap: nil)
|
|
12
35
|
props = {}
|
|
13
36
|
props[:layers] = layers unless layers.nil?
|
|
14
|
-
props[:initial_center] = initial_center unless initial_center.nil?
|
|
37
|
+
props[:initial_center] = normalize_map_coordinate(initial_center) unless initial_center.nil?
|
|
15
38
|
props[:initial_zoom] = initial_zoom unless initial_zoom.nil?
|
|
16
39
|
props[:min_zoom] = min_zoom unless min_zoom.nil?
|
|
17
40
|
props[:max_zoom] = max_zoom unless max_zoom.nil?
|
|
@@ -32,12 +55,52 @@ module Ruflet
|
|
|
32
55
|
super(type: TYPE, id: id, **props)
|
|
33
56
|
end
|
|
34
57
|
|
|
35
|
-
def center_on(coordinates, zoom: nil, timeout: 10, on_result: nil)
|
|
36
|
-
|
|
58
|
+
def center_on(point = nil, coordinates: nil, zoom: nil, duration: nil, curve: nil, cancel_ongoing_animations: nil, timeout: 10, on_result: nil)
|
|
59
|
+
point ||= coordinates
|
|
60
|
+
invoke_map(
|
|
61
|
+
"center_on",
|
|
62
|
+
args: compact_args(
|
|
63
|
+
"point" => normalize_map_coordinate(point),
|
|
64
|
+
"zoom" => zoom,
|
|
65
|
+
"duration" => duration,
|
|
66
|
+
"curve" => curve,
|
|
67
|
+
"cancel_ongoing_animations" => cancel_ongoing_animations
|
|
68
|
+
),
|
|
69
|
+
timeout: timeout,
|
|
70
|
+
on_result: on_result
|
|
71
|
+
)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def move_to(destination = nil, coordinates: nil, zoom: nil, rotation: nil, offset: nil, duration: nil, curve: nil, cancel_ongoing_animations: nil, timeout: 10, on_result: nil)
|
|
75
|
+
destination ||= coordinates
|
|
76
|
+
invoke_map(
|
|
77
|
+
"move_to",
|
|
78
|
+
args: compact_args(
|
|
79
|
+
"destination" => normalize_map_coordinate(destination),
|
|
80
|
+
"zoom" => zoom,
|
|
81
|
+
"rotation" => rotation,
|
|
82
|
+
"offset" => offset,
|
|
83
|
+
"duration" => duration,
|
|
84
|
+
"curve" => curve,
|
|
85
|
+
"cancel_ongoing_animations" => cancel_ongoing_animations
|
|
86
|
+
),
|
|
87
|
+
timeout: timeout,
|
|
88
|
+
on_result: on_result
|
|
89
|
+
)
|
|
37
90
|
end
|
|
38
91
|
|
|
39
|
-
def
|
|
40
|
-
invoke_map(
|
|
92
|
+
def zoom_to(zoom, duration: nil, curve: nil, cancel_ongoing_animations: nil, timeout: 10, on_result: nil)
|
|
93
|
+
invoke_map(
|
|
94
|
+
"zoom_to",
|
|
95
|
+
args: compact_args(
|
|
96
|
+
"zoom" => zoom,
|
|
97
|
+
"duration" => duration,
|
|
98
|
+
"curve" => curve,
|
|
99
|
+
"cancel_ongoing_animations" => cancel_ongoing_animations
|
|
100
|
+
),
|
|
101
|
+
timeout: timeout,
|
|
102
|
+
on_result: on_result
|
|
103
|
+
)
|
|
41
104
|
end
|
|
42
105
|
|
|
43
106
|
def zoom_in(delta: nil, timeout: 10, on_result: nil)
|
|
@@ -118,12 +181,14 @@ module Ruflet
|
|
|
118
181
|
end
|
|
119
182
|
|
|
120
183
|
class MarkerControl < Ruflet::Control
|
|
184
|
+
include MapValueNormalizer
|
|
185
|
+
|
|
121
186
|
TYPE = "marker".freeze
|
|
122
187
|
WIRE = "Marker".freeze
|
|
123
188
|
|
|
124
189
|
def initialize(id: nil, coordinates: nil, content: nil, height: nil, rotate: nil, width: nil, alignment: nil)
|
|
125
190
|
props = {}
|
|
126
|
-
props[:coordinates] = coordinates unless coordinates.nil?
|
|
191
|
+
props[:coordinates] = normalize_map_coordinate(coordinates) unless coordinates.nil?
|
|
127
192
|
props[:content] = content unless content.nil?
|
|
128
193
|
props[:height] = height unless height.nil?
|
|
129
194
|
props[:rotate] = rotate unless rotate.nil?
|
|
@@ -146,12 +211,14 @@ module Ruflet
|
|
|
146
211
|
end
|
|
147
212
|
|
|
148
213
|
class CircleMarkerControl < Ruflet::Control
|
|
214
|
+
include MapValueNormalizer
|
|
215
|
+
|
|
149
216
|
TYPE = "circlemarker".freeze
|
|
150
217
|
WIRE = "CircleMarker".freeze
|
|
151
218
|
|
|
152
219
|
def initialize(id: nil, coordinates: nil, radius: nil, color: nil, border_color: nil, border_stroke_width: nil, use_radius_in_meter: nil)
|
|
153
220
|
props = {}
|
|
154
|
-
props[:coordinates] = coordinates unless coordinates.nil?
|
|
221
|
+
props[:coordinates] = normalize_map_coordinate(coordinates) unless coordinates.nil?
|
|
155
222
|
props[:radius] = radius unless radius.nil?
|
|
156
223
|
props[:color] = color unless color.nil?
|
|
157
224
|
props[:border_color] = border_color unless border_color.nil?
|
|
@@ -174,12 +241,14 @@ module Ruflet
|
|
|
174
241
|
end
|
|
175
242
|
|
|
176
243
|
class PolylineMarkerControl < Ruflet::Control
|
|
244
|
+
include MapValueNormalizer
|
|
245
|
+
|
|
177
246
|
TYPE = "polylinemarker".freeze
|
|
178
247
|
WIRE = "PolylineMarker".freeze
|
|
179
248
|
|
|
180
249
|
def initialize(id: nil, coordinates: nil, color: nil, stroke_width: nil, border_color: nil, border_stroke_width: nil, gradient_colors: nil, stroke_cap: nil, stroke_join: nil)
|
|
181
250
|
props = {}
|
|
182
|
-
props[:coordinates] = coordinates unless coordinates.nil?
|
|
251
|
+
props[:coordinates] = normalize_map_coordinate(coordinates) unless coordinates.nil?
|
|
183
252
|
props[:color] = color unless color.nil?
|
|
184
253
|
props[:stroke_width] = stroke_width unless stroke_width.nil?
|
|
185
254
|
props[:border_color] = border_color unless border_color.nil?
|
|
@@ -204,12 +273,14 @@ module Ruflet
|
|
|
204
273
|
end
|
|
205
274
|
|
|
206
275
|
class PolygonMarkerControl < Ruflet::Control
|
|
276
|
+
include MapValueNormalizer
|
|
277
|
+
|
|
207
278
|
TYPE = "polygonmarker".freeze
|
|
208
279
|
WIRE = "PolygonMarker".freeze
|
|
209
280
|
|
|
210
281
|
def initialize(id: nil, coordinates: nil, color: nil, border_color: nil, border_stroke_width: nil, disable_holes_border: nil, label: nil)
|
|
211
282
|
props = {}
|
|
212
|
-
props[:coordinates] = coordinates unless coordinates.nil?
|
|
283
|
+
props[:coordinates] = normalize_map_coordinate(coordinates) unless coordinates.nil?
|
|
213
284
|
props[:color] = color unless color.nil?
|
|
214
285
|
props[:border_color] = border_color unless border_color.nil?
|
|
215
286
|
props[:border_stroke_width] = border_stroke_width unless border_stroke_width.nil?
|
|
@@ -4,11 +4,33 @@ module Ruflet
|
|
|
4
4
|
module UI
|
|
5
5
|
module Controls
|
|
6
6
|
module RufletComponents
|
|
7
|
+
# WebView control — parity with Flet's WebView
|
|
8
|
+
# (https://flet.dev/docs/controls/webview/).
|
|
9
|
+
#
|
|
10
|
+
# Properties: url, bgcolor, prevent_links, plus the usual layout props.
|
|
11
|
+
# Events: on_page_started, on_page_ended, on_web_resource_error,
|
|
12
|
+
# on_progress, on_url_change, on_scroll, on_console_message,
|
|
13
|
+
# on_javascript_alert_dialog.
|
|
14
|
+
# Methods (invoked over the wire on a mounted control): reload, go_back,
|
|
15
|
+
# go_forward, can_go_back, can_go_forward, run_javascript, load_html,
|
|
16
|
+
# load_request, load_file, scroll_to, scroll_by, clear_cache,
|
|
17
|
+
# clear_local_storage, enable_zoom, disable_zoom, set_javascript_mode,
|
|
18
|
+
# get_current_url, get_title, get_user_agent.
|
|
19
|
+
#
|
|
20
|
+
# Platform note: the native webview (and therefore run_javascript and the
|
|
21
|
+
# events/methods) runs on iOS, Android and macOS. On web it falls back to
|
|
22
|
+
# an <iframe>, which cannot run the methods and which most external sites
|
|
23
|
+
# block via X-Frame-Options/CSP — embed your own same-origin pages there.
|
|
7
24
|
class WebViewControl < Ruflet::Control
|
|
8
25
|
TYPE = "WebView".freeze
|
|
9
26
|
WIRE = "WebView".freeze
|
|
10
27
|
|
|
11
|
-
def initialize(id: nil, bgcolor: nil, data: nil, enable_javascript: nil, expand: nil,
|
|
28
|
+
def initialize(id: nil, bgcolor: nil, data: nil, enable_javascript: nil, expand: nil,
|
|
29
|
+
height: nil, key: nil, method: nil, opacity: nil, prevent_links: nil,
|
|
30
|
+
rtl: nil, tooltip: nil, url: nil, visible: nil, width: nil,
|
|
31
|
+
on_page_ended: nil, on_page_started: nil, on_web_resource_error: nil,
|
|
32
|
+
on_progress: nil, on_url_change: nil, on_scroll: nil,
|
|
33
|
+
on_console_message: nil, on_javascript_alert_dialog: nil)
|
|
12
34
|
props = {}
|
|
13
35
|
props[:bgcolor] = bgcolor unless bgcolor.nil?
|
|
14
36
|
props[:data] = data unless data.nil?
|
|
@@ -18,6 +40,7 @@ module Ruflet
|
|
|
18
40
|
props[:key] = key unless key.nil?
|
|
19
41
|
props[:method] = method unless method.nil?
|
|
20
42
|
props[:opacity] = opacity unless opacity.nil?
|
|
43
|
+
props[:prevent_links] = prevent_links unless prevent_links.nil?
|
|
21
44
|
props[:rtl] = rtl unless rtl.nil?
|
|
22
45
|
props[:tooltip] = tooltip unless tooltip.nil?
|
|
23
46
|
props[:url] = url unless url.nil?
|
|
@@ -26,8 +49,98 @@ module Ruflet
|
|
|
26
49
|
props[:on_page_ended] = on_page_ended unless on_page_ended.nil?
|
|
27
50
|
props[:on_page_started] = on_page_started unless on_page_started.nil?
|
|
28
51
|
props[:on_web_resource_error] = on_web_resource_error unless on_web_resource_error.nil?
|
|
52
|
+
props[:on_progress] = on_progress unless on_progress.nil?
|
|
53
|
+
props[:on_url_change] = on_url_change unless on_url_change.nil?
|
|
54
|
+
props[:on_scroll] = on_scroll unless on_scroll.nil?
|
|
55
|
+
props[:on_console_message] = on_console_message unless on_console_message.nil?
|
|
56
|
+
props[:on_javascript_alert_dialog] = on_javascript_alert_dialog unless on_javascript_alert_dialog.nil?
|
|
29
57
|
super(type: TYPE, id: id, **props)
|
|
30
58
|
end
|
|
59
|
+
|
|
60
|
+
# --- Navigation --------------------------------------------------
|
|
61
|
+
|
|
62
|
+
def reload = invoke_webview_method("reload")
|
|
63
|
+
def go_back = invoke_webview_method("go_back")
|
|
64
|
+
def go_forward = invoke_webview_method("go_forward")
|
|
65
|
+
|
|
66
|
+
def can_go_back(timeout: 10, &on_result)
|
|
67
|
+
invoke_webview_method("can_go_back", timeout: timeout, on_result: on_result)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def can_go_forward(timeout: 10, &on_result)
|
|
71
|
+
invoke_webview_method("can_go_forward", timeout: timeout, on_result: on_result)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# --- Loading content ---------------------------------------------
|
|
75
|
+
|
|
76
|
+
def load_request(url, method: "get")
|
|
77
|
+
invoke_webview_method("load_request", { "url" => url.to_s, "method" => method.to_s })
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def load_html(value, base_url: nil)
|
|
81
|
+
args = { "value" => value.to_s }
|
|
82
|
+
args["base_url"] = base_url.to_s unless base_url.nil?
|
|
83
|
+
invoke_webview_method("load_html", args)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def load_file(path)
|
|
87
|
+
invoke_webview_method("load_file", { "path" => path.to_s })
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# --- JavaScript injection ---------------------------------------
|
|
91
|
+
|
|
92
|
+
# Run arbitrary JS inside the page — e.g. hide a node so a native
|
|
93
|
+
# control can take its place:
|
|
94
|
+
# webview.run_javascript("document.getElementById('banner').remove()")
|
|
95
|
+
def run_javascript(value)
|
|
96
|
+
invoke_webview_method("run_javascript", { "value" => value.to_s })
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def set_javascript_mode(mode)
|
|
100
|
+
invoke_webview_method("set_javascript_mode", { "mode" => mode.to_s })
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# --- Scrolling ---------------------------------------------------
|
|
104
|
+
|
|
105
|
+
def scroll_to(x, y)
|
|
106
|
+
invoke_webview_method("scroll_to", { "x" => x.to_i, "y" => y.to_i })
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def scroll_by(x, y)
|
|
110
|
+
invoke_webview_method("scroll_by", { "x" => x.to_i, "y" => y.to_i })
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# --- Storage / zoom ----------------------------------------------
|
|
114
|
+
|
|
115
|
+
def clear_cache = invoke_webview_method("clear_cache")
|
|
116
|
+
def clear_local_storage = invoke_webview_method("clear_local_storage")
|
|
117
|
+
def enable_zoom = invoke_webview_method("enable_zoom")
|
|
118
|
+
def disable_zoom = invoke_webview_method("disable_zoom")
|
|
119
|
+
|
|
120
|
+
# --- Introspection (result delivered to the block) ---------------
|
|
121
|
+
|
|
122
|
+
def get_current_url(timeout: 10, &on_result)
|
|
123
|
+
invoke_webview_method("get_current_url", timeout: timeout, on_result: on_result)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def get_title(timeout: 10, &on_result)
|
|
127
|
+
invoke_webview_method("get_title", timeout: timeout, on_result: on_result)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def get_user_agent(timeout: 10, &on_result)
|
|
131
|
+
invoke_webview_method("get_user_agent", timeout: timeout, on_result: on_result)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
private
|
|
135
|
+
|
|
136
|
+
def invoke_webview_method(name, args = nil, timeout: 10, on_result: nil)
|
|
137
|
+
page = runtime_page
|
|
138
|
+
unless page && wire_id
|
|
139
|
+
raise "WebView ##{id} is not mounted yet — add it to the page before calling #{name}."
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
page.invoke(self, name, args: args, timeout: timeout, on_result: on_result)
|
|
143
|
+
end
|
|
31
144
|
end
|
|
32
145
|
end
|
|
33
146
|
end
|
|
@@ -50,10 +50,28 @@ module Ruflet
|
|
|
50
50
|
super(type: TYPE, id: id, **props)
|
|
51
51
|
end
|
|
52
52
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
53
|
+
def wait_until_ready_to_show(timeout: 10, on_result: nil)
|
|
54
|
+
invoke_window_method("wait_until_ready_to_show", timeout: timeout, on_result: on_result)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def to_front(timeout: 10, on_result: nil)
|
|
58
|
+
invoke_window_method("to_front", timeout: timeout, on_result: on_result)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def center(timeout: 10, on_result: nil)
|
|
62
|
+
invoke_window_method("center", timeout: timeout, on_result: on_result)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def close(timeout: 10, on_result: nil)
|
|
66
|
+
invoke_window_method("close", timeout: timeout, on_result: on_result)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def destroy(timeout: 10, on_result: nil)
|
|
70
|
+
invoke_window_method("destroy", timeout: timeout, on_result: on_result)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def start_dragging(timeout: 10, on_result: nil)
|
|
74
|
+
invoke_window_method("start_dragging", timeout: timeout, on_result: on_result)
|
|
57
75
|
end
|
|
58
76
|
|
|
59
77
|
def start_resizing(edge, timeout: 10, on_result: nil)
|
|
@@ -65,6 +83,12 @@ module Ruflet
|
|
|
65
83
|
on_result: on_result
|
|
66
84
|
)
|
|
67
85
|
end
|
|
86
|
+
|
|
87
|
+
private
|
|
88
|
+
|
|
89
|
+
def invoke_window_method(method_name, timeout:, on_result:)
|
|
90
|
+
runtime_page&.invoke(self, method_name, timeout: timeout, on_result: on_result)
|
|
91
|
+
end
|
|
68
92
|
end
|
|
69
93
|
end
|
|
70
94
|
end
|
|
@@ -9,6 +9,7 @@ module Ruflet
|
|
|
9
9
|
WIRE = "AudioRecorder".freeze
|
|
10
10
|
|
|
11
11
|
def initialize(id: nil, configuration: nil, data: nil, key: nil, on_state_change: nil, on_stream: nil, on_upload: nil)
|
|
12
|
+
configuration ||= {}
|
|
12
13
|
props = {}
|
|
13
14
|
props[:configuration] = configuration unless configuration.nil?
|
|
14
15
|
props[:data] = data unless data.nil?
|
|
@@ -44,6 +45,7 @@ module Ruflet
|
|
|
44
45
|
end
|
|
45
46
|
|
|
46
47
|
def start_recording(output_path: nil, configuration: nil, upload: nil, timeout: 10, on_result: nil)
|
|
48
|
+
configuration ||= {}
|
|
47
49
|
invoke_audio_recorder(
|
|
48
50
|
"start_recording",
|
|
49
51
|
args: compact_args(
|
data/lib/ruflet_ui.rb
CHANGED
|
@@ -140,7 +140,7 @@ module Ruflet
|
|
|
140
140
|
private
|
|
141
141
|
|
|
142
142
|
def control_delegate
|
|
143
|
-
Ruflet::
|
|
143
|
+
Ruflet::WidgetBuilder.new
|
|
144
144
|
end
|
|
145
145
|
end
|
|
146
146
|
end
|
|
@@ -154,8 +154,12 @@ module Kernel
|
|
|
154
154
|
def app(**opts, &block) = Ruflet::DSL.app(**opts, &block)
|
|
155
155
|
def page(**props, &block) = Ruflet::DSL.page(**props, &block)
|
|
156
156
|
|
|
157
|
+
# Bare widget helpers (view, text, container, …) build real controls anywhere
|
|
158
|
+
# — top-level scripts and Ruflet.run blocks — with no builder to instantiate.
|
|
159
|
+
# The framework owns this plumbing so dev code stays free of it; a fresh
|
|
160
|
+
# builder per call keeps the helpers stateless.
|
|
157
161
|
def control_delegate
|
|
158
|
-
Ruflet::
|
|
162
|
+
Ruflet::WidgetBuilder.new
|
|
159
163
|
end
|
|
160
164
|
|
|
161
165
|
if Ruflet::UI::SharedControlForwarders.respond_to?(:instance_methods)
|