rat_menu/
menubar.rs

1//! This widget will render a menubar and one level of menus.
2//!
3//! It is not a Widget itself, instead it will [split into](Menubar::into_widgets)
4//! a [MenubarLine] and a [MenubarPopup] widget that can be rendered.
5//! The MenubarLine can be rendered in its designated area anytime.
6//! The MenubarPopup must be rendered at the end of rendering,
7//! for it to be able to render above the other widgets.
8//!
9//! Event handling for the menubar must happen before handling
10//! events for the widgets that might be rendered in the background.
11//! Otherwise, mouse navigation will not work correctly.
12//!
13//! The structure of the menubar is defined with the trait
14//! [MenuStructure], and there is [StaticMenu](crate::StaticMenu)
15//! which can define the structure as static data.
16//!
17//! [Example](https://github.com/thscharler/rat-salsa/blob/master/rat-widget/examples/menubar1.rs)
18//!
19#![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/// Menubar widget.
40///
41/// This handles the configuration only, to get the widgets for rendering
42/// call [Menubar::into_widgets] and use both results for rendering.
43#[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/// Menubar line widget.
56///
57/// This will render the main menu bar.
58#[derive(Debug, Clone)]
59pub struct MenubarLine<'a> {
60    structure: Option<&'a dyn MenuStructure<'a>>,
61    menu: MenuLine<'a>,
62}
63
64/// Menubar popup widget.
65///
66/// Separate renderer for the popup part of the menu bar.
67#[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/// State & event-handling.
77#[derive(Debug, Clone)]
78pub struct MenubarState {
79    /// Area for the main-menubar.
80    /// __readonly__. renewed for each render.
81    pub area: Rect,
82    /// State for the menu.
83    pub bar: MenuLineState,
84    /// State for the last rendered popup menu.
85    pub popup: PopupMenuState,
86
87    /// Rendering is split into base-widget and menu-popup.
88    /// Relocate after rendering the popup.
89    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    /// Base style.
117    #[inline]
118    pub fn style(mut self, style: Style) -> Self {
119        self.menu = self.menu.style(style);
120        self
121    }
122
123    /// Block.
124    #[inline]
125    pub fn block(mut self, block: Block<'a>) -> Self {
126        self.menu = self.menu.block(block);
127        self
128    }
129
130    /// Title text.
131    #[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    /// Menu-title style.
138    #[inline]
139    pub fn title_style(mut self, style: Style) -> Self {
140        self.menu = self.menu.title_style(style);
141        self
142    }
143
144    /// Selection + Focus
145    #[inline]
146    pub fn focus_style(mut self, style: Style) -> Self {
147        self.menu = self.menu.focus_style(style);
148        self
149    }
150
151    /// Selection + Focus
152    #[inline]
153    pub fn right_style(mut self, style: Style) -> Self {
154        self.menu = self.menu.right_style(style);
155        self
156    }
157
158    /// Fixed width for the menu.
159    /// If not set it uses 1.5 times the length of the longest item.
160    #[inline]
161    pub fn popup_width(mut self, width: u16) -> Self {
162        self.popup = self.popup.menu_width(width);
163        self
164    }
165
166    /// Placement relative to the render-area.
167    #[inline]
168    pub fn popup_alignment(mut self, alignment: Alignment) -> Self {
169        self.popup_alignment = alignment;
170        self
171    }
172
173    /// Placement relative to the render-area.
174    #[inline]
175    pub fn popup_offset(mut self, offset: (i16, i16)) -> Self {
176        self.popup_offset = Some(offset);
177        self
178    }
179
180    /// Placement relative to the render-area.
181    #[inline]
182    pub fn popup_placement(mut self, placement: Placement) -> Self {
183        self.popup_placement = placement;
184        self
185    }
186
187    /// Base style for the popup-menu.
188    #[inline]
189    pub fn popup_style(mut self, style: Style) -> Self {
190        self.popup = self.popup.style(style);
191        self
192    }
193
194    /// Block for the popup-menu.
195    #[inline]
196    pub fn popup_block(mut self, block: Block<'a>) -> Self {
197        self.popup = self.popup.block(block);
198        self
199    }
200
201    /// Selection + Focus for the popup-menu.
202    #[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    /// Hotkey for the popup-menu.
209    #[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    /// Combined style.
216    #[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    /// Create the widgets for the Menubar. This returns a widget
234    /// for the menu-line and for the menu-popup.
235    ///
236    /// The menu-popup should be rendered after all widgets
237    /// that might be below the popup have been rendered.
238    #[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        // direct rendering of menubar + popup.
264        // relocate() will be called, not relocate_popup()
265        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    // Area of the main bar.
288    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    /// State.
339    /// For the specifics use the public fields `menu` and `popup`.
340    pub fn new() -> Self {
341        Self::default()
342    }
343
344    /// New state with a focus name.
345    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    /// Submenu visible/active.
354    pub fn popup_active(&self) -> bool {
355        self.popup.is_active()
356    }
357
358    /// Submenu visible/active.
359    pub fn set_popup_active(&mut self, active: bool) {
360        self.popup.set_active(active);
361    }
362
363    /// Set the z-value for the popup-menu.
364    ///
365    /// This is the z-index used when adding the menubar to
366    /// the focus list.
367    pub fn set_popup_z(&mut self, z: u16) {
368        self.popup.set_popup_z(z)
369    }
370
371    /// The z-index for the popup-menu.
372    pub fn popup_z(&self) -> u16 {
373        self.popup.popup_z()
374    }
375
376    /// Selected as menu/submenu
377    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        Self {
385            area: Default::default(),
386            bar: Default::default(),
387            popup: Default::default(),
388            relocate_popup: Default::default(),
389            non_exhaustive: NonExhaustive,
390        }
391    }
392}
393
394impl HasFocus for MenubarState {
395    fn build(&self, builder: &mut FocusBuilder) {
396        builder.widget_with_flags(self.focus(), self.area(), self.area_z(), self.navigable());
397        builder.widget_with_flags(
398            self.focus(),
399            self.popup.popup.area,
400            self.popup.popup.area_z,
401            Navigation::Mouse,
402        );
403    }
404
405    fn focus(&self) -> FocusFlag {
406        self.bar.focus.clone()
407    }
408
409    fn area(&self) -> Rect {
410        self.area
411    }
412}
413
414impl HasScreenCursor for MenubarState {
415    fn screen_cursor(&self) -> Option<(u16, u16)> {
416        None
417    }
418}
419
420impl RelocatableState for MenubarState {
421    fn relocate(&mut self, shift: (i16, i16), clip: Rect) {
422        if !self.relocate_popup {
423            self.area.relocate(shift, clip);
424            self.bar.relocate(shift, clip);
425            self.popup.relocate(shift, clip);
426            self.popup.relocate_popup(shift, clip);
427        }
428    }
429
430    fn relocate_popup(&mut self, shift: (i16, i16), clip: Rect) {
431        if self.relocate_popup {
432            self.relocate_popup = false;
433            self.area.relocate(shift, clip);
434            self.bar.relocate(shift, clip);
435            self.popup.relocate(shift, clip);
436            self.popup.relocate_popup(shift, clip);
437        }
438    }
439}
440
441impl HandleEvent<Event, Popup, MenuOutcome> for MenubarState {
442    fn handle(&mut self, event: &Event, _qualifier: Popup) -> MenuOutcome {
443        handle_menubar(self, event, Popup, Regular)
444    }
445}
446
447impl HandleEvent<Event, MouseOnly, MenuOutcome> for MenubarState {
448    fn handle(&mut self, event: &Event, _qualifier: MouseOnly) -> MenuOutcome {
449        handle_menubar(self, event, MouseOnly, MouseOnly)
450    }
451}
452
453fn handle_menubar<Q1, Q2>(
454    state: &mut MenubarState,
455    event: &Event,
456    qualifier1: Q1,
457    qualifier2: Q2,
458) -> MenuOutcome
459where
460    PopupMenuState: HandleEvent<Event, Q1, MenuOutcome>,
461    MenuLineState: HandleEvent<Event, Q2, MenuOutcome>,
462    MenuLineState: HandleEvent<Event, MouseOnly, MenuOutcome>,
463{
464    if !state.is_focused() {
465        state.set_popup_active(false);
466    }
467
468    if state.bar.is_focused() {
469        let mut r = if let Some(selected) = state.bar.selected() {
470            if state.popup_active() {
471                match state.popup.handle(event, qualifier1) {
472                    MenuOutcome::Hide => {
473                        // only hide on focus lost. ignore this one.
474                        MenuOutcome::Continue
475                    }
476                    MenuOutcome::Selected(n) => MenuOutcome::MenuSelected(selected, n),
477                    MenuOutcome::Activated(n) => MenuOutcome::MenuActivated(selected, n),
478                    r => r,
479                }
480            } else {
481                MenuOutcome::Continue
482            }
483        } else {
484            MenuOutcome::Continue
485        };
486
487        r = r.or_else(|| {
488            let old_selected = state.bar.selected();
489            let r = state.bar.handle(event, qualifier2);
490            match r {
491                MenuOutcome::Selected(_) => {
492                    if state.bar.selected == old_selected {
493                        state.popup.flip_active();
494                    } else {
495                        state.popup.select(None);
496                        state.popup.set_active(true);
497                    }
498                }
499                MenuOutcome::Activated(_) => {
500                    state.popup.flip_active();
501                }
502                _ => {}
503            }
504            r
505        });
506
507        r
508    } else {
509        state.bar.handle(event, MouseOnly)
510    }
511}
512
513/// Handle menu events for the popup-menu.
514///
515/// This one is separate, as it needs to be called before other event-handlers
516/// to cope with overlapping regions.
517///
518/// focus - is the menubar focused?
519pub fn handle_events(state: &mut MenubarState, focus: bool, event: &Event) -> MenuOutcome {
520    state.bar.focus.set(focus);
521    state.handle(event, Popup)
522}
523
524/// Handle menu events for the popup-menu.
525///
526/// This one is separate, as it needs to be called before other event-handlers
527/// to cope with overlapping regions.
528///
529/// focus - is the menubar focused?
530pub fn handle_popup_events(state: &mut MenubarState, focus: bool, event: &Event) -> MenuOutcome {
531    state.bar.focus.set(focus);
532    state.handle(event, Popup)
533}
534
535/// Handle only mouse-events.
536pub fn handle_mouse_events(state: &mut MenubarState, event: &Event) -> MenuOutcome {
537    state.handle(event, MouseOnly)
538}