Lines
71.15 %
Functions
38.1 %
Branches
100 %
//! Ratatui rendering. The draw layer is a pure function of
//! [`crate::app::App`]: take a `Frame` and an `App`, render the current
//! visual state. No I/O of its own; I/O is the transport's job.
use crate::app::{App, Tab};
use crate::modal::{self, Modal};
use crate::tabs;
use crate::widgets::{EditMode, VimMode};
use ratatui::Frame;
use ratatui::layout::{Constraint, Direction, Layout, Rect};
use ratatui::style::{Color, Modifier, Style};
use ratatui::text::{Line, Span};
use ratatui::widgets::{Block, Borders, Clear, Paragraph, Tabs};
pub fn draw(frame: &mut Frame, app: &App) {
let area = frame.area();
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(3),
Constraint::Min(1),
])
.split(area);
draw_tabs(frame, chunks[0], app);
draw_body(frame, chunks[1], app);
draw_status(frame, chunks[2], app);
if let Some(modal) = app.modals.top() {
draw_modal(frame, area, modal);
}
fn draw_tabs(frame: &mut Frame, area: Rect, app: &App) {
let titles: Vec<Line> = Tab::ALL.iter().map(|t| Line::from(t.label())).collect();
let selected = Tab::ALL
.iter()
.position(|t| *t == app.active_tab)
.unwrap_or(0);
let tabs = Tabs::new(titles)
.select(selected)
.block(Block::default().borders(Borders::ALL).title("nomisync-tui"))
.highlight_style(Style::default().add_modifier(Modifier::REVERSED));
frame.render_widget(tabs, area);
fn draw_body(frame: &mut Frame, area: Rect, app: &App) {
let body = tabs::placeholder_body(app.active_tab);
let widget = Paragraph::new(body).block(
Block::default()
.borders(Borders::ALL)
.title(app.active_tab.label()),
);
frame.render_widget(widget, area);
fn draw_status(frame: &mut Frame, area: Rect, app: &App) {
let edit_indicator = match app.command_line.mode() {
EditMode::Emacs => "emacs",
EditMode::Vim => match app.command_line.vim_mode() {
VimMode::Normal => "vim:normal",
VimMode::Insert => "vim:insert",
},
};
let content = if app.command_line_active {
format!(
":{} (cursor={})",
app.command_line.buffer(),
app.command_line.cursor()
)
} else if app.status.is_empty() {
format!("[{edit_indicator}] Tab/BTab tabs : cmdline ? help C-v edit-mode q quit")
} else {
format!("[{edit_indicator}] {}", app.status)
let line = Span::styled(content, Style::default().fg(Color::Gray));
let para = Paragraph::new(line).block(Block::default().borders(Borders::ALL));
frame.render_widget(para, area);
fn draw_modal(frame: &mut Frame, full: Rect, modal: &Modal) {
let area = centered_rect(60, 30, full);
frame.render_widget(Clear, area);
let (title, body) = modal_content(modal);
let widget = Paragraph::new(body).block(Block::default().title(title).borders(Borders::ALL));
fn modal_content(modal: &Modal) -> (&'static str, String) {
match modal {
Modal::Help => (
"Help",
"Keys:\n 1-5 jump to tab\n Tab next tab\n : command palette\n\
\n ? this help\n q/Esc close modal\n C-v toggle emacs/vim mode"
.to_string(),
),
Modal::ConfigSet(m) => {
let focus_marker = |field| {
if m.focus == field { "▸" } else { " " }
let body = format!(
"{} name: [{}]\n{} value: [{}]\n\nEnter to save, Esc to cancel.",
focus_marker(modal::ConfigSetField::Name),
m.name.buffer(),
focus_marker(modal::ConfigSetField::Value),
m.value.buffer(),
("Set config", body)
#[must_use]
pub fn centered_rect(pct_x: u16, pct_y: u16, r: Rect) -> Rect {
let popup_layout = Layout::default()
Constraint::Percentage((100 - pct_y) / 2),
Constraint::Percentage(pct_y),
.split(r);
Layout::default()
.direction(Direction::Horizontal)
Constraint::Percentage((100 - pct_x) / 2),
Constraint::Percentage(pct_x),
.split(popup_layout[1])[1]
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn centered_rect_clamps_to_parent() {
let parent = Rect::new(0, 0, 100, 100);
let r = centered_rect(60, 30, parent);
assert!(r.x + r.width <= parent.x + parent.width);
assert!(r.y + r.height <= parent.y + parent.height);