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)
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.decoration_fg[i] = c.decoration_fg
sc = self.char[i]
@ -170,6 +170,12 @@ class Line:
for i in range(cursor.x, cursor.x + sz):
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):
if self.combining_chars:
scc = self.combining_chars.copy()
@ -189,6 +195,10 @@ class Line:
dnum = min(ls - dest_start, ls)
if 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:
src_start, dest_start = at + num, at

View File

@ -543,12 +543,13 @@ class Screen(QObject):
y = self.cursor.y
if top <= y <= bottom:
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)
line = self.linebuf[y]
# TODO: Handle wide chars that get split at the right edge.
line.right_shift(x, num)
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):
"""Deletes the indicated # of characters, starting with the
@ -565,15 +566,19 @@ class Screen(QObject):
if top <= y <= bottom:
x = self.cursor.x
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.left_shift(x, num)
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
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.
@ -585,10 +590,11 @@ class Screen(QObject):
to all ``erase_*()`` and ``delete_*()`` methods.
"""
count = count or 1
for column in range(self.cursor.x,
min(self.cursor.x + count, self.columns)):
self.buffer[self.cursor.y][column] = self.cursor.attrs
x, y = self.cursor.x, self.cursor.y
# TODO: Same set of wide character questions as for delete_characters()
num = min(self.columns - x, count)
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):
"""Erases a line in a specific way.
@ -601,22 +607,30 @@ class Screen(QObject):
including cursor position.
* ``2`` -- Erases complete line.
:param bool private: when ``True`` character attributes are left
unchanged **not implemented**.
unchanged.
"""
s = n = 0
if how == 0:
# a) erase from the cursor to the end of line, including
# the cursor,
interval = range(self.cursor.x, self.columns)
s, n = self.cursor.x, self.columns - self.cursor.x
elif how == 1:
# b) erase from the beginning of the line to the cursor,
# including it,
interval = range(self.cursor.x + 1)
s, n = 0, self.cursor.x + 1
elif how == 2:
# c) erase the entire line.
interval = range(self.columns)
for column in interval:
self.buffer[self.cursor.y][column] = self.cursor.attrs
s, n = 0, self.columns
if n - s:
# TODO: Same set of questions as for delete_characters()
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):
"""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((s.cursor.x, s.cursor.y), (2, 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))))