aboutsummaryrefslogtreecommitdiff
path: root/alacritty/src/display/content.rs
diff options
context:
space:
mode:
authorChristian Duerr <contact@christianduerr.com>2021-01-24 21:45:36 +0000
committerGitHub <noreply@github.com>2021-01-24 21:45:36 +0000
commit530de00049c2afcc562d36ccdb3e6afa2fe396a5 (patch)
tree3dabbcef3fc4a2041f9027d82243aa0d70928153 /alacritty/src/display/content.rs
parent7291702f6b4fff10f2470f084abe0785b95659a0 (diff)
downloadr-alacritty-530de00049c2afcc562d36ccdb3e6afa2fe396a5.tar.gz
r-alacritty-530de00049c2afcc562d36ccdb3e6afa2fe396a5.tar.bz2
r-alacritty-530de00049c2afcc562d36ccdb3e6afa2fe396a5.zip
Move renderable cell transformation to alacritty
This refactors a large chunk of the alacritty_terminal API to expose all data necessary for rendering uniformly through the `renderable_content` call. This also no longer transforms the cells for rendering by a GUI but instead just reports the content from a terminal emulation perspective. The transformation into renderable cells is now done inside the alacritty crate. Since the terminal itself only ever needs to know about modified color RGB values, the configuration for colors was moved to the alacritty UI code.
Diffstat (limited to 'alacritty/src/display/content.rs')
-rw-r--r--alacritty/src/display/content.rs404
1 files changed, 404 insertions, 0 deletions
diff --git a/alacritty/src/display/content.rs b/alacritty/src/display/content.rs
new file mode 100644
index 00000000..81c2977f
--- /dev/null
+++ b/alacritty/src/display/content.rs
@@ -0,0 +1,404 @@
+use std::cmp::max;
+use std::mem;
+use std::ops::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};
+
+/// 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: RenderableSearch,
+ config: &'a Config<UIConfig>,
+ colors: &'a List,
+}
+
+impl<'a> RenderableContent<'a> {
+ pub fn new<T: EventListener>(
+ term: &'a Term<T>,
+ dfas: Option<&RegexSearch>,
+ config: &'a Config<UIConfig>,
+ colors: &'a List,
+ show_cursor: bool,
+ ) -> Self {
+ let search = dfas.map(|dfas| RenderableSearch::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 !show_cursor {
+ terminal_cursor.shape = CursorShape::Hidden;
+ } else if !term.is_focused && config.cursor.unfocused_hollow {
+ terminal_cursor.shape = CursorShape::HollowBlock;
+ }
+
+ Self { cursor: None, terminal_content, terminal_cursor, search, config, colors }
+ }
+
+ /// 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 colors = &content.config.ui_config.colors;
+ if is_selected {
+ let config_bg = colors.selection.background;
+ let selected_fg = 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.color(NamedColor::Background as usize);
+ bg_rgb = content.color(NamedColor::Foreground as usize);
+ bg_alpha = 1.0;
+ } else if config_bg != CellRgb::CellBackground {
+ bg_alpha = 1.0;
+ }
+ } else if content.search.advance(cell.point) {
+ // Highlight the cell if it is part of a search match.
+ let config_bg = colors.search.matches.background;
+ let matched_fg = 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()),
+ 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()
+ }
+
+ /// 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 search highlight tracking.
+#[derive(Default)]
+pub struct RenderableSearch {
+ /// All visible search matches.
+ matches: Vec<RangeInclusive<Point>>,
+
+ /// Index of the last match checked.
+ index: usize,
+}
+
+impl RenderableSearch {
+ /// Create a new renderable search iterator.
+ 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 { matches: iter.collect(), index: 0 }
+ }
+
+ /// 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) -> 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
+ }
+}