Skip to main content

ui/
winstate.rs

1// winstate -- the placement of the windows kept between runs (AF_SAVE_STATE)
2//
3//   state_save(&mut ini)    the main window (position, size, maximized) into [window], the
4//                           tool windows (open, placement) into [window ID]
5//   state_load(&ini)        read them back: the main window at once (window_place(0)), a
6//                           tool window when window_begin meets its id next (its open flag
7//                           and WinRect written into the app's Shared values)
8//
9// AF_SAVE_STATE in the AppDesc: the runtime reads config://windows.ini before it creates the
10// main window (at the saved size and position, maximized when it was; --size wins) and
11// writes it at the end of a window run, before UIC_EXIT.  jfgui_STATE=PATH uses another
12// file, jfgui_STATE=0 none.  Headless runs (--shot, --script, agents) never touch the file;
13// state_save / state_load work there as well (the main window: its size only).
14//
15// [window]: x / y (screen px of the client area's top left; Wayland places windows itself
16// and ignores them), w / h (DIP, the size before maximizing or minimizing), max (0 / 1).  A
17// position off every monitor (a monitor that is gone) is centred on the work area of the
18// nearest one.  [window ID] (decimal id): open (0 / 1) and x / y / w / h (DIP, relative to
19// the owner's client area: the WinRect).  Only tool windows (WF_TOOL, not WF_DIALOG /
20// WF_MODAL) are kept; sections of windows that did not run this time stay in the file.
21
22use alloc::format;
23use alloc::string::String;
24use alloc::vec::Vec;
25
26use base::Ini;
27
28use super::consts::*;
29use super::layout::px2dip_at;
30use super::Ui;
31
32const SEC_MAIN: &str = "window";
33const SEC_PREFIX: &str = "window ";
34const SIZE_MAX_DIP: i32 = 8192;
35const POS_MAX: i32 = 1 << 20;
36const VISIBLE_MIN: i32 = 48; // px of the window that must stay on a monitor
37
38/// the main window's saved placement
39#[derive(Clone, Copy, Default, Debug, PartialEq, Eq)]
40pub(crate) struct MainState {
41    pub(crate) x: i32,
42    pub(crate) y: i32,
43    pub(crate) pos: bool,
44    pub(crate) w: i32,
45    pub(crate) h: i32,
46    pub(crate) max: bool,
47}
48
49/// a tool window's saved state not applied yet
50#[derive(Clone, Copy, Debug)]
51pub(crate) struct Saved {
52    id: u32,
53    open: i32, // -1: not saved
54    rect: Option<WinRect>,
55    open_done: bool, // the open flag went to the app (window_begin met the id)
56}
57
58/// window kinds whose state is kept: tool windows that are no dialogs
59fn keepable(flags: u32) -> bool {
60    flags & WF_TOOL != 0 && flags & (WF_DIALOG | WF_MODAL) == 0
61}
62
63fn int(ini: &Ini, sec: &str, key: &str) -> Option<i64> {
64    let v = ini.get_int(sec, key, i64::MIN);
65    (v != i64::MIN).then_some(v)
66}
67
68/// the main window's entry of a state file
69pub(crate) fn main_saved(ini: &Ini) -> Option<MainState> {
70    let w = int(ini, SEC_MAIN, "w")?;
71    let h = int(ini, SEC_MAIN, "h")?;
72    if !(1..=SIZE_MAX_DIP as i64).contains(&w) || !(1..=SIZE_MAX_DIP as i64).contains(&h) {
73        return None;
74    }
75    let (x, y) = (int(ini, SEC_MAIN, "x"), int(ini, SEC_MAIN, "y"));
76    let pos = matches!((x, y), (Some(x), Some(y)) if x.abs() < POS_MAX as i64 && y.abs() < POS_MAX as i64);
77    Some(MainState {
78        x: if pos { x.unwrap_or(0) as i32 } else { 0 },
79        y: if pos { y.unwrap_or(0) as i32 } else { 0 },
80        pos,
81        w: w as i32,
82        h: h as i32,
83        max: int(ini, SEC_MAIN, "max").unwrap_or(0) != 0,
84    })
85}
86
87/// the state file of a window run: jfgui_STATE (a path, 0 none, 1 the default) or, with
88/// AF_SAVE_STATE, config://windows.ini
89pub(crate) fn state_file(flags: u32) -> Option<String> {
90    match plat::env("jfgui_STATE") {
91        Some(v) if v == "0" => None,
92        Some(v) if !v.is_empty() && v != "1" => Some(v),
93        Some(_) => Some(String::from("config://windows.ini")),
94        None => (flags & AF_SAVE_STATE != 0).then(|| String::from("config://windows.ini")),
95    }
96}
97
98/// a client rectangle (px) with less than VISIBLE_MIN px of its top edge on a work area (or
99/// its top above it: the title bar out of reach): centred on the work area instead
100pub(crate) fn fit_area(x: i32, y: i32, w: i32, h: i32, area: [i32; 4]) -> (i32, i32) {
101    let [ax, ay, aw, ah] = area;
102    if aw <= 0 || ah <= 0 {
103        return (x, y);
104    }
105    let seen_x = (x + w).min(ax + aw) - x.max(ax);
106    if seen_x >= VISIBLE_MIN.min(w) && y >= ay && y <= ay + ah - VISIBLE_MIN {
107        return (x, y);
108    }
109    (ax + ((aw - w) / 2).max(0), ay + ((ah - h) / 2).max(VISIBLE_MIN.min(ah / 8)))
110}
111
112impl Ui {
113    /// write the placement of the main window and of the tool windows into ini (see the
114    /// module header)
115    pub fn state_save(&mut self, ini: &mut Ini) {
116        if let Some(m) = self.ws_main_now() {
117            if m.pos {
118                ini.set_int(SEC_MAIN, "x", m.x as i64);
119                ini.set_int(SEC_MAIN, "y", m.y as i64);
120            } else {
121                ini.remove(SEC_MAIN, "x");
122                ini.remove(SEC_MAIN, "y");
123            }
124            ini.set_int(SEC_MAIN, "w", m.w as i64);
125            ini.set_int(SEC_MAIN, "h", m.h as i64);
126            ini.set_int(SEC_MAIN, "max", m.max as i64);
127        }
128        for i in 1..WIN_MAX {
129            let r = &self.m.win.recs[i];
130            if r.id == 0 || !keepable(r.flags) || r.id == super::inspect::INSPECT_WIN {
131                continue; // (the inspector's window is no window of the app)
132            }
133            let sec = format!("{}{}", SEC_PREFIX, r.id);
134            ini.set_int(&sec, "open", (r.state & super::win::UWI_OPEN != 0) as i64);
135            if r.state & super::win::UWI_SHOWN != 0 {
136                let rc = r.rect.get();
137                ini.set_int(&sec, "x", rc.x as i64);
138                ini.set_int(&sec, "y", rc.y as i64);
139                ini.set_int(&sec, "w", rc.w as i64);
140                ini.set_int(&sec, "h", rc.h as i64);
141            }
142        }
143    }
144
145    /// read the placements of state_save back (the main window now, the tool windows when
146    /// window_begin meets them)
147    pub fn state_load(&mut self, ini: &Ini) {
148        if let Some(m) = main_saved(ini) {
149            let mut flags = 0;
150            if m.pos && !self.headless {
151                flags |= WRF_POS | WRF_ABS;
152            }
153            if m.max && !self.headless {
154                flags |= WRF_MAX;
155            }
156            self.window_place(0, &WinRect::new(m.x, m.y, m.w, m.h, flags));
157            self.m.win.norm = m;
158        }
159        self.ws_windows(ini);
160    }
161
162    /// (runtime) the tool windows of a state file, applied when window_begin meets them
163    pub(crate) fn ws_windows(&mut self, ini: &Ini) {
164        let mut saved = Vec::new();
165        for sec in ini.sections() {
166            let Some(id) = sec.strip_prefix(SEC_PREFIX).and_then(|n| n.trim().parse::<u32>().ok()) else { continue };
167            if id == 0 {
168                continue;
169            }
170            let open = int(ini, sec, "open").map_or(-1, |v| (v != 0) as i32);
171            let rect = match (int(ini, sec, "x"), int(ini, sec, "y"), int(ini, sec, "w"), int(ini, sec, "h")) {
172                (Some(x), Some(y), Some(w), Some(h)) if x.abs() < POS_MAX as i64 && y.abs() < POS_MAX as i64 => Some(WinRect::new(
173                    x as i32,
174                    y as i32,
175                    w.clamp(1, SIZE_MAX_DIP as i64) as i32,
176                    h.clamp(1, SIZE_MAX_DIP as i64) as i32,
177                    WRF_POS,
178                )),
179                _ => None,
180            };
181            saved.retain(|s: &Saved| s.id != id);
182            saved.push(Saved { id, open, rect, open_done: false });
183        }
184        self.m.win.saved = saved;
185    }
186
187    /// (window_begin, before the window is looked at) a saved open flag goes into the app's
188    /// Shared once; windows that are not kept forget their entry
189    pub(crate) fn ws_open(&mut self, id: u32, flags: u32, open: Option<&super::Shared<i32>>) {
190        let Some(k) = self.m.win.saved.iter().position(|s| s.id == id) else { return };
191        if !keepable(flags) {
192            self.m.win.saved.remove(k);
193            return;
194        }
195        let s = &mut self.m.win.saved[k];
196        if s.open_done {
197            return;
198        }
199        s.open_done = true;
200        if let (Some(o), true) = (open, s.open >= 0) {
201            o.set(s.open);
202        }
203    }
204
205    /// (window_begin, the record's WinRect set) a saved placement goes into it once
206    pub(crate) fn ws_rect(&mut self, i: u32, id: u32) {
207        let Some(k) = self.m.win.saved.iter().position(|s| s.id == id) else { return };
208        let s = self.m.win.saved.remove(k);
209        let Some(src) = s.rect else { return };
210        let r = &mut self.m.win.recs[i as usize & (WIN_MAX - 1)];
211        let rc = r.rect.get();
212        r.rect.set(WinRect { x: src.x, y: src.y, w: src.w, h: src.h, flags: (rc.flags & !(WRF_CHANGED | WRF_MAX)) | WRF_POS });
213        r.state &= !super::win::UWI_DEFPOS;
214    }
215
216    /// (window events of the main window) remember its placement while it is neither
217    /// maximized nor minimized: the size and position state_save keeps for those.  A new
218    /// placement is a candidate first: X11 reports the size of a maximize before the state,
219    /// so a candidate as large as the window that turns out maximized is dropped
220    pub(crate) fn ws_track(&mut self) {
221        if self.headless {
222            return;
223        }
224        let big = self.ws_big();
225        self.ws_commit(big);
226        if !big {
227            self.m.win.cand = self.ws_main_live();
228        }
229    }
230
231    // maximized, minimized or full screen
232    fn ws_big(&self) -> bool {
233        self.uiw_plat_state(0) & (plat::WST_MAXIMIZED | plat::WST_MINIMIZED | plat::WST_FULLSCREEN) != 0
234    }
235
236    // the candidate becomes the normal placement unless the window is big now at its size
237    fn ws_commit(&mut self, big: bool) {
238        if let Some(c) = self.m.win.cand.take() {
239            if !big || self.ws_main_live().is_none_or(|l| (l.w, l.h) != (c.w, c.h)) {
240                self.m.win.norm = c;
241            }
242        }
243    }
244
245    // the main window as it is (position when known, size in DIP; max false)
246    fn ws_main_live(&self) -> Option<MainState> {
247        let fb = self.win_fb(0);
248        let dpi = self.win_dpi(0);
249        if fb.w <= 0 || fb.h <= 0 {
250            return None;
251        }
252        // the platform's client origin (None: not known, Wayland), else the last E_WINPOS,
253        // else the last known one
254        let rc = self.m.win.recs[0].rect.get();
255        let at = if self.headless {
256            None
257        } else if let Some(r) = plat::win_get_rect(self.uiw_pw(0)) {
258            Some((r[0], r[1]))
259        } else if rc.flags & WRF_POS != 0 {
260            Some((rc.x, rc.y))
261        } else if self.m.win.norm.pos {
262            Some((self.m.win.norm.x, self.m.win.norm.y))
263        } else {
264            None
265        };
266        Some(MainState {
267            x: at.map_or(0, |a| a.0),
268            y: at.map_or(0, |a| a.1),
269            pos: at.is_some(),
270            w: px2dip_at(fb.w, dpi),
271            h: px2dip_at(fb.h, dpi),
272            max: false,
273        })
274    }
275
276    // what state_save writes for the main window: its placement when it is normal, else the
277    // last normal one with the maximized flag
278    fn ws_main_now(&mut self) -> Option<MainState> {
279        if self.headless {
280            return self.ws_main_live();
281        }
282        let big = self.ws_big();
283        self.ws_commit(big);
284        let max = self.uiw_plat_state(0) & plat::WST_MAXIMIZED != 0;
285        let mut m = if big { self.m.win.norm } else { self.ws_main_live().unwrap_or(self.m.win.norm) };
286        if m.w <= 0 || m.h <= 0 {
287            return None;
288        }
289        m.max = max;
290        Some(m)
291    }
292}