1#![allow(clippy::uninlined_format_args)]
20use crate::_private::NonExhaustive;
21use crate::event::MenuOutcome;
22use crate::menuline::{MenuLine, MenuLineState};
23use crate::popup_menu::{PopupMenu, PopupMenuState};
24use crate::{MenuStructure, MenuStyle};
25use rat_cursor::HasScreenCursor;
26use rat_event::{ConsumedEvent, HandleEvent, MouseOnly, Popup, Regular};
27use rat_focus::{FocusBuilder, FocusFlag, HasFocus, Navigation};
28use rat_popup::Placement;
29use rat_reloc::RelocatableState;
30use ratatui_core::buffer::Buffer;
31use ratatui_core::layout::{Alignment, Rect};
32use ratatui_core::style::Style;
33use ratatui_core::text::Line;
34use ratatui_core::widgets::StatefulWidget;
35use ratatui_crossterm::crossterm::event::Event;
36use ratatui_widgets::block::Block;
37use std::fmt::Debug;
38
39#[derive(Debug, Clone)]
44pub struct Menubar<'a> {
45 structure: Option<&'a dyn MenuStructure<'a>>,
46
47 menu: MenuLine<'a>,
48
49 popup_alignment: Alignment,
50 popup_placement: Placement,
51 popup_offset: Option<(i16, i16)>,
52 popup: PopupMenu<'a>,
53}
54
55#[derive(Debug, Clone)]
59pub struct MenubarLine<'a> {
60 structure: Option<&'a dyn MenuStructure<'a>>,
61 menu: MenuLine<'a>,
62}
63
64#[derive(Debug, Clone)]
68pub struct MenubarPopup<'a> {
69 structure: Option<&'a dyn MenuStructure<'a>>,
70 popup_alignment: Alignment,
71 popup_placement: Placement,
72 popup_offset: Option<(i16, i16)>,
73 popup: PopupMenu<'a>,
74}
75
76#[derive(Debug, Clone)]
78pub struct MenubarState {
79 pub area: Rect,
82 pub bar: MenuLineState,
84 pub popup: PopupMenuState,
86
87 relocate_popup: bool,
90
91 pub non_exhaustive: NonExhaustive,
92}
93
94impl Default for Menubar<'_> {
95 fn default() -> Self {
96 Self {
97 structure: Default::default(),
98 menu: Default::default(),
99 popup_alignment: Alignment::Left,
100 popup_placement: Placement::AboveOrBelow,
101 popup_offset: Default::default(),
102 popup: Default::default(),
103 }
104 }
105}
106
107impl<'a> Menubar<'a> {
108 #[inline]
109 pub fn new(structure: &'a dyn MenuStructure<'a>) -> Self {
110 Self {
111 structure: Some(structure),
112 ..Default::default()
113 }
114 }
115
116 #[inline]
118 pub fn style(mut self, style: Style) -> Self {
119 self.menu = self.menu.style(style);
120 self
121 }
122
123 #[inline]
125 pub fn block(mut self, block: Block<'a>) -> Self {
126 self.menu = self.menu.block(block);
127 self
128 }
129
130 #[inline]
132 pub fn title(mut self, title: impl Into<Line<'a>>) -> Self {
133 self.menu = self.menu.title(title);
134 self
135 }
136
137 #[inline]
139 pub fn title_style(mut self, style: Style) -> Self {
140 self.menu = self.menu.title_style(style);
141 self
142 }
143
144 #[inline]
146 pub fn focus_style(mut self, style: Style) -> Self {
147 self.menu = self.menu.focus_style(style);
148 self
149 }
150
151 #[inline]
153 pub fn right_style(mut self, style: Style) -> Self {
154 self.menu = self.menu.right_style(style);
155 self
156 }
157
158 #[inline]
161 pub fn popup_width(mut self, width: u16) -> Self {
162 self.popup = self.popup.menu_width(width);
163 self
164 }
165
166 #[inline]
168 pub fn popup_alignment(mut self, alignment: Alignment) -> Self {
169 self.popup_alignment = alignment;
170 self
171 }
172
173 #[inline]
175 pub fn popup_offset(mut self, offset: (i16, i16)) -> Self {
176 self.popup_offset = Some(offset);
177 self
178 }
179
180 #[inline]
182 pub fn popup_placement(mut self, placement: Placement) -> Self {
183 self.popup_placement = placement;
184 self
185 }
186
187 #[inline]
189 pub fn popup_style(mut self, style: Style) -> Self {
190 self.popup = self.popup.style(style);
191 self
192 }
193
194 #[inline]
196 pub fn popup_block(mut self, block: Block<'a>) -> Self {
197 self.popup = self.popup.block(block);
198 self
199 }
200
201 #[inline]
203 pub fn popup_focus_style(mut self, style: Style) -> Self {
204 self.popup = self.popup.focus_style(style);
205 self
206 }
207
208 #[inline]
210 pub fn popup_right_style(mut self, style: Style) -> Self {
211 self.popup = self.popup.right_style(style);
212 self
213 }
214
215 #[inline]
217 pub fn styles(mut self, styles: MenuStyle) -> Self {
218 self.menu = self.menu.styles(styles.clone());
219 self.popup = self.popup.styles(styles.clone());
220
221 if let Some(alignment) = styles.popup.alignment {
222 self.popup_alignment = alignment;
223 }
224 if let Some(placement) = styles.popup.placement {
225 self.popup_placement = placement;
226 }
227 if let Some(offset) = styles.popup.offset {
228 self.popup_offset = Some(offset);
229 }
230 self
231 }
232
233 #[inline]
239 pub fn into_widgets(self) -> (MenubarLine<'a>, MenubarPopup<'a>) {
240 (
241 MenubarLine {
242 structure: self.structure,
243 menu: self.menu,
244 },
245 MenubarPopup {
246 structure: self.structure,
247 popup_alignment: self.popup_alignment,
248 popup_placement: self.popup_placement,
249 popup_offset: self.popup_offset,
250 popup: self.popup,
251 },
252 )
253 }
254}
255
256impl<'a> StatefulWidget for Menubar<'a> {
257 type State = MenubarState;
258
259 fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
260 let (menu, popup) = self.into_widgets();
261 menu.render(area, buf, state);
262 popup.render(Rect::default(), buf, state);
263 state.relocate_popup = false;
266 }
267}
268
269impl StatefulWidget for MenubarLine<'_> {
270 type State = MenubarState;
271
272 fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
273 render_menubar(self, area, buf, state);
274 }
275}
276
277fn render_menubar(
278 mut widget: MenubarLine<'_>,
279 area: Rect,
280 buf: &mut Buffer,
281 state: &mut MenubarState,
282) {
283 if let Some(structure) = &widget.structure {
284 structure.menus(&mut widget.menu.menu);
285 }
286 widget.menu.render(area, buf, &mut state.bar);
287 state.area = state.bar.area;
289 state.relocate_popup = true;
290}
291
292impl StatefulWidget for MenubarPopup<'_> {
293 type State = MenubarState;
294
295 fn render(self, _area: Rect, buf: &mut Buffer, state: &mut Self::State) {
296 render_menu_popup(self, buf, state);
297 }
298}
299
300fn render_menu_popup(mut widget: MenubarPopup<'_>, buf: &mut Buffer, state: &mut MenubarState) {
301 let Some(selected) = state.bar.selected() else {
302 return;
303 };
304 let Some(structure) = widget.structure else {
305 return;
306 };
307
308 if state.popup.is_active() {
309 let item = state.bar.item_areas[selected];
310
311 let popup_padding = widget.popup.get_block_padding();
312 let sub_offset = if let Some(offset) = widget.popup_offset {
313 offset
314 } else {
315 (-(popup_padding.left as i16 + 1), 0)
316 };
317
318 widget.popup = widget
319 .popup
320 .constraint(
321 widget
322 .popup_placement
323 .into_constraint(widget.popup_alignment, item),
324 )
325 .offset(sub_offset);
326 structure.submenu(selected, &mut widget.popup.menu);
327
328 if !widget.popup.menu.items.is_empty() {
329 let area = state.bar.item_areas[selected];
330 widget.popup.render(area, buf, &mut state.popup);
331 }
332 } else {
333 state.popup = Default::default();
334 }
335}
336
337impl MenubarState {
338 pub fn new() -> Self {
341 Self::default()
342 }
343
344 pub fn named(name: &'static str) -> Self {
346 Self {
347 bar: MenuLineState::named(format!("{}.bar", name).to_string().leak()),
348 popup: PopupMenuState::new(),
349 ..Default::default()
350 }
351 }
352
353 pub fn popup_active(&self) -> bool {
355 self.popup.is_active()
356 }
357
358 pub fn set_popup_active(&mut self, active: bool) {
360 self.popup.set_active(active);
361 }
362
363 pub fn set_popup_z(&mut self, z: u16) {
368 self.popup.set_popup_z(z)
369 }
370
371 pub fn popup_z(&self) -> u16 {
373 self.popup.popup_z()
374 }
375
376 pub fn selected(&self) -> (Option<usize>, Option<usize>) {
378 (self.bar.selected, self.popup.selected)
379 }
380}
381
382impl Default for MenubarState {
383 fn default() -> Self {
384 let mut z = Self {
385 area: Default::default(),
386 bar: MenuLineState::new(),
387 popup: PopupMenuState::new(),
388 relocate_popup: Default::default(),
389 non_exhaustive: NonExhaustive,
390 };
391 z.popup.focus = z.bar.focus.clone();
392 z
393 }
394}
395
396impl HasFocus for MenubarState {
397 fn build(&self, builder: &mut FocusBuilder) {
398 MenubarState::build_nav(&self, self.navigable(), builder);
399 }
400
401 fn build_nav(&self, navigable: Navigation, builder: &mut FocusBuilder) {
402 builder.leaf_with_flags(self.focus(), self.area(), self.area_z(), navigable);
403 builder.leaf_with_flags(
404 self.focus(),
405 self.popup.popup.area,
406 self.popup.popup.area_z,
407 Navigation::Mouse,
408 )
409 }
410
411 fn focus(&self) -> FocusFlag {
412 self.bar.focus.clone()
413 }
414
415 fn area(&self) -> Rect {
416 self.area
417 }
418}
419
420impl HasScreenCursor for MenubarState {
421 fn screen_cursor(&self) -> Option<(u16, u16)> {
422 None
423 }
424}
425
426impl RelocatableState for MenubarState {
427 fn relocate(&mut self, shift: (i16, i16), clip: Rect) {
428 if !self.relocate_popup {
429 self.area.relocate(shift, clip);
430 self.bar.relocate(shift, clip);
431 self.popup.relocate(shift, clip);
432 self.popup.relocate_popup(shift, clip);
433 }
434 }
435
436 fn relocate_popup(&mut self, shift: (i16, i16), clip: Rect) {
437 if self.relocate_popup {
438 self.relocate_popup = false;
439 self.area.relocate(shift, clip);
440 self.bar.relocate(shift, clip);
441 self.popup.relocate(shift, clip);
442 self.popup.relocate_popup(shift, clip);
443 }
444 }
445}
446
447impl HandleEvent<Event, Regular, MenuOutcome> for MenubarState {
448 fn handle(&mut self, event: &Event, _qualifier: Regular) -> MenuOutcome {
449 handle_menubar(self, event, Regular, Regular)
450 }
451}
452
453impl HandleEvent<Event, Popup, MenuOutcome> for MenubarState {
454 fn handle(&mut self, event: &Event, _qualifier: Popup) -> MenuOutcome {
455 handle_menubar(self, event, Popup, Regular)
456 }
457}
458
459impl HandleEvent<Event, MouseOnly, MenuOutcome> for MenubarState {
460 fn handle(&mut self, event: &Event, _qualifier: MouseOnly) -> MenuOutcome {
461 handle_menubar(self, event, MouseOnly, MouseOnly)
462 }
463}
464
465fn handle_menubar<Q1, Q2>(
466 state: &mut MenubarState,
467 event: &Event,
468 qualifier1: Q1,
469 qualifier2: Q2,
470) -> MenuOutcome
471where
472 PopupMenuState: HandleEvent<Event, Q1, MenuOutcome>,
473 MenuLineState: HandleEvent<Event, Q2, MenuOutcome>,
474 MenuLineState: HandleEvent<Event, MouseOnly, MenuOutcome>,
475{
476 if !state.is_focused() {
477 state.set_popup_active(false);
478 }
479
480 if state.bar.is_focused() {
481 let mut r = if let Some(selected) = state.bar.selected() {
482 if state.popup_active() {
483 match state.popup.handle(event, qualifier1) {
484 MenuOutcome::Hide => {
485 MenuOutcome::Continue
487 }
488 MenuOutcome::Selected(n) => MenuOutcome::MenuSelected(selected, n),
489 MenuOutcome::Activated(n) => MenuOutcome::MenuActivated(selected, n),
490 r => r,
491 }
492 } else {
493 MenuOutcome::Continue
494 }
495 } else {
496 MenuOutcome::Continue
497 };
498
499 r = r.or_else(|| {
500 let old_selected = state.bar.selected();
501 let r = state.bar.handle(event, qualifier2);
502 match r {
503 MenuOutcome::Selected(_) => {
504 if state.bar.selected == old_selected {
505 state.popup.flip_active();
506 } else {
507 state.popup.select(None);
508 state.popup.set_active(true);
509 }
510 }
511 MenuOutcome::Activated(_) => {
512 state.popup.flip_active();
513 }
514 _ => {}
515 }
516 r
517 });
518
519 r
520 } else {
521 state.bar.handle(event, MouseOnly)
522 }
523}
524
525pub fn handle_events(state: &mut MenubarState, focus: bool, event: &Event) -> MenuOutcome {
532 state.bar.focus.set(focus);
533 state.handle(event, Popup)
534}
535
536pub fn handle_popup_events(state: &mut MenubarState, focus: bool, event: &Event) -> MenuOutcome {
543 state.bar.focus.set(focus);
544 state.handle(event, Popup)
545}
546
547pub fn handle_mouse_events(state: &mut MenubarState, event: &Event) -> MenuOutcome {
549 state.handle(event, MouseOnly)
550}