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