@@ -85,17 +85,22 @@ struct Clayterm {
8585 int error_count ;
8686 int animating_count ;
8787 /* Caret state for hardware-cursor management. The renderer records the
88- * first text node carrying a caret declaration per frame, then locates
89- * the corresponding cell after Clay layout. had_caret_last_frame is the
90- * only cross-frame bit retained. */
88+ * first text node carrying a caret declaration per frame, precomputes the
89+ * caret's byte offset into that node's content, then places the cell as
90+ * a side effect of render_text's walk. had_caret_last_frame is the only
91+ * cross-frame bit retained. */
9192 const char
9293 * caret_text_chars ; /* start of caret-bearing text node's bytes, or NULL */
9394 int caret_text_length ; /* byte length of that text node */
94- uint32_t caret_offset ; /* code-point offset within that text node */
95- int caret_x , caret_y ; /* resolved cell (column, row), valid only when
96- caret_text_chars != NULL */
97- int has_caret ; /* 1 when the current frame placed a caret */
98- int had_caret_last_frame ; /* 1 when the previous frame placed a caret */
95+ uint32_t caret_offset_bytes ; /* target byte offset within that text node */
96+ int caret_placed ; /* 1 once the render walk has recorded a cell */
97+ int caret_tail_valid ; /* 1 when caret_tail_x/y hold a fallback cell */
98+ int caret_tail_x ,
99+ caret_tail_y ; /* trailing cell of the most recently walked caret slice */
100+ int caret_x ,
101+ caret_y ; /* resolved cell (column, row); -1 sentinel until placed */
102+ int has_caret ; /* 1 when the current frame will emit a cursor */
103+ int had_caret_last_frame ; /* 1 when the previous frame emitted a cursor */
99104};
100105
101106/* Memory layout inside the arena provided by the host:
@@ -324,70 +329,28 @@ static void render_rect(struct Clayterm *ct, int x0, int y0, int x1, int y1,
324329 setcell (ct , x , y , ' ' , ATTR_DEFAULT , bg );
325330}
326331
327- /**
328- * Locate the cell where the caret should be rendered given the per-line
329- * text commands produced by Clay's wrap pass. Iterates render commands
330- * in order, accumulating code points consumed across slices that belong
331- * to the caret text node, until the caret's code-point offset is reached.
332- */
333- static void locate_caret (struct Clayterm * ct , Clay_RenderCommandArray * cmds ) {
334- if (ct -> caret_text_chars == NULL ) {
335- return ;
336- }
337- const char * node_start = ct -> caret_text_chars ;
338- const char * node_end = node_start + ct -> caret_text_length ;
339- uint32_t target = ct -> caret_offset ;
340- uint32_t accumulated = 0 ;
341-
342- for (int32_t j = 0 ; j < cmds -> length ; j ++ ) {
343- Clay_RenderCommand * cmd = Clay_RenderCommandArray_Get (cmds , j );
344- if (cmd -> commandType != CLAY_RENDER_COMMAND_TYPE_TEXT ) {
345- continue ;
346- }
347- Clay_TextRenderData * t = & cmd -> renderData .text ;
348- const char * slice = t -> stringContents .chars ;
349- int slice_len = t -> stringContents .length ;
350- if (slice < node_start || slice >= node_end ) {
351- continue ;
352- }
353- /* count code points in this slice */
354- uint32_t slice_cps = 0 ;
355- int x_cells = 0 ;
356- const char * p = slice ;
357- int rem = slice_len ;
358- while (rem > 0 ) {
359- uint32_t cp ;
360- int n = utf8_decode (& cp , p );
361- if (n <= 0 ) {
362- n = 1 ;
363- cp = 0xfffd ;
364- }
365- if (accumulated + slice_cps == target ) {
366- ct -> caret_x = (int )cmd -> boundingBox .x + x_cells ;
367- ct -> caret_y = (int )cmd -> boundingBox .y ;
368- return ;
369- }
370- int cw = wcwidth (cp );
371- if (cw < 0 ) {
372- cw = 1 ;
373- }
374- x_cells += cw ;
375- slice_cps ++ ;
376- p += n ;
377- rem -= n ;
332+ /* Return the byte length of the first `cps` code points of `start`,
333+ * clamped to at most `max_bytes`. Used at decode time to convert the
334+ * caller's code-point caret offset into a stable byte offset. */
335+ static uint32_t utf8_bytes_for_cps (const char * start , uint32_t cps ,
336+ uint32_t max_bytes ) {
337+ uint32_t consumed = 0 ;
338+ const char * p = start ;
339+ int rem = (int )max_bytes ;
340+ for (uint32_t k = 0 ; k < cps && rem > 0 ; k ++ ) {
341+ uint32_t cp ;
342+ int n = utf8_decode (& cp , p );
343+ if (n <= 0 ) {
344+ n = 1 ;
378345 }
379- if (accumulated + slice_cps == target ) {
380- /* caret sits just after this slice's last code point */
381- ct -> caret_x = (int )cmd -> boundingBox .x + x_cells ;
382- ct -> caret_y = (int )cmd -> boundingBox .y ;
383- return ;
346+ if (n > rem ) {
347+ n = rem ;
384348 }
385- accumulated += slice_cps ;
349+ consumed += (uint32_t )n ;
350+ p += n ;
351+ rem -= n ;
386352 }
387- /* offset out of range: behavior is unspecified by the spec; leave
388- * caret_x/caret_y at their sentinel -1 values and let the emission
389- * step suppress visibility. */
390- ct -> has_caret = 0 ;
353+ return consumed ;
391354}
392355
393356static void render_text (struct Clayterm * ct , int x0 , int y0 ,
@@ -401,11 +364,46 @@ static void render_text(struct Clayterm *ct, int x0, int y0,
401364 uint32_t attrs = ((uint32_t )(uint8_t )t -> textColor .a ) << 24 ;
402365 fg |= attrs ;
403366
404- const char * p = t -> stringContents .chars ;
405- int rem = t -> stringContents .length ;
367+ const char * slice = t -> stringContents .chars ;
368+ int slice_len = t -> stringContents .length ;
369+
370+ /* Determine whether this slice belongs to the caret's text node and
371+ * whether the caret still needs to be placed. If so, resolve the cell
372+ * as a side effect of the walk instead of doing a separate pass. */
373+ const char * node_start = ct -> caret_text_chars ;
374+ const char * node_end = node_start ? node_start + ct -> caret_text_length : NULL ;
375+ int caret_relevant = (node_start != NULL && !ct -> caret_placed &&
376+ slice >= node_start && slice < node_end );
377+
378+ /* Pre-check: if the slice's first byte is already past the target,
379+ * whitespace at the wrap seam was dropped by the layout engine. Snap
380+ * the caret to this slice's origin. */
381+ if (caret_relevant ) {
382+ uint32_t slice_start_bytes = (uint32_t )(slice - node_start );
383+ if (slice_start_bytes > ct -> caret_offset_bytes ) {
384+ ct -> caret_x = x0 ;
385+ ct -> caret_y = y0 ;
386+ ct -> caret_placed = 1 ;
387+ caret_relevant = 0 ;
388+ }
389+ }
390+
391+ const char * p = slice ;
392+ int rem = slice_len ;
406393 int x = x0 ;
407394
408395 while (rem > 0 ) {
396+ /* Check at the top of each iteration: if the pointer we are about to
397+ * consume matches the target byte offset, the caret sits at the
398+ * current cell — right before this code point. */
399+ if (caret_relevant &&
400+ (uint32_t )(p - node_start ) == ct -> caret_offset_bytes ) {
401+ ct -> caret_x = x ;
402+ ct -> caret_y = y0 ;
403+ ct -> caret_placed = 1 ;
404+ caret_relevant = 0 ;
405+ }
406+
409407 uint32_t cp ;
410408 int n = utf8_decode (& cp , p );
411409 if (n <= 0 ) {
@@ -422,6 +420,16 @@ static void render_text(struct Clayterm *ct, int x0, int y0,
422420 p += n ;
423421 rem -= n ;
424422 }
423+
424+ /* Remember the trailing cell of this slice as the end-of-content
425+ * fallback. If no later slice claims the target and the caret still
426+ * hasn't been placed by the end of the render command loop, this cell
427+ * is where a caret at offset == content-length lands. */
428+ if (caret_relevant ) {
429+ ct -> caret_tail_x = x ;
430+ ct -> caret_tail_y = y0 ;
431+ ct -> caret_tail_valid = 1 ;
432+ }
425433}
426434
427435static void render_border (struct Clayterm * ct , int x0 , int y0 , int x1 , int y1 ,
@@ -646,7 +654,11 @@ void reduce(struct Clayterm *ct, uint32_t *buf, int len, int mode, int row,
646654 ct -> animating_count = 0 ;
647655 ct -> caret_text_chars = NULL ;
648656 ct -> caret_text_length = 0 ;
649- ct -> caret_offset = 0 ;
657+ ct -> caret_offset_bytes = 0 ;
658+ ct -> caret_placed = 0 ;
659+ ct -> caret_tail_valid = 0 ;
660+ ct -> caret_tail_x = -1 ;
661+ ct -> caret_tail_y = -1 ;
650662 ct -> caret_x = -1 ;
651663 ct -> caret_y = -1 ;
652664 ct -> has_caret = 0 ;
@@ -777,7 +789,7 @@ void reduce(struct Clayterm *ct, uint32_t *buf, int len, int mode, int row,
777789 i += str_words ;
778790
779791 /* A caret on empty content is a rendering commitment the layout
780- * engine cannot satisfy on its own — zero cells give locate_caret
792+ * engine cannot satisfy on its own — zero cells give render_text
781793 * nothing to attach the cursor to. Substitute a single space so
782794 * the caret lands at the text element's origin, per the spec's
783795 * "as if the content were a single space" outcome. */
@@ -788,11 +800,15 @@ void reduce(struct Clayterm *ct, uint32_t *buf, int len, int mode, int row,
788800
789801 /* Record the FIRST caret declaration per frame for the
790802 * single-hardware-cursor contract; later declarations are
791- * intentionally ignored (multi-cursor is unspecified). */
803+ * intentionally ignored (multi-cursor is unspecified).
804+ *
805+ * Convert the caller's code-point offset into a byte offset once
806+ * here so the render walk can identify the caret cell by simple
807+ * pointer arithmetic against the slice's chars pointer. */
792808 if (caret != 0xFFFFFFFF && ct -> caret_text_chars == NULL ) {
793809 ct -> caret_text_chars = str_chars ;
794810 ct -> caret_text_length = (int )str_len ;
795- ct -> caret_offset = caret ;
811+ ct -> caret_offset_bytes = utf8_bytes_for_cps ( str_chars , caret , str_len ) ;
796812 ct -> has_caret = 1 ;
797813 }
798814
@@ -822,9 +838,6 @@ void reduce(struct Clayterm *ct, uint32_t *buf, int len, int mode, int row,
822838
823839 Clay_RenderCommandArray cmds = Clay_EndLayout (deltaTime );
824840
825- /* resolve caret cell from this frame's text commands */
826- locate_caret (ct , & cmds );
827-
828841 /* reset output state */
829842 ct -> out .length = 0 ;
830843 ct -> lastfg = ct -> lastbg = 0xffffffff ;
@@ -917,6 +930,22 @@ void reduce(struct Clayterm *ct, uint32_t *buf, int len, int mode, int row,
917930 }
918931 }
919932
933+ /* End-of-content fallback: if a caret was declared and its target byte
934+ * offset is one past the last byte of the caret text node, the trailing
935+ * cell of the last walked caret slice is where it lands. */
936+ if (ct -> has_caret && !ct -> caret_placed ) {
937+ if (ct -> caret_tail_valid &&
938+ ct -> caret_offset_bytes == (uint32_t )ct -> caret_text_length ) {
939+ ct -> caret_x = ct -> caret_tail_x ;
940+ ct -> caret_y = ct -> caret_tail_y ;
941+ ct -> caret_placed = 1 ;
942+ } else {
943+ /* Offset out of range or otherwise unresolvable. Suppress the
944+ * cursor for this frame. */
945+ ct -> has_caret = 0 ;
946+ }
947+ }
948+
920949 if (mode == 1 ) {
921950 present_lines (ct );
922951 } else {
0 commit comments