aboutsummaryrefslogtreecommitdiff
path: root/alacritty/src/display
diff options
context:
space:
mode:
Diffstat (limited to 'alacritty/src/display')
-rw-r--r--alacritty/src/display/bell.rs122
-rw-r--r--alacritty/src/display/color.rs167
-rw-r--r--alacritty/src/display/content.rs404
-rw-r--r--alacritty/src/display/cursor.rs92
-rw-r--r--alacritty/src/display/meter.rs97
-rw-r--r--alacritty/src/display/mod.rs823
-rw-r--r--alacritty/src/display/wayland_theme.rs81
-rw-r--r--alacritty/src/display/window.rs497
8 files changed, 2283 insertions, 0 deletions
diff --git a/alacritty/src/display/bell.rs b/alacritty/src/display/bell.rs
new file mode 100644
index 00000000..1aee3ba6
--- /dev/null
+++ b/alacritty/src/display/bell.rs
@@ -0,0 +1,122 @@
+use std::time::{Duration, Instant};
+
+use crate::config::bell::{BellAnimation, BellConfig};
+
+pub struct VisualBell {
+ /// Visual bell animation.
+ animation: BellAnimation,
+
+ /// Visual bell duration.
+ duration: Duration,
+
+ /// The last time the visual bell rang, if at all.
+ start_time: Option<Instant>,
+}
+
+impl VisualBell {
+ /// Ring the visual bell, and return its intensity.
+ pub fn ring(&mut self) -> f64 {
+ let now = Instant::now();
+ self.start_time = Some(now);
+ self.intensity_at_instant(now)
+ }
+
+ /// Get the currently intensity of the visual bell. The bell's intensity
+ /// ramps down from 1.0 to 0.0 at a rate determined by the bell's duration.
+ pub fn intensity(&self) -> f64 {
+ self.intensity_at_instant(Instant::now())
+ }
+
+ /// Check whether or not the visual bell has completed "ringing".
+ pub fn completed(&mut self) -> bool {
+ match self.start_time {
+ Some(earlier) => {
+ if Instant::now().duration_since(earlier) >= self.duration {
+ self.start_time = None;
+ }
+ false
+ },
+ None => true,
+ }
+ }
+
+ /// Get the intensity of the visual bell at a particular instant. The bell's
+ /// intensity ramps down from 1.0 to 0.0 at a rate determined by the bell's
+ /// duration.
+ pub fn intensity_at_instant(&self, instant: Instant) -> f64 {
+ // If `duration` is zero, then the VisualBell is disabled; therefore,
+ // its `intensity` is zero.
+ if self.duration == Duration::from_secs(0) {
+ return 0.0;
+ }
+
+ match self.start_time {
+ // Similarly, if `start_time` is `None`, then the VisualBell has not
+ // been "rung"; therefore, its `intensity` is zero.
+ None => 0.0,
+
+ Some(earlier) => {
+ // Finally, if the `instant` at which we wish to compute the
+ // VisualBell's `intensity` occurred before the VisualBell was
+ // "rung", then its `intensity` is also zero.
+ if instant < earlier {
+ return 0.0;
+ }
+
+ let elapsed = instant.duration_since(earlier);
+ let elapsed_f =
+ elapsed.as_secs() as f64 + f64::from(elapsed.subsec_nanos()) / 1e9f64;
+ let duration_f = self.duration.as_secs() as f64
+ + f64::from(self.duration.subsec_nanos()) / 1e9f64;
+
+ // Otherwise, we compute a value `time` from 0.0 to 1.0
+ // inclusive that represents the ratio of `elapsed` time to the
+ // `duration` of the VisualBell.
+ let time = (elapsed_f / duration_f).min(1.0);
+
+ // We use this to compute the inverse `intensity` of the
+ // VisualBell. When `time` is 0.0, `inverse_intensity` is 0.0,
+ // and when `time` is 1.0, `inverse_intensity` is 1.0.
+ let inverse_intensity = match self.animation {
+ BellAnimation::Ease | BellAnimation::EaseOut => {
+ cubic_bezier(0.25, 0.1, 0.25, 1.0, time)
+ },
+ BellAnimation::EaseOutSine => cubic_bezier(0.39, 0.575, 0.565, 1.0, time),
+ BellAnimation::EaseOutQuad => cubic_bezier(0.25, 0.46, 0.45, 0.94, time),
+ BellAnimation::EaseOutCubic => cubic_bezier(0.215, 0.61, 0.355, 1.0, time),
+ BellAnimation::EaseOutQuart => cubic_bezier(0.165, 0.84, 0.44, 1.0, time),
+ BellAnimation::EaseOutQuint => cubic_bezier(0.23, 1.0, 0.32, 1.0, time),
+ BellAnimation::EaseOutExpo => cubic_bezier(0.19, 1.0, 0.22, 1.0, time),
+ BellAnimation::EaseOutCirc => cubic_bezier(0.075, 0.82, 0.165, 1.0, time),
+ BellAnimation::Linear => time,
+ };
+
+ // Since we want the `intensity` of the VisualBell to decay over
+ // `time`, we subtract the `inverse_intensity` from 1.0.
+ 1.0 - inverse_intensity
+ },
+ }
+ }
+
+ pub fn update_config(&mut self, bell_config: &BellConfig) {
+ self.animation = bell_config.animation;
+ self.duration = bell_config.duration();
+ }
+}
+
+impl From<&BellConfig> for VisualBell {
+ fn from(bell_config: &BellConfig) -> VisualBell {
+ VisualBell {
+ animation: bell_config.animation,
+ duration: bell_config.duration(),
+ start_time: None,
+ }
+ }
+}
+
+fn cubic_bezier(p0: f64, p1: f64, p2: f64, p3: f64, x: f64) -> f64 {
+ (1.0 - x).powi(3) * p0
+ + 3.0 * (1.0 - x).powi(2) * x * p1
+ + 3.0 * (1.0 - x) * x.powi(2) * p2
+ + x.powi(3) * p3
+}
diff --git a/alacritty/src/display/color.rs b/alacritty/src/display/color.rs
new file mode 100644
index 00000000..6e0de048
--- /dev/null
+++ b/alacritty/src/display/color.rs
@@ -0,0 +1,167 @@
+use std::ops::{Index, IndexMut};
+
+use log::trace;
+
+use alacritty_terminal::ansi::NamedColor;
+use alacritty_terminal::term::color::{Rgb, COUNT};
+
+use crate::config::color::Colors;
+
+/// Factor for automatic computation of dim colors.
+pub const DIM_FACTOR: f32 = 0.66;
+
+#[derive(Copy, Clone)]
+pub struct List([Rgb; COUNT]);
+
+impl<'a> From<&'a Colors> for List {
+ fn from(colors: &Colors) -> List {
+ // Type inference fails without this annotation.
+ let mut list = List([Rgb::default(); COUNT]);
+
+ list.fill_named(colors);
+ list.fill_cube(colors);
+ list.fill_gray_ramp(colors);
+
+ list
+ }
+}
+
+impl List {
+ pub fn fill_named(&mut self, colors: &Colors) {
+ // Normals.
+ self[NamedColor::Black] = colors.normal.black;
+ self[NamedColor::Red] = colors.normal.red;
+ self[NamedColor::Green] = colors.normal.green;
+ self[NamedColor::Yellow] = colors.normal.yellow;
+ self[NamedColor::Blue] = colors.normal.blue;
+ self[NamedColor::Magenta] = colors.normal.magenta;
+ self[NamedColor::Cyan] = colors.normal.cyan;
+ self[NamedColor::White] = colors.normal.white;
+
+ // Brights.
+ self[NamedColor::BrightBlack] = colors.bright.black;
+ self[NamedColor::BrightRed] = colors.bright.red;
+ self[NamedColor::BrightGreen] = colors.bright.green;
+ self[NamedColor::BrightYellow] = colors.bright.yellow;
+ self[NamedColor::BrightBlue] = colors.bright.blue;
+ self[NamedColor::BrightMagenta] = colors.bright.magenta;
+ self[NamedColor::BrightCyan] = colors.bright.cyan;
+ self[NamedColor::BrightWhite] = colors.bright.white;
+ self[NamedColor::BrightForeground] =
+ colors.primary.bright_foreground.unwrap_or(colors.primary.foreground);
+
+ // Foreground and background.
+ self[NamedColor::Foreground] = colors.primary.foreground;
+ self[NamedColor::Background] = colors.primary.background;
+
+ // Dims.
+ self[NamedColor::DimForeground] =
+ colors.primary.dim_foreground.unwrap_or(colors.primary.foreground * DIM_FACTOR);
+ match colors.dim {
+ Some(ref dim) => {
+ trace!("Using config-provided dim colors");
+ self[NamedColor::DimBlack] = dim.black;
+ self[NamedColor::DimRed] = dim.red;
+ self[NamedColor::DimGreen] = dim.green;
+ self[NamedColor::DimYellow] = dim.yellow;
+ self[NamedColor::DimBlue] = dim.blue;
+ self[NamedColor::DimMagenta] = dim.magenta;
+ self[NamedColor::DimCyan] = dim.cyan;
+ self[NamedColor::DimWhite] = dim.white;
+ },
+ None => {
+ trace!("Deriving dim colors from normal colors");
+ self[NamedColor::DimBlack] = colors.normal.black * DIM_FACTOR;
+ self[NamedColor::DimRed] = colors.normal.red * DIM_FACTOR;
+ self[NamedColor::DimGreen] = colors.normal.green * DIM_FACTOR;
+ self[NamedColor::DimYellow] = colors.normal.yellow * DIM_FACTOR;
+ self[NamedColor::DimBlue] = colors.normal.blue * DIM_FACTOR;
+ self[NamedColor::DimMagenta] = colors.normal.magenta * DIM_FACTOR;
+ self[NamedColor::DimCyan] = colors.normal.cyan * DIM_FACTOR;
+ self[NamedColor::DimWhite] = colors.normal.white * DIM_FACTOR;
+ },
+ }
+ }
+
+ pub fn fill_cube(&mut self, colors: &Colors) {
+ let mut index: usize = 16;
+ // Build colors.
+ for r in 0..6 {
+ for g in 0..6 {
+ for b in 0..6 {
+ // Override colors 16..232 with the config (if present).
+ if let Some(indexed_color) =
+ colors.indexed_colors.iter().find(|ic| ic.index() == index as u8)
+ {
+ self[index] = indexed_color.color;
+ } else {
+ self[index] = Rgb {
+ r: if r == 0 { 0 } else { r * 40 + 55 },
+ b: if b == 0 { 0 } else { b * 40 + 55 },
+ g: if g == 0 { 0 } else { g * 40 + 55 },
+ };
+ }
+ index += 1;
+ }
+ }
+ }
+
+ debug_assert!(index == 232);
+ }
+
+ pub fn fill_gray_ramp(&mut self, colors: &Colors) {
+ let mut index: usize = 232;
+
+ for i in 0..24 {
+ // Index of the color is number of named colors + number of cube colors + i.
+ let color_index = 16 + 216 + i;
+
+ // Override colors 232..256 with the config (if present).
+ if let Some(indexed_color) =
+ colors.indexed_colors.iter().find(|ic| ic.index() == color_index)
+ {
+ self[index] = indexed_color.color;
+ index += 1;
+ continue;
+ }
+
+ let value = i * 10 + 8;
+ self[index] = Rgb { r: value, g: value, b: value };
+ index += 1;
+ }
+
+ debug_assert!(index == 256);
+ }
+}
+
+impl Index<usize> for List {
+ type Output = Rgb;
+
+ #[inline]
+ fn index(&self, idx: usize) -> &Self::Output {
+ &self.0[idx]
+ }
+}
+
+impl IndexMut<usize> for List {
+ #[inline]
+ fn index_mut(&mut self, idx: usize) -> &mut Self::Output {
+ &mut self.0[idx]
+ }
+}
+
+impl Index<NamedColor> for List {
+ type Output = Rgb;
+
+ #[inline]
+ fn index(&self, idx: NamedColor) -> &Self::Output {
+ &self.0[idx as usize]
+ }
+}
+
+impl IndexMut<NamedColor> for List {
+ #[inline]
+ fn index_mut(&mut self, idx: NamedColor) -> &mut Self::Output {
+ &mut self.0[idx as usize]
+ }
+}
diff --git a/alacritty/src/display/content.rs b/alacritty/src/display/content.rs
new file mode 100644
index 00000000..81c2977f
--- /dev/null
+++ b/alacritty/src/display/content.rs
@@ -0,0 +1,404 @@
+use std::cmp::max;
+use std::mem;
+use std::ops::RangeInclusive;
+
+use alacritty_terminal::ansi::{Color, CursorShape, NamedColor};
+use alacritty_terminal::config::Config;
+use alacritty_terminal::event::EventListener;
+use alacritty_terminal::grid::{Dimensions, Indexed};
+use alacritty_terminal::index::{Column, Direction, Line, Point};
+use alacritty_terminal::term::cell::{Cell, Flags};
+use alacritty_terminal::term::color::{CellRgb, Rgb};
+use alacritty_terminal::term::search::{RegexIter, RegexSearch};
+use alacritty_terminal::term::{
+ RenderableContent as TerminalContent, RenderableCursor as TerminalCursor, Term, TermMode,
+};
+
+use crate::config::ui_config::UIConfig;
+use crate::display::color::{List, DIM_FACTOR};
+
+/// Minimum contrast between a fixed cursor color and the cell's background.
+pub const MIN_CURSOR_CONTRAST: f64 = 1.5;
+
+/// Maximum number of linewraps followed outside of the viewport during search highlighting.
+const MAX_SEARCH_LINES: usize = 100;
+
+/// Renderable terminal content.
+///
+/// This provides the terminal cursor and an iterator over all non-empty cells.
+pub struct RenderableContent<'a> {
+ terminal_content: TerminalContent<'a>,
+ terminal_cursor: TerminalCursor,
+ cursor: Option<RenderableCursor>,
+ search: RenderableSearch,
+ config: &'a Config<UIConfig>,
+ colors: &'a List,
+}
+
+impl<'a> RenderableContent<'a> {
+ pub fn new<T: EventListener>(
+ term: &'a Term<T>,
+ dfas: Option<&RegexSearch>,
+ config: &'a Config<UIConfig>,
+ colors: &'a List,
+ show_cursor: bool,
+ ) -> Self {
+ let search = dfas.map(|dfas| RenderableSearch::new(&term, dfas)).unwrap_or_default();
+ let terminal_content = term.renderable_content();
+
+ // Copy the cursor and override its shape if necessary.
+ let mut terminal_cursor = terminal_content.cursor;
+ if !show_cursor {
+ terminal_cursor.shape = CursorShape::Hidden;
+ } else if !term.is_focused && config.cursor.unfocused_hollow {
+ terminal_cursor.shape = CursorShape::HollowBlock;
+ }
+
+ Self { cursor: None, terminal_content, terminal_cursor, search, config, colors }
+ }
+
+ /// Viewport offset.
+ pub fn display_offset(&self) -> usize {
+ self.terminal_content.display_offset
+ }
+
+ /// Get the terminal cursor.
+ pub fn cursor(mut self) -> Option<RenderableCursor> {
+ // Drain the iterator to make sure the cursor is created.
+ while self.next().is_some() && self.cursor.is_none() {}
+
+ self.cursor
+ }
+
+ /// Get the RGB value for a color index.
+ pub fn color(&self, color: usize) -> Rgb {
+ self.terminal_content.colors[color].unwrap_or(self.colors[color])
+ }
+
+ /// Assemble the information required to render the terminal cursor.
+ ///
+ /// This will return `None` when there is no cursor visible.
+ fn renderable_cursor(&mut self, cell: &RenderableCell) -> Option<RenderableCursor> {
+ if self.terminal_cursor.shape == CursorShape::Hidden {
+ return None;
+ }
+
+ // Expand across wide cell when inside wide char or spacer.
+ let is_wide = if cell.flags.contains(Flags::WIDE_CHAR_SPACER) {
+ self.terminal_cursor.point.column -= 1;
+ true
+ } else {
+ cell.flags.contains(Flags::WIDE_CHAR)
+ };
+
+ // Cursor colors.
+ let color = if self.terminal_content.mode.contains(TermMode::VI) {
+ self.config.ui_config.colors.vi_mode_cursor
+ } else {
+ self.config.ui_config.colors.cursor
+ };
+ let mut cursor_color =
+ self.terminal_content.colors[NamedColor::Cursor].map_or(color.background, CellRgb::Rgb);
+ let mut text_color = color.foreground;
+
+ // Invert the cursor if it has a fixed background close to the cell's background.
+ if matches!(
+ cursor_color,
+ CellRgb::Rgb(color) if color.contrast(cell.bg) < MIN_CURSOR_CONTRAST
+ ) {
+ cursor_color = CellRgb::CellForeground;
+ text_color = CellRgb::CellBackground;
+ }
+
+ // Convert from cell colors to RGB.
+ let text_color = text_color.color(cell.fg, cell.bg);
+ let cursor_color = cursor_color.color(cell.fg, cell.bg);
+
+ Some(RenderableCursor {
+ point: self.terminal_cursor.point,
+ shape: self.terminal_cursor.shape,
+ cursor_color,
+ text_color,
+ is_wide,
+ })
+ }
+}
+
+impl<'a> Iterator for RenderableContent<'a> {
+ type Item = RenderableCell;
+
+ /// Gets the next renderable cell.
+ ///
+ /// Skips empty (background) cells and applies any flags to the cell state
+ /// (eg. invert fg and bg colors).
+ #[inline]
+ fn next(&mut self) -> Option<Self::Item> {
+ loop {
+ let cell = self.terminal_content.display_iter.next()?;
+ let mut cell = RenderableCell::new(self, cell);
+
+ if self.terminal_cursor.point == cell.point {
+ // Store the cursor which should be rendered.
+ self.cursor = self.renderable_cursor(&cell).map(|cursor| {
+ if cursor.shape == CursorShape::Block {
+ cell.fg = cursor.text_color;
+ cell.bg = cursor.cursor_color;
+
+ // Since we draw Block cursor by drawing cell below it with a proper color,
+ // we must adjust alpha to make it visible.
+ cell.bg_alpha = 1.;
+ }
+
+ cursor
+ });
+
+ return Some(cell);
+ } else if !cell.is_empty() && !cell.flags.contains(Flags::WIDE_CHAR_SPACER) {
+ // Skip empty cells and wide char spacers.
+ return Some(cell);
+ }
+ }
+ }
+}
+
+/// Cell ready for rendering.
+#[derive(Clone, Debug)]
+pub struct RenderableCell {
+ pub character: char,
+ pub zerowidth: Option<Vec<char>>,
+ pub point: Point,
+ pub fg: Rgb,
+ pub bg: Rgb,
+ pub bg_alpha: f32,
+ pub flags: Flags,
+ pub is_match: bool,
+}
+
+impl RenderableCell {
+ fn new<'a>(content: &mut RenderableContent<'a>, cell: Indexed<&Cell, Line>) -> Self {
+ // Lookup RGB values.
+ let mut fg_rgb = Self::compute_fg_rgb(content, cell.fg, cell.flags);
+ let mut bg_rgb = Self::compute_bg_rgb(content, cell.bg);
+
+ let mut bg_alpha = if cell.flags.contains(Flags::INVERSE) {
+ mem::swap(&mut fg_rgb, &mut bg_rgb);
+ 1.0
+ } else {
+ Self::compute_bg_alpha(cell.bg)
+ };
+
+ let is_selected = content
+ .terminal_content
+ .selection
+ .map_or(false, |selection| selection.contains_cell(&cell, content.terminal_cursor));
+ let mut is_match = false;
+
+ let colors = &content.config.ui_config.colors;
+ if is_selected {
+ let config_bg = colors.selection.background;
+ let selected_fg = colors.selection.foreground.color(fg_rgb, bg_rgb);
+ bg_rgb = config_bg.color(fg_rgb, bg_rgb);
+ fg_rgb = selected_fg;
+
+ if fg_rgb == bg_rgb && !cell.flags.contains(Flags::HIDDEN) {
+ // Reveal inversed text when fg/bg is the same.
+ fg_rgb = content.color(NamedColor::Background as usize);
+ bg_rgb = content.color(NamedColor::Foreground as usize);
+ bg_alpha = 1.0;
+ } else if config_bg != CellRgb::CellBackground {
+ bg_alpha = 1.0;
+ }
+ } else if content.search.advance(cell.point) {
+ // Highlight the cell if it is part of a search match.
+ let config_bg = colors.search.matches.background;
+ let matched_fg = colors.search.matches.foreground.color(fg_rgb, bg_rgb);
+ bg_rgb = config_bg.color(fg_rgb, bg_rgb);
+ fg_rgb = matched_fg;
+
+ if config_bg != CellRgb::CellBackground {
+ bg_alpha = 1.0;
+ }
+
+ is_match = true;
+ }
+
+ RenderableCell {
+ character: cell.c,
+ zerowidth: cell.zerowidth().map(|zerowidth| zerowidth.to_vec()),
+ point: cell.point,
+ fg: fg_rgb,
+ bg: bg_rgb,
+ bg_alpha,
+ flags: cell.flags,
+ is_match,
+ }
+ }
+
+ /// Check if cell contains any renderable content.
+ fn is_empty(&self) -> bool {
+ self.bg_alpha == 0.
+ && !self.flags.intersects(Flags::UNDERLINE | Flags::STRIKEOUT | Flags::DOUBLE_UNDERLINE)
+ && self.character == ' '
+ && self.zerowidth.is_none()
+ }
+
+ /// Get the RGB color from a cell's foreground color.
+ fn compute_fg_rgb(content: &mut RenderableContent<'_>, fg: Color, flags: Flags) -> Rgb {
+ let ui_config = &content.config.ui_config;
+ match fg {
+ Color::Spec(rgb) => match flags & Flags::DIM {
+ Flags::DIM => rgb * DIM_FACTOR,
+ _ => rgb,
+ },
+ Color::Named(ansi) => {
+ match (ui_config.draw_bold_text_with_bright_colors, flags & Flags::DIM_BOLD) {
+ // If no bright foreground is set, treat it like the BOLD flag doesn't exist.
+ (_, Flags::DIM_BOLD)
+ if ansi == NamedColor::Foreground
+ && ui_config.colors.primary.bright_foreground.is_none() =>
+ {
+ content.color(NamedColor::DimForeground as usize)
+ },
+ // Draw bold text in bright colors *and* contains bold flag.
+ (true, Flags::BOLD) => content.color(ansi.to_bright() as usize),
+ // Cell is marked as dim and not bold.
+ (_, Flags::DIM) | (false, Flags::DIM_BOLD) => {
+ content.color(ansi.to_dim() as usize)
+ },
+ // None of the above, keep original color..
+ _ => content.color(ansi as usize),
+ }
+ },
+ Color::Indexed(idx) => {
+ let idx = match (
+ ui_config.draw_bold_text_with_bright_colors,
+ flags & Flags::DIM_BOLD,
+ idx,
+ ) {
+ (true, Flags::BOLD, 0..=7) => idx as usize + 8,
+ (false, Flags::DIM, 8..=15) => idx as usize - 8,
+ (false, Flags::DIM, 0..=7) => NamedColor::DimBlack as usize + idx as usize,
+ _ => idx as usize,
+ };
+
+ content.color(idx)
+ },
+ }
+ }
+
+ /// Get the RGB color from a cell's background color.
+ #[inline]
+ fn compute_bg_rgb(content: &mut RenderableContent<'_>, bg: Color) -> Rgb {
+ match bg {
+ Color::Spec(rgb) => rgb,
+ Color::Named(ansi) => content.color(ansi as usize),
+ Color::Indexed(idx) => content.color(idx as usize),
+ }
+ }
+
+ /// Compute background alpha based on cell's original color.
+ ///
+ /// Since an RGB color matching the background should not be transparent, this is computed
+ /// using the named input color, rather than checking the RGB of the background after its color
+ /// is computed.
+ #[inline]
+ fn compute_bg_alpha(bg: Color) -> f32 {
+ if bg == Color::Named(NamedColor::Background) {
+ 0.
+ } else {
+ 1.
+ }
+ }
+}
+
+/// Cursor storing all information relevant for rendering.
+#[derive(Debug, Eq, PartialEq, Copy, Clone)]
+pub struct RenderableCursor {
+ shape: CursorShape,
+ cursor_color: Rgb,
+ text_color: Rgb,
+ is_wide: bool,
+ point: Point,
+}
+
+impl RenderableCursor {
+ pub fn color(&self) -> Rgb {
+ self.cursor_color
+ }
+
+ pub fn shape(&self) -> CursorShape {
+ self.shape
+ }
+
+ pub fn is_wide(&self) -> bool {
+ self.is_wide
+ }
+
+ pub fn point(&self) -> Point {
+ self.point
+ }
+}
+
+/// Regex search highlight tracking.
+#[derive(Default)]
+pub struct RenderableSearch {
+ /// All visible search matches.
+ matches: Vec<RangeInclusive<Point>>,
+
+ /// Index of the last match checked.
+ index: usize,
+}
+
+impl RenderableSearch {
+ /// Create a new renderable search iterator.
+ pub fn new<T>(term: &Term<T>, dfas: &RegexSearch) -> Self {
+ let viewport_end = term.grid().display_offset();
+ let viewport_start = viewport_end + term.screen_lines().0 - 1;
+
+ // Compute start of the first and end of the last line.
+ let start_point = Point::new(viewport_start, Column(0));
+ let mut start = term.line_search_left(start_point);
+ let end_point = Point::new(viewport_end, term.cols() - 1);
+ let mut end = term.line_search_right(end_point);
+
+ // Set upper bound on search before/after the viewport to prevent excessive blocking.
+ if start.line > viewport_start + MAX_SEARCH_LINES {
+ if start.line == 0 {
+ // Do not highlight anything if this line is the last.
+ return Self::default();
+ } else {
+ // Start at next line if this one is too long.
+ start.line -= 1;
+ }
+ }
+ end.line = max(end.line, viewport_end.saturating_sub(MAX_SEARCH_LINES));
+
+ // Create an iterater for the current regex search for all visible matches.
+ let iter = RegexIter::new(start, end, Direction::Right, term, dfas)
+ .skip_while(move |rm| rm.end().line > viewport_start)
+ .take_while(move |rm| rm.start().line >= viewport_end)
+ .map(|rm| {
+ let viewport_start = term.grid().clamp_buffer_to_visible(*rm.start());
+ let viewport_end = term.grid().clamp_buffer_to_visible(*rm.end());
+ viewport_start..=viewport_end
+ });
+
+ Self { matches: iter.collect(), index: 0 }
+ }
+
+ /// Advance the search tracker to the next point.
+ ///
+ /// This will return `true` if the point passed is part of a search match.
+ fn advance(&mut self, point: Point) -> bool {
+ while let Some(regex_match) = self.matches.get(self.index) {
+ if regex_match.start() > &point {
+ break;
+ } else if regex_match.end() < &point {
+ self.index += 1;
+ } else {
+ return true;
+ }
+ }
+ false
+ }
+}
diff --git a/alacritty/src/display/cursor.rs b/alacritty/src/display/cursor.rs
new file mode 100644
index 00000000..0750459d
--- /dev/null
+++ b/alacritty/src/display/cursor.rs
@@ -0,0 +1,92 @@
+//! Convert a cursor into an iterator of rects.
+
+use alacritty_terminal::ansi::CursorShape;
+use alacritty_terminal::term::color::Rgb;
+use alacritty_terminal::term::SizeInfo;
+
+use crate::display::content::RenderableCursor;
+use crate::renderer::rects::RenderRect;
+
+/// Trait for conversion into the iterator.
+pub trait IntoRects {
+ /// Consume the cursor for an iterator of rects.
+ fn rects(self, size_info: &SizeInfo, thickness: f32) -> CursorRects;
+}
+
+impl IntoRects for RenderableCursor {
+ fn rects(self, size_info: &SizeInfo, thickness: f32) -> CursorRects {
+ let point = self.point();
+ let x = point.column.0 as f32 * size_info.cell_width() + size_info.padding_x();
+ let y = point.line.0 as f32 * size_info.cell_height() + size_info.padding_y();
+
+ let mut width = size_info.cell_width();
+ let height = size_info.cell_height();
+
+ if self.is_wide() {
+ width *= 2.;
+ }
+
+ let thickness = (thickness * width as f32).round().max(1.);
+
+ match self.shape() {
+ CursorShape::Beam => beam(x, y, height, thickness, self.color()),
+ CursorShape::Underline => underline(x, y, width, height, thickness, self.color()),
+ CursorShape::HollowBlock => hollow(x, y, width, height, thickness, self.color()),
+ _ => CursorRects::default(),
+ }
+ }
+}
+
+/// Cursor rect iterator.
+#[derive(Default)]
+pub struct CursorRects {
+ rects: [Option<RenderRect>; 4],
+ index: usize,
+}
+
+impl From<RenderRect> for CursorRects {
+ fn from(rect: RenderRect) -> Self {
+ Self { rects: [Some(rect), None, None, None], index: 0 }
+ }
+}
+
+impl Iterator for CursorRects {
+ type Item = RenderRect;
+
+ fn next(&mut self) -> Option<Self::Item> {
+ let rect = self.rects.get_mut(self.index)?;
+ self.index += 1;
+ rect.take()
+ }
+}
+
+/// Create an iterator yielding a single beam rect.
+fn beam(x: f32, y: f32, height: f32, thickness: f32, color: Rgb) -> CursorRects {
+ RenderRect::new(x, y, thickness, height, color, 1.).into()
+}
+
+/// Create an iterator yielding a single underline rect.
+fn underline(x: f32, y: f32, width: f32, height: f32, thickness: f32, color: Rgb) -> CursorRects {
+ let y = y + height - thickness;
+ RenderRect::new(x, y, width, thickness, color, 1.).into()
+}
+
+/// Create an iterator yielding a rect for each side of the hollow block cursor.
+fn hollow(x: f32, y: f32, width: f32, height: f32, thickness: f32, color: Rgb) -> CursorRects {
+ let top_line = RenderRect::new(x, y, width, thickness, color, 1.);
+
+ let vertical_y = y + thickness;
+ let vertical_height = height - 2. * thickness;
+ let left_line = RenderRect::new(x, vertical_y, thickness, vertical_height, color, 1.);
+
+ let bottom_y = y + height - thickness;
+ let bottom_line = RenderRect::new(x, bottom_y, width, thickness, color, 1.);
+
+ let right_x = x + width - thickness;
+ let right_line = RenderRect::new(right_x, vertical_y, thickness, vertical_height, color, 1.);
+
+ CursorRects {
+ rects: [Some(top_line), Some(bottom_line), Some(left_line), Some(right_line)],
+ index: 0,
+ }
+}
diff --git a/alacritty/src/display/meter.rs b/alacritty/src/display/meter.rs
new file mode 100644
index 00000000..c07d901f
--- /dev/null
+++ b/alacritty/src/display/meter.rs
@@ -0,0 +1,97 @@
+//! Rendering time meter.
+//!
+//! Used to track rendering times and provide moving averages.
+//!
+//! # Examples
+//!
+//! ```rust
+//! // create a meter
+//! let mut meter = alacritty_terminal::meter::Meter::new();
+//!
+//! // Sample something.
+//! {
+//! let _sampler = meter.sampler();
+//! }
+//!
+//! // Get the moving average. The meter tracks a fixed number of samples, and
+//! // the average won't mean much until it's filled up at least once.
+//! println!("Average time: {}", meter.average());
+//! ```
+
+use std::time::{Duration, Instant};
+
+const NUM_SAMPLES: usize = 10;
+
+/// The meter.
+#[derive(Default)]
+pub struct Meter {
+ /// Track last 60 timestamps.
+ times: [f64; NUM_SAMPLES],
+
+ /// Average sample time in microseconds.
+ avg: f64,
+
+ /// Index of next time to update..
+ index: usize,
+}
+
+/// Sampler.
+///
+/// Samplers record how long they are "alive" for and update the meter on drop..
+pub struct Sampler<'a> {
+ /// Reference to meter that created the sampler.
+ meter: &'a mut Meter,
+
+ /// When the sampler was created.
+ created_at: Instant,
+}
+
+impl<'a> Sampler<'a> {
+ fn new(meter: &'a mut Meter) -> Sampler<'a> {
+ Sampler { meter, created_at: Instant::now() }
+ }
+
+ #[inline]
+ fn alive_duration(&self) -> Duration {
+ self.created_at.elapsed()
+ }
+}
+
+impl<'a> Drop for Sampler<'a> {
+ fn drop(&mut self) {
+ self.meter.add_sample(self.alive_duration());
+ }
+}
+
+impl Meter {
+ /// Create a meter.
+ pub fn new() -> Meter {
+ Default::default()
+ }
+
+ /// Get a sampler.
+ pub fn sampler(&mut self) -> Sampler<'_> {
+ Sampler::new(self)
+ }
+
+ /// Get the current average sample duration in microseconds.
+ pub fn average(&self) -> f64 {
+ self.avg
+ }
+
+ /// Add a sample.
+ ///
+ /// Used by Sampler::drop.
+ fn add_sample(&mut self, sample: Duration) {
+ let mut usec = 0f64;
+
+ usec += f64::from(sample.subsec_nanos()) / 1e3;
+ usec += (sample.as_secs() as f64) * 1e6;
+
+ let prev = self.times[self.index];
+ self.times[self.index] = usec;
+ self.avg -= prev / NUM_SAMPLES as f64;
+ self.avg += usec / NUM_SAMPLES as f64;
+ self.index = (self.index + 1) % NUM_SAMPLES;
+ }
+}
diff --git a/alacritty/src/display/mod.rs b/alacritty/src/display/mod.rs
new file mode 100644
index 00000000..2a55402e
--- /dev/null
+++ b/alacritty/src/display/mod.rs
@@ -0,0 +1,823 @@
+//! The display subsystem including window management, font rasterization, and
+//! GPU drawing.
+
+use std::cmp::min;
+use std::f64;
+use std::fmt::{self, Formatter};
+#[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))]
+use std::sync::atomic::Ordering;
+use std::time::Instant;
+
+use glutin::dpi::{PhysicalPosition, PhysicalSize};
+use glutin::event::ModifiersState;
+use glutin::event_loop::EventLoop;
+#[cfg(not(any(target_os = "macos", windows)))]
+use glutin::platform::unix::EventLoopWindowTargetExtUnix;
+use glutin::window::CursorIcon;
+use log::{debug, info};
+use parking_lot::MutexGuard;
+use unicode_width::UnicodeWidthChar;
+#[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))]
+use wayland_client::{Display as WaylandDisplay, EventQueue};
+
+use crossfont::{self, Rasterize, Rasterizer};
+
+use alacritty_terminal::ansi::NamedColor;
+use alacritty_terminal::event::{EventListener, OnResize};
+use alacritty_terminal::grid::Dimensions as _;
+use alacritty_terminal::index::{Column, Direction, Line, Point};
+use alacritty_terminal::selection::Selection;
+use alacritty_terminal::term::{SizeInfo, Term, TermMode, MIN_COLS, MIN_SCREEN_LINES};
+
+use crate::config::font::Font;
+use crate::config::window::Dimensions;
+#[cfg(not(windows))]
+use crate::config::window::StartupMode;
+use crate::config::Config;
+use crate::display::bell::VisualBell;
+use crate::display::color::List;
+use crate::display::content::RenderableContent;
+use crate::display::cursor::IntoRects;
+use crate::display::meter::Meter;
+use crate::display::window::Window;
+use crate::event::{Mouse, SearchState};
+use crate::message_bar::{MessageBuffer, MessageType};
+use crate::renderer::rects::{RenderLines, RenderRect};
+use crate::renderer::{self, GlyphCache, QuadRenderer};
+use crate::url::{Url, Urls};
+
+pub mod content;
+pub mod cursor;
+pub mod window;
+
+mod bell;
+mod color;
+mod meter;
+#[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))]
+mod wayland_theme;
+
+const FORWARD_SEARCH_LABEL: &str = "Search: ";
+const BACKWARD_SEARCH_LABEL: &str = "Backward Search: ";
+
+#[derive(Debug)]
+pub enum Error {
+ /// Error with window management.
+ Window(window::Error),
+
+ /// Error dealing with fonts.
+ Font(crossfont::Error),
+
+ /// Error in renderer.
+ Render(renderer::Error),
+
+ /// Error during buffer swap.
+ ContextError(glutin::ContextError),
+}
+
+impl std::error::Error for Error {
+ fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
+ match self {
+ Error::Window(err) => err.source(),
+ Error::Font(err) => err.source(),
+ Error::Render(err) => err.source(),
+ Error::ContextError(err) => err.source(),
+ }
+ }
+}
+
+impl fmt::Display for Error {
+ fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+ match self {
+ Error::Window(err) => err.fmt(f),
+ Error::Font(err) => err.fmt(f),
+ Error::Render(err) => err.fmt(f),
+ Error::ContextError(err) => err.fmt(f),
+ }
+ }
+}
+
+impl From<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)
+}
diff --git a/alacritty/src/display/wayland_theme.rs b/alacritty/src/display/wayland_theme.rs
new file mode 100644
index 00000000..1932ae01
--- /dev/null
+++ b/alacritty/src/display/wayland_theme.rs
@@ -0,0 +1,81 @@
+use glutin::platform::unix::{ARGBColor, Button, ButtonState, Element, Theme as WaylandTheme};
+
+use alacritty_terminal::term::color::Rgb;
+
+use crate::config::color::Colors;
+
+const INACTIVE_OPACITY: u8 = 127;
+
+#[derive(Debug, Clone)]
+pub struct AlacrittyWaylandTheme {
+ pub background: ARGBColor,
+ pub foreground: ARGBColor,
+ pub dim_foreground: ARGBColor,
+ pub hovered_close_icon: ARGBColor,
+ pub hovered_maximize_icon: ARGBColor,
+ pub hovered_minimize_icon: ARGBColor,
+}
+
+impl AlacrittyWaylandTheme {
+ pub fn new(colors: &Colors) -> Self {
+ let hovered_close_icon = colors.normal.red.into_rgba();
+ let hovered_maximize_icon = colors.normal.green.into_rgba();
+ let hovered_minimize_icon = colors.normal.yellow.into_rgba();
+ let foreground = colors.search_bar_foreground().into_rgba();
+ let background = colors.search_bar_background().into_rgba();
+
+ let mut dim_foreground = foreground;
+ dim_foreground.a = INACTIVE_OPACITY;
+
+ Self {
+ foreground,
+ background,
+ dim_foreground,
+ hovered_close_icon,
+ hovered_minimize_icon,
+ hovered_maximize_icon,
+ }
+ }
+}
+
+impl WaylandTheme for AlacrittyWaylandTheme {
+ fn element_color(&self, element: Element, window_active: bool) -> ARGBColor {
+ match element {
+ Element::Bar | Element::Separator => self.background,
+ Element::Text if window_active => self.foreground,
+ Element::Text => self.dim_foreground,
+ }
+ }
+
+ fn button_color(
+ &self,
+ button: Button,
+ state: ButtonState,
+ foreground: bool,
+ window_active: bool,
+ ) -> ARGBColor {
+ if !foreground {
+ return ARGBColor { a: 0, r: 0, g: 0, b: 0 };
+ } else if !window_active {
+ return self.dim_foreground;
+ }
+
+ match (state, button) {
+ (ButtonState::Idle, _) => self.foreground,
+ (ButtonState::Disabled, _) => self.dim_foreground,
+ (_, Button::Minimize) => self.hovered_minimize_icon,
+ (_, Button::Maximize) => self.hovered_maximize_icon,
+ (_, Button::Close) => self.hovered_close_icon,
+ }
+ }
+}
+
+trait IntoARGBColor {
+ fn into_rgba(self) -> ARGBColor;
+}
+
+impl IntoARGBColor for Rgb {
+ fn into_rgba(self) -> ARGBColor {
+ ARGBColor { a: 0xff, r: self.r, g: self.g, b: self.b }
+ }
+}
diff --git a/alacritty/src/display/window.rs b/alacritty/src/display/window.rs
new file mode 100644
index 00000000..b500e8f2
--- /dev/null
+++ b/alacritty/src/display/window.rs
@@ -0,0 +1,497 @@
+#[rustfmt::skip]
+#[cfg(not(any(target_os = "macos", windows)))]
+use {
+ std::sync::atomic::AtomicBool,
+ std::sync::Arc,
+
+ glutin::platform::unix::{WindowBuilderExtUnix, WindowExtUnix},
+};
+
+#[rustfmt::skip]
+#[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))]
+use {
+ wayland_client::protocol::wl_surface::WlSurface,
+ wayland_client::{Attached, EventQueue, Proxy},
+ glutin::platform::unix::EventLoopWindowTargetExtUnix,
+
+ crate::config::color::Colors,
+ crate::display::wayland_theme::AlacrittyWaylandTheme,
+};
+
+#[rustfmt::skip]
+#[cfg(all(feature = "x11", not(any(target_os = "macos", windows))))]
+use {
+ std::io::Cursor,
+
+ x11_dl::xlib::{Display as XDisplay, PropModeReplace, XErrorEvent, Xlib},
+ glutin::window::Icon,
+ png::Decoder,
+};
+
+use std::fmt::{self, Display, Formatter};
+
+#[cfg(target_os = "macos")]
+use cocoa::base::{id, NO, YES};
+use glutin::dpi::{PhysicalPosition, PhysicalSize};
+use glutin::event_loop::EventLoop;
+#[cfg(target_os = "macos")]
+use glutin::platform::macos::{WindowBuilderExtMacOS, WindowExtMacOS};
+#[cfg(windows)]
+use glutin::platform::windows::IconExtWindows;
+use glutin::window::{
+ CursorIcon, Fullscreen, UserAttentionType, Window as GlutinWindow, WindowBuilder, WindowId,
+};
+use glutin::{self, ContextBuilder, PossiblyCurrent, WindowedContext};
+#[cfg(target_os = "macos")]
+use objc::{msg_send, sel, sel_impl};
+#[cfg(target_os = "macos")]
+use raw_window_handle::{HasRawWindowHandle, RawWindowHandle};
+#[cfg(windows)]
+use winapi::shared::minwindef::WORD;
+
+use alacritty_terminal::index::Point;
+use alacritty_terminal::term::SizeInfo;
+
+use crate::config::window::{Decorations, WindowConfig};
+use crate::config::Config;
+use crate::gl;
+
+/// Window icon for `_NET_WM_ICON` property.
+#[cfg(all(feature = "x11", not(any(target_os = "macos", windows))))]
+static WINDOW_ICON: &[u8] = include_bytes!("../../alacritty.png");
+
+/// This should match the definition of IDI_ICON from `windows.rc`.
+#[cfg(windows)]
+const IDI_ICON: WORD = 0x101;
+
+/// Window errors.
+#[derive(Debug)]
+pub enum Error {
+ /// Error creating the window.
+ ContextCreation(glutin::CreationError),
+
+ /// Error dealing with fonts.
+ Font(crossfont::Error),
+
+ /// Error manipulating the rendering context.
+ Context(glutin::ContextError),
+}
+
+/// Result of fallible operations concerning a Window.
+type Result<T> = std::result::Result<T, Error>;
+
+impl std::error::Error for Error {
+ fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
+ match self {
+ Error::ContextCreation(err) => err.source(),
+ Error::Context(err) => err.source(),
+ Error::Font(err) => err.source(),
+ }
+ }
+}
+
+impl Display for Error {
+ fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+ match self {
+ Error::ContextCreation(err) => write!(f, "Error creating GL context; {}", err),
+ Error::Context(err) => write!(f, "Error operating on render context; {}", err),
+ Error::Font(err) => err.fmt(f),
+ }
+ }
+}
+
+impl From<glutin::CreationError> for Error {
+ fn from(val: glutin::CreationError) -> Self {
+ Error::ContextCreation(val)
+ }
+}
+
+impl From<glutin::ContextError> for Error {
+ fn from(val: glutin::ContextError) -> Self {
+ Error::Context(val)
+ }
+}
+
+impl From<crossfont::Error> for Error {
+ fn from(val: crossfont::Error) -> Self {
+ Error::Font(val)
+ }
+}
+
+fn create_gl_window<E>(
+ mut window: WindowBuilder,
+ event_loop: &EventLoop<E>,
+ srgb: bool,
+ vsync: bool,
+ dimensions: Option<PhysicalSize<u32>>,
+) -> Result<WindowedContext<PossiblyCurrent>> {
+ if let Some(dimensions) = dimensions {
+ window = window.with_inner_size(dimensions);
+ }
+
+ let windowed_context = ContextBuilder::new()
+ .with_srgb(srgb)
+ .with_vsync(vsync)
+ .with_hardware_acceleration(None)
+ .build_windowed(window, event_loop)?;
+
+ // Make the context current so OpenGL operations can run.
+ let windowed_context = unsafe { windowed_context.make_current().map_err(|(_, err)| err)? };
+
+ Ok(windowed_context)
+}
+
+/// A window which can be used for displaying the terminal.
+///
+/// Wraps the underlying windowing library to provide a stable API in Alacritty.
+pub struct Window {
+ /// Flag tracking frame redraw requests from Wayland compositor.
+ #[cfg(not(any(target_os = "macos", windows)))]
+ pub should_draw: Arc<AtomicBool>,
+
+ /// Attached Wayland surface to request new frame events.
+ #[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))]
+ pub wayland_surface: Option<Attached<WlSurface>>,
+
+ /// Cached DPR for quickly scaling pixel sizes.
+ pub dpr: f64,
+
+ windowed_context: WindowedContext<PossiblyCurrent>,
+ current_mouse_cursor: CursorIcon,
+ mouse_visible: bool,
+}
+
+impl Window {
+ /// Create a new window.
+ ///
+ /// This creates a window and fully initializes a window.
+ pub fn new<E>(
+ event_loop: &EventLoop<E>,
+ config: &Config,
+ size: Option<PhysicalSize<u32>>,
+ #[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))]
+ wayland_event_queue: Option<&EventQueue>,
+ ) -> Result<Window> {
+ let window_config = &config.ui_config.window;
+ let window_builder = Window::get_platform_window(&window_config.title, &window_config);
+
+ // Check if we're running Wayland to disable vsync.
+ #[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))]
+ let is_wayland = event_loop.is_wayland();
+ #[cfg(any(not(feature = "wayland"), target_os = "macos", windows))]
+ let is_wayland = false;
+
+ let windowed_context =
+ create_gl_window(window_builder.clone(), &event_loop, false, !is_wayland, size)
+ .or_else(|_| {
+ create_gl_window(window_builder, &event_loop, true, !is_wayland, size)
+ })?;
+
+ // Text cursor.
+ let current_mouse_cursor = CursorIcon::Text;
+ windowed_context.window().set_cursor_icon(current_mouse_cursor);
+
+ // Set OpenGL symbol loader. This call MUST be after window.make_current on windows.
+ gl::load_with(|symbol| windowed_context.get_proc_address(symbol) as *const _);
+
+ #[cfg(all(feature = "x11", not(any(target_os = "macos", windows))))]
+ if !is_wayland {
+ // On X11, embed the window inside another if the parent ID has been set.
+ if let Some(parent_window_id) = window_config.embed {
+ x_embed_window(windowed_context.window(), parent_window_id);
+ }
+ }
+
+ #[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))]
+ let wayland_surface = if is_wayland {
+ // Apply client side decorations theme.
+ let theme = AlacrittyWaylandTheme::new(&config.ui_config.colors);
+ windowed_context.window().set_wayland_theme(theme);
+
+ // Attach surface to Alacritty's internal wayland queue to handle frame callbacks.
+ let surface = windowed_context.window().wayland_surface().unwrap();
+ let proxy: Proxy<WlSurface> = unsafe { Proxy::from_c_ptr(surface as _) };
+ Some(proxy.attach(wayland_event_queue.as_ref().unwrap().token()))
+ } else {
+ None
+ };
+
+ let dpr = windowed_context.window().scale_factor();
+
+ Ok(Self {
+ current_mouse_cursor,
+ mouse_visible: true,
+ windowed_context,
+ #[cfg(not(any(target_os = "macos", windows)))]
+ should_draw: Arc::new(AtomicBool::new(true)),
+ #[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))]
+ wayland_surface,
+ dpr,
+ })
+ }
+
+ pub fn set_inner_size(&mut self, size: PhysicalSize<u32>) {
+ self.window().set_inner_size(size);
+ }
+
+ pub fn inner_size(&self) -> PhysicalSize<u32> {
+ self.window().inner_size()
+ }
+
+ #[inline]
+ pub fn set_visible(&self, visibility: bool) {
+ self.window().set_visible(visibility);
+ }
+
+ /// Set the window title.
+ #[inline]
+ pub fn set_title(&self, title: &str) {
+ self.window().set_title(title);
+ }
+
+ #[inline]
+ pub fn set_mouse_cursor(&mut self, cursor: CursorIcon) {
+ if cursor != self.current_mouse_cursor {
+ self.current_mouse_cursor = cursor;
+ self.window().set_cursor_icon(cursor);
+ }
+ }
+
+ /// Set mouse cursor visible.
+ pub fn set_mouse_visible(&mut self, visible: bool) {
+ if visible != self.mouse_visible {
+ self.mouse_visible = visible;
+ self.window().set_cursor_visible(visible);
+ }
+ }
+
+ #[cfg(not(any(target_os = "macos", windows)))]
+ pub fn get_platform_window(title: &str, window_config: &WindowConfig) -> WindowBuilder {
+ #[cfg(feature = "x11")]
+ let icon = {
+ let decoder = Decoder::new(Cursor::new(WINDOW_ICON));
+ let (info, mut reader) = decoder.read_info().expect("invalid embedded icon");
+ let mut buf = vec![0; info.buffer_size()];
+ let _ = reader.next_frame(&mut buf);
+ Icon::from_rgba(buf, info.width, info.height)
+ };
+
+ let builder = WindowBuilder::new()
+ .with_title(title)
+ .with_visible(false)
+ .with_transparent(true)
+ .with_decorations(window_config.decorations != Decorations::None)
+ .with_maximized(window_config.maximized())
+ .with_fullscreen(window_config.fullscreen());
+
+ #[cfg(feature = "x11")]
+ let builder = builder.with_window_icon(icon.ok());
+
+ #[cfg(feature = "wayland")]
+ let builder = builder.with_app_id(window_config.class.instance.to_owned());
+
+ #[cfg(feature = "x11")]
+ let builder = builder.with_class(
+ window_config.class.instance.to_owned(),
+ window_config.class.general.to_owned(),
+ );
+
+ #[cfg(feature = "x11")]
+ let builder = match &window_config.gtk_theme_variant {
+ Some(val) => builder.with_gtk_theme_variant(val.clone()),
+ None => builder,
+ };
+
+ builder
+ }
+
+ #[cfg(windows)]
+ pub fn get_platform_window(title: &str, window_config: &WindowConfig) -> WindowBuilder {
+ let icon = glutin::window::Icon::from_resource(IDI_ICON, None);
+
+ WindowBuilder::new()
+ .with_title(title)
+ .with_visible(false)
+ .with_decorations(window_config.decorations != Decorations::None)
+ .with_transparent(true)
+ .with_maximized(window_config.maximized())
+ .with_fullscreen(window_config.fullscreen())
+ .with_window_icon(icon.ok())
+ }
+
+ #[cfg(target_os = "macos")]
+ pub fn get_platform_window(title: &str, window_config: &WindowConfig) -> WindowBuilder {
+ let window = WindowBuilder::new()
+ .with_title(title)
+ .with_visible(false)
+ .with_transparent(true)
+ .with_maximized(window_config.maximized())
+ .with_fullscreen(window_config.fullscreen());
+
+ match window_config.decorations {
+ Decorations::Full => window,
+ Decorations::Transparent => window
+ .with_title_hidden(true)
+ .with_titlebar_transparent(true)
+ .with_fullsize_content_view(true),
+ Decorations::Buttonless => window
+ .with_title_hidden(true)
+ .with_titlebar_buttons_hidden(true)
+ .with_titlebar_transparent(true)
+ .with_fullsize_content_view(true),
+ Decorations::None => window.with_titlebar_hidden(true),
+ }
+ }
+
+ pub fn set_urgent(&self, is_urgent: bool) {
+ let attention = if is_urgent { Some(UserAttentionType::Critical) } else { None };
+
+ self.window().request_user_attention(attention);
+ }
+
+ pub fn set_outer_position(&self, pos: PhysicalPosition<i32>) {
+ self.window().set_outer_position(pos);
+ }
+
+ #[cfg(all(feature = "x11", not(any(target_os = "macos", windows))))]
+ pub fn x11_window_id(&self) -> Option<usize> {
+ self.window().xlib_window().map(|xlib_window| xlib_window as usize)
+ }
+
+ #[cfg(any(not(feature = "x11"), target_os = "macos", windows))]
+ pub fn x11_window_id(&self) -> Option<usize> {
+ None
+ }
+
+ pub fn window_id(&self) -> WindowId {
+ self.window().id()
+ }
+
+ #[cfg(not(any(target_os = "macos", windows)))]
+ pub fn set_maximized(&self, maximized: bool) {
+ self.window().set_maximized(maximized);
+ }
+
+ pub fn set_minimized(&self, minimized: bool) {
+ self.window().set_minimized(minimized);
+ }
+
+ /// Toggle the window's fullscreen state.
+ pub fn toggle_fullscreen(&mut self) {
+ self.set_fullscreen(self.window().fullscreen().is_none());
+ }
+
+ #[cfg(target_os = "macos")]
+ pub fn toggle_simple_fullscreen(&mut self) {
+ self.set_simple_fullscreen(!self.window().simple_fullscreen());
+ }
+
+ pub fn set_fullscreen(&mut self, fullscreen: bool) {
+ if fullscreen {
+ self.window().set_fullscreen(Some(Fullscreen::Borderless(None)));
+ } else {
+ self.window().set_fullscreen(None);
+ }
+ }
+
+ #[cfg(target_os = "macos")]
+ pub fn set_simple_fullscreen(&mut self, simple_fullscreen: bool) {
+ self.window().set_simple_fullscreen(simple_fullscreen);
+ }
+
+ #[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))]
+ pub fn wayland_display(&self) -> Option<*mut std::ffi::c_void> {
+ self.window().wayland_display()
+ }
+
+ #[cfg(not(any(feature = "wayland", target_os = "macos", windows)))]
+ pub fn wayland_display(&self) -> Option<*mut std::ffi::c_void> {
+ None
+ }
+
+ #[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))]
+ pub fn wayland_surface(&self) -> Option<&Attached<WlSurface>> {
+ self.wayland_surface.as_ref()
+ }
+
+ #[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))]
+ pub fn set_wayland_theme(&mut self, colors: &Colors) {
+ self.window().set_wayland_theme(AlacrittyWaylandTheme::new(colors));
+ }
+
+ /// Adjust the IME editor position according to the new location of the cursor.
+ pub fn update_ime_position(&mut self, point: Point, size: &SizeInfo) {
+ let nspot_x = f64::from(size.padding_x() + point.column.0 as f32 * size.cell_width());
+ let nspot_y = f64::from(size.padding_y() + (point.line.0 + 1) as f32 * size.cell_height());
+
+ self.window().set_ime_position(PhysicalPosition::new(nspot_x, nspot_y));
+ }
+
+ pub fn swap_buffers(&self) {
+ self.windowed_context.swap_buffers().expect("swap buffers");
+ }
+
+ pub fn resize(&self, size: PhysicalSize<u32>) {
+ self.windowed_context.resize(size);
+ }
+
+ /// Disable macOS window shadows.
+ ///
+ /// This prevents rendering artifacts from showing up when the window is transparent.
+ #[cfg(target_os = "macos")]
+ pub fn set_has_shadow(&self, has_shadows: bool) {
+ let raw_window = match self.window().raw_window_handle() {
+ RawWindowHandle::MacOS(handle) => handle.ns_window as id,
+ _ => return,
+ };
+
+ let value = if has_shadows { YES } else { NO };
+ unsafe {
+ let _: () = msg_send![raw_window, setHasShadow: value];
+ }
+ }
+
+ fn window(&self) -> &GlutinWindow {
+ self.windowed_context.window()
+ }
+}
+
+#[cfg(all(feature = "x11", not(any(target_os = "macos", windows))))]
+fn x_embed_window(window: &GlutinWindow, parent_id: std::os::raw::c_ulong) {
+ let (xlib_display, xlib_window) = match (window.xlib_display(), window.xlib_window()) {
+ (Some(display), Some(window)) => (display, window),
+ _ => return,
+ };
+
+ let xlib = Xlib::open().expect("get xlib");
+
+ unsafe {
+ let atom = (xlib.XInternAtom)(xlib_display as *mut _, "_XEMBED".as_ptr() as *const _, 0);
+ (xlib.XChangeProperty)(
+ xlib_display as _,
+ xlib_window as _,
+ atom,
+ atom,
+ 32,
+ PropModeReplace,
+ [0, 1].as_ptr(),
+ 2,
+ );
+
+ // Register new error handler.
+ let old_handler = (xlib.XSetErrorHandler)(Some(xembed_error_handler));
+
+ // Check for the existence of the target before attempting reparenting.
+ (xlib.XReparentWindow)(xlib_display as _, xlib_window as _, parent_id, 0, 0);
+
+ // Drain errors and restore original error handler.
+ (xlib.XSync)(xlib_display as _, 0);
+ (xlib.XSetErrorHandler)(old_handler);
+ }
+}
+
+#[cfg(all(feature = "x11", not(any(target_os = "macos", windows))))]
+unsafe extern "C" fn xembed_error_handler(_: *mut XDisplay, _: *mut XErrorEvent) -> i32 {
+ log::error!("Could not embed into specified window.");
+ std::process::exit(1);
+}