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