diff --git a/kitty/fonts/box_drawing.py b/kitty/fonts/box_drawing.py index 08ce2e500..7f66c5779 100644 --- a/kitty/fonts/box_drawing.py +++ b/kitty/fonts/box_drawing.py @@ -10,24 +10,26 @@ import math from functools import partial as p from itertools import repeat -from typing import cast, Callable +from typing import ( + Callable, Dict, Generator, Iterable, List, Optional, Sequence, Tuple, cast +) - -scale = (0.001, 1, 1.5, 2) +scale = (0.001, 1., 1.5, 2.) _dpi = 96.0 +BufType = bytearray -def set_scale(new_scale): +def set_scale(new_scale: Sequence[float]) -> None: global scale - scale = tuple(new_scale) + scale = (new_scale[0], new_scale[1], new_scale[2], new_scale[3]) -def thickness(level=1, horizontal=True): +def thickness(level: int = 1, horizontal: bool = True) -> int: pts = scale[level] return int(math.ceil(pts * (_dpi / 72.0))) -def draw_hline(buf, width, x1, x2, y, level): +def draw_hline(buf: BufType, width: int, x1: int, x2: int, y: int, level: int) -> None: ' Draw a horizontal line between [x1, x2) centered at y with the thickness given by level ' sz = thickness(level=level, horizontal=False) start = y - sz // 2 @@ -37,7 +39,7 @@ def draw_hline(buf, width, x1, x2, y, level): buf[offset + x] = 255 -def draw_vline(buf, width, y1, y2, x, level): +def draw_vline(buf: BufType, width: int, y1: int, y2: int, x: int, level: int) -> None: ' Draw a vertical line between [y1, y2) centered at x with the thickness given by level ' sz = thickness(level=level, horizontal=True) start = x - sz // 2 @@ -46,25 +48,25 @@ def draw_vline(buf, width, y1, y2, x, level): buf[x + y * width] = 255 -def half_hline(buf, width, height, level=1, which='left', extend_by=0): +def half_hline(buf: BufType, width: int, height: int, level: int = 1, which: str = 'left', extend_by: int = 0): x1, x2 = (0, extend_by + width // 2) if which == 'left' else (width // 2 - extend_by, width) draw_hline(buf, width, x1, x2, height // 2, level) -def half_vline(buf, width, height, level=1, which='top', extend_by=0): +def half_vline(buf: BufType, width: int, height: int, level: int = 1, which: str = 'top', extend_by: int = 0): y1, y2 = (0, height // 2 + extend_by) if which == 'top' else (height // 2 - extend_by, height) draw_vline(buf, width, y1, y2, width // 2, level) -def get_holes(sz, hole_sz, num): +def get_holes(sz: int, hole_sz: int, num: int) -> List[Tuple[int, ...]]: if num == 1: pts = [sz // 2] elif num == 2: ssz = (sz - 2 * hole_sz) // 3 - pts = (ssz + hole_sz // 2, 2 * ssz + hole_sz // 2 + hole_sz) + pts = [ssz + hole_sz // 2, 2 * ssz + hole_sz // 2 + hole_sz] elif num == 3: ssz = (sz - 3 * hole_sz) // 4 - pts = (ssz + hole_sz // 2, 2 * ssz + hole_sz // 2 + hole_sz, 3 * ssz + 2 * hole_sz + hole_sz // 2) + pts = [ssz + hole_sz // 2, 2 * ssz + hole_sz // 2 + hole_sz, 3 * ssz + 2 * hole_sz + hole_sz // 2] holes = [] for c in pts: holes.append(tuple(range(c - hole_sz // 2, c - hole_sz // 2 + hole_sz))) @@ -74,7 +76,7 @@ def get_holes(sz, hole_sz, num): hole_factor = 8 -def add_hholes(buf, width, height, level=1, num=1): +def add_hholes(buf: BufType, width: int, height: int, level: int = 1, num: int = 1) -> None: line_sz = thickness(level=level, horizontal=True) hole_sz = width // hole_factor start = height // 2 - line_sz // 2 @@ -86,7 +88,7 @@ def add_hholes(buf, width, height, level=1, num=1): buf[offset + x] = 0 -def add_vholes(buf, width, height, level=1, num=1): +def add_vholes(buf: BufType, width: int, height: int, level: int = 1, num: int = 1) -> None: line_sz = thickness(level=level, horizontal=False) hole_sz = height // hole_factor start = width // 2 - line_sz // 2 @@ -97,77 +99,77 @@ def add_vholes(buf, width, height, level=1, num=1): buf[x + width * y] = 0 -def hline(*a, level=1): - half_hline(*a, level=level) - half_hline(*a, level=level, which='right') +def hline(buf: BufType, width: int, height: int, level: int = 1) -> None: + half_hline(buf, width, height, level=level) + half_hline(buf, width, height, level=level, which='right') -def vline(*a, level=1): - half_vline(*a, level=level) - half_vline(*a, level=level, which='bottom') +def vline(buf: BufType, width: int, height: int, level=1) -> None: + half_vline(buf, width, height, level=level) + half_vline(buf, width, height, level=level, which='bottom') -def hholes(*a, level=1, num=1): - hline(*a, level=level) - add_hholes(*a, level=level, num=num) +def hholes(buf: BufType, width: int, height: int, level=1, num=1) -> None: + hline(buf, width, height, level=level) + add_hholes(buf, width, height, level=level, num=num) -def vholes(*a, level=1, num=1): - vline(*a, level=level) - add_vholes(*a, level=level, num=num) +def vholes(buf: BufType, width: int, height: int, level=1, num=1) -> None: + vline(buf, width, height, level=level) + add_vholes(buf, width, height, level=level, num=num) -def corner(*a, hlevel=1, vlevel=1, which=None): +def corner(buf: BufType, width: int, height: int, hlevel=1, vlevel=1, which=None) -> None: wh = 'right' if which in '┌└' else 'left' - half_hline(*a, level=hlevel, which=wh, extend_by=thickness(vlevel, horizontal=True) // 2) + half_hline(buf, width, height, level=hlevel, which=wh, extend_by=thickness(vlevel, horizontal=True) // 2) wv = 'top' if which in '└┘' else 'bottom' - half_vline(*a, level=vlevel, which=wv) + half_vline(buf, width, height, level=vlevel, which=wv) -def vert_t(*args, a=1, b=1, c=1, which=None): - half_vline(*args, level=a, which='top') - half_hline(*args, level=b, which='left' if which == '┤' else 'right') - half_vline(*args, level=c, which='bottom') +def vert_t(buf: BufType, width: int, height: int, a=1, b=1, c=1, which=None) -> None: + half_vline(buf, width, height, level=a, which='top') + half_hline(buf, width, height, level=b, which='left' if which == '┤' else 'right') + half_vline(buf, width, height, level=c, which='bottom') -def horz_t(*args, a=1, b=1, c=1, which=None): - half_hline(*args, level=a, which='left') - half_hline(*args, level=b, which='right') - half_vline(*args, level=c, which='top' if which == '┴' else 'bottom') +def horz_t(buf: BufType, width: int, height: int, a=1, b=1, c=1, which=None) -> None: + half_hline(buf, width, height, level=a, which='left') + half_hline(buf, width, height, level=b, which='right') + half_vline(buf, width, height, level=c, which='top' if which == '┴' else 'bottom') -def cross(*s, a=1, b=1, c=1, d=1): - half_hline(*s, level=a) - half_hline(*s, level=b, which='right') - half_vline(*s, level=c) - half_vline(*s, level=d, which='bottom') +def cross(buf: BufType, width: int, height: int, a=1, b=1, c=1, d=1) -> None: + half_hline(buf, width, height, level=a) + half_hline(buf, width, height, level=b, which='right') + half_vline(buf, width, height, level=c) + half_vline(buf, width, height, level=d, which='bottom') -def fill_region(buf, width, height, xlimits): +def fill_region(buf: BufType, width: int, height: int, xlimits: Iterable[Iterable[float]]) -> None: for y in range(height): offset = y * width for x, (upper, lower) in enumerate(xlimits): buf[x + offset] = 255 if upper <= y <= lower else 0 # Anti-alias the boundary, simple y-axis anti-aliasing for x, limits in enumerate(xlimits): - for y in limits: - for ypx in range(int(math.floor(y)), int(math.ceil(y)) + 1): + for yf in limits: + for ypx in range(int(math.floor(yf)), int(math.ceil(yf)) + 1): if 0 <= ypx < height: off = ypx * width + x - buf[off] = min(255, buf[off] + int((1 - abs(y - ypx)) * 255)) + buf[off] = min(255, buf[off] + int((1 - abs(yf - ypx)) * 255)) -def line_equation(x1, y1, x2, y2): +def line_equation(x1: int, y1: int, x2: int, y2: int) -> Callable[[int], float]: m = (y2 - y1) / (x2 - x1) c = y1 - m * x1 - def y(x): + def y(x: int) -> float: return m * x + c return y -def triangle(buf, width, height, left=True): +def triangle(buf: BufType, width: int, height: int, left: bool = True) -> None: ay1, by1, y2 = 0, height - 1, height // 2 if left: x1, x2 = 0, width - 1 @@ -179,23 +181,23 @@ def triangle(buf, width, height, left=True): fill_region(buf, width, height, xlimits) -def corner_triangle(buf, width, height, corner): +def corner_triangle(buf: BufType, width: int, height: int, corner: str) -> None: if corner == 'top-right' or corner == 'bottom-left': diagonal_y = line_equation(0, 0, width - 1, height - 1) if corner == 'top-right': - xlimits = [(0, diagonal_y(x)) for x in range(width)] + xlimits = [(0., diagonal_y(x)) for x in range(width)] elif corner == 'bottom-left': - xlimits = [(diagonal_y(x), height - 1) for x in range(width)] + xlimits = [(diagonal_y(x), height - 1.) for x in range(width)] else: diagonal_y = line_equation(width - 1, 0, 0, height - 1) if corner == 'top-left': - xlimits = [(0, diagonal_y(x)) for x in range(width)] + xlimits = [(0., diagonal_y(x)) for x in range(width)] elif corner == 'bottom-right': - xlimits = [(diagonal_y(x), height - 1) for x in range(width)] + xlimits = [(diagonal_y(x), height - 1.) for x in range(width)] fill_region(buf, width, height, xlimits) -def antialiased_1px_line(buf, width, height, p1, p2): +def antialiased_1px_line(buf: BufType, width: int, height: int, p1: Tuple[int, int], p2: Tuple[int, int]) -> None: # Draw an antialiased line using the Wu algorithm x1, y1 = p1 x2, y2 = p2 @@ -206,28 +208,28 @@ def antialiased_1px_line(buf, width, height, p1, p2): if steep: x1, y1, x2, y2, dx, dy = y1, x1, y2, x2, dy, dx - def p(x, y): + def p(x: int, y: int) -> Tuple[int, int]: return y, x else: - def p(x, y): + def p(x: int, y: int) -> Tuple[int, int]: return x, y if x2 < x1: x1, x2, y1, y2 = x2, x1, y2, y1 - def fpart(x): + def fpart(x: float) -> float: return x - int(x) - def rfpart(x): + def rfpart(x: float) -> float: return 1 - fpart(x) - def putpixel(p, alpha): + def putpixel(p: Tuple[int, int], alpha: float) -> None: x, y = p off = int(x + y * width) if 0 <= off < off_limit: buf[off] = int(min(buf[off] + (alpha * 255), 255)) - def draw_endpoint(pt): + def draw_endpoint(pt: Tuple[int, int]) -> int: x, y = pt xend = round(x) yend = y + grad * (xend - x) @@ -254,7 +256,7 @@ def antialiased_1px_line(buf, width, height, p1, p2): intery += grad -def antialiased_line(buf, width, height, p1, p2, level=1): +def antialiased_line(buf: BufType, width: int, height: int, p1: Tuple[int, int], p2: Tuple[int, int], level: int = 1) -> None: th = thickness(level) if th < 2: return antialiased_1px_line(buf, width, height, p1, p2) @@ -265,7 +267,7 @@ def antialiased_line(buf, width, height, p1, p2, level=1): antialiased_1px_line(buf, width, height, (x1, y1 + delta), (x2, y2 + delta)) -def cross_line(buf, width, height, left=True, level=1): +def cross_line(buf: BufType, width: int, height: int, left: bool = True, level: int = 1) -> None: if left: p1, p2 = (0, 0), (width - 1, height - 1) else: @@ -273,7 +275,7 @@ def cross_line(buf, width, height, left=True, level=1): antialiased_line(buf, width, height, p1, p2, level=level) -def half_cross_line(buf, width, height, which='tl', level=1): +def half_cross_line(buf: BufType, width: int, height: int, which: str = 'tl', level: int = 1) -> None: my = (height - 1) // 2 if which == 'tl': p1 = 0, 0 @@ -290,11 +292,14 @@ def half_cross_line(buf, width, height, which='tl', level=1): antialiased_line(buf, width, height, p1, p2, level=level) -def cubic_bezier(start, end, c1, c2): +BezierFunc = Callable[[float], float] - def bezier_eq(p0, p1, p2, p3): - def f(t): +def cubic_bezier(start: Tuple[int, int], end: Tuple[int, int], c1: Tuple[int, int], c2: Tuple[int, int]) -> Tuple[BezierFunc, BezierFunc]: + + def bezier_eq(p0: int, p1: int, p2: int, p3: int) -> BezierFunc: + + def f(t: float) -> float: tm1 = 1 - t tm1_3 = tm1 * tm1 * tm1 t_3 = t * t * t @@ -306,7 +311,7 @@ def cubic_bezier(start, end, c1, c2): return bezier_x, bezier_y -def find_bezier_for_D(width, height): +def find_bezier_for_D(width: int, height: int) -> int: cx = last_cx = width - 1 start = (0, 0) end = (0, height - 1) @@ -320,12 +325,12 @@ def find_bezier_for_D(width, height): cx += 1 -def get_bezier_limits(bezier_x, bezier_y): - start_x = bezier_x(0) +def get_bezier_limits(bezier_x: BezierFunc, bezier_y: BezierFunc) -> Generator[Tuple[float, float], None, int]: + start_x = int(bezier_x(0)) max_x = int(bezier_x(0.5)) - last_t, t_limit = 0, 0.5 + last_t, t_limit = 0., 0.5 - def find_t_for_x(x, start_t): + def find_t_for_x(x: int, start_t: float) -> float: if abs(bezier_x(start_t) - x) < 0.1: return start_t increment = t_limit - start_t @@ -354,7 +359,7 @@ def get_bezier_limits(bezier_x, bezier_y): yield upper, lower -def D(buf, width, height, left=True): +def D(buf: BufType, width: int, height: int, left: bool = True) -> None: c1x = find_bezier_for_D(width, height) start = (0, 0) end = (0, height - 1) @@ -374,7 +379,7 @@ def D(buf, width, height, left=True): buf[offset + dest_x] = mbuf[offset + src_x] -def half_dhline(buf, width, height, level=1, which='left', only=None): +def half_dhline(buf: BufType, width: int, height: int, level: int = 1, which: str = 'left', only: Optional[str] = None) -> Tuple[int, int]: x1, x2 = (0, width // 2) if which == 'left' else (width // 2, width) gap = thickness(level + 1, horizontal=False) if only != 'bottom': @@ -384,7 +389,7 @@ def half_dhline(buf, width, height, level=1, which='left', only=None): return height // 2 - gap, height // 2 + gap -def half_dvline(buf, width, height, level=1, which='top', only=None): +def half_dvline(buf: BufType, width: int, height: int, level: int = 1, which: str = 'top', only: Optional[str] = None) -> Tuple[int, int]: y1, y2 = (0, height // 2) if which == 'top' else (height // 2, height) gap = thickness(level + 1, horizontal=True) if only != 'right': @@ -394,33 +399,33 @@ def half_dvline(buf, width, height, level=1, which='top', only=None): return width // 2 - gap, width // 2 + gap -def dvline(*s, only=None, level=1): - half_dvline(*s, only=only, level=level) - return half_dvline(*s, only=only, which='bottom', level=level) +def dvline(buf: BufType, width: int, height: int, only=None, level=1): + half_dvline(buf, width, height, only=only, level=level) + return half_dvline(buf, width, height, only=only, which='bottom', level=level) -def dhline(*s, only=None, level=1): - half_dhline(*s, only=only, level=level) - return half_dhline(*s, only=only, which='bottom', level=level) +def dhline(buf: BufType, width: int, height: int, only=None, level=1): + half_dhline(buf, width, height, only=only, level=level) + return half_dhline(buf, width, height, only=only, which='bottom', level=level) -def dvcorner(*s, level=1, which='╒'): +def dvcorner(buf: BufType, width: int, height: int, level=1, which='╒'): hw = 'right' if which in '╒╘' else 'left' - half_dhline(*s, which=hw) + half_dhline(buf, width, height, which=hw) vw = 'top' if which in '╘╛' else 'bottom' gap = thickness(level + 1, horizontal=False) - half_vline(*s, which=vw, extend_by=gap // 2 + thickness(level, horizontal=False)) + half_vline(buf, width, height, which=vw, extend_by=gap // 2 + thickness(level, horizontal=False)) -def dhcorner(*s, level=1, which='╓'): +def dhcorner(buf: BufType, width: int, height: int, level=1, which='╓'): vw = 'top' if which in '╙╜' else 'bottom' - half_dvline(*s, which=vw) + half_dvline(buf, width, height, which=vw) hw = 'right' if which in '╓╙' else 'left' gap = thickness(level + 1, horizontal=True) - half_hline(*s, which=hw, extend_by=gap // 2 + thickness(level, horizontal=True)) + half_hline(buf, width, height, which=hw, extend_by=gap // 2 + thickness(level, horizontal=True)) -def dcorner(buf, width, height, level=1, which='╔'): +def dcorner(buf: BufType, width: int, height: int, level: int = 1, which: str = '╔') -> None: hw = 'right' if which in '╔╚' else 'left' vw = 'top' if which in '╚╝' else 'bottom' hgap = thickness(level + 1, horizontal=False) @@ -452,18 +457,18 @@ def dcorner(buf, width, height, level=1, which='╔'): draw_vline(buf, width, y1, y2, width // 2 + xdelta, level) -def dpip(*a, level=1, which='╟'): +def dpip(buf: BufType, width: int, height: int, level: int = 1, which: str = '╟') -> None: if which in '╟╢': - left, right = dvline(*a) - x1, x2 = (0, left) if which == '╢' else (right, a[1]) - draw_hline(a[0], a[1], x1, x2, a[2] // 2, level) + left, right = dvline(buf, width, height) + x1, x2 = (0, left) if which == '╢' else (right, width) + draw_hline(buf, width, x1, x2, height // 2, level) else: - top, bottom = dhline(*a) - y1, y2 = (0, top) if which == '╧' else (bottom, a[2]) - draw_vline(a[0], a[1], y1, y2, a[1] // 2, level) + top, bottom = dhline(buf, width, height) + y1, y2 = (0, top) if which == '╧' else (bottom, height) + draw_vline(buf, width, y1, y2, width // 2, level) -def inner_corner(buf, width, height, which='tl', level=1): +def inner_corner(buf: BufType, width: int, height: int, which: str = 'tl', level: int = 1) -> None: hgap = thickness(level + 1, horizontal=True) vgap = thickness(level + 1, horizontal=False) vthick = thickness(level, horizontal=True) // 2 @@ -475,7 +480,7 @@ def inner_corner(buf, width, height, which='tl', level=1): draw_vline(buf, width, y1, y2, width // 2 + (xd * hgap), level) -def vblock(buf, width, height, frac=1, gravity='top'): +def vblock(buf: BufType, width: int, height: int, frac: float = 1., gravity: str = 'top') -> None: num_rows = min(height, round(frac * height)) start = 0 if gravity == 'top' else height - num_rows for r in range(start, start + num_rows): @@ -484,7 +489,7 @@ def vblock(buf, width, height, frac=1, gravity='top'): buf[c] = 255 -def hblock(buf, width, height, frac=1, gravity='left'): +def hblock(buf: BufType, width: int, height: int, frac: float = 1., gravity: str = 'left') -> None: num_cols = min(width, round(frac * width)) start = 0 if gravity == 'left' else width - num_cols for r in range(height): @@ -493,7 +498,7 @@ def hblock(buf, width, height, frac=1, gravity='left'): buf[c] = 255 -def shade(buf, width, height, light=False, invert=False): +def shade(buf: BufType, width: int, height: int, light: bool = False, invert: bool = False) -> None: square_sz = max(1, width // 12) number_of_rows = height // square_sz number_of_cols = width // square_sz @@ -544,7 +549,7 @@ def quad(buf, width, height, x=0, y=0): buf[off + c] = 255 -box_chars = { +box_chars: Dict[str, List[Callable]] = { '─': [hline], '━': [p(hline, level=3)], '│': [vline], @@ -660,7 +665,7 @@ for chars, func_ in (('╒╕╘╛', dvcorner), ('╓╖╙╜', dhcorner), (' box_chars[ch] = [p(cast(Callable, func_), which=ch)] -def render_box_char(ch, buf, width, height, dpi=96.0): +def render_box_char(ch: str, buf: BufType, width: int, height: int, dpi: float = 96.0) -> BufType: global _dpi _dpi = dpi for func in box_chars[ch]: @@ -668,7 +673,7 @@ def render_box_char(ch, buf, width, height, dpi=96.0): return buf -def render_missing_glyph(buf, width, height): +def render_missing_glyph(buf: BufType, width: int, height: int) -> None: hgap = thickness(level=0, horizontal=True) + 1 vgap = thickness(level=0, horizontal=False) + 1 draw_hline(buf, width, hgap, width - hgap + 1, vgap, 0) @@ -677,7 +682,7 @@ def render_missing_glyph(buf, width, height): draw_vline(buf, width, vgap, height - vgap + 1, width - hgap, 0) -def test_char(ch, sz=48): +def test_char(ch: str, sz: int = 48) -> None: # kitty +runpy "from kitty.fonts.box_drawing import test_char; test_char('XXX')" from .render import display_bitmap, setup_for_testing from kitty.fast_data_types import concat_cells, set_send_sprite_to_gpu @@ -697,7 +702,7 @@ def test_char(ch, sz=48): set_send_sprite_to_gpu(None) -def test_drawing(sz=48, family='monospace'): +def test_drawing(sz: int = 48, family: str = 'monospace') -> None: from .render import display_bitmap, setup_for_testing from kitty.fast_data_types import concat_cells, set_send_sprite_to_gpu diff --git a/kitty_tests/fonts.py b/kitty_tests/fonts.py index cda77d640..c48f7ded2 100644 --- a/kitty_tests/fonts.py +++ b/kitty_tests/fonts.py @@ -50,11 +50,11 @@ class Rendering(BaseTest): def test_box_drawing(self): prerendered = len(self.sprites) - s = self.create_screen(cols=len(box_chars), lines=1, scrollback=0) + s = self.create_screen(cols=len(box_chars) + 1, lines=1, scrollback=0) s.draw(''.join(box_chars)) line = s.line(0) test_render_line(line) - self.assertEqual(len(self.sprites), prerendered + len(box_chars)) + self.assertEqual(len(self.sprites) - prerendered, len(box_chars)) def test_font_rendering(self): render_string('ab\u0347\u0305你好|\U0001F601|\U0001F64f|\U0001F63a|')