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

Modal Dialogs #839

Draft
wants to merge 5 commits into
base: master
Choose a base branch
from
Draft
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 CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ NOTE: [`epaint`](epaint/CHANGELOG.md), [`eframe`](eframe/CHANGELOG.md), [`egui_w
## Unreleased

### Added ⭐
* Add support for modal dialogs.
* Add horizontal scrolling support to `ScrollArea` and `Window` (opt-in).
* `TextEdit::layouter`: Add custom text layout for e.g. syntax highlighting or WYSIWYG.
* `Fonts::layout_job`: New text layout engine allowing mixing fonts, colors and styles, with underlining and strikethrough.
Expand Down
1 change: 1 addition & 0 deletions 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(crate) mod collapsing_header;
mod combo_box;
pub(crate) mod frame;
pub mod modal;
pub mod panel;
pub mod popup;
pub(crate) mod resize;
Expand Down
174 changes: 174 additions & 0 deletions egui/src/containers/modal.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
//! Show modal dialog.

use crate::*;

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

/// Common modal state
///
/// > A modal dialog is a dialog that appears on top of the main content and moves the system into a special mode requiring user interaction. This dialog disables the main content until the user explicitly interacts with the modal dialog.
/// > – [Modal & Nonmodal Dialogs: When (& When Not) to Use Them](https://www.nngroup.com/articles/modal-nonmodal-dialog/)
/// For this implementation, the above suggests copying the common state approach from [`MonoState`]
#[derive(Clone, Debug, Default)]
pub(crate) struct ModalMonoState {
/// The optional id the modal took focus from
previous_focused_id_opt: Option<Id>,
/// The id of the last modal shown to enforce modality
last_modal_id_opt: Option<Id>,
}

impl ModalMonoState {
/// The id source of the default modal
pub const DEFAULT_MODAL_ID_SOURCE: &'static str = "__default_modal";

/// Construct an id for the default modal
pub fn get_default_modal_id() -> Id {
Id::new(Self::DEFAULT_MODAL_ID_SOURCE)
}
/// Construct an interceptor color for the default modal
pub fn get_default_modal_interceptor_color() -> Color32 {
Color32::from_rgba_unmultiplied(0, 0, 0, 144)
}
}

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

/// Relinquish control of the modal. If the modal is showing, this must be called to show a new modal.
///
/// - No id is required to relinquish modal control – to prevent the application from entering a state where it's irrevocably stuck in a mode. In other words, the application must therefore track/manage when/whether it's allowable to relinquish control.
/// - Does nothing if the modal was not showing.
/// - If some id-bearing widget was previously focused, this returns the id.
pub fn relinquish_modal(ctx: &CtxRef) -> Option<Id> {
let last_modal_id_opt: Option<Id> = ctx
.memory()
.data_temp
.get_or_default::<ModalMonoState>()
.last_modal_id_opt;

ctx.memory()
.data_temp
.get_mut_or_default::<ModalMonoState>();
// try to determine whether the modal can be shown
let is_modal_controlled = last_modal_id_opt.is_some();
is_modal_controlled
.then(|| {
// modal control has been obtained
let previous_focused_id_opt: Option<Id> = ctx
.memory()
.data_temp
.get_mut_or_default::<ModalMonoState>()
.previous_focused_id_opt
.take();
let _ = ctx
.memory()
.data_temp
.get_mut_or_default::<ModalMonoState>()
.last_modal_id_opt
.take();
previous_focused_id_opt
})
.flatten()
}
/// Show a modal dialog that intercepts interaction with other ui elements whilst visible.
///
/// - The returned inner response includes the result of the provided contents ui function as well as the response from clicking the interaction interceptor.
/// - Returns `None` if a modal is already showing.
///
/// ```
/// # let mut ctx = egui::CtxRef::default();
/// # ctx.begin_frame(Default::default());
/// # let ctx = &ctx;
/// let id_0 = egui::Id::new("my_0th_modal");
/// let id_1 = egui::Id::new("my_1st_modal");
/// let r_opt = egui::modal::show_custom_modal(
/// ctx,
/// id_0,
/// None,
/// |ui| {
/// ui.label("This is a modal dialog");
/// });
/// assert_eq!(r_opt, Some(()), "A modal dialog with an id may show once");
/// let r_opt = egui::modal::show_custom_modal(
/// ctx,
/// id_0,
/// None,
/// |ui| {
/// ui.label("This is the same (by id) modal dialog");
/// });
/// assert_eq!(r_opt, Some(()), "A modal dialog with an id may show again/update");
/// let r_opt = egui::modal::show_custom_modal(
/// ctx,
/// id_1,
/// None,
/// |ui| {
/// ui.label("This wants to be a modal dialog, yet shall produce nary a ui ere the grotesque and catastrophic violation of some invariant. ");
/// });
/// assert_eq!(r_opt, None, "A modal dialog may not appear whilst another has control");
/// egui::modal::relinquish_modal(ctx);
/// let r_opt = egui::modal::show_custom_modal(
/// ctx,
/// id_1,
/// None,
/// |ui| {
/// ui.label("This wants to be a modal dialog, and its dreams are fulfilled.");
/// });
/// assert_eq!(r_opt, Some(()), "A modal dialog may appear after another has relinquished control");
/// ```
pub fn show_custom_modal<R>(
ctx: &CtxRef,
id: Id,
background_color_opt: Option<Color32>,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> Option<R> {
use containers::*;
// Clone some context state
let previous_focused_id_opt: Option<Id> = ctx.memory().focus();
let last_modal_id_opt = ctx
.memory()
.data_temp
.get_or_default::<ModalMonoState>()
.last_modal_id_opt;

// Enforce modality
let have_modal_control = last_modal_id_opt.is_none()
|| last_modal_id_opt == Some(id)
|| last_modal_id_opt == Some(ModalMonoState::get_default_modal_id());
if have_modal_control {
ctx.memory()
.data_temp
.get_mut_or_default::<ModalMonoState>()
.last_modal_id_opt
.replace(id);
ctx.memory()
.data_temp
.get_mut_or_default::<ModalMonoState>()
.previous_focused_id_opt = previous_focused_id_opt;
// show the modal taking up the whole screen
let InnerResponse { inner, .. } = Area::new(id)
.interactable(true)
.fixed_pos(Pos2::ZERO)
// .order(Order::Foreground)
.show(ctx, |ui| {
let background_color = background_color_opt
.unwrap_or_else(ModalMonoState::get_default_modal_interceptor_color);
let interceptor_rect = ui.ctx().input().screen_rect();
// create an empty interaction interceptor
// for some reason, using Sense::click() instead of Sense::hover()
// seems to intercept not only clicks to the unoccupied areas but also to the user-provided ui
ui.allocate_response(interceptor_rect.size(), Sense::hover());
let InnerResponse {
inner: user_ui_inner,
..
} = ui.allocate_ui_at_rect(interceptor_rect, |ui| {
// create a customizable visual indicator signifying to the user that this is a modal mode
ui.painter()
.add(Shape::rect_filled(interceptor_rect, 0.0, background_color));
add_contents(ui)
});
user_ui_inner
});
Some(inner)
} else {
None
}
}
3 changes: 2 additions & 1 deletion egui_demo_lib/src/apps/demo/demo_app_windows.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ impl Default for Demos {
Box::new(super::font_book::FontBook::default()),
Box::new(super::MiscDemoWindow::default()),
Box::new(super::multi_touch::MultiTouch::default()),
Box::new(super::modals::ModalOptions::default()),
Box::new(super::painting::Painting::default()),
Box::new(super::plot_demo::PlotDemo::default()),
Box::new(super::scrolling::Scrolling::default()),
Expand All @@ -43,7 +44,7 @@ impl Demos {
.name()
.to_owned(),
);

Self { demos, open }
}

Expand Down
1 change: 1 addition & 0 deletions egui_demo_lib/src/apps/demo/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ pub mod drag_and_drop;
pub mod font_book;
pub mod layout_test;
pub mod misc_demo_window;
pub mod modals;
pub mod multi_touch;
pub mod painting;
pub mod password;
Expand Down
146 changes: 146 additions & 0 deletions egui_demo_lib/src/apps/demo/modals.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
use egui::*;

#[derive(Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct ModalOptions {
id_source: String,
click_away_dismisses: bool,
background_color: Color32,
close_key_opt: Option<Key>,
is_modal_showing: bool,
id_error_message_opt: Option<String>,
}

impl Default for ModalOptions {
fn default() -> Self {
Self {
click_away_dismisses: true,
background_color: Color32::from_rgba_unmultiplied(64, 64, 64, 192),
close_key_opt: Some(Key::Escape),
id_source: String::from("demo_modal_options"),
is_modal_showing: false,
id_error_message_opt: None,
}
}
}

impl super::Demo for ModalOptions {
fn name(&self) -> &'static str {
"! Modal Options"
}

fn show(&mut self, ctx: &egui::CtxRef, open: &mut bool) {
use super::View as _;

// Create a window for controlling modal details
egui::Window::new("demo_modal_options")
.open(open)
.show(ctx, |ui| self.ui(ui));
}
}

