use std::borrow::Cow; use std::num::NonZeroU32; use std::ops::Deref; use std::{cmp, mem}; use alacritty_terminal::event::EventListener; use alacritty_terminal::grid::{Dimensions, Indexed}; use alacritty_terminal::index::{Column, Line, Point}; use alacritty_terminal::selection::SelectionRange; use alacritty_terminal::term::cell::{Cell, Flags, Hyperlink}; use alacritty_terminal::term::search::{Match, RegexSearch}; use alacritty_terminal::term::{self, RenderableContent as TerminalContent, Term, TermMode}; use alacritty_terminal::vte::ansi::{Color, CursorShape, NamedColor}; use crate::config::UiConfig; use crate::display::color::{CellRgb, List, Rgb, DIM_FACTOR}; use crate::display::hint::{self, HintState}; use crate::display::{Display, SizeInfo}; use crate::event::SearchState; /// Minimum contrast between a fixed cursor color and the cell's background. pub const MIN_CURSOR_CONTRAST: f64 = 1.5; /// Renderable terminal content. /// /// This provides the terminal cursor and an iterator over all non-empty cells. pub struct RenderableContent<'a> { terminal_content: TerminalContent<'a>, cursor: RenderableCursor, cursor_shape: CursorShape, cursor_point: Point, search: Option>, hint: Option>, config: &'a UiConfig, colors: &'a List, focused_match: Option<&'a Match>, size: &'a SizeInfo, } impl<'a> RenderableContent<'a> { pub fn new( config: &'a UiConfig, display: &'a mut Display, term: &'a Term, search_state: &'a mut SearchState, ) -> Self { let search = search_state.dfas().map(|dfas| HintMatches::visible_regex_matches(term, dfas)); let focused_match = search_state.focused_match(); let terminal_content = term.renderable_content(); // Find terminal cursor shape. let cursor_shape = if terminal_content.cursor.shape == CursorShape::Hidden || display.cursor_hidden || search_state.regex().is_some() || display.ime.preedit().is_some() { CursorShape::Hidden } else if !term.is_focused && config.cursor.unfocused_hollow { CursorShape::HollowBlock } else { terminal_content.cursor.shape }; // Convert terminal cursor point to viewport position. let cursor_point = terminal_content.cursor.point; let display_offset = terminal_content.display_offset; let cursor_point = term::point_to_viewport(display_offset, cursor_point).unwrap(); let hint = if display.hint_state.active() { display.hint_state.update_matches(term); Some(Hint::from(&display.hint_state)) } else { None }; Self { colors: &display.colors, size: &display.size_info, cursor: RenderableCursor::new_hidden(), terminal_content, focused_match, cursor_shape, cursor_point, search, config, hint, } } /// Viewport offset. pub fn display_offset(&self) -> usize { self.terminal_content.display_offset } /// Get the terminal cursor. pub fn cursor(mut self) -> RenderableCursor { // Assure this function is only called after the iterator has been drained. debug_assert!(self.next().is_none()); self.cursor } /// Get the RGB value for a color index. pub fn color(&self, color: usize) -> Rgb { self.terminal_content.colors[color].map(Rgb).unwrap_or(self.colors[color]) } pub fn selection_range(&self) -> Option { self.terminal_content.selection } /// Assemble the information required to render the terminal cursor. fn renderable_cursor(&mut self, cell: &RenderableCell) -> RenderableCursor { // Cursor colors. let color = if self.terminal_content.mode.contains(TermMode::VI) { self.config.colors.vi_mode_cursor } else { self.config.colors.cursor }; let cursor_color = self.terminal_content.colors[NamedColor::Cursor] .map_or(color.background, |c| CellRgb::Rgb(Rgb(c))); let text_color = color.foreground; let insufficient_contrast = (!matches!(cursor_color, CellRgb::Rgb(_)) || !matches!(text_color, CellRgb::Rgb(_))) && cell.fg.contrast(*cell.bg) < MIN_CURSOR_CONTRAST; // Convert from cell colors to RGB. let mut text_color = text_color.color(cell.fg, cell.bg); let mut cursor_color = cursor_color.color(cell.fg, cell.bg); // Invert cursor color with insufficient contrast to prevent invisible cursors. if insufficient_contrast { cursor_color = self.config.colors.primary.foreground; text_color = self.config.colors.primary.background; } let width = if cell.flags.contains(Flags::WIDE_CHAR) { NonZeroU32::new(2).unwrap() } else { NonZeroU32::new(1).unwrap() }; RenderableCursor { width, shape: self.cursor_shape, point: self.cursor_point, cursor_color, text_color, } } } impl<'a> Iterator for RenderableContent<'a> { type Item = RenderableCell; /// Gets the next renderable cell. /// /// Skips empty (background) cells and applies any flags to the cell state /// (eg. invert fg and bg colors). #[inline] fn next(&mut self) -> Option { loop { let cell = self.terminal_content.display_iter.next()?; let mut cell = RenderableCell::new(self, cell); if self.cursor_point == cell.point { // Store the cursor which should be rendered. self.cursor = self.renderable_cursor(&cell); if self.cursor.shape == CursorShape::Block { cell.fg = self.cursor.text_color; cell.bg = self.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.; } return Some(cell); } else if !cell.is_empty() && !cell.flags.contains(Flags::WIDE_CHAR_SPACER) { // Skip empty cells and wide char spacers. return Some(cell); } } } } /// Cell ready for rendering. #[derive(Clone, Debug)] pub struct RenderableCell { pub character: char, pub point: Point, pub fg: Rgb, pub bg: Rgb, pub bg_alpha: f32, pub underline: Rgb, pub flags: Flags, pub extra: Option>, } /// Extra storage with rarely present fields for [`RenderableCell`], to reduce the cell size we /// pass around. #[derive(Clone, Debug)] pub struct RenderableCellExtra { pub zerowidth: Option>, pub hyperlink: Option, } impl RenderableCell { fn new(content: &mut RenderableContent<'_>, cell: Indexed<&Cell>) -> Self { // Lookup RGB values. let mut fg = Self::compute_fg_rgb(content, cell.fg, cell.flags); let mut bg = Self::compute_bg_rgb(content, cell.bg); let mut bg_alpha = if cell.flags.contains(Flags::INVERSE) { mem::swap(&mut fg, &mut bg); 1.0 } else { Self::compute_bg_alpha(content.config, cell.bg) }; let is_selected = content.terminal_content.selection.map_or(false, |selection| { selection.contains_cell( &cell, content.terminal_content.cursor.point, content.cursor_shape, ) }); let display_offset = content.terminal_content.display_offset; let viewport_start = Point::new(Line(-(display_offset as i32)), Column(0)); let colors = &content.config.colors; let mut character = cell.c; let mut flags = cell.flags; let num_cols = content.size.columns(); if let Some((c, is_first)) = content .hint .as_mut() .and_then(|hint| hint.advance(viewport_start, num_cols, cell.point)) { if is_first { let (config_fg, config_bg) = (colors.hints.start.foreground, colors.hints.start.background); Self::compute_cell_rgb(&mut fg, &mut bg, &mut bg_alpha, config_fg, config_bg); } else if c.is_some() { let (config_fg, config_bg) = (colors.hints.end.foreground, colors.hints.end.background); Self::compute_cell_rgb(&mut fg, &mut bg, &mut bg_alpha, config_fg, config_bg); } else { flags.insert(Flags::UNDERLINE); } character = c.unwrap_or(character); } else if is_selected { let config_fg = colors.selection.foreground; let config_bg = colors.selection.background; Self::compute_cell_rgb(&mut fg, &mut bg, &mut bg_alpha, config_fg, config_bg); if fg == bg && !cell.flags.contains(Flags::HIDDEN) { // Reveal inversed text when fg/bg is the same. fg = content.color(NamedColor::Background as usize); bg = content.color(NamedColor::Foreground as usize); bg_alpha = 1.0; } } else if content.search.as_mut().map_or(false, |search| search.advance(cell.point)) { let focused = content.focused_match.map_or(false, |fm| fm.contains(&cell.point)); let (config_fg, config_bg) = if focused { (colors.search.focused_match.foreground, colors.search.focused_match.background) } else { (colors.search.matches.foreground, colors.search.matches.background) }; Self::compute_cell_rgb(&mut fg, &mut bg, &mut bg_alpha, config_fg, config_bg); } // Apply transparency to all renderable cells if `transparent_background_colors` is set if bg_alpha > 0. && content.config.colors.transparent_background_colors { bg_alpha = content.config.window_opacity(); } // Convert cell point to viewport position. let cell_point = cell.point; let point = term::point_to_viewport(display_offset, cell_point).unwrap(); let underline = cell .underline_color() .map_or(fg, |underline| Self::compute_fg_rgb(content, underline, flags)); let zerowidth = cell.zerowidth(); let hyperlink = cell.hyperlink(); let extra = (zerowidth.is_some() || hyperlink.is_some()).then(|| { Box::new(RenderableCellExtra { zerowidth: zerowidth.map(|zerowidth| zerowidth.to_vec()), hyperlink, }) }); RenderableCell { flags, character, bg_alpha, point, fg, bg, underline, extra } } /// Check if cell contains any renderable content. fn is_empty(&self) -> bool { self.bg_alpha == 0. && self.character == ' ' && self.extra.is_none() && !self.flags.intersects(Flags::ALL_UNDERLINES | Flags::STRIKEOUT) } /// Apply [`CellRgb`] colors to the cell's colors. fn compute_cell_rgb( cell_fg: &mut Rgb, cell_bg: &mut Rgb, bg_alpha: &mut f32, fg: CellRgb, bg: CellRgb, ) { let old_fg = mem::replace(cell_fg, fg.color(*cell_fg, *cell_bg)); *cell_bg = bg.color(old_fg, *cell_bg); if bg != CellRgb::CellBackground { *bg_alpha = 1.0; } } /// Get the RGB color from a cell's foreground color. fn compute_fg_rgb(content: &RenderableContent<'_>, fg: Color, flags: Flags) -> Rgb { let config = &content.config; match fg { Color::Spec(rgb) => match flags & Flags::DIM { Flags::DIM => { let rgb: Rgb = rgb.into(); rgb * DIM_FACTOR }, _ => rgb.into(), }, Color::Named(ansi) => { match (config.colors.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() => { content.color(NamedColor::DimForeground as usize) }, // Draw bold text in bright colors *and* contains bold flag. (true, Flags::BOLD) => content.color(ansi.to_bright() as usize), // Cell is marked as dim and not bold. (_, Flags::DIM) | (false, Flags::DIM_BOLD) => { content.color(ansi.to_dim() as usize) }, // None of the above, keep original color.. _ => content.color(ansi as usize), } }, Color::Indexed(idx) => { let idx = match ( config.colors.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, }; content.color(idx) }, } } /// Get the RGB color from a cell's background color. #[inline] fn compute_bg_rgb(content: &RenderableContent<'_>, bg: Color) -> Rgb { match bg { Color::Spec(rgb) => rgb.into(), Color::Named(ansi) => content.color(ansi as usize), Color::Indexed(idx) => content.color(idx as usize), } } /// 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(config: &UiConfig, bg: Color) -> f32 { if bg == Color::Named(NamedColor::Background) { 0. } else if config.colors.transparent_background_colors { config.window_opacity() } 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, width: NonZeroU32, point: Point, } impl RenderableCursor { fn new_hidden() -> Self { let shape = CursorShape::Hidden; let cursor_color = Rgb::default(); let text_color = Rgb::default(); let width = NonZeroU32::new(1).unwrap(); let point = Point::default(); Self { shape, cursor_color, text_color, width, point } } } impl RenderableCursor { pub fn new( point: Point, shape: CursorShape, cursor_color: Rgb, width: NonZeroU32, ) -> Self { Self { shape, cursor_color, text_color: cursor_color, width, point } } pub fn color(&self) -> Rgb { self.cursor_color } pub fn shape(&self) -> CursorShape { self.shape } pub fn width(&self) -> NonZeroU32 { self.width } pub fn point(&self) -> Point { self.point } } /// Regex hints for keyboard shortcuts. struct Hint<'a> { /// Hint matches and position. matches: HintMatches<'a>, /// Last match checked against current cell position. labels: &'a Vec>, } impl<'a> Hint<'a> { /// Advance the hint iterator. /// /// If the point is within a hint, the keyboard shortcut character that should be displayed at /// this position will be returned. /// /// The tuple's [`bool`] will be `true` when the character is the first for this hint. /// /// The tuple's [`Option`] will be [`None`] when the point is part of the match, but not /// part of the hint label. fn advance( &mut self, viewport_start: Point, num_cols: usize, point: Point, ) -> Option<(Option, bool)> { // Check if we're within a match at all. if !self.matches.advance(point) { return None; } // Match starting position on this line; linebreaks interrupt the hint labels. let start = self .matches .get(self.matches.index) .map(|bounds| cmp::max(*bounds.start(), viewport_start))?; // Position within the hint label. let line_delta = point.line.0 - start.line.0; let col_delta = point.column.0 as i32 - start.column.0 as i32; let label_position = usize::try_from(line_delta * num_cols as i32 + col_delta).unwrap_or(0); let is_first = label_position == 0; // Hint label character. let hint_char = self.labels[self.matches.index] .get(label_position) .copied() .map(|c| (Some(c), is_first)) .unwrap_or((None, false)); Some(hint_char) } } impl<'a> From<&'a HintState> for Hint<'a> { fn from(hint_state: &'a HintState) -> Self { let matches = HintMatches::new(hint_state.matches()); Self { labels: hint_state.labels(), matches } } } /// Visible hint match tracking. #[derive(Default)] struct HintMatches<'a> { /// All visible matches. matches: Cow<'a, [Match]>, /// Index of the last match checked. index: usize, } impl<'a> HintMatches<'a> { /// Create new renderable matches iterator.. fn new(matches: impl Into>) -> Self { Self { matches: matches.into(), index: 0 } } /// Create from regex matches on term visible part. fn visible_regex_matches(term: &Term, dfas: &mut RegexSearch) -> Self { let matches = hint::visible_regex_match_iter(term, dfas).collect::>(); Self::new(matches) } /// Advance the regex tracker to the next point. /// /// This will return `true` if the point passed is part of a regex match. fn advance(&mut self, point: Point) -> bool { while let Some(bounds) = self.get(self.index) { if bounds.start() > &point { break; } else if bounds.end() < &point { self.index += 1; } else { return true; } } false } } impl<'a> Deref for HintMatches<'a> { type Target = [Match]; fn deref(&self) -> &Self::Target { self.matches.deref() } }