git.lucas.co / cce-window-manager
window management library
git clone https://git.lucas.co/cce-window-manager.git

src/state.rs (4.5K)

  1 // Persisted session state, saved to $XDG_STATE_HOME/cce/state.json on shutdown
  2 // and restored on startup. Pure data — serialization and matching logic only;
  3 // the save/load I/O lives in `window_manager.rs`.
  4 
  5 use super::tiling::TilingMode;
  6 
  7 /// The desktop-grid geometry the geometries in this file were measured
  8 /// under. Saved so a LATER session under a different grid can re-tile a
  9 /// Tiled entry onto the same block of squares (`cells::remap_block`) instead
 10 /// of re-deriving its span from stale pixels — a box saved under one grid
 11 /// lands misaligned on another, touches extra cells, and the tiled snap
 12 /// then grows the window by a cell. Absent in files written before this
 13 /// field existed; the loader then has nothing to remap from and applies the
 14 /// geometry as-is.
 15 #[derive(serde::Serialize, serde::Deserialize, Clone, Copy, Debug, PartialEq)]
 16 pub struct SavedGrid {
 17     pub cell_w: f64,
 18     pub cell_h: f64,
 19     pub gap_width: f64,
 20     pub cell_inset: f64,
 21 }
 22 
 23 impl SavedGrid {
 24     pub fn from_params(p: &crate::snap::SnapParams) -> Self {
 25         Self { cell_w: p.cell_w, cell_h: p.cell_h, gap_width: p.gap_width, cell_inset: p.cell_inset }
 26     }
 27 
 28     /// The saved grid as snap params, borrowing everything non-geometric
 29     /// (threshold) from `current` — remapping needs geometry only.
 30     pub fn to_params(&self, current: &crate::snap::SnapParams) -> crate::snap::SnapParams {
 31         crate::snap::SnapParams {
 32             cell_w: self.cell_w,
 33             cell_h: self.cell_h,
 34             gap_width: self.gap_width,
 35             cell_inset: self.cell_inset,
 36             threshold: current.threshold,
 37         }
 38     }
 39 
 40     pub fn matches(&self, p: &crate::snap::SnapParams) -> bool {
 41         self.cell_w == p.cell_w
 42             && self.cell_h == p.cell_h
 43             && self.gap_width == p.gap_width
 44             && self.cell_inset == p.cell_inset
 45     }
 46 }
 47 
 48 #[derive(serde::Serialize, serde::Deserialize, Clone, Debug)]
 49 pub struct SavedWindowState {
 50     pub app_id: String,
 51     pub title: String,
 52     pub tiling_mode: TilingMode,
 53     pub minimized: bool,
 54     pub virtual_x: f64,
 55     pub virtual_y: f64,
 56     pub scale: f64,
 57     pub width: u32,
 58     pub height: u32,
 59     pub cmdline: String,
 60     #[serde(default)]
 61     pub focused: bool,
 62 }
 63 
 64 #[derive(serde::Serialize, serde::Deserialize, Clone, Debug)]
 65 pub struct SavedState {
 66     pub desk_pan_x: f64,
 67     pub desk_pan_y: f64,
 68     pub desk_zoom: f64,
 69     pub windows: Vec<SavedWindowState>,
 70     #[serde(default)]
 71     pub last_window_states: Vec<SavedWindowState>,
 72     /// See [`SavedGrid`]. `None` in pre-field files.
 73     #[serde(default)]
 74     pub grid: Option<SavedGrid>,
 75 }
 76 
 77 #[cfg(test)]
 78 mod tests {
 79     use super::*;
 80 
 81     /// A state file from before the grid field must still load — and one
 82     /// with it must round-trip.
 83     #[test]
 84     fn legacy_state_without_grid_still_loads() {
 85         let legacy = r#"{
 86             "desk_pan_x": 0.0, "desk_pan_y": 0.0, "desk_zoom": 1.0,
 87             "windows": [{
 88                 "app_id": "cce-terminal", "title": "t", "tiling_mode": "Tiled",
 89                 "minimized": false, "virtual_x": 4.0, "virtual_y": 4.0,
 90                 "scale": 1.0, "width": 1560, "height": 504, "cmdline": "cce-terminal"
 91             }]
 92         }"#;
 93         let s: SavedState = serde_json::from_str(legacy).unwrap();
 94         assert!(s.grid.is_none());
 95         assert_eq!(s.windows.len(), 1);
 96 
 97         let with_grid = SavedState {
 98             grid: Some(SavedGrid { cell_w: 512.0, cell_h: 512.0, gap_width: 16.0, cell_inset: 4.0 }),
 99             ..s
100         };
101         let json = serde_json::to_string(&with_grid).unwrap();
102         let back: SavedState = serde_json::from_str(&json).unwrap();
103         assert_eq!(back.grid, with_grid.grid);
104     }
105 
106     /// The saved-grid remap keeps a Tiled entry on ITS SQUARES across a gap
107     /// change: 3 columns at gap 16 stays 3 columns at gap 8, where a raw
108     /// pixel restore would misalign and span 4.
109     #[test]
110     fn saved_grid_remap_keeps_the_block() {
111         let old = SavedGrid { cell_w: 512.0, cell_h: 512.0, gap_width: 16.0, cell_inset: 4.0 };
112         let new = crate::snap::SnapParams {
113             cell_w: 512.0, cell_h: 512.0, gap_width: 8.0, cell_inset: 4.0, threshold: 24.0,
114         };
115         // Column -5, 3 cells wide, 1 tall, under the old grid.
116         let (x, y, w, h) = (-5.0 * 528.0 + 4.0, 4.0, 2.0 * 528.0 + 512.0 - 8.0, 504.0);
117         let (nx, ny, nw, nh) =
118             crate::cells::remap_block(x, y, w, h, &old.to_params(&new), &new);
119         assert_eq!((nx, ny), (-5.0 * 520.0 + 4.0, 4.0));
120         assert_eq!((nw, nh), (2.0 * 520.0 + 512.0 - 8.0, 504.0));
121     }
122 }