1
0
mirror of https://github.com/emilk/egui.git synced 2026-06-26 22:53:14 -04:00

Merge branch 'lucas/malmal/popup-backdrop' into lucas/malmal/main

This commit is contained in:
lucasmerlin
2026-02-20 23:58:34 +01:00
14 changed files with 169 additions and 64 deletions

View File

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

View File

@@ -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"
@@ -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",
]
@@ -2872,9 +2872,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"
@@ -4492,30 +4492,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",

View File

@@ -1,9 +1,22 @@
use emath::{Align2, 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<T>(self, ctx: &Context, content: impl FnOnce(&mut Ui) -> T) -> ModalResponse<T> {
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<T> {
/// 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<T> ModalResponse<T> {
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())
}

View File

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

View File

@@ -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,9 @@ pub struct Popup<'a> {
layout: Layout,
frame: Option<Frame>,
style: StyleModifier,
/// `None` = use style default, `Some(bool)` = explicit override
backdrop: Option<bool>,
backdrop_color: Option<Color32>,
}
impl<'a> Popup<'a> {
@@ -207,6 +210,8 @@ impl<'a> Popup<'a> {
layout: Layout::default(),
frame: None,
style: StyleModifier::default(),
backdrop: None,
backdrop_color: None,
}
}
@@ -410,6 +415,30 @@ 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.
/// The color is determined 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, 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
}
/// Get the [`Context`]
pub fn ctx(&self) -> &Context {
&self.ctx
@@ -553,6 +582,8 @@ impl<'a> Popup<'a> {
layout,
frame,
style,
backdrop,
backdrop_color,
} = self;
if kind != PopupKind::Tooltip {
@@ -588,7 +619,23 @@ impl<'a> Popup<'a> {
area = area.default_width(width);
}
// 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| {
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()));
frame.show(ui, content).inner
@@ -600,7 +647,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,
};

View File

@@ -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()),
);
{

View File

@@ -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<Key>,
/// 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<Pos2>,
/// 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)

View File

@@ -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,
}
{

View File

@@ -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.

View File

@@ -1024,6 +1024,26 @@ 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 backdrop color for popups.
///
/// 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`].
///
/// Default is `false`.
pub popup_backdrop: bool,
pub resize_corner_size: f32,
/// How the text cursor acts.
@@ -1460,6 +1480,10 @@ impl Visuals {
color: Color32::from_black_alpha(96),
},
modal_backdrop_color: Color32::from_black_alpha(100),
popup_backdrop_color: Color32::from_black_alpha(100),
popup_backdrop: false,
resize_corner_size: 12.0,
text_cursor: Default::default(),
@@ -2152,6 +2176,9 @@ impl Visuals {
panel_fill,
popup_shadow,
modal_backdrop_color,
popup_backdrop_color,
popup_backdrop,
resize_corner_size,
@@ -2333,6 +2360,18 @@ impl Visuals {
ui.label("Shadow");
ui.add(popup_shadow);
ui.end_row();
ui.label("Modal backdrop color");
ui.color_edit_button_srgba(modal_backdrop_color);
ui.end_row();
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();
});
});

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:604f716e687fc26abba92769fe2dae75d850b18598d2e8a9524451ab0f760251
size 65403
oid sha256:56b44d26946770c0878e11e3197633697ad339a7e8fcffe7279a6b4c45cd3582
size 65384

View File

@@ -721,11 +721,14 @@ impl<State> 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)]
{
@@ -856,6 +859,7 @@ impl From<SnapshotResults> for Vec<SnapshotError> {
}
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() {

View File

@@ -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]

View File

@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:3f60036a5af9b376cbf1fefaf8088ce41cfd0c19ec16f02b1d8b98c3e987972e
size 13276