Implement character manipulation APIs

This commit is contained in:
Kovid Goyal 2016-10-19 11:25:55 +05:30
parent 3ae0e4e5ac
commit 56bff2f4a7
3 changed files with 102 additions and 17 deletions

View File

@ -129,7 +129,7 @@ class Line:
((c.italic & 0b1) << ITALIC_SHIFT) | ((c.reverse & 0b1) << REVERSE_SHIFT) | ((c.strikethrough & 0b1) << STRIKE_SHIFT) ((c.italic & 0b1) << ITALIC_SHIFT) | ((c.reverse & 0b1) << REVERSE_SHIFT) | ((c.strikethrough & 0b1) << STRIKE_SHIFT)
def apply_cursor(self, c: Cursor, at: int=0, num: int=1, clear_char=False, char=' ') -> None: def apply_cursor(self, c: Cursor, at: int=0, num: int=1, clear_char=False, char=' ') -> None:
for i in range(at, at + num): for i in range(at, min(len(self), at + num)):
self.color[i] = ((c.bg & COL_MASK) << COL_SHIFT) | (c.fg & COL_MASK) self.color[i] = ((c.bg & COL_MASK) << COL_SHIFT) | (c.fg & COL_MASK)
self.decoration_fg[i] = c.decoration_fg self.decoration_fg[i] = c.decoration_fg
sc = self.char[i] sc = self.char[i]
@ -170,6 +170,12 @@ class Line:
for i in range(cursor.x, cursor.x + sz): for i in range(cursor.x, cursor.x + sz):
self.combining_chars.pop(i, None) self.combining_chars.pop(i, None)
def clear_text(self, start, num, clear_char=' '):
' Clear the text in the specified range, preserving existing attributes '
ch = ord(clear_char) & CHAR_MASK
for i in range(start, min(len(self), start + num)):
self.char[i] = (self.char[i] & ~CHAR_MASK) | ch
def copy_slice(self, src, dest, num): def copy_slice(self, src, dest, num):
if self.combining_chars: if self.combining_chars:
scc = self.combining_chars.copy() scc = self.combining_chars.copy()
@ -189,6 +195,10 @@ class Line:
dnum = min(ls - dest_start, ls) dnum = min(ls - dest_start, ls)
if dnum: if dnum:
self.copy_slice(src_start, dest_start, dnum) self.copy_slice(src_start, dest_start, dnum)
# Check if a wide character was split at the right edge
w = (self.char[-1] >> ATTRS_SHIFT) & 0b11
if w != 1:
self.char[-1] = (w << ATTRS_SHIFT) | ord(' ')
def left_shift(self, at: int, num: int) -> None: def left_shift(self, at: int, num: int) -> None:
src_start, dest_start = at + num, at src_start, dest_start = at + num, at

View File

@ -543,12 +543,13 @@ class Screen(QObject):
y = self.cursor.y y = self.cursor.y
if top <= y <= bottom: if top <= y <= bottom:
x = self.cursor.x x = self.cursor.x
# TODO: Check what to do if x is on the second char of a wide char
# pair.
num = min(self.columns - x, count) num = min(self.columns - x, count)
line = self.linebuf[y] line = self.linebuf[y]
# TODO: Handle wide chars that get split at the right edge.
line.right_shift(x, num) line.right_shift(x, num)
line.apply_cursor(self.cursor, x, num, clear_char=True) line.apply_cursor(self.cursor, x, num, clear_char=True)
self.update_cell_range(y, x, self.columns) self.update_cell_range(y, x, self.columns - 1)
def delete_characters(self, count=1): def delete_characters(self, count=1):
"""Deletes the indicated # of characters, starting with the """Deletes the indicated # of characters, starting with the
@ -565,15 +566,19 @@ class Screen(QObject):
if top <= y <= bottom: if top <= y <= bottom:
x = self.cursor.x x = self.cursor.x
num = min(self.columns - x, count) num = min(self.columns - x, count)
# TODO: Handle deletion of wide chars # TODO: Check if we need to count wide chars as one or two chars
# for this control code. Also, what happens if we start deleting
# at the second cell of a wide character, or delete only the first
# cell of a wide character?
line = self.linebuf[y] line = self.linebuf[y]
line.left_shift(x, num) line.left_shift(x, num)
line.apply_cursor(self.cursor, self.columns - num, num, clear_char=True) line.apply_cursor(self.cursor, self.columns - num, num, clear_char=True)
self.update_cell_range(y, x, self.columns - 1)
def erase_characters(self, count=None): def erase_characters(self, count=1):
"""Erases the indicated # of characters, starting with the """Erases the indicated # of characters, starting with the
character at cursor position. Character attributes are set character at cursor position. Character attributes are set
cursor attributes. The cursor remains in the same position. to cursor attributes. The cursor remains in the same position.
:param int count: number of characters to erase. :param int count: number of characters to erase.
@ -585,10 +590,11 @@ class Screen(QObject):
to all ``erase_*()`` and ``delete_*()`` methods. to all ``erase_*()`` and ``delete_*()`` methods.
""" """
count = count or 1 count = count or 1
x, y = self.cursor.x, self.cursor.y
for column in range(self.cursor.x, # TODO: Same set of wide character questions as for delete_characters()
min(self.cursor.x + count, self.columns)): num = min(self.columns - x, count)
self.buffer[self.cursor.y][column] = self.cursor.attrs self.linebuf[y].apply_cursor(self.cursor, x, num, clear_char=True)
self.update_cell_range(y, x, min(x + num, self.columns) - 1)
def erase_in_line(self, how=0, private=False): def erase_in_line(self, how=0, private=False):
"""Erases a line in a specific way. """Erases a line in a specific way.
@ -601,22 +607,30 @@ class Screen(QObject):
including cursor position. including cursor position.
* ``2`` -- Erases complete line. * ``2`` -- Erases complete line.
:param bool private: when ``True`` character attributes are left :param bool private: when ``True`` character attributes are left
unchanged **not implemented**. unchanged.
""" """
s = n = 0
if how == 0: if how == 0:
# a) erase from the cursor to the end of line, including # a) erase from the cursor to the end of line, including
# the cursor, # the cursor,
interval = range(self.cursor.x, self.columns) s, n = self.cursor.x, self.columns - self.cursor.x
elif how == 1: elif how == 1:
# b) erase from the beginning of the line to the cursor, # b) erase from the beginning of the line to the cursor,
# including it, # including it,
interval = range(self.cursor.x + 1) s, n = 0, self.cursor.x + 1
elif how == 2: elif how == 2:
# c) erase the entire line. # c) erase the entire line.
interval = range(self.columns) s, n = 0, self.columns
if n - s:
for column in interval: # TODO: Same set of questions as for delete_characters()
self.buffer[self.cursor.y][column] = self.cursor.attrs y = self.cursor.y
line = self.linebuf[y]
c = None if private else self.cursor
if private:
line.clear_text(s, n)
else:
line.apply_cursor(c, s, n, clear_char=True)
self.update_cell_range(y, s, min(s + n, self.columns - 1))
def erase_in_display(self, how=0, private=False): def erase_in_display(self, how=0, private=False):
"""Erases display in a specific way. """Erases display in a specific way.

