Skip to content

Commit

Permalink
Use ListItem in Stream Tree UI (#3153)
Browse files Browse the repository at this point in the history
### What

Use `ListItem` in the Stream Tree UI.

- Consistent UI in the stream tree (vs. stuff in the left panel)
- Revamped look of the selection/hovered state in the timeline itself.
- Cleaned up the code for the "Entity/instance hover cards". Now
hovering instances in the blueprint tree _also_ displays storage use
from #2997
- Improved `ListItem`
- New attribute to control the width allocation mode (currently: max
width or fit to label)
- Improved click/hover sensing to cover the full span area (= hover
highlight), not just the actually allocated space (icon + label). That
improves the behaviour of the left panel trees too.

Fixes #3045
Fixes #2738 (if we accept that dropdown button are "close enough" and
will be improved with #2734 anyways)
Fixes #2860

<img width="1817" alt="image"
src="https://github.com/rerun-io/rerun/assets/49431240/5492ae03-0da0-4417-b7a1-2bedd4da4e8c">


### 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/3153) (if
applicable)

- [PR Build Summary](https://build.rerun.io/pr/3153)
- [Docs
preview](https://rerun.io/preview/2c54fda5373adf90d1407dc161f65f2242dcd4a3/docs)
<!--DOCS-PREVIEW-->
- [Examples
preview](https://rerun.io/preview/2c54fda5373adf90d1407dc161f65f2242dcd4a3/examples)
<!--EXAMPLES-PREVIEW--><!--EXAMPLES-PREVIEW--><!--EXAMPLES-PREVIEW--><!--EXAMPLES-PREVIEW--><!--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/)
abey79 authored Sep 1, 2023
1 parent f5aa4a8 commit 6451f78
Showing 6 changed files with 314 additions and 143 deletions.
76 changes: 44 additions & 32 deletions crates/re_data_ui/src/item_ui.rs
Original file line number Diff line number Diff line change
@@ -2,6 +2,7 @@
//!
//! TODO(andreas): This is not a `data_ui`, can this go somewhere else, shouldn't be in `re_data_ui`.
use egui::Ui;
use re_data_store::InstancePath;
use re_log_types::{ComponentPath, EntityPath, TimeInt, Timeline};
use re_viewer_context::{
@@ -94,31 +95,11 @@ pub fn instance_path_button_to(
text: impl Into<egui::WidgetText>,
) -> egui::Response {
let item = Item::InstancePath(space_view_id, instance_path.clone());
let subtype_string = if instance_path.instance_key.is_splat() {
"Entity"
} else {
"Entity Instance"
};

let response = ui
.selectable_label(ctx.selection().contains(&item), text)
.on_hover_ui(|ui| {
ui.strong(subtype_string);
ui.label(format!("Path: {instance_path}"));

// TODO(emilk): give data_ui an alternate "everything on this timeline" query?
// Then we can move the size view into `data_ui`.
let query = ctx.current_query();

if instance_path.instance_key.is_splat() {
let store = &ctx.store_db.entity_db.data_store;
let stats = store.entity_stats(query.timeline, instance_path.entity_path.hash());
entity_stats_ui(ui, &query.timeline, &stats);
} else {
// TODO(emilk): per-component stats
}

instance_path.data_ui(ctx, ui, UiVerbosity::Reduced, &query);
instance_hover_card_ui(ui, ctx, instance_path);
});

cursor_interact_with_selectable(ctx, response, item)
@@ -245,21 +226,11 @@ pub fn data_blueprint_button_to(
let response = ui
.selectable_label(ctx.selection().contains(&item), text)
.on_hover_ui(|ui| {
data_blueprint_tooltip(ui, ctx, entity_path);
entity_hover_card_ui(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,
@@ -344,3 +315,44 @@ pub fn select_hovered_on_click(
}
}
}

/// Displays the "hover card" (i.e. big tooltip) for an instance or an entity.
///
/// The entity hover card is displayed the provided instance path is a splat.
pub fn instance_hover_card_ui(
ui: &mut Ui,
ctx: &mut ViewerContext<'_>,
instance_path: &InstancePath,
) {
let subtype_string = if instance_path.instance_key.is_splat() {
"Entity"
} else {
"Entity Instance"
};
ui.strong(subtype_string);
ui.label(format!("Path: {instance_path}"));

// TODO(emilk): give data_ui an alternate "everything on this timeline" query?
// Then we can move the size view into `data_ui`.
let query = ctx.current_query();

if instance_path.instance_key.is_splat() {
let store = &ctx.store_db.entity_db.data_store;
let stats = store.entity_stats(query.timeline, instance_path.entity_path.hash());
entity_stats_ui(ui, &query.timeline, &stats);
} else {
// TODO(emilk): per-component stats
}

instance_path.data_ui(ctx, ui, UiVerbosity::Reduced, &query);
}

/// Displays the "hover card" (i.e. big tooltip) for an entity.
pub fn entity_hover_card_ui(
ui: &mut egui::Ui,
ctx: &mut ViewerContext<'_>,
entity_path: &EntityPath,
) {
let instance_path = InstancePath::entity_splat(entity_path.clone());
instance_hover_card_ui(ui, ctx, &instance_path);
}
3 changes: 2 additions & 1 deletion crates/re_time_panel/src/data_density_graph.rs
Original file line number Diff line number Diff line change
@@ -503,8 +503,9 @@ pub fn data_density_graph_ui(
fn graph_color(ctx: &mut ViewerContext<'_>, item: &Item, ui: &mut egui::Ui) -> Color32 {
let is_selected = ctx.selection().contains(item);
if is_selected {
make_brighter(ui.visuals().selection.bg_fill)
make_brighter(ui.visuals().widgets.active.fg_stroke.color)
} else {
//TODO(ab): tokenize that!
Color32::from_gray(225)
}
}
240 changes: 147 additions & 93 deletions crates/re_time_panel/src/lib.rs
Original file line number Diff line number Diff line change
@@ -11,12 +11,14 @@ mod time_ranges_ui;
mod time_selection_ui;

use egui::emath::Rangef;
use egui::{pos2, Color32, CursorIcon, NumExt, PointerButton, Rect, Shape, Vec2};
use egui::{pos2, Color32, CursorIcon, NumExt, Painter, PointerButton, Rect, Shape, Ui, Vec2};
use std::slice;

use re_data_store::{EntityTree, InstancePath, TimeHistogram};
use re_data_ui::item_ui;
use re_log_types::{ComponentPath, EntityPathPart, TimeInt, TimeRange, TimeReal};
use re_viewer_context::{Item, TimeControl, TimeView, ViewerContext};
use re_ui::list_item::{ListItem, WidthAllocationMode};
use re_viewer_context::{HoverHighlight, Item, TimeControl, TimeView, ViewerContext};

use time_axis::TimelineAxis;
use time_control_ui::TimeControlUi;
@@ -115,10 +117,10 @@ impl TimePanel {
// Expanded:
ui.vertical(|ui| {
// Add back the margin we removed from the panel:
let mut top_rop_frame = egui::Frame::default();
top_rop_frame.inner_margin.right = margin.x;
top_rop_frame.inner_margin.bottom = margin.y;
let rop_row_rect = top_rop_frame
let mut top_row_frame = egui::Frame::default();
top_row_frame.inner_margin.right = margin.x;
top_row_frame.inner_margin.bottom = margin.y;
let top_row_rect = top_row_frame
.show(ui, |ui| {
ui.horizontal(|ui| {
ui.spacing_mut().interact_size = Vec2::splat(top_bar_height);
@@ -131,17 +133,17 @@ impl TimePanel {

// Draw separator between top bar and the rest:
ui.painter().hline(
0.0..=rop_row_rect.right(),
rop_row_rect.bottom(),
0.0..=top_row_rect.right(),
top_row_rect.bottom(),
ui.visuals().widgets.noninteractive.bg_stroke,
);

ui.spacing_mut().scroll_bar_outer_margin = 4.0; // needed, because we have no panel margin on the right side.

// Add extra margin on the left which was intentionally missing on the controls.
let mut top_rop_frame = egui::Frame::default();
top_rop_frame.inner_margin.left = 8.0;
top_rop_frame.show(ui, |ui| {
let mut streams_frame = egui::Frame::default();
streams_frame.inner_margin.left = margin.x;
streams_frame.show(ui, |ui| {
self.expanded_ui(ctx, ui);
});
});
@@ -202,6 +204,8 @@ impl TimePanel {
// tree |streams |
// | . . . . . . |
// | . . . . |
// ▲
// └ tree_max_y (= time_x_left)

self.next_col_right = ui.min_rect().left(); // next_col_right will expand during the call

@@ -305,7 +309,13 @@ impl TimePanel {
));

// All the entity rows and their data density graphs:
self.tree_ui(ctx, &time_area_response, &lower_time_area_painter, ui);
self.tree_ui(
ctx,
&time_area_response,
&lower_time_area_painter,
time_x_left,
ui,
);

{
// Paint a shadow between the stream names on the left
@@ -351,6 +361,7 @@ impl TimePanel {
ctx: &mut ViewerContext<'_>,
time_area_response: &egui::Response,
time_area_painter: &egui::Painter,
tree_max_y: f32,
ui: &mut egui::Ui,
) {
re_tracing::profile_function!();
@@ -362,13 +373,16 @@ impl TimePanel {
// We implement drag-to-scroll manually instead!
.drag_to_scroll(false)
.show(ui, |ui| {
ui.spacing_mut().item_spacing.y = 0.0; // no spacing needed for ListItems

if time_area_response.dragged_by(PointerButton::Primary) {
ui.scroll_with_delta(Vec2::Y * time_area_response.drag_delta().y);
}
self.show_children(
ctx,
time_area_response,
time_area_painter,
tree_max_y,
&ctx.store_db.entity_db.tree,
ui,
);
@@ -381,6 +395,7 @@ impl TimePanel {
ctx: &mut ViewerContext<'_>,
time_area_response: &egui::Response,
time_area_painter: &egui::Painter,
tree_max_y: f32,
last_path_part: &EntityPathPart,
tree: &EntityTree,
ui: &mut egui::Ui,
@@ -402,21 +417,43 @@ impl TimePanel {

let collapsing_header_id = ui.make_persistent_id(&tree.path);
let default_open = tree.path.len() <= 1 && !tree.is_leaf();
let (_collapsing_button_response, custom_header_response, body_returned) =
egui::collapsing_header::CollapsingState::load_with_default_open(
ui.ctx(),
collapsing_header_id,
default_open,
)
.show_header(ui, |ui| {
item_ui::entity_path_button_to(ctx, ui, None, &tree.path, text)
})
.body(|ui| {
self.show_children(ctx, time_area_response, time_area_painter, tree, ui);

let item = Item::InstancePath(None, InstancePath::entity_splat(tree.path.clone()));
let is_selected = ctx.selection().contains(&item);
let is_item_hovered =
ctx.selection_state().highlight_for_ui_element(&item) == HoverHighlight::Hovered;

let clip_rect_save = ui.clip_rect();
let mut clip_rect = clip_rect_save;
clip_rect.max.x = tree_max_y;
ui.set_clip_rect(clip_rect);

let re_ui::list_item::ShowCollapsingResponse {
item_response: response,
body_response,
} = ListItem::new(ctx.re_ui, text)
.width_allocation_mode(WidthAllocationMode::Compact)
.selected(is_selected)
.force_hovered(is_item_hovered)
.show_collapsing(ui, collapsing_header_id, default_open, |_, ui| {
self.show_children(
ctx,
time_area_response,
time_area_painter,
tree_max_y,
tree,
ui,
);
});

let is_closed = body_returned.is_none();
let response = custom_header_response.response;
ui.set_clip_rect(clip_rect_save);

let response = response
.on_hover_ui(|ui| re_data_ui::item_ui::entity_hover_card_ui(ui, ctx, &tree.path));

item_ui::select_hovered_on_click(ctx, &response, slice::from_ref(&item));

let is_closed = body_response.is_none();
let response_rect = response.rect;
self.next_col_right = self.next_col_right.max(response_rect.right());

@@ -431,33 +468,33 @@ impl TimePanel {
// ----------------------------------------------

// show the data in the time area:

if is_visible && is_closed {
let item = Item::InstancePath(None, InstancePath::entity_splat(tree.path.clone()));

paint_streams_guide_line(ctx, &item, ui, response_rect);

let empty = re_data_store::TimeHistogram::default();
let num_messages_at_time = tree
.prefix_times
.get(ctx.rec_cfg.time_ctrl.timeline())
.unwrap_or(&empty);

if is_visible {
let row_rect =
Rect::from_x_y_ranges(time_area_response.rect.x_range(), response_rect.y_range());

data_density_graph::data_density_graph_ui(
&mut self.data_density_graph_painter,
ctx,
time_area_response,
time_area_painter,
ui,
tree.num_timeless_messages(),
num_messages_at_time,
row_rect,
&self.time_ranges_ui,
&item,
);
highlight_timeline_row(ui, ctx, time_area_painter, &item, &row_rect);

// show the density graph only if that item is closed
if is_closed {
let empty = re_data_store::TimeHistogram::default();
let num_messages_at_time = tree
.prefix_times
.get(ctx.rec_cfg.time_ctrl.timeline())
.unwrap_or(&empty);

data_density_graph::data_density_graph_ui(
&mut self.data_density_graph_painter,
ctx,
time_area_response,
time_area_painter,
ui,
tree.num_timeless_messages(),
num_messages_at_time,
row_rect,
&self.time_ranges_ui,
&item,
);
}
}
}

@@ -466,6 +503,7 @@ impl TimePanel {
ctx: &mut ViewerContext<'_>,
time_area_response: &egui::Response,
time_area_painter: &egui::Painter,
tree_max_y: f32,
tree: &EntityTree,
ui: &mut egui::Ui,
) {
@@ -474,6 +512,7 @@ impl TimePanel {
ctx,
time_area_response,
time_area_painter,
tree_max_y,
last_component,
child,
ui,
@@ -482,7 +521,7 @@ impl TimePanel {

// If this is an entity:
if !tree.components.is_empty() {
let indent = ui.spacing().indent;
let clip_rect_save = ui.clip_rect();

for (component_name, data) in &tree.components {
if !data.times.has_timeline(ctx.rec_cfg.time_ctrl.timeline())
@@ -492,21 +531,33 @@ impl TimePanel {
}

let component_path = ComponentPath::new(tree.path.clone(), *component_name);

let response = ui
.horizontal(|ui| {
// Add some spacing to match CollapsingHeader:
ui.spacing_mut().item_spacing.x = 0.0;
let response =
ui.allocate_response(egui::vec2(indent, 0.0), egui::Sense::hover());
ui.painter().circle_filled(
response.rect.center(),
2.0,
ui.visuals().text_color(),
);
item_ui::component_path_button(ctx, ui, &component_path);
let component_name = component_path.component_name.short_name();
let item = Item::ComponentPath(component_path);

let mut clip_rect = clip_rect_save;
clip_rect.max.x = tree_max_y;
ui.set_clip_rect(clip_rect);

let response = ListItem::new(ctx.re_ui, component_name)
.selected(ctx.selection().contains(&item))
.width_allocation_mode(WidthAllocationMode::Compact)
.force_hovered(
ctx.selection_state().highlight_for_ui_element(&item)
== HoverHighlight::Hovered,
)
.with_icon_fn(|_, ui, rect, visual| {
ui.painter()
.circle_filled(rect.center(), 2.0, visual.text_color());
})
.response;
.show(ui);

ui.set_clip_rect(clip_rect_save);

re_data_ui::item_ui::select_hovered_on_click(
ctx,
&response,
slice::from_ref(&item),
);

let response_rect = response.rect;

@@ -534,14 +585,13 @@ impl TimePanel {
});

// show the data in the time area:
let item = Item::ComponentPath(component_path);
paint_streams_guide_line(ctx, &item, ui, response_rect);

let row_rect = Rect::from_x_y_ranges(
time_area_response.rect.x_range(),
response_rect.y_range(),
);

highlight_timeline_row(ui, ctx, time_area_painter, &item, &row_rect);

data_density_graph::data_density_graph_ui(
&mut self.data_density_graph_painter,
ctx,
@@ -610,6 +660,35 @@ impl TimePanel {
}
}

/// Draw the hovered/selected highlight background for a timeline row.
fn highlight_timeline_row(
ui: &mut Ui,
ctx: &ViewerContext<'_>,
painter: &Painter,
item: &Item,
row_rect: &Rect,
) {
let item_hovered =
ctx.selection_state().highlight_for_ui_element(item) == HoverHighlight::Hovered;
let item_selected = ctx.selection().contains(item);
let bg_color = if item_selected {
Some(ui.visuals().selection.bg_fill.gamma_multiply(0.4))
} else if item_hovered {
Some(
ui.visuals()
.widgets
.hovered
.weak_bg_fill
.gamma_multiply(0.3),
)
} else {
None
};
if let Some(bg_color) = bg_color {
painter.rect_filled(*row_rect, egui::Rounding::ZERO, bg_color);
}
}

fn collapsed_time_marker_and_time(ui: &mut egui::Ui, ctx: &mut ViewerContext<'_>) {
let space_needed_for_current_time = match ctx.rec_cfg.time_ctrl.timeline().typ() {
re_arrow_store::TimeType::Time => 220.0,
@@ -646,31 +725,6 @@ fn collapsed_time_marker_and_time(ui: &mut egui::Ui, ctx: &mut ViewerContext<'_>
current_time_ui(ctx, ui);
}

/// Painted behind the data density graph.
fn paint_streams_guide_line(
ctx: &mut ViewerContext<'_>,
item: &Item,
ui: &mut egui::Ui,
response_rect: Rect,
) {
let is_selected = ctx.selection().contains(item);
let is_hovered = ctx.hovered().contains(item);

let stroke_width = if is_hovered { 1.0 } else { 0.5 };

let line_color = if is_selected {
ui.visuals().selection.bg_fill
} else {
ui.visuals().widgets.noninteractive.bg_stroke.color
};

ui.painter().hline(
response_rect.right()..=ui.max_rect().right(),
response_rect.center().y,
(stroke_width, line_color),
);
}

fn help_button(ui: &mut egui::Ui) {
// TODO(andreas): Nicer help text like on space views.
re_ui::help_hover_button(ui).on_hover_text(
@@ -681,7 +735,7 @@ fn help_button(ui: &mut egui::Ui) {
Zoom: Ctrl/cmd + scroll, or drag up/down with secondary mouse button.\n\
Double-click to reset view.\n\
\n\
Press spacebar to play/pause.",
Press the space bar to play/pause.",
);
}

134 changes: 119 additions & 15 deletions crates/re_ui/src/list_item.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use crate::{Icon, ReUi};
use egui::epaint::text::TextWrapping;
use egui::{Align, Align2, Response, Shape, Ui};
use std::default::Default;

struct ListItemResponse {
response: Response,
@@ -16,22 +17,77 @@ pub struct ShowCollapsingResponse<R> {
pub body_response: Option<R>,
}

/// Specification of how the width of the [`ListItem`] must be allocated.
#[derive(Default, Clone, Copy, Debug)]
pub enum WidthAllocationMode {
/// Allocate the full available width.
///
/// This mode is useful for fixed-width container, but should be avoided for dynamically-sized
/// containers as they will immediately grow to their max width.
///
/// Examples of resulting layouts:
/// ```text
/// ◀──────available width────▶
///
/// ┌─────────────────────────┐
/// normal: │▼ □ label │
/// └─────────────────────────┘
/// ┌─────────────────────────┐
/// hovered: │▼ □ label ■ ■│
/// └─────────────────────────┘
/// ┌─────────────────────────┐
/// normal, long label: │▼ □ a very very long lab…│
/// └─────────────────────────┘
/// ┌─────────────────────────┐
/// hovered, long label: │▼ □ a very very long… ■ ■│
/// └─────────────────────────┘
/// ```
/// The allocated size is always the same, and the label is truncated depending on the available
/// space, which is further reduced whenever buttons are displayed.
#[default]
Available,

/// Allocate the width needed for the text and icon(s) (if any).
///
/// This mode doesn't account for buttons (if any). If buttons are enabled, the label will get
/// truncated when they are displayed.
///
/// Examples of resulting layouts:
/// ```text
/// ┌─────────┐
/// normal: │▼ □ label│
/// └─────────┘
/// ┌─────────┐
/// hovered: │▼ □ … ■ ■│
/// └─────────┘
/// ┌──────────────────────────┐
/// normal, long label: │▼ □ a very very long label│
/// └──────────────────────────┘
/// ┌──────────────────────────┐
/// hovered, long label: │▼ □ a very very long … ■ ■│
/// └──────────────────────────┘
/// ```
Compact,
}

/// Generic widget for use in lists.
///
/// Layout:
/// ```text
/// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
/// ┃┌──────┐ ┌──────┐ ┌──────┐┌──────┐┃
/// ┃│ __ │ │ │ │ ││ │┃
/// ┃│ \/ │ │ icon │ label │ btns ││ btns │┃
/// ┃│ │ │ │ │ ││ │┃
/// ┃└──────┘ └──────┘ └──────┘└──────┘┃
/// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
/// ┌───┬────────────────────────────────────────────────────────────┬───┐
/// │ │┌──────┐ ┌──────┐ ┌──────┐┌──────┐│ │
/// │ ││ __ │ │ │ │ ││ ││ │
/// │ ││ \/ │ │ icon │ label │ btns ││ btns ││ │
/// │ ││ │ │ │ │ ││ ││ │
/// │ │└──────┘ └──────┘ └──────┘└──────┘│ │
/// └───┴────────────────────────────────────────────────────────────┴───┘
/// ◀───────────── allocated width (used for layout) ───────────▶
/// ◀────────────── clip rectangle (used for highlighting) ─────────────▶
/// ```
///
/// Features:
/// - selectable
/// - full span highlighting
/// - full span highlighting based on clip rectangle
/// - optional icon
/// - optional on-hover buttons on the right
/// - optional collapsing behavior for trees
@@ -57,6 +113,7 @@ pub struct ListItem<'a> {
subdued: bool,
force_hovered: bool,
collapse_openness: Option<f32>,
width_allocation_mode: WidthAllocationMode,
icon_fn:
Option<Box<dyn FnOnce(&ReUi, &mut egui::Ui, egui::Rect, egui::style::WidgetVisuals) + 'a>>,
buttons_fn: Option<Box<dyn FnOnce(&ReUi, &mut egui::Ui) -> egui::Response + 'a>>,
@@ -73,6 +130,7 @@ impl<'a> ListItem<'a> {
subdued: false,
force_hovered: false,
collapse_openness: None,
width_allocation_mode: Default::default(),
icon_fn: None,
buttons_fn: None,
}
@@ -110,6 +168,12 @@ impl<'a> ListItem<'a> {
self
}

/// Set the width allocation mode.
pub fn width_allocation_mode(mut self, mode: WidthAllocationMode) -> Self {
self.width_allocation_mode = mode;
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| {
@@ -137,7 +201,11 @@ impl<'a> ListItem<'a> {

/// Provide a closure to display on-hover buttons on the right of the item.
///
/// Note that the a right to left layout is used, so the right-most button must be added first.
/// Notes:
/// - If buttons are used, the item will allocate the full available width of the parent. If the
/// enclosing UI adapts to the childrens width, it will unnecessarily grow. If buttons aren't
/// used, the item will only allocate the width needed for the text and icons if any.
/// - A right to left layout is used, so the right-most button must be added first.
pub fn with_buttons(
mut self,
buttons: impl FnOnce(&ReUi, &mut egui::Ui) -> egui::Response + 'a,
@@ -198,8 +266,47 @@ impl<'a> ListItem<'a> {
0.0
};

let desired_size = egui::vec2(ui.available_width(), ReUi::list_item_height());
let (rect, response) = ui.allocate_at_least(desired_size, egui::Sense::click());
/// Compute the "ideal" desired width of the item, accounting for text and icon(s) (but not
/// buttons).
fn icons_and_label_width(
ui: &mut egui::Ui,
item: &ListItem<'_>,
collapse_extra: f32,
icon_extra: f32,
) -> f32 {
let text_job = item.text.clone().into_text_job(
ui.style(),
egui::FontSelection::Default,
Align::LEFT,
);

let text_width = ui.fonts(|f| text_job.into_galley(f)).size().x;

// The `ceil()` is needed to avoid some rounding errors which leads to text being
// truncated even though we allocated enough space.
(collapse_extra + icon_extra + text_width).ceil()
}

let desired_width = match self.width_allocation_mode {
WidthAllocationMode::Available => ui.available_width(),
WidthAllocationMode::Compact => {
icons_and_label_width(ui, &self, collapse_extra, icon_extra)
}
};

let desired_size = egui::vec2(desired_width, ReUi::list_item_height());
let (rect, mut response) = ui.allocate_at_least(desired_size, egui::Sense::click());

// compute the full-span background rect
let mut bg_rect = rect;
bg_rect.extend_with_x(ui.clip_rect().right());
bg_rect.extend_with_x(ui.clip_rect().left());

// we want to be able to select/hover the item across its full span, so we sense that and
// update the response accordingly.
let full_span_response = ui.interact(bg_rect, response.id, egui::Sense::click());
response.clicked = full_span_response.clicked;
response.hovered = full_span_response.hovered;

// override_hover should not affect the returned response
let mut style_response = response.clone();
@@ -209,7 +316,7 @@ impl<'a> ListItem<'a> {

let mut collapse_response = None;

if ui.is_rect_visible(rect) {
if ui.is_rect_visible(bg_rect) {
let mut visuals = if self.active {
ui.style()
.interact_selectable(&style_response, self.selected)
@@ -222,9 +329,6 @@ impl<'a> ListItem<'a> {
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());
let background_frame = ui.painter().add(egui::Shape::Noop);

// Draw collapsing triangle
2 changes: 1 addition & 1 deletion crates/re_viewer/src/ui/recordings_panel.rs
Original file line number Diff line number Diff line change
@@ -80,7 +80,7 @@ fn recording_list_ui(ctx: &mut ViewerContext<'_>, ui: &mut egui::Ui) -> bool {
} else {
ctx.re_ui.list_item(app_id).active(false).show_collapsing(
ui,
ui.id().with(app_id),
ui.make_persistent_id(app_id),
true,
|_, ui| {
for store_db in store_dbs {
2 changes: 1 addition & 1 deletion crates/re_viewport/src/viewport_blueprint_ui.rs
Original file line number Diff line number Diff line change
@@ -264,7 +264,7 @@ impl ViewportBlueprint<'_> {
})
.show(ui)
.on_hover_ui(|ui| {
re_data_ui::item_ui::data_blueprint_tooltip(ui, ctx, entity_path);
re_data_ui::item_ui::entity_hover_card_ui(ui, ctx, entity_path);
});

item_ui::select_hovered_on_click(ctx, &response, &[item]);

0 comments on commit 6451f78

Please sign in to comment.