This commit is contained in:
Diamond White 2026-08-16 05:56:16 +00:00 committed by GitHub
commit d874ff41fa
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
33 changed files with 2699 additions and 152 deletions

View File

@ -1,6 +1,7 @@
name: Build AppImage
on:
workflow_dispatch:
push:
branches:
- 'main'

10
.idea/.gitignore vendored Normal file
View File

@ -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/

1198
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -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",

View File

@ -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",

View File

@ -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",

View File

@ -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",

View File

@ -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": "デフォルトで互換モードでアプリを起動",

View File

@ -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",

View File

@ -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": "默认以兼容模式运行应用程序",

View File

@ -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"),

View File

@ -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(())
}

View File

@ -0,0 +1,5 @@
pub struct DownloadableFile {
pub file_name: &'static str,
pub display_name: &'static str,
pub url: &'static str,
}

View File

@ -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;

View File

@ -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(())
}

View File

@ -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)
}

View File

@ -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"]

View File

@ -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>

View File

@ -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

View File

@ -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)

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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 } => {

View File

@ -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
}),
));
}

View File

@ -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();
}
}
}

View File

@ -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,

View File

@ -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")]

View File

@ -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,

View File

@ -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)]

View File

@ -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,

View File

@ -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,
}
}
}

View File

@ -240,6 +240,7 @@ impl DashInterface<()> for DashInterfaceEmulated {
monado: true,
passthru: false,
whisper: true,
swipe_to_type: true,
}
}