Always use the C library wcwidth() so that the cursor does not go out of sync
This commit is contained in:
parent
20b5534c71
commit
0cf57e2afe
@ -16,7 +16,7 @@ from freetype import (
|
|||||||
FT_LOAD_NO_HINTING, FT_PIXEL_MODE_GRAY
|
FT_LOAD_NO_HINTING, FT_PIXEL_MODE_GRAY
|
||||||
)
|
)
|
||||||
|
|
||||||
from .utils import get_logical_dpi, set_current_font_metrics
|
from .utils import get_logical_dpi, wcwidth
|
||||||
|
|
||||||
|
|
||||||
def escape_family_name(name):
|
def escape_family_name(name):
|
||||||
@ -73,7 +73,7 @@ def get_font_files(family):
|
|||||||
|
|
||||||
|
|
||||||
current_font_family = current_font_family_name = cff_size = cell_width = cell_height = baseline = None
|
current_font_family = current_font_family_name = cff_size = cell_width = cell_height = baseline = None
|
||||||
CharTexture = underline_position = underline_thickness = glyph_cache = None
|
CharTexture = underline_position = underline_thickness = None
|
||||||
alt_face_cache = {}
|
alt_face_cache = {}
|
||||||
|
|
||||||
|
|
||||||
@ -106,13 +106,12 @@ def font_for_char(char, bold=False, italic=False):
|
|||||||
|
|
||||||
def set_font_family(family, size_in_pts):
|
def set_font_family(family, size_in_pts):
|
||||||
global current_font_family, current_font_family_name, cff_size, cell_width, cell_height, CharTexture, baseline
|
global current_font_family, current_font_family_name, cff_size, cell_width, cell_height, CharTexture, baseline
|
||||||
global underline_position, underline_thickness, glyph_cache
|
global underline_position, underline_thickness
|
||||||
if current_font_family_name != family or cff_size != size_in_pts:
|
if current_font_family_name != family or cff_size != size_in_pts:
|
||||||
find_font_for_character.cache_clear()
|
find_font_for_character.cache_clear()
|
||||||
current_font_family = get_font_files(family)
|
current_font_family = get_font_files(family)
|
||||||
current_font_family_name = family
|
current_font_family_name = family
|
||||||
dpi = get_logical_dpi()
|
dpi = get_logical_dpi()
|
||||||
glyph_cache = GlyphWidthCache()
|
|
||||||
cff_size = int(64 * size_in_pts)
|
cff_size = int(64 * size_in_pts)
|
||||||
cff_size = {'width': cff_size, 'height': cff_size, 'hres': dpi[0], 'vres': dpi[1]}
|
cff_size = {'width': cff_size, 'height': cff_size, 'hres': dpi[0], 'vres': dpi[1]}
|
||||||
for fobj in current_font_family.values():
|
for fobj in current_font_family.values():
|
||||||
@ -124,7 +123,6 @@ def set_font_family(family, size_in_pts):
|
|||||||
underline_position = baseline - font_units_to_pixels(face.underline_position, face.units_per_EM, size_in_pts, dpi[1])
|
underline_position = baseline - font_units_to_pixels(face.underline_position, face.units_per_EM, size_in_pts, dpi[1])
|
||||||
underline_thickness = font_units_to_pixels(face.underline_thickness, face.units_per_EM, size_in_pts, dpi[1])
|
underline_thickness = font_units_to_pixels(face.underline_thickness, face.units_per_EM, size_in_pts, dpi[1])
|
||||||
CharTexture = ctypes.c_ubyte * (cell_width * cell_height)
|
CharTexture = ctypes.c_ubyte * (cell_width * cell_height)
|
||||||
set_current_font_metrics(glyph_cache.width)
|
|
||||||
font_for_char.cache_clear()
|
font_for_char.cache_clear()
|
||||||
alt_face_cache.clear()
|
alt_face_cache.clear()
|
||||||
return cell_width, cell_height
|
return cell_width, cell_height
|
||||||
@ -139,10 +137,16 @@ def font_units_to_pixels(x, units_per_em, size_in_pts, dpi):
|
|||||||
freetype_lock = Lock()
|
freetype_lock = Lock()
|
||||||
|
|
||||||
|
|
||||||
def render_char(text, bold=False, italic=False):
|
def render_to_bitmap(font, face, text):
|
||||||
# TODO: Handle non-normalizable combining chars. Probably need to use
|
load_char(font, face, text)
|
||||||
# harfbuzz for that
|
bitmap = face.glyph.bitmap
|
||||||
text = unicodedata.normalize('NFC', text)[0]
|
if bitmap.pixel_mode != FT_PIXEL_MODE_GRAY:
|
||||||
|
raise ValueError(
|
||||||
|
'FreeType rendered the glyph for {!r} with an unsupported pixel mode: {}'.format(text, bitmap.pixel_mode))
|
||||||
|
return bitmap
|
||||||
|
|
||||||
|
|
||||||
|
def render_char(text, bold=False, italic=False, width=1):
|
||||||
key = 'regular'
|
key = 'regular'
|
||||||
if bold:
|
if bold:
|
||||||
key = 'bi' if italic else 'bold'
|
key = 'bi' if italic else 'bold'
|
||||||
@ -157,11 +161,19 @@ def render_char(text, bold=False, italic=False):
|
|||||||
if face is None:
|
if face is None:
|
||||||
face = alt_face_cache[font] = Face(font.face)
|
face = alt_face_cache[font] = Face(font.face)
|
||||||
face.set_char_size(**cff_size)
|
face.set_char_size(**cff_size)
|
||||||
load_char(font, face, text)
|
bitmap = render_to_bitmap(font, face, text)
|
||||||
bitmap = face.glyph.bitmap
|
if width == 1 and bitmap.width > cell_width * 1.1:
|
||||||
if bitmap.pixel_mode != FT_PIXEL_MODE_GRAY:
|
# rescale the font size so that the glyph is visible in a single
|
||||||
raise ValueError(
|
# cell and hope somebody updates libc's wcwidth
|
||||||
'FreeType rendered the glyph for {!r} with an unsupported pixel mode: {}'.format(text, bitmap.pixel_mode))
|
sz = cff_size.copy()
|
||||||
|
sz['width'] = int(sz['width'] * cell_width / bitmap.width)
|
||||||
|
# Preserve aspect ratio
|
||||||
|
sz['height'] = int(sz['height'] * cell_width / bitmap.width)
|
||||||
|
try:
|
||||||
|
face.set_char_size(**sz)
|
||||||
|
bitmap = render_to_bitmap(font, face, text)
|
||||||
|
finally:
|
||||||
|
face.set_char_size(**cff_size)
|
||||||
m = face.glyph.metrics
|
m = face.glyph.metrics
|
||||||
return CharBitmap(bitmap.buffer, int(abs(m.horiBearingX) / 64),
|
return CharBitmap(bitmap.buffer, int(abs(m.horiBearingX) / 64),
|
||||||
int(abs(m.horiBearingY) / 64), int(m.horiAdvance / 64), bitmap.rows, bitmap.width)
|
int(abs(m.horiBearingY) / 64), int(m.horiAdvance / 64), bitmap.rows, bitmap.width)
|
||||||
@ -215,9 +227,13 @@ def split_char_bitmap(bitmap_char):
|
|||||||
|
|
||||||
|
|
||||||
def render_cell(text, bold=False, italic=False):
|
def render_cell(text, bold=False, italic=False):
|
||||||
bitmap_char = render_char(text, bold, italic)
|
# TODO: Handle non-normalizable combining chars. Probably need to use
|
||||||
|
# harfbuzz for that
|
||||||
|
text = unicodedata.normalize('NFC', text)[0]
|
||||||
|
width = wcwidth(text)
|
||||||
|
bitmap_char = render_char(text, bold, italic, width)
|
||||||
second = None
|
second = None
|
||||||
if is_wide_char(bitmap_char):
|
if width == 2:
|
||||||
bitmap_char, second = split_char_bitmap(bitmap_char)
|
bitmap_char, second = split_char_bitmap(bitmap_char)
|
||||||
second = place_char_in_cell(second)
|
second = place_char_in_cell(second)
|
||||||
|
|
||||||
@ -234,27 +250,6 @@ def create_cell_buffer(bitmap_char, src_start_row, dest_start_row, row_count, sr
|
|||||||
return dest
|
return dest
|
||||||
|
|
||||||
|
|
||||||
class GlyphWidthCache:
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self.clear()
|
|
||||||
|
|
||||||
def render(self, text, bold=False, italic=False):
|
|
||||||
first, second = render_cell(text, bold, italic)
|
|
||||||
self.width_map[text] = 1 if second is None else 2
|
|
||||||
|
|
||||||
def clear(self):
|
|
||||||
self.width_map = {}
|
|
||||||
|
|
||||||
def width(self, text):
|
|
||||||
try:
|
|
||||||
return self.width_map[text]
|
|
||||||
except KeyError:
|
|
||||||
pass
|
|
||||||
self.render(text)
|
|
||||||
return self.width_map[text]
|
|
||||||
|
|
||||||
|
|
||||||
def join_cells(*cells):
|
def join_cells(*cells):
|
||||||
dstride = len(cells) * cell_width
|
dstride = len(cells) * cell_width
|
||||||
ans = (ctypes.c_ubyte * (cell_height * dstride))()
|
ans = (ctypes.c_ubyte * (cell_height * dstride))()
|
||||||
@ -278,7 +273,7 @@ def cell_size():
|
|||||||
return cell_width, cell_height
|
return cell_width, cell_height
|
||||||
|
|
||||||
|
|
||||||
def test_rendering(text='\'Ping👁a⧽', sz=144, family='monospace'):
|
def test_rendering(text='\'Ping👁a⧽', sz=144, family='Ubuntu Mono for Kovid'):
|
||||||
set_font_family(family, sz)
|
set_font_family(family, sz)
|
||||||
cells = []
|
cells = []
|
||||||
for c in text:
|
for c in text:
|
||||||
|
|||||||
@ -11,7 +11,6 @@ import struct
|
|||||||
import fcntl
|
import fcntl
|
||||||
import signal
|
import signal
|
||||||
import ctypes
|
import ctypes
|
||||||
import unicodedata
|
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
from functools import lru_cache
|
from functools import lru_cache
|
||||||
from time import monotonic
|
from time import monotonic
|
||||||
@ -29,17 +28,10 @@ wcwidth_native.restype = ctypes.c_int
|
|||||||
|
|
||||||
@lru_cache(maxsize=2**13)
|
@lru_cache(maxsize=2**13)
|
||||||
def wcwidth(c: str) -> int:
|
def wcwidth(c: str) -> int:
|
||||||
if unicodedata.combining(c):
|
ans = min(2, wcwidth_native(c))
|
||||||
return 0
|
if ans == -1:
|
||||||
if wcwidth.current_font is None:
|
ans = 1
|
||||||
return min(2, wcwidth_native(c))
|
return ans
|
||||||
return wcwidth.current_font(c)
|
|
||||||
wcwidth.current_font = None
|
|
||||||
|
|
||||||
|
|
||||||
def set_current_font_metrics(current_font) -> None:
|
|
||||||
wcwidth.cache_clear()
|
|
||||||
wcwidth.current_font = current_font
|
|
||||||
|
|
||||||
|
|
||||||
def create_pty():
|
def create_pty():
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user