From 43ab8d195715dc76c8ba0d0afb9c4745e5e87884 Mon Sep 17 00:00:00 2001 From: Emil Lerch Date: Thu, 19 Mar 2026 11:10:26 -0700 Subject: [PATCH] centralize movement logic/debounce mouse wheel on cursor tabs --- src/tui.zig | 87 ++++++++++++++++++++++++++++------------------------- 1 file changed, 46 insertions(+), 41 deletions(-) diff --git a/src/tui.zig b/src/tui.zig index fcd8f54..ab96fef 100644 --- a/src/tui.zig +++ b/src/tui.zig @@ -308,6 +308,10 @@ pub const App = struct { classification_map: ?zfin.classification.ClassificationMap = 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: ChartState = .{}, 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 { switch (mouse.button) { .wheel_up => { - if (self.active_tab == .portfolio) { - 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; - } + self.moveBy(-3, true); return ctx.consumeAndRedraw(); }, .wheel_down => { - if (self.active_tab == .portfolio) { - 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; - } + self.moveBy(3, true); return ctx.consumeAndRedraw(); }, .left => { @@ -577,30 +563,11 @@ pub const App = struct { } }, .select_next => { - if (self.active_tab == .portfolio) { - 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; - } + self.moveBy(1, false); return ctx.consumeAndRedraw(); }, .select_prev => { - if (self.active_tab == .portfolio) { - 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; - } + self.moveBy(-1, false); return ctx.consumeAndRedraw(); }, .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 { const cursor_row = self.cursor + self.portfolio_header_lines; if (cursor_row < self.scroll_offset) {