use std::collections::BTreeMap;
use eframe::egui::{
self, Align2, Color32, FontId, Pos2, Rect, Rounding, Sense, Stroke, Ui, Vec2,
};
use uuid::Uuid;
use super::model::Timeline;
const NODE_W: f32 = 160.0;
const NODE_H: f32 = 56.0;
const COL_GAP: f32 = 220.0;
const ROW_GAP: f32 = 90.0;
pub fn draw_dep_graph(ui: &mut Ui, tl: &Timeline, selected_release: Option<Uuid>) {
let release = selected_release.or_else(|| tl.release_order.last().copied());
let Some(release_id) = release else {
ui.label("no releases recorded yet");
return;
};
let snapshot = tl.snapshot_for(&release_id);
let mut order_by_repo: BTreeMap<&str, usize> = BTreeMap::new();
for lane in &tl.lanes {
if let Some(n) = lane.nodes.iter().find(|n| n.release_id == release_id) {
let _ = n;
}
}
let mut repos: Vec<&str> = tl.lanes.iter().map(|l| l.repo.as_str()).collect();
repos.sort();
if let Some(snap) = snapshot {
repos = toposort(&repos, &snap.edges);
}
for (i, r) in repos.iter().enumerate() {
order_by_repo.insert(r, i);
}
let available = ui.available_size();
let needed_w = LEFT_PAD + repos.len() as f32 * COL_GAP + NODE_W;
let canvas = Vec2::new(available.x.max(needed_w), available.y.max(360.0));
let (rect, _resp) = ui.allocate_exact_size(canvas, Sense::hover());
let painter = ui.painter_at(rect);
painter.rect_filled(rect, Rounding::ZERO, Color32::from_rgb(18, 18, 24));
let row_for = |_repo: &str| 0usize;
let pos_for = |repo: &str| -> Pos2 {
let col = *order_by_repo.get(repo).unwrap_or(&0);
let row = row_for(repo);
Pos2::new(
rect.left() + LEFT_PAD + col as f32 * COL_GAP,
rect.top() + TOP_PAD + row as f32 * ROW_GAP,
)
};
if let Some(snap) = snapshot {
for e in &snap.edges {
let (Some(_), Some(_)) = (
order_by_repo.get(e.from.as_str()),
order_by_repo.get(e.to.as_str()),
) else {
continue;
};
let from = pos_for(&e.from) + Vec2::new(NODE_W / 2.0, NODE_H / 2.0);
let to = pos_for(&e.to) + Vec2::new(NODE_W / 2.0, NODE_H / 2.0);
draw_arrow(&painter, from, to, Color32::from_rgb(180, 140, 220));
let mid = Pos2::new((from.x + to.x) / 2.0, (from.y + to.y) / 2.0 - 8.0);
let via = e.via.iter().take(2).cloned().collect::<Vec<_>>().join(", ");
let label = if e.via.len() > 2 {
format!("{via} +{}", e.via.len() - 2)
} else {
via
};
painter.text(
mid,
Align2::CENTER_BOTTOM,
&label,
FontId::monospace(10.0),
Color32::from_rgb(190, 190, 210),
);
}
}
for lane in &tl.lanes {
let node_pos = pos_for(&lane.repo);
let node_rect = Rect::from_min_size(node_pos, Vec2::new(NODE_W, NODE_H));
let status_color = lane
.nodes
.iter()
.find(|n| n.release_id == release_id)
.map(|n| status_color(&n.gate_status))
.unwrap_or(Color32::from_rgb(80, 80, 100));
painter.rect_filled(node_rect, Rounding::same(6.0), Color32::from_rgb(30, 30, 40));
painter.rect_stroke(node_rect, Rounding::same(6.0), Stroke::new(2.0, status_color));
painter.text(
node_pos + Vec2::new(NODE_W / 2.0, 14.0),
Align2::CENTER_TOP,
&lane.repo,
FontId::proportional(15.0),
Color32::WHITE,
);
if let Some(n) = lane.nodes.iter().find(|n| n.release_id == release_id) {
painter.text(
node_pos + Vec2::new(NODE_W / 2.0, 34.0),
Align2::CENTER_TOP,
&n.sha[..n.sha.len().min(12)],
FontId::monospace(11.0),
Color32::from_rgb(200, 200, 210),
);
}
}
}
const LEFT_PAD: f32 = 30.0;
const TOP_PAD: f32 = 40.0;
fn status_color(status: &str) -> Color32 {
match status {
s if s.starts_with("succeeded_dry_run") => Color32::from_rgb(200, 160, 60),
s if s.starts_with("succeeded") => Color32::from_rgb(80, 180, 120),
"failed_test" => Color32::from_rgb(220, 80, 80),
"failed_bench" => Color32::from_rgb(220, 130, 60),
_ => Color32::from_rgb(140, 140, 160),
}
}
fn draw_arrow(painter: &egui::Painter, from: Pos2, to: Pos2, color: Color32) {
let stroke = Stroke::new(2.0, color);
painter.line_segment([from, to], stroke);
let dir = (to - from).normalized();
let perp = Vec2::new(-dir.y, dir.x);
let tip = to - dir * (NODE_W / 2.0 + 4.0);
let base = tip - dir * 10.0;
let a = base + perp * 5.0;
let b = base - perp * 5.0;
painter.add(egui::Shape::convex_polygon(vec![tip, a, b], color, Stroke::NONE));
}
fn toposort<'a>(
repos: &[&'a str],
edges: &'a [crate::warehouse::dep_graph::CrossRepoEdge],
) -> Vec<&'a str> {
use std::collections::{BTreeMap, BTreeSet, VecDeque};
let set: BTreeSet<&str> = repos.iter().copied().collect();
let mut indeg: BTreeMap<&str, usize> = repos.iter().map(|r| (*r, 0)).collect();
let mut adj: BTreeMap<&str, Vec<&str>> = BTreeMap::new();
for e in edges {
let from = e.from.as_str();
let to = e.to.as_str();
if !set.contains(from) || !set.contains(to) {
continue;
}
adj.entry(to).or_default().push(from);
*indeg.entry(from).or_insert(0) += 1;
}
let mut q: VecDeque<&str> = indeg.iter().filter(|(_, d)| **d == 0).map(|(r, _)| *r).collect();
let mut out = Vec::with_capacity(repos.len());
while let Some(r) = q.pop_front() {
out.push(r);
if let Some(children) = adj.get(r) {
for &c in children {
let d = indeg.get_mut(c).unwrap();
*d -= 1;
if *d == 0 {
q.push_back(c);
}
}
}
}
if out.len() == repos.len() {
out
} else {
repos.to_vec()
}
}