#include "render.h" #include "common.h" #include "media.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 size_t minibuffer_row_count(app_t *app) { if (!app || app->mode != APP_MODE_PREFIX || !app->message[0]) return 1; size_t rows = 1; for (const char *p = app->message; *p; ++p) { if (*p != '\n') continue; rows++; if (rows >= ECEX_MINIBUFFER_MAX_ROWS) return ECEX_MINIBUFFER_MAX_ROWS; } return rows; } 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 * (float)minibuffer_row_count(app) + 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 | Ln %zu, Col %zu | Top %zu | %zu bytes | %zu buffers / %zu windows%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), 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.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); 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) { if (app->message[0]) { const char *line = app->message; size_t row = 0; while (line && *line && row < ECEX_MINIBUFFER_MAX_ROWS) { const char *end = strchr(line, '\n'); size_t len = end ? (size_t)(end - line) : strlen(line); char scratch[ECEX_MINIBUFFER_SIZE]; if (len >= sizeof(scratch)) len = sizeof(scratch) - 1; memcpy(scratch, line, len); scratch[len] = '\0'; draw_text(&app->font, text_x, line_baseline(app, y + m.pad_y + app->font.line_height * (float)row), scratch); row++; if (!end) break; line = end + 1; } } else { 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 int c_syntax_ident_start(char c) { return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_'; } static int c_syntax_ident_char(char c) { return c_syntax_ident_start(c) || (c >= '0' && c <= '9'); } static int c_syntax_keyword(const char *text, size_t len) { static const char *keywords[] = { "alignas", "alignof", "auto", "bool", "break", "case", "char", "const", "constexpr", "continue", "default", "do", "double", "else", "enum", "extern", "false", "float", "for", "goto", "if", "inline", "int", "long", "nullptr", "register", "restrict", "return", "short", "signed", "sizeof", "static", "static_assert", "struct", "switch", "thread_local", "true", "typedef", "typeof", "typeof_unqual", "union", "unsigned", "void", "volatile", "while", "_Alignas", "_Alignof", "_Atomic", "_Bool", "_Complex", "_Generic", "_Imaginary", "_Noreturn", "_Static_assert", "_Thread_local", "NULL", "size_t", "ptrdiff_t", "uint8_t", "uint16_t", "uint32_t", "uint64_t", "int8_t", "int16_t", "int32_t", "int64_t", "FILE" }; for (size_t i = 0; i < sizeof(keywords) / sizeof(keywords[0]); i++) { if (strlen(keywords[i]) == len && strncmp(text, keywords[i], len) == 0) { return 1; } } return 0; } static int c_syntax_line_is_preprocessor(buffer_t *buf, size_t line_start, size_t line_end) { if (!buf || !buf->data) return 0; size_t i = line_start; while (i < line_end && (buf->data[i] == ' ' || buf->data[i] == '\t')) i++; return i < line_end && buf->data[i] == '#'; } static int c_syntax_block_comment_at(buffer_t *buf, size_t pos) { if (!buf || !buf->data) return 0; int in_block = 0; int in_string = 0; int in_char = 0; int escaped = 0; for (size_t i = 0; i < pos && i < buf->len; i++) { char c = buf->data[i]; char next = i + 1 < pos && i + 1 < buf->len ? buf->data[i + 1] : '\0'; if (in_block) { if (c == '*' && next == '/') { in_block = 0; i++; } continue; } if (in_string) { if (escaped) escaped = 0; else if (c == '\\') escaped = 1; else if (c == '"') in_string = 0; else if (c == '\n') in_string = 0; continue; } if (in_char) { if (escaped) escaped = 0; else if (c == '\\') escaped = 1; else if (c == '\'') in_char = 0; else if (c == '\n') in_char = 0; continue; } if (c == '/' && next == '*') { in_block = 1; i++; } else if (c == '/' && next == '/') { while (i < pos && i < buf->len && buf->data[i] != '\n') i++; } else if (c == '"') { in_string = 1; } else if (c == '\'') { in_char = 1; } } return in_block; } static void c_syntax_set_color(app_t *app, int kind, int highlighted) { if (highlighted) { set_editor_text_color(app, highlighted); return; } switch (kind) { case 1: glColor3f(0.43f, 0.78f, 1.00f); break; /* keyword */ case 2: glColor3f(0.68f, 0.91f, 0.42f); break; /* string */ case 3: glColor3f(0.64f, 0.67f, 0.73f); break; /* comment */ case 4: glColor3f(1.00f, 0.58f, 0.78f); break; /* number */ case 5: glColor3f(1.00f, 0.82f, 0.25f); break; /* preprocessor */ default: set_editor_text_color(app, highlighted); break; } } static void c_syntax_draw_span(app_t *app, buffer_t *buf, size_t start, size_t end, float *x, float baseline, int kind, int highlighted) { if (!app || !buf || !x || start >= end) return; char *span = buffer_substring(buf, start, end); if (!span) return; c_syntax_set_color(app, kind, highlighted); draw_text(&app->font, *x, baseline, span); *x += text_width(&app->font, span); free(span); } static void draw_c_syntax_line(app_t *app, buffer_t *buf, size_t line_start, size_t visible_start, size_t line_end, float x, float baseline, int highlighted) { if (!app || !buf || !buf->data || visible_start >= line_end) return; if (c_syntax_line_is_preprocessor(buf, line_start, line_end)) { c_syntax_draw_span(app, buf, visible_start, line_end, &x, baseline, 5, highlighted); return; } int in_block = c_syntax_block_comment_at(buf, visible_start); size_t pos = visible_start; while (pos < line_end) { if (in_block) { size_t end = pos; while (end + 1 < line_end && !(buf->data[end] == '*' && buf->data[end + 1] == '/')) { end++; } if (end + 1 < line_end) { end += 2; in_block = 0; } else { end = line_end; } c_syntax_draw_span(app, buf, pos, end, &x, baseline, 3, highlighted); pos = end; continue; } char c = buf->data[pos]; char next = pos + 1 < line_end ? buf->data[pos + 1] : '\0'; if (c == '/' && next == '/') { c_syntax_draw_span(app, buf, pos, line_end, &x, baseline, 3, highlighted); break; } if (c == '/' && next == '*') { size_t end = pos + 2; while (end + 1 < line_end && !(buf->data[end] == '*' && buf->data[end + 1] == '/')) { end++; } if (end + 1 < line_end) end += 2; else end = line_end; c_syntax_draw_span(app, buf, pos, end, &x, baseline, 3, highlighted); pos = end; continue; } if (c == '"' || c == '\'') { char quote = c; size_t end = pos + 1; int escaped = 0; while (end < line_end) { char sc = buf->data[end++]; if (escaped) { escaped = 0; } else if (sc == '\\') { escaped = 1; } else if (sc == quote) { break; } } c_syntax_draw_span(app, buf, pos, end, &x, baseline, 2, highlighted); pos = end; continue; } if ((c >= '0' && c <= '9') || (c == '.' && next >= '0' && next <= '9')) { size_t end = pos + 1; while (end < line_end) { char nc = buf->data[end]; if ((nc >= '0' && nc <= '9') || (nc >= 'a' && nc <= 'f') || (nc >= 'A' && nc <= 'F') || nc == 'x' || nc == 'X' || nc == 'u' || nc == 'U' || nc == 'l' || nc == 'L' || nc == '.' || nc == '+' || nc == '-') { end++; } else { break; } } c_syntax_draw_span(app, buf, pos, end, &x, baseline, 4, highlighted); pos = end; continue; } if (c_syntax_ident_start(c)) { size_t end = pos + 1; while (end < line_end && c_syntax_ident_char(buf->data[end])) end++; int kind = c_syntax_keyword(buf->data + pos, end - pos) ? 1 : 0; c_syntax_draw_span(app, buf, pos, end, &x, baseline, kind, highlighted); pos = end; continue; } c_syntax_draw_span(app, buf, pos, pos + 1, &x, baseline, 0, highlighted); pos++; } } 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); } float text_x = rect.x + m.content_x + gutter; float baseline = line_baseline(app, line_top); const char *mode_name = ecex_buffer_major_mode_name(app->ed, buf); if (mode_name && strcmp(mode_name, "c-mode") == 0) { draw_c_syntax_line(app, buf, line_start, start, line_end, text_x, baseline, highlighted); } else { char *line = buffer_substring(buf, start, line_end); if (!line) return; set_editor_text_color(app, highlighted); draw_text(&app->font, text_x, baseline, line); free(line); } } static void draw_media_buffer(app_t *app, buffer_t *buf, view_rect_t rect) { if (!app || !buf || !ecex_media_buffer_has_pixels(buf)) return; if (buf->media_texture == 0) { glGenTextures(1, &buf->media_texture); buf->media_texture_width = 0; buf->media_texture_height = 0; } glEnable(GL_TEXTURE_2D); glBindTexture(GL_TEXTURE_2D, buf->media_texture); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP); if (buf->media_dirty || buf->media_texture_width != buf->media_width || buf->media_texture_height != buf->media_height) { glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, buf->media_width, buf->media_height, 0, GL_RGBA, GL_UNSIGNED_BYTE, buf->media_pixels); buf->media_texture_width = buf->media_width; buf->media_texture_height = buf->media_height; buf->media_dirty = 0; } ui_metrics_t m = ui_metrics(app); float avail_x = rect.x + m.content_x; float avail_y = rect.y + m.content_top + app->font.line_height * 4.0f; float avail_w = rect.w - m.content_x * 2.0f; float avail_h = rect.h - (avail_y - rect.y) - m.content_bottom_pad; if (avail_w < 8.0f || avail_h < 8.0f) return; float iw = (float)buf->media_width; float ih = (float)buf->media_height; float scale = avail_w / iw; if (ih * scale > avail_h) scale = avail_h / ih; if (scale <= 0.0f) return; float draw_w = iw * scale; float draw_h = ih * scale; float x = avail_x + (avail_w - draw_w) * 0.5f; float y = avail_y + (avail_h - draw_h) * 0.5f; glColor3f(1.0f, 1.0f, 1.0f); glBegin(GL_QUADS); glTexCoord2f(0.0f, 0.0f); glVertex2f(x, y); glTexCoord2f(1.0f, 0.0f); glVertex2f(x + draw_w, y); glTexCoord2f(1.0f, 1.0f); glVertex2f(x + draw_w, y + draw_h); glTexCoord2f(0.0f, 1.0f); glVertex2f(x, y + draw_h); glEnd(); glDisable(GL_TEXTURE_2D); } 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 app_t *draw_context_app(ecex_draw_context_t *ctx) { return ctx ? (app_t *)ctx->internal : NULL; } static void draw_context_vertex(ecex_draw_context_t *ctx, float x, float y) { glVertex2f(ctx->x + x, ctx->y + y); } void ecex_draw_set_color(ecex_draw_context_t *ctx, float r, float g, float b, float a) { (void)ctx; glColor4f(r, g, b, a); } void ecex_draw_rect(ecex_draw_context_t *ctx, float x, float y, float w, float h) { if (!ctx || w <= 0.0f || h <= 0.0f) return; glDisable(GL_TEXTURE_2D); glBegin(GL_QUADS); draw_context_vertex(ctx, x, y); draw_context_vertex(ctx, x + w, y); draw_context_vertex(ctx, x + w, y + h); draw_context_vertex(ctx, x, y + h); glEnd(); } void ecex_draw_rect_outline(ecex_draw_context_t *ctx, float x, float y, float w, float h, float thickness) { if (!ctx || w <= 0.0f || h <= 0.0f) return; if (thickness <= 0.0f) thickness = 1.0f; ecex_draw_rect(ctx, x, y, w, thickness); ecex_draw_rect(ctx, x, y + h - thickness, w, thickness); ecex_draw_rect(ctx, x, y, thickness, h); ecex_draw_rect(ctx, x + w - thickness, y, thickness, h); } void ecex_draw_line(ecex_draw_context_t *ctx, float x1, float y1, float x2, float y2, float thickness) { if (!ctx) return; if (thickness <= 0.0f) thickness = 1.0f; glDisable(GL_TEXTURE_2D); glLineWidth(thickness); glBegin(GL_LINES); draw_context_vertex(ctx, x1, y1); draw_context_vertex(ctx, x2, y2); glEnd(); glLineWidth(1.0f); } void ecex_draw_text(ecex_draw_context_t *ctx, float x, float y, const char *text) { app_t *app = draw_context_app(ctx); if (!ctx || !app || !text) return; float baseline = y + (app->font.ascent_px > 1.0f ? app->font.ascent_px : app->font.size_px * 0.80f); draw_text(&app->font, ctx->x + x, ctx->y + baseline, text); } float ecex_draw_text_width(ecex_draw_context_t *ctx, const char *text) { app_t *app = draw_context_app(ctx); if (!ctx || !app || !text) return 0.0f; return text_width(&app->font, text); } void ecex_draw_text_aligned(ecex_draw_context_t *ctx, float x, float y, float w, const char *text, int align) { if (!ctx || !text) return; float tw = ecex_draw_text_width(ctx, text); float tx = x; if (align == ECEX_TEXT_ALIGN_CENTER) tx = x + (w - tw) * 0.5f; else if (align == ECEX_TEXT_ALIGN_RIGHT) tx = x + w - tw; ecex_draw_text(ctx, tx, y, text); } void ecex_draw_color_rgba8_i(ecex_draw_context_t *ctx, int r, int g, int b, int a) { if (!ctx) return; if (r < 0) r = 0; if (r > 255) r = 255; if (g < 0) g = 0; if (g > 255) g = 255; if (b < 0) b = 0; if (b > 255) b = 255; if (a < 0) a = 0; if (a > 255) a = 255; ecex_draw_set_color(ctx, (float)r / 255.0f, (float)g / 255.0f, (float)b / 255.0f, (float)a / 255.0f); } void ecex_draw_rect_i(ecex_draw_context_t *ctx, int x, int y, int w, int h) { ecex_draw_rect(ctx, (float)x, (float)y, (float)w, (float)h); } void ecex_draw_rect_outline_i(ecex_draw_context_t *ctx, int x, int y, int w, int h, int thickness) { ecex_draw_rect_outline(ctx, (float)x, (float)y, (float)w, (float)h, (float)thickness); } void ecex_draw_line_i(ecex_draw_context_t *ctx, int x1, int y1, int x2, int y2, int thickness) { ecex_draw_line(ctx, (float)x1, (float)y1, (float)x2, (float)y2, (float)thickness); } void ecex_draw_text_i(ecex_draw_context_t *ctx, int x, int y, const char *text) { ecex_draw_text(ctx, (float)x, (float)y, text); } static const char *ecex_draw_label_text(int label_id) { switch (label_id) { case 1: return "Tetris"; case 2: return "score: "; case 3: return "lines: "; case 4: return "level: "; case 5: return "next:"; case 6: return "keys:"; case 7: return "h/l or left/right move"; case 8: return "j or down soft drop"; case 9: return "k/up rotate"; case 10: return "space hard drop"; case 11: return "p pause, n new, q quit"; case 12: return "GAME OVER - press n"; case 13: return "PAUSED"; case 20: return "Render demo"; case 21: return "host vars and mouse input"; case 22: return "integer safe drawing"; case 23: return "smooth click animation"; case 24: return "position: "; case 25: return "click box to move cube"; default: return ""; } } static unsigned char ecex_ascii5x7_bits(char ch, int row) { /* Last-resort fallback for host labels when no editor font texture exists. */ if (row < 0 || row >= 7) return 0; switch (ch) { case '0': { static const unsigned char b[7] = {14,17,19,21,25,17,14}; return b[row]; } case '1': { static const unsigned char b[7] = {4,12,4,4,4,4,14}; return b[row]; } case '2': { static const unsigned char b[7] = {14,17,1,2,4,8,31}; return b[row]; } case '3': { static const unsigned char b[7] = {30,1,1,14,1,1,30}; return b[row]; } case '4': { static const unsigned char b[7] = {2,6,10,18,31,2,2}; return b[row]; } case '5': { static const unsigned char b[7] = {31,16,16,30,1,1,30}; return b[row]; } case '6': { static const unsigned char b[7] = {14,16,16,30,17,17,14}; return b[row]; } case '7': { static const unsigned char b[7] = {31,1,2,4,8,8,8}; return b[row]; } case '8': { static const unsigned char b[7] = {14,17,17,14,17,17,14}; return b[row]; } case '9': { static const unsigned char b[7] = {14,17,17,15,1,1,14}; return b[row]; } case 'A': case 'a': { static const unsigned char b[7] = {14,17,17,31,17,17,17}; return b[row]; } case 'B': case 'b': { static const unsigned char b[7] = {30,17,17,30,17,17,30}; return b[row]; } case 'C': case 'c': { static const unsigned char b[7] = {14,17,16,16,16,17,14}; return b[row]; } case 'D': case 'd': { static const unsigned char b[7] = {30,17,17,17,17,17,30}; return b[row]; } case 'E': case 'e': { static const unsigned char b[7] = {31,16,16,30,16,16,31}; return b[row]; } case 'F': case 'f': { static const unsigned char b[7] = {31,16,16,30,16,16,16}; return b[row]; } case 'G': case 'g': { static const unsigned char b[7] = {14,17,16,23,17,17,15}; return b[row]; } case 'H': case 'h': { static const unsigned char b[7] = {17,17,17,31,17,17,17}; return b[row]; } case 'I': case 'i': { static const unsigned char b[7] = {14,4,4,4,4,4,14}; return b[row]; } case 'J': case 'j': { static const unsigned char b[7] = {7,2,2,2,2,18,12}; return b[row]; } case 'K': case 'k': { static const unsigned char b[7] = {17,18,20,24,20,18,17}; return b[row]; } case 'L': case 'l': { static const unsigned char b[7] = {16,16,16,16,16,16,31}; return b[row]; } case 'M': case 'm': { static const unsigned char b[7] = {17,27,21,21,17,17,17}; return b[row]; } case 'N': case 'n': { static const unsigned char b[7] = {17,25,21,19,17,17,17}; return b[row]; } case 'O': case 'o': { static const unsigned char b[7] = {14,17,17,17,17,17,14}; return b[row]; } case 'P': case 'p': { static const unsigned char b[7] = {30,17,17,30,16,16,16}; return b[row]; } case 'Q': case 'q': { static const unsigned char b[7] = {14,17,17,17,21,18,13}; return b[row]; } case 'R': case 'r': { static const unsigned char b[7] = {30,17,17,30,20,18,17}; return b[row]; } case 'S': case 's': { static const unsigned char b[7] = {15,16,16,14,1,1,30}; return b[row]; } case 'T': case 't': { static const unsigned char b[7] = {31,4,4,4,4,4,4}; return b[row]; } case 'U': case 'u': { static const unsigned char b[7] = {17,17,17,17,17,17,14}; return b[row]; } case 'V': case 'v': { static const unsigned char b[7] = {17,17,17,17,17,10,4}; return b[row]; } case 'W': case 'w': { static const unsigned char b[7] = {17,17,17,21,21,21,10}; return b[row]; } case 'X': case 'x': { static const unsigned char b[7] = {17,17,10,4,10,17,17}; return b[row]; } case 'Y': case 'y': { static const unsigned char b[7] = {17,17,10,4,4,4,4}; return b[row]; } case 'Z': case 'z': { static const unsigned char b[7] = {31,1,2,4,8,16,31}; return b[row]; } case ':': { static const unsigned char b[7] = {0,4,4,0,4,4,0}; return b[row]; } case '-': { static const unsigned char b[7] = {0,0,0,14,0,0,0}; return b[row]; } case '/': { static const unsigned char b[7] = {1,1,2,4,8,16,16}; return b[row]; } case ',': { static const unsigned char b[7] = {0,0,0,0,4,4,8}; return b[row]; } case '.': { static const unsigned char b[7] = {0,0,0,0,0,12,12}; return b[row]; } case ' ': default: return 0; } } static void ecex_draw_mini_text_i(ecex_draw_context_t *ctx, int x, int y, const char *text) { int scale = 2; int advance = 12; int cx = x; const char *p = text; if (!ctx || !text) return; while (*p) { int row; for (row = 0; row < 7; ++row) { unsigned char bits = ecex_ascii5x7_bits(*p, row); int col; for (col = 0; col < 5; ++col) { if (bits & (1u << (4 - col))) { ecex_draw_rect_i(ctx, cx + col * scale, y + row * scale, scale, scale); } } } cx += advance; ++p; } } static int ecex_draw_has_loaded_font(ecex_draw_context_t *ctx) { app_t *app = draw_context_app(ctx); return app && app->font.texture != 0; } static void ecex_draw_host_text_i(ecex_draw_context_t *ctx, int x, int y, const char *text) { if (!ctx || !text) return; if (ecex_draw_has_loaded_font(ctx)) { ecex_draw_text(ctx, (float)x, (float)y, text); } else { ecex_draw_mini_text_i(ctx, x, y, text); } } static void ecex_draw_color_packed_rgba8_i(ecex_draw_context_t *ctx, unsigned int rgba) { ecex_draw_color_rgba8_i(ctx, (int)((rgba >> 24) & 0xffu), (int)((rgba >> 16) & 0xffu), (int)((rgba >> 8) & 0xffu), (int)(rgba & 0xffu)); } void ecex_draw_plugin_text_i(ecex_draw_context_t *ctx, void *owner, int id, int x, int y) { app_t *app; const char *text; if (!ctx || !ctx->internal) return; app = (app_t *)ctx->internal; if (!app || !app->ed) return; text = ecex_plugin_text_get_drawable(app->ed, owner, id); if (!text) text = ""; /* * Plugin-safe real-font path: plugin code passes only plugin/id and integer * coordinates. The string itself lives in plugin-owned host storage, and the * actual font renderer is called here on the host side, not directly from * JIT-owned stack/string memory. */ ecex_draw_host_text_i(ctx, x, y, text); } void ecex_draw_plugin_text_rect_i(ecex_draw_context_t *ctx, void *owner, int id, int x, int y, int w, int h, int padding, unsigned int bg_rgba, unsigned int fg_rgba) { app_t *app; const char *text; if (!ctx || !ctx->internal || w <= 0 || h <= 0) return; app = (app_t *)ctx->internal; if (!app || !app->ed) return; if (padding < 0) padding = 0; text = ecex_plugin_text_get_drawable(app->ed, owner, id); if (!text) text = ""; ecex_draw_color_packed_rgba8_i(ctx, bg_rgba); ecex_draw_rect_i(ctx, x, y, w, h); ecex_draw_color_packed_rgba8_i(ctx, fg_rgba); ecex_draw_host_text_i(ctx, x + padding, y + padding, text); } int ecex_draw_context_height_px(ecex_draw_context_t *ctx) { if (!ctx) return 0; return (int)ctx->h; } int ecex_draw_context_line_height_px(ecex_draw_context_t *ctx) { int line_h; if (!ctx) return 18; line_h = (int)ctx->line_height; return line_h < 18 ? 18 : line_h; } int ecex_markdown_body_y_px(ecex_draw_context_t *ctx) { int line_h; if (!ctx) return 36; line_h = ecex_draw_context_line_height_px(ctx); return (int)ctx->content_y + line_h * 2; } static int md_host_strlen(const char *s) { int n = 0; if (!s) return 0; while (s[n]) ++n; return n; } static int md_host_is_digit(char c) { return c >= '0' && c <= '9'; } static const char *md_host_skip_indent(const char *line) { if (!line) return ""; while (*line == ' ' || *line == '\t') ++line; return line; } static int md_host_line_is_fence(const char *line) { const char *p = md_host_skip_indent(line); return (p[0] == '`' && p[1] == '`' && p[2] == '`') || (p[0] == '~' && p[1] == '~' && p[2] == '~'); } static int md_host_line_is_hr(const char *line) { const char *p = md_host_skip_indent(line); char mark = 0; int count = 0; while (*p) { if (*p == ' ' || *p == '\t' || *p == '\r' || *p == '\n') { ++p; continue; } if (*p != '-' && *p != '*' && *p != '_') return 0; if (!mark) mark = *p; if (*p != mark) return 0; ++count; ++p; } return count >= 3; } static int md_host_heading_level(const char *line, const char **out_text) { const char *p = md_host_skip_indent(line); int level = 0; while (p[level] == '#' && level < 6) ++level; if (level > 0 && (p[level] == ' ' || p[level] == '\t' || p[level] == '\0')) { p += level; while (*p == ' ' || *p == '\t') ++p; if (out_text) *out_text = p; return level; } return 0; } static const char *md_host_list_text(const char *line) { const char *p = md_host_skip_indent(line); if ((p[0] == '-' || p[0] == '*' || p[0] == '+') && (p[1] == ' ' || p[1] == '\t')) { p += 2; while (*p == ' ' || *p == '\t') ++p; return p; } if (md_host_is_digit(p[0])) { const char *q = p; while (md_host_is_digit(*q)) ++q; if (*q == '.' && (q[1] == ' ' || q[1] == '\t')) { q += 2; while (*q == ' ' || *q == '\t') ++q; return q; } } return NULL; } static const char *md_host_quote_text(const char *line) { const char *p = md_host_skip_indent(line); if (*p != '>') return NULL; ++p; if (*p == ' ') ++p; return p; } static const char *md_host_trim_start(const char *text) { if (!text) return ""; while (*text == ' ' || *text == '\t') ++text; return text; } static int md_host_trim_len(const char *text) { int n; if (!text) return 0; while (*text == ' ' || *text == '\t') ++text; n = md_host_strlen(text); while (n > 0 && (text[n - 1] == ' ' || text[n - 1] == '\t')) --n; if (n > 224) n = 224; return n; } static void md_host_set_and_draw(ecex_draw_context_t *ctx, void *owner, int y, int style, const char *text) { app_t *app; const char *start; int len; if (!ctx || !ctx->internal) return; app = (app_t *)ctx->internal; if (!app || !app->ed) return; start = md_host_trim_start(text); len = md_host_trim_len(start); if (ecex_plugin_text_set((ecex_plugin_t *)owner, 2, start, len) == 0) { ecex_draw_markdown_line_auto_i(ctx, owner, 2, y, style); } } int ecex_markdown_draw_buffer_line_i(ecex_draw_context_t *ctx, void *owner, buffer_t *buffer, int line_index, int y, int in_code) { char line[512]; const char *text = NULL; int copied; int line_h; int heading; int next_in_code = in_code ? 1 : 0; int advance; app_t *app; if (!ctx || !ctx->internal || !buffer) return 18; app = (app_t *)ctx->internal; line_h = ecex_draw_context_line_height_px(ctx); copied = ecex_buffer_line_copy_text(buffer, line_index, line, (int)sizeof(line)); if (line_index < 4) { ecex_log_int("markdown_host_line: index=", line_index); ecex_log_int("markdown_host_line: copied=", copied); } if (copied < 0) return line_h; line[sizeof(line) - 1] = '\0'; if (md_host_line_is_fence(line)) { next_in_code = !next_in_code; if (app && app->ed) ecex_plugin_text_set((ecex_plugin_t *)owner, 2, next_in_code ? "code" : "end code", -1); ecex_draw_markdown_line_auto_i(ctx, owner, 2, y, 5); advance = line_h + 8; return advance | (next_in_code ? 0x10000 : 0); } if (next_in_code) { if (app && app->ed) ecex_plugin_text_set((ecex_plugin_t *)owner, 2, line, copied > 224 ? 224 : copied); ecex_draw_markdown_line_auto_i(ctx, owner, 2, y, 3); return line_h | 0x10000; } heading = md_host_heading_level(line, &text); if (heading) { md_host_set_and_draw(ctx, owner, y, 1, text); advance = line_h + (7 - heading) * 3 + 8; return advance; } if (md_host_line_is_hr(line)) { if (app && app->ed) ecex_plugin_text_set((ecex_plugin_t *)owner, 2, "", 0); ecex_draw_markdown_line_auto_i(ctx, owner, 2, y, 6); return line_h; } text = md_host_quote_text(line); if (text) { md_host_set_and_draw(ctx, owner, y, 2, text); return line_h; } text = md_host_list_text(line); if (text) { md_host_set_and_draw(ctx, owner, y, 4, text); return line_h; } if (line[0] == '\0') return line_h / 2; md_host_set_and_draw(ctx, owner, y, 0, line); return line_h; } void ecex_draw_markdown_canvas_i(ecex_draw_context_t *ctx, void *owner, int title_id, int x, int y, int w, int line_h) { app_t *app; const char *title; int full_w; int full_h; if (!ctx || !ctx->internal) return; app = (app_t *)ctx->internal; full_w = (int)ctx->w; full_h = (int)ctx->h; if (w < 1) w = full_w - x * 2; if (w < 1) w = 1; if (line_h < 18) line_h = 18; ecex_draw_color_rgba8_i(ctx, 29, 32, 33, 255); ecex_draw_rect_i(ctx, 0, 0, full_w, full_h); ecex_draw_color_rgba8_i(ctx, 250, 241, 199, 255); title = (app && app->ed) ? ecex_plugin_text_get_drawable(app->ed, owner, title_id) : ""; if (!title) title = ""; ecex_draw_text(ctx, (float)x, (float)y, title); ecex_draw_color_rgba8_i(ctx, 80, 73, 69, 255); ecex_draw_line_i(ctx, x, y + line_h + 8, x + w, y + line_h + 8, 1); } void ecex_draw_markdown_text_i(ecex_draw_context_t *ctx, void *owner, int text_id, int x, int y, int w, int line_h, int style) { app_t *app; const char *text; int h; if (!ctx || !ctx->internal) return; app = (app_t *)ctx->internal; text = (app && app->ed) ? ecex_plugin_text_get_drawable(app->ed, owner, text_id) : ""; if (!text) text = ""; if (w < 1) w = 1; if (line_h < 18) line_h = 18; h = line_h + 4; switch (style) { case 1: /* heading */ ecex_draw_color_rgba8_i(ctx, 69, 84, 96, 255); ecex_draw_rect_i(ctx, x, y - 5, w, h + 6); ecex_draw_color_rgba8_i(ctx, 250, 189, 47, 255); ecex_draw_text(ctx, (float)(x + 10), (float)y, text); break; case 2: /* quote */ ecex_draw_color_rgba8_i(ctx, 131, 165, 152, 255); ecex_draw_rect_i(ctx, x, y - 2, 4, h); ecex_draw_color_rgba8_i(ctx, 213, 196, 161, 255); ecex_draw_text(ctx, (float)(x + 14), (float)y, text); break; case 3: /* code */ ecex_draw_color_rgba8_i(ctx, 40, 40, 40, 255); ecex_draw_rect_i(ctx, x, y - 2, w, h); ecex_draw_color_rgba8_i(ctx, 213, 196, 161, 255); ecex_draw_text(ctx, (float)(x + 10), (float)y, text); break; case 4: /* list */ ecex_draw_color_rgba8_i(ctx, 184, 187, 38, 255); ecex_draw_rect_i(ctx, x + 6, y + line_h / 2 - 3, 6, 6); ecex_draw_color_rgba8_i(ctx, 235, 219, 178, 255); ecex_draw_text(ctx, (float)(x + 24), (float)y, text); break; case 5: /* fence */ ecex_draw_color_rgba8_i(ctx, 80, 73, 69, 255); ecex_draw_rect_i(ctx, x, y - 3, w, line_h + 6); ecex_draw_color_rgba8_i(ctx, 142, 192, 124, 255); ecex_draw_text(ctx, (float)(x + 10), (float)y, text); break; case 6: /* hr */ ecex_draw_color_rgba8_i(ctx, 80, 73, 69, 255); ecex_draw_line_i(ctx, x, y + line_h / 2, x + w, y + line_h / 2, 2); break; case 0: default: ecex_draw_color_rgba8_i(ctx, 235, 219, 178, 255); ecex_draw_text(ctx, (float)x, (float)y, text); break; } } void ecex_draw_markdown_canvas_auto_i(ecex_draw_context_t *ctx, void *owner, int title_id) { int x; int y; int w; int line_h; if (!ctx) return; ecex_log("draw_markdown_canvas_auto: enter"); x = (int)ctx->content_x; y = (int)ctx->content_y; w = (int)ctx->content_w; line_h = (int)ctx->line_height; if (line_h < 18) line_h = 18; if (w < 1) w = (int)ctx->w - x * 2; if (w < 1) w = 1; ecex_log("draw_markdown_canvas_auto: dispatch"); ecex_draw_markdown_canvas_i(ctx, owner, title_id, x, y, w, line_h); ecex_log("draw_markdown_canvas_auto: leave"); } void ecex_draw_markdown_line_auto_i(ecex_draw_context_t *ctx, void *owner, int text_id, int y, int style) { int x; int w; int line_h; if (!ctx) return; x = (int)ctx->content_x; w = (int)ctx->content_w; line_h = (int)ctx->line_height; if (line_h < 18) line_h = 18; if (w < 1) w = (int)ctx->w - x * 2; if (w < 1) w = 1; ecex_draw_markdown_text_i(ctx, owner, text_id, x, y, w, line_h, style); } void ecex_draw_label_i(ecex_draw_context_t *ctx, int x, int y, int label_id) { ecex_draw_host_text_i(ctx, x, y, ecex_draw_label_text(label_id)); } static void ecex_i32_to_ascii(int value, char *buf, size_t cap) { char tmp[16]; size_t n = 0; size_t out = 0; unsigned int v; if (!buf || cap == 0) return; if (value < 0) { if (out + 1 < cap) buf[out++] = '-'; v = (unsigned int)(-value); } else { v = (unsigned int)value; } do { tmp[n++] = (char)('0' + (v % 10u)); v /= 10u; } while (v && n < sizeof(tmp)); while (n && out + 1 < cap) buf[out++] = tmp[--n]; buf[out] = '\0'; } void ecex_draw_stat_i(ecex_draw_context_t *ctx, int x, int y, int label_id, int value) { char num[24]; char text[128]; const char *prefix = ecex_draw_label_text(label_id); if (!ctx || !prefix) return; ecex_i32_to_ascii(value, num, sizeof(num)); snprintf(text, sizeof(text), "%s%s", prefix, num); ecex_draw_host_text_i(ctx, x, y, text); } static int ecex_tetris_preview_shape_cell(int piece, int rot, int col, int row) { int p = piece % 7; int r = rot & 3; if (p < 0) p += 7; if (col < 0 || col >= 4 || row < 0 || row >= 4) return 0; if (p == 0) { if ((r & 1) == 0) return row == 1; return col == 1; } if (p == 1) return (row == 1 || row == 2) && (col == 1 || col == 2); if (p == 2) { if (r == 0) return (row == 1 && col >= 0 && col <= 2) || (row == 2 && col == 1); if (r == 1) return (col == 1 && row >= 0 && row <= 2) || (row == 1 && col == 2); if (r == 2) return (row == 1 && col >= 0 && col <= 2) || (row == 0 && col == 1); return (col == 1 && row >= 0 && row <= 2) || (row == 1 && col == 0); } if (p == 3) { if ((r & 1) == 0) return (row == 1 && (col == 1 || col == 2)) || (row == 2 && (col == 0 || col == 1)); return (col == 1 && (row == 0 || row == 1)) || (col == 2 && (row == 1 || row == 2)); } if (p == 4) { if ((r & 1) == 0) return (row == 1 && (col == 0 || col == 1)) || (row == 2 && (col == 1 || col == 2)); return (col == 2 && (row == 0 || row == 1)) || (col == 1 && (row == 1 || row == 2)); } if (p == 5) { if (r == 0) return (row == 1 && col >= 0 && col <= 2) || (row == 0 && col == 0); if (r == 1) return (col == 1 && row >= 0 && row <= 2) || (row == 0 && col == 2); if (r == 2) return (row == 1 && col >= 0 && col <= 2) || (row == 2 && col == 2); return (col == 1 && row >= 0 && row <= 2) || (row == 2 && col == 0); } if (r == 0) return (row == 1 && col >= 0 && col <= 2) || (row == 0 && col == 2); if (r == 1) return (col == 1 && row >= 0 && row <= 2) || (row == 2 && col == 2); if (r == 2) return (row == 1 && col >= 0 && col <= 2) || (row == 2 && col == 0); return (col == 1 && row >= 0 && row <= 2) || (row == 0 && col == 0); } static void ecex_draw_tetris_preview_color(ecex_draw_context_t *ctx, int piece, int alpha) { if (piece == 0) ecex_draw_color_rgba8_i(ctx, 51, 191, 242, alpha); else if (piece == 1) ecex_draw_color_rgba8_i(ctx, 242, 217, 51, alpha); else if (piece == 2) ecex_draw_color_rgba8_i(ctx, 179, 89, 242, alpha); else if (piece == 3) ecex_draw_color_rgba8_i(ctx, 77, 217, 89, alpha); else if (piece == 4) ecex_draw_color_rgba8_i(ctx, 242, 64, 64, alpha); else if (piece == 5) ecex_draw_color_rgba8_i(ctx, 64, 102, 242, alpha); else ecex_draw_color_rgba8_i(ctx, 242, 140, 51, alpha); } void ecex_draw_tetris_preview_i(ecex_draw_context_t *ctx, int piece, int x, int y, int cell, int alpha) { int r; int c; int p = piece % 7; if (!ctx) return; if (p < 0) p += 7; if (cell < 3) cell = 3; if (alpha < 0) alpha = 0; if (alpha > 255) alpha = 255; /* Clear a small preview box first so the old preview shape cannot linger * when the new piece occupies fewer cells than the previous I piece. */ ecex_draw_color_rgba8_i(ctx, 20, 23, 28, 255); ecex_draw_rect_i(ctx, x - 2, y - 2, cell * 4 + 4, cell * 4 + 4); for (r = 0; r < 4; ++r) { for (c = 0; c < 4; ++c) { if (!ecex_tetris_preview_shape_cell(p, 0, c, r)) continue; ecex_draw_tetris_preview_color(ctx, p, alpha); ecex_draw_rect_i(ctx, x + c * cell + 1, y + r * cell + 1, cell - 2, cell - 2); } } } void ecex_draw_rgba(ecex_draw_context_t *ctx, float x, float y, float w, float h, const unsigned char *rgba, int image_w, int image_h) { if (!ctx || !rgba || image_w <= 0 || image_h <= 0 || w <= 0.0f || h <= 0.0f) return; GLuint tex = 0; glGenTextures(1, &tex); if (tex == 0) return; glEnable(GL_TEXTURE_2D); glBindTexture(GL_TEXTURE_2D, tex); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, image_w, image_h, 0, GL_RGBA, GL_UNSIGNED_BYTE, rgba); glColor4f(1.0f, 1.0f, 1.0f, 1.0f); glBegin(GL_QUADS); glTexCoord2f(0.0f, 0.0f); draw_context_vertex(ctx, x, y); glTexCoord2f(1.0f, 0.0f); draw_context_vertex(ctx, x + w, y); glTexCoord2f(1.0f, 1.0f); draw_context_vertex(ctx, x + w, y + h); glTexCoord2f(0.0f, 1.0f); draw_context_vertex(ctx, x, y + h); glEnd(); glBindTexture(GL_TEXTURE_2D, 0); glDeleteTextures(1, &tex); glDisable(GL_TEXTURE_2D); } static ecex_draw_context_t make_draw_context(app_t *app, view_rect_t rect, size_t index) { ui_metrics_t m = ui_metrics(app); ecex_draw_context_t ctx; memset(&ctx, 0, sizeof(ctx)); ctx.x = rect.x; ctx.y = rect.y; ctx.w = rect.w; ctx.h = rect.h; ctx.content_x = m.content_x; ctx.content_y = m.content_top; ctx.content_w = rect.w - m.content_x * 2.0f; ctx.content_h = rect.h - m.content_top - m.content_bottom_pad; if (ctx.content_w < 0.0f) ctx.content_w = 0.0f; if (ctx.content_h < 0.0f) ctx.content_h = 0.0f; ctx.font_size = app->font.size_px; ctx.line_height = app->font.line_height; ctx.char_width = mono_cell_width(app); ctx.window_index = index; ctx.active = index == app->ed->current_window_index; ctx.internal = app; return ctx; } static void render_custom_buffer(app_t *app, buffer_t *buf, view_rect_t rect, size_t index) { if (!app || !buf || !buf->render_fn) return; ecex_draw_context_t ctx = make_draw_context(app, rect, index); int trace_callbacks = 0; const char *trace_env = getenv("ECEX_TRACE_CALLBACKS"); trace_callbacks = trace_env && trace_env[0] && trace_env[0] != '0'; if (trace_callbacks) { char msg[256]; snprintf(msg, sizeof(msg), "render callback start buffer=%p fn=%p userdata=%p window=%zu %.1fx%.1f", (void *)buf, (void *)buf->render_fn, buf->render_userdata, index, rect.w, rect.h); ecex_log_group_begin(msg); } int result = buf->render_fn(app->ed, buf, &ctx, buf->render_userdata); if (trace_callbacks) { char msg[128]; snprintf(msg, sizeof(msg), "render callback end buffer=%p result=%d", (void *)buf, result); ecex_log_group_end(msg); } } 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); int replace_content = buf->render_fn && (buf->render_flags & ECEX_RENDER_REPLACE_CONTENT); if (!replace_content) { 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 (buf->media_kind != ECEX_MEDIA_NONE) { draw_media_buffer(app, buf, rect); } if (index == app->ed->current_window_index && buf->media_kind == ECEX_MEDIA_NONE) { render_cursor(app, buf, rect); } } if (buf->render_fn) { render_custom_buffer(app, buf, rect, index); } 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); char frame_start[128]; snprintf(frame_start, sizeof(frame_start), "frame start size=%dx%d windows=%zu", app->width, app->height, ecex_window_count(app->ed)); ecex_log_group_begin(frame_start); 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); ecex_log_group_end("frame end"); }