From fa7ad6b7952760b8896bb74563be24e0bd6fd50e Mon Sep 17 00:00:00 2001 From: Gleb Mazovetskiy Date: Tue, 31 Oct 2023 14:47:34 +0000 Subject: [PATCH] Add a basic Quake-style console Enabled only in Debug mode. Runs Lua similar to the `lua` CLI. Supports multiline input with Shift+Enter. Missing features: 1. Scrollback. 2. Input history on up/down. Open with backtick, close with Esc. --- CMake/Assets.cmake | 2 + .../resources/assets/fonts/gamedialogred.trn | Bin 0 -> 256 bytes .../assets/fonts/gamedialogwhite.trn | Bin 0 -> 256 bytes Source/CMakeLists.txt | 2 + Source/DiabloUI/ui_flags.hpp | 45 +-- Source/debug.cpp | 10 - Source/diablo.cpp | 13 + Source/engine/render/scrollrt.cpp | 5 + Source/engine/render/text_render.cpp | 61 ++-- Source/engine/render/text_render.hpp | 22 +- Source/lua/lua.cpp | 15 +- Source/lua/lua.hpp | 6 +- Source/lua/repl.cpp | 59 ++++ Source/lua/repl.hpp | 14 + Source/panels/console.cpp | 267 ++++++++++++++++++ Source/panels/console.hpp | 15 + Source/utils/str_split.hpp | 14 +- 17 files changed, 462 insertions(+), 88 deletions(-) create mode 100644 Packaging/resources/assets/fonts/gamedialogred.trn create mode 100644 Packaging/resources/assets/fonts/gamedialogwhite.trn create mode 100644 Source/lua/repl.cpp create mode 100644 Source/lua/repl.hpp create mode 100644 Source/panels/console.cpp create mode 100644 Source/panels/console.hpp diff --git a/CMake/Assets.cmake b/CMake/Assets.cmake index af0cf737ea36..b8ff81a6f833 100644 --- a/CMake/Assets.cmake +++ b/CMake/Assets.cmake @@ -114,6 +114,8 @@ set(devilutionx_assets fonts/blue.trn fonts/buttonface.trn fonts/buttonpushed.trn + fonts/gamedialogwhite.trn + fonts/gamedialogred.trn fonts/golduis.trn fonts/goldui.trn fonts/grayuis.trn diff --git a/Packaging/resources/assets/fonts/gamedialogred.trn b/Packaging/resources/assets/fonts/gamedialogred.trn new file mode 100644 index 0000000000000000000000000000000000000000..5676b4e78c8146c38357e0235f4eb94671f944a3 GIT binary patch literal 256 zcmV+b0ssC00RjUA1qKHQ2?`4g4Gs?w5fT#=6&4p585$cL9UdPbAtECrB_<~*DJm;0 zEiNxGF)}kWH8wXmIXXK$Jw87`K|(`BMMg(RNlHshO-@fxQBqS>RaRG6Sz23MU0z>c zVPa!sWoBn+X=-b1ZEkOHadLBXb#`}nd3t+%eSUv{fr5jCg@%WSiHeJijgF6yk&=^? zm6n&7nVOrNot~edp`xRtrKYE-sj922t*)=Iv9hzYwYImoxw^Z&y}rM|!NSAD#m2|T z$;!*j&Cbuz(bCh@)z;V8+1lIO-QM4f;o{@u<>u$;>FVq3?e6dJ@$&QZ_4fDp`TG0( G{r>->c7H7Z literal 0 HcmV?d00001 diff --git a/Packaging/resources/assets/fonts/gamedialogwhite.trn b/Packaging/resources/assets/fonts/gamedialogwhite.trn new file mode 100644 index 0000000000000000000000000000000000000000..3e0f42ed41075e25116d551608c5fad16b58e7a1 GIT binary patch literal 256 zcmV+b0ssC00RjUA1qKHQ2?`4g4Gs?w5fT#=6&4p585$cL9UdPbAtECrB_<~*DJm;0 zEiNxGF)}kWH8wXmIXXK$Jw87`K|(`BMMg(RNlHshO-@fxQBqS>RaRG6Sz23MU0z>c zVPa!sWoBn+X=-b1ZEkOHadLBXb#`}nd3t+%eSUv{fr5jCg@%WSiHeJijgF6yk&=^? zm6n&7nVOrNot~edp`xRtrKYE-sj922t*)=Iv9hzYwYImoxw^Z&y}rM|!NSAD#m2|T z$;!*j&Cbuz(bCh@)z;V8+1lIO-QM5-;o{@u<>u$;>FVq3?e6dJ@$&QZ_4fDp`TG0( G{r>;4)_ result = RunLua(code); - if (!result.has_value()) { - return StrCat("> ", code, "\n") + std::move(result).error(); - } - return StrCat("> ", code, "\n") + std::move(result).value(); -} - std::string DebugCmdGiveGoldCheat(const std::string_view parameter) { Player &myPlayer = *MyPlayer; @@ -1285,7 +1276,6 @@ std::vector DebugCmdList = { { "searchitem", "Searches the automap for {item}", "{item}", &DebugCmdSearchItem }, { "searchobject", "Searches the automap for {object}", "{object}", &DebugCmdSearchObject }, { "clearsearch", "Search in the auto map is cleared", "", &DebugCmdClearSearch }, - { "lua", "Run Lua code", "{code}", &DebugCmdLua }, }; } // namespace diff --git a/Source/diablo.cpp b/Source/diablo.cpp index f3578f6f2242..f5a2947bda9a 100644 --- a/Source/diablo.cpp +++ b/Source/diablo.cpp @@ -61,6 +61,7 @@ #include "nthread.h" #include "objects.h" #include "options.h" +#include "panels/console.hpp" #include "panels/info_box.hpp" #include "panels/spell_book.hpp" #include "panels/spell_list.hpp" @@ -706,6 +707,12 @@ void GameEventHandler(const SDL_Event &event, uint16_t modState) return; } +#ifdef _DEBUG + if (ConsoleHandleEvent(event)) { + return; + } +#endif + if (IsTalkActive() && HandleTalkTextInputEvent(event)) { return; } @@ -1864,6 +1871,12 @@ void InitKeymapActions() ToggleChatLog(); }); #ifdef _DEBUG + sgOptions.Keymapper.AddAction( + "OpenConsole", + N_("Console"), + N_("Opens Lua console."), + '`', + OpenConsole); sgOptions.Keymapper.AddAction( "DebugToggle", "Debug toggle", diff --git a/Source/engine/render/scrollrt.cpp b/Source/engine/render/scrollrt.cpp index 2f720b93f3bb..4dc1ddec9278 100644 --- a/Source/engine/render/scrollrt.cpp +++ b/Source/engine/render/scrollrt.cpp @@ -34,6 +34,7 @@ #include "nthread.h" #include "options.h" #include "panels/charpanel.hpp" +#include "panels/console.hpp" #include "plrmsg.h" #include "qol/chatlog.h" #include "qol/floatingnumbers.h" @@ -1660,6 +1661,10 @@ void DrawAndBlit() DrawMain(out, hgt, drawInfoBox, drawHealth, drawMana, drawBelt, drawControlButtons); +#ifdef _DEBUG + DrawConsole(out); +#endif + RedrawComplete(); for (PanelDrawComponent component : enum_values()) { if (IsRedrawComponent(component)) { diff --git a/Source/engine/render/text_render.cpp b/Source/engine/render/text_render.cpp index d8482ed79495..d810a2ba3996 100644 --- a/Source/engine/render/text_render.cpp +++ b/Source/engine/render/text_render.cpp @@ -46,13 +46,14 @@ constexpr std::array LineHeights = { 12, 26, 38, 42, 50, 22 }; constexpr int SmallFontTallLineHeight = 16; std::array BaseLineOffset = { -3, -2, -3, -6, -7, 3 }; -std::array ColorTranslations = { +std::array ColorTranslations = { "fonts\\goldui.trn", "fonts\\grayui.trn", "fonts\\golduis.trn", "fonts\\grayuis.trn", - nullptr, + nullptr, // ColorDialogWhite + nullptr, // ColorDialogRed "fonts\\yellow.trn", nullptr, @@ -66,55 +67,43 @@ std::array ColorTranslations = { "fonts\\buttonface.trn", "fonts\\buttonpushed.trn", + "fonts\\gamedialogwhite.trn", + "fonts\\gamedialogred.trn", }; -std::array>, 15> ColorTranslationsData; - -GameFontTables GetSizeFromFlags(UiFlags flags) -{ - if (HasAnyOf(flags, UiFlags::FontSize24)) - return GameFont24; - else if (HasAnyOf(flags, UiFlags::FontSize30)) - return GameFont30; - else if (HasAnyOf(flags, UiFlags::FontSize42)) - return GameFont42; - else if (HasAnyOf(flags, UiFlags::FontSize46)) - return GameFont46; - else if (HasAnyOf(flags, UiFlags::FontSizeDialog)) - return FontSizeDialog; - - return GameFont12; -} +std::array>, 18> ColorTranslationsData; text_color GetColorFromFlags(UiFlags flags) { if (HasAnyOf(flags, UiFlags::ColorWhite)) return ColorWhite; - else if (HasAnyOf(flags, UiFlags::ColorBlue)) + if (HasAnyOf(flags, UiFlags::ColorBlue)) return ColorBlue; - else if (HasAnyOf(flags, UiFlags::ColorOrange)) + if (HasAnyOf(flags, UiFlags::ColorOrange)) return ColorOrange; - else if (HasAnyOf(flags, UiFlags::ColorRed)) + if (HasAnyOf(flags, UiFlags::ColorRed)) return ColorRed; - else if (HasAnyOf(flags, UiFlags::ColorBlack)) + if (HasAnyOf(flags, UiFlags::ColorBlack)) return ColorBlack; - else if (HasAnyOf(flags, UiFlags::ColorGold)) + if (HasAnyOf(flags, UiFlags::ColorGold)) return ColorGold; - else if (HasAnyOf(flags, UiFlags::ColorUiGold)) + if (HasAnyOf(flags, UiFlags::ColorUiGold)) return ColorUiGold; - else if (HasAnyOf(flags, UiFlags::ColorUiSilver)) + if (HasAnyOf(flags, UiFlags::ColorUiSilver)) return ColorUiSilver; - else if (HasAnyOf(flags, UiFlags::ColorUiGoldDark)) + if (HasAnyOf(flags, UiFlags::ColorUiGoldDark)) return ColorUiGoldDark; - else if (HasAnyOf(flags, UiFlags::ColorUiSilverDark)) + if (HasAnyOf(flags, UiFlags::ColorUiSilverDark)) return ColorUiSilverDark; - else if (HasAnyOf(flags, UiFlags::ColorDialogWhite)) - return ColorDialogWhite; - else if (HasAnyOf(flags, UiFlags::ColorYellow)) + if (HasAnyOf(flags, UiFlags::ColorDialogWhite)) + return gbRunGame ? ColorInGameDialogWhite : ColorDialogWhite; + if (HasAnyOf(flags, UiFlags::ColorDialogRed)) + return ColorInGameDialogRed; + if (HasAnyOf(flags, UiFlags::ColorYellow)) return ColorYellow; - else if (HasAnyOf(flags, UiFlags::ColorButtonface)) + if (HasAnyOf(flags, UiFlags::ColorButtonface)) return ColorButtonface; - else if (HasAnyOf(flags, UiFlags::ColorButtonpushed)) + if (HasAnyOf(flags, UiFlags::ColorButtonpushed)) return ColorButtonpushed; return ColorWhitegold; @@ -443,6 +432,8 @@ uint32_t DoDrawString(const Surface &out, std::string_view text, Rectangle rect, const uint8_t frame = next & 0xFF; const uint16_t width = (*currentFont.sprite)[frame].width(); if (next == U'\n' || characterPosition.x + width > rightMargin) { + if (next == '\n') + maybeDrawCursor(); const int nextLineY = characterPosition.y + opts.lineHeight; if (nextLineY >= bottomMargin) break; @@ -683,7 +674,7 @@ std::string WordWrapString(std::string_view text, unsigned width, GameFontTables */ uint32_t DrawString(const Surface &out, std::string_view text, const Rectangle &rect, TextRenderOptions opts) { - const GameFontTables size = GetSizeFromFlags(opts.flags); + const GameFontTables size = GetFontSizeFromUiFlags(opts.flags); const text_color color = GetColorFromFlags(opts.flags); int charactersInLine = 0; @@ -734,7 +725,7 @@ uint32_t DrawString(const Surface &out, std::string_view text, const Rectangle & void DrawStringWithColors(const Surface &out, std::string_view fmt, DrawStringFormatArg *args, std::size_t argsLen, const Rectangle &rect, TextRenderOptions opts) { - const GameFontTables size = GetSizeFromFlags(opts.flags); + const GameFontTables size = GetFontSizeFromUiFlags(opts.flags); const text_color color = GetColorFromFlags(opts.flags); int charactersInLine = 0; diff --git a/Source/engine/render/text_render.hpp b/Source/engine/render/text_render.hpp index 4e8caf3199ab..3724b7ef7bb5 100644 --- a/Source/engine/render/text_render.hpp +++ b/Source/engine/render/text_render.hpp @@ -20,6 +20,7 @@ #include "engine/clx_sprite.hpp" #include "engine/palette.h" #include "engine/rectangle.hpp" +#include "utils/enum_traits.h" namespace devilution { @@ -38,7 +39,8 @@ enum text_color : uint8_t { ColorUiGoldDark, ColorUiSilverDark, - ColorDialogWhite, + ColorDialogWhite, // Dialog white in main menu + ColorDialogRed, ColorYellow, ColorGold, @@ -52,8 +54,26 @@ enum text_color : uint8_t { ColorButtonface, ColorButtonpushed, + + ColorInGameDialogWhite, // Dialog white in-game + ColorInGameDialogRed, // Dialog red in-game }; +constexpr GameFontTables GetFontSizeFromUiFlags(UiFlags flags) +{ + if (HasAnyOf(flags, UiFlags::FontSize24)) + return GameFont24; + if (HasAnyOf(flags, UiFlags::FontSize30)) + return GameFont30; + if (HasAnyOf(flags, UiFlags::FontSize42)) + return GameFont42; + if (HasAnyOf(flags, UiFlags::FontSize46)) + return GameFont46; + if (HasAnyOf(flags, UiFlags::FontSizeDialog)) + return FontSizeDialog; + return GameFont12; +} + /** * @brief A format argument for `DrawStringWithColors`. */ diff --git a/Source/lua/lua.cpp b/Source/lua/lua.cpp index 93a84ed441b2..7a2b547a1636 100644 --- a/Source/lua/lua.cpp +++ b/Source/lua/lua.cpp @@ -4,7 +4,6 @@ #include #include -#include #include "engine/assets.hpp" #include "lua/modules/log.hpp" @@ -137,19 +136,9 @@ void LuaEvent(std::string_view name) CheckResult(fn()); } -tl::expected RunLua(std::string_view code) +sol::state &LuaState() { - sol::state &lua = *luaState; - const sol::protected_function_result result = lua.safe_script(code); - const bool valid = result.valid(); - if (!valid) { - if (result.get_type() == sol::type::string) { - return tl::make_unexpected(result.get()); - } - return tl::make_unexpected("Unknown Lua error"); - } - - return sol::utility::to_string(sol::stack_object(result)); + return *luaState; } } // namespace devilution diff --git a/Source/lua/lua.hpp b/Source/lua/lua.hpp index 2bb3785acb34..fed528763dfc 100644 --- a/Source/lua/lua.hpp +++ b/Source/lua/lua.hpp @@ -4,11 +4,15 @@ #include +namespace sol { +class state; +} // namespace sol + namespace devilution { void LuaInitialize(); void LuaShutdown(); void LuaEvent(std::string_view name); -tl::expected RunLua(std::string_view code); +sol::state &LuaState(); } // namespace devilution diff --git a/Source/lua/repl.cpp b/Source/lua/repl.cpp new file mode 100644 index 000000000000..620c21acaee9 --- /dev/null +++ b/Source/lua/repl.cpp @@ -0,0 +1,59 @@ +#ifdef _DEBUG +#include "lua/repl.hpp" + +#include +#include + +#include +#include +#include + +#include "lua/lua.hpp" +#include "utils/str_cat.hpp" + +namespace devilution { + +namespace { + +sol::protected_function_result TryRunLuaAsExpressionThenStatement(std::string_view code) +{ + // Try to compile as an expression first. This also how the `lua` repl is implemented. + const sol::state &lua = LuaState(); + std::string expression = StrCat("return ", code, ";"); + sol::detail::typical_chunk_name_t basechunkname = {}; + sol::load_status status = static_cast( + luaL_loadbufferx(lua.lua_state(), expression.data(), expression.size(), + sol::detail::make_chunk_name(expression, sol::detail::default_chunk_name(), basechunkname), "text")); + if (status != sol::load_status::ok) { + // Try as a statement + status = static_cast( + luaL_loadbufferx(lua.lua_state(), code.data(), code.size(), + sol::detail::make_chunk_name(code, sol::detail::default_chunk_name(), basechunkname), "text")); + if (status != sol::load_status::ok) { + return sol::protected_function_result(lua.lua_state(), sol::absolute_index(lua.lua_state(), -1), 0, 1, static_cast(status)); + } + } + sol::stack_aligned_protected_function pf(lua.lua_state(), -1); + return pf(); +} + +} // namespace + +tl::expected RunLuaReplLine(std::string_view code) +{ + const sol::protected_function_result result = TryRunLuaAsExpressionThenStatement(code); + const bool valid = result.valid(); + if (!valid) { + if (result.get_type() == sol::type::string) { + return tl::make_unexpected(result.get()); + } + return tl::make_unexpected("Unknown Lua error"); + } + if (result.get_type() == sol::type::none) { + return std::string {}; + } + return sol::utility::to_string(sol::stack_object(result)); +} + +} // namespace devilution +#endif // _DEBUG diff --git a/Source/lua/repl.hpp b/Source/lua/repl.hpp new file mode 100644 index 000000000000..74fd02b8bfa0 --- /dev/null +++ b/Source/lua/repl.hpp @@ -0,0 +1,14 @@ +#pragma once +#ifdef _DEBUG + +#include +#include + +#include + +namespace devilution { + +tl::expected RunLuaReplLine(std::string_view code); + +} // namespace devilution +#endif // _DEBUG diff --git a/Source/panels/console.cpp b/Source/panels/console.cpp new file mode 100644 index 000000000000..6b6fc1d7f356 --- /dev/null +++ b/Source/panels/console.cpp @@ -0,0 +1,267 @@ +#ifdef _DEBUG +#include "panels/console.hpp" + +#include + +#include + +#ifdef USE_SDL1 +#include "utils/sdl2_to_1_2_backports.h" +#endif + +#include "DiabloUI/text_input.hpp" +#include "control.h" +#include "engine.h" +#include "engine/displacement.hpp" +#include "engine/dx.h" +#include "engine/palette.h" +#include "engine/rectangle.hpp" +#include "engine/render/text_render.hpp" +#include "engine/size.hpp" +#include "engine/surface.hpp" +#include "lua/repl.hpp" +#include "utils/algorithm/container.hpp" +#include "utils/sdl_geometry.h" +#include "utils/str_cat.hpp" +#include "utils/str_split.hpp" + +namespace devilution { + +namespace { + +constexpr std::string_view Prompt = "> "; +bool IsConsoleVisible; +char ConsoleInputBuffer[4096]; +TextInputCursorState ConsoleInputCursor; +TextInputState ConsoleInputState { + TextInputState::Options { + .value = ConsoleInputBuffer, + .cursor = &ConsoleInputCursor, + .maxLength = sizeof(ConsoleInputBuffer) - 1, + } +}; +bool InputTextChanged = false; +std::string WrappedInputText { Prompt }; + +struct ConsoleLine { + enum Type { + Input, + Output, + Error + }; + + Type type; + std::string text; + std::string wrapped = {}; +}; + +std::vector ConsoleLines; + +Rectangle OuterRect; +Rectangle InputRect; +int InputRectHeight; +constexpr int LineHeight = 20; +constexpr int TextPaddingYTop = 0; +constexpr int TextPaddingYBottom = 4; +constexpr int TextPaddingX = 4; +constexpr uint8_t BorderColor = PAL8_YELLOW; +bool FirstRender; + +constexpr UiFlags TextUiFlags = UiFlags::FontSizeDialog; +constexpr UiFlags InputTextUiFlags = TextUiFlags | UiFlags::ColorDialogWhite; +constexpr UiFlags OutputTextUiFlags = TextUiFlags | UiFlags::ColorDialogWhite; +constexpr UiFlags ErrorTextUiFlags = TextUiFlags | UiFlags::ColorDialogRed; + +constexpr int TextSpacing = 0; +constexpr GameFontTables TextFontSize = GetFontSizeFromUiFlags(InputTextUiFlags); + +void CloseConsole() +{ + IsConsoleVisible = false; + SDL_StopTextInput(); +} + +void SendInput() +{ + const std::string_view input = ConsoleInputState.value(); + tl::expected result = RunLuaReplLine(input); + + ConsoleLines.emplace_back(ConsoleLine { .type = ConsoleLine::Input, .text = StrCat("> ", input) }); + + if (result.has_value()) { + if (!result->empty()) { + ConsoleLines.emplace_back(ConsoleLine { .type = ConsoleLine::Output, .text = *std::move(result) }); + } + } else { + if (!result.error().empty()) { + ConsoleLines.emplace_back(ConsoleLine { .type = ConsoleLine::Error, .text = std::move(result).error() }); + } else { + ConsoleLines.emplace_back(ConsoleLine { .type = ConsoleLine::Error, .text = "Unknown error" }); + } + } + + ConsoleInputState.clear(); +} + +void DrawInputText(const Surface &inputTextSurface, std::string_view originalInputText, std::string_view wrappedInputText) +{ + int lineY = 0; + int numRendered = -static_cast(Prompt.size()); + bool prevIsOriginalNewline = false; + for (const std::string_view line : SplitByChar(wrappedInputText, '\n')) { + const int lineCursorPosition = static_cast(ConsoleInputCursor.position) - numRendered; + const bool isCursorOnPrevLine = lineCursorPosition == 0 && !prevIsOriginalNewline && numRendered > 0; + DrawString( + inputTextSurface, line, { 0, lineY }, + TextRenderOptions { + .flags = InputTextUiFlags, + .spacing = TextSpacing, + .cursorPosition = isCursorOnPrevLine ? -1 : lineCursorPosition, + .highlightRange = { static_cast(ConsoleInputCursor.selection.begin) - numRendered, static_cast(ConsoleInputCursor.selection.end) - numRendered }, + }); + lineY += LineHeight; + numRendered += static_cast(line.size()); + prevIsOriginalNewline = static_cast(numRendered) < originalInputText.size() + && originalInputText[static_cast(numRendered)] == '\n'; + if (prevIsOriginalNewline) + ++numRendered; + } +} + +void DrawConsoleLines(const Surface &out) +{ + int lineYEnd = out.h() - 4; // Extra space for letters like g. + // NOLINTNEXTLINE(modernize-loop-convert) + for (auto it = ConsoleLines.rbegin(), itEnd = ConsoleLines.rend(); it != itEnd; ++it) { + ConsoleLine &consoleLine = *it; + if (consoleLine.wrapped.empty()) { + consoleLine.wrapped = WordWrapString(consoleLine.text, out.w(), TextFontSize, TextSpacing); + } + size_t end = consoleLine.wrapped.size(); + while (true) { + const size_t begin = consoleLine.wrapped.rfind('\n', end - 1) + 1; + const std::string_view line = std::string_view(consoleLine.wrapped.data() + begin, end - begin); + lineYEnd -= LineHeight; + switch (consoleLine.type) { + case ConsoleLine::Input: + DrawString(out, line, { 0, lineYEnd }, + TextRenderOptions { .flags = InputTextUiFlags, .spacing = TextSpacing }); + break; + case ConsoleLine::Output: + DrawString(out, line, { 0, lineYEnd }, + TextRenderOptions { .flags = OutputTextUiFlags, .spacing = TextSpacing }); + break; + case ConsoleLine::Error: + DrawString(out, line, { 0, lineYEnd }, + TextRenderOptions { .flags = ErrorTextUiFlags, .spacing = TextSpacing }); + break; + } + if (lineYEnd < 0 || begin == 0) + break; + end = begin - 1; + } + } +} + +} // namespace + +void OpenConsole() +{ + IsConsoleVisible = true; + FirstRender = true; +} + +bool ConsoleHandleEvent(const SDL_Event &event) +{ + if (!IsConsoleVisible) + return false; + if (HandleTextInputEvent(event, ConsoleInputState)) { + InputTextChanged = true; + return true; + } + const auto modState = SDL_GetModState(); + const bool isShift = (modState & KMOD_SHIFT) != 0; + switch (event.type) { + case SDL_KEYDOWN: + switch (event.key.keysym.sym) { + case SDLK_ESCAPE: + CloseConsole(); + return true; + case SDLK_RETURN: + case SDLK_KP_ENTER: + if (isShift) { + ConsoleInputState.type("\n"); + InputTextChanged = true; + } else { + SendInput(); + } + return true; + default: + return false; + } + break; + default: + return false; + } + return false; +} + +void DrawConsole(const Surface &out) +{ + if (!IsConsoleVisible) + return; + + OuterRect.position = { 0, 0 }; + OuterRect.size = { out.w(), out.h() - GetMainPanel().size.height - 2 }; + + const std::string_view originalInputText = ConsoleInputState.value(); + if (InputTextChanged) { + WrappedInputText = WordWrapString(StrCat(Prompt, originalInputText), OuterRect.size.width - 2 * TextPaddingX, TextFontSize, TextSpacing); + } + + const int numLines = static_cast(c_count(WrappedInputText, '\n')) + 1; + const int inputTextHeight = numLines * LineHeight; + InputRectHeight = inputTextHeight + TextPaddingYTop + TextPaddingYBottom; + + InputRect.position = { 0, OuterRect.size.height - InputRectHeight }; + InputRect.size = { OuterRect.size.width, InputRectHeight }; + const Rectangle inputTextRect { + { InputRect.position.x + TextPaddingX, InputRect.position.y + TextPaddingYTop }, + { InputRect.size.width - 2 * TextPaddingX, inputTextHeight } + }; + + if (FirstRender) { + const SDL_Rect sdlInputRect = MakeSdlRect(InputRect); + SDL_SetTextInputRect(&sdlInputRect); + SDL_StartTextInput(); + FirstRender = false; + } + + const Rectangle bgRect = OuterRect; + DrawHalfTransparentRectTo(out, bgRect.position.x, bgRect.position.y, bgRect.size.width, bgRect.size.height); + + DrawConsoleLines( + out.subregion( + TextPaddingX, + TextPaddingYTop, + OuterRect.size.width - 2 * TextPaddingX, + OuterRect.size.height - inputTextRect.size.height - 8)); + + DrawHorizontalLine(out, InputRect.position - Displacement { 0, 1 }, InputRect.size.width, BorderColor); + DrawInputText( + out.subregion( + inputTextRect.position.x, + inputTextRect.position.y, + // Extra space for the cursor on the right: + inputTextRect.size.width + TextPaddingX, + // Extra space for letters like g. + inputTextRect.size.height + TextPaddingYBottom), + originalInputText, + WrappedInputText); + + SDL_Rect sdlRect = MakeSdlRect(OuterRect); + BltFast(&sdlRect, &sdlRect); +} + +} // namespace devilution +#endif // _DEBUG diff --git a/Source/panels/console.hpp b/Source/panels/console.hpp new file mode 100644 index 000000000000..eb524a29419b --- /dev/null +++ b/Source/panels/console.hpp @@ -0,0 +1,15 @@ +#ifdef _DEBUG +#pragma once + +#include + +#include "engine/surface.hpp" + +namespace devilution { + +void OpenConsole(); +bool ConsoleHandleEvent(const SDL_Event &event); +void DrawConsole(const Surface &out); + +} // namespace devilution +#endif // _DEBUG diff --git a/Source/utils/str_split.hpp b/Source/utils/str_split.hpp index 630d64f79afc..20abbf97f8ef 100644 --- a/Source/utils/str_split.hpp +++ b/Source/utils/str_split.hpp @@ -17,10 +17,8 @@ class SplitByCharIterator { return SplitByCharIterator(split_by, text, text.substr(0, text.find(split_by))); } - static SplitByCharIterator end(std::string_view text, char split_by) // NOLINT(readability-identifier-naming) - { - return SplitByCharIterator(split_by, text, text.substr(text.size())); - } + // End iterator + SplitByCharIterator() = default; [[nodiscard]] std::string_view operator*() const { @@ -34,6 +32,10 @@ class SplitByCharIterator { SplitByCharIterator &operator++() { + if (slice_.data() + slice_.size() == text_.data() + text_.size()) { + slice_ = {}; + return *this; + } slice_ = text_.substr(slice_.data() - text_.data() + slice_.size()); if (!slice_.empty()) slice_.remove_prefix(1); // skip the split_by char @@ -66,7 +68,7 @@ class SplitByCharIterator { { } - const char split_by_; + const char split_by_ = '\0'; const std::string_view text_; std::string_view slice_; }; @@ -86,7 +88,7 @@ class SplitByChar { [[nodiscard]] SplitByCharIterator end() const // NOLINT(readability-identifier-naming) { - return SplitByCharIterator::end(text_, split_by_); + return {}; } private: