Monado app switcher, lang update

This commit is contained in:
Aleksander
2026-01-08 19:46:34 +01:00
parent 650bc99a95
commit e421c39539
22 changed files with 566 additions and 153 deletions

View File

@@ -1,7 +1,35 @@
<layout>
<include src="t_tab_title.xml" />
<include src="../t_group_box.xml" />
<!-- key: str, value: str -->
<template name="BoolFlag">
<div flex_direction="row" gap="4">
<label text="${key}" />
<label weight="bold" text="${value}" />
</div>
</template>
<!-- name, checked, flag_* -->
<template name="Cell">
<rectangle macro="group_box">
<CheckBox id="checkbox" text="${name}" checked="${checked}" />
<div flex_direction="row" gap="8">
<BoolFlag key="Active:" value="${flag_active}" />
<BoolFlag key="Focused:" value="${flag_focused}" />
<BoolFlag key="IO active:" value="${flag_io_active}" />
<BoolFlag key="Overlay:" value="${flag_overlay}" />
<BoolFlag key="Primary:" value="${flag_primary}" />
<BoolFlag key="Visible:" value="${flag_visible}" />
</div>
</rectangle>
</template>
<elements>
<TabTitle translation="MONADO_RUNTIME" icon="dashboard/monado.svg" />
<div id="list_parent" flex_direction="column" gap="8">
<!-- filled at runtime -->
</div>
</elements>
</layout>

View File

@@ -14,11 +14,11 @@
<include src="../t_group_box.xml" />
<elements>
<div flex_direction="row" gap="16">
<div flex_direction="row" gap="16" flex_grow="1">
<rectangle macro="group_box" id="icon_parent" padding="16" color="#0033aa66" color2="#00000022" gradient="vertical" justify_content="center">
</rectangle>
<div flex_direction="column" gap="8" min_width="720" max_width="720">
<div flex_direction="column" gap="8" flex_grow="1">
<label id="label_title" weight="bold" size="32" overflow="hidden" />
<Subtext label_id="label_exec" overflow="hidden" />
<Separator />

View File

