ruflet 0.0.1

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.
Files changed (71) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +3 -0
  3. data/lib/ruflet/version.rb +5 -0
  4. data/lib/ruflet.rb +40 -0
  5. data/lib/ruflet_protocol/ruflet/protocol.rb +62 -0
  6. data/lib/ruflet_protocol.rb +4 -0
  7. data/lib/ruflet_ui/ruflet/app.rb +20 -0
  8. data/lib/ruflet_ui/ruflet/colors.rb +200 -0
  9. data/lib/ruflet_ui/ruflet/control.rb +154 -0
  10. data/lib/ruflet_ui/ruflet/dsl.rb +182 -0
  11. data/lib/ruflet_ui/ruflet/event.rb +28 -0
  12. data/lib/ruflet_ui/ruflet/icon_data.rb +57 -0
  13. data/lib/ruflet_ui/ruflet/icons/cupertino/cupertino_icons.rb +54 -0
  14. data/lib/ruflet_ui/ruflet/icons/cupertino_icon_lookup.rb +112 -0
  15. data/lib/ruflet_ui/ruflet/icons/material_icon_lookup.rb +112 -0
  16. data/lib/ruflet_ui/ruflet/icons/material_icons.rb +55 -0
  17. data/lib/ruflet_ui/ruflet/page.rb +550 -0
  18. data/lib/ruflet_ui/ruflet/ui/control_factory.rb +22 -0
  19. data/lib/ruflet_ui/ruflet/ui/control_methods.rb +16 -0
  20. data/lib/ruflet_ui/ruflet/ui/control_registry.rb +13 -0
  21. data/lib/ruflet_ui/ruflet/ui/controls/cupertino/cupertino_action_sheet_control.rb +13 -0
  22. data/lib/ruflet_ui/ruflet/ui/controls/cupertino/cupertino_alert_dialog_control.rb +13 -0
  23. data/lib/ruflet_ui/ruflet/ui/controls/cupertino/cupertino_button_control.rb +13 -0
  24. data/lib/ruflet_ui/ruflet/ui/controls/cupertino/cupertino_dialog_action_control.rb +13 -0
  25. data/lib/ruflet_ui/ruflet/ui/controls/cupertino/cupertino_filled_button_control.rb +13 -0
  26. data/lib/ruflet_ui/ruflet/ui/controls/cupertino/cupertino_navigation_bar_control.rb +13 -0
  27. data/lib/ruflet_ui/ruflet/ui/controls/cupertino/cupertino_slider_control.rb +13 -0
  28. data/lib/ruflet_ui/ruflet/ui/controls/cupertino/cupertino_switch_control.rb +13 -0
  29. data/lib/ruflet_ui/ruflet/ui/controls/cupertino/cupertino_text_field_control.rb +13 -0
  30. data/lib/ruflet_ui/ruflet/ui/controls/material/alert_dialog_control.rb +13 -0
  31. data/lib/ruflet_ui/ruflet/ui/controls/material/app_bar_control.rb +13 -0
  32. data/lib/ruflet_ui/ruflet/ui/controls/material/bottom_sheet_control.rb +13 -0
  33. data/lib/ruflet_ui/ruflet/ui/controls/material/button_control.rb +24 -0
  34. data/lib/ruflet_ui/ruflet/ui/controls/material/checkbox_control.rb +13 -0
  35. data/lib/ruflet_ui/ruflet/ui/controls/material/column_control.rb +13 -0
  36. data/lib/ruflet_ui/ruflet/ui/controls/material/container_control.rb +13 -0
  37. data/lib/ruflet_ui/ruflet/ui/controls/material/drag_target_control.rb +13 -0
  38. data/lib/ruflet_ui/ruflet/ui/controls/material/draggable_control.rb +13 -0
  39. data/lib/ruflet_ui/ruflet/ui/controls/material/elevated_button_control.rb +13 -0
  40. data/lib/ruflet_ui/ruflet/ui/controls/material/filled_button_control.rb +13 -0
  41. data/lib/ruflet_ui/ruflet/ui/controls/material/floating_action_button_control.rb +28 -0
  42. data/lib/ruflet_ui/ruflet/ui/controls/material/gesture_detector_control.rb +13 -0
  43. data/lib/ruflet_ui/ruflet/ui/controls/material/icon_button_control.rb +13 -0
  44. data/lib/ruflet_ui/ruflet/ui/controls/material/icon_control.rb +24 -0
  45. data/lib/ruflet_ui/ruflet/ui/controls/material/image_control.rb +13 -0
  46. data/lib/ruflet_ui/ruflet/ui/controls/material/markdown_control.rb +13 -0
  47. data/lib/ruflet_ui/ruflet/ui/controls/material/navigation_bar_control.rb +13 -0
  48. data/lib/ruflet_ui/ruflet/ui/controls/material/navigation_bar_destination_control.rb +13 -0
  49. data/lib/ruflet_ui/ruflet/ui/controls/material/radio_control.rb +13 -0
  50. data/lib/ruflet_ui/ruflet/ui/controls/material/radio_group_control.rb +13 -0
  51. data/lib/ruflet_ui/ruflet/ui/controls/material/row_control.rb +13 -0
  52. data/lib/ruflet_ui/ruflet/ui/controls/material/snack_bar_control.rb +13 -0
  53. data/lib/ruflet_ui/ruflet/ui/controls/material/stack_control.rb +13 -0
  54. data/lib/ruflet_ui/ruflet/ui/controls/material/tab_bar_control.rb +13 -0
  55. data/lib/ruflet_ui/ruflet/ui/controls/material/tab_bar_view_control.rb +13 -0
  56. data/lib/ruflet_ui/ruflet/ui/controls/material/tab_control.rb +13 -0
  57. data/lib/ruflet_ui/ruflet/ui/controls/material/tabs_control.rb +63 -0
  58. data/lib/ruflet_ui/ruflet/ui/controls/material/text_button_control.rb +13 -0
  59. data/lib/ruflet_ui/ruflet/ui/controls/material/text_control.rb +13 -0
  60. data/lib/ruflet_ui/ruflet/ui/controls/material/text_field_control.rb +13 -0
  61. data/lib/ruflet_ui/ruflet/ui/controls/material/view_control.rb +13 -0
  62. data/lib/ruflet_ui/ruflet/ui/cupertino_control_factory.rb +40 -0
  63. data/lib/ruflet_ui/ruflet/ui/cupertino_control_methods.rb +26 -0
  64. data/lib/ruflet_ui/ruflet/ui/cupertino_control_registry.rb +35 -0
  65. data/lib/ruflet_ui/ruflet/ui/material_control_factory.rb +94 -0
  66. data/lib/ruflet_ui/ruflet/ui/material_control_methods.rb +116 -0
  67. data/lib/ruflet_ui/ruflet/ui/material_control_registry.rb +88 -0
  68. data/lib/ruflet_ui/ruflet/ui/shared_control_forwarders.rb +85 -0
  69. data/lib/ruflet_ui/ruflet/ui/widget_builder.rb +48 -0
  70. data/lib/ruflet_ui.rb +112 -0
  71. metadata +109 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: a46d9a04d401502b437f5f1f4bd16825d70fd83a0742f2222be7b3f44779806e
