optimize title and menubar redraw
This commit is contained in:
@@ -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
|
||||||
|
|||||||
+32
-14
@@ -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| {
|
||||||
|
|||||||
Reference in New Issue
Block a user