zaniah 0.10.0 → 0.11.0
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 +4 -4
- data/CHANGELOG.md +6 -0
- data/docs/adr/021-dock-layout-tree.md +33 -0
- data/docs/components.md +30 -0
- data/docs/gallery/components-dark.png +0 -0
- data/docs/gallery/components-high_contrast.png +0 -0
- data/docs/gallery/components-light.png +0 -0
- data/docs/native.md +35 -0
- data/docs/tui.md +7 -0
- data/examples/gallery.rb +32 -6
- data/lib/zaniah/clipboard.rb +23 -0
- data/lib/zaniah/element.rb +47 -2
- data/lib/zaniah/input/data_drop.rb +25 -0
- data/lib/zaniah/input/dispatcher.rb +1 -0
- data/lib/zaniah/input/keymap.rb +21 -0
- data/lib/zaniah/input/magnify.rb +7 -0
- data/lib/zaniah/input.rb +2 -0
- data/lib/zaniah/platform/headless/window.rb +70 -2
- data/lib/zaniah/platform/linux/wayland_window.rb +81 -11
- data/lib/zaniah/platform/linux/window.rb +158 -15
- data/lib/zaniah/platform/mac/window.rb +118 -11
- data/lib/zaniah/platform/windows/drag_drop.rb +273 -0
- data/lib/zaniah/platform/windows/window.rb +21 -2
- data/lib/zaniah/ui/calendar.rb +215 -0
- data/lib/zaniah/ui/dock_layout.rb +102 -0
- data/lib/zaniah/ui/dock_workspace.rb +271 -0
- data/lib/zaniah/ui/feedback.rb +122 -0
- data/lib/zaniah/ui/hover_card.rb +95 -0
- data/lib/zaniah/ui/property_grid.rb +154 -0
- data/lib/zaniah/ui/zoom_pan_view.rb +106 -0
- data/lib/zaniah/ui.rb +7 -0
- data/lib/zaniah/version.rb +1 -1
- data/sig/clipboard.rbs +20 -0
- data/sig/elements.rbs +5 -0
- data/sig/input.rbs +28 -4
- data/sig/ui.rbs +83 -0
- data/tools/generate_component_gallery.rb +2 -1
- metadata +12 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3a6d77e1edea911ee42064986e375f163213f45bedaeda9c6f3e409b42914cbd
|
|
4
|
+
data.tar.gz: 21970a583f79a01502abf982811820361a7628dc0ffffca0a2f308fa4852a6b7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 83d536f11608784994079181c05524fb3935cddf6076728967b78fc54b48ae91889c387efc047159f767dac8743ae5cb89f7c3497f75afa7b733133f6bc7e982
|
|
7
|
+
data.tar.gz: caa3b692cbe5435abc9571b0ccc3a09dc46db71c022a0ab2f80903dab7336c3818c3ae05ecc3ee6a0aa8ba657fb45bfa61a5218ec236546f229ba9d80614ace2
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.11.0 — 2026-09-25
|
|
4
|
+
|
|
5
|
+
- Add segmented controls, alerts, hover cards, calendar and date-range inputs.
|
|
6
|
+
- Add zoomable views, dockable workspaces, and editable property grids.
|
|
7
|
+
- Add typed drag-and-drop across desktop backends with explicit move completion.
|
|
8
|
+
|
|
3
9
|
## 0.10.0 — 2026-09-24
|
|
4
10
|
|
|
5
11
|
- Add Unicode bidirectional text layout, caret affinity, and Arabic joining.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# ADR 021: Represent dock layouts as a validated tree
|
|
2
|
+
|
|
3
|
+
- Status: Accepted
|
|
4
|
+
- Date: 2026-09-24
|
|
5
|
+
|
|
6
|
+
## Context
|
|
7
|
+
|
|
8
|
+
An application needs to save tabbed, split workspaces and restore them across
|
|
9
|
+
sessions. Dragging a tab changes both its group and potentially the split tree.
|
|
10
|
+
A fixed five-region dock cannot represent nested splits or stable panel identity.
|
|
11
|
+
|
|
12
|
+
## Decision
|
|
13
|
+
|
|
14
|
+
`UI::DockLayout` stores tab groups and binary splits with stable string IDs.
|
|
15
|
+
`to_h` and `from_h` provide a plain-data round trip; parsing rejects duplicate
|
|
16
|
+
IDs, empty groups, invalid active tabs, invalid orientations, and out-of-range
|
|
17
|
+
ratios. `UI::DockWorkspace` derives visible panes from this model and reuses
|
|
18
|
+
`UI::SplitPane` for dividers. Layout changes produce a new validated model.
|
|
19
|
+
The application owns persistence and handles `on_detach` notifications; the
|
|
20
|
+
component does not create windows.
|
|
21
|
+
|
|
22
|
+
## Alternatives considered
|
|
23
|
+
|
|
24
|
+
- A flat list of panels plus coordinates would make nested resizing and
|
|
25
|
+
restoration ambiguous.
|
|
26
|
+
- Extending `DockPanel` would change its simple five-region contract.
|
|
27
|
+
- Making detach automatically create windows would assume application policy.
|
|
28
|
+
|
|
29
|
+
## Consequences
|
|
30
|
+
|
|
31
|
+
Persisted layouts can be validated independently of rendering. Empty groups
|
|
32
|
+
are pruned after a move. Applications can choose their own storage format and
|
|
33
|
+
window ownership; they must keep panel IDs stable across releases.
|
data/docs/components.md
CHANGED
|
@@ -32,6 +32,7 @@ Zaniah::UI::Button.variants[:variant][:brand] = ->(theme) {
|
|
|
32
32
|
| L0 | `Card` | `(*children)`, `child` | theme surface | group |
|
|
33
33
|
| L0 | `Badge` | `(text, variant:)` | neutral/accent/success/warning/danger | text |
|
|
34
34
|
| L0 | `Kbd` | `(keys, platform:)`; `.for(action, keymap:)` | OS shortcut notation, including multi-stroke bindings | text |
|
|
35
|
+
| L0 | `Alert` | `(title, message:, variant:, action:, dismissible:, live:)`; `dismiss` | info/success/warning/danger; persistent until dismissed | alert when live, otherwise status |
|
|
35
36
|
| L0 | `Avatar` | `(name, image:, size:)` | initials or PNG | image |
|
|
36
37
|
| L0 | `Skeleton` | `(width:, height:)` | pulsing loading placeholder | progressbar/busy |
|
|
37
38
|
| L0 | `EmptyState` | `(title, message:, icon:, action:)` | compositional | group |
|
|
@@ -42,6 +43,7 @@ Zaniah::UI::Button.variants[:variant][:brand] = ->(theme) {
|
|
|
42
43
|
| L1 | `Checkbox` | `(label, value:, disabled:)`; `on_change` | true/false/mixed | checkbox |
|
|
43
44
|
| L1 | `Radio` | `(label, value:, disabled:)`; `on_change` | selected/unselected | radio |
|
|
44
45
|
| L1 | `RadioGroup` | `(options, value:)`; `on_change` | one selected value | radiogroup |
|
|
46
|
+
| L1 | `SegmentedControl` | `(options, value:)`; `on_change` | mutually exclusive segments; arrows/Home/End | radiogroup/radio |
|
|
45
47
|
| L1 | `Switch` | `(label, value:, disabled:)`; `on_change` | on/off | switch |
|
|
46
48
|
| L1 | `Slider` | `(value:, min:, max:, step:, label:)`; `on_change` | pointer + arrow/Home/End/Page keys | slider |
|
|
47
49
|
| L1 | `RangeSlider` | `(value: [low, high], ...)` | two thumbs | slider |
|
|
@@ -50,6 +52,7 @@ Zaniah::UI::Button.variants[:variant][:brand] = ->(theme) {
|
|
|
50
52
|
| L1 | `Meter` | `(value:, low:, high:, optimum:)` | thresholds | meter |
|
|
51
53
|
| L2 | `Tooltip` | `(text, anchor:, side:, open:)` | top/bottom/left/right | tooltip |
|
|
52
54
|
| L2 | `Popover` | `(content, anchor:, side:, width:, height:, open:, modal:)` | flipped and viewport-clamped | group |
|
|
55
|
+
| L2 | `HoverCard` | `(content, anchor:, open_delay:, close_delay:)` | hover or focus opens after a delay; non-modal | dialog |
|
|
53
56
|
| L2 | `ContextMenu`, `Menu` | `(items, anchor:, open:)` | pointer + arrows/Home/End/Enter/Esc | menu/menuitem |
|
|
54
57
|
| L2 | `MenuBar` | `(menus)` or `.from(app.menu_bar)` | declarative menu model or legacy pairs | menubar |
|
|
55
58
|
| L2 | `Dropdown` | `(label, items:, value:)`; `on_change` | menu-backed | button |
|
|
@@ -63,6 +66,8 @@ Zaniah::UI::Button.variants[:variant][:brand] = ->(theme) {
|
|
|
63
66
|
| L2 | `Combobox` | `(items, value:, label:, placeholder:, disabled:, matcher:)`; `on_change` | editable, filtered choices with highlighted matches | combobox |
|
|
64
67
|
| L2 | `MultiSelect` | `(items, value:, label:, disabled:)`; `on_change` | multiple selected badges | listbox |
|
|
65
68
|
| L2 | `DatePicker` | `(value, min:, max:, label:, disabled:)`; `on_change` | ISO date, day/week keyboard steps | combobox |
|
|
69
|
+
| L2 | `Calendar` | `(value:, min:, max:, range:, week_start:, month_names:)`; `on_change` | arrow keys move days/weeks, Page keys move months, Enter selects | grid/gridcell |
|
|
70
|
+
| L2 | `DateRangePicker` | `(value:, min:, max:, week_start:, month_names:)`; `on_change` | Calendar-backed start/end selection | combobox |
|
|
66
71
|
| L2 | `TimePicker` | `(value, step:, label:, disabled:)`; `on_change` | 24-hour time, minute/hour keyboard steps | combobox |
|
|
67
72
|
| L2 | `ColorPicker` | `(value, label:, swatches:, disabled:)`; `on_change` | hex input and swatches | combobox |
|
|
68
73
|
| L3 | `Tabs` | `(items, selected:)`; `on_change` | arrows/Home/End | tab/tabpanel |
|
|
@@ -82,11 +87,14 @@ Zaniah::UI::Button.variants[:variant][:brand] = ->(theme) {
|
|
|
82
87
|
| L3 | `SplitPane` | `(first, second, orientation:, ratio:, min:, max:)`; `on_change` | horizontal/vertical, draggable separator | group/separator |
|
|
83
88
|
| L3 | `PaneGrid` | `(panes, columns:, rows:, divider_size:, minimum:, keyboard_step:)`; `replace`, `on_resize` | arbitrary resizable grid, stable pane IDs | group/separator |
|
|
84
89
|
| L3 | `Resizable` | `(content, width:, height:, min_width:, min_height:, max_width:, max_height:)`; `on_resize` | drag or keyboard resize | group/separator |
|
|
90
|
+
| L3 | `ZoomPanView` | `(content, zoom:, min_zoom:, max_zoom:)`; `fit`, `zoom_to`, `view_to_content` | drag to pan, pinch or Ctrl+wheel to zoom, `+`/`-`/`0` when focused | group |
|
|
85
91
|
| L3 | `DockPanel` | `(center:, top:, right:, bottom:, left:)` | five-region layout | group |
|
|
92
|
+
| L3 | `DockWorkspace` | `(layout, render:)`; `on_layout_change`, `on_detach`; `DockLayout.to_h`/`.from_h` | drag tabs to move/split, arrows select, Alt+arrows move, Ctrl+Shift+arrows split, Ctrl+Shift+D requests detach | tablist/tab/tabpanel/separator |
|
|
86
93
|
| L3 | `ListView` | `(items, height:, row_height:, selected:)`; `on_select` | virtual rows and keyboard selection | list/listitem |
|
|
87
94
|
| L4 | `Table`, `DataGrid` | `(rows, columns:, height:, selection:, row_key:)`; `on_sort`, `on_select`, `on_edit`, `on_copy`, `on_paste` | virtual rows, sorting, resizing, editing, typed clipboard hooks | table/row/cell |
|
|
88
95
|
| L4 | `Grid` | `(rows:, columns:, row_height:, column_width:, frozen_rows:, frozen_columns:)`; `scroll_to`, range `selection`, `on_select`, `on_edit`, `on_fill`, `on_resize`, `on_copy`, `on_paste` | two-axis virtualization, frozen panes, visible-cell resize/fill and typed clipboard hooks | table |
|
|
89
96
|
| L4 | `TreeView` | `(items, height:, selected:)`; `expand`, `collapse`, `replace`, `replace_children`, `invalidate`, lazy `children` proc | arrows/Home/End | tree/treeitem |
|
|
97
|
+
| L4 | `PropertyGrid` | `(schema, values, height:, row_height:)`; `on_change`, `set` | typed existing controls, `Validation`, virtual rows | table/row/cell |
|
|
90
98
|
| L5 | `Sparkline` | `(values, width:, height:, color:, label:)` | line + tooltip | image |
|
|
91
99
|
| L5 | `LineChart`, `BarChart`, `StackedBarChart`, `AreaChart` | `(series, width:, height:, colors:, label:)`; `AreaChart(stacked:)` | shared axes, ticks, grid lines, color-keyed legend, tooltip | image |
|
|
92
100
|
| L5 | `PieChart`, `DonutChart` | `(data, width:, height:, colors:, label:)` | slices, shared color-keyed legend, tooltip | image |
|
|
@@ -101,6 +109,28 @@ All input components are keyboard operable. Disabled controls remain visible but
|
|
|
101
109
|
removed from focus traversal. Overlay components close on Esc; modal overlays restore
|
|
102
110
|
the previous focus. See [TUI](tui.md) for terminal representations.
|
|
103
111
|
|
|
112
|
+
`Calendar` and `DateRangePicker` use Ruby's `Date` and ISO 8601 strings. Pass
|
|
113
|
+
`week_start: 0..6` (Sunday is 0) and twelve `month_names:` to localize the grid
|
|
114
|
+
without an i18n dependency. A range picker returns a two-element array of dates;
|
|
115
|
+
its second value is `nil` while the user is choosing the end. `HoverCard` accepts
|
|
116
|
+
an element/component as its anchor to open on pointer hover or keyboard focus, or
|
|
117
|
+
a `Point`/`Bounds` for positioned use. Its delays follow the window's injected clock.
|
|
118
|
+
|
|
119
|
+
`ZoomPanView` uses content-local coordinates for `zoom_to` and `view_to_content`.
|
|
120
|
+
Call `fit` after the first layout; it scales the content into the current viewport.
|
|
121
|
+
Native macOS pinch emits `Input::Magnify`, while Ctrl+wheel provides a desktop fallback.
|
|
122
|
+
|
|
123
|
+
`DockLayout` is a validated tree of tab groups and horizontal/vertical splits with
|
|
124
|
+
stable string IDs. Use `DockLayout.tabs` and `.split` to build it, and persist
|
|
125
|
+
`layout.to_h` in the application; `DockLayout.from_h` restores it. `DockWorkspace`
|
|
126
|
+
passes the active panel ID to `render:` and emits a new layout on tab changes.
|
|
127
|
+
`on_detach` is a notification only; the application decides whether to open a
|
|
128
|
+
window and remove the panel. A pointer drop in the outer 20% of a group splits it.
|
|
129
|
+
`PropertyGrid` schema entries use `key`, optional `label`, `type` (`text`,
|
|
130
|
+
`textarea`, `number`, `boolean`, `select`, `color`, `date`, `time`), `options` for
|
|
131
|
+
select, and an optional `Validation`. Invalid edits remain in the control while
|
|
132
|
+
the last valid value is retained. Both components expose terminal fallbacks.
|
|
133
|
+
|
|
104
134
|
`Combobox` and `CommandPalette` default to case-insensitive substring matching in
|
|
105
135
|
input order. Pass `matcher:` to either component, or set a default with
|
|
106
136
|
`Zaniah.configure { |config| config.matcher = matcher }`. A matcher implements
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
data/docs/native.md
CHANGED
|
@@ -70,6 +70,41 @@ applications. macOS also preserves arbitrary MIME types under reversible
|
|
|
70
70
|
Wayland use their native format mechanisms. Native clipboard exchange still
|
|
71
71
|
depends on a running desktop session and should be checked on each target OS.
|
|
72
72
|
|
|
73
|
+
## Typed drag and drop
|
|
74
|
+
|
|
75
|
+
An element can declare the MIME types it accepts and optionally offer a drag
|
|
76
|
+
source. `on_drag_over` returns `:copy`, `:move`, `:link`, or `:none`; the chosen
|
|
77
|
+
operation must be offered by the source. A drop carries `Clipboard::Content`,
|
|
78
|
+
its logical window position, and the operation. File drops also emit the older
|
|
79
|
+
`Input::FileDrop` event so existing listeners continue to work.
|
|
80
|
+
Offering `:move` requires `on_move:` to remove the source data; it runs once
|
|
81
|
+
after a successful move, or when an X11 target acknowledges the `DELETE` selection.
|
|
82
|
+
|
|
83
|
+
```ruby
|
|
84
|
+
target.on_drop(types: ["image/png", "text/plain"]) do |drop, _cx|
|
|
85
|
+
insert(drop.content)
|
|
86
|
+
end
|
|
87
|
+
target.on_drag_over { |drag, _cx| drag.accepts?("image/png") ? :copy : :none }
|
|
88
|
+
source.draggable do |_mouse, _cx|
|
|
89
|
+
Zaniah::DragData.new(items: [Zaniah::Clipboard::Item.new("text/plain" => "hello")], operations: [:copy])
|
|
90
|
+
end
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Headless records the source in `drag_history`; tests may call `drag_over` and
|
|
94
|
+
`complete_drag` to deliver it deterministically. macOS uses AppKit dragging
|
|
95
|
+
sessions; X11 uses XDND (Ctrl requests copy, Shift move, Ctrl+Shift link when
|
|
96
|
+
offered); Wayland uses `wl_data_device`. A target needs `on_drop` and a matching
|
|
97
|
+
MIME type before it can accept a drag. Wayland cannot offer
|
|
98
|
+
link-only drags because its standard data-device action set has only copy and
|
|
99
|
+
move. All desktop protocols depend on another application accepting the MIME
|
|
100
|
+
type and operation. Windows currently preserves file drops through
|
|
101
|
+
`WM_DROPFILES` compatibility and also uses OLE `IDataObject`/`IDropTarget` and
|
|
102
|
+
`IDropSource` for typed data and native drag sources. A preview image is used
|
|
103
|
+
on macOS; Windows, X11, and Wayland currently use the native default cursor
|
|
104
|
+
without a custom preview. TUI has no cross-process drag protocol and only the
|
|
105
|
+
in-process Headless test path.
|
|
106
|
+
X11 XDND proxy windows are not supported.
|
|
107
|
+
|
|
73
108
|
## Window state and decorations
|
|
74
109
|
|
|
75
110
|
`window.frame` uses logical screen coordinates; `window.state.to_h` can be saved
|
data/docs/tui.md
CHANGED
|
@@ -8,25 +8,32 @@ tests and non-window integrations.
|
|
|
8
8
|
| --- | --- |
|
|
9
9
|
| Label, Badge, Avatar | text, `[badge]`, `(initials)` |
|
|
10
10
|
| Kbd | textual shortcut, such as `Ctrl+Shift+P`; multiple strokes are space-separated |
|
|
11
|
+
| Alert | persistent `! title: message`; dismissed alerts disappear, live alerts announce through accessibility |
|
|
11
12
|
| Icon | Unicode check/close/search/menu/info/warning symbol; unlabeled unknown icons are omitted |
|
|
12
13
|
| Divider, Card | `─`/`│` and box-drawing characters |
|
|
13
14
|
| Skeleton, Spinner, ProgressBar, Meter | shade cells, `◌`, or a ten-cell progress track; animation jumps to its terminal state |
|
|
14
15
|
| Button, IconButton, ToggleButton | `[ label ]`; disabled uses `( label )` |
|
|
15
16
|
| Checkbox, Radio, Switch | `[x]`, `(o)`, `[on ]` |
|
|
17
|
+
| SegmentedControl | `[A\|(B)\|C]`, with the selected segment in parentheses; arrows move selection |
|
|
16
18
|
| Slider, RangeSlider | ten-cell track and numeric value/range |
|
|
17
19
|
| Text inputs | `[value]`; password values are masked and validation errors have `!` |
|
|
18
20
|
| Select, Combobox, MultiSelect | labeled brackets, filtered menu marker, or comma-separated selected values |
|
|
19
21
|
| DatePicker, TimePicker, ColorPicker | labeled ISO date, 24-hour time, or hex color in brackets |
|
|
22
|
+
| Calendar, DateRangePicker | month grid with selected day in `[dd]`; picker shows `[start – end]`, with `…` for an unfinished range |
|
|
20
23
|
| Tooltip, Popover, Menu, MenuBar, Dropdown | status text or a box/menu with `>` selection marker; nested menus expose a Back row |
|
|
24
|
+
| HoverCard | content text while open; hidden while closed; focus uses the same open delay as hover |
|
|
21
25
|
| Tabs, Accordion, Collapsible | selected tab in brackets and `[+]`/`[-]` disclosure markers |
|
|
22
26
|
| ScrollView, Scrollbar | clipped child content and `│`/`─` track |
|
|
23
27
|
| Breadcrumb, Pagination, bars | slash-separated path, page status, space-separated content |
|
|
24
28
|
| Modal, Dialog, Drawer, CommandPalette | box-drawing overlay; palette filters results and supports Up/Down/Enter; focus stays inside until dismissed |
|
|
25
29
|
| SplitPane, PaneGrid, Resizable, DockPanel | `│`/`─` separators, resize corner, or ordered dock regions |
|
|
30
|
+
| ZoomPanView | zoom percentage and content text; focused `+`/`-`/`0` changes zoom, but pointer panning has no cell equivalent |
|
|
31
|
+
| DockWorkspace | active tab in parentheses inside `[tabs]`, split groups separated by `│` or `───`; arrows select, Alt+arrows move and Ctrl+Shift+arrows split without dragging |
|
|
26
32
|
| ListView | visible rows with `>` on the selected item |
|
|
27
33
|
| Toast | live status text |
|
|
28
34
|
| Table, DataGrid | header and visible rows separated with `|` |
|
|
29
35
|
| TreeView | indentation with `▸`/`▾` expansion markers |
|
|
36
|
+
| PropertyGrid | up to 20 `label: value` rows, with `!` and the validation message on errors; Up/Down/Page keys move the virtual row selection |
|
|
30
37
|
| Sparkline and all chart types | eight-level Unicode sparkline summary |
|
|
31
38
|
| Form, FormField | labeled values, `!` errors, and a submit button |
|
|
32
39
|
| CodeEditor | numbered logical lines; wrap, syntax colors, and inline IME underline degrade to plain cells |
|
data/examples/gallery.rb
CHANGED
|
@@ -57,19 +57,43 @@ module Gallery
|
|
|
57
57
|
.child(Zaniah::Div.new.flex_row.gap(12).children([
|
|
58
58
|
Zaniah::UI::DatePicker.new("2026-09-13"), Zaniah::UI::TimePicker.new("14:30"),
|
|
59
59
|
Zaniah::UI::ColorPicker.new("#2563eb")]))
|
|
60
|
+
feedback_dates = Zaniah::Div.new.w_full.gap(12)
|
|
61
|
+
.child(Zaniah::Div.new.flex_row.items_center.gap(16).children([
|
|
62
|
+
Zaniah::UI::SegmentedControl.new([["Day", :day], ["Week", :week], ["Month", :month]], value: :week),
|
|
63
|
+
Zaniah::UI::DateRangePicker.new(value: ["2026-09-01", "2026-09-30"])]))
|
|
64
|
+
.child(Zaniah::UI::Alert.new("Export complete", message: "Your file is ready", variant: :success, dismissible: true))
|
|
65
|
+
.child(Zaniah::UI::Calendar.new(value: "2026-09-24", week_start: 1))
|
|
66
|
+
dock_layout = Zaniah::UI::DockLayout.split(id: :work, orientation: :horizontal, ratio: 0.42,
|
|
67
|
+
first: Zaniah::UI::DockLayout.tabs(id: :left, panels: %i[Files Search]),
|
|
68
|
+
second: Zaniah::UI::DockLayout.tabs(id: :right, panels: %i[Editor Preview]))
|
|
69
|
+
property_schema = [
|
|
70
|
+
{key: :title, label: "Title", type: :text},
|
|
71
|
+
{key: :count, label: "Count", type: :number},
|
|
72
|
+
{key: :visible, label: "Visible", type: :boolean}
|
|
73
|
+
]
|
|
60
74
|
workspaces = Zaniah::Div.new.w_full.gap(12)
|
|
61
75
|
.child(Zaniah::Div.new.flex_row.gap(12).children([
|
|
62
76
|
Zaniah::UI::SplitPane.new(Zaniah::UI::Label.new("Left pane"), Zaniah::UI::Label.new("Right pane")).w(300).h(100),
|
|
63
77
|
Zaniah::UI::Resizable.new(Zaniah::UI::Label.new("Resize me"), width: 180, height: 100),
|
|
64
78
|
Zaniah::UI::ListView.new(%w[Alpha Beta Gamma Delta], height: 100, selected: 1).w(180)]))
|
|
65
|
-
.child(Zaniah::
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
79
|
+
.child(Zaniah::Div.new.flex_row.gap(12).children([
|
|
80
|
+
Zaniah::UI::DockPanel.new(
|
|
81
|
+
center: Zaniah::UI::Label.new("Editor"), top: Zaniah::UI::StatusBar.new(Zaniah::UI::Label.new("Top", size: :sm)),
|
|
82
|
+
left: Zaniah::UI::Sidebar.new(Zaniah::UI::Label.new("Files", size: :sm), width: 90),
|
|
83
|
+
bottom: Zaniah::UI::StatusBar.new(Zaniah::UI::Label.new("Ready", size: :sm))).w(420).h(120),
|
|
84
|
+
Zaniah::UI::ZoomPanView.new(Zaniah::Div.new.w(110).h(70).p(8).bg("#2563eb")
|
|
85
|
+
.child(Zaniah::UI::Label.new("Zoom / pan", size: :sm)), zoom: 1.3).w(180).h(120)
|
|
86
|
+
]))
|
|
87
|
+
.child(Zaniah::Div.new.flex_row.gap(12).children([
|
|
88
|
+
Zaniah::UI::DockWorkspace.new(dock_layout,
|
|
89
|
+
render: ->(id) { Zaniah::Div.new.p(10).child(Zaniah::UI::Label.new("#{id} panel")) }).w(440).h(190),
|
|
90
|
+
Zaniah::UI::PropertyGrid.new(property_schema,
|
|
91
|
+
{title: "Workspace", count: 3, visible: true}, height: 190).w(340)
|
|
92
|
+
]))
|
|
69
93
|
editors = Zaniah::Div.new.w_full.flex_row.gap(12).children([
|
|
70
94
|
Zaniah::UI::CodeEditor.new("def hello\n :world\nend\n", language: :ruby).w(390).h(150),
|
|
71
95
|
Zaniah::UI::RichText.new([{text: "Rich ", size: 20}, {text: "text", color: "#2563eb", size: 20}]).w(260)])
|
|
72
|
-
Zaniah::Div.new.h(
|
|
96
|
+
Zaniah::Div.new.h(3200).p(16).gap(12)
|
|
73
97
|
.child(section("Foundation", foundation, height: 180))
|
|
74
98
|
.child(section("Actions",
|
|
75
99
|
Zaniah::UI::Button.new("Primary").icon(:check).on_click(&clicks),
|
|
@@ -94,7 +118,8 @@ module Gallery
|
|
|
94
118
|
.child(section("Variants", variants, height: 285))
|
|
95
119
|
.child(section("Data", data, height: 440))
|
|
96
120
|
.child(section("Choice and pickers", choices, height: 210))
|
|
97
|
-
.child(section("
|
|
121
|
+
.child(section("Feedback and dates", feedback_dates, height: 410))
|
|
122
|
+
.child(section("Workspace layout", workspaces, height: 530))
|
|
98
123
|
.child(section("Editors", editors, height: 210))
|
|
99
124
|
.child(section("Forms", Zaniah::UI::Form.new.field(name: :email, label: "Release email", value: "ruby@example.com", validation: validation).w(320), height: 150))
|
|
100
125
|
end
|
|
@@ -110,6 +135,7 @@ module Gallery
|
|
|
110
135
|
when /\Atoast-(info|success|warning|danger)\z/ then Zaniah::UI::Toast.new("Saved", variant: Regexp.last_match(1).to_sym)
|
|
111
136
|
when "tooltip" then Zaniah::UI::Tooltip.new("Helpful text", anchor: anchor)
|
|
112
137
|
when "popover" then Zaniah::UI::Popover.new(content, anchor: anchor)
|
|
138
|
+
when "hover-card" then Zaniah::UI::HoverCard.new(Zaniah::UI::Label.new("Keyboard shortcuts"), anchor: anchor).open
|
|
113
139
|
when "menu" then Zaniah::UI::Menu.new(items, anchor: Zaniah::Point.new(anchor.x, anchor.bottom))
|
|
114
140
|
when "context-menu" then Zaniah::UI::ContextMenu.new(items, anchor: Zaniah::Point.new(anchor.x, anchor.bottom))
|
|
115
141
|
when "modal" then Zaniah::UI::Modal.new(content, title: "Modal")
|
data/lib/zaniah/clipboard.rb
CHANGED
|
@@ -37,4 +37,27 @@ module Zaniah
|
|
|
37
37
|
class Content < Item
|
|
38
38
|
end
|
|
39
39
|
end
|
|
40
|
+
|
|
41
|
+
class DragData
|
|
42
|
+
OPERATIONS = %i[copy move link].freeze
|
|
43
|
+
attr_reader :items, :image, :operations, :on_move
|
|
44
|
+
|
|
45
|
+
def initialize(items:, image: nil, operations: [:copy], on_move: nil)
|
|
46
|
+
raise TypeError, "drag items must be an Array of Clipboard::Item" unless items.is_a?(Array) && !items.empty? && items.all? { |item| item.is_a?(Clipboard::Item) }
|
|
47
|
+
raise ArgumentError, "invalid drag operations" unless operations.is_a?(Array) && !operations.empty? && operations.all? { |operation| OPERATIONS.include?(operation) }
|
|
48
|
+
raise ArgumentError, "move operations require an on_move callback" if operations.include?(:move) && !on_move.respond_to?(:call)
|
|
49
|
+
@items, @image, @operations, @on_move = items.dup.freeze, image, operations.uniq.freeze, on_move
|
|
50
|
+
freeze
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def types = @items.flat_map(&:types).uniq.freeze
|
|
54
|
+
def content(types: self.types)
|
|
55
|
+
raise TypeError, "drag types must be an Array" unless types.is_a?(Array)
|
|
56
|
+
formats = types.each_with_object({}) do |type, result|
|
|
57
|
+
item = @items.find { |entry| entry.types.include?(type) }
|
|
58
|
+
result[type] = item.fetch(type) if item
|
|
59
|
+
end
|
|
60
|
+
Clipboard::Content.new(formats)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
40
63
|
end
|
data/lib/zaniah/element.rb
CHANGED
|
@@ -105,9 +105,26 @@ module Zaniah
|
|
|
105
105
|
define_method(method) { |value| style(**{property => value}) }
|
|
106
106
|
end
|
|
107
107
|
|
|
108
|
-
%i[click hover drag scroll_wheel mouse_down mouse_up].each do |kind|
|
|
108
|
+
%i[click hover drag scroll_wheel magnify mouse_down mouse_up].each do |kind|
|
|
109
109
|
define_method("on_#{kind}") { |&block| @handlers[kind] = block; self }
|
|
110
110
|
end
|
|
111
|
+
def on_drop(types:, &block)
|
|
112
|
+
raise TypeError, "drop types must be an Array of MIME names" unless types.is_a?(Array) && !types.empty? && types.all? { |type| type.is_a?(String) && type.match?(%r{\A[^\s/]+/[^\s/]+\z}) }
|
|
113
|
+
raise ArgumentError, "drop handler required" unless block
|
|
114
|
+
@drop_types, @handlers[:drop] = types.uniq.freeze, block
|
|
115
|
+
self
|
|
116
|
+
end
|
|
117
|
+
def on_drag_over(&block)
|
|
118
|
+
raise ArgumentError, "drag-over handler required" unless block
|
|
119
|
+
@handlers[:drag_over] = block
|
|
120
|
+
self
|
|
121
|
+
end
|
|
122
|
+
def draggable(&block)
|
|
123
|
+
raise ArgumentError, "drag source required" unless block
|
|
124
|
+
@drag_source = block
|
|
125
|
+
self
|
|
126
|
+
end
|
|
127
|
+
def drop_types = @drop_types || []
|
|
111
128
|
|
|
112
129
|
def request_layout(cx)
|
|
113
130
|
@state = cx.state(@key, &@state_initializer) if @key && @state_initializer
|
|
@@ -159,8 +176,31 @@ module Zaniah
|
|
|
159
176
|
@focus_handle.focusable = !@static_flags.include?(:disabled)
|
|
160
177
|
cx.dispatcher.register_focus(@focus_handle)
|
|
161
178
|
end
|
|
162
|
-
unless @handlers.empty? && !@tooltip && !@context_menu && !@style_set.interactive? && !@focus_handle && !@window_drag_region && !@window_control_kind
|
|
179
|
+
unless @handlers.empty? && !@tooltip && !@context_menu && !@style_set.interactive? && !@focus_handle && !@window_drag_region && !@window_control_kind && !@drag_source
|
|
163
180
|
cx.dispatcher.hit(bounds, owner: self) do |event|
|
|
181
|
+
if @pending_drag && event.is_a?(Input::MouseMove)
|
|
182
|
+
original = @pending_drag
|
|
183
|
+
if (event.position.x - original.position.x).abs >= 4 || (event.position.y - original.position.y).abs >= 4
|
|
184
|
+
@pending_drag = nil
|
|
185
|
+
cx.dispatcher.release_mouse_capture
|
|
186
|
+
cx.window.begin_drag(@drag_source.call(original, cx), event: original)
|
|
187
|
+
next true
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
@pending_drag = nil if event.is_a?(Input::MouseUp)
|
|
191
|
+
if event.is_a?(Input::DragOver)
|
|
192
|
+
next false unless @handlers[:drop] && @drop_types.any? { |type| event.accepts?(type) }
|
|
193
|
+
operation = @handlers[:drag_over]&.call(event, cx)
|
|
194
|
+
operation ||= event.operations.first
|
|
195
|
+
operation = :none unless event.operations.include?(operation)
|
|
196
|
+
event.operation = operation
|
|
197
|
+
next operation != :none
|
|
198
|
+
end
|
|
199
|
+
if event.is_a?(Input::DataDrop)
|
|
200
|
+
next false unless @handlers[:drop] && @drop_types.any? { |type| event.content.types.include?(type) }
|
|
201
|
+
@handlers[:drop].call(event, cx)
|
|
202
|
+
next true
|
|
203
|
+
end
|
|
164
204
|
if event.is_a?(Input::MouseDown) && event.button == :right && @context_menu
|
|
165
205
|
cx.window.context_menu(@context_menu, position: event.position)
|
|
166
206
|
next true
|
|
@@ -172,11 +212,16 @@ module Zaniah
|
|
|
172
212
|
when Input::MouseMove then @dragging && @handlers.key?(:drag) ? :drag : :hover
|
|
173
213
|
when Input::MouseUp then :mouse_up
|
|
174
214
|
when Input::ScrollWheel then :scroll_wheel
|
|
215
|
+
when Input::Magnify then :magnify
|
|
175
216
|
end
|
|
176
217
|
handler = @handlers[kind]
|
|
177
218
|
@dragging = true if event.is_a?(Input::MouseDown) && @handlers[:drag]
|
|
178
219
|
@dragging = false if event.is_a?(Input::MouseUp)
|
|
179
220
|
handler&.call(event, cx)
|
|
221
|
+
if event.is_a?(Input::MouseDown) && event.button == :left && @drag_source
|
|
222
|
+
@pending_drag = event
|
|
223
|
+
next :capture
|
|
224
|
+
end
|
|
180
225
|
@dragging && event.is_a?(Input::MouseDown) ? :capture : !!handler || !!(@focus_handle && event.is_a?(Input::MouseDown)) || !!(@tooltip && event.is_a?(Input::MouseMove))
|
|
181
226
|
end
|
|
182
227
|
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Zaniah
|
|
4
|
+
module Input
|
|
5
|
+
DataDrop = Data.define(:content, :position, :operation)
|
|
6
|
+
|
|
7
|
+
class DragOver
|
|
8
|
+
OPERATIONS = %i[copy move link none].freeze
|
|
9
|
+
attr_reader :types, :position, :operations, :operation
|
|
10
|
+
|
|
11
|
+
def initialize(types:, position:, operations: [:copy])
|
|
12
|
+
raise TypeError, "drag types must be an Array of MIME names" unless types.is_a?(Array) && types.all? { |type| type.is_a?(String) }
|
|
13
|
+
raise TypeError, "drag position must be a Point" unless position.is_a?(Point)
|
|
14
|
+
raise ArgumentError, "invalid drag operations" unless operations.is_a?(Array) && operations.all? { |operation| OPERATIONS.include?(operation) && operation != :none }
|
|
15
|
+
@types, @position, @operations, @operation = types.uniq.freeze, position, operations.uniq.freeze, :none
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def accepts?(type) = @types.include?(type)
|
|
19
|
+
def operation=(value)
|
|
20
|
+
raise ArgumentError, "unsupported drag operation #{value.inspect}" unless value == :none || @operations.include?(value)
|
|
21
|
+
@operation = value
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -119,6 +119,7 @@ module Zaniah
|
|
|
119
119
|
@focus_tree.clear
|
|
120
120
|
@transforms.replace([Transform.identity])
|
|
121
121
|
end
|
|
122
|
+
def release_mouse_capture = @capture = nil
|
|
122
123
|
def hits = @hits.map { |bounds, _, owner, _, clip| Hit.new(clip ? bounds.intersect(clip) : bounds, owner) }.freeze
|
|
123
124
|
def hit_regions = @hits.map { |bounds, _, owner, transform, clip| HitRegion.new(bounds, owner, transform, clip) }.freeze
|
|
124
125
|
|
data/lib/zaniah/input/keymap.rb
CHANGED
|
@@ -91,6 +91,27 @@ module Zaniah
|
|
|
91
91
|
.bind("home", :first, context: "in_grid")
|
|
92
92
|
.bind("end", :last, context: "in_grid")
|
|
93
93
|
.bind("enter", :activate, context: "in_grid")
|
|
94
|
+
.bind("left", :previous_day, context: "in_calendar")
|
|
95
|
+
.bind("right", :next_day, context: "in_calendar")
|
|
96
|
+
.bind("up", :previous_week, context: "in_calendar")
|
|
97
|
+
.bind("down", :next_week, context: "in_calendar")
|
|
98
|
+
.bind("pageup", :previous_month, context: "in_calendar")
|
|
99
|
+
.bind("pagedown", :next_month, context: "in_calendar")
|
|
100
|
+
.bind("left", :previous_option, context: "in_dock")
|
|
101
|
+
.bind("right", :next_option, context: "in_dock")
|
|
102
|
+
.bind("home", :first, context: "in_dock")
|
|
103
|
+
.bind("end", :last, context: "in_dock")
|
|
104
|
+
.bind("alt-left", :move_previous, context: "in_dock")
|
|
105
|
+
.bind("alt-right", :move_next, context: "in_dock")
|
|
106
|
+
.bind("ctrl-shift-left", :split_left, context: "in_dock")
|
|
107
|
+
.bind("ctrl-shift-right", :split_right, context: "in_dock")
|
|
108
|
+
.bind("ctrl-shift-up", :split_top, context: "in_dock")
|
|
109
|
+
.bind("ctrl-shift-down", :split_bottom, context: "in_dock")
|
|
110
|
+
.bind("ctrl-shift-d", :detach, context: "in_dock")
|
|
111
|
+
.bind("+", :zoom_in, context: "in_zoom_pan && !in_text_field")
|
|
112
|
+
.bind("shift-=", :zoom_in, context: "in_zoom_pan && !in_text_field")
|
|
113
|
+
.bind("-", :zoom_out, context: "in_zoom_pan && !in_text_field")
|
|
114
|
+
.bind("0", :zoom_reset, context: "in_zoom_pan && !in_text_field")
|
|
94
115
|
.bind("up", :previous_option, context: "in_combobox")
|
|
95
116
|
.bind("down", :next_option, context: "in_combobox")
|
|
96
117
|
.bind("home", :first, context: "in_combobox")
|
data/lib/zaniah/input.rb
CHANGED
|
@@ -7,8 +7,10 @@ require_relative "input/mouse_down"
|
|
|
7
7
|
require_relative "input/mouse_up"
|
|
8
8
|
require_relative "input/mouse_move"
|
|
9
9
|
require_relative "input/scroll_wheel"
|
|
10
|
+
require_relative "input/magnify"
|
|
10
11
|
require_relative "input/composition"
|
|
11
12
|
require_relative "input/file_drop"
|
|
13
|
+
require_relative "input/data_drop"
|
|
12
14
|
require_relative "input/keystroke"
|
|
13
15
|
require_relative "input/context_predicate"
|
|
14
16
|
require_relative "input/keymap"
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "uri"
|
|
4
|
+
|
|
3
5
|
module Zaniah
|
|
4
6
|
module Platform
|
|
5
7
|
module Headless
|
|
@@ -9,7 +11,7 @@ module Zaniah
|
|
|
9
11
|
include Appearance
|
|
10
12
|
DEFAULT_CLEAR = "#181b20"
|
|
11
13
|
|
|
12
|
-
attr_reader :content_size, :scene, :device, :dispatcher, :scale_factor, :text_runs, :pointer_position, :cursor_style, :animator, :clock, :accessibility_tree, :accessibility_revision, :frame_stats, :last_root, :frame_number, :decorations, :transparent, :min_size, :resizable, :traffic_lights
|
|
14
|
+
attr_reader :content_size, :scene, :device, :dispatcher, :scale_factor, :text_runs, :pointer_position, :cursor_style, :animator, :clock, :accessibility_tree, :accessibility_revision, :frame_stats, :last_root, :frame_number, :decorations, :transparent, :min_size, :resizable, :traffic_lights, :drag_data, :drag_result, :drag_history
|
|
13
15
|
attr_accessor :text_system, :ime_state, :title, :app, :devtools
|
|
14
16
|
|
|
15
17
|
def initialize(width: 800, height: 600, title: "Zaniah UI", scale_factor: 1,
|
|
@@ -34,6 +36,7 @@ module Zaniah
|
|
|
34
36
|
@window_frame = Bounds.new(0, 0, width, height)
|
|
35
37
|
@maximized = @minimized = @fullscreen = @always_on_top = false
|
|
36
38
|
@dirty, @closed, @pointer_down, @cursor_style = true, false, false, :arrow
|
|
39
|
+
@drag_history = []
|
|
37
40
|
end
|
|
38
41
|
|
|
39
42
|
def on_input(&block) = @on_input = block
|
|
@@ -80,6 +83,70 @@ module Zaniah
|
|
|
80
83
|
write_clipboard([Clipboard::Item.new("text/plain" => value)])
|
|
81
84
|
value
|
|
82
85
|
end
|
|
86
|
+
def begin_drag(data, event: nil)
|
|
87
|
+
raise TypeError, "drag source must return DragData" unless data.is_a?(DragData)
|
|
88
|
+
@drag_data, @drag_result = data, nil
|
|
89
|
+
@drag_move_committed = false
|
|
90
|
+
@drag_history << [:begin, data, event].freeze
|
|
91
|
+
data
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def drag_over(types:, position:, operations: [:copy])
|
|
95
|
+
event = Input::DragOver.new(types: types, position: position, operations: operations)
|
|
96
|
+
@dispatcher.mouse(event)
|
|
97
|
+
event.operation
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def deliver_drop(content:, position:, operation: :copy, paths: nil)
|
|
101
|
+
raise TypeError, "drop content must be Clipboard::Content" unless content.is_a?(Clipboard::Content)
|
|
102
|
+
raise TypeError, "drop position must be a Point" unless position.is_a?(Point)
|
|
103
|
+
raise ArgumentError, "invalid drop operation" unless DragData::OPERATIONS.include?(operation)
|
|
104
|
+
handled = input(Input::DataDrop.new(content, position, operation))
|
|
105
|
+
paths ||= file_paths_in_drop(content)
|
|
106
|
+
input(Input::FileDrop.new(paths.freeze, position)) unless paths.empty?
|
|
107
|
+
handled ? operation : :none
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def complete_drag(position:, operation: nil, content: nil)
|
|
111
|
+
raise Error, "no drag is active" unless @drag_data
|
|
112
|
+
data = @drag_data
|
|
113
|
+
operation ||= drag_over(types: data.types, position: position, operations: data.operations)
|
|
114
|
+
raise ArgumentError, "drag operation was not offered" unless operation == :none || data.operations.include?(operation)
|
|
115
|
+
operation = deliver_drop(content: content || data.content, position: position, operation: operation) unless operation == :none
|
|
116
|
+
finish_drag_source(operation, position: position)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def finish_drag_source(operation, position: nil)
|
|
120
|
+
raise ArgumentError, "drag operation was not offered" unless operation == :none || @drag_data&.operations&.include?(operation)
|
|
121
|
+
operation = :none if operation == :move && !commit_drag_move
|
|
122
|
+
@drag_result = operation
|
|
123
|
+
@drag_history << [:complete, operation, position].freeze
|
|
124
|
+
@drag_data = nil
|
|
125
|
+
operation
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def commit_drag_move
|
|
129
|
+
return false unless @drag_data&.on_move
|
|
130
|
+
return true if @drag_move_committed
|
|
131
|
+
@drag_data.on_move.call
|
|
132
|
+
@drag_move_committed = true
|
|
133
|
+
rescue StandardError
|
|
134
|
+
false
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def file_paths_in_drop(content)
|
|
138
|
+
source = content.formats["text/uri-list"]
|
|
139
|
+
return [] unless source
|
|
140
|
+
source.each_line.filter_map do |line|
|
|
141
|
+
next if line.start_with?("#") || line.strip.empty?
|
|
142
|
+
uri = URI.parse(line.strip)
|
|
143
|
+
next unless uri.scheme == "file" && [nil, "", "localhost"].include?(uri.host) && uri.query.nil? && uri.fragment.nil?
|
|
144
|
+
path = URI::DEFAULT_PARSER.unescape(uri.path).force_encoding(Encoding::UTF_8)
|
|
145
|
+
path if path.start_with?("/") && path.valid_encoding? && !path.include?("\0")
|
|
146
|
+
rescue URI::InvalidURIError
|
|
147
|
+
nil
|
|
148
|
+
end
|
|
149
|
+
end
|
|
83
150
|
def set_cursor(style)
|
|
84
151
|
raise ArgumentError, "unknown cursor #{style}" unless %i[arrow text pointer crosshair resize_horizontal resize_vertical].include?(style)
|
|
85
152
|
return style if @cursor_style == style
|
|
@@ -224,7 +291,7 @@ module Zaniah
|
|
|
224
291
|
@tooltip = nil if event.is_a?(Input::MouseDown) || event.is_a?(Input::KeyDown)
|
|
225
292
|
@on_input&.call(event)
|
|
226
293
|
@devtools&.handle_input(event)
|
|
227
|
-
if event.respond_to?(:position)
|
|
294
|
+
handled = if event.respond_to?(:position)
|
|
228
295
|
@dispatcher.mouse(event)
|
|
229
296
|
elsif event.is_a?(Input::KeyDown)
|
|
230
297
|
@dispatcher.key(event.keystroke)
|
|
@@ -233,6 +300,7 @@ module Zaniah
|
|
|
233
300
|
end
|
|
234
301
|
@tooltip = nil if event.is_a?(Input::MouseMove) && !@tooltip_offered
|
|
235
302
|
request_frame
|
|
303
|
+
handled
|
|
236
304
|
end
|
|
237
305
|
|
|
238
306
|
def offer_tooltip(text, position:, delay: 0.5)
|