From 4843aeef5dffbc12709f646e196c701fad1dbe25 Mon Sep 17 00:00:00 2001 From: galister <22305755+galister@users.noreply.github.com> Date: Fri, 29 Mar 2024 12:21:12 +0100 Subject: [PATCH] feat: toast topics --- Cargo.lock | 1 + Cargo.toml | 2 +- src/backend/notifications.rs | 25 ++++++++++---- src/backend/openvr/mod.rs | 11 +++++- src/backend/openxr/mod.rs | 19 +++++++++-- src/config.rs | 15 ++++++++ src/gui/modular/button.rs | 66 ++++++++++++++++++++++++++---------- src/overlays/toast.rs | 50 ++++++++++++++++++++++++--- 8 files changed, 155 insertions(+), 34 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d7a6202..d0f2196 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1537,6 +1537,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dba885f996064df334b1639785897d1c58c0646750101b839b8359664cb26c0e" dependencies = [ "fixedbitset", + "serde", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index dd387f9..e12fd96 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,7 +23,7 @@ fontconfig-rs = "0.1.1" freetype-rs = "0.32.0" futures = "0.3.29" glam = { version = "0.24.1", features = ["approx"] } -idmap = "0.2.21" +idmap = { version = "0.2.21", features = ["serde"] } idmap-derive = "0.1.2" input-linux = "0.6.0" json = { version = "0.12.4", optional = true } diff --git a/src/backend/notifications.rs b/src/backend/notifications.rs index 2064f3f..2422073 100644 --- a/src/backend/notifications.rs +++ b/src/backend/notifications.rs @@ -9,7 +9,12 @@ use std::{ time::Duration, }; -use crate::{config::def_true, config_io, overlays::toast::Toast, state::AppState}; +use crate::{ + config::def_true, + config_io, + overlays::toast::{Toast, ToastTopic}, + state::AppState, +}; pub struct NotificationManager { rx_toast: mpsc::Receiver, @@ -167,9 +172,13 @@ impl NotificationManager { continue; } - let toast = Toast::new(msg.title, msg.content.unwrap_or_else(|| "".into())) - .with_timeout(msg.timeout.unwrap_or(5.)) - .with_sound(msg.volume.unwrap_or(-1.) >= 0.); // XSOverlay still plays at 0, + let toast = Toast::new( + ToastTopic::XSNotification, + msg.title, + msg.content.unwrap_or_else(|| "".into()), + ) + .with_timeout(msg.timeout.unwrap_or(5.)) + .with_sound(msg.volume.unwrap_or(-1.) >= 0.); // XSOverlay still plays at 0, match sender.try_send(toast) { Ok(_) => {} @@ -204,9 +213,11 @@ fn parse_dbus(msg: &dbus::Message) -> anyhow::Result { summary }; - Ok(Toast::new(title.into(), body.into()) - .with_timeout(5.0) - .with_opacity(1.0)) + Ok( + Toast::new(ToastTopic::DesktopNotification, title.into(), body.into()) + .with_timeout(5.0) + .with_opacity(1.0), + ) // leave the audio part to the desktop env } diff --git a/src/backend/openvr/mod.rs b/src/backend/openvr/mod.rs index 593f3dd..bbbbe4a 100644 --- a/src/backend/openvr/mod.rs +++ b/src/backend/openvr/mod.rs @@ -33,7 +33,10 @@ use crate::{ overlay::OverlayData, }, graphics::WlxGraphics, - overlays::watch::{watch_fade, WATCH_NAME}, + overlays::{ + toast::{Toast, ToastTopic}, + watch::{watch_fade, WATCH_NAME}, + }, state::AppState, }; @@ -178,6 +181,12 @@ pub fn openvr_run(running: Arc) -> Result<(), BackendError> { let ipd = (ipd * 10000.0).round() * 0.1; if (ipd - state.input_state.ipd).abs() > 0.05 { log::info!("IPD: {:.1} mm -> {:.1} mm", state.input_state.ipd, ipd); + Toast::new( + ToastTopic::IpdChange, + "IPD".into(), + format!("{:.1} mm", ipd).into(), + ) + .submit(&mut state); } state.input_state.ipd = ipd; } diff --git a/src/backend/openxr/mod.rs b/src/backend/openxr/mod.rs index 99aafd4..6d3c8c4 100644 --- a/src/backend/openxr/mod.rs +++ b/src/backend/openxr/mod.rs @@ -20,7 +20,10 @@ use crate::{ overlay::OverlayData, }, graphics::WlxGraphics, - overlays::watch::{watch_fade, WATCH_NAME}, + overlays::{ + toast::{Toast, ToastTopic}, + watch::{watch_fade, WATCH_NAME}, + }, state::AppState, }; @@ -220,8 +223,18 @@ pub fn openxr_run(running: Arc) -> Result<(), BackendError> { &xr_state.stage, )?; - (app_state.input_state.hmd, app_state.input_state.ipd) = - helpers::hmd_pose_from_views(&views); + let (hmd, ipd) = helpers::hmd_pose_from_views(&views); + app_state.input_state.hmd = hmd; + if (app_state.input_state.ipd - ipd).abs() > 0.01 { + log::info!("IPD changed: {} -> {}", app_state.input_state.ipd, ipd); + app_state.input_state.ipd = ipd; + Toast::new( + ToastTopic::IpdChange, + "IPD".into(), + format!("{:.1} mm", ipd).into(), + ) + .submit(&mut app_state); + } overlays .iter_mut() diff --git a/src/config.rs b/src/config.rs index 2ae497a..c9ec30b 100644 --- a/src/config.rs +++ b/src/config.rs @@ -4,8 +4,11 @@ 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::toast::DisplayMethod; +use crate::overlays::toast::ToastTopic; use crate::state::LeftRight; use anyhow::bail; +use idmap::IdMap; use log::error; use serde::Deserialize; use serde::Serialize; @@ -62,6 +65,15 @@ fn def_auto() -> Arc { "auto".into() } +fn def_toast_topics() -> IdMap { + let mut map = IdMap::new(); + map.insert(ToastTopic::System, DisplayMethod::Center); + map.insert(ToastTopic::DesktopNotification, DisplayMethod::Center); + map.insert(ToastTopic::XSNotification, DisplayMethod::Center); + map.insert(ToastTopic::IpdChange, DisplayMethod::Hide); + map +} + #[derive(Deserialize, Serialize)] pub struct GeneralConfig { #[serde(default = "def_watch_pos")] @@ -103,6 +115,9 @@ pub struct GeneralConfig { #[serde(default = "def_pw_tokens")] pub pw_tokens: Vec<(String, String)>, + #[serde(default = "def_toast_topics")] + pub toast_topics: IdMap, + #[serde(default = "def_osc_port")] pub osc_out_port: u16, diff --git a/src/gui/modular/button.rs b/src/gui/modular/button.rs index b457a78..edf5a29 100644 --- a/src/gui/modular/button.rs +++ b/src/gui/modular/button.rs @@ -17,7 +17,7 @@ use crate::{ overlay::RelativeTo, }, overlays::{ - toast::Toast, + toast::{Toast, ToastTopic}, watch::{save_watch, WATCH_NAME}, }, state::AppState, @@ -276,9 +276,13 @@ fn handle_action(action: &ButtonAction, press: &mut PressData, app: &mut AppStat body, seconds, } => { - Toast::new(message.clone(), body.clone().unwrap_or_else(|| "".into())) - .with_timeout(seconds.unwrap_or(5.)) - .submit(app); + Toast::new( + ToastTopic::System, + message.clone(), + body.clone().unwrap_or_else(|| "".into()), + ) + .with_timeout(seconds.unwrap_or(5.)) + .submit(app); } ButtonAction::ColorAdjust { channel, delta } => { let channel = *channel; @@ -303,6 +307,7 @@ fn run_system(action: &SystemAction, app: &mut AppState) { let at = now.add(i * sec); let display = 5 - i; Toast::new( + ToastTopic::System, format!("Fixing floor in {}", display).into(), "Place either controller on the floor.".into(), ) @@ -318,6 +323,7 @@ fn run_system(action: &SystemAction, app: &mut AppState) { SystemAction::ToggleNotifications => { app.session.config.notifications_enabled = !app.session.config.notifications_enabled; Toast::new( + ToastTopic::System, format!( "Notifications are {}.", if app.session.config.notifications_enabled { @@ -335,6 +341,7 @@ fn run_system(action: &SystemAction, app: &mut AppState) { app.session.config.notifications_sound_enabled = !app.session.config.notifications_sound_enabled; Toast::new( + ToastTopic::System, format!( "Notification sounds are {}.", if app.session.config.notifications_sound_enabled { @@ -383,7 +390,7 @@ fn run_exec(args: &ExecArgs, toast: &Option>, press: &mut PressData, ap Ok(proc) => { press.child = Some(proc); if let Some(toast) = toast.as_ref() { - Toast::new(toast.clone(), "".into()).submit(app); + Toast::new(ToastTopic::System, toast.clone(), "".into()).submit(app); } } Err(e) => { @@ -403,6 +410,7 @@ fn run_watch(data: &WatchAction, app: &mut AppState) { o.want_visible = false; o.saved_scale = Some(0.0); Toast::new( + ToastTopic::System, "Watch hidden".into(), "Use show/hide binding to restore.".into(), ) @@ -411,7 +419,8 @@ fn run_watch(data: &WatchAction, app: &mut AppState) { } else { o.want_visible = true; o.saved_scale = None; - Toast::new("Watch restored".into(), "".into()).submit(app); + Toast::new(ToastTopic::System, "Watch restored".into(), "".into()) + .submit(app); } }), )); @@ -434,9 +443,13 @@ fn run_watch(data: &WatchAction, app: &mut AppState) { 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); + Toast::new( + ToastTopic::System, + "Watch switched".into(), + "Check your other hand".into(), + ) + .with_timeout(3.) + .submit(app); }), )); audio_thump(app); @@ -504,7 +517,12 @@ fn run_overlay(overlay: &OverlaySelector, action: &OverlayAction, app: &mut AppS overlay.clone(), Box::new(|app, o| { o.reset(app, true); - Toast::new(format!("{} has been reset!", o.name).into(), "".into()).submit(app); + Toast::new( + ToastTopic::System, + format!("{} has been reset!", o.name).into(), + "".into(), + ) + .submit(app); }), )); } @@ -529,13 +547,18 @@ fn run_overlay(overlay: &OverlaySelector, action: &OverlayAction, app: &mut AppS o.show_hide = o.recenter; if !o.recenter { Toast::new( + ToastTopic::System, 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); + Toast::new( + ToastTopic::System, + format!("{} is now unlocked!", o.name).into(), + "".into(), + ) + .submit(app); } }), )); @@ -548,13 +571,18 @@ fn run_overlay(overlay: &OverlaySelector, action: &OverlayAction, app: &mut AppS o.interactable = !o.interactable; if !o.interactable { Toast::new( + ToastTopic::System, format!("{} is now non-interactable!", o.name).into(), "".into(), ) .submit(app); } else { - Toast::new(format!("{} is now interactable!", o.name).into(), "".into()) - .submit(app); + Toast::new( + ToastTopic::System, + format!("{} is now interactable!", o.name).into(), + "".into(), + ) + .submit(app); } }), )); @@ -585,9 +613,13 @@ fn run_window(window: &Arc, action: &WindowAction, app: &mut AppState) { Box::new({ let name = window.clone(); move |app| { - Toast::new("Check your desktop for popup.".into(), "".into()) - .with_sound(true) - .submit(app); + Toast::new( + ToastTopic::System, + "Check your desktop for popup.".into(), + "".into(), + ) + .with_sound(true) + .submit(app); crate::overlays::mirror::new_mirror(name.clone(), false, &app.session) } }), diff --git a/src/overlays/toast.rs b/src/overlays/toast.rs index 0567422..4a71d6c 100644 --- a/src/overlays/toast.rs +++ b/src/overlays/toast.rs @@ -4,7 +4,9 @@ use std::{ time::Instant, }; -use glam::vec3a; +use glam::{vec3a, Vec3A}; +use idmap_derive::IntegerId; +use serde::{Deserialize, Serialize}; use crate::{ backend::{ @@ -12,7 +14,7 @@ use crate::{ overlay::{OverlayBackend, OverlayState, RelativeTo}, }, gui::{color_parse, CanvasBuilder}, - state::AppState, + state::{AppState, LeftRight}, }; const FONT_SIZE: isize = 16; @@ -22,23 +24,40 @@ const TOAST_AUDIO_WAV: &[u8] = include_bytes!("../res/557297.wav"); static AUTO_INCREMENT: AtomicUsize = AtomicUsize::new(0); +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +pub enum DisplayMethod { + Hide, + Center, + Watch, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, IntegerId, Serialize, Deserialize)] +pub enum ToastTopic { + System, + DesktopNotification, + XSNotification, + IpdChange, +} + pub struct Toast { pub title: Arc, pub body: Arc, pub opacity: f32, pub timeout: f32, pub sound: bool, + pub topic: ToastTopic, } #[allow(dead_code)] impl Toast { - pub fn new(title: Arc, body: Arc) -> Self { + pub fn new(topic: ToastTopic, title: Arc, body: Arc) -> Self { Toast { title, body, opacity: 1.0, timeout: 3.0, sound: false, + topic, } } pub fn with_timeout(mut self, timeout: f32) -> Self { @@ -87,6 +106,27 @@ fn new_toast( name: Arc, app: &mut AppState, ) -> Option<(OverlayState, Box)> { + let current_method = app + .session + .config + .toast_topics + .get(&toast.topic) + .copied() + .unwrap_or(DisplayMethod::Hide); + + let (spawn_point, relative_to) = match current_method { + DisplayMethod::Hide => return None, + DisplayMethod::Center => (vec3a(0., -2.0, -0.5), RelativeTo::Head), + DisplayMethod::Watch => { + let relative_to = match app.session.config.watch_hand { + LeftRight::Left => RelativeTo::Hand(0), + LeftRight::Right => RelativeTo::Hand(1), + }; + let watch_pos = Vec3A::from_slice(&app.session.config.watch_pos); + (watch_pos + Vec3A::Y * 0.05, relative_to) + } + }; + let title = if toast.title.len() > 0 { toast.title } else { @@ -143,8 +183,8 @@ fn new_toast( name, want_visible: true, spawn_scale: size.0 * PIXELS_TO_METERS, - spawn_point: vec3a(0., -0.2, -0.5), - relative_to: RelativeTo::Head, + spawn_point, + relative_to, ..Default::default() }; let backend = Box::new(canvas.build());