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