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 Settings.

Filename: listings/saving_window_state/1/org.gtk.example.gschema.xml

<?xml version="1.0" encoding="utf-8"?>
<schemalist>
  <schema id="org.gtk.example" path="/org/gtk/example/">
    <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 do not 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 methods for getting and setting the window state.

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

mod imp;

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

glib::wrapper! {
    pub struct CustomWindow(ObjectSubclass<imp::CustomWindow>)
        @extends gtk::Widget, gtk::Window, gtk::ApplicationWindow,
        @implements gio::ActionMap, gio::ActionGroup;
}

impl CustomWindow {
    pub fn new(app: &Application) -> Self {
        let window: Self = Object::new(&[]).expect("Failed to create CustomWindow");
        window.set_application(Some(app));
        window
    }

    pub fn save_window_size(&self) -> Result<(), glib::BoolError> {
        // Get `settings` from `imp::CustomWindow`
        let settings = &imp::CustomWindow::from_instance(self).settings;

        // Get the size of the window
        let size = self.default_size();

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

        Ok(())
    }

    fn load_window_size(&self) {
        // Get `settings` from `imp::CustomWindow`
        let settings = &imp::CustomWindow::from_instance(self).settings;

        // Set the window state in `settings`
        let width = settings.int("window-width");
        let height = settings.int("window-height");
        let is_maximized = 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();
        }
    }
}

// Please ignore this line
// It is only there to make mdbook happy
fn main() {}

The implementation struct holds the settings. We also overload 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 glib::signal::Inhibit;
use gtk::{gio, glib};
use gtk::{subclass::prelude::*, ApplicationWindow};

pub struct CustomWindow {
    pub settings: Settings,
}

#[glib::object_subclass]
impl ObjectSubclass for CustomWindow {
    const NAME: &'static str = "CustomWindow";
    type Type = super::CustomWindow;
    type ParentType = ApplicationWindow;

    fn new() -> Self {
        Self {
            settings: Settings::new("org.gtk.example"),
        }
    }
}
impl ObjectImpl for CustomWindow {
    fn constructed(&self, obj: &Self::Type) {
        self.parent_constructed(obj);
        // Load latest window state
        obj.load_window_size();
    }
}
impl WidgetImpl for CustomWindow {}
impl WindowImpl for CustomWindow {
    // Save window state right before the window will be closed
    fn close_request(&self, obj: &Self::Type) -> Inhibit {
        if let Err(err) = obj.save_window_size() {
            log::error!("Failed to save window state, {}", &err);
        }
        // Do not inhibit the the default handler
        Inhibit(false)
    }
}
impl ApplicationWindowImpl for CustomWindow {}

// Please ignore this line
// It is only there to make mdbook happy
fn main() {}

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

Please note how we handle a failure in saving into the settings. We do not want to panic for recoverable errors. We might also not want to present all problems at the GUI. In our case we could not even do this, because the window will be immediately closed after the error occurs. Logging is the standard way of handling a situation like this. For that, we need to add the log crate and one of its front-ends, such as pretty_env_logger, to our dependencies.

Filename: listings/Cargo.toml

[dependencies]
log = "0.4"
pretty_env_logger = "0.4"

We then have to initialize pretty_env_logger by calling init in main.

Filename: listings/saving_window_state/1/main.rs

mod custom_window;

use custom_window::CustomWindow;
use gtk::prelude::*;
use gtk::{Application, Button};

fn main() {
    // Initialize logger
    pretty_env_logger::init();

    // Create a new application
    let app = Application::new(Some("org.gtk.example"), Default::default());
    app.connect_activate(build_ui);

    // Run the application
    app.run();
}

fn build_ui(application: &Application) {
    // Create a window
    let window = CustomWindow::new(application);

    // Create a button
    let button = Button::builder()
        .label("Press me!")
        .margin_top(12)
        .margin_bottom(12)
        .margin_start(12)
        .margin_end(12)
        .build();

    // Connect callback
    button.connect_clicked(move |button| {
        // Set the label to "Hello World!" after the button has been clicked on
        button.set_label("Hello World!");
    });

    // Add button
    window.set_child(Some(&button));
    window.present();
}

We can now modify the log level by setting the RUST_LOG environment variable as can be seen here