Fix hooks.

This commit is contained in:
Greyson Parrelli
2026-08-12 18:50:16 -04:00
parent dca5ec0b7d
commit 4e0e400372
7 changed files with 471 additions and 126 deletions
+37 -81
View File
@@ -58,43 +58,9 @@ layout_popover: *gtk.Popover,
/// they belong to are on screen.
layout_rows: std.ArrayListUnmanaged(*LayoutRow) = .empty,
/// What a tab's sidebar row is signalling. The four pane-level states, plus
/// one the panes can't know about on their own.
///
/// `done` is the whole reason this is a separate enum rather than just
/// `View.Status`. A pane that has gone back to idle is indistinguishable from
/// one that never ran, and "it finished" is exactly the thing worth knowing
/// when you're deciding which tab to go back to. So a tab that finishes work
/// while you are looking somewhere else latches into `done` and stays there
/// until you actually visit it.
const Attention = enum {
none,
busy,
done,
needs_input,
failed,
/// The dot's CSS class, or null when the row should show no dot at all.
fn class(self: Attention) ?[:0]const u8 {
return switch (self) {
.none => null,
.busy => "playpen-status-busy",
.done => "playpen-status-done",
.needs_input => "playpen-status-input",
.failed => "playpen-status-failed",
};
}
fn tooltip(self: Attention) [:0]const u8 {
return switch (self) {
.none => "",
.busy => "Working",
.done => "Finished while you were away",
.needs_input => "Waiting for you",
.failed => "Stopped on an error",
};
}
};
/// What a sidebar row is signalling. Defined with the dots themselves, since
/// a row and a pane header show the same five states for the same reasons.
const Attention = Pane.Attention;
/// A single tab: a view of one or more panes, plus the sidebar row that
/// selects it.
@@ -119,13 +85,14 @@ const Tab = struct {
rename_popover: *gtk.Popover,
rename_entry: *gtk.Entry,
/// What the panes were reporting last time we looked, so a busy → idle
/// transition can be told apart from an idle tab that never ran.
last_status: View.Status = .idle,
/// Set when work finished while this tab was not the visible one, and
/// cleared when it is next selected.
done_unseen: bool = false,
/// Set when a pane in this tab finished since the last time you opened it.
///
/// The row and the panes answer slightly different questions, which is why
/// this exists alongside the panes' own latches. The row's question is
/// "should I go there?", and opening the tab settles it whether or not you
/// then deal with every pane inside. A pane's question is "have you dealt
/// with me?", which only you can answer.
finished_since_visit: bool = false,
name: [16]u8,
name_len: usize,
@@ -136,15 +103,12 @@ const Tab = struct {
/// What the row should be showing right now.
///
/// The latch outranks `busy` deliberately: if one pane is still working
/// but another has already finished unseen, the finished one is the news.
/// Both halves have to hold. The flag alone would keep a row lit after you
/// answered the last pane in it without leaving the tab; the panes alone
/// would keep it lit after you had already come and looked.
fn attention(self: *const Tab) Attention {
return switch (self.view.status()) {
.needs_input => .needs_input,
.failed => .failed,
.busy => if (self.done_unseen) .done else .busy,
.idle => if (self.done_unseen) .done else .none,
};
const news = self.finished_since_visit and self.view.anyDoneUnanswered();
return .of(self.view.status(), news);
}
};
@@ -298,6 +262,7 @@ fn newTabEmpty(self: *Window) !*Tab {
.on_empty = &onViewEmpty,
.on_title = &onViewTitle,
.on_status = &onViewStatus,
.on_finished = &onViewFinished,
.ctx = tab,
});
errdefer view.destroy();
@@ -334,7 +299,7 @@ fn newTabEmpty(self: *Window) !*Tab {
// After the label rather than before it, so the dots down the sidebar
// line up in a column instead of being pushed around by title length.
tab.dot.as(gtk.Widget).addCssClass("playpen-status-dot");
Pane.setDot(tab.dot, null);
Pane.applyAttention(tab.row.as(gtk.Widget), tab.dot, .none);
row_box.append(tab.dot.as(gtk.Widget));
const close = gtk.Button.newFromIconName("window-close-symbolic");
@@ -710,12 +675,12 @@ fn select(self: *Window, tab: *Tab) void {
self.list.selectRow(tab.row);
tab.view.focus();
// Visiting the tab is what "seeing it" means, so this is where the
// finished-while-you-were-away flag is spent.
if (tab.done_unseen) {
tab.done_unseen = false;
self.refreshStatus(tab);
}
// Opening the tab is the acknowledgement the row was asking for, and the
// pane you land in counts as answered along with it. Any other pane in a
// split keeps its own dot until you go to it.
tab.finished_since_visit = false;
tab.view.answerFocused();
self.refreshStatus(tab);
}
fn indexOf(self: *Window, tab: *Tab) ?usize {
@@ -812,10 +777,7 @@ fn refreshLabel(self: *Window, tab: *Tab) void {
/// pane changing state doesn't change any of the text.
fn refreshStatus(self: *Window, tab: *Tab) void {
_ = self;
const attention = tab.attention();
Pane.setDot(tab.dot, attention.class());
tab.dot.as(gtk.Widget).setTooltipText(attention.tooltip());
Pane.applyAttention(tab.row.as(gtk.Widget), tab.dot, tab.attention());
}
fn onViewTitle(ctx: ?*anyopaque) void {
@@ -823,27 +785,21 @@ fn onViewTitle(ctx: ?*anyopaque) void {
tab.window.refreshLabel(tab);
}
/// A pane in this tab changed state.
///
/// The latch is set here rather than anywhere else because this is the only
/// place that sees the transition: by the time the user looks at the sidebar,
/// a tab that finished and a tab that never started look identical.
/// A pane in this tab changed state, or answered one it was carrying.
fn onViewStatus(ctx: ?*anyopaque) void {
const tab: *Tab = @ptrCast(@alignCast(ctx.?));
const self = tab.window;
tab.window.refreshStatus(tab);
}
const previous = tab.last_status;
const current = tab.view.status();
tab.last_status = current;
// Work that finishes in the tab you are already looking at needs no
// flag — you watched it happen.
const visible = self.activeTab() == tab;
if (!visible and current == .idle and previous != .idle) {
tab.done_unseen = true;
}
self.refreshStatus(tab);
/// A pane in this tab finished work.
///
/// Recorded even when the tab is the one on screen: you may well have watched
/// it stop and then gone somewhere else, and the next time you open this tab is
/// when that stops being news.
fn onViewFinished(ctx: ?*anyopaque) void {
const tab: *Tab = @ptrCast(@alignCast(ctx.?));
tab.finished_since_visit = true;
tab.window.refreshStatus(tab);
}
/// The view lost its last pane, so the tab goes with it.