From fbc7b7227171b41d96ca52df52e4cf1833f5fc6f Mon Sep 17 00:00:00 2001 From: Mark Andrus Roberts Date: Fri, 3 Feb 2017 15:34:52 -0800 Subject: [PATCH] Add visual bell support This commit adds support for a visual bell. Although the Handler in src/ansi.rs warns "Hopefully this is never implemented", I wanted to give it a try. A new config option is added, `visual_bell`, which sets the `duration` and `animation` function of the visual bell. The default `duration` is 150 ms, and the default `animation` is `EaseOutExpo`. To disable the visual bell, set its duration to 0. The visual bell is modeled by VisualBell in src/term/mod.rs. It has a method to ring the bell, `ring`, and another method, `intensity`. Both return the "intensity" of the bell, which ramps down from 1.0 to 0.0 at a rate set by `duration` and `animation`. Whether or not the Processor waits for events is now configurable in order to allow for smooth drawing of the visual bell. --- alacritty.yml | 26 ++++++++++ res/text.f.glsl | 3 +- res/text.v.glsl | 3 ++ src/config.rs | 75 +++++++++++++++++++++++++-- src/display.rs | 4 +- src/event.rs | 50 ++++++++++-------- src/renderer/mod.rs | 27 ++++++++-- src/term/mod.rs | 121 +++++++++++++++++++++++++++++++++++++++++++- 8 files changed, 276 insertions(+), 33 deletions(-) diff --git a/alacritty.yml b/alacritty.yml index 9a3a3711..28ddf365 100644 --- a/alacritty.yml +++ b/alacritty.yml @@ -118,6 +118,32 @@ colors: # cyan: '0x93a1a1' # white: '0xfdf6e3' +# Visual Bell +# +# Any time the BEL code is received, Alacritty "rings" the visual bell. Once +# rung, the terminal background will be set to white and transition back to the +# default background color. You can control the rate of this transition by +# setting the `duration` property (represented in milliseconds). You can also +# configure the transition function by setting the `animation` property. +# +# Possible values for `animation` +# `Ease` +# `EaseOut` +# `EaseOutSine` +# `EaseOutQuad` +# `EaseOutCubic` +# `EaseOutQuart` +# `EaseOutQuint` +# `EaseOutExpo` +# `EaseOutCirc` +# `Linear` +# +# To completely disable the visual bell, set its duration to 0. +# +#visual_bell: +# animation: EaseOutExpo +# duration: 150 + # Key bindings # # Each binding is defined as an object with some properties. Most of the diff --git a/res/text.f.glsl b/res/text.f.glsl index 770c1a36..70d50b38 100644 --- a/res/text.f.glsl +++ b/res/text.f.glsl @@ -15,6 +15,7 @@ in vec2 TexCoords; in vec3 fg; in vec3 bg; +flat in float vb; flat in int background; layout(location = 0, index = 0) out vec4 color; @@ -26,7 +27,7 @@ void main() { if (background != 0) { alphaMask = vec4(1.0, 1.0, 1.0, 1.0); - color = vec4(bg, 1.0); + color = vec4(bg + vb, 1.0); } else { alphaMask = vec4(texture(mask, TexCoords).rgb, 1.0); color = vec4(fg, 1.0); diff --git a/res/text.v.glsl b/res/text.v.glsl index 99234775..ccb6b8b5 100644 --- a/res/text.v.glsl +++ b/res/text.v.glsl @@ -36,10 +36,12 @@ out vec3 bg; uniform vec2 termDim; uniform vec2 cellDim; +uniform float visualBell; uniform int backgroundPass; // Orthographic projection uniform mat4 projection; +flat out float vb; flat out int background; void main() @@ -72,6 +74,7 @@ void main() TexCoords = uvOffset + vec2(position.x, 1 - position.y) * uvSize; } + vb = visualBell; background = backgroundPass; bg = backgroundColor / vec3(255.0, 255.0, 255.0); fg = textColor / vec3(255.0, 255.0, 255.0); diff --git a/src/config.rs b/src/config.rs index c610f419..eb4a9e61 100644 --- a/src/config.rs +++ b/src/config.rs @@ -4,10 +4,8 @@ //! parameters including font family and style, font size, etc. In the future, //! the config file will also hold user and platform specific keybindings. use std::borrow::Cow; -use std::env; -use std::fmt; -use std::fs::File; -use std::fs; +use std::{env, fmt}; +use std::fs::{self, File}; use std::io::{self, Read, Write}; use std::ops::{Index, IndexMut}; use std::path::{Path, PathBuf}; @@ -221,6 +219,64 @@ impl IndexMut for ColorList { } } +/// VisulBellAnimations are modeled after a subset of CSS transitions and Robert +/// Penner's Easing Functions. +#[derive(Clone, Copy, Debug, Deserialize)] +pub enum VisualBellAnimation { + Ease, // CSS + EaseOut, // CSS + EaseOutSine, // Penner + EaseOutQuad, // Penner + EaseOutCubic, // Penner + EaseOutQuart, // Penner + EaseOutQuint, // Penner + EaseOutExpo, // Penner + EaseOutCirc, // Penner + Linear, +} + +#[derive(Debug, Deserialize)] +pub struct VisualBellConfig { + /// Visual bell animation function + #[serde(default="default_visual_bell_animation")] + animation: VisualBellAnimation, + + /// Visual bell duration in milliseconds + #[serde(default="default_visual_bell_duration")] + duration: u16, +} + +fn default_visual_bell_animation() -> VisualBellAnimation { + VisualBellAnimation::EaseOutExpo +} + +fn default_visual_bell_duration() -> u16 { + 150 +} + +impl VisualBellConfig { + /// Visual bell animation + #[inline] + pub fn animation(&self) -> VisualBellAnimation { + self.animation + } + + /// Visual bell duration in milliseconds + #[inline] + pub fn duration(&self) -> Duration { + Duration::from_millis(self.duration as u64) + } +} + +impl Default for VisualBellConfig { + fn default() -> VisualBellConfig { + VisualBellConfig { + animation: default_visual_bell_animation(), + duration: default_visual_bell_duration(), + } + } +} + #[derive(Debug, Deserialize)] pub struct Shell<'a> { program: Cow<'a, str>, @@ -307,6 +363,10 @@ pub struct Config { /// Path where config was loaded from config_path: Option, + + /// Visual bell configuration + #[serde(default)] + visual_bell: VisualBellConfig, } #[cfg(not(target_os="macos"))] @@ -351,6 +411,7 @@ impl Default for Config { mouse: Default::default(), shell: None, config_path: None, + visual_bell: Default::default(), } } } @@ -1058,6 +1119,12 @@ impl Config { &self.dpi } + /// Get visual bell config + #[inline] + pub fn visual_bell(&self) -> &VisualBellConfig { + &self.visual_bell + } + /// Should show render timer #[inline] pub fn render_timer(&self) -> bool { diff --git a/src/display.rs b/src/display.rs index 0b242b33..9bfe7339 100644 --- a/src/display.rs +++ b/src/display.rs @@ -272,7 +272,7 @@ impl Display { /// This call may block if vsync is enabled pub fn draw(&mut self, mut terminal: MutexGuard, config: &Config, selection: &Selection) { // Clear dirty flag - terminal.dirty = false; + terminal.dirty = !terminal.visual_bell.completed(); if let Some(title) = terminal.get_next_title() { self.window.set_title(&title); @@ -291,6 +291,8 @@ impl Display { // mutable borrow let size_info = *terminal.size_info(); self.renderer.with_api(config, &size_info, |mut api| { + api.set_visual_bell(terminal.visual_bell.intensity() as f32); + api.clear(); // Draw the grid diff --git a/src/event.rs b/src/event.rs index f643c115..9a7d2e8e 100644 --- a/src/event.rs +++ b/src/event.rs @@ -135,6 +135,7 @@ pub struct Processor { mouse_bindings: Vec, mouse_config: config::Mouse, print_events: bool, + wait_for_event: bool, notifier: N, mouse: Mouse, resize_tx: mpsc::Sender<(u32, u32)>, @@ -170,6 +171,7 @@ impl Processor { mouse_bindings: config.mouse_bindings().to_vec(), mouse_config: config.mouse().to_owned(), print_events: options.print_events, + wait_for_event: true, notifier: notifier, resize_tx: resize_tx, ref_test: ref_test, @@ -245,7 +247,8 @@ impl Processor { } } - /// Process at least one event and handle any additional queued events. + /// Process events. When `wait_for_event` is set, this method is guaranteed + /// to process at least one event. pub fn process_events<'a>( &mut self, term: &'a FairMutex, @@ -276,28 +279,31 @@ impl Processor { } } - match window.wait_events().next() { - Some(event) => { - terminal = term.lock(); - context = ActionContext { - terminal: &mut terminal, - notifier: &mut self.notifier, - selection: &mut self.selection, - mouse: &mut self.mouse, - size_info: &self.size_info, - }; + let event = if self.wait_for_event { + window.wait_events().next() + } else { + None + }; - processor = input::Processor { - ctx: context, - mouse_config: &self.mouse_config, - key_bindings: &self.key_bindings[..], - mouse_bindings: &self.mouse_bindings[..], - }; + terminal = term.lock(); - process!(event); - }, - // Glutin guarantees the WaitEventsIterator never returns None. - None => unreachable!(), + context = ActionContext { + terminal: &mut terminal, + notifier: &mut self.notifier, + selection: &mut self.selection, + mouse: &mut self.mouse, + size_info: &self.size_info, + }; + + processor = input::Processor { + ctx: context, + mouse_config: &self.mouse_config, + key_bindings: &self.key_bindings[..], + mouse_bindings: &self.mouse_bindings[..] + }; + + if let Some(event) = event { + process!(event); } for event in window.poll_events() { @@ -305,6 +311,8 @@ impl Processor { } } + self.wait_for_event = !terminal.dirty; + terminal } diff --git a/src/renderer/mod.rs b/src/renderer/mod.rs index fe4ff29f..2d466f79 100644 --- a/src/renderer/mod.rs +++ b/src/renderer/mod.rs @@ -110,6 +110,9 @@ pub struct ShaderProgram { /// Cell dimensions (pixels) u_cell_dim: GLint, + /// Visual bell + u_visual_bell: GLint, + /// Background pass flag /// /// Rendering is split into two passes; 1 for backgrounds, and one for text @@ -321,6 +324,7 @@ pub struct RenderApi<'a> { atlas: &'a mut Vec, program: &'a mut ShaderProgram, colors: &'a ColorList, + visual_bell: f32, } #[derive(Debug)] @@ -646,6 +650,7 @@ impl QuadRenderer { atlas: &mut self.atlas, program: &mut self.program, colors: config.color_list(), + visual_bell: 0.0, }); unsafe { @@ -708,13 +713,18 @@ impl QuadRenderer { } impl<'a> RenderApi<'a> { + pub fn set_visual_bell(&mut self, visual_bell: f32) { + self.visual_bell = visual_bell; + self.program.set_visual_bell(visual_bell); + } + pub fn clear(&self) { let color = self.colors[NamedColor::Background]; unsafe { gl::ClearColor( - color.r as f32 / 255.0, - color.g as f32 / 255.0, - color.b as f32 / 255.0, + (self.visual_bell + color.r as f32 / 255.0).min(1.0), + (self.visual_bell + color.g as f32 / 255.0).min(1.0), + (self.visual_bell + color.b as f32 / 255.0).min(1.0), 1.0 ); gl::Clear(gl::COLOR_BUFFER_BIT); @@ -736,7 +746,6 @@ impl<'a> RenderApi<'a> { } unsafe { - self.program.set_background_pass(true); gl::DrawElementsInstanced(gl::TRIANGLES, 6, gl::UNSIGNED_INT, ptr::null(), @@ -941,11 +950,12 @@ impl ShaderProgram { } // get uniform locations - let (projection, term_dim, cell_dim, background) = unsafe { + let (projection, term_dim, cell_dim, visual_bell, background) = unsafe { ( gl::GetUniformLocation(program, cptr!(b"projection\0")), gl::GetUniformLocation(program, cptr!(b"termDim\0")), gl::GetUniformLocation(program, cptr!(b"cellDim\0")), + gl::GetUniformLocation(program, cptr!(b"visualBell\0")), gl::GetUniformLocation(program, cptr!(b"backgroundPass\0")), ) }; @@ -957,6 +967,7 @@ impl ShaderProgram { u_projection: projection, u_term_dim: term_dim, u_cell_dim: cell_dim, + u_visual_bell: visual_bell, u_background: background, }; @@ -988,6 +999,12 @@ impl ShaderProgram { } } + fn set_visual_bell(&self, visual_bell: f32) { + unsafe { + gl::Uniform1f(self.u_visual_bell, visual_bell); + } + } + fn set_background_pass(&self, background_pass: bool) { let value = if background_pass { 1 diff --git a/src/term/mod.rs b/src/term/mod.rs index b0ca2a59..a7cda171 100644 --- a/src/term/mod.rs +++ b/src/term/mod.rs @@ -18,12 +18,13 @@ use std::ops::{Deref, Range, Index, IndexMut}; use std::ptr; use std::cmp::min; use std::io; +use std::time::{Duration, Instant}; use ansi::{self, Color, NamedColor, Attr, Handler, CharsetIndex, StandardCharset}; use grid::{BidirectionalIterator, Grid, ClearRegion, ToRange}; use index::{self, Point, Column, Line, Linear, IndexRange, Contains, RangeInclusive, Side}; use selection::{Span, Selection}; -use config::{Config}; +use config::{Config, VisualBellAnimation}; pub mod cell; pub use self::cell::Cell; @@ -299,6 +300,119 @@ pub struct Cursor { charsets: Charsets, } +pub struct VisualBell { + /// Visual bell animation + animation: VisualBellAnimation, + + /// Visual bell duration + duration: Duration, + + /// The last time the visual bell rang, if at all + start_time: Option, +} + +fn cubic_bezier(p0: f64, p1: f64, p2: f64, p3: f64, x: f64) -> f64 { + (1.0 - x).powi(3) * p0 + + 3.0 * (1.0 - x).powi(2) * x * p1 + + 3.0 * (1.0 - x) * x.powi(2) * p2 + + x.powi(3) * p3 +} + +impl VisualBell { + pub fn new(config: &Config) -> VisualBell { + let visual_bell_config = config.visual_bell(); + VisualBell { + animation: visual_bell_config.animation(), + duration: visual_bell_config.duration(), + start_time: None, + } + } + + /// Ring the visual bell, and return its intensity. + pub fn ring(&mut self) -> f64 { + let now = Instant::now(); + self.start_time = Some(now); + self.intensity_at_instant(now) + } + + /// Get the currenty intensity of the visual bell. The bell's intensity + /// ramps down from 1.0 to 0.0 at a rate determined by the bell's duration. + pub fn intensity(&self) -> f64 { + self.intensity_at_instant(Instant::now()) + } + + /// Check whether or not the visual bell has completed "ringing". + pub fn completed(&self) -> bool { + match self.start_time { + Some(earlier) => Instant::now().duration_since(earlier) > self.duration, + None => true + } + } + + /// Get the intensity of the visual bell at a particular instant. The bell's + /// intensity ramps down from 1.0 to 0.0 at a rate determined by the bell's + /// duration. + pub fn intensity_at_instant(&self, instant: Instant) -> f64 { + // If `duration` is zero, then the VisualBell is disabled; therefore, + // its `intensity` is zero. + if self.duration == Duration::from_secs(0) { + return 0.0; + } + + match self.start_time { + // Similarly, if `start_time` is `None`, then the VisualBell has not + // been "rung"; therefore, its `intensity` is zero. + None => 0.0, + + Some(earlier) => { + // Finally, if the `instant` at which we wish to compute the + // VisualBell's `intensity` occurred before the VisualBell was + // "rung", then its `intensity` is also zero. + if instant < earlier { + return 0.0; + } + + let elapsed = instant.duration_since(earlier); + let elapsed_f = elapsed.as_secs() as f64 + + elapsed.subsec_nanos() as f64 / 1e9f64; + let duration_f = self.duration.as_secs() as f64 + + self.duration.subsec_nanos() as f64 / 1e9f64; + + // Otherwise, we compute a value `time` from 0.0 to 1.0 + // inclusive that represents the ratio of `elapsed` time to the + // `duration` of the VisualBell. + let time = (elapsed_f / duration_f).min(1.0); + + // We use this to compute the inverse `intensity` of the + // VisualBell. When `time` is 0.0, `inverse_intensity` is 0.0, + // and when `time` is 1.0, `inverse_intensity` is 1.0. + let inverse_intensity = match self.animation { + VisualBellAnimation::Ease => cubic_bezier(0.25, 0.1, 0.25, 1.0, time), + VisualBellAnimation::EaseOut => cubic_bezier(0.25, 0.1, 0.25, 1.0, time), + VisualBellAnimation::EaseOutSine => cubic_bezier(0.39, 0.575, 0.565, 1.0, time), + VisualBellAnimation::EaseOutQuad => cubic_bezier(0.25, 0.46, 0.45, 0.94, time), + VisualBellAnimation::EaseOutCubic => cubic_bezier(0.215, 0.61, 0.355, 1.0, time), + VisualBellAnimation::EaseOutQuart => cubic_bezier(0.165, 0.84, 0.44, 1.0, time), + VisualBellAnimation::EaseOutQuint => cubic_bezier(0.23, 1.0, 0.32, 1.0, time), + VisualBellAnimation::EaseOutExpo => cubic_bezier(0.19, 1.0, 0.22, 1.0, time), + VisualBellAnimation::EaseOutCirc => cubic_bezier(0.075, 0.82, 0.165, 1.0, time), + VisualBellAnimation::Linear => time, + }; + + // Since we want the `intensity` of the VisualBell to decay over + // `time`, we subtract the `inverse_intensity` from 1.0. + 1.0 - inverse_intensity + } + } + } + + pub fn update_config(&mut self, config: &Config) { + let visual_bell_config = config.visual_bell(); + self.animation = visual_bell_config.animation(); + self.duration = visual_bell_config.duration(); + } +} + pub struct Term { /// The grid grid: Grid, @@ -345,6 +459,8 @@ pub struct Term { pub dirty: bool, + pub visual_bell: VisualBell, + custom_cursor_colors: bool, /// Saved cursor from main grid @@ -424,6 +540,7 @@ impl Term { Term { next_title: None, dirty: false, + visual_bell: VisualBell::new(config), input_needs_wrap: false, grid: grid, alt_grid: alt, @@ -445,6 +562,7 @@ impl Term { pub fn update_config(&mut self, config: &Config) { self.custom_cursor_colors = config.custom_cursor_colors(); self.semantic_escape_chars = config.selection().semantic_escape_chars.clone(); + self.visual_bell.update_config(config); } #[inline] @@ -1027,6 +1145,7 @@ impl ansi::Handler for Term { #[inline] fn bell(&mut self) { trace!("bell"); + self.visual_bell.ring(); } #[inline]