From 9a7844987693909925b8663d8aa905231d291410 Mon Sep 17 00:00:00 2001 From: Christian Duerr Date: Fri, 13 Nov 2020 05:40:09 +0000 Subject: [PATCH] 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. --- CHANGELOG.md | 3 + alacritty.yml | 3 + alacritty/src/display.rs | 28 ++- alacritty/src/event.rs | 46 +++-- alacritty/src/input.rs | 18 +- alacritty/src/renderer/mod.rs | 1 + alacritty/src/url.rs | 1 + alacritty_terminal/src/config/colors.rs | 16 +- alacritty_terminal/src/grid/mod.rs | 34 +++- alacritty_terminal/src/term/mod.rs | 220 +++++++++++++----------- 10 files changed, 229 insertions(+), 141 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f8d615b7..5570807d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -100,6 +100,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Unicode 13 support - 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 +- Ability to make selections while search is active ### 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 - Renamed config option `visual_bell` to `bell` - Moved config option `dynamic_title` to `window.dynamic_title` +- When searching without vi mode, matches are only selected once search is cancelled ### 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 - Alacritty crashing at startup when the configured font does not exist - Font size rounding error +- Opening URLs while search is active ### Removed diff --git a/alacritty.yml b/alacritty.yml index 7795136d..2c96d23e 100644 --- a/alacritty.yml +++ b/alacritty.yml @@ -227,6 +227,9 @@ #matches: # foreground: '#000000' # background: '#ffffff' + #focused_match: + # foreground: CellBackground + # background: CellForeground #bar: # background: '#c5c8c6' diff --git a/alacritty/src/display.rs b/alacritty/src/display.rs index 6d683336..af21001e 100644 --- a/alacritty/src/display.rs +++ b/alacritty/src/display.rs @@ -27,7 +27,7 @@ use crossfont::{self, Rasterize, Rasterizer}; use alacritty_terminal::event::{EventListener, OnResize}; use alacritty_terminal::index::{Column, Direction, Point}; 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 crate::config::font::Font; @@ -437,7 +437,13 @@ impl Display { mods: ModifiersState, search_state: &SearchState, ) { - let grid_cells: Vec = 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::>(); let visual_bell_intensity = terminal.visual_bell.intensity(); let background_color = terminal.background_color(); 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| { // 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. urls.update(size_info.cols(), &cell); @@ -525,7 +545,7 @@ impl Display { } 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); // Create a new rectangle for the background. diff --git a/alacritty/src/event.rs b/alacritty/src/event.rs index 20f087c3..c1f81300 100644 --- a/alacritty/src/event.rs +++ b/alacritty/src/event.rs @@ -9,6 +9,7 @@ use std::fs; use std::fs::File; use std::io::Write; use std::mem; +use std::ops::RangeInclusive; use std::path::PathBuf; #[cfg(not(any(target_os = "macos", windows)))] use std::sync::atomic::Ordering; @@ -94,6 +95,9 @@ pub struct SearchState { /// Search origin in viewport coordinates relative to original display offset. origin: Point, + + /// Focused match during active search. + focused_match: Option>>, } impl SearchState { @@ -110,6 +114,11 @@ impl SearchState { pub fn direction(&self) -> Direction { self.direction } + + /// Focused match during vi-less search. + pub fn focused_match(&self) -> Option<&RangeInclusive>> { + self.focused_match.as_ref() + } } impl Default for SearchState { @@ -118,6 +127,7 @@ impl Default for SearchState { direction: Direction::Right, display_offset_delta: 0, origin: Point::default(), + focused_match: None, regex: None, } } @@ -209,7 +219,7 @@ impl<'a, N: Notify + 'a, T: EventListener> input::ActionContext for ActionCon selection.update(absolute_point, side); // 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; selection.include_all(); } @@ -385,6 +395,7 @@ impl<'a, N: Notify + 'a, T: EventListener> input::ActionContext for ActionCon let num_lines = self.terminal.screen_lines(); let num_cols = self.terminal.cols(); + self.search_state.focused_match = None; self.search_state.regex = Some(String::new()); self.search_state.direction = direction; @@ -393,9 +404,6 @@ impl<'a, N: Notify + 'a, T: EventListener> input::ActionContext for ActionCon self.search_state.origin = if self.terminal.mode().contains(TermMode::VI) { self.terminal.vi_mode_cursor.point } else { - // Clear search, since it is used as the active match. - self.terminal.selection = None; - match direction { Direction::Right => Point::new(Line(0), Column(0)), Direction::Left => Point::new(num_lines - 2, num_cols - 1), @@ -420,9 +428,15 @@ impl<'a, N: Notify + 'a, T: EventListener> input::ActionContext for ActionCon fn cancel_search(&mut self) { self.terminal.cancel_search(); - // Recover pre-search state in vi mode. if self.terminal.mode().contains(TermMode::VI) { + // Recover pre-search state in vi mode. 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(); @@ -431,8 +445,8 @@ impl<'a, N: Notify + 'a, T: EventListener> input::ActionContext for ActionCon #[inline] fn push_search(&mut self, c: char) { 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) { + // Clear selection so we do not obstruct any matches. self.terminal.selection = None; } @@ -533,8 +547,8 @@ impl<'a, N: Notify + 'a, T: EventListener> ActionContext<'a, N, T> { self.search_reset_state(); self.terminal.cancel_search(); - // Restart search without vi mode to clear the search origin. if !self.terminal.mode().contains(TermMode::VI) { + // Restart search without vi mode to clear the search origin. self.start_search(self.search_state.direction); } } 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.search_state.display_offset_delta = 0; + // Clear focused match. + self.search_state.focused_match = None; + // Reset vi mode cursor. let mut origin = self.search_state.origin; 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 { // Select the match when vi mode is not active. 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. let display_offset = self.terminal.grid().display_offset(); 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, ); } + + // Clear focused match. + self.search_state.focused_match = None; }, } self.search_state.regex = Some(regex); } - /// Close the search bar. + /// Cleanup the search state. fn exit_search(&mut self) { // Move vi cursor down if resize will pull content from history. 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.search_state.regex = None; self.terminal.dirty = true; + + // Clear focused match. + self.search_state.focused_match = None; } /// Get the absolute position of the search origin. diff --git a/alacritty/src/input.rs b/alacritty/src/input.rs index 9c75753a..348db610 100644 --- a/alacritty/src/input.rs +++ b/alacritty/src/input.rs @@ -360,14 +360,13 @@ impl<'a, T: EventListener, A: ActionContext> Processor<'a, T, A> { #[inline] pub fn mouse_moved(&mut self, position: PhysicalPosition) { - let search_active = self.ctx.search_active(); let size_info = self.ctx.size_info(); let (x, y) = position.into(); let lmb_pressed = self.ctx.mouse().left_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); } @@ -405,9 +404,7 @@ impl<'a, T: EventListener, A: ActionContext> Processor<'a, T, A> { // Don't launch URLs if mouse has moved. self.ctx.mouse_mut().block_url_launcher = true; - if (lmb_pressed || rmb_pressed) - && (self.ctx.modifiers().shift() || !self.ctx.mouse_mode()) - && !search_active + if (lmb_pressed || rmb_pressed) && (self.ctx.modifiers().shift() || !self.ctx.mouse_mode()) { self.ctx.update_selection(point, cell_side); } else if inside_text_area @@ -600,7 +597,7 @@ impl<'a, T: EventListener, A: ActionContext> Processor<'a, T, A> { self.ctx.update_selection(point, cell_side); // 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; } } @@ -635,7 +632,7 @@ impl<'a, T: EventListener, A: ActionContext> Processor<'a, T, A> { }; // 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; } } @@ -791,7 +788,7 @@ impl<'a, T: EventListener, A: ActionContext> Processor<'a, T, A> { }; self.ctx.window_mut().set_mouse_cursor(new_icon); - } else if !self.ctx.search_active() { + } else { match state { ElementState::Pressed => { self.process_mouse_bindings(button); @@ -963,6 +960,11 @@ impl<'a, T: EventListener, A: ActionContext> Processor<'a, T, A> { /// The provided mode, mods, and key must match what is allowed by a binding /// for its action to be executed. 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 mode = *self.ctx.terminal().mode(); let mouse_mode = self.ctx.mouse_mode(); diff --git a/alacritty/src/renderer/mod.rs b/alacritty/src/renderer/mod.rs index f628d24f..b347556d 100644 --- a/alacritty/src/renderer/mod.rs +++ b/alacritty/src/renderer/mod.rs @@ -958,6 +958,7 @@ impl<'a> RenderApi<'a> { bg_alpha, fg, bg: bg.unwrap_or(Rgb { r: 0, g: 0, b: 0 }), + is_match: false, }) .collect::>(); diff --git a/alacritty/src/url.rs b/alacritty/src/url.rs index f3f60dd3..5d35667d 100644 --- a/alacritty/src/url.rs +++ b/alacritty/src/url.rs @@ -207,6 +207,7 @@ mod tests { bg: Default::default(), bg_alpha: 0., flags: Flags::empty(), + is_match: false, }) .collect() } diff --git a/alacritty_terminal/src/config/colors.rs b/alacritty_terminal/src/config/colors.rs index 13a30bef..a292fde4 100644 --- a/alacritty_terminal/src/config/colors.rs +++ b/alacritty_terminal/src/config/colors.rs @@ -15,7 +15,7 @@ pub struct Colors { #[serde(deserialize_with = "failure_default")] pub vi_mode_cursor: CursorColors, #[serde(deserialize_with = "failure_default")] - pub selection: SelectionColors, + pub selection: InvertedCellColors, #[serde(deserialize_with = "failure_default")] normal: NormalColors, #[serde(deserialize_with = "failure_default")] @@ -124,16 +124,16 @@ impl CursorColors { #[serde(default)] #[derive(Deserialize, Debug, Copy, Clone, Default, PartialEq, Eq)] -pub struct SelectionColors { - #[serde(deserialize_with = "failure_default")] - text: DefaultBackgroundCellRgb, +pub struct InvertedCellColors { + #[serde(deserialize_with = "failure_default", alias = "text")] + foreground: DefaultBackgroundCellRgb, #[serde(deserialize_with = "failure_default")] background: DefaultForegroundCellRgb, } -impl SelectionColors { - pub fn text(self) -> CellRgb { - self.text.0 +impl InvertedCellColors { + pub fn foreground(self) -> CellRgb { + self.foreground.0 } pub fn background(self) -> CellRgb { @@ -147,6 +147,8 @@ pub struct SearchColors { #[serde(deserialize_with = "failure_default")] pub matches: MatchColors, #[serde(deserialize_with = "failure_default")] + pub focused_match: InvertedCellColors, + #[serde(deserialize_with = "failure_default")] bar: BarColors, } diff --git a/alacritty_terminal/src/grid/mod.rs b/alacritty_terminal/src/grid/mod.rs index 70dbc936..21e7e2f9 100644 --- a/alacritty_terminal/src/grid/mod.rs +++ b/alacritty_terminal/src/grid/mod.rs @@ -1,7 +1,7 @@ //! A specialized 2D grid implementation optimized for use in a terminal. 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}; @@ -368,6 +368,30 @@ impl Grid { } } + // 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>, + ) -> Option> { + 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. #[inline] pub fn visible_to_buffer(&self, point: Point) -> Point { @@ -759,12 +783,8 @@ impl<'a, T: 'a> DisplayIter<'a, T> { self.offset } - pub fn column(&self) -> Column { - self.col - } - - pub fn line(&self) -> Line { - self.line + pub fn point(&self) -> Point { + Point::new(self.line, self.col) } } diff --git a/alacritty_terminal/src/term/mod.rs b/alacritty_terminal/src/term/mod.rs index 6084d8b0..6e20f0e1 100644 --- a/alacritty_terminal/src/term/mod.rs +++ b/alacritty_terminal/src/term/mod.rs @@ -136,12 +136,44 @@ pub struct RenderableCellsIter<'a, C> { inner: DisplayIter<'a, Cell>, grid: &'a Grid, cursor: RenderableCursor, + show_cursor: bool, config: &'a Config, colors: &'a color::List, selection: Option>, 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 { + 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> { /// Create the renderable cells iterator. /// @@ -150,46 +182,65 @@ impl<'a, C> RenderableCellsIter<'a, C> { fn new( term: &'a Term, config: &'a Config, - selection: Option, + show_cursor: bool, ) -> 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 { cursor: term.renderable_cursor(config), - grid, - inner: grid.display_iter(), - selection: selection_range, + 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 { + // 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 { + // 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. fn is_selected(&self, point: Point) -> bool { let selection = match self.selection { @@ -265,6 +316,7 @@ pub struct RenderableCell { pub bg: Rgb, pub bg_alpha: f32, pub flags: Flags, + pub is_match: bool, } impl RenderableCell { @@ -282,9 +334,11 @@ impl RenderableCell { 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.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); fg_rgb = selected_fg; @@ -306,6 +360,8 @@ impl RenderableCell { if config_bg != CellRgb::CellBackground { bg_alpha = 1.0; } + + is_match = true; } let zerowidth = cell.zerowidth().map(|zerowidth| zerowidth.to_vec()); @@ -318,6 +374,7 @@ impl RenderableCell { bg: bg_rgb, bg_alpha, 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 { - 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 { use bitflags::bitflags; @@ -1049,10 +1039,34 @@ impl Term { /// A renderable cell is any cell which has content other than the default /// background color. Cells with an alternate background color are /// considered renderable as are cells with any text content. - pub fn renderable_cells<'b, C>(&'b self, config: &'b Config) -> RenderableCellsIter<'_, C> { - let selection = self.selection.as_ref().and_then(|s| s.to_range(self)); + pub fn renderable_cells<'b, C>( + &'b self, + config: &'b Config, + 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> { + 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. @@ -2836,7 +2850,7 @@ mod benches { mem::swap(&mut terminal.grid, &mut grid); b.iter(|| { - let iter = terminal.renderable_cells(&config); + let iter = terminal.renderable_cells(&config, true); for cell in iter { test::black_box(cell); }