1use sim_kernel::{Expr, Symbol};
10use sim_value::build;
11
12use crate::gesture::Hit;
13use crate::model::{Origin, intent};
14
15pub trait GlassesInputCapabilities {
20 fn has_input(&self, name: &str) -> bool;
22}
23
24impl GlassesInputCapabilities for [Symbol] {
25 fn has_input(&self, name: &str) -> bool {
26 self.iter()
27 .any(|symbol| symbol.namespace.is_none() && symbol.name.as_ref() == name)
28 }
29}
30
31impl GlassesInputCapabilities for Vec<Symbol> {
32 fn has_input(&self, name: &str) -> bool {
33 self.as_slice().has_input(name)
34 }
35}
36
37impl<T: GlassesInputCapabilities + ?Sized> GlassesInputCapabilities for &T {
38 fn has_input(&self, name: &str) -> bool {
39 (**self).has_input(name)
40 }
41}
42
43#[derive(Clone, Copy, Debug, PartialEq, Eq)]
45pub struct GlassesInputTiming {
46 pub debounce_ms: u64,
48 pub gaze_stable_ms: u64,
50 pub gaze_dwell_ms: u64,
52 pub head_stable_ms: u64,
54 pub hand_stable_ms: u64,
56 pub tap_sequence_ms: u64,
58 pub long_press_ms: u64,
60}
61
62impl Default for GlassesInputTiming {
63 fn default() -> Self {
64 Self {
65 debounce_ms: 80,
66 gaze_stable_ms: 120,
67 gaze_dwell_ms: 550,
68 head_stable_ms: 140,
69 hand_stable_ms: 80,
70 tap_sequence_ms: 450,
71 long_press_ms: 650,
72 }
73 }
74}
75
76#[derive(Clone, Copy, Debug, PartialEq, Eq)]
78pub enum GazePhase {
79 Enter,
81 Hold,
83 Dwell,
85}
86
87#[derive(Clone, Copy, Debug, PartialEq, Eq)]
89pub enum HeadGesture {
90 Nod,
92 Shake,
94 TiltLeft,
96 TiltRight,
98 TiltUp,
100 TiltDown,
102}
103
104impl HeadGesture {
105 fn token(self) -> &'static str {
106 match self {
107 HeadGesture::Nod => "head-nod",
108 HeadGesture::Shake => "head-shake",
109 HeadGesture::TiltLeft => "head-tilt-left",
110 HeadGesture::TiltRight => "head-tilt-right",
111 HeadGesture::TiltUp => "head-tilt-up",
112 HeadGesture::TiltDown => "head-tilt-down",
113 }
114 }
115}
116
117#[derive(Clone, Copy, Debug, PartialEq, Eq)]
119pub enum ControllerAction {
120 Press,
122 Move {
124 dx: i32,
126 dy: i32,
128 },
129 Edit,
131}
132
133#[derive(Clone, Debug, PartialEq)]
135pub enum GlassesRawInput {
136 Gaze {
138 phase: GazePhase,
140 hit: Hit,
142 stable_ms: u64,
144 vio_stable: bool,
146 at_ms: u64,
148 },
149 Head {
151 kind: HeadGesture,
153 target: Expr,
155 stable_ms: u64,
157 vio_stable: bool,
159 at_ms: u64,
161 },
162 HandRay {
164 hit: Hit,
166 stable_ms: u64,
168 vio_stable: bool,
170 at_ms: u64,
172 },
173 Pinch {
175 hit: Hit,
177 stable_ms: u64,
179 vio_stable: bool,
181 at_ms: u64,
183 },
184 Tap {
186 count: u8,
188 target: Expr,
190 span_ms: u64,
192 held_ms: u64,
194 at_ms: u64,
196 },
197 Button {
199 id: Symbol,
201 target: Option<Expr>,
203 held_ms: u64,
205 at_ms: u64,
207 },
208 Controller {
210 id: Symbol,
212 action: ControllerAction,
214 target: Expr,
216 at_ms: u64,
218 },
219}
220
221#[derive(Clone, Debug, Default)]
223pub struct GlassesIntentReducer {
224 timing: GlassesInputTiming,
225 last_accepted_ms: Option<u64>,
226}
227
228impl GlassesIntentReducer {
229 pub fn new() -> Self {
231 Self::default()
232 }
233
234 pub fn with_timing(timing: GlassesInputTiming) -> Self {
236 Self {
237 timing,
238 last_accepted_ms: None,
239 }
240 }
241
242 pub fn reduce<C: GlassesInputCapabilities + ?Sized>(
245 &mut self,
246 origin: Origin,
247 raw: GlassesRawInput,
248 profile: &C,
249 ) -> Option<Expr> {
250 let at_ms = raw.at_ms();
251 let intent = match raw {
252 GlassesRawInput::Gaze {
253 phase,
254 hit,
255 stable_ms,
256 vio_stable,
257 ..
258 } if profile.has_input("gaze") && vio_stable => {
259 self.gaze_intent(origin, phase, hit, stable_ms)
260 }
261 GlassesRawInput::Head {
262 kind,
263 target,
264 stable_ms,
265 vio_stable,
266 ..
267 } if profile.has_input("head")
268 && vio_stable
269 && stable_ms >= self.timing.head_stable_ms =>
270 {
271 head_intent(origin, kind, target)
272 }
273 GlassesRawInput::HandRay {
274 hit,
275 stable_ms,
276 vio_stable,
277 ..
278 } if has_any_input(profile, &["hand", "hand-ray"])
279 && vio_stable
280 && stable_ms >= self.timing.hand_stable_ms =>
281 {
282 hit.target
283 .clone()
284 .map(|target| move_focus(origin, target, "hand-ray"))
285 }
286 GlassesRawInput::Pinch {
287 hit,
288 stable_ms,
289 vio_stable,
290 ..
291 } if has_any_input(profile, &["hand", "pinch"])
292 && vio_stable
293 && stable_ms >= self.timing.hand_stable_ms =>
294 {
295 hit.target
296 .clone()
297 .map(|target| invoke(origin, target, "pinch", vec![]))
298 }
299 GlassesRawInput::Tap {
300 count,
301 target,
302 span_ms,
303 held_ms,
304 ..
305 } if profile.has_input("tap") => {
306 tap_intent(origin, count, target, span_ms, held_ms, self.timing)
307 }
308 GlassesRawInput::Button {
309 id,
310 target,
311 held_ms,
312 ..
313 } if profile.has_input("button") => {
314 if held_ms >= self.timing.long_press_ms {
315 Some(intent("dismiss", origin, vec![]))
316 } else {
317 let target = target.unwrap_or_else(|| Expr::Symbol(id.clone()));
318 Some(invoke(
319 origin,
320 target,
321 "button-press",
322 vec![Expr::Symbol(id)],
323 ))
324 }
325 }
326 GlassesRawInput::Controller {
327 id, action, target, ..
328 } if profile.has_input("controller") => controller_intent(origin, id, action, target),
329 _ => None,
330 }?;
331 self.accepts(at_ms).then_some(intent)
332 }
333
334 fn gaze_intent(
335 &self,
336 origin: Origin,
337 phase: GazePhase,
338 hit: Hit,
339 stable_ms: u64,
340 ) -> Option<Expr> {
341 let target = hit.target?;
342 match phase {
343 GazePhase::Dwell if stable_ms >= self.timing.gaze_dwell_ms => {
344 Some(select_one(origin, target))
345 }
346 GazePhase::Enter | GazePhase::Hold if stable_ms >= self.timing.gaze_stable_ms => {
347 Some(move_focus(origin, target, phase.token()))
348 }
349 _ => None,
350 }
351 }
352
353 fn accepts(&mut self, at_ms: u64) -> bool {
354 if let Some(last) = self.last_accepted_ms
355 && at_ms.saturating_sub(last) < self.timing.debounce_ms
356 {
357 return false;
358 }
359 self.last_accepted_ms = Some(at_ms);
360 true
361 }
362}
363
364impl GazePhase {
365 fn token(self) -> &'static str {
366 match self {
367 GazePhase::Enter => "gaze-enter",
368 GazePhase::Hold => "gaze-hold",
369 GazePhase::Dwell => "gaze-dwell",
370 }
371 }
372}
373
374impl GlassesRawInput {
375 fn at_ms(&self) -> u64 {
376 match self {
377 GlassesRawInput::Gaze { at_ms, .. }
378 | GlassesRawInput::Head { at_ms, .. }
379 | GlassesRawInput::HandRay { at_ms, .. }
380 | GlassesRawInput::Pinch { at_ms, .. }
381 | GlassesRawInput::Tap { at_ms, .. }
382 | GlassesRawInput::Button { at_ms, .. }
383 | GlassesRawInput::Controller { at_ms, .. } => *at_ms,
384 }
385 }
386}
387
388fn has_any_input<C: GlassesInputCapabilities + ?Sized>(profile: &C, names: &[&str]) -> bool {
389 names.iter().any(|name| profile.has_input(name))
390}
391
392fn head_intent(origin: Origin, kind: HeadGesture, target: Expr) -> Option<Expr> {
393 match kind {
394 HeadGesture::Nod => Some(invoke(origin, target, kind.token(), vec![])),
395 HeadGesture::Shake => Some(intent("dismiss", origin, vec![])),
396 HeadGesture::TiltLeft
397 | HeadGesture::TiltRight
398 | HeadGesture::TiltUp
399 | HeadGesture::TiltDown => Some(move_focus(origin, target, kind.token())),
400 }
401}
402
403fn tap_intent(
404 origin: Origin,
405 count: u8,
406 target: Expr,
407 span_ms: u64,
408 held_ms: u64,
409 timing: GlassesInputTiming,
410) -> Option<Expr> {
411 if count == 1 && held_ms >= timing.long_press_ms {
412 return Some(edit(origin, target));
413 }
414 if span_ms > timing.tap_sequence_ms {
415 return None;
416 }
417 match count {
418 1 => Some(select_one(origin, target)),
419 2 => Some(invoke(origin, target, "double-tap", vec![])),
420 _ => None,
421 }
422}
423
424fn controller_intent(
425 origin: Origin,
426 id: Symbol,
427 action: ControllerAction,
428 target: Expr,
429) -> Option<Expr> {
430 match action {
431 ControllerAction::Press => Some(invoke(
432 origin,
433 target,
434 "controller-press",
435 vec![Expr::Symbol(id)],
436 )),
437 ControllerAction::Move { dx, dy } if dx != 0 || dy != 0 => {
438 Some(move_by(origin, target, "controller-move", dx, dy))
439 }
440 ControllerAction::Edit => Some(edit(origin, target)),
441 _ => None,
442 }
443}
444
445fn select_one(origin: Origin, target: Expr) -> Expr {
446 intent(
447 "select",
448 origin,
449 vec![("targets", Expr::List(vec![target]))],
450 )
451}
452
453fn invoke(origin: Origin, target: Expr, op: &str, args: Vec<Expr>) -> Expr {
454 intent(
455 "invoke",
456 origin,
457 vec![
458 ("target", target),
459 ("op", Expr::Symbol(Symbol::qualified("glasses/input", op))),
460 ("args", Expr::List(args)),
461 ],
462 )
463}
464
465fn edit(origin: Origin, target: Expr) -> Expr {
466 intent(
467 "edit",
468 origin,
469 vec![("target", target), ("path", Expr::List(Vec::new()))],
470 )
471}
472
473fn move_focus(origin: Origin, target: Expr, source: &str) -> Expr {
474 intent(
475 "move",
476 origin,
477 vec![
478 ("node", target),
479 ("at", build::map(vec![("source", build::sym(source))])),
480 ],
481 )
482}
483
484fn move_by(origin: Origin, target: Expr, source: &str, dx: i32, dy: i32) -> Expr {
485 intent(
486 "move",
487 origin,
488 vec![
489 ("node", target),
490 (
491 "at",
492 build::map(vec![
493 ("source", build::sym(source)),
494 ("dx", build::float(f64::from(dx))),
495 ("dy", build::float(f64::from(dy))),
496 ]),
497 ),
498 ],
499 )
500}