From 87e5b1aa25ea61937fa5f79668d2a46e88707c5e Mon Sep 17 00:00:00 2001 From: Christian Duerr Date: Thu, 18 Jun 2020 01:02:56 +0000 Subject: [PATCH] Add automatic scrolling during selection This adds a new `Scheduler` which allows for staging events to be processed at a later time. If there is a selection active and the mouse is above or below the window, the viewport will now scroll torwards the direction of the mouse. The amount of lines scrolled depends on the distance of the mouse to the boundaries used for selection scrolling. To make it possible to scroll while in fullscreen, the selection scrolling area includes the padding of the window and is at least 5 pixels high in case there is not enough padding present. --- CHANGELOG.md | 1 + alacritty/src/config/monitor.rs | 3 +- alacritty/src/display.rs | 4 +- alacritty/src/event.rs | 134 +++++++++++++++++---------- alacritty/src/input.rs | 140 ++++++++++++++++++++--------- alacritty/src/logging.rs | 2 +- alacritty/src/main.rs | 4 +- alacritty/src/scheduler.rs | 102 +++++++++++++++++++++ alacritty/src/window.rs | 9 +- alacritty_terminal/src/event.rs | 8 -- alacritty_terminal/src/grid/mod.rs | 2 +- alacritty_terminal/src/term/mod.rs | 15 +++- alacritty_terminal/src/util.rs | 22 +---- 13 files changed, 313 insertions(+), 133 deletions(-) create mode 100644 alacritty/src/scheduler.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 54c5ebbe..b5abe418 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Font fallback on Windows - Support for Fontconfig embolden and matrix options - Opt-out compilation flag `winpty` to disable WinPTY support +- Scrolling during selection when mouse is at top/bottom of window ### Changed diff --git a/alacritty/src/config/monitor.rs b/alacritty/src/config/monitor.rs index d91b2e4b..42603c7e 100644 --- a/alacritty/src/config/monitor.rs +++ b/alacritty/src/config/monitor.rs @@ -4,10 +4,9 @@ use std::time::Duration; use notify::{watcher, DebouncedEvent, RecursiveMode, Watcher}; -use alacritty_terminal::event::{Event, EventListener}; use alacritty_terminal::util; -use crate::event::EventProxy; +use crate::event::{Event, EventProxy}; pub struct Monitor { _thread: ::std::thread::JoinHandle<()>, diff --git a/alacritty/src/display.rs b/alacritty/src/display.rs index 71c98559..d61a5bbd 100644 --- a/alacritty/src/display.rs +++ b/alacritty/src/display.rs @@ -23,7 +23,7 @@ use font::set_font_smoothing; use font::{self, Rasterize}; use alacritty_terminal::config::{Font, StartupMode}; -use alacritty_terminal::event::{Event, OnResize}; +use alacritty_terminal::event::OnResize; use alacritty_terminal::index::Line; use alacritty_terminal::message_bar::MessageBuffer; use alacritty_terminal::meter::Meter; @@ -119,7 +119,7 @@ pub struct Display { } impl Display { - pub fn new(config: &Config, event_loop: &EventLoop) -> Result { + pub fn new(config: &Config, event_loop: &EventLoop) -> Result { // Guess DPR based on first monitor. let estimated_dpr = event_loop.available_monitors().next().map(|m| m.scale_factor()).unwrap_or(1.); diff --git a/alacritty/src/event.rs b/alacritty/src/event.rs index 4e2a0578..084ebe1e 100644 --- a/alacritty/src/event.rs +++ b/alacritty/src/event.rs @@ -30,7 +30,7 @@ use font::{self, Size}; use alacritty_terminal::config::Font; use alacritty_terminal::config::LOG_TARGET_CONFIG; use alacritty_terminal::event::OnResize; -use alacritty_terminal::event::{Event, EventListener, Notify}; +use alacritty_terminal::event::{Event as TerminalEvent, EventListener, Notify}; use alacritty_terminal::grid::Scroll; use alacritty_terminal::index::{Column, Line, Point, Side}; use alacritty_terminal::message_bar::{Message, MessageBuffer}; @@ -40,7 +40,7 @@ use alacritty_terminal::term::cell::Cell; use alacritty_terminal::term::{ClipboardType, SizeInfo, Term, TermMode}; #[cfg(not(windows))] use alacritty_terminal::tty; -use alacritty_terminal::util::{limit, start_daemon}; +use alacritty_terminal::util::start_daemon; use crate::cli::Options; use crate::clipboard::Clipboard; @@ -48,9 +48,32 @@ use crate::config; use crate::config::Config; use crate::display::Display; use crate::input::{self, ActionContext as _, FONT_SIZE_STEP}; +use crate::scheduler::Scheduler; use crate::url::{Url, Urls}; use crate::window::Window; +/// Events dispatched through the UI event loop. +#[derive(Debug, Clone)] +pub enum Event { + TerminalEvent(TerminalEvent), + DPRChanged(f64, (u32, u32)), + Scroll(Scroll), + ConfigReload(PathBuf), + Message(Message), +} + +impl From for GlutinEvent<'_, Event> { + fn from(event: Event) -> Self { + GlutinEvent::UserEvent(event) + } +} + +impl From for Event { + fn from(event: TerminalEvent) -> Self { + Event::TerminalEvent(event) + } +} + #[derive(Default, Clone, Debug, PartialEq)] pub struct DisplayUpdate { pub dimensions: Option>, @@ -80,6 +103,7 @@ pub struct ActionContext<'a, N, T> { pub config: &'a mut Config, pub event_loop: &'a EventLoopWindowTarget, pub urls: &'a Urls, + pub scheduler: &'a mut Scheduler, font_size: &'a mut Size, } @@ -248,6 +272,25 @@ impl<'a, N: Notify + 'a, T: EventListener> input::ActionContext for ActionCon } } + /// Spawn URL launcher when clicking on URLs. + fn launch_url(&self, url: Url) { + if self.mouse.block_url_launcher { + return; + } + + if let Some(ref launcher) = self.config.ui_config.mouse.url.launcher { + let mut args = launcher.args().to_vec(); + let start = self.terminal.visible_to_buffer(url.start()); + let end = self.terminal.visible_to_buffer(url.end()); + args.push(self.terminal.bounds_to_string(start, end)); + + match start_daemon(launcher.program(), &args) { + Ok(_) => debug!("Launched {} with args {:?}", launcher.program(), args), + Err(_) => warn!("Unable to launch {} with args {:?}", launcher.program(), args), + } + } + } + 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.font.clone().with_size(*self.font_size); @@ -282,27 +325,12 @@ impl<'a, N: Notify + 'a, T: EventListener> input::ActionContext for ActionCon self.urls } - fn clipboard(&mut self) -> &mut Clipboard { + fn clipboard_mut(&mut self) -> &mut Clipboard { self.clipboard } - /// Spawn URL launcher when clicking on URLs. - fn launch_url(&self, url: Url) { - if self.mouse.block_url_launcher { - return; - } - - if let Some(ref launcher) = self.config.ui_config.mouse.url.launcher { - let mut args = launcher.args().to_vec(); - let start = self.terminal.visible_to_buffer(url.start()); - let end = self.terminal.visible_to_buffer(url.end()); - args.push(self.terminal.bounds_to_string(start, end)); - - match start_daemon(launcher.program(), &args) { - Ok(_) => debug!("Launched {} with args {:?}", launcher.program(), args), - Err(_) => warn!("Unable to launch {} with args {:?}", launcher.program(), args), - } - } + fn scheduler_mut(&mut self) -> &mut Scheduler { + self.scheduler } } @@ -369,7 +397,7 @@ pub struct Processor { message_buffer: MessageBuffer, display: Display, font_size: Size, - event_queue: Vec>, + event_queue: Vec>, } impl Processor { @@ -432,6 +460,8 @@ impl Processor { where T: EventListener, { + let mut scheduler = Scheduler::new(); + event_loop.run_return(|event, event_loop, control_flow| { if self.config.debug.print_events { info!("glutin event: {:?}", event); @@ -444,13 +474,16 @@ impl Processor { match event { // Check for shutdown. - GlutinEvent::UserEvent(Event::Exit) => { + GlutinEvent::UserEvent(Event::TerminalEvent(TerminalEvent::Exit)) => { *control_flow = ControlFlow::Exit; return; }, // Process events. GlutinEvent::RedrawEventsCleared => { - *control_flow = ControlFlow::Wait; + *control_flow = match scheduler.update(&mut self.event_queue) { + Some(instant) => ControlFlow::WaitUntil(instant), + None => ControlFlow::Wait, + }; if self.event_queue_empty() { return; @@ -463,8 +496,7 @@ impl Processor { } => { *control_flow = ControlFlow::Poll; let size = (new_inner_size.width, new_inner_size.height); - let event = GlutinEvent::UserEvent(Event::DPRChanged(scale_factor, size)); - self.event_queue.push(event); + self.event_queue.push(Event::DPRChanged(scale_factor, size).into()); return; }, // Transmute to extend lifetime, which exists only for `ScaleFactorChanged` event. @@ -495,6 +527,7 @@ impl Processor { font_size: &mut self.font_size, config: &mut self.config, urls: &self.display.urls, + scheduler: &mut scheduler, event_loop, }; let mut processor = input::Processor::new(context, &self.display.highlighted_url); @@ -529,7 +562,8 @@ impl Processor { // Request immediate re-draw if visual bell animation is not finished yet. if !terminal.visual_bell.completed() { - self.event_queue.push(GlutinEvent::UserEvent(Event::Wakeup)); + let event: Event = TerminalEvent::Wakeup.into(); + self.event_queue.push(event.into()); } // Redraw screen. @@ -571,26 +605,29 @@ impl Processor { processor.ctx.size_info.dpr = scale_factor; processor.ctx.terminal.dirty = true; }, - Event::Title(title) => processor.ctx.window.set_title(&title), - Event::Wakeup => processor.ctx.terminal.dirty = true, - Event::Urgent => { - processor.ctx.window.set_urgent(!processor.ctx.terminal.is_focused) - }, - Event::ConfigReload(path) => Self::reload_config(&path, processor), Event::Message(message) => { processor.ctx.message_buffer.push(message); processor.ctx.display_update_pending.message_buffer = true; processor.ctx.terminal.dirty = true; }, - Event::ClipboardStore(clipboard_type, content) => { - processor.ctx.clipboard.store(clipboard_type, content); + Event::ConfigReload(path) => Self::reload_config(&path, processor), + Event::Scroll(scroll) => processor.ctx.scroll(scroll), + Event::TerminalEvent(event) => match event { + TerminalEvent::Title(title) => processor.ctx.window.set_title(&title), + TerminalEvent::Wakeup => processor.ctx.terminal.dirty = true, + TerminalEvent::Urgent => { + processor.ctx.window.set_urgent(!processor.ctx.terminal.is_focused) + }, + TerminalEvent::ClipboardStore(clipboard_type, content) => { + processor.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()); + }, + TerminalEvent::MouseCursorDirty => processor.reset_mouse_cursor(), + TerminalEvent::Exit => (), }, - Event::ClipboardLoad(clipboard_type, format) => { - let text = format(processor.ctx.clipboard.load(clipboard_type).as_str()); - processor.ctx.write_to_pty(text.into_bytes()); - }, - Event::MouseCursorDirty => processor.reset_mouse_cursor(), - Event::Exit => (), }, GlutinEvent::RedrawRequested(_) => processor.ctx.terminal.dirty = true, GlutinEvent::WindowEvent { event, window_id, .. } => { @@ -624,12 +661,8 @@ impl Processor { processor.modifiers_input(modifiers) }, WindowEvent::CursorMoved { position, .. } => { - let (x, y) = position.into(); - let x = limit(x, 0, processor.ctx.size_info.width as i32); - let y = limit(y, 0, processor.ctx.size_info.height as i32); - processor.ctx.window.set_mouse_visible(true); - processor.mouse_moved(x as usize, y as usize); + processor.mouse_moved(position); }, WindowEvent::MouseWheel { delta, phase, .. } => { processor.ctx.window.set_mouse_visible(true); @@ -796,10 +829,15 @@ impl EventProxy { pub fn new(proxy: EventLoopProxy) -> Self { EventProxy(proxy) } -} -impl EventListener for EventProxy { - fn send_event(&self, event: Event) { + /// Send an event to the event loop. + pub fn send_event(&self, event: Event) { let _ = self.0.send_event(event); } } + +impl EventListener for EventProxy { + fn send_event(&self, event: TerminalEvent) { + let _ = self.0.send_event(Event::TerminalEvent(event)); + } +} diff --git a/alacritty/src/input.rs b/alacritty/src/input.rs index 2025f108..d43bb26f 100644 --- a/alacritty/src/input.rs +++ b/alacritty/src/input.rs @@ -6,12 +6,13 @@ //! determine what to do when a non-modifier key is pressed. use std::borrow::Cow; -use std::cmp::{min, Ordering}; +use std::cmp::{max, min, Ordering}; use std::marker::PhantomData; -use std::time::Instant; +use std::time::{Duration, Instant}; use log::{debug, trace, warn}; +use glutin::dpi::PhysicalPosition; use glutin::event::{ ElementState, KeyboardInput, ModifiersState, MouseButton, MouseScrollDelta, TouchPhase, }; @@ -21,7 +22,7 @@ use glutin::platform::macos::EventLoopWindowTargetExtMacOS; use glutin::window::CursorIcon; use alacritty_terminal::ansi::{ClearMode, Handler}; -use alacritty_terminal::event::{Event, EventListener}; +use alacritty_terminal::event::EventListener; use alacritty_terminal::grid::Scroll; use alacritty_terminal::index::{Column, Line, Point, Side}; use alacritty_terminal::message_bar::{self, Message}; @@ -33,13 +34,23 @@ use alacritty_terminal::vi_mode::ViMotion; use crate::clipboard::Clipboard; use crate::config::{Action, Binding, Config, Key, ViAction}; -use crate::event::{ClickState, Mouse}; +use crate::event::{ClickState, Event, Mouse}; +use crate::scheduler::{Scheduler, TimerId}; use crate::url::{Url, Urls}; use crate::window::Window; /// Font size change interval. pub const FONT_SIZE_STEP: f32 = 0.5; +/// Interval for mouse scrolling during selection outside of the boundaries. +const SELECTION_SCROLLING_INTERVAL: Duration = Duration::from_millis(15); + +/// Minimum number of pixels at the bottom/top where selection scrolling is performed. +const MIN_SELECTION_SCROLLING_HEIGHT: f64 = 5.; + +/// Number of pixels for increasing the selection scrolling speed factor by one. +const SELECTION_SCROLLING_STEP: f64 = 20.; + /// Processes input from glutin. /// /// An escape sequence may be emitted in case specific keys or key combinations @@ -77,10 +88,11 @@ pub trait ActionContext { fn message(&self) -> Option<&Message>; fn config(&self) -> &Config; fn event_loop(&self) -> &EventLoopWindowTarget; - fn clipboard(&mut self) -> &mut Clipboard; fn urls(&self) -> &Urls; fn launch_url(&self, url: Url); fn mouse_mode(&self) -> bool; + fn clipboard_mut(&mut self) -> &mut Clipboard; + fn scheduler_mut(&mut self) -> &mut Scheduler; } trait Execute { @@ -128,11 +140,11 @@ impl Execute for Action { #[cfg(not(any(target_os = "macos", windows)))] Action::CopySelection => ctx.copy_selection(ClipboardType::Selection), Action::Paste => { - let text = ctx.clipboard().load(ClipboardType::Clipboard); + let text = ctx.clipboard_mut().load(ClipboardType::Clipboard); paste(ctx, &text); }, Action::PasteSelection => { - let text = ctx.clipboard().load(ClipboardType::Selection); + let text = ctx.clipboard_mut().load(ClipboardType::Selection); paste(ctx, &text); }, Action::Command(ref program) => { @@ -304,9 +316,19 @@ impl<'a, T: EventListener, A: ActionContext> Processor<'a, T, A> { } #[inline] - pub fn mouse_moved(&mut self, x: usize, y: usize) { + pub fn mouse_moved(&mut self, position: PhysicalPosition) { let size_info = self.ctx.size_info(); + let (x, y) = position.into(); + + let lmb_pressed = self.ctx.mouse().left_button_state == ElementState::Pressed; + if !self.ctx.selection_is_empty() && lmb_pressed { + self.update_selection_scrolling(y); + } + + let x = min(max(x, 0), size_info.width as i32 - 1) as usize; + let y = min(max(y, 0), size_info.height as i32 - 1) as usize; + self.ctx.mouse_mut().x = x; self.ctx.mouse_mut().y = y; @@ -339,9 +361,7 @@ impl<'a, T: EventListener, A: ActionContext> Processor<'a, T, A> { self.ctx.window_mut().set_mouse_cursor(mouse_state.into()); let last_term_line = self.ctx.terminal().grid().num_lines() - 1; - if self.ctx.mouse().left_button_state == ElementState::Pressed - && (self.ctx.modifiers().shift() || !self.ctx.mouse_mode()) - { + if lmb_pressed && (self.ctx.modifiers().shift() || !self.ctx.mouse_mode()) { // Treat motion over message bar like motion over the last line. let line = min(point.line, last_term_line); @@ -356,7 +376,7 @@ impl<'a, T: EventListener, A: ActionContext> Processor<'a, T, A> { && point.line <= last_term_line && self.ctx.terminal().mode().intersects(TermMode::MOUSE_MOTION | TermMode::MOUSE_DRAG) { - if self.ctx.mouse().left_button_state == ElementState::Pressed { + if lmb_pressed { self.mouse_report(32, ElementState::Pressed); } else if self.ctx.mouse().middle_button_state == ElementState::Pressed { self.mouse_report(33, ElementState::Pressed); @@ -548,6 +568,7 @@ impl<'a, T: EventListener, A: ActionContext> Processor<'a, T, A> { self.ctx.launch_url(url); } + self.ctx.scheduler_mut().unschedule(TimerId::SelectionScrolling); self.copy_selection(); } @@ -870,32 +891,61 @@ impl<'a, T: EventListener, A: ActionContext> Processor<'a, T, A> { MouseState::Text } } + + /// Handle automatic scrolling when selecting above/below the window. + fn update_selection_scrolling(&mut self, mouse_y: i32) { + let size_info = self.ctx.size_info(); + let scheduler = self.ctx.scheduler_mut(); + + // Scale constants by DPI. + let min_height = (MIN_SELECTION_SCROLLING_HEIGHT * size_info.dpr) as i32; + let step = (SELECTION_SCROLLING_STEP * size_info.dpr) as i32; + + // Compute the height of the scrolling areas. + let end_top = max(min_height, size_info.padding_y as i32); + let height_bottom = max(min_height, size_info.padding_bottom() as i32); + let start_bottom = size_info.height as i32 - height_bottom; + + // Get distance from closest window boundary. + let delta = if mouse_y < end_top { + end_top - mouse_y + step + } else if mouse_y >= start_bottom { + start_bottom - mouse_y - step + } else { + scheduler.unschedule(TimerId::SelectionScrolling); + return; + }; + + // Scale number of lines scrolled based on distance to boundary. + let delta = delta as isize / step as isize; + let event = Event::Scroll(Scroll::Lines(delta)); + + // Schedule event. + match scheduler.get_mut(TimerId::SelectionScrolling) { + Some(timer) => timer.event = event.into(), + None => { + scheduler.schedule( + event.into(), + SELECTION_SCROLLING_INTERVAL, + true, + TimerId::SelectionScrolling, + ); + }, + } + } } #[cfg(test)] mod tests { - use std::borrow::Cow; - use std::time::Duration; + use super::*; - use glutin::event::{ - ElementState, Event, ModifiersState, MouseButton, VirtualKeyCode, WindowEvent, - }; - use glutin::event_loop::EventLoopWindowTarget; + use glutin::event::{Event as GlutinEvent, VirtualKeyCode, WindowEvent}; - use alacritty_terminal::event::{Event as TerminalEvent, EventListener}; - use alacritty_terminal::grid::Scroll; - use alacritty_terminal::index::{Point, Side}; - use alacritty_terminal::message_bar::{Message, MessageBuffer}; - use alacritty_terminal::selection::{Selection, SelectionType}; - use alacritty_terminal::term::{ClipboardType, SizeInfo, Term, TermMode}; + use alacritty_terminal::event::Event as TerminalEvent; + use alacritty_terminal::message_bar::MessageBuffer; + use alacritty_terminal::selection::Selection; - use crate::clipboard::Clipboard; - use crate::config::{ClickHandler, Config}; - use crate::event::{ClickState, Mouse}; - use crate::url::{Url, Urls}; - use crate::window::Window; - - use super::{Action, Binding, Processor}; + use crate::config::ClickHandler; const KEY: VirtualKeyCode = VirtualKeyCode::Key0; @@ -1014,7 +1064,11 @@ mod tests { self.config } - fn event_loop(&self) -> &EventLoopWindowTarget { + fn clipboard_mut(&mut self) -> &mut Clipboard { + self.clipboard + } + + fn event_loop(&self) -> &EventLoopWindowTarget { unimplemented!(); } @@ -1022,11 +1076,11 @@ mod tests { unimplemented!(); } - fn clipboard(&mut self) -> &mut Clipboard { - self.clipboard + fn launch_url(&self, _: Url) { + unimplemented!(); } - fn launch_url(&self, _: Url) { + fn scheduler_mut (&mut self) -> &mut Scheduler { unimplemented!(); } } @@ -1089,8 +1143,8 @@ mod tests { let mut processor = Processor::new(context, &None); - let event: Event::<'_, TerminalEvent> = $input; - if let Event::WindowEvent { + let event: GlutinEvent::<'_, TerminalEvent> = $input; + if let GlutinEvent::WindowEvent { event: WindowEvent::MouseInput { state, button, @@ -1130,7 +1184,7 @@ mod tests { name: single_click, initial_state: ClickState::None, initial_button: MouseButton::Other(0), - input: Event::WindowEvent { + input: GlutinEvent::WindowEvent { event: WindowEvent::MouseInput { state: ElementState::Pressed, button: MouseButton::Left, @@ -1146,7 +1200,7 @@ mod tests { name: single_right_click, initial_state: ClickState::None, initial_button: MouseButton::Other(0), - input: Event::WindowEvent { + input: GlutinEvent::WindowEvent { event: WindowEvent::MouseInput { state: ElementState::Pressed, button: MouseButton::Right, @@ -1162,7 +1216,7 @@ mod tests { name: single_middle_click, initial_state: ClickState::None, initial_button: MouseButton::Other(0), - input: Event::WindowEvent { + input: GlutinEvent::WindowEvent { event: WindowEvent::MouseInput { state: ElementState::Pressed, button: MouseButton::Middle, @@ -1178,7 +1232,7 @@ mod tests { name: double_click, initial_state: ClickState::Click, initial_button: MouseButton::Left, - input: Event::WindowEvent { + input: GlutinEvent::WindowEvent { event: WindowEvent::MouseInput { state: ElementState::Pressed, button: MouseButton::Left, @@ -1194,7 +1248,7 @@ mod tests { name: triple_click, initial_state: ClickState::DoubleClick, initial_button: MouseButton::Left, - input: Event::WindowEvent { + input: GlutinEvent::WindowEvent { event: WindowEvent::MouseInput { state: ElementState::Pressed, button: MouseButton::Left, @@ -1210,7 +1264,7 @@ mod tests { name: multi_click_separate_buttons, initial_state: ClickState::DoubleClick, initial_button: MouseButton::Left, - input: Event::WindowEvent { + input: GlutinEvent::WindowEvent { event: WindowEvent::MouseInput { state: ElementState::Pressed, button: MouseButton::Right, diff --git a/alacritty/src/logging.rs b/alacritty/src/logging.rs index 1e56f3cb..a6fcf3fb 100644 --- a/alacritty/src/logging.rs +++ b/alacritty/src/logging.rs @@ -15,11 +15,11 @@ use std::sync::{Arc, Mutex}; use glutin::event_loop::EventLoopProxy; use log::{self, Level}; -use alacritty_terminal::event::Event; use alacritty_terminal::message_bar::Message; use alacritty_terminal::term::color; use crate::cli::Options; +use crate::event::Event; const ALACRITTY_LOG_ENV: &str = "ALACRITTY_LOG"; diff --git a/alacritty/src/main.rs b/alacritty/src/main.rs index 9587aec3..ab4acaa7 100644 --- a/alacritty/src/main.rs +++ b/alacritty/src/main.rs @@ -23,7 +23,6 @@ use log::{error, info}; #[cfg(windows)] use winapi::um::wincon::{AttachConsole, FreeConsole, ATTACH_PARENT_PROCESS}; -use alacritty_terminal::event::Event; use alacritty_terminal::event_loop::{self, EventLoop, Msg}; #[cfg(target_os = "macos")] use alacritty_terminal::locale; @@ -42,6 +41,7 @@ mod event; mod input; mod logging; mod renderer; +mod scheduler; mod url; mod window; @@ -57,7 +57,7 @@ use crate::cli::Options; use crate::config::monitor::Monitor; use crate::config::Config; use crate::display::Display; -use crate::event::{EventProxy, Processor}; +use crate::event::{Event, EventProxy, Processor}; fn main() { panic::attach_handler(); diff --git a/alacritty/src/scheduler.rs b/alacritty/src/scheduler.rs new file mode 100644 index 00000000..a6559acc --- /dev/null +++ b/alacritty/src/scheduler.rs @@ -0,0 +1,102 @@ +//! Scheduler for emitting events at a specific time in the future. + +use std::collections::VecDeque; +use std::time::{Duration, Instant}; + +use glutin::event::Event as GlutinEvent; + +use crate::event::Event as AlacrittyEvent; + +type Event = GlutinEvent<'static, AlacrittyEvent>; + +/// Scheduler tracking all pending timers. +pub struct Scheduler { + timers: VecDeque, +} + +impl Default for Scheduler { + fn default() -> Self { + Self { timers: VecDeque::new() } + } +} + +impl Scheduler { + pub fn new() -> Self { + Self::default() + } + + /// Process all pending timers. + /// + /// If there are still timers pending after all ready events have been processed, the closest + /// pending deadline will be returned. + pub fn update(&mut self, event_queue: &mut Vec) -> Option { + let now = Instant::now(); + while !self.timers.is_empty() && self.timers[0].deadline <= now { + if let Some(timer) = self.timers.pop_front() { + // Automatically repeat the event. + if let Some(interval) = timer.interval { + self.schedule(timer.event.clone(), interval, true, timer.id); + } + + event_queue.push(timer.event); + } + } + + self.timers.get(0).map(|timer| timer.deadline) + } + + /// Schedule a new event. + pub fn schedule( + &mut self, + event: Event, + interval: Duration, + repeat: bool, + timer_id: TimerId, + ) { + let deadline = Instant::now() + interval; + + // Get insert position in the schedule. + let mut index = self.timers.len(); + loop { + if index == 0 { + break; + } + index -= 1; + + if self.timers[index].deadline < deadline { + break; + } + } + + // Set the automatic event repeat rate. + let interval = if repeat { Some(interval) } else { None }; + + self.timers.insert(index, Timer { interval, deadline, event, id: timer_id }); + } + + /// Cancel a scheduled event. + pub fn unschedule(&mut self, id: TimerId) -> Option { + let index = self.timers.iter().position(|timer| timer.id == id)?; + self.timers.remove(index).map(|timer| timer.event) + } + + /// Access a staged event by ID. + pub fn get_mut(&mut self, id: TimerId) -> Option<&mut Timer> { + self.timers.iter_mut().find(|timer| timer.id == id) + } +} + +/// ID uniquely identifying a timer. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum TimerId { + SelectionScrolling, +} + +/// Event scheduled to be emitted at a specific time. +pub struct Timer { + pub deadline: Instant, + pub event: Event, + + interval: Option, + id: TimerId, +} diff --git a/alacritty/src/window.rs b/alacritty/src/window.rs index 22e84780..4275f859 100644 --- a/alacritty/src/window.rs +++ b/alacritty/src/window.rs @@ -34,7 +34,6 @@ use glutin::{self, ContextBuilder, PossiblyCurrent, WindowedContext}; use winapi::shared::minwindef::WORD; use alacritty_terminal::config::{Decorations, StartupMode, WindowConfig}; -use alacritty_terminal::event::Event; #[cfg(not(windows))] use alacritty_terminal::term::{SizeInfo, Term}; @@ -103,9 +102,9 @@ impl From for Error { } } -fn create_gl_window( +fn create_gl_window( mut window: WindowBuilder, - event_loop: &EventLoop, + event_loop: &EventLoop, srgb: bool, vsync: bool, dimensions: Option>, @@ -147,8 +146,8 @@ impl Window { /// Create a new window. /// /// This creates a window and fully initializes a window. - pub fn new( - event_loop: &EventLoop, + pub fn new( + event_loop: &EventLoop, config: &Config, size: Option>, #[cfg(not(any(target_os = "macos", windows)))] wayland_event_queue: Option<&EventQueue>, diff --git a/alacritty_terminal/src/event.rs b/alacritty_terminal/src/event.rs index 20b105cd..296e3bb0 100644 --- a/alacritty_terminal/src/event.rs +++ b/alacritty_terminal/src/event.rs @@ -1,17 +1,12 @@ use std::borrow::Cow; use std::fmt::{self, Debug, Formatter}; -use std::path::PathBuf; use std::sync::Arc; -use crate::message_bar::Message; use crate::term::{ClipboardType, SizeInfo}; #[derive(Clone)] pub enum Event { - DPRChanged(f64, (u32, u32)), - ConfigReload(PathBuf), MouseCursorDirty, - Message(Message), Title(String), ClipboardStore(ClipboardType, String), ClipboardLoad(ClipboardType, Arc String + Sync + Send + 'static>), @@ -23,10 +18,7 @@ pub enum Event { impl Debug for Event { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { match self { - Event::DPRChanged(scale, size) => write!(f, "DPRChanged({}, {:?})", scale, size), - Event::ConfigReload(path) => write!(f, "ConfigReload({:?})", path), Event::MouseCursorDirty => write!(f, "MouseCursorDirty"), - Event::Message(msg) => write!(f, "Message({:?})", msg), Event::Title(title) => write!(f, "Title({})", title), Event::ClipboardStore(ty, text) => write!(f, "ClipboardStore({:?}, {})", ty, text), Event::ClipboardLoad(ty, _) => write!(f, "ClipboardLoad({:?})", ty), diff --git a/alacritty_terminal/src/grid/mod.rs b/alacritty_terminal/src/grid/mod.rs index 0c338fba..d5932639 100644 --- a/alacritty_terminal/src/grid/mod.rs +++ b/alacritty_terminal/src/grid/mod.rs @@ -148,7 +148,7 @@ pub struct Grid { max_scroll_limit: usize, } -#[derive(Copy, Clone)] +#[derive(Debug, Copy, Clone)] pub enum Scroll { Lines(isize), PageUp, diff --git a/alacritty_terminal/src/term/mod.rs b/alacritty_terminal/src/term/mod.rs index 6aac05d9..33d2e14e 100644 --- a/alacritty_terminal/src/term/mod.rs +++ b/alacritty_terminal/src/term/mod.rs @@ -691,13 +691,24 @@ impl SizeInfo { Column(((self.width - 2. * self.padding_x) / self.cell_width) as usize) } + #[inline] + pub fn padding_right(&self) -> usize { + (self.padding_x + (self.width - 2. * self.padding_x) % self.cell_width) as usize + } + + #[inline] + pub fn padding_bottom(&self) -> usize { + (self.padding_y + (self.height - 2. * self.padding_y) % self.cell_height) as usize + } + /// Check if coordinates are inside the terminal grid. /// /// The padding is not counted as part of the grid. + #[inline] pub fn contains_point(&self, x: usize, y: usize) -> bool { - x < (self.width - self.padding_x) as usize + x < (self.width as usize - self.padding_right()) && x >= self.padding_x as usize - && y < (self.height - self.padding_y) as usize + && y < (self.height as usize - self.padding_bottom()) && y >= self.padding_y as usize } diff --git a/alacritty_terminal/src/util.rs b/alacritty_terminal/src/util.rs index fde06966..f996359a 100644 --- a/alacritty_terminal/src/util.rs +++ b/alacritty_terminal/src/util.rs @@ -1,6 +1,6 @@ use std::ffi::OsStr; +use std::io; use std::process::{Command, Stdio}; -use std::{cmp, io}; #[cfg(not(windows))] use std::os::unix::process::CommandExt; @@ -13,22 +13,18 @@ use winapi::um::winbase::{CREATE_NEW_PROCESS_GROUP, CREATE_NO_WINDOW}; /// Threading utilities. pub mod thread { /// Like `thread::spawn`, but with a `name` argument. - pub fn spawn_named(name: S, f: F) -> ::std::thread::JoinHandle + pub fn spawn_named(name: S, f: F) -> std::thread::JoinHandle where F: FnOnce() -> T + Send + 'static, T: Send + 'static, S: Into, { - ::std::thread::Builder::new().name(name.into()).spawn(f).expect("thread spawn works") + std::thread::Builder::new().name(name.into()).spawn(f).expect("thread spawn works") } pub use std::thread::*; } -pub fn limit(value: T, min: T, max: T) -> T { - cmp::min(cmp::max(value, min), max) -} - #[cfg(not(windows))] pub fn start_daemon(program: &str, args: I) -> io::Result<()> where @@ -79,15 +75,3 @@ where .spawn() .map(|_| ()) } - -#[cfg(test)] -mod tests { - use super::limit; - - #[test] - fn limit_works() { - assert_eq!(10, limit(10, 0, 100)); - assert_eq!(10, limit(5, 10, 100)); - assert_eq!(100, limit(1000, 10, 100)); - } -}