1
//! Software menu-bar widget — the Linux fallback when there is no native global
2
//! menu (GNOME/KDE export their own; Windows uses `HMENU`, macOS the app menu).
3
//!
4
//! Renders the window's [`Menu`] (declared by the user via
5
//! `Dom::with_menu_bar(menu)`) as a horizontal bar of top-level items. Clicking a
6
//! top-level item opens its children as a dropdown popup positioned directly
7
//! below the item via [`CallbackInfo::open_menu_for_hit_node`] — which looks up
8
//! the clicked node's on-screen rect and opens a child window at its bottom-left
9
//! (the unified [`WindowPosition::RelativeToParentWindow`] path).
10
//!
11
//! ## Styling
12
//!
13
//! All styling is inline via `with_css(&str)` using the `system:` color namespace
14
//! (`system:window-background`, `system:text`, `system:selection-background`, …)
15
//! and the `system:ui` font, so the bar matches the OS theme without threading a
16
//! `SystemStyle` through. The bar MUST be injected at the *Dom* level (before
17
//! `StyledDom::create_from_dom`) so `scope_inline_css` scopes these rules in the
18
//! same flatten pass as the rest of the window — see
19
//! `shell2::common::layout::regenerate_layout`.
20
//!
21
//! ## Backreference pattern
22
//!
23
//! The dropdown's items are the user's own [`MenuItem`]s, each carrying the
24
//! `(RefAny, Callback)` backreference the user attached with
25
//! `StringMenuItem::with_callback(data, cb)`. Clicking a leaf fires that callback
26
//! with the user's data — so the bar is a thin shell and behaviour stays in user
27
//! code (see `doc/guide/en/architecture.md`, "the backreference pattern").
28
//!
29
//! [`WindowPosition::RelativeToParentWindow`]: azul_core::window::WindowPosition::RelativeToParentWindow
30

            
31
use azul_core::{
32
    callbacks::{CoreCallback, CoreCallbackData},
33
    dom::{Dom, EventFilter, HoverEventFilter, IdOrClass::Class, IdOrClassVec},
34
    menu::{Menu, MenuItem, MenuItemVec, StringMenuItem},
35
    refany::{OptionRefAny, RefAny},
36
};
37

            
38
/// Class on the injected bar root — also the detection marker the renderer / tests
39
/// use to recognise an injected software menu bar.
40
pub const MENUBAR_CLASS: &str = "__azul-native-menubar";
41
/// Class on each top-level bar item.
42
pub const MENUBAR_ITEM_CLASS: &str = "azul-menubar-item";
43

            
44
/// Inline CSS for the bar root: a full-width horizontal flex row themed from the
45
/// OS (`system:` colors + `system:ui` font). Bare declarations, so the rule is
46
/// scoped node-only at flatten time.
47
const MENUBAR_CSS: &str = "display: flex; \
48
     flex-direction: row; \
49
     align-items: stretch; \
50
     width: 100%; \
51
     height: 26px; \
52
     background: system:window-background; \
53
     color: system:text; \
54
     font-family: system:ui; \
55
     font-size: 14px; \
56
     padding-left: 2px;";
57

            
58
/// Inline CSS for a top-level item: vertically-centered click target with hover
59
/// feedback (the `:hover` block nests via CSS nesting in `parse_inline`).
60
const MENUBAR_ITEM_CSS: &str = "display: flex; \
61
     flex-direction: row; \
62
     align-items: center; \
63
     padding-left: 10px; \
64
     padding-right: 10px; \
65
     color: system:text; \
66
     cursor: pointer; \
67
     :hover { background: system:selection-background; color: system:selection-text; }";
68

            
69
/// Build the software menu-bar DOM from a [`Menu`].
70
///
71
/// The bar is a flex row of one item per top-level `MenuItem::String`
72
/// (separators / break-lines are not rendered in the bar). Inject the returned
73
/// `Dom` at the Dom level so its `with_css` rules are scoped in the main flatten.
74
176
pub fn build_menubar_dom(menu: &Menu) -> Dom {
75
176
    let mut bar = Dom::create_div()
76
176
        .with_ids_and_classes(IdOrClassVec::from_vec(vec![
77
176
            Class(MENUBAR_CLASS.into()),
78
176
            Class("azul-menubar".into()),
79
        ]))
80
176
        .with_css(MENUBAR_CSS);
81

            
82
924
    for item in menu.items.as_slice() {
83
924
        if let MenuItem::String(s) = item {
84
924
            bar = bar.with_child(build_menubar_item(s));
85
924
        }
86
    }
87

            
88
176
    bar
89
176
}
90

            
91
/// One clickable top-level bar item. Its `MouseUp` callback opens the item's
92
/// submenu (its children, or — for a top-level leaf — a one-item menu of itself
93
/// so the leaf's own callback still fires) below the item.
94
924
fn build_menubar_item(item: &StringMenuItem) -> Dom {
95
    // The submenu carried (by value) into the click callback as its RefAny.
96
924
    let submenu = if item.children.as_slice().is_empty() {
97
924
        Menu::create(MenuItemVec::from_vec(vec![MenuItem::String(item.clone())]))
98
    } else {
99
        Menu::create(item.children.clone())
100
    };
101

            
102
924
    Dom::create_div()
103
924
        .with_ids_and_classes(IdOrClassVec::from_vec(vec![Class(MENUBAR_ITEM_CLASS.into())]))
104
924
        .with_css(MENUBAR_ITEM_CSS)
105
924
        .with_child(Dom::create_text(item.label.clone()))
106
924
        .with_callbacks(
107
924
            vec![CoreCallbackData {
108
924
                event: EventFilter::Hover(HoverEventFilter::MouseUp),
109
924
                callback: CoreCallback {
110
924
                    cb: callbacks::menubar_item_click as usize,
111
924
                    ctx: OptionRefAny::None,
112
924
                },
113
924
                refany: RefAny::new(submenu),
114
924
            }]
115
924
            .into(),
116
        )
117
924
}
118

            
119
pub(crate) mod callbacks {
120
    use azul_core::{callbacks::Update, menu::Menu, refany::RefAny};
121

            
122
    use crate::callbacks::CallbackInfo;
123

            
124
    /// Top-level bar item clicked → open its submenu under the item. The submenu
125
    /// `Menu` is the callback's `data` (a backreference set at build time); its
126
    /// items carry the user's own callbacks, fired by the menu system on click.
127
    pub extern "C" fn menubar_item_click(mut data: RefAny, mut info: CallbackInfo) -> Update {
128
        if let Some(menu) = data.downcast_ref::<Menu>() {
129
            info.open_menu_for_hit_node(menu.clone());
130
        }
131
        Update::DoNothing
132
    }
133
}