Draw cursor with rect renderer
This commit makes cursors being drawn via rects, thus it's always above underlines/strikeouts. Also, since the cursor isn't a glyph anymore, it can't be obscured due to atlas switching while glyphs are rendered. Fixes #4404. Fixes #3471.
This commit is contained in:
parent
fdc10d270e
commit
12fbd0051c
|
@ -45,6 +45,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||||
- Crash due to clipboard not being properly released on Wayland
|
- Crash due to clipboard not being properly released on Wayland
|
||||||
- Shadow artifacts when resizing transparent windows on macOS
|
- Shadow artifacts when resizing transparent windows on macOS
|
||||||
- Missing glyph symbols not being rendered for missing glyphs on macOS and Windows
|
- Missing glyph symbols not being rendered for missing glyphs on macOS and Windows
|
||||||
|
- Underline cursor being obscured by underline
|
||||||
|
- Cursor not being rendered with a lot of unicode glyphs visible
|
||||||
|
|
||||||
### Removed
|
### Removed
|
||||||
|
|
||||||
|
|
|
@ -1,112 +1,92 @@
|
||||||
//! Helpers for creating different cursor glyphs from font metrics.
|
//! Convert a cursor into an iterator of rects.
|
||||||
|
|
||||||
use crossfont::{BitmapBuffer, Metrics, RasterizedGlyph};
|
|
||||||
|
|
||||||
use alacritty_terminal::ansi::CursorShape;
|
use alacritty_terminal::ansi::CursorShape;
|
||||||
|
use alacritty_terminal::term::color::Rgb;
|
||||||
|
use alacritty_terminal::term::render::RenderableCursor;
|
||||||
|
use alacritty_terminal::term::SizeInfo;
|
||||||
|
|
||||||
pub fn get_cursor_glyph(
|
use crate::renderer::rects::RenderRect;
|
||||||
cursor: CursorShape,
|
|
||||||
metrics: Metrics,
|
|
||||||
offset_x: i8,
|
|
||||||
offset_y: i8,
|
|
||||||
is_wide: bool,
|
|
||||||
cursor_thickness: f32,
|
|
||||||
) -> RasterizedGlyph {
|
|
||||||
// Calculate the cell metrics.
|
|
||||||
//
|
|
||||||
// NOTE: With Rust 1.47+ `f64 as usize` is defined to clamp automatically:
|
|
||||||
// https://github.com/rust-lang/rust/commit/14d608f1d8a0b84da5f3bccecb3efb3d35f980dc
|
|
||||||
let height = (metrics.line_height + f64::from(offset_y)).max(1.) as usize;
|
|
||||||
let mut width = (metrics.average_advance + f64::from(offset_x)).max(1.) as usize;
|
|
||||||
let line_width = (cursor_thickness * width as f32).round().max(1.) as usize;
|
|
||||||
|
|
||||||
// Double the cursor width if it's above a double-width glyph.
|
/// Trait for conversion into the iterator.
|
||||||
if is_wide {
|
pub trait IntoRects {
|
||||||
width *= 2;
|
/// Consume the cursor for an iterator of rects.
|
||||||
}
|
fn rects(self, size_info: &SizeInfo, thickness: f32) -> CursorRects;
|
||||||
|
|
||||||
match cursor {
|
|
||||||
CursorShape::HollowBlock => get_box_cursor_glyph(height, width, line_width),
|
|
||||||
CursorShape::Underline => get_underline_cursor_glyph(width, line_width),
|
|
||||||
CursorShape::Beam => get_beam_cursor_glyph(height, line_width),
|
|
||||||
CursorShape::Block => get_block_cursor_glyph(height, width),
|
|
||||||
CursorShape::Hidden => RasterizedGlyph::default(),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return a custom underline cursor character.
|
impl IntoRects for RenderableCursor {
|
||||||
pub fn get_underline_cursor_glyph(width: usize, line_width: usize) -> RasterizedGlyph {
|
fn rects(self, size_info: &SizeInfo, thickness: f32) -> CursorRects {
|
||||||
// Create a new rectangle, the height is relative to the font width.
|
let point = self.point();
|
||||||
let buffer = BitmapBuffer::RGB(vec![255u8; width * line_width * 3]);
|
let x = point.col.0 as f32 * size_info.cell_width() + size_info.padding_x();
|
||||||
|
let y = point.line.0 as f32 * size_info.cell_height() + size_info.padding_y();
|
||||||
|
|
||||||
// Create a custom glyph with the rectangle data attached to it.
|
let mut width = size_info.cell_width();
|
||||||
RasterizedGlyph {
|
let height = size_info.cell_height();
|
||||||
character: ' ',
|
|
||||||
top: line_width as i32,
|
|
||||||
left: 0,
|
|
||||||
height: line_width as i32,
|
|
||||||
width: width as i32,
|
|
||||||
buffer,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Return a custom beam cursor character.
|
if self.is_wide() {
|
||||||
pub fn get_beam_cursor_glyph(height: usize, line_width: usize) -> RasterizedGlyph {
|
width *= 2.;
|
||||||
// Create a new rectangle that is at least one pixel wide
|
}
|
||||||
let buffer = BitmapBuffer::RGB(vec![255u8; line_width * height * 3]);
|
|
||||||
|
|
||||||
// Create a custom glyph with the rectangle data attached to it
|
let thickness = (thickness * width as f32).round().max(1.);
|
||||||
RasterizedGlyph {
|
|
||||||
character: ' ',
|
|
||||||
top: height as i32,
|
|
||||||
left: 0,
|
|
||||||
height: height as i32,
|
|
||||||
width: line_width as i32,
|
|
||||||
buffer,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns a custom box cursor character.
|
match self.shape() {
|
||||||
pub fn get_box_cursor_glyph(height: usize, width: usize, line_width: usize) -> RasterizedGlyph {
|
CursorShape::Beam => beam(x, y, height, thickness, self.color()),
|
||||||
// Create a new box outline rectangle.
|
CursorShape::Underline => underline(x, y, width, height, thickness, self.color()),
|
||||||
let mut buffer = Vec::with_capacity(width * height * 3);
|
CursorShape::HollowBlock => hollow(x, y, width, height, thickness, self.color()),
|
||||||
for y in 0..height {
|
_ => CursorRects::default(),
|
||||||
for x in 0..width {
|
|
||||||
if y < line_width
|
|
||||||
|| y >= height - line_width
|
|
||||||
|| x < line_width
|
|
||||||
|| x >= width - line_width
|
|
||||||
{
|
|
||||||
buffer.append(&mut vec![255u8; 3]);
|
|
||||||
} else {
|
|
||||||
buffer.append(&mut vec![0u8; 3]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Create a custom glyph with the rectangle data attached to it.
|
/// Cursor rect iterator.
|
||||||
RasterizedGlyph {
|
#[derive(Default)]
|
||||||
character: ' ',
|
pub struct CursorRects {
|
||||||
top: height as i32,
|
rects: [Option<RenderRect>; 4],
|
||||||
left: 0,
|
index: usize,
|
||||||
height: height as i32,
|
}
|
||||||
width: width as i32,
|
|
||||||
buffer: BitmapBuffer::RGB(buffer),
|
impl From<RenderRect> for CursorRects {
|
||||||
|
fn from(rect: RenderRect) -> Self {
|
||||||
|
Self { rects: [Some(rect), None, None, None], index: 0 }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return a custom block cursor character.
|
impl Iterator for CursorRects {
|
||||||
pub fn get_block_cursor_glyph(height: usize, width: usize) -> RasterizedGlyph {
|
type Item = RenderRect;
|
||||||
// Create a completely filled glyph.
|
|
||||||
let buffer = BitmapBuffer::RGB(vec![255u8; width * height * 3]);
|
|
||||||
|
|
||||||
// Create a custom glyph with the rectangle data attached to it.
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
RasterizedGlyph {
|
let rect = self.rects.get_mut(self.index)?;
|
||||||
character: ' ',
|
self.index += 1;
|
||||||
top: height as i32,
|
rect.take()
|
||||||
left: 0,
|
}
|
||||||
height: height as i32,
|
}
|
||||||
width: width as i32,
|
|
||||||
buffer,
|
/// Create an iterator yielding a single beam rect.
|
||||||
|
fn beam(x: f32, y: f32, height: f32, thickness: f32, color: Rgb) -> CursorRects {
|
||||||
|
RenderRect::new(x, y, thickness, height, color, 1.).into()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create an iterator yielding a single underline rect.
|
||||||
|
fn underline(x: f32, y: f32, width: f32, height: f32, thickness: f32, color: Rgb) -> CursorRects {
|
||||||
|
let y = y + height - thickness;
|
||||||
|
RenderRect::new(x, y, width, thickness, color, 1.).into()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create an iterator yielding a rect for each side of the hollow block cursor.
|
||||||
|
fn hollow(x: f32, y: f32, width: f32, height: f32, thickness: f32, color: Rgb) -> CursorRects {
|
||||||
|
let top_line = RenderRect::new(x, y, width, thickness, color, 1.);
|
||||||
|
|
||||||
|
let vertical_y = y + thickness;
|
||||||
|
let vertical_height = height - 2. * thickness;
|
||||||
|
let left_line = RenderRect::new(x, vertical_y, thickness, vertical_height, color, 1.);
|
||||||
|
|
||||||
|
let bottom_y = y + height - thickness;
|
||||||
|
let bottom_line = RenderRect::new(x, bottom_y, width, thickness, color, 1.);
|
||||||
|
|
||||||
|
let right_x = x + width - thickness;
|
||||||
|
let right_line = RenderRect::new(right_x, vertical_y, thickness, vertical_height, color, 1.);
|
||||||
|
|
||||||
|
CursorRects {
|
||||||
|
rects: [Some(top_line), Some(bottom_line), Some(left_line), Some(right_line)],
|
||||||
|
index: 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,6 +33,7 @@ use crate::config::window::Dimensions;
|
||||||
#[cfg(not(windows))]
|
#[cfg(not(windows))]
|
||||||
use crate::config::window::StartupMode;
|
use crate::config::window::StartupMode;
|
||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
|
use crate::cursor::IntoRects;
|
||||||
use crate::event::{Mouse, SearchState};
|
use crate::event::{Mouse, SearchState};
|
||||||
use crate::message_bar::{MessageBuffer, MessageType};
|
use crate::message_bar::{MessageBuffer, MessageType};
|
||||||
use crate::meter::Meter;
|
use crate::meter::Meter;
|
||||||
|
@ -246,7 +247,7 @@ impl Display {
|
||||||
|
|
||||||
// Clear screen.
|
// Clear screen.
|
||||||
let background_color = config.colors.primary.background;
|
let background_color = config.colors.primary.background;
|
||||||
renderer.with_api(&config.ui_config, config.cursor, &size_info, |api| {
|
renderer.with_api(&config.ui_config, &size_info, |api| {
|
||||||
api.clear(background_color);
|
api.clear(background_color);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -268,7 +269,7 @@ impl Display {
|
||||||
#[cfg(not(any(target_os = "macos", windows)))]
|
#[cfg(not(any(target_os = "macos", windows)))]
|
||||||
if is_x11 {
|
if is_x11 {
|
||||||
window.swap_buffers();
|
window.swap_buffers();
|
||||||
renderer.with_api(&config.ui_config, config.cursor, &size_info, |api| {
|
renderer.with_api(&config.ui_config, &size_info, |api| {
|
||||||
api.finish();
|
api.finish();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -450,7 +451,14 @@ impl Display {
|
||||||
.and_then(|focused_match| terminal.grid().clamp_buffer_range_to_visible(focused_match));
|
.and_then(|focused_match| terminal.grid().clamp_buffer_range_to_visible(focused_match));
|
||||||
let cursor_hidden = self.cursor_hidden || search_state.regex().is_some();
|
let cursor_hidden = self.cursor_hidden || search_state.regex().is_some();
|
||||||
|
|
||||||
let grid_cells = terminal.renderable_cells(config, !cursor_hidden).collect::<Vec<_>>();
|
// Collect renderable content before the terminal is dropped.
|
||||||
|
let mut content = terminal.renderable_content(config, !cursor_hidden);
|
||||||
|
let mut grid_cells = Vec::new();
|
||||||
|
while let Some(cell) = content.next() {
|
||||||
|
grid_cells.push(cell);
|
||||||
|
}
|
||||||
|
let cursor = content.cursor();
|
||||||
|
|
||||||
let visual_bell_intensity = terminal.visual_bell.intensity();
|
let visual_bell_intensity = terminal.visual_bell.intensity();
|
||||||
let background_color = terminal.background_color();
|
let background_color = terminal.background_color();
|
||||||
let cursor_point = terminal.grid().cursor.point;
|
let cursor_point = terminal.grid().cursor.point;
|
||||||
|
@ -471,7 +479,7 @@ impl Display {
|
||||||
// Drop terminal as early as possible to free lock.
|
// Drop terminal as early as possible to free lock.
|
||||||
drop(terminal);
|
drop(terminal);
|
||||||
|
|
||||||
self.renderer.with_api(&config.ui_config, config.cursor, &size_info, |api| {
|
self.renderer.with_api(&config.ui_config, &size_info, |api| {
|
||||||
api.clear(background_color);
|
api.clear(background_color);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -482,7 +490,7 @@ impl Display {
|
||||||
{
|
{
|
||||||
let _sampler = self.meter.sampler();
|
let _sampler = self.meter.sampler();
|
||||||
|
|
||||||
self.renderer.with_api(&config.ui_config, config.cursor, &size_info, |mut api| {
|
self.renderer.with_api(&config.ui_config, &size_info, |mut api| {
|
||||||
// Iterate over all non-empty cells in the grid.
|
// Iterate over all non-empty cells in the grid.
|
||||||
for mut cell in grid_cells {
|
for mut cell in grid_cells {
|
||||||
// Invert the active match in vi-less search.
|
// Invert the active match in vi-less search.
|
||||||
|
@ -538,6 +546,13 @@ impl Display {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Push the cursor rects for rendering.
|
||||||
|
if let Some(cursor) = cursor {
|
||||||
|
for rect in cursor.rects(&size_info, config.cursor.thickness()) {
|
||||||
|
rects.push(rect);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Push visual bell after url/underline/strikeout rects.
|
// Push visual bell after url/underline/strikeout rects.
|
||||||
if visual_bell_intensity != 0. {
|
if visual_bell_intensity != 0. {
|
||||||
let visual_bell_rect = RenderRect::new(
|
let visual_bell_rect = RenderRect::new(
|
||||||
|
@ -576,7 +591,7 @@ impl Display {
|
||||||
// Relay messages to the user.
|
// Relay messages to the user.
|
||||||
let fg = config.colors.primary.background;
|
let fg = config.colors.primary.background;
|
||||||
for (i, message_text) in text.iter().enumerate() {
|
for (i, message_text) in text.iter().enumerate() {
|
||||||
self.renderer.with_api(&config.ui_config, config.cursor, &size_info, |mut api| {
|
self.renderer.with_api(&config.ui_config, &size_info, |mut api| {
|
||||||
api.render_string(glyph_cache, start_line + i, &message_text, fg, None);
|
api.render_string(glyph_cache, start_line + i, &message_text, fg, None);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -621,7 +636,7 @@ impl Display {
|
||||||
// On X11 `swap_buffers` does not block for vsync. However the next OpenGl command
|
// On X11 `swap_buffers` does not block for vsync. However the next OpenGl command
|
||||||
// will block to synchronize (this is `glClear` in Alacritty), which causes a
|
// will block to synchronize (this is `glClear` in Alacritty), which causes a
|
||||||
// permanent one frame delay.
|
// permanent one frame delay.
|
||||||
self.renderer.with_api(&config.ui_config, config.cursor, &size_info, |api| {
|
self.renderer.with_api(&config.ui_config, &size_info, |api| {
|
||||||
api.finish();
|
api.finish();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -668,7 +683,7 @@ impl Display {
|
||||||
|
|
||||||
let fg = config.colors.search_bar_foreground();
|
let fg = config.colors.search_bar_foreground();
|
||||||
let bg = config.colors.search_bar_background();
|
let bg = config.colors.search_bar_background();
|
||||||
self.renderer.with_api(&config.ui_config, config.cursor, &size_info, |mut api| {
|
self.renderer.with_api(&config.ui_config, &size_info, |mut api| {
|
||||||
api.render_string(glyph_cache, size_info.screen_lines(), &text, fg, Some(bg));
|
api.render_string(glyph_cache, size_info.screen_lines(), &text, fg, Some(bg));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -684,7 +699,7 @@ impl Display {
|
||||||
let fg = config.colors.primary.background;
|
let fg = config.colors.primary.background;
|
||||||
let bg = config.colors.normal.red;
|
let bg = config.colors.normal.red;
|
||||||
|
|
||||||
self.renderer.with_api(&config.ui_config, config.cursor, &size_info, |mut api| {
|
self.renderer.with_api(&config.ui_config, &size_info, |mut api| {
|
||||||
api.render_string(glyph_cache, size_info.screen_lines() - 2, &timing[..], fg, Some(bg));
|
api.render_string(glyph_cache, size_info.screen_lines() - 2, &timing[..], fg, Some(bg));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,8 +26,7 @@ use alacritty_terminal::event::EventListener;
|
||||||
use alacritty_terminal::grid::{Dimensions, Scroll};
|
use alacritty_terminal::grid::{Dimensions, Scroll};
|
||||||
use alacritty_terminal::index::{Column, Direction, Line, Point, Side};
|
use alacritty_terminal::index::{Column, Direction, Line, Point, Side};
|
||||||
use alacritty_terminal::selection::SelectionType;
|
use alacritty_terminal::selection::SelectionType;
|
||||||
use alacritty_terminal::term::mode::TermMode;
|
use alacritty_terminal::term::{ClipboardType, SizeInfo, Term, TermMode};
|
||||||
use alacritty_terminal::term::{ClipboardType, SizeInfo, Term};
|
|
||||||
use alacritty_terminal::vi_mode::ViMotion;
|
use alacritty_terminal::vi_mode::ViMotion;
|
||||||
|
|
||||||
use crate::clipboard::Clipboard;
|
use crate::clipboard::Clipboard;
|
||||||
|
|
|
@ -15,15 +15,14 @@ use fnv::FnvHasher;
|
||||||
use log::{debug, error, info};
|
use log::{debug, error, info};
|
||||||
use unicode_width::UnicodeWidthChar;
|
use unicode_width::UnicodeWidthChar;
|
||||||
|
|
||||||
use alacritty_terminal::config::Cursor;
|
|
||||||
use alacritty_terminal::index::{Column, Line};
|
use alacritty_terminal::index::{Column, Line};
|
||||||
use alacritty_terminal::term::cell::Flags;
|
use alacritty_terminal::term::cell::Flags;
|
||||||
use alacritty_terminal::term::color::Rgb;
|
use alacritty_terminal::term::color::Rgb;
|
||||||
use alacritty_terminal::term::{CursorKey, RenderableCell, RenderableCellContent, SizeInfo};
|
use alacritty_terminal::term::render::RenderableCell;
|
||||||
|
use alacritty_terminal::term::SizeInfo;
|
||||||
|
|
||||||
use crate::config::font::{Font, FontDescription};
|
use crate::config::font::{Font, FontDescription};
|
||||||
use crate::config::ui_config::{Delta, UIConfig};
|
use crate::config::ui_config::{Delta, UIConfig};
|
||||||
use crate::cursor;
|
|
||||||
use crate::gl;
|
use crate::gl;
|
||||||
use crate::gl::types::*;
|
use crate::gl::types::*;
|
||||||
use crate::renderer::rects::{RectRenderer, RenderRect};
|
use crate::renderer::rects::{RectRenderer, RenderRect};
|
||||||
|
@ -116,9 +115,6 @@ pub struct GlyphCache {
|
||||||
/// Cache of buffered glyphs.
|
/// Cache of buffered glyphs.
|
||||||
cache: HashMap<GlyphKey, Glyph, BuildHasherDefault<FnvHasher>>,
|
cache: HashMap<GlyphKey, Glyph, BuildHasherDefault<FnvHasher>>,
|
||||||
|
|
||||||
/// Cache of buffered cursor glyphs.
|
|
||||||
cursor_cache: HashMap<CursorKey, Glyph, BuildHasherDefault<FnvHasher>>,
|
|
||||||
|
|
||||||
/// Rasterizer for loading new glyphs.
|
/// Rasterizer for loading new glyphs.
|
||||||
rasterizer: Rasterizer,
|
rasterizer: Rasterizer,
|
||||||
|
|
||||||
|
@ -164,7 +160,6 @@ impl GlyphCache {
|
||||||
|
|
||||||
let mut cache = Self {
|
let mut cache = Self {
|
||||||
cache: HashMap::default(),
|
cache: HashMap::default(),
|
||||||
cursor_cache: HashMap::default(),
|
|
||||||
rasterizer,
|
rasterizer,
|
||||||
font_size: font.size(),
|
font_size: font.size(),
|
||||||
font_key: regular,
|
font_key: regular,
|
||||||
|
@ -328,7 +323,6 @@ impl GlyphCache {
|
||||||
pub fn clear_glyph_cache<L: LoadGlyph>(&mut self, loader: &mut L) {
|
pub fn clear_glyph_cache<L: LoadGlyph>(&mut self, loader: &mut L) {
|
||||||
loader.clear();
|
loader.clear();
|
||||||
self.cache = HashMap::default();
|
self.cache = HashMap::default();
|
||||||
self.cursor_cache = HashMap::default();
|
|
||||||
|
|
||||||
self.load_common_glyphs(loader);
|
self.load_common_glyphs(loader);
|
||||||
}
|
}
|
||||||
|
@ -459,7 +453,6 @@ pub struct RenderApi<'a> {
|
||||||
current_atlas: &'a mut usize,
|
current_atlas: &'a mut usize,
|
||||||
program: &'a mut TextShaderProgram,
|
program: &'a mut TextShaderProgram,
|
||||||
config: &'a UIConfig,
|
config: &'a UIConfig,
|
||||||
cursor_config: Cursor,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
@ -693,13 +686,7 @@ impl QuadRenderer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn with_api<F, T>(
|
pub fn with_api<F, T>(&mut self, config: &UIConfig, props: &SizeInfo, func: F) -> T
|
||||||
&mut self,
|
|
||||||
config: &UIConfig,
|
|
||||||
cursor_config: Cursor,
|
|
||||||
props: &SizeInfo,
|
|
||||||
func: F,
|
|
||||||
) -> T
|
|
||||||
where
|
where
|
||||||
F: FnOnce(RenderApi<'_>) -> T,
|
F: FnOnce(RenderApi<'_>) -> T,
|
||||||
{
|
{
|
||||||
|
@ -720,7 +707,6 @@ impl QuadRenderer {
|
||||||
current_atlas: &mut self.current_atlas,
|
current_atlas: &mut self.current_atlas,
|
||||||
program: &mut self.program,
|
program: &mut self.program,
|
||||||
config,
|
config,
|
||||||
cursor_config,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
unsafe {
|
unsafe {
|
||||||
|
@ -848,10 +834,11 @@ impl<'a> RenderApi<'a> {
|
||||||
let cells = string
|
let cells = string
|
||||||
.chars()
|
.chars()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.map(|(i, c)| RenderableCell {
|
.map(|(i, character)| RenderableCell {
|
||||||
line,
|
line,
|
||||||
column: Column(i),
|
column: Column(i),
|
||||||
inner: RenderableCellContent::Chars((c, None)),
|
character,
|
||||||
|
zerowidth: None,
|
||||||
flags: Flags::empty(),
|
flags: Flags::empty(),
|
||||||
bg_alpha,
|
bg_alpha,
|
||||||
fg,
|
fg,
|
||||||
|
@ -881,26 +868,6 @@ impl<'a> RenderApi<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn render_cell(&mut self, mut cell: RenderableCell, glyph_cache: &mut GlyphCache) {
|
pub fn render_cell(&mut self, mut cell: RenderableCell, glyph_cache: &mut GlyphCache) {
|
||||||
let (mut character, zerowidth) = match cell.inner {
|
|
||||||
RenderableCellContent::Cursor(cursor_key) => {
|
|
||||||
// Raw cell pixel buffers like cursors don't need to go through font lookup.
|
|
||||||
let metrics = glyph_cache.metrics;
|
|
||||||
let glyph = glyph_cache.cursor_cache.entry(cursor_key).or_insert_with(|| {
|
|
||||||
self.load_glyph(&cursor::get_cursor_glyph(
|
|
||||||
cursor_key.shape,
|
|
||||||
metrics,
|
|
||||||
self.config.font.offset.x,
|
|
||||||
self.config.font.offset.y,
|
|
||||||
cursor_key.is_wide,
|
|
||||||
self.cursor_config.thickness(),
|
|
||||||
))
|
|
||||||
});
|
|
||||||
self.add_render_item(&cell, glyph);
|
|
||||||
return;
|
|
||||||
},
|
|
||||||
RenderableCellContent::Chars((c, ref mut zerowidth)) => (c, zerowidth.take()),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Get font key for cell.
|
// Get font key for cell.
|
||||||
let font_key = match cell.flags & Flags::BOLD_ITALIC {
|
let font_key = match cell.flags & Flags::BOLD_ITALIC {
|
||||||
Flags::BOLD_ITALIC => glyph_cache.bold_italic_key,
|
Flags::BOLD_ITALIC => glyph_cache.bold_italic_key,
|
||||||
|
@ -911,11 +878,12 @@ impl<'a> RenderApi<'a> {
|
||||||
|
|
||||||
// Ignore hidden cells and render tabs as spaces to prevent font issues.
|
// Ignore hidden cells and render tabs as spaces to prevent font issues.
|
||||||
let hidden = cell.flags.contains(Flags::HIDDEN);
|
let hidden = cell.flags.contains(Flags::HIDDEN);
|
||||||
if character == '\t' || hidden {
|
if cell.character == '\t' || hidden {
|
||||||
character = ' ';
|
cell.character = ' ';
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut glyph_key = GlyphKey { font_key, size: glyph_cache.font_size, character };
|
let mut glyph_key =
|
||||||
|
GlyphKey { font_key, size: glyph_cache.font_size, character: cell.character };
|
||||||
|
|
||||||
// Add cell to batch.
|
// Add cell to batch.
|
||||||
match glyph_cache.get(glyph_key, self) {
|
match glyph_cache.get(glyph_key, self) {
|
||||||
|
@ -930,9 +898,9 @@ impl<'a> RenderApi<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Render visible zero-width characters.
|
// Render visible zero-width characters.
|
||||||
if let Some(zerowidth) = zerowidth.filter(|_| !hidden) {
|
if let Some(zerowidth) = cell.zerowidth.take().filter(|_| !hidden) {
|
||||||
for character in zerowidth.iter() {
|
for character in zerowidth {
|
||||||
glyph_key.character = *character;
|
glyph_key.character = character;
|
||||||
if let Ok(glyph) = glyph_cache.get(glyph_key, self) {
|
if let Ok(glyph) = glyph_cache.get(glyph_key, self) {
|
||||||
self.add_render_item(&cell, &glyph);
|
self.add_render_item(&cell, &glyph);
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,8 @@ use crossfont::Metrics;
|
||||||
use alacritty_terminal::index::{Column, Point};
|
use alacritty_terminal::index::{Column, Point};
|
||||||
use alacritty_terminal::term::cell::Flags;
|
use alacritty_terminal::term::cell::Flags;
|
||||||
use alacritty_terminal::term::color::Rgb;
|
use alacritty_terminal::term::color::Rgb;
|
||||||
use alacritty_terminal::term::{RenderableCell, SizeInfo};
|
use alacritty_terminal::term::render::RenderableCell;
|
||||||
|
use alacritty_terminal::term::SizeInfo;
|
||||||
|
|
||||||
use crate::gl;
|
use crate::gl;
|
||||||
use crate::gl::types::*;
|
use crate::gl::types::*;
|
||||||
|
|
|
@ -8,7 +8,8 @@ use urlocator::{UrlLocation, UrlLocator};
|
||||||
use alacritty_terminal::index::{Column, Point};
|
use alacritty_terminal::index::{Column, Point};
|
||||||
use alacritty_terminal::term::cell::Flags;
|
use alacritty_terminal::term::cell::Flags;
|
||||||
use alacritty_terminal::term::color::Rgb;
|
use alacritty_terminal::term::color::Rgb;
|
||||||
use alacritty_terminal::term::{RenderableCell, RenderableCellContent, SizeInfo};
|
use alacritty_terminal::term::render::RenderableCell;
|
||||||
|
use alacritty_terminal::term::SizeInfo;
|
||||||
|
|
||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
use crate::event::Mouse;
|
use crate::event::Mouse;
|
||||||
|
@ -72,12 +73,6 @@ impl Urls {
|
||||||
|
|
||||||
// Update tracked URLs.
|
// Update tracked URLs.
|
||||||
pub fn update(&mut self, num_cols: Column, cell: &RenderableCell) {
|
pub fn update(&mut self, num_cols: Column, cell: &RenderableCell) {
|
||||||
// Convert cell to character.
|
|
||||||
let c = match &cell.inner {
|
|
||||||
RenderableCellContent::Chars((c, _zerowidth)) => *c,
|
|
||||||
RenderableCellContent::Cursor(_) => return,
|
|
||||||
};
|
|
||||||
|
|
||||||
let point: Point = cell.into();
|
let point: Point = cell.into();
|
||||||
let mut end = point;
|
let mut end = point;
|
||||||
|
|
||||||
|
@ -107,7 +102,7 @@ impl Urls {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Advance parser.
|
// Advance parser.
|
||||||
let last_state = mem::replace(&mut self.state, self.locator.advance(c));
|
let last_state = mem::replace(&mut self.state, self.locator.advance(cell.character));
|
||||||
match (self.state, last_state) {
|
match (self.state, last_state) {
|
||||||
(UrlLocation::Url(_length, end_offset), UrlLocation::Scheme) => {
|
(UrlLocation::Url(_length, end_offset), UrlLocation::Scheme) => {
|
||||||
// Create empty URL.
|
// Create empty URL.
|
||||||
|
@ -204,8 +199,9 @@ mod tests {
|
||||||
fn text_to_cells(text: &str) -> Vec<RenderableCell> {
|
fn text_to_cells(text: &str) -> Vec<RenderableCell> {
|
||||||
text.chars()
|
text.chars()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.map(|(i, c)| RenderableCell {
|
.map(|(i, character)| RenderableCell {
|
||||||
inner: RenderableCellContent::Chars((c, None)),
|
character,
|
||||||
|
zerowidth: None,
|
||||||
line: Line(0),
|
line: Line(0),
|
||||||
column: Column(i),
|
column: Column(i),
|
||||||
fg: Default::default(),
|
fg: Default::default(),
|
||||||
|
|
|
@ -8,7 +8,7 @@ use std::ops::{self, Add, AddAssign, Deref, Range, Sub, SubAssign};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::grid::Dimensions;
|
use crate::grid::Dimensions;
|
||||||
use crate::term::RenderableCell;
|
use crate::term::render::RenderableCell;
|
||||||
|
|
||||||
/// The side of a cell.
|
/// The side of a cell.
|
||||||
pub type Side = Direction;
|
pub type Side = Direction;
|
||||||
|
|
|
@ -9,8 +9,10 @@ use std::convert::TryFrom;
|
||||||
use std::mem;
|
use std::mem;
|
||||||
use std::ops::{Bound, Range, RangeBounds};
|
use std::ops::{Bound, Range, RangeBounds};
|
||||||
|
|
||||||
use crate::grid::Dimensions;
|
use crate::ansi::CursorShape;
|
||||||
|
use crate::grid::{Dimensions, Grid, GridCell};
|
||||||
use crate::index::{Column, Line, Point, Side};
|
use crate::index::{Column, Line, Point, Side};
|
||||||
|
use crate::term::cell::Flags;
|
||||||
use crate::term::Term;
|
use crate::term::Term;
|
||||||
|
|
||||||
/// A Point and side within that point.
|
/// A Point and side within that point.
|
||||||
|
@ -42,14 +44,67 @@ impl<L> SelectionRange<L> {
|
||||||
Self { start, end, is_block }
|
Self { start, end, is_block }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn contains(&self, col: Column, line: L) -> bool
|
/// Check if a point lies within the selection.
|
||||||
|
pub fn contains(&self, point: Point<L>) -> bool
|
||||||
where
|
where
|
||||||
L: PartialEq + PartialOrd,
|
L: PartialEq + PartialOrd,
|
||||||
{
|
{
|
||||||
self.start.line <= line
|
self.start.line <= point.line
|
||||||
&& self.end.line >= line
|
&& self.end.line >= point.line
|
||||||
&& (self.start.col <= col || (self.start.line != line && !self.is_block))
|
&& (self.start.col <= point.col || (self.start.line != point.line && !self.is_block))
|
||||||
&& (self.end.col >= col || (self.end.line != line && !self.is_block))
|
&& (self.end.col >= point.col || (self.end.line != point.line && !self.is_block))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SelectionRange<Line> {
|
||||||
|
/// Check if the cell at a point is part of the selection.
|
||||||
|
pub fn contains_cell<T>(
|
||||||
|
&self,
|
||||||
|
grid: &Grid<T>,
|
||||||
|
point: Point,
|
||||||
|
cursor_point: Point,
|
||||||
|
cursor_shape: CursorShape,
|
||||||
|
) -> bool
|
||||||
|
where
|
||||||
|
T: GridCell,
|
||||||
|
{
|
||||||
|
// Do not invert block cursor at selection boundaries.
|
||||||
|
if cursor_shape == CursorShape::Block
|
||||||
|
&& cursor_point == point
|
||||||
|
&& (self.start == point
|
||||||
|
|| self.end == point
|
||||||
|
|| (self.is_block
|
||||||
|
&& ((self.start.line == point.line && self.end.col == point.col)
|
||||||
|
|| (self.end.line == point.line && self.start.col == point.col))))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Point itself is selected.
|
||||||
|
if self.contains(point) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
let num_cols = grid.cols();
|
||||||
|
|
||||||
|
// Convert to absolute coordinates to adjust for the display offset.
|
||||||
|
let buffer_point = grid.visible_to_buffer(point);
|
||||||
|
let cell = &grid[buffer_point];
|
||||||
|
|
||||||
|
// Check if wide char's spacers are selected.
|
||||||
|
if cell.flags().contains(Flags::WIDE_CHAR) {
|
||||||
|
let prev = point.sub(num_cols, 1);
|
||||||
|
let buffer_prev = grid.visible_to_buffer(prev);
|
||||||
|
let next = point.add(num_cols, 1);
|
||||||
|
|
||||||
|
// Check trailing spacer.
|
||||||
|
self.contains(next)
|
||||||
|
// Check line-wrapping, leading spacer.
|
||||||
|
|| (grid[buffer_prev].flags().contains(Flags::LEADING_WIDE_CHAR_SPACER)
|
||||||
|
&& self.contains(prev))
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
//! Exports the `Term` type which is a high-level API for the Grid.
|
//! Exports the `Term` type which is a high-level API for the Grid.
|
||||||
|
|
||||||
use std::cmp::{max, min};
|
use std::cmp::{max, min};
|
||||||
use std::iter::Peekable;
|
use std::ops::{Index, IndexMut, Range};
|
||||||
use std::ops::{Index, IndexMut, Range, RangeInclusive};
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
use std::{io, iter, mem, ptr, str};
|
use std::{io, mem, ptr, str};
|
||||||
|
|
||||||
|
use bitflags::bitflags;
|
||||||
use log::{debug, trace};
|
use log::{debug, trace};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use unicode_width::UnicodeWidthChar;
|
use unicode_width::UnicodeWidthChar;
|
||||||
|
@ -16,27 +16,23 @@ use crate::ansi::{
|
||||||
};
|
};
|
||||||
use crate::config::{BellAnimation, BellConfig, Config};
|
use crate::config::{BellAnimation, BellConfig, Config};
|
||||||
use crate::event::{Event, EventListener};
|
use crate::event::{Event, EventListener};
|
||||||
use crate::grid::{Dimensions, DisplayIter, Grid, IndexRegion, Indexed, Scroll};
|
use crate::grid::{Dimensions, Grid, IndexRegion, Scroll};
|
||||||
use crate::index::{self, Boundary, Column, Direction, IndexRange, Line, Point, Side};
|
use crate::index::{self, Boundary, Column, Direction, IndexRange, Line, Point, Side};
|
||||||
use crate::selection::{Selection, SelectionRange};
|
use crate::selection::{Selection, SelectionRange};
|
||||||
use crate::term::cell::{Cell, Flags, LineLength};
|
use crate::term::cell::{Cell, Flags, LineLength};
|
||||||
use crate::term::color::{CellRgb, Rgb, DIM_FACTOR};
|
use crate::term::color::Rgb;
|
||||||
use crate::term::search::{RegexIter, RegexSearch};
|
use crate::term::render::RenderableContent;
|
||||||
|
use crate::term::search::RegexSearch;
|
||||||
use crate::vi_mode::{ViModeCursor, ViMotion};
|
use crate::vi_mode::{ViModeCursor, ViMotion};
|
||||||
|
|
||||||
pub mod cell;
|
pub mod cell;
|
||||||
pub mod color;
|
pub mod color;
|
||||||
|
pub mod render;
|
||||||
mod search;
|
mod search;
|
||||||
|
|
||||||
/// Max size of the window title stack.
|
/// Max size of the window title stack.
|
||||||
const TITLE_STACK_MAX_DEPTH: usize = 4096;
|
const TITLE_STACK_MAX_DEPTH: usize = 4096;
|
||||||
|
|
||||||
/// Minimum contrast between a fixed cursor color and the cell's background.
|
|
||||||
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;
|
|
||||||
|
|
||||||
/// Default tab interval, corresponding to terminfo `it` value.
|
/// Default tab interval, corresponding to terminfo `it` value.
|
||||||
const INITIAL_TABSTOPS: usize = 8;
|
const INITIAL_TABSTOPS: usize = 8;
|
||||||
|
|
||||||
|
@ -48,433 +44,41 @@ pub const MIN_COLS: usize = 2;
|
||||||
/// Minimum number of visible lines.
|
/// Minimum number of visible lines.
|
||||||
pub const MIN_SCREEN_LINES: usize = 1;
|
pub const MIN_SCREEN_LINES: usize = 1;
|
||||||
|
|
||||||
/// Cursor storing all information relevant for rendering.
|
bitflags! {
|
||||||
#[derive(Debug, Eq, PartialEq, Copy, Clone, Deserialize)]
|
pub struct TermMode: u32 {
|
||||||
struct RenderableCursor {
|
const NONE = 0;
|
||||||
text_color: CellRgb,
|
const SHOW_CURSOR = 0b0000_0000_0000_0000_0001;
|
||||||
cursor_color: CellRgb,
|
const APP_CURSOR = 0b0000_0000_0000_0000_0010;
|
||||||
key: CursorKey,
|
const APP_KEYPAD = 0b0000_0000_0000_0000_0100;
|
||||||
point: Point,
|
const MOUSE_REPORT_CLICK = 0b0000_0000_0000_0000_1000;
|
||||||
rendered: bool,
|
const BRACKETED_PASTE = 0b0000_0000_0000_0001_0000;
|
||||||
}
|
const SGR_MOUSE = 0b0000_0000_0000_0010_0000;
|
||||||
|
const MOUSE_MOTION = 0b0000_0000_0000_0100_0000;
|
||||||
/// A key for caching cursor glyphs.
|
const LINE_WRAP = 0b0000_0000_0000_1000_0000;
|
||||||
#[derive(Debug, Eq, PartialEq, Copy, Clone, Hash, Deserialize)]
|
const LINE_FEED_NEW_LINE = 0b0000_0000_0001_0000_0000;
|
||||||
pub struct CursorKey {
|
const ORIGIN = 0b0000_0000_0010_0000_0000;
|
||||||
pub shape: CursorShape,
|
const INSERT = 0b0000_0000_0100_0000_0000;
|
||||||
pub is_wide: bool,
|
const FOCUS_IN_OUT = 0b0000_0000_1000_0000_0000;
|
||||||
}
|
const ALT_SCREEN = 0b0000_0001_0000_0000_0000;
|
||||||
|
const MOUSE_DRAG = 0b0000_0010_0000_0000_0000;
|
||||||
type MatchIter<'a> = Box<dyn Iterator<Item = RangeInclusive<Point<usize>>> + 'a>;
|
const MOUSE_MODE = 0b0000_0010_0000_0100_1000;
|
||||||
|
const UTF8_MOUSE = 0b0000_0100_0000_0000_0000;
|
||||||
/// Regex search highlight tracking.
|
const ALTERNATE_SCROLL = 0b0000_1000_0000_0000_0000;
|
||||||
pub struct RenderableSearch<'a> {
|
const VI = 0b0001_0000_0000_0000_0000;
|
||||||
iter: Peekable<MatchIter<'a>>,
|
const URGENCY_HINTS = 0b0010_0000_0000_0000_0000;
|
||||||
}
|
const ANY = std::u32::MAX;
|
||||||
|
|
||||||
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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Iterator that yields cells needing render.
|
impl Default for TermMode {
|
||||||
///
|
fn default() -> TermMode {
|
||||||
/// Yields cells that require work to be displayed (that is, not a an empty
|
TermMode::SHOW_CURSOR
|
||||||
/// background cell). Additionally, this manages some state of the grid only
|
| TermMode::LINE_WRAP
|
||||||
/// relevant for rendering like temporarily changing the cell with the cursor.
|
| TermMode::ALTERNATE_SCROLL
|
||||||
///
|
| TermMode::URGENCY_HINTS
|
||||||
/// This manages the cursor during a render. The cursor location is inverted to
|
|
||||||
/// draw it, and reverted after drawing to maintain state.
|
|
||||||
pub struct RenderableCellsIter<'a, C> {
|
|
||||||
inner: DisplayIter<'a, Cell>,
|
|
||||||
grid: &'a Grid<Cell>,
|
|
||||||
cursor: RenderableCursor,
|
|
||||||
config: &'a Config<C>,
|
|
||||||
colors: &'a color::List,
|
|
||||||
selection: Option<SelectionRange<Line>>,
|
|
||||||
search: RenderableSearch<'a>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a, C> Iterator for RenderableCellsIter<'a, 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.inner.point() {
|
|
||||||
// Handle cursor rendering.
|
|
||||||
if self.cursor.rendered {
|
|
||||||
return self.next_cursor_cell();
|
|
||||||
} else {
|
|
||||||
return Some(self.next_cursor());
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Handle non-cursor cells.
|
|
||||||
let cell = self.inner.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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, C> RenderableCellsIter<'a, C> {
|
|
||||||
/// Create the renderable cells iterator.
|
|
||||||
///
|
|
||||||
/// The cursor and terminal mode are required for properly displaying the
|
|
||||||
/// cursor.
|
|
||||||
fn new<T>(
|
|
||||||
term: &'a Term<T>,
|
|
||||||
config: &'a Config<C>,
|
|
||||||
show_cursor: bool,
|
|
||||||
) -> RenderableCellsIter<'a, C> {
|
|
||||||
RenderableCellsIter {
|
|
||||||
cursor: term.renderable_cursor(config, show_cursor),
|
|
||||||
grid: &term.grid,
|
|
||||||
inner: term.grid.display_iter(),
|
|
||||||
selection: term.visible_selection(),
|
|
||||||
config,
|
|
||||||
colors: &term.colors,
|
|
||||||
search: RenderableSearch::new(term),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the next renderable cell as the cell below the cursor.
|
|
||||||
fn next_cursor_cell(&mut self) -> Option<RenderableCell> {
|
|
||||||
// Handle cell below cursor.
|
|
||||||
let cell = self.inner.next()?;
|
|
||||||
let mut cell = RenderableCell::new(self, cell);
|
|
||||||
|
|
||||||
if self.cursor.key.shape == CursorShape::Block {
|
|
||||||
cell.fg = match self.cursor.cursor_color {
|
|
||||||
// Apply cursor color, or invert the cursor if it has a fixed background
|
|
||||||
// close to the cell's background.
|
|
||||||
CellRgb::Rgb(col) if col.contrast(cell.bg) < MIN_CURSOR_CONTRAST => cell.bg,
|
|
||||||
_ => self.cursor.text_color.color(cell.fg, cell.bg),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
Some(cell)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the next renderable cell as the cursor.
|
|
||||||
fn next_cursor(&mut self) -> RenderableCell {
|
|
||||||
// Handle cursor.
|
|
||||||
self.cursor.rendered = true;
|
|
||||||
|
|
||||||
let buffer_point = self.grid.visible_to_buffer(self.cursor.point);
|
|
||||||
let cell = Indexed {
|
|
||||||
inner: &self.grid[buffer_point.line][buffer_point.col],
|
|
||||||
column: self.cursor.point.col,
|
|
||||||
line: self.cursor.point.line,
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut cell = RenderableCell::new(self, cell);
|
|
||||||
cell.inner = RenderableCellContent::Cursor(self.cursor.key);
|
|
||||||
|
|
||||||
// Apply cursor color, or invert the cursor if it has a fixed background close
|
|
||||||
// to the cell's background.
|
|
||||||
if !matches!(
|
|
||||||
self.cursor.cursor_color,
|
|
||||||
CellRgb::Rgb(color) if color.contrast(cell.bg) < MIN_CURSOR_CONTRAST
|
|
||||||
) {
|
|
||||||
cell.fg = self.cursor.cursor_color.color(cell.fg, cell.bg);
|
|
||||||
}
|
|
||||||
|
|
||||||
cell
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Check selection state of a cell.
|
|
||||||
fn is_selected(&self, point: Point) -> bool {
|
|
||||||
let selection = match self.selection {
|
|
||||||
Some(selection) => selection,
|
|
||||||
None => return false,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Do not invert block cursor at selection boundaries.
|
|
||||||
if self.cursor.key.shape == CursorShape::Block
|
|
||||||
&& self.cursor.point == point
|
|
||||||
&& (selection.start == point
|
|
||||||
|| selection.end == point
|
|
||||||
|| (selection.is_block
|
|
||||||
&& ((selection.start.line == point.line && selection.end.col == point.col)
|
|
||||||
|| (selection.end.line == point.line && selection.start.col == point.col))))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Point itself is selected.
|
|
||||||
if selection.contains(point.col, point.line) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
let num_cols = self.grid.cols();
|
|
||||||
|
|
||||||
// Convert to absolute coordinates to adjust for the display offset.
|
|
||||||
let buffer_point = self.grid.visible_to_buffer(point);
|
|
||||||
let cell = &self.grid[buffer_point];
|
|
||||||
|
|
||||||
// Check if wide char's spacers are selected.
|
|
||||||
if cell.flags.contains(Flags::WIDE_CHAR) {
|
|
||||||
let prev = point.sub(num_cols, 1);
|
|
||||||
let buffer_prev = self.grid.visible_to_buffer(prev);
|
|
||||||
let next = point.add(num_cols, 1);
|
|
||||||
|
|
||||||
// Check trailing spacer.
|
|
||||||
selection.contains(next.col, next.line)
|
|
||||||
// Check line-wrapping, leading spacer.
|
|
||||||
|| (self.grid[buffer_prev].flags.contains(Flags::LEADING_WIDE_CHAR_SPACER)
|
|
||||||
&& selection.contains(prev.col, prev.line))
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
||||||
pub enum RenderableCellContent {
|
|
||||||
Chars((char, Option<Vec<char>>)),
|
|
||||||
Cursor(CursorKey),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub struct RenderableCell {
|
|
||||||
/// A _Display_ line (not necessarily an _Active_ line).
|
|
||||||
pub line: Line,
|
|
||||||
pub column: Column,
|
|
||||||
pub inner: RenderableCellContent,
|
|
||||||
pub fg: Rgb,
|
|
||||||
pub bg: Rgb,
|
|
||||||
pub bg_alpha: f32,
|
|
||||||
pub flags: Flags,
|
|
||||||
pub is_match: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RenderableCell {
|
|
||||||
fn new<'a, C>(iter: &mut RenderableCellsIter<'a, C>, cell: Indexed<&Cell>) -> Self {
|
|
||||||
let point = Point::new(cell.line, cell.column);
|
|
||||||
|
|
||||||
// Lookup RGB values.
|
|
||||||
let mut fg_rgb = Self::compute_fg_rgb(iter.config, iter.colors, cell.fg, cell.flags);
|
|
||||||
let mut bg_rgb = Self::compute_bg_rgb(iter.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 mut is_match = false;
|
|
||||||
|
|
||||||
if iter.is_selected(point) {
|
|
||||||
let config_bg = iter.config.colors.selection.background;
|
|
||||||
let selected_fg = iter.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 = iter.colors[NamedColor::Background];
|
|
||||||
bg_rgb = iter.colors[NamedColor::Foreground];
|
|
||||||
bg_alpha = 1.0;
|
|
||||||
} else if config_bg != CellRgb::CellBackground {
|
|
||||||
bg_alpha = 1.0;
|
|
||||||
}
|
|
||||||
} else if iter.search.advance(iter.grid.visible_to_buffer(point)) {
|
|
||||||
// Highlight the cell if it is part of a search match.
|
|
||||||
let config_bg = iter.config.colors.search.matches.background;
|
|
||||||
let matched_fg = iter.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;
|
|
||||||
}
|
|
||||||
|
|
||||||
let zerowidth = cell.zerowidth().map(|zerowidth| zerowidth.to_vec());
|
|
||||||
|
|
||||||
RenderableCell {
|
|
||||||
line: cell.line,
|
|
||||||
column: cell.column,
|
|
||||||
inner: RenderableCellContent::Chars((cell.c, zerowidth)),
|
|
||||||
fg: fg_rgb,
|
|
||||||
bg: bg_rgb,
|
|
||||||
bg_alpha,
|
|
||||||
flags: cell.flags,
|
|
||||||
is_match,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_empty(&self) -> bool {
|
|
||||||
self.bg_alpha == 0.
|
|
||||||
&& !self.flags.intersects(Flags::UNDERLINE | Flags::STRIKEOUT | Flags::DOUBLE_UNDERLINE)
|
|
||||||
&& self.inner == RenderableCellContent::Chars((' ', None))
|
|
||||||
}
|
|
||||||
|
|
||||||
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]
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[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],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub mod mode {
|
|
||||||
use bitflags::bitflags;
|
|
||||||
|
|
||||||
bitflags! {
|
|
||||||
pub struct TermMode: u32 {
|
|
||||||
const NONE = 0;
|
|
||||||
const SHOW_CURSOR = 0b0000_0000_0000_0000_0001;
|
|
||||||
const APP_CURSOR = 0b0000_0000_0000_0000_0010;
|
|
||||||
const APP_KEYPAD = 0b0000_0000_0000_0000_0100;
|
|
||||||
const MOUSE_REPORT_CLICK = 0b0000_0000_0000_0000_1000;
|
|
||||||
const BRACKETED_PASTE = 0b0000_0000_0000_0001_0000;
|
|
||||||
const SGR_MOUSE = 0b0000_0000_0000_0010_0000;
|
|
||||||
const MOUSE_MOTION = 0b0000_0000_0000_0100_0000;
|
|
||||||
const LINE_WRAP = 0b0000_0000_0000_1000_0000;
|
|
||||||
const LINE_FEED_NEW_LINE = 0b0000_0000_0001_0000_0000;
|
|
||||||
const ORIGIN = 0b0000_0000_0010_0000_0000;
|
|
||||||
const INSERT = 0b0000_0000_0100_0000_0000;
|
|
||||||
const FOCUS_IN_OUT = 0b0000_0000_1000_0000_0000;
|
|
||||||
const ALT_SCREEN = 0b0000_0001_0000_0000_0000;
|
|
||||||
const MOUSE_DRAG = 0b0000_0010_0000_0000_0000;
|
|
||||||
const MOUSE_MODE = 0b0000_0010_0000_0100_1000;
|
|
||||||
const UTF8_MOUSE = 0b0000_0100_0000_0000_0000;
|
|
||||||
const ALTERNATE_SCROLL = 0b0000_1000_0000_0000_0000;
|
|
||||||
const VI = 0b0001_0000_0000_0000_0000;
|
|
||||||
const URGENCY_HINTS = 0b0010_0000_0000_0000_0000;
|
|
||||||
const ANY = std::u32::MAX;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for TermMode {
|
|
||||||
fn default() -> TermMode {
|
|
||||||
TermMode::SHOW_CURSOR
|
|
||||||
| TermMode::LINE_WRAP
|
|
||||||
| TermMode::ALTERNATE_SCROLL
|
|
||||||
| TermMode::URGENCY_HINTS
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub use crate::term::mode::TermMode;
|
|
||||||
|
|
||||||
pub struct VisualBell {
|
pub struct VisualBell {
|
||||||
/// Visual bell animation.
|
/// Visual bell animation.
|
||||||
animation: BellAnimation,
|
animation: BellAnimation,
|
||||||
|
@ -1017,17 +621,19 @@ impl<T> Term<T> {
|
||||||
&mut self.grid
|
&mut self.grid
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Iterate over the *renderable* cells in the terminal.
|
/// Terminal content required for rendering.
|
||||||
///
|
///
|
||||||
/// A renderable cell is any cell which has content other than the default
|
/// A renderable cell is any cell which has content other than the default background color.
|
||||||
/// background color. Cells with an alternate background color are
|
/// Cells with an alternate background color are considered renderable, as are cells with any
|
||||||
/// considered renderable as are cells with any text content.
|
/// text content.
|
||||||
pub fn renderable_cells<'b, C>(
|
///
|
||||||
|
/// The cursor itself is always considered renderable and provided separately.
|
||||||
|
pub fn renderable_content<'b, C>(
|
||||||
&'b self,
|
&'b self,
|
||||||
config: &'b Config<C>,
|
config: &'b Config<C>,
|
||||||
show_cursor: bool,
|
show_cursor: bool,
|
||||||
) -> RenderableCellsIter<'_, C> {
|
) -> RenderableContent<'_, T, C> {
|
||||||
RenderableCellsIter::new(&self, config, show_cursor)
|
RenderableContent::new(&self, config, show_cursor)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the selection within the viewport.
|
/// Get the selection within the viewport.
|
||||||
|
@ -1393,67 +999,6 @@ impl<T> Term<T> {
|
||||||
|
|
||||||
cursor_cell
|
cursor_cell
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get rendering information about the active cursor.
|
|
||||||
fn renderable_cursor<C>(&self, config: &Config<C>, show_cursor: bool) -> RenderableCursor {
|
|
||||||
let vi_mode = self.mode.contains(TermMode::VI);
|
|
||||||
|
|
||||||
// Cursor position.
|
|
||||||
let mut point = if vi_mode {
|
|
||||||
self.vi_mode_cursor.point
|
|
||||||
} else {
|
|
||||||
let mut point = self.grid.cursor.point;
|
|
||||||
point.line += self.grid.display_offset();
|
|
||||||
point
|
|
||||||
};
|
|
||||||
|
|
||||||
// Cursor shape.
|
|
||||||
let hidden = !show_cursor
|
|
||||||
|| (!self.mode.contains(TermMode::SHOW_CURSOR) && !vi_mode)
|
|
||||||
|| point.line >= self.screen_lines();
|
|
||||||
|
|
||||||
let cursor_shape = if hidden {
|
|
||||||
point.line = Line(0);
|
|
||||||
CursorShape::Hidden
|
|
||||||
} else if !self.is_focused && config.cursor.unfocused_hollow {
|
|
||||||
CursorShape::HollowBlock
|
|
||||||
} else {
|
|
||||||
let cursor_style = self.cursor_style.unwrap_or(self.default_cursor_style);
|
|
||||||
|
|
||||||
if vi_mode {
|
|
||||||
self.vi_mode_cursor_style.unwrap_or(cursor_style).shape
|
|
||||||
} else {
|
|
||||||
cursor_style.shape
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Cursor colors.
|
|
||||||
let color = if vi_mode { config.colors.vi_mode_cursor } else { config.colors.cursor };
|
|
||||||
let cursor_color = if self.color_modified[NamedColor::Cursor as usize] {
|
|
||||||
CellRgb::Rgb(self.colors[NamedColor::Cursor])
|
|
||||||
} else {
|
|
||||||
color.background
|
|
||||||
};
|
|
||||||
let text_color = color.foreground;
|
|
||||||
|
|
||||||
// Expand across wide cell when inside wide char or spacer.
|
|
||||||
let buffer_point = self.visible_to_buffer(point);
|
|
||||||
let cell = &self.grid[buffer_point.line][buffer_point.col];
|
|
||||||
let is_wide = if cell.flags.contains(Flags::WIDE_CHAR_SPACER) {
|
|
||||||
point.col -= 1;
|
|
||||||
true
|
|
||||||
} else {
|
|
||||||
cell.flags.contains(Flags::WIDE_CHAR)
|
|
||||||
};
|
|
||||||
|
|
||||||
RenderableCursor {
|
|
||||||
text_color,
|
|
||||||
cursor_color,
|
|
||||||
key: CursorKey { shape: cursor_shape, is_wide },
|
|
||||||
point,
|
|
||||||
rendered: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> Dimensions for Term<T> {
|
impl<T> Dimensions for Term<T> {
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue