use std::collections::HashSet;
use super::{Component, EventContext, RenderContext};
use crate::input::{Event, Key};
use crate::scroll::ScrollState;
mod render;
mod types;
pub use types::{FlatSpan, SpanNode};
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(
feature = "serialization",
derive(serde::Serialize, serde::Deserialize)
)]
pub enum SpanTreeMessage {
SetRoots(Vec<SpanNode>),
SelectUp,
SelectDown,
Expand,
Collapse,
Toggle,
ExpandAll,
CollapseAll,
SetLabelWidth(u16),
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(
feature = "serialization",
derive(serde::Serialize, serde::Deserialize)
)]
pub enum SpanTreeOutput {
Selected(String),
Expanded(String),
Collapsed(String),
}
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(
feature = "serialization",
derive(serde::Serialize, serde::Deserialize)
)]
pub struct SpanTreeState {
roots: Vec<SpanNode>,
selected_index: Option<usize>,
expanded: HashSet<String>,
scroll: ScrollState,
global_start: f64,
global_end: f64,
label_width: u16,
title: Option<String>,
}
impl Default for SpanTreeState {
fn default() -> Self {
Self {
roots: Vec::new(),
selected_index: None,
expanded: HashSet::new(),
scroll: ScrollState::default(),
global_start: 0.0,
global_end: 0.0,
label_width: 30,
title: None,
}
}
}
impl SpanTreeState {
pub fn new(roots: Vec<SpanNode>) -> Self {
let mut expanded = HashSet::new();
for root in &roots {
Self::collect_expanded_ids(root, &mut expanded);
}
let (global_start, global_end) = Self::compute_global_range(&roots);
let selected_index = if roots.is_empty() { None } else { Some(0) };
let mut state = Self {
roots,
selected_index,
expanded,
scroll: ScrollState::default(),
global_start,
global_end,
label_width: 30,
title: None,
};
state.scroll.set_content_length(state.flatten().len());
state
}
pub fn with_title(mut self, title: impl Into<String>) -> Self {
self.title = Some(title.into());
self
}
pub fn with_label_width(mut self, width: u16) -> Self {
self.label_width = width;
self
}
pub fn roots(&self) -> &[SpanNode] {
&self.roots
}
pub fn roots_mut(&mut self) -> &mut Vec<SpanNode> {
&mut self.roots
}
pub fn set_roots(&mut self, roots: Vec<SpanNode>) {
self.expanded.clear();
for root in &roots {
Self::collect_expanded_ids(root, &mut self.expanded);
}
let (global_start, global_end) = Self::compute_global_range(&roots);
self.global_start = global_start;
self.global_end = global_end;
self.roots = roots;
self.selected_index = if self.roots.is_empty() { None } else { Some(0) };
self.scroll.set_content_length(self.flatten().len());
}
pub fn selected_index(&self) -> Option<usize> {
self.selected_index
}
pub fn selected_span(&self) -> Option<FlatSpan> {
let flat = self.flatten();
let idx = self.selected_index?;
flat.into_iter().nth(idx)
}
pub fn global_start(&self) -> f64 {
self.global_start
}
pub fn global_end(&self) -> f64 {
self.global_end
}
pub fn label_width(&self) -> u16 {
self.label_width
}
pub fn title(&self) -> Option<&str> {
self.title.as_deref()
}
pub fn set_title(&mut self, title: impl Into<String>) {
self.title = Some(title.into());
}
pub fn is_empty(&self) -> bool {
self.roots.is_empty()
}
pub fn expanded_ids(&self) -> &HashSet<String> {
&self.expanded
}
pub fn expand(&mut self, id: &str) {
self.expanded.insert(id.to_string());
self.scroll.set_content_length(self.flatten().len());
}
pub fn collapse(&mut self, id: &str) {
self.expanded.remove(id);
let visible = self.flatten().len();
if let Some(idx) = self.selected_index {
if idx >= visible {
self.selected_index = Some(visible.saturating_sub(1));
}
}
self.scroll.set_content_length(visible);
}
pub fn expand_all(&mut self) {
self.expanded.clear();
for root in &self.roots {
Self::collect_expanded_ids(root, &mut self.expanded);
}
self.scroll.set_content_length(self.flatten().len());
}
pub fn collapse_all(&mut self) {
self.expanded.clear();
self.selected_index = if self.roots.is_empty() { None } else { Some(0) };
self.scroll.set_content_length(self.flatten().len());
}
pub fn flatten(&self) -> Vec<FlatSpan> {
let mut result = Vec::new();
for root in &self.roots {
self.flatten_node(root, 0, &mut result);
}
result
}
pub fn update(&mut self, msg: SpanTreeMessage) -> Option<SpanTreeOutput> {
SpanTree::update(self, msg)
}
fn collect_expanded_ids(node: &SpanNode, expanded: &mut HashSet<String>) {
if node.has_children() {
expanded.insert(node.id.clone());
for child in &node.children {
Self::collect_expanded_ids(child, expanded);
}
}
}
fn compute_global_range(roots: &[SpanNode]) -> (f64, f64) {
let mut min_start = f64::INFINITY;
let mut max_end = f64::NEG_INFINITY;
for root in roots {
Self::compute_range_recursive(root, &mut min_start, &mut max_end);
}
if min_start.is_infinite() {
(0.0, 0.0)
} else {
(min_start, max_end)
}
}
fn compute_range_recursive(node: &SpanNode, min_start: &mut f64, max_end: &mut f64) {
if node.start < *min_start {
*min_start = node.start;
}
if node.end > *max_end {
*max_end = node.end;
}
for child in &node.children {
Self::compute_range_recursive(child, min_start, max_end);
}
}
fn flatten_node(&self, node: &SpanNode, depth: usize, result: &mut Vec<FlatSpan>) {
let is_expanded = self.expanded.contains(&node.id);
result.push(FlatSpan {
id: node.id.clone(),
label: node.label.clone(),
start: node.start,
end: node.end,
color: node.color,
status: node.status.clone(),
depth,
has_children: node.has_children(),
is_expanded,
});
if is_expanded {
for child in &node.children {
self.flatten_node(child, depth + 1, result);
}
}
}
}
pub struct SpanTree;
impl Component for SpanTree {
type State = SpanTreeState;
type Message = SpanTreeMessage;
type Output = SpanTreeOutput;
fn init() -> Self::State {
SpanTreeState::default()
}
fn update(state: &mut Self::State, msg: Self::Message) -> Option<Self::Output> {
match msg {
SpanTreeMessage::SetRoots(roots) => {
state.set_roots(roots);
None
}
SpanTreeMessage::SetLabelWidth(width) => {
state.label_width = width.clamp(10, 100);
None
}
SpanTreeMessage::ExpandAll => {
state.expand_all();
None
}
SpanTreeMessage::CollapseAll => {
state.collapse_all();
None
}
_ => {
let flat = state.flatten();
if flat.is_empty() {
return None;
}
let selected = state.selected_index?;
match msg {
SpanTreeMessage::SelectUp => {
if selected > 0 {
state.selected_index = Some(selected - 1);
let span = &flat[selected - 1];
return Some(SpanTreeOutput::Selected(span.id.clone()));
}
None
}
SpanTreeMessage::SelectDown => {
if selected < flat.len() - 1 {
state.selected_index = Some(selected + 1);
let span = &flat[selected + 1];
return Some(SpanTreeOutput::Selected(span.id.clone()));
}
None
}
SpanTreeMessage::Expand => {
if let Some(span) = flat.get(selected) {
if span.has_children && !span.is_expanded {
let id = span.id.clone();
state.expanded.insert(id.clone());
state.scroll.set_content_length(state.flatten().len());
return Some(SpanTreeOutput::Expanded(id));
}
}
None
}
SpanTreeMessage::Collapse => {
if let Some(span) = flat.get(selected) {
if span.has_children && span.is_expanded {
let id = span.id.clone();
state.expanded.remove(&id);
let new_flat = state.flatten();
if selected >= new_flat.len() {
state.selected_index = Some(new_flat.len().saturating_sub(1));
}
state.scroll.set_content_length(new_flat.len());
return Some(SpanTreeOutput::Collapsed(id));
}
}
None
}
SpanTreeMessage::Toggle => {
if let Some(span) = flat.get(selected) {
if span.has_children {
let id = span.id.clone();
if span.is_expanded {
state.expanded.remove(&id);
let new_flat = state.flatten();
if selected >= new_flat.len() {
state.selected_index =
Some(new_flat.len().saturating_sub(1));
}
state.scroll.set_content_length(new_flat.len());
return Some(SpanTreeOutput::Collapsed(id));
} else {
state.expanded.insert(id.clone());
state.scroll.set_content_length(state.flatten().len());
return Some(SpanTreeOutput::Expanded(id));
}
}
}
None
}
SpanTreeMessage::SetRoots(_)
| SpanTreeMessage::SetLabelWidth(_)
| SpanTreeMessage::ExpandAll
| SpanTreeMessage::CollapseAll => unreachable!(),
}
}
}
}
fn handle_event(
state: &Self::State,
event: &Event,
ctx: &EventContext,
) -> Option<Self::Message> {
if !ctx.focused || ctx.disabled {
return None;
}
if let Some(key) = event.as_key() {
let has_shift = key.modifiers.shift();
match key.code {
Key::Up | Key::Char('k') if !has_shift => Some(SpanTreeMessage::SelectUp),
Key::Down | Key::Char('j') if !has_shift => Some(SpanTreeMessage::SelectDown),
Key::Right | Key::Char('l') if has_shift => Some(SpanTreeMessage::SetLabelWidth(
state.label_width.saturating_add(2),
)),
Key::Left | Key::Char('h') if has_shift => Some(SpanTreeMessage::SetLabelWidth(
state.label_width.saturating_sub(2),
)),
Key::Right | Key::Char('l') => Some(SpanTreeMessage::Expand),
Key::Left | Key::Char('h') => Some(SpanTreeMessage::Collapse),
Key::Char(' ') | Key::Enter => Some(SpanTreeMessage::Toggle),
_ => None,
}
} else {
None
}
}
fn view(state: &Self::State, ctx: &mut RenderContext<'_, '_>) {
render::render_span_tree(
state,
ctx.frame,
ctx.area,
ctx.theme,
ctx.focused,
ctx.disabled,
);
}
}
#[cfg(test)]
mod snapshot_tests;
#[cfg(test)]
mod tests;