alacritty/alacritty/src/display/content.rs

506 lines
17 KiB
Rust
Raw Normal View History

use std::borrow::Cow;
use std::cmp::max;
use std::mem;
use std::ops::{Deref, DerefMut, RangeInclusive};
use alacritty_terminal::ansi::{Color, CursorShape, NamedColor};
use alacritty_terminal::config::Config;
use alacritty_terminal::event::EventListener;
use alacritty_terminal::grid::{Dimensions, Indexed};
use alacritty_terminal::index::{Column, Direction, Line, Point};
use alacritty_terminal::term::cell::{Cell, Flags};
use alacritty_terminal::term::color::{CellRgb, Rgb};
use alacritty_terminal::term::search::{RegexIter, RegexSearch};
use alacritty_terminal::term::{
RenderableContent as TerminalContent, RenderableCursor as TerminalCursor, Term, TermMode,
};
use crate::config::ui_config::UiConfig;
use crate::display::color::{List, DIM_FACTOR};
use crate::display::hint::HintState;
use crate::display::Display;
/// 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> {
terminal_content: TerminalContent<'a>,
terminal_cursor: TerminalCursor,
cursor: Option<RenderableCursor>,
search: Regex<'a>,
hint: Hint<'a>,
config: &'a Config<UiConfig>,
colors: &'a List,
}
impl<'a> RenderableContent<'a> {
pub fn new<T: EventListener>(
config: &'a Config<UiConfig>,
display: &'a mut Display,
term: &'a Term<T>,
search_dfas: Option<&RegexSearch>,
) -> Self {
let search = search_dfas.map(|dfas| Regex::new(&term, dfas)).unwrap_or_default();
let terminal_content = term.renderable_content();
// Copy the cursor and override its shape if necessary.
let mut terminal_cursor = terminal_content.cursor;
if terminal_cursor.shape == CursorShape::Hidden
|| display.cursor_hidden
|| search_dfas.is_some()
{
terminal_cursor.shape = CursorShape::Hidden;
} else if !term.is_focused && config.cursor.unfocused_hollow {
terminal_cursor.shape = CursorShape::HollowBlock;
}
display.hint_state.update_matches(term);
let hint = Hint::from(&display.hint_state);
let colors = &display.colors;
Self { cursor: None, terminal_content, terminal_cursor, search, config, colors, hint }
}
/// Viewport offset.
pub fn display_offset(&self) -> usize {
self.terminal_content.display_offset
}
/// 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
}
/// Get the RGB value for a color index.
pub fn color(&self, color: usize) -> Rgb {
self.terminal_content.colors[color].unwrap_or(self.colors[color])
}
/// 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.terminal_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.terminal_cursor.point.column -= 1;
true
} else {
cell.flags.contains(Flags::WIDE_CHAR)
};
// Cursor colors.
let color = if self.terminal_content.mode.contains(TermMode::VI) {
self.config.ui_config.colors.vi_mode_cursor
} else {
self.config.ui_config.colors.cursor
};
let mut cursor_color =
self.terminal_content.colors[NamedColor::Cursor].map_or(color.background, CellRgb::Rgb);
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.terminal_cursor.point,
shape: self.terminal_cursor.shape,
cursor_color,
text_color,
is_wide,
})
}
}
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<Self::Item> {
loop {
let cell = self.terminal_content.display_iter.next()?;
let mut cell = RenderableCell::new(self, cell);
if self.terminal_cursor.point == cell.point {
// 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 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 zerowidth: Option<Vec<char>>,
pub point: Point,
pub fg: Rgb,
pub bg: Rgb,
pub bg_alpha: f32,
pub flags: Flags,
pub is_match: bool,
}
impl RenderableCell {
fn new<'a>(content: &mut RenderableContent<'a>, cell: Indexed<&Cell, Line>) -> Self {
// Lookup RGB values.
let mut fg_rgb = Self::compute_fg_rgb(content, cell.fg, cell.flags);
let mut bg_rgb = Self::compute_bg_rgb(content, 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 is_selected = content
.terminal_content
.selection
.map_or(false, |selection| selection.contains_cell(&cell, content.terminal_cursor));
let mut is_match = false;
let mut character = cell.c;
let colors = &content.config.ui_config.colors;
if let Some((c, is_first)) = content.hint.advance(cell.point) {
let (config_fg, config_bg) = if is_first {
(colors.hints.start.foreground, colors.hints.start.background)
} else {
(colors.hints.end.foreground, colors.hints.end.background)
};
Self::compute_cell_rgb(&mut fg_rgb, &mut bg_rgb, &mut bg_alpha, config_fg, config_bg);
character = c;
} else if is_selected {
let config_fg = colors.selection.foreground;
let config_bg = colors.selection.background;
Self::compute_cell_rgb(&mut fg_rgb, &mut bg_rgb, &mut bg_alpha, config_fg, config_bg);
if fg_rgb == bg_rgb && !cell.flags.contains(Flags::HIDDEN) {
// Reveal inversed text when fg/bg is the same.
fg_rgb = content.color(NamedColor::Background as usize);
bg_rgb = content.color(NamedColor::Foreground as usize);
bg_alpha = 1.0;
}
} else if content.search.advance(cell.point) {
// Highlight the cell if it is part of a search match.
let config_fg = colors.search.matches.foreground;
let config_bg = colors.search.matches.background;
Self::compute_cell_rgb(&mut fg_rgb, &mut bg_rgb, &mut bg_alpha, config_fg, config_bg);
is_match = true;
}
RenderableCell {
character,
zerowidth: cell.zerowidth().map(|zerowidth| zerowidth.to_vec()),
point: cell.point,
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()
}
/// 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: &mut RenderableContent<'_>, fg: Color, flags: Flags) -> Rgb {
let ui_config = &content.config.ui_config;
match fg {
Color::Spec(rgb) => match flags & Flags::DIM {
Flags::DIM => rgb * DIM_FACTOR,
_ => rgb,
},
Color::Named(ansi) => {
match (ui_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
&& ui_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 (
ui_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,
};
content.color(idx)
},
}
}
/// Get the RGB color from a cell's background color.
#[inline]
fn compute_bg_rgb(content: &mut RenderableContent<'_>, bg: Color) -> Rgb {
match bg {
Color::Spec(rgb) => rgb,
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(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
}
}
/// Regex hints for keyboard shortcuts.
struct Hint<'a> {
/// Hint matches and position.
regex: Regex<'a>,
/// Last match checked against current cell position.
labels: &'a Vec<Vec<char>>,
}
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.
fn advance(&mut self, point: Point) -> Option<(char, bool)> {
// Check if we're within a match at all.
if !self.regex.advance(point) {
return None;
}
// Match starting position on this line; linebreaks interrupt the hint labels.
let start = self
.regex
.matches
.get(self.regex.index)
.map(|regex_match| regex_match.start())
.filter(|start| start.line == point.line)?;
// Position within the hint label.
let label_position = point.column.0 - start.column.0;
let is_first = label_position == 0;
// Hint label character.
self.labels[self.regex.index].get(label_position).copied().map(|c| (c, is_first))
}
}
impl<'a> From<&'a HintState> for Hint<'a> {
fn from(hint_state: &'a HintState) -> Self {
let regex = Regex { matches: Cow::Borrowed(hint_state.matches()), index: 0 };
Self { labels: hint_state.labels(), regex }
}
}
/// Wrapper for finding visible regex matches.
#[derive(Default, Clone)]
pub struct RegexMatches(Vec<RangeInclusive<Point>>);
impl RegexMatches {
/// Find all visible matches.
pub fn new<T>(term: &Term<T>, dfas: &RegexSearch) -> 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.
return Self::default();
} 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 = RegexIter::new(start, end, Direction::Right, term, dfas)
.skip_while(move |rm| rm.end().line > viewport_start)
.take_while(move |rm| rm.start().line >= viewport_end)
.map(|rm| {
let viewport_start = term.grid().clamp_buffer_to_visible(*rm.start());
let viewport_end = term.grid().clamp_buffer_to_visible(*rm.end());
viewport_start..=viewport_end
});
Self(iter.collect())
}
}
impl Deref for RegexMatches {
type Target = Vec<RangeInclusive<Point>>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for RegexMatches {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
/// Visible regex match tracking.
#[derive(Default)]
struct Regex<'a> {
/// All visible matches.
matches: Cow<'a, RegexMatches>,
/// Index of the last match checked.
index: usize,
}
impl<'a> Regex<'a> {
/// Create a new renderable regex iterator.
fn new<T>(term: &Term<T>, dfas: &RegexSearch) -> Self {
let matches = Cow::Owned(RegexMatches::new(term, dfas));
Self { index: 0, 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(regex_match) = self.matches.get(self.index) {
if regex_match.start() > &point {
break;
} else if regex_match.end() < &point {
self.index += 1;
} else {
return true;
}
}
false
}
}