diff options
Diffstat (limited to 'alacritty_terminal/src/term/render.rs')
-rw-r--r-- | alacritty_terminal/src/term/render.rs | 420 |
1 files changed, 420 insertions, 0 deletions
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 + } +} |