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();
|
||||
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,
|
||||
s_errors_only_enabled, s_rpc_trace_enabled,
|
||||
s_app_messages_enabled};
|
||||
bool has_text_filter = !outputFilter.text.empty();
|
||||
hasTextFilter = !outputFilter.text.empty();
|
||||
// Lowercased needle for in-line match highlighting.
|
||||
std::string filter_lower;
|
||||
if (has_text_filter) {
|
||||
filter_lower = std::string(filter_text_);
|
||||
std::transform(filter_lower.begin(), filter_lower.end(), filter_lower.begin(),
|
||||
if (hasTextFilter) {
|
||||
filterLower = std::string(filter_text_);
|
||||
std::transform(filterLower.begin(), filterLower.end(), filterLower.begin(),
|
||||
[](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.errorsOnly;
|
||||
visible_indices_.clear();
|
||||
@@ -622,120 +698,88 @@ void ConsoleTab::renderOutput()
|
||||
}
|
||||
} else {
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
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.
|
||||
// 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
|
||||
filter_match_count_ = hasTextFilter ? static_cast<int>(visible_indices_.size()) : 0;
|
||||
}
|
||||
|
||||
void ConsoleTab::handleOutputInteraction(ImVec2 mousePos, bool mouseInOutput)
|
||||
{
|
||||
ImGuiIO& io = ImGui::GetIO();
|
||||
ImVec2 mouse_pos = io.MousePos;
|
||||
|
||||
// Manual hit test: is mouse within this child window?
|
||||
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));
|
||||
|
||||
// Disable auto-scroll when user scrolls up (wheel scroll)
|
||||
if (mouse_in_output && io.MouseWheel > 0.0f) {
|
||||
|
||||
// Disable auto-scroll when the user scrolls up (wheel). Scrolling back down to the very
|
||||
// bottom re-enables it; that position check happens after EndChild() in render(), and is
|
||||
// skipped on the wheel-up frame (the scroll position hasn't caught up yet).
|
||||
if (mouseInOutput && io.MouseWheel > 0.0f) {
|
||||
scroll_.onUserScrolledUp();
|
||||
}
|
||||
// Scrolling down to the very bottom re-enables auto-scroll.
|
||||
// Actual position check happens after EndChild() using captured
|
||||
// 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) {
|
||||
// Text-selection cursor while hovering.
|
||||
if (mouseInOutput) {
|
||||
ImGui::SetMouseCursor(ImGuiMouseCursor_TextInput);
|
||||
}
|
||||
|
||||
// Mouse press - start selection (use raw IO mouse state)
|
||||
if (mouse_in_output && io.MouseClicked[0]) {
|
||||
selection_.beginDrag(screenToTextPos(mouse_pos));
|
||||
// Selection drag lifecycle (continues even if the mouse leaves the window).
|
||||
if (mouseInOutput && io.MouseClicked[0]) {
|
||||
selection_.beginDrag(screenToTextPos(mousePos));
|
||||
}
|
||||
|
||||
// Mouse drag - extend selection (continue even if mouse leaves the window)
|
||||
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]) {
|
||||
selection_.endDrag(screenToTextPos(mouse_pos));
|
||||
selection_.endDrag(screenToTextPos(mousePos));
|
||||
}
|
||||
|
||||
// Ctrl+C / Ctrl+A
|
||||
if (mouse_in_output || selection_.active()) {
|
||||
if (mouseInOutput || selection_.active()) {
|
||||
if (io.KeyCtrl && ImGui::IsKeyPressed(ImGuiKey_C)) {
|
||||
std::string selected = selectedText();
|
||||
if (!selected.empty()) {
|
||||
ImGui::SetClipboardText(selected.c_str());
|
||||
}
|
||||
if (!selected.empty()) ImGui::SetClipboardText(selected.c_str());
|
||||
}
|
||||
if (io.KeyCtrl && ImGui::IsKeyPressed(ImGuiKey_A) && !model_.empty()) {
|
||||
selection_.selectAll(static_cast<int>(model_.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_end_pos = selection_.rangeEnd();
|
||||
|
||||
// Render lines with selection highlighting.
|
||||
// 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.
|
||||
|
||||
// Cull to the visible viewport rows for cheap scrolling over large buffers.
|
||||
float scroll_y = ImGui::GetScrollY();
|
||||
float window_height = ImGui::GetWindowHeight();
|
||||
float visible_top = scroll_y;
|
||||
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;
|
||||
if (!layout_.cumulativeY.empty()) {
|
||||
int lo = 0, hi = static_cast<int>(layout_.cumulativeY.size()) - 1;
|
||||
while (lo < hi) {
|
||||
int mid = (lo + hi) / 2;
|
||||
float line_bottom = layout_.cumulativeY[mid] + layout_.heights[mid];
|
||||
if (line_bottom < visible_top) {
|
||||
lo = mid + 1;
|
||||
} else {
|
||||
hi = mid;
|
||||
}
|
||||
if (line_bottom < visible_top) lo = mid + 1;
|
||||
else hi = mid;
|
||||
}
|
||||
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())) {
|
||||
ImGui::Dummy(ImVec2(0, layout_.cumulativeY[first_visible]));
|
||||
}
|
||||
|
||||
|
||||
ImDrawList* dl = ImGui::GetWindowDrawList();
|
||||
ImU32 selColor = WithAlpha(Secondary(), 80);
|
||||
|
||||
// Render visible lines
|
||||
|
||||
int last_rendered_vi = first_visible - 1;
|
||||
for (int vi = first_visible; vi < visible_count; vi++) {
|
||||
if (vi < static_cast<int>(layout_.cumulativeY.size()) &&
|
||||
@@ -743,7 +787,7 @@ void ConsoleTab::renderOutput()
|
||||
break;
|
||||
}
|
||||
last_rendered_vi = vi;
|
||||
|
||||
|
||||
int i = visible_indices_[vi];
|
||||
const auto& line = model_[i];
|
||||
const auto& segs = layout_.segments[vi];
|
||||
@@ -799,23 +843,23 @@ void ConsoleTab::renderOutput()
|
||||
selByteEnd = (i == sel_end_pos.line) ? sel_end_pos.col
|
||||
: static_cast<int>(line.text.size());
|
||||
}
|
||||
|
||||
|
||||
for (const auto& seg : segs) {
|
||||
float rowY = lineOrigin.y + seg.yOffset;
|
||||
const char* segStart = line.text.c_str() + seg.byteStart;
|
||||
const char* segEnd = line.text.c_str() + seg.byteEnd;
|
||||
|
||||
if (s_scanline_enabled && line_height > 0.0f) {
|
||||
int rowIndex = static_cast<int>(std::floor((layout_.cumulativeY[vi] + seg.yOffset) / line_height + 0.5f));
|
||||
if (s_scanline_enabled && lineHeight > 0.0f) {
|
||||
int rowIndex = static_cast<int>(std::floor((layout_.cumulativeY[vi] + seg.yOffset) / lineHeight + 0.5f));
|
||||
scanline_rows_.push_back({rowY, rowY + seg.height, rowIndex});
|
||||
}
|
||||
|
||||
|
||||
// Selection highlight for this sub-row
|
||||
if (lineSelected && selByteStart < seg.byteEnd && selByteEnd > seg.byteStart) {
|
||||
int hlStart = std::max(selByteStart, seg.byteStart) - seg.byteStart;
|
||||
int hlEnd = std::min(selByteEnd, seg.byteEnd) - seg.byteStart;
|
||||
int segLen = seg.byteEnd - seg.byteStart;
|
||||
|
||||
|
||||
float xStart = 0.0f;
|
||||
if (hlStart > 0) {
|
||||
xStart = font->CalcTextSizeA(fontSize, FLT_MAX, 0,
|
||||
@@ -827,27 +871,27 @@ void ConsoleTab::renderOutput()
|
||||
if (hlEnd >= segLen && selByteEnd >= static_cast<int>(line.text.size())) {
|
||||
xEnd = std::max(xEnd + 8.0f, ImGui::GetWindowWidth());
|
||||
}
|
||||
|
||||
|
||||
dl->AddRectFilled(
|
||||
ImVec2(lineOrigin.x + xStart, rowY),
|
||||
ImVec2(lineOrigin.x + xEnd, rowY + seg.height),
|
||||
selColor);
|
||||
}
|
||||
|
||||
|
||||
// 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::transform(segLower.begin(), segLower.end(), segLower.begin(),
|
||||
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
|
||||
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 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),
|
||||
ImVec2(lineOrigin.x + xe, rowY + seg.height),
|
||||
IM_COL32(255, 210, 0, 70));
|
||||
pos += filter_lower.size();
|
||||
pos += filterLower.size();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -858,11 +902,11 @@ void ConsoleTab::renderOutput()
|
||||
channelTextColor(line.channel), segStart, segEnd);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Advance ImGui cursor by the total wrapped height of this line
|
||||
ImGui::Dummy(ImVec2(0, totalH));
|
||||
}
|
||||
|
||||
|
||||
// Add spacer for lines after last visible (to maintain correct content height)
|
||||
if (last_rendered_vi >= 0 && last_rendered_vi < visible_count - 1) {
|
||||
float rendered_height = (last_rendered_vi < static_cast<int>(layout_.cumulativeY.size()))
|
||||
@@ -873,118 +917,85 @@ void ConsoleTab::renderOutput()
|
||||
ImGui::Dummy(ImVec2(0, remaining_height));
|
||||
}
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
void ConsoleTab::drawOutputContextMenu()
|
||||
{
|
||||
if (!ImGui::BeginPopupContextWindow("ConsoleContextMenu")) return;
|
||||
|
||||
// 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()) {
|
||||
std::string shortTok = context_token_.size() > 22
|
||||
? context_token_.substr(0, 12) + "\xE2\x80\xA6" + context_token_.substr(context_token_.size() - 6)
|
||||
: context_token_;
|
||||
std::string label = std::string(TR("console_copy_value")) + " \"" + shortTok + "\"";
|
||||
if (ImGui::MenuItem(label.c_str())) {
|
||||
ImGui::SetClipboardText(context_token_.c_str());
|
||||
}
|
||||
ImGui::Separator();
|
||||
}
|
||||
if (ImGui::MenuItem(TR("copy"), "Ctrl+C", false, selection_.active())) {
|
||||
std::string selected = selectedText();
|
||||
if (!selected.empty()) {
|
||||
ImGui::SetClipboardText(selected.c_str());
|
||||
}
|
||||
}
|
||||
if (ImGui::MenuItem(TR("console_select_all"), "Ctrl+A")) {
|
||||
if (!model_.empty()) {
|
||||
selection_.selectAll(static_cast<int>(model_.size()),
|
||||
static_cast<int>(model_.back().text.size()));
|
||||
}
|
||||
if (!context_token_.empty()) {
|
||||
std::string shortTok = context_token_.size() > 22
|
||||
? context_token_.substr(0, 12) + "\xE2\x80\xA6" + context_token_.substr(context_token_.size() - 6)
|
||||
: context_token_;
|
||||
std::string label = std::string(TR("console_copy_value")) + " \"" + shortTok + "\"";
|
||||
if (ImGui::MenuItem(label.c_str())) {
|
||||
ImGui::SetClipboardText(context_token_.c_str());
|
||||
}
|
||||
ImGui::Separator();
|
||||
if (ImGui::MenuItem(TR("console_clear_console"))) {
|
||||
// View-only clear (main thread) — drop the visible lines and the selection.
|
||||
model_.clear();
|
||||
selection_.clear();
|
||||
}
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
|
||||
// "New output" indicator when user is scrolled up and new lines arrived
|
||||
if (!scroll_.autoScroll() && scroll_.newLines() > 0) {
|
||||
float indicW = 140.0f;
|
||||
float indicH = 24.0f;
|
||||
ImDrawList* dlInd = ImGui::GetWindowDrawList();
|
||||
ImVec2 wMin = ImGui::GetWindowPos();
|
||||
ImVec2 wSize = ImGui::GetWindowSize();
|
||||
float ix = wMin.x + (wSize.x - indicW) * 0.5f;
|
||||
float iy = wMin.y + wSize.y - indicH - 8.0f;
|
||||
ImVec2 iMin(ix, iy);
|
||||
ImVec2 iMax(ix + indicW, iy + indicH);
|
||||
|
||||
dlInd->AddRectFilled(iMin, iMax, IM_COL32(40, 40, 40, 220), 12.0f);
|
||||
dlInd->AddRect(iMin, iMax, IM_COL32(255, 218, 0, 120), 12.0f);
|
||||
|
||||
char buf[48];
|
||||
snprintf(buf, sizeof(buf), TR("console_new_lines"), scroll_.newLines());
|
||||
ImFont* capFont = Type().caption();
|
||||
if (!capFont) capFont = ImGui::GetFont();
|
||||
ImFont* icoFont = Type().iconSmall();
|
||||
if (!icoFont) icoFont = capFont;
|
||||
|
||||
// Measure icon + text to center them together
|
||||
ImVec2 icoSz = icoFont->CalcTextSizeA(icoFont->LegacySize, FLT_MAX, 0, ICON_MD_ARROW_DOWNWARD);
|
||||
ImVec2 txtSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, buf);
|
||||
float totalW = icoSz.x + txtSz.x;
|
||||
float startX = ix + (indicW - totalW) * 0.5f;
|
||||
float icoY = iy + (indicH - icoSz.y) * 0.5f;
|
||||
float txtY = iy + (indicH - txtSz.y) * 0.5f;
|
||||
ImU32 col = IM_COL32(255, 218, 0, 255);
|
||||
dlInd->AddText(icoFont, icoFont->LegacySize, ImVec2(startX, icoY), col, ICON_MD_ARROW_DOWNWARD);
|
||||
dlInd->AddText(capFont, capFont->LegacySize, ImVec2(startX + icoSz.x, txtY), col, buf);
|
||||
|
||||
// Click to jump to bottom
|
||||
ImGui::SetCursorScreenPos(iMin);
|
||||
if (ImGui::InvisibleButton("##scrollToBottom", ImVec2(indicW, indicH))) {
|
||||
scroll_.jumpToBottom();
|
||||
if (ImGui::MenuItem(TR("copy"), "Ctrl+C", false, selection_.active())) {
|
||||
std::string selected = selectedText();
|
||||
if (!selected.empty()) {
|
||||
ImGui::SetClipboardText(selected.c_str());
|
||||
}
|
||||
}
|
||||
if (ImGui::MenuItem(TR("console_select_all"), "Ctrl+A")) {
|
||||
if (!model_.empty()) {
|
||||
selection_.selectAll(static_cast<int>(model_.size()),
|
||||
static_cast<int>(model_.back().text.size()));
|
||||
}
|
||||
}
|
||||
ImGui::Separator();
|
||||
if (ImGui::MenuItem(TR("console_clear_console"))) {
|
||||
// View-only clear (main thread) — drop the visible lines and the selection.
|
||||
model_.clear();
|
||||
selection_.clear();
|
||||
}
|
||||
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;
|
||||
|
||||
float indicW = 140.0f;
|
||||
float indicH = 24.0f;
|
||||
ImDrawList* dlInd = ImGui::GetWindowDrawList();
|
||||
ImVec2 wMin = ImGui::GetWindowPos();
|
||||
ImVec2 wSize = ImGui::GetWindowSize();
|
||||
float ix = wMin.x + (wSize.x - indicW) * 0.5f;
|
||||
float iy = wMin.y + wSize.y - indicH - 8.0f;
|
||||
ImVec2 iMin(ix, iy);
|
||||
ImVec2 iMax(ix + indicW, iy + indicH);
|
||||
|
||||
dlInd->AddRectFilled(iMin, iMax, IM_COL32(40, 40, 40, 220), 12.0f);
|
||||
dlInd->AddRect(iMin, iMax, IM_COL32(255, 218, 0, 120), 12.0f);
|
||||
|
||||
char buf[48];
|
||||
snprintf(buf, sizeof(buf), TR("console_new_lines"), scroll_.newLines());
|
||||
ImFont* capFont = Type().caption();
|
||||
if (!capFont) capFont = ImGui::GetFont();
|
||||
ImFont* icoFont = Type().iconSmall();
|
||||
if (!icoFont) icoFont = capFont;
|
||||
|
||||
// Measure icon + text to center them together
|
||||
ImVec2 icoSz = icoFont->CalcTextSizeA(icoFont->LegacySize, FLT_MAX, 0, ICON_MD_ARROW_DOWNWARD);
|
||||
ImVec2 txtSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, buf);
|
||||
float totalW = icoSz.x + txtSz.x;
|
||||
float startX = ix + (indicW - totalW) * 0.5f;
|
||||
float icoY = iy + (indicH - icoSz.y) * 0.5f;
|
||||
float txtY = iy + (indicH - txtSz.y) * 0.5f;
|
||||
ImU32 col = IM_COL32(255, 218, 0, 255);
|
||||
dlInd->AddText(icoFont, icoFont->LegacySize, ImVec2(startX, icoY), col, ICON_MD_ARROW_DOWNWARD);
|
||||
dlInd->AddText(capFont, capFont->LegacySize, ImVec2(startX + icoSz.x, txtY), col, buf);
|
||||
|
||||
// Click to jump to bottom
|
||||
ImGui::SetCursorScreenPos(iMin);
|
||||
if (ImGui::InvisibleButton("##scrollToBottom", ImVec2(indicW, indicH))) {
|
||||
scroll_.jumpToBottom();
|
||||
}
|
||||
}
|
||||
|
||||
ConsoleTextPos ConsoleTab::screenToTextPos(ImVec2 screen_pos) const
|
||||
|
||||
@@ -101,9 +101,17 @@ private:
|
||||
void addFormattedResult(const std::string& result, bool is_error);
|
||||
void renderStatusHeader(ConsoleCommandExecutor& exec);
|
||||
void renderToolbar(ConsoleCommandExecutor& exec);
|
||||
void renderOutput();
|
||||
void renderInput(ConsoleCommandExecutor& exec);
|
||||
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.
|
||||
// (View-side because it needs layout_ + output_origin_; feeds the selection controller.)
|
||||
|
||||
Reference in New Issue
Block a user