Recently, while using egui
, I encountered a problem where I wanted to hide the window and then bring it up via a tray icon. I initially thought such a simple requirement should be very straightforward, but I found that the official eframe
framework of egui
does not support this functionality well. There has been ongoing discussion about this issue on GitHub, such as in this Issue.
In this article, I will first reproduce the issue with eframe
, and then introduce a solution implemented using egui_glow
and egui_winit
, providing a working example.
Reproducing the Issue#
Prerequisite Dependencies#
Here is my Cargo.toml
file:
[package]
name = "test_egui"
version = "0.1.0"
edition = "2024"
[dependencies]
egui = "0.31.1"
eframe = "0.31.1"
tokio = { version = "1.44.1", features = ["full"] }
Issue Reproduction#
To simplify the problem, I abstracted the issue into this model: Write a Rust program that controls the window to appear or disappear at regular intervals in a background thread.
To achieve this functionality, we can easily write the following code:
struct MyApp {}
impl eframe::App for MyApp {
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
egui::CentralPanel::default().show(ctx, |ui| {
ui.horizontal(|ui| {
ui.label("Hello, world!");
});
});
}
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let rt = tokio::runtime::Runtime::new()?;
let _guard = rt.enter();
eframe::run_native(
"Egui Test App",
Default::default(),
Box::new(|_cc| {
let ctx = _cc.egui_ctx.clone();
rt.spawn(async move {
loop {
println!("Running background task...");
ctx.send_viewport_cmd(egui::ViewportCommand::Visible(false));
ctx.request_repaint();
tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
ctx.send_viewport_cmd(egui::ViewportCommand::Visible(true));
ctx.request_repaint();
tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
}
});
Ok(Box::new(MyApp {}))
}),
)?;
Ok(())
}
Unfortunately, this code does not work as expected. We can see that the window can be hidden in the background thread, but calling ctx.send_viewport_cmd(egui::ViewportCommand::Visible(true));
has no effect. The logs show that the loop is executing normally, but the window does not reappear. This is because eframe
does not handle these events when the window is hidden. According to discussions on GitHub, this is currently an unresolved issue, so we need to temporarily avoid using eframe
and instead use the lower-level libraries egui_glow
and egui_winit
to implement this functionality.
Solution#
If we can handle the winit
event loop ourselves, we can easily customize the behavior of the window. I found an official example: Pure-glow.
Update Dependencies#
Since we are no longer using eframe
, we need to update the dependencies in Cargo.toml
as follows:
[dependencies]
egui = "0.31.1"
egui-winit = "0.31.1"
winit = "0.30.9"
glow = "0.16.0"
egui_glow = {version = "0.31.1", features = ["winit"]}
glutin = "0.32.2"
glutin-winit = "0.5.0"
tokio = { version = "1.44.1", features = ["full"] }
log = "0.4.26"
Modified main
#
fn main() -> Result<(), Box<dyn std::error::Error>> {
let event_loop = winit::event_loop::EventLoop::<event::UserEvent>::with_user_event()
.build()
.unwrap();
let proxy = event_loop.create_proxy();
let rt = tokio::runtime::Runtime::new()?;
let _guard = rt.enter();
let proxy_clone = proxy.clone(); // !NOTICE: clone the proxy for the background task
rt.spawn(async move {
loop {
println!("Running background task...");
proxy_clone
.send_event(event::UserEvent::HideWindow)
.unwrap();
proxy_clone
.send_event(event::UserEvent::Redraw(Duration::ZERO))
.unwrap();
tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
proxy_clone
.send_event(event::UserEvent::ShowWindow)
.unwrap();
proxy_clone
.send_event(event::UserEvent::Redraw(Duration::ZERO))
.unwrap();
tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
}
});
let mut app = app::GlowApp::new(
proxy,
Box::new(|egui_ctx| {
egui::CentralPanel::default().show(egui_ctx, |ui| {
ui.heading("Hello World!");
});
}),
);
event_loop.run_app(&mut app).expect("failed to run app");
Ok(())
}
This is my modified main
function, where the main change is that we use the winit
event loop to handle the window's behavior. In the background thread, we send UserEvent
through EventLoopProxy<UserEvent>
to control the display and hiding of the window.
Event Handling#
Similarly, we need to implement ApplicationHandler<UserEvent>
, where we accept and handle UserEvent
. If you need to manage some state of the application, you can directly add relevant fields (like the AppState
I commented out) in the GlowApp
struct and handle updates in user_event
. Here, I only suggest performing simple data display operations; calculations should be done in the background to avoid affecting UI smoothness. Additionally, you may need to update update_ui
to accept AppState
.
pub struct GlowApp {
proxy: winit::event_loop::EventLoopProxy<UserEvent>,
gl_window: Option<GlutinWindowContext>,
gl: Option<Arc<glow::Context>>,
egui_glow: Option<egui_glow::EguiGlow>,
repaint_delay: std::time::Duration,
clear_color: [f32; 3],
window_hidden: bool,
update_ui: Box<dyn Fn(&egui::Context) + Send + Sync + 'static>,
// state: AppState,
}
impl winit::application::ApplicationHandler<UserEvent> for GlowApp {
// ... skip other methods
fn user_event(&mut self, _event_loop: &winit::event_loop::ActiveEventLoop, event: UserEvent) {
match event {
UserEvent::Redraw(delay) => self.repaint_delay = delay,
UserEvent::ShowWindow => {
self.window_hidden = false;
if let Some(ref gl_window) = self.gl_window {
gl_window.window().set_visible(true);
gl_window.window().request_redraw();
}
}
UserEvent::HideWindow => {
self.window_hidden = true;
if let Some(ref gl_window) = self.gl_window {
gl_window.window().set_visible(false);
}
}
}
}
}
Summary#
At this point, the program window will periodically hide and reappear. If you want to implement tray icon event handling, you can refer to this example to add tray events to our events and then handle them in user_event
.
Complete Code#
Why didn't I present all the code directly above? Of course, it's because it's too long... I believe that understanding the above parts is enough to quickly modify for simple requirements; just copy my (or the official) example. Of course, my code has many small issues and is not suitable for direct production use, so please check carefully before using.
Additionally, I am also trying to build a template for an egui desktop application. Feel free to follow my GitHub page: mcthesw. If successful, I will update relevant information here.