Преглед изворни кода

feat(ui): Implement smart cursor blinking for GTK and TUI

- Cursor now only blinks when stationary for a set duration (1 second).
- Stops blinking and remains visible when the cursor is actively moving or has just moved.
- Implemented in both GtkEditor and TuiEditor by tracking last cursor movement time.
- Fixed minor syntax errors in TuiEditor's minibuffer activation callbacks.
Bernardo Magri пре 1 месец
родитељ
комит
51f9ebaced
3 измењених фајлова са 72 додато и 10 уклоњено
  1. 6 0
      include/lumacs/gtk_editor.hpp
  2. 24 7
      src/gtk_editor.cpp
  3. 42 3
      src/tui_editor.cpp

+ 6 - 0
include/lumacs/gtk_editor.hpp

@@ -7,6 +7,7 @@
 #include "lumacs/face.hpp"
 #include "lumacs/completion_common.hpp" // For CompletionCandidate
 
+#include <chrono> // For std::chrono
 #include <gtkmm.h>
 #include <pangomm.h>
 #include <memory>
@@ -47,12 +48,17 @@ private:
     Gtk::DrawingArea* drawing_area_ = nullptr; // Raw pointer to the main drawing area if single window
     Gtk::Widget* content_widget_ = nullptr; // The root widget of the editor content (Paned or DrawingArea)
     
+    // For cursor blinking logic
     bool cursor_visible_ = true;
     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
 
+
 protected:
     void on_activate();
     void on_draw(const Cairo::RefPtr<Cairo::Context>& cr, int width, int height);

+ 24 - 7
src/gtk_editor.cpp

@@ -34,7 +34,7 @@ LumacsWindow::LumacsWindow(const Glib::RefPtr<Gtk::Application>& application)
 }
 
 // GtkEditor method implementations
-GtkEditor::GtkEditor() : core_(nullptr), drawing_area_(nullptr), content_widget_(nullptr) {}
+GtkEditor::GtkEditor() : core_(nullptr), drawing_area_(nullptr), content_widget_(nullptr), last_cursor_move_time_(std::chrono::steady_clock::now()) {}
 
 GtkEditor::~GtkEditor() {
     // Disconnect cursor timer first to prevent callbacks during destruction
@@ -80,10 +80,19 @@ void GtkEditor::handle_editor_event(EditorEvent event) {
     // Handle layout changes
     if (event == EditorEvent::WindowLayoutChanged) {
         rebuild_layout();
+    } else if (event == EditorEvent::CursorMoved) {
+        // Cursor moved, reset blink timer and ensure cursor is visible
+        last_cursor_move_time_ = std::chrono::steady_clock::now();
+        cursor_visible_ = true;
+        if (content_widget_) {
+             // Redraw immediately to ensure cursor is visible at new position
+            queue_redraw_all_windows(content_widget_);
+        }
     }
 
     // Request redraw on most events - recursively find all drawing areas
-    if (content_widget_) {
+    // This catches CursorMoved too, but we added explicit redraw for immediate visibility
+    if (content_widget_ && event != EditorEvent::CursorMoved) { // Avoid double redraw for CursorMoved
         queue_redraw_all_windows(content_widget_);
     }
 
@@ -387,9 +396,9 @@ void GtkEditor::on_activate() {
         gtk_renderer_ = std::make_unique<GtkRenderer>(*core_, *drawing_area_);
     }
 
-    // Set up cursor blinking timer (500ms intervals like Emacs)
+    // Set up cursor blinking timer (BLINK_INTERVAL like Emacs)
     cursor_timer_connection_ = Glib::signal_timeout().connect(
-        sigc::mem_fun(*this, &GtkEditor::on_cursor_blink), 500
+        sigc::mem_fun(*this, &GtkEditor::on_cursor_blink), BLINK_INTERVAL.count()
     );
 
     // Initialize completion popup
@@ -428,9 +437,17 @@ bool GtkEditor::on_cursor_blink() {
     }
     
     try {
-        cursor_visible_ = !cursor_visible_;
-        core_->check_and_clear_message(); // Check and clear messages
-        drawing_area_->queue_draw();
+        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
+            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
     }

+ 42 - 3
src/tui_editor.cpp

@@ -37,6 +37,12 @@ private:
     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
+    
     // Private helper method declarations
     std::string resolve_key(int ch);
     bool handle_input(int ch);
@@ -91,6 +97,8 @@ 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
         int new_height, new_width;
@@ -106,6 +114,9 @@ void TuiEditor::run() {
             core_->set_viewport_size(content_width, content_height);
             debug_log << "Screen resized to: " << width_ << "x" << height_ << std::endl;
             debug_log << "Content area: " << content_width << "x" << content_height << std::endl;
+            // 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
         }
         
@@ -114,8 +125,29 @@ void TuiEditor::run() {
         
         // 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
             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
+                }
+            }
         }
     }
 }
@@ -125,6 +157,13 @@ void TuiEditor::handle_editor_event(EditorEvent event) {
         should_quit_ = true;
     } else if (event == EditorEvent::Message) {
         message_line_ = core_->last_message(); // Still update local message_line_ for rendering
+    } else if (event == EditorEvent::CursorMoved) {
+        last_cursor_move_time_ = std::chrono::steady_clock::now();
+        cursor_visible_ = true;
+        render(); // Ensure immediate redraw to show cursor at new position
+    } else if (event == EditorEvent::TransientMessageCleared) {
+        // Redraw to clear the message from the screen
+        render();
     } else if (event == EditorEvent::CommandMode) {
         core_->minibuffer_manager().activate_minibuffer(
             MinibufferMode::Command, ":",
@@ -135,7 +174,7 @@ void TuiEditor::handle_editor_event(EditorEvent event) {
                     auto result = core_->minibuffer_manager().parse_and_execute_command_string(input);
                     core_->set_message(result.message);
                 }
-            },
+            }, // Added comma here
             [this]() { core_->set_message("Cancelled"); }
         );
     } else if (event == EditorEvent::BufferSwitchMode) {
@@ -189,7 +228,7 @@ void TuiEditor::handle_editor_event(EditorEvent event) {
                 } else {
                     core_->set_message("Theme not found: " + input);
                 }
-            },
+            }, // Added comma here
             [this]() { core_->set_message("Cancelled"); }
         );
     } else if (event == EditorEvent::ISearchMode) {
@@ -513,7 +552,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) {
+        if (buffer_line_idx == cursor.line && is_active && cursor_visible_) {
             int cursor_screen_x = x + line_number_width + cursor.column;
             if (cursor_screen_x < x + width) {
                 char cursor_char = ' ';