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}