use crate::{
DefaultExpand, JsonTreeResponse, JsonTreeStyle,
node::JsonTreeNode,
render::{JsonTreeRenderer, RenderContext},
value::ToJsonTreeValue,
};
use egui::{Id, Ui};
use std::hash::Hash;
pub(crate) struct JsonTreeConfig<'a, T: ToJsonTreeValue> {
pub(crate) style: Option<JsonTreeStyle>,
pub(crate) default_expand: Option<DefaultExpand<'a>>,
pub(crate) auto_reset_expanded: bool,
pub(crate) renderer: JsonTreeRenderer<'a, T>,
}
impl<T: ToJsonTreeValue> Default for JsonTreeConfig<'_, T> {
fn default() -> Self {
Self {
style: Default::default(),
default_expand: Default::default(),
auto_reset_expanded: true,
renderer: Default::default(),
}
}
}
#[must_use = "You should call .show()"]
pub struct JsonTree<'a, T: ToJsonTreeValue> {
pub(crate) id: Id,
pub(crate) value: &'a T,
pub(crate) config: JsonTreeConfig<'a, T>,
}
impl<'a, T: ToJsonTreeValue> JsonTree<'a, T> {
pub fn new(id: impl Hash, value: &'a T) -> Self {
Self {
id: Id::new(id),
value,
config: JsonTreeConfig::default(),
}
}
pub fn style(mut self, style: JsonTreeStyle) -> Self {
self.config.style = Some(style);
self
}
pub fn default_expand(mut self, default_expand: DefaultExpand<'a>) -> Self {
self.config.default_expand = Some(default_expand);
self
}
pub fn auto_reset_expanded(mut self, auto_reset_expanded: bool) -> Self {
self.config.auto_reset_expanded = auto_reset_expanded;
self
}
pub fn on_render_if(
self,
condition: bool,
render_hook: impl FnMut(&mut Ui, RenderContext<'a, '_, T>) + 'a,
) -> Self {
if condition {
self.on_render(render_hook)
} else {
self
}
}
pub fn on_render(
mut self,
render_hook: impl FnMut(&mut Ui, RenderContext<'a, '_, T>) + 'a,
) -> Self {
self.config.renderer.render_hook = Some(Box::new(render_hook));
self
}
pub fn show(self, ui: &mut Ui) -> JsonTreeResponse {
JsonTreeNode::show(self, ui)
}
}
#[cfg(test)]
mod tests {
use std::sync::LazyLock;
use egui::accesskit::Role;
use egui_kittest::Node;
use egui_kittest::kittest::NodeT;
use egui_kittest::{Harness, kittest::Queryable};
use serde_json::{Value, json};
use crate::{DefaultExpand, JsonTree, JsonTreeStyle, ToggleButtonsState};
static OBJECT: LazyLock<Value> = LazyLock::new(|| {
json!({
"bar": {
"grep": 21,
"qux": false,
},
"baz": null,
"foo": [
1,
"two"
]
})
});
#[test]
fn render_object_with_toggle_buttons_visible_disabled() {
let harness = Harness::new_ui(|ui| {
JsonTree::new("id", &*OBJECT)
.default_expand(DefaultExpand::All)
.style(
JsonTreeStyle::new().toggle_buttons_state(ToggleButtonsState::VisibleDisabled),
)
.show(ui);
});
assert_eq!(query_all_collapsing_headers(&harness).count(), 3);
assert!(
query_all_collapsing_headers(&harness).all(|node| node.accesskit_node().is_disabled())
)
}
#[test]
fn render_object_with_toggle_buttons_visible_enabled() {
let harness = Harness::new_ui(|ui| {
JsonTree::new("id", &*OBJECT)
.default_expand(DefaultExpand::All)
.style(
JsonTreeStyle::new().toggle_buttons_state(ToggleButtonsState::VisibleEnabled),
)
.show(ui);
});
assert_eq!(query_all_collapsing_headers(&harness).count(), 3);
assert!(
query_all_collapsing_headers(&harness).all(|node| !node.accesskit_node().is_disabled())
)
}
#[test]
fn render_object_with_toggle_buttons_hidden() {
let harness = Harness::new_ui(|ui| {
JsonTree::new("id", &*OBJECT)
.default_expand(DefaultExpand::All)
.style(JsonTreeStyle::new().toggle_buttons_state(ToggleButtonsState::Hidden))
.show(ui);
});
assert_eq!(query_all_collapsing_headers(&harness).count(), 0);
}
#[test]
fn render_object_with_interaction_and_manual_reset_expanded() {
let mut harness = Harness::new_ui_state(
|ui, should_reset_expanded| {
let response = JsonTree::new("id", &*OBJECT)
.default_expand(DefaultExpand::None)
.style(JsonTreeStyle::new().abbreviate_root(true))
.show(ui);
if *should_reset_expanded {
response.reset_expanded(ui);
}
},
false,
);
assert_eq!(query_all_collapsing_headers(&harness).count(), 1);
assert_eq!(harness.query_all_by_role(Role::Label).count(), 1);
get_collapsing_header_node(&harness, "").click();
harness.run();
assert_eq!(query_all_collapsing_headers(&harness).count(), 3);
assert_eq!(harness.query_all_by_role(Role::Label).count(), 11);
get_collapsing_header_node(&harness, "/bar").click();
harness.run();
assert_eq!(harness.query_all_by_role(Role::Label).count(), 18);
assert!(harness.query_by_label("\"grep\"").is_some());
assert!(harness.query_by_label("21").is_some());
assert!(harness.query_by_label("\"qux\"").is_some());
assert!(harness.query_by_label("false").is_some());
*harness.state_mut() = true;
harness.run();
harness.run();
assert_eq!(query_all_collapsing_headers(&harness).count(), 1);
assert_eq!(harness.query_all_by_role(Role::Label).count(), 1);
}
#[test]
fn render_object_with_default_expand_none_and_abbreviated_root() {
let harness = Harness::new_ui(|ui| {
JsonTree::new("id", &*OBJECT)
.default_expand(DefaultExpand::None)
.style(JsonTreeStyle::new().abbreviate_root(true))
.show(ui);
});
assert_eq!(query_all_collapsing_headers(&harness).count(), 1);
assert_eq!(harness.get_by_role(Role::Label).value().unwrap(), "{...}");
}
#[test]
fn render_array_with_default_expand_none_and_abbreviated_root() {
let harness = Harness::new_ui(|ui| {
JsonTree::new("id", &json!([1, 2, 3]))
.default_expand(DefaultExpand::None)
.style(JsonTreeStyle::new().abbreviate_root(true))
.show(ui);
});
assert_eq!(query_all_collapsing_headers(&harness).count(), 1);
assert_eq!(harness.get_by_role(Role::Label).value().unwrap(), "[...]");
}
#[test]
fn render_object_with_default_expand_search_results_or_all_when_empty_string_expands_everything()
{
let harness = Harness::new_ui(|ui| {
JsonTree::new("id", &*OBJECT)
.default_expand(DefaultExpand::SearchResultsOrAll(""))
.show(ui);
});
assert_eq!(query_all_collapsing_headers(&harness).count(), 3);
assert_eq!(harness.query_all_by_role(Role::Label).count(), 25);
}
fn query_all_collapsing_headers<'a, S>(
harness: &'a Harness<'_, S>,
) -> impl Iterator<Item = Node<'a>> {
harness.query_all_by_role(Role::Button)
}
fn get_collapsing_header_node<'a, S>(
harness: &'a Harness<'_, S>,
pointer: &'a str,
) -> Node<'a> {
harness.get_by_role_and_label(Role::Button, pointer)
}
}