1
0
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:
Sylvain
2026-05-26 21:45:48 +02:00
committed by GitHub
parent c57e3c4b0c
commit 99a8d7b3ff
2 changed files with 70 additions and 1 deletions

View File

@@ -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()

View File

@@ -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),