summaryrefslogtreecommitdiff
path: root/src/gui
diff options
context:
space:
mode:
Diffstat (limited to 'src/gui')
-rw-r--r--src/gui/font.c30
-rw-r--r--src/gui/font.h8
-rw-r--r--src/gui/gui.zig46
3 files changed, 50 insertions, 34 deletions
diff --git a/src/gui/font.c b/src/gui/font.c
index 5989dd4e..5ea595f9 100644
--- a/src/gui/font.c
+++ b/src/gui/font.c
@@ -3,6 +3,7 @@
// fallback, and the light target for shape-preserving grayscale text.
#include "font.h"
#include <freetype-zig.h>
+#include <freetype/ftbitmap.h>
#include <stdlib.h>
#include <string.h>
@@ -126,7 +127,7 @@ void ui_font_cell_metrics(UIFont *f, float px, int32_t *cell_w, int32_t *cell_h,
*ascent = asc > 0 ? asc : 1;
}
-static int raster(UIFont *f, float px, int32_t cp, uint8_t *out,
+static int raster(UIFont *f, float px, int32_t cp, int bold, uint8_t *out,
int32_t stride, int32_t cell_w, int32_t cell_h,
int32_t ascent, int centered) {
if (!f || !out || stride < cell_w || cell_w <= 0 || cell_h <= 0)
@@ -140,6 +141,22 @@ static int raster(UIFont *f, float px, int32_t cp, uint8_t *out,
FT_Get_Char_Index(f->face, (FT_ULong)(uint32_t)cp);
if (FT_Load_Glyph(f->face, glyph_index, load_flags()) != 0)
return 0;
+ if (bold) {
+ // Synthetic bold thickens the hinted outline before it is rendered, so
+ // the wider stems are rasterized as one shape and keep hinted edges; the
+ // advance is untouched, as the cell grid requires. Strength is Ghostty's
+ // heuristic: 1/32 of the line height in 26.6, rounded up (one pixel at a
+ // 32 px line). A bitmap strike has no outline: widen it by one whole pixel,
+ // the classic overstrike, which keeps its pixels sharp.
+ FT_GlyphSlot slot = f->face->glyph;
+ if (slot->format == FT_GLYPH_FORMAT_OUTLINE) {
+ const FT_Pos height = f->face->size->metrics.height;
+ FT_Outline_Embolden(&slot->outline, (height + 31) / 32);
+ } else if (slot->format == FT_GLYPH_FORMAT_BITMAP &&
+ FT_GlyphSlot_Own_Bitmap(slot) == 0) {
+ FT_Bitmap_Embolden(f->library, &slot->bitmap, 64, 0);
+ }
+ }
if (FT_Render_Glyph(f->face->glyph, FT_RENDER_MODE_NORMAL) != 0)
return 0;
@@ -187,13 +204,14 @@ static int raster(UIFont *f, float px, int32_t cp, uint8_t *out,
return 1;
}
-int ui_font_raster(UIFont *f, float px, int32_t cp, uint8_t *out,
+int ui_font_raster(UIFont *f, float px, int32_t cp, int bold, uint8_t *out,
int32_t stride, int32_t cell_w, int32_t cell_h,
int32_t ascent) {
- return raster(f, px, cp, out, stride, cell_w, cell_h, ascent, 0);
+ return raster(f, px, cp, bold, out, stride, cell_w, cell_h, ascent, 0);
}
-int ui_font_raster_centered(UIFont *f, float px, int32_t cp, uint8_t *out,
- int32_t stride, int32_t width, int32_t height) {
- return raster(f, px, cp, out, stride, width, height, 0, 1);
+int ui_font_raster_centered(UIFont *f, float px, int32_t cp, int bold,
+ uint8_t *out, int32_t stride, int32_t width,
+ int32_t height) {
+ return raster(f, px, cp, bold, out, stride, width, height, 0, 1);
}
diff --git a/src/gui/font.h b/src/gui/font.h
index d965b418..4410bb7c 100644
--- a/src/gui/font.h
+++ b/src/gui/font.h
@@ -33,14 +33,16 @@ void ui_font_cell_metrics(UIFont *f, float px, int32_t *cell_w, int32_t *cell_h,
// Rasterize `cp` into `out` as a hinted grayscale cell mask (`stride` bytes per
// row). The origin is the baseline `ascent` pixels from the top. Pixels outside
// the glyph are zero. Missing codepoints render the face's .notdef glyph.
+// `bold` emboldens the outline before rendering (synthetic bold, same advance).
// Returns 1 when the glyph produced coverage, 0 for blank/error.
-int ui_font_raster(UIFont *f, float px, int32_t cp, uint8_t *out,
+int ui_font_raster(UIFont *f, float px, int32_t cp, int bold, uint8_t *out,
int32_t stride, int32_t cell_w, int32_t cell_h,
int32_t ascent);
// Center bitmap ink bounds, independent of advance and baseline bearings.
-int ui_font_raster_centered(UIFont *f, float px, int32_t cp, uint8_t *out,
- int32_t stride, int32_t width, int32_t height);
+int ui_font_raster_centered(UIFont *f, float px, int32_t cp, int bold,
+ uint8_t *out, int32_t stride, int32_t width,
+ int32_t height);
void ui_font_free(UIFont *f);
diff --git a/src/gui/gui.zig b/src/gui/gui.zig
index 48087d0b..7ad9fe9a 100644
--- a/src/gui/gui.zig
+++ b/src/gui/gui.zig
@@ -253,7 +253,7 @@ test "FreeType atlas raster is grayscale, bounded, and clears blank cells" {
var bitmap: [128 * 128]u8 = @splat(0xaa);
try std.testing.expectEqual(
@as(c_int, 1),
- c.ui_font_raster(font, size, 'H', &bitmap, stride, cell_w, cell_h, ascent),
+ c.ui_font_raster(font, size, 'H', 0, &bitmap, stride, cell_w, cell_h, ascent),
);
var ink: usize = 0;
@@ -269,7 +269,11 @@ test "FreeType atlas raster is grayscale, bounded, and clears blank cells" {
}
try std.testing.expect(ink > 0);
try std.testing.expect(soft > 0);
- decorateGlyph(&bitmap, @intCast(stride), @intCast(cell_w), @intCast(cell_h), ascent, .{ .bold = true });
+ // Synthetic bold thickens the outline inside the same cell.
+ try std.testing.expectEqual(
+ @as(c_int, 1),
+ c.ui_font_raster(font, size, 'H', 1, &bitmap, stride, cell_w, cell_h, ascent),
+ );
var bold_coverage: usize = 0;
for (0..@intCast(cell_h)) |y| {
for (0..@intCast(cell_w)) |x| bold_coverage += bitmap[y * @as(usize, @intCast(stride)) + x];
@@ -280,7 +284,7 @@ test "FreeType atlas raster is grayscale, bounded, and clears blank cells" {
try std.testing.expectEqual(
@as(c_int, 0),
- c.ui_font_raster(font, size, ' ', &bitmap, stride, cell_w, cell_h, ascent),
+ c.ui_font_raster(font, size, ' ', 0, &bitmap, stride, cell_w, cell_h, ascent),
);
for (0..@intCast(cell_h)) |y|
for (0..@intCast(cell_w)) |x|
@@ -326,6 +330,7 @@ test "tagline glyph shrinks into its own cell and stays vertically centered" {
font,
body_scale,
'H',
+ 0,
&body,
stride,
cell_w,
@@ -336,6 +341,7 @@ test "tagline glyph shrinks into its own cell and stays vertically centered" {
font,
tag.scale,
'H',
+ 0,
&tagline,
stride,
@intCast(tag.width),
@@ -2311,7 +2317,7 @@ fn runNative(init: std.process.Init, opts_in: pardes.Options, attach: ?[]const u
defer if (g.font_bytes.len != 0) gpa.free(g.font_bytes); // set by Font, if it ran
defer c.ui_font_free(g.font);
defer gpa.free(g.scroll_edge); // grown on demand by stepScroll
- _ = c.ui_font_raster(font, scale, ' ', atlas_stage.ptr, @intCast(atlas_w), @intCast(cell_w), @intCast(cell_h), asc);
+ _ = c.ui_font_raster(font, scale, ' ', 0, atlas_stage.ptr, @intCast(atlas_w), @intCast(cell_w), @intCast(cell_h), asc);
g.pen_x = cell_w;
if (attach) |requested| return attachRequested(gpa, &g, requested);
@@ -5896,7 +5902,7 @@ fn refitFont(g: *Gui, core: ?*pardes.Pardes) void {
fn resetGlyphAtlas(g: *Gui) void {
g.glyphs.clearRetainingCapacity();
@memset(g.atlas_stage, 0);
- _ = c.ui_font_raster(g.font, g.scale, ' ', g.atlas_stage.ptr, @intCast(atlas_w), @intCast(g.cell_w), @intCast(g.cell_h), g.ascent);
+ _ = c.ui_font_raster(g.font, g.scale, ' ', 0, g.atlas_stage.ptr, @intCast(atlas_w), @intCast(g.cell_w), @intCast(g.cell_h), g.ascent);
g.space_slot = .{ .u = 0, .v = 0 };
g.pen_x = g.cell_w;
g.pen_y = 0;
@@ -5938,11 +5944,11 @@ fn cachedGlyph(g: *Gui, cp: u32, role: pardes.FontRole, decoration: GlyphDecorat
const raster_w = if (centered) slot_w else if (role == .tagline) g.tagline_width else g.cell_w;
const raster_h = if (role == .tagline) g.tagline_height else g.cell_h;
if (centered) {
- _ = c.ui_font_raster_centered(face, scale, @intCast(cp), out, @intCast(atlas_w), @intCast(raster_w), @intCast(raster_h));
+ _ = c.ui_font_raster_centered(face, scale, @intCast(cp), @intFromBool(decoration.bold), out, @intCast(atlas_w), @intCast(raster_w), @intCast(raster_h));
} else {
- _ = c.ui_font_raster(face, scale, @intCast(cp), out, @intCast(atlas_w), @intCast(raster_w), @intCast(raster_h), baseline);
+ _ = c.ui_font_raster(face, scale, @intCast(cp), @intFromBool(decoration.bold), out, @intCast(atlas_w), @intCast(raster_w), @intCast(raster_h), baseline);
}
- decorateGlyph(out, atlas_w, raster_w, raster_h, baseline, decoration);
+ decorateLine(out, atlas_w, raster_w, raster_h, baseline, decoration);
if (centered) centerGlyphInk(out, raster_w, raster_h);
g.glyphs.put(key, s) catch return null;
g.pen_x += slot_w;
@@ -6056,23 +6062,13 @@ test "pane indicator halves share dissolve changes and safe exhausted atlas" {
}
}
-// Decorations stay inside the existing atlas slot, so text advance, caret and
-// selection geometry are identical to the regular glyph. The cached mask keeps
-// this work out of steady-state rendering and also decorates terminal spaces.
-fn decorateGlyph(out: [*]u8, stride: usize, width: usize, height: usize, baseline: i32, decoration: GlyphDecoration) void {
+// Underline and strikethrough stay inside the existing atlas slot, so text
+// advance, caret and selection geometry are identical to the regular glyph.
+// The cached mask keeps this work out of steady-state rendering and also
+// decorates terminal spaces. Bold is not drawn here: the rasterizer emboldens
+// the outline.
+fn decorateLine(out: [*]u8, stride: usize, width: usize, height: usize, baseline: i32, decoration: GlyphDecoration) void {
if (width == 0 or height == 0) return;
- if (decoration.bold) {
- // A fractional one-pixel embolden preserves counters at small UI sizes.
- // Right-to-left traversal reads the original left neighbour each time.
- for (0..height) |y| {
- var x = width - 1;
- while (x > 0) : (x -= 1) {
- const previous = out[y * stride + x - 1];
- const target = &out[y * stride + x];
- target.* = @max(target.*, @as(u8, @intCast(@as(u16, previous) * 3 / 4)));
- }
- }
- }
if (decoration.underline != .off) {
const base: usize = @intCast(@max(0, baseline));
const row = @min(base + 1, height - 1);
@@ -6115,7 +6111,7 @@ test "glyph decorations render spaces and stay inside their atlas slot" {
}) |decoration| {
var mask: [stride * (height + 1)]u8 = @splat(0xa5);
for (0..height) |y| @memset(mask[y * stride ..][0..width], 0);
- decorateGlyph(&mask, stride, width, height, 5, decoration);
+ decorateLine(&mask, stride, width, height, 5, decoration);
try std.testing.expect(inkBounds(&mask, stride, width, height) != null);
for (0..height) |y| for (mask[y * stride + width ..][0 .. stride - width]) |guard|
try std.testing.expectEqual(@as(u8, 0xa5), guard);