use ratatui::{
Frame,
layout::{Constraint, Flex, Layout, Rect},
style::{Modifier, Style},
text::{Line, Span},
widgets::{Block, Borders, Clear, Paragraph, Wrap},
};
use crate::app::App;
use crate::model::{CommentType, LineRange};
use crate::theme::Theme;
use crate::ui::styles;
pub fn render_comment_input(frame: &mut Frame, app: &App) {
let theme = &app.theme;
let area = centered_rect(60, 40, frame.area());
frame.render_widget(Clear, area);
let action = if app.editing_comment_id.is_some() {
"Edit"
} else {
"Add"
};
let comment_kind = if app.comment_is_file_level {
"File Comment".to_string()
} else if let Some((range, _)) = app.comment_line_range {
if range.is_single() {
format!("Line {} Comment", range.start)
} else {
format!("Lines {}-{} Comment", range.start, range.end)
}
} else {
match app.comment_line {
Some((line, _)) => format!("Line {line} Comment"),
None => "Line Comment".to_string(),
}
};
let newline_hint = if app.supports_keyboard_enhancement {
"Shift-Enter"
} else {
"Ctrl-J"
};
let block = Block::default()
.title(format!(
" {} {} [{}] (Enter to save, {} for newline) ",
action,
comment_kind,
app.comment_type.as_str(),
newline_hint
))
.borders(Borders::ALL)
.border_style(styles::border_style(theme, true));
let inner = block.inner(area);
frame.render_widget(block, area);
let type_style = styles::comment_type_style(theme, app.comment_type);
let type_hint = Line::from(vec![
Span::styled("Type: ", styles::dim_style(theme)),
Span::styled(app.comment_type.as_str(), type_style),
Span::styled(" (Tab to cycle)", styles::dim_style(theme)),
]);
let separator = Line::from(Span::styled(
"─".repeat(inner.width as usize),
styles::dim_style(theme),
));
let mut lines = vec![type_hint, separator, Line::from("")];
let cursor_style = Style::default()
.fg(theme.cursor_color)
.add_modifier(Modifier::UNDERLINED);
if app.comment_buffer.is_empty() {
lines.push(Line::from(vec![
Span::styled(" ", cursor_style),
Span::styled("Type your comment...", styles::dim_style(theme)),
]));
} else {
let buffer_lines: Vec<&str> = app.comment_buffer.split('\n').collect();
let mut char_offset = 0;
for (line_idx, text) in buffer_lines.iter().enumerate() {
let line_start = char_offset;
let line_end = char_offset + text.len();
let cursor_on_this_line = app.comment_cursor >= line_start
&& (app.comment_cursor <= line_end
|| (line_idx == buffer_lines.len() - 1
&& app.comment_cursor == app.comment_buffer.len()));
if cursor_on_this_line {
let cursor_pos_in_line = app.comment_cursor - line_start;
let cursor_pos_in_line = cursor_pos_in_line.min(text.len());
let (before_cursor, after_cursor) = text.split_at(cursor_pos_in_line);
if after_cursor.is_empty() {
lines.push(Line::from(vec![
Span::raw(before_cursor.to_string()),
Span::styled(" ", cursor_style),
]));
} else {
let mut chars = after_cursor.chars();
let cursor_char = chars.next().unwrap();
let remaining = chars.as_str();
lines.push(Line::from(vec![
Span::raw(before_cursor.to_string()),
Span::styled(cursor_char.to_string(), cursor_style),
Span::raw(remaining.to_string()),
]));
}
} else {
lines.push(Line::from(Span::raw(text.to_string())));
}
char_offset = line_end + 1;
}
}
let paragraph = Paragraph::new(lines).wrap(Wrap { trim: false });
frame.render_widget(paragraph, inner);
}
pub fn format_comment_lines(
theme: &Theme,
comment_type: CommentType,
content: &str,
line_range: Option<LineRange>,
) -> Vec<Line<'static>> {
let type_style = styles::comment_type_style(theme, comment_type);
let border_style = styles::comment_border_style(theme, comment_type);
let line_info = match line_range {
Some(range) if range.is_single() => format!("L{} ", range.start),
Some(range) => format!("L{}-L{} ", range.start, range.end),
None => String::new(),
};
let content_lines: Vec<&str> = content.split('\n').collect();
let mut result = Vec::new();
result.push(Line::from(vec![
Span::styled(" ╭─ ", border_style),
Span::styled(format!("[{}] ", comment_type.as_str()), type_style),
Span::styled(line_info, styles::dim_style(theme)),
Span::styled("─".repeat(30), border_style),
]));
for line in &content_lines {
result.push(Line::from(vec![
Span::styled(" │ ", border_style),
Span::raw(line.to_string()),
]));
}
result.push(Line::from(vec![Span::styled(
" ╰".to_string() + &"─".repeat(38),
border_style,
)]));
result
}
pub fn render_confirm_dialog(frame: &mut Frame, app: &App, message: &str) {
let theme = &app.theme;
let area = centered_rect(50, 20, frame.area());
frame.render_widget(Clear, area);
let block = Block::default()
.title(" Confirm ")
.borders(Borders::ALL)
.border_style(styles::border_style(theme, true));
let inner = block.inner(area);
frame.render_widget(block, area);
let lines = vec![
Line::from(""),
Line::from(Span::raw(message)),
Line::from(""),
Line::from(vec![
Span::styled(" [Y]", Style::default().add_modifier(Modifier::BOLD)),
Span::raw("es "),
Span::styled("[N]", Style::default().add_modifier(Modifier::BOLD)),
Span::raw("o"),
]),
];
let paragraph = Paragraph::new(lines).alignment(ratatui::layout::Alignment::Center);
frame.render_widget(paragraph, inner);
}
fn centered_rect(percent_x: u16, percent_y: u16, area: Rect) -> Rect {
let vertical = Layout::vertical([Constraint::Percentage(percent_y)]).flex(Flex::Center);
let horizontal = Layout::horizontal([Constraint::Percentage(percent_x)]).flex(Flex::Center);
let [area] = vertical.areas(area);
let [area] = horizontal.areas(area);
area
}