`ViewportBuilder::with_visible(false)` doesn't work on root viewport
- Dominant language
- Rust
- Stars
- 30.6k
- Forks
- 2.1k
- Avg merge
- 1d 9h
- Merged PRs (30d)
- 72
Description
### Problem
Consider the example below where a window tab can be torn off and merged into another window, just like in Chromium-based browsers.
When egui's root window with a single tab merges into another window, it has to be destroyed¹, but we can't destroy the root window, so the UI below tries to hide the root window and render all windows as deferred viewports:
This however, still produces a visible root window (the blank "Steady Controller" window in the above gif):
```rust
let native_options = eframe::NativeOptions {
...
viewport: egui::ViewportBuilder::default()
.with_title(app_name)
.with_visible(false) // <-- request not to show the root viewport
.with_active(false)
.with_inner_size([1.0, 1.0]),
..Default::default()
};
```
This happens because `EpiIntegration::post_rendering` unconditionally calls `window.set_visible(true)` after the first rendered frame, overriding the root viewport's `visible(false)` request.
Furthermore, if we minimize the blank root viewport, the remaining GUI will be throttled to 100 ms repaint interval.
### Solution
Ideally eframe supports an opt-in mode where `ViewportId::ROOT` remains the logical application/controller root but never owns a native window or rendering surface.
```rust
let native_options = eframe::NativeOptions {
root_viewport_mode: eframe::RootViewportMode::Windowless,
...
};
```
I have a working prototype in my fork, but it turned into a 2.5k-line change. eframe currently couples the logical root to both the primary winit window and renderer bootstrap, so making it truly windowless affects WGPU/Glow initialization, repaint scheduling, viewport creation, platform output, and zero-window shutdown. Before submitting it, I'd appreciate guidance on whether this should be split into preparatory PRs or kept as one feature PR.
---------------------
1) an alternative slight of hand solution like making the source root window take over all the target window's tabs and state on tab merge doesn't really work seamlessly: at some point during that shenanigan, before the deferred target window can be destroyed, two windows will appear on top of each other with their shadows stacked, resulting in an unwanted visual artifact
Contributor guide
Research direction
Start by tracing EpiIntegration::post_rendering and the root viewport path, then review the mentioned WGPU/Glow initialization, repaint scheduling, platform output, and zero-window shutdown areas. Done means an opt-in windowless root keeps ViewportId::ROOT logical without a native window or rendering surface, while deferred viewports continue rendering and shutdown remains correct.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- rust
- Domain
- desktop-dev
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 25/100