1use 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#[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#[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#[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#[derive(Debug, Default, Clone)]
87pub struct MenubarState {
88 pub area: Rect,
91 pub bar: MenuLineState,
93 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 #[inline]
126 pub fn title(mut self, title: impl Into<Line<'a>>) -> Self {
127 self.title = title.into();
128 self
129 }
130
131 #[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 #[inline]
169 pub fn style(mut self, style: Style) -> Self {
170 self.style = style;
171 self
172 }
173
174 #[inline]
176 pub fn title_style(mut self, style: Style) -> Self {
177 self.title_style = Some(style);
178 self
179 }
180
181 #[inline]
183 pub fn select_style(mut self, style: Style) -> Self {
184 self.select_style = Some(style);
185 self
186 }
187
188 #[inline]
190 pub fn focus_style(mut self, style: Style) -> Self {
191 self.focus_style = Some(style);
192 self
193 }
194
195 #[inline]
197 pub fn right_style(mut self, style: Style) -> Self {
198 self.right_style = Some(style);
199 self
200 }
201
202 pub fn popup_width(mut self, width: u16) -> Self {
205 self.popup = self.popup.width(width);
206 self
207 }
208
209 pub fn popup_alignment(mut self, alignment: Alignment) -> Self {
211 self.popup_alignment = alignment;
212 self
213 }
214
215 pub fn popup_placement(mut self, placement: Placement) -> Self {
217 self.popup_placement = placement;
218 self
219 }
220
221 pub fn popup_block(mut self, block: Block<'a>) -> Self {
223 self.popup = self.popup.block(block);
224 self
225 }
226
227 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 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 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 state.area = state.bar.area.union(state.popup.popup.area);
345 }
346 } else {
347 state.popup = Default::default();
348 }
349}
350
351impl MenubarState {
352 pub fn new() -> Self {
355 Self::default()
356 }
357
358 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 pub fn popup_active(&self) -> bool {
369 self.popup.is_active()
370 }
371
372 pub fn set_popup_active(&mut self, active: bool) {
374 self.popup.set_active(active);
375 }
376
377 pub fn set_popup_z(&mut self, z: u16) {
382 self.popup.set_popup_z(z)
383 }
384
385 pub fn popup_z(&self) -> u16 {
387 self.popup.popup_z()
388 }
389
390 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 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
488pub 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
503pub fn handle_mouse_events(
505 state: &mut MenuLineState,
506 event: &crossterm::event::Event,
507) -> MenuOutcome {
508 state.handle(event, MouseOnly)
509}