Saving Window State

Quite often, we want the window state to persist between sessions. If the user resizes or maximizes the window, they might expect to find it in the same state the next time they open the app. GTK does not provide this functionality out of the box, but luckily it is not too hard to manually implement it. We basically want two integers (height & width) and a boolean (is_maximized) to persist. We already know how to do this by using gio::Settings.

Filename: listings/saving_window_state/1/org.gtk_rs.SavingWindowState1.gschema.xml

<?xml version="1.0" encoding="utf-8"?>
<schemalist>
  <schema id="org.gtk_rs.SavingWindowState1" path="/org/gtk_rs/SavingWindowState1/">
    <key name="window-width" type="i">
      <default>-1</default>
      <summary>Default window width</summary>
    </key>
    <key name="window-height" type="i">
      <default>-1</default>
      <summary>Default window height</summary>
    </key>
    <key name="is-maximized" type="b">
      <default>false</default>
      <summary>Default window maximized behaviour</summary>
    </key>
  </schema>
</schemalist>

Since we don't care about intermediate state, we only load the window state when the window is constructed and save it when we close the window. That can be done by creating a custom window. First, we create one and add convenience methods for accessing settings as well as the window state.

Filename: listings/saving_window_state/1/custom_window/mod.rs

mod imp;

use gio::Settings;
use glib::Object;
use gtk::prelude::*;
use gtk::subclass::prelude::*;
use gtk::{gio, glib, Application};

use crate::APP_ID;

glib::wrapper! {
    pub struct Window(ObjectSubclass<imp::Window>)
        @extends gtk::ApplicationWindow, gtk::Window, gtk::Widget,
        @implements gio::ActionGroup, gio::ActionMap, gtk::Accessible, gtk::Buildable,
                    gtk::ConstraintTarget, gtk::Native, gtk::Root, gtk::ShortcutManager;
}

impl Window {
    pub fn new(app: &Application) -> Self {
        // Create new window
        Object::builder().property("application", app).build()
    }

    fn setup_settings(&self) {
        let settings = Settings::new(APP_ID);
        self.imp()
            .settings
            .set(settings)
            .expect("`settings` should not be set before calling `setup_settings`.");
    }

    fn settings(&self) -> &Settings {
        self.imp()
            .settings
            .get()
            .expect("`settings` should be set in `setup_settings`.")
    }

    pub fn save_window_size(&self) -> Result<(), glib::BoolError> {
        // Get the size of the window
        let size = self.default_size();

        // Set the window state in `settings`
        self.settings().set_int("window-width", size.0)?;
        self.settings().set_int("window-height", size.1)?;
        self.settings()
            .set_boolean("is-maximized", self.is_maximized())?;

        Ok(())
    }

    fn load_window_size(&self) {
        // Get the window state from `settings`
        let width = self.settings().int("window-width");
        let height = self.settings().int("window-height");
        let is_maximized = self.settings().boolean("is-maximized");

        // Set the size of the window
        self.set_default_size(width, height);

        // If the window was maximized when it was closed, maximize it again
        if is_maximized {
            self.maximize();
        }
    }
}

We set the property "application" by passing it to glib::Object::new. You can even set multiple properties that way. When creating new GObjects, this is nicer than calling the setter methods manually.

The implementation struct holds the settings. You can see that we embed Settings in std::cell::OnceCell. This is a nice alternative to RefCell<Option<T>> when you know that you will initialize the value only once.

We also override the constructed and close_request methods, where we load or save the window state.

Filename: listings/saving_window_state/1/custom_window/imp.rs

use gio::Settings;
use gtk::subclass::prelude::*;
use gtk::{gio, glib, ApplicationWindow};
use std::cell::OnceCell;

#[derive(Default)]
pub struct Window {
    pub settings: OnceCell<Settings>,
}

#[glib::object_subclass]
impl ObjectSubclass for Window {
    const NAME: &'static str = "MyGtkAppWindow";
    type Type = super::Window;
    type ParentType = ApplicationWindow;
}
impl ObjectImpl for Window {
    fn constructed(&self) {
        self.parent_constructed();
        // Load latest window state
        let obj = self.obj();
        obj.setup_settings();
        obj.load_window_size();
    }
}
impl WidgetImpl for Window {}
impl WindowImpl for Window {
    // Save window state right before the window will be closed
    fn close_request(&self) -> glib::Propagation {
        // Save window size
        self.obj()
            .save_window_size()
            .expect("Failed to save window state");
        // Allow to invoke other event handlers
        glib::Propagation::Proceed
    }
}
impl ApplicationWindowImpl for Window {}

That is it! Now our window retains its state between app sessions.