diff options
Diffstat (limited to 'alacritty/src/window_context.rs')
-rw-r--r-- | alacritty/src/window_context.rs | 374 |
1 files changed, 374 insertions, 0 deletions
diff --git a/alacritty/src/window_context.rs b/alacritty/src/window_context.rs new file mode 100644 index 00000000..caa69851 --- /dev/null +++ b/alacritty/src/window_context.rs @@ -0,0 +1,374 @@ +//! Terminal window context. + +use std::error::Error; +use std::fs::File; +use std::io::Write; +use std::mem; +#[cfg(not(any(target_os = "macos", windows)))] +use std::sync::atomic::Ordering; +use std::sync::Arc; + +use crossfont::Size; +use glutin::event::{Event as GlutinEvent, ModifiersState, WindowEvent}; +use glutin::event_loop::{EventLoopProxy, EventLoopWindowTarget}; +use glutin::window::WindowId; +use log::info; +use serde_json as json; +#[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))] +use wayland_client::EventQueue; + +use alacritty_terminal::event::Event as TerminalEvent; +use alacritty_terminal::event_loop::{EventLoop as PtyEventLoop, Msg, Notifier}; +use alacritty_terminal::grid::{Dimensions, Scroll}; +use alacritty_terminal::index::Direction; +use alacritty_terminal::sync::FairMutex; +use alacritty_terminal::term::{Term, TermMode}; +use alacritty_terminal::tty; + +use crate::clipboard::Clipboard; +use crate::config::Config; +use crate::display::Display; +use crate::event::{ActionContext, Event, EventProxy, EventType, Mouse, SearchState}; +use crate::input; +use crate::message_bar::MessageBuffer; +use crate::scheduler::Scheduler; + +/// Event context for one individual Alacritty window. +pub struct WindowContext { + pub message_buffer: MessageBuffer, + pub display: Display, + event_queue: Vec<GlutinEvent<'static, Event>>, + terminal: Arc<FairMutex<Term<EventProxy>>>, + modifiers: ModifiersState, + search_state: SearchState, + received_count: usize, + suppress_chars: bool, + notifier: Notifier, + font_size: Size, + mouse: Mouse, + dirty: bool, +} + +impl WindowContext { + /// Create a new terminal window context. + pub fn new( + config: &Config, + window_event_loop: &EventLoopWindowTarget<Event>, + proxy: EventLoopProxy<Event>, + #[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))] + wayland_event_queue: Option<&EventQueue>, + ) -> Result<Self, Box<dyn Error>> { + // Create a display. + // + // The display manages a window and can draw the terminal. + let display = Display::new( + config, + window_event_loop, + #[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))] + wayland_event_queue, + )?; + + info!( + "PTY dimensions: {:?} x {:?}", + display.size_info.screen_lines(), + display.size_info.columns() + ); + + let event_proxy = EventProxy::new(proxy, display.window.id()); + + // Create the terminal. + // + // This object contains all of the state about what's being displayed. It's + // wrapped in a clonable mutex since both the I/O loop and display need to + // access it. + let terminal = Term::new(config, display.size_info, event_proxy.clone()); + let terminal = Arc::new(FairMutex::new(terminal)); + + // Create the PTY. + // + // The PTY forks a process to run the shell on the slave side of the + // pseudoterminal. A file descriptor for the master side is retained for + // reading/writing to the shell. + let pty = tty::new(config, &display.size_info, display.window.x11_window_id()); + + // Create the pseudoterminal I/O loop. + // + // PTY I/O is ran on another thread as to not occupy cycles used by the + // renderer and input processing. Note that access to the terminal state is + // synchronized since the I/O loop updates the state, and the display + // consumes it periodically. + let event_loop = PtyEventLoop::new( + Arc::clone(&terminal), + event_proxy.clone(), + pty, + config.hold, + config.ui_config.debug.ref_test, + ); + + // The event loop channel allows write requests from the event processor + // to be sent to the pty loop and ultimately written to the pty. + let loop_tx = event_loop.channel(); + + // Kick off the I/O thread. + let _io_thread = event_loop.spawn(); + + // Start cursor blinking, in case `Focused` isn't sent on startup. + if config.cursor.style().blinking { + event_proxy.send_event(TerminalEvent::CursorBlinkingChange.into()); + } + + // Create context for the Alacritty window. + Ok(WindowContext { + font_size: config.ui_config.font.size(), + notifier: Notifier(loop_tx), + terminal, + display, + suppress_chars: Default::default(), + message_buffer: Default::default(), + received_count: Default::default(), + search_state: Default::default(), + event_queue: Default::default(), + modifiers: Default::default(), + mouse: Default::default(), + dirty: Default::default(), + }) + } + + /// Update the terminal window to the latest config. + pub fn update_config(&mut self, old_config: &Config, config: &Config) { + self.display.update_config(config); + self.terminal.lock().update_config(config); + + // Reload cursor if its thickness has changed. + if (old_config.cursor.thickness() - config.cursor.thickness()).abs() > f32::EPSILON { + self.display.pending_update.set_cursor_dirty(); + } + + if old_config.ui_config.font != config.ui_config.font { + // Do not update font size if it has been changed at runtime. + if self.font_size == old_config.ui_config.font.size() { + self.font_size = config.ui_config.font.size(); + } + + let font = config.ui_config.font.clone().with_size(self.font_size); + self.display.pending_update.set_font(font); + } + + // Update display if padding options were changed. + let window_config = &old_config.ui_config.window; + if window_config.padding(1.) != config.ui_config.window.padding(1.) + || window_config.dynamic_padding != config.ui_config.window.dynamic_padding + { + self.display.pending_update.dirty = true; + } + + // Live title reload. + if !config.ui_config.window.dynamic_title + || old_config.ui_config.window.title != config.ui_config.window.title + { + self.display.window.set_title(&config.ui_config.window.title); + } + + #[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))] + self.display.window.set_wayland_theme(&config.ui_config.colors); + + // 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")] + self.display.window.set_has_shadow(config.ui_config.window_opacity() >= 1.0); + + // Update hint keys. + self.display.hint_state.update_alphabet(config.ui_config.hints.alphabet()); + + // Update cursor blinking. + let event = Event::new(TerminalEvent::CursorBlinkingChange.into(), None); + self.event_queue.push(event.into()); + + self.dirty = true; + } + + /// Process events for this terminal window. + pub fn handle_event( + &mut self, + event_loop: &EventLoopWindowTarget<Event>, + event_proxy: &EventLoopProxy<Event>, + config: &mut Config, + clipboard: &mut Clipboard, + scheduler: &mut Scheduler, + event: GlutinEvent<'_, Event>, + ) { + match event { + // Skip further event handling with no staged updates. + GlutinEvent::RedrawEventsCleared if self.event_queue.is_empty() && !self.dirty => { + return; + }, + // Continue to process all pending events. + GlutinEvent::RedrawEventsCleared => (), + // Remap DPR change event to remove the lifetime. + GlutinEvent::WindowEvent { + event: WindowEvent::ScaleFactorChanged { scale_factor, new_inner_size }, + window_id, + } => { + let size = (new_inner_size.width, new_inner_size.height); + let event = Event::new(EventType::DprChanged(scale_factor, size), window_id); + self.event_queue.push(event.into()); + return; + }, + // Transmute to extend lifetime, which exists only for `ScaleFactorChanged` event. + // Since we remap that event to remove the lifetime, this is safe. + event => unsafe { + self.event_queue.push(mem::transmute(event)); + return; + }, + } + + let mut terminal = self.terminal.lock(); + + let old_is_searching = self.search_state.history_index.is_some(); + + let context = ActionContext { + message_buffer: &mut self.message_buffer, + received_count: &mut self.received_count, + suppress_chars: &mut self.suppress_chars, + search_state: &mut self.search_state, + modifiers: &mut self.modifiers, + font_size: &mut self.font_size, + notifier: &mut self.notifier, + display: &mut self.display, + mouse: &mut self.mouse, + dirty: &mut self.dirty, + terminal: &mut terminal, + event_proxy, + event_loop, + clipboard, + scheduler, + config, + }; + let mut processor = input::Processor::new(context); + + for event in self.event_queue.drain(..) { + processor.handle_event(event); + } + + // Process DisplayUpdate events. + if self.display.pending_update.dirty { + Self::submit_display_update( + &mut terminal, + &mut self.display, + &mut self.notifier, + &self.message_buffer, + &self.search_state, + old_is_searching, + config, + ); + } + + if self.dirty || self.mouse.hint_highlight_dirty { + self.dirty |= self.display.update_highlighted_hints( + &terminal, + config, + &self.mouse, + self.modifiers, + ); + self.mouse.hint_highlight_dirty = false; + } + + // Skip rendering on Wayland until we get frame event from compositor. + #[cfg(not(any(target_os = "macos", windows)))] + if !self.display.is_x11 && !self.display.window.should_draw.load(Ordering::Relaxed) { + return; + } + + if self.dirty { + self.dirty = false; + + // Request immediate re-draw if visual bell animation is not finished yet. + if !self.display.visual_bell.completed() { + self.display.window.request_redraw(); + } + + // Redraw screen. + self.display.draw(terminal, &self.message_buffer, config, &self.search_state); + } + } + + /// ID of this terminal context. + pub fn id(&self) -> WindowId { + self.display.window.id() + } + + /// Write the ref test results to the disk. + pub fn write_ref_test_results(&self) { + // Dump grid state. + let mut grid = self.terminal.lock().grid().clone(); + grid.initialize_all(); + grid.truncate(); + + let serialized_grid = json::to_string(&grid).expect("serialize grid"); + + let serialized_size = json::to_string(&self.display.size_info).expect("serialize size"); + + let serialized_config = format!("{{\"history_size\":{}}}", grid.history_size()); + + File::create("./grid.json") + .and_then(|mut f| f.write_all(serialized_grid.as_bytes())) + .expect("write grid.json"); + + File::create("./size.json") + .and_then(|mut f| f.write_all(serialized_size.as_bytes())) + .expect("write size.json"); + + File::create("./config.json") + .and_then(|mut f| f.write_all(serialized_config.as_bytes())) + .expect("write config.json"); + } + + /// Submit the pending changes to the `Display`. + fn submit_display_update( + terminal: &mut Term<EventProxy>, + display: &mut Display, + notifier: &mut Notifier, + message_buffer: &MessageBuffer, + search_state: &SearchState, + old_is_searching: bool, + config: &Config, + ) { + // Compute cursor positions before resize. + let num_lines = terminal.screen_lines(); + let cursor_at_bottom = terminal.grid().cursor.point.line + 1 == num_lines; + let origin_at_bottom = if terminal.mode().contains(TermMode::VI) { + terminal.vi_mode_cursor.point.line == num_lines - 1 + } else { + search_state.direction == Direction::Left + }; + + display.handle_update( + terminal, + notifier, + message_buffer, + search_state.history_index.is_some(), + config, + ); + + let new_is_searching = search_state.history_index.is_some(); + if !old_is_searching && new_is_searching { + // Scroll on search start to make sure origin is visible with minimal viewport motion. + let display_offset = terminal.grid().display_offset(); + if display_offset == 0 && cursor_at_bottom && !origin_at_bottom { + terminal.scroll_display(Scroll::Delta(1)); + } else if display_offset != 0 && origin_at_bottom { + terminal.scroll_display(Scroll::Delta(-1)); + } + } + } +} + +impl Drop for WindowContext { + fn drop(&mut self) { + // Shutdown the terminal's PTY. + let _ = self.notifier.0.send(Msg::Shutdown); + } +} |