1use serde::{Deserialize, Serialize};
2use std::collections::{HashMap, HashSet};
3use std::fs;
4use std::path::PathBuf;
5use std::sync::{mpsc, Arc, Mutex, Weak};
6use std::thread;
7use std::time::Duration;
8
9const GEOMETRY_FLUSH_DEBOUNCE: Duration = Duration::from_millis(80);
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum WindowSystemErrorKind {
13 InvalidLabel,
14 WindowAlreadyExists,
15 WindowCannotBeItsOwnParent,
16 ParentWindowNotFound,
17 WindowNotFound,
18 WindowReservationNotFound,
19 WindowRegistryLockPoisoned,
20 WindowStateStoreLockPoisoned,
21}
22
23impl WindowSystemErrorKind {
24 pub fn as_code(self) -> &'static str {
25 match self {
26 Self::InvalidLabel => "invalid-label",
27 Self::WindowAlreadyExists => "window-already-exists",
28 Self::WindowCannotBeItsOwnParent => "window-cannot-be-its-own-parent",
29 Self::ParentWindowNotFound => "parent-window-not-found",
30 Self::WindowNotFound => "window-not-found",
31 Self::WindowReservationNotFound => "window-reservation-not-found",
32 Self::WindowRegistryLockPoisoned => "window-registry-lock-poisoned",
33 Self::WindowStateStoreLockPoisoned => "window-state-store-lock-poisoned",
34 }
35 }
36}
37
38pub fn window_system_error(kind: WindowSystemErrorKind, message: impl Into<String>) -> String {
39 format!("{}: {}", kind.as_code(), message.into())
40}
41
42#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
43pub struct WindowGeometry {
44 pub x: f64,
45 pub y: f64,
46 pub width: f64,
47 pub height: f64,
48}
49
50#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
51pub struct WindowDescriptor {
52 pub label: String,
53 pub url: String,
54 pub parent: Option<String>,
55 pub title: Option<String>,
56 pub geometry: Option<WindowGeometry>,
57}
58
59#[derive(Debug, Default, Clone, Serialize, Deserialize)]
60struct WindowStateSnapshot {
61 windows: HashMap<String, WindowGeometry>,
62 tracked_windows: HashMap<String, WindowDescriptor>,
63}
64
65#[derive(Default)]
66struct WindowRegistryState {
67 windows: HashMap<String, WindowDescriptor>,
68 children: HashMap<String, HashSet<String>>,
69 reserved: HashSet<String>,
70 closing: HashSet<String>,
71}
72
73#[derive(Clone)]
74pub struct WindowStateStore {
75 inner: Arc<WindowStateStoreInner>,
76}
77
78struct WindowStateStoreInner {
79 path: PathBuf,
80 snapshot: Mutex<WindowStateSnapshot>,
81 flush_signal: mpsc::SyncSender<()>,
82}
83
84impl WindowStateStore {
85 pub fn new(path: PathBuf) -> Self {
86 let snapshot = load_snapshot(&path).unwrap_or_default();
87 let (flush_signal, flush_receiver) = mpsc::sync_channel(1);
88
89 let inner = Arc::new(WindowStateStoreInner {
90 path,
91 snapshot: Mutex::new(snapshot),
92 flush_signal,
93 });
94
95 spawn_geometry_flusher(Arc::downgrade(&inner), flush_receiver);
96
97 Self { inner }
98 }
99
100 pub fn restore(&self, label: &str) -> Option<WindowGeometry> {
101 self.inner
102 .snapshot
103 .lock()
104 .ok()
105 .and_then(|snapshot| snapshot.windows.get(label).cloned())
106 }
107
108 pub fn restore_tracked_windows(&self) -> Vec<WindowDescriptor> {
109 self.inner
110 .snapshot
111 .lock()
112 .ok()
113 .map(|snapshot| {
114 let mut windows: Vec<_> = snapshot
115 .tracked_windows
116 .values()
117 .cloned()
118 .map(|mut descriptor| {
119 if descriptor.geometry.is_none() {
120 descriptor.geometry = snapshot.windows.get(&descriptor.label).cloned();
121 }
122 descriptor
123 })
124 .collect();
125 windows.sort_by(|left, right| left.label.cmp(&right.label));
126 windows
127 })
128 .unwrap_or_default()
129 }
130
131 pub fn remember(&self, label: &str, geometry: WindowGeometry) -> Result<(), String> {
132 let mut snapshot = self.inner.snapshot.lock().map_err(|_| {
133 window_system_error(
134 WindowSystemErrorKind::WindowStateStoreLockPoisoned,
135 "window state store lock poisoned",
136 )
137 })?;
138 if snapshot.windows.get(label) == Some(&geometry) {
139 return Ok(());
140 }
141
142 snapshot.windows.insert(label.to_string(), geometry);
143 drop(snapshot);
144 self.schedule_flush();
145 Ok(())
146 }
147
148 pub fn remember_window(&self, descriptor: WindowDescriptor) -> Result<(), String> {
149 let mut snapshot = self.inner.snapshot.lock().map_err(|_| {
150 window_system_error(
151 WindowSystemErrorKind::WindowStateStoreLockPoisoned,
152 "window state store lock poisoned",
153 )
154 })?;
155
156 if snapshot.tracked_windows.get(&descriptor.label) == Some(&descriptor) {
157 return Ok(());
158 }
159
160 snapshot
161 .tracked_windows
162 .insert(descriptor.label.clone(), descriptor);
163 drop(snapshot);
164 self.schedule_flush();
165 Ok(())
166 }
167
168 pub fn forget(&self, label: &str) -> Result<(), String> {
169 let mut snapshot = self.inner.snapshot.lock().map_err(|_| {
170 window_system_error(
171 WindowSystemErrorKind::WindowStateStoreLockPoisoned,
172 "window state store lock poisoned",
173 )
174 })?;
175 if snapshot.windows.remove(label).is_none() {
176 return Ok(());
177 }
178
179 drop(snapshot);
180 self.schedule_flush();
181 Ok(())
182 }
183
184 pub fn forget_window(&self, label: &str) -> Result<(), String> {
185 let mut snapshot = self.inner.snapshot.lock().map_err(|_| {
186 window_system_error(
187 WindowSystemErrorKind::WindowStateStoreLockPoisoned,
188 "window state store lock poisoned",
189 )
190 })?;
191
192 if snapshot.tracked_windows.remove(label).is_some() {
193 drop(snapshot);
194 self.schedule_flush();
195 }
196
197 Ok(())
198 }
199
200 pub fn purge_window(&self, label: &str) -> Result<bool, String> {
205 let mut snapshot = self.inner.snapshot.lock().map_err(|_| {
206 window_system_error(
207 WindowSystemErrorKind::WindowStateStoreLockPoisoned,
208 "window state store lock poisoned",
209 )
210 })?;
211
212 let removed_geometry = snapshot.windows.remove(label).is_some();
213 let removed_descriptor = snapshot.tracked_windows.remove(label).is_some();
214
215 if !removed_geometry && !removed_descriptor {
216 return Ok(false);
217 }
218
219 drop(snapshot);
220 self.schedule_flush();
221 Ok(true)
222 }
223
224 fn schedule_flush(&self) {
225 let _ = self.inner.flush_signal.try_send(());
226 }
227
228 pub fn flush(&self) -> Result<(), String> {
229 self.inner.flush_snapshot()
230 }
231}
232
233impl WindowStateStoreInner {
234 fn flush_snapshot(&self) -> Result<(), String> {
235 let snapshot = self
236 .snapshot
237 .lock()
238 .map_err(|_| {
239 window_system_error(
240 WindowSystemErrorKind::WindowStateStoreLockPoisoned,
241 "window state store lock poisoned",
242 )
243 })?
244 .clone();
245
246 persist_snapshot(&self.path, &snapshot)
247 }
248}
249
250impl Drop for WindowStateStoreInner {
251 fn drop(&mut self) {
252 if let Err(err) = self.flush_snapshot() {
253 eprintln!("window-system: final geometry flush failed: {err}");
254 }
255 }
256}
257
258#[derive(Clone)]
259pub struct WindowRegistry {
260 inner: Arc<WindowRegistryInner>,
261}
262
263struct WindowRegistryInner {
264 state: Mutex<WindowRegistryState>,
265 state_store: WindowStateStore,
266}
267
268pub struct WindowReservation {
269 registry: WindowRegistry,
270 label: String,
271 committed: bool,
272}
273
274impl WindowReservation {
275 pub fn commit(mut self) {
276 self.committed = true;
277 }
278}
279
280impl Drop for WindowReservation {
281 fn drop(&mut self) {
282 if !self.committed {
283 self.registry.release_reservation(&self.label);
284 }
285 }
286}
287
288pub struct ClosingGuard {
289 registry: WindowRegistry,
290 label: String,
291}
292
293impl Drop for ClosingGuard {
294 fn drop(&mut self) {
295 self.registry.finish_closing(&self.label);
296 }
297}
298
299impl WindowRegistry {
300 pub fn new(state_store: WindowStateStore) -> Self {
301 Self {
302 inner: Arc::new(WindowRegistryInner {
303 state: Mutex::new(WindowRegistryState::default()),
304 state_store,
305 }),
306 }
307 }
308
309 pub fn reserve_window(
310 &self,
311 label: &str,
312 parent: Option<&str>,
313 ) -> Result<WindowReservation, String> {
314 let mut state = self.inner.state.lock().map_err(|_| {
315 window_system_error(
316 WindowSystemErrorKind::WindowRegistryLockPoisoned,
317 "window registry lock poisoned",
318 )
319 })?;
320
321 if label.trim().is_empty() {
322 return Err(window_system_error(
323 WindowSystemErrorKind::InvalidLabel,
324 "label is required",
325 ));
326 }
327
328 if state.windows.contains_key(label)
329 || state.reserved.contains(label)
330 || state.closing.contains(label)
331 {
332 return Err(window_system_error(
333 WindowSystemErrorKind::WindowAlreadyExists,
334 "window already exists",
335 ));
336 }
337
338 if let Some(parent) = parent {
339 if parent == label {
340 return Err(window_system_error(
341 WindowSystemErrorKind::WindowCannotBeItsOwnParent,
342 "window cannot be its own parent",
343 ));
344 }
345
346 if !state.windows.contains_key(parent) || state.closing.contains(parent) {
347 return Err(window_system_error(
348 WindowSystemErrorKind::ParentWindowNotFound,
349 format!("parent window not found: {parent}"),
350 ));
351 }
352 }
353
354 state.reserved.insert(label.to_string());
355
356 Ok(WindowReservation {
357 registry: self.clone(),
358 label: label.to_string(),
359 committed: false,
360 })
361 }
362
363 pub fn insert_reserved(&self, descriptor: WindowDescriptor) -> Result<(), String> {
364 let persisted_descriptor = descriptor.clone();
365 let mut state = self.inner.state.lock().map_err(|_| {
366 window_system_error(
367 WindowSystemErrorKind::WindowRegistryLockPoisoned,
368 "window registry lock poisoned",
369 )
370 })?;
371
372 if !state.reserved.remove(&descriptor.label) {
373 return Err(window_system_error(
374 WindowSystemErrorKind::WindowReservationNotFound,
375 "window reservation not found",
376 ));
377 }
378
379 state.update_window(descriptor);
380 drop(state);
381 self.inner
382 .state_store
383 .remember_window(persisted_descriptor)?;
384 Ok(())
385 }
386
387 pub fn upsert_live_window(&self, descriptor: WindowDescriptor) -> Result<bool, String> {
392 let persisted_descriptor = descriptor.clone();
393 let mut state = self.inner.state.lock().map_err(|_| {
394 window_system_error(
395 WindowSystemErrorKind::WindowRegistryLockPoisoned,
396 "window registry lock poisoned",
397 )
398 })?;
399
400 if state.windows.get(&descriptor.label) == Some(&descriptor) {
401 return Ok(false);
402 }
403
404 state.update_window(descriptor);
405 drop(state);
406 self.inner
407 .state_store
408 .remember_window(persisted_descriptor)?;
409 Ok(true)
410 }
411
412 pub fn insert(&self, descriptor: WindowDescriptor) -> Result<(), String> {
413 self.upsert_live_window(descriptor).map(|_| ())
414 }
415
416 pub fn remove(&self, label: &str) -> Result<Option<WindowDescriptor>, String> {
417 let mut state = self.inner.state.lock().map_err(|_| {
418 window_system_error(
419 WindowSystemErrorKind::WindowRegistryLockPoisoned,
420 "window registry lock poisoned",
421 )
422 })?;
423 let removed = state.remove_window(label);
424 drop(state);
425 if removed.is_some() {
426 self.inner.state_store.forget_window(label)?;
427 }
428 Ok(removed)
429 }
430
431 pub fn get(&self, label: &str) -> Result<Option<WindowDescriptor>, String> {
432 let state = self.inner.state.lock().map_err(|_| {
433 window_system_error(
434 WindowSystemErrorKind::WindowRegistryLockPoisoned,
435 "window registry lock poisoned",
436 )
437 })?;
438 Ok(state.windows.get(label).cloned())
439 }
440
441 pub fn list(&self) -> Result<Vec<WindowDescriptor>, String> {
442 let state = self.inner.state.lock().map_err(|_| {
443 window_system_error(
444 WindowSystemErrorKind::WindowRegistryLockPoisoned,
445 "window registry lock poisoned",
446 )
447 })?;
448 let mut list: Vec<_> = state.windows.values().cloned().collect();
449 list.sort_by(|left, right| left.label.cmp(&right.label));
450 Ok(list)
451 }
452
453 pub fn child_labels_of(&self, parent: &str) -> Result<Vec<String>, String> {
455 let state = self.inner.state.lock().map_err(|_| {
456 window_system_error(
457 WindowSystemErrorKind::WindowRegistryLockPoisoned,
458 "window registry lock poisoned",
459 )
460 })?;
461 let mut labels: Vec<_> = state
462 .children
463 .get(parent)
464 .into_iter()
465 .flat_map(|children| children.iter().cloned())
466 .collect();
467 labels.sort();
468 Ok(labels)
469 }
470
471 pub fn children_of(&self, parent: &str) -> Result<Vec<WindowDescriptor>, String> {
473 let labels = self.child_labels_of(parent)?;
474 let mut list = Vec::with_capacity(labels.len());
475
476 for label in labels {
477 if let Some(descriptor) = self.get(&label)? {
478 list.push(descriptor);
479 }
480 }
481
482 list.sort_by(|left, right| left.label.cmp(&right.label));
483 Ok(list)
484 }
485
486 pub fn restore_geometry(&self, label: &str) -> Option<WindowGeometry> {
487 self.inner.state_store.restore(label)
488 }
489
490 pub fn flush(&self) -> Result<(), String> {
491 self.inner.state_store.flush()
492 }
493
494 pub fn restore_tracked_windows(&self) -> Vec<WindowDescriptor> {
495 self.inner.state_store.restore_tracked_windows()
496 }
497
498 pub fn purge_persisted_window(&self, label: &str) -> Result<bool, String> {
503 self.inner.state_store.purge_window(label)
504 }
505
506 pub fn remember_geometry(&self, label: &str, geometry: WindowGeometry) -> Result<bool, String> {
507 let persisted_descriptor = {
508 let mut state = self.inner.state.lock().map_err(|_| {
509 window_system_error(
510 WindowSystemErrorKind::WindowRegistryLockPoisoned,
511 "window registry lock poisoned",
512 )
513 })?;
514
515 let Some(descriptor) = state.windows.get_mut(label) else {
516 return Ok(false);
517 };
518
519 if descriptor.geometry.as_ref() == Some(&geometry) {
520 return Ok(false);
521 }
522
523 descriptor.geometry = Some(geometry.clone());
524 descriptor.clone()
525 };
526
527 self.inner.state_store.remember(label, geometry)?;
528 self.inner
529 .state_store
530 .remember_window(persisted_descriptor)?;
531 Ok(true)
532 }
533
534 pub fn begin_closing(&self, label: &str) -> Result<Option<ClosingGuard>, String> {
535 let mut state = self.inner.state.lock().map_err(|_| {
536 window_system_error(
537 WindowSystemErrorKind::WindowRegistryLockPoisoned,
538 "window registry lock poisoned",
539 )
540 })?;
541
542 if state.closing.contains(label) {
543 return Ok(None);
544 }
545
546 state.closing.insert(label.to_string());
547 Ok(Some(ClosingGuard {
548 registry: self.clone(),
549 label: label.to_string(),
550 }))
551 }
552
553 fn release_reservation(&self, label: &str) {
554 if let Ok(mut state) = self.inner.state.lock() {
555 state.reserved.remove(label);
556 }
557 }
558
559 fn finish_closing(&self, label: &str) {
560 if let Ok(mut state) = self.inner.state.lock() {
561 state.closing.remove(label);
562 }
563 }
564}
565
566impl WindowRegistryState {
567 fn update_window(&mut self, descriptor: WindowDescriptor) {
568 let label = descriptor.label.clone();
569 let parent = descriptor.parent.clone();
570
571 if let Some(previous) = self.windows.insert(label.clone(), descriptor) {
572 self.detach_child(&previous.label, previous.parent.as_deref());
573 }
574
575 self.attach_child(&label, parent.as_deref());
576 }
577
578 fn remove_window(&mut self, label: &str) -> Option<WindowDescriptor> {
579 let descriptor = self.windows.remove(label)?;
580 self.detach_child(&descriptor.label, descriptor.parent.as_deref());
581 Some(descriptor)
582 }
583
584 fn attach_child(&mut self, label: &str, parent: Option<&str>) {
585 let Some(parent) = parent else {
586 return;
587 };
588
589 self.children
590 .entry(parent.to_string())
591 .or_default()
592 .insert(label.to_string());
593 }
594
595 fn detach_child(&mut self, label: &str, parent: Option<&str>) {
596 let Some(parent) = parent else {
597 return;
598 };
599
600 if let Some(children) = self.children.get_mut(parent) {
601 children.remove(label);
602 if children.is_empty() {
603 self.children.remove(parent);
604 }
605 }
606 }
607}
608
609fn load_snapshot(path: &PathBuf) -> Result<WindowStateSnapshot, String> {
610 if !path.exists() {
611 return Ok(WindowStateSnapshot::default());
612 }
613
614 let raw = fs::read_to_string(path).map_err(|err| err.to_string())?;
615 serde_json::from_str(&raw).map_err(|err| err.to_string())
616}
617
618fn persist_snapshot(path: &PathBuf, snapshot: &WindowStateSnapshot) -> Result<(), String> {
619 if let Some(parent) = path.parent() {
620 fs::create_dir_all(parent).map_err(|err| err.to_string())?;
621 }
622
623 let raw = serde_json::to_string_pretty(snapshot).map_err(|err| err.to_string())?;
624 fs::write(path, raw).map_err(|err| err.to_string())
625}
626
627fn spawn_geometry_flusher(inner: Weak<WindowStateStoreInner>, flush_receiver: mpsc::Receiver<()>) {
628 thread::spawn(move || {
629 while flush_receiver.recv().is_ok() {
630 loop {
631 match flush_receiver.recv_timeout(GEOMETRY_FLUSH_DEBOUNCE) {
632 Ok(_) => continue,
633 Err(mpsc::RecvTimeoutError::Timeout) => {
634 if let Some(inner) = inner.upgrade() {
635 if let Err(err) = inner.flush_snapshot() {
636 eprintln!(
637 "window-system: failed to persist window geometry snapshot: {err}"
638 );
639 }
640 } else {
641 break;
642 }
643 break;
644 }
645 Err(mpsc::RecvTimeoutError::Disconnected) => {
646 if let Some(inner) = inner.upgrade() {
647 if let Err(err) = inner.flush_snapshot() {
648 eprintln!(
649 "window-system: failed to persist window geometry snapshot: {err}"
650 );
651 }
652 }
653 return;
654 }
655 }
656 }
657 }
658 });
659}
660
661#[cfg(test)]
662mod tests {
663 use super::*;
664 use std::time::{SystemTime, UNIX_EPOCH};
665
666 fn unique_temp_path(name: &str) -> PathBuf {
667 let suffix = SystemTime::now()
668 .duration_since(UNIX_EPOCH)
669 .expect("system clock should be monotonic for tests")
670 .as_nanos();
671 std::env::temp_dir().join(format!("tauri-window-system-{name}-{suffix}.json"))
672 }
673
674 #[test]
675 fn remember_and_restore_geometry() {
676 let store = WindowStateStore::new(unique_temp_path("geometry"));
677 let geometry = WindowGeometry {
678 x: 10.0,
679 y: 20.0,
680 width: 300.0,
681 height: 200.0,
682 };
683
684 store
685 .remember("main", geometry.clone())
686 .expect("store should persist geometry");
687
688 assert_eq!(store.restore("main"), Some(geometry));
689 }
690
691 #[test]
692 fn remember_and_restore_tracked_window() {
693 let path = unique_temp_path("tracked-window");
694 let store = WindowStateStore::new(path.clone());
695 let descriptor = WindowDescriptor {
696 label: "main".into(),
697 url: "index.html".into(),
698 parent: None,
699 title: Some("Main".into()),
700 geometry: Some(WindowGeometry {
701 x: 10.0,
702 y: 20.0,
703 width: 300.0,
704 height: 200.0,
705 }),
706 };
707
708 store
709 .remember_window(descriptor.clone())
710 .expect("tracked window should persist");
711 store.flush().expect("snapshot should flush");
712
713 let restored = WindowStateStore::new(path).restore_tracked_windows();
714 assert_eq!(restored, vec![descriptor]);
715 }
716
717 #[test]
718 fn remember_geometry_reports_changes() {
719 let registry =
720 WindowRegistry::new(WindowStateStore::new(unique_temp_path("geometry-change")));
721 let geometry = WindowGeometry {
722 x: 10.0,
723 y: 20.0,
724 width: 300.0,
725 height: 200.0,
726 };
727
728 registry
729 .insert(WindowDescriptor {
730 label: "main".into(),
731 url: "index.html".into(),
732 parent: None,
733 title: None,
734 geometry: None,
735 })
736 .expect("main should insert");
737
738 assert!(registry
739 .remember_geometry("main", geometry.clone())
740 .expect("geometry should be recorded"));
741 assert!(!registry
742 .remember_geometry("main", geometry)
743 .expect("duplicate geometry should be ignored"));
744 }
745
746 #[test]
747 fn error_messages_are_prefixed_with_codes() {
748 let message = window_system_error(
749 WindowSystemErrorKind::WindowAlreadyExists,
750 "window already exists",
751 );
752
753 assert!(message.starts_with("window-already-exists: "));
754 }
755
756 #[test]
757 fn error_kind_codes_are_stable() {
758 let cases = [
759 (WindowSystemErrorKind::InvalidLabel, "invalid-label"),
760 (
761 WindowSystemErrorKind::WindowAlreadyExists,
762 "window-already-exists",
763 ),
764 (
765 WindowSystemErrorKind::WindowCannotBeItsOwnParent,
766 "window-cannot-be-its-own-parent",
767 ),
768 (
769 WindowSystemErrorKind::ParentWindowNotFound,
770 "parent-window-not-found",
771 ),
772 (WindowSystemErrorKind::WindowNotFound, "window-not-found"),
773 (
774 WindowSystemErrorKind::WindowReservationNotFound,
775 "window-reservation-not-found",
776 ),
777 (
778 WindowSystemErrorKind::WindowRegistryLockPoisoned,
779 "window-registry-lock-poisoned",
780 ),
781 (
782 WindowSystemErrorKind::WindowStateStoreLockPoisoned,
783 "window-state-store-lock-poisoned",
784 ),
785 ];
786
787 for (kind, code) in cases {
788 assert_eq!(kind.as_code(), code);
789 }
790 }
791
792 #[test]
793 fn remember_coalesces_to_latest_persisted_geometry() {
794 let path = unique_temp_path("debounce");
795 let store = WindowStateStore::new(path.clone());
796 let first = WindowGeometry {
797 x: 10.0,
798 y: 20.0,
799 width: 300.0,
800 height: 200.0,
801 };
802 let latest = WindowGeometry {
803 x: 40.0,
804 y: 50.0,
805 width: 640.0,
806 height: 480.0,
807 };
808
809 store
810 .remember("main", first)
811 .expect("first geometry should queue");
812 store
813 .remember("main", latest.clone())
814 .expect("latest geometry should queue");
815
816 for _ in 0..20 {
817 if path.exists() {
818 break;
819 }
820 std::thread::sleep(std::time::Duration::from_millis(25));
821 }
822
823 let raw = std::fs::read_to_string(path).expect("debounced snapshot should be written");
824 let snapshot: WindowStateSnapshot =
825 serde_json::from_str(&raw).expect("snapshot should deserialize");
826
827 assert_eq!(snapshot.windows.get("main"), Some(&latest));
828 }
829
830 #[test]
831 fn flush_writes_snapshot_immediately() {
832 let path = unique_temp_path("flush");
833 let store = WindowStateStore::new(path.clone());
834 let geometry = WindowGeometry {
835 x: 11.0,
836 y: 22.0,
837 width: 33.0,
838 height: 44.0,
839 };
840
841 store
842 .remember("main", geometry.clone())
843 .expect("geometry should queue");
844 store.flush().expect("flush should write immediately");
845
846 let raw = std::fs::read_to_string(path).expect("snapshot should exist");
847 let snapshot: WindowStateSnapshot =
848 serde_json::from_str(&raw).expect("snapshot should deserialize");
849
850 assert_eq!(snapshot.windows.get("main"), Some(&geometry));
851 }
852
853 #[test]
854 fn remove_does_not_clear_geometry() {
855 let registry = WindowRegistry::new(WindowStateStore::new(unique_temp_path("persist")));
856 let geometry = WindowGeometry {
857 x: 1.0,
858 y: 2.0,
859 width: 3.0,
860 height: 4.0,
861 };
862
863 registry
864 .insert(WindowDescriptor {
865 label: "main".into(),
866 url: "index.html".into(),
867 parent: None,
868 title: None,
869 geometry: None,
870 })
871 .expect("main should insert");
872 registry
873 .remember_geometry("main", geometry.clone())
874 .expect("geometry should persist");
875
876 registry.remove("main").expect("remove should work");
877
878 assert_eq!(registry.restore_geometry("main"), Some(geometry));
879 }
880
881 #[test]
882 fn registry_lists_and_filters_children() {
883 let registry = WindowRegistry::new(WindowStateStore::new(unique_temp_path("registry")));
884 registry
885 .insert(WindowDescriptor {
886 label: "main".into(),
887 url: "index.html".into(),
888 parent: None,
889 title: None,
890 geometry: None,
891 })
892 .expect("main should insert");
893 registry
894 .insert(WindowDescriptor {
895 label: "child".into(),
896 url: "child.html".into(),
897 parent: Some("main".into()),
898 title: None,
899 geometry: None,
900 })
901 .expect("child should insert");
902
903 assert_eq!(registry.list().unwrap().len(), 2);
904 assert_eq!(registry.children_of("main").unwrap().len(), 1);
905 assert_eq!(
906 registry.child_labels_of("main").unwrap(),
907 vec!["child".to_string()]
908 );
909 }
910
911 #[test]
912 fn update_reindexes_parent_relationships() {
913 let registry = WindowRegistry::new(WindowStateStore::new(unique_temp_path("reindex")));
914 registry
915 .insert(WindowDescriptor {
916 label: "main-a".into(),
917 url: "main-a.html".into(),
918 parent: None,
919 title: None,
920 geometry: None,
921 })
922 .expect("main-a should insert");
923 registry
924 .insert(WindowDescriptor {
925 label: "main-b".into(),
926 url: "main-b.html".into(),
927 parent: None,
928 title: None,
929 geometry: None,
930 })
931 .expect("main-b should insert");
932 registry
933 .insert(WindowDescriptor {
934 label: "child".into(),
935 url: "child.html".into(),
936 parent: Some("main-a".into()),
937 title: None,
938 geometry: None,
939 })
940 .expect("child should insert");
941
942 registry
943 .insert(WindowDescriptor {
944 label: "child".into(),
945 url: "child.html".into(),
946 parent: Some("main-b".into()),
947 title: None,
948 geometry: None,
949 })
950 .expect("child should reinsert with new parent");
951
952 assert!(registry.child_labels_of("main-a").unwrap().is_empty());
953 assert_eq!(
954 registry.child_labels_of("main-b").unwrap(),
955 vec!["child".to_string()]
956 );
957 }
958
959 #[test]
960 fn upsert_live_window_is_idempotent_for_identical_descriptors() {
961 let registry = WindowRegistry::new(WindowStateStore::new(unique_temp_path("upsert")));
962 let descriptor = WindowDescriptor {
963 label: "main".into(),
964 url: "index.html".into(),
965 parent: None,
966 title: Some("Main".into()),
967 geometry: None,
968 };
969
970 assert!(registry
971 .upsert_live_window(descriptor.clone())
972 .expect("first upsert should insert"));
973 assert!(!registry
974 .upsert_live_window(descriptor)
975 .expect("identical upsert should be ignored"));
976 }
977
978 #[test]
979 fn remove_updates_child_index() {
980 let registry = WindowRegistry::new(WindowStateStore::new(unique_temp_path("children")));
981 registry
982 .insert(WindowDescriptor {
983 label: "main".into(),
984 url: "index.html".into(),
985 parent: None,
986 title: None,
987 geometry: None,
988 })
989 .expect("main should insert");
990 registry
991 .insert(WindowDescriptor {
992 label: "child".into(),
993 url: "child.html".into(),
994 parent: Some("main".into()),
995 title: None,
996 geometry: None,
997 })
998 .expect("child should insert");
999
1000 registry.remove("child").expect("child should remove");
1001
1002 assert!(registry.child_labels_of("main").unwrap().is_empty());
1003 assert!(registry.children_of("main").unwrap().is_empty());
1004 }
1005
1006 #[test]
1007 fn purge_window_removes_tracked_window_and_geometry() {
1008 let path = unique_temp_path("purge");
1009 let store = WindowStateStore::new(path.clone());
1010 let geometry = WindowGeometry {
1011 x: 1.0,
1012 y: 2.0,
1013 width: 3.0,
1014 height: 4.0,
1015 };
1016 let descriptor = WindowDescriptor {
1017 label: "child".into(),
1018 url: "child.html".into(),
1019 parent: Some("main".into()),
1020 title: Some("Child".into()),
1021 geometry: Some(geometry.clone()),
1022 };
1023
1024 store
1025 .remember("child", geometry.clone())
1026 .expect("geometry should persist");
1027 store
1028 .remember_window(descriptor)
1029 .expect("tracked window should persist");
1030 store.flush().expect("snapshot should flush");
1031
1032 assert!(store.purge_window("child").expect("purge should work"));
1033 store.flush().expect("purged snapshot should flush");
1034
1035 let restored_store = WindowStateStore::new(path);
1036 assert!(restored_store.restore_tracked_windows().is_empty());
1037 assert_eq!(restored_store.restore("child"), None);
1038 }
1039
1040 #[test]
1041 fn reservation_rejects_duplicate_labels() {
1042 let registry = WindowRegistry::new(WindowStateStore::new(unique_temp_path("reserve")));
1043 let _reservation = registry
1044 .reserve_window("main", None)
1045 .expect("first reserve");
1046
1047 assert!(registry.reserve_window("main", None).is_err());
1048 }
1049
1050 #[test]
1051 fn reservation_rejects_missing_parent() {
1052 let registry =
1053 WindowRegistry::new(WindowStateStore::new(unique_temp_path("parent-missing")));
1054
1055 assert!(registry.reserve_window("child", Some("main")).is_err());
1056 }
1057
1058 #[test]
1059 fn reservation_rejects_self_parent() {
1060 let registry = WindowRegistry::new(WindowStateStore::new(unique_temp_path("self-parent")));
1061
1062 assert!(registry.reserve_window("main", Some("main")).is_err());
1063 }
1064
1065 #[test]
1066 fn reservation_rejects_closing_parent() {
1067 let registry =
1068 WindowRegistry::new(WindowStateStore::new(unique_temp_path("closing-parent")));
1069 registry
1070 .insert(WindowDescriptor {
1071 label: "main".into(),
1072 url: "index.html".into(),
1073 parent: None,
1074 title: None,
1075 geometry: None,
1076 })
1077 .expect("main should insert");
1078 let guard = registry
1079 .begin_closing("main")
1080 .expect("lock should succeed")
1081 .expect("closing should be marked");
1082
1083 assert!(registry.reserve_window("child", Some("main")).is_err());
1084 drop(guard);
1085 }
1086
1087 #[test]
1088 fn closing_guard_prevents_duplicate_begin() {
1089 let registry = WindowRegistry::new(WindowStateStore::new(unique_temp_path("closing")));
1090 let guard = registry
1091 .begin_closing("main")
1092 .expect("lock should succeed")
1093 .expect("first closing mark");
1094
1095 assert!(registry
1096 .begin_closing("main")
1097 .expect("lock should succeed")
1098 .is_none());
1099
1100 drop(guard);
1101 assert!(registry
1102 .begin_closing("main")
1103 .expect("lock should succeed")
1104 .is_some());
1105 }
1106}