use super::bridge;
use super::model::{self, MenuEntry, MenuItemSpec, MenuSection};
use super::state::MenuState;
use super::sync::MenuSync;
use crate::profile::ProfileManager;
use egui::containers::menu::{MenuButton, SubMenuButton};
use par_term_config::KeyBinding;
use par_term_keybindings::KeybindingRegistry;
const TRIGGER_GLYPH: &str = "\u{2630}";
const TRIGGER_GLYPH_SIZE: f32 = 13.0;
const MENU_MIN_WIDTH: f32 = 120.0;
const SUBMENU_MIN_WIDTH: f32 = 230.0;
pub const ENABLE_ENV_VAR: &str = "PAR_TERM_IN_APP_MENU";
const HAS_NATIVE_MENU_BAR: bool = cfg!(any(target_os = "macos", target_os = "windows"));
pub struct AppMenuUi {
sections: Vec<MenuSection>,
built: MenuSync,
registry: KeybindingRegistry,
state: MenuState,
open: bool,
}
impl Default for AppMenuUi {
fn default() -> Self {
Self::new()
}
}
impl AppMenuUi {
pub const BUTTON_WIDTH: f32 = 24.0;
pub fn new_with(keybindings: &[KeyBinding]) -> Self {
let mut built = MenuSync::new();
built.record(keybindings, None);
Self {
sections: model::menu_model_with(false, keybindings),
built,
registry: KeybindingRegistry::from_config(keybindings),
state: MenuState::default(),
open: false,
}
}
pub fn new() -> Self {
Self::new_with(&par_term_config::Config::default().keybindings)
}
pub fn sync(&mut self, keybindings: &[KeyBinding], state: MenuState) -> bool {
let rebuilt = self.built.rebuild_reason(keybindings, &None).is_some();
if rebuilt {
self.sections = model::menu_model_with(false, keybindings);
self.registry = KeybindingRegistry::from_config(keybindings);
self.built.record(keybindings, None);
}
self.state = state;
rebuilt
}
pub fn sections(&self) -> &[MenuSection] {
&self.sections
}
pub fn enabled() -> bool {
use std::sync::OnceLock;
static ENABLED: OnceLock<bool> = OnceLock::new();
*ENABLED
.get_or_init(|| enabled_with_override(std::env::var(ENABLE_ENV_VAR).ok().as_deref()))
}
pub fn is_open(&self) -> bool {
self.open
}
pub fn hide(&mut self, ctx: &egui::Context) {
let _ = bridge::take_toggle_request();
if self.open {
egui::Popup::close_all(ctx);
self.open = false;
}
}
pub fn show(&mut self, ui: &mut egui::Ui, profiles: &ProfileManager, height: f32) {
let button = egui::Button::new(
egui::RichText::new(TRIGGER_GLYPH)
.size(TRIGGER_GLYPH_SIZE)
.color(ui.visuals().weak_text_color()),
)
.min_size(egui::vec2(Self::BUTTON_WIDTH, height))
.fill(egui::Color32::TRANSPARENT);
let (response, inner) = MenuButton::from_button(button).ui(ui, |ui| {
ui.set_min_width(MENU_MIN_WIDTH);
for section in &self.sections {
SubMenuButton::new(section.title).ui(ui, |ui| {
ui.set_min_width(SUBMENU_MIN_WIDTH);
draw_entries(ui, §ion.entries, &self.state, profiles, &self.registry);
});
}
});
self.open = inner.is_some();
if bridge::take_toggle_request() {
egui::Popup::toggle_id(ui.ctx(), egui::Popup::default_response_id(&response));
ui.ctx().request_repaint();
}
if response.hovered() {
response.on_hover_text("Menu");
}
}
}
fn draw_entries(
ui: &mut egui::Ui,
entries: &[MenuEntry],
state: &MenuState,
profiles: &ProfileManager,
registry: &KeybindingRegistry,
) {
for entry in entries {
match entry {
MenuEntry::Separator => {
ui.separator();
}
MenuEntry::Item(spec) => draw_item(ui, spec, state),
MenuEntry::Submenu(sub) => {
ui.add_enabled_ui(state.satisfies(sub.requires), |ui| {
SubMenuButton::new(sub.title).ui(ui, |ui| {
ui.set_min_width(SUBMENU_MIN_WIDTH);
draw_entries(ui, &sub.entries, state, profiles, registry);
});
});
}
MenuEntry::Profiles => {
let entries = model::profile_entries(profiles.profiles_ordered(), registry);
draw_dynamic(ui, &entries, state);
if !entries.is_empty() {
ui.separator();
}
}
MenuEntry::Arrangements => {
draw_dynamic(
ui,
&model::arrangement_entries_from(&state.arrangements),
state,
);
}
MenuEntry::BringAllToFront => {}
}
}
}
fn draw_item(ui: &mut egui::Ui, spec: &MenuItemSpec, state: &MenuState) {
let label = state.label(spec);
let mut button = match spec.check {
Some(check) => egui::Button::selectable(state.checked(check), label.as_ref()),
None => egui::Button::new(label.as_ref()),
};
if let Some(accelerator) = &spec.accelerator {
button = button.shortcut_text(model::accelerator_label(accelerator));
}
if ui.add_enabled(state.enabled(spec), button).clicked() {
bridge::dispatch(spec.action);
}
}
fn draw_dynamic(ui: &mut egui::Ui, entries: &[model::DynamicEntry], state: &MenuState) {
for entry in entries {
let mut button = egui::Button::new(&entry.label);
if let Some(accelerator) = &entry.accelerator {
button = button.shortcut_text(model::accelerator_label(accelerator));
}
if ui.add_enabled(state.dynamic_enabled(), button).clicked() {
bridge::dispatch(entry.action);
}
}
}
fn enabled_with_override(value: Option<&str>) -> bool {
match value.map(str::trim) {
Some("1" | "true" | "on" | "yes") => true,
Some("0" | "false" | "off" | "no") => false,
_ => !HAS_NATIVE_MENU_BAR,
}
}
#[cfg(test)]
#[path = "egui_menu_tests.rs"]
mod tests;