← index
libmx2/MX2_Project/webmx/mx2/include/console.hpp
Source: libmx2/MX2_Project/webmx/mx2/include/console.hpp
/**
 * @file console.hpp
 * @brief OpenGL-backed in-game terminal console for libmx2.
 *
 * Provides four related classes:
 *  - console::CommandQueue  – thread-safe command dispatch queue
 *  - console::ConsoleChars  – per-character SDL_Surface glyph cache
 *  - console::Console       – core text-entry / output widget (SDL_Surface)
 *  - console::GLConsole     – OpenGL wrapper that renders Console via GLSprite
 *
 * Depends on mx.hpp, gl.hpp (forward-declared) and SDL2/SDL_ttf.
 * On Emscripten the threading primitives are replaced with no-op stubs.
 */
#ifndef __CONSOLE__H__MX2

#define __CONSOLE__H__MX2


#include "mx.hpp"

#include <atomic>

#include <condition_variable>

#include <functional>

#include <iostream>

#include <memory>

#include <mutex>

#include <queue>

#include <sstream>

#include <string>

#include <thread>

#include <unordered_map>

#include <vector>


namespace gl {
    class GLWindow;
    class GLSprite;
    class ShaderProgram;
} // namespace gl


#ifndef GL_UNSIGNED_INT

typedef unsigned int GLuint;
#endif


#if defined(__EMSCRIPTEN__)

class EmscriptenMutex {
  public:
    void lock() {}
    void unlock() {}
};

class EmscriptenGuard {
  public:
    EmscriptenGuard(EmscriptenMutex &) {}
};

#define THREAD_MUTEX EmscriptenMutex

#define THREAD_GUARD(mutex) EmscriptenGuard guard(mutex)

#define THREAD_ENABLED 0

#else

#define THREAD_MUTEX std::recursive_mutex

#define THREAD_GUARD(mutex) std::lock_guard<std::recursive_mutex> guard(mutex)

#define THREAD_ENABLED 1

#endif


namespace console {

    /**
     * @class CommandQueue
     * @brief Thread-safe FIFO queue for dispatching console commands to a worker thread.
     *
     * Commands are pushed from the rendering / event thread and consumed by a dedicated
     * background thread that invokes the associated callback.
     */
    class CommandQueue {
      public:
        /** @brief A single command entry with its text and completion callback. */
        struct Command {
            std::string text;                                                  ///< Raw command string.

            std::function<void(const std::string &, std::ostream &)> callback; ///< Handler invoked on the worker thread.

        };

        /** @brief Push a command onto the queue (thread-safe). */
        void push(const Command &cmd);

        /**
         * @brief Pop the next command (blocking on native, non-blocking on Emscripten).
         * @param cmd Output: the dequeued command.
         * @return @c true if a command was retrieved; @c false if the queue was shut down.
         */
        bool pop(Command &cmd);

        /** @brief Signal the worker to drain and stop. */
        void shutdown();

      private:
        std::queue<Command> queue;
#if THREAD_ENABLED

        std::mutex queue_mutex;
        std::condition_variable cv;
#else

        EmscriptenMutex queue_mutex;
#endif

        std::atomic<bool> active{true};
    };

    /**
     * @class ConsoleChars
     * @brief Glyph cache that maps each ASCII character to a pre-rendered SDL_Surface.
     *
     * Surfaces are produced via SDL_ttf and stored in an unordered_map keyed by char.
     * Recreate by calling load() after font changes.
     */
    class ConsoleChars {
      public:
        ConsoleChars();
        ~ConsoleChars();

        ConsoleChars(const ConsoleChars &) = delete;
        ConsoleChars &operator=(const ConsoleChars &) = delete;
        ConsoleChars(ConsoleChars &&) = delete;
        ConsoleChars &operator=(ConsoleChars &&) = delete;

