Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Menus

snora has two menu shapes:

  • Header menu — drop-down attached to the header bar (File / Edit / View). Triggered by clicking a labeled button. Item list renders inline below the button.
  • Context menu — floating menu, typically right-click. Renders at a caller-chosen point. See overlays.

Both share the Menu / MenuItem / MenuAction vocabulary from snora-core.

Application-defined ids

You define two enums for menu identities:

#![allow(unused)]
fn main() {
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum MyMenuId {
    File,
    View,
    Help,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum MyMenuItemId {
    New,
    Open,
    Quit,
    ToggleStatus,
    About,
}
}

snora is generic over both. It does not impose any string-based naming or numeric tagging — your enum is the source of truth.

Building menus

#![allow(unused)]
fn main() {
use snora::{Menu, MenuItem};

let menus = vec![
    Menu {
        id: MyMenuId::File,
        label: "File".into(),
        icon: None,
        items: vec![
            MenuItem { menu_id: MyMenuId::File, id: MyMenuItemId::New,  label: "New".into(),  icon: None },
            MenuItem { menu_id: MyMenuId::File, id: MyMenuItemId::Open, label: "Open…".into(), icon: None },
            MenuItem { menu_id: MyMenuId::File, id: MyMenuItemId::Quit, label: "Quit".into(),  icon: None },
        ],
    },
    // …View, Help…
];
}

MenuItem::menu_id repeats the parent menu id so that the [MenuAction::MenuItemPressed] event carries it without a second lookup.

Wiring into a header

#![allow(unused)]
fn main() {
use snora::{
    AppLayout, LayoutDirection, MenuAction,
    render, widget::app_header,
};

#[derive(Debug, Clone)]
enum Message {
    HeaderAction(MenuAction<MyMenuId, MyMenuItemId>),
    CloseMenus,
}

fn view(state: &State) -> iced::Element<'_, Message> {
    let header = app_header(
        "My App",
        menus,                              // built above
        &Message::HeaderAction,             // map MenuAction → Message
        state.active_menu.as_ref(),         // which menu is open
        None,                               // no end-controls
        LayoutDirection::Ltr,
    );

    let mut layout = AppLayout::new(state.body())
        .header(header)
        .on_close_menus(Message::CloseMenus);

    // Snora installs the click-outside backdrop only when
    // `header_menu` is `Some`. The actual dropdown is drawn inline by
    // `app_header`; this slot just opts the backdrop in.
    if state.active_menu.is_some() {
        layout = layout.header_menu(iced::widget::space().into());
    }

    render(layout)
}
}

Handling the actions

MenuAction has two variants:

#![allow(unused)]
fn main() {
match action {
    MenuAction::MenuPressed(id) => {
        // Toggle: same id closes; different id switches.
        state.active_menu = if state.active_menu == Some(id) { None } else { Some(id) };
    }
    MenuAction::MenuItemPressed { menu_id, menu_item_id } => {
        state.active_menu = None;            // close after pick
        state.dispatch(menu_id, menu_item_id);
    }
}
}

The “click the button to toggle, click an item to close” pattern is the most common; you are free to use a different model (e.g. hover-to-open) since snora only emits the events.

Why header_menu takes an empty Space

iced 0.14’s element tree does not expose absolute positioning at the overlay layer; the dropdown is drawn inside app_header and what populates AppLayout::header_menu is just the opt-in signal that “a menu is open, please install the click-outside backdrop”. Using Space (zero-sized, transparent) is the canonical idiom.

This may become an actual element in a future version if iced exposes absolute overlay positioning. The application-facing shape stays the same.