4
+ data.tar.gz: 6cd952e7b2cfe71fb900a9e6bfcab4459e0f5a573013ea45ef61c5fc5557374d
5
+ SHA512:
6
+ metadata.gz: b2357c3392cc63a15cc83efd9cab123bccc80eba1de9a7363e969b608268fc24a1741dae5f90ea1a2a15db6452c17a7ba5d97a2e45457e82f97222e221c62892
7
+ data.tar.gz: cdeea6994f53e37c3a622f9b0997a9b8f5bed7c4b49af60cfd0bfe0b37e9e1ea569bc242cdc95cadbc908b81aa0315e27eb957f34cfb00ea5f18846f2f77e57f
data/README.md ADDED
@@ -0,0 +1,3 @@
1
+ # ruflet
2
+
3
+ Part of Ruflet monorepo.
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ruflet
4
+ VERSION = "0.0.1" unless const_defined?(:VERSION)
5
+ end
data/lib/ruflet.rb ADDED
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "thread"
4
+ require "ruflet_protocol"
5
+ require "ruflet_ui"
6
+
7
+ module Ruflet
8
+ @run_interceptors = []
9
+ @run_interceptors_mutex = Mutex.new
10
+
11
+ module_function
12
+
13
+ def run(entrypoint = nil, host: "0.0.0.0", port: 8550, &block)
14
+ begin
15
+ require "ruflet_server"
16
+ rescue LoadError => e
17
+ raise LoadError, "Ruflet.run requires the 'ruflet_server' gem. Add it to your Gemfile.", e.backtrace
18
+ end
19
+
20
+ callback = entrypoint || block
21
+ raise ArgumentError, "Ruflet.run requires a callable entrypoint or block" unless callback.respond_to?(:call)
22
+
23
+ interceptor = @run_interceptors_mutex.synchronize { @run_interceptors.last }
24
+ if interceptor
25
+ result = interceptor.call(entrypoint: callback, host: host, port: port)
26
+ return result unless result == :pass
27
+ end
28
+
29
+ Server.new(host: host, port: port) do |page|
30
+ callback.call(page)
31
+ end.start
32
+ end
33
+
34
+ def with_run_interceptor(interceptor)
35
+ @run_interceptors_mutex.synchronize { @run_interceptors << interceptor }
36
+ yield
37
+ ensure
38
+ @run_interceptors_mutex.synchronize { @run_interceptors.delete(interceptor) }
39
+ end
40
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ruflet
4
+ module Protocol
5
+ ACTIONS = {
6
+ register_client: 1,
7
+ patch_control: 2,
8
+ control_event: 3,
9
+ update_control: 4,
10
+ invoke_control_method: 5,
11
+ session_crashed: 6,
12
+
13
+ # Legacy JSON protocol aliases kept for compatibility.
14
+ register_web_client: "registerWebClient",
15
+ page_event_from_web: "pageEventFromWeb",
16
+ update_control_props: "updateControlProps"
17
+ }.freeze
18
+
19
+ module_function
20
+
21
+ def pack_message(action:, payload:)
22
+ [action, payload]
23
+ end
24
+
25
+ def normalize_register_payload(payload)
26
+ page = payload["page"] || {}
27
+ {
28
+ "session_id" => payload["session_id"],
29
+ "page_name" => payload["page_name"] || "",
30
+ "route" => page["route"] || "/",
31
+ "width" => page["width"],
32
+ "height" => page["height"],
33
+ "platform" => page["platform"],
34
+ "platform_brightness" => page["platform_brightness"],
35
+ "media" => page["media"] || {}
36
+ }
37
+ end
38
+
39
+ def normalize_control_event_payload(payload)
40
+ {
41
+ "target" => payload["target"] || payload["eventTarget"],
42
+ "name" => payload["name"] || payload["eventName"],
43
+ "data" => payload["data"] || payload["eventData"]
44
+ }
45
+ end
46
+
47
+ def normalize_update_control_payload(payload)
48
+ {
49
+ "id" => payload["id"] || payload["target"] || payload["eventTarget"],
50
+ "props" => payload["props"].is_a?(Hash) ? payload["props"] : {}
51
+ }
52
+ end
53
+
54
+ def register_response(session_id:)
55
+ {
56
+ "session_id" => session_id,
57
+ "page_patch" => {},
58
+ "error" => nil
59
+ }
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "ruflet/version"
4
+ require_relative "ruflet_protocol/ruflet/protocol"
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ruflet
4
+ class App
5
+ def initialize(host: "0.0.0.0", port: 8550)
6
+ @host = host
7
+ @port = port
8
+ end
9
+
10
+ def run
11
+ Ruflet.run(host: @host, port: @port) do |page|
12
+ view(page)
13
+ end
14
+ end
15
+
16
+ def view(_page)
17
+ raise NotImplementedError, "#{self.class} must implement #view(page)"
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,200 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ruflet
4
+ module Colors
5
+ module_function
6
+
7
+ SEMANTIC_COLORS = {
8
+ PRIMARY: "primary",
9
+ ON_PRIMARY: "onprimary",
10
+ PRIMARY_CONTAINER: "primarycontainer",
11
+ ON_PRIMARY_CONTAINER: "onprimarycontainer",
12
+ PRIMARY_FIXED: "primaryfixed",
13
+ PRIMARY_FIXED_DIM: "primaryfixeddim",
14
+ ON_PRIMARY_FIXED: "onprimaryfixed",
15
+ ON_PRIMARY_FIXED_VARIANT: "onprimaryfixedvariant",
16
+ SECONDARY: "secondary",
17
+ ON_SECONDARY: "onsecondary",
18
+ SECONDARY_CONTAINER: "secondarycontainer",
19
+ ON_SECONDARY_CONTAINER: "onsecondarycontainer",
20
+ SECONDARY_FIXED: "secondaryfixed",
21
+ SECONDARY_FIXED_DIM: "secondaryfixeddim",
22
+ ON_SECONDARY_FIXED: "onsecondaryfixed",
23
+ ON_SECONDARY_FIXED_VARIANT: "onsecondaryfixedvariant",
24
+ TERTIARY: "tertiary",
25
+ ON_TERTIARY: "ontertiary",
26
+ TERTIARY_CONTAINER: "tertiarycontainer",
27
+ ON_TERTIARY_CONTAINER: "ontertiarycontainer",
28
+ TERTIARY_FIXED: "tertiaryfixed",
29
+ TERTIARY_FIXED_DIM: "tertiaryfixeddim",
30
+ ON_TERTIARY_FIXED: "ontertiaryfixed",
31
+ ON_TERTIARY_FIXED_VARIANT: "ontertiaryfixedvariant",
32
+ ERROR: "error",
33
+ ON_ERROR: "onerror",
34
+ ERROR_CONTAINER: "errorcontainer",
35
+ ON_ERROR_CONTAINER: "onerrorcontainer",
36
+ SURFACE: "surface",
37
+ ON_SURFACE: "onsurface",
38
+ ON_SURFACE_VARIANT: "onsurfacevariant",
39
+ SURFACE_TINT: "surfacetint",
40
+ SURFACE_DIM: "surfacedim",
41
+ SURFACE_BRIGHT: "surfacebright",
42
+ SURFACE_CONTAINER: "surfacecontainer",
43
+ SURFACE_CONTAINER_LOW: "surfacecontainerlow",
44
+ SURFACE_CONTAINER_LOWEST: "surfacecontainerlowest",
45
+ SURFACE_CONTAINER_HIGH: "surfacecontainerhigh",
46
+ SURFACE_CONTAINER_HIGHEST: "surfacecontainerhighest",
47
+ OUTLINE: "outline",
48
+ OUTLINE_VARIANT: "outlinevariant",
49
+ SHADOW: "shadow",
50
+ SCRIM: "scrim",
51
+ INVERSE_SURFACE: "inversesurface",
52
+ ON_INVERSE_SURFACE: "oninversesurface",
53
+ INVERSE_PRIMARY: "inverseprimary"
54
+ }.freeze
55
+
56
+ BASE_PRIMARY = %w[
57
+ amber blue bluegrey brown cyan deeporange deeppurple green grey indigo lightblue
58
+ lightgreen lime orange pink purple red teal yellow
59
+ ].freeze
60
+ BASE_ACCENT = %w[
61
+ amber blue cyan deeporange deeppurple green indigo lightblue lightgreen lime
62
+ orange pink purple red teal yellow
63
+ ].freeze
64
+ PRIMARY_SHADES = [50, 100, 200, 300, 400, 500, 600, 700, 800, 900].freeze
65
+ ACCENT_SHADES = [100, 200, 400, 700].freeze
66
+
67
+ FIXED_COLORS = {
68
+ BLACK: "black",
69
+ BLACK_12: "black12",
70
+ BLACK_26: "black26",
71
+ BLACK_38: "black38",
72
+ BLACK_45: "black45",
73
+ BLACK_54: "black54",
74
+ BLACK_87: "black87",
75
+ WHITE: "white",
76
+ WHITE_10: "white10",
77
+ WHITE_12: "white12",
78
+ WHITE_24: "white24",
79
+ WHITE_30: "white30",
80
+ WHITE_38: "white38",
81
+ WHITE_54: "white54",
82
+ WHITE_60: "white60",
83
+ WHITE_70: "white70",
84
+ TRANSPARENT: "transparent"
85
+ }.freeze
86
+
87
+ DEPRECATED_ALIASES = {
88
+ BLACK12: :BLACK_12,
89
+ BLACK26: :BLACK_26,
90
+ BLACK38: :BLACK_38,
91
+ BLACK45: :BLACK_45,
92
+ BLACK54: :BLACK_54,
93
+ BLACK87: :BLACK_87,
94
+ WHITE10: :WHITE_10,
95
+ WHITE12: :WHITE_12,
96
+ WHITE24: :WHITE_24,
97
+ WHITE30: :WHITE_30,
98
+ WHITE38: :WHITE_38,
99
+ WHITE54: :WHITE_54,
100
+ WHITE60: :WHITE_60,
101
+ WHITE70: :WHITE_70
102
+ }.freeze
103
+
104
+ def with_opacity(opacity, color)
105
+ value = Float(opacity)
106
+ raise ArgumentError, "opacity must be between 0.0 and 1.0 inclusive, got #{opacity}" unless value.between?(0.0, 1.0)
107
+
108
+ "#{normalize_color(color)},#{value}"
109
+ end
110
+
111
+ def random(exclude: nil, weights: nil)
112
+ choices = all_values.dup
113
+ excluded = Array(exclude).map { |c| normalize_color(c) }
114
+ choices.reject! { |c| excluded.include?(c) }
115
+ return nil if choices.empty?
116
+
117
+ if weights && !weights.empty?
118
+ expanded = choices.flat_map do |color|
119
+ weight = weights.fetch(color, weights.fetch(color.to_sym, 1)).to_i rescue 1
120
+ weight = 1 if weight <= 0
121
+ [color] * weight
122
+ end
123
+ return expanded.sample
124
+ end
125
+
126
+ choices.sample
127
+ end
128
+
129
+ def all_values
130
+ @all_values ||= constants(false)
131
+ .map { |c| const_get(c) }
132
+ .select { |v| v.is_a?(String) }
133
+ .uniq
134
+ .freeze
135
+ end
136
+
137
+ def normalize_color(color)
138
+ return color.to_s if color.is_a?(Symbol)
139
+ return color if color.is_a?(String)
140
+ return color.to_s unless color.respond_to?(:to_s)
141
+
142
+ color.to_s
143
+ end
144
+
145
+ BASE_PREFIX = {
146
+ "amber" => "AMBER",
147
+ "blue" => "BLUE",
148
+ "bluegrey" => "BLUE_GREY",
149
+ "brown" => "BROWN",
150
+ "cyan" => "CYAN",
151
+ "deeporange" => "DEEP_ORANGE",
152
+ "deeppurple" => "DEEP_PURPLE",
153
+ "green" => "GREEN",
154
+ "grey" => "GREY",
155
+ "indigo" => "INDIGO",
156
+ "lightblue" => "LIGHT_BLUE",
157
+ "lightgreen" => "LIGHT_GREEN",
158
+ "lime" => "LIME",
159
+ "orange" => "ORANGE",
160
+ "pink" => "PINK",
161
+ "purple" => "PURPLE",
162
+ "red" => "RED",
163
+ "teal" => "TEAL",
164
+ "yellow" => "YELLOW"
165
+ }.freeze
166
+
167
+ def constant_prefix_for(base_name)
168
+ BASE_PREFIX.fetch(base_name) { base_name.upcase }
169
+ end
170
+
171
+ SEMANTIC_COLORS.each { |k, v| const_set(k, v) }
172
+ FIXED_COLORS.each { |k, v| const_set(k, v) }
173
+
174
+ BASE_PRIMARY.each do |base|
175
+ prefix = constant_prefix_for(base)
176
+ const_set(prefix, base)
177
+ PRIMARY_SHADES.each do |shade|
178
+ const_set("#{prefix}_#{shade}", "#{base}#{shade}")
179
+ end
180
+ end
181
+
182
+ BASE_ACCENT.each do |base|
183
+ prefix = "#{constant_prefix_for(base)}_ACCENT"
184
+ const_set(prefix, "#{base}accent")
185
+ ACCENT_SHADES.each do |shade|
186
+ const_set("#{prefix}_#{shade}", "#{base}accent#{shade}")
187
+ end
188
+ end
189
+
190
+ DEPRECATED_ALIASES.each do |alias_name, target|
191
+ const_set(alias_name, const_get(target))
192
+ end
193
+
194
+ constants(false).each do |name|
195
+ next if respond_to?(name)
196
+
197
+ define_singleton_method(name) { const_get(name) }
198
+ end
199
+ end
200
+ end
@@ -0,0 +1,154 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "securerandom"
4
+ require_relative "ui/control_registry"
5
+ require_relative "icon_data"
6
+ require_relative "icons/material_icon_lookup"
7
+ require_relative "icons/cupertino_icon_lookup"
8
+
9
+ module Ruflet
10
+ class Control
11
+ TYPE_MAP = UI::ControlRegistry::TYPE_MAP
12
+ EVENT_PROPS = UI::ControlRegistry::EVENT_PROPS
13
+ HOST_EXPANDED_TYPES = %w[view row column].freeze
14
+
15
+ attr_reader :type, :id, :props, :children
16
+ attr_accessor :wire_id, :runtime_page
17
+
18
+ def initialize(type:, id: nil, **props)
19
+ @type = type.to_s.downcase
20
+ @id = (id || props.delete(:id) || "ctrl_#{SecureRandom.hex(4)}").to_s
21
+ @children = []
22
+ @handlers = {}
23
+ @wire_id = nil
24
+ @props = normalize_props(extract_handlers(preprocess_props(props)))
25
+ end
26
+
27
+ def on(event_name, &block)
28
+ name = normalized_event_name(event_name)
29
+ @handlers[name] = block
30
+ @props["on_#{name}"] = true
31
+ runtime_page&.update(self, "on_#{name}": true) if wire_id
32
+ self
33
+ end
34
+
35
+ def emit(event_name, event)
36
+ handler = @handlers[normalized_event_name(event_name)]
37
+ return false unless handler
38
+
39
+ handler.call(event)
40
+ true
41
+ end
42
+
43
+ def has_handler?(event_name)
44
+ @handlers.key?(normalized_event_name(event_name))
45
+ end
46
+
47
+ def to_patch
48
+ patch = {
49
+ "_c" => TYPE_MAP.fetch(type, type.split("_").map(&:capitalize).join),
50
+ "_i" => wire_id
51
+ }
52
+
53
+ internals = {}
54
+ internals["host_positioned"] = true if type == "stack"
55
+ internals["host_expanded"] = true if HOST_EXPANDED_TYPES.include?(type)
56
+ patch["_internals"] = internals unless internals.empty?
57
+
58
+ props.each { |k, v| patch[k] = serialize_value(v) }
59
+ patch["controls"] = children.map(&:to_patch) unless children.empty?
60
+ patch
61
+ end
62
+
63
+ private
64
+
65
+ def serialize_value(value)
66
+ case value
67
+ when Control
68
+ value.to_patch
69
+ when Ruflet::IconData
70
+ value.value
71
+ when Array
72
+ value.map { |v| serialize_value(v) }
73
+ when Hash
74
+ value.transform_values { |v| serialize_value(v) }
75
+ else
76
+ value
77
+ end
78
+ end
79
+
80
+ def extract_handlers(input)
81
+ output = input.dup
82
+
83
+ EVENT_PROPS.each do |prop, event_name|
84
+ string_prop = prop.to_s
85
+ next unless output.key?(prop) || output.key?(string_prop)
86
+
87
+ handler = output.key?(prop) ? output.delete(prop) : output.delete(string_prop)
88
+ @handlers[event_name] = handler if handler.respond_to?(:call)
89
+ output["on_#{event_name}"] = true
90
+ end
91
+
92
+ output
93
+ end
94
+
95
+ def normalize_props(hash)
96
+ hash.each_with_object({}) do |(k, v), result|
97
+ key = k.to_s
98
+ mapped_key = key
99
+ value =
100
+ if v.is_a?(Symbol)
101
+ v.to_s
102
+ elsif v.is_a?(Ruflet::IconData)
103
+ v.value
104
+ else
105
+ v
106
+ end
107
+ value = normalize_icon_prop(mapped_key, value)
108
+ value = normalize_color_prop(mapped_key, value)
109
+
110
+ result[mapped_key] = value
111
+ end
112
+ end
113
+
114
+ def preprocess_props(props)
115
+ props
116
+ end
117
+
118
+ def normalize_color_prop(key, value)
119
+ return value unless value.is_a?(String)
120
+ return value.downcase if color_prop_key?(key)
121
+
122
+ value
123
+ end
124
+
125
+ def color_prop_key?(key)
126
+ key == "color" || key == "bgcolor" || key.end_with?("_color")
127
+ end
128
+
129
+ def normalize_icon_prop(key, value)
130
+ return value unless icon_prop_key?(key)
131
+ codepoint = resolve_icon_codepoint(value)
132
+ codepoint.nil? ? value : codepoint
133
+ end
134
+
135
+ def icon_prop_key?(key)
136
+ key == "icon" || key.end_with?("_icon")
137
+ end
138
+
139
+ def resolve_icon_codepoint(value)
140
+ return nil unless value.is_a?(Integer) || value.is_a?(Symbol) || value.is_a?(String)
141
+
142
+ codepoint = Ruflet::MaterialIconLookup.codepoint_for(value)
143
+ if codepoint.nil? || (value.is_a?(Integer) && codepoint == value)
144
+ cupertino = Ruflet::CupertinoIconLookup.codepoint_for(value)
145
+ codepoint = cupertino unless cupertino.nil?
146
+ end
147
+ codepoint
148
+ end
149
+
150
+ def normalized_event_name(event_name)
151
+ event_name.to_s.sub(/\Aon_/, "")
152
+ end
153
+ end
154
+ end
@@ -0,0 +1,182 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "ui/control_methods"
4
+ require_relative "ui/control_factory"
5
+
6
+ module Ruflet
7
+ module DSL
8
+ module_function
9
+
10
+ def _pending_app
11
+ @_pending_app ||= App.new(host: "0.0.0.0", port: 8550)
12
+ end
13
+
14
+ def _reset_pending_app!
15
+ @_pending_app = App.new(host: "0.0.0.0", port: 8550)
16
+ end
17
+
18
+ def app(host: "0.0.0.0", port: 8550, &block)
19
+ return App.new(host: host, port: port).tap { |a| a.instance_eval(&block) } if block
20
+
21
+ pending = _pending_app
22
+ pending.set_endpoint!(host: host, port: port)
23
+ _reset_pending_app!
24
+ pending
25
+ end
26
+
27
+ def page(**props, &block) = _pending_app.page(**props, &block)
28
+ def control(type, **props, &block) = _pending_app.control(type, **props, &block)
29
+ def widget(type, **props, &block) = _pending_app.widget(type, **props, &block)
30
+ def column(**props, &block) = _pending_app.column(**props, &block)
31
+ def center(**props, &block) = _pending_app.center(**props, &block)
32
+ def row(**props, &block) = _pending_app.row(**props, &block)
33
+ def stack(**props, &block) = _pending_app.stack(**props, &block)
34
+ def container(**props, &block) = _pending_app.container(**props, &block)
35
+ def gesture_detector(**props, &block) = _pending_app.gesture_detector(**props, &block)
36
+ def gesturedetector(**props, &block) = _pending_app.gesturedetector(**props, &block)
37
+ def draggable(**props, &block) = _pending_app.draggable(**props, &block)
38
+ def drag_target(**props, &block) = _pending_app.drag_target(**props, &block)
39
+ def dragtarget(**props, &block) = _pending_app.dragtarget(**props, &block)
40
+ def text(value = nil, **props) = _pending_app.text(value, **props)
41
+ def button(**props) = _pending_app.button(**props)
42
+ def elevated_button(**props) = _pending_app.elevated_button(**props)
43
+ def elevatedbutton(**props) = _pending_app.elevatedbutton(**props)
44
+ def text_field(**props) = _pending_app.text_field(**props)
45
+ def textfield(**props) = _pending_app.textfield(**props)
46
+ def icon(**props) = _pending_app.icon(**props)
47
+ def image(src = nil, **props) = _pending_app.image(src, **props)
48
+ def icon_button(**props) = _pending_app.icon_button(**props)
49
+ def iconbutton(**props) = _pending_app.iconbutton(**props)
50
+ def app_bar(**props) = _pending_app.app_bar(**props)
51
+ def appbar(**props) = _pending_app.appbar(**props)
52
+ def text_button(**props) = _pending_app.text_button(**props)
53
+ def textbutton(**props) = _pending_app.textbutton(**props)
54
+ def filled_button(**props) = _pending_app.filled_button(**props)
55
+ def filledbutton(**props) = _pending_app.filledbutton(**props)
56
+ def checkbox(**props) = _pending_app.checkbox(**props)
57
+ def radio(**props) = _pending_app.radio(**props)
58
+ def radio_group(**props) = _pending_app.radio_group(**props)
59
+ def radiogroup(**props) = _pending_app.radiogroup(**props)
60
+ def alert_dialog(**props) = _pending_app.alert_dialog(**props)
61
+ def alertdialog(**props) = _pending_app.alertdialog(**props)
62
+ def snack_bar(**props) = _pending_app.snack_bar(**props)
63
+ def snackbar(**props) = _pending_app.snackbar(**props)
64
+ def bottom_sheet(**props) = _pending_app.bottom_sheet(**props)
65
+ def bottomsheet(**props) = _pending_app.bottomsheet(**props)
66
+ def markdown(value = nil, **props) = _pending_app.markdown(value, **props)
67
+ def floating_action_button(**props) = _pending_app.floating_action_button(**props)
68
+ def floatingactionbutton(**props) = _pending_app.floatingactionbutton(**props)
69
+ def tabs(**props, &block) = _pending_app.tabs(**props, &block)
70
+ def tab(**props, &block) = _pending_app.tab(**props, &block)
71
+ def tab_bar(**props, &block) = _pending_app.tab_bar(**props, &block)
72
+ def tabbar(**props, &block) = _pending_app.tabbar(**props, &block)
73
+ def tab_bar_view(**props, &block) = _pending_app.tab_bar_view(**props, &block)
74
+ def tabbarview(**props, &block) = _pending_app.tabbarview(**props, &block)
75
+ def navigation_bar(**props, &block) = _pending_app.navigation_bar(**props, &block)
76
+ def navigationbar(**props, &block) = _pending_app.navigationbar(**props, &block)
77
+ def navigation_bar_destination(**props, &block) = _pending_app.navigation_bar_destination(**props, &block)
78
+ def navigationbardestination(**props, &block) = _pending_app.navigationbardestination(**props, &block)
79
+ def fab(content = nil, **props) = _pending_app.fab(content, **props)
80
+ def cupertino_button(**props) = _pending_app.cupertino_button(**props)
81
+ def cupertinobutton(**props) = _pending_app.cupertinobutton(**props)
82
+ def cupertino_filled_button(**props) = _pending_app.cupertino_filled_button(**props)
83
+ def cupertinofilledbutton(**props) = _pending_app.cupertinofilledbutton(**props)
84
+ def cupertino_text_field(**props) = _pending_app.cupertino_text_field(**props)
85
+ def cupertinotextfield(**props) = _pending_app.cupertinotextfield(**props)
86
+ def cupertino_switch(**props) = _pending_app.cupertino_switch(**props)
87
+ def cupertinoswitch(**props) = _pending_app.cupertinoswitch(**props)
88
+ def cupertino_slider(**props) = _pending_app.cupertino_slider(**props)
89
+ def cupertinoslider(**props) = _pending_app.cupertinoslider(**props)
90
+ def cupertino_alert_dialog(**props) = _pending_app.cupertino_alert_dialog(**props)
91
+ def cupertinoalertdialog(**props) = _pending_app.cupertinoalertdialog(**props)
92
+ def cupertino_action_sheet(**props) = _pending_app.cupertino_action_sheet(**props)
93
+ def cupertinoactionsheet(**props) = _pending_app.cupertinoactionsheet(**props)
94
+ def cupertino_dialog_action(**props) = _pending_app.cupertino_dialog_action(**props)
95
+ def cupertinodialogaction(**props) = _pending_app.cupertinodialogaction(**props)
96
+ def cupertino_navigation_bar(**props) = _pending_app.cupertino_navigation_bar(**props)
97
+ def cupertinonavigationbar(**props) = _pending_app.cupertinonavigationbar(**props)
98
+
99
+ class App
100
+ include UI::ControlMethods
101
+
102
+ attr_reader :page_props, :host, :port
103
+
104
+ def initialize(host:, port:)
105
+ @host = host
106
+ @port = port
107
+ @roots = []
108
+ @stack = []
109
+ @page_props = { "route" => "/" }
110
+ @seq = 0
111
+ end
112
+
113
+ def set_endpoint!(host:, port:)
114
+ @host = host
115
+ @port = port
116
+ end
117
+
118
+ def page(**props, &block)
119
+ @page_props.merge!(normalize_props(props))
120
+ instance_eval(&block) if block
121
+ self
122
+ end
123
+
124
+ def widget(type, **props, &block)
125
+ control(type.to_s, **props, &block)
126
+ end
127
+
128
+ def control(type, **props, &block)
129
+ mapped_props = props.dup
130
+ prop_children = mapped_props.delete(:controls) || mapped_props.delete("controls")
131
+
132
+ id = mapped_props.delete(:id)&.to_s || next_id(type)
133
+ c = Ruflet::UI::ControlFactory.build(type.to_s, id: id, **normalize_props(mapped_props))
134
+ attach(c)
135
+
136
+ if block
137
+ @stack.push(c)
138
+ instance_eval(&block)
139
+ @stack.pop
140
+ end
141
+
142
+ if prop_children
143
+ Array(prop_children).each { |child| c.children << child if child.is_a?(Ruflet::Control) }
144
+ end
145
+
146
+ c
147
+ end
148
+
149
+ def run
150
+ app_roots = @roots
151
+ page_props = @page_props.dup
152
+
153
+ Ruflet::Server.new(host: host, port: port) do |runtime_page|
154
+ runtime_page.set_view_props(page_props)
155
+ runtime_page.add(*app_roots)
156
+ end.start
157
+ end
158
+
159
+ private
160
+
161
+ def build_widget(type, **props, &block) = control(type.to_s, **props, &block)
162
+
163
+ def attach(control)
164
+ if @stack.empty?
165
+ @roots << control
166
+ else
167
+ @stack.last.children << control
168
+ end
169
+ end
170
+
171
+ def normalize_props(hash)
172
+ hash.transform_keys(&:to_s).transform_values { |v| v.is_a?(Symbol) ? v.to_s : v }
173
+ end
174
+
175
+ def next_id(type)
176
+ @seq += 1
177
+ "#{type}_#{@seq}"
178
+ end
179
+
180
+ end
181
+ end
182
+ end