#![allow(clippy::uninlined_format_args)]
use crate::_private::NonExhaustive;
use crate::event::MenuOutcome;
use crate::menuline::{MenuLine, MenuLineState};
use crate::popup_menu::{PopupMenu, PopupMenuState};
use crate::{MenuStructure, MenuStyle};
use rat_cursor::HasScreenCursor;
use rat_event::{ConsumedEvent, HandleEvent, MouseOnly, Popup, Regular};
use rat_focus::{FocusBuilder, FocusFlag, HasFocus, Navigation};
use rat_popup::Placement;
use rat_reloc::RelocatableState;
use ratatui_core::buffer::Buffer;
use ratatui_core::layout::{Alignment, Rect};
use ratatui_core::style::Style;
use ratatui_core::text::Line;
use ratatui_core::widgets::StatefulWidget;
use ratatui_crossterm::crossterm::event::Event;
use ratatui_widgets::block::Block;
use std::fmt::Debug;
#[derive(Debug, Clone)]
pub struct Menubar<'a> {
structure: Option<&'a dyn MenuStructure<'a>>,
menu: MenuLine<'a>,
popup_alignment: Alignment,
popup_placement: Placement,
popup_offset: Option<(i16, i16)>,
popup: PopupMenu<'a>,
}
#[derive(Debug, Clone)]
pub struct MenubarLine<'a> {
structure: Option<&'a dyn MenuStructure<'a>>,
menu: MenuLine<'a>,
}
#[derive(Debug, Clone)]
pub struct MenubarPopup<'a> {
structure: Option<&'a dyn MenuStructure<'a>>,
popup_alignment: Alignment,
popup_placement: Placement,
popup_offset: Option<(i16, i16)>,
popup: PopupMenu<'a>,
}
#[derive(Debug, Clone)]
pub struct MenubarState {
pub area: Rect,
pub bar: MenuLineState,
pub popup: PopupMenuState,
relocate_popup: bool,
pub non_exhaustive: NonExhaustive,
}
impl Default for Menubar<'_> {
fn default() -> Self {
Self {
structure: Default::default(),
menu: Default::default(),
popup_alignment: Alignment::Left,
popup_placement: Placement::AboveOrBelow,
popup_offset: Default::default(),
popup: Default::default(),
}
}
}
impl<'a> Menubar<'a> {
#[inline]
pub fn new(structure: &'a dyn MenuStructure<'a>) -> Self {
Self {
structure: Some(structure),
..Default::default()
}
}
#[inline]
pub fn style(mut self, style: Style) -> Self {
self.menu = self.menu.style(style);
self
}
#[inline]
pub fn block(mut self, block: Block<'a>) -> Self {
self.menu = self.menu.block(block);
self
}
#[inline]
pub fn title(mut self, title: impl Into<Line<'a>>) -> Self {
self.menu = self.menu.title(title);
self
}
#[inline]
pub fn title_style(mut self, style: Style) -> Self {
self.menu = self.menu.title_style(style);
self
}
#[inline]
pub fn focus_style(mut self, style: Style) -> Self {
self.menu = self.menu.focus_style(style);
self
}
#[inline]
pub fn right_style(mut self, style: Style) -> Self {
self.menu = self.menu.right_style(style);
self
}
#[inline]
pub fn popup_width(mut self, width: u16) -> Self {
self.popup = self.popup.menu_width(width);
self
}
#[inline]
pub fn popup_alignment(mut self, alignment: Alignment) -> Self {
self.popup_alignment = alignment;
self
}
#[inline]
pub fn popup_offset(mut self, offset: (i16, i16)) -> Self {
self.popup_offset = Some(offset);
self
}
#[inline]
pub fn popup_placement(mut self, placement: Placement) -> Self {
self.popup_placement = placement;
self
}
#[inline]
pub fn popup_style(mut self, style: Style) -> Self {
self.popup = self.popup.style(style);
self
}
#[inline]
pub fn popup_block(mut self, block: Block<'a>) -> Self {
self.popup = self.popup.block(block);
self
}
#[inline]
pub fn popup_focus_style(mut self, style: Style) -> Self {
self.popup = self.popup.focus_style(style);
self
}
#[inline]
pub fn popup_right_style(mut self, style: Style) -> Self {
self.popup = self.popup.right_style(style);
self
}
#[inline]
pub fn styles(mut self, styles: MenuStyle) -> Self {
self.menu = self.menu.styles(styles.clone());
self.popup = self.popup.styles(styles.clone());
if let Some(alignment) = styles.popup.alignment {
self.popup_alignment = alignment;
}
if let Some(placement) = styles.popup.placement {
self.popup_placement = placement;
}
if let Some(offset) = styles.popup.offset {
self.popup_offset = Some(offset);
}
self
}
#[inline]
pub fn into_widgets(self) -> (MenubarLine<'a>, MenubarPopup<'a>) {
(
MenubarLine {
structure: self.structure,
menu: self.menu,
},
MenubarPopup {
structure: self.structure,
popup_alignment: self.popup_alignment,
popup_placement: self.popup_placement,
popup_offset: self.popup_offset,
popup: self.popup,
},
)
}
}
impl<'a> StatefulWidget for Menubar<'a> {
type State = MenubarState;
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
let (menu, popup) = self.into_widgets();
menu.render(area, buf, state);
popup.render(Rect::default(), buf, state);
state.relocate_popup = false;
}
}
impl StatefulWidget for MenubarLine<'_> {
type State = MenubarState;
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
render_menubar(self, area, buf, state);
}
}
fn render_menubar(
mut widget: MenubarLine<'_>,
area: Rect,
buf: &mut Buffer,
state: &mut MenubarState,
) {
if let Some(structure) = &widget.structure {
structure.menus(&mut widget.menu.menu);
}
widget.menu.render(area, buf, &mut state.bar);
state.area = state.bar.area;
state.relocate_popup = true;
}
impl StatefulWidget for MenubarPopup<'_> {
type State = MenubarState;
fn render(self, _area: Rect, buf: &mut Buffer, state: &mut Self::State) {
render_menu_popup(self, buf, state);
}
}
fn render_menu_popup(mut widget: MenubarPopup<'_>, buf: &mut Buffer, state: &mut MenubarState) {
let Some(selected) = state.bar.selected() else {
return;
};
let Some(structure) = widget.structure else {
return;
};
if state.popup.is_active() {
let item = state.bar.item_areas[selected];
let popup_padding = widget.popup.get_block_padding();
let sub_offset = if let Some(offset) = widget.popup_offset {
offset
} else {
(-(popup_padding.left as i16 + 1), 0)
};
widget.popup = widget
.popup
.constraint(
widget
.popup_placement
.into_constraint(widget.popup_alignment, item),
)
.offset(sub_offset);
structure.submenu(selected, &mut widget.popup.menu);
if !widget.popup.menu.items.is_empty() {
let area = state.bar.item_areas[selected];
widget.popup.render(area, buf, &mut state.popup);
}
} else {
state.popup = Default::default();
}
}
impl MenubarState {
pub fn new() -> Self {
Self::default()
}
pub fn named(name: &'static str) -> Self {
Self {
bar: MenuLineState::named(format!("{}.bar", name).to_string().leak()),
popup: PopupMenuState::new(),
..Default::default()
}
}
pub fn popup_active(&self) -> bool {
self.popup.is_active()
}
pub fn set_popup_active(&mut self, active: bool) {
self.popup.set_active(active);
}
pub fn set_popup_z(&mut self, z: u16) {
self.popup.set_popup_z(z)
}
pub fn popup_z(&self) -> u16 {
self.popup.popup_z()
}
pub fn selected(&self) -> (Option<usize>, Option<usize>) {
(self.bar.selected, self.popup.selected)
}
}
impl Default for MenubarState {
fn default() -> Self {
let mut z = Self {
area: Default::default(),
bar: MenuLineState::new(),
popup: PopupMenuState::new(),
relocate_popup: Default::default(),
non_exhaustive: NonExhaustive,
};
z.popup.focus = z.bar.focus.clone();
z
}
}
impl HasFocus for MenubarState {
fn build(&self, builder: &mut FocusBuilder) {
MenubarState::build_nav(&self, self.navigable(), builder);
}
fn build_nav(&self, navigable: Navigation, builder: &mut FocusBuilder) {
builder.leaf_with_flags(self.focus(), self.area(), self.area_z(), navigable);
builder.leaf_with_flags(
self.focus(),
self.popup.popup.area,
self.popup.popup.area_z,
Navigation::Mouse,
)
}
fn focus(&self) -> FocusFlag {
self.bar.focus.clone()
}
fn area(&self) -> Rect {
self.area
}
}
impl HasScreenCursor for MenubarState {
fn screen_cursor(&self) -> Option<(u16, u16)> {
None
}
}
impl RelocatableState for MenubarState {
fn relocate(&mut self, shift: (i16, i16), clip: Rect) {
if !self.relocate_popup {
self.area.relocate(shift, clip);
self.bar.relocate(shift, clip);
self.popup.relocate(shift, clip);
self.popup.relocate_popup(shift, clip);
}
}
fn relocate_popup(&mut self, shift: (i16, i16), clip: Rect) {
if self.relocate_popup {
self.relocate_popup = false;
self.area.relocate(shift, clip);
self.bar.relocate(shift, clip);
self.popup.relocate(shift, clip);
self.popup.relocate_popup(shift, clip);
}
}
}
impl HandleEvent<Event, Regular, MenuOutcome> for MenubarState {
fn handle(&mut self, event: &Event, _qualifier: Regular) -> MenuOutcome {
handle_menubar(self, event, Regular, Regular)
}
}
impl HandleEvent<Event, Popup, MenuOutcome> for MenubarState {
fn handle(&mut self, event: &Event, _qualifier: Popup) -> MenuOutcome {
handle_menubar(self, event, Popup, Regular)
}
}
impl HandleEvent<Event, MouseOnly, MenuOutcome> for MenubarState {
fn handle(&mut self, event: &Event, _qualifier: MouseOnly) -> MenuOutcome {
handle_menubar(self, event, MouseOnly, MouseOnly)
}
}
fn handle_menubar<Q1, Q2>(
state: &mut MenubarState,
event: &Event,
qualifier1: Q1,
qualifier2: Q2,
) -> MenuOutcome
where
PopupMenuState: HandleEvent<Event, Q1, MenuOutcome>,
MenuLineState: HandleEvent<Event, Q2, MenuOutcome>,
MenuLineState: HandleEvent<Event, MouseOnly, MenuOutcome>,
{
if !state.is_focused() {
state.set_popup_active(false);
}
if state.bar.is_focused() {
let mut r = if let Some(selected) = state.bar.selected() {
if state.popup_active() {
match state.popup.handle(event, qualifier1) {
MenuOutcome::Hide => {
MenuOutcome::Continue
}
MenuOutcome::Selected(n) => MenuOutcome::MenuSelected(selected, n),
MenuOutcome::Activated(n) => MenuOutcome::MenuActivated(selected, n),
r => r,
}
} else {
MenuOutcome::Continue
}
} else {
MenuOutcome::Continue
};
r = r.or_else(|| {
let old_selected = state.bar.selected();
let r = state.bar.handle(event, qualifier2);
match r {
MenuOutcome::Selected(_) => {
if state.bar.selected == old_selected {
state.popup.flip_active();
} else {
state.popup.select(None);
state.popup.set_active(true);
}
}
MenuOutcome::Activated(_) => {
state.popup.flip_active();
}
_ => {}
}
r
});
r
} else {
state.bar.handle(event, MouseOnly)
}
}
pub fn handle_events(state: &mut MenubarState, focus: bool, event: &Event) -> MenuOutcome {
state.bar.focus.set(focus);
state.handle(event, Popup)
}
pub fn handle_popup_events(state: &mut MenubarState, focus: bool, event: &Event) -> MenuOutcome {
state.bar.focus.set(focus);
state.handle(event, Popup)
}
pub fn handle_mouse_events(state: &mut MenubarState, event: &Event) -> MenuOutcome {
state.handle(event, MouseOnly)
}