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.
This commit is contained in:
Kirill Chibisov 2020-12-28 12:45:39 +03:00 committed by GitHub
parent fdc10d270e
commit 12fbd0051c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 649 additions and 668 deletions

View File

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

View File

@ -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<RenderRect>; 4],
index: usize,
}
impl From<RenderRect> 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<Self::Item> {
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,
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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
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<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);
}
}
}
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<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
}
/// 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.
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<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> {

View File

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