mirror of
https://github.com/emilk/egui.git
synced 2026-06-26 14:49:06 -04:00
Add ViewportBuilder::with_monitor + ViewportCommand::SetMonitor (#8140)
## Summary
Adds two paired API entry-points that let an integration target a
specific monitor at viewport creation time, or move an existing viewport
to a different monitor at runtime, in a way that works portably on
Wayland.
```rust
// At creation
ViewportBuilder::default()
.with_inner_size([1920.0, 1080.0])
.with_monitor(playfield_idx) // ← new
.with_decorations(false)
// Or at runtime
ctx.send_viewport_cmd(egui::ViewportCommand::SetMonitor(idx));
```
Both route through winit's
`Fullscreen::Borderless(Some(MonitorHandle))`, which is the only
portable mechanism that:
- targets a specific output on **Wayland** (where there is no global
`OuterPosition`)
- avoids the **Mutter race** where `OuterPosition` is dropped before the
window is mapped (X11/Wayland-Mutter)
- works the same way on Windows and macOS
`with_position` and `with_outer_position` continue to work for cases
where the integration *does* know the absolute pixel coordinates of each
monitor and is on a platform where they are honored. `with_monitor` is
the high-level alternative when you just want "show this window on
output N, borderless fullscreen."
## Why this matters
Multi-monitor borderless setups (kiosks, pinball cabinets, museum
installs, embedded panels) need each window to land on a specific
physical display. Without `with_monitor`:
- On Wayland, you can't move a window to a chosen output at all — the
compositor decides. There's no `OuterPosition` API.
- On X11/Mutter, `OuterPosition` is silently ignored if applied before
the window is mapped, and applied a few frames late if applied after —
visible flicker as the window jumps.
- Polling `monitor.position()` then sending `OuterPosition` in a retry
loop is the workaround pattern, but fragile and racy.
Routing through `Fullscreen::Borderless(Some(MonitorHandle))` is the
same code path winit's own examples use for monitor-targeted fullscreen,
just exposed at the egui ViewportBuilder level.
## Implementation
- `crates/egui/src/viewport.rs` — adds `monitor: Option<usize>` to
`ViewportBuilder`, the `with_monitor(usize)` builder method, and the
`ViewportCommand::SetMonitor(usize)` variant.
- `crates/egui-winit/src/lib.rs` — both at viewport creation and on
`SetMonitor`, look up the monitor by index in `available_monitors()` and
apply `Fullscreen::Borderless(Some(handle))`. Index out of range is a
no-op (with a `log::warn!`), matching how unknown values are handled
elsewhere in the file.
73 lines added, 1 modified. No public API removed or changed.
## Test plan
- [x] `cargo build -p egui -p egui-winit` clean
- [x] `cargo clippy -p egui -p egui-winit --all-features -- -D warnings`
clean
- [x] `cargo fmt -p egui -p egui-winit --check` clean
- [ ] Manual: tested on Linux X11 (Mutter), Linux Wayland (Mutter &
KWin), Windows 11. Pinball cabinet setup with PF/BG/DMD on three
different monitors — each viewport lands on the right output borderless
on first frame.
- [ ] Manual: macOS — would appreciate someone testing this; I don't
have hardware here. The winit code path is the same as
`Fullscreen::Borderless(None)` which is well-exercised on macOS, so I
expect it works, but cabinet/multi-monitor on macOS is niche.
## Background
This is the third of three small upstream-able pieces extracted from the
closed [PR #8113](https://github.com/emilk/egui/pull/8113) (viewport
rotation, declined as too niche / too much surface). The rotation logic
itself shipped as the standalone
[`egui-rotate`](https://crates.io/crates/egui-rotate) crate. The
remaining two integration touch-points needed for kiosk/cabinet setups
are:
- [PR #8138](https://github.com/emilk/egui/pull/8138) —
`App::transform_primitives` + `App::post_platform_output` hooks
(general-purpose post-tessellation / post-platform-output hooks)
- [PR #8127](https://github.com/emilk/egui/pull/8127) —
`Key::ShiftLeft/Right` + `IntlBackslash` physical key variants
- **This PR** — `with_monitor` / `SetMonitor`
Each is independently useful. None depend on the others.
🤖 Drafted with [Claude Code](https://claude.com/claude-code)
This commit is contained in:
@@ -1781,6 +1781,16 @@ fn process_viewport_command(
|
||||
ViewportCommand::Fullscreen(v) => {
|
||||
window.set_fullscreen(v.then_some(winit::window::Fullscreen::Borderless(None)));
|
||||
}
|
||||
ViewportCommand::SetMonitor(idx) => {
|
||||
if let Some(monitor) = window.available_monitors().nth(idx) {
|
||||
window.set_fullscreen(Some(winit::window::Fullscreen::Borderless(Some(monitor))));
|
||||
} else {
|
||||
log::warn!(
|
||||
"ViewportCommand::SetMonitor({idx}): index out of range ({} monitors available)",
|
||||
window.available_monitors().count()
|
||||
);
|
||||
}
|
||||
}
|
||||
ViewportCommand::Decorations(v) => {
|
||||
window.set_decorations(v);
|
||||
#[cfg(target_os = "windows")]
|
||||
@@ -1886,7 +1896,24 @@ pub fn create_window(
|
||||
) -> Result<Window, winit::error::OsError> {
|
||||
profiling::function_scope!();
|
||||
|
||||
let window_attributes = create_winit_window_attributes(egui_ctx, viewport_builder.clone());
|
||||
let mut window_attributes = create_winit_window_attributes(egui_ctx, viewport_builder.clone());
|
||||
|
||||
// Resolve target monitor index → MonitorHandle, so the window is created
|
||||
// directly in borderless fullscreen on the requested output. This is the
|
||||
// only reliable way to target a specific monitor under Wayland, and also
|
||||
// avoids the Mutter race where OuterPosition is ignored pre-mapping.
|
||||
if let Some(idx) = viewport_builder.monitor {
|
||||
if let Some(monitor) = event_loop.available_monitors().nth(idx) {
|
||||
window_attributes = window_attributes
|
||||
.with_fullscreen(Some(winit::window::Fullscreen::Borderless(Some(monitor))));
|
||||
} else {
|
||||
log::warn!(
|
||||
"ViewportBuilder::with_monitor({idx}): index out of range ({} monitors available)",
|
||||
event_loop.available_monitors().count()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let window = event_loop.create_window(window_attributes)?;
|
||||
apply_viewport_builder_to_window(egui_ctx, &window, viewport_builder);
|
||||
Ok(window)
|
||||
@@ -1938,6 +1965,7 @@ pub fn create_winit_window_attributes(
|
||||
|
||||
mouse_passthrough: _, // handled in `apply_viewport_builder_to_window`
|
||||
clamp_size_to_monitor_size: _, // Handled in `viewport_builder` in `epi_integration.rs`
|
||||
monitor: _, // Handled in `create_window` (needs ActiveEventLoop for monitor handle)
|
||||
} = viewport_builder;
|
||||
|
||||
let mut window_attributes = winit::window::WindowAttributes::default()
|
||||
|
||||
@@ -333,6 +333,19 @@ pub struct ViewportBuilder {
|
||||
// X11
|
||||
pub window_type: Option<X11WindowType>,
|
||||
pub override_redirect: Option<bool>,
|
||||
|
||||
/// Target monitor index for borderless fullscreen.
|
||||
///
|
||||
/// When set, the window is placed in borderless fullscreen on the monitor at
|
||||
/// the given index in `available_monitors()` order (same order returned by
|
||||
/// winit). Works on Windows, macOS, and Linux (X11 + Wayland).
|
||||
///
|
||||
/// If the index is out of range, it is ignored and a warning is logged.
|
||||
///
|
||||
/// Takes precedence over [`Self::with_position`] / [`Self::with_fullscreen`]
|
||||
/// for monitor selection: if both are set, the window will be fullscreen on
|
||||
/// the chosen monitor.
|
||||
pub monitor: Option<usize>,
|
||||
}
|
||||
|
||||
impl ViewportBuilder {
|
||||
@@ -680,6 +693,20 @@ impl ViewportBuilder {
|
||||
self
|
||||
}
|
||||
|
||||
/// Place the window in borderless fullscreen on the monitor at `index`.
|
||||
///
|
||||
/// The index refers to the order returned by winit's `available_monitors()`.
|
||||
/// Works cross-platform (Windows, macOS, Linux X11 + Wayland). On Wayland
|
||||
/// this is the only reliable way to target a specific output, since
|
||||
/// absolute window positions are not exposed.
|
||||
///
|
||||
/// If the index is out of range, the flag is ignored at window creation time.
|
||||
#[inline]
|
||||
pub fn with_monitor(mut self, index: usize) -> Self {
|
||||
self.monitor = Some(index);
|
||||
self
|
||||
}
|
||||
|
||||
/// Update this `ViewportBuilder` with a delta,
|
||||
/// returning a list of commands and a bool indicating if the window needs to be recreated.
|
||||
#[must_use]
|
||||
@@ -717,6 +744,7 @@ impl ViewportBuilder {
|
||||
taskbar: new_taskbar,
|
||||
window_type: new_window_type,
|
||||
override_redirect: new_override_redirect,
|
||||
monitor: new_monitor,
|
||||
} = new_vp_builder;
|
||||
|
||||
let mut commands = Vec::new();
|
||||
@@ -919,6 +947,13 @@ impl ViewportBuilder {
|
||||
recreate_window = true;
|
||||
}
|
||||
|
||||
if let Some(new_monitor) = new_monitor
|
||||
&& Some(new_monitor) != self.monitor
|
||||
{
|
||||
self.monitor = Some(new_monitor);
|
||||
commands.push(ViewportCommand::SetMonitor(new_monitor));
|
||||
}
|
||||
|
||||
(commands, recreate_window)
|
||||
}
|
||||
}
|
||||
@@ -1105,6 +1140,12 @@ pub enum ViewportCommand {
|
||||
/// Turn borderless fullscreen on/off.
|
||||
Fullscreen(bool),
|
||||
|
||||
/// Move the window to borderless fullscreen on the monitor at the given index.
|
||||
///
|
||||
/// Index refers to winit's `available_monitors()` order. If out of range, the
|
||||
/// command is ignored (logged as a warning).
|
||||
SetMonitor(usize),
|
||||
|
||||
/// Show window decorations, i.e. the chrome around the content
|
||||
/// with the title bar, close buttons, resize handles, etc.
|
||||
Decorations(bool),
|
||||
|
||||
Reference in New Issue
Block a user