emilk / emilk/egui

`ViewportBuilder::with_visible(false)` doesn't work on root viewport

Open
#8,459 2 comments 0 reactions 0 assignees View on GitHub
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:

Image

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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.