esc
Externally Scriptable Editor
git clone git://mccd.space/esc
| Log | Files | Refs | README |
commit ea79a68bf6f26d5ae61c101d53e5a93b5e2d8eca parent a88cbab0d1f560f8336cee6e055e003285381acf Author: Marc Coquand <marc@coquand.email> Date: Mon, 23 Feb 2026 21:37:26 +0100 Refactor - renderer Diffstat:
| M | editor.c | | | 25 | +++++++++++++++++++++++++ |
| M | editor.h | | | 8 | ++++++++ |
| M | main.c | | | 307 | +++---------------------------------------------------------------------------- |
| A | renderer.c | | | 277 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | renderer.h | | | 27 | +++++++++++++++++++++++++++ |
5 files changed, 345 insertions(+), 299 deletions(-)
diff --git a/editor.c b/editor.c
@@ -602,6 +602,31 @@ bool editor_has_selection(Editor *ed) {
return ed->selection_anchor != ed->cursor_idx;
}
+VisualPos editor_byte_to_visual_pos(const Editor *ed, int byte_idx) {
+ VisualPos pos = {0, 0};
+ const char *ptr = ed->text.data;
+ while (ptr < ed->text.data + byte_idx) {
+ int current_idx = (int)(ptr - ed->text.data);
+ EditorRange *r =
+ editor_get_replacement_at((Editor *)ed, current_idx);
+ if (r) {
+ pos.col += r->data.replacement.visual_cols;
+ ptr = ed->text.data + r->end_byte;
+ continue;
+ }
+ Uint32 cp = SDL_StepUTF8(&ptr, NULL);
+ if (cp == '\t') {
+ pos.col += TAB_SIZE - (pos.col % TAB_SIZE);
+ } else if (cp == '\n') {
+ pos.row++;
+ pos.col = 0;
+ } else {
+ pos.col++;
+ }
+ }
+ return pos;
+}
+
char *editor_get_selection(Editor *ed) {
if (ed->cursor_idx == ed->selection_anchor)
return NULL;
diff --git a/editor.h b/editor.h
@@ -37,6 +37,8 @@ typedef struct {
size_t ranges_count;
size_t ranges_capacity;
+ bool dirty;
+
// Metrics for coordinate calculations
int char_width;
float line_height;
@@ -83,4 +85,10 @@ char *editor_get_selection(Editor *ed);
void editor_select_word(Editor *ed);
bool editor_has_selection(Editor *ed);
EditorRange *editor_get_range_at(Editor *ed, int byte_idx);
+
+typedef struct {
+ int row;
+ int col;
+} VisualPos;
+VisualPos editor_byte_to_visual_pos(const Editor *ed, int byte_idx);
#endif
diff --git a/main.c b/main.c
@@ -1,5 +1,6 @@
#include "editor.h"
#include "fuse_ipc.h"
+#include "renderer.h"
#include "unix_utils.h"
#include <SDL3/SDL.h>
#include <SDL3/SDL_events.h>
@@ -211,303 +212,6 @@ void handle_events(Editor *ed, SDL_Renderer *renderer, float *scroll_x,
}
}
-void render_editor(SDL_Renderer *renderer, Editor *ed, TTF_Font *font,
- TTF_Font *bold_font, int char_width, float line_height,
- float cursor_height, float scroll_x, float scroll_y) {
- int render_w, render_h;
- SDL_GetRenderOutputSize(renderer, &render_w, &render_h);
-
- SDL_SetRenderDrawColor(renderer, 255, 255, 255, 255);
- SDL_RenderClear(renderer);
-
- if (ed->cursor_idx != ed->selection_anchor) {
- int sel_min = (ed->cursor_idx < ed->selection_anchor)
- ? ed->cursor_idx
- : ed->selection_anchor;
- int sel_max = (ed->cursor_idx > ed->selection_anchor)
- ? ed->cursor_idx
- : ed->selection_anchor;
-
- SDL_SetRenderDrawColor(renderer, 200, 220, 255,
- 255); // Light Blue
-
- int cur_row = 0, cur_col = 0;
- const char *p = ed->text.data;
- while (p < ed->text.data + ed->text.len) {
- int start_of_char_idx = (int)(p - ed->text.data);
-
- EditorRange *r =
- editor_get_range_at(ed, start_of_char_idx);
- if (r && r->type == RANGE_REPLACEMENT) {
- if (start_of_char_idx >= sel_min &&
- start_of_char_idx < sel_max) {
- SDL_FRect sel_rect = {
- 20.0f + (cur_col * char_width) -
- scroll_x,
- 20.0f + (cur_row * line_height) -
- scroll_y,
- (float)(r->data.replacement
- .visual_cols *
- char_width),
- cursor_height};
- SDL_RenderFillRect(renderer, &sel_rect);
- }
- cur_col += r->data.replacement.visual_cols;
- p = ed->text.data + r->end_byte;
- if ((int)(p - ed->text.data) >= sel_max)
- break;
- continue;
- }
-
- Uint32 cp = SDL_StepUTF8(&p, NULL);
- int cols_for_char = 1;
- if (cp == '\t') {
- cols_for_char = TAB_SIZE - (cur_col % TAB_SIZE);
- }
-
- if (start_of_char_idx >= sel_min &&
- start_of_char_idx < sel_max) {
- SDL_FRect sel_rect = {
- 20.0f + (cur_col * char_width) - scroll_x,
- 20.0f + (cur_row * line_height) - scroll_y,
- (float)(cols_for_char * char_width),
- cursor_height};
- SDL_RenderFillRect(renderer, &sel_rect);
- }
-
- if (cp == '\n') {
- cur_row++;
- cur_col = 0;
- } else {
- cur_col += cols_for_char;
- }
-
- if ((int)(p - ed->text.data) >= sel_max)
- break;
- }
- }
-
- if (font && ed->text.len > 0) {
- SDL_Color black = {0, 0, 0, 255};
- float current_y = 20.0f - scroll_y;
-
- const char *line_start = ed->text.data;
- while (line_start != NULL && *line_start != '\0') {
- const char *line_end = strchr(line_start, '\n');
- size_t line_len = line_end
- ? (size_t)(line_end - line_start)
- : strlen(line_start);
-
- if (line_len >= 0 && current_y + line_height > 0 &&
- current_y < render_h) {
- float current_x = 20.0f - scroll_x;
- int vis_col = 0;
-
- size_t i = 0;
- while (i < line_len) {
- size_t abs_idx =
- (line_start - ed->text.data) + i;
-
- // 1. Check for replacements first (they
- // hide text, so they take priority)
- EditorRange *rep =
- editor_get_replacement_at(
- ed, (int)abs_idx);
- size_t chunk_end = i;
-
- if (rep) {
- // Chunk is the entire
- // replacement (clamped to line
- // end)
- size_t range_relative_end =
- (size_t)(rep->end_byte -
- (line_start -
- ed->text.data));
- chunk_end =
- (range_relative_end <
- line_len)
- ? range_relative_end
- : line_len;
-
- // Draw replacement spaces
- int spaces =
- rep->data.replacement
- .visual_cols;
- if (spaces > 0) {
- current_x +=
- (spaces *
- char_width);
- vis_col += spaces;
- }
- } else {
- // 2. It's regular text. Get the
- // combined format for this
- // starting byte.
- RangeFormat current_format =
- editor_get_format_at(
- ed, (int)abs_idx);
- chunk_end = i + 1;
-
- // Scan forward to find where
- // the formatting changes
- while (chunk_end < line_len) {
- size_t next_abs_idx =
- (line_start -
- ed->text.data) +
- chunk_end;
-
- // Stop if a replacement
- // range begins
- if (editor_get_replacement_at(
- ed,
- (int)
- next_abs_idx)) {
- break;
- }
-
- // Stop if the combined
- // formatting changes
- RangeFormat next_format =
- editor_get_format_at(
- ed,
- (int)
- next_abs_idx);
- if (next_format.bold !=
- current_format
- .bold ||
- next_format
- .italic !=
- current_format
- .italic) {
- break;
- }
-
- chunk_end++;
- }
-
- // 3. Render the chunk using the
- // appropriate font (If you add
- // an italic font later, you'd
- // select it here too)
- TTF_Font *active_font =
- current_format.bold
- ? bold_font
- : font;
-
- // Extract and process tab/UTF-8
- // for this chunk
- char *chunk_text = malloc(
- (chunk_end - i) * TAB_SIZE +
- 1);
- int ct_idx = 0;
- for (size_t j = i;
- j < chunk_end; j++) {
- if (line_start[j] ==
- '\t') {
- int spaces =
- TAB_SIZE -
- (vis_col %
- TAB_SIZE);
- for (int s = 0;
- s < spaces;
- s++)
- chunk_text
- [ct_idx++] =
- ' ';
- vis_col +=
- spaces;
- } else {
- if ((line_start
- [j] &
- 0xC0) !=
- 0x80)
- vis_col++;
- chunk_text
- [ct_idx++] =
- line_start
- [j];
- }
- }
- chunk_text[ct_idx] = '\0';
-
- if (ct_idx > 0) {
- SDL_Surface *surf =
- TTF_RenderText_Blended(
- active_font,
- chunk_text,
- ct_idx, black);
- if (surf) {
- SDL_Texture *tex =
- SDL_CreateTextureFromSurface(
- renderer,
- surf);
- SDL_FRect dst =
- {current_x,
- current_y,
- (float)surf
- ->w,
- (float)surf
- ->h};
- SDL_RenderTexture(
- renderer,
- tex, NULL,
- &dst);
- current_x +=
- surf->w;
- SDL_DestroyTexture(
- tex);
- SDL_DestroySurface(
- surf);
- }
- }
- free(chunk_text);
- }
-
- // Move to the start of the next chunk
- i = chunk_end;
- }
- // Reset font style for safety
- TTF_SetFontStyle(font, TTF_STYLE_NORMAL);
- }
- current_y += line_height;
- line_start = line_end ? line_end + 1 : NULL;
- }
- }
-
- int cur_row = 0, cur_col = 0;
- const char *ptr = ed->text.data;
- while (ptr < ed->text.data + ed->cursor_idx) {
- int current_idx = (int)(ptr - ed->text.data);
- EditorRange *r = editor_get_replacement_at(ed, current_idx);
-
- if (r) {
- cur_col += r->data.replacement.visual_cols;
- ptr = ed->text.data + r->end_byte;
- continue;
- }
-
- Uint32 cp = SDL_StepUTF8(&ptr, NULL);
- if (cp == '\t') {
- cur_col += TAB_SIZE - (cur_col % TAB_SIZE);
- } else if (cp == '\n') {
- cur_row++;
- cur_col = 0;
- } else {
- cur_col++;
- }
- }
-
- SDL_FRect cursor_rect = {20.0f + (cur_col * char_width) - scroll_x,
- 20.0f + (cur_row * line_height) - scroll_y,
- 2.0f, (float)cursor_height};
-
- if (cursor_rect.y + line_height > 0 && cursor_rect.y < render_h) {
- SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255);
- SDL_RenderFillRect(renderer, &cursor_rect);
- }
-
- SDL_RenderPresent(renderer);
-}
static void sig_quit(int sig)
{
@@ -550,6 +254,10 @@ int main(int argc, char *argv[]) {
}
FuseIPC *ipc = fuse_ipc_start(ed);
+ RenderCtx ctx;
+ render_ctx_init(&ctx, renderer, font, bold_font, char_width,
+ line_height, cursor_height);
+
float scroll_x = 0.0f;
float scroll_y = 0.0f;
bool running = true;
@@ -561,9 +269,10 @@ int main(int argc, char *argv[]) {
handle_events(ed, renderer, &scroll_x, &scroll_y, line_height,
&running, &is_dragging);
+ ctx.scroll_x = scroll_x;
+ ctx.scroll_y = scroll_y;
editor_lock(ed);
- render_editor(renderer, ed, font, bold_font, char_width,
- line_height, cursor_height, scroll_x, scroll_y);
+ render_editor(&ctx, ed);
editor_unlock(ed);
}
diff --git a/renderer.c b/renderer.c
@@ -0,0 +1,277 @@
+#include "renderer.h"
+#include <SDL3/SDL.h>
+#include <SDL3_ttf/SDL_ttf.h>
+#include <stdbool.h>
+#include <stdlib.h>
+#include <string.h>
+
+static float col_to_screen_x(const RenderCtx *ctx, int col)
+{
+ return ctx->margin + col * ctx->char_width - ctx->scroll_x;
+}
+
+static float row_to_screen_y(const RenderCtx *ctx, int row)
+{
+ return ctx->margin + row * ctx->line_height - ctx->scroll_y;
+}
+
+static bool is_row_visible(const RenderCtx *ctx, int row)
+{
+ float y = row_to_screen_y(ctx, row);
+ return y + ctx->line_height > 0 && y < ctx->render_h;
+}
+
+static void render_selection_rect(const RenderCtx *ctx, VisualPos pos,
+ int col_width)
+{
+ SDL_FRect rect = {col_to_screen_x(ctx, pos.col),
+ row_to_screen_y(ctx, pos.row),
+ (float)(col_width * ctx->char_width),
+ ctx->cursor_height};
+ SDL_RenderFillRect(ctx->renderer, &rect);
+}
+
+static void render_selection(const RenderCtx *ctx, const Editor *ed)
+{
+ if (ed->cursor_idx == ed->selection_anchor)
+ return;
+
+ int sel_min = ed->cursor_idx < ed->selection_anchor
+ ? ed->cursor_idx
+ : ed->selection_anchor;
+ int sel_max = ed->cursor_idx > ed->selection_anchor
+ ? ed->cursor_idx
+ : ed->selection_anchor;
+
+ SDL_SetRenderDrawColor(ctx->renderer, 200, 220, 255, 255);
+
+ VisualPos cur = {0, 0};
+ const char *p = ed->text.data;
+ while (p < ed->text.data + ed->text.len) {
+ int start_of_char_idx = (int)(p - ed->text.data);
+
+ EditorRange *r =
+ editor_get_range_at((Editor *)ed, start_of_char_idx);
+ if (r && r->type == RANGE_REPLACEMENT) {
+ if (start_of_char_idx >= sel_min &&
+ start_of_char_idx < sel_max)
+ render_selection_rect(
+ ctx, cur,
+ r->data.replacement.visual_cols);
+ cur.col += r->data.replacement.visual_cols;
+ p = ed->text.data + r->end_byte;
+ if ((int)(p - ed->text.data) >= sel_max)
+ break;
+ continue;
+ }
+
+ Uint32 cp = SDL_StepUTF8(&p, NULL);
+ int cols_for_char = 1;
+ if (cp == '\t')
+ cols_for_char = TAB_SIZE - (cur.col % TAB_SIZE);
+
+ if (start_of_char_idx >= sel_min &&
+ start_of_char_idx < sel_max)
+ render_selection_rect(ctx, cur, cols_for_char);
+
+ if (cp == '\n') {
+ cur.row++;
+ cur.col = 0;
+ } else {
+ cur.col += cols_for_char;
+ }
+
+ if ((int)(p - ed->text.data) >= sel_max)
+ break;
+ }
+}
+
+/*
+ * Expand tabs to spaces in src[0..len), writing into out (which must be at
+ * least len*TAB_SIZE+1 bytes). vis_col_in is the visual column at the start
+ * of the chunk. Returns the number of bytes written (not counting the NUL).
+ */
+static int expand_tabs(const char *src, size_t len, char *out, int vis_col_in)
+{
+ int vis_col = vis_col_in;
+ int out_idx = 0;
+ for (size_t j = 0; j < len; j++) {
+ if (src[j] == '\t') {
+ int spaces = TAB_SIZE - (vis_col % TAB_SIZE);
+ for (int s = 0; s < spaces; s++)
+ out[out_idx++] = ' ';
+ vis_col += spaces;
+ } else {
+ if ((src[j] & 0xC0) != 0x80)
+ vis_col++;
+ out[out_idx++] = src[j];
+ }
+ }
+ out[out_idx] = '\0';
+ return out_idx;
+}
+
+/*
+ * Scan forward from rel_start within a line to where the format or a
+ * replacement range begins. Returns the exclusive end index (relative to
+ * line_start).
+ */
+static size_t find_chunk_end(const Editor *ed, const char *line_start,
+ size_t rel_start, size_t line_len,
+ RangeFormat fmt)
+{
+ size_t chunk_end = rel_start + 1;
+ while (chunk_end < line_len) {
+ size_t next_abs =
+ (size_t)(line_start - ed->text.data) + chunk_end;
+ if (editor_get_replacement_at((Editor *)ed, (int)next_abs))
+ break;
+ RangeFormat next_fmt =
+ editor_get_format_at((Editor *)ed, (int)next_abs);
+ if (next_fmt.bold != fmt.bold || next_fmt.italic != fmt.italic)
+ break;
+ chunk_end++;
+ }
+ return chunk_end;
+}
+
+/*
+ * Blit one format-uniform run of already-expanded text. Returns the new x
+ * position (x + pixels rendered).
+ */
+static float render_chunk(const RenderCtx *ctx, const char *expanded,
+ int expanded_len, TTF_Font *font, float x, float y)
+{
+ if (expanded_len <= 0)
+ return x;
+ SDL_Color black = {0, 0, 0, 255};
+ SDL_Surface *surf =
+ TTF_RenderText_Blended(font, expanded, expanded_len, black);
+ if (!surf)
+ return x;
+ SDL_Texture *tex = SDL_CreateTextureFromSurface(ctx->renderer, surf);
+ SDL_FRect dst = {x, y, (float)surf->w, (float)surf->h};
+ SDL_RenderTexture(ctx->renderer, tex, NULL, &dst);
+ float new_x = x + surf->w;
+ SDL_DestroyTexture(tex);
+ SDL_DestroySurface(surf);
+ return new_x;
+}
+
+/*
+ * Render one line of text, splitting it into format-uniform chunks and
+ * skipping over replacement ranges.
+ */
+static void render_line(const RenderCtx *ctx, const Editor *ed,
+ const char *line_start, size_t line_len, float x,
+ float y)
+{
+ int vis_col = 0;
+ size_t i = 0;
+ while (i < line_len) {
+ size_t abs_idx = (size_t)(line_start - ed->text.data) + i;
+
+ EditorRange *rep =
+ editor_get_replacement_at((Editor *)ed, (int)abs_idx);
+ if (rep) {
+ size_t range_rel_end =
+ (size_t)(rep->end_byte -
+ (int)(line_start - ed->text.data));
+ size_t chunk_end =
+ range_rel_end < line_len ? range_rel_end : line_len;
+ int spaces = rep->data.replacement.visual_cols;
+ x += spaces * ctx->char_width;
+ vis_col += spaces;
+ i = chunk_end;
+ continue;
+ }
+
+ RangeFormat fmt =
+ editor_get_format_at((Editor *)ed, (int)abs_idx);
+ size_t chunk_end =
+ find_chunk_end(ed, line_start, i, line_len, fmt);
+ TTF_Font *active_font = fmt.bold ? ctx->bold_font : ctx->font;
+
+ size_t raw_len = chunk_end - i;
+ char *buf = malloc(raw_len * TAB_SIZE + 1);
+ int expanded_len =
+ expand_tabs(line_start + i, raw_len, buf, vis_col);
+
+ /* Advance vis_col past this chunk. */
+ for (size_t j = i; j < chunk_end; j++) {
+ if (line_start[j] == '\t')
+ vis_col += TAB_SIZE - (vis_col % TAB_SIZE);
+ else if ((line_start[j] & 0xC0) != 0x80)
+ vis_col++;
+ }
+
+ x = render_chunk(ctx, buf, expanded_len, active_font, x, y);
+ free(buf);
+ i = chunk_end;
+ }
+}
+
+static void render_text(const RenderCtx *ctx, const Editor *ed)
+{
+ if (!ctx->font || ed->text.len == 0)
+ return;
+
+ int row = 0;
+ float current_y = ctx->margin - ctx->scroll_y;
+ const char *line_start = ed->text.data;
+ while (line_start != NULL && *line_start != '\0') {
+ const char *line_end = strchr(line_start, '\n');
+ size_t line_len = line_end ? (size_t)(line_end - line_start)
+ : strlen(line_start);
+
+ if (is_row_visible(ctx, row)) {
+ float x = ctx->margin - ctx->scroll_x;
+ render_line(ctx, ed, line_start, line_len, x,
+ current_y);
+ }
+
+ current_y += ctx->line_height;
+ row++;
+ line_start = line_end ? line_end + 1 : NULL;
+ }
+}
+
+static void render_cursor(const RenderCtx *ctx, const Editor *ed)
+{
+ VisualPos pos = editor_byte_to_visual_pos(ed, ed->cursor_idx);
+ float cx = col_to_screen_x(ctx, pos.col);
+ float cy = row_to_screen_y(ctx, pos.row);
+ if (cy + ctx->line_height > 0 && cy < ctx->render_h) {
+ SDL_FRect cursor_rect = {cx, cy, 2.0f, ctx->cursor_height};
+ SDL_SetRenderDrawColor(ctx->renderer, 0, 0, 0, 255);
+ SDL_RenderFillRect(ctx->renderer, &cursor_rect);
+ }
+}
+
+void render_ctx_init(RenderCtx *ctx, SDL_Renderer *renderer, TTF_Font *font,
+ TTF_Font *bold_font, int char_width, float line_height,
+ float cursor_height)
+{
+ ctx->renderer = renderer;
+ ctx->font = font;
+ ctx->bold_font = bold_font;
+ ctx->char_width = char_width;
+ ctx->line_height = line_height;
+ ctx->cursor_height = cursor_height;
+ ctx->scroll_x = 0.0f;
+ ctx->scroll_y = 0.0f;
+ ctx->render_w = 0;
+ ctx->render_h = 0;
+ ctx->margin = 20.0f;
+}
+
+void render_editor(RenderCtx *ctx, const Editor *ed)
+{
+ SDL_GetRenderOutputSize(ctx->renderer, &ctx->render_w, &ctx->render_h);
+ SDL_SetRenderDrawColor(ctx->renderer, 255, 255, 255, 255);
+ SDL_RenderClear(ctx->renderer);
+ render_selection(ctx, ed);
+ render_text(ctx, ed);
+ render_cursor(ctx, ed);
+ SDL_RenderPresent(ctx->renderer);
+}
diff --git a/renderer.h b/renderer.h
@@ -0,0 +1,27 @@
+#ifndef RENDERER_H
+#define RENDERER_H
+
+#include "editor.h"
+#include <SDL3/SDL.h>
+#include <SDL3_ttf/SDL_ttf.h>
+
+typedef struct {
+ SDL_Renderer *renderer;
+ TTF_Font *font;
+ TTF_Font *bold_font;
+ int char_width;
+ float line_height;
+ float cursor_height;
+ float scroll_x;
+ float scroll_y;
+ int render_w;
+ int render_h;
+ float margin;
+} RenderCtx;
+
+void render_ctx_init(RenderCtx *ctx, SDL_Renderer *renderer,
+ TTF_Font *font, TTF_Font *bold_font,
+ int char_width, float line_height, float cursor_height);
+void render_editor(RenderCtx *ctx, const Editor *ed);
+
+#endif