mirror of https://github.com/wayvr-org/wayvr.git
Merge d0f134e83d into cb3aa42bd9
This commit is contained in:
commit
d874ff41fa
|
|
@ -1,6 +1,7 @@
|
|||
name: Build AppImage
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- 'main'
|
||||
|
|
|
|||
|
|
@ -0,0 +1,10 @@
|
|||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# Ignored default folder with query files
|
||||
/queries/
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
||||
# Editor-based HTTP Client requests
|
||||
/httpRequests/
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -149,6 +149,8 @@
|
|||
"KEYBOARD_MIDDLE_CLICK": "Tastatur-Mittelklick",
|
||||
"KEYBOARD_MIDDLE_CLICK_HELP": "Modifier-Taste bei der Eingabe\nmit dem lila Laser",
|
||||
"KEYBOARD_SOUND_ENABLED": "Tastatursounds",
|
||||
"KEYBOARD_SWIPE_TO_TYPE_ENABLED": "Wischen zum Tippen",
|
||||
"KEYBOARD_SWIPE_TO_TYPE_ENABLED_HELP": "Funktioniert nur für Englisch! Die Ergebnisse sind auf anderen Tastaturen als QWERTY deutlich schlechter.",
|
||||
"LANGUAGE": "Sprache",
|
||||
"LEFT_HANDED_MOUSE": "Linkshänder-Maus",
|
||||
"LEFT_HANDED_MOUSE_HELP": "Nutze dies, wenn die Maustasten vertauscht sind",
|
||||
|
|
@ -239,6 +241,14 @@
|
|||
},
|
||||
"WHISPER_MODEL": "Sprache-zu-Text-Modell",
|
||||
"WHISPER_MODEL_HELP": "Zu verwendendes Whisper-Modell.\nGrößere Modelle liefern präzisere\nErgebnisse, verbrauchen aber mehr VRAM.",
|
||||
"SWIPE_TYPE": {
|
||||
"NEED_TO_DOWNLOAD_MODEL": "Das Wisch-Tipp-Modell muss heruntergeladen werden.\nJetzt herunterladen?",
|
||||
"MODEL_REQUIRED_TO_ENABLE": "Lade das Wisch-Tipp-Modell herunter, um diese Option zu aktivieren.",
|
||||
"REMOVE_MODEL": "Wisch-Tipp-Modell entfernen",
|
||||
"REMOVE_MODEL_HELP": "Löscht das Wisch-Tipp-Modell",
|
||||
"DOWNLOAD_MODEL": "Wisch-Tipp-Modell herunterladen",
|
||||
"DOWNLOAD_MODEL_HELP": "Wischen zum Tippen funktioniert nur auf Tastaturen mit QWERTY-Layout"
|
||||
},
|
||||
"GRID_OPACITY": "Bodenraster-Deckkraft",
|
||||
"GRID_OPACITY_HELP": "Deckkraft des Bodengitters, wenn die Skymap aktiviert ist",
|
||||
"XWAYLAND_BY_DEFAULT": "Apps standardmäßig im Kompatibilitätsmodus starten",
|
||||
|
|
|
|||
|
|
@ -149,6 +149,8 @@
|
|||
"KEYBOARD_MIDDLE_CLICK": "Keyboard middle click",
|
||||
"KEYBOARD_MIDDLE_CLICK_HELP": "Modifier to use when typing\nwith purple laser",
|
||||
"KEYBOARD_SOUND_ENABLED": "Keyboard sounds",
|
||||
"KEYBOARD_SWIPE_TO_TYPE_ENABLED": "Keyboard swipe to type",
|
||||
"KEYBOARD_SWIPE_TO_TYPE_ENABLED_HELP": "Only works for English! Results will be drastically worse on non qwerty keyboards.",
|
||||
"LANGUAGE": "Language",
|
||||
"LEFT_HANDED_MOUSE": "Left-handed mouse",
|
||||
"LEFT_HANDED_MOUSE_HELP": "Use this if mouse buttons are swapped",
|
||||
|
|
@ -239,6 +241,14 @@
|
|||
},
|
||||
"WHISPER_MODEL": "Speech-to-text model",
|
||||
"WHISPER_MODEL_HELP": "Whisper model to use.\nLarger models produce more accurate\nresults, but consume more VRAM.",
|
||||
"SWIPE_TYPE": {
|
||||
"NEED_TO_DOWNLOAD_MODEL": "The swipe-to-type model needs to be downloaded.\nDownload it now?",
|
||||
"MODEL_REQUIRED_TO_ENABLE": "Download the swipe-to-type model to enable this option.",
|
||||
"REMOVE_MODEL": "Remove swipe to type model",
|
||||
"REMOVE_MODEL_HELP": "Deletes the swipe to type model",
|
||||
"DOWNLOAD_MODEL": "Download swipe to type model",
|
||||
"DOWNLOAD_MODEL_HELP": "Swipe to type only works on qwerty layout keyboards"
|
||||
},
|
||||
"GRID_OPACITY": "Floor grid opacity",
|
||||
"GRID_OPACITY_HELP": "Opacity of the floor grid when the skymap is enabled",
|
||||
"XWAYLAND_BY_DEFAULT": "Run apps in Compatibility mode by default",
|
||||
|
|
|
|||
|
|
@ -149,6 +149,8 @@
|
|||
"KEYBOARD_MIDDLE_CLICK": "Clic central del teclado",
|
||||
"KEYBOARD_MIDDLE_CLICK_HELP": "Modificador al escribir\ncon el láser púrpura",
|
||||
"KEYBOARD_SOUND_ENABLED": "Sonidos del teclado",
|
||||
"KEYBOARD_SWIPE_TO_TYPE_ENABLED": "Deslizar para escribir",
|
||||
"KEYBOARD_SWIPE_TO_TYPE_ENABLED_HELP": "¡Solo funciona en inglés! Los resultados serán mucho peores en teclados que no sean QWERTY.",
|
||||
"LANGUAGE": "Idioma",
|
||||
"LEFT_HANDED_MOUSE": "Mouse para zurdos",
|
||||
"LEFT_HANDED_MOUSE_HELP": "Úsalo si los botones del ratón están invertidos",
|
||||
|
|
@ -239,6 +241,14 @@
|
|||
},
|
||||
"WHISPER_MODEL": "Modelo de dictado",
|
||||
"WHISPER_MODEL_HELP": "Modelo de Whisper a utilizar.\nLos modelos más grandes dan resultados\nmás precisos, pero consumen más VRAM.",
|
||||
"SWIPE_TYPE": {
|
||||
"NEED_TO_DOWNLOAD_MODEL": "El modelo de deslizar para escribir debe descargarse.\n¿Descargarlo ahora?",
|
||||
"MODEL_REQUIRED_TO_ENABLE": "Descarga el modelo de deslizar para escribir para habilitar esta opción.",
|
||||
"REMOVE_MODEL": "Eliminar el modelo de deslizar para escribir",
|
||||
"REMOVE_MODEL_HELP": "Borra el modelo de deslizar para escribir",
|
||||
"DOWNLOAD_MODEL": "Descargar el modelo de deslizar para escribir",
|
||||
"DOWNLOAD_MODEL_HELP": "Deslizar para escribir solo funciona en teclados con distribución QWERTY"
|
||||
},
|
||||
"GRID_OPACITY": "Opacidad de la rejilla del suelo",
|
||||
"GRID_OPACITY_HELP": "Opacidad de la rejilla del suelo cuando el skymap está activado",
|
||||
"XWAYLAND_BY_DEFAULT": "Ejecutar apps en modo de compatibilidad por defecto",
|
||||
|
|
|
|||
|
|
@ -149,6 +149,8 @@
|
|||
"KEYBOARD_MIDDLE_CLICK": "Tasto centrale tastiera",
|
||||
"KEYBOARD_MIDDLE_CLICK_HELP": "Tasto modificatore da usare quando si scrive\ncon il laser viola",
|
||||
"KEYBOARD_SOUND_ENABLED": "Suoni tastiera",
|
||||
"KEYBOARD_SWIPE_TO_TYPE_ENABLED": "Scorri per digitare",
|
||||
"KEYBOARD_SWIPE_TO_TYPE_ENABLED_HELP": "Funziona solo con l'inglese! I risultati saranno drasticamente peggiori su tastiere non QWERTY.",
|
||||
"LANGUAGE": "Lingua",
|
||||
"LEFT_HANDED_MOUSE": "Mouse per mancini",
|
||||
"LEFT_HANDED_MOUSE_HELP": "Usalo se i tasti del mouse sono invertiti",
|
||||
|
|
@ -239,6 +241,14 @@
|
|||
},
|
||||
"WHISPER_MODEL": "Modello speech-to-text",
|
||||
"WHISPER_MODEL_HELP": "Modello Whisper da utilizzare.\nModelli più grandi offrono risultati\npiù precisi, ma consumano più VRAM.",
|
||||
"SWIPE_TYPE": {
|
||||
"NEED_TO_DOWNLOAD_MODEL": "Il modello per scorri per digitare deve essere scaricato.\nScaricarlo ora?",
|
||||
"MODEL_REQUIRED_TO_ENABLE": "Scarica il modello per scorri per digitare per abilitare questa opzione.",
|
||||
"REMOVE_MODEL": "Rimuovi il modello per scorri per digitare",
|
||||
"REMOVE_MODEL_HELP": "Elimina il modello per scorri per digitare",
|
||||
"DOWNLOAD_MODEL": "Scarica il modello per scorri per digitare",
|
||||
"DOWNLOAD_MODEL_HELP": "Scorri per digitare funziona solo su tastiere con layout QWERTY"
|
||||
},
|
||||
"GRID_OPACITY": "Opacità griglia pavimento",
|
||||
"GRID_OPACITY_HELP": "Opacità della griglia del pavimento quando lo skymap è attivo",
|
||||
"XWAYLAND_BY_DEFAULT": "Avvia le app in modalità compatibilità per impostazione predefinita",
|
||||
|
|
|
|||
|
|
@ -149,6 +149,8 @@
|
|||
"KEYBOARD_MIDDLE_CLICK": "キーボード中クリック",
|
||||
"KEYBOARD_MIDDLE_CLICK_HELP": "紫色のレーザーで\n入力する際の修飾キー",
|
||||
"KEYBOARD_SOUND_ENABLED": "キーボード入力音",
|
||||
"KEYBOARD_SWIPE_TO_TYPE_ENABLED": "スワイプ入力",
|
||||
"KEYBOARD_SWIPE_TO_TYPE_ENABLED_HELP": "英語のみで動作します!QWERTY以外のキーボードでは結果が大幅に悪くなります。",
|
||||
"LANGUAGE": "言語",
|
||||
"LEFT_HANDED_MOUSE": "左利きマウス",
|
||||
"LEFT_HANDED_MOUSE_HELP": "マウスボタンを入れ替えて使用する場合にチェック",
|
||||
|
|
@ -239,6 +241,14 @@
|
|||
},
|
||||
"WHISPER_MODEL": "音声認識モデル",
|
||||
"WHISPER_MODEL_HELP": "使用するWhisperモデルを選択。\nモデルが大きいほど精度は向上するが、\nVRAMの消費量が増える。",
|
||||
"SWIPE_TYPE": {
|
||||
"NEED_TO_DOWNLOAD_MODEL": "スワイプ入力モデルをダウンロードする必要があります。\n今すぐダウンロードしますか?",
|
||||
"MODEL_REQUIRED_TO_ENABLE": "このオプションを有効にするには、スワイプ入力モデルをダウンロードしてください。",
|
||||
"REMOVE_MODEL": "スワイプ入力モデルを削除",
|
||||
"REMOVE_MODEL_HELP": "スワイプ入力モデルを削除します",
|
||||
"DOWNLOAD_MODEL": "スワイプ入力モデルをダウンロード",
|
||||
"DOWNLOAD_MODEL_HELP": "スワイプ入力はQWERTYレイアウトのキーボードでのみ動作します"
|
||||
},
|
||||
"GRID_OPACITY": "フロアグリッドの不透明度",
|
||||
"GRID_OPACITY_HELP": "スカイマップ有効時のフロアグリッドの不透明度",
|
||||
"XWAYLAND_BY_DEFAULT": "デフォルトで互換モードでアプリを起動",
|
||||
|
|
|
|||
|
|
@ -149,6 +149,8 @@
|
|||
"KEYBOARD_MIDDLE_CLICK": "Środkowy przycisk myszy (klawiatura)",
|
||||
"KEYBOARD_MIDDLE_CLICK_HELP": "Modyfikator używany podczas\npisania fioletowym laserem",
|
||||
"KEYBOARD_SOUND_ENABLED": "Dźwięki klawiatury",
|
||||
"KEYBOARD_SWIPE_TO_TYPE_ENABLED": "Przesuwaj, aby pisać",
|
||||
"KEYBOARD_SWIPE_TO_TYPE_ENABLED_HELP": "Działa tylko dla języka angielskiego! Wyniki będą znacznie gorsze na klawiaturach innych niż QWERTY.",
|
||||
"LANGUAGE": "Język",
|
||||
"LEFT_HANDED_MOUSE": "Mysz dla leworęcznych",
|
||||
"LEFT_HANDED_MOUSE_HELP": "Użyj, jeśli przyciski myszy są zamienione",
|
||||
|
|
@ -239,6 +241,14 @@
|
|||
},
|
||||
"WHISPER_MODEL": "Model mowy na tekst",
|
||||
"WHISPER_MODEL_HELP": "Model Whisper do użycia.\nWiększe modele dają dokładniejsze\nwyniki, ale zużywają więcej pamięci VRAM.",
|
||||
"SWIPE_TYPE": {
|
||||
"NEED_TO_DOWNLOAD_MODEL": "Model do pisania przesuwaniem musi zostać pobrany.\nPobrać go teraz?",
|
||||
"MODEL_REQUIRED_TO_ENABLE": "Pobierz model do pisania przesuwaniem, aby włączyć tę opcję.",
|
||||
"REMOVE_MODEL": "Usuń model do pisania przesuwaniem",
|
||||
"REMOVE_MODEL_HELP": "Usuwa model do pisania przesuwaniem",
|
||||
"DOWNLOAD_MODEL": "Pobierz model do pisania przesuwaniem",
|
||||
"DOWNLOAD_MODEL_HELP": "Pisanie przesuwaniem działa tylko na klawiaturach QWERTY"
|
||||
},
|
||||
"GRID_OPACITY": "Przezroczystość siatki podłogi",
|
||||
"GRID_OPACITY_HELP": "Przezroczystość siatki podłogi przy włączonym Skymapie",
|
||||
"XWAYLAND_BY_DEFAULT": "Uruchamiaj aplikacje domyślnie w trybie kompatybilności",
|
||||
|
|
|
|||
|
|
@ -149,6 +149,8 @@
|
|||
"KEYBOARD_MIDDLE_CLICK": "键盘鼠标中键",
|
||||
"KEYBOARD_MIDDLE_CLICK_HELP": "使用紫色激光输入时的\n修饰键",
|
||||
"KEYBOARD_SOUND_ENABLED": "键盘声音",
|
||||
"KEYBOARD_SWIPE_TO_TYPE_ENABLED": "滑动输入",
|
||||
"KEYBOARD_SWIPE_TO_TYPE_ENABLED_HELP": "仅支持英语!在非 QWERTY 键盘上效果会明显变差。",
|
||||
"LANGUAGE": "语言",
|
||||
"LEFT_HANDED_MOUSE": "左手鼠标模式",
|
||||
"LEFT_HANDED_MOUSE_HELP": "若鼠标按键已互换,请启用此项",
|
||||
|
|
@ -239,6 +241,14 @@
|
|||
},
|
||||
"WHISPER_MODEL": "语音转文本模型",
|
||||
"WHISPER_MODEL_HELP": "所使用的 Whisper 模型。\n模型越大,结果越准确,\n但占用的显存 (VRAM) 越多。",
|
||||
"SWIPE_TYPE": {
|
||||
"NEED_TO_DOWNLOAD_MODEL": "需要下载滑动输入模型。\n现在下载吗?",
|
||||
"MODEL_REQUIRED_TO_ENABLE": "请下载滑动输入模型以启用此选项。",
|
||||
"REMOVE_MODEL": "移除滑动输入模型",
|
||||
"REMOVE_MODEL_HELP": "删除滑动输入模型",
|
||||
"DOWNLOAD_MODEL": "下载滑动输入模型",
|
||||
"DOWNLOAD_MODEL_HELP": "滑动输入仅适用于 QWERTY 布局的键盘"
|
||||
},
|
||||
"GRID_OPACITY": "地面网格不透明度",
|
||||
"GRID_OPACITY_HELP": "启用天空盒时地面网格的不透明度",
|
||||
"XWAYLAND_BY_DEFAULT": "默认以兼容模式运行应用程序",
|
||||
|
|
|
|||
|
|
@ -83,6 +83,7 @@ pub(crate) enum Task {
|
|||
SavePlayspaceCenter,
|
||||
SetTab(TabNameEnum),
|
||||
SettingUpdated(SettingType),
|
||||
SwipeTypeAction(Rc<str>),
|
||||
UpdateBool(SettingType, bool),
|
||||
UpdateFloat(SettingType, f32),
|
||||
UpdateInt(SettingType, i32),
|
||||
|
|
@ -110,6 +111,17 @@ trait SettingsTab {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn push_task_string(
|
||||
&mut self,
|
||||
_action: &str,
|
||||
_config: &mut GeneralConfig,
|
||||
_change_kind: &mut Option<ConfigChangeKind>,
|
||||
_layout: &mut Layout,
|
||||
_state: &mut ParserState,
|
||||
) -> anyhow::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn context_menu_custom(
|
||||
&mut self,
|
||||
_action: Rc<str>,
|
||||
|
|
@ -254,6 +266,16 @@ impl<T> Tab<T> for TabSettings<T> {
|
|||
_ => { /* do nothing */ }
|
||||
}
|
||||
}
|
||||
Task::SwipeTypeAction(action) => {
|
||||
if let Some(tab) = self.current_tab.as_mut() {
|
||||
let config = frontend.interface.general_config(data);
|
||||
let mut change_kind: Option<ConfigChangeKind> = None;
|
||||
tab.push_task_string(&action, config, &mut change_kind, &mut frontend.layout, &mut self.state)?;
|
||||
if let Some(change_kind) = change_kind {
|
||||
frontend.interface.config_changed(data, change_kind);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -329,6 +351,7 @@ pub(crate) enum SettingType {
|
|||
InvertScrollDirectionY,
|
||||
KeyboardMiddleClick,
|
||||
KeyboardSoundEnabled,
|
||||
KeyboardSwipeToTypeEnabled,
|
||||
Language,
|
||||
LeftHandedMouse,
|
||||
LongPressDuration,
|
||||
|
|
@ -387,6 +410,7 @@ impl SettingType {
|
|||
Self::InvertScrollDirectionX => &mut config.invert_scroll_direction_x,
|
||||
Self::InvertScrollDirectionY => &mut config.invert_scroll_direction_y,
|
||||
Self::KeyboardSoundEnabled => &mut config.keyboard_sound_enabled,
|
||||
Self::KeyboardSwipeToTypeEnabled => &mut config.keyboard_swipe_to_type_enabled,
|
||||
Self::LeftHandedMouse => &mut config.left_handed_mouse,
|
||||
Self::NotificationsEnabled => &mut config.notifications_enabled,
|
||||
Self::NotificationsSoundEnabled => &mut config.notifications_sound_enabled,
|
||||
|
|
@ -535,6 +559,7 @@ impl SettingType {
|
|||
Self::InvertScrollDirectionY => Ok("APP_SETTINGS.INVERT_SCROLL_DIRECTION_Y"),
|
||||
Self::KeyboardMiddleClick => Ok("APP_SETTINGS.KEYBOARD_MIDDLE_CLICK"),
|
||||
Self::KeyboardSoundEnabled => Ok("APP_SETTINGS.KEYBOARD_SOUND_ENABLED"),
|
||||
Self::KeyboardSwipeToTypeEnabled => Ok("APP_SETTINGS.KEYBOARD_SWIPE_TO_TYPE_ENABLED"),
|
||||
Self::Language => Ok("APP_SETTINGS.LANGUAGE"),
|
||||
Self::LeftHandedMouse => Ok("APP_SETTINGS.LEFT_HANDED_MOUSE"),
|
||||
Self::LongPressDuration => Ok("APP_SETTINGS.LONG_PRESS_DURATION"),
|
||||
|
|
@ -589,6 +614,7 @@ impl SettingType {
|
|||
Self::InputCaptureMethod => Some("APP_SETTINGS.INPUT_CAPTURE_METHOD_HELP"),
|
||||
Self::InputEmulationMethod => Some("APP_SETTINGS.INPUT_EMULATION_METHOD_HELP"),
|
||||
Self::KeyboardMiddleClick => Some("APP_SETTINGS.KEYBOARD_MIDDLE_CLICK_HELP"),
|
||||
Self::KeyboardSwipeToTypeEnabled => Some("APP_SETTINGS.KEYBOARD_SWIPE_TO_TYPE_ENABLED_HELP"),
|
||||
Self::LeftHandedMouse => Some("APP_SETTINGS.LEFT_HANDED_MOUSE_HELP"),
|
||||
Self::MouseAcceleration => Some("APP_SETTINGS.POINTER_ACCELERATION_HELP"),
|
||||
Self::ScreenRenderDown => Some("APP_SETTINGS.SCREEN_RENDER_DOWN_HELP"),
|
||||
|
|
|
|||
|
|
@ -1,7 +1,11 @@
|
|||
use std::path::PathBuf;
|
||||
use std::rc::Rc;
|
||||
|
||||
use wgui::{
|
||||
components::button::{ButtonClickEvent, ComponentButton},
|
||||
components::{
|
||||
button::{ButtonClickEvent, ComponentButton},
|
||||
checkbox::ComponentCheckbox,
|
||||
},
|
||||
globals::WguiGlobals,
|
||||
i18n::Translation,
|
||||
layout::WidgetID,
|
||||
|
|
@ -13,16 +17,19 @@ use wgui::{
|
|||
};
|
||||
use wlx_common::{async_executor::AsyncExecutor, config::GeneralConfig, dash_interface::ConfigChangeKind};
|
||||
|
||||
use crate::util::downloadable_file::DownloadableFile;
|
||||
use crate::{
|
||||
frontend::FrontendTasks,
|
||||
tab::settings::{
|
||||
SettingType, SettingsMountParams, SettingsTab, TabNameEnum, Task as ParentTask, horiz_cell,
|
||||
macros::{MacroParams, options_category, options_checkbox, options_range_f32},
|
||||
mount_requires_restart,
|
||||
},
|
||||
util::{
|
||||
popup_manager::PopupHolder,
|
||||
swipe_type::{SWIPE_TYPE_MODEL, swipe_type_delete_all_models, swipe_type_model_path, swwipe_type_model_downloaded},
|
||||
whisper::{
|
||||
WHISPER_MODELS, WhisperModel, whisper_any_models_downloaded, whisper_delete_all_models, whisper_model_from_name,
|
||||
WHISPER_MODELS, whisper_any_models_downloaded, whisper_delete_all_models, whisper_model_from_name,
|
||||
whisper_model_path,
|
||||
},
|
||||
},
|
||||
|
|
@ -33,8 +40,12 @@ use crate::{
|
|||
enum Task {
|
||||
WhisperDownloadClosed,
|
||||
WhisperRemoveUnused,
|
||||
WhisperDownload(&'static WhisperModel),
|
||||
WhisperDownload(&'static DownloadableFile),
|
||||
WhisperDownloadDone,
|
||||
SwipeTypeDownloadClosed,
|
||||
SwipeTypeRemoveAll,
|
||||
SwipeTypeDownloadAll,
|
||||
SwipeTypeDownloadDone,
|
||||
CloseDialog,
|
||||
ReloadTab,
|
||||
}
|
||||
|
|
@ -46,7 +57,8 @@ pub struct State {
|
|||
globals: WguiGlobals,
|
||||
tasks: Tasks<Task>,
|
||||
parent_tasks: Tasks<ParentTask>,
|
||||
pending_download: Option<&'static WhisperModel>,
|
||||
pending_whisper_download: Option<&'static DownloadableFile>,
|
||||
pending_swipe_download: Option<&'static DownloadableFile>,
|
||||
}
|
||||
|
||||
impl SettingsTab for State {
|
||||
|
|
@ -57,7 +69,7 @@ impl SettingsTab for State {
|
|||
for task in self.tasks.drain() {
|
||||
match task {
|
||||
Task::WhisperDownloadClosed => {
|
||||
if let Some(model) = self.pending_download.take()
|
||||
if let Some(model) = self.pending_whisper_download.take()
|
||||
&& !whisper_model_path(model.file_name).exists()
|
||||
{
|
||||
// download failed, set to selection to none
|
||||
|
|
@ -71,11 +83,17 @@ impl SettingsTab for State {
|
|||
let _ = whisper_delete_all_models().log_err("could not remove whisper models");
|
||||
}
|
||||
Task::WhisperDownload(model) => {
|
||||
self.pending_download = Some(model);
|
||||
self.show_whisper_download_dialog(model, par.executor.clone());
|
||||
self.pending_whisper_download = Some(model);
|
||||
self.show_download_dialogue(
|
||||
model,
|
||||
par.executor.clone(),
|
||||
whisper_model_path(model.file_name),
|
||||
Task::WhisperDownloadClosed,
|
||||
Task::WhisperDownloadDone,
|
||||
);
|
||||
}
|
||||
Task::WhisperDownloadDone => {
|
||||
if let Some(model) = self.pending_download.take() {
|
||||
if let Some(model) = self.pending_whisper_download.take() {
|
||||
par.general_config.whisper_model = model.file_name.into();
|
||||
par.config_change_kind.replace(ConfigChangeKind::Other);
|
||||
|
||||
|
|
@ -83,6 +101,34 @@ impl SettingsTab for State {
|
|||
self.parent_tasks.push(ParentTask::SetTab(TabNameEnum::Features));
|
||||
}
|
||||
}
|
||||
Task::SwipeTypeDownloadClosed => {
|
||||
self.pending_swipe_download = None;
|
||||
if !swwipe_type_model_downloaded().unwrap_or_default() {
|
||||
par.general_config.keyboard_swipe_to_type_enabled = false;
|
||||
par.config_change_kind.replace(ConfigChangeKind::Other);
|
||||
self.parent_tasks.push(ParentTask::SetTab(TabNameEnum::Features));
|
||||
}
|
||||
}
|
||||
Task::SwipeTypeRemoveAll => {
|
||||
let _ = swipe_type_delete_all_models().log_err("could not remove swipe type models");
|
||||
par.general_config.keyboard_swipe_to_type_enabled = false;
|
||||
}
|
||||
Task::SwipeTypeDownloadAll => {
|
||||
self.pending_swipe_download = Some(&SWIPE_TYPE_MODEL);
|
||||
self.show_download_dialogue(
|
||||
&SWIPE_TYPE_MODEL,
|
||||
par.executor.clone(),
|
||||
swipe_type_model_path(SWIPE_TYPE_MODEL.file_name),
|
||||
Task::SwipeTypeDownloadClosed,
|
||||
Task::SwipeTypeDownloadDone,
|
||||
);
|
||||
}
|
||||
Task::SwipeTypeDownloadDone => {
|
||||
if let Some(_) = self.pending_swipe_download.take() {
|
||||
par.config_change_kind.replace(ConfigChangeKind::Other);
|
||||
self.parent_tasks.push(ParentTask::SetTab(TabNameEnum::Features));
|
||||
}
|
||||
}
|
||||
Task::CloseDialog => {
|
||||
let close_dialog = self.popup_dialog.get_close_callback(par.layout);
|
||||
close_dialog();
|
||||
|
|
@ -135,6 +181,27 @@ impl SettingsTab for State {
|
|||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn push_task_string(
|
||||
&mut self,
|
||||
action: &str,
|
||||
_config: &mut GeneralConfig,
|
||||
_change_kind: &mut Option<ConfigChangeKind>,
|
||||
_layout: &mut wgui::layout::Layout,
|
||||
_state: &mut wgui::parser::ParserState,
|
||||
) -> anyhow::Result<()> {
|
||||
match action {
|
||||
"swipe_type_download" => {
|
||||
self.show_swipe_type_model_dialog_box_download()?;
|
||||
}
|
||||
"swipe_type_remove" => {
|
||||
self.tasks.push(Task::SwipeTypeRemoveAll);
|
||||
self.tasks.push(Task::ReloadTab);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl State {
|
||||
|
|
@ -149,6 +216,10 @@ impl State {
|
|||
whisper_models_dropdown(par.mp, c)?;
|
||||
}
|
||||
|
||||
if par.feats.swipe_to_type {
|
||||
swipe_type_models_button(par.mp, c)?;
|
||||
swipe_type_enabled_checkbox(par.mp, c)?;
|
||||
}
|
||||
options_checkbox(par.mp, c, SettingType::NotificationsEnabled)?;
|
||||
options_checkbox(par.mp, c, SettingType::NotificationsSoundEnabled)?;
|
||||
options_checkbox(par.mp, c, SettingType::KeyboardSoundEnabled)?;
|
||||
|
|
@ -178,30 +249,38 @@ impl State {
|
|||
popup_dialog,
|
||||
frontend_tasks: par.frontend_tasks.clone(),
|
||||
globals: par.mp.doc_params.globals.clone(),
|
||||
pending_download: None,
|
||||
pending_whisper_download: None,
|
||||
pending_swipe_download: None,
|
||||
})
|
||||
}
|
||||
|
||||
fn show_whisper_download_dialog(&mut self, model: &WhisperModel, executor: AsyncExecutor) {
|
||||
fn show_download_dialogue(
|
||||
&mut self,
|
||||
file: &DownloadableFile,
|
||||
executor: AsyncExecutor,
|
||||
target_path: PathBuf,
|
||||
on_closed: Task,
|
||||
on_downloaded: Task,
|
||||
) {
|
||||
views::download_file::mount_popup(
|
||||
self.popup_download.clone(),
|
||||
self.frontend_tasks.clone(),
|
||||
self.tasks.make_callback_box(Task::WhisperDownloadClosed),
|
||||
self.tasks.make_callback_box(on_closed),
|
||||
views::download_file::Params {
|
||||
globals: self.globals.clone(),
|
||||
executor,
|
||||
target_path: whisper_model_path(model.file_name),
|
||||
url: model.url.into(),
|
||||
on_downloaded: self.tasks.make_callback_box(Task::WhisperDownloadDone),
|
||||
target_path,
|
||||
url: file.url.into(),
|
||||
on_downloaded: self.tasks.make_callback_box(on_downloaded),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
fn show_whisper_model_dialog_box_download(&mut self, model: &'static WhisperModel) -> anyhow::Result<()> {
|
||||
fn show_whisper_model_dialog_box_download(&mut self, model: &'static DownloadableFile) -> anyhow::Result<()> {
|
||||
const ACTION_DOWNLOAD: &str = "download";
|
||||
const ACTION_CANCEL: &str = "cancel";
|
||||
|
||||
self.pending_download = Some(model);
|
||||
self.pending_whisper_download = Some(model);
|
||||
|
||||
let tasks = self.tasks.clone();
|
||||
views::dialog_box::mount_popup(
|
||||
|
|
@ -239,6 +318,45 @@ impl State {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn show_swipe_type_model_dialog_box_download(&mut self) -> anyhow::Result<()> {
|
||||
const ACTION_DOWNLOAD: &str = "download";
|
||||
const ACTION_CANCEL: &str = "cancel";
|
||||
|
||||
let tasks = self.tasks.clone();
|
||||
views::dialog_box::mount_popup(
|
||||
self.popup_dialog.clone(),
|
||||
self.frontend_tasks.clone(),
|
||||
views::dialog_box::Params {
|
||||
globals: self.globals.clone(),
|
||||
message: Translation::from_translation_key("APP_SETTINGS.SWIPE_TYPE.NEED_TO_DOWNLOAD_MODEL"),
|
||||
entries: vec![
|
||||
views::dialog_box::ButtonEntry {
|
||||
content: Translation::from_translation_key("APP_SETTINGS.CANCEL"),
|
||||
icon: "dashboard/close.svg",
|
||||
action: ACTION_CANCEL,
|
||||
},
|
||||
views::dialog_box::ButtonEntry {
|
||||
content: Translation::from_translation_key("DOWNLOAD"),
|
||||
icon: "dashboard/download.svg",
|
||||
action: ACTION_DOWNLOAD,
|
||||
},
|
||||
],
|
||||
on_action_click: Box::new(move |action| match action {
|
||||
ACTION_DOWNLOAD => {
|
||||
tasks.push(Task::SwipeTypeDownloadAll);
|
||||
}
|
||||
ACTION_CANCEL => {
|
||||
tasks.push(Task::CloseDialog);
|
||||
tasks.push(Task::SwipeTypeDownloadClosed);
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn show_whisper_model_dialog_box_cleanup(&mut self) -> anyhow::Result<()> {
|
||||
const ACTION_REMOVE: &str = "remove";
|
||||
const ACTION_CANCEL: &str = "cancel";
|
||||
|
|
@ -355,3 +473,101 @@ fn whisper_models_dropdown(mp: &mut MacroParams, parent: WidgetID) -> anyhow::Re
|
|||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn swipe_type_models_button(mp: &mut MacroParams, parent: WidgetID) -> anyhow::Result<()> {
|
||||
let id = mp.idx.to_string();
|
||||
mp.idx += 1;
|
||||
|
||||
let id_cell = horiz_cell(mp.layout, parent)?;
|
||||
|
||||
let all_downloaded = swwipe_type_model_downloaded().unwrap_or_default();
|
||||
let (translation, icon, action) = if all_downloaded {
|
||||
(
|
||||
"APP_SETTINGS.SWIPE_TYPE.REMOVE_MODEL",
|
||||
"dashboard/trash.svg",
|
||||
"swipe_type_remove",
|
||||
)
|
||||
} else {
|
||||
(
|
||||
"APP_SETTINGS.SWIPE_TYPE.DOWNLOAD_MODEL",
|
||||
"dashboard/download.svg",
|
||||
"swipe_type_download",
|
||||
)
|
||||
};
|
||||
|
||||
let mut params = TemplateParams::new();
|
||||
params.insert("id", &id);
|
||||
params.insert("translation", translation);
|
||||
params.insert("icon", icon);
|
||||
|
||||
mp.parser_state
|
||||
.instantiate_template(mp.doc_params, "ButtonText", mp.layout, id_cell, params)?;
|
||||
|
||||
let btn = mp.parser_state.fetch_component_as::<ComponentButton>(&id)?;
|
||||
btn.on_click(Rc::new({
|
||||
let parent_tasks = mp.tasks.clone();
|
||||
let action = Rc::<str>::from(action);
|
||||
move |_common, _e: ButtonClickEvent| {
|
||||
parent_tasks.push(ParentTask::SwipeTypeAction(action.clone()));
|
||||
Ok(())
|
||||
}
|
||||
}));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn swipe_type_enabled_checkbox(mp: &mut MacroParams, parent: WidgetID) -> anyhow::Result<()> {
|
||||
let id = mp.idx.to_string();
|
||||
mp.idx += 1;
|
||||
|
||||
let setting = SettingType::KeyboardSwipeToTypeEnabled;
|
||||
|
||||
// cannot enable swipe-to-type until the model is downloaded
|
||||
let model_downloaded = swwipe_type_model_downloaded().unwrap_or_default();
|
||||
|
||||
let mut params = TemplateParams::new();
|
||||
params.insert("id", &id);
|
||||
|
||||
match setting.get_translation() {
|
||||
Ok(translation) => params.insert("translation", translation),
|
||||
Err(raw_text) => params.insert("text", raw_text),
|
||||
};
|
||||
|
||||
let tooltip = if model_downloaded {
|
||||
setting.get_tooltip()
|
||||
} else {
|
||||
Some("APP_SETTINGS.SWIPE_TYPE.MODEL_REQUIRED_TO_ENABLE")
|
||||
};
|
||||
if let Some(tooltip) = tooltip {
|
||||
params.insert("tooltip", tooltip);
|
||||
}
|
||||
|
||||
let checked = if *setting.mut_bool(mp.config) { "1" } else { "0" };
|
||||
params.insert("checked", checked);
|
||||
|
||||
let id_cell = horiz_cell(mp.layout, parent)?;
|
||||
|
||||
mp.parser_state
|
||||
.instantiate_template(mp.doc_params, "CheckBoxSetting", mp.layout, id_cell, params)?;
|
||||
|
||||
if setting.requires_restart() {
|
||||
mount_requires_restart(mp.layout, id_cell)?;
|
||||
}
|
||||
|
||||
let checkbox = mp.parser_state.fetch_component_as::<ComponentCheckbox>(&id)?;
|
||||
|
||||
if !model_downloaded {
|
||||
let mut common = mp.layout.common();
|
||||
checkbox.set_disabled(&mut common, true);
|
||||
}
|
||||
|
||||
checkbox.on_toggle(Box::new({
|
||||
let tasks = mp.tasks.clone();
|
||||
move |_common, e| {
|
||||
tasks.push(ParentTask::UpdateBool(setting, e.checked));
|
||||
Ok(())
|
||||
}
|
||||
}));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
pub struct DownloadableFile {
|
||||
pub file_name: &'static str,
|
||||
pub display_name: &'static str,
|
||||
pub url: &'static str,
|
||||
}
|
||||
|
|
@ -1,9 +1,11 @@
|
|||
pub mod cached_fetcher;
|
||||
pub mod downloadable_file;
|
||||
pub mod networking;
|
||||
pub mod openxr_bindings;
|
||||
pub mod pactl_wrapper;
|
||||
pub mod popup_manager;
|
||||
pub mod steam_utils;
|
||||
pub mod swipe_type;
|
||||
pub mod toast_manager;
|
||||
pub mod wgui_simple;
|
||||
pub mod whisper;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,47 @@
|
|||
use std::{fs, io, path::PathBuf};
|
||||
|
||||
use crate::util::downloadable_file::DownloadableFile;
|
||||
use wlx_common::data_dir;
|
||||
|
||||
pub const SWIPE_TYPE_MODEL: DownloadableFile = DownloadableFile {
|
||||
file_name: "en.tar",
|
||||
display_name: "English Qwerty (15 MiB)",
|
||||
url: "https://github.com/oneshinyboi/super-swipe-type/raw/refs/tags/v0.4.2/crates/super-swipe-type/assets/en.tar",
|
||||
};
|
||||
|
||||
pub fn swipe_type_model_folder() -> PathBuf {
|
||||
data_dir::get_path("swipe_type")
|
||||
}
|
||||
|
||||
pub fn swipe_type_model_path(file_name: &str) -> PathBuf {
|
||||
swipe_type_model_folder().join(file_name)
|
||||
}
|
||||
|
||||
pub fn swwipe_type_model_downloaded() -> io::Result<bool> {
|
||||
let path = swipe_type_model_folder();
|
||||
if !path.is_dir() {
|
||||
return Ok(false);
|
||||
}
|
||||
if !path.join(SWIPE_TYPE_MODEL.file_name).exists() {
|
||||
return Ok(false);
|
||||
}
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
pub fn swipe_type_delete_all_models() -> io::Result<()> {
|
||||
let path = swipe_type_model_folder();
|
||||
if !path.is_dir() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
for entry in fs::read_dir(path)? {
|
||||
let entry = entry?;
|
||||
let file_type = entry.file_type()?;
|
||||
|
||||
if file_type.is_file() || file_type.is_symlink() {
|
||||
fs::remove_file(entry.path())?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -1,42 +1,37 @@
|
|||
use std::{fs, io, path::PathBuf};
|
||||
|
||||
use crate::util::downloadable_file::DownloadableFile;
|
||||
use wlx_common::data_dir;
|
||||
|
||||
pub struct WhisperModel {
|
||||
pub file_name: &'static str,
|
||||
pub display_name: &'static str,
|
||||
pub url: &'static str,
|
||||
}
|
||||
|
||||
pub const WHISPER_MODELS: &[WhisperModel] = &[
|
||||
WhisperModel {
|
||||
pub const WHISPER_MODELS: &[DownloadableFile] = &[
|
||||
DownloadableFile {
|
||||
file_name: "ggml-base-q8_0.bin",
|
||||
display_name: "Base Q8 (78MiB)",
|
||||
url: "https://wayvr.org/files/whisper/ggml-base-q8_0.bin",
|
||||
},
|
||||
WhisperModel {
|
||||
DownloadableFile {
|
||||
file_name: "ggml-small-q8_0.bin",
|
||||
display_name: "Small Q8 (252MiB)",
|
||||
url: "https://wayvr.org/files/whisper/ggml-small-q8_0.bin",
|
||||
},
|
||||
WhisperModel {
|
||||
DownloadableFile {
|
||||
file_name: "ggml-large-v3-turbo-q5_0.bin",
|
||||
display_name: "Turbo Q5 (574MiB)",
|
||||
url: "https://wayvr.org/files/whisper/ggml-large-v3-turbo-q5_0.bin",
|
||||
},
|
||||
WhisperModel {
|
||||
DownloadableFile {
|
||||
file_name: "ggml-large-v3-turbo-q8_0.bin",
|
||||
display_name: "Turbo Q8 (874MiB)",
|
||||
url: "https://wayvr.org/files/whisper/ggml-large-v3-turbo-q8_0.bin",
|
||||
},
|
||||
WhisperModel {
|
||||
DownloadableFile {
|
||||
file_name: "ggml-large-v3-turbo.bin",
|
||||
display_name: "Turbo (1.5GiB)",
|
||||
url: "https://wayvr.org/files/whisper/ggml-large-v3-turbo.bin",
|
||||
},
|
||||
];
|
||||
|
||||
pub fn whisper_model_from_name(file_name: &str) -> Option<&'static WhisperModel> {
|
||||
pub fn whisper_model_from_name(file_name: &str) -> Option<&'static DownloadableFile> {
|
||||
WHISPER_MODELS.iter().find(|x| x.file_name == file_name)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -127,12 +127,13 @@ xkbcommon = {
|
|||
calloop = { version = "0.14.4", optional = true }
|
||||
calloop-wayland-source = { version = "0.4.1", optional = true }
|
||||
evdev = "0.13.2"
|
||||
super-swipe-type = { version = "0.4.2", optional = true }
|
||||
|
||||
[build-dependencies]
|
||||
regex.workspace = true
|
||||
|
||||
[features]
|
||||
default = ["openvr", "openxr", "osc", "wayland", "whisper", "x11"]
|
||||
default = ["openvr", "openxr", "osc", "wayland", "whisper", "x11", "swipe-to-type"]
|
||||
as-raw-xcb-connection = []
|
||||
feat-monado-metrics = []
|
||||
openvr = ["dep:json", "dep:ovr_overlay"]
|
||||
|
|
@ -147,5 +148,6 @@ whisper = [
|
|||
"dep:pipewire",
|
||||
"dep:whisper-rs"
|
||||
]
|
||||
swipe-to-type = ["dep:super-swipe-type"]
|
||||
x11 = ["dep:xcb", "wlx-capture/xshm", "xkbcommon/x11"]
|
||||
xcb = ["dep:xcb"]
|
||||
|
|
|
|||
|
|
@ -13,6 +13,11 @@
|
|||
width="${width}" height="${height}" min_width="${width}" min_height="${height}" max_width="${width}" max_height="${height}"
|
||||
/>
|
||||
|
||||
<macro name="prediction_rect"
|
||||
margin="2" padding="8 16" overflow="visible" box_sizing="border_box"
|
||||
border_color="outline" border="2" round="6" color="background_variant" color2="background" gradient="vertical"
|
||||
align_items="center" justify_content="center" />
|
||||
|
||||
<theme>
|
||||
<var key="text_size" value="20" />
|
||||
</theme>
|
||||
|
|
@ -94,6 +99,13 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Word prediction key for swipe typing -->
|
||||
<template name="KeyPrediction">
|
||||
<rectangle id="${id}" macro="prediction_rect" height="60" min_height="60" flex_grow="1" flex_shrink="1" flex_basis="0">
|
||||
<label text="${text}" size="16" flex_wrap="wrap" />
|
||||
</rectangle>
|
||||
</template>
|
||||
|
||||
<macro name="button_style" border="2" round="6"
|
||||
align_items="center" justify_content="center" padding="6" width="60" height="60" overflow="visible" />
|
||||
|
||||
|
|
@ -246,7 +258,10 @@
|
|||
</div>
|
||||
</div>
|
||||
</rectangle>
|
||||
<div width="100%" height="13" interactable="0" />
|
||||
<div width="100%" height="11" interactable="0" />
|
||||
<rectangle id="swipe_predictions_root" macro="bg_rect" padding="10" align_items="center" justify_content="space_between" flex_direction="row" min_height="60">
|
||||
</rectangle>
|
||||
<div width="100%" height="11" interactable="0" />
|
||||
<rectangle id="keyboard_root" macro="bg_rect" flex_direction="column" padding="10">
|
||||
</rectangle>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ use std::{
|
|||
use anyhow::Context;
|
||||
use glam::{DVec2, Vec2};
|
||||
use slotmap::DenseSlotMap;
|
||||
use smithay::wayland::selection::data_device::set_data_device_selection;
|
||||
use smithay::{
|
||||
backend::input::{Axis, AxisSource, ButtonState, Keycode},
|
||||
input::{
|
||||
|
|
@ -21,13 +22,13 @@ use smithay::{
|
|||
use wgui::log::LogErr;
|
||||
use xkbcommon::xkb;
|
||||
|
||||
use crate::backend::wayvr::{ExternalProcessRequest, WayVRTask};
|
||||
|
||||
use super::{
|
||||
ProcessWayVREnv,
|
||||
comp::{self, ClientState},
|
||||
process,
|
||||
};
|
||||
use crate::backend::wayvr::comp::Application;
|
||||
use crate::backend::wayvr::{ExternalProcessRequest, WayVRTask};
|
||||
|
||||
pub struct WayVRClient {
|
||||
pub client: wayland_server::Client,
|
||||
|
|
@ -259,6 +260,17 @@ impl WayVRCompositor {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn set_clipboard_text(&mut self, text: String) {
|
||||
set_data_device_selection::<Application>(
|
||||
&self.state.display_handle,
|
||||
&self.state.seat,
|
||||
vec![
|
||||
"text/plain;charset=utf-8".to_string(),
|
||||
"text/plain".to_string(),
|
||||
],
|
||||
text.as_bytes().into(),
|
||||
);
|
||||
}
|
||||
pub fn set_keymap(&mut self, keymap: &xkb::Keymap) -> anyhow::Result<()> {
|
||||
// Smithay only accepts keymaps in a string form due to thread safety concerns
|
||||
self.seat_keyboard
|
||||
|
|
|
|||
|
|
@ -1201,6 +1201,9 @@ impl WvrServerState {
|
|||
pub fn send_key(&mut self, virtual_key: u32, down: bool) {
|
||||
self.manager.send_key(virtual_key, down);
|
||||
}
|
||||
pub fn set_clipboard_text(&mut self, text: String) {
|
||||
self.manager.set_clipboard_text(text);
|
||||
}
|
||||
|
||||
pub fn set_keymap(&mut self, keymap: &xkb::Keymap) -> anyhow::Result<()> {
|
||||
self.manager.set_keymap(keymap)
|
||||
|
|
|
|||
|
|
@ -180,6 +180,7 @@ pub struct AutoSettings {
|
|||
pub context_menu_hold_and_release: bool,
|
||||
pub capture_method: CaptureMethod,
|
||||
pub keyboard_middle_click_mode: AltModifier,
|
||||
pub keyboard_swipe_to_type_enabled: bool,
|
||||
pub autostart_apps: Vec<WvrProcessLaunchParams>,
|
||||
pub pinned_apps: Vec<PinnedApp>,
|
||||
pub handsfree_pointer: HandsfreePointer,
|
||||
|
|
@ -255,6 +256,7 @@ pub fn save_settings(config: &GeneralConfig) -> anyhow::Result<()> {
|
|||
context_menu_hold_and_release: config.context_menu_hold_and_release,
|
||||
capture_method: config.capture_method,
|
||||
keyboard_middle_click_mode: config.keyboard_middle_click_mode,
|
||||
keyboard_swipe_to_type_enabled: config.keyboard_swipe_to_type_enabled,
|
||||
autostart_apps: config.autostart_apps.clone(),
|
||||
pinned_apps: config.pinned_apps.clone(),
|
||||
handsfree_pointer: config.handsfree_pointer,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,11 @@
|
|||
use std::{rc::Rc, time::Duration};
|
||||
|
||||
#[cfg(feature = "swipe-to-type")]
|
||||
use super::init_swipe_type_manager;
|
||||
use super::{
|
||||
KeyButtonData, KeyState, KeyboardState, handle_mouse_motion, handle_press, handle_release,
|
||||
layout::{self, KeyCapType},
|
||||
};
|
||||
use crate::{
|
||||
app_misc,
|
||||
gui::{
|
||||
|
|
@ -30,22 +36,18 @@ use wgui::{
|
|||
sprite::WidgetSprite,
|
||||
},
|
||||
};
|
||||
|
||||
use super::{
|
||||
KeyButtonData, KeyState, KeyboardState, handle_press, handle_release,
|
||||
layout::{self, KeyCapType},
|
||||
};
|
||||
#[cfg(feature = "swipe-to-type")]
|
||||
use wlx_common::data_dir;
|
||||
|
||||
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(),
|
||||
}
|
||||
}
|
||||
|
||||
fn bool_to_rc_str(val: bool) -> Rc<str> {
|
||||
if val { "1" } else { "0" }.into()
|
||||
}
|
||||
|
|
@ -137,7 +139,7 @@ pub(super) fn create_keyboard_panel(
|
|||
params.insert_rc("width", key_width.to_string().into());
|
||||
params.insert_rc("height", key_height.to_string().into());
|
||||
|
||||
let mut label = key.label.into_iter();
|
||||
let mut label = key.label.clone().into_iter();
|
||||
label
|
||||
.next()
|
||||
.and_then(|s| params.insert_rc("text", s.into()));
|
||||
|
|
@ -197,6 +199,9 @@ pub(super) fn create_keyboard_panel(
|
|||
})
|
||||
};
|
||||
|
||||
let key_cap_type: Rc<KeyCapType> = Rc::from(key.cap_type);
|
||||
let key_label: Rc<Vec<String>> = Rc::from(key.label);
|
||||
|
||||
let width_mul = 1. / my_size_f32;
|
||||
|
||||
panel.add_event_listener(
|
||||
|
|
@ -228,26 +233,69 @@ pub(super) fn create_keyboard_panel(
|
|||
EventListenerKind::MousePress,
|
||||
Box::new({
|
||||
let k = key_state.clone();
|
||||
let k_label = key_label.clone();
|
||||
let k_cap_type = key_cap_type.clone();
|
||||
move |common, data, app, state| {
|
||||
let CallbackMetadata::MouseButton(button) = data.metadata else {
|
||||
panic!("CallbackMetadata should contain MouseButton!");
|
||||
};
|
||||
let within_key_pos = data
|
||||
.metadata
|
||||
.get_mouse_pos_normalized(&common.alterables.transform_stack);
|
||||
|
||||
handle_press(app, &k, state, button);
|
||||
handle_press(
|
||||
app,
|
||||
&k,
|
||||
&k_label,
|
||||
&k_cap_type,
|
||||
&within_key_pos,
|
||||
state,
|
||||
button,
|
||||
button.device,
|
||||
);
|
||||
on_press_anim(k.clone(), common, data);
|
||||
Ok(EventResult::Pass)
|
||||
}
|
||||
}),
|
||||
);
|
||||
panel.add_event_listener(
|
||||
widget_id,
|
||||
EventListenerKind::MouseMotion,
|
||||
Box::new({
|
||||
let k = key_state.clone();
|
||||
let k_label = key_label.clone();
|
||||
let k_cap_type = key_cap_type.clone();
|
||||
move |common, data, _app, state| {
|
||||
let within_key_pos = data
|
||||
.metadata
|
||||
.get_mouse_pos_normalized(&common.alterables.transform_stack);
|
||||
let CallbackMetadata::MousePosition(position) = data.metadata else {
|
||||
panic!("CallbackMetadata should contain MousePosition!");
|
||||
};
|
||||
|
||||
handle_mouse_motion(
|
||||
&k,
|
||||
&k_label,
|
||||
&k_cap_type,
|
||||
state,
|
||||
&within_key_pos,
|
||||
position.device,
|
||||
);
|
||||
Ok(EventResult::Pass)
|
||||
}
|
||||
}),
|
||||
);
|
||||
panel.add_event_listener(
|
||||
widget_id,
|
||||
EventListenerKind::MouseRelease,
|
||||
Box::new({
|
||||
let k = key_state.clone();
|
||||
let k_cap_type = key_cap_type.clone();
|
||||
move |common, data, app, state| {
|
||||
if handle_release(app, &k, state) {
|
||||
if handle_release(app, &k, &k_cap_type, state) {
|
||||
on_release_anim(k.clone(), common, data);
|
||||
}
|
||||
|
||||
Ok(EventResult::Pass)
|
||||
}
|
||||
}),
|
||||
|
|
@ -317,6 +365,23 @@ pub(super) fn create_keyboard_panel(
|
|||
elems_changed = true;
|
||||
}
|
||||
}
|
||||
if !app.session.config.keyboard_swipe_to_type_enabled {
|
||||
panel.state.swipe_typing_manager = None;
|
||||
panel.state.swipe_candidate_slot = 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"),
|
||||
);
|
||||
|
||||
super::prediction_bar::set_visible(panel, true);
|
||||
}
|
||||
}
|
||||
|
||||
OverlayEventData::CustomCommand { element, command } => {
|
||||
|
|
@ -411,7 +476,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,
|
||||
|
|
@ -459,7 +524,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,
|
||||
|
|
@ -509,7 +574,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,
|
||||
|
|
@ -524,7 +589,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,
|
||||
|
|
|
|||
|
|
@ -246,7 +246,7 @@ pub(super) struct KeyData {
|
|||
pub(super) cap_type: KeyCapType,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
pub enum KeyCapType {
|
||||
/// Label an SVG
|
||||
Special,
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ use std::{
|
|||
sync::{Arc, atomic::Ordering},
|
||||
};
|
||||
|
||||
use crate::overlays::keyboard::layout::KeyCapType;
|
||||
use crate::overlays::keyboard::swipe_type::{PredictionSlot, SwipeTypingManager};
|
||||
use crate::overlays::toast::Toast;
|
||||
use crate::{
|
||||
KEYMAP_CHANGE,
|
||||
|
|
@ -29,16 +31,19 @@ use crate::{
|
|||
},
|
||||
};
|
||||
use anyhow::Context;
|
||||
use glam::{Affine3A, Quat, Vec3, vec3};
|
||||
use glam::{Affine3A, Quat, Vec2, Vec3, vec3};
|
||||
use regex::Regex;
|
||||
use slotmap::{SlotMap, new_key_type};
|
||||
use smallvec::SmallVec;
|
||||
use wgui::event::DeviceBitmask;
|
||||
use wgui::{
|
||||
color::WguiColor,
|
||||
event::{InternalStateChangeEvent, MouseButtonEvent, MouseButtonIndex},
|
||||
i18n::Translation,
|
||||
layout::WidgetID,
|
||||
};
|
||||
#[cfg(feature = "swipe-to-type")]
|
||||
use wlx_common::data_dir;
|
||||
use wlx_common::windowing::{OverlayWindowState, Positioning};
|
||||
use wlx_common::{
|
||||
config::AltModifier,
|
||||
|
|
@ -47,9 +52,122 @@ use wlx_common::{
|
|||
|
||||
pub mod builder;
|
||||
mod layout;
|
||||
mod prediction_bar;
|
||||
|
||||
#[cfg(feature = "swipe-to-type")]
|
||||
mod swipe_type;
|
||||
|
||||
#[cfg(not(feature = "swipe-to-type"))]
|
||||
mod swipe_type {
|
||||
use glam::Vec2;
|
||||
use wgui::event::{DeviceBitmask, MouseButtonIndex};
|
||||
|
||||
pub struct SwipeTypingManager;
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub struct PredictionSlot;
|
||||
|
||||
#[allow(dead_code)] // stub used only to satisfy compilation when the feature is disabled
|
||||
impl PredictionSlot {
|
||||
pub fn new() -> Self {
|
||||
Self
|
||||
}
|
||||
pub fn set(&self, _value: Option<Vec<String>>) {}
|
||||
pub fn take(&self) -> Option<Option<Vec<String>>> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[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, PredictionSlot)> {
|
||||
Ok((Self, PredictionSlot))
|
||||
}
|
||||
pub fn add_swipe(
|
||||
&mut self,
|
||||
_within_key_pos_normalized: &Vec2,
|
||||
_key_label: char,
|
||||
_device: DeviceBitmask,
|
||||
_index: Option<MouseButtonIndex>,
|
||||
) {
|
||||
}
|
||||
pub fn predict(&mut self) -> anyhow::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
pub fn reset(&mut self) {}
|
||||
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,
|
||||
) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)]
|
||||
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)]
|
||||
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> {
|
||||
|
|
@ -80,6 +198,8 @@ pub fn create_keyboard(app: &mut AppState) -> anyhow::Result<OverlayWindowConfig
|
|||
keymap_switch_layouts: app.session.config.keyboard_layouts.clone(),
|
||||
keymap_switch_index: 0,
|
||||
keymap_switch_pending: false,
|
||||
swipe_typing_manager: None,
|
||||
swipe_candidate_slot: None,
|
||||
};
|
||||
|
||||
let mut backend = KeyboardBackend {
|
||||
|
|
@ -107,14 +227,25 @@ pub fn create_keyboard(app: &mut AppState) -> anyhow::Result<OverlayWindowConfig
|
|||
transform: Affine3A::from_scale_rotation_translation(
|
||||
Vec3::ONE * width,
|
||||
Quat::from_rotation_x(-10f32.to_radians()),
|
||||
vec3(0.0, -0.65, -0.5),
|
||||
vec3(0.0, -0.69, -0.5),
|
||||
),
|
||||
..OverlayWindowState::default()
|
||||
},
|
||||
..OverlayWindowConfig::from_backend(Box::new(backend))
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(feature = "swipe-to-type")]
|
||||
pub(self) fn init_swipe_type_manager(state: &mut KeyboardState, model_path: std::path::PathBuf) {
|
||||
match SwipeTypingManager::new(model_path) {
|
||||
Ok((engine, slot)) => {
|
||||
state.swipe_typing_manager = Some(engine);
|
||||
state.swipe_candidate_slot = Some(slot);
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Error occurred while trying to load swipe engine: {}", e);
|
||||
}
|
||||
};
|
||||
}
|
||||
const fn alt_modifier_to_key(m: AltModifier) -> KeyModifier {
|
||||
match m {
|
||||
AltModifier::Shift => SHIFT,
|
||||
|
|
@ -147,8 +278,19 @@ impl KeyboardBackend {
|
|||
keymap: Option<&XkbKeymap>,
|
||||
app: &mut AppState,
|
||||
) -> anyhow::Result<KeyboardPanelKey> {
|
||||
let panel =
|
||||
create_keyboard_panel(app, keymap, self.default_state.take(), &self.wlx_layout)?;
|
||||
let mut state = self.default_state.take();
|
||||
|
||||
if app.session.config.keyboard_swipe_to_type_enabled {
|
||||
#[cfg(feature = "swipe-to-type")]
|
||||
init_swipe_type_manager(&mut state, data_dir::get_path("swipe_type").join("en.tar"));
|
||||
log::info!("swipe engine created");
|
||||
}
|
||||
|
||||
let mut panel = create_keyboard_panel(app, keymap, state, &self.wlx_layout)?;
|
||||
|
||||
if !app.session.config.keyboard_swipe_to_type_enabled {
|
||||
prediction_bar::set_visible(&mut panel, false);
|
||||
}
|
||||
|
||||
let id = self.layout_panels.insert(panel);
|
||||
if let Some(layout_name) = keymap.and_then(|k| k.get_name()) {
|
||||
|
|
@ -173,10 +315,10 @@ impl KeyboardBackend {
|
|||
if self.active_layout.eq(new_key) {
|
||||
return Ok(false);
|
||||
}
|
||||
self.internal_switch_keymap(*new_key);
|
||||
self.internal_switch_keymap(*new_key, app);
|
||||
} else {
|
||||
let new_key = self.add_new_keymap(Some(keymap), app)?;
|
||||
self.internal_switch_keymap(new_key);
|
||||
self.internal_switch_keymap(new_key, app);
|
||||
}
|
||||
app.tasks
|
||||
.enqueue(TaskType::Overlay(OverlayTask::GlobalChange(
|
||||
|
|
@ -185,20 +327,35 @@ impl KeyboardBackend {
|
|||
Ok(true)
|
||||
}
|
||||
|
||||
fn internal_switch_keymap(&mut self, new_key: KeyboardPanelKey) {
|
||||
let state_from = self
|
||||
fn internal_switch_keymap(&mut self, new_key: KeyboardPanelKey, app: &AppState) {
|
||||
let mut state_from = self
|
||||
.layout_panels
|
||||
.get_mut(self.active_layout)
|
||||
.unwrap()
|
||||
.state
|
||||
.take();
|
||||
|
||||
if app.session.config.keyboard_swipe_to_type_enabled {
|
||||
#[cfg(feature = "swipe-to-type")]
|
||||
init_swipe_type_manager(
|
||||
&mut state_from,
|
||||
data_dir::get_path("swipe_type").join("en.tar"),
|
||||
);
|
||||
}
|
||||
|
||||
self.active_layout = new_key;
|
||||
|
||||
self.layout_panels
|
||||
.get_mut(self.active_layout)
|
||||
.unwrap()
|
||||
.state = state_from;
|
||||
|
||||
if !app.session.config.keyboard_swipe_to_type_enabled {
|
||||
prediction_bar::set_visible(
|
||||
self.layout_panels.get_mut(self.active_layout).unwrap(),
|
||||
false,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn get_effective_keymap(&mut self) -> anyhow::Result<XkbKeymap> {
|
||||
|
|
@ -232,6 +389,13 @@ impl KeyboardBackend {
|
|||
}
|
||||
}
|
||||
|
||||
fn update_swipe_prediction_bar(&mut self, app: &mut AppState) -> anyhow::Result<()> {
|
||||
if prediction_bar::update(self.panel(), app)? {
|
||||
self.panel().process_custom_elems(app);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn switch_keymap_by_name(
|
||||
&mut self,
|
||||
keymap_name: &str,
|
||||
|
|
@ -335,6 +499,7 @@ impl OverlayBackend for KeyboardBackend {
|
|||
}
|
||||
}
|
||||
|
||||
self.update_swipe_prediction_bar(app)?;
|
||||
self.panel().should_render(app)
|
||||
}
|
||||
fn render(&mut self, app: &mut AppState, rdr: &mut RenderResources) -> anyhow::Result<()> {
|
||||
|
|
@ -399,6 +564,8 @@ struct KeyboardState {
|
|||
keymap_switch_layouts: Vec<Arc<str>>,
|
||||
keymap_switch_index: usize,
|
||||
keymap_switch_pending: bool,
|
||||
swipe_typing_manager: Option<SwipeTypingManager>,
|
||||
swipe_candidate_slot: Option<PredictionSlot>,
|
||||
}
|
||||
|
||||
macro_rules! take_and_leave_default {
|
||||
|
|
@ -421,6 +588,8 @@ impl KeyboardState {
|
|||
keymap_switch_layouts: std::mem::take(&mut self.keymap_switch_layouts),
|
||||
keymap_switch_index: self.keymap_switch_index,
|
||||
keymap_switch_pending: false,
|
||||
swipe_typing_manager: None,
|
||||
swipe_candidate_slot: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -471,26 +640,56 @@ enum KeyButtonData {
|
|||
},
|
||||
}
|
||||
|
||||
fn handle_mouse_motion(
|
||||
key: &KeyState,
|
||||
key_label: &Vec<String>,
|
||||
key_cap_type: &KeyCapType,
|
||||
keyboard: &mut KeyboardState,
|
||||
within_key_pos: &Option<Vec2>,
|
||||
device: DeviceBitmask,
|
||||
) {
|
||||
if let KeyButtonData::Key { .. } = &key.button_state
|
||||
&& let Some(swipe_manager) = keyboard.swipe_typing_manager.as_mut()
|
||||
{
|
||||
swipe_manager.handle_key_motion(key_cap_type, within_key_pos, key_label, device);
|
||||
}
|
||||
}
|
||||
fn handle_press(
|
||||
app: &mut AppState,
|
||||
key: &KeyState,
|
||||
key_label: &Vec<String>,
|
||||
key_cap_type: &KeyCapType,
|
||||
within_key_pos: &Option<Vec2>,
|
||||
keyboard: &mut KeyboardState,
|
||||
button: MouseButtonEvent,
|
||||
device: DeviceBitmask,
|
||||
) {
|
||||
match &key.button_state {
|
||||
KeyButtonData::Key { vk, pressed } => {
|
||||
keyboard.modifiers |= match button.index {
|
||||
MouseButtonIndex::Right => SHIFT,
|
||||
MouseButtonIndex::Middle => keyboard.alt_modifier,
|
||||
_ => 0,
|
||||
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,
|
||||
),
|
||||
None => KeyPressOutcome::Dispatch,
|
||||
};
|
||||
|
||||
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, true);
|
||||
pressed.set(true);
|
||||
play_key_click(app);
|
||||
if matches!(outcome, KeyPressOutcome::Dispatch) {
|
||||
keyboard.modifiers |= match button.index {
|
||||
MouseButtonIndex::Right => SHIFT,
|
||||
MouseButtonIndex::Middle => keyboard.alt_modifier,
|
||||
_ => 0,
|
||||
};
|
||||
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, true);
|
||||
pressed.set(true);
|
||||
play_key_click(app);
|
||||
}
|
||||
}
|
||||
KeyButtonData::Modifier { modifier, sticky } => {
|
||||
sticky.set(keyboard.modifiers & *modifier == 0);
|
||||
|
|
@ -537,20 +736,54 @@ fn handle_press(
|
|||
}
|
||||
}
|
||||
|
||||
fn handle_release(app: &mut AppState, key: &KeyState, keyboard: &mut KeyboardState) -> bool {
|
||||
fn handle_release(
|
||||
app: &mut AppState,
|
||||
key: &KeyState,
|
||||
k_cap_type: &KeyCapType,
|
||||
keyboard: &mut KeyboardState,
|
||||
) -> bool {
|
||||
match &key.button_state {
|
||||
KeyButtonData::Key { vk, pressed } => {
|
||||
pressed.set(false);
|
||||
let outcome = match keyboard.swipe_typing_manager.as_mut() {
|
||||
Some(swipe_manager) => {
|
||||
swipe_manager.handle_key_release(k_cap_type, keyboard.alt_modifier)
|
||||
}
|
||||
None => KeyReleaseOutcome::Normal,
|
||||
};
|
||||
|
||||
for m in &AUTO_RELEASE_MODS {
|
||||
if keyboard.modifiers & *m != 0 {
|
||||
keyboard.modifiers &= !*m;
|
||||
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
|
||||
.send_key_routed(app.wvr_server.as_mut(), *vk, true);
|
||||
pressed.set(true);
|
||||
app.hid_provider
|
||||
.send_key_routed(app.wvr_server.as_mut(), *vk, false);
|
||||
|
||||
for m in &AUTO_RELEASE_MODS {
|
||||
if keyboard.modifiers & *m != 0 {
|
||||
keyboard.modifiers &= !*m;
|
||||
}
|
||||
}
|
||||
play_key_click(app);
|
||||
}
|
||||
KeyReleaseOutcome::Normal => {
|
||||
pressed.set(false);
|
||||
|
||||
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
|
||||
}
|
||||
KeyButtonData::Modifier { modifier, sticky } => {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,167 @@
|
|||
use std::{collections::HashMap, rc::Rc};
|
||||
|
||||
use super::{
|
||||
KeyButtonData, KeyState, KeyboardState,
|
||||
builder::{new_doc_params, on_enter_anim, on_leave_anim, on_press_anim, on_release_anim},
|
||||
};
|
||||
use crate::{gui::panel::GuiPanel, state::AppState};
|
||||
use anyhow::bail;
|
||||
use slotmap::Key;
|
||||
use wgui::event::StyleSetRequest;
|
||||
use wgui::{
|
||||
event::EventListenerKind,
|
||||
layout::LayoutTask,
|
||||
parser::{Fetchable, TemplateParams},
|
||||
taffy::Display,
|
||||
widget::{EventResult, rectangle::WidgetRectangle},
|
||||
};
|
||||
|
||||
/// 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(slot) = panel.state.swipe_candidate_slot.as_mut()
|
||||
&& let Some(candidates) = slot.take()
|
||||
{
|
||||
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
|
||||
}),
|
||||
));
|
||||
}
|
||||
|
|
@ -0,0 +1,411 @@
|
|||
use super::layout::KeyCapType;
|
||||
use crate::state::AppState;
|
||||
use crate::subsystem::clipboard;
|
||||
use crate::subsystem::clipboard::ClipboardProvider;
|
||||
use crate::subsystem::hid::{CTRL, KeyModifier, SHIFT, VirtualKey};
|
||||
use crate::subsystem::input::InputFocus;
|
||||
use anyhow::bail;
|
||||
use glam::Vec2;
|
||||
use std::mem;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::mpsc::{Sender, channel};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::thread::{self, JoinHandle};
|
||||
use std::time::Instant;
|
||||
use super_swipe_type::SwipePoint;
|
||||
use super_swipe_type::keyboard_manager::QwertyKeyboardGrid;
|
||||
use super_swipe_type::swipe_orchestrator::SwipeOrchestrator;
|
||||
use wgui::event::{DeviceBitmask, MouseButtonIndex};
|
||||
use wgui::log::LogErr;
|
||||
|
||||
const PREDICTION_SUGGESTION_COUNT: usize = 5;
|
||||
|
||||
enum PredictionTask {
|
||||
Predict {
|
||||
swipe: Vec<SwipePoint>,
|
||||
last_word: Option<String>,
|
||||
},
|
||||
Shutdown,
|
||||
}
|
||||
#[derive(Clone)]
|
||||
pub struct PredictionSlot(Arc<PredictionSlotInner>);
|
||||
|
||||
struct PredictionSlotInner {
|
||||
present: AtomicBool,
|
||||
value: Mutex<Option<Vec<String>>>,
|
||||
}
|
||||
|
||||
impl PredictionSlot {
|
||||
pub fn new() -> Self {
|
||||
Self(Arc::new(PredictionSlotInner {
|
||||
present: AtomicBool::new(false),
|
||||
value: Mutex::new(None),
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn set(&self, value: Option<Vec<String>>) {
|
||||
*self.0.value.lock().unwrap() = value;
|
||||
self.0.present.store(true, Ordering::Release);
|
||||
}
|
||||
|
||||
/// Returns the latest pending value, if any, and clears the slot.
|
||||
/// `Some(Some(words))` is a fresh prediction, `Some(None)` is an explicit
|
||||
/// clear (mirrors the previous `send(None)`), `None` means nothing pending.
|
||||
pub fn take(&self) -> Option<Option<Vec<String>>> {
|
||||
if !self.0.present.swap(false, Ordering::Acquire) {
|
||||
return None;
|
||||
}
|
||||
let value = self.0.value.lock().unwrap().take();
|
||||
Some(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for PredictionSlot {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct SwipeTypingManager {
|
||||
keyboard_gird: QwertyKeyboardGrid,
|
||||
current_swipe: Vec<SwipePoint>,
|
||||
prediction_slot: PredictionSlot,
|
||||
prediction_task_sender: Sender<PredictionTask>,
|
||||
worker_thread: Option<JoinHandle<()>>,
|
||||
swipe_start_time: Option<Instant>,
|
||||
clipboard: Option<Box<dyn ClipboardProvider>>,
|
||||
swipe_left_first_key: bool,
|
||||
first_swipe_char: char,
|
||||
current_swipe_device: Option<DeviceBitmask>,
|
||||
current_swipe_mouse_button_index: Option<MouseButtonIndex>,
|
||||
last_swiped_word: Option<String>,
|
||||
}
|
||||
|
||||
impl SwipeTypingManager {
|
||||
pub fn select_alternate_prediction(
|
||||
&mut self,
|
||||
word: &String,
|
||||
app: &mut AppState,
|
||||
original_keyboard_mods: KeyModifier,
|
||||
) {
|
||||
Self::undo(app, original_keyboard_mods);
|
||||
self.select_word(word, app, original_keyboard_mods);
|
||||
}
|
||||
|
||||
pub fn select_word(
|
||||
&mut self,
|
||||
word: &String,
|
||||
app: &mut AppState,
|
||||
original_keyboard_mods: KeyModifier,
|
||||
) {
|
||||
self.last_swiped_word = Some(word.clone());
|
||||
let text_to_paste = format!("{word} ");
|
||||
|
||||
match app.hid_provider.get_input_focus() {
|
||||
InputFocus::PhysicalScreen => {
|
||||
if let Some(clipboard) = self.clipboard.as_mut() {
|
||||
clipboard.set_clipboard_utf8(&text_to_paste);
|
||||
Self::paste(app, original_keyboard_mods);
|
||||
}
|
||||
}
|
||||
InputFocus::WayVR => {
|
||||
if let Some(wvr_server) = app.wvr_server.as_mut() {
|
||||
wvr_server.set_clipboard_text(text_to_paste);
|
||||
Self::paste(app, original_keyboard_mods);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn undo(app: &mut AppState, original_keyboard_mods: KeyModifier) {
|
||||
app.hid_provider
|
||||
.set_modifiers_routed(app.wvr_server.as_mut(), CTRL);
|
||||
app.hid_provider
|
||||
.send_key_routed(app.wvr_server.as_mut(), VirtualKey::Z, true);
|
||||
app.hid_provider
|
||||
.send_key_routed(app.wvr_server.as_mut(), VirtualKey::Z, false);
|
||||
app.hid_provider
|
||||
.set_modifiers_routed(app.wvr_server.as_mut(), original_keyboard_mods);
|
||||
}
|
||||
|
||||
fn paste(app: &mut AppState, original_keyboard_mods: KeyModifier) {
|
||||
app.hid_provider
|
||||
.set_modifiers_routed(app.wvr_server.as_mut(), CTRL);
|
||||
app.hid_provider
|
||||
.send_key_routed(app.wvr_server.as_mut(), VirtualKey::V, true);
|
||||
app.hid_provider
|
||||
.send_key_routed(app.wvr_server.as_mut(), VirtualKey::V, false);
|
||||
app.hid_provider
|
||||
.set_modifiers_routed(app.wvr_server.as_mut(), original_keyboard_mods);
|
||||
}
|
||||
pub fn new(model_path: PathBuf) -> anyhow::Result<(SwipeTypingManager, PredictionSlot)> {
|
||||
let prediction_slot = PredictionSlot::new();
|
||||
let worker_slot = prediction_slot.clone();
|
||||
let (task_sender, task_receiver) = channel::<PredictionTask>();
|
||||
|
||||
// Spawn persistent worker thread
|
||||
let worker_thread = thread::spawn(move || {
|
||||
let mut swipe_engine = match SwipeOrchestrator::new_from_path(&model_path) {
|
||||
Ok(engine) => engine,
|
||||
Err(e) => {
|
||||
log::error!("Failed to initialize SwipeOrchestrator: {e}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
while let Ok(task) = task_receiver.recv() {
|
||||
match task {
|
||||
PredictionTask::Predict { swipe, last_word } => {
|
||||
match swipe_engine.predict(swipe, &last_word) {
|
||||
Ok(candidates) => {
|
||||
let words: Vec<String> = candidates
|
||||
.into_iter()
|
||||
.take(PREDICTION_SUGGESTION_COUNT)
|
||||
.map(|c| c.word)
|
||||
.collect();
|
||||
|
||||
worker_slot.set(Some(words));
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Prediction failed: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
PredictionTask::Shutdown => break,
|
||||
}
|
||||
}
|
||||
});
|
||||
let clipboard_provider: Option<Box<dyn ClipboardProvider>> = {
|
||||
#[cfg(feature = "wayland")]
|
||||
let wl = clipboard::wl::Provider::new()
|
||||
.log_err("Could not create Wayland clipboard provider")
|
||||
.ok()
|
||||
.map(|p| Box::new(p) as Box<dyn ClipboardProvider>);
|
||||
#[cfg(not(feature = "wayland"))]
|
||||
let wl = None;
|
||||
|
||||
#[cfg(feature = "x11")]
|
||||
let x11 = clipboard::x11::Provider::new()
|
||||
.log_err("Could not create X11 clipboard provider")
|
||||
.ok()
|
||||
.map(|p| Box::new(p) as Box<dyn ClipboardProvider>);
|
||||
#[cfg(not(feature = "x11"))]
|
||||
let x11 = None;
|
||||
|
||||
wl.or(x11)
|
||||
};
|
||||
Ok((
|
||||
Self {
|
||||
keyboard_gird: QwertyKeyboardGrid::new(),
|
||||
current_swipe: Vec::new(),
|
||||
prediction_slot: prediction_slot.clone(),
|
||||
prediction_task_sender: task_sender,
|
||||
worker_thread: Some(worker_thread),
|
||||
swipe_start_time: None,
|
||||
clipboard: clipboard_provider,
|
||||
swipe_left_first_key: false,
|
||||
first_swipe_char: char::default(),
|
||||
current_swipe_device: None,
|
||||
current_swipe_mouse_button_index: None,
|
||||
last_swiped_word: None,
|
||||
},
|
||||
prediction_slot,
|
||||
))
|
||||
}
|
||||
|
||||
pub fn predict(&mut self) -> anyhow::Result<()> {
|
||||
if self.is_current_swipe_empty() {
|
||||
bail!("nothing to predict");
|
||||
}
|
||||
|
||||
let current_swipe = mem::take(&mut self.current_swipe);
|
||||
let last_word = self.last_swiped_word.clone();
|
||||
self.reset_swipe();
|
||||
|
||||
self.prediction_task_sender.send(PredictionTask::Predict {
|
||||
swipe: current_swipe,
|
||||
last_word,
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn reset(&mut self) {
|
||||
self.reset_swipe();
|
||||
self.prediction_slot.set(None);
|
||||
self.last_swiped_word = None;
|
||||
}
|
||||
|
||||
fn reset_swipe(&mut self) {
|
||||
self.swipe_start_time = None;
|
||||
self.current_swipe = Vec::new();
|
||||
self.first_swipe_char = char::default();
|
||||
self.swipe_left_first_key = false;
|
||||
self.current_swipe_device = None;
|
||||
self.current_swipe_mouse_button_index = None;
|
||||
}
|
||||
|
||||
fn start_swipe(
|
||||
&mut self,
|
||||
key_label: char,
|
||||
device: DeviceBitmask,
|
||||
index: Option<MouseButtonIndex>,
|
||||
) -> Instant {
|
||||
let now = Instant::now();
|
||||
self.swipe_start_time = Some(now);
|
||||
self.first_swipe_char = key_label.to_ascii_lowercase();
|
||||
self.current_swipe_device = Some(device);
|
||||
self.current_swipe_mouse_button_index = index;
|
||||
now
|
||||
}
|
||||
|
||||
pub const fn did_swipe_leave_first_key(&self) -> bool {
|
||||
self.swipe_left_first_key
|
||||
}
|
||||
|
||||
pub const fn is_current_swipe_empty(&self) -> bool {
|
||||
self.current_swipe.is_empty()
|
||||
}
|
||||
|
||||
pub const fn current_swipe_mouse_button_index(&self) -> Option<MouseButtonIndex> {
|
||||
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
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if self.first_swipe_char != char::default()
|
||||
&& self.first_swipe_char != key_label.to_ascii_lowercase()
|
||||
{
|
||||
self.swipe_left_first_key = true;
|
||||
}
|
||||
|
||||
let key_pos = Vec2 {
|
||||
x: pos.x as f32,
|
||||
y: pos.y as f32,
|
||||
};
|
||||
|
||||
let start_time = match self.swipe_start_time {
|
||||
Some(time) => time,
|
||||
None => self.start_swipe(key_label, device, index),
|
||||
};
|
||||
|
||||
let within_key_pos_from_center = Vec2 {
|
||||
x: within_key_pos_normalized.x - 0.5,
|
||||
y: within_key_pos_normalized.y - 0.5,
|
||||
};
|
||||
let key_dimensions = Vec2 {
|
||||
x: QwertyKeyboardGrid::get_key_width() as f32,
|
||||
y: QwertyKeyboardGrid::get_key_height() as f32,
|
||||
};
|
||||
|
||||
let point = within_key_pos_from_center * key_dimensions + key_pos;
|
||||
let duration = Instant::now().duration_since(start_time).mul_f32(0.8); // multiply by .8 because library is trained on mobile swipes which happen on a smaller keyboard and are faster
|
||||
self.current_swipe
|
||||
.push(SwipePoint::new(point.x.into(), point.y.into(), duration));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for SwipeTypingManager {
|
||||
fn drop(&mut self) {
|
||||
let _ = self.prediction_task_sender.send(PredictionTask::Shutdown);
|
||||
if let Some(handle) = self.worker_thread.take() {
|
||||
let _ = handle.join();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -192,6 +192,10 @@ impl AppState {
|
|||
{
|
||||
feats.whisper = true;
|
||||
}
|
||||
#[cfg(feature = "swipe-to-type")]
|
||||
{
|
||||
feats.swipe_to_type = true;
|
||||
}
|
||||
|
||||
let mut app_state = Self {
|
||||
tasks,
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ pub mod hid;
|
|||
pub mod input;
|
||||
pub mod notifications;
|
||||
|
||||
#[cfg(feature = "whisper")]
|
||||
#[cfg(any(feature = "whisper", feature = "swipe-to-type"))]
|
||||
pub mod clipboard;
|
||||
|
||||
#[cfg(feature = "whisper")]
|
||||
|
|
|
|||
|
|
@ -66,6 +66,7 @@ struct State {
|
|||
checked: bool,
|
||||
hovered: bool,
|
||||
down: bool,
|
||||
disabled: bool,
|
||||
on_toggle: Option<CheckboxToggleCallback>,
|
||||
self_ref: Weak<ComponentCheckbox>,
|
||||
active_tooltip: Option<Rc<ComponentTooltip>>,
|
||||
|
|
@ -99,6 +100,32 @@ pub struct ComponentCheckbox {
|
|||
const COLOR_UNCHECKED: WguiColor = WguiColor::Raw(drawing::Color::new(0., 0., 0., 0.));
|
||||
const COLOR_HOVERED: WguiColor = WguiColorName::Tertiary.to_wgui_color();
|
||||
|
||||
fn disabled_color() -> WguiColor {
|
||||
WguiColor::from(WguiColorName::OnBackground).with_alpha(0.35)
|
||||
}
|
||||
|
||||
/// Color for the inner checkbox box, for any checked/hover/disabled combination.
|
||||
fn box_fill_color(checked: bool, hovered: bool, disabled: bool, checked_color: WguiColor) -> WguiColor {
|
||||
if disabled {
|
||||
if checked { disabled_color() } else { COLOR_UNCHECKED }
|
||||
} else if checked {
|
||||
if hovered { COLOR_HOVERED } else { checked_color }
|
||||
} else {
|
||||
COLOR_UNCHECKED
|
||||
}
|
||||
}
|
||||
|
||||
/// Color for the checkbox border, for any hover/press/disabled combination.
|
||||
fn box_border_color(hovered: bool, down: bool, disabled: bool) -> WguiColor {
|
||||
if disabled {
|
||||
disabled_color()
|
||||
} else if hovered || down {
|
||||
COLOR_HOVERED
|
||||
} else {
|
||||
WguiColorName::OnBackground.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl ComponentTrait for ComponentCheckbox {
|
||||
fn base(&self) -> &ComponentBase {
|
||||
&self.base
|
||||
|
|
@ -120,13 +147,9 @@ impl ComponentTrait for ComponentCheckbox {
|
|||
}
|
||||
}
|
||||
|
||||
fn set_box_checked(widgets: &layout::WidgetMap, data: &Data, checked: bool, hovered: bool) {
|
||||
fn set_box_checked(widgets: &layout::WidgetMap, data: &Data, checked: bool, hovered: bool, disabled: bool) {
|
||||
widgets.call(data.id_inner_box, |rect: &mut WidgetRectangle| {
|
||||
rect.params.color = if checked {
|
||||
if hovered { COLOR_HOVERED } else { data.color_checked }
|
||||
} else {
|
||||
COLOR_UNCHECKED
|
||||
}
|
||||
rect.params.color = box_fill_color(checked, hovered, disabled, data.color_checked);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -141,6 +164,7 @@ impl ComponentCheckbox {
|
|||
|
||||
pub fn set_checked(&self, common: &mut CallbackDataCommon, checked: bool) {
|
||||
let hovered;
|
||||
let disabled;
|
||||
{
|
||||
let mut state = self.state.borrow_mut();
|
||||
if state.checked == checked {
|
||||
|
|
@ -148,8 +172,9 @@ impl ComponentCheckbox {
|
|||
}
|
||||
state.checked = checked;
|
||||
hovered = state.hovered;
|
||||
disabled = state.disabled;
|
||||
}
|
||||
set_box_checked(&common.state.widgets, &self.data, checked, hovered);
|
||||
set_box_checked(&common.state.widgets, &self.data, checked, hovered, disabled);
|
||||
common.alterables.mark_redraw();
|
||||
}
|
||||
|
||||
|
|
@ -170,39 +195,88 @@ impl ComponentCheckbox {
|
|||
pub fn on_toggle(&self, func: CheckboxToggleCallback) {
|
||||
self.state.borrow_mut().on_toggle = Some(func);
|
||||
}
|
||||
|
||||
/// Enable or disable user interaction with the checkbox and dim its appearance.
|
||||
pub fn set_disabled(&self, common: &mut CallbackDataCommon, disabled: bool) {
|
||||
let checked;
|
||||
let hovered;
|
||||
let down;
|
||||
{
|
||||
let mut state = self.state.borrow_mut();
|
||||
if state.disabled == disabled {
|
||||
return;
|
||||
}
|
||||
state.disabled = disabled;
|
||||
checked = state.checked;
|
||||
hovered = state.hovered;
|
||||
down = state.down;
|
||||
}
|
||||
|
||||
set_box_checked(&common.state.widgets, &self.data, checked, hovered, disabled);
|
||||
|
||||
common
|
||||
.state
|
||||
.widgets
|
||||
.call(self.data.id_outer_box, |rect: &mut WidgetRectangle| {
|
||||
rect.params.border_color = box_border_color(hovered, down, disabled);
|
||||
});
|
||||
|
||||
if let Some(mut label) = common.state.widgets.get_as::<WidgetLabel>(self.data.id_label) {
|
||||
let color = if disabled {
|
||||
disabled_color()
|
||||
} else {
|
||||
WguiColorName::OnBackground.into()
|
||||
};
|
||||
label.set_color(common, color, true);
|
||||
}
|
||||
|
||||
common.alterables.mark_redraw();
|
||||
}
|
||||
|
||||
pub fn get_disabled(&self) -> bool {
|
||||
self.state.borrow().disabled
|
||||
}
|
||||
}
|
||||
|
||||
fn anim_hover(anim_data: &mut crate::animation::CallbackData<'_>, pos: f32, _pressed: bool) {
|
||||
fn anim_hover(anim_data: &mut crate::animation::CallbackData<'_>, pos: f32, pressed: bool, disabled: bool) {
|
||||
let rect = anim_data.obj.as_any_mut().downcast_mut::<WidgetRectangle>().unwrap();
|
||||
rect.params.border = 2.0;
|
||||
rect.params.border_color = if pos > 0.0 {
|
||||
COLOR_HOVERED
|
||||
} else {
|
||||
WguiColorName::OnBackground.into()
|
||||
};
|
||||
rect.params.border_color = box_border_color(pos > 0.0, pressed, disabled);
|
||||
}
|
||||
|
||||
fn anim_hover_in(state: &Rc<RefCell<State>>, data: &Rc<Data>, anim_mult: f32) -> Animation {
|
||||
let down = state.borrow().down;
|
||||
let down;
|
||||
let disabled;
|
||||
{
|
||||
let state = state.borrow();
|
||||
down = state.down;
|
||||
disabled = state.disabled;
|
||||
}
|
||||
Animation::new(
|
||||
data.id_outer_box,
|
||||
(5. * anim_mult) as _,
|
||||
AnimationEasing::OutQuad,
|
||||
Box::new(move |common, anim_data| {
|
||||
anim_hover(anim_data, anim_data.pos, down);
|
||||
anim_hover(anim_data, anim_data.pos, down, disabled);
|
||||
common.alterables.mark_redraw();
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
fn anim_hover_out(state: &Rc<RefCell<State>>, data: &Rc<Data>, anim_mult: f32) -> Animation {
|
||||
let down = state.borrow().down;
|
||||
let down;
|
||||
let disabled;
|
||||
{
|
||||
let state = state.borrow();
|
||||
down = state.down;
|
||||
disabled = state.disabled;
|
||||
}
|
||||
Animation::new(
|
||||
data.id_outer_box,
|
||||
(8. * anim_mult) as _,
|
||||
AnimationEasing::OutQuad,
|
||||
Box::new(move |common, anim_data| {
|
||||
anim_hover(anim_data, 1.0 - anim_data.pos, down);
|
||||
anim_hover(anim_data, 1.0 - anim_data.pos, down, disabled);
|
||||
common.alterables.mark_redraw();
|
||||
}),
|
||||
)
|
||||
|
|
@ -218,26 +292,31 @@ fn register_event_mouse_enter(
|
|||
listeners.register(
|
||||
EventListenerKind::MouseEnter,
|
||||
Box::new(move |common, _event_data, (), ()| {
|
||||
common.alterables.trigger_haptics();
|
||||
common.alterables.animate(anim_hover_in(&state, &data, anim_mult));
|
||||
let checked;
|
||||
let disabled;
|
||||
{
|
||||
let mut state = state.borrow_mut();
|
||||
checked = state.checked;
|
||||
disabled = state.disabled;
|
||||
state.hovered = true;
|
||||
}
|
||||
|
||||
if !disabled {
|
||||
common.alterables.trigger_haptics();
|
||||
common.alterables.animate(anim_hover_in(&state, &data, anim_mult));
|
||||
|
||||
if checked {
|
||||
common
|
||||
.state
|
||||
.widgets
|
||||
.call(data.id_inner_box, |rect: &mut WidgetRectangle| {
|
||||
rect.params.color = box_fill_color(checked, true, false, data.color_checked);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
ComponentTooltip::register_hover_in(common, &tooltip_info, data.id_container, state.clone());
|
||||
|
||||
let checked = {
|
||||
let mut state = state.borrow_mut();
|
||||
state.hovered = true;
|
||||
state.checked
|
||||
};
|
||||
|
||||
if checked {
|
||||
common
|
||||
.state
|
||||
.widgets
|
||||
.call(data.id_inner_box, |rect: &mut WidgetRectangle| {
|
||||
rect.params.color = COLOR_HOVERED;
|
||||
});
|
||||
}
|
||||
|
||||
Ok(EventResult::Pass)
|
||||
}),
|
||||
)
|
||||
|
|
@ -252,23 +331,28 @@ fn register_event_mouse_leave(
|
|||
listeners.register(
|
||||
EventListenerKind::MouseLeave,
|
||||
Box::new(move |common, _event_data, (), ()| {
|
||||
common.alterables.trigger_haptics();
|
||||
common.alterables.animate(anim_hover_out(&state, &data, anim_mult));
|
||||
|
||||
let checked = {
|
||||
let checked;
|
||||
let disabled;
|
||||
{
|
||||
let mut state = state.borrow_mut();
|
||||
checked = state.checked;
|
||||
disabled = state.disabled;
|
||||
state.hovered = false;
|
||||
state.active_tooltip = None;
|
||||
state.checked
|
||||
};
|
||||
}
|
||||
|
||||
if checked {
|
||||
common
|
||||
.state
|
||||
.widgets
|
||||
.call(data.id_inner_box, |rect: &mut WidgetRectangle| {
|
||||
rect.params.color = data.color_checked;
|
||||
});
|
||||
if !disabled {
|
||||
common.alterables.trigger_haptics();
|
||||
common.alterables.animate(anim_hover_out(&state, &data, anim_mult));
|
||||
|
||||
if checked {
|
||||
common
|
||||
.state
|
||||
.widgets
|
||||
.call(data.id_inner_box, |rect: &mut WidgetRectangle| {
|
||||
rect.params.color = box_fill_color(checked, false, false, data.color_checked);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Ok(EventResult::Pass)
|
||||
|
|
@ -303,18 +387,14 @@ fn register_event_mouse_press(
|
|||
.widgets
|
||||
.call(data.id_outer_box, |rect: &mut WidgetRectangle| {
|
||||
rect.params.border = 2.0;
|
||||
rect.params.border_color = if pressed_hovered {
|
||||
COLOR_HOVERED
|
||||
} else {
|
||||
WguiColorName::OnBackground.into()
|
||||
};
|
||||
rect.params.border_color = box_border_color(pressed_hovered, false, state.disabled);
|
||||
});
|
||||
|
||||
common.alterables.trigger_haptics();
|
||||
common.alterables.mark_redraw();
|
||||
common.alterables.unfocus();
|
||||
|
||||
if state.hovered {
|
||||
if state.hovered && !state.disabled {
|
||||
state.down = true;
|
||||
Ok(EventResult::Consumed)
|
||||
} else {
|
||||
|
|
@ -341,17 +421,13 @@ fn register_event_mouse_release(
|
|||
.widgets
|
||||
.call(data.id_outer_box, |rect: &mut WidgetRectangle| {
|
||||
rect.params.border = 2.0;
|
||||
rect.params.border_color = if released_hovered {
|
||||
COLOR_HOVERED
|
||||
} else {
|
||||
WguiColorName::OnBackground.into()
|
||||
};
|
||||
rect.params.border_color = box_border_color(released_hovered, was_down, state.disabled);
|
||||
});
|
||||
|
||||
common.alterables.trigger_haptics();
|
||||
common.alterables.mark_redraw();
|
||||
|
||||
if was_down {
|
||||
if !state.disabled && was_down {
|
||||
state.down = false;
|
||||
|
||||
if let Some(self_ref) = state.self_ref.upgrade()
|
||||
|
|
@ -369,7 +445,13 @@ fn register_event_mouse_release(
|
|||
});
|
||||
}
|
||||
|
||||
set_box_checked(&common.state.widgets, &data, state.checked, state.hovered);
|
||||
set_box_checked(
|
||||
&common.state.widgets,
|
||||
&data,
|
||||
state.checked,
|
||||
state.hovered,
|
||||
state.disabled,
|
||||
);
|
||||
if state.hovered
|
||||
&& let Some(on_toggle) = &state.on_toggle
|
||||
{
|
||||
|
|
@ -500,6 +582,7 @@ pub fn construct(ess: &mut ConstructEssentials, params: Params) -> anyhow::Resul
|
|||
checked: params.checked,
|
||||
down: false,
|
||||
hovered: false,
|
||||
disabled: false,
|
||||
on_toggle: None,
|
||||
self_ref: Weak::new(),
|
||||
active_tooltip: None,
|
||||
|
|
|
|||
|
|
@ -253,6 +253,10 @@ impl CallbackMetadata {
|
|||
let mouse_pos_abs = self.get_mouse_pos_absolute()?;
|
||||
Some(mouse_pos_abs - transform_stack.get().abs_pos)
|
||||
}
|
||||
pub fn get_mouse_pos_normalized(&self, transform_stack: &TransformStack) -> Option<Vec2> {
|
||||
let pos_relative = self.get_mouse_pos_relative(transform_stack)?;
|
||||
Some(pos_relative / transform_stack.parent().raw_dim)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
|
|
|
|||
|
|
@ -532,6 +532,9 @@ pub struct GeneralConfig {
|
|||
#[serde(default = "def_one")]
|
||||
pub grid_opacity: f32,
|
||||
|
||||
#[serde(default)]
|
||||
pub keyboard_swipe_to_type_enabled: bool,
|
||||
|
||||
#[serde(default)]
|
||||
pub chroma_key_params: ChromaKeyParams,
|
||||
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@ pub struct InterfaceFeats {
|
|||
pub monado: bool,
|
||||
pub passthru: bool,
|
||||
pub whisper: bool,
|
||||
pub swipe_to_type: bool,
|
||||
}
|
||||
|
||||
impl InterfaceFeats {
|
||||
|
|
@ -62,6 +63,7 @@ impl InterfaceFeats {
|
|||
monado: false,
|
||||
passthru: false,
|
||||
whisper: false,
|
||||
swipe_to_type: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -240,6 +240,7 @@ impl DashInterface<()> for DashInterfaceEmulated {
|
|||
monado: true,
|
||||
passthru: false,
|
||||
whisper: true,
|
||||
swipe_to_type: true,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue