Eframe application window does not close
Describe the bug There is bug with eframe where the application window remains open and cannot be closed while the program is running. If the previous window is left in this state for too long, the program is reported as not responding, even though code execution that does not involve the GUI still continues.
The application window is removed when the entire program exits. Alternatively, calling the "run_native" method replaces the previous window with a new one.
To Reproduce Steps to reproduce the behavior:
- Clone the repository that demonstrates the issue: https://github.com/tye-exe/egui_no_close .
- Run the application with
cargo run. - Attempt to close the window by clicking the "x" button.
- The window will remain open.
(The example program must be stopped via "Ctrl+C")
Expected behavior There should be no eframe window open after it is closed.
Video Example See here (window is replaced via calling "run_native").
Desktop:
- OS: NixOS 24.11 (to reproduce entire system see my config)
- Browser N/A
- Version N/A
Additional context Attempting to update to winit master branch (currently https://github.com/rust-windowing/winit/tree/cdbdd974fbf79b82b3fb1a4bc84ed717312a3bd2) did not resolve this issue.
I am currently trying to resolve this issue in eframe, but progress is slow as i'm unsure as to what i am doing.
I know that this is possible, as running the winit run on demand example shows the expected behavior.
Any additional help would be appreciated.
A fun thing that i found out is that this code: https://github.com/emilk/egui/blob/81b7e7f05a6b03fa2cd5bdc6d4ce5f598e16c628/crates/eframe/src/native/glow_integration.rs#L793-L798 does not get run on window termination.
This is because self.integration.should_close() is false when it is run the first (and only) time.
I've got some output logs showing this (ignore the random prints, i'm just trying anything):
[2025-05-17T20:20:45Z DEBUG eframe::native::glow_integration] Received WindowEvent::CloseRequested for viewport Some("FFFF")
I am either a sub-viewport or epi is not ready to close
ROOT Viewport ID: "FFFF"
My Viewport ID: "FFFF"
Am i root: true
event
[2025-05-17T20:20:45Z DEBUG eframe::native::epi_integration] Closing root viewport (ViewportCommand::CancelClose was not sent)
Closing root viewport (ViewportCommand::CancelClose was not sent)
Exiting winit app
Exit event loop
[2025-05-17T20:20:45Z DEBUG eframe::native::run] Asking to exit event loop…
[2025-05-17T20:20:45Z DEBUG eframe::native::run] eframe window closed
SystemTime { tv_sec: 1747513245, tv_nsec: 857124481 } Window Dropped
[2025-05-17T20:20:45Z WARN egui_glow::painter] You forgot to call destroy() on the egui glow painter. Resources will leak!
^C⏎
I'm not sure if it is at all relevant, but it might be useful to note down so here i am.
I have also found out that this issue does not seem to be a new one, but rather a sort of zombie bug that keeps appearing and disappearing. It was apparently patched in issue https://github.com/emilk/egui/issues/2892.
I tried to run an application with this version of egui and eframe, but when i closed it i got this output:
Output
warning: queue 0x7fd64c000ca0 destroyed while proxies still attached:
zwp_primary_selection_offer_v1#4278190081 still attached
wl_data_offer#4278190080 still attached
zwp_primary_selection_device_v1#39 still attached
zwp_primary_selection_device_manager_v1#22 still attached
wl_data_device#38 still attached
wl_data_device_manager#37 still attached
wl_seat#36 still attached
wl_registry#34 still attached
And when the window tried to reopen i got: fish: Job 1, 'cargo run' terminated by signal SIGSEGV (Address boundary error.
The latest version that does not suffer from the error issue on my computer is 0.25.0 version. However, this still has the current no close bug.
I've had some potential progress attempting to recreate this issue.
That is by holding a strong reference to the Window struct (as it is contained within an Arc), this keeps the window open while the other strong reference is held. The resulting behavior between this and what occurs in eframe is very similar. I will be focusing my efforts on trying to ensure that that there are no references in eframe to the window that persist after control is returned.
Example code using winit template
This example won't run on it's own. Modify the winit template if you wish to try this yourself.
#![allow(clippy::single_match)]
use std::sync::Arc;
use std::time::Duration;
use winit::application::ApplicationHandler;
use winit::event::WindowEvent;
use winit::event_loop::{ActiveEventLoop, EventLoop};
use winit::platform::run_on_demand::EventLoopExtRunOnDemand;
use winit::window::{Window, WindowId};
use crate::fill;
#[derive(Default)]
pub struct App {
idx: usize,
window_id: Option<WindowId>,
/// Modified to be an Arc
window: Option<Arc<Window>>,
}
impl<T: 'static> ApplicationHandler<T> for App {
fn about_to_wait(&mut self, _event_loop: &ActiveEventLoop) {
if let Some(window) = self.window.as_ref() {
window.request_redraw();
}
}
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
let window_attributes = Window::default_attributes()
.with_title("Fantastic window!")
.with_inner_size(winit::dpi::LogicalSize::new(128.0, 128.0));
let window = event_loop.create_window(window_attributes).unwrap();
self.window_id = Some(window.id());
self.window = Some(window.into());
// Thread holds lock
let win = self.window.clone();
std::thread::spawn(move || {
let id = std::thread::current().id();
println!("Grabbed Window: {} : id {id:?}", win.is_some());
std::thread::sleep(Duration::from_secs(20));
println!("Dropping Grab: {} : id {id:?}", win.is_some());
});
}
fn window_event(
&mut self,
event_loop: &ActiveEventLoop,
window_id: WindowId,
event: WindowEvent,
) {
if event == WindowEvent::Destroyed && self.window_id == Some(window_id) {
log::info!("Window {} Destroyed", self.idx);
self.window_id = None;
event_loop.exit();
return;
}
let window = match self.window.as_mut() {
Some(window) => window,
None => return,
};
match event {
WindowEvent::CloseRequested => {
log::info!("Window {} CloseRequested", self.idx);
fill::cleanup_window(window);
self.window = None;
}
WindowEvent::RedrawRequested => {
fill::fill_window(window);
}
_ => (),
}
}
}
pub fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut event_loop = EventLoop::new().unwrap();
let mut app = App {
idx: 1,
..Default::default()
};
loop {
event_loop.run_app_on_demand(&mut app)?;
println!("Waiting 1 second");
std::thread::sleep(Duration::from_secs(1));
}
}
I modified WinitApp::window() to return Option<Weak<Window>>, but this did not solve the issue.
I will try modifying other parts of the code.
Modified section:
https://github.com/emilk/egui/blob/81b7e7f05a6b03fa2cd5bdc6d4ce5f598e16c628/crates/eframe/src/native/winit_integration.rs#L66-L69
I'm running into this issue with MacOS as well. The window just hangs as the application continues.
Good to know it's not just me experiencing this issue.
I'll be happy to share any resources that i have if you're able to contribute at all.
As a further update, i changed the Viewport data structure to only allow for weak references to be given out from the window: Option<Arc<Window>> attribute.
(I did this by moving it into a separate module and only exposing getter functions).
Viewport Modifications
There are some extra methods for modifying a window, for convince.use std::sync::{Arc, Weak};
use ahash::HashSet;
use egui::{
DeferredViewportUiCallback, ViewportBuilder, ViewportClass, ViewportIdPair, ViewportInfo,
};
use winit::window::Window;
pub(crate) struct Viewport {
pub(crate) ids: ViewportIdPair,
pub(crate) class: ViewportClass,
pub(crate) builder: ViewportBuilder,
pub(crate) deferred_commands: Vec<egui::viewport::ViewportCommand>,
pub(crate) info: ViewportInfo,
pub(crate) actions_requested: HashSet<egui_winit::ActionRequested>,
/// The user-callback that shows the ui.
/// None for immediate viewports.
pub(crate) viewport_ui_cb: Option<Arc<DeferredViewportUiCallback>>,
// These three live and die together.
// TODO(emilk): clump them together into one struct!
pub(crate) gl_surface: Option<glutin::surface::Surface<glutin::surface::WindowSurface>>,
window: Option<Arc<Window>>,
pub(crate) egui_winit: Option<egui_winit::State>,
}
impl Viewport {
pub(crate) fn new(
ids: ViewportIdPair,
class: ViewportClass,
builder: ViewportBuilder,
deferred_commands: Vec<egui::viewport::ViewportCommand>,
info: ViewportInfo,
actions_requested: HashSet<egui_winit::ActionRequested>,
viewport_ui_cb: Option<Arc<DeferredViewportUiCallback>>,
gl_surface: Option<glutin::surface::Surface<glutin::surface::WindowSurface>>,
window: Option<Arc<Window>>,
egui_winit: Option<egui_winit::State>,
) -> Self {
Self {
ids,
class,
builder,
deferred_commands,
info,
actions_requested,
viewport_ui_cb,
gl_surface,
window,
egui_winit,
}
}
pub fn window(&self) -> Option<Weak<Window>> {
self.window.as_ref().map(|win| Arc::downgrade(win))
}
pub fn window_ref(&self) -> Option<Arc<Window>> {
self.window()
.as_ref()
.map(|win| Weak::upgrade(win))
.flatten()
}
/// Yes this could leak a strong reference, but this is a proof of concept to get it working enough, not working perfect.
pub fn window_modify(&mut self, callback: impl FnOnce(&mut Arc<Window>) -> ()) {
if let Some(ref mut win) = self.window {
callback(win)
}
}
pub fn window_set_none(&mut self) {
self.window = None;
}
}
However, this also did not solve the issue.
I have created a stripped-down version of eframe that i will do further testing on. This reduces the amount of code that i need to examine, so hopefully the issue will be easier to find.
https://github.com/tye-exe/eframe_rebuilt
In regard to the Window being dropped, I can verify that it is dropped when a close is requested by adding a print statement to impl Drop for Window on a local copy of winit and checking the output.
This shows that the window is dropped when expected.
Git diff for anyone interested
diff --git a/src/window.rs b/src/window.rs
index 0db22bb3..d42935ff 100644
--- a/src/window.rs
+++ b/src/window.rs
@@ -1,5 +1,6 @@
//! The [`Window`] struct and associated types.
use std::fmt;
+use std::time::SystemTime;
use crate::dpi::{PhysicalPosition, PhysicalSize, Position, Size};
use crate::error::{ExternalError, NotSupportedError};
@@ -55,7 +56,9 @@ impl Drop for Window {
if let Some(Fullscreen::Exclusive(_)) = w.fullscreen().map(|f| f.into()) {
w.set_fullscreen(None);
}
- })
+ });
+
+ println!("{:?} Window Dropped", SystemTime::now());
}
}
Which gives the output of this video:
https://github.com/user-attachments/assets/5b230410-8ad3-42cc-8711-3e774a59130e
So it's safe to say that the any reference to the Window is not the problem.
@tye-exe is it possible that the OS's window manager expects that, once a window is attached, the process must terminate before it closes the last window (to prevent zombie processes)?
Potentially, but eframe should not need to do this, as the winit run on demand example demonstrates that the desired behavior is possible.
https://github.com/user-attachments/assets/e9d675bb-c53b-407b-b096-714020cc5cf2
(cargo warning is a remnant of the drop logging)
Thanks for working on this!
I observed the same problem on Windows and on Linux (Fedora 42 with KDE).
A workaround for Linux is to force X11. Obviously this doesn't work on Windows.
Another workaround that I found was to run the window in a spawned thread and then wait for the thread to terminate. This seems to work on Linux and for Windows. Though, I need to call with_any_thread for each platform. Here a complete example for Linux with Wayland:
use eframe::egui;
fn main() {
std::thread::spawn(move || {
let options = eframe::NativeOptions {
event_loop_builder: Some(Box::new(move |builder| {
use winit::platform::wayland::EventLoopBuilderExtWayland;
builder.with_any_thread(true);
})),
..Default::default()
};
eframe::run_simple_native("Test", options, move |ctx, _frame| {
egui::CentralPanel::default().show(ctx, |ui| {
ui.label(format!("Close this window"));
});
})
.unwrap();
})
.join()
.unwrap();
loop {
println!("Window is closed");
std::thread::sleep(std::time::Duration::from_millis(1000));
}
}
I tried adapting what you suggested, and it did work if you only need to open and close the window once (at least on my stripped down version of eframe), but if you need to do this multiple times, then it gives the error: called 'Result::unwrap()' on an 'Err' value: WinitEventLoop(RecreationAttempt).
You can see my adaption here: https://github.com/tye-exe/eframe_rebuilt/blob/2fa6684b79f23130ac726d63f8cfd50c519693e8/threaded_app/src/main.rs
Inline:
// Code adapted from: https://github.com/emilk/egui/issues/6757#issuecomment-2980399010
fn main() {
env_logger::init();
loop {
spawn_gui();
std::thread::sleep(std::time::Duration::from_secs(1));
}
}
fn spawn_gui() {
std::thread::spawn(move || {
let options = eframe_rebuilt::epi::NativeOptions {
event_loop_builder: Some(Box::new(move |builder| {
use winit::platform::wayland::EventLoopBuilderExtWayland;
builder.with_any_thread(true);
})),
..Default::default()
};
eframe_rebuilt::run_native("test", options, Box::new(|_| Ok(Box::new(MyApp {})))).unwrap();
})
.join()
.unwrap();
}
struct MyApp {}
impl eframe_rebuilt::epi::App for MyApp {
fn update(&mut self, ctx: &egui::Context, frame: &mut eframe_rebuilt::epi::Frame) {
//
}
}
I made a script that compares the outputs of running eframe on the main thread and running it on a spawned thread, and the output is almost identical, so at a surface level the difference between it closing instantly and closing late is not currently output by the logs.
See my recreation repository for generating logs to compare: https://github.com/tye-exe/eframe_rebuilt/tree/5246848056d01d2c457b184bbca253a9a58fd50c
After testing on both Wayland and X11, I have found that the window closes as expected on X11, but lingers on Wayland. I will focus my efforts on investigating Wayland specific code.
I had to take a break investigating this issue due to life getting busy. I will be able to dedicate more time now.
A current work-around is to force winit to compile to X11 via unsetting the "WAYLAND_DISPLAY" environment variable (or setting it to nothing). The application can then run on Wayland using an X11 compatibility layer, such as XWayland, which allows for the window to close normally.
I have also investigated when a decent selection of the references counted attributes in ./crates/eframe/src/native/glow_integration.rs (this file), which all seem to be dropped when the window is "closed", so the issue might be the finial frame not being cleaned up rather than a dangling reference. I go into more detail in my blog post.
I have found a solution. See my PR for more information