1
0
Fork 0
mirror of https://github.com/alacritty/alacritty.git synced 2024-11-25 14:05:41 -05:00

Add ability to select text during search

This removes the restriction of not being able to select text while the
search is active, making it a bit less jarring of a UX when the user
tries to interact with the terminal during search.

Since the selection was used during vi-less search to highlight the
focused match, there is now an option for a focused match color, which
uses the inverted normal match color by default. This focused match is
used for both search modes.

Other mouse interactions are now also possible during search, like
opening URLs or clicking inside of mouse mode applications.
This commit is contained in:
Christian Duerr 2020-11-13 05:40:09 +00:00 committed by GitHub
parent b6d94e7b13
commit 9a78449876
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 229 additions and 141 deletions

View file

@ -100,6 +100,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Unicode 13 support - Unicode 13 support
- Option to run command on bell which can be set in `bell.command` - Option to run command on bell which can be set in `bell.command`
- Fallback to program specified in `$SHELL` variable on Linux/BSD if it is present - Fallback to program specified in `$SHELL` variable on Linux/BSD if it is present
- Ability to make selections while search is active
### Changed ### Changed
@ -120,6 +121,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- URLs are no longer highlighted without a clearly delimited scheme - URLs are no longer highlighted without a clearly delimited scheme
- Renamed config option `visual_bell` to `bell` - Renamed config option `visual_bell` to `bell`
- Moved config option `dynamic_title` to `window.dynamic_title` - Moved config option `dynamic_title` to `window.dynamic_title`
- When searching without vi mode, matches are only selected once search is cancelled
### Fixed ### Fixed
@ -141,6 +143,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Ingoring of default FreeType properties - Ingoring of default FreeType properties
- Alacritty crashing at startup when the configured font does not exist - Alacritty crashing at startup when the configured font does not exist
- Font size rounding error - Font size rounding error
- Opening URLs while search is active
### Removed ### Removed

View file

@ -227,6 +227,9 @@
#matches: #matches:
# foreground: '#000000' # foreground: '#000000'
# background: '#ffffff' # background: '#ffffff'
#focused_match:
# foreground: CellBackground
# background: CellForeground
#bar: #bar:
# background: '#c5c8c6' # background: '#c5c8c6'

View file

