@@ -85,17 +85,19 @@ 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. */
91- const char
92- * caret_text_chars ; /* start of caret-bearing text node's bytes, or NULL */
93- 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 */
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. */
92+ const char * caret_text_chars ; /* start of caret-bearing text node's bytes, or NULL */
93+ int caret_text_length ; /* byte length of that text node */
94+ uint32_t caret_offset_bytes ; /* target byte offset within that text node */
95+ int caret_placed ; /* 1 once the render walk has recorded a cell */
96+ int caret_tail_valid ; /* 1 when caret_tail_x/y hold a fallback cell */
97+ int caret_tail_x , caret_tail_y ; /* trailing cell of the most recently walked caret slice */
98+ int caret_x , caret_y ; /* resolved cell (column, row); -1 sentinel until placed */
99+ int has_caret ; /* 1 when the current frame will emit a cursor */
100+ int had_caret_last_frame ; /* 1 when the previous frame emitted a cursor */
99101};
100102
101103/* Memory layout inside the arena provided by the host:
@@ -324,70 +326,28 @@ static void render_rect(struct Clayterm *ct, int x0, int y0, int x1, int y1,
324326 setcell (ct , x , y , ' ' , ATTR_DEFAULT , bg );
325327}
326328
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 ;
329+ /* Return the byte length of the first `cps` code points of `start`,
330+ * clamped to at most `max_bytes`. Used at decode time to convert the
331+ * caller's code-point caret offset into a stable byte offset. */
332+ static uint32_t utf8_bytes_for_cps (const char * start , uint32_t cps ,
333+ uint32_t max_bytes ) {
334+ uint32_t consumed = 0 ;
335+ const char * p = start ;
336+ int rem = (int )max_bytes ;
337+ for (uint32_t k = 0 ; k < cps && rem > 0 ; k ++ ) {
338+ uint32_t cp ;
339+ int n = utf8_decode (& cp , p );
340+ if (n <= 0 ) {
341+ n = 1 ;
378342 }
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 ;
343+ if (n > rem ) {
344+ n = rem ;
384345 }
385- accumulated += slice_cps ;
346+ consumed += (uint32_t )n ;
347+ p += n ;
348+ rem -= n ;
386349 }
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 ;
350+ return consumed ;
391351}
392352
393353static void render_text (struct Clayterm * ct , int x0 , int y0 ,
@@ -401,11 +361,47 @@ static void render_text(struct Clayterm *ct, int x0, int y0,
401361 uint32_t attrs = ((uint32_t )(uint8_t )t -> textColor .a ) << 24 ;
402362 fg |= attrs ;
403363
404- const char * p = t -> stringContents .chars ;
405- int rem = t -> stringContents .length ;
364+ const char * slice = t -> stringContents .chars ;
365+ int slice_len = t -> stringContents .length ;
366+
367+ /* Determine whether this slice belongs to the caret's text node and
368+ * whether the caret still needs to be placed. If so, resolve the cell
369+ * as a side effect of the walk instead of doing a separate pass. */
370+ const char * node_start = ct -> caret_text_chars ;
371+ const char * node_end =
372+ node_start ? node_start + ct -> caret_text_length : NULL ;
373+ int caret_relevant = (node_start != NULL && !ct -> caret_placed &&
374+ slice >= node_start && slice < node_end );
375+
376+ /* Pre-check: if the slice's first byte is already past the target,
377+ * whitespace at the wrap seam was dropped by the layout engine. Snap
378+ * the caret to this slice's origin. */
379+ if (caret_relevant ) {
380+ uint32_t slice_start_bytes = (uint32_t )(slice - node_start );
381+ if (slice_start_bytes > ct -> caret_offset_bytes ) {
382+ ct -> caret_x = x0 ;
383+ ct -> caret_y = y0 ;
384+ ct -> caret_placed = 1 ;
385+ caret_relevant = 0 ;
386+ }
387+ }
388+
389+ const char * p = slice ;
390+ int rem = slice_len ;
406391 int x = x0 ;
407392
408393 while (rem > 0 ) {
394+ /* Check at the top of each iteration: if the pointer we are about to
395+ * consume matches the target byte offset, the caret sits at the
396+ * current cell — right before this code point. */
397+ if (caret_relevant &&
398+ (uint32_t )(p - node_start ) == ct -> caret_offset_bytes ) {
399+ ct -> caret_x = x ;
400+ ct -> caret_y = y0 ;
401+ ct -> caret_placed = 1 ;
402+ caret_relevant = 0 ;
403+ }
404+
409405 uint32_t cp ;
410406 int n = utf8_decode (& cp , p );
411407 if (n <= 0 ) {
@@ -422,6 +418,16 @@ static void render_text(struct Clayterm *ct, int x0, int y0,
422418 p += n ;
423419 rem -= n ;
424420 }
421+
422+ /* Remember the trailing cell of this slice as the end-of-content
423+ * fallback. If no later slice claims the target and the caret still
424+ * hasn't been placed by the end of the render command loop, this cell
425+ * is where a caret at offset == content-length lands. */
426+ if (caret_relevant ) {
427+ ct -> caret_tail_x = x ;
428+ ct -> caret_tail_y = y0 ;
429+ ct -> caret_tail_valid = 1 ;
430+ }
425431}
426432
427433static void render_border (struct Clayterm * ct , int x0 , int y0 , int x1 , int y1 ,
@@ -646,7 +652,11 @@ void reduce(struct Clayterm *ct, uint32_t *buf, int len, int mode, int row,
646652 ct -> animating_count = 0 ;
647653 ct -> caret_text_chars = NULL ;
648654 ct -> caret_text_length = 0 ;
649- ct -> caret_offset = 0 ;
655+ ct -> caret_offset_bytes = 0 ;
656+ ct -> caret_placed = 0 ;
657+ ct -> caret_tail_valid = 0 ;
658+ ct -> caret_tail_x = -1 ;
659+ ct -> caret_tail_y = -1 ;
650660 ct -> caret_x = -1 ;
651661 ct -> caret_y = -1 ;
652662 ct -> has_caret = 0 ;
@@ -777,7 +787,7 @@ void reduce(struct Clayterm *ct, uint32_t *buf, int len, int mode, int row,
777787 i += str_words ;
778788
779789 /* A caret on empty content is a rendering commitment the layout
780- * engine cannot satisfy on its own — zero cells give locate_caret
790+ * engine cannot satisfy on its own — zero cells give render_text
781791 * nothing to attach the cursor to. Substitute a single space so
782792 * the caret lands at the text element's origin, per the spec's
783793 * "as if the content were a single space" outcome. */
@@ -788,11 +798,16 @@ void reduce(struct Clayterm *ct, uint32_t *buf, int len, int mode, int row,
788798
789799 /* Record the FIRST caret declaration per frame for the
790800 * single-hardware-cursor contract; later declarations are
791- * intentionally ignored (multi-cursor is unspecified). */
801+ * intentionally ignored (multi-cursor is unspecified).
802+ *
803+ * Convert the caller's code-point offset into a byte offset once
804+ * here so the render walk can identify the caret cell by simple
805+ * pointer arithmetic against the slice's chars pointer. */
792806 if (caret != 0xFFFFFFFF && ct -> caret_text_chars == NULL ) {
793807 ct -> caret_text_chars = str_chars ;
794808 ct -> caret_text_length = (int )str_len ;
795- ct -> caret_offset = caret ;
809+ ct -> caret_offset_bytes =
810+ utf8_bytes_for_cps (str_chars , caret , str_len );
796811 ct -> has_caret = 1 ;
797812 }
798813
@@ -822,9 +837,6 @@ void reduce(struct Clayterm *ct, uint32_t *buf, int len, int mode, int row,
822837
823838 Clay_RenderCommandArray cmds = Clay_EndLayout (deltaTime );
824839
825- /* resolve caret cell from this frame's text commands */
826- locate_caret (ct , & cmds );
827-
828840 /* reset output state */
829841 ct -> out .length = 0 ;
830842 ct -> lastfg = ct -> lastbg = 0xffffffff ;
@@ -917,6 +929,22 @@ void reduce(struct Clayterm *ct, uint32_t *buf, int len, int mode, int row,
917929 }
918930 }
919931
932+ /* End-of-content fallback: if a caret was declared and its target byte
933+ * offset is one past the last byte of the caret text node, the trailing
934+ * cell of the last walked caret slice is where it lands. */
935+ if (ct -> has_caret && !ct -> caret_placed ) {
936+ if (ct -> caret_tail_valid &&
937+ ct -> caret_offset_bytes == (uint32_t )ct -> caret_text_length ) {
938+ ct -> caret_x = ct -> caret_tail_x ;
939+ ct -> caret_y = ct -> caret_tail_y ;
940+ ct -> caret_placed = 1 ;
941+ } else {
942+ /* Offset out of range or otherwise unresolvable. Suppress the
943+ * cursor for this frame. */
944+ ct -> has_caret = 0 ;
945+ }
946+ }
947+
920948 if (mode == 1 ) {
921949 present_lines (ct );
922950 } else {
0 commit comments