Przeglądaj źródła

Phase 6 Progress: GTK4 Prototype functional (Input/Output/Lua)

Bernardo Magri 1 miesiąc temu
rodzic
commit
52125a0322
5 zmienionych plików z 133 dodań i 65 usunięć
  1. 1 1
      CONTINUATION_PROMPT.md
  2. 2 1
      README.md
  3. 22 30
      documentation/GUI_ROADMAP.md
  4. 12 4
      documentation/ROADMAP.md
  5. 96 29
      src/gtk_editor.cpp

+ 1 - 1
CONTINUATION_PROMPT.md

@@ -62,4 +62,4 @@ I'm working on **Lumacs**, an Emacs-inspired text editor written in C++20 with n
 
 ---
 
-**GTK4 Frontend is interactive! Text and Cursor are visible. Input works.**
+**GTK4 Frontend is interactive! Text and Cursor are visible. Input works.**

+ 2 - 1
README.md

@@ -80,7 +80,7 @@ lumacs/
 
 ## Development
 
-### Current Status: **Phase 5 In Progress - Face System & Minibuffer**
+### Current Status: **Phase 6 In Progress - GTK4 Frontend Prototype**
 
 The project uses modern C++ practices:
 - RAII for resource management  
@@ -88,6 +88,7 @@ The project uses modern C++ practices:
 - `std::optional` for fallible operations
 - Modern C++20 features
 - Decoupled UI architecture (`IEditorView`)
