Fix cut off full width glyphs in last column

This resolves the issue with full width glyphs getting rendered in the
last column. Since they need at least two glyphs, it is not possible to
properly render them in the last column.

Instead of rendering half of the glyph in the last column, with the
other half cut off, an additional spacer is now inserted before the wide
glyph. This means that the specific glyph in question is then three
cells wide.

Fixes #2385.
This commit is contained in:
Christian Duerr 2020-01-09 23:06:41 +00:00 committed by GitHub
parent 5651c3f711
commit 3fb631b91c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 351 additions and 220 deletions

View File

@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed ### Fixed
- Incorrect default config path in `--help` on Windows and macOS - Incorrect default config path in `--help` on Windows and macOS
- Semantic selection stopping at full-width glyphs
- Full-width glyphs cut off in last column
## 0.4.1 ## 0.4.1

View File

@ -42,7 +42,7 @@ impl Url {
} }
pub fn end(&self) -> Point { pub fn end(&self) -> Point {
self.lines[self.lines.len() - 1].end.sub(self.num_cols, self.end_offset as usize) self.lines[self.lines.len() - 1].end.sub(self.num_cols, self.end_offset as usize, false)
} }
} }
@ -73,11 +73,6 @@ impl Urls {
// Update tracked URLs // Update tracked URLs
pub fn update(&mut self, num_cols: usize, cell: RenderableCell) { pub fn update(&mut self, num_cols: usize, cell: RenderableCell) {
// Ignore double-width spacers to prevent reset
if cell.flags.contains(Flags::WIDE_CHAR_SPACER) {
return;
}
// Convert cell to character // Convert cell to character
let c = match cell.inner { let c = match cell.inner {
RenderableCellContent::Chars(chars) => chars[0], RenderableCellContent::Chars(chars) => chars[0],
@ -85,20 +80,28 @@ impl Urls {
}; };
let point: Point = cell.into(); let point: Point = cell.into();
let mut end = point; let end = point;
// Reset URL when empty cells have been skipped // Reset URL when empty cells have been skipped
if point != Point::default() && Some(point.sub(num_cols, 1)) != self.last_point { if point != Point::default() && Some(point.sub(num_cols, 1, false)) != self.last_point {
self.reset(); self.reset();
} }
// Extend by one cell for double-width characters
if cell.flags.contains(Flags::WIDE_CHAR) {
end.col += 1;
}
self.last_point = Some(end); self.last_point = Some(end);
// Extend current state if a wide char spacer is encountered
if cell.flags.contains(Flags::WIDE_CHAR_SPACER) {
if let UrlLocation::Url(_, mut end_offset) = self.state {
if end_offset != 0 {
end_offset += 1;
}
self.extend_url(point, end, cell.fg, end_offset);
}
return;
}
// Advance parser // Advance parser
let last_state = mem::replace(&mut self.state, self.locator.advance(c)); let last_state = mem::replace(&mut self.state, self.locator.advance(c));
match (self.state, last_state) { match (self.state, last_state) {

View File

@ -19,8 +19,9 @@ use std::ops::{Deref, Index, IndexMut, Range, RangeFrom, RangeFull, RangeTo};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::index::{self, Column, IndexRange, Line, Point}; use crate::index::{Column, IndexRange, Line, Point};
use crate::selection::Selection; use crate::selection::Selection;
use crate::term::cell::Flags;
mod row; mod row;
pub use self::row::Row; pub use self::row::Row;
@ -68,8 +69,8 @@ impl<T: PartialEq> ::std::cmp::PartialEq for Grid<T> {
pub trait GridCell { pub trait GridCell {
fn is_empty(&self) -> bool; fn is_empty(&self) -> bool;
fn is_wrap(&self) -> bool; fn flags(&self) -> &Flags;
fn set_wrap(&mut self, wrap: bool); fn flags_mut(&mut self) -> &mut Flags;
/// Fast equality approximation. /// Fast equality approximation.
/// ///
@ -112,10 +113,10 @@ pub struct Grid<T> {
raw: Storage<T>, raw: Storage<T>,
/// Number of columns /// Number of columns
cols: index::Column, cols: Column,
/// Number of visible lines. /// Number of visible lines.
lines: index::Line, lines: Line,
/// Offset of displayed area /// Offset of displayed area
/// ///
@ -144,7 +145,7 @@ pub enum Scroll {
} }
impl<T: GridCell + PartialEq + Copy> Grid<T> { impl<T: GridCell + PartialEq + Copy> Grid<T> {
pub fn new(lines: index::Line, cols: index::Column, scrollback: usize, template: T) -> Grid<T> { pub fn new(lines: Line, cols: Column, scrollback: usize, template: T) -> Grid<T> {
let raw = Storage::with_capacity(lines, Row::new(cols, &template)); let raw = Storage::with_capacity(lines, Row::new(cols, &template));
Grid { Grid {
raw, raw,
@ -213,8 +214,8 @@ impl<T: GridCell + PartialEq + Copy> Grid<T> {
pub fn resize( pub fn resize(
&mut self, &mut self,
reflow: bool, reflow: bool,
lines: index::Line, lines: Line,
cols: index::Column, cols: Column,
cursor_pos: &mut Point, cursor_pos: &mut Point,
template: &T, template: &T,
) { ) {
@ -259,7 +260,7 @@ impl<T: GridCell + PartialEq + Copy> Grid<T> {
/// Alacritty keeps the cursor at the bottom of the terminal as long as there /// Alacritty keeps the cursor at the bottom of the terminal as long as there
/// is scrollback available. Once scrollback is exhausted, new lines are /// is scrollback available. Once scrollback is exhausted, new lines are
/// simply added to the bottom of the screen. /// simply added to the bottom of the screen.
fn grow_lines(&mut self, new_line_count: index::Line, template: &T) { fn grow_lines(&mut self, new_line_count: Line, template: &T) {
let lines_added = new_line_count - self.lines; let lines_added = new_line_count - self.lines;
// Need to "resize" before updating buffer // Need to "resize" before updating buffer
@ -276,107 +277,173 @@ impl<T: GridCell + PartialEq + Copy> Grid<T> {
self.display_offset = self.display_offset.saturating_sub(*lines_added); self.display_offset = self.display_offset.saturating_sub(*lines_added);
} }
fn grow_cols( // Grow number of columns in each row, reflowing if necessary
&mut self, fn grow_cols(&mut self, reflow: bool, cols: Column, cursor_pos: &mut Point, template: &T) {
reflow: bool, // Check if a row needs to be wrapped
cols: index::Column, let should_reflow = |row: &Row<T>| -> bool {
cursor_pos: &mut Point, let len = Column(row.len());
template: &T, reflow && len < cols && row[len - 1].flags().contains(Flags::WRAPLINE)
) { };
let mut new_empty_lines = 0; let mut new_empty_lines = 0;
let mut new_raw: Vec<Row<T>> = Vec::with_capacity(self.raw.len()); let mut reversed: Vec<Row<T>> = Vec::with_capacity(self.raw.len());
for (i, mut row) in self.raw.drain().enumerate().rev() { for (i, mut row) in self.raw.drain().enumerate().rev() {
if let Some(last_row) = new_raw.last_mut() { // FIXME: Rust 1.39.0+ allows moving in pattern guard here
// Grow the current line if there's wrapped content available // Check if reflowing shoud be performed
if reflow let mut last_row = reversed.last_mut();
&& last_row.len() < cols.0 let last_row = match last_row {
&& last_row.last().map(GridCell::is_wrap) == Some(true) Some(ref mut last_row) if should_reflow(last_row) => last_row,
{ _ => {
// Remove wrap flag before appending additional cells reversed.push(row);
if let Some(cell) = last_row.last_mut() { continue;
cell.set_wrap(false); },
};
// Remove wrap flag before appending additional cells
if let Some(cell) = last_row.last_mut() {
cell.flags_mut().remove(Flags::WRAPLINE);
}
// Remove leading spacers when reflowing wide char to the previous line
let last_len = last_row.len();
if last_len >= 2
&& !last_row[Column(last_len - 2)].flags().contains(Flags::WIDE_CHAR)
&& last_row[Column(last_len - 1)].flags().contains(Flags::WIDE_CHAR_SPACER)
{
last_row.shrink(Column(last_len - 1));
}
// Append as many cells from the next line as possible
let len = min(row.len(), cols.0 - last_row.len());
// Insert leading spacer when there's not enough room for reflowing wide char
let mut cells = if row[Column(len - 1)].flags().contains(Flags::WIDE_CHAR) {
let mut cells = row.front_split_off(len - 1);
let mut spacer = *template;
spacer.flags_mut().insert(Flags::WIDE_CHAR_SPACER);
cells.push(spacer);
cells
} else {
row.front_split_off(len)
};
last_row.append(&mut cells);
if row.is_empty() {
let raw_len = i + 1 + reversed.len();
if raw_len < self.lines.0 || self.scroll_limit == 0 {
// Add new line and move lines up if we can't pull from history
cursor_pos.line = Line(cursor_pos.line.saturating_sub(1));
new_empty_lines += 1;
} else {
// Make sure viewport doesn't move if line is outside of the visible
// area
if i < self.display_offset {
self.display_offset = self.display_offset.saturating_sub(1);
} }
// Append as many cells from the next line as possible // Remove one line from scrollback, since we just moved it to the
let len = min(row.len(), cols.0 - last_row.len()); // viewport
let mut cells = row.front_split_off(len); self.scroll_limit = self.scroll_limit.saturating_sub(1);
last_row.append(&mut cells); self.display_offset = min(self.display_offset, self.scroll_limit);
if row.is_empty() {
let raw_len = i + 1 + new_raw.len();
if raw_len < self.lines.0 || self.scroll_limit == 0 {
// Add new line and move lines up if we can't pull from history
cursor_pos.line = Line(cursor_pos.line.saturating_sub(1));
new_empty_lines += 1;
} else {
// Make sure viewport doesn't move if line is outside of the visible
// area
if i < self.display_offset {
self.display_offset = self.display_offset.saturating_sub(1);
}
// Remove one line from scrollback, since we just moved it to the
// viewport
self.scroll_limit = self.scroll_limit.saturating_sub(1);
self.display_offset = min(self.display_offset, self.scroll_limit);
}
// Don't push line into the new buffer
continue;
} else if let Some(cell) = last_row.last_mut() {
// Set wrap flag if next line still has cells
cell.set_wrap(true);
}
} }
// Don't push line into the new buffer
continue;
} else if let Some(cell) = last_row.last_mut() {
// Set wrap flag if next line still has cells
cell.flags_mut().insert(Flags::WRAPLINE);
} }
new_raw.push(row);
}
// Add padding lines
new_raw.append(&mut vec![Row::new(cols, template); new_empty_lines]);
// Fill remaining cells and reverse iterator
let mut reversed = Vec::with_capacity(new_raw.len());
for mut row in new_raw.drain(..).rev() {
if row.len() < cols.0 {
row.grow(cols, template);
}
reversed.push(row); reversed.push(row);
} }
self.raw.replace_inner(reversed); // Add padding lines
reversed.append(&mut vec![Row::new(cols, template); new_empty_lines]);
// Fill remaining cells and reverse iterator
let mut new_raw = Vec::with_capacity(reversed.len());
for mut row in reversed.drain(..).rev() {
if row.len() < cols.0 {
row.grow(cols, template);
}
new_raw.push(row);
}
self.raw.replace_inner(new_raw);
self.cols = cols; self.cols = cols;
} }
fn shrink_cols(&mut self, reflow: bool, cols: index::Column, template: &T) { // Shrink number of columns in each row, reflowing if necessary
fn shrink_cols(&mut self, reflow: bool, cols: Column, template: &T) {
let mut new_raw = Vec::with_capacity(self.raw.len()); let mut new_raw = Vec::with_capacity(self.raw.len());
let mut buffered = None; let mut buffered = None;
for (i, mut row) in self.raw.drain().enumerate().rev() { for (i, mut row) in self.raw.drain().enumerate().rev() {
// Append lines left over from previous row
if let Some(buffered) = buffered.take() { if let Some(buffered) = buffered.take() {
row.append_front(buffered); row.append_front(buffered);
} }
let mut wrapped = row.shrink(cols); loop {
new_raw.push(row); // FIXME: Rust 1.39.0+ allows moving in pattern guard here
// Check if reflowing shoud be performed
let wrapped = row.shrink(cols);
let mut wrapped = match wrapped {
Some(_) if reflow => wrapped.unwrap(),
_ => {
new_raw.push(row);
break;
},
};
while let (Some(mut wrapped_cells), true) = (wrapped.take(), reflow) { // Insert spacer if a wide char would be wrapped into the last column
// Set line as wrapped if cells got removed if row.len() >= cols.0 && row[cols - 1].flags().contains(Flags::WIDE_CHAR) {
if let Some(cell) = new_raw.last_mut().and_then(|r| r.last_mut()) { wrapped.insert(0, row[cols - 1]);
cell.set_wrap(true);
let mut spacer = *template;
spacer.flags_mut().insert(Flags::WIDE_CHAR_SPACER);
row[cols - 1] = spacer;
} }
if Some(true) == wrapped_cells.last().map(|c| c.is_wrap() && i >= 1) // Remove wide char spacer before shrinking
&& wrapped_cells.len() < cols.0 let len = wrapped.len();
if (len == 1 || (len >= 2 && !wrapped[len - 2].flags().contains(Flags::WIDE_CHAR)))
&& wrapped[len - 1].flags().contains(Flags::WIDE_CHAR_SPACER)
{
if len == 1 {
row[cols - 1].flags_mut().insert(Flags::WRAPLINE);
new_raw.push(row);
break;
} else {
wrapped[len - 2].flags_mut().insert(Flags::WRAPLINE);
wrapped.truncate(len - 1);
}
}
new_raw.push(row);
// Set line as wrapped if cells got removed
if let Some(cell) = new_raw.last_mut().and_then(|r| r.last_mut()) {
cell.flags_mut().insert(Flags::WRAPLINE);
}
if wrapped
.last()
.map(|c| c.flags().contains(Flags::WRAPLINE) && i >= 1)
.unwrap_or(false)
&& wrapped.len() < cols.0
{ {
// Make sure previous wrap flag doesn't linger around // Make sure previous wrap flag doesn't linger around
if let Some(cell) = wrapped_cells.last_mut() { if let Some(cell) = wrapped.last_mut() {
cell.set_wrap(false); cell.flags_mut().remove(Flags::WRAPLINE);
} }
// Add removed cells to start of next row // Add removed cells to start of next row
buffered = Some(wrapped_cells); buffered = Some(wrapped);
break;
} else { } else {
// Make sure viewport doesn't move if line is outside of the visible area // Make sure viewport doesn't move if line is outside of the visible area
if i < self.display_offset { if i < self.display_offset {
@ -384,17 +451,11 @@ impl<T: GridCell + PartialEq + Copy> Grid<T> {
} }
// Make sure new row is at least as long as new width // Make sure new row is at least as long as new width
let occ = wrapped_cells.len(); let occ = wrapped.len();
if occ < cols.0 { if occ < cols.0 {
wrapped_cells.append(&mut vec![*template; cols.0 - occ]); wrapped.append(&mut vec![*template; cols.0 - occ]);
} }
let mut row = Row::from_vec(wrapped_cells, occ); row = Row::from_vec(wrapped, occ);
// Since inserted might exceed cols, we need to check it again
wrapped = row.shrink(cols);
// Add new row with all removed cells
new_raw.push(row);
// Increase scrollback history // Increase scrollback history
self.scroll_limit = min(self.scroll_limit + 1, self.max_scroll_limit); self.scroll_limit = min(self.scroll_limit + 1, self.max_scroll_limit);
@ -415,7 +476,7 @@ impl<T: GridCell + PartialEq + Copy> Grid<T> {
/// of the terminal window. /// of the terminal window.
/// ///
/// Alacritty takes the same approach. /// Alacritty takes the same approach.
fn shrink_lines(&mut self, target: index::Line) { fn shrink_lines(&mut self, target: Line) {
let prev = self.lines; let prev = self.lines;
self.selection = None; self.selection = None;
@ -429,19 +490,14 @@ impl<T: GridCell + PartialEq + Copy> Grid<T> {
/// # Panics /// # Panics
/// ///
/// This method will panic if `Line` is larger than the grid dimensions /// This method will panic if `Line` is larger than the grid dimensions
pub fn line_to_offset(&self, line: index::Line) -> usize { pub fn line_to_offset(&self, line: Line) -> usize {
assert!(line < self.num_lines()); assert!(line < self.num_lines());
*(self.num_lines() - line - 1) *(self.num_lines() - line - 1)
} }
#[inline] #[inline]
pub fn scroll_down( pub fn scroll_down(&mut self, region: &Range<Line>, positions: Line, template: &T) {
&mut self,
region: &Range<index::Line>,
positions: index::Line,
template: &T,
) {
// Whether or not there is a scrolling region active, as long as it // Whether or not there is a scrolling region active, as long as it
// starts at the top, we can do a full rotation which just involves // starts at the top, we can do a full rotation which just involves
// changing the start index. // changing the start index.
@ -482,7 +538,7 @@ impl<T: GridCell + PartialEq + Copy> Grid<T> {
/// scroll_up moves lines at the bottom towards the top /// scroll_up moves lines at the bottom towards the top
/// ///
/// This is the performance-sensitive part of scrolling. /// This is the performance-sensitive part of scrolling.
pub fn scroll_up(&mut self, region: &Range<index::Line>, positions: index::Line, template: &T) { pub fn scroll_up(&mut self, region: &Range<Line>, positions: Line, template: &T) {
if region.start == Line(0) { if region.start == Line(0) {
// Update display offset when not pinned to active area // Update display offset when not pinned to active area
if self.display_offset != 0 { if self.display_offset != 0 {
@ -570,7 +626,7 @@ impl<T: GridCell + PartialEq + Copy> Grid<T> {
#[allow(clippy::len_without_is_empty)] #[allow(clippy::len_without_is_empty)]
impl<T> Grid<T> { impl<T> Grid<T> {
#[inline] #[inline]
pub fn num_lines(&self) -> index::Line { pub fn num_lines(&self) -> Line {
self.lines self.lines
} }
@ -579,7 +635,7 @@ impl<T> Grid<T> {
} }
#[inline] #[inline]
pub fn num_cols(&self) -> index::Column { pub fn num_cols(&self) -> Column {
self.cols self.cols
} }
@ -693,11 +749,11 @@ impl<'a, T> BidirectionalIterator for GridIterator<'a, T> {
} }
/// Index active region by line /// Index active region by line
impl<T> Index<index::Line> for Grid<T> { impl<T> Index<Line> for Grid<T> {
type Output = Row<T>; type Output = Row<T>;
#[inline] #[inline]
fn index(&self, index: index::Line) -> &Row<T> { fn index(&self, index: Line) -> &Row<T> {
&self.raw[index] &self.raw[index]
} }
} }
@ -712,9 +768,9 @@ impl<T> Index<usize> for Grid<T> {
} }
} }
impl<T> IndexMut<index::Line> for Grid<T> { impl<T> IndexMut<Line> for Grid<T> {
#[inline] #[inline]
fn index_mut(&mut self, index: index::Line) -> &mut Row<T> { fn index_mut(&mut self, index: Line) -> &mut Row<T> {
&mut self.raw[index] &mut self.raw[index]
} }
} }

View File

@ -330,17 +330,20 @@ mod test {
use crate::grid::storage::Storage; use crate::grid::storage::Storage;
use crate::grid::GridCell; use crate::grid::GridCell;
use crate::index::{Column, Line}; use crate::index::{Column, Line};
use crate::term::cell::Flags;
impl GridCell for char { impl GridCell for char {
fn is_empty(&self) -> bool { fn is_empty(&self) -> bool {
*self == ' ' || *self == '\t' *self == ' ' || *self == '\t'
} }
fn is_wrap(&self) -> bool { fn flags(&self) -> &Flags {
false unimplemented!();
} }
fn set_wrap(&mut self, _wrap: bool) {} fn flags_mut(&mut self) -> &mut Flags {
unimplemented!();
}
fn fast_eq(&self, other: Self) -> bool { fn fast_eq(&self, other: Self) -> bool {
self == &other self == &other

View File

@ -24,11 +24,13 @@ impl GridCell for usize {
*self == 0 *self == 0
} }
fn is_wrap(&self) -> bool { fn flags(&self) -> &Flags {
false unimplemented!();
} }
fn set_wrap(&mut self, _wrap: bool) {} fn flags_mut(&mut self) -> &mut Flags {
unimplemented!();
}
fn fast_eq(&self, other: Self) -> bool { fn fast_eq(&self, other: Self) -> bool {
self == &other self == &other

View File

@ -44,24 +44,32 @@ impl<L> Point<L> {
#[inline] #[inline]
#[must_use = "this returns the result of the operation, without modifying the original"] #[must_use = "this returns the result of the operation, without modifying the original"]
pub fn sub(mut self, num_cols: usize, length: usize) -> Point<L> pub fn sub(mut self, num_cols: usize, length: usize, absolute_indexing: bool) -> Point<L>
where where
L: Copy + Sub<usize, Output = L>, L: Copy + Add<usize, Output = L> + Sub<usize, Output = L>,
{ {
let line_changes = f32::ceil(length.saturating_sub(self.col.0) as f32 / num_cols as f32); let line_changes = f32::ceil(length.saturating_sub(self.col.0) as f32 / num_cols as f32);
self.line = self.line - line_changes as usize; if absolute_indexing {
self.line = self.line + line_changes as usize;
} else {
self.line = self.line - line_changes as usize;
}
self.col = Column((num_cols + self.col.0 - length % num_cols) % num_cols); self.col = Column((num_cols + self.col.0 - length % num_cols) % num_cols);
self self
} }
#[inline] #[inline]
#[must_use = "this returns the result of the operation, without modifying the original"] #[must_use = "this returns the result of the operation, without modifying the original"]
pub fn add(mut self, num_cols: usize, length: usize) -> Point<L> pub fn add(mut self, num_cols: usize, length: usize, absolute_indexing: bool) -> Point<L>
where where
L: Copy + Add<usize, Output = L>, L: Copy + Add<usize, Output = L> + Sub<usize, Output = L>,
{ {
let line_changes = length.saturating_sub(self.col.0) / num_cols; let line_changes = (length + self.col.0) / num_cols;
self.line = self.line + line_changes; if absolute_indexing {
self.line = self.line - line_changes;
} else {
self.line = self.line + line_changes;
}
self.col = Column((self.col.0 + length) % num_cols); self.col = Column((self.col.0 + length) % num_cols);
self self
} }

View File

@ -183,9 +183,9 @@ impl Selection {
} }
// Clamp to visible region in grid/normal // Clamp to visible region in grid/normal
let cols = term.dimensions().col; let num_cols = term.dimensions().col;
let lines = term.dimensions().line.0 as isize; let num_lines = term.dimensions().line.0 as isize;
let (start, end) = Selection::grid_clamp(start, end, lines, cols)?; let (start, end) = Selection::grid_clamp(start, end, num_lines, num_cols)?;
let span = match *self { let span = match *self {
Selection::Simple { ref region } => { Selection::Simple { ref region } => {
@ -214,16 +214,49 @@ impl Selection {
span.map(|mut span| { span.map(|mut span| {
let grid = term.grid(); let grid = term.grid();
if span.start.col < cols // Helper for checking if cell at `point` contains `flag`
&& grid[span.start.line][span.start.col].flags.contains(Flags::WIDE_CHAR_SPACER) let flag_at = |point: Point<usize>, flag: Flags| -> bool {
{ grid[point.line][point.col].flags.contains(flag)
span.start.col = Column(span.start.col.saturating_sub(1)); };
// Include all double-width cells and placeholders at top left of selection
if span.start.col < num_cols {
// Expand from wide char spacer to wide char
if span.start.line + 1 != grid.len() || span.start.col.0 != 0 {
let prev = span.start.sub(num_cols.0, 1, true);
if flag_at(span.start, Flags::WIDE_CHAR_SPACER)
&& flag_at(prev, Flags::WIDE_CHAR)
{
span.start = prev;
}
}
// Expand from wide char to wide char spacer for linewrapping
if span.start.line + 1 != grid.len() || span.start.col.0 != 0 {
let prev = span.start.sub(num_cols.0, 1, true);
if (prev.line + 1 != grid.len() || prev.col.0 != 0)
&& flag_at(prev, Flags::WIDE_CHAR_SPACER)
&& !flag_at(prev.sub(num_cols.0, 1, true), Flags::WIDE_CHAR)
{
span.start = prev;
}
}
} }
if span.end.col.0 < cols.saturating_sub(1) // Include all double-width cells and placeholders at bottom right of selection
&& grid[span.end.line][span.end.col].flags.contains(Flags::WIDE_CHAR) if span.end.line != 0 || span.end.col < num_cols {
{ // Expand from wide char spacer for linewrapping to wide char
span.end.col += 1; if (span.end.line + 1 != grid.len() || span.end.col.0 != 0)
&& flag_at(span.end, Flags::WIDE_CHAR_SPACER)
&& !flag_at(span.end.sub(num_cols.0, 1, true), Flags::WIDE_CHAR)
{
span.end = span.end.add(num_cols.0, 1, true);
}
// Expand from wide char to wide char spacer
if flag_at(span.end, Flags::WIDE_CHAR) {
span.end = span.end.add(num_cols.0, 1, true);
}
} }
span span
@ -445,7 +478,7 @@ mod test {
assert_eq!(selection.to_span(&term(1, 1)).unwrap(), Span { assert_eq!(selection.to_span(&term(1, 1)).unwrap(), Span {
start: location, start: location,
end: location, end: location,
is_block: false, is_block: false
}); });
} }
@ -463,7 +496,7 @@ mod test {
assert_eq!(selection.to_span(&term(1, 1)).unwrap(), Span { assert_eq!(selection.to_span(&term(1, 1)).unwrap(), Span {
start: location, start: location,
end: location, end: location,
is_block: false, is_block: false
}); });
} }
@ -586,7 +619,7 @@ mod test {
assert_eq!(selection.to_span(&term(5, 10)).unwrap(), Span { assert_eq!(selection.to_span(&term(5, 10)).unwrap(), Span {
start: Point::new(2, Column(4)), start: Point::new(2, Column(4)),
end: Point::new(0, Column(4)), end: Point::new(0, Column(4)),
is_block: true, is_block: true
}); });
} }

View File

@ -67,23 +67,23 @@ impl GridCell for Cell {
&& self.extra[0] == ' ' && self.extra[0] == ' '
&& self.bg == Color::Named(NamedColor::Background) && self.bg == Color::Named(NamedColor::Background)
&& self.fg == Color::Named(NamedColor::Foreground) && self.fg == Color::Named(NamedColor::Foreground)
&& !self && !self.flags.intersects(
.flags Flags::INVERSE
.intersects(Flags::INVERSE | Flags::UNDERLINE | Flags::STRIKEOUT | Flags::WRAPLINE) | Flags::UNDERLINE
| Flags::STRIKEOUT
| Flags::WRAPLINE
| Flags::WIDE_CHAR_SPACER,
)
} }
#[inline] #[inline]
fn is_wrap(&self) -> bool { fn flags(&self) -> &Flags {
self.flags.contains(Flags::WRAPLINE) &self.flags
} }
#[inline] #[inline]
fn set_wrap(&mut self, wrap: bool) { fn flags_mut(&mut self) -> &mut Flags {
if wrap { &mut self.flags
self.flags.insert(Flags::WRAPLINE);
} else {
self.flags.remove(Flags::WRAPLINE);
}
} }
#[inline] #[inline]

View File

@ -69,7 +69,9 @@ impl<T> Search for Term<T> {
let last_col = self.grid.num_cols() - Column(1); let last_col = self.grid.num_cols() - Column(1);
while let Some(cell) = iter.prev() { while let Some(cell) = iter.prev() {
if self.semantic_escape_chars.contains(cell.c) { if !cell.flags.intersects(Flags::WIDE_CHAR | Flags::WIDE_CHAR_SPACER)
&& self.semantic_escape_chars.contains(cell.c)
{
break; break;
} }
@ -91,7 +93,9 @@ impl<T> Search for Term<T> {
let last_col = self.grid.num_cols() - 1; let last_col = self.grid.num_cols() - 1;
while let Some(cell) = iter.next() { while let Some(cell) = iter.next() {
if self.semantic_escape_chars.contains(cell.c) { if !cell.flags.intersects(Flags::WIDE_CHAR | Flags::WIDE_CHAR_SPACER)
&& self.semantic_escape_chars.contains(cell.c)
{
break; break;
} }
@ -1181,6 +1185,42 @@ impl<T> Term<T> {
pub fn clipboard(&mut self) -> &mut Clipboard { pub fn clipboard(&mut self) -> &mut Clipboard {
&mut self.clipboard &mut self.clipboard
} }
/// Insert a linebreak at the current cursor position.
#[inline]
fn wrapline(&mut self)
where
T: EventListener,
{
if !self.mode.contains(TermMode::LINE_WRAP) {
return;
}
trace!("Wrapping input");
self.grid[&self.cursor.point].flags.insert(Flags::WRAPLINE);
if (self.cursor.point.line + 1) >= self.scroll_region.end {
self.linefeed();
} else {
self.cursor.point.line += 1;
}
self.cursor.point.col = Column(0);
self.input_needs_wrap = false;
}
/// Write `c` to the cell at the cursor position.
#[inline]
fn write_at_cursor(&mut self, c: char) -> &mut Cell
where
T: EventListener,
{
let cell = &mut self.grid[&self.cursor.point];
*cell = self.cursor.template;
cell.c = self.cursor.charsets[self.active_charset].map(c);
cell
}
} }
impl<T> TermInfo for Term<T> { impl<T> TermInfo for Term<T> {
@ -1195,7 +1235,7 @@ impl<T> TermInfo for Term<T> {
} }
} }
impl<T: EventListener> ansi::Handler for Term<T> { impl<T: EventListener> Handler for Term<T> {
#[inline] #[inline]
#[cfg(not(windows))] #[cfg(not(windows))]
fn set_title(&mut self, title: &str) { fn set_title(&mut self, title: &str) {
@ -1238,77 +1278,61 @@ impl<T: EventListener> ansi::Handler for Term<T> {
self.scroll_display(Scroll::Bottom); self.scroll_display(Scroll::Bottom);
} }
if self.input_needs_wrap {
if !self.mode.contains(TermMode::LINE_WRAP) {
return;
}
trace!("Wrapping input");
{
let location = Point { line: self.cursor.point.line, col: self.cursor.point.col };
let cell = &mut self.grid[&location];
cell.flags.insert(Flags::WRAPLINE);
}
if (self.cursor.point.line + 1) >= self.scroll_region.end {
self.linefeed();
} else {
self.cursor.point.line += 1;
}
self.cursor.point.col = Column(0);
self.input_needs_wrap = false;
}
// Number of cells the char will occupy // Number of cells the char will occupy
if let Some(width) = c.width() { let width = match c.width() {
let num_cols = self.grid.num_cols(); Some(width) => width,
None => return,
};
// If in insert mode, first shift cells to the right. // Handle zero-width characters
if self.mode.contains(TermMode::INSERT) && self.cursor.point.col + width < num_cols { if width == 0 {
let line = self.cursor.point.line; let mut col = self.cursor.point.col.0.saturating_sub(1);
let col = self.cursor.point.col; let line = self.cursor.point.line;
let line = &mut self.grid[line]; if self.grid[line][Column(col)].flags.contains(Flags::WIDE_CHAR_SPACER) {
col = col.saturating_sub(1);
let src = line[col..].as_ptr();
let dst = line[(col + width)..].as_mut_ptr();
unsafe {
// memmove
ptr::copy(src, dst, (num_cols - col - width).0);
}
} }
self.grid[line][Column(col)].push_extra(c);
return;
}
// Handle zero-width characters // Move cursor to next line
if width == 0 { if self.input_needs_wrap {
let mut col = self.cursor.point.col.0.saturating_sub(1); self.wrapline();
let line = self.cursor.point.line; }
if self.grid[line][Column(col)].flags.contains(Flags::WIDE_CHAR_SPACER) {
col = col.saturating_sub(1);
}
self.grid[line][Column(col)].push_extra(c);
return;
}
let cell = &mut self.grid[&self.cursor.point]; let num_cols = self.grid.num_cols();
*cell = self.cursor.template;
cell.c = self.cursor.charsets[self.active_charset].map(c);
// Handle wide chars // If in insert mode, first shift cells to the right
if width == 2 { if self.mode.contains(TermMode::INSERT) && self.cursor.point.col + width < num_cols {
cell.flags.insert(Flags::WIDE_CHAR); let line = self.cursor.point.line;
let col = self.cursor.point.col;
let line = &mut self.grid[line];
if self.cursor.point.col + 1 < num_cols { let src = line[col..].as_ptr();
self.cursor.point.col += 1; let dst = line[(col + width)..].as_mut_ptr();
let spacer = &mut self.grid[&self.cursor.point]; unsafe {
*spacer = self.cursor.template; ptr::copy(src, dst, (num_cols - col - width).0);
spacer.flags.insert(Flags::WIDE_CHAR_SPACER);
}
} }
} }
if (self.cursor.point.col + 1) < self.grid.num_cols() { if width == 1 {
self.write_at_cursor(c);
} else {
// Insert extra placeholder before wide char if glyph doesn't fit in this row anymore
if self.cursor.point.col + 1 >= num_cols {
self.write_at_cursor(' ').flags.insert(Flags::WIDE_CHAR_SPACER);
self.wrapline();
}
// Write full width glyph to current cursor cell
self.write_at_cursor(c).flags.insert(Flags::WIDE_CHAR);
// Write spacer to cell following the wide glyph
self.cursor.point.col += 1;
self.write_at_cursor(' ').flags.insert(Flags::WIDE_CHAR_SPACER);
}
if self.cursor.point.col + 1 < num_cols {
self.cursor.point.col += 1; self.cursor.point.col += 1;
} else { } else {
self.input_needs_wrap = true; self.input_needs_wrap = true;