Parcourir la source

refactor(frontend): Phase 2 - extract shared CursorBlinkController

Create a shared CursorBlinkController utility class that encapsulates
cursor blinking logic for both GTK and TUI frontends:

- Eliminates ~40 lines of duplicated cursor blinking code
- Consistent blink behavior across both frontends
- Single source of truth for blink interval (500ms) and threshold (1000ms)
- Cleaner, more maintainable frontend code

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Bernardo Magri il y a 1 mois
Parent
commit
303c69a28f
4 fichiers modifiés avec 106 ajouts et 63 suppressions
  1. 72 0
      include/lumacs/cursor_blink.hpp
  2. 3 5
      include/lumacs/gtk_editor.hpp
  3. 13 19
      src/gtk_editor.cpp
  4. 18 39
      src/tui_editor.cpp

+ 72 - 0
include/lumacs/cursor_blink.hpp

@@ -0,0 +1,72 @@
+#pragma once
+
+#include <chrono>
+
+namespace lumacs {
+
+/// @brief Shared cursor blinking logic for both GTK and TUI frontends.
+///
+/// This utility encapsulates the cursor blinking algorithm:
+/// - Cursor is always visible for a threshold period after movement
+/// - After the threshold, cursor blinks on/off at a fixed interval
+class CursorBlinkController {
+public:
+    /// Default blink interval (500ms on, 500ms off)
+    static constexpr std::chrono::milliseconds BLINK_INTERVAL{500};
+
+    /// Time cursor stays visible after movement before blinking starts
+    static constexpr std::chrono::milliseconds STATIONARY_THRESHOLD{1000};
+
+    CursorBlinkController() = default;
+
+    /// @brief Notify the controller that the cursor has moved.
+    /// This resets the blink timer and makes the cursor visible.
+    void notify_cursor_moved() {
+        last_move_time_ = std::chrono::steady_clock::now();
+        visible_ = true;
+    }
+
+    /// @brief Update the blink state based on elapsed time.
+    /// @return true if the visibility state changed (requires redraw)
+    bool update() {
+        auto now = std::chrono::steady_clock::now();
+        auto since_move = std::chrono::duration_cast<std::chrono::milliseconds>(
+            now - last_move_time_);
+
+        // Cursor stays visible during stationary threshold
+        if (since_move < STATIONARY_THRESHOLD) {
+            if (!visible_) {
+                visible_ = true;
+                return true;  // State changed
+            }
+            return false;
+        }
+
+        // After threshold, blink based on interval
+        auto blink_phase = std::chrono::duration_cast<std::chrono::milliseconds>(
+            now - last_move_time_ - STATIONARY_THRESHOLD);
+        bool should_be_visible = (blink_phase.count() / BLINK_INTERVAL.count()) % 2 == 0;
+
+        if (visible_ != should_be_visible) {
+            visible_ = should_be_visible;
+            return true;  // State changed
+        }
+
+        return false;
+    }
+
+    /// @brief Check if the cursor should currently be visible.
+    [[nodiscard]] bool is_visible() const { return visible_; }
+
+    /// @brief Force cursor to be visible (useful for input events).
+    void force_visible() {
+        visible_ = true;
+        last_move_time_ = std::chrono::steady_clock::now();
+    }
+
+private:
+    std::chrono::steady_clock::time_point last_move_time_ = std::chrono::steady_clock::now();
+    bool visible_ = true;
+};
+
+} // namespace lumacs

+ 3 - 5
include/lumacs/gtk_editor.hpp

@@ -6,6 +6,7 @@
 #include "lumacs/editor_core.hpp"
 #include "lumacs/face.hpp"
 #include "lumacs/completion_common.hpp" // For CompletionCandidate
+#include "lumacs/cursor_blink.hpp"
 
 #include <chrono> // For std::chrono
 #include <gtkmm.h>
@@ -50,12 +51,9 @@ private:
     Gtk::DrawingArea* minibuffer_drawing_area_ = nullptr; // Dedicated drawing area for minibuffer
     Gtk::Widget* content_widget_ = nullptr; // The root widget of the editor content (Paned or DrawingArea)
     
-    // For cursor blinking logic
-    bool cursor_visible_ = true;
+    // Cursor blinking using shared controller
+    CursorBlinkController cursor_blink_;
     sigc::connection cursor_timer_connection_;
-    std::chrono::steady_clock::time_point last_cursor_move_time_;
-    static constexpr std::chrono::milliseconds BLINK_INTERVAL = std::chrono::milliseconds(500);
-    static constexpr std::chrono::milliseconds BLINK_STATIONARY_THRESHOLD = std::chrono::milliseconds(1000); // 1 second
 
     std::unique_ptr<GtkRenderer> gtk_renderer_;
     std::unique_ptr<GtkCompletionPopup> completion_popup_; // Completion popup

