Skip to content

Commit b10412f

Browse files
andreakarashoclaude
andcommitted
Emit caret/selection as standard render commands and boost test coverage to 77%
- Add deltaTime parameter to BeginLayout for caret blink timing - TextInputWidget now emits Rectangle/Text/ScissorStart/End commands instead of a single Custom command, eliminating backend special-casing - Remove RenderTextInput from both Raylib renderers - Add 141 new tests covering: NineSlice, Color HSV, ShadowConfig, FloatingConfig, SharedElementConfig, TextInputFilters, TextInputStyle, SkinImage, StateImages, ClaySlice, RenderCommandExtensions, LayoutStyle, style MergeOver methods, DockLayout callback API, and DockBuilder edge cases Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 939bd3b commit b10412f

11 files changed

Lines changed: 1430 additions & 263 deletions

File tree

src/Clay.Example/RaylibRenderer.cs

Lines changed: 1 addition & 140 deletions
Original file line numberDiff line numberDiff line change
@@ -305,9 +305,7 @@ 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 TextInputWidget widget)
309-
RenderTextInput(box, widget);
310-
else if (data.CustomData is HsvGradientData gradient)
308+
if (data.CustomData is HsvGradientData gradient)
311309
RenderHsvGradient(box, gradient);
312310
}
313311

@@ -378,143 +376,6 @@ private unsafe void RenderHsvGradient(BoundingBox box, HsvGradientData gradient)
378376
}
379377
}
380378

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-
518379
private void RenderShadow(BoundingBox box, ShadowRenderData data)
519380
{
520381
// The bounding box arrives pre-expanded (offset + blur + spread already applied).

src/Clay.GameEditor/RaylibRenderer.cs

Lines changed: 1 addition & 110 deletions
Original file line numberDiff line numberDiff line change
@@ -251,9 +251,7 @@ 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 TextInputWidget widget)
255-
RenderTextInput(box, widget);
256-
else if (data.CustomData is HsvGradientData gradient)
254+
if (data.CustomData is HsvGradientData gradient)
257255
RenderHsvGradient(box, gradient);
258256
else if (data.CustomData is ViewportTextureData viewport)
259257
RenderViewportTexture(box, viewport);
@@ -317,113 +315,6 @@ private unsafe void RenderHsvGradient(BoundingBox box, HsvGradientData gradient)
317315
}
318316
}
319317

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-
427318
private void RenderShadow(BoundingBox box, ShadowRenderData data)
428319
{
429320
float blur = data.BlurRadius;

0 commit comments

Comments
 (0)