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