Skip to content

Commit

Permalink
Add support for modal windows to re_ui and use it for the Space Vie…
Browse files Browse the repository at this point in the history
…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
abey79 and Wumpf authored Dec 19, 2023
1 parent 6342c60 commit cde58b1
Show file tree
Hide file tree
Showing 5 changed files with 273 additions and 93 deletions.
55 changes: 48 additions & 7 deletions crates/re_ui/examples/re_ui_example.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ pub struct ExampleApp {

tree: egui_tiles::Tree<Tab>,

modal_handler: re_ui::modal::ModalHandler,

left_panel: bool,
right_panel: bool,
bottom_panel: bool,
Expand Down Expand Up @@ -99,6 +101,7 @@ impl ExampleApp {
text_log_rx,

tree,
modal_handler: Default::default(),

left_panel: true,
right_panel: true,
Expand Down Expand Up @@ -158,6 +161,8 @@ impl eframe::App for ExampleApp {
ui.strong("Bottom panel");
});

// LEFT PANEL

egui::SidePanel::left("left_panel")
.default_width(500.0)
.frame(egui::Frame {
Expand All @@ -180,13 +185,25 @@ impl eframe::App for ExampleApp {
});

if ui.button("Log info").clicked() {
re_log::info!("A lot of text on info level.\nA lot of text in fact. So much that we should ideally be auto-wrapping it at some point, much earlier than this.");
re_log::info!(
"A lot of text on info level.\nA lot of text in fact. So \
much that we should ideally be auto-wrapping it at some point, much \
earlier than this."
);
}
if ui.button("Log warn").clicked() {
re_log::warn!("A lot of text on warn level.\nA lot of text in fact. So much that we should ideally be auto-wrapping it at some point, much earlier than this.");
re_log::warn!(
"A lot of text on warn level.\nA lot of text in fact. So \
much that we should ideally be auto-wrapping it at some point, much \
earlier than this."
);
}
if ui.button("Log error").clicked() {
re_log::error!("A lot of text on error level.\nA lot of text in fact. So much that we should ideally be auto-wrapping it at some point, much earlier than this.");
re_log::error!(
"A lot of text on error level.\nA lot of text in fact. \
So much that we should ideally be auto-wrapping it at some point, much \
earlier than this."
);
}
});

Expand All @@ -204,6 +221,21 @@ impl eframe::App for ExampleApp {
});
ui.label(format!("Latest command: {}", self.latest_cmd));

// ---

if ui.button("Open modal").clicked() {
self.modal_handler.open();
}

self.modal_handler.ui(
&self.re_ui,
ui,
|| re_ui::modal::Modal::new("Modal window"),
|_, ui, _| ui.label("This is a modal window."),
);

// ---

self.re_ui.large_collapsing_header(ui, "Data", true, |ui| {
ui.label("Some data here");
});
Expand All @@ -213,10 +245,19 @@ impl eframe::App for ExampleApp {
ui.label("Some blueprint stuff here, that might be wide.");
self.re_ui.checkbox(ui, &mut self.dummy_bool, "Checkbox");

self.re_ui.collapsing_header(ui, "Collapsing header", true, |ui| {
ui.label("Some data here");
self.re_ui.checkbox(ui, &mut self.dummy_bool, "Checkbox");
});
self.re_ui.collapsing_header(
ui,
"Collapsing header",
true,
|ui| {
ui.label("Some data here");
self.re_ui.checkbox(
ui,
&mut self.dummy_bool,
"Checkbox",
);
},
);
});
});
});
Expand Down
1 change: 1 addition & 0 deletions crates/re_ui/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ pub mod egui_helpers;
pub mod icons;
mod layout_job_builder;
pub mod list_item;
pub mod modal;
pub mod toasts;
mod toggle_switch;

Expand Down
187 changes: 187 additions & 0 deletions crates/re_ui/src/modal.rs
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();
}
}
Loading

0 comments on commit cde58b1

Please sign in to comment.