Skip to content

Commit 94ef24f

Browse files
andreakarashoclaude
andcommitted
Revert text input to Custom render command approach
The standard render command emission approach had a fatal ordering bug: commands injected during the building phase were overwritten by GenerateRenderCommands in EndLayout. Reverts to renderer-side caret/selection drawing via Custom commands. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 71588ee commit 94ef24f

3 files changed

Lines changed: 259 additions & 179 deletions

File tree

src/Clay.Example/RaylibRenderer.cs

Lines changed: 140 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -305,7 +305,9 @@ void Patch(float sx, float sy, float sw, float sh, float dx, float dy, float dw,
305305

306306
private void RenderCustom(BoundingBox box, CustomRenderData data)
307307
{
308-
if (data.CustomData is HsvGradientData gradient)
308+
if (data.CustomData is TextInputWidget widget)
309+
RenderTextInput(box, widget);
310+
else if (data.CustomData is HsvGradientData gradient)
309311
RenderHsvGradient(box, gradient);
310312
}
311313

@@ -376,6 +378,143 @@ private unsafe void RenderHsvGradient(BoundingBox box, HsvGradientData gradient)
376378
}
377379
}
378380

381+
private void RenderTextInput(BoundingBox box, TextInputWidget widget)
382+
{
383+
var style = widget.CurrentStyle;
384+
var padding = style.Padding;
385+
386+
// Background
387+
var bgRect = new Rectangle(box.X, box.Y, box.Width, box.Height);
388+
var bgColor = widget.IsFocused ? style.FocusedBackgroundColor : style.BackgroundColor;
389+
if (style.CornerRadius.TopLeft > 0)
390+
{
391+
float minDim = Math.Min(box.Width, box.Height);
392+
float roundness = Math.Clamp((style.CornerRadius.TopLeft * 2) / minDim, 0, 1);
393+
Raylib.DrawRectangleRounded(bgRect, roundness, 8, ToRayColor(bgColor));
394+
}
395+
else
396+
{
397+
Raylib.DrawRectangleRec(bgRect, ToRayColor(bgColor));
398+
}
399+
400+
// Border
401+
if (style.Border.Width.Top > 0 || style.Border.Width.Left > 0)
402+
{
403+
float minDim = Math.Min(box.Width, box.Height);
404+
float roundness = style.CornerRadius.TopLeft > 0
405+
? Math.Clamp((style.CornerRadius.TopLeft * 2) / minDim, 0, 1) : 0;
406+
float lineThick = Math.Max(
407+
Math.Max(style.Border.Width.Top, style.Border.Width.Bottom),
408+
Math.Max(style.Border.Width.Left, style.Border.Width.Right));
409+
Raylib.DrawRectangleRoundedLines(bgRect, roundness, 8, lineThick, ToRayColor(style.Border.Color));
410+
}
411+
412+
// Clip content to element bounds
413+
PushScissor(box);
414+
415+
// Content area (offset by scroll)
416+
float textX = box.X + padding.Left;
417+
float textY = box.Y + padding.Top - widget.ScrollY;
418+
float lineHeight = widget.ComputedLineHeight;
419+
420+
// Calculate visible row range to skip off-screen lines
421+
int firstVisibleRow = Math.Max(0, (int)(widget.ScrollY / lineHeight) - 1);
422+
int lastVisibleRow = (int)((widget.ScrollY + box.Height) / lineHeight) + 1;
423+
424+
// Draw selection highlight
425+
if (widget.IsFocused && widget.HasSelection)
426+
{
427+
int selStart = Math.Min(widget.SelectionStart, widget.SelectionEnd);
428+
int selEnd = Math.Max(widget.SelectionStart, widget.SelectionEnd);
429+
var selColor = ToRayColor(style.SelectionColor);
430+
431+
// For each visible line that intersects the selection
432+
int pos = 0;
433+
int row = 0;
434+
string text = widget.Text;
435+
while (pos <= text.Length && pos < selEnd)
436+
{
437+
int lineStart = pos;
438+
int lineEnd = text.IndexOf('\n', pos);
439+
if (lineEnd < 0) lineEnd = text.Length;
440+
441+
// Only process visible rows
442+
if (row > lastVisibleRow) break;
443+
if (row >= firstVisibleRow && lineEnd > selStart && lineStart < selEnd)
444+
{
445+
int hlStart = Math.Max(lineStart, selStart);
446+
int hlEnd = Math.Min(lineEnd, selEnd);
447+
float x1 = textX + widget.MeasureSubstring(lineStart, hlStart);
448+
float x2 = textX + widget.MeasureSubstring(lineStart, hlEnd);
449+
float w = x2 - x1;
450+
451+
// Selection spans past the end of this line (into the \n):
452+
// show a minimal marker so the user sees the line is selected
453+
if (selEnd > lineEnd && w < 1f)
454+
w = 1f;
455+
456+
float y = textY + row * lineHeight;
457+
Raylib.DrawRectangleRec(
458+
new Rectangle(x1, y, w, lineHeight),
459+
selColor);
460+
}
461+
462+
pos = lineEnd + 1;
463+
row++;
464+
}
465+
}
466+
467+
// Draw only visible text lines (skip off-screen rows)
468+
if (widget.Text.Length > 0 && style.FontId < _fonts.Length)
469+
{
470+
var font = _fonts[style.FontId];
471+
var textColor = ToRayColor(style.TextColor);
472+
string text = widget.Text;
473+
int lineStart = 0;
474+
int row = 0;
475+
476+
// Skip to first visible row
477+
while (row < firstVisibleRow && lineStart <= text.Length)
478+
{
479+
int lineEnd = text.IndexOf('\n', lineStart);
480+
if (lineEnd < 0) { lineStart = text.Length + 1; break; }
481+
lineStart = lineEnd + 1;
482+
row++;
483+
}
484+
485+
// Render visible rows only
486+
while (lineStart <= text.Length && row <= lastVisibleRow)
487+
{
488+
int lineEnd = text.IndexOf('\n', lineStart);
489+
if (lineEnd < 0) lineEnd = text.Length;
490+
if (lineEnd > lineStart)
491+
{
492+
string line = text.Substring(lineStart, lineEnd - lineStart);
493+
Raylib.DrawTextEx(font, line,
494+
new System.Numerics.Vector2(textX, textY + row * lineHeight),
495+
style.FontSize, style.LetterSpacing, textColor);
496+
}
497+
lineStart = lineEnd + 1;
498+
row++;
499+
}
500+
}
501+
502+
// Draw cursor (blink every 0.5s)
503+
if (widget.IsFocused && (int)(Raylib.GetTime() * 2) % 2 == 0)
504+
{
505+
var (cursorRow, cursorCol) = widget.GetRowCol(widget.CursorIndex);
506+
int lineStart = widget.FindLineStart(widget.CursorIndex);
507+
float cursorX = textX + widget.MeasureSubstring(lineStart, widget.CursorIndex);
508+
float cursorY = textY + cursorRow * lineHeight;
509+
510+
Raylib.DrawRectangleRec(
511+
new Rectangle(cursorX, cursorY, 1.5f, lineHeight),
512+
ToRayColor(style.CursorColor));
513+
}
514+
515+
PopScissor();
516+
}
517+
379518
private void RenderShadow(BoundingBox box, ShadowRenderData data)
380519
{
381520
// The bounding box arrives pre-expanded (offset + blur + spread already applied).

src/Clay.GameEditor/RaylibRenderer.cs

Lines changed: 110 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -251,7 +251,9 @@ void Patch(float sx, float sy, float sw, float sh, float dx, float dy, float dw,
251251

252252
private void RenderCustom(BoundingBox box, CustomRenderData data)
253253
{
254-
if (data.CustomData is HsvGradientData gradient)
254+
if (data.CustomData is TextInputWidget widget)
255+
RenderTextInput(box, widget);
256+
else if (data.CustomData is HsvGradientData gradient)
255257
RenderHsvGradient(box, gradient);
256258
else if (data.CustomData is ViewportTextureData viewport)
257259
RenderViewportTexture(box, viewport);
@@ -315,6 +317,113 @@ private unsafe void RenderHsvGradient(BoundingBox box, HsvGradientData gradient)
315317
}
316318
}
317319

320+
private void RenderTextInput(BoundingBox box, TextInputWidget widget)
321+
{
322+
var style = widget.CurrentStyle;
323+
var padding = style.Padding;
324+
325+
var bgRect = new Rectangle(box.X, box.Y, box.Width, box.Height);
326+
var bgColor = widget.IsFocused ? style.FocusedBackgroundColor : style.BackgroundColor;
327+
if (style.CornerRadius.TopLeft > 0)
328+
{
329+
float minDim = Math.Min(box.Width, box.Height);
330+
float roundness = Math.Clamp((style.CornerRadius.TopLeft * 2) / minDim, 0, 1);
331+
Raylib.DrawRectangleRounded(bgRect, roundness, 8, ToRayColor(bgColor));
332+
}
333+
else
334+
{
335+
Raylib.DrawRectangleRec(bgRect, ToRayColor(bgColor));
336+
}
337+
338+
if (style.Border.Width.Top > 0 || style.Border.Width.Left > 0)
339+
{
340+
float minDim = Math.Min(box.Width, box.Height);
341+
float roundness = style.CornerRadius.TopLeft > 0
342+
? Math.Clamp((style.CornerRadius.TopLeft * 2) / minDim, 0, 1) : 0;
343+
float lineThick = Math.Max(
344+
Math.Max(style.Border.Width.Top, style.Border.Width.Bottom),
345+
Math.Max(style.Border.Width.Left, style.Border.Width.Right));
346+
Raylib.DrawRectangleRoundedLines(bgRect, roundness, 8, lineThick, ToRayColor(style.Border.Color));
347+
}
348+
349+
PushScissor(box);
350+
351+
float textX = box.X + padding.Left;
352+
float textY = box.Y + padding.Top - widget.ScrollY;
353+
float lineHeight = widget.ComputedLineHeight;
354+
355+
int firstVisibleRow = Math.Max(0, (int)(widget.ScrollY / lineHeight) - 1);
356+
int lastVisibleRow = (int)((widget.ScrollY + box.Height) / lineHeight) + 1;
357+
358+
if (widget.IsFocused && widget.HasSelection)
359+
{
360+
int selStart = Math.Min(widget.SelectionStart, widget.SelectionEnd);
361+
int selEnd = Math.Max(widget.SelectionStart, widget.SelectionEnd);
362+
var selColor = ToRayColor(style.SelectionColor);
363+
int pos = 0, row = 0;
364+
string text = widget.DisplayText;
365+
while (pos <= text.Length && pos < selEnd)
366+
{
367+
int lineStart = pos;
368+
int lineEnd = text.IndexOf('\n', pos);
369+
if (lineEnd < 0) lineEnd = text.Length;
370+
if (row > lastVisibleRow) break;
371+
if (row >= firstVisibleRow && lineEnd > selStart && lineStart < selEnd)
372+
{
373+
int hlStart = Math.Max(lineStart, selStart);
374+
int hlEnd = Math.Min(lineEnd, selEnd);
375+
float x1 = textX + widget.MeasureSubstring(lineStart, hlStart);
376+
float x2 = textX + widget.MeasureSubstring(lineStart, hlEnd);
377+
float w = x2 - x1;
378+
if (selEnd > lineEnd && w < 1f) w = 1f;
379+
Raylib.DrawRectangleRec(new Rectangle(x1, textY + row * lineHeight, w, lineHeight), selColor);
380+
}
381+
pos = lineEnd + 1;
382+
row++;
383+
}
384+
}
385+
386+
if (widget.Text.Length > 0 && style.FontId < _fonts.Length)
387+
{
388+
var font = _fonts[style.FontId];
389+
var textColor = ToRayColor(style.TextColor);
390+
string text = widget.DisplayText;
391+
int lineStart = 0, row = 0;
392+
while (row < firstVisibleRow && lineStart <= text.Length)
393+
{
394+
int lineEnd = text.IndexOf('\n', lineStart);
395+
if (lineEnd < 0) { lineStart = text.Length + 1; break; }
396+
lineStart = lineEnd + 1;
397+
row++;
398+
}
399+
while (lineStart <= text.Length && row <= lastVisibleRow)
400+
{
401+
int lineEnd = text.IndexOf('\n', lineStart);
402+
if (lineEnd < 0) lineEnd = text.Length;
403+
if (lineEnd > lineStart)
404+
{
405+
string line = text.Substring(lineStart, lineEnd - lineStart);
406+
Raylib.DrawTextEx(font, line,
407+
new System.Numerics.Vector2(textX, textY + row * lineHeight),
408+
style.FontSize, style.LetterSpacing, textColor);
409+
}
410+
lineStart = lineEnd + 1;
411+
row++;
412+
}
413+
}
414+
415+
if (widget.IsFocused && (int)(Raylib.GetTime() * 2) % 2 == 0)
416+
{
417+
var (cursorRow, cursorCol) = widget.GetRowCol(widget.CursorIndex);
418+
int lineStart = widget.FindLineStart(widget.CursorIndex);
419+
float cursorX = textX + widget.MeasureSubstring(lineStart, widget.CursorIndex);
420+
float cursorY = textY + cursorRow * lineHeight;
421+
Raylib.DrawRectangleRec(new Rectangle(cursorX, cursorY, 1.5f, lineHeight), ToRayColor(style.CursorColor));
422+
}
423+
424+
PopScissor();
425+
}
426+
318427
private void RenderShadow(BoundingBox box, ShadowRenderData data)
319428
{
320429
float blur = data.BlurRadius;

0 commit comments

Comments
 (0)