refactor(console): phase 5 — decompose renderOutput into focused sub-steps
renderOutput() was a ~410-line method doing filter-building, mouse/keyboard
interaction, the per-line draw loop, the new-output pill, and the right-click
context menu all inline. Split it into a thin orchestrator that calls five
single-purpose private methods (pure code motion, no behavior change):
- computeVisibleLines() -> the filter -> visible_indices_ + match count
- handleOutputInteraction() -> wheel-up, selection drag lifecycle, Ctrl+C/A
- drawVisibleLines() -> accent bars, JSON indent guides, selection +
filter highlight, scanline capture, text
- drawOutputContextMenu() -> the right-click menu
- drawNewOutputIndicator() -> the jump-to-bottom pill
Ordering and every operation are preserved exactly (interaction still runs
after layout build and before the draw; selection bounds are recomputed post-
interaction; scanline_rows_ cleared before the draw pushes to it). 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:
@@ -599,19 +599,95 @@ void ConsoleTab::renderOutput()
|
|||||||
output_scroll_y_ = ImGui::GetScrollY();
|
output_scroll_y_ = ImGui::GetScrollY();
|
||||||
scanline_rows_.clear();
|
scanline_rows_.clear();
|
||||||
|
|
||||||
// Build filtered line index list BEFORE mouse handling (so screenToTextPos works)
|
// Build the filtered visible-line index list BEFORE mouse handling (screenToTextPos maps
|
||||||
|
// through visible_indices_). Also yields the text-filter state used for highlighting.
|
||||||
|
bool has_text_filter = false;
|
||||||
|
std::string filter_lower;
|
||||||
|
computeVisibleLines(has_text_filter, filter_lower);
|
||||||
|
|
||||||
|
// Calculate wrapped heights AND build sub-row segments for each visible line. Each
|
||||||
|
// segment records which bytes of the source text appear on that visual row, so
|
||||||
|
// hit-testing and selection highlight can map screen positions to exact char offsets.
|
||||||
|
float wrap_width = ClampConsoleWrapWidth(ImGui::GetContentRegionAvail().x, padX);
|
||||||
|
ImFontConsoleMeasure measure(ImGui::GetFont(), ImGui::GetFontSize());
|
||||||
|
layout_ = BuildConsoleLayout(
|
||||||
|
static_cast<int>(visible_indices_.size()),
|
||||||
|
[this](int vi) -> const std::string& { return model_[visible_indices_[vi]].text; },
|
||||||
|
wrap_width, line_height, interLineGap, measure);
|
||||||
|
|
||||||
|
// Mouse/keyboard interaction (wheel-up detach, selection drag, Ctrl+C/A). Raw IO bypasses
|
||||||
|
// the child window's event consumption.
|
||||||
|
ImVec2 mouse_pos = ImGui::GetIO().MousePos;
|
||||||
|
ImVec2 win_min = ImGui::GetWindowPos();
|
||||||
|
ImVec2 win_max = ImVec2(win_min.x + ImGui::GetWindowWidth(),
|
||||||
|
win_min.y + ImGui::GetWindowHeight());
|
||||||
|
bool mouse_in_output = (mouse_pos.x >= win_min.x && mouse_pos.x < win_max.x &&
|
||||||
|
mouse_pos.y >= win_min.y && mouse_pos.y < win_max.y &&
|
||||||
|
!ImGui::IsPopupOpen("", ImGuiPopupFlags_AnyPopup));
|
||||||
|
handleOutputInteraction(mouse_pos, mouse_in_output);
|
||||||
|
|
||||||
|
// Draw the visible lines: accent bars, JSON indent guides, selection + filter highlight,
|
||||||
|
// scanline capture, and the text itself.
|
||||||
|
drawVisibleLines(padX, line_height, has_text_filter, filter_lower);
|
||||||
|
|
||||||
|
ImGui::Unindent(padX);
|
||||||
|
ImGui::PopStyleVar();
|
||||||
|
|
||||||
|
// Bottom padding keeps the last line above the fade-out zone.
|
||||||
|
// Always present so that scrollMaxY stays stable when auto-scroll
|
||||||
|
// toggles — otherwise the geometry shift clamps the user back to
|
||||||
|
// bottom and a single scroll-up tick can't escape.
|
||||||
|
{
|
||||||
|
float fadeZone = std::min(Type().caption()->LegacySize * 3.0f,
|
||||||
|
ImGui::GetWindowHeight() * 0.18f);
|
||||||
|
ImGui::Dummy(ImVec2(0, fadeZone));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-scroll - when enabled, always scroll to bottom of content
|
||||||
|
// This ensures daemon output stays visible and scrolled to bottom
|
||||||
|
if (scroll_.autoScroll()) {
|
||||||
|
ImGui::SetScrollHereY(1.0f);
|
||||||
|
scroll_.resetNewLines();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter indicator (text filter only — daemon toggle is already visible in toolbar)
|
||||||
|
if (has_text_filter) {
|
||||||
|
char filterBuf[128];
|
||||||
|
snprintf(filterBuf, sizeof(filterBuf), TR("console_showing_lines"),
|
||||||
|
static_cast<int>(visible_indices_.size()), model_.size());
|
||||||
|
ImVec2 indicatorPos = ImGui::GetCursorScreenPos();
|
||||||
|
ImGui::GetWindowDrawList()->AddText(indicatorPos,
|
||||||
|
WithAlpha(Warning(), 180), filterBuf);
|
||||||
|
ImGui::Dummy(ImVec2(0, ImGui::GetTextLineHeight()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Capture the hash/address under the cursor on right-click, for "Copy value".
|
||||||
|
if (ImGui::IsMouseClicked(ImGuiMouseButton_Right)) {
|
||||||
|
ConsoleTextPos tp = screenToTextPos(ImGui::GetMousePos());
|
||||||
|
context_token_.clear();
|
||||||
|
if (tp.line >= 0 && tp.line < static_cast<int>(model_.size()))
|
||||||
|
context_token_ = extractCopyableToken(model_[tp.line].text, tp.col);
|
||||||
|
}
|
||||||
|
|
||||||
|
drawOutputContextMenu();
|
||||||
|
drawNewOutputIndicator();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── renderOutput sub-steps ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
void ConsoleTab::computeVisibleLines(bool& hasTextFilter, std::string& filterLower)
|
||||||
|
{
|
||||||
ConsoleOutputFilter outputFilter{filter_text_, s_daemon_messages_enabled,
|
ConsoleOutputFilter outputFilter{filter_text_, s_daemon_messages_enabled,
|
||||||
s_errors_only_enabled, s_rpc_trace_enabled,
|
s_errors_only_enabled, s_rpc_trace_enabled,
|
||||||
s_app_messages_enabled};
|
s_app_messages_enabled};
|
||||||
bool has_text_filter = !outputFilter.text.empty();
|
hasTextFilter = !outputFilter.text.empty();
|
||||||
// Lowercased needle for in-line match highlighting.
|
// Lowercased needle for in-line match highlighting.
|
||||||
std::string filter_lower;
|
if (hasTextFilter) {
|
||||||
if (has_text_filter) {
|
filterLower = std::string(filter_text_);
|
||||||
filter_lower = std::string(filter_text_);
|
std::transform(filterLower.begin(), filterLower.end(), filterLower.begin(),
|
||||||
std::transform(filter_lower.begin(), filter_lower.end(), filter_lower.begin(),
|
|
||||||
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
|
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
|
||||||
}
|
}
|
||||||
bool has_filter = has_text_filter || !outputFilter.daemonMessagesEnabled ||
|
bool has_filter = hasTextFilter || !outputFilter.daemonMessagesEnabled ||
|
||||||
!outputFilter.rpcTraceEnabled || !outputFilter.appMessagesEnabled ||
|
!outputFilter.rpcTraceEnabled || !outputFilter.appMessagesEnabled ||
|
||||||
outputFilter.errorsOnly;
|
outputFilter.errorsOnly;
|
||||||
visible_indices_.clear();
|
visible_indices_.clear();
|
||||||
@@ -622,112 +698,81 @@ void ConsoleTab::renderOutput()
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// No filter - all lines are visible
|
// No filter - all lines are visible
|
||||||
for (int i = 0; i < static_cast<int>(model_.size()); i++) {
|
for (int i = 0; i < static_cast<int>(model_.size()); i++)
|
||||||
visible_indices_.push_back(i);
|
visible_indices_.push_back(i);
|
||||||
}
|
}
|
||||||
}
|
filter_match_count_ = hasTextFilter ? static_cast<int>(visible_indices_.size()) : 0;
|
||||||
int visible_count = static_cast<int>(visible_indices_.size());
|
}
|
||||||
filter_match_count_ = has_text_filter ? visible_count : 0;
|
|
||||||
|
|
||||||
// Calculate wrapped heights AND build sub-row segments for each visible line.
|
void ConsoleTab::handleOutputInteraction(ImVec2 mousePos, bool mouseInOutput)
|
||||||
// Each segment records which bytes of the source text appear on that visual
|
{
|
||||||
// row, so hit-testing and selection highlight can map screen positions to
|
|
||||||
// exact character offsets.
|
|
||||||
float wrap_width = ClampConsoleWrapWidth(ImGui::GetContentRegionAvail().x, padX);
|
|
||||||
|
|
||||||
ImFont* font = ImGui::GetFont();
|
|
||||||
float fontSize = ImGui::GetFontSize();
|
|
||||||
|
|
||||||
ImFontConsoleMeasure measure(font, fontSize);
|
|
||||||
layout_ = BuildConsoleLayout(
|
|
||||||
visible_count,
|
|
||||||
[this](int vi) -> const std::string& { return model_[visible_indices_[vi]].text; },
|
|
||||||
wrap_width, line_height, interLineGap, measure);
|
|
||||||
|
|
||||||
// Use raw IO for mouse handling to bypass child window event consumption
|
|
||||||
ImGuiIO& io = ImGui::GetIO();
|
ImGuiIO& io = ImGui::GetIO();
|
||||||
ImVec2 mouse_pos = io.MousePos;
|
|
||||||
|
|
||||||
// Manual hit test: is mouse within this child window?
|
// Disable auto-scroll when the user scrolls up (wheel). Scrolling back down to the very
|
||||||
ImVec2 win_min = ImGui::GetWindowPos();
|
// bottom re-enables it; that position check happens after EndChild() in render(), and is
|
||||||
ImVec2 win_max = ImVec2(win_min.x + ImGui::GetWindowWidth(),
|
// skipped on the wheel-up frame (the scroll position hasn't caught up yet).
|
||||||
win_min.y + ImGui::GetWindowHeight());
|
if (mouseInOutput && io.MouseWheel > 0.0f) {
|
||||||
bool mouse_in_output = (mouse_pos.x >= win_min.x && mouse_pos.x < win_max.x &&
|
|
||||||
mouse_pos.y >= win_min.y && mouse_pos.y < win_max.y &&
|
|
||||||
!ImGui::IsPopupOpen("", ImGuiPopupFlags_AnyPopup));
|
|
||||||
|
|
||||||
// Disable auto-scroll when user scrolls up (wheel scroll)
|
|
||||||
if (mouse_in_output && io.MouseWheel > 0.0f) {
|
|
||||||
scroll_.onUserScrolledUp();
|
scroll_.onUserScrolledUp();
|
||||||
}
|
}
|
||||||
// Scrolling down to the very bottom re-enables auto-scroll.
|
// Text-selection cursor while hovering.
|
||||||
// Actual position check happens after EndChild() using captured
|
if (mouseInOutput) {
|
||||||
// scroll values, but is skipped on the frame where wheel-up
|
|
||||||
// was detected (scroll position hasn't caught up yet).
|
|
||||||
|
|
||||||
// Set cursor to text selection when hovering
|
|
||||||
if (mouse_in_output) {
|
|
||||||
ImGui::SetMouseCursor(ImGuiMouseCursor_TextInput);
|
ImGui::SetMouseCursor(ImGuiMouseCursor_TextInput);
|
||||||
}
|
}
|
||||||
|
// Selection drag lifecycle (continues even if the mouse leaves the window).
|
||||||
// Mouse press - start selection (use raw IO mouse state)
|
if (mouseInOutput && io.MouseClicked[0]) {
|
||||||
if (mouse_in_output && io.MouseClicked[0]) {
|
selection_.beginDrag(screenToTextPos(mousePos));
|
||||||
selection_.beginDrag(screenToTextPos(mouse_pos));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mouse drag - extend selection (continue even if mouse leaves the window)
|
|
||||||
if (selection_.dragging() && io.MouseDown[0]) {
|
if (selection_.dragging() && io.MouseDown[0]) {
|
||||||
selection_.updateDrag(screenToTextPos(mouse_pos));
|
selection_.updateDrag(screenToTextPos(mousePos));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mouse release - finalize selection
|
|
||||||
if (selection_.dragging() && io.MouseReleased[0]) {
|
if (selection_.dragging() && io.MouseReleased[0]) {
|
||||||
selection_.endDrag(screenToTextPos(mouse_pos));
|
selection_.endDrag(screenToTextPos(mousePos));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ctrl+C / Ctrl+A
|
// Ctrl+C / Ctrl+A
|
||||||
if (mouse_in_output || selection_.active()) {
|
if (mouseInOutput || selection_.active()) {
|
||||||
if (io.KeyCtrl && ImGui::IsKeyPressed(ImGuiKey_C)) {
|
if (io.KeyCtrl && ImGui::IsKeyPressed(ImGuiKey_C)) {
|
||||||
std::string selected = selectedText();
|
std::string selected = selectedText();
|
||||||
if (!selected.empty()) {
|
if (!selected.empty()) ImGui::SetClipboardText(selected.c_str());
|
||||||
ImGui::SetClipboardText(selected.c_str());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (io.KeyCtrl && ImGui::IsKeyPressed(ImGuiKey_A) && !model_.empty()) {
|
if (io.KeyCtrl && ImGui::IsKeyPressed(ImGuiKey_A) && !model_.empty()) {
|
||||||
selection_.selectAll(static_cast<int>(model_.size()),
|
selection_.selectAll(static_cast<int>(model_.size()),
|
||||||
static_cast<int>(model_.back().text.size()));
|
static_cast<int>(model_.back().text.size()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Get selection bounds (ordered)
|
void ConsoleTab::drawVisibleLines(float padX, float lineHeight, bool hasTextFilter,
|
||||||
|
const std::string& filterLower)
|
||||||
|
{
|
||||||
|
using namespace material;
|
||||||
|
ImFont* font = ImGui::GetFont();
|
||||||
|
float fontSize = ImGui::GetFontSize();
|
||||||
|
const int visible_count = static_cast<int>(visible_indices_.size());
|
||||||
|
|
||||||
|
// Ordered selection bounds (only consulted when selection_.active()).
|
||||||
ConsoleTextPos sel_start_pos = selection_.rangeStart();
|
ConsoleTextPos sel_start_pos = selection_.rangeStart();
|
||||||
ConsoleTextPos sel_end_pos = selection_.rangeEnd();
|
ConsoleTextPos sel_end_pos = selection_.rangeEnd();
|
||||||
|
|
||||||
// Render lines with selection highlighting.
|
// Cull to the visible viewport rows for cheap scrolling over large buffers.
|
||||||
// Each line is split into pre-computed wrap segments rendered individually
|
|
||||||
// via AddText so that hit-testing and highlights map 1:1 to visual positions.
|
|
||||||
float scroll_y = ImGui::GetScrollY();
|
float scroll_y = ImGui::GetScrollY();
|
||||||
float window_height = ImGui::GetWindowHeight();
|
float window_height = ImGui::GetWindowHeight();
|
||||||
float visible_top = scroll_y;
|
float visible_top = scroll_y;
|
||||||
float visible_bottom = scroll_y + window_height;
|
float visible_bottom = scroll_y + window_height;
|
||||||
|
|
||||||
// Find first visible line using binary search
|
// Find first visible line using binary search.
|
||||||
int first_visible = 0;
|
int first_visible = 0;
|
||||||
if (!layout_.cumulativeY.empty()) {
|
if (!layout_.cumulativeY.empty()) {
|
||||||
int lo = 0, hi = static_cast<int>(layout_.cumulativeY.size()) - 1;
|
int lo = 0, hi = static_cast<int>(layout_.cumulativeY.size()) - 1;
|
||||||
while (lo < hi) {
|
while (lo < hi) {
|
||||||
int mid = (lo + hi) / 2;
|
int mid = (lo + hi) / 2;
|
||||||
float line_bottom = layout_.cumulativeY[mid] + layout_.heights[mid];
|
float line_bottom = layout_.cumulativeY[mid] + layout_.heights[mid];
|
||||||
if (line_bottom < visible_top) {
|
if (line_bottom < visible_top) lo = mid + 1;
|
||||||
lo = mid + 1;
|
else hi = mid;
|
||||||
} else {
|
|
||||||
hi = mid;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
first_visible = lo;
|
first_visible = lo;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add invisible spacer for lines before first visible (for correct scroll)
|
// Invisible spacer for lines before the first visible one (keeps scroll correct).
|
||||||
if (first_visible > 0 && first_visible < static_cast<int>(layout_.cumulativeY.size())) {
|
if (first_visible > 0 && first_visible < static_cast<int>(layout_.cumulativeY.size())) {
|
||||||
ImGui::Dummy(ImVec2(0, layout_.cumulativeY[first_visible]));
|
ImGui::Dummy(ImVec2(0, layout_.cumulativeY[first_visible]));
|
||||||
}
|
}
|
||||||
@@ -735,7 +780,6 @@ void ConsoleTab::renderOutput()
|
|||||||
ImDrawList* dl = ImGui::GetWindowDrawList();
|
ImDrawList* dl = ImGui::GetWindowDrawList();
|
||||||
ImU32 selColor = WithAlpha(Secondary(), 80);
|
ImU32 selColor = WithAlpha(Secondary(), 80);
|
||||||
|
|
||||||
// Render visible lines
|
|
||||||
int last_rendered_vi = first_visible - 1;
|
int last_rendered_vi = first_visible - 1;
|
||||||
for (int vi = first_visible; vi < visible_count; vi++) {
|
for (int vi = first_visible; vi < visible_count; vi++) {
|
||||||
if (vi < static_cast<int>(layout_.cumulativeY.size()) &&
|
if (vi < static_cast<int>(layout_.cumulativeY.size()) &&
|
||||||
@@ -805,8 +849,8 @@ void ConsoleTab::renderOutput()
|
|||||||
const char* segStart = line.text.c_str() + seg.byteStart;
|
const char* segStart = line.text.c_str() + seg.byteStart;
|
||||||
const char* segEnd = line.text.c_str() + seg.byteEnd;
|
const char* segEnd = line.text.c_str() + seg.byteEnd;
|
||||||
|
|
||||||
if (s_scanline_enabled && line_height > 0.0f) {
|
if (s_scanline_enabled && lineHeight > 0.0f) {
|
||||||
int rowIndex = static_cast<int>(std::floor((layout_.cumulativeY[vi] + seg.yOffset) / line_height + 0.5f));
|
int rowIndex = static_cast<int>(std::floor((layout_.cumulativeY[vi] + seg.yOffset) / lineHeight + 0.5f));
|
||||||
scanline_rows_.push_back({rowY, rowY + seg.height, rowIndex});
|
scanline_rows_.push_back({rowY, rowY + seg.height, rowIndex});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -835,19 +879,19 @@ void ConsoleTab::renderOutput()
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Filter match highlight (case-insensitive) within this segment.
|
// Filter match highlight (case-insensitive) within this segment.
|
||||||
if (has_text_filter && !filter_lower.empty() && seg.byteStart < seg.byteEnd) {
|
if (hasTextFilter && !filterLower.empty() && seg.byteStart < seg.byteEnd) {
|
||||||
std::string segLower(segStart, segEnd);
|
std::string segLower(segStart, segEnd);
|
||||||
std::transform(segLower.begin(), segLower.end(), segLower.begin(),
|
std::transform(segLower.begin(), segLower.end(), segLower.begin(),
|
||||||
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
|
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
|
||||||
size_t pos = 0;
|
size_t pos = 0;
|
||||||
while ((pos = segLower.find(filter_lower, pos)) != std::string::npos) {
|
while ((pos = segLower.find(filterLower, pos)) != std::string::npos) {
|
||||||
float xs = font->CalcTextSizeA(fontSize, FLT_MAX, 0, segStart, segStart + static_cast<int>(pos)).x;
|
float xs = font->CalcTextSizeA(fontSize, FLT_MAX, 0, segStart, segStart + static_cast<int>(pos)).x;
|
||||||
float xe = font->CalcTextSizeA(fontSize, FLT_MAX, 0, segStart,
|
float xe = font->CalcTextSizeA(fontSize, FLT_MAX, 0, segStart,
|
||||||
segStart + static_cast<int>(pos + filter_lower.size())).x;
|
segStart + static_cast<int>(pos + filterLower.size())).x;
|
||||||
dl->AddRectFilled(ImVec2(lineOrigin.x + xs, rowY),
|
dl->AddRectFilled(ImVec2(lineOrigin.x + xs, rowY),
|
||||||
ImVec2(lineOrigin.x + xe, rowY + seg.height),
|
ImVec2(lineOrigin.x + xe, rowY + seg.height),
|
||||||
IM_COL32(255, 210, 0, 70));
|
IM_COL32(255, 210, 0, 70));
|
||||||
pos += filter_lower.size();
|
pos += filterLower.size();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -873,48 +917,12 @@ void ConsoleTab::renderOutput()
|
|||||||
ImGui::Dummy(ImVec2(0, remaining_height));
|
ImGui::Dummy(ImVec2(0, remaining_height));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ImGui::Unindent(padX);
|
void ConsoleTab::drawOutputContextMenu()
|
||||||
ImGui::PopStyleVar();
|
{
|
||||||
|
if (!ImGui::BeginPopupContextWindow("ConsoleContextMenu")) return;
|
||||||
|
|
||||||
// Bottom padding keeps the last line above the fade-out zone.
|
|
||||||
// Always present so that scrollMaxY stays stable when auto-scroll
|
|
||||||
// toggles — otherwise the geometry shift clamps the user back to
|
|
||||||
// bottom and a single scroll-up tick can't escape.
|
|
||||||
{
|
|
||||||
float fadeZone = std::min(Type().caption()->LegacySize * 3.0f,
|
|
||||||
ImGui::GetWindowHeight() * 0.18f);
|
|
||||||
ImGui::Dummy(ImVec2(0, fadeZone));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Auto-scroll - when enabled, always scroll to bottom of content
|
|
||||||
// This ensures daemon output stays visible and scrolled to bottom
|
|
||||||
if (scroll_.autoScroll()) {
|
|
||||||
ImGui::SetScrollHereY(1.0f);
|
|
||||||
scroll_.resetNewLines();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filter indicator (text filter only — daemon toggle is already visible in toolbar)
|
|
||||||
if (has_text_filter) {
|
|
||||||
char filterBuf[128];
|
|
||||||
snprintf(filterBuf, sizeof(filterBuf), TR("console_showing_lines"),
|
|
||||||
visible_count, model_.size());
|
|
||||||
ImVec2 indicatorPos = ImGui::GetCursorScreenPos();
|
|
||||||
ImGui::GetWindowDrawList()->AddText(indicatorPos,
|
|
||||||
WithAlpha(Warning(), 180), filterBuf);
|
|
||||||
ImGui::Dummy(ImVec2(0, ImGui::GetTextLineHeight()));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Capture the hash/address under the cursor on right-click, for "Copy value".
|
|
||||||
if (ImGui::IsMouseClicked(ImGuiMouseButton_Right)) {
|
|
||||||
ConsoleTextPos tp = screenToTextPos(ImGui::GetMousePos());
|
|
||||||
context_token_.clear();
|
|
||||||
if (tp.line >= 0 && tp.line < static_cast<int>(model_.size()))
|
|
||||||
context_token_ = extractCopyableToken(model_[tp.line].text, tp.col);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Right-click context menu
|
|
||||||
if (ImGui::BeginPopupContextWindow("ConsoleContextMenu")) {
|
|
||||||
if (!context_token_.empty()) {
|
if (!context_token_.empty()) {
|
||||||
std::string shortTok = context_token_.size() > 22
|
std::string shortTok = context_token_.size() > 22
|
||||||
? context_token_.substr(0, 12) + "\xE2\x80\xA6" + context_token_.substr(context_token_.size() - 6)
|
? context_token_.substr(0, 12) + "\xE2\x80\xA6" + context_token_.substr(context_token_.size() - 6)
|
||||||
@@ -944,10 +952,14 @@ void ConsoleTab::renderOutput()
|
|||||||
selection_.clear();
|
selection_.clear();
|
||||||
}
|
}
|
||||||
ImGui::EndPopup();
|
ImGui::EndPopup();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ConsoleTab::drawNewOutputIndicator()
|
||||||
|
{
|
||||||
|
using namespace material;
|
||||||
|
// "New output" indicator when the user is scrolled up and new lines arrived.
|
||||||
|
if (scroll_.autoScroll() || scroll_.newLines() <= 0) return;
|
||||||
|
|
||||||
// "New output" indicator when user is scrolled up and new lines arrived
|
|
||||||
if (!scroll_.autoScroll() && scroll_.newLines() > 0) {
|
|
||||||
float indicW = 140.0f;
|
float indicW = 140.0f;
|
||||||
float indicH = 24.0f;
|
float indicH = 24.0f;
|
||||||
ImDrawList* dlInd = ImGui::GetWindowDrawList();
|
ImDrawList* dlInd = ImGui::GetWindowDrawList();
|
||||||
@@ -984,7 +996,6 @@ void ConsoleTab::renderOutput()
|
|||||||
if (ImGui::InvisibleButton("##scrollToBottom", ImVec2(indicW, indicH))) {
|
if (ImGui::InvisibleButton("##scrollToBottom", ImVec2(indicW, indicH))) {
|
||||||
scroll_.jumpToBottom();
|
scroll_.jumpToBottom();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ConsoleTextPos ConsoleTab::screenToTextPos(ImVec2 screen_pos) const
|
ConsoleTextPos ConsoleTab::screenToTextPos(ImVec2 screen_pos) const
|
||||||
|
|||||||
@@ -101,10 +101,18 @@ private:
|
|||||||
void addFormattedResult(const std::string& result, bool is_error);
|
void addFormattedResult(const std::string& result, bool is_error);
|
||||||
void renderStatusHeader(ConsoleCommandExecutor& exec);
|
void renderStatusHeader(ConsoleCommandExecutor& exec);
|
||||||
void renderToolbar(ConsoleCommandExecutor& exec);
|
void renderToolbar(ConsoleCommandExecutor& exec);
|
||||||
void renderOutput();
|
|
||||||
void renderInput(ConsoleCommandExecutor& exec);
|
void renderInput(ConsoleCommandExecutor& exec);
|
||||||
void renderCommandsPopup();
|
void renderCommandsPopup();
|
||||||
|
|
||||||
|
// renderOutput() orchestrates the scrollable output area; these are its sub-steps:
|
||||||
|
void renderOutput();
|
||||||
|
void computeVisibleLines(bool& hasTextFilter, std::string& filterLower); // -> visible_indices_
|
||||||
|
void handleOutputInteraction(ImVec2 mousePos, bool mouseInOutput); // wheel/selection/keys
|
||||||
|
void drawVisibleLines(float padX, float lineHeight, bool hasTextFilter,
|
||||||
|
const std::string& filterLower); // bars/guides/highlight/text
|
||||||
|
void drawNewOutputIndicator(); // jump-to-bottom pill
|
||||||
|
void drawOutputContextMenu(); // right-click menu
|
||||||
|
|
||||||
// Map a screen point to a raw {line, col} position via this frame's wrapped layout.
|
// Map a screen point to a raw {line, col} position via this frame's wrapped layout.
|
||||||
// (View-side because it needs layout_ + output_origin_; feeds the selection controller.)
|
// (View-side because it needs layout_ + output_origin_; feeds the selection controller.)
|
||||||
ConsoleTextPos screenToTextPos(ImVec2 screen_pos) const;
|
ConsoleTextPos screenToTextPos(ImVec2 screen_pos) const;
|
||||||
|
|||||||
Reference in New Issue
Block a user