diff --git a/dash-frontend/assets/lang/en.json b/dash-frontend/assets/lang/en.json index 0629f776..d193e624 100644 --- a/dash-frontend/assets/lang/en.json +++ b/dash-frontend/assets/lang/en.json @@ -226,6 +226,15 @@ }, "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_MODELS": "The swipe-to-type model needs to be downloaded.\nDownload them now?", + "MODELS_DOWNLOADED": "Model downloaded", + "MODELS_NOT_DOWNLOADED": "Model not downloaded", + "REMOVE_MODELS": "Remove swipe to type model", + "DOWNLOAD_MODELS": "Download swipe to type model ({} total)" + }, + "SWIPE_TYPE_MODELS": "Swipe-to-type models", + "SWIPE_TYPE_MODELS_HELP": "Model files used by swipe-to-type.\nAll of them are required.", "GRID_OPACITY": "Floor grid opacity", "GRID_OPACITY_HELP": "Opacity of the floor grid when the skybox is enabled", "XWAYLAND_BY_DEFAULT": "Run apps in Compatibility mode by default", diff --git a/dash-frontend/src/tab/settings/mod.rs b/dash-frontend/src/tab/settings/mod.rs index bff41c0c..04e8f39c 100644 --- a/dash-frontend/src/tab/settings/mod.rs +++ b/dash-frontend/src/tab/settings/mod.rs @@ -83,6 +83,7 @@ pub(crate) enum Task { SavePlayspaceCenter, SetTab(TabNameEnum), SettingUpdated(SettingType), + SwipeTypeAction(Rc), 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, + _layout: &mut Layout, + _state: &mut ParserState, + ) -> anyhow::Result<()> { + Ok(()) + } + fn context_menu_custom( &mut self, _action: Rc, @@ -254,6 +266,22 @@ impl Tab for TabSettings { _ => { /* 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 = 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); + } + } + } } } @@ -325,7 +353,6 @@ pub(crate) enum SettingType { InvertScrollDirectionY, KeyboardMiddleClick, KeyboardSoundEnabled, - KeyboardSwipeToTypeEnabled, Language, LeftHandedMouse, LongPressDuration, @@ -400,7 +427,6 @@ impl SettingType { Self::UsePassthrough => &mut config.use_passthrough, Self::UseSkybox => &mut config.use_skybox, Self::XwaylandByDefault => &mut config.xwayland_by_default, - Self::KeyboardSwipeToTypeEnabled => &mut config.keyboard_swipe_to_type_enabled, _ => panic!("Requested bool for non-bool SettingType"), } } @@ -524,7 +550,6 @@ 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"), @@ -576,7 +601,6 @@ 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"), diff --git a/dash-frontend/src/tab/settings/tab_features.rs b/dash-frontend/src/tab/settings/tab_features.rs index a73dafbb..e6996678 100644 --- a/dash-frontend/src/tab/settings/tab_features.rs +++ b/dash-frontend/src/tab/settings/tab_features.rs @@ -21,6 +21,10 @@ use crate::{ }, util::{ popup_manager::PopupHolder, + swipe_type::{ + SWIPE_TYPE_MODELS, SwipeTypeModel, swipe_type_all_models_downloaded, + swipe_type_delete_all_models, swipe_type_model_path, + }, whisper::{ WHISPER_MODELS, WhisperModel, whisper_any_models_downloaded, whisper_delete_all_models, whisper_model_from_name, whisper_model_path, @@ -35,6 +39,10 @@ enum Task { WhisperRemoveUnused, WhisperDownload(&'static WhisperModel), WhisperDownloadDone, + SwipeTypeDownloadClosed, + SwipeTypeRemoveAll, + SwipeTypeDownloadAll, + SwipeTypeDownloadDone, CloseDialog, ReloadTab, } @@ -46,7 +54,8 @@ pub struct State { globals: WguiGlobals, tasks: Tasks, parent_tasks: Tasks, - pending_download: Option<&'static WhisperModel>, + pending_whisper_download: Option<&'static WhisperModel>, + pending_swipe_download: Option<&'static [SwipeTypeModel]>, } impl SettingsTab for State { @@ -57,7 +66,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() { if !whisper_model_path(model.file_name).exists() { // download failed, set to selection to none par.general_config.whisper_model = "".into(); @@ -71,11 +80,11 @@ 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.pending_whisper_download = Some(model); self.show_whisper_download_dialog(model, par.executor.clone()); } 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 +92,28 @@ impl SettingsTab for State { self.parent_tasks.push(ParentTask::SetTab(TabNameEnum::Features)); } } + Task::SwipeTypeDownloadClosed => { + self.pending_swipe_download = None; + if !swipe_type_all_models_downloaded().unwrap_or_default() { + par.general_config.swipe_type_models_downloaded = 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"); + } + Task::SwipeTypeDownloadAll => { + self.pending_swipe_download = Some(SWIPE_TYPE_MODELS); + self.show_swipe_type_download_dialog(SWIPE_TYPE_MODELS, par.executor.clone()); + } + Task::SwipeTypeDownloadDone => { + if let Some(_) = self.pending_swipe_download.take() { + par.general_config.swipe_type_models_downloaded = true; + 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 +166,27 @@ impl SettingsTab for State { Ok(()) } + + fn push_task_string( + &mut self, + action: &str, + _config: &mut GeneralConfig, + _change_kind: &mut Option, + _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,11 +201,14 @@ impl State { whisper_models_dropdown(par.mp, c)?; } + if par.feats.swipe_to_type { + swipe_type_models_button(par.mp, c)?; + } + options_checkbox(par.mp, c, SettingType::NotificationsEnabled)?; options_checkbox(par.mp, c, SettingType::NotificationsSoundEnabled)?; options_checkbox(par.mp, c, SettingType::KeyboardSoundEnabled)?; - options_checkbox(par.mp, c, SettingType::KeyboardSwipeToTypeEnabled)?; - + if !par.feats.openxr || par.feats.monado { // monado or openvr options_checkbox(par.mp, c, SettingType::BlockGameInput)?; @@ -180,7 +235,8 @@ 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, }) } @@ -199,11 +255,16 @@ impl State { ); } + fn show_swipe_type_download_dialog(&mut self, models: &[SwipeTypeModel], executor: AsyncExecutor) { + //TODO: create views::download_files and use that + unimplemented!() + } + fn show_whisper_model_dialog_box_download(&mut self, model: &'static WhisperModel) -> 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( @@ -241,6 +302,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_MODELS"), + 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"; @@ -357,3 +457,45 @@ 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 = swipe_type_all_models_downloaded().unwrap_or_default(); + let (translation, icon, action) = if all_downloaded { + ( + "APP_SETTINGS.SWIPE_TYPE.REMOVE_MODELS", + "dashboard/trash.svg", + "swipe_type_remove", + ) + } else { + ( + "APP_SETTINGS.SWIPE_TYPE.DOWNLOAD_MODELS;5.0 MiB", + "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::(&id)?; + btn.on_click(Rc::new({ + let parent_tasks = mp.tasks.clone(); + let action = Rc::::from(action); + move |_common, _e: ButtonClickEvent| { + parent_tasks.push(ParentTask::SwipeTypeAction(action.clone())); + Ok(()) + } + })); + + Ok(()) +} \ No newline at end of file diff --git a/dash-frontend/src/util/mod.rs b/dash-frontend/src/util/mod.rs index 7eda5e07..f898e589 100644 --- a/dash-frontend/src/util/mod.rs +++ b/dash-frontend/src/util/mod.rs @@ -7,3 +7,4 @@ pub mod steam_utils; pub mod toast_manager; pub mod wgui_simple; pub mod whisper; +pub mod swipe_type; diff --git a/dash-frontend/src/util/swipe_type.rs b/dash-frontend/src/util/swipe_type.rs new file mode 100644 index 00000000..e0cfdeb4 --- /dev/null +++ b/dash-frontend/src/util/swipe_type.rs @@ -0,0 +1,70 @@ +use std::{fs, io, path::PathBuf}; + +use wlx_common::data_dir; + +/// The set of model files required by the swipe-to-type engine. +/// These correspond to the assets that `super-swipe-type` currently +/// downloads via `cached_path` in `SwipeOrchestrator::new()`. +pub struct SwipeTypeModel { + pub file_name: &'static str, + pub url: &'static str, +} + +pub const SWIPE_TYPE_MODELS: &[SwipeTypeModel] = &[ + SwipeTypeModel { + file_name: "swipe_encoder_android.onnx", + url: "https://wayvr.org/files/swipe_type/swipe_encoder_android.onnx", + }, + SwipeTypeModel { + file_name: "swipe_decoder_android.onnx", + url: "https://wayvr.org/files/swipe_type/swipe_decoder_android.onnx", + }, + SwipeTypeModel { + file_name: "en_wordlist.fst", + url: "https://wayvr.org/files/swipe_type/en_wordlist.fst", + }, + SwipeTypeModel { + file_name: "en_bigrams.fst", + url: "https://wayvr.org/files/swipe_type/en_bigrams.fst", + }, +]; + +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) +} + +/// Returns true when every required model file is present on disk. +pub fn swipe_type_all_models_downloaded() -> io::Result { + let path = swipe_type_model_folder(); + if !path.is_dir() { + return Ok(false); + } + for model in SWIPE_TYPE_MODELS { + if !path.join(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(()) +} \ No newline at end of file diff --git a/wayvr/src/overlays/dashboard.rs b/wayvr/src/overlays/dashboard.rs index fe972925..63c84a92 100644 --- a/wayvr/src/overlays/dashboard.rs +++ b/wayvr/src/overlays/dashboard.rs @@ -588,6 +588,10 @@ impl DashInterface for DashInterfaceLive { whisper: true, #[cfg(not(feature = "whisper"))] whisper: false, + #[cfg(feature = "swipe-to-type")] + swipe_to_type: true, + #[cfg(not(feature = "swipe-to-type"))] + swipe_to_type: false, } } diff --git a/wlx-common/src/dash_interface.rs b/wlx-common/src/dash_interface.rs index 420931a4..dd7526b6 100644 --- a/wlx-common/src/dash_interface.rs +++ b/wlx-common/src/dash_interface.rs @@ -50,6 +50,7 @@ pub struct InterfaceFeats { pub openxr: bool, pub monado: bool, pub whisper: bool, + pub swipe_to_type: bool, } pub trait DashInterface { diff --git a/wlx-common/src/dash_interface_emulated.rs b/wlx-common/src/dash_interface_emulated.rs index 9a4ff1a0..6d5f2aa7 100644 --- a/wlx-common/src/dash_interface_emulated.rs +++ b/wlx-common/src/dash_interface_emulated.rs @@ -238,6 +238,7 @@ impl DashInterface<()> for DashInterfaceEmulated { openxr: true, monado: true, whisper: true, + swipe_to_type: true, } }