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:
@@ -460,6 +460,7 @@ set(APP_SOURCES
|
||||
src/ui/windows/console_input_model.cpp
|
||||
src/ui/windows/console_model.cpp
|
||||
src/ui/windows/console_output_model.cpp
|
||||
src/ui/windows/console_scroll_controller.cpp
|
||||
src/ui/windows/console_selection_controller.cpp
|
||||
src/ui/windows/console_tab_helpers.cpp
|
||||
src/ui/windows/console_text_layout.cpp
|
||||
@@ -586,6 +587,7 @@ set(APP_HEADERS
|
||||
src/ui/windows/console_input_model.h
|
||||
src/ui/windows/console_model.h
|
||||
src/ui/windows/console_output_model.h
|
||||
src/ui/windows/console_scroll_controller.h
|
||||
src/ui/windows/console_selection_controller.h
|
||||
src/ui/windows/console_tab.h
|
||||
src/ui/windows/console_tab_helpers.h
|
||||
@@ -1025,6 +1027,7 @@ if(BUILD_TESTING)
|
||||
src/ui/windows/console_input_model.cpp
|
||||
src/ui/windows/console_model.cpp
|
||||
src/ui/windows/console_output_model.cpp
|
||||
src/ui/windows/console_scroll_controller.cpp
|
||||
src/ui/windows/console_selection_controller.cpp
|
||||
src/ui/windows/console_tab_helpers.cpp
|
||||
src/ui/windows/console_text_layout.cpp
|
||||
|
||||
39
src/ui/windows/console_scroll_controller.cpp
Normal file
39
src/ui/windows/console_scroll_controller.cpp
Normal file
@@ -0,0 +1,39 @@
|
||||
// DragonX Wallet - ImGui Edition
|
||||
// Copyright 2024-2026 The Hush Developers
|
||||
// Released under the GPLv3
|
||||
|
||||
#include "console_scroll_controller.h"
|
||||
|
||||
namespace dragonx {
|
||||
namespace ui {
|
||||
|
||||
void ConsoleScrollController::setAutoScroll(bool on)
|
||||
{
|
||||
auto_scroll_ = on;
|
||||
if (on) new_lines_ = 0; // re-pinning to the bottom clears the backlog counter
|
||||
}
|
||||
|
||||
void ConsoleScrollController::onUserScrolledUp()
|
||||
{
|
||||
auto_scroll_ = false;
|
||||
cooldown_ = kScrollUpCooldownSeconds;
|
||||
}
|
||||
|
||||
void ConsoleScrollController::tickCooldown(float dt)
|
||||
{
|
||||
if (cooldown_ > 0.0f) cooldown_ -= dt;
|
||||
}
|
||||
|
||||
void ConsoleScrollController::considerReenable(bool atBottom)
|
||||
{
|
||||
if (!auto_scroll_ && cooldown_ <= 0.0f && atBottom)
|
||||
pinToBottom();
|
||||
}
|
||||
|
||||
void ConsoleScrollController::onLinesAdded(int n)
|
||||
{
|
||||
if (!auto_scroll_ && n > 0) new_lines_ += n;
|
||||
}
|
||||
|
||||
} // namespace ui
|
||||
} // namespace dragonx
|
||||
63
src/ui/windows/console_scroll_controller.h
Normal file
63
src/ui/windows/console_scroll_controller.h
Normal file
@@ -0,0 +1,63 @@
|
||||
// DragonX Wallet - ImGui Edition
|
||||
// Copyright 2024-2026 The Hush Developers
|
||||
// Released under the GPLv3
|
||||
//
|
||||
// ConsoleScrollController — the console's auto-scroll state machine.
|
||||
//
|
||||
// It owns three coupled pieces of state that were previously loose fields on the god-class:
|
||||
// - auto_scroll_ : whether output is pinned to the bottom (follows new lines)
|
||||
// - a cooldown : a short grace period after a wheel-up so smooth-scroll can animate away
|
||||
// from the bottom before the "am I at the bottom?" re-enable check runs
|
||||
// - new_lines_ : how many lines arrived while scrolled up (drives the jump-to-bottom pill)
|
||||
//
|
||||
// It is ImGui-free: the view measures scroll positions and hands in a plain `atBottom` bool
|
||||
// and a frame delta; the controller decides when to re-pin. That keeps the interplay
|
||||
// (wheel-up pauses, cooldown gates the re-enable, new-line counting only while scrolled up)
|
||||
// unit testable.
|
||||
|
||||
#pragma once
|
||||
|
||||
namespace dragonx {
|
||||
namespace ui {
|
||||
|
||||
class ConsoleScrollController {
|
||||
public:
|
||||
// Seconds to ignore the at-bottom re-enable check after a user wheel-up, so an in-flight
|
||||
// smooth-scroll can move away from the bottom first.
|
||||
static constexpr float kScrollUpCooldownSeconds = 0.5f;
|
||||
|
||||
bool autoScroll() const { return auto_scroll_; }
|
||||
int newLines() const { return new_lines_; }
|
||||
|
||||
// User toggled the "auto-scroll" checkbox. Enabling re-pins to the bottom.
|
||||
void setAutoScroll(bool on);
|
||||
|
||||
// User scrolled up with the wheel — stop following output and start the cooldown.
|
||||
void onUserScrolledUp();
|
||||
|
||||
// Advance the cooldown timer by one frame.
|
||||
void tickCooldown(float dt);
|
||||
|
||||
// The view reports whether the viewport is currently at the bottom. If auto-scroll is off
|
||||
// and the cooldown has elapsed, reaching the bottom re-enables auto-scroll.
|
||||
void considerReenable(bool atBottom);
|
||||
|
||||
// `n` new lines were appended to the model this frame; count them only while scrolled up.
|
||||
void onLinesAdded(int n);
|
||||
|
||||
// Called each frame while pinned to the bottom (the view issues the actual scroll).
|
||||
void resetNewLines() { new_lines_ = 0; }
|
||||
|
||||
// User clicked the jump-to-bottom pill.
|
||||
void jumpToBottom() { pinToBottom(); }
|
||||
|
||||
private:
|
||||
void pinToBottom() { auto_scroll_ = true; new_lines_ = 0; }
|
||||
|
||||
bool auto_scroll_ = true;
|
||||
float cooldown_ = 0.0f;
|
||||
int new_lines_ = 0;
|
||||
};
|
||||
|
||||
} // namespace ui
|
||||
} // namespace dragonx
|
||||
@@ -268,14 +268,12 @@ void ConsoleTab::render(ConsoleCommandExecutor& exec)
|
||||
// At the bottom → re-enable; scrolled up → already disabled by wheel handler.
|
||||
// After wheel-up, wait for the cooldown so smooth-scroll can animate
|
||||
// away from the bottom before we check position again.
|
||||
if (scroll_up_cooldown_ > 0.0f)
|
||||
scroll_up_cooldown_ -= ImGui::GetIO().DeltaTime;
|
||||
if (!auto_scroll_ && scroll_up_cooldown_ <= 0.0f && consoleScrollMaxY > 0.0f) {
|
||||
scroll_.tickCooldown(ImGui::GetIO().DeltaTime);
|
||||
{
|
||||
float tolerance = Type().caption()->LegacySize * 1.5f;
|
||||
if (consoleScrollY >= consoleScrollMaxY - tolerance) {
|
||||
auto_scroll_ = true;
|
||||
new_lines_since_scroll_ = 0;
|
||||
}
|
||||
bool atBottom = consoleScrollMaxY > 0.0f &&
|
||||
consoleScrollY >= consoleScrollMaxY - tolerance;
|
||||
scroll_.considerReenable(atBottom);
|
||||
}
|
||||
|
||||
// CSS-style clipping mask
|
||||
@@ -283,7 +281,7 @@ void ConsoleTab::render(ConsoleCommandExecutor& exec)
|
||||
// inflating scrollMax so the mask thinks there's content below.
|
||||
{
|
||||
float fadeZone = std::min(Type().caption()->LegacySize * 3.0f, outputH * 0.18f);
|
||||
float effectiveScrollMax = auto_scroll_ ? consoleScrollMaxY
|
||||
float effectiveScrollMax = scroll_.autoScroll() ? consoleScrollMaxY
|
||||
: std::max(consoleScrollMaxY, consoleScrollY + 10.0f);
|
||||
ApplyScrollEdgeMask(dlOut, consoleParentVtx, consoleChildDL, consoleChildVtx,
|
||||
outPanelMin.y, outPanelMax.y, fadeZone, consoleScrollY, effectiveScrollMax);
|
||||
@@ -397,10 +395,9 @@ void ConsoleTab::renderToolbar(ConsoleCommandExecutor& exec)
|
||||
ImGui::SameLine();
|
||||
|
||||
// Auto-scroll toggle
|
||||
if (ImGui::Checkbox(TR("console_auto_scroll"), &auto_scroll_)) {
|
||||
if (auto_scroll_) {
|
||||
new_lines_since_scroll_ = 0;
|
||||
}
|
||||
bool autoScroll = scroll_.autoScroll();
|
||||
if (ImGui::Checkbox(TR("console_auto_scroll"), &autoScroll)) {
|
||||
scroll_.setAutoScroll(autoScroll);
|
||||
}
|
||||
|
||||
ImGui::SameLine();
|
||||
@@ -661,8 +658,7 @@ void ConsoleTab::renderOutput()
|
||||
|
||||
// Disable auto-scroll when user scrolls up (wheel scroll)
|
||||
if (mouse_in_output && io.MouseWheel > 0.0f) {
|
||||
auto_scroll_ = false;
|
||||
scroll_up_cooldown_ = 0.5f; // give smooth-scroll time to animate away
|
||||
scroll_.onUserScrolledUp();
|
||||
}
|
||||
// Scrolling down to the very bottom re-enables auto-scroll.
|
||||
// Actual position check happens after EndChild() using captured
|
||||
@@ -893,9 +889,9 @@ void ConsoleTab::renderOutput()
|
||||
|
||||
// Auto-scroll - when enabled, always scroll to bottom of content
|
||||
// This ensures daemon output stays visible and scrolled to bottom
|
||||
if (auto_scroll_) {
|
||||
if (scroll_.autoScroll()) {
|
||||
ImGui::SetScrollHereY(1.0f);
|
||||
new_lines_since_scroll_ = 0;
|
||||
scroll_.resetNewLines();
|
||||
}
|
||||
|
||||
// Filter indicator (text filter only — daemon toggle is already visible in toolbar)
|
||||
@@ -951,7 +947,7 @@ void ConsoleTab::renderOutput()
|
||||
}
|
||||
|
||||
// "New output" indicator when user is scrolled up and new lines arrived
|
||||
if (!auto_scroll_ && new_lines_since_scroll_ > 0) {
|
||||
if (!scroll_.autoScroll() && scroll_.newLines() > 0) {
|
||||
float indicW = 140.0f;
|
||||
float indicH = 24.0f;
|
||||
ImDrawList* dlInd = ImGui::GetWindowDrawList();
|
||||
@@ -966,7 +962,7 @@ void ConsoleTab::renderOutput()
|
||||
dlInd->AddRect(iMin, iMax, IM_COL32(255, 218, 0, 120), 12.0f);
|
||||
|
||||
char buf[48];
|
||||
snprintf(buf, sizeof(buf), TR("console_new_lines"), new_lines_since_scroll_);
|
||||
snprintf(buf, sizeof(buf), TR("console_new_lines"), scroll_.newLines());
|
||||
ImFont* capFont = Type().caption();
|
||||
if (!capFont) capFont = ImGui::GetFont();
|
||||
ImFont* icoFont = Type().iconSmall();
|
||||
@@ -986,8 +982,7 @@ void ConsoleTab::renderOutput()
|
||||
// Click to jump to bottom
|
||||
ImGui::SetCursorScreenPos(iMin);
|
||||
if (ImGui::InvisibleButton("##scrollToBottom", ImVec2(indicW, indicH))) {
|
||||
auto_scroll_ = true;
|
||||
new_lines_since_scroll_ = 0;
|
||||
scroll_.jumpToBottom();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1386,8 +1381,7 @@ void ConsoleTab::drainModel()
|
||||
selection_.shiftForEviction(static_cast<int>(dr.popped));
|
||||
|
||||
// Track new output that arrived while the user is scrolled up (for the indicator).
|
||||
if (!auto_scroll_)
|
||||
new_lines_since_scroll_ += static_cast<int>(dr.added);
|
||||
scroll_.onLinesAdded(static_cast<int>(dr.added));
|
||||
}
|
||||
|
||||
void ConsoleTab::addRpcTraceLine(const std::string& source, const std::string& method)
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
#include "console_channel.h"
|
||||
#include "console_model.h"
|
||||
#include "console_scroll_controller.h"
|
||||
#include "console_selection_controller.h"
|
||||
#include "console_text_layout.h"
|
||||
#include "../layout.h"
|
||||
@@ -117,11 +118,11 @@ private:
|
||||
std::vector<std::string> command_history_;
|
||||
int history_index_ = -1;
|
||||
char input_buffer_[4096] = {0};
|
||||
bool auto_scroll_ = true;
|
||||
float scroll_up_cooldown_ = 0.0f; // seconds to wait before re-enabling auto-scroll
|
||||
int new_lines_since_scroll_ = 0; // new lines while scrolled up (for indicator)
|
||||
// (log-ingestion cursors + result queue moved to the ConsoleCommandExecutor)
|
||||
|
||||
// Auto-scroll state machine (pin-to-bottom, wheel-up cooldown, new-line backlog count).
|
||||
ConsoleScrollController scroll_;
|
||||
|
||||
// Text-selection state + logic (anchor/caret, ordering, extraction, eviction shift).
|
||||
ConsoleSelectionController selection_;
|
||||
float output_scroll_y_ = 0.0f; // Track scroll position for selection
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user