Skip to content

Commit 808256b

Browse files
andreakarashoclaude
andcommitted
Add password text input support
TextInputStyle gains a Password flag and optional PasswordChar (defaults to '*'). When enabled, the widget measures and renders mask characters while keeping the real text in the underlying buffer. Cursor positioning and selection highlights use masked widths for correct alignment. ClayUI.TextInput gets a convenience password parameter. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 26eddd8 commit 808256b

4 files changed

Lines changed: 60 additions & 5 deletions

File tree

src/Clay.GameEditor/RaylibRenderer.cs

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,18 @@ private void RenderCustom(BoundingBox box, CustomRenderData data)
255255
RenderTextInput(box, widget);
256256
else if (data.CustomData is HsvGradientData gradient)
257257
RenderHsvGradient(box, gradient);
258+
else if (data.CustomData is ViewportTextureData viewport)
259+
RenderViewportTexture(box, viewport);
260+
}
261+
262+
public void RenderViewportTexture(BoundingBox box, ViewportTextureData viewport)
263+
{
264+
if (viewport.RenderTexture is not RenderTexture rt) return;
265+
var texture = rt.texture;
266+
// RenderTexture is flipped vertically, so we use negative height in source rect
267+
var source = new Rectangle(0, 0, texture.width, -texture.height);
268+
var dest = new Rectangle(box.X, box.Y, box.Width, box.Height);
269+
Raylib.DrawTexturePro(texture, source, dest, new System.Numerics.Vector2(0, 0), 0, Raylib.WHITE);
258270
}
259271

