Nimbin[12]?SDL & Graphics / sdl_runtime_compiler / commits / 3978d30

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

Fix vim mode scroll out of scope And add vim meta commands bottom right With Claude

Christian Immanuel · 2026-06-27 16:48 · 3978d3051aa72c66b563a071305f06cc8fe1e26e

 src/systems/wall_connection.cpp | 73 +++++++++++++++++++++++++++++++++--------
 src/systems/wall_connection.hpp | 21 +++++++-----
 src/text/handler.cpp            | 28 +++++++++++-----
 src/text/handler.hpp            |  9 +++++
 src/text/vim_buffer.hpp         | 12 +++++++
 5 files changed, 114 insertions(+), 29 deletions(-)

diff --git a/src/systems/wall_connection.cpp b/src/systems/wall_connection.cpp
index 388badb..fa15678 100644
--- a/src/systems/wall_connection.cpp
+++ b/src/systems/wall_connection.cpp
@@ -35,7 +35,7 @@ Connection::begin_Editor(const Vec3& pult_pos, const Text::Handler& editor)
 {
     _target   = Target::Editor;
     _pult_pos = pult_pos;
-    _editor   = &editor;           
+    _editor   = &editor;
     _cmd_last.clear();
     _cmdline.reset();
 }
@@ -84,22 +84,40 @@ Connection::rebuild_Cmdline(const std::string& text)
     _cmdline.reset();
     if (text.empty()) return;
 
-    
+
     _cmdline = Text::make_Token(text, Text::Token::depth_wall, 0.0016f, Vec3{0,0,0}, Config::Color::white);
 }
 
 
+void
+Connection::rebuild_ShowCmd(const std::string& text)
+{
+    _showcmd.reset();
+    if (text.empty()) return;
+    _showcmd = Text::make_Token(text, Text::Token::depth_wall, 0.0016f, Vec3{0,0,0}, Config::Color::white);
+}
+
+
 void
 Connection::update(double dt, const Camera& cam, float aspect, float fov_y)
 {
     if (!is_Active()) return;
     _blink += dt;
 
-    
-    
+
+
     if (_target == Target::Editor && _editor) {
         std::string ml = _editor->modeline();
         if (ml != _cmd_last) { rebuild_Cmdline(ml); _cmd_last = ml; }
+
+        // Bottom-right "showcmd": the pending operator/count (e.g. "2d", "c").
+        std::string sc = _editor->vim_ShowCmd();
+        if (sc != _showcmd_last) { rebuild_ShowCmd(sc); _showcmd_last = sc; }
+
+        // Tell the scrolloff how many rows actually fit, so the first/last line
+        // stays in view when zoomed in. Distance to the editor wall is along x.
+        const double dist_wall = SDL_fabs(cam.pos.x - _editor->wall_X());
+        _editor->set_ViewHalfHeight(dist_wall * static_cast<double>(SDL_tanf(fov_y * 0.5f)));
     }
 
     const Vec3 up = Math::cross(cam.right, cam.fwd);
@@ -108,9 +126,9 @@ Connection::update(double dt, const Camera& cam, float aspect, float fov_y)
     const double half_h = dist * static_cast<double>(SDL_tanf(fov_y * 0.5f));
     const double half_w = half_h * static_cast<double>(aspect);
 
-    
-    const double fx = 0.82;   
-    const double fy = 0.78;   
+
+    const double fx = 0.82;
+    const double fy = 0.78;
     const double left = half_w * fx;
     const double down = half_h * fy;
 
@@ -127,6 +145,18 @@ Connection::update(double dt, const Camera& cam, float aspect, float fov_y)
     _cmd_pos.y += (target.y - _cmd_pos.y) * k;
     _cmd_pos.z += (target.z - _cmd_pos.z) * k;
 
+    // Lower-RIGHT frustum corner for the showcmd (mirror of the cmdline corner).
+    const double right_off = half_w * 0.72;   // a touch inboard so short text stays on-screen
+    Vec3 sc_target {
+        cam.pos.x + cam.fwd.x * dist + cam.right.x * right_off - up.x * down,
+        cam.pos.y + cam.fwd.y * dist + cam.right.y * right_off - up.y * down,
+        cam.pos.z + cam.fwd.z * dist + cam.right.z * right_off - up.z * down,
+    };
+    if (!_showcmd_pos_init) { _showcmd_pos = sc_target; _showcmd_pos_init = true; }
+    _showcmd_pos.x += (sc_target.x - _showcmd_pos.x) * k;
+    _showcmd_pos.y += (sc_target.y - _showcmd_pos.y) * k;
+    _showcmd_pos.z += (sc_target.z - _showcmd_pos.z) * k;
+
     _cmd_yaw   = static_cast<float>(SDL_atan2(-cam.fwd.x, -cam.fwd.z));
 }
 
