mod buffer;
mod code_action;
mod completion;
mod copilot_signin;
mod explorer;
mod fuzzy;
mod grammar_list;
mod hints;
mod hover;
mod lsp_status;
mod signature;
mod status;
mod toast;
use ratatui::Frame;
use ratatui::layout::{Constraint, Direction, Layout, Rect};
use ratatui::style::{Color, Style};
use ratatui::text::Span;
use ratatui::widgets::Paragraph;
use crate::app::{App, PaneId, PaneLayout, PaneRect, Prompt, SplitDir};
pub(crate) const PANEL_BG: Color = Color::Reset;
pub(crate) const PANEL_BORDER_FG: Color = Color::Gray;
pub fn draw(f: &mut Frame, app: &App) {
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Min(1),
Constraint::Length(1),
Constraint::Length(1),
])
.split(f.area());
let mut rects: std::collections::HashMap<PaneId, Rect> = std::collections::HashMap::new();
compute_pane_rects(&app.layout, chunks[0], &mut rects);
{
let mut out = app.last_pane_rects.borrow_mut();
out.clear();
for (id, r) in rects.iter() {
out.insert(
*id,
PaneRect {
x: r.x,
y: r.y,
width: r.width,
height: r.height,
},
);
}
}
let active_rect = rects.get(&app.active_pane).copied().unwrap_or(chunks[0]);
for (&id, &rect) in &rects {
if id == app.active_pane {
buffer::draw_buffer(f, app, rect);
} else if let Some(buf) = app.buffer_for_pane(id) {
let eff = effective_editor_for_buffer(app, buf);
buffer::draw_buffer_inactive(f, buf, &eff, rect);
}
}
draw_pane_borders(f, &app.layout, chunks[0], app.active_pane);
status::draw_status(f, app, chunks[1]);
status::draw_command_line(f, app, chunks[2]);
toast::draw_toast(f, app, chunks[0]);
buffer::place_cursor(f, app, active_rect);
if let Prompt::Command(cp) = &app.prompt.state {
hints::draw_command_hints(f, cp, chunks[2]);
}
if matches!(app.prompt.state, Prompt::Fuzzy(_)) {
fuzzy::draw_fuzzy(f, app, f.area());
}
if matches!(app.prompt.state, Prompt::Explorer(_)) {
explorer::draw_explorer(f, app, f.area());
}
if matches!(app.prompt.state, Prompt::CodeActionMenu { .. }) {
code_action::draw_code_action_menu(f, app, active_rect);
}
if matches!(app.prompt.state, Prompt::Hover { .. }) {
hover::draw_hover(f, app, active_rect);
}
if matches!(app.prompt.state, Prompt::LspStatus { .. }) {
lsp_status::draw_lsp_status(f, app, f.area());
}
if matches!(app.prompt.state, Prompt::CopilotSignin { .. }) {
copilot_signin::draw_copilot_signin(f, app, f.area());
}
if matches!(app.prompt.state, Prompt::GrammarList { .. }) {
grammar_list::draw_grammar_list(f, app, f.area());
}
if matches!(app.prompt.state, Prompt::GrammarInstallConfirm { .. }) {
grammar_list::draw_grammar_install_prompt(f, app, f.area());
}
if app.completion.is_some() {
completion::draw_completion(f, app, active_rect);
}
if app.signature.is_some() {
signature::draw_signature(f, app, active_rect);
}
if !app.prompt.is_open() {
hints::draw_pending_hints(f, app, chunks[1]);
}
}
fn compute_pane_rects(
node: &PaneLayout,
area: Rect,
out: &mut std::collections::HashMap<PaneId, Rect>,
) {
match node {
PaneLayout::Leaf(id) => {
out.insert(*id, area);
}
PaneLayout::Split {
dir,
children,
ratios,
} => {
let sum: f32 = ratios.iter().sum();
let denom: u32 = 10_000;
let mut numers: Vec<u32> = ratios
.iter()
.map(|r| {
if sum > 0.0 {
((r / sum) * denom as f32).round() as u32
} else {
denom / children.len() as u32
}
})
.collect();
let total: u32 = numers.iter().sum();
if let Some(last) = numers.last_mut()
&& total < denom
{
*last += denom - total;
}
let direction = match dir {
SplitDir::Vertical => Direction::Horizontal,
SplitDir::Horizontal => Direction::Vertical,
};
let divider_cells = children.len().saturating_sub(1) as u16;
let usable = match direction {
Direction::Horizontal => area.width.saturating_sub(divider_cells),
Direction::Vertical => area.height.saturating_sub(divider_cells),
};
let mut constraints: Vec<Constraint> = Vec::with_capacity(children.len() * 2);
for (i, &n) in numers.iter().enumerate() {
let pane_size = ((n * usable as u32) / denom) as u16;
constraints.push(Constraint::Length(pane_size));
if i + 1 < children.len() {
constraints.push(Constraint::Length(1));
}
}
let chunks = Layout::default()
.direction(direction)
.constraints(constraints)
.split(area);
for (i, child) in children.iter().enumerate() {
let chunk = chunks[i * 2];
compute_pane_rects(child, chunk, out);
}
}
}
}
fn effective_editor_for_buffer(
app: &App,
buf: &crate::editor::Buffer,
) -> crate::config::EditorConfig {
let base = app.config.editor;
let Some(path) = buf.path.as_ref() else {
return base;
};
let Some(ext) = path.extension().and_then(|e| e.to_str()) else {
return base;
};
let Some(lang) = app.config.languages.by_extension(ext) else {
return base;
};
base.overlay(&lang.editor)
}
fn draw_pane_borders(f: &mut Frame, node: &PaneLayout, area: Rect, active: PaneId) {
use ratatui::widgets::Block;
use ratatui::widgets::BorderType;
if let PaneLayout::Split {
dir,
children,
ratios,
} = node
{
let sum: f32 = ratios.iter().sum();
let denom: u32 = 10_000;
let mut numers: Vec<u32> = ratios
.iter()
.map(|r| {
if sum > 0.0 {
((r / sum) * denom as f32).round() as u32
} else {
denom / children.len() as u32
}
})
.collect();
let total: u32 = numers.iter().sum();
if let Some(last) = numers.last_mut()
&& total < denom
{
*last += denom - total;
}
let direction = match dir {
SplitDir::Vertical => Direction::Horizontal,
SplitDir::Horizontal => Direction::Vertical,
};
let divider_cells = children.len().saturating_sub(1) as u16;
let usable = match direction {
Direction::Horizontal => area.width.saturating_sub(divider_cells),
Direction::Vertical => area.height.saturating_sub(divider_cells),
};
let mut constraints: Vec<Constraint> = Vec::with_capacity(children.len() * 2);
for (i, &n) in numers.iter().enumerate() {
let pane_size = ((n * usable as u32) / denom) as u16;
constraints.push(Constraint::Length(pane_size));
if i + 1 < children.len() {
constraints.push(Constraint::Length(1));
}
}
let chunks = Layout::default()
.direction(direction)
.constraints(constraints)
.split(area);
for i in 0..(children.len().saturating_sub(1)) {
let divider = chunks[i * 2 + 1];
let glyph = match direction {
Direction::Horizontal => "│",
Direction::Vertical => "─",
};
let text: Vec<ratatui::text::Line> = match direction {
Direction::Horizontal => (0..divider.height)
.map(|_| {
ratatui::text::Line::from(Span::styled(
glyph,
Style::default().fg(Color::DarkGray),
))
})
.collect(),
Direction::Vertical => vec![ratatui::text::Line::from(Span::styled(
glyph.repeat(divider.width as usize),
Style::default().fg(Color::DarkGray),
))],
};
f.render_widget(Paragraph::new(text), divider);
}
for (i, child) in children.iter().enumerate() {
let chunk = chunks[i * 2];
draw_pane_borders(f, child, chunk, active);
}
} else if let PaneLayout::Leaf(id) = node
&& *id == active
{
let _ = Block::default().border_type(BorderType::Plain);
}
}