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:
parent
47f8f1bac6
commit
1cebcd660b
|
@ -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
|
- 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
|
- Alacritty now spawns a login shell on macOS, as with Terminal.app and iTerm2
|
||||||
- Fixed zombie processes sticking around after launching URLs
|
- Fixed zombie processes sticking around after launching URLs
|
||||||
|
- Zero-width characters are now properly rendered without progressing the cursor
|
||||||
|
|
||||||
## Version 0.2.3
|
## Version 0.2.3
|
||||||
|
|
||||||
|
|
|
@ -120,7 +120,7 @@ pub struct ShaderProgram {
|
||||||
u_background: GLint,
|
u_background: GLint,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Copy, Debug, Clone)]
|
||||||
pub struct Glyph {
|
pub struct Glyph {
|
||||||
tex_id: GLuint,
|
tex_id: GLuint,
|
||||||
top: f32,
|
top: f32,
|
||||||
|
@ -835,7 +835,11 @@ impl<'a> RenderApi<'a> {
|
||||||
.map(|(i, c)| RenderableCell {
|
.map(|(i, c)| RenderableCell {
|
||||||
line,
|
line,
|
||||||
column: col + i,
|
column: col + i,
|
||||||
c,
|
chars: {
|
||||||
|
let mut chars = [' '; cell::MAX_ZEROWIDTH_CHARS + 1];
|
||||||
|
chars[0] = c;
|
||||||
|
chars
|
||||||
|
},
|
||||||
bg: color,
|
bg: color,
|
||||||
fg: Rgb { r: 0, g: 0, b: 0 },
|
fg: Rgb { r: 0, g: 0, b: 0 },
|
||||||
flags: cell::Flags::empty(),
|
flags: cell::Flags::empty(),
|
||||||
|
@ -879,23 +883,40 @@ impl<'a> RenderApi<'a> {
|
||||||
glyph_cache.font_key
|
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 {
|
let mut glyph_key = GlyphKey {
|
||||||
font_key,
|
font_key,
|
||||||
size: glyph_cache.font_size,
|
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
|
// Add cell to batch
|
||||||
{
|
{
|
||||||
let glyph = glyph_cache.get(glyph_key, self);
|
let glyph = glyph_cache.get(glyph_key, self);
|
||||||
self.add_render_item(&cell, glyph);
|
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
|
// FIXME This is a super hacky way to do underlined text. During
|
||||||
// a time crunch to release 0.1, this seemed like a really
|
// a time crunch to release 0.1, this seemed like a really
|
||||||
// easy, clean hack.
|
// easy, clean hack.
|
||||||
|
|
|
@ -15,9 +15,12 @@ use ansi::{NamedColor, Color};
|
||||||
use grid;
|
use grid;
|
||||||
use index::Column;
|
use index::Column;
|
||||||
|
|
||||||
|
// Maximum number of zerowidth characters which will be stored per cell.
|
||||||
|
pub const MAX_ZEROWIDTH_CHARS: usize = 5;
|
||||||
|
|
||||||
bitflags! {
|
bitflags! {
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
pub struct Flags: u32 {
|
pub struct Flags: u16 {
|
||||||
const INVERSE = 0b0_0000_0001;
|
const INVERSE = 0b0_0000_0001;
|
||||||
const BOLD = 0b0_0000_0010;
|
const BOLD = 0b0_0000_0010;
|
||||||
const ITALIC = 0b0_0000_0100;
|
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)]
|
#[derive(Copy, Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
|
||||||
pub struct Cell {
|
pub struct Cell {
|
||||||
pub c: char,
|
pub c: char,
|
||||||
pub fg: Color,
|
pub fg: Color,
|
||||||
pub bg: Color,
|
pub bg: Color,
|
||||||
pub flags: Flags,
|
pub flags: Flags,
|
||||||
|
#[serde(default="default_extra")]
|
||||||
|
pub extra: [char; MAX_ZEROWIDTH_CHARS],
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Cell {
|
impl Default for Cell {
|
||||||
|
@ -65,7 +74,7 @@ impl LineLength for grid::Row<Cell> {
|
||||||
}
|
}
|
||||||
|
|
||||||
for (index, cell) in self[..].iter().rev().enumerate() {
|
for (index, cell) in self[..].iter().rev().enumerate() {
|
||||||
if cell.c != ' ' {
|
if cell.c != ' ' || cell.extra[0] != ' ' {
|
||||||
length = Column(self.len() - index);
|
length = Column(self.len() - index);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -93,6 +102,7 @@ impl Cell {
|
||||||
|
|
||||||
pub fn new(c: char, fg: Color, bg: Color) -> Cell {
|
pub fn new(c: char, fg: Color, bg: Color) -> Cell {
|
||||||
Cell {
|
Cell {
|
||||||
|
extra: [' '; MAX_ZEROWIDTH_CHARS],
|
||||||
c,
|
c,
|
||||||
bg,
|
bg,
|
||||||
fg,
|
fg,
|
||||||
|
@ -102,9 +112,10 @@ impl Cell {
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn is_empty(&self) -> bool {
|
pub fn is_empty(&self) -> bool {
|
||||||
self.c == ' ' &&
|
self.c == ' '
|
||||||
self.bg == Color::Named(NamedColor::Background) &&
|
&& self.extra[0] == ' '
|
||||||
!self.flags.intersects(Flags::INVERSE | Flags::UNDERLINE)
|
&& self.bg == Color::Named(NamedColor::Background)
|
||||||
|
&& !self.flags.intersects(Flags::INVERSE | Flags::UNDERLINE)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
|
@ -112,6 +123,30 @@ impl Cell {
|
||||||
// memcpy template to self
|
// memcpy template to self
|
||||||
*self = *template;
|
*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)]
|
#[cfg(test)]
|
||||||
|
|
|
@ -424,7 +424,7 @@ pub struct RenderableCell {
|
||||||
/// A _Display_ line (not necessarily an _Active_ line)
|
/// A _Display_ line (not necessarily an _Active_ line)
|
||||||
pub line: Line,
|
pub line: Line,
|
||||||
pub column: Column,
|
pub column: Column,
|
||||||
pub c: char,
|
pub chars: [char; cell::MAX_ZEROWIDTH_CHARS + 1],
|
||||||
pub fg: Rgb,
|
pub fg: Rgb,
|
||||||
pub bg: Rgb,
|
pub bg: Rgb,
|
||||||
pub bg_alpha: f32,
|
pub bg_alpha: f32,
|
||||||
|
@ -488,7 +488,7 @@ impl<'a> Iterator for RenderableCellsIter<'a> {
|
||||||
line: cell.line,
|
line: cell.line,
|
||||||
column: cell.column,
|
column: cell.column,
|
||||||
flags: cell.flags,
|
flags: cell.flags,
|
||||||
c: cell.c,
|
chars: cell.chars(),
|
||||||
fg: fg_rgb,
|
fg: fg_rgb,
|
||||||
bg: bg_rgb,
|
bg: bg_rgb,
|
||||||
bg_alpha,
|
bg_alpha,
|
||||||
|
@ -1030,6 +1030,9 @@ impl Term {
|
||||||
for cell in &grid_line[cols.start..line_end] {
|
for cell in &grid_line[cols.start..line_end] {
|
||||||
if !cell.flags.contains(cell::Flags::WIDE_CHAR_SPACER) {
|
if !cell.flags.contains(cell::Flags::WIDE_CHAR_SPACER) {
|
||||||
self.push(cell.c);
|
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();
|
let num_cols = self.grid.num_cols();
|
||||||
{
|
{
|
||||||
// If in insert mode, first shift cells to the right.
|
// 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 line = self.cursor.point.line; // borrowck
|
||||||
let col = self.cursor.point.col;
|
let col = self.cursor.point.col;
|
||||||
let line = &mut self.grid[line];
|
let line = &mut self.grid[line];
|
||||||
|
@ -1383,6 +1388,18 @@ impl ansi::Handler for Term {
|
||||||
ptr::copy(src, dst, (num_cols - col - width).0);
|
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];
|
let cell = &mut self.grid[&self.cursor.point];
|
||||||
*cell = self.cursor.template;
|
*cell = self.cursor.template;
|
||||||
|
@ -1409,7 +1426,6 @@ impl ansi::Handler for Term {
|
||||||
} else {
|
} else {
|
||||||
self.input_needs_wrap = true;
|
self.input_needs_wrap = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
|
|
|
@ -51,6 +51,7 @@ ref_tests! {
|
||||||
zsh_tab_completion
|
zsh_tab_completion
|
||||||
history
|
history
|
||||||
grid_reset
|
grid_reset
|
||||||
|
zerowidth
|
||||||
}
|
}
|
||||||
|
|
||||||
fn read_u8<P>(path: P) -> Vec<u8>
|
fn read_u8<P>(path: P) -> Vec<u8>
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
[1m[7m%[27m[1m[0m
[0m[27m[24m[J[0;30;101m UL [0m[0;37;100m ~/…/tests/ref/zerowidth [0m[0;30;101m zerowidth-slice [0m [K[?2004he[90mcho "( [7m[90m<0361>[27m[39m[90m° [7m[90m<035c>[27m[39m[90mʖ [7m[90m<0361>[27m[39m[90m°)"[39m[32De[39mc[39mh[39mo[39m [39m"[39m([39m [39m[7m<[39m[7m0[39m[7m3[39m[7m6[39m[7m1[39m[7m>[27m[39m°[39m [39m[7m<[39m[7m0[39m[7m3[39m[7m5[39m[7mc[39m[7m>[27m[39mʖ[39m [39m[7m<[39m[7m0[39m[7m3[39m[7m6[39m[7m1[39m[7m>[27m[39m°[39m)[39m"[?2004l
|
||||||
|
( ͡° ͜ʖ ͡°)
|
||||||
|
[1m[7m%[27m[1m[0m
[0m[27m[24m[J[0;30;101m UL [0m[0;37;100m ~/…/tests/ref/zerowidth [0m[0;30;101m zerowidth-slice [0m [K[?2004h
|
|
@ -0,0 +1 @@
|
||||||
|
{"history_size":1000}
|
File diff suppressed because one or more lines are too long
|
@ -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}
|
Loading…
Reference in New Issue