use ratatui::{
layout::{Constraint, Layout, Rect},
style::{Color, Modifier, Style},
text::{Line, Span},
widgets::{Block, Clear, Paragraph},
Frame,
};
use super::app::{App, Page};
use super::dialog::DialogWidget;
use super::editor::EditorWidget;
use super::messages::MessagesWidget;
use super::sidebar::SidebarWidget;
use super::status_bar::StatusBarWidget;
use super::welcome::WelcomeWidget;
pub fn draw(f: &mut Frame, app: &App) {
let area = f.area();
f.render_widget(Clear, area);
f.render_widget(
Block::default().style(Style::default().bg(app.theme.bg)),
area,
);
match app.page {
Page::Welcome => draw_welcome(f, app),
Page::Chat => draw_chat(f, app),
}
if let Some(dialog) = &app.active_dialog {
let widget = DialogWidget {
dialog,
theme: &app.theme,
};
f.render_widget(widget, area);
}
}
fn draw_welcome(f: &mut Frame, app: &App) {
let area = f.area();
let version = env!("CARGO_PKG_VERSION");
let chunks = Layout::vertical([
Constraint::Min(10), Constraint::Length(1), ])
.split(area);
let welcome = WelcomeWidget { app };
f.render_widget(welcome, chunks[0]);
let status = StatusBarWidget {
cwd: &app.cwd,
version,
theme: &app.theme,
};
f.render_widget(status, chunks[1]);
}
fn draw_chat(f: &mut Frame, app: &App) {
let area = f.area();
let h_chunks =
Layout::horizontal([Constraint::Percentage(72), Constraint::Percentage(28)]).split(area);
let text_lines = app.editor.textarea.lines().len() as u16;
let has_attachments = !app.attachments.is_empty();
let editor_height = (text_lines + 5 + if has_attachments { 1 } else { 0 }).clamp(6, 12);
let left_chunks = Layout::vertical([
Constraint::Min(3), Constraint::Length(1), Constraint::Length(editor_height), ])
.split(h_chunks[0]);
let messages = MessagesWidget {
messages: &app.messages,
scroll_from_bottom: app.scroll_from_bottom,
theme: &app.theme,
streaming: app.streaming,
waiting_for_response: app.waiting_for_response,
_tick_counter: app.tick_counter,
_request_start: app.request_start,
link_registry: &app.link_registry,
};
f.render_widget(messages, left_chunks[0]);
let ma = left_chunks[0];
app.messages_area.set(ma);
{
use unicode_width::UnicodeWidthStr;
let mut cached = Vec::with_capacity(ma.height as usize);
for y in ma.y..ma.y + ma.height {
let mut line = String::new();
let mut x = ma.x;
while x < ma.x + ma.width {
if let Some(cell) = f.buffer_mut().cell((x, y)) {
let sym = cell.symbol();
line.push_str(sym);
let sym_w = UnicodeWidthStr::width(sym).max(1) as u16;
x += sym_w;
} else {
x += 1;
}
}
cached.push(line);
}
*app.rendered_lines.borrow_mut() = cached;
}
{
let mut rows = Vec::new();
let check_x = ma.x + 1; for y in ma.y..ma.y + ma.height {
if let Some(cell) = f.buffer_mut().cell((check_x, y)) {
if cell.symbol() == "│" && cell.fg == app.theme.input_border {
rows.push(y);
}
}
}
*app.user_msg_rows.borrow_mut() = rows;
}
{
use super::app::LinkRegion;
use unicode_width::UnicodeWidthStr;
let mut regions: Vec<LinkRegion> = Vec::new();
for y in ma.y..ma.y + ma.height {
let mut x = ma.x;
while x < ma.x + ma.width {
if let Some(cell) = f.buffer_mut().cell((x, y)) {
if cell.fg == app.theme.accent && cell.modifier.contains(Modifier::UNDERLINED) {
let x_start = x;
let mut text = String::new();
while x < ma.x + ma.width {
if let Some(c) = f.buffer_mut().cell((x, y)) {
if c.fg == app.theme.accent
&& c.modifier.contains(Modifier::UNDERLINED)
{
let sym = c.symbol();
text.push_str(sym);
let sym_w = UnicodeWidthStr::width(sym).max(1) as u16;
x += sym_w;
} else {
break;
}
} else {
break;
}
}
let text = text.trim().to_string();
if !text.is_empty() {
regions.push(LinkRegion {
y,
x_start,
x_end: x,
text,
});
}
continue;
}
}
let sym_w = f
.buffer_mut()
.cell((x, y))
.map(|c| UnicodeWidthStr::width(c.symbol()).max(1) as u16)
.unwrap_or(1);
x += sym_w;
}
}
*app.link_regions.borrow_mut() = regions;
}
if let Some(sel) = &app.selection {
let (start, end) =
if sel.start.1 < sel.end.1 || (sel.start.1 == sel.end.1 && sel.start.0 <= sel.end.0) {
(sel.start, sel.end)
} else {
(sel.end, sel.start)
};
for y in start.1..=end.1 {
if y < ma.y || y >= ma.y + ma.height {
continue;
}
let sx = if y == start.1 { start.0 } else { ma.x };
let ex = if y == end.1 {
end.0
} else {
ma.x + ma.width - 1
};
for x in sx..=ex {
if x >= ma.x && x < ma.x + ma.width {
if let Some(cell) = f.buffer_mut().cell_mut((x, y)) {
cell.set_style(
Style::default()
.bg(Color::Rgb(60, 60, 100))
.fg(Color::White),
);
}
}
}
}
}
if let Some((msg, _)) = &app.status_message {
let label = format!(" \u{2713} {} ", msg);
let toast_w = label.chars().count() as u16;
if ma.width > toast_w + 1 {
let toast_rect = Rect {
x: ma.x + ma.width - toast_w - 1,
y: ma.y,
width: toast_w,
height: 1,
};
let toast_line = Line::from(Span::styled(
label,
Style::default().fg(app.theme.bg).bg(app.theme.accent),
));
f.render_widget(Paragraph::new(toast_line), toast_rect);
}
}
if app.streaming {
let spinner = super::messages::SPINNER;
let frame = spinner[(app.tick_counter as usize) % spinner.len()];
let elapsed_str = app
.request_start
.map(|s| {
let d = s.elapsed();
let secs = d.as_secs();
if secs < 60 {
format!(" · {:.1}s", d.as_secs_f64())
} else {
format!(" · {}m {}s", secs / 60, secs % 60)
}
})
.unwrap_or_default();
let label = if app.waiting_for_response {
"Generating..."
} else {
"Generating"
};
let gen_line = Line::from(vec![
Span::styled(" ", Style::default()),
Span::styled(frame.to_string(), Style::default().fg(app.theme.accent)),
Span::styled(
format!(" {}{}", label, elapsed_str),
Style::default().fg(app.theme.muted),
),
]);
f.render_widget(Paragraph::new(gen_line), left_chunks[1]);
}
app.editor_area.set(left_chunks[2]);
let editor = EditorWidget {
state: &app.editor,
theme: &app.theme,
focused: app.active_dialog.is_none(),
scroll_offset: app.editor_scroll,
streaming: app.streaming,
attachments: &app.attachments,
attachment_selected: app.attachment_selected,
};
f.render_widget(editor, left_chunks[2]);
let sep_x = h_chunks[1].x;
for y in h_chunks[1].y..h_chunks[1].y + h_chunks[1].height {
if let Some(cell) = f.buffer_mut().cell_mut((sep_x, y)) {
cell.set_symbol("│");
cell.set_style(ratatui::style::Style::default().fg(app.theme.border));
}
}
let sidebar = SidebarWidget {
app,
theme: &app.theme,
};
let sidebar_area = ratatui::layout::Rect {
x: h_chunks[1].x + 2,
y: h_chunks[1].y,
width: h_chunks[1].width.saturating_sub(3),
height: h_chunks[1].height,
};
f.render_widget(sidebar, sidebar_area);
}