persist sets between sessions

This commit is contained in:
galister
2025-11-11 11:44:57 +09:00
parent 98e5d1b93d
commit bef31be9e2
11 changed files with 158 additions and 41 deletions

View File

@@ -28,6 +28,7 @@ use crate::{
task::{SystemTask, TaskType}, task::{SystemTask, TaskType},
BackendError, BackendError,
}, },
config::save_state,
graphics::{init_openvr_graphics, CommandBuffers}, graphics::{init_openvr_graphics, CommandBuffers},
overlays::{ overlays::{
toast::{Toast, ToastTopic}, toast::{Toast, ToastTopic},
@@ -368,8 +369,11 @@ pub fn openvr_run(
} }
// chaperone // chaperone
} // main_loop
// close font handles? overlays.persist_layout(&mut app);
if let Err(e) = save_state(&app.session.config) {
log::error!("Could not save state: {e:?}");
} }
log::warn!("OpenVR shutdown"); log::warn!("OpenVR shutdown");

View File

@@ -22,6 +22,7 @@ use crate::{
task::{SystemTask, TaskType}, task::{SystemTask, TaskType},
BackendError, BackendError,
}, },
config::save_state,
graphics::{init_openxr_graphics, CommandBuffers}, graphics::{init_openxr_graphics, CommandBuffers},
overlays::{ overlays::{
toast::{Toast, ToastTopic}, toast::{Toast, ToastTopic},
@@ -561,6 +562,11 @@ pub fn openxr_run(
//FIXME: Temporary workaround for Monado bug //FIXME: Temporary workaround for Monado bug
let watch = overlays.mut_by_id(watch_id).unwrap(); // want panic let watch = overlays.mut_by_id(watch_id).unwrap(); // want panic
watch.config.active_state.as_mut().unwrap().transform = watch_transform; watch.config.active_state.as_mut().unwrap().transform = watch_transform;
} // main_loop
overlays.persist_layout(&mut app);
if let Err(e) = save_state(&app.session.config) {
log::error!("Could not save state: {e:?}");
} }
Ok(()) Ok(())

View File

@@ -4,6 +4,7 @@ use std::sync::Arc;
use crate::config_io; use crate::config_io;
use crate::overlays::toast::{DisplayMethod, ToastTopic}; use crate::overlays::toast::{DisplayMethod, ToastTopic};
use crate::state::LeftRight; use crate::state::LeftRight;
use crate::windowing::set::SerializedWindowSet;
use chrono::Offset; use chrono::Offset;
use config::{Config, File}; use config::{Config, File};
use glam::{vec3, Affine3A, Quat, Vec3}; use glam::{vec3, Affine3A, Quat, Vec3};
@@ -126,6 +127,14 @@ const fn def_empty_vec_string() -> Vec<String> {
Vec::new() Vec::new()
} }
const fn def_sets() -> Vec<SerializedWindowSet> {
Vec::new()
}
const fn def_zero_u32() -> u32 {
0
}
fn def_timezones() -> Vec<String> { fn def_timezones() -> Vec<String> {
const EMEA: i32 = -60 * 60; // UTC-1 const EMEA: i32 = -60 * 60; // UTC-1
const APAC: i32 = 5 * 60 * 60; // UTC+5 const APAC: i32 = 5 * 60 * 60; // UTC+5
@@ -309,6 +318,12 @@ pub struct GeneralConfig {
#[serde(default = "def_false")] #[serde(default = "def_false")]
pub clock_12h: bool, pub clock_12h: bool,
#[serde(default = "def_sets")]
pub sets: Vec<SerializedWindowSet>,
#[serde(default = "def_zero_u32")]
pub last_set: u32,
} }
impl GeneralConfig { impl GeneralConfig {
@@ -480,9 +495,8 @@ pub fn save_settings(config: &GeneralConfig) -> anyhow::Result<()> {
#[derive(Serialize)] #[derive(Serialize)]
pub struct AutoState { pub struct AutoState {
pub show_screens: AStrSet, pub sets: Vec<SerializedWindowSet>,
pub curve_values: AStrMap<f32>, pub last_set: u32,
pub transform_values: AStrMap<Affine3A>,
} }
fn get_state_path() -> PathBuf { fn get_state_path() -> PathBuf {
@@ -491,15 +505,15 @@ fn get_state_path() -> PathBuf {
.join("zz-saved-state.json5") .join("zz-saved-state.json5")
} }
pub fn save_layout(config: &GeneralConfig) -> anyhow::Result<()> { pub fn save_state(config: &GeneralConfig) -> anyhow::Result<()> {
let conf = AutoState { let conf = AutoState {
show_screens: config.show_screens.clone(), sets: config.sets.clone(),
curve_values: config.curve_values.clone(), last_set: config.last_set.clone(),
transform_values: config.transform_values.clone(),
}; };
let json = serde_json::to_string_pretty(&conf).unwrap(); // want panic let json = serde_json::to_string_pretty(&conf).unwrap(); // want panic
std::fs::write(get_state_path(), json)?; std::fs::write(get_state_path(), json)?;
log::info!("State was saved successfully.");
Ok(()) Ok(())
} }

View File

@@ -17,7 +17,8 @@ use crate::{
}, },
config::load_config_with_conf_d, config::load_config_with_conf_d,
config_io, config_io,
overlays::wayvr::{WayVRData, executable_exists_in_path}, overlays::wayvr::{executable_exists_in_path, WayVRData},
state::LeftRight,
windowing::window::Positioning, windowing::window::Positioning,
}; };
@@ -36,8 +37,14 @@ impl AttachTo {
pub const fn get_positioning(&self) -> Positioning { pub const fn get_positioning(&self) -> Positioning {
match self { match self {
Self::None => Positioning::Floating, Self::None => Positioning::Floating,
Self::HandLeft => Positioning::FollowHand { hand: 0, lerp: 1.0 }, Self::HandLeft => Positioning::FollowHand {
Self::HandRight => Positioning::FollowHand { hand: 1, lerp: 1.0 }, hand: LeftRight::Left,
lerp: 1.0,
},
Self::HandRight => Positioning::FollowHand {
hand: LeftRight::Right,
lerp: 1.0,
},
Self::Stage => Positioning::Static, Self::Stage => Positioning::Static,
Self::Head => Positioning::FollowHead { lerp: 1.0 }, Self::Head => Positioning::FollowHead { lerp: 1.0 },
} }

View File

@@ -147,11 +147,17 @@ fn new_toast(toast: Toast, app: &mut AppState) -> Option<OverlayWindowConfig> {
let mut watch_pos = app.session.config.watch_pos + vec3(-0.005, -0.05, 0.02); let mut watch_pos = app.session.config.watch_pos + vec3(-0.005, -0.05, 0.02);
let mut watch_rot = app.session.config.watch_rot; let mut watch_rot = app.session.config.watch_rot;
let relative_to = match app.session.config.watch_hand { let relative_to = match app.session.config.watch_hand {
LeftRight::Left => Positioning::FollowHand { hand: 0, lerp: 1.0 }, LeftRight::Left => Positioning::FollowHand {
hand: LeftRight::Left,
lerp: 1.0,
},
LeftRight::Right => { LeftRight::Right => {
watch_pos.x = -watch_pos.x; watch_pos.x = -watch_pos.x;
watch_rot = watch_rot * Quat::from_rotation_x(PI) * Quat::from_rotation_z(PI); watch_rot = watch_rot * Quat::from_rotation_x(PI) * Quat::from_rotation_z(PI);
Positioning::FollowHand { hand: 1, lerp: 1.0 } Positioning::FollowHand {
hand: LeftRight::Right,
lerp: 1.0,
}
} }
}; };
(watch_pos, watch_rot, relative_to) (watch_pos, watch_rot, relative_to)

View File

@@ -44,7 +44,7 @@ pub fn create_watch(app: &mut AppState, num_sets: usize) -> anyhow::Result<Overl
.push(GuiTimer::new(Duration::from_millis(100), 0)); .push(GuiTimer::new(Duration::from_millis(100), 0));
let positioning = Positioning::FollowHand { let positioning = Positioning::FollowHand {
hand: app.session.config.watch_hand as _, hand: app.session.config.watch_hand,
lerp: 1.0, lerp: 1.0,
}; };

View File

@@ -1,7 +1,7 @@
use glam::Affine3A; use glam::Affine3A;
use idmap::IdMap; use idmap::IdMap;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use smallvec::{SmallVec, smallvec}; use smallvec::{smallvec, SmallVec};
use std::sync::Arc; use std::sync::Arc;
use wgui::{ use wgui::{
font_config::WguiFontConfig, gfx::WGfx, globals::WguiGlobals, font_config::WguiFontConfig, gfx::WGfx, globals::WguiGlobals,
@@ -189,7 +189,7 @@ pub struct ScreenMeta {
pub native_handle: u32, pub native_handle: u32,
} }
#[derive(Serialize, Deserialize, Clone, Copy, Default)] #[derive(Debug, Serialize, Deserialize, Clone, Copy, Default)]
#[repr(u8)] #[repr(u8)]
pub enum LeftRight { pub enum LeftRight {
#[default] #[default]

View File

@@ -1,5 +1,7 @@
use std::collections::HashMap;
use glam::{Affine3A, Vec3, Vec3A}; use glam::{Affine3A, Vec3, Vec3A};
use slotmap::{HopSlotMap, Key}; use slotmap::{HopSlotMap, Key, SecondaryMap};
use crate::{ use crate::{
overlays::{ overlays::{
@@ -8,7 +10,10 @@ use crate::{
}, },
state::AppState, state::AppState,
windowing::{ windowing::{
set::OverlayWindowSet, snap_upright, window::OverlayWindowData, OverlayID, OverlaySelector, set::{OverlayWindowSet, SerializedWindowSet},
snap_upright,
window::OverlayWindowData,
OverlayID, OverlaySelector,
}, },
}; };
@@ -43,7 +48,9 @@ where
if headless { if headless {
log::info!("Running in headless mode; keyboard will be en-US"); log::info!("Running in headless mode; keyboard will be en-US");
} else { } else {
// create one work set for each screen. // create one window set for each screen.
// this is the default and would be overwritten by
// OverlayWindowManager::restore_layout down below
match create_screens(app) { match create_screens(app) {
Ok((data, keymap)) => { Ok((data, keymap)) => {
let last_idx = data.screens.len() - 1; let last_idx = data.screens.len() - 1;
@@ -68,6 +75,7 @@ where
keyboard.config.show_on_spawn = true; keyboard.config.show_on_spawn = true;
let keyboard_id = me.add(keyboard, app); let keyboard_id = me.add(keyboard, app);
// is this needed?
me.switch_to_set(app, None); me.switch_to_set(app, None);
// copy keyboard to all sets // copy keyboard to all sets
@@ -87,13 +95,79 @@ where
let watch = OverlayWindowData::from_config(create_watch(app, me.sets.len())?); let watch = OverlayWindowData::from_config(create_watch(app, me.sets.len())?);
me.watch_id = me.add(watch, app); me.watch_id = me.add(watch, app);
me.switch_to_set(app, None); // overwrite default layout with saved layout, if exists
me.restore_layout(app);
Ok(me) Ok(me)
} }
} }
impl<T> OverlayWindowManager<T> { impl<T> OverlayWindowManager<T> {
pub fn persist_layout(&mut self, app: &mut AppState) {
app.session.config.sets.clear();
app.session.config.sets.reserve(self.sets.len());
app.session.config.last_set = self.restore_set as _;
let mut restore_after = false;
// only safe to save when current_set is None
if self.current_set.is_some() {
self.switch_to_set(app, None);
restore_after = true;
}
for set in self.sets.iter() {
let overlays: HashMap<_, _> = set
.overlays
.iter()
.filter_map(|(k, v)| {
let Some(n) = self.overlays.get(k).map(|o| o.config.name.clone()) else {
return None;
};
Some((n, v.clone()))
})
.collect();
let serialized = SerializedWindowSet {
name: set.name.clone(),
overlays,
};
app.session.config.sets.push(serialized);
}
if restore_after {
self.switch_to_set(app, Some(self.restore_set));
}
}
pub fn restore_layout(&mut self, app: &mut AppState) {
if app.session.config.sets.is_empty() {
// keep defaults
return;
}
// only safe to load when current_set is None
if self.current_set.is_some() {
self.switch_to_set(app, None);
}
self.sets.clear();
self.sets.reserve(app.session.config.sets.len());
for s in app.session.config.sets.iter() {
let overlays: SecondaryMap<_, _> = s
.overlays
.iter()
.filter_map(|(name, v)| self.lookup(&name).map(|id| (id, v.clone())))
.collect();
self.sets.push(OverlayWindowSet {
name: s.name.clone(),
overlays,
});
}
self.restore_set = (app.session.config.last_set as usize).min(self.sets.len() - 1);
}
pub fn mut_by_selector( pub fn mut_by_selector(
&mut self, &mut self,
selector: &OverlaySelector, selector: &OverlaySelector,
@@ -151,7 +225,6 @@ impl<T> OverlayWindowManager<T> {
if overlay.config.show_on_spawn { if overlay.config.show_on_spawn {
overlay.config.activate(app); overlay.config.activate(app);
} }
self.overlays.insert(overlay) self.overlays.insert(overlay)
} }
@@ -188,7 +261,7 @@ impl<T> OverlayWindowManager<T> {
if let Some(new_set) = new_set { if let Some(new_set) = new_set {
if new_set >= self.sets.len() { if new_set >= self.sets.len() {
log::warn!("switch_to_set: new_set is out of range ({new_set:?})"); log::error!("switch_to_set: new_set is out of range ({new_set:?})");
return; return;
} }

View File

@@ -4,7 +4,7 @@ use std::sync::Arc;
pub mod backend; pub mod backend;
pub mod manager; pub mod manager;
mod set; pub mod set;
pub mod window; pub mod window;
new_key_type! { new_key_type! {

View File

@@ -1,8 +1,18 @@
use std::{collections::HashMap, sync::Arc};
use serde::{Deserialize, Serialize};
use slotmap::SecondaryMap; use slotmap::SecondaryMap;
use crate::windowing::{window::OverlayWindowState, OverlayID}; use crate::windowing::{window::OverlayWindowState, OverlayID};
#[derive(Default)] #[derive(Default)]
pub struct OverlayWindowSet { pub struct OverlayWindowSet {
pub(super) name: Arc<str>,
pub(super) overlays: SecondaryMap<OverlayID, OverlayWindowState>, pub(super) overlays: SecondaryMap<OverlayID, OverlayWindowState>,
} }
#[derive(Clone, Serialize, Deserialize)]
pub struct SerializedWindowSet {
pub name: Arc<str>,
pub overlays: HashMap<Arc<str>, OverlayWindowState>,
}

View File

@@ -1,10 +1,11 @@
use glam::{Affine3A, Mat3A, Quat, Vec3, Vec3A}; use glam::{Affine3A, Mat3A, Quat, Vec3, Vec3A};
use serde::{Deserialize, Serialize};
use std::{f32::consts::PI, sync::Arc}; use std::{f32::consts::PI, sync::Arc};
use vulkano::image::view::ImageView; use vulkano::image::view::ImageView;
use crate::{ use crate::{
graphics::CommandBuffers, graphics::CommandBuffers,
state::AppState, state::{AppState, LeftRight},
subsystem::input::KeyboardFocus, subsystem::input::KeyboardFocus,
windowing::{ windowing::{
backend::{FrameMeta, OverlayBackend, ShouldRender}, backend::{FrameMeta, OverlayBackend, ShouldRender},
@@ -12,7 +13,7 @@ use crate::{
}, },
}; };
#[derive(Clone, Copy, Debug, Default)] #[derive(Clone, Copy, Debug, Default, Serialize, Deserialize)]
pub enum Positioning { pub enum Positioning {
/// Stays in place, recenters relative to HMD /// Stays in place, recenters relative to HMD
#[default] #[default]
@@ -26,19 +27,14 @@ pub enum Positioning {
/// Normally follows HMD, but paused due to interaction /// Normally follows HMD, but paused due to interaction
FollowHeadPaused { lerp: f32 }, FollowHeadPaused { lerp: f32 },
/// Following hand /// Following hand
FollowHand { hand: usize, lerp: f32 }, FollowHand { hand: LeftRight, lerp: f32 },
/// Normally follows hand, but paused due to interaction /// Normally follows hand, but paused due to interaction
FollowHandPaused { hand: usize, lerp: f32 }, FollowHandPaused { hand: LeftRight, lerp: f32 },
/// Follow another overlay
FollowOverlay { id: usize },
} }
impl Positioning { impl Positioning {
pub const fn moves_with_space(&self) -> bool { pub const fn moves_with_space(&self) -> bool {
matches!( matches!(self, Self::Floating | Self::Anchored | Self::Static)
self,
Self::Floating | Self::Anchored | Self::Static
)
} }
} }
@@ -154,9 +150,10 @@ impl OverlayWindowConfig {
let (target_transform, lerp) = match state.positioning { let (target_transform, lerp) = match state.positioning {
Positioning::FollowHead { lerp } => (app.input_state.hmd * cur_transform, lerp), Positioning::FollowHead { lerp } => (app.input_state.hmd * cur_transform, lerp),
Positioning::FollowHand { hand, lerp } => { Positioning::FollowHand { hand, lerp } => (
(app.input_state.pointers[hand].pose * cur_transform, lerp) app.input_state.pointers[hand as usize].pose * cur_transform,
} lerp,
),
_ => return, _ => return,
}; };
@@ -196,10 +193,10 @@ impl OverlayWindowConfig {
app.input_state.hmd app.input_state.hmd
} }
Positioning::FollowHand { hand, .. } | Positioning::FollowHandPaused { hand, .. } => { Positioning::FollowHand { hand, .. } | Positioning::FollowHandPaused { hand, .. } => {
app.input_state.pointers[hand].pose app.input_state.pointers[hand as usize].pose
} }
Positioning::Anchored => snap_upright(app.anchor, Vec3A::Y), Positioning::Anchored => snap_upright(app.anchor, Vec3A::Y),
Positioning::FollowOverlay { .. } | Positioning::Static => return false, Positioning::Static => return false,
}; };
self.saved_transform = Some(parent_transform.inverse() * state.transform); self.saved_transform = Some(parent_transform.inverse() * state.transform);
@@ -219,10 +216,10 @@ impl OverlayWindowConfig {
| Positioning::FollowHead { .. } | Positioning::FollowHead { .. }
| Positioning::FollowHeadPaused { .. } => app.input_state.hmd, | Positioning::FollowHeadPaused { .. } => app.input_state.hmd,
Positioning::FollowHand { hand, .. } | Positioning::FollowHandPaused { hand, .. } => { Positioning::FollowHand { hand, .. } | Positioning::FollowHandPaused { hand, .. } => {
app.input_state.pointers[hand].pose app.input_state.pointers[hand as usize].pose
} }
Positioning::Anchored => app.anchor, Positioning::Anchored => app.anchor,
Positioning::FollowOverlay { .. } | Positioning::Static => return, Positioning::Static => return,
}; };
if hard_reset { if hard_reset {
@@ -284,7 +281,7 @@ impl OverlayWindowConfig {
} }
// Contains the window state for a given set // Contains the window state for a given set
#[derive(Clone)] #[derive(Clone, Serialize, Deserialize)]
pub struct OverlayWindowState { pub struct OverlayWindowState {
pub transform: Affine3A, pub transform: Affine3A,
pub alpha: f32, pub alpha: f32,