use std::marker::PhantomData;
use ratatui::prelude::*;
use ratatui::widgets::{Block, Borders};
use super::{Component, EventContext, RenderContext};
use crate::input::{Event, Key};
pub mod layout;
mod render;
pub use layout::{LayoutRect, squarified_layout};
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(
feature = "serialization",
derive(serde::Serialize, serde::Deserialize)
)]
pub struct TreemapNode {
pub label: String,
pub value: f64,
pub color: Color,
pub children: Vec<TreemapNode>,
}
impl TreemapNode {
pub fn new(label: impl Into<String>, value: f64) -> Self {
Self {
label: label.into(),
value,
color: Color::Gray,
children: Vec::new(),
}
}
pub fn with_color(mut self, color: Color) -> Self {
self.color = color;
self
}
pub fn set_color(&mut self, color: Color) {
self.color = color;
}
pub fn color(&self) -> Color {
self.color
}
pub fn with_child(mut self, child: TreemapNode) -> Self {
self.children.push(child);
self
}
pub fn with_children(mut self, children: Vec<TreemapNode>) -> Self {
self.children = children;
self
}
pub fn total_value(&self) -> f64 {
if self.children.is_empty() {
self.value
} else {
self.children.iter().map(|c| c.total_value()).sum()
}
}
pub fn is_leaf(&self) -> bool {
self.children.is_empty()
}
}
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(
feature = "serialization",
derive(serde::Serialize, serde::Deserialize)
)]
pub enum TreemapMessage {
SetRoot(TreemapNode),
Clear,
ZoomIn,
ZoomOut,
ResetZoom,
SelectNext,
SelectPrev,
SelectChild,
SelectParent,
}
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(
feature = "serialization",
derive(serde::Serialize, serde::Deserialize)
)]
pub enum TreemapOutput {
NodeSelected {
label: String,
value: f64,
},
ZoomedIn(String),
ZoomedOut,
}
#[derive(Clone, Debug, Default, PartialEq)]
#[cfg_attr(
feature = "serialization",
derive(serde::Serialize, serde::Deserialize)
)]
pub struct TreemapState {
root: Option<TreemapNode>,
selected_path: Vec<usize>,
zoom_path: Vec<usize>,
title: Option<String>,
show_labels: bool,
show_values: bool,
}
impl TreemapState {
pub fn new() -> Self {
Self {
show_labels: true,
..Default::default()
}
}
pub fn with_root(mut self, root: TreemapNode) -> Self {
self.root = Some(root);
self.selected_path = vec![0];
self.zoom_path = Vec::new();
self
}
pub fn with_title(mut self, title: impl Into<String>) -> Self {
self.title = Some(title.into());
self
}
pub fn with_show_labels(mut self, show: bool) -> Self {
self.show_labels = show;
self
}
pub fn with_show_values(mut self, show: bool) -> Self {
self.show_values = show;
self
}
pub fn root(&self) -> Option<&TreemapNode> {
self.root.as_ref()
}
pub fn set_root(&mut self, root: TreemapNode) {
self.root = Some(root);
self.selected_path = vec![0];
self.zoom_path = Vec::new();
}
pub fn clear(&mut self) {
self.root = None;
self.selected_path.clear();
self.zoom_path.clear();
}
pub fn current_view_node(&self) -> Option<&TreemapNode> {
let root = self.root.as_ref()?;
navigate_to_node(root, &self.zoom_path)
}
pub fn selected_node(&self) -> Option<&TreemapNode> {
let view_node = self.current_view_node()?;
if self.selected_path.is_empty() {
return None;
}
let idx = self.selected_path[0];
view_node.children.get(idx)
}
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 show_labels(&self) -> bool {
self.show_labels
}
pub fn set_show_labels(&mut self, show: bool) {
self.show_labels = show;
}
pub fn show_values(&self) -> bool {
self.show_values
}
pub fn set_show_values(&mut self, show: bool) {
self.show_values = show;
}
pub(crate) fn selected_child_index(&self) -> usize {
self.selected_path.first().copied().unwrap_or(0)
}
pub fn update(&mut self, msg: TreemapMessage) -> Option<TreemapOutput> {
Treemap::update(self, msg)
}
fn current_child_count(&self) -> usize {
self.current_view_node()
.map(|n| n.children.len())
.unwrap_or(0)
}
}
fn navigate_to_node<'a>(root: &'a TreemapNode, path: &[usize]) -> Option<&'a TreemapNode> {
let mut current = root;
for &idx in path {
current = current.children.get(idx)?;
}
Some(current)
}
pub struct Treemap(PhantomData<()>);
impl Component for Treemap {
type State = TreemapState;
type Message = TreemapMessage;
type Output = TreemapOutput;
fn init() -> Self::State {
TreemapState::new()
}
fn handle_event(
_state: &Self::State,
event: &Event,
ctx: &EventContext,
) -> Option<Self::Message> {
if !ctx.focused || ctx.disabled {
return None;
}
let key = event.as_key()?;
match key.code {
Key::Left | Key::Char('h') => Some(TreemapMessage::SelectPrev),
Key::Right | Key::Char('l') => Some(TreemapMessage::SelectNext),
Key::Down | Key::Char('j') => Some(TreemapMessage::SelectChild),
Key::Up | Key::Char('k') => Some(TreemapMessage::SelectParent),
Key::Enter => Some(TreemapMessage::ZoomIn),
Key::Esc | Key::Backspace => Some(TreemapMessage::ZoomOut),
Key::Home => Some(TreemapMessage::ResetZoom),
_ => None,
}
}
fn update(state: &mut Self::State, msg: Self::Message) -> Option<Self::Output> {
match msg {
TreemapMessage::SetRoot(root) => {
state.set_root(root);
None
}
TreemapMessage::Clear => {
state.clear();
None
}
TreemapMessage::ZoomIn => {
let child_count = state.current_child_count();
if child_count == 0 {
return None;
}
let selected_idx = state.selected_child_index();
if selected_idx >= child_count {
return None;
}
let has_children = state
.current_view_node()
.and_then(|n| n.children.get(selected_idx))
.map(|n| !n.children.is_empty())
.unwrap_or(false);
let label = state
.current_view_node()
.and_then(|n| n.children.get(selected_idx))
.map(|n| n.label.clone())
.unwrap_or_default();
if has_children {
state.zoom_path.push(selected_idx);
state.selected_path = vec![0];
Some(TreemapOutput::ZoomedIn(label))
} else {
let value = state
.current_view_node()
.and_then(|n| n.children.get(selected_idx))
.map(|n| n.total_value())
.unwrap_or(0.0);
Some(TreemapOutput::NodeSelected { label, value })
}
}
TreemapMessage::ZoomOut => {
if state.zoom_path.is_empty() {
return None;
}
let popped = state.zoom_path.pop().unwrap_or(0);
state.selected_path = vec![popped];
Some(TreemapOutput::ZoomedOut)
}
TreemapMessage::ResetZoom => {
if state.zoom_path.is_empty() {
return None;
}
state.zoom_path.clear();
state.selected_path = vec![0];
Some(TreemapOutput::ZoomedOut)
}
TreemapMessage::SelectNext => {
let child_count = state.current_child_count();
if child_count == 0 {
return None;
}
let current = state.selected_child_index();
if current + 1 < child_count {
state.selected_path = vec![current + 1];
}
None
}
TreemapMessage::SelectPrev => {
let child_count = state.current_child_count();
if child_count == 0 {
return None;
}
let current = state.selected_child_index();
if current > 0 {
state.selected_path = vec![current - 1];
}
None
}
TreemapMessage::SelectChild => {
let child_count = state.current_child_count();
if child_count == 0 {
return None;
}
let selected_idx = state.selected_child_index();
let has_children = state
.current_view_node()
.and_then(|n| n.children.get(selected_idx))
.map(|n| !n.children.is_empty())
.unwrap_or(false);
if has_children {
let label = state
.current_view_node()
.and_then(|n| n.children.get(selected_idx))
.map(|n| n.label.clone())
.unwrap_or_default();
state.zoom_path.push(selected_idx);
state.selected_path = vec![0];
Some(TreemapOutput::ZoomedIn(label))
} else {
None
}
}
TreemapMessage::SelectParent => {
if state.zoom_path.is_empty() {
return None;
}
let popped = state.zoom_path.pop().unwrap_or(0);
state.selected_path = vec![popped];
Some(TreemapOutput::ZoomedOut)
}
}
}
fn view(state: &Self::State, ctx: &mut RenderContext<'_, '_>) {
if ctx.area.height < 3 || ctx.area.width < 3 {
return;
}
crate::annotation::with_registry(|reg| {
reg.register(
ctx.area,
crate::annotation::Annotation::container("treemap")
.with_focus(ctx.focused)
.with_disabled(ctx.disabled),
);
});
let border_style = if ctx.disabled {
ctx.theme.disabled_style()
} else if ctx.focused {
ctx.theme.focused_border_style()
} else {
ctx.theme.border_style()
};
let mut block = Block::default()
.borders(Borders::ALL)
.border_style(border_style);
if let Some(ref title) = state.title {
block = block.title(title.as_str());
}
let inner = block.inner(ctx.area);
ctx.frame.render_widget(block, ctx.area);
if inner.height == 0 || inner.width == 0 || state.root.is_none() {
return;
}
render::render_treemap(
state,
ctx.frame,
inner,
ctx.theme,
ctx.focused,
ctx.disabled,
);
}
}
#[cfg(test)]
mod snapshot_tests;
#[cfg(test)]
mod tests;