diff options
| author | Ayose <ayosec@gmail.com> | 2021-04-18 22:45:19 +0100 |
|---|---|---|
| committer | Ayose <ayosec@gmail.com> | 2021-04-18 22:45:19 +0100 |
| commit | e5e9c8293535ea6eaaa3d017cbfb322c7ac95c14 (patch) | |
| tree | b805d27d397246e3684224e7aac0b833d763e7b6 /alacritty/src | |
| parent | fd0218f2545762c0d13dc17a55a1b6a334e31c19 (diff) | |
| parent | 28abb1f9c78ab316126bdf94e2ca12f034f1d8fd (diff) | |
| download | r-alacritty-e5e9c8293535ea6eaaa3d017cbfb322c7ac95c14.tar.gz r-alacritty-e5e9c8293535ea6eaaa3d017cbfb322c7ac95c14.tar.bz2 r-alacritty-e5e9c8293535ea6eaaa3d017cbfb322c7ac95c14.zip | |
Merge remote-tracking branch 'vendor/master' into graphics
Diffstat (limited to 'alacritty/src')
| -rw-r--r-- | alacritty/src/config/bindings.rs | 7 | ||||
| -rw-r--r-- | alacritty/src/config/mouse.rs | 38 | ||||
| -rw-r--r-- | alacritty/src/config/ui_config.rs | 101 | ||||
| -rw-r--r-- | alacritty/src/display/content.rs | 24 | ||||
| -rw-r--r-- | alacritty/src/display/hint.rs | 188 | ||||
| -rw-r--r-- | alacritty/src/display/mod.rs | 122 | ||||
| -rw-r--r-- | alacritty/src/event.rs | 169 | ||||
| -rw-r--r-- | alacritty/src/input.rs | 288 | ||||
| -rw-r--r-- | alacritty/src/main.rs | 1 |
9 files changed, 565 insertions, 373 deletions
diff --git a/alacritty/src/config/bindings.rs b/alacritty/src/config/bindings.rs index 732875db..4e7c2fcb 100644 --- a/alacritty/src/config/bindings.rs +++ b/alacritty/src/config/bindings.rs @@ -161,6 +161,10 @@ pub enum Action { /// Hide the Alacritty window. Hide, + /// Hide all windows other than Alacritty on macOS. + #[cfg(target_os = "macos")] + HideOtherApplications, + /// Minimize the Alacritty window. Minimize, @@ -685,6 +689,7 @@ pub fn platform_key_bindings() -> Vec<KeyBinding> { C, ModifiersState::LOGO; Action::Copy; C, ModifiersState::LOGO, +BindingMode::VI, ~BindingMode::SEARCH; Action::ClearSelection; H, ModifiersState::LOGO; Action::Hide; + H, ModifiersState::LOGO | ModifiersState::ALT; Action::HideOtherApplications; M, ModifiersState::LOGO; Action::Minimize; Q, ModifiersState::LOGO; Action::Quit; W, ModifiersState::LOGO; Action::Quit; @@ -1157,7 +1162,7 @@ impl<'a> de::Deserialize<'a> for ModsWrapper { type Value = ModsWrapper; fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str("a subset of Shift|Control|Super|Command|Alt|Option") + f.write_str("None or a subset of Shift|Control|Super|Command|Alt|Option") } fn visit_str<E>(self, value: &str) -> Result<ModsWrapper, E> diff --git a/alacritty/src/config/mouse.rs b/alacritty/src/config/mouse.rs index 2aa7557c..aed1ab04 100644 --- a/alacritty/src/config/mouse.rs +++ b/alacritty/src/config/mouse.rs @@ -1,50 +1,12 @@ use std::time::Duration; -use glutin::event::ModifiersState; - use alacritty_config_derive::ConfigDeserialize; -use alacritty_terminal::config::Program; - -use crate::config::bindings::ModsWrapper; #[derive(ConfigDeserialize, Default, Clone, Debug, PartialEq, Eq)] pub struct Mouse { pub double_click: ClickHandler, pub triple_click: ClickHandler, pub hide_when_typing: bool, - pub url: Url, -} - -#[derive(ConfigDeserialize, Clone, Debug, PartialEq, Eq)] -pub struct Url { - /// Program for opening links. - pub launcher: Option<Program>, - - /// Modifier used to open links. - modifiers: ModsWrapper, -} - -impl Url { - pub fn mods(&self) -> ModifiersState { - self.modifiers.into_inner() - } -} - -impl Default for Url { - fn default() -> Url { - Url { - #[cfg(not(any(target_os = "macos", windows)))] - launcher: Some(Program::Just(String::from("xdg-open"))), - #[cfg(target_os = "macos")] - launcher: Some(Program::Just(String::from("open"))), - #[cfg(windows)] - launcher: Some(Program::WithArgs { - program: String::from("cmd"), - args: vec!["/c".to_string(), "start".to_string(), "".to_string()], - }), - modifiers: Default::default(), - } - } } #[derive(ConfigDeserialize, Clone, Debug, PartialEq, Eq)] diff --git a/alacritty/src/config/ui_config.rs b/alacritty/src/config/ui_config.rs index 3cd2ad88..135d12c8 100644 --- a/alacritty/src/config/ui_config.rs +++ b/alacritty/src/config/ui_config.rs @@ -21,6 +21,11 @@ use crate::config::font::Font; use crate::config::mouse::Mouse; use crate::config::window::WindowConfig; +/// Regex used for the default URL hint. +#[rustfmt::skip] +const URL_REGEX: &str = "(mailto:|gemini:|gopher:|https:|http:|news:|file:|git:|ssh:|ftp:)\ + [^\u{0000}-\u{001F}\u{007F}-\u{009F}<>\" {-}\\^⟨⟩`]+"; + #[derive(ConfigDeserialize, Debug, PartialEq)] pub struct UiConfig { /// Font configuration. @@ -90,13 +95,18 @@ impl Default for UiConfig { impl UiConfig { /// Generate key bindings for all keyboard hints. pub fn generate_hint_bindings(&mut self) { - for hint in self.hints.enabled.drain(..) { + for hint in &self.hints.enabled { + let binding = match hint.binding { + Some(binding) => binding, + None => continue, + }; + let binding = KeyBinding { - trigger: hint.binding.key, - mods: hint.binding.mods.0, + trigger: binding.key, + mods: binding.mods.0, mode: BindingMode::empty(), notmode: BindingMode::empty(), - action: Action::Hint(hint), + action: Action::Hint(hint.clone()), }; self.key_bindings.0.push(binding); @@ -197,13 +207,42 @@ pub struct Delta<T: Default> { } /// Regex terminal hints. -#[derive(ConfigDeserialize, Default, Debug, PartialEq, Eq)] +#[derive(ConfigDeserialize, Debug, PartialEq, Eq)] pub struct Hints { /// Characters for the hint labels. alphabet: HintsAlphabet, /// All configured terminal hints. - enabled: Vec<Hint>, + pub enabled: Vec<Hint>, +} + +impl Default for Hints { + fn default() -> Self { + // Add URL hint by default when no other hint is present. + let pattern = LazyRegexVariant::Pattern(String::from(URL_REGEX)); + let regex = LazyRegex(Rc::new(RefCell::new(pattern))); + + #[cfg(not(any(target_os = "macos", windows)))] + let action = HintAction::Command(Program::Just(String::from("xdg-open"))); + #[cfg(target_os = "macos")] + let action = HintAction::Command(Program::Just(String::from("open"))); + #[cfg(windows)] + let action = HintAction::Command(Program::WithArgs { + program: String::from("cmd"), + args: vec!["/c".to_string(), "start".to_string(), "".to_string()], + }); + + Self { + enabled: vec![Hint { + regex, + action, + post_processing: true, + mouse: Some(HintMouse { enabled: true, mods: Default::default() }), + binding: Default::default(), + }], + alphabet: Default::default(), + } + } } impl Hints { @@ -254,6 +293,8 @@ pub enum HintInternalAction { Paste, /// Select the text matching the hint. Select, + /// Move the vi mode cursor to the beginning of the hint. + MoveViModeCursor, } /// Actions for hint bindings. @@ -271,33 +312,51 @@ pub enum HintAction { /// Hint configuration. #[derive(Deserialize, Clone, Debug, PartialEq, Eq)] pub struct Hint { + /// Regex for finding matches. + pub regex: LazyRegex, + /// Action executed when this hint is triggered. #[serde(flatten)] pub action: HintAction, - /// Regex for finding matches. - pub regex: LazyRegex, + /// Hint text post processing. + #[serde(default)] + pub post_processing: bool, + + /// Hint mouse highlighting. + pub mouse: Option<HintMouse>, /// Binding required to search for this hint. - binding: HintBinding, + binding: Option<HintBinding>, } /// Binding for triggering a keyboard hint. #[derive(Deserialize, Copy, Clone, Debug, PartialEq, Eq)] pub struct HintBinding { pub key: Key, + #[serde(default)] + pub mods: ModsWrapper, +} + +/// Hint mouse highlighting. +#[derive(ConfigDeserialize, Default, Copy, Clone, Debug, PartialEq, Eq)] +pub struct HintMouse { + /// Hint mouse highlighting availability. + pub enabled: bool, + + /// Required mouse modifiers for hint highlighting. pub mods: ModsWrapper, } /// Lazy regex with interior mutability. -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq, Eq)] pub struct LazyRegex(Rc<RefCell<LazyRegexVariant>>); impl LazyRegex { /// Execute a function with the compiled regex DFAs as parameter. - pub fn with_compiled<T, F>(&self, f: F) -> T + pub fn with_compiled<T, F>(&self, mut f: F) -> T where - F: Fn(&RegexSearch) -> T, + F: FnMut(&RegexSearch) -> T, { f(self.0.borrow_mut().compiled()) } @@ -313,14 +372,6 @@ impl<'de> Deserialize<'de> for LazyRegex { } } -/// Implement placeholder to allow derive upstream, since we never need it for this struct itself. -impl PartialEq for LazyRegex { - fn eq(&self, _other: &Self) -> bool { - false - } -} -impl Eq for LazyRegex {} - /// Regex which is compiled on demand, to avoid expensive computations at startup. #[derive(Clone, Debug)] pub enum LazyRegexVariant { @@ -357,3 +408,13 @@ impl LazyRegexVariant { } } } + +impl PartialEq for LazyRegexVariant { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (Self::Pattern(regex), Self::Pattern(other_regex)) => regex == other_regex, + _ => false, + } + } +} +impl Eq for LazyRegexVariant {} diff --git a/alacritty/src/display/content.rs b/alacritty/src/display/content.rs index ae4bdd26..7b412c1d 100644 --- a/alacritty/src/display/content.rs +++ b/alacritty/src/display/content.rs @@ -19,15 +19,12 @@ use alacritty_terminal::term::{ use crate::config::ui_config::UiConfig; use crate::display::color::{List, DIM_FACTOR}; use crate::display::hint::HintState; -use crate::display::Display; +use crate::display::{self, Display, MAX_SEARCH_LINES}; use crate::event::SearchState; /// Minimum contrast between a fixed cursor color and the cell's background. pub const MIN_CURSOR_CONTRAST: f64 = 1.5; -/// Maximum number of linewraps followed outside of the viewport during search highlighting. -const MAX_SEARCH_LINES: usize = 100; - /// Renderable terminal content. /// /// This provides the terminal cursor and an iterator over all non-empty cells. @@ -139,8 +136,8 @@ impl<'a> RenderableContent<'a> { // Convert cursor point to viewport position. let cursor_point = self.terminal_cursor.point; - let line = (cursor_point.line + self.terminal_content.display_offset as i32).0 as usize; - let point = Point::new(line, cursor_point.column); + let display_offset = self.terminal_content.display_offset; + let point = display::point_to_viewport(display_offset, cursor_point).unwrap(); Some(RenderableCursor { shape: self.terminal_cursor.shape, @@ -221,10 +218,12 @@ impl RenderableCell { .selection .map_or(false, |selection| selection.contains_cell(&cell, content.terminal_cursor)); + let display_offset = content.terminal_content.display_offset; + let viewport_start = Point::new(Line(-(display_offset as i32)), Column(0)); + let colors = &content.config.ui_config.colors; let mut character = cell.c; - let colors = &content.config.ui_config.colors; - if let Some((c, is_first)) = content.hint.advance(cell.point) { + if let Some((c, is_first)) = content.hint.advance(viewport_start, cell.point) { let (config_fg, config_bg) = if is_first { (colors.hints.start.foreground, colors.hints.start.background) } else { @@ -260,8 +259,7 @@ impl RenderableCell { // Convert cell point to viewport position. let cell_point = cell.point; - let line = (cell_point.line + content.terminal_content.display_offset as i32).0 as usize; - let point = Point::new(line, cell_point.column); + let point = display::point_to_viewport(display_offset, cell_point).unwrap(); RenderableCell { zerowidth: cell.zerowidth().map(|zerowidth| zerowidth.to_vec()), @@ -412,7 +410,7 @@ impl<'a> Hint<'a> { /// this position will be returned. /// /// The tuple's [`bool`] will be `true` when the character is the first for this hint. - fn advance(&mut self, point: Point) -> Option<(char, bool)> { + 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) { return None; @@ -423,7 +421,7 @@ impl<'a> Hint<'a> { .regex .matches .get(self.regex.index) - .map(|regex_match| regex_match.start()) + .map(|regex_match| max(*regex_match.start(), viewport_start)) .filter(|start| start.line == point.line)?; // Position within the hint label. @@ -444,7 +442,7 @@ impl<'a> From<&'a HintState> for Hint<'a> { /// Wrapper for finding visible regex matches. #[derive(Default, Clone)] -pub struct RegexMatches(Vec<RangeInclusive<Point>>); +pub struct RegexMatches(pub Vec<RangeInclusive<Point>>); impl RegexMatches { /// Find all visible matches. diff --git a/alacritty/src/display/hint.rs b/alacritty/src/display/hint.rs index 2a5e9c65..6ab68f10 100644 --- a/alacritty/src/display/hint.rs +++ b/alacritty/src/display/hint.rs @@ -1,8 +1,16 @@ -use alacritty_terminal::term::search::Match; -use alacritty_terminal::term::Term; +use std::cmp::{max, min}; + +use glutin::event::ModifiersState; + +use alacritty_terminal::grid::BidirectionalIterator; +use alacritty_terminal::index::{Boundary, Point}; +use alacritty_terminal::term::search::{Match, RegexSearch}; +use alacritty_terminal::term::{Term, TermMode}; use crate::config::ui_config::{Hint, HintAction}; +use crate::config::Config; use crate::display::content::RegexMatches; +use crate::display::MAX_SEARCH_LINES; /// Percentage of characters in the hints alphabet used for the last character. const HINT_SPLIT_PERCENTAGE: f32 = 0.5; @@ -63,7 +71,20 @@ impl HintState { }; // Find visible matches. - self.matches = hint.regex.with_compiled(|regex| RegexMatches::new(term, regex)); + self.matches.0 = hint.regex.with_compiled(|regex| { + let mut matches = RegexMatches::new(term, regex); + + // Apply post-processing and search for sub-matches if necessary. + if hint.post_processing { + matches + .drain(..) + .map(|rm| HintPostProcessor::new(term, regex, rm).collect::<Vec<_>>()) + .flatten() + .collect() + } else { + matches.0 + } + }); // Cancel highlight with no visible matches. if self.matches.is_empty() { @@ -144,6 +165,7 @@ impl HintState { } /// Hint match which was selected by the user. +#[derive(Clone)] pub struct HintMatch { /// Action for handling the text. pub action: HintAction, @@ -217,6 +239,166 @@ impl HintLabels { } } +/// Check if there is a hint highlighted at the specified point. +pub fn highlighted_at<T>( + term: &Term<T>, + config: &Config, + point: Point, + mouse_mods: ModifiersState, +) -> Option<HintMatch> { + let mouse_mode = term.mode().intersects(TermMode::MOUSE_MODE); + + config.ui_config.hints.enabled.iter().find_map(|hint| { + // Check if all required modifiers are pressed. + let highlight = hint.mouse.map_or(false, |mouse| { + mouse.enabled + && mouse_mods.contains(mouse.mods.0) + && (!mouse_mode || mouse_mods.contains(ModifiersState::SHIFT)) + }); + if !highlight { + 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); + + // Function to verify if the specified point is inside the match. + let at_point = |rm: &Match| *rm.start() <= point && *rm.end() >= point; + + // Check if there's any match at the specified point. + let regex_match = term.regex_search_right(regex, start, end).filter(at_point)?; + + // 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) + }; + + regex_match.map(|bounds| HintMatch { action: hint.action.clone(), bounds }) + }) + }) +} + +/// Iterator over all post-processed matches inside an existing hint match. +struct HintPostProcessor<'a, T> { + /// Regex search DFAs. + regex: &'a RegexSearch, + + /// Terminal reference. + term: &'a Term<T>, + + /// Next hint match in the iterator. + next_match: Option<Match>, + + /// Start point for the next search. + start: Point, + + /// End point for the hint match iterator. + end: Point, +} + +impl<'a, T> HintPostProcessor<'a, T> { + /// Create a new iterator for an unprocessed match. + fn new(term: &'a Term<T>, regex: &'a RegexSearch, regex_match: Match) -> Self { + let end = *regex_match.end(); + let mut post_processor = Self { next_match: None, start: end, end, term, regex }; + + // Post-process the first hint match. + let next_match = post_processor.hint_post_processing(®ex_match); + post_processor.start = next_match.end().add(term, Boundary::Grid, 1); + post_processor.next_match = Some(next_match); + + post_processor + } + + /// Apply some hint post processing heuristics. + /// + /// This will check the end of the hint and make it shorter if certain characters are determined + /// to be unlikely to be intentionally part of the hint. + /// + /// This is most useful for identifying URLs appropriately. + fn hint_post_processing(&self, regex_match: &Match) -> Match { + let mut iter = self.term.grid().iter_from(*regex_match.start()); + + let mut c = iter.cell().c; + + // Truncate uneven number of brackets. + let end = *regex_match.end(); + let mut open_parents = 0; + let mut open_brackets = 0; + loop { + match c { + '(' => open_parents += 1, + '[' => open_brackets += 1, + ')' => { + if open_parents == 0 { + iter.prev(); + break; + } else { + open_parents -= 1; + } + }, + ']' => { + if open_brackets == 0 { + iter.prev(); + break; + } else { + open_brackets -= 1; + } + }, + _ => (), + } + + if iter.point() == end { + break; + } + + match iter.next() { + Some(indexed) => c = indexed.cell.c, + None => break, + } + } + + // Truncate trailing characters which are likely to be delimiters. + let start = *regex_match.start(); + while iter.point() != start { + if !matches!(c, '.' | ',' | ':' | ';' | '?' | '!' | '(' | '[' | '\'') { + break; + } + + match iter.prev() { + Some(indexed) => c = indexed.cell.c, + None => break, + } + } + + start..=iter.point() + } +} + +impl<'a, T> Iterator for HintPostProcessor<'a, T> { + type Item = Match; + + fn next(&mut self) -> Option<Self::Item> { + let next_match = self.next_match.take()?; + + if self.start <= self.end { + if let Some(rm) = self.term.regex_search_right(self.regex, self.start, self.end) { + let regex_match = self.hint_post_processing(&rm); + self.start = regex_match.end().add(self.term, Boundary::Grid, 1); + self.next_match = Some(regex_match); + } + } + + Some(next_match) + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/alacritty/src/display/mod.rs b/alacritty/src/display/mod.rs index 2c74920d..f6b55e93 100644 --- a/alacritty/src/display/mod.rs +++ b/alacritty/src/display/mod.rs @@ -2,6 +2,7 @@ //! GPU drawing. use std::cmp::min; +use std::convert::TryFrom; use std::f64; use std::fmt::{self, Formatter}; #[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))] @@ -27,6 +28,7 @@ use alacritty_terminal::event::{EventListener, OnResize}; use alacritty_terminal::grid::Dimensions as _; use alacritty_terminal::index::{Column, Direction, Line, Point}; use alacritty_terminal::selection::Selection; +use alacritty_terminal::term::cell::Flags; use alacritty_terminal::term::{SizeInfo, Term, TermMode, MIN_COLUMNS, MIN_SCREEN_LINES}; use crate::config::font::Font; @@ -38,14 +40,13 @@ use crate::display::bell::VisualBell; use crate::display::color::List; use crate::display::content::RenderableContent; use crate::display::cursor::IntoRects; -use crate::display::hint::HintState; +use crate::display::hint::{HintMatch, HintState}; use crate::display::meter::Meter; use crate::display::window::Window; use crate::event::{Mouse, SearchState}; use crate::message_bar::{MessageBuffer, MessageType}; use crate::renderer::rects::{RenderLines, RenderRect}; use crate::renderer::{self, GlyphCache, QuadRenderer}; -use crate::url::{Url, Urls}; pub mod content; pub mod cursor; @@ -58,7 +59,13 @@ mod meter; #[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))] mod wayland_theme; +/// 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: "; #[derive(Debug)] @@ -164,10 +171,12 @@ impl DisplayUpdate { pub struct Display { pub size_info: SizeInfo, pub window: Window, - pub urls: Urls, - /// Currently highlighted URL. - pub highlighted_url: Option<Url>, + /// Hint highlighted by the mouse. + pub highlighted_hint: Option<HintMatch>, + + /// Hint highlighted by the vi mode cursor. + pub vi_highlighted_hint: Option<HintMatch>, #[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))] pub wayland_event_queue: Option<EventQueue>, @@ -331,8 +340,8 @@ impl Display { hint_state, meter: Meter::new(), size_info, - urls: Urls::new(), - highlighted_url: None, + highlighted_hint: None, + vi_highlighted_hint: None, #[cfg(not(any(target_os = "macos", windows)))] is_x11, #[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))] @@ -473,8 +482,6 @@ impl Display { mut terminal: MutexGuard<'_, Term<T>>, message_buffer: &MessageBuffer, config: &Config, - mouse: &Mouse, - mods: ModifiersState, search_state: &SearchState, ) { // Collect renderable content before the terminal is dropped. @@ -492,10 +499,6 @@ impl Display { let metrics = self.glyph_cache.font_metrics(); let size_info = self.size_info; - let selection = !terminal.selection.as_ref().map(Selection::is_empty).unwrap_or(true); - let mouse_mode = terminal.mode().intersects(TermMode::MOUSE_MODE) - && !terminal.mode().contains(TermMode::VI); - let vi_mode = terminal.mode().contains(TermMode::VI); let vi_mode_cursor = if vi_mode { Some(terminal.vi_mode_cursor) } else { None }; @@ -513,7 +516,6 @@ impl Display { } let mut lines = RenderLines::new(); - let mut urls = Urls::new(); let mut graphics_list = renderer::graphics::RenderList::default(); // Draw grid. @@ -521,11 +523,18 @@ impl Display { let _sampler = self.meter.sampler(); let glyph_cache = &mut self.glyph_cache; + let highlighted_hint = &self.highlighted_hint; + let vi_highlighted_hint = &self.vi_highlighted_hint; self.renderer.with_api(&config.ui_config, &size_info, |mut api| { // Iterate over all non-empty cells in the grid. - for cell in grid_cells { - // Update URL underlines. - urls.update(&size_info, &cell); + for mut cell in grid_cells { + // Underline hints hovered by mouse or vi mode cursor. + let point = 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); + } // Update underline/strikeout. lines.update(&cell); @@ -543,33 +552,9 @@ impl Display { let mut rects = lines.rects(&metrics, &size_info); - // Update visible URLs. - self.urls = urls; - if let Some(url) = self.urls.highlighted(config, mouse, mods, mouse_mode, selection) { - rects.append(&mut url.rects(&metrics, &size_info)); - - self.window.set_mouse_cursor(CursorIcon::Hand); - - self.highlighted_url = Some(url); - } else if self.highlighted_url.is_some() { - self.highlighted_url = None; - - if mouse_mode { - self.window.set_mouse_cursor(CursorIcon::Default); - } else { - self.window.set_mouse_cursor(CursorIcon::Text); - } - } - if let Some(vi_mode_cursor) = vi_mode_cursor { - // Highlight URLs at the vi mode cursor position. - let vi_point = vi_mode_cursor.point; - let line = (vi_point.line + display_offset).0 as usize; - if let Some(url) = self.urls.find_at(Point::new(line, vi_point.column)) { - rects.append(&mut url.rects(&metrics, &size_info)); - } - // 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; self.draw_line_indicator(config, &size_info, total_lines, Some(vi_point), line); } else if search_state.regex().is_some() { @@ -683,6 +668,47 @@ impl Display { self.colors = List::from(&config.ui_config.colors); } + /// Update the mouse/vi mode cursor hint highlighting. + pub fn update_highlighted_hints<T>( + &mut self, + term: &Term<T>, + config: &Config, + mouse: &Mouse, + modifiers: ModifiersState, + ) { + // Update vi mode cursor hint. + if term.mode().contains(TermMode::VI) { + let mods = ModifiersState::all(); + let point = term.vi_mode_cursor.point; + self.vi_highlighted_hint = hint::highlighted_at(&term, config, point, mods); + } else { + self.vi_highlighted_hint = None; + } + + // Abort if mouse highlighting conditions are not met. + if !mouse.inside_text_area || !term.selection.as_ref().map_or(true, Selection::is_empty) { + self.highlighted_hint = None; + return; + } + + // Find highlighted hint at mouse position. + let point = mouse.point(&self.size_info, term.grid().display_offset()); + let highlighted_hint = hint::highlighted_at(&term, config, point, modifiers); + + // Update cursor shape. + if highlighted_hint.is_some() { + self.window.set_mouse_cursor(CursorIcon::Hand); + } else if self.highlighted_hint.is_some() { + if term.mode().intersects(TermMode::MOUSE_MODE) && !term.mode().contains(TermMode::VI) { + self.window.set_mouse_cursor(CursorIcon::Default); + } else { + self.window.set_mouse_cursor(CursorIcon::Text); + } + } + + self.highlighted_hint = highlighted_hint; + } + /// 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. @@ -794,6 +820,18 @@ impl Display { } } +/// Convert a terminal point to a viewport relative point. +pub fn point_to_viewport(display_offset: usize, point: Point) -> Option<Point<usize>> { + let viewport_line = point.line.0 + display_offset as i32; + usize::try_from(viewport_line).ok().map(|line| Point::new(line, point.column)) +} + +/// Convert a viewport relative point to a terminal point. +pub fn viewport_to_point(display_offset: usize, point: Point<usize>) -> Point { + let line = Line(point.line as i32) - display_offset; + Point::new(line, point.column) +} + /// Calculate the cell dimensions based on font metrics. /// /// This will return a tuple of the cell width and height. diff --git a/alacritty/src/event.rs b/alacritty/src/event.rs index 341f398a..a895514f 100644 --- a/alacritty/src/event.rs +++ b/alacritty/src/event.rs @@ -44,15 +44,14 @@ use crate::clipboard::Clipboard; use crate::config::ui_config::{HintAction, HintInternalAction}; use crate::config::{self, Config}; use crate::daemon::start_daemon; -use crate::display::hint::{HintMatch, HintState}; +use crate::display::hint::HintMatch; use crate::display::window::Window; -use crate::display::{Display, DisplayUpdate}; +use crate::display::{self, Display, DisplayUpdate}; use crate::input::{self, ActionContext as _, FONT_SIZE_STEP}; #[cfg(target_os = "macos")] use crate::macos; use crate::message_bar::{Message, MessageBuffer}; use crate::scheduler::{Scheduler, TimerId}; -use crate::url::{Url, Urls}; /// Duration after the last user input until an unlimited search is performed. pub const TYPING_SEARCH_DELAY: Duration = Duration::from_millis(500); @@ -207,17 +206,17 @@ impl<'a, N: Notify + 'a, T: EventListener> input::ActionContext<T> for ActionCon // Update selection. if self.terminal.mode().contains(TermMode::VI) - && self.terminal.selection.as_ref().map(|s| s.is_empty()) != Some(true) + && self.terminal.selection.as_ref().map_or(true, |s| !s.is_empty()) { self.update_selection(self.terminal.vi_mode_cursor.point, Side::Right); - } else if self.mouse().left_button_state == ElementState::Pressed - || self.mouse().right_button_state == ElementState::Pressed + } else if self.mouse.left_button_state == ElementState::Pressed + || self.mouse.right_button_state == ElementState::Pressed { - let point = self.mouse().point; - let line = Line(point.line as i32) - self.terminal.grid().display_offset(); - let point = Point::new(line, point.column); - self.update_selection(point, self.mouse().cell_side); + let display_offset = self.terminal.grid().display_offset(); + let point = self.mouse.point(&self.size_info(), display_offset); + self.update_selection(point, self.mouse.cell_side); } + self.copy_selection(ClipboardType::Selection); *self.dirty = true; } @@ -264,8 +263,6 @@ impl<'a, N: Notify + 'a, T: EventListener> input::ActionContext<T> for ActionCon self.terminal.selection = Some(selection); *self.dirty = true; - - self.copy_selection(ClipboardType::Selection); } fn start_selection(&mut self, ty: SelectionType, point: Point, side: Side) { @@ -322,13 +319,13 @@ impl<'a, N: Notify + 'a, T: EventListener> input::ActionContext<T> for ActionCon } #[inline] - fn window(&self) -> &Window { - &self.display.window + fn window(&mut self) -> &mut Window { + &mut self.display.window } #[inline] - fn window_mut(&mut self) -> &mut Window { - &mut self.display.window + fn display(&mut self) -> &mut Display { + &mut self.display } #[inline] @@ -385,30 +382,6 @@ impl<'a, N: Notify + 'a, T: EventListener> input::ActionContext<T> for ActionCon start_daemon(&alacritty, &args); } - /// Spawn URL launcher when clicking on URLs. - fn launch_url(&self, url: Url) { - if self.mouse.block_url_launcher { - return; - } - - if let Some(ref launcher) = self.config.ui_config.mouse.url.launcher { - let display_offset = self.terminal.grid().display_offset(); - let start = url.start(); - let start = Point::new(Line(start.line as i32 - display_offset as i32), start.column); - let end = url.end(); - let end = Point::new(Line(end.line as i32 - display_offset as i32), end.column); - - let mut args = launcher.args().to_vec(); - args.push(self.terminal.bounds_to_string(start, end)); - - start_daemon(launcher.program(), &args); - } - } - - fn highlighted_url(&self) -> Option<&Url> { - self.display.highlighted_url.as_ref() - } - fn change_font_size(&mut self, delta: f32) { *self.font_size = max(*self.font_size + delta, Size::new(FONT_SIZE_STEP)); let font = self.config.ui_config.font.clone().with_size(*self.font_size); @@ -487,6 +460,7 @@ impl<'a, N: Notify + 'a, T: EventListener> input::ActionContext<T> for ActionCon let end = *focused_match.end(); self.start_selection(SelectionType::Simple, start, Side::Left); self.update_selection(end, Side::Right); + self.copy_selection(ClipboardType::Selection); } self.search_state.dfas = None; @@ -645,42 +619,53 @@ impl<'a, N: Notify + 'a, T: EventListener> input::ActionContext<T> for ActionCon } } - fn hint_state(&mut self) -> &mut HintState { - &mut self.display.hint_state - } - /// Process a new character for keyboard hints. fn hint_input(&mut self, c: char) { - let action = self.display.hint_state.keyboard_input(self.terminal, c); + if let Some(hint) = self.display.hint_state.keyboard_input(self.terminal, c) { + self.mouse.block_hint_launcher = false; + self.trigger_hint(&hint); + } *self.dirty = true; + } - let HintMatch { action, bounds } = match action { - Some(action) => action, - None => return, - }; + /// Trigger a hint action. + fn trigger_hint(&mut self, hint: &HintMatch) { + if self.mouse.block_hint_launcher { + return; + } - match action { + match &hint.action { // Launch an external program. HintAction::Command(command) => { - let text = self.terminal.bounds_to_string(*bounds.start(), *bounds.end()); + let text = self.terminal.bounds_to_string(*hint.bounds.start(), *hint.bounds.end()); let mut args = command.args().to_vec(); args.push(text); start_daemon(command.program(), &args); }, // Copy the text to the clipboard. HintAction::Action(HintInternalAction::Copy) => { - let text = self.terminal.bounds_to_string(*bounds.start(), *bounds.end()); + let text = self.terminal.bounds_to_string(*hint.bounds.start(), *hint.bounds.end()); self.clipboard.store(ClipboardType::Clipboard, text); }, // Write the text to the PTY/search. HintAction::Action(HintInternalAction::Paste) => { - let text = self.terminal.bounds_to_string(*bounds.start(), *bounds.end()); + let text = self.terminal.bounds_to_string(*hint.bounds.start(), *hint.bounds.end()); self.paste(&text); }, // Select the text. HintAction::Action(HintInternalAction::Select) => { - self.start_selection(SelectionType::Simple, *bounds.start(), Side::Left); - self.update_selection(*bounds.end(), Side::Right); + self.start_selection(SelectionType::Simple, *hint.bounds.start(), Side::Left); + self.update_selection(*hint.bounds.end(), Side::Right); + self.copy_selection(ClipboardType::Selection); + }, + // Move the vi mode cursor. + HintAction::Action(HintInternalAction::MoveViModeCursor) => { + // Enter vi mode if we're not in it already. + if !self.terminal.mode().contains(TermMode::VI) { + self.terminal.toggle_vi_mode(); + } + + self.terminal.vi_goto_point(*hint.bounds.start()); }, } } @@ -731,10 +716,6 @@ impl<'a, N: Notify + 'a, T: EventListener> input::ActionContext<T> for ActionCon self.event_loop } - fn urls(&self) -> &Urls { - &self.display.urls - } - fn clipboard_mut(&mut self) -> &mut Clipboard { self.clipboard } @@ -908,9 +889,9 @@ pub struct Mouse { pub scroll_px: f64, pub cell_side: Side, pub lines_scrolled: f32, - pub block_url_launcher: bool, + pub block_hint_launcher: bool, + pub hint_highlight_dirty: bool, pub inside_text_area: bool, - pub point: Point<usize>, pub x: usize, pub y: usize, } @@ -925,17 +906,34 @@ impl Default for Mouse { right_button_state: ElementState::Released, click_state: ClickState::None, cell_side: Side::Left, - block_url_launcher: Default::default(), + hint_highlight_dirty: Default::default(), + block_hint_launcher: Default::default(), inside_text_area: Default::default(), lines_scrolled: Default::default(), scroll_px: Default::default(), - point: Default::default(), x: Default::default(), y: Default::default(), } } } +impl Mouse { + /// Convert mouse pixel coordinates to viewport point. + /// + /// If the coordinates are outside of the terminal grid, like positions inside the padding, the + /// coordinates will be clamped to the closest grid coordinates. + #[inline] + pub fn point(&self, size: &SizeInfo, display_offset: usize) -> Point { + let col = self.x.saturating_sub(size.padding_x() as usize) / (size.cell_width() as usize); + let col = min(Column(col), size.last_column()); + + let line = self.y.saturating_sub(size.padding_y() as usize) / (size.cell_height() as usize); + let line = min(line, size.bottommost_line().0 as usize); + + display::viewport_to_point(display_offset, Point::new(line, col)) + } +} + /// The event processor. /// /// Stores some state from received events and dispatches actions when they are @@ -1115,6 +1113,17 @@ impl<N: Notify + OnResize> Processor<N> { return; } + if self.dirty || self.mouse.hint_highlight_dirty { + self.display.update_highlighted_hints( + &terminal, + &self.config, + &self.mouse, + self.modifiers, + ); + self.mouse.hint_highlight_dirty = false; + self.dirty = true; + } + if self.dirty { self.dirty = false; @@ -1127,14 +1136,7 @@ impl<N: Notify + OnResize> Processor<N> { } // Redraw screen. - self.display.draw( - terminal, - &self.message_buffer, - &self.config, - &self.mouse, - self.modifiers, - &self.search_state, - ); + self.display.draw(terminal, &self.message_buffer, &self.config, &self.search_state); } }); @@ -1165,7 +1167,7 @@ impl<N: Notify + OnResize> Processor<N> { // Resize to event's dimensions, since no resize event is emitted on Wayland. display_update_pending.set_dimensions(PhysicalSize::new(width, height)); - processor.ctx.window_mut().dpr = scale_factor; + processor.ctx.window().dpr = scale_factor; *processor.ctx.dirty = true; }, Event::Message(message) => { @@ -1184,7 +1186,7 @@ impl<N: Notify + OnResize> Processor<N> { TerminalEvent::Title(title) => { let ui_config = &processor.ctx.config.ui_config; if ui_config.window.dynamic_title { - processor.ctx.window_mut().set_title(&title); + processor.ctx.window().set_title(&title); } }, TerminalEvent::ResetTitle => { @@ -1198,7 +1200,7 @@ impl<N: Notify + OnResize> Processor<N> { // Set window urgency. if processor.ctx.terminal.mode().contains(TermMode::URGENCY_HINTS) { let focused = processor.ctx.terminal.is_focused; - processor.ctx.window_mut().set_urgent(!focused); + processor.ctx.window().set_urgent(!focused); } // Ring visual bell. @@ -1220,6 +1222,7 @@ impl<N: Notify + OnResize> Processor<N> { let text = format(processor.ctx.display.colors[index]); processor.ctx.write_to_pty(text.into_bytes()); }, + TerminalEvent::PtyWrite(text) => processor.ctx.write_to_pty(text.into_bytes()), TerminalEvent::MouseCursorDirty => processor.reset_mouse_cursor(), TerminalEvent::Exit => (), TerminalEvent::CursorBlinkingChange(_) => { @@ -1251,16 +1254,16 @@ impl<N: Notify + OnResize> Processor<N> { }, WindowEvent::ReceivedCharacter(c) => processor.received_char(c), WindowEvent::MouseInput { state, button, .. } => { - processor.ctx.window_mut().set_mouse_visible(true); + processor.ctx.window().set_mouse_visible(true); processor.mouse_input(state, button); *processor.ctx.dirty = true; }, WindowEvent::CursorMoved { position, .. } => { - processor.ctx.window_mut().set_mouse_visible(true); + processor.ctx.window().set_mouse_visible(true); processor.mouse_moved(position); }, WindowEvent::MouseWheel { delta, phase, .. } => { - processor.ctx.window_mut().set_mouse_visible(true); + processor.ctx.window().set_mouse_visible(true); processor.mouse_wheel_input(delta, phase); }, WindowEvent::Focused(is_focused) => { @@ -1269,9 +1272,9 @@ impl<N: Notify + OnResize> Processor<N> { *processor.ctx.dirty = true; if is_focused { - processor.ctx.window_mut().set_urgent(false); + processor.ctx.window().set_urgent(false); } else { - processor.ctx.window_mut().set_mouse_visible(true); + processor.ctx.window().set_mouse_visible(true); } processor.ctx.update_cursor_blinking(); @@ -1285,7 +1288,7 @@ impl<N: Notify + OnResize> Processor<N> { WindowEvent::CursorLeft { .. } => { processor.ctx.mouse.inside_text_area = false; - if processor.ctx.highlighted_url().is_some() { + if processor.ctx.display().highlighted_hint.is_some() { *processor.ctx.dirty = true; } }, @@ -1382,12 +1385,12 @@ impl<N: Notify + OnResize> Processor<N> { if !config.ui_config.window.dynamic_title || processor.ctx.config.ui_config.window.title != config.ui_config.window.title { - processor.ctx.window_mut().set_title(&config.ui_config.window.title); + processor.ctx.window().set_title(&config.ui_config.window.title); } #[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))] if processor.ctx.event_loop.is_wayland() { - processor.ctx.window_mut().set_wayland_theme(&config.ui_config.colors); + processor.ctx.window().set_wayland_theme(&config.ui_config.colors); } // Set subpixel anti-aliasing. @@ -1396,7 +1399,7 @@ impl<N: Notify + OnResize> Processor<N> { // Disable shadows for transparent windows on macOS. #[cfg(target_os = "macos")] - processor.ctx.window_mut().set_has_shadow(config.ui_config.background_opacity() >= 1.0); + processor.ctx.window().set_has_shadow(config.ui_config.background_opacity() >= 1.0); // Update hint keys. processor.ctx.display.hint_state.update_alphabet(config.ui_config.hints.alphabet()); diff --git a/alacritty/src/input.rs b/alacritty/src/input.rs index a66511cf..3559b85e 100644 --- a/alacritty/src/input.rs +++ b/alacritty/src/input.rs @@ -22,7 +22,7 @@ use glutin::window::CursorIcon; use alacritty_terminal::ansi::{ClearMode, Handler}; use alacritty_terminal::event::EventListener; use alacritty_terminal::grid::{Dimensions, Scroll}; -use alacritty_terminal::index::{Boundary, Column, Direction, Line, Point, Side}; +use alacritty_terminal::index::{Boundary, Column, Direction, Point, Side}; use alacritty_terminal::selection::SelectionType; use alacritty_terminal::term::search::Match; use alacritty_terminal::term::{ClipboardType, SizeInfo, Term, TermMode}; @@ -31,12 +31,12 @@ use alacritty_terminal::vi_mode::ViMotion; use crate::clipboard::Clipboard; use crate::config::{Action, BindingMode, Config, Key, SearchAction, ViAction}; use crate::daemon::start_daemon; -use crate::display::hint::HintState; +use crate::display::hint::HintMatch; use crate::display::window::Window; +use crate::display::Display; use crate::event::{ClickState, Event, Mouse, TYPING_SEARCH_DELAY}; use crate::message_bar::{self, Message}; use crate::scheduler::{Scheduler, TimerId}; -use crate::url::{Url, Urls}; /// Font size change interval. pub const FONT_SIZE_STEP: f32 = 0.5; @@ -75,8 +75,8 @@ pub trait ActionContext<T: EventListener> { fn suppress_chars(&mut self) -> &mut bool; fn modifiers(&mut self) -> &mut ModifiersState; fn scroll(&mut self, _scroll: Scroll) {} - fn window(&self) -> &Window; - fn window_mut(&mut self) -> &mut Window; + fn window(&mut self) -> &mut Window; + fn display(&mut self) -> &mut Display; fn terminal(&self) -> &Term<T>; fn terminal_mut(&mut self) -> &mut Term<T>; fn spawn_new_instance(&mut self) {} @@ -86,9 +86,6 @@ pub trait ActionContext<T: EventListener> { fn message(&self) -> Option<&Message>; fn config(&self) -> &Config; fn event_loop(&self) -> &EventLoopWindowTarget<Event>; - fn urls(&self) -> &Urls; - fn launch_url(&self, _url: Url) {} - fn highlighted_url(&self) -> Option<&Url>; fn mouse_mode(&self) -> bool; fn clipboard_mut(&mut self) -> &mut Clipboard; fn scheduler_mut(&mut self) -> &mut Scheduler; @@ -105,8 +102,8 @@ pub trait ActionContext<T: EventListener> { fn search_active(&self) -> bool; fn on_typing_start(&mut self) {} fn toggle_vi_mode(&mut self) {} - fn hint_state(&mut self) -> &mut HintState; fn hint_input(&mut self, _character: char) {} + fn trigger_hint(&mut self, _hint: &HintMatch) {} fn paste(&mut self, _text: &str) {} } @@ -142,7 +139,7 @@ impl<T: EventListener> Execute<T> for Action { }, Action::Command(program) => start_daemon(program.program(), program.args()), Action::Hint(hint) => { - ctx.hint_state().start(hint.clone()); + ctx.display().hint_state.start(hint.clone()); ctx.mark_dirty(); }, Action::ToggleViMode => ctx.toggle_vi_mode(), @@ -164,12 +161,12 @@ impl<T: EventListener> Execute<T> for Action { Self::toggle_selection(ctx, SelectionType::Semantic); }, Action::ViAction(ViAction::Open) => { - ctx.mouse_mut().block_url_launcher = false; - let vi_point = ctx.terminal().vi_mode_cursor.point; - let line = (vi_point.line + ctx.terminal().grid().display_offset()).0 as usize; - if let Some(url) = ctx.urls().find_at(Point::new(line, vi_point.column)) { - ctx.launch_url(url); + let hint = ctx.display().vi_highlighted_hint.take(); + if let Some(hint) = &hint { + ctx.mouse_mut().block_hint_launcher = false; + ctx.trigger_hint(hint); } + ctx.display().vi_highlighted_hint = hint; }, Action::ViAction(ViAction::SearchNext) => { let terminal = ctx.terminal(); @@ -250,11 +247,13 @@ impl<T: EventListener> Execute<T> for Action { let text = ctx.clipboard_mut().load(ClipboardType::Selection); ctx.paste(&text); }, - Action::ToggleFullscreen => ctx.window_mut().toggle_fullscreen(), + Action::ToggleFullscreen => ctx.window().toggle_fullscreen(), #[cfg(target_os = "macos")] - Action::ToggleSimpleFullscreen => ctx.window_mut().toggle_simple_fullscreen(), + Action::ToggleSimpleFullscreen => ctx.window().toggle_simple_fullscreen(), #[cfg(target_os = "macos")] Action::Hide => ctx.event_loop().hide_application(), + #[cfg(target_os = "macos")] + Action::HideOtherApplications => ctx.event_loop().hide_other_applications(), #[cfg(not(target_os = "macos"))] Action::Hide => ctx.window().set_visible(false), Action::Minimize => ctx.window().set_minimized(true), @@ -325,25 +324,6 @@ impl<T: EventListener> Execute<T> for Action { } } -#[derive(Debug, Clone, PartialEq)] -pub enum MouseState { - Url(Url), - MessageBar, - MessageBarButton, - Mouse, - Text, -} - -impl From<MouseState> for CursorIcon { - fn from(mouse_state: MouseState) -> CursorIcon { - match mouse_state { - MouseState::Url(_) | MouseState::MessageBarButton => CursorIcon::Hand, - MouseState::Text => CursorIcon::Text, - _ => CursorIcon::Default, - } - } -} - impl<T: EventListener, A: ActionContext<T>> Processor<T, A> { pub fn new(ctx: A) -> Self { Self { ctx, _phantom: Default::default() } @@ -361,22 +341,19 @@ impl<T: EventListener, A: ActionContext<T>> Processor<T, A> { self.update_selection_scrolling(y); } + let display_offset = self.ctx.terminal().grid().display_offset(); + let old_point = self.ctx.mouse().point(&size_info, display_offset); + let x = min(max(x, 0), size_info.width() as i32 - 1) as usize; let y = min(max(y, 0), size_info.height() as i32 - 1) as usize; - self.ctx.mouse_mut().x = x; self.ctx.mouse_mut().y = y; let inside_text_area = size_info.contains_point(x, y); - let point = self.coords_to_point(x, y); let cell_side = self.cell_side(x); - let cell_changed = point != self.ctx.mouse().point; - - // Update mouse state and check for URL change. - let mouse_state = self.mouse_state(); - self.update_url_state(&mouse_state); - self.ctx.window_mut().set_mouse_cursor(mouse_state.into()); + let point = self.ctx.mouse().point(&size_info, display_offset); + let cell_changed = old_point != point; // If the mouse hasn't changed cells, do nothing. if !cell_changed @@ -388,18 +365,21 @@ impl<T: EventListener, A: ActionContext<T>> Processor<T, A> { self.ctx.mouse_mut().inside_text_area = inside_text_area; self.ctx.mouse_mut().cell_side = cell_side; - self.ctx.mouse_mut().point = point; + + // Update mouse state and check for URL change. + let mouse_state = self.cursor_state(); + self.ctx.window().set_mouse_cursor(mouse_state); + + // Prompt hint highlight update. + self.ctx.mouse_mut().hint_highlight_dirty = true; // Don't launch URLs if mouse has moved. - self.ctx.mouse_mut().block_url_launcher = true; + self.ctx.mouse_mut().block_hint_launcher = true; if (lmb_pressed || rmb_pressed) && (self.ctx.modifiers().shift() || !self.ctx.mouse_mode()) { - let line = Line(point.line as i32) - self.ctx.terminal().grid().display_offset(); - let point = Point::new(line, point.column); self.ctx.update_selection(point, cell_side); } else if cell_changed - && point.line < self.ctx.terminal().screen_lines() && self.ctx.terminal().mode().intersects(TermMode::MOUSE_MOTION | TermMode::MOUSE_DRAG) { if lmb_pressed { @@ -414,23 +394,6 @@ impl<T: EventListener, A: ActionContext<T>> Processor<T, A> { } } - /// Convert window space pixels to terminal grid coordinates. - /// - /// If the coordinates are outside of the terminal grid, like positions inside the padding, the - /// coordinates will be clamped to the closest grid coordinates. - #[inline] - fn coords_to_point(&self, x: usize, y: usize) -> Point<usize> { - let size = self.ctx.size_info(); - - let column = x.saturating_sub(size.padding_x() as usize) / (size.cell_width() as usize); - let column = min(Column(column), size.last_column()); - - let line = y.saturating_sub(size.padding_y() as usize) / (size.cell_height() as usize); - let line = min(line, size.bottommost_line().0 as usize); - - Point::new(line, column) - } - /// Check which side of a cell an X coordinate lies on. fn cell_side(&self, x: usize) -> Side { let size_info = self.ctx.size_info(); @@ -453,13 +416,45 @@ impl<T: EventListener, A: ActionContext<T>> Processor<T, A> { } } - fn normal_mouse_report(&mut self, button: u8) { - let Point { line, column } = self.ctx.mouse().point; + fn mouse_report(&mut self, button: u8, state: ElementState) { + let display_offset = self.ctx.terminal().grid().display_offset(); + let point = self.ctx.mouse().point(&self.ctx.size_info(), display_offset); + + // Assure the mouse point is not in the scrollback. + if point.line < 0 { + return; + } + + // Calculate modifiers value. + let mut mods = 0; + let modifiers = self.ctx.modifiers(); + if modifiers.shift() { + mods += 4; + } + if modifiers.alt() { + mods += 8; + } + if modifiers.ctrl() { + mods += 16; + } + + // Report mouse events. + if self.ctx.terminal().mode().contains(TermMode::SGR_MOUSE) { + self.sgr_mouse_report(point, button + mods, state); + } else if let ElementState::Released = state { + self.normal_mouse_report(point, 3 + mods); + } else { + self.normal_mouse_report(point, button + mods); + } + } + + fn normal_mouse_report(&mut self, point: Point, button: u8) { + let Point { line, column } = point; let utf8 = self.ctx.terminal().mode().contains(TermMode::UTF8_MOUSE); let max_point = if utf8 { 2015 } else { 223 }; - if line >= max_point || column >= Column(max_point) { + if line >= max_point || column >= max_point { return; } @@ -479,49 +474,24 @@ impl<T: EventListener, A: ActionContext<T>> Processor<T, A> { } if utf8 && line >= 95 { - msg.append(&mut mouse_pos_encode(line)); + msg.append(&mut mouse_pos_encode(line.0 as usize)); } else { - msg.push(32 + 1 + line as u8); + msg.push(32 + 1 + line.0 as u8); } self.ctx.write_to_pty(msg); } - fn sgr_mouse_report(&mut self, button: u8, state: ElementState) { - let Point { line, column } = self.ctx.mouse().point; + fn sgr_mouse_report(&mut self, point: Point, button: u8, state: ElementState) { let c = match state { ElementState::Pressed => 'M', ElementState::Released => 'm', }; - let msg = format!("\x1b[<{};{};{}{}", button, column + 1, line + 1, c); + let msg = format!("\x1b[<{};{};{}{}", button, point.column + 1, point.line + 1, c); self.ctx.write_to_pty(msg.into_bytes()); } - fn mouse_report(&mut self, button: u8, state: ElementState) { - // Calculate modifiers value. - let mut mods = 0; - let modifiers = self.ctx.modifiers(); - if modifiers.shift() { - mods += 4; - } - if modifiers.alt() { - mods += 8; - } - if modifiers.ctrl() { - mods += 16; - } - - // Report mouse events. - if self.ctx.terminal().mode().contains(TermMode::SGR_MOUSE) { - self.sgr_mouse_report(button + mods, state); - } else if let ElementState::Released = state { - self.normal_mouse_report(3 + mods); - } else { - self.normal_mouse_report(button + mods); - } - } - fn on_mouse_press(&mut self, button: MouseButton) { // Handle mouse mode. if !self.ctx.modifiers().shift() && self.ctx.mouse_mode() { @@ -560,10 +530,8 @@ impl<T: EventListener, A: ActionContext<T>> Processor<T, A> { }; // Load mouse point, treating message bar and padding as the closest cell. - let point = self.ctx.mouse().point; let display_offset = self.ctx.terminal().grid().display_offset(); - let absolute_line = Line(point.line as i32) - display_offset; - let point = Point::new(absolute_line, point.column); + let point = self.ctx.mouse().point(&self.ctx.size_info(), display_offset); match button { MouseButton::Left => self.on_left_click(point), @@ -617,7 +585,7 @@ impl<T: EventListener, A: ActionContext<T>> Processor<T, A> { match self.ctx.mouse().click_state { ClickState::Click => { // Don't launch URLs if this click cleared the selection. - self.ctx.mouse_mut().block_url_launcher = !self.ctx.selection_is_empty(); + self.ctx.mouse_mut().block_hint_launcher = !self.ctx.selection_is_empty(); self.ctx.clear_selection(); @@ -629,11 +597,11 @@ impl<T: EventListener, A: ActionContext<T>> Processor<T, A> { } }, ClickState::DoubleClick => { - self.ctx.mouse_mut().block_url_launcher = true; + self.ctx.mouse_mut().block_hint_launcher = true; self.ctx.start_selection(SelectionType::Semantic, point, side); }, ClickState::TripleClick => { - self.ctx.mouse_mut().block_url_launcher = true; + self.ctx.mouse_mut().block_hint_launcher = true; self.ctx.start_selection(SelectionType::Lines, point, side); }, ClickState::None => (), @@ -656,11 +624,19 @@ impl<T: EventListener, A: ActionContext<T>> Processor<T, A> { }; self.mouse_report(code, ElementState::Released); return; - } else if let (MouseButton::Left, MouseState::Url(url)) = (button, self.mouse_state()) { - self.ctx.launch_url(url); } + // Trigger hints highlighted by the mouse. + let hint = self.ctx.display().highlighted_hint.take(); + if let Some(hint) = hint.as_ref().filter(|_| button == MouseButton::Left) { + self.ctx.trigger_hint(hint); + } + self.ctx.display().highlighted_hint = hint; + self.ctx.scheduler_mut().unschedule(TimerId::SelectionScrolling); + + // Copy selection on release, to prevent flooding the display server. + self.ctx.copy_selection(ClipboardType::Selection); } pub fn mouse_wheel_input(&mut self, delta: MouseScrollDelta, phase: TouchPhase) { @@ -746,7 +722,7 @@ impl<T: EventListener, A: ActionContext<T>> Processor<T, A> { } // Skip normal mouse events if the message bar has been clicked. - if self.message_bar_mouse_state() == Some(MouseState::MessageBarButton) + if self.message_bar_cursor_state() == Some(CursorIcon::Hand) && state == ElementState::Pressed { let size = self.ctx.size_info(); @@ -771,7 +747,7 @@ impl<T: EventListener, A: ActionContext<T>> Processor<T, A> { }, }; - self.ctx.window_mut().set_mouse_cursor(new_icon); + self.ctx.window().set_mouse_cursor(new_icon); } else { match state { ElementState::Pressed => { @@ -786,7 +762,7 @@ impl<T: EventListener, A: ActionContext<T>> Processor<T, A> { /// Process key input. pub fn key_input(&mut self, input: KeyboardInput) { // All key bindings are disabled while a hint is being selected. - if self.ctx.hint_state().active() { + if self.ctx.display().hint_state.active() { *self.ctx.suppress_chars() = false; return; } @@ -811,17 +787,19 @@ impl<T: EventListener, A: ActionContext<T>> Processor<T, A> { pub fn modifiers_input(&mut self, modifiers: ModifiersState) { *self.ctx.modifiers() = modifiers; + // Prompt hint highlight update. + self.ctx.mouse_mut().hint_highlight_dirty = true; + // Update mouse state and check for URL change. - let mouse_state = self.mouse_state(); - self.update_url_state(&mouse_state); - self.ctx.window_mut().set_mouse_cursor(mouse_state.into()); + let mouse_state = self.cursor_state(); + self.ctx.window().set_mouse_cursor(mouse_state); } /// Reset mouse cursor based on modifier and terminal state. #[inline] pub fn reset_mouse_cursor(&mut self) { - let mouse_state = self.mouse_state(); - self.ctx.window_mut().set_mouse_cursor(mouse_state.into()); + let mouse_state = self.cursor_state(); + self.ctx.window().set_mouse_cursor(mouse_state); } /// Process a received character. @@ -829,7 +807,7 @@ impl<T: EventListener, A: ActionContext<T>> Processor<T, A> { let suppress_chars = *self.ctx.suppress_chars(); // Handle hint selection over anything else. - if self.ctx.hint_state().active() && !suppress_chars { + if self.ctx.display().hint_state.active() && !suppress_chars { self.ctx.hint_input(c); return; } @@ -923,8 +901,8 @@ impl<T: EventListener, A: ActionContext<T>> Processor<T, A> { } } - /// Check mouse state in relation to the message bar. - fn message_bar_mouse_state(&self) -> Option<MouseState> { + /// Check mouse icon state in relation to the message bar. + fn message_bar_cursor_state(&self) -> Option<CursorIcon> { // Since search is above the message bar, the button is offset by search's height. let search_height = if self.ctx.search_active() { 1 } else { 0 }; @@ -934,58 +912,36 @@ impl<T: EventListener, A: ActionContext<T>> Processor<T, A> { + size.cell_height() as usize * (size.screen_lines() + search_height); let mouse = self.ctx.mouse(); + let display_offset = self.ctx.terminal().grid().display_offset(); + let point = self.ctx.mouse().point(&self.ctx.size_info(), display_offset); + if self.ctx.message().is_none() || (mouse.y <= terminal_end) { None } else if mouse.y <= terminal_end + size.cell_height() as usize - && mouse.point.column + message_bar::CLOSE_BUTTON_TEXT.len() >= size.columns() + && point.column + message_bar::CLOSE_BUTTON_TEXT.len() >= size.columns() { - Some(MouseState::MessageBarButton) + Some(CursorIcon::Hand) } else { - Some(MouseState::MessageBar) - } - } - - /// Trigger redraw when URL highlight changed. - #[inline] - fn update_url_state(&mut self, mouse_state: &MouseState) { - let highlighted_url = self.ctx.highlighted_url(); - if let MouseState::Url(url) = mouse_state { - if Some(url) != highlighted_url { - self.ctx.mark_dirty(); - } - } else if highlighted_url.is_some() { - self.ctx.mark_dirty(); + Some(CursorIcon::Default) } } - /// Location of the mouse cursor. - fn mouse_state(&mut self) -> MouseState { - // Check message bar before URL to ignore URLs in the message bar. - if let Some(mouse_state) = self.message_bar_mouse_state() { - return mouse_state; - } + /// Icon state of the cursor. + fn cursor_state(&mut self) -> CursorIcon { + let display_offset = self.ctx.terminal().grid().display_offset(); + let point = self.ctx.mouse().point(&self.ctx.size_info(), display_offset); - let mouse_mode = self.ctx.mouse_mode(); + // Function to check if mouse is on top of a hint. + let hint_highlighted = |hint: &HintMatch| hint.bounds.contains(&point); - // Check for URL at mouse cursor. - let mods = *self.ctx.modifiers(); - let highlighted_url = self.ctx.urls().highlighted( - self.ctx.config(), - self.ctx.mouse(), - mods, - mouse_mode, - !self.ctx.selection_is_empty(), - ); - - if let Some(url) = highlighted_url { - return MouseState::Url(url); - } - - // Check mouse mode if location is not special. - if !self.ctx.modifiers().shift() && mouse_mode { - MouseState::Mouse + if let Some(mouse_state) = self.message_bar_cursor_state() { + mouse_state + } else if self.ctx.display().highlighted_hint.as_ref().map_or(false, hint_highlighted) { + CursorIcon::Hand + } else if !self.ctx.modifiers().shift() && self.ctx.mouse_mode() { + CursorIcon::Default } else { - MouseState::Text + CursorIcon::Text } } @@ -1127,11 +1083,11 @@ mod tests { &mut self.modifiers } - fn window(&self) -> &Window { + fn window(&mut self) -> &mut Window { unimplemented!(); } - fn window_mut(&mut self) -> &mut Window { + fn display(&mut self) -> &mut Display { unimplemented!(); } @@ -1155,21 +1111,9 @@ mod tests { unimplemented!(); } - fn urls(&self) -> &Urls { - unimplemented!(); - } - - fn highlighted_url(&self) -> Option<&Url> { - unimplemented!(); - } - fn scheduler_mut(&mut self) -> &mut Scheduler { unimplemented!(); } - - fn hint_state(&mut self) -> &mut HintState { - unimplemented!(); - } } macro_rules! test_clickstate { diff --git a/alacritty/src/main.rs b/alacritty/src/main.rs index 0914aee4..c68f3cfb 100644 --- a/alacritty/src/main.rs +++ b/alacritty/src/main.rs @@ -45,7 +45,6 @@ mod message_bar; mod panic; mod renderer; mod scheduler; -mod url; mod gl { #![allow(clippy::all)] |