Yoda 0.11.6

Browser for Gemini Protocol
use super::{BrowserAction, Profile, WindowAction};
use gtk::{
    Align, MenuButton,
    gio::{self},
    glib::{GString, Uri, UriFlags},
    prelude::{ActionExt, ToVariant},
};
use indexmap::IndexMap;
use std::{rc::Rc, sync::Arc};

// Config options

const LABEL_MAX_LENGTH: usize = 28;
pub trait Menu {
    fn menu(action: (&Rc<BrowserAction>, &Rc<WindowAction>), profile: &Arc<Profile>) -> Self;
}

#[rustfmt::skip] // @TODO template builder?
impl Menu for MenuButton {
    // Constructors

    /// Build new `Self`
     fn menu(
        (browser_action, window_action): (&Rc<BrowserAction>, &Rc<WindowAction>),
        profile: &Arc<Profile>,
    ) -> Self {
        // Main
        let main = gio::Menu::new();

            // Main > Page
            let main_page = gio::Menu::new();
                main_page.append(Some("New"), Some(&format!(
                    "{}.{}",
                    window_action.id,
                    window_action.append.simple_action.name()
                )));

                main_page.append(Some("Reload"), Some(&format!(
                    "{}.{}",
                    window_action.id,
                    window_action.reload.simple_action.name()
                )));

                main_page.append(Some("Find.."), Some(&format!(
                    "{}.{}",
                    window_action.id,
                    window_action.find.simple_action.name()
                )));

                // Main > Page > File
                let main_page_file = gio::Menu::new();

                    main_page_file.append(Some("Open.."), Some(&format!(
                        "{}.{}",
                        window_action.id,
                        window_action.open.simple_action.name()
                    )));

                    main_page_file.append(Some("Save as.."), Some(&format!(
                        "{}.{}",
                        window_action.id,
                        window_action.save_as.simple_action.name()
                    )));

                main_page.append_submenu(Some("File"), &main_page_file);

                // Main > Page > Mark
                let main_page_mark = gio::Menu::new();

                    main_page_mark.append(Some("Bookmark"), Some(&format!(
                        "{}.{}",
                        window_action.id,
                        window_action.bookmark.simple_action.name()
                    )));

                    main_page_mark.append(Some("Pin"), Some(&format!(
                        "{}.{}",
                        window_action.id,
                        window_action.pin.simple_action.name()
                    )));

                main_page.append_section(None, &main_page_mark);

                // Main > Page > Tools
                let main_page_tools = gio::Menu::new();

                main_page_tools.append(Some("Source"), Some(&format!(
                    "{}.{}",
                    window_action.id,
                    window_action.source.simple_action.name()
                )));

                main_page.append_section(None, &main_page_tools);

                // Main > Page > Navigation
                let main_page_navigation = gio::Menu::new();

                    main_page_navigation.append(Some("Home"), Some(&format!(
                        "{}.{}",
                        window_action.id,
                        window_action.home.simple_action.name()
                    )));

                    // Main > Page > Navigation > History
                    let main_page_navigation_history = gio::Menu::new();

                        main_page_navigation_history.append(Some("Back"), Some(&format!(
                            "{}.{}",
                            window_action.id,
                            window_action.history_back.simple_action.name()
                        )));

                        main_page_navigation_history.append(Some("Forward"), Some(&format!(
                            "{}.{}",
                            window_action.id,
                            window_action.history_forward.simple_action.name()
                        )));

                    main_page_navigation.append_submenu(Some("Navigation history"), &main_page_navigation_history);

                main_page.append_section(None, &main_page_navigation);

                // Main > Page > Close
                let main_page_close = gio::Menu::new();

                    main_page_close.append(Some("Current"), Some(&format!(
                        "{}.{}",
                        window_action.id,
                        window_action.close.simple_action.name()
                    )));

                    main_page_close.append(Some("All"), Some(&format!(
                        "{}.{}",
                        window_action.id,
                        window_action.close_all.simple_action.name()
                    )));

                    main_page.append_submenu(Some("Close"), &main_page_close);

                main.append_submenu(Some("Page"), &main_page);

            // Main > Bookmark
            // * menu items dynamically generated using profile memory pool and `set_create_popup_func`
            let main_bookmarks = gio::Menu::new();

                main.append_submenu(Some("Bookmarks"), &main_bookmarks);

            // Main > History
            let main_history = gio::Menu::new();

                // Main > History > Recently closed
                // * menu items dynamically generated using profile memory pool and `set_create_popup_func`
                let main_history_tab = gio::Menu::new();
                    main_history.append_submenu(Some("Recently closed"), &main_history_tab);

                // Main > History > Recent requests
                // * menu items dynamically generated using profile memory pool and `set_create_popup_func`
                let main_history_request = gio::Menu::new();
                    main_history.append_section(None, &main_history_request);

                main.append_submenu(Some("History"), &main_history);

            // Main > Tool
            let main_tool = gio::Menu::new();

                // Debug
                main_tool.append(Some("Debug"), Some(&format!(
                    "{}.{}",
                    browser_action.id,
                    browser_action.debug.simple_action.name()
                )));

                main_tool.append(Some("Profile"), Some(&format!(
                    "{}.{}",
                    browser_action.id,
                    browser_action.profile.simple_action.name()
                )));

                main_tool.append(Some("About"), Some(&format!(
                    "{}.{}",
                    browser_action.id,
                    browser_action.about.simple_action.name()
                )));

        main.append_submenu(Some("Tool"), &main_tool);

        main.append(Some("Quit"), Some(&format!(
            "{}.{}",
            browser_action.id,
            browser_action.close.simple_action.name()
        )));

        // Init main widget
        let menu_button = MenuButton::builder()
                .css_classes(["flat"])
                .icon_name("open-menu-symbolic")
                .menu_model(&main)
                .tooltip_text("Menu")
                .valign(Align::Center)
                .build();

            // Generate dynamical menu items
            menu_button.set_create_popup_func({
                let profile = profile.clone();
                let main_bookmarks = main_bookmarks.clone();
                let window_action = window_action.clone();
                move |_| {
                    // Bookmarks
                    main_bookmarks.remove_all();
                    for bookmark in profile.bookmark.recent(None) {
                        let menu_item = gio::MenuItem::new(Some(&ellipsize(&bookmark.request, LABEL_MAX_LENGTH)), None);
                            menu_item.set_action_and_target_value(Some(&format!(
                                "{}.{}",
                                window_action.id,
                                window_action.load.simple_action.name()
                            )), Some(&bookmark.request.to_variant()));

                        main_bookmarks.append_item(&menu_item);
                    } // @TODO `menu_item`

                    // Recently closed history
                    main_history_tab.remove_all();
                    for history in profile.history.recently_closed(None) {
                        let menu_item = gio::MenuItem::new(Some(&ellipsize(&history.request, LABEL_MAX_LENGTH)), None);
                            menu_item.set_action_and_target_value(Some(&format!(
                                "{}.{}",
                                window_action.id,
                                window_action.load.simple_action.name()
                            )), Some(&history.request.to_variant()));

                            main_history_tab.append_item(&menu_item);
                    } // @TODO `menu_item`

                    // Recently visited history
                    // * in first iteration, group records by it hostname
                    // * in second iteration, collect uri path as the menu sub-item label
                    main_history_request.remove_all();

                    let mut list: IndexMap<GString, Vec<Uri>> = IndexMap::new();
                    for history in profile.history.recently_opened(None) {
                        match Uri::parse(&history.request, UriFlags::NONE) {
                            Ok(uri) => list.entry(match uri.host() {
                                Some(host) => host,
                                None => uri.to_str(),
                            }).or_default().push(uri),
                            Err(_) => continue // @TODO
                        }
                    }

                    for (group, items) in list {
                        let list = gio::Menu::new();

                        // Show first menu item only without children menu
                        if items.len() == 1 {
                            main_history_request.append_item(&menu_item(&window_action, &items[0], true));

                        // Create children menu items related to parental host item
                        } else {
                            for uri in items {
                                list.append_item(&menu_item(&window_action, &uri, false));
                            }
                            main_history_request.append_submenu(Some(&group), &list);
                        }
                    }
                }
            });

        menu_button
    }
}

