ruflet_core 0.0.15 → 0.0.17

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: c1823b100c3e97ee941849763509b25179554464310eb640d7954bb48a30d821
4
- data.tar.gz: 80d3f6891f3418bf0b2b3aa2007e08e6578ca336b72bb48b9d84c54cac2397b5
3
+ metadata.gz: 82d5d751f2b92b2b740414e315e1b1f9e88389eb0423ec0ce7a040710e191d59
4
+ data.tar.gz: 728109060a0cff6ca564e2bfa5d7620e5f8182dc05cf82b571ae3280e7e0a015
5
5
  SHA512:
6
- metadata.gz: e37e04ea218fbc50da5d3a456ecdfe376f12d00fc802c0869da7dff977bafb6d0abab8eecff92e223b42002f94652e40f7d91d2eb7a62f04fbec72bf1c7dd600
7
- data.tar.gz: 42458a85edccff3f66fd88cdc71724a7a94dbf1853fbf080d0751da7b57e783694e0ba65091c908bb4b1f203d6a8e8725e9e594df762f7deb1996e29e1bc88ed
6
+ metadata.gz: bf40cdafd48b625d189f9625945350873b4936c0668916ce8556e9d07f47368584892b29e57e4fe2e4583230d7ce5c87297848b90c5d01ab1d7ddac0b60df4d2
7
+ data.tar.gz: 2d229699ad359fcbc5dc5640dd176227bd0dad3f01d641b9f44ea569b26ccea61926b16c465284bda7e2fb786b514215922b8fdda5aa0a6b5f6baca36c0303fe
data/README.md CHANGED
@@ -1,3 +1,31 @@
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(
15
+ container(
16
+ bgcolor: :surface_container_high,
17
+ padding: 24,
18
+ content: text("Hello Ruflet", color: "DeepOrange500")
19
+ )
20
+ )
21
+ end
22
+ ```
23
+
24
+ Color properties accept named colors and hex values. Use Ruby-style symbols
25
+ like `:deep_orange_500`, compact strings like `"DeepOrange500"`, spaced names
26
+ like `"Deep Orange 500"`, semantic theme names like `:primary_container`, or
27
+ hex strings like `"#ff6d00"`. Ruflet normalizes named colors before sending
28
+ them to the client.
29
+
30
+ Use `ruflet_server` to run a standalone server-driven application. Use
31
+ `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.15" unless const_defined?(:VERSION)
4
+ VERSION = "0.0.17" 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
@@ -151,11 +151,57 @@ 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
+ def self.normalize_property(key, value)
162
+ normalize_value(value, color_key?(key))
163
+ end
164
+
165
+ def self.color_key?(key)
166
+ name = key.to_s
167
+ name == "color" ||
168
+ name == "colors" ||
169
+ name.end_with?("bgcolor") ||
170
+ name.end_with?("_color") ||
171
+ name.end_with?("_colors") ||
172
+ name == "color_scheme_seed"
173
+ end
174
+
175
+ def self.normalize_value(value, color_context = false)
176
+ case value
177
+ when String
178
+ color_context ? canonicalize(value) : value
179
+ when Symbol
180
+ color_context ? canonicalize(value.to_s) : value
181
+ when Array
182
+ value.map { |nested| normalize_value(nested, color_context) }
183
+ when Hash
184
+ value.each_with_object({}) do |(nested_key, nested_value), result|
185
+ result[nested_key] = normalize_value(nested_value, color_context || color_key?(nested_key))
186
+ end
187
+ else
188
+ value
189
+ end
190
+ end
191
+
192
+ # Canonicalizes a named color into flet's wire format. Flet color names are
193
+ # lowercase with no separators ("bluegrey", "deeporange", "red500"), so we
194
+ # strip underscores, hyphens, and whitespace before downcasing. Hex values
195
+ # (#... / 0x...) are downcased without separator stripping; the optional
196
+ # ",opacity" suffix is preserved untouched.
197
+ def self.canonicalize(value)
198
+ return value unless value.is_a?(String)
199
+
200
+ color, separator, opacity = value.partition(",")
201
+ color = color.strip.downcase
202
+ color = color.gsub(/[_\-\s]+/, "") unless color.start_with?("#") || color.start_with?("0x")
203
+
204
+ "#{color}#{separator}#{opacity}"
159
205
  end
160
206
 
161
207
  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"
@@ -197,14 +198,11 @@ module Ruflet
197
198
  end
198
199
 
199
200
  def normalize_color_prop(key, value)
200
- return value unless value.is_a?(String)
201
- return value.downcase if color_prop_key?(key)
202
-
203
- value
201
+ Ruflet::Colors.normalize_property(key, value)
204
202
  end
205
203
 
206
204
  def color_prop_key?(key)
207
- key == "color" || key.end_with?("bgcolor") || key.end_with?("_color")
205
+ Ruflet::Colors.color_key?(key)
208
206
  end
209
207
 
210
208
  def normalize_icon_prop(key, value)