@ -27,7 +27,7 @@ use crossfont::{self, Rasterize, Rasterizer};
use alacritty_terminal::event::{EventListener, OnResize}; use alacritty_terminal::event::{EventListener, OnResize};
use alacritty_terminal::index::{Column, Direction, Point}; use alacritty_terminal::index::{Column, Direction, Point};
use alacritty_terminal::selection::Selection; use alacritty_terminal::selection::Selection;
use alacritty_terminal::term::{RenderableCell, SizeInfo, Term, TermMode}; use alacritty_terminal::term::{SizeInfo, Term, TermMode};
use alacritty_terminal::term::{MIN_COLS, MIN_SCREEN_LINES}; use alacritty_terminal::term::{MIN_COLS, MIN_SCREEN_LINES};
use crate::config::font::Font; use crate::config::font::Font;
@ -437,7 +437,13 @@ impl Display {
mods: ModifiersState, mods: ModifiersState,
search_state: &SearchState, search_state: &SearchState,
) { ) {
let grid_cells: Vec<RenderableCell> = terminal.renderable_cells(config).collect(); // Convert search match from viewport to absolute indexing.
let search_active = search_state.regex().is_some();
let viewport_match = search_state
.focused_match()
.and_then(|focused_match| terminal.grid().clamp_buffer_range_to_visible(focused_match));
let grid_cells = terminal.renderable_cells(config, !search_active).collect::<Vec<_>>();
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 +477,21 @@ impl Display {
self.renderer.with_api(&config.ui_config, config.cursor, &size_info, |mut api| { self.renderer.with_api(&config.ui_config, config.cursor, &size_info, |mut api| {
// Iterate over all non-empty cells in the grid. // Iterate over all non-empty cells in the grid.
for cell in grid_cells { for mut cell in grid_cells {
// Invert the active match in vi-less search.
let cell_point = Point::new(cell.line, cell.column);
if cell.is_match
&& viewport_match
.as_ref()
.map_or(false, |viewport_match| viewport_match.contains(&cell_point))
{
let colors = config.colors.search.focused_match;
let match_fg = colors.foreground().color(cell.fg, cell.bg);
cell.bg = colors.background().color(cell.fg, cell.bg);
cell.fg = match_fg;
cell.bg_alpha = 1.0;
}
// Update URL underlines. // Update URL underlines.
urls.update(size_info.cols(), &cell); urls.update(size_info.cols(), &cell);
@ -525,7 +545,7 @@ impl Display {
} }
if let Some(message) = message_buffer.message() { if let Some(message) = message_buffer.message() {
let search_offset = if search_state.regex().is_some() { 1 } else { 0 }; let search_offset = if search_active { 1 } else { 0 };
let text = message.text(&size_info); let text = message.text(&size_info);
// Create a new rectangle for the background. // Create a new rectangle for the background.

View file

@ -9,6 +9,7 @@ use std::fs;
use std::fs::File; use std::fs::File;
use std::io::Write; use std::io::Write;
use std::mem; use std::mem;
use std::ops::RangeInclusive;
use std::path::PathBuf; use std::path::PathBuf;
#[cfg(not(any(target_os = "macos", windows)))] #[cfg(not(any(target_os = "macos", windows)))]
use std::sync::atomic::Ordering; use std::sync::atomic::Ordering;
@ -94,6 +95,9 @@ pub struct SearchState {
/// Search origin in viewport coordinates relative to original display offset. /// Search origin in viewport coordinates relative to original display offset.
origin: Point, origin: Point,
/// Focused match during active search.
focused_match: Option<RangeInclusive<Point<usize>>>,
} }
impl SearchState { impl SearchState {
@ -110,6 +114,11 @@ impl SearchState {
pub fn direction(&self) -> Direction { pub fn direction(&self) -> Direction {
self.direction self.direction
} }
/// Focused match during vi-less search.
pub fn focused_match(&self) -> Option<&RangeInclusive<Point<usize>>> {
self.focused_match.as_ref()
}
} }
impl Default for SearchState { impl Default for SearchState {
@ -118,6 +127,7 @@ impl Default for SearchState {
direction: Direction::Right, direction: Direction::Right,
display_offset_delta: 0, display_offset_delta: 0,
origin: Point::default(), origin: Point::default(),
focused_match: None,
regex: None, regex: None,
} }
} }
@ -209,7 +219,7 @@ impl<'a, N: Notify + 'a, T: EventListener> input::ActionContext<T> for ActionCon
selection.update(absolute_point, side); selection.update(absolute_point, side);
// Move vi cursor and expand selection. // Move vi cursor and expand selection.
if self.terminal.mode().contains(TermMode::VI) { if self.terminal.mode().contains(TermMode::VI) && !self.search_active() {
self.terminal.vi_mode_cursor.point = point; self.terminal.vi_mode_cursor.point = point;
selection.include_all(); selection.include_all();
} }
@ -385,6 +395,7 @@ impl<'a, N: Notify + 'a, T: EventListener> input::ActionContext<T> for ActionCon
let num_lines = self.terminal.screen_lines(); let num_lines = self.terminal.screen_lines();
let num_cols = self.terminal.cols(); let num_cols = self.terminal.cols();
self.search_state.focused_match = None;
self.search_state.regex = Some(String::new()); self.search_state.regex = Some(String::new());
self.search_state.direction = direction; self.search_state.direction = direction;
@ -393,9 +404,6 @@ impl<'a, N: Notify + 'a, T: EventListener> input::ActionContext<T> for ActionCon
self.search_state.origin = if self.terminal.mode().contains(TermMode::VI) { self.search_state.origin = if self.terminal.mode().contains(TermMode::VI) {
self.terminal.vi_mode_cursor.point self.terminal.vi_mode_cursor.point
} else { } else {
// Clear search, since it is used as the active match.
self.terminal.selection = None;
match direction { match direction {
Direction::Right => Point::new(Line(0), Column(0)), Direction::Right => Point::new(Line(0), Column(0)),
Direction::Left => Point::new(num_lines - 2, num_cols - 1), Direction::Left => Point::new(num_lines - 2, num_cols - 1),
@ -420,9 +428,15 @@ impl<'a, N: Notify + 'a, T: EventListener> input::ActionContext<T> for ActionCon
fn cancel_search(&mut self) { fn cancel_search(&mut self) {
self.terminal.cancel_search(); self.terminal.cancel_search();
// Recover pre-search state in vi mode.
if self.terminal.mode().contains(TermMode::VI) { if self.terminal.mode().contains(TermMode::VI) {
// Recover pre-search state in vi mode.
self.search_reset_state(); self.search_reset_state();
} else if let Some(focused_match) = &self.search_state.focused_match {
// Create a selection for the focused match.
let start = self.terminal.grid().clamp_buffer_to_visible(*focused_match.start());
let end = self.terminal.grid().clamp_buffer_to_visible(*focused_match.end());
self.start_selection(SelectionType::Simple, start, Side::Left);
self.update_selection(end, Side::Right);
} }
self.exit_search(); self.exit_search();
@ -431,8 +445,8 @@ impl<'a, N: Notify + 'a, T: EventListener> input::ActionContext<T> for ActionCon
#[inline] #[inline]
fn push_search(&mut self, c: char) { fn push_search(&mut self, c: char) {
if let Some(regex) = self.search_state.regex.as_mut() { if let Some(regex) = self.search_state.regex.as_mut() {
// Prevent previous search selections from sticking around when not in vi mode.
if !self.terminal.mode().contains(TermMode::VI) { if !self.terminal.mode().contains(TermMode::VI) {
// Clear selection so we do not obstruct any matches.
self.terminal.selection = None; self.terminal.selection = None;
} }
@ -533,8 +547,8 @@ impl<'a, N: Notify + 'a, T: EventListener> ActionContext<'a, N, T> {
self.search_reset_state(); self.search_reset_state();
self.terminal.cancel_search(); self.terminal.cancel_search();
// Restart search without vi mode to clear the search origin.
if !self.terminal.mode().contains(TermMode::VI) { if !self.terminal.mode().contains(TermMode::VI) {
// Restart search without vi mode to clear the search origin.
self.start_search(self.search_state.direction); self.start_search(self.search_state.direction);
} }
} else { } else {
@ -554,6 +568,9 @@ impl<'a, N: Notify + 'a, T: EventListener> ActionContext<'a, N, T> {
self.terminal.scroll_display(Scroll::Delta(self.search_state.display_offset_delta)); self.terminal.scroll_display(Scroll::Delta(self.search_state.display_offset_delta));
self.search_state.display_offset_delta = 0; self.search_state.display_offset_delta = 0;
// Clear focused match.
self.search_state.focused_match = None;
// Reset vi mode cursor. // Reset vi mode cursor.
let mut origin = self.search_state.origin; let mut origin = self.search_state.origin;
origin.line = min(origin.line, self.terminal.screen_lines() - 1); origin.line = min(origin.line, self.terminal.screen_lines() - 1);
@ -586,12 +603,11 @@ impl<'a, N: Notify + 'a, T: EventListener> ActionContext<'a, N, T> {
} else { } else {
// Select the match when vi mode is not active. // Select the match when vi mode is not active.
self.terminal.scroll_to_point(*regex_match.start()); self.terminal.scroll_to_point(*regex_match.start());
let start = self.terminal.grid().clamp_buffer_to_visible(*regex_match.start());
let end = self.terminal.grid().clamp_buffer_to_visible(*regex_match.end());
self.start_selection(SelectionType::Simple, start, Side::Left);
self.update_selection(end, Side::Right);
} }
// Update the focused match.
self.search_state.focused_match = Some(regex_match);
// Store number of lines the viewport had to be moved. // Store number of lines the viewport had to be moved.
let display_offset = self.terminal.grid().display_offset(); let display_offset = self.terminal.grid().display_offset();
self.search_state.display_offset_delta += old_offset - display_offset as isize; self.search_state.display_offset_delta += old_offset - display_offset as isize;
@ -611,13 +627,16 @@ impl<'a, N: Notify + 'a, T: EventListener> ActionContext<'a, N, T> {
TimerId::DelayedSearch, TimerId::DelayedSearch,
); );
} }
// Clear focused match.
self.search_state.focused_match = None;
}, },
} }
self.search_state.regex = Some(regex); self.search_state.regex = Some(regex);
} }
/// Close the search bar. /// Cleanup the search state.
fn exit_search(&mut self) { fn exit_search(&mut self) {
// Move vi cursor down if resize will pull content from history. // Move vi cursor down if resize will pull content from history.
if self.terminal.history_size() != 0 if self.terminal.history_size() != 0
@ -630,6 +649,9 @@ impl<'a, N: Notify + 'a, T: EventListener> ActionContext<'a, N, T> {
self.display_update_pending.dirty = true; self.display_update_pending.dirty = true;
self.search_state.regex = None; self.search_state.regex = None;
self.terminal.dirty = true; self.terminal.dirty = true;
// Clear focused match.
self.search_state.focused_match = None;
} }
/// Get the absolute position of the search origin. /// Get the absolute position of the search origin.

View file

@ -360,14 +360,13 @@ impl<'a, T: EventListener, A: ActionContext<T>> Processor<'a, T, A> {
#[inline] #[inline]
pub fn mouse_moved(&mut self, position: PhysicalPosition<f64>) { pub fn mouse_moved(&mut self, position: PhysicalPosition<f64>) {
let search_active = self.ctx.search_active();
let size_info = self.ctx.size_info(); let size_info = self.ctx.size_info();
let (x, y) = position.into(); let (x, y) = position.into();
let lmb_pressed = self.ctx.mouse().left_button_state == ElementState::Pressed; let lmb_pressed = self.ctx.mouse().left_button_state == ElementState::Pressed;
let rmb_pressed = self.ctx.mouse().right_button_state == ElementState::Pressed; let rmb_pressed = self.ctx.mouse().right_button_state == ElementState::Pressed;
if !self.ctx.selection_is_empty() && (lmb_pressed || rmb_pressed) && !search_active { if !self.ctx.selection_is_empty() && (lmb_pressed || rmb_pressed) {
self.update_selection_scrolling(y); self.update_selection_scrolling(y);
} }
@ -405,9 +404,7 @@ impl<'a, T: EventListener, A: ActionContext<T>> Processor<'a, T, A> {
// Don't launch URLs if mouse has moved. // Don't launch URLs if mouse has moved.
self.ctx.mouse_mut().block_url_launcher = true; self.ctx.mouse_mut().block_url_launcher = true;
if (lmb_pressed || rmb_pressed) if (lmb_pressed || rmb_pressed) && (self.ctx.modifiers().shift() || !self.ctx.mouse_mode())
&& (self.ctx.modifiers().shift() || !self.ctx.mouse_mode())
&& !search_active
{ {
self.ctx.update_selection(point, cell_side); self.ctx.update_selection(point, cell_side);
} else if inside_text_area } else if inside_text_area
@ -600,7 +597,7 @@ impl<'a, T: EventListener, A: ActionContext<T>> Processor<'a, T, A> {
self.ctx.update_selection(point, cell_side); self.ctx.update_selection(point, cell_side);
// Move vi mode cursor to mouse click position. // Move vi mode cursor to mouse click position.
if self.ctx.terminal().mode().contains(TermMode::VI) { if self.ctx.terminal().mode().contains(TermMode::VI) && !self.ctx.search_active() {
self.ctx.terminal_mut().vi_mode_cursor.point = point; self.ctx.terminal_mut().vi_mode_cursor.point = point;
} }
} }
@ -635,7 +632,7 @@ impl<'a, T: EventListener, A: ActionContext<T>> Processor<'a, T, A> {
}; };
// Move vi mode cursor to mouse click position. // Move vi mode cursor to mouse click position.
if self.ctx.terminal().mode().contains(TermMode::VI) { if self.ctx.terminal().mode().contains(TermMode::VI) && !self.ctx.search_active() {
self.ctx.terminal_mut().vi_mode_cursor.point = point; self.ctx.terminal_mut().vi_mode_cursor.point = point;
} }
} }
@ -791,7 +788,7 @@ impl<'a, T: EventListener, A: ActionContext<T>> Processor<'a, T, A> {
}; };
self.ctx.window_mut().set_mouse_cursor(new_icon); self.ctx.window_mut().set_mouse_cursor(new_icon);
} else if !self.ctx.search_active() { } else {
match state { match state {
ElementState::Pressed => { ElementState::Pressed => {
self.process_mouse_bindings(button); self.process_mouse_bindings(button);
@ -963,6 +960,11 @@ impl<'a, T: EventListener, A: ActionContext<T>> Processor<'a, T, A> {
/// The provided mode, mods, and key must match what is allowed by a binding /// The provided mode, mods, and key must match what is allowed by a binding
/// for its action to be executed. /// for its action to be executed.
fn process_mouse_bindings(&mut self, button: MouseButton) { fn process_mouse_bindings(&mut self, button: MouseButton) {
// Ignore bindings while search is active.
if self.ctx.search_active() {
return;
}
let mods = *self.ctx.modifiers(); let mods = *self.ctx.modifiers();
let mode = *self.ctx.terminal().mode(); let mode = *self.ctx.terminal().mode();
let mouse_mode = self.ctx.mouse_mode(); let mouse_mode = self.ctx.mouse_mode();

View file

@ -958,6 +958,7 @@ impl<'a> RenderApi<'a> {
bg_alpha, bg_alpha,
fg, fg,
bg: bg.unwrap_or(Rgb { r: 0, g: 0, b: 0 }), bg: bg.unwrap_or(Rgb { r: 0, g: 0, b: 0 }),
is_match: false,
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();

View file

@ -207,6 +207,7 @@ mod tests {
bg: Default::default(), bg: Default::default(),
bg_alpha: 0., bg_alpha: 0.,
flags: Flags::empty(), flags: Flags::empty(),
is_match: false,
}) })
.collect() .collect()
} }

View file

@ -15,7 +15,7 @@ pub struct Colors {
#[serde(deserialize_with = "failure_default")] #[serde(deserialize_with = "failure_default")]
pub vi_mode_cursor: CursorColors, pub vi_mode_cursor: CursorColors,
#[serde(deserialize_with = "failure_default")] #[serde(deserialize_with = "failure_default")]
pub selection: SelectionColors, pub selection: InvertedCellColors,
#[serde(deserialize_with = "failure_default")] #[serde(deserialize_with = "failure_default")]
normal: NormalColors, normal: NormalColors,
#[serde(deserialize_with = "failure_default")] #[serde(deserialize_with = "failure_default")]
@ -124,16 +124,16 @@ impl CursorColors {
#[serde(default)] #[serde(default)]
#[derive(Deserialize, Debug, Copy, Clone, Default, PartialEq, Eq)] #[derive(Deserialize, Debug, Copy, Clone, Default, PartialEq, Eq)]
pub struct SelectionColors { pub struct InvertedCellColors {
#[serde(deserialize_with = "failure_default")] #[serde(deserialize_with = "failure_default", alias = "text")]
text: DefaultBackgroundCellRgb, foreground: DefaultBackgroundCellRgb,
#[serde(deserialize_with = "failure_default")] #[serde(deserialize_with = "failure_default")]
background: DefaultForegroundCellRgb, background: DefaultForegroundCellRgb,
} }
impl SelectionColors { impl InvertedCellColors {
pub fn text(self) -> CellRgb { pub fn foreground(self) -> CellRgb {
self.text.0 self.foreground.0
} }
pub fn background(self) -> CellRgb { pub fn background(self) -> CellRgb {
@ -147,6 +147,8 @@ pub struct SearchColors {
#[serde(deserialize_with = "failure_default")] #[serde(deserialize_with = "failure_default")]
pub matches: MatchColors, pub matches: MatchColors,
#[serde(deserialize_with = "failure_default")] #[serde(deserialize_with = "failure_default")]
pub focused_match: InvertedCellColors,
#[serde(deserialize_with = "failure_default")]
bar: BarColors, bar: BarColors,
} }

View file

@ -1,7 +1,7 @@
//! A specialized 2D grid implementation optimized for use in a terminal. //! A specialized 2D grid implementation optimized for use in a terminal.
use std::cmp::{max, min}; use std::cmp::{max, min};
use std::ops::{Deref, Index, IndexMut, Range, RangeFrom, RangeFull, RangeTo}; use std::ops::{Deref, Index, IndexMut, Range, RangeFrom, RangeFull, RangeInclusive, RangeTo};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -368,6 +368,30 @@ impl<T> Grid<T> {
} }
} }
// Clamp a buffer point based range to the viewport.
//
// This will make sure the content within the range is visible and return `None` whenever the
// entire range is outside the visible region.
pub fn clamp_buffer_range_to_visible(
&self,
range: &RangeInclusive<Point<usize>>,
) -> Option<RangeInclusive<Point>> {
let start = range.start();
let end = range.end();
// Check if the range is completely offscreen
let viewport_end = self.display_offset;
let viewport_start = viewport_end + self.lines.0 - 1;
if end.line > viewport_start || start.line < viewport_end {
return None;
}
let start = self.clamp_buffer_to_visible(*start);
let end = self.clamp_buffer_to_visible(*end);
Some(start..=end)
}
/// Convert viewport relative point to global buffer indexing. /// Convert viewport relative point to global buffer indexing.
#[inline] #[inline]
pub fn visible_to_buffer(&self, point: Point) -> Point<usize> { pub fn visible_to_buffer(&self, point: Point) -> Point<usize> {
@ -759,12 +783,8 @@ impl<'a, T: 'a> DisplayIter<'a, T> {
self.offset self.offset
} }
pub fn column(&self) -> Column { pub fn point(&self) -> Point {
self.col Point::new(self.line, self.col)
}
pub fn line(&self) -> Line {
self.line
} }
} }

View file

@ -136,12 +136,44 @@ pub struct RenderableCellsIter<'a, C> {
inner: DisplayIter<'a, Cell>, inner: DisplayIter<'a, Cell>,
grid: &'a Grid<Cell>, grid: &'a Grid<Cell>,
cursor: RenderableCursor, cursor: RenderableCursor,
show_cursor: bool,
config: &'a Config<C>, config: &'a Config<C>,
colors: &'a color::List, colors: &'a color::List,
selection: Option<SelectionRange<Line>>, selection: Option<SelectionRange<Line>>,
search: RenderableSearch<'a>, 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.show_cursor && self.cursor.point == self.inner.point() {
// Handle cursor rendering.
if self.cursor.rendered {
return self.next_cursor_cell();
} else {
return self.next_cursor();
}
} else {
// Handle non-cursor cells.
let cell = self.inner.next()?;
let cell = RenderableCell::new(self, cell);
// Skip empty cells.
if !cell.is_empty() {
return Some(cell);
}
}
}
}
}
impl<'a, C> RenderableCellsIter<'a, C> { impl<'a, C> RenderableCellsIter<'a, C> {
/// Create the renderable cells iterator. /// Create the renderable cells iterator.
/// ///
@ -150,46 +182,65 @@ impl<'a, C> RenderableCellsIter<'a, C> {
fn new<T>( fn new<T>(
term: &'a Term<T>, term: &'a Term<T>,
config: &'a Config<C>, config: &'a Config<C>,
selection: Option<SelectionRange>, show_cursor: bool,
) -> RenderableCellsIter<'a, C> { ) -> RenderableCellsIter<'a, C> {
let grid = &term.grid;
let selection_range = selection.and_then(|span| {
let (limit_start, limit_end) = if span.is_block {
(span.start.col, span.end.col)
} else {
(Column(0), grid.cols() - 1)
};
// Do not render completely offscreen selection.
let viewport_end = grid.display_offset();
let viewport_start = viewport_end + grid.screen_lines().0 - 1;
if span.end.line > viewport_start || span.start.line < viewport_end {
return None;
}
// Get on-screen lines of the selection's locations.
let mut start = grid.clamp_buffer_to_visible(span.start);
let mut end = grid.clamp_buffer_to_visible(span.end);
// Trim start/end with partially visible block selection.
start.col = max(limit_start, start.col);
end.col = min(limit_end, end.col);
Some(SelectionRange::new(start, end, span.is_block))
});
RenderableCellsIter { RenderableCellsIter {
cursor: term.renderable_cursor(config), cursor: term.renderable_cursor(config),
grid, show_cursor,
inner: grid.display_iter(), grid: &term.grid,
selection: selection_range, inner: term.grid.display_iter(),
selection: term.visible_selection(),
config, config,
colors: &term.colors, colors: &term.colors,
search: RenderableSearch::new(term), 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.style == CursorStyle::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) -> Option<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);
}
Some(cell)
}
/// Check selection state of a cell. /// Check selection state of a cell.
fn is_selected(&self, point: Point) -> bool { fn is_selected(&self, point: Point) -> bool {
let selection = match self.selection { let selection = match self.selection {
@ -265,6 +316,7 @@ pub struct RenderableCell {
pub bg: Rgb, pub bg: Rgb,
pub bg_alpha: f32, pub bg_alpha: f32,
pub flags: Flags, pub flags: Flags,
pub is_match: bool,
} }
impl RenderableCell { impl RenderableCell {
@ -282,9 +334,11 @@ impl RenderableCell {
Self::compute_bg_alpha(cell.bg) Self::compute_bg_alpha(cell.bg)
}; };
let mut is_match = false;
if iter.is_selected(point) { if iter.is_selected(point) {
let config_bg = iter.config.colors.selection.background(); let config_bg = iter.config.colors.selection.background();
let selected_fg = iter.config.colors.selection.text().color(fg_rgb, bg_rgb); let selected_fg = iter.config.colors.selection.foreground().color(fg_rgb, bg_rgb);
bg_rgb = config_bg.color(fg_rgb, bg_rgb); bg_rgb = config_bg.color(fg_rgb, bg_rgb);
fg_rgb = selected_fg; fg_rgb = selected_fg;
@ -306,6 +360,8 @@ impl RenderableCell {
if config_bg != CellRgb::CellBackground { if config_bg != CellRgb::CellBackground {
bg_alpha = 1.0; bg_alpha = 1.0;
} }
is_match = true;
} }
let zerowidth = cell.zerowidth().map(|zerowidth| zerowidth.to_vec()); let zerowidth = cell.zerowidth().map(|zerowidth| zerowidth.to_vec());
@ -318,6 +374,7 @@ impl RenderableCell {
bg: bg_rgb, bg: bg_rgb,
bg_alpha, bg_alpha,
flags: cell.flags, flags: cell.flags,
is_match,
} }
} }
@ -391,73 +448,6 @@ impl RenderableCell {
} }
} }
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.line == self.inner.line()
&& self.cursor.point.col == self.inner.column()
{
if self.cursor.rendered {
// Handle cell below cursor.
let cell = self.inner.next()?;
let mut cell = RenderableCell::new(self, cell);
if self.cursor.key.style == CursorStyle::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),
};
}
return Some(cell);
} else {
// 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);
}
return Some(cell);
}
} else {
let cell = self.inner.next()?;
let cell = RenderableCell::new(self, cell);
if !cell.is_empty() {
return Some(cell);
}
}
}
}
}
pub mod mode { pub mod mode {
use bitflags::bitflags; use bitflags::bitflags;
@ -1049,10 +1039,34 @@ impl<T> Term<T> {
/// 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. Cells with an alternate background color are /// background color. Cells with an alternate background color are
/// considered renderable as are cells with any text content. /// considered renderable as are cells with any text content.
pub fn renderable_cells<'b, C>(&'b self, config: &'b Config<C>) -> RenderableCellsIter<'_, C> { pub fn renderable_cells<'b, C>(
let selection = self.selection.as_ref().and_then(|s| s.to_range(self)); &'b self,
config: &'b Config<C>,
show_cursor: bool,
) -> RenderableCellsIter<'_, C> {
RenderableCellsIter::new(&self, config, show_cursor)
}
RenderableCellsIter::new(&self, config, selection) /// Get the selection within the viewport.
pub fn visible_selection(&self) -> Option<SelectionRange<Line>> {
let selection = self.selection.as_ref()?.to_range(self)?;
// Set horizontal limits for block selection.
let (limit_start, limit_end) = if selection.is_block {
(selection.start.col, selection.end.col)
} else {
(Column(0), self.cols() - 1)
};
let range = self.grid.clamp_buffer_range_to_visible(&(selection.start..=selection.end))?;
let mut start = *range.start();
let mut end = *range.end();
// Trim start/end with partially visible block selection.
start.col = max(limit_start, start.col);
end.col = min(limit_end, end.col);
Some(SelectionRange::new(start, end, selection.is_block))
} }
/// Resize terminal to new dimensions. /// Resize terminal to new dimensions.
@ -2836,7 +2850,7 @@ mod benches {
mem::swap(&mut terminal.grid, &mut grid); mem::swap(&mut terminal.grid, &mut grid);
b.iter(|| { b.iter(|| {
let iter = terminal.renderable_cells(&config); let iter = terminal.renderable_cells(&config, true);
for cell in iter { for cell in iter {
test::black_box(cell); test::black_box(cell);
} }