@@ -1,6 +1,6 @@
{
"HOME_SCREEN": "Startbildschirm",
"MONADO_RUNTIME": "Monado-Laufzeitumgebung",
"MONADO_RUNTIME": "Monado-Laufzeitumgebung",
"APPLICATIONS": "Anwendungen",
"GAMES": "Spiele",
"SETTINGS": "Einstellungen",

View File

@@ -127,7 +127,7 @@
"HOME_SCREEN": "Home",
"LIST_OF_PROCESSES": "Process list",
"LIST_OF_WINDOWS": "Window list",
"MONADO_RUNTIME": "Monado runtime",
"MONADO_RUNTIME": "Monado runtime",
"NO_WINDOWS_FOUND": "No windows found",
"POPUP_ADD_DISPLAY": {
"RESOLUTION": "Resolution"

View File

@@ -1,6 +1,6 @@
{
"HOME_SCREEN": "Inicio",
"MONADO_RUNTIME": "Monado tiempo de ejecución",
"MONADO_RUNTIME": "Monado tiempo de ejecución",
"APPLICATIONS": "Aplicaciones",
"GAMES": "Juegos",
"SETTINGS": "Ajustes",

View File

@@ -50,7 +50,7 @@
"USE_SKYBOX_HELP": "Wyświetlaj niebo, jeśli nie ma aplikacji sceny lub passthrough",
"USE_PASSTHROUGH_HELP": "Pozwól na passthrough, jeśli runtime XR to obsługuje",
"SCREEN_RENDER_DOWN_HELP": "Pomaga redukować aliasing na ekranach o wysokiej rozdzielczości",
"SETS_ON_WATCH": "Lista zestawówna zegarku",
"SETS_ON_WATCH": "Lista zestawów na zegarku",
"TROUBLESHOOTING": "Rozwiązywanie problemów",
"CLEAR_SAVED_STATE": "Wyczyść zapisany stan",
"CLEAR_PIPEWIRE_TOKENS": "Wyczyść tokeny PipeWire",

View File

@@ -1,43 +1,163 @@
use std::marker::PhantomData;
use std::{collections::HashMap, marker::PhantomData, rc::Rc};
use wgui::{
assets::AssetPath,
components::checkbox::ComponentCheckbox,
globals::WguiGlobals,
layout::WidgetID,
parser::{ParseDocumentParams, ParserState},
parser::{self, Fetchable, ParseDocumentParams, ParserState},
task::Tasks,
};
use wlx_common::dash_interface;
use crate::{
frontend::Frontend,
tab::{Tab, TabType},
};
#[derive(Debug)]
enum Task {
Refresh,
FocusClient(String),
}
pub struct TabMonado<T> {
#[allow(dead_code)]
pub state: ParserState,
state: ParserState,
tasks: Tasks<Task>,
marker: PhantomData<T>,
globals: WguiGlobals,
id_list_parent: WidgetID,
cells: Vec<parser::ParserData>,
ticks: u32,
}
impl<T> Tab<T> for TabMonado<T> {
fn get_type(&self) -> TabType {
TabType::Games
}
fn update(&mut self, frontend: &mut Frontend<T>, data: &mut T) -> anyhow::Result<()> {
for task in self.tasks.drain() {
match task {
Task::Refresh => self.refresh(frontend, data)?,
Task::FocusClient(name) => self.focus_client(frontend, data, name)?,
}
}
// every few seconds
if self.ticks.is_multiple_of(500) {
self.tasks.push(Task::Refresh);
}
self.ticks += 1;
Ok(())
}
}
fn doc_params(globals: &'_ WguiGlobals) -> ParseDocumentParams<'_> {
ParseDocumentParams {
globals: globals.clone(),
path: AssetPath::BuiltIn("gui/tab/monado.xml"),
extra: Default::default(),
}
}
fn yesno(n: bool) -> &'static str {
match n {
true => "yes",
false => "no",
}
}
impl<T> TabMonado<T> {
pub fn new(frontend: &mut Frontend<T>, parent_id: WidgetID) -> anyhow::Result<Self> {
let state = wgui::parser::parse_from_assets(
&ParseDocumentParams {
globals: frontend.layout.state.globals.clone(),
path: AssetPath::BuiltIn("gui/tab/monado.xml"),
extra: Default::default(),
},
&mut frontend.layout,
parent_id,
)?;
let globals = frontend.layout.state.globals.clone();
let state = wgui::parser::parse_from_assets(&doc_params(&globals), &mut frontend.layout, parent_id)?;
let id_list_parent = state.get_widget_id("list_parent")?;
let tasks = Tasks::<Task>::new();
tasks.push(Task::Refresh);
Ok(Self {
state,
marker: PhantomData,
tasks,
globals,
id_list_parent,
ticks: 0,
cells: Vec::new(),
})
}
fn mount_client(&mut self, frontend: &mut Frontend<T>, client: &dash_interface::MonadoClient) -> anyhow::Result<()> {
let mut par = HashMap::<Rc<str>, Rc<str>>::new();
par.insert(
"checked".into(),
if client.is_primary {
Rc::from("1")
} else {
Rc::from("0")
},
);
par.insert("name".into(), client.name.clone().into());
par.insert("flag_active".into(), yesno(client.is_active).into());
par.insert("flag_focused".into(), yesno(client.is_focused).into());
par.insert("flag_io_active".into(), yesno(client.is_io_active).into());
par.insert("flag_overlay".into(), yesno(client.is_overlay).into());
par.insert("flag_primary".into(), yesno(client.is_primary).into());
par.insert("flag_visible".into(), yesno(client.is_visible).into());
let state_cell = self.state.parse_template(
&doc_params(&self.globals),
"Cell",
&mut frontend.layout,
self.id_list_parent,
par,
)?;
let checkbox = state_cell.fetch_component_as::<ComponentCheckbox>("checkbox")?;
checkbox.on_toggle({
let tasks = self.tasks.clone();
let client_name = client.name.clone();
Box::new(move |_common, e| {
if e.checked {
tasks.push(Task::FocusClient(client_name.clone()));
}
Ok(())
})
});
self.cells.push(state_cell);
Ok(())
}
fn refresh(&mut self, frontend: &mut Frontend<T>, data: &mut T) -> anyhow::Result<()> {
log::debug!("refreshing monado client list");
let clients = frontend.interface.monado_client_list(data)?;
frontend.layout.remove_children(self.id_list_parent);
self.cells.clear();
for client in clients {
self.mount_client(frontend, &client)?;
}
Ok(())
}
fn focus_client(&mut self, frontend: &mut Frontend<T>, data: &mut T, name: String) -> anyhow::Result<()> {
frontend.interface.monado_client_focus(data, &name)?;
self.tasks.push(Task::Refresh);
Ok(())
}
}

View File

@@ -129,7 +129,7 @@ pub fn stop(app_id: AppID, force_kill: bool) -> anyhow::Result<()> {
log::info!("Killing process with PID {} and its children", game.pid);
let _ = std::process::Command::new("pkill")
.arg(if force_kill { "-9" } else { "-11" })
.arg(if force_kill { "-9" } else { "-15" })
.arg("-P")
.arg(format!("{}", game.pid))
.spawn()?;

View File

@@ -5,6 +5,7 @@ Glossary:
- wlx-overlay-s: The name of this software (also called WlxOverlay-S)
- WayVR: A Wayland compositor intended to be used in VR
- WayVR Dashboard: An application (and game) launcher which is displayed in front of the user
- Monado: A VR compositor
- OpenVR: API made by Valve
- OpenXR: API made by Khronos
- OSC: OpenSoundControl

View File

@@ -10,9 +10,16 @@ mod widget_rectangle;
mod widget_sprite;
use crate::{
assets::{normalize_path, AssetPath, AssetPathOwned}, components::{Component, ComponentWeak}, drawing::{self}, globals::WguiGlobals, i18n::Translation, layout::{Layout, LayoutParams, LayoutState, Widget, WidgetID, WidgetMap, WidgetPair}, log::LogErr, parser::{
assets::{AssetPath, AssetPathOwned, normalize_path},
components::{Component, ComponentWeak},
drawing::{self},
globals::WguiGlobals,
i18n::Translation,
layout::{Layout, LayoutParams, LayoutState, Widget, WidgetID, WidgetMap, WidgetPair},
log::LogErr,
parser::{
component_button::parse_component_button,
component_checkbox::{parse_component_checkbox, CheckboxKind},
component_checkbox::{CheckboxKind, parse_component_checkbox},
component_radio_group::parse_component_radio_group,
component_slider::parse_component_slider,
widget_div::parse_widget_div,
@@ -20,7 +27,9 @@ use crate::{
widget_label::parse_widget_label,
widget_rectangle::parse_widget_rectangle,
widget_sprite::parse_widget_sprite,
}, widget::ConstructEssentials, windowing::context_menu
},
widget::ConstructEssentials,
windowing::context_menu,
};
use anyhow::Context;
use ouroboros::self_referencing;
@@ -215,7 +224,10 @@ impl ParserState {
template_parameters: HashMap<Rc<str>, Rc<str>>,
) -> anyhow::Result<ParserData> {
let Some(template) = self.data.templates.get(template_name) else {
anyhow::bail!("{:?}: no template named \"{template_name}\" found", self.path.get_path_buf());
anyhow::bail!(
"{:?}: no template named \"{template_name}\" found",
self.path.get_path_buf().display()
);
};
let mut ctx = ParserContext {
@@ -291,7 +303,7 @@ impl ParserState {
if !other.starts_with('_') {
anyhow::bail!("unexpected \"{other}\" attribute");
}
attribs.push(AttribPair::new(key, replace_vars(value, &template_params)));
attribs.push(AttribPair::new(key, replace_vars(value, template_params)));
}
}
}
@@ -305,14 +317,12 @@ impl ParserState {
});
}
other => {
anyhow::bail!("{:?}: unexpected <{other}> tag", self.path.get_path_buf());
anyhow::bail!("{:?}: unexpected <{other}> tag", self.path.get_path_buf().display());
}
}
}
Ok(
cells,
)
Ok(cells)
}
}
@@ -470,23 +480,29 @@ impl ParserContext<'_> {
insert_color_vars!(self, "faded", def.faded_color, def.translucent_alpha);
insert_color_vars!(self, "bg", def.bg_color, def.translucent_alpha);
}
fn print_invalid_attrib(&self, tag_name: &str, key: &str, value: &str) {
log::warn!("{}: <{tag_name}> value for \"{key}\" is invalid: \"{value}\"", self.doc_params.path.get_str());
}
fn print_invalid_attrib(&self, tag_name: &str, key: &str, value: &str) {
log::warn!(
"{}: <{tag_name}> value for \"{key}\" is invalid: \"{value}\"",
self.doc_params.path.get_str()
);
}
fn print_missing_attrib(&self, tag_name: &str, attr: &str) {
log::warn!("{}: <{tag_name}> is missing \"{attr}\".", self.doc_params.path.get_str());
}
fn print_missing_attrib(&self, tag_name: &str, attr: &str) {
log::warn!(
"{}: <{tag_name}> is missing \"{attr}\".",
self.doc_params.path.get_str()
);
}
fn parse_val(&self, tag_name: &str, key: &str, value: &str) -> Option<f32> {
fn parse_val(&self, tag_name: &str, key: &str, value: &str) -> Option<f32> {
let Ok(val) = value.parse::<f32>() else {
self.print_invalid_attrib(tag_name, key, value);
return None;
};
Some(val)
}
}
fn parse_percent(&self, tag_name: &str, key: &str, value: &str) -> Option<f32> {
fn parse_percent(&self, tag_name: &str, key: &str, value: &str) -> Option<f32> {
let Some(val_str) = value.split('%').next() else {
self.print_invalid_attrib(tag_name, key, value);
return None;
@@ -497,20 +513,20 @@ fn parse_percent(&self, tag_name: &str, key: &str, value: &str) -> Option<f32> {
return None;
};
Some(val / 100.0)
}
}
fn parse_size_unit<T>(&self, tag_name: &str, key: &str, value: &str) -> Option<T>
where
fn parse_size_unit<T>(&self, tag_name: &str, key: &str, value: &str) -> Option<T>
where
T: taffy::prelude::FromPercent + taffy::prelude::FromLength,
{
{
if is_percent(value) {
Some(taffy::prelude::percent(self.parse_percent(tag_name, key, value)?))
} else {
Some(taffy::prelude::length(parse_f32(value)?))
}
}
}
fn parse_check_i32(&self, tag_name: &str, key: &str, value: &str, num: &mut i32) -> bool {
fn parse_check_i32(&self, tag_name: &str, key: &str, value: &str, num: &mut i32) -> bool {
if let Some(value) = parse_i32(value) {
*num = value;
true
@@ -518,9 +534,9 @@ fn parse_check_i32(&self, tag_name: &str, key: &str, value: &str, num: &mut i32)
self.print_invalid_attrib(tag_name, key, value);
false
}
}
}
fn parse_check_f32(&self, tag_name: &str, key: &str, value: &str, num: &mut f32) -> bool {
fn parse_check_f32(&self, tag_name: &str, key: &str, value: &str, num: &mut f32) -> bool {
if let Some(value) = parse_f32(value) {
*num = value;
true
@@ -528,7 +544,7 @@ fn parse_check_f32(&self, tag_name: &str, key: &str, value: &str, num: &mut f32)
self.print_invalid_attrib(tag_name, key, value);
false
}
}
}
}
fn parse_i32(value: &str) -> Option<i32> {
@@ -583,7 +599,6 @@ fn require_tag_by_name<'a>(node: &roxmltree::Node<'a, 'a>, name: &str) -> anyhow
get_tag_by_name(node, name).ok_or_else(|| anyhow::anyhow!("Tag \"{name}\" not found"))
}
fn parse_widget_other_internal(
template: &Rc<Template>,
template_parameters: HashMap<Rc<str>, Rc<str>>,
@@ -617,7 +632,10 @@ fn parse_widget_other(
attribs: &[AttribPair],
) -> anyhow::Result<()> {
let Some(template) = ctx.get_template(xml_tag_name) else {
log::error!("{}: Undefined tag named \"{xml_tag_name}\"", ctx.doc_params.path.get_str());
log::error!(
"{}: Undefined tag named \"{xml_tag_name}\"",
ctx.doc_params.path.get_str()
);
return Ok(()); // not critical
};
@@ -746,6 +764,7 @@ pub fn replace_vars(input: &str, vars: &HashMap<Rc<str>, Rc<str>>) -> Rc<str> {
}
#[allow(clippy::manual_strip)]
#[allow(clippy::single_match_else)]
fn process_attrib(
template_parameters: &HashMap<Rc<str>, Rc<str>>,
ctx: &ParserContext,
@@ -760,7 +779,7 @@ fn process_attrib(
None => {
log::warn!("{}: undefined variable \"{value}\"", ctx.doc_params.path.get_str());
AttribPair::new(key, "undefined")
},
}
}
} else {
AttribPair::new(key, replace_vars(value, template_parameters))
@@ -797,7 +816,10 @@ fn process_attribs<'a>(
res.push(process_attrib(&file.template_parameters, ctx, macro_key, macro_value));
}
} else {
log::warn!("{}: requested macro named \"{value}\" not found!", ctx.doc_params.path.get_str());
log::warn!(
"{}: requested macro named \"{value}\" not found!",
ctx.doc_params.path.get_str()
);
}
} else {
res.push(process_attrib(&file.template_parameters, ctx, key, value));
@@ -816,7 +838,10 @@ fn parse_tag_theme<'a>(ctx: &mut ParserContext, node: roxmltree::Node<'a, 'a>) {
}
"" => { /* ignore */ }
_ => {
log::warn!("{}: <{child_name}> is not a valid child to <theme>.", ctx.doc_params.path.get_str());
log::warn!(
"{}: <{child_name}> is not a valid child to <theme>.",
ctx.doc_params.path.get_str()
);
}
}
}
@@ -865,7 +890,11 @@ fn parse_tag_macro(file: &ParserFile, ctx: &mut ParserContext, node: roxmltree::
}
_ => {
if macro_attribs.insert(pair.attrib.clone(), pair.value).is_some() {
log::warn!("{}: macro attrib \"{}\" already defined!", ctx.doc_params.path.get_str(), pair.attrib);
log::warn!(
"{}: macro attrib \"{}\" already defined!",
ctx.doc_params.path.get_str(),
pair.attrib
);
}
}
}
@@ -963,19 +992,29 @@ fn parse_child<'a>(
new_widget_id = Some(parse_widget_div(file, ctx, child_node, parent_id, &attribs, tag_name)?);
}
"rectangle" => {
new_widget_id = Some(parse_widget_rectangle(file, ctx, child_node, parent_id, &attribs, tag_name)?);
new_widget_id = Some(parse_widget_rectangle(
file, ctx, child_node, parent_id, &attribs, tag_name,
)?);
}
"label" => {
new_widget_id = Some(parse_widget_label(file, ctx, child_node, parent_id, &attribs, tag_name)?);
new_widget_id = Some(parse_widget_label(
file, ctx, child_node, parent_id, &attribs, tag_name,
)?);
}
"sprite" => {
new_widget_id = Some(parse_widget_sprite(file, ctx, child_node, parent_id, &attribs, tag_name)?);
new_widget_id = Some(parse_widget_sprite(
file, ctx, child_node, parent_id, &attribs, tag_name,
)?);
}
"image" => {
new_widget_id = Some(parse_widget_image(file, ctx, child_node, parent_id, &attribs, tag_name)?);
new_widget_id = Some(parse_widget_image(
file, ctx, child_node, parent_id, &attribs, tag_name,
)?);
}
"Button" => {
new_widget_id = Some(parse_component_button(file, ctx, child_node, parent_id, &attribs, tag_name)?);
new_widget_id = Some(parse_component_button(
file, ctx, child_node, parent_id, &attribs, tag_name,
)?);
}
"Slider" => {
new_widget_id = Some(parse_component_slider(ctx, parent_id, &attribs, tag_name)?);
@@ -999,7 +1038,9 @@ fn parse_child<'a>(
)?);
}
"RadioGroup" => {
new_widget_id = Some(parse_component_radio_group(file, ctx, child_node, parent_id, &attribs, tag_name)?);
new_widget_id = Some(parse_component_radio_group(
file, ctx, child_node, parent_id, &attribs, tag_name,
)?);
}
"" => { /* ignore */ }
other_tag_name => {

View File

@@ -5,6 +5,17 @@ use wayvr_ipc::{
use crate::{config::GeneralConfig, desktop_finder::DesktopFinder};
#[derive(Clone)]
pub struct MonadoClient {
pub name: String,
pub is_primary: bool,
pub is_active: bool,
pub is_visible: bool,
pub is_focused: bool,
pub is_overlay: bool,
pub is_io_active: bool,
}
pub trait DashInterface<T> {
fn window_list(&mut self, data: &mut T) -> anyhow::Result<Vec<WvrWindow>>;
fn window_set_visible(&mut self, data: &mut T, handle: WvrWindowHandle, visible: bool) -> anyhow::Result<()>;
@@ -18,6 +29,8 @@ pub trait DashInterface<T> {
) -> anyhow::Result<WvrProcessHandle>;
fn process_list(&mut self, data: &mut T) -> anyhow::Result<Vec<WvrProcess>>;
fn process_terminate(&mut self, data: &mut T, handle: WvrProcessHandle) -> anyhow::Result<()>;
fn monado_client_list(&mut self, data: &mut T) -> anyhow::Result<Vec<MonadoClient>>;
fn monado_client_focus(&mut self, data: &mut T, name: &str) -> anyhow::Result<()>;
fn recenter_playspace(&mut self, data: &mut T) -> anyhow::Result<()>;
fn desktop_finder<'a>(&'a mut self, data: &'a mut T) -> &'a mut DesktopFinder;
fn general_config<'a>(&'a mut self, data: &'a mut T) -> &'a mut GeneralConfig;

View File

@@ -3,7 +3,12 @@ use wayvr_ipc::{
packet_server::{WvrProcess, WvrProcessHandle, WvrWindow, WvrWindowHandle},
};
use crate::{config::GeneralConfig, dash_interface::DashInterface, desktop_finder::DesktopFinder, gen_id};
use crate::{
config::GeneralConfig,
dash_interface::{self, DashInterface},
desktop_finder::DesktopFinder,
gen_id,
};
#[derive(Debug)]
pub struct EmuProcess {
@@ -56,6 +61,7 @@ pub struct DashInterfaceEmulated {
windows: EmuWindowVec,
desktop_finder: DesktopFinder,
general_config: GeneralConfig,
monado_clients: Vec<dash_interface::MonadoClient>,
}
impl DashInterfaceEmulated {
@@ -77,11 +83,42 @@ impl DashInterfaceEmulated {
// Use serde defaults
let general_config = serde_json::from_str("{}").unwrap();
let monado_clients = vec![
dash_interface::MonadoClient {
name: String::from("The Best VR Game 3000"),
is_active: true,
is_focused: true,
is_io_active: true,
is_overlay: false,
is_primary: true,
is_visible: true,
},
dash_interface::MonadoClient {
name: String::from("Second app"),
is_active: true,
is_focused: false,
is_io_active: true,
is_overlay: false,
is_primary: false,
is_visible: true,
},
dash_interface::MonadoClient {
name: String::from("Third app"),
is_active: true,
is_focused: false,
is_io_active: true,
is_overlay: false,
is_primary: false,
is_visible: true,
},
];
Self {
processes,
windows,
desktop_finder,
general_config,
monado_clients,
}
}
}
@@ -187,4 +224,23 @@ impl DashInterface<()> for DashInterfaceEmulated {
fn config_changed(&mut self, _: &mut ()) {}
fn restart(&mut self, _data: &mut ()) {}
fn monado_client_list(&mut self, _data: &mut ()) -> anyhow::Result<Vec<dash_interface::MonadoClient>> {
Ok(self.monado_clients.clone())
}
fn monado_client_focus(&mut self, _data: &mut (), name: &str) -> anyhow::Result<()> {
for client in self.monado_clients.iter_mut() {
client.is_focused = false;
client.is_active = false;
client.is_primary = false;
}
if let Some(client) = self.monado_clients.iter_mut().find(|m| m.name == name) {
client.is_active = true;
client.is_focused = true;
client.is_primary = true;
}
Ok(())
}
}

View File

@@ -3,7 +3,16 @@
"CENTER": "Zentrum"
},
"BAR": {
"ADD_MIRROR": "Neuen Spiegel-Overlay hinzufügen"
"ADD_MIRROR": "Neuen Spiegel-Overlay hinzufügen",
"EDIT_MODE_TOGGLE": "Bearbeitungsmodus umschalten",
"ADD_NEW_SET": "Neues Set hinzufügen",
"DELETE_CURRENT_SET": "Aktuelles Set löschen",
"TOGGLE_VISIBILITY": "Sichtbarkeit umschalten",
"RESET_POSITION": "Position zurücksetzen",
"RELOAD_FROM_DISK": "XML-Datei von der Festplatte neu laden",
"CLOSE_MIRROR": "Spiegel schließen",
"CLOSE_APP": "App schließen",
"FORCE_CLOSE_APP": "App zwangsweise schließen"
},
"WATCH": {
"RECENTER": "Spielbereich neu zentrieren",
@@ -14,7 +23,8 @@
"ADD_NEW_SET": "Neuen Satz hinzufügen",
"SWITCH_TO_SET": "Zum Satz wechseln",
"TOGGLE_FOR_CURRENT_SET": "Sichtbarkeit im aktuellen Satz umschalten",
"LONG_PRESS_TO_DELETE_SET": "Lange drücken, um Satz zu löschen"
"LONG_PRESS_TO_DELETE_SET": "Lange drücken, um Satz zu löschen",
"CLEANUP_MIRRORS": "Spiegel entfernen, die\nderzeit nicht sichtbar sind"
},
"EDIT_MODE": {
"ADJUST_CURVATURE": "Krümmung anpassen",
@@ -83,6 +93,8 @@
"EMPTY_SET": "Leeres Set!",
"LETS_ADD_OVERLAYS": "Lass uns ein paar Overlays von der Uhr hinzufügen!",
"FIXING_FLOOR": "Boden wird in 5 Sekunden fixiert...",
"ONE_CONTROLLER_ON_FLOOR": "Lege einen Controller auf den Boden!"
"ONE_CONTROLLER_ON_FLOOR": "Lege einen Controller auf den Boden!",
"CANNOT_ADD_SET": "Satz kann nicht hinzugefügt werden!",
"MAXIMUM_SETS_REACHED": "Maximale Anzahl an Sets erreicht."
}
}

View File

@@ -3,7 +3,16 @@
"CENTER": "Centro"
},
"BAR": {
"ADD_MIRROR": "Agregar una nueva superposición de espejo"
"ADD_MIRROR": "Agregar una nueva superposición de espejo",
"EDIT_MODE_TOGGLE": "Activar/desactivar el modo de edición",
"ADD_NEW_SET": "Añadir nuevo set",
"DELETE_CURRENT_SET": "Eliminar set actual",
"TOGGLE_VISIBILITY": "Alternar visibilidad",
"RESET_POSITION": "Restablecer posición",
"RELOAD_FROM_DISK": "Volver a cargar XML desde el disco",
"CLOSE_MIRROR": "Cerrar espejo",
"CLOSE_APP": "Cerrar aplicación",
"FORCE_CLOSE_APP": "Forzar cierre de la aplicación"
},
"WATCH": {
"RECENTER": "Recentrar el área de juego",
@@ -14,7 +23,8 @@
"ADD_NEW_SET": "Añadir un nuevo conjunto",
"SWITCH_TO_SET": "Cambiar al conjunto",
"TOGGLE_FOR_CURRENT_SET": "Alternar visibilidad en el conjunto actual",
"LONG_PRESS_TO_DELETE_SET": "Mantén presionado para eliminar el conjunto"
"LONG_PRESS_TO_DELETE_SET": "Mantén presionado para eliminar el conjunto",
"CLEANUP_MIRRORS": "Eliminar los espejos que\nno son actualmente visibles"
},
"EDIT_MODE": {
"ADJUST_CURVATURE": "Ajustar curvatura",
@@ -83,6 +93,8 @@
"EMPTY_SET": "¡Conjunto vacío!",
"LETS_ADD_OVERLAYS": "¡Añadamos algunos overlays desde el reloj!",
"FIXING_FLOOR": "Fijando el suelo en 5 segundos...",
"ONE_CONTROLLER_ON_FLOOR": "¡Coloca un mando en el suelo!"
"ONE_CONTROLLER_ON_FLOOR": "¡Coloca un mando en el suelo!",
"CANNOT_ADD_SET": "¡No se puede agregar el conjunto!",
"MAXIMUM_SETS_REACHED": "Se ha alcanzado el número máximo de sets."
}
}