        /**
         * @brief Load/reload the glyph cache from a font file.
         * @param fnt  Path to a TTF font file.
         * @param size Point size.
         * @param c    Colour applied to each glyph surface.
         */
        void load(const std::string &fnt, int size, const SDL_Color &c);

        /** @brief Release all cached SDL_Surface objects. */
        void clear();

        std::unordered_map<char, SDL_Surface *> characters; ///< Glyph surfaces keyed by char.

      protected:
        std::unique_ptr<mx::Font> font;
    };

    enum FadeState {
        FADE_NONE,
        FADE_IN,
        FADE_OUT
    };

    class GLConsole;

    /**
     * @class Console
     * @brief Core text-console widget backed by an SDL_Surface.
     *
     * Manages an output buffer, an editable input line, command history,
     * optional fade-in/out animation, and a thread-safe message queue.
     * Rendered to an SDL_Surface by drawText(); GLConsole uploads that surface
     * to a GL texture each frame.
     */
    class Console {
      public:
        Console();
        ~Console();

        Console(const Console &) = delete;
        Console &operator=(const Console &) = delete;
        Console(Console &&) = delete;
        Console &operator=(Console &&) = delete;

        friend class GLConsole;
        mx::mxUtil *util; ///< Pointer to asset-path utilities (not owned).


        /**
         * @brief Set position and size of the console rectangle.
         * @param x,y Top-left corner in pixels.
         * @param w,h Width and height in pixels.
         */
        void create(int x, int y, int w, int h);

        /**
         * @brief Load font and rebuild the glyph cache.
         * @param fnt  Path to a TTF font file.
         * @param size Point size.
         * @param col  Glyph render colour.
         */
        void load(const std::string &fnt, int size, const SDL_Color &col);

        /** @brief Clear the output buffer and reset scroll. */
        void clear();

        /**
         * @brief Append text to the output buffer.
         * @param str String to append (may contain newlines).
         */
        void print(const std::string &str);

        /**
         * @brief Append text from any thread (uses the internal message queue).
         * @param str String to append.
         */
        void thread_safe_print(const std::string &str);

        /**
         * @brief Handle a single character key press for the input line.
         * @param c Character received from SDL_TEXTINPUT or SDL_KEYDOWN.
         */
        void keypress(char c);

        /**
         * @brief Render the console contents to an internal SDL_Surface.
         * @return Pointer to the freshly rendered surface (owned by Console).
         */
        SDL_Surface *drawText();

        /** @brief Return the current input-line text. */
        std::string getText() const;

        /** @brief Replace the input-line text. */
        void setText(const std::string &text);

        /** @return Current caret position within the input line. */
        size_t getCursorPos() const;

        /** @brief Move the caret to @p pos within the input line. */
        void setCursorPos(size_t pos);

        /**
         * @brief Tokenise and dispatch a command string.
         * @param cmd Raw command text (may include arguments).
         */
        void procCmd(const std::string &cmd);

        /** @brief Change the prompt prefix (default: "$ "). */
        void setPrompt(const std::string &text);

        /** @brief Jump the scroll view to the most recent output. */
        void scrollToBottom();

        /**
         * @brief Register a command callback invoked when the user presses Enter.
         * @param window   Parent GL window.
         * @param callback Handler receives tokenised arguments; return @c true to suppress default handling.
         */
        void setCallback(gl::GLWindow *window,
                         std::function<bool(gl::GLWindow *win, const std::vector<std::string> &)> callback);

        void moveCursorLeft();          ///< Move caret one character to the left.

        void moveCursorRight();         ///< Move caret one character to the right.

        void moveCursorHome();          ///< Move caret to start of input line.

        void moveCursorEnd();           ///< Move caret to end of input line.

        void moveHistoryUp();           ///< Recall the previous command from history.

        void moveHistoryDown();         ///< Move forward through command history.

        void updateInputScrollOffset(); ///< Recalculate horizontal scroll for long input lines.


        int getHeight() const { return console_rect.h; } ///< @return Pixel height of the console area.

