// Win32 console Platform implementation. #pragma comment(lib, "user32.lib") // Since we need std::min() and std::max()... #define NOMINMAX #include <numeric> #include <sstream> #include <vector> #include <windows.h> #include "Platform_Win32Console.hpp" void fatal(std::wstring_view message) { MessageBoxW(nullptr, message.data(), L"Error", MB_OK | MB_ICONERROR); ExitProcess(1); } void fatal_from_lasterror( std::string_view file, unsigned int line, DWORD lasterror ) { auto fail = [lasterror]() { std::wstring message = L"Error retrieving error string for error " + std::to_wstring(lasterror) + L"?!"; fatal(message); }; LPWSTR formatted = nullptr; DWORD flags = ( FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS ); auto codepoints = FormatMessageW( flags, nullptr, lasterror, 0, reinterpret_cast<LPWSTR>(&formatted), 0, nullptr ); if(codepoints == 0) { fail(); } // std::wstring_convert is deprecated... So, MultiByteToWideChar it is. int file_len = static_cast<int>(file.size()); int file_utf16_len = MultiByteToWideChar( CP_UTF8, 0, file.data(), file_len, nullptr, 0 ); if(file_utf16_len == 0) { fail(); } // The terminating '\0' is already included here. std::wstring file_utf16(file_utf16_len, '\0'); if(!MultiByteToWideChar( CP_UTF8, 0, file.data(), file_len, file_utf16.data(), file_utf16_len )) { fail(); } std::wstringstream message; message << file_utf16 << ":" << std::to_wstring(line) << ": " << formatted; fatal(message.str()); } Platform_Win32Console::Platform_Win32Console(void *handle_out, void *handle_in) : handle_out(handle_out), handle_in(handle_in) { } std::unordered_set<char> Platform_Win32Console::get_input(void) { std::unordered_set<char> ret; DWORD events_available; if(!GetNumberOfConsoleInputEvents(handle_in, &events_available)) { fatal_from_lasterror(__FILE__, __LINE__, GetLastError()); } // ReadConsoleInput blocks if we have no events. So, don't even call it // in this case. if(events_available == 0) { return ret; } std::vector<INPUT_RECORD> input_records(events_available); DWORD events_read; if(!ReadConsoleInput( handle_in, input_records.data(), events_available, &events_read )) { fatal_from_lasterror(__FILE__, __LINE__, GetLastError()); } for(auto const &record : input_records) { if(record.EventType == KEY_EVENT) { auto const &key_event = record.Event.KeyEvent; if(key_event.bKeyDown) { ret.insert(record.Event.KeyEvent.uChar.AsciiChar); } } } return ret; } void Platform_Win32Console::render(PixelBuffer const &buffer) { if(buffer.h() & 0x1) { fatal(L"Rendered PixelBuffers must have a height divisible by 2!"); } COORD con_area = { static_cast<short>(buffer.w()), static_cast<short>(buffer.h() / 2) }; auto con_buffer_size = (con_area.X * con_area.Y); // Additonally keep the buffer from the previous frame, so that we're able // to only render the differences. static std::array<std::vector<CHAR_INFO>, 2> chars; static bool page = true; page = !page; auto &chars_cur = chars[page]; auto &chars_prev = chars[!page]; bool rerender_everything = false; if(chars_cur.size() != con_buffer_size) { // U+2580 UPPER HALF BLOCK chars_cur.resize(con_buffer_size, { { 0x2580 }, 0 }); // Will be resized next frame rerender_everything = (chars_prev.size() != con_buffer_size); // Resize console window and buffer CONSOLE_SCREEN_BUFFER_INFOEX csbi = { sizeof(csbi) }; GetConsoleScreenBufferInfoEx(handle_out, &csbi); COORD buffer_size = { static_cast<SHORT>(con_area.X + 1), static_cast<SHORT>(con_area.Y + 1) }; SMALL_RECT region = { 0, 0, con_area.X, con_area.Y }; SetConsoleWindowInfo(handle_out, true, ®ion); SetConsoleScreenBufferSize(handle_out, buffer_size); } for(auto const &it : buffer) { auto color_value = it.peek().to_value().value_or(0xFF); auto char_i = (((it.pos().y / 2) * buffer.w()) + it.pos().x); auto is_upper = ((it.pos().y & 0x1) == 0); if(is_upper) { chars_cur[char_i].Attributes = ((color_value & 0x1) ? FOREGROUND_INTENSITY : 0) | ((color_value & 0x2) ? FOREGROUND_RED : 0) | ((color_value & 0x4) ? FOREGROUND_GREEN : 0) | ((color_value & 0x8) ? FOREGROUND_BLUE : 0); } else { chars_cur[char_i].Attributes |= ((color_value & 0x1) ? BACKGROUND_INTENSITY : 0) | ((color_value & 0x2) ? BACKGROUND_RED : 0) | ((color_value & 0x4) ? BACKGROUND_GREEN : 0) | ((color_value & 0x8) ? BACKGROUND_BLUE : 0); } } SMALL_RECT region = { 0, 0, con_area.X, con_area.Y }; if(!rerender_everything) { region.Left = con_area.X; region.Top = con_area.Y; region.Right = 0; region.Bottom = 0; for(SHORT y = 0; y < con_area.Y; y++) { for(SHORT x = 0; x < con_area.X; x++) { auto i = ((y * buffer.w()) + x); if(chars_cur[i].Attributes != chars_prev[i].Attributes) { region.Left = std::min(region.Left, x); region.Top = std::min(region.Top, y); region.Right = std::max(region.Right, x); region.Bottom = std::max(region.Bottom, y); } } } } COORD buffer_coord = { region.Left, region.Top }; WriteConsoleOutputW( handle_out, chars_cur.data(), con_area, buffer_coord, ®ion ); } void Platform_Win32Console::set_title(std::string const &title) { SetConsoleTitleA(title.c_str()); } Platform_Win32Console Platform_Win32Console::init() { // Make sure to create a dedicated window, even if we launch from a shell FreeConsole(); // We don't care if this fails if(!AllocConsole()) { fatal_from_lasterror(__FILE__, __LINE__, GetLastError()); } auto handle_out = GetStdHandle(STD_OUTPUT_HANDLE); if(handle_out == INVALID_HANDLE_VALUE) { fatal_from_lasterror(__FILE__, __LINE__, GetLastError()); } auto handle_in = GetStdHandle(STD_INPUT_HANDLE); if(handle_in == INVALID_HANDLE_VALUE) { fatal_from_lasterror(__FILE__, __LINE__, GetLastError()); } CONSOLE_FONT_INFOEX cfi = { sizeof(CONSOLE_FONT_INFOEX) }; if(!GetCurrentConsoleFontEx(handle_out, false, &cfi)) { fatal_from_lasterror(__FILE__, __LINE__, GetLastError()); } // Make sure to enforce a default Windows font that actually has U+2580 wcscpy_s(cfi.FaceName, L"Lucida Console"); cfi.dwFontSize.X = 4; cfi.dwFontSize.Y = 4; if(!SetCurrentConsoleFontEx(handle_out, false, &cfi)) { fatal_from_lasterror(__FILE__, __LINE__, GetLastError()); } CONSOLE_CURSOR_INFO cursor_info = { 1, false }; SetConsoleCursorInfo(handle_out, &cursor_info); return { handle_out, handle_in }; }