aboutsummaryrefslogtreecommitdiff
path: root/src/render.c
diff options
context:
space:
mode:
authorDavid Moc <personal@cdatgoose.org>2026-05-30 21:53:05 +0200
committerDavid Moc <personal@cdatgoose.org>2026-05-30 21:53:05 +0200
commite930cc6bdc7f62befac063d7d9d016ffb0a64f1a (patch)
tree52118a1e990ae88f5f0410c8caea129609e22e19 /src/render.c
Added the old repo, refactored it, added the C jit.
Diffstat (limited to 'src/render.c')
-rw-r--r--src/render.c613
1 files changed, 613 insertions, 0 deletions
diff --git a/src/render.c b/src/render.c
new file mode 100644
index 0000000..64a85f1
--- /dev/null
+++ b/src/render.c
@@ -0,0 +1,613 @@
+#include "render.h"
+
+#include "common.h"
+
+#include <GLFW/glfw3.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+/*
+ * 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);
+}