led/src/term_ui/mod.rs

699 lines
22 KiB
Rust

#![allow(dead_code)]
mod screen;
pub mod smallstring;
use std::time::Duration;
use crossterm::{
event::{Event, KeyCode, KeyEvent, KeyModifiers},
style::Color,
};
use backend::buffer::BufferPath;
use crate::{
editor::Editor,
string_utils::{char_count, is_line_ending, line_ending_to_str, LineEnding},
utils::{digit_count, Timer},
};
use self::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: 0x30,
g: 0x30,
b: 0x30,
},
);
const STYLE_CURSOR: Style = Style(
Color::Rgb {
r: 0x00,
g: 0x00,
b: 0x00,
},
Color::Rgb {
r: 0xD0,
g: 0xD0,
b: 0xD0,
},
);
const STYLE_GUTTER_LINE_START: Style = Style(
Color::Rgb {
r: 0x78,
g: 0x78,
b: 0x78,
},
Color::Rgb {
r: 0x1D,
g: 0x1D,
b: 0x1D,
},
);
const STYLE_GUTTER_LINE_WRAP: Style = Style(
Color::Rgb {
r: 0x78,
g: 0x78,
b: 0x78,
},
Color::Rgb {
r: 0x27,
g: 0x27,
b: 0x27,
},
);
const COLOR_GUTTER_BAR: Color = Color::Rgb {
r: 0x18,
g: 0x18,
b: 0x18,
};
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.wrap_width = $term_ui.editor.view_dim.1;
// Draw!
{
$draw
};
$term_ui.screen.present();
}
}
};
}
pub struct TermUI {
screen: Screen,
editor: Editor,
width: usize,
height: usize,
quit: bool,
}
#[derive(Debug, Copy, Clone, PartialEq)]
enum LoopStatus {
Done,
Continue,
}
impl TermUI {
pub fn new_from_editor(ed: Editor) -> 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.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().expect("For some reason the file couldn't be saved. Also, TODO: this code path shouldn't panic.");
}
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::Up,
modifiers: KeyModifiers::CONTROL,
..
} => {
self.editor.cursor_up(8);
}
KeyEvent {
code: KeyCode::Down,
modifiers: EMPTY_MOD,
..
} => {
self.editor.cursor_down(1);
}
KeyEvent {
code: KeyCode::Down,
modifiers: KeyModifiers::CONTROL,
..
} => {
self.editor.cursor_down(8);
}
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.remove_text_behind_cursor(1);
}
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, 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 = match editor.buffer.path {
BufferPath::File(ref p) => format!("{}", p.display()),
BufferPath::Temp(i) => format!("Scratch #{}", i + 1),
};
let dirty_char = if editor.buffer.is_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.text.len_chars() > 0 {
(((editor.buffer.mark_sets[editor.c_msi].main().unwrap().head as f32)
/ (editor.buffer.text.len_chars() 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, c1: (usize, usize), c2: (usize, usize)) {
let view_pos = editor.buffer.mark_sets[editor.v_msi][0].head;
let cursors = &editor.buffer.mark_sets[editor.c_msi];
// Calculate all the starting info
let gutter_width = editor.editor_dim.1 - editor.view_dim.1;
let blank_gutter = &" "[..gutter_width - 1];
let line_index = editor.buffer.text.char_to_line(view_pos);
let (blocks_iter, char_offset) = editor.formatter.iter(&editor.buffer.text, view_pos);
let vis_line_offset = blocks_iter.clone().next().unwrap().0.vpos(char_offset);
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) {
self.screen
.draw(c1.1, y, blank_gutter, STYLE_GUTTER_LINE_WRAP);
self.screen.draw(
c1.1 + blank_gutter.len() - 1,
y,
"",
Style(COLOR_GUTTER_BAR, STYLE_GUTTER_LINE_WRAP.1),
);
}
// Loop through the blocks, printing them to the screen.
let mut is_first_loop = true;
let mut line_num = line_index + 1;
let mut char_index = view_pos - char_offset;
for (block_vis_iter, is_line_start) in blocks_iter {
if is_line_start && !is_first_loop {
line_num += 1;
}
is_first_loop = false;
// Print line number
if is_line_start {
let lnx = c1.1;
let lny = screen_line as usize;
if lny >= c1.0 && lny <= c2.0 {
self.screen.draw(
lnx,
lny,
&format!(
"{}{}",
&blank_gutter
[..(gutter_width - 2 - digit_count(line_num as u32, 10) as usize)],
line_num,
)[..],
STYLE_GUTTER_LINE_START,
);
self.screen.draw(
lnx + blank_gutter.len() - 1,
lny,
"",
Style(COLOR_GUTTER_BAR, STYLE_GUTTER_LINE_START.1),
);
}
}
// Loop through the graphemes of the block and print them to
// the screen.
let mut last_pos_y = 0;
for (g, (pos_y, pos_x), width) in block_vis_iter {
// Calculate the cell coordinates at which to draw the grapheme
if pos_y > last_pos_y {
screen_line += 1;
last_pos_y = pos_y;
}
let px = pos_x as isize + screen_col;
let py = 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 cursors.iter() {
if char_index >= c.range().start && char_index <= c.range().end {
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 += char_count(&g);
}
screen_line += 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 cursors.iter() {
if char_index >= c.range().start && char_index <= c.range().end {
at_cursor = true;
}
}
if at_cursor {
// Calculate the cell coordinates at which to draw the cursor
let pos_x = editor.formatter.get_horizontal(
&self.editor.buffer.text,
self.editor.buffer.text.len_chars(),
);
let mut px = pos_x as isize + screen_col;
let mut py = screen_line - 1;
if px > c2.1 as isize {
px = c1.1 as isize + screen_col;
py += 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);
}
}
}
}