optimize title and menubar redraw

This commit is contained in:
2026-08-29 15:27:31 +02:00
parent b20b3aeb5a
commit 1fff449547
2 changed files with 87 additions and 14 deletions
+55
View File
@@ -208,10 +208,22 @@ pub const TabBar = struct {
rects: std.ArrayList(Box) = .empty, rects: std.ArrayList(Box) = .empty,
windows: std.ArrayList(*Window) = .empty, windows: std.ArrayList(*Window) = .empty,
/// The labels painted into the strip that is on screen, in tab order and
/// NUL separated, so a redraw can be skipped when the titles it would
/// paint are the ones already there.
label_cache: std.ArrayList(u8) = .empty,
/// Scratch holding the labels a pending redraw would paint. Swapped into
/// `label_cache` once they have actually been painted.
label_next: std.ArrayList(u8) = .empty,
/// The window highlighted in the strip that is on screen.
drawn_focused: ?*Window = null,
pub fn deinit(self: *TabBar) void { pub fn deinit(self: *TabBar) void {
const gpa = self.wm.gpa; const gpa = self.wm.gpa;
self.rects.deinit(gpa); self.rects.deinit(gpa);
self.windows.deinit(gpa); self.windows.deinit(gpa);
self.label_cache.deinit(gpa);
self.label_next.deinit(gpa);
if (self.pool) |*p| p.deinit(); if (self.pool) |*p| p.deinit();
if (self.node) |n| n.destroy(); if (self.node) |n| n.destroy();
if (self.shell) |s| s.destroy(); if (self.shell) |s| s.destroy();
@@ -251,6 +263,24 @@ pub const TabBar = struct {
} }
const gpa = self.wm.gpa; const gpa = self.wm.gpa;
// Every retitle that lands on a tab drives a render sequence, and
// terminals retitle themselves constantly. Redrawing to produce a
// byte-identical strip would rasterise every glyph again and cost the
// compositor a full-width damage and a fresh texture upload, so
// compare against what is on screen first and leave the surface be
// when nothing visible moved.
self.label_next.clearRetainingCapacity();
const labels_recorded = self.recordLabels(windows);
if (labels_recorded and self.mapped and
self.drawn_focused == focused and
std.meta.eql(self.box, box) and
std.mem.eql(*Window, self.windows.items, windows) and
std.mem.eql(u8, self.label_cache.items, self.label_next.items))
{
return;
}
const pool = &self.pool.?; const pool = &self.pool.?;
const buffer = pool.acquire(box.width, box.height) catch |err| { const buffer = pool.acquire(box.width, box.height) catch |err| {
@@ -311,6 +341,29 @@ pub const TabBar = struct {
self.node.?.setPosition(box.x, box.y); self.node.?.setPosition(box.x, box.y);
self.box = box; self.box = box;
self.mapped = true; self.mapped = true;
if (labels_recorded) {
std.mem.swap(std.ArrayList(u8), &self.label_cache, &self.label_next);
} else {
// Nothing trustworthy to compare against, so make sure the next
// call redraws rather than skipping on a stale cache.
self.label_cache.clearRetainingCapacity();
}
self.drawn_focused = focused;
}
/// Fill `label_next` with the labels a redraw would paint. Returns false
/// if they could not be recorded, in which case the caller redraws and
/// invalidates the cache rather than trusting it.
fn recordLabels(self: *TabBar, windows: []const *Window) bool {
const gpa = self.wm.gpa;
for (windows) |win| {
self.label_next.appendSlice(gpa, label(win)) catch return false;
// A separator, so that neighbouring labels cannot run together and
// compare equal to a different split of the same text.
self.label_next.append(gpa, 0) catch return false;
}
return true;
} }
/// Unmap the bar. Must be called during a render sequence if it was /// Unmap the bar. Must be called during a render sequence if it was
@@ -324,6 +377,8 @@ pub const TabBar = struct {
self.mapped = false; self.mapped = false;
self.rects.clearRetainingCapacity(); self.rects.clearRetainingCapacity();
self.windows.clearRetainingCapacity(); self.windows.clearRetainingCapacity();
self.label_cache.clearRetainingCapacity();
self.drawn_focused = null;
} }
/// What to write on a window's tab: its title, or its app id while it has /// What to write on a window's tab: its title, or its app id while it has
+26 -8
View File
@@ -183,13 +183,29 @@ fn onTabBar(self: *Window) bool {
return out.state().layout == .tabbed; return out.state().layout == .tabbed;
} }
fn setString(self: *Window, field: *?[]u8, value: ?[*:0]const u8) void { /// Replace an owned string field, returning whether the value actually
/// changed. Clients re-send strings that have not moved — a terminal animating
/// a spinner in its title re-sets the same text several times a second — and
/// the caller can use the answer to skip the IPC broadcast and the tab bar
/// redraw those no-op updates would otherwise trigger.
fn setString(self: *Window, field: *?[]u8, value: ?[*:0]const u8) bool {
const gpa = self.wm.gpa; const gpa = self.wm.gpa;
const new: ?[]const u8 = if (value) |v| std.mem.span(v) else null;
const same = if (field.*) |old|
if (new) |n| std.mem.eql(u8, old, n) else false
else
new == null;
if (same) return false;
if (field.*) |old| gpa.free(old); if (field.*) |old| gpa.free(old);
field.* = null; field.* = null;
if (value) |v| { if (new) |n| {
field.* = gpa.dupe(u8, std.mem.span(v)) catch null; // A failed dupe leaves the field null, which compares unequal to the
// next event carrying the same text, so the copy is retried then.
field.* = gpa.dupe(u8, n) catch null;
} }
return true;
} }
fn onEvent(_: *river.WindowV1, event: river.WindowV1.Event, self: *Window) void { fn onEvent(_: *river.WindowV1, event: river.WindowV1.Event, self: *Window) void {
@@ -227,23 +243,25 @@ fn onEvent(_: *river.WindowV1, event: river.WindowV1.Event, self: *Window) void
}, },
.app_id => |ev| { .app_id => |ev| {
self.setString(&self.app_id, ev.app_id); if (self.setString(&self.app_id, ev.app_id)) {
self.applyRules(); self.applyRules();
self.wm.ipcDirty(); self.wm.ipcDirty();
}
}, },
.title => |ev| { .title => |ev| {
self.setString(&self.title, ev.title); if (self.setString(&self.title, ev.title)) {
self.applyRules(); self.applyRules();
self.wm.ipcDirty(); self.wm.ipcDirty();
// The tab bar has this title painted into it, so a window that is // The tab bar has this title painted into it, so a window that
// wearing a tab needs the strip redrawn. Terminals retitle // is wearing a tab needs the strip redrawn. Terminals retitle
// themselves constantly, so ask only when it will show. // themselves constantly, so ask only when it will show.
if (self.onTabBar()) self.wm.needsManage(); if (self.onTabBar()) self.wm.needsManage();
}
}, },
.identifier => |ev| { .identifier => |ev| {
self.setString(&self.identifier, ev.identifier); _ = self.setString(&self.identifier, ev.identifier);
}, },
.parent => |ev| { .parent => |ev| {