+ 13 - 19
src/gtk_editor.cpp

@@ -35,7 +35,7 @@ LumacsWindow::LumacsWindow(const Glib::RefPtr<Gtk::Application>& application)
 }
 
 // GtkEditor method implementations
-GtkEditor::GtkEditor() : core_(nullptr), drawing_area_(nullptr), content_widget_(nullptr), last_cursor_move_time_(std::chrono::steady_clock::now()) {}
+GtkEditor::GtkEditor() : core_(nullptr), drawing_area_(nullptr), content_widget_(nullptr) {}
 
 GtkEditor::~GtkEditor() {
     // Disconnect cursor timer first to prevent callbacks during destruction
@@ -83,8 +83,7 @@ void GtkEditor::handle_editor_event(EditorEvent event) {
         rebuild_layout();
         return;
     } else if (event == EditorEvent::CursorMoved) {
-        last_cursor_move_time_ = std::chrono::steady_clock::now();
-        cursor_visible_ = true;
+        cursor_blink_.notify_cursor_moved();
         if (content_widget_) {
             queue_redraw_all_windows(content_widget_);
         }
@@ -365,9 +364,10 @@ void GtkEditor::on_activate() {
         gtk_renderer_ = std::make_unique<GtkRenderer>(*core_, *window_);
     }
 
-    // Set up cursor blinking timer (BLINK_INTERVAL like Emacs)
+    // Set up cursor blinking timer
     cursor_timer_connection_ = Glib::signal_timeout().connect(
-        sigc::mem_fun(*this, &GtkEditor::on_cursor_blink), BLINK_INTERVAL.count()
+        sigc::mem_fun(*this, &GtkEditor::on_cursor_blink),
+        CursorBlinkController::BLINK_INTERVAL.count()
     );
 
     // Initialize completion popup (conditionally)
@@ -392,7 +392,7 @@ void GtkEditor::on_draw(const Cairo::RefPtr<Cairo::Context>& cr, int width, int
     // Safety check - don't draw if core is destroyed or gtk_renderer_ not initialized
     if (!core_ || !gtk_renderer_) return;
 
-    gtk_renderer_->on_draw(cr, width, height, cached_active_window_, cursor_visible_);
+    gtk_renderer_->on_draw(cr, width, height, cached_active_window_, cursor_blink_.is_visible());
 }
 
 // Cursor blinking callback
@@ -401,28 +401,22 @@ bool GtkEditor::on_cursor_blink() {
     if (!core_ || !drawing_area_ || !app_) {
         return false; // Stop the timer
     }
-    
+
     // Double check that the app is still running
     if (!app_->is_registered()) {
         return false; // Stop the timer
     }
-    
+
     try {
-        auto now = std::chrono::steady_clock::now();
-        if (now - last_cursor_move_time_ > BLINK_STATIONARY_THRESHOLD) {
-            // Only blink if cursor has been stationary for a while
-            cursor_visible_ = !cursor_visible_;
-            core_->check_and_clear_message(); // Check and clear messages
+        // Update cursor blink state
+        if (cursor_blink_.update()) {
+            core_->check_and_clear_message();
             drawing_area_->queue_draw();
-        } else {
-            // Cursor is still moving or just stopped, keep it visible
-            cursor_visible_ = true;
-            // No need to queue_draw here, it will be done on CursorMoved or next blink.
         }
     } catch (...) {
         return false; // Stop timer on any exception
     }
-    
+
     return true; // Continue timer
 }
 
@@ -486,7 +480,7 @@ Gtk::Widget* GtkEditor::create_widget_for_layout_node(std::shared_ptr<LayoutNode
         drawing_area->set_draw_func([this, weak_window, drawing_area](const Cairo::RefPtr<Cairo::Context>& cr, int width, int height) {
             if (auto window_ptr = weak_window.lock()) { // Correctly capture shared_ptr
                 if (gtk_renderer_) {
-                    gtk_renderer_->draw_window(cr, width, height, window_ptr, drawing_area, cached_active_window_, cursor_visible_);
+                    gtk_renderer_->draw_window(cr, width, height, window_ptr, drawing_area, cached_active_window_, cursor_blink_.is_visible());
                     gtk_renderer_->render_modeline_for_window(cr, width, height, window_ptr, cached_active_window_);
                 }
             }

+ 18 - 39
src/tui_editor.cpp

@@ -3,6 +3,7 @@
 #include "lumacs/lua_api.hpp"
 #include "lumacs/mode_activator.hpp"
 #include "lumacs/keybinding.hpp"
+#include "lumacs/cursor_blink.hpp"
 #include <ncurses.h>
 #include <memory>
 #include <chrono>
@@ -17,7 +18,7 @@ using namespace lumacs;
 class TuiEditor : public IEditorView {
 public:
     TuiEditor() : core_(nullptr) {}
-    
+
     ~TuiEditor() override {
         endwin(); // Cleanup ncurses
     }
@@ -34,12 +35,9 @@ private:
     bool should_quit_ = false;
     std::string message_line_;
     int height_ = 0, width_ = 0;
-    
-    // For cursor blinking logic
-    bool cursor_visible_ = true;
-    std::chrono::steady_clock::time_point last_cursor_move_time_ = std::chrono::steady_clock::now();
-    static constexpr std::chrono::milliseconds BLINK_INTERVAL = std::chrono::milliseconds(500);
-    static constexpr std::chrono::milliseconds BLINK_STATIONARY_THRESHOLD = std::chrono::milliseconds(1000); // 1 second
+
+    // Cursor blinking using shared controller
+    CursorBlinkController cursor_blink_;
     
     // Viewport bounds calculation (avoids code duplication)
     struct ViewportBounds {
@@ -112,8 +110,6 @@ void TuiEditor::run() {
     
     // Initial render
     render();
-    
-    std::chrono::steady_clock::time_point last_blink_toggle_time = std::chrono::steady_clock::now();
 
     while (!should_quit_) {
         // Handle screen resize
@@ -126,39 +122,23 @@ void TuiEditor::run() {
             core_->set_viewport_size(bounds.content_width, bounds.content_height);
             spdlog::debug("Screen resized to: {}x{}", width_, height_);
             spdlog::debug("Content area: {}x{}", bounds.content_width, bounds.content_height);
-            // Force cursor to be visible after resize, as it implies movement.
-            last_cursor_move_time_ = std::chrono::steady_clock::now();
-            cursor_visible_ = true;
-            render();  // Re-render after resize
+            cursor_blink_.notify_cursor_moved();  // Resize implies movement
+            render();
         }
-        
+
         // Get input (with timeout)
         int ch = getch();
-        
+
         // Only process input and render if we got actual input (not timeout)
         if (ch != ERR) {
-            last_cursor_move_time_ = std::chrono::steady_clock::now();
-            cursor_visible_ = true; // Ensure cursor is visible on input
+            cursor_blink_.notify_cursor_moved();
             handle_input(ch);
             render();
-            last_blink_toggle_time = std::chrono::steady_clock::now(); // Reset blink timer after input
-        } else { // No input (timeout occurred)
-            auto now = std::chrono::steady_clock::now();
-            if (now - last_cursor_move_time_ > BLINK_STATIONARY_THRESHOLD) {
-                // If stationary for long enough, start/continue blinking
-                if (now - last_blink_toggle_time > BLINK_INTERVAL) {
-                    cursor_visible_ = !cursor_visible_;
-                    core_->check_and_clear_message(); // Check and clear messages (like for GtkEditor's blink)
-                    render(); // Re-render to show/hide cursor
-                    last_blink_toggle_time = now;
-                }
-            } else {
-                // Still "moving" recently, keep cursor visible.
-                // If it was just hidden by a blink, make it visible again.
-                if (!cursor_visible_) {
-                    cursor_visible_ = true;
-                    render(); // Make sure it's visible
-                }
+        } else {
+            // No input (timeout) - update blink state
+            if (cursor_blink_.update()) {
+                core_->check_and_clear_message();
+                render();
             }
         }
     }
@@ -173,8 +153,7 @@ void TuiEditor::handle_editor_event(EditorEvent event) {
         message_line_ = core_->last_message();
         return;
     } else if (event == EditorEvent::CursorMoved) {
-        last_cursor_move_time_ = std::chrono::steady_clock::now();
-        cursor_visible_ = true;
+        cursor_blink_.notify_cursor_moved();
         render();
         return;
     } else if (event == EditorEvent::TransientMessageCleared) {
@@ -355,7 +334,7 @@ void TuiEditor::render() {
         curs_set(2); // Ensure visible (high visibility)
     } else {
         // Minibuffer inactive: place at buffer cursor
-        if (hw_cursor_x_ != -1 && hw_cursor_y_ != -1 && cursor_visible_) {
+        if (hw_cursor_x_ != -1 && hw_cursor_y_ != -1 && cursor_blink_.is_visible()) {
             move(hw_cursor_y_, hw_cursor_x_);
             curs_set(2); // High visibility
         } else {
@@ -543,7 +522,7 @@ void TuiEditor::render_window(std::shared_ptr<Window> window, int x, int y, int
         }
         
         // Show cursor if this is the cursor line and this is the active window
-        if (buffer_line_idx == cursor.line && is_active && cursor_visible_) {
+        if (buffer_line_idx == cursor.line && is_active && cursor_blink_.is_visible()) {
             int cursor_screen_x = x + line_number_width + cursor.column;
             if (cursor_screen_x < x + width) {
                 char cursor_char = ' ';