renderer: link set for more efficient matching

This commit is contained in:
Mitchell Hashimoto
2023-11-29 11:14:56 -08:00
parent 172a91e95d
commit 5a7596e1b1
3 changed files with 173 additions and 72 deletions

View File

@ -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;

View File

@ -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
View 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);
}
};