mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-15 00:06:09 +03:00
renderer: link set for more efficient matching
This commit is contained in:
@ -9,7 +9,6 @@ const builtin = @import("builtin");
|
||||
const glfw = @import("glfw");
|
||||
const objc = @import("objc");
|
||||
const macos = @import("macos");
|
||||
const oni = @import("oniguruma");
|
||||
const imgui = @import("imgui");
|
||||
const glslang = @import("glslang");
|
||||
const apprt = @import("../apprt.zig");
|
||||
@ -19,6 +18,7 @@ const terminal = @import("../terminal/main.zig");
|
||||
const renderer = @import("../renderer.zig");
|
||||
const math = @import("../math.zig");
|
||||
const Surface = @import("../Surface.zig");
|
||||
const link = @import("link.zig");
|
||||
const shadertoy = @import("shadertoy.zig");
|
||||
const assert = std.debug.assert;
|
||||
const Allocator = std.mem.Allocator;
|
||||
@ -120,11 +120,6 @@ texture_color: objc.Object, // MTLTexture
|
||||
/// Custom shader state. This is only set if we have custom shaders.
|
||||
custom_shader_state: ?CustomShaderState = null,
|
||||
|
||||
/// Our URL regex.
|
||||
/// One day, this will be part of DerivedConfig since all regex matchers
|
||||
/// will be part of the config.
|
||||
url_regex: oni.Regex,
|
||||
|
||||
pub const CustomShaderState = struct {
|
||||
/// The screen texture that we render the terminal to. If we don't have
|
||||
/// custom shaders, we render directly to the drawable.
|
||||
@ -159,6 +154,7 @@ pub const DerivedConfig = struct {
|
||||
invert_selection_fg_bg: bool,
|
||||
custom_shaders: std.ArrayListUnmanaged([]const u8),
|
||||
custom_shader_animation: bool,
|
||||
links: link.Set,
|
||||
|
||||
pub fn init(
|
||||
alloc_gpa: Allocator,
|
||||
@ -180,6 +176,12 @@ pub const DerivedConfig = struct {
|
||||
font_styles.set(.italic, config.@"font-style-italic" != .false);
|
||||
font_styles.set(.bold_italic, config.@"font-style-bold-italic" != .false);
|
||||
|
||||
// Our link configs
|
||||
const links = try link.Set.fromConfig(
|
||||
alloc,
|
||||
config.link.links.items,
|
||||
);
|
||||
|
||||
return .{
|
||||
.background_opacity = @max(0, @min(1, config.@"background-opacity")),
|
||||
.font_thicken = config.@"font-thicken",
|
||||
@ -214,12 +216,15 @@ pub const DerivedConfig = struct {
|
||||
|
||||
.custom_shaders = custom_shaders,
|
||||
.custom_shader_animation = config.@"custom-shader-animation",
|
||||
.links = links,
|
||||
|
||||
.arena = arena,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *DerivedConfig) void {
|
||||
const alloc = self.arena.allocator();
|
||||
self.links.deinit(alloc);
|
||||
self.arena.deinit();
|
||||
}
|
||||
};
|
||||
@ -348,16 +353,6 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal {
|
||||
const texture_greyscale = try initAtlasTexture(device, &options.font_group.atlas_greyscale);
|
||||
const texture_color = try initAtlasTexture(device, &options.font_group.atlas_color);
|
||||
|
||||
// Create our URL regex
|
||||
var url_re = try oni.Regex.init(
|
||||
configpkg.url.regex,
|
||||
.{},
|
||||
oni.Encoding.utf8,
|
||||
oni.Syntax.default,
|
||||
null,
|
||||
);
|
||||
errdefer url_re.deinit();
|
||||
|
||||
return Metal{
|
||||
.alloc = alloc,
|
||||
.config = options.config,
|
||||
@ -398,8 +393,6 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal {
|
||||
.texture_greyscale = texture_greyscale,
|
||||
.texture_color = texture_color,
|
||||
.custom_shader_state = custom_shader_state,
|
||||
|
||||
.url_regex = url_re,
|
||||
};
|
||||
}
|
||||
|
||||
@ -429,8 +422,6 @@ pub fn deinit(self: *Metal) void {
|
||||
|
||||
self.shaders.deinit(self.alloc);
|
||||
|
||||
self.url_regex.deinit();
|
||||
|
||||
self.* = undefined;
|
||||
}
|
||||
|
||||
@ -1396,31 +1387,12 @@ fn rebuildCells(
|
||||
defer arena.deinit();
|
||||
const arena_alloc = arena.allocator();
|
||||
|
||||
// All of our matching URLs. The selections in this list MUST be in
|
||||
// order of left-to-right top-to-bottom (English reading order). This
|
||||
// requirement is necessary for the URL highlighting to work properly
|
||||
// and has nothing to do with the locale.
|
||||
var urls = std.ArrayList(terminal.Selection).init(arena_alloc);
|
||||
|
||||
// Find all the cells that have URLs.
|
||||
var lineIter = screen.lineIterator(.viewport);
|
||||
while (lineIter.next()) |line| {
|
||||
const strmap = line.stringMap(arena_alloc) catch continue;
|
||||
defer strmap.deinit(arena_alloc);
|
||||
|
||||
var it = strmap.searchIterator(self.url_regex);
|
||||
while (true) {
|
||||
const match_ = it.next() catch |err| {
|
||||
log.warn("failed to search for URLs err={}", .{err});
|
||||
break;
|
||||
};
|
||||
var match = match_ orelse break;
|
||||
defer match.deinit();
|
||||
|
||||
// Store our selection
|
||||
try urls.append(match.selection());
|
||||
}
|
||||
}
|
||||
// Create our match set for the links.
|
||||
var link_match_set = try self.config.links.matchSet(
|
||||
arena_alloc,
|
||||
screen,
|
||||
.{}, // TODO: mouse hover point
|
||||
);
|
||||
|
||||
// Determine our x/y range for preedit. We don't want to render anything
|
||||
// here because we will render the preedit separately.
|
||||
@ -1439,10 +1411,6 @@ fn rebuildCells(
|
||||
// remains visible.
|
||||
var cursor_cell: ?mtl_shaders.Cell = null;
|
||||
|
||||
// Keep track of our current hint that is being tracked.
|
||||
var hint: ?terminal.Selection = if (urls.items.len > 0) urls.items[0] else null;
|
||||
var hint_i: usize = 0;
|
||||
|
||||
// Build each cell
|
||||
var rowIter = screen.rowIterator(.viewport);
|
||||
var y: usize = 0;
|
||||
@ -1534,30 +1502,14 @@ fn rebuildCells(
|
||||
// underline it.
|
||||
const cell: terminal.Screen.Cell = cell: {
|
||||
var cell = row.getCell(shaper_cell.x);
|
||||
if (hint) |sel| hint: {
|
||||
const pt: terminal.point.ScreenPoint = .{
|
||||
.x = shaper_cell.x,
|
||||
.y = y,
|
||||
};
|
||||
|
||||
// If the end is before the point then we try to
|
||||
// move to the next hint.
|
||||
var compare_sel: terminal.Selection = sel;
|
||||
if (sel.end.before(pt)) {
|
||||
hint_i += 1;
|
||||
if (hint_i >= urls.items.len) {
|
||||
hint = null;
|
||||
break :hint;
|
||||
}
|
||||
|
||||
compare_sel = urls.items[hint_i];
|
||||
hint = compare_sel;
|
||||
}
|
||||
|
||||
if (compare_sel.contains(pt)) {
|
||||
cell.attrs.underline = .single;
|
||||
break :hint;
|
||||
}
|
||||
// If our links contain this cell then we want to
|
||||
// underline it.
|
||||
if (link_match_set.orderedContains(.{
|
||||
.x = shaper_cell.x,
|
||||
.y = y,
|
||||
})) {
|
||||
cell.attrs.underline = .single;
|
||||
}
|
||||
|
||||
break :cell cell;
|
||||
|
@ -25,6 +25,12 @@ inspector: ?*Inspector = null,
|
||||
/// a future exercise.
|
||||
preedit: ?Preedit = null,
|
||||
|
||||
/// Mouse state. This only contains state relevant to what renderers
|
||||
/// need about the mouse.
|
||||
mouse: Mouse = .{},
|
||||
|
||||
pub const Mouse = struct {};
|
||||
|
||||
/// The pre-edit state. See Surface.preeditCallback for more information.
|
||||
pub const Preedit = struct {
|
||||
/// The codepoints to render as preedit text. We allow up to 16 codepoints
|
||||
|
143
src/renderer/link.zig
Normal file
143
src/renderer/link.zig
Normal file
@ -0,0 +1,143 @@
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
const oni = @import("oniguruma");
|
||||
const configpkg = @import("../config.zig");
|
||||
const inputpkg = @import("../input.zig");
|
||||
const terminal = @import("../terminal/main.zig");
|
||||
const point = terminal.point;
|
||||
const Screen = terminal.Screen;
|
||||
|
||||
const log = std.log.scoped(.renderer_link);
|
||||
|
||||
/// The link configuration needed for renderers.
|
||||
pub const Link = struct {
|
||||
/// The regular expression to match the link against.
|
||||
regex: oni.Regex,
|
||||
|
||||
/// The situations in which the link should be highlighted.
|
||||
highlight: inputpkg.Link.Highlight,
|
||||
|
||||
pub fn deinit(self: *Link) void {
|
||||
self.regex.deinit();
|
||||
}
|
||||
};
|
||||
|
||||
/// A set of links. This provides a higher level API for renderers
|
||||
/// to match against a viewport and determine if cells are part of
|
||||
/// a link.
|
||||
pub const Set = struct {
|
||||
links: []Link,
|
||||
|
||||
/// Returns the slice of links from the configuration.
|
||||
pub fn fromConfig(
|
||||
alloc: Allocator,
|
||||
config: []const inputpkg.Link,
|
||||
) !Set {
|
||||
var links = std.ArrayList(Link).init(alloc);
|
||||
defer links.deinit();
|
||||
|
||||
for (config) |link| {
|
||||
var regex = try link.oniRegex();
|
||||
errdefer regex.deinit();
|
||||
try links.append(.{
|
||||
.regex = regex,
|
||||
.highlight = link.highlight,
|
||||
});
|
||||
}
|
||||
|
||||
return .{ .links = try links.toOwnedSlice() };
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Set, alloc: Allocator) void {
|
||||
for (self.links) |*link| link.deinit();
|
||||
alloc.free(self.links);
|
||||
}
|
||||
|
||||
/// Returns the matchset for the viewport state. The matchset is the
|
||||
/// full set of matching links for the visible viewport. A link
|
||||
/// only matches if it is also in the correct state (i.e. hovered
|
||||
/// if necessary).
|
||||
///
|
||||
/// This is not a particularly efficient operation. This should be
|
||||
/// called sparingly.
|
||||
pub fn matchSet(
|
||||
self: *const Set,
|
||||
alloc: Allocator,
|
||||
screen: *Screen,
|
||||
mouse_pt: point.Viewport,
|
||||
) !MatchSet {
|
||||
_ = mouse_pt;
|
||||
|
||||
// This contains our list of matches. The matches are stored
|
||||
// as selections which contain the start and end points of
|
||||
// the match. There is no way to map these back to the link
|
||||
// configuration right now because we don't need to.
|
||||
var matches = std.ArrayList(terminal.Selection).init(alloc);
|
||||
defer matches.deinit();
|
||||
|
||||
// Iterate over all the visible lines.
|
||||
var lineIter = screen.lineIterator(.viewport);
|
||||
while (lineIter.next()) |line| {
|
||||
const strmap = line.stringMap(alloc) catch |err| {
|
||||
log.warn(
|
||||
"failed to build string map for link checking err={}",
|
||||
.{err},
|
||||
);
|
||||
continue;
|
||||
};
|
||||
defer strmap.deinit(alloc);
|
||||
|
||||
// Go through each link and see if we have any matches.
|
||||
for (self.links) |link| {
|
||||
var it = strmap.searchIterator(link.regex);
|
||||
while (true) {
|
||||
const match_ = it.next() catch |err| {
|
||||
log.warn("failed to search for link err={}", .{err});
|
||||
break;
|
||||
};
|
||||
var match = match_ orelse break;
|
||||
defer match.deinit();
|
||||
try matches.append(match.selection());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return .{ .matches = try matches.toOwnedSlice() };
|
||||
}
|
||||
};
|
||||
|
||||
/// MatchSet is the result of matching links against a screen. This contains
|
||||
/// all the matching links and operations on them such as whether a specific
|
||||
/// cell is part of a matched link.
|
||||
pub const MatchSet = struct {
|
||||
/// The matches.
|
||||
///
|
||||
/// Important: this must be in left-to-right top-to-bottom order.
|
||||
matches: []const terminal.Selection,
|
||||
i: usize = 0,
|
||||
|
||||
pub fn deinit(self: *MatchSet, alloc: Allocator) void {
|
||||
alloc.free(self.matches);
|
||||
}
|
||||
|
||||
/// Checks if the matchset contains the given pt. The points must be
|
||||
/// given in left-to-right top-to-bottom order. This is a stateful
|
||||
/// operation and giving a point out of order can cause invalid
|
||||
/// results.
|
||||
pub fn orderedContains(
|
||||
self: *MatchSet,
|
||||
pt: point.ScreenPoint,
|
||||
) bool {
|
||||
// If we're beyond the end of our possible matches, we're done.
|
||||
if (self.i >= self.matches.len) return false;
|
||||
|
||||
// If our selection ends before the point, then no point will ever
|
||||
// again match this selection so we move on to the next one.
|
||||
while (self.matches[self.i].end.before(pt)) {
|
||||
self.i += 1;
|
||||
if (self.i >= self.matches.len) return false;
|
||||
}
|
||||
|
||||
return self.matches[self.i].contains(pt);
|
||||
}
|
||||
};
|
Reference in New Issue
Block a user