View File

@ -100,3 +100,64 @@ class TestScreen(BaseTest):
self.ae(str(s.linebuf[4]), 'a\u0306b1\u030623') self.ae(str(s.linebuf[4]), 'a\u0306b1\u030623')
self.ae((s.cursor.x, s.cursor.y), (2, 4)) self.ae((s.cursor.x, s.cursor.y), (2, 4))
self.assertChanges(t, ignore='cursor', cells={4: ((0, 4),)}) self.assertChanges(t, ignore='cursor', cells={4: ((0, 4),)})
def test_char_manipulation(self):
s, t = self.create_screen()
def init():
s.reset(), t.reset()
s.draw(b'abcde')
s.cursor.bold = True
s.cursor_back(4)
t.reset()
self.ae(s.cursor.x, 1)
init()
s.insert_characters(2)
self.ae(str(s.linebuf[0]), 'a bc')
self.assertTrue(s.linebuf[0].cursor_from(1).bold)
self.assertChanges(t, ignore='cursor', cells={0: ((1, 4),)})
s.cursor_back(1)
s.insert_characters(20)
self.ae(str(s.linebuf[0]), ' ')
self.assertChanges(t, ignore='cursor', cells={0: ((0, 4),)})
s.draw('xココ'.encode('utf-8'))
s.cursor_back(5)
t.reset()
s.insert_characters(1)
self.ae(str(s.linebuf[0]), ' xコ ')
self.assertChanges(t, ignore='cursor', cells={0: ((0, 4),)})
init()
s.delete_characters(2)
self.ae(str(s.linebuf[0]), 'ade ')
self.assertTrue(s.linebuf[0].cursor_from(4).bold)
self.assertFalse(s.linebuf[0].cursor_from(2).bold)
self.assertChanges(t, ignore='cursor', cells={0: ((1, 4),)})
init()
s.erase_characters(2)
self.ae(str(s.linebuf[0]), 'a de')
self.assertTrue(s.linebuf[0].cursor_from(1).bold)
self.assertFalse(s.linebuf[0].cursor_from(4).bold)
self.assertChanges(t, cells={0: ((1, 2),)})
s.erase_characters(20)
self.ae(str(s.linebuf[0]), 'a ')
init()
s.erase_in_line()
self.ae(str(s.linebuf[0]), 'a ')
self.assertTrue(s.linebuf[0].cursor_from(1).bold)
self.assertFalse(s.linebuf[0].cursor_from(0).bold)
self.assertChanges(t, cells={0: ((1, 4),)})
init()
s.erase_in_line(1)
self.ae(str(s.linebuf[0]), ' cde')
self.assertChanges(t, cells={0: ((0, 2),)})
init()
s.erase_in_line(2)
self.ae(str(s.linebuf[0]), ' ')
self.assertChanges(t, cells={0: ((0, 4),)})
init()
s.erase_in_line(2, private=True)
self.ae((False, False, False, False, False), tuple(map(lambda i: s.linebuf[0].cursor_from(i).bold, range(5))))