#include "Engine/Core/Platform/launcher_support.h" #include "Engine/Core/Log/logging.h" #include "overlay_internal.h" #include "Extension/Console/commands.h" #include "console_suggestions.h" namespace dingosdk::overlay::detail { void append_console_line(std::string text, dingosdk::ConsoleSeverity severity = dingosdk::ConsoleSeverity::info, dingosdk::ConsoleSource source = dingosdk::ConsoleSource::command, std::uint64_t elapsed_ms = UINT64_MAX, dingosdk::logging::Context context = dingosdk::logging::Context::sdk) { auto& s = state(); if (text.size() > maximum_console_line_length) { text.resize(maximum_console_line_length - 3); text += "..."; } if (elapsed_ms == UINT64_MAX) elapsed_ms = dingosdk::console_timestamp_ms(); std::size_t begin = 0; do { const auto end = text.find('\n', begin); auto part = text.substr(begin, end == std::string::npos ? end : end - begin); if (!part.empty() && part.back() == '\r') part.pop_back(); dingosdk::ConsoleLogLine entry{0, std::move(part), elapsed_ms, severity, source, context}; if (s.console_lines.size() >= maximum_console_lines) s.console_lines.pop_front(); s.console_lines.push_back({entry, dingosdk::format_console_line(entry), dingosdk::format_console_line(entry, false)}); if (end == std::string::npos) break; begin = end + 1; } while (begin < text.size()); s.console_scroll_to_bottom = true; } std::vector console_tokens(std::string_view input, int cursor) { return dingosdk::console::completion_tokens(input, cursor); } std::vector console_completion_candidates(const std::vector& tokens) { auto& s = state(); std::vector words, result; for (const auto& token : tokens) words.push_back(token.text); s.console_suggestion_rows = dingosdk::console::game_commands().complete(words, s.model); for (const auto& row : s.console_suggestion_rows) result.push_back(row.text); return result; } std::string completion_text(const std::string& candidate, bool keep_quotes) { const bool quote = keep_quotes || std::any_of(candidate.begin(), candidate.end(), [](char character) { return std::isspace(static_cast(character)) != 0 || character == '\\' || character == '"' || character == '\''; }); if (!quote) return candidate; std::string output{"\""}; for (char character : candidate) { if (character == '\\' || character == '"') output.push_back('\\'); output.push_back(character); } output.push_back('"'); return output; } void replace_console_input(ImGuiInputTextCallbackData* data, int begin, int end, const std::string& replacement) { data->DeleteChars(begin, std::max(0, end - begin)); data->InsertChars(begin, replacement.c_str()); } std::string common_console_prefix(const std::vector& candidates) { if (candidates.empty()) return {}; std::size_t length = candidates.front().size(); for (std::size_t i = 1; i < candidates.size(); ++i) { length = std::min(length, candidates[i].size()); std::size_t shared = 0; while (shared < length) { const auto left = static_cast(candidates.front()[shared]); const auto right = static_cast(candidates[i][shared]); if (std::tolower(left) != std::tolower(right)) break; ++shared; } length = shared; } return candidates.front().substr(0, length); } void reset_completion() { auto& s = state(); s.completion_candidates.clear(); s.completion_index = 0; s.completion_last_buffer.clear(); } int console_input_callback(ImGuiInputTextCallbackData* data) { auto& s = state(); s.console_cursor = data->CursorPos; if (data->EventFlag == ImGuiInputTextFlags_CallbackAlways) { if (!s.console_clicked_completion.empty()) { // Refocusing InputText can move its caret. Complete the token that // supplied the clicked row, preserving any arguments after it. const auto tokens = console_tokens(data->Buf, s.console_clicked_cursor); const auto& token = tokens.back(); const auto replacement = completion_text(s.console_clicked_completion, token.quoted); replace_console_input(data, token.begin, token.end, replacement); int end = token.begin + static_cast(replacement.size()); if (end == data->BufTextLen || !std::isspace(static_cast(data->Buf[end]))) data->InsertChars(end, " "); data->CursorPos = std::min(end + 1, data->BufTextLen); data->SelectionStart = data->SelectionEnd = data->CursorPos; s.console_clicked_completion.clear(); reset_completion(); s.console_cursor = data->CursorPos; } return 0; } if (data->EventFlag == ImGuiInputTextFlags_CallbackEdit) { reset_completion(); return 0; } if (data->EventFlag == ImGuiInputTextFlags_CallbackHistory) { reset_completion(); if (s.console_history.empty()) return 0; if (data->EventKey == ImGuiKey_UpArrow) { if (s.console_history_position < 0) { s.console_history_draft.assign(data->Buf, static_cast(data->BufTextLen)); s.console_history_position = static_cast(s.console_history.size()) - 1; } else if (s.console_history_position > 0) --s.console_history_position; } else if (data->EventKey == ImGuiKey_DownArrow && s.console_history_position >= 0) { ++s.console_history_position; if (s.console_history_position >= static_cast(s.console_history.size())) s.console_history_position = -1; } const std::string replacement = s.console_history_position < 0 ? s.console_history_draft : s.console_history[static_cast(s.console_history_position)]; replace_console_input(data, 0, data->BufTextLen, replacement); return 0; } if (data->EventFlag != ImGuiInputTextFlags_CallbackCompletion) return 0; const std::string current(data->Buf, static_cast(data->BufTextLen)); if (!s.completion_candidates.empty() && current == s.completion_last_buffer) { const auto& candidate = s.completion_candidates[s.completion_index % s.completion_candidates.size()]; const auto replacement = completion_text(candidate, false); replace_console_input(data, s.completion_begin, s.completion_end, replacement); s.completion_end = s.completion_begin + static_cast(replacement.size()); s.completion_index = (s.completion_index + 1) % s.completion_candidates.size(); s.completion_last_buffer.assign(data->Buf, static_cast(data->BufTextLen)); return 0; } reset_completion(); const auto tokens = console_tokens(current, data->CursorPos); if (tokens.empty()) return 0; auto candidates = console_completion_candidates(tokens); if (candidates.empty()) return 0; const auto& token = tokens.back(); if (candidates.size() == 1) { const auto replacement = completion_text(candidates.front(), token.quoted); replace_console_input(data, token.begin, token.end, replacement); const int end = token.begin + static_cast(replacement.size()); if (end == data->BufTextLen || !std::isspace(static_cast(data->Buf[end]))) data->InsertChars(end, " "); return 0; } const auto shared = common_console_prefix(candidates); const auto replacement = completion_text(shared, token.quoted); replace_console_input(data, token.begin, token.end, replacement); s.completion_candidates = std::move(candidates); s.completion_begin = token.begin; s.completion_end = token.begin + static_cast(replacement.size()); s.completion_last_buffer.assign(data->Buf, static_cast(data->BufTextLen)); return 0; } std::string trimmed_console_command(const char* input) { std::string command = input ? input : ""; const auto first = std::find_if_not(command.begin(), command.end(), [](char character) { return std::isspace(static_cast(character)) != 0; }); const auto last = std::find_if_not(command.rbegin(), command.rend(), [](char character) { return std::isspace(static_cast(character)) != 0; }).base(); return first < last ? std::string(first, last) : std::string{}; } void show_console_history() { const auto& history = state().console_history; if (history.empty()) { dingosdk::logging::write(dingosdk::logging::Level::info, dingosdk::logging::Channel::command, "History is empty."); return; } dingosdk::logging::write(dingosdk::logging::Level::info, dingosdk::logging::Channel::command, "History:"); for (std::size_t i = 0; i < history.size(); ++i) dingosdk::logging::write(dingosdk::logging::Level::info, dingosdk::logging::Channel::command, " " + std::to_string(i + 1) + " " + history[i]); } void submit_console_command() { auto& s = state(); const auto command = trimmed_console_command(s.console_input.data()); s.console_input.fill('\0'); s.console_cursor = 0; s.console_clicked_completion.clear(); s.console_suggestions.clear(); s.console_history_position = -1; s.console_history_draft.clear(); reset_completion(); if (command.empty()) return; if (s.console_history.empty() || s.console_history.back() != command) { if (s.console_history.size() >= 128) s.console_history.pop_front(); s.console_history.push_back(command); } const auto parsed = dingosdk::parse_console_command(command); const dingosdk::console::Output output{ [](const std::string& line) { dingosdk::logging::write(line.starts_with("error:") ? dingosdk::logging::Level::error : dingosdk::logging::Level::info, dingosdk::logging::Channel::command, line); }, [&s] { s.console_log_sequence = dingosdk::logging::latest_sequence(); s.console_lines.clear(); s.console_scroll_to_bottom = false; s.console_force_scroll = false; }, [] { show_console_history(); } }; try { const auto& registry = dingosdk::console::game_commands(); if (registry.echoes(parsed.arguments)) output("> " + command); if (!parsed || registry.local(parsed.arguments, s.model)) { registry.execute(command, s.model, output); return; } } catch (const std::exception& error) { output(std::string("error: ") + error.what()); return; } if (!s.callbacks.queue_console_command) { dingosdk::logging::write(dingosdk::logging::Level::error, dingosdk::logging::Channel::command, "Command console is not connected."); return; } std::array result{}; const bool queued = s.callbacks.queue_console_command( s.callbacks.user, command.c_str(), result.data(), result.size()); result.back() = '\0'; if (!queued) dingosdk::logging::write(dingosdk::logging::Level::error, dingosdk::logging::Channel::command, result[0] ? result.data() : "Command rejected."); } void ingest_console_log(std::vector lines) { auto& s = state(); const auto by_sequence = [](const auto& left, const auto& right) { return left.sequence < right.sequence; }; if (!std::is_sorted(lines.begin(), lines.end(), by_sequence)) std::sort(lines.begin(), lines.end(), by_sequence); for (auto& line : lines) { if (!line.sequence || line.sequence <= s.console_log_sequence) continue; if (line.sequence - s.console_log_sequence > 1) { append_console_line("[... " + std::to_string( line.sequence - s.console_log_sequence - 1) + " log lines omitted ...]"); } s.console_log_sequence = line.sequence; append_console_line(std::move(line.text), line.severity, line.source, line.elapsed_ms, line.context); } } bool console_row_matches(const ConsoleRow& row) { const auto& s = state(); const auto source = static_cast(row.entry.source); if (source < s.console_sources.size() && !s.console_sources[source]) return false; const auto context = static_cast(row.entry.context); if (context < s.console_contexts.size() && !s.console_contexts[context]) return false; if (row.entry.severity == dingosdk::ConsoleSeverity::warning) { if (!s.console_show_warnings) return false; } else if (row.entry.severity == dingosdk::ConsoleSeverity::error || row.entry.severity == dingosdk::ConsoleSeverity::critical) { if (!s.console_show_errors) return false; } else if (!s.console_show_info) return false; return s.console_filter.PassFilter(row.formatted.c_str()); } ImVec4 console_color(dingosdk::ConsoleSeverity severity, dingosdk::logging::Context context) { using Severity = dingosdk::ConsoleSeverity; using Context = dingosdk::logging::Context; if (severity == Severity::warning) return ImGui::ColorConvertU32ToFloat4(dingosdk::skate_theme::warning); if (severity == Severity::error || severity == Severity::critical) return ImGui::ColorConvertU32ToFloat4(dingosdk::skate_theme::danger); if (severity == Severity::success) return ImGui::ColorConvertU32ToFloat4(dingosdk::skate_theme::good); switch (context) { case Context::client: return {0.55f, 0.82f, 0.64f, 1.0f}; case Context::server: return {0.40f, 0.62f, 0.95f, 1.0f}; case Context::ui: return {0.75f, 0.62f, 0.88f, 1.0f}; case Context::material: return {0.86f, 0.47f, 0.79f, 1.0f}; case Context::filesystem: return {0.42f, 0.77f, 0.91f, 1.0f}; case Context::audio: return {0.94f, 0.65f, 0.36f, 1.0f}; case Context::engine: return {0.86f, 0.89f, 0.91f, 1.0f}; default: return {0.62f, 0.67f, 0.71f, 1.0f}; } } void copy_console_to_clipboard() { std::string output; for (const auto& line : state().console_lines) { if (!console_row_matches(line)) continue; output += state().console_timestamps ? line.formatted : line.plain; output.push_back('\n'); } ImGui::SetClipboardText(output.c_str()); } // A toggle tile for the console's severity filters: blue with black text when // on, like the menu's selected tab, with the matching line count. bool console_chip(ImDrawList* draw, const char* label, std::size_t count, bool& on, float k, unsigned seed) { auto& s = state(); const std::string text = std::string(label) + " " + std::to_string(count); const auto extent = s.menu.bold->CalcTextSizeA(14 * k, FLT_MAX, 0, text.c_str()); const ImVec2 size(extent.x + 28 * k, ImGui::GetFrameHeight()); const auto at = ImGui::GetCursorScreenPos(); ImGui::PushID(label); const bool pressed = ImGui::InvisibleButton("##chip", size); const bool hovered = ImGui::IsItemHovered(); ImGui::PopID(); if (pressed) on = !on; using namespace dingosdk::overlay::theme; dingosdk::skate_theme::rough_rect(draw, at, ImVec2(at.x + size.x, at.y + size.y), on ? blue : hovered ? dingosdk::skate_theme::tile_light : dingosdk::skate_theme::tile_grey, seed, k); draw->AddText(s.menu.bold, 14 * k, ImVec2(at.x + 14 * k, at.y + (size.y - extent.y) * .5f), on ? dingosdk::skate_theme::black : muted, text.c_str()); return pressed; } // The OPTIONS panel: a styled drop-down under the button, laid out like the // menu's pages instead of ImGui's default menu. void draw_console_options(ImVec2 corner, float k) { auto& s = state(); using namespace dingosdk::overlay::theme; namespace skate = dingosdk::skate_theme; const auto P = [k](float value) { return value * k; }; ImGui::SetNextWindowPos(ImVec2(corner.x, corner.y + P(6)), ImGuiCond_Always, ImVec2(1, 0)); ImGui::SetNextWindowSize(ImVec2(P(480), 0)); ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(P(18), P(16))); ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(P(8), P(7))); ImGui::PushStyleVar(ImGuiStyleVar_PopupBorderSize, 0.0f); ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(P(9), P(6))); ImGui::PushStyleColor(ImGuiCol_PopupBg, IM_COL32(18, 18, 19, 252)); if (ImGui::BeginPopup("Console options")) { auto* draw = ImGui::GetWindowDrawList(); const auto origin = ImGui::GetWindowPos(); draw->AddRectFilled(origin, ImVec2(origin.x + ImGui::GetWindowWidth(), origin.y + P(3)), blue); const auto caption = [&](const char* text) { ImGui::PushFont(s.menu.bold); ImGui::TextDisabled("%s", text); ImGui::PopFont(); }; // A caption with small All / None buttons on the right. const auto group_caption = [&](const char* text, auto&& set_all) { caption(text); const float button = P(52); ImGui::SameLine(ImGui::GetContentRegionMax().x - button * 2 - ImGui::GetStyle().ItemSpacing.x); ImGui::PushID(text); if (ImGui::Button("All", ImVec2(button, 0))) set_all(true); ImGui::SameLine(); if (ImGui::Button("None", ImVec2(button, 0))) set_all(false); ImGui::PopID(); }; const auto grid = [&](const char* id, std::size_t count, int columns, auto&& label, auto&& value) { if (!ImGui::BeginTable(id, columns, ImGuiTableFlags_SizingStretchSame)) return; for (std::size_t i = 0; i < count; ++i) { ImGui::TableNextColumn(); ImGui::PushID(static_cast(i)); ImGui::Checkbox(label(i), &value(i)); ImGui::PopID(); } ImGui::EndTable(); }; caption("ACTIONS"); const float third = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x * 2) / 3; if (ImGui::Button("Clear", ImVec2(third, 0))) { s.console_lines.clear(); s.console_scroll_to_bottom = false; s.console_force_scroll = false; } ImGui::SameLine(); if (ImGui::Button("Copy lines", ImVec2(third, 0))) copy_console_to_clipboard(); if (ImGui::IsItemHovered()) ImGui::SetTooltip("Copy the lines the filters show."); ImGui::SameLine(); if (ImGui::Button("Jump to latest", ImVec2(third, 0))) { s.console_scroll_to_bottom = true; s.console_force_scroll = true; } ImGui::Spacing(); caption("VIEW"); ImGui::Checkbox("Follow new messages", &s.console_autoscroll); ImGui::SameLine(ImGui::GetContentRegionAvail().x * .5f + ImGui::GetStyle().WindowPadding.x); ImGui::Checkbox("Show timestamps", &s.console_timestamps); ImGui::Spacing(); static constexpr std::array source_names{ "SDK", "Client", "Server", "UI", "Engine", "Files", "Materials", "Audio"}; group_caption("SOURCES", [&](bool on) { for (auto& value : s.console_contexts) value = on; }); grid("##sources", s.console_contexts.size(), 4, [&](std::size_t i) { return source_names[i]; }, [&](std::size_t i) -> bool& { return s.console_contexts[i]; }); ImGui::Spacing(); group_caption("CATEGORIES", [&](bool on) { for (auto& value : s.console_sources) value = on; }); grid("##categories", s.console_sources.size(), 3, [&](std::size_t i) { return dingosdk::console_source_name(static_cast(i)); }, [&](std::size_t i) -> bool& { return s.console_sources[i]; }); ImGui::EndPopup(); } ImGui::PopStyleColor(); ImGui::PopStyleVar(4); } void draw_console() { auto& s = state(); if (!s.console_visible.load()) { s.console_input_active = false; return; } if (ImGui::IsKeyPressed(ImGuiKey_Escape, false)) { s.console_visible.store(false); s.console_input_active = false; return; } using namespace dingosdk::overlay::theme; namespace skate = dingosdk::skate_theme; // Same scale as the ReSkate menu (Settings > Interface > menu size). const float k = std::clamp(s.model.menu_scale, dingosdk::min_menu_scale, dingosdk::max_menu_scale); const auto P = [k](float value) { return value * k; }; auto& io = ImGui::GetIO(); const auto restore_font_scale = io.FontGlobalScale; io.FontGlobalScale = k; const auto* viewport = ImGui::GetMainViewport(); const ImVec2 maximum(std::max(1.0f, viewport->WorkSize.x), std::max(1.0f, viewport->WorkSize.y)); const ImVec2 initial_size(std::min(P(1120), maximum.x - 32), std::min(P(700), maximum.y - 32)); ImGui::SetNextWindowPos(s.console_layout_valid ? s.console_position : ImVec2(viewport->WorkPos.x + 16, viewport->WorkPos.y + 16), ImGuiCond_FirstUseEver); ImGui::SetNextWindowSize(s.console_layout_valid ? s.console_size : initial_size, ImGuiCond_FirstUseEver); ImGui::SetNextWindowSizeConstraints(ImVec2(std::min(P(560), maximum.x), std::min(P(420), maximum.y)), maximum); ImGui::PushFont(s.menu.body); ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0)); ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(P(9), P(6))); ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(P(8), P(8))); ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f); ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, 0.0f); ImGui::PushStyleVar(ImGuiStyleVar_ScrollbarRounding, 0.0f); ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 0.0f); ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f); ImGui::PushStyleVar(ImGuiStyleVar_PopupRounding, 0.0f); const int colours = skate::push_widget_colours(); constexpr ImGuiWindowFlags flags = ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse; bool opened = true; if (ImGui::Begin("Console###ReSkateConsole", nullptr, flags)) { const auto size = ImGui::GetWindowSize(); const auto current = ImGui::GetWindowPos(); const ImVec2 bounded(std::clamp(current.x, viewport->WorkPos.x, viewport->WorkPos.x + std::max(0.0f, maximum.x - size.x)), std::clamp(current.y, viewport->WorkPos.y, viewport->WorkPos.y + std::max(0.0f, maximum.y - size.y))); if (bounded.x != current.x || bounded.y != current.y) ImGui::SetWindowPos(bounded); const auto origin = ImGui::GetWindowPos(); s.console_layout_valid = true; s.console_position = origin; s.console_size = size; auto* draw = ImGui::GetWindowDrawList(); // Header band: brushed, tilted title like the menu's page headers. const float header = P(72); draw->AddRectFilled(origin, ImVec2(origin.x + size.x, origin.y + header), IM_COL32(10, 10, 11, 250)); { const int start = draw->VtxBuffer.Size; const ImVec2 at(origin.x + P(20), origin.y + P(12)); draw->AddText(s.menu.title, P(40), ImVec2(at.x + P(2), at.y + P(3)), IM_COL32(0, 0, 0, 160), "CONSOLE"); draw->AddText(s.menu.title, P(40), at, paper, "CONSOLE"); const auto extent = s.menu.title->CalcTextSizeA(P(40), FLT_MAX, 0, "CONSOLE"); skate::rotate_since(draw, start, -3.0f, ImVec2(at.x + extent.x * .5f, at.y + extent.y * .5f)); draw->AddText(s.menu.body, P(14), ImVec2(at.x + extent.x + P(18), at.y + P(20)), muted, "Commands, logs and live values."); } halftone(draw, ImVec2(origin.x + size.x - P(150), origin.y + P(18)), 10, 6, IM_COL32(61, 63, 64, 140)); ImGui::SetCursorPos(ImVec2(size.x - P(44), P(16))); if (ImGui::Button("X", ImVec2(P(28), P(28)))) opened = false; ImGui::SetCursorPos(ImVec2(P(16), header + P(12))); ImGui::BeginChild("##console-body", ImVec2(size.x - P(32), size.y - header - P(24)), ImGuiChildFlags_None, ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse); auto* body = ImGui::GetWindowDrawList(); // Filter row: severity tiles, the search box and the options menu. std::size_t infos = 0, warnings = 0, errors = 0; for (const auto& line : s.console_lines) { using Severity = dingosdk::ConsoleSeverity; if (line.entry.severity == Severity::warning) ++warnings; else if (line.entry.severity == Severity::error || line.entry.severity == Severity::critical) ++errors; else ++infos; } console_chip(body, "MESSAGES", infos, s.console_show_info, k, 21); ImGui::SameLine(0, P(6)); console_chip(body, "WARNINGS", warnings, s.console_show_warnings, k, 22); ImGui::SameLine(0, P(6)); console_chip(body, "ERRORS", errors, s.console_show_errors, k, 23); ImGui::SameLine(0, P(12)); const float options_width = s.menu.bold->CalcTextSizeA(P(15), FLT_MAX, 0, "OPTIONS").x + P(24); s.console_filter.Draw("##Console filter", ImGui::GetContentRegionAvail().x - options_width - P(8)); if (!s.console_filter.IsActive() && !ImGui::IsItemActive()) { const auto at = ImGui::GetItemRectMin(); body->AddText(s.menu.body, P(16), ImVec2(at.x + P(9), at.y + P(6)), muted, "Filter messages..."); } if (ImGui::IsItemHovered()) ImGui::SetTooltip("Search messages or sources. Use -text to exclude matches."); ImGui::SameLine(0, P(8)); ImGui::PushFont(s.menu.bold); if (ImGui::Button("OPTIONS", ImVec2(options_width, 0))) ImGui::OpenPopup("Console options"); ImGui::PopFont(); const auto options_corner = ImGui::GetItemRectMax(); draw_console_options(options_corner, k); ImGui::PushFont(s.menu.mono); s.console_suggestions = s.console_input[0] ? console_completion_candidates(console_tokens(s.console_input.data(), s.console_cursor)) : std::vector{}; // Keep rows visible while they take focus on mouse-down; Selectable // accepts the click on mouse-up in a later frame. const bool suggestions_open = !s.console_suggestions.empty(); const float input_height = ImGui::GetFrameHeightWithSpacing(); const float footer_height = P(28); const float divider_height = P(6); const float spacing = ImGui::GetStyle().ItemSpacing.y; const float divider_space = suggestions_open ? divider_height + spacing : 0; const float panels_height = std::max(1.0f, ImGui::GetContentRegionAvail().y - input_height - footer_height - divider_space - 2 * spacing); const ImVec2 panel_padding(P(12), P(10)); const float content_height = ImGui::GetTextLineHeightWithSpacing() * static_cast(s.console_suggestions.size()) + 2 * panel_padding.y + ImGui::GetStyle().ScrollbarSize; const float suggestions_height = suggestions_open ? std::min(content_height, std::clamp(panels_height * s.console_suggestions_fraction, std::min(P(80), panels_height), std::max(P(80), panels_height - P(48)))) : 0; const float scrollback_height = std::max(1.0f, panels_height - suggestions_height); std::vector filtered; for (std::size_t i = 0; i < s.console_lines.size(); ++i) if (console_row_matches(s.console_lines[i])) filtered.push_back(i); // Scrollback on a rough-cut tile, like the menu's panels. { const auto at = ImGui::GetCursorScreenPos(); skate::rough_rect(body, at, ImVec2(at.x + ImGui::GetContentRegionAvail().x, at.y + scrollback_height), skate::tile, 31, k); } ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, panel_padding); ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(P(4), P(3))); if (ImGui::BeginChild("Console scrollback", ImVec2(0, scrollback_height), ImGuiChildFlags_AlwaysUseWindowPadding, ImGuiWindowFlags_HorizontalScrollbar)) { ImGuiListClipper clipper; clipper.Begin(static_cast(filtered.size())); while (clipper.Step()) { for (int row = clipper.DisplayStart; row < clipper.DisplayEnd; ++row) { const auto& line = s.console_lines[filtered[static_cast(row)]]; ImGui::PushStyleColor(ImGuiCol_Text, console_color(line.entry.severity, line.entry.context)); ImGui::TextUnformatted((s.console_timestamps ? line.formatted : line.plain).c_str()); ImGui::PopStyleColor(); } } if (filtered.empty()) ImGui::TextDisabled(s.console_lines.empty() ? "Nothing logged yet." : "No lines match the filters."); if (s.console_scroll_to_bottom && (s.console_force_scroll || (s.console_autoscroll && s.console_was_at_bottom))) ImGui::SetScrollHereY(1.0f); s.console_scroll_to_bottom = false; s.console_force_scroll = false; s.console_was_at_bottom = ImGui::GetScrollY() >= ImGui::GetScrollMaxY() - 1.0f; } ImGui::EndChild(); ImGui::PopStyleVar(2); if (suggestions_open) { const auto at = ImGui::GetCursorScreenPos(); ImGui::InvisibleButton("Resize command list", ImVec2(-1, divider_height)); const bool dragging = ImGui::IsItemActive(); const bool hovered = ImGui::IsItemHovered(); if (hovered || dragging) ImGui::SetMouseCursor(ImGuiMouseCursor_ResizeNS); if (dragging && panels_height > 0) { s.console_suggestions_fraction = std::clamp(s.console_suggestions_fraction - ImGui::GetIO().MouseDelta.y / panels_height, 0.2f, 0.9f); } if (hovered && !dragging) ImGui::SetTooltip("Drag to resize the command list"); const float middle = at.y + divider_height * .5f; body->AddRectFilled(ImVec2(at.x, middle - P(1)), ImVec2(at.x + ImGui::GetItemRectSize().x, middle + P(1)), hovered || dragging ? blue : skate::tile_light); if (s.console_resizing_suggestions && !dragging) s.console_focus_requested.store(true); s.console_resizing_suggestions = dragging; } // Command line: blue prompt mark, the input, and a primary RUN button. if (s.console_focus_requested.exchange(false)) ImGui::SetKeyboardFocusHere(); constexpr ImGuiInputTextFlags input_flags = ImGuiInputTextFlags_EnterReturnsTrue | ImGuiInputTextFlags_CallbackCompletion | ImGuiInputTextFlags_CallbackHistory | ImGuiInputTextFlags_CallbackAlways | ImGuiInputTextFlags_CallbackEdit; const float submit_width = P(84); const float command_height = ImGui::GetFrameHeight(); { const auto at = ImGui::GetCursorScreenPos(); body->AddRectFilled(at, ImVec2(at.x + P(4), at.y + command_height), blue); ImGui::SetCursorScreenPos(ImVec2(at.x + P(4), at.y)); } ImGui::SetNextItemWidth(-submit_width - ImGui::GetStyle().ItemSpacing.x); bool submitted = ImGui::InputTextWithHint("##Console input", "Enter a command...", s.console_input.data(), s.console_input.size(), input_flags, console_input_callback, nullptr); s.console_input_active = ImGui::IsItemActive(); ImGui::SameLine(); ImGui::PushFont(s.menu.bold); skate::push_primary_button(); submitted |= ImGui::Button("RUN", ImVec2(submit_width, command_height)); skate::pop_primary_button(); ImGui::PopFont(); if (submitted) { submit_console_command(); s.console_focus_requested.store(true); } if (suggestions_open && suggestions_height > 0 && !submitted) { const auto at = ImGui::GetCursorScreenPos(); const float width = ImGui::GetContentRegionAvail().x; skate::rough_rect(body, at, ImVec2(at.x + width, at.y + suggestions_height), skate::tile, 32, k); body->AddRectFilled(at, ImVec2(at.x + P(4), at.y + suggestions_height), blue); ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, panel_padding); if (ImGui::BeginChild("Console suggestions", ImVec2(0, suggestions_height), ImGuiChildFlags_AlwaysUseWindowPadding, ImGuiWindowFlags_HorizontalScrollbar)) { for (const auto& row : s.console_suggestion_rows) { if (draw_console_suggestion(row, paper, muted)) { s.console_clicked_completion = row.text; s.console_clicked_cursor = s.console_cursor; s.console_focus_requested.store(true); } } } ImGui::EndChild(); ImGui::PopStyleVar(); } ImGui::PopFont(); // Footer: keycap hints like the menu's, and the line count. { const auto at = ImGui::GetCursorScreenPos(); const float y = at.y + std::max(0.0f, ImGui::GetContentRegionAvail().y - P(22)); static const auto close_key = dingosdk::launcher::key_cap(dingosdk::launcher::overlay_keys().console); float x = at.x; const float width = ImGui::GetContentRegionAvail().x; if (width >= P(620)) { x += keycap(body, s.menu.bold, k, ImVec2(x, y), "TAB", "Complete") + P(18); x += keycap(body, s.menu.bold, k, ImVec2(x, y), "\xE2\x86\x91\xE2\x86\x93", "History") + P(18); } keycap(body, s.menu.bold, k, ImVec2(x, y), close_key.c_str(), "or"); x += s.menu.bold->CalcTextSizeA(P(12), FLT_MAX, 0, close_key.c_str()).x + s.menu.bold->CalcTextSizeA(P(14), FLT_MAX, 0, "or").x + P(28); keycap(body, s.menu.bold, k, ImVec2(x, y), "ESC", "Close"); const auto count = std::to_string(filtered.size()) + " / " + std::to_string(s.console_lines.size()) + " lines"; const auto extent = s.menu.body->CalcTextSizeA(P(14), FLT_MAX, 0, count.c_str()); body->AddText(s.menu.body, P(14), ImVec2(at.x + width - extent.x, y + P(2)), muted, count.c_str()); } ImGui::EndChild(); } ImGui::End(); ImGui::PopStyleColor(colours); ImGui::PopStyleVar(9); ImGui::PopFont(); io.FontGlobalScale = restore_font_scale; if (!opened) { s.console_visible.store(false); s.console_input_active = false; } } }