//! The display subsystem including window management, font rasterization, and //! GPU drawing. use std::cmp; use std::fmt::{self, Formatter}; use std::mem::{self, ManuallyDrop}; use std::num::NonZeroU32; use std::ops::{Deref, DerefMut}; use std::time::{Duration, Instant}; use glutin::context::{NotCurrentContext, PossiblyCurrentContext}; use glutin::prelude::*; use glutin::surface::{Surface, SwapInterval, WindowSurface}; use log::{debug, info}; use parking_lot::MutexGuard; use serde::{Deserialize, Serialize}; use winit::dpi::PhysicalSize; use winit::keyboard::ModifiersState; use winit::raw_window_handle::RawWindowHandle; use winit::window::CursorIcon; use crossfont::{Rasterize, Rasterizer, Size as FontSize}; use unicode_width::UnicodeWidthChar; use alacritty_terminal::event::{EventListener, OnResize, WindowSize}; use alacritty_terminal::grid::Dimensions as TermDimensions; use alacritty_terminal::index::{Column, Direction, Line, Point}; use alacritty_terminal::selection::Selection; use alacritty_terminal::term::cell::Flags; use alacritty_terminal::term::{ self, LineDamageBounds, Term, TermDamage, TermMode, MIN_COLUMNS, MIN_SCREEN_LINES, }; use alacritty_terminal::vte::ansi::{CursorShape, NamedColor}; use crate::config::font::Font; use crate::config::window::Dimensions; #[cfg(not(windows))] use crate::config::window::StartupMode; use crate::config::UiConfig; use crate::display::bell::VisualBell; use crate::display::color::{List, Rgb}; use crate::display::content::{RenderableContent, RenderableCursor}; use crate::display::cursor::IntoRects; use crate::display::damage::{damage_y_to_viewport_y, DamageTracker}; use crate::display::hint::{HintMatch, HintState}; use crate::display::meter::Meter; use crate::display::window::Window; use crate::event::{Event, EventType, Mouse, SearchState}; use crate::message_bar::{MessageBuffer, MessageType}; use crate::renderer::rects::{RenderLine, RenderLines, RenderRect}; use crate::renderer::{self, GlyphCache, Renderer}; use crate::scheduler::{Scheduler, TimerId, Topic}; use crate::string::{ShortenDirection, StrShortener}; pub mod color; pub mod content; pub mod cursor; pub mod hint; pub mod window; mod bell; mod damage; mod meter; /// Label for the forward terminal search bar. const FORWARD_SEARCH_LABEL: &str = "Search: "; /// Label for the backward terminal search bar. const BACKWARD_SEARCH_LABEL: &str = "Backward Search: "; /// The character used to shorten the visible text like uri preview or search regex. const SHORTENER: char = '…'; /// Color which is used to highlight damaged rects when debugging. const DAMAGE_RECT_COLOR: Rgb = Rgb::new(255, 0, 255); #[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 context operations. Context(glutin::error::Error), } 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::Context(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::Context(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::error::Error) -> Self { Error::Context(val) } } /// Terminal size info. #[derive(Serialize, Deserialize, Debug, Copy, Clone, PartialEq, Eq)] pub struct SizeInfo { /// Terminal window width. width: T, /// Terminal window height. height: T, /// Width of individual cell. cell_width: T, /// Height of individual cell. cell_height: T, /// Horizontal window padding. padding_x: T, /// Vertical window padding. padding_y: T, /// Number of lines in the viewport. screen_lines: usize, /// Number of columns in the viewport. columns: usize, } impl From> for SizeInfo { fn from(size_info: SizeInfo) -> Self { Self { width: size_info.width as u32, height: size_info.height as u32, cell_width: size_info.cell_width as u32, cell_height: size_info.cell_height as u32, padding_x: size_info.padding_x as u32, padding_y: size_info.padding_y as u32, screen_lines: size_info.screen_lines, columns: size_info.screen_lines, } } } impl From> for WindowSize { fn from(size_info: SizeInfo) -> Self { Self { num_cols: size_info.columns() as u16, num_lines: size_info.screen_lines() as u16, cell_width: size_info.cell_width() as u16, cell_height: size_info.cell_height() as u16, } } } impl SizeInfo { #[inline] pub fn width(&self) -> T { self.width } #[inline] pub fn height(&self) -> T { self.height } #[inline] pub fn cell_width(&self) -> T { self.cell_width } #[inline] pub fn cell_height(&self) -> T { self.cell_height } #[inline] pub fn padding_x(&self) -> T { self.padding_x } #[inline] pub fn padding_y(&self) -> T { self.padding_y } } impl SizeInfo { #[allow(clippy::too_many_arguments)] pub fn new( width: f32, height: f32, cell_width: f32, cell_height: f32, mut padding_x: f32, mut padding_y: f32, dynamic_padding: bool, ) -> SizeInfo { if dynamic_padding { padding_x = Self::dynamic_padding(padding_x.floor(), width, cell_width); padding_y = Self::dynamic_padding(padding_y.floor(), height, cell_height); } let lines = (height - 2. * padding_y) / cell_height; let screen_lines = cmp::max(lines as usize, MIN_SCREEN_LINES); let columns = (width - 2. * padding_x) / cell_width; let columns = cmp::max(columns as usize, MIN_COLUMNS); SizeInfo { width, height, cell_width, cell_height, padding_x: padding_x.floor(), padding_y: padding_y.floor(), screen_lines, columns, } } #[inline] pub fn reserve_lines(&mut self, count: usize) { self.screen_lines = cmp::max(self.screen_lines.saturating_sub(count), MIN_SCREEN_LINES); } /// Check if coordinates are inside the terminal grid. /// /// The padding, message bar or search are not counted as part of the grid. #[inline] pub fn contains_point(&self, x: usize, y: usize) -> bool { x <= (self.padding_x + self.columns as f32 * self.cell_width) as usize && x > self.padding_x as usize && y <= (self.padding_y + self.screen_lines as f32 * self.cell_height) as usize && y > self.padding_y as usize } /// Calculate padding to spread it evenly around the terminal content. #[inline] fn dynamic_padding(padding: f32, dimension: f32, cell_dimension: f32) -> f32 { padding + ((dimension - 2. * padding) % cell_dimension) / 2. } } impl TermDimensions for SizeInfo { #[inline] fn columns(&self) -> usize { self.columns } #[inline] fn screen_lines(&self) -> usize { self.screen_lines } #[inline] fn total_lines(&self) -> usize { self.screen_lines() } } #[derive(Default, Clone, Debug, PartialEq, Eq)] 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 window: Window, pub size_info: SizeInfo, /// Hint highlighted by the mouse. pub highlighted_hint: Option, /// Frames since hint highlight was created. highlighted_hint_age: usize, /// Hint highlighted by the vi mode cursor. pub vi_highlighted_hint: Option, /// Frames since hint highlight was created. vi_highlighted_hint_age: usize, pub raw_window_handle: RawWindowHandle, /// UI cursor visibility for blinking. pub cursor_hidden: bool, pub visual_bell: VisualBell, /// Mapped RGB values for each terminal color. pub colors: List, /// State of the keyboard hints. pub hint_state: HintState, /// Unprocessed display updates. pub pending_update: DisplayUpdate, /// The renderer update that takes place only once before the actual rendering. pub pending_renderer_update: Option, /// The ime on the given display. pub ime: Ime, /// The state of the timer for frame scheduling. pub frame_timer: FrameTimer, /// Damage tracker for the given display. pub damage_tracker: DamageTracker, /// Font size used by the window. pub font_size: FontSize, // Mouse point position when highlighting hints. hint_mouse_point: Option, renderer: ManuallyDrop, surface: ManuallyDrop>, context: ManuallyDrop>, glyph_cache: GlyphCache, meter: Meter, } impl Display { pub fn new( window: Window, gl_context: NotCurrentContext, config: &UiConfig, _tabbed: bool, ) -> Result { let raw_window_handle = window.raw_window_handle(); let scale_factor = window.scale_factor as f32; let rasterizer = Rasterizer::new()?; let font_size = config.font.size().scale(scale_factor); debug!("Loading \"{}\" font", &config.font.normal().family); let font = config.font.clone().with_size(font_size); let mut glyph_cache = GlyphCache::new(rasterizer, &font)?; let metrics = glyph_cache.font_metrics(); let (cell_width, cell_height) = compute_cell_size(config, &metrics); // Resize the window to account for the user configured size. if let Some(dimensions) = config.window.dimensions() { let size = window_size(config, dimensions, cell_width, cell_height, scale_factor); window.request_inner_size(size); } // Create the GL surface to draw into. let surface = renderer::platform::create_gl_surface( &gl_context, window.inner_size(), window.raw_window_handle(), )?; // Make the context current. let context = gl_context.make_current(&surface)?; // Create renderer. let mut renderer = Renderer::new(&context, config.debug.renderer)?; // Load font common glyphs to accelerate rendering. debug!("Filling glyph cache with common glyphs"); renderer.with_loader(|mut api| { glyph_cache.reset_glyph_cache(&mut api); }); let padding = config.window.padding(window.scale_factor as f32); 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.window.dynamic_padding && config.window.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.clear(background_color, config.window_opacity()); // Disable shadows for transparent windows on macOS. #[cfg(target_os = "macos")] window.set_has_shadow(config.window_opacity() >= 1.0); let is_wayland = matches!(raw_window_handle, RawWindowHandle::Wayland(_)); // 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. if !is_wayland { surface.swap_buffers(&context).expect("failed to swap buffers."); renderer.finish(); } // Set resize increments for the newly created window. if config.window.resize_increments { window.set_resize_increments(PhysicalSize::new(cell_width, cell_height)); } window.set_visible(true); #[allow(clippy::single_match)] #[cfg(not(windows))] if !_tabbed { match config.window.startup_mode { #[cfg(target_os = "macos")] StartupMode::SimpleFullscreen => window.set_simple_fullscreen(true), StartupMode::Maximized if !is_wayland => window.set_maximized(true), _ => (), } } let hint_state = HintState::new(config.hints.alphabet()); let mut damage_tracker = DamageTracker::new(size_info.screen_lines(), size_info.columns()); damage_tracker.debug = config.debug.highlight_damage; // Disable vsync. if let Err(err) = surface.set_swap_interval(&context, SwapInterval::DontWait) { info!("Failed to disable vsync: {}", err); } Ok(Self { context: ManuallyDrop::new(Replaceable::new(context)), visual_bell: VisualBell::from(&config.bell), renderer: ManuallyDrop::new(renderer), surface: ManuallyDrop::new(surface), colors: List::from(&config.colors), frame_timer: FrameTimer::new(), raw_window_handle, damage_tracker, glyph_cache, hint_state, size_info, font_size, window, pending_renderer_update: Default::default(), vi_highlighted_hint_age: Default::default(), highlighted_hint_age: Default::default(), vi_highlighted_hint: Default::default(), highlighted_hint: Default::default(), hint_mouse_point: Default::default(), pending_update: Default::default(), cursor_hidden: Default::default(), meter: Default::default(), ime: Default::default(), }) } #[inline] pub fn gl_context(&self) -> &PossiblyCurrentContext { self.context.get() } pub fn make_not_current(&mut self) { if self.context.get().is_current() { self.context.replace_with(|context| { context .make_not_current() .expect("failed to disable context") .treat_as_possibly_current() }); } } pub fn make_current(&self) { if !self.context.get().is_current() { self.context.make_current(&self.surface).expect("failed to make context current") } } fn swap_buffers(&self) { #[allow(clippy::single_match)] let res = match (self.surface.deref(), &self.context.get()) { #[cfg(not(any(target_os = "macos", windows)))] (Surface::Egl(surface), PossiblyCurrentContext::Egl(context)) if matches!(self.raw_window_handle, RawWindowHandle::Wayland(_)) && !self.damage_tracker.debug => { let damage = self.damage_tracker.shape_frame_damage(self.size_info.into()); surface.swap_buffers_with_damage(context, &damage) }, (surface, context) => surface.swap_buffers(context), }; if let Err(err) = res { debug!("error calling swap_buffers: {}", err); } } /// Update font size and cell dimensions. /// /// This will return a tuple of the cell width and height. fn update_font_size( glyph_cache: &mut GlyphCache, config: &UiConfig, font: &Font, ) -> (f32, f32) { let _ = glyph_cache.update_font_size(font); // Compute new cell sizes. compute_cell_size(config, &glyph_cache.font_metrics()) } /// Reset glyph cache. fn reset_glyph_cache(&mut self) { let cache = &mut self.glyph_cache; self.renderer.with_loader(|mut api| { cache.reset_glyph_cache(&mut api); }); } // XXX: this function must not call to any `OpenGL` related tasks. Renderer updates are // performed in [`Self::process_renderer_update`] right before drawing. // /// Process update events. pub fn handle_update( &mut self, terminal: &mut Term, pty_resize_handle: &mut dyn OnResize, message_buffer: &MessageBuffer, search_state: &mut SearchState, config: &UiConfig, ) where T: EventListener, { let pending_update = mem::take(&mut self.pending_update); let (mut cell_width, mut cell_height) = (self.size_info.cell_width(), self.size_info.cell_height()); if pending_update.font().is_some() || pending_update.cursor_dirty() { let renderer_update = self.pending_renderer_update.get_or_insert(Default::default()); renderer_update.clear_font_cache = true } // Update font size and cell dimensions. if let Some(font) = pending_update.font() { let cell_dimensions = Self::update_font_size(&mut self.glyph_cache, config, font); cell_width = cell_dimensions.0; cell_height = cell_dimensions.1; info!("Cell size: {} x {}", cell_width, cell_height); // Mark entire terminal as damaged since glyph size could change without cell size // changes. self.damage_tracker.frame().mark_fully_damaged(); } let (mut width, mut height) = (self.size_info.width(), self.size_info.height()); if let Some(dimensions) = pending_update.dimensions() { width = dimensions.width as f32; height = dimensions.height as f32; } let padding = config.window.padding(self.window.scale_factor as f32); let mut new_size = SizeInfo::new( width, height, cell_width, cell_height, padding.0, padding.1, config.window.dynamic_padding, ); // Update number of column/lines in the viewport. let search_active = search_state.history_index.is_some(); let message_bar_lines = message_buffer.message().map_or(0, |m| m.text(&new_size).len()); let search_lines = usize::from(search_active); new_size.reserve_lines(message_bar_lines + search_lines); // Update resize increments. if config.window.resize_increments { self.window.set_resize_increments(PhysicalSize::new(cell_width, cell_height)); } // Resize when terminal when its dimensions have changed. if self.size_info.screen_lines() != new_size.screen_lines || self.size_info.columns() != new_size.columns() { // Resize PTY. pty_resize_handle.on_resize(new_size.into()); // Resize terminal. terminal.resize(new_size); // Resize damage tracking. self.damage_tracker.resize(new_size.screen_lines(), new_size.columns()); } // Check if dimensions have changed. if new_size != self.size_info { // Queue renderer update. let renderer_update = self.pending_renderer_update.get_or_insert(Default::default()); renderer_update.resize = true; // Clear focused search match. search_state.clear_focused_match(); } self.size_info = new_size; } // NOTE: Renderer updates are split off, since platforms like Wayland require resize and other // OpenGL operations to be performed right before rendering. Otherwise they could lock the // back buffer and render with the previous state. This also solves flickering during resizes. // /// Update the state of the renderer. pub fn process_renderer_update(&mut self) { let renderer_update = match self.pending_renderer_update.take() { Some(renderer_update) => renderer_update, _ => return, }; // Resize renderer. if renderer_update.resize { let width = NonZeroU32::new(self.size_info.width() as u32).unwrap(); let height = NonZeroU32::new(self.size_info.height() as u32).unwrap(); self.surface.resize(&self.context, width, height); } // Ensure we're modifying the correct OpenGL context. self.make_current(); if renderer_update.clear_font_cache { self.reset_glyph_cache(); } 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, mut terminal: MutexGuard<'_, Term>, scheduler: &mut Scheduler, message_buffer: &MessageBuffer, config: &UiConfig, search_state: &mut SearchState, ) { // Collect renderable content before the terminal is dropped. let mut content = RenderableContent::new(config, self, &terminal, search_state); let mut grid_cells = Vec::new(); for cell in &mut content { grid_cells.push(cell); } let selection_range = content.selection_range(); let foreground_color = content.color(NamedColor::Foreground as usize); 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 vi_mode = terminal.mode().contains(TermMode::VI); let vi_cursor_point = if vi_mode { Some(terminal.vi_mode_cursor.point) } else { None }; // Add damage from the terminal. match terminal.damage() { TermDamage::Full => self.damage_tracker.frame().mark_fully_damaged(), TermDamage::Partial(damaged_lines) => { for damage in damaged_lines { self.damage_tracker.frame().damage_line(damage); } }, } terminal.reset_damage(); // Drop terminal as early as possible to free lock. drop(terminal); // Invalidate highlighted hints if grid has changed. self.validate_hint_highlights(display_offset); // Add damage from alacritty's UI elements overlapping terminal. let requires_full_damage = self.visual_bell.intensity() != 0. || self.hint_state.active() || search_state.regex().is_some(); if requires_full_damage { self.damage_tracker.frame().mark_fully_damaged(); self.damage_tracker.next_frame().mark_fully_damaged(); } let vi_cursor_viewport_point = vi_cursor_point.and_then(|cursor| term::point_to_viewport(display_offset, cursor)); self.damage_tracker.damage_vi_cursor(vi_cursor_viewport_point); self.damage_tracker.damage_selection(selection_range, display_offset); // Make sure this window's OpenGL context is active. self.make_current(); self.renderer.clear(background_color, config.window_opacity()); let mut lines = RenderLines::new(); // Optimize loop hint comparator. let has_highlighted_hint = self.highlighted_hint.is_some() || self.vi_highlighted_hint.is_some(); // Draw grid. { let _sampler = self.meter.sampler(); // Ensure macOS hasn't reset our viewport. #[cfg(target_os = "macos")] self.renderer.set_viewport(&size_info); let glyph_cache = &mut self.glyph_cache; let highlighted_hint = &self.highlighted_hint; let vi_highlighted_hint = &self.vi_highlighted_hint; let damage_tracker = &mut self.damage_tracker; let cells = grid_cells.into_iter().map(|mut cell| { // Underline hints hovered by mouse or vi mode cursor. if has_highlighted_hint { let point = term::viewport_to_point(display_offset, cell.point); let hyperlink = cell.extra.as_ref().and_then(|extra| extra.hyperlink.as_ref()); let should_highlight = |hint: &Option| { hint.as_ref().map_or(false, |hint| hint.should_highlight(point, hyperlink)) }; if should_highlight(highlighted_hint) || should_highlight(vi_highlighted_hint) { damage_tracker.frame().damage_point(cell.point); cell.flags.insert(Flags::UNDERLINE); } } // Update underline/strikeout. lines.update(&cell); cell }); self.renderer.draw_cells(&size_info, glyph_cache, cells); } let mut rects = lines.rects(&metrics, &size_info); if let Some(vi_cursor_point) = vi_cursor_point { // Indicate vi mode by showing the cursor's position in the top right corner. let line = (-vi_cursor_point.line.0 + size_info.bottommost_line().0) as usize; let obstructed_column = Some(vi_cursor_point) .filter(|point| point.line == -(display_offset as i32)) .map(|point| point.column); self.draw_line_indicator(config, total_lines, obstructed_column, line); } else if search_state.regex().is_some() { // Show current display offset in vi-less search to indicate match position. self.draw_line_indicator(config, total_lines, None, display_offset); }; // Draw cursor. rects.extend(cursor.rects(&size_info, config.cursor.thickness())); // 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.bell.color, visual_bell_intensity as f32, ); rects.push(visual_bell_rect); } // Handle IME positioning and search bar rendering. 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(regex, search_label, size_info.columns()); // Render the search bar. self.draw_search(config, &search_text); // Draw search bar cursor. let line = size_info.screen_lines(); let column = Column(search_text.chars().count() - 1); // Add cursor to search bar if IME is not active. if self.ime.preedit().is_none() { let fg = config.colors.footer_bar_foreground(); let shape = CursorShape::Underline; let cursor_width = NonZeroU32::new(1).unwrap(); let cursor = RenderableCursor::new(Point::new(line, column), shape, fg, cursor_width); rects.extend(cursor.rects(&size_info, config.cursor.thickness())); } Some(Point::new(line, column)) }, None => { let num_lines = self.size_info.screen_lines(); match vi_cursor_viewport_point { None => term::point_to_viewport(display_offset, cursor_point) .filter(|point| point.line < num_lines), point => point, } }, }; // Handle IME. if self.ime.is_enabled() { if let Some(point) = ime_position { let (fg, bg) = if search_state.regex().is_some() { (config.colors.footer_bar_foreground(), config.colors.footer_bar_background()) } else { (foreground_color, background_color) }; self.draw_ime_preview(point, fg, bg, &mut rects, config); } } if let Some(message) = message_buffer.message() { let search_offset = usize::from(search_state.regex().is_some()); 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 as f32, size_info.padding_y()); let bg = match message.ty() { MessageType::Error => config.colors.normal.red, MessageType::Warning => config.colors.normal.yellow, }; let x = 0; let width = size_info.width() as i32; let height = (size_info.height() - y) as i32; let message_bar_rect = RenderRect::new(x as f32, y, width as f32, height as f32, bg, 1.); // Push message_bar in the end, so it'll be above all other content. rects.push(message_bar_rect); // Always damage message bar, since it could have messages of the same size in it. self.damage_tracker.frame().add_viewport_rect(&size_info, x, y as i32, width, height); // Draw rectangles. self.renderer.draw_rects(&size_info, &metrics, 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.draw_string( point, fg, bg, message_text.chars(), &size_info, glyph_cache, ); } } else { // Draw rectangles. self.renderer.draw_rects(&size_info, &metrics, rects); } self.draw_render_timer(config); // Draw hyperlink uri preview. if has_highlighted_hint { let cursor_point = vi_cursor_point.or(Some(cursor_point)); self.draw_hyperlink_preview(config, cursor_point, display_offset); } // Notify winit that we're about to present. self.window.pre_present_notify(); // Highlight damage for debugging. if self.damage_tracker.debug { let damage = self.damage_tracker.shape_frame_damage(self.size_info.into()); let mut rects = Vec::with_capacity(damage.len()); self.highlight_damage(&mut rects); self.renderer.draw_rects(&self.size_info, &metrics, rects); } // Clearing debug highlights from the previous frame requires full redraw. self.swap_buffers(); if matches!(self.raw_window_handle, RawWindowHandle::Xcb(_) | RawWindowHandle::Xlib(_)) { // 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.finish(); } // XXX: Request the new frame after swapping buffers, so the // time to finish OpenGL operations is accounted for in the timeout. if !matches!(self.raw_window_handle, RawWindowHandle::Wayland(_)) { self.request_frame(scheduler); } self.damage_tracker.swap_damage(); } /// Update to a new configuration. pub fn update_config(&mut self, config: &UiConfig) { self.damage_tracker.debug = config.debug.highlight_damage; self.visual_bell.update_config(&config.bell); self.colors = List::from(&config.colors); } /// Update the mouse/vi mode cursor hint highlighting. /// /// This will return whether the highlighted hints changed. pub fn update_highlighted_hints( &mut self, term: &Term, config: &UiConfig, mouse: &Mouse, modifiers: ModifiersState, ) -> bool { // Update vi mode cursor hint. let vi_highlighted_hint = if term.mode().contains(TermMode::VI) { let mods = ModifiersState::all(); let point = term.vi_mode_cursor.point; hint::highlighted_at(term, config, point, mods) } else { None }; let mut dirty = vi_highlighted_hint != self.vi_highlighted_hint; self.vi_highlighted_hint = vi_highlighted_hint; self.vi_highlighted_hint_age = 0; // Force full redraw if the vi mode highlight was cleared. if dirty { self.damage_tracker.frame().mark_fully_damaged(); } // Abort if mouse highlighting conditions are not met. if !mouse.inside_text_area || !term.selection.as_ref().map_or(true, Selection::is_empty) { if self.highlighted_hint.take().is_some() { self.damage_tracker.frame().mark_fully_damaged(); dirty = true; } return dirty; } // Find highlighted hint at mouse position. let point = mouse.point(&self.size_info, term.grid().display_offset()); let highlighted_hint = hint::highlighted_at(term, config, point, modifiers); // Update cursor shape. if highlighted_hint.is_some() { // If mouse changed the line, we should update the hyperlink preview, since the // highlighted hint could be disrupted by the old preview. dirty = self.hint_mouse_point.map_or(false, |p| p.line != point.line); self.hint_mouse_point = Some(point); self.window.set_mouse_cursor(CursorIcon::Pointer); } else if self.highlighted_hint.is_some() { self.hint_mouse_point = None; if term.mode().intersects(TermMode::MOUSE_MODE) && !term.mode().contains(TermMode::VI) { self.window.set_mouse_cursor(CursorIcon::Default); } else { self.window.set_mouse_cursor(CursorIcon::Text); } } let mouse_highlight_dirty = self.highlighted_hint != highlighted_hint; dirty |= mouse_highlight_dirty; self.highlighted_hint = highlighted_hint; self.highlighted_hint_age = 0; // Force full redraw if the mouse cursor highlight was changed. if mouse_highlight_dirty { self.damage_tracker.frame().mark_fully_damaged(); } dirty } #[inline(never)] fn draw_ime_preview( &mut self, point: Point, fg: Rgb, bg: Rgb, rects: &mut Vec, config: &UiConfig, ) { let preedit = match self.ime.preedit() { Some(preedit) => preedit, None => { // In case we don't have preedit, just set the popup point. self.window.update_ime_position(point, &self.size_info); return; }, }; let num_cols = self.size_info.columns(); // Get the visible preedit. let visible_text: String = match (preedit.cursor_byte_offset, preedit.cursor_end_offset) { (Some(byte_offset), Some(end_offset)) if end_offset.0 > num_cols => StrShortener::new( &preedit.text[byte_offset.0..], num_cols, ShortenDirection::Right, Some(SHORTENER), ), _ => { StrShortener::new(&preedit.text, num_cols, ShortenDirection::Left, Some(SHORTENER)) }, } .collect(); let visible_len = visible_text.chars().count(); let end = cmp::min(point.column.0 + visible_len, num_cols); let start = end.saturating_sub(visible_len); let start = Point::new(point.line, Column(start)); let end = Point::new(point.line, Column(end - 1)); let glyph_cache = &mut self.glyph_cache; let metrics = glyph_cache.font_metrics(); self.renderer.draw_string( start, fg, bg, visible_text.chars(), &self.size_info, glyph_cache, ); // Damage preedit inside the terminal viewport. if point.line < self.size_info.screen_lines() { let damage = LineDamageBounds::new(start.line, 0, num_cols); self.damage_tracker.frame().damage_line(damage); self.damage_tracker.next_frame().damage_line(damage); } // Add underline for preedit text. let underline = RenderLine { start, end, color: fg }; rects.extend(underline.rects(Flags::UNDERLINE, &metrics, &self.size_info)); let ime_popup_point = match preedit.cursor_end_offset { Some(cursor_end_offset) => { // Use hollow block when multiple characters are changed at once. let (shape, width) = if let Some(width) = NonZeroU32::new((cursor_end_offset.0 - cursor_end_offset.1) as u32) { (CursorShape::HollowBlock, width) } else { (CursorShape::Beam, NonZeroU32::new(1).unwrap()) }; let cursor_column = Column( (end.column.0 as isize - cursor_end_offset.0 as isize + 1).max(0) as usize, ); let cursor_point = Point::new(point.line, cursor_column); let cursor = RenderableCursor::new(cursor_point, shape, fg, width); rects.extend(cursor.rects(&self.size_info, config.cursor.thickness())); cursor_point }, _ => end, }; self.window.update_ime_position(ime_popup_point, &self.size_info); } /// Format search regex to account for the cursor and fullwidth characters. fn format_search(search_regex: &str, search_label: &str, max_width: usize) -> String { let label_len = search_label.len(); // Skip `search_regex` formatting if only label is visible. if label_len > max_width { return search_label[..max_width].to_owned(); } // The search string consists of `search_label` + `search_regex` + `cursor`. let mut bar_text = String::from(search_label); bar_text.extend(StrShortener::new( search_regex, max_width.wrapping_sub(label_len + 1), ShortenDirection::Left, Some(SHORTENER), )); // Add place for cursor. bar_text.push(' '); bar_text } /// Draw preview for the currently highlighted `Hyperlink`. #[inline(never)] fn draw_hyperlink_preview( &mut self, config: &UiConfig, cursor_point: Option, display_offset: usize, ) { let num_cols = self.size_info.columns(); let uris: Vec<_> = self .highlighted_hint .iter() .chain(&self.vi_highlighted_hint) .filter_map(|hint| hint.hyperlink().map(|hyperlink| hyperlink.uri())) .map(|uri| StrShortener::new(uri, num_cols, ShortenDirection::Right, Some(SHORTENER))) .collect(); if uris.is_empty() { return; } // The maximum amount of protected lines including the ones we'll show preview on. let max_protected_lines = uris.len() * 2; // Lines we shouldn't show preview on, because it'll obscure the highlighted hint. let mut protected_lines = Vec::with_capacity(max_protected_lines); if self.size_info.screen_lines() > max_protected_lines { // Prefer to show preview even when it'll likely obscure the highlighted hint, when // there's no place left for it. protected_lines.push(self.hint_mouse_point.map(|point| point.line)); protected_lines.push(cursor_point.map(|point| point.line)); } // Find the line in viewport we can draw preview on without obscuring protected lines. let viewport_bottom = self.size_info.bottommost_line() - Line(display_offset as i32); let viewport_top = viewport_bottom - (self.size_info.screen_lines() - 1); let uri_lines = (viewport_top.0..=viewport_bottom.0) .rev() .map(|line| Some(Line(line))) .filter_map(|line| { if protected_lines.contains(&line) { None } else { protected_lines.push(line); line } }) .take(uris.len()) .flat_map(|line| term::point_to_viewport(display_offset, Point::new(line, Column(0)))); let fg = config.colors.footer_bar_foreground(); let bg = config.colors.footer_bar_background(); for (uri, point) in uris.into_iter().zip(uri_lines) { // Damage the uri preview. let damage = LineDamageBounds::new(point.line, point.column.0, num_cols); self.damage_tracker.frame().damage_line(damage); // Damage the uri preview for the next frame as well. self.damage_tracker.next_frame().damage_line(damage); self.renderer.draw_string(point, fg, bg, uri, &self.size_info, &mut self.glyph_cache); } } /// Draw current search regex. #[inline(never)] fn draw_search(&mut self, config: &UiConfig, text: &str) { // Assure text length is at least num_cols. let num_cols = self.size_info.columns(); let text = format!("{text:, line: usize, ) { let columns = self.size_info.columns(); let text = format!("[{}/{}]", line, total_lines - 1); let column = Column(self.size_info.columns().saturating_sub(text.len())); let point = Point::new(0, column); // Damage the line indicator for current and next frame. let damage = LineDamageBounds::new(point.line, point.column.0, columns - 1); self.damage_tracker.frame().damage_line(damage); self.damage_tracker.next_frame().damage_line(damage); 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 obstructed_column.map_or(true, |obstructed_column| obstructed_column < column) { let glyph_cache = &mut self.glyph_cache; self.renderer.draw_string(point, fg, bg, text.chars(), &self.size_info, glyph_cache); } } /// Highlight damaged rects. /// /// This function is for debug purposes only. fn highlight_damage(&self, render_rects: &mut Vec) { for damage_rect in &self.damage_tracker.shape_frame_damage(self.size_info.into()) { let x = damage_rect.x as f32; let height = damage_rect.height as f32; let width = damage_rect.width as f32; let y = damage_y_to_viewport_y(&self.size_info, damage_rect) as f32; let render_rect = RenderRect::new(x, y, width, height, DAMAGE_RECT_COLOR, 0.5); render_rects.push(render_rect); } } /// Check whether a hint highlight needs to be cleared. fn validate_hint_highlights(&mut self, display_offset: usize) { let frame = self.damage_tracker.frame(); let hints = [ (&mut self.highlighted_hint, &mut self.highlighted_hint_age, true), (&mut self.vi_highlighted_hint, &mut self.vi_highlighted_hint_age, false), ]; for (hint, hint_age, reset_mouse) in hints { let (start, end) = match hint { Some(hint) => (*hint.bounds().start(), *hint.bounds().end()), None => continue, }; // Ignore hints that were created this frame. *hint_age += 1; if *hint_age == 1 { continue; } // Convert hint bounds to viewport coordinates. let start = term::point_to_viewport(display_offset, start).unwrap_or_default(); let end = term::point_to_viewport(display_offset, end).unwrap_or_else(|| { Point::new(self.size_info.screen_lines() - 1, self.size_info.last_column()) }); // Clear invalidated hints. if frame.intersects(start, end) { if reset_mouse { self.window.set_mouse_cursor(CursorIcon::Default); } frame.mark_fully_damaged(); *hint = None; } } } /// Request a new frame for a window on Wayland. fn request_frame(&mut self, scheduler: &mut Scheduler) { // Mark that we've used a frame. self.window.has_frame = false; // Get the display vblank interval. let monitor_vblank_interval = 1_000_000. / self .window .current_monitor() .and_then(|monitor| monitor.refresh_rate_millihertz()) .unwrap_or(60_000) as f64; // Now convert it to micro seconds. let monitor_vblank_interval = Duration::from_micros((1000. * monitor_vblank_interval) as u64); let swap_timeout = self.frame_timer.compute_timeout(monitor_vblank_interval); let window_id = self.window.id(); let timer_id = TimerId::new(Topic::Frame, window_id); let event = Event::new(EventType::Frame, window_id); scheduler.schedule(event, swap_timeout, false, timer_id); } } impl Drop for Display { fn drop(&mut self) { // Switch OpenGL context before dropping, otherwise objects (like programs) from other // contexts might be deleted when dropping renderer. self.make_current(); unsafe { ManuallyDrop::drop(&mut self.renderer); ManuallyDrop::drop(&mut self.context); ManuallyDrop::drop(&mut self.surface); } } } /// Input method state. #[derive(Debug, Default)] pub struct Ime { /// Whether the IME is enabled. enabled: bool, /// Current IME preedit. preedit: Option, } impl Ime { #[inline] pub fn set_enabled(&mut self, is_enabled: bool) { if is_enabled { self.enabled = is_enabled } else { // Clear state when disabling IME. *self = Default::default(); } } #[inline] pub fn is_enabled(&self) -> bool { self.enabled } #[inline] pub fn set_preedit(&mut self, preedit: Option) { self.preedit = preedit; } #[inline] pub fn preedit(&self) -> Option<&Preedit> { self.preedit.as_ref() } } #[derive(Debug, Default, PartialEq, Eq)] pub struct Preedit { /// The preedit text. text: String, /// Byte offset for cursor start into the preedit text. /// /// `None` means that the cursor is invisible. cursor_byte_offset: Option<(usize, usize)>, /// The cursor offset from the end of the start of the preedit in char width. cursor_end_offset: Option<(usize, usize)>, } impl Preedit { pub fn new(text: String, cursor_byte_offset: Option<(usize, usize)>) -> Self { let cursor_end_offset = if let Some(byte_offset) = cursor_byte_offset { // Convert byte offset into char offset. let start_to_end_offset = text[byte_offset.0..].chars().fold(0, |acc, ch| acc + ch.width().unwrap_or(1)); let end_to_end_offset = text[byte_offset.1..].chars().fold(0, |acc, ch| acc + ch.width().unwrap_or(1)); Some((start_to_end_offset, end_to_end_offset)) } else { None }; Self { text, cursor_byte_offset, cursor_end_offset } } } /// Pending renderer updates. /// /// All renderer updates are cached to be applied just before rendering, to avoid platform-specific /// rendering issues. #[derive(Debug, Default, Copy, Clone)] pub struct RendererUpdate { /// Should resize the window. resize: bool, /// Clear font caches. clear_font_cache: bool, } /// Struct for safe in-place replacement. /// /// This struct allows easily replacing struct fields that provide `self -> Self` methods in-place, /// without having to deal with constantly unwrapping the underlying [`Option`]. struct Replaceable(Option); impl Replaceable { pub fn new(inner: T) -> Self { Self(Some(inner)) } /// Replace the contents of the container. pub fn replace_with T>(&mut self, f: F) { self.0 = self.0.take().map(f); } /// Get immutable access to the wrapped value. pub fn get(&self) -> &T { self.0.as_ref().unwrap() } /// Get mutable access to the wrapped value. pub fn get_mut(&mut self) -> &mut T { self.0.as_mut().unwrap() } } impl Deref for Replaceable { type Target = T; fn deref(&self) -> &Self::Target { self.get() } } impl DerefMut for Replaceable { fn deref_mut(&mut self) -> &mut Self::Target { self.get_mut() } } /// The frame timer state. pub struct FrameTimer { /// Base timestamp used to compute sync points. base: Instant, /// The last timestamp we synced to. last_synced_timestamp: Instant, /// The refresh rate we've used to compute sync timestamps. refresh_interval: Duration, } impl FrameTimer { pub fn new() -> Self { let now = Instant::now(); Self { base: now, last_synced_timestamp: now, refresh_interval: Duration::ZERO } } /// Compute the delay that we should use to achieve the target frame /// rate. pub fn compute_timeout(&mut self, refresh_interval: Duration) -> Duration { let now = Instant::now(); // Handle refresh rate change. if self.refresh_interval != refresh_interval { self.base = now; self.last_synced_timestamp = now; self.refresh_interval = refresh_interval; return refresh_interval; } let next_frame = self.last_synced_timestamp + self.refresh_interval; if next_frame < now { // Redraw immediately if we haven't drawn in over `refresh_interval` microseconds. let elapsed_micros = (now - self.base).as_micros() as u64; let refresh_micros = self.refresh_interval.as_micros() as u64; self.last_synced_timestamp = now - Duration::from_micros(elapsed_micros % refresh_micros); Duration::ZERO } else { // Redraw on the next `refresh_interval` clock tick. self.last_synced_timestamp = next_frame; next_frame - now } } } /// 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: &UiConfig, metrics: &crossfont::Metrics) -> (f32, f32) { let offset_x = f64::from(config.font.offset.x); let offset_y = f64::from(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: &UiConfig, dimensions: Dimensions, cell_width: f32, cell_height: f32, scale_factor: f32, ) -> PhysicalSize { let padding = config.window.padding(scale_factor); let grid_width = cell_width * dimensions.columns.max(MIN_COLUMNS) as f32; let grid_height = cell_height * dimensions.lines.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) }