Skip to main content

snora_widgets/
header.rs

1//! A minimal desktop-style header bar.
2//!
3//! Layout (logical, ABDD):
4//!
5//! ```text
6//!  ┌────────────────────────────────────────────────────────────────┐
7//!  │ [title] [menu] [menu] [menu] ...           ...   [end_controls]│
8//!  └────────────────────────────────────────────────────────────────┘
9//!    └────────── start ──────────┘            └────── end ─────────┘
10//! ```
11//!
12//! Under [`LayoutDirection::Rtl`] the two groups swap sides automatically —
13//! individual elements inside each group keep their internal order.
14
15use std::fmt::Debug;
16
17use iced::{
18    Alignment::Center,
19    Element, Length, Padding,
20    widget::{container, space, text},
21};
22
23use snora_core::{LayoutDirection, Menu, MenuAction};
24
25use crate::direction::row_dir;
26use crate::style::chrome_container_style;
27use crate::menu::render_menu;
28
29/// Build an application header.
30///
31/// * `title` — the app name, rendered bold at the start edge.
32/// * `menus` — drop-down menus (File / View / ...). Rendered immediately
33///   after the title. Pass `vec![]` for a title-only header.
34/// * `on_menu_action` — maps [`MenuAction`] events into your message type.
35/// * `active_menu_id` — the currently-open menu, if any. Needed so the
36///   menu widget can render its dropdown items. Usually a field on your
37///   application state.
38/// * `end_controls` — optional element pinned to the end edge
39///   (right under LTR, left under RTL). Typically status indicators,
40///   theme toggles, etc.
41/// * `direction` — application's reading direction.
42pub fn app_header<'a, Message, MenuId, MenuItemId, F>(
43    title: &'a str,
44    menus: Vec<Menu<MenuId, MenuItemId>>,
45    on_menu_action: &'a F,
46    active_menu_id: Option<&MenuId>,
47    end_controls: Option<Element<'a, Message>>,
48    direction: LayoutDirection,
49) -> Element<'a, Message>
50where
51    Message: Clone + 'a,
52    MenuId: Clone + Debug + PartialEq + 'a,
53    MenuItemId: Clone + Debug + 'a,
54    F: Fn(MenuAction<MenuId, MenuItemId>) -> Message + 'a,
55{
56    // Start group: [title, gap, menus...].
57    let mut start_group = iced::widget::row![
58        text(title)
59            .font(iced::Font {
60                weight: iced::font::Weight::Bold,
61                ..Default::default()
62            })
63            .size(16),
64        container(space()).width(Length::Fixed(20.0)),
65    ]
66    .align_y(Center)
67    .spacing(12);
68
69    for menu in menus {
70        let is_active = active_menu_id == Some(&menu.id);
71        start_group = start_group.push(render_menu(menu, on_menu_action, is_active));
72    }
73
74    // Middle filler — pushes end_controls to the far edge.
75    let filler = container(space()).width(Length::Fill);
76
77    // Compose start + filler + end in logical order.
78    let end_side: Element<'_, Message> = match end_controls {
79        Some(ctrls) => iced::widget::row![filler, ctrls]
80            .align_y(Center)
81            .spacing(12)
82            .into(),
83        None => filler.into(),
84    };
85
86    let header_row = row_dir(direction, start_group, end_side).align_y(Center);
87
88    container(header_row)
89        .width(Length::Fill)
90        .padding(Padding::from([8.0, 16.0]))
91        .style(chrome_container_style)
92        .into()
93}