use crate::treemap::layout_hierarchical;
use crate::types::{ProjectAnalysis, TreeMapNode, TreeMapRect};
use dioxus::prelude::*;
use std::sync::{Arc, Mutex};
const WINDOW_WIDTH: f64 = 1200.0;
const WINDOW_HEIGHT: f64 = 800.0;
const PADDING: f64 = 2.0;
static ANALYSIS_DATA: Mutex<Option<Arc<ProjectAnalysis>>> = Mutex::new(None);
#[component]
pub fn App() -> Element {
let analysis = {
let data = ANALYSIS_DATA.lock().unwrap();
data.clone().expect("Analysis data not initialized")
};
let mut selected_node = use_signal(|| None::<String>);
let mut zoom_stack = use_signal(|| Vec::<String>::new());
let mut tree_nodes = TreeMapNode::from_analysis(&analysis);
let current_zoom = zoom_stack.read().last().cloned();
if let Some(ref crate_name) = current_zoom {
if let Some(node) = tree_nodes.iter_mut().find(|n| &n.label == crate_name) {
tree_nodes = vec![node.clone()];
}
}
let bounds = TreeMapRect::new(0.0, 0.0, WINDOW_WIDTH, WINDOW_HEIGHT);
layout_hierarchical(&mut tree_nodes, bounds, PADDING);
let on_select = move |label: String| {
selected_node.set(Some(label));
};
let on_zoom = move |label: String| {
zoom_stack.write().push(label);
};
let on_zoom_out = move |_| {
zoom_stack.write().pop();
};
rsx! {
style { {include_str!("ui_styles.css")} }
div { class: "container",
div { class: "header",
h1 { "Cargo Perspective - {analysis.root_crate}" }
div { class: "stats",
span { "Total calls: {analysis.total_calls}" }
span { " | Crates: {analysis.crates.len()}" }
}
if current_zoom.is_some() {
button {
class: "zoom-out-btn",
onclick: on_zoom_out,
"← Back to overview"
}
}
}
svg {
class: "treemap",
width: "{WINDOW_WIDTH}",
height: "{WINDOW_HEIGHT}",
view_box: "0 0 {WINDOW_WIDTH} {WINDOW_HEIGHT}",
for node in &tree_nodes {
CrateNode {
node: node.clone(),
selected: selected_node.read().clone(),
on_select: on_select,
on_zoom: on_zoom
}
}
}
if let Some(ref label) = *selected_node.read() {
{render_info_panel(label, &analysis)}
}
}
}
}
#[component]
fn CrateNode(
node: TreeMapNode,
selected: Option<String>,
on_select: EventHandler<String>,
on_zoom: EventHandler<String>,
) -> Element {
let rect = match &node.rect {
Some(r) => r.clone(),
None => {
return rsx! { g {} };
}
};
if rect.width < 1.0 || rect.height < 1.0 {
return rsx! { g {} };
}
let label = node.label.clone();
let is_selected = selected.as_ref() == Some(&label);
let color = "#805ad5";
let stroke_color = if is_selected { "#f56565" } else { "#2d3748" };
let stroke_width = if is_selected { "2" } else { "1" };
let opacity = 0.7 + (rect.area() / (WINDOW_WIDTH * WINDOW_HEIGHT) * 0.3).min(0.3);
let value = node.value;
let label_for_enter = label.clone();
let label_for_click = label.clone();
let label_text = label.clone();
rsx! {
g {
rect {
x: "{rect.x}",
y: "{rect.y}",
width: "{rect.width}",
height: "{rect.height}",
fill: color,
stroke: stroke_color,
stroke_width: stroke_width,
opacity: "{opacity}",
cursor: "pointer",
onmouseenter: move |_| {
on_select.call(label_for_enter.clone());
},
onclick: move |_| {
on_zoom.call(label_for_click.clone());
},
title { "{label_text} ({value} calls)" }
}
if rect.width > 50.0 && rect.height > 20.0 {
text {
x: "{rect.x + rect.width / 2.0}",
y: "{rect.y + rect.height / 2.0}",
text_anchor: "middle",
dominant_baseline: "middle",
font_size: "10",
fill: "white",
pointer_events: "none",
style: "user-select: none;",
"{truncate_label(&label, rect.width)}"
}
}
}
}
}
fn render_info_panel(label: &str, analysis: &ProjectAnalysis) -> Element {
let info = if let Some(crate_usage) = analysis.crates.get(label) {
format!(
"Crate: {}\nType: {}\nTotal calls: {}\nMethods: {}",
crate_usage.name,
if crate_usage.is_direct { "Direct" } else { "Transitive" },
crate_usage.total_calls,
crate_usage.methods.len()
)
} else {
for (crate_name, crate_usage) in &analysis.crates {
if let Some(method) = crate_usage.methods.get(label) {
return rsx! {
div { class: "info-panel",
h3 { "Method: {method.path}" }
p { "Crate: {crate_name}" }
p { "Calls: {method.call_count}" }
if method.call_sites.len() <= 10 {
details {
summary { "Call sites ({method.call_sites.len()})" }
ul {
for site in method.call_sites.iter() {
li { "{site.file_path}:{site.line}:{site.column}" }
}
}
}
} else {
p { "{method.call_sites.len()} call sites (too many to display)" }
}
}
};
}
}
format!("Selected: {}", label)
};
rsx! {
div { class: "info-panel",
pre { "{info}" }
}
}
}
fn truncate_label(label: &str, width: f64) -> String {
let max_chars = (width / 8.0) as usize; if label.len() <= max_chars {
label.to_string()
} else if max_chars > 3 {
format!("{}...", &label[..max_chars - 3])
} else {
String::new()
}
}
pub fn launch(analysis: ProjectAnalysis) {
{
let mut data = ANALYSIS_DATA.lock().unwrap();
*data = Some(Arc::new(analysis));
}
dioxus::launch(App);
}