fix persisting global & static overlays

This commit is contained in:
galister 2026-08-02 15:17:36 +09:00
parent ebb912db68
commit 05aac47716
12 changed files with 156 additions and 79 deletions

View File

@ -120,7 +120,7 @@
"DEFAULT_CURVATURE": "Default overlay curvature",
"DEFAULT_OPACITY": "Default overlay opacity",
"DEFAULT_POSITIONING": "Default overlay positioning",
"DEFAULT_OVERLAY_DEFAULTS_HELP": "Only applies to new overlays",
"OVERLAY_DEFAULTS_HELP": "Only applies to new overlays",
"ENABLED": "Enabled",
"FEATURES": "Features",
"FOCUS_FOLLOWS_MOUSE_MODE": "Mouse move on trigger touch",

View File

@ -851,7 +851,7 @@ fn start_grab(
app.tasks.enqueue(TaskType::Overlay(OverlayTask::Modify(
OverlaySelector::Name(ANCHOR_NAME.clone()),
Box::new(|app, o| {
o.activate(app);
o.activate(app, true);
}),
)));
@ -870,7 +870,7 @@ fn start_grab(
o.default_state.positioning = Positioning::FollowHand { hand, lerp: 0.1 };
o.default_state.align_to_hmd = true;
o.activate(app);
o.activate(app, true);
}),
)));
}

View File

