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

feat(ui): improve GTK text rendering, cursor positioning, and region highlighting

Bernardo Magri пре 1 месец
родитељ
комит
9075b1fac0
2 измењених фајлова са 180 додато и 146 уклоњено
  1. 9 88
      DEV_STATE.md
  2. 171 58
      src/gtk_editor.cpp

+ 9 - 88
DEV_STATE.md

@@ -22,172 +22,93 @@
 
 ## Current Module
 
-**Phase 10: Plugin System enhancements** - Enhancing the Lua API to expose more C++ core functionality for scripting.
-
-
+**Phase 11: UI Polish & Rendering** - Improving GTK text rendering, cursor precision, and visual feedback.
 
 ## File Manifest
 
 ```
-
 Lumacs/
-
 ├── CMakeLists.txt           # Build configuration
-
 ├── init.lua                 # Main Lua initialization
-
 ├── themes.lua              # Theme definitions
-
 ├── include/lumacs/
-
 │   ├── editor_core.hpp     # Core editor logic
-
 │   ├── buffer.hpp          # Text buffer management
-
 │   ├── lua_api.hpp         # C++/Lua bridge
-
 │   ├── gtk_editor.hpp      # GTK4 frontend
-
 │   ├── tui_editor.hpp      # TUI fallback
-
 │   ├── keybinding.hpp      # Key handling
-
 │   ├── theme.hpp           # Theme/face system
-
 │   └── [other headers]
-
 ├── src/
-
 │   ├── main.cpp            # Entry point
-
 │   ├── editor_core.cpp     # Core functionality
-
 │   ├── lua_api.cpp         # Lua bindings
-
 │   ├── gtk_editor.cpp      # GTK implementation
-
 │   └── [other .cpp files]
-
 ├── tests/
-
 ├── examples/
-
 ├── scripts/
-
 └── documentation/
-
 ```
 
-
-
 ## Done
 
 - ✅ **Phase 1-5**: Complete Emacs-like core functionality
-
 - ✅ **Phase 6 Core**: GTK4 frontend with text rendering
-
 - ✅ **Phase 7 Optimization**: Render caching and performance tuning
-
 - ✅ **Phase 8 Mouse**: Full mouse support (click-move, scroll, drag-select)
-
 - ✅ **Phase 9 Advanced UI**: Implemented Context Menus (right-click) and Hover Tooltips
-
 - ✅ **Input System**: Full keyboard input with modifiers working
-
 - ✅ **Cursor System**: Emacs-style blinking block cursor with color inversion
-
 - ✅ **Text Editing**: Basic insertion, deletion, movement operations working
-
 - ✅ **Keybinding System**: Fixed conflicts, C-n, C-p, C-f, C-b, C-a, C-e, C-s, C-k working
-
 - ✅ **Clean Exit**: Fixed shutdown crashes and memory issues
-
 - ✅ **Syntax Highlighting**: Face system with Pango rendering
-
 - ✅ **Text-Cursor Alignment**: Fixed text rendering offset issues
-
 - ✅ **Scrolling System**: Full vertical and horizontal scrolling with Page Up/Down support
-
 - ✅ **Kill Ring System**: Complete implementation with push/yank/yank-pop operations
-
 - ✅ **Mark & Region**: Full mark/region system in Buffer (set_mark, deactivate_mark, get_region)
-
 - ✅ **Buffer Management**: Complete buffer switching, listing, and management in EditorCore
-
 - ✅ **Core Emacs Features**: Registers, keyboard macros, rectangles, kill operations
-
 - ✅ **GTK Segfault Fix**: Resolved double-free on exit with proper cleanup handling
-
 - ✅ **GTK Window Splits**: Dynamic container layout with Gtk::Paned matching EditorCore's window tree
-
 - ✅ **Window Split Crash Fix**: Resolved window split crashes and text rendering issues
-
 - ✅ **Multi-Window Text Rendering**: Fixed text rendering in split windows with proper font metrics
-
 - ✅ **Split Window UI Polish**: Fixed modeline display, focus stability, and split ratio calculations
