From 3eefb28848549e77aebc813068ccb47ab3d29a03 Mon Sep 17 00:00:00 2001 From: oneshinyboi Date: Sat, 15 Aug 2026 22:57:18 -0500 Subject: [PATCH] separate swipe code from main keyboard code --- wayvr/src/assets/gui/keyboard.xml | 2 +- wayvr/src/overlays/keyboard/builder.rs | 182 +----------------- wayvr/src/overlays/keyboard/mod.rs | 148 ++++++-------- wayvr/src/overlays/keyboard/prediction_bar.rs | 174 +++++++++++++++++ wayvr/src/overlays/keyboard/swipe_type.rs | 76 +++++++- 5 files changed, 323 insertions(+), 259 deletions(-) create mode 100644 wayvr/src/overlays/keyboard/prediction_bar.rs diff --git a/wayvr/src/assets/gui/keyboard.xml b/wayvr/src/assets/gui/keyboard.xml index fb79613e..ec2305c0 100644 --- a/wayvr/src/assets/gui/keyboard.xml +++ b/wayvr/src/assets/gui/keyboard.xml @@ -15,7 +15,7 @@ diff --git a/wayvr/src/overlays/keyboard/builder.rs b/wayvr/src/overlays/keyboard/builder.rs index 58de9332..19a07674 100644 --- a/wayvr/src/overlays/keyboard/builder.rs +++ b/wayvr/src/overlays/keyboard/builder.rs @@ -1,4 +1,4 @@ -use std::{collections::HashMap, rc::Rc, time::Duration}; +use std::{rc::Rc, time::Duration}; use crate::{ app_misc, @@ -11,9 +11,8 @@ use crate::{ subsystem::hid::XkbKeymap, windowing::backend::OverlayEventData, }; -use anyhow::{bail, Context}; +use anyhow::Context; use glam::{FloatExt, Mat4, Vec2, vec2, vec3}; -use slotmap::Key; use smallvec::{SmallVec, smallvec}; use wgui::{ animation::{Animation, AnimationEasing}, @@ -31,9 +30,6 @@ use wgui::{ sprite::WidgetSprite, }, }; -use wgui::event::StyleSetRequest; -use wgui::layout::LayoutTask; -use wgui::taffy::Display; #[cfg(feature = "swipe-to-type")] use wlx_common::data_dir; use super::{KeyButtonData, KeyState, KeyboardState, handle_press, handle_release, layout::{self, KeyCapType}, handle_mouse_motion}; @@ -42,152 +38,13 @@ use super::init_swipe_type_manager; const PIXELS_PER_UNIT: f32 = 60.; -fn new_doc_params(panel: &mut GuiPanel) -> ParseDocumentParams<'static> { +pub(super) fn new_doc_params(panel: &mut GuiPanel) -> ParseDocumentParams<'static> { ParseDocumentParams { globals: panel.layout.state.globals.clone(), path: AssetPath::FileOrBuiltIn("gui/keyboard.xml"), extra: panel.doc_extra.take().unwrap_or_default(), } } - -pub(super) fn update_swipe_prediction_bar( - panel: &mut GuiPanel, - app: &mut AppState -) -> anyhow::Result { - let mut elements_changed = false; - - let anim_mult = app.wgui_theme.animation_mult; - - if let Some(recv) = panel.state.swipe_candidate_receiver.as_mut() - && let Ok(candidates) = recv.try_recv() { - - let predictions_root = panel.parser_state - .get_widget_id("swipe_predictions_root") - .unwrap_or_default(); - - if predictions_root.is_null() { - return Ok(elements_changed) - } - let doc_params = new_doc_params(panel); - - panel.layout.remove_children(predictions_root); - - let Some(new_suggestions) = candidates else { - return Ok(elements_changed) - }; - - let mut iter = new_suggestions.iter(); - let Some(best_prediction) = iter.next() else { - bail!("not enough swipe predictions"); - }; - if let Some(manager) = panel.state.swipe_typing_manager.as_mut() { - manager.select_word(best_prediction, app, panel.state.modifiers); - } - for (i, prediction) in iter.enumerate() { - let mut params = HashMap::new(); - let id: Rc = Rc::from(format!("Prediction-{i}")); - params.insert("id".into(), id.clone()); - params.insert("text".into(), prediction.clone().into()); - - panel.parser_state.instantiate_template( - &doc_params, - "KeyPrediction", - &mut panel.layout, - predictions_root, - TemplateParams::from_hashmap(params) - )?; - - if let Ok(widget_id) = panel.parser_state.get_widget_id(&id) { - let key_state = { - let rect = panel - .layout - .state - .widgets - .get_as::(widget_id) - .unwrap(); // want panic - - Rc::new(KeyState { - // fake button state just so we get key state for anims - button_state: KeyButtonData::Modifier { - modifier: 0, - sticky: core::cell::Cell::new(false), - }, - color: rect.params.color, - color2: rect.params.color2, - base_border_color: rect.params.border_color, - cur_border_color: rect.params.border_color.into(), - border: rect.params.border, - drawn_state: false.into(), - labels: Default::default(), - sprites: Default::default(), - }) - }; - panel.add_event_listener( - widget_id, - EventListenerKind::MousePress, - Box::new({ - let k = key_state.clone(); - let pred = prediction.clone(); - move |common, data, app, state| { - if let Some(manager) = state.swipe_typing_manager.as_mut() { - manager.select_alternate_prediction(&pred, app, state.modifiers); - on_press_anim(k.clone(), common, data) - } - Ok(EventResult::Pass) - } - }) - ); - panel.add_event_listener( - widget_id, - EventListenerKind::MouseEnter, - Box::new({ - let k = key_state.clone(); - move |common, data, _app, _state| { - on_enter_anim( - k.clone(), - common, - data, - anim_mult, - 0.0, - ); - Ok(EventResult::Pass) - } - }) - ); - panel.add_event_listener( - widget_id, - EventListenerKind::MouseLeave, - Box::new({ - let k = key_state.clone(); - move |common, data, _app, _state | { - on_leave_anim( - k.clone(), - common, - data, - anim_mult, - 0.0, - ); - Ok(EventResult::Pass) - } - }) - ); - panel.add_event_listener( - widget_id, - EventListenerKind::MouseRelease, - Box::new({ - let k = key_state.clone(); - move |common, data, _app, _state| { - on_release_anim(k.clone(), common, data); - Ok(EventResult::Pass) - } - }) - ); - } - } - elements_changed = true; - } - Ok(elements_changed) -} fn bool_to_rc_str(val: bool) -> Rc { if val { "1" } else { "0" }.into() } @@ -489,34 +346,13 @@ pub(super) fn create_keyboard_panel( panel.state.swipe_typing_manager = None; panel.state.swipe_candidate_receiver = None; - let predictions_root = panel.parser_state - .get_widget_id("swipe_predictions_root") - .unwrap_or_default(); - - if !predictions_root.is_null() { - panel.layout.remove_children(predictions_root); - - panel.layout.tasks.push(LayoutTask::SetWidgetStyle( - predictions_root, - StyleSetRequest::Display(Display::None), - )); - - } + super::prediction_bar::set_visible(panel, false); } if app.session.config.keyboard_swipe_to_type_enabled && panel.state.swipe_typing_manager.is_none() { #[cfg(feature = "swipe-to-type")] init_swipe_type_manager(&mut panel.state, data_dir::get_path("swipe_type").join("en.tar")); - let predictions_root = panel.parser_state - .get_widget_id("swipe_predictions_root") - .unwrap_or_default(); - - if !predictions_root.is_null() { - panel.layout.tasks.push(LayoutTask::SetWidgetStyle( - predictions_root, - StyleSetRequest::Display(Display::Flex), - )); - } + super::prediction_bar::set_visible(panel, true); } } @@ -612,7 +448,7 @@ fn set_anim_color( rect.params.border = key_state.border.lerp(key_state.border * 1.5, pos); } -fn on_enter_anim( +pub(super) fn on_enter_anim( key_state: Rc, common: &mut event::CallbackDataCommon, data: &event::CallbackData, @@ -660,7 +496,7 @@ fn on_enter_anim( )); } -fn on_leave_anim( +pub(super) fn on_leave_anim( key_state: Rc, common: &mut event::CallbackDataCommon, data: &event::CallbackData, @@ -710,7 +546,7 @@ fn on_leave_anim( )); } -fn on_press_anim( +pub(super) fn on_press_anim( key_state: Rc, common: &mut event::CallbackDataCommon, data: &mut event::CallbackData, @@ -725,7 +561,7 @@ fn on_press_anim( key_state.drawn_state.set(true); } -fn on_release_anim( +pub(super) fn on_release_anim( key_state: Rc, common: &mut event::CallbackDataCommon, data: &mut event::CallbackData, diff --git a/wayvr/src/overlays/keyboard/mod.rs b/wayvr/src/overlays/keyboard/mod.rs index c549006b..0885f45d 100644 --- a/wayvr/src/overlays/keyboard/mod.rs +++ b/wayvr/src/overlays/keyboard/mod.rs @@ -32,7 +32,7 @@ use crate::{ use anyhow::Context; use glam::{Affine3A, Quat, Vec3, vec3, Vec2}; use regex::Regex; -use slotmap::{SlotMap, new_key_type, Key}; +use slotmap::{SlotMap, new_key_type}; use smallvec::SmallVec; use wgui::{ color::WguiColor, @@ -40,10 +40,7 @@ use wgui::{ i18n::Translation, layout::WidgetID, }; -use wgui::event::{DeviceBitmask, StyleSetRequest}; -use wgui::layout::{LayoutTask}; -use wgui::parser::Fetchable; -use wgui::taffy::Display; +use wgui::event::DeviceBitmask; use wlx_common::windowing::{OverlayWindowState, Positioning}; use wlx_common::{ config::AltModifier, @@ -51,12 +48,12 @@ use wlx_common::{ }; #[cfg(feature = "swipe-to-type")] use wlx_common::data_dir; -use crate::overlays::keyboard::builder::update_swipe_prediction_bar; use crate::overlays::keyboard::layout::KeyCapType; use crate::overlays::keyboard::swipe_type::SwipeTypingManager; pub mod builder; mod layout; +mod prediction_bar; #[cfg(feature = "swipe-to-type")] mod swipe_type; @@ -69,6 +66,7 @@ mod swipe_type { pub struct SwipeTypingManager; + #[allow(dead_code)] // stub used only to satisfy compilation when the feature is disabled impl SwipeTypingManager { pub fn new(_model_folder: std::path::PathBuf) -> anyhow::Result<(Self, Receiver>>)> { Ok((Self, std::sync::mpsc::sync_channel(1).1)) @@ -79,6 +77,9 @@ mod swipe_type { pub fn did_swipe_leave_first_key(&self) -> bool { false } pub fn is_current_swipe_empty(&self) -> bool { true } pub fn current_swipe_mouse_button_index(&self) -> Option { None } + pub fn handle_key_press(&mut self, _key_cap_type: &super::layout::KeyCapType, _within_key_pos: &Option, _key_label: &[String], _device: DeviceBitmask, _index: MouseButtonIndex) -> super::KeyPressOutcome { super::KeyPressOutcome::Dispatch } + pub fn handle_key_motion(&mut self, _key_cap_type: &super::layout::KeyCapType, _within_key_pos: &Option, _key_label: &[String], _device: DeviceBitmask) {} + pub fn handle_key_release(&mut self, _key_cap_type: &super::layout::KeyCapType, _alt_modifier: crate::subsystem::hid::KeyModifier) -> super::KeyReleaseOutcome { super::KeyReleaseOutcome::Normal } pub fn select_word(&mut self, _word: &String, _app: &mut crate::state::AppState, _original_keyboard_mods: crate::subsystem::hid::KeyModifier) {} pub fn select_alternate_prediction(&mut self, _word: &String, _app: &mut crate::state::AppState, _original_keyboard_mods: crate::subsystem::hid::KeyModifier) {} } @@ -86,6 +87,26 @@ mod swipe_type { pub const KEYBOARD_NAME: &str = "kbd"; const AUTO_RELEASE_MODS: [KeyModifier; 5] = [SHIFT, CTRL, ALT, SUPER, ALTGR]; + +/// Outcome of handling a key press (or motion) against the swipe-to-type engine. +#[allow(dead_code)] // `Consumed` only constructed with `swipe-to-type` feature enabled +pub enum KeyPressOutcome { + /// The event was fed into swipe tracking. Do not dispatch a key. + Consumed, + /// Not a swipe target. Dispatch as a normal key event. + Dispatch, +} + +/// Outcome of handling a key release against the swipe-to-type engine. +#[allow(dead_code)] // `Predict`/`TapKey` only constructed with `swipe-to-type` feature enabled +pub enum KeyReleaseOutcome { + /// A swipe left the first key; prediction was already triggered. Nothing to dispatch. + Predict, + /// Pressed and released on the same key. Dispatch a tap with the given modifier. + TapKey { modifier: KeyModifier }, + /// Not a swipe target. Dispatch a normal key-up. + Normal, +} const SYSTEM_LAYOUT_ALIASES: [&str; 5] = ["mozc", "pinyin", "hangul", "sayura", "unikey"]; pub fn create_keyboard(app: &mut AppState) -> anyhow::Result { @@ -164,18 +185,6 @@ pub(self) fn init_swipe_type_manager(state: &mut KeyboardState, model_path: std: } }; } -#[cfg(feature = "swipe-to-type")] -pub(self) fn hide_swipe_type_manager(panel: &mut GuiPanel) { - let predictions_root = panel.parser_state - .get_widget_id("swipe_predictions_root") - .unwrap_or_default(); - if !predictions_root.is_null() { - panel.layout.tasks.push(LayoutTask::SetWidgetStyle( - predictions_root, - StyleSetRequest::Display(Display::None), - )); - } -} const fn alt_modifier_to_key(m: AltModifier) -> KeyModifier { match m { AltModifier::Shift => SHIFT, @@ -220,8 +229,7 @@ impl KeyboardBackend { create_keyboard_panel(app, keymap, state, &self.wlx_layout)?; if !app.session.config.keyboard_swipe_to_type_enabled { - #[cfg(feature = "swipe-to-type")] - hide_swipe_type_manager(&mut panel); + prediction_bar::set_visible(&mut panel, false); } let id = self.layout_panels.insert(panel); @@ -280,11 +288,12 @@ impl KeyboardBackend { .state = state_from; if !app.session.config.keyboard_swipe_to_type_enabled { - #[cfg(feature = "swipe-to-type")] - hide_swipe_type_manager(self.layout_panels - .get_mut(self.active_layout) - .unwrap() - ) + prediction_bar::set_visible( + self.layout_panels + .get_mut(self.active_layout) + .unwrap(), + false, + ); } } @@ -320,7 +329,7 @@ impl KeyboardBackend { } fn update_swipe_prediction_bar(&mut self, app: &mut AppState) -> anyhow::Result<()> { - if update_swipe_prediction_bar(self.panel(), app)? { + if prediction_bar::update(self.panel(), app)? { self.panel().process_custom_elems(app); } Ok(()) @@ -578,27 +587,11 @@ fn handle_mouse_motion( within_key_pos: &Option, device: DeviceBitmask ) { - if let Some(swipe_manager) = keyboard.swipe_typing_manager.as_mut() - && matches!(*key_cap_type, KeyCapType::Letter | KeyCapType::LetterAltGr) + if let KeyButtonData::Key { .. } = &key.button_state + && let Some(swipe_manager) = keyboard.swipe_typing_manager.as_mut() { - if !swipe_manager.is_current_swipe_empty() { - match &key.button_state { - KeyButtonData::Key { vk, pressed } => { - if let Some(pos) = within_key_pos { - // check because mouse motion can trigger despite hover being false - if pos.x >= 0.0 && pos.x <= 1.0 && pos.y >= 0.0 && pos.y <= 1.0 { - - if let Some(label) = key_label.first() { - swipe_manager.add_swipe(pos, label.chars().next().unwrap_or_default(), device, None); - } - } - } - } - _ => {} - } - } + swipe_manager.handle_key_motion(key_cap_type, within_key_pos, key_label, device); } - } fn handle_press( app: &mut AppState, @@ -612,16 +605,14 @@ fn handle_press( ) { match &key.button_state { KeyButtonData::Key { vk, pressed } => { - if let Some(swipe_manager) = keyboard.swipe_typing_manager.as_mut() - && matches!(*key_cap_type, KeyCapType::Letter | KeyCapType::LetterAltGr) - { - if let Some(pos) = within_key_pos { - if let Some(label) = key_label.first() { - swipe_manager.add_swipe(pos, label.chars().next().unwrap_or_default(), device, Some(button.index)); - } + let outcome = match keyboard.swipe_typing_manager.as_mut() { + Some(swipe_manager) => { + swipe_manager.handle_key_press(key_cap_type, within_key_pos, key_label, device, button.index) } - } - else { + None => KeyPressOutcome::Dispatch, + }; + + if matches!(outcome, KeyPressOutcome::Dispatch) { keyboard.modifiers |= match button.index { MouseButtonIndex::Right => SHIFT, MouseButtonIndex::Middle => keyboard.alt_modifier, @@ -683,25 +674,17 @@ fn handle_press( fn handle_release(app: &mut AppState, key: &KeyState, k_cap_type: &KeyCapType, keyboard: &mut KeyboardState) -> bool { match &key.button_state { KeyButtonData::Key { vk, pressed } => { - if let Some(swipe_manager) = keyboard.swipe_typing_manager.as_mut() - && matches!(*k_cap_type, KeyCapType::Letter | KeyCapType::LetterAltGr) - { - if swipe_manager.did_swipe_leave_first_key() { - match swipe_manager.predict() { - Ok(()) => {}, - Err(e) => { - log::error!("{}", e) - } - } + let outcome = match keyboard.swipe_typing_manager.as_mut() { + Some(swipe_manager) => { + swipe_manager.handle_key_release(k_cap_type, keyboard.alt_modifier) } - else { // pointer must have been released on the same key it was pressed on - keyboard.modifiers |= match swipe_manager.current_swipe_mouse_button_index() { - Some(MouseButtonIndex::Right) => SHIFT, - Some(MouseButtonIndex::Middle) => keyboard.alt_modifier, - _ => 0, - }; - swipe_manager.reset(); // drop swipe tracking that was started on press + None => KeyReleaseOutcome::Normal, + }; + match outcome { + KeyReleaseOutcome::Predict => {} + KeyReleaseOutcome::TapKey { modifier } => { + keyboard.modifiers |= modifier; app.hid_provider .set_modifiers_routed(app.wvr_server.as_mut(), keyboard.modifiers); app.hid_provider @@ -717,22 +700,19 @@ fn handle_release(app: &mut AppState, key: &KeyState, k_cap_type: &KeyCapType, k } play_key_click(app); } - } - else { - if let Some(swipe_manager) = keyboard.swipe_typing_manager.as_mut() { - swipe_manager.reset(); - } - pressed.set(false); + KeyReleaseOutcome::Normal => { + pressed.set(false); - for m in &AUTO_RELEASE_MODS { - if keyboard.modifiers & *m != 0 { - keyboard.modifiers &= !*m; + for m in &AUTO_RELEASE_MODS { + if keyboard.modifiers & *m != 0 { + keyboard.modifiers &= !*m; + } } + app.hid_provider + .send_key_routed(app.wvr_server.as_mut(), *vk, false); + app.hid_provider + .set_modifiers_routed(app.wvr_server.as_mut(), keyboard.modifiers); } - app.hid_provider - .send_key_routed(app.wvr_server.as_mut(), *vk, false); - app.hid_provider - .set_modifiers_routed(app.wvr_server.as_mut(), keyboard.modifiers); } true } diff --git a/wayvr/src/overlays/keyboard/prediction_bar.rs b/wayvr/src/overlays/keyboard/prediction_bar.rs new file mode 100644 index 00000000..29f0d21a --- /dev/null +++ b/wayvr/src/overlays/keyboard/prediction_bar.rs @@ -0,0 +1,174 @@ +use std::{collections::HashMap, rc::Rc}; + +use crate::{ + gui::panel::GuiPanel, + state::AppState, +}; +use anyhow::bail; +use slotmap::Key; +use super::{ + KeyButtonData, KeyState, KeyboardState, + builder::{ + new_doc_params, on_enter_anim, on_leave_anim, on_press_anim, on_release_anim, + }, +}; +use wgui::{ + event::EventListenerKind, + layout::LayoutTask, + parser::{Fetchable, TemplateParams}, + taffy::Display, + widget::{EventResult, rectangle::WidgetRectangle}, +}; +use wgui::event::StyleSetRequest; + +/// Root widget that hosts the swipe-to-type prediction candidates. +pub(super) const ROOT: &str = "swipe_predictions_root"; + +/// Render the prediction candidate bar from the latest received predictions. +pub(super) fn update( + panel: &mut GuiPanel, + app: &mut AppState, +) -> anyhow::Result { + let mut elements_changed = false; + + let anim_mult = app.wgui_theme.animation_mult; + + if let Some(recv) = panel.state.swipe_candidate_receiver.as_mut() + && let Ok(candidates) = recv.try_recv() + { + let predictions_root = panel + .parser_state + .get_widget_id(ROOT) + .unwrap_or_default(); + + if predictions_root.is_null() { + return Ok(elements_changed); + } + let doc_params = new_doc_params(panel); + + panel.layout.remove_children(predictions_root); + + let Some(new_suggestions) = candidates else { + return Ok(elements_changed); + }; + + let mut iter = new_suggestions.iter(); + let Some(best_prediction) = iter.next() else { + bail!("not enough swipe predictions"); + }; + if let Some(manager) = panel.state.swipe_typing_manager.as_mut() { + manager.select_word(best_prediction, app, panel.state.modifiers); + } + for (i, prediction) in iter.enumerate() { + let mut params = HashMap::new(); + let id: Rc = Rc::from(format!("Prediction-{i}")); + params.insert("id".into(), id.clone()); + params.insert("text".into(), prediction.clone().into()); + + panel.parser_state.instantiate_template( + &doc_params, + "KeyPrediction", + &mut panel.layout, + predictions_root, + TemplateParams::from_hashmap(params), + )?; + + if let Ok(widget_id) = panel.parser_state.get_widget_id(&id) { + let key_state = { + let rect = panel + .layout + .state + .widgets + .get_as::(widget_id) + .unwrap(); // want panic + + Rc::new(KeyState { + // fake button state just so we get key state for anims + button_state: KeyButtonData::Modifier { + modifier: 0, + sticky: core::cell::Cell::new(false), + }, + color: rect.params.color, + color2: rect.params.color2, + base_border_color: rect.params.border_color, + cur_border_color: rect.params.border_color.into(), + border: rect.params.border, + drawn_state: false.into(), + labels: Default::default(), + sprites: Default::default(), + }) + }; + panel.add_event_listener( + widget_id, + EventListenerKind::MousePress, + Box::new({ + let k = key_state.clone(); + let pred = prediction.clone(); + move |common, data, app, state| { + if let Some(manager) = state.swipe_typing_manager.as_mut() { + manager.select_alternate_prediction(&pred, app, state.modifiers); + on_press_anim(k.clone(), common, data); + } + Ok(EventResult::Pass) + } + }), + ); + panel.add_event_listener( + widget_id, + EventListenerKind::MouseEnter, + Box::new({ + let k = key_state.clone(); + move |common, data, _app, _state| { + on_enter_anim(k.clone(), common, data, anim_mult, 0.0); + Ok(EventResult::Pass) + } + }), + ); + panel.add_event_listener( + widget_id, + EventListenerKind::MouseLeave, + Box::new({ + let k = key_state.clone(); + move |common, data, _app, _state| { + on_leave_anim(k.clone(), common, data, anim_mult, 0.0); + Ok(EventResult::Pass) + } + }), + ); + panel.add_event_listener( + widget_id, + EventListenerKind::MouseRelease, + Box::new({ + let k = key_state.clone(); + move |common, data, _app, _state| { + on_release_anim(k.clone(), common, data); + Ok(EventResult::Pass) + } + }), + ); + } + } + elements_changed = true; + } + Ok(elements_changed) +} + +/// Show or hide the prediction candidate bar. +pub(super) fn set_visible(panel: &mut GuiPanel, visible: bool) { + let predictions_root = panel + .parser_state + .get_widget_id(ROOT) + .unwrap_or_default(); + if predictions_root.is_null() { + return; + } + + if !visible { + panel.layout.remove_children(predictions_root); + } + + panel.layout.tasks.push(LayoutTask::SetWidgetStyle( + predictions_root, + StyleSetRequest::Display(if visible { Display::Flex } else { Display::None }), + )); +} \ No newline at end of file diff --git a/wayvr/src/overlays/keyboard/swipe_type.rs b/wayvr/src/overlays/keyboard/swipe_type.rs index c7be4769..54991f43 100644 --- a/wayvr/src/overlays/keyboard/swipe_type.rs +++ b/wayvr/src/overlays/keyboard/swipe_type.rs @@ -1,5 +1,5 @@ use crate::state::AppState; -use crate::subsystem::hid::{KeyModifier, VirtualKey, CTRL}; +use crate::subsystem::hid::{KeyModifier, VirtualKey, CTRL, SHIFT}; use anyhow::{bail}; use glam::Vec2; use std::mem; @@ -15,6 +15,7 @@ use wgui::log::LogErr; use crate::subsystem::clipboard; use crate::subsystem::clipboard::ClipboardProvider; use crate::subsystem::input::InputFocus; +use super::layout::KeyCapType; const PREDICTION_SUGGESTION_COUNT: usize = 5; @@ -217,6 +218,79 @@ impl SwipeTypingManager { self.current_swipe_mouse_button_index } + /// Handle a key press. Letter keys feed the swipe engine and are consumed; + /// anything else is dispatched as a normal key. + pub fn handle_key_press( + &mut self, + key_cap_type: &KeyCapType, + within_key_pos: &Option, + key_label: &[String], + device: DeviceBitmask, + index: MouseButtonIndex, + ) -> super::KeyPressOutcome { + if !matches!(key_cap_type, KeyCapType::Letter | KeyCapType::LetterAltGr) { + return super::KeyPressOutcome::Dispatch; + } + + if let Some(pos) = within_key_pos + && let Some(label) = key_label.first() + { + self.add_swipe(pos, label.chars().next().unwrap_or_default(), device, Some(index)); + } + + super::KeyPressOutcome::Consumed + } + + /// Handle pointer motion over a key while swiping. + pub fn handle_key_motion( + &mut self, + key_cap_type: &KeyCapType, + within_key_pos: &Option, + key_label: &[String], + device: DeviceBitmask, + ) { + if !self.is_current_swipe_empty() + && matches!(key_cap_type, KeyCapType::Letter | KeyCapType::LetterAltGr) + && let Some(pos) = within_key_pos + && pos.x >= 0.0 + && pos.x <= 1.0 + && pos.y >= 0.0 + && pos.y <= 1.0 + && let Some(label) = key_label.first() + { + self.add_swipe(pos, label.chars().next().unwrap_or_default(), device, None); + } + } + + /// Handle a key release. A swipe that left the first key triggers a + /// prediction; a tap on the same key dispatches the key; anything else is + /// a normal key-up. + pub fn handle_key_release( + &mut self, + key_cap_type: &KeyCapType, + alt_modifier: KeyModifier, + ) -> super::KeyReleaseOutcome { + if !matches!(key_cap_type, KeyCapType::Letter | KeyCapType::LetterAltGr) { + self.reset(); + return super::KeyReleaseOutcome::Normal; + } + + if self.did_swipe_leave_first_key() { + if let Err(e) = self.predict() { + log::error!("{}", e); + } + return super::KeyReleaseOutcome::Predict; + } + + let modifier = match self.current_swipe_mouse_button_index() { + Some(MouseButtonIndex::Right) => SHIFT, + Some(MouseButtonIndex::Middle) => alt_modifier, + _ => 0, + }; + self.reset(); + super::KeyReleaseOutcome::TapKey { modifier } + } + pub fn add_swipe(&mut self, within_key_pos_normalized: &Vec2, key_label: char, device: DeviceBitmask, index: Option) { if let Some(pos) = self.keyboard_gird.key_positions.get(&key_label.to_ascii_lowercase()) { if let Some(current_device) = self.current_swipe_device && current_device != device {