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 StatefulWidget for MenubarLine<'_> {
251 type State = MenubarState;
252
253 fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
254 render_menubar(self, area, buf, state);
255 }
256}
257
258fn render_menubar(
259 mut widget: MenubarLine<'_>,
260 area: Rect,
261 buf: &mut Buffer,
262 state: &mut MenubarState,
263) {
264 if let Some(structure) = &widget.structure {
265 structure.menus(&mut widget.menu.menu);
266 }
267 widget.menu.render(area, buf, &mut state.bar);
268 state.area = state.bar.area;
270}
271
272impl StatefulWidget for MenubarPopup<'_> {
273 type State = MenubarState;
274
275 fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
276 render_menu_popup(self, area, buf, state);
277 }
278}
279
280fn render_menu_popup(
281 mut widget: MenubarPopup<'_>,
282 _area: Rect,
283 buf: &mut Buffer,
284 state: &mut MenubarState,
285) {
286 let Some(selected) = state.bar.selected() else {
287 return;
288 };
289 let Some(structure) = widget.structure else {
290 return;
291 };
292
293 if state.popup.is_active() {
294 let item = state.bar.item_areas[selected];
295
296 let popup_padding = widget.popup.get_block_padding();
297 let sub_offset = if let Some(offset) = widget.popup_offset {
298 offset
299 } else {
300 (-(popup_padding.left as i16 + 1), 0)
301 };
302
303 widget.popup = widget
304 .popup
305 .constraint(
306 widget
307 .popup_placement
308 .into_constraint(widget.popup_alignment, item),
309 )
310 .offset(sub_offset);
311 structure.submenu(selected, &mut widget.popup.menu);
312
313 if !widget.popup.menu.items.is_empty() {
314 let area = state.bar.item_areas[selected];
315 widget.popup.render(area, buf, &mut state.popup);
316 }
317 } else {
318 state.popup = Default::default();
319 }
320}
321
322impl MenubarState {
323 pub fn new() -> Self {
326 Self::default()
327 }
328
329 pub fn named(name: &'static str) -> Self {
331 Self {
332 bar: MenuLineState::named(format!("{}.bar", name).to_string().leak()),
333 popup: PopupMenuState::new(),
334 ..Default::default()
335 }
336 }
337
338 pub fn popup_active(&self) -> bool {
340 self.popup.is_active()
341 }
342
343 pub fn set_popup_active(&mut self, active: bool) {
345 self.popup.set_active(active);
346 }
347
348 pub fn set_popup_z(&mut self, z: u16) {
353 self.popup.set_popup_z(z)
354 }
355
356 pub fn popup_z(&self) -> u16 {
358 self.popup.popup_z()
359 }
360
361 pub fn selected(&self) -> (Option<usize>, Option<usize>) {
363 (self.bar.selected, self.popup.selected)
364 }
365}
366
367impl Default for MenubarState {
368 fn default() -> Self {
369 Self {
370 area: Default::default(),
371 bar: Default::default(),
372 popup: Default::default(),
373 non_exhaustive: NonExhaustive,
374 }
375 }
376}
377
378impl HasFocus for MenubarState {
379 fn build(&self, builder: &mut FocusBuilder) {
380 builder.widget_with_flags(self.focus(), self.area(), self.area_z(), self.navigable());
381 builder.widget_with_flags(
382 self.focus(),
383 self.popup.popup.area,
384 self.popup.popup.area_z,
385 Navigation::Mouse,
386 );
387 }
388
389 fn focus(&self) -> FocusFlag {
390 self.bar.focus.clone()
391 }
392
393 fn area(&self) -> Rect {
394 self.area
395 }
396}
397
398impl HasScreenCursor for MenubarState {
399 fn screen_cursor(&self) -> Option<(u16, u16)> {
400 None
401 }
402}
403
404impl RelocatableState for MenubarState {
405 fn relocate(&mut self, shift: (i16, i16), clip: Rect) {
406 self.area.relocate(shift, clip);
407 self.bar.relocate(shift, clip);
408 self.popup.relocate(shift, clip);
409 }
410
411 fn relocate_popup(&mut self, shift: (i16, i16), clip: Rect) {
412 self.popup.relocate_popup(shift, clip);
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_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_popup_events(
510 state: &mut MenubarState,
511 focus: bool,
512 event: &crossterm::event::Event,
513) -> MenuOutcome {
514 state.bar.focus.set(focus);
515 state.handle(event, Popup)
516}
517
518pub fn handle_mouse_events(
520 state: &mut MenubarState,
521 event: &crossterm::event::Event,
522) -> MenuOutcome {
523 state.handle(event, MouseOnly)
524}