more flexible tui async plan
This commit is contained in:
parent
8860efb371
commit
d9fd5d5b97
3 changed files with 68 additions and 48 deletions
41
src/tui.zig
41
src/tui.zig
|
|
@ -468,18 +468,11 @@ pub const App = struct {
|
|||
/// observation engine async dispatch, etc.) has a wall-clock-
|
||||
/// independent tick to react to.
|
||||
frame: u64 = 0,
|
||||
/// True when the active tab has async work in flight that needs
|
||||
/// poll-driven redraws (e.g. review's observation panel with
|
||||
/// pending async checks). Set by the tab (loadData / activate);
|
||||
/// cleared by the tab when the work completes or the tab
|
||||
/// deactivates. While set, the event handler keeps a one-shot
|
||||
/// vxfw Tick timer armed so the draw loop wakes up to poll —
|
||||
/// without it, vxfw sleeps until the next user event and async
|
||||
/// results would sit invisible until a keypress.
|
||||
wants_poll_tick: bool = false,
|
||||
/// True while a vxfw Tick command is scheduled and not yet
|
||||
/// delivered. Prevents stacking duplicate timers (each event
|
||||
/// would otherwise arm another).
|
||||
/// would otherwise arm another). Whether polling is WANTED is
|
||||
/// derived fresh from the active tab's optional
|
||||
/// `wantsPollTick` hook — see `typeErasedEventHandler`.
|
||||
poll_tick_armed: bool = false,
|
||||
|
||||
has_explicit_symbol: bool = false, // true if -s was used
|
||||
|
|
@ -532,16 +525,18 @@ pub const App = struct {
|
|||
|
||||
fn typeErasedEventHandler(ptr: *anyopaque, ctx: *vaxis.vxfw.EventContext, event: vaxis.vxfw.Event) anyerror!void {
|
||||
const self: *App = @ptrCast(@alignCast(ptr));
|
||||
// Poll-driven redraw support: while a tab has async work
|
||||
// in flight (`wants_poll_tick`), keep a one-shot vxfw
|
||||
// Tick timer armed. The timer fires a `.tick` event,
|
||||
// which requests a redraw; the draw pass runs the active
|
||||
// tab's `tick` hook, which polls the async work and
|
||||
// clears `wants_poll_tick` when done. Re-armed here on
|
||||
// every event (including the .tick itself) until the tab
|
||||
// clears the flag. 100ms cadence — fast enough to feel
|
||||
// Poll-driven redraw support: while the ACTIVE tab reports
|
||||
// async work in flight (via its optional `wantsPollTick`
|
||||
// hook), keep a one-shot vxfw Tick timer armed. The timer
|
||||
// fires a `.tick` event, which requests a redraw; the draw
|
||||
// pass runs the active tab's `tick` hook, which polls the
|
||||
// async work. The decision is derived fresh after every
|
||||
// event — no stored flag to choreograph across
|
||||
// activate/deactivate/completion, and tab switches are
|
||||
// picked up automatically because dispatch targets the
|
||||
// active tab. 100ms cadence — fast enough to feel
|
||||
// responsive, slow enough to be invisible in CPU terms.
|
||||
defer if (self.wants_poll_tick and !self.poll_tick_armed) {
|
||||
defer if (!self.poll_tick_armed and self.dispatchBool("wantsPollTick", .{})) {
|
||||
if (ctx.tick(100, self.widget())) {
|
||||
self.poll_tick_armed = true;
|
||||
} else |err| {
|
||||
|
|
@ -589,11 +584,11 @@ pub const App = struct {
|
|||
},
|
||||
.tick => {
|
||||
// Our scheduled poll timer fired. Mark it
|
||||
// un-armed (the defer above re-arms if the tab
|
||||
// still wants polling) and request a redraw so
|
||||
// the draw pass runs the tab's tick hook.
|
||||
// un-armed (the defer above re-arms if the active
|
||||
// tab still wants polling) and request a redraw
|
||||
// so the draw pass runs the tab's tick hook.
|
||||
self.poll_tick_armed = false;
|
||||
if (self.wants_poll_tick) {
|
||||
if (self.dispatchBool("wantsPollTick", .{})) {
|
||||
ctx.redraw = true;
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -260,10 +260,6 @@ pub const tab = struct {
|
|||
|
||||
pub fn activate(state: *State, app: *App) !void {
|
||||
if (tab.isDisabled(app)) return;
|
||||
// Re-arm the poll timer if we still have async checks in
|
||||
// flight from a previous activation (user switched away
|
||||
// and back while a check was running).
|
||||
if (state.observations_pending) app.wants_poll_tick = true;
|
||||
if (state.loaded) return;
|
||||
// Load the journal before building findings; `loadData`
|
||||
// joins both into `findings_view`.
|
||||
|
|
@ -271,15 +267,18 @@ pub const tab = struct {
|
|||
loadData(state, app);
|
||||
}
|
||||
|
||||
/// Pause poll-driven redraws while this tab is inactive. The
|
||||
/// tick hook only dispatches to the ACTIVE tab, so leaving
|
||||
/// `wants_poll_tick` set after switching away would spin the
|
||||
/// 100ms timer with nobody polling. `activate` re-arms when
|
||||
/// the user returns; the async check keeps running in the
|
||||
/// background either way.
|
||||
pub fn deactivate(state: *State, app: *App) void {
|
||||
_ = state;
|
||||
app.wants_poll_tick = false;
|
||||
pub const deactivate = framework.noopDeactivate(State);
|
||||
|
||||
/// Framework poll-tick hook: true while the observation panel
|
||||
/// has async checks in flight. Drives the App's poll timer so
|
||||
/// the `tick` hook below gets called without user input. The
|
||||
/// framework only asks the ACTIVE tab, so switching away
|
||||
/// pauses UI polling automatically (the checks keep running
|
||||
/// in the background; `tick` picks the results up when the
|
||||
/// user returns).
|
||||
pub fn wantsPollTick(state: *State, app: *App) bool {
|
||||
_ = app;
|
||||
return state.observations_pending;
|
||||
}
|
||||
|
||||
/// Manual refresh: drops the cached view and re-builds. Also
|
||||
|
|
@ -303,11 +302,11 @@ pub const tab = struct {
|
|||
/// Poll in-flight async observation checks. No-op (one bool
|
||||
/// test) unless `loadData` left the panel incomplete. When
|
||||
/// the last pending check resolves, rebuild the findings
|
||||
/// view so late findings appear, and clear both the local
|
||||
/// flag and the App-level poll-tick request so subsequent
|
||||
/// frames cost nothing.
|
||||
/// view so late findings appear, and clear the flag —
|
||||
/// `wantsPollTick` then answers false and the App's poll
|
||||
/// timer stops re-arming.
|
||||
///
|
||||
/// Redraw cadence: while `app.wants_poll_tick` is set, the
|
||||
/// Redraw cadence: while `wantsPollTick` answers true, the
|
||||
/// App keeps a 100ms vxfw Tick timer armed (see
|
||||
/// `typeErasedEventHandler`), so this hook gets called
|
||||
/// even when the user isn't generating events. Without
|
||||
|
|
@ -320,7 +319,6 @@ pub const tab = struct {
|
|||
const panel = if (view.observations) |*p| p else return;
|
||||
if (!panel.isComplete()) return;
|
||||
state.observations_pending = false;
|
||||
app.wants_poll_tick = false;
|
||||
rebuildFindingsView(state, app);
|
||||
}
|
||||
|
||||
|
|
@ -997,17 +995,17 @@ fn loadData(state: *State, app: *App) void {
|
|||
applySort(state);
|
||||
rebuildFindingsView(state, app);
|
||||
|
||||
// If any check is still running (async dispatch), arm the
|
||||
// tick-poll so the findings view refreshes when results
|
||||
// land. `isComplete` also transitions newly-finished checks,
|
||||
// so a fast async check that finished during buildReview is
|
||||
// If any check is still running (async dispatch), flag it so
|
||||
// the findings view refreshes when results land.
|
||||
// `isComplete` also transitions newly-finished checks, so a
|
||||
// fast async check that finished during buildReview is
|
||||
// caught right here without waiting for the next frame.
|
||||
// `wants_poll_tick` tells the App to keep a vxfw Tick timer
|
||||
// armed so the poll runs even with no user input.
|
||||
// While the flag is set, `wantsPollTick` answers true and
|
||||
// the App keeps a vxfw Tick timer armed so the poll runs
|
||||
// even with no user input.
|
||||
if (state.view) |*v| {
|
||||
if (v.observations) |*panel| {
|
||||
state.observations_pending = !panel.isComplete();
|
||||
if (state.observations_pending) app.wants_poll_tick = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -54,6 +54,23 @@
|
|||
//! pub fn handlePaste(state: *State, app: *App, text: []const u8) bool { ... }
|
||||
//! pub fn statusOverride(state: *State, app: *App) ?framework.StatusOverride { ... }
|
||||
//!
|
||||
//! /// Optional: does this tab currently have async work in
|
||||
//! /// flight that needs poll-driven redraws? While the ACTIVE
|
||||
//! /// tab answers true, the App keeps a one-shot vxfw Tick
|
||||
//! /// timer armed (~100ms cadence) so the draw loop wakes up
|
||||
//! /// and runs the tab's `tick` hook even with no user input —
|
||||
//! /// without it, async results would sit invisible until the
|
||||
//! /// next keypress.
|
||||
//! ///
|
||||
//! /// Answer from your own state (e.g. "my observation panel
|
||||
//! /// has pending checks"); do NOT cache the answer anywhere on
|
||||
//! /// App. The framework derives the polling decision fresh
|
||||
//! /// after every event, so tab switches and work completion
|
||||
//! /// are picked up automatically with zero choreography.
|
||||
//! /// Inactive tabs are never asked — switching away pauses
|
||||
//! /// UI polling while background work continues.
|
||||
//! pub fn wantsPollTick(state: *State, app: *App) bool { ... }
|
||||
//!
|
||||
//! // ── Context-change hooks (optional) ─────────────────────
|
||||
//! // Fire when a global context this tab depends on changes.
|
||||
//! // Tabs that don't care simply omit the method. Contrast with
|
||||
|
|
@ -471,6 +488,16 @@ pub fn validateTabModule(comptime Module: type) void {
|
|||
"pub fn statusOverride(state: *State, app: *App) ?StatusOverride { ... }",
|
||||
);
|
||||
}
|
||||
if (@hasDecl(tab_decl, "wantsPollTick")) {
|
||||
validator.expectFn(
|
||||
"Tab module",
|
||||
mod_name,
|
||||
tab_decl,
|
||||
"wantsPollTick",
|
||||
fn (*State, *App) bool,
|
||||
"pub fn wantsPollTick(state: *State, app: *App) bool { ... }",
|
||||
);
|
||||
}
|
||||
|
||||
// ── Draw hooks (mutually exclusive, exactly one required) ──
|
||||
//
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue