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