root/src/window.rs

use adw::prelude::*;
use adw::subclass::prelude::*;
use chrono::prelude::*;
use glib::clone;
use gtk::{gio, glib};
use std::cell::Cell;
use sun_times::sun_times;
use tracing::*;

use crate::application::ExampleApplication;
use crate::config::{APP_ID, PROFILE};
use crate::locations::*;

mod imp {
    use super::*;

    #[derive(Debug, gtk::CompositeTemplate)]
    #[template(resource = "/garden/patchouli/sunrise/ui/window.ui")]
    pub struct ExampleApplicationWindow {
        #[template_child]
        pub headerbar: TemplateChild<adw::HeaderBar>,
        #[template_child]
        pub location: TemplateChild<gtk::DropDown>,
        #[template_child]
        pub date: TemplateChild<adw::SpinRow>,
        #[template_child]
        pub sunrise: TemplateChild<gtk::Label>,
        #[template_child]
        pub sundown: TemplateChild<gtk::Label>,
        pub settings: gio::Settings,

        pub location_city: Cell<Option<&'static Location>>,
        pub now: Cell<DateTime<Local>>,
    }

    impl Default for ExampleApplicationWindow {
        fn default() -> Self {
            Self {
                headerbar: TemplateChild::default(),
                location: TemplateChild::default(),
                date: TemplateChild::default(),
                sunrise: TemplateChild::default(),
                sundown: TemplateChild::default(),
                settings: gio::Settings::new(APP_ID),
                location_city: Cell::new(None),
                now: Cell::new(chrono::Local::now()),
            }
        }
    }

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

        fn class_init(klass: &mut Self::Class) {
            klass.bind_template();
        }

        // You must call `Widget`'s `init_template()` within `instance_init()`.
        fn instance_init(obj: &glib::subclass::InitializingObject<Self>) {
            obj.init_template();
        }
    }

    impl ObjectImpl for ExampleApplicationWindow {
        fn constructed(&self) {
            self.parent_constructed();
            let obj = self.obj();

            // Devel Profile
            if PROFILE == "Devel" {
                obj.add_css_class("devel");
            }

            // Load latest window state
            obj.load_window_size();
        }
    }

    impl WidgetImpl for ExampleApplicationWindow {}
    impl WindowImpl for ExampleApplicationWindow {
        // Save window state on delete event
        fn close_request(&self) -> glib::Propagation {
            if let Err(err) = self.obj().save_window_size() {
                tracing::warn!("Failed to save window state, {}", &err);
            }

            // Pass close request on to the parent
            self.parent_close_request()
        }
    }

    impl ApplicationWindowImpl for ExampleApplicationWindow {}
    impl AdwApplicationWindowImpl for ExampleApplicationWindow {}
}

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

impl ExampleApplicationWindow {
    pub fn new(app: &ExampleApplication) -> Self {
        let this: Self = glib::Object::builder().property("application", app).build();
        this.init();
        this
    }

    fn init(&self) {
        let labels: Vec<_> = LOCATIONS.iter().map(|l| l.label).collect();
        let model = gtk::StringList::new(&labels);
        self.imp().location.set_model(Some(&model));

        match self.restore_city() {
            Ok(id) => self.imp().location.set_selected(id as u32),
            Err(_) => self.imp().location.set_selected(FALLBACK_ID),
        }

        self.set_suntimes();

        self.imp().location.connect_selected_notify(clone!(
            #[weak(rename_to = this)]
            self,
            move |dropdown| {
                let id = dropdown.selected();
                let _ = this.set_city(id);
                this.set_suntimes();
            }
        ));

        self.imp().date.connect_changed(clone!(
            #[weak(rename_to = this)]
            self,
            move |spin| {
                let value = spin.value();
                let new_date;
                if value < 0.0 {
                    new_date =
                        chrono::Local::now().checked_sub_days(chrono::Days::new(-value as u64));
                } else {
                    new_date =
                        chrono::Local::now().checked_add_days(chrono::Days::new(value as u64));
                }
                if let Some(now) = new_date {
                    this.imp().now.replace(now);
                    this.set_suntimes();
                }
            }
        ));
    }

    fn set_suntimes(&self) {
        let now = self.imp().now.get();
        let city = self.city();
        let times = sun_times(
            now.date_naive(),
            city.coords.1,
            city.coords.0,
            city.elevation as f64,
        );
        info!("{times:#?}");
        self.imp()
            .date
            .set_subtitle(format!("{}", now.format("%B %e")).as_str());
        if let Some((rise, down)) = times {
            let rise = DateTime::<Local>::from(rise).format("%H:%M");
            let down = DateTime::<Local>::from(down).format("%H:%M");
            self.imp().sunrise.set_text(format!("{rise}").as_str());
            self.imp().sundown.set_text(format!("{down}").as_str());
        }
    }

    fn city(&self) -> &'static Location {
        self.imp()
            .location_city
            .get()
            .unwrap_or_else(|| LOCATIONS.get(FALLBACK_ID as usize).unwrap())
    }

    fn set_city(&self, id: u32) -> Result<(), glib::BoolError> {
        let imp = self.imp();
        let city = LOCATIONS.get(id as usize);
        if let Some(city) = city.as_ref() {
            // TODO store key id instead of int offset
            imp.settings.set_int("city", id as i32)?;
            imp.settings.set_string("city-id", city.id)?;
        }
        imp.location_city.set(city);
        Ok(())
    }

    fn restore_city(&self) -> Result<usize, glib::BoolError> {
        let imp = self.imp();
        let id = imp.settings.int("city") as usize;
        let id_key = imp.settings.string("city-id");
        // get city form raw index
        let city = LOCATIONS.get(id);
        if let Some(city) = city {
            // check that it matches wikidata-key
            if city.id == id_key {
                info!("restored {} {}", id, city.label);
                imp.location_city.set(Some(city));
                Ok(id)
            } else {
                // get city from wikidata-key
                let city_by_key = LOCATIONS.iter().enumerate().find_map(|(i, c)| {
                    if c.id == id_key {
                        Some((i, c))
                    } else {
                        None
                    }
                });
                if let Some((i, city)) = city_by_key {
                    info!("restored by key {} {}", id_key, city.label);
                    imp.location_city.set(Some(city));
                    Ok(i)
                } else {
                    Err(glib::bool_error!("no city for id key"))
                }
            }
        } else {
            Err(glib::bool_error!("city out of bounds"))
        }
    }

    fn save_window_size(&self) -> Result<(), glib::BoolError> {
        let imp = self.imp();

        let (width, height) = self.default_size();

        imp.settings.set_int("window-width", width)?;
        imp.settings.set_int("window-height", height)?;

        imp.settings
            .set_boolean("is-maximized", self.is_maximized())?;

        Ok(())
    }

    fn load_window_size(&self) {
        let imp = self.imp();

        let width = imp.settings.int("window-width");
        let height = imp.settings.int("window-height");
        let is_maximized = imp.settings.boolean("is-maximized");

        self.set_default_size(width, height);

        if is_maximized {
            self.maximize();
        }
    }
}