centralize movement logic/debounce mouse wheel on cursor tabs

This commit is contained in:
Emil Lerch 2026-03-19 11:10:26 -07:00
parent 863111d801
commit 43ab8d1957
Signed by: lobo
GPG key ID: A7B62D657EF764F8

View file

@ -308,6 +308,10 @@ pub const App = struct {
classification_map: ?zfin.classification.ClassificationMap = null, classification_map: ?zfin.classification.ClassificationMap = null,
account_map: ?zfin.analysis.AccountMap = null, account_map: ?zfin.analysis.AccountMap = null,
// Mouse wheel debounce for cursor-based tabs (portfolio, options).
// Terminals often send multiple wheel events per physical tick.
last_wheel_ns: i128 = 0,
// Chart state (Kitty graphics) // Chart state (Kitty graphics)
chart: ChartState = .{}, chart: ChartState = .{},
vx_app: ?*vaxis.vxfw.App = null, // set during run(), for Kitty graphics access vx_app: ?*vaxis.vxfw.App = null, // set during run(), for Kitty graphics access
@ -346,29 +350,11 @@ pub const App = struct {
fn handleMouse(self: *App, ctx: *vaxis.vxfw.EventContext, mouse: vaxis.Mouse) void { fn handleMouse(self: *App, ctx: *vaxis.vxfw.EventContext, mouse: vaxis.Mouse) void {
switch (mouse.button) { switch (mouse.button) {
.wheel_up => { .wheel_up => {
if (self.active_tab == .portfolio) { self.moveBy(-3, true);
if (self.cursor > 0) self.cursor -= 1;
self.ensureCursorVisible();
} else if (self.active_tab == .options) {
if (self.options_cursor > 0) self.options_cursor -= 1;
self.ensureOptionsCursorVisible();
} else {
if (self.scroll_offset > 0) self.scroll_offset -= 3;
}
return ctx.consumeAndRedraw(); return ctx.consumeAndRedraw();
}, },
.wheel_down => { .wheel_down => {
if (self.active_tab == .portfolio) { self.moveBy(3, true);
if (self.portfolio_rows.items.len > 0 and self.cursor < self.portfolio_rows.items.len - 1)
self.cursor += 1;
self.ensureCursorVisible();
} else if (self.active_tab == .options) {
if (self.options_rows.items.len > 0 and self.options_cursor < self.options_rows.items.len - 1)
self.options_cursor += 1;
self.ensureOptionsCursorVisible();
} else {
self.scroll_offset += 3;
}
return ctx.consumeAndRedraw(); return ctx.consumeAndRedraw();
}, },
.left => { .left => {
@ -577,30 +563,11 @@ pub const App = struct {
} }
}, },
.select_next => { .select_next => {
if (self.active_tab == .portfolio) { self.moveBy(1, false);
if (self.portfolio_rows.items.len > 0 and self.cursor < self.portfolio_rows.items.len - 1)
self.cursor += 1;
self.ensureCursorVisible();
} else if (self.active_tab == .options) {
if (self.options_rows.items.len > 0 and self.options_cursor < self.options_rows.items.len - 1)
self.options_cursor += 1;
self.ensureOptionsCursorVisible();
} else {
self.scroll_offset += 1;
}
return ctx.consumeAndRedraw(); return ctx.consumeAndRedraw();
}, },
.select_prev => { .select_prev => {
if (self.active_tab == .portfolio) { self.moveBy(-1, false);
if (self.cursor > 0) self.cursor -= 1;
self.ensureCursorVisible();
} else if (self.active_tab == .options) {
if (self.options_cursor > 0)
self.options_cursor -= 1;
self.ensureOptionsCursorVisible();
} else {
if (self.scroll_offset > 0) self.scroll_offset -= 1;
}
return ctx.consumeAndRedraw(); return ctx.consumeAndRedraw();
}, },
.expand_collapse => { .expand_collapse => {
@ -737,6 +704,44 @@ pub const App = struct {
} }
} }
/// Move cursor/scroll. Positive = down, negative = up.
/// For portfolio and options tabs, moves the row cursor by 1.
/// For other tabs, adjusts scroll_offset by |n|.
/// When from_wheel is true, debounces on cursor tabs to absorb
/// duplicate events that terminals send per physical scroll tick.
fn moveBy(self: *App, n: isize, from_wheel: bool) void {
if (self.active_tab == .portfolio or self.active_tab == .options) {
if (from_wheel) {
const now = std.time.nanoTimestamp();
if (now - self.last_wheel_ns < 1 * std.time.ns_per_ms) return;
self.last_wheel_ns = now;
}
if (self.active_tab == .portfolio) {
stepCursor(&self.cursor, self.portfolio_rows.items.len, n);
self.ensureCursorVisible();
} else {
stepCursor(&self.options_cursor, self.options_rows.items.len, n);
self.ensureOptionsCursorVisible();
}
} else {
if (n > 0) {
self.scroll_offset += @intCast(n);
} else {
const abs: usize = @intCast(-n);
if (self.scroll_offset > abs) self.scroll_offset -= abs else self.scroll_offset = 0;
}
}
}
fn stepCursor(cursor: *usize, row_count: usize, direction: isize) void {
if (direction > 0) {
if (row_count > 0 and cursor.* < row_count - 1)
cursor.* += 1;
} else {
if (cursor.* > 0) cursor.* -= 1;
}
}
fn ensureCursorVisible(self: *App) void { fn ensureCursorVisible(self: *App) void {
const cursor_row = self.cursor + self.portfolio_header_lines; const cursor_row = self.cursor + self.portfolio_header_lines;
if (cursor_row < self.scroll_offset) { if (cursor_row < self.scroll_offset) {