Skip to content

Commit af13591

Browse files
committed
store flag in egui memory to track whether tree needs to reset expanded
set this in JsonTreeRespose::reset_expanded or when default expand changes reset expanded lazily, without need for HashSet<Id>
1 parent e56aa39 commit af13591

File tree

4 files changed

+46
-89
lines changed

4 files changed

+46
-89
lines changed

src/node.rs

Lines changed: 41 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
use std::collections::HashSet;
2-
31
use egui::{
42
Id, Ui,
53
collapsing_header::{CollapsingState, paint_default_icon},
@@ -20,6 +18,8 @@ use crate::{
2018
};
2119

2220
pub(crate) struct JsonTreeNode<'a, 'b, T: ToJsonTreeValue> {
21+
/// The Id of the entire tree that this node is part of.
22+
tree_id: Id,
2323
value: &'a T,
2424
parent: Option<JsonPointerSegment<'a>>,
2525
make_persistent_id: &'b dyn Fn(&[JsonPointerSegment]) -> Id,
@@ -36,36 +36,29 @@ impl<'a, 'b, T: ToJsonTreeValue> JsonTreeNode<'a, 'b, T> {
3636
let style = tree.config.style.unwrap_or_default();
3737
let default_expand = tree.config.default_expand.unwrap_or_default();
3838

39-
let mut reset_path_ids = HashSet::new();
40-
4139
let (inner_default_expand, search_term) = match default_expand {
4240
DefaultExpand::All => (InnerDefaultExpand::All, None),
4341
DefaultExpand::None => (InnerDefaultExpand::None, None),
4442
DefaultExpand::ToLevel(l) => (InnerDefaultExpand::ToLevel(l), None),
43+
DefaultExpand::SearchResults("") => (InnerDefaultExpand::None, None),
44+
DefaultExpand::SearchResultsOrAll("") => (InnerDefaultExpand::All, None),
4545
DefaultExpand::SearchResults(search_str)
4646
| DefaultExpand::SearchResultsOrAll(search_str) => {
47-
// Important: when searching for anything (even an empty string), we must always traverse the entire JSON value to
48-
// capture all reset_path_ids so all the open/closed states can be reset to respect the new search term when it changes.
4947
let search_term = SearchTerm::new(search_str);
5048
let search_match_path_ids = search_term.find_matching_paths_in(
5149
tree.value,
5250
style.abbreviate_root,
5351
&make_persistent_id,
54-
&mut reset_path_ids,
5552
);
56-
let inner_expand =
57-
if matches!(default_expand, DefaultExpand::SearchResultsOrAll("")) {
58-
InnerDefaultExpand::All
59-
} else {
60-
InnerDefaultExpand::Paths(search_match_path_ids)
61-
};
62-
(inner_expand, Some(search_term))
53+
(
54+
InnerDefaultExpand::Paths(search_match_path_ids),
55+
Some(search_term),
56+
)
6357
}
6458
};
6559

66-
let mut renderer = tree.config.renderer;
67-
6860
let node = JsonTreeNode {
61+
tree_id,
6962
value: tree.value,
7063
parent: None,
7164
make_persistent_id: &make_persistent_id,
@@ -76,47 +69,39 @@ impl<'a, 'b, T: ToJsonTreeValue> JsonTreeNode<'a, 'b, T> {
7669
},
7770
};
7871

79-
// Wrap in a vertical layout in case this tree is placed directly in a horizontal layout,
80-
// which does not allow indent layouts as direct children.
81-
ui.vertical(|ui| {
82-
// Centres the collapsing header icon.
83-
ui.spacing_mut().interact_size.y = node.config.style.resolve_font_id(ui).size;
84-
85-
node.show_impl(ui, &mut vec![], &mut reset_path_ids, &mut renderer);
86-
});
87-
88-
let should_reset_expanded = if tree.config.auto_reset_expanded {
89-
let default_expand_hash_id = ResetExpandedHashId(egui::util::hash(default_expand));
90-
let default_expand_changed = ui.ctx().data_mut(|d| {
72+
let should_reset_expanded = ui.ctx().data_mut(|d| {
73+
if tree.config.auto_reset_expanded {
74+
let default_expand_hash_id = ResetExpandedHashId(egui::util::hash(default_expand));
9175
let default_expand_changed =
9276
d.get_temp::<ResetExpandedHashId>(tree.id) != Some(default_expand_hash_id);
9377
if default_expand_changed {
9478
d.insert_temp(tree.id, default_expand_hash_id);
79+
d.insert_temp(tree.id, ShouldResetExpanded);
9580
}
96-
default_expand_changed
97-
});
98-
default_expand_changed
99-
} else {
100-
false
101-
};
81+
}
82+
d.remove_temp::<ShouldResetExpanded>(tree_id).is_some()
83+
});
10284

103-
let response = JsonTreeResponse {
104-
collapsing_state_ids: reset_path_ids,
105-
};
85+
let mut renderer = tree.config.renderer;
10686

107-
if should_reset_expanded {
108-
response.reset_expanded(ui);
109-
}
87+
// Wrap in a vertical layout in case this tree is placed directly in a horizontal layout,
88+
// which does not allow indent layouts as direct children.
89+
ui.vertical(|ui| {
90+
// Centres the collapsing header icon.
91+
ui.spacing_mut().interact_size.y = node.config.style.resolve_font_id(ui).size;
92+
93+
node.show_impl(ui, &mut vec![], &mut renderer, should_reset_expanded);
94+
});
11095

111-
response
96+
JsonTreeResponse { tree_id }
11297
}
11398

11499
fn show_impl(
115100
self,
116101
ui: &mut Ui,
117102
path_segments: &'b mut Vec<JsonPointerSegment<'a>>,
118-
reset_path_ids: &'b mut HashSet<Id>,
119103
renderer: &'b mut JsonTreeRenderer<'a, T>,
104+
should_reset_expanded: bool,
120105
) {
121106
match self.value.to_json_tree_value() {
122107
JsonTreeValue::Base(value, display_value, value_type) => {
@@ -168,10 +153,10 @@ impl<'a, 'b, T: ToJsonTreeValue> JsonTreeNode<'a, 'b, T> {
168153
self.show_expandable(
169154
ui,
170155
path_segments,
171-
reset_path_ids,
172156
renderer,
173157
entries,
174158
expandable_type,
159+
should_reset_expanded,
175160
);
176161
}
177162
};
@@ -181,10 +166,10 @@ impl<'a, 'b, T: ToJsonTreeValue> JsonTreeNode<'a, 'b, T> {
181166
self,
182167
ui: &mut Ui,
183168
path_segments: &'b mut Vec<JsonPointerSegment<'a>>,
184-
reset_path_ids: &'b mut HashSet<Id>,
185169
renderer: &'b mut JsonTreeRenderer<'a, T>,
186170
entries: Vec<(JsonPointerSegment<'a>, &'a T)>,
187171
expandable_type: ExpandableType,
172+
should_reset_expanded: bool,
188173
) {
189174
let JsonTreeNodeConfig {
190175
inner_default_expand,
@@ -198,7 +183,6 @@ impl<'a, 'b, T: ToJsonTreeValue> JsonTreeNode<'a, 'b, T> {
198183
};
199184

200185
let path_id = (self.make_persistent_id)(path_segments);
201-
reset_path_ids.insert(path_id);
202186

203187
let default_open = match &inner_default_expand {
204188
InnerDefaultExpand::All => true,
@@ -212,6 +196,9 @@ impl<'a, 'b, T: ToJsonTreeValue> JsonTreeNode<'a, 'b, T> {
212196
};
213197

214198
let mut state = CollapsingState::load_with_default_open(ui.ctx(), path_id, default_open);
199+
if should_reset_expanded {
200+
state.set_open(default_open);
201+
}
215202
let is_expanded = state.is_open();
216203

217204
let header_res = ui.horizontal_wrapped(|ui| {
@@ -417,13 +404,14 @@ impl<'a, 'b, T: ToJsonTreeValue> JsonTreeNode<'a, 'b, T> {
417404

418405
let mut add_nested_tree = |ui: &mut Ui| {
419406
let nested_tree = JsonTreeNode {
407+
tree_id: self.tree_id,
420408
value: elem,
421409
parent: Some(property),
422410
make_persistent_id: self.make_persistent_id,
423411
config: self.config,
424412
};
425413

426-
nested_tree.show_impl(ui, path_segments, reset_path_ids, renderer);
414+
nested_tree.show_impl(ui, path_segments, renderer, should_reset_expanded);
427415
};
428416

429417
if is_expandable && !toggle_buttons_hidden {
@@ -463,14 +451,9 @@ impl<'a, 'b, T: ToJsonTreeValue> JsonTreeNode<'a, 'b, T> {
463451
},
464452
);
465453
});
466-
467-
if renderer.render_hook.is_some() {
468-
// show_body_indented will store the CollapsingState,
469-
// but since the subsequent render call above could also mutate the state in the render hook,
470-
// we must store it again.
471-
state.store(ui.ctx());
472-
}
473454
}
455+
// Ensure we store any change to the state if we reset expanded or if the render hook mutated it.
456+
state.store(ui.ctx());
474457
}
475458
}
476459

@@ -484,3 +467,8 @@ struct JsonTreeNodeConfig {
484467
/// Avoids potential conflicts in case a `u64` happened to be stored against the same tree Id.
485468
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
486469
struct ResetExpandedHashId(u64);
470+
471+
/// Stored in `egui`'s `IdTypeMap` to indicate that the tree should reset its expanded arrays/objects before rendering on a given frame.
472+
/// Avoids potential conflicts in case a `bool` happened to be stored against the same tree Id.
473+
#[derive(Clone, Copy, PartialEq, Eq, Default)]
474+
pub(crate) struct ShouldResetExpanded;

src/response.rs

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
use std::collections::HashSet;
1+
use egui::{Id, Ui};
22

3-
use egui::{Id, Ui, collapsing_header::CollapsingState};
3+
use crate::node::ShouldResetExpanded;
44

55
/// The response from showing a [`JsonTree`](crate::JsonTree).
66
pub struct JsonTreeResponse {
7-
pub(crate) collapsing_state_ids: HashSet<Id>,
7+
pub(crate) tree_id: Id,
88
}
99

1010
impl JsonTreeResponse {
@@ -14,10 +14,7 @@ impl JsonTreeResponse {
1414
/// Call this whenever the `default_expand` setting changes,
1515
/// and/or you when wish to reset any manually collapsed/expanded arrays and objects to respect this setting.
1616
pub fn reset_expanded(&self, ui: &mut Ui) {
17-
for id in self.collapsing_state_ids.iter() {
18-
if let Some(state) = CollapsingState::load(ui.ctx(), *id) {
19-
state.remove(ui.ctx());
20-
}
21-
}
17+
ui.ctx()
18+
.data_mut(|d| d.insert_temp(self.tree_id, ShouldResetExpanded))
2219
}
2320
}

src/search.rs

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@ impl SearchTerm {
3232
value: &T,
3333
abbreviate_root: bool,
3434
make_persistent_id: &dyn Fn(&[JsonPointerSegment]) -> Id,
35-
reset_path_ids: &mut HashSet<Id>,
3635
) -> HashSet<Id> {
3736
let mut search_match_path_ids = HashSet::new();
3837

@@ -42,7 +41,6 @@ impl SearchTerm {
4241
&mut vec![],
4342
&mut search_match_path_ids,
4443
make_persistent_id,
45-
reset_path_ids,
4644
);
4745

4846
if !abbreviate_root && search_match_path_ids.len() == 1 {
@@ -64,7 +62,6 @@ fn search_impl<'a, T: ToJsonTreeValue>(
6462
path_segments: &mut Vec<JsonPointerSegment<'a>>,
6563
search_match_path_ids: &mut HashSet<Id>,
6664
make_persistent_id: &dyn Fn(&[JsonPointerSegment]) -> Id,
67-
reset_path_ids: &mut HashSet<Id>,
6865
) {
6966
match value.to_json_tree_value() {
7067
JsonTreeValue::Base(_, display_value, _) => {
@@ -76,10 +73,6 @@ fn search_impl<'a, T: ToJsonTreeValue>(
7673
for (property, val) in entries.iter() {
7774
path_segments.push(*property);
7875

79-
if val.is_expandable() {
80-
reset_path_ids.insert(make_persistent_id(path_segments));
81-
}
82-
8376
// Ignore matches for indices in an array.
8477
if expandable_type == ExpandableType::Object && search_term.matches(property) {
8578
update_matches(path_segments, search_match_path_ids, make_persistent_id);
@@ -91,7 +84,6 @@ fn search_impl<'a, T: ToJsonTreeValue>(
9184
path_segments,
9285
search_match_path_ids,
9386
make_persistent_id,
94-
reset_path_ids,
9587
);
9688
path_segments.pop();
9789
}

src/tree.rs

Lines changed: 0 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -107,23 +107,3 @@ impl<'a, T: ToJsonTreeValue> JsonTree<'a, T> {
107107
JsonTreeNode::show(self, ui)
108108
}
109109
}
110-
111-
#[cfg(test)]
112-
mod test {
113-
use crate::DefaultExpand;
114-
115-
use super::JsonTree;
116-
117-
#[test]
118-
fn test_search_populates_all_collapsing_state_ids_in_response() {
119-
let value = serde_json::json!({"foo": [1, 2, [3]], "bar": { "qux" : false, "thud": { "a/b": [4, 5, { "m~n": "Greetings!" }]}, "grep": 21}, "baz": null});
120-
121-
egui::__run_test_ui(|ui| {
122-
let response = JsonTree::new("id", &value)
123-
.default_expand(DefaultExpand::SearchResults("g"))
124-
.show(ui);
125-
126-
assert_eq!(response.collapsing_state_ids.len(), 7);
127-
});
128-
}
129-
}

0 commit comments

Comments
 (0)