View File

@@ -3,7 +3,16 @@
"CENTER": "センター"
},
"BAR": {
"ADD_MIRROR": "新しいミラーを追加"
"ADD_MIRROR": "新しいミラーを追加",
"EDIT_MODE_TOGGLE": "編集モードの切り替え",
"ADD_NEW_SET": "新しいセットを追加",
"DELETE_CURRENT_SET": "現在のセットを削除",
"TOGGLE_VISIBILITY": "表示/非表示の切り替え",
"RESET_POSITION": "位置をリセット",
"RELOAD_FROM_DISK": "ディスクからXMLを再読み込み",
"CLOSE_MIRROR": "ミラーを閉じる",
"CLOSE_APP": "アプリを閉じる",
"FORCE_CLOSE_APP": "アプリを強制終了"
},
"WATCH": {
"RECENTER": "プレイスペースをリセンター",
@@ -14,7 +23,8 @@
"ADD_NEW_SET": "新しいセットを追加",
"SWITCH_TO_SET": "セットに切り替える",
"TOGGLE_FOR_CURRENT_SET": "現在のセットで表示を切り替え",
"LONG_PRESS_TO_DELETE_SET": "長押しでセットを削除"
"LONG_PRESS_TO_DELETE_SET": "長押しでセットを削除",
"CLEANUP_MIRRORS": "現在表示されていないミラーを削除"
},
"EDIT_MODE": {
"ADJUST_CURVATURE": "曲率の調整",
@@ -81,6 +91,8 @@
"EMPTY_SET": "空のセットです!",
"LETS_ADD_OVERLAYS": "ウォッチからオーバーレイを追加しましょう!",
"FIXING_FLOOR": "5秒後にフロアを固定します...",
"ONE_CONTROLLER_ON_FLOOR": "コントローラーを床に置いてください!"
"ONE_CONTROLLER_ON_FLOOR": "コントローラーを床に置いてください!",
"CANNOT_ADD_SET": "セットを追加できません!",
"MAXIMUM_SETS_REACHED": "最大セット数に達しました。"
}
}

