Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
163 changes: 104 additions & 59 deletions src/Renderer.zig
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ pending_resize: ?ViewportSize = null,
/// Reusable instance of RowContent to reduce allocations
row: RowContent = .{},

/// Cached information about font metrics, used for glyph scaling
font_info: ?FontInfo = null,

/// Bold text coloring configuration.
Expand Down Expand Up @@ -426,21 +427,21 @@ fn applyProps(env: emacs.Env, start: i64, end: i64, props: CellProps, default_co

if (face_props.len > 0) {
const face = env.funcall(s.list, face_props.items());
env.putTextProperty(start_val, end_val, s.face, face);
env.putTextProperty(start_val, end_val, "face", face);
}

if (props.hyperlink) {
env.putTextProperty(start_val, end_val, s.@"help-echo", s.@"ghostel--native-link-help-echo");
env.putTextProperty(start_val, end_val, s.@"mouse-face", s.highlight);
env.putTextProperty(start_val, end_val, s.keymap, env.symbolValue("ghostel-link-map"));
env.putTextProperty(start_val, end_val, "help-echo", s.@"ghostel--native-link-help-echo");
env.putTextProperty(start_val, end_val, "mouse-face", s.highlight);
env.putTextProperty(start_val, end_val, "keymap", env.symbolValue("ghostel-link-map"));
}

if (props.prompt) {
env.putTextProperty(start_val, end_val, emacs.sym.@"ghostel-prompt", env.t());
env.putTextProperty(start_val, end_val, "ghostel-prompt", env.t());
}

if (props.input) {
env.putTextProperty(start_val, end_val, emacs.sym.@"ghostel-input", env.t());
env.putTextProperty(start_val, end_val, "ghostel-input", env.t());
}
}

Expand Down Expand Up @@ -490,10 +491,11 @@ pub const RowContent = struct {
};

const CellInfo = struct {
byte_start: usize,
byte_len: usize,
char_start: usize,
char_len: usize,
col: i64,
byte_start: i64,
byte_end: i64,
char_start: i64,
char_end: i64,
wide: bool,
};

