548 lines
22 KiB
Zig
548 lines
22 KiB
Zig
//! TUI tab framework — common contract types for tab modules.
|
|
//!
|
|
//! Every TUI tab module exports a `pub const tab = struct { ... };`
|
|
//! that conforms to the contract documented here. A small comptime
|
|
//! walker in `src/tui.zig` discovers tabs via the `tab_modules`
|
|
//! anonymous struct literal and uses these types to wire dispatch,
|
|
//! the help overlay, and the status-line hint.
|
|
//!
|
|
//! ## Contract (per-tab)
|
|
//!
|
|
//! ```zig
|
|
//! pub const Action = enum { /* tab-local keybind actions */ };
|
|
//! pub const State = struct { /* tab-private state */ };
|
|
//!
|
|
//! pub const tab = struct {
|
|
//! pub const ActionT = Action;
|
|
//! pub const StateT = State;
|
|
//!
|
|
//! /// Default keybindings for this tab.
|
|
//! pub const default_bindings: []const TabBinding(Action) = &.{ ... };
|
|
//!
|
|
//! /// One label per Action variant for the help overlay.
|
|
//! pub const action_labels =
|
|
//! std.enums.EnumArray(Action, []const u8).init(.{ ... });
|
|
//!
|
|
//! /// Subset of actions that show in the status-line hint.
|
|
//! pub const status_hints: []const Action = &.{ ... };
|
|
//!
|
|
//! // ── Lifecycle hooks (required) ──────────────────────────
|
|
//! pub fn init(state: *State, app: *App) !void { ... }
|
|
//! pub fn deinit(state: *State, app: *App) void { ... }
|
|
//! pub fn activate(state: *State, app: *App) !void { ... }
|
|
//! pub fn deactivate(state: *State, app: *App) void { ... }
|
|
//! pub fn reload(state: *State, app: *App) !void { ... }
|
|
//! pub fn tick(state: *State, app: *App, frame: u64) void { ... }
|
|
//!
|
|
//! // ── Action dispatch (required) ──────────────────────────
|
|
//! pub fn handleAction(state: *State, app: *App, action: Action) void { ... }
|
|
//!
|
|
//! // ── Event hooks (each optional) ─────────────────────────
|
|
//! // Tabs that don't care about an event class simply omit the
|
|
//! // method. The framework's dispatcher checks `@hasDecl` and
|
|
//! // skips the call entirely. Each method returns `bool` —
|
|
//! // true means "consumed; don't fall through to global
|
|
//! // handling."
|
|
//! //
|
|
//! // Chrome ownership: the framework owns the tab bar (row 0)
|
|
//! // and filters chrome-region events out before dispatching.
|
|
//! // Tab handlers receive only events the framework didn't
|
|
//! // claim — so `handleMouse` will never see a row-0 click,
|
|
//! // and there's no need for tab handlers to test `row == 0`
|
|
//! // themselves. (Future: a per-tab "claim chrome region" hook
|
|
//! // would let a tab opt in to drawing into chrome and
|
|
//! // receiving its events; for now, chrome is framework-only.)
|
|
//! pub fn handleKey(state: *State, app: *App, key: vaxis.Key) bool { ... }
|
|
//! pub fn handleMouse(state: *State, app: *App, mouse: vaxis.Mouse) bool { ... }
|
|
//! pub fn handlePaste(state: *State, app: *App, text: []const u8) 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
|
|
//! // `reload` (drops data AND triggers a fetch); these hooks
|
|
//! // drop data but DON'T trigger a fetch — the fetch happens
|
|
//! // lazily on next `activate`.
|
|
//! pub fn onSymbolChange(state: *State, app: *App) void { ... }
|
|
//!
|
|
//! /// Fired when the user invokes a global scroll-to-extreme
|
|
//! /// action (`g`/`G`). Tabs with a cursor reset it to match
|
|
//! /// the new scroll position. Tabs without a cursor omit
|
|
//! /// this hook.
|
|
//! pub fn onScroll(state: *State, app: *App, where: ScrollEdge) void { ... }
|
|
//!
|
|
//! /// Fired when the user invokes a relative cursor-move
|
|
//! /// (`j`/`k`, ↑/↓, mouse wheel). `delta` is signed: positive
|
|
//! /// = down, negative = up. Magnitude is 1 for keys, larger
|
|
//! /// for wheel events. Tabs with a row cursor step it,
|
|
//! /// clamp to row count, and ensure visibility; return
|
|
//! /// `true` to consume. Tabs without a cursor (or with empty
|
|
//! /// rows) return `false` so the framework falls through to
|
|
//! /// scroll-by-`delta` instead.
|
|
//! pub fn onCursorMove(state: *State, app: *App, delta: isize) bool { ... }
|
|
//!
|
|
//! // ── Misc (required) ─────────────────────────────────────
|
|
//! pub fn isDisabled(app: *App) bool { ... }
|
|
//! };
|
|
//! ```
|
|
//!
|
|
//! Tabs without keybind actions ship with `Action = enum {}` and
|
|
//! empty `default_bindings` / `action_labels` / `status_hints`.
|
|
//! No implicit defaults — the contract is fully explicit for
|
|
//! action-related fields and lifecycle hooks. The event hooks
|
|
//! (`handleKey`, `handleMouse`, `handlePaste`) and context-change
|
|
//! hooks (`onSymbolChange`) are the exception: their absence means
|
|
//! "this tab doesn't process that event class."
|
|
//!
|
|
//! Lifecycle hooks that aren't meaningful for a given tab can use
|
|
//! the `noop*` factory helpers below to inherit no-op
|
|
//! implementations:
|
|
//!
|
|
//! ```zig
|
|
//! pub const tab = struct {
|
|
//! pub const deactivate = framework.noopDeactivate(State);
|
|
//! pub const tick = framework.noopTick(State);
|
|
//! // ... other hooks defined explicitly ...
|
|
//! };
|
|
//! ```
|
|
|
|
const std = @import("std");
|
|
const vaxis = @import("vaxis");
|
|
|
|
/// Re-exported KeyCombo so tab modules don't need to import
|
|
/// keybinds.zig directly for binding declarations.
|
|
pub const KeyCombo = struct {
|
|
codepoint: u21,
|
|
mods: vaxis.Key.Modifiers = .{},
|
|
};
|
|
|
|
/// A single (action, key) pair for a tab's `default_bindings`.
|
|
/// Generic over the tab's Action enum so `default_bindings` can
|
|
/// be type-checked at comptime against the tab's own enum.
|
|
pub fn TabBinding(comptime ActionT: type) type {
|
|
return struct {
|
|
action: ActionT,
|
|
key: KeyCombo,
|
|
};
|
|
}
|
|
|
|
/// Argument passed to `onScroll` indicating which extreme the
|
|
/// user scrolled to. `top` corresponds to the global `scroll_top`
|
|
/// action (default: `g`); `bottom` corresponds to `scroll_bottom`
|
|
/// (default: `G`).
|
|
pub const ScrollEdge = enum { top, bottom };
|
|
|
|
// ── Lifecycle hook factories (no-op defaults) ─────────────────
|
|
//
|
|
// Tabs that don't need a particular lifecycle hook can declare
|
|
// `pub const deactivate = framework.noopDeactivate(State);` etc.
|
|
// This keeps the contract explicit (every required field is named
|
|
// in the tab struct) while letting tabs avoid writing dummy bodies.
|
|
//
|
|
// Event hooks (handleKey, handleMouse, handlePaste) and context-
|
|
// change hooks (onSymbolChange) are NOT in this list — they're
|
|
// optional via `@hasDecl` checking, so a tab that doesn't care
|
|
// simply omits the method.
|
|
|
|
const App = @import("../tui.zig").App;
|
|
|
|
/// Returns a `deactivate(state, app) void` no-op for the given
|
|
/// State type.
|
|
pub fn noopDeactivate(comptime StateT: type) fn (*StateT, *App) void {
|
|
return struct {
|
|
fn f(_: *StateT, _: *App) void {}
|
|
}.f;
|
|
}
|
|
|
|
/// Returns a `tick(state, app, frame) void` no-op for the given
|
|
/// State type.
|
|
pub fn noopTick(comptime StateT: type) fn (*StateT, *App, u64) void {
|
|
return struct {
|
|
fn f(_: *StateT, _: *App, _: u64) void {}
|
|
}.f;
|
|
}
|
|
|
|
/// Returns an `isDisabled(app) bool` that always returns false.
|
|
/// The most common case — most tabs are always enabled.
|
|
pub fn alwaysEnabled() fn (*App) bool {
|
|
return struct {
|
|
fn f(_: *App) bool {
|
|
return false;
|
|
}
|
|
}.f;
|
|
}
|
|
|
|
// ── Comptime contract validation ──────────────────────────────
|
|
//
|
|
// `validateTabModule(comptime Module: type)` walks a tab module
|
|
// at comptime and asserts it conforms to the framework contract.
|
|
// Every error message includes the full expected signature so the
|
|
// developer knows exactly what to add, copy-paste ready.
|
|
//
|
|
// Call sites: the registry in `src/tui.zig` calls this once per
|
|
// entry. Each `<name>_tab.zig` can also opt in via a comptime
|
|
// block in the file itself for faster local feedback:
|
|
//
|
|
// ```zig
|
|
// comptime { framework.validateTabModule(@This()); }
|
|
// ```
|
|
|
|
/// Validate a tab module against the framework contract. Emits a
|
|
/// `@compileError` with the full expected signature for any
|
|
/// missing or wrong-shape decl.
|
|
///
|
|
/// The contract is documented at the top of this file. This
|
|
/// function is the source of truth — if you change the contract,
|
|
/// update both.
|
|
pub fn validateTabModule(comptime Module: type) void {
|
|
comptime {
|
|
const mod_name = @typeName(Module);
|
|
|
|
// ── Top-level decls ────────────────────────────────────
|
|
if (!@hasDecl(Module, "Action")) {
|
|
@compileError("Tab module `" ++ mod_name ++ "` is missing `pub const Action = enum { ... };` " ++
|
|
"(use `enum {}` if the tab has no keybind actions)");
|
|
}
|
|
if (!@hasDecl(Module, "State")) {
|
|
@compileError("Tab module `" ++ mod_name ++ "` is missing `pub const State = struct { ... };`");
|
|
}
|
|
if (!@hasDecl(Module, "tab")) {
|
|
@compileError("Tab module `" ++ mod_name ++ "` is missing `pub const tab = struct { ... };` " ++
|
|
"(see src/tui/tab_framework.zig for the full contract)");
|
|
}
|
|
|
|
const Action = Module.Action;
|
|
const State = Module.State;
|
|
const tab_decl = Module.tab;
|
|
|
|
// Sanity: Action must be an enum, State must be a struct.
|
|
if (@typeInfo(Action) != .@"enum") {
|
|
@compileError("Tab module `" ++ mod_name ++ "`: `Action` must be an enum type, got " ++ @typeName(Action));
|
|
}
|
|
if (@typeInfo(State) != .@"struct") {
|
|
@compileError("Tab module `" ++ mod_name ++ "`: `State` must be a struct type, got " ++ @typeName(State));
|
|
}
|
|
|
|
// ── Type aliases inside `tab` ──────────────────────────
|
|
if (!@hasDecl(tab_decl, "ActionT")) {
|
|
@compileError("Tab module `" ++ mod_name ++ "`: `tab` is missing `pub const ActionT = Action;`");
|
|
}
|
|
if (!@hasDecl(tab_decl, "StateT")) {
|
|
@compileError("Tab module `" ++ mod_name ++ "`: `tab` is missing `pub const StateT = State;`");
|
|
}
|
|
|
|
// ── Binding / label / hint constants ───────────────────
|
|
expectDeclWithType(
|
|
mod_name,
|
|
tab_decl,
|
|
"default_bindings",
|
|
[]const TabBinding(Action),
|
|
"pub const default_bindings: []const TabBinding(Action) = &.{ ... };",
|
|
);
|
|
expectDeclWithType(
|
|
mod_name,
|
|
tab_decl,
|
|
"action_labels",
|
|
std.enums.EnumArray(Action, []const u8),
|
|
"pub const action_labels = std.enums.EnumArray(Action, []const u8).init(.{ ... });",
|
|
);
|
|
expectDeclWithType(
|
|
mod_name,
|
|
tab_decl,
|
|
"status_hints",
|
|
[]const Action,
|
|
"pub const status_hints: []const Action = &.{ ... };",
|
|
);
|
|
|
|
// ── Lifecycle hooks (required) ─────────────────────────
|
|
expectFnInferredError(
|
|
mod_name,
|
|
tab_decl,
|
|
"init",
|
|
&.{ *State, *App },
|
|
void,
|
|
"pub fn init(state: *State, app: *App) !void { ... }",
|
|
);
|
|
expectFn(
|
|
mod_name,
|
|
tab_decl,
|
|
"deinit",
|
|
fn (*State, *App) void,
|
|
"pub fn deinit(state: *State, app: *App) void { ... }",
|
|
);
|
|
expectFnInferredError(
|
|
mod_name,
|
|
tab_decl,
|
|
"activate",
|
|
&.{ *State, *App },
|
|
void,
|
|
"pub fn activate(state: *State, app: *App) !void { ... }",
|
|
);
|
|
expectFn(
|
|
mod_name,
|
|
tab_decl,
|
|
"deactivate",
|
|
fn (*State, *App) void,
|
|
"pub fn deactivate(state: *State, app: *App) void { ... }",
|
|
);
|
|
expectFnInferredError(
|
|
mod_name,
|
|
tab_decl,
|
|
"reload",
|
|
&.{ *State, *App },
|
|
void,
|
|
"pub fn reload(state: *State, app: *App) !void { ... }",
|
|
);
|
|
expectFn(
|
|
mod_name,
|
|
tab_decl,
|
|
"tick",
|
|
fn (*State, *App, u64) void,
|
|
"pub fn tick(state: *State, app: *App, frame: u64) void { ... }",
|
|
);
|
|
|
|
// ── Action dispatch (required) ─────────────────────────
|
|
expectFn(
|
|
mod_name,
|
|
tab_decl,
|
|
"handleAction",
|
|
fn (*State, *App, Action) void,
|
|
"pub fn handleAction(state: *State, app: *App, action: Action) void { ... }",
|
|
);
|
|
|
|
// ── Misc (required) ────────────────────────────────────
|
|
expectFn(
|
|
mod_name,
|
|
tab_decl,
|
|
"isDisabled",
|
|
fn (*App) bool,
|
|
"pub fn isDisabled(app: *App) bool { ... }",
|
|
);
|
|
|
|
// ── Event hooks (optional, but typed when present) ─────
|
|
// `@hasDecl` returns true; if the signature is wrong we
|
|
// surface that as a hard error so a typo'd signature
|
|
// doesn't silently get skipped at dispatch time.
|
|
if (@hasDecl(tab_decl, "handleKey")) {
|
|
expectFn(
|
|
mod_name,
|
|
tab_decl,
|
|
"handleKey",
|
|
fn (*State, *App, vaxis.Key) bool,
|
|
"pub fn handleKey(state: *State, app: *App, key: vaxis.Key) bool { ... }",
|
|
);
|
|
}
|
|
if (@hasDecl(tab_decl, "handleMouse")) {
|
|
expectFn(
|
|
mod_name,
|
|
tab_decl,
|
|
"handleMouse",
|
|
fn (*State, *App, vaxis.Mouse) bool,
|
|
"pub fn handleMouse(state: *State, app: *App, mouse: vaxis.Mouse) bool { ... }",
|
|
);
|
|
}
|
|
if (@hasDecl(tab_decl, "handlePaste")) {
|
|
expectFn(
|
|
mod_name,
|
|
tab_decl,
|
|
"handlePaste",
|
|
fn (*State, *App, []const u8) bool,
|
|
"pub fn handlePaste(state: *State, app: *App, text: []const u8) bool { ... }",
|
|
);
|
|
}
|
|
|
|
// ── Context-change hooks (optional, typed when present) ──
|
|
if (@hasDecl(tab_decl, "onSymbolChange")) {
|
|
expectFn(
|
|
mod_name,
|
|
tab_decl,
|
|
"onSymbolChange",
|
|
fn (*State, *App) void,
|
|
"pub fn onSymbolChange(state: *State, app: *App) void { ... }",
|
|
);
|
|
}
|
|
if (@hasDecl(tab_decl, "onScroll")) {
|
|
expectFn(
|
|
mod_name,
|
|
tab_decl,
|
|
"onScroll",
|
|
fn (*State, *App, ScrollEdge) void,
|
|
"pub fn onScroll(state: *State, app: *App, where: ScrollEdge) void { ... }",
|
|
);
|
|
}
|
|
if (@hasDecl(tab_decl, "onCursorMove")) {
|
|
expectFn(
|
|
mod_name,
|
|
tab_decl,
|
|
"onCursorMove",
|
|
fn (*State, *App, isize) bool,
|
|
"pub fn onCursorMove(state: *State, app: *App, delta: isize) bool { ... }",
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Internal helper: assert a `tab` decl exists and has the
|
|
/// expected type. The `expected_signature` string is shown
|
|
/// verbatim in the error message so the developer can copy-paste
|
|
/// the fix.
|
|
fn expectDeclWithType(
|
|
comptime mod_name: []const u8,
|
|
comptime tab_decl: type,
|
|
comptime decl_name: []const u8,
|
|
comptime ExpectedT: type,
|
|
comptime expected_signature: []const u8,
|
|
) void {
|
|
comptime {
|
|
if (!@hasDecl(tab_decl, decl_name)) {
|
|
@compileError("Tab module `" ++ mod_name ++ "`: `tab` is missing `" ++ decl_name ++
|
|
"`. Expected:\n " ++ expected_signature);
|
|
}
|
|
const ActualT = @TypeOf(@field(tab_decl, decl_name));
|
|
if (ActualT != ExpectedT) {
|
|
@compileError("Tab module `" ++ mod_name ++ "`: `tab." ++ decl_name ++
|
|
"` has wrong type.\n Expected: " ++ @typeName(ExpectedT) ++
|
|
"\n Got: " ++ @typeName(ActualT) ++
|
|
"\n Expected signature:\n " ++ expected_signature);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Internal helper: assert a `tab` function decl exists and has
|
|
/// the expected fn-pointer type. The `expected_signature` string
|
|
/// is shown verbatim in the error message.
|
|
///
|
|
/// For functions returning `!void` (or any error union with an
|
|
/// inferred error set), pass `expectFnInferredError` instead —
|
|
/// this helper requires exact type equality, which means the
|
|
/// expected type must use `anyerror` and the actual function must
|
|
/// also use `anyerror`. In practice that's not what `pub fn foo()
|
|
/// !void` produces (Zig infers an empty error set), so for
|
|
/// fallible-but-error-set-inferred hooks we need the looser check.
|
|
fn expectFn(
|
|
comptime mod_name: []const u8,
|
|
comptime tab_decl: type,
|
|
comptime decl_name: []const u8,
|
|
comptime ExpectedT: type,
|
|
comptime expected_signature: []const u8,
|
|
) void {
|
|
comptime {
|
|
if (!@hasDecl(tab_decl, decl_name)) {
|
|
@compileError("Tab module `" ++ mod_name ++ "`: `tab` is missing `" ++ decl_name ++
|
|
"`. Expected:\n " ++ expected_signature);
|
|
}
|
|
const ActualT = @TypeOf(@field(tab_decl, decl_name));
|
|
if (ActualT != ExpectedT) {
|
|
@compileError("Tab module `" ++ mod_name ++ "`: `tab." ++ decl_name ++
|
|
"` has wrong signature.\n Expected: " ++ @typeName(ExpectedT) ++
|
|
"\n Got: " ++ @typeName(ActualT) ++
|
|
"\n Full expected declaration:\n " ++ expected_signature);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Internal helper: assert a `tab` fallible function decl exists
|
|
/// and matches the expected parameter types + returns an error
|
|
/// union ending in `Return`. The error set itself is not checked,
|
|
/// because `pub fn foo() !void` produces an inferred (often empty)
|
|
/// error set whose name varies per function. This loosens equality
|
|
/// to "params match, return is `<some_error_set>!Return`."
|
|
fn expectFnInferredError(
|
|
comptime mod_name: []const u8,
|
|
comptime tab_decl: type,
|
|
comptime decl_name: []const u8,
|
|
comptime expected_params: []const type,
|
|
comptime ExpectedReturn: type,
|
|
comptime expected_signature: []const u8,
|
|
) void {
|
|
comptime {
|
|
if (!@hasDecl(tab_decl, decl_name)) {
|
|
@compileError("Tab module `" ++ mod_name ++ "`: `tab` is missing `" ++ decl_name ++
|
|
"`. Expected:\n " ++ expected_signature);
|
|
}
|
|
const ActualT = @TypeOf(@field(tab_decl, decl_name));
|
|
const info = @typeInfo(ActualT);
|
|
if (info != .@"fn") {
|
|
@compileError("Tab module `" ++ mod_name ++ "`: `tab." ++ decl_name ++
|
|
"` must be a function, got " ++ @typeName(ActualT) ++
|
|
"\n Full expected declaration:\n " ++ expected_signature);
|
|
}
|
|
const fn_info = info.@"fn";
|
|
|
|
// Verify params count + types.
|
|
if (fn_info.params.len != expected_params.len) {
|
|
@compileError("Tab module `" ++ mod_name ++ "`: `tab." ++ decl_name ++
|
|
"` has wrong arity.\n Got: " ++ @typeName(ActualT) ++
|
|
"\n Full expected declaration:\n " ++ expected_signature);
|
|
}
|
|
for (expected_params, 0..) |Expected, i| {
|
|
const ActualParam = fn_info.params[i].type orelse {
|
|
@compileError("Tab module `" ++ mod_name ++ "`: `tab." ++ decl_name ++
|
|
"` parameter " ++ std.fmt.comptimePrint("{d}", .{i}) ++
|
|
" is `anytype`, but the contract requires `" ++ @typeName(Expected) ++ "`.\n" ++
|
|
" Full expected declaration:\n " ++ expected_signature);
|
|
};
|
|
if (ActualParam != Expected) {
|
|
@compileError("Tab module `" ++ mod_name ++ "`: `tab." ++ decl_name ++
|
|
"` parameter " ++ std.fmt.comptimePrint("{d}", .{i}) ++
|
|
" has wrong type.\n Expected: " ++ @typeName(Expected) ++
|
|
"\n Got: " ++ @typeName(ActualParam) ++
|
|
"\n Full expected declaration:\n " ++ expected_signature);
|
|
}
|
|
}
|
|
|
|
// Verify return is an error union ending in ExpectedReturn.
|
|
const ActualReturn = fn_info.return_type orelse {
|
|
@compileError("Tab module `" ++ mod_name ++ "`: `tab." ++ decl_name ++
|
|
"` has no return type.\n Full expected declaration:\n " ++ expected_signature);
|
|
};
|
|
const ret_info = @typeInfo(ActualReturn);
|
|
if (ret_info != .error_union) {
|
|
@compileError("Tab module `" ++ mod_name ++ "`: `tab." ++ decl_name ++
|
|
"` must return an error union (`!" ++ @typeName(ExpectedReturn) ++
|
|
"`), got " ++ @typeName(ActualReturn) ++
|
|
"\n Full expected declaration:\n " ++ expected_signature);
|
|
}
|
|
if (ret_info.error_union.payload != ExpectedReturn) {
|
|
@compileError("Tab module `" ++ mod_name ++ "`: `tab." ++ decl_name ++
|
|
"` returns wrong payload type.\n Expected: !" ++ @typeName(ExpectedReturn) ++
|
|
"\n Got: " ++ @typeName(ActualReturn) ++
|
|
"\n Full expected declaration:\n " ++ expected_signature);
|
|
}
|
|
}
|
|
}
|
|
|
|
// ── Tests ─────────────────────────────────────────────────────
|
|
|
|
const testing = std.testing;
|
|
|
|
test "TabBinding generic over Action" {
|
|
const A = enum { foo, bar };
|
|
const B = TabBinding(A);
|
|
const b: B = .{ .action = .foo, .key = .{ .codepoint = 'a' } };
|
|
try testing.expectEqual(A.foo, b.action);
|
|
try testing.expectEqual(@as(u21, 'a'), b.key.codepoint);
|
|
}
|
|
|
|
test "noopDeactivate returns a callable no-op" {
|
|
const S = struct { x: u32 = 0 };
|
|
const fn_ptr = noopDeactivate(S);
|
|
var s: S = .{};
|
|
var dummy_app: App = undefined;
|
|
fn_ptr(&s, &dummy_app);
|
|
try testing.expectEqual(@as(u32, 0), s.x);
|
|
}
|
|
|
|
test "noopTick returns a callable no-op" {
|
|
const S = struct { x: u32 = 0 };
|
|
const fn_ptr = noopTick(S);
|
|
var s: S = .{};
|
|
var dummy_app: App = undefined;
|
|
fn_ptr(&s, &dummy_app, 42);
|
|
try testing.expectEqual(@as(u32, 0), s.x);
|
|
}
|
|
|
|
test "alwaysEnabled returns false" {
|
|
const fn_ptr = alwaysEnabled();
|
|
var dummy_app: App = undefined;
|
|
try testing.expect(!fn_ptr(&dummy_app));
|
|
}
|