1#![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#[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#[derive(Debug, Clone)]
57pub struct MenubarLine<'a> {
58 structure: Option<&'a dyn MenuStructure<'a>>,
59 menu: MenuLine<'a>,
60}
61
62#[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#[derive(Debug, Clone)]
76pub struct MenubarState {
77 pub area: Rect,
80 pub bar: MenuLineState,
82 pub popup: PopupMenuState,
84
85 relocate_popup: bool,
88
89 pub non_exhaustive: NonExhaustive,
90}
91
92impl Default for Menubar<'_> {
93 fn default() -> Self {
94 Self {
95 structure: Default::default(),
96 menu: Default::default(),
97 popup_alignment: Alignment::Left,
98 popup_placement: Placement::AboveOrBelow,
99 popup_offset: Default::default(),
100 popup: Default::default(),
101 }
102 }
103}
104
105impl<'a> Menubar<'a> {
106 #[inline]
107 pub fn new(structure: &'a dyn MenuStructure<'a>) -> Self {
108 Self {
109 structure: Some(structure),
110 ..Default::default()
111 }
112 }
113
114 #[inline]
116 pub fn style(mut self, style: Style) -> Self {
117 self.menu = self.menu.style(style);
118 self
119 }
120
121 #[inline]
123 pub fn block(mut self, block: Block<'a>) -> Self {
124 self.menu = self.menu.block(block);
125 self
126 }
127
128 #[inline]
130 pub fn title(mut self, title: impl Into<Line<'a>>) -> Self {
131 self.menu = self.menu.title(title);
132 self
133 }
134
135 #[inline]
137 pub fn title_style(mut self, style: Style) -> Self {
138 self.menu = self.menu.title_style(style);
139 self
140 }
141
142 #[inline]
144 pub fn focus_style(mut self, style: Style) -> Self {
145 self.menu = self.menu.focus_style(style);
146 self
147 }
148
149 #[inline]
151 pub fn right_style(mut self, style: Style) -> Self {
152 self.menu = self.menu.right_style(style);
153 self
154 }
155
156 #[inline]
159 pub fn popup_width(mut self, width: u16) -> Self {
160 self.popup = self.popup.menu_width(width);
161 self
162 }
163
164 #[inline]
166 pub fn popup_alignment(mut self, alignment: Alignment) -> Self {
167 self.popup_alignment = alignment;
168 self
169 }
170
171 #[inline]
173 pub fn popup_offset(mut self, offset: (i16, i16)) -> Self {
174 self.popup_offset = Some(offset);
175 self
176 }
177
178 #[inline]
180 pub fn popup_placement(mut self, placement: Placement) -> Self {
181 self.popup_placement = placement;
182 self
183 }
184
185 #[inline]
187 pub fn popup_style(mut self, style: Style) -> Self {
188 self.popup = self.popup.style(style);
189 self
190 }
191
192 #[inline]
194 pub fn popup_block(mut self, block: Block<'a>) -> Self {
195 self.popup = self.popup.block(block);
196 self
197 }
198
199 #[inline]
201 pub fn popup_focus_style(mut self, style: Style) -> Self {
202 self.popup = self.popup.focus_style(style);
203 self
204 }
205
206 #[inline]
208 pub fn popup_right_style(mut self, style: Style) -> Self {
209 self.popup = self.popup.right_style(style);
210 self
211 }
212
213 #[inline]
215 pub fn styles(mut self, styles: MenuStyle) -> Self {
216 self.menu = self.menu.styles(styles.clone());
217 self.popup = self.popup.styles(styles.clone());
218
219 if let Some(alignment) = styles.popup.alignment {
220 self.popup_alignment = alignment;
221 }
222 if let Some(placement) = styles.popup.placement {
223 self.popup_placement = placement;
224 }
225 if let Some(offset) = styles.popup.offset {
226 self.popup_offset = Some(offset);
227 }
228 self
229 }
230
231 #[inline]
237 pub fn into_widgets(self) -> (MenubarLine<'a>, MenubarPopup<'a>) {
238 (
239 MenubarLine {
240 structure: self.structure,
241 menu: self.menu,
242 },
243 MenubarPopup {
244 structure: self.structure,
245 popup_alignment: self.popup_alignment,
246 popup_placement: self.popup_placement,
247 popup_offset: self.popup_offset,
248 popup: self.popup,
249 },
250 )
251 }
252}
253
254impl<'a> StatefulWidget for Menubar<'a> {
255 type State = MenubarState;
256
257 fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
258 let (menu, popup) = self.into_widgets();
259 menu.render(area, buf, state);
260 popup.render(Rect::default(), buf, state);
261 state.relocate_popup = false;
264 }
265}
266
267impl StatefulWidget for MenubarLine<'_> {
268 type State = MenubarState;
269
270 fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
271 render_menubar(self, area, buf, state);
272 }
273}
274
275fn render_menubar(
276 mut widget: MenubarLine<'_>,
277 area: Rect,
278 buf: &mut Buffer,
279 state: &mut MenubarState,
280) {
281 if let Some(structure) = &widget.structure {
282 structure.menus(&mut widget.menu.menu);
283 }
284 widget.menu.render(area, buf, &mut state.bar);
285 state.area = state.bar.area;
287 state.relocate_popup = true;
288}
289
290impl StatefulWidget for MenubarPopup<'_> {
291 type State = MenubarState;
292
293 fn render(self, _area: Rect, buf: &mut Buffer, state: &mut Self::State) {
294 render_menu_popup(self, buf, state);
295 }
296}
297
298fn render_menu_popup(mut widget: MenubarPopup<'_>, buf: &mut Buffer, state: &mut MenubarState) {
299 let Some(selected) = state.bar.selected() else {
300 return;
301 };
302 let Some(structure) = widget.structure else {
303 return;
304 };
305
306 if state.popup.is_active() {
307 let item = state.bar.item_areas[selected];
308
309 let popup_padding = widget.popup.get_block_padding();
310 let sub_offset = if let Some(offset) = widget.popup_offset {
311 offset
312 } else {
313 (-(popup_padding.left as i16 + 1), 0)
314 };
315
316 widget.popup = widget
317 .popup
318 .constraint(
319 widget
320 .popup_placement
321 .into_constraint(widget.popup_alignment, item),
322 )
323 .offset(sub_offset);
324 structure.submenu(selected, &mut widget.popup.menu);
325
326 if !widget.popup.menu.items.is_empty() {
327 let area = state.bar.item_areas[selected];
328 widget.popup.render(area, buf, &mut state.popup);
329 }
330 } else {
331 state.popup = Default::default();
332 }
333}
334
335impl MenubarState {
336 pub fn new() -> Self {
339 Self::default()
340 }
341
342 pub fn named(name: &'static str) -> Self {
344 Self {
345 bar: MenuLineState::named(format!("{}.bar", name).to_string().leak()),
346 popup: PopupMenuState::new(),
347 ..Default::default()
348 }
349 }
350
351 pub fn popup_active(&self) -> bool {
353 self.popup.is_active()
354 }
355
356 pub fn set_popup_active(&mut self, active: bool) {
358 self.popup.set_active(active);
359 }
360
361 pub fn set_popup_z(&mut self, z: u16) {
366 self.popup.set_popup_z(z)
367 }
368
369 pub fn popup_z(&self) -> u16 {
371 self.popup.popup_z()
372 }
373
374 pub fn selected(&self) -> (Option<usize>, Option<usize>) {
376 (self.bar.selected, self.popup.selected)
377 }
378}
379
380impl Default for MenubarState {
381 fn default() -> Self {
382 Self {
383 area: Default::default(),
384 bar: Default::default(),
385 popup: Default::default(),
386 relocate_popup: Default::default(),
387 non_exhaustive: NonExhaustive,
388 }
389 }
390}
391
392impl HasFocus for MenubarState {
393 fn build(&self, builder: &mut FocusBuilder) {
394 builder.widget_with_flags(self.focus(), self.area(), self.area_z(), self.navigable());
395 builder.widget_with_flags(
396 self.focus(),
397 self.popup.popup.area,
398 self.popup.popup.area_z,
399 Navigation::Mouse,
400 );
401 }
402
403 fn focus(&self) -> FocusFlag {
404 self.bar.focus.clone()
405 }
406
407 fn area(&self) -> Rect {
408 self.area
409 }
410}
411
412impl HasScreenCursor for MenubarState {
413 fn screen_cursor(&self) -> Option<(u16, u16)> {
414 None
415 }
416}
417
418impl RelocatableState for MenubarState {
419 fn relocate(&mut self, shift: (i16, i16), clip: Rect) {
420 if !self.relocate_popup {
421 self.area.relocate(shift, clip);
422 self.bar.relocate(shift, clip);
423 self.popup.relocate(shift, clip);
424 self.popup.relocate_popup(shift, clip);
425 }
426 }
427
428 fn relocate_popup(&mut self, shift: (i16, i16), clip: Rect) {
429 if self.relocate_popup {
430 self.relocate_popup = false;
431 self.area.relocate(shift, clip);
432 self.bar.relocate(shift, clip);
433 self.popup.relocate(shift, clip);
434 self.popup.relocate_popup(shift, clip);
435 }
436 }
437}
438
439impl HandleEvent<crossterm::event::Event, Popup, MenuOutcome> for MenubarState {
440 fn handle(&mut self, event: &crossterm::event::Event, _qualifier: Popup) -> MenuOutcome {
441 handle_menubar(self, event, Popup, Regular)
442 }
443}
444
445impl HandleEvent<crossterm::event::Event, MouseOnly, MenuOutcome> for MenubarState {
446 fn handle(&mut self, event: &crossterm::event::Event, _qualifier: MouseOnly) -> MenuOutcome {
447 handle_menubar(self, event, MouseOnly, MouseOnly)
448 }
449}
450
451fn handle_menubar<Q1, Q2>(
452 state: &mut MenubarState,
453 event: &crossterm::event::Event,
454 qualifier1: Q1,
455 qualifier2: Q2,
456) -> MenuOutcome
457where
458 PopupMenuState: HandleEvent<crossterm::event::Event, Q1, MenuOutcome>,
459 MenuLineState: HandleEvent<crossterm::event::Event, Q2, MenuOutcome>,
460 MenuLineState: HandleEvent<crossterm::event::Event, MouseOnly, MenuOutcome>,
461{
462 if !state.is_focused() {
463 state.set_popup_active(false);
464 }
465
466 if state.bar.is_focused() {
467 let mut r = if let Some(selected) = state.bar.selected() {
468 if state.popup_active() {
469 match state.popup.handle(event, qualifier1) {
470 MenuOutcome::Hide => {
471 MenuOutcome::Continue
473 }
474 MenuOutcome::Selected(n) => MenuOutcome::MenuSelected(selected, n),
475 MenuOutcome::Activated(n) => MenuOutcome::MenuActivated(selected, n),
476 r => r,
477 }
478 } else {
479 MenuOutcome::Continue
480 }
481 } else {
482 MenuOutcome::Continue
483 };
484
485 r = r.or_else(|| {
486 let old_selected = state.bar.selected();
487 let r = state.bar.handle(event, qualifier2);
488 match r {
489 MenuOutcome::Selected(_) => {
490 if state.bar.selected == old_selected {
491 state.popup.flip_active();
492 } else {
493 state.popup.select(None);
494 state.popup.set_active(true);
495 }
496 }
497 MenuOutcome::Activated(_) => {
498 state.popup.flip_active();
499 }
500 _ => {}
501 }
502 r
503 });
504
505 r
506 } else {
507 state.bar.handle(event, MouseOnly)
508 }
509}
510
511pub fn handle_events(
518 state: &mut MenubarState,
519 focus: bool,
520 event: &crossterm::event::Event,
521) -> MenuOutcome {
522 state.bar.focus.set(focus);
523 state.handle(event, Popup)
524}
525
526pub fn handle_popup_events(
533 state: &mut MenubarState,
534 focus: bool,
535 event: &crossterm::event::Event,
536) -> MenuOutcome {
537 state.bar.focus.set(focus);
538 state.handle(event, Popup)
539}
540
541pub fn handle_mouse_events(
543 state: &mut MenubarState,
544 event: &crossterm::event::Event,
545) -> MenuOutcome {
546 state.handle(event, MouseOnly)
547}