@@ -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)
@@ -183,6 +183,7 @@ module Ruflet
183
183
  id: "_dialogs",
184
184
  controls: []
185
185
  )
186
+ @mounted_services = []
186
187
  @invoke_waiters = {}
187
188
  @invoke_callbacks = {}
188
189
  @invoke_waiters_mutex = Mutex.new
@@ -244,6 +245,42 @@ module Ruflet
244
245
  @view_props["bgcolor"] = normalize_value("bgcolor", value)
245
246
  end
246
247
 
248
+ # Client-reported page properties. The Flutter client sends these in its
249
+ # register payload (see Protocol.normalize_register_payload), where they are
250
+ # stored in @client_details; expose them as readers so apps can do
251
+ # `page.width`, `page.platform`, etc. without reaching into client_details.
252
+ def width
253
+ client_reported_prop("width")
254
+ end
255
+
256
+ def height
257
+ client_reported_prop("height")
258
+ end
259
+
260
+ def platform
261
+ client_reported_prop("platform")
262
+ end
263
+
264
+ def platform_brightness
265
+ client_reported_prop("platform_brightness")
266
+ end
267
+
268
+ def web
269
+ client_reported_prop("web")
270
+ end
271
+
272
+ def pwa
273
+ client_reported_prop("pwa")
274
+ end
275
+
276
+ def wasm
277
+ client_reported_prop("wasm")
278
+ end
279
+
280
+ def media
281
+ client_reported_prop("media")
282
+ end
283
+
247
284
  def add(*controls, appbar: nil, bottom_appbar: nil, floating_action_button: nil, navigation_bar: nil, dialog: nil, snack_bar: nil, bottom_sheet: nil)
248
285
  controls = controls.flatten
249
286
  visited = Set.new
@@ -412,6 +449,10 @@ module Ruflet
412
449
  @page_event_handlers["view_pop"] = handler
413
450
  end
414
451
 
452
+ def on_resize=(handler)
453
+ @page_event_handlers["resize"] = handler
454
+ end
455
+
415
456
  def on(event_name, &block)
416
457
  @page_event_handlers[event_name.to_s.sub(/\Aon_/, "")] = block
417
458
  self
@@ -500,6 +541,12 @@ module Ruflet
500
541
  def bottom_sheet=(value)
501
542
  @bottom_sheet = value
502
543
  refresh_dialogs_container!
544
+ # Mirror dialog= / snack_bar=: once the dialogs container is mounted, push
545
+ # the change as an in-place patch. Without this, opening a bottom sheet
546
+ # after the container is already mounted (e.g. a prior dialog/snackbar)
547
+ # never reaches the client, since send_view_patch skips _dialogs once
548
+ # mounted. This is what made NativeApp's modal: sheets fail to open.
549
+ push_dialogs_update! if @dialogs_container_mounted
503
550
  end
504
551
 
505
552
  def bottomsheet=(value)
@@ -1239,6 +1286,12 @@ module Ruflet
1239
1286
  # view — the picker that "reappears after going home".
1240
1287
  dismiss_tracked_dialogs! if route_from_event && route_from_event != @page_props["route"]
1241
1288
  @page_props["route"] = route_from_event if route_from_event
1289
+ elsif name.to_s == "resize"
1290
+ # The client reports the live page size via the "resize" event. Store
1291
+ # it so `page.width`/`page.height` reflect the real viewport — without
1292
+ # this, responsive layouts collapse on clients (e.g. embedded/iOS)
1293
+ # that don't know their size at the initial handshake.
1294
+ store_reported_page_size(data)
1242
1295
  end
1243
1296
  dispatch_page_event(name: name, data: data)
1244
1297
  return
@@ -1301,6 +1354,12 @@ module Ruflet
1301
1354
 
1302
1355
  private
1303
1356
 
1357
+ def client_reported_prop(name)
1358
+ return @page_props[name] if @page_props.key?(name)
1359
+
1360
+ @client_details[name]
1361
+ end
1362
+
1304
1363
  def embedded_async_timeout_available?
1305
1364
  !Object.const_defined?(:RUFLET_EMBEDDED_FAKE_THREAD)
1306
1365
  end
@@ -1515,6 +1574,7 @@ module Ruflet
1515
1574
  raise ArgumentError, "page #{key} must use an icon name string, not #{value.inspect}"
1516
1575
  end
1517
1576
 
1577
+ value = Ruflet::Colors.normalize_property(key, value)
1518
1578
  return value.value if value.is_a?(Ruflet::IconData)
1519
1579
  value.is_a?(Symbol) ? value.to_s : value
1520
1580
  end
@@ -1540,8 +1600,14 @@ module Ruflet
1540
1600
  query_string = route_value.to_s.split("?", 2)[1].to_s
1541
1601
  return {} if query_string.empty?
1542
1602
 
