diff --git a/kitty/boss.py b/kitty/boss.py index 44be2a550..7cfc64884 100644 --- a/kitty/boss.py +++ b/kitty/boss.py @@ -11,7 +11,7 @@ from .fast_data_types import ( GLFW_KEY_DOWN, GLFW_KEY_UP, ChildMonitor, destroy_global_data, destroy_sprite_map, glfw_post_empty_event, layout_sprite_map ) -from .fonts.render import render_cell_wrapper, set_font_family +from .fonts.render import render_cell_wrapper, set_font_family, resize_fonts from .keys import get_key_map, get_sent_data, get_shortcut from .session import create_session from .tabs import SpecialWindow, TabManager @@ -142,8 +142,7 @@ class Boss: self.current_font_size = new_size w, h = cell_size.width, cell_size.height windows = tuple(filter(None, self.window_id_map.values())) - cell_size.width, cell_size.height = set_font_family( - self.opts, override_font_size=self.current_font_size) + cell_size.width, cell_size.height = resize_fonts(self.current_font_size) layout_sprite_map(cell_size.width, cell_size.height, render_cell_wrapper) for window in windows: window.screen.rescale_images(w, h) diff --git a/kitty/config.py b/kitty/config.py index 1edd3b8d3..49dce121a 100644 --- a/kitty/config.py +++ b/kitty/config.py @@ -165,8 +165,7 @@ def parse_symbol_map(val): return abort() if b < a or max(a, b) > sys.maxunicode or min(a, b) < 1: return abort() - for y in range(a, b + 1): - symbol_map[chr(y)] = family + symbol_map[(a, b)] = family return symbol_map diff --git a/kitty/data-types.c b/kitty/data-types.c index ba6eb2373..2775a441c 100644 --- a/kitty/data-types.c +++ b/kitty/data-types.c @@ -157,6 +157,7 @@ extern int init_Screen(PyObject *); extern int init_Face(PyObject *); extern bool init_freetype_library(PyObject*); extern bool init_fontconfig_library(PyObject*); +extern bool init_fonts(PyObject*); extern bool init_glfw(PyObject *m); extern bool init_sprites(PyObject *module); extern bool init_state(PyObject *module); @@ -201,6 +202,7 @@ PyInit_fast_data_types(void) { if (!init_freetype_library(m)) return NULL; if (!init_fontconfig_library(m)) return NULL; #endif + if (!init_fonts(m)) return NULL; #define OOF(n) #n, offsetof(Cell, n) if (PyModule_AddObject(m, "CELL", Py_BuildValue("{sI sI sI sI sI sI sI sI sI}", diff --git a/kitty/data-types.h b/kitty/data-types.h index 37ccbf8c8..ffb4cab24 100644 --- a/kitty/data-types.h +++ b/kitty/data-types.h @@ -45,6 +45,7 @@ typedef enum MouseShapes { BEAM, HAND, ARROW } MouseShape; #define DECORATION_MASK 3 #define BOLD_SHIFT 4 #define ITALIC_SHIFT 5 +#define BI_VAL(attrs) ((attrs >> 4) & 3) #define REVERSE_SHIFT 6 #define STRIKE_SHIFT 7 #define COL_MASK 0xFFFFFFFF @@ -73,6 +74,7 @@ typedef enum MouseShapes { BEAM, HAND, ARROW } MouseShape; #define COPY_SELF_CELL(s, d) COPY_CELL(self, s, self, d) #define METHOD(name, arg_type) {#name, (PyCFunction)name, arg_type, name##_doc}, +#define METHODB(name, arg_type) {#name, (PyCFunction)name, arg_type, ""} #define BOOL_GETSET(type, x) \ static PyObject* x##_get(type *self, void UNUSED *closure) { PyObject *ans = self->x ? Py_True : Py_False; Py_INCREF(ans); return ans; } \ diff --git a/kitty/fonts.c b/kitty/fonts.c new file mode 100644 index 000000000..c454cc176 --- /dev/null +++ b/kitty/fonts.c @@ -0,0 +1,216 @@ +/* + * fonts.c + * Copyright (C) 2017 Kovid Goyal + * + * Distributed under terms of the GPL3 license. + */ + +#include "fonts.h" +#include "state.h" + +typedef uint16_t glyph_index; + +typedef struct { + sprite_index x, y, z; +} SpriteIndex; + + +typedef struct { + PyObject *face; + hb_font_t *hb_font; + // Map glyph ids to sprite map co-ords + SpriteIndex *sprite_map; + bool bold, italic; +} Font; + +static Font medium_font = {0}, bold_font = {0}, italic_font = {0}, bi_font = {0}, box_font = {0}, missing_font = {0}, blank_font = {0}; +static Font fallback_fonts[256] = {{0}}; +static PyObject *get_fallback_font = NULL; + +static inline bool +alloc_font(Font *f, PyObject *face, bool bold, bool italic) { + f->sprite_map = calloc(1 << (sizeof(glyph_index) * 8), sizeof(SpriteIndex)); + if (f->sprite_map == NULL) return false; + f->face = face; Py_INCREF(face); + f->hb_font = harfbuzz_font_for_face(face); + f->bold = bold; f->italic = italic; + return true; +} + +static inline void +clear_font(Font *f) { + Py_CLEAR(f->face); + free(f->sprite_map); f->sprite_map = NULL; + f->hb_font = NULL; + f->bold = false; f->italic = false; +} + +static unsigned int cell_width = 0, cell_height = 0, baseline = 0, underline_position = 0, underline_thickness = 0; + +static inline PyObject* +update_cell_metrics(float pt_sz, float xdpi, float ydpi) { +#define CALL(f) { if ((f)->face && !set_size_for_face((f)->face, pt_sz, xdpi, ydpi)) return NULL; } + CALL(&medium_font); CALL(&bold_font); CALL(&italic_font); CALL(&bi_font); + for (size_t i = 0; fallback_fonts[i].face != NULL; i++) { + CALL(fallback_fonts + i); + } +#undef CALL + cell_metrics(medium_font.face, &cell_width, &cell_height, &baseline, &underline_position, &underline_thickness); + if (!cell_width) { PyErr_SetString(PyExc_ValueError, "Failed to calculate cell width for the specified font."); return NULL; } + if (OPT(adjust_line_height_px) != 0) cell_height += OPT(adjust_line_height_px); + if (OPT(adjust_line_height_frac) != 0.f) cell_height *= OPT(adjust_line_height_frac); + if (cell_height < 4) { PyErr_SetString(PyExc_ValueError, "line height too small after adjustment"); return NULL; } + if (cell_height > 1000) { PyErr_SetString(PyExc_ValueError, "line height too large after adjustment"); return NULL; } + underline_position = MIN(cell_height - 1, underline_position); + return Py_BuildValue("IIIII", cell_width, cell_height, baseline, underline_position, underline_thickness); +} + +static PyObject* +set_font_size(PyObject UNUSED *m, PyObject *args) { + float pt_sz, xdpi, ydpi; + if (!PyArg_ParseTuple(args, "fff", &pt_sz, &xdpi, &ydpi)) return NULL; + return update_cell_metrics(pt_sz, xdpi, ydpi); +} + +static inline bool +has_cell_text(Font *self, Cell *cell) { + if (!face_has_codepoint(self->face, cell->ch)) return false; + if (cell->cc) { + if (!face_has_codepoint(self->face, cell->cc & CC_MASK)) return false; + char_type cc = cell->cc >> 16; + if (cc && !face_has_codepoint(self->face, cc)) return false; + } + return true; +} + + +static inline Font* +fallback_font(Cell *cell) { + bool bold = (cell->attrs >> BOLD_SHIFT) & 1; + bool italic = (cell->attrs >> ITALIC_SHIFT) & 1; + size_t i; + + for (i = 0; fallback_fonts[i].face != NULL; i++) { + if (fallback_fonts[i].bold == bold && fallback_fonts[i].italic == italic && has_cell_text(fallback_fonts + i, cell)) { + return fallback_fonts + i; + } + } + if (get_fallback_font == NULL || i == (sizeof(fallback_fonts)/sizeof(fallback_fonts[0])-1)) return &missing_font; + Py_UCS4 buf[10]; + size_t n = cell_as_unicode(cell, true, buf); + PyObject *face = PyObject_CallFunction(get_fallback_font, "NOO", PyUnicode_FromKindAndData(PyUnicode_4BYTE_KIND, buf, n), bold ? Py_True : Py_False, italic ? Py_True : Py_False); + if (face == NULL) { PyErr_Print(); return &missing_font; } + if (face == Py_None) { Py_DECREF(face); return &missing_font; } + if (!alloc_font(fallback_fonts + i, face, bold, italic)) { fatal("Out of memory"); } + return fallback_fonts + i; +} + +typedef struct { + char_type left, right; + size_t font_idx; +} SymbolMap; +static SymbolMap* symbol_maps = NULL; +static Font *symbol_map_fonts = NULL; +static size_t symbol_maps_count = 0, symbol_map_fonts_count = 0; + +static inline Font* +in_symbol_maps(char_type ch) { + for (size_t i = 0; i < symbol_maps_count; i++) { + if (symbol_maps[i].left <= ch && ch <= symbol_maps[i].right) return symbol_map_fonts + symbol_maps[i].font_idx; + } + return NULL; +} + + +Font* +font_for_cell(Cell *cell) { + Font *ans; +START_ALLOW_CASE_RANGE + switch(cell->ch) { + case 0: + return &blank_font; + case 0x2500 ... 0x2570: + case 0x2574 ... 0x2577: + case 0xe0b0: + case 0xe0b2: + return &box_font; + default: + ans = in_symbol_maps(cell->ch); + if (ans != NULL) return ans; + switch(BI_VAL(cell->attrs)) { + case 0: + ans = &medium_font; + break; + case 1: + ans = bold_font.face ? &bold_font : &medium_font; + break; + case 2: + ans = italic_font.face ? &italic_font : &medium_font; + break; + case 4: + ans = bi_font.face ? &bi_font : &medium_font; + break; + } + if (has_cell_text(ans, cell)) return ans; + return fallback_font(cell); + + } +END_ALLOW_CASE_RANGE +} + +static PyObject* +set_font(PyObject UNUSED *m, PyObject *args) { + PyObject *sm, *smf, *medium, *bold = NULL, *italic = NULL, *bi = NULL; + float xdpi, ydpi, pt_sz; + if (!PyArg_ParseTuple(args, "OO!O!fffO|OOO", &get_fallback_font, &PyTuple_Type, &sm, &PyTuple_Type, &smf, &pt_sz, &xdpi, &ydpi, &medium, &bold, &italic, &bi)) return NULL; + if (!alloc_font(&medium_font, medium, false, false)) return PyErr_NoMemory(); + if (bold && !alloc_font(&bold_font, bold, false, false)) return PyErr_NoMemory(); + if (italic && !alloc_font(&italic_font, italic, false, false)) return PyErr_NoMemory(); + if (bi && !alloc_font(&bi_font, bi, false, false)) return PyErr_NoMemory(); + + symbol_maps_count = PyTuple_GET_SIZE(sm); + if (symbol_maps_count > 0) { + symbol_maps = malloc(symbol_maps_count * sizeof(SymbolMap)); + symbol_map_fonts_count = PyTuple_GET_SIZE(smf); + symbol_map_fonts = calloc(symbol_map_fonts_count, sizeof(Font)); + if (symbol_maps == NULL || symbol_map_fonts == NULL) return PyErr_NoMemory(); + + for (size_t i = 0; i < symbol_map_fonts_count; i++) { + PyObject *face; + int bold, italic; + if (!PyArg_ParseTuple(PyTuple_GET_ITEM(smf, i), "Opp", &face, &bold, &italic)) return NULL; + if (!alloc_font(symbol_map_fonts + i, face, bold != 0, italic != 0)) return PyErr_NoMemory(); + } + for (size_t i = 0; i < symbol_maps_count; i++) { + unsigned int left, right, font_idx; + if (!PyArg_ParseTuple(PyTuple_GET_ITEM(sm, i), "III", &left, &right, &font_idx)) return NULL; + symbol_maps[i].left = left; symbol_maps[i].right = right; symbol_maps[i].font_idx = font_idx; + } + } + return update_cell_metrics(pt_sz, xdpi, ydpi); +} + +static void +finalize(void) { + Py_CLEAR(get_fallback_font); + clear_font(&medium_font); clear_font(&bold_font); clear_font(&italic_font); clear_font(&bi_font); + for (size_t i = 0; fallback_fonts[i].face != NULL; i++) clear_font(fallback_fonts + i); + for (size_t i = 0; symbol_map_fonts_count; i++) clear_font(symbol_map_fonts + i); + free(symbol_maps); free(symbol_map_fonts); +} + +static PyMethodDef module_methods[] = { + METHODB(set_font_size, METH_VARARGS), + METHODB(set_font, METH_VARARGS), + {NULL, NULL, 0, NULL} /* Sentinel */ +}; + +bool +init_fonts(PyObject *module) { + if (Py_AtExit(finalize) != 0) { + PyErr_SetString(PyExc_RuntimeError, "Failed to register the fonts module at exit handler"); + return false; + } + if (PyModule_AddFunctions(module, module_methods) != 0) return false; + return true; +} diff --git a/kitty/fonts.h b/kitty/fonts.h new file mode 100644 index 000000000..b93283919 --- /dev/null +++ b/kitty/fonts.h @@ -0,0 +1,20 @@ +/* + * Copyright (C) 2017 Kovid Goyal + * + * Distributed under terms of the GPL3 license. + */ + +#pragma once + +#include "lineops.h" +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wpedantic" +#include +#pragma GCC diagnostic pop + + + +bool face_has_codepoint(PyObject *, char_type); +hb_font_t* harfbuzz_font_for_face(PyObject*); +bool set_size_for_face(PyObject*, float, float, float); +void cell_metrics(PyObject*, unsigned int*, unsigned int*, unsigned int*, unsigned int*, unsigned int*); diff --git a/kitty/fonts/fontconfig.py b/kitty/fonts/fontconfig.py index ccae6679a..9f785a628 100644 --- a/kitty/fonts/fontconfig.py +++ b/kitty/fonts/fontconfig.py @@ -3,19 +3,18 @@ # License: GPL v3 Copyright: 2016, Kovid Goyal import os -import re from collections import namedtuple from kitty.fast_data_types import Face, get_fontconfig_font -def escape_family_name(name): - return re.sub(r'([-:,\\])', lambda m: '\\' + m.group(1), name) +def face_from_font(font, pt_sz, xdpi, ydpi): + return Face(font.path, font.index, font.hinting, font.hintstyle, pt_sz, xdpi, ydpi) Font = namedtuple( 'Font', - 'face hinting hintstyle bold italic scalable outline weight slant index' + 'path hinting hintstyle bold italic scalable outline weight slant index' ) @@ -76,13 +75,21 @@ def find_font_for_characters( size_in_pts=size_in_pts, dpi=dpi ) - if not ans.face or not os.path.exists(ans.face): + if not ans.path or not os.path.exists(ans.path): raise FontNotFound( 'Failed to find font for characters: {!r}'.format(chars) ) return ans +def font_for_text(text, current_font_family, pt_sz, xdpi, ydpi, bold=False, italic=False): + dpi = (xdpi + ydpi) / 2 + try: + return find_font_for_characters(current_font_family, text, bold=bold, italic=italic, size_in_pts=pt_sz, dpi=dpi) + except FontNotFound: + return find_font_for_characters(current_font_family, text, bold=bold, italic=italic, size_in_pts=pt_sz, dpi=dpi, allow_bitmaped_fonts=True) + + def get_font_information(family, bold=False, italic=False): return get_font(family, bold, italic) @@ -102,7 +109,7 @@ def get_font_files(opts): return ans n = get_font_information(get_family()) - ans['regular'] = n._replace(face=Face(n.face, n.index, n.hinting, n.hintstyle)) + ans['medium'] = n def do(key): b = get_font_information( @@ -110,8 +117,8 @@ def get_font_files(opts): bold=key in ('bold', 'bi'), italic=key in ('italic', 'bi') ) - if b.face != n.face: - ans[key] = b._replace(face=Face(b.face, b.index, b.hinting, b.hintstyle)) + if b.path != n.path: + ans[key] = b do('bold'), do('italic'), do('bi') return ans @@ -119,4 +126,4 @@ def get_font_files(opts): def font_for_family(family): ans = get_font_information(family) - return ans._replace(face=Face(ans.face, ans.index, ans.hinting, ans.hintstyle)) + return ans diff --git a/kitty/fonts/render.py b/kitty/fonts/render.py index 0384f64e3..2787b4996 100644 --- a/kitty/fonts/render.py +++ b/kitty/fonts/render.py @@ -3,14 +3,69 @@ # License: GPL v3 Copyright: 2016, Kovid Goyal import ctypes +from collections import namedtuple from math import sin, pi, ceil, floor, sqrt from kitty.constants import isosx +from kitty.utils import get_logical_dpi +from kitty.fast_data_types import set_font, set_font_size from .box_drawing import render_box_char, is_renderable_box_char if isosx: - from .core_text import set_font_family, render_cell as rc, current_cell # noqa + pass else: - from .freetype import set_font_family, render_cell as rc, current_cell # noqa + from .fontconfig import get_font_files, font_for_text, face_from_font, font_for_family + + +def create_face(font): + s = set_font_family.state + return face_from_font(font, s.pt_sz, s.xdpi, s.ydpi) + + +def create_symbol_map(opts): + val = opts.symbol_map + family_map = {} + faces = [] + for family in val.values(): + if family not in family_map: + o = create_face(font_for_family(family)) + family_map[family] = len(faces) + faces.append(o) + sm = tuple((a, b, family_map[f]) for (a, b), f in val.items()) + return sm, tuple(faces) + + +FontState = namedtuple('FontState', 'family pt_sz xdpi ydpi cell_width cell_height baseline underline_position underline_thickness') + + +def get_fallback_font(text, bold, italic): + state = set_font_family.state + return create_face(font_for_text(text, state.family, state.pt_sz, state.xdpi, state.ydpi, bold, italic)) + + +def set_font_family(opts, override_font_size=None): + if hasattr(set_font_family, 'state'): + raise ValueError('Cannot set font family more than once, use resize_fonts() to change size') + sz = override_font_size or opts.font_size + xdpi, ydpi = get_logical_dpi() + set_font_family.state = FontState('', sz, xdpi, ydpi, 0, 0, 0, 0, 0) + font_map = get_font_files(opts) + faces = [create_face(font_map['medium'])] + for k in 'bold italic bi'.split(): + if k in font_map: + faces.append(create_face(font_map[k])) + sm, sfaces = create_symbol_map(opts) + cell_width, cell_height, baseline, underline_position, underline_thickness = set_font(get_fallback_font, sm, sfaces, sz, xdpi, ydpi, *faces) + set_font_family.state = FontState(opts.font_family, sz, xdpi, ydpi, cell_width, cell_height, baseline, underline_position, underline_thickness) + return cell_width, cell_height + + +def resize_fonts(new_sz, xdpi=None, ydpi=None): + s = set_font_family.state + xdpi = xdpi or s.xdpi + ydpi = ydpi or s.ydpi + cell_width, cell_height, baseline, underline_position, underline_thickness = set_font_size(new_sz, xdpi, ydpi) + set_font_family.state = FontState( + s.family, new_sz, xdpi, ydpi, cell_width, cell_height, baseline, underline_position, underline_thickness) def add_line(buf, cell_width, position, thickness, cell_height): diff --git a/kitty/freetype.c b/kitty/freetype.c index 61a4adc65..362791f31 100644 --- a/kitty/freetype.c +++ b/kitty/freetype.c @@ -5,14 +5,10 @@ * Distributed under terms of the GPL3 license. */ -#include "data-types.h" +#include "fonts.h" #include #include #include -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wpedantic" -#include -#pragma GCC diagnostic pop #include #if HB_VERSION_MAJOR > 1 || (HB_VERSION_MAJOR == 1 && (HB_VERSION_MINOR > 0 || (HB_VERSION_MINOR == 0 && HB_VERSION_MICRO >= 5))) @@ -31,10 +27,10 @@ typedef struct { int ascender, descender, height, max_advance_width, max_advance_height, underline_position, underline_thickness; int hinting, hintstyle; bool is_scalable; + float size_in_pts; FT_F26Dot6 char_width, char_height; FT_UInt xdpi, ydpi; PyObject *path; - hb_buffer_t *harfbuzz_buffer; hb_font_t *harfbuzz_font; } Face; @@ -94,6 +90,12 @@ set_font_size(Face *self, FT_F26Dot6 char_width, FT_F26Dot6 char_height, FT_UInt return !error; } +bool +set_size_for_face(PyObject *self, float pt_sz, float xdpi, float ydpi) { + FT_UInt w = (FT_UInt)(ceilf(pt_sz * 64)); + ((Face*)self)->size_in_pts = pt_sz; + return set_font_size((Face*)self, w, w, (FT_UInt)xdpi, (FT_UInt) ydpi); +} static PyObject* new(PyTypeObject *type, PyObject *args, PyObject UNUSED *kwds) { @@ -101,8 +103,8 @@ new(PyTypeObject *type, PyObject *args, PyObject UNUSED *kwds) { char *path; int error, hinting, hintstyle; long index; - /* unsigned int columns=80, lines=24, scrollback=0; */ - if (!PyArg_ParseTuple(args, "slii", &path, &index, &hinting, &hintstyle)) return NULL; + unsigned int size_in_pts, xdpi, ydpi; + if (!PyArg_ParseTuple(args, "sliifff", &path, &index, &hinting, &hintstyle, &size_in_pts, &xdpi, &ydpi)) return NULL; self = (Face *)type->tp_alloc(type, 0); if (self != NULL) { @@ -114,10 +116,8 @@ new(PyTypeObject *type, PyObject *args, PyObject UNUSED *kwds) { CPY(units_per_EM); CPY(ascender); CPY(descender); CPY(height); CPY(max_advance_width); CPY(max_advance_height); CPY(underline_position); CPY(underline_thickness); #undef CPY self->is_scalable = FT_IS_SCALABLE(self->face); - self->harfbuzz_buffer = hb_buffer_create(); self->hinting = hinting; self->hintstyle = hintstyle; - if (self->harfbuzz_buffer == NULL || !hb_buffer_allocation_successful(self->harfbuzz_buffer) || !hb_buffer_pre_allocate(self->harfbuzz_buffer, 20)) { Py_CLEAR(self); return PyErr_NoMemory(); } - if (!set_font_size(self, 10, 20, 96, 96)) { Py_CLEAR(self); return NULL; } + if (!set_size_for_face((PyObject*)self, size_in_pts, xdpi, ydpi)) { Py_CLEAR(self); return NULL; } self->harfbuzz_font = hb_ft_font_create(self->face, NULL); if (self->harfbuzz_font == NULL) { Py_CLEAR(self); return PyErr_NoMemory(); } } @@ -126,7 +126,6 @@ new(PyTypeObject *type, PyObject *args, PyObject UNUSED *kwds) { static void dealloc(Face* self) { - if (self->harfbuzz_buffer) hb_buffer_destroy(self->harfbuzz_buffer); if (self->harfbuzz_font) hb_font_destroy(self->harfbuzz_font); if (self->face) FT_Done_Face(self->face); Py_CLEAR(self->path); @@ -143,17 +142,6 @@ repr(Face *self) { } - -static PyObject* -set_char_size(Face *self, PyObject *args) { -#define set_char_size_doc "set_char_size(width, height, xdpi, ydpi) -> set the character size. width, height is in 1/64th of a pt. dpi is in pixels per inch" - long char_width, char_height; - unsigned int xdpi, ydpi; - if (!PyArg_ParseTuple(args, "llII", &char_width, &char_height, &xdpi, &ydpi)) return NULL; - if (!set_font_size(self, char_width, char_height, xdpi, ydpi)) return NULL; - Py_RETURN_NONE; -} - static inline int get_load_flags(int hinting, int hintstyle, int base) { int flags = base; @@ -172,253 +160,38 @@ load_glyph(Face *self, int glyph_index) { return true; } -static PyObject* -get_char_index(Face *self, PyObject *args) { -#define get_char_index_doc "" - int code; - unsigned int ans; - if (!PyArg_ParseTuple(args, "C", &code)) return NULL; - ans = FT_Get_Char_Index(self->face, code); - - return Py_BuildValue("I", ans); -} - -static PyObject* +static inline unsigned int calc_cell_width(Face *self) { -#define calc_cell_width_doc "" - unsigned long ans = 0; + unsigned int ans = 0; for (char_type i = 32; i < 128; i++) { int glyph_index = FT_Get_Char_Index(self->face, i); - if (!load_glyph(self, glyph_index)) return NULL; - ans = MAX(ans, (unsigned long)ceilf((float)self->face->glyph->metrics.horiAdvance / 64.f)); - } - return PyLong_FromUnsignedLong(ans); -} - -static PyStructSequence_Field shape_fields[] = { - {"glyph_id", NULL}, - {"cluster", NULL}, - {"mask", NULL}, - {"x_offset", NULL}, - {"y_offset", NULL}, - {"x_advance", NULL}, - {"y_advance", NULL}, - {NULL} -}; -static PyStructSequence_Desc shape_fields_desc = {"Shape", NULL, shape_fields, 7}; -static PyTypeObject ShapeFieldsType = {{{0}}}; - -static inline PyObject* -shape_to_py(unsigned int i, hb_glyph_info_t *info, hb_glyph_position_t *pos) { - PyObject *ans = PyStructSequence_New(&ShapeFieldsType); - if (ans == NULL) return NULL; -#define SI(num, src, attr, conv, func, div) PyStructSequence_SET_ITEM(ans, num, func(((conv)src[i].attr) / div)); if (PyStructSequence_GET_ITEM(ans, num) == NULL) { Py_CLEAR(ans); return PyErr_NoMemory(); } -#define INFO(num, attr) SI(num, info, attr, unsigned long, PyLong_FromUnsignedLong, 1) -#define POS(num, attr) SI(num + 3, pos, attr, double, PyFloat_FromDouble, 64.0) - INFO(0, codepoint); INFO(1, cluster); INFO(2, mask); - POS(0, x_offset); POS(1, y_offset); POS(2, x_advance); POS(3, y_advance); -#undef INFO -#undef POS -#undef SI - return ans; -} - -typedef struct { - unsigned int length; - hb_glyph_info_t *info; - hb_glyph_position_t *positions; -} ShapeData; - - -static inline void -_shape(Face *self, const char *string, int len, ShapeData *ans) { - hb_buffer_clear_contents(self->harfbuzz_buffer); -#ifdef HARBUZZ_HAS_LOAD_FLAGS - hb_ft_font_set_load_flags(self->harfbuzz_font, get_load_flags(self->hinting, self->hintstyle, FT_LOAD_DEFAULT)); -#endif - hb_buffer_add_utf8(self->harfbuzz_buffer, string, len, 0, len); - hb_buffer_guess_segment_properties(self->harfbuzz_buffer); - hb_shape(self->harfbuzz_font, self->harfbuzz_buffer, NULL, 0); - - unsigned int info_length, positions_length; - ans->info = hb_buffer_get_glyph_infos(self->harfbuzz_buffer, &info_length); - ans->positions = hb_buffer_get_glyph_positions(self->harfbuzz_buffer, &positions_length); - ans->length = MIN(info_length, positions_length); -} - - -static PyObject* -shape(Face *self, PyObject *args) { -#define shape_doc "shape(text)" - const char *string; - int len; - if (!PyArg_ParseTuple(args, "s#", &string, &len)) return NULL; - - ShapeData sd; - _shape(self, string, len, &sd); - PyObject *ans = PyTuple_New(sd.length); - if (ans == NULL) return NULL; - for (unsigned int i = 0; i < sd.length; i++) { - PyObject *s = shape_to_py(i, sd.info, sd.positions); - if (s == NULL) { Py_CLEAR(ans); return NULL; } - PyTuple_SET_ITEM(ans, i, s); + if (load_glyph(self, glyph_index)) { + ans = MAX(ans, (unsigned long)ceilf((float)self->face->glyph->metrics.horiAdvance / 64.f)); + } } return ans; } -typedef struct { - unsigned char* buf; - size_t start_x, width, stride; - size_t rows; -} ProcessedBitmap; - - -static inline void -trim_borders(ProcessedBitmap *ans, size_t extra) { - bool column_has_text = false; - - // Trim empty columns from the right side of the bitmap - for (ssize_t x = ans->width - 1; !column_has_text && x > -1 && extra > 0; x--) { - for (size_t y = 0; y < ans->rows && !column_has_text; y++) { - if (ans->buf[x + y * ans->stride] > 200) column_has_text = true; - } - if (!column_has_text) { ans->width--; extra--; } - } - - ans->start_x = extra; - ans->width -= extra; +static inline int +font_units_to_pixels(Face *self, int x) { + return (int)ceilf(((float)x * (((float)self->size_in_pts * (float)self->ydpi) / (72.f * (float)self->units_per_EM)))); } - -static inline bool -render_bitmap(Face *self, int glyph_id, ProcessedBitmap *ans, unsigned int cell_width, unsigned int num_cells, int bold, int italic, bool rescale) { - if (!load_glyph(self, glyph_id)) return false; - unsigned int max_width = cell_width * num_cells; - FT_Bitmap *bitmap = &self->face->glyph->bitmap; - ans->buf = bitmap->buffer; - ans->start_x = 0; ans->width = bitmap->width; - ans->stride = bitmap->pitch < 0 ? -bitmap->pitch : bitmap->pitch; - ans->rows = bitmap->rows; - if (ans->width > max_width) { - size_t extra = bitmap->width - max_width; - if (italic && extra < cell_width / 2) { - trim_borders(ans, extra); - } else if (rescale && self->is_scalable && extra > MAX(2, cell_width / 3)) { - FT_F26Dot6 char_width = self->char_width, char_height = self->char_height; - float ar = (float)max_width / (float)bitmap->width; - if (set_font_size(self, (FT_F26Dot6)((float)self->char_width * ar), (FT_F26Dot6)((float)self->char_height * ar), self->xdpi, self->ydpi)) { - if (!render_bitmap(self, glyph_id, ans, cell_width, num_cells, bold, italic, false)) return false; - if (!set_font_size(self, char_width, char_height, self->xdpi, self->ydpi)) return false; - } - } - } - return true; +void +cell_metrics(PyObject *s, unsigned int* cell_width, unsigned int* cell_height, unsigned int* baseline, unsigned int* underline_position, unsigned int* underline_thickness) { + Face *self = (Face*)s; + *cell_width = calc_cell_width(self); + *cell_height = font_units_to_pixels(self, self->height); + *baseline = font_units_to_pixels(self, self->ascender); + *underline_position = MIN(*cell_height - 1, *baseline - font_units_to_pixels(self, self->underline_position)); + *underline_thickness = font_units_to_pixels(self, self->underline_thickness); } -static inline void -place_bitmap_in_cell(unsigned char *cell, ProcessedBitmap *bm, size_t cell_width, size_t cell_height, float x_offset, float y_offset, FT_Glyph_Metrics *metrics, size_t baseline) { - // We want the glyph to be positioned inside the cell based on the bearingX - // and bearingY values, making sure that it does not overflow the cell. - - // Calculate column bounds - ssize_t xoff = (ssize_t)(x_offset + (float)metrics->horiBearingX / 64.f); - size_t src_start_column = bm->start_x, dest_start_column = 0, extra; - if (xoff < 0) src_start_column += -xoff; - else dest_start_column = xoff; - // Move the dest start column back if the width overflows because of it - if (dest_start_column > 0 && dest_start_column + bm->width > cell_width) { - extra = dest_start_column + bm->width - cell_width; - dest_start_column = extra > dest_start_column ? 0 : dest_start_column - extra; - } - - // Calculate row bounds - ssize_t yoff = (ssize_t)(y_offset + (float)metrics->horiBearingY / 64.f); - size_t src_start_row, dest_start_row; - if (yoff > 0 && (size_t)yoff > baseline) { - src_start_row = 0; - dest_start_row = 0; - } else { - src_start_row = 0; - dest_start_row = baseline - yoff; - } - - /* printf("src_start_row: %zu src_start_column: %zu dest_start_row: %zu dest_start_column: %zu\n", src_start_row, src_start_column, dest_start_row, dest_start_column); */ - - for (size_t sr = src_start_row, dr = dest_start_row; sr < bm->rows && dr < cell_height; sr++, dr++) { - for(size_t sc = src_start_column, dc = dest_start_column; sc < bm->width && dc < cell_width; sc++, dc++) { - uint16_t val = cell[dr * cell_width + dc]; - val = (val + bm->buf[sr * bm->stride + sc]) % 256; - cell[dr * cell_width + dc] = val; - } - } +bool +face_has_codepoint(PyObject *s, char_type cp) { + return FT_Get_Char_Index(((Face*)s)->face, cp) > 0; } -static PyObject* -draw_single_glyph(Face *self, PyObject *args) { -#define draw_single_glyph_doc "draw_complex_glyph(codepoint, cell_width, cell_height, cell_buffer, num_cells, bold, italic, baseline)" - int bold, italic; - unsigned int cell_width, cell_height, num_cells, baseline, codepoint; - PyObject *addr; - if (!PyArg_ParseTuple(args, "IIIO!IppI", &codepoint, &cell_width, &cell_height, &PyLong_Type, &addr, &num_cells, &bold, &italic, &baseline)) return NULL; - unsigned char *cell = PyLong_AsVoidPtr(addr); - int glyph_id = FT_Get_Char_Index(self->face, codepoint); - ProcessedBitmap bm; - if (!render_bitmap(self, glyph_id, &bm, cell_width, num_cells, bold, italic, true)) return NULL; - place_bitmap_in_cell(cell, &bm, cell_width * num_cells, cell_height, 0, 0, &self->face->glyph->metrics, baseline); - Py_RETURN_NONE; -} - -static PyObject* -draw_complex_glyph(Face *self, PyObject *args) { -#define draw_complex_glyph_doc "draw_complex_glyph(text, cell_width, cell_height, cell_buffer, num_cells, bold, italic, baseline)" - const char *text; - int text_len, bold, italic; - unsigned int cell_width, cell_height, num_cells, baseline; - PyObject *addr; - float x = 0.f, y = 0.f; - if (!PyArg_ParseTuple(args, "s#IIO!IppI", &text, &text_len, &cell_width, &cell_height, &PyLong_Type, &addr, &num_cells, &bold, &italic, &baseline)) return NULL; - unsigned char *cell = PyLong_AsVoidPtr(addr); - ShapeData sd; - _shape(self, text, text_len, &sd); - ProcessedBitmap bm; - - for (unsigned i = 0; i < sd.length; i++) { - if (sd.info[i].codepoint == 0) continue; - if (!render_bitmap(self, sd.info[i].codepoint, &bm, cell_width, num_cells, bold, italic, true)) return NULL; - x += (float)sd.positions[i].x_offset / 64.0f; - y = (float)sd.positions[i].y_offset / 64.0f; - place_bitmap_in_cell(cell, &bm, cell_width * num_cells, cell_height, x, y, &self->face->glyph->metrics, baseline); - x += (float)sd.positions[i].x_advance / 64.0f; - - } - Py_RETURN_NONE; -} - -static PyObject* -split_cells(Face UNUSED *self, PyObject *args) { -#define split_cells_doc "split_cells(cell_width, cell_height, src, *cells)" - unsigned int cell_width, cell_height; - unsigned char *cells[10], *src; - size_t num_cells = PyTuple_GET_SIZE(args) - 3; - if (num_cells > sizeof(cells)/sizeof(cells[0])) { PyErr_SetString(PyExc_ValueError, "Too many cells being split"); return NULL; } - cell_width = PyLong_AsUnsignedLong(PyTuple_GET_ITEM(args, 0)); - cell_height = PyLong_AsUnsignedLong(PyTuple_GET_ITEM(args, 1)); - src = PyLong_AsVoidPtr(PyTuple_GET_ITEM(args, 2)); - for (size_t i = 3; i < num_cells + 3; i++) cells[i - 3] = PyLong_AsVoidPtr(PyTuple_GET_ITEM(args, i)); - - size_t stride = num_cells * cell_width; - for (size_t y = 0; y < cell_height; y++) { - for (size_t i = 0; i < num_cells; i++) { - unsigned char *dest = cells[i] + y * cell_width; - for (size_t x = 0; x < cell_width; x++) { - dest[x] = src[y * stride + i * cell_width + x]; - } - } - } - Py_RETURN_NONE; -} - - // Boilerplate {{{ static PyMemberDef members[] = { @@ -437,13 +210,6 @@ static PyMemberDef members[] = { }; static PyMethodDef methods[] = { - METHOD(set_char_size, METH_VARARGS) - METHOD(shape, METH_VARARGS) - METHOD(draw_complex_glyph, METH_VARARGS) - METHOD(draw_single_glyph, METH_VARARGS) - METHOD(split_cells, METH_VARARGS) - METHOD(get_char_index, METH_VARARGS) - METHOD(calc_cell_width, METH_NOARGS) {NULL} /* Sentinel */ }; @@ -482,13 +248,6 @@ init_freetype_library(PyObject *m) { PyErr_SetString(FreeType_Exception, "Failed to register the freetype library at exit handler"); return false; } - if (PyStructSequence_InitType2(&ShapeFieldsType, &shape_fields_desc) != 0) return false; - PyModule_AddObject(m, "ShapeFields", (PyObject*)&ShapeFieldsType); - PyModule_AddIntMacro(m, FT_LOAD_RENDER); - PyModule_AddIntMacro(m, FT_LOAD_TARGET_NORMAL); - PyModule_AddIntMacro(m, FT_LOAD_TARGET_LIGHT); - PyModule_AddIntMacro(m, FT_LOAD_NO_HINTING); - PyModule_AddIntMacro(m, FT_PIXEL_MODE_GRAY); return true; } diff --git a/kitty/line.c b/kitty/line.c index c647085db..e30b0440a 100644 --- a/kitty/line.c +++ b/kitty/line.c @@ -165,6 +165,22 @@ text_at(Line* self, Py_ssize_t xval) { return line_text_at(self->cells[xval].ch, self->cells[xval].cc); } +size_t +cell_as_unicode(Cell *cell, bool include_cc, Py_UCS4 *buf) { + size_t n = 1; + buf[0] = cell->ch; + if (include_cc) { + char_type cc = cell->cc; + Py_UCS4 cc1 = cc & CC_MASK, cc2; + if (cc1) { + buf[1] = cc1; n++; + cc2 = cc >> 16; + if (cc2) { buf[2] = cc2; n++; } + } + } + return n; +} + PyObject* unicode_in_range(Line *self, index_type start, index_type limit, bool include_cc, char leading_char) { size_t n = 0; @@ -177,16 +193,7 @@ unicode_in_range(Line *self, index_type start, index_type limit, bool include_cc if (previous_width == 2) { previous_width = 0; continue; }; ch = ' '; } - buf[n++] = ch; - if (include_cc) { - char_type cc = self->cells[i].cc; - Py_UCS4 cc1 = cc & CC_MASK, cc2; - if (cc1) { - buf[n++] = cc1; - cc2 = cc >> 16; - if (cc2) buf[n++] = cc2; - } - } + n += cell_as_unicode(self->cells + i, include_cc, buf + n); previous_width = self->cells[i].attrs & WIDTH_MASK; } return PyUnicode_FromKindAndData(PyUnicode_4BYTE_KIND, buf, n); diff --git a/kitty/lineops.h b/kitty/lineops.h index 33e3b6373..0976824ba 100644 --- a/kitty/lineops.h +++ b/kitty/lineops.h @@ -67,6 +67,7 @@ index_type line_url_start_at(Line *self, index_type x); index_type line_url_end_at(Line *self, index_type x); index_type line_as_ansi(Line *self, Py_UCS4 *buf, index_type buflen); unsigned int line_length(Line *self); +size_t cell_as_unicode(Cell *cell, bool include_cc, Py_UCS4 *buf); PyObject* unicode_in_range(Line *self, index_type start, index_type limit, bool include_cc, char leading_char); void linebuf_init_line(LineBuf *, index_type); diff --git a/kitty/state.c b/kitty/state.c index ef5b867aa..e828d75d6 100644 --- a/kitty/state.c +++ b/kitty/state.c @@ -180,6 +180,16 @@ PYWRAP1(set_options) { Py_DECREF(ret); if (PyErr_Occurred()) return NULL; Py_DECREF(chars); + + PyObject *al = PyObject_GetAttrString(args, "adjust_line_height"); + if (PyFloat_Check(al)) { + OPT(adjust_line_height_frac) = (float)PyFloat_AsDouble(al); + OPT(adjust_line_height_px) = 0; + } else { + OPT(adjust_line_height_frac) = 0; + OPT(adjust_line_height_px) = (int)PyLong_AsLong(al); + } + Py_DECREF(al); #undef S Py_RETURN_NONE; } diff --git a/kitty/state.h b/kitty/state.h index c7c04b7e9..42f64e6e0 100644 --- a/kitty/state.h +++ b/kitty/state.h @@ -20,6 +20,8 @@ typedef struct { double repaint_delay, input_delay; bool focus_follows_mouse; bool macos_option_as_alt; + int adjust_line_height_px; + float adjust_line_height_frac; } Options; typedef struct {