@@ -141,8 +171,8 @@ Connection::render(GlRenderer& gl, const Camera& cam, const Light& light) const
         draw_Electric     (gl, cam);
     }
     else if (_target == Target::Editor) {
-        
-        
+
+
         if (_cmdline) {
             const double hw = static_cast<double>(_cmdline->half_extent);
             _cmdline->pos = Vec3{
@@ -159,6 +189,23 @@ Connection::render(GlRenderer& gl, const Camera& cam, const Light& light) const
 
         draw_EditorCursor(gl, cam, light);
         draw_Electric    (gl, cam);
+
+        // Bottom-right showcmd: right-align by anchoring its right edge at the
+        // corner (offset left by its half-width), mirroring the cmdline's left
+        // anchor on the opposite side.
+        if (_showcmd) {
+            const double hw = static_cast<double>(_showcmd->half_extent);
+            _showcmd->pos = Vec3{
+                _showcmd_pos.x - cam.right.x * hw,
+                _showcmd_pos.y - cam.right.y * hw,
+                _showcmd_pos.z - cam.right.z * hw,
+            };
+            _showcmd->yaw   = _cmd_yaw;
+            _showcmd->pitch = 0.0f;
+
+            gl.clear_Depth();
+            _showcmd->draw(gl, cam, light);
+        }
     }
 }
 
@@ -194,7 +241,7 @@ Connection::draw_EditorCursor(GlRenderer& gl, const Camera& cam, const Light& li
     if (SDL_fmod(_blink, 0.8) > 0.4) return;
 
     const Text::Handler::EditingMetrics m = _editor->editing_Metrics();
-    if (m.char_w <= 0.0) return;                 
+    if (m.char_w <= 0.0) return;
 
     const int    row   = _editor->cursor_Row();
     const int    col   = _editor->cursor_Col();
@@ -205,8 +252,8 @@ Connection::draw_EditorCursor(GlRenderer& gl, const Camera& cam, const Light& li
 
     Vec3 c { tf.position.x, row_y - (m.he_y + 0.06), cz };
 
-    
-    
+
+
     const Vec3  ref   { tf.position.x, row_y, cz };
     const float pitch = Wall::tilt_TwardEye(ref, cam.pos);
 
@@ -260,6 +307,6 @@ Connection::draw_Electric(GlRenderer& gl, const Camera& cam) const
 }
 
 
-} 
+}
 
 } // namespace Nimbin
diff --git a/src/systems/wall_connection.hpp b/src/systems/wall_connection.hpp
index f84e4da..8ecf833 100644
--- a/src/systems/wall_connection.hpp
+++ b/src/systems/wall_connection.hpp
@@ -4,6 +4,7 @@
 
 #include "text/token.hpp"
 
