From cd4a7da3eff988cdbb1a13a9a76179ec51f810d6 Mon Sep 17 00:00:00 2001 From: Jeremy Fitzhardinge Date: Sat, 28 Dec 2019 15:21:48 -0800 Subject: [PATCH] WIP cursor blinking https://github.com/wez/wezterm/issues/83 --- src/frontend/gui/termwindow.rs | 44 +++++++------ src/mux/renderable.rs | 7 ++- src/server/listener.rs | 4 +- src/server/tab.rs | 17 ++--- src/termwiztermtab.rs | 19 +++--- term/src/terminalstate.rs | 34 ++++++---- termwiz/src/surface/mod.rs | 109 ++++++++++++++++++--------------- 7 files changed, 133 insertions(+), 101 deletions(-) diff --git a/src/frontend/gui/termwindow.rs b/src/frontend/gui/termwindow.rs index 764517f3e5c..54ac78c8ef5 100644 --- a/src/frontend/gui/termwindow.rs +++ b/src/frontend/gui/termwindow.rs @@ -85,7 +85,6 @@ pub struct TermWindow { last_mouse_coords: (usize, i64), scroll_drag_start: Option, config_generation: usize, - created_instant: Instant, last_scroll_info: (VisibleRowIndex, usize), /// Gross workaround for managing async keyboard fetching @@ -600,7 +599,6 @@ impl TermWindow { last_mouse_coords: (0, -1), scroll_drag_start: None, config_generation: config.generation(), - created_instant: Instant::now(), last_scroll_info: (0, 0), clipboard_contents: Arc::clone(&clipboard_contents), }), @@ -643,7 +641,7 @@ impl TermWindow { if config.cursor_blink_rate != 0 { let shape = config .default_cursor_style - .effective_shape(render.get_cursor_position().shape); + .effective_shape(render.get_cursor_position().0.shape); if shape.is_blinking() { let now = Instant::now(); if now.duration_since(last_blink_paint) @@ -929,7 +927,7 @@ impl TermWindow { fn update_text_cursor(&mut self, tab: &Rc) { let term = tab.renderer(); - let cursor = term.get_cursor_position(); + let (cursor, _) = term.get_cursor_position(); if let Some(win) = self.window.as_ref() { let config = configuration(); let r = Rect::new( @@ -1247,12 +1245,15 @@ impl TermWindow { let mut term = tab.renderer(); let cursor = { - let cursor = term.get_cursor_position(); - CursorPosition { - x: cursor.x, - y: cursor.y + first_line_offset as i64, - ..cursor - } + let (cursor, last_moved) = term.get_cursor_position(); + ( + CursorPosition { + x: cursor.x, + y: cursor.y + first_line_offset as i64, + ..cursor + }, + last_moved, + ) }; if self.show_tab_bar { @@ -1381,12 +1382,15 @@ impl TermWindow { let mut term = tab.renderer(); let cursor = { - let cursor = term.get_cursor_position(); - CursorPosition { - x: cursor.x, - y: cursor.y + first_line_offset as i64, - ..cursor - } + let (cursor, last_moved) = term.get_cursor_position(); + ( + CursorPosition { + x: cursor.x, + y: cursor.y + first_line_offset as i64, + ..cursor + }, + last_moved, + ) }; let gl_state = self.render_state.opengl(); @@ -1533,7 +1537,7 @@ impl TermWindow { line_idx: usize, line: &Line, selection: Range, - cursor: &CursorPosition, + cursor: &(CursorPosition, Instant), terminal: &dyn Renderable, palette: &ColorPalette, quads: &mut MappedQuads, @@ -1786,7 +1790,7 @@ impl TermWindow { line_idx: usize, line: &Line, selection: Range, - cursor: &CursorPosition, + cursor: &(CursorPosition, Instant), terminal: &dyn Renderable, palette: &ColorPalette, ) -> anyhow::Result<()> { @@ -2066,7 +2070,7 @@ impl TermWindow { &self, line_idx: usize, cell_idx: usize, - cursor: &CursorPosition, + &(cursor, last_moved): &(CursorPosition, Instant), selection: &Range, fg_color: Color, bg_color: Color, @@ -2093,7 +2097,7 @@ impl TermWindow { // If the result is even then the cursor is "on", else it // is "off" let now = std::time::Instant::now(); - let milli_uptime = now.duration_since(self.created_instant).as_millis(); + let milli_uptime = now.duration_since(last_moved).as_millis(); let ticks = milli_uptime / config.cursor_blink_rate as u128; if (ticks & 1) == 0 { shape diff --git a/src/mux/renderable.rs b/src/mux/renderable.rs index 959516c99fa..f2442e66e02 100644 --- a/src/mux/renderable.rs +++ b/src/mux/renderable.rs @@ -2,6 +2,7 @@ use downcast_rs::{impl_downcast, Downcast}; use std::borrow::Cow; use std::ops::Range; use std::sync::Arc; +use std::time::Instant; use term::{CursorPosition, Line, Terminal, TerminalState, VisibleRowIndex}; use termwiz::hyperlink::Hyperlink; @@ -10,8 +11,8 @@ use termwiz::hyperlink::Hyperlink; /// surfaces via a multiplexer. pub trait Renderable: Downcast { /// Returns the 0-based cursor position relative to the top left of - /// the visible screen - fn get_cursor_position(&self) -> CursorPosition; + /// the visible screen. Also return the last time the cursor moved. + fn get_cursor_position(&self) -> (CursorPosition, Instant); /// Returns the set of visible lines that are dirty. /// The return value is a Vec<(line_idx, line, selrange)>, where @@ -43,7 +44,7 @@ pub trait Renderable: Downcast { impl_downcast!(Renderable); impl Renderable for Terminal { - fn get_cursor_position(&self) -> CursorPosition { + fn get_cursor_position(&self) -> (CursorPosition, Instant) { self.cursor_pos() } diff --git a/src/server/listener.rs b/src/server/listener.rs index 19c3539e249..ea518d14641 100644 --- a/src/server/listener.rs +++ b/src/server/listener.rs @@ -445,8 +445,8 @@ impl ClientSurfaceState { renderable.make_all_lines_dirty(); } - let (x, y) = self.surface.cursor_position(); - let cursor = renderable.get_cursor_position(); + let (x, y, _) = self.surface.cursor_position(); + let (cursor, _) = renderable.get_cursor_position(); if (x != cursor.x) || (y as i64 != cursor.y) { // Update the cursor, but if we're scrolled back // and it is our of range, skip the update. diff --git a/src/server/tab.rs b/src/server/tab.rs index 1047f345a12..a24e1646fc9 100644 --- a/src/server/tab.rs +++ b/src/server/tab.rs @@ -400,15 +400,18 @@ impl RenderableInner { } impl Renderable for RenderableState { - fn get_cursor_position(&self) -> CursorPosition { + fn get_cursor_position(&self) -> (CursorPosition, Instant) { let surface = &self.inner.borrow().surface; - let (x, y) = surface.cursor_position(); + let (x, y, last_moved) = surface.cursor_position(); let shape = surface.cursor_shape(); - CursorPosition { - x, - y: y as i64, - shape, - } + ( + CursorPosition { + x, + y: y as i64, + shape, + }, + last_moved, + ) } fn get_dirty_lines(&self) -> Vec<(usize, Cow, Range)> { diff --git a/src/termwiztermtab.rs b/src/termwiztermtab.rs index 9506139dc4c..79b5bb1aadb 100644 --- a/src/termwiztermtab.rs +++ b/src/termwiztermtab.rs @@ -22,7 +22,7 @@ use std::ops::Range; use std::rc::Rc; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, Mutex}; -use std::time::Duration; +use std::time::{Duration, Instant}; use term::color::ColorPalette; use term::selection::SelectionRange; use term::{ @@ -65,15 +65,18 @@ impl std::io::Write for RenderableState { } impl Renderable for RenderableState { - fn get_cursor_position(&self) -> CursorPosition { + fn get_cursor_position(&self) -> (CursorPosition, Instant) { let surface = &self.inner.borrow().surface; - let (x, y) = surface.cursor_position(); + let (x, y, last_moved) = surface.cursor_position(); let shape = surface.cursor_shape(); - CursorPosition { - x, - y: y as i64, - shape, - } + ( + CursorPosition { + x, + y: y as i64, + shape, + }, + last_moved, + ) } fn get_dirty_lines(&self) -> Vec<(usize, Cow, Range)> { diff --git a/term/src/terminalstate.rs b/term/src/terminalstate.rs index 823b40b61a1..daca0c5fc36 100644 --- a/term/src/terminalstate.rs +++ b/term/src/terminalstate.rs @@ -9,6 +9,7 @@ use log::{debug, error}; use ordered_float::NotNan; use std::fmt::Write; use std::sync::Arc; +use std::time::Instant; use termwiz::escape::csi::{ Cursor, CursorStyle, DecPrivateMode, DecPrivateModeCode, Device, Edit, EraseInDisplay, EraseInLine, Mode, Sgr, TerminalMode, TerminalModeCode, Window, @@ -154,6 +155,8 @@ pub struct TerminalState { /// The current cursor position, relative to the top left /// of the screen. 0-based index. cursor: CursorPosition, + /// Last time the cursor moved, for blinking + cursor_moved_time: Instant, /// if true, implicitly move to the next line on the next /// printed character @@ -275,6 +278,7 @@ impl TerminalState { screen, pen: CellAttributes::default(), cursor: CursorPosition::default(), + cursor_moved_time: Instant::now(), scroll_region: 0..physical_rows as VisibleRowIndex, wrap_next: false, insert: false, @@ -1231,17 +1235,20 @@ impl TerminalState { } /// Returns the 0-based cursor position relative to the top left of - /// the visible screen - pub fn cursor_pos(&self) -> CursorPosition { - CursorPosition { - x: self.cursor.x, - y: self.cursor.y + self.viewport_offset, - shape: if self.cursor_visible { - self.cursor.shape - } else { - CursorShape::Hidden + /// the visible screen. Also returns the last time the cursor moved. + pub fn cursor_pos(&self) -> (CursorPosition, Instant) { + ( + CursorPosition { + x: self.cursor.x, + y: self.cursor.y + self.viewport_offset, + shape: if self.cursor_visible { + self.cursor.shape + } else { + CursorShape::Hidden + }, }, - } + self.cursor_moved_time, + ) } /// Returns the currently highlighted hyperlink @@ -1264,10 +1271,15 @@ impl TerminalState { let rows = self.screen().physical_rows; let cols = self.screen().physical_cols; + let old_x = self.cursor.x; + let new_x = x.min(cols as i64 - 1) as usize; let old_y = self.cursor.y; let new_y = y.min(rows as i64 - 1); - self.cursor.x = x.min(cols as i64 - 1) as usize; + if (old_x, old_y) != (new_x, new_y) { + self.cursor_moved_time = Instant::now(); + } + self.cursor.x = new_x; self.cursor.y = new_y; self.wrap_next = false; diff --git a/termwiz/src/surface/mod.rs b/termwiz/src/surface/mod.rs index 380e81240d0..234070460ca 100644 --- a/termwiz/src/surface/mod.rs +++ b/termwiz/src/surface/mod.rs @@ -92,6 +92,7 @@ pub struct Surface { attributes: CellAttributes, xpos: usize, ypos: usize, + last_moved: Instant, seqno: SequenceNo, changes: Vec, cursor_shape: CursorShape, @@ -99,6 +100,20 @@ pub struct Surface { title: String, } +struct Instant(std::time::Instant); + +impl Instant { + fn now() -> Self { + Instant(std::time::Instant::now()) + } +} + +impl Default for Instant { + fn default() -> Self { + Instant::now() + } +} + #[derive(Default)] struct DiffState { changes: Vec, @@ -186,8 +201,8 @@ impl Surface { (self.width, self.height) } - pub fn cursor_position(&self) -> (usize, usize) { - (self.xpos, self.ypos) + pub fn cursor_position(&self) -> (usize, usize, std::time::Instant) { + (self.xpos, self.ypos, self.last_moved.0) } pub fn cursor_shape(&self) -> CursorShape { @@ -337,6 +352,7 @@ impl Surface { } self.xpos = 0; self.ypos = 0; + self.last_moved = Instant::now(); } fn clear_eos(&mut self, color: ColorAttribute) { @@ -381,58 +397,51 @@ impl Surface { } } + fn down_or_scroll(&mut self) -> usize { + if self.ypos + 1 >= self.height { + self.scroll_screen_up(); + self.ypos + } else { + self.ypos + 1 + } + } + fn print_text(&mut self, text: &str) { for g in UnicodeSegmentation::graphemes(text, true) { - if g == "\r\n" { - self.xpos = 0; - let new_y = self.ypos + 1; - if new_y >= self.height { - self.scroll_screen_up(); - } else { - self.ypos = new_y; - } - continue; - } - - if g == "\r" { - self.xpos = 0; - continue; - } - - if g == "\n" { - let new_y = self.ypos + 1; - if new_y >= self.height { - self.scroll_screen_up(); - } else { - self.ypos = new_y; - } - continue; - } - - if self.xpos >= self.width { - let new_y = self.ypos + 1; - if new_y >= self.height { - self.scroll_screen_up(); + let (new_x, new_y) = if g == "\r\n" { + (0, self.down_or_scroll()) + } else if g == "\r" { + (0, self.ypos) + } else if g == "\n" { + (self.xpos, self.down_or_scroll()) + } else { + let (new_x, new_y) = if self.xpos >= self.width { + (0, self.down_or_scroll()) } else { - self.ypos = new_y; - } - self.xpos = 0; + (self.xpos, self.ypos) + }; + + let cell = Cell::new_grapheme(g, self.attributes.clone()); + // the max(1) here is to ensure that we advance to the next cell + // position for zero-width graphemes. We want to make sure that + // they occupy a cell so that we can re-emit them when we output them. + // If we didn't do this, then we'd effectively filter them out from + // the model, which seems like a lossy design choice. + let width = cell.width().max(1); + + self.lines[new_y].set_cell(new_x, cell); + + // Increment the position now; we'll defer processing + // wrapping until the next printed character, otherwise + // we'll eagerly scroll when we reach the right margin. + (new_x + width, new_y) + }; + + if (new_x, new_y) != (self.xpos, self.ypos) { + self.xpos = new_x; + self.ypos = new_y; + self.last_moved = Instant::now(); } - - let cell = Cell::new_grapheme(g, self.attributes.clone()); - // the max(1) here is to ensure that we advance to the next cell - // position for zero-width graphemes. We want to make sure that - // they occupy a cell so that we can re-emit them when we output them. - // If we didn't do this, then we'd effectively filter them out from - // the model, which seems like a lossy design choice. - let width = cell.width().max(1); - - self.lines[self.ypos].set_cell(self.xpos, cell); - - // Increment the position now; we'll defer processing - // wrapping until the next printed character, otherwise - // we'll eagerly scroll when we reach the right margin. - self.xpos += width; } }