diff options
Diffstat (limited to 'src/gui')
| -rw-r--r-- | src/gui/font.c | 30 | ||||
| -rw-r--r-- | src/gui/font.h | 8 | ||||
| -rw-r--r-- | src/gui/gui.zig | 46 |
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); |
