From 12fbd0051cd743bcea79f45777325f76485fd865 Mon Sep 17 00:00:00 2001 From: Kirill Chibisov Date: Mon, 28 Dec 2020 12:45:39 +0300 Subject: [PATCH] Draw cursor with rect renderer This commit makes cursors being drawn via rects, thus it's always above underlines/strikeouts. Also, since the cursor isn't a glyph anymore, it can't be obscured due to atlas switching while glyphs are rendered. Fixes #4404. Fixes #3471. --- CHANGELOG.md | 2 + alacritty/src/cursor.rs | 166 ++++---- alacritty/src/display.rs | 33 +- alacritty/src/input.rs | 3 +- alacritty/src/renderer/mod.rs | 58 +-- alacritty/src/renderer/rects.rs | 3 +- alacritty/src/url.rs | 16 +- alacritty_terminal/src/index.rs | 2 +- alacritty_terminal/src/selection.rs | 67 +++- alacritty_terminal/src/term/mod.rs | 547 +++----------------------- alacritty_terminal/src/term/render.rs | 420 ++++++++++++++++++++ 11 files changed, 649 insertions(+), 668 deletions(-) create mode 100644 alacritty_terminal/src/term/render.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 63c7c8ba..c05d2a31 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -45,6 +45,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Crash due to clipboard not being properly released on Wayland - Shadow artifacts when resizing transparent windows on macOS - Missing glyph symbols not being rendered for missing glyphs on macOS and Windows +- Underline cursor being obscured by underline +- Cursor not being rendered with a lot of unicode glyphs visible ### Removed diff --git a/alacritty/src/cursor.rs b/alacritty/src/cursor.rs index 806f6ff8..a9fba66a 100644 --- a/alacritty/src/cursor.rs +++ b/alacritty/src/cursor.rs @@ -1,112 +1,92 @@ -//! Helpers for creating different cursor glyphs from font metrics. - -use crossfont::{BitmapBuffer, Metrics, RasterizedGlyph}; +//! Convert a cursor into an iterator of rects. use alacritty_terminal::ansi::CursorShape; +use alacritty_terminal::term::color::Rgb; +use alacritty_terminal::term::render::RenderableCursor; +use alacritty_terminal::term::SizeInfo; -pub fn get_cursor_glyph( - cursor: CursorShape, - metrics: Metrics, - offset_x: i8, - offset_y: i8, - is_wide: bool, - cursor_thickness: f32, -) -> RasterizedGlyph { - // Calculate the cell metrics. - // - // NOTE: With Rust 1.47+ `f64 as usize` is defined to clamp automatically: - // https://github.com/rust-lang/rust/commit/14d608f1d8a0b84da5f3bccecb3efb3d35f980dc - let height = (metrics.line_height + f64::from(offset_y)).max(1.) as usize; - let mut width = (metrics.average_advance + f64::from(offset_x)).max(1.) as usize; - let line_width = (cursor_thickness * width as f32).round().max(1.) as usize; +use crate::renderer::rects::RenderRect; - // Double the cursor width if it's above a double-width glyph. - if is_wide { - width *= 2; - } - - match cursor { - CursorShape::HollowBlock => get_box_cursor_glyph(height, width, line_width), - CursorShape::Underline => get_underline_cursor_glyph(width, line_width), - CursorShape::Beam => get_beam_cursor_glyph(height, line_width), - CursorShape::Block => get_block_cursor_glyph(height, width), - CursorShape::Hidden => RasterizedGlyph::default(), - } +/// Trait for conversion into the iterator. +pub trait IntoRects { + /// Consume the cursor for an iterator of rects. + fn rects(self, size_info: &SizeInfo, thickness: f32) -> CursorRects; } -/// Return a custom underline cursor character. -pub fn get_underline_cursor_glyph(width: usize, line_width: usize) -> RasterizedGlyph { - // Create a new rectangle, the height is relative to the font width. - let buffer = BitmapBuffer::RGB(vec![255u8; width * line_width * 3]); +impl IntoRects for RenderableCursor { + fn rects(self, size_info: &SizeInfo, thickness: f32) -> CursorRects { + let point = self.point(); + let x = point.col.0 as f32 * size_info.cell_width() + size_info.padding_x(); + let y = point.line.0 as f32 * size_info.cell_height() + size_info.padding_y(); - // Create a custom glyph with the rectangle data attached to it. - RasterizedGlyph { - character: ' ', - top: line_width as i32, - left: 0, - height: line_width as i32, - width: width as i32, - buffer, - } -} + let mut width = size_info.cell_width(); + let height = size_info.cell_height(); -/// Return a custom beam cursor character. -pub fn get_beam_cursor_glyph(height: usize, line_width: usize) -> RasterizedGlyph { - // Create a new rectangle that is at least one pixel wide - let buffer = BitmapBuffer::RGB(vec![255u8; line_width * height * 3]); + if self.is_wide() { + width *= 2.; + } - // Create a custom glyph with the rectangle data attached to it - RasterizedGlyph { - character: ' ', - top: height as i32, - left: 0, - height: height as i32, - width: line_width as i32, - buffer, - } -} + let thickness = (thickness * width as f32).round().max(1.); -/// Returns a custom box cursor character. -pub fn get_box_cursor_glyph(height: usize, width: usize, line_width: usize) -> RasterizedGlyph { - // Create a new box outline rectangle. - let mut buffer = Vec::with_capacity(width * height * 3); - for y in 0..height { - for x in 0..width { - if y < line_width - || y >= height - line_width - || x < line_width - || x >= width - line_width - { - buffer.append(&mut vec![255u8; 3]); - } else { - buffer.append(&mut vec![0u8; 3]); - } + match self.shape() { + CursorShape::Beam => beam(x, y, height, thickness, self.color()), + CursorShape::Underline => underline(x, y, width, height, thickness, self.color()), + CursorShape::HollowBlock => hollow(x, y, width, height, thickness, self.color()), + _ => CursorRects::default(), } } +} - // Create a custom glyph with the rectangle data attached to it. - RasterizedGlyph { - character: ' ', - top: height as i32, - left: 0, - height: height as i32, - width: width as i32, - buffer: BitmapBuffer::RGB(buffer), +/// Cursor rect iterator. +#[derive(Default)] +pub struct CursorRects { + rects: [Option; 4], + index: usize, +} + +impl From for CursorRects { + fn from(rect: RenderRect) -> Self { + Self { rects: [Some(rect), None, None, None], index: 0 } } } -/// Return a custom block cursor character. -pub fn get_block_cursor_glyph(height: usize, width: usize) -> RasterizedGlyph { - // Create a completely filled glyph. - let buffer = BitmapBuffer::RGB(vec![255u8; width * height * 3]); +impl Iterator for CursorRects { + type Item = RenderRect; - // Create a custom glyph with the rectangle data attached to it. - RasterizedGlyph { - character: ' ', - top: height as i32, - left: 0, - height: height as i32, - width: width as i32, - buffer, + fn next(&mut self) -> Option { + let rect = self.rects.get_mut(self.index)?; + self.index += 1; + rect.take() + } +} + +/// Create an iterator yielding a single beam rect. +fn beam(x: f32, y: f32, height: f32, thickness: f32, color: Rgb) -> CursorRects { + RenderRect::new(x, y, thickness, height, color, 1.).into() +} + +/// Create an iterator yielding a single underline rect. +fn underline(x: f32, y: f32, width: f32, height: f32, thickness: f32, color: Rgb) -> CursorRects { + let y = y + height - thickness; + RenderRect::new(x, y, width, thickness, color, 1.).into() +} + +/// Create an iterator yielding a rect for each side of the hollow block cursor. +fn hollow(x: f32, y: f32, width: f32, height: f32, thickness: f32, color: Rgb) -> CursorRects { + let top_line = RenderRect::new(x, y, width, thickness, color, 1.); + + let vertical_y = y + thickness; + let vertical_height = height - 2. * thickness; + let left_line = RenderRect::new(x, vertical_y, thickness, vertical_height, color, 1.); + + let bottom_y = y + height - thickness; + let bottom_line = RenderRect::new(x, bottom_y, width, thickness, color, 1.); + + let right_x = x + width - thickness; + let right_line = RenderRect::new(right_x, vertical_y, thickness, vertical_height, color, 1.); + + CursorRects { + rects: [Some(top_line), Some(bottom_line), Some(left_line), Some(right_line)], + index: 0, } } diff --git a/alacritty/src/display.rs b/alacritty/src/display.rs index 7fbf0d54..4084d639 100644 --- a/alacritty/src/display.rs +++ b/alacritty/src/display.rs @@ -33,6 +33,7 @@ use crate::config::window::Dimensions; #[cfg(not(windows))] use crate::config::window::StartupMode; use crate::config::Config; +use crate::cursor::IntoRects; use crate::event::{Mouse, SearchState}; use crate::message_bar::{MessageBuffer, MessageType}; use crate::meter::Meter; @@ -246,7 +247,7 @@ impl Display { // Clear screen. let background_color = config.colors.primary.background; - renderer.with_api(&config.ui_config, config.cursor, &size_info, |api| { + renderer.with_api(&config.ui_config, &size_info, |api| { api.clear(background_color); }); @@ -268,7 +269,7 @@ impl Display { #[cfg(not(any(target_os = "macos", windows)))] if is_x11 { window.swap_buffers(); - renderer.with_api(&config.ui_config, config.cursor, &size_info, |api| { + renderer.with_api(&config.ui_config, &size_info, |api| { api.finish(); }); } @@ -450,7 +451,14 @@ impl Display { .and_then(|focused_match| terminal.grid().clamp_buffer_range_to_visible(focused_match)); let cursor_hidden = self.cursor_hidden || search_state.regex().is_some(); - let grid_cells = terminal.renderable_cells(config, !cursor_hidden).collect::>(); + // Collect renderable content before the terminal is dropped. + let mut content = terminal.renderable_content(config, !cursor_hidden); + let mut grid_cells = Vec::new(); + while let Some(cell) = content.next() { + grid_cells.push(cell); + } + let cursor = content.cursor(); + let visual_bell_intensity = terminal.visual_bell.intensity(); let background_color = terminal.background_color(); let cursor_point = terminal.grid().cursor.point; @@ -471,7 +479,7 @@ impl Display { // Drop terminal as early as possible to free lock. drop(terminal); - self.renderer.with_api(&config.ui_config, config.cursor, &size_info, |api| { + self.renderer.with_api(&config.ui_config, &size_info, |api| { api.clear(background_color); }); @@ -482,7 +490,7 @@ impl Display { { let _sampler = self.meter.sampler(); - self.renderer.with_api(&config.ui_config, config.cursor, &size_info, |mut api| { + self.renderer.with_api(&config.ui_config, &size_info, |mut api| { // Iterate over all non-empty cells in the grid. for mut cell in grid_cells { // Invert the active match in vi-less search. @@ -538,6 +546,13 @@ impl Display { } } + // Push the cursor rects for rendering. + if let Some(cursor) = cursor { + for rect in cursor.rects(&size_info, config.cursor.thickness()) { + rects.push(rect); + } + } + // Push visual bell after url/underline/strikeout rects. if visual_bell_intensity != 0. { let visual_bell_rect = RenderRect::new( @@ -576,7 +591,7 @@ impl Display { // Relay messages to the user. let fg = config.colors.primary.background; for (i, message_text) in text.iter().enumerate() { - self.renderer.with_api(&config.ui_config, config.cursor, &size_info, |mut api| { + self.renderer.with_api(&config.ui_config, &size_info, |mut api| { api.render_string(glyph_cache, start_line + i, &message_text, fg, None); }); } @@ -621,7 +636,7 @@ impl Display { // On X11 `swap_buffers` does not block for vsync. However the next OpenGl command // will block to synchronize (this is `glClear` in Alacritty), which causes a // permanent one frame delay. - self.renderer.with_api(&config.ui_config, config.cursor, &size_info, |api| { + self.renderer.with_api(&config.ui_config, &size_info, |api| { api.finish(); }); } @@ -668,7 +683,7 @@ impl Display { let fg = config.colors.search_bar_foreground(); let bg = config.colors.search_bar_background(); - self.renderer.with_api(&config.ui_config, config.cursor, &size_info, |mut api| { + self.renderer.with_api(&config.ui_config, &size_info, |mut api| { api.render_string(glyph_cache, size_info.screen_lines(), &text, fg, Some(bg)); }); } @@ -684,7 +699,7 @@ impl Display { let fg = config.colors.primary.background; let bg = config.colors.normal.red; - self.renderer.with_api(&config.ui_config, config.cursor, &size_info, |mut api| { + self.renderer.with_api(&config.ui_config, &size_info, |mut api| { api.render_string(glyph_cache, size_info.screen_lines() - 2, &timing[..], fg, Some(bg)); }); } diff --git a/alacritty/src/input.rs b/alacritty/src/input.rs index 4f66721c..55799dc5 100644 --- a/alacritty/src/input.rs +++ b/alacritty/src/input.rs @@ -26,8 +26,7 @@ use alacritty_terminal::event::EventListener; use alacritty_terminal::grid::{Dimensions, Scroll}; use alacritty_terminal::index::{Column, Direction, Line, Point, Side}; use alacritty_terminal::selection::SelectionType; -use alacritty_terminal::term::mode::TermMode; -use alacritty_terminal::term::{ClipboardType, SizeInfo, Term}; +use alacritty_terminal::term::{ClipboardType, SizeInfo, Term, TermMode}; use alacritty_terminal::vi_mode::ViMotion; use crate::clipboard::Clipboard; diff --git a/alacritty/src/renderer/mod.rs b/alacritty/src/renderer/mod.rs index ca3553dc..70ac993b 100644 --- a/alacritty/src/renderer/mod.rs +++ b/alacritty/src/renderer/mod.rs @@ -15,15 +15,14 @@ use fnv::FnvHasher; use log::{debug, error, info}; use unicode_width::UnicodeWidthChar; -use alacritty_terminal::config::Cursor; use alacritty_terminal::index::{Column, Line}; use alacritty_terminal::term::cell::Flags; use alacritty_terminal::term::color::Rgb; -use alacritty_terminal::term::{CursorKey, RenderableCell, RenderableCellContent, SizeInfo}; +use alacritty_terminal::term::render::RenderableCell; +use alacritty_terminal::term::SizeInfo; use crate::config::font::{Font, FontDescription}; use crate::config::ui_config::{Delta, UIConfig}; -use crate::cursor; use crate::gl; use crate::gl::types::*; use crate::renderer::rects::{RectRenderer, RenderRect}; @@ -116,9 +115,6 @@ pub struct GlyphCache { /// Cache of buffered glyphs. cache: HashMap>, - /// Cache of buffered cursor glyphs. - cursor_cache: HashMap>, - /// Rasterizer for loading new glyphs. rasterizer: Rasterizer, @@ -164,7 +160,6 @@ impl GlyphCache { let mut cache = Self { cache: HashMap::default(), - cursor_cache: HashMap::default(), rasterizer, font_size: font.size(), font_key: regular, @@ -328,7 +323,6 @@ impl GlyphCache { pub fn clear_glyph_cache(&mut self, loader: &mut L) { loader.clear(); self.cache = HashMap::default(); - self.cursor_cache = HashMap::default(); self.load_common_glyphs(loader); } @@ -459,7 +453,6 @@ pub struct RenderApi<'a> { current_atlas: &'a mut usize, program: &'a mut TextShaderProgram, config: &'a UIConfig, - cursor_config: Cursor, } #[derive(Debug)] @@ -693,13 +686,7 @@ impl QuadRenderer { } } - pub fn with_api( - &mut self, - config: &UIConfig, - cursor_config: Cursor, - props: &SizeInfo, - func: F, - ) -> T + pub fn with_api(&mut self, config: &UIConfig, props: &SizeInfo, func: F) -> T where F: FnOnce(RenderApi<'_>) -> T, { @@ -720,7 +707,6 @@ impl QuadRenderer { current_atlas: &mut self.current_atlas, program: &mut self.program, config, - cursor_config, }); unsafe { @@ -848,10 +834,11 @@ impl<'a> RenderApi<'a> { let cells = string .chars() .enumerate() - .map(|(i, c)| RenderableCell { + .map(|(i, character)| RenderableCell { line, column: Column(i), - inner: RenderableCellContent::Chars((c, None)), + character, + zerowidth: None, flags: Flags::empty(), bg_alpha, fg, @@ -881,26 +868,6 @@ impl<'a> RenderApi<'a> { } pub fn render_cell(&mut self, mut cell: RenderableCell, glyph_cache: &mut GlyphCache) { - let (mut character, zerowidth) = match cell.inner { - RenderableCellContent::Cursor(cursor_key) => { - // Raw cell pixel buffers like cursors don't need to go through font lookup. - let metrics = glyph_cache.metrics; - let glyph = glyph_cache.cursor_cache.entry(cursor_key).or_insert_with(|| { - self.load_glyph(&cursor::get_cursor_glyph( - cursor_key.shape, - metrics, - self.config.font.offset.x, - self.config.font.offset.y, - cursor_key.is_wide, - self.cursor_config.thickness(), - )) - }); - self.add_render_item(&cell, glyph); - return; - }, - RenderableCellContent::Chars((c, ref mut zerowidth)) => (c, zerowidth.take()), - }; - // Get font key for cell. let font_key = match cell.flags & Flags::BOLD_ITALIC { Flags::BOLD_ITALIC => glyph_cache.bold_italic_key, @@ -911,11 +878,12 @@ impl<'a> RenderApi<'a> { // Ignore hidden cells and render tabs as spaces to prevent font issues. let hidden = cell.flags.contains(Flags::HIDDEN); - if character == '\t' || hidden { - character = ' '; + if cell.character == '\t' || hidden { + cell.character = ' '; } - let mut glyph_key = GlyphKey { font_key, size: glyph_cache.font_size, character }; + let mut glyph_key = + GlyphKey { font_key, size: glyph_cache.font_size, character: cell.character }; // Add cell to batch. match glyph_cache.get(glyph_key, self) { @@ -930,9 +898,9 @@ impl<'a> RenderApi<'a> { } // Render visible zero-width characters. - if let Some(zerowidth) = zerowidth.filter(|_| !hidden) { - for character in zerowidth.iter() { - glyph_key.character = *character; + if let Some(zerowidth) = cell.zerowidth.take().filter(|_| !hidden) { + for character in zerowidth { + glyph_key.character = character; if let Ok(glyph) = glyph_cache.get(glyph_key, self) { self.add_render_item(&cell, &glyph); } diff --git a/alacritty/src/renderer/rects.rs b/alacritty/src/renderer/rects.rs index 1f50da87..cfd17379 100644 --- a/alacritty/src/renderer/rects.rs +++ b/alacritty/src/renderer/rects.rs @@ -6,7 +6,8 @@ use crossfont::Metrics; use alacritty_terminal::index::{Column, Point}; use alacritty_terminal::term::cell::Flags; use alacritty_terminal::term::color::Rgb; -use alacritty_terminal::term::{RenderableCell, SizeInfo}; +use alacritty_terminal::term::render::RenderableCell; +use alacritty_terminal::term::SizeInfo; use crate::gl; use crate::gl::types::*; diff --git a/alacritty/src/url.rs b/alacritty/src/url.rs index b33b532e..f4bf8205 100644 --- a/alacritty/src/url.rs +++ b/alacritty/src/url.rs @@ -8,7 +8,8 @@ use urlocator::{UrlLocation, UrlLocator}; use alacritty_terminal::index::{Column, Point}; use alacritty_terminal::term::cell::Flags; use alacritty_terminal::term::color::Rgb; -use alacritty_terminal::term::{RenderableCell, RenderableCellContent, SizeInfo}; +use alacritty_terminal::term::render::RenderableCell; +use alacritty_terminal::term::SizeInfo; use crate::config::Config; use crate::event::Mouse; @@ -72,12 +73,6 @@ impl Urls { // Update tracked URLs. pub fn update(&mut self, num_cols: Column, cell: &RenderableCell) { - // Convert cell to character. - let c = match &cell.inner { - RenderableCellContent::Chars((c, _zerowidth)) => *c, - RenderableCellContent::Cursor(_) => return, - }; - let point: Point = cell.into(); let mut end = point; @@ -107,7 +102,7 @@ impl Urls { } // Advance parser. - let last_state = mem::replace(&mut self.state, self.locator.advance(c)); + let last_state = mem::replace(&mut self.state, self.locator.advance(cell.character)); match (self.state, last_state) { (UrlLocation::Url(_length, end_offset), UrlLocation::Scheme) => { // Create empty URL. @@ -204,8 +199,9 @@ mod tests { fn text_to_cells(text: &str) -> Vec { text.chars() .enumerate() - .map(|(i, c)| RenderableCell { - inner: RenderableCellContent::Chars((c, None)), + .map(|(i, character)| RenderableCell { + character, + zerowidth: None, line: Line(0), column: Column(i), fg: Default::default(), diff --git a/alacritty_terminal/src/index.rs b/alacritty_terminal/src/index.rs index 2ee679db..8b84b7f5 100644 --- a/alacritty_terminal/src/index.rs +++ b/alacritty_terminal/src/index.rs @@ -8,7 +8,7 @@ use std::ops::{self, Add, AddAssign, Deref, Range, Sub, SubAssign}; use serde::{Deserialize, Serialize}; use crate::grid::Dimensions; -use crate::term::RenderableCell; +use crate::term::render::RenderableCell; /// The side of a cell. pub type Side = Direction; diff --git a/alacritty_terminal/src/selection.rs b/alacritty_terminal/src/selection.rs index 9c3fa598..9402fc21 100644 --- a/alacritty_terminal/src/selection.rs +++ b/alacritty_terminal/src/selection.rs @@ -9,8 +9,10 @@ use std::convert::TryFrom; use std::mem; use std::ops::{Bound, Range, RangeBounds}; -use crate::grid::Dimensions; +use crate::ansi::CursorShape; +use crate::grid::{Dimensions, Grid, GridCell}; use crate::index::{Column, Line, Point, Side}; +use crate::term::cell::Flags; use crate::term::Term; /// A Point and side within that point. @@ -42,14 +44,67 @@ impl SelectionRange { Self { start, end, is_block } } - pub fn contains(&self, col: Column, line: L) -> bool + /// Check if a point lies within the selection. + pub fn contains(&self, point: Point) -> bool where L: PartialEq + PartialOrd, { - self.start.line <= line - && self.end.line >= line - && (self.start.col <= col || (self.start.line != line && !self.is_block)) - && (self.end.col >= col || (self.end.line != line && !self.is_block)) + self.start.line <= point.line + && self.end.line >= point.line + && (self.start.col <= point.col || (self.start.line != point.line && !self.is_block)) + && (self.end.col >= point.col || (self.end.line != point.line && !self.is_block)) + } +} + +impl SelectionRange { + /// Check if the cell at a point is part of the selection. + pub fn contains_cell( + &self, + grid: &Grid, + point: Point, + cursor_point: Point, + cursor_shape: CursorShape, + ) -> bool + where + T: GridCell, + { + // Do not invert block cursor at selection boundaries. + if cursor_shape == CursorShape::Block + && cursor_point == point + && (self.start == point + || self.end == point + || (self.is_block + && ((self.start.line == point.line && self.end.col == point.col) + || (self.end.line == point.line && self.start.col == point.col)))) + { + return false; + } + + // Point itself is selected. + if self.contains(point) { + return true; + } + + let num_cols = grid.cols(); + + // Convert to absolute coordinates to adjust for the display offset. + let buffer_point = grid.visible_to_buffer(point); + let cell = &grid[buffer_point]; + + // Check if wide char's spacers are selected. + if cell.flags().contains(Flags::WIDE_CHAR) { + let prev = point.sub(num_cols, 1); + let buffer_prev = grid.visible_to_buffer(prev); + let next = point.add(num_cols, 1); + + // Check trailing spacer. + self.contains(next) + // Check line-wrapping, leading spacer. + || (grid[buffer_prev].flags().contains(Flags::LEADING_WIDE_CHAR_SPACER) + && self.contains(prev)) + } else { + false + } } } diff --git a/alacritty_terminal/src/term/mod.rs b/alacritty_terminal/src/term/mod.rs index 64493bd9..70d670fe 100644 --- a/alacritty_terminal/src/term/mod.rs +++ b/alacritty_terminal/src/term/mod.rs @@ -1,12 +1,12 @@ //! Exports the `Term` type which is a high-level API for the Grid. use std::cmp::{max, min}; -use std::iter::Peekable; -use std::ops::{Index, IndexMut, Range, RangeInclusive}; +use std::ops::{Index, IndexMut, Range}; use std::sync::Arc; use std::time::{Duration, Instant}; -use std::{io, iter, mem, ptr, str}; +use std::{io, mem, ptr, str}; +use bitflags::bitflags; use log::{debug, trace}; use serde::{Deserialize, Serialize}; use unicode_width::UnicodeWidthChar; @@ -16,27 +16,23 @@ use crate::ansi::{ }; use crate::config::{BellAnimation, BellConfig, Config}; use crate::event::{Event, EventListener}; -use crate::grid::{Dimensions, DisplayIter, Grid, IndexRegion, Indexed, Scroll}; +use crate::grid::{Dimensions, Grid, IndexRegion, Scroll}; use crate::index::{self, Boundary, Column, Direction, IndexRange, Line, Point, Side}; use crate::selection::{Selection, SelectionRange}; use crate::term::cell::{Cell, Flags, LineLength}; -use crate::term::color::{CellRgb, Rgb, DIM_FACTOR}; -use crate::term::search::{RegexIter, RegexSearch}; +use crate::term::color::Rgb; +use crate::term::render::RenderableContent; +use crate::term::search::RegexSearch; use crate::vi_mode::{ViModeCursor, ViMotion}; pub mod cell; pub mod color; +pub mod render; mod search; /// Max size of the window title stack. const TITLE_STACK_MAX_DEPTH: usize = 4096; -/// Minimum contrast between a fixed cursor color and the cell's background. -const MIN_CURSOR_CONTRAST: f64 = 1.5; - -/// Maximum number of linewraps followed outside of the viewport during search highlighting. -const MAX_SEARCH_LINES: usize = 100; - /// Default tab interval, corresponding to terminfo `it` value. const INITIAL_TABSTOPS: usize = 8; @@ -48,433 +44,41 @@ pub const MIN_COLS: usize = 2; /// Minimum number of visible lines. pub const MIN_SCREEN_LINES: usize = 1; -/// Cursor storing all information relevant for rendering. -#[derive(Debug, Eq, PartialEq, Copy, Clone, Deserialize)] -struct RenderableCursor { - text_color: CellRgb, - cursor_color: CellRgb, - key: CursorKey, - point: Point, - rendered: bool, -} - -/// A key for caching cursor glyphs. -#[derive(Debug, Eq, PartialEq, Copy, Clone, Hash, Deserialize)] -pub struct CursorKey { - pub shape: CursorShape, - pub is_wide: bool, -} - -type MatchIter<'a> = Box>> + 'a>; - -/// Regex search highlight tracking. -pub struct RenderableSearch<'a> { - iter: Peekable>, -} - -impl<'a> RenderableSearch<'a> { - /// Create a new renderable search iterator. - fn new(term: &'a Term) -> Self { - let viewport_end = term.grid().display_offset(); - let viewport_start = viewport_end + term.screen_lines().0 - 1; - - // Compute start of the first and end of the last line. - let start_point = Point::new(viewport_start, Column(0)); - let mut start = term.line_search_left(start_point); - let end_point = Point::new(viewport_end, term.cols() - 1); - let mut end = term.line_search_right(end_point); - - // Set upper bound on search before/after the viewport to prevent excessive blocking. - if start.line > viewport_start + MAX_SEARCH_LINES { - if start.line == 0 { - // Do not highlight anything if this line is the last. - let iter: MatchIter<'a> = Box::new(iter::empty()); - return Self { iter: iter.peekable() }; - } else { - // Start at next line if this one is too long. - start.line -= 1; - } - } - end.line = max(end.line, viewport_end.saturating_sub(MAX_SEARCH_LINES)); - - // Create an iterater for the current regex search for all visible matches. - let iter: MatchIter<'a> = Box::new( - RegexIter::new(start, end, Direction::Right, &term) - .skip_while(move |rm| rm.end().line > viewport_start) - .take_while(move |rm| rm.start().line >= viewport_end), - ); - - Self { iter: iter.peekable() } - } - - /// Advance the search tracker to the next point. - /// - /// This will return `true` if the point passed is part of a search match. - fn advance(&mut self, point: Point) -> bool { - while let Some(regex_match) = &self.iter.peek() { - if regex_match.start() > &point { - break; - } else if regex_match.end() < &point { - let _ = self.iter.next(); - } else { - return true; - } - } - false +bitflags! { + pub struct TermMode: u32 { + const NONE = 0; + const SHOW_CURSOR = 0b0000_0000_0000_0000_0001; + const APP_CURSOR = 0b0000_0000_0000_0000_0010; + const APP_KEYPAD = 0b0000_0000_0000_0000_0100; + const MOUSE_REPORT_CLICK = 0b0000_0000_0000_0000_1000; + const BRACKETED_PASTE = 0b0000_0000_0000_0001_0000; + const SGR_MOUSE = 0b0000_0000_0000_0010_0000; + const MOUSE_MOTION = 0b0000_0000_0000_0100_0000; + const LINE_WRAP = 0b0000_0000_0000_1000_0000; + const LINE_FEED_NEW_LINE = 0b0000_0000_0001_0000_0000; + const ORIGIN = 0b0000_0000_0010_0000_0000; + const INSERT = 0b0000_0000_0100_0000_0000; + const FOCUS_IN_OUT = 0b0000_0000_1000_0000_0000; + const ALT_SCREEN = 0b0000_0001_0000_0000_0000; + const MOUSE_DRAG = 0b0000_0010_0000_0000_0000; + const MOUSE_MODE = 0b0000_0010_0000_0100_1000; + const UTF8_MOUSE = 0b0000_0100_0000_0000_0000; + const ALTERNATE_SCROLL = 0b0000_1000_0000_0000_0000; + const VI = 0b0001_0000_0000_0000_0000; + const URGENCY_HINTS = 0b0010_0000_0000_0000_0000; + const ANY = std::u32::MAX; } } -/// Iterator that yields cells needing render. -/// -/// Yields cells that require work to be displayed (that is, not a an empty -/// background cell). Additionally, this manages some state of the grid only -/// relevant for rendering like temporarily changing the cell with the cursor. -/// -/// This manages the cursor during a render. The cursor location is inverted to -/// draw it, and reverted after drawing to maintain state. -pub struct RenderableCellsIter<'a, C> { - inner: DisplayIter<'a, Cell>, - grid: &'a Grid, - cursor: RenderableCursor, - config: &'a Config, - colors: &'a color::List, - selection: Option>, - search: RenderableSearch<'a>, -} - -impl<'a, C> Iterator for RenderableCellsIter<'a, C> { - type Item = RenderableCell; - - /// Gets the next renderable cell. - /// - /// Skips empty (background) cells and applies any flags to the cell state - /// (eg. invert fg and bg colors). - #[inline] - fn next(&mut self) -> Option { - loop { - if self.cursor.point == self.inner.point() { - // Handle cursor rendering. - if self.cursor.rendered { - return self.next_cursor_cell(); - } else { - return Some(self.next_cursor()); - } - } else { - // Handle non-cursor cells. - let cell = self.inner.next()?; - let cell = RenderableCell::new(self, cell); - - // Skip empty cells and wide char spacers. - if !cell.is_empty() && !cell.flags.contains(Flags::WIDE_CHAR_SPACER) { - return Some(cell); - } - } - } +impl Default for TermMode { + fn default() -> TermMode { + TermMode::SHOW_CURSOR + | TermMode::LINE_WRAP + | TermMode::ALTERNATE_SCROLL + | TermMode::URGENCY_HINTS } } -impl<'a, C> RenderableCellsIter<'a, C> { - /// Create the renderable cells iterator. - /// - /// The cursor and terminal mode are required for properly displaying the - /// cursor. - fn new( - term: &'a Term, - config: &'a Config, - show_cursor: bool, - ) -> RenderableCellsIter<'a, C> { - RenderableCellsIter { - cursor: term.renderable_cursor(config, show_cursor), - grid: &term.grid, - inner: term.grid.display_iter(), - selection: term.visible_selection(), - config, - colors: &term.colors, - search: RenderableSearch::new(term), - } - } - - /// Get the next renderable cell as the cell below the cursor. - fn next_cursor_cell(&mut self) -> Option { - // Handle cell below cursor. - let cell = self.inner.next()?; - let mut cell = RenderableCell::new(self, cell); - - if self.cursor.key.shape == CursorShape::Block { - cell.fg = match self.cursor.cursor_color { - // Apply cursor color, or invert the cursor if it has a fixed background - // close to the cell's background. - CellRgb::Rgb(col) if col.contrast(cell.bg) < MIN_CURSOR_CONTRAST => cell.bg, - _ => self.cursor.text_color.color(cell.fg, cell.bg), - }; - } - - Some(cell) - } - - /// Get the next renderable cell as the cursor. - fn next_cursor(&mut self) -> RenderableCell { - // Handle cursor. - self.cursor.rendered = true; - - let buffer_point = self.grid.visible_to_buffer(self.cursor.point); - let cell = Indexed { - inner: &self.grid[buffer_point.line][buffer_point.col], - column: self.cursor.point.col, - line: self.cursor.point.line, - }; - - let mut cell = RenderableCell::new(self, cell); - cell.inner = RenderableCellContent::Cursor(self.cursor.key); - - // Apply cursor color, or invert the cursor if it has a fixed background close - // to the cell's background. - if !matches!( - self.cursor.cursor_color, - CellRgb::Rgb(color) if color.contrast(cell.bg) < MIN_CURSOR_CONTRAST - ) { - cell.fg = self.cursor.cursor_color.color(cell.fg, cell.bg); - } - - cell - } - - /// Check selection state of a cell. - fn is_selected(&self, point: Point) -> bool { - let selection = match self.selection { - Some(selection) => selection, - None => return false, - }; - - // Do not invert block cursor at selection boundaries. - if self.cursor.key.shape == CursorShape::Block - && self.cursor.point == point - && (selection.start == point - || selection.end == point - || (selection.is_block - && ((selection.start.line == point.line && selection.end.col == point.col) - || (selection.end.line == point.line && selection.start.col == point.col)))) - { - return false; - } - - // Point itself is selected. - if selection.contains(point.col, point.line) { - return true; - } - - let num_cols = self.grid.cols(); - - // Convert to absolute coordinates to adjust for the display offset. - let buffer_point = self.grid.visible_to_buffer(point); - let cell = &self.grid[buffer_point]; - - // Check if wide char's spacers are selected. - if cell.flags.contains(Flags::WIDE_CHAR) { - let prev = point.sub(num_cols, 1); - let buffer_prev = self.grid.visible_to_buffer(prev); - let next = point.add(num_cols, 1); - - // Check trailing spacer. - selection.contains(next.col, next.line) - // Check line-wrapping, leading spacer. - || (self.grid[buffer_prev].flags.contains(Flags::LEADING_WIDE_CHAR_SPACER) - && selection.contains(prev.col, prev.line)) - } else { - false - } - } -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub enum RenderableCellContent { - Chars((char, Option>)), - Cursor(CursorKey), -} - -#[derive(Clone, Debug)] -pub struct RenderableCell { - /// A _Display_ line (not necessarily an _Active_ line). - pub line: Line, - pub column: Column, - pub inner: RenderableCellContent, - pub fg: Rgb, - pub bg: Rgb, - pub bg_alpha: f32, - pub flags: Flags, - pub is_match: bool, -} - -impl RenderableCell { - fn new<'a, C>(iter: &mut RenderableCellsIter<'a, C>, cell: Indexed<&Cell>) -> Self { - let point = Point::new(cell.line, cell.column); - - // Lookup RGB values. - let mut fg_rgb = Self::compute_fg_rgb(iter.config, iter.colors, cell.fg, cell.flags); - let mut bg_rgb = Self::compute_bg_rgb(iter.colors, cell.bg); - - let mut bg_alpha = if cell.flags.contains(Flags::INVERSE) { - mem::swap(&mut fg_rgb, &mut bg_rgb); - 1.0 - } else { - Self::compute_bg_alpha(cell.bg) - }; - - let mut is_match = false; - - if iter.is_selected(point) { - let config_bg = iter.config.colors.selection.background; - let selected_fg = iter.config.colors.selection.foreground.color(fg_rgb, bg_rgb); - bg_rgb = config_bg.color(fg_rgb, bg_rgb); - fg_rgb = selected_fg; - - if fg_rgb == bg_rgb && !cell.flags.contains(Flags::HIDDEN) { - // Reveal inversed text when fg/bg is the same. - fg_rgb = iter.colors[NamedColor::Background]; - bg_rgb = iter.colors[NamedColor::Foreground]; - bg_alpha = 1.0; - } else if config_bg != CellRgb::CellBackground { - bg_alpha = 1.0; - } - } else if iter.search.advance(iter.grid.visible_to_buffer(point)) { - // Highlight the cell if it is part of a search match. - let config_bg = iter.config.colors.search.matches.background; - let matched_fg = iter.config.colors.search.matches.foreground.color(fg_rgb, bg_rgb); - bg_rgb = config_bg.color(fg_rgb, bg_rgb); - fg_rgb = matched_fg; - - if config_bg != CellRgb::CellBackground { - bg_alpha = 1.0; - } - - is_match = true; - } - - let zerowidth = cell.zerowidth().map(|zerowidth| zerowidth.to_vec()); - - RenderableCell { - line: cell.line, - column: cell.column, - inner: RenderableCellContent::Chars((cell.c, zerowidth)), - fg: fg_rgb, - bg: bg_rgb, - bg_alpha, - flags: cell.flags, - is_match, - } - } - - fn is_empty(&self) -> bool { - self.bg_alpha == 0. - && !self.flags.intersects(Flags::UNDERLINE | Flags::STRIKEOUT | Flags::DOUBLE_UNDERLINE) - && self.inner == RenderableCellContent::Chars((' ', None)) - } - - fn compute_fg_rgb(config: &Config, colors: &color::List, fg: Color, flags: Flags) -> Rgb { - match fg { - Color::Spec(rgb) => match flags & Flags::DIM { - Flags::DIM => rgb * DIM_FACTOR, - _ => rgb, - }, - Color::Named(ansi) => { - match (config.draw_bold_text_with_bright_colors, flags & Flags::DIM_BOLD) { - // If no bright foreground is set, treat it like the BOLD flag doesn't exist. - (_, Flags::DIM_BOLD) - if ansi == NamedColor::Foreground - && config.colors.primary.bright_foreground.is_none() => - { - colors[NamedColor::DimForeground] - }, - // Draw bold text in bright colors *and* contains bold flag. - (true, Flags::BOLD) => colors[ansi.to_bright()], - // Cell is marked as dim and not bold. - (_, Flags::DIM) | (false, Flags::DIM_BOLD) => colors[ansi.to_dim()], - // None of the above, keep original color.. - _ => colors[ansi], - } - }, - Color::Indexed(idx) => { - let idx = match ( - config.draw_bold_text_with_bright_colors, - flags & Flags::DIM_BOLD, - idx, - ) { - (true, Flags::BOLD, 0..=7) => idx as usize + 8, - (false, Flags::DIM, 8..=15) => idx as usize - 8, - (false, Flags::DIM, 0..=7) => NamedColor::DimBlack as usize + idx as usize, - _ => idx as usize, - }; - - colors[idx] - }, - } - } - - /// Compute background alpha based on cell's original color. - /// - /// Since an RGB color matching the background should not be transparent, this is computed - /// using the named input color, rather than checking the RGB of the background after its color - /// is computed. - #[inline] - fn compute_bg_alpha(bg: Color) -> f32 { - if bg == Color::Named(NamedColor::Background) { - 0. - } else { - 1. - } - } - - #[inline] - fn compute_bg_rgb(colors: &color::List, bg: Color) -> Rgb { - match bg { - Color::Spec(rgb) => rgb, - Color::Named(ansi) => colors[ansi], - Color::Indexed(idx) => colors[idx], - } - } -} - -pub mod mode { - use bitflags::bitflags; - - bitflags! { - pub struct TermMode: u32 { - const NONE = 0; - const SHOW_CURSOR = 0b0000_0000_0000_0000_0001; - const APP_CURSOR = 0b0000_0000_0000_0000_0010; - const APP_KEYPAD = 0b0000_0000_0000_0000_0100; - const MOUSE_REPORT_CLICK = 0b0000_0000_0000_0000_1000; - const BRACKETED_PASTE = 0b0000_0000_0000_0001_0000; - const SGR_MOUSE = 0b0000_0000_0000_0010_0000; - const MOUSE_MOTION = 0b0000_0000_0000_0100_0000; - const LINE_WRAP = 0b0000_0000_0000_1000_0000; - const LINE_FEED_NEW_LINE = 0b0000_0000_0001_0000_0000; - const ORIGIN = 0b0000_0000_0010_0000_0000; - const INSERT = 0b0000_0000_0100_0000_0000; - const FOCUS_IN_OUT = 0b0000_0000_1000_0000_0000; - const ALT_SCREEN = 0b0000_0001_0000_0000_0000; - const MOUSE_DRAG = 0b0000_0010_0000_0000_0000; - const MOUSE_MODE = 0b0000_0010_0000_0100_1000; - const UTF8_MOUSE = 0b0000_0100_0000_0000_0000; - const ALTERNATE_SCROLL = 0b0000_1000_0000_0000_0000; - const VI = 0b0001_0000_0000_0000_0000; - const URGENCY_HINTS = 0b0010_0000_0000_0000_0000; - const ANY = std::u32::MAX; - } - } - - impl Default for TermMode { - fn default() -> TermMode { - TermMode::SHOW_CURSOR - | TermMode::LINE_WRAP - | TermMode::ALTERNATE_SCROLL - | TermMode::URGENCY_HINTS - } - } -} - -pub use crate::term::mode::TermMode; - pub struct VisualBell { /// Visual bell animation. animation: BellAnimation, @@ -1017,17 +621,19 @@ impl Term { &mut self.grid } - /// Iterate over the *renderable* cells in the terminal. + /// Terminal content required for rendering. /// - /// A renderable cell is any cell which has content other than the default - /// background color. Cells with an alternate background color are - /// considered renderable as are cells with any text content. - pub fn renderable_cells<'b, C>( + /// A renderable cell is any cell which has content other than the default background color. + /// Cells with an alternate background color are considered renderable, as are cells with any + /// text content. + /// + /// The cursor itself is always considered renderable and provided separately. + pub fn renderable_content<'b, C>( &'b self, config: &'b Config, show_cursor: bool, - ) -> RenderableCellsIter<'_, C> { - RenderableCellsIter::new(&self, config, show_cursor) + ) -> RenderableContent<'_, T, C> { + RenderableContent::new(&self, config, show_cursor) } /// Get the selection within the viewport. @@ -1393,67 +999,6 @@ impl Term { cursor_cell } - - /// Get rendering information about the active cursor. - fn renderable_cursor(&self, config: &Config, show_cursor: bool) -> RenderableCursor { - let vi_mode = self.mode.contains(TermMode::VI); - - // Cursor position. - let mut point = if vi_mode { - self.vi_mode_cursor.point - } else { - let mut point = self.grid.cursor.point; - point.line += self.grid.display_offset(); - point - }; - - // Cursor shape. - let hidden = !show_cursor - || (!self.mode.contains(TermMode::SHOW_CURSOR) && !vi_mode) - || point.line >= self.screen_lines(); - - let cursor_shape = if hidden { - point.line = Line(0); - CursorShape::Hidden - } else if !self.is_focused && config.cursor.unfocused_hollow { - CursorShape::HollowBlock - } else { - let cursor_style = self.cursor_style.unwrap_or(self.default_cursor_style); - - if vi_mode { - self.vi_mode_cursor_style.unwrap_or(cursor_style).shape - } else { - cursor_style.shape - } - }; - - // Cursor colors. - let color = if vi_mode { config.colors.vi_mode_cursor } else { config.colors.cursor }; - let cursor_color = if self.color_modified[NamedColor::Cursor as usize] { - CellRgb::Rgb(self.colors[NamedColor::Cursor]) - } else { - color.background - }; - let text_color = color.foreground; - - // Expand across wide cell when inside wide char or spacer. - let buffer_point = self.visible_to_buffer(point); - let cell = &self.grid[buffer_point.line][buffer_point.col]; - let is_wide = if cell.flags.contains(Flags::WIDE_CHAR_SPACER) { - point.col -= 1; - true - } else { - cell.flags.contains(Flags::WIDE_CHAR) - }; - - RenderableCursor { - text_color, - cursor_color, - key: CursorKey { shape: cursor_shape, is_wide }, - point, - rendered: false, - } - } } impl Dimensions for Term { diff --git a/alacritty_terminal/src/term/render.rs b/alacritty_terminal/src/term/render.rs new file mode 100644 index 00000000..eb4740b3 --- /dev/null +++ b/alacritty_terminal/src/term/render.rs @@ -0,0 +1,420 @@ +use std::cmp::max; +use std::iter; +use std::iter::Peekable; +use std::mem; +use std::ops::RangeInclusive; + +use crate::ansi::{Color, CursorShape, NamedColor}; +use crate::config::Config; +use crate::grid::{Dimensions, DisplayIter, Indexed}; +use crate::index::{Column, Direction, Line, Point}; +use crate::selection::SelectionRange; +use crate::term::cell::{Cell, Flags}; +use crate::term::color::{self, CellRgb, Rgb, DIM_FACTOR}; +use crate::term::search::RegexIter; +use crate::term::{Term, TermMode}; + +/// Minimum contrast between a fixed cursor color and the cell's background. +pub const MIN_CURSOR_CONTRAST: f64 = 1.5; + +/// Maximum number of linewraps followed outside of the viewport during search highlighting. +const MAX_SEARCH_LINES: usize = 100; + +/// Renderable terminal content. +/// +/// This provides the terminal cursor and an iterator over all non-empty cells. +pub struct RenderableContent<'a, T, C> { + term: &'a Term, + config: &'a Config, + display_iter: DisplayIter<'a, Cell>, + selection: Option>, + search: RenderableSearch<'a>, + cursor: Option, + cursor_shape: CursorShape, + cursor_point: Point, +} + +impl<'a, T, C> RenderableContent<'a, T, C> { + pub fn new(term: &'a Term, config: &'a Config, show_cursor: bool) -> Self { + // Cursor position. + let vi_mode = term.mode.contains(TermMode::VI); + let mut cursor_point = if vi_mode { + term.vi_mode_cursor.point + } else { + let mut point = term.grid.cursor.point; + point.line += term.grid.display_offset(); + point + }; + + // Cursor shape. + let cursor_shape = if !show_cursor + || (!term.mode.contains(TermMode::SHOW_CURSOR) && !vi_mode) + || cursor_point.line >= term.screen_lines() + { + cursor_point.line = Line(0); + CursorShape::Hidden + } else if !term.is_focused && config.cursor.unfocused_hollow { + CursorShape::HollowBlock + } else { + let cursor_style = term.cursor_style.unwrap_or(term.default_cursor_style); + + if vi_mode { + term.vi_mode_cursor_style.unwrap_or(cursor_style).shape + } else { + cursor_style.shape + } + }; + + Self { + display_iter: term.grid.display_iter(), + selection: term.visible_selection(), + search: RenderableSearch::new(term), + cursor: None, + cursor_shape, + cursor_point, + config, + term, + } + } + + /// Get the terminal cursor. + pub fn cursor(mut self) -> Option { + // Drain the iterator to make sure the cursor is created. + while self.next().is_some() && self.cursor.is_none() {} + + self.cursor + } + + /// Assemble the information required to render the terminal cursor. + /// + /// This will return `None` when there is no cursor visible. + fn renderable_cursor(&mut self, cell: &RenderableCell) -> Option { + if self.cursor_shape == CursorShape::Hidden { + return None; + } + + // Expand across wide cell when inside wide char or spacer. + let is_wide = if cell.flags.contains(Flags::WIDE_CHAR_SPACER) { + self.cursor_point.col -= 1; + true + } else { + cell.flags.contains(Flags::WIDE_CHAR) + }; + + // Cursor colors. + let color = if self.term.mode.contains(TermMode::VI) { + self.config.colors.vi_mode_cursor + } else { + self.config.colors.cursor + }; + let mut cursor_color = if self.term.color_modified[NamedColor::Cursor as usize] { + CellRgb::Rgb(self.term.colors[NamedColor::Cursor]) + } else { + color.background + }; + let mut text_color = color.foreground; + + // Invert the cursor if it has a fixed background close to the cell's background. + if matches!( + cursor_color, + CellRgb::Rgb(color) if color.contrast(cell.bg) < MIN_CURSOR_CONTRAST + ) { + cursor_color = CellRgb::CellForeground; + text_color = CellRgb::CellBackground; + } + + // Convert from cell colors to RGB. + let text_color = text_color.color(cell.fg, cell.bg); + let cursor_color = cursor_color.color(cell.fg, cell.bg); + + Some(RenderableCursor { + point: self.cursor_point, + shape: self.cursor_shape, + cursor_color, + text_color, + is_wide, + }) + } +} + +impl<'a, T, C> Iterator for RenderableContent<'a, T, C> { + type Item = RenderableCell; + + /// Gets the next renderable cell. + /// + /// Skips empty (background) cells and applies any flags to the cell state + /// (eg. invert fg and bg colors). + #[inline] + fn next(&mut self) -> Option { + loop { + if self.cursor_point == self.display_iter.point() { + // Handle cell at cursor position. + let cell = self.display_iter.next()?; + let mut cell = RenderableCell::new(self, cell); + + // Store the cursor which should be rendered. + self.cursor = self.renderable_cursor(&cell).map(|cursor| { + if cursor.shape == CursorShape::Block { + cell.fg = cursor.text_color; + cell.bg = cursor.cursor_color; + + // Since we draw Block cursor by drawing cell below it with a proper color, + // we must adjust alpha to make it visible. + cell.bg_alpha = 1.; + } + + cursor + }); + + return Some(cell); + } else { + // Handle non-cursor cells. + let cell = self.display_iter.next()?; + let cell = RenderableCell::new(self, cell); + + // Skip empty cells and wide char spacers. + if !cell.is_empty() && !cell.flags.contains(Flags::WIDE_CHAR_SPACER) { + return Some(cell); + } + } + } + } +} + +/// Cell ready for rendering. +#[derive(Clone, Debug)] +pub struct RenderableCell { + pub character: char, + pub zerowidth: Option>, + pub line: Line, + pub column: Column, + pub fg: Rgb, + pub bg: Rgb, + pub bg_alpha: f32, + pub flags: Flags, + pub is_match: bool, +} + +impl RenderableCell { + fn new<'a, T, C>(content: &mut RenderableContent<'a, T, C>, cell: Indexed<&Cell>) -> Self { + let point = Point::new(cell.line, cell.column); + + // Lookup RGB values. + let mut fg_rgb = + Self::compute_fg_rgb(content.config, &content.term.colors, cell.fg, cell.flags); + let mut bg_rgb = Self::compute_bg_rgb(&content.term.colors, cell.bg); + + let mut bg_alpha = if cell.flags.contains(Flags::INVERSE) { + mem::swap(&mut fg_rgb, &mut bg_rgb); + 1.0 + } else { + Self::compute_bg_alpha(cell.bg) + }; + + let grid = content.term.grid(); + let is_selected = content.selection.map_or(false, |selection| { + selection.contains_cell(grid, point, content.cursor_point, content.cursor_shape) + }); + let mut is_match = false; + + if is_selected { + let config_bg = content.config.colors.selection.background; + let selected_fg = content.config.colors.selection.foreground.color(fg_rgb, bg_rgb); + bg_rgb = config_bg.color(fg_rgb, bg_rgb); + fg_rgb = selected_fg; + + if fg_rgb == bg_rgb && !cell.flags.contains(Flags::HIDDEN) { + // Reveal inversed text when fg/bg is the same. + fg_rgb = content.term.colors[NamedColor::Background]; + bg_rgb = content.term.colors[NamedColor::Foreground]; + bg_alpha = 1.0; + } else if config_bg != CellRgb::CellBackground { + bg_alpha = 1.0; + } + } else if content.search.advance(grid.visible_to_buffer(point)) { + // Highlight the cell if it is part of a search match. + let config_bg = content.config.colors.search.matches.background; + let matched_fg = content.config.colors.search.matches.foreground.color(fg_rgb, bg_rgb); + bg_rgb = config_bg.color(fg_rgb, bg_rgb); + fg_rgb = matched_fg; + + if config_bg != CellRgb::CellBackground { + bg_alpha = 1.0; + } + + is_match = true; + } + + RenderableCell { + character: cell.c, + zerowidth: cell.zerowidth().map(|zerowidth| zerowidth.to_vec()), + line: cell.line, + column: cell.column, + fg: fg_rgb, + bg: bg_rgb, + bg_alpha, + flags: cell.flags, + is_match, + } + } + + /// Check if cell contains any renderable content. + fn is_empty(&self) -> bool { + self.bg_alpha == 0. + && !self.flags.intersects(Flags::UNDERLINE | Flags::STRIKEOUT | Flags::DOUBLE_UNDERLINE) + && self.character == ' ' + && self.zerowidth.is_none() + } + + /// Get the RGB color from a cell's foreground color. + fn compute_fg_rgb(config: &Config, colors: &color::List, fg: Color, flags: Flags) -> Rgb { + match fg { + Color::Spec(rgb) => match flags & Flags::DIM { + Flags::DIM => rgb * DIM_FACTOR, + _ => rgb, + }, + Color::Named(ansi) => { + match (config.draw_bold_text_with_bright_colors, flags & Flags::DIM_BOLD) { + // If no bright foreground is set, treat it like the BOLD flag doesn't exist. + (_, Flags::DIM_BOLD) + if ansi == NamedColor::Foreground + && config.colors.primary.bright_foreground.is_none() => + { + colors[NamedColor::DimForeground] + }, + // Draw bold text in bright colors *and* contains bold flag. + (true, Flags::BOLD) => colors[ansi.to_bright()], + // Cell is marked as dim and not bold. + (_, Flags::DIM) | (false, Flags::DIM_BOLD) => colors[ansi.to_dim()], + // None of the above, keep original color.. + _ => colors[ansi], + } + }, + Color::Indexed(idx) => { + let idx = match ( + config.draw_bold_text_with_bright_colors, + flags & Flags::DIM_BOLD, + idx, + ) { + (true, Flags::BOLD, 0..=7) => idx as usize + 8, + (false, Flags::DIM, 8..=15) => idx as usize - 8, + (false, Flags::DIM, 0..=7) => NamedColor::DimBlack as usize + idx as usize, + _ => idx as usize, + }; + + colors[idx] + }, + } + } + + /// Get the RGB color from a cell's background color. + #[inline] + fn compute_bg_rgb(colors: &color::List, bg: Color) -> Rgb { + match bg { + Color::Spec(rgb) => rgb, + Color::Named(ansi) => colors[ansi], + Color::Indexed(idx) => colors[idx], + } + } + + /// Compute background alpha based on cell's original color. + /// + /// Since an RGB color matching the background should not be transparent, this is computed + /// using the named input color, rather than checking the RGB of the background after its color + /// is computed. + #[inline] + fn compute_bg_alpha(bg: Color) -> f32 { + if bg == Color::Named(NamedColor::Background) { + 0. + } else { + 1. + } + } +} + +/// Cursor storing all information relevant for rendering. +#[derive(Debug, Eq, PartialEq, Copy, Clone)] +pub struct RenderableCursor { + shape: CursorShape, + cursor_color: Rgb, + text_color: Rgb, + is_wide: bool, + point: Point, +} + +impl RenderableCursor { + pub fn color(&self) -> Rgb { + self.cursor_color + } + + pub fn shape(&self) -> CursorShape { + self.shape + } + + pub fn is_wide(&self) -> bool { + self.is_wide + } + + pub fn point(&self) -> Point { + self.point + } +} + +type MatchIter<'a> = Box>> + 'a>; + +/// Regex search highlight tracking. +struct RenderableSearch<'a> { + iter: Peekable>, +} + +impl<'a> RenderableSearch<'a> { + /// Create a new renderable search iterator. + fn new(term: &'a Term) -> Self { + let viewport_end = term.grid().display_offset(); + let viewport_start = viewport_end + term.screen_lines().0 - 1; + + // Compute start of the first and end of the last line. + let start_point = Point::new(viewport_start, Column(0)); + let mut start = term.line_search_left(start_point); + let end_point = Point::new(viewport_end, term.cols() - 1); + let mut end = term.line_search_right(end_point); + + // Set upper bound on search before/after the viewport to prevent excessive blocking. + if start.line > viewport_start + MAX_SEARCH_LINES { + if start.line == 0 { + // Do not highlight anything if this line is the last. + let iter: MatchIter<'a> = Box::new(iter::empty()); + return Self { iter: iter.peekable() }; + } else { + // Start at next line if this one is too long. + start.line -= 1; + } + } + end.line = max(end.line, viewport_end.saturating_sub(MAX_SEARCH_LINES)); + + // Create an iterater for the current regex search for all visible matches. + let iter: MatchIter<'a> = Box::new( + RegexIter::new(start, end, Direction::Right, &term) + .skip_while(move |rm| rm.end().line > viewport_start) + .take_while(move |rm| rm.start().line >= viewport_end), + ); + + Self { iter: iter.peekable() } + } + + /// Advance the search tracker to the next point. + /// + /// This will return `true` if the point passed is part of a search match. + fn advance(&mut self, point: Point) -> bool { + while let Some(regex_match) = &self.iter.peek() { + if regex_match.start() > &point { + break; + } else if regex_match.end() < &point { + let _ = self.iter.next(); + } else { + return true; + } + } + false + } +}