diff options
Diffstat (limited to 'src/render.c')
| -rw-r--r-- | src/render.c | 613 |
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); +} |
