Drag-operation census
Every operation in the workspace whose only route is a pointer drag. WCAG 2.2 SC 2.5.7 Dragging Movements (AA) requires each of them to gain a single-pointer alternative that is not a drag; package P39 was sized from this list, and its exit criterion is that no row below still reads "none" in the Non-drag route today column.
P39 routed seven rows — 4, 9, 10, 11, 12, 13 and 16 — leaving fifteen short of a full route: the four still marked Partial and the eleven marked None. The commands it added, the vocabulary they share and what each remaining row needs are in docs/a11y/non-drag-alternatives.md; this table stays the ledger. Rows P39 changed carry their enclosing function rather than a line number.
Measured against wt-touch @ 89c7a03c, and every line number below was read at
that commit rather than inferred. Read them as pinned to it, not as current.
The touch programme rewrote most of these files afterwards: re-deriving the
primary citation of all 34 rows that carry one found exactly one still resolving
to the line it named. So treat a file:line here as a starting point to grep from,
and prefer the function names — a line number is a claim with a short shelf life,
which is why rows re-derived since carry the enclosing function instead.
Rows marked Compliant already satisfy 2.5.7 and are recorded so P39 does not re-litigate them.
# reproduce the candidate set
grep -rnE '\.on_drag\(|start_drag(_with_preview)?\(|capture_pointer\(|DragPhase' \
--include=*.rs crates/ | grep -v tests.rs
Summary
Counted off each row's own Non-drag route today verdict, so the split is re-derivable from the table rather than a separate judgement: 37 census rows, plus 4 more recorded and dismissed in their own table below.
| Status | Rows |
|---|---|
| Compliant today (keyboard and/or AT route exists) — the row reads "Yes" | 22 |
| Partial — an AT action or a menu exists but no keyboard chord, or only part of the operation is covered | 4 |
| No non-drag route at all | 11 |
| Not a drag / no obligation (recorded and dismissed) | 4 |
Two verdicts do not open with one of the three words and are read as written: row 21 is Partial (rail-to-rail transfer has a route, reorder within the rail does not) and row 30 is None (Escape cancels a drag; nothing starts one).
The single highest-leverage remaining fix is row 30: a framework-level keyboard
pick-up / put-down mode in teksilo-core's drag pipeline would retire rows 14, 17,
19 and 29 at once — four of the eleven, and all four of the same shape (a payload
leaving one surface for another).
Census
| # | File : location | Operation | What it changes | Non-drag route today | Proposed non-drag command |
|---|---|---|---|---|---|
| 1 | slider.rs Slider::build | Drag the thumb to set the value | the bound Signal<f32> | Yes. on_key — arrows ±step, Home/End = min/max; on_access_action Increment/Decrement. The horizontal arrows now mirror under RTL through arrow_increases, off the same cached flag the pointer path reads, so the keys and the painted axis agree (they did not: ArrowRight moved the knob left). No PageUp/PageDown. | Compliant. Optional: PageUp/PageDown (×10) and Action::SetValue. A19's long-press stepper popover is not built and is not needed for 2.5.7 — the keyboard route is complete. |
| 2 | crates/teksilo-widgets/src/color_picker/hue_strip.rs:162 | Drag the hue strip | hue Signal<f32> (0–360) | Yes. on_key :190 — arrows ±1°, PageUp/Down ±15°, Home/End; on_access_action :243, advertised :358-360. | Compliant. |
| 3 | crates/teksilo-widgets/src/color_picker/alpha_strip.rs:153 | Drag the alpha strip | alpha Signal<f32> (0–1) | Yes. on_key :181 — arrows ±0.01, PageUp/Down ±0.10, Home/End; on_access_action :229. | Compliant. |
| 4 | color_picker/hsv_canvas.rs HsvCanvas::build | 2-D drag over the saturation × value canvas | saturation + value_hsv via set_hsv | Yes. Focusable, with arrows ±1 % and Shift+arrows ±10 % (CanvasStep); four named custom actions, one per direction, because Action::Increment cannot say which axis of a pair it means; each step announces both axes. The node is a named Role::Group carrying both as its value — it was a bare GenericContainer, which the walker prunes, so nothing it advertised was reachable. Its children stay excluded (three gradient layers). show_hsv_spinners now defaults on, so the numeric entry is present without an application asking. | Compliant. The arrows deliberately do not mirror under RTL — the saturation gradient is painted left-to-right in both directions, so a mirrored arrow would disagree with the picture. |
| 5 | crates/teksilo-widgets/src/splitter/handle.rs:261 (capture at :296) | Resize a splitter pane | SplitterModel pane sizes via commit_resize | Yes. on_key :480 — arrows ±keyboard_step_px, Home/End = min/max, Enter = collapse; on_double_tap :427; on_access_action :555 Increment/Decrement/Collapse/Expand, advertised :712-717. | Compliant. |
| 6 | crates/teksilo-widgets/src/docking/resize_handle.rs:234 | Resize a dock side | DockingModel::set_side_size / set_side_visible | Yes. on_key :293 — arrows ±KEYBOARD_STEP, Home = hide, End = show, Enter = toggle; on_double_tap :285; on_access_action :329, advertised :405-409. | Compliant. |
| 7 | table_view/header.rs HeaderCell::build | Column resize (TableView and TreeTableView — one shared header) | column_widths + column_widths_signal via commit_resize | Partial. on_access_action Increment/Decrement by COLUMN_RESIZE_STEP. No keyboard route: the header cell is .focusable(false) and has no on_key. | Make the header cell focusable with a roving tab index across the header row: ←/→ = ±step, Home/End = min/max, plus a header context menu "Column width ▸ Wider / Narrower / Fit to content / Reset" and "Auto-size all columns". The focusable header is the prerequisite shared with row 8. |
| 8 | table_view/header.rs HeaderCell::build | Column reorder (TableView + TreeTableView) | column_order_signal: Signal<Vec<String>> (the strip's on_drop) | None. No on_key, no custom AT action, no context menu — only the programmatic TableView::set_column_order. | A header context menu carrying common::ordered_move's four moves plus its custom actions, exactly as the tab header now does. Corrected: the "optionally Ctrl+Shift+←/→" this row used to propose is not available — table_view/keyboard.rs's arrow arm already reads that chord as an additive selection extension (common::list_nav::selection_op maps accelerator + Shift to ExtendAdditive). The keyboard route therefore needs the focusable header row of row 7. |
| 9 | list_view/body_pane.rs ListBodyPane::build (start_drag) | ListView row reorder | source accept_drop → ListModel order; selection follows | Yes. All four moves: Alt+↑/↓ and Alt+Home/End, four rows in the row's own context menu, four AccessKit custom actions, one utterance per commit. One closure serves all four routes (common::ordered_move::RowMover), and each builds the synthetic RowDragData the drag builds. | Compliant for in-view reorder (export is row 14). |
| 10 | tree_view/body_pane.rs TreeViewBodyPane::build (start_drag) | TreeView row reorder and reparent | TreeSource::sibling_move / reparent → the source's own accept_drop → tree shape | Yes. Sibling reorder: all four moves (Alt+↑/↓, Alt+Home/End) over the row's sibling set. Reparent: the accelerator plus ] / [ on every platform, and Alt+→ / Alt+← (mirrored under RTL) off macOS, where ⌥→/⌥← already expand a subtree; both commit the drag's own DropPosition::Into and sibling-of-parent drops. All six are menu rows and custom actions on the row; a reorder announces the new sibling position, a reparent the new level. An indent reveals its new parent, or the row it moved would be unreachable. | Compliant. |
| 11 | table_view/body_pane.rs BodyPane::build (start_drag) | TableView row reorder | accept_drop → model order | Yes. All four moves, on the same RowMover the other views use: Alt+↑/↓, Alt+Home/End, four menu rows on the row, four custom actions, one utterance. This row's old verdict was stale — an Alt+↑/↓ branch already existed when P39 arrived, in table_view/widget_impl.rs's key-handler wrapper rather than in keyboard.rs, which is where this row looked. | Compliant. |
| 12 | tree_table_view/body_pane.rs TreeBodyPane::build (start_drag) | TreeTableView row reorder and reparent | TreeSource::sibling_move / reparent → tree shape | Yes. The same six commands as row 10, on the same shared closures, still gated on no active sort — a sorted view is not showing the model's order, so moving a row in it would say nothing about where the row went. | Compliant. |
| 13 | grid_view/body_pane.rs GridBodyPane::build (start_drag) | GridView tile reorder | accept_drop_fn → model order | Yes. Alt+the horizontal arrows (±1, mirrored under RTL) and Alt+Home/End are the four named moves; Alt+↑/↓ stays the grid's own whole-row step, which is not one of the four. Four menu rows and four custom actions per tile, one utterance per commit. An application's own tile_context_menu replaces the framework's menu (it is installed first, deliberately); the custom actions survive either way. | Compliant. |
| 14 | crates/teksilo-widgets/src/data_views.rs:1030 (on_drag_ended :1038) | Cross-widget / OS row export from all five views (.exportable, .export_external) | on_rows_transferred_out on the source, on_rows_received on the target | None. No access_custom_action anywhere in the data views; the Alt+arrow routes are strictly in-view. | A cut/paste two-step: Ctrl+X marks the selection for transfer, Ctrl+V on the target view or DropTarget completes it; expose as AT custom actions "Cut rows" / "Paste rows here". Subsumed by row 30 if that lands. |
| 15 | crates/teksilo-widgets/src/grid_view/selection.rs:102, attached at grid_view.rs:1280 | GridView marquee (rubber-band) selection | SelectionModel::select_indices(hits, additive) | Yes. grid_view/keyboard.rs:140 Ctrl/⌘+A = select all (Ctrl+Shift+A = clear); :312/:414 Shift+arrows extend the range. | Compliant — every selection the marquee can produce is reachable. |
| 16 | tab_widget/header.rs TabHeader::build (start_drag_with_preview) | Tab reorder within a bar | on_reorder(from, to) | Yes. All four moves through one closure: Alt+the bar's own two arrows and Alt+Home/End (read ahead of the navigation arms, which do not look at the modifiers), a framework context menu when the delegate supplies none, and four custom actions — the two far ends beside the two steps that shipped. Ids stay explicit (move_action_id), and CLOSE_ACTION_ID keeps its historical 2. Suppressed for a pinned tab, whose order the pinned strip fixes. | Compliant. |
| 17 | crates/teksilo-widgets/src/tab_widget/bar.rs:1880-1990 (on_drop / on_tab_received) | Cross-bar tab transfer (accept_external_tabs) | on_tab_received(item, to_index); on_transfer_out on the source | None. The two custom actions only move within one bar. | AT custom action "Move to other group" + a tab context-menu "Move to ▸ <bar>", mirroring docking/context_menu.rs's move_to_submenu. |
| 18 | crates/teksilo-widgets/src/accordion.rs:568 → crates/teksilo-widgets/src/docking/panel.rs:863 | Drag a dock out of a split pane | promote_to_tab / move_dock / split_into_tab / stack_into_tab | Partial (menu). The ⋮ options menu (docking/context_menu.rs:152) offers "Move to new activity" (:167) and "Move to side ▸" (:176), keyboard-reachable because the button is focusable. The drag itself is gated on DockPolicy::allow_dock_drag. | Mostly covered; the missing placements are row 19. |
| 19 | crates/teksilo-widgets/src/docking/panel.rs:1061 (5 zones declared :1054-1058) | Drag-to-dock: split or stack into a specific pane position | stack_into_tab (centre), split_into_tab(before/after) (edges) | None. Neither split_into_tab nor stack_into_tab appears in docking/context_menu.rs. | Extend dock_options_menu with "Split ▸ Left / Right / Above / Below" and "Group with ▸ <other dock in this activity>". |
| 20 | crates/teksilo-widgets/src/docking/panel.rs:305 and :637 | Drop a tab or dock onto a whole side | move_tab(tab, side, at) / move_dock(dock, side) | Yes (menu). activity_context_menu "Move to ▸ <enabled side>" (context_menu.rs:102, move_to_submenu :278), reachable by Menu key / Shift+F10 on the focusable rail item or tab. | Compliant. |
| 21 | crates/teksilo-widgets/src/docking/activity_bar.rs:2125 (drop at :808) | Activity-rail reorder, and rail → rail / strip transfer | move_tab(tab, side, visible_pos) / promote_to_tab | Transfer: yes (rail item .focusable(true) :2146 + .context_menu(activity_context_menu) :2138). Reorder within the rail: none — the menu has Hide / Move to / checklist / size, no move-up-down. | Add common::ordered_move's four moves to activity_context_menu and Alt+↑/↓, Alt+Home/End to the rail's on_key, using the same visible-index mapping the drop handler computes. |
| 22 | crates/teksilo-widgets/src/tool_box.rs:888 (on_header_drag) | ToolBox section header drag (app-defined hook) | whatever the app's on_header_drag(idx, ctx) does | None from the framework. The header has on_key :787 and Action::Click/Expand/Collapse :942-946, but nothing invokes the drag hook. Unused inside the workspace today. | Ship a paired on_header_move(idx, OrderedMove) hook the widget calls from Alt+↑/↓ / Alt+Home/End, a menu row and a custom action, so every consumer of on_header_drag inherits an alternative rather than writing one. |
| 23 | crates/teksilo-widgets/src/rich_text/mouse.rs:279 (advance :474, commit :583) | Resize an embedded image by its corner grip | on_image_resized(ImageResize{..}) → the text-document model | None. No image keys in rich_text/keyboard.rs; rich_text/context_menu.rs has no image entries. | Context menu "Image size ▸ Larger / Smaller / Original / Custom…" on the selected image, plus Ctrl+Alt+←/→ while selected_image is set; advertise both as AT custom actions. |
| 24 | crates/teksilo-widgets/src/rich_text/mouse.rs:175 (armed via PendingTextDrag :460) | Drag selected text (move / copy) | clipboard-equivalent payload; document mutation on drop | Yes. Ctrl+X / C / V and Ctrl+Shift+V in rich_text/keyboard.rs:296-303 reach the same outcome. | Compliant. |
| 25 | crates/teksilo-scene/src/view/gestures_impl.rs:868 (hit branch ~:955) | SceneView item drag-to-move | SceneModel::set_local_pos for every selected item | Yes. on_key :689 → :717 Alt+arrows nudges the selection (Shift = ×10); the comment at :711 cites 2.5.7. | Compliant. (v1 ignores view rotation.) |
| 26 | same handler, :967 (commit :1145-1161) | SceneView marquee selection | SceneSelection | None. No select-all, no Shift/Ctrl+arrow extend, and the synthetic SyntheticKind::SceneItem nodes advertise no Action::Click / Focus (view/a11y_impl.rs). Lightweight items are not focusable widgets. | Ctrl+A = select all in the scene; Tab / Shift+Tab rove a focus ring over items with Space = toggle-select and Shift+Space = extend; advertise Click + Focus on the synthetic nodes. |
| 27 | same handler, :929-943 (port drag) | Magnetism: connect two magnets by dragging a wire | the consumer's on_connect(MagnetConnection) | Yes. view/magnetism.rs:172 — the connect_key (default m) enters connect mode, arrows/Home/End move the target, Enter/Space confirms, Esc cancels; synthetic SceneMagnet AT nodes with roving active_descendant. | Compliant. |
| 28 | crates/teksilo-widgets/src/drop_zone.rs:471 (hover :442) | Drop OS files onto a DropZone | on_files_dropped / on_text_dropped / on_urls_dropped | Yes. The built-in Browse… button (:371-414, show_browse_button defaults true at :105) opens the native file dialog and calls the same on_files callback. | Compliant. The caveat is real and now documented at the setter: show_browse_button(false) removes the zone's only non-drag route, so an application that hides it owes the same action another affordance. Not a runtime warning — a zone whose surrounding UI already offers a file chooser is a legitimate configuration, and a debug assertion could not tell the two apart. |
| 29 | crates/teksilo-widgets/src/drop_target.rs:695 (on_region_drop :708) | Drop onto a wrapping DropTarget, including its five-zone form | the app's on_drop / on_region_drop(region, ..) | None. accessibility() :759 sets only Role::Group; no keys, no actions, no menu. It is a generic container, so the alternative has to be framework-supplied or app-side. | An optional .on_paste_here(..) plus one AT custom action per declared region ("Drop here", "Drop above", …), and a documented mark-source-then-activate-target pattern. Subsumed by row 30. |
| 30 | crates/teksilo-core/src/widget_tree/drag_drop_impl.rs (start_drag / start_drag_with_preview; begin_external_drag :138) | The generic drag-and-drop pipeline itself | DragSession + the target's on_drop | Escape cancels only. There is no keyboard pick-up / put-down mode anywhere in core. | A framework-level keyboard DnD mode: a "lift" command parks the payload, Tab moves to a target, Enter drops, Esc cancels. Highest-leverage single fix — it retires rows 14, 17, 19 and 29. |
| 31 | title_bar/drag_region.rs DragRegion::build | Move the window (custom chrome) | PlatformTitleBarHost::begin_drag() | None from Teksilo. accessibility() set_hidden()s the region ("no keyboard or AT analogue"), and Teksilo's fallback window menu (window_menu.rs build_window_menu) has Restore / Maximize / Minimize / Close — no Move. on_double_tap toggles maximize, a non-drag route for maximize only. Partly answered off-Teksilo: where the platform owns a window menu (has_window_menu()), a long press or a secondary click asks the OS for it (show_window_menu) and its Move entry is the 2.5.7 route — which is what title-bar.md and drag_region.md claim. The gap is therefore the platforms on the fallback, X11 among them. | Add Move to build_window_menu, driving a keyboard move mode (arrows nudge, Enter commits, Esc reverts) — the Win32 system-menu convention, and the same shape the OS menu already offers elsewhere. |
| 32 | title_bar/resize_strip.rs ResizeStrip::build | Resize the window (custom chrome) | PlatformTitleBarHost::begin_resize(edge) | None. No keys, no AT node, and Teksilo's fallback window menu has no Size entry. Same platform split as row 31: an OS window menu carries Size where the platform owns one. | Add Size to build_window_menu (pick an edge, then arrows), plus a WindowState-driven programmatic resize the menu can call. |
| 33 | crates/teksilo-widgets/src/scroll_bar.rs:393 | Drag the scrollbar thumb | set_scroll → the owner's scroll Signal<f32> | Yes. Track click pages (on_tap :434) — a single-pointer route to any position; wheel via ScrollArea::on_scroll :647; ScrollArea advertises Action::ScrollUp/Down/Left/Right (scroll_area.rs:1188-1199, handled :793-811). Caveat: the bar's own on_key :477 is dead code — the bar is .focusable(false) :372, accessibility() :582 calls set_hidden(), and it is a sibling of the content, so KeyDown never reaches it. | Compliant via track click + AT. Cleanup: delete the dead on_key or move those chords onto ScrollArea. |
| 34 | text_input_field/mouse.rs handle_pointer_event, PointerMove arm | Drag-select text in TextInput / SearchField / SpinBox / PasswordField | cursor.set_position(.., KeepAnchor) | Yes. text_input_field/keyboard.rs handle_key — Shift+arrow extend on Key::ArrowLeft/ArrowRight, Key::A if ctrl select-all (ctrl is Modifiers::command(), so ⌘A on macOS); double/triple tap = word/line. | Compliant. |
| 35 | rich_text/mouse.rs handle_pointer_event, PointerMove arm | Drag-select text in RichTextEditor / Viewer | cursor.set_position(.., KeepAnchor) | Yes. rich_text/keyboard.rs handle_key — Shift+arrows, including the table-cell extension (try_extend_cell_selection); Ctrl/⌘+A runs apply_select_all_ladder. | Compliant. |
| 36 | code_editor/mouse.rs handle_pointer_event, PointerMove arm (Alt-click multi-caret at its PointerDown arm) | Drag-select (and Alt-click multi-caret drag) in CodeEditor / LogView | DragState::Selecting → selection ranges | Yes. code_editor/keyboard.rs handle_key carries the full Shift+arrow set and Key::A if ctrl → CodeCommand::SelectAll. | Compliant. |
| 37 | teksilo-terminal/src/terminal.rs pointer_handler, its Local selection drag branch | Drag-select terminal text | engine.selection_update(row, col, side) | Partial. The widget's on_double_tap / on_triple_tap both call select_at (SelectionKind::Word / Line), so word and line selection need no drag; and the context menu built by terminal/menu.rs build_menu carries Select all, reached by a hold, a right-click, an assistive client's ShowContextMenu, and the framework's keyboard chord — is_context_menu_chord (Menu key, Shift+F10, Ctrl+Shift+M on macOS) is intercepted in pointer_router.rs above the widget, so it reaches the terminal despite its keyboard_capture(true). What is still drag-only is an arbitrary range: no Shift+arrow selection exists, and no key is bound to TerminalController::select_all() — the only chords the widget reads are is_copy_chord / is_paste_chord (⌘C / ⌘V on macOS, Ctrl+Shift+C / Ctrl+Shift+V elsewhere). | Add Shift+arrow / Shift+Home/End range selection over the grid — the terminal has no caret, so this needs a scrollback selection cursor. A direct Select all chord is optional now that the menu row is keyboard-reachable. |
Recorded and dismissed
| Site | Why it carries no 2.5.7 obligation |
|---|---|
Drag auto-scroll ticks — list_view.rs:1608, tree_view/widget_impl.rs:947, table_view.rs:2073, tree_table_view.rs:2105, tab_widget/bar.rs:2060, grid_view.rs | not an operation, just edge scrolling during an already-started drag |
crates/teksilo-scene/src/view/gestures_impl.rs:591 pinch-zoom and :877 hand-drag panning | keyboard equivalents in the same on_key (:804-807 pan, zoom clamps :747+) |
crates/teksilo-scene/src/minimap.rs:242 | on_tap only — not a drag |
crates/teksilo-widgets/src/primitives/text_widget.rs:320 PointerMove | inline-link hover cursor, not a drag |
teksilo-inspector, teksilo-preview-ui, teksilo-webview and teksilo-charts
contain no drag operations at all.
Cross-references
- Grab geometry for every handle above is in the density inventory (Grab class).
- The hover-gated affordances that guard some of these handles — the Splitter handle's dwell reveal in particular — are in the hover census.