-
 - ✅ **Window Split Freeze Fix**: Resolved GTK signal_realize callback issues causing freezes during splits
-
 - ✅ **Split Window Cursor Fix**: Fixed cursor movement to work in focused window rather than original active window
-
 - ⚠️ **Focus Jumping Partial Fix**: Identified spurious next_window() calls but focus still jumps during typing
-
 - ✅ **Split Ratio Improvement**: Implemented initial split ratio using signal_map with fallback
-
 - ✅ **Modeline**: Implemented detailed Emacs-like modeline with active/inactive states, flags, percentage, and mode display
-
 - ✅ **Minibuffer Polish**: Implemented Tab completion (buffers/files), command history (up/down), and kill-buffer logic
+- ✅ **GTK Text Rendering**: Refactored to use Pango::AttrList for correct multi-font and styled text rendering.
+- ✅ **GTK Cursor**: Fixed cursor positioning for variable width fonts using Pango layout metrics.
+- ✅ **GTK Region**: Implemented visual highlighting for selected regions (mark/point).
 
-
-
-## Todo  
-
-
+## Todo
 
 1. **Lua API Enhancements**: Expose core C++ functionality to Lua.
-
-
-
    - ⚠️ Exposing Clipboard functionality: Encountered compilation issues with GTKmm clipboard API and `std::erase` ambiguity. Needs further investigation.
 
-
-
 2. **Plugin Management**: Implement dynamic loading and lifecycle management of Lua plugins.
 
-
-
 3. **Lua Debugging**: Integrate basic debugging support for Lua scripts.
 
-
-
-
-
-
-
 ## Technical Debt/Notes
 
 - **Lua Bridge**: The lua_api.cpp contains the critical C++/Lua boundary code
-
 - **GTK Threading**: All GTK operations must stay on main thread
-
 - **Memory Management**: Using RAII and smart pointers throughout C++ code
-
 - **Face System**: Themes are fully integrated with Pango text rendering
-
 - **Cursor Implementation**: Blinking timer with 500ms intervals, proper cleanup on exit
-
 - **Scrolling Architecture**: Viewport system with 3-line vertical and 5-column horizontal margins
-
 - **Build System**: CMake-based with proper dependency management
-
 - **Testing**: Unit test framework in place for core components
-
-
+- **Rendering Performance**: Rendering cache was temporarily removed during Pango refactor. Monitor performance on large buffers.
 
 ## Current Focus
 
-**Phase 10 Plugin System enhancements**:
-
-- Enhancing Lua API with clipboard functions
-
-- Exposing Window scrolling to Lua scripts
+**Phase 11: UI Polish**:
+- Testing multi-font support.
+- Polishing selection visuals.

+ 171 - 58
src/gtk_editor.cpp

@@ -141,6 +141,62 @@ public:
 
 private:
     EditorCore* core_;