@ -354,7 +354,7 @@ pub fn openxr_run(args: &Args) -> Result<(), BackendError> {
let watch = overlays.mut_by_id(watch_id).unwrap(); // want panic
if watch.config.active_state.is_none() {
watch.config.activate(&mut app);
watch.config.activate(&mut app, true);
}
let watch_state = watch.config.active_state.as_mut().unwrap();
let watch_transform = watch_state.transform;

View File

@ -6,6 +6,7 @@ use std::{
};
use serde::Deserialize;
use strum::EnumIs;
use crate::{
backend::input,
@ -97,10 +98,12 @@ pub enum ToggleMode {
Toggle,
}
#[derive(Clone)]
#[derive(Clone, EnumIs)]
pub enum SpawnPos {
/// Always spawn at the designated pos
/// Always spawn at designated position
Fixed,
/// Always spawn at designated position and orientation
FixedNoRealign,
/// Automatically spread out for user experience
Spread,
/// Spawn relative to a different overlay

View File

@ -956,7 +956,7 @@ impl WvrServerState {
tasks.enqueue(TaskType::Overlay(OverlayTask::Modify(
OverlaySelector::Name(ALTTAB_HELP_NAME.clone()),
Box::new(move |app, o| {
o.activate(app);
o.activate(app, true);
}),
)));
} else {

View File

@ -11,7 +11,7 @@ use wlx_common::{
config::{
AltModifier, CaptureMethod, ChromaKeyParams, DefaultPositioning, GeneralConfig,
HandsfreeAltTab, HandsfreePointer, InputCaptureMethod, InputEmulationMethod, PinnedApp,
SerializedWindowSet, SerializedWindowStates,
SerializedWindowSet,
},
config_io,
locale::Language,
@ -287,7 +287,7 @@ pub fn save_settings(config: &GeneralConfig) -> anyhow::Result<()> {
#[derive(Serialize)]
pub struct AutoState {
pub sets: Vec<SerializedWindowSet>,
pub global_set: SerializedWindowStates,
pub spawn_overlays: Vec<Arc<str>>,
pub last_set: u32,
pub attribs: AStrMap<Vec<BackendAttribValue>>,
}
@ -302,7 +302,7 @@ pub fn save_state(config: &GeneralConfig) -> anyhow::Result<()> {
let conf = AutoState {
sets: config.sets.clone(),
last_set: config.last_set,
global_set: config.global_set.clone(),
spawn_overlays: config.spawn_overlays.clone(),
attribs: config.attribs.clone(),
};

View File

@ -562,7 +562,9 @@ pub(super) fn setup_custom_button<S: 'static>(
return Ok(EventResult::Pass);
}
let name = crate::overlays::passthrough::new_passtrhu_name();
let name = crate::overlays::passthrough::new_passthru_name(
&app.session.config.spawn_overlays,
);
app.tasks.enqueue(TaskType::Overlay(OverlayTask::Spawn(
OverlaySelector::Name(name.clone()),
SpawnPos::Spread,

View File

@ -27,16 +27,22 @@ use crate::{
},
};
pub const PASSTHRU_PREFIX: &str = "__wvr_passthru-";
pub const PASSTHRU_PREFIX: &str = "Passthru-";
static PASSTHRU_COUNTER: AtomicUsize = AtomicUsize::new(1);
pub fn new_passtrhu_name() -> Arc<str> {
format!(
"{PASSTHRU_PREFIX}{}",
PASSTHRU_COUNTER.fetch_add(1, Ordering::Relaxed)
)
.into()
pub fn new_passthru_name(existing: &[Arc<str>]) -> Arc<str> {
let mut max_num = PASSTHRU_COUNTER.load(Ordering::Relaxed) - 1;
for name in existing {
if let Some(n) = name
.strip_prefix(PASSTHRU_PREFIX)
.and_then(|s| s.parse::<usize>().ok())
{
max_num = max_num.max(n);
}
}
let next = max_num + 1;
format!("{PASSTHRU_PREFIX}{next}").into()
}
pub fn new_passthru(name: Arc<str>, app: &AppState) -> OverlayWindowConfig {

View File

@ -24,6 +24,7 @@ use crate::{
dashboard::{DASH_NAME, create_dash_frontend},
edit::EditWrapperManager,
keyboard::create_keyboard,
passthrough::{PASSTHRU_PREFIX, new_passthru},
screen::create_screens,
toast::Toast,
watch::{WATCH_NAME, create_watch},
@ -42,6 +43,7 @@ use crate::{
};
pub const MAX_OVERLAY_SETS: usize = 6;
pub const GLOBAL_SET: &str = "global";
pub struct OverlayWindowManager<T> {
wrappers: EditWrapperManager,
@ -163,20 +165,21 @@ where
let saved_passthrus: Vec<_> = app
.session
.config
.global_set
.spawn_overlays
.iter()
.filter(|(name, _)| name.starts_with(crate::overlays::passthrough::PASSTHRU_PREFIX))
.map(|(name, state)| (name.clone(), state.clone()))
.filter(|name| name.starts_with(PASSTHRU_PREFIX))
.cloned()
.collect();
for (name, saved_state) in saved_passthrus {
for name in saved_passthrus {
if me.lookup(&name).is_none() {
let config = crate::overlays::passthrough::new_passthru(name.clone(), app);
let id = me.add(OverlayWindowData::from_config(config), app);
if let Some(o) = me.mut_by_id(id) {
o.config.active_state = Some(saved_state);
o.config.reset(app, false);
log::debug!("restored passthru {}", name);
}
let mut config = new_passthru(name.clone(), app);
config.show_on_spawn = !me.global_set.hidden_overlays.arc_get(&*name).is_some();
me.add_with_spawn_pos(
OverlayWindowData::from_config(config),
app,
SpawnPos::FixedNoRealign,
);
}
}
@ -208,7 +211,7 @@ where
OverlayTask::ResetOverlay(sel) => {
if let Some(o) = self.mut_by_selector(&sel) {
let was_active = o.config.is_active();
o.config.activate(app);
o.config.activate(app, true);
if !was_active {
self.visible_overlays_changed(app)?;
}
@ -260,7 +263,7 @@ where
o.config.reset(app, false);
} else {
// no saved state
o.config.activate(app);
o.config.activate(app, true);
}
self.visible_overlays_changed(app)?;
}
@ -272,7 +275,7 @@ where
self.mut_by_selector(&OverlaySelector::Name(DASH_NAME.into()))
{
if overlay.config.active_state.is_none() {
overlay.config.activate(app);
overlay.config.activate(app, true);
} else {
overlay.config.deactivate();
}
@ -294,7 +297,7 @@ where
}
self.sets.push(OverlayWindowSet::default());
self.switch_to_set(app, Some(new_idx), false);
self.overlays[self.keyboard_id].config.activate(app);
self.overlays[self.keyboard_id].config.activate(app, true);
self.sets_changed(app);
self.visible_overlays_changed(app)?;
}
@ -335,7 +338,7 @@ where
if watch.config.active_state.is_some() {
watch.config.deactivate();
} else {
watch.config.activate(app);
watch.config.activate(app, true);
}
}
@ -481,10 +484,11 @@ where
}
}
const SAVED_ATTRIBS: [BackendAttrib; 3] = [
const SAVED_ATTRIBS: &[BackendAttrib] = &[
BackendAttrib::Stereo,
BackendAttrib::StereoFullFrame,
BackendAttrib::MouseTransform,
BackendAttrib::WindowSize,
];
impl<T> OverlayWindowManager<T> {
@ -503,7 +507,7 @@ impl<T> OverlayWindowManager<T> {
}
pub fn persist_layout(&mut self, app: &mut AppState) {
app.session.config.global_set.clear();
app.session.config.spawn_overlays.clear();
app.session.config.sets.clear();
app.session.config.sets.reserve(self.sets.len());
app.session.config.last_set = self.restore_set as _;
@ -533,7 +537,12 @@ impl<T> OverlayWindowManager<T> {
}
}
let hidden_overlays: HashMap<_, _> = set.hidden_overlays.iter().cloned().collect();
let hidden_overlays: HashMap<_, _> = set
.hidden_overlays
.iter()
.filter(|(k, _)| !overlays.contains_key(k.as_ref()))
.map(|(k, v)| (k.clone(), v.clone()))
.collect();
let serialized = SerializedWindowSet {
name: set.name.clone(),
@ -543,29 +552,38 @@ impl<T> OverlayWindowManager<T> {
app.session.config.sets.push(serialized);
}
// global overlays
for o in self.overlays.values() {
if matches!(o.config.category, OverlayCategory::Passthru) {
app.session
.config
.spawn_overlays
.push(o.config.name.clone());
}
}
// serialize global set into `sets` with key GLOBAL_SET
// overlays: active states + inactive states; hidden_overlays: explicitly toggled off
let mut global_overlays: HashMap<_, _> =
self.global_set.inactive_overlays.iter().cloned().collect();
for o in self.overlays.values() {
if o.config.global {
if let Some(state) = &o.config.active_state {
app.session
.config
.global_set
.insert(o.config.name.clone(), state.clone());
global_overlays.insert(o.config.name.clone(), state.clone());
}
}
}
for (name, state) in &self.global_set.hidden_overlays {
app.session
.config
.global_set
.insert(name.clone(), state.clone());
}
for (name, state) in &self.global_set.inactive_overlays {
app.session
.config
.global_set
.insert(name.clone(), state.clone());
}
let global_hidden: HashMap<_, _> = self
.global_set
.hidden_overlays
.iter()
.filter(|(k, _)| !global_overlays.contains_key(k.as_ref()))
.map(|(k, v)| (k.clone(), v.clone()))
.collect();
app.session.config.sets.push(SerializedWindowSet {
name: GLOBAL_SET.into(),
overlays: global_overlays,
hidden_overlays: global_hidden,
});
// BackendAttrib
for o in self.overlays.values() {
@ -599,6 +617,10 @@ impl<T> OverlayWindowManager<T> {
self.sets.reserve(app.session.config.sets.len());
for (i, s) in app.session.config.sets.iter().enumerate() {
if s.name.as_ref() == GLOBAL_SET {
continue;
}
let mut overlays = SecondaryMap::new();
let mut inactive_overlays = AStrMap::new();
@ -628,33 +650,69 @@ impl<T> OverlayWindowManager<T> {
});
}
// global overlays
for (name, ows) in app.session.config.global_set.clone() {
let mut ows = ows.clone();
// global overlays: prefer `sets` with key GLOBAL_SET, fall back to legacy `global_set`
if let Some(global_s) = app
.session
.config
.sets
.iter()
.find(|s| s.name.as_ref() == GLOBAL_SET)
{
let global_s = global_s.clone();
let mut overlays = SecondaryMap::new();
let mut inactive_overlays = AStrMap::new();
// fix angle_fade missing on watch if loading older state
if name.as_ref() == WATCH_NAME {
ows.angle_fade = true;
for (name, o) in &global_s.overlays {
if let Some(id) = self.lookup(name) {
log::debug!("global set: loaded state for {name}");
overlays.insert(id, o.clone());
if let Some(ov) = self.mut_by_id(id)
&& ov.config.active_state.is_some()
{
ov.config.global = true;
ov.config.active_state = Some(o.clone());
ov.config.reset(app, false);
}
} else {
log::debug!(
"global set has saved state for {name} which doesn't exist. will apply state once added."
);
inactive_overlays.arc_set(name.clone(), o.clone());
}
}
if let Some(oid) = self.lookup(&name)
&& let Some(o) = self.mut_by_id(oid)
{
o.config.global = true;
if o.config.active_state.is_none() {
self.global_set.hidden_overlays.arc_set(name.clone(), ows);
} else {
let hidden_overlays: AStrMap<_> = global_s
.hidden_overlays
.iter()
.map(|(k, v)| (k.clone(), v.clone()))
.collect();
self.global_set.overlays = overlays;
self.global_set.inactive_overlays = inactive_overlays;
self.global_set.hidden_overlays = hidden_overlays;
} else {
for (name, ows) in app.session.config.global_set.clone() {
let mut ows = ows.clone();
// fix angle_fade missing on watch if loading older state
if name.as_ref() == WATCH_NAME {
ows.angle_fade = true;
}
if let Some(oid) = self.lookup(&name)
&& let Some(o) = self.mut_by_id(oid)
&& o.config.active_state.is_some()
{
o.config.global = true;
o.config.active_state = Some(ows);
o.config.reset(app, false);
log::debug!("global set: loaded state for {name}");
} else {
log::debug!(
"global set has saved state for {name} which doesn't exist or is inactive. will apply state once added."
);
self.global_set.inactive_overlays.arc_set(name.clone(), ows);
}
log::debug!("global set: loaded state for {name}");
} else {
log::debug!(
"global set has saved state for {name} which doesn't exist. will apply state once added."
);
self.global_set
.inactive_overlays
.arc_set(name.clone(), ows.clone());
}
}
@ -875,7 +933,9 @@ impl<T> OverlayWindowManager<T> {
if !shown && show_on_spawn {
log::debug!("activating {name} due to show_on_spawn");
self.overlays[oid].config.activate(app);
self.overlays[oid]
.config
.activate(app, !spawn_pos.is_fixed_no_realign());
self.apply_spawn_pos(app, oid, spawn_pos);
}
if !internal && let Err(e) = self.overlays_changed(app) {
@ -889,7 +949,7 @@ impl<T> OverlayWindowManager<T> {
fn apply_spawn_pos(&mut self, app: &mut AppState, oid: OverlayID, spawn_pos: SpawnPos) {
match spawn_pos {
SpawnPos::Fixed => {}
SpawnPos::Fixed | SpawnPos::FixedNoRealign => {}
SpawnPos::Spread => {
let Some(parent_id) = self.spread_parent_for(oid) else {
return;

View File

@ -122,11 +122,11 @@ impl OverlayWindowConfig {
/// only call this directly for `OverlayCategory::Internal`
/// for anything else, use `OverlayTask::ToggleOverlay` instead
pub fn activate(&mut self, app: &mut AppState) {
pub fn activate(&mut self, app: &mut AppState, realign: bool) {
log::debug!("activate {}", self.name.as_ref());
self.dirty = true;
self.active_state = Some(self.default_state.clone());
self.reset(app, true);
self.reset(app, realign);
}
/// only call this directly for `OverlayCategory::Internal`

View File

@ -498,8 +498,13 @@ pub struct GeneralConfig {
pub sets: Vec<SerializedWindowSet>,
#[serde(default)]
/// Obsolete, only used for reading
/// Use `sets` with `name: "global"`
pub global_set: SerializedWindowStates,
#[serde(default)]
pub spawn_overlays: Vec<Arc<str>>,
#[serde(default)]
pub autostart_apps: Vec<WvrProcessLaunchParams>,

View File

@ -27,6 +27,7 @@ pub enum BackendAttrib {
MouseTransform,
Icon,
Resizable,
WindowSize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]