1use crate::app::App;
2use ratatui::{
3 layout::{Constraint, Direction, Layout},
4 style::{Modifier, Style},
5 text::{Line, Span},
6 widgets::{Block, Borders, List, ListItem, Paragraph},
7 Frame,
8};
9
10pub fn render(frame: &mut Frame, app: &App) {
11 let area = frame.area();
12 let theme = &app.tui_theme;
13
14 let chunks = Layout::default()
16 .direction(Direction::Vertical)
17 .constraints([
18 Constraint::Min(0), Constraint::Length(1), ])
21 .split(area);
22
23 let graph_area = chunks[0];
24 let footer_area = chunks[1];
25
26 let block = Block::default()
27 .title("Dependency Graph")
28 .borders(Borders::ALL)
29 .border_style(Style::default().fg(theme.border))
30 .title_style(
31 Style::default()
32 .fg(theme.primary)
33 .add_modifier(Modifier::BOLD),
34 );
35
36 let inner = block.inner(graph_area);
37 frame.render_widget(block, graph_area);
38
39 if app.features.is_empty() {
40 return;
41 }
42
43 let mut items = Vec::new();
45
46 for feature in &app.features {
47 let status_color = if feature.passes {
49 theme.done
50 } else if feature.in_progress {
51 theme.in_progress
52 } else {
53 theme.pending
54 };
55
56 let icon = if feature.passes {
58 "✓"
59 } else if feature.in_progress {
60 "◉"
61 } else {
62 "○"
63 };
64
65 let is_blocked = !feature.dependencies.is_empty()
67 && feature.dependencies.iter().any(|dep_id| {
68 app.features
69 .iter()
70 .find(|f| f.id == *dep_id)
71 .is_none_or(|f| !f.passes)
72 });
73
74 let border_color = if is_blocked {
75 theme.blocked
76 } else {
77 status_color
78 };
79
80 items.push(ListItem::new(vec![
82 Line::from(vec![
83 Span::styled("┌─", Style::default().fg(border_color)),
84 Span::styled(
85 format!(" {} {} ", icon, feature.name),
86 Style::default()
87 .fg(status_color)
88 .add_modifier(Modifier::BOLD),
89 ),
90 Span::styled("─┐", Style::default().fg(border_color)),
91 ]),
92 Line::from(vec![
93 Span::styled("│ ", Style::default().fg(border_color)),
94 Span::styled(&feature.category, Style::default().fg(theme.secondary)),
95 Span::styled(" │", Style::default().fg(border_color)),
96 ]),
97 Line::from(vec![Span::styled(
98 "└──────────────────┘",
99 Style::default().fg(border_color),
100 )]),
101 ]));
102
103 if !feature.dependencies.is_empty() {
105 for dep_id in &feature.dependencies {
106 if let Some(dep) = app.features.iter().find(|f| f.id == *dep_id) {
107 let dep_status = if dep.passes {
108 "✓"
109 } else if dep.in_progress {
110 "◉"
111 } else {
112 "○"
113 };
114
115 items.push(ListItem::new(Line::from(vec![
116 Span::styled(" ↓ ", Style::default().fg(theme.accent)),
117 Span::styled(
118 format!("{dep_status} depends on: "),
119 Style::default().fg(theme.muted),
120 ),
121 Span::styled(&dep.name, Style::default().fg(theme.foreground)),
122 ])));
123 }
124 }
125 }
126
127 items.push(ListItem::new(Line::from("")));
128 }
129
130 let list = List::new(items);
131 frame.render_widget(list, inner);
132
133 let footer_text = if let Some(status) = app.get_status_message() {
135 status.to_string()
136 } else {
137 " 1-4:views r:refresh t:theme ?:help q:quit".to_string()
138 };
139
140 let footer = Paragraph::new(footer_text).style(Style::default().fg(theme.muted));
141 frame.render_widget(footer, footer_area);
142}