zaniah 0.4.0 → 0.5.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 +4 -4
- data/CHANGELOG.md +13 -0
- data/README.md +7 -4
- data/docs/accessibility.md +38 -12
- data/docs/adr/011-stable-accessibility-identity.md +26 -0
- data/docs/components.md +18 -3
- data/docs/drag_drop.md +46 -0
- data/docs/layout.md +20 -0
- data/docs/text.md +18 -0
- data/docs/tui.md +1 -1
- data/lib/zaniah/accessibility/linux/service.rb +23 -8
- data/lib/zaniah/accessibility/linux.rb +1 -1
- data/lib/zaniah/accessibility/mac.rb +20 -5
- data/lib/zaniah/accessibility/native_tree.rb +13 -3
- data/lib/zaniah/accessibility/node.rb +5 -2
- data/lib/zaniah/accessibility/tree.rb +112 -6
- data/lib/zaniah/accessibility/windows/provider.rb +56 -14
- data/lib/zaniah/accessibility/windows.rb +4 -0
- data/lib/zaniah/accessibility.rb +7 -0
- data/lib/zaniah/drag_drop.rb +246 -0
- data/lib/zaniah/input/keymap.rb +3 -0
- data/lib/zaniah/text_system/low_resolution_text_cache.rb +133 -0
- data/lib/zaniah/text_system.rb +1 -0
- data/lib/zaniah/ui/component.rb +2 -2
- data/lib/zaniah/ui/data.rb +0 -131
- data/lib/zaniah/ui/pane_grid.rb +327 -0
- data/lib/zaniah/ui/tree_view.rb +462 -0
- data/lib/zaniah/ui.rb +2 -0
- data/lib/zaniah/version.rb +1 -1
- data/lib/zaniah.rb +1 -0
- data/sig/accessibility.rbs +31 -2
- data/sig/drag_drop.rbs +41 -0
- data/sig/text.rbs +15 -0
- data/sig/ui.rbs +31 -0
- metadata +8 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0a864e71f3de289770aafa36f520440c0adab078b763412544bca3f9fb32cacb
|
|
4
|
+
data.tar.gz: fc0e6fb0c05f8d0769305954cc5a730258947845b8438328df6d66ca172e80ad
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 16ff7f0e3cc0ce444b502eaff32faaab6cf503af5bf0a4ad547a305aa9394c91d9960052e60bc94ce775521bd54e42ddadfff49b6f896147ca00771e3e4aa59f
|
|
7
|
+
data.tar.gz: 959e485686a3e92a155916a306afc0eb7848f6556d5c1bc6cbfe9c30a605701213dd37c79c978d49b807c60b25e9a47a3cc1e3fff9731cf0ae5a0432a7522f51
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.5.1 — 2026-09-16
|
|
6
|
+
|
|
7
|
+
- Let lazy tree views accept externally loaded children and invalidate cached subtrees without blocking the UI thread.
|
|
8
|
+
- Prevent replacement tree sources from inheriting stale lazy children.
|
|
9
|
+
|
|
10
|
+
## 0.5.0 — 2026-09-15
|
|
11
|
+
|
|
12
|
+
- Make tree views lazy and viewport-virtualized while preserving stable-ID state across source rebuilds.
|
|
13
|
+
- Add bounded, thread-safe low-resolution text textures with per-line invalidation and repeated-shape raster reuse for minimaps.
|
|
14
|
+
- Add stable-ID pointer and keyboard reordering with virtual targets and live accessibility status.
|
|
15
|
+
- Add arbitrary stable-ID pane grids with resizable fixed, fractional, and minmax tracks.
|
|
16
|
+
- Preserve screen-reader identity across virtualization and reordering, with direct tree and pane actions plus native focus and live-region events.
|
|
17
|
+
|
|
5
18
|
## 0.4.0 — 2026-09-15
|
|
6
19
|
|
|
7
20
|
- Add inline and block text overlays with wrapping-aware layout, hit testing, and row-local relayout.
|
data/README.md
CHANGED
|
@@ -37,10 +37,12 @@ the drawing surface beneath the editor.
|
|
|
37
37
|
- Gradients, transforms, paths, SVG, shadows, clipping, and GPU glyph atlases
|
|
38
38
|
- Retained element state, subscriptions, and non-blocking background tasks
|
|
39
39
|
- Scroll views, inertial input, and uniform or variable-height virtual lists
|
|
40
|
-
-
|
|
40
|
+
- Stable-ID pointer and keyboard reordering without materializing virtual collections
|
|
41
|
+
- Stable-ID split-pane grids with fixed, fractional, and minmax tracks
|
|
42
|
+
- OpenType shaping, font fallback, Japanese wrapping, text overlays, low-resolution text caching, selection, editing, and IME
|
|
41
43
|
- Themes, state styles, keyed animation, springs, and reduced-motion support
|
|
42
|
-
- Opt-in controls, overlays, tables, trees, charts, forms, and terminal fallbacks
|
|
43
|
-
- Spatial keyboard focus and a diffed cross-platform accessibility tree
|
|
44
|
+
- Opt-in controls, overlays, tables, externally completable lazy virtual trees, charts, forms, and terminal fallbacks
|
|
45
|
+
- Spatial keyboard focus and a stable-ID, event-diffed cross-platform accessibility tree
|
|
44
46
|
- F12 inspector, frame statistics, hot reload, and headless golden-image tests
|
|
45
47
|
- Native input, IME, clipboard, file-drop, display, and filesystem events
|
|
46
48
|
- RBS declarations for the public API
|
|
@@ -114,7 +116,8 @@ Native backends use the operating system libraries through Ruby's Fiddle. See
|
|
|
114
116
|
- [Native backends](docs/native.md) — windows, displays, file watching, and terminals
|
|
115
117
|
- [Text system](docs/text.md) — font discovery, shaping, rasterization, and caching
|
|
116
118
|
- [SVG and lists](docs/vector_and_list.md) — static vector icons and virtual lists
|
|
117
|
-
- [
|
|
119
|
+
- [Drag-and-drop reordering](docs/drag_drop.md) — stable IDs, virtual targets, and keyboard access
|
|
120
|
+
- [Layout and scrolling](docs/layout.md) — Grid, resizable pane grids, scrolling, sticky positioning, and RTL foundations
|
|
118
121
|
- [Themes](docs/theme.md) — semantic tokens and state styles
|
|
119
122
|
- [Components](docs/components.md) — controls, overlays, data views, charts, and forms
|
|
120
123
|
- [Animation](docs/animation.md) — easing, springs, transitions, and reduced motion
|
data/docs/accessibility.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# Accessibility
|
|
2
2
|
|
|
3
3
|
Every window maintains a backend-neutral `Accessibility::Tree` beside its render
|
|
4
|
-
tree. Components expose role, label, value, bounds, states, children,
|
|
5
|
-
through `Accessibility::Node`. The window publishes
|
|
6
|
-
semantic tree changes.
|
|
4
|
+
tree. Components expose a stable ID, role, label, value, bounds, states, children,
|
|
5
|
+
and actions through `Accessibility::Node`. The window publishes notifications only
|
|
6
|
+
when the semantic tree changes.
|
|
7
7
|
|
|
8
8
|
```ruby
|
|
9
9
|
window.accessibility_tree.each do |node, path|
|
|
@@ -11,10 +11,13 @@ window.accessibility_tree.each do |node, path|
|
|
|
11
11
|
end
|
|
12
12
|
```
|
|
13
13
|
|
|
14
|
-
`window.accessibility_revision` increments after each semantic change
|
|
15
|
-
`window.accessibility_tree.changes` contains `added`, `removed`,
|
|
16
|
-
records
|
|
17
|
-
|
|
14
|
+
`window.accessibility_revision` increments after each semantic change.
|
|
15
|
+
`window.accessibility_tree.changes` contains `added`, `removed`, `updated`, or
|
|
16
|
+
`moved` records; stable IDs let a reorder remain a move instead of two unrelated
|
|
17
|
+
updates. IDs must be immutable and unique among siblings. The accompanying
|
|
18
|
+
`events` reduce those changes to deterministic `structure`, `property`, `layout`,
|
|
19
|
+
`focus`, and `announcement` events. This makes accessibility assertions
|
|
20
|
+
deterministic on the headless backend without mocking an operating system API.
|
|
18
21
|
|
|
19
22
|
## Component semantics
|
|
20
23
|
|
|
@@ -30,6 +33,7 @@ Custom components implement the same method:
|
|
|
30
33
|
def accessibility_node(_cx)
|
|
31
34
|
Zaniah::Accessibility.node(
|
|
32
35
|
role: :button,
|
|
36
|
+
id: :refresh,
|
|
33
37
|
label: "Refresh",
|
|
34
38
|
states: {busy: @loading},
|
|
35
39
|
actions: @loading ? [] : [:press]
|
|
@@ -37,20 +41,42 @@ def accessibility_node(_cx)
|
|
|
37
41
|
end
|
|
38
42
|
```
|
|
39
43
|
|
|
44
|
+
If a component synthesizes semantic descendants, it can route their actions
|
|
45
|
+
without coordinate hit testing:
|
|
46
|
+
|
|
47
|
+
```ruby
|
|
48
|
+
def accessibility_action(node, action)
|
|
49
|
+
refresh(node.id) if action == :press
|
|
50
|
+
end
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
`TreeView` uses item IDs for its viewport-only descendants and reports the
|
|
54
|
+
selected descendant as focused while the tree owns keyboard focus. Its select,
|
|
55
|
+
expand, and collapse actions call the tree model directly. `PaneGrid` gives panes
|
|
56
|
+
and dividers stable IDs and routes divider increment, decrement, minimum, and
|
|
57
|
+
maximum actions through the same resizing path as the keyboard. A
|
|
58
|
+
`DragDrop::Reorder` exposes keyboard reorder actions and a stable, atomic polite
|
|
59
|
+
live region for move and cancellation results. Offscreen virtual rows remain
|
|
60
|
+
unmaterialized; when they re-enter the viewport, their IDs reconnect them to the
|
|
61
|
+
same native identity.
|
|
62
|
+
|
|
40
63
|
Decorative content should return `nil`. Container elements without an explicit
|
|
41
64
|
node retain semantic descendants in a generated `group` node.
|
|
42
65
|
|
|
43
66
|
## Native bridges
|
|
44
67
|
|
|
45
|
-
- macOS publishes an `NSAccessibilityElement` hierarchy with
|
|
46
|
-
values, state, bounds, and children, then posts
|
|
68
|
+
- macOS publishes an `NSAccessibilityElement` hierarchy with identifiers, roles,
|
|
69
|
+
labels, values, state, bounds, and children, then posts layout, value, focus,
|
|
70
|
+
and announcement notifications from semantic events.
|
|
47
71
|
- Windows answers `WM_GETOBJECT` with `IRawElementProviderSimple`, fragment,
|
|
48
72
|
fragment-root, and Invoke providers. UI Automation clients can navigate and
|
|
49
|
-
query
|
|
73
|
+
query stable Automation IDs and receive structure, property, layout, focus,
|
|
74
|
+
and live-region events rather than relying on refresh events alone.
|
|
50
75
|
- Linux registers `Accessible`, `Component`, `Action`, and `Application`
|
|
51
76
|
objects on the AT-SPI D-Bus, embeds the application root in the registry,
|
|
52
|
-
|
|
53
|
-
|
|
77
|
+
retains stable `AccessibleId` values and object paths, and emits matching
|
|
78
|
+
object, state, and announcement events. Actions route back through the normal
|
|
79
|
+
input dispatcher or a component's direct semantic action on every platform.
|
|
54
80
|
|
|
55
81
|
The Linux provider is exercised in CI inside a private D-Bus session. Native
|
|
56
82
|
macOS and Windows objects retain the same `Accessibility::NativeTree` path and
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# ADR 011: Match accessibility descendants by stable identity
|
|
2
|
+
|
|
3
|
+
- Status: Accepted
|
|
4
|
+
- Date: 2026-09-15
|
|
5
|
+
- Decision deadline: before screen-reader virtualization support
|
|
6
|
+
|
|
7
|
+
## Context
|
|
8
|
+
|
|
9
|
+
Position-only semantic paths change when a virtual viewport scrolls or siblings
|
|
10
|
+
are reordered. Native bridges then expose a previously known item as a different
|
|
11
|
+
object, and coordinate-generated actions can target the wrong virtual row.
|
|
12
|
+
|
|
13
|
+
## Decision
|
|
14
|
+
|
|
15
|
+
Let semantic nodes carry an optional immutable ID that is unique among siblings.
|
|
16
|
+
Match identified siblings by ID when diffing and retain native runtime IDs across
|
|
17
|
+
tree publications. Keep offscreen rows absent. Route actions on synthesized nodes
|
|
18
|
+
back to their owning component, and derive native structure, property, layout,
|
|
19
|
+
focus, and live-region events from the shared semantic diff.
|
|
20
|
+
|
|
21
|
+
## Consequences
|
|
22
|
+
|
|
23
|
+
Virtual tree rows, reordered panes, and live status nodes regain their native
|
|
24
|
+
identity when their semantic node moves. Components remain responsible for stable
|
|
25
|
+
domain IDs and expose no platform objects. Unidentified nodes retain positional
|
|
26
|
+
matching and coordinate action fallback for compatibility.
|
data/docs/components.md
CHANGED
|
@@ -79,11 +79,12 @@ Zaniah::UI::Button.variants[:variant][:brand] = ->(theme) {
|
|
|
79
79
|
| L3 | `Toast` | `(message, variant:, queue:)`; `dismiss` | info/success/warning/danger | live status |
|
|
80
80
|
| L3 | `CommandPalette` | `(commands, open:, placeholder:)` | searchable modal | dialog/list |
|
|
81
81
|
| L3 | `SplitPane` | `(first, second, orientation:, ratio:, min:, max:)`; `on_change` | horizontal/vertical, draggable separator | group/separator |
|
|
82
|
+
| L3 | `PaneGrid` | `(panes, columns:, rows:, divider_size:, minimum:, keyboard_step:)`; `replace`, `on_resize` | arbitrary resizable grid, stable pane IDs | group/separator |
|
|
82
83
|
| L3 | `Resizable` | `(content, width:, height:, min_width:, min_height:, max_width:, max_height:)`; `on_resize` | drag or keyboard resize | group/separator |
|
|
83
84
|
| L3 | `DockPanel` | `(center:, top:, right:, bottom:, left:)` | five-region layout | group |
|
|
84
85
|
| L3 | `ListView` | `(items, height:, row_height:, selected:)`; `on_select` | virtual rows and keyboard selection | list/listitem |
|
|
85
86
|
| L4 | `Table`, `DataGrid` | `(rows, columns:, height:, selection:, row_key:)`; `on_sort`, `on_select`, `on_edit` | virtual rows, sorting, resizing, editing | table/row/cell |
|
|
86
|
-
| L4 | `TreeView` | `(items, height:, selected:)`; `expand`, `collapse`, lazy `children` proc | arrows/Home/End | tree/treeitem |
|
|
87
|
+
| L4 | `TreeView` | `(items, height:, selected:)`; `expand`, `collapse`, `replace`, `replace_children`, `invalidate`, lazy `children` proc | arrows/Home/End | tree/treeitem |
|
|
87
88
|
| L5 | `Sparkline` | `(values, width:, height:, color:, label:)` | line + tooltip | image |
|
|
88
89
|
| L5 | `LineChart`, `BarChart` | `(series, width:, height:, colors:, label:)` | axes, legend, tooltip | image |
|
|
89
90
|
| L5 | `Validation` | `required`, `format`, `length`, `number`, `rule` | composable rules | n/a |
|
|
@@ -98,12 +99,26 @@ the previous focus. See [TUI](tui.md) for terminal representations.
|
|
|
98
99
|
|
|
99
100
|
Table columns are hashes with `key`, and optional `label`, `width`, `sortable`,
|
|
100
101
|
`resizable`, `editable`, and `render`. Tree items accept hashes containing `id`,
|
|
101
|
-
`label`, and either an array or lazy proc in `children`.
|
|
102
|
+
`label`, and either an array or lazy proc in `children`. A lazy proc receives the
|
|
103
|
+
item value, runs on first expansion, and is cached after it succeeds; a raised
|
|
104
|
+
exception leaves it available for retry. A loader can return a placeholder while
|
|
105
|
+
work runs elsewhere; call `replace_children(id, children)` on the UI thread to
|
|
106
|
+
apply the result without changing selection, focus, or expansion. It returns
|
|
107
|
+
`false` when the ID is missing or is not lazy. `invalidate(id)` likewise returns
|
|
108
|
+
`false` for missing or non-lazy items; otherwise it discards that lazy result and
|
|
109
|
+
its loaded descendants so the next expansion calls the loader again. Omit the ID
|
|
110
|
+
to invalidate all loaded results. `replace(items)` starts a
|
|
111
|
+
new source generation, so it never inherits loaded children from the old source.
|
|
112
|
+
Completed children must be an array with unique, non-nil stable IDs, at most 64
|
|
113
|
+
levels deep and 100,000 items total. Invalid results leave the previous children
|
|
114
|
+
unchanged. Tree rows and accessibility nodes are built only for the current
|
|
115
|
+
viewport.
|
|
102
116
|
|
|
103
117
|
In a table, Up/Down/Home/End/Page keys move and select rows, Shift+Up/Down extends
|
|
104
118
|
a range, and Cmd/Ctrl+A selects every row in multiple-selection mode. Sortable
|
|
105
119
|
headers and resize handles are separate Tab stops; Enter sorts and arrow/Page keys
|
|
106
|
-
resize. Tree views use Up/Down to select
|
|
120
|
+
resize. Tree views use Up/Down to select, Right to expand or enter the first child,
|
|
121
|
+
and Left to collapse or return to the parent.
|
|
107
122
|
|
|
108
123
|
Run `bundle exec ruby tools/generate_component_gallery.rb` to rebuild the dark,
|
|
109
124
|
light, and high-contrast component sheets plus overlay variants in all three themes.
|
data/docs/drag_drop.md
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Drag-and-drop reordering
|
|
2
|
+
|
|
3
|
+
`DragDrop::Reorder` coordinates pointer and keyboard reordering without owning the
|
|
4
|
+
application's collection. It identifies items by stable IDs and reports a
|
|
5
|
+
`DragDrop::Target` whose position is `before`, `after`, or `inside`.
|
|
6
|
+
|
|
7
|
+
```ruby
|
|
8
|
+
reorder = Zaniah::DragDrop::Reorder.new(
|
|
9
|
+
threshold: 4,
|
|
10
|
+
locate: ->(point, source_id) { target_at(point, excluding: source_id) },
|
|
11
|
+
keyboard: ->(id, direction) { adjacent_target(id, direction) },
|
|
12
|
+
label: ->(id) { item_label(id) }
|
|
13
|
+
).on_drop do |event|
|
|
14
|
+
move_item(event.source_id, event.target.id, event.target.position)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
row
|
|
18
|
+
.on_mouse_down { |event, _| reorder.press(row_id, event.position) }
|
|
19
|
+
.on_drag { |event, _| reorder.move(event.position) }
|
|
20
|
+
.on_mouse_up { |event, _| reorder.release(event.position) }
|
|
21
|
+
.focusable(context: {reorderable: true}) { |action| reorder.action(row_id, action) }
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
The pointer locator is called only after the drag threshold is crossed. Virtual
|
|
25
|
+
lists can derive a row with `List::HeightIndex#index_at`; uniform lists can use
|
|
26
|
+
division by row height. Tree views can resolve a visible segment and return an
|
|
27
|
+
`inside` target only for containers. No API requires enumerating or rendering the
|
|
28
|
+
whole collection. Return `nil` for collection-specific no-ops, such as placing an
|
|
29
|
+
item after its current predecessor.
|
|
30
|
+
|
|
31
|
+
Listen with `on_event` for `press`, `start`, `enter`, `over`, `leave`, `drop`, and
|
|
32
|
+
`cancel`. Call `remove(id)` when application data removes a source or current
|
|
33
|
+
target, and call `cancel` when an enclosing interaction ends. Callback failures
|
|
34
|
+
reset transient drag state before being re-raised. Callbacks may update the
|
|
35
|
+
application collection, but must not re-enter their `Reorder` controller; an
|
|
36
|
+
attempt raises `Zaniah::Error` and cancels the active operation.
|
|
37
|
+
|
|
38
|
+
For keyboard access, mark the focused element with `reorderable: true`. Alt+Up
|
|
39
|
+
and Alt+Down dispatch `reorder_before` and `reorder_after`; Escape dispatches
|
|
40
|
+
`cancel_reorder`. `accessibility_actions(id)` returns the actions a containing
|
|
41
|
+
item should expose, and `action(id, action)` routes them without pointer geometry.
|
|
42
|
+
`accessibility_node` exposes the last result as a stable, atomic polite `status`
|
|
43
|
+
node. Include that node in the containing component's accessibility children, or
|
|
44
|
+
forward `on_announce` to its existing live region. Native file drops are separate
|
|
45
|
+
and are not handled by this primitive. The controller belongs to its UI owner:
|
|
46
|
+
call it on that owner's event thread and `cancel` before the owner is discarded.
|
data/docs/layout.md
CHANGED
|
@@ -17,6 +17,26 @@ Zaniah::Div.new.style(
|
|
|
17
17
|
Place an item with `grid_column: 2` or span inclusive tracks with
|
|
18
18
|
`grid_column: 1..3`. Named areas and `auto-fit`/`auto-fill` are not supported.
|
|
19
19
|
|
|
20
|
+
`UI::PaneGrid` turns a rectangular matrix into independently resizable rows and
|
|
21
|
+
columns. Each non-empty cell is a `PaneGrid::Pane`, an `id`/`content` hash, or
|
|
22
|
+
an `[id, content]` pair. IDs must be unique and remain stable when `replace` is
|
|
23
|
+
used so retained pane state survives source rebuilds. Tracks accept fixed pixel
|
|
24
|
+
numbers, `px`, `fr`, or `minmax` values.
|
|
25
|
+
|
|
26
|
+
```ruby
|
|
27
|
+
grid = Zaniah::UI::PaneGrid.new(
|
|
28
|
+
[[[:editor, editor], [:terminal, terminal]]],
|
|
29
|
+
columns: [Zaniah.minmax(Zaniah.px(240), Zaniah.fr(2)), Zaniah.fr(1)],
|
|
30
|
+
rows: [Zaniah.fr(1)]
|
|
31
|
+
)
|
|
32
|
+
grid.on_resize { |event, _context| save_split(event.axis, event.divider, event.before) }
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Dividers support pointer drag, arrow keys, Page Up/Down, Home, and End. Their
|
|
36
|
+
stable accessibility IDs, focus state, value, orientation, and direct increment,
|
|
37
|
+
decrement, minimum, and maximum actions use the same clamped resize path. Nested
|
|
38
|
+
`PaneGrid` instances compose without owning editor, terminal, or document state.
|
|
39
|
+
|
|
20
40
|
`ScrollView` accepts one child and clips it to the viewport. Its `axis` is
|
|
21
41
|
`:vertical`, `:horizontal`, or `:both`; use `scroll_to` for programmatic
|
|
22
42
|
scrolling and `scroll_state` for offsets and edge checks.
|
data/docs/text.md
CHANGED
|
@@ -79,6 +79,24 @@ A missing, corrupt, or read-only cache does not prevent rendering. Applications
|
|
|
79
79
|
own cache-directory retention. See [the RBS declarations](../sig/text.rbs) for
|
|
80
80
|
the complete API.
|
|
81
81
|
|
|
82
|
+
For minimaps and other compact previews, cache Alhena's downsampled outlines as
|
|
83
|
+
one R8 texture per source line:
|
|
84
|
+
|
|
85
|
+
```ruby
|
|
86
|
+
cache = Zaniah::TextSystem::LowResolutionTextCache.new(
|
|
87
|
+
width: 100, height: 2, scale: 0.1
|
|
88
|
+
)
|
|
89
|
+
texture = cache.texture(42, outlines: outlines)
|
|
90
|
+
cache.invalidate(42) # regenerate only the edited source line on next access
|
|
91
|
+
cache.close
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
The cache is thread-safe, count- and byte-bounded, and uses least-recently-used
|
|
95
|
+
eviction. Textures already returned to a scene remain valid after eviction or
|
|
96
|
+
invalidation. A cache has fixed width, height, and scale; invalidate a line when
|
|
97
|
+
its outlines change for any reason, including a font change. Call `close` when
|
|
98
|
+
the cache is no longer needed.
|
|
99
|
+
|
|
82
100
|
## Development checks
|
|
83
101
|
|
|
84
102
|
```sh
|
data/docs/tui.md
CHANGED
|
@@ -21,7 +21,7 @@ tests and non-window integrations.
|
|
|
21
21
|
| ScrollView, Scrollbar | clipped child content and `│`/`─` track |
|
|
22
22
|
| Breadcrumb, Pagination, bars | slash-separated path, page status, space-separated content |
|
|
23
23
|
| Modal, Dialog, Drawer, CommandPalette | box-drawing overlay; focus stays inside until dismissed |
|
|
24
|
-
| SplitPane, Resizable, DockPanel | `│`/`─` separators, resize corner, or ordered dock regions |
|
|
24
|
+
| SplitPane, PaneGrid, Resizable, DockPanel | `│`/`─` separators, resize corner, or ordered dock regions |
|
|
25
25
|
| ListView | visible rows with `>` on the selected item |
|
|
26
26
|
| Toast | live status text |
|
|
27
27
|
| Table, DataGrid | header and visible rows separated with `|` |
|
|
@@ -84,15 +84,15 @@ module Zaniah
|
|
|
84
84
|
parse_interfaces
|
|
85
85
|
end
|
|
86
86
|
|
|
87
|
-
def update(root)
|
|
87
|
+
def update(root, events: [])
|
|
88
88
|
application = Accessibility.node(role: :application, label: window_title,
|
|
89
89
|
bounds: root.bounds, children: [root])
|
|
90
|
-
@tree = NativeTree.new(application)
|
|
90
|
+
@tree = NativeTree.new(application, previous: @tree)
|
|
91
91
|
@entries = @tree.to_h { |entry| [path(entry), entry] }
|
|
92
92
|
(@registrations.keys - @entries.keys).each { |object_path| unregister(object_path) }
|
|
93
93
|
(@entries.keys - @registrations.keys).each { |object_path| register(object_path) }
|
|
94
94
|
embed unless @embedded
|
|
95
|
-
|
|
95
|
+
emit_changes(events)
|
|
96
96
|
true
|
|
97
97
|
end
|
|
98
98
|
|
|
@@ -179,10 +179,23 @@ module Zaniah
|
|
|
179
179
|
@embedded = true
|
|
180
180
|
end
|
|
181
181
|
|
|
182
|
-
def
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
182
|
+
def emit_changes(events)
|
|
183
|
+
events = [nil] if events.empty?
|
|
184
|
+
events.each do |event|
|
|
185
|
+
member, detail, active = case event&.kind
|
|
186
|
+
when :structure then ["ChildrenChanged", event.node.id.to_s, 1]
|
|
187
|
+
when :property then ["PropertyChange", event.node.id.to_s, 0]
|
|
188
|
+
when :focus then ["StateChanged", "focused", 1]
|
|
189
|
+
when :announcement then ["Announcement", event.node.label.to_s, 1]
|
|
190
|
+
else ["VisibleDataChanged", "", 0]
|
|
191
|
+
end
|
|
192
|
+
entry = event && @tree[[0, *event.path]] unless %i[structure layout].include?(event&.kind)
|
|
193
|
+
parameters = tuple(string(detail), int32(active), int32(0),
|
|
194
|
+
variant(string(event&.node&.label || "")), array("{sv}", []))
|
|
195
|
+
gio.fn(:g_dbus_connection_emit_signal, [P, P, P, P, P, P, P], I)
|
|
196
|
+
.call(@connection, 0, entry ? path(entry) : ROOT_PATH,
|
|
197
|
+
"org.a11y.atspi.Event.Object", member, parameters, 0)
|
|
198
|
+
end
|
|
186
199
|
end
|
|
187
200
|
|
|
188
201
|
def method_call(object_path, interface, method, parameters, invocation)
|
|
@@ -269,7 +282,9 @@ module Zaniah
|
|
|
269
282
|
when [ACCESSIBLE, "Parent"] then reference(entry.parent)
|
|
270
283
|
when [ACCESSIBLE, "ChildCount"] then int32(entry.children.length)
|
|
271
284
|
when [ACCESSIBLE, "Locale"] then string(ENV["LANG"] || "C")
|
|
272
|
-
when [ACCESSIBLE, "AccessibleId"]
|
|
285
|
+
when [ACCESSIBLE, "AccessibleId"]
|
|
286
|
+
id = entry.node.id
|
|
287
|
+
string((id.nil? ? entry.runtime_id : id).to_s)
|
|
273
288
|
when [ACTION, "NActions"] then int32(entry.node.actions.length)
|
|
274
289
|
when [APPLICATION, "ToolkitName"] then string("Zaniah")
|
|
275
290
|
when [APPLICATION, "Version"], [APPLICATION, "ToolkitVersion"] then string(Zaniah::VERSION)
|
|
@@ -11,7 +11,7 @@ module Zaniah
|
|
|
11
11
|
def publish(window, root, _changes)
|
|
12
12
|
return false unless root
|
|
13
13
|
service = services[window.object_id] ||= Service.new(window)
|
|
14
|
-
service.update(root)
|
|
14
|
+
service.update(root, events: window.accessibility_tree.events)
|
|
15
15
|
rescue Fiddle::DLError, Errno::ENOENT, Error
|
|
16
16
|
false
|
|
17
17
|
end
|
|
@@ -28,7 +28,8 @@ module Zaniah
|
|
|
28
28
|
return unless root && defined?(Platform::Mac::O) && window.respond_to?(:view)
|
|
29
29
|
install
|
|
30
30
|
ELEMENTS.delete_if { |_element, (owner, _node)| owner.equal?(window) }
|
|
31
|
-
|
|
31
|
+
previous = window.instance_variable_get(:@native_accessibility_tree)
|
|
32
|
+
tree = NativeTree.new(root, previous: previous)
|
|
32
33
|
children = mutable_array
|
|
33
34
|
append(children, native_element(window, tree, tree.root, window.view))
|
|
34
35
|
object = window.view
|
|
@@ -72,8 +73,9 @@ module Zaniah
|
|
|
72
73
|
o.string(ROLES.fetch(node.role, "AXGroup")), screen_bounds(window, tree.bounds(entry)),
|
|
73
74
|
o.string(node.label || node.value&.to_s || node.role.to_s), parent,
|
|
74
75
|
args: [:pointer, :rect, :pointer, :pointer])
|
|
75
|
-
ELEMENTS[element] = [window, node, tree.bounds(entry)]
|
|
76
|
+
ELEMENTS[element] = [window, node, tree.bounds(entry), entry.runtime_id]
|
|
76
77
|
o.send(element, "setAccessibilityFrameInParentSpace:", parent_bounds(tree, entry), args: [:rect], result: :void)
|
|
78
|
+
o.send(element, "setAccessibilityIdentifier:", o.string(node.id.to_s), args: [:pointer], result: :void) unless node.id.nil?
|
|
77
79
|
o.send(element, "setAccessibilityEnabled:", node.states[:disabled] ? 0 : 1, args: [:bool], result: :void)
|
|
78
80
|
o.send(element, "setAccessibilityValue:", native_value(node.value), args: [:pointer], result: :void) unless node.value.nil?
|
|
79
81
|
set_boolean(element, "setAccessibilitySelected:", node.states[:selected]) if node.states.key?(:selected)
|
|
@@ -126,9 +128,22 @@ module Zaniah
|
|
|
126
128
|
def notify(window, object)
|
|
127
129
|
function = Platform::Mac::APPKIT.fn(:NSAccessibilityPostNotification,
|
|
128
130
|
[Fiddle::TYPE_VOIDP, Fiddle::TYPE_VOIDP], Fiddle::TYPE_VOID)
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
131
|
+
notifications(window.accessibility_tree.events).each do |event, name|
|
|
132
|
+
runtime_id = window.instance_variable_get(:@native_accessibility_tree)&.[](event.path)&.runtime_id unless %i[structure layout].include?(event.kind)
|
|
133
|
+
target = ELEMENTS.find { |_element, (owner, _node, _bounds, id)| owner.equal?(window) && id == runtime_id }&.first || object
|
|
134
|
+
function.call(target, Platform::Mac::O.string(name))
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def notifications(events)
|
|
139
|
+
events.filter_map do |event|
|
|
140
|
+
name = case event.kind
|
|
141
|
+
when :structure, :layout then "AXLayoutChanged"
|
|
142
|
+
when :property then "AXValueChanged"
|
|
143
|
+
when :focus then "AXFocusedUIElementChanged"
|
|
144
|
+
when :announcement then "AXAnnouncementRequested"
|
|
145
|
+
end
|
|
146
|
+
[event, name] if name
|
|
132
147
|
end
|
|
133
148
|
end
|
|
134
149
|
end
|
|
@@ -9,9 +9,11 @@ module Zaniah
|
|
|
9
9
|
|
|
10
10
|
attr_reader :root
|
|
11
11
|
|
|
12
|
-
def initialize(root)
|
|
12
|
+
def initialize(root, previous: nil)
|
|
13
13
|
@entries = {}
|
|
14
|
-
@
|
|
14
|
+
@identities = {}
|
|
15
|
+
@previous_ids = previous ? previous.__send__(:runtime_ids).dup : {}
|
|
16
|
+
@next_runtime_id = (@previous_ids.values.max || 0) + 1
|
|
15
17
|
@root = build(root, nil, []) if root
|
|
16
18
|
end
|
|
17
19
|
|
|
@@ -32,6 +34,8 @@ module Zaniah
|
|
|
32
34
|
end
|
|
33
35
|
|
|
34
36
|
def focused(window)
|
|
37
|
+
semantic = find { |entry| entry.node.states[:focused] }
|
|
38
|
+
return semantic if semantic
|
|
35
39
|
bounds = window.dispatcher.focused&.bounds
|
|
36
40
|
bounds && hit(Point.new(bounds.x + bounds.width / 2.0, bounds.y + bounds.height / 2.0))
|
|
37
41
|
end
|
|
@@ -39,14 +43,20 @@ module Zaniah
|
|
|
39
43
|
private
|
|
40
44
|
|
|
41
45
|
def build(node, parent, path)
|
|
42
|
-
|
|
46
|
+
path = path.freeze
|
|
47
|
+
key = [(parent && identity(parent)), node.id.nil? ? [:index, path.last] : [:id, node.id]].freeze
|
|
48
|
+
runtime_id = @previous_ids[key] ||= next_runtime_id
|
|
49
|
+
entry = NativeEntry.new(path: path, node: node, parent: parent, children: [], runtime_id: runtime_id)
|
|
43
50
|
@entries[entry.path] = entry
|
|
51
|
+
@identities[entry.object_id] = key
|
|
44
52
|
children = node.children.map.with_index { |child, index| build(child, entry, path + [index]) }.freeze
|
|
45
53
|
entry.children.replace(children)
|
|
46
54
|
entry.children.freeze
|
|
47
55
|
entry
|
|
48
56
|
end
|
|
49
57
|
|
|
58
|
+
def identity(entry) = @identities.fetch(entry.object_id)
|
|
59
|
+
def runtime_ids = @previous_ids
|
|
50
60
|
def next_runtime_id = (@next_runtime_id += 1) - 1
|
|
51
61
|
end
|
|
52
62
|
end
|
|
@@ -2,9 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
module Zaniah
|
|
4
4
|
module Accessibility
|
|
5
|
-
Node = Data.define(:role, :label, :value, :bounds, :states, :children, :actions)
|
|
5
|
+
Node = Data.define(:role, :label, :value, :bounds, :states, :children, :actions) do
|
|
6
|
+
def id = states[:accessibility_id]
|
|
7
|
+
end
|
|
6
8
|
|
|
7
|
-
def self.node(role:, label: nil, value: nil, bounds: nil, states: {}, children: [], actions: [])
|
|
9
|
+
def self.node(role:, id: nil, label: nil, value: nil, bounds: nil, states: {}, children: [], actions: [])
|
|
10
|
+
states = states.merge(accessibility_id: id) unless id.nil?
|
|
8
11
|
Node.new(role: role.to_sym, label: label&.to_s, value: value, bounds: bounds,
|
|
9
12
|
states: states.dup.freeze, children: children.dup.freeze, actions: actions.map(&:to_sym).freeze)
|
|
10
13
|
end
|