use crate::core::{Point, Rect};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HitConfidence {
Low,
Medium,
High,
}
#[derive(Debug, Clone)]
pub struct KeyHit {
pub character: char,
pub confidence: f32,
pub level: HitConfidence,
pub position: Point,
}
#[derive(Debug, Clone)]
pub struct HolographicKey {
pub character: char,
pub bounds: Rect,
pub shift_character: Option<char>,
}
impl HolographicKey {
pub const fn new(character: char, bounds: Rect) -> Self {
Self { character, bounds, shift_character: None }
}
pub fn with_shift(mut self, ch: char) -> Self {
self.shift_character = Some(ch);
self
}
}
#[derive(Debug)]
pub struct KeyboardLayout {
pub keys: Vec<HolographicKey>,
pub surface_width: f32,
pub surface_height: f32,
pub shift_active: bool,
}
impl Default for KeyboardLayout {
fn default() -> Self {
Self::qwerty()
}
}
impl KeyboardLayout {
pub fn qwerty() -> Self {
let surface_width = 300.0;
let surface_height = 120.0;
let cols = 10;
let rows = 3;
let key_w = surface_width / cols as f32;
let key_h = surface_height / rows as f32;
let row1_chars: Vec<char> = "QWERTYUIOP".chars().collect();
let row2_chars: Vec<char> = "ASDFGHJKL".chars().collect();
let row3_chars: Vec<char> = "ZXCVBNM".chars().collect();
let mut keys = Vec::with_capacity(28);
for (i, &ch) in row1_chars.iter().enumerate() {
let x = i as f32 * key_w;
let y = 0.0;
let lower = ch.to_ascii_lowercase();
keys.push(
HolographicKey::new(
lower,
Rect::new(x as i32, y as i32, key_w as u32, key_h as u32),
)
.with_shift(ch),
);
}
let stagger = key_w * 0.25;
for (i, &ch) in row2_chars.iter().enumerate() {
let x = stagger + i as f32 * key_w;
let y = key_h;
let lower = ch.to_ascii_lowercase();
keys.push(
HolographicKey::new(
lower,
Rect::new(x as i32, y as i32, key_w as u32, key_h as u32),
)
.with_shift(ch),
);
}
for (i, &ch) in row3_chars.iter().enumerate() {
let x = key_w * 1.5 + i as f32 * key_w;
let y = key_h * 2.0;
let lower = ch.to_ascii_lowercase();
keys.push(
HolographicKey::new(
lower,
Rect::new(x as i32, y as i32, key_w as u32, key_h as u32),
)
.with_shift(ch),
);
}
let space_y = key_h * 2.0;
let space_x = key_w * 2.0;
let space_w = key_w * 6.0;
keys.push(HolographicKey::new(
' ',
Rect::new(space_x as i32, space_y as i32, space_w as u32, key_h as u32),
));
let bs_x = key_w * 9.0;
keys.push(HolographicKey::new(
'\u{0008}', Rect::new(bs_x as i32, 0, key_w as u32, key_h as u32),
));
Self { keys, surface_width, surface_height, shift_active: false }
}
pub fn key_at(&self, pos: Point) -> Option<&HolographicKey> {
self.keys.iter().find(|k| k.bounds.contains_point(pos))
}
pub fn character_for(&self, key: &HolographicKey) -> char {
if self.shift_active {
key.shift_character.unwrap_or(key.character)
} else {
key.character
}
}
pub fn toggle_shift(&mut self) {
self.shift_active = !self.shift_active;
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum FingerState {
Idle,
Approaching,
Pressed,
Releasing,
}
#[derive(Debug)]
pub struct HolographicKeyboardDetector {
state: FingerState,
layout: KeyboardLayout,
press_threshold: f32,
release_threshold: f32,
last_position: Option<Point>,
press_position: Option<Point>,
pressed_key: Option<char>,
drag_cancel_distance: f32,
}
impl Default for HolographicKeyboardDetector {
fn default() -> Self {
Self {
state: FingerState::Idle,
layout: KeyboardLayout::default(),
press_threshold: 5.0, release_threshold: 15.0, last_position: None,
press_position: None,
pressed_key: None,
drag_cancel_distance: 8.0, }
}
}
impl HolographicKeyboardDetector {
pub fn new() -> Self {
Self::default()
}
pub fn with_layout(layout: KeyboardLayout) -> Self {
Self { layout, ..Default::default() }
}
pub fn with_press_threshold(mut self, threshold: f32) -> Self {
self.press_threshold = threshold;
self
}
pub fn with_release_threshold(mut self, threshold: f32) -> Self {
self.release_threshold = threshold;
self
}
pub fn process_depth(&mut self, pos: Point, depth: f32) -> Option<KeyHit> {
self.last_position = Some(pos);
match self.state {
FingerState::Idle => {
if depth < self.press_threshold {
self.state = FingerState::Approaching;
self.press_position = Some(pos);
}
None
}
FingerState::Approaching => {
if depth < self.press_threshold * 0.5 {
self.state = FingerState::Pressed;
self.pressed_key =
self.layout.key_at(pos).map(|k| self.layout.character_for(k));
None
} else if depth > self.release_threshold {
self.reset();
None
} else {
None
}
}
FingerState::Pressed => {
if let Some(press_pos) = self.press_position {
let dx = (pos.x - press_pos.x) as f32;
let dy = (pos.y - press_pos.y) as f32;
let dist = (dx * dx + dy * dy).sqrt();
if dist > self.drag_cancel_distance {
self.reset();
return None;
}
}
if depth > self.release_threshold {
self.state = FingerState::Releasing;
let hit = self.pressed_key.map(|ch| {
let confidence = Self::compute_confidence(depth, self.release_threshold);
KeyHit {
character: ch,
confidence,
level: if confidence > 0.8 {
HitConfidence::High
} else if confidence > 0.5 {
HitConfidence::Medium
} else {
HitConfidence::Low
},
position: pos,
}
});
self.reset();
return hit;
}
None
}
FingerState::Releasing => {
self.reset();
None
}
}
}
pub fn process_timeout(&mut self) {
self.reset();
}
pub fn reset(&mut self) {
self.state = FingerState::Idle;
self.last_position = None;
self.press_position = None;
self.pressed_key = None;
}
pub fn layout_mut(&mut self) -> &mut KeyboardLayout {
&mut self.layout
}
fn compute_confidence(depth: f32, release_threshold: f32) -> f32 {
if depth <= 0.0 {
return 0.0;
}
let ratio = depth / release_threshold;
(ratio.min(2.0) / 2.0).clamp(0.0, 1.0)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_layout_has_expected_key_count() {
let layout = KeyboardLayout::qwerty();
assert_eq!(layout.keys.len(), 28);
}
#[test]
fn key_at_returns_correct_character() {
let layout = KeyboardLayout::qwerty();
let key = layout.key_at(Point::new(5, 5)).unwrap();
assert_eq!(key.character, 'q');
}
#[test]
fn detector_starts_idle() {
let detector = HolographicKeyboardDetector::new();
assert_eq!(detector.state, FingerState::Idle);
}
#[test]
fn approaching_depth_transitions_to_approaching() {
let mut detector = HolographicKeyboardDetector::new();
let result = detector.process_depth(Point::new(5, 5), 4.0);
assert_eq!(detector.state, FingerState::Approaching);
assert!(result.is_none());
}
#[test]
fn press_and_release_produces_key_hit() {
let mut detector = HolographicKeyboardDetector::new();
let _ = detector.process_depth(Point::new(5, 5), 4.0);
let _ = detector.process_depth(Point::new(5, 5), 2.0);
assert_eq!(detector.state, FingerState::Pressed);
assert_eq!(detector.pressed_key, Some('q'));
let hit = detector.process_depth(Point::new(5, 5), 20.0);
assert!(hit.is_some());
let hit = hit.unwrap();
assert_eq!(hit.character, 'q');
assert_eq!(detector.state, FingerState::Idle);
}
#[test]
fn drag_during_press_cancels_hit() {
let mut detector = HolographicKeyboardDetector::new();
let _ = detector.process_depth(Point::new(5, 5), 4.0);
let _ = detector.process_depth(Point::new(5, 5), 2.0);
assert_eq!(detector.pressed_key, Some('q'));
let result = detector.process_depth(Point::new(50, 50), 2.0);
assert!(result.is_none());
assert_eq!(detector.state, FingerState::Idle);
}
#[test]
fn timeout_resets_detector() {
let mut detector = HolographicKeyboardDetector::new();
let _ = detector.process_depth(Point::new(5, 5), 4.0);
assert_eq!(detector.state, FingerState::Approaching);
let _ = detector.process_depth(Point::new(5, 5), 1.0);
assert_eq!(detector.state, FingerState::Pressed);
detector.process_timeout();
assert_eq!(detector.state, FingerState::Idle);
}
#[test]
fn shift_toggle_works() {
let mut layout = KeyboardLayout::qwerty();
assert!(!layout.shift_active);
layout.toggle_shift();
assert!(layout.shift_active);
let q_char = layout.character_for(layout.key_at(Point::new(5, 5)).unwrap());
assert_eq!(q_char, 'Q');
layout.toggle_shift();
assert!(!layout.shift_active);
let q_char = layout.character_for(layout.key_at(Point::new(5, 5)).unwrap());
assert_eq!(q_char, 'q');
}
#[test]
fn confidence_scales_with_depth() {
let c1 = HolographicKeyboardDetector::compute_confidence(15.0, 15.0);
let c2 = HolographicKeyboardDetector::compute_confidence(30.0, 15.0);
assert!((c1 - 0.5).abs() < 0.01);
assert!((c2 - 1.0).abs() < 0.01);
}
}