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 support for modal windows to re_ui and use it for the Space View entity picker #4577

Merged
merged 7 commits into from
Dec 19, 2023
Merged
Show file tree
Hide file tree
Changes from 5 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
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
184 changes: 184 additions & 0 deletions crates/re_ui/src/modal.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
/// 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) -> 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).
abey79 marked this conversation as resolved.
Show resolved Hide resolved
/// This is typically use via the [`ModalHandler`] helper object.
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) -> 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())
.collapsible(false)
.resizable(true)
abey79 marked this conversation as resolved.
Show resolved Hide resolved
.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)
});

// 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
Loading