From 8f1abe13e6b80da181ee856e6d5a19c7731dbedc Mon Sep 17 00:00:00 2001 From: Kirill Chibisov Date: Wed, 2 Feb 2022 00:12:58 +0300 Subject: [PATCH] Add damage tracking and reporting to compatible compositors This allows compositors to only process damaged (that is, updated) regions of our window buffer, which for larger window sizes (think 4k) should significantly reduce compositing workload under compositors that support/honor it, which is good for performance, battery life and lower latency over remote connections like VNC. On Wayland, clients are expected to always report correct damage, so this makes us a good citizen there. It can also aid remote desktop (waypipe, rdp, vnc, ...) and other types of screencopy by having damage bubble up correctly. Fixes #3186. --- CHANGELOG.md | 3 +- Cargo.lock | 2 +- alacritty.yml | 3 + alacritty/Cargo.toml | 2 +- alacritty/src/config/debug.rs | 4 + alacritty/src/display/content.rs | 53 +- alacritty/src/display/damage.rs | 86 +++ alacritty/src/display/meter.rs | 2 +- alacritty/src/display/mod.rs | 185 ++++- alacritty/src/display/window.rs | 10 +- alacritty/src/event.rs | 6 +- alacritty/src/input.rs | 4 +- alacritty/src/renderer/builtin_font.rs | 2 +- alacritty_terminal/Cargo.toml | 2 +- alacritty_terminal/src/config/mod.rs | 2 +- alacritty_terminal/src/config/scrolling.rs | 2 +- alacritty_terminal/src/selection.rs | 1 + alacritty_terminal/src/term/mod.rs | 755 ++++++++++++++++++--- alacritty_terminal/src/vi_mode.rs | 2 +- 19 files changed, 991 insertions(+), 135 deletions(-) create mode 100644 alacritty/src/display/damage.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index edcdc07e320..90995deda7d 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 diff --git a/Cargo.lock b/Cargo.lock index 6936b448d43..2274c25ccd8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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 c9959497a4b..85de5701a7d 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 ef46eb2c264..b1438c23611 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 f52cdf900fa..3fa987a5dd6 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 72d79f7ee2b..3b5499920ac 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, + cursor: RenderableCursor, cursor_shape: CursorShape, cursor_point: Point, search: Option>, @@ -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 { + 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 { + 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 { - 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) { @@ -371,6 +369,17 @@ pub struct RenderableCursor { point: Point, } +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 00000000000..d6a69a2d355 --- /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>, + size_info: SizeInfo, +} + +impl<'a> RenderDamageIterator<'a> { + pub fn new(damaged_lines: TermDamageIterator<'a>, size_info: SizeInfo) -> 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 { + 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 c07d901fb48..9ccfe52d8b9 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 d9ec8593887..7d53e678d08 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, 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( + &mut self, + terminal: &mut MutexGuard<'_, Term>, + selection_range: Option, + 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 = 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( &mut self, - terminal: MutexGuard<'_, Term>, + mut terminal: MutexGuard<'_, Term>, 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, 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, len: u32) { + if !self.collect_damage() { + return; + } + + let size_info: SizeInfo = 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(&self, terminal: &mut Term) { + 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) { + 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> { 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) -> 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 493e5ef9c69..712b4ac9601 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) { self.windowed_context.resize(size); } diff --git a/alacritty/src/event.rs b/alacritty/src/event.rs index aea6010d898..8bd1dec75b6 100644 --- a/alacritty/src/event.rs +++ b/alacritty/src/event.rs @@ -768,7 +768,11 @@ impl<'a, N: Notify + 'a, T: EventListener> input::ActionContext 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 0aa2cbba9a4..51bd3fc56cf 100644 --- a/alacritty/src/input.rs +++ b/alacritty/src/input.rs @@ -801,7 +801,9 @@ impl> Processor { 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 f3dbe9bb4ad..057984667f5 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 be8d17dda49..269b110ab59 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 ", "Joe Wilm "] 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 09161e033d7..e99c37b5296 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 159b0f444f0..9a5a718c84a 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 f00622d1044..669db6a2740 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 3fa57f7f111..7b0667e2717 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 { /// 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> for SizeInfo { + fn from(size_info: SizeInfo) -> 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 SizeInfo { + #[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 { #[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.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, + + /// Old terminal cursor point. + last_cursor: Point, + + /// Old selection range. + last_selection: Option, +} + +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) { + 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 { /// Information about cell dimensions. cell_width: usize, cell_height: usize, + + /// Information about damaged cells. + damage: TermDamageState, } impl Term { @@ -277,6 +445,7 @@ impl Term { 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 Term { 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 { @@ -300,6 +474,9 @@ impl Term { 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 Term { 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) -> 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 Term { } 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 Term { 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 Term { // 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 Term { 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 Term { 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 Term { 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 Term { // 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 Term { 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 Term { 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 Term { // Clear grid. self.grid.reset_region(..); + self.mark_fully_damaged(); } #[inline] @@ -659,7 +908,9 @@ impl Term { } // 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 Term { where T: EventListener, { + self.damage_vi_cursor(); // Move viewport to make point visible. self.scroll_to_point(point); @@ -676,6 +928,7 @@ impl Term { 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 Term { 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 Term { } } + 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 Term { 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 Term { 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 Handler for Term { cell.c = 'E'; } } + + self.mark_fully_damaged(); } #[inline] @@ -944,8 +1218,10 @@ impl Handler for Term { (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 Handler for Term { 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 Handler for Term { #[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 Handler for Term { 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 Handler for Term { #[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 Handler for Term { 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 Handler for Term { #[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 Handler for Term { 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 Handler for Term { 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 Handler for Term { #[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 Handler for Term { } 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 Handler for Term { 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 Handler for Term { 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 Handler for Term { } // 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 Handler for Term { // We have no history to clear. ansi::ClearMode::Saved => (), } + + self.mark_fully_damaged(); } #[inline] @@ -1491,6 +1789,7 @@ impl Handler for Term { self.mode.insert(TermMode::default()); self.event_proxy.send_event(Event::CursorBlinkingChange); + self.mark_fully_damaged(); } #[inline] @@ -1500,7 +1799,9 @@ impl Handler for Term { 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 Handler for Term { 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 Handler for Term { 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 for TabStops { } /// Terminal cursor rendering information. -#[derive(Copy, Clone)] +#[derive(Copy, Clone, PartialEq, Eq)] pub struct RenderableCursor { pub shape: CursorShape, pub point: Point, @@ -2429,6 +2733,285 @@ mod tests { assert_eq!(term.grid.cursor.point, Point::new(Line(4), Column(0))); } + #[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); diff --git a/alacritty_terminal/src/vi_mode.rs b/alacritty_terminal/src/vi_mode.rs index de5c61b5bed..8a77b760df9 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, }