Skip to content

Commit 5f59f7b

Browse files
authored
Wrap filter bar (#11195)
1 parent b2f8439 commit 5f59f7b

File tree

4 files changed

+140
-28
lines changed

4 files changed

+140
-28
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7627,6 +7627,7 @@ dependencies = [
76277627
"static_assertions",
76287628
"thiserror 1.0.69",
76297629
"tokio",
7630+
"vec1",
76307631
]
76317632

76327633
[[package]]

crates/viewer/re_dataframe_ui/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ nohash-hasher.workspace = true
4646
parking_lot.workspace = true
4747
serde.workspace = true
4848
thiserror.workspace = true
49+
vec1.workspace = true
4950

5051

5152
[dev-dependencies]

crates/viewer/re_dataframe_ui/src/filters/filter_ui.rs

Lines changed: 135 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1+
use std::sync::Arc;
2+
13
use egui::{Frame, Margin};
24

35
use re_ui::{SyntaxHighlighting, UiExt as _, syntax_highlighting::SyntaxHighlightedBuilder};
46

7+
use super::{ComparisonOperator, Filter, FilterOperation};
58
use crate::TableBlueprint;
6-
use crate::filters::{ComparisonOperator, Filter, FilterOperation};
79

810
/// Action to take based on the user interaction.
911
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
@@ -112,27 +114,56 @@ impl FilterState {
112114
right: 16,
113115
})
114116
.show(ui, |ui| {
115-
ui.horizontal(|ui| {
116-
let active_index = self.active_filter.take();
117+
let active_index = self.active_filter.take();
118+
let mut remove_idx = None;
119+
120+
// TODO(#11194): ideally, egui would allow wrapping `Frame` widget itself. Remove
121+
// this when it does.
122+
let prepared_uis = self
123+
.filters
124+
.iter()
125+
.map(|filter| filter.prepare_ui(ui))
126+
.collect::<Vec<_>>();
127+
let item_spacing = ui.style().spacing.item_spacing.x;
128+
let available_width = ui.available_width();
129+
let mut rows = vec1::vec1![vec![]];
130+
let mut current_left_position = 0.0;
131+
for (index, prepared_ui) in prepared_uis.iter().enumerate() {
132+
if current_left_position > 0.0
133+
&& current_left_position + prepared_ui.desired_width() > available_width
134+
{
135+
rows.push(vec![]);
136+
current_left_position = 0.0;
137+
}
138+
139+
rows.last_mut().push(index);
140+
current_left_position += prepared_ui.desired_width() + item_spacing;
141+
}
142+
143+
for row in rows {
144+
ui.horizontal(|ui| {
145+
for index in row {
146+
let filter_id = ui.make_persistent_id(index);
147+
let filter = &mut self.filters[index];
148+
let prepared_ui = &prepared_uis[index];
117149

118-
let mut remove_idx = None;
119-
for (index, filter) in self.filters.iter_mut().enumerate() {
120-
let filter_id = ui.make_persistent_id(index);
121-
let result = filter.ui(ui, filter_id, Some(index) == active_index);
150+
let result =
151+
filter.ui(ui, prepared_ui, filter_id, Some(index) == active_index);
122152

123-
action = action.merge(result.filter_action);
153+
action = action.merge(result.filter_action);
124154

125-
if result.should_delete_filter {
126-
remove_idx = Some(index);
155+
if result.should_delete_filter {
156+
remove_idx = Some(index);
157+
}
127158
}
128-
}
159+
});
160+
}
129161

130-
if let Some(remove_idx) = remove_idx {
131-
self.active_filter = None;
132-
self.filters.remove(remove_idx);
133-
should_commit = true;
134-
}
135-
});
162+
if let Some(remove_idx) = remove_idx {
163+
self.active_filter = None;
164+
self.filters.remove(remove_idx);
165+
should_commit = true;
166+
}
136167
});
137168

138169
action
@@ -145,31 +176,64 @@ struct DisplayFilterUiResult {
145176
should_delete_filter: bool,
146177
}
147178

179+
// TODO(#11194): used by the manual wrapping code. Remove when no longer needed.
180+
struct FilterPreparedUi {
181+
frame: Frame,
182+
galley: Arc<egui::Galley>,
183+
desired_width: f32,
184+
}
185+
186+
impl FilterPreparedUi {
187+
fn desired_width(&self) -> f32 {
188+
self.desired_width
189+
}
190+
}
191+
148192
impl Filter {
193+
/// Prepare the UI for this filter
194+
fn prepare_ui(&self, ui: &egui::Ui) -> FilterPreparedUi {
195+
let layout_job = SyntaxHighlightedBuilder::new()
196+
.with_body(&self.column_name)
197+
.with_keyword(" ")
198+
.with(&self.operation)
199+
.into_job(ui.style());
200+
201+
let galley = ui.fonts(|f| f.layout_job(layout_job));
202+
203+
let frame = Frame::new()
204+
.inner_margin(Margin::symmetric(4, 4))
205+
.stroke(ui.tokens().table_filter_frame_stroke)
206+
.corner_radius(2.0);
207+
208+
let desired_width = galley.size().x
209+
+ ui.style().spacing.item_spacing.x
210+
+ ui.tokens().small_icon_size.x
211+
+ frame.total_margin().sum().x;
212+
213+
FilterPreparedUi {
214+
frame,
215+
galley,
216+
desired_width,
217+
}
218+
}
219+
149220
/// UI for a single filter.
150221
#[must_use]
151222
fn ui(
152223
&mut self,
153224
ui: &mut egui::Ui,
225+
prepared_ui: &FilterPreparedUi,
154226
filter_id: egui::Id,
155227
activate_filter: bool,
156228
) -> DisplayFilterUiResult {
157229
let mut should_delete_filter = false;
158230
let mut action_due_to_filter_deletion = FilterUiAction::None;
159231

160-
let response = Frame::new()
161-
.inner_margin(Margin::symmetric(4, 4))
162-
.stroke(ui.tokens().table_filter_frame_stroke)
163-
.corner_radius(2.0)
232+
let response = prepared_ui
233+
.frame
164234
.show(ui, |ui| {
165-
let widget_text = SyntaxHighlightedBuilder::new()
166-
.with_body(&self.column_name)
167-
.with_keyword(" ")
168-
.with(&self.operation)
169-
.into_widget_text(ui.style());
170-
171235
let text_response = ui.add(
172-
egui::Label::new(widget_text)
236+
egui::Label::new(Arc::clone(&prepared_ui.galley))
173237
.selectable(false)
174238
.sense(egui::Sense::click()),
175239
);
@@ -473,4 +537,47 @@ mod tests {
473537
harness.snapshot(format!("popup_ui_{test_name}"));
474538
}
475539
}
540+
541+
#[test]
542+
fn test_filter_wrapping() {
543+
let filters = vec![
544+
Filter::new(
545+
"some:column:name",
546+
FilterOperation::StringContains("some query string".to_owned()),
547+
),
548+
Filter::new(
549+
"other:column:name",
550+
FilterOperation::StringContains("hello".to_owned()),
551+
),
552+
Filter::new(
553+
"short:name",
554+
FilterOperation::StringContains("world".to_owned()),
555+
),
556+
Filter::new(
557+
"looooog:name",
558+
FilterOperation::StringContains("some more querying text here".to_owned()),
559+
),
560+
Filter::new(
561+
"world",
562+
FilterOperation::StringContains(":wave:".to_owned()),
563+
),
564+
];
565+
566+
let mut filters = FilterState {
567+
filters,
568+
active_filter: None,
569+
};
570+
571+
let mut harness = egui_kittest::Harness::builder()
572+
.with_size(egui::Vec2::new(700.0, 500.0))
573+
.build_ui(|ui| {
574+
re_ui::apply_style_and_install_loaders(ui.ctx());
575+
576+
filters.filter_bar_ui(ui, &mut TableBlueprint::default());
577+
});
578+
579+
harness.run();
580+
581+
harness.snapshot("filter_wrapping");
582+
}
476583
}
Lines changed: 3 additions & 0 deletions
Loading

0 commit comments

Comments
 (0)