aboutsummaryrefslogtreecommitdiff
path: root/alacritty/src/display
diff options
context:
space:
mode:
authorChristian Duerr <contact@christianduerr.com>2021-01-24 21:45:36 +0000
committerGitHub <noreply@github.com>2021-01-24 21:45:36 +0000
commit530de00049c2afcc562d36ccdb3e6afa2fe396a5 (patch)
tree3dabbcef3fc4a2041f9027d82243aa0d70928153 /alacritty/src/display
parent7291702f6b4fff10f2470f084abe0785b95659a0 (diff)
downloadr-alacritty-530de00049c2afcc562d36ccdb3e6afa2fe396a5.tar.gz
r-alacritty-530de00049c2afcc562d36ccdb3e6afa2fe396a5.tar.bz2
r-alacritty-530de00049c2afcc562d36ccdb3e6afa2fe396a5.zip
Move renderable cell transformation to alacritty
This refactors a large chunk of the alacritty_terminal API to expose all data necessary for rendering uniformly through the `renderable_content` call. This also no longer transforms the cells for rendering by a GUI but instead just reports the content from a terminal emulation perspective. The transformation into renderable cells is now done inside the alacritty crate. Since the terminal itself only ever needs to know about modified color RGB values, the configuration for colors was moved to the alacritty UI code.
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);
+}