ruflet_core 0.0.19 → 0.0.20
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/README.md +4 -1
- data/lib/ruflet/version.rb +1 -1
- data/lib/ruflet_protocol/ruflet/protocol.rb +20 -2
- data/lib/ruflet_ui/ruflet/dsl.rb +1 -0
- data/lib/ruflet_ui/ruflet/event.rb +9 -0
- data/lib/ruflet_ui/ruflet/extensions/qrcode_scanner.rb +72 -0
- data/lib/ruflet_ui/ruflet/extensions.rb +101 -0
- data/lib/ruflet_ui/ruflet/page.rb +121 -5
- data/lib/ruflet_ui/ruflet/ui/control_factory.rb +19 -2
- data/lib/ruflet_ui/ruflet/ui/controls/ruflet_controls.rb +3 -3
- data/lib/ruflet_ui/ruflet/ui/controls/shared/{fletapp_control.rb → ruflet_app_control.rb} +4 -2
- data/lib/ruflet_ui/ruflet/ui/controls/shared/ruflet_controls.rb +3 -3
- data/lib/ruflet_ui/ruflet/ui/material_control_methods.rb +5 -0
- data/lib/ruflet_ui/ruflet/ui/material_control_registry.rb +1 -0
- data/lib/ruflet_ui/ruflet/ui/shared_control_forwarders.rb +1 -0
- data/lib/ruflet_ui.rb +17 -2
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7b5d3e1bde28f482994a21faa5d1412808941a9a5f6384df22ec4307d7ce4f35
|
|
4
|
+
data.tar.gz: d5d90041346619db3781eb5f67747ccf91d9657f4b62ba1edafd806f959bd5a0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3b7aa35ac477467d595e0d4e2ce7e246cd5ed309d20bca5a1ef658e4f67735fbc829042750722cf0a513cd300d8621c69977697712e580c5b90d3a482be8377b
|
|
7
|
+
data.tar.gz: 462a296c2022360be5ce1df9ddb97400fb6c6ec87b7a443681f7104f78c4060cb1350c889b41f3b9ad090ba2be27da6e140cfa7c40a5d4093382faf842303109
|
data/README.md
CHANGED
|
@@ -15,7 +15,10 @@ Ruflet.run do |page|
|
|
|
15
15
|
container(
|
|
16
16
|
bgcolor: :surface_container_high,
|
|
17
17
|
padding: 24,
|
|
18
|
-
content: text(
|
|
18
|
+
content: text(
|
|
19
|
+
value: "Hello Ruflet",
|
|
20
|
+
style: { color: "DeepOrange500", size: 20, weight: "w600" }
|
|
21
|
+
)
|
|
19
22
|
)
|
|
20
23
|
)
|
|
21
24
|
end
|
data/lib/ruflet/version.rb
CHANGED
|
@@ -78,10 +78,28 @@ module Ruflet
|
|
|
78
78
|
}
|
|
79
79
|
end
|
|
80
80
|
|
|
81
|
-
|
|
81
|
+
# The client's reply to `invoke_control_method`: the pending call id plus its
|
|
82
|
+
# result/error. Ruflet::ConnectionProtocol (ruflet_rails' Rack-hijack adapter
|
|
83
|
+
# and any other host adapter) normalizes this before handing it to
|
|
84
|
+
# Page#handle_invoke_method_result, which matches `call_id` back to the
|
|
85
|
+
# waiting callback. Without this method that path fell through to the DSL's
|
|
86
|
+
# method_missing and every invoke result was silently discarded.
|
|
87
|
+
def normalize_invoke_method_result_payload(payload)
|
|
88
|
+
payload = {} unless payload.is_a?(Hash)
|
|
89
|
+
{
|
|
90
|
+
"call_id" => payload["call_id"] || payload["callId"],
|
|
91
|
+
"result" => payload.key?("result") ? payload["result"] : payload["value"],
|
|
92
|
+
"error" => payload["error"]
|
|
93
|
+
}
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# page_patch defaults to nil rather than {} because mruby cannot parse a
|
|
97
|
+
# hash literal as a keyword argument default, and this file is compiled
|
|
98
|
+
# into the embedded VM.
|
|
99
|
+
def register_response(session_id:, page_patch: nil)
|
|
82
100
|
{
|
|
83
101
|
"session_id" => session_id,
|
|
84
|
-
"page_patch" => {},
|
|
102
|
+
"page_patch" => page_patch || {},
|
|
85
103
|
"error" => ""
|
|
86
104
|
}
|
|
87
105
|
end
|
data/lib/ruflet_ui/ruflet/dsl.rb
CHANGED
|
@@ -321,6 +321,7 @@ module Ruflet
|
|
|
321
321
|
def video(**props) = _pending_app.video(**props)
|
|
322
322
|
def code_editor(value = nil, **props) = _pending_app.code_editor(value, **props)
|
|
323
323
|
def codeeditor(value = nil, **props) = _pending_app.codeeditor(value, **props)
|
|
324
|
+
def lottie(src = nil, **props) = _pending_app.lottie(src, **props)
|
|
324
325
|
def rive(src = nil, **props) = _pending_app.rive(src, **props)
|
|
325
326
|
def fab(content = nil, **props) = _pending_app.fab(content, **props)
|
|
326
327
|
def cupertino_button(content = nil, **props) = _pending_app.cupertino_button(content, **props)
|
|
@@ -17,6 +17,15 @@ module Ruflet
|
|
|
17
17
|
@control = control
|
|
18
18
|
end
|
|
19
19
|
|
|
20
|
+
# A stable convenience accessor shared by built-in and extension events.
|
|
21
|
+
# Extension event names are intentionally not part of the core typed-event
|
|
22
|
+
# registry, so fall back to the same value extraction used by GenericEvent.
|
|
23
|
+
def value
|
|
24
|
+
return typed_data.value if typed_data&.respond_to?(:value)
|
|
25
|
+
|
|
26
|
+
Events::GenericEvent.from_data(data).value
|
|
27
|
+
end
|
|
28
|
+
|
|
20
29
|
private
|
|
21
30
|
|
|
22
31
|
def parse_data(raw)
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ruflet
|
|
4
|
+
module Extensions
|
|
5
|
+
class QrCodeScannerControl < Ruflet::Control
|
|
6
|
+
TYPE = "qrcode_scanner".freeze
|
|
7
|
+
WIRE = "qrcode_scanner".freeze
|
|
8
|
+
KEYWORDS = %i[
|
|
9
|
+
align animate_align animate_margin animate_offset animate_opacity
|
|
10
|
+
animate_position animate_rotation animate_scale animate_size aspect_ratio
|
|
11
|
+
auto_start auto_zoom badge bottom camera_facing col data detection_speed
|
|
12
|
+
detection_timeout disabled expand expand_loose fit formats height
|
|
13
|
+
invert_image key left margin offset opacity return_image right rotate rtl
|
|
14
|
+
scale scan_window size_change_interval tap_to_focus tooltip top
|
|
15
|
+
torch_enabled visible width zoom_scale on_animation_end on_detect on_error
|
|
16
|
+
on_size_change
|
|
17
|
+
].freeze
|
|
18
|
+
|
|
19
|
+
def initialize(id: nil, **props)
|
|
20
|
+
super(type: TYPE, id: id, **props)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def start(timeout: 10, on_result: nil)
|
|
24
|
+
invoke_scanner("start", timeout: timeout, on_result: on_result)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def stop(timeout: 10, on_result: nil)
|
|
28
|
+
invoke_scanner("stop", timeout: timeout, on_result: on_result)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def switch_camera(timeout: 10, on_result: nil)
|
|
32
|
+
invoke_scanner("switch_camera", timeout: timeout, on_result: on_result)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def toggle_torch(timeout: 10, on_result: nil)
|
|
36
|
+
invoke_scanner("toggle_torch", timeout: timeout, on_result: on_result)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def set_zoom_scale(value, timeout: 10, on_result: nil)
|
|
40
|
+
invoke_scanner("set_zoom_scale", args: { "value" => value }, timeout: timeout, on_result: on_result)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def reset_zoom_scale(timeout: 10, on_result: nil)
|
|
44
|
+
invoke_scanner("reset_zoom_scale", timeout: timeout, on_result: on_result)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def preprocess_props(props)
|
|
50
|
+
mapped = props.dup
|
|
51
|
+
mapped[:formats] = Array(mapped[:formats]).map(&:to_s) if mapped.key?(:formats)
|
|
52
|
+
mapped["formats"] = Array(mapped["formats"]).map(&:to_s) if mapped.key?("formats")
|
|
53
|
+
mapped
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def invoke_scanner(name, args: nil, timeout: 10, on_result: nil)
|
|
57
|
+
runtime_page&.invoke(self, name, args: args, timeout: timeout, on_result: on_result)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
register_control(
|
|
62
|
+
:qrcode_scanner,
|
|
63
|
+
control_class: QrCodeScannerControl,
|
|
64
|
+
flutter: {
|
|
65
|
+
package: "ruflet_qrcode_scanner",
|
|
66
|
+
import: "package:ruflet_qrcode_scanner/ruflet_qrcode_scanner.dart",
|
|
67
|
+
alias: "ruflet_qrcode_scanner",
|
|
68
|
+
constructor: "Extension"
|
|
69
|
+
}
|
|
70
|
+
)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ruflet
|
|
4
|
+
# Registry used by Ruflet extension gems to add typed Ruby controls and
|
|
5
|
+
# describe the matching Flet Flutter package. Registering a control makes its
|
|
6
|
+
# helpers available in the bare DSL, Ruflet::DSL, Ruflet, Ruflet::UI, and
|
|
7
|
+
# WidgetBuilder without editing Ruflet's generated control method tables.
|
|
8
|
+
module Extensions
|
|
9
|
+
Registration = Struct.new(
|
|
10
|
+
:name,
|
|
11
|
+
:control_class,
|
|
12
|
+
:helpers,
|
|
13
|
+
:flutter,
|
|
14
|
+
keyword_init: true
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
@registrations = {}
|
|
18
|
+
|
|
19
|
+
class << self
|
|
20
|
+
def register_control(name, control_class:, helpers: nil, flutter: nil)
|
|
21
|
+
key = normalize_name(name)
|
|
22
|
+
helper_names = Array(helpers || key).map { |helper| normalize_name(helper) }.uniq.freeze
|
|
23
|
+
registration = Registration.new(
|
|
24
|
+
name: key,
|
|
25
|
+
control_class: control_class,
|
|
26
|
+
helpers: helper_names,
|
|
27
|
+
flutter: normalize_flutter_metadata(flutter)
|
|
28
|
+
).freeze
|
|
29
|
+
|
|
30
|
+
existing = @registrations[key]
|
|
31
|
+
if existing && existing != registration
|
|
32
|
+
raise ArgumentError, "Ruflet extension `#{key}` is already registered"
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
helper_names.each do |helper|
|
|
36
|
+
UI::ControlFactory.register_extension(helper, control_class)
|
|
37
|
+
define_control_helper(helper)
|
|
38
|
+
end
|
|
39
|
+
@registrations[key] = registration
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def [](name)
|
|
43
|
+
@registrations[normalize_name(name)]
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def each(&block)
|
|
47
|
+
return enum_for(:each) unless block
|
|
48
|
+
|
|
49
|
+
@registrations.values.each(&block)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def registered?(name)
|
|
53
|
+
@registrations.key?(normalize_name(name))
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
def normalize_name(value)
|
|
59
|
+
name = value.to_s.strip.downcase.tr("-", "_")
|
|
60
|
+
raise ArgumentError, "Extension name cannot be empty" if name.empty?
|
|
61
|
+
raise ArgumentError, "Invalid extension name `#{value}`" unless name.match?(/\A[a-z][a-z0-9_]*\z/)
|
|
62
|
+
|
|
63
|
+
name
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def normalize_flutter_metadata(metadata)
|
|
67
|
+
return nil if metadata.nil?
|
|
68
|
+
|
|
69
|
+
values = metadata.transform_keys(&:to_sym)
|
|
70
|
+
package = values.fetch(:package).to_s
|
|
71
|
+
import = values.fetch(:import, "package:#{package}/#{package}.dart").to_s
|
|
72
|
+
alias_name = values.fetch(:alias, package).to_s
|
|
73
|
+
constructor = values.fetch(:constructor, "Extension").to_s
|
|
74
|
+
{
|
|
75
|
+
package: package,
|
|
76
|
+
import: import,
|
|
77
|
+
alias: alias_name,
|
|
78
|
+
constructor: constructor
|
|
79
|
+
}.freeze
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def define_control_helper(helper)
|
|
83
|
+
type = helper
|
|
84
|
+
|
|
85
|
+
UI::ControlMethods.send(:define_method, helper) do |**props, &block|
|
|
86
|
+
control(type, **props, &block)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
UI::SharedControlForwarders.send(:define_method, helper) do |**props, &block|
|
|
90
|
+
control_delegate.public_send(type, **props, &block)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
DSL.define_singleton_method(helper) do |**props, &block|
|
|
94
|
+
_pending_app.public_send(type, **props, &block)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
Kernel.send(:private, helper) if Kernel.method_defined?(helper)
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
@@ -300,6 +300,90 @@ module Ruflet
|
|
|
300
300
|
self
|
|
301
301
|
end
|
|
302
302
|
|
|
303
|
+
# Complete page state as a property map for the register_client response
|
|
304
|
+
# (Flet's page_patch). The client applies this through Control.update,
|
|
305
|
+
# which merges by control id and keeps existing instances alive — the only
|
|
306
|
+
# patch path that survives a re-register on a client with prior state
|
|
307
|
+
# (reconnect after a backend restart). The op-based view patch replaces
|
|
308
|
+
# control instances and detaches containers on such clients.
|
|
309
|
+
def register_page_patch
|
|
310
|
+
refresh_control_indexes!
|
|
311
|
+
patch = { "views" => build_view_patches }
|
|
312
|
+
@page_props.each { |key, value| patch[key] = serialize_patch_value(value) }
|
|
313
|
+
@overlay_container_mounted = true if @overlay_container.wire_id
|
|
314
|
+
@dialogs_container_mounted = true if @dialogs_container.wire_id
|
|
315
|
+
@services_container_mounted = true if @services_container.wire_id
|
|
316
|
+
patch
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
# Page props that must survive a hot reload: the route (kept on purpose),
|
|
320
|
+
# the client-reported window, and the internal overlay/services/dialogs
|
|
321
|
+
# containers (kept mounted so the client keeps their bindings).
|
|
322
|
+
RELOAD_PRESERVED_PAGE_PROPS = %w[route window _overlay _dialogs _services].freeze
|
|
323
|
+
|
|
324
|
+
# Quietly clears session content and page chrome so a reloaded app block
|
|
325
|
+
# can re-render on this same Page without recreating it. The page object
|
|
326
|
+
# must survive a hot reload: a new Page re-sends the internal
|
|
327
|
+
# overlay/service/dialogs containers, which replaces their client-side
|
|
328
|
+
# instances and detaches them (see build_page_patch_ops) — after that,
|
|
329
|
+
# dialog and service patches are silently ignored by the client.
|
|
330
|
+
#
|
|
331
|
+
# Chrome (appbar, drawer, FAB, bgcolor, title, ...) lives in @view_props
|
|
332
|
+
# and @page_props. If the reloaded block no longer sets a piece of chrome,
|
|
333
|
+
# the previous run's value would linger, so both are cleared here. The
|
|
334
|
+
# view is fully rebuilt on the next patch, which drops any @view_props not
|
|
335
|
+
# re-set; page-level props merge on the client, so finalize_reload! sends
|
|
336
|
+
# explicit nils for the ones that disappeared. Sends nothing itself.
|
|
337
|
+
def reset_for_reload!
|
|
338
|
+
@root_controls = []
|
|
339
|
+
@views = []
|
|
340
|
+
@dialogs = []
|
|
341
|
+
@dialog = nil
|
|
342
|
+
@snack_bar = nil
|
|
343
|
+
@bottom_sheet = nil
|
|
344
|
+
@route_change_seen_since_reset = false
|
|
345
|
+
|
|
346
|
+
@reload_prior_page_prop_keys = @page_props.keys - RELOAD_PRESERVED_PAGE_PROPS
|
|
347
|
+
@view_props = {}
|
|
348
|
+
@page_props = @page_props.select { |key, _| RELOAD_PRESERVED_PAGE_PROPS.include?(key) }
|
|
349
|
+
@page_props["route"] ||= (@client_details["route"] || "/")
|
|
350
|
+
@page_props["window"] ||= @window
|
|
351
|
+
@overlay_container.children.clear
|
|
352
|
+
refresh_overlay_container!
|
|
353
|
+
refresh_services_container!
|
|
354
|
+
refresh_dialogs_container!
|
|
355
|
+
self
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
# Called after the reloaded app block ran, before the reload view patch.
|
|
359
|
+
# Page-level props merge on the client (the Page control is not recreated),
|
|
360
|
+
# so chrome the reloaded block dropped must be sent as an explicit nil or
|
|
361
|
+
# it lingers. View props need no such treatment: the View is rebuilt
|
|
362
|
+
# wholesale from @view_props. Also flushes the overlay container, which
|
|
363
|
+
# stays mounted and is skipped by a plain view patch.
|
|
364
|
+
def finalize_reload!
|
|
365
|
+
keys = @reload_prior_page_prop_keys
|
|
366
|
+
if keys
|
|
367
|
+
(keys - @page_props.keys).each { |key| @page_props[key] = nil }
|
|
368
|
+
@reload_prior_page_prop_keys = nil
|
|
369
|
+
end
|
|
370
|
+
push_overlay_update!
|
|
371
|
+
self
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
# Called after the reloaded app block ran. The route survives a reload
|
|
375
|
+
# (page props are kept), but the client only sends its route_change event
|
|
376
|
+
# at connect time — apps that render routes exclusively inside
|
|
377
|
+
# on_route_change would be stuck on stale content after a reload. Replay
|
|
378
|
+
# the event for them, unless the block already routed itself (page.go).
|
|
379
|
+
def replay_route_after_reload!
|
|
380
|
+
return self if @route_change_seen_since_reset
|
|
381
|
+
return self unless @page_event_handlers["route_change"].respond_to?(:call)
|
|
382
|
+
|
|
383
|
+
dispatch_page_event(name: "route_change", data: @page_props["route"])
|
|
384
|
+
self
|
|
385
|
+
end
|
|
386
|
+
|
|
303
387
|
def overlay
|
|
304
388
|
@overlay_container.children
|
|
305
389
|
end
|
|
@@ -643,9 +727,22 @@ module Ruflet
|
|
|
643
727
|
end
|
|
644
728
|
|
|
645
729
|
def show_bottom_sheet(bottom_sheet_control)
|
|
730
|
+
@bottom_sheet = bottom_sheet_control
|
|
646
731
|
show_dialog(bottom_sheet_control)
|
|
647
732
|
end
|
|
648
733
|
|
|
734
|
+
def show_bottomsheet(bottom_sheet_control)
|
|
735
|
+
show_bottom_sheet(bottom_sheet_control)
|
|
736
|
+
end
|
|
737
|
+
|
|
738
|
+
def close_bottom_sheet(bottom_sheet_control = nil)
|
|
739
|
+
close_dialog(bottom_sheet_control || @bottom_sheet)
|
|
740
|
+
end
|
|
741
|
+
|
|
742
|
+
def close_bottomsheet(bottom_sheet_control = nil)
|
|
743
|
+
close_bottom_sheet(bottom_sheet_control)
|
|
744
|
+
end
|
|
745
|
+
|
|
649
746
|
def show_banner(banner_control)
|
|
650
747
|
show_dialog(banner_control)
|
|
651
748
|
end
|
|
@@ -654,16 +751,27 @@ module Ruflet
|
|
|
654
751
|
close_dialog(banner_control)
|
|
655
752
|
end
|
|
656
753
|
|
|
754
|
+
# Flet-compatible generic overlay API. Prefer close_bottom_sheet for sheets
|
|
755
|
+
# so the page's current bottom-sheet slot is handled explicitly.
|
|
756
|
+
def open(dialog_control)
|
|
757
|
+
show_dialog(dialog_control)
|
|
758
|
+
end
|
|
759
|
+
|
|
760
|
+
def close(dialog_control = nil)
|
|
761
|
+
close_dialog(dialog_control)
|
|
762
|
+
end
|
|
763
|
+
|
|
657
764
|
def close_dialog(dialog_control = nil)
|
|
658
765
|
target = dialog_control || latest_open_dialog
|
|
659
766
|
return nil unless target
|
|
660
767
|
|
|
768
|
+
clear_dialog_slots(target)
|
|
769
|
+
# The client pops a dialog's route only when the mounted control sees
|
|
770
|
+
# its `open` prop flip to false (alert_dialog.dart tracks open/_open).
|
|
771
|
+
# Replacing the dialogs container recreates the control client-side and
|
|
772
|
+
# the transition is lost, so patch the open prop on the control itself.
|
|
661
773
|
target.props["open"] = false
|
|
662
|
-
|
|
663
|
-
@snack_bar = nil if @snack_bar.equal?(target)
|
|
664
|
-
@bottom_sheet = nil if @bottom_sheet.equal?(target)
|
|
665
|
-
refresh_dialogs_container!
|
|
666
|
-
push_dialogs_update!
|
|
774
|
+
update(target, open: false)
|
|
667
775
|
target
|
|
668
776
|
end
|
|
669
777
|
|
|
@@ -1585,6 +1693,7 @@ module Ruflet
|
|
|
1585
1693
|
def dispatch_page_event(name:, data:)
|
|
1586
1694
|
event_name = name.to_s
|
|
1587
1695
|
event_name = event_name[3..-1] if event_name.start_with?("on_")
|
|
1696
|
+
@route_change_seen_since_reset = true if event_name == "route_change"
|
|
1588
1697
|
handler = @page_event_handlers[event_name]
|
|
1589
1698
|
return unless handler.respond_to?(:call)
|
|
1590
1699
|
|
|
@@ -1720,10 +1829,17 @@ module Ruflet
|
|
|
1720
1829
|
return false unless @dialogs.include?(control)
|
|
1721
1830
|
|
|
1722
1831
|
@dialogs.delete(control)
|
|
1832
|
+
clear_dialog_slots(control)
|
|
1723
1833
|
refresh_dialogs_container!
|
|
1724
1834
|
true
|
|
1725
1835
|
end
|
|
1726
1836
|
|
|
1837
|
+
def clear_dialog_slots(control)
|
|
1838
|
+
@dialog = nil if @dialog.equal?(control)
|
|
1839
|
+
@snack_bar = nil if @snack_bar.equal?(control)
|
|
1840
|
+
@bottom_sheet = nil if @bottom_sheet.equal?(control)
|
|
1841
|
+
end
|
|
1842
|
+
|
|
1727
1843
|
def assign_split_prop(key, value)
|
|
1728
1844
|
if key == "vertical_alignment" || key == "horizontal_alignment"
|
|
1729
1845
|
@page_props[key] = value
|
|
@@ -13,12 +13,14 @@ module Ruflet
|
|
|
13
13
|
Services::RufletServices::CLASS_MAP
|
|
14
14
|
.merge(Controls::RufletControls::CLASS_MAP)
|
|
15
15
|
.freeze
|
|
16
|
+
EXTENSION_CLASS_MAP = {}
|
|
16
17
|
CONSTRUCTOR_KEYWORDS_CACHE = {}
|
|
17
18
|
|
|
18
19
|
COMMON_ATTRIBUTES = %i[flip ref transform].freeze
|
|
19
20
|
ATTRIBUTE_OVERRIDES = {
|
|
20
21
|
"expansionpanellist" => %i[auto_scroll on_scroll scroll scroll_interval],
|
|
21
|
-
"
|
|
22
|
+
"ruflet_app" => %i[assets_dir on_python_output],
|
|
23
|
+
"rufletapp" => %i[assets_dir on_python_output],
|
|
22
24
|
"image" => %i[
|
|
23
25
|
align animate_align animate_margin animate_offset animate_opacity animate_position
|
|
24
26
|
animate_rotation animate_scale animate_size aspect_ratio badge bottom col disabled
|
|
@@ -40,7 +42,7 @@ module Ruflet
|
|
|
40
42
|
if ENV["RUFLET_DEBUG"] == "1" && normalized_type == "floatingactionbutton"
|
|
41
43
|
Kernel.warn("[factory] type=#{normalized_type} id=#{id.inspect} props=#{props.inspect}")
|
|
42
44
|
end
|
|
43
|
-
klass = CLASS_MAP[normalized_type]
|
|
45
|
+
klass = EXTENSION_CLASS_MAP[normalized_type] || CLASS_MAP[normalized_type]
|
|
44
46
|
if klass
|
|
45
47
|
normalized_props, supplemental_props = normalize_constructor_props(klass, normalized_type, props)
|
|
46
48
|
if ENV["RUFLET_DEBUG"] == "1" && normalized_type == "floatingactionbutton"
|
|
@@ -96,6 +98,21 @@ module Ruflet
|
|
|
96
98
|
[]
|
|
97
99
|
end
|
|
98
100
|
|
|
101
|
+
def register_extension(type, klass)
|
|
102
|
+
key = type.to_s.downcase
|
|
103
|
+
raise ArgumentError, "Extension control type cannot be empty" if key.empty?
|
|
104
|
+
raise ArgumentError, "Extension control must inherit Ruflet::Control" unless klass <= Ruflet::Control
|
|
105
|
+
|
|
106
|
+
existing = EXTENSION_CLASS_MAP[key]
|
|
107
|
+
if existing && existing != klass
|
|
108
|
+
raise ArgumentError, "Extension control `#{key}` is already registered by #{existing}"
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
EXTENSION_CLASS_MAP[key] = klass
|
|
112
|
+
CONSTRUCTOR_KEYWORDS_CACHE.delete(klass)
|
|
113
|
+
klass
|
|
114
|
+
end
|
|
115
|
+
|
|
99
116
|
def normalize_common_value(value)
|
|
100
117
|
case value
|
|
101
118
|
when Hash
|
|
@@ -115,7 +115,6 @@ require_relative "shared/dismissible_control"
|
|
|
115
115
|
require_relative "shared/draggable_control"
|
|
116
116
|
require_relative "shared/dragtarget_control"
|
|
117
117
|
require_relative "shared/fill_control"
|
|
118
|
-
require_relative "shared/fletapp_control"
|
|
119
118
|
require_relative "shared/gesturedetector_control"
|
|
120
119
|
require_relative "shared/gridview_control"
|
|
121
120
|
require_relative "shared/hero_control"
|
|
@@ -140,6 +139,7 @@ require_relative "shared/reorderabledraghandle_control"
|
|
|
140
139
|
require_relative "shared/responsiverow_control"
|
|
141
140
|
require_relative "shared/row_control"
|
|
142
141
|
require_relative "shared/rotatedbox_control"
|
|
142
|
+
require_relative "shared/ruflet_app_control"
|
|
143
143
|
require_relative "shared/safearea_control"
|
|
144
144
|
require_relative "shared/screenshot_control"
|
|
145
145
|
require_relative "shared/semantics_control"
|
|
@@ -301,8 +301,6 @@ module Ruflet
|
|
|
301
301
|
"fillediconbutton" => RufletComponents::FilledIconButtonControl,
|
|
302
302
|
"filledtonalbutton" => RufletComponents::FilledTonalButtonControl,
|
|
303
303
|
"filledtonaliconbutton" => RufletComponents::FilledTonalIconButtonControl,
|
|
304
|
-
"flet_app" => RufletComponents::FletAppControl,
|
|
305
|
-
"fletapp" => RufletComponents::FletAppControl,
|
|
306
304
|
"floating_action_button" => RufletComponents::FloatingActionButtonControl,
|
|
307
305
|
"floatingactionbutton" => RufletComponents::FloatingActionButtonControl,
|
|
308
306
|
"gesture_detector" => RufletComponents::GestureDetectorControl,
|
|
@@ -399,6 +397,8 @@ module Ruflet
|
|
|
399
397
|
"row" => RufletComponents::RowControl,
|
|
400
398
|
"rotated_box" => RufletComponents::RotatedBoxControl,
|
|
401
399
|
"rotatedbox" => RufletComponents::RotatedBoxControl,
|
|
400
|
+
"ruflet_app" => RufletComponents::RufletAppControl,
|
|
401
|
+
"rufletapp" => RufletComponents::RufletAppControl,
|
|
402
402
|
"safe_area" => RufletComponents::SafeAreaControl,
|
|
403
403
|
"safearea" => RufletComponents::SafeAreaControl,
|
|
404
404
|
"screenshot" => RufletComponents::ScreenshotControl,
|
|
@@ -4,8 +4,10 @@ module Ruflet
|
|
|
4
4
|
module UI
|
|
5
5
|
module Controls
|
|
6
6
|
module RufletComponents
|
|
7
|
-
class
|
|
8
|
-
TYPE = "
|
|
7
|
+
class RufletAppControl < Ruflet::Control
|
|
8
|
+
TYPE = "ruflet_app".freeze
|
|
9
|
+
# The client widget is still registered under its upstream name, so the
|
|
10
|
+
# wire type stays FletApp while the Ruby API is ruflet_app.
|
|
9
11
|
WIRE = "FletApp".freeze
|
|
10
12
|
|
|
11
13
|
KEYWORDS = [:align, :animate_align, :animate_margin, :animate_offset, :animate_opacity, :animate_position, :animate_rotation, :animate_scale, :animate_size, :app_error_message, :app_startup_screen_message, :args, :aspect_ratio, :badge, :bottom, :col, :data, :disabled, :expand, :expand_loose, :force_pyodide, :height, :key, :left, :margin, :offset, :opacity, :reconnect_interval_ms, :reconnect_timeout_ms, :right, :rotate, :rtl, :scale, :show_app_startup_screen, :size_change_interval, :tooltip, :top, :url, :visible, :width, :on_animation_end, :on_error, :on_size_change].freeze
|
|
@@ -14,7 +14,6 @@ require_relative "dismissible_control"
|
|
|
14
14
|
require_relative "draggable_control"
|
|
15
15
|
require_relative "dragtarget_control"
|
|
16
16
|
require_relative "fill_control"
|
|
17
|
-
require_relative "fletapp_control"
|
|
18
17
|
require_relative "gesturedetector_control"
|
|
19
18
|
require_relative "gridview_control"
|
|
20
19
|
require_relative "hero_control"
|
|
@@ -39,6 +38,7 @@ require_relative "reorderabledraghandle_control"
|
|
|
39
38
|
require_relative "responsiverow_control"
|
|
40
39
|
require_relative "row_control"
|
|
41
40
|
require_relative "rotatedbox_control"
|
|
41
|
+
require_relative "ruflet_app_control"
|
|
42
42
|
require_relative "safearea_control"
|
|
43
43
|
require_relative "screenshot_control"
|
|
44
44
|
require_relative "semantics_control"
|
|
@@ -81,8 +81,6 @@ module Ruflet
|
|
|
81
81
|
"draggable" => RufletComponents::DraggableControl,
|
|
82
82
|
"dragtarget" => RufletComponents::DragTargetControl,
|
|
83
83
|
"fill" => RufletComponents::FillControl,
|
|
84
|
-
"flet_app" => RufletComponents::FletAppControl,
|
|
85
|
-
"fletapp" => RufletComponents::FletAppControl,
|
|
86
84
|
"gesture_detector" => RufletComponents::GestureDetectorControl,
|
|
87
85
|
"gesturedetector" => RufletComponents::GestureDetectorControl,
|
|
88
86
|
"grid_view" => RufletComponents::GridViewControl,
|
|
@@ -117,6 +115,8 @@ module Ruflet
|
|
|
117
115
|
"row" => RufletComponents::RowControl,
|
|
118
116
|
"rotated_box" => RufletComponents::RotatedBoxControl,
|
|
119
117
|
"rotatedbox" => RufletComponents::RotatedBoxControl,
|
|
118
|
+
"ruflet_app" => RufletComponents::RufletAppControl,
|
|
119
|
+
"rufletapp" => RufletComponents::RufletAppControl,
|
|
120
120
|
"safe_area" => RufletComponents::SafeAreaControl,
|
|
121
121
|
"safearea" => RufletComponents::SafeAreaControl,
|
|
122
122
|
"screenshot" => RufletComponents::ScreenshotControl,
|
|
@@ -702,6 +702,11 @@ module Ruflet
|
|
|
702
702
|
build_widget(:codeeditor, **mapped)
|
|
703
703
|
end
|
|
704
704
|
def codeeditor(value = nil, **props) = code_editor(value, **props)
|
|
705
|
+
def lottie(src = nil, **props)
|
|
706
|
+
mapped = props.dup
|
|
707
|
+
mapped[:src] = src unless src.nil?
|
|
708
|
+
build_widget(:lottie, **mapped)
|
|
709
|
+
end
|
|
705
710
|
def rive(src = nil, **props)
|
|
706
711
|
mapped = props.dup
|
|
707
712
|
mapped[:src] = src unless src.nil?
|
|
@@ -282,6 +282,7 @@ module Ruflet
|
|
|
282
282
|
def video(**props) = control_delegate.video(**props)
|
|
283
283
|
def code_editor(value = nil, **props) = control_delegate.code_editor(value, **props)
|
|
284
284
|
def codeeditor(value = nil, **props) = control_delegate.codeeditor(value, **props)
|
|
285
|
+
def lottie(src = nil, **props) = control_delegate.lottie(src, **props)
|
|
285
286
|
def rive(src = nil, **props) = control_delegate.rive(src, **props)
|
|
286
287
|
def cupertino_button(content = nil, **props) = control_delegate.cupertino_button(content, **props)
|
|
287
288
|
def cupertinobutton(content = nil, **props) = control_delegate.cupertinobutton(content, **props)
|
data/lib/ruflet_ui.rb
CHANGED
|
@@ -3,8 +3,6 @@
|
|
|
3
3
|
require "ruflet_protocol"
|
|
4
4
|
require_relative "ruflet_ui/ruflet/colors"
|
|
5
5
|
require_relative "ruflet_ui/ruflet/icon_data"
|
|
6
|
-
require_relative "ruflet_ui/ruflet/icons/material/material_icons"
|
|
7
|
-
require_relative "ruflet_ui/ruflet/icons/cupertino/cupertino_icons"
|
|
8
6
|
require_relative "ruflet_ui/ruflet/types/text_style"
|
|
9
7
|
require_relative "ruflet_ui/ruflet/types/geometry"
|
|
10
8
|
require_relative "ruflet_ui/ruflet/control"
|
|
@@ -15,8 +13,25 @@ require_relative "ruflet_ui/ruflet/event"
|
|
|
15
13
|
require_relative "ruflet_ui/ruflet/page"
|
|
16
14
|
require_relative "ruflet_ui/ruflet/app"
|
|
17
15
|
require_relative "ruflet_ui/ruflet/dsl"
|
|
16
|
+
require_relative "ruflet_ui/ruflet/extensions"
|
|
17
|
+
require_relative "ruflet_ui/ruflet/extensions/qrcode_scanner"
|
|
18
18
|
|
|
19
19
|
module Ruflet
|
|
20
|
+
# Icon tables are large (the material set alone parses a ~234KB map and
|
|
21
|
+
# defines thousands of constants) but the common `icon("home")` path passes
|
|
22
|
+
# the name straight through — nothing in the runtime touches these modules
|
|
23
|
+
# unless an app references a constant like `Ruflet::Icons::HOME`. Autoload
|
|
24
|
+
# them so boot (and every full restart) skips that cost until first use.
|
|
25
|
+
#
|
|
26
|
+
# CRuby only: under the embedded mruby VM the framework is concatenated into
|
|
27
|
+
# one blob with no filesystem require, so these modules are already defined
|
|
28
|
+
# there and the guard keeps the autoload calls out of that path entirely.
|
|
29
|
+
if RUBY_ENGINE != "mruby"
|
|
30
|
+
icons_root = File.expand_path("ruflet_ui/ruflet/icons", __dir__)
|
|
31
|
+
autoload :MaterialIcons, File.join(icons_root, "material/material_icons")
|
|
32
|
+
autoload :CupertinoIcons, File.join(icons_root, "cupertino/cupertino_icons")
|
|
33
|
+
end
|
|
34
|
+
|
|
20
35
|
TextStyle = UI::Types::TextStyle
|
|
21
36
|
StrutStyle = UI::Types::StrutStyle
|
|
22
37
|
TextOverflow = UI::Types::TextOverflow
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: ruflet_core
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.0.
|
|
4
|
+
version: 0.0.20
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- AdamMusa
|
|
@@ -29,6 +29,8 @@ files:
|
|
|
29
29
|
- lib/ruflet_ui/ruflet/dsl.rb
|
|
30
30
|
- lib/ruflet_ui/ruflet/event.rb
|
|
31
31
|
- lib/ruflet_ui/ruflet/events/gesture_events.rb
|
|
32
|
+
- lib/ruflet_ui/ruflet/extensions.rb
|
|
33
|
+
- lib/ruflet_ui/ruflet/extensions/qrcode_scanner.rb
|
|
32
34
|
- lib/ruflet_ui/ruflet/icon_data.rb
|
|
33
35
|
- lib/ruflet_ui/ruflet/icons/cupertino/cupertino_icons.json
|
|
34
36
|
- lib/ruflet_ui/ruflet/icons/cupertino/cupertino_icons.rb
|
|
@@ -161,7 +163,6 @@ files:
|
|
|
161
163
|
- lib/ruflet_ui/ruflet/ui/controls/shared/draggable_control.rb
|
|
162
164
|
- lib/ruflet_ui/ruflet/ui/controls/shared/dragtarget_control.rb
|
|
163
165
|
- lib/ruflet_ui/ruflet/ui/controls/shared/fill_control.rb
|
|
164
|
-
- lib/ruflet_ui/ruflet/ui/controls/shared/fletapp_control.rb
|
|
165
166
|
- lib/ruflet_ui/ruflet/ui/controls/shared/gesturedetector_control.rb
|
|
166
167
|
- lib/ruflet_ui/ruflet/ui/controls/shared/gridview_control.rb
|
|
167
168
|
- lib/ruflet_ui/ruflet/ui/controls/shared/hero_control.rb
|
|
@@ -186,6 +187,7 @@ files:
|
|
|
186
187
|
- lib/ruflet_ui/ruflet/ui/controls/shared/responsiverow_control.rb
|
|
187
188
|
- lib/ruflet_ui/ruflet/ui/controls/shared/rotatedbox_control.rb
|
|
188
189
|
- lib/ruflet_ui/ruflet/ui/controls/shared/row_control.rb
|
|
190
|
+
- lib/ruflet_ui/ruflet/ui/controls/shared/ruflet_app_control.rb
|
|
189
191
|
- lib/ruflet_ui/ruflet/ui/controls/shared/ruflet_controls.rb
|
|
190
192
|
- lib/ruflet_ui/ruflet/ui/controls/shared/safearea_control.rb
|
|
191
193
|
- lib/ruflet_ui/ruflet/ui/controls/shared/screenshot_control.rb
|