        int getWidth() const { return console_rect.w; }  ///< @return Pixel width of the console area.


        std::string promptText = "$ ";  ///< Prompt string displayed before the input line.

        Uint32 cursorBlinkTime = 0;     ///< SDL_GetTicks() value of the last cursor blink toggle.

        bool cursorVisible = true;      ///< Whether the cursor is currently shown.

        gl::GLWindow *window = nullptr; ///< Owning GL window (weak reference).


        void startFadeIn();                                      ///< Begin a fade-in animation.

        void startFadeOut();                                     ///< Begin a fade-out animation.

        bool isFading() const { return fadeState != FADE_NONE; } ///< @return @c true while animating.

        bool isVisible() const { return alpha > 0; }             ///< @return @c true if at least partially visible.

        void show();                                             ///< Make the console fully visible immediately.

        void hide();                                             ///< Make the console invisible immediately.

        void reload();                                           ///< Reload the glyph cache with the current font settings.


        /** @return @c true if the glyph cache has been populated. */
        bool loaded() const {
            THREAD_GUARD(console_mutex);
            return c_chars.characters.size() > 0;
        }

        bool isScrollDragging() const { return scrollDragging; } ///< @return Whether the scrollbar thumb is being dragged.


        /**
         * @brief Change font size and colour without reloading the widget.
         * @param size New point size.
         * @param col  New colour.
         */
        void setTextAttrib(const int size, const SDL_Color &col);

        /**
         * @brief Access the internal output buffer directly as an ostream.
         * @return Reference to the internal ostringstream.
         */
        std::ostringstream &bufferData();

        /**
         * @brief Register a callback for raw input-line submission (Enter key).
         * @param callback Handler receives the raw input string; return value is unused.
         */
        void setInputCallback(std::function<int(gl::GLWindow *win, const std::string &)> callback);

        std::string inputBuffer;         ///< Current content of the input line (mirrors internal state).

        bool needsRedraw = true;         ///< Set to trigger an SDL_Surface redraw.

        bool needsReflow = true;         ///< Set to trigger line-wrapping recalculation.

        FadeState fadeState = FADE_NONE; ///< Current fade animation state.


        /** @brief Process pending messages from the thread-safe print queue (call each frame). */
        void process_message_queue();

#if defined(__EMSCRIPTEN__)

        /** @brief Single-threaded command queue drain (Emscripten only). */
        void processCommandQueue();
#endif


        void scrollUp();       ///< Scroll output up by one line.

        void scrollDown();     ///< Scroll output down by one line.

        void resetScroll();    ///< Reset scroll to the bottom.

        void scrollPageUp();   ///< Scroll up by one page.

        void scrollPageDown(); ///< Scroll down by one page.


        /**
         * @brief Handle a mouse-wheel scroll event.
         * @param mouseX,mouseY Cursor position.
         * @param wheelY        Scroll delta (positive = up).
         */
        void handleMouseScroll(int mouseX, int mouseY, int wheelY);

        /**
         * @brief Test whether the mouse is over the scrollbar thumb.
         * @return @c true if the cursor is inside the scrollbar rectangle.
         */
        bool isMouseOverScrollbar(int mouseX, int mouseY) const;

        void beginScrollDrag(int mouseY);  ///< Start a scrollbar thumb drag.

        void updateScrollDrag(int mouseY); ///< Continue a scrollbar thumb drag.

        void endScrollDrag();              ///< Release the scrollbar thumb.

