Skip to content

Commit

Permalink
Use ListItem in blueprint tree UI (#3118)
Browse files Browse the repository at this point in the history
### What

This PR back-ports `ListItem` into the blueprint tree UI. This includes
multiple fixes and additions to `ListItem` itself.

Note that this PR strictly replicates the current behaviour, which has
several issues:
- containers look inactive although they are active
- hidden elements look inactive although they are active
- cannot unhide one children of a hidden group
- hiding a container doesn't dim its children
- more generally, it would be nice to improve the visual distinction
between containers, space views, data group and data item (i.e. if you
_know_ what a space view vs. data group is, you'll find them, but the
tree UI does very little to actually _teach_ that distinction)

Fixes:
- #3044 
- #2946
- #2855

<img width="1274" alt="image"
src="https://github.com/rerun-io/rerun/assets/49431240/b84512ea-cbd0-450f-aebb-1c8fb6aea98a">


### 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 [demo.rerun.io](https://demo.rerun.io/pr/3118) (if
applicable)

- [PR Build Summary](https://build.rerun.io/pr/3118)
- [Docs
preview](https://rerun.io/preview/a33ae6b49d223f5fb3ed15da9ff14740b1b4efae/docs)
<!--DOCS-PREVIEW-->
- [Examples
preview](https://rerun.io/preview/a33ae6b49d223f5fb3ed15da9ff14740b1b4efae/examples)
<!--EXAMPLES-PREVIEW--><!--EXAMPLES-PREVIEW--><!--EXAMPLES-PREVIEW--><!--EXAMPLES-PREVIEW--><!--EXAMPLES-PREVIEW-->
- [Recent benchmark results](https://ref.rerun.io/dev/bench/)
- [Wasm size tracking](https://ref.rerun.io/dev/sizes/)
  • Loading branch information
abey79 authored Aug 29, 2023
1 parent 4029681 commit 86466ae
Show file tree
Hide file tree
Showing 9 changed files with 319 additions and 353 deletions.
14 changes: 11 additions & 3 deletions crates/re_data_ui/src/item_ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -245,13 +245,21 @@ pub fn data_blueprint_button_to(
let response = ui
.selectable_label(ctx.selection().contains(&item), text)
.on_hover_ui(|ui| {
ui.strong("Space View Entity");
ui.label(format!("Path: {entity_path}"));
entity_path.data_ui(ctx, ui, UiVerbosity::Reduced, &ctx.current_query());
data_blueprint_tooltip(ui, ctx, entity_path);
});
cursor_interact_with_selectable(ctx, response, item)
}

pub fn data_blueprint_tooltip(
ui: &mut egui::Ui,
ctx: &mut ViewerContext<'_>,
entity_path: &EntityPath,
) {
ui.strong("Space View Entity");
ui.label(format!("Path: {entity_path}"));
entity_path.data_ui(ctx, ui, UiVerbosity::Reduced, &ctx.current_query());
}

pub fn time_button(
ctx: &mut ViewerContext<'_>,
ui: &mut egui::Ui,
Expand Down
23 changes: 14 additions & 9 deletions crates/re_ui/examples/re_ui_example.rs
Original file line number Diff line number Diff line change
Expand Up @@ -317,15 +317,20 @@ impl eframe::App for ExampleApp {
self.re_ui
.list_item("Collapsing list item with icon")
.with_icon(&re_ui::icons::SPACE_VIEW_2D)
.show_collapsing(ui, true, |_re_ui, ui| {
self.re_ui.list_item("Sub-item").show(ui);
self.re_ui.list_item("Sub-item").show(ui);
self.re_ui
.list_item("Sub-item with icon")
.with_icon(&re_ui::icons::SPACE_VIEW_TEXT)
.show(ui);
self.re_ui.list_item("Sub-item").show(ui);
});
.show_collapsing(
ui,
"collapsing example".into(),
true,
|_re_ui, ui| {
self.re_ui.list_item("Sub-item").show(ui);
self.re_ui.list_item("Sub-item").show(ui);
self.re_ui
.list_item("Sub-item with icon")
.with_icon(&re_ui::icons::SPACE_VIEW_TEXT)
.show(ui);
self.re_ui.list_item("Sub-item").show(ui);
},
);
});
});
});
Expand Down
109 changes: 85 additions & 24 deletions crates/re_ui/src/list_item.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,15 @@ struct ListItemResponse {
collapse_response: Option<Response>,
}

/// Responses returned by [`ListItem::show_collapsing`].
pub struct ShowCollapsingResponse<R> {
/// Response from the item itself.
pub item_response: Response,

/// Response from the body, if it was displayed.
pub body_response: Option<R>,
}

/// Generic widget for use in lists.
///
/// Layout:
Expand Down Expand Up @@ -45,6 +54,8 @@ pub struct ListItem<'a> {
re_ui: &'a ReUi,
active: bool,
selected: bool,
subdued: bool,
force_hovered: bool,
collapse_openness: Option<f32>,
icon_fn:
Option<Box<dyn FnOnce(&ReUi, &mut egui::Ui, egui::Rect, egui::style::WidgetVisuals) + 'a>>,
Expand All @@ -59,6 +70,8 @@ impl<'a> ListItem<'a> {
re_ui,
active: true,
selected: false,
subdued: false,
force_hovered: false,
collapse_openness: None,
icon_fn: None,
buttons_fn: None,
Expand All @@ -77,6 +90,26 @@ impl<'a> ListItem<'a> {
self
}

/// Set the subdued state of the item.
// TODO(ab): this is a hack to implement the behavior of the blueprint tree UI, where active
// widget are displayed in a subdued state (container, hidden space views/entities). One
// slightly more correct way would be to override the color using a (color, index) pair
// related to the design system table.
pub fn subdued(mut self, subdued: bool) -> Self {
self.subdued = subdued;
self
}

/// Override the hovered state even if the item is not actually hovered.
///
/// Used to highlight items representing things that are hovered elsewhere in the UI. Note that
/// the [`egui::Response`] returned by [`Self::show`] and ]`Self::show_collapsing`] will still
/// reflect the actual hover state.
pub fn force_hovered(mut self, force_hovered: bool) -> Self {
self.force_hovered = force_hovered;
self
}

/// Provide an [`Icon`] to be displayed on the left of the item.
pub fn with_icon(self, icon: &'a Icon) -> Self {
self.with_icon_fn(|re_ui, ui, rect, visuals| {
Expand Down Expand Up @@ -115,40 +148,45 @@ impl<'a> ListItem<'a> {

/// Draw the item.
pub fn show(self, ui: &mut Ui) -> Response {
self.ui(ui).response
self.ui(ui, None).response
}

/// Draw the item as a collapsing header.
pub fn show_collapsing<R>(
mut self,
ui: &mut Ui,
id: egui::Id,
default_open: bool,
add_body: impl FnOnce(&ReUi, &mut egui::Ui) -> R,
) {
) -> ShowCollapsingResponse<R> {
let mut state = egui::collapsing_header::CollapsingState::load_with_default_open(
ui.ctx(),
ui.make_persistent_id(ui.id().with(self.text.text())),
id,
default_open,
);

// enable collapsing arrow
self.collapse_openness = Some(state.openness(ui.ctx()));

let re_ui = self.re_ui;
let response = self.ui(ui);
let response = self.ui(ui, Some(id));

if let Some(collapse_response) = response.collapse_response {
if collapse_response.clicked() {
state.toggle(ui);
}
}

state.show_body_indented(&response.response, ui, |ui| {
add_body(re_ui, ui);
});
let body_response =
state.show_body_indented(&response.response, ui, |ui| add_body(re_ui, ui));

ShowCollapsingResponse {
item_response: response.response,
body_response: body_response.map(|r| r.inner),
}
}

fn ui(self, ui: &mut Ui) -> ListItemResponse {
fn ui(self, ui: &mut Ui, id: Option<egui::Id>) -> ListItemResponse {
let collapse_extra = if self.collapse_openness.is_some() {
ReUi::collapsing_triangle_size().x + ReUi::text_to_icon_padding()
} else {
Expand All @@ -163,15 +201,27 @@ impl<'a> ListItem<'a> {
let desired_size = egui::vec2(ui.available_width(), ReUi::list_item_height());
let (rect, response) = ui.allocate_at_least(desired_size, egui::Sense::click());

// override_hover should not affect the returned response
let mut style_response = response.clone();
if self.force_hovered {
style_response.hovered = true;
}

let mut collapse_response = None;

if ui.is_rect_visible(rect) {
let visuals = if self.active {
ui.style().interact_selectable(&response, self.selected)
let mut visuals = if self.active {
ui.style()
.interact_selectable(&style_response, self.selected)
} else {
ui.visuals().widgets.inactive
};

if self.subdued {
//TODO(ab): hack, see ['ListItem::subdued']
visuals.fg_stroke.color = visuals.fg_stroke.color.gamma_multiply(0.5);
}

let mut bg_rect = rect;
bg_rect.extend_with_x(ui.clip_rect().right());
bg_rect.extend_with_x(ui.clip_rect().left());
Expand All @@ -185,7 +235,11 @@ impl<'a> ListItem<'a> {
));
let triangle_rect =
egui::Rect::from_min_size(triangle_pos, ReUi::collapsing_triangle_size());
let resp = ui.interact(triangle_rect, ui.id(), egui::Sense::click());
let resp = ui.interact(
triangle_rect,
id.unwrap_or(ui.id()).with("collapsing_triangle"),
egui::Sense::click(),
);
ReUi::paint_collapsing_triangle(ui, openness, &resp);
collapse_response = Some(resp);
}
Expand All @@ -201,18 +255,25 @@ impl<'a> ListItem<'a> {
}

// Handle buttons
let button_response =
if self.active && ui.interact(rect, ui.id(), egui::Sense::hover()).hovered() {
if let Some(buttons) = self.buttons_fn {
let mut ui =
ui.child_ui(rect, egui::Layout::right_to_left(egui::Align::Center));
Some(buttons(self.re_ui, &mut ui))
} else {
None
}
let button_response = if self.active
&& ui
.interact(
rect,
id.unwrap_or(ui.id()).with("buttons"),
egui::Sense::hover(),
)
.hovered()
{
if let Some(buttons) = self.buttons_fn {
let mut ui =
ui.child_ui(rect, egui::Layout::right_to_left(egui::Align::Center));
Some(buttons(self.re_ui, &mut ui))
} else {
None
};
}
} else {
None
};

// Draw text next to the icon.
let mut text_rect = rect;
Expand Down Expand Up @@ -247,9 +308,9 @@ impl<'a> ListItem<'a> {
let bg_fill = if button_response.map_or(false, |r| r.hovered()) {
Some(visuals.bg_fill)
} else if self.selected
|| response.hovered()
|| response.highlighted()
|| response.has_focus()
|| style_response.hovered()
|| style_response.highlighted()
|| style_response.has_focus()
{
Some(visuals.weak_bg_fill)
} else {
Expand Down
16 changes: 5 additions & 11 deletions crates/re_viewer/src/app_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -165,21 +165,15 @@ impl AppState {

// ListItem don't need vertical spacing so we disable it, but restore it
// before drawing the blueprint panel.
// TODO(ab): remove this once the blueprint tree uses list items
let v_space = ui.spacing().item_spacing.y;
ui.spacing_mut().item_spacing.y = 0.0;

recordings_panel_ui(&mut ctx, ui);
let recording_shown = recordings_panel_ui(&mut ctx, ui);

// TODO(ab): remove this frame once the blueprint tree uses list items
egui::Frame {
inner_margin: re_ui::ReUi::panel_margin(),
..Default::default()
if recording_shown {
ui.add_space(4.0);
}
.show(ui, |ui| {
ui.spacing_mut().item_spacing.y = v_space;
blueprint_panel_ui(&mut viewport.blueprint, &mut ctx, ui, &spaces_info);
});

blueprint_panel_ui(&mut viewport.blueprint, &mut ctx, ui, &spaces_info);
},
);

Expand Down
22 changes: 12 additions & 10 deletions crates/re_viewer/src/ui/blueprint_panel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,19 @@ pub fn blueprint_panel_ui(
ui: &mut egui::Ui,
spaces_info: &SpaceInfoCollection,
) {
ctx.re_ui.panel_title_bar_with_buttons(
ui,
"Blueprint",
Some("The Blueprint is where you can configure the Rerun Viewer"),
|ui| {
blueprint.add_new_spaceview_button_ui(ctx, ui, spaces_info);
reset_button_ui(blueprint, ctx, ui, spaces_info);
},
);
ctx.re_ui.panel_content(ui, |_, ui| {
ctx.re_ui.panel_title_bar_with_buttons(
ui,
"Blueprint",
Some("The Blueprint is where you can configure the Rerun Viewer"),
|ui| {
blueprint.add_new_spaceview_button_ui(ctx, ui, spaces_info);
reset_button_ui(blueprint, ctx, ui, spaces_info);
},
);

blueprint.tree_ui(ctx, ui);
blueprint.tree_ui(ctx, ui);
});
}

fn reset_button_ui(
Expand Down
30 changes: 20 additions & 10 deletions crates/re_viewer/src/ui/recordings_panel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ static TIME_FORMAT_DESCRIPTION: once_cell::sync::Lazy<
> = once_cell::sync::Lazy::new(|| format_description!(version = 2, "[hour]:[minute]:[second]Z"));

/// Show the currently open Recordings in a selectable list.
pub fn recordings_panel_ui(ctx: &mut ViewerContext<'_>, ui: &mut egui::Ui) {
///
/// Returns `true` if any recordings were shown.
pub fn recordings_panel_ui(ctx: &mut ViewerContext<'_>, ui: &mut egui::Ui) -> bool {
ctx.re_ui.panel_content(ui, |re_ui, ui| {
re_ui.panel_title_bar_with_buttons(
ui,
Expand All @@ -25,12 +27,16 @@ pub fn recordings_panel_ui(ctx: &mut ViewerContext<'_>, ui: &mut egui::Ui) {
.max_height(300.)
.show(ui, |ui| {
ctx.re_ui
.panel_content(ui, |_re_ui, ui| recording_list_ui(ctx, ui));
});
.panel_content(ui, |_re_ui, ui| recording_list_ui(ctx, ui))
})
.inner
}

/// Draw the recording list.
///
/// Returns `true` if any recordings were shown.
#[allow(clippy::blocks_in_if_conditions)]
fn recording_list_ui(ctx: &mut ViewerContext<'_>, ui: &mut egui::Ui) {
fn recording_list_ui(ctx: &mut ViewerContext<'_>, ui: &mut egui::Ui) -> bool {
let ViewerContext {
store_context,
command_sender,
Expand All @@ -46,7 +52,7 @@ fn recording_list_ui(ctx: &mut ViewerContext<'_>, ui: &mut egui::Ui) {
}

if store_dbs_map.is_empty() {
return;
return false;
}

for store_dbs in store_dbs_map.values_mut() {
Expand All @@ -72,10 +78,11 @@ fn recording_list_ui(ctx: &mut ViewerContext<'_>, ui: &mut egui::Ui) {
.send_system(SystemCommand::SetRecordingId(store_db.store_id().clone()));
}
} else {
ctx.re_ui
.list_item(app_id)
.active(false)
.show_collapsing(ui, true, |_, ui| {
ctx.re_ui.list_item(app_id).active(false).show_collapsing(
ui,
ui.id().with(app_id),
true,
|_, ui| {
for store_db in store_dbs {
if recording_ui(
ctx.re_ui,
Expand All @@ -92,9 +99,12 @@ fn recording_list_ui(ctx: &mut ViewerContext<'_>, ui: &mut egui::Ui) {
));
}
}
});
},
);
}
}

true
}

/// Show the UI for a single recording.
Expand Down
Loading

0 comments on commit 86466ae

Please sign in to comment.