|
@@ -0,0 +1,1440 @@
|
|
|
|
|
+#include "lumacs/gtk_editor.hpp"
|
|
|
|
|
+#include "lumacs/editor_core.hpp"
|
|
|
|
|
+#include "lumacs/lua_api.hpp"
|
|
|
|
|
+#include "lumacs/keybinding.hpp"
|
|
|
|
|
+#include "lumacs/command_system.hpp"
|
|
|
|
|
+#include <iostream>
|
|
|
|
|
+#include <filesystem>
|
|
|
|
|
+#include <vector>
|
|
|
|
|
+#include <functional> // For std::function
|
|
|
|
|
+#include <cctype> // For std::isalnum
|
|
|
|
|
+
|
|
|
|
|
+// Check if GTK is enabled in build
|
|
|
|
|
+#ifdef LUMACS_WITH_GTK
|
|
|
|
|
+#include <gtkmm.h>
|
|
|
|
|
+#include <pangomm.h>
|
|
|
|
|
+
|
|
|
|
|
+namespace lumacs {
|
|
|
|
|
+
|
|
|
|
|
+// Custom Gtk::ApplicationWindow to make constructor public
|
|
|
|
|
+class LumacsWindow : public Gtk::ApplicationWindow {
|
|
|
|
|
+public:
|
|
|
|
|
+ explicit LumacsWindow(const Glib::RefPtr<Gtk::Application>& application)
|
|
|
|
|
+ : Gtk::ApplicationWindow(application) {
|
|
|
|
|
+ set_title("Lumacs - GTK4");
|
|
|
|
|
+ set_default_size(1024, 768);
|
|
|
|
|
+ }
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+class GtkEditor : public IEditorView {
|
|
|
|
|
+public:
|
|
|
|
|
+ GtkEditor() : core_(nullptr) {}
|
|
|
|
|
+ ~GtkEditor() override {
|
|
|
|
|
+ // Disconnect cursor timer first to prevent callbacks during destruction
|
|
|
|
|
+ if (cursor_timer_connection_.connected()) {
|
|
|
|
|
+ cursor_timer_connection_.disconnect();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Clear core pointer to prevent any callbacks during GTK cleanup
|
|
|
|
|
+ core_ = nullptr;
|
|
|
|
|
+
|
|
|
|
|
+ // If we still have an app reference, try to quit gracefully
|
|
|
|
|
+ if (app_ && app_->is_registered()) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ app_->quit();
|
|
|
|
|
+ } catch (...) {
|
|
|
|
|
+ // Ignore exceptions during cleanup
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Clear widget pointers - GTK manages their lifetime
|
|
|
|
|
+ drawing_area_ = nullptr;
|
|
|
|
|
+ window_ = nullptr;
|
|
|
|
|
+ // Let app_ RefPtr be destroyed naturally
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ void init() override {
|
|
|
|
|
+ // Initialize GTK application
|
|
|
|
|
+ app_ = Gtk::Application::create("org.lumacs.editor");
|
|
|
|
|
+ app_->signal_activate().connect(sigc::mem_fun(*this, &GtkEditor::on_activate));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ void run() override {
|
|
|
|
|
+ // Run the application's event loop
|
|
|
|
|
+ app_->run();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ void handle_editor_event(EditorEvent event) override {
|
|
|
|
|
+ // Safety check during destruction
|
|
|
|
|
+ if (!core_ || !app_) return;
|
|
|
|
|
+
|
|
|
|
|
+ // Handle layout changes
|
|
|
|
|
+ if (event == EditorEvent::WindowLayoutChanged) {
|
|
|
|
|
+ rebuild_layout();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Request redraw on most events - recursively find all drawing areas
|
|
|
|
|
+ if (content_widget_) {
|
|
|
|
|
+ queue_redraw_all_windows(content_widget_);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ bool mode_changed = false;
|
|
|
|
|
+
|
|
|
|
|
+ // Handle mode switching events
|
|
|
|
|
+ if (event == EditorEvent::CommandMode) {
|
|
|
|
|
+ mode_ = Mode::Command;
|
|
|
|
|
+ mode_changed = true;
|
|
|
|
|
+ } else if (event == EditorEvent::FindFileMode) {
|
|
|
|
|
+ mode_ = Mode::FindFile;
|
|
|
|
|
+ mode_changed = true;
|
|
|
|
|
+ } else if (event == EditorEvent::BufferSwitchMode) {
|
|
|
|
|
+ mode_ = Mode::BufferSwitch;
|
|
|
|
|
+ mode_changed = true;
|
|
|
|
|
+ } else if (event == EditorEvent::KillBufferMode) {
|
|
|
|
|
+ mode_ = Mode::KillBuffer;
|
|
|
|
|
+ mode_changed = true;
|
|
|
|
|
+ } else if (event == EditorEvent::ThemeSelectionMode) {
|
|
|
|
|
+ mode_ = Mode::ThemeSelection;
|
|
|
|
|
+ mode_changed = true;
|
|
|
|
|
+ } else if (event == EditorEvent::ISearchMode) {
|
|
|
|
|
+ mode_ = Mode::ISearch;
|
|
|
|
|
+ mode_changed = true;
|
|
|
|
|
+ } else if (event == EditorEvent::Quit) {
|
|
|
|
|
+ // Disconnect timer before quitting to prevent segfault
|
|
|
|
|
+ if (cursor_timer_connection_.connected()) {
|
|
|
|
|
+ cursor_timer_connection_.disconnect();
|
|
|
|
|
+ }
|
|
|
|
|
+ // Use idle callback to quit safely after current event processing
|
|
|
|
|
+ Glib::signal_idle().connect_once([this]() {
|
|
|
|
|
+ if (app_) {
|
|
|
|
|
+ app_->quit();
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (mode_changed) {
|
|
|
|
|
+ command_buffer_.clear();
|
|
|
|
|
+ message_line_.clear();
|
|
|
|
|
+ history_index_ = minibuffer_history_.size(); // Reset history index to end (new input)
|
|
|
|
|
+ if (content_widget_) queue_redraw_all_windows(content_widget_);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ void set_core(EditorCore* core) override {
|
|
|
|
|
+ core_ = core;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Helper to recursively find and redraw all drawing areas
|
|
|
|
|
+ void queue_redraw_all_windows(Gtk::Widget* widget) {
|
|
|
|
|
+ if (!widget) return;
|
|
|
|
|
+
|
|
|
|
|
+ if (auto drawing_area = dynamic_cast<Gtk::DrawingArea*>(widget)) {
|
|
|
|
|
+ drawing_area->queue_draw();
|
|
|
|
|
+ } else if (auto paned = dynamic_cast<Gtk::Paned*>(widget)) {
|
|
|
|
|
+ if (auto start_child = paned->get_start_child()) {
|
|
|
|
|
+ queue_redraw_all_windows(start_child);
|
|
|
|
|
+ }
|
|
|
|
|
+ if (auto end_child = paned->get_end_child()) {
|
|
|
|
|
+ queue_redraw_all_windows(end_child);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+private:
|
|
|
|
|
+ EditorCore* core_;
|
|
|
|
|
+ std::shared_ptr<Window> cached_active_window_; // Cached to prevent focus jumping during redraws
|
|
|
|
|
+
|
|
|
|
|
+ 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
|
|
|
|
|
+ enum class Mode {
|
|
|
|
|
+ Normal,
|
|
|
|
|
+ Command,
|
|
|
|
|
+ FindFile,
|
|
|
|
|
+ BufferSwitch,
|
|
|
|
|
+ KillBuffer,
|
|
|
|
|
+ ThemeSelection,
|
|
|
|
|
+ ConfirmKill,
|
|
|
|
|
+ ISearch
|
|
|
|
|
+ };
|
|
|
|
|
+ Mode mode_ = Mode::Normal;
|
|
|
|
|
+ std::string command_buffer_;
|
|
|
|
|
+ std::string message_line_;
|
|
|
|
|
+ std::vector<std::string> minibuffer_history_;
|
|
|
|
|
+ size_t history_index_ = 0;
|
|
|
|
|
+ // Completion state
|
|
|
|
|
+ size_t completion_index_ = 0;
|
|
|
|
|
+ std::string last_completion_input_;
|
|
|
|
|
+
|
|
|
|
|
+ // Helper to run Lua completion
|
|
|
|
|
+ std::vector<std::string> run_completion(const std::string& mode, const std::string& input) {
|
|
|
|
|
+ if (!core_) return {};
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ std::vector<CompletionCandidate> candidates;
|
|
|
|
|
+
|
|
|
|
|
+ if (mode == "Command") {
|
|
|
|
|
+ candidates = core_->command_system().complete_command(input);
|
|
|
|
|
+ } else if (mode == "BufferSwitch" || mode == "KillBuffer") {
|
|
|
|
|
+ candidates = core_->command_system().complete_buffer_name(input);
|
|
|
|
|
+ } else if (mode == "FindFile") {
|
|
|
|
|
+ candidates = core_->command_system().complete_file_path(input);
|
|
|
|
|
+ } else if (mode == "ThemeSelection") {
|
|
|
|
|
+ candidates = core_->command_system().complete_theme_name(input);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // Fallback to Lua completion for custom modes
|
|
|
|
|
+ if (core_->lua_api()) {
|
|
|
|
|
+ sol::function func = core_->lua_api()->state()["get_completion_candidates"];
|
|
|
|
|
+ if (func.valid()) {
|
|
|
|
|
+ std::vector<std::string> old_candidates = func(mode, input);
|
|
|
|
|
+ std::vector<std::string> result;
|
|
|
|
|
+ for (const auto& candidate : old_candidates) {
|
|
|
|
|
+ result.push_back(candidate);
|
|
|
|
|
+ }
|
|
|
|
|
+ return result;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Convert CompletionCandidate to string vector
|
|
|
|
|
+ std::vector<std::string> result;
|
|
|
|
|
+ for (const auto& candidate : candidates) {
|
|
|
|
|
+ result.push_back(candidate.text);
|
|
|
|
|
+ }
|
|
|
|
|
+ return result;
|
|
|
|
|
+
|
|
|
|
|
+ } catch (const std::exception& e) {
|
|
|
|
|
+ std::cerr << "Completion error: " << e.what() << std::endl;
|
|
|
|
|
+ }
|
|
|
|
|
+ return {};
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Member variables
|
|
|
|
|
+ Gtk::DrawingArea* drawing_area_ = nullptr; // For single-window compatibility
|
|
|
|
|
+ Gtk::Widget* content_widget_ = nullptr; // Will be either drawing_area_ or a split container
|
|
|
|
|
+
|
|
|
|
|
+ // Font caching
|
|
|
|
|
+ Pango::FontDescription font_desc_;
|
|
|
|
|
+ bool font_initialized_ = false;
|
|
|
|
|
+ double char_width_ = 0;
|
|
|
|
|
+ double line_height_ = 0;
|
|
|
|
|
+ double ascent_ = 0;
|
|
|
|
|
+
|
|
|
|
|
+ // Render Caching
|
|
|
|
|
+ struct LineCache {
|
|
|
|
|
+ std::string text;
|
|
|
|
|
+ Glib::RefPtr<Pango::Layout> layout;
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ struct WindowCache {
|
|
|
|
|
+ std::vector<LineCache> lines;
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ std::map<Window*, WindowCache> render_cache_;
|
|
|
|
|
+
|
|
|
|
|
+ // Layout padding
|
|
|
|
|
+ static constexpr double PADDING_LEFT = 8.0;
|
|
|
|
|
+ static constexpr double PADDING_TOP = 8.0;
|
|
|
|
|
+ static constexpr double PADDING_RIGHT = 8.0;
|
|
|
|
|
+ static constexpr double PADDING_BOTTOM = 8.0;
|
|
|
|
|
+
|
|
|
|
|
+ // Cursor blinking
|
|
|
|
|
+ bool cursor_visible_ = true;
|
|
|
|
|
+ sigc::connection cursor_timer_connection_;
|
|
|
|
|
+
|
|
|
|
|
+protected:
|
|
|
|
|
+ void on_activate() {
|
|
|
|
|
+ // Create main window and associate with the application
|
|
|
|
|
+ // Note: The window is owned by the application through GObject reference counting
|
|
|
|
|
+ // We just keep a raw pointer for access, but don't manage its lifetime
|
|
|
|
|
+ window_ = new LumacsWindow(app_);
|
|
|
|
|
+
|
|
|
|
|
+ // Build initial layout (single window)
|
|
|
|
|
+ rebuild_layout();
|
|
|
|
|
+
|
|
|
|
|
+ // Handle window close event
|
|
|
|
|
+ window_->signal_close_request().connect([this]() -> bool {
|
|
|
|
|
+ // Cleanup before closing
|
|
|
|
|
+ if (cursor_timer_connection_.connected()) {
|
|
|
|
|
+ cursor_timer_connection_.disconnect();
|
|
|
|
|
+ }
|
|
|
|
|
+ core_ = nullptr;
|
|
|
|
|
+ drawing_area_ = nullptr;
|
|
|
|
|
+ content_widget_ = nullptr;
|
|
|
|
|
+
|
|
|
|
|
+ // Allow window to close
|
|
|
|
|
+ return false; // false means "allow close"
|
|
|
|
|
+ }, false);
|
|
|
|
|
+
|
|
|
|
|
+ // Show window
|
|
|
|
|
+ window_->present();
|
|
|
|
|
+ if (drawing_area_) {
|
|
|
|
|
+ drawing_area_->grab_focus();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Set up cursor blinking timer (500ms intervals like Emacs)
|
|
|
|
|
+ cursor_timer_connection_ = Glib::signal_timeout().connect(
|
|
|
|
|
+ sigc::mem_fun(*this, &GtkEditor::on_cursor_blink), 500
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Rendering
|
|
|
|
|
+ void on_draw(const Cairo::RefPtr<Cairo::Context>& cr, int width, int height) {
|
|
|
|
|
+ // Safety check - don't draw if core is null (during destruction)
|
|
|
|
|
+ if (!core_) return;
|
|
|
|
|
+
|
|
|
|
|
+ const auto cursor = core_->active_window()->cursor();
|
|
|
|
|
+
|
|
|
|
|
+ // 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 Pango layout
|
|
|
|
|
+ auto layout = Pango::Layout::create(drawing_area_->get_pango_context());
|
|
|
|
|
+
|
|
|
|
|
+ // Font configuration
|
|
|
|
|
+ Pango::FontDescription font_desc("Monospace 12");
|
|
|
|
|
+ layout->set_font_description(font_desc);
|
|
|
|
|
+
|
|
|
|
|
+ // Get font metrics
|
|
|
|
|
+ Pango::FontMetrics metrics = layout->get_context()->get_metrics(font_desc);
|
|
|
|
|
+ line_height_ = (double)metrics.get_height() / PANGO_SCALE;
|
|
|
|
|
+ ascent_ = (double)metrics.get_ascent() / PANGO_SCALE;
|
|
|
|
|
+
|
|
|
|
|
+ // Measure character width (for a single 'm' character)
|
|
|
|
|
+ layout->set_text("m");
|
|
|
|
|
+ Pango::Rectangle ink_rect, logical_rect;
|
|
|
|
|
+ layout->get_pixel_extents(ink_rect, logical_rect);
|
|
|
|
|
+ char_width_ = (double)logical_rect.get_width(); // Already in pixels, no PANGO_SCALE needed
|
|
|
|
|
+
|
|
|
|
|
+ // Update core's viewport size based on actual font metrics and padding
|
|
|
|
|
+ int content_width_px = width - static_cast<int>(PADDING_LEFT + PADDING_RIGHT);
|
|
|
|
|
+ int content_height_px = height - static_cast<int>(PADDING_TOP + PADDING_BOTTOM);
|
|
|
|
|
+
|
|
|
|
|
+ int visible_lines = static_cast<int>(content_height_px / line_height_);
|
|
|
|
|
+ int visible_cols = static_cast<int>(content_width_px / char_width_);
|
|
|
|
|
+
|
|
|
|
|
+ // Reserve space for modeline and minibuffer at bottom
|
|
|
|
|
+ int editor_lines = std::max(0, visible_lines - 2); // Reserve lines for modeline and minibuffer
|
|
|
|
|
+ core_->set_viewport_size(visible_cols, editor_lines);
|
|
|
|
|
+
|
|
|
|
|
+ // Get default foreground color from theme
|
|
|
|
|
+ // auto theme = core_->active_theme(); // Redundant, theme already defined
|
|
|
|
|
+ Color fg = theme ? theme->get_fg_color(ThemeElement::Normal) : Color(255, 255, 255); // Default to white
|
|
|
|
|
+ cr->set_source_rgb(fg.r / 255.0, fg.g / 255.0, fg.b / 255.0);
|
|
|
|
|
+
|
|
|
|
|
+ // Render visible lines
|
|
|
|
|
+ const auto& buffer = core_->buffer();
|
|
|
|
|
+ auto [start_line, end_line] = core_->active_window()->visible_line_range();
|
|
|
|
|
+ int horizontal_offset = core_->active_window()->viewport().horizontal_offset;
|
|
|
|
|
+
|
|
|
|
|
+ for (int screen_y = 0; screen_y < editor_lines && start_line + screen_y < end_line; ++screen_y) {
|
|
|
|
|
+ size_t buffer_line_idx = start_line + screen_y;
|
|
|
|
|
+ const auto& line_text = buffer.line(buffer_line_idx);
|
|
|
|
|
+
|
|
|
|
|
+ // Apply horizontal scrolling - show only the visible portion of the line
|
|
|
|
|
+ std::string visible_text;
|
|
|
|
|
+ if (horizontal_offset < static_cast<int>(line_text.length())) {
|
|
|
|
|
+ visible_text = line_text.substr(horizontal_offset);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ layout->set_text(visible_text);
|
|
|
|
|
+
|
|
|
|
|
+ // Render text at proper position (Cairo expects top-left, not baseline)
|
|
|
|
|
+ double text_x = PADDING_LEFT;
|
|
|
|
|
+ double text_y = PADDING_TOP + screen_y * line_height_;
|
|
|
|
|
+ cr->move_to(text_x, text_y);
|
|
|
|
|
+ layout->show_in_cairo_context(cr);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Render Cursor - Emacs-style blinking block cursor with color inversion
|
|
|
|
|
+ if (cursor_visible_ && 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_;
|
|
|
|
|
+
|
|
|
|
|
+ // Get the line text and calculate exact cursor position using Pango text measurement
|
|
|
|
|
+ size_t buffer_line_idx = cursor.line;
|
|
|
|
|
+ const auto& cursor_line_text = buffer.line(buffer_line_idx);
|
|
|
|
|
+
|
|
|
|
|
+ // Calculate the exact X position by measuring text up to cursor position
|
|
|
|
|
+ double cursor_screen_x = PADDING_LEFT + (static_cast<int>(cursor.column) - horizontal_offset) * char_width_;
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+ // Only render cursor if it's visible horizontally
|
|
|
|
|
+ if (cursor_screen_x >= PADDING_LEFT && cursor_screen_x < (width - PADDING_RIGHT)) {
|
|
|
|
|
+ // Get the character under cursor for rendering with inverted colors
|
|
|
|
|
+ char cursor_char = (cursor.column < cursor_line_text.length()) ? cursor_line_text[cursor.column] : ' ';
|
|
|
|
|
+
|
|
|
|
|
+ // Draw block cursor background (inverted background color)
|
|
|
|
|
+ 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();
|
|
|
|
|
+
|
|
|
|
|
+ // Draw the character with inverted color (background color as foreground)
|
|
|
|
|
+ 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);
|
|
|
|
|
+
|
|
|
|
|
+ layout->set_text(std::string(1, cursor_char));
|
|
|
|
|
+ cr->move_to(cursor_screen_x, cursor_y);
|
|
|
|
|
+ layout->show_in_cairo_context(cr);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Render Modeline above minibuffer
|
|
|
|
|
+ render_modeline(cr, width, height, layout);
|
|
|
|
|
+
|
|
|
|
|
+ // Render Minibuffer at bottom of screen
|
|
|
|
|
+ render_minibuffer(cr, width, height, layout);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Cursor blinking callback
|
|
|
|
|
+ bool on_cursor_blink() {
|
|
|
|
|
+ // Safety check - don't blink if core is destroyed or no drawing area
|
|
|
|
|
+ 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 {
|
|
|
|
|
+ cursor_visible_ = !cursor_visible_;
|
|
|
|
|
+ drawing_area_->queue_draw();
|
|
|
|
|
+ } catch (...) {
|
|
|
|
|
+ return false; // Stop timer on any exception
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return true; // Continue timer
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Rebuild the GTK layout to match the core's window tree
|
|
|
|
|
+ void rebuild_layout() {
|
|
|
|
|
+ if (!core_ || !window_) return;
|
|
|
|
|
+
|
|
|
|
|
+ auto root_layout = core_->root_layout();
|
|
|
|
|
+ if (!root_layout) return;
|
|
|
|
|
+
|
|
|
|
|
+ // Remove existing content
|
|
|
|
|
+ if (content_widget_) {
|
|
|
|
|
+ window_->unset_child();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Clear the drawing area reference since we're rebuilding
|
|
|
|
|
+ drawing_area_ = nullptr;
|
|
|
|
|
+
|
|
|
|
|
+ // Clear render cache to prevent stale window pointers
|
|
|
|
|
+ render_cache_.clear();
|
|
|
|
|
+
|
|
|
|
|
+ // Initialize cached active window to prevent focus jumping
|
|
|
|
|
+ cached_active_window_ = core_->active_window();
|
|
|
|
|
+
|
|
|
|
|
+ // Create new layout based on the tree
|
|
|
|
|
+ content_widget_ = create_widget_for_layout_node(root_layout);
|
|
|
|
|
+ if (content_widget_) {
|
|
|
|
|
+ window_->set_child(*content_widget_);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Create GTK widget tree from LayoutNode tree
|
|
|
|
|
+ Gtk::Widget* create_widget_for_layout_node(std::shared_ptr<LayoutNode> node) {
|
|
|
|
|
+ if (!node) return nullptr;
|
|
|
|
|
+
|
|
|
|
|
+ if (node->type == LayoutNode::Type::Leaf) {
|
|
|
|
|
+ // Create a new DrawingArea for this window
|
|
|
|
|
+ auto drawing_area = Gtk::make_managed<Gtk::DrawingArea>();
|
|
|
|
|
+
|
|
|
|
|
+ // Set up drawing for this specific window
|
|
|
|
|
+ // Use a weak reference to the window to avoid crashes if the layout is rebuilt
|
|
|
|
|
+ std::weak_ptr<Window> weak_window = node->window;
|
|
|
|
|
+ drawing_area->set_draw_func([this, weak_window, drawing_area](const Cairo::RefPtr<Cairo::Context>& cr, int width, int height) {
|
|
|
|
|
+ if (auto window = weak_window.lock()) {
|
|
|
|
|
+ draw_window(cr, width, height, window, drawing_area);
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ drawing_area->set_focusable(true);
|
|
|
|
|
+
|
|
|
|
|
+ // Add input handling
|
|
|
|
|
+ auto controller = Gtk::EventControllerKey::create();
|
|
|
|
|
+ // Use weak reference to window for key handling
|
|
|
|
|
+ std::weak_ptr<Window> weak_window_key = node->window;
|
|
|
|
|
+ controller->signal_key_pressed().connect([this, weak_window_key](guint keyval, guint keycode, Gdk::ModifierType state) -> bool {
|
|
|
|
|
+ // Ensure this window is active when it receives key input
|
|
|
|
|
+ if (auto window = weak_window_key.lock()) {
|
|
|
|
|
+ if (core_) {
|
|
|
|
|
+ core_->set_active_window(window);
|
|
|
|
|
+ cached_active_window_ = window; // Cache for rendering to prevent focus jumping
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return on_key_pressed(keyval, keycode, state);
|
|
|
|
|
+ }, false);
|
|
|
|
|
+ drawing_area->add_controller(controller);
|
|
|
|
|
+
|
|
|
|
|
+ // Add click handling to set active window explicitly and move cursor
|
|
|
|
|
+ // We use GestureClick instead of EventControllerFocus to avoid spurious focus changes
|
|
|
|
|
+ auto click_controller = Gtk::GestureClick::create();
|
|
|
|
|
+ std::weak_ptr<Window> weak_window_click = node->window;
|
|
|
|
|
+ click_controller->signal_pressed().connect([this, weak_window_click, drawing_area](int /*n_press*/, double x, double y) {
|
|
|
|
|
+ if (auto window = weak_window_click.lock()) {
|
|
|
|
|
+ // 1. Activate Window
|
|
|
|
|
+ if (core_ && core_->active_window() != window) {
|
|
|
|
|
+ core_->set_active_window(window);
|
|
|
|
|
+ cached_active_window_ = window; // Cache for rendering to prevent focus jumping
|
|
|
|
|
+ }
|
|
|
|
|
+ // IMPORTANT: Grab keyboard focus for this widget
|
|
|
|
|
+ drawing_area->grab_focus();
|
|
|
|
|
+
|
|
|
|
|
+ // 2. Move Cursor
|
|
|
|
|
+ if (auto pos = resolve_screen_pos(window, x, y)) {
|
|
|
|
|
+ window->set_cursor(*pos);
|
|
|
|
|
+ // Clear mark on simple click
|
|
|
|
|
+ window->buffer().deactivate_mark();
|
|
|
|
|
+ drawing_area->queue_draw();
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ drawing_area->add_controller(click_controller);
|
|
|
|
|
+
|
|
|
|
|
+ // Add Drag Gesture for Selection
|
|
|
|
|
+ auto drag_controller = Gtk::GestureDrag::create();
|
|
|
|
|
+ std::weak_ptr<Window> weak_window_drag = node->window;
|
|
|
|
|
+
|
|
|
|
|
+ drag_controller->signal_drag_begin().connect([this, weak_window_drag, drawing_area](double x, double y) {
|
|
|
|
|
+ if (auto window = weak_window_drag.lock()) {
|
|
|
|
|
+ if (auto pos = resolve_screen_pos(window, x, y)) {
|
|
|
|
|
+ // Set mark at start of drag
|
|
|
|
|
+ window->buffer().set_mark(*pos);
|
|
|
|
|
+ window->set_cursor(*pos);
|
|
|
|
|
+ drawing_area->queue_draw();
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ drag_controller->signal_drag_update().connect([this, weak_window_drag, drawing_area, drag_controller](double dx, double dy) {
|
|
|
|
|
+ if (auto window = weak_window_drag.lock()) {
|
|
|
|
|
+ double start_x, start_y;
|
|
|
|
|
+ if (drag_controller->get_start_point(start_x, start_y)) {
|
|
|
|
|
+ double current_x = start_x + dx;
|
|
|
|
|
+ double current_y = start_y + dy;
|
|
|
|
|
+
|
|
|
|
|
+ if (auto pos = resolve_screen_pos(window, current_x, current_y)) {
|
|
|
|
|
+ window->set_cursor(*pos);
|
|
|
|
|
+ drawing_area->queue_draw();
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ drawing_area->add_controller(drag_controller);
|
|
|
|
|
+
|
|
|
|
|
+ // Add scroll handling
|
|
|
|
|
+ auto scroll_controller = Gtk::EventControllerScroll::create();
|
|
|
|
|
+ scroll_controller->set_flags(Gtk::EventControllerScroll::Flags::VERTICAL);
|
|
|
|
|
+ std::weak_ptr<Window> weak_window_scroll = node->window;
|
|
|
|
|
+ scroll_controller->signal_scroll().connect([weak_window_scroll, drawing_area](double /*dx*/, double dy) -> bool {
|
|
|
|
|
+ if (auto window = weak_window_scroll.lock()) {
|
|
|
|
|
+ // dy is usually 1.0 or -1.0 for wheel steps
|
|
|
|
|
+ // Scroll 3 lines per step
|
|
|
|
|
+ int lines = static_cast<int>(dy * 3.0);
|
|
|
|
|
+ if (lines != 0) {
|
|
|
|
|
+ window->scroll_lines(lines);
|
|
|
|
|
+ drawing_area->queue_draw();
|
|
|
|
|
+ }
|
|
|
|
|
+ return true;
|
|
|
|
|
+ }
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }, true);
|
|
|
|
|
+ drawing_area->add_controller(scroll_controller);
|
|
|
|
|
+
|
|
|
|
|
+ // Context menus and tooltips removed per user request (Phase A.1)
|
|
|
|
|
+
|
|
|
|
|
+ // Store reference for single-window compatibility
|
|
|
|
|
+ if (!drawing_area_) {
|
|
|
|
|
+ drawing_area_ = drawing_area;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return drawing_area;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // Create a paned container for splits
|
|
|
|
|
+ Gtk::Paned* paned = nullptr;
|
|
|
|
|
+ if (node->type == LayoutNode::Type::HorizontalSplit) {
|
|
|
|
|
+ paned = Gtk::make_managed<Gtk::Paned>(Gtk::Orientation::VERTICAL);
|
|
|
|
|
+ } else { // VerticalSplit
|
|
|
|
|
+ paned = Gtk::make_managed<Gtk::Paned>(Gtk::Orientation::HORIZONTAL);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Recursively create children
|
|
|
|
|
+ auto child1 = create_widget_for_layout_node(node->child1);
|
|
|
|
|
+ auto child2 = create_widget_for_layout_node(node->child2);
|
|
|
|
|
+
|
|
|
|
|
+ if (child1) paned->set_start_child(*child1);
|
|
|
|
|
+ if (child2) paned->set_end_child(*child2);
|
|
|
|
|
+
|
|
|
|
|
+ // Set initial position based on ratio
|
|
|
|
|
+ // Use signal_map to set position when widget is ready
|
|
|
|
|
+ paned->signal_map().connect([paned, node](){
|
|
|
|
|
+ int width = paned->get_width();
|
|
|
|
|
+ int height = paned->get_height();
|
|
|
|
|
+ int size = (paned->get_orientation() == Gtk::Orientation::HORIZONTAL) ? width : height;
|
|
|
|
|
+
|
|
|
|
|
+ // Fallback if size not yet available
|
|
|
|
|
+ if (size <= 1) size = 1000; // Assume a reasonable default window size
|
|
|
|
|
+
|
|
|
|
|
+ paned->set_position(static_cast<int>(size * node->ratio));
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ return paned;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Draw a specific window (factored out from on_draw)
|
|
|
|
|
+ void draw_window(const Cairo::RefPtr<Cairo::Context>& cr, int width, int height, std::shared_ptr<Window> window, Gtk::DrawingArea* widget) {
|
|
|
|
|
+ if (!core_ || !window) return;
|
|
|
|
|
+
|
|
|
|
|
+ const auto cursor = window->cursor();
|
|
|
|
|
+ const auto& buffer = window->buffer();
|
|
|
|
|
+ auto theme = core_->active_theme();
|
|
|
|
|
+
|
|
|
|
|
+ // Fill background
|
|
|
|
|
+ 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();
|
|
|
|
|
+
|
|
|
|
|
+ // Ensure metrics are initialized
|
|
|
|
|
+ if (!font_initialized_) {
|
|
|
|
|
+ auto layout = widget->create_pango_layout("m");
|
|
|
|
|
+ font_desc_ = Pango::FontDescription("Monospace 12");
|
|
|
|
|
+ layout->set_font_description(font_desc_);
|
|
|
|
|
+
|
|
|
|
|
+ Pango::FontMetrics metrics = layout->get_context()->get_metrics(font_desc_);
|
|
|
|
|
+ line_height_ = (double)metrics.get_height() / PANGO_SCALE;
|
|
|
|
|
+ ascent_ = (double)metrics.get_ascent() / PANGO_SCALE;
|
|
|
|
|
+
|
|
|
|
|
+ Pango::Rectangle ink_rect, logical_rect;
|
|
|
|
|
+ layout->get_pixel_extents(ink_rect, logical_rect);
|
|
|
|
|
+ char_width_ = (double)logical_rect.get_width();
|
|
|
|
|
+
|
|
|
|
|
+ font_initialized_ = true;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Update window's viewport size based on actual font metrics and padding
|
|
|
|
|
+ int content_width_px = width - static_cast<int>(PADDING_LEFT + PADDING_RIGHT);
|
|
|
|
|
+ int content_height_px = height - static_cast<int>(PADDING_TOP + PADDING_BOTTOM);
|
|
|
|
|
+
|
|
|
|
|
+ int visible_lines = static_cast<int>(content_height_px / line_height_);
|
|
|
|
|
+ int visible_cols = static_cast<int>(content_width_px / char_width_);
|
|
|
|
|
+
|
|
|
|
|
+ // Reserve space for modeline (all windows) and minibuffer (main window only)
|
|
|
|
|
+ // Use cached active window to prevent focus jumping during async redraws
|
|
|
|
|
+ bool is_main_window = (window == cached_active_window_);
|
|
|
|
|
+ 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);
|
|
|
|
|
+
|
|
|
|
|
+ // Render visible lines
|
|
|
|
|
+ auto [start_line, end_line] = window->visible_line_range();
|
|
|
|
|
+ int horizontal_offset = window->viewport().horizontal_offset;
|
|
|
|
|
+
|
|
|
|
|
+ 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
|
|
|
|
|
+ std::string visible_text;
|
|
|
|
|
+ if (horizontal_offset < static_cast<int>(line_text.length())) {
|
|
|
|
|
+ visible_text = line_text.substr(horizontal_offset);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 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);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 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);
|
|
|
|
|
+
|
|
|
|
|
+ cr->set_source_rgb(fg.r / 255.0, fg.g / 255.0, fg.b / 255.0);
|
|
|
|
|
+ layout->show_in_cairo_context(cr);
|
|
|
|
|
+
|
|
|
|
|
+ // Render Cursor
|
|
|
|
|
+ // Use cached active window to prevent focus jumping during async redraws
|
|
|
|
|
+ bool should_show_cursor = (window == cached_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_);
|
|
|
|
|
+
|
|
|
|
|
+ render_modeline_for_window(cr, width, height, temp_layout, window);
|
|
|
|
|
+
|
|
|
|
|
+ if (is_main_window) {
|
|
|
|
|
+ render_minibuffer(cr, width, height, temp_layout);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+ // 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) {
|
|
|
|
|
+ if (!core_ || !window) return;
|
|
|
|
|
+
|
|
|
|
|
+ // Use cached active window to prevent focus jumping during async redraws
|
|
|
|
|
+ bool is_active = (window == cached_active_window_);
|
|
|
|
|
+
|
|
|
|
|
+ // Calculate modeline position (second line from bottom)
|
|
|
|
|
+ double modeline_y = height - (2 * line_height_) - PADDING_BOTTOM;
|
|
|
|
|
+ double modeline_x = PADDING_LEFT;
|
|
|
|
|
+
|
|
|
|
|
+ // Get theme colors
|
|
|
|
|
+ auto theme = core_->active_theme();
|
|
|
|
|
+ ThemeElement element = is_active ? ThemeElement::StatusLine : ThemeElement::StatusLineInactive;
|
|
|
|
|
+
|
|
|
|
|
+ Color bg = theme ? theme->get_bg_color(element) : (is_active ? Color(60, 60, 60) : Color(40, 40, 40));
|
|
|
|
|
+ Color fg = theme ? theme->get_fg_color(element) : (is_active ? Color(220, 220, 220) : Color(160, 160, 160));
|
|
|
|
|
+
|
|
|
|
|
+ // Draw modeline background
|
|
|
|
|
+ cr->set_source_rgb(bg.r / 255.0, bg.g / 255.0, bg.b / 255.0);
|
|
|
|
|
+ cr->rectangle(0, modeline_y, width, line_height_);
|
|
|
|
|
+ cr->fill();
|
|
|
|
|
+
|
|
|
|
|
+ // Build modeline content using ModelineManager
|
|
|
|
|
+ auto content = core_->modeline_manager().generate_content(window, is_active);
|
|
|
|
|
+
|
|
|
|
|
+ double x_offset = modeline_x;
|
|
|
|
|
+ for (const auto& chunk : content) {
|
|
|
|
|
+ Color chunk_fg = fg;
|
|
|
|
|
+
|
|
|
|
|
+ // Resolve chunk face if needed
|
|
|
|
|
+ if (theme && !chunk.face_name.empty()) {
|
|
|
|
|
+ if (auto face = theme->get_face(chunk.face_name)) {
|
|
|
|
|
+ if (face->foreground) chunk_fg = *face->foreground;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ layout->set_text(chunk.text);
|
|
|
|
|
+
|
|
|
|
|
+ // Apply attributes
|
|
|
|
|
+ Pango::AttrList attr_list;
|
|
|
|
|
+ if (theme && !chunk.face_name.empty()) {
|
|
|
|
|
+ if (auto face = theme->get_face(chunk.face_name)) {
|
|
|
|
|
+ apply_face_attributes(attr_list, *face, 0, chunk.text.length());
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ layout->set_attributes(attr_list);
|
|
|
|
|
+
|
|
|
|
|
+ cr->set_source_rgb(chunk_fg.r / 255.0, chunk_fg.g / 255.0, chunk_fg.b / 255.0);
|
|
|
|
|
+ cr->move_to(x_offset, modeline_y);
|
|
|
|
|
+ layout->show_in_cairo_context(cr);
|
|
|
|
|
+
|
|
|
|
|
+ // Advance
|
|
|
|
|
+ int w, h;
|
|
|
|
|
+ layout->get_pixel_size(w, h);
|
|
|
|
|
+ x_offset += w;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Render the modeline above minibuffer
|
|
|
|
|
+ void render_modeline(const Cairo::RefPtr<Cairo::Context>& cr, int width, int height,
|
|
|
|
|
+ const Glib::RefPtr<Pango::Layout>& layout) {
|
|
|
|
|
+ if (!core_) return;
|
|
|
|
|
+
|
|
|
|
|
+ // Calculate modeline position (second line from bottom)
|
|
|
|
|
+ double modeline_y = height - (2 * line_height_) - PADDING_BOTTOM;
|
|
|
|
|
+ double modeline_x = PADDING_LEFT;
|
|
|
|
|
+
|
|
|
|
|
+ // Get theme colors
|
|
|
|
|
+ auto theme = core_->active_theme();
|
|
|
|
|
+ Color bg = theme ? theme->get_bg_color(ThemeElement::StatusLine) : Color(40, 40, 40);
|
|
|
|
|
+ Color fg = theme ? theme->get_fg_color(ThemeElement::StatusLine) : Color(200, 200, 200);
|
|
|
|
|
+
|
|
|
|
|
+ // Draw modeline background
|
|
|
|
|
+ cr->set_source_rgb(bg.r / 255.0, bg.g / 255.0, bg.b / 255.0);
|
|
|
|
|
+ cr->rectangle(0, modeline_y, width, line_height_);
|
|
|
|
|
+ cr->fill();
|
|
|
|
|
+
|
|
|
|
|
+ // Build modeline content using ModelineManager
|
|
|
|
|
+ auto content = core_->modeline_manager().generate_content(core_->active_window(), true);
|
|
|
|
|
+
|
|
|
|
|
+ double x_offset = modeline_x;
|
|
|
|
|
+ for (const auto& chunk : content) {
|
|
|
|
|
+ Color chunk_fg = fg;
|
|
|
|
|
+ if (theme && !chunk.face_name.empty()) {
|
|
|
|
|
+ if (auto face = theme->get_face(chunk.face_name)) {
|
|
|
|
|
+ if (face->foreground) chunk_fg = *face->foreground;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ layout->set_text(chunk.text);
|
|
|
|
|
+
|
|
|
|
|
+ Pango::AttrList attr_list;
|
|
|
|
|
+ if (theme && !chunk.face_name.empty()) {
|
|
|
|
|
+ if (auto face = theme->get_face(chunk.face_name)) {
|
|
|
|
|
+ apply_face_attributes(attr_list, *face, 0, chunk.text.length());
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ layout->set_attributes(attr_list);
|
|
|
|
|
+
|
|
|
|
|
+ cr->set_source_rgb(chunk_fg.r / 255.0, chunk_fg.g / 255.0, chunk_fg.b / 255.0);
|
|
|
|
|
+ cr->move_to(x_offset, modeline_y);
|
|
|
|
|
+ layout->show_in_cairo_context(cr);
|
|
|
|
|
+
|
|
|
|
|
+ int w, h;
|
|
|
|
|
+ layout->get_pixel_size(w, h);
|
|
|
|
|
+ x_offset += w;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Render the minibuffer at bottom of screen
|
|
|
|
|
+ void render_minibuffer(const Cairo::RefPtr<Cairo::Context>& cr, int width, int height,
|
|
|
|
|
+ const Glib::RefPtr<Pango::Layout>& layout) {
|
|
|
|
|
+ if (!core_) return;
|
|
|
|
|
+
|
|
|
|
|
+ // Calculate minibuffer position (bottom line with padding)
|
|
|
|
|
+ double minibuffer_y = height - line_height_ - PADDING_BOTTOM;
|
|
|
|
|
+ double minibuffer_x = PADDING_LEFT;
|
|
|
|
|
+
|
|
|
|
|
+ // Get theme colors
|
|
|
|
|
+ auto theme = core_->active_theme();
|
|
|
|
|
+ Color bg = theme ? theme->get_bg_color(ThemeElement::Background) : Color(0, 0, 0);
|
|
|
|
|
+ Color fg = theme ? theme->get_fg_color(ThemeElement::Normal) : Color(255, 255, 255);
|
|
|
|
|
+
|
|
|
|
|
+ // Draw minibuffer background (slightly different shade)
|
|
|
|
|
+ cr->set_source_rgb(bg.r / 255.0 * 0.9, bg.g / 255.0 * 0.9, bg.b / 255.0 * 0.9);
|
|
|
|
|
+ cr->rectangle(0, minibuffer_y - 2, width, line_height_ + 4);
|
|
|
|
|
+ cr->fill();
|
|
|
|
|
+
|
|
|
|
|
+ // Draw separator line above minibuffer
|
|
|
|
|
+ cr->set_source_rgb(fg.r / 255.0 * 0.5, fg.g / 255.0 * 0.5, fg.b / 255.0 * 0.5);
|
|
|
|
|
+ cr->set_line_width(1.0);
|
|
|
|
|
+ cr->move_to(0, minibuffer_y - 2);
|
|
|
|
|
+ cr->line_to(width, minibuffer_y - 2);
|
|
|
|
|
+ cr->stroke();
|
|
|
|
|
+
|
|
|
|
|
+ // Prepare minibuffer text
|
|
|
|
|
+ std::string minibuffer_text;
|
|
|
|
|
+ if (mode_ != Mode::Normal) {
|
|
|
|
|
+ // Show appropriate prompt based on mode
|
|
|
|
|
+ switch (mode_) {
|
|
|
|
|
+ case Mode::Command:
|
|
|
|
|
+ minibuffer_text = "M-x " + command_buffer_;
|
|
|
|
|
+ break;
|
|
|
|
|
+ case Mode::FindFile:
|
|
|
|
|
+ minibuffer_text = "Find file: " + command_buffer_;
|
|
|
|
|
+ break;
|
|
|
|
|
+ case Mode::BufferSwitch:
|
|
|
|
|
+ minibuffer_text = "Switch to buffer: " + command_buffer_;
|
|
|
|
|
+ break;
|
|
|
|
|
+ case Mode::KillBuffer:
|
|
|
|
|
+ minibuffer_text = "Kill buffer: " + command_buffer_;
|
|
|
|
|
+ break;
|
|
|
|
|
+ case Mode::ThemeSelection:
|
|
|
|
|
+ minibuffer_text = "Set theme: " + command_buffer_;
|
|
|
|
|
+ break;
|
|
|
|
|
+ case Mode::ISearch:
|
|
|
|
|
+ minibuffer_text = "I-search: " + command_buffer_;
|
|
|
|
|
+ break;
|
|
|
|
|
+ default:
|
|
|
|
|
+ minibuffer_text = command_buffer_;
|
|
|
|
|
+ break;
|
|
|
|
|
+ }
|
|
|
|
|
+ } else if (!message_line_.empty()) {
|
|
|
|
|
+ // Show message in minibuffer
|
|
|
|
|
+ minibuffer_text = message_line_;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Render minibuffer text
|
|
|
|
|
+ if (!minibuffer_text.empty()) {
|
|
|
|
|
+ cr->set_source_rgb(fg.r / 255.0, fg.g / 255.0, fg.b / 255.0);
|
|
|
|
|
+ layout->set_text(minibuffer_text);
|
|
|
|
|
+ cr->move_to(minibuffer_x, minibuffer_y);
|
|
|
|
|
+ layout->show_in_cairo_context(cr);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Render minibuffer cursor if in interactive mode
|
|
|
|
|
+ if (mode_ != Mode::Normal) {
|
|
|
|
|
+ // Calculate cursor position in minibuffer
|
|
|
|
|
+ std::string prompt_text;
|
|
|
|
|
+ switch (mode_) {
|
|
|
|
|
+ case Mode::Command: prompt_text = "M-x "; break;
|
|
|
|
|
+ case Mode::FindFile: prompt_text = "Find file: "; break;
|
|
|
|
|
+ case Mode::BufferSwitch: prompt_text = "Switch to buffer: "; break;
|
|
|
|
|
+ case Mode::KillBuffer: prompt_text = "Kill buffer: "; break;
|
|
|
|
|
+ case Mode::ThemeSelection: prompt_text = "Set theme: "; break;
|
|
|
|
|
+ case Mode::ISearch: prompt_text = "I-search: "; break;
|
|
|
|
|
+ default: break;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Measure prompt + command buffer to position cursor
|
|
|
|
|
+ std::string text_to_cursor = prompt_text + command_buffer_;
|
|
|
|
|
+ layout->set_text(text_to_cursor);
|
|
|
|
|
+ Pango::Rectangle ink_rect, logical_rect;
|
|
|
|
|
+ layout->get_pixel_extents(ink_rect, logical_rect);
|
|
|
|
|
+ double cursor_x = minibuffer_x + logical_rect.get_width();
|
|
|
|
|
+
|
|
|
|
|
+ // Draw minibuffer cursor
|
|
|
|
|
+ cr->set_source_rgb(fg.r / 255.0, fg.g / 255.0, fg.b / 255.0);
|
|
|
|
|
+ cr->rectangle(cursor_x, minibuffer_y, 2.0, line_height_);
|
|
|
|
|
+ cr->fill();
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ 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;
|
|
|
|
|
+
|
|
|
|
|
+ // 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;
|
|
|
|
|
+ case GDK_KEY_Escape: key_name = "Escape"; break;
|
|
|
|
|
+ case GDK_KEY_BackSpace: key_name = "Backspace"; break;
|
|
|
|
|
+ case GDK_KEY_Delete: key_name = "Delete"; break;
|
|
|
|
|
+ case GDK_KEY_Up: key_name = "ArrowUp"; break;
|
|
|
|
|
+ case GDK_KEY_Down: key_name = "ArrowDown"; break;
|
|
|
|
|
+ case GDK_KEY_Left: key_name = "ArrowLeft"; break;
|
|
|
|
|
+ case GDK_KEY_Right: key_name = "ArrowRight"; break;
|
|
|
|
|
+ case GDK_KEY_Home: key_name = "Home"; break;
|
|
|
|
|
+ case GDK_KEY_End: key_name = "End"; break;
|
|
|
|
|
+ case GDK_KEY_Page_Up: key_name = "PageUp"; break;
|
|
|
|
|
+ case GDK_KEY_Page_Down: key_name = "PageDown"; break;
|
|
|
|
|
+ case GDK_KEY_F3: key_name = "F3"; break;
|
|
|
|
|
+ case GDK_KEY_F4: key_name = "F4"; break;
|
|
|
|
|
+ default:
|
|
|
|
|
+ // Handle printable characters
|
|
|
|
|
+ if (keyval >= GDK_KEY_a && keyval <= GDK_KEY_z) {
|
|
|
|
|
+ key_name = std::string(1, static_cast<char>(keyval));
|
|
|
|
|
+ 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) {
|
|
|
|
|
+ key_name = std::string(1, static_cast<char>(keyval));
|
|
|
|
|
+ }
|
|
|
|
|
+ break;
|
|
|
|
|
+ }
|
|
|
|
|
+ return key_name;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Helper to convert screen coordinates to buffer position
|
|
|
|
|
+ std::optional<Position> resolve_screen_pos(std::shared_ptr<Window> window, double x, double y) {
|
|
|
|
|
+ if (!window || line_height_ <= 0 || char_width_ <= 0) return std::nullopt;
|
|
|
|
|
+
|
|
|
|
|
+ int row = static_cast<int>((y - PADDING_TOP) / line_height_);
|
|
|
|
|
+ int col = static_cast<int>((x - PADDING_LEFT) / char_width_);
|
|
|
|
|
+
|
|
|
|
|
+ if (row < 0) row = 0;
|
|
|
|
|
+ if (col < 0) col = 0;
|
|
|
|
|
+
|
|
|
|
|
+ auto viewport = window->viewport();
|
|
|
|
|
+ size_t target_line = viewport.scroll_offset + row;
|
|
|
|
|
+ size_t target_col = viewport.horizontal_offset + col;
|
|
|
|
|
+
|
|
|
|
|
+ // Clamp to buffer bounds
|
|
|
|
|
+ if (target_line >= window->buffer().line_count()) {
|
|
|
|
|
+ target_line = window->buffer().line_count() - 1;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Clamp column to line length
|
|
|
|
|
+ size_t line_len = window->buffer().line(target_line).length();
|
|
|
|
|
+ if (target_col > line_len) target_col = line_len;
|
|
|
|
|
+
|
|
|
|
|
+ return Position{target_line, target_col};
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Input
|
|
|
|
|
+ bool on_key_pressed(guint keyval, guint /*keycode*/, Gdk::ModifierType state) {
|
|
|
|
|
+ // Safety check - don't process keys if core is destroyed
|
|
|
|
|
+ if (!core_) return false;
|
|
|
|
|
+
|
|
|
|
|
+ // Make cursor visible immediately when typing
|
|
|
|
|
+ cursor_visible_ = true;
|
|
|
|
|
+
|
|
|
|
|
+ // 1. Resolve the base key name
|
|
|
|
|
+ std::string key_name = resolve_key(keyval, state);
|
|
|
|
|
+ if (key_name.empty()) return false;
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+ // 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 (content_widget_) queue_redraw_all_windows(content_widget_);
|
|
|
|
|
+ return true;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (key_name == "Tab") {
|
|
|
|
|
+ std::string mode_str;
|
|
|
|
|
+ switch (mode_) {
|
|
|
|
|
+ case Mode::Command: mode_str = "Command"; break;
|
|
|
|
|
+ case Mode::BufferSwitch: mode_str = "BufferSwitch"; break;
|
|
|
|
|
+ case Mode::KillBuffer: mode_str = "KillBuffer"; break;
|
|
|
|
|
+ case Mode::FindFile: mode_str = "FindFile"; break;
|
|
|
|
|
+ case Mode::ThemeSelection: mode_str = "ThemeSelection"; break;
|
|
|
|
|
+ default: break;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (!mode_str.empty()) {
|
|
|
|
|
+ // Reset cycling if input changed (naive check, ideally tracked elsewhere)
|
|
|
|
|
+ if (command_buffer_ != last_completion_input_) {
|
|
|
|
|
+ completion_index_ = 0;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ std::vector<std::string> matches = run_completion(mode_str, command_buffer_);
|
|
|
|
|
+
|
|
|
|
|
+ // Fallback for FindFile if Lua returns nothing (temporary until Lua impl is complete)
|
|
|
|
|
+ if (matches.empty() && mode_ == Mode::FindFile) {
|
|
|
|
|
+ // Simple C++ fallback logic for FindFile could go here, or we just rely on Lua.
|
|
|
|
|
+ // For now, if Lua returns empty, we do nothing.
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (matches.empty()) {
|
|
|
|
|
+ message_line_ = "No match";
|
|
|
|
|
+ } else if (matches.size() == 1) {
|
|
|
|
|
+ command_buffer_ = matches[0];
|
|
|
|
|
+ last_completion_input_ = command_buffer_; // Update last input to match current
|
|
|
|
|
+ message_line_ = "Sole match";
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // Multiple matches
|
|
|
|
|
+ // 1. Find common prefix
|
|
|
|
|
+ std::string common = matches[0];
|
|
|
|
|
+ for (size_t i = 1; i < matches.size(); ++i) {
|
|
|
|
|
+ const std::string& s = matches[i];
|
|
|
|
|
+ size_t j = 0;
|
|
|
|
|
+ while (j < common.size() && j < s.size() && common[j] == s[j]) {
|
|
|
|
|
+ j++;
|
|
|
|
|
+ }
|
|
|
|
|
+ common = common.substr(0, j);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 2. Logic:
|
|
|
|
|
+ // If current input is shorter than prefix, complete to prefix.
|
|
|
|
|
+ // If current input IS the prefix (or longer/different), cycle.
|
|
|
|
|
+
|
|
|
|
|
+ if (command_buffer_.length() < common.length()) {
|
|
|
|
|
+ command_buffer_ = common;
|
|
|
|
|
+ completion_index_ = 0; // Reset cycling
|
|
|
|
|
+
|
|
|
|
|
+ // Show first few completions in message
|
|
|
|
|
+ std::string candidates_preview;
|
|
|
|
|
+ for (size_t i = 0; i < std::min(size_t(5), matches.size()); ++i) {
|
|
|
|
|
+ if (i > 0) candidates_preview += ", ";
|
|
|
|
|
+ candidates_preview += matches[i];
|
|
|
|
|
+ }
|
|
|
|
|
+ if (matches.size() > 5) {
|
|
|
|
|
+ candidates_preview += "...";
|
|
|
|
|
+ }
|
|
|
|
|
+ message_line_ = std::to_string(matches.size()) + " matches: " + candidates_preview;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // Cycle
|
|
|
|
|
+ command_buffer_ = matches[completion_index_ % matches.size()];
|
|
|
|
|
+ completion_index_++;
|
|
|
|
|
+
|
|
|
|
|
+ // Show current match with context
|
|
|
|
|
+ std::string current_match = matches[(completion_index_ - 1) % matches.size()];
|
|
|
|
|
+ message_line_ = "Match " + std::to_string((completion_index_ - 1) % matches.size() + 1) + "/" + std::to_string(matches.size()) + ": " + current_match;
|
|
|
|
|
+ }
|
|
|
|
|
+ last_completion_input_ = command_buffer_;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (content_widget_) queue_redraw_all_windows(content_widget_);
|
|
|
|
|
+ return true;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+ if (key_name == "Return") {
|
|
|
|
|
+ // Add to history
|
|
|
|
|
+ if (!command_buffer_.empty() && (minibuffer_history_.empty() || minibuffer_history_.back() != command_buffer_)) {
|
|
|
|
|
+ minibuffer_history_.push_back(command_buffer_);
|
|
|
|
|
+ }
|
|
|
|
|
+ history_index_ = minibuffer_history_.size();
|
|
|
|
|
+
|
|
|
|
|
+ // Execute command logic
|
|
|
|
|
+ if (mode_ == Mode::Command) {
|
|
|
|
|
+ if (command_buffer_ == "quit" || command_buffer_ == "q") {
|
|
|
|
|
+ app_->quit();
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // Use new command system
|
|
|
|
|
+ auto result = core_->command_system().execute(command_buffer_);
|
|
|
|
|
+ if (result.success) {
|
|
|
|
|
+ if (!result.message.empty()) {
|
|
|
|
|
+ message_line_ = result.message;
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ message_line_ = result.message;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ } 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";
|
|
|
|
|
+ } else if (mode_ == Mode::KillBuffer) {
|
|
|
|
|
+ if (core_->close_buffer(command_buffer_)) message_line_ = "Killed buffer";
|
|
|
|
|
+ else message_line_ = "Buffer not found";
|
|
|
|
|
+ } else if (mode_ == Mode::ThemeSelection) {
|
|
|
|
|
+ auto theme_names = core_->theme_manager().theme_names();
|
|
|
|
|
+ auto it = std::find(theme_names.begin(), theme_names.end(), command_buffer_);
|
|
|
|
|
+ if (it != theme_names.end()) {
|
|
|
|
|
+ core_->set_theme(command_buffer_);
|
|
|
|
|
+ message_line_ = "Switched to theme: " + command_buffer_;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ message_line_ = "Theme not found: " + command_buffer_;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ mode_ = Mode::Normal;
|
|
|
|
|
+ command_buffer_.clear();
|
|
|
|
|
+ if (content_widget_) queue_redraw_all_windows(content_widget_);
|
|
|
|
|
+ return true;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // History Navigation
|
|
|
|
|
+ if (key_name == "ArrowUp") {
|
|
|
|
|
+ if (history_index_ > 0) {
|
|
|
|
|
+ history_index_--;
|
|
|
|
|
+ command_buffer_ = minibuffer_history_[history_index_];
|
|
|
|
|
+ if (content_widget_) queue_redraw_all_windows(content_widget_);
|
|
|
|
|
+ }
|
|
|
|
|
+ return true;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (key_name == "ArrowDown") {
|
|
|
|
|
+ if (history_index_ < minibuffer_history_.size()) {
|
|
|
|
|
+ history_index_++;
|
|
|
|
|
+ if (history_index_ == minibuffer_history_.size()) {
|
|
|
|
|
+ command_buffer_.clear();
|
|
|
|
|
+ } else {
|
|
|
|
|
+ command_buffer_ = minibuffer_history_[history_index_];
|
|
|
|
|
+ }
|
|
|
|
|
+ if (content_widget_) queue_redraw_all_windows(content_widget_);
|
|
|
|
|
+ }
|
|
|
|
|
+ return true;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (key_name == "Backspace") {
|
|
|
|
|
+ if (!command_buffer_.empty()) command_buffer_.pop_back();
|
|
|
|
|
+ if (content_widget_) queue_redraw_all_windows(content_widget_);
|
|
|
|
|
+ return true;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Simple character input
|
|
|
|
|
+ if (key_name.length() == 1 && !is_control && !is_lumacs_meta) {
|
|
|
|
|
+ command_buffer_ += key_name;
|
|
|
|
|
+ if (content_widget_) queue_redraw_all_windows(content_widget_);
|
|
|
|
|
+ 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
|
|
|
|
|
+
|
|
|
|
|
+ KeyResult result = core_->lua_api()->process_key(key_name);
|
|
|
|
|
+
|
|
|
|
|
+ // Fallback handlers for common editing keys
|
|
|
|
|
+ if (result == KeyResult::Unbound) {
|
|
|
|
|
+ // Return - insert newline
|
|
|
|
|
+ if (key_name == "Return") {
|
|
|
|
|
+ auto cursor = core_->cursor();
|
|
|
|
|
+ core_->buffer().insert_newline(cursor);
|
|
|
|
|
+ core_->active_window()->set_cursor({cursor.line + 1, 0});
|
|
|
|
|
+ }
|
|
|
|
|
+ // Backspace - delete character
|
|
|
|
|
+ else if (key_name == "Backspace") {
|
|
|
|
|
+ auto cursor = core_->cursor();
|
|
|
|
|
+ core_->buffer().erase_char(cursor);
|
|
|
|
|
+ if (cursor.column > 0) {
|
|
|
|
|
+ core_->active_window()->set_cursor({cursor.line, cursor.column - 1});
|
|
|
|
|
+ } else if (cursor.line > 0) {
|
|
|
|
|
+ // Join with previous line
|
|
|
|
|
+ const auto& prev_line = core_->buffer().line(cursor.line - 1);
|
|
|
|
|
+ size_t prev_line_len = prev_line.length();
|
|
|
|
|
+ core_->active_window()->set_cursor({cursor.line - 1, prev_line_len});
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ // Delete - delete character forward
|
|
|
|
|
+ else if (key_name == "Delete") {
|
|
|
|
|
+ auto cursor = core_->cursor();
|
|
|
|
|
+ core_->buffer().erase_char({cursor.line, cursor.column + 1});
|
|
|
|
|
+ // No cursor movement needed for forward delete
|
|
|
|
|
+ }
|
|
|
|
|
+ // Arrow key navigation - use Window methods for proper scrolling
|
|
|
|
|
+ else if (key_name == "ArrowUp") {
|
|
|
|
|
+ core_->active_window()->move_up();
|
|
|
|
|
+ }
|
|
|
|
|
+ else if (key_name == "ArrowDown") {
|
|
|
|
|
+ core_->active_window()->move_down();
|
|
|
|
|
+ }
|
|
|
|
|
+ else if (key_name == "ArrowLeft") {
|
|
|
|
|
+ core_->active_window()->move_left();
|
|
|
|
|
+ }
|
|
|
|
|
+ else if (key_name == "ArrowRight") {
|
|
|
|
|
+ core_->active_window()->move_right();
|
|
|
|
|
+ }
|
|
|
|
|
+ // Page navigation - scroll multiple lines
|
|
|
|
|
+ else if (key_name == "PageUp") {
|
|
|
|
|
+ auto window = core_->active_window();
|
|
|
|
|
+ auto cursor = core_->cursor();
|
|
|
|
|
+ int page_size = std::max(1, window->viewport().height - 2); // Leave 2 lines overlap
|
|
|
|
|
+
|
|
|
|
|
+ // Move cursor up by page size
|
|
|
|
|
+ size_t new_line = (cursor.line >= static_cast<size_t>(page_size))
|
|
|
|
|
+ ? cursor.line - page_size
|
|
|
|
|
+ : 0;
|
|
|
|
|
+
|
|
|
|
|
+ window->set_cursor({new_line, cursor.column});
|
|
|
|
|
+ }
|
|
|
|
|
+ else if (key_name == "PageDown") {
|
|
|
|
|
+ auto window = core_->active_window();
|
|
|
|
|
+ auto cursor = core_->cursor();
|
|
|
|
|
+ int page_size = std::max(1, window->viewport().height - 2); // Leave 2 lines overlap
|
|
|
|
|
+
|
|
|
|
|
+ // Move cursor down by page size
|
|
|
|
|
+ size_t max_line = core_->buffer().line_count() - 1;
|
|
|
|
|
+ size_t new_line = std::min(cursor.line + page_size, max_line);
|
|
|
|
|
+
|
|
|
|
|
+ window->set_cursor({new_line, cursor.column});
|
|
|
|
|
+ }
|
|
|
|
|
+ // Home/End navigation
|
|
|
|
|
+ else if (key_name == "Home") {
|
|
|
|
|
+ core_->active_window()->move_to_line_start();
|
|
|
|
|
+ }
|
|
|
|
|
+ else if (key_name == "End") {
|
|
|
|
|
+ core_->active_window()->move_to_line_end();
|
|
|
|
|
+ }
|
|
|
|
|
+ // Insert printable characters if unbound
|
|
|
|
|
+ else if (key_name.size() == 1 && key_name[0] >= 32 && key_name[0] <= 126) {
|
|
|
|
|
+ auto cursor = core_->cursor();
|
|
|
|
|
+ core_->buffer().insert_char(cursor, key_name[0]);
|
|
|
|
|
+ core_->active_window()->set_cursor({cursor.line, cursor.column + 1});
|
|
|
|
|
+
|
|
|
|
|
+ // Debug cursor position
|
|
|
|
|
+ auto new_cursor = core_->cursor();
|
|
|
|
|
+ std::cerr << "[DEBUG] Inserted '" << key_name[0] << "' at (" << cursor.line << "," << cursor.column
|
|
|
|
|
+ << ") -> cursor now at (" << new_cursor.line << "," << new_cursor.column << ")" << std::endl;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Request redraw after processing input
|
|
|
|
|
+ if (content_widget_) {
|
|
|
|
|
+ queue_redraw_all_windows(content_widget_);
|
|
|
|
|
+ }
|
|
|
|
|
+ return true;
|
|
|
|
|
+ }
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+std::unique_ptr<IEditorView> create_gtk_editor() {
|
|
|
|
|
+ return std::make_unique<GtkEditor>();
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+} // namespace lumacs
|
|
|
|
|
+
|
|
|
|
|
+#else // LUMACS_WITH_GTK not defined
|
|
|
|
|
+
|
|
|
|
|
+namespace lumacs {
|
|
|
|
|
+std::unique_ptr<IEditorView> create_gtk_editor() {
|
|
|
|
|
+ std::cerr << "Error: Lumacs was built without GTK support." << std::endl;
|
|
|
|
|
+ return nullptr;
|
|
|
|
|
+}
|
|
|
|
|
+} // namespace lumacs
|
|
|
|
|
+
|
|
|
|
|
+#endif
|