View File

@@ -3,7 +3,16 @@
"CENTER": "Centrum"
},
"BAR": {
"ADD_MIRROR": "Dodaj nowy widok lustrzany"
"ADD_MIRROR": "Dodaj nowy widok lustrzany",
"EDIT_MODE_TOGGLE": "Przełącz tryb edycji",
"ADD_NEW_SET": "Dodaj nowy zestaw",
"DELETE_CURRENT_SET": "Usuń aktualny zestaw",
"TOGGLE_VISIBILITY": "Przełącz widoczność",
"RESET_POSITION": "Zresetuj pozycję",
"RELOAD_FROM_DISK": "Przeładuj XML z dysku",
"CLOSE_MIRROR": "Zamknij lustro",
"CLOSE_APP": "Zamknij aplikację",
"FORCE_CLOSE_APP": "Wymuś zamknięcie aplikacji"
},
"WATCH": {
"RECENTER": "Wyśrodkuj przestrzeń gry",
@@ -14,7 +23,8 @@
"ADD_NEW_SET": "Dodaj nowy zestaw",
"SWITCH_TO_SET": "Przełącz na zestaw",
"TOGGLE_FOR_CURRENT_SET": "Przełącz widoczność w bieżącym zestawie",
"LONG_PRESS_TO_DELETE_SET": "Przytrzymaj, aby usunąć zestaw"
"LONG_PRESS_TO_DELETE_SET": "Przytrzymaj, aby usunąć zestaw",
"CLEANUP_MIRRORS": "Usuń lustra, które\nnie są obecnie widoczne"
},
"EDIT_MODE": {
"ADJUST_CURVATURE": "Dostosuj zakrzywienie",
@@ -81,6 +91,8 @@
"EMPTY_SET": "Pusty zestaw!",
"LETS_ADD_OVERLAYS": "Dodajmy kilka nakładek z zegarka!",
"FIXING_FLOOR": "Naprawianie podłogi za 5 sekund...",
"ONE_CONTROLLER_ON_FLOOR": "Umieść jeden kontroler na podłodze!"
"ONE_CONTROLLER_ON_FLOOR": "Umieść jeden kontroler na podłodze!",
"CANNOT_ADD_SET": "Nie można dodać zestawu!",
"MAXIMUM_SETS_REACHED": "Osiągnięto maksymalną liczbę zestawów."
}
}

