gtk_renderer.cpp 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616
  1. #include "lumacs/gtk_renderer.hpp"
  2. #include "lumacs/editor_core.hpp"
  3. #include "lumacs/minibuffer_manager.hpp" // For MinibufferManager and MinibufferMode
  4. #include <algorithm> // For std::max, std::min
  5. #include <spdlog/spdlog.h> // Added for debug logging
  6. namespace lumacs {
  7. GtkRenderer::GtkRenderer(EditorCore& core, Gtk::Widget& context_widget)
  8. : core_(core), context_widget_(context_widget) {
  9. initialize_font_metrics(); // Initialize font metrics once during construction
  10. }
  11. void GtkRenderer::initialize_font_metrics() {
  12. if (font_initialized_) return;
  13. // Use a minimal string for layout creation
  14. auto layout = context_widget_.create_pango_layout(" ");
  15. font_desc_ = Pango::FontDescription("Monospace 12");
  16. layout->set_font_description(font_desc_);
  17. Pango::FontMetrics metrics = layout->get_context()->get_metrics(font_desc_);
  18. line_height_ = (double)metrics.get_height() / PANGO_SCALE;
  19. ascent_ = (double)metrics.get_ascent() / PANGO_SCALE;
  20. // Get character width more precisely using index_to_pos for a known monospace char
  21. layout->set_text("M"); // Use a single, representative character
  22. Pango::Rectangle pos0 = layout->index_to_pos(0); // Position at index 0 (before 'M')
  23. Pango::Rectangle pos1 = layout->index_to_pos(1); // Position at index 1 (after 'M')
  24. char_width_ = (double)(pos1.get_x() - pos0.get_x()) / PANGO_SCALE;
  25. spdlog::debug("GtkRenderer font metrics: line_height={}, char_width={}", line_height_, char_width_);
  26. font_initialized_ = true;
  27. }
  28. void GtkRenderer::apply_face_attributes(Pango::AttrList& attr_list, const FaceAttributes& face, int start_index, int end_index) {
  29. if (start_index >= end_index) return;
  30. // Foreground
  31. if (face.foreground) {
  32. auto attr = Pango::Attribute::create_attr_foreground(
  33. face.foreground->r * 257, face.foreground->g * 257, face.foreground->b * 257);
  34. attr.set_start_index(start_index);
  35. attr.set_end_index(end_index);
  36. attr_list.insert(attr);
  37. }
  38. // Background
  39. if (face.background) {
  40. auto attr = Pango::Attribute::create_attr_background(
  41. face.background->r * 257, face.background->g * 257, face.background->b * 257);
  42. attr.set_start_index(start_index);
  43. attr.set_end_index(end_index);
  44. attr_list.insert(attr);
  45. }
  46. // Font Family
  47. if (face.family) {
  48. auto attr = Pango::Attribute::create_attr_family(*face.family);
  49. attr.set_start_index(start_index);
  50. attr.set_end_index(end_index);
  51. attr_list.insert(attr);
  52. }
  53. // Weight
  54. if (face.weight) {
  55. Pango::Weight w = Pango::Weight::NORMAL;
  56. if (*face.weight == FontWeight::Bold) w = Pango::Weight::BOLD;
  57. else if (*face.weight == FontWeight::Light) w = Pango::Weight::LIGHT;
  58. auto attr = Pango::Attribute::create_attr_weight(w);
  59. attr.set_start_index(start_index);
  60. attr.set_end_index(end_index);
  61. attr_list.insert(attr);
  62. }
  63. // Slant/Style
  64. if (face.slant) {
  65. Pango::Style s = Pango::Style::NORMAL;
  66. if (*face.slant == FontSlant::Italic) s = Pango::Style::ITALIC;
  67. else if (*face.slant == FontSlant::Oblique) s = Pango::Style::OBLIQUE;
  68. auto attr = Pango::Attribute::create_attr_style(s);
  69. attr.set_start_index(start_index);
  70. attr.set_end_index(end_index);
  71. attr_list.insert(attr);
  72. }
  73. // Underline
  74. if (face.underline && *face.underline) {
  75. auto attr = Pango::Attribute::create_attr_underline(Pango::Underline::SINGLE);
  76. attr.set_start_index(start_index);
  77. attr.set_end_index(end_index);
  78. attr_list.insert(attr);
  79. }
  80. }
  81. std::optional<Position> GtkRenderer::resolve_screen_pos(std::shared_ptr<Window> window, double x, double y) {
  82. if (!window || line_height_ <= 0 || char_width_ <= 0) return std::nullopt;
  83. int row = static_cast<int>((y - PADDING_TOP) / line_height_);
  84. int col = static_cast<int>((x - PADDING_LEFT) / char_width_);
  85. if (row < 0) row = 0;
  86. if (col < 0) col = 0;
  87. auto viewport = window->viewport();
  88. size_t target_line = viewport.scroll_offset + row;
  89. size_t target_col = viewport.horizontal_offset + col;
  90. // Clamp to buffer bounds
  91. if (target_line >= window->buffer().line_count()) {
  92. target_line = window->buffer().line_count() - 1;
  93. }
  94. // Clamp column to line length
  95. size_t line_len = window->buffer().line(target_line).length();
  96. if (target_col > line_len) target_col = line_len;
  97. return Position{target_line, target_col};
  98. }
  99. void GtkRenderer::on_draw(const Cairo::RefPtr<Cairo::Context>& cr, int width, int height,
  100. std::shared_ptr<Window> active_window_cache, bool cursor_visible_state) {
  101. // Safety check - don't draw if core has no active theme (during destruction)
  102. if (!core_.theme_manager().active_theme()) return;
  103. // Fill background of the entire drawing area
  104. auto theme = core_.theme_manager().active_theme();
  105. Color bg = theme->get_bg_color(ThemeElement::Background);
  106. cr->set_source_rgb(bg.r / 255.0, bg.g / 255.0, bg.b / 255.0);
  107. cr->paint();
  108. // Get the active window from core to render it
  109. auto active_window = core_.active_window();
  110. if (!active_window) return;
  111. // Render the active window (main editor area)
  112. draw_window(cr, width, height, active_window, &main_drawing_area_, active_window_cache, cursor_visible_state);
  113. // Use a temporary layout for modeline/minibuffer as they are dynamic
  114. auto temp_layout = Pango::Layout::create(context_widget_.get_pango_context());
  115. temp_layout->set_font_description(font_desc_);
  116. // Render modeline
  117. render_modeline_for_window(cr, width, height, active_window, active_window_cache);
  118. // Render minibuffer
  119. render_minibuffer(cr, width, height);
  120. }
  121. void GtkRenderer::draw_window(const Cairo::RefPtr<Cairo::Context>& cr, int width, int height,
  122. std::shared_ptr<Window> window, Gtk::DrawingArea* widget,
  123. std::shared_ptr<Window> active_window_cache, bool cursor_visible_state) {
  124. if (!core_.theme_manager().active_theme() || !window || !widget) return;
  125. // Use a temporary layout that is properly initialized
  126. auto layout = Pango::Layout::create(widget->get_pango_context());
  127. layout->set_font_description(font_desc_);
  128. const auto cursor = window->cursor();
  129. const auto& buffer = window->buffer();
  130. auto theme = core_.theme_manager().active_theme();
  131. // Fill background
  132. Color bg = theme->get_bg_color(ThemeElement::Background);
  133. cr->set_source_rgb(bg.r / 255.0, bg.g / 255.0, bg.b / 255.0);
  134. cr->paint();
  135. int content_width_px = width - static_cast<int>(PADDING_LEFT + PADDING_RIGHT);
  136. int content_height_px = height - static_cast<int>(PADDING_TOP + PADDING_BOTTOM);
  137. int visible_lines = static_cast<int>(content_height_px / line_height_);
  138. int visible_cols = static_cast<int>(content_width_px / char_width_);
  139. // Reserve space for modeline (1 line). Minibuffer is now separate.
  140. int editor_lines = std::max(0, visible_lines - 1);
  141. window->set_viewport_size(visible_cols, editor_lines);
  142. // Region/Mark Calculation
  143. std::optional<Range> selection_range;
  144. if (buffer.has_active_mark() && buffer.mark()) {
  145. selection_range = buffer.get_region(window->cursor());
  146. }
  147. // Get default foreground color from theme
  148. Color fg = theme->get_fg_color(ThemeElement::Normal);
  149. cr->set_source_rgb(fg.r / 255.0, fg.g / 255.0, fg.b / 255.0);
  150. // Render visible lines
  151. auto [start_line, end_line] = window->visible_line_range();
  152. int horizontal_offset = window->viewport().horizontal_offset;
  153. auto& window_cache = render_cache_[window.get()];
  154. for (int screen_y = 0; screen_y < editor_lines && start_line + screen_y < end_line; ++screen_y) {
  155. size_t buffer_line_idx = start_line + screen_y;
  156. if (buffer_line_idx >= buffer.line_count()) break; // Safety check
  157. const auto& line_text = buffer.line(buffer_line_idx);
  158. const auto& styles = buffer.get_line_styles(buffer_line_idx);
  159. // Apply horizontal scrolling
  160. std::string visible_text;
  161. if (horizontal_offset < static_cast<int>(line_text.length())) {
  162. visible_text = line_text.substr(horizontal_offset);
  163. }
  164. // Always set text for the layout before any further operations, especially cursor positioning
  165. layout->set_text(visible_text);
  166. double text_x = PADDING_LEFT;
  167. double text_y = PADDING_TOP + screen_y * line_height_;
  168. // Check for Region/Selection Intersection
  169. bool has_selection = false;
  170. int sel_start = 0, sel_end = 0;
  171. if (selection_range) {
  172. if (buffer_line_idx >= selection_range->start.line && buffer_line_idx <= selection_range->end.line) {
  173. has_selection = true;
  174. size_t s_col = (buffer_line_idx == selection_range->start.line) ? selection_range->start.column : 0;
  175. size_t e_col = (buffer_line_idx == selection_range->end.line) ? selection_range->end.column : line_text.length();
  176. sel_start = static_cast<int>(s_col) - horizontal_offset;
  177. sel_end = static_cast<int>(e_col) - horizontal_offset;
  178. sel_start = std::max(0, sel_start);
  179. sel_end = std::min(static_cast<int>(visible_text.length()), sel_end);
  180. if (sel_start >= sel_end) has_selection = false;
  181. }
  182. }
  183. bool use_cache = false;
  184. if (!has_selection && window_cache.count(buffer_line_idx)) {
  185. auto& entry = window_cache[buffer_line_idx];
  186. if (entry.text_content == visible_text) { // Simple validation
  187. cr->set_source(entry.surface, text_x, text_y);
  188. cr->paint();
  189. use_cache = true;
  190. }
  191. }
  192. if (!use_cache) {
  193. // Draw fresh
  194. // layout->set_text(visible_text); // Moved this line outside the if block
  195. Pango::AttrList attr_list;
  196. // 1. Apply Syntax Highlighting
  197. for (const auto& style : styles) {
  198. if (auto face = theme->get_face(style.attr.face_name)) {
  199. int start = static_cast<int>(style.range.start.column) - horizontal_offset;
  200. int end = static_cast<int>(style.range.end.column) - horizontal_offset;
  201. start = std::max(0, start);
  202. end = std::min(static_cast<int>(visible_text.length()), end);
  203. if (start < end) {
  204. apply_face_attributes(attr_list, *face, start, end);
  205. }
  206. }
  207. }
  208. // 2. Apply Region/Selection Highlight (if any)
  209. if (has_selection) {
  210. if (auto region_face = theme->get_face("region")) {
  211. apply_face_attributes(attr_list, *region_face, sel_start, sel_end);
  212. } else {
  213. auto attr = Pango::Attribute::create_attr_background(0x4444, 0x4444, 0x4444);
  214. attr.set_start_index(sel_start);
  215. attr.set_end_index(sel_end);
  216. attr_list.insert(attr);
  217. }
  218. }
  219. layout->set_attributes(attr_list);
  220. // Render
  221. if (!has_selection) {
  222. // Render to Cache surface first
  223. int surf_width = std::max(1, static_cast<int>(content_width_px));
  224. auto surface = Cairo::ImageSurface::create(Cairo::Surface::Format::ARGB32, surf_width, static_cast<int>(line_height_)); auto cr_surf = Cairo::Context::create(surface);
  225. // Clear surface
  226. cr_surf->set_operator(Cairo::Context::Operator::CLEAR);
  227. cr_surf->paint();
  228. cr_surf->set_operator(Cairo::Context::Operator::OVER);
  229. cr_surf->set_source_rgb(fg.r / 255.0, fg.g / 255.0, fg.b / 255.0);
  230. layout->show_in_cairo_context(cr_surf); // Render layout to surface
  231. // Store
  232. LineCacheEntry entry;
  233. entry.text_content = visible_text;
  234. entry.surface = surface;
  235. entry.styles = styles;
  236. window_cache[buffer_line_idx] = entry;
  237. // Blit to screen
  238. cr->set_source(surface, text_x, text_y);
  239. cr->paint();
  240. } else {
  241. // Render directly to screen (don't cache selected text)
  242. cr->move_to(text_x, text_y);
  243. cr->set_source_rgb(fg.r / 255.0, fg.g / 255.0, fg.b / 255.0);
  244. layout->show_in_cairo_context(cr);
  245. }
  246. }
  247. // Render Cursor (assuming `cursor_visible_` is managed by GtkEditor)
  248. bool cursor_visible_ = cursor_visible_state;
  249. bool is_active_window = (window == active_window_cache);
  250. if (is_active_window && cursor_visible_ && buffer_line_idx == cursor.line) {
  251. int cursor_idx = static_cast<int>(cursor.column) - horizontal_offset;
  252. spdlog::debug("Cursor debug: Logical Curs ({},{}) | H-Offset {} | VisTextLen {} | CursorIdx {}",
  253. cursor.line, cursor.column, horizontal_offset, visible_text.length(), cursor_idx);
  254. Pango::Rectangle pos;
  255. if (cursor_idx < 0) {
  256. // Out of view
  257. spdlog::debug("Cursor debug: Cursor out of view (left)");
  258. continue; // Don't draw if out of view
  259. } else if (cursor_idx > static_cast<int>(visible_text.length())) {
  260. // Past end of line (after last character)
  261. pos = layout->index_to_pos(visible_text.length());
  262. int diff = cursor_idx - static_cast<int>(visible_text.length());
  263. if (diff > 0) {
  264. pos.set_x(pos.get_x() + diff * char_width_ * PANGO_SCALE);
  265. }
  266. spdlog::debug("Cursor debug: Past end of line. PangoX={}, FinalPangoX={}", layout->index_to_pos(visible_text.length()).get_x(), pos.get_x());
  267. } else {
  268. pos = layout->index_to_pos(cursor_idx);
  269. spdlog::debug("Cursor debug: Within line. PangoX={}", pos.get_x());
  270. }
  271. double cursor_screen_x = PADDING_LEFT + (pos.get_x() / (double)PANGO_SCALE);
  272. spdlog::debug("Cursor debug: ScreenX={}, ScreenY={} | CharWidth={}, LineHeight={}",
  273. cursor_screen_x, text_y, char_width_, line_height_);
  274. if (cursor_screen_x >= PADDING_LEFT && cursor_screen_x < (width - PADDING_RIGHT)) {
  275. // Determine cursor width
  276. double cur_width = char_width_;
  277. if (cursor_idx < static_cast<int>(visible_text.length())) {
  278. Pango::Rectangle next_pos;
  279. next_pos = layout->index_to_pos(cursor_idx + 1);
  280. cur_width = (next_pos.get_x() - pos.get_x()) / (double)PANGO_SCALE;
  281. }
  282. // Draw Cursor Block
  283. Color cursor_bg = fg;
  284. cr->set_source_rgb(cursor_bg.r / 255.0, cursor_bg.g / 255.0, cursor_bg.b / 255.0);
  285. cr->rectangle(cursor_screen_x, text_y, cur_width, line_height_);
  286. cr->fill();
  287. // Draw Character Inverted
  288. if (cursor_idx < static_cast<int>(visible_text.length())) {
  289. char cursor_char = visible_text[cursor_idx];
  290. cr->set_source_rgb(bg.r / 255.0, bg.g / 255.0, bg.b / 255.0);
  291. auto cursor_layout = widget->create_pango_layout(std::string(1, cursor_char));
  292. cursor_layout->set_font_description(font_desc_);
  293. cr->move_to(cursor_screen_x, text_y);
  294. cursor_layout->show_in_cairo_context(cr);
  295. }
  296. }
  297. }
  298. }
  299. // Refined Loop with Selection Logic:
  300. // (This replaces the loop in the file)
  301. // Since I cannot easily rewrite the whole loop with complex logic insertion in one go without potentially breaking,
  302. // I will rewrite the loop logic completely in the replacement string.
  303. } // Closing brace for GtkRenderer::draw_window method
  304. void GtkRenderer::render_modeline_for_window(const Cairo::RefPtr<Cairo::Context>& cr, int width, int height,
  305. std::shared_ptr<Window> window,
  306. std::shared_ptr<Window> active_window_cache) {
  307. if (!core_.theme_manager().active_theme() || !window) return;
  308. // The logic for is_active needs to be passed from GtkEditor
  309. bool is_active = (window == active_window_cache);
  310. // Calculate modeline position (bottom of the drawing area)
  311. // Minibuffer is separate, so modeline is at the very bottom
  312. double modeline_y = height - line_height_ - PADDING_BOTTOM;
  313. double modeline_x = PADDING_LEFT;
  314. // Get theme colors
  315. auto theme = core_.theme_manager().active_theme();
  316. ThemeElement element = is_active ? ThemeElement::StatusLine : ThemeElement::StatusLineInactive;
  317. Color bg = theme->get_bg_color(element);
  318. Color fg = theme->get_fg_color(element);
  319. // Draw modeline background
  320. cr->set_source_rgb(bg.r / 255.0, bg.g / 255.0, bg.b / 255.0);
  321. cr->rectangle(0, modeline_y, width, line_height_);
  322. cr->fill();
  323. // Build modeline content using ModelineManager
  324. auto content = core_.modeline_manager().generate_content(window, is_active);
  325. double x_offset = modeline_x;
  326. for (const auto& chunk : content) {
  327. Color chunk_fg = fg;
  328. // Resolve chunk face if needed
  329. if (!chunk.face_name.empty()) {
  330. if (auto face = theme->get_face(chunk.face_name)) {
  331. if (face->foreground) chunk_fg = *face->foreground;
  332. }
  333. }
  334. // Use a temporary layout for modeline chunk
  335. auto layout = Pango::Layout::create(context_widget_.get_pango_context());
  336. layout->set_font_description(font_desc_);
  337. layout->set_text(chunk.text);
  338. // Apply attributes
  339. Pango::AttrList attr_list;
  340. if (!chunk.face_name.empty()) {
  341. if (auto face = theme->get_face(chunk.face_name)) {
  342. apply_face_attributes(attr_list, *face, 0, static_cast<int>(chunk.text.length()));
  343. }
  344. }
  345. layout->set_attributes(attr_list);
  346. cr->set_source_rgb(chunk_fg.r / 255.0, chunk_fg.g / 255.0, chunk_fg.b / 255.0);
  347. cr->move_to(x_offset, modeline_y);
  348. layout->show_in_cairo_context(cr);
  349. // Advance
  350. int w, h;
  351. layout->get_pixel_size(w, h);
  352. x_offset += w;
  353. }
  354. }
  355. void GtkRenderer::render_minibuffer(const Cairo::RefPtr<Cairo::Context>& cr, int width, int height) {
  356. if (!core_.theme_manager().active_theme()) return;
  357. // Get theme colors
  358. auto theme = core_.theme_manager().active_theme();
  359. Color bg = theme->get_bg_color(ThemeElement::Background);
  360. Color fg = theme->get_fg_color(ThemeElement::Normal);
  361. // ALWAYS draw minibuffer background to prevent it from disappearing
  362. // Clear the ENTIRE surface first to avoid artifacts
  363. cr->set_source_rgb(bg.r / 255.0 * 0.9, bg.g / 255.0 * 0.9, bg.b / 255.0 * 0.9);
  364. cr->rectangle(0, 0, width, height); // Clear full dedicated area
  365. cr->fill();
  366. // Draw separator line above minibuffer
  367. cr->set_source_rgb(fg.r / 255.0 * 0.5, fg.g / 255.0 * 0.5, fg.b / 255.0 * 0.5);
  368. cr->set_line_width(1.0);
  369. cr->move_to(0, 0);
  370. cr->line_to(width, 0);
  371. cr->stroke();
  372. // Only render text if minibuffer is active or a message is set
  373. if (!core_.minibuffer_manager().is_active() && core_.last_message().empty()) {
  374. return;
  375. }
  376. // Calculate minibuffer position (bottom line with padding)
  377. double minibuffer_y = height - line_height_ - PADDING_BOTTOM;
  378. double minibuffer_x = PADDING_LEFT;
  379. // Prepare minibuffer text
  380. std::string minibuffer_text;
  381. std::string prompt_part;
  382. std::string input_part;
  383. if (core_.minibuffer_manager().is_active()) {
  384. prompt_part = core_.minibuffer_manager().get_prompt();
  385. input_part = core_.minibuffer_manager().get_input_buffer();
  386. minibuffer_text = prompt_part + input_part;
  387. } else if (!core_.last_message().empty()) {
  388. minibuffer_text = core_.last_message();
  389. }
  390. // Render minibuffer text
  391. if (!minibuffer_text.empty()) {
  392. cr->set_source_rgb(fg.r / 255.0, fg.g / 255.0, fg.b / 255.0);
  393. auto layout = Pango::Layout::create(context_widget_.get_pango_context());
  394. layout->set_font_description(font_desc_);
  395. layout->set_text(minibuffer_text);
  396. // Apply face attributes for prompt if active
  397. if (core_.minibuffer_manager().is_active()) {
  398. Pango::AttrList attr_list;
  399. if (auto face = theme->get_face("minibuffer-prompt")) {
  400. apply_face_attributes(attr_list, *face, 0, static_cast<int>(prompt_part.length()));
  401. }
  402. layout->set_attributes(attr_list);
  403. }
  404. cr->move_to(minibuffer_x, minibuffer_y);
  405. layout->show_in_cairo_context(cr);
  406. }
  407. // Render minibuffer cursor if active and visible (assuming cursor_visible is managed by GtkEditor)
  408. bool cursor_visible_ = true; // Placeholder - in real usage passed or managed
  409. if (core_.minibuffer_manager().is_active()) { // Assuming always visible or managed externally
  410. // Calculate cursor position in minibuffer
  411. // text is prompt + input. Cursor is relative to input start.
  412. size_t cursor_idx = prompt_part.length() + core_.minibuffer_manager().get_cursor_position();
  413. auto layout = Pango::Layout::create(context_widget_.get_pango_context());
  414. layout->set_font_description(font_desc_);
  415. layout->set_text(minibuffer_text);
  416. Pango::Rectangle pos = layout->index_to_pos(static_cast<int>(cursor_idx));
  417. double cursor_screen_x = minibuffer_x + (pos.get_x() / (double)PANGO_SCALE);
  418. // Draw Cursor Block
  419. // Use char_width_ as default, or measure actual char
  420. double cur_width = char_width_;
  421. // If not at end, measure actual char width
  422. if (cursor_idx < minibuffer_text.length()) {
  423. Pango::Rectangle next_pos = layout->index_to_pos(static_cast<int>(cursor_idx + 1));
  424. cur_width = (next_pos.get_x() - pos.get_x()) / (double)PANGO_SCALE;
  425. }
  426. cr->set_source_rgb(fg.r / 255.0, fg.g / 255.0, fg.b / 255.0);
  427. cr->rectangle(cursor_screen_x, minibuffer_y, cur_width, line_height_);
  428. cr->fill();
  429. // Draw Character Inverted (if not at end)
  430. if (cursor_idx < minibuffer_text.length()) {
  431. // Extract char at cursor
  432. // Note: simple indexing works for ASCII, but for UTF-8 need careful extraction.
  433. // Pango layout handles rendering, we just need to render that one char at the pos.
  434. // Simpler: Set color to BG and re-render the layout clipped?
  435. // Or just render single char?
  436. // Let's reuse the main editor approach: create layout for single char.
  437. // But we need the exact byte string for the character at cursor_idx (utf8 aware).
  438. // For now, let's rely on std::string indexing assuming ASCII for command/paths mostly,
  439. // or use Pango to iterate?
  440. // Glib::ustring is better for UTF8.
  441. // Let's assume standard char for now to fix the main visual bug.
  442. char cursor_char = minibuffer_text[cursor_idx];
  443. cr->set_source_rgb(bg.r / 255.0, bg.g / 255.0, bg.b / 255.0);
  444. auto cursor_layout = context_widget_.create_pango_layout(std::string(1, cursor_char));
  445. cursor_layout->set_font_description(font_desc_);
  446. cr->move_to(cursor_screen_x, minibuffer_y);
  447. cursor_layout->show_in_cairo_context(cr);
  448. }
  449. // Render completion overlay if applicable
  450. auto current_completion = core_.minibuffer_manager().get_current_completion();
  451. if (current_completion && input_part != *current_completion) {
  452. // Safety check: ensure input_part length is valid for substring extraction
  453. if (input_part.length() <= current_completion->length()) {
  454. std::string completion_suffix = current_completion->substr(input_part.length());
  455. if (!completion_suffix.empty()) {
  456. // Position completion at the end of the input text (cursor might be in middle)
  457. // completion suffix appends to the END of input.
  458. // Calculate position of the end of input
  459. size_t input_end_idx = prompt_part.length() + input_part.length();
  460. Pango::Rectangle end_pos = layout->index_to_pos(static_cast<int>(input_end_idx));
  461. double completion_x = minibuffer_x + (end_pos.get_x() / (double)PANGO_SCALE);
  462. auto completion_layout = Pango::Layout::create(context_widget_.get_pango_context());
  463. completion_layout->set_font_description(font_desc_);
  464. completion_layout->set_text(completion_suffix); // Only draw suffix
  465. Pango::AttrList completion_attr_list;
  466. if (auto face = theme->get_face("minibuffer-completion")) {
  467. apply_face_attributes(completion_attr_list, *face, 0, static_cast<int>(completion_suffix.length()));
  468. } else {
  469. // Fallback: dimmed foreground
  470. Color dim_fg = fg;
  471. dim_fg.r = static_cast<unsigned char>(dim_fg.r * 0.7);
  472. dim_fg.g = static_cast<unsigned char>(dim_fg.g * 0.7);
  473. dim_fg.b = static_cast<unsigned char>(dim_fg.b * 0.7);
  474. auto attr = Pango::Attribute::create_attr_foreground(dim_fg.r * 257, dim_fg.g * 257, dim_fg.b * 257);
  475. attr.set_start_index(0);
  476. attr.set_end_index(static_cast<int>(completion_suffix.length()));
  477. completion_attr_list.insert(attr);
  478. }
  479. completion_layout->set_attributes(completion_attr_list);
  480. cr->set_source_rgb(fg.r / 255.0, fg.g / 255.0, fg.b / 255.0);
  481. cr->move_to(completion_x, minibuffer_y);
  482. completion_layout->show_in_cairo_context(cr);
  483. }
  484. }
  485. }
  486. }
  487. }
  488. bool GtkRenderer::get_minibuffer_coords(int& x, int& y, int& width, int& height) const {
  489. // These coordinates are relative to the main drawing area.
  490. // The minibuffer is always at the bottom of the main drawing area.
  491. width = context_widget_.get_width();
  492. height = static_cast<int>(line_height_ + PADDING_TOP + PADDING_BOTTOM); // Approximate minibuffer height
  493. x = 0; // Starts at the left edge
  494. y = context_widget_.get_height() - height; // Position from the bottom
  495. return true;
  496. }
  497. void GtkRenderer::invalidate_cache() {
  498. render_cache_.clear();
  499. spdlog::debug("GtkRenderer: Cache invalidated due to theme change.");
  500. }
  501. } // namespace lumacs