| // 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::sync::mpsc; |
| |
| use parking_lot::{MutexGuard}; |
| |
| use Rgb; |
| use cli; |
| use config::Config; |
| use font::{self, Rasterize, Metrics}; |
| use meter::Meter; |
| use renderer::{self, GlyphCache, QuadRenderer, Rect}; |
| use selection::Selection; |
| use term::{cell, Term, SizeInfo, RenderableCell}; |
| |
| use window::{self, Size, Pixels, Window, SetInnerSize}; |
| |
| #[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<&::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<(u32, u32)>, |
| tx: mpsc::Sender<(u32, u32)>, |
| meter: Meter, |
| font_size: font::Size, |
| size_info: SizeInfo, |
| last_background_color: Rgb, |
| } |
| |
| /// 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(); |
| |
| // Create the window where Alacritty will be displayed |
| let mut window = Window::new(&options.title, config.window())?; |
| |
| // get window properties for initializing the other subsystems |
| let mut viewport_size = window.inner_size_pixels() |
| .expect("glutin returns window size"); |
| let dpr = window.hidpi_factor(); |
| |
| info!("device_pixel_ratio: {}", dpr); |
| |
| // Create renderer |
| let mut renderer = QuadRenderer::new(config, viewport_size)?; |
| |
| let (glyph_cache, cell_width, cell_height) = |
| Self::new_glyph_cache(&window, &mut renderer, config)?; |
| |
| |
| let dimensions = options.dimensions() |
| .unwrap_or_else(|| config.dimensions()); |
| |
| // Resize window to specified dimensions unless one or both dimensions are 0 |
| if dimensions.columns_u32() > 0 && dimensions.lines_u32() > 0 { |
| let width = cell_width as u32 * dimensions.columns_u32(); |
| let height = cell_height as u32 * dimensions.lines_u32(); |
| |
| let new_viewport_size = Size { |
| width: Pixels(width + 2 * config.padding().x as u32), |
| height: Pixels(height + 2 * config.padding().y as u32), |
| }; |
| |
| window.set_inner_size(&new_viewport_size); |
| renderer.resize(new_viewport_size.width.0 as _, new_viewport_size.height.0 as _); |
| viewport_size = new_viewport_size |
| } |
| |
| info!("Cell Size: ({} x {})", cell_width, cell_height); |
| |
| let size_info = SizeInfo { |
| width: viewport_size.width.0 as f32, |
| height: viewport_size.height.0 as f32, |
| cell_width: cell_width as f32, |
| cell_height: cell_height as f32, |
| padding_x: config.padding().x.floor(), |
| padding_y: config.padding().y.floor(), |
| }; |
| |
| // 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: window, |
| renderer: renderer, |
| glyph_cache: glyph_cache, |
| render_timer: render_timer, |
| tx: tx, |
| rx: rx, |
| meter: Meter::new(), |
| font_size: font::Size::new(0.), |
| size_info: size_info, |
| last_background_color: background_color, |
| }) |
| } |
| |
| fn new_glyph_cache(window : &Window, renderer : &mut QuadRenderer, config: &Config) |
| -> Result<(GlyphCache, f32, f32), Error> |
| { |
| let font = config.font().clone(); |
| let dpr = window.hidpi_factor(); |
| let rasterizer = font::Rasterizer::new(dpr, 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 {}", 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 metrics = glyph_cache.font_metrics(); |
| let cell_width = (metrics.average_advance + f64::from(font.offset().x)) as u32; |
| let cell_height = (metrics.line_height + f64::from(font.offset().y)) as u32; |
| |
| Ok((glyph_cache, cell_width as f32, cell_height as f32)) |
| } |
| |
| pub fn update_glyph_cache(&mut self, config: &Config) { |
| let cache = &mut self.glyph_cache; |
| let size = self.font_size; |
| self.renderer.with_loader(|mut api| { |
| let _ = cache.update_font_size(config.font(), size, &mut api); |
| }); |
| |
| let metrics = cache.font_metrics(); |
| self.size_info.cell_width = ((metrics.average_advance + f64::from(config.font().offset().x)) as f32).floor(); |
| self.size_info.cell_height = ((metrics.line_height + f64::from(config.font().offset().y)) as f32).floor(); |
| } |
| |
| #[inline] |
| pub fn resize_channel(&self) -> mpsc::Sender<(u32, u32)> { |
| 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, |
| items: &mut [&mut OnResize] |
| ) { |
| // 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(sz) = self.rx.try_recv() { |
| new_size = Some(sz); |
| } |
| |
| // Font size modification detected |
| if terminal.font_size != self.font_size { |
| self.font_size = terminal.font_size; |
| self.update_glyph_cache(config); |
| |
| if new_size == None { |
| // Force a resize to refresh things |
| new_size = Some((self.size_info.width as u32, |
| self.size_info.height as u32)); |
| } |
| } |
| |
| // Receive any resize events; only call gl::Viewport on last |
| // available |
| if let Some((w, h)) = new_size.take() { |
| self.size_info.width = w as f32; |
| self.size_info.height = h as f32; |
| |
| let size = &self.size_info; |
| terminal.resize(size); |
| |
| for item in items { |
| item.on_resize(size) |
| } |
| |
| self.window.resize(w, h); |
| self.renderer.resize(w as i32, h as i32); |
| } |
| |
| } |
| |
| /// 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, mut terminal: MutexGuard<Term>, config: &Config, selection: Option<&Selection>) { |
| // 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); |
| } |
| } |
| |
| let size_info = *terminal.size_info(); |
| |
| let background_color = terminal.background_color(); |
| let background_color_changed = background_color != self.last_background_color; |
| self.last_background_color = background_color; |
| |
| { |
| let glyph_cache = &mut self.glyph_cache; |
| let metrics = glyph_cache.font_metrics(); |
| let mut cell_line_rects = Vec::new(); |
| |
| // Draw grid |
| { |
| let _sampler = self.meter.sampler(); |
| |
| // Make a copy of size_info since the closure passed here |
| // borrows terminal mutably |
| // |
| // TODO I wonder if the renderable cells iter could avoid the |
| // mutable borrow |
| let window_focused = self.window.is_focused; |
| self.renderer.with_api(config, &size_info, |mut api| { |
| // Clear screen to update whole background with new color |
| if background_color_changed { |
| api.clear(background_color); |
| } |
| |
| // Store underline/strikeout information beyond current cell |
| let mut last_cell = None; |
| let mut start_underline: Option<RenderableCell> = None; |
| let mut start_strikeout: Option<RenderableCell> = None; |
| |
| // Iterate over all non-empty cells in the grid |
| for cell in terminal.renderable_cells(config, selection, window_focused) { |
| // Check if there is a new underline |
| if let Some(underline) = calculate_cell_line_state( |
| cell, |
| &mut start_underline, |
| &last_cell, |
| &metrics, |
| &size_info, |
| cell::Flags::UNDERLINE, |
| ) { |
| cell_line_rects.push(underline); |
| } |
| |
| // Check if there is a new strikeout |
| if let Some(strikeout) = calculate_cell_line_state( |
| cell, |
| &mut start_strikeout, |
| &last_cell, |
| &metrics, |
| &size_info, |
| cell::Flags::STRIKEOUT, |
| ) { |
| cell_line_rects.push(strikeout); |
| } |
| |
| // Change the last checked cell for underline/strikeout |
| last_cell = Some(cell); |
| |
| // Draw the cell |
| api.render_cell(cell, glyph_cache); |
| } |
| |
| // If underline hasn't been reset, draw until the last cell |
| if let Some(start) = start_underline { |
| cell_line_rects.push( |
| cell_line_rect( |
| &start, |
| &last_cell.unwrap(), |
| &metrics, &size_info, |
| cell::Flags::UNDERLINE |
| ) |
| ); |
| } |
| |
| // If strikeout hasn't been reset, draw until the last cell |
| if let Some(start) = start_strikeout { |
| cell_line_rects.push( |
| cell_line_rect( |
| &start, |
| &last_cell.unwrap(), |
| &metrics, &size_info, |
| cell::Flags::STRIKEOUT |
| ) |
| ); |
| } |
| }); |
| } |
| |
| // Draw rectangles |
| let visual_bell_intensity = terminal.visual_bell.intensity(); |
| self.renderer.draw_rects(config, &size_info, visual_bell_intensity, cell_line_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[..], glyph_cache, color); |
| }); |
| } |
| } |
| |
| // Unlock the terminal mutex; following call to swap_buffers() may block |
| drop(terminal); |
| self.window |
| .swap_buffers() |
| .expect("swap buffers"); |
| |
| // Clear after swap_buffers 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. |
| self.renderer.with_api(config, &size_info, |api| { |
| api.clear(background_color); |
| }); |
| } |
| |
| pub fn get_window_id(&self) -> Option<usize> { |
| self.window.get_window_id() |
| } |
| |
| /// Adjust the XIM editor position according to the new location of the cursor |
| pub fn update_ime_position(&mut self, terminal: &Term) { |
| use index::{Point, Line, Column}; |
| use term::SizeInfo; |
| let Point{line: Line(row), col: Column(col)} = terminal.cursor().point; |
| let SizeInfo{cell_width: cw, |
| cell_height: ch, |
| padding_x: px, |
| padding_y: py, ..} = *terminal.size_info(); |
| let nspot_y = (py + (row + 1) as f32 * ch) as i16; |
| let nspot_x = (px + col as f32 * cw) as i16; |
| self.window().send_xim_spot(nspot_x, nspot_y); |
| } |
| } |
| |
| // Check if the underline/strikeout state has changed |
| // This returns a new rectangle whenever an underline/strikeout ends |
| fn calculate_cell_line_state( |
| cell: RenderableCell, |
| start_cell_line: &mut Option<RenderableCell>, |
| last_cell: &Option<RenderableCell>, |
| metrics: &Metrics, |
| size_info: &SizeInfo, |
| flag: cell::Flags, |
| ) -> Option<(Rect<u32>, Rgb)> { |
| match *start_cell_line { |
| // If line is already started, check for end |
| Some(start) => { |
| // No change in line |
| if cell.line == start.line && cell.flags.contains(flag) && cell.fg == start.fg { |
| return None; |
| } |
| |
| // Check if we need to start a new line |
| *start_cell_line = if cell.flags.contains(flag) { |
| // Start a new line |
| Some(cell) |
| } else { |
| // Disable line |
| None |
| }; |
| |
| return Some(cell_line_rect( |
| &start, |
| &last_cell.unwrap(), |
| metrics, |
| size_info, |
| flag, |
| )); |
| } |
| // Check for new start of line |
| None => if cell.flags.contains(flag) { |
| *start_cell_line = Some(cell); |
| }, |
| } |
| None |
| } |
| |
| // Create a colored rectangle for an underline/strikeout based on two cells |
| fn cell_line_rect( |
| start: &RenderableCell, |
| end: &RenderableCell, |
| metrics: &Metrics, |
| size: &SizeInfo, |
| flag: cell::Flags, |
| ) -> (Rect<u32>, Rgb) { |
| let x = (start.column.0 as f64 * metrics.average_advance) as u32; |
| let end_x = ((end.column.0 + 1) as f64 * metrics.average_advance) as u32; |
| let width = end_x - x; |
| |
| let (y, height) = match flag { |
| cell::Flags::UNDERLINE => { |
| // Calculate the bottom point of the underline |
| let underline_bottom = metrics.line_height as f32 + metrics.descent |
| - metrics.underline_position |
| + metrics.underline_thickness / 2.; |
| |
| // Check if underline is out of bounds |
| let y = if underline_bottom > metrics.line_height as f32 { |
| // Put underline at the bottom of the cell rect |
| ((start.line.0 as f32 + 1.) * metrics.line_height as f32 |
| - metrics.underline_thickness) |
| .round() as u32 |
| } else { |
| // Get the baseline position and offset it down by (-) underline position |
| // then move it up by half the underline thickness |
| ((start.line.0 as f32 + 1.) * metrics.line_height as f32 + metrics.descent |
| - metrics.underline_position |
| - metrics.underline_thickness / 2.) |
| .round() as u32 |
| }; |
| let height = metrics.underline_thickness as u32; |
| (y, height) |
| } |
| cell::Flags::STRIKEOUT => { |
| // Get the baseline position and offset it up by strikeout position |
| // then move it up by half the strikeout thickness |
| let y = ((start.line.0 as f32 + 1.) * metrics.line_height as f32 + metrics.descent |
| - metrics.strikeout_position |
| - metrics.strikeout_thickness / 2.) |
| .round() as u32; |
| let height = metrics.strikeout_thickness as u32; |
| (y, height) |
| } |
| _ => panic!("Invalid flag for cell line drawing specified"), |
| }; |
| |
| let rect = Rect::new( |
| x + size.padding_x as u32, |
| y + size.padding_y as u32, |
| width, |
| height, |
| ); |
| let color = start.fg; |
| |
| (rect, color) |
| } |