Damage only terminal inside `alacritty_terminal`

The damage tracking was including selection and vi_cursor which were
rendering viewport related, however all the damage tracking inside
the `alacritty_terminal` was _terminal viewport_ related, meaning that
it should be affected by `display_offset`.

Refactor the damage tracking so `alacritty_terminal` is only tracking
actual terminal updates and properly applying display offset to them,
while `alacritty` pulls this damage into its own UI damage state.

Fixes #7111.
This commit is contained in:
Kirill Chibisov 2023-11-23 16:48:09 +04:00 committed by GitHub
parent 0589b71894
commit 40160c5da1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 383 additions and 342 deletions

View File

@ -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

View File

@ -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 }
}

View File

@ -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)

View File

@ -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 {

View File

@ -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;

View File

@ -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.
// 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]