|
|
@@ -3,39 +3,73 @@
|
|
|
#include "lumacs/minibuffer_manager.hpp" // For MinibufferManager and MinibufferMode
|
|
|
#include "lumacs/keybinding.hpp" // For KeyBindingManager
|
|
|
#include "lumacs/message_formatter.hpp"
|
|
|
-#include <algorithm> // For std::max, std::min
|
|
|
+#include <algorithm> // For std::max, std::min, std::round
|
|
|
+#include <cmath> // For std::ceil, std::floor, std::round
|
|
|
#include <spdlog/spdlog.h> // Added for debug logging
|
|
|
+#include <glibmm/ustring.h> // For UTF-8 handling
|
|
|
|
|
|
namespace lumacs {
|
|
|
|
|
|
GtkRenderer::GtkRenderer(EditorCore& core, Gtk::Widget& context_widget)
|
|
|
: core_(core), context_widget_(context_widget) {
|
|
|
+ // Get font size from config
|
|
|
+ font_size_ = core_.config().get<int>("font_size", 12);
|
|
|
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 = context_widget_.create_pango_layout(" ");
|
|
|
- font_desc_ = Pango::FontDescription("Monospace 12");
|
|
|
+ font_desc_ = Pango::FontDescription("Monospace " + std::to_string(font_size_));
|
|
|
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;
|
|
|
-
|
|
|
+
|
|
|
+ // Configure font options for crisp text rendering
|
|
|
+ auto pango_context = layout->get_context();
|
|
|
+ Cairo::FontOptions font_options;
|
|
|
+ font_options.set_antialias(Cairo::ANTIALIAS_SUBPIXEL);
|
|
|
+ font_options.set_hint_style(Cairo::FontOptions::HintStyle::MEDIUM);
|
|
|
+ font_options.set_hint_metrics(Cairo::FontOptions::HintMetrics::ON);
|
|
|
+ font_options.set_subpixel_order(Cairo::SUBPIXEL_ORDER_RGB);
|
|
|
+ pango_context->set_cairo_font_options(font_options);
|
|
|
+
|
|
|
+ Pango::FontMetrics metrics = pango_context->get_metrics(font_desc_);
|
|
|
+
|
|
|
+ // Calculate line height with proper spacing - use ascent + descent + small padding
|
|
|
+ double ascent = static_cast<double>(metrics.get_ascent()) / PANGO_SCALE;
|
|
|
+ double descent = static_cast<double>(metrics.get_descent()) / PANGO_SCALE;
|
|
|
+
|
|
|
+ // Round up line height to avoid fractional pixel issues
|
|
|
+ line_height_ = std::ceil(ascent + descent + 2.0); // +2 for minimal line spacing
|
|
|
+ ascent_ = ascent;
|
|
|
+ descent_ = descent;
|
|
|
+
|
|
|
// 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;
|
|
|
+ char_width_ = static_cast<double>(pos1.get_x() - pos0.get_x()) / PANGO_SCALE;
|
|
|
+
|
|
|
+ // Get HiDPI scale factor
|
|
|
+ scale_factor_ = context_widget_.get_scale_factor();
|
|
|
+
|
|
|
+ spdlog::debug("GtkRenderer font metrics: line_height={}, ascent={}, descent={}, char_width={}, scale={}",
|
|
|
+ line_height_, ascent_, descent_, char_width_, scale_factor_);
|
|
|
|
|
|
- spdlog::debug("GtkRenderer font metrics: line_height={}, char_width={}", line_height_, char_width_);
|
|
|
-
|
|
|
font_initialized_ = true;
|
|
|
}
|
|
|
|
|
|
+size_t GtkRenderer::char_to_byte_index(const std::string& str, size_t char_pos) {
|
|
|
+ // Convert character position to byte index for UTF-8 string
|
|
|
+ Glib::ustring ustr(str);
|
|
|
+ if (char_pos >= ustr.length()) {
|
|
|
+ return str.length();
|
|
|
+ }
|
|
|
+ // Get the byte offset for the character at char_pos
|
|
|
+ auto it = ustr.begin();
|
|
|
+ std::advance(it, char_pos);
|
|
|
+ return it.base() - ustr.begin().base();
|
|
|
+}
|
|
|
+
|
|
|
void GtkRenderer::apply_face_attributes(Pango::AttrList& attr_list, const FaceAttributes& face, int start_index, int end_index) {
|
|
|
if (start_index >= end_index) return;
|
|
|
|
|
|
@@ -166,13 +200,21 @@ void GtkRenderer::draw_window(const Cairo::RefPtr<Cairo::Context>& cr, int width
|
|
|
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_);
|
|
|
+ // Use floor for visible lines/cols to avoid rendering partial lines
|
|
|
+ int visible_lines = static_cast<int>(std::floor(content_height_px / line_height_));
|
|
|
+ int visible_cols = static_cast<int>(std::floor(content_width_px / char_width_));
|
|
|
|
|
|
// Reserve space for modeline (1 line). Minibuffer is now separate.
|
|
|
int editor_lines = std::max(0, visible_lines - 1);
|
|
|
window->set_viewport_size(visible_cols, editor_lines);
|
|
|
|
|
|
+ // Visual line mode (soft word wrap)
|
|
|
+ bool visual_line_mode = core_.visual_line_mode();
|
|
|
+ if (visual_line_mode) {
|
|
|
+ layout->set_width(content_width_px * PANGO_SCALE);
|
|
|
+ layout->set_wrap(Pango::WrapMode::WORD_CHAR);
|
|
|
+ }
|
|
|
+
|
|
|
// Region/Mark Calculation
|
|
|
std::optional<Range> selection_range;
|
|
|
if (buffer.has_active_mark() && buffer.mark()) {
|
|
|
@@ -185,28 +227,53 @@ void GtkRenderer::draw_window(const Cairo::RefPtr<Cairo::Context>& cr, int width
|
|
|
|
|
|
// Render visible lines
|
|
|
auto [start_line, end_line] = window->visible_line_range();
|
|
|
- int horizontal_offset = window->viewport().horizontal_offset;
|
|
|
-
|
|
|
+ int horizontal_offset = visual_line_mode ? 0 : 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
|
|
|
+
|
|
|
+ // In visual line mode, we need to track screen rows consumed
|
|
|
+ int current_screen_row = 0;
|
|
|
+ size_t buffer_line_idx = start_line;
|
|
|
+
|
|
|
+ while (current_screen_row < editor_lines && buffer_line_idx < buffer.line_count()) {
|
|
|
+ // Legacy loop condition for non-wrapped mode
|
|
|
+ if (!visual_line_mode && buffer_line_idx >= end_line) break;
|
|
|
+
|
|
|
const auto& line_text = buffer.line(buffer_line_idx);
|
|
|
const auto& styles = buffer.get_line_styles(buffer_line_idx);
|
|
|
-
|
|
|
- // Apply horizontal scrolling
|
|
|
+
|
|
|
+ // Apply horizontal scrolling (disabled in visual line mode)
|
|
|
std::string visible_text;
|
|
|
if (horizontal_offset < static_cast<int>(line_text.length())) {
|
|
|
visible_text = line_text.substr(horizontal_offset);
|
|
|
+ } else if (visual_line_mode) {
|
|
|
+ visible_text = line_text; // Show full line in visual mode
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
// 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_;
|
|
|
-
|
|
|
+
|
|
|
+ // Calculate how many screen rows this line takes (for visual line mode)
|
|
|
+ int line_screen_rows = 1;
|
|
|
+ if (visual_line_mode && !visible_text.empty()) {
|
|
|
+ int layout_height = 0;
|
|
|
+ layout->get_pixel_size(layout_height, layout_height);
|
|
|
+ auto iter = layout->get_iter();
|
|
|
+ int line_count = 0;
|
|
|
+ do {
|
|
|
+ line_count++;
|
|
|
+ } while (iter.next_line());
|
|
|
+ line_screen_rows = std::max(1, line_count);
|
|
|
+ }
|
|
|
+
|
|
|
+ // Calculate pixel-aligned positions to avoid blurry text
|
|
|
+ // Round to whole pixels to prevent subpixel rendering artifacts
|
|
|
+ double text_x = std::round(PADDING_LEFT);
|
|
|
+ double text_y = std::round(PADDING_TOP + current_screen_row * line_height_);
|
|
|
+
|
|
|
+ // For wrapped lines, we need the full layout height
|
|
|
+ double effective_line_height = visual_line_mode ? (line_screen_rows * line_height_) : line_height_;
|
|
|
+
|
|
|
// Check for Region/Selection Intersection
|
|
|
bool has_selection = false;
|
|
|
int sel_start = 0, sel_end = 0;
|
|
|
@@ -230,9 +297,9 @@ void GtkRenderer::draw_window(const Cairo::RefPtr<Cairo::Context>& cr, int width
|
|
|
bool use_cache = false;
|
|
|
if (!has_selection && window_cache.count(buffer_line_idx)) {
|
|
|
auto& entry = window_cache[buffer_line_idx];
|
|
|
- // Validate both text content AND styles to ensure cache is still valid
|
|
|
- // (styles change on theme switch or syntax highlighting updates)
|
|
|
- if (entry.text_content == visible_text && entry.styles == styles) {
|
|
|
+ // Validate text content, styles, AND scale factor to ensure cache is still valid
|
|
|
+ // (styles change on theme switch, scale changes on display switch)
|
|
|
+ if (entry.text_content == visible_text && entry.styles == styles && entry.scale == scale_factor_) {
|
|
|
cr->set_source(entry.surface, text_x, text_y);
|
|
|
cr->paint();
|
|
|
use_cache = true;
|
|
|
@@ -274,37 +341,80 @@ void GtkRenderer::draw_window(const Cairo::RefPtr<Cairo::Context>& cr, int width
|
|
|
layout->set_attributes(attr_list);
|
|
|
|
|
|
// Render
|
|
|
- if (!has_selection) {
|
|
|
- // Render to Cache surface first
|
|
|
- int surf_width = std::max(1, static_cast<int>(content_width_px));
|
|
|
-
|
|
|
- auto surface = Cairo::ImageSurface::create(Cairo::Surface::Format::ARGB32, surf_width, static_cast<int>(line_height_)); auto cr_surf = Cairo::Context::create(surface);
|
|
|
-
|
|
|
+ if (!has_selection && !visual_line_mode) {
|
|
|
+ // Render to Cache surface first (disabled in visual line mode for simplicity)
|
|
|
+ // Create HiDPI-aware surface
|
|
|
+ int surf_width = std::max(1, static_cast<int>(std::ceil(content_width_px * scale_factor_)));
|
|
|
+ int surf_height = static_cast<int>(std::ceil(effective_line_height * scale_factor_));
|
|
|
+
|
|
|
+ auto surface = Cairo::ImageSurface::create(Cairo::Surface::Format::ARGB32, surf_width, surf_height);
|
|
|
+ // Set device scale for HiDPI - this makes Cairo render at higher resolution
|
|
|
+ cairo_surface_set_device_scale(surface->cobj(), scale_factor_, scale_factor_);
|
|
|
+
|
|
|
+ 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
|
|
|
+
|
|
|
+ // Store with scale factor for cache validation
|
|
|
LineCacheEntry entry;
|
|
|
entry.text_content = visible_text;
|
|
|
entry.surface = surface;
|
|
|
entry.styles = styles;
|
|
|
+ entry.scale = scale_factor_;
|
|
|
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)
|
|
|
+ // Render directly to screen (don't cache selected text or wrapped lines)
|
|
|
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);
|
|
|
}
|
|
|
+ } else if (visual_line_mode) {
|
|
|
+ // In visual line mode, always render directly (simpler for wrapped text)
|
|
|
+ Pango::AttrList attr_list;
|
|
|
+
|
|
|
+ // Apply Syntax Highlighting
|
|
|
+ for (const auto& style : styles) {
|
|
|
+ if (auto face = theme->get_face(style.attr.face_name)) {
|
|
|
+ int start = static_cast<int>(style.range.start.column);
|
|
|
+ int end = static_cast<int>(style.range.end.column);
|
|
|
+
|
|
|
+ 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);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // Apply selection 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);
|
|
|
+
|
|
|
+ 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)
|
|
|
@@ -368,13 +478,11 @@ void GtkRenderer::draw_window(const Cairo::RefPtr<Cairo::Context>& cr, int width
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
- }
|
|
|
- // 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.
|
|
|
|
|
|
+ // Advance to next line
|
|
|
+ buffer_line_idx++;
|
|
|
+ current_screen_row += line_screen_rows;
|
|
|
+ }
|
|
|
|
|
|
} // Closing brace for GtkRenderer::draw_window method
|
|
|
|
|
|
@@ -386,43 +494,44 @@ void GtkRenderer::render_modeline_for_window(const Cairo::RefPtr<Cairo::Context>
|
|
|
|
|
|
// The logic for is_active needs to be passed from GtkEditor
|
|
|
bool is_active = (window == active_window_cache);
|
|
|
-
|
|
|
+
|
|
|
// Calculate modeline position (bottom of the drawing area)
|
|
|
// Minibuffer is separate, so modeline is at the very bottom
|
|
|
- double modeline_y = height - line_height_ - PADDING_BOTTOM;
|
|
|
- double modeline_x = PADDING_LEFT;
|
|
|
-
|
|
|
+ // Use pixel-aligned coordinates to avoid blurry text
|
|
|
+ double modeline_y = std::round(height - line_height_ - PADDING_BOTTOM);
|
|
|
+ double modeline_x = std::round(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(context_widget_.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()) {
|
|
|
@@ -435,11 +544,11 @@ void GtkRenderer::render_modeline_for_window(const Cairo::RefPtr<Cairo::Context>
|
|
|
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
|
|
|
+
|
|
|
+ // Advance - keep pixel aligned
|
|
|
int w, h;
|
|
|
layout->get_pixel_size(w, h);
|
|
|
- x_offset += w;
|
|
|
+ x_offset = std::round(x_offset + w);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -485,22 +594,23 @@ void GtkRenderer::render_minibuffer(const Cairo::RefPtr<Cairo::Context>& cr, int
|
|
|
|
|
|
// If showing extended echo area lines, render them first (above the main minibuffer line)
|
|
|
if (showing_echo_lines) {
|
|
|
- double hint_y = PADDING_TOP;
|
|
|
+ double hint_y = std::round(PADDING_TOP);
|
|
|
cr->set_source_rgb(fg.r / 255.0, fg.g / 255.0, fg.b / 255.0);
|
|
|
|
|
|
for (const auto& line : echo_area_lines) {
|
|
|
auto layout = Pango::Layout::create(context_widget_.get_pango_context());
|
|
|
layout->set_font_description(font_desc_);
|
|
|
layout->set_text(line);
|
|
|
- cr->move_to(PADDING_LEFT, hint_y);
|
|
|
+ cr->move_to(std::round(PADDING_LEFT), hint_y);
|
|
|
layout->show_in_cairo_context(cr);
|
|
|
- hint_y += line_height_;
|
|
|
+ hint_y = std::round(hint_y + line_height_);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// Calculate minibuffer position (bottom line with padding)
|
|
|
- double minibuffer_y = height - line_height_ - PADDING_BOTTOM;
|
|
|
- double minibuffer_x = PADDING_LEFT;
|
|
|
+ // Use pixel-aligned coordinates to avoid blurry text
|
|
|
+ double minibuffer_y = std::round(height - line_height_ - PADDING_BOTTOM);
|
|
|
+ double minibuffer_x = std::round(PADDING_LEFT);
|
|
|
|
|
|
// Prepare minibuffer text
|
|
|
std::string minibuffer_text;
|
|
|
@@ -587,78 +697,81 @@ void GtkRenderer::render_minibuffer(const Cairo::RefPtr<Cairo::Context>& cr, int
|
|
|
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 - in real usage passed or managed
|
|
|
- if (core_.minibuffer_manager().is_active()) { // Assuming always visible or managed externally
|
|
|
- // Calculate cursor position in minibuffer
|
|
|
- // text is prompt + input. Cursor is relative to input start.
|
|
|
- size_t cursor_idx = prompt_part.length() + core_.minibuffer_manager().get_cursor_position();
|
|
|
-
|
|
|
+ // Render minibuffer cursor if active and visible
|
|
|
+ if (core_.minibuffer_manager().is_active()) {
|
|
|
+ // Calculate cursor position in minibuffer using proper UTF-8 byte indexing
|
|
|
+ // Cursor position from minibuffer is in characters, we need byte index for Pango
|
|
|
+ size_t cursor_char_pos = core_.minibuffer_manager().get_cursor_position();
|
|
|
+
|
|
|
+ // Convert character positions to byte indices for UTF-8 strings
|
|
|
+ // prompt_part byte length + byte offset within input_part for cursor position
|
|
|
+ size_t prompt_byte_len = prompt_part.length(); // Prompt is typically ASCII, bytes == chars
|
|
|
+ size_t cursor_byte_idx = prompt_byte_len + char_to_byte_index(input_part, cursor_char_pos);
|
|
|
+
|
|
|
auto layout = Pango::Layout::create(context_widget_.get_pango_context());
|
|
|
layout->set_font_description(font_desc_);
|
|
|
- layout->set_text(minibuffer_text);
|
|
|
-
|
|
|
- Pango::Rectangle pos = layout->index_to_pos(static_cast<int>(cursor_idx));
|
|
|
- double cursor_screen_x = minibuffer_x + (pos.get_x() / (double)PANGO_SCALE);
|
|
|
-
|
|
|
- // Draw Cursor Block
|
|
|
- // Use char_width_ as default, or measure actual char
|
|
|
+ layout->set_text(minibuffer_text);
|
|
|
+
|
|
|
+ Pango::Rectangle pos = layout->index_to_pos(static_cast<int>(cursor_byte_idx));
|
|
|
+ double cursor_screen_x = std::round(minibuffer_x + (pos.get_x() / static_cast<double>(PANGO_SCALE)));
|
|
|
+
|
|
|
+ // Draw Cursor Block - measure actual char width using Pango
|
|
|
double cur_width = char_width_;
|
|
|
- // If not at end, measure actual char width
|
|
|
- if (cursor_idx < minibuffer_text.length()) {
|
|
|
- Pango::Rectangle next_pos = layout->index_to_pos(static_cast<int>(cursor_idx + 1));
|
|
|
- cur_width = (next_pos.get_x() - pos.get_x()) / (double)PANGO_SCALE;
|
|
|
+ size_t text_byte_len = minibuffer_text.length();
|
|
|
+ if (cursor_byte_idx < text_byte_len) {
|
|
|
+ // Get next character's byte index for width calculation
|
|
|
+ Glib::ustring utext(minibuffer_text);
|
|
|
+ size_t total_char_pos = Glib::ustring(prompt_part).length() + cursor_char_pos;
|
|
|
+ if (total_char_pos < utext.length()) {
|
|
|
+ size_t next_byte_idx = char_to_byte_index(minibuffer_text, total_char_pos + 1);
|
|
|
+ Pango::Rectangle next_pos = layout->index_to_pos(static_cast<int>(next_byte_idx));
|
|
|
+ cur_width = (next_pos.get_x() - pos.get_x()) / static_cast<double>(PANGO_SCALE);
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
cr->set_source_rgb(fg.r / 255.0, fg.g / 255.0, fg.b / 255.0);
|
|
|
cr->rectangle(cursor_screen_x, minibuffer_y, cur_width, line_height_);
|
|
|
cr->fill();
|
|
|
-
|
|
|
+
|
|
|
// Draw Character Inverted (if not at end)
|
|
|
- if (cursor_idx < minibuffer_text.length()) {
|
|
|
- // Extract char at cursor
|
|
|
- // Note: simple indexing works for ASCII, but for UTF-8 need careful extraction.
|
|
|
- // Pango layout handles rendering, we just need to render that one char at the pos.
|
|
|
- // Simpler: Set color to BG and re-render the layout clipped?
|
|
|
- // Or just render single char?
|
|
|
- // Let's reuse the main editor approach: create layout for single char.
|
|
|
- // But we need the exact byte string for the character at cursor_idx (utf8 aware).
|
|
|
-
|
|
|
- // For now, let's rely on std::string indexing assuming ASCII for command/paths mostly,
|
|
|
- // or use Pango to iterate?
|
|
|
- // Glib::ustring is better for UTF8.
|
|
|
- // Let's assume standard char for now to fix the main visual bug.
|
|
|
- char cursor_char = minibuffer_text[cursor_idx];
|
|
|
-
|
|
|
- cr->set_source_rgb(bg.r / 255.0, bg.g / 255.0, bg.b / 255.0);
|
|
|
- auto cursor_layout = context_widget_.create_pango_layout(std::string(1, cursor_char));
|
|
|
- cursor_layout->set_font_description(font_desc_);
|
|
|
- cr->move_to(cursor_screen_x, minibuffer_y);
|
|
|
- cursor_layout->show_in_cairo_context(cr);
|
|
|
+ if (cursor_byte_idx < text_byte_len) {
|
|
|
+ // Extract the UTF-8 character at cursor position using Glib::ustring
|
|
|
+ Glib::ustring utext(minibuffer_text);
|
|
|
+ size_t total_char_pos = Glib::ustring(prompt_part).length() + cursor_char_pos;
|
|
|
+ if (total_char_pos < utext.length()) {
|
|
|
+ // Get single character as string
|
|
|
+ Glib::ustring cursor_char_str = utext.substr(total_char_pos, 1);
|
|
|
+
|
|
|
+ cr->set_source_rgb(bg.r / 255.0, bg.g / 255.0, bg.b / 255.0);
|
|
|
+ auto cursor_layout = context_widget_.create_pango_layout(cursor_char_str);
|
|
|
+ cursor_layout->set_font_description(font_desc_);
|
|
|
+ cr->move_to(cursor_screen_x, minibuffer_y);
|
|
|
+ cursor_layout->show_in_cairo_context(cr);
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
// Render completion overlay if applicable
|
|
|
auto current_completion = core_.minibuffer_manager().get_current_completion();
|
|
|
if (current_completion && input_part != *current_completion) {
|
|
|
- // Safety check: ensure input_part length is valid for substring extraction
|
|
|
- if (input_part.length() <= current_completion->length()) {
|
|
|
- std::string completion_suffix = current_completion->substr(input_part.length());
|
|
|
+ // Use character-based length comparison for proper UTF-8 handling
|
|
|
+ Glib::ustring u_input(input_part);
|
|
|
+ Glib::ustring u_completion(*current_completion);
|
|
|
+ if (u_input.length() <= u_completion.length()) {
|
|
|
+ Glib::ustring completion_suffix = u_completion.substr(u_input.length());
|
|
|
if (!completion_suffix.empty()) {
|
|
|
- // Position completion at the end of the input text (cursor might be in middle)
|
|
|
- // completion suffix appends to the END of input.
|
|
|
-
|
|
|
- // Calculate position of the end of input
|
|
|
- size_t input_end_idx = prompt_part.length() + input_part.length();
|
|
|
- Pango::Rectangle end_pos = layout->index_to_pos(static_cast<int>(input_end_idx));
|
|
|
- double completion_x = minibuffer_x + (end_pos.get_x() / (double)PANGO_SCALE);
|
|
|
+ // Calculate position of the end of input using byte index
|
|
|
+ size_t input_end_byte_idx = prompt_byte_len + input_part.length();
|
|
|
+ Pango::Rectangle end_pos = layout->index_to_pos(static_cast<int>(input_end_byte_idx));
|
|
|
+ double completion_x = std::round(minibuffer_x + (end_pos.get_x() / static_cast<double>(PANGO_SCALE)));
|
|
|
|
|
|
auto completion_layout = Pango::Layout::create(context_widget_.get_pango_context());
|
|
|
completion_layout->set_font_description(font_desc_);
|
|
|
- completion_layout->set_text(completion_suffix); // Only draw suffix
|
|
|
-
|
|
|
+ completion_layout->set_text(completion_suffix);
|
|
|
+
|
|
|
Pango::AttrList completion_attr_list;
|
|
|
+ std::string suffix_str = completion_suffix;
|
|
|
if (auto face = theme->get_face("minibuffer-completion")) {
|
|
|
- apply_face_attributes(completion_attr_list, *face, 0, static_cast<int>(completion_suffix.length()));
|
|
|
+ apply_face_attributes(completion_attr_list, *face, 0, static_cast<int>(suffix_str.length()));
|
|
|
} else {
|
|
|
// Fallback: dimmed foreground
|
|
|
Color dim_fg = fg;
|
|
|
@@ -667,13 +780,13 @@ void GtkRenderer::render_minibuffer(const Cairo::RefPtr<Cairo::Context>& cr, int
|
|
|
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(0);
|
|
|
- attr.set_end_index(static_cast<int>(completion_suffix.length()));
|
|
|
+ attr.set_end_index(static_cast<int>(suffix_str.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(completion_x, minibuffer_y);
|
|
|
+ cr->move_to(completion_x, minibuffer_y);
|
|
|
completion_layout->show_in_cairo_context(cr);
|
|
|
}
|
|
|
}
|
|
|
@@ -697,4 +810,18 @@ void GtkRenderer::invalidate_cache() {
|
|
|
spdlog::debug("GtkRenderer: Cache invalidated due to theme change.");
|
|
|
}
|
|
|
|
|
|
+void GtkRenderer::set_font_size(int size) {
|
|
|
+ if (size < 6) size = 6; // Minimum readable size
|
|
|
+ if (size > 72) size = 72; // Maximum reasonable size
|
|
|
+
|
|
|
+ font_size_ = size;
|
|
|
+ core_.config().set("font_size", size);
|
|
|
+
|
|
|
+ // Reinitialize font metrics
|
|
|
+ initialize_font_metrics();
|
|
|
+ invalidate_cache();
|
|
|
+
|
|
|
+ spdlog::debug("GtkRenderer: Font size changed to {}", size);
|
|
|
+}
|
|
|
+
|
|
|
} // namespace lumacs
|