+
+    void apply_face_attributes(Pango::AttrList& attr_list, const FaceAttributes& face, int start_index, int end_index) {
+        if (start_index >= end_index) return;
+
+        // Foreground
+        if (face.foreground) {
+            auto attr = Pango::Attribute::create_attr_foreground(
+                face.foreground->r * 257, face.foreground->g * 257, face.foreground->b * 257);
+            attr.set_start_index(start_index);
+            attr.set_end_index(end_index);
+            attr_list.insert(attr);
+        }
+        // Background
+        if (face.background) {
+            auto attr = Pango::Attribute::create_attr_background(
+                face.background->r * 257, face.background->g * 257, face.background->b * 257);
+            attr.set_start_index(start_index);
+            attr.set_end_index(end_index);
+            attr_list.insert(attr);
+        }
+        // Font Family
+        if (face.family) {
+            auto attr = Pango::Attribute::create_attr_family(*face.family);
+            attr.set_start_index(start_index);
+            attr.set_end_index(end_index);
+            attr_list.insert(attr);
+        }
+        // Weight
+        if (face.weight) {
+            Pango::Weight w = Pango::Weight::NORMAL;
+            if (*face.weight == FontWeight::Bold) w = Pango::Weight::BOLD;
+            else if (*face.weight == FontWeight::Light) w = Pango::Weight::LIGHT;
+            auto attr = Pango::Attribute::create_attr_weight(w);
+            attr.set_start_index(start_index);
+            attr.set_end_index(end_index);
+            attr_list.insert(attr);
+        }
+        // Slant/Style
+        if (face.slant) {
+            Pango::Style s = Pango::Style::NORMAL;
+            if (*face.slant == FontSlant::Italic) s = Pango::Style::ITALIC;
+            else if (*face.slant == FontSlant::Oblique) s = Pango::Style::OBLIQUE;
+            auto attr = Pango::Attribute::create_attr_style(s);
+            attr.set_start_index(start_index);
+            attr.set_end_index(end_index);
+            attr_list.insert(attr);
+        }
+        // Underline
+        if (face.underline && *face.underline) {
+            auto attr = Pango::Attribute::create_attr_underline(Pango::Underline::SINGLE);
+            attr.set_start_index(start_index);
+            attr.set_end_index(end_index);
+            attr_list.insert(attr);
+        }
+    }
+
     Glib::RefPtr<Gtk::Application> app_;
     Gtk::Window* window_ = nullptr; // Store window pointer for widget access only (not lifetime management)
     // Input Mode State
@@ -658,17 +714,14 @@ protected:
         if (!core_ || !window) return;
 
         const auto cursor = window->cursor();
+        const auto& buffer = window->buffer();
+        auto theme = core_->active_theme();
 
         // Fill background
-        auto theme = core_->active_theme();
         Color bg = theme ? theme->get_bg_color(ThemeElement::Background) : Color(0, 0, 0);
-        
         cr->set_source_rgb(bg.r / 255.0, bg.g / 255.0, bg.b / 255.0);
         cr->paint();
 
