diff --git a/README.md b/README.md index 6f93bab..daeb551 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,7 @@ let response = JsonTree::new("customised-tree", &value) }) .default_expand(DefaultExpand::All) .abbreviate_root(true) // Show {...} when the root object is collapsed. + .toggle_buttons_state(ToggleButtonsState::VisibleDisabled) .on_render(|ui, ctx| { // Customise rendering of the JsonTree, and/or handle interactions. match ctx { diff --git a/examples/demo.rs b/examples/demo.rs index 6a5c1a7..0be0181 100644 --- a/examples/demo.rs +++ b/examples/demo.rs @@ -12,7 +12,7 @@ use egui_json_tree::{ DefaultRender, RenderBaseValueContext, RenderContext, RenderExpandableDelimiterContext, RenderPropertyContext, }, - DefaultExpand, JsonTree, + DefaultExpand, JsonTree, ToggleButtonsState, }; use serde_json::{json, Value}; @@ -625,6 +625,53 @@ impl Show for JsonEditorExample { } } +struct ToggleButtonsCustomisationDemo { + value: Value, + toggle_buttons_state: ToggleButtonsState, +} + +impl ToggleButtonsCustomisationDemo { + fn new(value: Value) -> Self { + Self { + value, + toggle_buttons_state: Default::default(), + } + } +} + +impl Show for ToggleButtonsCustomisationDemo { + fn title(&self) -> &'static str { + "Toggle Buttons Customisation" + } + + fn show(&mut self, ui: &mut Ui) { + ui.vertical(|ui| { + ui.horizontal(|ui| { + ui.selectable_value( + &mut self.toggle_buttons_state, + ToggleButtonsState::VisibleEnabled, + "Visible and enabled", + ); + ui.selectable_value( + &mut self.toggle_buttons_state, + ToggleButtonsState::VisibleDisabled, + "Visible and disabled", + ); + ui.selectable_value( + &mut self.toggle_buttons_state, + ToggleButtonsState::Hidden, + "Hidden", + ); + }); + + JsonTree::new("show", &self.value) + .default_expand(DefaultExpand::All) + .toggle_buttons_state(self.toggle_buttons_state) + .show(ui); + }); + } +} + struct DemoApp { examples: Vec>, open_example_idx: Option, @@ -655,7 +702,8 @@ impl Default for DemoApp { Box::new(CustomExample::new("Custom Input")), Box::new(SearchExample::new(complex_object.clone())), Box::new(CopyToClipboardExample::new(complex_object.clone())), - Box::new(JsonEditorExample::new(complex_object)), + Box::new(JsonEditorExample::new(complex_object.clone())), + Box::new(ToggleButtonsCustomisationDemo::new(complex_object)), ], open_example_idx: None, } diff --git a/src/lib.rs b/src/lib.rs index fa45c2f..98407e1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,7 +8,7 @@ //! # DefaultRender, RenderBaseValueContext, RenderContext, RenderExpandableDelimiterContext, //! # RenderPropertyContext, //! # }, -//! # DefaultExpand, JsonTree, JsonTreeStyle +//! # DefaultExpand, JsonTree, JsonTreeStyle, ToggleButtonsState //! # }; //! # egui::__run_test_ui(|ui| { //! let value = serde_json::json!({ "foo": "bar", "fizz": [1, 2, 3]}); @@ -24,6 +24,7 @@ //! }) //! .default_expand(DefaultExpand::All) //! .abbreviate_root(true) // Show {...} when the root object is collapsed. +//! .toggle_buttons_state(ToggleButtonsState::VisibleDisabled) //! .on_render(|ui, ctx| { //! // Customise rendering of the JsonTree, and/or handle interactions. //! match ctx { @@ -76,6 +77,7 @@ mod node; mod response; mod search; mod style; +mod toggle_buttons_state; mod tree; pub mod delimiters; @@ -86,4 +88,5 @@ pub mod value; pub use default_expand::DefaultExpand; pub use response::JsonTreeResponse; pub use style::JsonTreeStyle; +pub use toggle_buttons_state::ToggleButtonsState; pub use tree::JsonTree; diff --git a/src/node.rs b/src/node.rs index d14b13a..0967d1e 100644 --- a/src/node.rs +++ b/src/node.rs @@ -1,6 +1,9 @@ use std::collections::{HashMap, HashSet}; -use egui::{collapsing_header::CollapsingState, Id, Ui}; +use egui::{ + collapsing_header::{paint_default_icon, CollapsingState}, + Id, Ui, +}; use crate::{ delimiters::{SpacingDelimiter, ARRAY_DELIMITERS, OBJECT_DELIMITERS}, @@ -13,7 +16,7 @@ use crate::{ search::SearchTerm, tree::JsonTreeConfig, value::{ExpandableType, JsonTreeValue, ToJsonTreeValue}, - DefaultExpand, JsonTreeStyle, + DefaultExpand, JsonTreeStyle, ToggleButtonsState, }; pub(crate) struct JsonTreeNode<'a, T: ToJsonTreeValue> { @@ -69,6 +72,7 @@ impl<'a, T: ToJsonTreeValue> JsonTreeNode<'a, T> { abbreviate_root: config.abbreviate_root, style: config.style, search_term, + toggle_buttons_state: config.toggle_buttons_state, }; // Wrap in a vertical layout in case this tree is placed directly in a horizontal layout, @@ -175,6 +179,7 @@ fn show_expandable<'a, 'b, T: ToJsonTreeValue>( abbreviate_root, style, search_term, + toggle_buttons_state, } = config; let delimiters = match expandable.expandable_type { @@ -193,228 +198,243 @@ fn show_expandable<'a, 'b, T: ToJsonTreeValue>( .entry(path_segments.to_vec()) .or_insert_with(|| make_persistent_id(path_segments)); - let state = CollapsingState::load_with_default_open(ui.ctx(), id_source, default_open); + let mut state = CollapsingState::load_with_default_open(ui.ctx(), id_source, default_open); let is_expanded = state.is_open(); - state - .show_header(ui, |ui| { - ui.horizontal_wrapped(|ui| { - ui.spacing_mut().item_spacing.x = 0.0; + let header_res = ui.horizontal_wrapped(|ui| { + ui.spacing_mut().item_spacing.x = 0.0; - if path_segments.is_empty() && !is_expanded { - if *abbreviate_root { - renderer.render_expandable_delimiter( - ui, - RenderExpandableDelimiterContext { - delimiter: delimiters.collapsed, - value: expandable.value, - pointer: JsonPointer(path_segments), - style, - }, - ); - return; - } + if let Some(enabled) = toggle_buttons_state.enabled() { + ui.add_enabled_ui(enabled, |ui| { + state.show_toggle_button(ui, paint_default_icon) + }); + } + + if path_segments.is_empty() && !is_expanded { + if *abbreviate_root { + renderer.render_expandable_delimiter( + ui, + RenderExpandableDelimiterContext { + delimiter: delimiters.collapsed, + value: expandable.value, + pointer: JsonPointer(path_segments), + style, + }, + ); + return; + } - renderer.render_expandable_delimiter( + renderer.render_expandable_delimiter( + ui, + RenderExpandableDelimiterContext { + delimiter: delimiters.opening, + value: expandable.value, + pointer: JsonPointer(path_segments), + style, + }, + ); + renderer.render_spacing_delimiter( + ui, + RenderSpacingDelimiterContext { + delimiter: SpacingDelimiter::Empty, + style, + }, + ); + + let entries_len = expandable.entries.len(); + + for (idx, (property, elem)) in expandable.entries.iter().enumerate() { + path_segments.push(*property); + + // Don't show array indices when the array is collapsed. + if matches!(expandable.expandable_type, ExpandableType::Object) { + renderer.render_property( ui, - RenderExpandableDelimiterContext { - delimiter: delimiters.opening, - value: expandable.value, + RenderPropertyContext { + property: *property, + value: elem, pointer: JsonPointer(path_segments), style, + search_term: search_term.as_ref(), }, ); renderer.render_spacing_delimiter( ui, RenderSpacingDelimiterContext { - delimiter: SpacingDelimiter::Empty, + delimiter: SpacingDelimiter::Colon, style, }, ); + } - let entries_len = expandable.entries.len(); - - for (idx, (property, elem)) in expandable.entries.iter().enumerate() { - path_segments.push(*property); - - // Don't show array indices when the array is collapsed. - if matches!(expandable.expandable_type, ExpandableType::Object) { - renderer.render_property( - ui, - RenderPropertyContext { - property: *property, - value: elem, - pointer: JsonPointer(path_segments), - style, - search_term: search_term.as_ref(), - }, - ); - renderer.render_spacing_delimiter( - ui, - RenderSpacingDelimiterContext { - delimiter: SpacingDelimiter::Colon, - style, - }, - ); - } - - match elem.to_json_tree_value() { - JsonTreeValue::Base(value, display_value, value_type) => { - renderer.render_value( - ui, - RenderBaseValueContext { - value, - display_value, - value_type, - pointer: JsonPointer(path_segments), - style, - search_term: search_term.as_ref(), - }, - ); - } - JsonTreeValue::Expandable(entries, expandable_type) => { - let nested_delimiters = match expandable_type { - ExpandableType::Array => &ARRAY_DELIMITERS, - ExpandableType::Object => &OBJECT_DELIMITERS, - }; - - let delimiter = if entries.is_empty() { - nested_delimiters.collapsed_empty - } else { - nested_delimiters.collapsed - }; - - renderer.render_expandable_delimiter( - ui, - RenderExpandableDelimiterContext { - delimiter, - value: elem, - pointer: JsonPointer(path_segments), - style, - }, - ); - } - }; - - let spacing = if idx == entries_len - 1 { - SpacingDelimiter::Empty - } else { - SpacingDelimiter::Comma - }; - - renderer.render_spacing_delimiter( - ui, - RenderSpacingDelimiterContext { - delimiter: spacing, - style, - }, - ); - - path_segments.pop(); - } - - renderer.render_expandable_delimiter( - ui, - RenderExpandableDelimiterContext { - delimiter: delimiters.closing, - value: expandable.value, - pointer: JsonPointer(path_segments), - style, - }, - ); - } else { - if let Some(property) = expandable.parent { - renderer.render_property( + match elem.to_json_tree_value() { + JsonTreeValue::Base(value, display_value, value_type) => { + renderer.render_value( ui, - RenderPropertyContext { - property, - value: expandable.value, + RenderBaseValueContext { + value, + display_value, + value_type, pointer: JsonPointer(path_segments), style, - search_term: config.search_term.as_ref(), - }, - ); - renderer.render_spacing_delimiter( - ui, - RenderSpacingDelimiterContext { - delimiter: SpacingDelimiter::Colon, - style, + search_term: search_term.as_ref(), }, ); } + JsonTreeValue::Expandable(entries, expandable_type) => { + let nested_delimiters = match expandable_type { + ExpandableType::Array => &ARRAY_DELIMITERS, + ExpandableType::Object => &OBJECT_DELIMITERS, + }; - if is_expanded { - renderer.render_expandable_delimiter( - ui, - RenderExpandableDelimiterContext { - delimiter: delimiters.opening, - value: expandable.value, - pointer: JsonPointer(path_segments), - style, - }, - ); - } else { - let delimiter = if expandable.entries.is_empty() { - delimiters.collapsed_empty + let delimiter = if entries.is_empty() { + nested_delimiters.collapsed_empty } else { - delimiters.collapsed + nested_delimiters.collapsed }; + renderer.render_expandable_delimiter( ui, RenderExpandableDelimiterContext { delimiter, - value: expandable.value, + value: elem, pointer: JsonPointer(path_segments), style, }, ); } - } - }); - }) - .body(|ui| { - for (property, elem) in expandable.entries { - let is_expandable = elem.is_expandable(); + }; - path_segments.push(property); + let spacing = if idx == entries_len - 1 { + SpacingDelimiter::Empty + } else { + SpacingDelimiter::Comma + }; - let mut add_nested_tree = |ui: &mut Ui| { - let nested_tree = JsonTreeNode { - id: expandable.id, - value: elem, - parent: Some(property), - }; + renderer.render_spacing_delimiter( + ui, + RenderSpacingDelimiterContext { + delimiter: spacing, + style, + }, + ); - nested_tree.show_impl( - ui, - path_segments, - path_id_map, - make_persistent_id, - config, - renderer, - ); - }; + path_segments.pop(); + } + + renderer.render_expandable_delimiter( + ui, + RenderExpandableDelimiterContext { + delimiter: delimiters.closing, + value: expandable.value, + pointer: JsonPointer(path_segments), + style, + }, + ); + } else { + if let Some(property) = expandable.parent { + renderer.render_property( + ui, + RenderPropertyContext { + property, + value: expandable.value, + pointer: JsonPointer(path_segments), + style, + search_term: config.search_term.as_ref(), + }, + ); + renderer.render_spacing_delimiter( + ui, + RenderSpacingDelimiterContext { + delimiter: SpacingDelimiter::Colon, + style, + }, + ); + } - if is_expandable { - add_nested_tree(ui); + if is_expanded { + renderer.render_expandable_delimiter( + ui, + RenderExpandableDelimiterContext { + delimiter: delimiters.opening, + value: expandable.value, + pointer: JsonPointer(path_segments), + style, + }, + ); + } else { + let delimiter = if expandable.entries.is_empty() { + delimiters.collapsed_empty } else { - ui.scope(|ui| { - ui.visuals_mut().indent_has_left_vline = false; - ui.spacing_mut().indent = - ui.spacing().icon_width + ui.spacing().icon_spacing; + delimiters.collapsed + }; + renderer.render_expandable_delimiter( + ui, + RenderExpandableDelimiterContext { + delimiter, + value: expandable.value, + pointer: JsonPointer(path_segments), + style, + }, + ); + } + } + }); - ui.indent(id_source, add_nested_tree); - }); - } + let toggle_buttons_hidden = *toggle_buttons_state == ToggleButtonsState::Hidden; + if toggle_buttons_hidden { + ui.visuals_mut().indent_has_left_vline = true; + ui.spacing_mut().indent = (ui.spacing().icon_width + ui.spacing().icon_spacing) / 2.0; + } - path_segments.pop(); + state.show_body_indented(&header_res.response, ui, |ui| { + for (property, elem) in expandable.entries { + let is_expandable = elem.is_expandable(); + + path_segments.push(property); + + let mut add_nested_tree = |ui: &mut Ui| { + let nested_tree = JsonTreeNode { + id: expandable.id, + value: elem, + parent: Some(property), + }; + + nested_tree.show_impl( + ui, + path_segments, + path_id_map, + make_persistent_id, + config, + renderer, + ); + }; + + if is_expandable && !toggle_buttons_hidden { + add_nested_tree(ui); + } else { + ui.scope(|ui| { + ui.visuals_mut().indent_has_left_vline = false; + ui.spacing_mut().indent = ui.spacing().icon_width + ui.spacing().icon_spacing; + + if toggle_buttons_hidden { + ui.spacing_mut().indent /= 2.0; + } + + ui.indent(id_source, add_nested_tree); + }); } - }); + + path_segments.pop(); + } + }); if is_expanded { ui.horizontal_wrapped(|ui| { - let indent = ui.spacing().icon_width / 2.0; - ui.add_space(indent); + if !toggle_buttons_hidden { + let indent = ui.spacing().icon_width / 2.0; + ui.add_space(indent); + } renderer.render_expandable_delimiter( ui, RenderExpandableDelimiterContext { @@ -433,6 +453,7 @@ struct JsonTreeNodeConfig<'a> { abbreviate_root: bool, style: JsonTreeStyle, search_term: Option, + toggle_buttons_state: ToggleButtonsState, } #[derive(Debug, Clone)] diff --git a/src/toggle_buttons_state.rs b/src/toggle_buttons_state.rs new file mode 100644 index 0000000..21aba55 --- /dev/null +++ b/src/toggle_buttons_state.rs @@ -0,0 +1,18 @@ +/// Setting for the visibility and interactivity of the toggle buttons for expanding/collapsing objects and arrays. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub enum ToggleButtonsState { + #[default] + VisibleEnabled, + VisibleDisabled, + Hidden, +} + +impl ToggleButtonsState { + pub(crate) fn enabled(&self) -> Option { + match self { + ToggleButtonsState::VisibleEnabled => Some(true), + ToggleButtonsState::VisibleDisabled => Some(false), + ToggleButtonsState::Hidden => None, + } + } +} diff --git a/src/tree.rs b/src/tree.rs index d5e4382..31b5870 100644 --- a/src/tree.rs +++ b/src/tree.rs @@ -2,7 +2,7 @@ use crate::{ node::JsonTreeNode, render::{JsonTreeRenderer, RenderContext}, value::ToJsonTreeValue, - DefaultExpand, JsonTreeResponse, JsonTreeStyle, + DefaultExpand, JsonTreeResponse, JsonTreeStyle, ToggleButtonsState, }; use egui::{Id, Ui}; use std::hash::Hash; @@ -12,6 +12,7 @@ pub(crate) struct JsonTreeConfig<'a, T: ToJsonTreeValue> { pub(crate) default_expand: DefaultExpand<'a>, pub(crate) abbreviate_root: bool, pub(crate) renderer: JsonTreeRenderer<'a, T>, + pub(crate) toggle_buttons_state: ToggleButtonsState, } impl<'a, T: ToJsonTreeValue> Default for JsonTreeConfig<'a, T> { @@ -21,6 +22,7 @@ impl<'a, T: ToJsonTreeValue> Default for JsonTreeConfig<'a, T> { default_expand: Default::default(), abbreviate_root: Default::default(), renderer: Default::default(), + toggle_buttons_state: Default::default(), } } } @@ -105,6 +107,12 @@ impl<'a, T: ToJsonTreeValue> JsonTree<'a, T> { self } + /// Override the visibility and interactivity of the toggle buttons for expanding/collapsing objects and arrays. + pub fn toggle_buttons_state(mut self, toggle_buttons_state: ToggleButtonsState) -> Self { + self.config.toggle_buttons_state = toggle_buttons_state; + self + } + /// Show the JSON tree visualisation within the `Ui`. pub fn show(self, ui: &mut Ui) -> JsonTreeResponse { JsonTreeNode::new(self.id, self.value).show_with_config(ui, self.config)