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
data/Theme/Style.qml ADDED
@@ -0,0 +1,47 @@
1
+ pragma Singleton
2
+ import QtQuick
3
+
4
+ QtObject {
5
+ readonly property int cornerRadius: 10
6
+ readonly property int normalBorderWidth: 1
7
+
8
+ function space(value) { return Number(value) }
9
+ function selectionFillFor(foreground, accent) { return Qt.rgba(accent.r, accent.g, accent.b, 0.34) }
10
+ function hoverFillFor(foreground, accent) { return Qt.rgba(accent.r, accent.g, accent.b, 0.14) }
11
+ function selectedFillFor(foreground, accent) { return Qt.rgba(accent.r, accent.g, accent.b, 0.22) }
12
+
13
+ readonly property QtObject font: QtObject {
14
+ readonly property string family: "Sans Serif"
15
+ readonly property int caption: 11
16
+ readonly property int bodySmall: 12
17
+ readonly property int body: 14
18
+ readonly property int subtitle: 16
19
+ readonly property int heading: 20
20
+ readonly property int display: 32
21
+ readonly property int icon: 16
22
+ }
23
+
24
+ readonly property QtObject spacing: QtObject {
25
+ readonly property int xs: 4
26
+ readonly property int sm: 8
27
+ readonly property int md: 12
28
+ readonly property int lg: 16
29
+ readonly property int xl: 24
30
+ readonly property int panelGap: 10
31
+ readonly property int controlGap: 8
32
+ readonly property int controlHeight: 36
33
+ readonly property int controlPaddingX: 12
34
+ readonly property int controlPaddingY: 8
35
+ readonly property int inputPaddingY: 8
36
+ readonly property int rowPaddingX: 10
37
+ readonly property int popupRowHeight: 36
38
+ readonly property int searchablePopupMinHeight: 160
39
+ readonly property int numberFieldWidth: 108
40
+ }
41
+
42
+ readonly property QtObject bar: QtObject {
43
+ readonly property int iconSlot: 30
44
+ readonly property int iconCanvas: 18
45
+ readonly property int iconFont: 15
46
+ }
47
+ }
data/Theme/Util.qml ADDED
@@ -0,0 +1,6 @@
1
+ pragma Singleton
2
+ import QtQuick
3
+
4
+ QtObject {
5
+ function alpha(color, opacity) { return Qt.rgba(color.r, color.g, color.b, Number(opacity)) }
6
+ }
data/Theme/qmldir ADDED
@@ -0,0 +1,5 @@
1
+ module Zui.Theme
2
+ singleton Color 1.0 Color.qml
3
+ singleton Style 1.0 Style.qml
4
+ singleton Border 1.0 Border.qml
5
+ singleton Util 1.0 Util.qml
data/bin/zui ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "../lib/zui"
5
+ require_relative "../lib/zui/cli"
6
+
7
+ exit Zui::CLI.run(ARGV)
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Zui
4
+ class Animation
5
+ EASINGS = %w[linear in_quad out_quad in_out_quad in_cubic out_cubic in_out_cubic
6
+ in_back out_back in_out_back in_elastic out_elastic in_out_elastic
7
+ in_bounce out_bounce in_out_bounce].freeze
8
+
9
+ attr_reader :duration, :easing, :delay
10
+
11
+ def initialize(duration: 200, easing: :in_out_quad, delay: 0)
12
+ @duration = Integer(duration)
13
+ @delay = Integer(delay)
14
+ @easing = easing.to_s
15
+ raise ArgumentError, "animation duration must be between 0 and 60 seconds" unless (0..60_000).cover?(@duration)
16
+ raise ArgumentError, "animation delay must be between 0 and 60 seconds" unless (0..60_000).cover?(@delay)
17
+ raise ArgumentError, "unsupported easing: #{@easing}" unless EASINGS.include?(@easing)
18
+ end
19
+
20
+ def to_h
21
+ { "duration" => duration, "easing" => easing, "delay" => delay }
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,327 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json" unless Object.const_defined?(:JSON)
4
+ require "thread" unless Object.const_defined?(:Mutex)
5
+
6
+ module Zui
7
+ class Application
8
+ ANIMATION_DEFAULTS = { "opacity" => 1.0, "scale" => 1.0, "rotation" => 0.0, "z" => 0.0 }.freeze
9
+ attr_reader :state, :surfaces, :surface_options, :components
10
+
11
+ def initialize(components: DEFAULT_COMPONENTS, ui: nil, &definition)
12
+ @surfaces = {}
13
+ @surface_options = {}
14
+ @nodes = {}
15
+ @bindings = []
16
+ @structures = []
17
+ @handlers = {}
18
+ @sequence = 0
19
+ @output = nil
20
+ @error = $stderr
21
+ @running = false
22
+ @write_lock = Mutex.new
23
+ @state_change_lock = Mutex.new
24
+ @components = components.dup
25
+ @builder = Builder.new(self)
26
+ builder_extensions = Array(ui).compact
27
+ unless builder_extensions.all? { |extension| extension.is_a?(Module) }
28
+ raise ArgumentError, "ui extensions must be modules"
29
+ end
30
+ @builder.extend(*builder_extensions) unless builder_extensions.empty?
31
+ @state = StateStore.new(method(:state_changed))
32
+ @scheduler = Scheduler.new(evaluator: method(:evaluate), on_error: method(:report_internal_error))
33
+ @builder.instance_eval(&definition) if definition
34
+ raise ArgumentError, "plugin defines no surfaces" if @surfaces.empty?
35
+ end
36
+
37
+ def define_state(name, initial) = @state.define(name, initial)
38
+
39
+ def build_node(type, explicit_id: nil, props: {})
40
+ definition = @components.fetch(type)
41
+ @sequence += 1
42
+ id = explicit_id ? explicit_id.to_s : "#{type}.#{@sequence}"
43
+ validate_id!(id)
44
+ raise ArgumentError, "duplicate control id: #{id}" if @nodes.key?(id)
45
+
46
+ normalized_props = normalize_props(props)
47
+ unknown = normalized_props.keys - definition.properties.map(&:to_s)
48
+ raise ArgumentError, "unsupported properties for #{type}: #{unknown.join(', ')}" unless unknown.empty?
49
+ Node.new(type:, id:, props: normalized_props).tap { |node| @nodes[id] = node }
50
+ end
51
+
52
+ def add_surface(name, node, options: {})
53
+ key = name.to_s
54
+ validate_id!(key)
55
+ raise ArgumentError, "duplicate surface: #{key}" if @surfaces.key?(key)
56
+ @surfaces[key] = node
57
+ @surface_options[key] = options.transform_keys(&:to_s).transform_values { |value| normalize_value(value) }
58
+ end
59
+
60
+ def register_binding(node, property, reader, animation: nil)
61
+ property = property.to_s
62
+ unless @components.fetch(node.type).properties.map(&:to_s).include?(property)
63
+ raise ArgumentError, "unsupported bound property for #{node.type}: #{property}"
64
+ end
65
+ value = normalize_value(evaluate(reader), property)
66
+ node.props[property] = value
67
+ @bindings << Binding.new(node:, property:, reader:, last_value: value, animation:)
68
+ end
69
+
70
+ def register_structure(node, renderer)
71
+ @structures << StructuralBinding.new(node:, renderer:, last_children: node.children.map(&:to_h))
72
+ end
73
+
74
+ def register_handler(control_id, event, handler)
75
+ raise ArgumentError, "handler requires a block" unless handler
76
+ node = @nodes.fetch(control_id.to_s) { raise ArgumentError, "unknown event control: #{control_id}" }
77
+ event_name = event.to_s
78
+ declared = @components.fetch(node.type).events.map(&:to_s)
79
+ unless declared.include?(event_name) || %w[mount unmount].include?(event_name)
80
+ raise ArgumentError, "#{node.type} does not declare event: #{event_name}"
81
+ end
82
+ node.subscribe(event_name)
83
+ key = [control_id.to_s, event_name]
84
+ raise ArgumentError, "duplicate handler for #{key.join('/')}" if @handlers.key?(key)
85
+ @handlers[key] = handler
86
+ end
87
+
88
+ def emit_effect(name, payload = {})
89
+ emit("v" => PROTOCOL_VERSION, "type" => "effect", "name" => name.to_s, "payload" => payload)
90
+ end
91
+
92
+ def animate(node, properties, animation)
93
+ emit_animation(node, animation_tracks(node, properties, animation))
94
+ end
95
+
96
+ def animation_tracks(node, properties, animation)
97
+ definition = @components.fetch(node.type)
98
+ properties.map do |property, value|
99
+ property_name = property.to_s
100
+ unless definition.properties.map(&:to_s).include?(property_name)
101
+ raise ArgumentError, "unsupported animated property for #{node.type}: #{property_name}"
102
+ end
103
+ normalized = normalize_value(value, property_name)
104
+ track = {
105
+ "property" => property_name,
106
+ "from" => node.props.fetch(property_name, ANIMATION_DEFAULTS[property_name]),
107
+ "to" => normalized
108
+ }.merge(animation.to_h)
109
+ node.props[property_name] = normalized
110
+ track
111
+ end
112
+ end
113
+
114
+ def emit_animation(node, tracks)
115
+ raise ArgumentError, "animation requires at least one property" if tracks.empty?
116
+ emit("v" => PROTOCOL_VERSION, "type" => "patch", "op" => "animate", "id" => node.id, "tracks" => tracks)
117
+ node
118
+ end
119
+
120
+ def tree = @surfaces.transform_values(&:to_h)
121
+ def normalize_value(value, property = nil) = Value.normalize(value, property:)
122
+
123
+ def start(output: $stdout, error: $stderr)
124
+ @output = output
125
+ @error = error
126
+ @output.sync = true if @output.respond_to?(:sync=)
127
+ @error.sync = true if @error.respond_to?(:sync=)
128
+ @running = true
129
+ pid = Object.const_defined?(:Process) && Process.respond_to?(:pid) ? Process.pid : 0
130
+ emit("v" => PROTOCOL_VERSION, "type" => "ready", "pid" => pid, "surfaces" => @surfaces.keys)
131
+ emit("v" => PROTOCOL_VERSION, "type" => "render", "components" => @components.protocol_schema,
132
+ "surfaces" => tree, "surface_options" => @surface_options)
133
+ @scheduler.start
134
+ self
135
+ end
136
+
137
+ def run(input: $stdin, output: $stdout, error: $stderr)
138
+ start(output:, error:)
139
+ input.each_line do |line|
140
+ receive(line)
141
+ rescue StandardError => exception
142
+ report_internal_error(exception)
143
+ end
144
+ ensure
145
+ stop
146
+ end
147
+
148
+ def stop
149
+ @running = false
150
+ @scheduler.stop
151
+ self
152
+ end
153
+
154
+ def schedule(kind, interval: 0, immediate: false, &block)
155
+ @scheduler.schedule(kind, interval:, immediate:, &block)
156
+ end
157
+
158
+ def receive(raw_line)
159
+ raise ProtocolError, "message exceeds #{MAX_MESSAGE_BYTES} bytes" if raw_line.bytesize > MAX_MESSAGE_BYTES
160
+ message = JSON.parse(raw_line)
161
+ if message.is_a?(Hash) && message["v"] == PROTOCOL_VERSION && message["type"] == "tick"
162
+ @scheduler.tick
163
+ return
164
+ end
165
+ validate_message!(message)
166
+ dispatch_event(message)
167
+ rescue JSON::ParserError => exception
168
+ emit_protocol_error("invalid_json", exception.message)
169
+ rescue ProtocolError => exception
170
+ emit_protocol_error("invalid_message", exception.message)
171
+ end
172
+
173
+ private
174
+
175
+ def state_changed(_name, _previous, _value)
176
+ @state_change_lock.synchronize do
177
+ @structures.dup.each do |structure|
178
+ reconcile_structure(structure) if @structures.include?(structure)
179
+ end
180
+ @bindings.dup.each { |binding| update_binding(binding) }
181
+ end
182
+ end
183
+
184
+ def update_binding(binding)
185
+ value = normalize_value(evaluate(binding.reader), binding.property)
186
+ return if value == binding.last_value
187
+ binding.last_value = value
188
+ binding.node.props[binding.property] = value
189
+ patch = {
190
+ "v" => PROTOCOL_VERSION, "type" => "patch", "op" => "set",
191
+ "id" => binding.node.id, "property" => binding.property, "value" => value
192
+ }
193
+ patch["animation"] = binding.animation.to_h if binding.animation
194
+ emit(patch)
195
+ end
196
+
197
+ def reconcile_structure(structure)
198
+ previous_children = structure.last_children
199
+ structure.node.children.dup.each { |child| unregister_subtree(child) }
200
+ structure.node.children.clear
201
+ @builder.rebuild(structure.node, &structure.renderer)
202
+ children = structure.node.children.map(&:to_h)
203
+ return if children == structure.last_children
204
+ if patchable_trees?(previous_children, children)
205
+ emit_property_patches(previous_children, children)
206
+ structure.last_children = children
207
+ return
208
+ end
209
+ structure.last_children = children
210
+ emit("v" => PROTOCOL_VERSION, "type" => "patch", "op" => "replace_children",
211
+ "id" => structure.node.id, "children" => children)
212
+ end
213
+
214
+ def patchable_trees?(previous, current)
215
+ return false unless previous.length == current.length
216
+ previous.zip(current).all? do |before, after|
217
+ before["id"] == after["id"] && before["type"] == after["type"] &&
218
+ before.fetch("events", []) == after.fetch("events", []) &&
219
+ before.fetch("props", {}).keys.sort == after.fetch("props", {}).keys.sort &&
220
+ patchable_trees?(before.fetch("children", []), after.fetch("children", []))
221
+ end
222
+ end
223
+
224
+ def emit_property_patches(previous, current)
225
+ previous.zip(current).each do |before, after|
226
+ before.fetch("props", {}).each do |property, old_value|
227
+ value = after.fetch("props", {}).fetch(property)
228
+ next if value == old_value
229
+ next if echoed_input_patch?(after.fetch("id"), property, value)
230
+ emit("v" => PROTOCOL_VERSION, "type" => "patch", "op" => "set",
231
+ "id" => after.fetch("id"), "property" => property, "value" => value)
232
+ end
233
+ emit_property_patches(before.fetch("children", []), after.fetch("children", []))
234
+ end
235
+ end
236
+
237
+ def echoed_input_patch?(control_id, property, value)
238
+ @active_event && @active_event["event"] == "input" && @active_event["id"] == control_id &&
239
+ %w[text value].include?(property) && @active_event.dig("payload", "value") == value
240
+ end
241
+
242
+ def unregister_subtree(node)
243
+ node.children.each { |child| unregister_subtree(child) }
244
+ @nodes.delete(node.id)
245
+ @bindings.delete_if { |binding| binding.node.equal?(node) }
246
+ @structures.delete_if { |structure| structure.node.equal?(node) }
247
+ @handlers.delete_if { |(control_id, _event), _handler| control_id == node.id }
248
+ end
249
+
250
+ def dispatch_event(message)
251
+ key = [message.fetch("id"), message.fetch("event")]
252
+ handler = @handlers[key]
253
+ raise ProtocolError, "unknown event target: #{key.join('/')}" unless handler
254
+ begin
255
+ @active_event = message
256
+ @builder.instance_exec(message["payload"] || {}, &handler)
257
+ ensure
258
+ @active_event = nil
259
+ end
260
+ acknowledgement = {
261
+ "v" => PROTOCOL_VERSION, "type" => "ack", "seq" => message["seq"],
262
+ "id" => message.fetch("id"), "event" => message.fetch("event")
263
+ }
264
+ acknowledgement["rss_kib"] = process_rss_kib if message.dig("payload", "diagnostics") == true
265
+ emit(acknowledgement)
266
+ rescue StandardError => exception
267
+ emit("v" => PROTOCOL_VERSION, "type" => "handler_error", "seq" => message["seq"],
268
+ "id" => message["id"], "message" => exception.message.to_s[0, 500])
269
+ @error.puts("zui handler error: #{exception.class}: #{exception.message}")
270
+ end
271
+
272
+ def validate_message!(message)
273
+ raise ProtocolError, "message must be an object" unless message.is_a?(Hash)
274
+ raise ProtocolError, "unsupported protocol version" unless message["v"] == PROTOCOL_VERSION
275
+ raise ProtocolError, "unsupported message type" unless message["type"] == "event"
276
+ raise ProtocolError, "invalid surface" unless VALID_ID.match?(message["surface"].to_s)
277
+ raise ProtocolError, "unknown surface" unless @surfaces.key?(message["surface"])
278
+ raise ProtocolError, "invalid control id" unless VALID_ID.match?(message["id"].to_s)
279
+ unless surface_contains?(@surfaces.fetch(message["surface"]), message["id"])
280
+ raise ProtocolError, "control does not belong to surface"
281
+ end
282
+ raise ProtocolError, "invalid event" unless VALID_EVENT.match?(message["event"].to_s)
283
+ unless message["payload"].nil? || message["payload"].is_a?(Hash)
284
+ raise ProtocolError, "payload must be an object"
285
+ end
286
+ raise ProtocolError, "seq must be an integer" unless message["seq"].nil? || message["seq"].is_a?(Integer)
287
+ end
288
+
289
+ def surface_contains?(node, control_id)
290
+ node.id == control_id || node.children.any? { |child| surface_contains?(child, control_id) }
291
+ end
292
+
293
+ def evaluate(callable) = @builder.instance_exec(&callable)
294
+
295
+ def normalize_props(props)
296
+ props.each_with_object({}) { |(key, value), result| result[key.to_s] = normalize_value(value, key) }
297
+ end
298
+
299
+ def validate_id!(id)
300
+ raise ArgumentError, "invalid id: #{id.inspect}" unless VALID_ID.match?(id)
301
+ end
302
+
303
+ def emit(message)
304
+ return unless @running && @output
305
+ encoded = JSON.generate(message)
306
+ raise ProtocolError, "outgoing message exceeds #{MAX_MESSAGE_BYTES} bytes" if encoded.bytesize > MAX_MESSAGE_BYTES
307
+ @write_lock.synchronize { @output.puts(encoded) }
308
+ end
309
+
310
+ def emit_protocol_error(code, message)
311
+ emit("v" => PROTOCOL_VERSION, "type" => "protocol_error", "code" => code,
312
+ "message" => message.to_s[0, 500])
313
+ end
314
+
315
+ def report_internal_error(exception)
316
+ @error.puts("zui runtime error: #{exception.class}: #{exception.message}")
317
+ emit("v" => PROTOCOL_VERSION, "type" => "runtime_error", "message" => exception.message.to_s[0, 500])
318
+ end
319
+
320
+ def process_rss_kib
321
+ line = File.read("/proc/self/status").each_line.find { |entry| entry.start_with?("VmRSS:") }
322
+ line ? line.split[1].to_i : 0
323
+ rescue SystemCallError
324
+ 0
325
+ end
326
+ end
327
+ end