From b045f46b1287a37afcc68f382e52f461c56b8ac2 Mon Sep 17 00:00:00 2001 From: galister <22305755+galister@users.noreply.github.com> Date: Sun, 25 Feb 2024 19:27:48 +0100 Subject: [PATCH] modular ui rework --- src/backend/common.rs | 50 +- src/backend/notifications.rs | 1 - src/backend/openvr/helpers.rs | 95 +++- src/backend/openvr/mod.rs | 17 +- src/backend/openvr/playspace.rs | 17 +- src/backend/openxr/mod.rs | 3 + src/backend/overlay.rs | 32 +- src/config.rs | 88 ++- src/gui/mod.rs | 1 + src/gui/modular/button.rs | 609 +++++++++++++++++++++ src/gui/modular/label.rs | 223 ++++++++ src/gui/modular/mod.rs | 334 ++++++++++++ src/overlays/custom.rs | 51 ++ src/overlays/keyboard.rs | 4 +- src/overlays/mod.rs | 1 + src/overlays/toast.rs | 18 +- src/overlays/watch.rs | 924 ++------------------------------ src/res/settings.yaml | 519 ++++++++++++++++++ src/res/watch.yaml | 191 ++++--- src/state.rs | 32 +- 20 files changed, 2161 insertions(+), 1049 deletions(-) create mode 100644 src/gui/modular/button.rs create mode 100644 src/gui/modular/label.rs create mode 100644 src/gui/modular/mod.rs create mode 100644 src/overlays/custom.rs create mode 100644 src/res/settings.yaml diff --git a/src/backend/common.rs b/src/backend/common.rs index 7f5b899..cdebc2f 100644 --- a/src/backend/common.rs +++ b/src/backend/common.rs @@ -9,14 +9,15 @@ use openxr as xr; use glam::{Affine3A, Vec2, Vec3A}; use idmap::IdMap; +use serde::Deserialize; use thiserror::Error; use crate::{ overlays::{ keyboard::create_keyboard, - watch::{create_watch, WATCH_NAME, WATCH_SCALE}, + watch::{create_watch, WATCH_NAME}, }, - state::AppState, + state::{AppState, ScreenMeta}, }; use super::overlay::{OverlayBackend, OverlayData, OverlayState}; @@ -56,8 +57,15 @@ where crate::overlays::screen::get_screens_x11(&app.session)? }; - let mut watch = create_watch::(app, &screens)?; - log::info!("Watch Rotation: {:?}", watch.state.spawn_rotation); + app.screens.clear(); + for screen in screens.iter() { + app.screens.push(ScreenMeta { + name: screen.state.name.clone(), + id: screen.state.id, + }); + } + + let mut watch = create_watch::(app)?; watch.state.want_visible = true; overlays.insert(watch.state.id, watch); @@ -147,13 +155,14 @@ where } // toggle watch back on if it was hidden if !any_shown && *o.state.name == *WATCH_NAME { - o.state.spawn_scale = WATCH_SCALE * app.session.config.watch_scale; + o.state.reset(app, true); } }) } } -#[derive(Clone)] +#[derive(Clone, Deserialize)] +#[serde(untagged)] pub enum OverlaySelector { Id(usize), Name(Arc), @@ -181,17 +190,30 @@ impl Ord for AppTask { } } +pub enum SystemTask { + ColorGain(ColorChannel, f32), + ResetPlayspace, + FixFloor, +} + +pub type OverlayTask = dyn FnOnce(&mut AppState, &mut OverlayState) + Send; +pub type CreateOverlayTask = + dyn FnOnce(&mut AppState) -> Option<(OverlayState, Box)> + Send; + pub enum TaskType { Global(Box), - Overlay( - OverlaySelector, - Box, - ), - CreateOverlay( - OverlaySelector, - Box Option<(OverlayState, Box)> + Send>, - ), + Overlay(OverlaySelector, Box), + CreateOverlay(OverlaySelector, Box), DropOverlay(OverlaySelector), + System(SystemTask), +} + +#[derive(Deserialize, Clone, Copy)] +pub enum ColorChannel { + R, + G, + B, + All, } pub struct TaskContainer { diff --git a/src/backend/notifications.rs b/src/backend/notifications.rs index 8e0b956..520e878 100644 --- a/src/backend/notifications.rs +++ b/src/backend/notifications.rs @@ -143,7 +143,6 @@ impl NotificationManager { continue; } }; - log::info!("Received notification message: {}", json_str); let msg = match serde_json::from_str::(json_str) { Ok(m) => m, Err(e) => { diff --git a/src/backend/openvr/helpers.rs b/src/backend/openvr/helpers.rs index ca3c3d2..8cd2940 100644 --- a/src/backend/openvr/helpers.rs +++ b/src/backend/openvr/helpers.rs @@ -1,8 +1,10 @@ +use std::ffi::CStr; + use glam::Affine3A; -use ovr_overlay::{pose::Matrix3x4, sys::HmdMatrix34_t}; +use ovr_overlay::{pose::Matrix3x4, settings::SettingsManager, sys::HmdMatrix34_t}; use thiserror::Error; -use crate::backend::common::BackendError; +use crate::backend::common::{BackendError, ColorChannel}; pub trait Affine3AConvert { fn from_affine(affine: Affine3A) -> Self; @@ -96,3 +98,92 @@ impl From for BackendError { BackendError::Fatal(anyhow::Error::new(e)) } } + +use cstr::cstr; +const STEAMVR_SECTION: &CStr = cstr!("steamvr"); +const COLOR_GAIN_CSTR: [&'static CStr; 3] = [ + cstr!("hmdDisplayColorGainR"), + cstr!("hmdDisplayColorGainG"), + cstr!("hmdDisplayColorGainB"), +]; + +pub(super) fn adjust_gain( + settings: &mut SettingsManager, + ch: ColorChannel, + delta: f32, +) -> Option<()> { + let current = [ + settings + .get_float(STEAMVR_SECTION, COLOR_GAIN_CSTR[0]) + .ok()?, + settings + .get_float(STEAMVR_SECTION, COLOR_GAIN_CSTR[1]) + .ok()?, + settings + .get_float(STEAMVR_SECTION, COLOR_GAIN_CSTR[2]) + .ok()?, + ]; + + // prevent user from turning everything black + let mut min = if current[0] + current[1] + current[2] < 0.11 { + 0.1 + } else { + 0.0 + }; + + match ch { + ColorChannel::R => { + settings + .set_float( + STEAMVR_SECTION, + COLOR_GAIN_CSTR[0], + (current[0] + delta).clamp(min, 1.0), + ) + .ok()?; + } + ColorChannel::G => { + settings + .set_float( + STEAMVR_SECTION, + COLOR_GAIN_CSTR[1], + (current[1] + delta).clamp(min, 1.0), + ) + .ok()?; + } + ColorChannel::B => { + settings + .set_float( + STEAMVR_SECTION, + COLOR_GAIN_CSTR[2], + (current[2] + delta).clamp(min, 1.0), + ) + .ok()?; + } + ColorChannel::All => { + min *= 0.3333; + settings + .set_float( + STEAMVR_SECTION, + COLOR_GAIN_CSTR[0], + (current[0] + delta).clamp(min, 1.0), + ) + .ok()?; + settings + .set_float( + STEAMVR_SECTION, + COLOR_GAIN_CSTR[1], + (current[1] + delta).clamp(min, 1.0), + ) + .ok()?; + settings + .set_float( + STEAMVR_SECTION, + COLOR_GAIN_CSTR[2], + (current[2] + delta).clamp(min, 1.0), + ) + .ok()?; + } + } + + Some(()) +} diff --git a/src/backend/openvr/mod.rs b/src/backend/openvr/mod.rs index 85e6b6b..f2abe77 100644 --- a/src/backend/openvr/mod.rs +++ b/src/backend/openvr/mod.rs @@ -20,9 +20,11 @@ use vulkano::{ use crate::{ backend::{ + common::SystemTask, input::interact, notifications::NotificationManager, openvr::{ + helpers::adjust_gain, input::{set_action_manifest, OpenVrInputSource}, lines::LinePool, manifest::{install_manifest, uninstall_manifest}, @@ -64,11 +66,11 @@ pub fn openvr_run(running: Arc) -> Result<(), BackendError> { log::info!("Using OpenVR runtime"); - let mut overlay_mngr = context.overlay_mngr(); - //let mut settings_mngr = context.settings_mngr(); 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 chaperone_mgr = context.chaperone_setup_mngr(); let mut compositor_mngr = context.compositor_mngr(); @@ -185,6 +187,17 @@ pub fn openvr_run(running: Arc) -> Result<(), BackendError> { overlays.remove_by_selector(&sel); } } + TaskType::System(task) => match task { + SystemTask::ColorGain(channel, value) => { + let _ = adjust_gain(&mut settings_mngr, channel, value); + } + SystemTask::FixFloor => { + space_mover.fix_floor(&mut chaperone_mgr, &state.input_state); + } + SystemTask::ResetPlayspace => { + space_mover.reset_offset(&mut chaperone_mgr); + } + }, } } diff --git a/src/backend/openvr/playspace.rs b/src/backend/openvr/playspace.rs index 7b914ee..982617f 100644 --- a/src/backend/openvr/playspace.rs +++ b/src/backend/openvr/playspace.rs @@ -1,7 +1,10 @@ use glam::Vec3A; use ovr_overlay::{chaperone_setup::ChaperoneSetupManager, sys::EChaperoneConfigFile}; -use crate::{backend::common::OverlayContainer, state::AppState}; +use crate::{ + backend::{common::OverlayContainer, input::InputState}, + state::AppState, +}; use super::overlay::OpenVrOverlayData; @@ -58,6 +61,18 @@ impl PlayspaceMover { } } + pub fn reset_offset(&mut self, chaperone_mgr: &mut ChaperoneSetupManager) { + self.offset = Vec3A::ZERO; + self.apply_offset(chaperone_mgr); + } + + 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; diff --git a/src/backend/openxr/mod.rs b/src/backend/openxr/mod.rs index 4183d14..6dcd02f 100644 --- a/src/backend/openxr/mod.rs +++ b/src/backend/openxr/mod.rs @@ -330,6 +330,9 @@ pub fn openxr_run(running: Arc) -> Result<(), BackendError> { // set for deletion after all images are done showing delete_queue.push((o, cur_frame + 5)); } + TaskType::System(_task) => { + // Not implemented + } } } diff --git a/src/backend/overlay.rs b/src/backend/overlay.rs index 7e8c6bb..2956d5d 100644 --- a/src/backend/overlay.rs +++ b/src/backend/overlay.rs @@ -30,6 +30,7 @@ pub struct OverlayState { pub alpha: f32, pub transform: Affine3A, pub saved_point: Option, + pub saved_scale: Option, pub spawn_scale: f32, // aka width pub spawn_point: Vec3A, pub spawn_rotation: Quat, @@ -52,6 +53,7 @@ impl Default for OverlayState { alpha: 1.0, relative_to: RelativeTo::None, saved_point: None, + saved_scale: None, spawn_scale: 1.0, spawn_point: Vec3A::NEG_Z, spawn_rotation: Quat::IDENTITY, @@ -97,10 +99,11 @@ impl OverlayState { pub fn auto_movement(&mut self, app: &mut AppState) { if let Some(parent) = self.parent_transform(app) { + let scale = self.saved_scale.unwrap_or(self.spawn_scale); let point = self.saved_point.unwrap_or(self.spawn_point); self.transform = parent * Affine3A::from_scale_rotation_translation( - Vec3::ONE * self.spawn_scale, + Vec3::ONE * scale, self.spawn_rotation, point.into(), ); @@ -110,13 +113,12 @@ impl OverlayState { } pub fn reset(&mut self, app: &mut AppState, hard_reset: bool) { - let scale = if hard_reset { + if hard_reset { self.saved_point = None; - self.spawn_scale - } else { - self.transform.x_axis.length() - }; + self.saved_scale = None; + } + let scale = self.saved_scale.unwrap_or(self.spawn_scale); let point = self.saved_point.unwrap_or(self.spawn_point); let translation = app.input_state.hmd.transform_point3a(point); @@ -284,17 +286,9 @@ impl InteractionHandler for SplitOverlayBackend { pub fn ui_transform(extent: &[u32; 2]) -> Affine2 { let center = Vec2 { x: 0.5, y: 0.5 }; - if extent[1] > extent[0] { - Affine2::from_cols( - Vec2::X * (extent[1] as f32 / extent[0] as f32), - Vec2::NEG_Y, - center, - ) - } else { - Affine2::from_cols( - Vec2::X, - Vec2::NEG_Y * (extent[0] as f32 / extent[1] as f32), - center, - ) - } + Affine2::from_cols( + Vec2::X, + Vec2::NEG_Y * (extent[0] as f32 / extent[1] as f32), + center, + ) } diff --git a/src/config.rs b/src/config.rs index cafc1e6..696fb90 100644 --- a/src/config.rs +++ b/src/config.rs @@ -2,13 +2,26 @@ use std::sync::Arc; use crate::config_io; use crate::config_io::get_conf_d_path; +use crate::gui::modular::ModularUiConfig; use crate::load_with_fallback; -use crate::overlays::keyboard; -use crate::overlays::watch::WatchConfig; +use crate::state::LeftRight; +use anyhow::bail; use log::error; use serde::Deserialize; use serde::Serialize; +pub fn def_watch_pos() -> [f32; 3] { + [-0.03, -0.01, 0.125] +} + +pub fn def_watch_rot() -> [f32; 4] { + [-0.7071066, 0.0007963618, 0.7071066, 0.0] +} + +pub fn def_left() -> LeftRight { + LeftRight::Left +} + pub fn def_pw_tokens() -> Vec<(String, String)> { Vec::new() } @@ -51,9 +64,24 @@ fn def_auto() -> Arc { #[derive(Deserialize, Serialize)] pub struct GeneralConfig { + #[serde(default = "def_watch_pos")] + pub watch_pos: [f32; 3], + + #[serde(default = "def_watch_rot")] + pub watch_rot: [f32; 4], + + #[serde(default = "def_left")] + pub watch_hand: LeftRight, + #[serde(default = "def_click_freeze_time_ms")] pub click_freeze_time_ms: u32, + #[serde(default = "def_true")] + pub notifications_enabled: bool, + + #[serde(default = "def_true")] + pub notifications_sound_enabled: bool, + #[serde(default = "def_true")] pub keyboard_sound_enabled: bool, @@ -63,15 +91,15 @@ pub struct GeneralConfig { #[serde(default = "def_one")] pub desktop_view_scale: f32, - #[serde(default = "def_one")] - pub watch_scale: f32, - #[serde(default = "def_half")] pub watch_view_angle_min: f32, #[serde(default = "def_point7")] pub watch_view_angle_max: f32, + #[serde(default = "def_one")] + pub long_press_duration: f32, + #[serde(default = "def_pw_tokens")] pub pw_tokens: Vec<(String, String)>, @@ -110,18 +138,54 @@ impl GeneralConfig { fn post_load(&self) { GeneralConfig::sanitize_range("keyboard_scale", self.keyboard_scale, 0.0, 5.0); GeneralConfig::sanitize_range("desktop_view_scale", self.desktop_view_scale, 0.0, 5.0); - GeneralConfig::sanitize_range("watch_scale", self.watch_scale, 0.0, 5.0); } } -pub fn load_keyboard() -> keyboard::Layout { - let yaml_data = load_with_fallback!("keyboard.yaml", "res/keyboard.yaml"); - serde_yaml::from_str(&yaml_data).expect("Failed to parse keyboard.yaml") +const FALLBACKS: [&str; 3] = [ + include_str!("res/keyboard.yaml"), + include_str!("res/watch.yaml"), + include_str!("res/settings.yaml"), +]; + +const FILES: [&str; 3] = ["keyboard.yaml", "watch.yaml", "settings.yaml"]; + +#[derive(Clone, Copy)] +#[repr(usize)] +pub enum ConfigType { + Keyboard, + Watch, + Settings, } -pub fn load_watch() -> WatchConfig { - let yaml_data = load_with_fallback!("watch.yaml", "res/watch.yaml"); - serde_yaml::from_str(&yaml_data).expect("Failed to parse watch.yaml") +pub fn load_known_yaml(config_type: ConfigType) -> T +where + T: for<'de> Deserialize<'de>, +{ + let fallback = FALLBACKS[config_type as usize]; + 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::(yaml_data) { + Ok(d) => return d, + Err(e) => { + error!("Failed to parse {}, falling back to defaults.", file_name); + error!("{}", e); + } + } + } + } + // can only get here if internal fallback is broken + panic!("No usable config found."); +} + +pub fn load_custom_ui(name: &str) -> anyhow::Result { + let filename = format!("{}.yaml", name); + let Some(yaml_data) = config_io::load(&filename) else { + bail!("Could not read file at {}", &filename); + }; + Ok(serde_yaml::from_str(&yaml_data)?) } pub fn load_general() -> GeneralConfig { diff --git a/src/gui/mod.rs b/src/gui/mod.rs index ff9601f..ce6767d 100644 --- a/src/gui/mod.rs +++ b/src/gui/mod.rs @@ -20,6 +20,7 @@ use crate::{ }; pub mod font; +pub mod modular; const RES_DIVIDER: usize = 4; diff --git a/src/gui/modular/button.rs b/src/gui/modular/button.rs new file mode 100644 index 0000000..4d18009 --- /dev/null +++ b/src/gui/modular/button.rs @@ -0,0 +1,609 @@ +use std::{ + f32::consts::PI, + io::Cursor, + ops::Add, + process::{self, Child}, + sync::Arc, + time::{Duration, Instant}, +}; + +use glam::{Quat, Vec3A}; +use rodio::{Decoder, Source}; +use serde::Deserialize; + +use crate::{ + backend::{ + common::{ColorChannel, OverlaySelector, SystemTask, TaskType}, + input::PointerMode, + overlay::RelativeTo, + }, + overlays::{ + mirror, + toast::Toast, + watch::{save_watch, WATCH_NAME}, + }, + state::AppState, +}; + +use super::{ExecArgs, ModularControl, ModularData}; + +#[derive(Deserialize, Clone, Copy)] +pub enum ViewAngleKind { + /// The cosine of the angle at which the watch becomes fully transparent + MinOpacity, + /// The cosine of the angle at which the watch becomes fully opaque + MaxOpacity, +} + +#[derive(Deserialize, Clone, Copy)] +pub enum Axis { + X, + Y, + Z, +} + +#[derive(Deserialize, Clone)] +pub enum SystemAction { + PlayspaceResetOffset, + PlayspaceFixFloor, + RecalculateExtent, + PersistConfig, +} + +#[derive(Deserialize, Clone)] +pub enum WatchAction { + /// Hide the watch until Show/Hide binding is used + Hide, + /// Switch the watch to the opposite controller + SwitchHands, + /// Change the fade behavior of the watch + ViewAngle { + kind: ViewAngleKind, + delta: f32, + }, + Rotation { + axis: Axis, + delta: f32, + }, + Position { + axis: Axis, + delta: f32, + }, +} + +#[derive(Deserialize, Clone)] +pub enum OverlayAction { + /// Reset the overlay to be in front of the HMD with its original scale + Reset, + /// Toggle the visibility of the overlay + ToggleVisible, + /// Toggle the ability to grab and recenter the overlay + ToggleImmovable, + /// Toggle the ability of the overlay to reacto to laser pointer + ToggleInteraction, + /// Change the opacity of the overlay + Opacity { delta: f32 }, +} + +#[derive(Deserialize, Clone)] +pub enum WindowAction { + /// Create a new mirror window, or show/hide an existing one + ShowMirror, + /// Create a new UI window, or show/hide an existing one + ShowUi, + /// Destroy a previously created window, if it exists + Destroy, +} + +#[derive(Deserialize, Clone)] +#[serde(tag = "type")] +pub enum ButtonAction { + Exec { + command: ExecArgs, + toast: Option>, + }, + Watch { + action: WatchAction, + }, + Overlay { + target: OverlaySelector, + action: OverlayAction, + }, + Window { + target: Arc, + action: WindowAction, + }, + Toast { + message: Arc, + body: Option>, + seconds: Option, + }, + ColorAdjust { + channel: ColorChannel, + delta: f32, + }, + System { + action: SystemAction, + }, +} + +pub(super) struct PressData { + last_down: Instant, + last_mode: PointerMode, + child: Option, +} +impl Clone for PressData { + fn clone(&self) -> Self { + Self { + last_down: self.last_down, + last_mode: self.last_mode, + child: None, + } + } +} +impl Default for PressData { + fn default() -> Self { + Self { + last_down: Instant::now(), + last_mode: PointerMode::Left, + child: None, + } + } +} + +#[derive(Deserialize, Default, Clone)] +pub struct ButtonData { + #[serde(skip)] + pub(super) press: PressData, + + pub(super) click_down: Option>, + pub(super) click_up: Option>, + pub(super) long_click_up: Option>, + pub(super) right_down: Option>, + pub(super) right_up: Option>, + pub(super) long_right_up: Option>, + pub(super) middle_down: Option>, + pub(super) middle_up: Option>, + pub(super) long_middle_up: Option>, + pub(super) scroll_down: Option>, + pub(super) scroll_up: Option>, +} + +pub fn modular_button_init(button: &mut ModularControl, data: &ButtonData) { + button.state = Some(ModularData::Button(data.clone())); + button.on_press = Some(modular_button_dn); + button.on_release = Some(modular_button_up); + button.on_scroll = Some(modular_button_scroll); +} + +fn modular_button_dn( + button: &mut ModularControl, + _: &mut (), + app: &mut AppState, + mode: PointerMode, +) { + // want panic + let ModularData::Button(data) = button.state.as_mut().unwrap() else { + panic!("modular_button_dn: button state is not Button"); + }; + + data.press.last_down = Instant::now(); + data.press.last_mode = mode; + + let actions = match mode { + PointerMode::Left => data.click_down.as_ref(), + PointerMode::Right => data.right_down.as_ref(), + PointerMode::Middle => data.middle_down.as_ref(), + _ => None, + }; + + if let Some(actions) = actions { + for action in actions { + handle_action(action, &mut data.press, app); + } + } +} + +fn modular_button_up(button: &mut ModularControl, _: &mut (), app: &mut AppState) { + // want panic + let ModularData::Button(data) = button.state.as_mut().unwrap() else { + panic!("modular_button_up: button state is not Button"); + }; + + let now = Instant::now(); + let duration = now - data.press.last_down; + let long_press = duration.as_secs_f32() > app.session.config.long_press_duration; + + let actions = match data.press.last_mode { + PointerMode::Left => { + if long_press { + data.long_click_up.as_ref() + } else { + data.click_up.as_ref() + } + } + PointerMode::Right => { + if long_press { + data.long_right_up.as_ref() + } else { + data.right_up.as_ref() + } + } + PointerMode::Middle => { + if long_press { + data.long_middle_up.as_ref() + } else { + data.middle_up.as_ref() + } + } + _ => None, + }; + + if let Some(actions) = actions { + for action in actions { + handle_action(action, &mut data.press, app); + } + } +} + +fn modular_button_scroll(button: &mut ModularControl, _: &mut (), app: &mut AppState, delta: f32) { + // want panic + let ModularData::Button(data) = button.state.as_mut().unwrap() else { + panic!("modular_button_scroll: button state is not Button"); + }; + + let actions = if delta < 0.0 { + data.scroll_down.as_ref() + } else { + data.scroll_up.as_ref() + }; + + if let Some(actions) = actions { + for action in actions { + handle_action(action, &mut data.press, app); + } + } +} + +fn handle_action(action: &ButtonAction, press: &mut PressData, app: &mut AppState) { + match action { + ButtonAction::Exec { command, toast } => run_exec(command, toast, press, app), + ButtonAction::Watch { action } => run_watch(action, app), + ButtonAction::Overlay { target, action } => run_overlay(target, action, app), + ButtonAction::Window { target, action } => run_window(target, action, app), + ButtonAction::Toast { + message, + body, + seconds, + } => { + Toast::new(message.clone(), body.clone().unwrap_or_else(|| "".into())) + .with_timeout(seconds.unwrap_or(5.)) + .submit(app); + } + ButtonAction::ColorAdjust { channel, delta } => { + let channel = *channel; + let delta = *delta; + app.tasks + .enqueue(TaskType::System(SystemTask::ColorGain(channel, delta))); + } + ButtonAction::System { action } => run_system(action, app), + } +} + +fn run_system(action: &SystemAction, app: &mut AppState) { + match action { + SystemAction::PlayspaceResetOffset => { + app.tasks + .enqueue(TaskType::System(SystemTask::ResetPlayspace)); + } + SystemAction::PlayspaceFixFloor => { + let now = Instant::now(); + let sec = Duration::from_secs(1); + for i in 0..5 { + let at = now.add(i * sec); + let display = 5 - i; + Toast::new( + format!("Fixing floor in {}", display).into(), + "Place either controller on the floor.".into(), + ) + .with_timeout(1.0) + .submit_at(app, at); + } + app.tasks + .enqueue_at(TaskType::System(SystemTask::FixFloor), now.add(5 * sec)); + } + SystemAction::RecalculateExtent => { + todo!() + } + SystemAction::PersistConfig => { + if let Err(e) = save_watch(app) { + log::error!("Failed to save watch config: {:?}", e); + }; + } + } +} + +fn run_exec(args: &ExecArgs, toast: &Option>, press: &mut PressData, app: &mut AppState) { + if let Some(proc) = press.child.as_mut() { + match proc.try_wait() { + Ok(Some(code)) => { + if !code.success() { + log::error!("Child process exited with code: {}", code); + } + press.child = None; + } + Ok(None) => { + log::warn!("Unable to launch child process: previous child not exited yet"); + return; + } + Err(e) => { + press.child = None; + log::error!("Error checking child process: {:?}", e); + } + } + } + let args = args.iter().map(|s| s.as_ref()).collect::>(); + match process::Command::new(args[0]).args(&args[1..]).spawn() { + Ok(proc) => { + press.child = Some(proc); + if let Some(toast) = toast.as_ref() { + Toast::new(toast.clone(), "".into()).submit(app); + } + } + Err(e) => { + log::error!("Failed to spawn process {:?}: {:?}", args, e); + } + }; +} + +fn run_watch(data: &WatchAction, app: &mut AppState) { + match data { + WatchAction::Hide => { + app.tasks.enqueue(TaskType::Overlay( + OverlaySelector::Name(WATCH_NAME.into()), + Box::new(|app, o| { + if o.saved_scale.is_none() { + o.saved_scale = Some(o.spawn_scale); + o.want_visible = false; + o.saved_scale = Some(0.0); + Toast::new( + "Watch hidden".into(), + "Use show/hide binding to restore.".into(), + ) + .with_timeout(3.) + .submit(app); + } else { + o.want_visible = true; + o.saved_scale = None; + Toast::new("Watch restored".into(), "".into()).submit(app); + } + }), + )); + audio_thump(app); + } + WatchAction::SwitchHands => { + app.tasks.enqueue(TaskType::Overlay( + OverlaySelector::Name(WATCH_NAME.into()), + Box::new(|app, o| { + if let RelativeTo::Hand(0) = o.relative_to { + o.relative_to = RelativeTo::Hand(1); + o.spawn_rotation = Quat::from_slice(&app.session.config.watch_rot) + * Quat::from_rotation_x(PI) + * Quat::from_rotation_z(PI); + o.spawn_point = Vec3A::from_slice(&app.session.config.watch_pos); + o.spawn_point.x *= -1.; + } else { + o.relative_to = RelativeTo::Hand(0); + o.spawn_rotation = Quat::from_slice(&app.session.config.watch_rot); + o.spawn_point = Vec3A::from_slice(&app.session.config.watch_pos); + } + o.dirty = true; + Toast::new("Watch switched".into(), "Check your other hand".into()) + .with_timeout(3.) + .submit(app); + }), + )); + audio_thump(app); + } + WatchAction::ViewAngle { kind, delta } => match kind { + ViewAngleKind::MinOpacity => { + let diff = (app.session.config.watch_view_angle_max + - app.session.config.watch_view_angle_min) + + delta; + + app.session.config.watch_view_angle_min = (app.session.config.watch_view_angle_max + - diff) + .clamp(0.0, app.session.config.watch_view_angle_max - 0.05); + } + ViewAngleKind::MaxOpacity => { + let diff = app.session.config.watch_view_angle_max + - app.session.config.watch_view_angle_min; + + app.session.config.watch_view_angle_max = + (app.session.config.watch_view_angle_max + delta).clamp(0.05, 1.0); + + app.session.config.watch_view_angle_min = (app.session.config.watch_view_angle_max + - diff) + .clamp(0.0, app.session.config.watch_view_angle_max - 0.05); + } + }, + WatchAction::Rotation { axis, delta } => { + let rot = match axis { + Axis::X => Quat::from_rotation_x(delta.to_radians()), + Axis::Y => Quat::from_rotation_y(delta.to_radians()), + Axis::Z => Quat::from_rotation_z(delta.to_radians()), + }; + app.tasks.enqueue(TaskType::Overlay( + OverlaySelector::Name(WATCH_NAME.into()), + Box::new(move |app, o| { + o.spawn_rotation = o.spawn_rotation * rot; + app.session.config.watch_rot = o.spawn_rotation.into(); + o.dirty = true; + }), + )); + } + WatchAction::Position { axis, delta } => { + let delta = *delta; + let axis = match axis { + Axis::X => 0, + Axis::Y => 1, + Axis::Z => 2, + }; + app.tasks.enqueue(TaskType::Overlay( + OverlaySelector::Name(WATCH_NAME.into()), + Box::new(move |app, o| { + o.spawn_point[axis] += delta; + app.session.config.watch_pos = o.spawn_point.into(); + o.dirty = true; + }), + )); + } + } +} + +fn run_overlay(overlay: &OverlaySelector, action: &OverlayAction, app: &mut AppState) { + match action { + OverlayAction::Reset => { + app.tasks.enqueue(TaskType::Overlay( + overlay.clone(), + Box::new(|app, o| { + o.reset(app, true); + Toast::new(format!("{} has been reset!", o.name).into(), "".into()).submit(app); + }), + )); + } + OverlayAction::ToggleVisible => { + app.tasks.enqueue(TaskType::Overlay( + overlay.clone(), + Box::new(|app, o| { + o.want_visible = !o.want_visible; + if o.recenter { + o.show_hide = o.want_visible; + o.reset(app, false); + } + }), + )); + } + OverlayAction::ToggleImmovable => { + app.tasks.enqueue(TaskType::Overlay( + overlay.clone(), + Box::new(|app, o| { + o.recenter = !o.recenter; + o.grabbable = o.recenter; + o.show_hide = o.recenter; + if !o.recenter { + Toast::new( + format!("{} is now locked in place!", o.name).into(), + "".into(), + ) + .submit(app); + } else { + Toast::new(format!("{} is now unlocked!", o.name).into(), "".into()) + .submit(app); + } + }), + )); + audio_thump(app); + } + OverlayAction::ToggleInteraction => { + app.tasks.enqueue(TaskType::Overlay( + overlay.clone(), + Box::new(|app, o| { + o.interactable = !o.interactable; + if !o.interactable { + Toast::new( + format!("{} is now non-interactable!", o.name).into(), + "".into(), + ) + .submit(app); + } else { + Toast::new(format!("{} is now interactable!", o.name).into(), "".into()) + .submit(app); + } + }), + )); + audio_thump(app); + } + OverlayAction::Opacity { delta } => { + let delta = *delta; + app.tasks.enqueue(TaskType::Overlay( + overlay.clone(), + Box::new(move |_, o| { + o.alpha = (o.alpha + delta).clamp(0.1, 1.0); + o.dirty = true; + log::debug!("{}: alpha {}", o.name, o.alpha); + }), + )); + } + } +} + +#[cfg(feature = "wayland")] +fn run_window(window: &Arc, 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); + } + }), + )); + app.tasks.enqueue(TaskType::CreateOverlay( + OverlaySelector::Name(window.clone()), + Box::new({ + let name = window.clone(); + move |app| { + Toast::new("Check your desktop for popup.".into(), "".into()) + .with_sound(true) + .submit(app); + mirror::new_mirror(name.clone(), false, &app.session) + } + }), + )); + } + 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({ + let name = window.clone(); + move |app| custom::create_custom(app, name) + }), + )); + } + WindowAction::Destroy => { + app.tasks + .enqueue(TaskType::DropOverlay(OverlaySelector::Name(window.clone()))); + } + } +} + +#[cfg(not(feature = "wayland"))] +fn run_action_window(_: Arc, _: &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"); + let cursor = Cursor::new(wav); + let source = Decoder::new_wav(cursor).unwrap(); + let _ = handle.play_raw(source.convert_samples()); + } +} diff --git a/src/gui/modular/label.rs b/src/gui/modular/label.rs new file mode 100644 index 0000000..6d06b3f --- /dev/null +++ b/src/gui/modular/label.rs @@ -0,0 +1,223 @@ +use chrono::Local; +use chrono_tz::Tz; +use glam::Vec3; +use smallvec::SmallVec; +use std::{ + io::Read, + process::{self, Stdio}, + sync::Arc, + time::Instant, +}; + +use crate::{gui::modular::FALLBACK_COLOR, state::AppState}; + +use serde::Deserialize; + +use super::{color_parse_or_default, ExecArgs, ModularControl, ModularData}; + +#[derive(Deserialize)] +#[serde(tag = "source")] +pub enum LabelContent { + Static { + text: Arc, + }, + Exec { + exec: ExecArgs, + interval: f32, + }, + Clock { + format: Arc, + timezone: Option>, + }, + Battery { + device: usize, + low_threshold: f32, + low_color: Arc, + charging_color: Arc, + }, +} + +pub enum LabelData { + Battery { + device: usize, + low_threshold: f32, + normal_color: Vec3, + low_color: Vec3, + charging_color: Vec3, + }, + Clock { + format: Arc, + timezone: Option, + }, + Exec { + last_exec: Instant, + interval: f32, + exec: Vec>, + child: Option, + }, +} + +pub fn modular_label_init(label: &mut ModularControl, content: &LabelContent) { + let state = match content { + LabelContent::Battery { + device, + low_threshold, + low_color, + charging_color, + } => Some(LabelData::Battery { + device: *device, + low_threshold: *low_threshold, + normal_color: label.fg_color, + low_color: color_parse_or_default(low_color), + charging_color: color_parse_or_default(charging_color), + }), + LabelContent::Clock { format, timezone } => { + let tz: Option = timezone.as_ref().map(|tz| { + tz.parse().unwrap_or_else(|_| { + log::error!("Failed to parse timezone '{}'", &tz); + label.set_fg_color(FALLBACK_COLOR); + Tz::UTC + }) + }); + + Some(LabelData::Clock { + format: format.clone(), + timezone: tz, + }) + } + LabelContent::Exec { exec, interval } => Some(LabelData::Exec { + last_exec: Instant::now(), + interval: *interval, + exec: exec.clone(), + child: None, + }), + LabelContent::Static { text } => { + label.set_text(&text); + None + } + }; + + if let Some(state) = state { + label.state = Some(ModularData::Label(state)); + label.on_update = Some(label_update); + } +} + +pub(super) fn label_update(control: &mut ModularControl, _: &mut (), app: &mut AppState) { + let ModularData::Label(data) = control.state.as_mut().unwrap() else { + panic!("Label control has no state"); + }; + match data { + LabelData::Battery { + device, + low_threshold, + normal_color, + low_color, + charging_color, + } => { + let device = app.input_state.devices.get(*device); + + let tags = ["", "H", "L", "R", "T"]; + + if let Some(device) = device { + let (text, color) = device + .soc + .map(|soc| { + let text = format!( + "{}{}", + tags[device.role as usize], + (soc * 100.).min(99.) as u32 + ); + let color = if device.charging { + *charging_color + } else if soc < *low_threshold { + *low_color + } else { + *normal_color + }; + (text, color) + }) + .unwrap_or_else(|| ("".into(), Vec3::ZERO)); + + control.set_text(&text); + control.set_fg_color(color); + } else { + control.set_text(""); + } + } + LabelData::Clock { format, timezone } => { + let format = format.clone(); + if let Some(tz) = timezone { + let date = Local::now().with_timezone(tz); + control.set_text(&format!("{}", &date.format(&format))); + } else { + let date = Local::now(); + control.set_text(&format!("{}", &date.format(&format))); + } + } + LabelData::Exec { + last_exec, + interval, + exec, + child, + } => { + if let Some(mut proc) = child.take() { + match proc.try_wait() { + Ok(Some(code)) => { + if !code.success() { + log::error!("Child process exited with code: {}", code); + } else { + if let Some(mut stdout) = proc.stdout.take() { + let mut buf = String::new(); + if stdout.read_to_string(&mut buf).is_ok() { + control.set_text(&buf); + } else { + log::error!("Failed to read stdout for child process"); + return; + } + return; + } + log::error!("No stdout for child process"); + return; + } + } + Ok(None) => { + *child = Some(proc); + // not exited yet + return; + } + Err(e) => { + *child = None; + log::error!("Error checking child process: {:?}", e); + return; + } + } + } + + if Instant::now() + .saturating_duration_since(*last_exec) + .as_secs_f32() + > *interval + { + *last_exec = Instant::now(); + let args = exec + .iter() + .map(|s| s.as_ref()) + .collect::>(); + + match process::Command::new(args[0]) + .args(&args[1..]) + .stdout(Stdio::piped()) + .spawn() + { + Ok(proc) => { + *child = Some(proc); + } + Err(e) => { + log::error!("Failed to spawn process {:?}: {:?}", args, e); + } + }; + } + } + } +} diff --git a/src/gui/modular/mod.rs b/src/gui/modular/mod.rs new file mode 100644 index 0000000..f87d7be --- /dev/null +++ b/src/gui/modular/mod.rs @@ -0,0 +1,334 @@ +pub mod button; +pub mod label; +//pub mod slider; + +use std::sync::Arc; + +use glam::Vec3; +use serde::Deserialize; + +use crate::{backend::common::OverlaySelector, state::AppState}; + +use self::{ + button::{modular_button_init, ButtonAction, ButtonData, OverlayAction}, + label::{modular_label_init, LabelContent, LabelData}, +}; + +use super::{color_parse, Canvas, CanvasBuilder, Control}; + +type ModularControl = Control<(), ModularData>; +type ExecArgs = Vec>; + +const FALLBACK_COLOR: Vec3 = Vec3 { + x: 1., + y: 0., + z: 1., +}; + +#[derive(Deserialize)] +pub struct ModularUiConfig { + pub width: f32, + pub size: [u32; 2], + pub spawn_pos: Option<[f32; 3]>, + pub elements: Vec, +} + +#[derive(Deserialize)] +pub struct OverlayListTemplate { + click_down: Option, + click_up: Option, + long_click_up: Option, + right_down: Option, + right_up: Option, + long_right_up: Option, + middle_down: Option, + middle_up: Option, + long_middle_up: Option, + scroll_down: Option, + scroll_up: Option, +} + +#[allow(dead_code)] +#[derive(Deserialize)] +#[serde(tag = "type")] +pub enum ModularElement { + Panel { + rect: [f32; 4], + bg_color: Arc, + }, + Label { + rect: [f32; 4], + font_size: isize, + fg_color: Arc, + #[serde(flatten)] + data: LabelContent, + }, + Button { + rect: [f32; 4], + font_size: isize, + fg_color: Arc, + bg_color: Arc, + text: Arc, + #[serde(flatten)] + data: ButtonData, + }, + /// Convenience type to save you from having to create a bunch of labels + BatteryList { + rect: [f32; 4], + font_size: isize, + fg_color: Arc, + fg_color_low: Arc, + fg_color_charging: Arc, + low_threshold: f32, + num_devices: usize, + layout: ListLayout, + }, + OverlayList { + rect: [f32; 4], + font_size: isize, + fg_color: Arc, + bg_color: Arc, + layout: ListLayout, + #[serde(flatten)] + template: OverlayListTemplate, + }, +} + +#[derive(Deserialize, Clone)] +pub enum ButtonFunc { + HideWatch, + SwitchWatchHand, +} + +#[derive(Deserialize)] +pub enum ListLayout { + Horizontal, + Vertical, +} + +pub enum ModularData { + Label(LabelData), + Button(ButtonData), +} + +pub fn modular_canvas( + size: &[u32; 2], + elements: &[ModularElement], + state: &AppState, +) -> anyhow::Result> { + let mut canvas = CanvasBuilder::new( + size[0] as _, + size[1] as _, + state.graphics.clone(), + state.format, + (), + )?; + let empty_str: Arc = Arc::from(""); + for elem in elements.iter() { + match elem { + ModularElement::Panel { + rect: [x, y, w, h], + bg_color, + } => { + canvas.bg_color = color_parse(&bg_color).unwrap_or(FALLBACK_COLOR); + canvas.panel(*x, *y, *w, *h); + } + ModularElement::Label { + rect: [x, y, w, h], + font_size, + fg_color, + 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); + } + ModularElement::Button { + rect: [x, y, w, h], + font_size, + bg_color, + fg_color, + 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.font_size = *font_size; + let button = canvas.button(*x, *y, *w, *h, text.clone()); + modular_button_init(button, data); + } + ModularElement::BatteryList { + rect: [x, y, w, h], + font_size, + fg_color, + fg_color_low, + fg_color_charging, + low_threshold, + num_devices, + layout, + } => { + let num_buttons = *num_devices as f32; + let mut button_x = *x; + let mut button_y = *y; + let low_threshold = low_threshold * 0.01; + let (button_w, button_h) = match layout { + ListLayout::Horizontal => (*w / num_buttons, *h), + ListLayout::Vertical => (*w, *h / num_buttons), + }; + + let fg_color = color_parse(&fg_color).unwrap_or(FALLBACK_COLOR); + canvas.font_size = *font_size; + canvas.fg_color = fg_color; + + for i in 0..*num_devices { + let label = canvas.label_centered( + button_x + 2., + button_y + 2., + button_w - 4., + button_h - 4., + empty_str.clone(), + ); + modular_label_init( + label, + &LabelContent::Battery { + device: i, + low_threshold, + low_color: fg_color_low.clone(), + charging_color: fg_color_charging.clone(), + }, + ); + + button_x += match layout { + ListLayout::Horizontal => button_w, + ListLayout::Vertical => 0., + }; + button_y += match layout { + ListLayout::Horizontal => 0., + ListLayout::Vertical => button_h, + }; + } + } + ModularElement::OverlayList { + rect: [x, y, w, h], + font_size, + fg_color, + bg_color, + layout, + template, + } => { + let num_buttons = state.screens.len() as f32; + let mut button_x = *x; + let mut button_y = *y; + let (button_w, button_h) = match layout { + ListLayout::Horizontal => (*w / num_buttons, *h), + 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.font_size = *font_size; + + for screen in state.screens.iter() { + let button = canvas.button( + button_x + 2., + button_y + 2., + button_w - 4., + button_h - 4., + screen.name.clone(), + ); + + // cursed + let data = ButtonData { + click_down: template.click_down.as_ref().map(|f| { + vec![ButtonAction::Overlay { + target: OverlaySelector::Id(screen.id), + action: f.clone(), + }] + }), + click_up: template.click_up.as_ref().map(|f| { + vec![ButtonAction::Overlay { + target: OverlaySelector::Id(screen.id), + action: f.clone(), + }] + }), + long_click_up: template.long_click_up.as_ref().map(|f| { + vec![ButtonAction::Overlay { + target: OverlaySelector::Id(screen.id), + action: f.clone(), + }] + }), + right_down: template.right_down.as_ref().map(|f| { + vec![ButtonAction::Overlay { + target: OverlaySelector::Id(screen.id), + action: f.clone(), + }] + }), + right_up: template.right_up.as_ref().map(|f| { + vec![ButtonAction::Overlay { + target: OverlaySelector::Id(screen.id), + action: f.clone(), + }] + }), + long_right_up: template.long_right_up.as_ref().map(|f| { + vec![ButtonAction::Overlay { + target: OverlaySelector::Id(screen.id), + action: f.clone(), + }] + }), + middle_down: template.middle_down.as_ref().map(|f| { + vec![ButtonAction::Overlay { + target: OverlaySelector::Id(screen.id), + action: f.clone(), + }] + }), + middle_up: template.middle_up.as_ref().map(|f| { + vec![ButtonAction::Overlay { + target: OverlaySelector::Id(screen.id), + action: f.clone(), + }] + }), + long_middle_up: template.long_middle_up.as_ref().map(|f| { + vec![ButtonAction::Overlay { + target: OverlaySelector::Id(screen.id), + action: f.clone(), + }] + }), + scroll_down: template.scroll_down.as_ref().map(|f| { + vec![ButtonAction::Overlay { + target: OverlaySelector::Id(screen.id), + action: f.clone(), + }] + }), + scroll_up: template.scroll_up.as_ref().map(|f| { + vec![ButtonAction::Overlay { + target: OverlaySelector::Id(screen.id), + action: f.clone(), + }] + }), + ..Default::default() + }; + + modular_button_init(button, &data); + + button_x += match layout { + ListLayout::Horizontal => button_w, + ListLayout::Vertical => 0., + }; + button_y += match layout { + ListLayout::Horizontal => 0., + ListLayout::Vertical => button_h, + }; + } + } + } + } + Ok(canvas.build()) +} + +pub fn color_parse_or_default(color: &str) -> Vec3 { + color_parse(color).unwrap_or_else(|e| { + log::error!("Failed to parse color '{}': {}", color, e); + FALLBACK_COLOR + }) +} diff --git a/src/overlays/custom.rs b/src/overlays/custom.rs new file mode 100644 index 0000000..4a773d6 --- /dev/null +++ b/src/overlays/custom.rs @@ -0,0 +1,51 @@ +use std::sync::Arc; + +use glam::Vec3A; + +use crate::{ + backend::overlay::{ui_transform, OverlayBackend, OverlayState}, + config::{load_custom_ui, load_known_yaml, ConfigType}, + gui::modular::{modular_canvas, ModularUiConfig}, + state::AppState, +}; + +const SETTINGS_NAME: &str = "settings"; + +pub fn create_custom( + state: &AppState, + name: Arc, +) -> Option<(OverlayState, Box)> { + let config = if &*name == SETTINGS_NAME { + load_known_yaml::(ConfigType::Settings) + } else { + match load_custom_ui(&name) { + Ok(config) => config, + Err(e) => { + log::error!("Failed to load custom UI config for {}: {:?}", name, e); + return None; + } + } + }; + + let canvas = match modular_canvas(&config.size, &config.elements, state) { + Ok(canvas) => canvas, + Err(e) => { + log::error!("Failed to create canvas for {}: {:?}", name, e); + return None; + } + }; + + let state = OverlayState { + name: name.clone(), + want_visible: true, + interactable: true, + grabbable: true, + spawn_scale: config.width, + spawn_point: Vec3A::from_array(config.spawn_pos.unwrap_or([0., 0., -0.5])), + interaction_transform: ui_transform(&config.size), + ..Default::default() + }; + let backend = Box::new(canvas); + + Some((state, backend)) +} diff --git a/src/overlays/keyboard.rs b/src/overlays/keyboard.rs index 6911572..ccd3fc4 100644 --- a/src/overlays/keyboard.rs +++ b/src/overlays/keyboard.rs @@ -10,7 +10,7 @@ use crate::{ input::PointerMode, overlay::{OverlayData, OverlayState}, }, - config, + config::{self, ConfigType}, gui::{color_parse, CanvasBuilder, Control}, hid::{KeyModifier, VirtualKey, ALT, CTRL, KEYS_TO_MODS, META, SHIFT, SUPER}, state::AppState, @@ -268,7 +268,7 @@ pub struct Layout { impl Layout { fn load_from_disk() -> Layout { - let mut layout = config::load_keyboard(); + let mut layout = config::load_known_yaml::(ConfigType::Keyboard); layout.post_load(); layout } diff --git a/src/overlays/mod.rs b/src/overlays/mod.rs index 4d89225..38cfcd4 100644 --- a/src/overlays/mod.rs +++ b/src/overlays/mod.rs @@ -1,3 +1,4 @@ +pub mod custom; pub mod keyboard; #[cfg(feature = "wayland")] pub mod mirror; diff --git a/src/overlays/toast.rs b/src/overlays/toast.rs index fb7d591..aae17c7 100644 --- a/src/overlays/toast.rs +++ b/src/overlays/toast.rs @@ -2,6 +2,7 @@ use std::{ io::Cursor, ops::Add, sync::{atomic::AtomicUsize, Arc}, + time::Instant, }; use rodio::{Decoder, Source}; @@ -55,19 +56,24 @@ impl Toast { self } pub fn submit(self, app: &mut AppState) { + self.submit_at(app, Instant::now()); + } + pub fn submit_at(self, app: &mut AppState, instant: Instant) { let auto_increment = AUTO_INCREMENT.fetch_add(1, std::sync::atomic::Ordering::Relaxed); let name: Arc = format!("toast-{}", auto_increment).into(); let selector = OverlaySelector::Name(name.clone()); - let destroy_at = - std::time::Instant::now().add(std::time::Duration::from_secs_f32(self.timeout)); + let destroy_at = instant.add(std::time::Duration::from_secs_f32(self.timeout)); let has_sound = self.sound; - app.tasks.enqueue(TaskType::CreateOverlay( - selector.clone(), - Box::new(move |app| new_toast(self, name, app)), - )); + app.tasks.enqueue_at( + TaskType::CreateOverlay( + selector.clone(), + Box::new(move |app| new_toast(self, name, app)), + ), + instant, + ); app.tasks .enqueue_at(TaskType::DropOverlay(selector), destroy_at); diff --git a/src/overlays/watch.rs b/src/overlays/watch.rs index dc7cde6..eb5dff2 100644 --- a/src/overlays/watch.rs +++ b/src/overlays/watch.rs @@ -1,907 +1,49 @@ -use std::{ - f32::consts::PI, - io::{Cursor, Read}, - process::{self, Stdio}, - sync::Arc, - time::Instant, -}; - -use chrono::Local; -use chrono_tz::Tz; -use glam::{Quat, Vec3, Vec3A}; -use rodio::{Decoder, Source}; -use serde::Deserialize; +use glam::{Quat, Vec3A}; +use serde::{Deserialize, Serialize}; +use std::path::PathBuf; use crate::{ - backend::{ - common::{OverlaySelector, TaskType}, - input::PointerMode, - overlay::{ui_transform, OverlayData, OverlayState, RelativeTo}, - }, - config::load_watch, - gui::{color_parse, CanvasBuilder, Control}, - state::AppState, -}; - -use super::{keyboard::KEYBOARD_NAME, toast::Toast}; - -const FALLBACK_COLOR: Vec3 = Vec3 { - x: 1., - y: 0., - z: 1., + backend::overlay::{ui_transform, OverlayData, OverlayState, RelativeTo}, + config::{def_left, def_watch_pos, def_watch_rot, load_known_yaml, ConfigType}, + config_io, + gui::modular::{modular_canvas, ModularUiConfig}, + state::{AppState, LeftRight}, }; pub const WATCH_NAME: &str = "watch"; -pub const WATCH_SCALE: f32 = 0.11; -pub fn create_watch( - state: &AppState, - screens: &[OverlayData], -) -> anyhow::Result> +pub fn create_watch(state: &AppState) -> anyhow::Result> where O: Default, { - let config = load_watch(); + let config = load_known_yaml::(ConfigType::Watch); - let mut canvas = CanvasBuilder::new( - config.watch_size[0] as _, - config.watch_size[1] as _, - state.graphics.clone(), - state.format, - (), - )?; - let empty_str: Arc = Arc::from(""); + let canvas = modular_canvas(&config.size, &config.elements, state)?; - for elem in config.watch_elements.into_iter() { - match elem { - WatchElement::Panel { - rect: [x, y, w, h], - bg_color, - } => { - canvas.bg_color = color_parse(&bg_color).unwrap_or(FALLBACK_COLOR); - canvas.panel(x, y, w, h); - } - WatchElement::Label { - rect: [x, y, w, h], - font_size, - fg_color, - text, - } => { - canvas.font_size = font_size; - canvas.fg_color = color_parse(&fg_color).unwrap_or(FALLBACK_COLOR); - canvas.label(x, y, w, h, text); - } - WatchElement::Clock { - rect: [x, y, w, h], - font_size, - fg_color, - format, - timezone, - } => { - canvas.font_size = font_size; - canvas.fg_color = color_parse(&fg_color).unwrap_or(FALLBACK_COLOR); - - let tz: Option = timezone.map(|tz| { - tz.parse().unwrap_or_else(|_| { - log::error!("Failed to parse timezone '{}'", &tz); - canvas.fg_color = FALLBACK_COLOR; - Tz::UTC - }) - }); - - let label = canvas.label(x, y, w, h, empty_str.clone()); - label.state = Some(ElemState::Clock { - timezone: tz, - format, - }); - label.on_update = Some(clock_update); - } - WatchElement::ExecLabel { - rect: [x, y, w, h], - font_size, - fg_color, - exec, - interval, - } => { - canvas.font_size = font_size; - canvas.fg_color = color_parse(&fg_color).unwrap_or(FALLBACK_COLOR); - let label = canvas.label(x, y, w, h, empty_str.clone()); - label.state = Some(ElemState::AutoExec { - last_exec: Instant::now(), - interval, - exec, - child: None, - }); - label.on_update = Some(exec_label_update); - } - WatchElement::ExecButton { - rect: [x, y, w, h], - font_size, - bg_color, - fg_color, - exec, - text, - } => { - 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()); - button.state = Some(ElemState::ExecButton { exec, child: None }); - button.on_press = Some(exec_button); - } - WatchElement::FuncButton { - rect: [x, y, w, h], - font_size, - bg_color, - fg_color, - func, - func_right, - func_middle, - text, - } => { - 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()); - button.state = Some(ElemState::FuncButton { - func, - func_right, - func_middle, - }); - button.on_press = Some(btn_func_dn); - } - WatchElement::Batteries { - rect: [x, y, w, h], - font_size, - num_devices, - normal_fg_color, - low_fg_color, - charging_fg_color, - low_threshold, - layout, - .. - } => { - let num_buttons = num_devices as f32; - let mut button_x = x; - let mut button_y = y; - let (button_w, button_h) = match layout { - ListLayout::Horizontal => (w / num_buttons, h), - ListLayout::Vertical => (w, h / num_buttons), - }; - - let fg_color = color_parse(&normal_fg_color).unwrap_or(FALLBACK_COLOR); - let fg_color_low = color_parse(&low_fg_color).unwrap_or(FALLBACK_COLOR); - let fg_color_charging = color_parse(&charging_fg_color).unwrap_or(FALLBACK_COLOR); - canvas.font_size = font_size; - canvas.fg_color = fg_color; - - for i in 0..num_devices { - let label = canvas.label_centered( - button_x + 2., - button_y + 2., - button_w - 4., - button_h - 4., - empty_str.clone(), - ); - label.state = Some(ElemState::Battery { - device: i as _, - low_threshold: low_threshold * 0.01, - fg_color, - fg_color_low, - fg_color_charging, - }); - label.on_update = Some(battery_update); - - button_x += match layout { - ListLayout::Horizontal => button_w, - ListLayout::Vertical => 0., - }; - button_y += match layout { - ListLayout::Horizontal => 0., - ListLayout::Vertical => button_h, - }; - } - } - WatchElement::KeyboardButton { - rect: [x, y, w, h], - font_size, - fg_color, - bg_color, - text, - } => { - 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 keyboard = canvas.button(x, y, w, h, text); - keyboard.state = Some(ElemState::OverlayButton { - pressed_at: Instant::now(), - mode: PointerMode::Left, - overlay: OverlaySelector::Name(KEYBOARD_NAME.into()), - }); - keyboard.on_press = Some(overlay_button_dn); - keyboard.on_release = Some(overlay_button_up); - keyboard.on_scroll = Some(overlay_button_scroll); - } - WatchElement::OverlayList { - rect: [x, y, w, h], - font_size, - fg_color, - bg_color, - layout, - } => { - let num_buttons = screens.len() as f32; - let mut button_x = x; - let mut button_y = y; - let (button_w, button_h) = match layout { - ListLayout::Horizontal => (w / num_buttons, h), - 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.font_size = font_size; - - for screen in screens.iter() { - let button = canvas.button( - button_x + 2., - button_y + 2., - button_w - 4., - button_h - 4., - screen.state.name.clone(), - ); - button.state = Some(ElemState::OverlayButton { - pressed_at: Instant::now(), - mode: PointerMode::Left, - overlay: OverlaySelector::Id(screen.state.id), - }); - - button.on_press = Some(overlay_button_dn); - button.on_release = Some(overlay_button_up); - button.on_scroll = Some(overlay_button_scroll); - - button_x += match layout { - ListLayout::Horizontal => button_w, - ListLayout::Vertical => 0., - }; - button_y += match layout { - ListLayout::Horizontal => 0., - ListLayout::Vertical => button_h, - }; - } - } - #[cfg(feature = "wayland")] - WatchElement::MirrorButton { - rect: [x, y, w, h], - font_size, - bg_color, - fg_color, - text, - name, - show_hide, - } => { - 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()); - button.state = Some(ElemState::Mirror { name, show_hide }); - button.on_press = Some(btn_mirror_dn); - button.on_scroll = Some(overlay_button_scroll); - } - } - } - - let relative_to = RelativeTo::Hand(state.session.watch_hand); + let relative_to = RelativeTo::Hand(state.session.config.watch_hand as usize); Ok(OverlayData { state: OverlayState { name: WATCH_NAME.into(), want_visible: true, interactable: true, - spawn_scale: WATCH_SCALE * state.session.config.watch_scale, - spawn_point: state.session.watch_pos.into(), - spawn_rotation: state.session.watch_rot, - interaction_transform: ui_transform(&config.watch_size), + spawn_scale: config.width, + spawn_point: Vec3A::from_slice(&state.session.config.watch_pos), + spawn_rotation: Quat::from_slice(&state.session.config.watch_rot), + interaction_transform: ui_transform(&config.size), relative_to, ..Default::default() }, - backend: Box::new(canvas.build()), + backend: Box::new(canvas), ..Default::default() }) } -enum ElemState { - Battery { - device: usize, - low_threshold: f32, - fg_color: Vec3, - fg_color_low: Vec3, - fg_color_charging: Vec3, - }, - Clock { - timezone: Option, - format: Arc, - }, - AutoExec { - last_exec: Instant, - interval: f32, - exec: Vec>, - child: Option, - }, - OverlayButton { - pressed_at: Instant, - mode: PointerMode, - overlay: OverlaySelector, - }, - ExecButton { - exec: Vec>, - child: Option, - }, - FuncButton { - func: ButtonFunc, - func_right: Option, - func_middle: Option, - }, - #[cfg(feature = "wayland")] - Mirror { name: Arc, show_hide: bool }, -} - -#[cfg(feature = "wayland")] -fn btn_mirror_dn( - control: &mut Control<(), ElemState>, - _: &mut (), - app: &mut AppState, - mode: PointerMode, -) { - let ElemState::Mirror { name, show_hide } = control.state.as_ref().unwrap() - // want panic - else { - log::error!("Mirror state not found"); - return; - }; - - let selector = OverlaySelector::Name(name.clone()); - - match mode { - PointerMode::Left => { - app.tasks.enqueue(TaskType::Overlay( - selector.clone(), - Box::new(|_app, o| { - o.want_visible = !o.want_visible; - }), - )); - - app.tasks.enqueue(TaskType::CreateOverlay( - selector, - Box::new({ - let name = name.clone(); - let show_hide = *show_hide; - move |app| super::mirror::new_mirror(name.clone(), show_hide, &app.session) - }), - )); - } - PointerMode::Right => { - app.tasks.enqueue(TaskType::Overlay( - selector, - Box::new(|_app, o| { - o.grabbable = !o.grabbable; - o.interactable = o.grabbable; - }), - )); - } - PointerMode::Middle => { - app.tasks.enqueue(TaskType::DropOverlay(selector)); - } - _ => {} - } -} - -fn audio_thump(app: &mut AppState) { - if let Some(handle) = app.audio.get_handle() { - let wav = include_bytes!("../res/380885.wav"); - let cursor = Cursor::new(wav); - let source = Decoder::new_wav(cursor).unwrap(); - let _ = handle.play_raw(source.convert_samples()); - } -} - -fn btn_func_dn( - control: &mut Control<(), ElemState>, - _: &mut (), - app: &mut AppState, - mode: PointerMode, -) { - let ElemState::FuncButton { - func, - func_right, - func_middle, - } = control.state.as_ref().unwrap() - // want panic - else { - log::error!("FuncButton state not found"); - return; - }; - - let func = match mode { - PointerMode::Left => func, - PointerMode::Right => func_right.as_ref().unwrap_or(func), - PointerMode::Middle => func_middle.as_ref().unwrap_or(func), - _ => return, - }; - - match func { - ButtonFunc::HideWatch => { - app.tasks.enqueue(TaskType::Overlay( - OverlaySelector::Name(WATCH_NAME.into()), - Box::new(|app, o| { - o.want_visible = false; - o.spawn_scale = 0.0; - Toast::new( - "Watch hidden".into(), - "Use show/hide button to restore.".into(), - ) - .with_timeout(3.) - .submit(app); - }), - )); - audio_thump(app); - } - ButtonFunc::SwitchWatchHand => { - app.tasks.enqueue(TaskType::Overlay( - OverlaySelector::Name(WATCH_NAME.into()), - Box::new(|app, o| { - if let RelativeTo::Hand(0) = o.relative_to { - o.relative_to = RelativeTo::Hand(1); - o.spawn_rotation = app.session.watch_rot - * Quat::from_rotation_x(PI) - * Quat::from_rotation_z(PI); - o.spawn_point = app.session.watch_pos.into(); - o.spawn_point.x *= -1.; - } else { - o.relative_to = RelativeTo::Hand(0); - o.spawn_rotation = app.session.watch_rot; - o.spawn_point = app.session.watch_pos.into(); - } - Toast::new("Watch switched".into(), "Check your other hand".into()) - .with_timeout(3.) - .submit(app); - }), - )); - audio_thump(app); - } - } -} - -fn battery_update(control: &mut Control<(), ElemState>, _: &mut (), app: &mut AppState) { - let ElemState::Battery { - device, - low_threshold, - fg_color, - fg_color_low, - fg_color_charging, - } = control.state.as_ref().unwrap() - // want panic - else { - return; - }; - let device = app.input_state.devices.get(*device); - - let tags = ["", "H", "L", "R", "T"]; - - if let Some(device) = device { - let (text, color) = device - .soc - .map(|soc| { - let text = format!( - "{}{}", - tags[device.role as usize], - (soc * 100.).min(99.) as u32 - ); - let color = if device.charging { - *fg_color_charging - } else if soc < *low_threshold { - *fg_color_low - } else { - *fg_color - }; - (text, color) - }) - .unwrap_or_else(|| ("".into(), Vec3::ZERO)); - - control.set_text(&text); - control.set_fg_color(color); - } else { - control.set_text(""); - } -} - -fn exec_button( - control: &mut Control<(), ElemState>, - _: &mut (), - _: &mut AppState, - _mode: PointerMode, -) { - let ElemState::ExecButton { - exec, - ref mut child, - .. - } = control.state.as_mut().unwrap() - // want panic - else { - log::error!("ExecButton state not found"); - return; - }; - if let Some(proc) = child { - match proc.try_wait() { - Ok(Some(code)) => { - if !code.success() { - log::error!("Child process exited with code: {}", code); - } - *child = None; - } - Ok(None) => { - log::warn!("Unable to launch child process: previous child not exited yet"); - return; - } - Err(e) => { - *child = None; - log::error!("Error checking child process: {:?}", e); - } - } - } - let args = exec.iter().map(|s| s.as_ref()).collect::>(); - match process::Command::new(args[0]).args(&args[1..]).spawn() { - Ok(proc) => { - *child = Some(proc); - } - Err(e) => { - log::error!("Failed to spawn process {:?}: {:?}", args, e); - } - }; -} - -fn exec_label_update(control: &mut Control<(), ElemState>, _: &mut (), _: &mut AppState) { - let ElemState::AutoExec { - ref mut last_exec, - interval, - exec, - ref mut child, - } = control.state.as_mut().unwrap() - // want panic - else { - log::error!("AutoExec state not found"); - return; - }; - - if let Some(mut proc) = child.take() { - match proc.try_wait() { - Ok(Some(code)) => { - if !code.success() { - log::error!("Child process exited with code: {}", code); - } else { - if let Some(mut stdout) = proc.stdout.take() { - let mut buf = String::new(); - if stdout.read_to_string(&mut buf).is_ok() { - control.set_text(&buf); - } else { - log::error!("Failed to read stdout for child process"); - return; - } - return; - } - log::error!("No stdout for child process"); - return; - } - } - Ok(None) => { - *child = Some(proc); - // not exited yet - return; - } - Err(e) => { - *child = None; - log::error!("Error checking child process: {:?}", e); - return; - } - } - } - - if Instant::now() - .saturating_duration_since(*last_exec) - .as_secs_f32() - > *interval - { - *last_exec = Instant::now(); - let args = exec.iter().map(|s| s.as_ref()).collect::>(); - - match process::Command::new(args[0]) - .args(&args[1..]) - .stdout(Stdio::piped()) - .spawn() - { - Ok(proc) => { - *child = Some(proc); - } - Err(e) => { - log::error!("Failed to spawn process {:?}: {:?}", args, e); - } - }; - } -} - -fn clock_update(control: &mut Control<(), ElemState>, _: &mut (), _: &mut AppState) { - // want panic - let ElemState::Clock { timezone, format } = control.state.as_ref().unwrap() else { - log::error!("Clock state not found"); - return; - }; - - if let Some(tz) = timezone { - let date = Local::now().with_timezone(tz); - control.set_text(&format!("{}", &date.format(format))); - } else { - let date = Local::now(); - control.set_text(&format!("{}", &date.format(format))); - } -} - -fn overlay_button_scroll( - control: &mut Control<(), ElemState>, - _: &mut (), - app: &mut AppState, - delta: f32, -) { - // want panic - let overlay = match &mut control.state.as_mut().unwrap() { - ElemState::OverlayButton { overlay, .. } => overlay.clone(), - ElemState::Mirror { name, .. } => OverlaySelector::Name(name.clone()), - _ => { - log::error!("OverlayButton state not found"); - return; - } - }; - - if delta > 0. { - app.tasks.enqueue(TaskType::Overlay( - overlay, - Box::new(|_, o| { - o.alpha = (o.alpha + 0.025).min(1.); - o.dirty = true; - log::debug!("{}: alpha {}", o.name, o.alpha); - }), - )); - } else { - app.tasks.enqueue(TaskType::Overlay( - overlay, - Box::new(|_, o| { - o.alpha = (o.alpha - 0.025).max(0.1); - o.dirty = true; - log::debug!("{}: alpha {}", o.name, o.alpha); - }), - )); - } -} - -fn overlay_button_dn( - control: &mut Control<(), ElemState>, - _: &mut (), - _: &mut AppState, - ptr_mode: PointerMode, -) { - let ElemState::OverlayButton { - ref mut pressed_at, - ref mut mode, - .. - } = control.state.as_mut().unwrap() - // want panic - else { - log::error!("OverlayButton state not found"); - return; - }; - *pressed_at = Instant::now(); - *mode = ptr_mode; -} - -fn overlay_button_up(control: &mut Control<(), ElemState>, _: &mut (), app: &mut AppState) { - let ElemState::OverlayButton { - pressed_at, - mode, - overlay, - } = control.state.as_ref().unwrap() - // want panic - else { - log::error!("OverlayButton state not found"); - return; - }; - - if Instant::now() - .saturating_duration_since(*pressed_at) - .as_millis() - < 2000 - { - match mode { - PointerMode::Left => { - app.tasks.enqueue(TaskType::Overlay( - overlay.clone(), - Box::new(|app, o| { - o.want_visible = !o.want_visible; - if o.recenter { - o.show_hide = o.want_visible; - o.reset(app, false); - } - }), - )); - } - PointerMode::Right => { - app.tasks.enqueue(TaskType::Overlay( - overlay.clone(), - Box::new(|app, o| { - o.recenter = !o.recenter; - o.grabbable = o.recenter; - o.show_hide = o.recenter; - if !o.recenter { - Toast::new( - format!("{} is now locked in place!", o.name).into(), - "Right-click again to toggle.".into(), - ) - .submit(app); - } else { - Toast::new(format!("{} is now unlocked!", o.name).into(), "".into()) - .submit(app); - } - }), - )); - audio_thump(app); - } - PointerMode::Middle => { - app.tasks.enqueue(TaskType::Overlay( - overlay.clone(), - Box::new(|app, o| { - o.interactable = !o.interactable; - if !o.interactable { - Toast::new( - format!("{} is now non-interactable!", o.name).into(), - "Middle-click again to toggle.".into(), - ) - .submit(app); - } else { - Toast::new( - format!("{} is now interactable!", o.name).into(), - "".into(), - ) - .submit(app); - } - }), - )); - audio_thump(app); - } - _ => {} - } - } else { - app.tasks.enqueue(TaskType::Overlay( - overlay.clone(), - Box::new(|app, o| { - o.reset(app, true); - }), - )); - } -} - -#[derive(Deserialize)] -pub struct WatchConfig { - watch_hand: LeftRight, - watch_size: [u32; 2], - watch_elements: Vec, -} - -#[allow(dead_code)] -#[derive(Deserialize)] -#[serde(tag = "type")] -enum WatchElement { - Panel { - rect: [f32; 4], - bg_color: Arc, - }, - Label { - rect: [f32; 4], - font_size: isize, - fg_color: Arc, - text: Arc, - }, - Clock { - rect: [f32; 4], - font_size: isize, - fg_color: Arc, - format: Arc, - timezone: Option>, - }, - ExecLabel { - rect: [f32; 4], - font_size: isize, - fg_color: Arc, - exec: Vec>, - interval: f32, - }, - ExecButton { - rect: [f32; 4], - font_size: isize, - bg_color: Arc, - fg_color: Arc, - exec: Vec>, - text: Arc, - }, - Batteries { - rect: [f32; 4], - font_size: isize, - low_threshold: f32, - num_devices: u16, - normal_fg_color: Arc, - normal_bg_color: Arc, - low_fg_color: Arc, - low_bg_color: Arc, - charging_fg_color: Arc, - charging_bg_color: Arc, - layout: ListLayout, - }, - KeyboardButton { - rect: [f32; 4], - font_size: isize, - fg_color: Arc, - bg_color: Arc, - text: Arc, - }, - OverlayList { - rect: [f32; 4], - font_size: isize, - fg_color: Arc, - bg_color: Arc, - layout: ListLayout, - }, - FuncButton { - rect: [f32; 4], - font_size: isize, - bg_color: Arc, - fg_color: Arc, - func: ButtonFunc, - func_right: Option, - func_middle: Option, - text: Arc, - }, - #[cfg(feature = "wayland")] - MirrorButton { - rect: [f32; 4], - font_size: isize, - bg_color: Arc, - fg_color: Arc, - name: Arc, - text: Arc, - show_hide: bool, - }, -} - -#[derive(Deserialize)] -enum ButtonFunc { - HideWatch, - SwitchWatchHand, -} - -#[derive(Deserialize)] -enum ListLayout { - Horizontal, - Vertical, -} - -#[derive(Deserialize)] -enum LeftRight { - Left, - Right, -} - pub fn watch_fade(app: &mut AppState, watch: &mut OverlayData) where D: Default, { - if watch.state.spawn_scale < f32::EPSILON { + if watch.state.saved_scale.is_some_and(|s| s < f32::EPSILON) { watch.state.want_visible = false; return; } @@ -925,3 +67,33 @@ where watch.state.alpha = watch.state.alpha.clamp(0., 1.); } } + +#[derive(Deserialize, Serialize)] +pub struct WatchConf { + #[serde(default = "def_watch_pos")] + pub watch_pos: [f32; 3], + + #[serde(default = "def_watch_rot")] + pub watch_rot: [f32; 4], + + #[serde(default = "def_left")] + pub watch_hand: LeftRight, +} + +fn get_config_path() -> PathBuf { + let mut path = config_io::get_conf_d_path(); + path.push("watch_state.yaml"); + path +} +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_hand: app.session.config.watch_hand, + }; + + let yaml = serde_yaml::to_string(&conf)?; + std::fs::write(get_config_path(), yaml)?; + + Ok(()) +} diff --git a/src/res/settings.yaml b/src/res/settings.yaml new file mode 100644 index 0000000..b07f085 --- /dev/null +++ b/src/res/settings.yaml @@ -0,0 +1,519 @@ +# lookng to make changes? +# drop me in ~/.config/wlxoverlay.settings.yaml +# +# i will likely change this later. edit with that in mind + +width: 0.3 + +size: [600, 520] + +# +X: right, +Y: up, +Z: back +spawn_pos: [0, -0.1, -0.5] + +elements: + - type: Panel + rect: [0, 0, 600, 800] + bg_color: "#102030" + + - type: Label + rect: [15, 35, 600, 70] + font_size: 24 + fg_color: "#ffffff" + source: Static + text: Settings + + - type: Button + rect: [560, 0, 40, 40] + font_size: 16 + bg_color: "#880000" + fg_color: "#ffffff" + text: X + click_down: + - type: Window + target: "settings" + action: Destroy + + - type: Panel + rect: [50, 53, 500, 1] + bg_color: "#c0c0c0" + + ####### Watch Section ####### + + - type: Label + rect: [15, 85, 570, 24] + font_size: 18 + fg_color: "#ffffff" + source: Static + text: Watch + + - type: Panel + rect: [250, 105, 1, 100] + bg_color: "#c0c0c0" + + - type: Label + rect: [288, 105, 100, 24] + font_size: 12 + fg_color: "#ffffff" + source: Static + text: Visibility + + - type: Button + rect: [270, 120, 100, 30] + font_size: 12 + fg_color: "#ffffff" + bg_color: "#306060" + text: "Hide" + click_down: + - type: Watch + action: Hide + + - type: Button + rect: [270, 170, 100, 30] + font_size: 12 + fg_color: "#ffffff" + bg_color: "#306060" + text: "Swap Hand" + click_down: + - type: Watch + action: SwitchHands + + - type: Panel + rect: [390, 105, 1, 100] + bg_color: "#c0c0c0" + + - type: Label + rect: [430, 105, 120, 24] + font_size: 12 + fg_color: "#ffffff" + source: Static + text: Watch Fade + + - type: Button + rect: [410, 120, 140, 30] + font_size: 12 + fg_color: "#ffffff" + bg_color: "#306060" + text: "Cutoff Point" + click_down: + - type: Toast + message: Use stick up/down while hovering the button! + scroll_up: + - type: Watch + action: + ViewAngle: {kind: "MaxOpacity", delta: 0.01} + scroll_down: + - type: Watch + action: + ViewAngle: {kind: "MaxOpacity", delta: -0.01} + + - type: Button + rect: [410, 170, 140, 30] + font_size: 12 + fg_color: "#ffffff" + bg_color: "#306060" + text: "Cutoff Strength" + click_down: + - type: Toast + message: Use stick up/down while hovering the button! + scroll_up: + - type: Watch + action: + ViewAngle: {kind: "MinOpacity", delta: 0.01} + scroll_down: + - type: Watch + action: + ViewAngle: {kind: "MinOpacity", delta: -0.01} + + - type: Label + rect: [25, 140, 90, 30] + font_size: 12 + fg_color: "#ffffff" + source: Static + text: Rotation + + - type: Button + rect: [108, 120, 30, 30] + font_size: 12 + fg_color: "#ffffff" + bg_color: "#306060" + text: "X" + click_down: + - type: Toast + message: Use stick up/down while hovering the button! + scroll_up: + - type: Watch + action: + Rotation: {axis: "X", delta: 0.25} + scroll_down: + - type: Watch + action: + Rotation: {axis: "X", delta: -0.25} + + - type: Button + rect: [153, 120, 30, 30] + font_size: 12 + fg_color: "#ffffff" + bg_color: "#306060" + text: "Y" + click_down: + - type: Toast + message: Use stick up/down while hovering the button! + scroll_up: + - type: Watch + action: + Rotation: {axis: "Y", delta: 0.25} + scroll_down: + - type: Watch + action: + Rotation: {axis: "Y", delta: -0.25} + + - type: Button + rect: [198, 120, 30, 30] + font_size: 12 + fg_color: "#ffffff" + bg_color: "#306060" + text: "Z" + click_down: + - type: Toast + message: Use stick up/down while hovering the button! + scroll_up: + - type: Watch + action: + Rotation: {axis: "Z", delta: 0.25} + scroll_down: + - type: Watch + action: + Rotation: {axis: "Z", delta: -0.25} + + - type: Label + rect: [25, 190, 90, 30] + font_size: 12 + fg_color: "#ffffff" + source: Static + text: Position + + - type: Button + rect: [108, 170, 30, 30] + font_size: 12 + fg_color: "#ffffff" + bg_color: "#306060" + text: "X" + click_down: + - type: Toast + message: Use stick up/down while hovering the button! + scroll_up: + - type: Watch + action: + Position: {axis: "X", delta: 0.001} + scroll_down: + - type: Watch + action: + Position: {axis: "X", delta: -0.001} + + - type: Button + rect: [153, 170, 30, 30] + font_size: 12 + fg_color: "#ffffff" + bg_color: "#306060" + text: "Y" + click_down: + - type: Toast + message: Use stick up/down while hovering the button! + scroll_up: + - type: Watch + action: + Position: {axis: "Y", delta: 0.001} + scroll_down: + - type: Watch + action: + Position: {axis: "Y", delta: -0.001} + + - type: Button + rect: [198, 170, 30, 30] + font_size: 12 + fg_color: "#ffffff" + bg_color: "#306060" + text: "Z" + click_down: + - type: Toast + message: Use stick up/down while hovering the button! + scroll_up: + - type: Watch + action: + Position: {axis: "Z", delta: 0.001} + scroll_down: + - type: Watch + action: + Position: {axis: "Z", delta: -0.001} + + - type: Panel + rect: [50, 220, 500, 1] + bg_color: "#c0c0c0" + + ####### Mirror Section ####### + - type: Label + rect: [15, 255, 570, 24] + font_size: 18 + fg_color: "#ffffff" + source: Static + text: Mirrors + + - type: Label + rect: [25, 290, 30, 30] + font_size: 12 + fg_color: "#ffffff" + source: Static + text: M1 + + - type: Button + rect: [60, 270, 110, 30] + font_size: 12 + fg_color: "#ffffff" + bg_color: "#707070" + text: "Show/Hide" + click_down: + - type: Window + target: M1 + action: ShowMirror + + - type: Button + rect: [185, 270, 60, 30] + font_size: 12 + fg_color: "#ffffff" + bg_color: "#707070" + text: "Lock" + click_down: + - type: Overlay + target: M1 + action: ToggleInteraction + + - type: Button + rect: [258, 270, 30, 30] + font_size: 12 + fg_color: "#ffffff" + bg_color: "#880000" + text: "X" + click_down: + - type: Window + target: M1 + action: Destroy + + - type: Label + rect: [25, 340, 30, 30] + font_size: 12 + fg_color: "#ffffff" + source: Static + text: M2 + + - type: Button + rect: [60, 320, 110, 30] + font_size: 12 + fg_color: "#ffffff" + bg_color: "#707070" + text: "Show/Hide" + click_down: + - type: Window + target: M2 + action: ShowMirror + + - type: Button + rect: [185, 320, 60, 30] + font_size: 12 + fg_color: "#ffffff" + bg_color: "#707070" + text: "Lock" + click_down: + - type: Overlay + target: M2 + action: ToggleInteraction + + - type: Button + rect: [258, 320, 30, 30] + font_size: 12 + fg_color: "#ffffff" + bg_color: "#880000" + text: "X" + click_down: + - type: Window + target: M2 + action: Destroy + + - type: Label + rect: [25, 390, 30, 30] + font_size: 12 + fg_color: "#ffffff" + source: Static + text: M3 + + - type: Button + rect: [60, 370, 110, 30] + font_size: 12 + fg_color: "#ffffff" + bg_color: "#707070" + text: "Show/Hide" + click_down: + - type: Window + target: M3 + action: ShowMirror + + - type: Button + rect: [185, 370, 60, 30] + font_size: 12 + fg_color: "#ffffff" + bg_color: "#707070" + text: "Lock" + click_down: + - type: Overlay + target: M3 + action: ToggleInteraction + + - type: Button + rect: [258, 370, 30, 30] + font_size: 12 + fg_color: "#ffffff" + bg_color: "#880000" + text: "X" + click_down: + - type: Window + target: M3 + action: Destroy + + - type: Panel + rect: [300, 240, 1, 200] + bg_color: "#c0c0c0" + + ####### Color Gain Section ####### + + - type: Label + rect: [325, 255, 90, 24] + font_size: 18 + fg_color: "#ffffff" + source: Static + text: Color Gain + + - type: Label + rect: [470, 255, 90, 30] + font_size: 12 + fg_color: "#ffffff" + source: Static + text: (SteamVR) + + - type: Button + rect: [330, 270, 60, 30] + font_size: 12 + fg_color: "#ffffff" + bg_color: "#707070" + text: "All" + click_down: + - type: Toast + message: Use stick up/down while hovering the button! + scroll_up: + - type: ColorAdjust + channel: All + delta: 0.01 + scroll_down: + - type: ColorAdjust + channel: All + delta: -0.01 + + - type: Button + rect: [405, 270, 30, 30] + font_size: 12 + fg_color: "#ffffff" + bg_color: "#701010" + text: "R" + click_down: + - type: Toast + message: Use stick up/down while hovering the button! + scroll_up: + - type: ColorAdjust + channel: R + delta: 0.01 + scroll_down: + - type: ColorAdjust + channel: R + delta: -0.01 + + - type: Button + rect: [450, 270, 30, 30] + font_size: 12 + fg_color: "#ffffff" + bg_color: "#107010" + text: "G" + click_down: + - type: Toast + message: Use stick up/down while hovering the button! + scroll_up: + - type: ColorAdjust + channel: G + delta: 0.01 + scroll_down: + - type: ColorAdjust + channel: G + delta: -0.01 + + - type: Button + rect: [495, 270, 30, 30] + font_size: 12 + fg_color: "#ffffff" + bg_color: "#101070" + text: "B" + click_down: + - type: Toast + message: Use stick up/down while hovering the button! + scroll_up: + - type: ColorAdjust + channel: B + delta: 0.01 + scroll_down: + - type: ColorAdjust + channel: B + delta: -0.01 + + - type: Panel + rect: [325, 315, 225, 1] + bg_color: "#c0c0c0" + + ####### Playspace Section ####### + + - type: Label + rect: [325, 345, 90, 24] + font_size: 18 + fg_color: "#ffffff" + source: Static + text: Playspace + + - type: Button + rect: [330, 360, 220, 30] + font_size: 12 + fg_color: "#ffffff" + bg_color: "#206060" + text: "Fix Floor" + click_down: + - type: System + action: PlayspaceFixFloor + + - type: Button + rect: [330, 410, 220, 30] + font_size: 12 + fg_color: "#ffffff" + bg_color: "#206060" + text: "Reset Offset" + click_down: + - type: System + action: PlayspaceResetOffset + + - type: Panel + rect: [50, 460, 500, 1] + bg_color: "#c0c0c0" + + - type: Button + rect: [330, 480, 220, 30] + font_size: 12 + fg_color: "#ffffff" + bg_color: "#206060" + text: "Save Config" + click_down: + - type: System + action: PersistConfig + diff --git a/src/res/watch.yaml b/src/res/watch.yaml index 6e28628..482273a 100644 --- a/src/res/watch.yaml +++ b/src/res/watch.yaml @@ -1,61 +1,61 @@ -# Tips for optimization: -# - try to re-use font sizes, every loaded font size uses additional VRAM. - -# common properties: -# - rect: bounding rectangle [x, y, width, height] -# (x, y) = (0, 0) is top-left -# - bg_color: background color of panels and buttons -# - fg_color: color of text +# lookng to make changes? +# drop me in ~/.config/wlxoverlay.settings.yaml # -# element types: -# panel - simple colored rectangle -# rect: bounding rectangle -# bg_color: color of rectangle. quotation marks mandatory. -# -# clock - date/time display with custom formatting -# rect: bounding rectangle -# fg_color: color of text. quotation marks mandatory. -# format: chrono format to print, see https://docs.rs/chrono/latest/chrono/format/strftime/index.html -# timezone: timezone to use, leave empty for local -# render resolution of the watch +width: 0.115 -watch_view_angle: 0.5 # 0 = 90 deg, 1 = 0 deg +size: [400, 200] -# TODO -watch_hand: Left - -# TODO -watch_offset: [] - -# TODO -watch_rotation: [] - -# TODO -watch_size: [400, 200] - -watch_elements: +elements: # background panel - type: Panel rect: [0, 0, 400, 200] bg_color: "#353535" - - type: FuncButton + - type: Button rect: [2, 162, 26, 36] font_size: 14 bg_color: "#808040" fg_color: "#ffffff" - func: SwitchWatchHand - func_right: HideWatch - func_middle: ~ - text: "W" + text: "C" + click_up: + - type: Window + target: settings + action: ShowUi - - type: KeyboardButton + # Keyboard button + - type: Button rect: [32, 162, 60, 36] font_size: 14 fg_color: "#FFFFFF" bg_color: "#406050" - text: "Kbd" + text: Kbd + click_up: + - type: Overlay + target: "kbd" + action: ToggleVisible + long_click_up: + - type: Overlay + target: "kbd" + action: Reset + right_up: + - type: Overlay + target: "kbd" + action: ToggleImmovable + middle_up: + - type: Overlay + target: "kbd" + action: ToggleInteraction + scroll_up: + - type: Overlay + target: "kbd" + action: + Opacity: { delta: 0.025 } + scroll_down: + - type: Overlay + target: "kbd" + action: + Opacity: { delta: -0.025 } # bottom row, of keyboard + overlays - type: OverlayList @@ -64,109 +64,100 @@ watch_elements: fg_color: "#FFFFFF" bg_color: "#405060" layout: Horizontal + click_up: ToggleVisible + long_click_up: Reset + right_up: ToggleImmovable + middle_up: ToggleInteraction + scroll_up: + Opacity: { delta: 0.025 } + scroll_down: + Opacity: { delta: -0.025 } - # main clock with date and day-of-week - - type: Clock + # local clock + - type: Label rect: [19, 90, 200, 50] - #format: "%h:%M %p" # 11:59 PM - format: "%H:%M" # 23:59 font_size: 46 fg_color: "#ffffff" - - type: Clock + source: Clock + format: "%H:%M" # 23:59 + #format: "%h:%M %p" # 11:59 PM + + # local date + - type: Label rect: [20, 117, 200, 20] + font_size: 14 + fg_color: "#ffffff" + source: Clock format: "%x" # local date representation - font_size: 14 - fg_color: "#ffffff" - - type: Clock + + # local day-of-week + - type: Label rect: [20, 137, 200, 50] - #format: "%a" # Tue - format: "%A" # Tuesday font_size: 14 fg_color: "#ffffff" + source: Clock + format: "%A" # Tuesday + #format: "%a" # Tue # alt clock 1 - - type: Clock + - type: Label rect: [210, 90, 200, 50] - timezone: "Asia/Tokyo" # change TZ1 here - format: "%H:%M" font_size: 24 fg_color: "#99BBAA" + source: Clock + timezone: "Asia/Tokyo" # change TZ1 here + format: "%H:%M" # 23:59 + #format: "%h:%M %p" # 11:59 PM - type: Label rect: [210, 60, 200, 50] font_size: 14 fg_color: "#99BBAA" + source: Static text: "Tokyo" # change TZ1 label here # alt clock 2 - - type: Clock + - type: Label rect: [210, 150, 200, 50] - timezone: "America/Chicago" # change TZ2 here - format: "%H:%M" font_size: 24 fg_color: "#AA99BB" + source: Clock + timezone: "America/Chicago" # change TZ2 here + format: "%H:%M" # 23:59 + #format: "%h:%M %p" # 11:59 PM - type: Label rect: [210, 120, 200, 50] font_size: 14 fg_color: "#AA99BB" + source: Static text: "Chicago" # change TZ2 label here - - type: Batteries + # batteries + - type: BatteryList rect: [0, 0, 400, 30] font_size: 14 + fg_color: "#99BBAA" + fg_color_low: "#B06060" + fg_color_charging: "#6080A0" num_devices: 9 - low_threshold: 20 layout: Horizontal - normal_fg_color: "#99BBAA" - # below is not yet implemented - normal_bg_color: "#353535" - low_fg_color: "#B06060" - low_bg_color: "#353535" - charging_fg_color: "#6080A0" - charging_bg_color: "#353535" - - # sample - # - type: ExecLabel - # rect: [50, 20, 200, 50] - # font_size: 14 - # fg_color: "#FFFFFF" - # exec: ["echo", "customize me! see watch.yaml"] - # interval: 0 # seconds - - ### MirrorButton - # Bring an additional PipeWire screen, window, region or virtual screen share into VR. - # These are view-only, and will not respond to pointers by moving your mouse. - # You may have as many as you like, but the `name` must be unique for each. - # Controls: - # - Blue Click: Show/hide. Shows pipewire prompt on first show. - # - Orange Click: Toggle lock in place - # - Purple Click: Stop capture. After doing this, you may Blue-click again to select a different source. - # - Scroll: Adjust opacity - # Warning: - # - Window shares may stop updating if the window goes off-screen or is on an inactive workspace - # - Resizing, minimizing, maximizing windows may break stuff. Complain to your xdg-desktop-portal implementation. - # - Selections are not saved across sessions - - #- type: MirrorButton - # rect: [354, 0, 46, 32] - # font_size: 14 - # fg_color: "#FFFFFF" - # bg_color: "#B05050" - # name: "M1" - # text: "M1" - # show_hide: false # should it respond to show/hide binding? + low_threshold: 20 # volume buttons - - type: ExecButton + - type: Button rect: [327, 52, 46, 32] font_size: 14 fg_color: "#FFFFFF" bg_color: "#505050" text: "+" - exec: [ "pactl", "set-sink-volume", "@DEFAULT_SINK@", "+5%" ] - - type: ExecButton + click_down: + - type: Exec + command: [ "pactl", "set-sink-volume", "@DEFAULT_SINK@", "+5%" ] + - type: Button rect: [327, 116, 46, 32] font_size: 14 fg_color: "#FFFFFF" bg_color: "#505050" text: "-" - exec: [ "pactl", "set-sink-volume", "@DEFAULT_SINK@", "-5%" ] + click_down: + - type: Exec + command: [ "pactl", "set-sink-volume", "@DEFAULT_SINK@", "-5%" ] diff --git a/src/state.rs b/src/state.rs index e62181a..a4ee855 100644 --- a/src/state.rs +++ b/src/state.rs @@ -1,8 +1,10 @@ use std::{path::PathBuf, sync::Arc}; use anyhow::bail; -use glam::{Quat, Vec3}; +use glam::Vec3; use rodio::{OutputStream, OutputStreamHandle}; +use serde::{Deserialize, Serialize}; +use smallvec::{smallvec, SmallVec}; use vulkano::format::Format; use crate::{ @@ -15,9 +17,6 @@ use crate::{ shaders::{frag_color, frag_glyph, frag_sprite, frag_srgb, vert_common}, }; -pub const WATCH_DEFAULT_POS: Vec3 = Vec3::new(-0.03, -0.01, 0.125); -pub const WATCH_DEFAULT_ROT: Quat = Quat::from_xyzw(-0.7071066, 0.0007963618, 0.7071066, 0.0); - pub struct AppState { pub fc: FontCache, pub session: AppSession, @@ -27,6 +26,7 @@ pub struct AppState { pub input_state: InputState, pub hid_provider: Box, pub audio: AudioOutput, + pub screens: SmallVec<[ScreenMeta; 8]>, } impl AppState { @@ -62,6 +62,7 @@ impl AppState { input_state: InputState::new(), hid_provider: crate::hid::initialize(), audio: AudioOutput::new(), + screens: smallvec![], }) } } @@ -70,12 +71,6 @@ pub struct AppSession { pub config_root_path: PathBuf, pub config: GeneralConfig, - pub watch_hand: usize, - pub watch_pos: Vec3, - pub watch_rot: Quat, - - pub primary_hand: usize, - pub color_norm: Vec3, pub color_shift: Vec3, pub color_alt: Vec3, @@ -91,10 +86,6 @@ impl AppSession { AppSession { config_root_path, config, - primary_hand: 1, - watch_hand: 0, - watch_pos: WATCH_DEFAULT_POS, - watch_rot: WATCH_DEFAULT_ROT, color_norm: Vec3 { x: 0., y: 1., @@ -145,3 +136,16 @@ impl AudioOutput { self.audio_stream.as_ref().map(|(_, h)| h) } } + +pub struct ScreenMeta { + pub name: Arc, + pub id: usize, +} + +#[derive(Serialize, Deserialize, Clone, Copy, Default)] +#[repr(u8)] +pub enum LeftRight { + #[default] + Left, + Right, +}