Fix hooks.
This commit is contained in:
+37
-81
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user