diff --git a/kitty/decorations.c b/kitty/decorations.c index 1df1ae425..a16a91bb1 100644 --- a/kitty/decorations.c +++ b/kitty/decorations.c @@ -8,6 +8,11 @@ #include "decorations.h" #include "state.h" +typedef uint32_t uint; + +static uint max(uint a, uint b) { return a > b ? a : b; } +static uint min(uint a, uint b) { return a < b ? a : b; } + #define STRAIGHT_UNDERLINE_LOOP \ unsigned half = fcm.underline_thickness / 2; \ DecorationGeometry ans = {.top = half > fcm.underline_position ? 0 : fcm.underline_position - half}; \ @@ -35,8 +40,8 @@ add_strikethrough(uint8_t *buf, FontCellMetrics fcm) { DecorationGeometry add_missing_glyph(uint8_t *buf, FontCellMetrics fcm) { DecorationGeometry ans = {.height=fcm.cell_height}; - unsigned thickness = MIN(fcm.underline_thickness, fcm.strikethrough_thickness); - thickness = MIN(thickness, fcm.cell_width); + unsigned thickness = min(fcm.underline_thickness, fcm.strikethrough_thickness); + thickness = min(thickness, fcm.cell_width); for (unsigned y = 0; y < ans.height; y++) { uint8_t *line = buf + fcm.cell_width * y; if (y < thickness || y >= ans.height - thickness) memset(line, 0xff, fcm.cell_width); @@ -51,9 +56,9 @@ add_missing_glyph(uint8_t *buf, FontCellMetrics fcm) { DecorationGeometry add_double_underline(uint8_t *buf, FontCellMetrics fcm) { unsigned a = fcm.underline_position > fcm.underline_thickness ? fcm.underline_position - fcm.underline_thickness : 0; - a = MIN(a, fcm.cell_height - 1); - unsigned b = MIN(fcm.underline_position, fcm.cell_height - 1); - unsigned top = MIN(a, b), bottom = MAX(a, b); + a = min(a, fcm.cell_height - 1); + unsigned b = min(fcm.underline_position, fcm.cell_height - 1); + unsigned top = min(a, b), bottom = max(a, b); int deficit = 2 - (bottom - top); if (deficit > 0) { if (bottom + deficit < fcm.cell_height) bottom += deficit; @@ -62,8 +67,8 @@ add_double_underline(uint8_t *buf, FontCellMetrics fcm) { if (deficit > 1) top -= deficit - 1; } else top -= deficit; } - top = MAX(0u, MIN(top, fcm.cell_height - 1u)); - bottom = MAX(0u, MIN(bottom, fcm.cell_height - 1u)); + top = max(0u, min(top, fcm.cell_height - 1u)); + bottom = max(0u, min(bottom, fcm.cell_height - 1u)); memset(buf + fcm.cell_width * top, 0xff, fcm.cell_width); memset(buf + fcm.cell_width * bottom, 0xff, fcm.cell_width); DecorationGeometry ans = {.top=top, .height = bottom + 1 - top}; @@ -72,7 +77,7 @@ add_double_underline(uint8_t *buf, FontCellMetrics fcm) { static unsigned distribute_dots(unsigned available_space, unsigned num_of_dots, unsigned *summed_gaps, unsigned *gaps) { - unsigned dot_size = MAX(1u, available_space / (2u * num_of_dots)); + unsigned dot_size = max(1u, available_space / (2u * num_of_dots)); unsigned extra = 2 * num_of_dots * dot_size; extra = available_space > extra ? available_space - extra : 0; for (unsigned i = 0; i < num_of_dots; i++) gaps[i] = dot_size; @@ -124,9 +129,9 @@ add_dashed_underline(uint8_t *buf, FontCellMetrics fcm) { static unsigned add_intensity(uint8_t *buf, unsigned x, unsigned y, uint8_t val, unsigned max_y, unsigned position, unsigned cell_width) { y += position; - y = MIN(y, max_y); + y = min(y, max_y); unsigned idx = cell_width * y + x; - buf[idx] = MIN(255, buf[idx] + val); + buf[idx] = min(255, buf[idx] + val); return y; } @@ -137,10 +142,10 @@ add_curl_underline(uint8_t *buf, FontCellMetrics fcm) { unsigned half_thickness = fcm.underline_thickness / 2; unsigned top = fcm.underline_position > half_thickness ? fcm.underline_position - half_thickness : 0; unsigned max_height = fcm.cell_height - top; // descender from the font - unsigned half_height = MAX(1u, max_height / 4u); + unsigned half_height = max(1u, max_height / 4u); unsigned thickness; - if (OPT(undercurl_style) & 2) thickness = MAX(half_height, fcm.underline_thickness); - else thickness = MAX(1u, fcm.underline_thickness) - (fcm.underline_thickness < 3u ? 1u : 2u); + if (OPT(undercurl_style) & 2) thickness = max(half_height, fcm.underline_thickness); + else thickness = max(1u, fcm.underline_thickness) - (fcm.underline_thickness < 3u ? 1u : 2u); unsigned position = fcm.underline_position; // Ensure curve doesn't exceed cell boundary at the bottom @@ -168,7 +173,7 @@ add_curl_underline(uint8_t *buf, FontCellMetrics fcm) { static void vert(uint8_t *ans, bool is_left_edge, double width_pt, double dpi_x, FontCellMetrics fcm) { - unsigned width = MAX(1u, MIN((unsigned)(round(width_pt * dpi_x / 72.0)), fcm.cell_width)); + unsigned width = max(1u, min((unsigned)(round(width_pt * dpi_x / 72.0)), fcm.cell_width)); const unsigned left = is_left_edge ? 0 : (fcm.cell_width > width ? fcm.cell_width - width : 0); for (unsigned y = 0; y < fcm.cell_height; y++) { const unsigned offset = y * fcm.cell_width + left; @@ -178,7 +183,7 @@ vert(uint8_t *ans, bool is_left_edge, double width_pt, double dpi_x, FontCellMet static unsigned horz(uint8_t *ans, bool is_top_edge, double height_pt, double dpi_y, FontCellMetrics fcm) { - unsigned height = MAX(1u, MIN((unsigned)(round(height_pt * dpi_y / 72.0)), fcm.cell_height)); + unsigned height = max(1u, min((unsigned)(round(height_pt * dpi_y / 72.0)), fcm.cell_height)); const unsigned top = is_top_edge ? 0 : (fcm.cell_height > height ? fcm.cell_height - height : 0); for (unsigned y = top; y < top + height; y++) { const unsigned offset = y * fcm.cell_width; @@ -210,3 +215,437 @@ add_hollow_cursor(uint8_t *buf, FontCellMetrics fcm, double dpi_x, double dpi_y) DecorationGeometry ans = {.height=fcm.cell_height}; return ans; } +typedef struct Range { + uint start, end; +} Range; + +typedef struct Canvas { + uint8_t *mask; + uint width, height, supersample_factor; + struct { double x, y; } dpi; + Range *holes; uint holes_count, holes_capacity; + struct { double upper, lower; } *y_limits; uint y_limits_count, y_limits_capacity; +} Canvas; + +static void +append_hole(Canvas *self, Range hole) { + ensure_space_for(self, holes, self->holes[0], self->holes_count + 1, holes_capacity, self->width, false); + self->holes[self->holes_count++] = hole; +} + +static void +append_limit(Canvas *self, double upper, double lower) { + ensure_space_for(self, y_limits, self->y_limits[0], self->y_limits_count + 1, y_limits_capacity, self->width, false); + self->y_limits[self->y_limits_count].upper = upper; + self->y_limits[self->y_limits_count++].lower = lower; +} + + +static uint +thickness(Canvas *self, uint level, bool horizontal) { + level = min(level, arraysz(OPT(box_drawing_scale))); + double pts = OPT(box_drawing_scale)[level]; + double dpi = horizontal ? self->dpi.x : self->dpi.y; + return self->supersample_factor * (uint)ceil(pts * dpi / 72.0); +} + +static uint +minus(uint a, uint b) { // saturating subtraction (a > b ? a - b : 0) + uint res = a - b; + res &= -(res <= a); + return res; +} + +static const uint hole_factor = 8; + +static void +get_holes(Canvas *self, uint sz, uint hole_sz, uint num) { + uint all_holes_use = (num + 1) * hole_sz; + uint individual_block_size = max(1u, minus(sz, all_holes_use) / (num + 1)); + uint half_hole_sz = hole_sz / 2; + int pos = - half_hole_sz; + while (pos < (int)sz) { + uint left = pos > 0 ? pos : 0; + uint right = min(sz, pos + hole_sz); + if (right > left) append_hole(self, (Range){left, right}); + pos = right + individual_block_size; + } +} + +static void +add_hholes(Canvas *self, uint level, uint num) { + uint line_sz = thickness(self, level, true); + uint hole_sz = self->width / hole_factor; + uint start = minus(self->height / 2, line_sz / 2); + get_holes(self, self->width, hole_sz, num); + for (uint y = 0; y < start + line_sz; y++) { + uint offset = y * self->width; + for (uint i = 0; i < self->holes_count; i++) memset(self->mask + offset + self->holes[i].start, 0, self->holes[i].end - self->holes[i].start); + } +} + +static void +add_vholes(Canvas *self, uint level, uint num) { + uint line_sz = thickness(self, level, false); + uint hole_sz = self->height / hole_factor; + uint start = minus(self->width / 2, line_sz / 2); + get_holes(self, self->height, hole_sz, num); + for (uint i = 0; i < self->holes_count; i++) { + for (uint y = self->holes[i].start; y < self->holes[i].end; y++) { + uint offset = y * self->width; + memset(self->mask + offset + start, 0, line_sz); + } + } +} + + +static void +draw_hline(Canvas *self, uint x1, uint x2, uint y, uint level) { + // Draw a horizontal line between [x1, x2) centered at y with the thickness given by level and self->supersample_factor + uint sz = thickness(self, level, false); + uint start = minus(y, sz / 2); + for (uint y = start; y < min(start + sz, self->height); y++) { + uint8_t *py = self->mask + y * self->width; + memset(py + x1, 255, minus(x2, x1)); + } +} + +static void +draw_vline(Canvas *self, uint y1, uint y2, uint x, uint level) { + // Draw a vertical line between [y1, y2) centered at x with the thickness given by level and self->supersample_factor + uint sz = thickness(self, level, true); + uint start = minus(x, sz / 2), end = min(start + sz, self->width), xsz = minus(end, start); + for (uint y = y1; y < y2; y++) { + uint8_t *py = self->mask + y * self->width; + memset(py + start, 255, xsz); + } +} + +static void +half_hline(Canvas *self, uint level, bool right_half, uint extend_by) { + uint x1, x2; + if (right_half) { + x1 = minus(self->width / 2u, extend_by); x2 = self->width; + } else { + x1 = 0; x2 = self->width / 2 + extend_by; + } + draw_hline(self, x1, x2, self->height / 2, level); +} + + +static void +half_vline(Canvas *self, uint level, bool bottom_half, uint extend_by) { + uint y1, y2; + if (bottom_half) { + y1 = minus(self->height / 2u, extend_by); y2 = self->height; + } else { + y1 = 0; y2 = self->height / 2 + extend_by; + } + draw_vline(self, y1, y2, self->width / 2, level); +} + +static void +hline(Canvas *self, uint level) { + half_hline(self, level, false, 0); + half_hline(self, level, true, 0); +} + +static void +vline(Canvas *self, uint level) { + half_vline(self, level, false, 0); + half_vline(self, level, true, 0); +} + +static void +hholes(Canvas *self, uint level, uint num) { + hline(self, level); + add_hholes(self, level, num); +} + +static void +vholes(Canvas *self, uint level, uint num) { + vline(self, level); + add_vholes(self, level, num); +} + +static uint8_t +plus(uint8_t a, uint8_t b) { + uint8_t res = a + b; + res |= -(res < a); + return res; +} + +static uint8_t +average_intensity(const Canvas *src, uint dest_x, uint dest_y) { + uint src_x = dest_x * src->supersample_factor, src_y = dest_y * src->supersample_factor; + uint total = 0; + for (uint y = src_y; y < src_y + src->supersample_factor; y++) { + uint offset = src->width * y; + for (uint x = src_x; x < src_x + src->supersample_factor; x++) total += src->mask[offset + x]; + } + return (total / (src->supersample_factor * src->supersample_factor)) & 0xff; +} + +static void +downsample(const Canvas *src, Canvas *dest) { + for (uint y = 0; y < dest->height; y++) { + uint offset = dest->width * y; + for (uint x = 0; x < dest->width; x++) { + dest->mask[offset + x] = plus(dest->mask[offset + x], average_intensity(src, x, y)); + } + } +} + +typedef struct StraightLine { + double m, c; +} StraightLine; + + +static StraightLine +line_from_points(int x1, int y1, int x2, int y2) { + StraightLine ans = {.m = (y2 - y1) / ((double)(x2 - x1))}; + ans.c = y1 - ans.m * x1; + return ans; +} + +static double +line_y(StraightLine l, int x) { + return l.m * x + l.c; +} + +#define calc_limits(self, lower_y, upper_y) { \ + if (!self->y_limits) { \ + self->y_limits_count = self->width; self->y_limits = malloc(sizeof(self->y_limits[0]) * self->y_limits_count); \ + if (!self->y_limits) fatal("Out of memory"); \ + } \ + for (uint x = 0; x < self->width; x++) { self->y_limits[x].lower = lower_y; self->y_limits[x].upper = upper_y; } \ +} + +static void +fill_region(Canvas *self, bool inverted) { + uint8_t full = 0, empty = 0; if (inverted) empty = 255; else full = 255; + for (uint y = 0; y < self->height; y++) { + uint offset = y * self->width; + for (uint x = 0; x < self->width && x < self->y_limits_count; x++) { + self->mask[offset + x] = self->y_limits[x].lower <= y && y <= self->y_limits[x].upper ? full : empty; + } + } +} + +static void +triangle(Canvas *self, bool left, bool inverted) { + int ay1 = 0, by1 = self->height - 1, y2 = self->height / 2, x1 = 0, x2 = 0; + if (left) x2 = self->width - 1; else x1 = self->width - 1; + StraightLine uppery = line_from_points(x1, ay1, x2, y2); + StraightLine lowery = line_from_points(x1, by1, x2, y2); + calc_limits(self, line_y(uppery, x), line_y(lowery, x)); + fill_region(self, inverted); +} + +typedef enum Corner { + TOP_LEFT, TOP_RIGHT, BOTTOM_LEFT, BOTTOM_RIGHT +} Corner; + +typedef struct Point { int x, y; } Point; + +static void +thick_line(Canvas *self, uint thickness_in_pixels, Point p1, Point p2) { + if (p1.x > p2.x) SWAP(p1, p2); + StraightLine l = line_from_points(p1.x, p1.y, p2.x, p2.y); + div_t d = div(thickness_in_pixels, 2); + int delta = d.quot, extra = d.rem; + for (int x = p1.x > 0 ? p1.x : 0; x < (int)self->width && x < p2.x + 1; x++) { + int y_p = (int)line_y(l, x); + for (int y = MAX(0, y_p - delta); y < MIN(y_p + delta + extra, (int)self->height); y++) { + self->mask[x + y * self->width] = 255; + } + } +} + +static void +half_cross_line(Canvas *self, uint level, Corner corner) { + uint my = minus(self->height, 1) / 2; Point p1 = {0}, p2 = {0}; + switch (corner) { + case TOP_LEFT: p2.x = minus(self->width, 1); p2.y = my; break; + case BOTTOM_LEFT: p1.x = minus(self->width, 1); p1.y = my; p2.y = self->height -1; break; + case TOP_RIGHT: p1.x = minus(self->width, 1); p2.y = my; break; + case BOTTOM_RIGHT: p2.x = minus(self->width, 1), p2.y = minus(self->height, 1); p1.y = my; break; + } + thick_line(self, thickness(self, level, true), p1, p2); +} + +typedef struct CubicBezier { + Point start, c1, c2, end; +} CubicBezier; + +#define bezier_eq(which) { \ + double tm1 = 1 - t; \ + double tm1_3 = tm1 * tm1 * tm1; \ + double t_3 = t * t * t; \ + return tm1_3 * cb.start.which + 3 * t * tm1 * (tm1 * cb.c1.which + t * cb.c2.which) + t_3 * cb.end.which; \ +} +static double +bezier_x(CubicBezier cb, double t) { bezier_eq(x); } +static double +bezier_y(CubicBezier cb, double t) { bezier_eq(y); } +#undef bezier_eq + +static int +find_bezier_for_D(int width, int height) { + int cx = width - 1, last_cx = cx; + CubicBezier cb = {.end={0, height - 1}, .c2={0, height - 1}}; + while (true) { + cb.c1.x = cx; cb.c2.x = cx; + if (bezier_x(cb, 0.5) > width - 1) return last_cx; + last_cx = cx++; + } +} + +static double +find_t_for_x(CubicBezier cb, int x, double start_t) { + if (fabs(bezier_x(cb, start_t) - x) < 0.1) return start_t; + static const double t_limit = 0.5; + double increment = t_limit - start_t; + if (increment <= 0) return start_t; + while (true) { + double q = bezier_x(cb, start_t + increment); + if (fabs(q - x) < 0.1) return start_t + increment; + if (q > x) { + increment /= 2.0; + if (increment < 1e-6) { + log_error("Failed to find cubic bezier t for x=%d\n", x); + return start_t; + } + } else { + start_t += increment; + increment = t_limit - start_t; + if (increment <= 0) return start_t; + } + } +} + + +static void +get_bezier_limits(Canvas *self, CubicBezier cb) { + int start_x = (int)bezier_x(cb, 0), max_x = (int)bezier_x(cb, 0.5); + double last_t = 0.; + for (int x = start_x; x < max_x + 1; x++) { + if (x > start_x) last_t = find_t_for_x(cb, x, last_t); + double upper = bezier_y(cb, last_t), lower = bezier_y(cb, 1.0 - last_t); + if (fabs(upper - lower) <= 2.0) break; // avoid pip on end of D + append_limit(self, lower, upper); + } +} + +static void +filled_D(Canvas *self, bool left) { + int c1x = find_bezier_for_D(self->width, self->height); + CubicBezier cb = {.end={0, self->height-1}, .c1 = {c1x, 0}, .c2 = {c1x, self->height - 1}}; + get_bezier_limits(self, cb); + if (left) fill_region(self, false); + else { + RAII_ALLOC(uint8_t, mbuf, calloc(self->width, self->height)); + if (!mbuf) fatal("Out of memory"); + uint8_t *buf = self->mask; + self->mask = mbuf; + fill_region(self, false); + self->mask = buf; + for (uint y = 0; y < self->height; y++) { + uint offset = y * self->width; + for (uint src_x = 0; src_x < self->width; src_x++) { + uint dest_x = self->width - 1 - src_x; + buf[offset + dest_x] = mbuf[offset + src_x]; + } + } + + } +} + +#define NAME position_set +#define KEY_TY int64_t +#include "kitty-verstable.h" + +#define draw_parametrized_curve(self, level, xfunc, yfunc) { \ + div_t d = div(thickness(self, level, true)); \ + int delta = d.quot, extra = d.rem; \ + uint num_samples = self->height * 8; \ + position_set seen; vt_init(&seen); \ + for (uint i = 0; i < num_samples; i++) { \ + double t = i / (double)num_samples; \ + int32_t x_p = xfunc, y_p = yfunc; \ + int64_t key = (x_p << 32) | y_p; \ + position_set_itr q = vt_get(&seen, key); \ + if (!vt_is_end(q)) continue; \ + if (vt_is_end(vt_insert(&seen, key))) fatal("Out of memory"); \ + for (int y = MAX(0, y_p - delta); y < MIN(y_p + delta + extra, (int)self->height); y++) { \ + uint offset = y * self->width, start = MAX(0, x_p - delta); \ + memset(self->mask + offset + start, 255, minus((uint)MIN(x_p + delta + extra, self->width), start)); \ + } \ + } \ + vt_cleanup(&seen); \ +} + +void +render_box_char(char_type ch, uint8_t *buf, unsigned width, unsigned height, double dpi_x, double dpi_y) { + Canvas canvas = {.mask=buf, .width = width, .height = height, .dpi={.x=dpi_x, .y=dpi_y}, .supersample_factor=1u}, ss = canvas; + ss.mask = buf + width*height; ss.supersample_factor = SUPERSAMPLE_FACTOR; ss.width *= SUPERSAMPLE_FACTOR; ss.height *= SUPERSAMPLE_FACTOR; + memset(canvas.mask, 0, width * height * sizeof(canvas.mask[0])); + Canvas *c = &canvas; + +#define CC(expr) expr; break +#define SS(expr) memset(ss.mask, 0, ss.width * ss.height * sizeof(ss.mask[0])); c = &ss, expr; downsample(&ss, &canvas); break +#define C(ch, func, ...) case ch: CC(func(c, __VA_ARGS__)) +#define S(ch, func, ...) case ch: SS(func(c, __VA_ARGS__)) + + switch(ch) { + C(L'─', hline, 1); + C(L'━', hline, 3); + C(L'│', vline, 1); + C(L'┃', vline, 3); + + C(L'╌', hholes, 1, 1); + C(L'╍', hholes, 3, 1); + C(L'┄', hholes, 1, 2); + C(L'┅', hholes, 3, 2); + C(L'┈', hholes, 1, 3); + C(L'┉', hholes, 3, 3); + + C(L'╎', vholes, 1, 1); + C(L'╏', vholes, 3, 1); + C(L'┆', vholes, 1, 2); + C(L'┇', vholes, 3, 2); + C(L'┊', vholes, 1, 3); + C(L'┋', vholes, 3, 3); + + C(L'╴', half_hline, 1, false, 0); + C(L'╵', half_vline, 1, false, 0); + C(L'╶', half_hline, 1, true, 0); + C(L'╷', half_vline, 1, true, 0); + C(L'╸', half_hline, 3, false, 0); + C(L'╹', half_vline, 3, false, 0); + C(L'╺', half_hline, 3, true, 0); + C(L'╻', half_vline, 3, true, 0); + case L'╾': CC(half_hline(c, 3, false, 0); half_hline(c, 1, true, 0)); + case L'╼': CC(half_hline(c, 1, false, 0); half_hline(c, 3, true, 0)); + case L'╿': CC(half_vline(c, 3, false, 0); half_vline(c, 1, true, 0)); + case L'╽': CC(half_vline(c, 1, false, 0); half_vline(c, 3, true, 0)); + + S(L'', triangle, true, false); + S(L'', triangle, true, true); + case L'': SS(half_cross_line(c, 1, TOP_LEFT); half_cross_line(c, 1, BOTTOM_LEFT)); + S(L'', triangle, false, false); + S(L'', triangle, false, true); + case L'': SS(half_cross_line(c, 1, TOP_RIGHT); half_cross_line(c, 1, BOTTOM_RIGHT)); + + S(L'', filled_D, true); + S(L'◗', filled_D, true); + S(L'', filled_D, false); + S(L'◖', filled_D, false); + } +#undef CC +#undef SS +#undef C +#undef S + free(canvas.holes); free(canvas.y_limits); + free(ss.holes); free(ss.y_limits); +} diff --git a/kitty/decorations.h b/kitty/decorations.h index 80ffb06f1..8d47281f7 100644 --- a/kitty/decorations.h +++ b/kitty/decorations.h @@ -23,3 +23,5 @@ DecorationGeometry add_missing_glyph(uint8_t *buf, FontCellMetrics fcm); DecorationGeometry add_beam_cursor(uint8_t *buf, FontCellMetrics fcm, double dpi_x); DecorationGeometry add_underline_cursor(uint8_t *buf, FontCellMetrics fcm, double dpi_y); DecorationGeometry add_hollow_cursor(uint8_t *buf, FontCellMetrics fcm, double dpi_x, double dpi_y); +void render_box_char(char_type ch, uint8_t *buf, unsigned width, unsigned height, double dpi_x, double dpi_y); +#define SUPERSAMPLE_FACTOR 4u diff --git a/kitty/fast_data_types.pyi b/kitty/fast_data_types.pyi index 80c3279da..10d6b80ac 100644 --- a/kitty/fast_data_types.pyi +++ b/kitty/fast_data_types.pyi @@ -998,7 +998,7 @@ def ring_bell() -> None: pass -def concat_cells(cell_width: int, cell_height: int, is_32_bit: bool, cells: Tuple[bytes, ...]) -> bytes: +def concat_cells(cell_width: int, cell_height: int, is_32_bit: bool, cells: Tuple[bytes, ...], bgcolor: int = 0) -> bytes: pass @@ -1715,6 +1715,7 @@ def glfw_get_system_color_theme(query_if_unintialized: bool = True) -> Literal[' def set_redirect_keys_to_overlay(os_window_id: int, tab_id: int, window_id: int, overlay_window_id: int) -> None: ... def buffer_keys_in_window(os_window_id: int, tab_id: int, window_id: int, enabled: bool = True) -> bool: ... def sprite_idx_to_pos(idx: int, xnum: int, ynum: int) -> tuple[int, int, int]: ... +def render_box_char(ch: int, width: int, height: int, dpi_x: float = 96.0, dpi_y: float = 96.0) -> bytes: ... class MousePosition(TypedDict): cell_x: int diff --git a/kitty/fonts.c b/kitty/fonts.c index abec47a66..30a0481db 100644 --- a/kitty/fonts.c +++ b/kitty/fonts.c @@ -67,8 +67,9 @@ typedef struct { typedef struct Canvas { pixel *buf; + uint8_t *alpha_mask; unsigned current_cells, alloced_cells, alloced_scale, current_scale; - size_t size_in_bytes; + size_t size_in_bytes, alpha_mask_sz_in_bytes; } Canvas; #define NAME fallback_font_map_t @@ -175,6 +176,12 @@ ensure_canvas_can_fit(FontGroup *fg, unsigned cells, unsigned scale) { fg->canvas.current_scale = scale; if (fg->canvas.buf) memset(fg->canvas.buf, 0, cs(cells, scale)); #undef cs + size_in_bytes = (sizeof(fg->canvas.alpha_mask[0]) * SUPERSAMPLE_FACTOR * SUPERSAMPLE_FACTOR * 2 * fg->fcm.cell_width * fg->fcm.cell_height * scale * scale); + if (size_in_bytes > fg->canvas.alpha_mask_sz_in_bytes) { + fg->canvas.alpha_mask_sz_in_bytes = size_in_bytes; + fg->canvas.alpha_mask = malloc(fg->canvas.alpha_mask_sz_in_bytes * sizeof(fg->canvas.alpha_mask[0])); + if (!fg->canvas.alpha_mask) fatal("Out of memory allocating canvas"); + } } @@ -225,7 +232,7 @@ del_font(Font *f) { static void del_font_group(FontGroup *fg) { - free(fg->canvas.buf); fg->canvas.buf = NULL; fg->canvas = (Canvas){0}; + free(fg->canvas.buf); free(fg->canvas.alpha_mask); fg->canvas = (Canvas){0}; free_sprite_data((FONTS_DATA_HANDLE)fg); vt_cleanup(&fg->fallback_font_map); vt_cleanup(&fg->scaled_font_map); @@ -1034,18 +1041,16 @@ render_box_cell(FontGroup *fg, RunFont rf, CPUCell *cpu_cell, GPUCell *gpu_cell, RAII_PyObject(ret, NULL); uint8_t *alpha_mask = NULL; ensure_canvas_can_fit(fg, num_glyphs + 1, rf.scale); if (num_glyphs == 1) { - ret = PyObject_CallFunction(box_drawing_function, "IIId", ch, width, height, (fg->logical_dpi_x + fg->logical_dpi_y) / 2.0); - if (ret == NULL) failed; - alpha_mask = PyLong_AsVoidPtr(PyTuple_GET_ITEM(ret, 0)); + render_box_char(ch, fg->canvas.alpha_mask, width, height, fg->logical_dpi_x, fg->logical_dpi_y); + alpha_mask = fg->canvas.alpha_mask; } else { alpha_mask = ((uint8_t*)fg->canvas.buf) + fg->canvas.size_in_bytes - (num_glyphs * width * height); unsigned cnum = 0; for (unsigned i = 0; i < num_glyphs; i++) { unsigned int ch = global_glyph_render_scratch.lc->chars[cnum++]; while (!ch) ch = global_glyph_render_scratch.lc->chars[cnum++]; - RAII_PyObject(r, PyObject_CallFunction(box_drawing_function, "IIId", ch, width, height, (fg->logical_dpi_x + fg->logical_dpi_y) / 2.0)); - if (r == NULL) failed; - uint8_t *src = PyLong_AsVoidPtr(PyTuple_GET_ITEM(r, 0)); + render_box_char(ch, fg->canvas.alpha_mask, width, height, fg->logical_dpi_x, fg->logical_dpi_y); + uint8_t *src = fg->canvas.alpha_mask; for (unsigned y = 0; y < height; y++) { uint8_t *dest_row = alpha_mask + y*num_glyphs*width + i*width; memcpy(dest_row, src + y * width, width); @@ -2076,13 +2081,27 @@ test_render_line(PyObject UNUSED *self, PyObject *args) { Py_RETURN_NONE; } +static uint32_t +alpha_blend(uint32_t fg, uint32_t bg) { + uint32_t r1 = (fg >> 16) & 0xFF, g1 = (fg >> 8) & 0xFF, b1 = fg & 0xFF, a = (fg >> 24) & 0xff; + uint32_t r2 = (bg >> 16) & 0xFF, g2 = (bg >> 8) & 0xFF, b2 = bg & 0xFF; + float alpha = a / 255.f; + +#define mix(x) uint32_t x = ((uint32_t)(alpha * x##1 + (1.0f - alpha) * x##2)) & 0xff; + mix(r); mix(g); mix(b); +#undef mix + // Combine components into result color + return (0xff000000) | (r << 16) | (g << 8) | b; +} + static PyObject* concat_cells(PyObject UNUSED *self, PyObject *args) { // Concatenate cells returning RGBA data unsigned int cell_width, cell_height; int is_32_bit; PyObject *cells; - if (!PyArg_ParseTuple(args, "IIpO!", &cell_width, &cell_height, &is_32_bit, &PyTuple_Type, &cells)) return NULL; + unsigned long bgcolor = 0; + if (!PyArg_ParseTuple(args, "IIpO!|k", &cell_width, &cell_height, &is_32_bit, &PyTuple_Type, &cells, &bgcolor)) return NULL; size_t num_cells = PyTuple_GET_SIZE(cells), r, c, i; PyObject *ans = PyBytes_FromStringAndSize(NULL, (size_t)4 * cell_width * cell_height * num_cells); if (ans == NULL) return PyErr_NoMemory(); @@ -2092,22 +2111,11 @@ concat_cells(PyObject UNUSED *self, PyObject *args) { void *s = ((uint8_t*)PyBytes_AS_STRING(PyTuple_GET_ITEM(cells, c))); if (is_32_bit) { pixel *src = (pixel*)s + cell_width * r; - for (i = 0; i < cell_width; i++, dest++) { - uint8_t *rgba = (uint8_t*)dest; - rgba[0] = (src[i] >> 24) & 0xff; - rgba[1] = (src[i] >> 16) & 0xff; - rgba[2] = (src[i] >> 8) & 0xff; - rgba[3] = src[i] & 0xff; - } + for (i = 0; i < cell_width; i++, dest++) dest[0] = alpha_blend(src[0], bgcolor); } else { uint8_t *src = (uint8_t*)s + cell_width * r; - for (i = 0; i < cell_width; i++, dest++) { - uint8_t *rgba = (uint8_t*)dest; - if (src[i]) { memset(rgba, 0xff, 3); rgba[3] = src[i]; } - else *dest = 0; - } + for (i = 0; i < cell_width; i++, dest++) dest[0] = alpha_blend(0x00ffffff | ((src[i] & 0xff) << 24), bgcolor); } - } } return ans; @@ -2293,6 +2301,18 @@ sprite_idx_to_pos(PyObject *self UNUSED, PyObject *args) { return Py_BuildValue("III", x, y, z); } +static PyObject* +pyrender_box_char(PyObject *self UNUSED, PyObject *args) { + unsigned int ch; + unsigned long width, height; double dpi_x = 96., dpi_y = 96.; + if (!PyArg_ParseTuple(args, "Ikk|dd", &ch, &width, &height, &dpi_x, &dpi_y)) return NULL; + RAII_PyObject(ans, PyBytes_FromStringAndSize(NULL, width*16 * height*16)); + if (!ans) return NULL; + render_box_char(ch, (uint8_t*)PyBytes_AS_STRING(ans), width, height, dpi_x, dpi_y); + if (_PyBytes_Resize(&ans, width * height) != 0) return NULL; + return Py_NewRef(ans); +} + static PyMethodDef module_methods[] = { METHODB(set_font_data, METH_VARARGS), METHODB(sprite_idx_to_pos, METH_VARARGS), @@ -2308,6 +2328,7 @@ static PyMethodDef module_methods[] = { METHODB(test_render_line, METH_VARARGS), METHODB(get_fallback_font, METH_VARARGS), {"specialize_font_descriptor", (PyCFunction)pyspecialize_font_descriptor, METH_VARARGS, ""}, + {"render_box_char", (PyCFunction)pyrender_box_char, METH_VARARGS, ""}, {NULL, NULL, 0, NULL} /* Sentinel */ }; diff --git a/kitty/fonts/box_drawing.py b/kitty/fonts/box_drawing.py index 4c435bad3..295e5fe90 100644 --- a/kitty/fonts/box_drawing.py +++ b/kitty/fonts/box_drawing.py @@ -8,8 +8,7 @@ import math from collections.abc import Iterable, Iterator, MutableSequence, Sequence -from functools import lru_cache, wraps -from functools import partial as p +from functools import lru_cache, partial from itertools import repeat from typing import Any, Callable, Literal, Optional @@ -18,6 +17,13 @@ _dpi = 96.0 BufType = MutableSequence[int] +def p(f: Any, *a: Any, **kw: Any) -> Any: + ans = partial(f, *a, **kw) + if hasattr(f, 'supersample_factor'): + setattr(ans, 'supersample_factor', f.supersample_factor) + return ans + + def set_scale(new_scale: Sequence[float]) -> None: global scale scale = (new_scale[0], new_scale[1], new_scale[2], new_scale[3]) @@ -144,6 +150,11 @@ def cross(buf: BufType, width: int, height: int, a: int = 1, b: int = 1, c: int half_vline(buf, width, height, level=d, which='bottom') +def print_hash(x: bytes, prefix: str = 'native:') -> None: + from hashlib import sha256 + print(prefix, sha256(x).hexdigest()) + + def downsample(src: BufType, dest: BufType, dest_width: int, dest_height: int, factor: int = 4) -> None: src_width = factor * dest_width @@ -167,19 +178,23 @@ class SSByteArray(bytearray): supersample_factor = 1 +def ss(buf: BufType, width: int, height: int, *funcs: Callable[..., None]) -> None: + supersample_factor = getattr(funcs[0], 'supersample_factor') + w, h = supersample_factor * width, supersample_factor * height + ssbuf = SSByteArray(w * h) + ssbuf.supersample_factor = supersample_factor + for f in funcs: + f(ssbuf, w, h) + downsample(ssbuf, buf, width, height, factor=supersample_factor) + + def supersampled(supersample_factor: int = 4) -> Callable[[Callable[..., None]], Callable[..., None]]: # Anti-alias the drawing performed by the wrapped function by # using supersampling def create_wrapper(f: Callable[..., None]) -> Callable[..., None]: - @wraps(f) - def supersampled_wrapper(buf: BufType, width: int, height: int, *args: Any, **kw: Any) -> None: - w, h = supersample_factor * width, supersample_factor * height - ssbuf = SSByteArray(w * h) - ssbuf.supersample_factor = supersample_factor - f(ssbuf, w, h, *args, **kw) - downsample(ssbuf, buf, width, height, factor=supersample_factor) - return supersampled_wrapper + setattr(f, 'supersample_factor', supersample_factor) + return f return create_wrapper @@ -1429,32 +1444,84 @@ for i in range(1, 7): 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]: - func(buf, width, height) + funcs = box_chars[ch] + if hasattr(funcs[0], 'supersample_factor'): + ss(buf, width, height, *funcs) + else: + for func in box_chars[ch]: + func(buf, width, height) return buf -def test_char(ch: str, sz: int = 48) -> None: - # kitty +runpy "from kitty.fonts.box_drawing import test_char; test_char('XXX')" +def test_chars(chars: str = '╌', sz: int = 128) -> None: + # kitty +runpy "from kitty.fonts.box_drawing import test_chars; test_chars('XXX')" from kitty.fast_data_types import concat_cells, set_send_sprite_to_gpu + from kitty.fast_data_types import render_box_char as native_render_box_char from .render import display_bitmap, setup_for_testing + if not chars: + import sys + chars = sys.argv[-1] with setup_for_testing('monospace', sz) as (_, width, height): - buf = bytearray(width * height) try: - render_box_char(ch, buf, width, height) + for ch in chars: + print('Rendering', ch) + buf = bytearray(width * height) + render_box_char(ch, buf, width, height) - def join_cells(*cells: bytes) -> bytes: - cells = tuple(bytes(x) for x in cells) - return concat_cells(width, height, False, cells) + def join_cells(*cells: bytes) -> bytes: + cells = tuple(bytes(x) for x in cells) + return concat_cells(width, height, False, cells) - rgb_data = join_cells(buf) - display_bitmap(rgb_data, width, height) - print() + rgb_data = join_cells(buf) + display_bitmap(rgb_data, width, height) + print() + + nb = native_render_box_char(ord(ch), width, height) + rgb_data = concat_cells(width, height, False, (nb,)) + display_bitmap(rgb_data, width, height) + print() finally: set_send_sprite_to_gpu(None) +def port_chars() -> None: + from kitty.fast_data_types import concat_cells + from kitty.fast_data_types import render_box_char as native_render_box_char + + from .render import display_bitmap, setup_for_testing + + def join_cells(*cells: bytes) -> bytes: + cells = tuple(bytes(x) for x in cells) + return concat_cells(width, height, False, cells) + + for sz in (127, 8, 11, 12, 13): + with setup_for_testing('monospace', sz) as (_, width, height): + for ch in box_chars: + buf = bytearray(width * height) + render_box_char(ch, buf, width, height) + nb = native_render_box_char(ord(ch), width, height) + if bytes(buf) != nb: + print(f'Failed to match for char: {ch=} ({hex(ord(ch))}) at {width=} {height=}') + count = 0 + for y in range(height): + for x in range(width): + if buf[y*width + x] != nb[y*width + x]: + print(f'differing byte at {x=} {y=}. Expected: {buf[y*width + x]} Actual: {nb[y*width + x]}') + count += 1 + if count > 5: + break + if count > 5: + break + rgb_data = join_cells(buf) + display_bitmap(rgb_data, width, height) + print() + rgb_data = concat_cells(width, height, False, (nb,)) + display_bitmap(rgb_data, width, height) + print() + raise SystemExit(1) + + def test_drawing(sz: int = 48, family: str = 'monospace', start: int = 0x2500, num_rows: int = 10, num_cols: int = 16) -> None: from kitty.fast_data_types import concat_cells, set_send_sprite_to_gpu diff --git a/kitty/options/definition.py b/kitty/options/definition.py index c563728b4..cddfd42fa 100644 --- a/kitty/options/definition.py +++ b/kitty/options/definition.py @@ -199,7 +199,7 @@ might cause rendering artifacts, so use with care. ''') opt('box_drawing_scale', '0.001, 1, 1.5, 2', - option_type='box_drawing_scale', + option_type='box_drawing_scale', ctype='!box_drawing_scale', long_text=''' The sizes of the lines used for the box drawing Unicode characters. These values are in pts. They will be scaled by the monitor DPI to arrive at a pixel value. diff --git a/kitty/options/to-c-generated.h b/kitty/options/to-c-generated.h index 66dc88c2d..28e9cbdca 100644 --- a/kitty/options/to-c-generated.h +++ b/kitty/options/to-c-generated.h @@ -70,6 +70,19 @@ convert_from_opts_modify_font(PyObject *py_opts, Options *opts) { Py_DECREF(ret); } +static void +convert_from_python_box_drawing_scale(PyObject *val, Options *opts) { + box_drawing_scale(val, opts); +} + +static void +convert_from_opts_box_drawing_scale(PyObject *py_opts, Options *opts) { + PyObject *ret = PyObject_GetAttrString(py_opts, "box_drawing_scale"); + if (ret == NULL) return; + convert_from_python_box_drawing_scale(ret, opts); + Py_DECREF(ret); +} + static void convert_from_python_undercurl_style(PyObject *val, Options *opts) { opts->undercurl_style = undercurl_style(val); @@ -1174,6 +1187,8 @@ convert_opts_from_python_opts(PyObject *py_opts, Options *opts) { if (PyErr_Occurred()) return false; convert_from_opts_modify_font(py_opts, opts); if (PyErr_Occurred()) return false; + convert_from_opts_box_drawing_scale(py_opts, opts); + if (PyErr_Occurred()) return false; convert_from_opts_undercurl_style(py_opts, opts); if (PyErr_Occurred()) return false; convert_from_opts_underline_exclusion(py_opts, opts); diff --git a/kitty/options/to-c.h b/kitty/options/to-c.h index 8173c8ef6..6609a5f60 100644 --- a/kitty/options/to-c.h +++ b/kitty/options/to-c.h @@ -390,6 +390,13 @@ underline_exclusion(PyObject *val, Options *opts) { else opts->underline_exclusion.unit = 0; } +static inline void +box_drawing_scale(PyObject *val, Options *opts) { + for (unsigned i = 0; i < MIN(arraysz(opts->box_drawing_scale), (size_t)PyTuple_GET_SIZE(val)); i++) { + opts->box_drawing_scale[i] = PyFloat_AsFloat(PyTuple_GET_ITEM(val, i)); + } +} + static inline void text_composition_strategy(PyObject *val, Options *opts) { if (!PyUnicode_Check(val)) { PyErr_SetString(PyExc_TypeError, "text_rendering_strategy must be a string"); return; } diff --git a/kitty/state.h b/kitty/state.h index 25005c216..bb5fd21bf 100644 --- a/kitty/state.h +++ b/kitty/state.h @@ -125,6 +125,7 @@ typedef struct { struct { Animation *cursor, *visual_bell; } animation; unsigned undercurl_style; struct { float thickness; int unit; } underline_exclusion; + float box_drawing_scale[4]; } Options; typedef struct WindowLogoRenderData {