#pragma once #include #include #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& 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& get_Lines () const { return _lines; } DynArray as_Lines () const; void append_Tokens (DynArray block); void apply_Highlight(const DynArray& spans, uint64_t gen); Token::Data* token_At_Byte(size_t off); TokenPtr detach (Token::Data* t); void attach (DynArray 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& out); double scroll_DeltaToEye (size_t line_idx, double eye_y) const; void bounce (size_t line_idx); DynArray 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& 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& 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* borrowed) { _borrowed = borrowed; } void clear_Borrowed() { _borrowed = nullptr; } bool is_Empty () const { return _lines.empty() || _lines[0].tokens.empty(); } const DynArray& get_Words () const; void draw (GlRenderer& gl, const Camera& cam, const Light& light) const { render(gl, cam, light); } private: DynArray _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 _status; const DynArray* _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 _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 _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& block, const Vec3& impact); void ensure_WordSeparators(Line& l); void attach_Editing (DynArray& 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