Contain the app UI inside of a component

This commit is contained in:
Savanni D'Gerinel 2024-05-06 01:38:22 -04:00
parent 8f4d424d1d
commit 54b34d81ec
3 changed files with 103 additions and 81 deletions

View File

@ -1,10 +1,9 @@
mod ui;
mod state; mod state;
mod types; mod types;
use crossterm::{ use ui::Canvas;
event::{self, KeyCode, KeyEvent, KeyModifiers}, use crossterm::event::{self, KeyCode, KeyEvent, KeyModifiers};
terminal::{disable_raw_mode, enable_raw_mode},
};
use state::AppState; use state::AppState;
use std::{ use std::{
env, io, env, io,
@ -12,80 +11,12 @@ use std::{
thread, thread,
time::{Duration, Instant}, time::{Duration, Instant},
}; };
use tui::{ use tui::{backend::CrosstermBackend, Terminal};
backend::CrosstermBackend,
layout::{Alignment, Constraint, Direction, Layout},
style::{Color, Style},
widgets::{Block, BorderType, Borders, Paragraph},
Terminal,
};
// const TITLE: &str = "Text Editor Challenge"; // const TITLE: &str = "Text Editor Challenge";
const COPYRIGHT: &str = "(c) Savanni D'Gerinel - all rights reserved"; // const COPYRIGHT: &str = "(c) Savanni D'Gerinel - all rights reserved";
const TICK_RATE_MS: u64 = 200; const TICK_RATE_MS: u64 = 200;
fn render<T>(app_state: &AppState, terminal: &mut Terminal<T>) -> Result<(), anyhow::Error>
where
T: tui::backend::Backend,
{
terminal.draw(|rect| {
let size = rect.size();
let chunks = Layout::default()
.direction(Direction::Vertical)
.margin(2)
.constraints(
[
Constraint::Length(3),
Constraint::Min(2),
Constraint::Length(3),
]
.as_ref(),
)
.split(size);
let title = Paragraph::new(
app_state
.path
.clone()
.map(|path| path.to_string_lossy().into_owned())
.unwrap_or("No file opened".to_owned()),
)
.style(Style::default().fg(Color::LightCyan))
.alignment(Alignment::Center)
.block(
Block::default()
.borders(Borders::ALL)
.style(Style::default().fg(Color::White))
.title("Copyright")
.border_type(BorderType::Plain),
);
rect.render_widget(title, chunks[0]);
let cp = Paragraph::new(COPYRIGHT)
.style(Style::default().fg(Color::LightCyan))
.alignment(Alignment::Center)
.block(
Block::default()
.borders(Borders::ALL)
.style(Style::default().fg(Color::White))
.title("Copyright")
.border_type(BorderType::Plain),
);
rect.render_widget(cp, chunks[2]);
// TODO: rework scrolling as soon as I have a working cursor
let contents = Paragraph::new(app_state.contents.contents()).scroll((0, 0));
rect.render_widget(contents, chunks[1]);
// TODO: keeping track of the index of the top row and subtract that from the cursor row.
let (row, column) = app_state.cursor.addr();
let row = row as u16;
let column = column as u16;
rect.set_cursor(chunks[1].x + column, chunks[1].y + row);
})?;
Ok(())
}
#[derive(Debug)] #[derive(Debug)]
enum Event<I> { enum Event<I> {
Input(I), Input(I),
@ -113,14 +44,14 @@ fn handle_input(tx: mpsc::Sender<Event<KeyEvent>>, tick_rate: Duration) {
fn app_loop<T>( fn app_loop<T>(
mut app_state: AppState, mut app_state: AppState,
terminal: &mut Terminal<T>, mut screen: Canvas<T>,
rx: mpsc::Receiver<Event<KeyEvent>>, rx: mpsc::Receiver<Event<KeyEvent>>,
) -> Result<(), anyhow::Error> ) -> Result<(), anyhow::Error>
where where
T: tui::backend::Backend, T: tui::backend::Backend,
{ {
loop { loop {
render(&app_state, terminal)?; screen.render(&app_state)?;
match rx.recv()? { match rx.recv()? {
Event::Input(event) Event::Input(event)
@ -152,17 +83,14 @@ fn main() -> Result<(), anyhow::Error> {
handle_input(tx, tick_rate); handle_input(tx, tick_rate);
}); });
enable_raw_mode()?;
let stdout = io::stdout(); let stdout = io::stdout();
let backend = CrosstermBackend::new(stdout); let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?; let mut terminal = Terminal::new(backend)?;
terminal.clear()?; terminal.clear()?;
let result = app_loop(app_state, &mut terminal, rx); let screen = Canvas::new(terminal);
disable_raw_mode()?; let result = app_loop(app_state, screen, rx);
terminal.show_cursor()?;
result?; result?;

View File

@ -5,6 +5,9 @@ pub struct Document {
impl Document { impl Document {
pub fn new(contents: Vec<String>) -> Self { pub fn new(contents: Vec<String>) -> Self {
if contents.len() > (u16::MAX.into()) {
panic!("Document row count exceeds u16::MAX. The current scrolling code cannot handle that.");
}
Self{ Self{
rows: contents rows: contents
} }

View File

@ -0,0 +1,91 @@
use crossterm::terminal::{disable_raw_mode, enable_raw_mode};
use tui::{layout::{Alignment, Constraint, Direction, Layout}, style::{Color, Style}, widgets::{Block, BorderType, Borders, Paragraph}, Terminal};
use crate::state::AppState;
pub struct Canvas<T: tui::backend::Backend> {
top_row: usize,
terminal: Terminal<T>,
}
impl<T: tui::backend::Backend> Canvas<T> {
pub fn new(terminal: Terminal<T>) -> Self {
enable_raw_mode().unwrap();
Self {
top_row: 0,
terminal
}
}
pub fn render(&mut self, app_state: &AppState) -> Result<(), anyhow::Error>
{
self.terminal.draw(|rect| {
let size = rect.size();
let chunks = Layout::default()
.direction(Direction::Vertical)
.margin(2)
.constraints(
[
Constraint::Min(2),
Constraint::Length(3),
// Constraint::Length(3),
]
.as_ref(),
)
.split(size);
let title = Paragraph::new(
app_state
.path
.clone()
.map(|path| path.to_string_lossy().into_owned())
.unwrap_or("No file opened".to_owned()),
)
.style(Style::default().fg(Color::LightCyan))
.alignment(Alignment::Center)
.block(
Block::default()
.borders(Borders::ALL)
.style(Style::default().fg(Color::White))
.border_type(BorderType::Plain),
);
rect.render_widget(title, chunks[1]);
/*
let cp = Paragraph::new(COPYRIGHT)
.style(Style::default().fg(Color::LightCyan))
.alignment(Alignment::Center)
.block(
Block::default()
.borders(Borders::ALL)
.style(Style::default().fg(Color::White))
.title("Copyright")
.border_type(BorderType::Plain),
);
rect.render_widget(cp, chunks[2]);
*/
let (row, column) = app_state.cursor.addr();
if row == self.top_row && row >= 1 {
self.top_row -= 1;
} else if row - self.top_row == (chunks[0].height - 1).into() {
self.top_row += 1;
}
let contents = Paragraph::new(app_state.contents.contents()).scroll((self.top_row as u16, 0));
rect.render_widget(contents, chunks[0]);
rect.set_cursor(chunks[0].x + column as u16, chunks[0].y + (row - self.top_row) as u16);
})?;
Ok(())
}
}
impl<T: tui::backend::Backend> Drop for Canvas<T> {
fn drop(&mut self) {
let _ = disable_raw_mode();
let _ = self.terminal.show_cursor();
}
}