Files
ElyOffice/crates/ely-ui/src/settings/frame.rs
T
ZacharyZhang-NY b7fca0146d Ely Office: six offline-first desktop apps in Rust and GPUI
ElyDoc, ElyTable, ElyShow (LibreOfficeKit), ElyPDF (MuPDF), ElyTree (Git),
ElyMark (Milkdown and CodeMirror in CEF). ElyMail and ElyAgent follow.
2026-10-08 21:20:08 -04:00

145 lines
5.2 KiB
Rust

//! Each app's main window opens where it was left (§18.1).
use std::time::Duration;
use ely_platform::{AppId, settings::Store};
use gpui::{App, AppContext as _, Bounds, Context, Entity, Global, Pixels, Window, WindowBounds, point, px, size};
use serde_json::{Value, json};
const KEY: &str = "window";
const SETTLE: Duration = Duration::from_millis(500);
/// Where the window was last, if that place still shows.
pub fn restore(app: AppId, cx: &mut App) -> Option<WindowBounds> {
let unreadable = |why: String, cx: &mut App| {
log::error!("settings: the window place is unreadable: {why}");
cx.global_mut::<super::Current>().app_error.insert(super::WINDOW, why.into());
None
};
let saved = match Store::app(app).and_then(|store| store.read()) {
Ok(fields) => fields.get(KEY).cloned()?,
Err(error) => return unreadable(error.to_string(), cx),
};
let Some(bounds) = parse(&saved) else {
return unreadable(format!("the window place cannot be {saved}"), cx);
};
let center = bounds.get_bounds().center();
// A display since unplugged would hide the window.
cx.displays().iter().any(|display| display.bounds().contains(&center)).then_some(bounds)
}
fn parse(saved: &Value) -> Option<WindowBounds> {
let number = |key: &str| saved.get(key)?.as_f64().map(|n| px(n as f32));
let bounds = Bounds::new(point(number("x")?, number("y")?), size(number("width")?, number("height")?));
match saved.get("mode")?.as_str()? {
"windowed" => Some(WindowBounds::Windowed(bounds)),
"maximized" => Some(WindowBounds::Maximized(bounds)),
"fullscreen" => Some(WindowBounds::Fullscreen(bounds)),
_ => None,
}
}
fn written(bounds: WindowBounds) -> Value {
let (mode, frame): (&str, Bounds<Pixels>) = match bounds {
WindowBounds::Windowed(frame) => ("windowed", frame),
WindowBounds::Maximized(frame) => ("maximized", frame),
WindowBounds::Fullscreen(frame) => ("fullscreen", frame),
};
let n = |pixels: Pixels| f64::from(f32::from(pixels));
json!({
"mode": mode,
"x": n(frame.origin.x),
"y": n(frame.origin.y),
"width": n(frame.size.width),
"height": n(frame.size.height),
})
}
struct Frame {
app: AppId,
moves: u64,
saved: u64,
last: Option<WindowBounds>,
}
impl Frame {
/// Records a write; failures show and retry later.
fn settled(&mut self, moves: u64, written: std::io::Result<()>, cx: &mut App) {
match written {
Ok(()) => {
self.saved = moves;
super::unsaved(super::WINDOW, None, cx);
}
Err(error) => {
log::error!("settings: the window place is not saved: {error}");
super::unsaved(super::WINDOW, Some(error.to_string().into()), cx);
}
}
}
/// Writes a pending place now; quitting cannot wait.
fn flush(&mut self) {
let Some(bounds) = self.last.filter(|_| self.moves != self.saved) else {
return;
};
match Store::app(self.app).and_then(|store| store.set(KEY, written(bounds))) {
Ok(()) => self.saved = self.moves,
Err(error) => log::error!("settings: the window place is not saved at quit: {error}"),
}
}
}
/// The followed windows stay alive with the app.
struct Frames(Vec<Entity<Frame>>);
impl Global for Frames {}
/// Saves `window`'s place shortly after it stops changing.
pub fn follow(app: AppId, window: &mut Window, cx: &mut App) {
let frame = cx.new(|cx: &mut Context<Frame>| {
// Quitting right after a move still keeps the place.
cx.on_app_quit(|frame, _| {
frame.flush();
async {}
})
.detach();
cx.observe_window_bounds(window, |frame, window, cx| {
frame.moves += 1;
frame.last = Some(window.window_bounds());
let (moves, app, bounds) = (frame.moves, frame.app, window.window_bounds());
cx.spawn(async move |frame, cx| {
cx.background_executor().timer(SETTLE).await;
if frame.read_with(cx, |frame, _| frame.moves != moves).unwrap_or(true) {
return;
}
let saved = cx
.background_executor()
.spawn(async move { Store::app(app)?.set(KEY, written(bounds)) })
.await;
frame.update(cx, |frame, cx| frame.settled(moves, saved, cx)).ok();
})
.detach();
})
.detach();
Frame { app, moves: 0, saved: 0, last: None }
});
if !cx.has_global::<Frames>() {
cx.set_global(Frames(Vec::new()));
}
cx.global_mut::<Frames>().0.push(frame);
}
#[cfg(test)]
mod tests {
use gpui::{Bounds, WindowBounds, point, px, size};
#[test]
fn a_place_reads_back_as_written() {
let frame = Bounds::new(point(px(40.0), px(60.0)), size(px(900.0), px(600.0)));
for bounds in [WindowBounds::Windowed(frame), WindowBounds::Maximized(frame), WindowBounds::Fullscreen(frame)] {
assert_eq!(super::parse(&super::written(bounds)), Some(bounds));
}
assert_eq!(super::parse(&serde_json::json!({"mode": "tiled", "x": 0})), None);
}
}