Compare commits

...
28 changed files with 500 additions and 182 deletions

No files matched your search

+12
View File
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="32"
height="32"
viewBox="0 0 24 24"
version="1.1"
id="svg1"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs id="defs1" />
<path fill="currentColor" d="m 10,12 c 0,-1.104569 0.895431,-2 2,-2 1.104569,0 2,0.895431 2,2 0,1.104569 -0.895431,2 -2,2 -1.104569,0 -2,-0.895431 -2,-2" id="path1" />
</svg>

After

Width:  |  Height:  |  Size: 422 B

+13
View File
@@ -6,6 +6,16 @@
<var key="side_button_size" value="48" />
</vars>
<blueprint name="context_menu_power_options">
<context_menu>
<cell translation="APP_SETTINGS.RESTART_WAYVR" action="restart" sprite_src_builtin="@/dashboard/refresh.svg"/>
<cell translation="APP_SETTINGS.CLOSE_WAYVR" action="close" sprite_src_builtin="@/dashboard/close.svg"/>
<separator/>
<cell translation="POWER_OPTIONS.SUSPEND" action="suspend" sprite_src_builtin="@/dashboard/sleep.svg"/>
<cell translation="POWER_OPTIONS.SHUTDOWN" action="shutdown" sprite_src_builtin="@/dashboard/power.svg"/>
</context_menu>
</blueprint>
<template name="SideButton">
<Button
id="${id}"
@@ -136,6 +146,9 @@
<!-- Left bottom side -->
<div margin_left="8">
<Button id="btn_power" color="background(transparent)" border_color="background(transparent)" tooltip="POWER_OPTIONS.TITLE" tooltip_side="top">
<sprite src_builtin="@/dashboard/power.svg" width="24" height="24" margin="8" />
</Button>
<Button id="btn_audio" color="background(transparent)" border_color="background(transparent)" tooltip="AUDIO.VOLUME" tooltip_side="top">
<sprite src_builtin="@/dashboard/volume.svg" width="24" height="24" margin="8" />
</Button>
+4 -1
View File
@@ -1,6 +1,9 @@
<layout version="2">
<template name="DialogBoxButton">
<Button id="btn" translation="CLOSE_WINDOW" align_self="start" sprite_src_builtin="${icon}"/>
<Button id="btn" translation="CLOSE_WINDOW" align_self="start" sprite_src_builtin="${icon}"
text_size="16"
padding_left="16" padding_right="16"
padding_top="8" padding_bottom="8"/>
</template>
<elements>
+12 -4
View File
@@ -197,8 +197,6 @@
"SAVE_PLAYSPACE_CENTER_HELP": "Nutze aktuellen Playspace für die Playspace-Reset-Belegung.",
"RESET_PLAYSPACE_CENTER": "Playspace-Zentrum zurücksetzen",
"RESET_PLAYSPACE_CENTER_HELP": "Vorheriges Playspace-Zentrum zurücksetzen.",
"RESTART_SOFTWARE": "Software neu starten",
"RESTART_SOFTWARE_HELP": "Einstellungen übernehmen, die einen Neustart erfordern",
"ROUND_MULTIPLIER": "Abrundung der UI-Kanten",
"SCREEN_RENDER_DOWN": "Bildschirm mit niedrigerer Auflösung rendern",
"SCREEN_RENDER_DOWN_HELP": "Hilft gegen Treppeneffekte bei hochauflösenden Bildschirmen",
@@ -263,7 +261,9 @@
"TITLE": "Diagnose",
"ENABLE_REBAR": "Aktiviere resizable bar in den BIOS-Einstellungen für optimale VR-Performance"
},
"TUTORIAL": "Tutorial"
"TUTORIAL": "Tutorial",
"RESTART_WAYVR": "WayVR neu starten",
"CLOSE_WAYVR": "WayVR schließen"
},
"APPLICATION_LAUNCHER": "Programmstarter",
"APPLICATION_STARTED": "Anwendung gestartet",
@@ -368,5 +368,13 @@
"REMOVE_FROM_FAVOURITES": "Aus angehefteten Anwendungen entfernen",
"EMPTY": "Leer",
"INSTALLED": "Installiert",
"NOT_INSTALLED": "Nicht installiert"
"NOT_INSTALLED": "Nicht installiert",
"POWER_OPTIONS": {
"TITLE": "Energieoptionen",
"SHUTDOWN": "Herunterfahren",
"SUSPEND": "Ruhezustand",
"SHUTDOWN_ARE_YOU_SURE": "Bist du sicher, dass du den Computer ausschalten willst?"
},
"YES": "Ja",
"NO": "Nein"
}
+8 -2
View File
@@ -208,8 +208,8 @@ APP_SETTINGS.SAVE_PLAYSPACE_CENTER,Save playspace center,"Button in Settings > S
APP_SETTINGS.SAVE_PLAYSPACE_CENTER_HELP,Use current playspace for reset playspace binding.,"Tooltip for the Save playspace center button. ""Playspace"" was previously translated as ""${DEFINITIONS.PLAYSPACE}""."
APP_SETTINGS.RESET_PLAYSPACE_CENTER,Reset playspace center,"Button in Settings > Space Drag tab. Clears the previously saved playspace center. ""Playspace"" was previously translated as ""${DEFINITIONS.PLAYSPACE}""."
APP_SETTINGS.RESET_PLAYSPACE_CENTER_HELP,Clear previously set playspace center.,"Tooltip for the Reset playspace center button. ""Playspace"" was previously translated as ""${DEFINITIONS.PLAYSPACE}""."
APP_SETTINGS.RESTART_SOFTWARE,Restart software,Button in Settings > Troubleshooting tab. Restarts the WayVR software to apply settings that require a restart.
APP_SETTINGS.RESTART_SOFTWARE_HELP,Apply settings that require a restart,Tooltip for the Restart software button.
APP_SETTINGS.RESTART_WAYVR,Restart WayVR,Button in Settings which restarts WayVR
APP_SETTINGS.CLOSE_WAYVR,Close WayVR,Button in Settings which closes WayVR
APP_SETTINGS.ROUND_MULTIPLIER,UI Edge roundness,Slider in Settings > Look & Feel tab. Controls how rounded the corners of UI elements appear (range 0.1-5.0).
APP_SETTINGS.SCREEN_RENDER_DOWN,Render screen at lower resolution,Checkbox in Settings > Miscellaneous tab. Renders the captured desktop screen at a lower resolution to improve performance or reduce aliasing.
APP_SETTINGS.SCREEN_RENDER_DOWN_HELP,Helps with aliasing on high-res screens,Tooltip for the lower-resolution screen render setting.
@@ -336,6 +336,10 @@ MODIFICATION_DATE,Modification date,Column label in the remote skymap downloader
MONADO_RUNTIME,Monado runtime,Tab label in the dashboard sidebar for the Monado runtime tab.
LOADING,Loading...,Label in the loading spinner template used across the UI.
POPUP_ADD_DISPLAY.RESOLUTION,Resolution,Label for the resolution field in the add display popup.
POWER_OPTIONS.TITLE,"Power options","A button which allows the user to perform a specific power action"
POWER_OPTIONS.SHUTDOWN,"Shut down","One of the power options: shuts down the computer"
POWER_OPTIONS.SUSPEND,"Suspend","One of the power options: suspends the computer"
POWER_OPTIONS.SHUTDOWN_ARE_YOU_SURE,"Are you sure you want to turn off your computer?","Question dialog box before shutting down the computer"
PROCESS.FORCE_KILL,Force-kill,Tooltip on the kill button in the running games list.
PROCESS.STOP,Stop,Tooltip on the stop button in the running games list.
PROCESS_LIST,Process list,Tab name and group box title in the Monado runtime process list section.
@@ -357,3 +361,5 @@ REMOVE_FROM_FAVOURITES,Remove from pinned apps,"Label on the unpin section in th
EMPTY,Empty,Label shown in the pinned apps section when there are no pinned apps.
INSTALLED,"Installed","Installation flag (installed/not installed)"
NOT_INSTALLED,"Not installed","Installation flag (installed/not installed)"
YES,"Yes","Just a simple 'Yes' message, button in the dialog box"
NO,"No","Just a simple 'No' message, button in the dialog box"
1 DEFINITIONS.ANCHORED Anchored A movement option that makes this window move together with the rest of the workspace. Emphasize the "moving together" part as opposed to "not moving at all".
208 APP_SETTINGS.SPACE_GRAVITY_GRAVITY_HELP Amount of downwards force. 0.0 - no gravity Tooltip for the gravity setting.
209 APP_SETTINGS.SPACE_GRAVITY_GROUND_FRICTION Ground friction Setting in the space gravity physics panel. Friction when touching the ground (0.0 = ice-like, 1.0 = rough).
210 APP_SETTINGS.SPACE_GRAVITY_GROUND_FRICTION_HELP Amount of friction slowing you down if you're touching the ground. 0.0 - no friction (just like on ice), 1.0 - rough surface Tooltip for the ground friction setting.
211 APP_SETTINGS.SPACE_GRAVITY_FLOOR_HEIGHT Floor height Setting in the space gravity physics panel. The Y position where the virtual floor is.
212 APP_SETTINGS.SPACE_GRAVITY_FLOOR_HEIGHT_HELP The Y position where the floor is. Gravity stops when you reach this height. Tooltip for the floor height setting.
213 APP_SETTINGS.SPACE_ROTATE_UNLOCKED Allow space rotate on all axes Checkbox in Settings > Space Drag tab. Unlocks space rotate to work on all axes. "Space" in this context refers to "Playspace" which was translated as "${DEFINITIONS.PLAYSPACE}".
214 APP_SETTINGS.ROTATIONS HMD rotations Number of full 360-degree rotations of the HMD since startup. Shown in Settings > Statistics.
215 APP_SETTINGS.SESSION_TIME Session time Time since WayVR started. Shown in Settings > Statistics.
336
337
338
339
340
341
342
343
344
345
361
362
363
364
365
+11 -3
View File
@@ -211,8 +211,8 @@
"SAVE_PLAYSPACE_CENTER_HELP": "Use current playspace for reset playspace binding.",
"RESET_PLAYSPACE_CENTER": "Reset playspace center",
"RESET_PLAYSPACE_CENTER_HELP": "Clear previously set playspace center.",
"RESTART_SOFTWARE": "Restart software",
"RESTART_SOFTWARE_HELP": "Apply settings that require a restart",
"RESTART_WAYVR": "Restart WayVR",
"CLOSE_WAYVR": "Close WayVR",
"ROUND_MULTIPLIER": "UI Edge roundness",
"SCREEN_RENDER_DOWN": "Render screen at lower resolution",
"SCREEN_RENDER_DOWN_HELP": "Helps with aliasing on high-res screens",
@@ -346,6 +346,12 @@
"POPUP_ADD_DISPLAY": {
"RESOLUTION": "Resolution"
},
"POWER_OPTIONS": {
"TITLE": "Power options",
"SHUTDOWN": "Shut down",
"SUSPEND": "Suspend",
"SHUTDOWN_ARE_YOU_SURE": "Are you sure you want to turn off your computer?"
},
"PROCESS": {
"FORCE_KILL": "Force-kill",
"STOP": "Stop"
@@ -368,5 +374,7 @@
"REMOVE_FROM_FAVOURITES": "Remove from pinned apps",
"EMPTY": "Empty",
"INSTALLED": "Installed",
"NOT_INSTALLED": "Not installed"
"NOT_INSTALLED": "Not installed",
"YES": "Yes",
"NO": "No"
}
+12 -4
View File
@@ -197,8 +197,6 @@
"SAVE_PLAYSPACE_CENTER_HELP": "Usa el espacio de juego actual para el comando de reiniciar espacio de juego.",
"RESET_PLAYSPACE_CENTER": "Reiniciar centro del espacio de juego",
"RESET_PLAYSPACE_CENTER_HELP": "Restablece el centro del espacio de juego definido previamente.",
"RESTART_SOFTWARE": "Reiniciar software",
"RESTART_SOFTWARE_HELP": "Aplicar ajustes que requieren reiniciar",
"ROUND_MULTIPLIER": "Redondeo de bordes de la UI",
"SCREEN_RENDER_DOWN": "Renderizar pantalla a resolución menor",
"SCREEN_RENDER_DOWN_HELP": "Ayuda con el aliasing en pantallas de alta resolución",
@@ -263,7 +261,9 @@
"TITLE": "Diagnósticos",
"ENABLE_REBAR": "Activa [resizable bar] en la configuración de la BIOS para un rendimiento óptimo de VR"
},
"TUTORIAL": "Tutorial"
"TUTORIAL": "Tutorial",
"RESTART_WAYVR": "Reiniciar WayVR",
"CLOSE_WAYVR": "Cerrar WayVR"
},
"APPLICATION_LAUNCHER": "Lanzador de aplicaciones",
"APPLICATION_STARTED": "Aplicación iniciada",
@@ -368,5 +368,13 @@
"REMOVE_FROM_FAVOURITES": "Quitar de aplicaciones fijadas",
"EMPTY": "Vacío",
"INSTALLED": "Instalado",
"NOT_INSTALLED": "No instalado"
"NOT_INSTALLED": "No instalado",
"POWER_OPTIONS": {
"TITLE": "Opciones de energía",
"SHUTDOWN": "Apagar",
"SUSPEND": "Suspender",
"SHUTDOWN_ARE_YOU_SURE": "¿Seguro que quieres apagar el ordenador?"
},
"YES": "Sí",
"NO": "No"
}
+12 -4
View File
@@ -197,8 +197,6 @@
"SAVE_PLAYSPACE_CENTER_HELP": "Usa il playspace corrente per il comando di reset del playspace.",
"RESET_PLAYSPACE_CENTER": "Resetta centro playspace",
"RESET_PLAYSPACE_CENTER_HELP": "Rimuovi il centro del playspace impostato precedentemente.",
"RESTART_SOFTWARE": "Riavvia software",
"RESTART_SOFTWARE_HELP": "Applica le impostazioni che richiedono il riavvio",
"ROUND_MULTIPLIER": "Arrotondamento bordi UI",
"SCREEN_RENDER_DOWN": "Rendering schermo a risoluzione inferiore",
"SCREEN_RENDER_DOWN_HELP": "Riduce l'effetto aliasing sugli schermi ad alta risoluzione",
@@ -263,7 +261,9 @@
"TITLE": "Diagnostica",
"ENABLE_REBAR": "Abilita Resizable BAR nelle impostazioni del BIOS per prestazioni VR ottimali"
},
"TUTORIAL": "Tutorial"
"TUTORIAL": "Tutorial",
"RESTART_WAYVR": "Riavvia WayVR",
"CLOSE_WAYVR": "Chiudi WayVR"
},
"APPLICATION_LAUNCHER": "Avvio applicazioni",
"APPLICATION_STARTED": "Applicazione avviata",
@@ -368,5 +368,13 @@
"REMOVE_FROM_FAVOURITES": "Rimuovi dalle app fissate",
"EMPTY": "Vuoto",
"INSTALLED": "Installato",
"NOT_INSTALLED": "Non installato"
"NOT_INSTALLED": "Non installato",
"POWER_OPTIONS": {
"TITLE": "Opzioni di alimentazione",
"SHUTDOWN": "Spegni",
"SUSPEND": "Sospendi",
"SHUTDOWN_ARE_YOU_SURE": "Sei sicuro di voler spegnere il computer?"
},
"YES": "Sì",
"NO": "No"
}
+12 -4
View File
@@ -197,8 +197,6 @@
"SAVE_PLAYSPACE_CENTER_HELP": "現在のプレイスペースを、プレイスペースリセットのバインディングに使用する。",
"RESET_PLAYSPACE_CENTER": "プレイスペースの中心をリセット",
"RESET_PLAYSPACE_CENTER_HELP": "設定済みのプレイスペースの中心点をリセットする。",
"RESTART_SOFTWARE": "ソフトウェアを再起動",
"RESTART_SOFTWARE_HELP": "再起動が必要な設定を適用する",
"ROUND_MULTIPLIER": "UIの角の丸み",
"SCREEN_RENDER_DOWN": "低解像度で画面をレンダリング",
"SCREEN_RENDER_DOWN_HELP": "高解像度ディスプレイでのジャギーを軽減する",
@@ -263,7 +261,9 @@
"TITLE": "診断",
"ENABLE_REBAR": "VRパフォーマンスを最適化するため、BIOS設定でresizable BARを有効にして下さい"
},
"TUTORIAL": "チュートリアル"
"TUTORIAL": "チュートリアル",
"RESTART_WAYVR": "WayVRを再起動",
"CLOSE_WAYVR": "WayVRを閉じる"
},
"APPLICATION_LAUNCHER": "アプリランチャー",
"APPLICATION_STARTED": "アプリが起動しました",
@@ -368,5 +368,13 @@
"REMOVE_FROM_FAVOURITES": "固定済みアプリから外す",
"EMPTY": "なし",
"INSTALLED": "インストール済み",
"NOT_INSTALLED": "未インストール"
"NOT_INSTALLED": "未インストール",
"POWER_OPTIONS": {
"TITLE": "電源オプション",
"SHUTDOWN": "シャットダウン",
"SUSPEND": "サスペンド",
"SHUTDOWN_ARE_YOU_SURE": "コンピューターをシャットダウンしてもよろしいですか?"
},
"YES": "はい",
"NO": "いいえ"
}
+12 -4
View File
@@ -197,8 +197,6 @@
"SAVE_PLAYSPACE_CENTER_HELP": "Użyj aktualnej przestrzeni gry dla przypisania resetowania przestrzeni.",
"RESET_PLAYSPACE_CENTER": "Zresetuj centrum przestrzeni gry",
"RESET_PLAYSPACE_CENTER_HELP": "Resetuj aktualny środek przestrzeni gry.",
"RESTART_SOFTWARE": "Uruchom ponownie program",
"RESTART_SOFTWARE_HELP": "Zastosuj ustawienia wymagające restartu",
"ROUND_MULTIPLIER": "Zaokrąglenie krawędzi UI",
"SCREEN_RENDER_DOWN": "Renderuj ekran w niższej rozdzielczości",
"SCREEN_RENDER_DOWN_HELP": "Pomaga zredukować aliasing na ekranach o wysokiej rozdzielczości",
@@ -263,7 +261,9 @@
"TITLE": "Diagnostyka",
"ENABLE_REBAR": "Włącz resizable BAR w ustawieniach BIOS, aby uzyskać optymalną wydajność VR"
},
"TUTORIAL": "Samouczek"
"TUTORIAL": "Samouczek",
"RESTART_WAYVR": "Zrestartuj WayVR",
"CLOSE_WAYVR": "Zamknij WayVR"
},
"APPLICATION_LAUNCHER": "Launcher aplikacji",
"APPLICATION_STARTED": "Uruchomiono aplikację",
@@ -368,5 +368,13 @@
"REMOVE_FROM_FAVOURITES": "Usuń z przypiętych aplikacji",
"EMPTY": "Puste",
"INSTALLED": "Zainstalowano",
"NOT_INSTALLED": "Nie zainstalowano"
"NOT_INSTALLED": "Nie zainstalowano",
"POWER_OPTIONS": {
"TITLE": "Opcje zasilania",
"SHUTDOWN": "Wyłącz",
"SUSPEND": "Uśpij",
"SHUTDOWN_ARE_YOU_SURE": "Czy na pewno chcesz wyłączyć komputer?"
},
"YES": "Tak",
"NO": "Nie"
}
+12 -4
View File
@@ -197,8 +197,6 @@
"SAVE_PLAYSPACE_CENTER_HELP": "将当前的活动空间设置为重置活动空间的绑定点。",
"RESET_PLAYSPACE_CENTER": "重置活动空间中心",
"RESET_PLAYSPACE_CENTER_HELP": "重置之前设置的活动空间中心。",
"RESTART_SOFTWARE": "重启软件",
"RESTART_SOFTWARE_HELP": "应用需要重启才能生效的设置",
"ROUND_MULTIPLIER": "UI 边缘圆角",
"SCREEN_RENDER_DOWN": "低分辨率渲染屏幕",
"SCREEN_RENDER_DOWN_HELP": "有助于减轻高分辨率屏幕上的锯齿现象",
@@ -263,7 +261,9 @@
"TITLE": "诊断",
"ENABLE_REBAR": "请在 BIOS 设置中启用 Resizable BAR 以获得最佳 VR 性能"
},
"TUTORIAL": "教程"
"TUTORIAL": "教程",
"RESTART_WAYVR": "重启 WayVR",
"CLOSE_WAYVR": "关闭 WayVR"
},
"APPLICATION_LAUNCHER": "应用启动器",
"APPLICATION_STARTED": "应用程序已启动",
@@ -368,5 +368,13 @@
"REMOVE_FROM_FAVOURITES": "从固定应用中移除",
"EMPTY": "暂无",
"INSTALLED": "已安装",
"NOT_INSTALLED": "未安装"
"NOT_INSTALLED": "未安装",
"POWER_OPTIONS": {
"TITLE": "电源选项",
"SHUTDOWN": "关机",
"SUSPEND": "暂停",
"SHUTDOWN_ARE_YOU_SURE": "确定要关机吗?"
},
"YES": "确定",
"NO": "否"
}
+109 -5
View File
@@ -1,11 +1,11 @@
use std::{path::PathBuf, rc::Rc};
use std::{path::PathBuf, process::Command, rc::Rc};
use chrono::Timelike;
use glam::Vec2;
use strum::EnumCount;
use wgui::{
assets::{AssetPathRef, AssetProvider},
components::button::ComponentButton,
components::{ComponentTrait, button::ComponentButton},
event::{CallbackDataCommon, StyleSetRequest},
font_config::WguiFontConfig,
globals::WguiGlobals,
@@ -17,7 +17,10 @@ use wgui::{
task::Tasks,
theme::WguiTheme,
widget::{label::WidgetLabel, rectangle::WidgetRectangle, sprite::WidgetSprite},
windowing::window::{WguiWindow, WguiWindowParams, WguiWindowParamsExtra, WguiWindowPlacement},
windowing::{
context_menu::{self, ContextMenu},
window::{WguiWindow, WguiWindowParams, WguiWindowParamsExtra, WguiWindowPlacement},
},
};
use wlx_common::{
async_executor::AsyncExecutor,
@@ -35,10 +38,11 @@ use crate::{
settings::TabSettings, welcome::TabWelcome,
},
util::{
popup_manager::{MountPopupOnceParams, PopupManager, PopupManagerParams},
is_exec_installed,
popup_manager::{MountPopupOnceParams, PopupHolder, PopupManager, PopupManagerParams},
toast_manager::ToastManager,
},
views,
views::{self, ViewUpdateParams},
};
pub struct FrontendWidgets {
@@ -74,9 +78,11 @@ pub struct Frontend<T> {
toast_manager: ToastManager,
timestep: Timestep,
sounds_to_play: Vec<SoundType>,
context_menu_power: ContextMenu,
window_audio_settings: WguiWindow,
view_audio_settings: Option<views::audio_settings::View>,
popup_dialog_box: PopupHolder<views::dialog_box::View>,
}
pub struct FrontendUpdateParams<'a, T> {
@@ -116,11 +122,13 @@ pub enum FrontendTask {
MountPopupOnce(MountPopupOnceParams),
RefreshPopupManager,
ShowAudioSettings,
ShowPowerSettings(WidgetID /* source widget */),
UpdateAudioSettingsView,
RecenterPlayspace,
PushToast(Translation),
PlaySound(SoundType),
OpenURL(Rc<str>),
RunCommand(String /* program */, Vec<String> /* args */),
HideDashboard,
MarkTutorialGraduated,
}
@@ -202,9 +210,11 @@ impl<T: 'static> Frontend<T> {
popup_manager,
toast_manager,
window_audio_settings: WguiWindow::default(),
context_menu_power: Default::default(),
view_audio_settings: None,
executor: params.executor,
sounds_to_play: Vec::new(),
popup_dialog_box: Default::default(),
};
// init some things first
@@ -275,12 +285,77 @@ impl<T: 'static> Frontend<T> {
Ok(())
}
fn action_poweroff_confirm(&mut self) {
let tasks = self.tasks.clone();
views::dialog_box::mount_popup(
self.popup_dialog_box.clone(),
self.tasks.clone(),
views::dialog_box::Params {
globals: self.globals.clone(),
message: Translation::from_translation_key("POWER_OPTIONS.SHUTDOWN_ARE_YOU_SURE"),
entries: views::dialog_box::get_entries_yes_no(),
on_action_click: Box::new(move |action| {
if action == "yes" {
tasks.push(FrontendTask::RunCommand(
String::from("loginctl"),
vec![String::from("poweroff")],
));
}
}),
},
);
}
fn process_power_action(&mut self, action: &str, data: &mut T) {
if !is_exec_installed("loginctl") {
// TODO: add support for other init systems?
self.tasks.push(FrontendTask::PushToast(Translation::from_raw_text(
"loginctl is not installed",
)));
return;
}
match action {
"restart" => {
self.interface.restart(data);
}
"close" => {
self.interface.close(data);
}
"suspend" => {
self.tasks.push(FrontendTask::RunCommand(
String::from("loginctl"),
vec![String::from("suspend")],
));
}
"shutdown" => {
self.action_poweroff_confirm();
}
_ => {
unreachable!()
}
}
}
fn tick(&mut self, params: FrontendUpdateParams<T>) -> anyhow::Result<FrontendUpdateResult> {
// fixme: timer events instead of this thing
if self.ticks.is_multiple_of(1000) {
self.update_time(params.data)?;
}
// TODO: refactor: simplify update function (no configs or T are required in most cases)
let mut config_change_kind = None;
self.popup_dialog_box.update(&mut ViewUpdateParams {
layout: &mut self.layout,
executor: &self.executor,
general_config: self.interface.general_config(params.data),
config_change_kind: &mut config_change_kind,
})?;
if let context_menu::TickResult::Action(action) = self.context_menu_power.tick(&mut self.layout, &mut self.state)? {
self.process_power_action(&action, params.data);
}
{
// always 30 times per second
while self.timestep.on_tick() {
@@ -371,12 +446,14 @@ impl<T: 'static> Frontend<T> {
FrontendTask::MountPopupOnce(popup_params) => self.mount_popup_once(popup_params, params.data)?,
FrontendTask::RefreshPopupManager => self.refresh_popup_manager()?,
FrontendTask::ShowAudioSettings => self.action_show_audio_settings()?,
FrontendTask::ShowPowerSettings(pos) => self.action_show_power_settings(pos)?,
FrontendTask::UpdateAudioSettingsView => self.action_update_audio_settings()?,
FrontendTask::RecenterPlayspace => self.action_recenter_playspace(params.data)?,
FrontendTask::PushToast(content) => self.toast_manager.push(content),
FrontendTask::PlaySound(sound_type) => self.queue_play_sound(sound_type),
FrontendTask::HideDashboard => self.action_hide_dashboard(params.data),
FrontendTask::OpenURL(url) => self.action_open_url(url),
FrontendTask::RunCommand(program, args) => self.action_run_command(program, args),
FrontendTask::MarkTutorialGraduated => self.action_tutorial_graduated(params.data),
};
Ok(())
@@ -503,6 +580,11 @@ impl<T: 'static> Frontend<T> {
FrontendTask::ShowAudioSettings,
);
let btn_power = self.state.fetch_component_as::<ComponentButton>("btn_power")?;
self
.tasks
.handle_button(&btn_power, FrontendTask::ShowPowerSettings(btn_power.base().get_id()));
// "Recenter playspace" bottom bar button
self.tasks.handle_button(
&self.state.fetch_component_as::<ComponentButton>("btn_recenter")?,
@@ -512,6 +594,18 @@ impl<T: 'static> Frontend<T> {
Ok(())
}
fn action_show_power_settings(&mut self, source_id: WidgetID) -> anyhow::Result<()> {
self.context_menu_power.open(context_menu::OpenParams {
on_custom_attribs: None,
position: source_id.into(),
blueprint: context_menu::Blueprint::Template {
template_name: "context_menu_power_options".into(),
template_params: Default::default(),
},
});
Ok(())
}
fn action_show_audio_settings(&mut self) -> anyhow::Result<()> {
self.window_audio_settings.open(&mut WguiWindowParams {
position: Vec2::new(64.0, 64.0),
@@ -576,4 +670,14 @@ impl<T: 'static> Frontend<T> {
url
))));
}
fn action_run_command(&mut self, command: String, args: Vec<String>) {
if let Err(e) = Command::new(command).args(args).spawn() {
self
.tasks
.push(FrontendTask::PushToast(Translation::from_raw_text_string(format!(
"Failed to launch command: {e}"
))))
};
}
}
+4 -4
View File
@@ -333,7 +333,7 @@ where
let tasks = mp.tasks.clone();
move |_common, e: ButtonClickEvent| {
tasks.push(Task::OpenContextMenu(
e.mouse_pos_absolute.unwrap_or_default(),
e.mouse_pos_absolute.into(),
EnumType::VARIANTS
.iter()
.filter_map(|item| {
@@ -360,12 +360,12 @@ where
let text = &title.text;
let translated = if title.translated { "1" } else { "0" };
Some(context_menu::Cell {
Some(context_menu::Cell::Button(context_menu::Button {
action_name: Some(format!("{setting_str};{id};{value};{text};{translated}").into()),
title,
tooltip,
attribs: vec![],
})
..Default::default()
}))
})
.collect(),
));
+1 -7
View File
@@ -1,4 +1,3 @@
use glam::Vec2;
use std::{marker::PhantomData, rc::Rc, str::FromStr};
use strum::{AsRefStr, EnumProperty, EnumString};
use wgui::{
@@ -81,11 +80,10 @@ pub(crate) enum Task {
ClearPipewireTokens,
ClearSavedState,
DeleteAllConfigs,
OpenContextMenu(Vec2, Vec<context_menu::Cell>),
OpenContextMenu(context_menu::Position, Vec<context_menu::Cell>),
RemoveAutostartApp(Rc<str>),
ResetPlayspace,
ResetPlayspaceCenter,
RestartSoftware,
SavePlayspaceCenter,
SetTab(TabNameEnum),
SettingUpdated(SettingType),
@@ -231,10 +229,6 @@ impl<T> Tab<T> for TabSettings<T> {
frontend.interface.playspace_task(data, DashPlayspaceTask::Reset)?;
return Ok(());
}
Task::RestartSoftware => {
frontend.interface.restart(data);
return Ok(());
}
Task::OpenContextMenu(position, cells) => {
self.context_menu.open(context_menu::OpenParams {
on_custom_attribs: None,
@@ -1,27 +1,17 @@
use wgui::layout::WidgetID;
use crate::tab::settings::{
SettingsMountParams, SettingsTab,
macros::{MacroParams, options_category, options_diagnostics_drm_card, options_diagnostics_row},
use crate::{
tab::settings::{
SettingsMountParams, SettingsTab,
macros::{MacroParams, options_category, options_diagnostics_drm_card, options_diagnostics_row},
},
util::is_exec_installed,
};
pub struct State {}
impl SettingsTab for State {}
// works good enough
fn is_exec_installed(exec: &str) -> bool {
let mut cmd = std::process::Command::new("sh");
cmd.arg("-c");
cmd.arg(format!("command -v {}", exec));
let Ok(res) = cmd.output() else {
return false;
};
res.status.success()
}
#[derive(Default)]
struct LspciInfo {
// slot: String,
+16 -15
View File
@@ -328,32 +328,33 @@ fn whisper_models_dropdown(mp: &mut MacroParams, parent: WidgetID) -> anyhow::Re
let mut cells = WHISPER_MODELS
.iter()
.enumerate()
.map(|(idx, item)| context_menu::Cell {
action_name: Some(format!(";{id};{}", item.file_name).into()),
title: Translation::from_raw_text_string(format!(
"{}{}",
item.display_name,
if downloaded_models.contains(&idx) { " ✅" } else { "" }
)),
tooltip: None,
attribs: vec![],
.map(|(idx, item)| {
context_menu::Cell::Button(context_menu::Button {
action_name: Some(format!(";{id};{}", item.file_name).into()),
title: Translation::from_raw_text(item.display_name),
tooltip: None,
attribs: vec![],
sprite_src_builtin: if downloaded_models.contains(&idx) {
Some("@/dashboard/check.svg".into())
} else {
None
},
})
})
.collect::<Vec<_>>();
cells.insert(
0,
context_menu::Cell {
context_menu::Cell::Button(context_menu::Button {
action_name: Some(format!(";{id};").into()),
title: Translation::from_translation_key("APP_SETTINGS.OPTION.NONE"),
tooltip: None,
attribs: vec![],
},
..Default::default()
}),
);
parent_tasks.push(ParentTask::OpenContextMenu(
e.mouse_pos_absolute.unwrap_or_default(),
cells,
));
parent_tasks.push(ParentTask::OpenContextMenu(e.mouse_pos_absolute.into(), cells));
Ok(())
}
}));
@@ -36,13 +36,6 @@ impl State {
"@/dashboard/circle.svg",
Task::DeleteAllConfigs,
)?;
options_danger_button(
par.mp,
c,
"APP_SETTINGS.RESTART_SOFTWARE",
"@/dashboard/refresh.svg",
Task::RestartSoftware,
)?;
Ok(State {})
}
}
+13
View File
@@ -7,3 +7,16 @@ pub mod steam_utils;
pub mod toast_manager;
pub mod wgui_simple;
pub mod whisper;
// works good enough
pub fn is_exec_installed(exec: &str) -> bool {
let mut cmd = std::process::Command::new("sh");
cmd.arg("-c");
cmd.arg(format!("command -v {}", exec));
let Ok(res) = cmd.output() else {
return false;
};
res.status.success()
}
+56 -56
View File
@@ -39,7 +39,7 @@ use crate::{
enum Task {
Save,
Cancel,
OpenContextMenu(glam::Vec2, Vec<context_menu::Cell>),
OpenContextMenu(context_menu::Position, Vec<context_menu::Cell>),
UpdateThreshold(Rc<str>, XrInputSide, usize, f32),
AddChordMember(Rc<str>, XrInputSide),
RemoveChordMember(Rc<str>, XrInputSide, usize),
@@ -804,20 +804,21 @@ fn chord_hand_dropdown(
let cells = [XrInputSide::Left, XrInputSide::Right]
.into_iter()
.filter(|side| profile.find_userpath(*side).is_some())
.map(|side| context_menu::Cell {
action_name: Some(
format!(
"chord_hand;{};{};{};{}",
action,
output_side.as_ref(),
member_idx,
side.as_ref()
)
.into(),
),
title: side_translation(side),
tooltip: None,
attribs: vec![],
.map(|side| {
context_menu::Cell::Button(context_menu::Button {
action_name: Some(
format!(
"chord_hand;{};{};{};{}",
action,
output_side.as_ref(),
member_idx,
side.as_ref()
)
.into(),
),
title: side_translation(side),
..Default::default()
})
})
.collect();
@@ -842,20 +843,21 @@ fn chord_subpath_dropdown(
.unwrap_or_else(|| Translation::from_translation_key("APP_SETTINGS.OPTION.NONE"));
let cells = available
.iter()
.map(|subpath| context_menu::Cell {
action_name: Some(
format!(
"chord_subpath;{};{};{};{}",
action,
output_side.as_ref(),
member_idx,
subpath.as_ref()
)
.into(),
),
title: subpath.translation(),
tooltip: None,
attribs: vec![],
.map(|subpath| {
context_menu::Cell::Button(context_menu::Button {
action_name: Some(
format!(
"chord_subpath;{};{};{};{}",
action,
output_side.as_ref(),
member_idx,
subpath.as_ref()
)
.into(),
),
title: subpath.translation(),
..Default::default()
})
})
.collect();
@@ -885,20 +887,21 @@ fn chord_component_dropdown(
.unwrap_or_else(|| Translation::from_raw_text_rc(Default::default()));
let cells = available
.iter()
.map(|component| context_menu::Cell {
action_name: Some(
format!(
"chord_comp;{};{};{};{}",
action,
output_side.as_ref(),
member_idx,
component.as_ref()
)
.into(),
),
title: component.translation(),
tooltip: None,
attribs: vec![],
.map(|component| {
context_menu::Cell::Button(context_menu::Button {
action_name: Some(
format!(
"chord_comp;{};{};{};{}",
action,
output_side.as_ref(),
member_idx,
component.as_ref()
)
.into(),
),
title: component.translation(),
..Default::default()
})
})
.collect();
@@ -1003,23 +1006,23 @@ fn create_dropdown<B: 'static + BindingsDropdown>(
) -> anyhow::Result<()> {
let mut cells = available
.iter()
.map(|item| context_menu::Cell {
action_name: Some(item.action_str(&action, side)),
title: item.translation(),
tooltip: None,
attribs: vec![],
.map(|item| {
context_menu::Cell::Button(context_menu::Button {
action_name: Some(item.action_str(&action, side)),
title: item.translation(),
..Default::default()
})
})
.collect::<Vec<_>>();
if let Some(action_str) = B::clear_str(&action, side) {
cells.insert(
0,
context_menu::Cell {
context_menu::Cell::Button(context_menu::Button {
action_name: Some(action_str),
title: Translation::from_translation_key("APP_SETTINGS.OPTION.NONE"),
tooltip: None,
attribs: vec![],
},
..Default::default()
}),
);
}
@@ -1052,10 +1055,7 @@ fn create_dropdown_with_cells(
let tasks = mp.tasks.clone();
let cells = cells.clone();
move |_common, e: ButtonClickEvent| {
tasks.push(Task::OpenContextMenu(
e.mouse_pos_absolute.unwrap_or_default(),
cells.clone(),
));
tasks.push(Task::OpenContextMenu(e.mouse_pos_absolute.into(), cells.clone()));
Ok(())
}
}));
+22 -3
View File
@@ -32,6 +32,21 @@ enum Task {
ActionClicked(&'static str),
}
pub fn get_entries_yes_no() -> Vec<ButtonEntry> {
vec![
ButtonEntry {
content: Translation::from_translation_key("YES"),
icon: "@/dashboard/check.svg",
action: "yes",
},
ButtonEntry {
content: Translation::from_translation_key("NO"),
icon: "@/dashboard/cancel.svg",
action: "no",
},
]
}
pub struct View {
tasks: Tasks<Task>,
@@ -52,6 +67,13 @@ fn doc_params(globals: &WguiGlobals) -> ParseDocumentParams<'_> {
impl ViewTrait for View {
fn update(&mut self, _par: &mut ViewUpdateParams) -> anyhow::Result<()> {
self.tick();
Ok(())
}
}
impl View {
pub fn tick(&mut self) {
for task in self.tasks.drain() {
match task {
Task::ActionClicked(action) => {
@@ -65,11 +87,8 @@ impl ViewTrait for View {
}
}
}
Ok(())
}
}
impl View {
pub fn new(
layout: &mut Layout,
id_parent: WidgetID,
+6 -4
View File
@@ -33,7 +33,7 @@ use wlx_common::locale::WayVRLangProvider;
#[derive(Clone)]
pub enum TestbedTask {
ShowPopup,
ShowContextMenu(Vec2),
ShowContextMenu(context_menu::Position),
}
struct Data {
@@ -196,7 +196,7 @@ impl TestbedGeneric {
button_context_menu.on_click({
let tasks = testbed.tasks.clone();
Rc::new(move |_common, m| {
tasks.push(TestbedTask::ShowContextMenu(m.boundary.bottom_left()));
tasks.push(TestbedTask::ShowContextMenu(m.boundary.into()));
Ok(())
})
});
@@ -212,7 +212,9 @@ impl TestbedGeneric {
) -> anyhow::Result<()> {
match task {
TestbedTask::ShowPopup => self.show_popup(params, data)?,
TestbedTask::ShowContextMenu(position) => self.show_context_menu(params, data, *position),
TestbedTask::ShowContextMenu(position) => {
self.show_context_menu(params, data, position.clone())
}
}
Ok(())
@@ -251,7 +253,7 @@ impl TestbedGeneric {
&mut self,
_params: &mut TestbedUpdateParams,
data: &mut Data,
position: Vec2,
position: context_menu::Position,
) {
data.context_menu.open(context_menu::OpenParams {
on_custom_attribs: Some(Rc::new(move |custom_attribs| {
+1 -1
View File
@@ -244,7 +244,7 @@ pub(super) fn setup_custom_button<S: 'static>(
template_name: template_name.clone(),
template_params: template_params.clone(),
},
position: data.metadata.get_mouse_pos_absolute().unwrap(), //want panic
position: data.metadata.get_mouse_pos_absolute().unwrap().into(), // want panic
});
Ok(EventResult::Consumed)
}
+4
View File
@@ -564,6 +564,10 @@ impl DashInterface<AppState> for DashInterfaceLive {
}
}
fn close(&mut self, _data: &mut AppState) {
RUNNING.store(false, Ordering::Relaxed);
}
fn restart(&mut self, _data: &mut AppState) {
RUNNING.store(false, Ordering::Relaxed);
RESTART.store(true, Ordering::Relaxed);
+17 -3
View File
@@ -1,7 +1,21 @@
<layout version="2">
<!-- text: str -->
<template name="Cell">
<Button id="button" text="${text}" tooltip_str="${tooltip_str}" weight="bold" border="0" padding="4" color="background(transparent)" height="24" justify_content="start" />
<macro name="ContextMenuCell" tooltip_str="${tooltip_str}" weight="bold" border="0" padding_left="4" padding_right="4" color="background(transparent)" justify_content="start" min_height="28"/>
<!--
text
-->
<template name="CellButton">
<Button id="button" text="${text}" macro="ContextMenuCell"/>
</template>
<!--
text, sprite_src_builtin
-->
<template name="CellButtonSprite">
<Button id="button" macro="ContextMenuCell" gap="8">
<sprite src_builtin="${sprite_src_builtin}" width="24" height="24"/>
<label text="${text}" weight="bold"/>
</Button>
</template>
<template name="Separator">
+8 -2
View File
@@ -374,11 +374,15 @@ impl ParserState {
'children: for child in el_context_menu.children() {
match child.tag_name().name() {
"" => {}
"separator" => {
cells.push(context_menu::Cell::Separator);
}
"cell" => {
let mut title: Option<Translation> = None;
let mut tooltip: Option<Translation> = None;
let mut action_name: Option<Rc<str>> = None;
let mut attribs = Vec::<AttribPair>::new();
let mut sprite_src_builtin: Option<Rc<str>> = None;
for attrib in child.attributes() {
let (key, value) = (attrib.name(), attrib.value());
@@ -389,6 +393,7 @@ impl ParserState {
"tooltip" => tooltip = Some(Translation::from_translation_key(value)),
"tooltip_str" => tooltip = Some(Translation::from_raw_text(value)),
"action" => action_name = Some(value.into()),
"sprite_src_builtin" => sprite_src_builtin = Some(value.into()),
"skip" => {
let resolved = replace_vars(value, template_params);
//FIXME: this is always empty
@@ -406,12 +411,13 @@ impl ParserState {
}
let title = title.context("No text/translation provided")?;
cells.push(context_menu::Cell {
cells.push(context_menu::Cell::Button(context_menu::Button {
title,
tooltip,
action_name,
sprite_src_builtin,
attribs,
});
}));
}
other => {
anyhow::bail!("{:?}: unexpected <{other}> tag", self.xml_path.get_path().display());
+114 -29
View File
@@ -1,9 +1,10 @@
use crate::{
assets::AssetPathRef,
components::{ComponentTrait, button::ComponentButton},
drawing::Boundary,
globals::WguiGlobals,
i18n::Translation,
layout::Layout,
layout::{Layout, WidgetID},
parser::{self, Fetchable, ParserState, TemplateParams},
task::Tasks,
windowing::window::{WguiWindow, WguiWindowParams, WguiWindowParamsExtra, WguiWindowPlacement},
@@ -11,14 +12,21 @@ use crate::{
use glam::Vec2;
use std::rc::Rc;
#[derive(Clone)]
pub struct Cell {
#[derive(Default, Clone)]
pub struct Button {
pub title: Translation,
pub tooltip: Option<Translation>,
pub action_name: Option<Rc<str>>,
pub sprite_src_builtin: Option<Rc<str>>,
pub attribs: Vec<parser::AttribPair>,
}
#[derive(Clone)]
pub enum Cell {
Button(Button),
Separator,
}
pub enum Blueprint {
Cells(Vec<Cell>),
Template {
@@ -27,9 +35,47 @@ pub enum Blueprint {
},
}
#[derive(Clone)]
pub enum Position {
Unspecified,
Absolute(Vec2),
Widget(WidgetID),
Boundary(Boundary),
}
// absolute position
impl From<Vec2> for Position {
fn from(value: Vec2) -> Self {
Self::Absolute(value)
}
}
impl From<Option<Vec2>> for Position {
fn from(value: Option<Vec2>) -> Self {
match value {
Some(v) => Self::Absolute(v),
None => Self::Unspecified,
}
}
}
// relative to widget center
impl From<WidgetID> for Position {
fn from(value: WidgetID) -> Self {
Self::Widget(value)
}
}
// relative to absolute rectangle center
impl From<Boundary> for Position {
fn from(value: Boundary) -> Self {
Self::Boundary(value)
}
}
pub struct OpenParams {
pub on_custom_attribs: Option<parser::OnCustomAttribsFunc>,
pub position: Vec2,
pub position: Position,
pub blueprint: Blueprint,
}
@@ -89,14 +135,24 @@ impl ContextMenu {
Blueprint::Cells(cells) => cells,
};
let absolute_pos = match params.position {
Position::Unspecified => Vec2::new(0.0, 0.0),
Position::Absolute(pos) => pos,
Position::Boundary(boundary) => boundary.center(),
Position::Widget(widget_id) => {
let widget = layout.state.widgets.fetch(widget_id)?;
widget.state().data.cached_absolute_boundary.center()
}
};
let root_size = layout.state.get_widget_size(layout.content_root_widget);
let (window_pos, placement) = if params.position.y < root_size.y / 2.0 {
(params.position, WguiWindowPlacement::TopLeft)
let (window_pos, placement) = if absolute_pos.y < root_size.y / 2.0 {
(absolute_pos, WguiWindowPlacement::TopLeft)
} else {
// invert y axis (position.y is counted from the bottom)
(
Vec2::new(params.position.x, root_size.y - params.position.y),
Vec2::new(absolute_pos.x, root_size.y - absolute_pos.y),
WguiWindowPlacement::BottomLeft,
)
};
@@ -121,33 +177,62 @@ impl ContextMenu {
let id_buttons = inner_parser.get_widget_id("buttons")?;
for (idx, cell) in cells.iter().enumerate() {
let mut par = TemplateParams::new();
par.insert_rc("text", cell.title.generate(&mut globals.i18n()));
if let Some(tooltip) = cell.tooltip.as_ref() {
par.insert_rc("tooltip_str", tooltip.generate(&mut globals.i18n()));
let mut has_sprites = false;
for cell in &cells {
if let Cell::Button(button) = cell
&& button.sprite_src_builtin.is_some()
{
has_sprites = true;
}
}
let mut data_cell = inner_parser.parse_template_only(&doc_params, "Cell", layout, id_buttons, par)?;
for cell in &cells {
match cell {
Cell::Button(cell) => {
let mut par = TemplateParams::new();
par.insert_rc("text", cell.title.generate(&mut globals.i18n()));
let button = data_cell.fetch_component_as::<ComponentButton>("button")?;
let button_id = button.base().get_id();
parser_state.data.take_results_from(&mut data_cell);
self
.tasks
.handle_button(&button, Task::ActionClicked(cell.action_name.clone()));
if has_sprites {
par.insert_rc(
"sprite_src_builtin",
cell
.sprite_src_builtin
.clone()
.unwrap_or_else(|| Rc::from("@/dashboard/dot.svg")),
);
}
if let Some(tooltip) = &cell.tooltip {
par.insert_rc("tooltip_str", tooltip.generate(&mut globals.i18n()));
}
if let Some(c) = params.on_custom_attribs.as_ref() {
(*c)(parser::CustomAttribsInfo {
pairs: &cell.attribs,
parent_id: id_buttons,
widget_id: button_id,
widgets: &layout.state.widgets,
});
}
let mut data_cell = inner_parser.parse_template_only(
&doc_params,
if has_sprites { "CellButtonSprite" } else { "CellButton" },
layout,
id_buttons,
par,
)?;
if idx < cells.len() - 1 {
inner_parser.realize_template(&doc_params, "Separator", layout, id_buttons, Default::default())?;
let button = data_cell.fetch_component_as::<ComponentButton>("button")?;
let button_id = button.base().get_id();
parser_state.data.take_results_from(&mut data_cell);
self
.tasks
.handle_button(&button, Task::ActionClicked(cell.action_name.clone()));
if let Some(c) = params.on_custom_attribs.as_ref() {
(*c)(parser::CustomAttribsInfo {
pairs: &cell.attribs,
parent_id: id_buttons,
widget_id: button_id,
widgets: &layout.state.widgets,
});
}
}
Cell::Separator => {
inner_parser.realize_template(&doc_params, "Separator", layout, id_buttons, Default::default())?;
}
}
}
Ok(())
+1
View File
@@ -97,6 +97,7 @@ pub trait DashInterface<T> {
fn general_config<'a>(&'a mut self, data: &'a mut T) -> &'a mut GeneralConfig;
fn config_changed(&mut self, data: &mut T, kind: ConfigChangeKind);
fn restart(&mut self, data: &mut T);
fn close(&mut self, data: &mut T);
fn toggle_dashboard(&mut self, data: &mut T);
fn get_feats(&mut self, data: &mut T) -> InterfaceFeats;
fn hmd_stats(&mut self, data: &mut T) -> HmdStats;
@@ -231,6 +231,8 @@ impl DashInterface<()> for DashInterfaceEmulated {
fn restart(&mut self, _data: &mut ()) {}
fn close(&mut self, _data: &mut ()) {}
fn toggle_dashboard(&mut self, _data: &mut ()) {}
fn get_feats(&mut self, _data: &mut ()) -> dash_interface::InterfaceFeats {