Skip to content

Commit

Permalink
Move entity filter "edit" button to a section header icon (#6662)
Browse files Browse the repository at this point in the history
### What

* Builds on #6657
* Part of #6655 

This PR is mainly a refactor that abstract the buttons used by
`PropertyContent` and `SectionCollapsingHeader`. (This refactor is
partial and should also be applied to `LabelContent` -> #6191).

This enables action button in section header, used to remove the "edit"
button of the entity filter:
<img width="356" alt="image"
src="https://github.com/rerun-io/rerun/assets/49431240/f430805e-2b93-4134-bdbc-fcbbdbbc68e9">


### 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 examples from latest `main` build:
[rerun.io/viewer](https://rerun.io/viewer/pr/6662?manifest_url=https://app.rerun.io/version/main/examples_manifest.json)
* Using full set of examples from `nightly` build:
[rerun.io/viewer](https://rerun.io/viewer/pr/6662?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
* [x] If applicable, add a new check to the [release
checklist](https://github.com/rerun-io/rerun/blob/main/tests/python/release_checklist)!

- [PR Build Summary](https://build.rerun.io/pr/6662)
- [Recent benchmark results](https://build.rerun.io/graphs/crates.html)
- [Wasm size tracking](https://build.rerun.io/graphs/sizes.html)

To run all checks from `main`, comment on the PR with `@rerun-bot
full-check`.
  • Loading branch information
abey79 authored Jun 27, 2024
1 parent e325f79 commit 3178dd5
Show file tree
Hide file tree
Showing 12 changed files with 295 additions and 252 deletions.
2 changes: 1 addition & 1 deletion crates/re_selection_panel/src/defaults_ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ pub fn view_components_defaults_section_ui(
let reason_we_cannot_add_more = components_to_show_in_add_menu.as_ref().err().cloned();

let mut add_button_is_open = false;
let mut add_button = re_ui::HeaderMenuButton::new(&re_ui::icons::ADD, |ui| {
let mut add_button = re_ui::list_item::ItemMenuButton::new(&re_ui::icons::ADD, |ui| {
add_button_is_open = true;
ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend);
add_popup_ui(
Expand Down
203 changes: 98 additions & 105 deletions crates/re_selection_panel/src/selection_panel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -364,12 +364,18 @@ The last rule matching `/world/house` is `+ /world/**`, so it is included.

if let Some(view) = blueprint.view(view_id) {
ui.section_collapsing_header("Entity path filter")
.button(
list_item::ItemActionButton::new(&re_ui::icons::EDIT, || {
self.space_view_entity_modal.open(*view_id);
})
.hover_text("Modify the entity query using the editor"),
)
.help_ui(entity_path_filter_help_ui)
.show(ui, |ui| {
// TODO(#6075): Because `list_item_scope` changes it. Temporary until everything is `ListItem`.
ui.spacing_mut().item_spacing.y = ui.ctx().style().spacing.item_spacing.y;

if let Some(new_entity_path_filter) = self.entity_path_filter_ui(
if let Some(new_entity_path_filter) = entity_path_filter_ui(
ctx,
ui,
*view_id,
Expand Down Expand Up @@ -421,110 +427,6 @@ The last rule matching `/world/house` is `+ /world/**`, so it is included.
visible_time_range_ui_for_view(ctx, ui, view);
}
}

/// Returns a new filter when the editing is done, and there has been a change.
fn entity_path_filter_ui(
&mut self,
ctx: &ViewerContext<'_>,
ui: &mut egui::Ui,
view_id: SpaceViewId,
filter: &EntityPathFilter,
origin: &EntityPath,
) -> Option<EntityPathFilter> {
fn syntax_highlight_entity_path_filter(
style: &egui::Style,
mut string: &str,
) -> egui::text::LayoutJob {
let font_id = egui::TextStyle::Body.resolve(style);

let mut job = egui::text::LayoutJob::default();

while !string.is_empty() {
let newline = string.find('\n').unwrap_or(string.len() - 1);
let line = &string[..=newline];
string = &string[newline + 1..];
let is_exclusion = line.trim_start().starts_with('-');

let color = if is_exclusion {
egui::Color32::LIGHT_RED
} else {
egui::Color32::LIGHT_GREEN
};

let text_format = egui::TextFormat {
font_id: font_id.clone(),
color,
..Default::default()
};

job.append(line, 0.0, text_format);
}

job
}

fn text_layouter(
ui: &egui::Ui,
string: &str,
wrap_width: f32,
) -> std::sync::Arc<egui::Galley> {
let mut layout_job = syntax_highlight_entity_path_filter(ui.style(), string);
layout_job.wrap.max_width = wrap_width;
ui.fonts(|f| f.layout_job(layout_job))
}

// We store the string we are temporarily editing in the `Ui`'s temporary data storage.
// This is so it can contain invalid rules while the user edits it, and it's only normalized
// when they press enter, or stops editing.
let filter_text_id = ui.id().with("filter_text");

let mut filter_string = ui.data_mut(|data| {
data.get_temp_mut_or_insert_with::<String>(filter_text_id, || filter.formatted())
.clone()
});

let response =
ui.add(egui::TextEdit::multiline(&mut filter_string).layouter(&mut text_layouter));

if response.has_focus() {
ui.data_mut(|data| data.insert_temp::<String>(filter_text_id, filter_string.clone()));
} else {
// Reconstruct it from the filter next frame
ui.data_mut(|data| data.remove::<String>(filter_text_id));
}

// Show some statistics about the query, print a warning text if something seems off.
let query = ctx.lookup_query_result(view_id);
if query.num_matching_entities == 0 {
ui.label(ui.ctx().warning_text("Does not match any entity"));
} else if query.num_matching_entities == 1 {
ui.label("Matches 1 entity");
} else {
ui.label(format!("Matches {} entities", query.num_matching_entities));
}
if query.num_matching_entities != 0 && query.num_visualized_entities == 0 {
// TODO(andreas): Talk about this root bit only if it's a spatial view.
ui.label(ui.ctx().warning_text(
format!("This view is not able to visualize any of the matched entities using the current root \"{origin:?}\"."),
));
}

if ui
.button("Edit")
.on_hover_text("Modify the entity query using the editor")
.clicked()
{
self.space_view_entity_modal.open(view_id);
}

// Apply the edit.
let new_filter = EntityPathFilter::parse_forgiving(&filter_string, &Default::default());
if &new_filter == filter {
None // no change
} else {
Some(new_filter)
}
}
}

fn entity_selection_ui(
Expand Down Expand Up @@ -571,6 +473,97 @@ fn clone_space_view_button_ui(
}
}

/// Returns a new filter when the editing is done, and there has been a change.
fn entity_path_filter_ui(
ctx: &ViewerContext<'_>,
ui: &mut egui::Ui,
view_id: SpaceViewId,
filter: &EntityPathFilter,
origin: &EntityPath,
) -> Option<EntityPathFilter> {
fn syntax_highlight_entity_path_filter(
style: &egui::Style,
mut string: &str,
) -> egui::text::LayoutJob {
let font_id = egui::TextStyle::Body.resolve(style);

let mut job = egui::text::LayoutJob::default();

while !string.is_empty() {
let newline = string.find('\n').unwrap_or(string.len() - 1);
let line = &string[..=newline];
string = &string[newline + 1..];
let is_exclusion = line.trim_start().starts_with('-');

let color = if is_exclusion {
egui::Color32::LIGHT_RED
} else {
egui::Color32::LIGHT_GREEN
};

let text_format = egui::TextFormat {
font_id: font_id.clone(),
color,
..Default::default()
};

job.append(line, 0.0, text_format);
}

job
}

fn text_layouter(ui: &egui::Ui, string: &str, wrap_width: f32) -> std::sync::Arc<egui::Galley> {
let mut layout_job = syntax_highlight_entity_path_filter(ui.style(), string);
layout_job.wrap.max_width = wrap_width;
ui.fonts(|f| f.layout_job(layout_job))
}

// We store the string we are temporarily editing in the `Ui`'s temporary data storage.
// This is so it can contain invalid rules while the user edits it, and it's only normalized
// when they press enter, or stops editing.
let filter_text_id = ui.id().with("filter_text");

let mut filter_string = ui.data_mut(|data| {
data.get_temp_mut_or_insert_with::<String>(filter_text_id, || filter.formatted())
.clone()
});

let response =
ui.add(egui::TextEdit::multiline(&mut filter_string).layouter(&mut text_layouter));

if response.has_focus() {
ui.data_mut(|data| data.insert_temp::<String>(filter_text_id, filter_string.clone()));
} else {
// Reconstruct it from the filter next frame
ui.data_mut(|data| data.remove::<String>(filter_text_id));
}

// Show some statistics about the query, print a warning text if something seems off.
let query = ctx.lookup_query_result(view_id);
if query.num_matching_entities == 0 {
ui.label(ui.ctx().warning_text("Does not match any entity"));
} else if query.num_matching_entities == 1 {
ui.label("Matches 1 entity");
} else {
ui.label(format!("Matches {} entities", query.num_matching_entities));
}
if query.num_matching_entities != 0 && query.num_visualized_entities == 0 {
// TODO(andreas): Talk about this root bit only if it's a spatial view.
ui.label(ui.ctx().warning_text(
format!("This view is not able to visualize any of the matched entities using the current root \"{origin:?}\"."),
));
}

// Apply the edit.
let new_filter = EntityPathFilter::parse_forgiving(&filter_string, &Default::default());
if &new_filter == filter {
None // no change
} else {
Some(new_filter)
}
}

fn container_children(
ctx: &ViewerContext<'_>,
blueprint: &ViewportBlueprint,
Expand Down
2 changes: 1 addition & 1 deletion crates/re_selection_panel/src/visualizer_ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ pub fn visualizer_ui(
&active_visualizers,
);

let button = re_ui::HeaderMenuButton::new(&re_ui::icons::ADD, |ui| {
let button = list_item::ItemMenuButton::new(&re_ui::icons::ADD, |ui| {
menu_add_new_visualizer(
ctx,
ui,
Expand Down
Binary file added crates/re_ui/data/icons/edit.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion crates/re_ui/examples/re_ui_example/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,7 @@ impl eframe::App for ExampleApp {
// ---

ui.section_collapsing_header("Data")
.button(re_ui::HeaderMenuButton::new(&re_ui::icons::ADD, |ui| {
.button(list_item::ItemMenuButton::new(&re_ui::icons::ADD, |ui| {
ui.weak("empty");
}))
.show(ui, |ui| {
Expand Down
2 changes: 2 additions & 0 deletions crates/re_ui/src/icons.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ pub const ADD: Icon = Icon::new("add", include_bytes!("../data/icons/add.png"));
pub const REMOVE: Icon = Icon::new("remove", include_bytes!("../data/icons/remove.png"));

pub const RESET: Icon = Icon::new("reset", include_bytes!("../data/icons/reset.png"));

pub const EDIT: Icon = Icon::new("edit", include_bytes!("../data/icons/edit.png"));
pub const MORE: Icon = Icon::new("more", include_bytes!("../data/icons/more.png"));

pub const CLOSE: Icon = Icon::new("close", include_bytes!("../data/icons/close.png"));
Expand Down
2 changes: 1 addition & 1 deletion crates/re_ui/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ pub use self::{
design_tokens::DesignTokens,
icons::Icon,
layout_job_builder::LayoutJobBuilder,
section_collapsing_header::{HeaderMenuButton, SectionCollapsingHeader},
section_collapsing_header::SectionCollapsingHeader,
syntax_highlighting::SyntaxHighlighting,
ui_ext::UiExt,
};
Expand Down
Loading

0 comments on commit 3178dd5

Please sign in to comment.