Skip to content

Commit 5260225

Browse files
authored
Merge pull request #803 from switchifyapp/codex/reusable-scanner
Extract reusable menu and keyboard scanner core
2 parents c7b6920 + d8fbd5d commit 5260225

8 files changed

Lines changed: 627 additions & 168 deletions

File tree

docs/scanner-architecture.md

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
# Scanner architecture
2+
3+
The scanner has four boundaries: content, traversal, presentation and execution.
4+
All desktop scanning shares the Rust `Session<Technique>` lifecycle and
5+
`scanning_runtime::Adapter` platform boundary. Local and Remote switches feed
6+
that same session. No item scanner captures keys, runs native input, persists
7+
settings or owns a timer thread.
8+
9+
## Adding an item scanner
10+
11+
Use `scan_items::ItemScanner<Action>` with a small typed action enum. Supply
12+
`scan_tree::Node::Group` nodes with stable group IDs and `Leaf` action identities.
13+
Group IDs must be unique among siblings; leaf identities must be unique within
14+
their group. They must identify the operation, not its current label or bounds.
15+
The `rows` convenience constructor is for fixed row layouts: its positional row
16+
IDs are not suitable when groups can reorder. Supply explicit groups in that case.
17+
Empty groups are removed. Explicitly identified groups retain their identity even
18+
with one child. Anonymous branches and fixed single-item rows collapse as before.
19+
20+
Call `handle` with normalized switch actions and `advance` with elapsed time only
21+
when the containing session permits movement. The core owns the interval,
22+
direction, completed passes, suspension and nested Back navigation. Read its
23+
navigator to build a presentation. Do not add a second interval in the provider.
24+
`Policy` preserves the existing menu/keyboard differences during migration:
25+
menus resume their selection; keyboards resume at the root. Keyboard manual
26+
steps reset completed passes; existing menu manual steps do not.
27+
28+
Use `replace` when action availability or layout structure changes. Identified
29+
groups and leaves retain their selection through reordering; a removed target
30+
returns to the nearest surviving ancestor's first item with a full interval.
31+
Any changed content invalidates pending activation. Identical content preserves
32+
both timing and pending activation. Update labels and bounds separately when the
33+
available actions have not changed. Do not put captured text into action IDs.
34+
35+
Before asynchronous execution, call `begin_activation` and retain the returned
36+
revision. While pending, the core does not advance or select. Complete with that
37+
same revision; stale or repeated completions return false. Restart and content
38+
replacement cancel pending activation. The runtime's existing input generation
39+
checks remain authoritative across whole-session replacement, disconnect and
40+
capture cancellation; an item revision is local to one scanner instance.
41+
42+
The tests in `scan_items` provide a platform-free example using editing actions,
43+
including reordered content, cancellation, exactly-once completion and timing.
44+
45+
## Existing providers
46+
47+
- `scan_menu` supplies typed menu actions and tile geometry. It delegates traversal
48+
to the item scanner and retains its existing three-column layout and parent stack.
49+
- `scan_keyboard` supplies keys, modifiers, pages, prediction availability and
50+
geometry. It delegates traversal and pending activation to the item scanner.
51+
Prediction tokens still guard insertion, and queued prediction replacement
52+
remains a keyboard policy. Unchanged suggestions never restart the interval.
53+
- `point_scan` retains its continuous movement strategy and uses the shared
54+
navigator for grid scanning. `point_workflow` composes point, menus, countdown,
55+
keyboard and execution-error stages within one session.
56+
- Native hosts render `Frame` without activating Switchify. Domain actions are
57+
executed through the existing typed workflow requests and platform adapters.
58+
59+
## Following stages
60+
61+
Issue #801 adds validated shared settings and per-area overrides to this core.
62+
Issue #802 adds a main-window React content adapter and foreground/modal ownership.
63+
Those stages do not require individual screens to implement traversal or timers.
64+
They must preserve the current input generation checks, cleanup, native focus,
65+
point countdown cancellation and Remote behaviour.

src-tauri/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ mod protocol;
2525
mod remote_scan;
2626
mod scan_executor;
2727
mod scan_host;
28+
mod scan_items;
2829
mod scan_keyboard;
2930
mod scan_menu;
3031
mod scan_tile;

src-tauri/src/point_workflow.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,7 @@ impl Workflow {
188188
}
189189
Item::Setting(setting) => return Some(Request::Setting(setting)),
190190
Item::Display(next) => return Some(Request::Display(next)),
191-
Item::Pause => self.menu.suspended = true,
191+
Item::Pause => self.menu.suspend(),
192192
Item::Reverse => {
193193
self.menu.handle(Action::Reverse);
194194
}
@@ -260,7 +260,7 @@ impl Technique for Workflow {
260260
self.stage = Stage::Menu;
261261
self.menu = Menu::new(Kind::Actions, self.point.config.block_interval_ms);
262262
self.parent_menu.clear();
263-
self.menu.suspended = true;
263+
self.menu.suspend();
264264
self.error = Some(message);
265265
}
266266
fn execution_succeeded(&mut self) {
@@ -407,7 +407,7 @@ impl Technique for Workflow {
407407
fn phase(&self) -> Phase {
408408
match self.stage {
409409
Stage::KeyboardOpening => Phase::Workflow(WorkflowPhase::KeyboardOpening),
410-
Stage::Keyboard => Phase::Workflow(if self.keyboard.suspended {
410+
Stage::Keyboard => Phase::Workflow(if self.keyboard.suspended() {
411411
WorkflowPhase::KeyboardSuspended
412412
} else {
413413
WorkflowPhase::Keyboard
@@ -417,7 +417,7 @@ impl Technique for Workflow {
417417
Stage::Point => Phase::Point(self.point.phase()),
418418
Stage::Destination => Phase::Workflow(WorkflowPhase::DragDestination),
419419
Stage::Executing => Phase::Workflow(WorkflowPhase::Executing),
420-
Stage::Menu => Phase::Workflow(if self.menu.suspended {
420+
Stage::Menu => Phase::Workflow(if self.menu.suspended() {
421421
WorkflowPhase::MenuSuspended
422422
} else if self.menu.kind == Kind::ConfirmDrag {
423423
WorkflowPhase::DragConfirmation
@@ -826,7 +826,7 @@ mod tests {
826826
w.advance(5000);
827827
assert_eq!(w.frame(), frame);
828828
assert!(w.handle(Action::Select).is_none());
829-
assert!(!w.menu.suspended);
829+
assert!(!w.menu.suspended());
830830
assert_eq!(w.source, (120, 80));
831831
}
832832
#[test]

src-tauri/src/scan_host.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -799,7 +799,9 @@ mod title_tests {
799799
for units in [1.0, 2.0] {
800800
let mut menu = Menu::new(Kind::Actions, 500);
801801
for paused in [false, true] {
802-
menu.suspended = paused;
802+
if paused {
803+
menu.suspend();
804+
}
803805
let frame = menu.frame((0, 0), screen, units);
804806
let label = frame.label.as_ref().unwrap();
805807
let MenuTitle { rect, scale } =

0 commit comments

Comments
 (0)