use std::collections::HashSet;
use serde_json::Value;
const INDENT: &str = " ";
const ROOT_LABEL: &str = "$";
const SCALAR_MARK: &str = "·";
const GLYPH_COLLAPSED: &str = "▶";
const GLYPH_EXPANDED: &str = "▼";
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Row {
pub depth: usize,
pub label: String,
pub node: Node,
pub path: String,
pub collapsed: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Node {
Scalar(String),
Object(usize),
Array(usize),
}
pub fn flatten(value: &Value, root: &str, collapsed: &HashSet<String>) -> Vec<Row> {
let mut rows = Vec::new();
push_rows(value, ROOT_LABEL, root, 0, collapsed, &mut rows);
rows
}
fn push_rows(
value: &Value,
label: &str,
path: &str,
depth: usize,
collapsed: &HashSet<String>,
rows: &mut Vec<Row>,
) {
let node = node_of(value);
let is_container = !matches!(node, Node::Scalar(_));
let collapsed_here = is_container && collapsed.contains(path);
rows.push(Row {
depth,
label: label.to_string(),
node,
path: path.to_string(),
collapsed: collapsed_here,
});
if collapsed_here {
return;
}
match value {
Value::Object(map) => {
for (key, child) in map {
push_rows(
child,
key,
&format!("{path}/{key}"),
depth + 1,
collapsed,
rows,
);
}
}
Value::Array(items) => {
for (i, child) in items.iter().enumerate() {
push_rows(
child,
&format!("[{i}]"),
&format!("{path}/{i}"),
depth + 1,
collapsed,
rows,
);
}
}
_ => {}
}
}
fn node_of(value: &Value) -> Node {
match value {
Value::Object(map) => Node::Object(map.len()),
Value::Array(items) => Node::Array(items.len()),
scalar => Node::Scalar(scalar.to_string()),
}
}
pub fn render(ui: &mut egui::Ui, value: &Value, root: &str, collapsed: &mut HashSet<String>) {
for row in flatten(value, root, collapsed) {
render_row(ui, &row, collapsed);
}
}
fn render_row(ui: &mut egui::Ui, row: &Row, collapsed: &mut HashSet<String>) {
ui.horizontal(|ui| {
if row.depth > 0 {
ui.monospace(INDENT.repeat(row.depth));
}
match &row.node {
Node::Scalar(text) => {
ui.monospace(SCALAR_MARK);
ui.monospace(format!("{}:", row.label));
ui.monospace(text);
}
Node::Object(len) => container_row(ui, row, collapsed, &format!("object {len} keys")),
Node::Array(len) => container_row(ui, row, collapsed, &format!("array {len} items")),
}
});
}
fn container_row(ui: &mut egui::Ui, row: &Row, collapsed: &mut HashSet<String>, summary: &str) {
let glyph = if row.collapsed {
GLYPH_COLLAPSED
} else {
GLYPH_EXPANDED
};
let toggle = ui
.add(egui::Label::new(egui::RichText::new(glyph).monospace()).sense(egui::Sense::click()));
if toggle.clicked() {
toggle_path(collapsed, &row.path);
}
ui.monospace(format!("{}:", row.label));
ui.monospace(summary);
}
pub(crate) fn toggle_path(collapsed: &mut HashSet<String>, path: &str) {
if !collapsed.remove(path) {
collapsed.insert(path.to_string());
}
}
#[cfg(test)]
mod tests;