aboutsummaryrefslogtreecommitdiff
path: root/alacritty/src/display/mod.rs
diff options
context:
space:
mode:
Diffstat (limited to 'alacritty/src/display/mod.rs')
-rw-r--r--alacritty/src/display/mod.rs823
1 files changed, 823 insertions, 0 deletions
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<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::ContextError> for Error {
+ fn from(val: glutin::ContextError) -> Self {
+ Error::ContextError(val)
+ }
+}
+
+#[derive(Default, Clone, Debug, PartialEq)]
+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;
+ }
+}
+
+/// 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<Url>,
+
+ #[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))]
+ pub wayland_event_queue: Option<EventQueue>,
+
+ #[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<E>(config: &Config, event_loop: &EventLoop<E>) -> Result<Display, Error> {
+ // 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<T>(
+ &mut self,
+ terminal: &mut Term<T>,
+ 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<T: EventListener>(
+ &mut self,
+ terminal: MutexGuard<'_, Term<T>>,
+ 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<Point>,
+ 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<u32> {
+ 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)
+}