esc
Externally Scriptable Editor
git clone git://mccd.space/esc
| Log | Files | Refs | README |
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 }