-        // Create temporary layout for metrics if needed (using widget context is better but this works with cache)
-        // Note: We use widget->create_pango_layout() for cached lines to persist them
-        
         // Ensure metrics are initialized
         if (!font_initialized_) {
              auto layout = widget->create_pango_layout("m");
@@ -698,25 +751,25 @@ protected:
         int editor_lines = is_main_window ? std::max(0, visible_lines - 2) : std::max(0, visible_lines - 1);
         window->set_viewport_size(visible_cols, editor_lines);
 
+        // Region/Mark Calculation
+        std::optional<Range> selection_range;
+        if (buffer.has_active_mark() && buffer.mark()) {
+             selection_range = buffer.get_region(window->cursor());
+        }
+
         // Get default foreground color from theme
         Color fg = theme ? theme->get_fg_color(ThemeElement::Normal) : Color(255, 255, 255);
-        cr->set_source_rgb(fg.r / 255.0, fg.g / 255.0, fg.b / 255.0);
 
-        // Render visible lines using cache
-        const auto& buffer = window->buffer();
+        // Render visible lines
         auto [start_line, end_line] = window->visible_line_range();
         int horizontal_offset = window->viewport().horizontal_offset;
         
-        // Get or create window cache
-        WindowCache& cache = render_cache_[window.get()];
-        if (cache.lines.size() < static_cast<size_t>(editor_lines)) {
-            cache.lines.resize(editor_lines);
-        }
-
         for (int screen_y = 0; screen_y < editor_lines; ++screen_y) {
             if (start_line + screen_y >= end_line) break;
             
             size_t buffer_line_idx = start_line + screen_y;
+            if (buffer_line_idx >= buffer.line_count()) break;
+
             const auto& line_text = buffer.line(buffer_line_idx);
 
             // Apply horizontal scrolling
@@ -725,61 +778,120 @@ protected:
                 visible_text = line_text.substr(horizontal_offset);
             }
             
-            // Check cache
-            LineCache& line_cache = cache.lines[screen_y];
-            if (!line_cache.layout || line_cache.text != visible_text) {
-                // Update cache
-                if (!line_cache.layout) {
-                    line_cache.layout = widget->create_pango_layout(visible_text);
-                    line_cache.layout->set_font_description(font_desc_);
-                } else {
-                    line_cache.layout->set_text(visible_text);
+            // Create Layout
+            auto layout = widget->create_pango_layout(visible_text);
+            layout->set_font_description(font_desc_);
+
+            // Create Attribute List
+            Pango::AttrList attr_list;
+
+            // 1. Apply Syntax Highlighting
+            const auto& styles = buffer.get_line_styles(buffer_line_idx);
+            for (const auto& style : styles) {
+                if (theme) {
+                    if (auto face = theme->get_face(style.attr.face_name)) {
+                         int start = static_cast<int>(style.range.start.column) - horizontal_offset;
+                         int end = static_cast<int>(style.range.end.column) - horizontal_offset;
+                         
+                         start = std::max(0, start);
+                         end = std::min(static_cast<int>(visible_text.length()), end);
+                         
+                         if (start < end) {
+                             apply_face_attributes(attr_list, *face, start, end);
+                         }
+                    }
                 }
-                line_cache.text = visible_text;
             }
 
+            // 2. Apply Region/Selection Highlight
+            if (selection_range) {
+                 if (buffer_line_idx >= selection_range->start.line && buffer_line_idx <= selection_range->end.line) {
+                     size_t sel_start_col = (buffer_line_idx == selection_range->start.line) ? selection_range->start.column : 0;
+                     size_t sel_end_col = (buffer_line_idx == selection_range->end.line) ? selection_range->end.column : line_text.length();
+                     
+                     int start = static_cast<int>(sel_start_col) - horizontal_offset;
+                     int end = static_cast<int>(sel_end_col) - horizontal_offset;
+                     
+                     start = std::max(0, start);
+                     end = std::min(static_cast<int>(visible_text.length()), end);
+                     
+                     if (start < end) {
+                         if (auto region_face = theme->get_face("region")) {
+                              apply_face_attributes(attr_list, *region_face, start, end);
+                         } else {
+                              // Fallback: Standard selection blue/gray
+                              auto attr = Pango::Attribute::create_attr_background(0x4444, 0x4444, 0x4444);
+                              attr.set_start_index(start);
+                              attr.set_end_index(end);
+                              attr_list.insert(attr);
+                         }
+                     }
+                 }
+            }
+
+            layout->set_attributes(attr_list);
+
             // Render text at proper position
             double text_x = PADDING_LEFT;
             double text_y = PADDING_TOP + screen_y * line_height_;
             cr->move_to(text_x, text_y);
-            line_cache.layout->show_in_cairo_context(cr);
-        }
-
-        // Render Cursor (reuse last layout if possible, or create temp)
-        bool should_show_cursor = (window == core_->active_window()) && cursor_visible_;
-        if (should_show_cursor && 
-            cursor.line >= static_cast<size_t>(start_line) && 
-            cursor.line < static_cast<size_t>(end_line)) {
             
-            int screen_y = cursor.line - start_line;
-            double cursor_y = PADDING_TOP + screen_y * line_height_;
-            double cursor_screen_x = PADDING_LEFT + (static_cast<int>(cursor.column) - horizontal_offset) * char_width_;
+            cr->set_source_rgb(fg.r / 255.0, fg.g / 255.0, fg.b / 255.0);
+            layout->show_in_cairo_context(cr);
             
-            if (cursor_screen_x >= PADDING_LEFT && cursor_screen_x < (width - PADDING_RIGHT)) {
-                size_t buffer_line_idx = cursor.line;
-                const auto& cursor_line_text = buffer.line(buffer_line_idx);
-                char cursor_char = (cursor.column < cursor_line_text.length()) ? cursor_line_text[cursor.column] : ' ';
-                
-                Color cursor_bg = theme ? theme->get_fg_color(ThemeElement::Normal) : Color(255, 255, 255);
-                cr->set_source_rgb(cursor_bg.r / 255.0, cursor_bg.g / 255.0, cursor_bg.b / 255.0);
-                cr->rectangle(cursor_screen_x, cursor_y, char_width_, line_height_);
-                cr->fill();
-                
-                if (cursor_char != '\0' && cursor_char != ' ') {
-                    Color cursor_fg = theme ? theme->get_bg_color(ThemeElement::Background) : Color(0, 0, 0);
-                    cr->set_source_rgb(cursor_fg.r / 255.0, cursor_fg.g / 255.0, cursor_fg.b / 255.0);
-                    
-                    // Just create a temp layout for the cursor char - it's small and changes often
-                    auto cursor_layout = Pango::Layout::create(cr);
-                    cursor_layout->set_font_description(font_desc_);
-                    cursor_layout->set_text(std::string(1, cursor_char));
-                    
-                    cr->move_to(cursor_screen_x, cursor_y);
-                    cursor_layout->show_in_cairo_context(cr);
-                }
+            // Render Cursor
+            bool should_show_cursor = (window == core_->active_window()) && cursor_visible_;
+            if (should_show_cursor && buffer_line_idx == cursor.line) {
+                 int cursor_idx = static_cast<int>(cursor.column) - horizontal_offset;
+                 
+                 Pango::Rectangle pos;
+                 if (cursor_idx < 0) {
+                     // Out of view
+                 } else if (cursor_idx > static_cast<int>(visible_text.length())) {
+                      // Past end of line
+                      pos = layout->index_to_pos(visible_text.length()); 
+                      int diff = cursor_idx - visible_text.length();
+                      if (diff > 0) {
+                           pos.set_x(pos.get_x() + diff * char_width_ * PANGO_SCALE);
+                      }
+                 } else {
+                     pos = layout->index_to_pos(cursor_idx);
+                 }
+
+                 double cursor_screen_x = PADDING_LEFT + (pos.get_x() / (double)PANGO_SCALE);
+                 
+                 if (cursor_screen_x >= PADDING_LEFT && cursor_screen_x < (width - PADDING_RIGHT)) {
+                     // Determine cursor width
+                     double cur_width = char_width_;
+                     if (cursor_idx < static_cast<int>(visible_text.length())) {
+                          Pango::Rectangle next_pos;
+                          next_pos = layout->index_to_pos(cursor_idx + 1);
+                          cur_width = (next_pos.get_x() - pos.get_x()) / (double)PANGO_SCALE;
+                     }
+                     
+                     // Draw Cursor Block
+                     Color cursor_bg = fg;
+                     cr->set_source_rgb(cursor_bg.r / 255.0, cursor_bg.g / 255.0, cursor_bg.b / 255.0);
+                     cr->rectangle(cursor_screen_x, text_y, cur_width, line_height_);
+                     cr->fill();
+                     
+                     // Draw Character Inverted
+                     if (cursor_idx < static_cast<int>(visible_text.length())) {
+                         char cursor_char = visible_text[cursor_idx];
+                         cr->set_source_rgb(bg.r / 255.0, bg.g / 255.0, bg.b / 255.0);
+                         
+                         auto cursor_layout = widget->create_pango_layout(std::string(1, cursor_char));
+                         cursor_layout->set_font_description(font_desc_);
+                         // We should ideally copy attributes here too, but it's complex.
+                         // Defaulting to base font is acceptable for the inverted character.
+                         
+                         cr->move_to(cursor_screen_x, text_y);
+                         cursor_layout->show_in_cairo_context(cr);
+                     }
+                 }
             }
         }
-        
+
         // Use a temporary layout for modeline/minibuffer as they are dynamic and not part of the main text grid
         auto temp_layout = Pango::Layout::create(cr);
         temp_layout->set_font_description(font_desc_);
@@ -791,6 +903,7 @@ protected:
         }
     }
 
+
     // Render modeline for a specific window
     void render_modeline_for_window(const Cairo::RefPtr<Cairo::Context>& cr, int width, int height, 
                                    const Glib::RefPtr<Pango::Layout>& layout, std::shared_ptr<Window> window) {