ruflet_core 0.0.14 → 0.0.16

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: d10c0b00cedaedd9a12709dd513e56aef545f9ba61b5756481e50b270ca647ce
4
- data.tar.gz: 9a247d9aefdee2f5c667c3c5ae55235d17eb1a709c4dc402b0c794ecd5f0a752
3
+ metadata.gz: 888ec7addc07079a2ef3b649047b8747d031b446489d3015fb96cb5c2101b339
4
+ data.tar.gz: 342aac065292f59865cf01ace0347531803b41692eac2d3453fe5751c773fe0a
5
5
  SHA512:
6
- metadata.gz: 18548e0fe324df386b50cb0d5320e70283d64385244f8cf2101c0add56dd62e9a2ae5a8a29fccb86bc058eef18ccd511efc8218305121d58dba7ba40de73639d
7
- data.tar.gz: da7f855ee5f0721dbfb9ac249b6f34a7857e4a1e5e9ad2846fbb5954ae92b7c528aea6551796e6f1bcc2cec5ef7ad47aa75a7bdda48d18da3d82f6c94f9ceb56
6
+ metadata.gz: 2ef5114b3cc6389dafae6ab528990b7be92c0bc2367c62fc2eb0920feaec0dccc17400df7961df5b162bfbc539c2489818f77ac1dd91a040ebb87e1964016906
7
+ data.tar.gz: 9ad45dc326587bb2a601733b3c1efef6199c9d38424e8a82979d7b9f357fa44ffb579f19de30a3e2c8b2dd78681baf867b7555e9c21883f1f60dd44b7f4f50e0
data/README.md CHANGED
@@ -1,3 +1,19 @@
1
- # ruflet
1
+ # ruflet_core
2
2
 
3
- Part of Ruflet monorepo.
3
+ `ruflet_core` provides Ruflet's Ruby UI API: controls, control builders, page
4
+ operations, events, services, and application lifecycle behavior.
5
+
6
+ Applications normally receive this package through a generated Ruflet project
7
+ or through `ruflet_rails`; it is not a standalone application server.
8
+
9
+ ```ruby
10
+ require "ruflet"
11
+
12
+ Ruflet.run do |page|
13
+ page.title = "Hello"
14
+ page.add(text("Hello Ruflet"))
15
+ end
16
+ ```
17
+
18
+ Use `ruflet_server` to run a standalone server-driven application. Use
19
+ `ruflet_rails` when the UI is hosted by Rails.
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Ruflet
4
- VERSION = "0.0.14" unless const_defined?(:VERSION)
4
+ VERSION = "0.0.16" unless const_defined?(:VERSION)
5
5
  end
data/lib/ruflet_core.rb CHANGED
@@ -10,7 +10,8 @@ module Ruflet
10
10
 
11
11
  module_function
12
12
 
13
- def run(entrypoint = nil, host: "0.0.0.0", port: 8550, &block)
13
+ def run(entrypoint = nil, host: "0.0.0.0", port: nil, &block)
14
+ port = normalize_run_port(port || ENV["RUFLET_PORT"] || 8550)
14
15
  callback = entrypoint || block
15
16
  raise ArgumentError, "Ruflet.run requires a callable entrypoint or block" unless callback.respond_to?(:call)
16
17
 
@@ -37,4 +38,10 @@ module Ruflet
37
38
  ensure
38
39
  @run_interceptors_mutex.synchronize { @run_interceptors.delete(interceptor) }
39
40
  end
41
+
42
+ def normalize_run_port(value)
43
+ Integer(value)
44
+ rescue ArgumentError, TypeError
45
+ 8550
46
+ end
40
47
  end
@@ -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: {}, error: nil)
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,
@@ -151,11 +151,25 @@ module Ruflet
151
151
  end
152
152
 
153
153
  def self.normalize_color(color)
154
- return color.to_s if color.is_a?(Symbol)
155
- return color if color.is_a?(String)
156
- return color.to_s unless color.respond_to?(:to_s)
154
+ return canonicalize(color.to_s) if color.is_a?(Symbol)
155
+ return canonicalize(color) if color.is_a?(String)
156
+ return canonicalize(color.to_s) unless color.respond_to?(:to_s)
157
157
 
158
- color.to_s
158
+ canonicalize(color.to_s)
159
+ end
160
+
161
+ # Canonicalizes a named color into flet's wire format. Flet color names are
162
+ # lowercase with no separators ("bluegrey", "deeporange", "red500"), so we
163
+ # strip underscores/whitespace and downcase. Hex values (#... / 0x...) and
164
+ # the optional ",opacity" suffix are preserved untouched.
165
+ def self.canonicalize(value)
166
+ return value unless value.is_a?(String)
167
+
168
+ color, separator, opacity = value.partition(",")
169
+ color = color.strip.downcase
170
+ color = color.delete("_ \t\n") unless color.start_with?("#") || color.start_with?("0x")
171
+
172
+ "#{color}#{separator}#{opacity}"
159
173
  end
160
174
 
