pub mod text;
pub mod widgets;
use ratatui::Frame;
use ratatui::layout::{Constraint, Layout, Rect};
use crate::app::App;
use crate::editor::buffer::Buffer;
use crate::editor::selection::Range;
use crate::ui::widgets::{
CommandBar, EditorView, FileTree, Popup, SearchBox, StatusBar, Tab, TabBar, editor_view,
};
#[derive(Debug, Clone, Copy)]
pub struct Regions {
pub tabs: Option<Rect>,
pub tree: Option<Rect>,
pub editor: Rect,
pub status: Rect,
pub command: Rect,
}
impl Regions {
#[must_use]
pub fn split(area: Rect, show_tabs: bool, show_tree: bool) -> Self {
let [tabs, body, status, command] = Layout::vertical([
Constraint::Length(u16::from(show_tabs)),
Constraint::Min(1),
Constraint::Length(1),
Constraint::Length(1),
])
.areas(area);
let tree_width = if show_tree {
(body.width / 4)
.clamp(16, 40)
.min(body.width.saturating_sub(20))
} else {
0
};
let [tree, editor] =
Layout::horizontal([Constraint::Length(tree_width), Constraint::Min(1)]).areas(body);
Self {
tabs: show_tabs.then_some(tabs),
tree: (tree_width > 0).then_some(tree),
editor,
status,
command,
}
}
}
pub fn draw(frame: &mut Frame, app: &mut App) {
let show_tabs = app.config.show_tabs && app.buffers.len() > 1;
let regions = Regions::split(frame.area(), show_tabs, app.tree_visible);
app.viewport_height = regions.editor.height;
{
let App {
buffers,
active,
config,
..
} = &mut *app;
let buffer = &mut buffers[*active];
editor_view::scroll_into_view(buffer, config, regions.editor);
if config.syntax_highlighting {
let last = buffer.view.top_line + usize::from(regions.editor.height);
let Buffer {
document, syntax, ..
} = buffer;
syntax.ensure(document, last);
}
}
let editor = EditorView {
buffer: app.buffer(),
theme: &app.theme,
config: &app.config,
selection: selection_range(app),
search: app.search.is_active().then_some(&app.search),
active_match: active_match(app),
};
let editor_caret = editor.caret_position(regions.editor);
frame.render_widget(editor, regions.editor);
if let Some(area) = regions.tabs {
let tabs: Vec<Tab<'_>> = app
.buffers
.iter()
.map(|buffer| Tab {
name: buffer.document.display_name(),
dirty: buffer.document.is_dirty(),
})
.collect();
frame.render_widget(
TabBar {
tabs: &tabs,
active: app.active,
theme: &app.theme,
},
area,
);
}
if let (Some(area), Some(tree)) = (regions.tree, app.tree.as_ref()) {
let title = tree
.root()
.file_name()
.and_then(std::ffi::OsStr::to_str)
.unwrap_or("files");
frame.render_widget(
FileTree {
entries: tree.entries(),
selected: app.tree_selected,
focused: app.mode == crate::app::mode::Mode::Tree,
title,
theme: &app.theme,
},
area,
);
}
frame.render_widget(status_bar(app), regions.status);
let prompt_caret = if app.mode.is_search() {
let search_box = SearchBox {
query: &app.search.query,
forward: app.search.forward,
error: app.search.error(),
theme: &app.theme,
};
let caret = search_box.caret_position(regions.command);
frame.render_widget(search_box, regions.command);
Some(caret)
} else {
let command_bar = CommandBar {
command: app.mode.is_command().then_some(app.command_line.as_str()),
status: &app.status,
theme: &app.theme,
};
let caret = command_bar.caret_position(regions.command);
frame.render_widget(command_bar, regions.command);
caret
};
if let Some((title, body)) = app.popup.as_ref() {
frame.render_widget(
Popup {
title,
body,
hint: "press any key",
theme: &app.theme,
},
frame.area(),
);
return;
}
if let Some(position) = prompt_caret.or(editor_caret) {
frame.set_cursor_position(position);
}
}
fn active_match(app: &App) -> Option<Range> {
if !app.search.is_active() {
return None;
}
let buffer = app.buffer();
let head = buffer.document.pos_to_char(buffer.cursor().head);
let line = buffer.cursor().head.line;
let line_start = buffer.document.line_start(line);
app.search
.matches_in_line(&buffer.document.line_string(line))
.into_iter()
.map(|found| Range {
start: line_start + found.start,
end: line_start + found.end,
})
.find(|range| range.contains(head))
}
fn selection_range(app: &App) -> Option<Range> {
let buffer = app.buffer();
let cursor = buffer.cursor();
match app.mode {
crate::app::mode::Mode::Visual => Some(Range::of(&cursor, &buffer.document)),
crate::app::mode::Mode::VisualLine => Some(Range::of_lines(&cursor, &buffer.document)),
_ => None,
}
}
fn status_bar(app: &App) -> StatusBar<'_> {
let buffer = app.buffer();
StatusBar {
mode: app.mode,
name: buffer.document.display_name(),
dirty: buffer.document.is_dirty(),
language: buffer.syntax.language_name(),
position: buffer.cursor().head,
line_count: buffer.document.len_lines(),
line_ending: buffer.document.line_ending(),
cursor_count: buffer.cursors().len(),
theme: &app.theme,
}
}