Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 32 additions & 1 deletion src-tauri/src/point_scan.rs
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ impl PointSettings {
fn validate(&self) -> Result<(), String> {
if self.speed > 4
|| !(2..=10).contains(&self.grid_size)
|| !(250..=5000).contains(&self.block_interval_ms)
|| !(100..=10000).contains(&self.block_interval_ms)
|| !(100..=100_000).contains(&self.auto_select_delay_ms)
|| !self.auto_select_delay_ms.is_multiple_of(100)
{
Expand Down Expand Up @@ -513,6 +513,37 @@ impl Technique for Engine {

#[cfg(test)]
mod tests {
#[test]
fn auto_scan_interval_bounds_and_legacy_values_roundtrip() {
for interval in [100, 200, 250, 750, 1000, 10000] {
let mut config = super::Config {
block_interval_ms: interval,
..Default::default()
};
config.scan_preferences.point.interval_ms = Some(interval);
config.scan_preferences.menu.interval_ms = Some(interval);
config.scan_preferences.keyboard.interval_ms = Some(interval);
config.validate().unwrap();
let restored: super::Config =
serde_json::from_str(&serde_json::to_string(&config).unwrap()).unwrap();
for area in [
crate::scan_preferences::Area::Point,
crate::scan_preferences::Area::Menu,
crate::scan_preferences::Area::Keyboard,
] {
assert_eq!(restored.resolved(area).interval_ms, interval);
}
}
for interval in [0, 99, 10001, u64::MAX] {
assert!(super::Config {
block_interval_ms: interval,
..Default::default()
}
.validate()
.is_err());
}
}

use super::*;
use crate::scanning::Session;
fn engine(config: Config) -> Session<Engine> {
Expand Down
28 changes: 11 additions & 17 deletions src-tauri/src/scan_menu.rs
Original file line number Diff line number Diff line change
Expand Up @@ -378,21 +378,15 @@ impl Setting {
}
}
pub fn apply(self, config: &mut crate::point_scan::Config) {
const RATES: &[u64] = &[250, 500, 750, 1000, 1500, 2000, 3000, 4000, 5000];
match self {
Self::FasterScan | Self::SlowerScan => {
let current = config
.resolved(crate::scan_preferences::Area::Menu)
.interval_ms;
let next = if self == Self::FasterScan {
RATES
.iter()
.rev()
.copied()
.find(|v| *v < current)
.unwrap_or(RATES[0])
current.saturating_sub(100).max(100)
} else {
RATES.iter().copied().find(|v| *v > current).unwrap_or(5000)
current.saturating_add(100).min(10000)
};
if config.scan_preferences.menu.interval_ms.is_some() {
config.scan_preferences.menu.interval_ms = Some(next);
Expand Down Expand Up @@ -640,25 +634,25 @@ mod tests {
assert!(visited.contains(&Kind::Zoom));
}
#[test]
fn settings_step_presets_and_clamp_at_limits() {
fn settings_step_tenths_and_clamp_at_limits() {
let mut c = crate::point_scan::Config {
block_interval_ms: 650,
..Default::default()
};
Setting::FasterScan.apply(&mut c);
assert_eq!(c.block_interval_ms, 500);
assert_eq!(c.block_interval_ms, 550);
Setting::SlowerScan.apply(&mut c);
assert_eq!(c.block_interval_ms, 750);
for _ in 0..20 {
assert_eq!(c.block_interval_ms, 650);
for _ in 0..110 {
Setting::FasterScan.apply(&mut c);
Setting::SlowerLine.apply(&mut c);
}
assert_eq!((c.block_interval_ms, c.speed), (250, 0));
for _ in 0..20 {
assert_eq!((c.block_interval_ms, c.speed), (100, 0));
for _ in 0..110 {
Setting::SlowerScan.apply(&mut c);
Setting::FasterLine.apply(&mut c);
}
assert_eq!((c.block_interval_ms, c.speed), (5000, 4));
assert_eq!((c.block_interval_ms, c.speed), (10000, 4));
c.validate().unwrap();
}
#[test]
Expand Down Expand Up @@ -732,12 +726,12 @@ mod tests {
let mut c = crate::point_scan::Config::default();
c.scan_preferences.menu.interval_ms = Some(500);
Setting::SlowerScan.apply(&mut c);
assert_eq!(c.scan_preferences.menu.interval_ms, Some(750));
assert_eq!(c.scan_preferences.menu.interval_ms, Some(600));
assert_eq!(c.block_interval_ms, 1000);
assert_eq!(c.point().block_interval_ms, 1000);
c.scan_preferences.menu.interval_ms = None;
Setting::FasterScan.apply(&mut c);
assert_eq!(c.block_interval_ms, 750);
assert_eq!(c.block_interval_ms, 900);
assert_eq!(c.scan_preferences.menu.interval_ms, None);
}
}
4 changes: 2 additions & 2 deletions src-tauri/src/scan_preferences.rs
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ impl Preferences {
automatic: local.automatic.unwrap_or(automatic),
interval_ms: local
.interval_ms
.filter(|v| (250..=5000).contains(v))
.filter(|v| (100..=10000).contains(v))
.unwrap_or(interval_ms),
direction: local.direction.unwrap_or(self.direction),
pass_limit: local.pass_limit.filter(|v| valid_passes(*v)).unwrap_or(
Expand Down Expand Up @@ -146,7 +146,7 @@ pub fn deserialize_preferences<'de, D: serde::Deserializer<'de>>(
&mut preferences.menu,
&mut preferences.keyboard,
] {
local.interval_ms = local.interval_ms.filter(|v| (250..=5000).contains(v));
local.interval_ms = local.interval_ms.filter(|v| (100..=10000).contains(v));
local.pass_limit = local.pass_limit.filter(|v| valid_passes(*v));
}
Ok(preferences)
Expand Down
31 changes: 24 additions & 7 deletions src/PointScan.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -279,17 +279,17 @@ it("keeps rate values in manual mode and preserves keyboard settings when resett
await screen.findByText(initial.message);
fireEvent.click(screen.getByRole("button", { name: "Customise keyboard" }));
fireEvent.click(screen.getByRole("checkbox", { name: "Word prediction" }));
fireEvent.change(screen.getByLabelText("Auto scan rate"), { target: { value: "500" } });
fireEvent.click(screen.getByRole("button", { name: "Decrease auto scan interval by 0.1 seconds" }));
fireEvent.click(screen.getByRole("checkbox", { name: "Automatic scanning" }));
expect(screen.getByLabelText("Auto scan rate")).toBeDisabled();
expect(screen.getByLabelText("Auto scan rate")).toHaveValue("500");
expect(screen.getByRole("button", { name: "Decrease auto scan interval by 0.1 seconds" })).toBeDisabled();
expect(screen.getByLabelText("Auto scan rate")).toHaveTextContent("0.9 s");
fireEvent.click(screen.getByRole("button", { name: "Use default for auto scan rate" }));
await waitFor(() => expect(screen.getByRole("group", { name: "Auto scan rate setting" })).toHaveFocus());
expect(screen.getByLabelText("Auto scan rate")).toBeDisabled();
expect(screen.getByLabelText("Auto scan rate")).toHaveValue("1000");
expect(screen.getByRole("button", { name: "Decrease auto scan interval by 0.1 seconds" })).toBeDisabled();
expect(screen.getByLabelText("Auto scan rate")).toHaveTextContent("1 s");
fireEvent.click(screen.getByRole("button", { name: "Reset scanning overrides" }));
expect(screen.getByLabelText("Auto scan rate")).toBeEnabled();
expect(screen.getByLabelText("Auto scan rate")).toHaveValue("1000");
expect(screen.getByRole("button", { name: "Decrease auto scan interval by 0.1 seconds" })).toBeEnabled();
expect(screen.getByLabelText("Auto scan rate")).toHaveTextContent("1 s");
expect(screen.getByRole("checkbox", { name: "Word prediction" })).not.toBeChecked();
await waitFor(() => expect(mocks.invoke).toHaveBeenLastCalledWith("configure_point_scan", {
config: expect.objectContaining({ wordPrediction: false, scanPreferences: expect.objectContaining({ keyboard: {} }) })
Expand Down Expand Up @@ -364,3 +364,20 @@ it("saves the keyboard after-typing choice and preserves it while manual", async
await waitFor(() => expect(screen.getByRole("button", { name: "Wait for Select" })).toBeEnabled());
expect(screen.getByRole("button", { name: "Wait for Select" })).toHaveAttribute("aria-pressed", "true");
});

it.each([100, 250, 750, 10000])("adjusts and persists the shared interval from %i ms without rounding legacy values", async (rate) => {
mocks.invoke.mockImplementation(async (command, args) => command === "get_point_scan"
? { ...initial, config: { ...initial.config, blockIntervalMs: rate } } : { ...initial, ...args });
render(<PointScan />);
await screen.findByText(initial.message);
const decrease = screen.getByRole("button", { name: "Decrease auto scan interval by 0.1 seconds" });
const increase = screen.getByRole("button", { name: "Increase auto scan interval by 0.1 seconds" });
expect(decrease).toHaveProperty("disabled", rate === 100);
expect(increase).toHaveProperty("disabled", rate === 10000);
const next = rate === 100 ? 200 : Math.max(100, rate - 100);
fireEvent.click(rate === 100 ? increase : decrease);
expect(screen.getByLabelText("Auto scan rate")).toHaveTextContent(`${next / 1000} s`);
await waitFor(() => expect(mocks.invoke).toHaveBeenLastCalledWith("configure_point_scan", {
config: expect.objectContaining({ blockIntervalMs: next })
}));
});
1 change: 0 additions & 1 deletion src/scanning/preferences.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ export const defaultScanPreferences: ScanPreferences = {
direction: 'forward', passLimit: 3, pattern: 'grouped', thickness: 'standard',
point: {}, menu: {}, keyboard: {},
};
export const scanIntervals = [250, 500, 750, 1000, 1500, 2000, 3000, 4000, 5000];
export function sharedOptions(config: PointScanConfig): ScanOptions {
const settings = config.scanPreferences ?? defaultScanPreferences;
return { automatic: config.automatic, intervalMs: config.blockIntervalMs, color: config.scannerColor,
Expand Down
10 changes: 6 additions & 4 deletions src/settings/ScannerPreferences.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useLayoutEffect, useRef, useState, type ReactNode } from 'react';
import type { ScanningController, ScannerColor, PointScanConfig } from '../scanning/useScanning';
import { areaOptions, sharedOptions, defaultScanPreferences, scanIntervals, type ScanArea, type ScanOptions } from '../scanning/preferences';
import { areaOptions, sharedOptions, defaultScanPreferences, type ScanArea, type ScanOptions } from '../scanning/preferences';
import { SettingGroup, Toggle, OptionGroup } from './controls';

export function ScannerPreferences({ controller }: { controller: ScanningController }) {
Expand Down Expand Up @@ -144,9 +144,11 @@ export function ScannerPreferences({ controller }: { controller: ScanningControl
onChange={value => update('keyboardWaitAfterTyping', value === 'wait')}
note={{ summary: effective.automatic ? 'After typing a key or suggestion, wait for Select before scanning again.' : 'Used in automatic keyboard scanning. Your choice is kept while scanning manually.' }} />}
{field('intervalMs', 'Auto scan rate', locked => <>
<label className="exact-speed"><span>Auto scan rate</span><select disabled={locked || !effective.automatic} value={effective.intervalMs} onChange={event => change('intervalMs', Number(event.target.value))}>
{[...new Set([...scanIntervals, effective.intervalMs])].sort((a, b) => a - b).map(value => <option key={value} value={value}>{value / 1000} {value === 1000 ? 'second' : 'seconds'}</option>)}
</select></label>
<div className="exact-speed"><span>Auto scan rate</span><div className="scan-rate-stepper">
<button type="button" aria-label="Decrease auto scan interval by 0.1 seconds" disabled={locked || !effective.automatic || effective.intervalMs <= 100} onClick={() => change('intervalMs', Math.max(100, effective.intervalMs - 100))}>−</button>
<output aria-label="Auto scan rate" aria-live="polite">{effective.intervalMs / 1000} s</output>
<button type="button" aria-label="Increase auto scan interval by 0.1 seconds" disabled={locked || !effective.automatic || effective.intervalMs >= 10000} onClick={() => change('intervalMs', Math.min(10000, effective.intervalMs + 100))}>+</button>
</div></div>
<p className="setting-note">{effective.automatic ? 'Time each row or item stays highlighted. Lower values scan faster. Line speed is separate.' : 'Used in automatic scanning. Your rate is kept while scanning manually.'}</p>
</>)}
{field('direction', 'Initial direction', locked => <OptionGroup<ScanOptions['direction']> legend="Initial direction" disabled={locked} value={effective.direction} options={[{ value: 'forward', label: 'Forward' }, { value: 'reverse', label: 'Reverse' }]} onChange={value => change('direction', value)} />)}
Expand Down
4 changes: 4 additions & 0 deletions src/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -437,3 +437,7 @@ button:disabled { cursor: default; opacity: 0.55; }
.scanner-area-badge { grid-column: 2; grid-row: 2 / 4; align-self: center; margin: 0; }
.scanner-area-card > span:not(.scanner-area-badge) { grid-column: 1; }
}

.scan-rate-stepper { display: flex; align-items: center; gap: 8px; }
.scan-rate-stepper button { min-width: 44px; min-height: 44px; font-size: 22px; }
.scan-rate-stepper output { min-width: 72px; text-align: center; font-variant-numeric: tabular-nums; }
Loading