aboutsummaryrefslogtreecommitdiff
path: root/alacritty/src/display
diff options
context:
space:
mode:
authorKirill Chibisov <contact@kchibisov.com>2022-07-10 20:11:28 +0300
committerGitHub <noreply@github.com>2022-07-10 20:11:28 +0300
commit694a52bcffeffdc9e163818c3b2ac5c39e26f1ef (patch)
treea774babc1869b4c700d7df1478dbbfe5b2c3bcda /alacritty/src/display
parent8451b75689b44f11ec1707af7e26d915772c3972 (diff)
downloadr-alacritty-694a52bcffeffdc9e163818c3b2ac5c39e26f1ef.tar.gz
r-alacritty-694a52bcffeffdc9e163818c3b2ac5c39e26f1ef.tar.bz2
r-alacritty-694a52bcffeffdc9e163818c3b2ac5c39e26f1ef.zip
Add support for hyperlink escape sequence
This commit adds support for hyperlink escape sequence `OSC 8 ; params ; URI ST`. The configuration option responsible for those is `hints.enabled.hyperlinks`. Fixes #922.
Diffstat (limited to 'alacritty/src/display')
-rw-r--r--alacritty/src/display/content.rs147
-rw-r--r--alacritty/src/display/hint.rs242
-rw-r--r--alacritty/src/display/mod.rs263
3 files changed, 450 insertions, 202 deletions
diff --git a/alacritty/src/display/content.rs b/alacritty/src/display/content.rs
index 77571d94..478982bb 100644
--- a/alacritty/src/display/content.rs
+++ b/alacritty/src/display/content.rs
@@ -1,22 +1,21 @@
use std::borrow::Cow;
-use std::cmp::{max, min};
-use std::mem;
-use std::ops::{Deref, DerefMut, RangeInclusive};
+use std::ops::Deref;
+use std::{cmp, mem};
use alacritty_terminal::ansi::{Color, CursorShape, NamedColor};
use alacritty_terminal::event::EventListener;
-use alacritty_terminal::grid::{Dimensions, Indexed};
-use alacritty_terminal::index::{Column, Direction, Line, Point};
+use alacritty_terminal::grid::Indexed;
+use alacritty_terminal::index::{Column, Line, Point};
use alacritty_terminal::selection::SelectionRange;
-use alacritty_terminal::term::cell::{Cell, Flags};
+use alacritty_terminal::term::cell::{Cell, Flags, Hyperlink};
use alacritty_terminal::term::color::{CellRgb, Rgb};
-use alacritty_terminal::term::search::{Match, RegexIter, RegexSearch};
+use alacritty_terminal::term::search::{Match, RegexSearch};
use alacritty_terminal::term::{self, RenderableContent as TerminalContent, Term, TermMode};
use crate::config::UiConfig;
use crate::display::color::{List, DIM_FACTOR};
-use crate::display::hint::HintState;
-use crate::display::{Display, MAX_SEARCH_LINES};
+use crate::display::hint::{self, HintState};
+use crate::display::Display;
use crate::event::SearchState;
/// Minimum contrast between a fixed cursor color and the cell's background.
@@ -30,7 +29,7 @@ pub struct RenderableContent<'a> {
cursor: RenderableCursor,
cursor_shape: CursorShape,
cursor_point: Point<usize>,
- search: Option<Regex<'a>>,
+ search: Option<HintMatches<'a>>,
hint: Option<Hint<'a>>,
config: &'a UiConfig,
colors: &'a List,
@@ -44,7 +43,7 @@ impl<'a> RenderableContent<'a> {
term: &'a Term<T>,
search_state: &'a SearchState,
) -> Self {
- let search = search_state.dfas().map(|dfas| Regex::new(term, dfas));
+ let search = search_state.dfas().map(|dfas| HintMatches::visible_regex_matches(term, dfas));
let focused_match = search_state.focused_match();
let terminal_content = term.renderable_content();
@@ -181,13 +180,21 @@ impl<'a> Iterator for RenderableContent<'a> {
#[derive(Clone, Debug)]
pub struct RenderableCell {
pub character: char,
- pub zerowidth: Option<Vec<char>>,
pub point: Point<usize>,
pub fg: Rgb,
pub bg: Rgb,
pub bg_alpha: f32,
pub underline: Rgb,
pub flags: Flags,
+ pub extra: Option<Box<RenderableCellExtra>>,
+}
+
+/// Extra storage with rarely present fields for [`RenderableCell`], to reduce the cell size we
+/// pass around.
+#[derive(Clone, Debug)]
+pub struct RenderableCellExtra {
+ pub zerowidth: Option<Vec<char>>,
+ pub hyperlink: Option<Hyperlink>,
}
impl RenderableCell {
@@ -257,23 +264,24 @@ impl RenderableCell {
.underline_color()
.map_or(fg, |underline| Self::compute_fg_rgb(content, underline, flags));
- RenderableCell {
- zerowidth: cell.zerowidth().map(|zerowidth| zerowidth.to_vec()),
- flags,
- character,
- bg_alpha,
- point,
- fg,
- bg,
- underline,
- }
+ let zerowidth = cell.zerowidth();
+ let hyperlink = cell.hyperlink();
+
+ let extra = (zerowidth.is_some() || hyperlink.is_some()).then(|| {
+ Box::new(RenderableCellExtra {
+ zerowidth: zerowidth.map(|zerowidth| zerowidth.to_vec()),
+ hyperlink,
+ })
+ });
+
+ RenderableCell { flags, character, bg_alpha, point, fg, bg, underline, extra }
}
/// Check if cell contains any renderable content.
fn is_empty(&self) -> bool {
self.bg_alpha == 0.
&& self.character == ' '
- && self.zerowidth.is_none()
+ && self.extra.is_none()
&& !self.flags.intersects(Flags::ALL_UNDERLINES | Flags::STRIKEOUT)
}
@@ -406,7 +414,7 @@ impl RenderableCursor {
/// Regex hints for keyboard shortcuts.
struct Hint<'a> {
/// Hint matches and position.
- regex: Regex<'a>,
+ matches: HintMatches<'a>,
/// Last match checked against current cell position.
labels: &'a Vec<Vec<char>>,
@@ -421,16 +429,15 @@ impl<'a> Hint<'a> {
/// The tuple's [`bool`] will be `true` when the character is the first for this hint.
fn advance(&mut self, viewport_start: Point, point: Point) -> Option<(char, bool)> {
// Check if we're within a match at all.
- if !self.regex.advance(point) {
+ if !self.matches.advance(point) {
return None;
}
// Match starting position on this line; linebreaks interrupt the hint labels.
let start = self
- .regex
.matches
- .get(self.regex.index)
- .map(|regex_match| max(*regex_match.start(), viewport_start))
+ .get(self.matches.index)
+ .map(|bounds| cmp::max(*bounds.start(), viewport_start))
.filter(|start| start.line == point.line)?;
// Position within the hint label.
@@ -438,85 +445,47 @@ impl<'a> Hint<'a> {
let is_first = label_position == 0;
// Hint label character.
- self.labels[self.regex.index].get(label_position).copied().map(|c| (c, is_first))
+ self.labels[self.matches.index].get(label_position).copied().map(|c| (c, is_first))
}
}
impl<'a> From<&'a HintState> for Hint<'a> {
fn from(hint_state: &'a HintState) -> Self {
- let regex = Regex { matches: Cow::Borrowed(hint_state.matches()), index: 0 };
- Self { labels: hint_state.labels(), regex }
- }
-}
-
-/// Wrapper for finding visible regex matches.
-#[derive(Default, Clone)]
-pub struct RegexMatches(pub Vec<RangeInclusive<Point>>);
-
-impl RegexMatches {
- /// Find all visible matches.
- pub fn new<T>(term: &Term<T>, dfas: &RegexSearch) -> Self {
- let viewport_start = Line(-(term.grid().display_offset() as i32));
- let viewport_end = viewport_start + term.bottommost_line();
-
- // 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.last_column());
- let mut end = term.line_search_right(end_point);
-
- // Set upper bound on search before/after the viewport to prevent excessive blocking.
- start.line = max(start.line, viewport_start - MAX_SEARCH_LINES);
- end.line = min(end.line, viewport_end + MAX_SEARCH_LINES);
-
- // Create an iterater for the current regex search for all visible matches.
- let iter = RegexIter::new(start, end, Direction::Right, term, dfas)
- .skip_while(move |rm| rm.end().line < viewport_start)
- .take_while(move |rm| rm.start().line <= viewport_end);
-
- Self(iter.collect())
- }
-}
-
-impl Deref for RegexMatches {
- type Target = Vec<RangeInclusive<Point>>;
-
- fn deref(&self) -> &Self::Target {
- &self.0
- }
-}
-
-impl DerefMut for RegexMatches {
- fn deref_mut(&mut self) -> &mut Self::Target {
- &mut self.0
+ let matches = HintMatches::new(hint_state.matches());
+ Self { labels: hint_state.labels(), matches }
}
}
-/// Visible regex match tracking.
+/// Visible hint match tracking.
#[derive(Default)]
-struct Regex<'a> {
+struct HintMatches<'a> {
/// All visible matches.
- matches: Cow<'a, RegexMatches>,
+ matches: Cow<'a, [Match]>,
/// Index of the last match checked.
index: usize,
}
-impl<'a> Regex<'a> {
- /// Create a new renderable regex iterator.
- fn new<T>(term: &Term<T>, dfas: &RegexSearch) -> Self {
- let matches = Cow::Owned(RegexMatches::new(term, dfas));
- Self { index: 0, matches }
+impl<'a> HintMatches<'a> {
+ /// Create new renderable matches iterator..
+ fn new(matches: impl Into<Cow<'a, [Match]>>) -> Self {
+ Self { matches: matches.into(), index: 0 }
+ }
+
+ /// Create from regex matches on term visable part.
+ fn visible_regex_matches<T>(term: &Term<T>, dfas: &RegexSearch) -> Self {
+ let matches = hint::visible_regex_match_iter(term, dfas).collect::<Vec<_>>();
+ Self::new(matches)
}
/// Advance the regex tracker to the next point.
///
/// This will return `true` if the point passed is part of a regex match.
fn advance(&mut self, point: Point) -> bool {
- while let Some(regex_match) = self.matches.get(self.index) {
- if regex_match.start() > &point {
+ while let Some(bounds) = self.get(self.index) {
+ if bounds.start() > &point {
break;
- } else if regex_match.end() < &point {
+ } else if bounds.end() < &point {
self.index += 1;
} else {
return true;
@@ -525,3 +494,11 @@ impl<'a> Regex<'a> {
false
}
}
+
+impl<'a> Deref for HintMatches<'a> {
+ type Target = [Match];
+
+ fn deref(&self) -> &Self::Target {
+ self.matches.deref()
+ }
+}
diff --git a/alacritty/src/display/hint.rs b/alacritty/src/display/hint.rs
index 8cdb9708..a59f2bc7 100644
--- a/alacritty/src/display/hint.rs
+++ b/alacritty/src/display/hint.rs
@@ -1,16 +1,20 @@
-use std::cmp::{max, min};
+use std::cmp::Reverse;
+use std::collections::HashSet;
+use std::iter;
use glutin::event::ModifiersState;
-use alacritty_terminal::grid::BidirectionalIterator;
-use alacritty_terminal::index::{Boundary, Direction, Point};
+use alacritty_terminal::grid::{BidirectionalIterator, Dimensions};
+use alacritty_terminal::index::{Boundary, Column, Direction, Line, Point};
+use alacritty_terminal::term::cell::Hyperlink;
use alacritty_terminal::term::search::{Match, RegexIter, RegexSearch};
use alacritty_terminal::term::{Term, TermMode};
use crate::config::ui_config::{Hint, HintAction};
use crate::config::UiConfig;
-use crate::display::content::RegexMatches;
-use crate::display::MAX_SEARCH_LINES;
+
+/// Maximum number of linewraps followed outside of the viewport during search highlighting.
+pub const MAX_SEARCH_LINES: usize = 100;
/// Percentage of characters in the hints alphabet used for the last character.
const HINT_SPLIT_PERCENTAGE: f32 = 0.5;
@@ -24,7 +28,7 @@ pub struct HintState {
alphabet: String,
/// Visible matches.
- matches: RegexMatches,
+ matches: Vec<Match>,
/// Key label for each visible match.
labels: Vec<Vec<char>>,
@@ -70,20 +74,29 @@ impl HintState {
None => return,
};
- // Find visible matches.
- self.matches.0 = hint.regex.with_compiled(|regex| {
- let mut matches = RegexMatches::new(term, regex);
+ // Clear current matches.
+ self.matches.clear();
- // Apply post-processing and search for sub-matches if necessary.
- if hint.post_processing {
- matches
- .drain(..)
- .flat_map(|rm| HintPostProcessor::new(term, regex, rm).collect::<Vec<_>>())
- .collect()
- } else {
- matches.0
- }
- });
+ // Add escape sequence hyperlinks.
+ if hint.content.hyperlinks {
+ self.matches.extend(visible_unique_hyperlink_iter(term));
+ }
+
+ // Add visible regex matches.
+ if let Some(regex) = hint.content.regex.as_ref() {
+ regex.with_compiled(|regex| {
+ let matches = visible_regex_match_iter(term, regex);
+
+ // Apply post-processing and search for sub-matches if necessary.
+ if hint.post_processing {
+ self.matches.extend(matches.flat_map(|rm| {
+ HintPostProcessor::new(term, regex, rm).collect::<Vec<_>>()
+ }));
+ } else {
+ self.matches.extend(matches);
+ }
+ });
+ }
// Cancel highlight with no visible matches.
if self.matches.is_empty() {
@@ -91,6 +104,10 @@ impl HintState {
return;
}
+ // Sort and dedup ranges. Currently overlapped but not exactly same ranges are kept.
+ self.matches.sort_by_key(|bounds| (*bounds.start(), Reverse(*bounds.end())));
+ self.matches.dedup_by_key(|bounds| *bounds.start());
+
let mut generator = HintLabels::new(&self.alphabet, HINT_SPLIT_PERCENTAGE);
let match_count = self.matches.len();
let keys_len = self.keys.len();
@@ -135,7 +152,9 @@ impl HintState {
self.stop();
- Some(HintMatch { action, bounds })
+ // Hyperlinks take precedence over regex matches.
+ let hyperlink = term.grid()[*bounds.start()].hyperlink();
+ Some(HintMatch { action, bounds, hyperlink })
} else {
// Store character to preserve the selection.
self.keys.push(c);
@@ -150,7 +169,7 @@ impl HintState {
}
/// Visible hint regex matches.
- pub fn matches(&self) -> &RegexMatches {
+ pub fn matches(&self) -> &[Match] {
&self.matches
}
@@ -167,10 +186,33 @@ impl HintState {
#[derive(PartialEq, Eq, Debug, Clone)]
pub struct HintMatch {
/// Action for handling the text.
- pub action: HintAction,
+ action: HintAction,
/// Terminal range matching the hint.
- pub bounds: Match,
+ bounds: Match,
+
+ hyperlink: Option<Hyperlink>,
+}
+
+impl HintMatch {
+ #[inline]
+ pub fn should_highlight(&self, point: Point, pointed_hyperlink: Option<&Hyperlink>) -> bool {
+ self.bounds.contains(&point) && self.hyperlink.as_ref() == pointed_hyperlink
+ }
+
+ #[inline]
+ pub fn action(&self) -> &HintAction {
+ &self.action
+ }
+
+ #[inline]
+ pub fn bounds(&self) -> &Match {
+ &self.bounds
+ }
+
+ pub fn hyperlink(&self) -> Option<&Hyperlink> {
+ self.hyperlink.as_ref()
+ }
}
/// Generator for creating new hint labels.
@@ -238,6 +280,77 @@ impl HintLabels {
}
}
+/// Iterate over all visible regex matches.
+pub fn visible_regex_match_iter<'a, T>(
+ term: &'a Term<T>,
+ regex: &'a RegexSearch,
+) -> impl Iterator<Item = Match> + 'a {
+ let viewport_start = Line(-(term.grid().display_offset() as i32));
+ let viewport_end = viewport_start + term.bottommost_line();
+ let mut start = term.line_search_left(Point::new(viewport_start, Column(0)));
+ let mut end = term.line_search_right(Point::new(viewport_end, Column(0)));
+ start.line = start.line.max(viewport_start - MAX_SEARCH_LINES);
+ end.line = end.line.min(viewport_start + MAX_SEARCH_LINES);
+
+ RegexIter::new(start, end, Direction::Right, term, regex)
+ .skip_while(move |rm| rm.end().line < viewport_start)
+ .take_while(move |rm| rm.start().line <= viewport_end)
+}
+
+/// Iterate over all visible hyperlinks, yanking only unique ones.
+pub fn visible_unique_hyperlink_iter<T>(term: &Term<T>) -> impl Iterator<Item = Match> + '_ {
+ let mut display_iter = term.grid().display_iter().peekable();
+
+ // Avoid creating hints for the same hyperlinks, but from a different places.
+ let mut unique_hyperlinks = HashSet::new();
+
+ iter::from_fn(move || {
+ // Find the start of the next unique hyperlink.
+ let (cell, hyperlink) = display_iter.find_map(|cell| {
+ let hyperlink = cell.hyperlink()?;
+ unique_hyperlinks.contains(&hyperlink).then(|| {
+ unique_hyperlinks.insert(hyperlink.clone());
+ (cell, hyperlink)
+ })
+ })?;
+
+ let start = cell.point;
+ let mut end = start;
+
+ // Find the end bound of just found unique hyperlink.
+ while let Some(next_cell) = display_iter.peek() {
+ // Cell at display iter doesn't match, yield the hyperlink and start over with
+ // `find_map`.
+ if next_cell.hyperlink().as_ref() != Some(&hyperlink) {
+ break;
+ }
+
+ // Advance to the next cell.
+ end = next_cell.point;
+ let _ = display_iter.next();
+ }
+
+ Some(start..=end)
+ })
+}
+
+/// Retrieve the match, if the specified point is inside the content matching the regex.
+fn regex_match_at<T>(
+ term: &Term<T>,
+ point: Point,
+ regex: &RegexSearch,
+ post_processing: bool,
+) -> Option<Match> {
+ let regex_match = visible_regex_match_iter(term, regex).find(|rm| rm.contains(&point))?;
+
+ // Apply post-processing and search for sub-matches if necessary.
+ if post_processing {
+ HintPostProcessor::new(term, regex, regex_match).find(|rm| rm.contains(&point))
+ } else {
+ Some(regex_match)
+ }
+}
+
/// Check if there is a hint highlighted at the specified point.
pub fn highlighted_at<T>(
term: &Term<T>,
@@ -258,30 +371,73 @@ pub fn highlighted_at<T>(
return None;
}
- hint.regex.with_compiled(|regex| {
- // Setup search boundaries.
- let mut start = term.line_search_left(point);
- start.line = max(start.line, point.line - MAX_SEARCH_LINES);
- let mut end = term.line_search_right(point);
- end.line = min(end.line, point.line + MAX_SEARCH_LINES);
+ if let Some((hyperlink, bounds)) =
+ hint.content.hyperlinks.then(|| hyperlink_at(term, point)).flatten()
+ {
+ return Some(HintMatch {
+ bounds,
+ action: hint.action.clone(),
+ hyperlink: Some(hyperlink),
+ });
+ }
- // Function to verify that the specified point is inside the match.
- let at_point = |rm: &Match| *rm.end() >= point && *rm.start() <= point;
+ if let Some(bounds) = hint.content.regex.as_ref().and_then(|regex| {
+ regex.with_compiled(|regex| regex_match_at(term, point, regex, hint.post_processing))
+ }) {
+ return Some(HintMatch { bounds, action: hint.action.clone(), hyperlink: None });
+ }
- // Check if there's any match at the specified point.
- let mut iter = RegexIter::new(start, end, Direction::Right, term, regex);
- let regex_match = iter.find(at_point)?;
+ None
+ })
+}
- // Apply post-processing and search for sub-matches if necessary.
- let regex_match = if hint.post_processing {
- HintPostProcessor::new(term, regex, regex_match).find(at_point)
- } else {
- Some(regex_match)
- };
+/// Retrieve the hyperlink with its range, if there is one at the specified point.
+fn hyperlink_at<T>(term: &Term<T>, point: Point) -> Option<(Hyperlink, Match)> {
+ let hyperlink = term.grid()[point].hyperlink()?;
- regex_match.map(|bounds| HintMatch { action: hint.action.clone(), bounds })
- })
- })
+ let viewport_start = Line(-(term.grid().display_offset() as i32));
+ let viewport_end = viewport_start + term.bottommost_line();
+
+ let mut match_start = Point::new(point.line, Column(0));
+ let mut match_end = Point::new(point.line, Column(term.columns() - 1));
+ let grid = term.grid();
+
+ // Find adjacent lines that have the same `hyperlink`. The end purpose to highlight hyperlinks
+ // that span across multiple lines or not directly attached to each other.
+
+ // Find the closest to the viewport start adjucent line.
+ while match_start.line > viewport_start {
+ let next_line = match_start.line - 1i32;
+ // Iterate over all the cells in the grid's line and check if any of those cells contains
+ // the hyperlink we've found at original `point`.
+ let line_contains_hyperlink = grid[next_line]
+ .into_iter()
+ .any(|cell| cell.hyperlink().map(|h| h == hyperlink).unwrap_or(false));
+
+ // There's no hyperlink on the next line, break.
+ if !line_contains_hyperlink {
+ break;
+ }
+
+ match_start.line = next_line;
+ }
+
+ // Ditto for the end.
+ while match_end.line < viewport_end {
+ let next_line = match_end.line + 1i32;
+
+ let line_contains_hyperlink = grid[next_line]
+ .into_iter()
+ .any(|cell| cell.hyperlink().map(|h| h == hyperlink).unwrap_or(false));
+
+ if !line_contains_hyperlink {
+ break;
+ }
+
+ match_end.line = next_line;
+ }
+
+ Some((hyperlink, match_start..=match_end))
}
/// Iterator over all post-processed matches inside an existing hint match.
diff --git a/alacritty/src/display/mod.rs b/alacritty/src/display/mod.rs
index 99df511f..f1f51ec7 100644
--- a/alacritty/src/display/mod.rs
+++ b/alacritty/src/display/mod.rs
@@ -16,7 +16,6 @@ use glutin::Rect as DamageRect;
use log::{debug, info};
use parking_lot::MutexGuard;
use serde::{Deserialize, Serialize};
-use unicode_width::UnicodeWidthChar;
#[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))]
use wayland_client::EventQueue;
@@ -49,6 +48,7 @@ use crate::event::{Mouse, SearchState};
use crate::message_bar::{MessageBuffer, MessageType};
use crate::renderer::rects::{RenderLines, RenderRect};
use crate::renderer::{self, GlyphCache, Renderer};
+use crate::string::{ShortenDirection, StrShortener};
pub mod content;
pub mod cursor;
@@ -60,15 +60,15 @@ mod color;
mod damage;
mod meter;
-/// Maximum number of linewraps followed outside of the viewport during search highlighting.
-pub const MAX_SEARCH_LINES: usize = 100;
-
/// Label for the forward terminal search bar.
const FORWARD_SEARCH_LABEL: &str = "Search: ";
/// Label for the backward terminal search bar.
const BACKWARD_SEARCH_LABEL: &str = "Backward Search: ";
+/// The character used to shorten the visible text like uri preview or search regex.
+const SHORTENER: char = '…';
+
/// Color which is used to highlight damaged rects when debugging.
const DAMAGE_RECT_COLOR: Rgb = Rgb { r: 255, g: 0, b: 255 };
@@ -362,9 +362,13 @@ pub struct Display {
/// The renderer update that takes place only once before the actual rendering.
pub pending_renderer_update: Option<RendererUpdate>,
+ // Mouse point position when highlighting hints.
+ hint_mouse_point: Option<Point>,
+
is_damage_supported: bool,
debug_damage: bool,
damage_rects: Vec<DamageRect>,
+ next_frame_damage_rects: Vec<DamageRect>,
renderer: Renderer,
glyph_cache: GlyphCache,
meter: Meter,
@@ -515,10 +519,11 @@ impl Display {
let hint_state = HintState::new(config.hints.alphabet());
let is_damage_supported = window.swap_buffers_with_damage_supported();
let debug_damage = config.debug.highlight_damage;
- let damage_rects = if is_damage_supported || debug_damage {
- Vec::with_capacity(size_info.screen_lines())
+ let (damage_rects, next_frame_damage_rects) = if is_damage_supported || debug_damage {
+ let vec = Vec::with_capacity(size_info.screen_lines());
+ (vec.clone(), vec)
} else {
- Vec::new()
+ (Vec::new(), Vec::new())
};
Ok(Self {
@@ -540,6 +545,8 @@ impl Display {
is_damage_supported,
debug_damage,
damage_rects,
+ next_frame_damage_rects,
+ hint_mouse_point: None,
})
}
@@ -757,7 +764,7 @@ impl Display {
let size_info = self.size_info;
let vi_mode = terminal.mode().contains(TermMode::VI);
- let vi_mode_cursor = if vi_mode { Some(terminal.vi_mode_cursor) } else { None };
+ let vi_cursor_point = if vi_mode { Some(terminal.vi_mode_cursor.point) } else { None };
if self.collect_damage() {
self.update_damage(&mut terminal, selection_range, search_state);
@@ -772,6 +779,10 @@ impl Display {
self.renderer.clear(background_color, config.window_opacity());
let mut lines = RenderLines::new();
+ // Optimize loop hint comparator.
+ let has_highlighted_hint =
+ self.highlighted_hint.is_some() || self.vi_highlighted_hint.is_some();
+
// Draw grid.
{
let _sampler = self.meter.sampler();
@@ -783,16 +794,26 @@ impl Display {
let glyph_cache = &mut self.glyph_cache;
let highlighted_hint = &self.highlighted_hint;
let vi_highlighted_hint = &self.vi_highlighted_hint;
+
self.renderer.draw_cells(
&size_info,
glyph_cache,
grid_cells.into_iter().map(|mut cell| {
// Underline hints hovered by mouse or vi mode cursor.
let point = term::viewport_to_point(display_offset, cell.point);
- if highlighted_hint.as_ref().map_or(false, |h| h.bounds.contains(&point))
- || vi_highlighted_hint.as_ref().map_or(false, |h| h.bounds.contains(&point))
- {
- cell.flags.insert(Flags::UNDERLINE);
+
+ if has_highlighted_hint {
+ let hyperlink =
+ cell.extra.as_ref().and_then(|extra| extra.hyperlink.as_ref());
+ if highlighted_hint
+ .as_ref()
+ .map_or(false, |hint| hint.should_highlight(point, hyperlink))
+ || vi_highlighted_hint
+ .as_ref()
+ .map_or(false, |hint| hint.should_highlight(point, hyperlink))
+ {
+ cell.flags.insert(Flags::UNDERLINE);
+ }
}
// Update underline/strikeout.
@@ -805,18 +826,17 @@ impl Display {
let mut rects = lines.rects(&metrics, &size_info);
- if let Some(vi_mode_cursor) = vi_mode_cursor {
+ if let Some(vi_cursor_point) = vi_cursor_point {
// Indicate vi mode by showing the cursor's position in the top right corner.
- let vi_point = vi_mode_cursor.point;
- let line = (-vi_point.line.0 + size_info.bottommost_line().0) as usize;
- let obstructed_column = Some(vi_point)
+ let line = (-vi_cursor_point.line.0 + size_info.bottommost_line().0) as usize;
+ let obstructed_column = Some(vi_cursor_point)
.filter(|point| point.line == -(display_offset as i32))
.map(|point| point.column);
- self.draw_line_indicator(config, &size_info, total_lines, obstructed_column, line);
+ self.draw_line_indicator(config, total_lines, obstructed_column, line);
} else if search_state.regex().is_some() {
// Show current display offset in vi-less search to indicate match position.
- self.draw_line_indicator(config, &size_info, total_lines, None, display_offset);
- }
+ self.draw_line_indicator(config, total_lines, None, display_offset);
+ };
// Draw cursor.
for rect in cursor.rects(&size_info, config.terminal_config.cursor.thickness()) {
@@ -868,14 +888,21 @@ impl Display {
let fg = config.colors.primary.background;
for (i, message_text) in text.iter().enumerate() {
let point = Point::new(start_line + i, Column(0));
- self.renderer.draw_string(point, fg, bg, message_text, &size_info, glyph_cache);
+ self.renderer.draw_string(
+ point,
+ fg,
+ bg,
+ message_text.chars(),
+ &size_info,
+ glyph_cache,
+ );
}
} else {
// Draw rectangles.
self.renderer.draw_rects(&size_info, &metrics, rects);
}
- self.draw_render_timer(config, &size_info);
+ self.draw_render_timer(config);
// Handle search and IME positioning.
let ime_position = match search_state.regex() {
@@ -885,10 +912,10 @@ impl Display {
Direction::Left => BACKWARD_SEARCH_LABEL,
};
- let search_text = Self::format_search(&size_info, regex, search_label);
+ let search_text = Self::format_search(regex, search_label, size_info.columns());
// Render the search bar.
- self.draw_search(config, &size_info, &search_text);
+ self.draw_search(config, &search_text);
// Compute IME position.
let line = Line(size_info.screen_lines() as i32 + 1);
@@ -897,6 +924,11 @@ impl Display {
None => cursor_point,
};
+ // Draw hyperlink uri preview.
+ if has_highlighted_hint {
+ self.draw_hyperlink_preview(config, vi_cursor_point, display_offset);
+ }
+
// Update IME position.
self.window.update_ime_position(ime_position, &self.size_info);
@@ -921,6 +953,9 @@ impl Display {
}
self.damage_rects.clear();
+
+ // Append damage rects we've enqueued for the next frame.
+ mem::swap(&mut self.damage_rects, &mut self.next_frame_damage_rects);
}
/// Update to a new configuration.
@@ -964,8 +999,13 @@ impl Display {
// Update cursor shape.
if highlighted_hint.is_some() {
+ // If mouse changed the line, we should update the hyperlink preview, since the
+ // highlighted hint could be disrupted by the old preview.
+ dirty = self.hint_mouse_point.map(|p| p.line != point.line).unwrap_or(false);
+ self.hint_mouse_point = Some(point);
self.window.set_mouse_cursor(CursorIcon::Hand);
} else if self.highlighted_hint.is_some() {
+ self.hint_mouse_point = None;
if term.mode().intersects(TermMode::MOUSE_MODE) && !term.mode().contains(TermMode::VI) {
self.window.set_mouse_cursor(CursorIcon::Default);
} else {
@@ -980,74 +1020,148 @@ impl Display {
}
/// Format search regex to account for the cursor and fullwidth characters.
- fn format_search(size_info: &SizeInfo, search_regex: &str, search_label: &str) -> String {
- // Add spacers for wide chars.
- let mut formatted_regex = String::with_capacity(search_regex.len());
- for c in search_regex.chars() {
- formatted_regex.push(c);
- if c.width() == Some(2) {
- formatted_regex.push(' ');
- }
+ fn format_search(search_regex: &str, search_label: &str, max_width: usize) -> String {
+ let label_len = search_label.len();
+
+ // Skip `search_regex` formatting if only label is visible.
+ if label_len > max_width {
+ return search_label[..max_width].to_owned();
}
- // Add cursor to show whitespace.
- formatted_regex.push('_');
+ // The search string consists of `search_label` + `search_regex` + `cursor`.
+ let mut bar_text = String::from(search_label);
+ bar_text.extend(StrShortener::new(
+ search_regex,
+ max_width.wrapping_sub(label_len + 1),
+ ShortenDirection::Left,
+ Some(SHORTENER),
+ ));
- // Truncate beginning of the search regex if it exceeds the viewport width.
- let num_cols = size_info.columns();
- let label_len = search_label.chars().count();
- let regex_len = formatted_regex.chars().count();
- let truncate_len = cmp::min((regex_len + label_len).saturating_sub(num_cols), regex_len);
- let index = formatted_regex.char_indices().nth(truncate_len).map(|(i, _c)| i).unwrap_or(0);
- let truncated_regex = &formatted_regex[index..];
+ bar_text.push('_');
- // Add search label to the beginning of the search regex.
- let mut bar_text = format!("{}{}", search_label, truncated_regex);
+ bar_text
+ }
- // Make sure the label alone doesn't exceed the viewport width.
- bar_text.truncate(num_cols);
+ /// Draw preview for the currently highlighted `Hyperlink`.
+ #[inline(never)]
+ fn draw_hyperlink_preview(
+ &mut self,
+ config: &UiConfig,
+ vi_cursor_point: Option<Point>,
+ display_offset: usize,
+ ) {
+ let num_cols = self.size_info.columns();
+ let uris: Vec<_> = self
+ .highlighted_hint
+ .iter()
+ .chain(&self.vi_highlighted_hint)
+ .filter_map(|hint| hint.hyperlink().map(|hyperlink| hyperlink.uri()))
+ .map(|uri| StrShortener::new(uri, num_cols, ShortenDirection::Right, Some(SHORTENER)))
+ .collect();
+
+ if uris.is_empty() {
+ return;
+ }
- bar_text
+ // The maximum amount of protected lines including the ones we'll show preview on.
+ let max_protected_lines = uris.len() * 2;
+
+ // Lines we shouldn't shouldn't show preview on, because it'll obscure the highlighted
+ // hint.
+ let mut protected_lines = Vec::with_capacity(max_protected_lines);
+ if self.size_info.screen_lines() >= max_protected_lines {
+ // Prefer to show preview even when it'll likely obscure the highlighted hint, when
+ // there's no place left for it.
+ protected_lines.push(self.hint_mouse_point.map(|point| point.line));
+ protected_lines.push(vi_cursor_point.map(|point| point.line));
+ }
+
+ // Find the line in viewport we can draw preview on without obscuring protected lines.
+ let viewport_bottom = self.size_info.bottommost_line() - Line(display_offset as i32);
+ let viewport_top = viewport_bottom - (self.size_info.screen_lines() - 1);
+ let uri_lines = (viewport_top.0..=viewport_bottom.0)
+ .rev()
+ .map(|line| Some(Line(line)))
+ .filter_map(|line| {
+ if protected_lines.contains(&line) {
+ None
+ } else {
+ protected_lines.push(line);
+ line
+ }
+ })
+ .take(uris.len())
+ .flat_map(|line| term::point_to_viewport(display_offset, Point::new(line, Column(0))));
+
+ let fg = config.colors.footer_bar_foreground();
+ let bg = config.colors.footer_bar_background();
+ for (uri, point) in uris.into_iter().zip(uri_lines) {
+ // Damage the uri preview.
+ if self.collect_damage() {
+ let uri_preview_damage = self.damage_from_point(point, num_cols as u32);
+ self.damage_rects.push(uri_preview_damage);
+
+ // Damage the uri preview for the next frame as well.
+ self.next_frame_damage_rects.push(uri_preview_damage);
+ }
+
+ self.renderer.draw_string(point, fg, bg, uri, &self.size_info, &mut self.glyph_cache);
+ }
}
/// Draw current search regex.
- fn draw_search(&mut self, config: &UiConfig, size_info: &SizeInfo, text: &str) {
- let glyph_cache = &mut self.glyph_cache;
- let num_cols = size_info.columns();
-
+ #[inline(never)]
+ fn draw_search(&mut self, config: &UiConfig, text: &str) {
// Assure text length is at least num_cols.
+ let num_cols = self.size_info.columns();
let text = format!("{:<1$}", text, num_cols);
- let point = Point::new(size_info.screen_lines(), Column(0));
- let fg = config.colors.search_bar_foreground();
- let bg = config.colors.search_bar_background();
+ let point = Point::new(self.size_info.screen_lines(), Column(0));
- self.renderer.draw_string(point, fg, bg, &text, size_info, glyph_cache);
+ let fg = config.colors.footer_bar_foreground();
+ let bg = config.colors.footer_bar_background();
+
+ self.renderer.draw_string(
+ point,
+ fg,
+ bg,
+ text.chars(),
+ &self.size_info,
+ &mut self.glyph_cache,
+ );
}
/// Draw render timer.
- fn draw_render_timer(&mut self, config: &UiConfig, size_info: &SizeInfo) {
+ #[inline(never)]
+ fn draw_render_timer(&mut self, config: &UiConfig) {
if !config.debug.render_timer {
return;
}
let timing = format!("{:.3} usec", self.meter.average());
- let point = Point::new(size_info.screen_lines().saturating_sub(2), Column(0));
+ let point = Point::new(self.size_info.screen_lines().saturating_sub(2), Column(0));
let fg = config.colors.primary.background;
let bg = config.colors.normal.red;
- // Damage the entire line.
- self.damage_from_point(point, self.size_info.columns() as u32);
+ if self.collect_damage() {
+ // Damage the entire line.
+ let render_timer_damage =
+ self.damage_from_point(point, self.size_info.columns() as u32);
+ self.damage_rects.push(render_timer_damage);
+
+ // Damage the render timer for the next frame.
+ self.next_frame_damage_rects.push(render_timer_damage)
+ }
let glyph_cache = &mut self.glyph_cache;
- self.renderer.draw_string(point, fg, bg, &timing, size_info, glyph_cache);
+ self.renderer.draw_string(point, fg, bg, timing.chars(), &self.size_info, glyph_cache);
}
/// Draw an indicator for the position of a line in history.
+ #[inline(never)]
fn draw_line_indicator(
&mut self,
config: &UiConfig,
- size_info: &SizeInfo,
total_lines: usize,
obstructed_column: Option<Column>,
line: usize,
@@ -1064,14 +1178,16 @@ impl Display {
}
let text = format!("[{}/{}]", line, total_lines - 1);
- let column = Column(size_info.columns().saturating_sub(text.len()));
+ let column = Column(self.size_info.columns().saturating_sub(text.len()));
let point = Point::new(0, column);
// Damage the maximum possible length of the format text, which could be achieved when
// using `MAX_SCROLLBACK_LINES` as current and total lines adding a `3` for formatting.
const MAX_SIZE: usize = 2 * num_digits(MAX_SCROLLBACK_LINES) + 3;
- let damage_point = Point::new(0, Column(size_info.columns().saturating_sub(MAX_SIZE)));
- self.damage_from_point(damage_point, MAX_SIZE as u32);
+ let damage_point = Point::new(0, Column(self.size_info.columns().saturating_sub(MAX_SIZE)));
+ if self.collect_damage() {
+ self.damage_rects.push(self.damage_from_point(damage_point, MAX_SIZE as u32));
+ }
let colors = &config.colors;
let fg = colors.line_indicator.foreground.unwrap_or(colors.primary.background);
@@ -1080,23 +1196,20 @@ impl Display {
// Do not render anything if it would obscure the vi mode cursor.
if obstructed_column.map_or(true, |obstructed_column| obstructed_column < column) {
let glyph_cache = &mut self.glyph_cache;
- self.renderer.draw_string(point, fg, bg, &text, size_info, glyph_cache);
+ self.renderer.draw_string(point, fg, bg, text.chars(), &self.size_info, glyph_cache);
}
}
/// Damage `len` starting from a `point`.
- #[inline]
- fn damage_from_point(&mut self, point: Point<usize>, len: u32) {
- if !self.collect_damage() {
- return;
- }
-
+ ///
+ /// This method also enqueues damage for the next frame automatically.
+ fn damage_from_point(&self, point: Point<usize>, len: u32) -> DamageRect {
let size_info: SizeInfo<u32> = self.size_info.into();
let x = size_info.padding_x() + point.column.0 as u32 * size_info.cell_width();
let y_top = size_info.height() - size_info.padding_y();
let y = y_top - (point.line as u32 + 1) * size_info.cell_height();
let width = len as u32 * size_info.cell_width();
- self.damage_rects.push(DamageRect { x, y, width, height: size_info.cell_height() })
+ DamageRect { x, y, width, height: size_info.cell_height() }
}
/// Damage currently highlighted `Display` hints.
@@ -1105,10 +1218,12 @@ impl Display {
let display_offset = terminal.grid().display_offset();
let last_visible_line = terminal.screen_lines() - 1;
for hint in self.highlighted_hint.iter().chain(&self.vi_highlighted_hint) {
- for point in (hint.bounds.start().line.0..=hint.bounds.end().line.0).flat_map(|line| {
- term::point_to_viewport(display_offset, Point::new(Line(line), Column(0)))
- .filter(|point| point.line <= last_visible_line)
- }) {
+ for point in
+ (hint.bounds().start().line.0..=hint.bounds().end().line.0).flat_map(|line| {
+ term::point_to_viewport(display_offset, Point::new(Line(line), Column(0)))
+ .filter(|point| point.line <= last_visible_line)
+ })
+ {
terminal.damage_line(point.line, 0, terminal.columns() - 1);
}
}