aboutsummaryrefslogtreecommitdiff
path: root/alacritty/src/event.rs
diff options
context:
space:
mode:
Diffstat (limited to 'alacritty/src/event.rs')
-rw-r--r--alacritty/src/event.rs831
1 files changed, 340 insertions, 491 deletions
diff --git a/alacritty/src/event.rs b/alacritty/src/event.rs
index 8e8fac08..09e74a9d 100644
--- a/alacritty/src/event.rs
+++ b/alacritty/src/event.rs
@@ -2,16 +2,12 @@
use std::borrow::Cow;
use std::cmp::{max, min};
-use std::collections::VecDeque;
+use std::collections::{HashMap, VecDeque};
+use std::error::Error;
use std::fmt::Debug;
#[cfg(not(any(target_os = "macos", windows)))]
use std::fs;
-use std::fs::File;
-use std::io::Write;
-use std::path::{Path, PathBuf};
-#[cfg(not(any(target_os = "macos", windows)))]
-use std::sync::atomic::Ordering;
-use std::sync::Arc;
+use std::path::PathBuf;
use std::time::{Duration, Instant};
use std::{env, f32, mem};
@@ -21,35 +17,38 @@ use glutin::event_loop::{ControlFlow, EventLoop, EventLoopProxy, EventLoopWindow
use glutin::platform::run_return::EventLoopExtRunReturn;
#[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))]
use glutin::platform::unix::EventLoopWindowTargetExtUnix;
-use log::info;
-use serde_json as json;
+use glutin::window::WindowId;
+use log::{error, info};
+#[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))]
+use wayland_client::{Display as WaylandDisplay, EventQueue};
use crossfont::{self, Size};
use alacritty_terminal::config::LOG_TARGET_CONFIG;
-use alacritty_terminal::event::{Event as TerminalEvent, EventListener, Notify, OnResize};
+use alacritty_terminal::event::{Event as TerminalEvent, EventListener, Notify};
+use alacritty_terminal::event_loop::Notifier;
use alacritty_terminal::grid::{Dimensions, Scroll};
use alacritty_terminal::index::{Boundary, Column, Direction, Line, Point, Side};
use alacritty_terminal::selection::{Selection, SelectionType};
-use alacritty_terminal::sync::FairMutex;
use alacritty_terminal::term::search::{Match, RegexSearch};
use alacritty_terminal::term::{ClipboardType, SizeInfo, Term, TermMode};
#[cfg(not(windows))]
use alacritty_terminal::tty;
-use crate::cli::Options as CLIOptions;
+use crate::cli::Options as CliOptions;
use crate::clipboard::Clipboard;
use crate::config::ui_config::{HintAction, HintInternalAction};
use crate::config::{self, Config};
use crate::daemon::start_daemon;
use crate::display::hint::HintMatch;
use crate::display::window::Window;
-use crate::display::{self, Display, DisplayUpdate};
+use crate::display::{self, Display};
use crate::input::{self, ActionContext as _, FONT_SIZE_STEP};
#[cfg(target_os = "macos")]
use crate::macos;
use crate::message_bar::{Message, MessageBuffer};
-use crate::scheduler::{Scheduler, TimerId};
+use crate::scheduler::{Scheduler, TimerId, Topic};
+use crate::window_context::WindowContext;
/// Duration after the last user input until an unlimited search is performed.
pub const TYPING_SEARCH_DELAY: Duration = Duration::from_millis(500);
@@ -60,16 +59,20 @@ const MAX_SEARCH_WHILE_TYPING: Option<usize> = Some(1000);
/// Maximum number of search terms stored in the history.
const MAX_SEARCH_HISTORY_SIZE: usize = 255;
-/// Events dispatched through the UI event loop.
+/// Alacritty events.
#[derive(Debug, Clone)]
-pub enum Event {
- Terminal(TerminalEvent),
- DprChanged(f64, (u32, u32)),
- Scroll(Scroll),
- ConfigReload(PathBuf),
- Message(Message),
- BlinkCursor,
- SearchNext,
+pub struct Event {
+ /// Limit event to a specific window.
+ window_id: Option<WindowId>,
+
+ /// Event payload.
+ payload: EventType,
+}
+
+impl Event {
+ pub fn new<I: Into<Option<WindowId>>>(payload: EventType, window_id: I) -> Self {
+ Self { window_id: window_id.into(), payload }
+ }
}
impl From<Event> for GlutinEvent<'_, Event> {
@@ -78,16 +81,32 @@ impl From<Event> for GlutinEvent<'_, Event> {
}
}
-impl From<TerminalEvent> for Event {
+/// Alacritty events.
+#[derive(Debug, Clone)]
+pub enum EventType {
+ DprChanged(f64, (u32, u32)),
+ Terminal(TerminalEvent),
+ ConfigReload(PathBuf),
+ Message(Message),
+ Scroll(Scroll),
+ CreateWindow,
+ BlinkCursor,
+ SearchNext,
+}
+
+impl From<TerminalEvent> for EventType {
fn from(event: TerminalEvent) -> Self {
- Event::Terminal(event)
+ Self::Terminal(event)
}
}
/// Regex search state.
pub struct SearchState {
/// Search direction.
- direction: Direction,
+ pub direction: Direction,
+
+ /// Current position in the search history.
+ pub history_index: Option<usize>,
/// Change in display offset since the beginning of the search.
display_offset_delta: i32,
@@ -106,9 +125,6 @@ pub struct SearchState {
/// in history which is currently being previewed.
history: VecDeque<String>,
- /// Current position in the search history.
- history_index: Option<usize>,
-
/// Compiled search automatons.
dfas: Option<RegexSearch>,
}
@@ -164,14 +180,13 @@ pub struct ActionContext<'a, N, T> {
pub modifiers: &'a mut ModifiersState,
pub display: &'a mut Display,
pub message_buffer: &'a mut MessageBuffer,
- pub display_update_pending: &'a mut DisplayUpdate,
pub config: &'a mut Config,
pub event_loop: &'a EventLoopWindowTarget<Event>,
+ pub event_proxy: &'a EventLoopProxy<Event>,
pub scheduler: &'a mut Scheduler,
pub search_state: &'a mut SearchState,
- cli_options: &'a CLIOptions,
- font_size: &'a mut Size,
- dirty: &'a mut bool,
+ pub font_size: &'a mut Size,
+ pub dirty: &'a mut bool,
}
impl<'a, N: Notify + 'a, T: EventListener> input::ActionContext<T> for ActionContext<'a, N, T> {
@@ -380,23 +395,27 @@ impl<'a, N: Notify + 'a, T: EventListener> input::ActionContext<T> for ActionCon
start_daemon(&alacritty, &args);
}
+ fn create_new_window(&mut self) {
+ let _ = self.event_proxy.send_event(Event::new(EventType::CreateWindow, None));
+ }
+
fn change_font_size(&mut self, delta: f32) {
*self.font_size = max(*self.font_size + delta, Size::new(FONT_SIZE_STEP));
let font = self.config.ui_config.font.clone().with_size(*self.font_size);
- self.display_update_pending.set_font(font);
+ self.display.pending_update.set_font(font);
*self.dirty = true;
}
fn reset_font_size(&mut self) {
*self.font_size = self.config.ui_config.font.size();
- self.display_update_pending.set_font(self.config.ui_config.font.clone());
+ self.display.pending_update.set_font(self.config.ui_config.font.clone());
*self.dirty = true;
}
#[inline]
fn pop_message(&mut self) {
if !self.message_buffer.is_empty() {
- self.display_update_pending.dirty = true;
+ self.display.pending_update.dirty = true;
self.message_buffer.pop();
*self.dirty = true;
}
@@ -433,7 +452,7 @@ impl<'a, N: Notify + 'a, T: EventListener> input::ActionContext<T> for ActionCon
};
}
- self.display_update_pending.dirty = true;
+ self.display.pending_update.dirty = true;
*self.dirty = true;
}
@@ -446,7 +465,8 @@ impl<'a, N: Notify + 'a, T: EventListener> input::ActionContext<T> for ActionCon
}
// Force unlimited search if the previous one was interrupted.
- if self.scheduler.scheduled(TimerId::DelayedSearch) {
+ let timer_id = TimerId::new(Topic::DelayedSearch, self.display.window.id());
+ if self.scheduler.scheduled(timer_id) {
self.goto_match(None);
}
@@ -610,9 +630,10 @@ impl<'a, N: Notify + 'a, T: EventListener> input::ActionContext<T> for ActionCon
#[inline]
fn on_typing_start(&mut self) {
// Disable cursor blinking.
- let blink_interval = self.config.cursor.blink_interval();
- if let Some(timer) = self.scheduler.get_mut(TimerId::BlinkCursor) {
- timer.deadline = Instant::now() + Duration::from_millis(blink_interval);
+ let timer_id = TimerId::new(Topic::BlinkCursor, self.display.window.id());
+ if let Some(timer) = self.scheduler.unschedule(timer_id) {
+ let interval = Duration::from_millis(self.config.cursor.blink_interval());
+ self.scheduler.schedule(timer.event, interval, true, timer.id);
self.display.cursor_hidden = false;
*self.dirty = true;
}
@@ -795,7 +816,8 @@ impl<'a, N: Notify + 'a, T: EventListener> ActionContext<'a, N, T> {
/// Reset terminal to the state before search was started.
fn search_reset_state(&mut self) {
// Unschedule pending timers.
- self.scheduler.unschedule(TimerId::DelayedSearch);
+ let timer_id = TimerId::new(Topic::DelayedSearch, self.display.window.id());
+ self.scheduler.unschedule(timer_id);
// Clear focused match.
self.search_state.focused_match = None;
@@ -849,19 +871,17 @@ impl<'a, N: Notify + 'a, T: EventListener> ActionContext<'a, N, T> {
self.search_state.display_offset_delta += old_offset - display_offset as i32;
// Since we found a result, we require no delayed re-search.
- self.scheduler.unschedule(TimerId::DelayedSearch);
+ let timer_id = TimerId::new(Topic::DelayedSearch, self.display.window.id());
+ self.scheduler.unschedule(timer_id);
},
// Reset viewport only when we know there is no match, to prevent unnecessary jumping.
None if limit.is_none() => self.search_reset_state(),
None => {
// Schedule delayed search if we ran into our search limit.
- if !self.scheduler.scheduled(TimerId::DelayedSearch) {
- self.scheduler.schedule(
- Event::SearchNext.into(),
- TYPING_SEARCH_DELAY,
- false,
- TimerId::DelayedSearch,
- );
+ let timer_id = TimerId::new(Topic::DelayedSearch, self.display.window.id());
+ if !self.scheduler.scheduled(timer_id) {
+ let event = Event::new(EventType::SearchNext, self.display.window.id());
+ self.scheduler.schedule(event, TYPING_SEARCH_DELAY, false, timer_id);
}
// Clear focused match.
@@ -874,7 +894,7 @@ impl<'a, N: Notify + 'a, T: EventListener> ActionContext<'a, N, T> {
/// Cleanup the search state.
fn exit_search(&mut self) {
- self.display_update_pending.dirty = true;
+ self.display.pending_update.dirty = true;
self.search_state.history_index = None;
*self.dirty = true;
@@ -895,14 +915,12 @@ impl<'a, N: Notify + 'a, T: EventListener> ActionContext<'a, N, T> {
let blinking = cursor_style.blinking_override().unwrap_or(terminal_blinking);
// Update cursor blinking state.
- self.scheduler.unschedule(TimerId::BlinkCursor);
+ let timer_id = TimerId::new(Topic::BlinkCursor, self.display.window.id());
+ self.scheduler.unschedule(timer_id);
if blinking && self.terminal.is_focused {
- self.scheduler.schedule(
- GlutinEvent::UserEvent(Event::BlinkCursor),
- Duration::from_millis(self.config.cursor.blink_interval()),
- true,
- TimerId::BlinkCursor,
- )
+ let event = Event::new(EventType::BlinkCursor, self.display.window.id());
+ let interval = Duration::from_millis(self.config.cursor.blink_interval());
+ self.scheduler.schedule(event, interval, true, timer_id);
} else {
self.display.cursor_hidden = false;
*self.dirty = true;
@@ -975,305 +993,88 @@ impl Mouse {
}
}
-/// The event processor.
-///
-/// Stores some state from received events and dispatches actions when they are
-/// triggered.
-pub struct Processor<N> {
- notifier: N,
- mouse: Mouse,
- received_count: usize,
- suppress_chars: bool,
- modifiers: ModifiersState,
- config: Config,
- message_buffer: MessageBuffer,
- display: Display,
- font_size: Size,
- event_queue: Vec<GlutinEvent<'static, Event>>,
- search_state: SearchState,
- cli_options: CLIOptions,
- dirty: bool,
-}
-
-impl<N: Notify + OnResize> Processor<N> {
- /// Create a new event processor.
- ///
- /// Takes a writer which is expected to be hooked up to the write end of a PTY.
- pub fn new(
- notifier: N,
- message_buffer: MessageBuffer,
- config: Config,
- display: Display,
- cli_options: CLIOptions,
- ) -> Processor<N> {
- Processor {
- font_size: config.ui_config.font.size(),
- message_buffer,
- cli_options,
- notifier,
- display,
- config,
- received_count: Default::default(),
- suppress_chars: Default::default(),
- search_state: Default::default(),
- event_queue: Default::default(),
- modifiers: Default::default(),
- mouse: Default::default(),
- dirty: Default::default(),
- }
- }
-
- /// Return `true` if `event_queue` is empty, `false` otherwise.
- #[inline]
- #[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))]
- fn event_queue_empty(&mut self) -> bool {
- let wayland_event_queue = match self.display.wayland_event_queue.as_mut() {
- Some(wayland_event_queue) => wayland_event_queue,
- // Since frame callbacks do not exist on X11, just check for event queue.
- None => return self.event_queue.is_empty(),
- };
-
- // Check for pending frame callbacks on Wayland.
- let events_dispatched = wayland_event_queue
- .dispatch_pending(&mut (), |_, _, _| {})
- .expect("failed to dispatch event queue");
-
- self.event_queue.is_empty() && events_dispatched == 0
- }
-
- /// Return `true` if `event_queue` is empty, `false` otherwise.
- #[inline]
- #[cfg(any(not(feature = "wayland"), target_os = "macos", windows))]
- fn event_queue_empty(&mut self) -> bool {
- self.event_queue.is_empty()
- }
-
- /// Run the event loop.
- pub fn run<T>(&mut self, terminal: Arc<FairMutex<Term<T>>>, mut event_loop: EventLoop<Event>)
- where
- T: EventListener,
- {
- let mut scheduler = Scheduler::new();
-
- // Start the initial cursor blinking timer.
- if self.config.cursor.style().blinking {
- let event: Event = TerminalEvent::CursorBlinkingChange(true).into();
- self.event_queue.push(event.into());
- }
-
- // NOTE: Since this takes a pointer to the winit event loop, it MUST be dropped first.
- #[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))]
- let mut clipboard = unsafe { Clipboard::new(event_loop.wayland_display()) };
- #[cfg(any(not(feature = "wayland"), target_os = "macos", windows))]
- let mut clipboard = Clipboard::new();
-
- event_loop.run_return(|event, event_loop, control_flow| {
- if self.config.ui_config.debug.print_events {
- info!("glutin event: {:?}", event);
- }
-
- // Ignore all events we do not care about.
- if Self::skip_event(&event) {
- return;
- }
-
- match event {
- // Check for shutdown.
- GlutinEvent::UserEvent(Event::Terminal(TerminalEvent::Exit)) => {
- *control_flow = ControlFlow::Exit;
- return;
- },
- // Process events.
- GlutinEvent::RedrawEventsCleared => {
- *control_flow = match scheduler.update(&mut self.event_queue) {
- Some(instant) => ControlFlow::WaitUntil(instant),
- None => ControlFlow::Wait,
- };
-
- if self.event_queue_empty() {
- return;
- }
- },
- // Remap DPR change event to remove lifetime.
- GlutinEvent::WindowEvent {
- event: WindowEvent::ScaleFactorChanged { scale_factor, new_inner_size },
- ..
- } => {
- *control_flow = ControlFlow::Poll;
- let size = (new_inner_size.width, new_inner_size.height);
- self.event_queue.push(Event::DprChanged(scale_factor, size).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 {
- *control_flow = ControlFlow::Poll;
- self.event_queue.push(mem::transmute(event));
- return;
- },
- }
-
- let mut terminal = terminal.lock();
-
- let mut display_update_pending = DisplayUpdate::default();
- let old_is_searching = self.search_state.history_index.is_some();
-
- let context = ActionContext {
- terminal: &mut terminal,
- notifier: &mut self.notifier,
- mouse: &mut self.mouse,
- clipboard: &mut clipboard,
- received_count: &mut self.received_count,
- suppress_chars: &mut self.suppress_chars,
- modifiers: &mut self.modifiers,
- message_buffer: &mut self.message_buffer,
- display_update_pending: &mut display_update_pending,
- display: &mut self.display,
- font_size: &mut self.font_size,
- config: &mut self.config,
- scheduler: &mut scheduler,
- search_state: &mut self.search_state,
- cli_options: &self.cli_options,
- dirty: &mut self.dirty,
- event_loop,
- };
- let mut processor = input::Processor::new(context);
-
- for event in self.event_queue.drain(..) {
- Processor::handle_event(event, &mut processor);
- }
-
- // Process DisplayUpdate events.
- if display_update_pending.dirty {
- self.submit_display_update(&mut terminal, old_is_searching, display_update_pending);
- }
-
- // 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.mouse.hint_highlight_dirty {
- self.dirty |= self.display.update_highlighted_hints(
- &terminal,
- &self.config,
- &self.mouse,
- self.modifiers,
- );
- self.mouse.hint_highlight_dirty = false;
- }
-
- if self.dirty {
- self.dirty = false;
-
- // Request immediate re-draw if visual bell animation is not finished yet.
- if !self.display.visual_bell.completed() {
- let event: Event = TerminalEvent::Wakeup.into();
- self.event_queue.push(event.into());
-
- *control_flow = ControlFlow::Poll;
- }
-
- // Redraw screen.
- self.display.draw(terminal, &self.message_buffer, &self.config, &self.search_state);
- }
- });
-
- // Write ref tests to disk.
- if self.config.ui_config.debug.ref_test {
- self.write_ref_test_results(&terminal.lock());
- }
- }
-
+impl input::Processor<EventProxy, ActionContext<'_, Notifier, EventProxy>> {
/// Handle events from glutin.
///
/// Doesn't take self mutably due to borrow checking.
- fn handle_event<T>(
- event: GlutinEvent<'_, Event>,
- processor: &mut input::Processor<T, ActionContext<'_, N, T>>,
- ) where
- T: EventListener,
- {
+ pub fn handle_event(&mut self, event: GlutinEvent<'_, Event>) {
match event {
- GlutinEvent::UserEvent(event) => match event {
- Event::DprChanged(scale_factor, (width, height)) => {
- let display_update_pending = &mut processor.ctx.display_update_pending;
+ GlutinEvent::UserEvent(Event { payload, .. }) => match payload {
+ EventType::DprChanged(scale_factor, (width, height)) => {
+ let display_update_pending = &mut self.ctx.display.pending_update;
// Push current font to update its DPR.
- let font = processor.ctx.config.ui_config.font.clone();
- display_update_pending.set_font(font.with_size(*processor.ctx.font_size));
+ let font = self.ctx.config.ui_config.font.clone();
+ display_update_pending.set_font(font.with_size(*self.ctx.font_size));
// Resize to event's dimensions, since no resize event is emitted on Wayland.
display_update_pending.set_dimensions(PhysicalSize::new(width, height));
- processor.ctx.window().dpr = scale_factor;
- *processor.ctx.dirty = true;
+ self.ctx.window().dpr = scale_factor;
+ *self.ctx.dirty = true;
},
- Event::Message(message) => {
- processor.ctx.message_buffer.push(message);
- processor.ctx.display_update_pending.dirty = true;
- *processor.ctx.dirty = true;
+ EventType::SearchNext => self.ctx.goto_match(None),
+ EventType::Scroll(scroll) => self.ctx.scroll(scroll),
+ EventType::BlinkCursor => {
+ self.ctx.display.cursor_hidden ^= true;
+ *self.ctx.dirty = true;
},
- Event::SearchNext => processor.ctx.goto_match(None),
- Event::ConfigReload(path) => Self::reload_config(&path, processor),
- Event::Scroll(scroll) => processor.ctx.scroll(scroll),
- Event::BlinkCursor => {
- processor.ctx.display.cursor_hidden ^= true;
- *processor.ctx.dirty = true;
+ EventType::Message(message) => {
+ self.ctx.message_buffer.push(message);
+ self.ctx.display.pending_update.dirty = true;
+ *self.ctx.dirty = true;
},
- Event::Terminal(event) => match event {
+ EventType::Terminal(event) => match event {
TerminalEvent::Title(title) => {
- let ui_config = &processor.ctx.config.ui_config;
+ let ui_config = &self.ctx.config.ui_config;
if ui_config.window.dynamic_title {
- processor.ctx.window().set_title(&title);
+ self.ctx.window().set_title(&title);
}
},
TerminalEvent::ResetTitle => {
- let ui_config = &processor.ctx.config.ui_config;
+ let ui_config = &self.ctx.config.ui_config;
if ui_config.window.dynamic_title {
- processor.ctx.display.window.set_title(&ui_config.window.title);
+ self.ctx.display.window.set_title(&ui_config.window.title);
}
},
- TerminalEvent::Wakeup => *processor.ctx.dirty = true,
+ TerminalEvent::Wakeup => *self.ctx.dirty = true,
TerminalEvent::Bell => {
// Set window urgency.
- if processor.ctx.terminal.mode().contains(TermMode::URGENCY_HINTS) {
- let focused = processor.ctx.terminal.is_focused;
- processor.ctx.window().set_urgent(!focused);
+ if self.ctx.terminal.mode().contains(TermMode::URGENCY_HINTS) {
+ let focused = self.ctx.terminal.is_focused;
+ self.ctx.window().set_urgent(!focused);
}
// Ring visual bell.
- processor.ctx.display.visual_bell.ring();
+ self.ctx.display.visual_bell.ring();
// Execute bell command.
- if let Some(bell_command) = &processor.ctx.config.ui_config.bell.command {
+ if let Some(bell_command) = &self.ctx.config.ui_config.bell.command {
start_daemon(bell_command.program(), bell_command.args());
}
},
TerminalEvent::ClipboardStore(clipboard_type, content) => {
- processor.ctx.clipboard.store(clipboard_type, content);
+ self.ctx.clipboard.store(clipboard_type, content);
},
TerminalEvent::ClipboardLoad(clipboard_type, format) => {
- let text = format(processor.ctx.clipboard.load(clipboard_type).as_str());
- processor.ctx.write_to_pty(text.into_bytes());
+ let text = format(self.ctx.clipboard.load(clipboard_type).as_str());
+ self.ctx.write_to_pty(text.into_bytes());
},
TerminalEvent::ColorRequest(index, format) => {
- let text = format(processor.ctx.display.colors[index]);
- processor.ctx.write_to_pty(text.into_bytes());
+ let text = format(self.ctx.display.colors[index]);
+ self.ctx.write_to_pty(text.into_bytes());
},
- TerminalEvent::PtyWrite(text) => processor.ctx.write_to_pty(text.into_bytes()),
- TerminalEvent::MouseCursorDirty => processor.reset_mouse_cursor(),
+ TerminalEvent::PtyWrite(text) => self.ctx.write_to_pty(text.into_bytes()),
+ TerminalEvent::MouseCursorDirty => self.reset_mouse_cursor(),
TerminalEvent::Exit => (),
- TerminalEvent::CursorBlinkingChange(_) => {
- processor.ctx.update_cursor_blinking();
- },
+ TerminalEvent::CursorBlinkingChange => self.ctx.update_cursor_blinking(),
},
+ EventType::ConfigReload(_) | EventType::CreateWindow => (),
},
- GlutinEvent::RedrawRequested(_) => *processor.ctx.dirty = true,
- GlutinEvent::WindowEvent { event, window_id, .. } => {
+ GlutinEvent::RedrawRequested(_) => *self.ctx.dirty = true,
+ GlutinEvent::WindowEvent { event, .. } => {
match event {
- WindowEvent::CloseRequested => processor.ctx.terminal.exit(),
+ WindowEvent::CloseRequested => self.ctx.terminal.exit(),
WindowEvent::Resized(size) => {
// Minimizing the window sends a Resize event with zero width and
// height. But there's no need to ever actually resize to this.
@@ -1283,53 +1084,49 @@ impl<N: Notify + OnResize> Processor<N> {
return;
}
- processor.ctx.display_update_pending.set_dimensions(size);
- *processor.ctx.dirty = true;
+ self.ctx.display.pending_update.set_dimensions(size);
+ *self.ctx.dirty = true;
},
WindowEvent::KeyboardInput { input, is_synthetic: false, .. } => {
- processor.key_input(input);
+ self.key_input(input);
},
- WindowEvent::ModifiersChanged(modifiers) => {
- processor.modifiers_input(modifiers)
- },
- WindowEvent::ReceivedCharacter(c) => processor.received_char(c),
+ WindowEvent::ModifiersChanged(modifiers) => self.modifiers_input(modifiers),
+ WindowEvent::ReceivedCharacter(c) => self.received_char(c),
WindowEvent::MouseInput { state, button, .. } => {
- processor.ctx.window().set_mouse_visible(true);
- processor.mouse_input(state, button);
- *processor.ctx.dirty = true;
+ self.ctx.window().set_mouse_visible(true);
+ self.mouse_input(state, button);
+ *self.ctx.dirty = true;
},
WindowEvent::CursorMoved { position, .. } => {
- processor.ctx.window().set_mouse_visible(true);
- processor.mouse_moved(position);
+ self.ctx.window().set_mouse_visible(true);
+ self.mouse_moved(position);
},
WindowEvent::MouseWheel { delta, phase, .. } => {
- processor.ctx.window().set_mouse_visible(true);
- processor.mouse_wheel_input(delta, phase);
+ self.ctx.window().set_mouse_visible(true);
+ self.mouse_wheel_input(delta, phase);
},
WindowEvent::Focused(is_focused) => {
- if window_id == processor.ctx.window().window_id() {
- processor.ctx.terminal.is_focused = is_focused;
- *processor.ctx.dirty = true;
-
- if is_focused {
- processor.ctx.window().set_urgent(false);
- } else {
- processor.ctx.window().set_mouse_visible(true);
- }
-
- processor.ctx.update_cursor_blinking();
- processor.on_focus_change(is_focused);
+ self.ctx.terminal.is_focused = is_focused;
+ *self.ctx.dirty = true;
+
+ if is_focused {
+ self.ctx.window().set_urgent(false);
+ } else {
+ self.ctx.window().set_mouse_visible(true);
}
+
+ self.ctx.update_cursor_blinking();
+ self.on_focus_change(is_focused);
},
WindowEvent::DroppedFile(path) => {
let path: String = path.to_string_lossy().into();
- processor.ctx.write_to_pty((path + " ").into_bytes());
+ self.ctx.write_to_pty((path + " ").into_bytes());
},
WindowEvent::CursorLeft { .. } => {
- processor.ctx.mouse.inside_text_area = false;
+ self.ctx.mouse.inside_text_area = false;
- if processor.ctx.display().highlighted_hint.is_some() {
- *processor.ctx.dirty = true;
+ if self.ctx.display().highlighted_hint.is_some() {
+ *self.ctx.dirty = true;
}
},
WindowEvent::KeyboardInput { is_synthetic: true, .. }
@@ -1354,6 +1151,194 @@ impl<N: Notify + OnResize> Processor<N> {
| GlutinEvent::LoopDestroyed => (),
}
}
+}
+
+/// The event processor.
+///
+/// Stores some state from received events and dispatches actions when they are
+/// triggered.
+pub struct Processor {
+ #[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))]
+ wayland_event_queue: Option<EventQueue>,
+ windows: HashMap<WindowId, WindowContext>,
+ cli_options: CliOptions,
+ config: Config,
+}
+
+impl Processor {
+ /// Create a new event processor.
+ ///
+ /// Takes a writer which is expected to be hooked up to the write end of a PTY.
+ pub fn new(
+ config: Config,
+ cli_options: CliOptions,
+ _event_loop: &EventLoop<Event>,
+ ) -> Processor {
+ // Initialize Wayland event queue, to handle Wayland callbacks.
+ #[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))]
+ let wayland_event_queue = _event_loop.wayland_display().map(|display| {
+ let display = unsafe { WaylandDisplay::from_external_display(display as _) };
+ display.create_event_queue()
+ });
+
+ Processor {
+ windows: HashMap::new(),
+ cli_options,
+ config,
+ #[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))]
+ wayland_event_queue,
+ }
+ }
+
+ /// Create a new terminal window.
+ pub fn create_window(
+ &mut self,
+ event_loop: &EventLoopWindowTarget<Event>,
+ proxy: EventLoopProxy<Event>,
+ ) -> Result<(), Box<dyn Error>> {
+ let window_context = WindowContext::new(
+ &self.config,
+ event_loop,
+ proxy,
+ #[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))]
+ self.wayland_event_queue.as_ref(),
+ )?;
+ self.windows.insert(window_context.id(), window_context);
+ Ok(())
+ }
+
+ /// Run the event loop.
+ pub fn run(&mut self, mut event_loop: EventLoop<Event>) {
+ let proxy = event_loop.create_proxy();
+ let mut scheduler = Scheduler::new(proxy.clone());
+
+ // NOTE: Since this takes a pointer to the winit event loop, it MUST be dropped first.
+ #[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))]
+ let mut clipboard = unsafe { Clipboard::new(event_loop.wayland_display()) };
+ #[cfg(any(not(feature = "wayland"), target_os = "macos", windows))]
+ let mut clipboard = Clipboard::new();
+
+ event_loop.run_return(|event, event_loop, control_flow| {
+ if self.config.ui_config.debug.print_events {
+ info!("glutin event: {:?}", event);
+ }
+
+ // Ignore all events we do not care about.
+ if Self::skip_event(&event) {
+ return;
+ }
+
+ match event {
+ // Check for shutdown.
+ GlutinEvent::UserEvent(Event {
+ window_id: Some(window_id),
+ payload: EventType::Terminal(TerminalEvent::Exit),
+ }) => {
+ // Remove the closed terminal.
+ let window_context = match self.windows.remove(&window_id) {
+ Some(window_context) => window_context,
+ None => return,
+ };
+
+ // Unschedule pending events.
+ scheduler.unschedule_window(window_context.id());
+
+ // Shutdown if no more terminals are open.
+ if self.windows.is_empty() {
+ // Write ref tests of last window to disk.
+ if self.config.ui_config.debug.ref_test {
+ window_context.write_ref_test_results();
+ }
+
+ *control_flow = ControlFlow::Exit;
+ }
+ },
+ // Process all pending events.
+ GlutinEvent::RedrawEventsCleared => {
+ *control_flow = match scheduler.update() {
+ Some(instant) => ControlFlow::WaitUntil(instant),
+ None => ControlFlow::Wait,
+ };
+
+ // Check for pending frame callbacks on Wayland.
+ #[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))]
+ if let Some(wayland_event_queue) = self.wayland_event_queue.as_mut() {
+ wayland_event_queue
+ .dispatch_pending(&mut (), |_, _, _| {})
+ .expect("failed to dispatch wayland event queue");
+ }
+
+ // Dispatch event to all windows.
+ for window_context in self.windows.values_mut() {
+ window_context.handle_event(
+ event_loop,
+ &proxy,
+ &mut self.config,
+ &mut clipboard,
+ &mut scheduler,
+ GlutinEvent::RedrawEventsCleared,
+ );
+ }
+ },
+ // Process config update.
+ GlutinEvent::UserEvent(Event {
+ payload: EventType::ConfigReload(path), ..
+ }) => {
+ // Clear config logs from message bar for all terminals.
+ for window_context in self.windows.values_mut() {
+ if !window_context.message_buffer.is_empty() {
+ window_context.message_buffer.remove_target(LOG_TARGET_CONFIG);
+ window_context.display.pending_update.dirty = true;
+ }
+ }
+
+ // Load config and update each terminal.
+ if let Ok(config) = config::reload(&path, &self.cli_options) {
+ let old_config = mem::replace(&mut self.config, config);
+
+ for window_context in self.windows.values_mut() {
+ window_context.update_config(&old_config, &self.config);
+ }
+ }
+ },
+ // Create a new terminal window.
+ GlutinEvent::UserEvent(Event { payload: EventType::CreateWindow, .. }) => {
+ if let Err(err) = self.create_window(event_loop, proxy.clone()) {
+ error!("Could not open window: {:?}", err);
+ }
+ },
+ // Process events affecting all windows.
+ GlutinEvent::UserEvent(event @ Event { window_id: None, .. }) => {
+ for window_context in self.windows.values_mut() {
+ window_context.handle_event(
+ event_loop,
+ &proxy,
+ &mut self.config,
+ &mut clipboard,
+ &mut scheduler,
+ event.clone().into(),
+ );
+ }
+ },
+ // Process window-specific events.
+ GlutinEvent::WindowEvent { window_id, .. }
+ | GlutinEvent::UserEvent(Event { window_id: Some(window_id), .. })
+ | GlutinEvent::RedrawRequested(window_id) => {
+ if let Some(window_context) = self.windows.get_mut(&window_id) {
+ window_context.handle_event(
+ event_loop,
+ &proxy,
+ &mut self.config,
+ &mut clipboard,
+ &mut scheduler,
+ event,
+ );
+ }
+ },
+ _ => (),
+ }
+ });
+ }
/// Check if an event is irrelevant and can be skipped.
fn skip_event(event: &GlutinEvent<'_, Event>) -> bool {
@@ -1377,163 +1362,27 @@ impl<N: Notify + OnResize> Processor<N> {
_ => false,
}
}
-
- /// Reload the configuration files from disk.
- fn reload_config<T>(path: &Path, processor: &mut input::Processor<T, ActionContext<'_, N, T>>)
- where
- T: EventListener,
- {
- if !processor.ctx.message_buffer.is_empty() {
- processor.ctx.message_buffer.remove_target(LOG_TARGET_CONFIG);
- processor.ctx.display_update_pending.dirty = true;
- }
-
- let config = match config::reload(path, processor.ctx.cli_options) {
- Ok(config) => config,
- Err(_) => return,
- };
-
- processor.ctx.display.update_config(&config);
- processor.ctx.terminal.update_config(&config);
-
- // Reload cursor if its thickness has changed.
- if (processor.ctx.config.cursor.thickness() - config.cursor.thickness()).abs()
- > f32::EPSILON
- {
- processor.ctx.display_update_pending.set_cursor_dirty();
- }
-
- if processor.ctx.config.ui_config.font != config.ui_config.font {
- // Do not update font size if it has been changed at runtime.
- if *processor.ctx.font_size == processor.ctx.config.ui_config.font.size() {
- *processor.ctx.font_size = config.ui_config.font.size();
- }
-
- let font = config.ui_config.font.clone().with_size(*processor.ctx.font_size);
- processor.ctx.display_update_pending.set_font(font);
- }
-
- // Update display if padding options were changed.
- let window_config = &processor.ctx.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
- {
- processor.ctx.display_update_pending.dirty = true;
- }
-
- // Live title reload.
- if !config.ui_config.window.dynamic_title
- || processor.ctx.config.ui_config.window.title != config.ui_config.window.title
- {
- processor.ctx.window().set_title(&config.ui_config.window.title);
- }
-
- #[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))]
- if processor.ctx.event_loop.is_wayland() {
- processor.ctx.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")]
- processor.ctx.window().set_has_shadow(config.ui_config.window_opacity() >= 1.0);
-
- // Update hint keys.
- processor.ctx.display.hint_state.update_alphabet(config.ui_config.hints.alphabet());
-
- *processor.ctx.config = config;
-
- // Update cursor blinking.
- processor.ctx.update_cursor_blinking();
-
- *processor.ctx.dirty = true;
- }
-
- /// Submit the pending changes to the `Display`.
- fn submit_display_update<T>(
- &mut self,
- terminal: &mut Term<T>,
- old_is_searching: bool,
- display_update_pending: DisplayUpdate,
- ) where
- T: EventListener,
- {
- // 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 {
- self.search_state.direction == Direction::Left
- };
-
- self.display.handle_update(
- terminal,
- &mut self.notifier,
- &self.message_buffer,
- self.search_state.history_index.is_some(),
- &self.config,
- display_update_pending,
- );
-
- let new_is_searching = self.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));
- }
- }
- }
-
- /// Write the ref test results to the disk.
- fn write_ref_test_results<T>(&self, terminal: &Term<T>) {
- // Dump grid state.
- let mut grid = terminal.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");
- }
}
#[derive(Debug, Clone)]
-pub struct EventProxy(EventLoopProxy<Event>);
+pub struct EventProxy {
+ proxy: EventLoopProxy<Event>,
+ window_id: WindowId,
+}
impl EventProxy {
- pub fn new(proxy: EventLoopProxy<Event>) -> Self {
- EventProxy(proxy)
+ pub fn new(proxy: EventLoopProxy<Event>, window_id: WindowId) -> Self {
+ Self { proxy, window_id }
}
/// Send an event to the event loop.
- pub fn send_event(&self, event: Event) {
- let _ = self.0.send_event(event);
+ pub fn send_event(&self, event: EventType) {
+ let _ = self.proxy.send_event(Event::new(event, self.window_id));
}
}
impl EventListener for EventProxy {
fn send_event(&self, event: TerminalEvent) {
- let _ = self.0.send_event(Event::Terminal(event));
+ let _ = self.proxy.send_event(Event::new(event.into(), self.window_id));
}
}