|
|
@@ -4,6 +4,7 @@
|
|
|
#include "lumacs/keybinding.hpp"
|
|
|
#include "lumacs/command_system.hpp"
|
|
|
#include "lumacs/minibuffer_manager.hpp" // Include for MinibufferManager and MinibufferMode
|
|
|
+#include "lumacs/gtk_renderer.hpp" // Include for GtkRenderer
|
|
|
#include <iostream>
|
|
|
#include <filesystem>
|
|
|
#include <vector>
|
|
|
@@ -29,7 +30,7 @@ public:
|
|
|
|
|
|
class GtkEditor : public IEditorView {
|
|
|
public:
|
|
|
- GtkEditor() : core_(nullptr) {}
|
|
|
+ GtkEditor() : core_(nullptr), drawing_area_(nullptr), content_widget_(nullptr) {}
|
|
|
~GtkEditor() override {
|
|
|
// Disconnect cursor timer first to prevent callbacks during destruction
|
|
|
if (cursor_timer_connection_.connected()) {
|
|
|
@@ -49,8 +50,10 @@ public:
|
|
|
}
|
|
|
|
|
|
// Clear widget pointers - GTK manages their lifetime
|
|
|
+ // drawing_area_ is now a raw pointer managed by Gtk::make_managed, so no delete needed
|
|
|
drawing_area_ = nullptr;
|
|
|
window_ = nullptr;
|
|
|
+ content_widget_ = nullptr; // Also managed by Gtk::make_managed
|
|
|
// Let app_ RefPtr be destroyed naturally
|
|
|
}
|
|
|
|
|
|
@@ -186,96 +189,18 @@ 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)
|
|
|
- // 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_;
|
|
|
|
|
|
+ // GtkRenderer instance
|
|
|
+ std::unique_ptr<GtkRenderer> gtk_renderer_;
|
|
|
+
|
|
|
protected:
|
|
|
void on_activate() {
|
|
|
// Create main window and associate with the application
|
|
|
@@ -306,6 +231,11 @@ protected:
|
|
|
drawing_area_->grab_focus();
|
|
|
}
|
|
|
|
|
|
+ // Initialize GtkRenderer after drawing_area_ is set
|
|
|
+ if (core_ && drawing_area_) {
|
|
|
+ gtk_renderer_ = std::make_unique<GtkRenderer>(*core_, *drawing_area_);
|
|
|
+ }
|
|
|
+
|
|
|
// 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
|
|
|
@@ -315,116 +245,9 @@ protected:
|
|
|
// 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);
|
|
|
+ if (!core_ || !gtk_renderer_) return;
|
|
|
|
|
|
- // 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);
|
|
|
+ gtk_renderer_->on_draw(cr, width, height, cached_active_window_, cursor_visible_);
|
|
|
}
|
|
|
|
|
|
// Cursor blinking callback
|
|
|
@@ -465,9 +288,6 @@ protected:
|
|
|
// 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();
|
|
|
|
|
|
@@ -490,8 +310,8 @@ protected:
|
|
|
// 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);
|
|
|
+ if (auto window = weak_window.lock() && gtk_renderer_) {
|
|
|
+ gtk_renderer_->draw_window(cr, width, height, window, drawing_area, cached_active_window_, cursor_visible_);
|
|
|
}
|
|
|
});
|
|
|
|
|
|
@@ -528,11 +348,13 @@ protected:
|
|
|
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();
|
|
|
+ if (gtk_renderer_) {
|
|
|
+ if (auto pos = gtk_renderer_->resolve_screen_pos(window, x, y)) {
|
|
|
+ window->set_cursor(*pos);
|
|
|
+ // Clear mark on simple click
|
|
|
+ window->buffer().deactivate_mark();
|
|
|
+ drawing_area->queue_draw();
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
});
|
|
|
@@ -544,11 +366,13 @@ protected:
|
|
|
|
|
|
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();
|
|
|
+ if (gtk_renderer_) {
|
|
|
+ if (auto pos = gtk_renderer_->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();
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
});
|
|
|
@@ -556,11 +380,11 @@ protected:
|
|
|
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)) {
|
|
|
+ if (drag_controller->get_start_point(start_x, start_y) && gtk_renderer_) {
|
|
|
double current_x = start_x + dx;
|
|
|
double current_y = start_y + dy;
|
|
|
|
|
|
- if (auto pos = resolve_screen_pos(window, current_x, current_y)) {
|
|
|
+ if (auto pos = gtk_renderer_->resolve_screen_pos(window, current_x, current_y)) {
|
|
|
window->set_cursor(*pos);
|
|
|
drawing_area->queue_draw();
|
|
|
}
|
|
|
@@ -629,423 +453,7 @@ protected:
|
|
|
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;
|
|
|
-
|
|
|
- // Only render if minibuffer is active or a message is set
|
|
|
- if (!core_->minibuffer_manager().is_active() && core_->last_message().empty()) {
|
|
|
- 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;
|
|
|
- std::string prompt_part;
|
|
|
- std::string input_part;
|
|
|
-
|
|
|
- if (core_->minibuffer_manager().is_active()) {
|
|
|
- prompt_part = core_->minibuffer_manager().get_prompt();
|
|
|
- input_part = core_->minibuffer_manager().get_input_buffer();
|
|
|
- minibuffer_text = prompt_part + input_part;
|
|
|
- } else if (!core_->last_message().empty()) {
|
|
|
- minibuffer_text = core_->last_message();
|
|
|
- }
|
|
|
-
|
|
|
- // 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);
|
|
|
-
|
|
|
- // Apply face attributes for prompt if active
|
|
|
- if (core_->minibuffer_manager().is_active()) {
|
|
|
- Pango::AttrList attr_list;
|
|
|
- if (auto face = theme->get_face("minibuffer-prompt")) {
|
|
|
- apply_face_attributes(attr_list, *face, 0, prompt_part.length());
|
|
|
- }
|
|
|
- layout->set_attributes(attr_list);
|
|
|
- }
|
|
|
-
|
|
|
- cr->move_to(minibuffer_x, minibuffer_y);
|
|
|
- layout->show_in_cairo_context(cr);
|
|
|
- }
|
|
|
-
|
|
|
- // Render minibuffer cursor if active and visible
|
|
|
- if (core_->minibuffer_manager().is_active() && cursor_visible_) {
|
|
|
- // Calculate cursor position in minibuffer
|
|
|
- layout->set_text(minibuffer_text); // Measure full text
|
|
|
- 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();
|
|
|
-
|
|
|
- // Render completion overlay if applicable
|
|
|
- auto current_completion = core_->minibuffer_manager().get_current_completion();
|
|
|
- if (current_completion && input_part != *current_completion) {
|
|
|
- std::string completion_suffix = current_completion->substr(input_part.length());
|
|
|
- if (!completion_suffix.empty()) {
|
|
|
- auto completion_layout = Pango::Layout::create(cr);
|
|
|
- completion_layout->set_font_description(font_desc_);
|
|
|
- completion_layout->set_text(minibuffer_text + completion_suffix);
|
|
|
-
|
|
|
- Pango::AttrList completion_attr_list;
|
|
|
- if (auto face = theme->get_face("minibuffer-completion")) { // Assuming a face for completion
|
|
|
- apply_face_attributes(completion_attr_list, *face, minibuffer_text.length(), completion_suffix.length());
|
|
|
- } else {
|
|
|
- // Fallback: dimmed foreground
|
|
|
- Color dim_fg = fg;
|
|
|
- dim_fg.r = static_cast<unsigned char>(dim_fg.r * 0.7);
|
|
|
- dim_fg.g = static_cast<unsigned char>(dim_fg.g * 0.7);
|
|
|
- dim_fg.b = static_cast<unsigned char>(dim_fg.b * 0.7);
|
|
|
- auto attr = Pango::Attribute::create_attr_foreground(dim_fg.r * 257, dim_fg.g * 257, dim_fg.b * 257);
|
|
|
- attr.set_start_index(minibuffer_text.length());
|
|
|
- attr.set_end_index(minibuffer_text.length() + completion_suffix.length());
|
|
|
- completion_attr_list.insert(attr);
|
|
|
- }
|
|
|
- completion_layout->set_attributes(completion_attr_list);
|
|
|
-
|
|
|
- cr->set_source_rgb(fg.r / 255.0, fg.g / 255.0, fg.b / 255.0);
|
|
|
- cr->move_to(minibuffer_x, minibuffer_y);
|
|
|
- completion_layout->show_in_cairo_context(cr);
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
+
|
|
|
std::string resolve_key(guint keyval, Gdk::ModifierType state) {
|
|
|
// Handle modifier keys
|
|
|
unsigned int state_uint = static_cast<unsigned int>(state);
|
|
|
@@ -1078,8 +486,6 @@ protected:
|
|
|
} 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));
|
|
|
}
|
|
|
@@ -1088,32 +494,6 @@ protected:
|
|
|
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
|