      protected:
        mutable THREAD_MUTEX console_mutex;
        std::queue<std::string> message_queue;
        ConsoleChars c_chars;
        bool userScrolling = false;
        std::string font;
        int font_size = 16;
        SDL_Color color = {255, 255, 255, 255};
        std::ostringstream data;
        std::string input_text;
        SDL_Surface *surface = nullptr;
        SDL_Rect console_rect;
        size_t cursorPos = 0;
        size_t stopPosition = 0;
        size_t inputCursorPos = 0;
        int cursorX = 0;
        int cursorY = 0;
        bool showInput = true;
        void checkScroll();
        void updateCursorPosition();
        void checkForLineWrap();
        std::function<bool(gl::GLWindow *win, const std::vector<std::string> &)> callback = nullptr;
        std::function<int(gl::GLWindow *window, const std::string &)> callbackEnter = nullptr;
        bool callbackSet = false;
        std::vector<std::string> commandHistory;
        int historyIndex = -1;
        std::string tempBuffer;
        unsigned char alpha = 188;
        Uint32 fadeStartTime = 0;
        unsigned int fadeDuration = 500;
        int inputScrollOffset = 0;
        struct TextLine {
            std::string text;
            size_t startPos;
            size_t length;
        };
        std::vector<TextLine> lines;
        bool inMultilineMode;
        int braceCount;
        std::string multilineBuffer;
        std::string originalPrompt;
        std::string continuationPrompt;
        void processMultilineInput(const std::string &cmd);
        bool checkBraceBalance(const std::string &text);
        void calculateLines();
        std::unique_ptr<std::jthread> worker_thread;
        CommandQueue command_queue;
        std::atomic<bool> worker_active{true};
        void worker_thread_func();
        bool enterCallbackSet = false;
        void clearText();

        int visibleLineCount = 0;
        int scrollOffset = 0;
        bool scrollDragging = false;
        int scrollDragStartY = 0;
        int scrollDragStartOffset = 0;
        SDL_Rect scrollbarRect = {0, 0, 0, 0};
    };

    /**
     * @class GLConsole
     * @brief OpenGL-rendered overlay console built on top of Console.
     *
     * GLConsole manages a gl::GLSprite and uploads the Console's SDL_Surface
     * to a GL texture every frame. It handles window resize, fade animation,
     * and optional full-height stretching — making it easy to drop an interactive
     * terminal into any GLWindow-based application.
     */
    class GLConsole {
      public:
        GLConsole();
        ~GLConsole();

        GLConsole(const GLConsole &) = delete;
        GLConsole &operator=(const GLConsole &) = delete;
        GLConsole(GLConsole &&) = delete;
        GLConsole &operator=(GLConsole &&) = delete;

        /** @brief Destroy the GL texture and sprite resources. */
        void release();

        friend class Console;

        /**
         * @brief Load and position the console inside a specific rectangle.
         * @param win  Parent GL window.
         * @param rc   Desired console rectangle in window-local pixels.
         * @param fnt  TTF font path.
         * @param size Font point size.
         * @param col  Text colour.
         */
        void load(gl::GLWindow *win, const SDL_Rect &rc, const std::string &fnt, int size, const SDL_Color &col);

        /**
         * @brief Load the console spanning the full window width.
         * @param win  Parent GL window.
         * @param fnt  TTF font path.
         * @param size Font point size.
         * @param col  Text colour.
         */
        void load(gl::GLWindow *win, const std::string &fnt, int size, const SDL_Color &col);

        /**
         * @brief Upload SDL_Surface and blit the GL texture each frame.
         * @param win Current GL window (provides gl context).
         */
        void draw(gl::GLWindow *win);

        /**
         * @brief Low-level SDL event handler (keyboard + mouse).
         * @param e SDL event reference.
         */
        void handleEvent(SDL_Event &e);

        /**
         * @brief High-level event handler with window context.
         * @param win Parent GL window.
         * @param e   SDL event reference.
         */
        void event(gl::GLWindow *win, SDL_Event &e);

        /** @brief Append text to the console output. */
        void print(const std::string &data);

        /** @brief Append text followed by a newline. */
        void println(const std::string &data);

        /** @brief Thread-safe variant of print(). */
        void thread_safe_print(const std::string &data);

        /**
         * @brief Handle window resize — rebuilds GL texture at new dimensions.
         * @param win Parent GL window.
         * @param w,h New viewport size in pixels.
         */
        void resize(gl::GLWindow *win, int w, int h);

        /** @brief Change the prompt prefix string. */
        void setPrompt(const std::string &prompt);

        /** @brief Register a command callback (same semantics as Console::setCallback()). */
        void setCallback(gl::GLWindow *window,
                         std::function<bool(gl::GLWindow *win, const std::vector<std::string> &)> callback);

        /** @brief Register a raw-input callback (same semantics as Console::setInputCallback()). */
        void setInputCallback(std::function<int(gl::GLWindow *window, const std::string &)> callback);

        void show(); ///< Make console visible.

        void hide(); ///< Hide console.


        /** @brief Enable/disable vertical stretch to fill the window height. */
        void setStretch(bool stretch) { stretch_value = stretch; }

        /** @brief Change font size/colour (propagates to inner Console). */
        void setTextAttrib(const int size, const SDL_Color &col);

        int getWidth() const;  ///< @return Console pixel width.

        int getHeight() const; ///< @return Console pixel height.


        /** @brief Update the inner Console's window pointer. */
        void setWindow(gl::GLWindow *win) { console.window = win; }

        /**
         * @brief Handle built-in commands (clear, quit, help, …).
         * @param cmd Tokenised command vector.
         * @return @c true if the command was consumed.
         */
        bool procDefaultCommands(const std::vector<std::string> &cmd);

        /** @brief Reset all registered callbacks to no-ops. */
        void clear_callbacks() {
            console.callback = [](gl::GLWindow *window, const std::vector<std::string> &args) -> bool {
                return false;
            };
            console.callbackEnter = [](gl::GLWindow *window, const std::string &text) -> int {
                return 0;
            };
            console.callbackSet = false;
            console.enterCallbackSet = false;
        }

        /**
         * @brief printf-style helper that formats a string and calls print().
         * @tparam Args Variadic format arguments.
         * @param format printf format string.
         * @param args   Format arguments.
         */
        template <typename... Args>
        void printf(const char *format, Args... args) {
            if (format == nullptr)
                return;
            int size = std::snprintf(nullptr, 0, format, args...);
            if (size < 0) {
                print("[printf formatting error]\n");
                return;
            }
            std::vector<char> buffer(static_cast<size_t>(size) + 1);
            std::snprintf(buffer.data(), buffer.size(), format, args...);
            print(std::string(buffer.data()));
        }

        /** @return Stream reference to the inner output buffer. */
        std::ostream &bufferData() { return console.bufferData(); }

        /** @return Stream containing the last submitted input line. */
        std::istream &inputData();

        /** @brief Flush the thread-safe message queue into the output buffer (call each frame). */
        void process_message_queue();

        bool isVisible() const { return console.isVisible(); } ///< @return @c true if the console is at least partially visible.

        bool isFading() const { return console.isFading(); }   ///< @return @c true while a fade animation is running.


        /** @brief Set an absolute height override for the stretch mode. */
        void setStretchHeight(int value);

        /** @brief Clear the entire output buffer. */
        void clearText();

        /** @brief Drain the command queue and process pending messages (call each frame). */
        void refresh() {
#if defined(__EMSCRIPTEN__)

            console.processCommandQueue();
#endif

            console.process_message_queue();
        }

      protected:
        mutable std::mutex gl_mutex;
        Console console;
        bool stretch_value = true;
        SDL_Rect rc;
        int stretch_height = 0;
        std::unique_ptr<gl::GLSprite> sprite = nullptr;
        std::unique_ptr<gl::ShaderProgram> shader;
        GLuint texture = 0;
        GLuint loadTextFromSurface(SDL_Surface *surf);
        void updateTexture(GLuint tex, SDL_Surface *surf);
        std::string font;
        int font_size = 16;
        SDL_Color color = {255, 255, 255, 255};

      private:
        std::istringstream inputStream;
    };
} // namespace console


#endif