#include "render.h" #include "common.h" #include #include #include #include /* * All UI measurements are derived from the active font. draw_text() uses a * baseline y coordinate, so render.c keeps both line-top and baseline values * explicit. This avoids the old fixed 24/28/34 pixel constants drifting when * font size changes. */ typedef struct view_rect { float x; float y; float w; float h; } view_rect_t; typedef struct ui_metrics { float pad_x; float pad_y; float content_x; float content_top; float content_bottom_pad; float status_h; float minibuffer_h; float cursor_w; float cursor_h; } ui_metrics_t; static float ecex_maxf(float a, float b) { return a > b ? a : b; } static ui_metrics_t ui_metrics(app_t *app) { float size = app && app->font.size_px > 1.0f ? app->font.size_px : 16.0f; float line_h = app && app->font.line_height > 1.0f ? app->font.line_height : size * 1.20f; ui_metrics_t m; m.pad_x = ecex_maxf(8.0f, size * 0.75f); m.pad_y = ecex_maxf(4.0f, size * 0.35f); m.content_x = m.pad_x; m.content_top = m.pad_y; m.content_bottom_pad = m.pad_y; m.status_h = line_h + m.pad_y * 2.0f; m.minibuffer_h = line_h + m.pad_y * 2.0f; m.cursor_w = ecex_maxf(2.0f, size * 0.10f); m.cursor_h = ecex_maxf(1.0f, app ? app->font.ascent_px + app->font.descent_px : size); return m; } static float line_baseline(app_t *app, float line_top) { float ascent = app->font.ascent_px > 1.0f ? app->font.ascent_px : app->font.size_px * 0.80f; return line_top + ascent; } static void setup_2d(int width, int height) { glViewport(0, 0, width, height); glMatrixMode(GL_PROJECTION); glLoadIdentity(); glOrtho(0, width, height, 0, -1, 1); glMatrixMode(GL_MODELVIEW); glLoadIdentity(); } static void draw_rect(float x, float y, float w, float h) { glBegin(GL_QUADS); glVertex2f(x, y); glVertex2f(x + w, y); glVertex2f(x + w, y + h); glVertex2f(x, y + h); glEnd(); } static float mono_cell_width(app_t *app) { float w = text_width(&app->font, "M"); return w > 1.0f ? w : app->font.size_px * 0.6f; } static int minibuffer_visible(app_t *app) { return app->mode == APP_MODE_MX || app->mode == APP_MODE_PREFIX || app->mode == APP_MODE_PROMPT || app->mode == APP_MODE_ISEARCH || app->message[0] != '\0'; } static size_t visible_row_count_for_rect(app_t *app, view_rect_t r) { ui_metrics_t m = ui_metrics(app); float usable = r.h - m.content_top - m.content_bottom_pad; if (usable <= app->font.line_height || app->font.line_height <= 1.0f) { return 1; } size_t rows = (size_t)(usable / app->font.line_height); return rows ? rows : 1; } static float gutter_width(app_t *app, buffer_t *buf) { if (!app || !app->ed || !buf || !app->ed->theme.line_numbers_enabled) return 0.0f; char tmp[32]; snprintf(tmp, sizeof(tmp), "%zu ", buffer_line_count(buf)); return text_width(&app->font, tmp) + ui_metrics(app).pad_x * 0.5f; } static size_t visible_col_count_for_rect(app_t *app, view_rect_t r) { ui_metrics_t m = ui_metrics(app); float cell = mono_cell_width(app); buffer_t *buf = ecex_current_buffer(app->ed); float width = r.w - m.content_x * 2.0f - gutter_width(app, buf); if (width <= cell) return 1; return (size_t)(width / cell); } static size_t offset_for_line(buffer_t *buf, size_t target_line) { if (!buf || target_line == 0) return 0; size_t line = 0; for (size_t i = 0; i < buf->len; i++) { if (buf->data[i] == '\n') { line++; if (line == target_line) { return i + 1; } } } return buf->len; } static void ensure_cursor_visible(app_t *app, buffer_t *buf, view_rect_t rect) { if (!app || !buf) return; size_t rows = visible_row_count_for_rect(app, rect); size_t cols = visible_col_count_for_rect(app, rect); size_t cursor_line = buffer_current_line_number(buf); if (cursor_line > 0) cursor_line--; size_t cursor_col = buffer_current_column(buf); if (cursor_line < buf->scroll_line) { buf->scroll_line = cursor_line; } else if (cursor_line >= buf->scroll_line + rows) { buf->scroll_line = cursor_line - rows + 1; } if (cursor_col < buf->scroll_col) { buf->scroll_col = cursor_col; } else if (cursor_col >= buf->scroll_col + cols) { buf->scroll_col = cursor_col - cols + 1; } } static void render_status_bar(app_t *app) { buffer_t *buf = ecex_current_buffer(app->ed); if (!buf) return; ui_metrics_t m = ui_metrics(app); float y = (float)app->height - m.status_h; float text_x = m.pad_x; float text_y = line_baseline(app, y + m.pad_y); glDisable(GL_TEXTURE_2D); glColor3f(app->ed->theme.status_bg.r, app->ed->theme.status_bg.g, app->ed->theme.status_bg.b); draw_rect(0.0f, y, (float)app->width, m.status_h); glColor3f(app->ed->theme.status_border.r, app->ed->theme.status_border.g, app->ed->theme.status_border.b); draw_rect(0.0f, y, (float)app->width, 1.0f); char status[768]; snprintf(status, sizeof(status), " %s%s %s line:%zu col:%zu top:%zu size:%zu buffers:%zu windows:%zu commands:%zu%s%s", buf->name ? buf->name : "(unnamed)", buf->modified ? " *" : "", ecex_buffer_major_mode_name(app->ed, buf), buffer_current_line_number(buf), buffer_current_column(buf), buf->scroll_line + 1, buf->len, app->ed->buffer_count, ecex_window_count(app->ed), app->ed->command_count, buf->path ? " " : "", buf->path ? buf->path : ""); glColor3f(app->ed->theme.status_fg.r, app->ed->theme.status_fg.g, app->ed->theme.status_fg.b); draw_text(&app->font, text_x, text_y, status); } static void render_command_completion(app_t *app, const char *line, float x, float y) { const char *completion = ecex_complete_command(app->ed, app->minibuffer); if (!completion || !completion[0]) return; size_t input_len = strlen(app->minibuffer); if (input_len == 0) return; float ghost_x = x + text_width(&app->font, line); glColor3f(app->ed->theme.completion_fg.r, app->ed->theme.completion_fg.g, app->ed->theme.completion_fg.b); if (strncmp(completion, app->minibuffer, input_len) == 0 && completion[input_len] != '\0') { draw_text(&app->font, ghost_x, y, completion + input_len); } else if (strcmp(completion, app->minibuffer) != 0) { char hint[ECEX_MINIBUFFER_SIZE + 16]; snprintf(hint, sizeof(hint), " -> %s", completion); draw_text(&app->font, ghost_x, y, hint); } } static void render_minibuffer(app_t *app) { if (!minibuffer_visible(app)) { return; } ui_metrics_t m = ui_metrics(app); float h = m.minibuffer_h; float y = (float)app->height - m.status_h - h; float text_x = m.pad_x; float text_y = line_baseline(app, y + m.pad_y); glDisable(GL_TEXTURE_2D); glColor3f(app->ed->theme.minibuffer_bg.r, app->ed->theme.minibuffer_bg.g, app->ed->theme.minibuffer_bg.b); draw_rect(0.0f, y, (float)app->width, h); glColor3f(app->ed->theme.minibuffer_fg.r, app->ed->theme.minibuffer_fg.g, app->ed->theme.minibuffer_fg.b); if (app->mode == APP_MODE_ISEARCH) { char line[ECEX_MINIBUFFER_SIZE + 64]; snprintf(line, sizeof(line), "%s%s%s", app->isearch_backward ? "I-search backward: " : "I-search: ", app->isearch_query, app->isearch_has_match || app->isearch_len == 0 ? "" : " [failing]"); draw_text(&app->font, text_x, text_y, line); } else if (app->mode == APP_MODE_MX) { char line[ECEX_MINIBUFFER_SIZE + 8]; snprintf(line, sizeof(line), "M-x %s", app->minibuffer); draw_text(&app->font, text_x, text_y, line); render_command_completion(app, line, text_x, text_y); } else if (app->mode == APP_MODE_PROMPT) { char line[1200]; snprintf(line, sizeof(line), "%s%s", app->prompt_label, app->prompt_input); draw_text(&app->font, text_x, text_y, line); if (app->prompt_completion_active && app->prompt_completion_count > 0) { char hint[128]; snprintf(hint, sizeof(hint), " [%zu/%zu]", app->prompt_completion_index + 1, app->prompt_completion_count); glColor3f(app->ed->theme.completion_fg.r, app->ed->theme.completion_fg.g, app->ed->theme.completion_fg.b); draw_text(&app->font, text_x + text_width(&app->font, line), text_y, hint); if (app->prompt_completion_preview_count > 0) { float row_h = app->font.line_height; float popup_pad = m.pad_y; float popup_h = row_h * (float)app->prompt_completion_preview_count + popup_pad * 2.0f; float popup_y = y - popup_h; glDisable(GL_TEXTURE_2D); glColor3f(app->ed->theme.minibuffer_bg.r, app->ed->theme.minibuffer_bg.g, app->ed->theme.minibuffer_bg.b); draw_rect(0.0f, popup_y, (float)app->width, popup_h); for (size_t i = 0; i < app->prompt_completion_preview_count; i++) { size_t absolute = app->prompt_completion_preview_start + i; char row[1100]; snprintf(row, sizeof(row), "%c %s", absolute == app->prompt_completion_index ? '>' : ' ', app->prompt_completion_preview[i]); if (absolute == app->prompt_completion_index) { glColor3f(app->ed->theme.minibuffer_fg.r, app->ed->theme.minibuffer_fg.g, app->ed->theme.minibuffer_fg.b); } else { glColor3f(app->ed->theme.completion_fg.r, app->ed->theme.completion_fg.g, app->ed->theme.completion_fg.b); } draw_text(&app->font, text_x, line_baseline(app, popup_y + popup_pad + row_h * (float)i), row); } } } } else if (app->mode == APP_MODE_PREFIX) { char line[ECEX_PREFIX_SIZE + 8]; snprintf(line, sizeof(line), "%s-", app->prefix); draw_text(&app->font, text_x, text_y, line); } else { draw_text(&app->font, text_x, text_y, app->message); } } static int should_highlight_interactive_line(buffer_t *buf, size_t zero_based_line) { if (!buf || !buffer_is_interactive(buf)) return 0; size_t current_line = buffer_current_line_number(buf); if (current_line > 0) current_line--; return current_line == zero_based_line; } static void set_editor_text_color(app_t *app, int highlighted) { if (highlighted) { glColor3f(app->ed->theme.interactive_highlight_fg.r, app->ed->theme.interactive_highlight_fg.g, app->ed->theme.interactive_highlight_fg.b); } else { glColor3f(app->ed->theme.fg.r, app->ed->theme.fg.g, app->ed->theme.fg.b); } } static float text_width_range(app_t *app, buffer_t *buf, size_t start, size_t end) { char *text = buffer_substring(buf, start, end); if (!text) return 0.0f; float width = text_width(&app->font, text); free(text); return width; } static void draw_selection_for_line(app_t *app, buffer_t *buf, view_rect_t rect, size_t visible_start, size_t line_end, float line_top) { if (!buffer_has_selection(buf)) return; size_t sel_start = 0; size_t sel_end = 0; buffer_selection_range(buf, &sel_start, &sel_end); if (sel_end <= visible_start || sel_start > line_end) return; size_t draw_start = ECEX_MAX(sel_start, visible_start); size_t draw_end = ECEX_MIN(sel_end, line_end); float x = rect.x + ui_metrics(app).content_x + gutter_width(app, buf) + text_width_range(app, buf, visible_start, draw_start); float w = text_width_range(app, buf, draw_start, draw_end); /* If the selection contains only this line's newline, show a visible sliver * at end-of-line instead of making the active region appear to vanish. */ if (w < 1.0f && sel_end > line_end && draw_start == line_end) { w = mono_cell_width(app) * 0.5f; } if (w < 1.0f) return; glDisable(GL_TEXTURE_2D); glColor3f(app->ed->theme.region_bg.r, app->ed->theme.region_bg.g, app->ed->theme.region_bg.b); draw_rect(x, line_top, w, app->font.line_height); } static void draw_buffer_line(app_t *app, buffer_t *buf, view_rect_t rect, size_t zero_based_line, size_t line_start, size_t line_end, float line_top) { if (line_start > line_end) return; ui_metrics_t m = ui_metrics(app); int highlighted = should_highlight_interactive_line(buf, zero_based_line); size_t current_zero = buffer_current_line_number(buf); if (current_zero > 0) current_zero--; if (app->ed->theme.current_line_enabled && current_zero == zero_based_line) { glDisable(GL_TEXTURE_2D); glColor3f(app->ed->theme.current_line_bg.r, app->ed->theme.current_line_bg.g, app->ed->theme.current_line_bg.b); draw_rect(rect.x, line_top, rect.w, app->font.line_height); } if (highlighted) { glDisable(GL_TEXTURE_2D); glColor3f(app->ed->theme.interactive_highlight_bg.r, app->ed->theme.interactive_highlight_bg.g, app->ed->theme.interactive_highlight_bg.b); draw_rect(rect.x, line_top, rect.w, app->font.line_height); } size_t line_len = line_end - line_start; size_t col = ECEX_MIN(buf->scroll_col, line_len); size_t start = line_start + col; draw_selection_for_line(app, buf, rect, start, line_end, line_top); float gutter = gutter_width(app, buf); if (gutter > 0.0f) { char nbuf[32]; snprintf(nbuf, sizeof(nbuf), "%zu", zero_based_line + 1); glColor3f(app->ed->theme.completion_fg.r, app->ed->theme.completion_fg.g, app->ed->theme.completion_fg.b); draw_text(&app->font, rect.x + m.pad_x * 0.5f, line_baseline(app, line_top), nbuf); } char *line = buffer_substring(buf, start, line_end); if (!line) return; set_editor_text_color(app, highlighted); draw_text(&app->font, rect.x + m.content_x + gutter, line_baseline(app, line_top), line); free(line); } static void render_cursor(app_t *app, buffer_t *buf, view_rect_t rect) { ui_metrics_t m = ui_metrics(app); size_t cursor_line = buffer_current_line_number(buf); if (cursor_line > 0) cursor_line--; if (cursor_line < buf->scroll_line) return; size_t visible_row = cursor_line - buf->scroll_line; if (visible_row >= visible_row_count_for_rect(app, rect)) return; size_t line_start = buffer_current_line_start(buf); size_t cursor_col = buffer_current_column(buf); size_t visible_start = line_start + ECEX_MIN(buf->scroll_col, cursor_col); char *prefix = buffer_substring(buf, visible_start, buf->point); float x = rect.x + m.content_x + gutter_width(app, buf) + (prefix ? text_width(&app->font, prefix) : 0.0f); float line_top = rect.y + m.content_top + (float)visible_row * app->font.line_height; float cursor_top = line_top + (app->font.line_height - m.cursor_h) * 0.5f; free(prefix); glDisable(GL_TEXTURE_2D); glColor3f(app->ed->theme.cursor.r, app->ed->theme.cursor.g, app->ed->theme.cursor.b); draw_rect(x, cursor_top, m.cursor_w, m.cursor_h); } static void draw_window_border(app_t *app, view_rect_t rect, int active) { glDisable(GL_TEXTURE_2D); if (active) { glColor3f(app->ed->theme.cursor.r, app->ed->theme.cursor.g, app->ed->theme.cursor.b); } else { glColor3f(app->ed->theme.status_border.r, app->ed->theme.status_border.g, app->ed->theme.status_border.b); } draw_rect(rect.x, rect.y, rect.w, 1.0f); draw_rect(rect.x, rect.y + rect.h - 1.0f, rect.w, 1.0f); draw_rect(rect.x, rect.y, 1.0f, rect.h); draw_rect(rect.x + rect.w - 1.0f, rect.y, 1.0f, rect.h); } static void render_buffer_window(app_t *app, ecex_window_t *win, size_t index, float editor_h) { if (!app || !win || !win->buffer) return; view_rect_t rect; rect.x = win->x * (float)app->width; rect.y = win->y * editor_h; rect.w = win->w * (float)app->width; rect.h = win->h * editor_h; if (rect.w < 2.0f || rect.h < 2.0f) return; buffer_t *buf = win->buffer; ui_metrics_t m = ui_metrics(app); int sx = (int)rect.x; int sy = app->height - (int)(rect.y + rect.h); int sw = (int)rect.w; int sh = (int)rect.h; if (sw < 1 || sh < 1) return; glEnable(GL_SCISSOR_TEST); glScissor(sx, sy, sw, sh); if (index == app->ed->current_window_index) { ensure_cursor_visible(app, buf, rect); } size_t rows = visible_row_count_for_rect(app, rect); size_t pos = offset_for_line(buf, buf->scroll_line); float line_top = rect.y + m.content_top; for (size_t row = 0; row < rows; row++) { size_t line_start = pos; size_t line_end = buffer_line_end_at(buf, line_start); draw_buffer_line(app, buf, rect, buf->scroll_line + row, line_start, line_end, line_top); if (line_end >= buf->len) break; pos = line_end + 1; line_top += app->font.line_height; } if (index == app->ed->current_window_index) { render_cursor(app, buf, rect); } glDisable(GL_SCISSOR_TEST); draw_window_border(app, rect, index == app->ed->current_window_index); } static void render_windows(app_t *app) { ui_metrics_t m = ui_metrics(app); float editor_h = (float)app->height - m.status_h; if (minibuffer_visible(app)) editor_h -= m.minibuffer_h; if (editor_h < 1.0f) editor_h = 1.0f; if (app->ed->window_count == 0) return; for (size_t i = 0; i < app->ed->window_count; i++) { render_buffer_window(app, &app->ed->windows[i], i, editor_h); } } void render(app_t *app) { if (!app || !app->window || !app->ed) return; glfwGetFramebufferSize(app->window, &app->width, &app->height); setup_2d(app->width, app->height); glClearColor(app->ed->theme.bg.r, app->ed->theme.bg.g, app->ed->theme.bg.b, 1.0f); glClear(GL_COLOR_BUFFER_BIT); glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); render_windows(app); render_minibuffer(app); render_status_bar(app); }