rat_menu/
menubar.rs

1//!
2//! A menubar with sub-menus.
3//!
4//! Combines [MenuLine] and [PopupMenu] and adds a [MenuStructure] trait
5//! to bind all together.
6//!
7//! Rendering and events are split in base-widget and popup.
8//! Use [Menubar] to set all configurations and then call [Menubar::into_widgets].
9//! This creates a [MenubarLine] and a [MenubarPopup] which implement
10//! the rendering traits. MenubarPopup must render *after* all regular
11//! widgets, MenubarLine can render whenever.
12//!
13//! Event-handling for the popup menu works with the [Popup] qualifier,
14//! and must be called before the [Regular] event-handlers to work correctly.
15//! Event-handling for the menu line is via the [Regular] event-handler.
16//!
17use crate::event::MenuOutcome;
18use crate::menuline::{MenuLine, MenuLineState};
19use crate::popup_menu::{PopupMenu, PopupMenuState};
20use crate::{MenuStructure, MenuStyle};
21use rat_event::{ConsumedEvent, HandleEvent, MouseOnly, Popup, Regular};
22use rat_focus::{FocusBuilder, FocusFlag, HasFocus, Navigation};
23use rat_popup::Placement;
24use ratatui::buffer::Buffer;
25use ratatui::layout::{Alignment, Rect};
26use ratatui::style::Style;
27use ratatui::text::Line;
28use ratatui::widgets::{Block, StatefulWidget};
29use std::fmt::Debug;
30
31/// Menubar widget.
32/// This handles the configuration only, to get the widgets for rendering
33/// call [Menubar::into_widgets] and use both results for rendering.
34#[derive(Debug, Clone)]
35pub struct Menubar<'a> {
36    structure: Option<&'a dyn MenuStructure<'a>>,
37
38    title: Line<'a>,
39    style: Style,
40    title_style: Option<Style>,
41    select_style: Option<Style>,
42    focus_style: Option<Style>,
43    highlight_style: Option<Style>,
44    disabled_style: Option<Style>,
45    right_style: Option<Style>,
46
47    popup_alignment: Alignment,
48    popup_placement: Placement,
49    popup: PopupMenu<'a>,
50}
51
52/// Menubar line widget.
53/// This implements the actual render function.
54#[derive(Debug, Clone)]
55pub struct MenubarLine<'a> {
56    structure: Option<&'a dyn MenuStructure<'a>>,
57
58    title: Line<'a>,
59    style: Style,
60    title_style: Option<Style>,
61    select_style: Option<Style>,
62    focus_style: Option<Style>,
63    highlight_style: Option<Style>,
64    disabled_style: Option<Style>,
65    right_style: Option<Style>,
66}
67
68/// Menubar popup widget.
69/// Separate renderer for the popup part of the menubar.
70#[derive(Debug, Clone)]
71pub struct MenubarPopup<'a> {
72    structure: Option<&'a dyn MenuStructure<'a>>,
73
74    style: Style,
75    focus_style: Option<Style>,
76    highlight_style: Option<Style>,
77    disabled_style: Option<Style>,
78    right_style: Option<Style>,
79
80    popup_alignment: Alignment,
81    popup_placement: Placement,
82    popup: PopupMenu<'a>,
83}
84
85/// State & event-handling.
86#[derive(Debug, Default, Clone)]
87pub struct MenubarState {
88    /// Area for the menubar.
89    /// __readonly__. renewed for each render.
90    pub area: Rect,
91    /// State for the menu.
92    pub bar: MenuLineState,
93    /// State for the last rendered popup menu.
94    pub popup: PopupMenuState,
95}
96
97impl Default for Menubar<'_> {
98    fn default() -> Self {
99        Self {
100            structure: None,
101            title: Default::default(),
102            style: Default::default(),
103            title_style: None,
104            select_style: None,
105            focus_style: None,
106            highlight_style: None,
107            disabled_style: None,
108            right_style: None,
109            popup_alignment: Alignment::Left,
110            popup_placement: Placement::AboveOrBelow,
111            popup: Default::default(),
112        }
113    }
114}
115
116impl<'a> Menubar<'a> {
117    pub fn new(structure: &'a dyn MenuStructure<'a>) -> Self {
118        Self {
119            structure: Some(structure),
120            ..Default::default()
121        }
122    }
123
124    /// Title text.
125    #[inline]
126    pub fn title(mut self, title: impl Into<Line<'a>>) -> Self {
127        self.title = title.into();
128        self
129    }
130
131    /// Combined style.
132    #[inline]
133    pub fn styles(mut self, styles: MenuStyle) -> Self {
134        self.popup = self.popup.styles(styles.clone());
135
136        self.style = styles.style;
137        if styles.highlight.is_some() {
138            self.highlight_style = styles.highlight;
139        }
140        if styles.disabled.is_some() {
141            self.disabled_style = styles.disabled;
142        }
143        if styles.focus.is_some() {
144            self.focus_style = styles.focus;
145        }
146        if styles.title.is_some() {
147            self.title_style = styles.title;
148        }
149        if styles.select.is_some() {
150            self.select_style = styles.select;
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    pub fn select_style(mut self, style: Style) -> Self {
184        self.select_style = Some(style);
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 borders.
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                select_style: self.select_style,
240                focus_style: self.focus_style,
241                highlight_style: self.highlight_style,
242                disabled_style: self.disabled_style,
243                right_style: self.right_style,
244            },
245            MenubarPopup {
246                structure: self.structure,
247                style: self.style,
248                focus_style: self.focus_style,
249                highlight_style: self.highlight_style,
250                disabled_style: self.disabled_style,
251                right_style: self.right_style,
252                popup_alignment: self.popup_alignment,
253                popup_placement: self.popup_placement,
254                popup: self.popup,
255            },
256        )
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    widget: &MenubarLine<'_>,
270    area: Rect,
271    buf: &mut Buffer,
272    state: &mut MenubarState,
273) {
274    let mut menu = MenuLine::new()
275        .title(widget.title.clone())
276        .style(widget.style)
277        .title_style_opt(widget.title_style)
278        .select_style_opt(widget.select_style)
279        .focus_style_opt(widget.focus_style)
280        .highlight_style_opt(widget.highlight_style)
281        .disabled_style_opt(widget.disabled_style)
282        .right_style_opt(widget.right_style);
283
284    if let Some(structure) = &widget.structure {
285        structure.menus(&mut menu.menu);
286    }
287    menu.render(area, buf, &mut state.bar);
288
289    // Combined area + each part with a z-index.
290    state.area = state.bar.area;
291}
292
293impl StatefulWidget for MenubarPopup<'_> {
294    type State = MenubarState;
295
296    fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
297        render_menu_popup(self, area, buf, state);
298    }
299}
300
301fn render_menu_popup(
302    widget: MenubarPopup<'_>,
303    _area: Rect,
304    buf: &mut Buffer,
305    state: &mut MenubarState,
306) {
307    // Combined area + each part with a z-index.
308    state.area = state.bar.area;
309
310    let Some(selected) = state.bar.selected() else {
311        return;
312    };
313    let Some(structure) = widget.structure else {
314        return;
315    };
316
317    if state.popup.is_active() {
318        let item = state.bar.item_areas[selected];
319
320        let popup_padding = widget.popup.get_block_padding();
321        let sub_offset = (-(popup_padding.left as i16 + 1), 0);
322
323        let mut popup = widget
324            .popup
325            .constraint(
326                widget
327                    .popup_placement
328                    .into_constraint(widget.popup_alignment, item),
329            )
330            .offset(sub_offset)
331            .style(widget.style)
332            .focus_style_opt(widget.focus_style)
333            .highlight_style_opt(widget.highlight_style)
334            .disabled_style_opt(widget.disabled_style)
335            .right_style_opt(widget.right_style);
336
337        structure.submenu(selected, &mut popup.menu);
338
339        if !popup.menu.items.is_empty() {
340            let area = state.bar.item_areas[selected];
341            popup.render(area, buf, &mut state.popup);
342
343            // Combined area + each part with a z-index.
344            state.area = state.bar.area.union(state.popup.popup.area);
345        }
346    } else {
347        state.popup = Default::default();
348    }
349}
350
351impl MenubarState {
352    /// State.
353    /// For the specifics use the public fields `menu` and `popup`.
354    pub fn new() -> Self {
355        Self::default()
356    }
357
358    /// New state with a focus name.
359    pub fn named(name: &'static str) -> Self {
360        Self {
361            bar: MenuLineState::named(format!("{}.bar", name).to_string().leak()),
362            popup: PopupMenuState::new(),
363            ..Default::default()
364        }
365    }
366
367    /// Submenu visible/active.
368    pub fn popup_active(&self) -> bool {
369        self.popup.is_active()
370    }
371
372    /// Submenu visible/active.
373    pub fn set_popup_active(&mut self, active: bool) {
374        self.popup.set_active(active);
375    }
376
377    /// Set the z-value for the popup-menu.
378    ///
379    /// This is the z-index used when adding the menubar to
380    /// the focus list.
381    pub fn set_popup_z(&mut self, z: u16) {
382        self.popup.set_popup_z(z)
383    }
384
385    /// The z-index for the popup-menu.
386    pub fn popup_z(&self) -> u16 {
387        self.popup.popup_z()
388    }
389
390    /// Selected as menu/submenu
391    pub fn selected(&self) -> (Option<usize>, Option<usize>) {
392        (self.bar.selected, self.popup.selected)
393    }
394}
395
396impl HasFocus for MenubarState {
397    fn build(&self, builder: &mut FocusBuilder) {
398        builder.widget_with_flags(self.focus(), self.area(), self.area_z(), self.navigable());
399        builder.widget_with_flags(
400            self.focus(),
401            self.popup.popup.area,
402            self.popup.popup.area_z,
403            Navigation::Mouse,
404        );
405    }
406
407    fn focus(&self) -> FocusFlag {
408        self.bar.focus.clone()
409    }
410
411    fn area(&self) -> Rect {
412        self.area
413    }
414}
415
416impl HandleEvent<crossterm::event::Event, Popup, MenuOutcome> for MenubarState {
417    fn handle(&mut self, event: &crossterm::event::Event, _qualifier: Popup) -> MenuOutcome {
418        handle_menubar(self, event, Popup, Regular)
419    }
420}
421
422impl HandleEvent<crossterm::event::Event, MouseOnly, MenuOutcome> for MenubarState {
423    fn handle(&mut self, event: &crossterm::event::Event, _qualifier: MouseOnly) -> MenuOutcome {
424        handle_menubar(self, event, MouseOnly, MouseOnly)
425    }
426}
427
428fn handle_menubar<Q1, Q2>(
429    state: &mut MenubarState,
430    event: &crossterm::event::Event,
431    qualifier1: Q1,
432    qualifier2: Q2,
433) -> MenuOutcome
434where
435    PopupMenuState: HandleEvent<crossterm::event::Event, Q1, MenuOutcome>,
436    MenuLineState: HandleEvent<crossterm::event::Event, Q2, MenuOutcome>,
437    MenuLineState: HandleEvent<crossterm::event::Event, MouseOnly, MenuOutcome>,
438{
439    if !state.is_focused() {
440        state.set_popup_active(false);
441    }
442
443    if state.bar.is_focused() {
444        let mut r = if let Some(selected) = state.bar.selected() {
445            if state.popup_active() {
446                match state.popup.handle(event, qualifier1) {
447                    MenuOutcome::Hide => {
448                        // only hide on focus lost. ignore this one.
449                        MenuOutcome::Continue
450                    }
451                    MenuOutcome::Selected(n) => MenuOutcome::MenuSelected(selected, n),
452                    MenuOutcome::Activated(n) => MenuOutcome::MenuActivated(selected, n),
453                    r => r,
454                }
455            } else {
456                MenuOutcome::Continue
457            }
458        } else {
459            MenuOutcome::Continue
460        };
461
462        r = r.or_else(|| {
463            let old_selected = state.bar.selected();
464            let r = state.bar.handle(event, qualifier2);
465            match r {
466                MenuOutcome::Selected(_) => {
467                    if state.bar.selected == old_selected {
468                        state.popup.flip_active();
469                    } else {
470                        state.popup.select(None);
471                        state.popup.set_active(true);
472                    }
473                }
474                MenuOutcome::Activated(_) => {
475                    state.popup.flip_active();
476                }
477                _ => {}
478            }
479            r
480        });
481
482        r
483    } else {
484        state.bar.handle(event, MouseOnly)
485    }
486}
487
488/// Handle menu events for the popup-menu.
489///
490/// This one is separate, as it needs to be called before other event-handlers
491/// to cope with overlapping regions.
492///
493/// focus - is the menubar focused?
494pub fn handle_popup_events(
495    state: &mut MenubarState,
496    focus: bool,
497    event: &crossterm::event::Event,
498) -> MenuOutcome {
499    state.bar.focus.set(focus);
500    state.handle(event, Popup)
501}
502
503/// Handle only mouse-events.
504pub fn handle_mouse_events(
505    state: &mut MenuLineState,
506    event: &crossterm::event::Event,
507) -> MenuOutcome {
508    state.handle(event, MouseOnly)
509}