diff options
Diffstat (limited to 'alacritty/src/display')
-rw-r--r-- | alacritty/src/display/bell.rs | 122 | ||||
-rw-r--r-- | alacritty/src/display/color.rs | 167 | ||||
-rw-r--r-- | alacritty/src/display/content.rs | 404 | ||||
-rw-r--r-- | alacritty/src/display/cursor.rs | 92 | ||||
-rw-r--r-- | alacritty/src/display/meter.rs | 97 | ||||
-rw-r--r-- | alacritty/src/display/mod.rs | 823 | ||||
-rw-r--r-- | alacritty/src/display/wayland_theme.rs | 81 | ||||
-rw-r--r-- | alacritty/src/display/window.rs | 497 |
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); +} |