|
@@ -10,6 +10,16 @@
|
|
|
|
|
|
|
|
namespace lumacs {
|
|
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 {
|
|
class GtkEditor : public IEditorView {
|
|
|
public:
|
|
public:
|
|
|
GtkEditor() : core_(nullptr) {}
|
|
GtkEditor() : core_(nullptr) {}
|
|
@@ -18,34 +28,12 @@ public:
|
|
|
void init() override {
|
|
void init() override {
|
|
|
// Initialize GTK application
|
|
// Initialize GTK application
|
|
|
app_ = Gtk::Application::create("org.lumacs.editor");
|
|
app_ = Gtk::Application::create("org.lumacs.editor");
|
|
|
|
|
+ app_->signal_activate().connect(sigc::mem_fun(*this, &GtkEditor::on_activate));
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
void run() override {
|
|
void run() override {
|
|
|
- // Create main window
|
|
|
|
|
- auto window = std::make_shared<Gtk::Window>();
|
|
|
|
|
- window->set_title("Lumacs - GTK4");
|
|
|
|
|
- window->set_default_size(1024, 768);
|
|
|
|
|
-
|
|
|
|
|
- // Create drawing area for text rendering
|
|
|
|
|
- drawing_area_ = Gtk::make_managed<Gtk::DrawingArea>();
|
|
|
|
|
- drawing_area_->set_draw_func(sigc::mem_fun(*this, &GtkEditor::on_draw));
|
|
|
|
|
-
|
|
|
|
|
- // Add to window
|
|
|
|
|
- window->set_child(*drawing_area_);
|
|
|
|
|
-
|
|
|
|
|
- // Input handling
|
|
|
|
|
- auto controller = Gtk::EventControllerKey::create();
|
|
|
|
|
- controller->signal_key_pressed().connect(sigc::mem_fun(*this, &GtkEditor::on_key_pressed), false);
|
|
|
|
|
- window->add_controller(controller);
|
|
|
|
|
-
|
|
|
|
|
- // Store reference to keep window alive
|
|
|
|
|
- window_ = window;
|
|
|
|
|
-
|
|
|
|
|
- // Connect tick callback for animations/cursor blinking (optional, mostly for redraw requests)
|
|
|
|
|
- // For now, we rely on event-based redraws
|
|
|
|
|
-
|
|
|
|
|
- // Run the application
|
|
|
|
|
- app_->run(*window);
|
|
|
|
|
|
|
+ // Run the application's event loop
|
|
|
|
|
+ app_->run();
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
void handle_editor_event(EditorEvent event) override {
|
|
void handle_editor_event(EditorEvent event) override {
|
|
@@ -55,7 +43,7 @@ public:
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
if (event == EditorEvent::Quit) {
|
|
if (event == EditorEvent::Quit) {
|
|
|
- if (window_) window_->close();
|
|
|
|
|
|
|
+ app_->quit(); // Quit the application gracefully
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -66,8 +54,34 @@ public:
|
|
|
private:
|
|
private:
|
|
|
EditorCore* core_;
|
|
EditorCore* core_;
|
|
|
Glib::RefPtr<Gtk::Application> app_;
|
|
Glib::RefPtr<Gtk::Application> app_;
|
|
|
- std::shared_ptr<Gtk::Window> window_;
|
|
|
|
|
|
|
+ // Window is managed by Gtk::Application
|
|
|
Gtk::DrawingArea* drawing_area_ = nullptr;
|
|
Gtk::DrawingArea* drawing_area_ = nullptr;
|
|
|
|
|
+ double char_width_ = 0;
|
|
|
|
|
+ double line_height_ = 0;
|
|
|
|
|
+ double ascent_ = 0;
|
|
|
|
|
+
|
|
|
|
|
+protected:
|
|
|
|
|
+ void on_activate() {
|
|
|
|
|
+ // Create main window and associate with the application
|
|
|
|
|
+ auto window = Glib::RefPtr<LumacsWindow>(new LumacsWindow(app_));
|
|
|
|
|
+
|
|
|
|
|
+ // Create drawing area for text rendering
|
|
|
|
|
+ drawing_area_ = Gtk::make_managed<Gtk::DrawingArea>();
|
|
|
|
|
+ drawing_area_->set_draw_func(sigc::mem_fun(*this, &GtkEditor::on_draw));
|
|
|
|
|
+ drawing_area_->set_focusable(true);
|
|
|
|
|
+
|
|
|
|
|
+ // Add to window
|
|
|
|
|
+ window->set_child(*drawing_area_);
|
|
|
|
|
+
|
|
|
|
|
+ // Input handling
|
|
|
|
|
+ auto controller = Gtk::EventControllerKey::create();
|
|
|
|
|
+ controller->signal_key_pressed().connect(sigc::mem_fun(*this, &GtkEditor::on_key_pressed), false);
|
|
|
|
|
+ drawing_area_->add_controller(controller);
|
|
|
|
|
+
|
|
|
|
|
+ // Show window
|
|
|
|
|
+ window->present();
|
|
|
|
|
+ drawing_area_->grab_focus();
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
// Rendering
|
|
// Rendering
|
|
|
void on_draw(const Cairo::RefPtr<Cairo::Context>& cr, int width, int height) {
|
|
void on_draw(const Cairo::RefPtr<Cairo::Context>& cr, int width, int height) {
|
|
@@ -85,30 +99,128 @@ private:
|
|
|
Pango::FontDescription font_desc("Monospace 12");
|
|
Pango::FontDescription font_desc("Monospace 12");
|
|
|
layout->set_font_description(font_desc);
|
|
layout->set_font_description(font_desc);
|
|
|
|
|
|
|
|
- // Render visible lines
|
|
|
|
|
- // TODO: Calculate visible range based on scroll and font height
|
|
|
|
|
- // For now, just render top lines
|
|
|
|
|
|
|
+ // 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;
|
|
|
|
|
|
|
|
- const auto& buffer = core_->buffer();
|
|
|
|
|
- double y = 0;
|
|
|
|
|
- int line_height = 20; // Estimate, should get from metrics
|
|
|
|
|
|
|
+ // 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() / PANGO_SCALE; // Use get_width()
|
|
|
|
|
+
|
|
|
|
|
+ // Update core's viewport size based on actual font metrics
|
|
|
|
|
+ int content_width_px = width; // Use actual width from parameter
|
|
|
|
|
+ int content_height_px = height; // Use actual height from parameter
|
|
|
|
|
+
|
|
|
|
|
+ int visible_lines = static_cast<int>(content_height_px / line_height_);
|
|
|
|
|
+ int visible_cols = static_cast<int>(content_width_px / char_width_);
|
|
|
|
|
|
|
|
- for (size_t i = 0; i < buffer.line_count() && y < height; ++i) {
|
|
|
|
|
- layout->set_text(buffer.line(i));
|
|
|
|
|
|
|
+ // Leave 1 line for minibuffer at bottom, adjust content area
|
|
|
|
|
+ int editor_lines = std::max(0, visible_lines - 1); // Reserve one line for future 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();
|
|
|
|
|
+
|
|
|
|
|
+ for (int screen_y = 0; screen_y < editor_lines && start_line + screen_y < end_line; ++screen_y) { // Use editor_lines
|
|
|
|
|
+ size_t buffer_line_idx = start_line + screen_y;
|
|
|
|
|
+ const auto& line_text = buffer.line(buffer_line_idx);
|
|
|
|
|
+
|
|
|
|
|
+ layout->set_text(line_text);
|
|
|
|
|
|
|
|
// TODO: Apply Pango Attributes based on buffer styles (Faces)
|
|
// TODO: Apply Pango Attributes based on buffer styles (Faces)
|
|
|
|
|
|
|
|
- cr->move_to(0, y);
|
|
|
|
|
|
|
+ cr->move_to(0, screen_y * line_height_ + ascent_);
|
|
|
layout->show_in_cairo_context(cr);
|
|
layout->show_in_cairo_context(cr);
|
|
|
- y += line_height;
|
|
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Render Cursor
|
|
|
|
|
+ const auto cursor = core_->cursor();
|
|
|
|
|
+ if (cursor.line >= start_line && cursor.line < end_line) {
|
|
|
|
|
+ int cursor_screen_x = static_cast<int>(cursor.column * char_width_);
|
|
|
|
|
+ int cursor_screen_y = static_cast<int>((cursor.line - start_line) * line_height_);
|
|
|
|
|
+
|
|
|
|
|
+ // Get cursor color from theme
|
|
|
|
|
+ Color cursor_color = theme ? theme->get_fg_color(ThemeElement::Cursor) : Color(255, 255, 255);
|
|
|
|
|
+ cr->set_source_rgb(cursor_color.r / 255.0, cursor_color.g / 255.0, cursor_color.b / 255.0);
|
|
|
|
|
+
|
|
|
|
|
+ // Draw a block cursor
|
|
|
|
|
+ cr->rectangle(cursor_screen_x, cursor_screen_y, char_width_, line_height_);
|
|
|
|
|
+ cr->fill();
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// Input
|
|
// Input
|
|
|
- bool on_key_pressed(guint keyval, guint keycode, Gdk::ModifierType state) {
|
|
|
|
|
- // TODO: Map GTK key events to Lumacs key names
|
|
|
|
|
- // std::string key_name = map_key(keyval, state);
|
|
|
|
|
- // core_->lua_api()->process_key(key_name);
|
|
|
|
|
|
|
+ bool on_key_pressed(guint keyval, guint /*keycode*/, Gdk::ModifierType state) {
|
|
|
|
|
+ std::string key_name;
|
|
|
|
|
+
|
|
|
|
|
+ // 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
|
|
|
|
|
+ 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_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) { // 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.
|
|
|
|
|
+ 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
|
|
|
|
|
+ key_name = std::string(1, static_cast<char>(keyval));
|
|
|
|
|
+ } else {
|
|
|
|
|
+ return false; // Unhandled key
|
|
|
|
|
+ }
|
|
|
|
|
+ break;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ 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;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ std::cout << "Key pressed: " << key_name << std::endl; // Debug print
|
|
|
|
|
+ core_->lua_api()->process_key(key_name);
|
|
|
return true;
|
|
return true;
|
|
|
}
|
|
}
|
|
|
};
|
|
};
|
|
@@ -128,4 +240,4 @@ std::unique_ptr<IEditorView> create_gtk_editor() {
|
|
|
}
|
|
}
|
|
|
} // namespace lumacs
|
|
} // namespace lumacs
|
|
|
|
|
|
|
|
-#endif
|
|
|
|
|
|
|
+#endif
|