diff options
-rw-r--r-- | CHANGELOG.md | 3 | ||||
-rw-r--r-- | Cargo.lock | 2 | ||||
-rw-r--r-- | alacritty.yml | 3 | ||||
-rw-r--r-- | alacritty/Cargo.toml | 2 | ||||
-rw-r--r-- | alacritty/src/config/debug.rs | 4 | ||||
-rw-r--r-- | alacritty/src/display/content.rs | 53 | ||||
-rw-r--r-- | alacritty/src/display/damage.rs | 86 | ||||
-rw-r--r-- | alacritty/src/display/meter.rs | 2 | ||||
-rw-r--r-- | alacritty/src/display/mod.rs | 185 | ||||
-rw-r--r-- | alacritty/src/display/window.rs | 10 | ||||
-rw-r--r-- | alacritty/src/event.rs | 6 | ||||
-rw-r--r-- | alacritty/src/input.rs | 4 | ||||
-rw-r--r-- | alacritty/src/renderer/builtin_font.rs | 2 | ||||
-rw-r--r-- | alacritty_terminal/Cargo.toml | 2 | ||||
-rw-r--r-- | alacritty_terminal/src/config/mod.rs | 2 | ||||
-rw-r--r-- | alacritty_terminal/src/config/scrolling.rs | 2 | ||||
-rw-r--r-- | alacritty_terminal/src/selection.rs | 1 | ||||
-rw-r--r-- | alacritty_terminal/src/term/mod.rs | 755 | ||||
-rw-r--r-- | alacritty_terminal/src/vi_mode.rs | 2 |
19 files changed, 991 insertions, 135 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index edcdc07e..90995ded 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,7 +13,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### Added - - Option `font.builtin_box_drawing` to disable the built-in font for drawing box characters +- Option `font.builtin_box_drawing` to disable the built-in font for drawing box characters +- Track and report surface damage information to Wayland compositors ### Changed @@ -56,7 +56,7 @@ dependencies = [ [[package]] name = "alacritty_terminal" -version = "0.16.1-dev" +version = "0.17.0-dev" dependencies = [ "alacritty_config_derive", "base64", diff --git a/alacritty.yml b/alacritty.yml index c9959497..85de5701 100644 --- a/alacritty.yml +++ b/alacritty.yml @@ -876,3 +876,6 @@ # Print all received window events. #print_events: false + + # Highlight window damage information. + #highlight_damage: false diff --git a/alacritty/Cargo.toml b/alacritty/Cargo.toml index ef46eb2c..b1438c23 100644 --- a/alacritty/Cargo.toml +++ b/alacritty/Cargo.toml @@ -11,7 +11,7 @@ rust-version = "1.56.0" [dependencies.alacritty_terminal] path = "../alacritty_terminal" -version = "0.16.1-dev" +version = "0.17.0-dev" default-features = false [dependencies.alacritty_config_derive] diff --git a/alacritty/src/config/debug.rs b/alacritty/src/config/debug.rs index f52cdf90..3fa987a5 100644 --- a/alacritty/src/config/debug.rs +++ b/alacritty/src/config/debug.rs @@ -15,6 +15,9 @@ pub struct Debug { /// Should show render timer. pub render_timer: bool, + /// Highlight damage information produced by alacritty. + pub highlight_damage: bool, + /// Record ref test. #[config(skip)] pub ref_test: bool, @@ -27,6 +30,7 @@ impl Default for Debug { print_events: Default::default(), persistent_logging: Default::default(), render_timer: Default::default(), + highlight_damage: Default::default(), ref_test: Default::default(), } } diff --git a/alacritty/src/display/content.rs b/alacritty/src/display/content.rs index 72d79f7e..3b549992 100644 --- a/alacritty/src/display/content.rs +++ b/alacritty/src/display/content.rs @@ -7,6 +7,7 @@ use alacritty_terminal::ansi::{Color, CursorShape, NamedColor}; use alacritty_terminal::event::EventListener; use alacritty_terminal::grid::{Dimensions, Indexed}; use alacritty_terminal::index::{Column, Direction, Line, Point}; +use alacritty_terminal::selection::SelectionRange; use alacritty_terminal::term::cell::{Cell, Flags}; use alacritty_terminal::term::color::{CellRgb, Rgb}; use alacritty_terminal::term::search::{Match, RegexIter, RegexSearch}; @@ -26,7 +27,7 @@ pub const MIN_CURSOR_CONTRAST: f64 = 1.5; /// This provides the terminal cursor and an iterator over all non-empty cells. pub struct RenderableContent<'a> { terminal_content: TerminalContent<'a>, - cursor: Option<RenderableCursor>, + cursor: RenderableCursor, cursor_shape: CursorShape, cursor_point: Point<usize>, search: Option<Regex<'a>>, @@ -73,7 +74,7 @@ impl<'a> RenderableContent<'a> { Self { colors: &display.colors, - cursor: None, + cursor: RenderableCursor::new_hidden(), terminal_content, focused_match, cursor_shape, @@ -90,7 +91,7 @@ impl<'a> RenderableContent<'a> { } /// Get the terminal cursor. - pub fn cursor(mut self) -> Option<RenderableCursor> { + pub fn cursor(mut self) -> RenderableCursor { // Assure this function is only called after the iterator has been drained. debug_assert!(self.next().is_none()); @@ -102,14 +103,14 @@ impl<'a> RenderableContent<'a> { self.terminal_content.colors[color].unwrap_or(self.colors[color]) } + pub fn selection_range(&self) -> Option<SelectionRange> { + self.terminal_content.selection + } + /// Assemble the information required to render the terminal cursor. /// /// This will return `None` when there is no cursor visible. - fn renderable_cursor(&mut self, cell: &RenderableCell) -> Option<RenderableCursor> { - if self.cursor_shape == CursorShape::Hidden { - return None; - } - + fn renderable_cursor(&mut self, cell: &RenderableCell) -> RenderableCursor { // Cursor colors. let color = if self.terminal_content.mode.contains(TermMode::VI) { self.config.colors.vi_mode_cursor @@ -134,13 +135,13 @@ impl<'a> RenderableContent<'a> { text_color = self.config.colors.primary.background; } - Some(RenderableCursor { + RenderableCursor { is_wide: cell.flags.contains(Flags::WIDE_CHAR), shape: self.cursor_shape, point: self.cursor_point, cursor_color, text_color, - }) + } } } @@ -159,18 +160,15 @@ impl<'a> Iterator for RenderableContent<'a> { if self.cursor_point == cell.point { // Store the cursor which should be rendered. - self.cursor = self.renderable_cursor(&cell).map(|cursor| { - if cursor.shape == CursorShape::Block { - cell.fg = cursor.text_color; - cell.bg = cursor.cursor_color; - - // Since we draw Block cursor by drawing cell below it with a proper color, - // we must adjust alpha to make it visible. - cell.bg_alpha = 1.; - } - - cursor - }); + self.cursor = self.renderable_cursor(&cell); + if self.cursor.shape == CursorShape::Block { + cell.fg = self.cursor.text_color; + cell.bg = self.cursor.cursor_color; + + // Since we draw Block cursor by drawing cell below it with a proper color, + // we must adjust alpha to make it visible. + cell.bg_alpha = 1.; + } return Some(cell); } else if !cell.is_empty() && !cell.flags.contains(Flags::WIDE_CHAR_SPACER) { @@ -372,6 +370,17 @@ pub struct RenderableCursor { } impl RenderableCursor { + fn new_hidden() -> Self { + let shape = CursorShape::Hidden; + let cursor_color = Rgb::default(); + let text_color = Rgb::default(); + let is_wide = false; + let point = Point::default(); + Self { shape, cursor_color, text_color, is_wide, point } + } +} + +impl RenderableCursor { pub fn color(&self) -> Rgb { self.cursor_color } diff --git a/alacritty/src/display/damage.rs b/alacritty/src/display/damage.rs new file mode 100644 index 00000000..d6a69a2d --- /dev/null +++ b/alacritty/src/display/damage.rs @@ -0,0 +1,86 @@ +use std::cmp; +use std::iter::Peekable; + +use glutin::Rect; + +use alacritty_terminal::term::{LineDamageBounds, SizeInfo, TermDamageIterator}; + +/// Iterator which converts `alacritty_terminal` damage information into renderer damaged rects. +pub struct RenderDamageIterator<'a> { + damaged_lines: Peekable<TermDamageIterator<'a>>, + size_info: SizeInfo<u32>, +} + +impl<'a> RenderDamageIterator<'a> { + pub fn new(damaged_lines: TermDamageIterator<'a>, size_info: SizeInfo<u32>) -> Self { + Self { damaged_lines: damaged_lines.peekable(), size_info } + } + + #[inline] + fn rect_for_line(&self, line_damage: LineDamageBounds) -> Rect { + let size_info = &self.size_info; + let y_top = size_info.height() - size_info.padding_y(); + let x = size_info.padding_x() + line_damage.left as u32 * size_info.cell_width(); + let y = y_top - (line_damage.line + 1) as u32 * size_info.cell_height(); + let width = (line_damage.right - line_damage.left + 1) as u32 * size_info.cell_width(); + Rect { x, y, height: size_info.cell_height(), width } + } + + // Make sure to damage near cells to include wide chars. + #[inline] + fn overdamage(&self, mut rect: Rect) -> Rect { + let size_info = &self.size_info; + rect.x = rect.x.saturating_sub(size_info.cell_width()); + rect.width = cmp::min(size_info.width() - rect.x, rect.width + 2 * size_info.cell_width()); + rect.y = rect.y.saturating_sub(size_info.cell_height() / 2); + rect.height = cmp::min(size_info.height() - rect.y, rect.height + size_info.cell_height()); + + rect + } +} + +impl<'a> Iterator for RenderDamageIterator<'a> { + type Item = Rect; + + fn next(&mut self) -> Option<Rect> { + let line = self.damaged_lines.next()?; + let mut total_damage_rect = self.overdamage(self.rect_for_line(line)); + + // Merge rectangles which overlap with each other. + while let Some(line) = self.damaged_lines.peek().copied() { + let next_rect = self.overdamage(self.rect_for_line(line)); + if !rects_overlap(total_damage_rect, next_rect) { + break; + } + + total_damage_rect = merge_rects(total_damage_rect, next_rect); + let _ = self.damaged_lines.next(); + } + + Some(total_damage_rect) + } +} + +/// Check if two given [`glutin::Rect`] overlap. +fn rects_overlap(lhs: Rect, rhs: Rect) -> bool { + !( + // `lhs` is left of `rhs`. + lhs.x + lhs.width < rhs.x + // `lhs` is right of `rhs`. + || rhs.x + rhs.width < lhs.x + // `lhs` is below `rhs`. + || lhs.y + lhs.height < rhs.y + // `lhs` is above `rhs`. + || rhs.y + rhs.height < lhs.y + ) +} + +/// Merge two [`glutin::Rect`] by producing the smallest rectangle that contains both. +#[inline] +fn merge_rects(lhs: Rect, rhs: Rect) -> Rect { + let left_x = cmp::min(lhs.x, rhs.x); + let right_x = cmp::max(lhs.x + lhs.width, rhs.x + rhs.width); + let y_top = cmp::max(lhs.y + lhs.height, rhs.y + rhs.height); + let y_bottom = cmp::min(lhs.y, rhs.y); + Rect { x: left_x, y: y_bottom, width: right_x - left_x, height: y_top - y_bottom } +} diff --git a/alacritty/src/display/meter.rs b/alacritty/src/display/meter.rs index c07d901f..9ccfe52d 100644 --- a/alacritty/src/display/meter.rs +++ b/alacritty/src/display/meter.rs @@ -31,7 +31,7 @@ pub struct Meter { /// Average sample time in microseconds. avg: f64, - /// Index of next time to update.. + /// Index of next time to update. index: usize, } diff --git a/alacritty/src/display/mod.rs b/alacritty/src/display/mod.rs index d9ec8593..7d53e678 100644 --- a/alacritty/src/display/mod.rs +++ b/alacritty/src/display/mod.rs @@ -1,13 +1,12 @@ //! The display subsystem including window management, font rasterization, and //! GPU drawing. -use std::cmp::min; use std::convert::TryFrom; use std::fmt::{self, Formatter}; #[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))] use std::sync::atomic::Ordering; use std::time::Instant; -use std::{f64, mem}; +use std::{cmp, mem}; use glutin::dpi::PhysicalSize; use glutin::event::ModifiersState; @@ -15,6 +14,7 @@ use glutin::event_loop::EventLoopWindowTarget; #[cfg(all(feature = "x11", not(any(target_os = "macos", windows))))] use glutin::platform::unix::EventLoopWindowTargetExtUnix; use glutin::window::CursorIcon; +use glutin::Rect as DamageRect; use log::{debug, info}; use parking_lot::MutexGuard; use unicode_width::UnicodeWidthChar; @@ -24,12 +24,16 @@ use wayland_client::EventQueue; use crossfont::{self, Rasterize, Rasterizer}; use alacritty_terminal::ansi::NamedColor; +use alacritty_terminal::config::MAX_SCROLLBACK_LINES; use alacritty_terminal::event::{EventListener, OnResize}; use alacritty_terminal::grid::Dimensions as _; use alacritty_terminal::index::{Column, Direction, Line, Point}; -use alacritty_terminal::selection::Selection; +use alacritty_terminal::selection::{Selection, SelectionRange}; use alacritty_terminal::term::cell::Flags; -use alacritty_terminal::term::{SizeInfo, Term, TermMode, MIN_COLUMNS, MIN_SCREEN_LINES}; +use alacritty_terminal::term::color::Rgb; +use alacritty_terminal::term::{ + SizeInfo, Term, TermDamage, TermMode, MIN_COLUMNS, MIN_SCREEN_LINES, +}; use crate::config::font::Font; #[cfg(not(windows))] @@ -40,6 +44,7 @@ use crate::display::bell::VisualBell; use crate::display::color::List; use crate::display::content::RenderableContent; use crate::display::cursor::IntoRects; +use crate::display::damage::RenderDamageIterator; use crate::display::hint::{HintMatch, HintState}; use crate::display::meter::Meter; use crate::display::window::Window; @@ -55,6 +60,7 @@ pub mod window; mod bell; mod color; +mod damage; mod meter; /// Maximum number of linewraps followed outside of the viewport during search highlighting. @@ -66,6 +72,9 @@ const FORWARD_SEARCH_LABEL: &str = "Search: "; /// Label for the backward terminal search bar. const BACKWARD_SEARCH_LABEL: &str = "Backward Search: "; +/// Color which is used to highlight damaged rects when debugging. +const DAMAGE_RECT_COLOR: Rgb = Rgb { r: 255, g: 0, b: 255 }; + #[derive(Debug)] pub enum Error { /// Error with window management. @@ -193,6 +202,9 @@ pub struct Display { /// Unprocessed display updates. pub pending_update: DisplayUpdate, + is_damage_supported: bool, + debug_damage: bool, + damage_rects: Vec<DamageRect>, renderer: QuadRenderer, glyph_cache: GlyphCache, meter: Meter, @@ -319,6 +331,13 @@ impl Display { } let hint_state = HintState::new(config.hints.alphabet()); + let is_damage_supported = window.swap_buffers_with_damage_supported(); + let debug_damage = config.debug.highlight_damage; + let damage_rects = if is_damage_supported || debug_damage { + Vec::with_capacity(size_info.screen_lines()) + } else { + Vec::new() + }; Ok(Self { window, @@ -335,6 +354,9 @@ impl Display { visual_bell: VisualBell::from(&config.bell), colors: List::from(&config.colors), pending_update: Default::default(), + is_damage_supported, + debug_damage, + damage_rects, }) } @@ -457,10 +479,58 @@ impl Display { self.window.resize(physical); self.renderer.resize(&self.size_info); + if self.collect_damage() { + let lines = self.size_info.screen_lines(); + if lines > self.damage_rects.len() { + self.damage_rects.reserve(lines); + } else { + self.damage_rects.shrink_to(lines); + } + } + info!("Padding: {} x {}", self.size_info.padding_x(), self.size_info.padding_y()); info!("Width: {}, Height: {}", self.size_info.width(), self.size_info.height()); } + fn update_damage<T: EventListener>( + &mut self, + terminal: &mut MutexGuard<'_, Term<T>>, + selection_range: Option<SelectionRange>, + search_state: &SearchState, + ) { + let requires_full_damage = self.visual_bell.intensity() != 0. + || self.hint_state.active() + || search_state.regex().is_some(); + if requires_full_damage { + terminal.mark_fully_damaged(); + } + + self.damage_highlighted_hints(terminal); + let size_info: SizeInfo<u32> = self.size_info.into(); + match terminal.damage(selection_range) { + TermDamage::Full => { + let screen_rect = + DamageRect { x: 0, y: 0, width: size_info.width(), height: size_info.height() }; + self.damage_rects.push(screen_rect); + }, + TermDamage::Partial(damaged_lines) => { + let damaged_rects = RenderDamageIterator::new(damaged_lines, size_info); + for damaged_rect in damaged_rects { + self.damage_rects.push(damaged_rect); + } + }, + } + terminal.reset_damage(); + + // Ensure that the content requiring full damage is cleaned up again on the next frame. + if requires_full_damage { + terminal.mark_fully_damaged(); + } + + // Damage highlighted hints for the next frame as well, so we'll clear them. + self.damage_highlighted_hints(terminal); + } + /// Draw the screen. /// /// A reference to Term whose state is being drawn must be provided. @@ -468,7 +538,7 @@ impl Display { /// This call may block if vsync is enabled. pub fn draw<T: EventListener>( &mut self, - terminal: MutexGuard<'_, Term<T>>, + mut terminal: MutexGuard<'_, Term<T>>, message_buffer: &MessageBuffer, config: &UiConfig, search_state: &SearchState, @@ -479,6 +549,7 @@ impl Display { for cell in &mut content { grid_cells.push(cell); } + let selection_range = content.selection_range(); let background_color = content.color(NamedColor::Background as usize); let display_offset = content.display_offset(); let cursor = content.cursor(); @@ -491,6 +562,11 @@ impl Display { let vi_mode = terminal.mode().contains(TermMode::VI); let vi_mode_cursor = if vi_mode { Some(terminal.vi_mode_cursor) } else { None }; + if self.collect_damage() { + self.damage_rects.clear(); + self.update_damage(&mut terminal, selection_range, search_state); + } + // Drop terminal as early as possible to free lock. drop(terminal); @@ -549,11 +625,9 @@ impl Display { self.draw_line_indicator(config, &size_info, total_lines, None, display_offset); } - // Push the cursor rects for rendering. - if let Some(cursor) = cursor { - for rect in cursor.rects(&size_info, config.terminal_config.cursor.thickness()) { - rects.push(rect); - } + // Draw cursor. + for rect in cursor.rects(&size_info, config.terminal_config.cursor.thickness()) { + rects.push(rect); } // Push visual bell after url/underline/strikeout rects. @@ -570,6 +644,10 @@ impl Display { rects.push(visual_bell_rect); } + if self.debug_damage { + self.highlight_damage(&mut rects); + } + if let Some(message) = message_buffer.message() { let search_offset = if search_state.regex().is_some() { 1 } else { 0 }; let text = message.text(&size_info); @@ -636,7 +714,12 @@ impl Display { #[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))] self.request_frame(&self.window); - self.window.swap_buffers(); + // Clearing debug highlights from the previous frame requires full redraw. + if self.is_damage_supported && !self.debug_damage { + self.window.swap_buffers_with_damage(&self.damage_rects); + } else { + self.window.swap_buffers(); + } #[cfg(all(feature = "x11", not(any(target_os = "macos", windows))))] if self.is_x11 { @@ -651,6 +734,7 @@ impl Display { /// Update to a new configuration. pub fn update_config(&mut self, config: &UiConfig) { + self.debug_damage = config.debug.highlight_damage; self.visual_bell.update_config(&config.bell); self.colors = List::from(&config.colors); } @@ -722,7 +806,7 @@ impl Display { let num_cols = size_info.columns(); let label_len = search_label.chars().count(); let regex_len = formatted_regex.chars().count(); - let truncate_len = min((regex_len + label_len).saturating_sub(num_cols), regex_len); + let truncate_len = cmp::min((regex_len + label_len).saturating_sub(num_cols), regex_len); let index = formatted_regex.char_indices().nth(truncate_len).map(|(i, _c)| i).unwrap_or(0); let truncated_regex = &formatted_regex[index..]; @@ -758,13 +842,15 @@ impl Display { return; } - let glyph_cache = &mut self.glyph_cache; - let timing = format!("{:.3} usec", self.meter.average()); let point = Point::new(size_info.screen_lines().saturating_sub(2), Column(0)); let fg = config.colors.primary.background; let bg = config.colors.normal.red; + // Damage the entire line. + self.damage_from_point(point, self.size_info.columns() as u32); + + let glyph_cache = &mut self.glyph_cache; self.renderer.with_api(config, size_info, |mut api| { api.draw_string(glyph_cache, point, fg, bg, &timing); }); @@ -779,8 +865,26 @@ impl Display { obstructed_column: Option<Column>, line: usize, ) { + const fn num_digits(mut number: u32) -> usize { + let mut res = 0; + loop { + number /= 10; + res += 1; + if number == 0 { + break res; + } + } + } + let text = format!("[{}/{}]", line, total_lines - 1); let column = Column(size_info.columns().saturating_sub(text.len())); + let point = Point::new(0, column); + + // Damage the maximum possible length of the format text, which could be achieved when + // using `MAX_SCROLLBACK_LINES` as current and total lines adding a `3` for formatting. + const MAX_LEN: usize = num_digits(MAX_SCROLLBACK_LINES) + 3; + self.damage_from_point(Point::new(0, point.column - MAX_LEN), MAX_LEN as u32 * 2); + let colors = &config.colors; let fg = colors.line_indicator.foreground.unwrap_or(colors.primary.background); let bg = colors.line_indicator.background.unwrap_or(colors.primary.foreground); @@ -789,11 +893,60 @@ impl Display { if obstructed_column.map_or(true, |obstructed_column| obstructed_column < column) { let glyph_cache = &mut self.glyph_cache; self.renderer.with_api(config, size_info, |mut api| { - api.draw_string(glyph_cache, Point::new(0, column), fg, bg, &text); + api.draw_string(glyph_cache, point, fg, bg, &text); }); } } + /// Damage `len` starting from a `point`. + #[inline] + fn damage_from_point(&mut self, point: Point<usize>, len: u32) { + if !self.collect_damage() { + return; + } + + let size_info: SizeInfo<u32> = self.size_info.into(); + let x = size_info.padding_x() + point.column.0 as u32 * size_info.cell_width(); + let y_top = size_info.height() - size_info.padding_y(); + let y = y_top - (point.line as u32 + 1) * size_info.cell_height(); + let width = len as u32 * size_info.cell_width(); + self.damage_rects.push(DamageRect { x, y, width, height: size_info.cell_height() }) + } + + /// Damage currently highlighted `Display` hints. + #[inline] + fn damage_highlighted_hints<T: EventListener>(&self, terminal: &mut Term<T>) { + let display_offset = terminal.grid().display_offset(); + for hint in self.highlighted_hint.iter().chain(&self.vi_highlighted_hint) { + for point in (hint.bounds.start().line.0..=hint.bounds.end().line.0).flat_map(|line| { + point_to_viewport(display_offset, Point::new(Line(line), Column(0))) + }) { + terminal.damage_line(point.line, 0, terminal.columns() - 1); + } + } + } + + /// Returns `true` if damage information should be collected, `false` otherwise. + #[inline] + fn collect_damage(&self) -> bool { + self.is_damage_supported || self.debug_damage + } + + /// Highlight damaged rects. + /// + /// This function is for debug purposes only. + fn highlight_damage(&self, render_rects: &mut Vec<RenderRect>) { + for damage_rect in &self.damage_rects { + let x = damage_rect.x as f32; + let height = damage_rect.height as f32; + let width = damage_rect.width as f32; + let y = self.size_info.height() - damage_rect.y as f32 - height; + let render_rect = RenderRect::new(x, y, width, height, DAMAGE_RECT_COLOR, 0.5); + + render_rects.push(render_rect); + } + } + /// Requst a new frame for a window on Wayland. #[inline] #[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))] @@ -824,12 +977,14 @@ impl Drop for Display { } /// Convert a terminal point to a viewport relative point. +#[inline] pub fn point_to_viewport(display_offset: usize, point: Point) -> Option<Point<usize>> { let viewport_line = point.line.0 + display_offset as i32; usize::try_from(viewport_line).ok().map(|line| Point::new(line, point.column)) } /// Convert a viewport relative point to a terminal point. +#[inline] pub fn viewport_to_point(display_offset: usize, point: Point<usize>) -> Point { let line = Line(point.line as i32) - display_offset; Point::new(line, point.column) diff --git a/alacritty/src/display/window.rs b/alacritty/src/display/window.rs index 493e5ef9..712b4ac9 100644 --- a/alacritty/src/display/window.rs +++ b/alacritty/src/display/window.rs @@ -39,7 +39,7 @@ use glutin::platform::windows::IconExtWindows; use glutin::window::{ CursorIcon, Fullscreen, UserAttentionType, Window as GlutinWindow, WindowBuilder, WindowId, }; -use glutin::{self, ContextBuilder, PossiblyCurrent, WindowedContext}; +use glutin::{self, ContextBuilder, PossiblyCurrent, Rect, WindowedContext}; #[cfg(target_os = "macos")] use objc::{msg_send, sel, sel_impl}; #[cfg(target_os = "macos")] @@ -428,6 +428,14 @@ impl Window { self.windowed_context.swap_buffers().expect("swap buffers"); } + pub fn swap_buffers_with_damage(&self, damage: &[Rect]) { + self.windowed_context.swap_buffers_with_damage(damage).expect("swap buffes with damage"); + } + + pub fn swap_buffers_with_damage_supported(&self) -> bool { + self.windowed_context.swap_buffers_with_damage_supported() + } + pub fn resize(&self, size: PhysicalSize<u32>) { self.windowed_context.resize(size); } diff --git a/alacritty/src/event.rs b/alacritty/src/event.rs index aea6010d..8bd1dec7 100644 --- a/alacritty/src/event.rs +++ b/alacritty/src/event.rs @@ -768,7 +768,11 @@ impl<'a, N: Notify + 'a, T: EventListener> input::ActionContext<T> for ActionCon /// Toggle the vi mode status. #[inline] fn toggle_vi_mode(&mut self) { - if !self.terminal.mode().contains(TermMode::VI) { + if self.terminal.mode().contains(TermMode::VI) { + // Damage line indicator and Vi cursor if we're leaving Vi mode. + self.terminal.damage_vi_cursor(); + self.terminal.damage_line(0, 0, self.terminal.columns() - 1); + } else { self.clear_selection(); } diff --git a/alacritty/src/input.rs b/alacritty/src/input.rs index 0aa2cbba..51bd3fc5 100644 --- a/alacritty/src/input.rs +++ b/alacritty/src/input.rs @@ -801,7 +801,9 @@ impl<T: EventListener, A: ActionContext<T>> Processor<T, A> { self.ctx.on_typing_start(); - self.ctx.scroll(Scroll::Bottom); + if self.ctx.terminal().grid().display_offset() != 0 { + self.ctx.scroll(Scroll::Bottom); + } self.ctx.clear_selection(); let utf8_len = c.len_utf8(); diff --git a/alacritty/src/renderer/builtin_font.rs b/alacritty/src/renderer/builtin_font.rs index f3dbe9bb..05798466 100644 --- a/alacritty/src/renderer/builtin_font.rs +++ b/alacritty/src/renderer/builtin_font.rs @@ -784,7 +784,7 @@ impl Canvas { } #[cfg(test)] -mod test { +mod tests { use super::*; use crossfont::Metrics; diff --git a/alacritty_terminal/Cargo.toml b/alacritty_terminal/Cargo.toml index be8d17dd..269b110a 100644 --- a/alacritty_terminal/Cargo.toml +++ b/alacritty_terminal/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "alacritty_terminal" -version = "0.16.1-dev" +version = "0.17.0-dev" authors = ["Christian Duerr <contact@christianduerr.com>", "Joe Wilm <joe@jwilm.com>"] license = "Apache-2.0" description = "Library for writing terminal emulators" diff --git a/alacritty_terminal/src/config/mod.rs b/alacritty_terminal/src/config/mod.rs index 09161e03..e99c37b5 100644 --- a/alacritty_terminal/src/config/mod.rs +++ b/alacritty_terminal/src/config/mod.rs @@ -10,7 +10,7 @@ mod scrolling; use crate::ansi::{CursorShape, CursorStyle}; -pub use crate::config::scrolling::Scrolling; +pub use crate::config::scrolling::{Scrolling, MAX_SCROLLBACK_LINES}; pub const LOG_TARGET_CONFIG: &str = "alacritty_config_derive"; const MIN_BLINK_INTERVAL: u64 = 10; diff --git a/alacritty_terminal/src/config/scrolling.rs b/alacritty_terminal/src/config/scrolling.rs index 159b0f44..9a5a718c 100644 --- a/alacritty_terminal/src/config/scrolling.rs +++ b/alacritty_terminal/src/config/scrolling.rs @@ -4,7 +4,7 @@ use serde::{Deserialize, Deserializer}; use alacritty_config_derive::ConfigDeserialize; /// Maximum scrollback amount configurable. -const MAX_SCROLLBACK_LINES: u32 = 100_000; +pub const MAX_SCROLLBACK_LINES: u32 = 100_000; /// Struct for scrolling related settings. #[derive(ConfigDeserialize, Copy, Clone, Debug, PartialEq, Eq)] diff --git a/alacritty_terminal/src/selection.rs b/alacritty_terminal/src/selection.rs index f00622d1..669db6a2 100644 --- a/alacritty_terminal/src/selection.rs +++ b/alacritty_terminal/src/selection.rs @@ -41,6 +41,7 @@ pub struct SelectionRange { impl SelectionRange { pub fn new(start: Point, end: Point, is_block: bool) -> Self { + assert!(start <= end); Self { start, end, is_block } } } diff --git a/alacritty_terminal/src/term/mod.rs b/alacritty_terminal/src/term/mod.rs index 3fa57f7f..7b0667e2 100644 --- a/alacritty_terminal/src/term/mod.rs +++ b/alacritty_terminal/src/term/mod.rs @@ -1,9 +1,8 @@ //! Exports the `Term` type which is a high-level API for the Grid. -use std::cmp::{max, min}; use std::ops::{Index, IndexMut, Range}; use std::sync::Arc; -use std::{mem, ptr, str}; +use std::{cmp, mem, ptr, slice, str}; use bitflags::bitflags; use log::{debug, trace}; @@ -62,7 +61,7 @@ bitflags! { const ALTERNATE_SCROLL = 0b0000_1000_0000_0000_0000; const VI = 0b0001_0000_0000_0000_0000; const URGENCY_HINTS = 0b0010_0000_0000_0000_0000; - const ANY = std::u32::MAX; + const ANY = u32::MAX; } } @@ -77,24 +76,24 @@ impl Default for TermMode { /// Terminal size info. #[derive(Serialize, Deserialize, Debug, Copy, Clone, PartialEq)] -pub struct SizeInfo { +pub struct SizeInfo<T = f32> { /// Terminal window width. - width: f32, + width: T, /// Terminal window height. - height: f32, + height: T, /// Width of individual cell. - cell_width: f32, + cell_width: T, /// Height of individual cell. - cell_height: f32, + cell_height: T, /// Horizontal window padding. - padding_x: f32, + padding_x: T, /// Vertical window padding. - padding_y: f32, + padding_y: T, /// Number of lines in the viewport. screen_lines: usize, @@ -103,7 +102,54 @@ pub struct SizeInfo { columns: usize, } -impl SizeInfo { +impl From<SizeInfo<f32>> for SizeInfo<u32> { + fn from(size_info: SizeInfo<f32>) -> Self { + Self { + width: size_info.width as u32, + height: size_info.height as u32, + cell_width: size_info.cell_width as u32, + cell_height: size_info.cell_height as u32, + padding_x: size_info.padding_x as u32, + padding_y: size_info.padding_y as u32, + screen_lines: size_info.screen_lines, + columns: size_info.screen_lines, + } + } +} + +impl<T: Clone + Copy> SizeInfo<T> { + #[inline] + pub fn width(&self) -> T { + self.width + } + + #[inline] + pub fn height(&self) -> T { + self.height + } + + #[inline] + pub fn cell_width(&self) -> T { + self.cell_width + } + + #[inline] + pub fn cell_height(&self) -> T { + self.cell_height + } + + #[inline] + pub fn padding_x(&self) -> T { + self.padding_x + } + + #[inline] + pub fn padding_y(&self) -> T { + self.padding_y + } +} + +impl SizeInfo<f32> { #[allow(clippy::too_many_arguments)] pub fn new( width: f32, @@ -120,10 +166,10 @@ impl SizeInfo { } let lines = (height - 2. * padding_y) / cell_height; - let screen_lines = max(lines as usize, MIN_SCREEN_LINES); + let screen_lines = cmp::max(lines as usize, MIN_SCREEN_LINES); let columns = (width - 2. * padding_x) / cell_width; - let columns = max(columns as usize, MIN_COLUMNS); + let columns = cmp::max(columns as usize, MIN_COLUMNS); SizeInfo { width, @@ -139,7 +185,7 @@ impl SizeInfo { #[inline] pub fn reserve_lines(&mut self, count: usize) { - self.screen_lines = max(self.screen_lines.saturating_sub(count), MIN_SCREEN_LINES); + self.screen_lines = cmp::max(self.screen_lines.saturating_sub(count), MIN_SCREEN_LINES); } /// Check if coordinates are inside the terminal grid. @@ -153,57 +199,176 @@ impl SizeInfo { && y > self.padding_y as usize } + /// Calculate padding to spread it evenly around the terminal content. #[inline] - pub fn width(&self) -> f32 { - self.width + fn dynamic_padding(padding: f32, dimension: f32, cell_dimension: f32) -> f32 { + padding + ((dimension - 2. * padding) % cell_dimension) / 2. } +} +impl Dimensions for SizeInfo { #[inline] - pub fn height(&self) -> f32 { - self.height + fn columns(&self) -> usize { + self.columns } #[inline] - pub fn cell_width(&self) -> f32 { - self.cell_width + fn screen_lines(&self) -> usize { + self.screen_lines } #[inline] - pub fn cell_height(&self) -> f32 { - self.cell_height + fn total_lines(&self) -> usize { + self.screen_lines() } +} +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct LineDamageBounds { + /// Damaged line number. + pub line: usize, + + /// Leftmost damaged column. + pub left: usize, + + /// Rightmost damaged column. + pub right: usize, +} + +impl LineDamageBounds { #[inline] - pub fn padding_x(&self) -> f32 { - self.padding_x + pub fn undamaged(line: usize, num_cols: usize) -> Self { + Self { line, left: num_cols, right: 0 } } #[inline] - pub fn padding_y(&self) -> f32 { - self.padding_y + pub fn reset(&mut self, num_cols: usize) { + *self = Self::undamaged(self.line, num_cols); } - /// Calculate padding to spread it evenly around the terminal content. #[inline] - fn dynamic_padding(padding: f32, dimension: f32, cell_dimension: f32) -> f32 { - padding + ((dimension - 2. * padding) % cell_dimension) / 2. + pub fn expand(&mut self, left: usize, right: usize) { + self.left = cmp::min(self.left, left); + self.right = cmp::max(self.right, right); + } + + #[inline] + pub fn is_damaged(&self) -> bool { + self.left <= self.right } } -impl Dimensions for SizeInfo { +/// Terminal damage information collected since the last [`Term::reset_damage`] call. +#[derive(Debug)] +pub enum TermDamage<'a> { + /// The entire terminal is damaged. + Full, + + /// Iterator over damaged lines in the terminal. + Partial(TermDamageIterator<'a>), +} + +/// Iterator over the terminal's damaged lines. +#[derive(Clone, Debug)] +pub struct TermDamageIterator<'a> { + line_damage: slice::Iter<'a, LineDamageBounds>, +} + +impl<'a> TermDamageIterator<'a> { + fn new(line_damage: &'a [LineDamageBounds]) -> Self { + Self { line_damage: line_damage.iter() } + } +} + +impl<'a> Iterator for TermDamageIterator<'a> { + type Item = LineDamageBounds; + + fn next(&mut self) -> Option<Self::Item> { + self.line_damage.find(|line| line.is_damaged()).copied() + } +} + +/// State of the terminal damage. +struct TermDamageState { + /// Hint whether terminal should be damaged entirely regardless of the actual damage changes. + is_fully_damaged: bool, + + /// Information about damage on terminal lines. + lines: Vec<LineDamageBounds>, + + /// Old terminal cursor point. + last_cursor: Point, + + /// Old selection range. + last_selection: Option<SelectionRange>, +} + +impl TermDamageState { + fn new(num_cols: usize, num_lines: usize) -> Self { + let lines = + (0..num_lines).map(|line| LineDamageBounds::undamaged(line, num_cols)).collect(); + + Self { + is_fully_damaged: true, + lines, + last_cursor: Default::default(), + last_selection: Default::default(), + } + } + #[inline] - fn columns(&self) -> usize { - self.columns + fn resize(&mut self, num_cols: usize, num_lines: usize) { + // Reset point, so old cursor won't end up outside of the viewport. + self.last_cursor = Default::default(); + self.last_selection = None; + self.is_fully_damaged = true; + + self.lines.clear(); + self.lines.reserve(num_lines); + for line in 0..num_lines { + self.lines.push(LineDamageBounds::undamaged(line, num_cols)); + } } + /// Damage point inside of the viewport. #[inline] - fn screen_lines(&self) -> usize { - self.screen_lines + fn damage_point(&mut self, point: Point<usize>) { + self.damage_line(point.line, point.column.0 as usize, point.column.0 as usize); } + /// Expand `line`'s damage to span at least `left` to `right` column. #[inline] - fn total_lines(&self) -> usize { - self.screen_lines() + fn damage_line(&mut self, line: usize, left: usize, right: usize) { + self.lines[line].expand(left, right); + } + + fn damage_selection( + &mut self, + selection: SelectionRange, + display_offset: usize, + num_cols: usize, + ) { + let display_offset = display_offset as i32; + let last_visible_line = self.lines.len() as i32 - 1; + + // Don't damage invisible selection. + if selection.end.line.0 + display_offset < 0 + || selection.start.line.0.abs() < display_offset - last_visible_line + { + return; + }; + + let start = cmp::max(selection.start.line.0 + display_offset, 0); + let end = cmp::min(cmp::max(selection.end.line.0 + display_offset, 0), last_visible_line); + for line in start as usize..=end as usize { + self.damage_line(line, 0, num_cols - 1); + } + } + + /// Reset information about terminal damage. + fn reset(&mut self, num_cols: usize) { + self.is_fully_damaged = false; + self.lines.iter_mut().for_each(|line| line.reset(num_cols)); } } @@ -269,6 +434,9 @@ pub struct Term<T> { /// Information about cell dimensions. cell_width: usize, cell_height: usize, + + /// Information about damaged cells. + damage: TermDamageState, } impl<T> Term<T> { @@ -277,6 +445,7 @@ impl<T> Term<T> { where T: EventListener, { + let old_display_offset = self.grid.display_offset(); self.grid.scroll_display(scroll); self.event_proxy.send_event(Event::MouseCursorDirty); @@ -284,8 +453,13 @@ impl<T> Term<T> { let viewport_start = -(self.grid.display_offset() as i32); let viewport_end = viewport_start + self.bottommost_line().0; let vi_cursor_line = &mut self.vi_mode_cursor.point.line.0; - *vi_cursor_line = min(viewport_end, max(viewport_start, *vi_cursor_line)); + *vi_cursor_line = cmp::min(viewport_end, cmp::max(viewport_start, *vi_cursor_line)); self.vi_mode_recompute_selection(); + + // Damage everything if display offset changed. + if old_display_offset != self.grid().display_offset() { + self.mark_fully_damaged(); + } } pub fn new(config: &Config, size: SizeInfo, event_proxy: T) -> Term<T> { @@ -300,6 +474,9 @@ impl<T> Term<T> { let scroll_region = Line(0)..Line(grid.screen_lines() as i32); + // Initialize terminal damage, covering the entire terminal upon launch. + let damage = TermDamageState::new(num_cols, num_lines); + Term { grid, inactive_grid: alt, @@ -320,7 +497,68 @@ impl<T> Term<T> { selection: None, cell_width: size.cell_width as usize, cell_height: size.cell_height as usize, + damage, + } + } + + #[must_use] + pub fn damage(&mut self, selection: Option<SelectionRange>) -> TermDamage<'_> { + // Ensure the entire terminal is damaged after entering insert mode. + // Leaving is handled in the ansi handler. + if self.mode.contains(TermMode::INSERT) { + self.mark_fully_damaged(); + } + + // Early return if the entire terminal is damaged. + if self.damage.is_fully_damaged { + self.damage.last_cursor = self.grid.cursor.point; + self.damage.last_selection = selection; + return TermDamage::Full; + } + + // Add information about old cursor position and new one if they are not the same, so we + // cover everything that was produced by `Term::input`. + if self.damage.last_cursor != self.grid.cursor.point { + // Cursor cooridanates are always inside viewport even if you have `display_offset`. + let point = + Point::new(self.damage.last_cursor.line.0 as usize, self.damage.last_cursor.column); + self.damage.damage_point(point); } + + // Always damage current cursor. + self.damage_cursor(); + self.damage.last_cursor = self.grid.cursor.point; + + // Damage Vi cursor if it's present. + if self.mode.contains(TermMode::VI) { + self.damage_vi_cursor(); + } + + if self.damage.last_selection != selection { + let display_offset = self.grid().display_offset(); + for selection in self.damage.last_selection.into_iter().chain(selection) { + self.damage.damage_selection(selection, display_offset, self.columns()); + } + } + self.damage.last_selection = selection; + + TermDamage::Partial(TermDamageIterator::new(&self.damage.lines)) + } + + /// Resets the terminal damage information. + pub fn reset_damage(&mut self) { + self.damage.reset(self.columns()); + } + + #[inline] + pub fn mark_fully_damaged(&mut self) { + self.damage.is_fully_damaged = true; + } + + /// Damage line in a terminal viewport. + #[inline] + pub fn damage_line(&mut self, line: usize, left: usize, right: usize) { + self.damage.damage_line(line, left, right); } pub fn update_config(&mut self, config: &Config) @@ -343,6 +581,9 @@ impl<T> Term<T> { } else { self.grid.update_history(config.scrolling.history() as usize); } + + // Damage everything on config updates. + self.mark_fully_damaged(); } /// Convert the active selection to a String. @@ -398,7 +639,7 @@ impl<T> Term<T> { let mut text = String::new(); let grid_line = &self.grid[line]; - let line_length = min(grid_line.line_length(), cols.end + 1); + let line_length = cmp::min(grid_line.line_length(), cols.end + 1); // Include wide char when trailing spacer is selected. if grid_line[cols.start].flags.contains(Flags::WIDE_CHAR_SPACER) { @@ -496,8 +737,8 @@ impl<T> Term<T> { // Move vi mode cursor with the content. let history_size = self.history_size(); let mut delta = num_lines as i32 - old_lines as i32; - let min_delta = min(0, num_lines as i32 - self.grid.cursor.point.line.0 - 1); - delta = min(max(delta, min_delta), history_size as i32); + let min_delta = cmp::min(0, num_lines as i32 - self.grid.cursor.point.line.0 - 1); + delta = cmp::min(cmp::max(delta, min_delta), history_size as i32); self.vi_mode_cursor.point.line += delta; // Invalidate selection and tabs only when necessary. @@ -519,11 +760,15 @@ impl<T> Term<T> { let vi_point = self.vi_mode_cursor.point; let viewport_top = Line(-(self.grid.display_offset() as i32)); let viewport_bottom = viewport_top + self.bottommost_line(); - self.vi_mode_cursor.point.line = max(min(vi_point.line, viewport_bottom), viewport_top); - self.vi_mode_cursor.point.column = min(vi_point.column, self.last_column()); + self.vi_mode_cursor.point.line = + cmp::max(cmp::min(vi_point.line, viewport_bottom), viewport_top); + self.vi_mode_cursor.point.column = cmp::min(vi_point.column, self.last_column()); // Reset scrolling region. self.scroll_region = Line(0)..Line(self.screen_lines() as i32); + + // Resize damage information. + self.damage.resize(num_cols, num_lines); } /// Active terminal modes. @@ -548,6 +793,7 @@ impl<T> Term<T> { mem::swap(&mut self.grid, &mut self.inactive_grid); self.mode ^= TermMode::ALT_SCREEN; self.selection = None; + self.mark_fully_damaged(); } /// Scroll screen down. @@ -558,8 +804,8 @@ impl<T> Term<T> { fn scroll_down_relative(&mut self, origin: Line, mut lines: usize) { trace!("Scrolling down relative: origin={}, lines={}", origin, lines); - lines = min(lines, (self.scroll_region.end - self.scroll_region.start).0 as usize); - lines = min(lines, (self.scroll_region.end - origin).0 as usize); + lines = cmp::min(lines, (self.scroll_region.end - self.scroll_region.start).0 as usize); + lines = cmp::min(lines, (self.scroll_region.end - origin).0 as usize); let region = origin..self.scroll_region.end; @@ -570,11 +816,12 @@ impl<T> Term<T> { // Scroll vi mode cursor. let line = &mut self.vi_mode_cursor.point.line; if region.start <= *line && region.end > *line { - *line = min(*line + lines, region.end - 1); + *line = cmp::min(*line + lines, region.end - 1); } // Scroll between origin and bottom self.grid.scroll_down(®ion, lines); + self.mark_fully_damaged(); } /// Scroll screen up @@ -585,7 +832,7 @@ impl<T> Term<T> { fn scroll_up_relative(&mut self, origin: Line, mut lines: usize) { trace!("Scrolling up relative: origin={}, lines={}", origin, lines); - lines = min(lines, (self.scroll_region.end - self.scroll_region.start).0 as usize); + lines = cmp::min(lines, (self.scroll_region.end - self.scroll_region.start).0 as usize); let region = origin..self.scroll_region.end; @@ -599,8 +846,9 @@ impl<T> Term<T> { let top = if region.start == 0 { viewport_top } else { region.start }; let line = &mut self.vi_mode_cursor.point.line; if (top <= *line) && region.end > *line { - *line = max(*line - lines, top); + *line = cmp::max(*line - lines, top); } + self.mark_fully_damaged(); } fn deccolm(&mut self) @@ -613,6 +861,7 @@ impl<T> Term<T> { // Clear grid. self.grid.reset_region(..); + self.mark_fully_damaged(); } #[inline] @@ -659,7 +908,9 @@ impl<T> Term<T> { } // Move cursor. + self.damage_vi_cursor(); self.vi_mode_cursor = self.vi_mode_cursor.motion(self, motion); + self.damage_vi_cursor(); self.vi_mode_recompute_selection(); } @@ -669,6 +920,7 @@ impl<T> Term<T> { where T: EventListener, { + self.damage_vi_cursor(); // Move viewport to make point visible. self.scroll_to_point(point); @@ -676,6 +928,7 @@ impl<T> Term<T> { self.vi_mode_cursor.point = point; self.vi_mode_recompute_selection(); + self.damage_vi_cursor(); } /// Update the active selection to match the vi mode cursor position. @@ -720,7 +973,7 @@ impl<T> Term<T> { point.line += 1; }, Direction::Right if flags.contains(Flags::WIDE_CHAR) => { - point.column = min(point.column + 1, self.last_column()); + point.column = cmp::min(point.column + 1, self.last_column()); }, Direction::Left if flags.intersects(Flags::WIDE_CHAR | Flags::WIDE_CHAR_SPACER) => { if flags.contains(Flags::WIDE_CHAR_SPACER) { @@ -757,6 +1010,10 @@ impl<T> Term<T> { } } + pub fn colors(&self) -> &Colors { + &self.colors + } + /// Insert a linebreak at the current cursor position. #[inline] fn wrapline(&mut self) @@ -774,11 +1031,13 @@ impl<T> Term<T> { if self.grid.cursor.point.line + 1 >= self.scroll_region.end { self.linefeed(); } else { + self.damage_cursor(); self.grid.cursor.point.line += 1; } self.grid.cursor.point.column = Column(0); self.grid.cursor.input_needs_wrap = false; + self.damage_cursor(); } /// Write `c` to the cell at the cursor position. @@ -819,8 +1078,21 @@ impl<T> Term<T> { cursor_cell.flags = flags; } - pub fn colors(&self) -> &Colors { - &self.colors + #[inline] + fn damage_cursor(&mut self) { + // The normal cursor coordinates are always in viewport. + let point = + Point::new(self.grid.cursor.point.line.0 as usize, self.grid.cursor.point.column); + self.damage.damage_point(point); + } + + /// Damage `Vi` mode cursor. + #[inline] + pub fn damage_vi_cursor(&mut self) { + let line = (self.grid.display_offset() as i32 + self.vi_mode_cursor.point.line.0) + .clamp(0, self.screen_lines() as i32 - 1) as usize; + let vi_point = Point::new(line, self.vi_mode_cursor.point.column); + self.damage.damage_point(vi_point); } } @@ -933,6 +1205,8 @@ impl<T: EventListener> Handler for Term<T> { cell.c = 'E'; } } + + self.mark_fully_damaged(); } #[inline] @@ -944,8 +1218,10 @@ impl<T: EventListener> Handler for Term<T> { (Line(0), self.bottommost_line()) }; - self.grid.cursor.point.line = max(min(line + y_offset, max_y), Line(0)); - self.grid.cursor.point.column = min(col, self.last_column()); + self.damage_cursor(); + self.grid.cursor.point.line = cmp::max(cmp::min(line + y_offset, max_y), Line(0)); + self.grid.cursor.point.column = cmp::min(col, self.last_column()); + self.damage_cursor(); self.grid.cursor.input_needs_wrap = false; } @@ -967,13 +1243,15 @@ impl<T: EventListener> Handler for Term<T> { let bg = cursor.template.bg; // Ensure inserting within terminal bounds - let count = min(count, self.columns() - cursor.point.column.0); + let count = cmp::min(count, self.columns() - cursor.point.column.0); let source = cursor.point.column; let destination = cursor.point.column.0 + count; let num_cells = self.columns() - destination; let line = cursor.point.line; + self.damage.damage_line(line.0 as usize, 0, self.columns() - 1); + let row = &mut self.grid[line][..]; for offset in (0..num_cells).rev() { @@ -1002,16 +1280,24 @@ impl<T: EventListener> Handler for Term<T> { #[inline] fn move_forward(&mut self, cols: Column) { trace!("Moving forward: {}", cols); - let last_column = self.last_column(); - self.grid.cursor.point.column = min(self.grid.cursor.point.column + cols, last_column); + let last_column = cmp::min(self.grid.cursor.point.column + cols, self.last_column()); + + let cursor_line = self.grid.cursor.point.line.0 as usize; + self.damage.damage_line(cursor_line, self.grid.cursor.point.column.0, last_column.0); + + self.grid.cursor.point.column = last_column; self.grid.cursor.input_needs_wrap = false; } #[inline] fn move_backward(&mut self, cols: Column) { trace!("Moving backward: {}", cols); - self.grid.cursor.point.column = - Column(self.grid.cursor.point.column.saturating_sub(cols.0)); + let column = self.grid.cursor.point.column.saturating_sub(cols.0); + + let cursor_line = self.grid.cursor.point.line.0 as usize; + self.damage.damage_line(cursor_line, column, self.grid.cursor.point.column.0); + + self.grid.cursor.point.column = Column(column); self.grid.cursor.input_needs_wrap = false; } @@ -1100,8 +1386,11 @@ impl<T: EventListener> Handler for Term<T> { trace!("Backspace"); if self.grid.cursor.point.column > Column(0) { + let line = self.grid.cursor.point.line.0 as usize; + let column = self.grid.cursor.point.column.0 as usize; self.grid.cursor.point.column -= 1; self.grid.cursor.input_needs_wrap = false; + self.damage.damage_line(line, column - 1, column); } } @@ -1109,7 +1398,10 @@ impl<T: EventListener> Handler for Term<T> { #[inline] fn carriage_return(&mut self) { trace!("Carriage return"); - self.grid.cursor.point.column = Column(0); + let new_col = 0; + let line = self.grid.cursor.point.line.0 as usize; + self.damage_line(line, new_col, self.grid.cursor.point.column.0); + self.grid.cursor.point.column = Column(new_col); self.grid.cursor.input_needs_wrap = false; } @@ -1121,7 +1413,9 @@ impl<T: EventListener> Handler for Term<T> { if next == self.scroll_region.end { self.scroll_up(1); } else if next < self.screen_lines() { + self.damage_cursor(); self.grid.cursor.point.line += 1; + self.damage_cursor(); } } @@ -1199,7 +1493,7 @@ impl<T: EventListener> Handler for Term<T> { #[inline] fn delete_lines(&mut self, lines: usize) { let origin = self.grid.cursor.point.line; - let lines = min(self.screen_lines() - origin.0 as usize, lines); + let lines = cmp::min(self.screen_lines() - origin.0 as usize, lines); trace!("Deleting {} lines", lines); @@ -1215,11 +1509,12 @@ impl<T: EventListener> Handler for Term<T> { trace!("Erasing chars: count={}, col={}", count, cursor.point.column); let start = cursor.point.column; - let end = min(start + count, Column(self.columns())); + let end = cmp::min(start + count, Column(self.columns())); // Cleared cells have current background color set. let bg = self.grid.cursor.template.bg; let line = cursor.point.line; + self.damage.damage_line(line.0 as usize, start.0, end.0); let row = &mut self.grid[line]; for cell in &mut row[start..end] { *cell = bg.into(); @@ -1233,13 +1528,14 @@ impl<T: EventListener> Handler for Term<T> { let bg = cursor.template.bg; // Ensure deleting within terminal bounds. - let count = min(count, columns); + let count = cmp::min(count, columns); let start = cursor.point.column.0; - let end = min(start + count, columns - 1); + let end = cmp::min(start + count, columns - 1); let num_cells = columns - end; let line = cursor.point.line; + self.damage.damage_line(line.0 as usize, 0, self.columns() - 1); let row = &mut self.grid[line][..]; for offset in 0..num_cells { @@ -1257,7 +1553,9 @@ impl<T: EventListener> Handler for Term<T> { #[inline] fn move_backward_tabs(&mut self, count: u16) { trace!("Moving backward {} tabs", count); + self.damage_cursor(); + let old_col = self.grid.cursor.point.column.0; for _ in 0..count { let mut col = self.grid.cursor.point.column; for i in (0..(col.0)).rev() { @@ -1268,6 +1566,9 @@ impl<T: EventListener> Handler for Term<T> { } self.grid.cursor.point.column = col; } + + let line = self.grid.cursor.point.line.0 as usize; + self.damage_line(line, self.grid.cursor.point.column.0, old_col); } #[inline] @@ -1286,7 +1587,9 @@ impl<T: EventListener> Handler for Term<T> { fn restore_cursor_position(&mut self) { trace!("Restoring cursor position"); + self.damage_cursor(); self.grid.cursor = self.grid.saved_cursor.clone(); + self.damage_cursor(); } #[inline] @@ -1295,26 +1598,19 @@ impl<T: EventListener> Handler for Term<T> { let cursor = &self.grid.cursor; let bg = cursor.template.bg; - let point = cursor.point; - let row = &mut self.grid[point.line]; - match mode { - ansi::LineClearMode::Right => { - for cell in &mut row[point.column..] { - *cell = bg.into(); - } - }, - ansi::LineClearMode::Left => { - for cell in &mut row[..=point.column] { - *cell = bg.into(); - } - }, - ansi::LineClearMode::All => { - for cell in &mut row[..] { - *cell = bg.into(); - } - }, + let (left, right) = match mode { + ansi::LineClearMode::Right => (point.column, Column(self.columns())), + ansi::LineClearMode::Left => (Column(0), point.column + 1), + ansi::LineClearMode::All => (Column(0), Column(self.columns())), + }; + + self.damage.damage_line(point.line.0 as usize, left.0, right.0 - 1); + + let row = &mut self.grid[point.line]; + for cell in &mut row[left..right] { + *cell = bg.into(); } let range = self.grid.cursor.point.line..=self.grid.cursor.point.line; @@ -1406,7 +1702,7 @@ impl<T: EventListener> Handler for Term<T> { } // Clear up to the current column in the current line. - let end = min(cursor.column + 1, Column(self.columns())); + let end = cmp::min(cursor.column + 1, Column(self.columns())); for cell in &mut self.grid[cursor.line][..end] { *cell = bg.into(); } @@ -1455,6 +1751,8 @@ impl<T: EventListener> Handler for Term<T> { // We have no history to clear. ansi::ClearMode::Saved => (), } + + self.mark_fully_damaged(); } #[inline] @@ -1491,6 +1789,7 @@ impl<T: EventListener> Handler for Term<T> { self.mode.insert(TermMode::default()); self.event_proxy.send_event(Event::CursorBlinkingChange); + self.mark_fully_damaged(); } #[inline] @@ -1500,7 +1799,9 @@ impl<T: EventListener> Handler for Term<T> { if self.grid.cursor.point.line == self.scroll_region.start { self.scroll_down(1); } else { - self.grid.cursor.point.line = max(self.grid.cursor.point.line - 1, Line(0)); + self.damage_cursor(); + self.grid.cursor.point.line = cmp::max(self.grid.cursor.point.line - 1, Line(0)); + self.damage_cursor(); } } @@ -1632,7 +1933,10 @@ impl<T: EventListener> Handler for Term<T> { ansi::Mode::LineFeedNewLine => self.mode.remove(TermMode::LINE_FEED_NEW_LINE), ansi::Mode::Origin => self.mode.remove(TermMode::ORIGIN), ansi::Mode::ColumnMode => self.deccolm(), - ansi::Mode::Insert => self.mode.remove(TermMode::INSERT), + ansi::Mode::Insert => { + self.mode.remove(TermMode::INSERT); + self.mark_fully_damaged(); + }, ansi::Mode::BlinkingCursor => { let style = self.cursor_style.get_or_insert(self.default_cursor_style); style.blinking = false; @@ -1661,8 +1965,8 @@ impl<T: EventListener> Handler for Term<T> { trace!("Setting scrolling region: ({};{})", start, end); let screen_lines = Line(self.screen_lines() as i32); - self.scroll_region.start = min(start, screen_lines); - self.scroll_region.end = min(end, screen_lines); + self.scroll_region.start = cmp::min(start, screen_lines); + self.scroll_region.end = cmp::min(end, screen_lines); self.goto(Line(0), Column(0)); } @@ -1833,7 +2137,7 @@ impl IndexMut<Column> for TabStops { } /// Terminal cursor rendering information. -#[derive(Copy, Clone)] +#[derive(Copy, Clone, PartialEq, Eq)] pub struct RenderableCursor { pub shape: CursorShape, pub point: Point, @@ -2430,6 +2734,285 @@ mod tests { } #[test] + fn damage_public_usage() { + let size = SizeInfo::new(10.0, 10.0, 1.0, 1.0, 0.0, 0.0, false); + let mut term = Term::new(&Config::default(), size, ()); + // Reset terminal for partial damage tests since it's initialized as fully damaged. + term.reset_damage(); + + // Test that we damage input form [`Term::input`]. + + let left = term.grid.cursor.point.column.0; + term.input('d'); + term.input('a'); + term.input('m'); + term.input('a'); + term.input('g'); + term.input('e'); + let right = term.grid.cursor.point.column.0; + + let mut damaged_lines = match term.damage(None) { + TermDamage::Full => panic!("Expected partial damage, however got Full"), + TermDamage::Partial(damaged_lines) => damaged_lines, + }; + assert_eq!(damaged_lines.next(), Some(LineDamageBounds { line: 0, left, right })); + assert_eq!(damaged_lines.next(), None); + term.reset_damage(); + + // Check that selection we've passed was properly damaged. + + let line = 1; + let left = 0; + let right = term.columns() - 1; + let mut selection = + Selection::new(SelectionType::Block, Point::new(Line(line), Column(3)), Side::Left); + selection.update(Point::new(Line(line), Column(5)), Side::Left); + let selection_range = selection.to_range(&term); + + let mut damaged_lines = match term.damage(selection_range) { + TermDamage::Full => panic!("Expected partial damage, however got Full"), + TermDamage::Partial(damaged_lines) => damaged_lines, + }; + let line = line as usize; + // Skip cursor damage information, since we're just testing selection. + damaged_lines.next(); + assert_eq!(damaged_lines.next(), Some(LineDamageBounds { line, left, right })); + assert_eq!(damaged_lines.next(), None); + term.reset_damage(); + + // Check that existing selection gets damaged when it is removed. + + let mut damaged_lines = match term.damage(None) { + TermDamage::Full => panic!("Expected partial damage, however got Full"), + TermDamage::Partial(damaged_lines) => damaged_lines, + }; + // Skip cursor damage information, since we're just testing selection clearing. + damaged_lines.next(); + assert_eq!(damaged_lines.next(), Some(LineDamageBounds { line, left, right })); + assert_eq!(damaged_lines.next(), None); + term.reset_damage(); + + // Check that `Vi` cursor in vi mode is being always damaged. + + term.toggle_vi_mode(); + // Put Vi cursor to a different location than normal cursor. + term.vi_goto_point(Point::new(Line(5), Column(5))); + // Reset damage, so the damage information from `vi_goto_point` won't affect test. + term.reset_damage(); + let vi_cursor_point = term.vi_mode_cursor.point; + let line = vi_cursor_point.line.0 as usize; + let left = vi_cursor_point.column.0 as usize; + let right = left; + let mut damaged_lines = match term.damage(None) { + TermDamage::Full => panic!("Expected partial damage, however got Full"), + TermDamage::Partial(damaged_lines) => damaged_lines, + }; + // Skip cursor damage information, since we're just testing Vi cursor. + damaged_lines.next(); + assert_eq!(damaged_lines.next(), Some(LineDamageBounds { line, left, right })); + assert_eq!(damaged_lines.next(), None); + } + + #[test] + fn damage_cursor_movements() { + let size = SizeInfo::new(10.0, 10.0, 1.0, 1.0, 0.0, 0.0, false); + let mut term = Term::new(&Config::default(), size, ()); + let num_cols = term.columns(); + // Reset terminal for partial damage tests since it's initialized as fully damaged. + term.reset_damage(); + + term.goto(Line(1), Column(1)); + + // NOTE While we can use `[Term::damage]` to access terminal damage information, in the + // following tests we will be accessing `term.damage.lines` directly to avoid adding extra + // damage information (like cursor and Vi cursor), which we're not testing. + + assert_eq!(term.damage.lines[0], LineDamageBounds { line: 0, left: 0, right: 0 }); + assert_eq!(term.damage.lines[1], LineDamageBounds { line: 1, left: 1, right: 1 }); + term.damage.reset(num_cols); + + term.move_forward(Column(3)); + assert_eq!(term.damage.lines[1], LineDamageBounds { line: 1, left: 1, right: 4 }); + term.damage.reset(num_cols); + + term.move_backward(Column(8)); + assert_eq!(term.damage.lines[1], LineDamageBounds { line: 1, left: 0, right: 4 }); + term.goto(Line(5), Column(5)); + term.damage.reset(num_cols); + + term.backspace(); + term.backspace(); + assert_eq!(term.damage.lines[5], LineDamageBounds { line: 5, left: 3, right: 5 }); + term.damage.reset(num_cols); + + term.move_up(1); + assert_eq!(term.damage.lines[5], LineDamageBounds { line: 5, left: 3, right: 3 }); + assert_eq!(term.damage.lines[4], LineDamageBounds { line: 4, left: 3, right: 3 }); + term.damage.reset(num_cols); + + term.move_down(1); + term.move_down(1); + assert_eq!(term.damage.lines[4], LineDamageBounds { line: 4, left: 3, right: 3 }); + assert_eq!(term.damage.lines[5], LineDamageBounds { line: 5, left: 3, right: 3 }); + assert_eq!(term.damage.lines[6], LineDamageBounds { line: 6, left: 3, right: 3 }); + term.damage.reset(num_cols); + + term.wrapline(); + assert_eq!(term.damage.lines[6], LineDamageBounds { line: 6, left: 3, right: 3 }); + assert_eq!(term.damage.lines[7], LineDamageBounds { line: 7, left: 0, right: 0 }); + term.move_forward(Column(3)); + term.move_up(1); + term.damage.reset(num_cols); + + term.linefeed(); + assert_eq!(term.damage.lines[6], LineDamageBounds { line: 6, left: 3, right: 3 }); + assert_eq!(term.damage.lines[7], LineDamageBounds { line: 7, left: 3, right: 3 }); + term.damage.reset(num_cols); + + term.carriage_return(); + assert_eq!(term.damage.lines[7], LineDamageBounds { line: 7, left: 0, right: 3 }); + term.damage.reset(num_cols); + + term.erase_chars(Column(5)); + assert_eq!(term.damage.lines[7], LineDamageBounds { line: 7, left: 0, right: 5 }); + term.damage.reset(num_cols); + + term.delete_chars(3); + let right = term.columns() - 1; + assert_eq!(term.damage.lines[7], LineDamageBounds { line: 7, left: 0, right }); + term.move_forward(Column(term.columns())); + term.damage.reset(num_cols); + + term.move_backward_tabs(1); + assert_eq!(term.damage.lines[7], LineDamageBounds { line: 7, left: 8, right }); + term.save_cursor_position(); + term.goto(Line(1), Column(1)); + term.damage.reset(num_cols); + + term.restore_cursor_position(); + assert_eq!(term.damage.lines[1], LineDamageBounds { line: 1, left: 1, right: 1 }); + assert_eq!(term.damage.lines[7], LineDamageBounds { line: 7, left: 8, right: 8 }); + term.damage.reset(num_cols); + + term.clear_line(ansi::LineClearMode::All); + assert_eq!(term.damage.lines[7], LineDamageBounds { line: 7, left: 0, right }); + term.damage.reset(num_cols); + + term.clear_line(ansi::LineClearMode::Left); + assert_eq!(term.damage.lines[7], LineDamageBounds { line: 7, left: 0, right: 8 }); + term.damage.reset(num_cols); + + term.clear_line(ansi::LineClearMode::Right); + assert_eq!(term.damage.lines[7], LineDamageBounds { line: 7, left: 8, right }); + term.damage.reset(num_cols); + + term.reverse_index(); + assert_eq!(term.damage.lines[7], LineDamageBounds { line: 7, left: 8, right: 8 }); + assert_eq!(term.damage.lines[6], LineDamageBounds { line: 6, left: 8, right: 8 }); + } + + #[test] + fn damage_vi_movements() { + let size = SizeInfo::new(10.0, 10.0, 1.0, 1.0, 0.0, 0.0, false); + let mut term = Term::new(&Config::default(), size, ()); + let num_cols = term.columns(); + // Reset terminal for partial damage tests since it's initialized as fully damaged. + term.reset_damage(); + + // Enable Vi mode. + term.toggle_vi_mode(); + + // NOTE While we can use `[Term::damage]` to access terminal damage information, in the + // following tests we will be accessing `term.damage.lines` directly to avoid adding extra + // damage information (like cursor and Vi cursor), which we're not testing. + + term.vi_goto_point(Point::new(Line(5), Column(5))); + assert_eq!(term.damage.lines[0], LineDamageBounds { line: 0, left: 0, right: 0 }); + assert_eq!(term.damage.lines[5], LineDamageBounds { line: 5, left: 5, right: 5 }); + term.damage.reset(num_cols); + + term.vi_motion(ViMotion::Up); + term.vi_motion(ViMotion::Right); + term.vi_motion(ViMotion::Up); + term.vi_motion(ViMotion::Left); + assert_eq!(term.damage.lines[3], LineDamageBounds { line: 3, left: 5, right: 6 }); + assert_eq!(term.damage.lines[4], LineDamageBounds { line: 4, left: 5, right: 6 }); + assert_eq!(term.damage.lines[5], LineDamageBounds { line: 5, left: 5, right: 5 }); + + // Ensure that we haven't damaged entire terminal during the test. + assert!(!term.damage.is_fully_damaged); + } + + #[test] + fn full_damage() { + let size = SizeInfo::new(100.0, 10.0, 1.0, 1.0, 0.0, 0.0, false); + let mut term = Term::new(&Config::default(), size, ()); + + assert!(term.damage.is_fully_damaged); + for _ in 0..20 { + term.newline(); + } + term.reset_damage(); + + term.clear_screen(ansi::ClearMode::Above); + assert!(term.damage.is_fully_damaged); + term.reset_damage(); + + term.scroll_display(Scroll::Top); + assert!(term.damage.is_fully_damaged); + term.reset_damage(); + + // Sequential call to scroll display without doing anything shouldn't damage. + term.scroll_display(Scroll::Top); + assert!(!term.damage.is_fully_damaged); + term.reset_damage(); + + term.update_config(&Config::default()); + assert!(term.damage.is_fully_damaged); + term.reset_damage(); + + term.scroll_down_relative(Line(5), 2); + assert!(term.damage.is_fully_damaged); + term.reset_damage(); + + term.scroll_up_relative(Line(3), 2); + assert!(term.damage.is_fully_damaged); + term.reset_damage(); + + term.deccolm(); + assert!(term.damage.is_fully_damaged); + term.reset_damage(); + + term.decaln(); + assert!(term.damage.is_fully_damaged); + term.reset_damage(); + + term.set_mode(ansi::Mode::Insert); + // Just setting `Insert` mode shouldn't mark terminal as damaged. + assert!(!term.damage.is_fully_damaged); + term.reset_damage(); + + // However requesting terminal damage should mark terminal as fully damaged in `Insert` + // mode. + let _ = term.damage(None); + assert!(term.damage.is_fully_damaged); + term.reset_damage(); + + term.unset_mode(ansi::Mode::Insert); + assert!(term.damage.is_fully_damaged); + term.reset_damage(); + + // Keep this as a last check, so we don't have to deal with restoring from alt-screen. + term.swap_alt(); + assert!(term.damage.is_fully_damaged); + term.reset_damage(); + + let size = SizeInfo::new(10.0, 10.0, 1.0, 1.0, 0.0, 0.0, false); + term.resize(size); + assert!(term.damage.is_fully_damaged); + } + + #[test] fn window_title() { let size = SizeInfo::new(21.0, 51.0, 3.0, 3.0, 0.0, 0.0, false); let mut term = Term::new(&Config::default(), size, ()); diff --git a/alacritty_terminal/src/vi_mode.rs b/alacritty_terminal/src/vi_mode.rs index de5c61b5..8a77b760 100644 --- a/alacritty_terminal/src/vi_mode.rs +++ b/alacritty_terminal/src/vi_mode.rs @@ -52,7 +52,7 @@ pub enum ViMotion { } /// Cursor tracking vi mode position. -#[derive(Default, Copy, Clone)] +#[derive(Default, Copy, Clone, PartialEq, Eq)] pub struct ViModeCursor { pub point: Point, } |