diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f558080..96573405 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -72,6 +72,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Cursor being hidden after reaching cursor blinking timeout - Message bar content getting stuck after closing with multiple messages on Wayland - Vi cursor position not redrawn on PageUp/PageDown without scrollback +- Cursor not updating when blinking and viewport is scrolled ### Removed diff --git a/alacritty/src/display/damage.rs b/alacritty/src/display/damage.rs index 82230dff..24033fa5 100644 --- a/alacritty/src/display/damage.rs +++ b/alacritty/src/display/damage.rs @@ -1,20 +1,196 @@ -use std::cmp; use std::iter::Peekable; +use std::{cmp, mem}; use glutin::surface::Rect; +use alacritty_terminal::index::Point; +use alacritty_terminal::selection::SelectionRange; use alacritty_terminal::term::{LineDamageBounds, TermDamageIterator}; use crate::display::SizeInfo; +/// State of the damage tracking for the [`Display`]. +/// +/// [`Display`]: crate::display::Display +#[derive(Debug)] +pub struct DamageTracker { + /// Position of the previously drawn Vi cursor. + pub old_vi_cursor: Option>, + /// The location of the old selection. + pub old_selection: Option, + /// Highlight damage submitted for the compositor. + pub debug: bool, + + /// The damage for the frames. + frames: [FrameDamage; 2], + screen_lines: usize, + columns: usize, +} + +impl DamageTracker { + pub fn new(screen_lines: usize, columns: usize) -> Self { + let mut tracker = Self { + columns, + screen_lines, + debug: false, + old_vi_cursor: None, + old_selection: None, + frames: Default::default(), + }; + tracker.resize(screen_lines, columns); + tracker + } + + #[inline] + #[must_use] + pub fn frame(&mut self) -> &mut FrameDamage { + &mut self.frames[0] + } + + #[inline] + #[must_use] + pub fn next_frame(&mut self) -> &mut FrameDamage { + &mut self.frames[1] + } + + /// Advance to the next frame resetting the state for the active frame. + #[inline] + pub fn swap_damage(&mut self) { + let screen_lines = self.screen_lines; + let columns = self.columns; + self.frame().reset(screen_lines, columns); + self.frames.swap(0, 1); + } + + /// Resize the damage information in the tracker. + pub fn resize(&mut self, screen_lines: usize, columns: usize) { + self.screen_lines = screen_lines; + self.columns = columns; + for frame in &mut self.frames { + frame.reset(screen_lines, columns); + } + self.frame().full = true; + } + + /// Damage vi cursor inside the viewport. + pub fn damage_vi_cursor(&mut self, mut vi_cursor: Option>) { + mem::swap(&mut self.old_vi_cursor, &mut vi_cursor); + + if self.frame().full { + return; + } + + if let Some(vi_cursor) = self.old_vi_cursor { + self.frame().damage_point(vi_cursor); + } + + if let Some(vi_cursor) = vi_cursor { + self.frame().damage_point(vi_cursor); + } + } + + /// Get shaped frame damage for the active frame. + pub fn shape_frame_damage(&self, size_info: SizeInfo) -> Vec { + if self.frames[0].full { + vec![Rect::new(0, 0, size_info.width() as i32, size_info.height() as i32)] + } else { + let lines_damage = RenderDamageIterator::new( + TermDamageIterator::new(&self.frames[0].lines, 0), + &size_info, + ); + lines_damage.chain(self.frames[0].rects.iter().copied()).collect() + } + } + + /// Add the current frame's selection damage. + pub fn damage_selection( + &mut self, + mut selection: Option, + display_offset: usize, + ) { + mem::swap(&mut self.old_selection, &mut selection); + + if self.frame().full || selection == self.old_selection { + return; + } + + for selection in self.old_selection.into_iter().chain(selection) { + let display_offset = display_offset as i32; + let last_visible_line = self.screen_lines as i32 - 1; + let columns = self.columns; + + // Ignore invisible selection. + if selection.end.line.0 + display_offset < 0 + || selection.start.line.0.abs() < display_offset - last_visible_line + { + continue; + }; + + let start = cmp::max(selection.start.line.0 + display_offset, 0) as usize; + let end = (selection.end.line.0 + display_offset).clamp(0, last_visible_line) as usize; + for line in start..=end { + self.frame().lines[line].expand(0, columns - 1); + } + } + } +} + +/// Damage state for the rendering frame. +#[derive(Debug, Default)] +pub struct FrameDamage { + /// The entire frame needs to be redrawn. + full: bool, + /// Terminal lines damaged in the given frame. + lines: Vec, + /// Rectangular regions damage in the given frame. + rects: Vec, +} + +impl FrameDamage { + /// Damage line for the given frame. + #[inline] + pub fn damage_line(&mut self, damage: LineDamageBounds) { + self.lines[damage.line].expand(damage.left, damage.right); + } + + #[inline] + pub fn damage_point(&mut self, point: Point) { + self.lines[point.line].expand(point.column.0, point.column.0); + } + + /// Mark the frame as fully damaged. + #[inline] + pub fn mark_fully_damaged(&mut self) { + self.full = true; + } + + /// Add a damage rectangle. + /// + /// This allows covering elements outside of the terminal viewport, like message bar. + #[inline] + pub fn add_rect(&mut self, x: i32, y: i32, width: i32, height: i32) { + self.rects.push(Rect { x, y, width, height }); + } + + fn reset(&mut self, num_lines: usize, num_cols: usize) { + self.full = false; + self.rects.clear(); + self.lines.clear(); + self.lines.reserve(num_lines); + for line in 0..num_lines { + self.lines.push(LineDamageBounds::undamaged(line, num_cols)); + } + } +} + /// Iterator which converts `alacritty_terminal` damage information into renderer damaged rects. -pub struct RenderDamageIterator<'a> { +struct RenderDamageIterator<'a> { damaged_lines: Peekable>, - size_info: SizeInfo, + size_info: &'a SizeInfo, } impl<'a> RenderDamageIterator<'a> { - pub fn new(damaged_lines: TermDamageIterator<'a>, size_info: SizeInfo) -> Self { + pub fn new(damaged_lines: TermDamageIterator<'a>, size_info: &'a SizeInfo) -> Self { Self { damaged_lines: damaged_lines.peekable(), size_info } } diff --git a/alacritty/src/display/meter.rs b/alacritty/src/display/meter.rs index 9ccfe52d..e263100f 100644 --- a/alacritty/src/display/meter.rs +++ b/alacritty/src/display/meter.rs @@ -64,11 +64,6 @@ impl<'a> Drop for Sampler<'a> { } impl Meter { - /// Create a meter. - pub fn new() -> Meter { - Default::default() - } - /// Get a sampler. pub fn sampler(&mut self) -> Sampler<'_> { Sampler::new(self) diff --git a/alacritty/src/display/mod.rs b/alacritty/src/display/mod.rs index 878f03e1..28c91cdb 100644 --- a/alacritty/src/display/mod.rs +++ b/alacritty/src/display/mod.rs @@ -10,7 +10,7 @@ use std::time::{Duration, Instant}; use glutin::context::{NotCurrentContext, PossiblyCurrentContext}; use glutin::prelude::*; -use glutin::surface::{Rect as DamageRect, Surface, SwapInterval, WindowSurface}; +use glutin::surface::{Surface, SwapInterval, WindowSurface}; use log::{debug, info}; use parking_lot::MutexGuard; @@ -26,13 +26,15 @@ use unicode_width::UnicodeWidthChar; use alacritty_terminal::event::{EventListener, OnResize, WindowSize}; use alacritty_terminal::grid::Dimensions as TermDimensions; use alacritty_terminal::index::{Column, Direction, Line, Point}; -use alacritty_terminal::selection::{Selection, SelectionRange}; +use alacritty_terminal::selection::Selection; use alacritty_terminal::term::cell::Flags; -use alacritty_terminal::term::{self, Term, TermDamage, TermMode, MIN_COLUMNS, MIN_SCREEN_LINES}; +use alacritty_terminal::term::{ + self, point_to_viewport, LineDamageBounds, Term, TermDamage, TermMode, MIN_COLUMNS, + MIN_SCREEN_LINES, +}; use alacritty_terminal::vte::ansi::{CursorShape, NamedColor}; use crate::config::font::Font; -use crate::config::scrolling::MAX_SCROLLBACK_LINES; use crate::config::window::Dimensions; #[cfg(not(windows))] use crate::config::window::StartupMode; @@ -41,7 +43,7 @@ use crate::display::bell::VisualBell; use crate::display::color::{List, Rgb}; use crate::display::content::{RenderableContent, RenderableCursor}; use crate::display::cursor::IntoRects; -use crate::display::damage::RenderDamageIterator; +use crate::display::damage::DamageTracker; use crate::display::hint::{HintMatch, HintState}; use crate::display::meter::Meter; use crate::display::window::Window; @@ -370,6 +372,9 @@ pub struct Display { /// The state of the timer for frame scheduling. pub frame_timer: FrameTimer, + /// Damage tracker for the given display. + pub damage_tracker: DamageTracker, + // Mouse point position when highlighting hints. hint_mouse_point: Option, @@ -379,9 +384,6 @@ pub struct Display { context: ManuallyDrop>, - debug_damage: bool, - damage_rects: Vec, - next_frame_damage_rects: Vec, glyph_cache: GlyphCache, meter: Meter, } @@ -487,13 +489,8 @@ impl Display { let hint_state = HintState::new(config.hints.alphabet()); - let debug_damage = config.debug.highlight_damage; - let (damage_rects, next_frame_damage_rects) = if is_wayland || debug_damage { - let vec = Vec::with_capacity(size_info.screen_lines()); - (vec.clone(), vec) - } else { - (Vec::new(), Vec::new()) - }; + let mut damage_tracker = DamageTracker::new(size_info.screen_lines(), size_info.columns()); + damage_tracker.debug = config.debug.highlight_damage; // Disable vsync. if let Err(err) = surface.set_swap_interval(&context, SwapInterval::DontWait) { @@ -501,28 +498,26 @@ impl Display { } Ok(Self { - window, context: ManuallyDrop::new(Replaceable::new(context)), - surface: ManuallyDrop::new(surface), + visual_bell: VisualBell::from(&config.bell), renderer: ManuallyDrop::new(renderer), + surface: ManuallyDrop::new(surface), + colors: List::from(&config.colors), + frame_timer: FrameTimer::new(), + raw_window_handle, + damage_tracker, glyph_cache, hint_state, - meter: Meter::new(), size_info, - ime: Ime::new(), - highlighted_hint: None, - vi_highlighted_hint: None, - cursor_hidden: false, - frame_timer: FrameTimer::new(), - visual_bell: VisualBell::from(&config.bell), - colors: List::from(&config.colors), - pending_update: Default::default(), + window, pending_renderer_update: Default::default(), - debug_damage, - damage_rects, - raw_window_handle, - next_frame_damage_rects, - hint_mouse_point: None, + vi_highlighted_hint: Default::default(), + highlighted_hint: Default::default(), + hint_mouse_point: Default::default(), + pending_update: Default::default(), + cursor_hidden: Default::default(), + meter: Default::default(), + ime: Default::default(), }) } @@ -554,9 +549,10 @@ impl Display { #[cfg(not(any(target_os = "macos", windows)))] (Surface::Egl(surface), PossiblyCurrentContext::Egl(context)) if matches!(self.raw_window_handle, RawWindowHandle::Wayland(_)) - && !self.debug_damage => + && !self.damage_tracker.debug => { - surface.swap_buffers_with_damage(context, &self.damage_rects) + let damage = self.damage_tracker.shape_frame_damage(self.size_info.into()); + surface.swap_buffers_with_damage(context, &damage) }, (surface, context) => surface.swap_buffers(context), }; @@ -652,11 +648,19 @@ impl Display { self.window.set_resize_increments(PhysicalSize::new(cell_width, cell_height)); } - // Resize PTY. - pty_resize_handle.on_resize(new_size.into()); + // Resize when terminal when its dimensions have changed. + if self.size_info.screen_lines() != new_size.screen_lines + || self.size_info.columns() != new_size.columns() + { + // Resize PTY. + pty_resize_handle.on_resize(new_size.into()); - // Resize terminal. - terminal.resize(new_size); + // Resize terminal. + terminal.resize(new_size); + + // Resize damage tracking. + self.damage_tracker.resize(new_size.screen_lines(), new_size.columns()); + } // Check if dimensions have changed. if new_size != self.size_info { @@ -697,62 +701,8 @@ impl Display { 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()); - - // Damage the entire screen after processing update. - self.fully_damage(); - } - - /// Damage the entire window. - fn fully_damage(&mut self) { - let screen_rect = - DamageRect::new(0, 0, self.size_info.width() as i32, self.size_info.height() as i32); - - self.damage_rects.push(screen_rect); - } - - 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); - match terminal.damage(selection_range) { - TermDamage::Full => self.fully_damage(), - TermDamage::Partial(damaged_lines) => { - let damaged_rects = RenderDamageIterator::new(damaged_lines, self.size_info.into()); - 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. @@ -788,13 +738,40 @@ impl Display { let vi_mode = terminal.mode().contains(TermMode::VI); let vi_cursor_point = if vi_mode { Some(terminal.vi_mode_cursor.point) } else { None }; + // Add damage from the terminal. if self.collect_damage() { - self.update_damage(&mut terminal, selection_range, search_state); + match terminal.damage() { + TermDamage::Full => self.damage_tracker.frame().mark_fully_damaged(), + TermDamage::Partial(damaged_lines) => { + for damage in damaged_lines { + self.damage_tracker.frame().damage_line(damage); + } + }, + } + terminal.reset_damage(); } // Drop terminal as early as possible to free lock. drop(terminal); + // Add damage from alacritty's UI elements overlapping terminal. + if self.collect_damage() { + let requires_full_damage = self.visual_bell.intensity() != 0. + || self.hint_state.active() + || search_state.regex().is_some(); + + if requires_full_damage { + self.damage_tracker.frame().mark_fully_damaged(); + self.damage_tracker.next_frame().mark_fully_damaged(); + } + + let vi_cursor_viewport_point = + vi_cursor_point.and_then(|cursor| point_to_viewport(display_offset, cursor)); + + self.damage_tracker.damage_vi_cursor(vi_cursor_viewport_point); + self.damage_tracker.damage_selection(selection_range, display_offset); + } + // Make sure this window's OpenGL context is active. self.make_current(); @@ -816,6 +793,7 @@ impl Display { let glyph_cache = &mut self.glyph_cache; let highlighted_hint = &self.highlighted_hint; let vi_highlighted_hint = &self.vi_highlighted_hint; + let damage_tracker = &mut self.damage_tracker; self.renderer.draw_cells( &size_info, @@ -835,6 +813,9 @@ impl Display { .map_or(false, |hint| hint.should_highlight(point, hyperlink)) { cell.flags.insert(Flags::UNDERLINE); + // Damage hints for the current and next frames. + damage_tracker.frame().damage_point(cell.point); + damage_tracker.next_frame().damage_point(cell.point); } } @@ -924,10 +905,6 @@ impl Display { } } - if self.debug_damage { - self.highlight_damage(&mut rects); - } - if let Some(message) = message_buffer.message() { let search_offset = usize::from(search_state.regex().is_some()); let text = message.text(&size_info); @@ -951,7 +928,7 @@ impl Display { rects.push(message_bar_rect); // Always damage message bar, since it could have messages of the same size in it. - self.damage_rects.push(DamageRect { x, y: y as i32, width, height }); + self.damage_tracker.frame().add_rect(x, y as i32, width, height); // Draw rectangles. self.renderer.draw_rects(&size_info, &metrics, rects); @@ -986,6 +963,14 @@ impl Display { // Notify winit that we're about to present. self.window.pre_present_notify(); + // Highlight damage for debugging. + if self.damage_tracker.debug { + let damage = self.damage_tracker.shape_frame_damage(self.size_info.into()); + let mut rects = Vec::with_capacity(damage.len()); + self.highlight_damage(&mut rects); + self.renderer.draw_rects(&self.size_info, &metrics, rects); + } + // Clearing debug highlights from the previous frame requires full redraw. self.swap_buffers(); @@ -1002,15 +987,12 @@ impl Display { self.request_frame(scheduler); } - self.damage_rects.clear(); - - // Append damage rects we've enqueued for the next frame. - mem::swap(&mut self.damage_rects, &mut self.next_frame_damage_rects); + self.damage_tracker.swap_damage(); } /// Update to a new configuration. pub fn update_config(&mut self, config: &UiConfig) { - self.debug_damage = config.debug.highlight_damage; + self.damage_tracker.debug = config.debug.highlight_damage; self.visual_bell.update_config(&config.bell); self.colors = List::from(&config.colors); } @@ -1123,10 +1105,11 @@ impl Display { glyph_cache, ); - if self.collect_damage() { - let damage = self.damage_from_point(Point::new(start.line, Column(0)), num_cols as u32); - self.damage_rects.push(damage); - self.next_frame_damage_rects.push(damage); + // Damage preedit inside the terminal viewport. + if self.collect_damage() && point.line < self.size_info.screen_lines() { + let damage = LineDamageBounds::new(start.line, 0, num_cols); + self.damage_tracker.frame().damage_line(damage); + self.damage_tracker.next_frame().damage_line(damage); } // Add underline for preedit text. @@ -1235,11 +1218,11 @@ impl Display { for (uri, point) in uris.into_iter().zip(uri_lines) { // Damage the uri preview. if self.collect_damage() { - let uri_preview_damage = self.damage_from_point(point, num_cols as u32); - self.damage_rects.push(uri_preview_damage); + let damage = LineDamageBounds::new(point.line, point.column.0, num_cols); + self.damage_tracker.frame().damage_line(damage); // Damage the uri preview for the next frame as well. - self.next_frame_damage_rects.push(uri_preview_damage); + self.damage_tracker.next_frame().damage_line(damage); } self.renderer.draw_string(point, fg, bg, uri, &self.size_info, &mut self.glyph_cache); @@ -1281,13 +1264,10 @@ impl Display { let bg = config.colors.normal.red; if self.collect_damage() { - // Damage the entire line. - let render_timer_damage = - self.damage_from_point(point, self.size_info.columns() as u32); - self.damage_rects.push(render_timer_damage); - + let damage = LineDamageBounds::new(point.line, point.column.0, timing.len()); + self.damage_tracker.frame().damage_line(damage); // Damage the render timer for the next frame. - self.next_frame_damage_rects.push(render_timer_damage) + self.damage_tracker.next_frame().damage_line(damage); } let glyph_cache = &mut self.glyph_cache; @@ -1303,27 +1283,16 @@ 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 columns = self.size_info.columns(); let text = format!("[{}/{}]", line, total_lines - 1); let column = Column(self.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_SIZE: usize = 2 * num_digits(MAX_SCROLLBACK_LINES) + 3; - let damage_point = Point::new(0, Column(self.size_info.columns().saturating_sub(MAX_SIZE))); if self.collect_damage() { - self.damage_rects.push(self.damage_from_point(damage_point, MAX_SIZE as u32)); + let damage = LineDamageBounds::new(point.line, point.column.0, columns - 1); + self.damage_tracker.frame().damage_line(damage); + // Damage it on the next frame in case it goes away. + self.damage_tracker.next_frame().damage_line(damage); } let colors = &config.colors; @@ -1337,46 +1306,17 @@ impl Display { } } - /// Damage `len` starting from a `point`. - /// - /// This method also enqueues damage for the next frame automatically. - fn damage_from_point(&self, point: Point, len: u32) -> DamageRect { - 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 * size_info.cell_width(); - DamageRect::new(x as i32, y as i32, width as i32, size_info.cell_height() as i32) - } - - /// Damage currently highlighted `Display` hints. - #[inline] - fn damage_highlighted_hints(&self, terminal: &mut Term) { - let display_offset = terminal.grid().display_offset(); - let last_visible_line = terminal.screen_lines() - 1; - 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| { - term::point_to_viewport(display_offset, Point::new(Line(line), Column(0))) - .filter(|point| point.line <= last_visible_line) - }) - { - 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 { - matches!(self.raw_window_handle, RawWindowHandle::Wayland(_)) || self.debug_damage + matches!(self.raw_window_handle, RawWindowHandle::Wayland(_)) || self.damage_tracker.debug } /// 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 { + for damage_rect in &self.damage_tracker.shape_frame_damage(self.size_info.into()) { let x = damage_rect.x as f32; let height = damage_rect.height as f32; let width = damage_rect.width as f32; @@ -1438,10 +1378,6 @@ pub struct Ime { } impl Ime { - pub fn new() -> Self { - Default::default() - } - #[inline] pub fn set_enabled(&mut self, is_enabled: bool) { if is_enabled { diff --git a/alacritty/src/event.rs b/alacritty/src/event.rs index e6f77e8c..f19aa2f9 100644 --- a/alacritty/src/event.rs +++ b/alacritty/src/event.rs @@ -517,7 +517,7 @@ impl<'a, N: Notify + 'a, T: EventListener> input::ActionContext for ActionCon // Enable IME so we can input into the search bar with it if we were in Vi mode. self.window().set_ime_allowed(true); - self.terminal.mark_fully_damaged(); + self.display.damage_tracker.frame().mark_fully_damaged(); self.display.pending_update.dirty = true; } @@ -853,10 +853,7 @@ impl<'a, N: Notify + 'a, T: EventListener> input::ActionContext for ActionCon // If we had search running when leaving Vi mode we should mark terminal fully damaged // to cleanup highlighted results. if self.search_state.dfas.take().is_some() { - self.terminal.mark_fully_damaged(); - } else { - // Damage line indicator. - self.terminal.damage_line(0, 0, self.terminal.columns() - 1); + self.display.damage_tracker.frame().mark_fully_damaged(); } } else { self.clear_selection(); @@ -1029,7 +1026,7 @@ impl<'a, N: Notify + 'a, T: EventListener> ActionContext<'a, N, T> { let vi_mode = self.terminal.mode().contains(TermMode::VI); self.window().set_ime_allowed(!vi_mode); - self.terminal.mark_fully_damaged(); + self.display.damage_tracker.frame().mark_fully_damaged(); self.display.pending_update.dirty = true; self.search_state.history_index = None; diff --git a/alacritty_terminal/src/term/mod.rs b/alacritty_terminal/src/term/mod.rs index 3df1d128..f56fa605 100644 --- a/alacritty_terminal/src/term/mod.rs +++ b/alacritty_terminal/src/term/mod.rs @@ -109,6 +109,11 @@ pub struct LineDamageBounds { } impl LineDamageBounds { + #[inline] + pub fn new(line: usize, left: usize, right: usize) -> Self { + Self { line, left, right } + } + #[inline] pub fn undamaged(line: usize, num_cols: usize) -> Self { Self { line, left: num_cols, right: 0 } @@ -141,15 +146,19 @@ pub enum TermDamage<'a> { Partial(TermDamageIterator<'a>), } -/// Iterator over the terminal's damaged lines. +/// Iterator over the terminal's viewport damaged lines. #[derive(Clone, Debug)] pub struct TermDamageIterator<'a> { line_damage: slice::Iter<'a, LineDamageBounds>, + display_offset: usize, } impl<'a> TermDamageIterator<'a> { - fn new(line_damage: &'a [LineDamageBounds]) -> Self { - Self { line_damage: line_damage.iter() } + pub fn new(line_damage: &'a [LineDamageBounds], display_offset: usize) -> Self { + let num_lines = line_damage.len(); + // Filter out invisible damage. + let line_damage = &line_damage[..num_lines.saturating_sub(display_offset)]; + Self { display_offset, line_damage: line_damage.iter() } } } @@ -157,26 +166,26 @@ impl<'a> Iterator for TermDamageIterator<'a> { type Item = LineDamageBounds; fn next(&mut self) -> Option { - self.line_damage.find(|line| line.is_damaged()).copied() + self.line_damage.find_map(|line| { + line.is_damaged().then_some(LineDamageBounds::new( + line.line + self.display_offset, + line.left, + line.right, + )) + }) } } /// State of the terminal damage. struct TermDamageState { /// Hint whether terminal should be damaged entirely regardless of the actual damage changes. - is_fully_damaged: bool, + full: bool, /// Information about damage on terminal lines. lines: Vec, /// Old terminal cursor point. last_cursor: Point, - - /// Last Vi cursor point. - last_vi_cursor_point: Option>, - - /// Old selection range. - last_selection: Option, } impl TermDamageState { @@ -184,22 +193,14 @@ impl TermDamageState { let lines = (0..num_lines).map(|line| LineDamageBounds::undamaged(line, num_cols)).collect(); - Self { - is_fully_damaged: true, - lines, - last_cursor: Default::default(), - last_vi_cursor_point: Default::default(), - last_selection: Default::default(), - } + Self { full: true, lines, last_cursor: Default::default() } } #[inline] 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_vi_cursor_point = None; - self.last_selection = None; - self.is_fully_damaged = true; + self.full = true; self.lines.clear(); self.lines.reserve(num_lines); @@ -220,32 +221,9 @@ impl TermDamageState { 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 = (selection.end.line.0 + display_offset).clamp(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.full = false; self.lines.iter_mut().for_each(|line| line.reset(num_cols)); } } @@ -417,30 +395,27 @@ impl Term { } } + /// Collect the information about the changes in the lines, which + /// could be used to minimize the amount of drawing operations. + /// + /// The user controlled elements, like `Vi` mode cursor and `Selection` are **not** part of the + /// collected damage state. Those could easily be tracked by comparing their old and new + /// value between adjacent frames. + /// + /// After reading damage [`reset_damage`] should be called. + /// + /// [`reset_damage`]: Self::reset_damage #[must_use] - pub fn damage(&mut self, selection: Option) -> TermDamage<'_> { + pub fn damage(&mut self) -> 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(); } - // Update tracking of cursor, selection, and vi mode cursor. - - let display_offset = self.grid().display_offset(); - let vi_cursor_point = if self.mode.contains(TermMode::VI) { - point_to_viewport(display_offset, self.vi_mode_cursor.point) - } else { - None - }; - let previous_cursor = mem::replace(&mut self.damage.last_cursor, self.grid.cursor.point); - let previous_selection = mem::replace(&mut self.damage.last_selection, selection); - let previous_vi_cursor_point = - mem::replace(&mut self.damage.last_vi_cursor_point, vi_cursor_point); - // Early return if the entire terminal is damaged. - if self.damage.is_fully_damaged { + if self.damage.full { return TermDamage::Full; } @@ -455,24 +430,10 @@ impl Term { // Always damage current cursor. self.damage_cursor(); - // Vi mode doesn't update the terminal content, thus only last vi cursor position and the - // new one should be damaged. - if let Some(previous_vi_cursor_point) = previous_vi_cursor_point { - self.damage.damage_point(previous_vi_cursor_point) - } - - // Damage Vi cursor if it's present. - if let Some(vi_cursor_point) = self.damage.last_vi_cursor_point { - self.damage.damage_point(vi_cursor_point); - } - - if self.damage.last_selection != previous_selection { - for selection in self.damage.last_selection.into_iter().chain(previous_selection) { - self.damage.damage_selection(selection, display_offset, self.columns()); - } - } - - TermDamage::Partial(TermDamageIterator::new(&self.damage.lines)) + // NOTE: damage which changes all the content when the display offset is non-zero (e.g. + // scrolling) is handled via full damage. + let display_offset = self.grid().display_offset(); + TermDamage::Partial(TermDamageIterator::new(&self.damage.lines, display_offset)) } /// Resets the terminal damage information. @@ -481,14 +442,8 @@ impl Term { } #[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); + fn mark_fully_damaged(&mut self) { + self.damage.full = true; } /// Set new options for the [`Term`]. @@ -1323,7 +1278,7 @@ impl Handler for Term { trace!("Carriage return"); 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.damage.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; } @@ -1491,7 +1446,7 @@ impl Handler for Term { } let line = self.grid.cursor.point.line.0 as usize; - self.damage_line(line, self.grid.cursor.point.column.0, old_col); + self.damage.damage_line(line, self.grid.cursor.point.column.0, old_col); } #[inline] @@ -2888,7 +2843,7 @@ mod tests { term.input('e'); let right = term.grid.cursor.point.column.0; - let mut damaged_lines = match term.damage(None) { + let mut damaged_lines = match term.damage() { TermDamage::Full => panic!("Expected partial damage, however got Full"), TermDamage::Partial(damaged_lines) => damaged_lines, }; @@ -2896,70 +2851,51 @@ mod tests { assert_eq!(damaged_lines.next(), None); term.reset_damage(); - // Check that selection we've passed was properly damaged. + // Create scrollback. + for _ in 0..20 { + term.newline(); + } - 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); + match term.damage() { + TermDamage::Full => (), + TermDamage::Partial(_) => panic!("Expected Full damage, however got Partial "), + }; + term.reset_damage(); - let mut damaged_lines = match term.damage(selection_range) { + term.scroll_display(Scroll::Delta(10)); + term.reset_damage(); + + // No damage when scrolled into viewport. + for idx in 0..term.columns() { + term.goto(idx as i32, idx); + } + let mut damaged_lines = match term.damage() { 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; - 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); - // Ensure that old Vi cursor got damaged as well. + // Scroll back into the viewport, so we have 2 visible lines which terminal can write + // to. + term.scroll_display(Scroll::Delta(-2)); term.reset_damage(); - term.toggle_vi_mode(); - let mut damaged_lines = match term.damage(None) { + + term.goto(0, 0); + term.goto(1, 0); + term.goto(2, 0); + let display_offset = term.grid().display_offset(); + let mut damaged_lines = match term.damage() { 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(), + Some(LineDamageBounds { line: display_offset, left: 0, right: 0 }) + ); + assert_eq!( + damaged_lines.next(), + Some(LineDamageBounds { line: display_offset + 1, left: 0, right: 0 }) + ); assert_eq!(damaged_lines.next(), None); } @@ -3066,85 +3002,85 @@ mod tests { let size = TermSize::new(100, 10); let mut term = Term::new(Config::default(), &size, VoidListener); - assert!(term.damage.is_fully_damaged); + assert!(term.damage.full); for _ in 0..20 { term.newline(); } term.reset_damage(); term.clear_screen(ansi::ClearMode::Above); - assert!(term.damage.is_fully_damaged); + assert!(term.damage.full); term.reset_damage(); term.scroll_display(Scroll::Top); - assert!(term.damage.is_fully_damaged); + assert!(term.damage.full); 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); + assert!(!term.damage.full); term.reset_damage(); term.set_options(Config::default()); - assert!(term.damage.is_fully_damaged); + assert!(term.damage.full); term.reset_damage(); term.scroll_down_relative(Line(5), 2); - assert!(term.damage.is_fully_damaged); + assert!(term.damage.full); term.reset_damage(); term.scroll_up_relative(Line(3), 2); - assert!(term.damage.is_fully_damaged); + assert!(term.damage.full); term.reset_damage(); term.deccolm(); - assert!(term.damage.is_fully_damaged); + assert!(term.damage.full); term.reset_damage(); term.decaln(); - assert!(term.damage.is_fully_damaged); + assert!(term.damage.full); term.reset_damage(); term.set_mode(NamedMode::Insert.into()); // Just setting `Insert` mode shouldn't mark terminal as damaged. - assert!(!term.damage.is_fully_damaged); + assert!(!term.damage.full); term.reset_damage(); let color_index = 257; term.set_color(color_index, Rgb::default()); - assert!(term.damage.is_fully_damaged); + assert!(term.damage.full); term.reset_damage(); // Setting the same color once again shouldn't trigger full damage. term.set_color(color_index, Rgb::default()); - assert!(!term.damage.is_fully_damaged); + assert!(!term.damage.full); term.reset_color(color_index); - assert!(term.damage.is_fully_damaged); + assert!(term.damage.full); term.reset_damage(); // We shouldn't trigger fully damage when cursor gets update. term.set_color(NamedColor::Cursor as usize, Rgb::default()); - assert!(!term.damage.is_fully_damaged); + assert!(!term.damage.full); // However requesting terminal damage should mark terminal as fully damaged in `Insert` // mode. - let _ = term.damage(None); - assert!(term.damage.is_fully_damaged); + let _ = term.damage(); + assert!(term.damage.full); term.reset_damage(); term.unset_mode(NamedMode::Insert.into()); - assert!(term.damage.is_fully_damaged); + assert!(term.damage.full); 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); + assert!(term.damage.full); term.reset_damage(); let size = TermSize::new(10, 10); term.resize(size); - assert!(term.damage.is_fully_damaged); + assert!(term.damage.full); } #[test]