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

ListItem 2.0 (part 6): split full-span range management to a dedicated module #6211

Merged
merged 5 commits into from
May 6, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
61 changes: 30 additions & 31 deletions crates/re_data_ui/src/editors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -255,50 +255,49 @@ fn edit_marker_shape_ui(

let marker_text = edit_marker.to_string();

let item_width = 100.0;

egui::ComboBox::from_id_source("marker_shape")
.selected_text(marker_text) // TODO(emilk): Show marker shape in the selected text
.width(ui.available_width().at_most(100.0))
.width(
ui.available_width()
.at_most(item_width + ui.spacing().menu_margin.sum().x),
)
.height(320.0)
.show_ui(ui, |ui| {
// no spacing between list items
ui.spacing_mut().item_spacing.y = 0.0;

let item_width = 100.0;

// workaround to force `ui.max_rect()` to reflect the content size
ui.allocate_space(egui::vec2(item_width, 0.0));
ui.set_width(item_width);

let background_x_range = ui
.spacing()
.menu_margin
.expand_rect(ui.max_rect())
.x_range();

re_ui::list_item2::list_item_scope(
ui,
"marker_shape",
Some(background_x_range),
|ui| {
for marker in MarkerShape::ALL {
let response = ctx
.re_ui
.list_item2()
.selected(edit_marker == marker)
.show_flat(
ui,
re_ui::list_item2::LabelContent::new(marker.to_string())
.min_desired_width(item_width)
.with_icon_fn(|_re_ui, ui, rect, visuals| {
paint_marker(ui, marker.into(), rect, visuals.text_color());
}),
);

if response.clicked() {
edit_marker = marker;
}
let list_ui = |ui: &mut egui::Ui| {
for marker in MarkerShape::ALL {
let response = ctx
.re_ui
.list_item2()
.selected(edit_marker == marker)
.show_flat(
ui,
re_ui::list_item2::LabelContent::new(marker.to_string())
.min_desired_width(item_width)
.with_icon_fn(|_re_ui, ui, rect, visuals| {
paint_marker(ui, marker.into(), rect, visuals.text_color());
}),
);

if response.clicked() {
edit_marker = marker;
}
},
);
}
};

re_ui::full_span::full_span_scope(ui, background_x_range, |ui| {
re_ui::list_item2::list_item_scope(ui, "marker_shape", list_ui);
});
});

if edit_marker != current_marker {
Expand Down
16 changes: 4 additions & 12 deletions crates/re_ui/examples/re_ui_example/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -312,19 +312,11 @@ impl eframe::App for ExampleApp {
.frame(panel_frame)
.min_width(0.0)
.show_animated(egui_ctx, self.show_right_panel, |ui| {
// TODO(#6156): this is still needed for some full-span widgets
ui.set_clip_rect(ui.max_rect());

// define the hover/selection background highlight range for all nested `ListItem`s
re_ui::list_item2::list_item_scope(
ui,
"right_panel_list_item_scope",
Some(ui.max_rect().x_range()),
|ui| {
ui.spacing_mut().item_spacing.y = 0.0;
self.right_panel.ui(&self.re_ui, ui);
},
);
re_ui::full_span::full_span_scope(ui, ui.max_rect().x_range(), |ui| {
ui.spacing_mut().item_spacing.y = 0.0;
self.right_panel.ui(&self.re_ui, ui);
});
});

egui::CentralPanel::default()
Expand Down
62 changes: 36 additions & 26 deletions crates/re_ui/examples/re_ui_example/right_panel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,13 @@ impl RightPanel {
ui.label("Hierarchical:");
});

if self.show_hierarchical_demo {
self.hierarchical_drag_and_drop.ui(re_ui, ui);
} else {
self.drag_and_drop.ui(re_ui, ui);
}
list_item2::list_item_scope(ui, "drag_and_drop", |ui| {
if self.show_hierarchical_demo {
self.hierarchical_drag_and_drop.ui(re_ui, ui);
} else {
self.drag_and_drop.ui(re_ui, ui);
}
});
});

ui.add_space(20.0);
Expand All @@ -62,7 +64,10 @@ impl RightPanel {

re_ui.panel_content(ui, |re_ui, ui| {
re_ui.panel_title_bar(ui, "Demo: ListItem APIs", None);
self.list_item_api_demo(re_ui, ui);

list_item2::list_item_scope(ui, "list_item_api", |ui| {
self.list_item_api_demo(re_ui, ui);
});
});

ui.add_space(20.0);
Expand All @@ -81,25 +86,30 @@ impl RightPanel {
.auto_shrink([false, true])
.show(ui, |ui| {
re_ui.panel_content(ui, |re_ui, ui| {
for i in 0..10 {
let label = if i == 4 {
"That's one heck of a loooooooong label!".to_owned()
} else {
format!("Some item {i}")
};

// Note: we use `exact_width(true)` here to force the item to allocate
// as much as needed for the label, which in turn will trigger the
// scroll area.
if re_ui
.list_item2()
.selected(Some(i) == self.selected_list_item)
.show_flat(ui, list_item2::LabelContent::new(&label).exact_width(true))
.clicked()
{
self.selected_list_item = Some(i);
list_item2::list_item_scope(ui, "scroll_area_demo", |ui| {
for i in 0..10 {
let label = if i == 4 {
"That's one heck of a loooooooong label!".to_owned()
} else {
format!("Some item {i}")
};

// Note: we use `exact_width(true)` here to force the item to allocate
// as much as needed for the label, which in turn will trigger the
// scroll area.
if re_ui
.list_item2()
.selected(Some(i) == self.selected_list_item)
.show_flat(
ui,
list_item2::LabelContent::new(&label).exact_width(true),
)
.clicked()
{
self.selected_list_item = Some(i);
}
}
}
});
});
});
}
Expand Down Expand Up @@ -200,7 +210,7 @@ impl RightPanel {
|re_ui, ui| {
// By using an inner scope, we allow the nested properties to not align themselves
// to the parent property, which in this particular case looks better.
list_item2::list_item_scope(ui, "inner_scope", None, |ui| {
list_item2::list_item_scope(ui, "inner_scope", |ui| {
re_ui.list_item2().show_hierarchical(
ui,
list_item2::PropertyContent::new("Bool").value_bool(self.boolean),
Expand Down Expand Up @@ -255,7 +265,7 @@ impl RightPanel {
|re_ui, ui| {
// By using an inner scope, we allow the nested properties to not align themselves
// to the parent property, which in this particular case looks better.
list_item2::list_item_scope(ui, "inner_scope", None, |ui| {
list_item2::list_item_scope(ui, "inner_scope", |ui| {
fn demo_item(re_ui: &ReUi, ui: &mut egui::Ui) {
re_ui.list_item2().show_hierarchical(
ui,
Expand Down
60 changes: 60 additions & 0 deletions crates/re_ui/src/full_span.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
//! Support for full-span widgets.
//!
//! Full-span widgets are widgets which draw beyond the boundaries of `ui.max_rect()`, e.g. to
//! provide highlighting without margin, like [`crate::list_item2`]. Typically, in the context of a
//! side panel, the full span is the entire width of the panel, excluding any margin.
//!
//! This module maintains a stack of full span values (effectively[`egui::Rangef`]) using nestable
//! scopes (via [`full_span_scope`]) and makes them available to widgets (via [`get_full_span`]).

#[derive(Clone, Default)]
struct FullSpanStack(Vec<egui::Rangef>);

/// Set up a full-span scope.
///
/// Note:
/// - Uses [`egui::Ui::scope`] internally, so it's safe to modify `ui` in the closure.
/// - Can be nested since the full-span range is stored in a stack.
pub fn full_span_scope<R>(
ui: &mut egui::Ui,
background_x_range: egui::Rangef,
content: impl FnOnce(&mut egui::Ui) -> R,
) -> R {
// push
ui.ctx().data_mut(|writer| {
let stack: &mut FullSpanStack = writer.get_temp_mut_or_default(egui::Id::NULL);
stack.0.push(background_x_range);
});

//
let result = ui.scope(content).inner;

// pop
ui.ctx().data_mut(|writer| {
let stack: &mut FullSpanStack = writer.get_temp_mut_or_default(egui::Id::NULL);
stack.0.pop();
});

result
}

/// Retrieve the current full-span scope.
///
/// If called outside a [`full_span_scope`], this function emits a warning and returns the clip
/// rectangle width. In debug build, it panics.
pub fn get_full_span(ui: &egui::Ui) -> egui::Rangef {
let range = ui.ctx().data_mut(|writer| {
let stack: &mut FullSpanStack = writer.get_temp_mut_or_default(egui::Id::NULL);
stack.0.last().copied()
});

if range.is_none() {
re_log::warn_once!("Full span requested outside a `full_span_scope()`");
}
debug_assert!(
range.is_some(),
"Full span requested outside a `full_span_scope()`"
);

range.unwrap_or(ui.clip_rect().x_range())
}
1 change: 1 addition & 0 deletions crates/re_ui/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ mod syntax_highlighting;
mod toggle_switch;

pub mod drag_and_drop;
pub mod full_span;
pub mod icons;
pub mod list_item;
pub mod list_item2;
Expand Down
11 changes: 8 additions & 3 deletions crates/re_ui/src/list_item2/list_item.rs
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,8 @@ impl<'a> ListItem<'a> {
}

/// Draw the item as part of a flat list.
///
/// *Important*: must be called while nested in a [`super::list_item_scope`].
pub fn show_flat(self, ui: &mut Ui, content: impl ListItemContent + 'a) -> Response {
// Note: the purpose of the scope is to minimise interferences on subsequent items' id
ui.scope(|ui| self.ui(ui, None, 0.0, Box::new(content)))
Expand All @@ -126,6 +128,8 @@ impl<'a> ListItem<'a> {
}

/// Draw the item as a leaf node from a hierarchical list.
///
/// *Important*: must be called while nested in a [`super::list_item_scope`].
pub fn show_hierarchical(self, ui: &mut Ui, content: impl ListItemContent + 'a) -> Response {
// Note: the purpose of the scope is to minimise interferences on subsequent items' id
ui.scope(|ui| {
Expand All @@ -141,6 +145,8 @@ impl<'a> ListItem<'a> {
}

/// Draw the item as a non-leaf node from a hierarchical list.
///
/// *Important*: must be called while nested in a [`super::list_item_scope`].
pub fn show_hierarchical_with_children<R>(
mut self,
ui: &mut Ui,
Expand Down Expand Up @@ -239,9 +245,8 @@ impl<'a> ListItem<'a> {
// We use the state set by ListItemContainer to determine how far the background should
// extend.
let layout_info = LayoutInfoStack::top(ui.ctx());
let mut bg_rect = rect;
bg_rect.set_left(layout_info.background_x_range.min);
bg_rect.set_right(layout_info.background_x_range.max);
let bg_rect =
egui::Rect::from_x_y_ranges(crate::full_span::get_full_span(ui), rect.y_range());

// Record the max allocated width.
layout_info.register_max_item_width(ui.ctx(), rect.right() - layout_info.left_x);
Expand Down
2 changes: 1 addition & 1 deletion crates/re_ui/src/list_item2/property_content.rs
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ impl ListItemContent for PropertyContent<'_> {
} = *self;

// │ │
// │◀─────────────────────layout_info.background_x_range─────────────────────────▶│
// │◀─────────────────────────────get_full_span()────────────────────────────────▶│
// │ │
// │ ◀────────layout_info.left_column_width─────────▶│┌──COLUMN_SPACING │
// │ ▼ │
Expand Down
Loading
Loading