gstore 0.8.1

Global and local state management in redux style for GTK applications written in Rust
// SPDX-License-Identifier: GPL-3.0-or-later

use gdk4::prelude::ActionExt;
use std::{cell::Cell, collections::HashMap, sync::Mutex};

use crate::print_perf;

mod action;
pub use action::*;

mod widgets;
pub use widgets::*;

/// A global Store
///
/// The store holds global app app data with static lifetime.
///
/// It can be manipulated by dispatching actions (See [Action](crate::Action)).
///
/// Each action is handled by the reducer - A function which matches the
/// action and has a mutable ref to the state in the store.
///
/// The reducer must not have any side effects (E.g. saving the state to a file).
///
/// Side effects are implemented in middlewares (See [Middleware](crate::Middleware)).
pub struct Store<S: std::fmt::Debug + Clone + Default + PartialEq + Eq + 'static> {
    state: S,
    middlewares: Vec<Box<dyn Middleware<S>>>,
    reducer: Box<dyn Fn(&Action, &mut S) + 'static>,
    selectors: HashMap<SelectorId, Selector<S>>,
    reduce_lock: Mutex<u8>,
    reduce_depth: Cell<u64>,
}

impl<S: std::fmt::Debug + Clone + Default + PartialEq + Eq + 'static> std::fmt::Debug for Store<S> {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("Store")
            .field("state", &self.state)
            .field("selectors", &self.selectors)
            .finish()
    }
}

#[derive(Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
pub struct SelectorId(u128);

impl<S: std::fmt::Debug + Clone + Default + PartialEq + Eq> Store<S> {
    /// Create a new Store.
    /// # Arguments
    /// - default_state: The initial state of the store.
    /// - reducer: The root reducer for this store.
    /// - middlewares: A list of [Middleware](crate::Middleware).
    pub fn new(
        default_state: S,
        reducer: impl Fn(&Action, &mut S) + 'static,
        middlewares: Vec<Box<dyn Middleware<S>>>,
    ) -> Self {
        Store {
            state: default_state,
            middlewares,
            reducer: Box::new(reducer),
            selectors: HashMap::default(),
            reduce_lock: Default::default(),
            reduce_depth: Cell::new(0),
        }
    }

    fn select_internal(
        &mut self,
        name: &str,
        selector: impl Fn(&S) -> S + 'static,
        callback: impl Fn(&S) + 'static,
        select_full_state: bool,
    ) -> u128 {
        callback(&self.state);
        let last_selector_state = selector(&self.state);
        trace!("Adding selector '{}'.", name);
        let id = uuid::Uuid::new_v4().as_u128();
        self.selectors.insert(
            SelectorId(id),
            Selector::new(
                name,
                select_full_state,
                selector,
                last_selector_state,
                callback,
            ),
        );
        id
    }

    /// Select a slice of the state where everything else is Default::default().
    pub fn select(
        &mut self,
        name: &str,
        selector: impl Fn(&S) -> S + 'static,
        callback: impl Fn(&S) + 'static,
    ) -> SelectorId {
        let id;
        print_perf!(
            id = self.select_internal(name, selector, callback, false);
            format!("Call selector {}", name)
        );
        SelectorId(id)
    }

    // Remove the given selector
    pub fn deselect(&mut self, id: SelectorId) {
        if let Some(selector) = self.selectors.remove(&id) {
            trace!("deselect {:?} {}", id, selector.name);
        }
    }

    /// Select a slice of the state with receiving the whole state.
    pub fn select_full(
        &mut self,
        name: &str,
        selector: impl Fn(&S) -> S + 'static,
        callback: impl Fn(&S) + 'static,
    ) -> SelectorId {
        let id;
        print_perf!(
            id = self.select_internal(name, selector, callback, true);
            format!("Call full state selector {}", name)
        );
        SelectorId(id)
    }

    /// Dispatch the given arguments as an [Action](crate::Action).
    pub fn dispatch(&mut self, action: String, argument: Option<glib::Variant>) {
        self.reduce_depth.set(self.reduce_depth.take() + 1);

        let message = format!("Dispatch action {:?} with argument {:?}", action, argument);
        print_perf! (
            {
                let action = Action::new(action, argument);

                trace!(
                    "Reduce action {:?} for state.",
                    action.name().to_string(),
                );

                print_perf!(
                    for middleware in &self.middlewares {
                        middleware.pre_reduce(&action, &self.state);
                    };
                    format!("Call {} middlewares pre_reduce", self.middlewares.len())
                );

                // reducing phase
                {
                    let lock = self.reduce_lock.try_lock();
                    if lock.is_err() {
                        error!("Can not dispatch '{}' during reduce", action.name());
                        return;
                    }
                    let reducer = &self.reducer;
                    print_perf!(
                        reducer(&action, &mut self.state);
                        format!("Reduce action {:?}", action.name())
                    );
                    trace!("Reduced to: {:?}", &self.state);
                    drop(lock);
                    print_perf!(
                        {
                            for (_sid, selector) in self.selectors.iter_mut() {
                                let sel = &selector.selector;
                                let selected_state = sel(&self.state);
                                if selector.last_state != selected_state {
                                    let callb = &selector.callback;
                                    if selector.full {
                                        callb(&self.state);
                                    } else {
                                        callb(&selected_state);
                                    }
                                    selector.last_state = selected_state;
                                }
                            }
                        };
                        format!("Determine n of {} selectors for action {:?}", self.selectors.len(), action.name())
                    );
                }

                print_perf!(
                    for middleware in &self.middlewares {
                        middleware.post_reduce(&action, &self.state);
                    };
                    format!("Call {} middlewares post_reduce", self.middlewares.len())
                );
            };
            message
        );

        self.reduce_depth.set(self.reduce_depth.take() - 1);
    }