1543
- CGI.parse(query_string).each_with_object({}) do |(key, values), result|
1544
- result[key] = values.size == 1 ? values.first : values
1603
+ query_string.split("&").each_with_object({}) do |pair, result|
1604
+ next if pair.empty?
1605
+
1606
+ key, value = pair.split("=", 2)
1607
+ key = CGI.unescape(key.to_s.tr("+", " "))
1608
+ value = CGI.unescape(value.to_s.tr("+", " "))
1609
+ previous = result[key]
1610
+ result[key] = previous.nil? ? value : Array(previous) << value
1545
1611
  end
1546
1612
  end
1547
1613
 
@@ -1556,6 +1622,15 @@ module Ruflet
1556
1622
  end
1557
1623
  end
1558
1624
 
1625
+ def store_reported_page_size(data)
1626
+ return unless data.is_a?(Hash)
1627
+
1628
+ width = data["width"] || data[:width]
1629
+ height = data["height"] || data[:height]
1630
+ @page_props["width"] = width unless width.nil?
1631
+ @page_props["height"] = height unless height.nil?
1632
+ end
1633
+
1559
1634
  def dispatch_page_event(name:, data:)
1560
1635
  handler = @page_event_handlers[name.to_s.sub(/\Aon_/, "")]
1561
1636
  return unless handler.respond_to?(:call)
@@ -1640,14 +1715,61 @@ module Ruflet
1640
1715
  def push_services_update!
1641
1716
  refresh_control_indexes!
1642
1717
 
1643
- if @services_container.wire_id
1718
+ unless @services_container.wire_id
1719
+ # First mount: the whole list ships inside the page/view patch.
1720
+ send_view_patch
1721
+ @mounted_services = Array(@services_container.props["_services"]).dup
1722
+ return
1723
+ end
1724
+
1725
+ current = Array(@services_container.props["_services"])
1726
+ ops = services_patch_ops(@mounted_services, current)
1727
+ unless ops.empty?
1728
+ # Incrementally add/remove individual services. Re-sending the whole
1729
+ # `_services` list as full objects (a single replace op) makes the
1730
+ # client run Control.fromMap for EVERY service, replacing each Control
1731
+ # instance while ServiceRegistry only binds ids it hasn't seen — so an
1732
+ # already-mounted service (share, clipboard, haptic) keeps its old
1733
+ # binding while controlsIndex points at the fresh, listener-less
1734
+ # instance, and its next invoke hangs. That was the "after Copy, Share
1735
+ # stops working" bug. Add/remove ops leave untouched services intact.
1644
1736
  send_message(Protocol::ACTIONS[:patch_control], {
1645
1737
  "id" => @services_container.wire_id,
1646
- "patch" => [[0], [0, 0, "_services", serialize_patch_value(@services_container.props["_services"])]]
1738
+ "patch" => [[0, { "_services" => [1] }], *ops]
1647
1739
  })
1648
- else
1649
- send_view_patch
1650
1740
  end
1741
+ @mounted_services = current.dup
1742
+ end
1743
+
1744
+ # Diff the previously-mounted services list against the current one and emit
1745
+ # add (1) / remove (2) ops against the `_services` list (patch node 1).
1746
+ # Services are normally append-only; a moved entry is expressed as
1747
+ # remove + add. Existing entries are never re-serialized, so their client
1748
+ # Control instances (and invoke listeners) survive.
1749
+ def services_patch_ops(previous, current)
1750
+ ops = []
1751
+ working = previous.dup
1752
+
1753
+ (working.length - 1).downto(0) do |index|
1754
+ next if current.any? { |service| service.equal?(working[index]) }
1755
+
1756
+ ops << [2, 1, index]
1757
+ working.delete_at(index)
1758
+ end
1759
+
1760
+ current.each_with_index do |service, index|
1761
+ next if working[index]&.equal?(service)
1762
+
1763
+ existing = working.index { |candidate| candidate.equal?(service) }
1764
+ if existing
1765
+ ops << [2, 1, existing]
1766
+ working.delete_at(existing)
1767
+ end
1768
+ ops << [1, 1, index, serialize_patch_value(service)]
1769
+ working.insert(index, service)
1770
+ end
1771
+
1772
+ ops
1651
1773
  end
1652
1774
 
1653
1775
  def push_dialogs_update!
@@ -1780,6 +1902,7 @@ module Ruflet
1780
1902
  existing = service_by_type(type)
1781
1903
  return [existing, false] if existing
1782
1904
 
1905
+ # `service` already syncs via add_service -> push_services_update!.
1783
1906
  [service(type), true]
1784
1907
  end
1785
1908
 
@@ -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
@@ -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
@@ -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)
@@ -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)
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.15
4
+ version: 0.0.17
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:
@@ -249,7 +253,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
249
253
  - !ruby/object:Gem::Version
250
254
  version: '0'
251
255
  requirements: []
252
- rubygems_version: 3.7.2
256
+ rubygems_version: 4.0.11
253
257
  specification_version: 4
254
258
  summary: Ruflet core runtime package.
255
259
  test_files: []