summaryrefslogtreecommitdiff
path: root/alacritty_terminal/src/display.rs
diff options
context:
space:
mode:
Diffstat (limited to 'alacritty_terminal/src/display.rs')
-rw-r--r--alacritty_terminal/src/display.rs560
1 files changed, 560 insertions, 0 deletions
diff --git a/alacritty_terminal/src/display.rs b/alacritty_terminal/src/display.rs
new file mode 100644
index 00000000..1d5799f6
--- /dev/null
+++ b/alacritty_terminal/src/display.rs
@@ -0,0 +1,560 @@
+// Copyright 2016 Joe Wilm, The Alacritty Project Contributors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+//! The display subsystem including window management, font rasterization, and
+//! GPU drawing.
+use std::f64;
+use std::sync::mpsc;
+
+use glutin::dpi::{PhysicalPosition, PhysicalSize};
+use glutin::EventsLoop;
+use parking_lot::MutexGuard;
+
+use crate::cli;
+use crate::config::{Config, StartupMode};
+use crate::index::Line;
+use crate::message_bar::Message;
+use crate::meter::Meter;
+use crate::renderer::rects::{Rect, Rects};
+use crate::renderer::{self, GlyphCache, QuadRenderer};
+use crate::sync::FairMutex;
+use crate::term::color::Rgb;
+use crate::term::{RenderableCell, SizeInfo, Term};
+use crate::window::{self, Window};
+use font::{self, Rasterize};
+
+#[derive(Debug)]
+pub enum Error {
+ /// Error with window management
+ Window(window::Error),
+
+ /// Error dealing with fonts
+ Font(font::Error),
+
+ /// Error in renderer
+ Render(renderer::Error),
+}
+
+impl ::std::error::Error for Error {
+ fn cause(&self) -> Option<&dyn (::std::error::Error)> {
+ match *self {
+ Error::Window(ref err) => Some(err),
+ Error::Font(ref err) => Some(err),
+ Error::Render(ref err) => Some(err),
+ }
+ }
+
+ fn description(&self) -> &str {
+ match *self {
+ Error::Window(ref err) => err.description(),
+ Error::Font(ref err) => err.description(),
+ Error::Render(ref err) => err.description(),
+ }
+ }
+}
+
+impl ::std::fmt::Display for Error {
+ fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
+ match *self {
+ Error::Window(ref err) => err.fmt(f),
+ Error::Font(ref err) => err.fmt(f),
+ Error::Render(ref err) => err.fmt(f),
+ }
+ }
+}
+
+impl From<window::Error> for Error {
+ fn from(val: window::Error) -> Error {
+ Error::Window(val)
+ }
+}
+
+impl From<font::Error> for Error {
+ fn from(val: font::Error) -> Error {
+ Error::Font(val)
+ }
+}
+
+impl From<renderer::Error> for Error {
+ fn from(val: renderer::Error) -> Error {
+ Error::Render(val)
+ }
+}
+
+/// The display wraps a window, font rasterizer, and GPU renderer
+pub struct Display {
+ window: Window,
+ renderer: QuadRenderer,
+ glyph_cache: GlyphCache,
+ render_timer: bool,
+ rx: mpsc::Receiver<PhysicalSize>,
+ tx: mpsc::Sender<PhysicalSize>,
+ meter: Meter,
+ font_size: font::Size,
+ size_info: SizeInfo,
+ last_message: Option<Message>,
+}
+
+/// Can wakeup the render loop from other threads
+pub struct Notifier(window::Proxy);
+
+/// Types that are interested in when the display is resized
+pub trait OnResize {
+ fn on_resize(&mut self, size: &SizeInfo);
+}
+
+impl Notifier {
+ pub fn notify(&self) {
+ self.0.wakeup_event_loop();
+ }
+}
+
+impl Display {
+ pub fn notifier(&self) -> Notifier {
+ Notifier(self.window.create_window_proxy())
+ }
+
+ pub fn update_config(&mut self, config: &Config) {
+ self.render_timer = config.render_timer();
+ }
+
+ /// Get size info about the display
+ pub fn size(&self) -> &SizeInfo {
+ &self.size_info
+ }
+
+ pub fn new(config: &Config, options: &cli::Options) -> Result<Display, Error> {
+ // Extract some properties from config
+ let render_timer = config.render_timer();
+
+ // Guess DPR based on first monitor
+ let event_loop = EventsLoop::new();
+ let estimated_dpr =
+ event_loop.get_available_monitors().next().map(|m| m.get_hidpi_factor()).unwrap_or(1.);
+
+ // Guess the target window dimensions
+ let metrics = GlyphCache::static_metrics(config, estimated_dpr as f32)?;
+ let (cell_width, cell_height) = Self::compute_cell_size(config, &metrics);
+ let dimensions =
+ Self::calculate_dimensions(config, options, estimated_dpr, cell_width, cell_height);
+
+ debug!("Estimated DPR: {}", estimated_dpr);
+ debug!("Estimated Cell Size: {} x {}", cell_width, cell_height);
+ debug!("Estimated Dimensions: {:?}", dimensions);
+
+ // Create the window where Alacritty will be displayed
+ let logical = dimensions.map(|d| PhysicalSize::new(d.0, d.1).to_logical(estimated_dpr));
+ let mut window = Window::new(event_loop, &options, config.window(), logical)?;
+
+ let dpr = window.hidpi_factor();
+ info!("Device pixel ratio: {}", dpr);
+
+ // get window properties for initializing the other subsystems
+ let mut viewport_size =
+ window.inner_size_pixels().expect("glutin returns window size").to_physical(dpr);
+
+ // Create renderer
+ let mut renderer = QuadRenderer::new()?;
+
+ let (glyph_cache, cell_width, cell_height) =
+ Self::new_glyph_cache(dpr, &mut renderer, config)?;
+
+ let mut padding_x = f64::from(config.padding().x) * dpr;
+ let mut padding_y = f64::from(config.padding().y) * dpr;
+
+ if let Some((width, height)) =
+ Self::calculate_dimensions(config, options, dpr, cell_width, cell_height)
+ {
+ if dimensions == Some((width, height)) {
+ info!("Estimated DPR correctly, skipping resize");
+ } else {
+ viewport_size = PhysicalSize::new(width, height);
+ window.set_inner_size(viewport_size.to_logical(dpr));
+ }
+ } else if config.window().dynamic_padding() {
+ // Make sure additional padding is spread evenly
+ let cw = f64::from(cell_width);
+ let ch = f64::from(cell_height);
+ padding_x = padding_x + (viewport_size.width - 2. * padding_x) % cw / 2.;
+ padding_y = padding_y + (viewport_size.height - 2. * padding_y) % ch / 2.;
+ }
+
+ padding_x = padding_x.floor();
+ padding_y = padding_y.floor();
+
+ // Update OpenGL projection
+ renderer.resize(viewport_size, padding_x as f32, padding_y as f32);
+
+ info!("Cell Size: {} x {}", cell_width, cell_height);
+ info!("Padding: {} x {}", padding_x, padding_y);
+
+ let size_info = SizeInfo {
+ dpr,
+ width: viewport_size.width as f32,
+ height: viewport_size.height as f32,
+ cell_width: cell_width as f32,
+ cell_height: cell_height as f32,
+ padding_x: padding_x as f32,
+ padding_y: padding_y as f32,
+ };
+
+ // Channel for resize events
+ //
+ // macOS has a callback for getting resize events, the channel is used
+ // to queue resize events until the next draw call. Unfortunately, it
+ // seems that the event loop is blocked until the window is done
+ // resizing. If any drawing were to happen during a resize, it would
+ // need to be in the callback.
+ let (tx, rx) = mpsc::channel();
+
+ // Clear screen
+ let background_color = config.colors().primary.background;
+ renderer.with_api(config, &size_info, |api| {
+ api.clear(background_color);
+ });
+
+ Ok(Display {
+ window,
+ renderer,
+ glyph_cache,
+ render_timer,
+ tx,
+ rx,
+ meter: Meter::new(),
+ font_size: config.font().size(),
+ size_info,
+ last_message: None,
+ })
+ }
+
+ fn calculate_dimensions(
+ config: &Config,
+ options: &cli::Options,
+ dpr: f64,
+ cell_width: f32,
+ cell_height: f32,
+ ) -> Option<(f64, f64)> {
+ let dimensions = options.dimensions().unwrap_or_else(|| config.dimensions());
+
+ if dimensions.columns_u32() == 0
+ || dimensions.lines_u32() == 0
+ || config.window().startup_mode() != StartupMode::Windowed
+ {
+ return None;
+ }
+
+ let padding_x = f64::from(config.padding().x) * dpr;
+ let padding_y = f64::from(config.padding().y) * dpr;
+
+ // Calculate new size based on cols/lines specified in config
+ let grid_width = cell_width as u32 * dimensions.columns_u32();
+ let grid_height = cell_height as u32 * dimensions.lines_u32();
+
+ let width = (f64::from(grid_width) + 2. * padding_x).floor();
+ let height = (f64::from(grid_height) + 2. * padding_y).floor();
+
+ Some((width, height))
+ }
+
+ fn new_glyph_cache(
+ dpr: f64,
+ renderer: &mut QuadRenderer,
+ config: &Config,
+ ) -> Result<(GlyphCache, f32, f32), Error> {
+ let font = config.font().clone();
+ let rasterizer = font::Rasterizer::new(dpr as f32, config.use_thin_strokes())?;
+
+ // Initialize glyph cache
+ let glyph_cache = {
+ info!("Initializing glyph cache...");
+ let init_start = ::std::time::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) = Self::compute_cell_size(config, &glyph_cache.font_metrics());
+
+ Ok((glyph_cache, cw, ch))
+ }
+
+ pub fn update_glyph_cache(&mut self, config: &Config) {
+ let cache = &mut self.glyph_cache;
+ let dpr = self.size_info.dpr;
+ let size = self.font_size;
+
+ self.renderer.with_loader(|mut api| {
+ let _ = cache.update_font_size(config.font(), size, dpr, &mut api);
+ });
+
+ let (cw, ch) = Self::compute_cell_size(config, &cache.font_metrics());
+ self.size_info.cell_width = cw;
+ self.size_info.cell_height = ch;
+ }
+
+ fn compute_cell_size(config: &Config, metrics: &font::Metrics) -> (f32, f32) {
+ let offset_x = f64::from(config.font().offset().x);
+ let offset_y = f64::from(config.font().offset().y);
+ (
+ f32::max(1., ((metrics.average_advance + offset_x) as f32).floor()),
+ f32::max(1., ((metrics.line_height + offset_y) as f32).floor()),
+ )
+ }
+
+ #[inline]
+ pub fn resize_channel(&self) -> mpsc::Sender<PhysicalSize> {
+ self.tx.clone()
+ }
+
+ pub fn window(&mut self) -> &mut Window {
+ &mut self.window
+ }
+
+ /// Process pending resize events
+ pub fn handle_resize(
+ &mut self,
+ terminal: &mut MutexGuard<'_, Term>,
+ config: &Config,
+ pty_resize_handle: &mut dyn OnResize,
+ processor_resize_handle: &mut dyn OnResize,
+ ) {
+ let previous_cols = self.size_info.cols();
+ let previous_lines = self.size_info.lines();
+
+ // Resize events new_size and are handled outside the poll_events
+ // iterator. This has the effect of coalescing multiple resize
+ // events into one.
+ let mut new_size = None;
+
+ // Take most recent resize event, if any
+ while let Ok(size) = self.rx.try_recv() {
+ new_size = Some(size);
+ }
+
+ // Update the DPR
+ let dpr = self.window.hidpi_factor();
+
+ // Font size/DPI factor modification detected
+ let font_changed =
+ terminal.font_size != self.font_size || (dpr - self.size_info.dpr).abs() > f64::EPSILON;
+
+ // Skip resize if nothing changed
+ if let Some(new_size) = new_size {
+ if !font_changed
+ && (new_size.width - f64::from(self.size_info.width)).abs() < f64::EPSILON
+ && (new_size.height - f64::from(self.size_info.height)).abs() < f64::EPSILON
+ {
+ return;
+ }
+ }
+
+ if font_changed || self.last_message != terminal.message_buffer_mut().message() {
+ if new_size == None {
+ // Force a resize to refresh things
+ new_size = Some(PhysicalSize::new(
+ f64::from(self.size_info.width) / self.size_info.dpr * dpr,
+ f64::from(self.size_info.height) / self.size_info.dpr * dpr,
+ ));
+ }
+
+ self.font_size = terminal.font_size;
+ self.last_message = terminal.message_buffer_mut().message();
+ self.size_info.dpr = dpr;
+ }
+
+ if font_changed {
+ self.update_glyph_cache(config);
+ }
+
+ if let Some(psize) = new_size.take() {
+ let width = psize.width as f32;
+ let height = psize.height as f32;
+ let cell_width = self.size_info.cell_width;
+ let cell_height = self.size_info.cell_height;
+
+ self.size_info.width = width;
+ self.size_info.height = height;
+
+ let mut padding_x = f32::from(config.padding().x) * dpr as f32;
+ let mut padding_y = f32::from(config.padding().y) * dpr as f32;
+
+ if config.window().dynamic_padding() {
+ padding_x = padding_x + ((width - 2. * padding_x) % cell_width) / 2.;
+ padding_y = padding_y + ((height - 2. * padding_y) % cell_height) / 2.;
+ }
+
+ self.size_info.padding_x = padding_x.floor();
+ self.size_info.padding_y = padding_y.floor();
+
+ let size = &self.size_info;
+ terminal.resize(size);
+ processor_resize_handle.on_resize(size);
+
+ // Subtract message bar lines for pty size
+ let mut pty_size = *size;
+ if let Some(message) = terminal.message_buffer_mut().message() {
+ pty_size.height -= pty_size.cell_height * message.text(&size).len() as f32;
+ }
+
+ if previous_cols != pty_size.cols() || previous_lines != pty_size.lines() {
+ pty_resize_handle.on_resize(&pty_size);
+ }
+
+ self.window.resize(psize);
+ self.renderer.resize(psize, self.size_info.padding_x, self.size_info.padding_y);
+ }
+ }
+
+ /// 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(&mut self, terminal: &FairMutex<Term>, config: &Config) {
+ let mut terminal = terminal.lock();
+ let size_info = *terminal.size_info();
+ let visual_bell_intensity = terminal.visual_bell.intensity();
+ let background_color = terminal.background_color();
+ let metrics = self.glyph_cache.font_metrics();
+
+ let window_focused = self.window.is_focused;
+ let grid_cells: Vec<RenderableCell> =
+ terminal.renderable_cells(config, window_focused, metrics).collect();
+
+ // Get message from terminal to ignore modifications after lock is dropped
+ let message_buffer = terminal.message_buffer_mut().message();
+
+ // Clear dirty flag
+ terminal.dirty = !terminal.visual_bell.completed();
+
+ if let Some(title) = terminal.get_next_title() {
+ self.window.set_title(&title);
+ }
+
+ if let Some(mouse_cursor) = terminal.get_next_mouse_cursor() {
+ self.window.set_mouse_cursor(mouse_cursor);
+ }
+
+ if let Some(is_urgent) = terminal.next_is_urgent.take() {
+ // We don't need to set the urgent flag if we already have the
+ // user's attention.
+ if !is_urgent || !self.window.is_focused {
+ self.window.set_urgent(is_urgent);
+ }
+ }
+
+ // Clear when terminal mutex isn't held. Mesa for
+ // some reason takes a long time to call glClear(). The driver descends
+ // into xcb_connect_to_fd() which ends up calling __poll_nocancel()
+ // which blocks for a while.
+ //
+ // By keeping this outside of the critical region, the Mesa bug is
+ // worked around to some extent. Since this doesn't actually address the
+ // issue of glClear being slow, less time is available for input
+ // handling and rendering.
+ drop(terminal);
+
+ self.renderer.with_api(config, &size_info, |api| {
+ api.clear(background_color);
+ });
+
+ {
+ let glyph_cache = &mut self.glyph_cache;
+ let mut rects = Rects::new(&metrics, &size_info);
+
+ // Draw grid
+ {
+ let _sampler = self.meter.sampler();
+
+ self.renderer.with_api(config, &size_info, |mut api| {
+ // Iterate over all non-empty cells in the grid
+ for cell in grid_cells {
+ // Update underline/strikeout
+ rects.update_lines(&size_info, &cell);
+
+ // Draw the cell
+ api.render_cell(cell, glyph_cache);
+ }
+ });
+ }
+
+ if let Some(message) = message_buffer {
+ let text = message.text(&size_info);
+
+ // Create a new rectangle for the background
+ let start_line = size_info.lines().0 - text.len();
+ let y = size_info.padding_y + size_info.cell_height * start_line as f32;
+ let rect = Rect::new(0., y, size_info.width, size_info.height - y);
+ rects.push(rect, message.color());
+
+ // Draw rectangles including the new background
+ self.renderer.draw_rects(config, &size_info, visual_bell_intensity, rects);
+
+ // Relay messages to the user
+ let mut offset = 1;
+ for message_text in text.iter().rev() {
+ self.renderer.with_api(config, &size_info, |mut api| {
+ api.render_string(
+ &message_text,
+ Line(size_info.lines().saturating_sub(offset)),
+ glyph_cache,
+ None,
+ );
+ });
+ offset += 1;
+ }
+ } else {
+ // Draw rectangles
+ self.renderer.draw_rects(config, &size_info, visual_bell_intensity, rects);
+ }
+
+ // Draw render timer
+ if self.render_timer {
+ let timing = format!("{:.3} usec", self.meter.average());
+ let color = Rgb { r: 0xd5, g: 0x4e, b: 0x53 };
+ self.renderer.with_api(config, &size_info, |mut api| {
+ api.render_string(&timing[..], size_info.lines() - 2, glyph_cache, Some(color));
+ });
+ }
+ }
+
+ self.window.swap_buffers().expect("swap buffers");
+ }
+
+ pub fn get_window_id(&self) -> Option<usize> {
+ self.window.get_window_id()
+ }
+
+ /// Adjust the IME editor position according to the new location of the cursor
+ pub fn update_ime_position(&mut self, terminal: &Term) {
+ let point = terminal.cursor().point;
+ let SizeInfo { cell_width: cw, cell_height: ch, padding_x: px, padding_y: py, .. } =
+ *terminal.size_info();
+
+ let dpr = self.window().hidpi_factor();
+ let nspot_x = f64::from(px + point.col.0 as f32 * cw);
+ let nspot_y = f64::from(py + (point.line.0 + 1) as f32 * ch);
+
+ self.window().set_ime_spot(PhysicalPosition::from((nspot_x, nspot_y)).to_logical(dpr));
+ }
+}