diff --git a/CMake/Assets.cmake b/CMake/Assets.cmake index af0cf737e..b8ff81a6f 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 000000000..5676b4e78 Binary files /dev/null and b/Packaging/resources/assets/fonts/gamedialogred.trn differ diff --git a/Packaging/resources/assets/fonts/gamedialogwhite.trn b/Packaging/resources/assets/fonts/gamedialogwhite.trn new file mode 100644 index 000000000..3e0f42ed4 Binary files /dev/null and b/Packaging/resources/assets/fonts/gamedialogwhite.trn differ diff --git a/Source/CMakeLists.txt b/Source/CMakeLists.txt index 51ae44cd9..9fd547560 100644 --- a/Source/CMakeLists.txt +++ b/Source/CMakeLists.txt @@ -133,10 +133,12 @@ set(libdevilutionx_SRCS levels/trigs.cpp lua/lua.cpp + lua/repl.cpp lua/modules/render.cpp lua/modules/log.cpp panels/charpanel.cpp + panels/console.cpp panels/info_box.cpp panels/mainpanel.cpp panels/spell_book.cpp diff --git a/Source/DiabloUI/text_input.cpp b/Source/DiabloUI/text_input.cpp index ea99dce43..02fb94a3b 100644 --- a/Source/DiabloUI/text_input.cpp +++ b/Source/DiabloUI/text_input.cpp @@ -103,7 +103,7 @@ bool HandleInputEvent(const SDL_Event &event, TextInputState &state, return !isCtrl && !isAlt && event.key.keysym.sym >= SDLK_SPACE && event.key.keysym.sym <= SDLK_z; #endif - } + } break; #ifndef USE_SDL1 case SDL_TEXTINPUT: #ifdef __vita__ @@ -118,6 +118,7 @@ bool HandleInputEvent(const SDL_Event &event, TextInputState &state, default: return false; } + return false; } } // namespace diff --git a/Source/DiabloUI/ui_flags.hpp b/Source/DiabloUI/ui_flags.hpp index 1afdad7d8..ae02f8c99 100644 --- a/Source/DiabloUI/ui_flags.hpp +++ b/Source/DiabloUI/ui_flags.hpp @@ -22,28 +22,29 @@ enum class UiFlags : uint32_t { ColorUiGoldDark = 1 << 8, ColorUiSilverDark = 1 << 9, ColorDialogWhite = 1 << 10, - ColorYellow = 1 << 11, - ColorGold = 1 << 12, - ColorBlack = 1 << 13, - ColorWhite = 1 << 14, - ColorWhitegold = 1 << 15, - ColorRed = 1 << 16, - ColorBlue = 1 << 17, - ColorOrange = 1 << 18, - ColorButtonface = 1 << 19, - ColorButtonpushed = 1 << 20, - - AlignCenter = 1 << 21, - AlignRight = 1 << 22, - VerticalCenter = 1 << 23, - - KerningFitSpacing = 1 << 24, - - ElementDisabled = 1 << 25, - ElementHidden = 1 << 26, - - PentaCursor = 1 << 27, - Outlined = 1 << 28, + ColorDialogRed = 1 << 11, + ColorYellow = 1 << 12, + ColorGold = 1 << 13, + ColorBlack = 1 << 14, + ColorWhite = 1 << 15, + ColorWhitegold = 1 << 16, + ColorRed = 1 << 17, + ColorBlue = 1 << 18, + ColorOrange = 1 << 19, + ColorButtonface = 1 << 20, + ColorButtonpushed = 1 << 21, + + AlignCenter = 1 << 22, + AlignRight = 1 << 23, + VerticalCenter = 1 << 24, + + KerningFitSpacing = 1 << 25, + + ElementDisabled = 1 << 26, + ElementHidden = 1 << 27, + + PentaCursor = 1 << 28, + Outlined = 1 << 29, /** @brief Ensures that the if current element is active that the next element is also visible. */ NeedsNextElement = 1 << 30, diff --git a/Source/debug.cpp b/Source/debug.cpp index d10468746..e9676ced7 100644 --- a/Source/debug.cpp +++ b/Source/debug.cpp @@ -153,15 +153,6 @@ std::string DebugCmdHelp(const std::string_view parameter) return StrCat("Description: ", dbgCmdItem.description, "\nParameters: ", dbgCmdItem.requiredParameter); } -std::string DebugCmdLua(std::string_view code) -{ - tl::expected 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 f3578f6f2..d69c3233b 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."), + SDLK_BACKQUOTE, + OpenConsole); sgOptions.Keymapper.AddAction( "DebugToggle", "Debug toggle", diff --git a/Source/engine/render/scrollrt.cpp b/Source/engine/render/scrollrt.cpp index 2f720b93f..4dc1ddec9 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 d8482ed79..d810a2ba3 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 4e8caf319..3724b7ef7 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 93a84ed44..7a2b547a1 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 2bb3785ac..fed528763 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 000000000..5906bf4d1 --- /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 fn(lua.lua_state(), -1); + return fn(); +} + +} // namespace + +tl::expected RunLuaReplLine(std::string_view code) +{ + const sol::protected_function_result result = TryRunLuaAsExpressionThenStatement(code); + if (!result.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 000000000..74fd02b8b --- /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 000000000..ebdd52c0f --- /dev/null +++ b/Source/panels/console.cpp @@ -0,0 +1,275 @@ +#ifdef _DEBUG +#include "panels/console.hpp" + +#include +#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 : uint8_t { + 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) { + // Make console open on the top-left keyboard key even if it is not a backtick. + if (event.type == SDL_KEYDOWN && event.key.keysym.scancode == SDL_SCANCODE_GRAVE) { + OpenConsole(); + return true; + } + 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"); + } else { + SendInput(); + } + InputTextChanged = true; + 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); + InputTextChanged = false; + } + + 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 000000000..eb524a294 --- /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/sdl2_to_1_2_backports.h b/Source/utils/sdl2_to_1_2_backports.h index a62ab88ae..a1b9e31d5 100644 --- a/Source/utils/sdl2_to_1_2_backports.h +++ b/Source/utils/sdl2_to_1_2_backports.h @@ -51,6 +51,8 @@ #define SDLK_LGUI SDLK_LSUPER #define SDLK_RGUI SDLK_RSUPER +#define SDL_SCANCODE_GRAVE 53 + // Haptic events are not supported in SDL1. #define SDL_INIT_HAPTIC 0 diff --git a/Source/utils/str_split.hpp b/Source/utils/str_split.hpp index 630d64f79..20abbf97f 100644 --- a/Source/utils/str_split.hpp +++ b/Source/utils/str_split.hpp @@ -17,10 +17,8 @@ public: 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 @@ public: 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 @@ private: { } - const char split_by_; + const char split_by_ = '\0'; const std::string_view text_; std::string_view slice_; }; @@ -86,7 +88,7 @@ public: [[nodiscard]] SplitByCharIterator end() const // NOLINT(readability-identifier-naming) { - return SplitByCharIterator::end(text_, split_by_); + return {}; } private: