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 glfw = @import("glfw");
|
||||||
const objc = @import("objc");
|
const objc = @import("objc");
|
||||||
const macos = @import("macos");
|
const macos = @import("macos");
|
||||||
const oni = @import("oniguruma");
|
|
||||||
const imgui = @import("imgui");
|
const imgui = @import("imgui");
|
||||||
const glslang = @import("glslang");
|
const glslang = @import("glslang");
|
||||||
const apprt = @import("../apprt.zig");
|
const apprt = @import("../apprt.zig");
|
||||||
@ -19,6 +18,7 @@ const terminal = @import("../terminal/main.zig");
|
|||||||
const renderer = @import("../renderer.zig");
|
const renderer = @import("../renderer.zig");
|
||||||
const math = @import("../math.zig");
|
const math = @import("../math.zig");
|
||||||
const Surface = @import("../Surface.zig");
|
const Surface = @import("../Surface.zig");
|
||||||
|
const link = @import("link.zig");
|
||||||
const shadertoy = @import("shadertoy.zig");
|
const shadertoy = @import("shadertoy.zig");
|
||||||
const assert = std.debug.assert;
|
const assert = std.debug.assert;
|
||||||
const Allocator = std.mem.Allocator;
|
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. This is only set if we have custom shaders.
|
||||||
custom_shader_state: ?CustomShaderState = null,
|
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 {
|
pub const CustomShaderState = struct {
|
||||||
/// The screen texture that we render the terminal to. If we don't have
|
/// The screen texture that we render the terminal to. If we don't have
|
||||||
/// custom shaders, we render directly to the drawable.
|
/// custom shaders, we render directly to the drawable.
|
||||||
@ -159,6 +154,7 @@ pub const DerivedConfig = struct {
|
|||||||
invert_selection_fg_bg: bool,
|
invert_selection_fg_bg: bool,
|
||||||
custom_shaders: std.ArrayListUnmanaged([]const u8),
|
custom_shaders: std.ArrayListUnmanaged([]const u8),
|
||||||
custom_shader_animation: bool,
|
custom_shader_animation: bool,
|
||||||
|
links: link.Set,
|
||||||
|
|
||||||
pub fn init(
|
pub fn init(
|
||||||
alloc_gpa: Allocator,
|
alloc_gpa: Allocator,
|
||||||
@ -180,6 +176,12 @@ pub const DerivedConfig = struct {
|
|||||||
font_styles.set(.italic, config.@"font-style-italic" != .false);
|
font_styles.set(.italic, config.@"font-style-italic" != .false);
|
||||||
font_styles.set(.bold_italic, config.@"font-style-bold-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 .{
|
return .{
|
||||||
.background_opacity = @max(0, @min(1, config.@"background-opacity")),
|
.background_opacity = @max(0, @min(1, config.@"background-opacity")),
|
||||||
.font_thicken = config.@"font-thicken",
|
.font_thicken = config.@"font-thicken",
|
||||||
@ -214,12 +216,15 @@ pub const DerivedConfig = struct {
|
|||||||
|
|
||||||
.custom_shaders = custom_shaders,
|
.custom_shaders = custom_shaders,
|
||||||
.custom_shader_animation = config.@"custom-shader-animation",
|
.custom_shader_animation = config.@"custom-shader-animation",
|
||||||
|
.links = links,
|
||||||
|
|
||||||
.arena = arena,
|
.arena = arena,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn deinit(self: *DerivedConfig) void {
|
pub fn deinit(self: *DerivedConfig) void {
|
||||||
|
const alloc = self.arena.allocator();
|
||||||
|
self.links.deinit(alloc);
|
||||||
self.arena.deinit();
|
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_greyscale = try initAtlasTexture(device, &options.font_group.atlas_greyscale);
|
||||||
const texture_color = try initAtlasTexture(device, &options.font_group.atlas_color);
|
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{
|
return Metal{
|
||||||
.alloc = alloc,
|
.alloc = alloc,
|
||||||
.config = options.config,
|
.config = options.config,
|
||||||
@ -398,8 +393,6 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal {
|
|||||||
.texture_greyscale = texture_greyscale,
|
.texture_greyscale = texture_greyscale,
|
||||||
.texture_color = texture_color,
|
.texture_color = texture_color,
|
||||||
.custom_shader_state = custom_shader_state,
|
.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.shaders.deinit(self.alloc);
|
||||||
|
|
||||||
self.url_regex.deinit();
|
|
||||||
|
|
||||||
self.* = undefined;
|
self.* = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1396,31 +1387,12 @@ fn rebuildCells(
|
|||||||
defer arena.deinit();
|
defer arena.deinit();
|
||||||
const arena_alloc = arena.allocator();
|
const arena_alloc = arena.allocator();
|
||||||
|
|
||||||
// All of our matching URLs. The selections in this list MUST be in
|
// Create our match set for the links.
|
||||||
// order of left-to-right top-to-bottom (English reading order). This
|
var link_match_set = try self.config.links.matchSet(
|
||||||
// requirement is necessary for the URL highlighting to work properly
|
arena_alloc,
|
||||||
// and has nothing to do with the locale.
|
screen,
|
||||||
var urls = std.ArrayList(terminal.Selection).init(arena_alloc);
|
.{}, // TODO: mouse hover point
|
||||||
|
);
|
||||||
// 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());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Determine our x/y range for preedit. We don't want to render anything
|
// Determine our x/y range for preedit. We don't want to render anything
|
||||||
// here because we will render the preedit separately.
|
// here because we will render the preedit separately.
|
||||||
@ -1439,10 +1411,6 @@ fn rebuildCells(
|
|||||||
// remains visible.
|
// remains visible.
|
||||||
var cursor_cell: ?mtl_shaders.Cell = null;
|
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
|
// Build each cell
|
||||||
var rowIter = screen.rowIterator(.viewport);
|
var rowIter = screen.rowIterator(.viewport);
|
||||||
var y: usize = 0;
|
var y: usize = 0;
|
||||||
@ -1534,30 +1502,14 @@ fn rebuildCells(
|
|||||||
// underline it.
|
// underline it.
|
||||||
const cell: terminal.Screen.Cell = cell: {
|
const cell: terminal.Screen.Cell = cell: {
|
||||||
var cell = row.getCell(shaper_cell.x);
|
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
|
// If our links contain this cell then we want to
|
||||||
// move to the next hint.
|
// underline it.
|
||||||
var compare_sel: terminal.Selection = sel;
|
if (link_match_set.orderedContains(.{
|
||||||
if (sel.end.before(pt)) {
|
.x = shaper_cell.x,
|
||||||
hint_i += 1;
|
.y = y,
|
||||||
if (hint_i >= urls.items.len) {
|
})) {
|
||||||
hint = null;
|
cell.attrs.underline = .single;
|
||||||
break :hint;
|
|
||||||
}
|
|
||||||
|
|
||||||
compare_sel = urls.items[hint_i];
|
|
||||||
hint = compare_sel;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (compare_sel.contains(pt)) {
|
|
||||||
cell.attrs.underline = .single;
|
|
||||||
break :hint;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
break :cell cell;
|
break :cell cell;
|
||||||
|
@ -25,6 +25,12 @@ inspector: ?*Inspector = null,
|
|||||||
/// a future exercise.
|
/// a future exercise.
|
||||||
preedit: ?Preedit = null,
|
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.
|
/// The pre-edit state. See Surface.preeditCallback for more information.
|
||||||
pub const Preedit = struct {
|
pub const Preedit = struct {
|
||||||
/// The codepoints to render as preedit text. We allow up to 16 codepoints
|
/// 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