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.
- checksums.yaml +7 -0
- data/Components/Builtins/Accordion.qml +208 -0
- data/Components/Builtins/Action.qml +43 -0
- data/Components/Builtins/ActionButton.qml +19 -0
- data/Components/Builtins/ActionGroup.qml +81 -0
- data/Components/Builtins/AlertDialog.qml +398 -0
- data/Components/Builtins/AnchorAnimation.qml +3 -0
- data/Components/Builtins/AnchorChanges.qml +11 -0
- data/Components/Builtins/AnimatedImage.qml +39 -0
- data/Components/Builtins/Animation.qml +3 -0
- data/Components/Builtins/AnimationController.qml +11 -0
- data/Components/Builtins/ApplicationWindow.qml +103 -0
- data/Components/Builtins/AreaChart.qml +70 -0
- data/Components/Builtins/AspectRatio.qml +30 -0
- data/Components/Builtins/Audio.qml +85 -0
- data/Components/Builtins/AudioInput.qml +10 -0
- data/Components/Builtins/AudioOutput.qml +10 -0
- data/Components/Builtins/Avatar.qml +46 -0
- data/Components/Builtins/Badge.qml +38 -0
- data/Components/Builtins/Banner.qml +133 -0
- data/Components/Builtins/BarChart.qml +67 -0
- data/Components/Builtins/BarIconButton.qml +30 -0
- data/Components/Builtins/BarIndicator.qml +26 -0
- data/Components/Builtins/Behavior.qml +13 -0
- data/Components/Builtins/Blur.qml +14 -0
- data/Components/Builtins/BorderImage.qml +38 -0
- data/Components/Builtins/BorderOverlay.qml +25 -0
- data/Components/Builtins/BottomSheet.qml +211 -0
- data/Components/Builtins/Breadcrumb.qml +169 -0
- data/Components/Builtins/BubbleChart.qml +3 -0
- data/Components/Builtins/BusyIndicator.qml +24 -0
- data/Components/Builtins/Button.qml +55 -0
- data/Components/Builtins/ButtonGroup.qml +18 -0
- data/Components/Builtins/Calendar.qml +55 -0
- data/Components/Builtins/Camera.qml +18 -0
- data/Components/Builtins/CandlestickChart.qml +3 -0
- data/Components/Builtins/Canvas.qml +159 -0
- data/Components/Builtins/CaptureSession.qml +19 -0
- data/Components/Builtins/Card.qml +23 -0
- data/Components/Builtins/Carousel.qml +72 -0
- data/Components/Builtins/Center.qml +20 -0
- data/Components/Builtins/CheckDelegate.qml +132 -0
- data/Components/Builtins/Checkbox.qml +62 -0
- data/Components/Builtins/Chip.qml +65 -0
- data/Components/Builtins/Circle.qml +35 -0
- data/Components/Builtins/Clipboard.qml +29 -0
- data/Components/Builtins/ColorAnimation.qml +3 -0
- data/Components/Builtins/ColorPicker.qml +70 -0
- data/Components/Builtins/Colorize.qml +14 -0
- data/Components/Builtins/Column.qml +17 -0
- data/Components/Builtins/ColumnLayout.qml +13 -0
- data/Components/Builtins/ConfirmDialog.qml +20 -0
- data/Components/Builtins/ConstrainedBox.qml +28 -0
- data/Components/Builtins/Container.qml +30 -0
- data/Components/Builtins/ContextMenu.qml +140 -0
- data/Components/Builtins/CursorSurface.qml +18 -0
- data/Components/Builtins/DataTable.qml +69 -0
- data/Components/Builtins/DatePicker.qml +180 -0
- data/Components/Builtins/DayOfWeekRow.qml +11 -0
- data/Components/Builtins/DelayButton.qml +47 -0
- data/Components/Builtins/DelegateModel.qml +10 -0
- data/Components/Builtins/DelegateModelGroup.qml +10 -0
- data/Components/Builtins/Dial.qml +42 -0
- data/Components/Builtins/Dialog.qml +128 -0
- data/Components/Builtins/DialogButtonBox.qml +137 -0
- data/Components/Builtins/Divider.qml +26 -0
- data/Components/Builtins/DonutChart.qml +3 -0
- data/Components/Builtins/DoubleSpinBox.qml +58 -0
- data/Components/Builtins/DragArea.qml +43 -0
- data/Components/Builtins/Drawer.qml +107 -0
- data/Components/Builtins/DropArea.qml +19 -0
- data/Components/Builtins/DropShadow.qml +16 -0
- data/Components/Builtins/Dropdown.qml +19 -0
- data/Components/Builtins/ExpansionPanel.qml +157 -0
- data/Components/Builtins/FilePicker.qml +140 -0
- data/Components/Builtins/FittedBox.qml +40 -0
- data/Components/Builtins/Flickable.qml +40 -0
- data/Components/Builtins/Flipable.qml +45 -0
- data/Components/Builtins/Flow.qml +16 -0
- data/Components/Builtins/FocusScope.qml +29 -0
- data/Components/Builtins/FolderListModel.qml +9 -0
- data/Components/Builtins/FolderPicker.qml +93 -0
- data/Components/Builtins/FontLoader.qml +21 -0
- data/Components/Builtins/FontPicker.qml +91 -0
- data/Components/Builtins/Frame.qml +71 -0
- data/Components/Builtins/FrameAnimation.qml +13 -0
- data/Components/Builtins/Gauge.qml +3 -0
- data/Components/Builtins/Glow.qml +15 -0
- data/Components/Builtins/Gradient.qml +93 -0
- data/Components/Builtins/Grid.qml +16 -0
- data/Components/Builtins/GridLayout.qml +16 -0
- data/Components/Builtins/GridView.qml +226 -0
- data/Components/Builtins/GroupBox.qml +95 -0
- data/Components/Builtins/Heatmap.qml +3 -0
- data/Components/Builtins/Histogram.qml +3 -0
- data/Components/Builtins/HorizontalHeader.qml +23 -0
- data/Components/Builtins/HorizontalHeaderDelegate.qml +39 -0
- data/Components/Builtins/HoverArea.qml +19 -0
- data/Components/Builtins/Icon.qml +16 -0
- data/Components/Builtins/Image.qml +110 -0
- data/Components/Builtins/ImageCapture.qml +14 -0
- data/Components/Builtins/ItemDelegate.qml +152 -0
- data/Components/Builtins/KeyCatcher.qml +25 -0
- data/Components/Builtins/Label.qml +43 -0
- data/Components/Builtins/LayoutItemProxy.qml +51 -0
- data/Components/Builtins/Legend.qml +86 -0
- data/Components/Builtins/Line.qml +28 -0
- data/Components/Builtins/LineChart.qml +87 -0
- data/Components/Builtins/ListModel.qml +8 -0
- data/Components/Builtins/ListView.qml +65 -0
- data/Components/Builtins/Loader.qml +33 -0
- data/Components/Builtins/Markdown.qml +23 -0
- data/Components/Builtins/MediaDevices.qml +10 -0
- data/Components/Builtins/MediaPlayer.qml +27 -0
- data/Components/Builtins/MediaRecorder.qml +16 -0
- data/Components/Builtins/Menu.qml +119 -0
- data/Components/Builtins/MenuBar.qml +128 -0
- data/Components/Builtins/MenuItem.qml +77 -0
- data/Components/Builtins/MenuSeparator.qml +30 -0
- data/Components/Builtins/MessageDialog.qml +104 -0
- data/Components/Builtins/ModalSheet.qml +222 -0
- data/Components/Builtins/ModelView3d.qml +50 -0
- data/Components/Builtins/MonthGrid.qml +23 -0
- data/Components/Builtins/MultiEffect.qml +48 -0
- data/Components/Builtins/MultiSelect.qml +23 -0
- data/Components/Builtins/NavigationRail.qml +154 -0
- data/Components/Builtins/NumberAnimation.qml +3 -0
- data/Components/Builtins/NumberField.qml +18 -0
- data/Components/Builtins/OpacityAnimator.qml +3 -0
- data/Components/Builtins/OpacityMask.qml +21 -0
- data/Components/Builtins/OpticalGlyph.qml +16 -0
- data/Components/Builtins/Page.qml +111 -0
- data/Components/Builtins/PageIndicator.qml +63 -0
- data/Components/Builtins/Pagination.qml +158 -0
- data/Components/Builtins/Pane.qml +72 -0
- data/Components/Builtins/PanelHero.qml +15 -0
- data/Components/Builtins/ParallelAnimation.qml +3 -0
- data/Components/Builtins/ParentAnimation.qml +3 -0
- data/Components/Builtins/ParentChange.qml +10 -0
- data/Components/Builtins/ParticleSystem.qml +81 -0
- data/Components/Builtins/PasswordField.qml +58 -0
- data/Components/Builtins/Path.qml +35 -0
- data/Components/Builtins/PathAnimation.qml +3 -0
- data/Components/Builtins/PauseAnimation.qml +3 -0
- data/Components/Builtins/PieChart.qml +3 -0
- data/Components/Builtins/PinchArea.qml +23 -0
- data/Components/Builtins/Popup.qml +130 -0
- data/Components/Builtins/Progress.qml +15 -0
- data/Components/Builtins/ProgressRing.qml +120 -0
- data/Components/Builtins/PropertyAction.qml +11 -0
- data/Components/Builtins/PropertyAnimation.qml +3 -0
- data/Components/Builtins/PropertyChanges.qml +10 -0
- data/Components/Builtins/RadarChart.qml +3 -0
- data/Components/Builtins/RadialGauge.qml +3 -0
- data/Components/Builtins/RadioButton.qml +46 -0
- data/Components/Builtins/RadioDelegate.qml +115 -0
- data/Components/Builtins/RadioGroup.qml +58 -0
- data/Components/Builtins/RangeSlider.qml +39 -0
- data/Components/Builtins/Rectangle.qml +23 -0
- data/Components/Builtins/RectangularShadow.qml +23 -0
- data/Components/Builtins/ReorderableList.qml +204 -0
- data/Components/Builtins/RichText.qml +23 -0
- data/Components/Builtins/RotationAnimation.qml +3 -0
- data/Components/Builtins/RotationAnimator.qml +3 -0
- data/Components/Builtins/RoundButton.qml +42 -0
- data/Components/Builtins/Row.qml +17 -0
- data/Components/Builtins/RowLayout.qml +13 -0
- data/Components/Builtins/ScaleAnimator.qml +3 -0
- data/Components/Builtins/ScatterChart.qml +3 -0
- data/Components/Builtins/ScreenCapture.qml +78 -0
- data/Components/Builtins/ScriptAction.qml +15 -0
- data/Components/Builtins/Scroll.qml +18 -0
- data/Components/Builtins/ScrollBar.qml +18 -0
- data/Components/Builtins/ScrollIndicator.qml +15 -0
- data/Components/Builtins/SearchField.qml +178 -0
- data/Components/Builtins/SearchableDropdown.qml +21 -0
- data/Components/Builtins/SectionHeader.qml +14 -0
- data/Components/Builtins/SelectableText.qml +33 -0
- data/Components/Builtins/SelectionRectangle.qml +17 -0
- data/Components/Builtins/Separator.qml +13 -0
- data/Components/Builtins/SequentialAnimation.qml +3 -0
- data/Components/Builtins/Settings.qml +10 -0
- data/Components/Builtins/ShaderEffect.qml +115 -0
- data/Components/Builtins/ShaderEffectSource.qml +61 -0
- data/Components/Builtins/Shaders/grayscale.frag +14 -0
- data/Components/Builtins/Shaders/grayscale.frag.qsb +0 -0
- data/Components/Builtins/Shaders/passthrough.frag +11 -0
- data/Components/Builtins/Shaders/passthrough.frag.qsb +0 -0
- data/Components/Builtins/Shaders/pixelate.frag +16 -0
- data/Components/Builtins/Shaders/pixelate.frag.qsb +0 -0
- data/Components/Builtins/Shaders/vignette.frag +18 -0
- data/Components/Builtins/Shaders/vignette.frag.qsb +0 -0
- data/Components/Builtins/Shaders/wave.frag +18 -0
- data/Components/Builtins/Shaders/wave.frag.qsb +0 -0
- data/Components/Builtins/Shape.qml +53 -0
- data/Components/Builtins/Skeleton.qml +78 -0
- data/Components/Builtins/Slider.qml +21 -0
- data/Components/Builtins/SmoothedAnimation.qml +3 -0
- data/Components/Builtins/Snackbar.qml +181 -0
- data/Components/Builtins/SortFilterProxyModel.qml +16 -0
- data/Components/Builtins/SoundEffect.qml +10 -0
- data/Components/Builtins/Spacer.qml +13 -0
- data/Components/Builtins/Sparkline.qml +3 -0
- data/Components/Builtins/SpinBox.qml +44 -0
- data/Components/Builtins/SplitView.qml +31 -0
- data/Components/Builtins/SpringAnimation.qml +3 -0
- data/Components/Builtins/Stack.qml +14 -0
- data/Components/Builtins/StackLayout.qml +19 -0
- data/Components/Builtins/StackView.qml +122 -0
- data/Components/Builtins/StackedBarChart.qml +3 -0
- data/Components/Builtins/StandardPaths.qml +9 -0
- data/Components/Builtins/State.qml +13 -0
- data/Components/Builtins/StateGroup.qml +11 -0
- data/Components/Builtins/Support/AnimationDriver.qml +79 -0
- data/Components/Builtins/Support/AnimationGroupDriver.qml +32 -0
- data/Components/Builtins/Support/ChartCanvas.qml +629 -0
- data/Components/Builtins/Support/ModelView3dScene.qml +292 -0
- data/Components/Builtins/Support/OptionalModuleState.js +5 -0
- data/Components/Builtins/SwipeDelegate.qml +231 -0
- data/Components/Builtins/SwipeView.qml +69 -0
- data/Components/Builtins/SwitchDelegate.qml +140 -0
- data/Components/Builtins/TabBar.qml +117 -0
- data/Components/Builtins/TabButton.qml +105 -0
- data/Components/Builtins/TableView.qml +388 -0
- data/Components/Builtins/TableViewDelegate.qml +17 -0
- data/Components/Builtins/Tabs.qml +138 -0
- data/Components/Builtins/Text.qml +24 -0
- data/Components/Builtins/TextArea.qml +59 -0
- data/Components/Builtins/TextField.qml +21 -0
- data/Components/Builtins/TextMetrics.qml +39 -0
- data/Components/Builtins/TimePicker.qml +213 -0
- data/Components/Builtins/Timer.qml +13 -0
- data/Components/Builtins/Toast.qml +213 -0
- data/Components/Builtins/Toggle.qml +19 -0
- data/Components/Builtins/ToggleSwitch.qml +22 -0
- data/Components/Builtins/ToolBar.qml +61 -0
- data/Components/Builtins/ToolButton.qml +41 -0
- data/Components/Builtins/ToolSeparator.qml +36 -0
- data/Components/Builtins/Tooltip.qml +20 -0
- data/Components/Builtins/Transition.qml +3 -0
- data/Components/Builtins/TreeView.qml +423 -0
- data/Components/Builtins/TreeViewDelegate.qml +46 -0
- data/Components/Builtins/Tumbler.qml +18 -0
- data/Components/Builtins/UniformAnimator.qml +3 -0
- data/Components/Builtins/VectorAnimation.qml +3 -0
- data/Components/Builtins/VectorImage.qml +62 -0
- data/Components/Builtins/VerticalHeader.qml +21 -0
- data/Components/Builtins/VerticalHeaderDelegate.qml +35 -0
- data/Components/Builtins/Video.qml +54 -0
- data/Components/Builtins/VideoOutput.qml +54 -0
- data/Components/Builtins/WebView.qml +57 -0
- data/Components/Builtins/WeekNumberColumn.qml +13 -0
- data/Components/Builtins/WidgetButton.qml +26 -0
- data/Components/Builtins/Window.qml +90 -0
- data/Components/Builtins/WindowCapture.qml +11 -0
- data/Components/Builtins/Wrap.qml +18 -0
- data/Components/Builtins/XAnimator.qml +3 -0
- data/Components/Builtins/YAnimator.qml +3 -0
- data/Components/README.md +45 -0
- data/ControlNode.qml +1369 -0
- data/Controls/BarIconButton.qml +27 -0
- data/Controls/BarIndicator.qml +22 -0
- data/Controls/BorderOverlay.qml +10 -0
- data/Controls/BorderSurface.qml +10 -0
- data/Controls/Button.qml +78 -0
- data/Controls/ButtonGroup.qml +19 -0
- data/Controls/ConfirmDialog.qml +37 -0
- data/Controls/CursorSurface.qml +29 -0
- data/Controls/Dropdown.qml +59 -0
- data/Controls/MultiSelect.qml +32 -0
- data/Controls/NumberField.qml +24 -0
- data/Controls/OpticalGlyph.qml +14 -0
- data/Controls/PanelActionButton.qml +10 -0
- data/Controls/PanelHero.qml +18 -0
- data/Controls/PanelKeyCatcher.qml +25 -0
- data/Controls/PanelSectionHeader.qml +11 -0
- data/Controls/PanelSeparator.qml +10 -0
- data/Controls/PanelSlider.qml +36 -0
- data/Controls/PanelToolTip.qml +13 -0
- data/Controls/SearchableDropdown.qml +11 -0
- data/Controls/TextField.qml +25 -0
- data/Controls/Toggle.qml +22 -0
- data/Controls/ToggleSwitch.qml +26 -0
- data/Controls/WidgetButton.qml +35 -0
- data/Controls/qmldir +25 -0
- data/Desktop.qml +76 -0
- data/LICENSE +22 -0
- data/README.md +112 -0
- data/Service.qml +430 -0
- data/Theme/Border.qml +22 -0
- data/Theme/Color.qml +22 -0
- data/Theme/Style.qml +47 -0
- data/Theme/Util.qml +6 -0
- data/Theme/qmldir +5 -0
- data/bin/zui +7 -0
- data/lib/zui/animation.rb +24 -0
- data/lib/zui/application.rb +327 -0
- data/lib/zui/builder.rb +725 -0
- data/lib/zui/cli.rb +91 -0
- data/lib/zui/command.rb +101 -0
- data/lib/zui/component_registry.rb +91 -0
- data/lib/zui/components.rb +262 -0
- data/lib/zui/distribution.rb +200 -0
- data/lib/zui/generator.rb +84 -0
- data/lib/zui/host.rb +123 -0
- data/lib/zui/node.rb +30 -0
- data/lib/zui/platform.rb +40 -0
- data/lib/zui/protocol.rb +29 -0
- data/lib/zui/runner.rb +37 -0
- data/lib/zui/runtime.rb +23 -0
- data/lib/zui/scheduler.rb +136 -0
- data/lib/zui/source_bundle.rb +63 -0
- data/lib/zui/state_store.rb +100 -0
- data/lib/zui/value.rb +44 -0
- data/lib/zui.rb +33 -0
- data/native/CMakeLists.txt +45 -0
- data/native/ZuiClipboard.cpp +16 -0
- data/native/ZuiClipboard.h +16 -0
- data/native/ZuiProcess.cpp +84 -0
- data/native/ZuiProcess.h +34 -0
- data/native/main.cpp +64 -0
- data/vendor/host/linux-x86_64/PROVENANCE.md +15 -0
- data/vendor/host/linux-x86_64/zui-host +0 -0
- data/vendor/host/linux-x86_64/zui-host.sha256 +1 -0
- 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
data/Theme/qmldir
ADDED
data/bin/zui
ADDED
|
@@ -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
|