diff --git a/src-tauri/src/mouse_repeat.rs b/src-tauri/src/mouse_repeat.rs index 5cd835cd..deefbd64 100644 --- a/src-tauri/src/mouse_repeat.rs +++ b/src-tauri/src/mouse_repeat.rs @@ -156,6 +156,37 @@ pub struct ScanMoveRepeat { movement: MoveRepeatState, } +/// The scanned scroll control uses Remote's immediate first step followed by +/// one step per configured interval. A delayed scanner tick never bursts +/// multiple scroll events into the foreground app. +pub struct ScanScrollRepeat { + direction: i8, + elapsed_ms: u64, +} + +impl ScanScrollRepeat { + pub fn new(direction: i8) -> Self { + Self { + direction, + elapsed_ms: 0, + } + } + + pub fn dy(&self) -> i32 { + i32::from(self.direction) * 5 + } + + pub fn advance(&mut self, elapsed_ms: u64, interval_ms: u32) -> bool { + let interval = u64::from(interval_ms.max(1)); + self.elapsed_ms = self.elapsed_ms.saturating_add(elapsed_ms); + if self.elapsed_ms < interval { + return false; + } + self.elapsed_ms %= interval; + true + } +} + impl ScanMoveRepeat { pub fn new(dx: i32, dy: i32, now: Instant) -> Self { Self { @@ -704,6 +735,19 @@ mod tests { assert!((647..=649).contains(&distance)); } + #[test] + fn scanned_scroll_uses_remote_step_and_interval_without_bursting() { + let mut repeat = ScanScrollRepeat::new(-1); + assert_eq!(repeat.dy(), -5); + assert!(!repeat.advance(79, 80)); + assert!(repeat.advance(1, 80)); + assert!(!repeat.advance(40, 80)); + assert!(repeat.advance(40, 80)); + assert!(repeat.advance(250, 80)); + assert!(!repeat.advance(69, 80)); + assert!(repeat.advance(1, 80)); + } + #[test] fn parses_only_bounded_move_and_scroll_commands() { assert_eq!( diff --git a/src-tauri/src/overlay.rs b/src-tauri/src/overlay.rs index 955e1f70..d3209f5f 100644 --- a/src-tauri/src/overlay.rs +++ b/src-tauri/src/overlay.rs @@ -313,10 +313,18 @@ impl OverlayEngine { settings.cursor_overlay_enabled = true; settings.cursor_overlay_visibility = "whileControlling".into(); let update = self.handle(Command::Show(feedback, settings), now); - if dragging && matches!(feedback, PointerFeedback::Scroll { .. }) { + if dragging + && matches!( + feedback, + PointerFeedback::Scroll { .. } | PointerFeedback::RepeatScroll { .. } + ) + { self.drag_active = true; } - if matches!(feedback, PointerFeedback::RepeatMove { .. }) { + if matches!( + feedback, + PointerFeedback::RepeatMove { .. } | PointerFeedback::RepeatScroll { .. } + ) { self.deadline = None; } update @@ -1094,6 +1102,13 @@ mod tests { now, ); assert_eq!(engine.deadline, None); + engine.show_scan_mouse( + PointerFeedback::RepeatScroll { dx: 0, dy: 5 }, + settings.clone(), + now, + ); + assert_eq!(engine.deadline, None); + assert!(engine.drag_active); engine.show_scan_mouse(PointerFeedback::Drag, settings, now); assert!(matches!(engine.end_scan_mouse(now), Update::Hide)); assert!(!engine.visible); diff --git a/src-tauri/src/point_scan_runtime.rs b/src-tauri/src/point_scan_runtime.rs index 9bf4de75..4df96e85 100644 --- a/src-tauri/src/point_scan_runtime.rs +++ b/src-tauri/src/point_scan_runtime.rs @@ -222,6 +222,7 @@ impl Adapter for PointScan { settings.pointer_scale_percent, settings.mouse_repeat_acceleration_duration_ms, settings.move_repeat_interval_ms, + settings.scroll_repeat_interval_ms, settings.mouse_repeat_enabled, ); } else if technique.keyboard_open() { @@ -244,6 +245,7 @@ impl Adapter for PointScan { | Request::Prediction { .. } | Request::MouseMove { .. } | Request::MouseMoveAbsolute { .. } + | Request::MouseScroll { .. } ) } fn deferred(request: &Request) -> bool { @@ -387,6 +389,7 @@ mod tests { token: 1, index: 0 })); + assert!(PointScan::preserve_visuals(&Request::MouseScroll { dy: 5 })); assert!(!PointScan::preserve_visuals( &crate::point_workflow::default_click((10, 20)) )); diff --git a/src-tauri/src/point_workflow.rs b/src-tauri/src/point_workflow.rs index eeefd408..60f11566 100644 --- a/src-tauri/src/point_workflow.rs +++ b/src-tauri/src/point_workflow.rs @@ -84,6 +84,7 @@ pub enum WorkflowPhase { Mouse, MouseSuspended, MouseMoving, + MouseScrolling, Menu, MenuSuspended, AutoSelecting, @@ -97,6 +98,7 @@ enum Stage { KeyboardOpening, Mouse, MouseMoving, + MouseScrolling, Idle, Countdown, Point, @@ -111,8 +113,10 @@ pub struct Workflow { mouse_area: Rect, return_to_mouse: bool, move_repeat: Option, + scroll_repeat: Option, mouse_repeat_enabled: bool, mouse_move_interval_ms: u32, + mouse_scroll_interval_ms: u32, mouse_acceleration_ms: u32, point: Engine, stage: Stage, @@ -140,8 +144,10 @@ impl Workflow { mouse_area: screen, return_to_mouse: false, move_repeat: None, + scroll_repeat: None, mouse_repeat_enabled: true, mouse_move_interval_ms: 250, + mouse_scroll_interval_ms: 250, mouse_acceleration_ms: 1000, point: Engine::new(config, screen, scale)?, stage: Stage::Idle, @@ -157,7 +163,9 @@ impl Workflow { fn active_preferences(&self) -> crate::scan_preferences::Resolved { match self.stage { Stage::Keyboard | Stage::KeyboardOpening => self.point.config.keyboard_scan, - Stage::Mouse | Stage::MouseMoving => self.point.config.mouse_scan, + Stage::Mouse | Stage::MouseMoving | Stage::MouseScrolling => { + self.point.config.mouse_scan + } Stage::Menu => self.point.config.menu_scan, _ => self.point.config.scan, } @@ -187,6 +195,7 @@ impl Workflow { ); self.return_to_mouse = false; self.move_repeat = None; + self.scroll_repeat = None; self.parent_menu.clear(); self.stage = Stage::Point; self.point.start(); @@ -205,12 +214,21 @@ impl Workflow { matches!(self.stage, Stage::Keyboard | Stage::KeyboardOpening) } pub fn mouse_open(&self) -> bool { - matches!(self.stage, Stage::Mouse | Stage::MouseMoving) + matches!( + self.stage, + Stage::Mouse | Stage::MouseMoving | Stage::MouseScrolling + ) } pub fn control_mode(&self) -> crate::point_scan::ControlMode { self.point.config.control_mode } pub fn set_mouse_area(&mut self, area: Rect, screen: Rect, scale: f64, displays: usize) { + if self.point.screen != screen && self.stage == Stage::MouseScrolling { + self.stage = Stage::Mouse; + self.scroll_repeat = None; + self.pending = None; + self.mouse.restart(); + } self.mouse_area = area; self.point.screen = screen; self.point.units_per_logical_pixel = scale; @@ -221,15 +239,18 @@ impl Workflow { speed: u16, acceleration_ms: u32, move_interval_ms: u32, + scroll_interval_ms: u32, repeat_enabled: bool, ) { self.mouse.speed_percent = speed; self.mouse_acceleration_ms = acceleration_ms; self.mouse_move_interval_ms = move_interval_ms; + self.mouse_scroll_interval_ms = scroll_interval_ms; self.mouse_repeat_enabled = repeat_enabled; - if !repeat_enabled && self.stage == Stage::MouseMoving { + if !repeat_enabled && matches!(self.stage, Stage::MouseMoving | Stage::MouseScrolling) { self.stage = Stage::Mouse; self.move_repeat = None; + self.scroll_repeat = None; self.pending = None; self.mouse.restart(); } @@ -244,6 +265,10 @@ impl Workflow { accelerated: self.mouse_acceleration_ms > 0, dragging: self.mouse.dragging, }), + Stage::MouseScrolling => Some(PointerFeedback::RepeatScroll { + dx: 0, + dy: self.scroll_repeat.as_ref()?.dy(), + }), Stage::Mouse => Some(if self.mouse.dragging { PointerFeedback::Drag } else { @@ -278,6 +303,7 @@ impl Workflow { self.point.config.control_mode = crate::point_scan::ControlMode::Mouse; self.stage = Stage::Mouse; self.move_repeat = None; + self.scroll_repeat = None; self.mouse = crate::scan_mouse::MousePanel::new( self.point.config.mouse_scan, 1, @@ -340,9 +366,19 @@ impl Workflow { self.mouse.dragging = !self.mouse.dragging; Some(Request::MouseDrag) } - Key::Scroll(direction) => Some(Request::MouseScroll { - dy: i32::from(direction) * 5, - }), + Key::Scroll(direction) => { + if self.mouse_repeat_enabled { + let repeat = crate::mouse_repeat::ScanScrollRepeat::new(direction); + let dy = repeat.dy(); + self.scroll_repeat = Some(repeat); + self.stage = Stage::MouseScrolling; + Some(Request::MouseScroll { dy }) + } else { + Some(Request::MouseScroll { + dy: i32::from(direction) * 5, + }) + } + } Key::Speed(direction) => Some(Request::MouseSpeed(direction)), Key::Monitor(dx, dy) => Some(Request::MouseMonitor(dx, dy)), Key::Keyboard => { @@ -482,6 +518,7 @@ impl Technique for Workflow { if self.mouse_open() { self.stage = Stage::Mouse; self.move_repeat = None; + self.scroll_repeat = None; self.mouse.dragging = false; self.mouse.failed(); self.error = None; @@ -525,6 +562,7 @@ impl Technique for Workflow { ); self.return_to_mouse = false; self.move_repeat = None; + self.scroll_repeat = None; self.stage = Stage::Idle; self.parent_menu.clear(); self.pending = None; @@ -544,11 +582,12 @@ impl Technique for Workflow { self.active_preferences().automatic } fn switch_pressed(&mut self) -> bool { - if self.stage != Stage::MouseMoving { + if !matches!(self.stage, Stage::MouseMoving | Stage::MouseScrolling) { return false; } self.stage = Stage::Mouse; self.move_repeat = None; + self.scroll_repeat = None; self.pending = None; self.mouse.restart(); true @@ -592,7 +631,7 @@ impl Technique for Workflow { return self.mouse_key(key); } } - Stage::MouseMoving => {} + Stage::MouseMoving | Stage::MouseScrolling => {} Stage::Point | Stage::Destination => { if let Some(point) = self.point.handle(action) { if self.stage == Stage::Destination { @@ -667,6 +706,12 @@ impl Technique for Workflow { if dx != 0 || dy != 0 { self.pending = Some(Request::MouseMove { dx, dy }); } + } else if self.stage == Stage::MouseScrolling { + if let Some(repeat) = self.scroll_repeat.as_mut() { + if repeat.advance(ms, self.mouse_scroll_interval_ms) { + self.pending = Some(Request::MouseScroll { dy: repeat.dy() }); + } + } } else if self.stage == Stage::Countdown { if !context.paused && !context.switch_held { self.elapsed = self @@ -709,6 +754,7 @@ impl Technique for Workflow { WorkflowPhase::Mouse }), Stage::MouseMoving => Phase::Workflow(WorkflowPhase::MouseMoving), + Stage::MouseScrolling => Phase::Workflow(WorkflowPhase::MouseScrolling), Stage::Keyboard => Phase::Workflow(if self.keyboard.suspended() { WorkflowPhase::KeyboardSuspended } else { @@ -735,11 +781,19 @@ impl Technique for Workflow { self.point.units_per_logical_pixel, self.point.config.scanner_color, ), - Stage::Mouse | Stage::MouseMoving => self.mouse.frame( + Stage::Mouse | Stage::MouseMoving | Stage::MouseScrolling => self.mouse.frame( self.mouse_area, self.point.units_per_logical_pixel, self.point.config.mouse_scan.color, - self.stage == Stage::MouseMoving, + match self.stage { + Stage::MouseMoving => Some(crate::scan_mouse::RepeatPrompt::Moving), + Stage::MouseScrolling => Some(crate::scan_mouse::RepeatPrompt::Scrolling( + self.scroll_repeat + .as_ref() + .map_or(1, |repeat| repeat.dy().signum()), + )), + _ => None, + }, ), Stage::Countdown => { let scale = self.point.units_per_logical_pixel; @@ -924,7 +978,7 @@ mod tests { let selected: Vec<_> = session .technique .mouse - .frame(screen, 1.0, Default::default(), false) + .frame(screen, 1.0, Default::default(), None) .tiles .into_iter() .filter(|tile| tile.selected) @@ -946,7 +1000,7 @@ mod tests { }; let mut workflow = Workflow::new(Config::default().point(), screen, 1.0).unwrap(); workflow.open_mouse(); - workflow.set_mouse_settings(50, 1000, 250, false); + workflow.set_mouse_settings(50, 1000, 250, 250, false); assert_eq!( workflow.mouse_key(crate::scan_mouse::Key::Move(-1, 1)), Some(Request::MouseMove { dx: -6, dy: 6 }) @@ -968,6 +1022,113 @@ mod tests { assert!(!workflow.switch_pressed()); } + #[test] + fn scanned_scroll_repeats_at_saved_interval_and_stops_on_press() { + let mut workflow = session(false).technique; + workflow.open_mouse(); + workflow.set_mouse_settings(100, 1000, 250, 120, true); + for (direction, dy) in [(1, 5), (-1, -5)] { + assert_eq!( + workflow.mouse_key(crate::scan_mouse::Key::Scroll(direction)), + Some(Request::MouseScroll { dy }) + ); + assert_eq!( + workflow.phase(), + Phase::Workflow(WorkflowPhase::MouseScrolling) + ); + assert_eq!( + workflow.mouse_feedback(), + Some(crate::input::PointerFeedback::RepeatScroll { dx: 0, dy }) + ); + workflow.update( + 119, + UpdateContext { + movement_enabled: true, + paused: false, + switch_held: false, + }, + ); + assert_eq!(workflow.take_selection(), None); + workflow.update( + 1, + UpdateContext { + movement_enabled: true, + paused: false, + switch_held: false, + }, + ); + assert_eq!(workflow.take_selection(), Some(Request::MouseScroll { dy })); + workflow.update( + 370, + UpdateContext { + movement_enabled: true, + paused: false, + switch_held: false, + }, + ); + assert_eq!(workflow.take_selection(), Some(Request::MouseScroll { dy })); + assert!(workflow.switch_pressed()); + assert!(!workflow.switch_pressed()); + assert_eq!(workflow.phase(), Phase::Workflow(WorkflowPhase::Mouse)); + workflow.update( + 120, + UpdateContext { + movement_enabled: true, + paused: false, + switch_held: false, + }, + ); + assert_eq!(workflow.take_selection(), None); + } + } + + #[test] + fn scanned_scroll_single_step_and_cleanup_paths() { + let mut workflow = session(false).technique; + workflow.open_mouse(); + workflow.set_mouse_settings(100, 1000, 250, 80, false); + assert_eq!( + workflow.mouse_key(crate::scan_mouse::Key::Scroll(1)), + Some(Request::MouseScroll { dy: 5 }) + ); + assert_eq!(workflow.phase(), Phase::Workflow(WorkflowPhase::Mouse)); + assert!(!workflow.switch_pressed()); + workflow.set_mouse_settings(100, 1000, 250, 80, true); + workflow.mouse_key(crate::scan_mouse::Key::Scroll(-1)); + workflow.execution_failed("input failed".into()); + assert_eq!(workflow.mouse_feedback(), None); + assert_eq!( + workflow.scroll_repeat.as_ref().map(|repeat| repeat.dy()), + None + ); + workflow.handle(Action::Select); + workflow.mouse_key(crate::scan_mouse::Key::Scroll(1)); + workflow.set_mouse_settings(100, 1000, 250, 80, false); + assert_eq!(workflow.phase(), Phase::Workflow(WorkflowPhase::Mouse)); + assert!(!workflow.switch_pressed()); + workflow.set_mouse_settings(100, 1000, 250, 80, true); + workflow.mouse_key(crate::scan_mouse::Key::Scroll(1)); + let next_screen = Rect { + x: 1000.0, + y: 20.0, + width: 1000.0, + height: 800.0, + }; + workflow.set_mouse_area(next_screen, next_screen, 1.0, 2); + assert_eq!(workflow.phase(), Phase::Workflow(WorkflowPhase::Mouse)); + assert_eq!( + workflow.scroll_repeat.as_ref().map(|repeat| repeat.dy()), + None + ); + workflow.mouse_key(crate::scan_mouse::Key::Scroll(1)); + workflow.reset(); + assert_eq!( + workflow.scroll_repeat.as_ref().map(|repeat| repeat.dy()), + None + ); + assert_eq!(workflow.mouse_feedback(), None); + } + #[test] fn mouse_keyboard_return_and_drag_state() { let screen = Rect { diff --git a/src-tauri/src/scan_mouse.rs b/src-tauri/src/scan_mouse.rs index d2a3cd5d..4fa9c31f 100644 --- a/src-tauri/src/scan_mouse.rs +++ b/src-tauri/src/scan_mouse.rs @@ -23,6 +23,12 @@ pub enum Key { Close, } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum RepeatPrompt { + Moving, + Scrolling(i32), +} + pub struct MousePanel { pub more: bool, pub top: bool, @@ -163,13 +169,24 @@ impl MousePanel { _ => TileRole::Utility, } } - /// `moving` shows the repeat-movement prompt and highlights nothing. - pub fn frame(&self, screen: Rect, units: f64, color: ScannerColor, moving: bool) -> Frame { + /// A repeating action shows its stop prompt and highlights nothing. + pub fn frame( + &self, + screen: Rect, + units: f64, + color: ScannerColor, + repeating: Option, + ) -> Frame { let row_scan = self.scan.row_scan(); let (active_row, active_column) = self.scan.position(&self.rows); let escaping = self.scan.nav.escaping(); - let status = if moving { - "Moving pointer · Press any switch to stop".to_owned() + let status = if let Some(repeating) = repeating { + match repeating { + RepeatPrompt::Moving => "Moving pointer · Press any switch to stop", + RepeatPrompt::Scrolling(dy) if dy > 0 => "Scrolling up · Press any switch to stop", + RepeatPrompt::Scrolling(_) => "Scrolling down · Press any switch to stop", + } + .to_owned() } else if self.error { "Mouse action failed · Select to resume".to_owned() } else if self.scan.suspended { @@ -187,7 +204,7 @@ impl MousePanel { &self.label(self.rows[active_row][active_column.unwrap_or(0)]), ) }; - let scanning = !moving && !self.scan.suspended; + let scanning = repeating.is_none() && !self.scan.suspended; Panel { rows: self .rows @@ -259,7 +276,7 @@ mod tests { width: 1280.0, height: 720.0, }; - let frame = panel.frame(screen, 1.0, ScannerColor::default(), false); + let frame = panel.frame(screen, 1.0, ScannerColor::default(), None); assert!(frame.tiles.iter().any(|tile| tile.selected)); assert!(frame .tiles @@ -284,7 +301,7 @@ mod tests { .unwrap() .rect }; - let frame = panel.frame(screen, 1.0, ScannerColor::default(), false); + let frame = panel.frame(screen, 1.0, ScannerColor::default(), None); assert_eq!(tile(&frame, "←").x, tile(&frame, "↖").x); assert_eq!(tile(&frame, "←").width, tile(&frame, "↖").width); assert_eq!(tile(&frame, "→").x, tile(&frame, "↗").x); @@ -295,7 +312,7 @@ mod tests { panel.handle(Action::Next); panel.handle(Action::Select); panel.handle(Action::Next); - let frame = panel.frame(screen, 1.0, ScannerColor::default(), false); + let frame = panel.frame(screen, 1.0, ScannerColor::default(), None); let selected: Vec<_> = frame.tiles.iter().filter(|tile| tile.selected).collect(); assert_eq!(selected.len(), 1); assert_eq!(selected[0].text, "→"); @@ -312,7 +329,7 @@ mod tests { }; let color = ScannerColor::default(); let mut panel = MousePanel::new(Resolved::default(), 1, 100); - let frame = panel.frame(screen, 1.0, color, false); + let frame = panel.frame(screen, 1.0, color, None); let keyboard = crate::scan_keyboard::Keyboard::new(false).frame(screen, 1.0, color); assert_eq!(frame.tiles[0].rect, keyboard.tiles[0].rect); assert!(frame.label.is_none()); @@ -325,23 +342,32 @@ mod tests { assert_eq!(close.style.unwrap().role, TileRole::Toolbar); panel.handle(Action::Select); - let status = panel.frame(screen, 1.0, color, false).tiles.pop().unwrap(); + let status = panel.frame(screen, 1.0, color, None).tiles.pop().unwrap(); assert_eq!(status.text, "Movement · Select Left click"); panel.choose(Key::More); panel.handle(Action::Select); - let status = panel.frame(screen, 1.0, color, false).tiles.pop().unwrap(); + let status = panel.frame(screen, 1.0, color, None).tiles.pop().unwrap(); assert_eq!(status.text, "More controls · Select Slower 100%"); - let moving = panel.frame(screen, 1.0, color, true); + let moving = panel.frame(screen, 1.0, color, Some(RepeatPrompt::Moving)); assert!(moving.tiles.iter().all(|tile| !tile.selected)); assert_eq!( moving.tiles.last().unwrap().text, "Moving pointer · Press any switch to stop" ); + for (direction, expected) in [ + (1, "Scrolling up · Press any switch to stop"), + (-1, "Scrolling down · Press any switch to stop"), + ] { + let scrolling = + panel.frame(screen, 1.0, color, Some(RepeatPrompt::Scrolling(direction))); + assert!(scrolling.tiles.iter().all(|tile| !tile.selected)); + assert_eq!(scrolling.tiles.last().unwrap().text, expected); + } panel.failed(); - let failed = panel.frame(screen, 1.0, color, false); + let failed = panel.frame(screen, 1.0, color, None); assert!(failed.tiles.iter().all(|tile| !tile.selected)); assert_eq!( failed.tiles.last().unwrap().text, diff --git a/src-tauri/src/scanning_runtime.rs b/src-tauri/src/scanning_runtime.rs index c7758e8b..db284b45 100644 --- a/src-tauri/src/scanning_runtime.rs +++ b/src-tauri/src/scanning_runtime.rs @@ -521,6 +521,12 @@ fn dispatch( engine.technique.execution_succeeded(); } } + let pointer_feedback = mouse_action_feedback( + pointer_feedback, + d.engine + .as_ref() + .and_then(|engine| A::cursor_feedback(&engine.technique)), + ); let mouse_token = d .cursor_suppression .map(|(token, _)| token) @@ -549,6 +555,19 @@ fn dispatch( } Ok(()) } + +fn mouse_action_feedback( + action: Option, + active: Option, +) -> Option { + match (action, active) { + ( + Some(crate::input::PointerFeedback::Scroll { .. }), + Some(repeat @ crate::input::PointerFeedback::RepeatScroll { .. }), + ) => Some(repeat), + (feedback, _) => feedback, + } +} fn render_countdown(countdown: Option<&crate::scanning::Countdown>) -> Result<(), String> { COUNTDOWN.with(|slot| { let mut slot = slot.borrow_mut(); @@ -1117,6 +1136,19 @@ mod config_file_tests { #[cfg(test)] mod ownership_tests { + #[test] + fn repeat_scroll_feedback_stays_persistent_after_each_step() { + use crate::input::PointerFeedback; + let step = Some(PointerFeedback::Scroll { dx: 0, dy: 5 }); + let repeating = Some(PointerFeedback::RepeatScroll { dx: 0, dy: 5 }); + assert_eq!(super::mouse_action_feedback(step, repeating), repeating); + assert_eq!( + super::mouse_action_feedback(step, Some(PointerFeedback::Move)), + step + ); + assert_eq!(super::mouse_action_feedback(step, None), step); + } + #[test] fn keyboard_updates_preserve_unchanged_tiles_and_restore_background_order() { let keyboard = crate::scan_keyboard::Keyboard::new(false); diff --git a/src/scanning/useScanning.ts b/src/scanning/useScanning.ts index 94ac4e3b..eab673e7 100644 --- a/src/scanning/useScanning.ts +++ b/src/scanning/useScanning.ts @@ -24,7 +24,7 @@ export type PointScanState = { remote?: boolean; config: PointScanConfig; enabled: boolean; - phase: "autoSelecting" | "idle" | "row" | "rowEscape" | "cell" | "x" | "y" | "menu" | "menuSuspended" | "dragDestination" | "dragConfirmation" | "executing" | "keyboard" | "keyboardSuspended" | "keyboardOpening" | "mouse" | "mouseSuspended" | "mouseMoving"; + phase: "autoSelecting" | "idle" | "row" | "rowEscape" | "cell" | "x" | "y" | "menu" | "menuSuspended" | "dragDestination" | "dragConfirmation" | "executing" | "keyboard" | "keyboardSuspended" | "keyboardOpening" | "mouse" | "mouseSuspended" | "mouseMoving" | "mouseScrolling"; paused: boolean; message: string; supported: boolean; diff --git a/src/settings/PointerSection.tsx b/src/settings/PointerSection.tsx index fff516b8..69e20d11 100644 --- a/src/settings/PointerSection.tsx +++ b/src/settings/PointerSection.tsx @@ -40,7 +40,7 @@ export function PointerSection({ settings, update }: { settings: AppSettings; up
- update("mouseRepeatEnabled", value)} /> + update("mouseRepeatEnabled", value)} />
legend="Movement interval" columns="four" disabled={!settings.mouseRepeatEnabled} options={secondsOptions(repeatIntervalOptions)} value={settings.moveRepeatIntervalMs} onChange={(next) => update("moveRepeatIntervalMs", next)} /> legend="Movement acceleration" columns="four" disabled={!settings.mouseRepeatEnabled} options={accelerationOptions} value={settings.mouseRepeatAccelerationDurationMs} onChange={(next) => update("mouseRepeatAccelerationDurationMs", next)} /> diff --git a/src/settings/ScannerPreferences.tsx b/src/settings/ScannerPreferences.tsx index f7e6dfa8..adf856db 100644 --- a/src/settings/ScannerPreferences.tsx +++ b/src/settings/ScannerPreferences.tsx @@ -68,7 +68,7 @@ export function ScannerPreferences({ controller }: { controller: ScanningControl

{names[area]}

{descriptions[area]} Change any value to customise it; other settings follow the shared defaults.

} {area === 'shared' &&
{(['point', 'mouse'] as const).map(key => )}
} - {area === 'mouse' &&

The ring stays visible while Mouse is open. Select a direction to move; with Repeat mouse movement on, the next switch press stops movement. Pointer speed and repeat controls are under Mouse in the sidebar. Switch to Point returns to screen selection.

} + {area === 'mouse' &&

The ring stays visible while Mouse is open. Select a direction to move or a scroll tile to scroll; with Repeat mouse movement and scrolling on, the next switch press stops either action. Pointer speed and repeat controls are under Mouse in the sidebar. Switch to Point returns to screen selection.

} {area === 'point' && <> { expect(screen.getByRole("heading", { name: "Mouse", level: 1 })).toBeInTheDocument(); expect(screen.getByText(/Pointer speed and repeat for Mouse scanning and Remote/)).toBeInTheDocument(); expect(screen.queryByRole("checkbox", { name: "Click when I stop" })).not.toBeInTheDocument(); - expect(screen.getByRole("checkbox", { name: "Repeat mouse movement" })).toBeChecked(); + expect(screen.getByRole("checkbox", { name: "Repeat mouse movement and scrolling" })).toBeChecked(); fireEvent.click(screen.getByRole("button", { name: "Repeat timing" })); expect(screen.getByRole("slider", { name: "Pointer speed" })).toHaveValue("5.0"); - expect(screen.getByRole("checkbox", { name: "Repeat mouse movement" })).toBeChecked(); + expect(screen.getByRole("checkbox", { name: "Repeat mouse movement and scrolling" })).toBeChecked(); expect(screen.getByRole("group", { name: "Movement acceleration" })).not.toBeDisabled(); expect(screen.getAllByRole("button", { name: "Medium" })[0]).toHaveAttribute("aria-pressed", "true"); setPointerSpeed(50); @@ -191,7 +191,7 @@ describe("Switchify PC settings", () => { fireEvent.click(screen.getByRole("button", { name: "Fine tune speed" })); fireEvent.click(screen.getByRole("button", { name: "Faster" })); expect(screen.getByRole("slider", { name: "Pointer speed" })).toHaveAttribute("aria-valuetext", `Level ${speedLevel(55).toFixed(3)} of 10`); - fireEvent.click(screen.getByRole("checkbox", { name: "Repeat mouse movement" })); + fireEvent.click(screen.getByRole("checkbox", { name: "Repeat mouse movement and scrolling" })); expect(screen.getByRole("group", { name: "Movement acceleration" })).toBeDisabled(); });