Skip to content

egui-winit: Add ViewportBuilder::with_monitor and ViewportCommand::SetMonitor#8140

Open
Le-Syl21 wants to merge 1 commit into
emilk:mainfrom
Le-Syl21:viewport-with-monitor
Open

egui-winit: Add ViewportBuilder::with_monitor and ViewportCommand::SetMonitor#8140
Le-Syl21 wants to merge 1 commit into
emilk:mainfrom
Le-Syl21:viewport-with-monitor

Conversation

@Le-Syl21
Copy link
Copy Markdown

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.

// 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

  • cargo build -p egui -p egui-winit clean
  • cargo clippy -p egui -p egui-winit --all-features -- -D warnings clean
  • 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 (viewport rotation, declined as too niche / too much surface). The rotation logic itself shipped as the standalone egui-rotate crate. The remaining two integration touch-points needed for kiosk/cabinet setups are:

  • PR #8138App::transform_primitives + App::post_platform_output hooks (general-purpose post-tessellation / post-platform-output hooks)
  • PR #8127Key::ShiftLeft/Right + IntlBackslash physical key variants
  • This PRwith_monitor / SetMonitor

Each is independently useful. None depend on the others.

🤖 Drafted with Claude Code

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 30, 2026

Preview available at https://egui-pr-preview.github.io/pr/8140-viewport-with-monitor
Note that it might take a couple seconds for the update to show up after the preview_build workflow has completed.

View snapshot changes at kitdiff

Le-Syl21 added a commit to Le-Syl21/PinReady that referenced this pull request Apr 30, 2026
… labels

Architectural refactor of the egui dependency surface.

The previous full-rotation egui fork (~3000 lines diff) is split into:
  - The standalone `egui-rotate` crate (rotation logic + software cursor),
    published on crates.io: https://crates.io/crates/egui-rotate
  - A slim Le-Syl21/egui fork on `pinready-deps` (~150 lines diff total),
    pending three independent upstream PRs:
      * eframe::App::transform_primitives + post_platform_output hooks
        (emilk/egui#8138)
      * ViewportBuilder::with_monitor / ViewportCommand::SetMonitor
        (emilk/egui#8140)
      * Key::ShiftLeft/Right + IntlBackslash physical key variants
        (emilk/egui#8127)
  - Localized SDL key labels via the `sdl-keybridge` crate, replacing
    PinReady's hand-curated `key_*` rust-i18n entries.

User-visible changes:
  - Wizard input page: key labels now localized in the user's UI language
    via sdl-keybridge (e.g. "Alt Gauche" instead of "Left Alt" in French).
  - Cabinet mode: cursor I-beam in TextEdits now renders perpendicular to
    rotated text (was parallel before — fixed in egui-rotate 0.1.2).
  - No behavior change for desktop / wizard mode.

App-side changes (internal):
  - new fields on `App`: `rotation`, `cursor: SoftwareCursor`,
    `last_cursor_icon`
  - `set_rotation()` setter called from `main` at construction
  - `enable_kiosk_cursor()` now configures `cursor.set_scale(3.0)` +
    `cursor.set_lock(true)`
  - implements `eframe::App::raw_input_hook` (input rotation + cursor capture),
    `transform_primitives` (output rotation, ROOT viewport only via
    explicit viewport_id parameter), and `post_platform_output` (cursor
    icon capture + suppression)
  - software cursor drawn at the top of `fn ui` on a Foreground layer

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Comment thread crates/egui-winit/src/lib.rs Outdated
Comment on lines +1798 to +1799
let monitors: Vec<_> = event_loop.available_monitors().collect();
if let Some(monitor) = monitors.get(idx) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why collect?

Suggested change
let monitors: Vec<_> = event_loop.available_monitors().collect();
if let Some(monitor) = monitors.get(idx) {
if let Some(monitor) = event_loop.available_monitors().nth(idx) {

ViewportBuilder::with_monitor(index) and ViewportCommand::SetMonitor(index)
route through winit's Fullscreen::Borderless(Some(MonitorHandle)), which is
the only portable way to target a specific output on Wayland and avoids the
Mutter race where OuterPosition is dropped before the window is mapped.

Cross-platform: Windows, macOS, Linux X11, Linux Wayland.
@Le-Syl21 Le-Syl21 force-pushed the viewport-with-monitor branch from 708af0b to 14a4377 Compare April 30, 2026 22:23
Copy link
Copy Markdown
Author

@Le-Syl21 Le-Syl21 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, applied. Force-pushed with the fix — nth(idx) in the success path, available_monitors().count() only re-evaluated when we actually need it for the diagnostic.

@Le-Syl21
Copy link
Copy Markdown
Author

@Mingun good catch, applied — force-pushed. Uses nth(idx) in the hot path; available_monitors().count() only runs when the index is out-of-range to keep the diagnostic.

self
}

/// Place the window in borderless fullscreen on the monitor at `index`.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, it's weird that the window is always placed in borderless fullscreen if this is set, what if I want to place it on a specific window with a normal fullscreen or with a certain size?

Copy link
Copy Markdown
Collaborator

@lucasmerlin lucasmerlin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants