-
Notifications
You must be signed in to change notification settings - Fork 373
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add support for modal windows to
re_ui
and use it for the Space Vie…
…w entity picker (#4577) ### What Since egui doesn't have support for modal windows yet (emilk/egui#686), we rolled our own for the Space View entity picker ("Add/remove entities"). This PR abstracts the modal creation code into a dedicated feature in `re_ui`, and use that abstraction for the space view entity picker. The new `re_ui::modal` has support for two aspects of modal window management: - The modal window itself (`re_ui::modal::Modal`), which handles dimming the background, a close button, and exiting with ESC and/or clicking outside of the window. - The handling of the modal window (`re_ui::modal::ModalHandler`). The `Modal` struct should be kept only while the modal is actually shown, so it's typically held in and `Option<Modal>`. `ModalHandler` implements the house keeping of populating the `Option` when the modal must be displayed, and dropping it when the modal is closed. **Note**: with this PR, the modals are now properly centred, although erroneous centring behaviour might happen that require deleting your `app.ron`. This will eventually be fixed with a new egui release that includes emilk/egui#3721 Usage: <img width="583" alt="image" src="https://github.com/rerun-io/rerun/assets/49431240/a079def4-f11a-4eeb-a3ec-6c777d078bdd"> <img width="443" alt="image" src="https://github.com/rerun-io/rerun/assets/49431240/13114dd4-3051-486c-9e4a-df4c783a0780"> ### Checklist * [x] I have read and agree to [Contributor Guide](https://github.com/rerun-io/rerun/blob/main/CONTRIBUTING.md) and the [Code of Conduct](https://github.com/rerun-io/rerun/blob/main/CODE_OF_CONDUCT.md) * [x] I've included a screenshot or gif (if applicable) * [x] I have tested the web demo (if applicable): * Using newly built examples: [app.rerun.io](https://app.rerun.io/pr/4577/index.html) * Using examples from latest `main` build: [app.rerun.io](https://app.rerun.io/pr/4577/index.html?manifest_url=https://app.rerun.io/version/main/examples_manifest.json) * Using full set of examples from `nightly` build: [app.rerun.io](https://app.rerun.io/pr/4577/index.html?manifest_url=https://app.rerun.io/version/nightly/examples_manifest.json) * [x] The PR title and labels are set such as to maximize their usefulness for the next release's CHANGELOG - [PR Build Summary](https://build.rerun.io/pr/4577) - [Docs preview](https://rerun.io/preview/fb5add0047f108769c413393d65d52e362cbcc8e/docs) <!--DOCS-PREVIEW--> - [Examples preview](https://rerun.io/preview/fb5add0047f108769c413393d65d52e362cbcc8e/examples) <!--EXAMPLES-PREVIEW--> - [Recent benchmark results](https://build.rerun.io/graphs/crates.html) - [Wasm size tracking](https://build.rerun.io/graphs/sizes.html) --------- Co-authored-by: Andreas Reich <[email protected]>
- Loading branch information
Showing
5 changed files
with
273 additions
and
93 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,187 @@ | ||
/// Helper object to handle a [`Modal`] window. | ||
/// | ||
/// A [`Modal`] is typically held only so long as it is displayed, so it's typically stored in an | ||
/// [`Option`]. This helper object handles that for your. | ||
/// | ||
/// Usage: | ||
/// ``` | ||
/// # use re_ui::modal::{Modal, ModalHandler}; | ||
/// # use re_ui::ReUi; | ||
/// let mut modal_handler = ModalHandler::default(); | ||
/// | ||
/// # egui::__run_test_ui(|ui| { | ||
/// # let re_ui = ReUi::load_and_apply(ui.ctx()); | ||
/// # let re_ui = &re_ui; | ||
/// if ui.button("Open").clicked() { | ||
/// modal_handler.open(); | ||
/// } | ||
/// | ||
/// modal_handler.ui(re_ui, ui, || Modal::new("Modal Window"), |_, ui, _| { | ||
/// ui.label("Modal content"); | ||
/// }); | ||
/// # }); | ||
/// ``` | ||
#[derive(Default)] | ||
pub struct ModalHandler { | ||
modal: Option<Modal>, | ||
should_open: bool, | ||
} | ||
|
||
impl ModalHandler { | ||
/// Open the model next time the [`ModalHandler::ui`] method is called. | ||
pub fn open(&mut self) { | ||
self.should_open = true; | ||
} | ||
|
||
/// Draw the modal window, creating/destroying it as required. | ||
pub fn ui<R>( | ||
&mut self, | ||
re_ui: &crate::ReUi, | ||
ui: &mut egui::Ui, | ||
make_modal: impl FnOnce() -> Modal, | ||
content_ui: impl FnOnce(&crate::ReUi, &mut egui::Ui, &mut bool) -> R, | ||
) -> Option<R> { | ||
if self.modal.is_none() && self.should_open { | ||
self.modal = Some(make_modal()); | ||
self.should_open = false; | ||
} | ||
|
||
if let Some(modal) = &mut self.modal { | ||
let ModalResponse { inner, open } = modal.ui(re_ui, ui, content_ui); | ||
|
||
if !open { | ||
self.modal = None; | ||
} | ||
|
||
inner | ||
} else { | ||
None | ||
} | ||
} | ||
} | ||
|
||
/// Response returned by [`Modal::ui`]. | ||
pub struct ModalResponse<R> { | ||
/// What the content closure returned, if it was actually run. | ||
pub inner: Option<R>, | ||
|
||
/// Whether the modal should remain open. | ||
pub open: bool, | ||
} | ||
|
||
/// Show a modal window with Rerun style. | ||
/// | ||
/// [`Modal`] fakes as a modal window, since egui [doesn't have them yet](https://github.com/emilk/egui/issues/686). | ||
/// This done by dimming the background and capturing clicks outside the window. | ||
/// | ||
/// Note that [`Modal`] are typically used via the [`ModalHandler`] helper object to reduce boilerplate. | ||
pub struct Modal { | ||
title: String, | ||
default_height: Option<f32>, | ||
} | ||
|
||
impl Modal { | ||
/// Create a new modal with the given title. | ||
pub fn new(title: &str) -> Self { | ||
Self { | ||
title: title.to_owned(), | ||
default_height: None, | ||
} | ||
} | ||
|
||
/// Set the default height of the modal window. | ||
#[inline] | ||
pub fn default_height(mut self, default_height: f32) -> Self { | ||
self.default_height = Some(default_height); | ||
self | ||
} | ||
|
||
/// Show the modal window. | ||
/// | ||
/// Typically called by [`ModalHandler::ui`]. | ||
pub fn ui<R>( | ||
&mut self, | ||
re_ui: &crate::ReUi, | ||
ui: &mut egui::Ui, | ||
content_ui: impl FnOnce(&crate::ReUi, &mut egui::Ui, &mut bool) -> R, | ||
) -> ModalResponse<R> { | ||
Self::dim_background(ui); | ||
|
||
let mut open = ui.input(|i| !i.key_pressed(egui::Key::Escape)); | ||
|
||
let mut window = egui::Window::new(&self.title) | ||
.pivot(egui::Align2::CENTER_CENTER) | ||
.fixed_pos(ui.ctx().screen_rect().center()) | ||
.constrain_to(ui.ctx().screen_rect()) | ||
.collapsible(false) | ||
.resizable(true) | ||
.frame(egui::Frame { | ||
fill: ui.visuals().panel_fill, | ||
inner_margin: crate::ReUi::view_padding().into(), | ||
..Default::default() | ||
}) | ||
.title_bar(false); | ||
|
||
if let Some(default_height) = self.default_height { | ||
window = window.default_height(default_height); | ||
} | ||
|
||
let response = window.show(ui.ctx(), |ui| { | ||
Self::title_bar(re_ui, ui, &self.title, &mut open); | ||
content_ui(re_ui, ui, &mut open) | ||
}); | ||
|
||
// Any click outside causes the window to close. | ||
let cursor_was_over_window = response | ||
.as_ref() | ||
.and_then(|response| { | ||
ui.input(|i| i.pointer.interact_pos()) | ||
.map(|interact_pos| response.response.rect.contains(interact_pos)) | ||
}) | ||
.unwrap_or(false); | ||
if !cursor_was_over_window && ui.input(|i| i.pointer.any_pressed()) { | ||
open = false; | ||
} | ||
|
||
ModalResponse { | ||
inner: response.and_then(|response| response.inner), | ||
open, | ||
} | ||
} | ||
|
||
/// Dim the background to indicate that the window is modal. | ||
#[allow(clippy::needless_pass_by_ref_mut)] | ||
fn dim_background(ui: &mut egui::Ui) { | ||
let painter = egui::Painter::new( | ||
ui.ctx().clone(), | ||
egui::LayerId::new(egui::Order::PanelResizeLine, egui::Id::new("DimLayer")), | ||
egui::Rect::EVERYTHING, | ||
); | ||
painter.add(egui::Shape::rect_filled( | ||
ui.ctx().screen_rect(), | ||
egui::Rounding::ZERO, | ||
egui::Color32::from_black_alpha(128), | ||
)); | ||
} | ||
|
||
/// Display a title bar in our own style. | ||
fn title_bar(re_ui: &crate::ReUi, ui: &mut egui::Ui, title: &str, open: &mut bool) { | ||
ui.horizontal(|ui| { | ||
ui.strong(title); | ||
|
||
ui.add_space(16.0); | ||
|
||
let mut ui = ui.child_ui( | ||
ui.max_rect(), | ||
egui::Layout::right_to_left(egui::Align::Center), | ||
); | ||
if re_ui | ||
.small_icon_button(&mut ui, &crate::icons::CLOSE) | ||
.clicked() | ||
{ | ||
*open = false; | ||
} | ||
}); | ||
ui.separator(); | ||
} | ||
} |
Oops, something went wrong.