    pub fn is_last_reduce_phase(&self) -> bool {
        self.reduce_depth.get() == 1
    }

    /// Delegate GTK actions as gstore::Actions to this store.
    pub fn delegate(
        &'static mut self,
    ) -> glib::Sender<(gdk4::gio::SimpleAction, Option<glib::Variant>)> {
        let (sender, receiver) = glib::MainContext::channel::<(
            gdk4::gio::SimpleAction,
            Option<glib::Variant>,
        )>(glib::PRIORITY_HIGH);
        receiver.attach(None, move |(action, argument)| {
            debug!("Delegate gtk action {:?} {:?}.", action, argument);
            self.dispatch(action.name().to_string(), argument);
            glib::Continue(true)
        });
        sender
    }
}

struct Selector<S: std::fmt::Debug + Clone + Default + 'static> {
    name: String,
    full: bool,
    selector: Box<dyn Fn(&S) -> S + 'static>,
    last_state: S,
    callback: Box<dyn Fn(&S) + 'static>,
}

impl<S: std::fmt::Debug + Clone + Default + 'static> Selector<S> {
    fn new(
        name: &str,
        full: bool,
        selector: impl Fn(&S) -> S + 'static,
        last_state: S,
        callback: impl Fn(&S) + 'static,
    ) -> Self {
        Selector {
            name: name.to_string(),
            full,
            selector: Box::new(selector),
            last_state,
            callback: Box::new(callback),
        }
    }
}

impl<S: std::fmt::Debug + Clone + Default + 'static> std::fmt::Debug for Selector<S> {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("Selector")
            .field("name", &self.name)
            .finish()
    }
}

/// A Middleware allows to handle actions before and after state mutation.
///
/// This allows implementing side effects for actions.
pub trait Middleware<S: std::fmt::Debug + Clone + Default + PartialEq + Eq + 'static> {
    fn pre_reduce(&self, _: &Action, _: &S) {}
    fn post_reduce(&self, _: &Action, _: &S) {}
}

#[macro_export]
macro_rules! dispatch {
    (
        $action:expr $(, $argument:expr)?
    ) => {{
        let action = $action;
        let mut argument = None;
        $(
        argument = Some(glib::ToVariant::to_variant(&$argument));
        )?
        store().dispatch(action.to_string(), argument);
    }};
}

#[macro_export]
macro_rules! select_state {
    (
        $name:expr, $selector:expr, $callback:expr
    ) => {
        store().select($name, $selector, $callback);
    };
}

#[macro_export]
macro_rules! select {
    (
        |$state:ident| $($selector:expr),*$(,)? => $callback:expr
    ) => {
        #[allow(clippy::redundant_closure_call)]
        store().select(
            vec![
                $(
                stringify!($selector)
                ),*
            ].join(", ").as_str(),
            |s| {
                let mut $state: State = Default::default();
                $(
                let sel = |$state: &State| $selector.clone();
                $selector = sel(s);
                )*
                $state
            },
            $callback,
        );
    };
}

#[macro_export]
macro_rules! store {
    (
        $state:ty
    ) => {
        pub type Store = gstore::Store<$state>;

        static mut STORE: Option<Store> = None;

        /// Initialize the store and global state
        ///
        /// # Arguments
        /// - default_state: The initial state.
        /// - reducer: Handle actions and mutate the state
        /// - middlewares: Pre- and post-reduce handlers
        pub fn init_store(
            default_state: $state,
            reducer: impl Fn(&gstore::Action, &mut $state) + 'static,
            middlewares: Vec<Box<dyn gstore::Middleware<$state>>>,
        ) {
            unsafe {
                STORE = Some(Store::new(default_state, reducer, middlewares));
                STORE.as_mut().unwrap().dispatch(gstore::INIT.into(), None);
            }
        }

        /// Get a static reference to the store
        pub fn store() -> &'static mut Store {
            unsafe { STORE.as_mut().expect("Store is not initialized!") }
        }
    };
}