refactor(console): phase 4b — extract ConsoleScrollController

Pull the auto-scroll concern out of ConsoleTab into a focused, ImGui-free
ConsoleScrollController (console_scroll_controller.{h,cpp}). It owns the three
coupled fields that were loose on the god-class — auto_scroll_, the wheel-up
cooldown, and the new-lines-while-scrolled-up backlog count — and the state
machine tying them together: wheel-up detaches + starts a cooldown, the cooldown
gates the at-bottom re-enable check, backlog counting happens only while
detached, and re-pinning (checkbox / jump pill / reaching the bottom) clears it.

The view keeps only what needs ImGui: it measures scroll position, hands the
controller a plain `atBottom` bool + frame delta, and issues the actual
SetScrollHereY. ConsoleTab loses three more members and all the scattered
auto-scroll bookkeeping collapses to scroll_.* calls.

Adds testConsoleScrollController: pinned start ignores backlog, wheel-up detach +
cooldown gating, re-enable only when at bottom, checkbox toggle semantics, and
the jump-to-bottom pill. Full-node + lite build clean; ctest green; source
hygiene clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-01 18:52:15 -05:00
parent 0cdbbd024e
commit 18c1de87db
6 changed files with 185 additions and 25 deletions

View File

@@ -15,6 +15,7 @@
#include "ui/windows/console_input_model.h"
#include "ui/windows/console_model.h"
#include "ui/windows/console_output_model.h"
#include "ui/windows/console_scroll_controller.h"
#include "ui/windows/console_selection_controller.h"
#include "ui/windows/console_tab_helpers.h"
#include "ui/windows/console_text_layout.h"
@@ -2338,6 +2339,64 @@ void testConsoleTextLayout()
EXPECT_EQ(ExtractConsoleSelection(2, get2, std::vector<int>{0}, {0, 1}, {1, 3}), std::string("ello"));
}
// ConsoleScrollController — auto-scroll state machine (pin, wheel-up cooldown, backlog).
void testConsoleScrollController()
{
using dragonx::ui::ConsoleScrollController;
// Starts pinned to the bottom; new lines while pinned are not counted as a backlog.
{
ConsoleScrollController s;
EXPECT_TRUE(s.autoScroll());
s.onLinesAdded(5);
EXPECT_EQ(s.newLines(), 0);
}
// Wheel-up detaches, starts the cooldown, and begins counting backlog lines.
{
ConsoleScrollController s;
s.onUserScrolledUp();
EXPECT_FALSE(s.autoScroll());
s.onLinesAdded(3);
s.onLinesAdded(2);
EXPECT_EQ(s.newLines(), 5);
// At-bottom is ignored until the cooldown elapses.
s.considerReenable(true);
EXPECT_FALSE(s.autoScroll());
s.tickCooldown(ConsoleScrollController::kScrollUpCooldownSeconds + 0.01f);
// Elapsed, but only re-pins when actually at the bottom.
s.considerReenable(false);
EXPECT_FALSE(s.autoScroll());
s.considerReenable(true);
EXPECT_TRUE(s.autoScroll());
EXPECT_EQ(s.newLines(), 0); // re-pinning clears the backlog
}
// Checkbox toggle: enabling clears the backlog, disabling preserves state.
{
ConsoleScrollController s;
s.onUserScrolledUp();
s.onLinesAdded(4);
EXPECT_EQ(s.newLines(), 4);
s.setAutoScroll(true);
EXPECT_TRUE(s.autoScroll());
EXPECT_EQ(s.newLines(), 0);
s.setAutoScroll(false);
EXPECT_FALSE(s.autoScroll());
}
// Jump-to-bottom pill re-pins and clears the backlog.
{
ConsoleScrollController s;
s.onUserScrolledUp();
s.onLinesAdded(9);
s.jumpToBottom();
EXPECT_TRUE(s.autoScroll());
EXPECT_EQ(s.newLines(), 0);
}
}
// ConsoleSelectionController — anchor/caret state, ordering, select-all, eviction shift.
void testConsoleSelectionController()
{
@@ -5192,6 +5251,7 @@ int main()
testDaemonLifecycleAdapters();
testConsoleTextLayout();
testConsoleModel();
testConsoleScrollController();
testConsoleSelectionController();
testRendererHelpers();
testConsoleInputModel();