use sim_kernel::{Expr, Symbol};
use sim_value::build;
use crate::gesture::Hit;
use crate::model::{Origin, intent};
pub trait WristInputCapabilities {
fn has_input(&self, name: &str) -> bool;
}
impl WristInputCapabilities for [Symbol] {
fn has_input(&self, name: &str) -> bool {
self.iter()
.any(|symbol| symbol.namespace.is_none() && symbol.name.as_ref() == name)
}
}
impl WristInputCapabilities for Vec<Symbol> {
fn has_input(&self, name: &str) -> bool {
self.as_slice().has_input(name)
}
}
impl<T: WristInputCapabilities + ?Sized> WristInputCapabilities for &T {
fn has_input(&self, name: &str) -> bool {
(**self).has_input(name)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct WristInputTiming {
pub debounce_ms: u64,
pub tap_sequence_ms: u64,
pub long_press_ms: u64,
pub raise_stable_ms: u64,
}
impl Default for WristInputTiming {
fn default() -> Self {
Self {
debounce_ms: 80,
tap_sequence_ms: 450,
long_press_ms: 650,
raise_stable_ms: 180,
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub enum WristRawInput {
Button {
id: Symbol,
held_ms: u64,
at_ms: u64,
},
Crown {
delta: i32,
press: bool,
target: Expr,
at_ms: u64,
},
Tap {
count: u8,
target: Expr,
span_ms: u64,
at_ms: u64,
},
Raise {
target: Expr,
stable_ms: u64,
at_ms: u64,
},
Touch {
hit: Hit,
at_ms: u64,
},
}
#[derive(Clone, Debug, Default)]
pub struct WristIntentReducer {
timing: WristInputTiming,
last_accepted_ms: Option<u64>,
}
impl WristIntentReducer {
pub fn new() -> Self {
Self::default()
}
pub fn with_timing(timing: WristInputTiming) -> Self {
Self {
timing,
last_accepted_ms: None,
}
}
pub fn reduce<C: WristInputCapabilities + ?Sized>(
&mut self,
origin: Origin,
raw: WristRawInput,
profile: &C,
) -> Option<Expr> {
let at_ms = raw.at_ms();
let intent = match raw {
WristRawInput::Button { id, held_ms, .. } if profile.has_input("button") => {
if held_ms >= self.timing.long_press_ms {
Some(intent("dismiss", origin, vec![]))
} else {
Some(invoke(
origin,
Expr::Symbol(id.clone()),
"button-press",
vec![Expr::Symbol(id)],
))
}
}
WristRawInput::Crown {
delta,
press,
target,
..
} if has_any_input(profile, &["crown", "rotary"]) => {
if press {
Some(invoke(origin, target, "crown-press", vec![]))
} else if delta != 0 {
Some(move_selection(origin, target, delta))
} else {
None
}
}
WristRawInput::Tap {
count,
target,
span_ms,
..
} if profile.has_input("tap") && span_ms <= self.timing.tap_sequence_ms => {
tap_pattern(origin, count, target)
}
WristRawInput::Raise {
target, stable_ms, ..
} if profile.has_input("raise") && stable_ms >= self.timing.raise_stable_ms => {
Some(select_one(origin, target))
}
WristRawInput::Touch { hit, .. } if profile.has_input("touch") => hit
.target
.clone()
.map(|target| move_selection(origin, target, 1)),
_ => None,
}?;
self.accepts(at_ms).then_some(intent)
}
fn accepts(&mut self, at_ms: u64) -> bool {
if let Some(last) = self.last_accepted_ms
&& at_ms.saturating_sub(last) < self.timing.debounce_ms
{
return false;
}
self.last_accepted_ms = Some(at_ms);
true
}
}
impl WristRawInput {
fn at_ms(&self) -> u64 {
match self {
WristRawInput::Button { at_ms, .. }
| WristRawInput::Crown { at_ms, .. }
| WristRawInput::Tap { at_ms, .. }
| WristRawInput::Raise { at_ms, .. }
| WristRawInput::Touch { at_ms, .. } => *at_ms,
}
}
}
fn has_any_input<C: WristInputCapabilities + ?Sized>(profile: &C, names: &[&str]) -> bool {
names.iter().any(|name| profile.has_input(name))
}
fn tap_pattern(origin: Origin, count: u8, target: Expr) -> Option<Expr> {
match count {
1 => Some(select_one(origin, target)),
2 => Some(invoke(origin, target, "double-tap", vec![])),
3 => Some(intent(
"edit",
origin,
vec![("target", target), ("path", Expr::List(Vec::new()))],
)),
_ => None,
}
}
fn select_one(origin: Origin, target: Expr) -> Expr {
intent(
"select",
origin,
vec![("targets", Expr::List(vec![target]))],
)
}
fn invoke(origin: Origin, target: Expr, op: &str, args: Vec<Expr>) -> Expr {
intent(
"invoke",
origin,
vec![
("target", target),
("op", Expr::Symbol(Symbol::qualified("watch/input", op))),
("args", Expr::List(args)),
],
)
}
fn move_selection(origin: Origin, target: Expr, delta: i32) -> Expr {
intent(
"move",
origin,
vec![
("node", target),
(
"at",
build::map(vec![("selection-delta", build::float(f64::from(delta)))]),
),
],
)
}