#include "lumacs/gtk_renderer.hpp" #include "lumacs/editor_core.hpp" #include "lumacs/minibuffer_manager.hpp" // For MinibufferManager and MinibufferMode #include // For std::max, std::min #include // Added for debug logging namespace lumacs { GtkRenderer::GtkRenderer(EditorCore& core, Gtk::DrawingArea& main_drawing_area) : core_(core), main_drawing_area_(main_drawing_area) { initialize_font_metrics(); // Initialize font metrics once during construction } void GtkRenderer::initialize_font_metrics() { if (font_initialized_) return; // Use a minimal string for layout creation auto layout = main_drawing_area_.create_pango_layout(" "); 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; // Get character width more precisely using index_to_pos for a known monospace char layout->set_text("M"); // Use a single, representative character Pango::Rectangle pos0 = layout->index_to_pos(0); // Position at index 0 (before 'M') Pango::Rectangle pos1 = layout->index_to_pos(1); // Position at index 1 (after 'M') char_width_ = (double)(pos1.get_x() - pos0.get_x()) / PANGO_SCALE; spdlog::debug("GtkRenderer font metrics: line_height={}, char_width={}", line_height_, char_width_); font_initialized_ = true; } void GtkRenderer::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); } } std::optional GtkRenderer::resolve_screen_pos(std::shared_ptr window, double x, double y) { if (!window || line_height_ <= 0 || char_width_ <= 0) return std::nullopt; int row = static_cast((y - PADDING_TOP) / line_height_); int col = static_cast((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}; } void GtkRenderer::on_draw(const Cairo::RefPtr& cr, int width, int height, std::shared_ptr active_window_cache, bool cursor_visible_state) { // Safety check - don't draw if core has no active theme (during destruction) if (!core_.theme_manager().active_theme()) return; // Fill background of the entire drawing area auto theme = core_.theme_manager().active_theme(); Color bg = theme->get_bg_color(ThemeElement::Background); cr->set_source_rgb(bg.r / 255.0, bg.g / 255.0, bg.b / 255.0); cr->paint(); // Get the active window from core to render it auto active_window = core_.active_window(); if (!active_window) return; // Render the active window (main editor area) draw_window(cr, width, height, active_window, &main_drawing_area_, active_window_cache, cursor_visible_state); // Use a temporary layout for modeline/minibuffer as they are dynamic auto temp_layout = Pango::Layout::create(main_drawing_area_.get_pango_context()); temp_layout->set_font_description(font_desc_); // Render modeline render_modeline_for_window(cr, width, height, active_window, active_window_cache); // Render minibuffer render_minibuffer(cr, width, height); } void GtkRenderer::draw_window(const Cairo::RefPtr& cr, int width, int height, std::shared_ptr window, Gtk::DrawingArea* widget, std::shared_ptr active_window_cache, bool cursor_visible_state) { if (!core_.theme_manager().active_theme() || !window || !widget) return; // Use a temporary layout that is properly initialized auto layout = Pango::Layout::create(widget->get_pango_context()); layout->set_font_description(font_desc_); const auto cursor = window->cursor(); const auto& buffer = window->buffer(); auto theme = core_.theme_manager().active_theme(); // Fill background Color bg = theme->get_bg_color(ThemeElement::Background); cr->set_source_rgb(bg.r / 255.0, bg.g / 255.0, bg.b / 255.0); cr->paint(); int content_width_px = width - static_cast(PADDING_LEFT + PADDING_RIGHT); int content_height_px = height - static_cast(PADDING_TOP + PADDING_BOTTOM); int visible_lines = static_cast(content_height_px / line_height_); int visible_cols = static_cast(content_width_px / char_width_); // Reserve space for modeline (1 line). Minibuffer is global and rendered separately. bool is_main_window = (window == active_window_cache); 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 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->get_fg_color(ThemeElement::Normal); cr->set_source_rgb(fg.r / 255.0, fg.g / 255.0, fg.b / 255.0); // Render visible lines auto [start_line, end_line] = window->visible_line_range(); int horizontal_offset = window->viewport().horizontal_offset; auto& window_cache = render_cache_[window.get()]; 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; if (buffer_line_idx >= buffer.line_count()) break; // Safety check const auto& line_text = buffer.line(buffer_line_idx); const auto& styles = buffer.get_line_styles(buffer_line_idx); // Apply horizontal scrolling std::string visible_text; if (horizontal_offset < static_cast(line_text.length())) { visible_text = line_text.substr(horizontal_offset); } // Always set text for the layout before any further operations, especially cursor positioning layout->set_text(visible_text); double text_x = PADDING_LEFT; double text_y = PADDING_TOP + screen_y * line_height_; // Check for Region/Selection Intersection bool has_selection = false; int sel_start = 0, sel_end = 0; if (selection_range) { if (buffer_line_idx >= selection_range->start.line && buffer_line_idx <= selection_range->end.line) { has_selection = true; size_t s_col = (buffer_line_idx == selection_range->start.line) ? selection_range->start.column : 0; size_t e_col = (buffer_line_idx == selection_range->end.line) ? selection_range->end.column : line_text.length(); sel_start = static_cast(s_col) - horizontal_offset; sel_end = static_cast(e_col) - horizontal_offset; sel_start = std::max(0, sel_start); sel_end = std::min(static_cast(visible_text.length()), sel_end); if (sel_start >= sel_end) has_selection = false; } } bool use_cache = false; if (!has_selection && window_cache.count(buffer_line_idx)) { auto& entry = window_cache[buffer_line_idx]; if (entry.text_content == visible_text) { // Simple validation cr->set_source(entry.surface, text_x, text_y); cr->paint(); use_cache = true; } } if (!use_cache) { // Draw fresh // layout->set_text(visible_text); // Moved this line outside the if block Pango::AttrList attr_list; // 1. Apply Syntax Highlighting for (const auto& style : styles) { if (auto face = theme->get_face(style.attr.face_name)) { int start = static_cast(style.range.start.column) - horizontal_offset; int end = static_cast(style.range.end.column) - horizontal_offset; start = std::max(0, start); end = std::min(static_cast(visible_text.length()), end); if (start < end) { apply_face_attributes(attr_list, *face, start, end); } } } // 2. Apply Region/Selection Highlight (if any) if (has_selection) { if (auto region_face = theme->get_face("region")) { apply_face_attributes(attr_list, *region_face, sel_start, sel_end); } else { auto attr = Pango::Attribute::create_attr_background(0x4444, 0x4444, 0x4444); attr.set_start_index(sel_start); attr.set_end_index(sel_end); attr_list.insert(attr); } } layout->set_attributes(attr_list); // Render if (!has_selection) { // Render to Cache surface first int surf_width = std::max(1, static_cast(content_width_px)); auto surface = Cairo::ImageSurface::create(Cairo::Surface::Format::ARGB32, surf_width, static_cast(line_height_)); auto cr_surf = Cairo::Context::create(surface); // Clear surface cr_surf->set_operator(Cairo::Context::Operator::CLEAR); cr_surf->paint(); cr_surf->set_operator(Cairo::Context::Operator::OVER); cr_surf->set_source_rgb(fg.r / 255.0, fg.g / 255.0, fg.b / 255.0); layout->show_in_cairo_context(cr_surf); // Render layout to surface // Store LineCacheEntry entry; entry.text_content = visible_text; entry.surface = surface; entry.styles = styles; window_cache[buffer_line_idx] = entry; // Blit to screen cr->set_source(surface, text_x, text_y); cr->paint(); } else { // Render directly to screen (don't cache selected text) 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 (assuming `cursor_visible_` is managed by GtkEditor) bool cursor_visible_ = cursor_visible_state; bool is_active_window = (window == active_window_cache); if (is_active_window && cursor_visible_ && buffer_line_idx == cursor.line) { int cursor_idx = static_cast(cursor.column) - horizontal_offset; spdlog::debug("Cursor debug: Logical Curs ({},{}) | H-Offset {} | VisTextLen {} | CursorIdx {}", cursor.line, cursor.column, horizontal_offset, visible_text.length(), cursor_idx); Pango::Rectangle pos; if (cursor_idx < 0) { // Out of view spdlog::debug("Cursor debug: Cursor out of view (left)"); continue; // Don't draw if out of view } else if (cursor_idx > static_cast(visible_text.length())) { // Past end of line (after last character) pos = layout->index_to_pos(visible_text.length()); int diff = cursor_idx - static_cast(visible_text.length()); if (diff > 0) { pos.set_x(pos.get_x() + diff * char_width_ * PANGO_SCALE); } spdlog::debug("Cursor debug: Past end of line. PangoX={}, FinalPangoX={}", layout->index_to_pos(visible_text.length()).get_x(), pos.get_x()); } else { pos = layout->index_to_pos(cursor_idx); spdlog::debug("Cursor debug: Within line. PangoX={}", pos.get_x()); } double cursor_screen_x = PADDING_LEFT + (pos.get_x() / (double)PANGO_SCALE); spdlog::debug("Cursor debug: ScreenX={}, ScreenY={} | CharWidth={}, LineHeight={}", cursor_screen_x, text_y, char_width_, line_height_); 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(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(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_); cr->move_to(cursor_screen_x, text_y); cursor_layout->show_in_cairo_context(cr); } } } } // Refined Loop with Selection Logic: // (This replaces the loop in the file) // Since I cannot easily rewrite the whole loop with complex logic insertion in one go without potentially breaking, // I will rewrite the loop logic completely in the replacement string. } // Closing brace for GtkRenderer::draw_window method void GtkRenderer::render_modeline_for_window(const Cairo::RefPtr& cr, int width, int height, std::shared_ptr window, std::shared_ptr active_window_cache) { if (!core_.theme_manager().active_theme() || !window) return; // The logic for is_active needs to be passed from GtkEditor bool is_active = (window == active_window_cache); // Calculate modeline position (second line from bottom) double modeline_y = height - (2 * line_height_) - PADDING_BOTTOM; // Position above minibuffer double modeline_x = PADDING_LEFT; // Get theme colors auto theme = core_.theme_manager().active_theme(); ThemeElement element = is_active ? ThemeElement::StatusLine : ThemeElement::StatusLineInactive; Color bg = theme->get_bg_color(element); Color fg = theme->get_fg_color(element); // 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 (!chunk.face_name.empty()) { if (auto face = theme->get_face(chunk.face_name)) { if (face->foreground) chunk_fg = *face->foreground; } } // Use a temporary layout for modeline chunk auto layout = Pango::Layout::create(main_drawing_area_.get_pango_context()); layout->set_font_description(font_desc_); layout->set_text(chunk.text); // Apply attributes Pango::AttrList attr_list; if (!chunk.face_name.empty()) { if (auto face = theme->get_face(chunk.face_name)) { apply_face_attributes(attr_list, *face, 0, static_cast(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; } } void GtkRenderer::render_minibuffer(const Cairo::RefPtr& cr, int width, int height) { if (!core_.theme_manager().active_theme()) return; // Get theme colors auto theme = core_.theme_manager().active_theme(); Color bg = theme->get_bg_color(ThemeElement::Background); Color fg = theme->get_fg_color(ThemeElement::Normal); // ALWAYS draw minibuffer background to prevent it from disappearing // Clear the ENTIRE surface first to avoid artifacts 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, 0, width, height); // Clear full dedicated area 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, 0); cr->line_to(width, 0); cr->stroke(); // Only render text 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; // 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); auto layout = Pango::Layout::create(main_drawing_area_.get_pango_context()); layout->set_font_description(font_desc_); 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, static_cast(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 (assuming cursor_visible is managed by GtkEditor) bool cursor_visible_ = true; // Placeholder if (core_.minibuffer_manager().is_active() && cursor_visible_) { // Calculate cursor position in minibuffer auto layout = Pango::Layout::create(main_drawing_area_.get_pango_context()); layout->set_font_description(font_desc_); 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()) { // Calculate width of existing text to position suffix Pango::Rectangle ink_rect, logical_rect; layout->get_pixel_extents(ink_rect, logical_rect); double text_width = logical_rect.get_width(); auto completion_layout = Pango::Layout::create(main_drawing_area_.get_pango_context()); completion_layout->set_font_description(font_desc_); completion_layout->set_text(completion_suffix); // Only draw suffix Pango::AttrList completion_attr_list; if (auto face = theme->get_face("minibuffer-completion")) { apply_face_attributes(completion_attr_list, *face, 0, static_cast(completion_suffix.length())); } else { // Fallback: dimmed foreground Color dim_fg = fg; dim_fg.r = static_cast(dim_fg.r * 0.7); dim_fg.g = static_cast(dim_fg.g * 0.7); dim_fg.b = static_cast(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(0); attr.set_end_index(static_cast(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 + text_width, minibuffer_y); // Position after text completion_layout->show_in_cairo_context(cr); } } } } bool GtkRenderer::get_minibuffer_coords(int& x, int& y, int& width, int& height) const { // These coordinates are relative to the main drawing area. // The minibuffer is always at the bottom of the main drawing area. width = main_drawing_area_.get_width(); height = static_cast(line_height_ + PADDING_TOP + PADDING_BOTTOM); // Approximate minibuffer height x = 0; // Starts at the left edge y = main_drawing_area_.get_height() - height; // Position from the bottom return true; } void GtkRenderer::invalidate_cache() { render_cache_.clear(); spdlog::debug("GtkRenderer: Cache invalidated due to theme change."); } } // namespace lumacs