impl super::View for ModalOptions {
fn ui(&mut self, ui: &mut egui::Ui) {
let Self {
click_away_dismisses,
background_color,
close_key_opt,
id_source,
is_modal_showing,
id_error_message_opt,
} = self;

ui.group(|ui| {
ui.horizontal(|ui| {
if ui.button("show modal").clicked() {
*is_modal_showing = true;
}
});
});

ui.horizontal(|ui| {
ui.group(|ui| {
ui.vertical(|ui| {
ui.horizontal(|ui| {
ui.label("Id:");
ui.text_edit_singleline(id_source)
.on_hover_text("Enter a custom id");
});
if let Some(id_error_message) = id_error_message_opt.as_ref() {
*is_modal_showing = false;
ui.colored_label(Color32::RED, id_error_message);
if ui.button("Relinquish id-based control").clicked() {
egui::modal::relinquish_modal(ui.ctx());
*id_error_message_opt = None;
}
}
// ui.checkbox(
// click_away_dismisses,
// "click_away_dismisses"
// );
});
});
});
let outer_ctx = ui.ctx();
if *is_modal_showing {
let id = Id::new(id_source);
if let Some(mut response) = egui::modal::show_custom_modal(
outer_ctx,
id,
Some(*background_color),
|_ui| {
// Note that the inner Area needs to be shown with the outer context to appear above the modal interceptor
// Also, the area needs to be in the foreground to appear atop the modal's inner ui
Area::new("An area for some modal content")
.anchor(Align2::CENTER_CENTER, [0.0, 0.0])
.order(Order::Foreground)
.show(outer_ctx, |ui| {
ui.group(|ui| {
ui.vertical(|ui| {
ui.label(format!("This modal was created with the egui Id `{:?}`", id) );
// ui.separator();
ui.label("You cannot interact with the items behind the modal, but you can interact with the ui here.");
if let Some(close_key) = close_key_opt.as_ref() {
// ui.separator();
ui.label(format!("This modal can be closed using the `{:?}` key", close_key) );
if ui.ctx().input().key_pressed(*close_key) {
*is_modal_showing = false;
}
}
if ui.button("hide modal").clicked() {
*is_modal_showing = false;
}
if ui.button("hide modal and relinquish id-based control").clicked() {
egui::modal::relinquish_modal(ui.ctx());
*is_modal_showing = false;
}
egui::widgets::color_picker::color_edit_button_srgba(
ui,
background_color, color_picker::Alpha::BlendOrAdditive
);
}).response
}).inner
}).inner
},
) {
response = response.interact(Sense::click());
if response.has_focus() && response.clicked_elsewhere() && *click_away_dismisses {
println!("clicked away {:#?}", response);
*is_modal_showing = false;
}
response.request_focus();
*id_error_message_opt = None;
} else {
*id_error_message_opt = Some(
"relinquish_modal must be called to show a modal with a new id ".to_string(),
);
}
}
ui.separator();

ui.horizontal(|ui| {
egui::reset_button(ui, self);
ui.add(crate::__egui_github_link_file!());
});
}
}