Cues for the app's own buttons (auditory scanning, auditory touch) are said in the language their words are shown in: by an English voice where the message has no translation yet. The board area and the message carry the person's language (lang), whatever language the app's words are in. Co-Authored-By: Claude Opus 5.5 <[email protected]>
562 lines
26 KiB
Rust
562 lines
26 KiB
Rust
//! The person's switches: which key, gamepad button or tap is which switch
|
|
//! (access.rs `AccessPrefs::switches`, taught in Settings → Switches by
|
|
//! pressing the switch), and what a press does.
|
|
//!
|
|
//! Each switch is read on its own: a press counts unless that switch is down
|
|
//! already, or was let go less than the release time ago (a bouncing
|
|
//! contact, a hand that shakes as it lets go); and when the person set a
|
|
//! hold time, only once it has been held that long. Then it does its job
|
|
//! (access.rs `switch_job`): in switch scanning, select, move the highlight
|
|
//! on, move it back, or back out of a row; while pointing or looking, switch
|
|
//! 1 or 2 selects what is pointed at, when the person asked for that.
|
|
//!
|
|
//! In inverse scanning switch 1 held lets the highlight move (the timer
|
|
//! moves it while it is down), and letting go selects.
|
|
//!
|
|
//! **The way out** (SPEC §5.7 item 8): a person is locked out when their
|
|
//! switch does nothing any more (it was taught away, or the access method
|
|
//! was changed to one it has no part in). Holding such a key or gamepad
|
|
//! button — one that does nothing for them now — for [`ESCAPE_MS`] brings
|
|
//! switch scanning back with it as switch 1, keeping their other switches,
|
|
//! and what moves the highlight where it still works; it is provisional,
|
|
//! kept by pressing that switch again once it has come up (a bounce as it
|
|
//! comes up does not keep it). For someone who looks, points or touches,
|
|
//! only their switches count: a hand resting on a laptop's keyboard is
|
|
//! never the way out. A switch that works for them is never
|
|
//! counted, however long it is held (resting on it, or holding it in
|
|
//! inverse scanning, is using it); nor are keys pressed with ⌘, Ctrl or Alt,
|
|
//! modifiers, or the keys a keyboard holds down to move and scroll.
|
|
|
|
use crate::access::SwitchInput;
|
|
|
|
/// The input a key event is: its code (where it is on the keyboard), named
|
|
/// by the key it types. A key event without a code (some on-screen
|
|
/// keyboards send none) goes by the key.
|
|
pub fn key_input(code: &str, key: &str) -> SwitchInput {
|
|
let code = match (code, key) {
|
|
("", " ") => "Space".to_string(), // i18n: not UI (a key's code)
|
|
("", k) => k.to_string(),
|
|
(c, _) => c.to_string(),
|
|
};
|
|
let name = match key {
|
|
" " => "Space".to_string(), // i18n: not UI (a stored key name; access.rs describe() names it)
|
|
k if k.chars().count() == 1 => k.to_uppercase(),
|
|
k => k.to_string(),
|
|
};
|
|
SwitchInput::Key { code, name }
|
|
}
|
|
|
|
#[cfg(feature = "hydrate")]
|
|
pub use browser::*;
|
|
|
|
#[cfg(feature = "hydrate")]
|
|
mod browser {
|
|
use aac_core::{AccessMethod, ScanAdvance};
|
|
use leptos::prelude::*;
|
|
|
|
use crate::access::{SwitchInput, SwitchJob, switch_job};
|
|
use crate::state::AacAppState;
|
|
|
|
/// How long "press your switch" waits before it gives up.
|
|
const LEARN_MS: u32 = 15_000;
|
|
|
|
/// How long holding a key or button that does nothing for the person
|
|
/// brings switch scanning back (the way out).
|
|
pub const ESCAPE_MS: u32 = 8_000;
|
|
|
|
thread_local! {
|
|
/// The teaching was started by switch scanning or dwell: what is
|
|
/// taught is provisional (provisional.rs).
|
|
static LEARN_BY_ACCESS: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
|
|
/// The keys and buttons down now that do nothing for the person, each
|
|
/// with its press number, for the way out.
|
|
static HELD: std::cell::RefCell<std::collections::HashMap<String, u64>> = std::cell::RefCell::new(std::collections::HashMap::new());
|
|
/// The key or button whose hold brought the way out, until it comes
|
|
/// up: its key-up presses nothing.
|
|
static WAY_OUT_BY: std::cell::RefCell<Option<String>> = const { std::cell::RefCell::new(None) };
|
|
/// When it came up (a press that keeps the way out comes after, by
|
|
/// at least the release time).
|
|
static WAY_OUT_UP_AT: std::cell::Cell<f64> = const { std::cell::Cell::new(f64::NEG_INFINITY) };
|
|
static HOLDS: std::cell::Cell<u64> = const { std::cell::Cell::new(0) };
|
|
}
|
|
|
|
thread_local! {
|
|
/// Someone helping pressed Tab in an open panel: the keys there are
|
|
/// the keyboard's until the panel closes or a switch that is not a
|
|
/// key is pressed.
|
|
static KEYBOARD_HELPER: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
|
|
}
|
|
|
|
/// Tab pressed (and not a switch) with a panel open: someone is working
|
|
/// the panel with a keyboard.
|
|
pub fn keyboard_helper_started() {
|
|
KEYBOARD_HELPER.with(|k| k.set(true));
|
|
}
|
|
|
|
/// Whether a key on a control of the open panel is the keyboard's, not a
|
|
/// switch press: someone helping moved into the panel with Tab.
|
|
pub fn keyboard_helper_has_keys() -> bool {
|
|
if !crate::actions::panel_open() {
|
|
KEYBOARD_HELPER.with(|k| k.set(false));
|
|
}
|
|
KEYBOARD_HELPER.with(std::cell::Cell::get)
|
|
}
|
|
|
|
/// Whether a tap is one of the person's switches now: a switch taught,
|
|
/// switch scanning on, and no board being edited.
|
|
pub fn tap_is_switch_now(state: &AacAppState) -> bool {
|
|
matches!(state.profile.access_method.get_untracked(), AccessMethod::SwitchScan { .. })
|
|
&& !state.board.edit_mode.get_untracked()
|
|
&& state.profile.access.with_untracked(|a| a.tap_is_switch())
|
|
}
|
|
|
|
/// Which switch `input` is, for the person.
|
|
pub fn switch_of(state: &AacAppState, input: &SwitchInput) -> Option<usize> {
|
|
state.profile.access.with_untracked(|a| a.switch_of(input))
|
|
}
|
|
|
|
/// Start teaching switch `n`: the next key, gamepad button or tap (the
|
|
/// learning panel's own tap target) becomes it.
|
|
pub fn start_learning(state: &AacAppState, n: usize) {
|
|
LEARN_BY_ACCESS.with(|l| l.set(crate::provisional::by_access()));
|
|
state.interaction.switch_learning.set(Some(n));
|
|
let learning = state.interaction.switch_learning;
|
|
gloo_timers::callback::Timeout::new(LEARN_MS, move || {
|
|
if learning.try_get_untracked() == Some(Some(n)) {
|
|
learning.set(None);
|
|
}
|
|
})
|
|
.forget();
|
|
}
|
|
|
|
/// Stop teaching, with nothing learned.
|
|
pub fn stop_learning(state: &AacAppState) {
|
|
state.interaction.switch_learning.set(None);
|
|
}
|
|
|
|
/// Whether "press your switch" is on screen: the teaching is only taken
|
|
/// while it is (a press after Settings closed is the person's, not a
|
|
/// switch to learn).
|
|
fn learning_shown() -> bool {
|
|
web_sys::window()
|
|
.and_then(|w| w.document())
|
|
.and_then(|d| d.query_selector(".switch-learning").ok().flatten())
|
|
.is_some()
|
|
}
|
|
|
|
/// While a switch is being taught (and "press your switch" is on
|
|
/// screen), `input` becomes it, and true is returned: the press is not
|
|
/// acted on. Escape stops the teaching. A change that would leave the
|
|
/// person nothing to select with is refused, and said so; one taught by
|
|
/// scanning or dwell is provisional.
|
|
pub fn learn(state: &AacAppState, input: &SwitchInput) -> bool {
|
|
let Some(n) = state.interaction.switch_learning.get_untracked() else { return false };
|
|
if !learning_shown() {
|
|
stop_learning(state);
|
|
return false;
|
|
}
|
|
if matches!(input, SwitchInput::Key { code, .. } if code == "Escape") { // i18n: not UI (a key code)
|
|
stop_learning(state);
|
|
return true;
|
|
}
|
|
let method = state.profile.access_method.get_untracked();
|
|
let mut next = state.profile.access.get_untracked();
|
|
let message = match next.teach(n, input.clone(), method) {
|
|
Ok(()) => {
|
|
crate::provisional::guard(state, LEARN_BY_ACCESS.with(|l| l.get()));
|
|
state.profile.access.set(next);
|
|
crate::i18n::tr_args("switches-learned", &crate::ui_args!(number = n + 1, input = input.describe()))
|
|
}
|
|
Err(refused) => refused.explain(),
|
|
};
|
|
state.interaction.switch_learning.set(None);
|
|
crate::state::announce(state.chrome.announcement, &message.text);
|
|
crate::state::show_toast(state.chrome.toasts, message, crate::state::ToastLevel::Success);
|
|
true
|
|
}
|
|
|
|
/// Take switch `n`'s inputs away, unless that would leave nothing to
|
|
/// select with (said so). Made by scanning or dwell, provisional.
|
|
pub fn clear(state: &AacAppState, n: usize) {
|
|
let method = state.profile.access_method.get_untracked();
|
|
let mut next = state.profile.access.get_untracked();
|
|
match next.clear(n, method) {
|
|
Ok(()) => {
|
|
crate::provisional::guard(state, false);
|
|
state.profile.access.set(next);
|
|
}
|
|
Err(refused) => {
|
|
crate::state::announce(state.chrome.announcement, &refused.explain().text);
|
|
crate::state::show_toast(state.chrome.toasts, refused.explain(), crate::state::ToastLevel::Warning);
|
|
}
|
|
}
|
|
}
|
|
|
|
// ─── The way out ─────────────────────────────────────────────────────
|
|
|
|
/// Keys never counted for the way out: modifiers, and keys a keyboard
|
|
/// holds down to move, scroll or delete. A switch interface sends none
|
|
/// of them.
|
|
pub fn never_way_out(code: &str) -> bool {
|
|
// i18n-off: key codes
|
|
matches!(
|
|
code,
|
|
"ShiftLeft" | "ShiftRight" | "ControlLeft" | "ControlRight" | "AltLeft" | "AltRight" | "MetaLeft"
|
|
| "MetaRight" | "OSLeft" | "OSRight" | "CapsLock" | "Fn" | "FnLock" | "NumLock" | "ContextMenu"
|
|
| "Tab" | "Escape" | "Backspace" | "Delete" | "ArrowUp" | "ArrowDown" | "ArrowLeft" | "ArrowRight"
|
|
| "PageUp" | "PageDown" | "Home" | "End"
|
|
) || code.starts_with("Audio")
|
|
|| code.starts_with("Media")
|
|
|| code.starts_with("Browser")
|
|
// i18n-on
|
|
}
|
|
|
|
/// Whether holding `input` counts for the way out (access.rs
|
|
/// `way_out_counts`): it does nothing for the person now, and is one of
|
|
/// their switches (any input, while they scan).
|
|
fn idle_input(state: &AacAppState, input: &SwitchInput) -> bool {
|
|
let method = state.profile.access_method.get_untracked();
|
|
state.profile.access.with_untracked(|a| a.way_out_counts(input, method))
|
|
}
|
|
|
|
/// A key or button (`id`: "key:KeyA", "pad:4") went down: if it does
|
|
/// nothing for the person now and is still down after [`ESCAPE_MS`] —
|
|
/// and still does nothing for them — the way out.
|
|
pub fn hold_started(state: &AacAppState, input: &SwitchInput, id: String) {
|
|
let press = HOLDS.with(|h| {
|
|
h.set(h.get() + 1);
|
|
h.get()
|
|
});
|
|
if HELD.with(|h| h.borrow().contains_key(&id)) || !idle_input(state, input) || learning_shown() {
|
|
return;
|
|
}
|
|
HELD.with(|h| h.borrow_mut().insert(id.clone(), press));
|
|
let (state, input) = (state.clone(), input.clone());
|
|
gloo_timers::callback::Timeout::new(ESCAPE_MS, move || {
|
|
let still = HELD.with(|h| h.borrow().get(&id) == Some(&press));
|
|
if still {
|
|
HELD.with(|h| h.borrow_mut().remove(&id));
|
|
if idle_input(&state, &input) {
|
|
WAY_OUT_BY.with(|w| *w.borrow_mut() = Some(id));
|
|
way_out(&state, &input);
|
|
}
|
|
}
|
|
})
|
|
.forget();
|
|
}
|
|
|
|
/// It came up. True when its hold brought the way out: letting it go
|
|
/// then presses nothing (the browser would press the button that has
|
|
/// the focus).
|
|
pub fn hold_ended(id: &str) -> bool {
|
|
HELD.with(|h| h.borrow_mut().remove(id));
|
|
let was = WAY_OUT_BY.with(|w| {
|
|
let mut w = w.borrow_mut();
|
|
if w.as_deref() == Some(id) { w.take().is_some() } else { false }
|
|
});
|
|
if was {
|
|
WAY_OUT_UP_AT.with(|u| u.set(crate::clock::now_ms()));
|
|
}
|
|
was
|
|
}
|
|
|
|
/// How long ago the switch held for the way out came up, if it has
|
|
/// (after a reload: long ago). None while it is still down.
|
|
pub fn way_out_released_for() -> Option<f64> {
|
|
if WAY_OUT_BY.with(|w| w.borrow().is_some()) {
|
|
return None;
|
|
}
|
|
Some(crate::clock::now_ms() - WAY_OUT_UP_AT.with(std::cell::Cell::get))
|
|
}
|
|
|
|
/// No key is held any more: a modifier came up (the keys pressed with
|
|
/// it may never send theirs, as on a Mac keyboard), or the page lost the
|
|
/// focus or was hidden.
|
|
pub fn keys_ended() {
|
|
HELD.with(|h| h.borrow_mut().retain(|id, _| !id.starts_with("key:")));
|
|
}
|
|
|
|
/// Nothing is held any more (the page lost the focus).
|
|
pub fn holds_ended() {
|
|
HELD.with(|h| h.borrow_mut().clear());
|
|
}
|
|
|
|
/// Switch scanning back, with `held` as switch 1 (access.rs `way_out`),
|
|
/// and the scanning times from before a change still waiting to be kept
|
|
/// — or, with none, times that lock nobody out: no slower scan made
|
|
/// faster, no hold time, the usual release time. Provisional, kept by
|
|
/// pressing `held` again once it has come up; and said, aloud where a
|
|
/// voice can, and on screen.
|
|
pub fn way_out(state: &AacAppState, held: &SwitchInput) {
|
|
stop_learning(state);
|
|
let method = state.profile.access_method.get_untracked();
|
|
let (prefs, next) = state.profile.access.with_untracked(|a| a.way_out(held, method));
|
|
let waiting = crate::provisional::waiting_timing();
|
|
crate::provisional::guard_way_out(state, held);
|
|
state.interaction.scan_holding.set(false);
|
|
let timing = waiting.unwrap_or_else(|| {
|
|
let now = crate::components::shell::timing_now(state);
|
|
let usual = aac_core::ScanConfig::default();
|
|
crate::persistence::TimingPrefs {
|
|
scan_rate_ms: now.scan_rate_ms.max(usual.scan_rate_ms),
|
|
scan_auto: false,
|
|
switch_hold_ms: 0,
|
|
switch_release_ms: usual.release_time_ms,
|
|
..now
|
|
}
|
|
});
|
|
crate::components::shell::apply_timing(state, timing);
|
|
state.profile.access.set(prefs);
|
|
state.profile.access_method.set(next);
|
|
crate::components::keyboard::way_out(state);
|
|
crate::provisional::made(state);
|
|
let message = crate::i18n::tr_args("switches-way-out", &crate::ui_args!(input = held.describe()));
|
|
crate::state::announce(state.chrome.announcement, &message.text);
|
|
let _ = crate::cues::say_app(state, &message, || {});
|
|
}
|
|
|
|
/// Whether switch `n` does anything with the access method in use: in
|
|
/// switch scanning, whenever something is on screen to scan; pointing
|
|
/// or looking, switches 1 and 2 when they select what is pointed at.
|
|
pub fn in_use(state: &AacAppState, n: usize) -> bool {
|
|
// Any switch answers the alphabet by ear, whatever the access method
|
|
// — after an open panel, whose scan has the switches first.
|
|
if !crate::actions::panel_open() && crate::components::alphabet_by_ear::open(state) {
|
|
return true;
|
|
}
|
|
match state.profile.access_method.get_untracked() {
|
|
AccessMethod::SwitchScan { .. } => crate::scan::running(state),
|
|
AccessMethod::EyeGaze | AccessMethod::HeadTracking | AccessMethod::Pointer => {
|
|
n < 2 && state.profile.access.with_untracked(|a| a.switch_select)
|
|
}
|
|
_ => false,
|
|
}
|
|
}
|
|
|
|
/// Switch `n` went down. It counts as a press unless it is down already
|
|
/// (a second press with no release between), or was let go less than
|
|
/// the release time ago; and when the person set a hold time
|
|
/// (acceptance), only once it has been held that long.
|
|
pub fn switch_down(state: &AacAppState, n: usize) {
|
|
crate::cues::unlock(state);
|
|
let i = &state.interaction;
|
|
if i.switch_held.with_untracked(|h| h[n]) {
|
|
return;
|
|
}
|
|
i.switch_held.update(|h| h[n] = true);
|
|
let now = crate::clock::now_ms();
|
|
let (accept, release) = i.scan_engine.with_untracked(|e| (e.config.acceptance_time_ms, e.config.release_time_ms));
|
|
if i.switch_released_at.with_untracked(|r| r[n]).is_some_and(|at| now >= at && now - at < f64::from(release)) {
|
|
return;
|
|
}
|
|
if accept == 0 {
|
|
pressed(state, n, now);
|
|
return;
|
|
}
|
|
let press = i.switch_press.with_untracked(|p| p[n]) + 1;
|
|
i.switch_press.update(|p| p[n] = press);
|
|
let state = state.clone();
|
|
gloo_timers::callback::Timeout::new(accept, move || {
|
|
let i = &state.interaction;
|
|
let still = i.switch_held.try_with_untracked(|h| h[n]) == Some(true)
|
|
&& i.switch_press.try_with_untracked(|p| p[n]) == Some(press);
|
|
if still && in_use(&state, n) {
|
|
pressed(&state, n, now);
|
|
}
|
|
})
|
|
.forget();
|
|
}
|
|
|
|
/// Switch `n` went up (a key or button let go, a finger lifted, or the
|
|
/// page losing the focus while it was down): the release time counts
|
|
/// from here, and a hold shorter than the acceptance time does not
|
|
/// count. In inverse scanning, letting go of switch 1 selects.
|
|
pub fn switch_up(state: &AacAppState, n: usize) {
|
|
let i = &state.interaction;
|
|
if i.switch_held.try_with_untracked(|h| h[n]) != Some(true) {
|
|
return;
|
|
}
|
|
i.switch_held.update(|h| h[n] = false);
|
|
let now = crate::clock::now_ms();
|
|
i.switch_released_at.update(|r| r[n] = Some(now));
|
|
i.switch_press.update(|p| p[n] += 1);
|
|
if n == 0 && i.scan_holding.get_untracked() {
|
|
i.scan_holding.set(false);
|
|
crate::scan::select(state, now);
|
|
}
|
|
}
|
|
|
|
/// Every switch let go (the page lost the focus).
|
|
pub fn all_up(state: &AacAppState) {
|
|
for n in 0..crate::access::SWITCHES {
|
|
switch_up(state, n);
|
|
}
|
|
}
|
|
|
|
thread_local! {
|
|
/// The touch that is a switch press now, by its pointer id.
|
|
static TAP_DOWN: std::cell::Cell<Option<i32>> = const { std::cell::Cell::new(None) };
|
|
/// That touch's click is not a press of what it landed on.
|
|
static TAP_CLICK: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
|
|
}
|
|
|
|
/// A tap as a switch: once the person taught one, in switch scanning a
|
|
/// touch or click on the board, the message or its buttons — or, with a
|
|
/// panel open, around it (and on it, once the person has started its
|
|
/// scan) — is that switch, down when the finger touches and up when it
|
|
/// lifts, and does not also press what it lands on. The navigation bar
|
|
/// keeps its taps, so the person helping can open Settings; so do a
|
|
/// panel's Close, and "press your switch" while a switch is being taught.
|
|
///
|
|
/// Only the click the finger itself makes (a trusted event) is held
|
|
/// back: the app's own presses of what the switch chose (a button of the
|
|
/// bar, Back, a panel's control) are synthetic clicks, and pass. Each
|
|
/// touch starts afresh, so a touch the browser cancelled (a long press,
|
|
/// a scroll) never leaves a later tap held back.
|
|
pub fn listen_taps(state: &AacAppState) {
|
|
use wasm_bindgen::JsCast;
|
|
use wasm_bindgen::prelude::Closure;
|
|
|
|
let Some(window) = web_sys::window() else { return };
|
|
let capture = web_sys::AddEventListenerOptions::new();
|
|
capture.set_capture(true);
|
|
let down = {
|
|
let state = state.clone();
|
|
Closure::<dyn Fn(web_sys::PointerEvent)>::new(move |ev: web_sys::PointerEvent| {
|
|
TAP_CLICK.with(|t| t.set(false));
|
|
let Some(n) = switch_of(&state, &SwitchInput::Tap) else { return };
|
|
// Editing a board takes its taps for the editing.
|
|
if !matches!(state.profile.access_method.get_untracked(), AccessMethod::SwitchScan { .. })
|
|
|| state.board.edit_mode.get_untracked()
|
|
{
|
|
return;
|
|
}
|
|
let Some(el) = ev.target().and_then(|t| t.dyn_into::<web_sys::Element>().ok()) else { return };
|
|
let within = |sel: &str| el.closest(sel).ok().flatten().is_some();
|
|
// A panel or menu over the board (the bar asking to keep a
|
|
// change, in the message bar's place, is not one).
|
|
let overlay = el
|
|
.owner_document()
|
|
.and_then(|d| d.query_selector(".panel-overlay, #nav-more-menu, #scan-picker").ok().flatten())
|
|
.is_some();
|
|
let zone = if overlay {
|
|
// While the panel's scan waits (just opened, or someone
|
|
// helping is working in it), only the dimmed page around
|
|
// it is the switch, and the panel's taps are theirs; once
|
|
// the person has started its scan, a tap anywhere is the
|
|
// switch. The panel's own Close always keeps its tap.
|
|
if crate::components::dialog_scan::running() {
|
|
!within(".switch-learning, .toast-container, .panel-close")
|
|
} else {
|
|
within(".panel-backdrop")
|
|
}
|
|
} else {
|
|
within(".aac-shell") && !within(".navigation-bar, .toast-container, .setup-wizard, .help-banner")
|
|
};
|
|
if !zone || !in_use(&state, n) {
|
|
return;
|
|
}
|
|
ev.prevent_default();
|
|
ev.stop_immediate_propagation();
|
|
TAP_DOWN.with(|t| t.set(Some(ev.pointer_id())));
|
|
TAP_CLICK.with(|t| t.set(true));
|
|
switch_down(&state, n);
|
|
})
|
|
};
|
|
let up = {
|
|
let state = state.clone();
|
|
Closure::<dyn Fn(web_sys::PointerEvent)>::new(move |ev: web_sys::PointerEvent| {
|
|
if TAP_DOWN.with(|t| t.get()) != Some(ev.pointer_id()) {
|
|
return;
|
|
}
|
|
TAP_DOWN.with(|t| t.set(None));
|
|
if let Some(n) = switch_of(&state, &SwitchInput::Tap) {
|
|
switch_up(&state, n);
|
|
}
|
|
})
|
|
};
|
|
let click = Closure::<dyn Fn(web_sys::MouseEvent)>::new(move |ev: web_sys::MouseEvent| {
|
|
if ev.is_trusted() && TAP_CLICK.with(|t| t.replace(false)) {
|
|
ev.prevent_default();
|
|
ev.stop_immediate_propagation();
|
|
}
|
|
});
|
|
for (name, f) in [
|
|
("pointerdown", down.as_ref()),
|
|
("pointerup", up.as_ref()),
|
|
("pointercancel", up.as_ref()),
|
|
("click", click.as_ref()),
|
|
] {
|
|
let _ = window.add_event_listener_with_callback_and_add_event_listener_options(name, f.unchecked_ref(), &capture);
|
|
}
|
|
down.forget();
|
|
up.forget();
|
|
click.forget();
|
|
}
|
|
|
|
/// A press of switch `n` that counts, made at `down_at`.
|
|
fn pressed(state: &AacAppState, n: usize, down_at: f64) {
|
|
// The alphabet by ear reads the letters aloud: the switch that
|
|
// selects is yes (an open panel first: its scan has the switches).
|
|
// Moving switches have nothing to move there.
|
|
if !crate::actions::panel_open() && crate::components::alphabet_by_ear::open(state) {
|
|
let job = match state.profile.access_method.get_untracked() {
|
|
AccessMethod::SwitchScan { .. } => switch_job(n, state.interaction.scan_engine.with_untracked(|e| e.config.advance)),
|
|
_ => SwitchJob::Select,
|
|
};
|
|
if job == SwitchJob::Select {
|
|
crate::components::alphabet_by_ear::press(state);
|
|
}
|
|
return;
|
|
}
|
|
match state.profile.access_method.get_untracked() {
|
|
AccessMethod::SwitchScan { .. } => {
|
|
let advance = state.interaction.scan_engine.with_untracked(|e| e.config.advance);
|
|
// A switch that is not a key: the person, not a keyboard helper.
|
|
if !state.profile.access.with_untracked(|a| {
|
|
a.switches[n].iter().any(|i| matches!(i, SwitchInput::Key { .. }))
|
|
}) {
|
|
KEYBOARD_HELPER.with(|k| k.set(false));
|
|
}
|
|
// One cell to choose, and nothing else in the scan: every
|
|
// switch chooses it at once (proposed SPEC §5.9).
|
|
if crate::behaviours::scan_one_target(state).is_some() {
|
|
crate::scan::select(state, down_at);
|
|
return;
|
|
}
|
|
match switch_job(n, advance) {
|
|
SwitchJob::Select => crate::scan::select(state, down_at),
|
|
SwitchJob::Move if advance == ScanAdvance::Hold => {
|
|
// The highlight moves while it is held: the first
|
|
// step at once, then the timer's.
|
|
state.interaction.scan_holding.set(true);
|
|
crate::scan::step(state, true);
|
|
}
|
|
SwitchJob::Move => crate::scan::step(state, true),
|
|
SwitchJob::Back => crate::scan::retreat(state),
|
|
SwitchJob::Out => crate::scan::back_out(state),
|
|
}
|
|
}
|
|
AccessMethod::EyeGaze | AccessMethod::HeadTracking | AccessMethod::Pointer => {
|
|
crate::pointing::switch_select(state);
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn a_key_is_its_place_on_the_keyboard_named_by_what_it_types() {
|
|
assert_eq!(key_input("Space", " "), SwitchInput::space());
|
|
assert_eq!(key_input("Digit1", "1"), SwitchInput::Key { code: "Digit1".into(), name: "1".into() });
|
|
assert_eq!(key_input("KeyA", "a"), SwitchInput::Key { code: "KeyA".into(), name: "A".into() });
|
|
assert_eq!(key_input("F7", "F7"), SwitchInput::Key { code: "F7".into(), name: "F7".into() });
|
|
// No code (some on-screen keyboards): by the key.
|
|
assert_eq!(key_input("", " "), SwitchInput::space());
|
|
assert_eq!(key_input("", "Enter"), SwitchInput::enter());
|
|
}
|
|
}
|