esc

Externally Scriptable Editor

git clone git://mccd.space/esc

commit ea79a68bf6f26d5ae61c101d53e5a93b5e2d8eca
parent a88cbab0d1f560f8336cee6e055e003285381acf
Author: Marc Coquand <marc@coquand.email>
Date:   Mon, 23 Feb 2026 21:37:26 +0100

Refactor - renderer

Diffstat:
Meditor.c | 25+++++++++++++++++++++++++
Meditor.h | 8++++++++
Mmain.c | 307+++----------------------------------------------------------------------------
Arenderer.c | 277+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Arenderer.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