161
175
  BASE_PREFIX = {
@@ -5,6 +5,7 @@ require "securerandom"
5
5
  rescue LoadError
6
6
  nil
7
7
  end
8
+ require_relative "colors"
8
9
  require_relative "icon_data"
9
10
  require_relative "icons/material_icon_lookup"
10
11
  require_relative "icons/cupertino_icon_lookup"
@@ -47,6 +48,37 @@ module Ruflet
47
48
  @handlers.key?(normalized_event_name(event_name))
48
49
  end
49
50
 
51
+ # Read a prop by name: control["value"] or control[:value].
52
+ def [](key)
53
+ @props[key.to_s]
54
+ end
55
+
56
+ # Write a prop by name: control["value"] = "x".
57
+ def []=(key, value)
58
+ @props[key.to_s] = value
59
+ end
60
+
61
+ # Convenience dot access to props, mirroring Flet-style controls:
62
+ # control.value # => @props["value"] (reads an existing prop)
63
+ # control.value = "hello" # => sets @props["value"]
64
+ # Reads only resolve props that exist, so typos still raise NoMethodError
65
+ # instead of silently returning nil. Defined methods (type, id, props,
66
+ # children, on, emit, to_patch, …) are never shadowed.
67
+ def method_missing(name, *args, &block)
68
+ key = name.to_s
69
+ if key.end_with?("=")
70
+ return @props[key[0..-2]] = args.first
71
+ end
72
+ return @props[key] if args.empty? && @props.key?(key)
73
+
74
+ super
75
+ end
76
+
77
+ def respond_to_missing?(name, include_private = false)
78
+ key = name.to_s
79
+ key.end_with?("=") || @props.key?(key) || super
80
+ end
81
+
50
82
  def to_patch
51
83
  wire_type = schema_wire_type_for_class
52
84
  if wire_type.nil?
@@ -167,7 +199,7 @@ module Ruflet
167
199
 
168
200
  def normalize_color_prop(key, value)
169
201
  return value unless value.is_a?(String)
170
- return value.downcase if color_prop_key?(key)
202
+ return Ruflet::Colors.canonicalize(value) if color_prop_key?(key)
171
203
 
172
204
  value
173
205
  end
@@ -68,6 +68,15 @@ module Ruflet
68
68
  def autocompletesuggestion(key = nil, **props) = _pending_app.autocompletesuggestion(key, **props)
69
69
  def context_menu(content = nil, **props) = _pending_app.context_menu(content, **props)
70
70
  def contextmenu(content = nil, **props) = _pending_app.contextmenu(content, **props)
71
+ def autofill_group(content = nil, **props) = _pending_app.autofill_group(content, **props)
72
+ def autofillgroup(content = nil, **props) = _pending_app.autofillgroup(content, **props)
73
+ def hero(content = nil, **props) = _pending_app.hero(content, **props)
74
+ def overlay(children = nil, **props) = _pending_app.overlay(children, **props)
75
+ def shader_mask(content = nil, **props) = _pending_app.shader_mask(content, **props)
76
+ def shadermask(content = nil, **props) = _pending_app.shadermask(content, **props)
77
+ def shimmer(content = nil, **props) = _pending_app.shimmer(content, **props)
78
+ def text_span(text = nil, **props) = _pending_app.text_span(text, **props)
79
+ def textspan(text = nil, **props) = _pending_app.textspan(text, **props)
71
80
  def keyboard_listener(content = nil, **props) = _pending_app.keyboard_listener(content, **props)
72
81
  def keyboardlistener(content = nil, **props) = _pending_app.keyboardlistener(content, **props)
73
82
  def gesture_detector(**props, &block) = _pending_app.gesture_detector(**props, &block)
@@ -302,6 +311,10 @@ module Ruflet
302
311
  def web_view(**props) = _pending_app.web_view(**props)
303
312
  def webview(**props) = _pending_app.webview(**props)
304
313
  def video(**props) = _pending_app.video(**props)
314
+ def spinkit(**variant) = _pending_app.spinkit(**variant)
315
+ def code_editor(value = nil, **props) = _pending_app.code_editor(value, **props)
316
+ def codeeditor(value = nil, **props) = _pending_app.codeeditor(value, **props)
317
+ def rive(src = nil, **props) = _pending_app.rive(src, **props)
305
318
  def fab(content = nil, **props) = _pending_app.fab(content, **props)
306
319
  def cupertino_button(content = nil, **props) = _pending_app.cupertino_button(content, **props)
307
320
  def cupertinobutton(content = nil, **props) = _pending_app.cupertinobutton(content, **props)
@@ -244,6 +244,42 @@ module Ruflet
244
244
  @view_props["bgcolor"] = normalize_value("bgcolor", value)
245
245
  end
246
246
 
247
+ # Client-reported page properties. The Flutter client sends these in its
248
+ # register payload (see Protocol.normalize_register_payload), where they are
249
+ # stored in @client_details; expose them as readers so apps can do
250
+ # `page.width`, `page.platform`, etc. without reaching into client_details.
251
+ def width
252
+ client_reported_prop("width")
253
+ end
254
+
255
+ def height
256
+ client_reported_prop("height")
257
+ end
258
+
259
+ def platform
260
+ client_reported_prop("platform")
261
+ end
262
+
263
+ def platform_brightness
264
+ client_reported_prop("platform_brightness")
265
+ end
266
+
267
+ def web
268
+ client_reported_prop("web")
269
+ end
270
+
271
+ def pwa
272
+ client_reported_prop("pwa")
273
+ end
274
+
275
+ def wasm
276
+ client_reported_prop("wasm")
277
+ end
278
+
279
+ def media
280
+ client_reported_prop("media")
281
+ end
282
+
247
283
  def add(*controls, appbar: nil, bottom_appbar: nil, floating_action_button: nil, navigation_bar: nil, dialog: nil, snack_bar: nil, bottom_sheet: nil)
248
284
  controls = controls.flatten
249
285
  visited = Set.new
@@ -412,6 +448,10 @@ module Ruflet
412
448
  @page_event_handlers["view_pop"] = handler
413
449
  end
414
450
 
451
+ def on_resize=(handler)
452
+ @page_event_handlers["resize"] = handler
453
+ end
454
+
415
455
  def on(event_name, &block)
416
456
  @page_event_handlers[event_name.to_s.sub(/\Aon_/, "")] = block
417
457
  self
@@ -533,7 +573,7 @@ module Ruflet
533
573
  call_id = "call_#{Ruflet::Control.generate_id}"
534
574
  if on_result.respond_to?(:call)
535
575
  @invoke_waiters_mutex.synchronize { @invoke_callbacks[call_id] = on_result }
536
- unless timeout.nil?
576
+ if embedded_async_timeout_available? && !timeout.nil?
537
577
  Thread.new(call_id, timeout.to_f) do |pending_call_id, invoke_timeout|
538
578
  sleep([invoke_timeout, 0.0].max + 0.1)
539
579
  callback = @invoke_waiters_mutex.synchronize { @invoke_callbacks.delete(pending_call_id) }
@@ -1157,12 +1197,15 @@ module Ruflet
1157
1197
  def close_dialog(dialog_control)
1158
1198
  return self unless dialog_control
1159
1199
 
1160
- before_dialog_count = @dialogs_container.props["controls"].length
1161
1200
  dialog_control.props["open"] = false
1162
1201
  @dialog = nil if @dialog.equal?(dialog_control)
1163
1202
  remove_dialog_tracking(dialog_control)
1164
1203
  refresh_dialogs_container!
1165
- push_dialogs_update!(force_view: @dialogs_container.props["controls"].length < before_dialog_count)
1204
+ # Patch the dialogs container in place. Forcing a full view re-render
1205
+ # here would remount the whole overlay — fatal while another dialog
1206
+ # (e.g. the form behind a nested picker) is still open. The empty case
1207
+ # is handled inside push_dialogs_update!.
1208
+ push_dialogs_update!
1166
1209
  self
1167
1210
  end
1168
1211
 
@@ -1231,7 +1274,17 @@ module Ruflet
1231
1274
  if page_control_target?(target)
1232
1275
  if name.to_s == "route_change"
1233
1276
  route_from_event = extract_route(data)
1277
+ # Dialogs (including pickers) belong to the view that opened them.
1278
+ # Navigating away must dismiss them, or they ghost onto the next
1279
+ # view — the picker that "reappears after going home".
1280
+ dismiss_tracked_dialogs! if route_from_event && route_from_event != @page_props["route"]
1234
1281
  @page_props["route"] = route_from_event if route_from_event
1282
+ elsif name.to_s == "resize"
1283
+ # The client reports the live page size via the "resize" event. Store
1284
+ # it so `page.width`/`page.height` reflect the real viewport — without
1285
+ # this, responsive layouts collapse on clients (e.g. embedded/iOS)
1286
+ # that don't know their size at the initial handshake.
1287
+ store_reported_page_size(data)
1235
1288
  end
1236
1289
  dispatch_page_event(name: name, data: data)
1237
1290
  return
@@ -1240,11 +1293,16 @@ module Ruflet
1240
1293
  control = @wire_index[target.to_i] || @control_index[target.to_s]
1241
1294
  return unless control
1242
1295
 
1243
- event = Event.new(name: name, target: target, raw_data: data, page: self, control: control)
1296
+ event = Ruflet::Event.new(name: name, target: target, raw_data: data, page: self, control: control)
1244
1297
  apply_event_value_to_control(control, event) if %w[change select select_change].include?(name.to_s)
1245
- before_dialog_count = @dialogs_container.props["controls"].length
1298
+ # Material/Cupertino pickers dismiss themselves on the client once a
1299
+ # value is confirmed, but only send a value event — never a close. Mark
1300
+ # the dialog closed here so show_dialog can reopen it next time.
1301
+ mark_picker_dialog_closed(control, name)
1246
1302
  if dialog_close_event?(control, name) && remove_dialog_tracking(control)
1247
- push_dialogs_update!(force_view: @dialogs_container.props["controls"].length < before_dialog_count)
1303
+ # Patch the container in place; never force a full view re-render that
1304
+ # would remount a still-open parent dialog (the nested-picker case).
1305
+ push_dialogs_update!
1248
1306
  end
1249
1307
 
1250
1308
  control.emit(name, event)
@@ -1289,6 +1347,16 @@ module Ruflet
1289
1347
 
1290
1348
  private
1291
1349
 
1350
+ def client_reported_prop(name)
1351
+ return @page_props[name] if @page_props.key?(name)
1352
+
1353
+ @client_details[name]
1354
+ end
1355
+
1356
+ def embedded_async_timeout_available?
1357
+ !Object.const_defined?(:RUFLET_EMBEDDED_FAKE_THREAD)
1358
+ end
1359
+
1292
1360
  def invoke_and_wait(control_or_id, method_name, args: nil, timeout: 10)
1293
1361
  control_id =
1294
1362
  if page_control_target?(control_or_id)
@@ -1540,6 +1608,15 @@ module Ruflet
1540
1608
  end
1541
1609
  end
1542
1610
 
1611
+ def store_reported_page_size(data)
1612
+ return unless data.is_a?(Hash)
1613
+
1614
+ width = data["width"] || data[:width]
1615
+ height = data["height"] || data[:height]
1616
+ @page_props["width"] = width unless width.nil?
1617
+ @page_props["height"] = height unless height.nil?
1618
+ end
1619
+
1543
1620
  def dispatch_page_event(name:, data:)
1544
1621
  handler = @page_event_handlers[name.to_s.sub(/\Aon_/, "")]
1545
1622
  return unless handler.respond_to?(:call)
@@ -1634,15 +1711,15 @@ module Ruflet
1634
1711
  end
1635
1712
  end
1636
1713
 
1637
- def push_dialogs_update!(force_view: false)
1714
+ def push_dialogs_update!
1638
1715
  refresh_control_indexes!
1639
1716
 
1640
- if force_view || @dialogs_container.props["controls"].empty?
1641
- @dialogs_container_mounted = false
1642
- send_view_patch
1643
- return
1644
- end
1645
-
1717
+ # Once the dialogs container is mounted, every change — opening, closing,
1718
+ # even down to no dialogs at all — is an in-place patch of its controls
1719
+ # list. Re-sending the view (or the container as a whole object) would
1720
+ # replace the live container instance on the Flutter side, detaching its
1721
+ # listeners and breaking any other dialog still open. Only the very first
1722
+ # dialog, before the container has a wire id, needs a view patch to mount.
1646
1723
  if @dialogs_container.wire_id
1647
1724
  send_message(Protocol::ACTIONS[:patch_control], {
1648
1725
  "id" => @dialogs_container.wire_id,
@@ -1670,6 +1747,36 @@ module Ruflet
1670
1747
  name == "dismiss" || (%w[change select select_change].include?(name) && @dialogs.include?(control) && control.props["open"] == false)
1671
1748
  end
1672
1749
 
1750
+ # Picker dialogs that auto-dismiss on the client after a selection. Their
1751
+ # confirm sends a value event (change/select), not a close, so the server
1752
+ # must flip `open` to false or show_dialog's open-guard blocks reopening.
1753
+ PICKER_DIALOG_TYPES = %w[
1754
+ datepicker daterangepicker timepicker
1755
+ cupertinodatepicker cupertinotimerpicker
1756
+ ].freeze
1757
+
1758
+ def picker_dialog?(control)
1759
+ PICKER_DIALOG_TYPES.include?(control.type.to_s.tr("_", "").downcase)
1760
+ end
1761
+
1762
+ def mark_picker_dialog_closed(control, name)
1763
+ return unless picker_dialog?(control)
1764
+ return unless %w[change select select_change dismiss].include?(name.to_s)
1765
+
1766
+ control.props["open"] = false
1767
+ end
1768
+
1769
+ # Close and untrack every dialog currently shown. Called on navigation so
1770
+ # a dialog opened in one view does not linger as an overlay on the next.
1771
+ def dismiss_tracked_dialogs!
1772
+ return if @dialogs.empty?
1773
+
1774
+ @dialogs.each { |dialog| dialog.props["open"] = false }
1775
+ @dialogs.clear
1776
+ refresh_dialogs_container!
1777
+ push_dialogs_update! if @dialogs_container_mounted
1778
+ end
1779
+
1673
1780
  def remove_dialog_tracking(control)
1674
1781
  return false unless @dialogs.include?(control)
1675
1782
 
@@ -1734,6 +1841,7 @@ module Ruflet
1734
1841
  existing = service_by_type(type)
1735
1842
  return [existing, false] if existing
1736
1843
 
1844
+ # `service` already syncs via add_service -> push_services_update!.
1737
1845
  [service(type), true]
1738
1846
  end
1739
1847
 
@@ -1821,7 +1929,7 @@ module Ruflet
1821
1929
  call_id = "call_#{Ruflet::Control.generate_id}"
1822
1930
  if on_result.respond_to?(:call)
1823
1931
  @invoke_waiters_mutex.synchronize { @invoke_callbacks[call_id] = on_result }
1824
- unless timeout.nil?
1932
+ if embedded_async_timeout_available? && !timeout.nil?
1825
1933
  Thread.new(call_id, timeout.to_f) do |pending_call_id, invoke_timeout|
1826
1934
  sleep([invoke_timeout, 0.0].max + 0.1)
1827
1935
  callback = @invoke_waiters_mutex.synchronize { @invoke_callbacks.delete(pending_call_id) }
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ruflet
4
+ module UI
5
+ module Controls
6
+ module RufletComponents
7
+ # CodeEditor control — parity with Flet's CodeEditor extension
8
+ # (https://flet.dev/docs/controls/codeeditor/).
9
+ #
10
+ # Properties: value, language, code_theme, text_style, padding, selection,
11
+ # gutter_style, autocomplete, autocomplete_words, issues, read_only,
12
+ # autofocus, plus the usual layout props.
13
+ # Events: on_change, on_selection_change, on_focus, on_blur.
14
+ # Methods (invoked over the wire on a mounted control): focus, fold_at,
15
+ # fold_comment_at_line_zero, fold_imports.
16
+ #
17
+ # `language` accepts a highlight.js identifier (e.g. "python", "ruby",
18
+ # "javascript"); `code_theme` accepts a highlight.js theme name shared
19
+ # with Markdown (e.g. "atom-one-light", "atom-one-dark", "monokai-sublime").
20
+ class CodeEditorControl < Ruflet::Control
21
+ TYPE = "CodeEditor".freeze
22
+ WIRE = "CodeEditor".freeze
23
+
24
+ def initialize(id: nil, adaptive: nil, autocomplete: nil, autocomplete_words: nil,
25
+ autofocus: nil, badge: nil, code_theme: nil, col: nil, data: nil,
26
+ disabled: nil, expand: nil, expand_loose: nil, gutter_style: nil,
27
+ height: nil, issues: nil, key: nil, language: nil, opacity: nil,
28
+ padding: nil, read_only: nil, rtl: nil, selection: nil,
29
+ text_style: nil, tooltip: nil, value: nil, visible: nil, width: nil,
30
+ on_blur: nil, on_change: nil, on_focus: nil, on_selection_change: nil)
31
+ props = {}
32
+ props[:adaptive] = adaptive unless adaptive.nil?
33
+ props[:autocomplete] = autocomplete unless autocomplete.nil?
34
+ props[:autocomplete_words] = autocomplete_words unless autocomplete_words.nil?
35
+ props[:autofocus] = autofocus unless autofocus.nil?
36
+ props[:badge] = badge unless badge.nil?
37
+ props[:code_theme] = code_theme unless code_theme.nil?
38
+ props[:col] = col unless col.nil?
39
+ props[:data] = data unless data.nil?
40
+ props[:disabled] = disabled unless disabled.nil?
41
+ props[:expand] = expand unless expand.nil?
42
+ props[:expand_loose] = expand_loose unless expand_loose.nil?
43
+ props[:gutter_style] = gutter_style unless gutter_style.nil?
44
+ props[:height] = height unless height.nil?
45
+ props[:issues] = issues unless issues.nil?
46
+ props[:key] = key unless key.nil?
47
+ props[:language] = language unless language.nil?
48
+ props[:opacity] = opacity unless opacity.nil?
49
+ props[:padding] = padding unless padding.nil?
50
+ props[:read_only] = read_only unless read_only.nil?
51
+ props[:rtl] = rtl unless rtl.nil?
52
+ props[:selection] = selection unless selection.nil?
53
+ props[:text_style] = text_style unless text_style.nil?
54
+ props[:tooltip] = tooltip unless tooltip.nil?
55
+ props[:value] = value unless value.nil?
56
+ props[:visible] = visible unless visible.nil?
57
+ props[:width] = width unless width.nil?
58
+ props[:on_blur] = on_blur unless on_blur.nil?
59
+ props[:on_change] = on_change unless on_change.nil?
60
+ props[:on_focus] = on_focus unless on_focus.nil?
61
+ props[:on_selection_change] = on_selection_change unless on_selection_change.nil?
62
+ super(type: TYPE, id: id, **props)
63
+ end
64
+
65
+ # Request focus for the editor.
66
+ def focus = invoke_editor_method("focus")
67
+
68
+ # Fold the code block that starts at the given line number.
69
+ def fold_at(line_number)
70
+ invoke_editor_method("fold_at", { "line_number" => line_number.to_i })
71
+ end
72
+
73
+ # Fold the comment block at line 0 (e.g. a license header).
74
+ def fold_comment_at_line_zero = invoke_editor_method("fold_comment_at_line_zero")
75
+
76
+ # Fold all import sections.
77
+ def fold_imports = invoke_editor_method("fold_imports")
78
+
79
+ private
80
+
81
+ def invoke_editor_method(name, args = nil, timeout: 10, on_result: nil)
82
+ page = runtime_page
83
+ unless page && wire_id
84
+ raise "CodeEditor ##{id} is not mounted yet — add it to the page before calling #{name}."
85
+ end
86
+
87
+ page.invoke(self, name, args: args, timeout: timeout, on_result: on_result)
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ruflet
4
+ module UI
5
+ module Controls
6
+ module RufletComponents
7
+ # Rive control — parity with Flet's Rive extension
8
+ # (https://flet.dev/docs/controls/rive/). Renders a Rive
9
+ # (https://rive.app) animation from a `.riv` file.
10
+ #
11
+ # Properties: src, placeholder, artboard, alignment, enable_antialiasing,
12
+ # use_artboard_size, fit, speed_multiplier, animations, state_machines,
13
+ # headers, clip_rect, plus the usual layout props.
14
+ # No events or methods — playback is driven by `animations` /
15
+ # `state_machines` and `speed_multiplier`.
16
+ #
17
+ # `src` is required and may be a network URL (e.g.
18
+ # "https://cdn.rive.app/animations/vehicles.riv") or a bundled asset path.
19
+ #
20
+ # Note: the Flet API names the artboard property `artboard`, but the
21
+ # underlying renderer reads `art_board` / `use_art_board_size` on the wire.
22
+ # Both spellings are accepted here and normalized to the wire keys.
23
+ class RiveControl < Ruflet::Control
24
+ TYPE = "Rive".freeze
25
+ WIRE = "Rive".freeze
26
+
27
+ def initialize(id: nil, adaptive: nil, alignment: nil, animate_offset: nil,
28
+ animate_opacity: nil, animate_position: nil, animate_rotation: nil,
29
+ animate_scale: nil, animations: nil, art_board: nil, artboard: nil,
30
+ aspect_ratio: nil, badge: nil, bottom: nil, clip_rect: nil, col: nil,
31
+ data: nil, disabled: nil, enable_antialiasing: nil, expand: nil,
32
+ expand_loose: nil, fit: nil, headers: nil, height: nil, key: nil,
33
+ left: nil, offset: nil, opacity: nil, placeholder: nil, right: nil,
34
+ rotate: nil, rtl: nil, scale: nil, speed_multiplier: nil, src: nil,
35
+ state_machines: nil, tooltip: nil, top: nil, use_art_board_size: nil,
36
+ use_artboard_size: nil, visible: nil, width: nil)
37
+ # Accept both the Flet-style names and the wire keys.
38
+ art_board = artboard if art_board.nil?
39
+ use_art_board_size = use_artboard_size if use_art_board_size.nil?
40
+
41
+ props = {}
42
+ props[:adaptive] = adaptive unless adaptive.nil?
43
+ props[:alignment] = alignment unless alignment.nil?
44
+ props[:animate_offset] = animate_offset unless animate_offset.nil?
45
+ props[:animate_opacity] = animate_opacity unless animate_opacity.nil?
46
+ props[:animate_position] = animate_position unless animate_position.nil?
47
+ props[:animate_rotation] = animate_rotation unless animate_rotation.nil?
48
+ props[:animate_scale] = animate_scale unless animate_scale.nil?
49
+ props[:animations] = animations unless animations.nil?
50
+ props[:art_board] = art_board unless art_board.nil?
51
+ props[:aspect_ratio] = aspect_ratio unless aspect_ratio.nil?
52
+ props[:badge] = badge unless badge.nil?
53
+ props[:bottom] = bottom unless bottom.nil?
54
+ props[:clip_rect] = clip_rect unless clip_rect.nil?
55
+ props[:col] = col unless col.nil?
56
+ props[:data] = data unless data.nil?
57
+ props[:disabled] = disabled unless disabled.nil?
58
+ props[:enable_antialiasing] = enable_antialiasing unless enable_antialiasing.nil?
59
+ props[:expand] = expand unless expand.nil?
60
+ props[:expand_loose] = expand_loose unless expand_loose.nil?
61
+ props[:fit] = fit unless fit.nil?
62
+ props[:headers] = headers unless headers.nil?
63
+ props[:height] = height unless height.nil?
64
+ props[:key] = key unless key.nil?
65
+ props[:left] = left unless left.nil?
66
+ props[:offset] = offset unless offset.nil?
67
+ props[:opacity] = opacity unless opacity.nil?
68
+ props[:placeholder] = placeholder unless placeholder.nil?
69
+ props[:right] = right unless right.nil?
70
+ props[:rotate] = rotate unless rotate.nil?
71
+ props[:rtl] = rtl unless rtl.nil?
72
+ props[:scale] = scale unless scale.nil?
73
+ props[:speed_multiplier] = speed_multiplier unless speed_multiplier.nil?
74
+ props[:src] = src unless src.nil?
75
+ props[:state_machines] = state_machines unless state_machines.nil?
76
+ props[:tooltip] = tooltip unless tooltip.nil?
77
+ props[:top] = top unless top.nil?
78
+ props[:use_art_board_size] = use_art_board_size unless use_art_board_size.nil?
79
+ props[:visible] = visible unless visible.nil?
80
+ props[:width] = width unless width.nil?
81
+ super(type: TYPE, id: id, **props)
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
@@ -15,6 +15,7 @@ require_relative "card_control"
15
15
  require_relative "checkbox_control"
16
16
  require_relative "chip_control"
17
17
  require_relative "circleavatar_control"
18
+ require_relative "codeeditor_control"
18
19
  require_relative "container_control"
19
20
  require_relative "contextmenu_control"
20
21
  require_relative "datacell_control"
@@ -57,6 +58,7 @@ require_relative "radio_control"
57
58
  require_relative "radiogroup_control"
58
59
  require_relative "rangeslider_control"
59
60
  require_relative "reorderablelistview_control"
61
+ require_relative "rive_control"
60
62
  require_relative "searchbar_control"
61
63
  require_relative "segment_control"
62
64
  require_relative "segmentedbutton_control"
@@ -120,6 +122,8 @@ module Ruflet
120
122
  "chip" => RufletComponents::ChipControl,
121
123
  "circle_avatar" => RufletComponents::CircleAvatarControl,
122
124
  "circleavatar" => RufletComponents::CircleAvatarControl,
125
+ "code_editor" => RufletComponents::CodeEditorControl,
126
+ "codeeditor" => RufletComponents::CodeEditorControl,
123
127
  "container" => RufletComponents::ContainerControl,
124
128
  "context_menu" => RufletComponents::ContextMenuControl,
125
129
  "contextmenu" => RufletComponents::ContextMenuControl,
@@ -235,6 +239,7 @@ module Ruflet
235
239
  "rangeslider" => RufletComponents::RangeSliderControl,
236
240
  "reorderable_list_view" => RufletComponents::ReorderableListViewControl,
237
241
  "reorderablelistview" => RufletComponents::ReorderableListViewControl,
242
+ "rive" => RufletComponents::RiveControl,
238
243
  "search_bar" => RufletComponents::SearchBarControl,
239
244
  "searchbar" => RufletComponents::SearchBarControl,
240
245
  "segment" => RufletComponents::SegmentControl,
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ruflet
4
+ module UI
5
+ module Controls
6
+ module RufletComponents
7
+ # Base for every flet_spinkit variant (https://flet.dev/docs/controls/spinkit/).
8
+ # Each concrete spinner is a LayoutControl with its own wire name
9
+ # ("SpinKitRotatingCircle", "SpinKitWave", ...). The Dart widget reads
10
+ # color/size/duration for all of them plus the optional line_width /
11
+ # border_width / item_count / wave_type used by a few variants, so those
12
+ # are accepted on every spinner (the client ignores the ones a variant
13
+ # doesn't use, matching the upstream implementation).
14
+ class SpinKitControl < Ruflet::Control
15
+ def initialize(id: nil, align: nil, animate_align: nil, animate_offset: nil, animate_opacity: nil, animate_position: nil, animate_rotation: nil, animate_scale: nil, animate_size: nil, aspect_ratio: nil, badge: nil, border_width: nil, bottom: nil, col: nil, color: nil, data: nil, disabled: nil, duration: nil, expand: nil, expand_loose: nil, height: nil, item_count: nil, key: nil, left: nil, line_width: nil, offset: nil, opacity: nil, right: nil, rotate: nil, rtl: nil, scale: nil, size: nil, tooltip: nil, top: nil, visible: nil, wave_type: nil, width: nil, on_animation_end: nil)
16
+ raise ArgumentError, "spinkit size must be greater than or equal to 0" unless size.nil? || size >= 0
17
+
18
+ props = {}
19
+ props[:align] = align unless align.nil?
20
+ props[:animate_align] = animate_align unless animate_align.nil?
21
+ props[:animate_offset] = animate_offset unless animate_offset.nil?
22
+ props[:animate_opacity] = animate_opacity unless animate_opacity.nil?
23
+ props[:animate_position] = animate_position unless animate_position.nil?
24
+ props[:animate_rotation] = animate_rotation unless animate_rotation.nil?
25
+ props[:animate_scale] = animate_scale unless animate_scale.nil?
26
+ props[:animate_size] = animate_size unless animate_size.nil?
27
+ props[:aspect_ratio] = aspect_ratio unless aspect_ratio.nil?
28
+ props[:badge] = badge unless badge.nil?
29
+ props[:border_width] = border_width unless border_width.nil?
30
+ props[:bottom] = bottom unless bottom.nil?
31
+ props[:col] = col unless col.nil?
32
+ props[:color] = color unless color.nil?
33
+ props[:data] = data unless data.nil?
34
+ props[:disabled] = disabled unless disabled.nil?
35
+ props[:duration] = duration unless duration.nil?
36
+ props[:expand] = expand unless expand.nil?
37
+ props[:expand_loose] = expand_loose unless expand_loose.nil?
38
+ props[:height] = height unless height.nil?
39
+ props[:item_count] = item_count unless item_count.nil?
40
+ props[:key] = key unless key.nil?
41
+ props[:left] = left unless left.nil?
42
+ props[:line_width] = line_width unless line_width.nil?
43
+ props[:offset] = offset unless offset.nil?
44
+ props[:opacity] = opacity unless opacity.nil?
45
+ props[:right] = right unless right.nil?
46
+ props[:rotate] = rotate unless rotate.nil?
47
+ props[:rtl] = rtl unless rtl.nil?
48
+ props[:scale] = scale unless scale.nil?
49
+ props[:size] = size unless size.nil?
50
+ props[:tooltip] = tooltip unless tooltip.nil?
51
+ props[:top] = top unless top.nil?
52
+ props[:visible] = visible unless visible.nil?
53
+ props[:wave_type] = wave_type unless wave_type.nil?
54
+ props[:width] = width unless width.nil?
55
+ props[:on_animation_end] = on_animation_end unless on_animation_end.nil?
56
+ super(type: self.class::TYPE, id: id, **props)
57
+ end
58
+ end
59
+
60
+ # wire name ("_c") => ruflet type key. Mirrors flet_spinkit's 30 controls.
61
+ SPINKIT_WIRE_TO_TYPE = {
62
+ "SpinKitRotatingCircle" => "spinkit_rotating_circle",
63
+ "SpinKitRotatingPlain" => "spinkit_rotating_plain",
64
+ "SpinKitDoubleBounce" => "spinkit_double_bounce",
65
+ "SpinKitWave" => "spinkit_wave",
66
+ "SpinKitWanderingCubes" => "spinkit_wandering_cubes",
67
+ "SpinKitFadingFour" => "spinkit_fading_four",
68
+ "SpinKitFadingCube" => "spinkit_fading_cube",
69
+ "SpinKitPulse" => "spinkit_pulse",
70
+ "SpinKitChasingDots" => "spinkit_chasing_dots",
71
+ "SpinKitThreeBounce" => "spinkit_three_bounce",
72
+ "SpinKitCircle" => "spinkit_circle",
73
+ "SpinKitCubeGrid" => "spinkit_cube_grid",
74
+ "SpinKitFadingCircle" => "spinkit_fading_circle",
75
+ "SpinKitFoldingCube" => "spinkit_folding_cube",
76
+ "SpinKitPumpingHeart" => "spinkit_pumping_heart",
77
+ "SpinKitHourGlass" => "spinkit_hour_glass",
78
+ "SpinKitPouringHourGlass" => "spinkit_pouring_hour_glass",
79
+ "SpinKitPouringHourGlassRefined" => "spinkit_pouring_hour_glass_refined",
80
+ "SpinKitFadingGrid" => "spinkit_fading_grid",
81
+ "SpinKitRing" => "spinkit_ring",
82
+ "SpinKitRipple" => "spinkit_ripple",
83
+ "SpinKitDualRing" => "spinkit_dual_ring",
84
+ "SpinKitSpinningCircle" => "spinkit_spinning_circle",
85
+ "SpinKitSpinningLines" => "spinkit_spinning_lines",
86
+ "SpinKitSquareCircle" => "spinkit_square_circle",
87
+ "SpinKitThreeInOut" => "spinkit_three_in_out",
88
+ "SpinKitDancingSquare" => "spinkit_dancing_square",
89
+ "SpinKitPianoWave" => "spinkit_piano_wave",
90
+ "SpinKitPulsingGrid" => "spinkit_pulsing_grid",
91
+ "SpinKitWaveSpinner" => "spinkit_wave_spinner"
92
+ }.freeze
93
+
94
+ # type key => control class, e.g. "spinkit_wave" => SpinKitWaveControl.
95
+ SPINKIT_CONTROLS = {}
96
+
97
+ SPINKIT_WIRE_TO_TYPE.each do |wire, type_key|
98
+ klass = Class.new(SpinKitControl)
99
+ klass.const_set(:TYPE, type_key)
100
+ klass.const_set(:WIRE, wire)
101
+ const_set("#{wire}Control", klass)
102
+ SPINKIT_CONTROLS[type_key] = klass
103
+ end
104
+
105
+ SPINKIT_CONTROLS.freeze
106
+ end
107
+ end
108
+ end
109
+ end
@@ -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, height: nil, key: nil, method: nil, opacity: nil, rtl: nil, tooltip: nil, url: nil, visible: nil, width: nil, on_page_ended: nil, on_page_started: nil, on_web_resource_error: 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
@@ -39,6 +39,7 @@ require_relative "materials/card_control"
39
39
  require_relative "materials/checkbox_control"
40
40
  require_relative "materials/chip_control"
41
41
  require_relative "materials/circleavatar_control"
42
+ require_relative "materials/codeeditor_control"
42
43
  require_relative "materials/container_control"
43
44
  require_relative "materials/contextmenu_control"
44
45
  require_relative "materials/datacell_control"
@@ -58,6 +59,7 @@ require_relative "materials/filledbutton_control"
58
59
  require_relative "materials/fillediconbutton_control"
59
60
  require_relative "materials/filledtonalbutton_control"
60
61
  require_relative "materials/filledtonaliconbutton_control"
62
+ require_relative "materials/spinkit_controls"
61
63
  require_relative "materials/floatingactionbutton_control"
62
64
  require_relative "materials/iconbutton_control"
63
65
  require_relative "materials/listtile_control"
@@ -81,6 +83,7 @@ require_relative "materials/radio_control"
81
83
  require_relative "materials/radiogroup_control"
82
84
  require_relative "materials/rangeslider_control"
83
85
  require_relative "materials/reorderablelistview_control"
86
+ require_relative "materials/rive_control"
84
87
  require_relative "materials/searchbar_control"
85
88
  require_relative "materials/segment_control"
86
89
  require_relative "materials/segmentedbutton_control"
@@ -206,6 +209,8 @@ module Ruflet
206
209
  "circle" => RufletComponents::CircleControl,
207
210
  "circle_avatar" => RufletComponents::CircleAvatarControl,
208
211
  "circleavatar" => RufletComponents::CircleAvatarControl,
212
+ "code_editor" => RufletComponents::CodeEditorControl,
213
+ "codeeditor" => RufletComponents::CodeEditorControl,
209
214
  "color" => RufletComponents::ColorControl,
210
215
  "column" => RufletComponents::ColumnControl,
211
216
  "container" => RufletComponents::ContainerControl,
@@ -408,6 +413,7 @@ module Ruflet
408
413
  "reorderable_list_view" => RufletComponents::ReorderableListViewControl,
409
414
  "reorderabledraghandle" => RufletComponents::ReorderableDragHandleControl,
410
415
  "reorderablelistview" => RufletComponents::ReorderableListViewControl,
416
+ "rive" => RufletComponents::RiveControl,
411
417
  "responsive_row" => RufletComponents::ResponsiveRowControl,
412
418
  "responsiverow" => RufletComponents::ResponsiveRowControl,
413
419
  "row" => RufletComponents::RowControl,
@@ -464,7 +470,7 @@ module Ruflet
464
470
  "window" => RufletComponents::WindowControl,
465
471
  "window_drag_area" => RufletComponents::WindowDragAreaControl,
466
472
  "windowdragarea" => RufletComponents::WindowDragAreaControl,
467
- }.freeze
473
+ }.merge(RufletComponents::SPINKIT_CONTROLS).freeze
468
474
  end
469
475
  end
470
476
  end
@@ -50,10 +50,28 @@ module Ruflet
50
50
  super(type: TYPE, id: id, **props)
51
51
  end
52
52
 
53
- %w[wait_until_ready_to_show to_front center close destroy start_dragging].each do |method_name|
54
- define_method(method_name) do |timeout: 10, on_result: nil|
55
- runtime_page&.invoke(self, method_name, timeout: timeout, on_result: on_result)
56
- end
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
@@ -81,6 +81,39 @@ module Ruflet
81
81
  build_widget(:contextmenu, **mapped)
82
82
  end
83
83
  def contextmenu(content = nil, **props) = context_menu(content, **props)
84
+ def autofill_group(content = nil, **props)
85
+ mapped = props.dup
86
+ mapped[:content] = content unless content.nil?
87
+ build_widget(:autofillgroup, **mapped)
88
+ end
89
+ def autofillgroup(content = nil, **props) = autofill_group(content, **props)
90
+ def hero(content = nil, **props)
91
+ mapped = props.dup
92
+ mapped[:content] = content unless content.nil?
93
+ build_widget(:hero, **mapped)
94
+ end
95
+ def overlay(children = nil, **props)
96
+ mapped = props.dup
97
+ mapped[:controls] = children unless children.nil?
98
+ build_widget(:overlay, **mapped)
99
+ end
100
+ def shader_mask(content = nil, **props)
101
+ mapped = props.dup
102
+ mapped[:content] = content unless content.nil?
103
+ build_widget(:shadermask, **mapped)
104
+ end
105
+ def shadermask(content = nil, **props) = shader_mask(content, **props)
106
+ def shimmer(content = nil, **props)
107
+ mapped = props.dup
108
+ mapped[:content] = content unless content.nil?
109
+ build_widget(:shimmer, **mapped)
110
+ end
111
+ def text_span(text = nil, **props)
112
+ mapped = props.dup
113
+ mapped[:text] = text unless text.nil?
114
+ build_widget(:textspan, **mapped)
115
+ end
116
+ def textspan(text = nil, **props) = text_span(text, **props)
84
117
  def keyboard_listener(content = nil, **props)
85
118
  mapped = props.dup
86
119
  mapped[:content] = content unless content.nil?
@@ -606,6 +639,31 @@ module Ruflet
606
639
  def webview(**props) = web_view(**props)
607
640
  def video(**props) = build_widget(:video, **props)
608
641
 
642
+ # spinkit(wave: { color: "red", size: 50 }) — one variant keyword whose
643
+ # value is the props hash. See https://flet.dev/docs/controls/spinkit/
644
+ def spinkit(**variant)
645
+ raise ArgumentError, "spinkit expects exactly one variant, e.g. spinkit(wave: { color: ... })" unless variant.size == 1
646
+
647
+ name, props = variant.first
648
+ props ||= {}
649
+ raise ArgumentError, "spinkit #{name} options must be a Hash" unless props.is_a?(Hash)
650
+
651
+ build_widget(:"spinkit_#{name}", **props)
652
+ end
653
+
654
+ def code_editor(value = nil, **props)
655
+ mapped = props.dup
656
+ mapped[:value] = value unless value.nil?
657
+ build_widget(:codeeditor, **mapped)
658
+ end
659
+ def codeeditor(value = nil, **props) = code_editor(value, **props)
660
+
661
+ def rive(src = nil, **props)
662
+ mapped = props.dup
663
+ mapped[:src] = src unless src.nil?
664
+ build_widget(:rive, **mapped)
665
+ end
666
+
609
667
  def fab(content = nil, **props)
610
668
  mapped = normalize_fab_props(props.dup, content)
611
669
  build_widget(:floatingactionbutton, **mapped)
@@ -8,7 +8,8 @@ module Ruflet
8
8
  TYPE = "audiorecorder".freeze
9
9
  WIRE = "AudioRecorder".freeze
10
10
 
11
- def initialize(id: nil, configuration: {}, data: nil, key: nil, on_state_change: nil, on_stream: nil, on_upload: nil)
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?
@@ -43,7 +44,8 @@ module Ruflet
43
44
  )
