egui
egui copied to clipboard
eFrame: Native system menubar
Is your feature request related to a problem? Please describe. Although users now generally accept non-native UIs, some system integration is often expected or appreciated. I believe system menu bar is one of these things, esp. on macOs
Describe the solution you'd like Native system menu bar integration on the major three platforms: Windows, Mac and Linux. Linux could possibly emulated (or omitted and provided by egui) to avoid a Gtk or Qt dependency, which may not be desired.
Describe alternatives you've considered There is an FLTK egui frame project, but it would be nice to have a native option right in eframe
Additional context The primary driver is macOs since the system menu bar is on the top of the screen. If just that were added, that might be sufficient, but it would be ideal to gain this on windows/linux as well
This is an absolute must for me to consider using eframe on macOS. I started messing about to see if I could make any progress (bear in mind I've never touched this project before and I've only barely worked with the objc2 project before). This is what I've got so far:
It's really not much, but it's something. Here's the code that produced this:
diff --git a/crates/eframe/src/native/app_icon.rs b/crates/eframe/src/native/app_icon.rs
index 840bf367..3569f92a 100644
--- a/crates/eframe/src/native/app_icon.rs
+++ b/crates/eframe/src/native/app_icon.rs
@@ -203,9 +203,12 @@ fn set_title_and_icon_mac(title: &str, icon_data: Option<&IconData>) -> AppIconS
use crate::icon_data::IconDataExt as _;
crate::profile_function!();
- use objc2::ClassType;
- use objc2_app_kit::{NSApplication, NSImage};
- use objc2_foundation::{NSData, NSString};
+ use log::debug;
+ use objc2::{
+ declare_class, msg_send_id, mutability, rc::Retained, sel, ClassType, DeclaredClass,
+ };
+ use objc2_app_kit::{NSApplication, NSImage, NSMenu, NSMenuItem};
+ use objc2_foundation::{ns_string, MainThreadMarker, NSData, NSObject, NSString};
let png_bytes = if let Some(icon_data) = icon_data {
match icon_data.to_png_bytes() {
@@ -250,6 +253,58 @@ fn set_title_and_icon_mac(title: &str, icon_data: Option<&IconData>) -> AppIconS
app_menu.setTitle(&NSString::from_str(title));
}
}
+
+ let test_menu_item = main_menu.addItemWithTitle_action_keyEquivalent(
+ ns_string!(""),
+ None,
+ ns_string!(""),
+ );
+ let mtm = MainThreadMarker::new().expect("must be on the main thread");
+ let test_menu = NSMenu::initWithTitle(mtm.alloc::<NSMenu>(), ns_string!("Test Menu"));
+
+ declare_class! {
+ struct TestMenuObject;
+
+ unsafe impl ClassType for TestMenuObject {
+ type Super = NSObject;
+ type Mutability = mutability::InteriorMutable;
+ const NAME: &'static str = "TestMenuObject";
+ }
+
+ impl DeclaredClass for TestMenuObject {}
+
+ unsafe impl TestMenuObject {
+ #[method(testButton1)]
+ fn __test_button_1() {
+ debug!("Test Button 1 clicked");
+ }
+
+ #[method(testButton2)]
+ fn __test_button_2() {
+ debug!("Test Button 2 clicked");
+ }
+ }
+ }
+
+ let test_menu_object: Retained<TestMenuObject> =
+ msg_send_id![TestMenuObject::alloc(), init];
+ let test_menu_button_1 = test_menu.addItemWithTitle_action_keyEquivalent(
+ ns_string!("Test Button 1"),
+ Some(sel!(testButton1)),
+ ns_string!("1"),
+ );
+
+ test_menu_button_1.setTarget(Some(&test_menu_object));
+ test_menu.addItem(&NSMenuItem::separatorItem(mtm));
+
+ let test_menu_button_2 = test_menu.addItemWithTitle_action_keyEquivalent(
+ ns_string!("Test Button 2"),
+ Some(sel!(testButton2)),
+ ns_string!("2"),
+ );
+
+ test_menu_button_2.setTarget(Some(&test_menu_object));
+ test_menu_item.setSubmenu(Some(&test_menu));
}
// The title in the Dock apparently can't be changed.
Unfortunately, you can't click these test menu buttons, I haven't managed to figure out how to get that to work, I'm tired and I want to go to bed now.
I don't really plan to be the one to actually implement the functionality described in this issue as I feel I lack the expertise necessary, but I want to draw attention to this issue, I want to bump it a little bit, I really want this to happen.
I have attained... partial functionality!
I think I understand what I need to do to have custom actions on menu items, but I'm not sure how exactly to do it. I think I need to add a method to the class conforming to NSApplicationDelegate, but it looks like to me that eframe doesn't really handle the specific creation of such a class itself. That seems to be delegated (pun unintended) to, I would assume, winit.
Any reason why this functionality would need to live in egui or eframe?
Any reason why this functionality would need to live in egui or eframe?
I think since the NSApp is created by egui, all AppDelegates need to be handled there and cannot be handled outside. That is my understanding, but I might be wrong.
I think this is handled in the function where the Icon is set: https://github.com/emilk/egui/blob/cf965aaa30987a5b6fa2380f37c0ce8cb869347d/crates/eframe/src/native/app_icon.rs#L202
Any reason why this functionality would need to live in egui or eframe?
Is there any other alternative? to achieving this? this is a big deal for me to finally use egui
@valentinegb how did you implement this?
@valentinegb how did you implement this?
I don't really remember, sorry, it's been too long for me ^^'
as far as I know, as soon as egui upgrades winit to 0.31, NSApp main thread handler will be available and the user can do everything without having to interfere with egui.
See here: https://github.com/emilk/egui/issues/5620
and the winit discussion: https://github.com/rust-windowing/winit/issues/4260
Here how I got it working with all the options in macos
//main.rs
#[derive()]
struct App {
// The object needs to be alive for the lifetime of the app
#[cfg(target_os = "macos")]
menu_handler: Option<objc2::rc::Retained<AppMenuHandler>>,
}
impl App {
pub fn new() -> Self {
Self {
#[cfg(target_os = "macos")]
menu_handler: None,
}
}
}
pub async fn run() -> std::result::Result<(), eframe::Error> {
let options = eframe::NativeOptions {
viewport: eframe::egui::ViewportBuilder::default()
.with_resizable(true)
.with_active(true)
.with_fullsize_content_view(false)
.with_icon(IconData::default())
.with_inner_size([1200.0, 650.0])
.with_min_inner_size([800.0, 600.0])
.with_title_shown(true)
.with_mouse_passthrough(false),
centered: true,
..Default::default()
};
#[cfg(target_os = "macos")]
let handle = make_handler().unwrap();
eframe::run_native(
"App",
options,
Box::new(|_cc| {
#[cfg(target_os = "macos")]
create_global_menu(handle);
let mut app = Box::new(App::new());
#[cfg(target_os = "macos")]
{
app.menu_handler = handle;
}
Ok(app)
}),
)
}
//create_menu.rs
use objc2::rc::Retained;
use objc2::runtime::AnyObject;
use objc2::{ClassType, define_class, msg_send, sel};
use objc2_app_kit::NSApp;
use objc2_app_kit::{NSMenu, NSMenuItem};
use objc2_foundation::{MainThreadMarker, NSObject, NSString};
#[expect(unsafe_code)]
pub fn create_global_menu(handler: Retained<AppMenuHandler>) {
unsafe {
let mtm = MainThreadMarker::new().expect("must be on main thread");
let app = NSApp(mtm);
// Change the title in the top bar - for python processes this would be again "python" otherwise.
if let Some(main_menu) = app.mainMenu()
&& let Some(item) = main_menu.itemAtIndex(0)
&& let Some(app_menu) = item.submenu()
{
//File menu
let file_menu_item = NSMenuItem::new(mtm);
main_menu.addItem(&file_menu_item);
let file_menu = NSMenu::initWithTitle(mtm.alloc(), &NSString::from_str("File"));
let open_item = NSMenuItem::initWithTitle_action_keyEquivalent(
mtm.alloc(),
&NSString::from_str("Open…"),
Some(sel!(openDocument:)),
&NSString::from_str("o"),
);
open_item.setTarget(Some(handler.as_ref() as &objc2::runtime::AnyObject));
let save_item = NSMenuItem::initWithTitle_action_keyEquivalent(
mtm.alloc(),
&NSString::from_str("Save"),
Some(sel!(saveDocument:)),
&NSString::from_str("s"),
);
save_item.setTarget(Some(handler.as_ref() as &objc2::runtime::AnyObject)); // Add this line!
let close_item = NSMenuItem::initWithTitle_action_keyEquivalent(
mtm.alloc(),
&NSString::from_str("Close"),
Some(sel!(performClose:)),
&NSString::from_str("w"),
);
file_menu.addItem(&save_item);
file_menu.addItem(&open_item);
file_menu.addItem(&NSMenuItem::separatorItem(mtm));
file_menu.addItem(&close_item);
file_menu_item.setSubmenu(Some(&file_menu));
//Edit menu
let edit_menu_item = NSMenuItem::new(mtm);
main_menu.addItem(&edit_menu_item);
let edit_menu = NSMenu::initWithTitle(mtm.alloc(), &NSString::from_str("Edit"));
edit_menu.addItem(&NSMenuItem::initWithTitle_action_keyEquivalent(
mtm.alloc(),
&NSString::from_str("Undo"),
Some(sel!(undo:)),
&NSString::from_str("z"),
));
edit_menu.addItem(&NSMenuItem::initWithTitle_action_keyEquivalent(
mtm.alloc(),
&NSString::from_str("Redo"),
Some(sel!(redo:)),
&NSString::from_str("Z"),
));
edit_menu.addItem(&NSMenuItem::separatorItem(mtm));
edit_menu.addItem(&NSMenuItem::initWithTitle_action_keyEquivalent(
mtm.alloc(),
&NSString::from_str("Cut"),
Some(sel!(cut:)),
&NSString::from_str("x"),
));
edit_menu.addItem(&NSMenuItem::initWithTitle_action_keyEquivalent(
mtm.alloc(),
&NSString::from_str("Copy"),
Some(sel!(copy:)),
&NSString::from_str("c"),
));
edit_menu.addItem(&NSMenuItem::initWithTitle_action_keyEquivalent(
mtm.alloc(),
&NSString::from_str("Paste"),
Some(sel!(paste:)),
&NSString::from_str("v"),
));
edit_menu.addItem(&NSMenuItem::initWithTitle_action_keyEquivalent(
mtm.alloc(),
&NSString::from_str("Select All"),
Some(sel!(selectAll:)),
&NSString::from_str("a"),
));
edit_menu.addItem(&NSMenuItem::initWithTitle_action_keyEquivalent(
mtm.alloc(),
&NSString::from_str("Select All"),
Some(sel!(selectAll:)),
&NSString::from_str("d"),
));
edit_menu_item.setSubmenu(Some(&edit_menu));
//View menu
let view_menu_item = NSMenuItem::new(mtm);
main_menu.addItem(&view_menu_item);
let view_menu = NSMenu::initWithTitle(mtm.alloc(), &NSString::from_str("View"));
view_menu.addItem(&NSMenuItem::initWithTitle_action_keyEquivalent(
mtm.alloc(),
&NSString::from_str("Enter Full Screen"),
Some(sel!(toggleFullScreen:)),
&NSString::from_str("f"),
));
view_menu_item.setSubmenu(Some(&view_menu));
//Window menu
let window_menu_item = NSMenuItem::new(mtm);
main_menu.addItem(&window_menu_item);
let window_menu = NSMenu::initWithTitle(mtm.alloc(), &NSString::from_str("Window"));
window_menu.addItem(&NSMenuItem::initWithTitle_action_keyEquivalent(
mtm.alloc(),
&NSString::from_str("Minimize"),
Some(sel!(performMiniaturize:)),
&NSString::from_str("m"),
));
window_menu.addItem(&NSMenuItem::initWithTitle_action_keyEquivalent(
mtm.alloc(),
&NSString::from_str("Zoom"),
Some(sel!(performZoom:)),
&NSString::new(),
));
window_menu_item.setSubmenu(Some(&window_menu));
//Help menu
let help_menu_item = NSMenuItem::new(mtm);
main_menu.addItem(&help_menu_item);
let help_menu = NSMenu::initWithTitle(mtm.alloc(), &NSString::from_str("Help"));
let help_item = NSMenuItem::initWithTitle_action_keyEquivalent(
mtm.alloc(),
&NSString::from_str("Help"),
Some(sel!(showHelp:)),
&NSString::from_str("?"),
);
help_menu.addItem(&help_item);
help_menu_item.setSubmenu(Some(&help_menu));
}
}
}
//This class is used to override the existing methods for the menu items
define_class!(
#[unsafe(super(NSObject))]
#[name = "AppMenuHandler"]
pub struct AppMenuHandler;
impl AppMenuHandler {
#[unsafe(method(init))]
fn init(&self) -> *mut Self {
unsafe { objc2::msg_send![super(self), init] }
}
#[unsafe(method(openDocument:))]
fn open_document(&self, _sender: &AnyObject) {
println!("Open menu item clicked!");
}
#[unsafe(method(saveDocument:))]
fn save_document(&self, _sender: &AnyObject) {
println!("Save menu item clicked!");
}
#[unsafe(method(validateMenuItem:))]
fn validate_menu_item(&self, _item: &NSMenuItem) -> bool {
true
}
}
);
pub fn make_handler() -> Option<Retained<AppMenuHandler>> {
unsafe {
let obj: *mut AppMenuHandler = msg_send![AppMenuHandler::class(), alloc];
Retained::from_raw(msg_send![obj, init])
}
}
This is an example using muda.
use egui::IconData;
use muda::Menu;
use muda::{
MenuEvent, MenuItem, PredefinedMenuItem, Submenu,
accelerator::{Accelerator, Code, Modifiers},
};
mod modules;
mod state;
#[derive()]
struct App {
menu: Option<Menu>,
menu_rx: Option<std::sync::mpsc::Receiver<MenuEvent>>,
}
impl App {
pub fn new() -> Self {
Self {
menu: None,
menu_rx: None,
}
}
fn handle_menu_event(&mut self, event: MenuEvent) {
// Get the menu item ID
let id = event.id;
// You'll need to store menu item IDs to identify which was clicked
// For now, you can print to see what's being clicked
println!("Menu event received: {id:?}");
// Handle specific menu items
// Example:
// if id == self.save_item_id {
// // Handle save
// }
}
}
impl eframe::App for App {
fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) {
// Poll menu events
// Take the receiver temporarily
if let Some(rx) = self.menu_rx.take() {
while let Ok(event) = rx.try_recv() {
self.handle_menu_event(event);
}
// Put it back
self.menu_rx = Some(rx);
}
egui::CentralPanel::default().show(ctx, |ui| {
ui.centered_and_justified(|ui| {
ui.label("Muda Test.");
});
});
}
}
pub async fn run() -> std::result::Result<(), eframe::Error> {
let options = eframe::NativeOptions {
viewport: eframe::egui::ViewportBuilder::default()
.with_resizable(true)
.with_active(true)
.with_fullsize_content_view(false)
.with_icon(IconData::default())
.with_inner_size([1200.0, 650.0])
.with_min_inner_size([800.0, 600.0])
.with_title_shown(true)
.with_mouse_passthrough(false),
centered: true,
..Default::default()
};
#[cfg(target_os = "macos")]
eframe::run_native(
"App",
options,
Box::new(move |_cc| {
let mut app = Box::new(App::new());
if app.menu.is_none() {
let m = create_menu();
m.0.init_for_nsapp(); // <-- now safe: NSApp is ready
app.menu = Some(m.0);
app.menu_rx = Some(m.1);
}
Ok(app)
}),
)
}
#[cfg(target_os = "macos")]
fn create_menu() -> (Menu, std::sync::mpsc::Receiver<MenuEvent>) {
use muda::MenuEvent;
let menu = Menu::new();
// Set up menu event channel
let (tx, rx) = std::sync::mpsc::channel();
MenuEvent::set_event_handler(Some(move |event| {
let _ = tx.send(event);
}));
// App menu (first menu with app name)
let app_menu = Submenu::new("App", true);
app_menu
.append(&PredefinedMenuItem::about(None, None))
.unwrap();
app_menu.append(&PredefinedMenuItem::separator()).unwrap();
app_menu
.append(&PredefinedMenuItem::services(None))
.unwrap();
app_menu.append(&PredefinedMenuItem::separator()).unwrap();
app_menu.append(&PredefinedMenuItem::hide(None)).unwrap();
app_menu
.append(&PredefinedMenuItem::hide_others(None))
.unwrap();
app_menu
.append(&PredefinedMenuItem::show_all(None))
.unwrap();
app_menu.append(&PredefinedMenuItem::separator()).unwrap();
app_menu.append(&PredefinedMenuItem::quit(None)).unwrap();
menu.append(&app_menu).unwrap();
// File menu
let file_menu = Submenu::new("File", true);
let new_item = MenuItem::new("New", true, None);
let open_item = MenuItem::new("Open", true, None);
let save_item = MenuItem::new(
"Save",
true,
Some(Accelerator::new(Some(Modifiers::CONTROL), Code::KeyS)),
);
file_menu.append(&new_item).unwrap();
file_menu.append(&open_item).unwrap();
file_menu.append(&save_item).unwrap();
file_menu.append(&PredefinedMenuItem::separator()).unwrap();
file_menu
.append(&PredefinedMenuItem::close_window(None))
.unwrap();
menu.append(&file_menu).unwrap();
// Edit menu
let edit_menu = Submenu::new("Edit", true);
edit_menu.append(&PredefinedMenuItem::undo(None)).unwrap();
edit_menu.append(&PredefinedMenuItem::redo(None)).unwrap();
edit_menu.append(&PredefinedMenuItem::separator()).unwrap();
edit_menu.append(&PredefinedMenuItem::cut(None)).unwrap();
edit_menu.append(&PredefinedMenuItem::copy(None)).unwrap();
edit_menu.append(&PredefinedMenuItem::paste(None)).unwrap();
edit_menu
.append(&PredefinedMenuItem::select_all(None))
.unwrap();
menu.append(&edit_menu).unwrap();
// Window menu
let window_menu = Submenu::new("Window", true);
window_menu
.append(&PredefinedMenuItem::minimize(None))
.unwrap();
window_menu
.append(&PredefinedMenuItem::maximize(None))
.unwrap();
window_menu
.append(&PredefinedMenuItem::separator())
.unwrap();
window_menu
.append(&PredefinedMenuItem::fullscreen(None))
.unwrap();
menu.append(&window_menu).unwrap();
(menu, rx)
}
@razein97 I really appreciate the examples, they've been very helpful.
I have noticed that one will want to call egui::Context::request_repaint() (passing in cc.egui_ctx.clone() to the menu creation function from eframe::run_native's callback works well) after sending an event to the window from the menu (ie: right after the tx.send() in the second example). Otherwise it can be some time before the window is updated. It's noticable on macos when using the menus with the cursor and avoiding moving the cursor after clicking the menu entry.