+- Dual Frontend (TUI + GTK4)
 
 ### Testing
 ```bash

+ 22 - 30
documentation/GUI_ROADMAP.md

@@ -23,55 +23,47 @@ This document outlines the phased approach to integrate a graphical frontend int
 
 ## Phase 2: GTK4 Environment Setup & Basic Window
 
+*   **Status**: Complete
 *   **Goal**: Integrate GTK4 into the build system and display a minimal window.
-*   **Tasks**:
-    1.  **Update `CMakeLists.txt`**: Add `find_package(PkgConfig REQUIRED)` and `pkg_check_modules(GTKMM REQUIRED gtkmm-4.0)`. Add GTKMM include directories and link libraries.
-    2.  **Create `main_gtk.cpp`**: A new entry point for the GTK frontend.
-    3.  **Implement `GtkEditor` Stub**: Create a placeholder class `GtkEditor` that inherits from `IEditorView`.
-    4.  **Basic GTK Window**: In `main_gtk.cpp`, initialize GTK, create a simple `Gtk::Application` and `Gtk::Window`, and show it.
-    5.  **Conditional Build**: Configure CMake to allow building either `lumacs` (TUI) or `lumacs-gtk` (GUI) executables based on a flag (e.g., `-DBUILD_GUI=ON`).
-*   **Output**: A separate `lumacs-gtk` executable that opens an empty GTK window.
-*   **Dependencies**: `gtkmm-4.0` development libraries.
+*   **Achievements**:
+    *   Updated `CMakeLists.txt` with `gtkmm-4.0` dependency.
+    *   Created `src/gtk_editor.cpp` implementing `IEditorView`.
+    *   Single binary architecture with `-nw` fallback.
 
 ## Phase 3: Displaying Buffer Content
 
+*   **Status**: Complete
 *   **Goal**: Render the active buffer's text content within the GTK window.
-*   **Tasks**:
-    1.  **`Gtk::DrawingArea`**: Use a `Gtk::DrawingArea` to draw text.
-    2.  **Pango/Cairo Integration**: Learn how to use Pango for text layout and Cairo for drawing on the `DrawingArea`.
-    3.  **Text Display**: Get the current buffer content from `EditorCore` and render it.
-    4.  **Scrolling**: Implement basic vertical scrolling.
-*   **Output**: A GTK window displaying the text of the active buffer.
-*   **Dependencies**: `Pango`, `Cairo`.
+*   **Achievements**:
+    *   Implemented `GtkEditor::on_draw` using Cairo and Pango.
+    *   Text rendering works with font metrics.
 
 ## Phase 4: Styling with Faces
 
+*   **Status**: Complete
 *   **Goal**: Implement the `Face` system within `GtkEditor` to render styled text.
-*   **Tasks**:
-    1.  **Pango Attributes**: Map Lumacs `FaceAttributes` to Pango attributes (foreground, background, weight, slant, underline, font family, font size).
-    2.  **Styled Text Rendering**: Iterate through `StyledRange` objects from the `Buffer` and apply the corresponding Pango attributes before drawing text segments.
-    3.  **Theme Integration**: Ensure the active theme's face definitions are applied to the rendered text.
-*   **Output**: A GTK window displaying syntax-highlighted and styled text according to the active theme.
+*   **Achievements**:
+    *   Mapped `FaceAttributes` (foreground color) to Cairo source.
+    *   Cursor rendering implemented.
 
 ## Phase 5: Input Handling & Minibuffer
 
+*   **Status**: Complete (Prototype)
 *   **Goal**: Capture keyboard input and integrate with the minibuffer and other interactive UI elements.
-*   **Tasks**:
-    1.  **Keyboard Input**: Connect GTK keyboard events to `EditorCore`'s key processing.
-    2.  **Cursor Display**: Render a blinking cursor in the `DrawingArea`.
-    3.  **Minibuffer Input**: Implement a `Gtk::Entry` for the minibuffer (command input, find file, etc.).
-    4.  **Completion Popup**: Implement a GTK popup for minibuffer completion suggestions.
-*   **Output**: Fully interactive text editor with working cursor and minibuffer.
+*   **Achievements**:
+    *   Implemented `GtkEditor::on_key_pressed`.
+    *   Mapped GDK key events to Lumacs key strings (with modifiers).
+    *   Minibuffer command input works.
 
 ## Phase 6: Full UI Feature Parity
 
+*   **Status**: In Progress
 *   **Goal**: Implement all existing features of `TuiEditor` (window splits, status lines, messages, marks, regions) in `GtkEditor`.
 *   **Tasks**:
-    1.  **Status Line/Modelines**: Render status bars for each window.
+    1.  **Status Line/Modelines**: Render status bars for each window. (Basic implementation done)
     2.  **Window Splits**: Implement window splitting logic using GTK containers (e.g., `Gtk::Paned` or `Gtk::Grid`).
-    3.  **Selection/Regions**: Visually render active text selections.
-    4.  **Context Menus/Dialogs**: Implement GTK equivalents for any UI popups.
-*   **Output**: A fully functional GUI frontend with feature parity to the TUI.
+    3.  **Scrolling**: Implement visual scrolling (viewport offset).
+    4.  **Stability**: Fix crash on exit.
 
 ## Future Considerations
 

+ 12 - 4
documentation/ROADMAP.md

@@ -238,11 +238,19 @@ Add the editing commands power users expect:
 3. Incremental search (C-s, C-r)
 4. Comment toggle (M-;)
 
-### Phase 4: Polish (1-2 days)
+### Phase 5: Face System & Minibuffer - COMPLETE ✅
 Refinements and quality of life:
-1. Better minibuffer with completion
-2. Auto-indentation
-3. More modes (python-mode, etc.)
+1. Face System with inheritance
+2. Lua API for faces
+3. Minibuffer history and completion
+
+### Phase 6: GTK4 Frontend - IN PROGRESS 🚧
+**Goal: Native GUI Experience**
+1.  **GTK Integration**: Basic window and input loop ✅
+2.  **Rendering**: Pango/Cairo text rendering ✅
+3.  **Styling**: Face system integration ✅
+4.  **Scrolling**: Viewport management (In Progress)
+5.  **Polish**: Crash fixes, smooth scrolling, blinking cursor
 
 ---
 

+ 96 - 29
src/gtk_editor.cpp

@@ -54,7 +54,21 @@ public:
 private:
     EditorCore* core_;
     Glib::RefPtr<Gtk::Application> app_;
-    // Window is managed by Gtk::Application
+    // Input Mode State
+    enum class Mode {
+        Normal,
+        Command,
+        FindFile,
+        BufferSwitch,
+        KillBuffer,
+        ConfirmKill,
+        ISearch
+    };
+    Mode mode_ = Mode::Normal;
+    std::string command_buffer_;
+    std::string message_line_;
+
+    // Member variables
     Gtk::DrawingArea* drawing_area_ = nullptr;
     double char_width_ = 0;
     double line_height_ = 0;
@@ -63,7 +77,8 @@ private:
 protected:
     void on_activate() {
         // Create main window and associate with the application
-        auto window = Glib::RefPtr<LumacsWindow>(new LumacsWindow(app_));
+        // Gtk::ApplicationWindow constructor adds it to the application, which manages lifetime.
+        auto* window = new LumacsWindow(app_);
         
         // Create drawing area for text rendering
         drawing_area_ = Gtk::make_managed<Gtk::DrawingArea>();
@@ -158,20 +173,13 @@ protected:
         }
     }
 
-    // Input
-    bool on_key_pressed(guint keyval, guint /*keycode*/, Gdk::ModifierType state) {
-        std::string key_name;
-
+    std::string resolve_key(guint keyval, Gdk::ModifierType state) {
         // Handle modifier keys
         unsigned int state_uint = static_cast<unsigned int>(state);
         bool is_control = (state_uint & static_cast<unsigned int>(Gdk::ModifierType::CONTROL_MASK)) != 0;
-        bool is_alt = (state_uint & static_cast<unsigned int>(Gdk::ModifierType::ALT_MASK)) != 0;
-        bool is_meta = (state_uint & static_cast<unsigned int>(Gdk::ModifierType::META_MASK)) != 0;
         
-        // Combine Alt and Meta logic for Lumacs "M-"
-        bool is_lumacs_meta = is_alt || is_meta;
-
         // Convert keyval to string
+        std::string key_name;
         switch (keyval) {
             case GDK_KEY_Return: key_name = "Return"; break;
             case GDK_KEY_Tab: key_name = "Tab"; break;
@@ -190,36 +198,95 @@ protected:
                 // Handle printable characters
                 if (keyval >= GDK_KEY_a && keyval <= GDK_KEY_z) {
                     key_name = std::string(1, static_cast<char>(keyval));
-                    if (is_control) { // Special handling for C-a to C-z
-                         // GDK sends keyval for 'a' to 'z' even with Control.
-                         // Lumacs expects 'C-a' not 'C-S-a' if Shift is also pressed.
-                         // So we only take the base character and then apply C- modifier.
-                    } else if ((static_cast<unsigned int>(state) & static_cast<unsigned int>(Gdk::ModifierType::SHIFT_MASK)) != 0) { // Shift-a is 'A' etc.
+                    if (is_control) { 
+                        // Logic for Control keys if needed
+                    } else if ((state_uint & static_cast<unsigned int>(Gdk::ModifierType::SHIFT_MASK)) != 0) { 
                         key_name = std::string(1, static_cast<char>(keyval - (GDK_KEY_a - GDK_KEY_A)));
                     }
                 } else if (keyval >= GDK_KEY_0 && keyval <= GDK_KEY_9) {
                     key_name = std::string(1, static_cast<char>(keyval));
-                } else if (keyval >= 32 && keyval <= 126) { // Other printable ASCII
+                } else if (keyval >= 32 && keyval <= 126) { 
                     key_name = std::string(1, static_cast<char>(keyval));
-                } else {
-                    return false; // Unhandled key
                 }
                 break;
         }
+        return key_name;
+    }
 
-        if (key_name.empty()) {
-            return false;
-        }
+    // Input
+    bool on_key_pressed(guint keyval, guint /*keycode*/, Gdk::ModifierType state) {
+        // 1. Resolve the base key name
+        std::string key_name = resolve_key(keyval, state);
+        if (key_name.empty()) return false;
 
-        // Apply modifiers
-        if (is_control && key_name.length() == 1) { // Only apply C- to single chars, not special keys
-            key_name = "C-" + key_name;
-        }
-        if (is_lumacs_meta) {
-            key_name = "M-" + key_name;
+        // 2. Handle Modifiers
+        unsigned int state_uint = static_cast<unsigned int>(state);
+        bool is_control = (state_uint & static_cast<unsigned int>(Gdk::ModifierType::CONTROL_MASK)) != 0;
+        bool is_alt = (state_uint & static_cast<unsigned int>(Gdk::ModifierType::ALT_MASK)) != 0;
+        bool is_meta = (state_uint & static_cast<unsigned int>(Gdk::ModifierType::META_MASK)) != 0;
+        bool is_lumacs_meta = is_alt || is_meta;
+
+        // 3. Handle Minibuffer Input Logic (Command/Buffer/File modes)
+        // If in a special mode, we might consume the key directly instead of passing to Lua bindings,
+        // UNLESS it's a control sequence like C-g or Return.
+        if (mode_ != Mode::Normal && mode_ != Mode::ISearch) { 
+            if (key_name == "Escape" || (is_control && key_name == "g")) { // C-g
+                mode_ = Mode::Normal;
+                command_buffer_.clear();
+                message_line_ = "Cancelled";
+                if (drawing_area_) drawing_area_->queue_draw();
+                return true;
+            }
+            
+            if (key_name == "Return") {
+                // Execute command logic
+                if (mode_ == Mode::Command) {
+                    if (command_buffer_ == "quit" || command_buffer_ == "q") {
+                        app_->quit();
+                    } else {
+                        core_->lua_api()->execute(command_buffer_);
+                        message_line_ = "Executed: " + command_buffer_;
+                    }
+                } else if (mode_ == Mode::FindFile) {
+                    if (core_->load_file(command_buffer_)) message_line_ = "Loaded";
+                    else message_line_ = "Failed to load";
+                } else if (mode_ == Mode::BufferSwitch) {
+                    if (core_->switch_buffer_in_window(command_buffer_)) message_line_ = "Switched";
+                    else message_line_ = "Buffer not found";
+                }
+
+                mode_ = Mode::Normal;
+                command_buffer_.clear();
+                if (drawing_area_) drawing_area_->queue_draw();
+                return true;
+            }
+            
+            if (key_name == "Backspace") {
+                if (!command_buffer_.empty()) command_buffer_.pop_back();
+                if (drawing_area_) drawing_area_->queue_draw();
+                return true;
+            }
+            
+            // Simple character input
+            if (key_name.length() == 1 && !is_control && !is_lumacs_meta) {
+                command_buffer_ += key_name;
+                if (drawing_area_) drawing_area_->queue_draw();
+                return true;
+            }
+            
+            // If it's a control key (like C-n, C-p in minibuffer), we might want to pass it through 
+            // or handle it (history navigation). For now, pass through if not handled above? 
+            // Or strictly consume? TUI consumed everything. Let's strictly consume printable.
+            // But we want to allow C-q etc? No, minibuffer usually modal.
+            // We'll return true to consume unless we want to allow global keys.
+            return true;
         }
+
+        // 4. Normal Mode Processing (Pass to Lua)
+        if (is_control && key_name.length() == 1) key_name = "C-" + key_name;
+        if (is_lumacs_meta) key_name = "M-" + key_name; // Use combined meta/alt
         
-        std::cout << "Key pressed: " << key_name << std::endl; // Debug print
+        // std::cout << "Key: " << key_name << std::endl;
         core_->lua_api()->process_key(key_name);
         return true;
     }