Fix rendering of zero-width characters

Instead of rendering zero-width characters as full characters, they are
now properly rendered without advancing the cursor.

Because of performance limitations, this implementation only supports up
to 5 zero-width characters per cell. However, as a result of this
limitation there should not be any performance impact.

This fixes #1317, fixes #696 and closes #1318.
This commit is contained in:
Christian Duerr 2018-12-09 15:28:22 +00:00 committed by GitHub
parent 47f8f1bac6
commit 1cebcd660b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 97 additions and 17 deletions

View File

@ -28,6 +28,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Fixed a bad type conversion which could cause underflow on a window resize
- Alacritty now spawns a login shell on macOS, as with Terminal.app and iTerm2
- Fixed zombie processes sticking around after launching URLs
- Zero-width characters are now properly rendered without progressing the cursor
## Version 0.2.3

View File

@ -120,7 +120,7 @@ pub struct ShaderProgram {
u_background: GLint,
}
#[derive(Debug, Clone)]
#[derive(Copy, Debug, Clone)]
pub struct Glyph {
tex_id: GLuint,
top: f32,
@ -835,7 +835,11 @@ impl<'a> RenderApi<'a> {
.map(|(i, c)| RenderableCell {
line,
column: col + i,
c,
chars: {
let mut chars = [' '; cell::MAX_ZEROWIDTH_CHARS + 1];
chars[0] = c;
chars
},
bg: color,
fg: Rgb { r: 0, g: 0, b: 0 },
flags: cell::Flags::empty(),
@ -879,23 +883,40 @@ impl<'a> RenderApi<'a> {
glyph_cache.font_key
};
// Don't render text of HIDDEN cells
let chars = if cell.flags.contains(cell::Flags::HIDDEN) {
[' '; cell::MAX_ZEROWIDTH_CHARS + 1]
} else {
cell.chars
};
let mut glyph_key = GlyphKey {
font_key,
size: glyph_cache.font_size,
c: cell.c,
c: chars[0],
};
// Don't render text of HIDDEN cells
if cell.flags.contains(cell::Flags::HIDDEN) {
glyph_key.c = ' ';
}
// Add cell to batch
{
let glyph = glyph_cache.get(glyph_key, self);
self.add_render_item(&cell, glyph);
}
// Render zero-width characters
for c in (&chars[1..]).iter().filter(|c| **c != ' ') {
glyph_key.c = *c;
let mut glyph = *glyph_cache.get(glyph_key, self);
// The metrics of zero-width characters are based on rendering
// the character after the current cell, with the anchor at the
// right side of the preceding character. Since we render the
// zero-width characters inside the preceding character, the
// anchor has been moved to the right by one cell.
glyph.left += glyph_cache.metrics.average_advance as f32;
self.add_render_item(&cell, &glyph);
}
// FIXME This is a super hacky way to do underlined text. During
// a time crunch to release 0.1, this seemed like a really
// easy, clean hack.

View File

@ -15,9 +15,12 @@ use ansi::{NamedColor, Color};
use grid;
use index::Column;
// Maximum number of zerowidth characters which will be stored per cell.
pub const MAX_ZEROWIDTH_CHARS: usize = 5;
bitflags! {
#[derive(Serialize, Deserialize)]
pub struct Flags: u32 {
pub struct Flags: u16 {
const INVERSE = 0b0_0000_0001;
const BOLD = 0b0_0000_0010;
const ITALIC = 0b0_0000_0100;
@ -31,12 +34,18 @@ bitflags! {
}
}
const fn default_extra() -> [char; MAX_ZEROWIDTH_CHARS] {
[' '; MAX_ZEROWIDTH_CHARS]
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
pub struct Cell {
pub c: char,
pub fg: Color,
pub bg: Color,
pub flags: Flags,
#[serde(default="default_extra")]
pub extra: [char; MAX_ZEROWIDTH_CHARS],
}
impl Default for Cell {
@ -65,7 +74,7 @@ impl LineLength for grid::Row<Cell> {
}
for (index, cell) in self[..].iter().rev().enumerate() {
if cell.c != ' ' {
if cell.c != ' ' || cell.extra[0] != ' ' {
length = Column(self.len() - index);
break;
}
@ -93,6 +102,7 @@ impl Cell {
pub fn new(c: char, fg: Color, bg: Color) -> Cell {
Cell {
extra: [' '; MAX_ZEROWIDTH_CHARS],
c,
bg,
fg,
@ -102,9 +112,10 @@ impl Cell {
#[inline]
pub fn is_empty(&self) -> bool {
self.c == ' ' &&
self.bg == Color::Named(NamedColor::Background) &&
!self.flags.intersects(Flags::INVERSE | Flags::UNDERLINE)
self.c == ' '
&& self.extra[0] == ' '
&& self.bg == Color::Named(NamedColor::Background)
&& !self.flags.intersects(Flags::INVERSE | Flags::UNDERLINE)
}
#[inline]
@ -112,6 +123,30 @@ impl Cell {
// memcpy template to self
*self = *template;
}
#[inline]
pub fn chars(&self) -> [char; MAX_ZEROWIDTH_CHARS + 1] {
unsafe {
let mut chars = [std::mem::uninitialized(); MAX_ZEROWIDTH_CHARS + 1];
std::ptr::write(&mut chars[0], self.c);
std::ptr::copy_nonoverlapping(
self.extra.as_ptr(),
chars.as_mut_ptr().offset(1),
self.extra.len(),
);
chars
}
}
#[inline]
pub fn push_extra(&mut self, c: char) {
for elem in self.extra.iter_mut() {
if elem == &' ' {
*elem = c;
break;
}
}
}
}
#[cfg(test)]

View File

@ -424,7 +424,7 @@ pub struct RenderableCell {
/// A _Display_ line (not necessarily an _Active_ line)
pub line: Line,
pub column: Column,
pub c: char,
pub chars: [char; cell::MAX_ZEROWIDTH_CHARS + 1],
pub fg: Rgb,
pub bg: Rgb,
pub bg_alpha: f32,
@ -488,7 +488,7 @@ impl<'a> Iterator for RenderableCellsIter<'a> {
line: cell.line,
column: cell.column,
flags: cell.flags,
c: cell.c,
chars: cell.chars(),
fg: fg_rgb,
bg: bg_rgb,
bg_alpha,
@ -1030,6 +1030,9 @@ impl Term {
for cell in &grid_line[cols.start..line_end] {
if !cell.flags.contains(cell::Flags::WIDE_CHAR_SPACER) {
self.push(cell.c);
for c in (&cell.chars()[1..]).iter().filter(|c| **c != ' ') {
self.push(*c);
}
}
}
@ -1371,7 +1374,9 @@ impl ansi::Handler for Term {
let num_cols = self.grid.num_cols();
{
// If in insert mode, first shift cells to the right.
if self.mode.contains(mode::TermMode::INSERT) && self.cursor.point.col + width < num_cols {
if self.mode.contains(mode::TermMode::INSERT)
&& self.cursor.point.col + width < num_cols
{
let line = self.cursor.point.line; // borrowck
let col = self.cursor.point.col;
let line = &mut self.grid[line];
@ -1383,6 +1388,18 @@ impl ansi::Handler for Term {
ptr::copy(src, dst, (num_cols - col - width).0);
}
}
if width == 0 {
let mut col = self.cursor.point.col.0.saturating_sub(1);
let line = self.cursor.point.line;
if self.grid[line][Column(col)]
.flags
.contains(cell::Flags::WIDE_CHAR_SPACER)
{
col.saturating_sub(1);
}
self.grid[line][Column(col)].push_extra(c);
return;
}
let cell = &mut self.grid[&self.cursor.point];
*cell = self.cursor.template;
@ -1409,7 +1426,6 @@ impl ansi::Handler for Term {
} else {
self.input_needs_wrap = true;
}
}
#[inline]

View File

@ -51,6 +51,7 @@ ref_tests! {
zsh_tab_completion
history
grid_reset
zerowidth
}
fn read_u8<P>(path: P) -> Vec<u8>

View File

@ -0,0 +1,3 @@
%  UL  ~/…/tests/ref/zerowidth  zerowidth-slice  [?2004hecho "( <0361>° <035c>ʖ <0361>°)"echo "( <0361>° <035c>ʖ <0361>°)"[?2004l
( ͡° ͜ʖ ͡°)
%  UL  ~/…/tests/ref/zerowidth  zerowidth-slice  [?2004h

View File

@ -0,0 +1 @@
{"history_size":1000}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
{"width":939.0,"height":503.0,"cell_width":8.0,"cell_height":16.0,"padding_x":5.0,"padding_y":3.0,"dpr":1.0}