Expand Down Expand Up @@ -542,8 +544,9 @@ pub const RowContent = struct {
const row_hints = try readRowHints(raw_row);

var current_prop_key: ?CellPropKey = null;
var col: i64 = 0;
try gt.rs_row.read(row, gt.RS_ROW_DATA_CELLS, row_cells);
while (gt.rs_row_cells_next(row_cells.*)) {
while (gt.rs_row_cells_next(row_cells.*)) : (col += 1) {
const raw_cell = try gt.rs_row_cells.get(
gt.c.GhosttyCell,
row_cells.*,
Expand Down Expand Up @@ -609,10 +612,11 @@ pub const RowContent = struct {
// to fit into the monospace grid.
if (self.graphemes.items.len > 1 or self.graphemes.items[0] >= adjustment_threshold) {
try self.adjust_cells.append(RowContent.allocator, .{
.byte_start = byte_start,
.byte_len = self.text.items.len - byte_start,
.char_start = char_start,
.char_len = self.char_len - char_start,
.col = col,
.byte_start = @intCast(byte_start),
.byte_end = @intCast(self.text.items.len),
.char_start = @intCast(char_start),
.char_end = @intCast(self.char_len),
.wide = wide == gt.c.GHOSTTY_CELL_WIDE_WIDE,
});
}
Expand Down Expand Up @@ -687,55 +691,95 @@ fn readRowHints(row: gt.c.GhosttyRow) !RowHints {
};
}

fn adjustGlyphs(self: *Self, env: emacs.Env, row_start: i64) void {
fn adjustGlyphs(self: *Self, env: emacs.Env, row_start: i64, row_end: i64) void {
if (self.row.adjust_cells.items.len == 0) return;
if (self.font_info == null) return;
const window = env.f("selected-window", .{});
if (env.isNil(window)) return;

for (self.row.adjust_cells.items) |*cell| {
self.adjustGlyph(env, window, row_start, row_end, cell);
}
}

fn adjustGlyph(
self: *Self,
env: emacs.Env,
window: emacs.Value,
row_start: i64,
row_end: i64,
cell: *const RowContent.CellInfo,
) void {
const default_font_info = self.font_info.?;

const s = emacs.sym;

const window = env.f("selected-window", .{});
if (env.isNil(window)) return;

for (self.row.adjust_cells.items) |cell| {
const start_val = env.makeInteger(row_start + @as(i64, @intCast(cell.char_start)));
const end_val = env.makeInteger(row_start + @as(i64, @intCast(cell.char_start + cell.char_len)));
const font = env.f("font-at", .{ start_val, window });
// TODO: Maybe we should replace the cell with something else if there
// is no font. Today, it will just show the missing char glyph,
// which will push the line size bigger. This is rare, though.
// Most chars are covered by SOME font on the system.
if (env.isNil(font)) continue;

const font_info = env.f("query-font", .{font});
const ascent = env.extractInteger(env.vecGet(font_info, 4));
const descent = env.extractInteger(env.vecGet(font_info, 5));
const height = ascent + descent;

const glyphs = env.f("font-get-glyphs", .{ font, start_val, end_val });
if (env.vecSize(glyphs) == 0) continue;

// Each element is a vector containing information of a glyph in this format:
// [FROM-IDX TO-IDX C CODE WIDTH LBEARING RBEARING ASCENT DESCENT ADJUSTMENT]
const glyph = env.vecGet(glyphs, 0);
const width = env.extractInteger(env.vecGet(glyph, 4));
const num_cells: i64 = if (cell.wide) 2 else 1;

const max_width = default_font_info.width * num_cells;

// Skip adjustments if size already matches perfectly
if (max_width == width and default_font_info.height == height) continue;

// We add a fudge factor of +1 to the denominator to ensure fit
const scale_width = @as(f64, @floatFromInt(max_width)) / @as(f64, @floatFromInt(width + 1));
const scale_height = @as(f64, @floatFromInt(default_font_info.height)) / @as(f64, @floatFromInt(height + 1));
const scale = @min(scale_width, scale_height);

const min_width_spec = env.list(.{ s.@"min-width", env.list(.{num_cells}) });
const scale_spec = env.list(.{ s.height, scale });
const display_spec = env.list(.{ min_width_spec, scale_spec });
_ = env.f("put-text-property", .{ start_val, end_val, s.display, display_spec });
const start_val = env.makeInteger(row_start + @as(i64, @intCast(cell.char_start)));
const end_val = env.makeInteger(row_start + @as(i64, @intCast(cell.char_end)));
const font = env.f("font-at", .{ start_val, window });
// TODO: Maybe we should replace the cell with something else if there
// is no font. Today, it will just show the missing char glyph,
// which will push the line size bigger. This is rare, though.
// Most chars are covered by SOME font on the system.
if (env.isNil(font)) return;

const font_info = env.f("query-font", .{font});
const ascent = env.extractInteger(env.vecGet(font_info, 4));
const descent = env.extractInteger(env.vecGet(font_info, 5));
const height = ascent + descent;

const glyphs = env.f("font-get-glyphs", .{ font, start_val, end_val });
if (env.vecSize(glyphs) == 0) return;

// Each element is a vector containing information of a glyph in this format:
// [FROM-IDX TO-IDX C CODE WIDTH LBEARING RBEARING ASCENT DESCENT ADJUSTMENT]
const glyph = env.vecGet(glyphs, 0);
const width = env.extractInteger(env.vecGet(glyph, 4));
var char_width: i64 = if (cell.wide) 2 else 1;
var slot_width = default_font_info.width * char_width;

// Skip adjustments if size already matches perfectly
if (width == slot_width and height == default_font_info.height) return;

// Let's check if we can claim some space after the glyph to be able to render
// it larger than the cell size while still maintaining alignment.
const pre_char_width = char_width;
while (cell.col + char_width < self.size.cols) : ({
char_width += 1;
slot_width = default_font_info.width * char_width;
}) {
const cell_aspect = @as(f64, @floatFromInt(slot_width)) / @as(f64, @floatFromInt(default_font_info.height));
const glyph_aspect = @as(f64, @floatFromInt(width)) / @as(f64, @floatFromInt(height));
// If the aspect of the glyph is narrower than that of the cell, we're done
if (glyph_aspect < cell_aspect) break;

const claim_pos = row_start + cell.char_end + (char_width - pre_char_width);
// Lines are right-trimmed of trailing spaces, so positions at and past
// the newline represent empty space we can freely claim.
if (claim_pos >= row_end - 1) continue;

const c = env.extractInteger(env.f("char-after", .{claim_pos}));
Comment thread
emil-e marked this conversation as resolved.
if (c == ' ') {
env.putTextProperty(
claim_pos,
claim_pos + 1,
"display",
env.cons(s.space, env.list(.{ s.@":width", 0 })),
);
} else {
break;
}
}
Comment thread
emil-e marked this conversation as resolved.

// We add a fudge factor of +1 to the denominator to ensure fit
const scale_width = @as(f64, @floatFromInt(slot_width)) / @as(f64, @floatFromInt(width + 1));
const scale_height = @as(f64, @floatFromInt(default_font_info.height)) / @as(f64, @floatFromInt(height + 1));
const scale = @min(scale_width, scale_height);

const min_width_spec = env.list(.{ s.@"min-width", env.list(.{char_width}) });
const scale_spec = env.list(.{ s.height, scale });
const display_spec = env.list(.{ min_width_spec, scale_spec });
_ = env.f("put-text-property", .{ start_val, end_val, s.display, display_spec });
}

/// Insert row text and apply property runs.
Expand All @@ -754,6 +798,7 @@ fn insertRow(

const row_start = env.extractInteger(env.point());
env.insert(self.row.text.items);
const row_end = env.extractInteger(env.point());

for (self.row.runs.items) |*run| {
if (run.end_char <= run.start_char) continue;
Expand All @@ -765,13 +810,13 @@ fn insertRow(
}
}

self.adjustGlyphs(env, row_start);
self.adjustGlyphs(env, row_start, row_end);

if (try self.isRowWrapped()) {
// Mark newlines from soft-wrapped rows so copy mode can filter them
const point = env.point();
const nl_pos = env.makeInteger(env.extractInteger(point) - 1);
env.putTextProperty(nl_pos, point, emacs.sym.@"ghostel-wrap", env.t());
env.putTextProperty(nl_pos, point, "ghostel-wrap", env.t());
}
}

Expand Down
13 changes: 11 additions & 2 deletions src/emacs.zig
Original file line number Diff line number Diff line change
Expand Up @@ -304,8 +304,14 @@ pub const Env = struct {
_ = self.f("delete-region", .{ start, end });
}

pub fn putTextProperty(self: Env, start: Value, end: Value, prop: Value, value: Value) void {
_ = self.f("put-text-property", .{ start, end, prop, value });
pub fn putTextProperty(
self: Env,
start: anytype,
end: anytype,
comptime prop: []const u8,
value: anytype,
) void {
_ = self.f("put-text-property", .{ start, end, @field(sym, prop), value });
Comment thread
emil-e marked this conversation as resolved.
}

/// Create a unibyte string (for binary data like PNG images).
Expand Down Expand Up @@ -390,8 +396,10 @@ const interned_symbols = [_][:0]const u8{
":style",
":underline",
":weight",
":width",
"bold",
"bright",
"char-after",
"char-before",
"cons",
"dash",
Expand Down Expand Up @@ -466,6 +474,7 @@ const interned_symbols = [_][:0]const u8{
"selected-window",
"set",
"set-marker",
"space",
"symbol-value",
"t",
"wave",
Expand Down
Loading
Loading