Repository: alacritty/alacritty (language: rust)

Issue:
Cursor is not damaged when blinking and scrolled up



Discussion / hints:

Fix patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0f558080b59..96573405d18 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 82230dff11b..24033fa54c6 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<Point<usize>>,
+    /// The location of the old selection.
+    pub old_selection: Option<SelectionRange>,
+    /// 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<Point<usize>>) {
+        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<u32>) -> Vec<Rect> {
+        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<SelectionRange>,
+        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<LineDamageBounds>,
+    /// Rectangular regions damage in the given frame.
+    rects: Vec<Rect>,
+}
+
+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<usize>) {
+        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<TermDamageIterator<'a>>,
-    size_info: SizeInfo<u32>,
+    size_info: &'a SizeInfo<u32>,
 }
 
 impl<'a> RenderDamageIterator<'a> {
-    pub fn new(damaged_lines: TermDamageIterator<'a>, size_info: SizeInfo<u32>) -> Self {
+    pub fn new(damaged_lines: TermDamageIterator<'a>, size_info: &'a SizeInfo<u32>) -> Self {
         Self { damaged_lines: damaged_lines.peekable(), size_info }
     }
 
diff --git a/alacritty/src/display/meter.rs b/alacritty/src/display/meter.rs
index 9ccfe52d8b9..e263100fdfb 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 878f03e164b..28c91cdbd6b 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<Point>,
 
@@ -379,9 +384,6 @@ pub struct Display {
 
     context: ManuallyDrop<Replaceable<PossiblyCurrentContext>>,
 
-    debug_damage: bool,
-    damage_rects: Vec<DamageRect>,
-    next_frame_damage_rects: Vec<DamageRect>,
     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<T: EventListener>(
-        &mut self,
-        terminal: &mut MutexGuard<'_, Term<T>>,
-        selection_range: Option<SelectionRange>,
-        search_state: &SearchState,
-    ) {
-        let requires_full_damage = self.visual_bell.intensity() != 0.
-            || self.hint_state.active()
-            || search_state.regex().is_some();
-        if requires_full_damage {
-            terminal.mark_fully_damaged();
-        }
-
-        self.damage_highlighted_hints(terminal);
-        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<Column>,
         line: usize,
     ) {
-        const fn num_digits(mut number: u32) -> usize {
-            let mut res = 0;
-            loop {
-                number /= 10;
-                res += 1;
-                if number == 0 {
-                    break res;
-                }
-            }
-        }
-
+        let 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<usize>, len: u32) -> DamageRect {
-        let size_info: SizeInfo<u32> = self.size_info.into();
-        let x = size_info.padding_x() + point.column.0 as u32 * size_info.cell_width();
-        let y_top = size_info.height() - size_info.padding_y();
-        let y = y_top - (point.line as u32 + 1) * size_info.cell_height();
-        let width = len * 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<T: EventListener>(&self, terminal: &mut Term<T>) {
-        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<RenderRect>) {
-        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 e6f77e8c1a6..f19aa2f9f76 100644
--- a/alacritty/src/event.rs
+++ b/alacritty/src/event.rs
@@ -517,7 +517,7 @@ impl<'a, N: Notify + 'a, T: EventListener> input::ActionContext<T> 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<T> 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 3df1d12875e..f56fa605b6c 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::Item> {
-        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<LineDamageBounds>,
 
     /// Old terminal cursor point.
     last_cursor: Point,
-
-    /// Last Vi cursor point.
-    last_vi_cursor_point: Option<Point<usize>>,
-
-    /// Old selection range.
-    last_selection: Option<SelectionRange>,
 }
 
 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<T> Term<T> {
         }
     }
 
+    /// 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<SelectionRange>) -> 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<T> Term<T> {
         // 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<T> Term<T> {
     }
 
     #[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<T: EventListener> Handler for Term<T> {
         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<T: EventListener> Handler for Term<T> {
         }
 
         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.
-
-        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.
+        // Create scrollback.
+        for _ in 0..20 {
+            term.newline();
+        }
 
-        let mut damaged_lines = match term.damage(None) {
-            TermDamage::Full => panic!("Expected partial damage, however got Full"),
-            TermDamage::Partial(damaged_lines) => damaged_lines,
+        match term.damage() {
+            TermDamage::Full => (),
+            TermDamage::Partial(_) => panic!("Expected Full damage, however got Partial "),
         };
-        // 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.scroll_display(Scroll::Delta(10));
         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) {
+        // 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,
         };
-        // 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]


Test patch: