Nimbin[12]?SDL & Graphics / sdl_runtime_compiler / src/text/handler.hpp

sdl_runtime_compiler git · main

SDL3 game for running and compiling code at runtime

sdl3 c++ compiler dlopen cmake · first commit 2026-04-19 · last commit 2026-07-03 (3 months ago) · synced 3 days ago · upstream: git.ide3.de/hsnr/sdl-runtime-compiler

C++ 72.3% C 26.2%
git clone https://git.christianimmanuel.de/sdl-graphics/sdl_runtime_compiler.gitwget https://git.christianimmanuel.de/sdl-graphics/sdl_runtime_compiler/archive/sdl_runtime_compiler.tar.gz
src/text/handler.hpp 9.5 KB · 240 lines raw
#pragma once

#include <cstdint>
#include <string>

#include "entities/camera.hpp"
#include "text/text.hpp"
#include "text/token.hpp"
#include "text/vim_buffer.hpp"
#include "utils/globals.hpp"
#include "utils/light.hpp"
#include "wall/wall.hpp"

namespace
Nimbin
{
class GlRenderer;

namespace Text
{

class Handler
{
public:
    static constexpr
           size_t HIT_BASE    = size_t(1) << 32;
    static bool   is_NumberHit(size_t idx) { return idx >= HIT_BASE; }
    static size_t line_Of     (size_t idx) { return idx - HIT_BASE; }

    struct NumberHit { size_t index; Vec3 aabb_Min; Vec3 aabb_Max; };

    struct EditingMetrics {
        double char_w;
        double he_y;
        double left_z;
        double row0_y;
    };


    Handler() = default;
    Handler(Wall::Role role, float world_scale, LayoutMode mode);

    void set_Layout      (LayoutMode m);
    void set_FaceMode    (FaceMode f)  { _face = f; }
    void set_Lexing      (bool on)     { _lex_enabled = on; }
    void set_ShowNumbers (bool on, bool both_columns = false);
    void set_Bordered    (bool on)     { _bordered  = on; }
    void set_Underline   (bool on)     { _underline = on; }
    void set_DefaultStyle(float depth, float scale, double line_height, double word_spacing);
    void set_Lines    (const DynArray<std::string>& lines);

    Wall::Role role  () const { return _role; }
    LayoutMode layout() const { return _layout; }
    void set_AppendBaseline(double y)     { _append_y = y; }
    void set_AppendColor   (SDL_FColor c) { _append_color = c; }

    void clear();

    void append_Line  (const std::string& text);
    const DynArray<Line>&
         get_Lines    () const { return _lines; }
    DynArray<std::string>
         as_Lines     () const;
    void append_Tokens  (DynArray<TokenPtr> block);
    void apply_Highlight(const DynArray<HiSpan>& spans, uint64_t gen);

    Token::Data* token_At_Byte(size_t off);
    TokenPtr     detach       (Token::Data* t);
    void         attach       (DynArray<TokenPtr> tokens, const Vec3& impact);

    // Spatial classifier for thrown words on the always-editing editor grid.
    // 80/20 rule: a non-whitespace token's central 80% (of its column span) is a
    // hit → DropToken; the outer 20% inserts beside it; whitespace cells always
    // insert; vertical gaps between rows create a new line (row may be negative
    // to prepend above the top, or >= line count to append below).
    enum class HitKind : uint8_t { Insert, NewLine, DropToken };
    struct EditorHit { HitKind kind; int row; int col; size_t line_idx; size_t token_idx; };
    EditorHit classify_Impact(const Vec3& impact) const;
    bool      is_Editing() const { return _layout == LayoutMode::Editing; }
    void         clear_ToFloor(DynArray<TokenPtr>& out);
    double       scroll_DeltaToEye (size_t line_idx, double eye_y) const;

    void        bounce            (size_t line_idx);
    DynArray<NumberHit>
                number_Hitboxes   () const;

    bool        is_Dirty   () const { return _lex_enabled && _dirty; }
    uint64_t    generation () const { return _gen; }
    std::string render_Source();
    void        mark_Dirty ();

    void update (double dt, const Camera& cam);
    void render (GlRenderer& gl, const Camera& cam, const Light& light) const;
    void scroll (double delta);


    // ── vim cursor support (Editing) ──────────────────────────────────────────
    EditingMetrics editing_Metrics() const;
    double      row_Y       (int row) const;

    // ── live editing (Editing) ───────────────────────────────────────────────
    enum class EditReq : uint8_t { None, Write, WriteQuit, Exit, Discard };

    void    begin_Edit (double eye_y);
    void    end_Edit   ();
    bool    edit_Active() const { return _capturing; }
    EditReq feed_Keys  (DynArray<int>& keys, double eye_y);
    void    mark_Saved ();

    int cursor_Row      () const;
    int cursor_Col      () const;
    int cursor_VisualCol() const;
    bool cursor_ToToken (Token::Data* t);   // click-to-place: move cursor to a clicked wall token
    bool cursor_AtImpact(const Vec3& impact); // click-to-place: exact char under the crosshair

    struct CursorHit { int row = 0; int byte = 0; bool ok = false; };
    CursorHit resolve_Impact   (const Vec3& impact) const;  // map crosshair → char, current layout
    void      place_CursorInsert(const CursorHit& hit);     // apply after begin_Edit + scroll in
    const std::string& cmdline_Text() const;
    std::string        modeline    () const;
    std::string        vim_ShowCmd () const;
    VimBuffer::Mode    cursor_Mode () const;
    const DynArray<std::string>&
                       status_Lines() const;

    // Frustum half-height (world units) visible at the editor wall, so the Editing
    // scrolloff knows how many rows actually fit and can keep the first/last line
    // in view when zoomed in. Set each frame by the Connection (const view-state
    // cache); 0 means "unknown" -> fall back to the default margin.
    void   set_ViewHalfHeight(double h) const { _view_half_h = h; }
    double wall_X() const { return _transform.position.x; }

    // ── facecam label ───────────────────────────────────────────────────
    void set_Text      (const std::string& text);
    void set_Text      (const std::string& text, SDL_FColor color, float depth, float scale);
    void set_Centre    (const Vec3& p);
    void attach_To     (const Vec3* anchor, const Vec3& offset = {});
    void detach        ();
    void set_FaceCamera (bool on) { _facecam = on; }
    void set_FaceYaw   (float y) { _face_yaw  = y;  }
    void set_Style     (SDL_FColor color, float depth, float scale);
    void set_Collidable(bool on);
    void kick          (size_t word_index, const Vec3& projectile_vel, const Vec3& normal);
    bool view_Is       (size_t tag) const { return tag != 0 && tag == _view_tag; }
    void set_View      (size_t tag)       { _view_tag = tag; }
    void set_Borrowed  (const DynArray<TokenPtr>* borrowed) { _borrowed = borrowed; }
    void clear_Borrowed() { _borrowed = nullptr; }
    bool is_Empty      () const { return _lines.empty() || _lines[0].tokens.empty(); }
    const DynArray<TokenPtr>&
         get_Words     () const;
    void draw          (GlRenderer& gl, const Camera& cam, const Light& light) const { render(gl, cam, light); }

private:
    DynArray<Line>  _lines;

    Wall::Role      _role     { Wall::Role::Editor };
    Wall::Transform _transform{};
    LayoutMode      _layout   { LayoutMode::Display };
    FaceMode        _face     { FaceMode::Pitch };

    float  _depth        { Token::depth_wall };
    float  _scale        { 0.003f };
    double _line_height  { 0.0 };
    double _word_spacing { 0.0 };
    double _append_y     { 0.0 };
    SDL_FColor _append_color { 0.45f, 0.45f, 0.45f, 1.0f };

    DynArray<std::string>     _status;
    const DynArray<TokenPtr>* _borrowed { nullptr };

    size_t _view_tag { 0 };          // identity of currently-materialised info-wall content; 0 = none

    bool _lex_enabled  { true };
    bool _show_numbers { false };
    bool _both_columns { false };
    bool _bordered     { false };
    bool _underline    { false };

    // lexing
    bool        _dirty { false };
    uint64_t    _gen   { 0 };
    std::string _last_src;

    // Editing grid cache (vim layout + cursor)
    double _char_w     { 0.0 };
    double _he_y       { 0.0 };
    double _code_left_z{ 0.0 };
    double _row0_y     { 0.0 };
    double _eye_y      { 0.0 };
    mutable double _view_half_h { 0.0 };   // frustum half-height visible at the wall (set by Connection)
    int    _scroll_top { 0 };        // index of the row stuck to the top of the window

    cpputils::unique_ptr<VimBuffer> _vim;
    unsigned                        _vim_seen_version { 0 };
    bool                            _capturing { false };   // keys are being captured (connected & editing)

    enum class PositionMode : uint8_t { Absolute, Anchor };
    const Vec3*  _anchor_ptr    { nullptr };
    bool         _facecam     { true };
    bool         _collidable    { true };
    float        _face_yaw      { 0.0f };
    Vec3         _attach_offset {};
    Vec3         _centre        {};
    SDL_FColor   _style_color   { 1.0f, 1.0f, 0.0f, 1.0f };
    std::string  _label_text;
    PositionMode _pos_mode      { PositionMode::Absolute };
    DynArray<double> _label_off;

    void   relayout_FaceCamera();
    void   facecam_Update  (double dt, const Camera& cam);
    Vec3   label_Centre      () const;

    // layout
    void   relayout       ();
    void   relayout_Display();
    void   relayout_Editing   ();
    void   sync_VimFromLines();   // editor: push _lines content back into the persistent buffer

    // Display placement internals
    struct Target {
        enum   Kind { Merge, Insert } kind;
        size_t line_index;
        double new_y;
        double push_dist;
    };

    Target resolve_Target  (double y) const;
    Line&  apply_Target    (const Target& t);
    void   attach_Display  (DynArray<TokenPtr>& block, const Vec3& impact);
    void   ensure_WordSeparators(Line& l);
    void   attach_Editing     (DynArray<TokenPtr>& block, const Vec3& impact);
    void   maintain_Numbers(double dt, const Camera& cam);
    void   apply_Tilt      (const Camera& cam);
    bool   impact_To_RowCol(const Vec3& impact, int& row, int& col) const;
};


} // namespace Text

} // namespace Nimbin