1#![allow(clippy::uninlined_format_args)]
18use crate::event::MenuOutcome;
19use crate::menuline::{MenuLine, MenuLineState};
20use crate::popup_menu::{PopupMenu, PopupMenuState};
21use crate::{MenuStructure, MenuStyle};
22use rat_event::{ConsumedEvent, HandleEvent, MouseOnly, Popup, Regular};
23use rat_focus::{FocusBuilder, FocusFlag, HasFocus, Navigation};
24use rat_popup::Placement;
25use ratatui::buffer::Buffer;
26use ratatui::layout::{Alignment, Rect};
27use ratatui::style::Style;
28use ratatui::text::Line;
29use ratatui::widgets::{Block, StatefulWidget};
30use std::fmt::Debug;
31
32#[derive(Debug, Clone)]
36pub struct Menubar<'a> {
37 structure: Option<&'a dyn MenuStructure<'a>>,
38
39 title: Line<'a>,
40 style: Style,
41 title_style: Option<Style>,
42 select_style: Option<Style>,
43 focus_style: Option<Style>,
44 highlight_style: Option<Style>,
45 disabled_style: Option<Style>,
46 right_style: Option<Style>,
47
48 popup_alignment: Alignment,
49 popup_placement: Placement,
50 popup: PopupMenu<'a>,
51}
52
53#[derive(Debug, Clone)]
56pub struct MenubarLine<'a> {
57 structure: Option<&'a dyn MenuStructure<'a>>,
58
59 title: Line<'a>,
60 style: Style,
61 title_style: Option<Style>,
62 select_style: Option<Style>,
63 focus_style: Option<Style>,
64 highlight_style: Option<Style>,
65 disabled_style: Option<Style>,
66 right_style: Option<Style>,
67}
68
69#[derive(Debug, Clone)]
72pub struct MenubarPopup<'a> {
73 structure: Option<&'a dyn MenuStructure<'a>>,
74
75 style: Style,
76 focus_style: Option<Style>,
77 highlight_style: Option<Style>,
78 disabled_style: Option<Style>,
79 right_style: Option<Style>,
80
81 popup_alignment: Alignment,
82 popup_placement: Placement,
83 popup: PopupMenu<'a>,
84}
85
86#[derive(Debug, Default, Clone)]
88pub struct MenubarState {
89 pub area: Rect,
92 pub bar: MenuLineState,
94 pub popup: PopupMenuState,
96}
97
98impl Default for Menubar<'_> {
99 fn default() -> Self {
100 Self {
101 structure: None,
102 title: Default::default(),
103 style: Default::default(),
104 title_style: None,
105 select_style: None,
106 focus_style: None,
107 highlight_style: None,
108 disabled_style: None,
109 right_style: None,
110 popup_alignment: Alignment::Left,
111 popup_placement: Placement::AboveOrBelow,
112 popup: Default::default(),
113 }
114 }
115}
116
117impl<'a> Menubar<'a> {
118 pub fn new(structure: &'a dyn MenuStructure<'a>) -> Self {
119 Self {
120 structure: Some(structure),
121 ..Default::default()
122 }
123 }
124
125 #[inline]
127 pub fn title(mut self, title: impl Into<Line<'a>>) -> Self {
128 self.title = title.into();
129 self
130 }
131
132 #[inline]
134 pub fn styles(mut self, styles: MenuStyle) -> Self {
135 self.popup = self.popup.styles(styles.clone());
136
137 self.style = styles.style;
138 if styles.highlight.is_some() {
139 self.highlight_style = styles.highlight;
140 }
141 if styles.disabled.is_some() {
142 self.disabled_style = styles.disabled;
143 }
144 if styles.focus.is_some() {
145 self.focus_style = styles.focus;
146 }
147 if styles.title.is_some() {
148 self.title_style = styles.title;
149 }
150 if styles.select.is_some() {
151 self.select_style = styles.select;
152 }
153 if styles.focus.is_some() {
154 self.focus_style = styles.focus;
155 }
156 if styles.right.is_some() {
157 self.right_style = styles.right;
158 }
159 if let Some(alignment) = styles.popup.alignment {
160 self.popup_alignment = alignment;
161 }
162 if let Some(placement) = styles.popup.placement {
163 self.popup_placement = placement;
164 }
165 self
166 }
167
168 #[inline]
170 pub fn style(mut self, style: Style) -> Self {
171 self.style = style;
172 self
173 }
174
175 #[inline]
177 pub fn title_style(mut self, style: Style) -> Self {
178 self.title_style = Some(style);
179 self
180 }
181
182 #[inline]
184 pub fn select_style(mut self, style: Style) -> Self {
185 self.select_style = Some(style);
186 self
187 }
188
189 #[inline]
191 pub fn focus_style(mut self, style: Style) -> Self {
192 self.focus_style = Some(style);
193 self
194 }
195
196 #[inline]
198 pub fn right_style(mut self, style: Style) -> Self {
199 self.right_style = Some(style);
200 self
201 }
202
203 pub fn popup_width(mut self, width: u16) -> Self {
206 self.popup = self.popup.width(width);
207 self
208 }
209
210 pub fn popup_alignment(mut self, alignment: Alignment) -> Self {
212 self.popup_alignment = alignment;
213 self
214 }
215
216 pub fn popup_placement(mut self, placement: Placement) -> Self {
218 self.popup_placement = placement;
219 self
220 }
221
222 pub fn popup_block(mut self, block: Block<'a>) -> Self {
224 self.popup = self.popup.block(block);
225 self
226 }
227
228 pub fn into_widgets(self) -> (MenubarLine<'a>, MenubarPopup<'a>) {
234 (
235 MenubarLine {
236 structure: self.structure,
237 title: self.title,
238 style: self.style,
239 title_style: self.title_style,
240 select_style: self.select_style,
241 focus_style: self.focus_style,
242 highlight_style: self.highlight_style,
243 disabled_style: self.disabled_style,
244 right_style: self.right_style,
245 },
246 MenubarPopup {
247 structure: self.structure,
248 style: self.style,
249 focus_style: self.focus_style,
250 highlight_style: self.highlight_style,
251 disabled_style: self.disabled_style,
252 right_style: self.right_style,
253 popup_alignment: self.popup_alignment,
254 popup_placement: self.popup_placement,
255 popup: self.popup,
256 },
257 )
258 }
259}
260
261impl StatefulWidget for MenubarLine<'_> {
262 type State = MenubarState;
263
264 fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
265 render_menubar(&self, area, buf, state);
266 }
267}
268
269fn render_menubar(
270 widget: &MenubarLine<'_>,
271 area: Rect,
272 buf: &mut Buffer,
273 state: &mut MenubarState,
274) {
275 let mut menu = MenuLine::new()
276 .title(widget.title.clone())
277 .style(widget.style)
278 .title_style_opt(widget.title_style)
279 .select_style_opt(widget.select_style)
280 .focus_style_opt(widget.focus_style)
281 .highlight_style_opt(widget.highlight_style)
282 .disabled_style_opt(widget.disabled_style)
283 .right_style_opt(widget.right_style);
284
285 if let Some(structure) = &widget.structure {
286 structure.menus(&mut menu.menu);
287 }
288 menu.render(area, buf, &mut state.bar);
289
290 state.area = state.bar.area;
292}
293
294impl StatefulWidget for MenubarPopup<'_> {
295 type State = MenubarState;
296
297 fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
298 render_menu_popup(self, area, buf, state);
299 }
300}
301
302fn render_menu_popup(
303 widget: MenubarPopup<'_>,
304 _area: Rect,
305 buf: &mut Buffer,
306 state: &mut MenubarState,
307) {
308 state.area = state.bar.area;
310
311 let Some(selected) = state.bar.selected() else {
312 return;
313 };
314 let Some(structure) = widget.structure else {
315 return;
316 };
317
318 if state.popup.is_active() {
319 let item = state.bar.item_areas[selected];
320
321 let popup_padding = widget.popup.get_block_padding();
322 let sub_offset = (-(popup_padding.left as i16 + 1), 0);
323
324 let mut popup = widget
325 .popup
326 .constraint(
327 widget
328 .popup_placement
329 .into_constraint(widget.popup_alignment, item),
330 )
331 .offset(sub_offset)
332 .style(widget.style)
333 .focus_style_opt(widget.focus_style)
334 .highlight_style_opt(widget.highlight_style)
335 .disabled_style_opt(widget.disabled_style)
336 .right_style_opt(widget.right_style);
337
338 structure.submenu(selected, &mut popup.menu);
339
340 if !popup.menu.items.is_empty() {
341 let area = state.bar.item_areas[selected];
342 popup.render(area, buf, &mut state.popup);
343
344 state.area = state.bar.area.union(state.popup.popup.area);
346 }
347 } else {
348 state.popup = Default::default();
349 }
350}
351
352impl MenubarState {
353 pub fn new() -> Self {
356 Self::default()
357 }
358
359 pub fn named(name: &'static str) -> Self {
361 Self {
362 bar: MenuLineState::named(format!("{}.bar", name).to_string().leak()),
363 popup: PopupMenuState::new(),
364 ..Default::default()
365 }
366 }
367
368 pub fn popup_active(&self) -> bool {
370 self.popup.is_active()
371 }
372
373 pub fn set_popup_active(&mut self, active: bool) {
375 self.popup.set_active(active);
376 }
377
378 pub fn set_popup_z(&mut self, z: u16) {
383 self.popup.set_popup_z(z)
384 }
385
386 pub fn popup_z(&self) -> u16 {
388 self.popup.popup_z()
389 }
390
391 pub fn selected(&self) -> (Option<usize>, Option<usize>) {
393 (self.bar.selected, self.popup.selected)
394 }
395}
396
397impl HasFocus for MenubarState {
398 fn build(&self, builder: &mut FocusBuilder) {
399 builder.widget_with_flags(self.focus(), self.area(), self.area_z(), self.navigable());
400 builder.widget_with_flags(
401 self.focus(),
402 self.popup.popup.area,
403 self.popup.popup.area_z,
404 Navigation::Mouse,
405 );
406 }
407
408 fn focus(&self) -> FocusFlag {
409 self.bar.focus.clone()
410 }
411
412 fn area(&self) -> Rect {
413 self.area
414 }
415}
416
417impl HandleEvent<crossterm::event::Event, Popup, MenuOutcome> for MenubarState {
418 fn handle(&mut self, event: &crossterm::event::Event, _qualifier: Popup) -> MenuOutcome {
419 handle_menubar(self, event, Popup, Regular)
420 }
421}
422
423impl HandleEvent<crossterm::event::Event, MouseOnly, MenuOutcome> for MenubarState {
424 fn handle(&mut self, event: &crossterm::event::Event, _qualifier: MouseOnly) -> MenuOutcome {
425 handle_menubar(self, event, MouseOnly, MouseOnly)
426 }
427}
428
429fn handle_menubar<Q1, Q2>(
430 state: &mut MenubarState,
431 event: &crossterm::event::Event,
432 qualifier1: Q1,
433 qualifier2: Q2,
434) -> MenuOutcome
435where
436 PopupMenuState: HandleEvent<crossterm::event::Event, Q1, MenuOutcome>,
437 MenuLineState: HandleEvent<crossterm::event::Event, Q2, MenuOutcome>,
438 MenuLineState: HandleEvent<crossterm::event::Event, MouseOnly, MenuOutcome>,
439{
440 if !state.is_focused() {
441 state.set_popup_active(false);
442 }
443
444 if state.bar.is_focused() {
445 let mut r = if let Some(selected) = state.bar.selected() {
446 if state.popup_active() {
447 match state.popup.handle(event, qualifier1) {
448 MenuOutcome::Hide => {
449 MenuOutcome::Continue
451 }
452 MenuOutcome::Selected(n) => MenuOutcome::MenuSelected(selected, n),
453 MenuOutcome::Activated(n) => MenuOutcome::MenuActivated(selected, n),
454 r => r,
455 }
456 } else {
457 MenuOutcome::Continue
458 }
459 } else {
460 MenuOutcome::Continue
461 };
462
463 r = r.or_else(|| {
464 let old_selected = state.bar.selected();
465 let r = state.bar.handle(event, qualifier2);
466 match r {
467 MenuOutcome::Selected(_) => {
468 if state.bar.selected == old_selected {
469 state.popup.flip_active();
470 } else {
471 state.popup.select(None);
472 state.popup.set_active(true);
473 }
474 }
475 MenuOutcome::Activated(_) => {
476 state.popup.flip_active();
477 }
478 _ => {}
479 }
480 r
481 });
482
483 r
484 } else {
485 state.bar.handle(event, MouseOnly)
486 }
487}
488
489pub fn handle_popup_events(
496 state: &mut MenubarState,
497 focus: bool,
498 event: &crossterm::event::Event,
499) -> MenuOutcome {
500 state.bar.focus.set(focus);
501 state.handle(event, Popup)
502}
503
504pub fn handle_mouse_events(
506 state: &mut MenuLineState,
507 event: &crossterm::event::Event,
508) -> MenuOutcome {
509 state.handle(event, MouseOnly)
510}