44
45
  end
45
46
 
46
- def start_recording(output_path: nil, configuration: {}, upload: nil, timeout: 10, on_result: nil)
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(
@@ -26,6 +26,15 @@ module Ruflet
26
26
  def autocompletesuggestion(key = nil, **props) = control_delegate.autocompletesuggestion(key, **props)
27
27
  def context_menu(content = nil, **props) = control_delegate.context_menu(content, **props)
28
28
  def contextmenu(content = nil, **props) = control_delegate.contextmenu(content, **props)
29
+ def autofill_group(content = nil, **props) = control_delegate.autofill_group(content, **props)
30
+ def autofillgroup(content = nil, **props) = control_delegate.autofillgroup(content, **props)
31
+ def hero(content = nil, **props) = control_delegate.hero(content, **props)
32
+ def overlay(children = nil, **props) = control_delegate.overlay(children, **props)
33
+ def shader_mask(content = nil, **props) = control_delegate.shader_mask(content, **props)
34
+ def shadermask(content = nil, **props) = control_delegate.shadermask(content, **props)
35
+ def shimmer(content = nil, **props) = control_delegate.shimmer(content, **props)
36
+ def text_span(text = nil, **props) = control_delegate.text_span(text, **props)
37
+ def textspan(text = nil, **props) = control_delegate.textspan(text, **props)
29
38
  def keyboard_listener(content = nil, **props) = control_delegate.keyboard_listener(content, **props)
30
39
  def keyboardlistener(content = nil, **props) = control_delegate.keyboardlistener(content, **props)
31
40
  def gesture_detector(**props, &block) = control_delegate.gesture_detector(**props, &block)
@@ -261,6 +270,10 @@ module Ruflet
261
270
  def web_view(**props) = control_delegate.web_view(**props)
262
271
  def webview(**props) = control_delegate.webview(**props)
263
272
  def video(**props) = control_delegate.video(**props)
273
+ def spinkit(**variant) = control_delegate.spinkit(**variant)
274
+ def code_editor(value = nil, **props) = control_delegate.code_editor(value, **props)
275
+ def codeeditor(value = nil, **props) = control_delegate.codeeditor(value, **props)
276
+ def rive(src = nil, **props) = control_delegate.rive(src, **props)
264
277
  def cupertino_button(content = nil, **props) = control_delegate.cupertino_button(content, **props)
265
278
  def cupertinobutton(content = nil, **props) = control_delegate.cupertinobutton(content, **props)
266
279
  def cupertino_filled_button(content = nil, **props) = control_delegate.cupertino_filled_button(content, **props)
data/lib/ruflet_ui.rb CHANGED
@@ -140,7 +140,7 @@ module Ruflet
140
140
  private
141
141
 
142
142
  def control_delegate
143
- Ruflet::DSL
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::DSL
162
+ Ruflet::WidgetBuilder.new
159
163
  end
160
164
 
161
165
  if Ruflet::UI::SharedControlForwarders.respond_to?(:instance_methods)
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.14
4
+ version: 0.0.16
5
5
  platform: ruby
6
6
  authors:
7
7
  - AdamMusa
@@ -83,6 +83,7 @@ files:
83
83
  - lib/ruflet_ui/ruflet/ui/controls/materials/checkbox_control.rb
84
84
  - lib/ruflet_ui/ruflet/ui/controls/materials/chip_control.rb
85
85
  - lib/ruflet_ui/ruflet/ui/controls/materials/circleavatar_control.rb
86
+ - lib/ruflet_ui/ruflet/ui/controls/materials/codeeditor_control.rb
86
87
  - lib/ruflet_ui/ruflet/ui/controls/materials/container_control.rb
87
88
  - lib/ruflet_ui/ruflet/ui/controls/materials/contextmenu_control.rb
88
89
  - lib/ruflet_ui/ruflet/ui/controls/materials/datacell_control.rb
@@ -125,6 +126,7 @@ files:
125
126
  - lib/ruflet_ui/ruflet/ui/controls/materials/radiogroup_control.rb
126
127
  - lib/ruflet_ui/ruflet/ui/controls/materials/rangeslider_control.rb
127
128
  - lib/ruflet_ui/ruflet/ui/controls/materials/reorderablelistview_control.rb
129
+ - lib/ruflet_ui/ruflet/ui/controls/materials/rive_control.rb
128
130
  - lib/ruflet_ui/ruflet/ui/controls/materials/ruflet_controls.rb
129
131
  - lib/ruflet_ui/ruflet/ui/controls/materials/searchbar_control.rb
130
132
  - lib/ruflet_ui/ruflet/ui/controls/materials/segment_control.rb
@@ -132,6 +134,7 @@ files:
132
134
  - lib/ruflet_ui/ruflet/ui/controls/materials/selectionarea_control.rb
133
135
  - lib/ruflet_ui/ruflet/ui/controls/materials/slider_control.rb
134
136
  - lib/ruflet_ui/ruflet/ui/controls/materials/snackbar_control.rb
137
+ - lib/ruflet_ui/ruflet/ui/controls/materials/spinkit_controls.rb
135
138
  - lib/ruflet_ui/ruflet/ui/controls/materials/submenubutton_control.rb
136
139
  - lib/ruflet_ui/ruflet/ui/controls/materials/switch_control.rb
137
140
  - lib/ruflet_ui/ruflet/ui/controls/materials/tab_control.rb
@@ -233,7 +236,8 @@ files:
233
236
  - lib/ruflet_ui/ruflet/ui/shared_control_forwarders.rb
234
237
  - lib/ruflet_ui/ruflet/ui/widget_builder.rb
235
238
  homepage: https://github.com/AdamMusa/Ruflet
236
- licenses: []
239
+ licenses:
240
+ - MIT
237
241
  metadata: {}
238
242
  rdoc_options: []
239
243
  require_paths: