Skip to main content

rightkit_tauri_shell/
window_state.rs

1//! DPI-independent window geometry persistence and restore.
2//!
3//! Base: CodeRight's `window_state` (the most developed donor: logical-pixel storage, v0 -> v1
4//! migration, exact-dimension / intersecting / nearest-monitor selection, on-screen title bar
5//! clamp, maximised-state memory). Changes: no tokio (debounce on a thread), the state file lives
6//! under the shell's per-app directory, and every label is tracked, not only `main`.
7
8use std::collections::HashMap;
9use std::path::{Path, PathBuf};
10use std::sync::atomic::{AtomicU64, Ordering};
11use std::sync::{Arc, Mutex};
12use std::time::Duration;
13
14use serde::{Deserialize, Serialize};
15use tauri::{AppHandle, LogicalPosition, LogicalSize, Manager, Monitor, Position, Runtime, Size};
16
17const CURRENT_VERSION: u32 = 1;
18const STATE_FILE: &str = "window-state.json";
19const MIN_WIDTH: f64 = 600.0;
20const MIN_HEIGHT: f64 = 400.0;
21pub(crate) const CLAMP_MARGIN_X: f64 = 80.0;
22pub(crate) const CLAMP_MARGIN_Y: f64 = 40.0;
23const SAVE_DEBOUNCE_MS: u64 = 250;
24
25#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
26pub struct MonitorSnapshot {
27    pub x: f64,
28    pub y: f64,
29    pub width: f64,
30    pub height: f64,
31    pub scale: f64,
32}
33
34impl MonitorSnapshot {
35    pub(crate) fn from_monitor(m: &Monitor) -> Self {
36        let scale = m.scale_factor();
37        let scale = if scale.is_finite() && scale > 0.0 {
38            scale
39        } else {
40            1.0
41        };
42        Self {
43            x: f64::from(m.position().x) / scale,
44            y: f64::from(m.position().y) / scale,
45            width: f64::from(m.size().width) / scale,
46            height: f64::from(m.size().height) / scale,
47            scale,
48        }
49    }
50    fn center(self) -> (f64, f64) {
51        (self.x + self.width / 2.0, self.y + self.height / 2.0)
52    }
53}
54
55#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
56pub struct WindowState {
57    pub x: f64,
58    pub y: f64,
59    pub width: f64,
60    pub height: f64,
61    pub maximized: bool,
62    /// The window was minimised when last saved. Its `x`, `y`, `width` and `height` are the last
63    /// normal geometry, never the off-screen rectangle a minimised window reports.
64    #[serde(default)]
65    pub minimized: bool,
66    pub monitor: MonitorSnapshot,
67    #[serde(default)]
68    pub version: u32,
69}
70
71#[derive(Debug, Default, Clone, Serialize, Deserialize)]
72struct PersistedStore {
73    #[serde(flatten)]
74    states: HashMap<String, WindowState>,
75}
76
77#[derive(Debug, Clone, Copy, PartialEq)]
78pub struct LogicalRect {
79    pub x: f64,
80    pub y: f64,
81    pub width: f64,
82    pub height: f64,
83}
84
85fn default_normal(monitor: MonitorSnapshot) -> WindowState {
86    let width = 1200.0_f64.min(monitor.width);
87    let height = 800.0_f64.min(monitor.height);
88    WindowState {
89        x: monitor.x + (monitor.width - width).max(0.0) / 2.0,
90        y: monitor.y + (monitor.height - height).max(0.0) / 2.0,
91        width,
92        height,
93        maximized: false,
94        minimized: false,
95        monitor,
96        version: CURRENT_VERSION,
97    }
98}
99
100fn migrate(mut store: PersistedStore) -> PersistedStore {
101    for s in store.states.values_mut() {
102        if s.version < CURRENT_VERSION {
103            let scale = if s.monitor.scale.is_finite() && s.monitor.scale > 0.0 {
104                s.monitor.scale
105            } else {
106                1.0
107            };
108            s.x /= scale;
109            s.y /= scale;
110            s.width /= scale;
111            s.height /= scale;
112            s.version = CURRENT_VERSION;
113        }
114    }
115    store
116}
117
118fn load_store(dir: &Path) -> PersistedStore {
119    match std::fs::read_to_string(dir.join(STATE_FILE)) {
120        Ok(raw) if !raw.trim().is_empty() => {
121            serde_json::from_str(&raw).map(migrate).unwrap_or_default()
122        }
123        _ => PersistedStore::default(),
124    }
125}
126
127static STAGING_SEQ: AtomicU64 = AtomicU64::new(0);
128
129fn save_store(dir: &Path, store: &PersistedStore) -> Result<(), String> {
130    std::fs::create_dir_all(dir).map_err(|e| format!("create {}: {e}", dir.display()))?;
131    let body = serde_json::to_string_pretty(store).map_err(|e| format!("serialize: {e}"))?;
132    // Unique staging per publication: concurrent writers never share (or rename away) one
133    // fixed temp file, and the rename is the only step that touches the live file.
134    let seq = STAGING_SEQ.fetch_add(1, Ordering::Relaxed);
135    let tmp = dir.join(format!("{STATE_FILE}.tmp.{}-{seq}", std::process::id()));
136    std::fs::write(&tmp, body).map_err(|e| format!("write {}: {e}", tmp.display()))?;
137    std::fs::rename(&tmp, dir.join(STATE_FILE))
138        .inspect_err(|_| {
139            let _ = std::fs::remove_file(&tmp);
140        })
141        .map_err(|e| format!("rename state: {e}"))
142}
143
144fn distance(ax: f64, ay: f64, bx: f64, by: f64) -> f64 {
145    (ax - bx).powi(2) + (ay - by).powi(2)
146}
147fn distance_to_center(m: &MonitorSnapshot, p: (f64, f64)) -> f64 {
148    distance(m.center().0, m.center().1, p.0, p.1)
149}
150
151/// Pure geometry projection: where should a saved window land on the current monitors?
152pub fn restoreable_state(state: &WindowState, monitors: &[MonitorSnapshot]) -> LogicalRect {
153    let saved_monitor = state.monitor;
154    let w = LogicalRect {
155        x: state.x,
156        y: state.y,
157        width: state.width,
158        height: state.height,
159    };
160    let exact = monitors
161        .iter()
162        .filter(|m| {
163            (m.width - saved_monitor.width).abs() < 0.5
164                && (m.height - saved_monitor.height).abs() < 0.5
165        })
166        .min_by(|a, b| {
167            distance(a.x, a.y, saved_monitor.x, saved_monitor.y).total_cmp(&distance(
168                b.x,
169                b.y,
170                saved_monitor.x,
171                saved_monitor.y,
172            ))
173        });
174    let center = (w.x + w.width / 2.0, w.y + w.height / 2.0);
175    let intersecting = monitors
176        .iter()
177        .filter(|m| {
178            w.x < m.x + m.width
179                && w.x + w.width > m.x
180                && w.y < m.y + m.height
181                && w.y + w.height > m.y
182        })
183        .min_by(|a, b| distance_to_center(a, center).total_cmp(&distance_to_center(b, center)));
184    let target = exact.or(intersecting).copied().unwrap_or_else(|| {
185        *monitors
186            .iter()
187            .min_by(|a, b| {
188                distance_to_center(a, saved_monitor.center())
189                    .total_cmp(&distance_to_center(b, saved_monitor.center()))
190            })
191            .expect("non-empty monitors")
192    });
193    let mut x = w.x + target.x - saved_monitor.x;
194    let mut y = w.y + target.y - saved_monitor.y;
195    let width = w.width.min(target.width).max(MIN_WIDTH.min(target.width));
196    let height = w
197        .height
198        .min(target.height)
199        .max(MIN_HEIGHT.min(target.height));
200    let max_x = target.x + target.width - CLAMP_MARGIN_X;
201    let max_y = target.y + target.height - CLAMP_MARGIN_Y;
202    let min_x = target.x - (width - CLAMP_MARGIN_X).max(0.0);
203    x = if min_x <= max_x {
204        x.clamp(min_x, max_x)
205    } else {
206        target.x
207    };
208    y = if target.y <= max_y {
209        y.clamp(target.y, max_y)
210    } else {
211        target.y
212    };
213    LogicalRect {
214        x,
215        y,
216        width,
217        height,
218    }
219}
220
221/// Result of [`WindowStateStore::import_legacy`].
222#[derive(Debug, Clone, Copy, PartialEq, Eq)]
223pub enum LegacyImport {
224    /// The legacy file was read; `entries` were added (0 when every label already existed).
225    Imported { entries: usize },
226    /// An earlier call already imported this source.
227    AlreadyImported,
228    /// No legacy file exists (nothing marked; a later call may still import).
229    NoLegacyFile,
230}
231
232/// Per-app store with debounced saves.
233pub struct WindowStateStore {
234    dir: PathBuf,
235    generations: Mutex<HashMap<String, Arc<AtomicU64>>>,
236    /// Serialises every load -> modify -> publish cycle. Without it two saves (a debounced
237    /// thread and an exit save, or two labels) can each load, insert only their own entry and
238    /// publish, silently dropping the other's; an older scheduled save can also win the rename.
239    io: Mutex<()>,
240}
241
242impl WindowStateStore {
243    pub fn new(dir: PathBuf) -> Self {
244        Self {
245            dir,
246            generations: Mutex::new(HashMap::new()),
247            io: Mutex::new(()),
248        }
249    }
250    fn io_lock(&self) -> std::sync::MutexGuard<'_, ()> {
251        self.io.lock().unwrap_or_else(|e| e.into_inner())
252    }
253    pub fn dir(&self) -> &Path {
254        &self.dir
255    }
256    fn generation(&self, label: &str) -> Arc<AtomicU64> {
257        Arc::clone(
258            self.generations
259                .lock()
260                .unwrap_or_else(|e| e.into_inner())
261                .entry(label.to_owned())
262                .or_default(),
263        )
264    }
265
266    /// The state saved for `label`, if any. Reads the file; applies nothing to any window.
267    pub fn saved(&self, label: &str) -> Option<WindowState> {
268        let _io = self.io_lock();
269        load_store(&self.dir).states.remove(label)
270    }
271
272    /// Import a legacy per-app geometry store once (M1-G5; CodeRight `window_state/mod.rs:92,110-118`
273    /// shape: a flat `label -> state` JSON object, `version` 0 meaning physical pixels).
274    ///
275    /// Each legacy entry runs through the same v0 -> v1 migration the store applies on load, then is
276    /// added under its owner label (`label_map` is `(legacy, owner)` pairs; an empty map keeps every
277    /// label). An owner entry that already exists is never overwritten. Idempotent: a marker file
278    /// `window-state.imported-<source_id>` is written after the first attempt that found the legacy
279    /// file (a missing file is not marked, so a later-appearing file still imports), and later
280    /// calls return [`LegacyImport::AlreadyImported`]. The legacy file is left in place. A legacy
281    /// file that does not parse is reported as an error and not marked.
282    pub fn import_legacy(
283        &self,
284        source_id: &str,
285        legacy_file: &Path,
286        label_map: &[(&str, &str)],
287    ) -> Result<LegacyImport, String> {
288        if source_id.is_empty()
289            || !source_id
290                .chars()
291                .all(|c| c.is_ascii_alphanumeric() || matches!(c, '-' | '_'))
292        {
293            return Err("invalid import source id".into());
294        }
295        let marker = self.dir.join(format!("window-state.imported-{source_id}"));
296        if marker.exists() {
297            return Ok(LegacyImport::AlreadyImported);
298        }
299        let raw = match std::fs::read_to_string(legacy_file) {
300            Ok(raw) => raw,
301            Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
302                return Ok(LegacyImport::NoLegacyFile)
303            }
304            Err(e) => return Err(format!("read {}: {e}", legacy_file.display())),
305        };
306        // Marker check, RMW and marker publication are one cycle: a concurrent save between
307        // the load and the publish would otherwise be overwritten by the imported snapshot.
308        let _io = self.io_lock();
309        if marker.exists() {
310            return Ok(LegacyImport::AlreadyImported);
311        }
312        let legacy: PersistedStore = if raw.trim().is_empty() {
313            PersistedStore::default()
314        } else {
315            serde_json::from_str(&raw)
316                .map(migrate)
317                .map_err(|e| format!("parse {}: {e}", legacy_file.display()))?
318        };
319        let mut store = load_store(&self.dir);
320        let mut imported = 0;
321        for (label, state) in legacy.states {
322            let target = if label_map.is_empty() {
323                label
324            } else {
325                match label_map.iter().find(|(from, _)| *from == label) {
326                    Some((_, to)) => (*to).to_owned(),
327                    None => continue,
328                }
329            };
330            if store.states.contains_key(&target) {
331                continue;
332            }
333            store.states.insert(target, state);
334            imported += 1;
335        }
336        if imported > 0 {
337            save_store(&self.dir, &store)?;
338        }
339        std::fs::create_dir_all(&self.dir)
340            .map_err(|e| format!("create {}: {e}", self.dir.display()))?;
341        std::fs::write(&marker, b"1").map_err(|e| format!("write {}: {e}", marker.display()))?;
342        Ok(LegacyImport::Imported { entries: imported })
343    }
344
345    /// Record `state` for `label` without touching a window (a host that tracks its own geometry,
346    /// or a test).
347    pub fn save_state(&self, label: &str, state: WindowState) -> Result<(), String> {
348        let _io = self.io_lock();
349        let mut store = load_store(&self.dir);
350        store.states.insert(label.to_owned(), state);
351        save_store(&self.dir, &store)
352    }
353
354    /// Apply saved geometry to `label` (hidden or not). Returns whether a saved state existed.
355    /// Positions only; equal to [`WindowStateStore::restore_with`] with the default options.
356    pub fn restore<R: Runtime>(&self, app: &AppHandle<R>, label: &str) -> Result<bool, String> {
357        self.restore_with(app, label, &RestoreOptions::default())
358    }
359
360    /// [`WindowStateStore::restore`] that can also bring the window up: `options` may unminimise,
361    /// show and focus it, and re-minimise a window saved minimised. The visibility options apply
362    /// whenever the window exists, saved geometry or not (a first run still shows). Returns whether
363    /// saved geometry was applied. Nothing calls this for you outside the full `Shell` plugin.
364    pub fn restore_with<R: Runtime>(
365        &self,
366        app: &AppHandle<R>,
367        label: &str,
368        options: &RestoreOptions,
369    ) -> Result<bool, String> {
370        let Some(window) = app.get_webview_window(label) else {
371            return Ok(false);
372        };
373        // The visibility guard has the last word on unminimise, show and focus.
374        let constrained = options.constrained(crate::lifecycle::reveal_mode(
375            app,
376            crate::lifecycle::RevealSource::Restore,
377        ));
378        let options = &constrained;
379        let monitors = window
380            .available_monitors()
381            .map_err(|e| format!("available_monitors: {e}"))?;
382        let snapshots: Vec<_> = monitors.iter().map(MonitorSnapshot::from_monitor).collect();
383        let state = self.saved(label).filter(|_| !snapshots.is_empty());
384        let mut target = WindowTarget(&window);
385        match state {
386            Some(state) => {
387                let rect = restoreable_state(&state, &snapshots);
388                apply_restore(&mut target, rect, &state, options);
389                Ok(true)
390            }
391            None => {
392                apply_visibility(&mut target, options, false);
393                Ok(false)
394            }
395        }
396    }
397
398    pub fn schedule_save<R: Runtime>(self: &Arc<Self>, app: AppHandle<R>, label: String) {
399        let token = self.begin_save(&label);
400        let this = Arc::clone(self);
401        std::thread::spawn(move || {
402            std::thread::sleep(Duration::from_millis(SAVE_DEBOUNCE_MS));
403            if this.generation(&label).load(Ordering::SeqCst) == token {
404                let _ = this.save_generation(&app, &label, token);
405            }
406        });
407    }
408
409    // Token allocation and publication share one lock: a newer invocation invalidates all
410    // older snapshots, including an in-flight debounced save and immediate exit saves.
411    fn begin_save(&self, label: &str) -> u64 {
412        let _io = self.io_lock();
413        self.generation(label).fetch_add(1, Ordering::SeqCst) + 1
414    }
415
416    pub fn save_now<R: Runtime>(&self, app: &AppHandle<R>, label: &str) -> Result<(), String> {
417        self.save_generation(app, label, self.begin_save(label))
418    }
419
420    fn save_generation<R: Runtime>(
421        &self,
422        app: &AppHandle<R>,
423        label: &str,
424        token: u64,
425    ) -> Result<(), String> {
426        let Some(window) = app.get_webview_window(label) else {
427            return Ok(());
428        };
429        if window.is_fullscreen().unwrap_or(false) {
430            return Ok(());
431        }
432        let minimized = window.is_minimized().unwrap_or(false);
433        let scale = window.scale_factor().map_err(|e| format!("scale: {e}"))?;
434        let Some(monitor) = window
435            .current_monitor()
436            .map_err(|e| format!("current_monitor: {e}"))?
437        else {
438            return Ok(());
439        };
440        let monitor = MonitorSnapshot::from_monitor(&monitor);
441        let maximized = window.is_maximized().unwrap_or(false);
442        // A minimised window reports an off-screen rectangle on some platforms: never read it.
443        let geometry = if maximized || minimized {
444            None
445        } else {
446            let p = window
447                .outer_position()
448                .map_err(|e| format!("outer_position: {e}"))?;
449            let s = window
450                .inner_size()
451                .map_err(|e| format!("inner_size: {e}"))?;
452            Some((
453                f64::from(p.x) / scale,
454                f64::from(p.y) / scale,
455                f64::from(s.width) / scale,
456                f64::from(s.height) / scale,
457            ))
458        };
459        self.publish_generation(label, token, monitor, maximized, minimized, geometry)
460    }
461
462    fn publish_generation(
463        &self,
464        label: &str,
465        token: u64,
466        monitor: MonitorSnapshot,
467        maximized: bool,
468        minimized: bool,
469        geometry: Option<(f64, f64, f64, f64)>,
470    ) -> Result<(), String> {
471        let _io = self.io_lock();
472        if self.generation(label).load(Ordering::SeqCst) != token {
473            return Ok(());
474        }
475        let mut store = load_store(&self.dir);
476        let state = next_state(
477            store.states.get(label),
478            monitor,
479            maximized,
480            minimized,
481            geometry,
482        );
483        store.states.insert(label.to_owned(), state);
484        save_store(&self.dir, &store)
485    }
486}
487
488/// The state to save next. `geometry` is the live `(x, y, width, height)` in logical pixels and is
489/// only read for a window that is neither maximised nor minimised; otherwise the last normal
490/// geometry is kept (or the default rectangle when none was saved).
491pub fn next_state(
492    prev: Option<&WindowState>,
493    monitor: MonitorSnapshot,
494    maximized: bool,
495    minimized: bool,
496    geometry: Option<(f64, f64, f64, f64)>,
497) -> WindowState {
498    match (maximized || minimized, geometry) {
499        (false, Some((x, y, width, height))) => WindowState {
500            x,
501            y,
502            width,
503            height,
504            maximized: false,
505            minimized: false,
506            monitor,
507            version: CURRENT_VERSION,
508        },
509        _ => {
510            let normal = prev.cloned().unwrap_or_else(|| default_normal(monitor));
511            WindowState {
512                // A minimised window may report either; keep what was saved if it was maximised.
513                maximized: maximized || (minimized && normal.maximized),
514                minimized,
515                monitor,
516                version: CURRENT_VERSION,
517                ..normal
518            }
519        }
520    }
521}
522
523/// What [`WindowStateStore::restore_with`] does besides positioning. The default does nothing else,
524/// which is what `restore` and the full plugin have always done.
525#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
526pub struct RestoreOptions {
527    /// Unminimise the window.
528    pub unminimize: bool,
529    /// Show the window.
530    pub show: bool,
531    /// Give the window focus (raises it; leave off for a window that must not steal focus).
532    pub focus: bool,
533    /// Minimise again a window that was saved minimised. Applied last, after `show`.
534    pub restore_minimized: bool,
535}
536
537impl RestoreOptions {
538    /// These options as the visibility guard allows them: `Forbidden` strips unminimise, show,
539    /// focus and re-minimise; `Background` strips unminimise and focus (QA never raises a window).
540    pub fn constrained(self, mode: crate::lifecycle::RevealMode) -> Self {
541        use crate::lifecycle::RevealMode;
542        match mode {
543            RevealMode::Foreground => self,
544            RevealMode::Background => Self {
545                unminimize: false,
546                focus: false,
547                ..self
548            },
549            RevealMode::Forbidden => Self {
550                unminimize: false,
551                show: false,
552                focus: false,
553                restore_minimized: false,
554            },
555        }
556    }
557}
558
559/// The window calls a restore makes, so the order is testable without a window.
560pub trait RestoreTarget {
561    fn unmaximize(&mut self);
562    fn set_size(&mut self, width: f64, height: f64);
563    fn set_position(&mut self, x: f64, y: f64);
564    fn maximize(&mut self);
565    fn unminimize(&mut self);
566    fn show(&mut self);
567    fn focus(&mut self);
568    fn minimize(&mut self);
569}
570
571struct WindowTarget<'a, R: Runtime>(&'a tauri::WebviewWindow<R>);
572
573impl<R: Runtime> RestoreTarget for WindowTarget<'_, R> {
574    fn unmaximize(&mut self) {
575        let _ = self.0.unmaximize();
576    }
577    fn set_size(&mut self, width: f64, height: f64) {
578        let _ = self
579            .0
580            .set_size(Size::Logical(LogicalSize { width, height }));
581    }
582    fn set_position(&mut self, x: f64, y: f64) {
583        let _ = self
584            .0
585            .set_position(Position::Logical(LogicalPosition { x, y }));
586    }
587    fn maximize(&mut self) {
588        let _ = self.0.maximize();
589    }
590    fn unminimize(&mut self) {
591        let _ = self.0.unminimize();
592    }
593    fn show(&mut self) {
594        let _ = self.0.show();
595    }
596    fn focus(&mut self) {
597        let _ = self.0.set_focus();
598    }
599    fn minimize(&mut self) {
600        let _ = self.0.minimize();
601    }
602}
603
604/// Geometry first (size and position, then maximised), then the requested visibility.
605pub fn apply_restore<T: RestoreTarget>(
606    target: &mut T,
607    rect: LogicalRect,
608    state: &WindowState,
609    options: &RestoreOptions,
610) {
611    target.unmaximize();
612    target.set_size(rect.width, rect.height);
613    target.set_position(rect.x, rect.y);
614    if state.maximized {
615        target.maximize();
616    }
617    apply_visibility(target, options, state.minimized);
618}
619
620fn apply_visibility<T: RestoreTarget>(
621    target: &mut T,
622    options: &RestoreOptions,
623    was_minimized: bool,
624) {
625    if options.unminimize {
626        target.unminimize();
627    }
628    if options.show {
629        target.show();
630    }
631    if options.focus {
632        target.focus();
633    }
634    if options.restore_minimized && was_minimized {
635        target.minimize();
636    }
637}
638
639#[cfg(test)]
640mod publication_order_tests {
641    use super::*;
642
643    #[test]
644    fn newer_immediate_save_invalidates_paused_same_label_snapshot() {
645        let dir = tempfile::tempdir().unwrap();
646        let store = Arc::new(WindowStateStore::new(dir.path().to_owned()));
647        let (captured_tx, captured_rx) = std::sync::mpsc::channel();
648        let (resume_tx, resume_rx) = std::sync::mpsc::channel();
649        let old_store = Arc::clone(&store);
650        let old = std::thread::spawn(move || {
651            let token = old_store.begin_save("main");
652            let monitor = MonitorSnapshot {
653                x: 0.0,
654                y: 0.0,
655                width: 1920.0,
656                height: 1080.0,
657                scale: 1.0,
658            };
659            captured_tx.send(()).unwrap();
660            resume_rx.recv().unwrap();
661            old_store
662                .publish_generation(
663                    "main",
664                    token,
665                    monitor,
666                    false,
667                    false,
668                    Some((1.0, 2.0, 300.0, 400.0)),
669                )
670                .unwrap();
671        });
672        captured_rx.recv().unwrap();
673        let token = store.begin_save("main");
674        store
675            .publish_generation(
676                "main",
677                token,
678                MonitorSnapshot {
679                    x: 0.0,
680                    y: 0.0,
681                    width: 1920.0,
682                    height: 1080.0,
683                    scale: 1.0,
684                },
685                false,
686                false,
687                Some((20.0, 30.0, 600.0, 700.0)),
688            )
689            .unwrap();
690        resume_tx.send(()).unwrap();
691        old.join().unwrap();
692        let state = store.saved("main").unwrap();
693        assert_eq!(
694            (state.x, state.y, state.width, state.height),
695            (20.0, 30.0, 600.0, 700.0)
696        );
697    }
698}