mirror of https://github.com/wayvr-org/wayvr.git
separate swipe code from main keyboard code
This commit is contained in:
parent
5d69d496b0
commit
3eefb28848
|
|
@ -15,7 +15,7 @@
|
|||
|
||||
<macro name="prediction_rect"
|
||||
margin="2" padding="8 16" overflow="visible" box_sizing="border_box"
|
||||
border_color="~color_accent_translucent" border="2" round="6" color="~color_accent_40" color2="~color_accent_10" gradient="vertical"
|
||||
border_color="~color_accent_translucent" border="2" round="6" color="background_variant" color2="background" gradient="vertical"
|
||||
align_items="center" justify_content="center" />
|
||||
|
||||
<theme>
|
||||
|
|
|
|||
|
|
@ -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<KeyboardState>) -> ParseDocumentParams<'static> {
|
||||
pub(super) fn new_doc_params(panel: &mut GuiPanel<KeyboardState>) -> 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<KeyboardState>,
|
||||
app: &mut AppState
|
||||
) -> anyhow::Result<bool> {
|
||||
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<str> = 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::<WidgetRectangle>(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<str> {
|
||||
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<KeyState>,
|
||||
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<KeyState>,
|
||||
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<KeyState>,
|
||||
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<KeyState>,
|
||||
common: &mut event::CallbackDataCommon,
|
||||
data: &mut event::CallbackData,
|
||||
|
|
|
|||
|
|
@ -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<Option<Vec<String>>>)> {
|
||||
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<MouseButtonIndex> { None }
|
||||
pub fn handle_key_press(&mut self, _key_cap_type: &super::layout::KeyCapType, _within_key_pos: &Option<Vec2>, _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<Vec2>, _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<OverlayWindowConfig> {
|
||||
|
|
@ -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<KeyboardState>) {
|
||||
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<Vec2>,
|
||||
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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<KeyboardState>,
|
||||
app: &mut AppState,
|
||||
) -> anyhow::Result<bool> {
|
||||
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<str> = 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::<WidgetRectangle>(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<KeyboardState>, 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 }),
|
||||
));
|
||||
}
|
||||
|
|
@ -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<Vec2>,
|
||||
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<Vec2>,
|
||||
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<MouseButtonIndex>) {
|
||||
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 {
|
||||
|
|
|
|||
Loading…
Reference in New Issue