Screen color picker: no DX12 crash, and the overlay can be left on Windows (#4443) (#4704)

* fix(picker): the screen color picker no longer crashes on DX12 or traps the overlay on Windows (#4443)

Two Windows-only faults in the Pick screen color window:

- Created always-on-top, the monitor-sized picker window makes the DX12
  backend fail swapchain creation (0x887A0001, DXGI_ERROR_INVALID_CALL);
  wgpu then panics in Surface::get_current_texture_view. The window is now
  created at the normal level and raised once its first frame is painted.
- As a deferred viewport the picker was painted on its own, and the main
  window stopped being redrawn while it was up on Windows (OpenGL). A click
  or Esc only stored an answer that the main window's tick() never took, so
  the overlay could not be left. It is now an immediate viewport, painted
  inside the main window's frame.

Verified on Windows 11 / Intel UHD 630: DX12 (default) opens, picks and
cancels; OpenGL picks and cancels; macOS still picks and cancels. Adds a
test that the picker viewport is not built always-on-top.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* Screen picker: raise the window level once per picker window, not every frame

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
This commit is contained in:
Zuujis
2026-10-13 00:11:29 -04:00
committed by GitHub
co-authored by Claude Sonnet 5.5 Brandon Thomas
parent 715f4fd397
commit 9cba6a66d8
+18 -2
View File
@@ -259,7 +259,9 @@ fn viewport_builder(ctx: &Context, image: &ScreenImage) -> egui::ViewportBuilder
.with_resizable(false)
.with_position(image.position / ctx.zoom_factor())
.with_inner_size(image.size / ctx.zoom_factor())
.with_window_level(egui::WindowLevel::AlwaysOnTop)
// Not `AlwaysOnTop` here: a monitor-sized window created topmost makes the DX12 backend
// fail its swapchain creation (0x887A0001) and the app panics. The level is raised once
// the first frame has been painted, in `show`.
// Do not expose the native window's empty first frame over the desktop.
// The viewport is made visible after its snapshot has been painted below.
.with_visible(false)
@@ -280,7 +282,10 @@ pub fn show(ctx: &Context) {
let shared = r.clone();
let vid = egui::ViewportId::from_hash_of(("screen-color", generation, n));
let builder = viewport_builder(ctx, &image);
ctx.show_viewport_deferred(vid, builder, move |ui, _| {
// Immediate, so it paints inside the main window's own frame: as a deferred viewport the
// main window stopped being redrawn on Windows (OpenGL) once the picker was up, so a click
// or Esc was never turned into an answer and the overlay could not be left.
ctx.show_viewport_immediate(vid, builder, move |ui, _| {
{
let s = lock(&shared);
if s.generation != generation || s.owner.is_none() {
@@ -334,6 +339,11 @@ pub fn show(ctx: &Context) {
// Showing a new native viewport before this frame is painted can briefly cover
// the screen with the window's default white clear color.
ui.ctx().send_viewport_cmd(egui::ViewportCommand::Visible(true));
// Once per picker window: `show` runs every frame, so a local flag would resend it.
let raised = Id::new(("screen-picker-raised", ui.ctx().viewport_id()));
if !ui.ctx().data_mut(|d| std::mem::replace(d.get_temp_mut_or_default::<bool>(raised), true)) {
ui.ctx().send_viewport_cmd(egui::ViewportCommand::WindowLevel(egui::WindowLevel::AlwaysOnTop));
}
});
}
}
@@ -393,6 +403,12 @@ mod tests {
assert_eq!(builder.visible, Some(false));
}
#[test]
fn screen_picker_viewport_is_not_created_always_on_top() {
// Creating the monitor-sized window topmost fails DX12 swapchain creation (Windows).
let builder = viewport_builder(&Context::default(), &image());
assert_eq!(builder.window_level, None);
}
#[test]
fn pixel_mapping_handles_display_edges_negative_origins_and_hidpi() {
let i = image();
let rect = Rect::from_min_size(i.position, i.size);