Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Modal and Memory::set_modal_layer #5358

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1229,6 +1229,7 @@ dependencies = [
"ahash",
"backtrace",
"document-features",
"egui_kittest",
"emath",
"epaint",
"log",
Expand Down
4 changes: 4 additions & 0 deletions crates/egui/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -98,3 +98,7 @@ log = { workspace = true, optional = true }
puffin = { workspace = true, optional = true }
ron = { workspace = true, optional = true }
serde = { workspace = true, optional = true, features = ["derive", "rc"] }


[dev-dependencies]
egui_kittest = { workspace = true, features = ["wgpu", "snapshot"] }
2 changes: 2 additions & 0 deletions crates/egui/src/containers/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ pub(crate) mod area;
pub mod collapsing_header;
mod combo_box;
pub mod frame;
pub mod modal;
pub mod panel;
pub mod popup;
pub(crate) mod resize;
Expand All @@ -18,6 +19,7 @@ pub use {
collapsing_header::{CollapsingHeader, CollapsingResponse},
combo_box::*,
frame::Frame,
modal::{Modal, ModalResponse},
panel::{CentralPanel, SidePanel, TopBottomPanel},
popup::*,
resize::Resize,
Expand Down
158 changes: 158 additions & 0 deletions crates/egui/src/containers/modal.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
use crate::{
Area, Color32, Context, Frame, Id, InnerResponse, Order, Response, Sense, Ui, UiBuilder,
};
use emath::{Align2, Vec2};

/// A modal dialog.
/// Similar to a [`crate::Window`] but centered and with a backdrop that
/// blocks input to the rest of the UI.
///
/// You can show multiple modals on top of each other. The top most modal will always be
/// the most recently shown one.
pub struct Modal {
pub area: Area,
pub backdrop_color: Color32,
pub frame: Option<Frame>,
}

impl Modal {
/// Create a new Modal. The id is passed to the area.
pub fn new(id: Id) -> Self {
Self {
area: Self::default_area(id),
backdrop_color: Color32::from_black_alpha(100),
frame: None,
}
}

/// Returns an area customized for a modal.
/// Makes these changes to the default area:
/// - sense: hover
/// - anchor: center
/// - order: foreground
pub fn default_area(id: Id) -> Area {
Area::new(id)
.sense(Sense::hover())
.anchor(Align2::CENTER_CENTER, Vec2::ZERO)
.order(Order::Foreground)
}

/// Set the frame of the modal.
///
/// Default is [`Frame::popup`].
#[inline]
pub fn frame(mut self, frame: Frame) -> Self {
self.frame = Some(frame);
self
}

/// Set the backdrop color of the modal.
///
/// Default is `Color32::from_black_alpha(100)`.
#[inline]
pub fn backdrop_color(mut self, color: Color32) -> Self {
self.backdrop_color = color;
self
}

/// Set the area of the modal.
///
/// Default is [`Modal::default_area`].
#[inline]
pub fn area(mut self, area: Area) -> Self {
self.area = area;
self
}

/// Show the modal.
pub fn show<T>(self, ctx: &Context, content: impl FnOnce(&mut Ui) -> T) -> ModalResponse<T> {
let (is_top_modal, any_popup_open) = ctx.memory_mut(|mem| {
mem.set_modal_layer(self.area.layer());
(
mem.top_modal_layer() == Some(self.area.layer()),
mem.any_popup_open(),
)
});
let InnerResponse {
inner: (inner, backdrop_response),
response,
} = self.area.show(ctx, |ui| {
let mut backdrop = ui.new_child(UiBuilder::new().max_rect(ui.ctx().screen_rect()));
let backdrop_response = backdrop_ui(&mut backdrop, self.backdrop_color);

let frame = self.frame.unwrap_or_else(|| Frame::popup(ui.style()));

// 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: true,
drag: true,
focusable: false,
}),
|ui| frame.show(ui, content).inner,
)
.inner;

(inner, backdrop_response)
});

ModalResponse {
response,
backdrop_response,
inner,
is_top_modal,
any_popup_open,
}
}
}

fn backdrop_ui(ui: &mut Ui, color: Color32) -> Response {
// Ensure we capture any click and drag events
let response = ui.allocate_response(
ui.available_size(),
Sense {
click: true,
drag: true,
focusable: false,
},
);

ui.painter().rect_filled(response.rect, 0.0, color);

response
}