/// Format dynamically generated strings for menu item label
/// * crop resulting string at the middle position on new `value` longer than `limit`
fn ellipsize(value: &str, limit: usize) -> String {
    if value.len() <= limit {
        return value.to_string();
    }

    let length = (limit - 2) / 2;

    format!("{}..{}", &value[..length], &value[value.len() - length..])
}

/// Format [Uri](https://docs.gtk.org/glib/struct.Uri.html)
/// as [MenuItem](https://docs.gtk.org/gio/class.MenuItem.html) label
fn uri_to_label(uri: &Uri, is_parent: bool) -> GString {
    let path = uri.path();
    if path == "/" || path.is_empty() {
        if is_parent {
            uri.host().unwrap_or(uri.to_str())
        } else {
            gtk::glib::gformat!("{}{path}", uri.host().unwrap_or(uri.to_str()))
        }
    } else {
        path
    }
}

/// Shared helper to create new [MenuItem](https://docs.gtk.org/gio/class.MenuItem.html)
fn menu_item(action: &WindowAction, uri: &Uri, is_parent: bool) -> gio::MenuItem {
    let item = gio::MenuItem::new(
        Some(&ellipsize(&uri_to_label(uri, is_parent), LABEL_MAX_LENGTH)),
        None,
    );
    item.set_action_and_target_value(
        Some(&format!(
            "{}.{}",
            action.id,
            action.load.simple_action.name()
        )),
        Some(&uri.to_string().to_variant()),
    );
    item
}