From b3ffbca2ab01b31751d10685fc4d3ac463e4d9a9 Mon Sep 17 00:00:00 2001 From: Lucas Meurer Date: Tue, 13 Jan 2026 11:39:53 +0100 Subject: [PATCH 01/12] Prevent snapshot update workflow to run on main (#7842) Turns out if you don't give the gh workflow dispatch a ref it runs it on main, oops --- .github/workflows/update_kittest_snapshots.yml | 1 + tests/egui_tests/tests/snapshots/text_edit_halign.png | 3 --- 2 files changed, 1 insertion(+), 3 deletions(-) delete mode 100644 tests/egui_tests/tests/snapshots/text_edit_halign.png diff --git a/.github/workflows/update_kittest_snapshots.yml b/.github/workflows/update_kittest_snapshots.yml index 9c67c52a9..bab1192d2 100644 --- a/.github/workflows/update_kittest_snapshots.yml +++ b/.github/workflows/update_kittest_snapshots.yml @@ -14,6 +14,7 @@ jobs: update-snapshots: name: Update snapshots from artifact runs-on: ubuntu-latest + if: github.ref_name != 'main' # We never want to update snapshots directly on main permissions: contents: write diff --git a/tests/egui_tests/tests/snapshots/text_edit_halign.png b/tests/egui_tests/tests/snapshots/text_edit_halign.png deleted file mode 100644 index 5c56f1b19..000000000 --- a/tests/egui_tests/tests/snapshots/text_edit_halign.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:3f60036a5af9b376cbf1fefaf8088ce41cfd0c19ec16f02b1d8b98c3e987972e -size 13276 From 83e61c6fb064591e5cacb655156621f7eeafacc8 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Mon, 19 Jan 2026 09:01:11 +0100 Subject: [PATCH 02/12] Improve docs of key/button "down" state (#7851) --- crates/egui/src/input_state/mod.rs | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/crates/egui/src/input_state/mod.rs b/crates/egui/src/input_state/mod.rs index 49027c06e..37faf64c2 100644 --- a/crates/egui/src/input_state/mod.rs +++ b/crates/egui/src/input_state/mod.rs @@ -319,7 +319,9 @@ pub struct InputState { /// Which modifier keys are down at the start of the frame? pub modifiers: Modifiers, - // The keys that are currently being held down. + /// The keys that are currently being held down. + /// + /// Keys released this frame are NOT considered down. pub keys_down: HashSet, /// In-order events received this frame @@ -765,6 +767,8 @@ impl InputState { } /// Is the given key currently held down? + /// + /// Keys released this frame are NOT considered down. pub fn key_down(&self, desired_key: Key) -> bool { self.keys_down.contains(&desired_key) } @@ -1018,6 +1022,7 @@ pub struct PointerState { /// Used for calculating velocity of pointer. pos_history: History, + /// Buttons currently down, excluding those released this frame. down: [bool; NUM_POINTER_BUTTONS], /// Where did the current click/drag originate? @@ -1405,6 +1410,8 @@ impl PointerState { } /// Is any pointer button currently down? + /// + /// Buttons released this frame are NOT considered down. pub fn any_down(&self) -> bool { self.down.iter().any(|&down| down) } @@ -1460,6 +1467,8 @@ impl PointerState { } /// Is this button currently down? + /// + /// Buttons released this frame are NOT considered down. #[inline(always)] pub fn button_down(&self, button: PointerButton) -> bool { self.down[button as usize] @@ -1516,18 +1525,24 @@ impl PointerState { } /// Is the primary button currently down? + /// + /// Buttons released this frame are NOT considered down. #[inline(always)] pub fn primary_down(&self) -> bool { self.button_down(PointerButton::Primary) } /// Is the secondary button currently down? + /// + /// Buttons released this frame are NOT considered down. #[inline(always)] pub fn secondary_down(&self) -> bool { self.button_down(PointerButton::Secondary) } /// Is the middle button currently down? + /// + /// Buttons released this frame are NOT considered down. #[inline(always)] pub fn middle_down(&self) -> bool { self.button_down(PointerButton::Middle) From fa78d25564a5dbcb546ff6db0a9e14cb603ba03b Mon Sep 17 00:00:00 2001 From: Yuri Kunde Schlesner Date: Mon, 19 Jan 2026 10:47:10 -0300 Subject: [PATCH 03/12] egui_kittest: Close debug_open_snapshot temp file before viewing it (#7841) The image file written by debug_open_snapshot was being kept open by the `tempfile` object. On Windows, this prevented `open::that` from successfully launching the viewer sometimes because the file remained locked, which can be avoided by first releasing the file handle. --- crates/egui_kittest/src/snapshot.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/egui_kittest/src/snapshot.rs b/crates/egui_kittest/src/snapshot.rs index 4d139fcbb..8134dbdf0 100644 --- a/crates/egui_kittest/src/snapshot.rs +++ b/crates/egui_kittest/src/snapshot.rs @@ -721,11 +721,14 @@ impl Harness<'_, State> { }) .unwrap(); + // Close temp file so it isn't locked when `open` tries to launch it (on Windows) + let path = temp_file.into_temp_path(); + #[expect(clippy::print_stdout)] { println!("Wrote debug snapshot to: {}", path.display()); } - let result = open::that(path); + let result = open::that(&path); if let Err(err) = result { #[expect(clippy::print_stderr)] { From 91a1e6f23e4cb52c2c01dd5d454f9854f03c793e Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Tue, 3 Feb 2026 11:05:37 +0100 Subject: [PATCH 04/12] Ignore that bincode is unmaintained --- deny.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/deny.toml b/deny.toml index 1b3bd8b12..e07d476fa 100644 --- a/deny.toml +++ b/deny.toml @@ -33,6 +33,7 @@ version = 2 ignore = [ "RUSTSEC-2024-0320", # unmaintained yaml-rust pulled in by syntect "RUSTSEC-2024-0436", # unmaintained paste pulled via metal/wgpu, see https://github.com/gfx-rs/metal-rs/issues/349 + "RUSTSEC-2025-0141", # https://rustsec.org/advisories/RUSTSEC-2025-0141 - bincode is unmaintained - https://git.sr.ht/~stygianentity/bincode/tree/v3.0/item/README.md ] [bans] From 67d87233fff7ea97b363c009c8212359f1d88cf6 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Tue, 3 Feb 2026 14:48:04 +0100 Subject: [PATCH 05/12] use #[track_caller] in kitdiff --- crates/egui_kittest/src/snapshot.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/egui_kittest/src/snapshot.rs b/crates/egui_kittest/src/snapshot.rs index 8134dbdf0..28b5ac986 100644 --- a/crates/egui_kittest/src/snapshot.rs +++ b/crates/egui_kittest/src/snapshot.rs @@ -859,6 +859,7 @@ impl From for Vec { } impl Drop for SnapshotResults { + #[track_caller] fn drop(&mut self) { // Don't panic if we are already panicking (the test probably failed for another reason) if std::thread::panicking() { From e33050f14bafbb7421c3fc2997d648b3c5ac7c16 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Wed, 4 Feb 2026 16:59:12 +0100 Subject: [PATCH 06/12] Update bytes crate --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a99c0d281..d774fa806 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -698,9 +698,9 @@ checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" [[package]] name = "bytes" -version = "1.8.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ac0150caa2ae65ca5bd83f25c7de183dea78d4d366469f148435e2acfbad0da" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" [[package]] name = "calloop" From f1cde5aab495824da6cf8e9d3c4b87a8d007a6f1 Mon Sep 17 00:00:00 2001 From: Roman Popov Date: Wed, 4 Feb 2026 08:52:39 -0800 Subject: [PATCH 07/12] Fix `CentralPanel::show_inside_dyn` to round `panel_rect` (#7868) While using CentralPanel::show_inside_dyn inside egui_tiles I've noticed that sometimes it renders "Unaligned" message with certain tile positions. Match SidePanel behavior by calling round_ui() on the panel rect. image --------- Co-authored-by: Emil Ernerfeldt --- crates/egui/src/containers/panel.rs | 2 +- crates/egui/src/context.rs | 2 +- crates/egui/src/placer.rs | 2 ++ crates/egui_demo_lib/tests/snapshots/demos/Misc Demos.png | 4 ++-- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/crates/egui/src/containers/panel.rs b/crates/egui/src/containers/panel.rs index 6281e6b41..f2a4c3b67 100644 --- a/crates/egui/src/containers/panel.rs +++ b/crates/egui/src/containers/panel.rs @@ -1066,7 +1066,7 @@ impl CentralPanel { id, UiBuilder::new() .layer_id(LayerId::background()) - .max_rect(ctx.available_rect().round_ui()), + .max_rect(ctx.available_rect()), ); panel_ui.set_clip_rect(ctx.content_rect()); diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index e28048edf..2665d5edd 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -798,7 +798,7 @@ impl Context { Id::new((ctx.viewport_id(), "__top_ui")), UiBuilder::new() .layer_id(LayerId::background()) - .max_rect(ctx.available_rect().round_ui()), + .max_rect(ctx.available_rect()), ); { diff --git a/crates/egui/src/placer.rs b/crates/egui/src/placer.rs index b5f68f72d..8c54204b9 100644 --- a/crates/egui/src/placer.rs +++ b/crates/egui/src/placer.rs @@ -1,4 +1,5 @@ use crate::{Layout, Painter, Pos2, Rect, Region, Vec2, grid, vec2}; +use emath::GuiRounding as _; #[cfg(debug_assertions)] use crate::{Align2, Color32, Stroke}; @@ -92,6 +93,7 @@ impl Placer { } else { self.layout.available_rect_before_wrap(&self.region) } + .round_ui() } /// Amount of space available for a widget. diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Misc Demos.png b/crates/egui_demo_lib/tests/snapshots/demos/Misc Demos.png index a12313e97..95c172d26 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Misc Demos.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Misc Demos.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:604f716e687fc26abba92769fe2dae75d850b18598d2e8a9524451ab0f760251 -size 65403 +oid sha256:56b44d26946770c0878e11e3197633697ad339a7e8fcffe7279a6b4c45cd3582 +size 65384 From 3cd52881b4237ca362a21b2c975adde51a7df35a Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Tue, 10 Feb 2026 11:57:43 +0100 Subject: [PATCH 08/12] Update crate --- Cargo.lock | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d774fa806..9173deaff 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1155,9 +1155,9 @@ checksum = "5c297a1c74b71ae29df00c3e22dd9534821d60eb9af5a0192823fa2acea70c2a" [[package]] name = "deranged" -version = "0.3.11" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" dependencies = [ "powerfmt", ] @@ -2870,9 +2870,9 @@ checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" [[package]] name = "num-conv" -version = "0.1.0" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" +checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" [[package]] name = "num-traits" @@ -4490,30 +4490,30 @@ dependencies = [ [[package]] name = "time" -version = "0.3.36" +version = "0.3.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" dependencies = [ "deranged", "itoa", "num-conv", "powerfmt", - "serde", + "serde_core", "time-core", "time-macros", ] [[package]] name = "time-core" -version = "0.1.2" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" [[package]] name = "time-macros" -version = "0.2.18" +version = "0.2.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" dependencies = [ "num-conv", "time-core", From 64a96ef3917d4e305a79fa5f460af5fc73eabcf8 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Tue, 10 Feb 2026 12:24:30 +0100 Subject: [PATCH 09/12] Stop ctrl+arrow etc from moving focus (#7897) Previously any pressing of arrow keys would move the focus, but now we check that there are no modifier keys pressed down --- crates/egui/src/memory/mod.rs | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/crates/egui/src/memory/mod.rs b/crates/egui/src/memory/mod.rs index 9c91c1322..fbc8e6f68 100644 --- a/crates/egui/src/memory/mod.rs +++ b/crates/egui/src/memory/mod.rs @@ -543,22 +543,19 @@ impl Focus { .. } = event && let Some(cardinality) = match key { - crate::Key::ArrowUp => Some(FocusDirection::Up), - crate::Key::ArrowRight => Some(FocusDirection::Right), - crate::Key::ArrowDown => Some(FocusDirection::Down), - crate::Key::ArrowLeft => Some(FocusDirection::Left), + crate::Key::ArrowUp if !modifiers.any() => Some(FocusDirection::Up), + crate::Key::ArrowRight if !modifiers.any() => Some(FocusDirection::Right), + crate::Key::ArrowDown if !modifiers.any() => Some(FocusDirection::Down), + crate::Key::ArrowLeft if !modifiers.any() => Some(FocusDirection::Left), - crate::Key::Tab => { - if modifiers.shift { - Some(FocusDirection::Previous) - } else { - Some(FocusDirection::Next) - } - } - crate::Key::Escape => { + crate::Key::Tab if !modifiers.any() => Some(FocusDirection::Next), + crate::Key::Tab if modifiers.shift_only() => Some(FocusDirection::Previous), + + crate::Key::Escape if !modifiers.any() => { self.focused_widget = None; Some(FocusDirection::None) } + _ => None, } { From 08f3fd2dc1051c47315437877a01effe6fd84e15 Mon Sep 17 00:00:00 2001 From: Lucas Meurer Date: Tue, 17 Feb 2026 11:08:18 +0100 Subject: [PATCH 10/12] Fix scroll area not consuming scroll events (#7904) This fixes scrolling in a nested scroll area also scrolling the outer scroll area https://github.com/user-attachments/assets/ade40b1e-c974-4806-8045-881bea590d4a --- crates/egui/src/containers/scroll_area.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/crates/egui/src/containers/scroll_area.rs b/crates/egui/src/containers/scroll_area.rs index 8f46223d2..2616fb414 100644 --- a/crates/egui/src/containers/scroll_area.rs +++ b/crates/egui/src/containers/scroll_area.rs @@ -1198,10 +1198,9 @@ impl Prepared { // Clear scroll delta so no parent scroll will use it: ui.input_mut(|input| { if always_scroll_enabled_direction { - input.smooth_scroll_delta()[0] = 0.0; - input.smooth_scroll_delta()[1] = 0.0; + input.smooth_scroll_delta = Vec2::ZERO; } else { - input.smooth_scroll_delta()[d] = 0.0; + input.smooth_scroll_delta[d] = 0.0; } }); From 66df116fa08c268894970639c125dc7c5b3429b8 Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Fri, 20 Feb 2026 19:40:47 +0100 Subject: [PATCH 11/12] Add popup backdrops --- crates/egui/src/containers/modal.rs | 58 +++++++++++++++-------------- crates/egui/src/containers/popup.rs | 45 ++++++++++++++++++++-- crates/egui/src/style.rs | 30 +++++++++++++++ 3 files changed, 102 insertions(+), 31 deletions(-) diff --git a/crates/egui/src/containers/modal.rs b/crates/egui/src/containers/modal.rs index 23190ddf6..cdd38f010 100644 --- a/crates/egui/src/containers/modal.rs +++ b/crates/egui/src/containers/modal.rs @@ -1,9 +1,22 @@ -use emath::{Align2, Vec2}; +use emath::{Align2, Rect, Vec2}; use crate::{ - Area, Color32, Context, Frame, Id, InnerResponse, Order, Response, Sense, Ui, UiBuilder, UiKind, + Area, Color32, Context, Frame, Id, InnerResponse, Order, Response, Sense, Ui, UiKind, }; +/// Paint a full-screen backdrop on the given [`Ui`] and return whether +/// a click landed outside `content_rect` (i.e. on the backdrop). +/// +/// This is used by both [`Modal`] and [`crate::Popup`]. +pub(crate) fn paint_backdrop(ui: &mut Ui, color: Color32) -> bool { + let bg_rect = ui.ctx().viewport_rect(); + + let response = ui.interact(bg_rect, ui.unique_id().with("backdrop"), Sense::click_and_drag()); + ui.painter().rect_filled(response.rect, 0.0, color); + + response.clicked() && !ui.response().contains_pointer() +} + /// A modal dialog. /// /// Similar to a [`crate::Window`] but centered and with a backdrop that @@ -26,7 +39,7 @@ impl Modal { pub fn new(id: Id) -> Self { Self { area: Self::default_area(id), - backdrop_color: Color32::from_black_alpha(100), + backdrop_color: Color32::PLACEHOLDER, frame: None, } } @@ -57,7 +70,7 @@ impl Modal { /// Set the backdrop color of the modal. /// - /// Default is `Color32::from_black_alpha(100)`. + /// Default comes from [`crate::Visuals::modal_backdrop_color`]. #[inline] pub fn backdrop_color(mut self, color: Color32) -> Self { self.backdrop_color = color; @@ -77,42 +90,34 @@ impl Modal { pub fn show(self, ctx: &Context, content: impl FnOnce(&mut Ui) -> T) -> ModalResponse { let Self { area, - backdrop_color, + mut backdrop_color, frame, } = self; + if backdrop_color == Color32::PLACEHOLDER { + backdrop_color = ctx.global_style().visuals.modal_backdrop_color; + } + let is_top_modal = ctx.memory_mut(|mem| { mem.set_modal_layer(area.layer()); mem.top_modal_layer() == Some(area.layer()) }); let any_popup_open = crate::Popup::is_any_open(ctx); let InnerResponse { - inner: (inner, backdrop_response), + inner: (inner, backdrop_clicked), response, } = area.show(ctx, |ui| { - let bg_rect = ui.ctx().content_rect(); - let bg_sense = Sense::CLICK | Sense::DRAG; - let mut backdrop = ui.new_child(UiBuilder::new().sense(bg_sense).max_rect(bg_rect)); - backdrop.set_min_size(bg_rect.size()); - ui.painter().rect_filled(bg_rect, 0.0, backdrop_color); - let backdrop_response = backdrop.response(); + let backdrop_clicked = paint_backdrop(ui, backdrop_color); let frame = frame.unwrap_or_else(|| Frame::popup(ui.style())); + let inner = frame.show(ui, content).inner; - // We need the extra scope with the sense since frame can't have a sense and since we - // need to prevent the clicks from passing through to the backdrop. - let inner = ui - .scope_builder(UiBuilder::new().sense(Sense::CLICK | Sense::DRAG), |ui| { - frame.show(ui, content).inner - }) - .inner; - - (inner, backdrop_response) + (inner, backdrop_clicked) }); ModalResponse { response, - backdrop_response, + backdrop_clicked, inner, is_top_modal, any_popup_open, @@ -125,11 +130,8 @@ pub struct ModalResponse { /// The response of the modal contents pub response: Response, - /// The response of the modal backdrop. - /// - /// A click on this means the user clicked outside the modal, - /// in which case you might want to close the modal. - pub backdrop_response: Response, + /// Whether the backdrop was clicked (i.e. a click landed outside the modal). + pub backdrop_clicked: bool, /// The inner response from the content closure pub inner: T, @@ -157,7 +159,7 @@ impl ModalResponse { let ui_close_called = self.response.should_close(); - self.backdrop_response.clicked() + self.backdrop_clicked || ui_close_called || (self.is_top_modal && !self.any_popup_open && escape_clicked()) } diff --git a/crates/egui/src/containers/popup.rs b/crates/egui/src/containers/popup.rs index 0fb2a9f2a..6bdccaad2 100644 --- a/crates/egui/src/containers/popup.rs +++ b/crates/egui/src/containers/popup.rs @@ -5,8 +5,8 @@ use std::iter::once; use emath::{Align, Pos2, Rect, RectAlign, Vec2, vec2}; use crate::{ - Area, AreaState, Context, Frame, Id, InnerResponse, Key, LayerId, Layout, Order, Response, - Sense, Ui, UiKind, UiStackInfo, + Area, AreaState, Color32, Context, Frame, Id, InnerResponse, Key, LayerId, Layout, Order, + Response, Sense, Ui, UiKind, UiStackInfo, containers::menu::{MenuConfig, MenuState, menu_style}, style::StyleModifier, }; @@ -185,6 +185,8 @@ pub struct Popup<'a> { layout: Layout, frame: Option, style: StyleModifier, + /// `None` = use style default, `Some(None)` = no backdrop, `Some(Some(color))` = this color + backdrop_color: Option>, } impl<'a> Popup<'a> { @@ -207,6 +209,7 @@ impl<'a> Popup<'a> { layout: Layout::default(), frame: None, style: StyleModifier::default(), + backdrop_color: None, } } @@ -410,6 +413,22 @@ impl<'a> Popup<'a> { self } + /// Show a backdrop behind the popup. + /// + /// The backdrop covers the entire screen, blocking interaction with the rest of the UI. + /// + /// - `None` — no backdrop is shown. + /// - `Some(color)` — show a backdrop with this color. + /// - `Some(Color32::PLACEHOLDER)` — use the default from [`crate::Visuals::popup_backdrop_color`]. + /// + /// By default, this is controlled by [`crate::Visuals::popup_backdrop_color`]. + /// Calling this method overrides the global style for this popup. + #[inline] + pub fn backdrop(mut self, color: Option) -> Self { + self.backdrop_color = Some(color); + self + } + /// Get the [`Context`] pub fn ctx(&self) -> &Context { &self.ctx @@ -553,6 +572,7 @@ impl<'a> Popup<'a> { layout, frame, style, + backdrop_color, } = self; if kind != PopupKind::Tooltip { @@ -588,7 +608,26 @@ impl<'a> Popup<'a> { area = area.default_width(width); } + // Resolve backdrop color: per-instance override, or fall back to global style + let resolved_backdrop = match backdrop_color { + Some(explicit) => explicit, + None => ctx.global_style().visuals.popup_backdrop_color, + }; + let resolved_backdrop = match resolved_backdrop { + Some(color) if color == Color32::PLACEHOLDER => { + ctx.global_style().visuals.popup_backdrop_color + } + other => other, + }; + + let mut backdrop_clicked = false; let mut response = area.show(&ctx, |ui| { + backdrop_clicked = if let Some(color) = resolved_backdrop { + super::modal::paint_backdrop(ui, color) && was_open_last_frame + } else { + false + }; + style.apply(ui.style_mut()); let frame = frame.unwrap_or_else(|| Frame::popup(ui.style())); frame.show(ui, content).inner @@ -600,7 +639,7 @@ impl<'a> Popup<'a> { let closed_by_click = match close_behavior { PopupCloseBehavior::CloseOnClick => close_click, PopupCloseBehavior::CloseOnClickOutside => { - close_click && response.response.clicked_elsewhere() + backdrop_clicked || (close_click && response.response.clicked_elsewhere()) } PopupCloseBehavior::IgnoreClicks => false, }; diff --git a/crates/egui/src/style.rs b/crates/egui/src/style.rs index a555b9ace..794d61601 100644 --- a/crates/egui/src/style.rs +++ b/crates/egui/src/style.rs @@ -1024,6 +1024,20 @@ pub struct Visuals { pub popup_shadow: Shadow, + /// The backdrop color for modals. + /// + /// Default is `Color32::from_black_alpha(100)`. + pub modal_backdrop_color: Color32, + + /// The default backdrop color for popups. + /// + /// If `Some`, popups show a backdrop by default. + /// If `None` (default), popups don't show a backdrop unless explicitly enabled + /// via [`crate::Popup::backdrop`]. + /// + /// Individual popups can still override this with [`crate::Popup::backdrop`]. + pub popup_backdrop_color: Option, + pub resize_corner_size: f32, /// How the text cursor acts. @@ -1460,6 +1474,9 @@ impl Visuals { color: Color32::from_black_alpha(96), }, + modal_backdrop_color: Color32::from_black_alpha(100), + popup_backdrop_color: None, + resize_corner_size: 12.0, text_cursor: Default::default(), @@ -2152,6 +2169,8 @@ impl Visuals { panel_fill, popup_shadow, + modal_backdrop_color, + popup_backdrop_color, resize_corner_size, @@ -2333,6 +2352,17 @@ impl Visuals { ui.label("Shadow"); ui.add(popup_shadow); ui.end_row(); + + ui.label("Modal backdrop"); + ui.color_edit_button_srgba(modal_backdrop_color); + ui.end_row(); + + ui_optional_color( + ui, + popup_backdrop_color, + Color32::from_black_alpha(100), + "Popup backdrop", + ); }); }); From 63360ae5277a9f21a1b19b5bdb9562d18dd47a1a Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Fri, 20 Feb 2026 23:46:28 +0100 Subject: [PATCH 12/12] Better popup api --- crates/egui/src/containers/modal.rs | 2 +- crates/egui/src/containers/popup.rs | 56 ++++++++++++++++------------- crates/egui/src/style.rs | 35 +++++++++++------- 3 files changed, 55 insertions(+), 38 deletions(-) diff --git a/crates/egui/src/containers/modal.rs b/crates/egui/src/containers/modal.rs index cdd38f010..e84ac85ba 100644 --- a/crates/egui/src/containers/modal.rs +++ b/crates/egui/src/containers/modal.rs @@ -1,4 +1,4 @@ -use emath::{Align2, Rect, Vec2}; +use emath::{Align2, Vec2}; use crate::{ Area, Color32, Context, Frame, Id, InnerResponse, Order, Response, Sense, Ui, UiKind, diff --git a/crates/egui/src/containers/popup.rs b/crates/egui/src/containers/popup.rs index 6bdccaad2..cef01c397 100644 --- a/crates/egui/src/containers/popup.rs +++ b/crates/egui/src/containers/popup.rs @@ -185,8 +185,9 @@ pub struct Popup<'a> { layout: Layout, frame: Option, style: StyleModifier, - /// `None` = use style default, `Some(None)` = no backdrop, `Some(Some(color))` = this color - backdrop_color: Option>, + /// `None` = use style default, `Some(bool)` = explicit override + backdrop: Option, + backdrop_color: Option, } impl<'a> Popup<'a> { @@ -209,6 +210,7 @@ impl<'a> Popup<'a> { layout: Layout::default(), frame: None, style: StyleModifier::default(), + backdrop: None, backdrop_color: None, } } @@ -416,15 +418,23 @@ impl<'a> Popup<'a> { /// Show a backdrop behind the popup. /// /// The backdrop covers the entire screen, blocking interaction with the rest of the UI. + /// The color is determined by [`crate::Visuals::popup_backdrop_color`]. /// - /// - `None` — no backdrop is shown. - /// - `Some(color)` — show a backdrop with this color. - /// - `Some(Color32::PLACEHOLDER)` — use the default from [`crate::Visuals::popup_backdrop_color`]. - /// - /// By default, this is controlled by [`crate::Visuals::popup_backdrop_color`]. + /// By default, this is controlled by [`crate::Visuals::popup_backdrop`]. /// Calling this method overrides the global style for this popup. + /// + /// Note: submenus never show a backdrop, even if this is set to `true`. #[inline] - pub fn backdrop(mut self, color: Option) -> Self { + pub fn backdrop(mut self, show: bool) -> Self { + self.backdrop = Some(show); + self + } + + /// Override the backdrop color for this popup. + /// + /// By default, the color comes from [`crate::Visuals::popup_backdrop_color`]. + #[inline] + pub fn backdrop_color(mut self, color: Color32) -> Self { self.backdrop_color = Some(color); self } @@ -572,6 +582,7 @@ impl<'a> Popup<'a> { layout, frame, style, + backdrop, backdrop_color, } = self; @@ -608,25 +619,22 @@ impl<'a> Popup<'a> { area = area.default_width(width); } - // Resolve backdrop color: per-instance override, or fall back to global style - let resolved_backdrop = match backdrop_color { - Some(explicit) => explicit, - None => ctx.global_style().visuals.popup_backdrop_color, - }; - let resolved_backdrop = match resolved_backdrop { - Some(color) if color == Color32::PLACEHOLDER => { - ctx.global_style().visuals.popup_backdrop_color - } - other => other, - }; + // Resolve whether to show a backdrop: + // - Explicit per-instance override takes priority + // - Otherwise, fall back to global style + // - Submenus (parent layer is Foreground) never show a backdrop + let is_submenu = layer_id.order == Order::Foreground; + let show_backdrop = !is_submenu + && backdrop.unwrap_or_else(|| ctx.global_style().visuals.popup_backdrop); let mut backdrop_clicked = false; let mut response = area.show(&ctx, |ui| { - backdrop_clicked = if let Some(color) = resolved_backdrop { - super::modal::paint_backdrop(ui, color) && was_open_last_frame - } else { - false - }; + if show_backdrop { + let color = backdrop_color + .unwrap_or_else(|| ctx.global_style().visuals.popup_backdrop_color); + backdrop_clicked = + super::modal::paint_backdrop(ui, color) && was_open_last_frame; + } style.apply(ui.style_mut()); let frame = frame.unwrap_or_else(|| Frame::popup(ui.style())); diff --git a/crates/egui/src/style.rs b/crates/egui/src/style.rs index 794d61601..27ce2516c 100644 --- a/crates/egui/src/style.rs +++ b/crates/egui/src/style.rs @@ -1029,14 +1029,20 @@ pub struct Visuals { /// Default is `Color32::from_black_alpha(100)`. pub modal_backdrop_color: Color32, - /// The default backdrop color for popups. + /// The backdrop color for popups. /// - /// If `Some`, popups show a backdrop by default. - /// If `None` (default), popups don't show a backdrop unless explicitly enabled - /// via [`crate::Popup::backdrop`]. + /// Only used when [`Self::popup_backdrop`] is `true` or when a popup + /// explicitly enables the backdrop via [`crate::Popup::backdrop`]. + /// + /// Default is `Color32::from_black_alpha(100)`. + pub popup_backdrop_color: Color32, + + /// Whether popups show a backdrop by default. /// /// Individual popups can still override this with [`crate::Popup::backdrop`]. - pub popup_backdrop_color: Option, + /// + /// Default is `false`. + pub popup_backdrop: bool, pub resize_corner_size: f32, @@ -1475,7 +1481,8 @@ impl Visuals { }, modal_backdrop_color: Color32::from_black_alpha(100), - popup_backdrop_color: None, + popup_backdrop_color: Color32::from_black_alpha(100), + popup_backdrop: false, resize_corner_size: 12.0, @@ -2171,6 +2178,7 @@ impl Visuals { popup_shadow, modal_backdrop_color, popup_backdrop_color, + popup_backdrop, resize_corner_size, @@ -2353,16 +2361,17 @@ impl Visuals { ui.add(popup_shadow); ui.end_row(); - ui.label("Modal backdrop"); + ui.label("Modal backdrop color"); ui.color_edit_button_srgba(modal_backdrop_color); ui.end_row(); - ui_optional_color( - ui, - popup_backdrop_color, - Color32::from_black_alpha(100), - "Popup backdrop", - ); + ui.label("Popup backdrop color"); + ui.color_edit_button_srgba(popup_backdrop_color); + ui.end_row(); + + ui.label("Popup backdrop"); + ui.checkbox(popup_backdrop, ""); + ui.end_row(); }); });