use eframe::egui::text::{LayoutJob, TextFormat};
use eframe::egui::{self, Button, Color32, Id, Popup, TextStyle, Ui};
use rcmd_tui::app::{App, EDIT_MENUS, MENUS, MenuBar, MenuBarFor, menu_hotkey, menu_label};
#[derive(Clone, Copy, Default)]
pub struct Request {
pub open_first: bool,
pub enabled: bool,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum WindowEntry {
Font,
}
pub fn show(
app: &mut App,
ui: &mut Ui,
request: Request,
focus_first: &mut bool,
) -> Option<WindowEntry> {
let (kind, enabled) = app.menu_bar_for();
let enabled = enabled && request.enabled;
let mut extra = None;
match kind {
MenuBarFor::Panels => {
if let Some((menu, action)) = bar(
ui,
MENUS,
enabled,
request.open_first,
focus_first,
&mut extra,
) {
app.run_menu_entry(menu, action);
app.set_dirty();
}
}
MenuBarFor::Editor => {
if let Some((_, action)) = bar(
ui,
EDIT_MENUS,
enabled,
request.open_first,
focus_first,
&mut extra,
) {
app.run_edit_menu_entry(action);
app.set_dirty();
}
}
}
extra
}
fn window_entries(ui: &mut Ui, typed: Option<char>, extra: &mut Option<WindowEntry>) -> bool {
ui.separator();
let button = ui.add(Button::new(label(ui, "Fon&t...")));
if button.clicked() || typed == Some('t') {
*extra = Some(WindowEntry::Font);
ui.close();
return true;
}
false
}
fn bar<A: Copy>(
ui: &mut Ui,
menus: MenuBar<'_, A>,
enabled: bool,
open_first: bool,
focus_first: &mut bool,
extra: &mut Option<WindowEntry>,
) -> Option<(usize, A)> {
let ctx = ui.ctx().clone();
let typed = if Popup::is_any_open(&ctx) {
typed_letter(&ctx)
} else {
None
};
let mut chosen = None;
let mut popups: Vec<Id> = Vec::with_capacity(menus.len());
let mut consumed = false;
ui.add_enabled_ui(enabled, |ui| {
egui::MenuBar::new().ui(ui, |ui| {
for (i, (title, entries)) in menus.iter().enumerate() {
let response = ui
.menu_button(label(ui, title), |ui| {
let mut first = true;
for entry in entries.iter() {
let Some((text, key, action)) = entry else {
ui.separator();
continue;
};
let button = ui.add(Button::new(label(ui, text)).shortcut_text(*key));
if first && std::mem::take(focus_first) {
button.request_focus();
}
first = false;
let by_letter = typed.is_some() && typed == menu_hotkey(text);
if button.clicked() || by_letter {
chosen = Some((i, *action));
consumed = true;
ui.close();
}
}
if *title == "&Options" && window_entries(ui, typed, extra) {
consumed = true;
}
})
.response;
popups.push(Popup::default_response_id(&response));
}
});
});
if open_first && enabled {
if let Some(id) = popups.first() {
Popup::open_id(&ctx, *id);
}
*focus_first = true;
ctx.request_repaint();
} else if Popup::is_any_open(&ctx) {
let left = ctx.input(|i| i.key_pressed(egui::Key::ArrowLeft));
let right = ctx.input(|i| i.key_pressed(egui::Key::ArrowRight));
if left || right {
if let Some(cur) = popups.iter().position(|id| Popup::is_id_open(&ctx, *id)) {
let next = if right {
(cur + 1) % popups.len()
} else {
(cur + popups.len() - 1) % popups.len()
};
Popup::close_all(&ctx);
Popup::open_id(&ctx, popups[next]);
*focus_first = true;
ctx.request_repaint();
}
} else if let Some(c) = typed
&& !consumed
&& let Some(i) = menus
.iter()
.position(|(title, _)| menu_hotkey(title) == Some(c))
{
Popup::open_id(&ctx, popups[i]);
*focus_first = true;
ctx.request_repaint();
}
}
chosen
}
fn label(ui: &Ui, text: &str) -> LayoutJob {
let (pre, hot, post) = menu_label(text);
let font = TextStyle::Button.resolve(ui.style());
let plain = TextFormat::simple(font.clone(), Color32::PLACEHOLDER);
let mut job = LayoutJob::default();
job.append(pre, 0.0, plain.clone());
if let Some(c) = hot {
let mut under = plain.clone();
under.underline = egui::Stroke::new(1.0, Color32::PLACEHOLDER);
job.append(&c.to_string(), 0.0, under);
}
job.append(post, 0.0, plain);
job
}
fn typed_letter(ctx: &egui::Context) -> Option<char> {
ctx.input(|i| {
i.events.iter().find_map(|event| match event {
egui::Event::Text(text) => {
let mut chars = text.chars();
match (chars.next(), chars.next()) {
(Some(c), None) if c.is_alphabetic() => Some(c.to_ascii_lowercase()),
_ => None,
}
}
_ => None,
})
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn hotkey_letters_are_unique_within_every_dropdown() {
fn check<A>(menus: MenuBar<'_, A>) {
for (title, entries) in menus {
let mut seen = if *title == "&Options" {
vec!['t']
} else {
Vec::new()
};
for (text, ..) in entries.iter().flatten() {
if let Some(c) = menu_hotkey(text) {
assert!(!seen.contains(&c), "{title}: letter {c:?} twice");
seen.push(c);
}
}
}
}
check(MENUS);
check(EDIT_MENUS);
}
#[test]
fn label_drops_only_the_marker() {
let ctx = egui::Context::default();
let mut out = ctx.run_ui(Default::default(), |ui| {
let job = label(ui, "Filtered vie&w...");
assert_eq!(job.text, "Filtered view...");
assert_eq!(job.sections.len(), 3);
assert_eq!(job.sections[1].byte_range.start.0, 12);
assert_eq!(job.sections[1].byte_range.end.0, 13);
let job = label(ui, "no marker");
assert_eq!(job.text, "no marker");
assert_eq!(job.sections.len(), 1);
});
out.textures_delta.clear();
}
fn app() -> App {
let cfg = rcmd_tui::config::Config {
subshell: false,
..Default::default()
};
let mut app = App::new(&[], cfg, Vec::new(), &rcmd_tui::app::Startup::Panels).unwrap();
app.set_external_menubar();
app
}
fn frame(
ctx: &egui::Context,
app: &mut App,
events: Vec<egui::Event>,
focus: &mut bool,
) -> Option<WindowEntry> {
let input = egui::RawInput {
events,
screen_rect: Some(egui::Rect::from_min_size(
egui::Pos2::ZERO,
egui::vec2(800.0, 600.0),
)),
..Default::default()
};
let request = Request {
open_first: app.take_menu_request(),
enabled: true,
};
let mut entry = None;
let mut out = ctx.run_ui(input, |ui| {
entry = egui::Panel::top("menubar")
.show(ui, |ui| show(app, ui, request, focus))
.inner;
});
out.textures_delta.clear();
entry
}
#[test]
fn the_windows_font_entry_is_reached_by_letter() {
use ratatui::crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
let ctx = egui::Context::default();
let mut app = app();
let mut focus = false;
frame(&ctx, &mut app, Vec::new(), &mut focus);
app.on_key(KeyEvent::new(KeyCode::F(9), KeyModifiers::NONE));
frame(&ctx, &mut app, Vec::new(), &mut focus);
frame(&ctx, &mut app, Vec::new(), &mut focus);
frame(
&ctx,
&mut app,
vec![egui::Event::Text("o".into())],
&mut focus,
);
frame(&ctx, &mut app, Vec::new(), &mut focus);
assert!(Popup::is_any_open(&ctx), "Options is open");
let entry = frame(
&ctx,
&mut app,
vec![egui::Event::Text("t".into())],
&mut focus,
);
assert_eq!(entry, Some(WindowEntry::Font));
assert!(!app.exiting());
frame(&ctx, &mut app, Vec::new(), &mut focus);
assert!(!Popup::is_any_open(&ctx), "and the dropdown closed");
}
#[test]
fn f9_opens_the_bar_and_letters_walk_it() {
use ratatui::crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
let ctx = egui::Context::default();
let mut app = app();
let mut focus = false;
frame(&ctx, &mut app, Vec::new(), &mut focus);
assert!(!Popup::is_any_open(&ctx));
app.on_key(KeyEvent::new(KeyCode::F(9), KeyModifiers::NONE));
assert!(app.menu_requested(), "F9 asks the bar to open");
assert!(
app.menu.is_none(),
"and does not drop the terminal build's menu"
);
frame(&ctx, &mut app, Vec::new(), &mut focus);
frame(&ctx, &mut app, Vec::new(), &mut focus);
assert!(Popup::is_any_open(&ctx), "F9 opened a dropdown");
assert!(!app.exiting());
frame(
&ctx,
&mut app,
vec![egui::Event::Text("f".into())],
&mut focus,
);
frame(&ctx, &mut app, Vec::new(), &mut focus);
assert!(
Popup::is_any_open(&ctx),
"a title letter keeps a dropdown open"
);
assert!(!app.exiting(), "and runs nothing");
frame(
&ctx,
&mut app,
vec![egui::Event::Text("q".into())],
&mut focus,
);
assert!(app.exiting(), "File > Quit by its letter");
frame(&ctx, &mut app, Vec::new(), &mut focus);
assert!(!Popup::is_any_open(&ctx), "and the dropdown closed with it");
}
}