armas_basic/components/
menubar.rs1use super::content::ContentContext;
27use super::dropdown_menu::{DropdownMenu, MenuBuilder};
28use egui::{Id, Rect, Sense, Stroke, Ui};
29
30const TRIGGER_PADDING_X: f32 = 12.0; const TRIGGER_PADDING_Y: f32 = 6.0; const TRIGGER_RADIUS: f32 = 4.0; const BAR_HEIGHT: f32 = 40.0; const BAR_RADIUS: f32 = 6.0; const BAR_PADDING: f32 = 4.0; type TriggerRenderFn = Box<dyn Fn(&mut Ui, &ContentContext)>;
41
42enum MenuTrigger {
44 Text(String),
46 Custom { width: f32, render: TriggerRenderFn },
48}
49
50struct MenuEntry {
52 trigger: MenuTrigger,
53 items: Vec<super::dropdown_menu::MenuItemData>,
54}
55
56pub struct MenubarBuilder {
58 entries: Vec<MenuEntry>,
59}
60
61impl MenubarBuilder {
62 const fn new() -> Self {
63 Self {
64 entries: Vec::new(),
65 }
66 }
67
68 pub fn menu(&mut self, label: impl Into<String>, content: impl FnOnce(&mut MenuBuilder)) {
70 let mut builder = MenuBuilder::new();
71 content(&mut builder);
72 self.entries.push(MenuEntry {
73 trigger: MenuTrigger::Text(label.into()),
74 items: builder.into_items(),
75 });
76 }
77
78 pub fn menu_ui(
95 &mut self,
96 trigger_width: f32,
97 trigger: impl Fn(&mut Ui, &ContentContext) + 'static,
98 content: impl FnOnce(&mut MenuBuilder),
99 ) {
100 let mut builder = MenuBuilder::new();
101 content(&mut builder);
102 self.entries.push(MenuEntry {
103 trigger: MenuTrigger::Custom {
104 width: trigger_width,
105 render: Box::new(trigger),
106 },
107 items: builder.into_items(),
108 });
109 }
110}
111
112pub struct Menubar {
114 id: Id,
115}
116
117pub struct MenubarResponse {
119 pub response: egui::Response,
121}
122
123impl Menubar {
124 pub fn new(id: impl Into<Id>) -> Self {
126 Self { id: id.into() }
127 }
128
129 pub fn show(self, ui: &mut Ui, menus: impl FnOnce(&mut MenubarBuilder)) -> MenubarResponse {
131 let theme = crate::ext::ArmasContextExt::armas_theme(ui.ctx());
132
133 let mut builder = MenubarBuilder::new();
135 menus(&mut builder);
136 let entries = builder.entries;
137
138 let state_id = self.id.with("menubar_active");
140 let mut active_menu: Option<usize> =
141 ui.ctx().data_mut(|d| d.get_temp(state_id).unwrap_or(None));
142
143 let (bar_rect, bar_response) =
145 ui.allocate_exact_size(egui::vec2(ui.available_width(), BAR_HEIGHT), Sense::hover());
146
147 ui.painter()
148 .rect_filled(bar_rect, BAR_RADIUS, theme.background());
149 ui.painter().rect_stroke(
150 bar_rect,
151 BAR_RADIUS,
152 Stroke::new(1.0, theme.border()),
153 egui::epaint::StrokeKind::Inside,
154 );
155
156 let mut trigger_rects: Vec<Rect> = Vec::new();
158 let mut x = bar_rect.left() + BAR_PADDING;
159 let trigger_text_size = theme.typography.base;
160 let trigger_y =
161 bar_rect.top() + (BAR_HEIGHT - TRIGGER_PADDING_Y * 2.0 - trigger_text_size) / 2.0;
162
163 for (i, entry) in entries.iter().enumerate() {
164 let trigger_height = trigger_text_size + TRIGGER_PADDING_Y * 2.0;
165
166 let trigger_width = match &entry.trigger {
167 MenuTrigger::Text(label) => {
168 let galley = ui.painter().layout_no_wrap(
169 label.clone(),
170 egui::FontId::proportional(trigger_text_size),
171 theme.foreground(),
172 );
173 galley.size().x + TRIGGER_PADDING_X * 2.0
174 }
175 MenuTrigger::Custom { width, .. } => *width,
176 };
177
178 let trigger_rect = Rect::from_min_size(
179 egui::pos2(x, trigger_y),
180 egui::vec2(trigger_width, trigger_height),
181 );
182
183 let trigger_response = ui.interact(trigger_rect, self.id.with(i), Sense::click());
184 let is_active = active_menu == Some(i);
185 let is_hovered = trigger_response.hovered();
186
187 if is_active || is_hovered {
189 ui.painter()
190 .rect_filled(trigger_rect, TRIGGER_RADIUS, theme.accent());
191 }
192
193 let text_color = if is_active || is_hovered {
195 theme.accent_foreground()
196 } else {
197 theme.foreground()
198 };
199
200 match &entry.trigger {
201 MenuTrigger::Text(label) => {
202 ui.painter().text(
203 trigger_rect.center(),
204 egui::Align2::CENTER_CENTER,
205 label,
206 egui::FontId::proportional(trigger_text_size),
207 text_color,
208 );
209 }
210 MenuTrigger::Custom { render, .. } => {
211 let mut child_ui = ui.new_child(
212 egui::UiBuilder::new()
213 .max_rect(trigger_rect)
214 .layout(egui::Layout::left_to_right(egui::Align::Center)),
215 );
216 child_ui.style_mut().visuals.override_text_color = Some(text_color);
217
218 let ctx = ContentContext {
219 color: text_color,
220 font_size: trigger_text_size,
221 is_active,
222 };
223 render(&mut child_ui, &ctx);
224 }
225 }
226
227 if trigger_response.clicked() {
229 if is_active {
230 active_menu = None;
231 } else {
232 active_menu = Some(i);
233 }
234 }
235
236 if active_menu.is_some() && !is_active && is_hovered {
238 active_menu = Some(i);
239 }
240
241 trigger_rects.push(trigger_rect);
242 x += trigger_width + 2.0;
243 }
244
245 if let Some(active_idx) = active_menu {
247 if active_idx < entries.len() {
248 let anchor = trigger_rects[active_idx];
249 let mut dropdown =
250 DropdownMenu::new(self.id.with(("dropdown", active_idx))).open(true);
251
252 let entry_items = &entries[active_idx].items;
253 let response = dropdown.show(ui.ctx(), anchor, |menu| {
254 replay_items(menu, entry_items);
255 });
256
257 if response.selected.is_some() || response.clicked_outside {
258 active_menu = None;
259 }
260 }
261 }
262
263 ui.ctx().data_mut(|d| d.insert_temp(state_id, active_menu));
265
266 MenubarResponse {
267 response: bar_response,
268 }
269 }
270}
271
272fn replay_items(menu: &mut MenuBuilder, items: &[super::dropdown_menu::MenuItemData]) {
274 for item_data in items {
275 menu.push_item(item_data.clone());
276 }
277}