summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKirill Chibisov <contact@kchibisov.com>2020-12-28 12:45:39 +0300
committerGitHub <noreply@github.com>2020-12-28 09:45:39 +0000
commit12fbd0051cd743bcea79f45777325f76485fd865 (patch)
tree8e09bf529451b21bfffaa27ed42116338837216c
parentfdc10d270e423e6bec756cab61b502e28260129e (diff)
downloadalacritty-12fbd0051cd743bcea79f45777325f76485fd865.tar.gz
alacritty-12fbd0051cd743bcea79f45777325f76485fd865.zip
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.
-rw-r--r--CHANGELOG.md2
-rw-r--r--alacritty/src/cursor.rs168
-rw-r--r--alacritty/src/display.rs33
-rw-r--r--alacritty/src/input.rs3
-rw-r--r--alacritty/src/renderer/mod.rs58
-rw-r--r--alacritty/src/renderer/rects.rs3
-rw-r--r--alacritty/src/url.rs16
-rw-r--r--alacritty_terminal/src/index.rs2
-rw-r--r--alacritty_terminal/src/selection.rs67
-rw-r--r--alacritty_terminal/src/term/mod.rs547
-rw-r--r--alacritty_terminal/src/term/render.rs420
11 files changed, 650 insertions, 669 deletions
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;
-
- // Double the cursor width if it's above a double-width glyph.
- if is_wide {
- width *= 2;
- }
+use crate::renderer::rects::RenderRect;
- 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]);
-
- // 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,
+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();
+
+ let mut width = size_info.cell_width();
+ let height = size_info.cell_height();
+
+ if self.is_wide() {
+ width *= 2.;
+ }
+
+ let thickness = (thickness * width as f32).round().max(1.);
+
+ 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(),
+ }
}
}
-/// 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]);
-
- // 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,
- }
+/// Cursor rect iterator.
+#[derive(Default)]
+pub struct CursorRects {
+ rects: [Option<RenderRect>; 4],
+ index: usize,
}
-/// 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]);
- }
- }
+impl From<RenderRect> for CursorRects {
+ fn from(rect: RenderRect) -> Self {
+ Self { rects: [Some(rect), None, None, None], index: 0 }
}
+}
+
+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: BitmapBuffer::RGB(buffer),
+ fn next(&mut self) -> Option<Self::Item> {
+ let rect = self.rects.get_mut(self.index)?;
+ self.index += 1;
+ rect.take()
}
}
-/// 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]);
-
- // 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,
+/// 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::<Vec<_>>();
+ // 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<GlyphKey, Glyph, BuildHasherDefault<FnvHasher>>,
- /// Cache of buffered cursor glyphs.
- cursor_cache: HashMap<CursorKey, Glyph, BuildHasherDefault<FnvHasher>>,
-
/// 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<L: LoadGlyph>(&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<F, T>(
- &mut self,
- config: &UIConfig,
- cursor_config: Cursor,
- props: &SizeInfo,
- func: F,
- ) -> T
+ pub fn with_api<F, T>(&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<RenderableCell> {
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<L> SelectionRange<L> {
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<L>) -> 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<Line> {
+ /// Check if the cell at a point is part of the selection.
+ pub fn contains_cell<T>(
+ &self,
+ grid: &Grid<T>,
+ 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<dyn Iterator<Item = RangeInclusive<Point<usize>>> + 'a>;
-
-/// Regex search highlight tracking.
-pub struct RenderableSearch<'a> {
- iter: Peekable<MatchIter<'a>>,
-}
-
-impl<'a> RenderableSearch<'a> {
- /// Create a new renderable search iterator.
- fn new<T>(term: &'a Term<T>) -> 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<usize>) -> 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
- }
-}
-
-/// 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<Cell>,
- cursor: RenderableCursor,
- config: &'a Config<C>,
- colors: &'a color::List,
- selection: Option<SelectionRange<Line>>,
- 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<Self::Item> {
- 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);
- }
- }
- }
+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<'a, C> RenderableCellsIter<'a, C> {
- /// Create the renderable cells iterator.
- ///
- /// The cursor and terminal mode are required for properly displaying the
- /// cursor.
- fn new<T>(
- term: &'a Term<T>,
- config: &'a Config<C>,
- 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<RenderableCell> {
- // 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
+impl Default for TermMode {
+ fn default() -> TermMode {
+ TermMode::SHOW_CURSOR
+ | TermMode::LINE_WRAP
+ | TermMode::ALTERNATE_SCROLL
+ | TermMode::URGENCY_HINTS
}
-
- /// 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<Vec<char>>)),
- 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<C>(config: &Config<C>, 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<T> Term<T> {
&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.
///
- /// 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>(
+ /// The cursor itself is always considered renderable and provided separately.
+ pub fn renderable_content<'b, C>(
&'b self,
config: &'b Config<C>,
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<T> Term<T> {
cursor_cell
}
-
- /// Get rendering information about the active cursor.
- fn renderable_cursor<C>(&self, config: &Config<C>, 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<T> Dimensions for Term<T> {
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<T>,
+ config: &'a Config<C>,
+ display_iter: DisplayIter<'a, Cell>,
+ selection: Option<SelectionRange<Line>>,
+ search: RenderableSearch<'a>,
+ cursor: Option<RenderableCursor>,
+ cursor_shape: CursorShape,
+ cursor_point: Point,
+}
+
+impl<'a, T, C> RenderableContent<'a, T, C> {
+ pub fn new(term: &'a Term<T>, config: &'a Config<C>, 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<RenderableCursor> {
+ // 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<RenderableCursor> {
+ 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<Self::Item> {
+ 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<Vec<char>>,
+ 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<C>(config: &Config<C>, 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<dyn Iterator<Item = RangeInclusive<Point<usize>>> + 'a>;
+
+/// Regex search highlight tracking.
+struct RenderableSearch<'a> {
+ iter: Peekable<MatchIter<'a>>,
+}
+
+impl<'a> RenderableSearch<'a> {
+ /// Create a new renderable search iterator.
+ fn new<T>(term: &'a Term<T>) -> 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<usize>) -> 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
+ }
+}