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::buffer::Buffer;
31use ratatui::layout::{Alignment, Rect};
32use ratatui::style::Style;
33use ratatui::text::Line;
34use ratatui::widgets::{Block, StatefulWidget};
35use std::fmt::Debug;
36
37/// Menubar widget.
38///
39/// This handles the configuration only, to get the widgets for rendering
40/// call [Menubar::into_widgets] and use both results for rendering.
41#[derive(Debug, Clone)]
42pub struct Menubar<'a> {
43    structure: Option<&'a dyn MenuStructure<'a>>,
44
45    menu: MenuLine<'a>,
46
47    popup_alignment: Alignment,
48    popup_placement: Placement,
49    popup_offset: Option<(i16, i16)>,
50    popup: PopupMenu<'a>,
51}
52
53/// Menubar line widget.
54///
55/// This will render the main menu bar.
56#[derive(Debug, Clone)]
57pub struct MenubarLine<'a> {
58    structure: Option<&'a dyn MenuStructure<'a>>,
59    menu: MenuLine<'a>,
60}
61
62/// Menubar popup widget.
63///
64/// Separate renderer for the popup part of the menu bar.
65#[derive(Debug, Clone)]
66pub struct MenubarPopup<'a> {
67    structure: Option<&'a dyn MenuStructure<'a>>,
68    popup_alignment: Alignment,
69    popup_placement: Placement,
70    popup_offset: Option<(i16, i16)>,
71    popup: PopupMenu<'a>,
72}
73
74/// State & event-handling.
75#[derive(Debug, Clone)]
76pub struct MenubarState {
77    /// Area for the main-menubar.
78    /// __readonly__. renewed for each render.
79    pub area: Rect,
80    /// State for the menu.
81    pub bar: MenuLineState,
82    /// State for the last rendered popup menu.
83    pub popup: PopupMenuState,
84
85    /// Rendering is split into base-widget and menu-popup.
86    /// Relocate after rendering the popup.
87    relocate_popup: bool,
88
89    pub non_exhaustive: NonExhaustive,
90}
91
92impl Default for Menubar<'_> {
93    fn default() -> Self {
94        Self {
95            structure: Default::default(),
96            menu: Default::default(),
97            popup_alignment: Alignment::Left,
98            popup_placement: Placement::AboveOrBelow,
99            popup_offset: Default::default(),
100            popup: Default::default(),
101        }
102    }
103}
104
105impl<'a> Menubar<'a> {
106    #[inline]
107    pub fn new(structure: &'a dyn MenuStructure<'a>) -> Self {
108        Self {
109            structure: Some(structure),
110            ..Default::default()
111        }
112    }
113
114    /// Base style.
115    #[inline]
116    pub fn style(mut self, style: Style) -> Self {
117        self.menu = self.menu.style(style);
118        self
119    }
120
121    /// Block.
122    #[inline]
123    pub fn block(mut self, block: Block<'a>) -> Self {
124        self.menu = self.menu.block(block);
125        self
126    }
127
128    /// Title text.
129    #[inline]
130    pub fn title(mut self, title: impl Into<Line<'a>>) -> Self {
131        self.menu = self.menu.title(title);
132        self
133    }
134
135    /// Menu-title style.
136    #[inline]
137    pub fn title_style(mut self, style: Style) -> Self {
138        self.menu = self.menu.title_style(style);
139        self
140    }
141
142    /// Selection + Focus
143    #[inline]
144    pub fn focus_style(mut self, style: Style) -> Self {
145        self.menu = self.menu.focus_style(style);
146        self
147    }
148
149    /// Selection + Focus
150    #[inline]
151    pub fn right_style(mut self, style: Style) -> Self {
152        self.menu = self.menu.right_style(style);
153        self
154    }
155
156    /// Fixed width for the menu.
157    /// If not set it uses 1.5 times the length of the longest item.
158    #[inline]
159    pub fn popup_width(mut self, width: u16) -> Self {
160        self.popup = self.popup.menu_width(width);
161        self
162    }
163
164    /// Placement relative to the render-area.
165    #[inline]
166    pub fn popup_alignment(mut self, alignment: Alignment) -> Self {
167        self.popup_alignment = alignment;
168        self
169    }
170
171    /// Placement relative to the render-area.
172    #[inline]
173    pub fn popup_offset(mut self, offset: (i16, i16)) -> Self {
174        self.popup_offset = Some(offset);
175        self
176    }
177
178    /// Placement relative to the render-area.
179    #[inline]
180    pub fn popup_placement(mut self, placement: Placement) -> Self {
181        self.popup_placement = placement;
182        self
183    }
184
185    /// Base style for the popup-menu.
186    #[inline]
187    pub fn popup_style(mut self, style: Style) -> Self {
188        self.popup = self.popup.style(style);
189        self
190    }
191
192    /// Block for the popup-menu.
193    #[inline]
194    pub fn popup_block(mut self, block: Block<'a>) -> Self {
195        self.popup = self.popup.block(block);
196        self
197    }
198
199    /// Selection + Focus for the popup-menu.
200    #[inline]
201    pub fn popup_focus_style(mut self, style: Style) -> Self {
202        self.popup = self.popup.focus_style(style);
203        self
204    }
205
206    /// Hotkey for the popup-menu.
207    #[inline]
208    pub fn popup_right_style(mut self, style: Style) -> Self {
209        self.popup = self.popup.right_style(style);
210        self
211    }
212
213    /// Combined style.
214    #[inline]
215    pub fn styles(mut self, styles: MenuStyle) -> Self {
216        self.menu = self.menu.styles(styles.clone());
217        self.popup = self.popup.styles(styles.clone());
218
219        if let Some(alignment) = styles.popup.alignment {
220            self.popup_alignment = alignment;
221        }
222        if let Some(placement) = styles.popup.placement {
223            self.popup_placement = placement;
224        }
225        if let Some(offset) = styles.popup.offset {
226            self.popup_offset = Some(offset);
227        }
228        self
229    }
230
231    /// Create the widgets for the Menubar. This returns a widget
232    /// for the menu-line and for the menu-popup.
233    ///
234    /// The menu-popup should be rendered after all widgets
235    /// that might be below the popup have been rendered.
236    #[inline]
237    pub fn into_widgets(self) -> (MenubarLine<'a>, MenubarPopup<'a>) {
238        (
239            MenubarLine {
240                structure: self.structure,
241                menu: self.menu,
242            },
243            MenubarPopup {
244                structure: self.structure,
245                popup_alignment: self.popup_alignment,
246                popup_placement: self.popup_placement,
247                popup_offset: self.popup_offset,
248                popup: self.popup,
249            },
250        )
251    }
252}
253
254impl<'a> StatefulWidget for Menubar<'a> {
255    type State = MenubarState;
256
257    fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
258        let (menu, popup) = self.into_widgets();
259        menu.render(area, buf, state);
260        popup.render(Rect::default(), buf, state);
261        // direct rendering of menubar + popup.
262        // relocate() will be called, not relocate_popup()
263        state.relocate_popup = false;
264    }
265}
266
267impl StatefulWidget for MenubarLine<'_> {
268    type State = MenubarState;
269
270    fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
271        render_menubar(self, area, buf, state);
272    }
273}
274
275fn render_menubar(
276    mut widget: MenubarLine<'_>,
277    area: Rect,
278    buf: &mut Buffer,
279    state: &mut MenubarState,
280) {
281    if let Some(structure) = &widget.structure {
282        structure.menus(&mut widget.menu.menu);
283    }
284    widget.menu.render(area, buf, &mut state.bar);
285    // Area of the main bar.
286    state.area = state.bar.area;
287    state.relocate_popup = true;
288}
289
290impl StatefulWidget for MenubarPopup<'_> {
291    type State = MenubarState;
292
293    fn render(self, _area: Rect, buf: &mut Buffer, state: &mut Self::State) {
294        render_menu_popup(self, buf, state);
295    }
296}
297
298fn render_menu_popup(mut widget: MenubarPopup<'_>, buf: &mut Buffer, state: &mut MenubarState) {
299    let Some(selected) = state.bar.selected() else {
300        return;
301    };
302    let Some(structure) = widget.structure else {
303        return;
304    };
305
306    if state.popup.is_active() {
307        let item = state.bar.item_areas[selected];
308
309        let popup_padding = widget.popup.get_block_padding();
310        let sub_offset = if let Some(offset) = widget.popup_offset {
311            offset
312        } else {
313            (-(popup_padding.left as i16 + 1), 0)
314        };
315
316        widget.popup = widget
317            .popup
318            .constraint(
319                widget
320                    .popup_placement
321                    .into_constraint(widget.popup_alignment, item),
322            )
323            .offset(sub_offset);
324        structure.submenu(selected, &mut widget.popup.menu);
325
326        if !widget.popup.menu.items.is_empty() {
327            let area = state.bar.item_areas[selected];
328            widget.popup.render(area, buf, &mut state.popup);
329        }
330    } else {
331        state.popup = Default::default();
332    }
333}
334
335impl MenubarState {
336    /// State.
337    /// For the specifics use the public fields `menu` and `popup`.
338    pub fn new() -> Self {
339        Self::default()
340    }
341
342    /// New state with a focus name.
343    pub fn named(name: &'static str) -> Self {
344        Self {
345            bar: MenuLineState::named(format!("{}.bar", name).to_string().leak()),
346            popup: PopupMenuState::new(),
347            ..Default::default()
348        }
349    }
350
351    /// Submenu visible/active.
352    pub fn popup_active(&self) -> bool {
353        self.popup.is_active()
354    }
355
356    /// Submenu visible/active.
357    pub fn set_popup_active(&mut self, active: bool) {
358        self.popup.set_active(active);
359    }
360
361    /// Set the z-value for the popup-menu.
362    ///
363    /// This is the z-index used when adding the menubar to
364    /// the focus list.
365    pub fn set_popup_z(&mut self, z: u16) {
366        self.popup.set_popup_z(z)
367    }
368
369    /// The z-index for the popup-menu.
370    pub fn popup_z(&self) -> u16 {
371        self.popup.popup_z()
372    }
373
374    /// Selected as menu/submenu
375    pub fn selected(&self) -> (Option<usize>, Option<usize>) {
376        (self.bar.selected, self.popup.selected)
377    }
378}
379
380impl Default for MenubarState {
381    fn default() -> Self {
382        Self {
383            area: Default::default(),
384            bar: Default::default(),
385            popup: Default::default(),
386            relocate_popup: Default::default(),
387            non_exhaustive: NonExhaustive,
388        }
389    }
390}
391
392impl HasFocus for MenubarState {
393    fn build(&self, builder: &mut FocusBuilder) {
394        builder.widget_with_flags(self.focus(), self.area(), self.area_z(), self.navigable());
395        builder.widget_with_flags(
396            self.focus(),
397            self.popup.popup.area,
398            self.popup.popup.area_z,
399            Navigation::Mouse,
400        );
401    }
402
403    fn focus(&self) -> FocusFlag {
404        self.bar.focus.clone()
405    }
406
407    fn area(&self) -> Rect {
408        self.area
409    }
410}
411
412impl HasScreenCursor for MenubarState {
413    fn screen_cursor(&self) -> Option<(u16, u16)> {
414        None
415    }
416}
417
418impl RelocatableState for MenubarState {
419    fn relocate(&mut self, shift: (i16, i16), clip: Rect) {
420        if !self.relocate_popup {
421            self.area.relocate(shift, clip);
422            self.bar.relocate(shift, clip);
423            self.popup.relocate(shift, clip);
424            self.popup.relocate_popup(shift, clip);
425        }
426    }
427
428    fn relocate_popup(&mut self, shift: (i16, i16), clip: Rect) {
429        if self.relocate_popup {
430            self.relocate_popup = false;
431            self.area.relocate(shift, clip);
432            self.bar.relocate(shift, clip);
433            self.popup.relocate(shift, clip);
434            self.popup.relocate_popup(shift, clip);
435        }
436    }
437}
438
439impl HandleEvent<crossterm::event::Event, Popup, MenuOutcome> for MenubarState {
440    fn handle(&mut self, event: &crossterm::event::Event, _qualifier: Popup) -> MenuOutcome {
441        handle_menubar(self, event, Popup, Regular)
442    }
443}
444
445impl HandleEvent<crossterm::event::Event, MouseOnly, MenuOutcome> for MenubarState {
446    fn handle(&mut self, event: &crossterm::event::Event, _qualifier: MouseOnly) -> MenuOutcome {
447        handle_menubar(self, event, MouseOnly, MouseOnly)
448    }
449}
450
451fn handle_menubar<Q1, Q2>(
452    state: &mut MenubarState,
453    event: &crossterm::event::Event,
454    qualifier1: Q1,
455    qualifier2: Q2,
456) -> MenuOutcome
457where
458    PopupMenuState: HandleEvent<crossterm::event::Event, Q1, MenuOutcome>,
459    MenuLineState: HandleEvent<crossterm::event::Event, Q2, MenuOutcome>,
460    MenuLineState: HandleEvent<crossterm::event::Event, MouseOnly, MenuOutcome>,
461{
462    if !state.is_focused() {
463        state.set_popup_active(false);
464    }
465
466    if state.bar.is_focused() {
467        let mut r = if let Some(selected) = state.bar.selected() {
468            if state.popup_active() {
469                match state.popup.handle(event, qualifier1) {
470                    MenuOutcome::Hide => {
471                        // only hide on focus lost. ignore this one.
472                        MenuOutcome::Continue
473                    }
474                    MenuOutcome::Selected(n) => MenuOutcome::MenuSelected(selected, n),
475                    MenuOutcome::Activated(n) => MenuOutcome::MenuActivated(selected, n),
476                    r => r,
477                }
478            } else {
479                MenuOutcome::Continue
480            }
481        } else {
482            MenuOutcome::Continue
483        };
484
485        r = r.or_else(|| {
486            let old_selected = state.bar.selected();
487            let r = state.bar.handle(event, qualifier2);
488            match r {
489                MenuOutcome::Selected(_) => {
490                    if state.bar.selected == old_selected {
491                        state.popup.flip_active();
492                    } else {
493                        state.popup.select(None);
494                        state.popup.set_active(true);
495                    }
496                }
497                MenuOutcome::Activated(_) => {
498                    state.popup.flip_active();
499                }
500                _ => {}
501            }
502            r
503        });
504
505        r
506    } else {
507        state.bar.handle(event, MouseOnly)
508    }
509}
510
511/// Handle menu events for the popup-menu.
512///
513/// This one is separate, as it needs to be called before other event-handlers
514/// to cope with overlapping regions.
515///
516/// focus - is the menubar focused?
517pub fn handle_events(
518    state: &mut MenubarState,
519    focus: bool,
520    event: &crossterm::event::Event,
521) -> MenuOutcome {
522    state.bar.focus.set(focus);
523    state.handle(event, Popup)
524}
525
526/// Handle menu events for the popup-menu.
527///
528/// This one is separate, as it needs to be called before other event-handlers
529/// to cope with overlapping regions.
530///
531/// focus - is the menubar focused?
532pub fn handle_popup_events(
533    state: &mut MenubarState,
534    focus: bool,
535    event: &crossterm::event::Event,
536) -> MenuOutcome {
537    state.bar.focus.set(focus);
538    state.handle(event, Popup)
539}
540
541/// Handle only mouse-events.
542pub fn handle_mouse_events(
543    state: &mut MenubarState,
544    event: &crossterm::event::Event,
545) -> MenuOutcome {
546    state.handle(event, MouseOnly)
547}