260272
private unsafe void RenderHsvGradient(BoundingBox box, HsvGradientData gradient)
@@ -349,7 +361,7 @@ private void RenderTextInput(BoundingBox box, TextInputWidget widget)
349361
int selEnd = Math.Max(widget.SelectionStart, widget.SelectionEnd);
350362
var selColor = ToRayColor(style.SelectionColor);
351363
int pos = 0, row = 0;
352-
string text = widget.Text;
364+
string text = widget.DisplayText;
353365
while (pos <= text.Length && pos < selEnd)
354366
{
355367
int lineStart = pos;
@@ -375,7 +387,7 @@ private void RenderTextInput(BoundingBox box, TextInputWidget widget)
375387
{
376388
var font = _fonts[style.FontId];
377389
var textColor = ToRayColor(style.TextColor);
378-
string text = widget.Text;
390+
string text = widget.DisplayText;
379391
int lineStart = 0, row = 0;
380392
while (row < firstVisibleRow && lineStart <= text.Length)
381393
{

src/Clay/ClayUI.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1380,9 +1380,10 @@ public static void ProgressBar(float value, float min = 0f, float max = 1f, Prog
13801380
/// <param name="text">Reference to the text string. Updated when the user edits it.</param>
13811381
/// <param name="style">Visual style. Use <see cref="TextInputStyle.Default"/> or customize.</param>
13821382
/// <param name="singleLine">Block newlines and make up/down act as left/right.</param>
1383-
public static bool TextInput(string label, ref string text, TextInputStyle? style = null, bool singleLine = true)
1383+
public static bool TextInput(string label, ref string text, TextInputStyle? style = null, bool singleLine = true, bool password = false)
13841384
{
13851385
var s = style ?? DefaultTextInputStyle;
1386+
if (password) s.Password = true;
13861387
var id = StableId(label);
13871388

13881389
if (IsDisabled)

src/Clay/Widgets/TextInputStyle.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,17 @@ public struct TextInputStyle
5454
/// </summary>
5555
public Func<char, bool>? CharFilter;
5656

57+
/// <summary>
58+
/// When true, displays mask characters instead of the actual text (for password fields).
59+
/// The underlying text value is unaffected.
60+
/// </summary>
61+
public bool Password;
62+
63+
/// <summary>
64+
/// The character used to mask text when <see cref="Password"/> is true. Defaults to '*'.
65+
/// </summary>
66+
public char PasswordChar;
67+
5768
/// <summary>
5869
/// When true, the text input ignores all pointer interaction (click-to-focus, drag selection).
5970
/// Set by ClayUI when inside a disabled region.

src/Clay/Widgets/TextInputWidget.cs

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,11 @@ public string Text
8585
/// <summary>The style applied during the last <see cref="Element"/> call.</summary>
8686
public TextInputStyle CurrentStyle { get; private set; }
8787

88+
/// <summary>Returns the text to display — masked if in password mode, otherwise the real text.</summary>
89+
public string DisplayText => CurrentStyle.Password
90+
? new string(CurrentStyle.PasswordChar == '\0' ? '*' : CurrentStyle.PasswordChar, _chars.Count)
91+
: Text;
92+
8893
/// <summary>Current vertical scroll offset in pixels (for multiline inputs).</summary>
8994
public float ScrollY => _scrollY;
9095

@@ -171,7 +176,15 @@ public float MeasureSubstring(int from, int to)
171176
{
172177
if (from >= to || from >= _chars.Count) return 0;
173178
to = Math.Min(to, _chars.Count);
174-
var span = CollectionsMarshal.AsSpan(_chars).Slice(from, to - from);
179+
int len = to - from;
180+
if (CurrentStyle.Password)
181+
{
182+
char mask = CurrentStyle.PasswordChar == '\0' ? '*' : CurrentStyle.PasswordChar;
183+
Span<char> maskBuf = len <= 256 ? stackalloc char[len] : new char[len];
184+
maskBuf.Fill(mask);
185+
return _measurer.MeasureText(maskBuf, _fontId, _fontSize, _letterSpacing).Width;
186+
}
187+
var span = CollectionsMarshal.AsSpan(_chars).Slice(from, len);
175188
return _measurer.MeasureText(span, _fontId, _fontSize, _letterSpacing).Width;
176189
}
177190

@@ -322,6 +335,12 @@ float ITextEditHandler.GetCharWidth(int lineStartIndex, int charIndex)
322335
if (idx >= _chars.Count) return 0;
323336
var span = CollectionsMarshal.AsSpan(_chars);
324337
if (span[idx] == '\n') return 0;
338+
if (CurrentStyle.Password)
339+
{
340+
char mask = CurrentStyle.PasswordChar == '\0' ? '*' : CurrentStyle.PasswordChar;
341+
ReadOnlySpan<char> maskSpan = stackalloc char[] { mask };
342+
return _measurer.MeasureText(maskSpan, _fontId, _fontSize, _letterSpacing).Width;
343+
}
325344
return _measurer.MeasureText(span.Slice(idx, 1), _fontId, _fontSize, _letterSpacing).Width;
326345
}
327346

@@ -339,7 +358,19 @@ void ITextEditHandler.LayoutRow(out TextEditRow row, int lineStartIndex)
339358

340359
float width = 0;
341360
if (textChars > 0)
342-
width = _measurer.MeasureText(span.Slice(lineStartIndex, textChars), _fontId, _fontSize, _letterSpacing).Width;
361+
{
362+
if (CurrentStyle.Password)
363+
{
364+
char mask = CurrentStyle.PasswordChar == '\0' ? '*' : CurrentStyle.PasswordChar;
365+
Span<char> maskBuf = textChars <= 256 ? stackalloc char[textChars] : new char[textChars];
366+
maskBuf.Fill(mask);
367+
width = _measurer.MeasureText(maskBuf, _fontId, _fontSize, _letterSpacing).Width;
368+
}
369+
else
370+
{
371+
width = _measurer.MeasureText(span.Slice(lineStartIndex, textChars), _fontId, _fontSize, _letterSpacing).Width;
372+
}
373+
}
343374

344375
float lh = _cachedLineHeight;
345376
row = new TextEditRow

0 commit comments

Comments
 (0)