Compare commits

...
20 Commits
Author SHA1 Message Date
galister e7456a9b0a WIP: space drag rework 2024-03-07 13:17:12 +01:00
galister 507c0677c8 ShowUi & ShowMirror: no implicit show/hide 2024-03-06 20:43:40 +01:00
galister 1d9b60fc07 update ovr_overlay 2024-03-06 19:09:57 +01:00
galister 85d9ea9127 simplify a thing 2024-03-06 19:03:40 +01:00
galister a55ade6741 modular-ui: use boxes where it makes sense 2024-03-06 18:45:14 +01:00
galister 5800d7d99d refactor: graphics type aliases 2024-03-06 18:41:34 +01:00
galister 27d77ad638 clippy 2024-03-06 18:20:56 +01:00
galister 2aed560950 osc: only interactable in openOverlayCount 2024-03-06 18:20:20 +01:00
galister 1a70ea4fcc settings: notifications section rearrange 2024-03-06 18:18:27 +01:00
galister c73a01fb7f watch: save fade settings 2024-03-06 18:16:34 +01:00
galister 97fd6c1fc0 openvr: support appimage autostart 2024-03-06 18:14:33 +01:00
galister 39ce2a7c00 increase X11 screen width cap to 2560 2024-03-02 03:00:44 +01:00
galister 0030ad43eb add instructions for input group 2024-03-02 03:00:44 +01:00
galister 3006e52795 Update README.md 2024-03-02 00:11:10 +00:00
galister cfb432622e add building from source & aur 2024-03-01 22:51:56 +01:00
galister b49add2e89 reduce warnings when --no-default-features 2024-03-01 22:26:45 +01:00
galister da1936823f fix build 2024-03-01 22:14:37 +01:00
galister f42572d4cb fix more no-default-features builds 2024-03-01 22:13:06 +01:00
galister d562d4b236 fix dependency tree when --no-default-features 2024-03-01 21:43:50 +01:00
galister 75dcfc8096 fix pw required with no wayland feature 2024-03-01 21:39:54 +01:00
23 changed files with 373 additions and 205 deletions

No files matched your search

