zui 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 (325) hide show
  1. checksums.yaml +7 -0
  2. data/Components/Builtins/Accordion.qml +208 -0
  3. data/Components/Builtins/Action.qml +43 -0
  4. data/Components/Builtins/ActionButton.qml +19 -0
  5. data/Components/Builtins/ActionGroup.qml +81 -0
  6. data/Components/Builtins/AlertDialog.qml +398 -0
  7. data/Components/Builtins/AnchorAnimation.qml +3 -0
  8. data/Components/Builtins/AnchorChanges.qml +11 -0
  9. data/Components/Builtins/AnimatedImage.qml +39 -0
  10. data/Components/Builtins/Animation.qml +3 -0
  11. data/Components/Builtins/AnimationController.qml +11 -0
  12. data/Components/Builtins/ApplicationWindow.qml +103 -0
  13. data/Components/Builtins/AreaChart.qml +70 -0
  14. data/Components/Builtins/AspectRatio.qml +30 -0
  15. data/Components/Builtins/Audio.qml +85 -0
  16. data/Components/Builtins/AudioInput.qml +10 -0
  17. data/Components/Builtins/AudioOutput.qml +10 -0
  18. data/Components/Builtins/Avatar.qml +46 -0
  19. data/Components/Builtins/Badge.qml +38 -0
  20. data/Components/Builtins/Banner.qml +133 -0
  21. data/Components/Builtins/BarChart.qml +67 -0
  22. data/Components/Builtins/BarIconButton.qml +30 -0
  23. data/Components/Builtins/BarIndicator.qml +26 -0
  24. data/Components/Builtins/Behavior.qml +13 -0
  25. data/Components/Builtins/Blur.qml +14 -0
  26. data/Components/Builtins/BorderImage.qml +38 -0
  27. data/Components/Builtins/BorderOverlay.qml +25 -0
  28. data/Components/Builtins/BottomSheet.qml +211 -0
  29. data/Components/Builtins/Breadcrumb.qml +169 -0
  30. data/Components/Builtins/BubbleChart.qml +3 -0
  31. data/Components/Builtins/BusyIndicator.qml +24 -0
  32. data/Components/Builtins/Button.qml +55 -0
  33. data/Components/Builtins/ButtonGroup.qml +18 -0
  34. data/Components/Builtins/Calendar.qml +55 -0
  35. data/Components/Builtins/Camera.qml +18 -0
  36. data/Components/Builtins/CandlestickChart.qml +3 -0
  37. data/Components/Builtins/Canvas.qml +159 -0
  38. data/Components/Builtins/CaptureSession.qml +19 -0
  39. data/Components/Builtins/Card.qml +23 -0
  40. data/Components/Builtins/Carousel.qml +72 -0
  41. data/Components/Builtins/Center.qml +20 -0
  42. data/Components/Builtins/CheckDelegate.qml +132 -0
  43. data/Components/Builtins/Checkbox.qml +62 -0
  44. data/Components/Builtins/Chip.qml +65 -0
  45. data/Components/Builtins/Circle.qml +35 -0
  46. data/Components/Builtins/Clipboard.qml +29 -0
  47. data/Components/Builtins/ColorAnimation.qml +3 -0
  48. data/Components/Builtins/ColorPicker.qml +70 -0
  49. data/Components/Builtins/Colorize.qml +14 -0
  50. data/Components/Builtins/Column.qml +17 -0
  51. data/Components/Builtins/ColumnLayout.qml +13 -0
  52. data/Components/Builtins/ConfirmDialog.qml +20 -0
  53. data/Components/Builtins/ConstrainedBox.qml +28 -0
  54. data/Components/Builtins/Container.qml +30 -0
  55. data/Components/Builtins/ContextMenu.qml +140 -0
  56. data/Components/Builtins/CursorSurface.qml +18 -0
  57. data/Components/Builtins/DataTable.qml +69 -0
  58. data/Components/Builtins/DatePicker.qml +180 -0
  59. data/Components/Builtins/DayOfWeekRow.qml +11 -0
  60. data/Components/Builtins/DelayButton.qml +47 -0
  61. data/Components/Builtins/DelegateModel.qml +10 -0
  62. data/Components/Builtins/DelegateModelGroup.qml +10 -0
  63. data/Components/Builtins/Dial.qml +42 -0
  64. data/Components/Builtins/Dialog.qml +128 -0
  65. data/Components/Builtins/DialogButtonBox.qml +137 -0
  66. data/Components/Builtins/Divider.qml +26 -0
  67. data/Components/Builtins/DonutChart.qml +3 -0
  68. data/Components/Builtins/DoubleSpinBox.qml +58 -0
  69. data/Components/Builtins/DragArea.qml +43 -0
  70. data/Components/Builtins/Drawer.qml +107 -0
  71. data/Components/Builtins/DropArea.qml +19 -0
  72. data/Components/Builtins/DropShadow.qml +16 -0
  73. data/Components/Builtins/Dropdown.qml +19 -0
  74. data/Components/Builtins/ExpansionPanel.qml +157 -0
  75. data/Components/Builtins/FilePicker.qml +140 -0
  76. data/Components/Builtins/FittedBox.qml +40 -0
  77. data/Components/Builtins/Flickable.qml +40 -0
  78. data/Components/Builtins/Flipable.qml +45 -0
  79. data/Components/Builtins/Flow.qml +16 -0
  80. data/Components/Builtins/FocusScope.qml +29 -0
  81. data/Components/Builtins/FolderListModel.qml +9 -0
  82. data/Components/Builtins/FolderPicker.qml +93 -0
  83. data/Components/Builtins/FontLoader.qml +21 -0
  84. data/Components/Builtins/FontPicker.qml +91 -0
  85. data/Components/Builtins/Frame.qml +71 -0
  86. data/Components/Builtins/FrameAnimation.qml +13 -0
  87. data/Components/Builtins/Gauge.qml +3 -0
  88. data/Components/Builtins/Glow.qml +15 -0
  89. data/Components/Builtins/Gradient.qml +93 -0
  90. data/Components/Builtins/Grid.qml +16 -0
  91. data/Components/Builtins/GridLayout.qml +16 -0
  92. data/Components/Builtins/GridView.qml +226 -0
  93. data/Components/Builtins/GroupBox.qml +95 -0
  94. data/Components/Builtins/Heatmap.qml +3 -0
  95. data/Components/Builtins/Histogram.qml +3 -0
  96. data/Components/Builtins/HorizontalHeader.qml +23 -0
  97. data/Components/Builtins/HorizontalHeaderDelegate.qml +39 -0
  98. data/Components/Builtins/HoverArea.qml +19 -0
  99. data/Components/Builtins/Icon.qml +16 -0
  100. data/Components/Builtins/Image.qml +110 -0
  101. data/Components/Builtins/ImageCapture.qml +14 -0
  102. data/Components/Builtins/ItemDelegate.qml +152 -0
  103. data/Components/Builtins/KeyCatcher.qml +25 -0
  104. data/Components/Builtins/Label.qml +43 -0
  105. data/Components/Builtins/LayoutItemProxy.qml +51 -0
  106. data/Components/Builtins/Legend.qml +86 -0
  107. data/Components/Builtins/Line.qml +28 -0
  108. data/Components/Builtins/LineChart.qml +87 -0
  109. data/Components/Builtins/ListModel.qml +8 -0
  110. data/Components/Builtins/ListView.qml +65 -0
  111. data/Components/Builtins/Loader.qml +33 -0
  112. data/Components/Builtins/Markdown.qml +23 -0
  113. data/Components/Builtins/MediaDevices.qml +10 -0
  114. data/Components/Builtins/MediaPlayer.qml +27 -0
  115. data/Components/Builtins/MediaRecorder.qml +16 -0
  116. data/Components/Builtins/Menu.qml +119 -0
  117. data/Components/Builtins/MenuBar.qml +128 -0
  118. data/Components/Builtins/MenuItem.qml +77 -0
  119. data/Components/Builtins/MenuSeparator.qml +30 -0
  120. data/Components/Builtins/MessageDialog.qml +104 -0
  121. data/Components/Builtins/ModalSheet.qml +222 -0
  122. data/Components/Builtins/ModelView3d.qml +50 -0
  123. data/Components/Builtins/MonthGrid.qml +23 -0
  124. data/Components/Builtins/MultiEffect.qml +48 -0
  125. data/Components/Builtins/MultiSelect.qml +23 -0
  126. data/Components/Builtins/NavigationRail.qml +154 -0
  127. data/Components/Builtins/NumberAnimation.qml +3 -0
  128. data/Components/Builtins/NumberField.qml +18 -0
  129. data/Components/Builtins/OpacityAnimator.qml +3 -0
  130. data/Components/Builtins/OpacityMask.qml +21 -0
  131. data/Components/Builtins/OpticalGlyph.qml +16 -0
  132. data/Components/Builtins/Page.qml +111 -0
  133. data/Components/Builtins/PageIndicator.qml +63 -0
  134. data/Components/Builtins/Pagination.qml +158 -0
  135. data/Components/Builtins/Pane.qml +72 -0
  136. data/Components/Builtins/PanelHero.qml +15 -0
  137. data/Components/Builtins/ParallelAnimation.qml +3 -0
  138. data/Components/Builtins/ParentAnimation.qml +3 -0
  139. data/Components/Builtins/ParentChange.qml +10 -0
  140. data/Components/Builtins/ParticleSystem.qml +81 -0
  141. data/Components/Builtins/PasswordField.qml +58 -0
  142. data/Components/Builtins/Path.qml +35 -0
  143. data/Components/Builtins/PathAnimation.qml +3 -0
  144. data/Components/Builtins/PauseAnimation.qml +3 -0
  145. data/Components/Builtins/PieChart.qml +3 -0
  146. data/Components/Builtins/PinchArea.qml +23 -0
  147. data/Components/Builtins/Popup.qml +130 -0
  148. data/Components/Builtins/Progress.qml +15 -0
  149. data/Components/Builtins/ProgressRing.qml +120 -0
  150. data/Components/Builtins/PropertyAction.qml +11 -0
  151. data/Components/Builtins/PropertyAnimation.qml +3 -0
  152. data/Components/Builtins/PropertyChanges.qml +10 -0
  153. data/Components/Builtins/RadarChart.qml +3 -0
  154. data/Components/Builtins/RadialGauge.qml +3 -0
  155. data/Components/Builtins/RadioButton.qml +46 -0
  156. data/Components/Builtins/RadioDelegate.qml +115 -0
  157. data/Components/Builtins/RadioGroup.qml +58 -0
  158. data/Components/Builtins/RangeSlider.qml +39 -0
  159. data/Components/Builtins/Rectangle.qml +23 -0
  160. data/Components/Builtins/RectangularShadow.qml +23 -0
  161. data/Components/Builtins/ReorderableList.qml +204 -0
  162. data/Components/Builtins/RichText.qml +23 -0
  163. data/Components/Builtins/RotationAnimation.qml +3 -0
  164. data/Components/Builtins/RotationAnimator.qml +3 -0
  165. data/Components/Builtins/RoundButton.qml +42 -0
  166. data/Components/Builtins/Row.qml +17 -0
  167. data/Components/Builtins/RowLayout.qml +13 -0
  168. data/Components/Builtins/ScaleAnimator.qml +3 -0
  169. data/Components/Builtins/ScatterChart.qml +3 -0
  170. data/Components/Builtins/ScreenCapture.qml +78 -0
  171. data/Components/Builtins/ScriptAction.qml +15 -0
  172. data/Components/Builtins/Scroll.qml +18 -0
  173. data/Components/Builtins/ScrollBar.qml +18 -0
  174. data/Components/Builtins/ScrollIndicator.qml +15 -0
  175. data/Components/Builtins/SearchField.qml +178 -0
  176. data/Components/Builtins/SearchableDropdown.qml +21 -0
  177. data/Components/Builtins/SectionHeader.qml +14 -0
  178. data/Components/Builtins/SelectableText.qml +33 -0
  179. data/Components/Builtins/SelectionRectangle.qml +17 -0
  180. data/Components/Builtins/Separator.qml +13 -0
  181. data/Components/Builtins/SequentialAnimation.qml +3 -0
  182. data/Components/Builtins/Settings.qml +10 -0
  183. data/Components/Builtins/ShaderEffect.qml +115 -0
  184. data/Components/Builtins/ShaderEffectSource.qml +61 -0
  185. data/Components/Builtins/Shaders/grayscale.frag +14 -0
  186. data/Components/Builtins/Shaders/grayscale.frag.qsb +0 -0
  187. data/Components/Builtins/Shaders/passthrough.frag +11 -0
  188. data/Components/Builtins/Shaders/passthrough.frag.qsb +0 -0
  189. data/Components/Builtins/Shaders/pixelate.frag +16 -0
  190. data/Components/Builtins/Shaders/pixelate.frag.qsb +0 -0
  191. data/Components/Builtins/Shaders/vignette.frag +18 -0
  192. data/Components/Builtins/Shaders/vignette.frag.qsb +0 -0
  193. data/Components/Builtins/Shaders/wave.frag +18 -0
  194. data/Components/Builtins/Shaders/wave.frag.qsb +0 -0
  195. data/Components/Builtins/Shape.qml +53 -0
  196. data/Components/Builtins/Skeleton.qml +78 -0
  197. data/Components/Builtins/Slider.qml +21 -0
  198. data/Components/Builtins/SmoothedAnimation.qml +3 -0
  199. data/Components/Builtins/Snackbar.qml +181 -0
  200. data/Components/Builtins/SortFilterProxyModel.qml +16 -0
  201. data/Components/Builtins/SoundEffect.qml +10 -0
  202. data/Components/Builtins/Spacer.qml +13 -0
  203. data/Components/Builtins/Sparkline.qml +3 -0
  204. data/Components/Builtins/SpinBox.qml +44 -0
  205. data/Components/Builtins/SplitView.qml +31 -0
  206. data/Components/Builtins/SpringAnimation.qml +3 -0
  207. data/Components/Builtins/Stack.qml +14 -0
  208. data/Components/Builtins/StackLayout.qml +19 -0
  209. data/Components/Builtins/StackView.qml +122 -0
  210. data/Components/Builtins/StackedBarChart.qml +3 -0
  211. data/Components/Builtins/StandardPaths.qml +9 -0
  212. data/Components/Builtins/State.qml +13 -0
  213. data/Components/Builtins/StateGroup.qml +11 -0
  214. data/Components/Builtins/Support/AnimationDriver.qml +79 -0
  215. data/Components/Builtins/Support/AnimationGroupDriver.qml +32 -0
  216. data/Components/Builtins/Support/ChartCanvas.qml +629 -0
  217. data/Components/Builtins/Support/ModelView3dScene.qml +292 -0
  218. data/Components/Builtins/Support/OptionalModuleState.js +5 -0
  219. data/Components/Builtins/SwipeDelegate.qml +231 -0
  220. data/Components/Builtins/SwipeView.qml +69 -0
  221. data/Components/Builtins/SwitchDelegate.qml +140 -0
  222. data/Components/Builtins/TabBar.qml +117 -0
  223. data/Components/Builtins/TabButton.qml +105 -0
  224. data/Components/Builtins/TableView.qml +388 -0
  225. data/Components/Builtins/TableViewDelegate.qml +17 -0
  226. data/Components/Builtins/Tabs.qml +138 -0
  227. data/Components/Builtins/Text.qml +24 -0
  228. data/Components/Builtins/TextArea.qml +59 -0
  229. data/Components/Builtins/TextField.qml +21 -0
  230. data/Components/Builtins/TextMetrics.qml +39 -0
  231. data/Components/Builtins/TimePicker.qml +213 -0
  232. data/Components/Builtins/Timer.qml +13 -0
  233. data/Components/Builtins/Toast.qml +213 -0
  234. data/Components/Builtins/Toggle.qml +19 -0
  235. data/Components/Builtins/ToggleSwitch.qml +22 -0
  236. data/Components/Builtins/ToolBar.qml +61 -0
  237. data/Components/Builtins/ToolButton.qml +41 -0
  238. data/Components/Builtins/ToolSeparator.qml +36 -0
  239. data/Components/Builtins/Tooltip.qml +20 -0
  240. data/Components/Builtins/Transition.qml +3 -0
  241. data/Components/Builtins/TreeView.qml +423 -0
  242. data/Components/Builtins/TreeViewDelegate.qml +46 -0
  243. data/Components/Builtins/Tumbler.qml +18 -0
  244. data/Components/Builtins/UniformAnimator.qml +3 -0
  245. data/Components/Builtins/VectorAnimation.qml +3 -0
  246. data/Components/Builtins/VectorImage.qml +62 -0
  247. data/Components/Builtins/VerticalHeader.qml +21 -0
  248. data/Components/Builtins/VerticalHeaderDelegate.qml +35 -0
  249. data/Components/Builtins/Video.qml +54 -0
  250. data/Components/Builtins/VideoOutput.qml +54 -0
  251. data/Components/Builtins/WebView.qml +57 -0
  252. data/Components/Builtins/WeekNumberColumn.qml +13 -0
  253. data/Components/Builtins/WidgetButton.qml +26 -0
  254. data/Components/Builtins/Window.qml +90 -0
  255. data/Components/Builtins/WindowCapture.qml +11 -0
  256. data/Components/Builtins/Wrap.qml +18 -0
  257. data/Components/Builtins/XAnimator.qml +3 -0
  258. data/Components/Builtins/YAnimator.qml +3 -0
  259. data/Components/README.md +45 -0
  260. data/ControlNode.qml +1369 -0
  261. data/Controls/BarIconButton.qml +27 -0
  262. data/Controls/BarIndicator.qml +22 -0
  263. data/Controls/BorderOverlay.qml +10 -0
  264. data/Controls/BorderSurface.qml +10 -0
  265. data/Controls/Button.qml +78 -0
  266. data/Controls/ButtonGroup.qml +19 -0
  267. data/Controls/ConfirmDialog.qml +37 -0
  268. data/Controls/CursorSurface.qml +29 -0
  269. data/Controls/Dropdown.qml +59 -0
  270. data/Controls/MultiSelect.qml +32 -0
  271. data/Controls/NumberField.qml +24 -0
  272. data/Controls/OpticalGlyph.qml +14 -0
  273. data/Controls/PanelActionButton.qml +10 -0
  274. data/Controls/PanelHero.qml +18 -0
  275. data/Controls/PanelKeyCatcher.qml +25 -0
  276. data/Controls/PanelSectionHeader.qml +11 -0
  277. data/Controls/PanelSeparator.qml +10 -0
  278. data/Controls/PanelSlider.qml +36 -0
  279. data/Controls/PanelToolTip.qml +13 -0
  280. data/Controls/SearchableDropdown.qml +11 -0
  281. data/Controls/TextField.qml +25 -0
  282. data/Controls/Toggle.qml +22 -0
  283. data/Controls/ToggleSwitch.qml +26 -0
  284. data/Controls/WidgetButton.qml +35 -0
  285. data/Controls/qmldir +25 -0
  286. data/Desktop.qml +76 -0
  287. data/LICENSE +22 -0
  288. data/README.md +112 -0
  289. data/Service.qml +430 -0
  290. data/Theme/Border.qml +22 -0
  291. data/Theme/Color.qml +22 -0
  292. data/Theme/Style.qml +47 -0
  293. data/Theme/Util.qml +6 -0
  294. data/Theme/qmldir +5 -0
  295. data/bin/zui +7 -0
  296. data/lib/zui/animation.rb +24 -0
  297. data/lib/zui/application.rb +327 -0
  298. data/lib/zui/builder.rb +725 -0
  299. data/lib/zui/cli.rb +91 -0
  300. data/lib/zui/command.rb +101 -0
  301. data/lib/zui/component_registry.rb +91 -0
  302. data/lib/zui/components.rb +262 -0
  303. data/lib/zui/distribution.rb +200 -0
  304. data/lib/zui/generator.rb +84 -0
  305. data/lib/zui/host.rb +123 -0
  306. data/lib/zui/node.rb +30 -0
  307. data/lib/zui/platform.rb +40 -0
  308. data/lib/zui/protocol.rb +29 -0
  309. data/lib/zui/runner.rb +37 -0
  310. data/lib/zui/runtime.rb +23 -0
  311. data/lib/zui/scheduler.rb +136 -0
  312. data/lib/zui/source_bundle.rb +63 -0
  313. data/lib/zui/state_store.rb +100 -0
  314. data/lib/zui/value.rb +44 -0
  315. data/lib/zui.rb +33 -0
  316. data/native/CMakeLists.txt +45 -0
  317. data/native/ZuiClipboard.cpp +16 -0
  318. data/native/ZuiClipboard.h +16 -0
  319. data/native/ZuiProcess.cpp +84 -0
  320. data/native/ZuiProcess.h +34 -0
  321. data/native/main.cpp +64 -0
  322. data/vendor/host/linux-x86_64/PROVENANCE.md +15 -0
  323. data/vendor/host/linux-x86_64/zui-host +0 -0
  324. data/vendor/host/linux-x86_64/zui-host.sha256 +1 -0
  325. metadata +365 -0
