13 SDL_LockSurface(surface);
14 Uint8* pixels = (Uint8*)surface->pixels;
15 int pitch = surface->pitch;
16 Uint8* tempRow =
new Uint8[pitch];
17 for (
int y = 0; y < surface->h / 2; ++y) {
18 Uint8* row1 = pixels + y * pitch;
19 Uint8* row2 = pixels + (surface->h - y - 1) * pitch;
20 memcpy(tempRow, row1, pitch);
21 memcpy(row1, row2, pitch);
22 memcpy(row2, tempRow, pitch);
26 SDL_UnlockSurface(surface);
31 font = std::make_unique<mx::Font>();
33 std::cerr <<
"Failed to create font object" << std::endl;
45 std::string chars =
" 0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!@#$%^&*()_+[]{}|;:,.<>?`~\"\'/\\-=";
46 for (
char c : chars) {
47 SDL_Surface *surface = TTF_RenderGlyph_Solid(
font->unwrap(), c, col);
51 std::cerr <<
"Failed to load character: " << c << std::endl;
60 if(p.second !=
nullptr) {
61 SDL_FreeSurface(p.second);
83 cv.wait_for(lock, std::chrono::milliseconds(100),
153 surface = SDL_CreateRGBSurfaceWithFormat(0, w, h, 32, SDL_PIXELFORMAT_RGBA32);
155 std::cerr <<
"Failed to create surface: " << SDL_GetError() << std::endl;
163 void Console::load(
const std::string &fnt,
int size,
const SDL_Color &col) {
220 if (
c_chars.characters.empty())
return;
229 charWidth =
c_chars.characters[
'A']->w * 4;
231 auto it =
c_chars.characters.find(c);
232 charWidth = (it !=
c_chars.characters.end()) ?
233 it->second->w :
c_chars.characters[
'A']->w;
235 promptWidth += charWidth;
239 int cursorPixelPos = promptWidth;
245 charWidth =
c_chars.characters[
'A']->w * 4;
247 auto it =
c_chars.characters.find(c);
248 charWidth = (it !=
c_chars.characters.end()) ?
249 it->second->w :
c_chars.characters[
'A']->w;
251 cursorPixelPos += charWidth;
261 charWidth =
c_chars.characters[
'A']->w * 4;
263 auto it =
c_chars.characters.find(c);
264 charWidth = (it !=
c_chars.characters.end()) ?
265 it->second->w :
c_chars.characters[
'A']->w;
267 cursorPixelPos -= charWidth;
297 }
else if (c == 127) {
307 }
else if (c == 13) {
318 for (
char ch : cmd) {
328 size_t firstOpenBrace = processedCommand.find(
'{');
329 if (firstOpenBrace != std::string::npos) {
330 processedCommand.erase(firstOpenBrace, 1);
332 size_t lastCloseBrace = processedCommand.find_last_of(
'}');
333 if (lastCloseBrace != std::string::npos) {
334 processedCommand.erase(lastCloseBrace, 1);
356 if (cmd.find(
'{') != std::string::npos && cmd.find(
'}') == std::string::npos) {
358 for (
char ch : cmd) {
376 }
else if (c ==
'{') {
384 }
else if (c ==
'}') {
392 std::string processedCommand;
394 if (firstOpenBrace != std::string::npos) {
397 size_t lastCloseBrace =
inputBuffer.find_last_of(
'}');
398 if (lastCloseBrace != std::string::npos) {
408 procCmd(processedCommand +
"\n");
419 }
catch (
const std::exception &e) {
420 std::cerr <<
"Error in keypress: " << e.what() << std::endl;
426 std::string text =
data.str();
431 int charWidth =
c_chars.characters[
'A']->w;
434 for (
size_t i = 0; i < text.length(); ++i) {
440 y +=
c_chars.characters[
'A']->h;
444 width = charWidth * 4;
446 auto it =
c_chars.characters.find(c);
447 if (it !=
c_chars.characters.end()) {
448 width = it->second->w;
450 width =
c_chars.characters[
'A']->w;
458 y +=
c_chars.characters[
'A']->h;
470 std::string text =
data.str();
489 y +=
c_chars.characters[
'A']->h;
495 y +=
c_chars.characters[
'A']->h;
500 width =
c_chars.characters[
'A']->w * 4;
502 auto it =
c_chars.characters.find(c);
503 if (it !=
c_chars.characters.end()) {
504 width = it->second->w;
506 width =
c_chars.characters[
'A']->w;
525 int charHeight =
c_chars.characters[
'A']->h;
526 int bottomPadding = charHeight + 10;
528 int maxVisibleLines = (promptY -
console_rect.y) / charHeight;
529 int maxScrollbackLines = maxVisibleLines * 50;
531 while (
static_cast<int>(
lines.size()) > maxScrollbackLines && !
lines.empty()) {
532 size_t removeLen =
lines[0].length;
533 std::string buffer =
data.str();
534 if (removeLen < buffer.length() && buffer[removeLen] ==
'\n') {
537 if (removeLen > buffer.length()) removeLen = buffer.length();
538 std::string newBuffer = buffer.substr(removeLen);
544 std::string finalText =
data.str();
553 auto it_A_check =
c_chars.characters.find(
'A');
554 if (!
surface ||
c_chars.characters.empty() || it_A_check ==
c_chars.characters.end() || !it_A_check->second || it_A_check->second->h == 0) {
559 std::string text =
data.str();
562 size_t lineStart = 0;
563 size_t currentPos = 0;
564 int currentLineWidth = 0;
566 SDL_Surface* char_A_surface = it_A_check->second;
567 int defaultCharWidth = char_A_surface->w;
568 int lineHeight = char_A_surface->h;
570 while (currentPos < text.length()) {
571 char c = text[currentPos];
575 lines.push_back({text.substr(lineStart, currentPos - lineStart), lineStart, currentPos - lineStart});
576 lineStart = currentPos + 1;
577 currentLineWidth = 0;
583 charWidth = defaultCharWidth * 4;
585 auto it_char =
c_chars.characters.find(c);
586 if (it_char !=
c_chars.characters.end() && it_char->second) {
587 charWidth = it_char->second->w;
589 charWidth = defaultCharWidth;
593 if (lineStart < currentPos && (currentLineWidth + charWidth) > maxWidth) {
594 lines.push_back({text.substr(lineStart, currentPos - lineStart), lineStart, currentPos - lineStart});
595 lineStart = currentPos;
596 currentLineWidth = charWidth;
598 currentLineWidth += charWidth;
603 if (lineStart < text.length()) {
604 lines.push_back({text.substr(lineStart), lineStart, text.length() - lineStart});
610 int bottomPadding = lineHeight + 10;
614 int maxVisibleLines = (promptY -
console_rect.y) / lineHeight;
615 maxVisibleLines -= 1;
616 if (maxVisibleLines < 1) maxVisibleLines = 1;
625 Uint32 currentTime = SDL_GetTicks();
634 if (t < 0.0f) t = 0.0f;
635 if (t > 1.0f) t = 1.0f;
636 alpha = (
unsigned char)(1 + (188 - 1) * t);
644 if (t < 0.0f) t = 0.0f;
645 if (t > 1.0f) t = 1.0f;
646 alpha = (
unsigned char)(188 * (1.0f - t));
665 int lineHeight =
c_chars.characters[
'A']->h;
666 int bottomPadding = lineHeight + 10;
669 SDL_Rect separatorRect = {
675 SDL_FillRect(
surface, &separatorRect, SDL_MapRGBA(
surface->format, 75, 75, 75, 220));
677 int maxVisibleLines = (promptY -
console_rect.y) / lineHeight;
678 maxVisibleLines -= 1;
679 if (maxVisibleLines < 1) maxVisibleLines = 1;
681 size_t startLine = 0;
682 if (
static_cast<int>(
lines.size()) > maxVisibleLines) {
683 int totalLines =
lines.size();
685 startLine = std::max(0, lastVisibleLine - maxVisibleLines + 1);
691 for (
size_t i = startLine; i <
lines.size() && i < startLine + maxVisibleLines; ++i) {
692 const auto& line =
lines[i];
693 if (y >= promptY)
break;
696 for (
char c : line.text) {
697 auto it =
c_chars.characters.find(c);
698 if (it !=
c_chars.characters.end()) {
699 SDL_Surface *char_surface = it->second;
700 SDL_Rect dest_rect = {x, y, char_surface->w, char_surface->h};
701 SDL_BlitSurface(char_surface,
nullptr,
surface, &dest_rect);
702 x += char_surface->w;
703 }
else if (c ==
'\t') {
704 x +=
c_chars.characters[
'A']->w * 4;
717 auto it =
c_chars.characters.find(c);
718 if (it !=
c_chars.characters.end()) {
719 SDL_Surface *char_surface = it->second;
720 SDL_Rect dest_rect = {x, promptY, char_surface->w, char_surface->h};
721 SDL_BlitSurface(char_surface,
nullptr,
surface, &dest_rect);
722 x += char_surface->w;
723 }
else if (c ==
'\t') {
724 x +=
c_chars.characters[
'A']->w * 4;
740 charWidth =
c_chars.characters[
'A']->w * 4;
742 auto it =
c_chars.characters.find(c);
743 charWidth = (it !=
c_chars.characters.end()) ?
744 it->second->w :
c_chars.characters[
'A']->w;
752 auto it =
c_chars.characters.find(c);
753 if (it !=
c_chars.characters.end()) {
754 SDL_Surface *char_surface = it->second;
755 SDL_Rect dest_rect = {x, promptY, char_surface->w, char_surface->h};
756 SDL_BlitSurface(char_surface,
nullptr,
surface, &dest_rect);
768 SDL_Rect cursorRect = {cursorXPos, promptY, 2,
c_chars.characters[
'A']->h};
769 SDL_FillRect(
surface, &cursorRect, SDL_MapRGBA(
surface->format, 255, 255, 255, 255));
773 int total =
lines.size();
775 int scrollbarWidth = 15;
780 SDL_Rect trackRect = {scrollbarX, scrollbarY, scrollbarWidth, scrollbarHeight};
781 SDL_FillRect(
surface, &trackRect, SDL_MapRGBA(
surface->format, 40, 40, 40, 180));
789 int thumbHeight = std::max(20, (
int)(thumbRatio * scrollbarHeight));
791 int thumbY = scrollbarY + (int)(scrollProgress * (scrollbarHeight - thumbHeight));
793 SDL_Rect thumbRect = {scrollbarX + 1, thumbY, scrollbarWidth - 2, thumbHeight};
794 SDL_FillRect(
surface, &thumbRect, SDL_MapRGBA(
surface->format, 120, 120, 120, 0));
800 std::vector<std::string>
tokenize(
const std::string& input) {
801 std::vector<std::string> tokens;
802 std::istringstream iss(input);
804 while (iss >> token) {
805 tokens.push_back(token);
896 if (!SDL_GL_GetCurrentContext()) {
931 std::ostringstream result;
934 if (!result.str().empty()) {
937 }
catch (
const std::exception& e) {
945 ev.type = SDL_USEREVENT;
959 static constexpr size_t MAX_CHARS = 300000;
961 if (s.size() > MAX_CHARS) {
962 s = s.substr(s.size() - MAX_CHARS);
977 if (cmd_text.empty()) {
984 const size_t MAX_HISTORY = 50;
993 std::vector<std::string> tokens =
tokenize(cmd_text);
995 command_queue.push({cmd_text, [
this, cmd_text](
const std::string& text, std::ostream& output) {
1001 processCommandQueue();
1007 if (!
callback(this->window, tokens)) {
1016#ifdef __EMSCRIPTEN__
1017 void Console::processCommandQueue() {
1021 std::ostringstream result;
1024 if (!result.str().empty()) {
1027 }
catch (
const std::exception& e) {
1035 ev.type = SDL_USEREVENT;
1099 console.thread_safe_print(data);
1103 console.process_message_queue();
1107 console.setInputCallback(callback);
1112 std::cerr <<
"Surface is null." << std::endl;
1116 glGenTextures(1, &texture_id);
1117 glBindTexture(GL_TEXTURE_2D, texture_id);
1118 GLenum pixelFormat = GL_RGBA;
1119 glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, surf->w, surf->h, 0,
1120 pixelFormat, GL_UNSIGNED_BYTE, surf->pixels);
1122 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
1123 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
1141 shader = std::make_unique<gl::ShaderProgram>();
1146 sprite = std::make_unique<gl::GLSprite>();
1157 console.setTextAttrib(size, col);
1163 int consoleWidth = win->
w - 50;
1164 int consoleHeight = (win->
h / 2) - 50;
1168 rc.w = consoleWidth;
1169 rc.h = consoleHeight;
1172 load(win,
rc, fnt, size, col);
1178 glActiveTexture(GL_TEXTURE0);
1179 glBindTexture(GL_TEXTURE_2D, tex);
1180 glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, surf->w, surf->h, GL_RGBA, GL_UNSIGNED_BYTE, surf->pixels);
1181 glBindTexture(GL_TEXTURE_2D, 0);
1185 Uint32 currentTime = SDL_GetTicks();
1193 if (currentTime -
console.cursorBlinkTime > 500) {
1195 console.cursorBlinkTime = currentTime;
1199 SDL_Surface *surface =
nullptr;
1204 shader->setUniform(
"textTexture", 0);
1209 int consoleWidth =
console.getWidth();
1210 int consoleHeight =
console.getHeight();
1211 sprite->draw(
texture, 25.0f, 25.0f, consoleWidth, consoleHeight);
1215 console.thread_safe_print(data);
1216 SDL_Event ev{SDL_USEREVENT};
1225 if(e.type == SDL_TEXTINPUT) {
1226 for(
int i = 0; i < SDL_TEXTINPUTEVENT_TEXT_SIZE && e.text.text[i] !=
'\0'; ++i) {
1227 console.keypress(e.text.text[i]);
1231 if(e.type == SDL_KEYDOWN) {
1233 if(e.key.keysym.sym == SDLK_ESCAPE) {
1235 }
else if(e.key.keysym.sym == SDLK_RETURN) {
1237 }
else if(e.key.keysym.sym == SDLK_BACKSPACE) {
1239 }
else if(e.key.keysym.sym == SDLK_TAB) {
1241 }
else if(e.key.keysym.sym == SDLK_LEFT) {
1244 }
else if(e.key.keysym.sym == SDLK_RIGHT) {
1247 }
else if(e.key.keysym.sym == SDLK_HOME) {
1250 }
else if(e.key.keysym.sym == SDLK_END) {
1253 }
else if(e.key.keysym.sym == SDLK_UP) {
1256 }
else if(e.key.keysym.sym == SDLK_DOWN) {
1260 else if(e.key.keysym.sym == SDLK_PAGEUP) {
1264 else if(e.key.keysym.sym == SDLK_PAGEDOWN) {
1267 }
else if(e.key.keysym.sym == SDLK_DELETE) {
1271 else if(e.type == SDL_MOUSEWHEEL) {
1274 SDL_GetMouseState(&x, &y);
1275 console.handleMouseScroll(x, y, e.wheel.y);
1279 else if(e.type == SDL_MOUSEBUTTONDOWN) {
1281 if (
console.isMouseOverScrollbar(e.button.x, e.button.y)) {
1282 console.beginScrollDrag(e.button.y);
1287 else if(e.type == SDL_MOUSEMOTION) {
1289 console.updateScrollDrag(e.motion.y);
1293 else if(e.type == SDL_MOUSEBUTTONUP) {
1302#if defined(__EMSCRIPTEN__)
1317 rc.h = (h / 2) - 50;
1329 console.setCallback(window, callback);
1341 }
else if(cmd.size() == 1 && (cmd[0] ==
"about" || cmd[0] ==
"version")) {
1343 print(
"writen by Jared Bruni\n");
1344 print(
"https://lostsidedead.biz\n");
1346 else if (cmd.size() > 0 && cmd[0] ==
"clear") {
1349 }
else if (cmd.size() > 0 && cmd[0] ==
"exit") {
1350 print(
"Exiting...\n");
1355 }
else if(cmd.size() == 5 && cmd[0] ==
"settext") {
1356 int size = std::stoi(cmd[1]);
1358 color.r = std::stoi(cmd[2]);
1359 color.g = std::stoi(cmd[3]);
1360 color.b = std::stoi(cmd[4]);
1363 print(
"Text attributes set to size " + std::to_string(size) +
1364 " and color (" + std::to_string(
color.r) +
", " +
1365 std::to_string(
color.g) +
", " + std::to_string(
color.b) +
")\n");
1368 print(
"Unknown command: " + cmd[0] +
"\n");
1407 int total =
lines.size();
1410 int totalLines =
lines.size();
1419 int maxLocal = trackH - thumbMinH;
1420 localY = std::max(0, std::min(maxLocal, localY));
1422 float progress = 1.0f - (float)localY / (
float)maxLocal;
1438 }
else if (wheelY < 0) {
bool pop(Command &cmd)
Pop the next command (blocking on native, non-blocking on Emscripten).
std::queue< Command > queue
void push(const Command &cmd)
Push a command onto the queue (thread-safe).
std::atomic< bool > active
std::condition_variable cv
void shutdown()
Signal the worker to drain and stop.
std::unordered_map< char, SDL_Surface * > characters
Glyph surfaces keyed by char.
void load(const std::string &fnt, int size, const SDL_Color &c)
Load/reload the glyph cache from a font file.
std::unique_ptr< mx::Font > font
void clear()
Release all cached SDL_Surface objects.
std::vector< TextLine > lines
FadeState fadeState
Current fade animation state.
void moveCursorLeft()
Move caret one character to the left.
void show()
Make the console fully visible immediately.
void resetScroll()
Reset scroll to the bottom.
void scrollPageDown()
Scroll down by one page.
SDL_Surface * drawText()
Render the console contents to an internal SDL_Surface.
void procCmd(const std::string &cmd)
Tokenise and dispatch a command string.
void moveCursorEnd()
Move caret to end of input line.
std::unique_ptr< std::thread > worker_thread
std::recursive_mutex console_mutex
std::function< int(gl::GLWindow *window, const std::string &)> callbackEnter
void updateCursorPosition()
void create(int x, int y, int w, int h)
Set position and size of the console rectangle.
unsigned int fadeDuration
std::ostringstream & bufferData()
Access the internal output buffer directly as an ostream.
void scrollToBottom()
Jump the scroll view to the most recent output.
CommandQueue command_queue
std::vector< std::string > commandHistory
gl::GLWindow * window
Owning GL window (weak reference).
void print(const std::string &str)
Append text to the output buffer.
void endScrollDrag()
Release the scrollbar thumb.
std::string originalPrompt
std::queue< std::string > message_queue
std::function< bool(gl::GLWindow *win, const std::vector< std::string > &)> callback
void moveHistoryUp()
Recall the previous command from history.
void scrollDown()
Scroll output down by one line.
void moveCursorHome()
Move caret to start of input line.
void thread_safe_print(const std::string &str)
Append text from any thread (uses the internal message queue).
void updateScrollDrag(int mouseY)
Continue a scrollbar thumb drag.
void beginScrollDrag(int mouseY)
Start a scrollbar thumb drag.
void reload()
Reload the glyph cache with the current font settings.
void process_message_queue()
Process pending messages from the thread-safe print queue (call each frame).
bool needsReflow
Set to trigger line-wrapping recalculation.
void scrollPageUp()
Scroll up by one page.
std::string promptText
Prompt string displayed before the input line.
void clear()
Clear the output buffer and reset scroll.
std::atomic< bool > worker_active
void handleMouseScroll(int mouseX, int mouseY, int wheelY)
Handle a mouse-wheel scroll event.
void setCallback(gl::GLWindow *window, std::function< bool(gl::GLWindow *win, const std::vector< std::string > &)> callback)
Register a command callback invoked when the user presses Enter.
void updateInputScrollOffset()
Recalculate horizontal scroll for long input lines.
void scrollUp()
Scroll output up by one line.
int scrollDragStartOffset
bool isMouseOverScrollbar(int mouseX, int mouseY) const
Test whether the mouse is over the scrollbar thumb.
bool cursorVisible
Whether the cursor is currently shown.
void keypress(char c)
Handle a single character key press for the input line.
void setTextAttrib(const int size, const SDL_Color &col)
Change font size and colour without reloading the widget.
void moveHistoryDown()
Move forward through command history.
void worker_thread_func()
void moveCursorRight()
Move caret one character to the right.
void startFadeOut()
Begin a fade-out animation.
bool needsRedraw
Set to trigger an SDL_Surface redraw.
std::string inputBuffer
Current content of the input line (mirrors internal state).
std::string multilineBuffer
void setInputCallback(std::function< int(gl::GLWindow *win, const std::string &)> callback)
Register a callback for raw input-line submission (Enter key).
void load(const std::string &fnt, int size, const SDL_Color &col)
Load font and rebuild the glyph cache.
void startFadeIn()
Begin a fade-in animation.
void hide()
Make the console invisible immediately.
std::istringstream inputStream
void updateTexture(GLuint tex, SDL_Surface *surf)
std::unique_ptr< gl::GLSprite > sprite
void setPrompt(const std::string &prompt)
Change the prompt prefix string.
void draw(gl::GLWindow *win)
Upload SDL_Surface and blit the GL texture each frame.
void release()
Destroy the GL texture and sprite resources.
void clearText()
Clear the entire output buffer.
void process_message_queue()
Flush the thread-safe message queue into the output buffer (call each frame).
void print(const std::string &data)
Append text to the console output.
void println(const std::string &data)
Append text followed by a newline.
void show()
Make console visible.
void setStretchHeight(int value)
Set an absolute height override for the stretch mode.
bool procDefaultCommands(const std::vector< std::string > &cmd)
Handle built-in commands (clear, quit, help, …).
std::istream & inputData()
GLuint loadTextFromSurface(SDL_Surface *surf)
void event(gl::GLWindow *win, SDL_Event &e)
High-level event handler with window context.
void resize(gl::GLWindow *win, int w, int h)
Handle window resize — rebuilds GL texture at new dimensions.
std::unique_ptr< gl::ShaderProgram > shader
void setTextAttrib(const int size, const SDL_Color &col)
Change font size/colour (propagates to inner Console).
void refresh()
Drain the command queue and process pending messages (call each frame).
void setInputCallback(std::function< int(gl::GLWindow *window, const std::string &)> callback)
Register a raw-input callback (same semantics as Console::setInputCallback()).
void load(gl::GLWindow *win, const SDL_Rect &rc, const std::string &fnt, int size, const SDL_Color &col)
Load and position the console inside a specific rectangle.
void setCallback(gl::GLWindow *window, std::function< bool(gl::GLWindow *win, const std::vector< std::string > &)> callback)
Register a command callback (same semantics as Console::setCallback()).
void thread_safe_print(const std::string &data)
Thread-safe variant of print().
A textured screen-space quad drawn with an OpenGL shader.
SDL2 + OpenGL/ES window with event loop and console support.
int w
Current window width.
int h
Current window height.
void quit()
Request the event loop to stop.
mx::mxUtil util
Asset path and utility helpers.
General-purpose exception class for the mx library.
RAII wrapper around a TTF_Font handle.
#define PROJECT_VERSION_MINOR
Minor version number of the project.
#define PROJECT_VERSION_MAJOR
Enable OpenGL support.
OpenGL-backed in-game terminal console for libmx2.
#define THREAD_GUARD(mutex)
OpenGL window, shader program, sprite, and text rendering classes.
std::vector< std::string > tokenize(const std::string &input)
SDL_Surface * flipSurface(SDL_Surface *surface)
const char * vSource
Default vertex shader source.
const char * fSource
Default fragment shader source.
A single command entry with its text and completion callback.
std::string text
Raw command string.
std::function< void(const std::string &, std::ostream &)> callback
Handler invoked on the worker thread.