Generated
+4 -4
View File
@@ -1081,7 +1081,7 @@ version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412"
dependencies = [
"libloading 0.8.1",
"libloading 0.7.4",
]
[[package]]
@@ -2198,7 +2198,7 @@ dependencies = [
[[package]]
name = "ovr_overlay"
version = "0.0.0"
source = "git+https://github.com/galister/ovr_overlay_oyasumi#c9794ab4439242fbaa908a2c625ed47fcf173508"
source = "git+https://github.com/galister/ovr_overlay_oyasumi#376e817f9bf6438a486fbbfc6cf6ac2b8ff8ae36"
dependencies = [
"byteorder",
"derive_more",
@@ -2213,7 +2213,7 @@ dependencies = [
[[package]]
name = "ovr_overlay_sys"
version = "0.0.0"
source = "git+https://github.com/galister/ovr_overlay_oyasumi#c9794ab4439242fbaa908a2c625ed47fcf173508"
source = "git+https://github.com/galister/ovr_overlay_oyasumi#376e817f9bf6438a486fbbfc6cf6ac2b8ff8ae36"
dependencies = [
"autocxx",
"autocxx-build",
@@ -3703,7 +3703,7 @@ dependencies = [
[[package]]
name = "wlx-capture"
version = "0.2.1"
source = "git+https://github.com/galister/wlx-capture#9ba8a769378f2cd049baef947a0de8195eeae2a5"
source = "git+https://github.com/galister/wlx-capture#9c5368ef4535b7525de41d3ba5b076fb326e974c"
dependencies = [
"ashpd",
"drm-fourcc",
+1 -1
View File
@@ -43,7 +43,7 @@ strum = { version = "0.25.0", features = ["derive"] }
thiserror = "1.0.56"
vulkano = { git = "https://github.com/vulkano-rs/vulkano", rev = "94f50f1" }
vulkano-shaders = { git = "https://github.com/vulkano-rs/vulkano", rev = "94f50f1" }
wlx-capture = { git = "https://github.com/galister/wlx-capture" }
wlx-capture = { git = "https://github.com/galister/wlx-capture", default-features = false }
xdg = "2.5.2"
[features]
+9 -6
View File
@@ -18,15 +18,18 @@ We are available on either:
Questions/issues specific to WlxOverlay-S will be handled in the `wlxoverlay` chat room.
# First Setup
# Setup
Recommend grabbing [rustup](https://rustup.rs/) if you don't have it yet.
1. Grab the latest AppImage from [Releases](https://github.com/galister/wlx-overlay-s/releases).
1. `chmod +x WlxOverlay-S-*.AppImage`
1. Start Monado or SteamVR.
1. Run the overlay
Start Monado or SteamVR.
AUR package is [wlx-overlay-s-git](https://aur.archlinux.org/packages/wlx-overlay-s-git).
```sh
cargo run --release
```
You may also want to [build from source](https://github.com/galister/wlx-overlay-s/wiki/Building-from-Source).
# First Start
**If you get a screen share pop-up, check the terminal and select the screens in the order it tells you to.**
+1 -1
View File
@@ -96,7 +96,7 @@ impl NotificationManager {
let sender2 = self.tx_toast.clone();
let result = c.add_match(rule_with_eavesdrop, move |_: (), _, msg| {
if let Ok(toast) = parse_dbus(&msg) {
if let Ok(toast) = parse_dbus(msg) {
match sender2.try_send(toast) {
Ok(_) => {}
Err(e) => {
+4 -4
View File
@@ -7,12 +7,12 @@ use thiserror::Error;
use crate::backend::common::{BackendError, ColorChannel};
pub trait Affine3AConvert {
fn from_affine(affine: Affine3A) -> Self;
fn from_affine(affine: &Affine3A) -> Self;
fn to_affine(&self) -> Affine3A;
}
impl Affine3AConvert for Matrix3x4 {
fn from_affine(affine: Affine3A) -> Self {
fn from_affine(affine: &Affine3A) -> Self {
Matrix3x4([
[
affine.matrix3.x_axis.x,
@@ -46,7 +46,7 @@ impl Affine3AConvert for Matrix3x4 {
}
impl Affine3AConvert for HmdMatrix34_t {
fn from_affine(affine: Affine3A) -> Self {
fn from_affine(affine: &Affine3A) -> Self {
HmdMatrix34_t {
m: [
[
@@ -101,7 +101,7 @@ impl From<OVRError> for BackendError {
use cstr::cstr;
const STEAMVR_SECTION: &CStr = cstr!("steamvr");
const COLOR_GAIN_CSTR: [&'static CStr; 3] = [
const COLOR_GAIN_CSTR: [&CStr; 3] = [
cstr!("hmdDisplayColorGainR"),
cstr!("hmdDisplayColorGainG"),
cstr!("hmdDisplayColorGainB"),
+10 -4
View File
@@ -9,12 +9,18 @@ use crate::config_io::CONFIG_ROOT_PATH;
const APP_KEY: &str = "galister.wlxoverlay-s";
pub(super) fn install_manifest(app_mgr: &mut ApplicationsManager) -> anyhow::Result<()> {
let executable_pathbuf = std::env::current_exe()?;
let executable_path = executable_pathbuf
.to_str()
.ok_or_else(|| anyhow::anyhow!("Invalid executable path"))?;
let manifest_path = CONFIG_ROOT_PATH.join("wlx-overlay-s.vrmanifest");
let appimage_path = std::env::var("APPIMAGE");
let executable_pathbuf = std::env::current_exe()?;
let executable_path = match appimage_path {
Ok(ref path) => path,
Err(_) => executable_pathbuf
.to_str()
.ok_or_else(|| anyhow::anyhow!("Invalid executable path"))?,
};
if let Ok(true) = app_mgr.is_application_installed(APP_KEY) {
if let Ok(mut file) = File::open(&manifest_path) {
let mut buf = String::new();
+36 -22
View File
@@ -1,7 +1,7 @@
use std::{
collections::VecDeque,
sync::{
atomic::{AtomicBool, Ordering},
atomic::{AtomicBool, AtomicUsize, Ordering},
Arc,
},
time::{Duration, Instant},
@@ -46,6 +46,8 @@ pub mod manifest;
pub mod overlay;
pub mod playspace;
static FRAME_COUNTER: AtomicUsize = AtomicUsize::new(0);
pub fn openvr_uninstall() {
let app_type = EVRApplicationType::VRApplication_Overlay;
let Ok(context) = ovr_overlay::Context::init(app_type) else {
@@ -67,15 +69,15 @@ pub fn openvr_run(running: Arc<AtomicBool>) -> Result<(), BackendError> {
log::info!("Using OpenVR runtime");
let mut app_mgr = context.applications_mngr();
let mut input_mngr = context.input_mngr();
let mut system_mngr = context.system_mngr();
let mut overlay_mngr = context.overlay_mngr();
let mut settings_mngr = context.settings_mngr();
let mut input_mgr = context.input_mngr();
let mut system_mgr = context.system_mngr();
let mut overlay_mgr = context.overlay_mngr();
let mut settings_mgr = context.settings_mngr();
let mut chaperone_mgr = context.chaperone_setup_mngr();
let mut compositor_mngr = context.compositor_mngr();
let mut compositor_mgr = context.compositor_mngr();
let device_extensions_fn = |device: &PhysicalDevice| {
let names = compositor_mngr.get_vulkan_device_extensions_required(device.handle().as_raw());
let names = compositor_mgr.get_vulkan_device_extensions_required(device.handle().as_raw());
let ext = DeviceExtensions::from_iter(names.iter().map(|s| s.as_str()));
ext
};
@@ -99,17 +101,19 @@ pub fn openvr_run(running: Arc<AtomicBool>) -> Result<(), BackendError> {
notifications.run_udp();
let mut space_mover = playspace::PlayspaceMover::new();
space_mover.playspace_changed(&mut compositor_mgr, &mut chaperone_mgr);
#[cfg(feature = "osc")]
let mut osc_sender =
crate::backend::osc::OscSender::new(state.session.config.osc_out_port).ok();
state.hid_provider.set_desktop_extent(overlays.extent);
set_action_manifest(&mut input_mngr)?;
set_action_manifest(&mut input_mgr)?;
let mut input_source = OpenVrInputSource::new(&mut input_mngr)?;
let mut input_source = OpenVrInputSource::new(&mut input_mgr)?;
let Ok(refresh_rate) = system_mngr.get_tracked_device_property::<f32>(
let Ok(refresh_rate) = system_mgr.get_tracked_device_property::<f32>(
TrackedDeviceIndex::HMD,
ETrackedDeviceProperty::Prop_DisplayFrequency_Float,
) else {
@@ -135,7 +139,9 @@ pub fn openvr_run(running: Arc<AtomicBool>) -> Result<(), BackendError> {
break 'main_loop;
}
while let Some(event) = system_mngr.poll_next_event() {
let cur_frame = FRAME_COUNTER.fetch_add(1, Ordering::Relaxed);
while let Some(event) = system_mgr.poll_next_event() {
match event.event_type {
EVREventType::VREvent_Quit => {
log::warn!("Received quit event, shutting down.");
@@ -146,12 +152,17 @@ pub fn openvr_run(running: Arc<AtomicBool>) -> Result<(), BackendError> {
| EVREventType::VREvent_TrackedDeviceUpdated => {
next_device_update = Instant::now();
}
EVREventType::VREvent_SeatedZeroPoseReset
| EVREventType::VREvent_StandingZeroPoseReset
| EVREventType::VREvent_ChaperoneUniverseHasChanged => {
space_mover.playspace_changed(&mut compositor_mgr, &mut chaperone_mgr);
}
_ => {}
}
}
if next_device_update <= Instant::now() {
input_source.update_devices(&mut system_mngr, &mut state);
input_source.update_devices(&mut system_mgr, &mut state);
next_device_update = Instant::now() + Duration::from_secs(30);
}
@@ -171,9 +182,10 @@ pub fn openvr_run(running: Arc<AtomicBool>) -> Result<(), BackendError> {
continue;
};
let Some((state, backend)) = f(&mut state) else {
let Some((mut state, backend)) = f(&mut state) else {
continue;
};
state.birthframe = cur_frame;
overlays.add(OverlayData {
state,
@@ -183,13 +195,15 @@ pub fn openvr_run(running: Arc<AtomicBool>) -> Result<(), BackendError> {
}
TaskType::DropOverlay(sel) => {
if let Some(o) = overlays.mut_by_selector(&sel) {
o.destroy(&mut overlay_mngr);
overlays.remove_by_selector(&sel);
if o.state.birthframe < cur_frame {
o.destroy(&mut overlay_mgr);
overlays.remove_by_selector(&sel);
}
}
}
TaskType::System(task) => match task {
SystemTask::ColorGain(channel, value) => {
let _ = adjust_gain(&mut settings_mngr, channel, value);
let _ = adjust_gain(&mut settings_mgr, channel, value);
}
SystemTask::FixFloor => {
space_mover.fix_floor(&mut chaperone_mgr, &state.input_state);
@@ -202,7 +216,7 @@ pub fn openvr_run(running: Arc<AtomicBool>) -> Result<(), BackendError> {
}
state.input_state.pre_update();
input_source.update(&mut input_mngr, &mut system_mngr, &mut state);
input_source.update(&mut input_mgr, &mut system_mgr, &mut state);
state.input_state.post_update();
if state
@@ -231,14 +245,14 @@ pub fn openvr_run(running: Arc<AtomicBool>) -> Result<(), BackendError> {
&state.input_state.hmd,
);
if let Some(haptics) = haptics {
input_source.haptics(&mut input_mngr, idx, haptics)
input_source.haptics(&mut input_mgr, idx, haptics)
}
}
lines.update(&mut overlay_mngr, &mut state)?;
lines.update(&mut overlay_mgr, &mut state)?;
for o in overlays.iter_mut() {
o.after_input(&mut overlay_mngr, &mut state)?;
o.after_input(&mut overlay_mgr, &mut state)?;
}
#[cfg(feature = "osc")]
@@ -258,7 +272,7 @@ pub fn openvr_run(running: Arc<AtomicBool>) -> Result<(), BackendError> {
overlays
.iter_mut()
.for_each(|o| o.after_render(&mut overlay_mngr, &state.graphics));
.for_each(|o| o.after_render(&mut overlay_mgr, &state.graphics));
// chaperone
@@ -268,7 +282,7 @@ pub fn openvr_run(running: Arc<AtomicBool>) -> Result<(), BackendError> {
let mut seconds_since_vsync = 0f32;
std::thread::sleep(Duration::from_secs_f32(
if system_mngr.get_time_since_last_vsync(&mut seconds_since_vsync, &mut 0u64) {
if system_mgr.get_time_since_last_vsync(&mut seconds_since_vsync, &mut 0u64) {
(frame_time - seconds_since_vsync).max(0.0)
} else {
frame_time
+1 -1
View File
@@ -191,7 +191,7 @@ impl OverlayData<OpenVrOverlayData> {
return;
};
let transform = Matrix3x4::from_affine(self.state.transform);
let transform = Matrix3x4::from_affine(&self.state.transform);
if let Err(e) = overlay.set_transform_absolute(
handle,
+112 -40
View File
@@ -1,25 +1,34 @@
use glam::Vec3A;
use ovr_overlay::{chaperone_setup::ChaperoneSetupManager, sys::EChaperoneConfigFile};
use glam::{Affine3A, Vec3A};
use ovr_overlay::{
chaperone_setup::ChaperoneSetupManager,
compositor::CompositorManager,
sys::{EChaperoneConfigFile, ETrackingUniverseOrigin, HmdMatrix34_t},
};
use crate::{
backend::{common::OverlayContainer, input::InputState},
state::AppState,
};
use super::overlay::OpenVrOverlayData;
use super::{helpers::Affine3AConvert, overlay::OpenVrOverlayData};
struct DragData {
pose: Affine3A,
pose_inverse: Affine3A,
hand: usize,
hand_pos: Vec3A,
}
pub(super) struct PlayspaceMover {
drag_hand: Option<usize>,
offset: Vec3A,
start_position: Vec3A,
universe: ETrackingUniverseOrigin,
last: Option<DragData>,
}
impl PlayspaceMover {
pub fn new() -> Self {
Self {
drag_hand: None,
offset: Vec3A::ZERO,
start_position: Vec3A::ZERO,
universe: ETrackingUniverseOrigin::TrackingUniverseRawAndUncalibrated,
last: None,
}
}
@@ -29,16 +38,19 @@ impl PlayspaceMover {
overlays: &mut OverlayContainer<OpenVrOverlayData>,
state: &AppState,
) {
if let Some(hand) = self.drag_hand {
let pointer = &state.input_state.pointers[hand];
let universe = self.universe.clone();
if let Some(data) = self.last.as_mut() {
let pointer = &state.input_state.pointers[data.hand];
if !pointer.now.space_drag {
self.drag_hand = None;
self.last = None;
log::info!("End space drag");
return;
}
let new_hand = state.input_state.pointers[data.hand].pose.translation;
let new_hand_local = new_hand;
let hand_pos = state.input_state.pointers[hand].pose.translation;
let relative_pos = hand_pos - self.start_position;
let relative_pos = data.pose.transform_vector3a(new_hand_local - data.hand_pos);
log::info!("Space drag: {:?}", relative_pos);
overlays.iter_mut().for_each(|overlay| {
if overlay.state.grabbable {
@@ -47,13 +59,27 @@ impl PlayspaceMover {
}
});
self.offset += relative_pos;
self.apply_offset(chaperone_mgr);
let mut mat = data.pose.clone();
mat.translation += relative_pos;
//data.hand_pos = data.pose.inverse().transform_point3a(new_hand);
set_working_copy(&universe, chaperone_mgr, &mat);
chaperone_mgr.commit_working_copy(EChaperoneConfigFile::EChaperoneConfigFile_Live);
} else {
for (i, pointer) in state.input_state.pointers.iter().enumerate() {
if pointer.now.space_drag {
self.drag_hand = Some(i);
self.start_position = pointer.pose.translation;
let Some(mat) = get_working_copy(&universe, chaperone_mgr) else {
log::warn!("Can't space drag - failed to get zero pose");
return;
};
let pose_inverse = mat.inverse();
let hand_pos = pointer.pose.translation;
self.last = Some(DragData {
pose: mat,
pose_inverse,
hand: i,
hand_pos,
});
log::info!("Start space drag");
break;
}
@@ -62,35 +88,81 @@ impl PlayspaceMover {
}
pub fn reset_offset(&mut self, chaperone_mgr: &mut ChaperoneSetupManager) {
self.offset = Vec3A::ZERO;
self.apply_offset(chaperone_mgr);
set_working_copy(&self.universe, chaperone_mgr, &Affine3A::IDENTITY);
chaperone_mgr.commit_working_copy(EChaperoneConfigFile::EChaperoneConfigFile_Live);
if self.last.is_some() {
log::info!("Space drag interrupted by manual reset");
self.last = None;
}
}
pub fn fix_floor(&mut self, chaperone_mgr: &mut ChaperoneSetupManager, input: &InputState) {
let y1 = input.pointers[0].pose.translation.y;
let y2 = input.pointers[1].pose.translation.y;
self.offset.y += y1.min(y2) - 0.03;
self.apply_offset(chaperone_mgr);
}
pub fn reset(&mut self) {
self.offset = Vec3A::ZERO;
self.start_position = Vec3A::ZERO;
}
fn apply_offset(&mut self, chaperone_mgr: &mut ChaperoneSetupManager) {
let Some(mut zero_pose) =
chaperone_mgr.get_working_standing_zero_pose_to_raw_tracking_pose()
else {
log::warn!("Can't space drag - failed to get zero pose");
let Some(mut mat) = get_working_copy(&self.universe, chaperone_mgr) else {
log::warn!("Can't fix floor - failed to get zero pose");
return;
};
zero_pose.m[0][3] = self.offset.x;
zero_pose.m[1][3] = self.offset.y;
zero_pose.m[2][3] = self.offset.z;
chaperone_mgr.set_working_standing_zero_pose_to_raw_tracking_pose(&zero_pose);
mat.translation.y += y1.min(y2) - 0.03;
set_working_copy(&self.universe, chaperone_mgr, &mat);
chaperone_mgr.commit_working_copy(EChaperoneConfigFile::EChaperoneConfigFile_Live);
}
pub fn playspace_changed(
&mut self,
compositor_mgr: &mut CompositorManager,
_chaperone_mgr: &mut ChaperoneSetupManager,
) {
let new_universe = compositor_mgr.get_tracking_space();
if new_universe != self.universe {
log::info!(
"Playspace changed: {} -> {}",
universe_str(&self.universe),
universe_str(&new_universe)
);
self.universe = new_universe;
}
if self.last.is_some() {
log::info!("Space drag interrupted by external change");
self.last = None;
}
}
}
fn universe_str(universe: &ETrackingUniverseOrigin) -> &'static str {
match universe {
ETrackingUniverseOrigin::TrackingUniverseSeated => "Seated",
ETrackingUniverseOrigin::TrackingUniverseStanding => "Standing",
ETrackingUniverseOrigin::TrackingUniverseRawAndUncalibrated => "Raw",
}
}
fn get_working_copy(
universe: &ETrackingUniverseOrigin,
chaperone_mgr: &mut ChaperoneSetupManager,
) -> Option<Affine3A> {
chaperone_mgr.revert_working_copy();
let mat = match universe {
ETrackingUniverseOrigin::TrackingUniverseStanding => {
chaperone_mgr.get_working_standing_zero_pose_to_raw_tracking_pose()
}
_ => chaperone_mgr.get_working_seated_zero_pose_to_raw_tracking_pose(),
};
mat.map(|m| m.to_affine())
}
fn set_working_copy(
universe: &ETrackingUniverseOrigin,
chaperone_mgr: &mut ChaperoneSetupManager,
mat: &Affine3A,
) {
let mat = HmdMatrix34_t::from_affine(mat);
match universe {
ETrackingUniverseOrigin::TrackingUniverseStanding => {
chaperone_mgr.set_working_standing_zero_pose_to_raw_tracking_pose(&mat)
}
_ => chaperone_mgr.set_working_seated_zero_pose_to_raw_tracking_pose(&mat),
};
}
+9 -4
View File
@@ -315,9 +315,10 @@ pub fn openxr_run(running: Arc<AtomicBool>) -> Result<(), BackendError> {
continue;
};
let Some((state, backend)) = f(&mut app_state) else {
let Some((mut state, backend)) = f(&mut app_state) else {
continue;
};
state.birthframe = cur_frame;
overlays.add(OverlayData {
state,
@@ -326,9 +327,13 @@ pub fn openxr_run(running: Arc<AtomicBool>) -> Result<(), BackendError> {
});
}
TaskType::DropOverlay(sel) => {
let o = overlays.remove_by_selector(&sel);
// set for deletion after all images are done showing
delete_queue.push((o, cur_frame + 5));
if let Some(o) = overlays.mut_by_selector(&sel) {
if o.state.birthframe < cur_frame {
let o = overlays.remove_by_selector(&sel);
// set for deletion after all images are done showing
delete_queue.push((o, cur_frame + 5));
}
}
}
TaskType::System(_task) => {
// Not implemented
+5 -1
View File
@@ -65,7 +65,11 @@ impl OscSender {
match o.state.name.as_ref() {
WATCH_NAME => has_wrist = true,
KEYBOARD_NAME => has_keyboard = true,
_ => num_overlays += 1,
_ => {
if o.state.interactable {
num_overlays += 1
}
}
}
}
+2
View File
@@ -37,6 +37,7 @@ pub struct OverlayState {
pub relative_to: RelativeTo,
pub primary_pointer: Option<usize>,
pub interaction_transform: Affine2,
pub birthframe: usize,
}
impl Default for OverlayState {
@@ -60,6 +61,7 @@ impl Default for OverlayState {
transform: Affine3A::IDENTITY,
primary_pointer: None,
interaction_transform: Affine2::IDENTITY,
birthframe: 0,
}
}
}
+8 -10
View File
@@ -42,11 +42,11 @@ fn def_one() -> f32 {
1.0
}
fn def_half() -> f32 {
pub fn def_half() -> f32 {
0.5
}
fn def_point7() -> f32 {
pub fn def_point7() -> f32 {
0.7
}
@@ -165,14 +165,12 @@ where
let file_name = FILES[config_type as usize];
let maybe_override = config_io::load(file_name);
for yaml in [maybe_override.as_deref(), Some(fallback)].iter() {
if let Some(yaml_data) = yaml {
match serde_yaml::from_str::<T>(yaml_data) {
Ok(d) => return d,
Err(e) => {
error!("Failed to parse {}, falling back to defaults.", file_name);
error!("{}", e);
}
for yaml in [maybe_override.as_deref(), Some(fallback)].iter().flatten() {
match serde_yaml::from_str::<T>(yaml) {
Ok(d) => return d,
Err(e) => {
error!("Failed to parse {}, falling back to defaults.", file_name);
error!("{}", e);
}
}
}
+38 -35
View File
@@ -10,14 +10,20 @@ use ash::vk::SubmitInfo;
use smallvec::smallvec;
#[cfg(feature = "openvr")]
use vulkano::{
device::physical::PhysicalDeviceType, instance::InstanceCreateFlags,
instance::InstanceExtensions,
};
use vulkano::{device::physical::PhysicalDeviceType, instance::InstanceCreateFlags};
#[cfg(feature = "openxr")]
use {ash::vk, std::os::raw::c_void};
pub type Vert2Buf = Subbuffer<[Vert2Uv]>;
pub type IndexBuf = Subbuffer<[u16]>;
pub type LegacyPipeline = WlxPipeline<WlxPipelineLegacy>;
pub type DynamicPipeline = WlxPipeline<WlxPipelineDynamic>;
pub type LegacyPass = WlxPass<WlxPipelineLegacy>;
pub type DynamicPass = WlxPass<WlxPipelineDynamic>;
use vulkano::{
buffer::{
allocator::{SubbufferAllocator, SubbufferAllocatorCreateInfo},
@@ -47,7 +53,7 @@ use vulkano::{
Image, ImageCreateInfo, ImageLayout, ImageTiling, ImageType, ImageUsage, SampleCount,
SubresourceLayout,
},
instance::{Instance, InstanceCreateInfo},
instance::{Instance, InstanceCreateInfo, InstanceExtensions},
memory::{
allocator::{
AllocationCreateInfo, GenericMemoryAllocatorCreateInfo, MemoryAllocator,
@@ -121,8 +127,8 @@ pub struct WlxGraphics {
pub command_buffer_allocator: Arc<StandardCommandBufferAllocator>,
pub descriptor_set_allocator: Arc<StandardDescriptorSetAllocator>,
pub quad_verts: Subbuffer<[Vert2Uv]>,
pub quad_indices: Subbuffer<[u16]>,
pub quad_verts: Vert2Buf,
pub quad_indices: IndexBuf,
pub shared_shaders: RwLock<HashMap<&'static str, Arc<ShaderModule>>>,
}
@@ -477,7 +483,7 @@ impl WlxGraphics {
fn default_quad(
memory_allocator: Arc<StandardMemoryAllocator>,
) -> anyhow::Result<(Subbuffer<[Vert2Uv]>, Subbuffer<[u16]>)> {
) -> anyhow::Result<(Vert2Buf, IndexBuf)> {
let vertices = [
Vert2Uv {
in_pos: [0., 0.],
@@ -535,7 +541,7 @@ impl WlxGraphics {
y: f32,
w: f32,
h: f32,
) -> anyhow::Result<Subbuffer<[Vert2Uv]>> {
) -> anyhow::Result<Vert2Buf> {
let rw = width;
let rh = height;
@@ -707,8 +713,8 @@ impl WlxGraphics {
frag: Arc<ShaderModule>,
format: Format,
blend: Option<AttachmentBlend>,
) -> anyhow::Result<Arc<WlxPipeline<WlxPipelineLegacy>>> {
Ok(Arc::new(WlxPipeline::<WlxPipelineLegacy>::new(
) -> anyhow::Result<Arc<LegacyPipeline>> {
Ok(Arc::new(LegacyPipeline::new(
render_target,
self.clone(),
vert,
@@ -727,8 +733,8 @@ impl WlxGraphics {
blend: Option<AttachmentBlend>,
initial_layout: ImageLayout,
final_layout: ImageLayout,
) -> anyhow::Result<Arc<WlxPipeline<WlxPipelineLegacy>>> {
Ok(Arc::new(WlxPipeline::<WlxPipelineLegacy>::new_with_layout(
) -> anyhow::Result<Arc<LegacyPipeline>> {
Ok(Arc::new(LegacyPipeline::new_with_layout(
render_target,
self.clone(),
vert,
@@ -747,8 +753,8 @@ impl WlxGraphics {
frag: Arc<ShaderModule>,
format: Format,
blend: Option<AttachmentBlend>,
) -> anyhow::Result<Arc<WlxPipeline<WlxPipelineDynamic>>> {
Ok(Arc::new(WlxPipeline::<WlxPipelineDynamic>::new(
) -> anyhow::Result<Arc<DynamicPipeline>> {
Ok(Arc::new(DynamicPipeline::new(
self.clone(),
vert,
frag,
@@ -844,10 +850,7 @@ pub struct WlxCommandBuffer {
#[allow(dead_code)]
impl WlxCommandBuffer {
pub fn begin_render_pass(
&mut self,
pipeline: &WlxPipeline<WlxPipelineLegacy>,
) -> anyhow::Result<()> {
pub fn begin_render_pass(&mut self, pipeline: &LegacyPipeline) -> anyhow::Result<()> {
self.command_buffer.begin_render_pass(
RenderPassBeginInfo {
clear_values: vec![Some([0.0, 0.0, 0.0, 0.0].into())],
@@ -1034,11 +1037,11 @@ impl WlxPipeline<WlxPipelineDynamic> {
pub fn create_pass(
self: &Arc<Self>,
dimensions: [f32; 2],
vertex_buffer: Subbuffer<[Vert2Uv]>,
index_buffer: Subbuffer<[u16]>,
vertex_buffer: Vert2Buf,
index_buffer: IndexBuf,
descriptor_sets: Vec<Arc<DescriptorSet>>,
) -> anyhow::Result<WlxPass<WlxPipelineDynamic>> {
WlxPass::<WlxPipelineDynamic>::new(
) -> anyhow::Result<DynamicPass> {
DynamicPass::new(
self.clone(),
dimensions,
vertex_buffer,
@@ -1203,11 +1206,11 @@ impl WlxPipeline<WlxPipelineLegacy> {
pub fn create_pass(
self: &Arc<Self>,
dimensions: [f32; 2],
vertex_buffer: Subbuffer<[Vert2Uv]>,
index_buffer: Subbuffer<[u16]>,
vertex_buffer: Vert2Buf,
index_buffer: IndexBuf,
descriptor_sets: Vec<Arc<DescriptorSet>>,
) -> anyhow::Result<WlxPass<WlxPipelineLegacy>> {
WlxPass::<WlxPipelineLegacy>::new(
) -> anyhow::Result<LegacyPass> {
LegacyPass::new(
self.clone(),
dimensions,
vertex_buffer,
@@ -1281,18 +1284,18 @@ impl<D> WlxPipeline<D> {
#[allow(dead_code)]
pub struct WlxPass<D> {
pipeline: Arc<WlxPipeline<D>>,
vertex_buffer: Subbuffer<[Vert2Uv]>,
index_buffer: Subbuffer<[u16]>,
vertex_buffer: Vert2Buf,
index_buffer: IndexBuf,
descriptor_sets: Vec<Arc<DescriptorSet>>,
pub command_buffer: Arc<CommandBuffer>,
}
impl WlxPass<WlxPipelineLegacy> {
fn new(
pipeline: Arc<WlxPipeline<WlxPipelineLegacy>>,
pipeline: Arc<LegacyPipeline>,
dimensions: [f32; 2],
vertex_buffer: Subbuffer<[Vert2Uv]>,
index_buffer: Subbuffer<[u16]>,
vertex_buffer: Vert2Buf,
index_buffer: IndexBuf,
descriptor_sets: Vec<Arc<DescriptorSet>>,
) -> anyhow::Result<Self> {
let viewport = Viewport {
@@ -1349,10 +1352,10 @@ impl WlxPass<WlxPipelineLegacy> {
impl WlxPass<WlxPipelineDynamic> {
fn new(
pipeline: Arc<WlxPipeline<WlxPipelineDynamic>>,
pipeline: Arc<DynamicPipeline>,
dimensions: [f32; 2],
vertex_buffer: Subbuffer<[Vert2Uv]>,
index_buffer: Subbuffer<[u16]>,
vertex_buffer: Vert2Buf,
index_buffer: IndexBuf,
descriptor_sets: Vec<Arc<DescriptorSet>>,
) -> anyhow::Result<Self> {
let viewport = Viewport {
+6 -31
View File
@@ -19,7 +19,6 @@ use crate::{
overlay::RelativeTo,
},
overlays::{
mirror,
toast::Toast,
watch::{save_watch, WATCH_NAME},
},
@@ -173,7 +172,7 @@ pub struct ButtonData {
}
pub fn modular_button_init(button: &mut ModularControl, data: &ButtonData) {
button.state = Some(ModularData::Button(data.clone()));
button.state = Some(ModularData::Button(Box::new(data.clone())));
button.on_press = Some(modular_button_dn);
button.on_release = Some(modular_button_up);
button.on_scroll = Some(modular_button_scroll);
@@ -475,7 +474,7 @@ fn run_watch(data: &WatchAction, app: &mut AppState) {
app.tasks.enqueue(TaskType::Overlay(
OverlaySelector::Name(WATCH_NAME.into()),
Box::new(move |app, o| {
o.spawn_rotation = o.spawn_rotation * rot;
o.spawn_rotation *= rot;
app.session.config.watch_rot = o.spawn_rotation.into();
o.dirty = true;
}),
@@ -577,23 +576,12 @@ fn run_overlay(overlay: &OverlaySelector, action: &OverlayAction, app: &mut AppS
}
}
#[cfg(feature = "wayland")]
fn run_window(window: &Arc<str>, action: &WindowAction, app: &mut AppState) {
use crate::overlays::custom;
let selector = OverlaySelector::Name(window.clone());
match action {
WindowAction::ShowMirror => {
app.tasks.enqueue(TaskType::Overlay(
selector,
Box::new(|app, o| {
o.want_visible = !o.want_visible;
if o.recenter {
o.show_hide = o.want_visible;
o.reset(app, false);
}
}),
));
#[cfg(feature = "wayland")]
app.tasks.enqueue(TaskType::CreateOverlay(
OverlaySelector::Name(window.clone()),
Box::new({
@@ -602,22 +590,14 @@ fn run_window(window: &Arc<str>, action: &WindowAction, app: &mut AppState) {
Toast::new("Check your desktop for popup.".into(), "".into())
.with_sound(true)
.submit(app);
mirror::new_mirror(name.clone(), false, &app.session)
crate::overlays::mirror::new_mirror(name.clone(), false, &app.session)
}
}),
));
#[cfg(not(feature = "wayland"))]
log::warn!("Mirror not available without Wayland feature.");
}
WindowAction::ShowUi => {
app.tasks.enqueue(TaskType::Overlay(
selector,
Box::new(|app, o| {
o.want_visible = !o.want_visible;
if o.recenter {
o.show_hide = o.want_visible;
o.reset(app, false);
}
}),
));
app.tasks.enqueue(TaskType::CreateOverlay(
OverlaySelector::Name(window.clone()),
Box::new({
@@ -633,11 +613,6 @@ fn run_window(window: &Arc<str>, action: &WindowAction, app: &mut AppState) {
}
}
#[cfg(not(feature = "wayland"))]
fn run_action_window(_: Arc<str>, _: &WindowAction, _: &mut AppState) {
log::warn!("Cannot run Window action without Wayland feature.");
}
fn audio_thump(app: &mut AppState) {
if let Some(handle) = app.audio.get_handle() {
let wav = include_bytes!("../../res/380885.wav");
+3 -3
View File
@@ -92,13 +92,13 @@ pub fn modular_label_init(label: &mut ModularControl, content: &LabelContent) {
child: None,
}),
LabelContent::Static { text } => {
label.set_text(&text);
label.set_text(text);
None
}
};
if let Some(state) = state {
label.state = Some(ModularData::Label(state));
label.state = Some(ModularData::Label(Box::new(state)));
label.on_update = Some(label_update);
}
}
@@ -107,7 +107,7 @@ pub(super) fn label_update(control: &mut ModularControl, _: &mut (), app: &mut A
let ModularData::Label(data) = control.state.as_mut().unwrap() else {
panic!("Label control has no state");
};
match data {
match data.as_mut() {
LabelData::Battery {
device,
low_threshold,
+13 -13
View File
@@ -70,7 +70,7 @@ pub enum ModularElement {
bg_color: Arc<str>,
text: Arc<str>,
#[serde(flatten)]
data: ButtonData,
data: Box<ButtonData>,
},
/// Convenience type to save you from having to create a bunch of labels
BatteryList {
@@ -90,7 +90,7 @@ pub enum ModularElement {
bg_color: Arc<str>,
layout: ListLayout,
#[serde(flatten)]
template: OverlayListTemplate,
template: Box<OverlayListTemplate>,
},
}
@@ -107,8 +107,8 @@ pub enum ListLayout {
}
pub enum ModularData {
Label(LabelData),
Button(ButtonData),
Label(Box<LabelData>),
Button(Box<ButtonData>),
}
pub fn modular_canvas(
@@ -130,7 +130,7 @@ pub fn modular_canvas(
rect: [x, y, w, h],
bg_color,
} => {
canvas.bg_color = color_parse(&bg_color).unwrap_or(FALLBACK_COLOR);
canvas.bg_color = color_parse(bg_color).unwrap_or(FALLBACK_COLOR);
canvas.panel(*x, *y, *w, *h);
}
ModularElement::Label {
@@ -140,9 +140,9 @@ pub fn modular_canvas(
data,
} => {
canvas.font_size = *font_size;
canvas.fg_color = color_parse(&fg_color).unwrap_or(FALLBACK_COLOR);
let mut label = canvas.label(*x, *y, *w, *h, empty_str.clone());
modular_label_init(&mut label, data);
canvas.fg_color = color_parse(fg_color).unwrap_or(FALLBACK_COLOR);
let label = canvas.label(*x, *y, *w, *h, empty_str.clone());
modular_label_init(label, data);
}
ModularElement::Button {
rect: [x, y, w, h],
@@ -152,8 +152,8 @@ pub fn modular_canvas(
text,
data,
} => {
canvas.bg_color = color_parse(&bg_color).unwrap_or(FALLBACK_COLOR);
canvas.fg_color = color_parse(&fg_color).unwrap_or(FALLBACK_COLOR);
canvas.bg_color = color_parse(bg_color).unwrap_or(FALLBACK_COLOR);
canvas.fg_color = color_parse(fg_color).unwrap_or(FALLBACK_COLOR);
canvas.font_size = *font_size;
let button = canvas.button(*x, *y, *w, *h, text.clone());
modular_button_init(button, data);
@@ -177,7 +177,7 @@ pub fn modular_canvas(
ListLayout::Vertical => (*w, *h / num_buttons),
};
let fg_color = color_parse(&fg_color).unwrap_or(FALLBACK_COLOR);
let fg_color = color_parse(fg_color).unwrap_or(FALLBACK_COLOR);
canvas.font_size = *font_size;
canvas.fg_color = fg_color;
@@ -225,8 +225,8 @@ pub fn modular_canvas(
ListLayout::Vertical => (*w, *h / num_buttons),
};
canvas.bg_color = color_parse(&bg_color).unwrap_or(FALLBACK_COLOR);
canvas.fg_color = color_parse(&fg_color).unwrap_or(FALLBACK_COLOR);
canvas.bg_color = color_parse(bg_color).unwrap_or(FALLBACK_COLOR);
canvas.fg_color = color_parse(fg_color).unwrap_or(FALLBACK_COLOR);
canvas.font_size = *font_size;
for screen in state.screens.iter() {
+8 -1
View File
@@ -17,7 +17,14 @@ pub fn initialize() -> Box<dyn HidProvider> {
return Box::new(uinput);
}
log::error!("Could not create uinput provider. Keyboard/Mouse input will not work!");
log::error!("Check if you're in `input` group: `id -nG`");
log::error!("To check if you're in input group, run: id -nG");
if let Ok(user) = std::env::var("USER") {
log::error!(
"To add yourself to the input group, run: sudo usermod -aG input {}",
user
);
log::error!("After adding yourself to the input group, you will need to reboot.");
}
Box::new(DummyProvider {})
}
+34 -6
View File
@@ -42,9 +42,9 @@ struct Args {
fn main() -> Result<(), Box<dyn std::error::Error>> {
let args = Args::parse();
if let Some(log_to) = args.log_to {
if let Some(ref log_to) = args.log_to {
flexi_logger::Logger::try_with_env_or_str("info")?
.log_to_file(FileSpec::default().directory(&log_to))
.log_to_file(FileSpec::default().directory(log_to))
.log_to_stdout()
.start()?;
println!("Logging to: {}", &log_to);
@@ -72,16 +72,16 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
}
});
auto_run(running, args.openvr, args.openxr);
auto_run(running, args);
Ok(())
}
fn auto_run(running: Arc<AtomicBool>, openvr: bool, openxr: bool) {
fn auto_run(running: Arc<AtomicBool>, args: Args) {
use backend::common::BackendError;
#[cfg(feature = "openxr")]
if !openvr {
if !args_get_openvr(&args) {
use crate::backend::openxr::openxr_run;
match openxr_run(running.clone()) {
Ok(()) => return,
@@ -94,7 +94,7 @@ fn auto_run(running: Arc<AtomicBool>, openvr: bool, openxr: bool) {
}
#[cfg(feature = "openvr")]
if !openxr {
if !args_get_openxr(&args) {
use crate::backend::openvr::openvr_run;
match openvr_run(running.clone()) {
Ok(()) => return,
@@ -107,4 +107,32 @@ fn auto_run(running: Arc<AtomicBool>, openvr: bool, openxr: bool) {
}
log::error!("No more backends to try");
#[cfg(not(any(feature = "openvr", feature = "openxr")))]
compile_error!("No VR support! Enable either openvr or openxr features!");
#[cfg(not(any(feature = "wayland", feature = "x11")))]
compile_error!("No desktop support! Enable either wayland or x11 features!");
}
#[allow(dead_code)]
fn args_get_openvr(_args: &Args) -> bool {
#[cfg(feature = "openvr")]
let ret = _args.openvr;
#[cfg(not(feature = "openvr"))]
let ret = false;
ret
}
#[allow(dead_code)]
fn args_get_openxr(_args: &Args) -> bool {
#[cfg(feature = "openxr")]
let ret = _args.openxr;
#[cfg(not(feature = "openxr"))]
let ret = false;
ret
}
+35 -6
View File
@@ -16,7 +16,6 @@ use wlx_capture::{
DrmFormat, MouseMeta, WlxFrame, DRM_FORMAT_ABGR8888, DRM_FORMAT_ARGB8888,
DRM_FORMAT_XBGR8888, DRM_FORMAT_XRGB8888,
},
wayland::wayland_client::protocol::wl_output::Transform,
WlxCapture,
};
@@ -27,7 +26,7 @@ use {
std::{collections::HashMap, error::Error, f32::consts::PI, ops::Deref, path::PathBuf},
wlx_capture::{
pipewire::{pipewire_select_screen, PipewireCapture},
wayland::{WlxClient, WlxOutput},
wayland::{wayland_client::protocol::wl_output, WlxClient, WlxOutput},
wlr_dmabuf::WlrDmabufCapture,
wlr_screencopy::WlrScreencopyCapture,
},
@@ -566,14 +565,16 @@ where
interaction: Box::new(ScreenInteractionHandler::new(
vec2(output.logical_pos.0 as f32, output.logical_pos.1 as f32),
vec2(output.logical_size.0 as f32, output.logical_size.1 as f32),
output.transform,
output.transform.into(),
)),
});
let axis = Vec3::new(0., 0., 1.);
let transform = output.transform.into();
let angle = if session.config.upright_screen_fix {
match output.transform {
match transform {
Transform::_90 | Transform::Flipped90 => PI / 2.,
Transform::_180 | Transform::Flipped180 => PI,
Transform::_270 | Transform::Flipped270 => -PI / 2.,
@@ -584,7 +585,7 @@ where
};
let center = Vec2 { x: 0.5, y: 0.5 };
let interaction_transform = match output.transform {
let interaction_transform = match transform {
Transform::_90 | Transform::Flipped90 => Affine2::from_cols(
Vec2::NEG_Y * (output.size.0 as f32 / output.size.1 as f32),
Vec2::NEG_X,
@@ -797,11 +798,39 @@ where
Ok((overlays, extent))
}
#[allow(unused)]
pub enum Transform {
Normal,
_90,
_180,
_270,
Flipped90,
Flipped180,
Flipped270,
}
#[cfg(feature = "wayland")]
impl From<wl_output::Transform> for Transform {
fn from(t: wl_output::Transform) -> Transform {
match t {
wl_output::Transform::Normal => Transform::Normal,
wl_output::Transform::_90 => Transform::_90,
wl_output::Transform::_180 => Transform::_180,
wl_output::Transform::_270 => Transform::_270,
wl_output::Transform::Flipped => Transform::Flipped180,
wl_output::Transform::Flipped90 => Transform::Flipped90,
wl_output::Transform::Flipped180 => Transform::Flipped180,
wl_output::Transform::Flipped270 => Transform::Flipped270,
_ => Transform::Normal,
}
}
}
fn extent_from_res(res: (i32, i32)) -> [u32; 3] {
// screens above a certain resolution will have severe aliasing
// TODO make dynamic. maybe don't go above HMD resolution?
let w = res.0.min(1920) as u32;
let w = res.0.min(2560) as u32;
let h = (res.1 as f32 / res.0 as f32 * w as f32) as u32;
[w, h, 1]
}
+13 -3
View File
@@ -4,7 +4,9 @@ use std::path::PathBuf;
use crate::{
backend::overlay::{ui_transform, OverlayData, OverlayState, RelativeTo},
config::{def_left, def_watch_pos, def_watch_rot, load_known_yaml, ConfigType},
config::{
def_half, def_left, def_point7, def_watch_pos, def_watch_rot, load_known_yaml, ConfigType,
},
config_io,
gui::modular::{modular_canvas, ModularUiConfig},
state::{AppState, LeftRight},
@@ -78,6 +80,12 @@ pub struct WatchConf {
#[serde(default = "def_left")]
pub watch_hand: LeftRight,
#[serde(default = "def_half")]
pub watch_view_angle_min: f32,
#[serde(default = "def_point7")]
pub watch_view_angle_max: f32,
}
fn get_config_path() -> PathBuf {
@@ -87,9 +95,11 @@ fn get_config_path() -> PathBuf {
}
pub fn save_watch(app: &mut AppState) -> anyhow::Result<()> {
let conf = WatchConf {
watch_pos: app.session.config.watch_pos.clone(),
watch_rot: app.session.config.watch_rot.clone(),
watch_pos: app.session.config.watch_pos,
watch_rot: app.session.config.watch_rot,
watch_hand: app.session.config.watch_hand,
watch_view_angle_min: app.session.config.watch_view_angle_min,
watch_view_angle_max: app.session.config.watch_view_angle_max,
};
let yaml = serde_yaml::to_string(&conf)?;
+16 -7
View File
@@ -270,10 +270,13 @@ elements:
fg_color: "#ffffff"
bg_color: "#707070"
text: "Show/Hide"
click_down:
click_down: # ToggleVisible if exists, else create
- type: Overlay
target: M1
action: ToggleVisible # only fires if overlay exists
- type: Window
target: M1
action: ShowMirror
action: ShowMirror # only fires if not exists
- type: Button
rect: [185, 270, 60, 30]
@@ -311,6 +314,9 @@ elements:
bg_color: "#707070"
text: "Show/Hide"
click_down:
- type: Overlay
target: M2
action: ToggleVisible
- type: Window
target: M2
action: ShowMirror
@@ -351,6 +357,9 @@ elements:
bg_color: "#707070"
text: "Show/Hide"
click_down:
- type: Overlay
target: M3
action: ToggleVisible
- type: Window
target: M3
action: ShowMirror
@@ -509,14 +518,14 @@ elements:
bg_color: "#c0c0c0"
- type: Label
rect: [325, 480, 90, 24]
rect: [325, 490, 90, 24]
font_size: 18
fg_color: "#ffffff"
source: Static
text: Notifications
- type: Button
rect: [330, 495, 220, 30]
rect: [330, 505, 220, 30]
font_size: 12
fg_color: "#ffffff"
bg_color: "#606020"
@@ -526,7 +535,7 @@ elements:
action: ToggleNotifications
- type: Button
rect: [330, 545, 220, 30]
rect: [330, 555, 220, 30]
font_size: 12
fg_color: "#ffffff"
bg_color: "#606020"
@@ -536,11 +545,11 @@ elements:
action: ToggleNotificationSounds
- type: Panel
rect: [50, 595, 500, 1]
rect: [50, 605, 500, 1]
bg_color: "#c0c0c0"
- type: Button
rect: [330, 615, 220, 30]
rect: [330, 625, 220, 30]
font_size: 12
fg_color: "#ffffff"
bg_color: "#206060"
+5 -2
View File
@@ -18,10 +18,13 @@ elements:
bg_color: "#808040"
fg_color: "#ffffff"
text: "C"
click_up:
click_up: # destroy if exists, otherwise create
- type: Window
target: settings
action: ShowUi
action: ShowUi # only triggers if not exists
- type: Window
target: settings
action: Destroy # only triggers if exists since before current frame
# Keyboard button
- type: Button