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