@@ -0,0 +1,725 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Zui
4
+ class Builder
5
+ UNSET = Object.new.freeze
6
+ CONTAINERS = %i[
7
+ row column container grid row_layout column_layout grid_layout flow center card
8
+ stack scroll rectangle aspect_ratio constrained_box fitted_box wrap split_view stack_layout loader flickable focus_scope flipable border_image key_catcher
9
+ page pane frame group_box tabs stack_view swipe_view drawer expansion_panel accordion tool_bar popup dialog
10
+ shader_effect shader_effect_source multi_effect rectangular_shadow opacity_mask blur drop_shadow colorize glow
11
+ drag_area drop_area pinch_area hover_area capture_session cursor_surface
12
+ ].freeze
13
+ VALUE_INPUTS = {
14
+ text_field: :text,
15
+ text_area: :text,
16
+ password_field: :text,
17
+ number_field: :value,
18
+ slider: :value,
19
+ dial: :value,
20
+ spin_box: :value,
21
+ double_spin_box: :value,
22
+ color_picker: :color,
23
+ date_picker: :date,
24
+ time_picker: :time,
25
+ file_picker: :path,
26
+ folder_picker: :path,
27
+ font_picker: :family,
28
+ dropdown: :value,
29
+ searchable_dropdown: :value,
30
+ multi_select: :values,
31
+ button_group: :value,
32
+ radio_group: :value
33
+ }.freeze
34
+
35
+ def initialize(application)
36
+ @application = application
37
+ @stack = []
38
+ @dynamic_scopes = []
39
+ end
40
+
41
+ def component(type, id: nil, **props, &block)
42
+ definition = @application.components.fetch(type)
43
+ node = @application.build_node(type, explicit_id: id || scoped_id(type), props:)
44
+ append(node)
45
+ if block
46
+ raise ArgumentError, "#{type} is not a container" unless definition.container
47
+ within(node, &block)
48
+ end
49
+ node
50
+ end
51
+ alias widget component
52
+ alias qml_component component
53
+
54
+ CONTAINERS.each do |type|
55
+ define_method(type) { |id: nil, **props, &block| component(type, id:, **props, &block) }
56
+ end
57
+
58
+ def register_component(name, qml:, properties: [], events: [], property_map: {}, event_map: {}, container: false, auto_bind: true)
59
+ unless @stack.empty? && @application.surfaces.empty?
60
+ raise ArgumentError, "components must be registered before a surface"
61
+ end
62
+ @application.components.register(name, qml:, properties:, events:, property_map:, event_map:, container:, auto_bind:)
63
+ end
64
+
65
+ def state(name = nil, initial = UNSET, target: nil, properties: nil, id: nil, **props)
66
+ unless target.nil?
67
+ state_props = props.merge(target: node_id(target), name: name.to_s)
68
+ state_props[:properties] = properties unless properties.nil?
69
+ return component(:state, id:, **state_props)
70
+ end
71
+ return @application.state if name.nil?
72
+ raise ArgumentError, "state requires an initial value" if initial.equal?(UNSET)
73
+ @application.define_state(name, initial)
74
+ end
75
+
76
+ APP_OPTIONS = %i[title width height min_width min_height max_width max_height color visible maximized fullscreen].freeze
77
+
78
+ def bar_widget(&block) = surface("bar", id: "bar", &block)
79
+ def panel(name, &block) = surface(name.to_s, id: "panel.#{name}", &block)
80
+
81
+ def app(name = :main, **options, &block)
82
+ unknown = options.keys - APP_OPTIONS
83
+ raise ArgumentError, "unsupported app options: #{unknown.join(', ')}" unless unknown.empty?
84
+ surface(name.to_s, id: "app.#{name}", options:, &block)
85
+ end
86
+
87
+ def dynamic(type: :column, id: nil, **props, &renderer)
88
+ raise ArgumentError, "dynamic requires a block" unless renderer
89
+ definition = @application.components.fetch(type)
90
+ raise ArgumentError, "dynamic component must be a container: #{type}" unless definition.container
91
+
92
+ node = component(type, id:, **props)
93
+ within_dynamic(node, &renderer)
94
+ @application.register_structure(node, renderer)
95
+ node
96
+ end
97
+
98
+ def text(value = UNSET, id: nil, **props, &reader)
99
+ node = component(:text, id:, **props)
100
+ bind_or_set(node, "text", value, reader)
101
+ node
102
+ end
103
+
104
+ def label(value, id: nil, **props) = component(:label, id:, text: value.to_s, **props)
105
+ def rich_text(markup, id: nil, **props) = component(:rich_text, id:, text: markup.to_s, **props)
106
+ def markdown(source, id: nil, **props) = component(:markdown, id:, text: source.to_s, **props)
107
+ def selectable_text(value, id: nil, **props) = component(:selectable_text, id:, text: value.to_s, **props)
108
+
109
+ def icon(name, id: nil, **props) = component(:icon, id:, name: name.to_s, **props)
110
+ def tooltip(value, id: nil, **props) = component(:tooltip, id:, text: value.to_s, **props)
111
+ def image(source, id: nil, **props) = component(:image, id:, source: source.to_s, **props)
112
+ def vector_image(source, id: nil, **props) = component(:vector_image, id:, source: source.to_s, **props)
113
+ def model_view_3d(source, id: nil, **props) = component(:model_view_3d, id:, source: source.to_s, **props)
114
+ def font_loader(source, id: nil, **props) = component(:font_loader, id:, source: source.to_s, **props)
115
+ def text_metrics(value, id: nil, **props) = component(:text_metrics, id:, text: value.to_s, **props)
116
+ def web_view(url = "", id: nil, **props) = component(:web_view, id:, url: url.to_s, **props)
117
+ def animated_image(source, id: nil, **props) = component(:animated_image, id:, source: source.to_s, **props)
118
+ def video(source, id: nil, **props) = component(:video, id:, source: source.to_s, **props)
119
+ def audio(source, id: nil, **props) = component(:audio, id:, source: source.to_s, **props)
120
+ def avatar(source = nil, id: nil, **props)
121
+ source.nil? ? component(:avatar, id:, **props) : component(:avatar, id:, source: source.to_s, **props)
122
+ end
123
+ def badge(value = nil, id: nil, **props) = component(:badge, id:, value:, **props)
124
+ def chip(label, id: nil, **props, &handler)
125
+ action_component(:chip, :text, label, id:, props:, handler:)
126
+ end
127
+ def border_image(source, id: nil, **props, &block) = component(:border_image, id:, source: source.to_s, **props, &block)
128
+ def window(title = "", id: nil, **props, &block)
129
+ component(:window, id:, title: title.to_s, **props, &block)
130
+ end
131
+ def application_window(title = "", id: nil, **props, &block)
132
+ component(:application_window, id:, title: title.to_s, **props, &block)
133
+ end
134
+ def page(title = "", id: nil, **props, &block)
135
+ component(:page, id:, title: title.to_s, **props, &block)
136
+ end
137
+ def pane(id: nil, **props, &block)
138
+ component(:pane, id:, **props, &block)
139
+ end
140
+ def frame(id: nil, **props, &block)
141
+ component(:frame, id:, **props, &block)
142
+ end
143
+ def group_box(title = "", id: nil, **props, &block)
144
+ component(:group_box, id:, title: title.to_s, **props, &block)
145
+ end
146
+ def tabs(labels = [], id: nil, **props, &block)
147
+ component(:tabs, id:, labels: Array(labels), **props, &block)
148
+ end
149
+ def tab_bar(items = [], id: nil, **props)
150
+ component(:tab_bar, id:, items: Array(items), **props)
151
+ end
152
+ def tab_button(label = "", id: nil, **props, &handler)
153
+ action_component(:tab_button, :text, label, id:, props:, handler:)
154
+ end
155
+ def page_indicator(count, id: nil, **props)
156
+ component(:page_indicator, id:, count:, **props)
157
+ end
158
+ def stack_view(id: nil, **props, &block)
159
+ component(:stack_view, id:, **props, &block)
160
+ end
161
+ def swipe_view(id: nil, **props, &block)
162
+ component(:swipe_view, id:, **props, &block)
163
+ end
164
+ def drawer(id: nil, **props, &block)
165
+ component(:drawer, id:, **props, &block)
166
+ end
167
+ def navigation_rail(items = [], id: nil, **props)
168
+ component(:navigation_rail, id:, items: Array(items), **props)
169
+ end
170
+ def breadcrumb(items = [], id: nil, **props)
171
+ component(:breadcrumb, id:, items: Array(items), **props)
172
+ end
173
+ def pagination(count, id: nil, **props)
174
+ component(:pagination, id:, count:, **props)
175
+ end
176
+ def expansion_panel(title = "", id: nil, **props, &block)
177
+ component(:expansion_panel, id:, title: title.to_s, **props, &block)
178
+ end
179
+ def accordion(titles = [], id: nil, **props, &block)
180
+ component(:accordion, id:, titles: Array(titles), **props, &block)
181
+ end
182
+ def tool_bar(id: nil, **props, &block)
183
+ component(:tool_bar, id:, **props, &block)
184
+ end
185
+ def tool_separator(id: nil, **props)
186
+ component(:tool_separator, id:, **props)
187
+ end
188
+ def menu(items = [], id: nil, **props)
189
+ component(:menu, id:, items: Array(items), **props)
190
+ end
191
+ def menu_item(label = "", id: nil, value: nil, **props, &handler)
192
+ item_props = props.merge(text: label.to_s)
193
+ item_props[:value] = value unless value.nil?
194
+ node = component(:menu_item, id:, **item_props)
195
+ @application.register_handler(node.id, :trigger, handler) if handler
196
+ node
197
+ end
198
+ def menu_separator(id: nil, **props)
199
+ component(:menu_separator, id:, **props)
200
+ end
201
+ def menu_bar(menus = [], id: nil, **props)
202
+ component(:menu_bar, id:, menus: Array(menus), **props)
203
+ end
204
+ def context_menu(items = [], id: nil, target: nil, **props)
205
+ target_id = target.is_a?(Node) ? target.id : target
206
+ menu_props = props.merge(items: Array(items))
207
+ menu_props[:target] = target_id.to_s unless target_id.nil?
208
+ component(:context_menu, id:, **menu_props)
209
+ end
210
+ def popup(id: nil, **props, &block)
211
+ component(:popup, id:, **props, &block)
212
+ end
213
+ def dialog(title = "", id: nil, **props, &block)
214
+ component(:dialog, id:, title: title.to_s, **props, &block)
215
+ end
216
+ def alert_dialog(title = "", message = "", id: nil, **props)
217
+ component(:alert_dialog, id:, title: title.to_s, message: message.to_s, **props)
218
+ end
219
+ def message_dialog(title = "", message = "", id: nil, **props)
220
+ component(:message_dialog, id:, title: title.to_s, message: message.to_s, **props)
221
+ end
222
+ def bottom_sheet(id: nil, **props, &block)
223
+ component(:bottom_sheet, id:, **props, &block)
224
+ end
225
+ def modal_sheet(title = "", id: nil, **props, &block)
226
+ component(:modal_sheet, id:, title: title.to_s, **props, &block)
227
+ end
228
+ def snackbar(message = "", id: nil, **props)
229
+ component(:snackbar, id:, message: message.to_s, **props)
230
+ end
231
+ def banner(message = "", id: nil, **props)
232
+ component(:banner, id:, message: message.to_s, **props)
233
+ end
234
+ def toast(message = "", id: nil, **props)
235
+ component(:toast, id:, message: message.to_s, **props)
236
+ end
237
+ def busy_indicator(running = true, id: nil, **props)
238
+ component(:busy_indicator, id:, running: running, **props)
239
+ end
240
+ def progress_ring(value = UNSET, id: nil, **props)
241
+ ring_props = props.dup
242
+ if value.equal?(UNSET)
243
+ ring_props[:indeterminate] = true unless ring_props.key?(:indeterminate)
244
+ else
245
+ ring_props[:value] = value
246
+ end
247
+ component(:progress_ring, id:, **ring_props)
248
+ end
249
+ def skeleton(id: nil, **props)
250
+ component(:skeleton, id:, **props)
251
+ end
252
+ def confirm_dialog(message = "", id: nil, **props)
253
+ component(:confirm_dialog, id:, message: message.to_s, **props)
254
+ end
255
+ def panel_hero(title = "", id: nil, **props)
256
+ component(:panel_hero, id:, title: title.to_s, **props)
257
+ end
258
+ def optical_glyph(text = "", id: nil, **props)
259
+ component(:optical_glyph, id:, text: text.to_s, **props)
260
+ end
261
+ def widget_button(text = "", id: nil, **props, &handler)
262
+ action_component(:widget_button, :text, text, id:, props:, handler:)
263
+ end
264
+ def list_view(items = [], id: nil, **props, &handler)
265
+ node = component(:list_view, id:, items: Array(items), **props)
266
+ @application.register_handler(node.id, :activate, handler) if handler
267
+ node
268
+ end
269
+ def item_delegate(text = "", id: nil, value: nil, **props, &handler)
270
+ item_props = props.merge(text: text.to_s)
271
+ item_props[:value] = value unless value.nil?
272
+ node = component(:item_delegate, id:, **item_props)
273
+ @application.register_handler(node.id, :activate, handler) if handler
274
+ node
275
+ end
276
+ def check_delegate(text = "", id: nil, value: nil, checked: UNSET, **props, &handler)
277
+ item_props = props.merge(text: text.to_s)
278
+ item_props[:value] = value unless value.nil?
279
+ item_props[:checked] = checked unless checked.equal?(UNSET)
280
+ node = component(:check_delegate, id:, **item_props)
281
+ @application.register_handler(node.id, :change, handler) if handler
282
+ node
283
+ end
284
+ def radio_delegate(text = "", id: nil, value: nil, checked: UNSET, **props, &handler)
285
+ item_props = props.merge(text: text.to_s)
286
+ item_props[:value] = value unless value.nil?
287
+ item_props[:checked] = checked unless checked.equal?(UNSET)
288
+ node = component(:radio_delegate, id:, **item_props)
289
+ @application.register_handler(node.id, :select, handler) if handler
290
+ node
291
+ end
292
+ def switch_delegate(text = "", id: nil, value: nil, checked: UNSET, **props, &handler)
293
+ item_props = props.merge(text: text.to_s)
294
+ item_props[:value] = value unless value.nil?
295
+ item_props[:checked] = checked unless checked.equal?(UNSET)
296
+ node = component(:switch_delegate, id:, **item_props)
297
+ @application.register_handler(node.id, :change, handler) if handler
298
+ node
299
+ end
300
+ def swipe_delegate(text = "", id: nil, value: nil, **props, &handler)
301
+ item_props = props.merge(text: text.to_s)
302
+ item_props[:value] = value unless value.nil?
303
+ node = component(:swipe_delegate, id:, **item_props)
304
+ @application.register_handler(node.id, :activate, handler) if handler
305
+ node
306
+ end
307
+ def grid_view(items = [], id: nil, **props, &handler)
308
+ node = component(:grid_view, id:, items: Array(items), **props)
309
+ @application.register_handler(node.id, :activate, handler) if handler
310
+ node
311
+ end
312
+ def table_view(rows = [], id: nil, columns: [], **props, &handler)
313
+ node = component(:table_view, id:, rows: Array(rows), columns: Array(columns), **props)
314
+ @application.register_handler(node.id, :activate, handler) if handler
315
+ node
316
+ end
317
+
318
+ def tree_view(rows = [], id: nil, columns: [], children_field: :children, **props, &handler)
319
+ node = component(:tree_view, id:, rows: Array(rows), columns: Array(columns),
320
+ children_field: children_field.to_s, **props)
321
+ @application.register_handler(node.id, :activate, handler) if handler
322
+ node
323
+ end
324
+
325
+ def data_table(rows = [], id: nil, columns: [], **props, &handler)
326
+ node = component(:data_table, id:, rows: Array(rows), columns: Array(columns), **props)
327
+ @application.register_handler(node.id, :activate, handler) if handler
328
+ node
329
+ end
330
+
331
+ def horizontal_header(sections = [], id: nil, **props) = component(:horizontal_header, id:, sections: Array(sections), **props)
332
+ def vertical_header(sections = [], id: nil, **props) = component(:vertical_header, id:, sections: Array(sections), **props)
333
+ def table_view_delegate(text = "", id: nil, **props) = component(:table_view_delegate, id:, text: text.to_s, **props)
334
+ def tree_view_delegate(text = "", id: nil, **props) = component(:tree_view_delegate, id:, text: text.to_s, **props)
335
+ def horizontal_header_delegate(text = "", id: nil, **props) = component(:horizontal_header_delegate, id:, text: text.to_s, **props)
336
+ def vertical_header_delegate(text = "", id: nil, **props) = component(:vertical_header_delegate, id:, text: text.to_s, **props)
337
+ def reorderable_list(items = [], id: nil, **props) = component(:reorderable_list, id:, items: Array(items), **props)
338
+ def carousel(items = [], id: nil, **props) = component(:carousel, id:, items: Array(items), **props)
339
+ def calendar(date = nil, id: nil, **props)
340
+ calendar_props = props.dup
341
+ calendar_props[:date] = date.to_s unless date.nil?
342
+ component(:calendar, id:, **calendar_props)
343
+ end
344
+ def month_grid(id: nil, **props) = component(:month_grid, id:, **props)
345
+ def week_number_column(id: nil, **props) = component(:week_number_column, id:, **props)
346
+ def day_of_week_row(id: nil, **props) = component(:day_of_week_row, id:, **props)
347
+ def tumbler(items = [], id: nil, **props) = component(:tumbler, id:, items: Array(items), **props)
348
+
349
+ def canvas(commands = [], id: nil, **props)
350
+ component(:canvas, id:, commands: Array(commands), **props)
351
+ end
352
+
353
+ def shape(path_data = "", id: nil, **props)
354
+ component(:shape, id:, path: path_data.to_s, **props)
355
+ end
356
+
357
+ def line(x1 = 0, y1 = 0, x2 = 100, y2 = 0, id: nil, **props)
358
+ component(:line, id:, x1:, y1:, x2:, y2:, **props)
359
+ end
360
+
361
+ def path(data = "", id: nil, **props)
362
+ component(:path, id:, data: data.to_s, **props)
363
+ end
364
+
365
+ def circle(radius = 50, id: nil, **props)
366
+ component(:circle, id:, radius:, **props)
367
+ end
368
+
369
+ def gradient(colors = [], id: nil, **props)
370
+ component(:gradient, id:, colors: Array(colors), **props)
371
+ end
372
+
373
+ def shader_effect(fragment_shader = nil, id: nil, shader: nil, **props, &block)
374
+ effect_props = props.dup
375
+ effect_props[:shader] = shader.to_s unless shader.nil?
376
+ effect_props[:fragment_shader] = fragment_shader.to_s unless fragment_shader.nil?
377
+ component(:shader_effect, id:, **effect_props, &block)
378
+ end
379
+
380
+ def shader_effect_source(id: nil, **props, &block)
381
+ component(:shader_effect_source, id:, **props, &block)
382
+ end
383
+
384
+ def particle_system(id: nil, **props)
385
+ component(:particle_system, id:, **props)
386
+ end
387
+
388
+ def selection_rectangle(target, id: nil, **props) = component(:selection_rectangle, id:, target: node_id(target), **props)
389
+ def scroll_bar(target = nil, id: nil, **props)
390
+ props = props.merge(target: node_id(target)) unless target.nil?
391
+ component(:scroll_bar, id:, **props)
392
+ end
393
+ def scroll_indicator(target = nil, id: nil, **props)
394
+ props = props.merge(target: node_id(target)) unless target.nil?
395
+ component(:scroll_indicator, id:, **props)
396
+ end
397
+
398
+ def layout_item_proxy(target, id: nil, **props)
399
+ target_id = target.is_a?(Node) ? target.id : target.to_s
400
+ raise ArgumentError, "layout_item_proxy target cannot be empty" if target_id.empty?
401
+ component(:layout_item_proxy, id:, target: target_id, **props)
402
+ end
403
+ def border_overlay(id: nil, **props) = component(:border_overlay, id:, **props)
404
+ def spacer(id: nil, **props) = component(:spacer, id:, **props)
405
+ def separator(id: nil, **props) = component(:separator, id:, **props)
406
+ def divider(id: nil, **props) = component(:divider, id:, **props)
407
+ def section_header(value, id: nil, **props) = component(:section_header, id:, text: value.to_s, **props)
408
+ def dialog_button_box(buttons = [], id: nil, **props)
409
+ component(:dialog_button_box, id:, buttons: Array(buttons), **props)
410
+ end
411
+ def action(label, id: nil, **props, &handler)
412
+ node = component(:action, id:, text: label.to_s, **props)
413
+ @application.register_handler(node.id, :trigger, handler) if handler
414
+ node
415
+ end
416
+ def action_group(actions, id: nil, checked: nil, **props)
417
+ action_ids = Array(actions).map { |action_node| action_node.is_a?(Node) ? action_node.id : action_node.to_s }
418
+ checked_id = checked.is_a?(Node) ? checked.id : checked&.to_s
419
+ component(:action_group, id:, action_ids:, checked_action: checked_id, **props)
420
+ end
421
+
422
+ def button(label, id: nil, **props, &handler)
423
+ action_component(:button, :text, label, id:, props:, handler:)
424
+ end
425
+
426
+ def round_button(label = "", id: nil, **props, &handler)
427
+ action_component(:round_button, :text, label, id:, props:, handler:)
428
+ end
429
+
430
+ def tool_button(label = "", id: nil, **props, &handler)
431
+ action_component(:tool_button, :text, label, id:, props:, handler:)
432
+ end
433
+
434
+ def delay_button(label, id: nil, **props, &handler)
435
+ node = action_component(:delay_button, :text, label, id:, props: props, handler: nil)
436
+ @application.register_handler(node.id, :activate, handler) if handler
437
+ node
438
+ end
439
+
440
+ def action_button(icon, id: nil, **props, &handler)
441
+ action_component(:action_button, :icon, icon, id:, props:, handler:)
442
+ end
443
+
444
+ def bar_icon_button(icon, id: nil, **props, &handler)
445
+ action_component(:bar_icon_button, :icon, icon, id:, props:, handler:)
446
+ end
447
+
448
+ def bar_indicator(active_icon, id: nil, **props, &handler)
449
+ action_component(:bar_indicator, :active_icon, active_icon, id:, props:, handler:)
450
+ end
451
+
452
+ def toggle(label = "", id: nil, checked: UNSET, **props, &handler)
453
+ input_component(:toggle, :checked, checked, id:, props: props.merge(label: label.to_s), handler:)
454
+ end
455
+
456
+ def checkbox(label = "", id: nil, checked: UNSET, **props, &handler)
457
+ input_component(:checkbox, :checked, checked, id:, props: props.merge(label: label.to_s), handler:)
458
+ end
459
+
460
+ def radio_button(label = "", id: nil, checked: UNSET, **props, &handler)
461
+ input_component(:radio_button, :checked, checked, id:, props: props.merge(label: label.to_s), handler:)
462
+ end
463
+
464
+ def search_field(value = "", id: nil, **props, &handler)
465
+ node = component(:search_field, id:, text: value.to_s, **props)
466
+ @application.register_handler(node.id, :search, handler) if handler
467
+ node
468
+ end
469
+
470
+ def toggle_switch(id: nil, checked: UNSET, **props, &handler)
471
+ input_component(:toggle_switch, :checked, checked, id:, props:, handler:)
472
+ end
473
+
474
+ VALUE_INPUTS.each do |type, property|
475
+ define_method(type) do |value = UNSET, id: nil, **props, &handler|
476
+ input_component(type, property, value, id:, props:, handler:)
477
+ end
478
+ end
479
+
480
+ def progress(value = UNSET, id: nil, **props)
481
+ input_component(:progress, :value, value, id:, props:)
482
+ end
483
+
484
+ def range_slider(lower, upper, id: nil, **props, &handler)
485
+ node = component(:range_slider, id:, lower:, upper:, **props)
486
+ @application.register_handler(node.id, :change, handler) if handler
487
+ node
488
+ end
489
+
490
+ def line_chart(values, id: nil, **props)
491
+ component(:line_chart, id:, values:, **props)
492
+ end
493
+
494
+ def area_chart(values, id: nil, **props)
495
+ component(:area_chart, id:, values:, **props)
496
+ end
497
+
498
+ def bar_chart(values, id: nil, **props)
499
+ component(:bar_chart, id:, values:, **props)
500
+ end
501
+
502
+ CHART_COMPONENTS = %i[
503
+ stacked_bar_chart pie_chart donut_chart scatter_chart bubble_chart radar_chart heatmap
504
+ sparkline gauge radial_gauge histogram candlestick_chart
505
+ ].freeze
506
+ CHART_COMPONENTS.each do |type|
507
+ define_method(type) do |values = [], id: nil, **props|
508
+ component(type, id:, values: Array(values), **props)
509
+ end
510
+ end
511
+
512
+ def legend(items = [], id: nil, **props) = component(:legend, id:, items: Array(items), **props)
513
+
514
+ def on_click(&handler)
515
+ raise ArgumentError, "on_click must be inside a surface or control" if @stack.empty?
516
+ on(@stack.last, :click, &handler)
517
+ end
518
+
519
+ def on(target_or_event, event = nil, &handler)
520
+ raise ArgumentError, "on requires a block" unless handler
521
+ if target_or_event.is_a?(Node)
522
+ raise ArgumentError, "on(node, event) requires an event" unless event
523
+ node, event_name = target_or_event, event
524
+ else
525
+ raise ArgumentError, "on must be inside a surface or control" if @stack.empty?
526
+ node, event_name = @stack.last, target_or_event
527
+ end
528
+ @application.register_handler(node.id, event_name, handler)
529
+ end
530
+
531
+ def property(name, value = UNSET, &reader)
532
+ raise ArgumentError, "property must be inside a control" if @stack.empty?
533
+ bind_or_set(@stack.last, name.to_s, value, reader)
534
+ end
535
+
536
+ def bind(node, property, animation: nil, &reader)
537
+ raise ArgumentError, "bind requires a node returned by a component method" unless node.is_a?(Node)
538
+ raise ArgumentError, "bind requires a block" unless reader
539
+ transition = normalize_animation(animation)
540
+ @application.register_binding(node, property.to_s, reader, animation: transition)
541
+ node
542
+ end
543
+
544
+ def animation(target: nil, id: nil, **options)
545
+ return Animation.new(**options) if target.nil?
546
+ component(:animation, id:, target: node_id(target), **options)
547
+ end
548
+
549
+ TARGET_ANIMATION_COMPONENTS = %i[
550
+ number_animation color_animation rotation_animation vector_animation path_animation property_animation
551
+ spring_animation smoothed_animation anchor_animation parent_animation opacity_animator rotation_animator
552
+ scale_animator x_animator y_animator uniform_animator animation_controller behavior transition state_group
553
+ property_changes anchor_changes parent_change
554
+ ].freeze
555
+ TARGET_ANIMATION_COMPONENTS.each do |type|
556
+ define_method(type) do |target, id: nil, **props|
557
+ component(type, id:, target: node_id(target), **props)
558
+ end
559
+ end
560
+
561
+ def pause_animation(duration = 0, id: nil, **props) = component(:pause_animation, id:, duration:, **props)
562
+ def script_action(action = nil, id: nil, **props) = component(:script_action, id:, action:, **props)
563
+ def property_action(target, property:, value:, id: nil, **props)
564
+ component(:property_action, id:, target: node_id(target), property: property.to_s, value:, **props)
565
+ end
566
+ def parallel_animation(animations = [], id: nil, **props)
567
+ component(:parallel_animation, id:, animations: normalize_animation_specs(animations), **props)
568
+ end
569
+ def sequential_animation(animations = [], id: nil, **props)
570
+ component(:sequential_animation, id:, animations: normalize_animation_specs(animations), **props)
571
+ end
572
+ def frame_animation(id: nil, **props) = component(:frame_animation, id:, **props)
573
+ def timer(interval, id: nil, **props) = component(:timer, id:, interval:, **props)
574
+
575
+ def media_player(source = "", id: nil, **props) = component(:media_player, id:, source: source.to_s, **props)
576
+ def video_output(source = nil, id: nil, **props)
577
+ props = props.merge(source: node_id(source)) unless source.nil?
578
+ component(:video_output, id:, **props)
579
+ end
580
+ def sound_effect(source = "", id: nil, **props) = component(:sound_effect, id:, source: source.to_s, **props)
581
+ def camera(id: nil, **props) = component(:camera, id:, **props)
582
+ def image_capture(session = nil, id: nil, **props)
583
+ props = props.merge(session: node_id(session)) unless session.nil?
584
+ component(:image_capture, id:, **props)
585
+ end
586
+ def media_recorder(session = nil, id: nil, **props)
587
+ props = props.merge(session: node_id(session)) unless session.nil?
588
+ component(:media_recorder, id:, **props)
589
+ end
590
+ def audio_input(id: nil, **props) = component(:audio_input, id:, **props)
591
+ def audio_output(id: nil, **props) = component(:audio_output, id:, **props)
592
+ def media_devices(id: nil, **props) = component(:media_devices, id:, **props)
593
+ def screen_capture(id: nil, **props) = component(:screen_capture, id:, **props)
594
+ def window_capture(id: nil, **props) = component(:window_capture, id:, **props)
595
+
596
+ def list_model(items = [], id: nil, **props) = component(:list_model, id:, items: Array(items), **props)
597
+ def delegate_model(items = [], id: nil, **props) = component(:delegate_model, id:, items: Array(items), **props)
598
+ def delegate_model_group(items = [], id: nil, **props) = component(:delegate_model_group, id:, items: Array(items), **props)
599
+ def sort_filter_proxy_model(items = [], id: nil, **props) = component(:sort_filter_proxy_model, id:, items: Array(items), **props)
600
+ def folder_list_model(folder = "", id: nil, **props) = component(:folder_list_model, id:, folder: folder.to_s, **props)
601
+ def settings(values = {}, id: nil, **props) = component(:settings, id:, values:, **props)
602
+ def standard_paths(location = :home, id: nil, **props) = component(:standard_paths, id:, location: location.to_s, **props)
603
+ def clipboard(text = UNSET, id: nil, **props)
604
+ props = props.merge(text:) unless text.equal?(UNSET)
605
+ component(:clipboard, id:, **props)
606
+ end
607
+
608
+ def animate(node, properties, duration: 200, easing: :in_out_quad, delay: 0)
609
+ transition = Animation.new(duration:, easing:, delay:)
610
+ @application.animate(node, properties, transition)
611
+ node
612
+ end
613
+
614
+ def animate_sequence(node, steps)
615
+ elapsed = 0
616
+ tracks = steps.flat_map do |step|
617
+ options = step.transform_keys(&:to_sym)
618
+ properties = options.fetch(:to)
619
+ transition = Animation.new(
620
+ duration: options.fetch(:duration, 200),
621
+ easing: options.fetch(:easing, :in_out_quad),
622
+ delay: elapsed + options.fetch(:delay, 0)
623
+ )
624
+ elapsed = transition.delay + transition.duration + options.fetch(:pause, 0)
625
+ @application.animation_tracks(node, properties, transition)
626
+ end
627
+ @application.emit_animation(node, tracks)
628
+ node
629
+ end
630
+ def transaction(&block) = @application.state.transaction { instance_eval(&block) }
631
+ def after(seconds, &block) = @application.schedule(:after, interval: seconds, &block)
632
+ def every(seconds, immediate: false, &block) = @application.schedule(:every, interval: seconds, immediate:, &block)
633
+ def async(&block) = @application.schedule(:async, &block)
634
+ def run_command(argv, **options) = Command.run(argv, **options)
635
+ def rebuild(node, &renderer) = within_dynamic(node, &renderer)
636
+ def open_panel(name) = @application.emit_effect("open_panel", "surface" => name.to_s)
637
+
638
+ def close_panel(name = nil)
639
+ @application.emit_effect("close_panel", name.nil? ? {} : { "surface" => name.to_s })
640
+ end
641
+
642
+ private
643
+
644
+ def node_id(value)
645
+ value.is_a?(Node) ? value.id : value.to_s
646
+ end
647
+
648
+ def normalize_animation_specs(animations)
649
+ Array(animations).map do |animation_spec|
650
+ next animation_spec unless animation_spec.is_a?(Hash)
651
+
652
+ animation_spec.each_with_object({}) do |(key, value), normalized|
653
+ normalized[key] = key.to_sym == :target && value.is_a?(Node) ? value.id : value
654
+ end
655
+ end
656
+ end
657
+
658
+ def scoped_id(type)
659
+ return nil if @dynamic_scopes.empty?
660
+ scope = @dynamic_scopes.last
661
+ scope[:sequence] += 1
662
+ "#{scope.fetch(:id)}.#{type}.#{scope.fetch(:sequence)}"
663
+ end
664
+
665
+ def within_dynamic(node, &block)
666
+ @dynamic_scopes.push({ id: node.id, sequence: 0 })
667
+ within(node, &block)
668
+ ensure
669
+ @dynamic_scopes.pop
670
+ end
671
+
672
+ def surface(name, id:, options: {}, &block)
673
+ raise ArgumentError, "surface requires a block" unless block
674
+ node = @application.build_node(:container, explicit_id: id)
675
+ @application.add_surface(name, node, options:)
676
+ within(node, &block)
677
+ node
678
+ end
679
+
680
+ def action_component(type, property, value, id:, props:, handler:)
681
+ node = component(type, id:, **props.merge(property => value.to_s))
682
+ @application.register_handler(node.id, :click, handler) if handler
683
+ node
684
+ end
685
+
686
+ def input_component(type, property, value, id:, props:, handler: nil)
687
+ props = props.merge(property => value) unless value.equal?(UNSET)
688
+ node = component(type, id:, **props)
689
+ event = %i[text_field password_field].include?(type) ? :input : :change
690
+ @application.register_handler(node.id, event, handler) if handler
691
+ node
692
+ end
693
+
694
+ def within(node, &block)
695
+ @stack.push(node)
696
+ instance_eval(&block)
697
+ ensure
698
+ @stack.pop
699
+ end
700
+
701
+ def append(node)
702
+ raise ArgumentError, "#{node.type} must be inside a surface or container" if @stack.empty?
703
+ @stack.last.children << node
704
+ end
705
+
706
+ def bind_or_set(node, property, value, reader)
707
+ if reader
708
+ raise ArgumentError, "pass a value or a reactive block, not both" unless value.equal?(UNSET)
709
+ @application.register_binding(node, property, reader)
710
+ else
711
+ raise ArgumentError, "#{property} requires a value or block" if value.equal?(UNSET)
712
+ node.props[property] = @application.normalize_value(value, property)
713
+ end
714
+ end
715
+
716
+ def normalize_animation(animation)
717
+ case animation
718
+ when nil then nil
719
+ when Animation then animation
720
+ when Hash then Animation.new(**animation)
721
+ else raise ArgumentError, "animation must be a Zui::Animation or options hash"
722
+ end
723
+ end
724
+ end
725
+ end