use std::cmp;
use std::fmt::{self, Formatter};
use std::mem::{self, ManuallyDrop};
use std::num::NonZeroU32;
use std::ops::Deref;
use std::time::{Duration, Instant};
use glutin::config::GetGlConfig;
use glutin::context::{NotCurrentContext, PossiblyCurrentContext};
use glutin::display::GetGlDisplay;
use glutin::error::ErrorKind;
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, MIN_COLUMNS, MIN_SCREEN_LINES, Term, TermDamage, TermMode,
};
use alacritty_terminal::vte::ansi::{CursorShape, NamedColor};
use crate::config::UiConfig;
use crate::config::debug::RendererPreference;
use crate::config::font::Font;
use crate::config::window::Dimensions;
#[cfg(not(windows))]
use crate::config::window::StartupMode;
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::{DamageTracker, damage_y_to_viewport_y};
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, platform};
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;
const FORWARD_SEARCH_LABEL: &str = "Search: ";
const BACKWARD_SEARCH_LABEL: &str = "Backward Search: ";
const SHORTENER: char = '…';
const DAMAGE_RECT_COLOR: Rgb = Rgb::new(255, 0, 255);
#[derive(Debug)]
pub enum Error {
Window(window::Error),
Font(crossfont::Error),
Render(renderer::Error),
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<window::Error> for Error {
fn from(val: window::Error) -> Self {
Error::Window(val)
}
}
impl From<crossfont::Error> for Error {
fn from(val: crossfont::Error) -> Self {
Error::Font(val)
}
}
impl From<renderer::Error> for Error {
fn from(val: renderer::Error) -> Self {
Error::Render(val)
}
}
impl From<glutin::error::Error> for Error {
fn from(val: glutin::error::Error) -> Self {
Error::Context(val)
}
}
#[derive(Serialize, Deserialize, Debug, Copy, Clone, PartialEq, Eq)]
pub struct SizeInfo<T = f32> {
width: T,
height: T,
cell_width: T,
cell_height: T,
padding_x: T,
padding_y: T,
screen_lines: usize,
columns: usize,
}
impl From<SizeInfo<f32>> for SizeInfo<u32> {
fn from(size_info: SizeInfo<f32>) -> 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<SizeInfo<f32>> for WindowSize {
fn from(size_info: SizeInfo<f32>) -> 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<T: Clone + Copy> SizeInfo<T> {
#[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<f32> {
#[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);
}
#[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
}
#[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<PhysicalSize<u32>>,
cursor_dirty: bool,
font: Option<Font>,
}
impl DisplayUpdate {
pub fn dimensions(&self) -> Option<PhysicalSize<u32>> {
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<u32>) {
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;
}
}
pub struct Display {
pub window: Window,
pub size_info: SizeInfo,
pub highlighted_hint: Option<HintMatch>,
highlighted_hint_age: usize,
pub vi_highlighted_hint: Option<HintMatch>,
vi_highlighted_hint_age: usize,
pub raw_window_handle: RawWindowHandle,
pub cursor_hidden: bool,
pub visual_bell: VisualBell,
pub colors: List,
pub hint_state: HintState,
pub pending_update: DisplayUpdate,
pub pending_renderer_update: Option<RendererUpdate>,
pub ime: Ime,
pub frame_timer: FrameTimer,
pub damage_tracker: DamageTracker,
pub font_size: FontSize,
hint_mouse_point: Option<Point>,
renderer: ManuallyDrop<Renderer>,
renderer_preference: Option<RendererPreference>,
surface: ManuallyDrop<Surface<WindowSurface>>,
context: ManuallyDrop<PossiblyCurrentContext>,
glyph_cache: GlyphCache,
meter: Meter,
}
impl Display {
pub fn new(
window: Window,
gl_context: NotCurrentContext,
config: &UiConfig,
_tabbed: bool,
) -> Result<Display, Error> {
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);
if let Some(dimensions) = config.window.dimensions() {
let size = window_size(config, dimensions, cell_width, cell_height, scale_factor);
window.request_inner_size(size);
}
let surface = platform::create_gl_surface(
&gl_context,
window.inner_size(),
window.raw_window_handle(),
)?;
let context = gl_context.make_current(&surface)?;
let mut renderer = Renderer::new(&context, config.debug.renderer)?;
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();
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: {cell_width} x {cell_height}");
info!("Padding: {} x {}", size_info.padding_x(), size_info.padding_y());
info!("Width: {}, Height: {}", size_info.width(), size_info.height());
renderer.resize(&size_info);
let background_color = config.colors.primary.background;
renderer.clear(background_color, config.window_opacity());
#[cfg(target_os = "macos")]
window.set_has_shadow(config.window_opacity() >= 1.0);
let is_wayland = matches!(raw_window_handle, RawWindowHandle::Wayland(_));
if !is_wayland {
surface.swap_buffers(&context).expect("failed to swap buffers.");
renderer.finish();
}
if config.window.resize_increments {
window.set_resize_increments(PhysicalSize::new(cell_width, cell_height));
}
window.set_visible(true);
#[cfg(target_os = "macos")]
window.focus_window();
#[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;
if let Err(err) = surface.set_swap_interval(&context, SwapInterval::DontWait) {
info!("Failed to disable vsync: {err}");
}
Ok(Self {
context: ManuallyDrop::new(context),
visual_bell: VisualBell::from(&config.bell),
renderer: ManuallyDrop::new(renderer),
renderer_preference: config.debug.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
}
pub fn make_not_current(&mut self) {
if self.context.is_current() {
self.context.make_not_current_in_place().expect("failed to disable context");
}
}
pub fn make_current(&mut self) {
let is_current = self.context.is_current();
let context_loss = if is_current {
self.renderer.was_context_reset()
} else {
match self.context.make_current(&self.surface) {
Err(err) if err.error_kind() == ErrorKind::ContextLost => {
info!("Context lost for window {:?}", self.window.id());
true
},
_ => false,
}
};
if !context_loss {
return;
}
let gl_display = self.context.display();
let gl_config = self.context.config();
let raw_window_handle = Some(self.window.raw_window_handle());
let context = platform::create_gl_context(&gl_display, &gl_config, raw_window_handle)
.expect("failed to recreate context.");
unsafe {
ManuallyDrop::drop(&mut self.renderer);
ManuallyDrop::drop(&mut self.context);
}
let context = context.treat_as_possibly_current();
self.context = ManuallyDrop::new(context);
self.context.make_current(&self.surface).expect("failed to reativate context after reset.");
let renderer = Renderer::new(&self.context, self.renderer_preference)
.expect("failed to recreate renderer after reset");
self.renderer = ManuallyDrop::new(renderer);
self.renderer.resize(&self.size_info);
self.reset_glyph_cache();
self.damage_tracker.frame().mark_fully_damaged();
debug!("Recovered window {:?} from gpu reset", self.window.id());
}
fn swap_buffers(&self) {
#[allow(clippy::single_match)]
let res = match (self.surface.deref(), &self.context.deref()) {
#[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}");
}
}
fn update_font_size(
glyph_cache: &mut GlyphCache,
config: &UiConfig,
font: &Font,
) -> (f32, f32) {
let _ = glyph_cache.update_font_size(font);
compute_cell_size(config, &glyph_cache.font_metrics())
}
fn reset_glyph_cache(&mut self) {
let cache = &mut self.glyph_cache;
self.renderer.with_loader(|mut api| {
cache.reset_glyph_cache(&mut api);
});
}
pub fn handle_update<T>(
&mut self,
terminal: &mut Term<T>,
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
}
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: {cell_width} x {cell_height}");
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,
);
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);
if config.window.resize_increments {
self.window.set_resize_increments(PhysicalSize::new(cell_width, cell_height));
}
if self.size_info.screen_lines() != new_size.screen_lines
|| self.size_info.columns() != new_size.columns()
{
pty_resize_handle.on_resize(new_size.into());
terminal.resize(new_size);
self.damage_tracker.resize(new_size.screen_lines(), new_size.columns());
}
if new_size != self.size_info {
let renderer_update = self.pending_renderer_update.get_or_insert(Default::default());
renderer_update.resize = true;
search_state.clear_focused_match();
}
self.size_info = new_size;
}
pub fn process_renderer_update(&mut self) {
let renderer_update = match self.pending_renderer_update.take() {
Some(renderer_update) => renderer_update,
_ => return,
};
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);
}
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());
}
pub fn draw<T: EventListener>(
&mut self,
mut terminal: MutexGuard<'_, Term<T>>,
scheduler: &mut Scheduler,
message_buffer: &MessageBuffer,
config: &UiConfig,
search_state: &mut SearchState,
) {
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 };
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);
self.validate_hint_highlights(display_offset);
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);
self.make_current();
self.renderer.clear(background_color, config.window_opacity());
let mut lines = RenderLines::new();
let has_highlighted_hint =
self.highlighted_hint.is_some() || self.vi_highlighted_hint.is_some();
{
let _sampler = self.meter.sampler();
#[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| {
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<HintMatch>| {
hint.as_ref().is_some_and(|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);
}
}
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 {
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() {
self.draw_line_indicator(config, total_lines, None, display_offset);
};
rects.extend(cursor.rects(&size_info, config.cursor.thickness()));
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);
}
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());
self.draw_search(config, &search_text);
let line = size_info.screen_lines();
let column = Column(search_text.chars().count() - 1);
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,
}
},
};
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);
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.);
rects.push(message_bar_rect);
self.damage_tracker.frame().add_viewport_rect(&size_info, x, y as i32, width, height);
self.renderer.draw_rects(&size_info, &metrics, rects);
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 {
self.renderer.draw_rects(&size_info, &metrics, rects);
}
self.draw_render_timer(config);
if has_highlighted_hint {
let cursor_point = vi_cursor_point.or(Some(cursor_point));
self.draw_hyperlink_preview(config, cursor_point, display_offset);
}
self.window.pre_present_notify();
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);
}
self.swap_buffers();
if matches!(self.raw_window_handle, RawWindowHandle::Xcb(_) | RawWindowHandle::Xlib(_)) {
self.renderer.finish();
}
if !matches!(self.raw_window_handle, RawWindowHandle::Wayland(_)) {
self.request_frame(scheduler);
}
self.damage_tracker.swap_damage();
}
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);
}
pub fn update_highlighted_hints<T>(
&mut self,
term: &Term<T>,
config: &UiConfig,
mouse: &Mouse,
modifiers: ModifiersState,
) -> bool {
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;
if dirty {
self.damage_tracker.frame().mark_fully_damaged();
}
if !self.window.mouse_visible()
|| !mouse.inside_text_area
|| !term.selection.as_ref().is_none_or(Selection::is_empty)
{
if self.highlighted_hint.take().is_some() {
self.damage_tracker.frame().mark_fully_damaged();
dirty = true;
}
return dirty;
}
let point = mouse.point(&self.size_info, term.grid().display_offset());
let highlighted_hint = hint::highlighted_at(term, config, point, modifiers);
if highlighted_hint.is_some() {
dirty = self.hint_mouse_point.is_some_and(|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;
if mouse_highlight_dirty {
self.damage_tracker.frame().mark_fully_damaged();
}
dirty
}
#[inline(never)]
fn draw_ime_preview(
&mut self,
point: Point<usize>,
fg: Rgb,
bg: Rgb,
rects: &mut Vec<RenderRect>,
config: &UiConfig,
) {
let preedit = match self.ime.preedit() {
Some(preedit) => preedit,
None => {
self.window.update_ime_position(point, &self.size_info);
return;
},
};
let num_cols = self.size_info.columns();
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,
);
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);
}
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) => {
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);
}
fn format_search(search_regex: &str, search_label: &str, max_width: usize) -> String {
let label_len = search_label.len();
if label_len > max_width {
return search_label[..max_width].to_owned();
}
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),
));
bar_text.push(' ');
bar_text
}
#[inline(never)]
fn draw_hyperlink_preview(
&mut self,
config: &UiConfig,
cursor_point: Option<Point>,
display_offset: usize,
) {
let num_cols = self.size_info.columns();
let uris: Vec<_> = self
.highlighted_hint
.iter()
.chain(&self.vi_highlighted_hint)
.filter_map(|hint| hint.hyperlink().map(|hyperlink| hyperlink.uri()))
.map(|uri| StrShortener::new(uri, num_cols, ShortenDirection::Right, Some(SHORTENER)))
.collect();
if uris.is_empty() {
return;
}
let max_protected_lines = uris.len() * 2;
let mut protected_lines = Vec::with_capacity(max_protected_lines);
if self.size_info.screen_lines() > max_protected_lines {
protected_lines.push(self.hint_mouse_point.map(|point| point.line));
protected_lines.push(cursor_point.map(|point| point.line));
}
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) {
let damage = LineDamageBounds::new(point.line, point.column.0, num_cols);
self.damage_tracker.frame().damage_line(damage);
self.damage_tracker.next_frame().damage_line(damage);
self.renderer.draw_string(point, fg, bg, uri, &self.size_info, &mut self.glyph_cache);
}
}
#[inline(never)]
fn draw_search(&mut self, config: &UiConfig, text: &str) {
let num_cols = self.size_info.columns();
let text = format!("{text:<num_cols$}");
let point = Point::new(self.size_info.screen_lines(), Column(0));
let fg = config.colors.footer_bar_foreground();
let bg = config.colors.footer_bar_background();
self.renderer.draw_string(
point,
fg,
bg,
text.chars(),
&self.size_info,
&mut self.glyph_cache,
);
}
#[inline(never)]
fn draw_render_timer(&mut self, config: &UiConfig) {
if !config.debug.render_timer {
return;
}
let timing = format!("{:.3} usec", self.meter.average());
let point = Point::new(self.size_info.screen_lines().saturating_sub(2), Column(0));
let fg = config.colors.primary.background;
let bg = config.colors.normal.red;
let damage = LineDamageBounds::new(point.line, point.column.0, timing.len());
self.damage_tracker.frame().damage_line(damage);
self.damage_tracker.next_frame().damage_line(damage);
let glyph_cache = &mut self.glyph_cache;
self.renderer.draw_string(point, fg, bg, timing.chars(), &self.size_info, glyph_cache);
}
#[inline(never)]
fn draw_line_indicator(
&mut self,
config: &UiConfig,
total_lines: usize,
obstructed_column: Option<Column>,
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);
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);
if obstructed_column.is_none_or(|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);
}
}
fn highlight_damage(&self, render_rects: &mut Vec<RenderRect>) {
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);
}
}
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),
];
let num_lines = self.size_info.screen_lines();
for (hint, hint_age, reset_mouse) in hints {
let (start, end) = match hint {
Some(hint) => (*hint.bounds().start(), *hint.bounds().end()),
None => continue,
};
*hint_age += 1;
if *hint_age == 1 {
continue;
}
let start = term::point_to_viewport(display_offset, start)
.filter(|point| point.line < num_lines)
.unwrap_or_default();
let end = term::point_to_viewport(display_offset, end)
.filter(|point| point.line < num_lines)
.unwrap_or_else(|| Point::new(num_lines - 1, self.size_info.last_column()));
if frame.intersects(start, end) {
if reset_mouse {
self.window.set_mouse_cursor(CursorIcon::Default);
}
frame.mark_fully_damaged();
*hint = None;
}
}
}
fn request_frame(&mut self, scheduler: &mut Scheduler) {
self.window.has_frame = false;
let monitor_vblank_interval = 1_000_000.
/ self
.window
.current_monitor()
.and_then(|monitor| monitor.refresh_rate_millihertz())
.unwrap_or(60_000) as f64;
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) {
self.make_current();
unsafe {
ManuallyDrop::drop(&mut self.renderer);
ManuallyDrop::drop(&mut self.context);
ManuallyDrop::drop(&mut self.surface);
}
}
}
#[derive(Debug, Default)]
pub struct Ime {
enabled: bool,
preedit: Option<Preedit>,
}
impl Ime {
#[inline]
pub fn set_enabled(&mut self, is_enabled: bool) {
if is_enabled {
self.enabled = is_enabled
} else {
*self = Default::default();
}
}
#[inline]
pub fn is_enabled(&self) -> bool {
self.enabled
}
#[inline]
pub fn set_preedit(&mut self, preedit: Option<Preedit>) {
self.preedit = preedit;
}
#[inline]
pub fn preedit(&self) -> Option<&Preedit> {
self.preedit.as_ref()
}
}
#[derive(Debug, Default, PartialEq, Eq)]
pub struct Preedit {
text: String,
cursor_byte_offset: Option<(usize, usize)>,
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 {
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 }
}
}
#[derive(Debug, Default, Copy, Clone)]
pub struct RendererUpdate {
resize: bool,
clear_font_cache: bool,
}
pub struct FrameTimer {
base: Instant,
last_synced_timestamp: Instant,
refresh_interval: Duration,
}
impl FrameTimer {
pub fn new() -> Self {
let now = Instant::now();
Self { base: now, last_synced_timestamp: now, refresh_interval: Duration::ZERO }
}
pub fn compute_timeout(&mut self, refresh_interval: Duration) -> Duration {
let now = Instant::now();
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 {
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 {
self.last_synced_timestamp = next_frame;
next_frame - now
}
}
}
#[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,
)
}
fn window_size(
config: &UiConfig,
dimensions: Dimensions,
cell_width: f32,
cell_height: f32,
scale_factor: f32,
) -> PhysicalSize<u32> {
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)
}