mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-14 15:56:13 +03:00
Line segmentation into text runs
This commit is contained in:
@ -31,6 +31,11 @@ pub const Buffer = struct {
|
|||||||
c.hb_buffer_reset(self.handle);
|
c.hb_buffer_reset(self.handle);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the number of items in the buffer.
|
||||||
|
pub fn getLength(self: Buffer) u32 {
|
||||||
|
return c.hb_buffer_get_length(self.handle);
|
||||||
|
}
|
||||||
|
|
||||||
/// Sets the type of buffer contents. Buffers are either empty, contain
|
/// Sets the type of buffer contents. Buffers are either empty, contain
|
||||||
/// characters (before shaping), or contain glyphs (the result of shaping).
|
/// characters (before shaping), or contain glyphs (the result of shaping).
|
||||||
pub fn setContentType(self: Buffer, ct: ContentType) void {
|
pub fn setContentType(self: Buffer, ct: ContentType) void {
|
||||||
|
@ -63,11 +63,16 @@ pub fn addFace(self: *Group, alloc: Allocator, style: Style, face: Face) !void {
|
|||||||
/// This represents a specific font in the group.
|
/// This represents a specific font in the group.
|
||||||
pub const FontIndex = packed struct {
|
pub const FontIndex = packed struct {
|
||||||
/// The number of bits we use for the index.
|
/// The number of bits we use for the index.
|
||||||
const idx_bits = 8 - StyleArray.len;
|
const idx_bits = 8 - @typeInfo(@typeInfo(Style).Enum.tag_type).Int.bits;
|
||||||
pub const IndexInt = @Type(.{ .Int = .{ .signedness = .unsigned, .bits = idx_bits } });
|
pub const IndexInt = @Type(.{ .Int = .{ .signedness = .unsigned, .bits = idx_bits } });
|
||||||
|
|
||||||
style: Style,
|
style: Style = .regular,
|
||||||
idx: IndexInt,
|
idx: IndexInt = 0,
|
||||||
|
|
||||||
|
/// Convert to int
|
||||||
|
pub fn int(self: FontIndex) u8 {
|
||||||
|
return @bitCast(u8, self);
|
||||||
|
}
|
||||||
|
|
||||||
test {
|
test {
|
||||||
// We never want to take up more than a byte since font indexes are
|
// We never want to take up more than a byte since font indexes are
|
||||||
|
178
src/font/Shaper.zig
Normal file
178
src/font/Shaper.zig
Normal file
@ -0,0 +1,178 @@
|
|||||||
|
//! This struct handles text shaping.
|
||||||
|
const Shaper = @This();
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
const Allocator = std.mem.Allocator;
|
||||||
|
const harfbuzz = @import("harfbuzz");
|
||||||
|
const Atlas = @import("../Atlas.zig");
|
||||||
|
const Face = @import("main.zig").Face;
|
||||||
|
const Group = @import("main.zig").Group;
|
||||||
|
const GroupCache = @import("main.zig").GroupCache;
|
||||||
|
const Library = @import("main.zig").Library;
|
||||||
|
const Style = @import("main.zig").Style;
|
||||||
|
const terminal = @import("../terminal/main.zig");
|
||||||
|
|
||||||
|
/// The font group to use under the covers
|
||||||
|
group: *GroupCache,
|
||||||
|
|
||||||
|
/// The buffer used for text shaping. We reuse it across multiple shaping
|
||||||
|
/// calls to prevent allocations.
|
||||||
|
hb_buf: harfbuzz.Buffer,
|
||||||
|
|
||||||
|
pub fn init(group: *GroupCache) !Shaper {
|
||||||
|
return Shaper{
|
||||||
|
.group = group,
|
||||||
|
.hb_buf = try harfbuzz.Buffer.create(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: *Shaper) void {
|
||||||
|
self.hb_buf.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns an iterator that returns one text run at a time for the
|
||||||
|
/// given terminal row. Note that text runs are are only valid one at a time
|
||||||
|
/// for a Shaper struct since they share state.
|
||||||
|
pub fn runIterator(self: *Shaper, row: terminal.Screen.Row) RunIterator {
|
||||||
|
return .{ .shaper = self, .row = row };
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A single text run. A text run is only valid for one Shaper and
|
||||||
|
/// until the next run is created.
|
||||||
|
pub const TextRun = struct {
|
||||||
|
font_index: Group.FontIndex,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const RunIterator = struct {
|
||||||
|
shaper: *Shaper,
|
||||||
|
row: terminal.Screen.Row,
|
||||||
|
i: usize = 0,
|
||||||
|
|
||||||
|
pub fn next(self: *RunIterator, alloc: Allocator) !?TextRun {
|
||||||
|
if (self.i >= self.row.len) return null;
|
||||||
|
|
||||||
|
// Track the font for our curent run
|
||||||
|
var current_font: Group.FontIndex = .{};
|
||||||
|
|
||||||
|
// Reset the buffer for our current run
|
||||||
|
self.shaper.hb_buf.reset();
|
||||||
|
self.shaper.hb_buf.setContentType(.unicode);
|
||||||
|
|
||||||
|
// Go through cell by cell and accumulate while we build our run.
|
||||||
|
var j: usize = self.i;
|
||||||
|
while (j < self.row.len) : (j += 1) {
|
||||||
|
const cell = self.row[j];
|
||||||
|
|
||||||
|
// Ignore tailing wide spacers, this will get fixed up by the shaper
|
||||||
|
if (cell.empty() or cell.attrs.wide_spacer_tail) continue;
|
||||||
|
|
||||||
|
const style: Style = if (cell.attrs.bold)
|
||||||
|
.bold
|
||||||
|
else
|
||||||
|
.regular;
|
||||||
|
|
||||||
|
// Determine the font for this cell
|
||||||
|
const font_idx_opt = try self.shaper.group.indexForCodepoint(alloc, style, cell.char);
|
||||||
|
const font_idx = font_idx_opt.?;
|
||||||
|
if (j == self.i) current_font = font_idx;
|
||||||
|
|
||||||
|
// If our fonts are not equal, then we're done with our run.
|
||||||
|
if (font_idx.int() != current_font.int()) break;
|
||||||
|
|
||||||
|
// Continue with our run
|
||||||
|
self.shaper.hb_buf.add(cell.char, @intCast(u32, j - self.i));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finalize our buffer
|
||||||
|
self.shaper.hb_buf.guessSegmentProperties();
|
||||||
|
|
||||||
|
// Move our cursor
|
||||||
|
self.i = j;
|
||||||
|
|
||||||
|
return TextRun{ .font_index = current_font };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
test "run iterator" {
|
||||||
|
const testing = std.testing;
|
||||||
|
const alloc = testing.allocator;
|
||||||
|
|
||||||
|
var testdata = try testShaper(alloc);
|
||||||
|
defer testdata.deinit();
|
||||||
|
|
||||||
|
{
|
||||||
|
// Make a screen with some data
|
||||||
|
var screen = try terminal.Screen.init(alloc, 3, 5, 0);
|
||||||
|
defer screen.deinit(alloc);
|
||||||
|
screen.testWriteString("ABCD");
|
||||||
|
|
||||||
|
// Get our run iterator
|
||||||
|
var shaper = testdata.shaper;
|
||||||
|
var it = shaper.runIterator(screen.getRow(.{ .screen = 0 }));
|
||||||
|
var count: usize = 0;
|
||||||
|
while (try it.next(alloc)) |_| count += 1;
|
||||||
|
try testing.expectEqual(@as(usize, 1), count);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
// Make a screen with some data
|
||||||
|
var screen = try terminal.Screen.init(alloc, 3, 5, 0);
|
||||||
|
defer screen.deinit(alloc);
|
||||||
|
screen.testWriteString("A😃D");
|
||||||
|
|
||||||
|
// Get our run iterator
|
||||||
|
var shaper = testdata.shaper;
|
||||||
|
var it = shaper.runIterator(screen.getRow(.{ .screen = 0 }));
|
||||||
|
var count: usize = 0;
|
||||||
|
while (try it.next(alloc)) |_| {
|
||||||
|
count += 1;
|
||||||
|
|
||||||
|
// All runs should be exactly length 1
|
||||||
|
std.log.warn("YES", .{});
|
||||||
|
try testing.expectEqual(@as(u32, 1), shaper.hb_buf.getLength());
|
||||||
|
}
|
||||||
|
try testing.expectEqual(@as(usize, 3), count);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const TestShaper = struct {
|
||||||
|
alloc: Allocator,
|
||||||
|
shaper: Shaper,
|
||||||
|
cache: *GroupCache,
|
||||||
|
lib: Library,
|
||||||
|
|
||||||
|
pub fn deinit(self: *TestShaper) void {
|
||||||
|
self.shaper.deinit();
|
||||||
|
self.cache.deinit(self.alloc);
|
||||||
|
self.alloc.destroy(self.cache);
|
||||||
|
self.lib.deinit();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Helper to return a fully initialized shaper.
|
||||||
|
fn testShaper(alloc: Allocator) !TestShaper {
|
||||||
|
const testFont = @import("test.zig").fontRegular;
|
||||||
|
const testEmoji = @import("test.zig").fontEmoji;
|
||||||
|
|
||||||
|
var lib = try Library.init();
|
||||||
|
errdefer lib.deinit();
|
||||||
|
|
||||||
|
var cache_ptr = try alloc.create(GroupCache);
|
||||||
|
errdefer alloc.destroy(cache_ptr);
|
||||||
|
cache_ptr.* = try GroupCache.init(alloc, try Group.init(alloc));
|
||||||
|
errdefer cache_ptr.*.deinit(alloc);
|
||||||
|
|
||||||
|
// Setup group
|
||||||
|
try cache_ptr.group.addFace(alloc, .regular, try Face.init(lib, testFont, .{ .points = 12 }));
|
||||||
|
try cache_ptr.group.addFace(alloc, .regular, try Face.init(lib, testEmoji, .{ .points = 12 }));
|
||||||
|
|
||||||
|
var shaper = try init(cache_ptr);
|
||||||
|
errdefer shaper.deinit();
|
||||||
|
|
||||||
|
return TestShaper{
|
||||||
|
.alloc = alloc,
|
||||||
|
.shaper = shaper,
|
||||||
|
.cache = cache_ptr,
|
||||||
|
.lib = lib,
|
||||||
|
};
|
||||||
|
}
|
@ -5,6 +5,7 @@ pub const Group = @import("Group.zig");
|
|||||||
pub const GroupCache = @import("GroupCache.zig");
|
pub const GroupCache = @import("GroupCache.zig");
|
||||||
pub const Glyph = @import("Glyph.zig");
|
pub const Glyph = @import("Glyph.zig");
|
||||||
pub const Library = @import("Library.zig");
|
pub const Library = @import("Library.zig");
|
||||||
|
pub const Shaper = @import("Shaper.zig");
|
||||||
|
|
||||||
/// The styles that a family can take.
|
/// The styles that a family can take.
|
||||||
pub const Style = enum(u2) {
|
pub const Style = enum(u2) {
|
||||||
|
@ -1093,7 +1093,7 @@ pub fn testString(self: Screen, alloc: Allocator, tag: RowIndexTag) ![]const u8
|
|||||||
/// Writes a basic string into the screen for testing. Newlines (\n) separate
|
/// Writes a basic string into the screen for testing. Newlines (\n) separate
|
||||||
/// each row. If a line is longer than the available columns, soft-wrapping
|
/// each row. If a line is longer than the available columns, soft-wrapping
|
||||||
/// will occur.
|
/// will occur.
|
||||||
fn testWriteString(self: *Screen, text: []const u8) void {
|
pub fn testWriteString(self: *Screen, text: []const u8) void {
|
||||||
var y: usize = 0;
|
var y: usize = 0;
|
||||||
var x: usize = 0;
|
var x: usize = 0;
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user