diff --git a/src/backend/common.rs b/src/backend/common.rs index 2b56684..934f1e0 100644 --- a/src/backend/common.rs +++ b/src/backend/common.rs @@ -15,8 +15,9 @@ use thiserror::Error; use wlx_capture::wayland::{OutputChangeEvent, WlxClient}; use crate::{ + config::{AStrMapExt, AStrSetExt}, overlays::{ - keyboard::create_keyboard, + keyboard::{create_keyboard, KEYBOARD_NAME}, screen::{create_screen_interaction, create_screen_renderer_wl, load_pw_token_config}, watch::{create_watch, create_watch_canvas, WATCH_NAME}, }, @@ -66,15 +67,21 @@ where let mut show_screens = app.session.config.show_screens.clone(); if show_screens.is_empty() { if let Some((_, s, _)) = data.screens.first() { - show_screens.push(s.name.clone()); + show_screens.arc_ins(s.name.clone()); } } for (meta, mut state, backend) in data.screens { - if show_screens.contains(&state.name) { + if show_screens.arc_get(state.name.as_ref()) { state.show_hide = true; state.want_visible = false; } + state.curvature = app + .session + .config + .curve_values + .arc_get(state.name.as_ref()) + .copied(); overlays.insert( state.id, OverlayData:: { @@ -93,6 +100,12 @@ where let mut keyboard = create_keyboard(app)?; keyboard.state.show_hide = true; keyboard.state.want_visible = false; + keyboard.state.curvature = app + .session + .config + .curve_values + .arc_get(KEYBOARD_NAME) + .copied(); overlays.insert(keyboard.state.id, keyboard); Ok(Self { overlays, wl }) @@ -283,7 +296,10 @@ where self.overlays.values_mut().for_each(|o| { if o.state.show_hide { o.state.want_visible = !any_shown; - if o.state.want_visible && app.session.config.realign_on_showhide && o.state.recenter { + if o.state.want_visible + && app.session.config.realign_on_showhide + && o.state.recenter + { o.state.reset(app, false); } } diff --git a/src/backend/input.rs b/src/backend/input.rs index dd2ed04..a5d8a5a 100644 --- a/src/backend/input.rs +++ b/src/backend/input.rs @@ -6,7 +6,7 @@ use glam::{Affine3A, Vec2, Vec3A}; use ovr_overlay::TrackedDeviceIndex; use smallvec::{smallvec, SmallVec}; -use crate::config::GeneralConfig; +use crate::config::{save_state, AStrMapExt, GeneralConfig}; use crate::state::AppState; use super::{ @@ -246,6 +246,7 @@ struct RayHit { pub struct GrabData { pub offset: Vec3A, pub grabbed_id: usize, + pub old_curvature: Option, } #[repr(u8)] @@ -288,7 +289,7 @@ where let mut pointer = &mut app.input_state.pointers[idx]; if let Some(grab_data) = pointer.interaction.grabbed { if let Some(grabbed) = overlays.mut_by_id(grab_data.grabbed_id) { - pointer.handle_grabbed(grabbed, hmd, &app.session.config); + pointer.handle_grabbed(grabbed, hmd, &mut app.session.config); } else { log::warn!("Grabbed overlay {} does not exist", grab_data.grabbed_id); pointer.interaction.grabbed = None; @@ -469,12 +470,17 @@ impl Pointer { self.interaction.grabbed = Some(GrabData { offset, grabbed_id: overlay.state.id, + old_curvature: overlay.state.curvature, }); log::info!("Hand {}: grabbed {}", self.idx, overlay.state.name); } - fn handle_grabbed(&mut self, overlay: &mut OverlayData, hmd: &Affine3A, config: &GeneralConfig) - where + fn handle_grabbed( + &mut self, + overlay: &mut OverlayData, + hmd: &Affine3A, + config: &mut GeneralConfig, + ) where O: Default, { if self.now.grab { @@ -509,6 +515,26 @@ impl Pointer { hmd.inverse() .transform_point3a(overlay.state.transform.translation), ); + + if let Some(grab_data) = self.interaction.grabbed.as_ref() { + let mut state_dirty = false; + if overlay.state.curvature != grab_data.old_curvature { + if let Some(val) = overlay.state.curvature { + config.curve_values.arc_ins(overlay.state.name.clone(), val); + } else { + let ref_name = overlay.state.name.as_ref(); + config.curve_values.arc_rm(ref_name); + } + state_dirty = true; + } + if state_dirty { + match save_state(config) { + Ok(_) => log::debug!("Saved state"), + Err(e) => log::error!("Failed to save state: {:?}", e), + } + } + } + self.interaction.grabbed = None; log::info!("Hand {}: dropped {}", self.idx, overlay.state.name); } diff --git a/src/config.rs b/src/config.rs index 104af2c..2116da7 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,3 +1,4 @@ +use std::path::PathBuf; use std::sync::Arc; use crate::config_io; @@ -15,6 +16,68 @@ use log::error; use serde::Deserialize; use serde::Serialize; +pub type AStrMap = Vec<(Arc, V)>; + +pub trait AStrMapExt { + fn arc_ins(&mut self, key: Arc, value: V) -> bool; + fn arc_get(&self, key: &str) -> Option<&V>; + fn arc_rm(&mut self, key: &str) -> Option; +} + +impl AStrMapExt for AStrMap { + fn arc_ins(&mut self, key: Arc, value: V) -> bool { + if self.iter().any(|(k, _)| k.as_ref().eq(key.as_ref())) { + return false; + } + self.push((key, value)); + true + } + + fn arc_get(&self, key: &str) -> Option<&V> { + self.iter() + .find_map(|(k, v)| if k.as_ref().eq(key) { Some(v) } else { None }) + } + + fn arc_rm(&mut self, key: &str) -> Option { + let index = self.iter().position(|(k, _)| k.as_ref().eq(key)); + index.map(|i| self.remove(i).1) + } +} + +pub type AStrSet = Vec>; + +pub trait AStrSetExt { + fn arc_ins(&mut self, value: Arc) -> bool; + fn arc_get(&self, value: &str) -> bool; + fn arc_rm(&mut self, value: &str) -> bool; +} + +impl AStrSetExt for AStrSet { + fn arc_ins(&mut self, value: Arc) -> bool { + if self.iter().any(|v| v.as_ref().eq(value.as_ref())) { + return false; + } + self.push(value); + true + } + + fn arc_get(&self, value: &str) -> bool { + self.iter().any(|v| v.as_ref().eq(value)) + } + + fn arc_rm(&mut self, value: &str) -> bool { + let index = self.iter().position(|v| v.as_ref().eq(value)); + index + .map(|i| { + self.remove(i); + true + }) + .unwrap_or(false) + } +} + +pub type PwTokenMap = AStrMap; + pub fn def_watch_pos() -> [f32; 3] { [-0.03, -0.01, 0.125] } @@ -27,8 +90,8 @@ pub fn def_left() -> LeftRight { LeftRight::Left } -pub fn def_pw_tokens() -> Vec<(String, String)> { - Vec::new() +pub fn def_pw_tokens() -> PwTokenMap { + AStrMap::new() } fn def_click_freeze_time_ms() -> u32 { @@ -59,8 +122,12 @@ fn def_osc_port() -> u16 { 9000 } -fn def_screens() -> Vec> { - vec![] +fn def_screens() -> AStrSet { + AStrSet::new() +} + +fn def_curve_values() -> AStrMap { + AStrMap::new() } fn def_auto() -> Arc { @@ -113,7 +180,7 @@ pub struct GeneralConfig { pub long_press_duration: f32, #[serde(default = "def_pw_tokens")] - pub pw_tokens: Vec<(String, String)>, + pub pw_tokens: PwTokenMap, #[serde(default = "def_osc_port")] pub osc_out_port: u16, @@ -125,7 +192,10 @@ pub struct GeneralConfig { pub double_cursor_fix: bool, #[serde(default = "def_screens")] - pub show_screens: Vec>, + pub show_screens: AStrSet, + + #[serde(default = "def_curve_values")] + pub curve_values: AStrMap, #[serde(default = "def_auto")] pub capture_method: Arc, @@ -270,3 +340,67 @@ pub fn load_general() -> GeneralConfig { } }; } + +// Config that is saved from the settings panel + +#[derive(Serialize)] +pub struct AutoSettings { + pub watch_pos: [f32; 3], + pub watch_rot: [f32; 4], + pub watch_hand: LeftRight, + pub watch_view_angle_min: f32, + pub watch_view_angle_max: f32, + pub notifications_enabled: bool, + pub notifications_sound_enabled: bool, + pub realign_on_showhide: bool, + pub allow_sliding: bool, +} + +fn get_settings_path() -> PathBuf { + let mut path = config_io::get_conf_d_path(); + path.push("zz-saved-config.json5"); + path +} +pub fn save_settings(config: &GeneralConfig) -> anyhow::Result<()> { + let conf = AutoSettings { + watch_pos: config.watch_pos, + watch_rot: config.watch_rot, + watch_hand: config.watch_hand, + watch_view_angle_min: config.watch_view_angle_min, + watch_view_angle_max: config.watch_view_angle_max, + notifications_enabled: config.notifications_enabled, + notifications_sound_enabled: config.notifications_sound_enabled, + realign_on_showhide: config.realign_on_showhide, + allow_sliding: config.allow_sliding, + }; + + let json = serde_json::to_string_pretty(&conf).unwrap(); // want panic + std::fs::write(get_settings_path(), json)?; + + Ok(()) +} + +// Config that is saved after manipulating overlays + +#[derive(Serialize)] +pub struct AutoState { + pub show_screens: AStrSet, + pub curve_values: AStrMap, +} + +fn get_state_path() -> PathBuf { + let mut path = config_io::get_conf_d_path(); + path.push("zz-saved-state.json5"); + path +} +pub fn save_state(config: &GeneralConfig) -> anyhow::Result<()> { + let conf = AutoState { + show_screens: config.show_screens.clone(), + curve_values: config.curve_values.clone(), + }; + + let json = serde_json::to_string_pretty(&conf).unwrap(); // want panic + std::fs::write(get_state_path(), json)?; + + Ok(()) +} diff --git a/src/gui/modular/button.rs b/src/gui/modular/button.rs index 5a0ceed..31c0d42 100644 --- a/src/gui/modular/button.rs +++ b/src/gui/modular/button.rs @@ -1,14 +1,13 @@ use std::{ f32::consts::PI, ops::Add, - path::PathBuf, process::{self, Child}, sync::Arc, time::{Duration, Instant}, }; use glam::{Quat, Vec3A, Vec4}; -use serde::{Deserialize, Serialize}; +use serde::Deserialize; use crate::{ backend::{ @@ -16,13 +15,12 @@ use crate::{ input::PointerMode, overlay::RelativeTo, }, - config::{def_half, def_left, def_point7, def_true, def_watch_pos, def_watch_rot}, - config_io, + config::{save_settings, save_state, AStrSetExt}, overlays::{ toast::{Toast, ToastTopic}, watch::WATCH_NAME, }, - state::{AppState, LeftRight}, + state::AppState, }; use super::{ExecArgs, ModularControl, ModularData}; @@ -44,6 +42,7 @@ pub enum Axis { #[derive(Deserialize, Clone)] pub enum HighlightTest { + AllowSliding, AutoRealign, NotificationSounds, Notifications, @@ -51,6 +50,7 @@ pub enum HighlightTest { #[derive(Deserialize, Clone)] pub enum SystemAction { + ToggleAllowSliding, ToggleAutoRealign, ToggleNotificationSounds, ToggleNotifications, @@ -289,6 +289,7 @@ fn modular_button_highlight( if let Some(test) = &data.highlight { let lit = match test { + HighlightTest::AllowSliding => app.session.config.allow_sliding, HighlightTest::AutoRealign => app.session.config.realign_on_showhide, HighlightTest::NotificationSounds => app.session.config.notifications_sound_enabled, HighlightTest::Notifications => app.session.config.notifications_enabled, @@ -330,19 +331,30 @@ fn handle_action(action: &ButtonAction, press: &mut PressData, app: &mut AppStat } } +const ENABLED_DISABLED: [&str; 2] = ["enabled", "disabled"]; + fn run_system(action: &SystemAction, app: &mut AppState) { match action { + SystemAction::ToggleAllowSliding => { + app.session.config.allow_sliding = !app.session.config.allow_sliding; + Toast::new( + ToastTopic::System, + format!( + "Sliding is {}.", + ENABLED_DISABLED[app.session.config.allow_sliding as usize] + ) + .into(), + "".into(), + ) + .submit(app); + } SystemAction::ToggleAutoRealign => { app.session.config.realign_on_showhide = !app.session.config.realign_on_showhide; Toast::new( ToastTopic::System, format!( "Auto realign is {}.", - if app.session.config.realign_on_showhide { - "enabled" - } else { - "disabled" - } + ENABLED_DISABLED[app.session.config.realign_on_showhide as usize] ) .into(), "".into(), @@ -379,11 +391,7 @@ fn run_system(action: &SystemAction, app: &mut AppState) { ToastTopic::System, format!( "Notifications are {}.", - if app.session.config.notifications_enabled { - "enabled" - } else { - "disabled" - } + ENABLED_DISABLED[app.session.config.notifications_enabled as usize] ) .into(), "".into(), @@ -397,11 +405,7 @@ fn run_system(action: &SystemAction, app: &mut AppState) { ToastTopic::System, format!( "Notification sounds are {}.", - if app.session.config.notifications_sound_enabled { - "enabled" - } else { - "disabled" - } + ENABLED_DISABLED[app.session.config.notifications_sound_enabled as usize] ) .into(), "".into(), @@ -409,7 +413,7 @@ fn run_system(action: &SystemAction, app: &mut AppState) { .submit(app); } SystemAction::PersistConfig => { - if let Err(e) = save_settings(app) { + if let Err(e) = save_settings(&app.session.config) { log::error!("Failed to save config: {:?}", e); } } @@ -585,6 +589,20 @@ fn run_overlay(overlay: &OverlaySelector, action: &OverlayAction, app: &mut AppS o.show_hide = o.want_visible; o.reset(app, false); } + + let mut state_dirty = false; + if !o.want_visible { + state_dirty |= app.session.config.show_screens.arc_rm(o.name.as_ref()); + } else if o.want_visible { + state_dirty |= app.session.config.show_screens.arc_ins(o.name.clone()); + } + + if state_dirty { + match save_state(&app.session.config) { + Ok(_) => log::debug!("Saved state"), + Err(e) => log::error!("Failed to save state: {:?}", e), + } + } }), )); } @@ -698,53 +716,3 @@ const THUMP_AUDIO_WAV: &[u8] = include_bytes!("../../res/380885.wav"); fn audio_thump(app: &mut AppState) { app.audio.play(THUMP_AUDIO_WAV); } - -#[derive(Deserialize, Serialize)] -pub struct AutoSettings { - #[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_half")] - pub watch_view_angle_min: f32, - - #[serde(default = "def_point7")] - pub watch_view_angle_max: f32, - - #[serde(default = "def_true")] - pub notifications_enabled: bool, - - #[serde(default = "def_true")] - pub notifications_sound_enabled: bool, - - #[serde(default = "def_true")] - pub realign_on_showhide: bool, -} - -fn get_config_path() -> PathBuf { - let mut path = config_io::get_conf_d_path(); - path.push("zz-saved-config.json5"); - path -} -pub fn save_settings(app: &mut AppState) -> anyhow::Result<()> { - let conf = AutoSettings { - watch_pos: app.session.config.watch_pos, - watch_rot: app.session.config.watch_rot, - watch_hand: app.session.config.watch_hand, - watch_view_angle_min: app.session.config.watch_view_angle_min, - watch_view_angle_max: app.session.config.watch_view_angle_max, - notifications_enabled: app.session.config.notifications_enabled, - notifications_sound_enabled: app.session.config.notifications_sound_enabled, - realign_on_showhide: app.session.config.realign_on_showhide, - }; - - let json = serde_json::to_string_pretty(&conf).unwrap(); // want panic - std::fs::write(get_config_path(), json)?; - - Ok(()) -} diff --git a/src/overlays/screen.rs b/src/overlays/screen.rs index 4e0933b..fea1d40 100644 --- a/src/overlays/screen.rs +++ b/src/overlays/screen.rs @@ -23,7 +23,7 @@ use wlx_capture::{ use { crate::config_io, glam::Vec3, - std::{collections::HashMap, error::Error, f32::consts::PI, ops::Deref, path::PathBuf}, + std::{error::Error, f32::consts::PI, ops::Deref, path::PathBuf}, wlx_capture::{ pipewire::{pipewire_select_screen, PipewireCapture}, wayland::{wayland_client::protocol::wl_output, WlxClient, WlxOutput}, @@ -42,7 +42,7 @@ use crate::{ input::{Haptics, InteractionHandler, PointerHit, PointerMode}, overlay::{OverlayRenderer, OverlayState, SplitOverlayBackend}, }, - config::def_pw_tokens, + config::{def_pw_tokens, AStrMapExt, PwTokenMap}, graphics::{fourcc_to_vk, WlxCommandBuffer, WlxPipeline, WlxPipelineLegacy}, hid::{MOUSE_LEFT, MOUSE_MIDDLE, MOUSE_RIGHT}, state::{AppSession, AppState, ScreenMeta}, @@ -505,7 +505,7 @@ pub fn create_screen_renderer_wl( output: &WlxOutput, has_wlr_dmabuf: bool, has_wlr_screencopy: bool, - pw_token_store: &mut HashMap, + pw_token_store: &mut PwTokenMap, session: &AppSession, ) -> Option { let mut capture: Option = None; @@ -527,7 +527,7 @@ pub fn create_screen_renderer_wl( let display_name = output.name.deref(); // Find existing token by display - let token = pw_token_store.get(display_name).map(|s| s.as_str()); + let token = pw_token_store.arc_get(display_name).map(|s| s.as_str()); if let Some(t) = token { log::info!( @@ -542,10 +542,7 @@ pub fn create_screen_renderer_wl( capture = Some(renderer); if let Some(token) = restore_token { - if pw_token_store - .insert(String::from(display_name), token.clone()) - .is_none() - { + if pw_token_store.arc_ins(display_name.into(), token.clone()) { log::info!("Adding Pipewire token {}", token); } } @@ -607,11 +604,6 @@ fn create_screen_state( OverlayState { name: name.clone(), - show_hide: session - .config - .show_screens - .iter() - .any(|s| s.as_ref() == name.as_ref()), grabbable: true, recenter: true, interactable: true, @@ -626,7 +618,7 @@ fn create_screen_state( #[derive(Deserialize, Serialize, Default)] pub struct TokenConf { #[serde(default = "def_pw_tokens")] - pub pw_tokens: Vec<(String, String)>, + pub pw_tokens: PwTokenMap, } #[cfg(feature = "wayland")] @@ -637,31 +629,18 @@ fn get_pw_token_path() -> PathBuf { } #[cfg(feature = "wayland")] -pub fn save_pw_token_config(tokens: &HashMap) -> Result<(), Box> { - let mut conf = TokenConf::default(); - - for (name, token) in tokens { - conf.pw_tokens.push((name.clone(), token.clone())); - } - - let yaml = serde_yaml::to_string(&conf)?; +pub fn save_pw_token_config(tokens: &PwTokenMap) -> Result<(), Box> { + let yaml = serde_yaml::to_string(tokens)?; std::fs::write(get_pw_token_path(), yaml)?; Ok(()) } #[cfg(feature = "wayland")] -pub fn load_pw_token_config() -> Result, Box> { - let mut map: HashMap = HashMap::new(); - +pub fn load_pw_token_config() -> Result> { let yaml = std::fs::read_to_string(get_pw_token_path())?; let conf: TokenConf = serde_yaml::from_str(yaml.as_str())?; - - for (name, token) in conf.pw_tokens { - map.insert(name, token); - } - - Ok(map) + Ok(conf.pw_tokens) } pub(crate) struct ScreenCreateData { @@ -681,13 +660,15 @@ pub fn create_screens_wayland( wl: &mut WlxClient, app: &mut AppState, ) -> anyhow::Result { + use crate::config::AStrMap; + let mut screens = vec![]; // Load existing Pipewire tokens from file - let mut pw_tokens: HashMap = if let Ok(conf) = load_pw_token_config() { + let mut pw_tokens: PwTokenMap = if let Ok(conf) = load_pw_token_config() { conf } else { - HashMap::new() + AStrMap::new() }; let pw_tokens_copy = pw_tokens.clone(); diff --git a/src/res/settings.yaml b/src/res/settings.yaml index c38a7ed..39f1839 100644 --- a/src/res/settings.yaml +++ b/src/res/settings.yaml @@ -565,6 +565,17 @@ elements: action: ToggleAutoRealign highlight: AutoRealign + - type: Button + rect: [30, 555, 220, 30] + font_size: 12 + fg_color: "#ffffff" + bg_color: "#401010" + text: "Grab+Scroll Slide" + click_down: + - type: System + action: ToggleAllowSliding + highlight: AllowSliding + ####### Footer Section ####### - type: Panel