/// The response of a modal dialog.
pub struct ModalResponse<T> {
/// The response of the modal contents
pub response: Response,

/// The response of the modal backdrop
pub backdrop_response: Response,

/// The inner response from the content closure
pub inner: T,

/// Is this the top most modal?
pub is_top_modal: bool,

/// Is there any popup open?
/// We need to check this before the modal contents are shown, so we can know if any popup
/// was open when checking if the escape key was clicked.
pub any_popup_open: bool,
}

impl<T> ModalResponse<T> {
/// Should the modal be closed?
/// Returns true if:
/// - the backdrop was clicked
/// - this is the top most modal, no popup is open and the escape key was pressed
pub fn should_close(&self) -> bool {
let ctx = &self.response.ctx;
let escape_clicked = ctx.input(|i| i.key_pressed(crate::Key::Escape));
self.backdrop_response.clicked()
|| (self.is_top_modal && !self.any_popup_open && escape_clicked)
}
}
9 changes: 6 additions & 3 deletions crates/egui/src/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1160,6 +1160,9 @@ impl Context {
/// same widget, then `allow_focus` should only be true once (like in [`Ui::new`] (true) and [`Ui::remember_min_rect`] (false)).
#[allow(clippy::too_many_arguments)]
pub(crate) fn create_widget(&self, w: WidgetRect, allow_focus: bool) -> Response {
let interested_in_focus =
w.enabled && w.sense.focusable && self.memory(|mem| mem.allows_interaction(w.layer_id));

// Remember this widget
self.write(|ctx| {
let viewport = ctx.viewport();
Expand All @@ -1169,12 +1172,12 @@ impl Context {
// but also to know when we have reached the widget we are checking for cover.
viewport.this_pass.widgets.insert(w.layer_id, w);

if allow_focus && w.sense.focusable {
ctx.memory.interested_in_focus(w.id);
if allow_focus && interested_in_focus {
ctx.memory.interested_in_focus(w.id, w.layer_id);
}
});

if allow_focus && (!w.enabled || !w.sense.focusable || !w.layer_id.allow_interaction()) {
if allow_focus && !interested_in_focus {
// Not interested or allowed input:
self.memory_mut(|mem| mem.surrender_focus(w.id));
}
Expand Down
1 change: 1 addition & 0 deletions crates/egui/src/layers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ impl LayerId {
}

#[inline(always)]
#[deprecated = "Use `Memory::allows_interaction` instead"]
pub fn allow_interaction(&self) -> bool {
self.order.allow_interaction()
}
Expand Down
90 changes: 86 additions & 4 deletions crates/egui/src/memory/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -513,6 +513,12 @@ pub(crate) struct Focus {
/// Set when looking for widget with navigational keys like arrows, tab, shift+tab.
focus_direction: FocusDirection,

/// The top-most modal layer from the previous frame.
top_modal_layer: Option<LayerId>,

/// The top-most modal layer from the current frame.
top_modal_layer_current_frame: Option<LayerId>,

/// A cache of widget IDs that are interested in focus with their corresponding rectangles.
focus_widgets_cache: IdMap<Rect>,
}
Expand Down Expand Up @@ -623,6 +629,8 @@ impl Focus {
self.focused_widget = None;
}
}

self.top_modal_layer = self.top_modal_layer_current_frame.take();
}

pub(crate) fn had_focus_last_frame(&self, id: Id) -> bool {
Expand Down Expand Up @@ -676,6 +684,14 @@ impl Focus {
self.last_interested = Some(id);
}

fn set_modal_layer(&mut self, layer_id: LayerId) {
self.top_modal_layer_current_frame = Some(layer_id);
}

pub(crate) fn top_modal_layer(&self) -> Option<LayerId> {
self.top_modal_layer
}

fn reset_focus(&mut self) {
self.focus_direction = FocusDirection::None;
}
Expand Down Expand Up @@ -877,18 +893,61 @@ impl Memory {
}
}

/// Does this layer allow interaction?
/// Returns true if
/// - the layer is not behind a modal layer
/// - the [`Order`] allows interaction
pub fn allows_interaction(&self, layer_id: LayerId) -> bool {
let is_above_modal_layer =
if let Some(modal_layer) = self.focus().and_then(|f| f.top_modal_layer) {
matches!(
self.areas().compare_order(layer_id, modal_layer),
std::cmp::Ordering::Equal | std::cmp::Ordering::Greater
)
} else {
true
};
let ordering_allows_interaction = layer_id.order.allow_interaction();
is_above_modal_layer && ordering_allows_interaction
}

/// Register this widget as being interested in getting keyboard focus.
/// This will allow the user to select it with tab and shift-tab.
/// This is normally done automatically when handling interactions,
/// but it is sometimes useful to pre-register interest in focus,
/// e.g. before deciding which type of underlying widget to use,
/// as in the [`crate::DragValue`] widget, so a widget can be focused
/// and rendered correctly in a single frame.
///
/// Pass in the `layer_id` of the layer that the widget is in.
#[inline(always)]
pub fn interested_in_focus(&mut self, id: Id) {
pub fn interested_in_focus(&mut self, id: Id, layer_id: LayerId) {
if !self.allows_interaction(layer_id) {
return;
}
self.focus_mut().interested_in_focus(id);
}

/// Limit focus to widgets on the given layer and above.
/// If this is called multiple times per frame, the top layer wins.
pub fn set_modal_layer(&mut self, layer_id: LayerId) {
if let Some(current) = self.focus().and_then(|f| f.top_modal_layer_current_frame) {
if matches!(
self.areas().compare_order(layer_id, current),
std::cmp::Ordering::Less
) {
return;
}
}

self.focus_mut().set_modal_layer(layer_id);
}

/// Get the top modal layer (from the previous frame).
pub fn top_modal_layer(&self) -> Option<LayerId> {
self.focus()?.top_modal_layer()
}

/// Stop editing the active [`TextEdit`](crate::TextEdit) (if any).
#[inline(always)]
pub fn stop_text_input(&mut self) {
Expand Down Expand Up @@ -1037,6 +1096,9 @@ impl Memory {

// ----------------------------------------------------------------------------

/// Map containing the index of each layer in the order list, for quick lookups.
type OrderMap = HashMap<LayerId, usize>;

/// Keeps track of [`Area`](crate::containers::area::Area)s, which are free-floating [`Ui`](crate::Ui)s.
/// These [`Area`](crate::containers::area::Area)s can be in any [`Order`].
#[derive(Clone, Debug, Default)]
Expand All @@ -1048,6 +1110,9 @@ pub struct Areas {
/// Back-to-front, top is last.
order: Vec<LayerId>,

/// Actual order of the layers, pre-calculated each frame.
order_map: OrderMap,

visible_last_frame: ahash::HashSet<LayerId>,
visible_current_frame: ahash::HashSet<LayerId>,

Expand Down Expand Up @@ -1079,12 +1144,28 @@ impl Areas {
}

/// For each layer, which [`Self::order`] is it in?
pub(crate) fn order_map(&self) -> HashMap<LayerId, usize> {
self.order
pub(crate) fn order_map(&self) -> &OrderMap {
&self.order_map
}

/// Compare the order of two layers, based on the order list from last frame.
/// May return [`std::cmp::Ordering::Equal`] if the layers are not in the order list.
pub(crate) fn compare_order(&self, a: LayerId, b: LayerId) -> std::cmp::Ordering {
if let (Some(a), Some(b)) = (self.order_map.get(&a), self.order_map.get(&b)) {
a.cmp(b)
} else {
a.order.cmp(&b.order)
}
}

/// Calculates the order map.
fn calculate_order_map(&mut self) {
self.order_map = self
.order
.iter()
.enumerate()
.map(|(i, id)| (*id, i))
.collect()
.collect();
}

pub(crate) fn set_state(&mut self, layer_id: LayerId, state: area::AreaState) {
Expand Down Expand Up @@ -1209,6 +1290,7 @@ impl Areas {
};
order.splice(parent_pos..=parent_pos, moved_layers);
}
self.calculate_order_map();
}
}

Expand Down
2 changes: 1 addition & 1 deletion crates/egui/src/widgets/drag_value.rs
Original file line number Diff line number Diff line change
Expand Up @@ -452,7 +452,7 @@ impl<'a> Widget for DragValue<'a> {
// in button mode for just one frame. This is important for
// screen readers.
let is_kb_editing = ui.memory_mut(|mem| {
mem.interested_in_focus(id);
mem.interested_in_focus(id, ui.layer_id());
mem.has_focus(id)
});

Expand Down
Loading
Loading