led/src/term_ui/mod.rs
Nathan Vegdahl 19adb08170 Reworking the text formatting code.
It's a mess of indirection and over-abstraction, and this commit
is the first step at cleaning that up.  In addition to making the
code easier to follow, it's also notably faster.

The only downside is we've (temporarily) lost indentation
continuation on line wrapping.  But that can be added back without
too much trouble later.
2020-02-11 20:24:34 +09:00

650 lines
21 KiB
Rust

#![allow(dead_code)]
pub mod formatter;
mod screen;
pub mod smallstring;
use std::time::Duration;
use crossterm::{
event::{Event, KeyCode, KeyEvent, KeyModifiers},
style::Color,
};
use crate::{
editor::Editor,
formatter::{block_count, block_index_and_range, char_range_from_block_index, LineFormatter},
string_utils::{is_line_ending, line_ending_to_str, LineEnding},
utils::{digit_count, RopeGraphemes, Timer},
};
use self::{
formatter::ConsoleLineFormatter,
screen::{Screen, Style},
};
const EMPTY_MOD: KeyModifiers = KeyModifiers::empty();
const UPDATE_TICK_MS: u64 = 10;
// Color theme.
// Styles are (FG, BG).
const STYLE_MAIN: Style = Style(
Color::Rgb {
r: 0xD0,
g: 0xD0,
b: 0xD0,
},
Color::Rgb {
r: 0x28,
g: 0x28,
b: 0x28,
},
);
const STYLE_CURSOR: Style = Style(
Color::Rgb {
r: 0x00,
g: 0x00,
b: 0x00,
},
Color::Rgb {
r: 0xD0,
g: 0xD0,
b: 0xD0,
},
);
const STYLE_GUTTER: Style = Style(
Color::Rgb {
r: 0x70,
g: 0x70,
b: 0x70,
},
Color::Rgb {
r: 0x22,
g: 0x22,
b: 0x22,
},
);
const STYLE_INFO: Style = Style(
Color::Rgb {
r: 0xC0,
g: 0xC0,
b: 0xC0,
},
Color::Rgb {
r: 0x14,
g: 0x14,
b: 0x14,
},
);
/// Generalized ui loop.
macro_rules! ui_loop {
($term_ui:ident,draw $draw:block,key_press($key:ident) $key_press:block) => {
let mut stop = false;
let mut timer = Timer::new();
// Draw the editor to screen for the first time
{
$draw
};
$term_ui.screen.present();
// UI loop
loop {
let mut should_redraw = false;
// Handle input.
// Doing this as a polled loop isn't necessary in the current
// implementation, but it will be useful in the future when we may
// want to re-draw on e.g. async syntax highlighting updates, or
// update based on a file being modified outside our process.
loop {
if crossterm::event::poll(Duration::from_millis(UPDATE_TICK_MS)).unwrap() {
match crossterm::event::read().unwrap() {
Event::Key($key) => {
let (status, state_changed) = || -> (LoopStatus, bool) { $key_press }();
should_redraw |= state_changed;
if status == LoopStatus::Done {
stop = true;
break;
}
}
Event::Mouse(_) => {
break;
}
Event::Resize(w, h) => {
$term_ui.width = w as usize;
$term_ui.height = h as usize;
$term_ui.screen.resize(w as usize, h as usize);
should_redraw = true;
break;
}
}
// If too much time has passed since the last redraw,
// break so we can draw if needed. This keeps an onslaught
// of input (e.g. when pasting a large piece of text) from
// visually freezing the UI.
if timer.elapsed() >= UPDATE_TICK_MS {
timer.tick();
break;
}
} else {
break;
}
}
// Check if we're done
if stop || $term_ui.quit {
break;
}
// Draw the editor to screen
if should_redraw {
// Make sure display dimensions are up-to-date.
$term_ui
.editor
.update_dim($term_ui.height - 1, $term_ui.width);
$term_ui
.editor
.formatter
.set_wrap_width($term_ui.editor.view_dim.1);
// Draw!
{
$draw
};
$term_ui.screen.present();
}
}
};
}
pub struct TermUI {
screen: Screen,
editor: Editor<ConsoleLineFormatter>,
width: usize,
height: usize,
quit: bool,
}
#[derive(Debug, Copy, Clone, PartialEq)]
enum LoopStatus {
Done,
Continue,
}
impl TermUI {
pub fn new() -> TermUI {
TermUI::new_from_editor(Editor::new(ConsoleLineFormatter::new(4)))
}
pub fn new_from_editor(ed: Editor<ConsoleLineFormatter>) -> TermUI {
let (w, h) = crossterm::terminal::size().unwrap();
let mut editor = ed;
editor.update_dim(h as usize - 1, w as usize);
TermUI {
screen: Screen::new(),
editor: editor,
width: w as usize,
height: h as usize,
quit: false,
}
}
pub fn main_ui_loop(&mut self) {
// Hide cursor
self.screen.hide_cursor();
// Set terminal size info
let (w, h) = crossterm::terminal::size().unwrap();
self.width = w as usize;
self.height = h as usize;
self.editor.update_dim(self.height - 1, self.width);
self.editor.formatter.set_wrap_width(self.editor.view_dim.1);
self.screen.resize(w as usize, h as usize);
// Start the UI
ui_loop!(
self,
// Draw
draw {
self.screen.clear(STYLE_MAIN.1);
self.draw_editor(&self.editor, (0, 0), (self.height - 1, self.width - 1));
},
// Handle input
key_press(key) {
let mut state_changed = true;
match key {
KeyEvent {
code: KeyCode::Char('q'),
modifiers: KeyModifiers::CONTROL,
} => {
self.quit = true;
return (LoopStatus::Done, true);
}
KeyEvent {
code: KeyCode::Char('s'),
modifiers: KeyModifiers::CONTROL,
} => {
self.editor.save_if_dirty();
}
KeyEvent {
code: KeyCode::Char('z'),
modifiers: KeyModifiers::CONTROL,
} => {
self.editor.undo();
}
KeyEvent {
code: KeyCode::Char('y'),
modifiers: KeyModifiers::CONTROL,
} => {
self.editor.redo();
}
KeyEvent {
code: KeyCode::Char('l'),
modifiers: KeyModifiers::CONTROL,
} => {
self.go_to_line_ui_loop();
}
KeyEvent {
code: KeyCode::PageUp,
modifiers: EMPTY_MOD,
} => {
self.editor.page_up();
}
KeyEvent {
code: KeyCode::PageDown,
modifiers: EMPTY_MOD,
} => {
self.editor.page_down();
}
KeyEvent {
code: KeyCode::Up,
modifiers: EMPTY_MOD,
} => {
self.editor.cursor_up(1);
}
KeyEvent {
code: KeyCode::Down,
modifiers: EMPTY_MOD,
} => {
self.editor.cursor_down(1);
}
KeyEvent {
code: KeyCode::Left,
modifiers: EMPTY_MOD,
} => {
self.editor.cursor_left(1);
}
KeyEvent {
code: KeyCode::Right,
modifiers: EMPTY_MOD,
} => {
self.editor.cursor_right(1);
}
KeyEvent {
code: KeyCode::Enter,
modifiers: EMPTY_MOD,
} => {
let nl = line_ending_to_str(self.editor.line_ending_type);
self.editor.insert_text_at_cursor(nl);
}
KeyEvent {
code: KeyCode::Tab,
modifiers: EMPTY_MOD,
} => {
self.editor.insert_tab_at_cursor();
}
KeyEvent {
code: KeyCode::Backspace,
modifiers: EMPTY_MOD,
} => {
self.editor.backspace_at_cursor();
}
KeyEvent {
code: KeyCode::Delete,
modifiers: EMPTY_MOD,
} => {
self.editor.remove_text_in_front_of_cursor(1);
}
// Character
KeyEvent {
code: KeyCode::Char(c),
modifiers: EMPTY_MOD,
} => {
self.editor.insert_text_at_cursor(&c.to_string()[..]);
}
_ => {
state_changed = false;
}
}
(LoopStatus::Continue, state_changed)
}
);
}
fn go_to_line_ui_loop(&mut self) {
let mut cancel = false;
let prefix = "Jump to line: ";
let mut line = String::new();
ui_loop!(
self,
// Draw
draw {
self.screen.clear(STYLE_MAIN.1);
self.draw_editor(&self.editor, (0, 0), (self.height - 1, self.width - 1));
for i in 0..self.width {
self.screen.draw(i, 0, " ", STYLE_INFO);
}
self.screen.draw(1, 0, prefix, STYLE_INFO);
self.screen.draw(
prefix.len() + 1,
0,
&line[..],
STYLE_INFO,
);
self.screen.set_cursor(prefix.len() + 1, 0);
},
// Handle input
key_press(key) {
let mut state_changed = true;
match key {
KeyEvent {
code: KeyCode::Char('q'),
modifiers: KeyModifiers::CONTROL,
} => {
self.quit = true;
return (LoopStatus::Done, true);
}
KeyEvent {
code: KeyCode::Esc,
modifiers: EMPTY_MOD,
} => {
cancel = true;
return (LoopStatus::Done, true);
}
KeyEvent {
code: KeyCode::Enter,
modifiers: EMPTY_MOD,
} => {
return (LoopStatus::Done, true);
}
KeyEvent {
code: KeyCode::Backspace,
modifiers: EMPTY_MOD,
} => {
line.pop();
}
// Character
KeyEvent {
code: KeyCode::Char(c),
modifiers: EMPTY_MOD,
} => {
if c.is_numeric() {
line.push(c);
}
}
_ => {
state_changed = false;
}
}
return (LoopStatus::Continue, state_changed);
}
);
// Jump to line!
if !cancel {
if let Ok(n) = line.parse::<usize>() {
self.editor.jump_to_line(n.saturating_sub(1));
}
}
}
fn draw_editor(
&self,
editor: &Editor<ConsoleLineFormatter>,
c1: (usize, usize),
c2: (usize, usize),
) {
// Fill in top row with info line color
for i in c1.1..(c2.1 + 1) {
self.screen.draw(i, c1.0, " ", STYLE_INFO);
}
// Filename and dirty marker
let filename = editor.file_path.display();
let dirty_char = if editor.dirty { "*" } else { "" };
let name = format!("{}{}", filename, dirty_char);
self.screen.draw(c1.1 + 1, c1.0, &name[..], STYLE_INFO);
// Percentage position in document
// TODO: use view instead of cursor for calculation if there is more
// than one cursor.
let percentage: usize = if editor.buffer.char_count() > 0 {
(((editor.cursors[0].range.0 as f32) / (editor.buffer.char_count() as f32)) * 100.0)
as usize
} else {
100
};
let pstring = format!("{}%", percentage);
self.screen.draw(
c2.1.saturating_sub(pstring.len()),
c1.0,
&pstring[..],
STYLE_INFO,
);
// Text encoding info and tab style
let nl = match editor.line_ending_type {
LineEnding::None => "None",
LineEnding::CRLF => "CRLF",
LineEnding::LF => "LF",
LineEnding::VT => "VT",
LineEnding::FF => "FF",
LineEnding::CR => "CR",
LineEnding::NEL => "NEL",
LineEnding::LS => "LS",
LineEnding::PS => "PS",
};
let soft_tabs_str = if editor.soft_tabs { "spaces" } else { "tabs" };
let info_line = format!(
"UTF8:{} {}:{}",
nl, soft_tabs_str, editor.soft_tab_width as usize
);
self.screen
.draw(c2.1.saturating_sub(30), c1.0, &info_line[..], STYLE_INFO);
// Draw main text editing area
self.draw_editor_text(editor, (c1.0 + 1, c1.1), c2);
}
fn draw_editor_text(
&self,
editor: &Editor<ConsoleLineFormatter>,
c1: (usize, usize),
c2: (usize, usize),
) {
// Calculate all the starting info
let gutter_width = editor.editor_dim.1 - editor.view_dim.1;
let (line_index, col_i) = editor.buffer.index_to_line_col(editor.view_pos.0);
let temp_line = editor.buffer.get_line(line_index);
let (mut line_block_index, block_range) = block_index_and_range(&temp_line, col_i);
let mut char_index = editor.buffer.line_col_to_index((line_index, block_range.0));
let (vis_line_offset, _) = editor.formatter.index_to_v2d(
RopeGraphemes::new(&temp_line.slice(block_range.0..block_range.1)),
editor.view_pos.0 - char_index,
);
let mut screen_line = c1.0 as isize - vis_line_offset as isize;
let screen_col = c1.1 as isize + gutter_width as isize;
// Fill in the gutter with the appropriate background
for y in c1.0..(c2.0 + 1) {
for x in c1.1..(c1.1 + gutter_width - 1) {
self.screen.draw(x, y, " ", STYLE_GUTTER);
}
}
let mut line_num = line_index + 1;
for line in editor.buffer.line_iter_at_index(line_index) {
// Print line number
if line_block_index == 0 {
let lnx = c1.1 + (gutter_width - 2 - digit_count(line_num as u32, 10) as usize);
let lny = screen_line as usize;
if lny >= c1.0 && lny <= c2.0 {
self.screen
.draw(lnx, lny, &format!("{}", line_num)[..], STYLE_GUTTER);
}
}
// Loop through the graphemes of the line and print them to
// the screen.
let max_block_index = block_count(&line) - 1;
let block_range = char_range_from_block_index(&line, line_block_index);
let mut g_iter = editor.formatter.iter(RopeGraphemes::new(
&line.slice(block_range.0..block_range.1),
));
let mut last_pos_y = 0;
let mut lines_traversed: usize = 0;
loop {
for (g, (pos_y, pos_x), width) in g_iter {
if last_pos_y != pos_y {
if last_pos_y < pos_y {
lines_traversed += pos_y - last_pos_y;
}
last_pos_y = pos_y;
}
// Calculate the cell coordinates at which to draw the grapheme
let px = pos_x as isize + screen_col - editor.view_pos.1 as isize;
let py = lines_traversed as isize + screen_line;
// If we're off the bottom, we're done
if py > c2.0 as isize {
return;
}
// Draw the grapheme to the screen if it's in bounds
if (px >= c1.1 as isize) && (py >= c1.0 as isize) && (px <= c2.1 as isize) {
// Check if the character is within a cursor
let mut at_cursor = false;
for c in editor.cursors.iter() {
if char_index >= c.range.0 && char_index <= c.range.1 {
at_cursor = true;
self.screen.set_cursor(px as usize, py as usize);
}
}
// Actually print the character
if is_line_ending(&g) {
if at_cursor {
self.screen
.draw(px as usize, py as usize, " ", STYLE_CURSOR);
}
} else if g == "\t" {
for i in 0..width {
let tpx = px as usize + i;
if tpx <= c2.1 {
self.screen.draw(tpx as usize, py as usize, " ", STYLE_MAIN);
}
}
if at_cursor {
self.screen
.draw(px as usize, py as usize, " ", STYLE_CURSOR);
}
} else {
if at_cursor {
self.screen.draw(px as usize, py as usize, &g, STYLE_CURSOR);
} else {
self.screen.draw(px as usize, py as usize, &g, STYLE_MAIN);
}
}
}
char_index += g.chars().count();
}
// Move on to the next block.
line_block_index += 1;
if line_block_index <= max_block_index {
let block_range = char_range_from_block_index(&line, line_block_index);
g_iter = editor.formatter.iter(RopeGraphemes::new(
&line.slice(block_range.0..block_range.1),
));
lines_traversed += 1;
} else {
break;
}
}
line_block_index = 0;
screen_line += lines_traversed as isize + 1;
line_num += 1;
}
// If we get here, it means we reached the end of the text buffer
// without going off the bottom of the screen. So draw the cursor
// at the end if needed.
// Check if the character is within a cursor
let mut at_cursor = false;
for c in editor.cursors.iter() {
if char_index >= c.range.0 && char_index <= c.range.1 {
at_cursor = true;
}
}
if at_cursor {
// Calculate the cell coordinates at which to draw the cursor
let pos_x = editor
.formatter
.index_to_horizontal_v2d(&self.editor.buffer, self.editor.buffer.char_count());
let px = pos_x as isize + screen_col - editor.view_pos.1 as isize;
let py = screen_line - 1;
if (px >= c1.1 as isize)
&& (py >= c1.0 as isize)
&& (px <= c2.1 as isize)
&& (py <= c2.0 as isize)
{
self.screen
.draw(px as usize, py as usize, " ", STYLE_CURSOR);
self.screen.set_cursor(px as usize, py as usize);
}
}
}
}