View File

@@ -14,14 +14,18 @@ impl InputBlocker {
}
}
pub fn update(&mut self, state: &AppState, watch_id: OverlayID, monado: &mut Monado) {
if !state.session.config.block_game_input {
pub fn update(&mut self, app: &mut AppState, watch_id: OverlayID) {
let Some(monado) = &mut app.monado else {
return; // monado not available
};
if !app.session.config.block_game_input {
return;
}
let any_hovered = state.input_state.pointers.iter().any(|p| {
let any_hovered = app.input_state.pointers.iter().any(|p| {
p.interaction.hovered_id.is_some_and(|id| {
id != watch_id || !state.session.config.block_game_input_ignore_watch
id != watch_id || !app.session.config.block_game_input_ignore_watch
})
});

View File

@@ -11,7 +11,7 @@ use serde::{Deserialize, Serialize};
use wlx_common::config_io;
use crate::{
backend::input::{Haptics, Pointer, TrackedDevice, TrackedDeviceRole},
backend::input::{Haptics, InputState, Pointer, TrackedDevice, TrackedDeviceRole},
state::{AppSession, AppState},
};
@@ -227,12 +227,12 @@ impl OpenXrInputSource {
fn update_device_battery_status(
device: &mut mnd::Device,
role: TrackedDeviceRole,
app: &mut AppState,
input_state: &mut InputState,
) {
if let Ok(status) = device.battery_status()
&& status.present
{
app.input_state.devices.push(TrackedDevice {
input_state.devices.push(TrackedDevice {
soc: Some(status.charge),
charging: status.charging,
role,
@@ -247,7 +247,11 @@ impl OpenXrInputSource {
}
}
pub fn update_devices(app: &mut AppState, monado: &mut mnd::Monado) -> bool {
pub fn update_devices(app: &mut AppState) -> bool {
let Some(monado) = &mut app.monado else {
return false; // monado not available
};
let old_len = app.input_state.devices.len();
app.input_state.devices.clear();
@@ -267,13 +271,14 @@ impl OpenXrInputSource {
),
];
let mut seen = Vec::<u32>::with_capacity(32);
for (mnd_role, wlx_role) in roles {
let device = monado.device_from_role(mnd_role);
if let Ok(mut device) = device
&& !seen.contains(&device.index)
{
seen.push(device.index);
Self::update_device_battery_status(&mut device, wlx_role, app);
Self::update_device_battery_status(&mut device, wlx_role, &mut app.input_state);
}
}
if let Ok(devices) = monado.devices() {
@@ -284,7 +289,7 @@ impl OpenXrInputSource {
} else {
TrackedDeviceRole::None
};
Self::update_device_battery_status(&mut device, role, app);
Self::update_device_battery_status(&mut device, role, &mut app.input_state);
}
}
}

View File

@@ -7,7 +7,6 @@ use std::{
use glam::{Affine3A, Vec3};
use input::OpenXrInputSource;
use libmonado::Monado;
use openxr as xr;
use skybox::create_skybox;
use vulkano::{Handle, VulkanObject};
@@ -98,17 +97,15 @@ pub fn openxr_run(show_by_default: bool, headless: bool) -> Result<(), BackendEr
let mut delete_queue = vec![];
let mut monado = Monado::auto_connect()
.map_err(|e| log::warn!("Will not use libmonado: {e}"))
.ok();
app.monado_init();
let mut playspace = monado.as_mut().and_then(|m| {
let mut playspace = app.monado.as_mut().and_then(|m| {
playspace::PlayspaceMover::new(m)
.map_err(|e| log::warn!("Will not use Monado playspace mover: {e}"))
.ok()
});
let mut blocker = monado.is_some().then(blocker::InputBlocker::new);
let mut blocker = app.monado.is_some().then(blocker::InputBlocker::new);
let (session, mut frame_wait, mut frame_stream) = unsafe {
let raw_session = helpers::create_overlay_session(
@@ -223,10 +220,8 @@ pub fn openxr_run(show_by_default: bool, headless: bool) -> Result<(), BackendEr
}
}
if next_device_update <= Instant::now()
&& let Some(monado) = &mut monado
{
let changed = OpenXrInputSource::update_devices(&mut app, monado);
if app.monado.is_some() && next_device_update <= Instant::now() {
let changed = OpenXrInputSource::update_devices(&mut app);
if changed {
overlays.devices_changed(&mut app)?;
}
@@ -278,11 +273,7 @@ pub fn openxr_run(show_by_default: bool, headless: bool) -> Result<(), BackendEr
app.input_state.post_update(&app.session);
if let Some(ref mut blocker) = blocker {
blocker.update(
&app,
watch_id,
monado.as_mut().unwrap(), // safe
);
blocker.update(&mut app, watch_id);
}
if app
@@ -307,11 +298,7 @@ pub fn openxr_run(show_by_default: bool, headless: bool) -> Result<(), BackendEr
watch_fade(&mut app, overlays.mut_by_id(watch_id).unwrap()); // want panic
if let Some(ref mut space_mover) = playspace {
space_mover.update(
&mut overlays,
&app,
monado.as_mut().unwrap(), // safe
);
space_mover.update(&mut overlays, &mut app);
}
for o in overlays.values_mut() {
@@ -489,8 +476,8 @@ pub fn openxr_run(show_by_default: bool, headless: bool) -> Result<(), BackendEr
overlays.handle_task(&mut app, task)?;
}
TaskType::Playspace(task) => {
if let (Some(playspace), Some(monado)) = (playspace.as_mut(), monado.as_mut()) {
playspace.handle_task(&app, monado, task);
if let Some(playspace) = playspace.as_mut() {
playspace.handle_task(&mut app, task);
}
}
#[cfg(feature = "openvr")]

View File

@@ -43,7 +43,11 @@ impl PlayspaceMover {
})
}
pub fn handle_task(&mut self, app: &AppState, monado: &mut Monado, task: PlayspaceTask) {
pub fn handle_task(&mut self, app: &mut AppState, task: PlayspaceTask) {
let Some(monado) = &mut app.monado else {
return; // monado not available
};
match task {
PlayspaceTask::FixFloor => {
self.fix_floor(&app.input_state, monado);
@@ -60,9 +64,12 @@ impl PlayspaceMover {
pub fn update(
&mut self,
overlays: &mut OverlayWindowManager<OpenXrOverlayData>,
app: &AppState,
monado: &mut Monado,
app: &mut AppState,
) {
let Some(monado) = &mut app.monado else {
return; // monado not available
};
for pointer in &app.input_state.pointers {
if pointer.now.space_reset {
if !pointer.before.space_reset {

View File

@@ -16,7 +16,7 @@ use wgui::{
widget::EventResult,
};
use wlx_common::{
dash_interface::DashInterface,
dash_interface::{self, DashInterface},
overlays::{BackendAttrib, BackendAttribValue},
};
use wlx_common::{
@@ -444,4 +444,92 @@ impl DashInterface<AppState> for DashInterfaceLive {
RUNNING.store(false, Ordering::Relaxed);
RESTART.store(true, Ordering::Relaxed);
}
fn monado_client_list(
&mut self,
app: &mut AppState,
) -> anyhow::Result<Vec<dash_interface::MonadoClient>> {
let Some(monado) = &mut app.monado else {
return Ok(Vec::new()); // no monado available
};
let clients = monado_list_clients_filtered(monado)?;
let mut res = Vec::<dash_interface::MonadoClient>::new();
for mut client in clients {
let name = client.name()?;
let state = client.state()?;
res.push(dash_interface::MonadoClient {
name,
is_primary: state.contains(libmonado::ClientState::ClientPrimaryApp),
is_active: state.contains(libmonado::ClientState::ClientSessionActive),
is_visible: state.contains(libmonado::ClientState::ClientSessionVisible),
is_focused: state.contains(libmonado::ClientState::ClientSessionFocused),
is_overlay: state.contains(libmonado::ClientState::ClientSessionOverlay),
is_io_active: state.contains(libmonado::ClientState::ClientIoActive),
});
}
Ok(res)
}
fn monado_client_focus(&mut self, app: &mut AppState, name: &str) -> anyhow::Result<()> {
let Some(monado) = &mut app.monado else {
return Ok(()); // no monado avoilable
};
monado_client_focus(monado, name)?;
// Restart monado (BUG!)
// https://gitlab.freedesktop.org/monado/monado/-/issues/497
app.monado_init();
Ok(())
}
}
const CLIENT_NAME_BLACKLIST: [&str; 2] = ["wlx-overlay-s", "libmonado"];
fn monado_list_clients_filtered(
monado: &mut libmonado::Monado,
) -> anyhow::Result<Vec<libmonado::Client<'_>>> {
let mut clients: Vec<_> = monado.clients()?.into_iter().collect();
let clients: Vec<_> = clients
.iter_mut()
.filter_map(|client| {
let Ok(name) = client.name() else {
return None;
};
for cell in CLIENT_NAME_BLACKLIST {
if cell == name {
// blacklisted!
return None;
}
}
Some(client.clone())
})
.collect();
Ok(clients)
}
fn monado_client_focus(monado: &mut libmonado::Monado, name: &str) -> anyhow::Result<()> {
let clients = monado_list_clients_filtered(monado)?;
for mut client in clients {
let client_name = client.name()?;
if client_name != name {
continue;
}
log::info!("Monado focus set to {client_name}");
client.set_primary()?;
return Ok(());
}
Ok(())
}

View File

@@ -64,6 +64,9 @@ pub struct AppState {
#[cfg(feature = "wayvr")]
pub wvr_server: Option<WvrServerState>,
#[cfg(feature = "openxr")]
pub monado: Option<libmonado::Monado>,
}
#[allow(unused_mut)]
@@ -163,8 +166,20 @@ impl AppState {
#[cfg(feature = "wayvr")]
wvr_server,
#[cfg(feature = "openxr")]
monado: None,
})
}
#[cfg(feature = "openxr")]
pub fn monado_init(&mut self) {
log::debug!("Connecting to Monado IPC");
self.monado = None; // stop connection first
self.monado = libmonado::Monado::auto_connect()
.map_err(|e| log::warn!("Will not use libmonado: {e}"))
.ok();
}
}
pub struct AppSession {