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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 412cca0911d7e0d196902336dff3da369a776e146713f511b4571fe3e693f99e
4
- data.tar.gz: 9f07083cd60681b95aec47ec0a269c9c0ef42365ee23f8f627f2ea6795e82a66
3
+ metadata.gz: 7b5d3e1bde28f482994a21faa5d1412808941a9a5f6384df22ec4307d7ce4f35
4
+ data.tar.gz: d5d90041346619db3781eb5f67747ccf91d9657f4b62ba1edafd806f959bd5a0
5
5
  SHA512:
6
- metadata.gz: 77edfb41b260ca90eaa82cc02fd63d81f5c8212845eba2fec5289d31d8d45d1b6c6cf5b02ebf911fbce91bc070a3f591f60d4c9d6a2906f5c87f7456fed1d83c
7
- data.tar.gz: 70606ebdd3a54ea6d10924b742a2821f5ce05b6ddf0ae0e8c0c23ed69d6042a40452aca569dd48d43b36fa7f85c99c96f674f22ad2722dc075bbdb5fa55f4d5f
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("Hello Ruflet", color: "DeepOrange500")
18
+ content: text(
19
+ value: "Hello Ruflet",
20
+ style: { color: "DeepOrange500", size: 20, weight: "w600" }
21
+ )
19
22
  )
20
23
  )
21
24
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Ruflet
4
- VERSION = "0.0.19" unless const_defined?(:VERSION)
4
+ VERSION = "0.0.20" unless const_defined?(:VERSION)
5
5
  end
@@ -78,10 +78,28 @@ module Ruflet
78
78
  }
79
79
  end
80
80
 
81
- def register_response(session_id:)
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
@@ -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
- @dialog = nil if @dialog.equal?(target)
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
- "fletapp" => %i[assets_dir on_python_output],
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 FletAppControl < Ruflet::Control
8
- TYPE = "fletapp".freeze
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?
@@ -116,6 +116,7 @@ module Ruflet
116
116
  "simple_attribution" => "SimpleAttribution",
117
117
  "codeeditor" => "CodeEditor",
118
118
  "code_editor" => "CodeEditor",
119
+ "lottie" => "Lottie",
119
120
  "flashlight" => "Flashlight",
120
121
  "barchart" => "BarChart",
121
122
  "barchartgroup" => "group",
@@ -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.19
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