start dashboard swipe type download button

This commit is contained in:
Diamond White 2026-08-14 15:56:04 -05:00
parent 18d3dc5e93
commit 404b5ea659
8 changed files with 264 additions and 12 deletions

View File

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

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

View File

@ -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<Task>,
parent_tasks: Tasks<ParentTask>,
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<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,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::<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(())
}

View File

@ -7,3 +7,4 @@ pub mod steam_utils;
pub mod toast_manager;
pub mod wgui_simple;
pub mod whisper;
pub mod swipe_type;

View File

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

View File

@ -588,6 +588,10 @@ impl DashInterface<AppState> 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,
}
}

View File

@ -50,6 +50,7 @@ pub struct InterfaceFeats {
pub openxr: bool,
pub monado: bool,
pub whisper: bool,
pub swipe_to_type: bool,
}
pub trait DashInterface<T> {

View File

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