+
 namespace
 Nimbin
 {
@@ -37,25 +38,29 @@ public:
 
 
 private:
-    std::string _cmd_last;
-    std::string _prog_last;
-
-    Vec3 _cmd_pos {};
-    Vec3 _pult_pos{};
+    bool   _cmd_pos_init { false };
+    bool   _showcmd_pos_init { false };
+    float  _cmd_yaw      { 0.0f };
+    double _blink        { 0.0 };
 
     Target _target { Target::None };
 
     const Text::Handler* _editor { nullptr };
 
     Text::TokenPtr _cmdline;
+    Text::TokenPtr _showcmd;
     Text::TokenPtr _prog_input;
 
-    float  _cmd_yaw      { 0.0f };
-    double _blink        { 0.0 };
+    Vec3 _cmd_pos     {};
+    Vec3 _showcmd_pos {};
+    Vec3 _pult_pos    {};
 
-    bool   _cmd_pos_init { false };
+    std::string _cmd_last;
+    std::string _showcmd_last;
+    std::string _prog_last;
 
     void rebuild_Cmdline       (const std::string& text);
+    void rebuild_ShowCmd       (const std::string& text);
     void rebuild_ProgramPreview(const std::string& buffer);
     void draw_EditorCursor (GlRenderer& gl, const Camera& cam, const Light& light) const;
     void draw_ProgramCursor(GlRenderer& gl, const Camera& cam, const Light& light) const;
diff --git a/src/text/handler.cpp b/src/text/handler.cpp
index 2efbeb5..50536be 100644
--- a/src/text/handler.cpp
+++ b/src/text/handler.cpp
@@ -808,18 +808,23 @@ Handler::relayout_Editing()
     const double anchor_bottom = bottom_y + static_cast<double>(N - 1) * lh;
 
     if (_capturing) {
-        // vim-style scrolloff. MARGIN lines are kept above & below the cursor, so the
-        // cursor stays centred and the text scrolls on every step. PAD leaves a few
-        // blank rows of overscroll at the file ends so the first/last line sits just
-        // inside the view instead of flush against (and clipped by) the screen edge.
-        constexpr int MARGIN = 4;   // lines above & below the cursor (lower if rows clip)
-        constexpr int PAD    = 1;   // blank rows kept above the first / below the last line
-        const int     visible = 2 * MARGIN + 1;
+        // vim-style scrolloff, zoom-aware: rows_half = how many rows fit between
+        // the eye and the top edge at the current zoom; MARGIN can't exceed it, so
+        // the first/last line stays in view (gg no longer loses line 1 off the top
+        // when zoomed in). 0 → unknown → fall back to the cap.
+        constexpr int MARGIN_MAX = 4;   // cap when zoomed out (vim scrolloff feel)
+        constexpr int PAD        = 1;   // blank rows kept at the file ends
+        const int rows_half = (_view_half_h > 0.0)
+                            ? static_cast<int>(SDL_floor(_view_half_h / lh))
+                            : MARGIN_MAX;
+        const int MARGIN  = SDL_clamp(rows_half - PAD, 1, MARGIN_MAX);
+        const int visible = 2 * MARGIN + 1;
         _scroll_top = SDL_clamp(R - MARGIN, -PAD, SDL_max(0, N - visible) + PAD);
 
         _row0_y = (_eye_y + static_cast<double>(MARGIN) * lh)
                 + static_cast<double>(_scroll_top) * lh;
-    } else {
+    }
+    else {
         _row0_y = anchor_bottom;       // at rest: whole file bottom-aligned to the floor
     }
 
@@ -1419,6 +1424,13 @@ Handler::modeline() const
 }
 
 
+std::string
+Handler::vim_ShowCmd() const
+{
+    return _vim ? _vim->get_ShowCmd() : std::string{};
+}
+
+
 void
 Handler::set_Style(SDL_FColor color, float depth, float scale)
 {
diff --git a/src/text/handler.hpp b/src/text/handler.hpp
index 1d3798e..0d0c201 100644
--- a/src/text/handler.hpp
+++ b/src/text/handler.hpp
@@ -118,10 +118,18 @@ public:
     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);
@@ -179,6 +187,7 @@ private:
     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;
diff --git a/src/text/vim_buffer.hpp b/src/text/vim_buffer.hpp
index 81e2ff9..73339cb 100644
--- a/src/text/vim_buffer.hpp
+++ b/src/text/vim_buffer.hpp
@@ -8,6 +8,7 @@
 namespace
 Nimbin
 {
+
 class VimBuffer {
 public:
     enum class Mode   : uint8_t { Normal, Insert, Command };
@@ -50,6 +51,17 @@ public:
          get_Cmdline () const { return _cmd; }
     const std::string&
          get_Status  () const { return _status; }
+
+    // Pending operator + count for the bottom-right "showcmd" area, e.g. "d",
+    // "2d", "c", "g", or a bare count "3". Empties as soon as the command
+    // completes (both _pending and _count are cleared on dispatch).
+    std::string get_ShowCmd() const
+    {
+        std::string s;
+        if (_count > 0) s += std::to_string(_count);
+        s += _pending;
+        return s;
+    }
     void start_Insert() { _mode = Mode::Insert; _col = 0; touch(); }
 
     void enter_Insert() { _mode = Mode::Insert; clamp_Col(); touch(); }   // insert at current cursor