From 530de00049c2afcc562d36ccdb3e6afa2fe396a5 Mon Sep 17 00:00:00 2001 From: Christian Duerr Date: Sun, 24 Jan 2021 21:45:36 +0000 Subject: Move renderable cell transformation to alacritty This refactors a large chunk of the alacritty_terminal API to expose all data necessary for rendering uniformly through the `renderable_content` call. This also no longer transforms the cells for rendering by a GUI but instead just reports the content from a terminal emulation perspective. The transformation into renderable cells is now done inside the alacritty crate. Since the terminal itself only ever needs to know about modified color RGB values, the configuration for colors was moved to the alacritty UI code. --- alacritty/Cargo.toml | 2 +- alacritty/src/config/bell.rs | 70 +++ alacritty/src/config/color.rs | 212 +++++++++ alacritty/src/config/mod.rs | 14 +- alacritty/src/config/ui_config.rs | 14 + alacritty/src/cursor.rs | 92 ---- alacritty/src/display.rs | 794 ------------------------------- alacritty/src/display/bell.rs | 122 +++++ alacritty/src/display/color.rs | 167 +++++++ alacritty/src/display/content.rs | 404 ++++++++++++++++ alacritty/src/display/cursor.rs | 92 ++++ alacritty/src/display/meter.rs | 97 ++++ alacritty/src/display/mod.rs | 823 +++++++++++++++++++++++++++++++++ alacritty/src/display/wayland_theme.rs | 81 ++++ alacritty/src/display/window.rs | 497 ++++++++++++++++++++ alacritty/src/event.rs | 249 ++++++---- alacritty/src/input.rs | 157 +++---- alacritty/src/main.rs | 6 - alacritty/src/meter.rs | 97 ---- alacritty/src/renderer/mod.rs | 9 +- alacritty/src/renderer/rects.rs | 16 +- alacritty/src/url.rs | 37 +- alacritty/src/wayland_theme.rs | 80 ---- alacritty/src/window.rs | 498 -------------------- 24 files changed, 2843 insertions(+), 1787 deletions(-) create mode 100644 alacritty/src/config/bell.rs create mode 100644 alacritty/src/config/color.rs delete mode 100644 alacritty/src/cursor.rs delete mode 100644 alacritty/src/display.rs create mode 100644 alacritty/src/display/bell.rs create mode 100644 alacritty/src/display/color.rs create mode 100644 alacritty/src/display/content.rs create mode 100644 alacritty/src/display/cursor.rs create mode 100644 alacritty/src/display/meter.rs create mode 100644 alacritty/src/display/mod.rs create mode 100644 alacritty/src/display/wayland_theme.rs create mode 100644 alacritty/src/display/window.rs delete mode 100644 alacritty/src/meter.rs delete mode 100644 alacritty/src/wayland_theme.rs delete mode 100644 alacritty/src/window.rs (limited to 'alacritty') diff --git a/alacritty/Cargo.toml b/alacritty/Cargo.toml index 73724fbe..923659d2 100644 --- a/alacritty/Cargo.toml +++ b/alacritty/Cargo.toml @@ -10,7 +10,7 @@ edition = "2018" [dependencies.alacritty_terminal] path = "../alacritty_terminal" -version = "0.12.1-dev" +version = "0.13.0-dev" default-features = false [dependencies.alacritty_config_derive] diff --git a/alacritty/src/config/bell.rs b/alacritty/src/config/bell.rs new file mode 100644 index 00000000..2516e2b3 --- /dev/null +++ b/alacritty/src/config/bell.rs @@ -0,0 +1,70 @@ +use std::time::Duration; + +use alacritty_config_derive::ConfigDeserialize; + +use alacritty_terminal::config::Program; +use alacritty_terminal::term::color::Rgb; + +#[derive(ConfigDeserialize, Clone, Debug, PartialEq, Eq)] +pub struct BellConfig { + /// Visual bell animation function. + pub animation: BellAnimation, + + /// Command to run on bell. + pub command: Option, + + /// Visual bell flash color. + pub color: Rgb, + + /// Visual bell duration in milliseconds. + duration: u16, +} + +impl Default for BellConfig { + fn default() -> Self { + Self { + color: Rgb { r: 255, g: 255, b: 255 }, + animation: Default::default(), + command: Default::default(), + duration: Default::default(), + } + } +} + +impl BellConfig { + pub fn duration(&self) -> Duration { + Duration::from_millis(self.duration as u64) + } +} + +/// `VisualBellAnimations` are modeled after a subset of CSS transitions and Robert +/// Penner's Easing Functions. +#[derive(ConfigDeserialize, Clone, Copy, Debug, PartialEq, Eq)] +pub enum BellAnimation { + // CSS animation. + Ease, + // CSS animation. + EaseOut, + // Penner animation. + EaseOutSine, + // Penner animation. + EaseOutQuad, + // Penner animation. + EaseOutCubic, + // Penner animation. + EaseOutQuart, + // Penner animation. + EaseOutQuint, + // Penner animation. + EaseOutExpo, + // Penner animation. + EaseOutCirc, + // Penner animation. + Linear, +} + +impl Default for BellAnimation { + fn default() -> Self { + BellAnimation::EaseOutExpo + } +} diff --git a/alacritty/src/config/color.rs b/alacritty/src/config/color.rs new file mode 100644 index 00000000..cd5d964d --- /dev/null +++ b/alacritty/src/config/color.rs @@ -0,0 +1,212 @@ +use serde::de::Error as SerdeError; +use serde::{Deserialize, Deserializer}; + +use alacritty_config_derive::ConfigDeserialize; +use alacritty_terminal::term::color::{CellRgb, Rgb}; + +#[derive(ConfigDeserialize, Clone, Debug, Default, PartialEq, Eq)] +pub struct Colors { + pub primary: PrimaryColors, + pub cursor: InvertedCellColors, + pub vi_mode_cursor: InvertedCellColors, + pub selection: InvertedCellColors, + pub normal: NormalColors, + pub bright: BrightColors, + pub dim: Option, + pub indexed_colors: Vec, + pub search: SearchColors, + pub line_indicator: LineIndicatorColors, +} + +impl Colors { + pub fn search_bar_foreground(&self) -> Rgb { + self.search.bar.foreground.unwrap_or(self.primary.background) + } + + pub fn search_bar_background(&self) -> Rgb { + self.search.bar.background.unwrap_or(self.primary.foreground) + } +} + +#[derive(ConfigDeserialize, Copy, Clone, Default, Debug, PartialEq, Eq)] +pub struct LineIndicatorColors { + pub foreground: Option, + pub background: Option, +} + +#[derive(Deserialize, Copy, Clone, Default, Debug, PartialEq, Eq)] +pub struct IndexedColor { + pub color: Rgb, + + index: ColorIndex, +} + +impl IndexedColor { + #[inline] + pub fn index(&self) -> u8 { + self.index.0 + } +} + +#[derive(Copy, Clone, Default, Debug, PartialEq, Eq)] +struct ColorIndex(u8); + +impl<'de> Deserialize<'de> for ColorIndex { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let index = u8::deserialize(deserializer)?; + + if index < 16 { + Err(SerdeError::custom( + "Config error: indexed_color's index is {}, but a value bigger than 15 was \ + expected; ignoring setting", + )) + } else { + Ok(Self(index)) + } + } +} + +#[derive(ConfigDeserialize, Debug, Copy, Clone, PartialEq, Eq)] +pub struct InvertedCellColors { + #[config(alias = "text")] + pub foreground: CellRgb, + #[config(alias = "cursor")] + pub background: CellRgb, +} + +impl Default for InvertedCellColors { + fn default() -> Self { + Self { foreground: CellRgb::CellBackground, background: CellRgb::CellForeground } + } +} + +#[derive(ConfigDeserialize, Debug, Copy, Clone, Default, PartialEq, Eq)] +pub struct SearchColors { + pub focused_match: InvertedCellColors, + pub matches: MatchColors, + bar: BarColors, +} + +#[derive(ConfigDeserialize, Debug, Copy, Clone, PartialEq, Eq)] +pub struct MatchColors { + pub foreground: CellRgb, + pub background: CellRgb, +} + +impl Default for MatchColors { + fn default() -> Self { + Self { + background: CellRgb::Rgb(Rgb { r: 0xff, g: 0xff, b: 0xff }), + foreground: CellRgb::Rgb(Rgb { r: 0x00, g: 0x00, b: 0x00 }), + } + } +} + +#[derive(ConfigDeserialize, Debug, Copy, Clone, Default, PartialEq, Eq)] +pub struct BarColors { + foreground: Option, + background: Option, +} + +#[derive(ConfigDeserialize, Clone, Debug, PartialEq, Eq)] +pub struct PrimaryColors { + pub foreground: Rgb, + pub background: Rgb, + pub bright_foreground: Option, + pub dim_foreground: Option, +} + +impl Default for PrimaryColors { + fn default() -> Self { + PrimaryColors { + background: Rgb { r: 0x1d, g: 0x1f, b: 0x21 }, + foreground: Rgb { r: 0xc5, g: 0xc8, b: 0xc6 }, + bright_foreground: Default::default(), + dim_foreground: Default::default(), + } + } +} + +#[derive(ConfigDeserialize, Clone, Debug, PartialEq, Eq)] +pub struct NormalColors { + pub black: Rgb, + pub red: Rgb, + pub green: Rgb, + pub yellow: Rgb, + pub blue: Rgb, + pub magenta: Rgb, + pub cyan: Rgb, + pub white: Rgb, +} + +impl Default for NormalColors { + fn default() -> Self { + NormalColors { + black: Rgb { r: 0x1d, g: 0x1f, b: 0x21 }, + red: Rgb { r: 0xcc, g: 0x66, b: 0x66 }, + green: Rgb { r: 0xb5, g: 0xbd, b: 0x68 }, + yellow: Rgb { r: 0xf0, g: 0xc6, b: 0x74 }, + blue: Rgb { r: 0x81, g: 0xa2, b: 0xbe }, + magenta: Rgb { r: 0xb2, g: 0x94, b: 0xbb }, + cyan: Rgb { r: 0x8a, g: 0xbe, b: 0xb7 }, + white: Rgb { r: 0xc5, g: 0xc8, b: 0xc6 }, + } + } +} + +#[derive(ConfigDeserialize, Clone, Debug, PartialEq, Eq)] +pub struct BrightColors { + pub black: Rgb, + pub red: Rgb, + pub green: Rgb, + pub yellow: Rgb, + pub blue: Rgb, + pub magenta: Rgb, + pub cyan: Rgb, + pub white: Rgb, +} + +impl Default for BrightColors { + fn default() -> Self { + BrightColors { + black: Rgb { r: 0x66, g: 0x66, b: 0x66 }, + red: Rgb { r: 0xd5, g: 0x4e, b: 0x53 }, + green: Rgb { r: 0xb9, g: 0xca, b: 0x4a }, + yellow: Rgb { r: 0xe7, g: 0xc5, b: 0x47 }, + blue: Rgb { r: 0x7a, g: 0xa6, b: 0xda }, + magenta: Rgb { r: 0xc3, g: 0x97, b: 0xd8 }, + cyan: Rgb { r: 0x70, g: 0xc0, b: 0xb1 }, + white: Rgb { r: 0xea, g: 0xea, b: 0xea }, + } + } +} + +#[derive(ConfigDeserialize, Clone, Debug, PartialEq, Eq)] +pub struct DimColors { + pub black: Rgb, + pub red: Rgb, + pub green: Rgb, + pub yellow: Rgb, + pub blue: Rgb, + pub magenta: Rgb, + pub cyan: Rgb, + pub white: Rgb, +} + +impl Default for DimColors { + fn default() -> Self { + DimColors { + black: Rgb { r: 0x13, g: 0x14, b: 0x15 }, + red: Rgb { r: 0x86, g: 0x43, b: 0x43 }, + green: Rgb { r: 0x77, g: 0x7c, b: 0x44 }, + yellow: Rgb { r: 0x9e, g: 0x82, b: 0x4c }, + blue: Rgb { r: 0x55, g: 0x6a, b: 0x7d }, + magenta: Rgb { r: 0x75, g: 0x61, b: 0x7b }, + cyan: Rgb { r: 0x5b, g: 0x7d, b: 0x78 }, + white: Rgb { r: 0x82, g: 0x84, b: 0x82 }, + } + } +} diff --git a/alacritty/src/config/mod.rs b/alacritty/src/config/mod.rs index 0673ffd5..a782f5fe 100644 --- a/alacritty/src/config/mod.rs +++ b/alacritty/src/config/mod.rs @@ -1,5 +1,5 @@ use std::fmt::{self, Display, Formatter}; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use std::{env, fs, io}; use log::{error, info}; @@ -9,6 +9,8 @@ use serde_yaml::Value; use alacritty_terminal::config::{Config as TermConfig, LOG_TARGET_CONFIG}; +pub mod bell; +pub mod color; pub mod debug; pub mod font; pub mod monitor; @@ -123,10 +125,10 @@ pub fn load(options: &Options) -> Config { } /// Attempt to reload the configuration file. -pub fn reload(config_path: &PathBuf, options: &Options) -> Result { +pub fn reload(config_path: &Path, options: &Options) -> Result { // Load config, propagating errors. let config_options = options.config_options().clone(); - let mut config = load_from(&config_path, config_options)?; + let mut config = load_from(config_path, config_options)?; // Override config with CLI options. options.override_config(&mut config); @@ -135,7 +137,7 @@ pub fn reload(config_path: &PathBuf, options: &Options) -> Result { } /// Load configuration file and log errors. -fn load_from(path: &PathBuf, cli_config: Value) -> Result { +fn load_from(path: &Path, cli_config: Value) -> Result { match read_config(path, cli_config) { Ok(config) => Ok(config), Err(err) => { @@ -146,7 +148,7 @@ fn load_from(path: &PathBuf, cli_config: Value) -> Result { } /// Deserialize configuration file from path. -fn read_config(path: &PathBuf, cli_config: Value) -> Result { +fn read_config(path: &Path, cli_config: Value) -> Result { let mut config_paths = Vec::new(); let mut config_value = parse_config(&path, &mut config_paths, IMPORT_RECURSION_LIMIT)?; @@ -162,7 +164,7 @@ fn read_config(path: &PathBuf, cli_config: Value) -> Result { /// Deserialize all configuration files as generic Value. fn parse_config( - path: &PathBuf, + path: &Path, config_paths: &mut Vec, recursion_limit: usize, ) -> Result { diff --git a/alacritty/src/config/ui_config.rs b/alacritty/src/config/ui_config.rs index 25f9fb91..b3b3021a 100644 --- a/alacritty/src/config/ui_config.rs +++ b/alacritty/src/config/ui_config.rs @@ -6,7 +6,9 @@ use serde::{Deserialize, Deserializer}; use alacritty_config_derive::ConfigDeserialize; use alacritty_terminal::config::{Percentage, LOG_TARGET_CONFIG}; +use crate::config::bell::BellConfig; use crate::config::bindings::{self, Binding, KeyBinding, MouseBinding}; +use crate::config::color::Colors; use crate::config::debug::Debug; use crate::config::font::Font; use crate::config::mouse::Mouse; @@ -31,6 +33,15 @@ pub struct UIConfig { /// Live config reload. pub live_config_reload: bool, + /// Bell configuration. + pub bell: BellConfig, + + /// RGB values for colors. + pub colors: Colors, + + /// Should draw bold text with brighter colors instead of bold font. + pub draw_bold_text_with_bright_colors: bool, + /// Path where config was loaded from. #[config(skip)] pub config_paths: Vec, @@ -58,6 +69,9 @@ impl Default for UIConfig { key_bindings: Default::default(), mouse_bindings: Default::default(), background_opacity: Default::default(), + bell: Default::default(), + colors: Default::default(), + draw_bold_text_with_bright_colors: Default::default(), } } } diff --git a/alacritty/src/cursor.rs b/alacritty/src/cursor.rs deleted file mode 100644 index a9fba66a..00000000 --- a/alacritty/src/cursor.rs +++ /dev/null @@ -1,92 +0,0 @@ -//! Convert a cursor into an iterator of rects. - -use alacritty_terminal::ansi::CursorShape; -use alacritty_terminal::term::color::Rgb; -use alacritty_terminal::term::render::RenderableCursor; -use alacritty_terminal::term::SizeInfo; - -use crate::renderer::rects::RenderRect; - -/// Trait for conversion into the iterator. -pub trait IntoRects { - /// Consume the cursor for an iterator of rects. - fn rects(self, size_info: &SizeInfo, thickness: f32) -> CursorRects; -} - -impl IntoRects for RenderableCursor { - fn rects(self, size_info: &SizeInfo, thickness: f32) -> CursorRects { - let point = self.point(); - let x = point.col.0 as f32 * size_info.cell_width() + size_info.padding_x(); - let y = point.line.0 as f32 * size_info.cell_height() + size_info.padding_y(); - - let mut width = size_info.cell_width(); - let height = size_info.cell_height(); - - if self.is_wide() { - width *= 2.; - } - - let thickness = (thickness * width as f32).round().max(1.); - - match self.shape() { - CursorShape::Beam => beam(x, y, height, thickness, self.color()), - CursorShape::Underline => underline(x, y, width, height, thickness, self.color()), - CursorShape::HollowBlock => hollow(x, y, width, height, thickness, self.color()), - _ => CursorRects::default(), - } - } -} - -/// Cursor rect iterator. -#[derive(Default)] -pub struct CursorRects { - rects: [Option; 4], - index: usize, -} - -impl From for CursorRects { - fn from(rect: RenderRect) -> Self { - Self { rects: [Some(rect), None, None, None], index: 0 } - } -} - -impl Iterator for CursorRects { - type Item = RenderRect; - - fn next(&mut self) -> Option { - let rect = self.rects.get_mut(self.index)?; - self.index += 1; - rect.take() - } -} - -/// Create an iterator yielding a single beam rect. -fn beam(x: f32, y: f32, height: f32, thickness: f32, color: Rgb) -> CursorRects { - RenderRect::new(x, y, thickness, height, color, 1.).into() -} - -/// Create an iterator yielding a single underline rect. -fn underline(x: f32, y: f32, width: f32, height: f32, thickness: f32, color: Rgb) -> CursorRects { - let y = y + height - thickness; - RenderRect::new(x, y, width, thickness, color, 1.).into() -} - -/// Create an iterator yielding a rect for each side of the hollow block cursor. -fn hollow(x: f32, y: f32, width: f32, height: f32, thickness: f32, color: Rgb) -> CursorRects { - let top_line = RenderRect::new(x, y, width, thickness, color, 1.); - - let vertical_y = y + thickness; - let vertical_height = height - 2. * thickness; - let left_line = RenderRect::new(x, vertical_y, thickness, vertical_height, color, 1.); - - let bottom_y = y + height - thickness; - let bottom_line = RenderRect::new(x, bottom_y, width, thickness, color, 1.); - - let right_x = x + width - thickness; - let right_line = RenderRect::new(right_x, vertical_y, thickness, vertical_height, color, 1.); - - CursorRects { - rects: [Some(top_line), Some(bottom_line), Some(left_line), Some(right_line)], - index: 0, - } -} diff --git a/alacritty/src/display.rs b/alacritty/src/display.rs deleted file mode 100644 index 5e885b53..00000000 --- a/alacritty/src/display.rs +++ /dev/null @@ -1,794 +0,0 @@ -//! The display subsystem including window management, font rasterization, and -//! GPU drawing. - -use std::cmp::min; -use std::f64; -use std::fmt::{self, Formatter}; -#[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))] -use std::sync::atomic::Ordering; -use std::time::Instant; - -use glutin::dpi::{PhysicalPosition, PhysicalSize}; -use glutin::event::ModifiersState; -use glutin::event_loop::EventLoop; -#[cfg(not(any(target_os = "macos", windows)))] -use glutin::platform::unix::EventLoopWindowTargetExtUnix; -use glutin::window::CursorIcon; -use log::{debug, info}; -use parking_lot::MutexGuard; -use unicode_width::UnicodeWidthChar; -#[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))] -use wayland_client::{Display as WaylandDisplay, EventQueue}; - -use crossfont::{self, Rasterize, Rasterizer}; - -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::{SizeInfo, Term, TermMode, MIN_COLS, MIN_SCREEN_LINES}; - -use crate::config::font::Font; -use crate::config::window::Dimensions; -#[cfg(not(windows))] -use crate::config::window::StartupMode; -use crate::config::Config; -use crate::cursor::IntoRects; -use crate::event::{Mouse, SearchState}; -use crate::message_bar::{MessageBuffer, MessageType}; -use crate::meter::Meter; -use crate::renderer::rects::{RenderLines, RenderRect}; -use crate::renderer::{self, GlyphCache, QuadRenderer}; -use crate::url::{Url, Urls}; -use crate::window::{self, Window}; - -const FORWARD_SEARCH_LABEL: &str = "Search: "; -const BACKWARD_SEARCH_LABEL: &str = "Backward Search: "; - -#[derive(Debug)] -pub enum Error { - /// Error with window management. - Window(window::Error), - - /// Error dealing with fonts. - Font(crossfont::Error), - - /// Error in renderer. - Render(renderer::Error), - - /// Error during buffer swap. - ContextError(glutin::ContextError), -} - -impl std::error::Error for Error { - fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - match self { - Error::Window(err) => err.source(), - Error::Font(err) => err.source(), - Error::Render(err) => err.source(), - Error::ContextError(err) => err.source(), - } - } -} - -impl fmt::Display for Error { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - match self { - Error::Window(err) => err.fmt(f), - Error::Font(err) => err.fmt(f), - Error::Render(err) => err.fmt(f), - Error::ContextError(err) => err.fmt(f), - } - } -} - -impl From for Error { - fn from(val: window::Error) -> Self { - Error::Window(val) - } -} - -impl From for Error { - fn from(val: crossfont::Error) -> Self { - Error::Font(val) - } -} - -impl From for Error { - fn from(val: renderer::Error) -> Self { - Error::Render(val) - } -} - -impl From for Error { - fn from(val: glutin::ContextError) -> Self { - Error::ContextError(val) - } -} - -#[derive(Default, Clone, Debug, PartialEq)] -pub struct DisplayUpdate { - pub dirty: bool, - - dimensions: Option>, - cursor_dirty: bool, - font: Option, -} - -impl DisplayUpdate { - pub fn dimensions(&self) -> Option> { - self.dimensions - } - - pub fn font(&self) -> Option<&Font> { - self.font.as_ref() - } - - pub fn cursor_dirty(&self) -> bool { - self.cursor_dirty - } - - pub fn set_dimensions(&mut self, dimensions: PhysicalSize) { - self.dimensions = Some(dimensions); - self.dirty = true; - } - - pub fn set_font(&mut self, font: Font) { - self.font = Some(font); - self.dirty = true; - } - - pub fn set_cursor_dirty(&mut self) { - self.cursor_dirty = true; - self.dirty = true; - } -} - -/// The display wraps a window, font rasterizer, and GPU renderer. -pub struct Display { - pub size_info: SizeInfo, - pub window: Window, - pub urls: Urls, - - /// Currently highlighted URL. - pub highlighted_url: Option, - - #[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))] - pub wayland_event_queue: Option, - - #[cfg(not(any(target_os = "macos", windows)))] - pub is_x11: bool, - - /// UI cursor visibility for blinking. - pub cursor_hidden: bool, - - renderer: QuadRenderer, - glyph_cache: GlyphCache, - meter: Meter, -} - -impl Display { - pub fn new(config: &Config, event_loop: &EventLoop) -> Result { - // Guess DPR based on first monitor. - let estimated_dpr = - event_loop.available_monitors().next().map(|m| m.scale_factor()).unwrap_or(1.); - - // Guess the target window dimensions. - let metrics = GlyphCache::static_metrics(config.ui_config.font.clone(), estimated_dpr)?; - let (cell_width, cell_height) = compute_cell_size(config, &metrics); - - // Guess the target window size if the user has specified the number of lines/columns. - let dimensions = config.ui_config.window.dimensions(); - let estimated_size = dimensions.map(|dimensions| { - window_size(config, dimensions, cell_width, cell_height, estimated_dpr) - }); - - debug!("Estimated DPR: {}", estimated_dpr); - debug!("Estimated window size: {:?}", estimated_size); - debug!("Estimated cell size: {} x {}", cell_width, cell_height); - - #[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))] - let mut wayland_event_queue = None; - - // Initialize Wayland event queue, to handle Wayland callbacks. - #[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))] - if let Some(display) = event_loop.wayland_display() { - let display = unsafe { WaylandDisplay::from_external_display(display as _) }; - wayland_event_queue = Some(display.create_event_queue()); - } - - // Spawn the Alacritty window. - let mut window = Window::new( - event_loop, - &config, - estimated_size, - #[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))] - wayland_event_queue.as_ref(), - )?; - - info!("Device pixel ratio: {}", window.dpr); - - // Create renderer. - let mut renderer = QuadRenderer::new()?; - - let (glyph_cache, cell_width, cell_height) = - Self::new_glyph_cache(window.dpr, &mut renderer, config)?; - - if let Some(dimensions) = dimensions { - if (estimated_dpr - window.dpr).abs() < f64::EPSILON { - info!("Estimated DPR correctly, skipping resize"); - } else { - // Resize the window again if the DPR was not estimated correctly. - let size = window_size(config, dimensions, cell_width, cell_height, window.dpr); - window.set_inner_size(size); - } - } - - let padding = config.ui_config.window.padding(window.dpr); - let viewport_size = window.inner_size(); - - // Create new size with at least one column and row. - let size_info = SizeInfo::new( - viewport_size.width as f32, - viewport_size.height as f32, - cell_width, - cell_height, - padding.0, - padding.1, - config.ui_config.window.dynamic_padding && dimensions.is_none(), - ); - - info!("Cell size: {} x {}", cell_width, cell_height); - info!("Padding: {} x {}", size_info.padding_x(), size_info.padding_y()); - info!("Width: {}, Height: {}", size_info.width(), size_info.height()); - - // Update OpenGL projection. - renderer.resize(&size_info); - - // Clear screen. - let background_color = config.colors.primary.background; - renderer.with_api(&config.ui_config, &size_info, |api| { - api.clear(background_color); - }); - - // Set subpixel anti-aliasing. - #[cfg(target_os = "macos")] - crossfont::set_font_smoothing(config.ui_config.font.use_thin_strokes); - - // Disable shadows for transparent windows on macOS. - #[cfg(target_os = "macos")] - window.set_has_shadow(config.ui_config.background_opacity() >= 1.0); - - #[cfg(all(feature = "x11", not(any(target_os = "macos", windows))))] - let is_x11 = event_loop.is_x11(); - #[cfg(not(any(feature = "x11", target_os = "macos", windows)))] - let is_x11 = false; - - // On Wayland we can safely ignore this call, since the window isn't visible until you - // actually draw something into it and commit those changes. - #[cfg(not(any(target_os = "macos", windows)))] - if is_x11 { - window.swap_buffers(); - renderer.with_api(&config.ui_config, &size_info, |api| { - api.finish(); - }); - } - - window.set_visible(true); - - // Set window position. - // - // TODO: replace `set_position` with `with_position` once available. - // Upstream issue: https://github.com/rust-windowing/winit/issues/806. - if let Some(position) = config.ui_config.window.position { - window.set_outer_position(PhysicalPosition::from((position.x, position.y))); - } - - #[allow(clippy::single_match)] - #[cfg(not(windows))] - match config.ui_config.window.startup_mode { - #[cfg(target_os = "macos")] - StartupMode::SimpleFullscreen => window.set_simple_fullscreen(true), - #[cfg(not(target_os = "macos"))] - StartupMode::Maximized if is_x11 => window.set_maximized(true), - _ => (), - } - - Ok(Self { - window, - renderer, - glyph_cache, - meter: Meter::new(), - size_info, - urls: Urls::new(), - highlighted_url: None, - #[cfg(not(any(target_os = "macos", windows)))] - is_x11, - #[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))] - wayland_event_queue, - cursor_hidden: false, - }) - } - - fn new_glyph_cache( - dpr: f64, - renderer: &mut QuadRenderer, - config: &Config, - ) -> Result<(GlyphCache, f32, f32), Error> { - let font = config.ui_config.font.clone(); - let rasterizer = Rasterizer::new(dpr as f32, config.ui_config.font.use_thin_strokes)?; - - // Initialize glyph cache. - let glyph_cache = { - info!("Initializing glyph cache..."); - let init_start = Instant::now(); - - let cache = - renderer.with_loader(|mut api| GlyphCache::new(rasterizer, &font, &mut api))?; - - let stop = init_start.elapsed(); - let stop_f = stop.as_secs() as f64 + f64::from(stop.subsec_nanos()) / 1_000_000_000f64; - info!("... finished initializing glyph cache in {}s", stop_f); - - cache - }; - - // Need font metrics to resize the window properly. This suggests to me the - // font metrics should be computed before creating the window in the first - // place so that a resize is not needed. - let (cw, ch) = compute_cell_size(config, &glyph_cache.font_metrics()); - - Ok((glyph_cache, cw, ch)) - } - - /// Update font size and cell dimensions. - /// - /// This will return a tuple of the cell width and height. - fn update_glyph_cache(&mut self, config: &Config, font: &Font) -> (f32, f32) { - let cache = &mut self.glyph_cache; - let dpr = self.window.dpr; - - self.renderer.with_loader(|mut api| { - let _ = cache.update_font_size(font, dpr, &mut api); - }); - - // Compute new cell sizes. - compute_cell_size(config, &self.glyph_cache.font_metrics()) - } - - /// Clear glyph cache. - fn clear_glyph_cache(&mut self) { - let cache = &mut self.glyph_cache; - self.renderer.with_loader(|mut api| { - cache.clear_glyph_cache(&mut api); - }); - } - - /// Process update events. - pub fn handle_update( - &mut self, - terminal: &mut Term, - pty_resize_handle: &mut dyn OnResize, - message_buffer: &MessageBuffer, - search_active: bool, - config: &Config, - update_pending: DisplayUpdate, - ) where - T: EventListener, - { - let (mut cell_width, mut cell_height) = - (self.size_info.cell_width(), self.size_info.cell_height()); - - // Update font size and cell dimensions. - if let Some(font) = update_pending.font() { - let cell_dimensions = self.update_glyph_cache(config, font); - cell_width = cell_dimensions.0; - cell_height = cell_dimensions.1; - - info!("Cell size: {} x {}", cell_width, cell_height); - } else if update_pending.cursor_dirty() { - self.clear_glyph_cache(); - } - - let (mut width, mut height) = (self.size_info.width(), self.size_info.height()); - if let Some(dimensions) = update_pending.dimensions() { - width = dimensions.width as f32; - height = dimensions.height as f32; - } - - let padding = config.ui_config.window.padding(self.window.dpr); - - self.size_info = SizeInfo::new( - width, - height, - cell_width, - cell_height, - padding.0, - padding.1, - config.ui_config.window.dynamic_padding, - ); - - // Update number of column/lines in the viewport. - let message_bar_lines = - message_buffer.message().map(|m| m.text(&self.size_info).len()).unwrap_or(0); - let search_lines = if search_active { 1 } else { 0 }; - self.size_info.reserve_lines(message_bar_lines + search_lines); - - // Resize PTY. - pty_resize_handle.on_resize(&self.size_info); - - // Resize terminal. - terminal.resize(self.size_info); - - // Resize renderer. - let physical = - PhysicalSize::new(self.size_info.width() as u32, self.size_info.height() as u32); - self.window.resize(physical); - self.renderer.resize(&self.size_info); - - info!("Padding: {} x {}", self.size_info.padding_x(), self.size_info.padding_y()); - info!("Width: {}, Height: {}", self.size_info.width(), self.size_info.height()); - } - - /// Draw the screen. - /// - /// A reference to Term whose state is being drawn must be provided. - /// - /// This call may block if vsync is enabled. - pub fn draw( - &mut self, - terminal: MutexGuard<'_, Term>, - message_buffer: &MessageBuffer, - config: &Config, - mouse: &Mouse, - mods: ModifiersState, - search_state: &SearchState, - ) { - // Convert search match from viewport to absolute indexing. - let search_active = search_state.regex().is_some(); - let viewport_match = search_state - .focused_match() - .and_then(|focused_match| terminal.grid().clamp_buffer_range_to_visible(focused_match)); - let cursor_hidden = self.cursor_hidden || search_state.regex().is_some(); - - // Collect renderable content before the terminal is dropped. - let mut content = terminal.renderable_content(config, !cursor_hidden); - let mut grid_cells = Vec::new(); - while let Some(cell) = content.next() { - grid_cells.push(cell); - } - let cursor = content.cursor(); - - let visual_bell_intensity = terminal.visual_bell.intensity(); - let display_offset = terminal.grid().display_offset(); - let background_color = terminal.background_color(); - let cursor_point = terminal.grid().cursor.point; - let total_lines = terminal.grid().total_lines(); - 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 }; - - // Drop terminal as early as possible to free lock. - drop(terminal); - - self.renderer.with_api(&config.ui_config, &size_info, |api| { - api.clear(background_color); - }); - - let mut lines = RenderLines::new(); - let mut urls = Urls::new(); - - // Draw grid. - { - let _sampler = self.meter.sampler(); - - let glyph_cache = &mut self.glyph_cache; - self.renderer.with_api(&config.ui_config, &size_info, |mut api| { - // Iterate over all non-empty cells in the grid. - for mut cell in grid_cells { - // Invert the active match during search. - if cell.is_match - && viewport_match - .as_ref() - .map_or(false, |viewport_match| viewport_match.contains(&cell.point())) - { - let colors = config.colors.search.focused_match; - let match_fg = colors.foreground.color(cell.fg, cell.bg); - cell.bg = colors.background.color(cell.fg, cell.bg); - cell.fg = match_fg; - cell.bg_alpha = 1.0; - } - - // Update URL underlines. - urls.update(size_info.cols(), &cell); - - // Update underline/strikeout. - lines.update(&cell); - - // Draw the cell. - api.render_cell(cell, glyph_cache); - } - }); - } - - 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_mode_point = vi_mode_cursor.point; - if let Some(url) = self.urls.find_at(vi_mode_point) { - rects.append(&mut url.rects(&metrics, &size_info)); - } - - // Indicate vi mode by showing the cursor's position in the top right corner. - let line = size_info.screen_lines() + display_offset - vi_mode_point.line - 1; - self.draw_line_indicator(config, &size_info, total_lines, Some(vi_mode_point), line.0); - } else if search_active { - // Show current display offset in vi-less search to indicate match position. - self.draw_line_indicator(config, &size_info, total_lines, None, display_offset); - } - - // Push the cursor rects for rendering. - if let Some(cursor) = cursor { - for rect in cursor.rects(&size_info, config.cursor.thickness()) { - rects.push(rect); - } - } - - // Push visual bell after url/underline/strikeout rects. - if visual_bell_intensity != 0. { - let visual_bell_rect = RenderRect::new( - 0., - 0., - size_info.width(), - size_info.height(), - config.bell().color, - visual_bell_intensity as f32, - ); - rects.push(visual_bell_rect); - } - - if let Some(message) = message_buffer.message() { - let search_offset = if search_active { 1 } else { 0 }; - let text = message.text(&size_info); - - // Create a new rectangle for the background. - let start_line = size_info.screen_lines() + search_offset; - let y = size_info.cell_height().mul_add(start_line.0 as f32, size_info.padding_y()); - - let bg = match message.ty() { - MessageType::Error => config.colors.normal.red, - MessageType::Warning => config.colors.normal.yellow, - }; - - let message_bar_rect = - RenderRect::new(0., y, size_info.width(), size_info.height() - y, bg, 1.); - - // Push message_bar in the end, so it'll be above all other content. - rects.push(message_bar_rect); - - // Draw rectangles. - self.renderer.draw_rects(&size_info, rects); - - // Relay messages to the user. - let glyph_cache = &mut self.glyph_cache; - 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.with_api(&config.ui_config, &size_info, |mut api| { - api.render_string(glyph_cache, point, fg, bg, &message_text); - }); - } - } else { - // Draw rectangles. - self.renderer.draw_rects(&size_info, rects); - } - - self.draw_render_timer(config, &size_info); - - // Handle search and IME positioning. - let ime_position = match search_state.regex() { - Some(regex) => { - let search_label = match search_state.direction() { - Direction::Right => FORWARD_SEARCH_LABEL, - Direction::Left => BACKWARD_SEARCH_LABEL, - }; - - let search_text = Self::format_search(&size_info, regex, search_label); - - // Render the search bar. - self.draw_search(config, &size_info, &search_text); - - // Compute IME position. - Point::new(size_info.screen_lines() + 1, Column(search_text.chars().count() - 1)) - }, - None => cursor_point, - }; - - // Update IME position. - self.window.update_ime_position(ime_position, &self.size_info); - - // Frame event should be requested before swaping buffers, since it requires surface - // `commit`, which is done by swap buffers under the hood. - #[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))] - self.request_frame(&self.window); - - self.window.swap_buffers(); - - #[cfg(all(feature = "x11", not(any(target_os = "macos", windows))))] - if self.is_x11 { - // On X11 `swap_buffers` does not block for vsync. However the next OpenGl command - // will block to synchronize (this is `glClear` in Alacritty), which causes a - // permanent one frame delay. - self.renderer.with_api(&config.ui_config, &size_info, |api| { - api.finish(); - }); - } - } - - /// 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(' '); - } - } - - // Add cursor to show whitespace. - formatted_regex.push('_'); - - // Truncate beginning of the search regex if it exceeds the viewport width. - let num_cols = size_info.cols().0; - let label_len = search_label.chars().count(); - let regex_len = formatted_regex.chars().count(); - let truncate_len = 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..]; - - // Add search label to the beginning of the search regex. - let mut bar_text = format!("{}{}", search_label, truncated_regex); - - // Make sure the label alone doesn't exceed the viewport width. - bar_text.truncate(num_cols); - - bar_text - } - - /// Draw current search regex. - fn draw_search(&mut self, config: &Config, size_info: &SizeInfo, text: &str) { - let glyph_cache = &mut self.glyph_cache; - let num_cols = size_info.cols().0; - - // Assure text length is at least num_cols. - 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(); - - self.renderer.with_api(&config.ui_config, &size_info, |mut api| { - api.render_string(glyph_cache, point, fg, bg, &text); - }); - } - - /// Draw render timer. - fn draw_render_timer(&mut self, config: &Config, size_info: &SizeInfo) { - if !config.ui_config.debug.render_timer { - return; - } - - let glyph_cache = &mut self.glyph_cache; - - let timing = format!("{:.3} usec", self.meter.average()); - let point = Point::new(size_info.screen_lines() - 2, Column(0)); - let fg = config.colors.primary.background; - let bg = config.colors.normal.red; - - self.renderer.with_api(&config.ui_config, &size_info, |mut api| { - api.render_string(glyph_cache, point, fg, bg, &timing); - }); - } - - /// Draw an indicator for the position of a line in history. - fn draw_line_indicator( - &mut self, - config: &Config, - size_info: &SizeInfo, - total_lines: usize, - vi_mode_point: Option, - line: usize, - ) { - let text = format!("[{}/{}]", line, total_lines - 1); - let column = Column(size_info.cols().0.saturating_sub(text.len())); - let colors = &config.colors; - let fg = colors.line_indicator.foreground.unwrap_or(colors.primary.background); - let bg = colors.line_indicator.background.unwrap_or(colors.primary.foreground); - - // Do not render anything if it would obscure the vi mode cursor. - if vi_mode_point.map_or(true, |point| point.line.0 != 0 || point.col < column) { - let glyph_cache = &mut self.glyph_cache; - self.renderer.with_api(&config.ui_config, &size_info, |mut api| { - api.render_string(glyph_cache, Point::new(Line(0), column), fg, bg, &text); - }); - } - } - - /// Requst a new frame for a window on Wayland. - #[inline] - #[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))] - fn request_frame(&self, window: &Window) { - let surface = match window.wayland_surface() { - Some(surface) => surface, - None => return, - }; - - let should_draw = self.window.should_draw.clone(); - - // Mark that window was drawn. - should_draw.store(false, Ordering::Relaxed); - - // Request a new frame. - surface.frame().quick_assign(move |_, _, _| { - should_draw.store(true, Ordering::Relaxed); - }); - } -} - -/// Calculate the cell dimensions based on font metrics. -/// -/// This will return a tuple of the cell width and height. -#[inline] -fn compute_cell_size(config: &Config, metrics: &crossfont::Metrics) -> (f32, f32) { - let offset_x = f64::from(config.ui_config.font.offset.x); - let offset_y = f64::from(config.ui_config.font.offset.y); - ( - (metrics.average_advance + offset_x).floor().max(1.) as f32, - (metrics.line_height + offset_y).floor().max(1.) as f32, - ) -} - -/// Calculate the size of the window given padding, terminal dimensions and cell size. -fn window_size( - config: &Config, - dimensions: Dimensions, - cell_width: f32, - cell_height: f32, - dpr: f64, -) -> PhysicalSize { - let padding = config.ui_config.window.padding(dpr); - - let grid_width = cell_width * dimensions.columns.0.max(MIN_COLS) as f32; - let grid_height = cell_height * dimensions.lines.0.max(MIN_SCREEN_LINES) as f32; - - let width = (padding.0).mul_add(2., grid_width).floor(); - let height = (padding.1).mul_add(2., grid_height).floor(); - - PhysicalSize::new(width as u32, height as u32) -} diff --git a/alacritty/src/display/bell.rs b/alacritty/src/display/bell.rs new file mode 100644 index 00000000..1aee3ba6 --- /dev/null +++ b/alacritty/src/display/bell.rs @@ -0,0 +1,122 @@ +use std::time::{Duration, Instant}; + +use crate::config::bell::{BellAnimation, BellConfig}; + +pub struct VisualBell { + /// Visual bell animation. + animation: BellAnimation, + + /// Visual bell duration. + duration: Duration, + + /// The last time the visual bell rang, if at all. + start_time: Option, +} + +impl VisualBell { + /// Ring the visual bell, and return its intensity. + pub fn ring(&mut self) -> f64 { + let now = Instant::now(); + self.start_time = Some(now); + self.intensity_at_instant(now) + } + + /// Get the currently intensity of the visual bell. The bell's intensity + /// ramps down from 1.0 to 0.0 at a rate determined by the bell's duration. + pub fn intensity(&self) -> f64 { + self.intensity_at_instant(Instant::now()) + } + + /// Check whether or not the visual bell has completed "ringing". + pub fn completed(&mut self) -> bool { + match self.start_time { + Some(earlier) => { + if Instant::now().duration_since(earlier) >= self.duration { + self.start_time = None; + } + false + }, + None => true, + } + } + + /// Get the intensity of the visual bell at a particular instant. The bell's + /// intensity ramps down from 1.0 to 0.0 at a rate determined by the bell's + /// duration. + pub fn intensity_at_instant(&self, instant: Instant) -> f64 { + // If `duration` is zero, then the VisualBell is disabled; therefore, + // its `intensity` is zero. + if self.duration == Duration::from_secs(0) { + return 0.0; + } + + match self.start_time { + // Similarly, if `start_time` is `None`, then the VisualBell has not + // been "rung"; therefore, its `intensity` is zero. + None => 0.0, + + Some(earlier) => { + // Finally, if the `instant` at which we wish to compute the + // VisualBell's `intensity` occurred before the VisualBell was + // "rung", then its `intensity` is also zero. + if instant < earlier { + return 0.0; + } + + let elapsed = instant.duration_since(earlier); + let elapsed_f = + elapsed.as_secs() as f64 + f64::from(elapsed.subsec_nanos()) / 1e9f64; + let duration_f = self.duration.as_secs() as f64 + + f64::from(self.duration.subsec_nanos()) / 1e9f64; + + // Otherwise, we compute a value `time` from 0.0 to 1.0 + // inclusive that represents the ratio of `elapsed` time to the + // `duration` of the VisualBell. + let time = (elapsed_f / duration_f).min(1.0); + + // We use this to compute the inverse `intensity` of the + // VisualBell. When `time` is 0.0, `inverse_intensity` is 0.0, + // and when `time` is 1.0, `inverse_intensity` is 1.0. + let inverse_intensity = match self.animation { + BellAnimation::Ease | BellAnimation::EaseOut => { + cubic_bezier(0.25, 0.1, 0.25, 1.0, time) + }, + BellAnimation::EaseOutSine => cubic_bezier(0.39, 0.575, 0.565, 1.0, time), + BellAnimation::EaseOutQuad => cubic_bezier(0.25, 0.46, 0.45, 0.94, time), + BellAnimation::EaseOutCubic => cubic_bezier(0.215, 0.61, 0.355, 1.0, time), + BellAnimation::EaseOutQuart => cubic_bezier(0.165, 0.84, 0.44, 1.0, time), + BellAnimation::EaseOutQuint => cubic_bezier(0.23, 1.0, 0.32, 1.0, time), + BellAnimation::EaseOutExpo => cubic_bezier(0.19, 1.0, 0.22, 1.0, time), + BellAnimation::EaseOutCirc => cubic_bezier(0.075, 0.82, 0.165, 1.0, time), + BellAnimation::Linear => time, + }; + + // Since we want the `intensity` of the VisualBell to decay over + // `time`, we subtract the `inverse_intensity` from 1.0. + 1.0 - inverse_intensity + }, + } + } + + pub fn update_config(&mut self, bell_config: &BellConfig) { + self.animation = bell_config.animation; + self.duration = bell_config.duration(); + } +} + +impl From<&BellConfig> for VisualBell { + fn from(bell_config: &BellConfig) -> VisualBell { + VisualBell { + animation: bell_config.animation, + duration: bell_config.duration(), + start_time: None, + } + } +} + +fn cubic_bezier(p0: f64, p1: f64, p2: f64, p3: f64, x: f64) -> f64 { + (1.0 - x).powi(3) * p0 + + 3.0 * (1.0 - x).powi(2) * x * p1 + + 3.0 * (1.0 - x) * x.powi(2) * p2 + + x.powi(3) * p3 +} diff --git a/alacritty/src/display/color.rs b/alacritty/src/display/color.rs new file mode 100644 index 00000000..6e0de048 --- /dev/null +++ b/alacritty/src/display/color.rs @@ -0,0 +1,167 @@ +use std::ops::{Index, IndexMut}; + +use log::trace; + +use alacritty_terminal::ansi::NamedColor; +use alacritty_terminal::term::color::{Rgb, COUNT}; + +use crate::config::color::Colors; + +/// Factor for automatic computation of dim colors. +pub const DIM_FACTOR: f32 = 0.66; + +#[derive(Copy, Clone)] +pub struct List([Rgb; COUNT]); + +impl<'a> From<&'a Colors> for List { + fn from(colors: &Colors) -> List { + // Type inference fails without this annotation. + let mut list = List([Rgb::default(); COUNT]); + + list.fill_named(colors); + list.fill_cube(colors); + list.fill_gray_ramp(colors); + + list + } +} + +impl List { + pub fn fill_named(&mut self, colors: &Colors) { + // Normals. + self[NamedColor::Black] = colors.normal.black; + self[NamedColor::Red] = colors.normal.red; + self[NamedColor::Green] = colors.normal.green; + self[NamedColor::Yellow] = colors.normal.yellow; + self[NamedColor::Blue] = colors.normal.blue; + self[NamedColor::Magenta] = colors.normal.magenta; + self[NamedColor::Cyan] = colors.normal.cyan; + self[NamedColor::White] = colors.normal.white; + + // Brights. + self[NamedColor::BrightBlack] = colors.bright.black; + self[NamedColor::BrightRed] = colors.bright.red; + self[NamedColor::BrightGreen] = colors.bright.green; + self[NamedColor::BrightYellow] = colors.bright.yellow; + self[NamedColor::BrightBlue] = colors.bright.blue; + self[NamedColor::BrightMagenta] = colors.bright.magenta; + self[NamedColor::BrightCyan] = colors.bright.cyan; + self[NamedColor::BrightWhite] = colors.bright.white; + self[NamedColor::BrightForeground] = + colors.primary.bright_foreground.unwrap_or(colors.primary.foreground); + + // Foreground and background. + self[NamedColor::Foreground] = colors.primary.foreground; + self[NamedColor::Background] = colors.primary.background; + + // Dims. + self[NamedColor::DimForeground] = + colors.primary.dim_foreground.unwrap_or(colors.primary.foreground * DIM_FACTOR); + match colors.dim { + Some(ref dim) => { + trace!("Using config-provided dim colors"); + self[NamedColor::DimBlack] = dim.black; + self[NamedColor::DimRed] = dim.red; + self[NamedColor::DimGreen] = dim.green; + self[NamedColor::DimYellow] = dim.yellow; + self[NamedColor::DimBlue] = dim.blue; + self[NamedColor::DimMagenta] = dim.magenta; + self[NamedColor::DimCyan] = dim.cyan; + self[NamedColor::DimWhite] = dim.white; + }, + None => { + trace!("Deriving dim colors from normal colors"); + self[NamedColor::DimBlack] = colors.normal.black * DIM_FACTOR; + self[NamedColor::DimRed] = colors.normal.red * DIM_FACTOR; + self[NamedColor::DimGreen] = colors.normal.green * DIM_FACTOR; + self[NamedColor::DimYellow] = colors.normal.yellow * DIM_FACTOR; + self[NamedColor::DimBlue] = colors.normal.blue * DIM_FACTOR; + self[NamedColor::DimMagenta] = colors.normal.magenta * DIM_FACTOR; + self[NamedColor::DimCyan] = colors.normal.cyan * DIM_FACTOR; + self[NamedColor::DimWhite] = colors.normal.white * DIM_FACTOR; + }, + } + } + + pub fn fill_cube(&mut self, colors: &Colors) { + let mut index: usize = 16; + // Build colors. + for r in 0..6 { + for g in 0..6 { + for b in 0..6 { + // Override colors 16..232 with the config (if present). + if let Some(indexed_color) = + colors.indexed_colors.iter().find(|ic| ic.index() == index as u8) + { + self[index] = indexed_color.color; + } else { + self[index] = Rgb { + r: if r == 0 { 0 } else { r * 40 + 55 }, + b: if b == 0 { 0 } else { b * 40 + 55 }, + g: if g == 0 { 0 } else { g * 40 + 55 }, + }; + } + index += 1; + } + } + } + + debug_assert!(index == 232); + } + + pub fn fill_gray_ramp(&mut self, colors: &Colors) { + let mut index: usize = 232; + + for i in 0..24 { + // Index of the color is number of named colors + number of cube colors + i. + let color_index = 16 + 216 + i; + + // Override colors 232..256 with the config (if present). + if let Some(indexed_color) = + colors.indexed_colors.iter().find(|ic| ic.index() == color_index) + { + self[index] = indexed_color.color; + index += 1; + continue; + } + + let value = i * 10 + 8; + self[index] = Rgb { r: value, g: value, b: value }; + index += 1; + } + + debug_assert!(index == 256); + } +} + +impl Index for List { + type Output = Rgb; + + #[inline] + fn index(&self, idx: usize) -> &Self::Output { + &self.0[idx] + } +} + +impl IndexMut for List { + #[inline] + fn index_mut(&mut self, idx: usize) -> &mut Self::Output { + &mut self.0[idx] + } +} + +impl Index for List { + type Output = Rgb; + + #[inline] + fn index(&self, idx: NamedColor) -> &Self::Output { + &self.0[idx as usize] + } +} + +impl IndexMut for List { + #[inline] + fn index_mut(&mut self, idx: NamedColor) -> &mut Self::Output { + &mut self.0[idx as usize] + } +} diff --git a/alacritty/src/display/content.rs b/alacritty/src/display/content.rs new file mode 100644 index 00000000..81c2977f --- /dev/null +++ b/alacritty/src/display/content.rs @@ -0,0 +1,404 @@ +use std::cmp::max; +use std::mem; +use std::ops::RangeInclusive; + +use alacritty_terminal::ansi::{Color, CursorShape, NamedColor}; +use alacritty_terminal::config::Config; +use alacritty_terminal::event::EventListener; +use alacritty_terminal::grid::{Dimensions, Indexed}; +use alacritty_terminal::index::{Column, Direction, Line, Point}; +use alacritty_terminal::term::cell::{Cell, Flags}; +use alacritty_terminal::term::color::{CellRgb, Rgb}; +use alacritty_terminal::term::search::{RegexIter, RegexSearch}; +use alacritty_terminal::term::{ + RenderableContent as TerminalContent, RenderableCursor as TerminalCursor, Term, TermMode, +}; + +use crate::config::ui_config::UIConfig; +use crate::display::color::{List, DIM_FACTOR}; + +/// Minimum contrast between a fixed cursor color and the cell's background. +pub const MIN_CURSOR_CONTRAST: f64 = 1.5; + +/// Maximum number of linewraps followed outside of the viewport during search highlighting. +const MAX_SEARCH_LINES: usize = 100; + +/// Renderable terminal content. +/// +/// This provides the terminal cursor and an iterator over all non-empty cells. +pub struct RenderableContent<'a> { + terminal_content: TerminalContent<'a>, + terminal_cursor: TerminalCursor, + cursor: Option, + search: RenderableSearch, + config: &'a Config, + colors: &'a List, +} + +impl<'a> RenderableContent<'a> { + pub fn new( + term: &'a Term, + dfas: Option<&RegexSearch>, + config: &'a Config, + colors: &'a List, + show_cursor: bool, + ) -> Self { + let search = dfas.map(|dfas| RenderableSearch::new(&term, dfas)).unwrap_or_default(); + let terminal_content = term.renderable_content(); + + // Copy the cursor and override its shape if necessary. + let mut terminal_cursor = terminal_content.cursor; + if !show_cursor { + terminal_cursor.shape = CursorShape::Hidden; + } else if !term.is_focused && config.cursor.unfocused_hollow { + terminal_cursor.shape = CursorShape::HollowBlock; + } + + Self { cursor: None, terminal_content, terminal_cursor, search, config, colors } + } + + /// Viewport offset. + pub fn display_offset(&self) -> usize { + self.terminal_content.display_offset + } + + /// Get the terminal cursor. + pub fn cursor(mut self) -> Option { + // Drain the iterator to make sure the cursor is created. + while self.next().is_some() && self.cursor.is_none() {} + + self.cursor + } + + /// Get the RGB value for a color index. + pub fn color(&self, color: usize) -> Rgb { + self.terminal_content.colors[color].unwrap_or(self.colors[color]) + } + + /// Assemble the information required to render the terminal cursor. + /// + /// This will return `None` when there is no cursor visible. + fn renderable_cursor(&mut self, cell: &RenderableCell) -> Option { + if self.terminal_cursor.shape == CursorShape::Hidden { + return None; + } + + // Expand across wide cell when inside wide char or spacer. + let is_wide = if cell.flags.contains(Flags::WIDE_CHAR_SPACER) { + self.terminal_cursor.point.column -= 1; + true + } else { + cell.flags.contains(Flags::WIDE_CHAR) + }; + + // Cursor colors. + let color = if self.terminal_content.mode.contains(TermMode::VI) { + self.config.ui_config.colors.vi_mode_cursor + } else { + self.config.ui_config.colors.cursor + }; + let mut cursor_color = + self.terminal_content.colors[NamedColor::Cursor].map_or(color.background, CellRgb::Rgb); + let mut text_color = color.foreground; + + // Invert the cursor if it has a fixed background close to the cell's background. + if matches!( + cursor_color, + CellRgb::Rgb(color) if color.contrast(cell.bg) < MIN_CURSOR_CONTRAST + ) { + cursor_color = CellRgb::CellForeground; + text_color = CellRgb::CellBackground; + } + + // Convert from cell colors to RGB. + let text_color = text_color.color(cell.fg, cell.bg); + let cursor_color = cursor_color.color(cell.fg, cell.bg); + + Some(RenderableCursor { + point: self.terminal_cursor.point, + shape: self.terminal_cursor.shape, + cursor_color, + text_color, + is_wide, + }) + } +} + +impl<'a> Iterator for RenderableContent<'a> { + type Item = RenderableCell; + + /// Gets the next renderable cell. + /// + /// Skips empty (background) cells and applies any flags to the cell state + /// (eg. invert fg and bg colors). + #[inline] + fn next(&mut self) -> Option { + loop { + let cell = self.terminal_content.display_iter.next()?; + let mut cell = RenderableCell::new(self, cell); + + if self.terminal_cursor.point == cell.point { + // Store the cursor which should be rendered. + self.cursor = self.renderable_cursor(&cell).map(|cursor| { + if cursor.shape == CursorShape::Block { + cell.fg = cursor.text_color; + cell.bg = cursor.cursor_color; + + // Since we draw Block cursor by drawing cell below it with a proper color, + // we must adjust alpha to make it visible. + cell.bg_alpha = 1.; + } + + cursor + }); + + return Some(cell); + } else if !cell.is_empty() && !cell.flags.contains(Flags::WIDE_CHAR_SPACER) { + // Skip empty cells and wide char spacers. + return Some(cell); + } + } + } +} + +/// Cell ready for rendering. +#[derive(Clone, Debug)] +pub struct RenderableCell { + pub character: char, + pub zerowidth: Option>, + pub point: Point, + pub fg: Rgb, + pub bg: Rgb, + pub bg_alpha: f32, + pub flags: Flags, + pub is_match: bool, +} + +impl RenderableCell { + fn new<'a>(content: &mut RenderableContent<'a>, cell: Indexed<&Cell, Line>) -> Self { + // Lookup RGB values. + let mut fg_rgb = Self::compute_fg_rgb(content, cell.fg, cell.flags); + let mut bg_rgb = Self::compute_bg_rgb(content, cell.bg); + + let mut bg_alpha = if cell.flags.contains(Flags::INVERSE) { + mem::swap(&mut fg_rgb, &mut bg_rgb); + 1.0 + } else { + Self::compute_bg_alpha(cell.bg) + }; + + let is_selected = content + .terminal_content + .selection + .map_or(false, |selection| selection.contains_cell(&cell, content.terminal_cursor)); + let mut is_match = false; + + let colors = &content.config.ui_config.colors; + if is_selected { + let config_bg = colors.selection.background; + let selected_fg = colors.selection.foreground.color(fg_rgb, bg_rgb); + bg_rgb = config_bg.color(fg_rgb, bg_rgb); + fg_rgb = selected_fg; + + if fg_rgb == bg_rgb && !cell.flags.contains(Flags::HIDDEN) { + // Reveal inversed text when fg/bg is the same. + fg_rgb = content.color(NamedColor::Background as usize); + bg_rgb = content.color(NamedColor::Foreground as usize); + bg_alpha = 1.0; + } else if config_bg != CellRgb::CellBackground { + bg_alpha = 1.0; + } + } else if content.search.advance(cell.point) { + // Highlight the cell if it is part of a search match. + let config_bg = colors.search.matches.background; + let matched_fg = colors.search.matches.foreground.color(fg_rgb, bg_rgb); + bg_rgb = config_bg.color(fg_rgb, bg_rgb); + fg_rgb = matched_fg; + + if config_bg != CellRgb::CellBackground { + bg_alpha = 1.0; + } + + is_match = true; + } + + RenderableCell { + character: cell.c, + zerowidth: cell.zerowidth().map(|zerowidth| zerowidth.to_vec()), + point: cell.point, + fg: fg_rgb, + bg: bg_rgb, + bg_alpha, + flags: cell.flags, + is_match, + } + } + + /// Check if cell contains any renderable content. + fn is_empty(&self) -> bool { + self.bg_alpha == 0. + && !self.flags.intersects(Flags::UNDERLINE | Flags::STRIKEOUT | Flags::DOUBLE_UNDERLINE) + && self.character == ' ' + && self.zerowidth.is_none() + } + + /// Get the RGB color from a cell's foreground color. + fn compute_fg_rgb(content: &mut RenderableContent<'_>, fg: Color, flags: Flags) -> Rgb { + let ui_config = &content.config.ui_config; + match fg { + Color::Spec(rgb) => match flags & Flags::DIM { + Flags::DIM => rgb * DIM_FACTOR, + _ => rgb, + }, + Color::Named(ansi) => { + match (ui_config.draw_bold_text_with_bright_colors, flags & Flags::DIM_BOLD) { + // If no bright foreground is set, treat it like the BOLD flag doesn't exist. + (_, Flags::DIM_BOLD) + if ansi == NamedColor::Foreground + && ui_config.colors.primary.bright_foreground.is_none() => + { + content.color(NamedColor::DimForeground as usize) + }, + // Draw bold text in bright colors *and* contains bold flag. + (true, Flags::BOLD) => content.color(ansi.to_bright() as usize), + // Cell is marked as dim and not bold. + (_, Flags::DIM) | (false, Flags::DIM_BOLD) => { + content.color(ansi.to_dim() as usize) + }, + // None of the above, keep original color.. + _ => content.color(ansi as usize), + } + }, + Color::Indexed(idx) => { + let idx = match ( + ui_config.draw_bold_text_with_bright_colors, + flags & Flags::DIM_BOLD, + idx, + ) { + (true, Flags::BOLD, 0..=7) => idx as usize + 8, + (false, Flags::DIM, 8..=15) => idx as usize - 8, + (false, Flags::DIM, 0..=7) => NamedColor::DimBlack as usize + idx as usize, + _ => idx as usize, + }; + + content.color(idx) + }, + } + } + + /// Get the RGB color from a cell's background color. + #[inline] + fn compute_bg_rgb(content: &mut RenderableContent<'_>, bg: Color) -> Rgb { + match bg { + Color::Spec(rgb) => rgb, + Color::Named(ansi) => content.color(ansi as usize), + Color::Indexed(idx) => content.color(idx as usize), + } + } + + /// Compute background alpha based on cell's original color. + /// + /// Since an RGB color matching the background should not be transparent, this is computed + /// using the named input color, rather than checking the RGB of the background after its color + /// is computed. + #[inline] + fn compute_bg_alpha(bg: Color) -> f32 { + if bg == Color::Named(NamedColor::Background) { + 0. + } else { + 1. + } + } +} + +/// Cursor storing all information relevant for rendering. +#[derive(Debug, Eq, PartialEq, Copy, Clone)] +pub struct RenderableCursor { + shape: CursorShape, + cursor_color: Rgb, + text_color: Rgb, + is_wide: bool, + point: Point, +} + +impl RenderableCursor { + pub fn color(&self) -> Rgb { + self.cursor_color + } + + pub fn shape(&self) -> CursorShape { + self.shape + } + + pub fn is_wide(&self) -> bool { + self.is_wide + } + + pub fn point(&self) -> Point { + self.point + } +} + +/// Regex search highlight tracking. +#[derive(Default)] +pub struct RenderableSearch { + /// All visible search matches. + matches: Vec>, + + /// Index of the last match checked. + index: usize, +} + +impl RenderableSearch { + /// Create a new renderable search iterator. + pub fn new(term: &Term, dfas: &RegexSearch) -> Self { + let viewport_end = term.grid().display_offset(); + let viewport_start = viewport_end + term.screen_lines().0 - 1; + + // Compute start of the first and end of the last line. + let start_point = Point::new(viewport_start, Column(0)); + let mut start = term.line_search_left(start_point); + let end_point = Point::new(viewport_end, term.cols() - 1); + let mut end = term.line_search_right(end_point); + + // Set upper bound on search before/after the viewport to prevent excessive blocking. + if start.line > viewport_start + MAX_SEARCH_LINES { + if start.line == 0 { + // Do not highlight anything if this line is the last. + return Self::default(); + } else { + // Start at next line if this one is too long. + start.line -= 1; + } + } + end.line = max(end.line, viewport_end.saturating_sub(MAX_SEARCH_LINES)); + + // Create an iterater for the current regex search for all visible matches. + let iter = 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) + .map(|rm| { + let viewport_start = term.grid().clamp_buffer_to_visible(*rm.start()); + let viewport_end = term.grid().clamp_buffer_to_visible(*rm.end()); + viewport_start..=viewport_end + }); + + Self { matches: iter.collect(), index: 0 } + } + + /// Advance the search tracker to the next point. + /// + /// This will return `true` if the point passed is part of a search match. + fn advance(&mut self, point: Point) -> bool { + while let Some(regex_match) = self.matches.get(self.index) { + if regex_match.start() > &point { + break; + } else if regex_match.end() < &point { + self.index += 1; + } else { + return true; + } + } + false + } +} diff --git a/alacritty/src/display/cursor.rs b/alacritty/src/display/cursor.rs new file mode 100644 index 00000000..0750459d --- /dev/null +++ b/alacritty/src/display/cursor.rs @@ -0,0 +1,92 @@ +//! Convert a cursor into an iterator of rects. + +use alacritty_terminal::ansi::CursorShape; +use alacritty_terminal::term::color::Rgb; +use alacritty_terminal::term::SizeInfo; + +use crate::display::content::RenderableCursor; +use crate::renderer::rects::RenderRect; + +/// Trait for conversion into the iterator. +pub trait IntoRects { + /// Consume the cursor for an iterator of rects. + fn rects(self, size_info: &SizeInfo, thickness: f32) -> CursorRects; +} + +impl IntoRects for RenderableCursor { + fn rects(self, size_info: &SizeInfo, thickness: f32) -> CursorRects { + let point = self.point(); + let x = point.column.0 as f32 * size_info.cell_width() + size_info.padding_x(); + let y = point.line.0 as f32 * size_info.cell_height() + size_info.padding_y(); + + let mut width = size_info.cell_width(); + let height = size_info.cell_height(); + + if self.is_wide() { + width *= 2.; + } + + let thickness = (thickness * width as f32).round().max(1.); + + match self.shape() { + CursorShape::Beam => beam(x, y, height, thickness, self.color()), + CursorShape::Underline => underline(x, y, width, height, thickness, self.color()), + CursorShape::HollowBlock => hollow(x, y, width, height, thickness, self.color()), + _ => CursorRects::default(), + } + } +} + +/// Cursor rect iterator. +#[derive(Default)] +pub struct CursorRects { + rects: [Option; 4], + index: usize, +} + +impl From for CursorRects { + fn from(rect: RenderRect) -> Self { + Self { rects: [Some(rect), None, None, None], index: 0 } + } +} + +impl Iterator for CursorRects { + type Item = RenderRect; + + fn next(&mut self) -> Option { + let rect = self.rects.get_mut(self.index)?; + self.index += 1; + rect.take() + } +} + +/// Create an iterator yielding a single beam rect. +fn beam(x: f32, y: f32, height: f32, thickness: f32, color: Rgb) -> CursorRects { + RenderRect::new(x, y, thickness, height, color, 1.).into() +} + +/// Create an iterator yielding a single underline rect. +fn underline(x: f32, y: f32, width: f32, height: f32, thickness: f32, color: Rgb) -> CursorRects { + let y = y + height - thickness; + RenderRect::new(x, y, width, thickness, color, 1.).into() +} + +/// Create an iterator yielding a rect for each side of the hollow block cursor. +fn hollow(x: f32, y: f32, width: f32, height: f32, thickness: f32, color: Rgb) -> CursorRects { + let top_line = RenderRect::new(x, y, width, thickness, color, 1.); + + let vertical_y = y + thickness; + let vertical_height = height - 2. * thickness; + let left_line = RenderRect::new(x, vertical_y, thickness, vertical_height, color, 1.); + + let bottom_y = y + height - thickness; + let bottom_line = RenderRect::new(x, bottom_y, width, thickness, color, 1.); + + let right_x = x + width - thickness; + let right_line = RenderRect::new(right_x, vertical_y, thickness, vertical_height, color, 1.); + + CursorRects { + rects: [Some(top_line), Some(bottom_line), Some(left_line), Some(right_line)], + index: 0, + } +} diff --git a/alacritty/src/display/meter.rs b/alacritty/src/display/meter.rs new file mode 100644 index 00000000..c07d901f --- /dev/null +++ b/alacritty/src/display/meter.rs @@ -0,0 +1,97 @@ +//! Rendering time meter. +//! +//! Used to track rendering times and provide moving averages. +//! +//! # Examples +//! +//! ```rust +//! // create a meter +//! let mut meter = alacritty_terminal::meter::Meter::new(); +//! +//! // Sample something. +//! { +//! let _sampler = meter.sampler(); +//! } +//! +//! // Get the moving average. The meter tracks a fixed number of samples, and +//! // the average won't mean much until it's filled up at least once. +//! println!("Average time: {}", meter.average()); +//! ``` + +use std::time::{Duration, Instant}; + +const NUM_SAMPLES: usize = 10; + +/// The meter. +#[derive(Default)] +pub struct Meter { + /// Track last 60 timestamps. + times: [f64; NUM_SAMPLES], + + /// Average sample time in microseconds. + avg: f64, + + /// Index of next time to update.. + index: usize, +} + +/// Sampler. +/// +/// Samplers record how long they are "alive" for and update the meter on drop.. +pub struct Sampler<'a> { + /// Reference to meter that created the sampler. + meter: &'a mut Meter, + + /// When the sampler was created. + created_at: Instant, +} + +impl<'a> Sampler<'a> { + fn new(meter: &'a mut Meter) -> Sampler<'a> { + Sampler { meter, created_at: Instant::now() } + } + + #[inline] + fn alive_duration(&self) -> Duration { + self.created_at.elapsed() + } +} + +impl<'a> Drop for Sampler<'a> { + fn drop(&mut self) { + self.meter.add_sample(self.alive_duration()); + } +} + +impl Meter { + /// Create a meter. + pub fn new() -> Meter { + Default::default() + } + + /// Get a sampler. + pub fn sampler(&mut self) -> Sampler<'_> { + Sampler::new(self) + } + + /// Get the current average sample duration in microseconds. + pub fn average(&self) -> f64 { + self.avg + } + + /// Add a sample. + /// + /// Used by Sampler::drop. + fn add_sample(&mut self, sample: Duration) { + let mut usec = 0f64; + + usec += f64::from(sample.subsec_nanos()) / 1e3; + usec += (sample.as_secs() as f64) * 1e6; + + let prev = self.times[self.index]; + self.times[self.index] = usec; + self.avg -= prev / NUM_SAMPLES as f64; + self.avg += usec / NUM_SAMPLES as f64; + self.index = (self.index + 1) % NUM_SAMPLES; + } +} diff --git a/alacritty/src/display/mod.rs b/alacritty/src/display/mod.rs new file mode 100644 index 00000000..2a55402e --- /dev/null +++ b/alacritty/src/display/mod.rs @@ -0,0 +1,823 @@ +//! The display subsystem including window management, font rasterization, and +//! GPU drawing. + +use std::cmp::min; +use std::f64; +use std::fmt::{self, Formatter}; +#[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))] +use std::sync::atomic::Ordering; +use std::time::Instant; + +use glutin::dpi::{PhysicalPosition, PhysicalSize}; +use glutin::event::ModifiersState; +use glutin::event_loop::EventLoop; +#[cfg(not(any(target_os = "macos", windows)))] +use glutin::platform::unix::EventLoopWindowTargetExtUnix; +use glutin::window::CursorIcon; +use log::{debug, info}; +use parking_lot::MutexGuard; +use unicode_width::UnicodeWidthChar; +#[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))] +use wayland_client::{Display as WaylandDisplay, EventQueue}; + +use crossfont::{self, Rasterize, Rasterizer}; + +use alacritty_terminal::ansi::NamedColor; +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::{SizeInfo, Term, TermMode, MIN_COLS, MIN_SCREEN_LINES}; + +use crate::config::font::Font; +use crate::config::window::Dimensions; +#[cfg(not(windows))] +use crate::config::window::StartupMode; +use crate::config::Config; +use crate::display::bell::VisualBell; +use crate::display::color::List; +use crate::display::content::RenderableContent; +use crate::display::cursor::IntoRects; +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; +pub mod window; + +mod bell; +mod color; +mod meter; +#[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))] +mod wayland_theme; + +const FORWARD_SEARCH_LABEL: &str = "Search: "; +const BACKWARD_SEARCH_LABEL: &str = "Backward Search: "; + +#[derive(Debug)] +pub enum Error { + /// Error with window management. + Window(window::Error), + + /// Error dealing with fonts. + Font(crossfont::Error), + + /// Error in renderer. + Render(renderer::Error), + + /// Error during buffer swap. + ContextError(glutin::ContextError), +} + +impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Error::Window(err) => err.source(), + Error::Font(err) => err.source(), + Error::Render(err) => err.source(), + Error::ContextError(err) => err.source(), + } + } +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self { + Error::Window(err) => err.fmt(f), + Error::Font(err) => err.fmt(f), + Error::Render(err) => err.fmt(f), + Error::ContextError(err) => err.fmt(f), + } + } +} + +impl From for Error { + fn from(val: window::Error) -> Self { + Error::Window(val) + } +} + +impl From for Error { + fn from(val: crossfont::Error) -> Self { + Error::Font(val) + } +} + +impl From for Error { + fn from(val: renderer::Error) -> Self { + Error::Render(val) + } +} + +impl From for Error { + fn from(val: glutin::ContextError) -> Self { + Error::ContextError(val) + } +} + +#[derive(Default, Clone, Debug, PartialEq)] +pub struct DisplayUpdate { + pub dirty: bool, + + dimensions: Option>, + cursor_dirty: bool, + font: Option, +} + +impl DisplayUpdate { + pub fn dimensions(&self) -> Option> { + self.dimensions + } + + pub fn font(&self) -> Option<&Font> { + self.font.as_ref() + } + + pub fn cursor_dirty(&self) -> bool { + self.cursor_dirty + } + + pub fn set_dimensions(&mut self, dimensions: PhysicalSize) { + self.dimensions = Some(dimensions); + self.dirty = true; + } + + pub fn set_font(&mut self, font: Font) { + self.font = Some(font); + self.dirty = true; + } + + pub fn set_cursor_dirty(&mut self) { + self.cursor_dirty = true; + self.dirty = true; + } +} + +/// The display wraps a window, font rasterizer, and GPU renderer. +pub struct Display { + pub size_info: SizeInfo, + pub window: Window, + pub urls: Urls, + + /// Currently highlighted URL. + pub highlighted_url: Option, + + #[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))] + pub wayland_event_queue: Option, + + #[cfg(not(any(target_os = "macos", windows)))] + pub is_x11: bool, + + /// UI cursor visibility for blinking. + pub cursor_hidden: bool, + + pub visual_bell: VisualBell, + + /// Mapped RGB values for each terminal color. + pub colors: List, + + renderer: QuadRenderer, + glyph_cache: GlyphCache, + meter: Meter, +} + +impl Display { + pub fn new(config: &Config, event_loop: &EventLoop) -> Result { + // Guess DPR based on first monitor. + let estimated_dpr = + event_loop.available_monitors().next().map(|m| m.scale_factor()).unwrap_or(1.); + + // Guess the target window dimensions. + let metrics = GlyphCache::static_metrics(config.ui_config.font.clone(), estimated_dpr)?; + let (cell_width, cell_height) = compute_cell_size(config, &metrics); + + // Guess the target window size if the user has specified the number of lines/columns. + let dimensions = config.ui_config.window.dimensions(); + let estimated_size = dimensions.map(|dimensions| { + window_size(config, dimensions, cell_width, cell_height, estimated_dpr) + }); + + debug!("Estimated DPR: {}", estimated_dpr); + debug!("Estimated window size: {:?}", estimated_size); + debug!("Estimated cell size: {} x {}", cell_width, cell_height); + + #[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))] + let mut wayland_event_queue = None; + + // Initialize Wayland event queue, to handle Wayland callbacks. + #[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))] + if let Some(display) = event_loop.wayland_display() { + let display = unsafe { WaylandDisplay::from_external_display(display as _) }; + wayland_event_queue = Some(display.create_event_queue()); + } + + // Spawn the Alacritty window. + let mut window = Window::new( + event_loop, + &config, + estimated_size, + #[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))] + wayland_event_queue.as_ref(), + )?; + + info!("Device pixel ratio: {}", window.dpr); + + // Create renderer. + let mut renderer = QuadRenderer::new()?; + + let (glyph_cache, cell_width, cell_height) = + Self::new_glyph_cache(window.dpr, &mut renderer, config)?; + + if let Some(dimensions) = dimensions { + if (estimated_dpr - window.dpr).abs() < f64::EPSILON { + info!("Estimated DPR correctly, skipping resize"); + } else { + // Resize the window again if the DPR was not estimated correctly. + let size = window_size(config, dimensions, cell_width, cell_height, window.dpr); + window.set_inner_size(size); + } + } + + let padding = config.ui_config.window.padding(window.dpr); + let viewport_size = window.inner_size(); + + // Create new size with at least one column and row. + let size_info = SizeInfo::new( + viewport_size.width as f32, + viewport_size.height as f32, + cell_width, + cell_height, + padding.0, + padding.1, + config.ui_config.window.dynamic_padding && dimensions.is_none(), + ); + + info!("Cell size: {} x {}", cell_width, cell_height); + info!("Padding: {} x {}", size_info.padding_x(), size_info.padding_y()); + info!("Width: {}, Height: {}", size_info.width(), size_info.height()); + + // Update OpenGL projection. + renderer.resize(&size_info); + + // Clear screen. + let background_color = config.ui_config.colors.primary.background; + renderer.with_api(&config.ui_config, &size_info, |api| { + api.clear(background_color); + }); + + // Set subpixel anti-aliasing. + #[cfg(target_os = "macos")] + crossfont::set_font_smoothing(config.ui_config.font.use_thin_strokes); + + // Disable shadows for transparent windows on macOS. + #[cfg(target_os = "macos")] + window.set_has_shadow(config.ui_config.background_opacity() >= 1.0); + + #[cfg(all(feature = "x11", not(any(target_os = "macos", windows))))] + let is_x11 = event_loop.is_x11(); + #[cfg(not(any(feature = "x11", target_os = "macos", windows)))] + let is_x11 = false; + + // On Wayland we can safely ignore this call, since the window isn't visible until you + // actually draw something into it and commit those changes. + #[cfg(not(any(target_os = "macos", windows)))] + if is_x11 { + window.swap_buffers(); + renderer.with_api(&config.ui_config, &size_info, |api| { + api.finish(); + }); + } + + window.set_visible(true); + + // Set window position. + // + // TODO: replace `set_position` with `with_position` once available. + // Upstream issue: https://github.com/rust-windowing/winit/issues/806. + if let Some(position) = config.ui_config.window.position { + window.set_outer_position(PhysicalPosition::from((position.x, position.y))); + } + + #[allow(clippy::single_match)] + #[cfg(not(windows))] + match config.ui_config.window.startup_mode { + #[cfg(target_os = "macos")] + StartupMode::SimpleFullscreen => window.set_simple_fullscreen(true), + #[cfg(not(target_os = "macos"))] + StartupMode::Maximized if is_x11 => window.set_maximized(true), + _ => (), + } + + Ok(Self { + window, + renderer, + glyph_cache, + meter: Meter::new(), + size_info, + urls: Urls::new(), + highlighted_url: None, + #[cfg(not(any(target_os = "macos", windows)))] + is_x11, + #[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))] + wayland_event_queue, + cursor_hidden: false, + visual_bell: VisualBell::from(&config.ui_config.bell), + colors: List::from(&config.ui_config.colors), + }) + } + + fn new_glyph_cache( + dpr: f64, + renderer: &mut QuadRenderer, + config: &Config, + ) -> Result<(GlyphCache, f32, f32), Error> { + let font = config.ui_config.font.clone(); + let rasterizer = Rasterizer::new(dpr as f32, config.ui_config.font.use_thin_strokes)?; + + // Initialize glyph cache. + let glyph_cache = { + info!("Initializing glyph cache..."); + let init_start = Instant::now(); + + let cache = + renderer.with_loader(|mut api| GlyphCache::new(rasterizer, &font, &mut api))?; + + let stop = init_start.elapsed(); + let stop_f = stop.as_secs() as f64 + f64::from(stop.subsec_nanos()) / 1_000_000_000f64; + info!("... finished initializing glyph cache in {}s", stop_f); + + cache + }; + + // Need font metrics to resize the window properly. This suggests to me the + // font metrics should be computed before creating the window in the first + // place so that a resize is not needed. + let (cw, ch) = compute_cell_size(config, &glyph_cache.font_metrics()); + + Ok((glyph_cache, cw, ch)) + } + + /// Update font size and cell dimensions. + /// + /// This will return a tuple of the cell width and height. + fn update_glyph_cache(&mut self, config: &Config, font: &Font) -> (f32, f32) { + let cache = &mut self.glyph_cache; + let dpr = self.window.dpr; + + self.renderer.with_loader(|mut api| { + let _ = cache.update_font_size(font, dpr, &mut api); + }); + + // Compute new cell sizes. + compute_cell_size(config, &self.glyph_cache.font_metrics()) + } + + /// Clear glyph cache. + fn clear_glyph_cache(&mut self) { + let cache = &mut self.glyph_cache; + self.renderer.with_loader(|mut api| { + cache.clear_glyph_cache(&mut api); + }); + } + + /// Process update events. + pub fn handle_update( + &mut self, + terminal: &mut Term, + pty_resize_handle: &mut dyn OnResize, + message_buffer: &MessageBuffer, + search_active: bool, + config: &Config, + update_pending: DisplayUpdate, + ) where + T: EventListener, + { + let (mut cell_width, mut cell_height) = + (self.size_info.cell_width(), self.size_info.cell_height()); + + // Update font size and cell dimensions. + if let Some(font) = update_pending.font() { + let cell_dimensions = self.update_glyph_cache(config, font); + cell_width = cell_dimensions.0; + cell_height = cell_dimensions.1; + + info!("Cell size: {} x {}", cell_width, cell_height); + } else if update_pending.cursor_dirty() { + self.clear_glyph_cache(); + } + + let (mut width, mut height) = (self.size_info.width(), self.size_info.height()); + if let Some(dimensions) = update_pending.dimensions() { + width = dimensions.width as f32; + height = dimensions.height as f32; + } + + let padding = config.ui_config.window.padding(self.window.dpr); + + self.size_info = SizeInfo::new( + width, + height, + cell_width, + cell_height, + padding.0, + padding.1, + config.ui_config.window.dynamic_padding, + ); + + // Update number of column/lines in the viewport. + let message_bar_lines = + message_buffer.message().map(|m| m.text(&self.size_info).len()).unwrap_or(0); + let search_lines = if search_active { 1 } else { 0 }; + self.size_info.reserve_lines(message_bar_lines + search_lines); + + // Resize PTY. + pty_resize_handle.on_resize(&self.size_info); + + // Resize terminal. + terminal.resize(self.size_info); + + // Resize renderer. + let physical = + PhysicalSize::new(self.size_info.width() as u32, self.size_info.height() as u32); + self.window.resize(physical); + self.renderer.resize(&self.size_info); + + info!("Padding: {} x {}", self.size_info.padding_x(), self.size_info.padding_y()); + info!("Width: {}, Height: {}", self.size_info.width(), self.size_info.height()); + } + + /// Draw the screen. + /// + /// A reference to Term whose state is being drawn must be provided. + /// + /// This call may block if vsync is enabled. + pub fn draw( + &mut self, + terminal: MutexGuard<'_, Term>, + message_buffer: &MessageBuffer, + config: &Config, + mouse: &Mouse, + mods: ModifiersState, + search_state: &SearchState, + ) { + // Convert search match from viewport to absolute indexing. + let search_active = search_state.regex().is_some(); + let viewport_match = search_state + .focused_match() + .and_then(|focused_match| terminal.grid().clamp_buffer_range_to_visible(focused_match)); + let cursor_hidden = self.cursor_hidden || search_state.regex().is_some(); + + // Collect renderable content before the terminal is dropped. + let dfas = search_state.dfas(); + let colors = &self.colors; + let mut content = RenderableContent::new(&terminal, dfas, config, colors, !cursor_hidden); + let mut grid_cells = Vec::new(); + while let Some(cell) = content.next() { + grid_cells.push(cell); + } + let background_color = content.color(NamedColor::Background as usize); + let display_offset = content.display_offset(); + let cursor = content.cursor(); + + let cursor_point = terminal.grid().cursor.point; + let total_lines = terminal.grid().total_lines(); + 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 }; + + // Drop terminal as early as possible to free lock. + drop(terminal); + + self.renderer.with_api(&config.ui_config, &size_info, |api| { + api.clear(background_color); + }); + + let mut lines = RenderLines::new(); + let mut urls = Urls::new(); + + // Draw grid. + { + let _sampler = self.meter.sampler(); + + let glyph_cache = &mut self.glyph_cache; + self.renderer.with_api(&config.ui_config, &size_info, |mut api| { + // Iterate over all non-empty cells in the grid. + for mut cell in grid_cells { + // Invert the active match during search. + if cell.is_match + && viewport_match + .as_ref() + .map_or(false, |viewport_match| viewport_match.contains(&cell.point)) + { + let colors = config.ui_config.colors.search.focused_match; + let match_fg = colors.foreground.color(cell.fg, cell.bg); + cell.bg = colors.background.color(cell.fg, cell.bg); + cell.fg = match_fg; + cell.bg_alpha = 1.0; + } + + // Update URL underlines. + urls.update(size_info.cols(), &cell); + + // Update underline/strikeout. + lines.update(&cell); + + // Draw the cell. + api.render_cell(cell, glyph_cache); + } + }); + } + + 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_mode_point = vi_mode_cursor.point; + if let Some(url) = self.urls.find_at(vi_mode_point) { + rects.append(&mut url.rects(&metrics, &size_info)); + } + + // Indicate vi mode by showing the cursor's position in the top right corner. + let line = size_info.screen_lines() + display_offset - vi_mode_point.line - 1; + self.draw_line_indicator(config, &size_info, total_lines, Some(vi_mode_point), line.0); + } else if search_active { + // Show current display offset in vi-less search to indicate match position. + self.draw_line_indicator(config, &size_info, total_lines, None, display_offset); + } + + // Push the cursor rects for rendering. + if let Some(cursor) = cursor { + for rect in cursor.rects(&size_info, config.cursor.thickness()) { + rects.push(rect); + } + } + + // Push visual bell after url/underline/strikeout rects. + let visual_bell_intensity = self.visual_bell.intensity(); + if visual_bell_intensity != 0. { + let visual_bell_rect = RenderRect::new( + 0., + 0., + size_info.width(), + size_info.height(), + config.ui_config.bell.color, + visual_bell_intensity as f32, + ); + rects.push(visual_bell_rect); + } + + if let Some(message) = message_buffer.message() { + let search_offset = if search_active { 1 } else { 0 }; + let text = message.text(&size_info); + + // Create a new rectangle for the background. + let start_line = size_info.screen_lines() + search_offset; + let y = size_info.cell_height().mul_add(start_line.0 as f32, size_info.padding_y()); + + let bg = match message.ty() { + MessageType::Error => config.ui_config.colors.normal.red, + MessageType::Warning => config.ui_config.colors.normal.yellow, + }; + + let message_bar_rect = + RenderRect::new(0., y, size_info.width(), size_info.height() - y, bg, 1.); + + // Push message_bar in the end, so it'll be above all other content. + rects.push(message_bar_rect); + + // Draw rectangles. + self.renderer.draw_rects(&size_info, rects); + + // Relay messages to the user. + let glyph_cache = &mut self.glyph_cache; + let fg = config.ui_config.colors.primary.background; + for (i, message_text) in text.iter().enumerate() { + let point = Point::new(start_line + i, Column(0)); + self.renderer.with_api(&config.ui_config, &size_info, |mut api| { + api.render_string(glyph_cache, point, fg, bg, &message_text); + }); + } + } else { + // Draw rectangles. + self.renderer.draw_rects(&size_info, rects); + } + + self.draw_render_timer(config, &size_info); + + // Handle search and IME positioning. + let ime_position = match search_state.regex() { + Some(regex) => { + let search_label = match search_state.direction() { + Direction::Right => FORWARD_SEARCH_LABEL, + Direction::Left => BACKWARD_SEARCH_LABEL, + }; + + let search_text = Self::format_search(&size_info, regex, search_label); + + // Render the search bar. + self.draw_search(config, &size_info, &search_text); + + // Compute IME position. + Point::new(size_info.screen_lines() + 1, Column(search_text.chars().count() - 1)) + }, + None => cursor_point, + }; + + // Update IME position. + self.window.update_ime_position(ime_position, &self.size_info); + + // Frame event should be requested before swaping buffers, since it requires surface + // `commit`, which is done by swap buffers under the hood. + #[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))] + self.request_frame(&self.window); + + self.window.swap_buffers(); + + #[cfg(all(feature = "x11", not(any(target_os = "macos", windows))))] + if self.is_x11 { + // On X11 `swap_buffers` does not block for vsync. However the next OpenGl command + // will block to synchronize (this is `glClear` in Alacritty), which causes a + // permanent one frame delay. + self.renderer.with_api(&config.ui_config, &size_info, |api| { + api.finish(); + }); + } + } + + /// Update to a new configuration. + pub fn update_config(&mut self, config: &Config) { + self.visual_bell.update_config(&config.ui_config.bell); + self.colors = List::from(&config.ui_config.colors); + } + + /// 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(' '); + } + } + + // Add cursor to show whitespace. + formatted_regex.push('_'); + + // Truncate beginning of the search regex if it exceeds the viewport width. + let num_cols = size_info.cols().0; + let label_len = search_label.chars().count(); + let regex_len = formatted_regex.chars().count(); + let truncate_len = 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..]; + + // Add search label to the beginning of the search regex. + let mut bar_text = format!("{}{}", search_label, truncated_regex); + + // Make sure the label alone doesn't exceed the viewport width. + bar_text.truncate(num_cols); + + bar_text + } + + /// Draw current search regex. + fn draw_search(&mut self, config: &Config, size_info: &SizeInfo, text: &str) { + let glyph_cache = &mut self.glyph_cache; + let num_cols = size_info.cols().0; + + // Assure text length is at least num_cols. + let text = format!("{:<1$}", text, num_cols); + + let point = Point::new(size_info.screen_lines(), Column(0)); + let fg = config.ui_config.colors.search_bar_foreground(); + let bg = config.ui_config.colors.search_bar_background(); + + self.renderer.with_api(&config.ui_config, &size_info, |mut api| { + api.render_string(glyph_cache, point, fg, bg, &text); + }); + } + + /// Draw render timer. + fn draw_render_timer(&mut self, config: &Config, size_info: &SizeInfo) { + if !config.ui_config.debug.render_timer { + return; + } + + let glyph_cache = &mut self.glyph_cache; + + let timing = format!("{:.3} usec", self.meter.average()); + let point = Point::new(size_info.screen_lines() - 2, Column(0)); + let fg = config.ui_config.colors.primary.background; + let bg = config.ui_config.colors.normal.red; + + self.renderer.with_api(&config.ui_config, &size_info, |mut api| { + api.render_string(glyph_cache, point, fg, bg, &timing); + }); + } + + /// Draw an indicator for the position of a line in history. + fn draw_line_indicator( + &mut self, + config: &Config, + size_info: &SizeInfo, + total_lines: usize, + vi_mode_point: Option, + line: usize, + ) { + let text = format!("[{}/{}]", line, total_lines - 1); + let column = Column(size_info.cols().0.saturating_sub(text.len())); + let colors = &config.ui_config.colors; + let fg = colors.line_indicator.foreground.unwrap_or(colors.primary.background); + let bg = colors.line_indicator.background.unwrap_or(colors.primary.foreground); + + // Do not render anything if it would obscure the vi mode cursor. + if vi_mode_point.map_or(true, |point| point.line.0 != 0 || point.column < column) { + let glyph_cache = &mut self.glyph_cache; + self.renderer.with_api(&config.ui_config, &size_info, |mut api| { + api.render_string(glyph_cache, Point::new(Line(0), column), fg, bg, &text); + }); + } + } + + /// Requst a new frame for a window on Wayland. + #[inline] + #[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))] + fn request_frame(&self, window: &Window) { + let surface = match window.wayland_surface() { + Some(surface) => surface, + None => return, + }; + + let should_draw = self.window.should_draw.clone(); + + // Mark that window was drawn. + should_draw.store(false, Ordering::Relaxed); + + // Request a new frame. + surface.frame().quick_assign(move |_, _, _| { + should_draw.store(true, Ordering::Relaxed); + }); + } +} + +/// Calculate the cell dimensions based on font metrics. +/// +/// This will return a tuple of the cell width and height. +#[inline] +fn compute_cell_size(config: &Config, metrics: &crossfont::Metrics) -> (f32, f32) { + let offset_x = f64::from(config.ui_config.font.offset.x); + let offset_y = f64::from(config.ui_config.font.offset.y); + ( + (metrics.average_advance + offset_x).floor().max(1.) as f32, + (metrics.line_height + offset_y).floor().max(1.) as f32, + ) +} + +/// Calculate the size of the window given padding, terminal dimensions and cell size. +fn window_size( + config: &Config, + dimensions: Dimensions, + cell_width: f32, + cell_height: f32, + dpr: f64, +) -> PhysicalSize { + let padding = config.ui_config.window.padding(dpr); + + let grid_width = cell_width * dimensions.columns.0.max(MIN_COLS) as f32; + let grid_height = cell_height * dimensions.lines.0.max(MIN_SCREEN_LINES) as f32; + + let width = (padding.0).mul_add(2., grid_width).floor(); + let height = (padding.1).mul_add(2., grid_height).floor(); + + PhysicalSize::new(width as u32, height as u32) +} diff --git a/alacritty/src/display/wayland_theme.rs b/alacritty/src/display/wayland_theme.rs new file mode 100644 index 00000000..1932ae01 --- /dev/null +++ b/alacritty/src/display/wayland_theme.rs @@ -0,0 +1,81 @@ +use glutin::platform::unix::{ARGBColor, Button, ButtonState, Element, Theme as WaylandTheme}; + +use alacritty_terminal::term::color::Rgb; + +use crate::config::color::Colors; + +const INACTIVE_OPACITY: u8 = 127; + +#[derive(Debug, Clone)] +pub struct AlacrittyWaylandTheme { + pub background: ARGBColor, + pub foreground: ARGBColor, + pub dim_foreground: ARGBColor, + pub hovered_close_icon: ARGBColor, + pub hovered_maximize_icon: ARGBColor, + pub hovered_minimize_icon: ARGBColor, +} + +impl AlacrittyWaylandTheme { + pub fn new(colors: &Colors) -> Self { + let hovered_close_icon = colors.normal.red.into_rgba(); + let hovered_maximize_icon = colors.normal.green.into_rgba(); + let hovered_minimize_icon = colors.normal.yellow.into_rgba(); + let foreground = colors.search_bar_foreground().into_rgba(); + let background = colors.search_bar_background().into_rgba(); + + let mut dim_foreground = foreground; + dim_foreground.a = INACTIVE_OPACITY; + + Self { + foreground, + background, + dim_foreground, + hovered_close_icon, + hovered_minimize_icon, + hovered_maximize_icon, + } + } +} + +impl WaylandTheme for AlacrittyWaylandTheme { + fn element_color(&self, element: Element, window_active: bool) -> ARGBColor { + match element { + Element::Bar | Element::Separator => self.background, + Element::Text if window_active => self.foreground, + Element::Text => self.dim_foreground, + } + } + + fn button_color( + &self, + button: Button, + state: ButtonState, + foreground: bool, + window_active: bool, + ) -> ARGBColor { + if !foreground { + return ARGBColor { a: 0, r: 0, g: 0, b: 0 }; + } else if !window_active { + return self.dim_foreground; + } + + match (state, button) { + (ButtonState::Idle, _) => self.foreground, + (ButtonState::Disabled, _) => self.dim_foreground, + (_, Button::Minimize) => self.hovered_minimize_icon, + (_, Button::Maximize) => self.hovered_maximize_icon, + (_, Button::Close) => self.hovered_close_icon, + } + } +} + +trait IntoARGBColor { + fn into_rgba(self) -> ARGBColor; +} + +impl IntoARGBColor for Rgb { + fn into_rgba(self) -> ARGBColor { + ARGBColor { a: 0xff, r: self.r, g: self.g, b: self.b } + } +} diff --git a/alacritty/src/display/window.rs b/alacritty/src/display/window.rs new file mode 100644 index 00000000..b500e8f2 --- /dev/null +++ b/alacritty/src/display/window.rs @@ -0,0 +1,497 @@ +#[rustfmt::skip] +#[cfg(not(any(target_os = "macos", windows)))] +use { + std::sync::atomic::AtomicBool, + std::sync::Arc, + + glutin::platform::unix::{WindowBuilderExtUnix, WindowExtUnix}, +}; + +#[rustfmt::skip] +#[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))] +use { + wayland_client::protocol::wl_surface::WlSurface, + wayland_client::{Attached, EventQueue, Proxy}, + glutin::platform::unix::EventLoopWindowTargetExtUnix, + + crate::config::color::Colors, + crate::display::wayland_theme::AlacrittyWaylandTheme, +}; + +#[rustfmt::skip] +#[cfg(all(feature = "x11", not(any(target_os = "macos", windows))))] +use { + std::io::Cursor, + + x11_dl::xlib::{Display as XDisplay, PropModeReplace, XErrorEvent, Xlib}, + glutin::window::Icon, + png::Decoder, +}; + +use std::fmt::{self, Display, Formatter}; + +#[cfg(target_os = "macos")] +use cocoa::base::{id, NO, YES}; +use glutin::dpi::{PhysicalPosition, PhysicalSize}; +use glutin::event_loop::EventLoop; +#[cfg(target_os = "macos")] +use glutin::platform::macos::{WindowBuilderExtMacOS, WindowExtMacOS}; +#[cfg(windows)] +use glutin::platform::windows::IconExtWindows; +use glutin::window::{ + CursorIcon, Fullscreen, UserAttentionType, Window as GlutinWindow, WindowBuilder, WindowId, +}; +use glutin::{self, ContextBuilder, PossiblyCurrent, WindowedContext}; +#[cfg(target_os = "macos")] +use objc::{msg_send, sel, sel_impl}; +#[cfg(target_os = "macos")] +use raw_window_handle::{HasRawWindowHandle, RawWindowHandle}; +#[cfg(windows)] +use winapi::shared::minwindef::WORD; + +use alacritty_terminal::index::Point; +use alacritty_terminal::term::SizeInfo; + +use crate::config::window::{Decorations, WindowConfig}; +use crate::config::Config; +use crate::gl; + +/// Window icon for `_NET_WM_ICON` property. +#[cfg(all(feature = "x11", not(any(target_os = "macos", windows))))] +static WINDOW_ICON: &[u8] = include_bytes!("../../alacritty.png"); + +/// This should match the definition of IDI_ICON from `windows.rc`. +#[cfg(windows)] +const IDI_ICON: WORD = 0x101; + +/// Window errors. +#[derive(Debug)] +pub enum Error { + /// Error creating the window. + ContextCreation(glutin::CreationError), + + /// Error dealing with fonts. + Font(crossfont::Error), + + /// Error manipulating the rendering context. + Context(glutin::ContextError), +} + +/// Result of fallible operations concerning a Window. +type Result = std::result::Result; + +impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Error::ContextCreation(err) => err.source(), + Error::Context(err) => err.source(), + Error::Font(err) => err.source(), + } + } +} + +impl Display for Error { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self { + Error::ContextCreation(err) => write!(f, "Error creating GL context; {}", err), + Error::Context(err) => write!(f, "Error operating on render context; {}", err), + Error::Font(err) => err.fmt(f), + } + } +} + +impl From for Error { + fn from(val: glutin::CreationError) -> Self { + Error::ContextCreation(val) + } +} + +impl From for Error { + fn from(val: glutin::ContextError) -> Self { + Error::Context(val) + } +} + +impl From for Error { + fn from(val: crossfont::Error) -> Self { + Error::Font(val) + } +} + +fn create_gl_window( + mut window: WindowBuilder, + event_loop: &EventLoop, + srgb: bool, + vsync: bool, + dimensions: Option>, +) -> Result> { + if let Some(dimensions) = dimensions { + window = window.with_inner_size(dimensions); + } + + let windowed_context = ContextBuilder::new() + .with_srgb(srgb) + .with_vsync(vsync) + .with_hardware_acceleration(None) + .build_windowed(window, event_loop)?; + + // Make the context current so OpenGL operations can run. + let windowed_context = unsafe { windowed_context.make_current().map_err(|(_, err)| err)? }; + + Ok(windowed_context) +} + +/// A window which can be used for displaying the terminal. +/// +/// Wraps the underlying windowing library to provide a stable API in Alacritty. +pub struct Window { + /// Flag tracking frame redraw requests from Wayland compositor. + #[cfg(not(any(target_os = "macos", windows)))] + pub should_draw: Arc, + + /// Attached Wayland surface to request new frame events. + #[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))] + pub wayland_surface: Option>, + + /// Cached DPR for quickly scaling pixel sizes. + pub dpr: f64, + + windowed_context: WindowedContext, + current_mouse_cursor: CursorIcon, + mouse_visible: bool, +} + +impl Window { + /// Create a new window. + /// + /// This creates a window and fully initializes a window. + pub fn new( + event_loop: &EventLoop, + config: &Config, + size: Option>, + #[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))] + wayland_event_queue: Option<&EventQueue>, + ) -> Result { + let window_config = &config.ui_config.window; + let window_builder = Window::get_platform_window(&window_config.title, &window_config); + + // Check if we're running Wayland to disable vsync. + #[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))] + let is_wayland = event_loop.is_wayland(); + #[cfg(any(not(feature = "wayland"), target_os = "macos", windows))] + let is_wayland = false; + + let windowed_context = + create_gl_window(window_builder.clone(), &event_loop, false, !is_wayland, size) + .or_else(|_| { + create_gl_window(window_builder, &event_loop, true, !is_wayland, size) + })?; + + // Text cursor. + let current_mouse_cursor = CursorIcon::Text; + windowed_context.window().set_cursor_icon(current_mouse_cursor); + + // Set OpenGL symbol loader. This call MUST be after window.make_current on windows. + gl::load_with(|symbol| windowed_context.get_proc_address(symbol) as *const _); + + #[cfg(all(feature = "x11", not(any(target_os = "macos", windows))))] + if !is_wayland { + // On X11, embed the window inside another if the parent ID has been set. + if let Some(parent_window_id) = window_config.embed { + x_embed_window(windowed_context.window(), parent_window_id); + } + } + + #[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))] + let wayland_surface = if is_wayland { + // Apply client side decorations theme. + let theme = AlacrittyWaylandTheme::new(&config.ui_config.colors); + windowed_context.window().set_wayland_theme(theme); + + // Attach surface to Alacritty's internal wayland queue to handle frame callbacks. + let surface = windowed_context.window().wayland_surface().unwrap(); + let proxy: Proxy = unsafe { Proxy::from_c_ptr(surface as _) }; + Some(proxy.attach(wayland_event_queue.as_ref().unwrap().token())) + } else { + None + }; + + let dpr = windowed_context.window().scale_factor(); + + Ok(Self { + current_mouse_cursor, + mouse_visible: true, + windowed_context, + #[cfg(not(any(target_os = "macos", windows)))] + should_draw: Arc::new(AtomicBool::new(true)), + #[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))] + wayland_surface, + dpr, + }) + } + + pub fn set_inner_size(&mut self, size: PhysicalSize) { + self.window().set_inner_size(size); + } + + pub fn inner_size(&self) -> PhysicalSize { + self.window().inner_size() + } + + #[inline] + pub fn set_visible(&self, visibility: bool) { + self.window().set_visible(visibility); + } + + /// Set the window title. + #[inline] + pub fn set_title(&self, title: &str) { + self.window().set_title(title); + } + + #[inline] + pub fn set_mouse_cursor(&mut self, cursor: CursorIcon) { + if cursor != self.current_mouse_cursor { + self.current_mouse_cursor = cursor; + self.window().set_cursor_icon(cursor); + } + } + + /// Set mouse cursor visible. + pub fn set_mouse_visible(&mut self, visible: bool) { + if visible != self.mouse_visible { + self.mouse_visible = visible; + self.window().set_cursor_visible(visible); + } + } + + #[cfg(not(any(target_os = "macos", windows)))] + pub fn get_platform_window(title: &str, window_config: &WindowConfig) -> WindowBuilder { + #[cfg(feature = "x11")] + let icon = { + let decoder = Decoder::new(Cursor::new(WINDOW_ICON)); + let (info, mut reader) = decoder.read_info().expect("invalid embedded icon"); + let mut buf = vec![0; info.buffer_size()]; + let _ = reader.next_frame(&mut buf); + Icon::from_rgba(buf, info.width, info.height) + }; + + let builder = WindowBuilder::new() + .with_title(title) + .with_visible(false) + .with_transparent(true) + .with_decorations(window_config.decorations != Decorations::None) + .with_maximized(window_config.maximized()) + .with_fullscreen(window_config.fullscreen()); + + #[cfg(feature = "x11")] + let builder = builder.with_window_icon(icon.ok()); + + #[cfg(feature = "wayland")] + let builder = builder.with_app_id(window_config.class.instance.to_owned()); + + #[cfg(feature = "x11")] + let builder = builder.with_class( + window_config.class.instance.to_owned(), + window_config.class.general.to_owned(), + ); + + #[cfg(feature = "x11")] + let builder = match &window_config.gtk_theme_variant { + Some(val) => builder.with_gtk_theme_variant(val.clone()), + None => builder, + }; + + builder + } + + #[cfg(windows)] + pub fn get_platform_window(title: &str, window_config: &WindowConfig) -> WindowBuilder { + let icon = glutin::window::Icon::from_resource(IDI_ICON, None); + + WindowBuilder::new() + .with_title(title) + .with_visible(false) + .with_decorations(window_config.decorations != Decorations::None) + .with_transparent(true) + .with_maximized(window_config.maximized()) + .with_fullscreen(window_config.fullscreen()) + .with_window_icon(icon.ok()) + } + + #[cfg(target_os = "macos")] + pub fn get_platform_window(title: &str, window_config: &WindowConfig) -> WindowBuilder { + let window = WindowBuilder::new() + .with_title(title) + .with_visible(false) + .with_transparent(true) + .with_maximized(window_config.maximized()) + .with_fullscreen(window_config.fullscreen()); + + match window_config.decorations { + Decorations::Full => window, + Decorations::Transparent => window + .with_title_hidden(true) + .with_titlebar_transparent(true) + .with_fullsize_content_view(true), + Decorations::Buttonless => window + .with_title_hidden(true) + .with_titlebar_buttons_hidden(true) + .with_titlebar_transparent(true) + .with_fullsize_content_view(true), + Decorations::None => window.with_titlebar_hidden(true), + } + } + + pub fn set_urgent(&self, is_urgent: bool) { + let attention = if is_urgent { Some(UserAttentionType::Critical) } else { None }; + + self.window().request_user_attention(attention); + } + + pub fn set_outer_position(&self, pos: PhysicalPosition) { + self.window().set_outer_position(pos); + } + + #[cfg(all(feature = "x11", not(any(target_os = "macos", windows))))] + pub fn x11_window_id(&self) -> Option { + self.window().xlib_window().map(|xlib_window| xlib_window as usize) + } + + #[cfg(any(not(feature = "x11"), target_os = "macos", windows))] + pub fn x11_window_id(&self) -> Option { + None + } + + pub fn window_id(&self) -> WindowId { + self.window().id() + } + + #[cfg(not(any(target_os = "macos", windows)))] + pub fn set_maximized(&self, maximized: bool) { + self.window().set_maximized(maximized); + } + + pub fn set_minimized(&self, minimized: bool) { + self.window().set_minimized(minimized); + } + + /// Toggle the window's fullscreen state. + pub fn toggle_fullscreen(&mut self) { + self.set_fullscreen(self.window().fullscreen().is_none()); + } + + #[cfg(target_os = "macos")] + pub fn toggle_simple_fullscreen(&mut self) { + self.set_simple_fullscreen(!self.window().simple_fullscreen()); + } + + pub fn set_fullscreen(&mut self, fullscreen: bool) { + if fullscreen { + self.window().set_fullscreen(Some(Fullscreen::Borderless(None))); + } else { + self.window().set_fullscreen(None); + } + } + + #[cfg(target_os = "macos")] + pub fn set_simple_fullscreen(&mut self, simple_fullscreen: bool) { + self.window().set_simple_fullscreen(simple_fullscreen); + } + + #[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))] + pub fn wayland_display(&self) -> Option<*mut std::ffi::c_void> { + self.window().wayland_display() + } + + #[cfg(not(any(feature = "wayland", target_os = "macos", windows)))] + pub fn wayland_display(&self) -> Option<*mut std::ffi::c_void> { + None + } + + #[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))] + pub fn wayland_surface(&self) -> Option<&Attached> { + self.wayland_surface.as_ref() + } + + #[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))] + pub fn set_wayland_theme(&mut self, colors: &Colors) { + self.window().set_wayland_theme(AlacrittyWaylandTheme::new(colors)); + } + + /// Adjust the IME editor position according to the new location of the cursor. + pub fn update_ime_position(&mut self, point: Point, size: &SizeInfo) { + let nspot_x = f64::from(size.padding_x() + point.column.0 as f32 * size.cell_width()); + let nspot_y = f64::from(size.padding_y() + (point.line.0 + 1) as f32 * size.cell_height()); + + self.window().set_ime_position(PhysicalPosition::new(nspot_x, nspot_y)); + } + + pub fn swap_buffers(&self) { + self.windowed_context.swap_buffers().expect("swap buffers"); + } + + pub fn resize(&self, size: PhysicalSize) { + self.windowed_context.resize(size); + } + + /// Disable macOS window shadows. + /// + /// This prevents rendering artifacts from showing up when the window is transparent. + #[cfg(target_os = "macos")] + pub fn set_has_shadow(&self, has_shadows: bool) { + let raw_window = match self.window().raw_window_handle() { + RawWindowHandle::MacOS(handle) => handle.ns_window as id, + _ => return, + }; + + let value = if has_shadows { YES } else { NO }; + unsafe { + let _: () = msg_send![raw_window, setHasShadow: value]; + } + } + + fn window(&self) -> &GlutinWindow { + self.windowed_context.window() + } +} + +#[cfg(all(feature = "x11", not(any(target_os = "macos", windows))))] +fn x_embed_window(window: &GlutinWindow, parent_id: std::os::raw::c_ulong) { + let (xlib_display, xlib_window) = match (window.xlib_display(), window.xlib_window()) { + (Some(display), Some(window)) => (display, window), + _ => return, + }; + + let xlib = Xlib::open().expect("get xlib"); + + unsafe { + let atom = (xlib.XInternAtom)(xlib_display as *mut _, "_XEMBED".as_ptr() as *const _, 0); + (xlib.XChangeProperty)( + xlib_display as _, + xlib_window as _, + atom, + atom, + 32, + PropModeReplace, + [0, 1].as_ptr(), + 2, + ); + + // Register new error handler. + let old_handler = (xlib.XSetErrorHandler)(Some(xembed_error_handler)); + + // Check for the existence of the target before attempting reparenting. + (xlib.XReparentWindow)(xlib_display as _, xlib_window as _, parent_id, 0, 0); + + // Drain errors and restore original error handler. + (xlib.XSync)(xlib_display as _, 0); + (xlib.XSetErrorHandler)(old_handler); + } +} + +#[cfg(all(feature = "x11", not(any(target_os = "macos", windows))))] +unsafe extern "C" fn xembed_error_handler(_: *mut XDisplay, _: *mut XErrorEvent) -> i32 { + log::error!("Could not embed into specified window."); + std::process::exit(1); +} diff --git a/alacritty/src/event.rs b/alacritty/src/event.rs index d83469ee..26f781dc 100644 --- a/alacritty/src/event.rs +++ b/alacritty/src/event.rs @@ -12,7 +12,7 @@ use std::fs::File; use std::io::Write; use std::mem; use std::ops::RangeInclusive; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; #[cfg(not(any(target_os = "macos", windows)))] use std::sync::atomic::Ordering; use std::sync::Arc; @@ -35,6 +35,7 @@ use alacritty_terminal::grid::{Dimensions, Scroll}; use alacritty_terminal::index::{Boundary, Column, Direction, Line, Point, Side}; use alacritty_terminal::selection::{Selection, SelectionType}; use alacritty_terminal::sync::FairMutex; +use alacritty_terminal::term::search::{Match, RegexSearch}; use alacritty_terminal::term::{ClipboardType, SizeInfo, Term, TermMode}; #[cfg(not(windows))] use alacritty_terminal::tty; @@ -44,6 +45,7 @@ use crate::clipboard::Clipboard; use crate::config; use crate::config::Config; use crate::daemon::start_daemon; +use crate::display::window::Window; use crate::display::{Display, DisplayUpdate}; use crate::input::{self, ActionContext as _, FONT_SIZE_STEP}; #[cfg(target_os = "macos")] @@ -51,7 +53,6 @@ use crate::macos; use crate::message_bar::{Message, MessageBuffer}; use crate::scheduler::{Scheduler, TimerId}; use crate::url::{Url, Urls}; -use crate::window::Window; /// Duration after the last user input until an unlimited search is performed. pub const TYPING_SEARCH_DELAY: Duration = Duration::from_millis(500); @@ -102,13 +103,17 @@ pub struct SearchState { /// Search regex and history. /// - /// When a search is currently active, the first element will be what the user can modify in - /// the current search session. While going through history, the [`history_index`] will point - /// to the element in history which is currently being previewed. + /// During an active search, the first element is the user's current input. + /// + /// While going through history, the [`SearchState::history_index`] will point to the element + /// in history which is currently being previewed. history: VecDeque, /// Current position in the search history. history_index: Option, + + /// Compiled search automatons. + dfas: Option, } impl SearchState { @@ -131,6 +136,11 @@ impl SearchState { self.focused_match.as_ref() } + /// Active search dfas. + pub fn dfas(&self) -> Option<&RegexSearch> { + self.dfas.as_ref() + } + /// Search regex text if a search is active. fn regex_mut(&mut self) -> Option<&mut String> { self.history_index.and_then(move |index| self.history.get_mut(index)) @@ -146,6 +156,7 @@ impl Default for SearchState { history_index: Default::default(), history: Default::default(), origin: Default::default(), + dfas: Default::default(), } } } @@ -154,31 +165,37 @@ pub struct ActionContext<'a, N, T> { pub notifier: &'a mut N, pub terminal: &'a mut Term, pub clipboard: &'a mut Clipboard, - pub size_info: &'a mut SizeInfo, pub mouse: &'a mut Mouse, pub received_count: &'a mut usize, pub suppress_chars: &'a mut bool, pub modifiers: &'a mut ModifiersState, - pub window: &'a mut Window, + pub display: &'a mut Display, pub message_buffer: &'a mut MessageBuffer, pub display_update_pending: &'a mut DisplayUpdate, pub config: &'a mut Config, pub event_loop: &'a EventLoopWindowTarget, - pub urls: &'a Urls, pub scheduler: &'a mut Scheduler, pub search_state: &'a mut SearchState, - cursor_hidden: &'a mut bool, cli_options: &'a CLIOptions, font_size: &'a mut Size, + dirty: bool, } impl<'a, N: Notify + 'a, T: EventListener> input::ActionContext for ActionContext<'a, N, T> { + #[inline] fn write_to_pty>>(&mut self, val: B) { self.notifier.notify(val); } + /// Request a redraw. + #[inline] + fn mark_dirty(&mut self) { + self.dirty = true; + } + + #[inline] fn size_info(&self) -> SizeInfo { - *self.size_info + self.display.size_info } fn scroll(&mut self, scroll: Scroll) { @@ -202,8 +219,10 @@ impl<'a, N: Notify + 'a, T: EventListener> input::ActionContext for ActionCon { let point = self.size_info().pixels_to_coords(self.mouse().x, self.mouse().y); let cell_side = self.mouse().cell_side; - self.update_selection(Point { line: point.line, col: point.col }, cell_side); + self.update_selection(point, cell_side); } + + self.dirty = true; } fn copy_selection(&mut self, ty: ClipboardType) { @@ -220,7 +239,7 @@ impl<'a, N: Notify + 'a, T: EventListener> input::ActionContext for ActionCon fn clear_selection(&mut self) { self.terminal.selection = None; - self.terminal.dirty = true; + self.dirty = true; } fn update_selection(&mut self, mut point: Point, side: Side) { @@ -243,13 +262,13 @@ impl<'a, N: Notify + 'a, T: EventListener> input::ActionContext for ActionCon } self.terminal.selection = Some(selection); - self.terminal.dirty = true; + self.dirty = true; } fn start_selection(&mut self, ty: SelectionType, point: Point, side: Side) { let point = self.terminal.visible_to_buffer(point); self.terminal.selection = Some(Selection::new(ty, point, side)); - self.terminal.dirty = true; + self.dirty = true; } fn toggle_selection(&mut self, ty: SelectionType, point: Point, side: Side) { @@ -259,7 +278,7 @@ impl<'a, N: Notify + 'a, T: EventListener> input::ActionContext for ActionCon }, Some(selection) if !selection.is_empty() => { selection.ty = ty; - self.terminal.dirty = true; + self.dirty = true; }, _ => self.start_selection(ty, point, side), } @@ -269,8 +288,8 @@ impl<'a, N: Notify + 'a, T: EventListener> input::ActionContext for ActionCon let x = self.mouse.x as usize; let y = self.mouse.y as usize; - if self.size_info.contains_point(x, y) { - Some(self.size_info.pixels_to_coords(x, y)) + if self.display.size_info.contains_point(x, y) { + Some(self.display.size_info.pixels_to_coords(x, y)) } else { None } @@ -309,12 +328,12 @@ impl<'a, N: Notify + 'a, T: EventListener> input::ActionContext for ActionCon #[inline] fn window(&self) -> &Window { - self.window + &self.display.window } #[inline] fn window_mut(&mut self) -> &mut Window { - self.window + &mut self.display.window } #[inline] @@ -387,17 +406,21 @@ impl<'a, N: Notify + 'a, T: EventListener> input::ActionContext for ActionCon } } + 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); self.display_update_pending.set_font(font); - self.terminal.dirty = true; + self.dirty = true; } fn reset_font_size(&mut self) { *self.font_size = self.config.ui_config.font.size(); self.display_update_pending.set_font(self.config.ui_config.font.clone()); - self.terminal.dirty = true; + self.dirty = true; } #[inline] @@ -405,6 +428,7 @@ impl<'a, N: Notify + 'a, T: EventListener> input::ActionContext for ActionCon if !self.message_buffer.is_empty() { self.display_update_pending.dirty = true; self.message_buffer.pop(); + self.dirty = true; } } @@ -437,7 +461,7 @@ impl<'a, N: Notify + 'a, T: EventListener> input::ActionContext for ActionCon } self.display_update_pending.dirty = true; - self.terminal.dirty = true; + self.dirty = true; } #[inline] @@ -458,8 +482,6 @@ impl<'a, N: Notify + 'a, T: EventListener> input::ActionContext for ActionCon #[inline] fn cancel_search(&mut self) { - self.terminal.cancel_search(); - if self.terminal.mode().contains(TermMode::VI) { // Recover pre-search state in vi mode. self.search_reset_state(); @@ -471,6 +493,8 @@ impl<'a, N: Notify + 'a, T: EventListener> input::ActionContext for ActionCon self.update_selection(end, Side::Right); } + self.search_state.dfas = None; + self.exit_search(); } @@ -591,6 +615,29 @@ impl<'a, N: Notify + 'a, T: EventListener> input::ActionContext for ActionCon self.search_state.origin = origin_relative; } + /// Find the next search match. + fn search_next( + &mut self, + origin: Point, + direction: Direction, + side: Side, + ) -> Option { + self.search_state + .dfas + .as_ref() + .and_then(|dfas| self.terminal.search_next(dfas, origin, direction, side, None)) + } + + #[inline] + fn search_direction(&self) -> Direction { + self.search_state.direction + } + + #[inline] + fn search_active(&self) -> bool { + self.search_state.history_index.is_some() + } + /// Handle keyboard typing start. /// /// This will temporarily disable some features like terminal cursor blinking or the mouse @@ -603,24 +650,27 @@ impl<'a, N: Notify + 'a, T: EventListener> input::ActionContext for ActionCon let blink_interval = self.config.cursor.blink_interval(); if let Some(timer) = self.scheduler.get_mut(TimerId::BlinkCursor) { timer.deadline = Instant::now() + Duration::from_millis(blink_interval); - *self.cursor_hidden = false; - self.terminal.dirty = true; + self.display.cursor_hidden = false; + self.dirty = true; } // Hide mouse cursor. if self.config.ui_config.mouse.hide_when_typing { - self.window.set_mouse_visible(false); + self.display.window.set_mouse_visible(false); } } + /// Toggle the vi mode status. #[inline] - fn search_direction(&self) -> Direction { - self.search_state.direction - } + fn toggle_vi_mode(&mut self) { + if !self.terminal.mode().contains(TermMode::VI) { + self.clear_selection(); + } - #[inline] - fn search_active(&self) -> bool { - self.search_state.history_index.is_some() + self.cancel_search(); + self.terminal.toggle_vi_mode(); + + self.dirty = true; } fn message(&self) -> Option<&Message> { @@ -636,7 +686,7 @@ impl<'a, N: Notify + 'a, T: EventListener> input::ActionContext for ActionCon } fn urls(&self) -> &Urls { - self.urls + &self.display.urls } fn clipboard_mut(&mut self) -> &mut Clipboard { @@ -657,22 +707,22 @@ impl<'a, N: Notify + 'a, T: EventListener> ActionContext<'a, N, T> { // Hide cursor while typing into the search bar. if self.config.ui_config.mouse.hide_when_typing { - self.window.set_mouse_visible(false); + self.display.window.set_mouse_visible(false); } if regex.is_empty() { // Stop search if there's nothing to search for. self.search_reset_state(); - self.terminal.cancel_search(); + self.search_state.dfas = None; } else { - // Create terminal search from the new regex string. - self.terminal.start_search(®ex); + // Create search dfas for the new regex string. + self.search_state.dfas = RegexSearch::new(®ex).ok(); // Update search highlighting. self.goto_match(MAX_SEARCH_WHILE_TYPING); } - self.terminal.dirty = true; + self.dirty = true; } /// Reset terminal to the state before search was started. @@ -697,22 +747,26 @@ impl<'a, N: Notify + 'a, T: EventListener> ActionContext<'a, N, T> { // Reset vi mode cursor. let mut origin = self.search_state.origin; origin.line = min(origin.line, self.terminal.screen_lines() - 1); - origin.col = min(origin.col, self.terminal.cols() - 1); + origin.column = min(origin.column, self.terminal.cols() - 1); self.terminal.vi_mode_cursor.point = origin; + + self.dirty = true; } /// Jump to the first regex match from the search origin. fn goto_match(&mut self, mut limit: Option) { - if self.search_state.history_index.is_none() { - return; - } + let dfas = match &self.search_state.dfas { + Some(dfas) => dfas, + None => return, + }; // Limit search only when enough lines are available to run into the limit. limit = limit.filter(|&limit| limit <= self.terminal.total_lines()); // Jump to the next match. let direction = self.search_state.direction; - match self.terminal.search_next(self.absolute_origin(), direction, Side::Left, limit) { + let origin = self.absolute_origin(); + match self.terminal.search_next(dfas, origin, direction, Side::Left, limit) { Some(regex_match) => { let old_offset = self.terminal.grid().display_offset() as isize; @@ -752,7 +806,7 @@ impl<'a, N: Notify + 'a, T: EventListener> ActionContext<'a, N, T> { }, } - self.terminal.dirty = true; + self.dirty = true; } /// Cleanup the search state. @@ -767,7 +821,7 @@ impl<'a, N: Notify + 'a, T: EventListener> ActionContext<'a, N, T> { self.display_update_pending.dirty = true; self.search_state.history_index = None; - self.terminal.dirty = true; + self.dirty = true; // Clear focused match. self.search_state.focused_match = None; @@ -781,7 +835,7 @@ impl<'a, N: Notify + 'a, T: EventListener> ActionContext<'a, N, T> { fn absolute_origin(&self) -> Point { let mut relative_origin = self.search_state.origin; relative_origin.line = min(relative_origin.line, self.terminal.screen_lines() - 1); - relative_origin.col = min(relative_origin.col, self.terminal.cols() - 1); + relative_origin.column = min(relative_origin.column, self.terminal.cols() - 1); let mut origin = self.terminal.visible_to_buffer(relative_origin); origin.line = (origin.line as isize + self.search_state.display_offset_delta) as usize; origin @@ -809,8 +863,8 @@ impl<'a, N: Notify + 'a, T: EventListener> ActionContext<'a, N, T> { TimerId::BlinkCursor, ) } else { - *self.cursor_hidden = false; - self.terminal.dirty = true; + self.display.cursor_hidden = false; + self.dirty = true; } } } @@ -1012,27 +1066,26 @@ impl Processor { notifier: &mut self.notifier, mouse: &mut self.mouse, clipboard: &mut self.clipboard, - size_info: &mut self.display.size_info, received_count: &mut self.received_count, suppress_chars: &mut self.suppress_chars, modifiers: &mut self.modifiers, message_buffer: &mut self.message_buffer, display_update_pending: &mut display_update_pending, - window: &mut self.display.window, + display: &mut self.display, font_size: &mut self.font_size, config: &mut self.config, - urls: &self.display.urls, scheduler: &mut scheduler, search_state: &mut self.search_state, cli_options: &self.cli_options, - cursor_hidden: &mut self.display.cursor_hidden, + dirty: false, event_loop, }; - let mut processor = input::Processor::new(context, &self.display.highlighted_url); + let mut processor = input::Processor::new(context); for event in self.event_queue.drain(..) { Processor::handle_event(event, &mut processor); } + let dirty = processor.ctx.dirty; // Process DisplayUpdate events. if display_update_pending.dirty { @@ -1045,11 +1098,9 @@ impl Processor { return; } - if terminal.dirty { - terminal.dirty = false; - + if dirty { // Request immediate re-draw if visual bell animation is not finished yet. - if !terminal.visual_bell.completed() { + if !self.display.visual_bell.completed() { let event: Event = TerminalEvent::Wakeup.into(); self.event_queue.push(event.into()); @@ -1079,7 +1130,7 @@ impl Processor { /// Doesn't take self mutably due to borrow checking. fn handle_event( event: GlutinEvent<'_, Event>, - processor: &mut input::Processor<'_, T, ActionContext<'_, N, T>>, + processor: &mut input::Processor>, ) where T: EventListener, { @@ -1095,40 +1146,48 @@ impl Processor { // 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.dpr = scale_factor; - processor.ctx.terminal.dirty = true; + processor.ctx.window_mut().dpr = scale_factor; + processor.ctx.dirty = true; }, Event::Message(message) => { processor.ctx.message_buffer.push(message); processor.ctx.display_update_pending.dirty = true; - processor.ctx.terminal.dirty = true; + processor.ctx.dirty = true; }, Event::SearchNext => processor.ctx.goto_match(None), Event::ConfigReload(path) => Self::reload_config(&path, processor), Event::Scroll(scroll) => processor.ctx.scroll(scroll), Event::BlinkCursor => { - *processor.ctx.cursor_hidden ^= true; - processor.ctx.terminal.dirty = true; + processor.ctx.display.cursor_hidden ^= true; + processor.ctx.dirty = true; }, Event::TerminalEvent(event) => match event { TerminalEvent::Title(title) => { let ui_config = &processor.ctx.config.ui_config; if ui_config.window.dynamic_title { - processor.ctx.window.set_title(&title); + processor.ctx.window_mut().set_title(&title); } }, TerminalEvent::ResetTitle => { let ui_config = &processor.ctx.config.ui_config; if ui_config.window.dynamic_title { - processor.ctx.window.set_title(&ui_config.window.title); + processor.ctx.display.window.set_title(&ui_config.window.title); } }, - TerminalEvent::Wakeup => processor.ctx.terminal.dirty = true, + TerminalEvent::Wakeup => processor.ctx.dirty = true, TerminalEvent::Bell => { - let bell_command = processor.ctx.config.bell().command.as_ref(); - let _ = bell_command.map(|cmd| start_daemon(cmd.program(), cmd.args())); + // Set window urgency. if processor.ctx.terminal.mode().contains(TermMode::URGENCY_HINTS) { - processor.ctx.window.set_urgent(!processor.ctx.terminal.is_focused); + let focused = processor.ctx.terminal.is_focused; + processor.ctx.window_mut().set_urgent(!focused); + } + + // Ring visual bell. + processor.ctx.display.visual_bell.ring(); + + // Execute bell command. + if let Some(bell_command) = &processor.ctx.config.ui_config.bell.command { + start_daemon(bell_command.program(), bell_command.args()); } }, TerminalEvent::ClipboardStore(clipboard_type, content) => { @@ -1138,6 +1197,10 @@ impl Processor { let text = format(processor.ctx.clipboard.load(clipboard_type).as_str()); processor.ctx.write_to_pty(text.into_bytes()); }, + TerminalEvent::ColorRequest(index, format) => { + let text = format(processor.ctx.display.colors[index]); + processor.ctx.write_to_pty(text.into_bytes()); + }, TerminalEvent::MouseCursorDirty => processor.reset_mouse_cursor(), TerminalEvent::Exit => (), TerminalEvent::CursorBlinkingChange(_) => { @@ -1145,7 +1208,7 @@ impl Processor { }, }, }, - GlutinEvent::RedrawRequested(_) => processor.ctx.terminal.dirty = true, + GlutinEvent::RedrawRequested(_) => processor.ctx.dirty = true, GlutinEvent::WindowEvent { event, window_id, .. } => { match event { WindowEvent::CloseRequested => processor.ctx.terminal.exit(), @@ -1159,37 +1222,37 @@ impl Processor { } processor.ctx.display_update_pending.set_dimensions(size); - processor.ctx.terminal.dirty = true; + processor.ctx.dirty = true; }, WindowEvent::KeyboardInput { input, is_synthetic: false, .. } => { processor.key_input(input); }, + WindowEvent::ModifiersChanged(modifiers) => { + processor.modifiers_input(modifiers) + }, WindowEvent::ReceivedCharacter(c) => processor.received_char(c), WindowEvent::MouseInput { state, button, .. } => { - processor.ctx.window.set_mouse_visible(true); + processor.ctx.window_mut().set_mouse_visible(true); processor.mouse_input(state, button); - processor.ctx.terminal.dirty = true; - }, - WindowEvent::ModifiersChanged(modifiers) => { - processor.modifiers_input(modifiers) + processor.ctx.dirty = true; }, WindowEvent::CursorMoved { position, .. } => { - processor.ctx.window.set_mouse_visible(true); + processor.ctx.window_mut().set_mouse_visible(true); processor.mouse_moved(position); }, WindowEvent::MouseWheel { delta, phase, .. } => { - processor.ctx.window.set_mouse_visible(true); + processor.ctx.window_mut().set_mouse_visible(true); processor.mouse_wheel_input(delta, phase); }, WindowEvent::Focused(is_focused) => { - if window_id == processor.ctx.window.window_id() { + if window_id == processor.ctx.window().window_id() { processor.ctx.terminal.is_focused = is_focused; - processor.ctx.terminal.dirty = true; + processor.ctx.dirty = true; if is_focused { - processor.ctx.window.set_urgent(false); + processor.ctx.window_mut().set_urgent(false); } else { - processor.ctx.window.set_mouse_visible(true); + processor.ctx.window_mut().set_mouse_visible(true); } processor.ctx.update_cursor_blinking(); @@ -1203,8 +1266,8 @@ impl Processor { WindowEvent::CursorLeft { .. } => { processor.ctx.mouse.inside_text_area = false; - if processor.highlighted_url.is_some() { - processor.ctx.terminal.dirty = true; + if processor.ctx.highlighted_url().is_some() { + processor.ctx.dirty = true; } }, WindowEvent::KeyboardInput { is_synthetic: true, .. } @@ -1253,10 +1316,9 @@ impl Processor { } } - fn reload_config( - path: &PathBuf, - processor: &mut input::Processor<'_, T, ActionContext<'_, N, T>>, - ) where + /// Reload the configuration files from disk. + fn reload_config(path: &Path, processor: &mut input::Processor>) + where T: EventListener, { if !processor.ctx.message_buffer.is_empty() { @@ -1269,6 +1331,7 @@ impl Processor { Err(_) => return, }; + processor.ctx.display.update_config(&config); processor.ctx.terminal.update_config(&config); // Reload cursor if its thickness has changed. @@ -1300,12 +1363,12 @@ impl Processor { if !config.ui_config.window.dynamic_title || processor.ctx.config.ui_config.window.title != config.ui_config.window.title { - processor.ctx.window.set_title(&config.ui_config.window.title); + processor.ctx.window_mut().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.set_wayland_theme(&config.colors); + processor.ctx.window_mut().set_wayland_theme(&config.ui_config.colors); } // Set subpixel anti-aliasing. @@ -1314,14 +1377,14 @@ impl Processor { // Disable shadows for transparent windows on macOS. #[cfg(target_os = "macos")] - processor.ctx.window.set_has_shadow(config.ui_config.background_opacity() >= 1.0); + processor.ctx.window_mut().set_has_shadow(config.ui_config.background_opacity() >= 1.0); *processor.ctx.config = config; // Update cursor blinking. processor.ctx.update_cursor_blinking(); - processor.ctx.terminal.dirty = true; + processor.ctx.dirty = true; } /// Submit the pending changes to the `Display`. diff --git a/alacritty/src/input.rs b/alacritty/src/input.rs index 155fab07..778dffc7 100644 --- a/alacritty/src/input.rs +++ b/alacritty/src/input.rs @@ -26,17 +26,18 @@ use alacritty_terminal::event::EventListener; use alacritty_terminal::grid::{Dimensions, Scroll}; use alacritty_terminal::index::{Boundary, Column, Direction, Line, Point, Side}; use alacritty_terminal::selection::SelectionType; +use alacritty_terminal::term::search::Match; use alacritty_terminal::term::{ClipboardType, SizeInfo, Term, TermMode}; use alacritty_terminal::vi_mode::ViMotion; use crate::clipboard::Clipboard; use crate::config::{Action, Binding, BindingMode, Config, Key, SearchAction, ViAction}; use crate::daemon::start_daemon; +use crate::display::window::Window; 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}; -use crate::window::Window; /// Font size change interval. pub const FONT_SIZE_STEP: f32 = 0.5; @@ -54,20 +55,20 @@ const SELECTION_SCROLLING_STEP: f64 = 20.; /// /// An escape sequence may be emitted in case specific keys or key combinations /// are activated. -pub struct Processor<'a, T: EventListener, A: ActionContext> { +pub struct Processor> { pub ctx: A, - pub highlighted_url: &'a Option, _phantom: PhantomData, } pub trait ActionContext { - fn write_to_pty>>(&mut self, data: B); + fn write_to_pty>>(&mut self, _data: B) {} + fn mark_dirty(&mut self) {} fn size_info(&self) -> SizeInfo; - fn copy_selection(&mut self, ty: ClipboardType); - fn start_selection(&mut self, ty: SelectionType, point: Point, side: Side); - fn toggle_selection(&mut self, ty: SelectionType, point: Point, side: Side); - fn update_selection(&mut self, point: Point, side: Side); - fn clear_selection(&mut self); + fn copy_selection(&mut self, _ty: ClipboardType) {} + fn start_selection(&mut self, _ty: SelectionType, _point: Point, _side: Side) {} + fn toggle_selection(&mut self, _ty: SelectionType, _point: Point, _side: Side) {} + fn update_selection(&mut self, _point: Point, _side: Side) {} + fn clear_selection(&mut self) {} fn selection_is_empty(&self) -> bool; fn mouse_mut(&mut self) -> &mut Mouse; fn mouse(&self) -> &Mouse; @@ -75,34 +76,42 @@ pub trait ActionContext { fn received_count(&mut self) -> &mut usize; fn suppress_chars(&mut self) -> &mut bool; fn modifiers(&mut self) -> &mut ModifiersState; - fn scroll(&mut self, scroll: Scroll); + fn scroll(&mut self, _scroll: Scroll) {} fn window(&self) -> &Window; fn window_mut(&mut self) -> &mut Window; fn terminal(&self) -> &Term; fn terminal_mut(&mut self) -> &mut Term; - fn spawn_new_instance(&mut self); - fn change_font_size(&mut self, delta: f32); - fn reset_font_size(&mut self); - fn pop_message(&mut self); + fn spawn_new_instance(&mut self) {} + fn change_font_size(&mut self, _delta: f32) {} + fn reset_font_size(&mut self) {} + fn pop_message(&mut self) {} fn message(&self) -> Option<&Message>; fn config(&self) -> &Config; fn event_loop(&self) -> &EventLoopWindowTarget; fn urls(&self) -> &Urls; - fn launch_url(&self, url: Url); + 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; - fn start_search(&mut self, direction: Direction); - fn confirm_search(&mut self); - fn cancel_search(&mut self); - fn search_input(&mut self, c: char); - fn search_pop_word(&mut self); - fn search_history_previous(&mut self); - fn search_history_next(&mut self); - fn advance_search_origin(&mut self, direction: Direction); + fn start_search(&mut self, _direction: Direction) {} + fn confirm_search(&mut self) {} + fn cancel_search(&mut self) {} + fn search_input(&mut self, _c: char) {} + fn search_pop_word(&mut self) {} + fn search_history_previous(&mut self) {} + fn search_history_next(&mut self) {} + fn search_next( + &mut self, + origin: Point, + direction: Direction, + side: Side, + ) -> Option; + fn advance_search_origin(&mut self, _direction: Direction) {} fn search_direction(&self) -> Direction; fn search_active(&self) -> bool; - fn on_typing_start(&mut self); + fn on_typing_start(&mut self) {} + fn toggle_vi_mode(&mut self) {} } trait Execute { @@ -120,8 +129,8 @@ impl Execute for Binding { impl Action { fn toggle_selection(ctx: &mut A, ty: SelectionType) where - T: EventListener, A: ActionContext, + T: EventListener, { let cursor_point = ctx.terminal().vi_mode_cursor.point; ctx.toggle_selection(ty, cursor_point, Side::Left); @@ -151,10 +160,11 @@ impl Execute for Action { start_daemon(program, args); }, - Action::ToggleViMode => ctx.terminal_mut().toggle_vi_mode(), + Action::ToggleViMode => ctx.toggle_vi_mode(), Action::ViMotion(motion) => { ctx.on_typing_start(); - ctx.terminal_mut().vi_motion(motion) + ctx.terminal_mut().vi_motion(motion); + ctx.mark_dirty(); }, Action::ViAction(ViAction::ToggleNormalSelection) => { Self::toggle_selection(ctx, SelectionType::Simple) @@ -183,9 +193,9 @@ impl Execute for Action { Direction::Left => vi_point.sub_absolute(terminal, Boundary::Wrap, 1), }; - let regex_match = terminal.search_next(origin, direction, Side::Left, None); - if let Some(regex_match) = regex_match { + if let Some(regex_match) = ctx.search_next(origin, direction, Side::Left) { ctx.terminal_mut().vi_goto_point(*regex_match.start()); + ctx.mark_dirty(); } }, Action::ViAction(ViAction::SearchPrevious) => { @@ -197,9 +207,9 @@ impl Execute for Action { Direction::Left => vi_point.sub_absolute(terminal, Boundary::Wrap, 1), }; - let regex_match = terminal.search_next(origin, direction, Side::Left, None); - if let Some(regex_match) = regex_match { + if let Some(regex_match) = ctx.search_next(origin, direction, Side::Left) { ctx.terminal_mut().vi_goto_point(*regex_match.start()); + ctx.mark_dirty(); } }, Action::ViAction(ViAction::SearchStart) => { @@ -208,9 +218,9 @@ impl Execute for Action { .visible_to_buffer(terminal.vi_mode_cursor.point) .sub_absolute(terminal, Boundary::Wrap, 1); - let regex_match = terminal.search_next(origin, Direction::Left, Side::Left, None); - if let Some(regex_match) = regex_match { + if let Some(regex_match) = ctx.search_next(origin, Direction::Left, Side::Left) { ctx.terminal_mut().vi_goto_point(*regex_match.start()); + ctx.mark_dirty(); } }, Action::ViAction(ViAction::SearchEnd) => { @@ -219,9 +229,9 @@ impl Execute for Action { .visible_to_buffer(terminal.vi_mode_cursor.point) .add_absolute(terminal, Boundary::Wrap, 1); - let regex_match = terminal.search_next(origin, Direction::Right, Side::Right, None); - if let Some(regex_match) = regex_match { + if let Some(regex_match) = ctx.search_next(origin, Direction::Right, Side::Right) { ctx.terminal_mut().vi_goto_point(*regex_match.end()); + ctx.mark_dirty(); } }, Action::SearchAction(SearchAction::SearchFocusNext) => { @@ -328,6 +338,7 @@ impl Execute for Action { // Move vi mode cursor. ctx.terminal_mut().vi_mode_cursor.point.line = Line(0); ctx.terminal_mut().vi_motion(ViMotion::FirstOccupied); + ctx.mark_dirty(); }, Action::ScrollToBottom => { ctx.scroll(Scroll::Bottom); @@ -339,6 +350,7 @@ impl Execute for Action { // Move to beginning twice, to always jump across linewraps. term.vi_motion(ViMotion::FirstOccupied); term.vi_motion(ViMotion::FirstOccupied); + ctx.mark_dirty(); }, Action::ClearHistory => ctx.terminal_mut().clear_screen(ClearMode::Saved), Action::ClearLogNotice => ctx.pop_message(), @@ -387,9 +399,9 @@ impl From for CursorIcon { } } -impl<'a, T: EventListener, A: ActionContext> Processor<'a, T, A> { - pub fn new(ctx: A, highlighted_url: &'a Option) -> Self { - Self { ctx, highlighted_url, _phantom: Default::default() } +impl> Processor { + pub fn new(ctx: A) -> Self { + Self { ctx, _phantom: Default::default() } } #[inline] @@ -415,7 +427,7 @@ impl<'a, T: EventListener, A: ActionContext> Processor<'a, T, A> { let cell_side = self.get_mouse_side(); let cell_changed = - point.line != self.ctx.mouse().line || point.col != self.ctx.mouse().column; + point.line != self.ctx.mouse().line || point.column != self.ctx.mouse().column; // Update mouse state and check for URL change. let mouse_state = self.mouse_state(); @@ -433,7 +445,7 @@ impl<'a, T: EventListener, A: ActionContext> Processor<'a, T, A> { self.ctx.mouse_mut().inside_text_area = inside_text_area; self.ctx.mouse_mut().cell_side = cell_side; self.ctx.mouse_mut().line = point.line; - self.ctx.mouse_mut().column = point.col; + self.ctx.mouse_mut().column = point.column; // Don't launch URLs if mouse has moved. self.ctx.mouse_mut().block_url_launcher = true; @@ -757,7 +769,7 @@ impl<'a, T: EventListener, A: ActionContext> Processor<'a, T, A> { // Try to restore vi mode cursor position, to keep it above its previous content. let term = self.ctx.terminal_mut(); term.vi_mode_cursor.point = term.grid().clamp_buffer_to_visible(absolute); - term.vi_mode_cursor.point.col = absolute.col; + term.vi_mode_cursor.point.column = absolute.column; // Update selection. if term.mode().contains(TermMode::VI) { @@ -986,12 +998,13 @@ impl<'a, T: EventListener, A: ActionContext> Processor<'a, T, A> { /// 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) != self.highlighted_url.as_ref() { - self.ctx.terminal_mut().dirty = true; + if Some(url) != highlighted_url { + self.ctx.mark_dirty(); } - } else if self.highlighted_url.is_some() { - self.ctx.terminal_mut().dirty = true; + } else if highlighted_url.is_some() { + self.ctx.mark_dirty(); } } @@ -1084,10 +1097,7 @@ mod tests { const KEY: VirtualKeyCode = VirtualKeyCode::Key0; struct MockEventProxy; - - impl EventListener for MockEventProxy { - fn send_event(&self, _event: TerminalEvent) {} - } + impl EventListener for MockEventProxy {} struct ActionContext<'a, T> { pub terminal: &'a mut Term, @@ -1103,39 +1113,14 @@ mod tests { } impl<'a, T: EventListener> super::ActionContext for ActionContext<'a, T> { - fn write_to_pty>>(&mut self, _val: B) {} - - fn update_selection(&mut self, _point: Point, _side: Side) {} - - fn start_selection(&mut self, _ty: SelectionType, _point: Point, _side: Side) {} - - fn toggle_selection(&mut self, _ty: SelectionType, _point: Point, _side: Side) {} - - fn copy_selection(&mut self, _: ClipboardType) {} - - fn clear_selection(&mut self) {} - - fn spawn_new_instance(&mut self) {} - - fn change_font_size(&mut self, _delta: f32) {} - - fn reset_font_size(&mut self) {} - - fn start_search(&mut self, _direction: Direction) {} - - fn confirm_search(&mut self) {} - - fn cancel_search(&mut self) {} - - fn search_input(&mut self, _c: char) {} - - fn search_pop_word(&mut self) {} - - fn search_history_previous(&mut self) {} - - fn search_history_next(&mut self) {} - - fn advance_search_origin(&mut self, _direction: Direction) {} + fn search_next( + &mut self, + _origin: Point, + _direction: Direction, + _side: Side, + ) -> Option { + None + } fn search_direction(&self) -> Direction { Direction::Right @@ -1234,17 +1219,13 @@ mod tests { unimplemented!(); } - fn launch_url(&self, _: Url) { + fn highlighted_url(&self) -> Option<&Url> { unimplemented!(); } fn scheduler_mut(&mut self) -> &mut Scheduler { unimplemented!(); } - - fn on_typing_start(&mut self) { - unimplemented!(); - } } macro_rules! test_clickstate { @@ -1293,7 +1274,7 @@ mod tests { config: &cfg, }; - let mut processor = Processor::new(context, &None); + let mut processor = Processor::new(context); let event: GlutinEvent::<'_, TerminalEvent> = $input; if let GlutinEvent::WindowEvent { diff --git a/alacritty/src/main.rs b/alacritty/src/main.rs index 1bcf64b9..30eff73d 100644 --- a/alacritty/src/main.rs +++ b/alacritty/src/main.rs @@ -32,7 +32,6 @@ use alacritty_terminal::tty; mod cli; mod clipboard; mod config; -mod cursor; mod daemon; mod display; mod event; @@ -41,16 +40,11 @@ mod logging; #[cfg(target_os = "macos")] mod macos; mod message_bar; -mod meter; #[cfg(windows)] mod panic; mod renderer; mod scheduler; mod url; -mod window; - -#[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))] -mod wayland_theme; mod gl { #![allow(clippy::all)] diff --git a/alacritty/src/meter.rs b/alacritty/src/meter.rs deleted file mode 100644 index c07d901f..00000000 --- a/alacritty/src/meter.rs +++ /dev/null @@ -1,97 +0,0 @@ -//! Rendering time meter. -//! -//! Used to track rendering times and provide moving averages. -//! -//! # Examples -//! -//! ```rust -//! // create a meter -//! let mut meter = alacritty_terminal::meter::Meter::new(); -//! -//! // Sample something. -//! { -//! let _sampler = meter.sampler(); -//! } -//! -//! // Get the moving average. The meter tracks a fixed number of samples, and -//! // the average won't mean much until it's filled up at least once. -//! println!("Average time: {}", meter.average()); -//! ``` - -use std::time::{Duration, Instant}; - -const NUM_SAMPLES: usize = 10; - -/// The meter. -#[derive(Default)] -pub struct Meter { - /// Track last 60 timestamps. - times: [f64; NUM_SAMPLES], - - /// Average sample time in microseconds. - avg: f64, - - /// Index of next time to update.. - index: usize, -} - -/// Sampler. -/// -/// Samplers record how long they are "alive" for and update the meter on drop.. -pub struct Sampler<'a> { - /// Reference to meter that created the sampler. - meter: &'a mut Meter, - - /// When the sampler was created. - created_at: Instant, -} - -impl<'a> Sampler<'a> { - fn new(meter: &'a mut Meter) -> Sampler<'a> { - Sampler { meter, created_at: Instant::now() } - } - - #[inline] - fn alive_duration(&self) -> Duration { - self.created_at.elapsed() - } -} - -impl<'a> Drop for Sampler<'a> { - fn drop(&mut self) { - self.meter.add_sample(self.alive_duration()); - } -} - -impl Meter { - /// Create a meter. - pub fn new() -> Meter { - Default::default() - } - - /// Get a sampler. - pub fn sampler(&mut self) -> Sampler<'_> { - Sampler::new(self) - } - - /// Get the current average sample duration in microseconds. - pub fn average(&self) -> f64 { - self.avg - } - - /// Add a sample. - /// - /// Used by Sampler::drop. - fn add_sample(&mut self, sample: Duration) { - let mut usec = 0f64; - - usec += f64::from(sample.subsec_nanos()) / 1e3; - usec += (sample.as_secs() as f64) * 1e6; - - let prev = self.times[self.index]; - self.times[self.index] = usec; - self.avg -= prev / NUM_SAMPLES as f64; - self.avg += usec / NUM_SAMPLES as f64; - self.index = (self.index + 1) % NUM_SAMPLES; - } -} diff --git a/alacritty/src/renderer/mod.rs b/alacritty/src/renderer/mod.rs index 39e53b82..1f203332 100644 --- a/alacritty/src/renderer/mod.rs +++ b/alacritty/src/renderer/mod.rs @@ -17,11 +17,11 @@ use unicode_width::UnicodeWidthChar; use alacritty_terminal::index::Point; use alacritty_terminal::term::cell::Flags; use alacritty_terminal::term::color::Rgb; -use alacritty_terminal::term::render::RenderableCell; use alacritty_terminal::term::SizeInfo; use crate::config::font::{Font, FontDescription}; use crate::config::ui_config::{Delta, UIConfig}; +use crate::display::content::RenderableCell; use crate::gl; use crate::gl::types::*; use crate::renderer::rects::{RectRenderer, RenderRect}; @@ -480,8 +480,8 @@ impl Batch { cell_flags.set(RenderingGlyphFlags::WIDE_CHAR, cell.flags.contains(Flags::WIDE_CHAR)); self.instances.push(InstanceData { - col: cell.column.0 as u16, - row: cell.line.0 as u16, + col: cell.point.column.0 as u16, + row: cell.point.line.0 as u16, top: glyph.top, left: glyph.left, @@ -829,8 +829,7 @@ impl<'a> RenderApi<'a> { .chars() .enumerate() .map(|(i, character)| RenderableCell { - line: point.line, - column: point.col + i, + point: Point::new(point.line, point.column + i), character, zerowidth: None, flags: Flags::empty(), diff --git a/alacritty/src/renderer/rects.rs b/alacritty/src/renderer/rects.rs index cfd17379..80886c95 100644 --- a/alacritty/src/renderer/rects.rs +++ b/alacritty/src/renderer/rects.rs @@ -6,9 +6,9 @@ use crossfont::Metrics; use alacritty_terminal::index::{Column, Point}; use alacritty_terminal::term::cell::Flags; use alacritty_terminal::term::color::Rgb; -use alacritty_terminal::term::render::RenderableCell; use alacritty_terminal::term::SizeInfo; +use crate::display::content::RenderableCell; use crate::gl; use crate::gl::types::*; use crate::renderer; @@ -105,8 +105,8 @@ impl RenderLine { mut thickness: f32, color: Rgb, ) -> RenderRect { - let start_x = start.col.0 as f32 * size.cell_width(); - let end_x = (end.col.0 + 1) as f32 * size.cell_width(); + let start_x = start.column.0 as f32 * size.cell_width(); + let end_x = (end.column.0 + 1) as f32 * size.cell_width(); let width = end_x - start_x; // Make sure lines are always visible. @@ -169,16 +169,16 @@ impl RenderLines { } // Include wide char spacer if the current cell is a wide char. - let mut end: Point = cell.into(); + let mut end = cell.point; if cell.flags.contains(Flags::WIDE_CHAR) { - end.col += 1; + end.column += 1; } // Check if there's an active line. if let Some(line) = self.inner.get_mut(&flag).and_then(|lines| lines.last_mut()) { if cell.fg == line.color - && cell.column == line.end.col + 1 - && cell.line == line.end.line + && cell.point.column == line.end.column + 1 + && cell.point.line == line.end.line { // Update the length of the line. line.end = end; @@ -187,7 +187,7 @@ impl RenderLines { } // Start new line if there currently is none. - let line = RenderLine { start: cell.into(), end, color: cell.fg }; + let line = RenderLine { start: cell.point, end, color: cell.fg }; match self.inner.get_mut(&flag) { Some(lines) => lines.push(line), None => { diff --git a/alacritty/src/url.rs b/alacritty/src/url.rs index f4bf8205..d86b514a 100644 --- a/alacritty/src/url.rs +++ b/alacritty/src/url.rs @@ -8,10 +8,10 @@ use urlocator::{UrlLocation, UrlLocator}; use alacritty_terminal::index::{Column, Point}; use alacritty_terminal::term::cell::Flags; use alacritty_terminal::term::color::Rgb; -use alacritty_terminal::term::render::RenderableCell; use alacritty_terminal::term::SizeInfo; use crate::config::Config; +use crate::display::content::RenderableCell; use crate::event::Mouse; use crate::renderer::rects::{RenderLine, RenderRect}; @@ -73,12 +73,12 @@ impl Urls { // Update tracked URLs. pub fn update(&mut self, num_cols: Column, cell: &RenderableCell) { - let point: Point = cell.into(); + let point = cell.point; let mut end = point; // Include the following wide char spacer. if cell.flags.contains(Flags::WIDE_CHAR) { - end.col += 1; + end.column += 1; } // Reset URL when empty cells have been skipped. @@ -119,13 +119,13 @@ impl Urls { (UrlLocation::Url(_length, end_offset), UrlLocation::Url(..)) => { self.extend_url(point, end, cell.fg, end_offset); }, - (UrlLocation::Scheme, _) => self.scheme_buffer.push((cell.into(), cell.fg)), + (UrlLocation::Scheme, _) => self.scheme_buffer.push((cell.point, cell.fg)), (UrlLocation::Reset, _) => self.reset(), _ => (), } // Reset at un-wrapped linebreak. - if cell.column + 1 == num_cols && !cell.flags.contains(Flags::WRAPLINE) { + if cell.point.column + 1 == num_cols && !cell.flags.contains(Flags::WRAPLINE) { self.reset(); } } @@ -202,8 +202,7 @@ mod tests { .map(|(i, character)| RenderableCell { character, zerowidth: None, - line: Line(0), - column: Column(i), + point: Point::new(Line(0), Column(i)), fg: Default::default(), bg: Default::default(), bg_alpha: 0., @@ -227,8 +226,8 @@ mod tests { } let url = urls.urls.first().unwrap(); - assert_eq!(url.start().col, Column(5)); - assert_eq!(url.end().col, Column(23)); + assert_eq!(url.start().column, Column(5)); + assert_eq!(url.end().column, Column(23)); } #[test] @@ -244,14 +243,14 @@ mod tests { assert_eq!(urls.urls.len(), 3); - assert_eq!(urls.urls[0].start().col, Column(5)); - assert_eq!(urls.urls[0].end().col, Column(9)); + assert_eq!(urls.urls[0].start().column, Column(5)); + assert_eq!(urls.urls[0].end().column, Column(9)); - assert_eq!(urls.urls[1].start().col, Column(11)); - assert_eq!(urls.urls[1].end().col, Column(15)); + assert_eq!(urls.urls[1].start().column, Column(11)); + assert_eq!(urls.urls[1].end().column, Column(15)); - assert_eq!(urls.urls[2].start().col, Column(17)); - assert_eq!(urls.urls[2].end().col, Column(21)); + assert_eq!(urls.urls[2].start().column, Column(17)); + assert_eq!(urls.urls[2].end().column, Column(21)); } #[test] @@ -267,10 +266,10 @@ mod tests { assert_eq!(urls.urls.len(), 2); - assert_eq!(urls.urls[0].start().col, Column(5)); - assert_eq!(urls.urls[0].end().col, Column(17)); + assert_eq!(urls.urls[0].start().column, Column(5)); + assert_eq!(urls.urls[0].end().column, Column(17)); - assert_eq!(urls.urls[1].start().col, Column(20)); - assert_eq!(urls.urls[1].end().col, Column(28)); + assert_eq!(urls.urls[1].start().column, Column(20)); + assert_eq!(urls.urls[1].end().column, Column(28)); } } diff --git a/alacritty/src/wayland_theme.rs b/alacritty/src/wayland_theme.rs deleted file mode 100644 index 5d3bd922..00000000 --- a/alacritty/src/wayland_theme.rs +++ /dev/null @@ -1,80 +0,0 @@ -use glutin::platform::unix::{ARGBColor, Button, ButtonState, Element, Theme as WaylandTheme}; - -use alacritty_terminal::config::Colors; -use alacritty_terminal::term::color::Rgb; - -const INACTIVE_OPACITY: u8 = 127; - -#[derive(Debug, Clone)] -pub struct AlacrittyWaylandTheme { - pub background: ARGBColor, - pub foreground: ARGBColor, - pub dim_foreground: ARGBColor, - pub hovered_close_icon: ARGBColor, - pub hovered_maximize_icon: ARGBColor, - pub hovered_minimize_icon: ARGBColor, -} - -impl AlacrittyWaylandTheme { - pub fn new(colors: &Colors) -> Self { - let hovered_close_icon = colors.normal.red.into_rgba(); - let hovered_maximize_icon = colors.normal.green.into_rgba(); - let hovered_minimize_icon = colors.normal.yellow.into_rgba(); - let foreground = colors.search_bar_foreground().into_rgba(); - let background = colors.search_bar_background().into_rgba(); - - let mut dim_foreground = foreground; - dim_foreground.a = INACTIVE_OPACITY; - - Self { - foreground, - background, - dim_foreground, - hovered_close_icon, - hovered_minimize_icon, - hovered_maximize_icon, - } - } -} - -impl WaylandTheme for AlacrittyWaylandTheme { - fn element_color(&self, element: Element, window_active: bool) -> ARGBColor { - match element { - Element::Bar | Element::Separator => self.background, - Element::Text if window_active => self.foreground, - Element::Text => self.dim_foreground, - } - } - - fn button_color( - &self, - button: Button, - state: ButtonState, - foreground: bool, - window_active: bool, - ) -> ARGBColor { - if !foreground { - return ARGBColor { a: 0, r: 0, g: 0, b: 0 }; - } else if !window_active { - return self.dim_foreground; - } - - match (state, button) { - (ButtonState::Idle, _) => self.foreground, - (ButtonState::Disabled, _) => self.dim_foreground, - (_, Button::Minimize) => self.hovered_minimize_icon, - (_, Button::Maximize) => self.hovered_maximize_icon, - (_, Button::Close) => self.hovered_close_icon, - } - } -} - -trait IntoARGBColor { - fn into_rgba(self) -> ARGBColor; -} - -impl IntoARGBColor for Rgb { - fn into_rgba(self) -> ARGBColor { - ARGBColor { a: 0xff, r: self.r, g: self.g, b: self.b } - } -} diff --git a/alacritty/src/window.rs b/alacritty/src/window.rs deleted file mode 100644 index 3661d406..00000000 --- a/alacritty/src/window.rs +++ /dev/null @@ -1,498 +0,0 @@ -#[rustfmt::skip] -#[cfg(not(any(target_os = "macos", windows)))] -use { - std::sync::atomic::AtomicBool, - std::sync::Arc, - - glutin::platform::unix::{WindowBuilderExtUnix, WindowExtUnix}, -}; - -#[rustfmt::skip] -#[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))] -use { - wayland_client::protocol::wl_surface::WlSurface, - wayland_client::{Attached, EventQueue, Proxy}, - glutin::platform::unix::EventLoopWindowTargetExtUnix, - - alacritty_terminal::config::Colors, - - crate::wayland_theme::AlacrittyWaylandTheme, -}; - -#[rustfmt::skip] -#[cfg(all(feature = "x11", not(any(target_os = "macos", windows))))] -use { - std::io::Cursor, - - x11_dl::xlib::{Display as XDisplay, PropModeReplace, XErrorEvent, Xlib}, - glutin::window::Icon, - png::Decoder, -}; - -use std::fmt::{self, Display, Formatter}; - -#[cfg(target_os = "macos")] -use cocoa::base::{id, NO, YES}; -use glutin::dpi::{PhysicalPosition, PhysicalSize}; -use glutin::event_loop::EventLoop; -#[cfg(target_os = "macos")] -use glutin::platform::macos::{WindowBuilderExtMacOS, WindowExtMacOS}; -#[cfg(windows)] -use glutin::platform::windows::IconExtWindows; -use glutin::window::{ - CursorIcon, Fullscreen, UserAttentionType, Window as GlutinWindow, WindowBuilder, WindowId, -}; -use glutin::{self, ContextBuilder, PossiblyCurrent, WindowedContext}; -#[cfg(target_os = "macos")] -use objc::{msg_send, sel, sel_impl}; -#[cfg(target_os = "macos")] -use raw_window_handle::{HasRawWindowHandle, RawWindowHandle}; -#[cfg(windows)] -use winapi::shared::minwindef::WORD; - -use alacritty_terminal::index::Point; -use alacritty_terminal::term::SizeInfo; - -use crate::config::window::{Decorations, WindowConfig}; -use crate::config::Config; -use crate::gl; - -/// Window icon for `_NET_WM_ICON` property. -#[cfg(all(feature = "x11", not(any(target_os = "macos", windows))))] -static WINDOW_ICON: &[u8] = include_bytes!("../alacritty.png"); - -/// This should match the definition of IDI_ICON from `windows.rc`. -#[cfg(windows)] -const IDI_ICON: WORD = 0x101; - -/// Window errors. -#[derive(Debug)] -pub enum Error { - /// Error creating the window. - ContextCreation(glutin::CreationError), - - /// Error dealing with fonts. - Font(crossfont::Error), - - /// Error manipulating the rendering context. - Context(glutin::ContextError), -} - -/// Result of fallible operations concerning a Window. -type Result = std::result::Result; - -impl std::error::Error for Error { - fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - match self { - Error::ContextCreation(err) => err.source(), - Error::Context(err) => err.source(), - Error::Font(err) => err.source(), - } - } -} - -impl Display for Error { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - match self { - Error::ContextCreation(err) => write!(f, "Error creating GL context; {}", err), - Error::Context(err) => write!(f, "Error operating on render context; {}", err), - Error::Font(err) => err.fmt(f), - } - } -} - -impl From for Error { - fn from(val: glutin::CreationError) -> Self { - Error::ContextCreation(val) - } -} - -impl From for Error { - fn from(val: glutin::ContextError) -> Self { - Error::Context(val) - } -} - -impl From for Error { - fn from(val: crossfont::Error) -> Self { - Error::Font(val) - } -} - -fn create_gl_window( - mut window: WindowBuilder, - event_loop: &EventLoop, - srgb: bool, - vsync: bool, - dimensions: Option>, -) -> Result> { - if let Some(dimensions) = dimensions { - window = window.with_inner_size(dimensions); - } - - let windowed_context = ContextBuilder::new() - .with_srgb(srgb) - .with_vsync(vsync) - .with_hardware_acceleration(None) - .build_windowed(window, event_loop)?; - - // Make the context current so OpenGL operations can run. - let windowed_context = unsafe { windowed_context.make_current().map_err(|(_, err)| err)? }; - - Ok(windowed_context) -} - -/// A window which can be used for displaying the terminal. -/// -/// Wraps the underlying windowing library to provide a stable API in Alacritty. -pub struct Window { - /// Flag tracking frame redraw requests from Wayland compositor. - #[cfg(not(any(target_os = "macos", windows)))] - pub should_draw: Arc, - - /// Attached Wayland surface to request new frame events. - #[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))] - pub wayland_surface: Option>, - - /// Cached DPR for quickly scaling pixel sizes. - pub dpr: f64, - - windowed_context: WindowedContext, - current_mouse_cursor: CursorIcon, - mouse_visible: bool, -} - -impl Window { - /// Create a new window. - /// - /// This creates a window and fully initializes a window. - pub fn new( - event_loop: &EventLoop, - config: &Config, - size: Option>, - #[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))] - wayland_event_queue: Option<&EventQueue>, - ) -> Result { - let window_config = &config.ui_config.window; - let window_builder = Window::get_platform_window(&window_config.title, &window_config); - - // Check if we're running Wayland to disable vsync. - #[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))] - let is_wayland = event_loop.is_wayland(); - #[cfg(any(not(feature = "wayland"), target_os = "macos", windows))] - let is_wayland = false; - - let windowed_context = - create_gl_window(window_builder.clone(), &event_loop, false, !is_wayland, size) - .or_else(|_| { - create_gl_window(window_builder, &event_loop, true, !is_wayland, size) - })?; - - // Text cursor. - let current_mouse_cursor = CursorIcon::Text; - windowed_context.window().set_cursor_icon(current_mouse_cursor); - - // Set OpenGL symbol loader. This call MUST be after window.make_current on windows. - gl::load_with(|symbol| windowed_context.get_proc_address(symbol) as *const _); - - #[cfg(all(feature = "x11", not(any(target_os = "macos", windows))))] - if !is_wayland { - // On X11, embed the window inside another if the parent ID has been set. - if let Some(parent_window_id) = window_config.embed { - x_embed_window(windowed_context.window(), parent_window_id); - } - } - - #[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))] - let wayland_surface = if is_wayland { - // Apply client side decorations theme. - let theme = AlacrittyWaylandTheme::new(&config.colors); - windowed_context.window().set_wayland_theme(theme); - - // Attach surface to Alacritty's internal wayland queue to handle frame callbacks. - let surface = windowed_context.window().wayland_surface().unwrap(); - let proxy: Proxy = unsafe { Proxy::from_c_ptr(surface as _) }; - Some(proxy.attach(wayland_event_queue.as_ref().unwrap().token())) - } else { - None - }; - - let dpr = windowed_context.window().scale_factor(); - - Ok(Self { - current_mouse_cursor, - mouse_visible: true, - windowed_context, - #[cfg(not(any(target_os = "macos", windows)))] - should_draw: Arc::new(AtomicBool::new(true)), - #[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))] - wayland_surface, - dpr, - }) - } - - pub fn set_inner_size(&mut self, size: PhysicalSize) { - self.window().set_inner_size(size); - } - - pub fn inner_size(&self) -> PhysicalSize { - self.window().inner_size() - } - - #[inline] - pub fn set_visible(&self, visibility: bool) { - self.window().set_visible(visibility); - } - - /// Set the window title. - #[inline] - pub fn set_title(&self, title: &str) { - self.window().set_title(title); - } - - #[inline] - pub fn set_mouse_cursor(&mut self, cursor: CursorIcon) { - if cursor != self.current_mouse_cursor { - self.current_mouse_cursor = cursor; - self.window().set_cursor_icon(cursor); - } - } - - /// Set mouse cursor visible. - pub fn set_mouse_visible(&mut self, visible: bool) { - if visible != self.mouse_visible { - self.mouse_visible = visible; - self.window().set_cursor_visible(visible); - } - } - - #[cfg(not(any(target_os = "macos", windows)))] - pub fn get_platform_window(title: &str, window_config: &WindowConfig) -> WindowBuilder { - #[cfg(feature = "x11")] - let icon = { - let decoder = Decoder::new(Cursor::new(WINDOW_ICON)); - let (info, mut reader) = decoder.read_info().expect("invalid embedded icon"); - let mut buf = vec![0; info.buffer_size()]; - let _ = reader.next_frame(&mut buf); - Icon::from_rgba(buf, info.width, info.height) - }; - - let builder = WindowBuilder::new() - .with_title(title) - .with_visible(false) - .with_transparent(true) - .with_decorations(window_config.decorations != Decorations::None) - .with_maximized(window_config.maximized()) - .with_fullscreen(window_config.fullscreen()); - - #[cfg(feature = "x11")] - let builder = builder.with_window_icon(icon.ok()); - - #[cfg(feature = "wayland")] - let builder = builder.with_app_id(window_config.class.instance.to_owned()); - - #[cfg(feature = "x11")] - let builder = builder.with_class( - window_config.class.instance.to_owned(), - window_config.class.general.to_owned(), - ); - - #[cfg(feature = "x11")] - let builder = match &window_config.gtk_theme_variant { - Some(val) => builder.with_gtk_theme_variant(val.clone()), - None => builder, - }; - - builder - } - - #[cfg(windows)] - pub fn get_platform_window(title: &str, window_config: &WindowConfig) -> WindowBuilder { - let icon = glutin::window::Icon::from_resource(IDI_ICON, None); - - WindowBuilder::new() - .with_title(title) - .with_visible(false) - .with_decorations(window_config.decorations != Decorations::None) - .with_transparent(true) - .with_maximized(window_config.maximized()) - .with_fullscreen(window_config.fullscreen()) - .with_window_icon(icon.ok()) - } - - #[cfg(target_os = "macos")] - pub fn get_platform_window(title: &str, window_config: &WindowConfig) -> WindowBuilder { - let window = WindowBuilder::new() - .with_title(title) - .with_visible(false) - .with_transparent(true) - .with_maximized(window_config.maximized()) - .with_fullscreen(window_config.fullscreen()); - - match window_config.decorations { - Decorations::Full => window, - Decorations::Transparent => window - .with_title_hidden(true) - .with_titlebar_transparent(true) - .with_fullsize_content_view(true), - Decorations::Buttonless => window - .with_title_hidden(true) - .with_titlebar_buttons_hidden(true) - .with_titlebar_transparent(true) - .with_fullsize_content_view(true), - Decorations::None => window.with_titlebar_hidden(true), - } - } - - pub fn set_urgent(&self, is_urgent: bool) { - let attention = if is_urgent { Some(UserAttentionType::Critical) } else { None }; - - self.window().request_user_attention(attention); - } - - pub fn set_outer_position(&self, pos: PhysicalPosition) { - self.window().set_outer_position(pos); - } - - #[cfg(all(feature = "x11", not(any(target_os = "macos", windows))))] - pub fn x11_window_id(&self) -> Option { - self.window().xlib_window().map(|xlib_window| xlib_window as usize) - } - - #[cfg(any(not(feature = "x11"), target_os = "macos", windows))] - pub fn x11_window_id(&self) -> Option { - None - } - - pub fn window_id(&self) -> WindowId { - self.window().id() - } - - #[cfg(not(any(target_os = "macos", windows)))] - pub fn set_maximized(&self, maximized: bool) { - self.window().set_maximized(maximized); - } - - pub fn set_minimized(&self, minimized: bool) { - self.window().set_minimized(minimized); - } - - /// Toggle the window's fullscreen state. - pub fn toggle_fullscreen(&mut self) { - self.set_fullscreen(self.window().fullscreen().is_none()); - } - - #[cfg(target_os = "macos")] - pub fn toggle_simple_fullscreen(&mut self) { - self.set_simple_fullscreen(!self.window().simple_fullscreen()); - } - - pub fn set_fullscreen(&mut self, fullscreen: bool) { - if fullscreen { - self.window().set_fullscreen(Some(Fullscreen::Borderless(None))); - } else { - self.window().set_fullscreen(None); - } - } - - #[cfg(target_os = "macos")] - pub fn set_simple_fullscreen(&mut self, simple_fullscreen: bool) { - self.window().set_simple_fullscreen(simple_fullscreen); - } - - #[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))] - pub fn wayland_display(&self) -> Option<*mut std::ffi::c_void> { - self.window().wayland_display() - } - - #[cfg(not(any(feature = "wayland", target_os = "macos", windows)))] - pub fn wayland_display(&self) -> Option<*mut std::ffi::c_void> { - None - } - - #[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))] - pub fn wayland_surface(&self) -> Option<&Attached> { - self.wayland_surface.as_ref() - } - - #[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))] - pub fn set_wayland_theme(&mut self, colors: &Colors) { - self.window().set_wayland_theme(AlacrittyWaylandTheme::new(colors)); - } - - /// Adjust the IME editor position according to the new location of the cursor. - pub fn update_ime_position(&mut self, point: Point, size: &SizeInfo) { - let nspot_x = f64::from(size.padding_x() + point.col.0 as f32 * size.cell_width()); - let nspot_y = f64::from(size.padding_y() + (point.line.0 + 1) as f32 * size.cell_height()); - - self.window().set_ime_position(PhysicalPosition::new(nspot_x, nspot_y)); - } - - pub fn swap_buffers(&self) { - self.windowed_context.swap_buffers().expect("swap buffers"); - } - - pub fn resize(&self, size: PhysicalSize) { - self.windowed_context.resize(size); - } - - /// Disable macOS window shadows. - /// - /// This prevents rendering artifacts from showing up when the window is transparent. - #[cfg(target_os = "macos")] - pub fn set_has_shadow(&self, has_shadows: bool) { - let raw_window = match self.window().raw_window_handle() { - RawWindowHandle::MacOS(handle) => handle.ns_window as id, - _ => return, - }; - - let value = if has_shadows { YES } else { NO }; - unsafe { - let _: () = msg_send![raw_window, setHasShadow: value]; - } - } - - fn window(&self) -> &GlutinWindow { - self.windowed_context.window() - } -} - -#[cfg(all(feature = "x11", not(any(target_os = "macos", windows))))] -fn x_embed_window(window: &GlutinWindow, parent_id: std::os::raw::c_ulong) { - let (xlib_display, xlib_window) = match (window.xlib_display(), window.xlib_window()) { - (Some(display), Some(window)) => (display, window), - _ => return, - }; - - let xlib = Xlib::open().expect("get xlib"); - - unsafe { - let atom = (xlib.XInternAtom)(xlib_display as *mut _, "_XEMBED".as_ptr() as *const _, 0); - (xlib.XChangeProperty)( - xlib_display as _, - xlib_window as _, - atom, - atom, - 32, - PropModeReplace, - [0, 1].as_ptr(), - 2, - ); - - // Register new error handler. - let old_handler = (xlib.XSetErrorHandler)(Some(xembed_error_handler)); - - // Check for the existence of the target before attempting reparenting. - (xlib.XReparentWindow)(xlib_display as _, xlib_window as _, parent_id, 0, 0); - - // Drain errors and restore original error handler. - (xlib.XSync)(xlib_display as _, 0); - (xlib.XSetErrorHandler)(old_handler); - } -} - -#[cfg(all(feature = "x11", not(any(target_os = "macos", windows))))] -unsafe extern "C" fn xembed_error_handler(_: *mut XDisplay, _: *mut XErrorEvent) -> i32 { - log::error!("Could not embed into specified window."); - std::process::exit(1); -} -- cgit