#![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::() { 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); } } } }