esc

Externally Scriptable Editor

git clone git://mccd.space/esc

renderer.c (8274B)

      1 #include "renderer.h"
      2 #include <SDL3/SDL.h>
      3 #include <SDL3_ttf/SDL_ttf.h>
      4 #include <stdbool.h>
      5 #include <stdlib.h>
      6 #include <string.h>
      7 
      8 static float col_to_screen_x(const RenderCtx *ctx, int col)
      9 {
     10 	return ctx->margin + col * ctx->char_width - ctx->scroll_x;
     11 }
     12 
     13 static float row_to_screen_y(const RenderCtx *ctx, int row)
     14 {
     15 	return ctx->margin + row * ctx->line_height - ctx->scroll_y;
     16 }
     17 
     18 static bool is_row_visible(const RenderCtx *ctx, int row)
     19 {
     20 	float y = row_to_screen_y(ctx, row);
     21 	return y + ctx->line_height > 0 && y < ctx->render_h;
     22 }
     23 
     24 static void render_selection_rect(const RenderCtx *ctx, VisualPos pos,
     25 				  int col_width)
     26 {
     27 	SDL_FRect rect = {col_to_screen_x(ctx, pos.col),
     28 			  row_to_screen_y(ctx, pos.row),
     29 			  (float)(col_width * ctx->char_width),
     30 			  ctx->cursor_height};
     31 	SDL_RenderFillRect(ctx->renderer, &rect);
     32 }
     33 
     34 static void render_selection(const RenderCtx *ctx, const Editor *ed)
     35 {
     36 	if (ed->cursor_idx == ed->selection_anchor)
     37 		return;
     38 
     39 	int sel_min = ed->cursor_idx < ed->selection_anchor
     40 			  ? ed->cursor_idx
     41 			  : ed->selection_anchor;
     42 	int sel_max = ed->cursor_idx > ed->selection_anchor
     43 			  ? ed->cursor_idx
     44 			  : ed->selection_anchor;
     45 
     46 	SDL_SetRenderDrawColor(ctx->renderer, 200, 220, 255, 255);
     47 
     48 	VisualPos cur = {0, 0};
     49 	const char *p = ed->text.data;
     50 	while (p < ed->text.data + ed->text.len) {
     51 		int start_of_char_idx = (int)(p - ed->text.data);
     52 
     53 		EditorRange *r =
     54 		    editor_get_range_at((Editor *)ed, start_of_char_idx);
     55 		if (r && r->type == RANGE_REPLACEMENT) {
     56 			if (start_of_char_idx >= sel_min &&
     57 			    start_of_char_idx < sel_max)
     58 				render_selection_rect(
     59 				    ctx, cur,
     60 				    r->data.replacement.visual_cols);
     61 			cur.col += r->data.replacement.visual_cols;
     62 			p = ed->text.data + r->end_byte;
     63 			if ((int)(p - ed->text.data) >= sel_max)
     64 				break;
     65 			continue;
     66 		}
     67 
     68 		Uint32 cp = SDL_StepUTF8(&p, NULL);
     69 		int cols_for_char = 1;
     70 		if (cp == '\t')
     71 			cols_for_char = TAB_SIZE - (cur.col % TAB_SIZE);
     72 
     73 		if (start_of_char_idx >= sel_min &&
     74 		    start_of_char_idx < sel_max)
     75 			render_selection_rect(ctx, cur, cols_for_char);
     76 
     77 		if (cp == '\n') {
     78 			cur.row++;
     79 			cur.col = 0;
     80 		} else {
     81 			cur.col += cols_for_char;
     82 		}
     83 
     84 		if ((int)(p - ed->text.data) >= sel_max)
     85 			break;
     86 	}
     87 }
     88 
     89 /*
     90  * Expand tabs to spaces in src[0..len), writing into out (which must be at
     91  * least len*TAB_SIZE+1 bytes).  vis_col_in is the visual column at the start
     92  * of the chunk.  Returns the number of bytes written (not counting the NUL).
     93  */
     94 static int expand_tabs(const char *src, size_t len, char *out, int vis_col_in)
     95 {
     96 	int vis_col = vis_col_in;
     97 	int out_idx = 0;
     98 	for (size_t j = 0; j < len; j++) {
     99 		if (src[j] == '\t') {
    100 			int spaces = TAB_SIZE - (vis_col % TAB_SIZE);
    101 			for (int s = 0; s < spaces; s++)
    102 				out[out_idx++] = ' ';
    103 			vis_col += spaces;
    104 		} else {
    105 			if ((src[j] & 0xC0) != 0x80)
    106 				vis_col++;
    107 			out[out_idx++] = src[j];
    108 		}
    109 	}
    110 	out[out_idx] = '\0';
    111 	return out_idx;
    112 }
    113 
    114 /*
    115  * Scan forward from rel_start within a line to where the format or a
    116  * replacement range begins.  Returns the exclusive end index (relative to
    117  * line_start).
    118  */
    119 static size_t find_chunk_end(const Editor *ed, const char *line_start,
    120 			     size_t rel_start, size_t line_len,
    121 			     RangeFormat fmt)
    122 {
    123 	size_t chunk_end = rel_start + 1;
    124 	while (chunk_end < line_len) {
    125 		size_t next_abs =
    126 		    (size_t)(line_start - ed->text.data) + chunk_end;
    127 		if (editor_get_replacement_at((Editor *)ed, (int)next_abs))
    128 			break;
    129 		RangeFormat next_fmt =
    130 		    editor_get_format_at((Editor *)ed, (int)next_abs);
    131 		if (next_fmt.bold != fmt.bold || next_fmt.italic != fmt.italic)
    132 			break;
    133 		chunk_end++;
    134 	}
    135 	return chunk_end;
    136 }
    137 
    138 /*
    139  * Blit one format-uniform run of already-expanded text.  Returns the new x
    140  * position (x + pixels rendered).
    141  */
    142 static float render_chunk(const RenderCtx *ctx, const char *expanded,
    143 			  int expanded_len, TTF_Font *font, float x, float y)
    144 {
    145 	if (expanded_len <= 0)
    146 		return x;
    147 	SDL_Color black = {0, 0, 0, 255};
    148 	SDL_Surface *surf =
    149 	    TTF_RenderText_Blended(font, expanded, expanded_len, black);
    150 	if (!surf)
    151 		return x;
    152 	SDL_Texture *tex = SDL_CreateTextureFromSurface(ctx->renderer, surf);
    153 	SDL_FRect dst = {x, y, (float)surf->w, (float)surf->h};
    154 	SDL_RenderTexture(ctx->renderer, tex, NULL, &dst);
    155 	float new_x = x + surf->w;
    156 	SDL_DestroyTexture(tex);
    157 	SDL_DestroySurface(surf);
    158 	return new_x;
    159 }
    160 
    161 /*
    162  * Render one line of text, splitting it into format-uniform chunks and
    163  * skipping over replacement ranges.
    164  */
    165 static void render_line(const RenderCtx *ctx, const Editor *ed,
    166 			const char *line_start, size_t line_len, float x,
    167 			float y)
    168 {
    169 	int vis_col = 0;
    170 	size_t i = 0;
    171 	while (i < line_len) {
    172 		size_t abs_idx = (size_t)(line_start - ed->text.data) + i;
    173 
    174 		EditorRange *rep =
    175 		    editor_get_replacement_at((Editor *)ed, (int)abs_idx);
    176 		if (rep) {
    177 			size_t range_rel_end =
    178 			    (size_t)(rep->end_byte -
    179 				     (int)(line_start - ed->text.data));
    180 			size_t chunk_end =
    181 			    range_rel_end < line_len ? range_rel_end : line_len;
    182 			int spaces = rep->data.replacement.visual_cols;
    183 			x += spaces * ctx->char_width;
    184 			vis_col += spaces;
    185 			i = chunk_end;
    186 			continue;
    187 		}
    188 
    189 		RangeFormat fmt =
    190 		    editor_get_format_at((Editor *)ed, (int)abs_idx);
    191 		size_t chunk_end =
    192 		    find_chunk_end(ed, line_start, i, line_len, fmt);
    193 		TTF_Font *active_font = fmt.bold ? ctx->bold_font : ctx->font;
    194 
    195 		size_t raw_len = chunk_end - i;
    196 		char *buf = malloc(raw_len * TAB_SIZE + 1);
    197 		int expanded_len =
    198 		    expand_tabs(line_start + i, raw_len, buf, vis_col);
    199 
    200 		/* Advance vis_col past this chunk. */
    201 		for (size_t j = i; j < chunk_end; j++) {
    202 			if (line_start[j] == '\t')
    203 				vis_col += TAB_SIZE - (vis_col % TAB_SIZE);
    204 			else if ((line_start[j] & 0xC0) != 0x80)
    205 				vis_col++;
    206 		}
    207 
    208 		x = render_chunk(ctx, buf, expanded_len, active_font, x, y);
    209 		free(buf);
    210 		i = chunk_end;
    211 	}
    212 }
    213 
    214 static void render_text(const RenderCtx *ctx, const Editor *ed)
    215 {
    216 	if (!ctx->font || ed->text.len == 0)
    217 		return;
    218 
    219 	int row = 0;
    220 	float current_y = ctx->margin - ctx->scroll_y;
    221 	const char *line_start = ed->text.data;
    222 	while (line_start != NULL && *line_start != '\0') {
    223 		const char *line_end = strchr(line_start, '\n');
    224 		size_t line_len = line_end ? (size_t)(line_end - line_start)
    225 					   : strlen(line_start);
    226 
    227 		if (is_row_visible(ctx, row)) {
    228 			float x = ctx->margin - ctx->scroll_x;
    229 			render_line(ctx, ed, line_start, line_len, x,
    230 				    current_y);
    231 		}
    232 
    233 		current_y += ctx->line_height;
    234 		row++;
    235 		line_start = line_end ? line_end + 1 : NULL;
    236 	}
    237 }
    238 
    239 static void render_cursor(const RenderCtx *ctx, const Editor *ed)
    240 {
    241 	VisualPos pos = editor_byte_to_visual_pos(ed, ed->cursor_idx);
    242 	float cx = col_to_screen_x(ctx, pos.col);
    243 	float cy = row_to_screen_y(ctx, pos.row);
    244 	if (cy + ctx->line_height > 0 && cy < ctx->render_h) {
    245 		SDL_FRect cursor_rect = {cx, cy, 2.0f, ctx->cursor_height};
    246 		SDL_SetRenderDrawColor(ctx->renderer, 0, 0, 0, 255);
    247 		SDL_RenderFillRect(ctx->renderer, &cursor_rect);
    248 	}
    249 }
    250 
    251 void render_ctx_init(RenderCtx *ctx, SDL_Renderer *renderer, TTF_Font *font,
    252 		     TTF_Font *bold_font, int char_width, float line_height,
    253 		     float cursor_height)
    254 {
    255 	ctx->renderer = renderer;
    256 	ctx->font = font;
    257 	ctx->bold_font = bold_font;
    258 	ctx->char_width = char_width;
    259 	ctx->line_height = line_height;
    260 	ctx->cursor_height = cursor_height;
    261 	ctx->scroll_x = 0.0f;
    262 	ctx->scroll_y = 0.0f;
    263 	ctx->render_w = 0;
    264 	ctx->render_h = 0;
    265 	ctx->margin = 20.0f;
    266 }
    267 
    268 static void render_separators(RenderCtx *ctx, const Editor *ed)
    269 {
    270 	for (int i = 0; i < ed->files_count - 1; i++) {
    271 		int boundary_byte = ed->files[i].buf_end;
    272 		VisualPos vp = editor_byte_to_visual_pos(ed, boundary_byte);
    273 		float y = ctx->margin + vp.row * ctx->line_height
    274 			  - ctx->scroll_y;
    275 		if (y >= 0 && y < (float)ctx->render_h) {
    276 			SDL_SetRenderDrawColor(ctx->renderer, 80, 80, 80, 255);
    277 			SDL_RenderLine(ctx->renderer, 0, y,
    278 				       (float)ctx->render_w, y);
    279 		}
    280 	}
    281 }
    282 
    283 void render_editor(RenderCtx *ctx, const Editor *ed)
    284 {
    285 	SDL_GetRenderOutputSize(ctx->renderer, &ctx->render_w, &ctx->render_h);
    286 	SDL_SetRenderDrawColor(ctx->renderer, 255, 255, 255, 255);
    287 	SDL_RenderClear(ctx->renderer);
    288 	render_selection(ctx, ed);
    289 	render_separators(ctx, ed);
    290 	render_text(ctx, ed);
    291 	render_cursor(ctx, ed);
    292 	SDL_RenderPresent(ctx->renderer);
    293 }