summaryrefslogtreecommitdiff
path: root/alacritty_terminal/src/term/render.rs
diff options
context:
space:
mode:
Diffstat (limited to 'alacritty_terminal/src/term/render.rs')
-rw-r--r--alacritty_terminal/src/term/render.rs420
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
+ }
+}