snora-widgets 0.27.0

Optional prefab iced widgets for the Snora GUI framework — header, footer, sidebar, menu, icon.
Documentation
//! Render a [`snora_core::Menu`] into an `iced::Element`.
//!
//! A menu renders as a column: the trigger button on top, and — if the
//! menu is currently active — the item list below. The engine installs
//! the click-outside backdrop at the application level, so this widget
//! only paints the menu's own surface.

use std::fmt::Debug;

use iced::{
    Alignment::Center,
    Element,
    widget::{button, column, container, row, text},
};

use snora_core::{Menu, MenuAction};

use crate::icon::icon_element;
use crate::style::menu_button_style;

/// Geometry parameter [`build_menu`] takes, letting [`render_menu`]
/// (unstyled) and the `design`-gated styled variant (RFC-040) share one
/// implementation.
pub(crate) struct MenuGeometry {
    /// Gap between a menu/item's icon and its label.
    pub(crate) gap: f32,
}

impl MenuGeometry {
    /// Today's literal, unmodified.
    pub(crate) const fn unstyled() -> Self {
        Self { gap: 6.0 }
    }
}

/// Render a single menu (header button + dropdown when active).
///
/// `on_action` is called for every interaction and must map the resulting
/// [`MenuAction`] into the application's message type. A plain function
/// item works well here because it has a zero-sized `'static` type and
/// can be passed by reference.
pub fn render_menu<'a, Message, MenuId, MenuItemId, F>(
    menu: Menu<MenuId, MenuItemId>,
    on_action: &'a F,
    is_active: bool,
) -> Element<'a, Message>
where
    Message: Clone + 'a,
    MenuId: Clone + Debug + PartialEq + 'a,
    MenuItemId: Clone + Debug + 'a,
    F: Fn(MenuAction<MenuId, MenuItemId>) -> Message + 'a,
{
    build_menu(menu, on_action, is_active, MenuGeometry::unstyled())
}

pub(crate) fn build_menu<'a, Message, MenuId, MenuItemId, F>(
    menu: Menu<MenuId, MenuItemId>,
    on_action: &'a F,
    is_active: bool,
    geometry: MenuGeometry,
) -> Element<'a, Message>
where
    Message: Clone + 'a,
    MenuId: Clone + Debug + PartialEq + 'a,
    MenuItemId: Clone + Debug + 'a,
    F: Fn(MenuAction<MenuId, MenuItemId>) -> Message + 'a,
{
    // Trigger button (always visible).
    let mut header_content = row![].spacing(geometry.gap).align_y(Center);
    if let Some(ref ic) = menu.icon {
        header_content = header_content.push(icon_element(ic));
    }
    header_content = header_content.push(text(menu.label).size(14));

    let trigger_msg = on_action(MenuAction::MenuPressed(menu.id.clone()));

    let mut stack = column![];
    stack = stack.push(
        button(header_content)
            .style(menu_button_style)
            .on_press(trigger_msg),
    );

    if !is_active {
        return container(stack).into();
    }

    // Dropdown — only rendered when this menu is active.
    for item in menu.items {
        let mut btn_content = row![].spacing(geometry.gap).align_y(Center);
        if let Some(ref ic) = item.icon {
            btn_content = btn_content.push(icon_element(ic));
        }
        btn_content = btn_content.push(text(item.label).size(14));

        let msg = on_action(MenuAction::MenuItemPressed {
            menu_id: item.menu_id.clone(),
            menu_item_id: item.id.clone(),
        });

        stack = stack.push(button(btn_content).style(menu_button_style).on_press(msg));
    }

    container(stack).into()
}