Vendored ImGui input text implementation. Implements text input widgets, multiline support, clipboard integration, cursor/selection, undo/redo, keyboard and mouse handling, rendering text and selection, and callback/character filtering logic.
// Vendored from zeljkovranjes/sbox-imgui; see LICENSE and PROVENANCE.md in this folder.
using Sandbox;
#pragma warning disable CS0649 // Upstream reserves fields for optional widgets and features.
namespace SboxSuperuser.Engine.Vendor.ImGui;
internal static partial class ImGui
{
internal static bool IsTextInputActive()
{
var g = G;
return g is not null && g.ActiveId != 0 && g.InputTextState.ID == g.ActiveId;
}
internal static bool TempInputIsActive( int id )
{
var g = G;
return g.ActiveId == id && g.TempInputId == id;
}
private static bool _clipboardProviderInstalled;
private static void EnsureClipboardProvider()
{
if ( _clipboardProviderInstalled )
return;
_clipboardProviderInstalled = true;
ImGuiHost.ClipboardCopyProvider = InputTextClipboardCopy;
}
/// <summary>Called by the input panel when the OS asks for clipboard contents (Ctrl+C / Ctrl+X).</summary>
private static string InputTextClipboardCopy( bool cut )
{
var g = G;
if ( g is null || !IsTextInputActive() )
return null;
var state = g.InputTextState;
if ( !state.HasSelection )
return null;
bool isPassword = (state.Flags & ImGuiInputTextFlags.Password) != 0;
if ( isPassword )
return null;
string selected = state.Text.Substring( state.SelMin, state.SelMax - state.SelMin );
if ( cut && (state.Flags & ImGuiInputTextFlags.ReadOnly) == 0 )
{
state.PushUndo();
int min = state.SelMin;
state.Text = state.Text.Remove( min, state.SelMax - min );
state.Cursor = state.SelectStart = min;
state.Edited = true;
}
g.InputTextClipboardHandledByPanel = true;
return selected;
}
#region Public API
public static bool InputText( string label, ref string text, ImGuiInputTextFlags flags = ImGuiInputTextFlags.None, ImGuiInputTextCallback callback = null )
=> InputTextEx( label, null, ref text, 0, Vector2.Zero, flags & ~ImGuiInputTextFlags.Multiline, callback );
/// <summary>Text input limited to <paramref name="maxLength"/> characters (equivalent of Dear ImGui's buffer size).</summary>
public static bool InputText( string label, ref string text, int maxLength, ImGuiInputTextFlags flags = ImGuiInputTextFlags.None, ImGuiInputTextCallback callback = null )
=> InputTextEx( label, null, ref text, maxLength, Vector2.Zero, flags & ~ImGuiInputTextFlags.Multiline, callback );
public static bool InputTextMultiline( string label, ref string text, Vector2 size = default, ImGuiInputTextFlags flags = ImGuiInputTextFlags.None, ImGuiInputTextCallback callback = null )
=> InputTextEx( label, null, ref text, 0, size, flags | ImGuiInputTextFlags.Multiline, callback );
public static bool InputTextWithHint( string label, string hint, ref string text, ImGuiInputTextFlags flags = ImGuiInputTextFlags.None, ImGuiInputTextCallback callback = null )
=> InputTextEx( label, hint, ref text, 0, Vector2.Zero, flags & ~ImGuiInputTextFlags.Multiline, callback );
#endregion
/// <summary>
/// Turn an existing item (e.g. a drag or slider) into a text field for keyboard input. Used for Ctrl+click / double-click editing.
/// </summary>
internal static bool TempInputText( ImRect bb, int id, string label, ref string text, ImGuiInputTextFlags flags )
{
var g = G;
bool init = g.TempInputId != id;
if ( init )
ClearActiveID();
g.CurrentWindow.DC.CursorPos = bb.Min;
bool valueChanged = InputTextEx( label, null, ref text, 0, bb.Size, flags | ImGuiInputTextFlags.MergedItem, null, init, id, bb );
if ( init )
g.TempInputId = g.ActiveId;
return valueChanged;
}
/// <summary>Text-input version of a numeric widget. Returns true when the value changed.</summary>
internal static bool TempInputScalar( ImRect bb, int id, string label, ref double value, string format, bool isInteger, bool hasClamp, double clampMin, double clampMax )
{
var g = G;
var fmt = ParseFormatTrimDecorations( format );
if ( string.IsNullOrEmpty( fmt ) )
fmt = isInteger ? "%d" : "%.3f";
string buf = FormatNumber( fmt, value, isInteger ).Trim();
var flags = ImGuiInputTextFlags.AutoSelectAll | InputScalarDefaultCharsFilter( isInteger, format );
bool valueChanged = false;
if ( TempInputText( bb, id, label, ref buf, flags ) )
{
double old = value;
double v = ApplyExpression( buf, old, format );
if ( isInteger )
v = Math.Round( v, MidpointRounding.AwayFromZero );
if ( hasClamp )
{
if ( clampMin < clampMax ) v = Math.Clamp( v, clampMin, clampMax );
else if ( clampMin == clampMax ) v = clampMin;
}
value = v;
valueChanged = v != old;
if ( valueChanged )
MarkItemEdited( id );
}
return valueChanged;
}
internal static ImGuiInputTextFlags InputScalarDefaultCharsFilter( bool isInteger, string format )
{
if ( !isInteger )
return ImGuiInputTextFlags.CharsScientific;
if ( FormatIsHex( format ) )
return ImGuiInputTextFlags.CharsHexadecimal;
return ImGuiInputTextFlags.CharsDecimal;
}
#region Character filtering / word boundaries
private static bool InputTextFilterCharacter( ref char c, ImGuiInputTextFlags flags, ImGuiInputTextCallback callback, bool fromClipboard, ImGuiInputTextState state )
{
bool applyNamedFilters = true;
if ( c < 0x20 )
{
bool pass = (c == '\n' && (flags & ImGuiInputTextFlags.Multiline) != 0) || (c == '\t' && (flags & ImGuiInputTextFlags.AllowTabInput) != 0);
if ( !pass )
return false;
applyNamedFilters = false;
}
if ( !fromClipboard )
{
if ( c == 127 )
return false;
if ( c >= 0xE000 && c <= 0xF8FF )
return false;
}
if ( applyNamedFilters && (flags & (ImGuiInputTextFlags.CharsDecimal | ImGuiInputTextFlags.CharsHexadecimal | ImGuiInputTextFlags.CharsUppercase | ImGuiInputTextFlags.CharsNoBlank | ImGuiInputTextFlags.CharsScientific)) != 0 )
{
if ( (flags & ImGuiInputTextFlags.CharsDecimal) != 0 )
if ( !(c >= '0' && c <= '9') && c != '.' && c != '-' && c != '+' && c != '*' && c != '/' )
return false;
if ( (flags & ImGuiInputTextFlags.CharsScientific) != 0 )
if ( !(c >= '0' && c <= '9') && c != '.' && c != '-' && c != '+' && c != '*' && c != '/' && c != 'e' && c != 'E' )
return false;
if ( (flags & ImGuiInputTextFlags.CharsHexadecimal) != 0 )
if ( !(c >= '0' && c <= '9') && !(c >= 'a' && c <= 'f') && !(c >= 'A' && c <= 'F') )
return false;
if ( (flags & ImGuiInputTextFlags.CharsUppercase) != 0 && c >= 'a' && c <= 'z' )
c = (char)(c + ('A' - 'a'));
if ( (flags & ImGuiInputTextFlags.CharsNoBlank) != 0 && (c == ' ' || c == '\t' || c == ' ') )
return false;
}
if ( (flags & ImGuiInputTextFlags.CallbackCharFilter) != 0 && callback is not null )
{
var data = new ImGuiInputTextCallbackData
{
EventFlag = ImGuiInputTextFlags.CallbackCharFilter,
EventChar = c,
Flags = flags,
};
data.SetBufSilently( state?.Text );
if ( callback( data ) != 0 )
return false;
if ( data.EventChar == 0 )
return false;
c = data.EventChar;
}
return true;
}
private static bool IsWordSeparator( char c )
=> char.IsWhiteSpace( c ) || ",;(){}[]|.!?\"'`/\\:<>=+-*&^%$#@~".IndexOf( c ) >= 0;
private static int WordLeft( string text, int pos )
{
pos = Math.Clamp( pos, 0, text.Length );
while ( pos > 0 && IsWordSeparator( text[pos - 1] ) ) pos--;
while ( pos > 0 && !IsWordSeparator( text[pos - 1] ) ) pos--;
return pos;
}
private static int WordRight( string text, int pos )
{
pos = Math.Clamp( pos, 0, text.Length );
while ( pos < text.Length && !IsWordSeparator( text[pos] ) ) pos++;
while ( pos < text.Length && IsWordSeparator( text[pos] ) && text[pos] != '\n' ) pos++;
return pos;
}
private static int LineStart( string text, int pos )
{
pos = Math.Clamp( pos, 0, text.Length );
while ( pos > 0 && text[pos - 1] != '\n' ) pos--;
return pos;
}
private static int LineEnd( string text, int pos )
{
pos = Math.Clamp( pos, 0, text.Length );
while ( pos < text.Length && text[pos] != '\n' ) pos++;
return pos;
}
#endregion
#region Text layout helpers
private static float TextWidthPrefix( string line, int count )
{
if ( count <= 0 || string.IsNullOrEmpty( line ) )
return 0f;
count = Math.Min( count, line.Length );
return MeasureTextWidth( line.Substring( 0, count ), G.FontPointSize );
}
/// <summary>Column within a line closest to a pixel x offset (binary search over prefix widths).</summary>
private static int ColumnFromX( string line, float x )
{
if ( string.IsNullOrEmpty( line ) || x <= 0f )
return 0;
int lo = 0, hi = line.Length;
while ( lo < hi )
{
int mid = (lo + hi + 1) / 2;
if ( TextWidthPrefix( line, mid ) <= x ) lo = mid;
else hi = mid - 1;
}
if ( lo < line.Length )
{
float a = TextWidthPrefix( line, lo );
float b = TextWidthPrefix( line, lo + 1 );
if ( x - a > b - x )
lo++;
}
return lo;
}
/// <summary>Convert a character index to (line, column).</summary>
private static void IndexToLineCol( string text, int index, out int line, out int col, out int lineStartIdx )
{
line = 0;
lineStartIdx = 0;
index = Math.Clamp( index, 0, text.Length );
for ( int i = 0; i < index; i++ )
{
if ( text[i] == '\n' )
{
line++;
lineStartIdx = i + 1;
}
}
col = index - lineStartIdx;
}
private static int LocateIndex( string displayText, Vector2 rel, bool multiline )
{
var g = G;
if ( !multiline )
return ColumnFromX( displayText, rel.x );
var lines = displayText.Split( '\n' );
int lineIdx = Math.Clamp( (int)MathF.Floor( rel.y / g.FontSize ), 0, lines.Length - 1 );
int idx = 0;
for ( int i = 0; i < lineIdx; i++ )
idx += lines[i].Length + 1;
return idx + ColumnFromX( lines[lineIdx], rel.x );
}
#endregion
/// <summary>
/// Core text input implementation (port of Dear ImGui's InputTextEx).
/// </summary>
internal static bool InputTextEx( string label, string hint, ref string text, int maxLength, Vector2 sizeArg, ImGuiInputTextFlags flags, ImGuiInputTextCallback callback,
bool forceActivate = false, int mergedId = 0, ImRect? mergedBb = null )
{
var g = G;
var window = GetCurrentWindow();
if ( window.SkipItems )
return false;
EnsureClipboardProvider();
text ??= string.Empty;
var io = g.IO;
var style = g.Style;
bool isMultiline = (flags & ImGuiInputTextFlags.Multiline) != 0;
bool isReadOnly = (flags & ImGuiInputTextFlags.ReadOnly) != 0 || (g.CurrentItemFlags & ImGuiItemFlags.ReadOnly) != 0;
bool isPassword = (flags & ImGuiInputTextFlags.Password) != 0 && !isMultiline;
bool isMerged = (flags & ImGuiInputTextFlags.MergedItem) != 0;
if ( isMultiline )
BeginGroup();
int id = isMerged && mergedId != 0 ? mergedId : window.GetID( label );
var labelSize = CalcTextSize( label, true );
var frameSize = CalcItemSize( sizeArg, CalcItemWidth(), (isMultiline ? g.FontSize * 8.0f : labelSize.y) + style.FramePadding.y * 2.0f );
var totalSize = new Vector2( frameSize.x + (labelSize.x > 0.0f ? style.ItemInnerSpacing.x + labelSize.x : 0.0f), frameSize.y );
var frameBb = isMerged && mergedBb.HasValue ? mergedBb.Value : new ImRect( window.DC.CursorPos, window.DC.CursorPos + frameSize );
var totalBb = new ImRect( frameBb.Min, frameBb.Min + totalSize );
var drawWindow = window;
var innerSize = frameSize;
var itemFlagsBackup = ImGuiItemFlags.None;
var itemStatusBackup = ImGuiItemStatusFlags.None;
if ( isMultiline )
{
var backupPos = window.DC.CursorPos;
ItemSize( totalBb, style.FramePadding.y );
if ( !ItemAdd( totalBb, id, frameBb, ImGuiItemFlags.Inputable ) )
{
EndGroup();
return false;
}
itemFlagsBackup = g.LastItemData.InFlags;
itemStatusBackup = g.LastItemData.StatusFlags;
window.DC.CursorPos = backupPos;
PushStyleColor( ImGuiCol.ChildBg, style.Colors[(int)ImGuiCol.FrameBg] );
PushStyleVar( ImGuiStyleVar.ChildRounding, style.FrameRounding );
PushStyleVar( ImGuiStyleVar.ChildBorderSize, style.FrameBorderSize );
PushStyleVar( ImGuiStyleVar.WindowPadding, Vector2.Zero );
bool childVisible = BeginChildEx( label, id, frameBb.Size, ImGuiChildFlags.Borders, ImGuiWindowFlags.NoMove );
PopStyleVar( 3 );
PopStyleColor();
if ( !childVisible )
{
EndChild();
EndGroup();
return false;
}
drawWindow = g.CurrentWindow;
drawWindow.DC.CursorPos += style.FramePadding;
innerSize.x -= drawWindow.ScrollbarSizes.x;
}
else if ( !isMerged )
{
ItemSize( totalBb, style.FramePadding.y );
if ( !ItemAdd( totalBb, id, frameBb, ImGuiItemFlags.Inputable ) )
return false;
itemFlagsBackup = g.LastItemData.InFlags;
}
else
{
itemFlagsBackup = g.LastItemData.InFlags;
}
bool hovered = ItemHoverable( frameBb, id, itemFlagsBackup );
if ( hovered )
g.MouseCursor = ImGuiMouseCursor.TextInput;
var state = g.InputTextState;
bool stateIsOurs = state.ID == id;
bool focusRequestedByCode = g.FocusedTextInputRequestId == id;
if ( focusRequestedByCode )
g.FocusedTextInputRequestId = 0;
bool userClicked = hovered && io.MouseClicked[0];
int scrollbarId = isMultiline ? drawWindow.GetID( "#SCROLLY" ) : 0;
bool userScrollActive = isMultiline && g.ActiveId == scrollbarId && scrollbarId != 0;
bool userScrollFinish = isMultiline && g.ActiveIdPreviousFrame == scrollbarId && scrollbarId != 0 && g.ActiveId != scrollbarId && stateIsOurs;
bool initMakeActive = userClicked || userScrollFinish || focusRequestedByCode || forceActivate;
bool initState = initMakeActive || userScrollActive;
bool selectAll = false;
if ( initState && g.ActiveId != id )
{
bool recycle = stateIsOurs && state.Text == text;
if ( !recycle )
{
state.Text = text;
state.Cursor = state.SelectStart = 0;
state.ScrollX = 0f;
state.UndoStack.Clear();
state.RedoStack.Clear();
}
state.ID = id;
state.InitialText = text;
state.Flags = flags;
state.MaxLength = maxLength;
state.Edited = false;
state.CursorAnim = 0f;
stateIsOurs = true;
if ( (flags & ImGuiInputTextFlags.AutoSelectAll) != 0 || focusRequestedByCode )
selectAll = true;
if ( isMerged && forceActivate )
selectAll = true;
if ( !recycle && !selectAll )
state.Cursor = state.SelectStart = text.Length;
}
if ( g.ActiveId != id && initMakeActive )
{
SetActiveID( id, window );
FocusWindow( window );
}
bool clearActiveId = false;
if ( g.ActiveId == id && io.MouseClicked[0] && !initState && !initMakeActive && !hovered )
clearActiveId = true;
bool valueChanged = false;
bool validated = false;
bool renderCursor = g.ActiveId == id || userScrollActive;
bool renderSelection = renderCursor;
if ( g.ActiveId == id && stateIsOurs )
{
state.Flags = flags;
state.MaxLength = maxLength;
state.ClampPositions();
g.WantTextInputNextFrame = 1;
// Allow clicking other widgets while we are focused (but not while selecting with the mouse).
g.ActiveIdAllowOverlap = !io.MouseDown[0];
KeepAliveID( id );
string displayText = isPassword ? new string( '*', state.Text.Length ) : state.Text;
var textOrigin = isMultiline
? drawWindow.DC.CursorPos
: frameBb.Min + style.FramePadding - new Vector2( state.ScrollX, 0f );
if ( !isMultiline && labelSize.y > 0 )
textOrigin.y = frameBb.Min.y + style.FramePadding.y;
var mouseRel = io.MousePos - textOrigin;
// Mouse handling
if ( selectAll )
{
state.SelectAll();
state.SelectedAllMouseLock = true;
state.CursorFollow = true;
}
else if ( hovered && io.MouseClicked[0] && io.MouseClickedCount[0] >= 2 && !io.KeyShift )
{
int idx = LocateIndex( displayText, mouseRel, isMultiline );
if ( io.MouseClickedCount[0] >= 3 )
{
state.SelectStart = LineStart( state.Text, idx );
state.Cursor = LineEnd( state.Text, idx );
}
else
{
int ws = idx, we = idx;
while ( ws > 0 && !IsWordSeparator( state.Text[ws - 1] ) ) ws--;
while ( we < state.Text.Length && !IsWordSeparator( state.Text[we] ) ) we++;
state.SelectStart = ws;
state.Cursor = we;
}
state.SelectedAllMouseLock = true;
state.CursorAnim = -0.30f;
}
else if ( io.MouseClicked[0] && !state.SelectedAllMouseLock )
{
if ( hovered )
{
int idx = LocateIndex( displayText, mouseRel, isMultiline );
state.Cursor = idx;
if ( !io.KeyShift )
state.SelectStart = idx;
state.CursorAnim = -0.30f;
state.CursorFollow = true;
}
}
else if ( io.MouseDown[0] && !state.SelectedAllMouseLock && (io.MouseDelta.x != 0f || io.MouseDelta.y != 0f) )
{
state.Cursor = LocateIndex( displayText, mouseRel, isMultiline );
state.CursorAnim = -0.30f;
state.CursorFollow = true;
}
if ( state.SelectedAllMouseLock && !io.MouseDown[0] )
state.SelectedAllMouseLock = false;
// Keyboard: typed characters
if ( !isReadOnly )
{
foreach ( var ch in io.InputQueueCharacters )
{
if ( ch == '\n' || ch == '\r' || ch == '\t' )
continue; // handled through keys
char c = ch;
if ( !InputTextFilterCharacter( ref c, flags, callback, false, state ) )
continue;
InsertText( state, c.ToString(), flags );
}
}
// Keyboard: editing keys
string beforeKeys = state.Text;
foreach ( var k in io.InputQueueKeys )
{
bool shift = k.Shift;
bool ctrl = k.Ctrl;
switch ( k.Key )
{
case ImGuiKey.LeftArrow:
if ( state.HasSelection && !shift )
state.Cursor = state.SelMin;
else
state.Cursor = ctrl ? WordLeft( state.Text, state.Cursor ) : Math.Max( 0, state.Cursor - 1 );
if ( !shift ) state.ClearSelection();
state.CursorAnim = -0.30f;
state.CursorFollow = true;
break;
case ImGuiKey.RightArrow:
if ( state.HasSelection && !shift )
state.Cursor = state.SelMax;
else
state.Cursor = ctrl ? WordRight( state.Text, state.Cursor ) : Math.Min( state.Text.Length, state.Cursor + 1 );
if ( !shift ) state.ClearSelection();
state.CursorAnim = -0.30f;
state.CursorFollow = true;
break;
case ImGuiKey.UpArrow:
case ImGuiKey.DownArrow:
if ( isMultiline )
{
MoveCursorVertical( state, displayText, k.Key == ImGuiKey.UpArrow ? -1 : 1 );
if ( !shift ) state.ClearSelection();
}
else if ( (flags & ImGuiInputTextFlags.CallbackHistory) != 0 && callback is not null )
{
RunCallback( state, flags, callback, ImGuiInputTextFlags.CallbackHistory, k.Key );
}
state.CursorAnim = -0.30f;
state.CursorFollow = true;
break;
case ImGuiKey.PageUp:
case ImGuiKey.PageDown:
if ( isMultiline )
{
int pageLines = Math.Max( 1, (int)(innerSize.y / g.FontSize) - 1 );
for ( int n = 0; n < pageLines; n++ )
MoveCursorVertical( state, displayText, k.Key == ImGuiKey.PageUp ? -1 : 1 );
if ( !shift ) state.ClearSelection();
state.CursorFollow = true;
}
break;
case ImGuiKey.Home:
state.Cursor = ctrl || !isMultiline ? 0 : LineStart( state.Text, state.Cursor );
if ( !shift ) state.ClearSelection();
state.CursorAnim = -0.30f;
state.CursorFollow = true;
break;
case ImGuiKey.End:
state.Cursor = ctrl || !isMultiline ? state.Text.Length : LineEnd( state.Text, state.Cursor );
if ( !shift ) state.ClearSelection();
state.CursorAnim = -0.30f;
state.CursorFollow = true;
break;
case ImGuiKey.Delete:
if ( isReadOnly ) break;
state.PushUndo();
if ( state.HasSelection )
DeleteSelection( state );
else if ( state.Cursor < state.Text.Length )
{
int end = ctrl ? WordRight( state.Text, state.Cursor ) : state.Cursor + 1;
state.Text = state.Text.Remove( state.Cursor, end - state.Cursor );
}
state.ClearSelection();
state.CursorAnim = -0.30f;
break;
case ImGuiKey.Backspace:
if ( isReadOnly ) break;
state.PushUndo();
if ( state.HasSelection )
DeleteSelection( state );
else if ( state.Cursor > 0 )
{
int start = ctrl ? WordLeft( state.Text, state.Cursor ) : state.Cursor - 1;
state.Text = state.Text.Remove( start, state.Cursor - start );
state.Cursor = start;
}
state.ClearSelection();
state.CursorAnim = -0.30f;
state.CursorFollow = true;
break;
case ImGuiKey.Enter:
case ImGuiKey.KeypadEnter:
{
bool ctrlEnterForNewLine = (flags & ImGuiInputTextFlags.CtrlEnterForNewLine) != 0;
if ( !isMultiline || (ctrlEnterForNewLine && !ctrl) || (!ctrlEnterForNewLine && ctrl) )
{
validated = true;
if ( !io.ConfigInputTextEnterKeepActive || !isMultiline )
clearActiveId = !io.ConfigInputTextEnterKeepActive;
if ( io.ConfigInputTextEnterKeepActive && !isMultiline )
{
state.SelectAll();
clearActiveId = false;
}
}
else if ( !isReadOnly )
{
char c = '\n';
if ( InputTextFilterCharacter( ref c, flags, callback, false, state ) )
{
state.PushUndo();
InsertText( state, c.ToString(), flags );
}
}
break;
}
case ImGuiKey.Escape:
if ( (flags & ImGuiInputTextFlags.EscapeClearsAll) != 0 )
{
if ( state.Text.Length > 0 && !isReadOnly )
{
state.PushUndo();
state.Text = string.Empty;
state.Cursor = state.SelectStart = 0;
}
else
{
clearActiveId = true;
renderCursor = renderSelection = false;
}
}
else
{
if ( !isReadOnly && state.Text != state.InitialText )
{
state.PushUndo();
state.Text = state.InitialText;
state.Cursor = state.SelectStart = state.Text.Length;
}
clearActiveId = true;
renderCursor = renderSelection = false;
}
break;
case ImGuiKey.Tab:
if ( (flags & ImGuiInputTextFlags.CallbackCompletion) != 0 && callback is not null && !isReadOnly )
{
RunCallback( state, flags, callback, ImGuiInputTextFlags.CallbackCompletion, ImGuiKey.Tab );
}
else if ( (flags & ImGuiInputTextFlags.AllowTabInput) != 0 && !isReadOnly )
{
char c = '\t';
if ( InputTextFilterCharacter( ref c, flags, callback, false, state ) )
{
state.PushUndo();
InsertText( state, c.ToString(), flags );
}
}
break;
case ImGuiKey.A:
if ( ctrl && !shift )
{
state.SelectAll();
state.CursorFollow = true;
}
break;
case ImGuiKey.C:
case ImGuiKey.X:
if ( ctrl && !g.InputTextClipboardHandledByPanel && state.HasSelection && !isPassword )
{
string sel = state.Text.Substring( state.SelMin, state.SelMax - state.SelMin );
Sandbox.UI.Clipboard.SetText( sel );
if ( k.Key == ImGuiKey.X && !isReadOnly )
{
state.PushUndo();
DeleteSelection( state );
}
}
break;
case ImGuiKey.Z:
if ( ctrl && !isReadOnly && (flags & ImGuiInputTextFlags.NoUndoRedo) == 0 )
{
if ( shift ) Redo( state ); else Undo( state );
}
break;
case ImGuiKey.Y:
if ( ctrl && !isReadOnly && (flags & ImGuiInputTextFlags.NoUndoRedo) == 0 )
Redo( state );
break;
}
}
g.InputTextClipboardHandledByPanel = false;
// Paste
if ( io.PastedText is not null && !isReadOnly )
{
var sb = new System.Text.StringBuilder();
foreach ( var ch in io.PastedText )
{
char c = ch;
if ( c == '\r' ) continue;
if ( c == '\n' && !isMultiline ) { c = ' '; }
if ( InputTextFilterCharacter( ref c, flags, callback, true, state ) )
sb.Append( c );
}
if ( sb.Length > 0 )
{
state.PushUndo();
InsertText( state, sb.ToString(), flags );
}
}
if ( isReadOnly )
state.Text = text;
// Always callback
if ( (flags & ImGuiInputTextFlags.CallbackAlways) != 0 && callback is not null )
RunCallback( state, flags, callback, ImGuiInputTextFlags.CallbackAlways, ImGuiKey.None );
state.ClampPositions();
if ( state.Text != text && !isReadOnly )
{
if ( (flags & ImGuiInputTextFlags.CallbackEdit) != 0 && callback is not null && state.Text != beforeKeys )
RunCallback( state, flags, callback, ImGuiInputTextFlags.CallbackEdit, ImGuiKey.None );
text = state.Text;
valueChanged = true;
}
}
if ( clearActiveId && g.ActiveId == id )
ClearActiveID();
// ---- Render ----
var drawList = drawWindow.DrawList;
if ( !isMultiline )
{
RenderFrame( frameBb.Min, frameBb.Max, GetColorU32Internal( ImGuiCol.FrameBg ), true, style.FrameRounding );
}
bool isActive = g.ActiveId == id && stateIsOurs;
string bufDisplay = isActive || (stateIsOurs && renderCursor) ? state.Text : text;
if ( isPassword )
bufDisplay = new string( '*', bufDisplay.Length );
var clipRect = isMultiline
? new ImRect( drawWindow.InnerClipRect.Min, drawWindow.InnerClipRect.Max )
: new ImRect( frameBb.Min.x + style.FramePadding.x * 0.5f, frameBb.Min.y, frameBb.Max.x - style.FramePadding.x * 0.5f, frameBb.Max.y );
var drawPos = isMultiline ? drawWindow.DC.CursorPos : frameBb.Min + style.FramePadding;
var lines = bufDisplay.Split( '\n' );
float textMaxWidth = 0f;
if ( isMultiline )
{
foreach ( var l in lines )
textMaxWidth = MathF.Max( textMaxWidth, TextWidthPrefix( l, l.Length ) );
}
var textSize = new Vector2( textMaxWidth, lines.Length * g.FontSize );
float scrollX = 0f;
if ( isActive )
{
IndexToLineCol( bufDisplay, state.Cursor, out int curLine, out int curCol, out _ );
var cursorOffset = new Vector2( TextWidthPrefix( lines[curLine], curCol ), (curLine + 1) * g.FontSize );
if ( state.CursorFollow )
{
if ( !isMultiline && (flags & ImGuiInputTextFlags.NoHorizontalScroll) == 0 )
{
float scrollIncrementX = innerSize.x * 0.25f;
float visibleWidth = innerSize.x - style.FramePadding.x;
if ( cursorOffset.x < state.ScrollX )
state.ScrollX = ImTrunc( MathF.Max( 0.0f, cursorOffset.x - scrollIncrementX ) );
else if ( cursorOffset.x - visibleWidth >= state.ScrollX )
state.ScrollX = ImTrunc( cursorOffset.x - visibleWidth + scrollIncrementX );
}
else
{
state.ScrollX = 0f;
}
if ( isMultiline )
{
float scrollY = drawWindow.Scroll.y;
if ( cursorOffset.y - g.FontSize < scrollY )
scrollY = MathF.Max( 0.0f, cursorOffset.y - g.FontSize );
else if ( cursorOffset.y - (innerSize.y - style.FramePadding.y * 2.0f) >= scrollY )
scrollY = cursorOffset.y - innerSize.y + style.FramePadding.y * 2.0f;
float scrollMaxY = MathF.Max( (textSize.y + style.FramePadding.y * 2.0f) - innerSize.y, 0.0f );
scrollY = Math.Clamp( scrollY, 0.0f, scrollMaxY );
drawPos.y += drawWindow.Scroll.y - scrollY;
drawWindow.Scroll = new Vector2( drawWindow.Scroll.x, scrollY );
}
state.CursorFollow = false;
}
scrollX = isMultiline ? 0f : state.ScrollX;
// Selection
if ( renderSelection && state.HasSelection )
{
var selCol = GetColorU32Internal( ImGuiCol.TextSelectedBg, renderCursor ? 1.0f : 0.6f );
IndexToLineCol( bufDisplay, state.SelMin, out int l0, out int c0, out _ );
IndexToLineCol( bufDisplay, state.SelMax, out int l1, out int c1, out _ );
drawList.PushClipRect( clipRect.Min, clipRect.Max, true );
for ( int ln = l0; ln <= l1; ln++ )
{
float y = drawPos.y + ln * g.FontSize;
if ( y > clipRect.Max.y + g.FontSize ) break;
if ( y + g.FontSize < clipRect.Min.y ) continue;
string line = lines[ln];
float x0 = ln == l0 ? TextWidthPrefix( line, c0 ) : 0f;
float x1 = ln == l1 ? TextWidthPrefix( line, c1 ) : TextWidthPrefix( line, line.Length ) + MathF.Max( 2f, g.FontSize * 0.25f );
var rMin = new Vector2( drawPos.x - scrollX + x0, y );
var rMax = new Vector2( drawPos.x - scrollX + x1, y + g.FontSize );
drawList.AddRectFilled( rMin, rMax, selCol );
}
drawList.PopClipRect();
}
// Text
RenderTextLines( drawList, drawPos - new Vector2( scrollX, 0 ), lines, GetColorU32Internal( ImGuiCol.Text ), clipRect );
// Cursor
if ( renderCursor )
{
state.CursorAnim += io.DeltaTime;
bool cursorIsVisible = !io.ConfigInputTextCursorBlink || state.CursorAnim <= 0.0f || ImFmod( state.CursorAnim, 1.20f ) <= 0.80f;
var cursorScreenPos = ImFloor( drawPos + cursorOffset - new Vector2( scrollX, 0f ) );
var cursorRect = new ImRect( cursorScreenPos.x, cursorScreenPos.y - g.FontSize + 0.5f, cursorScreenPos.x + 1.0f, cursorScreenPos.y - 1.5f );
if ( cursorIsVisible && cursorRect.Overlaps( clipRect ) )
drawList.AddLine( cursorRect.Min, cursorRect.BL, GetColorU32Internal( ImGuiCol.InputTextCursor ) );
}
}
else
{
if ( bufDisplay.Length == 0 && hint is not null )
{
RenderTextLines( drawList, drawPos, LabelText( hint ).Split( '\n' ), GetColorU32Internal( ImGuiCol.TextDisabled ), clipRect );
}
else
{
RenderTextLines( drawList, drawPos, lines, GetColorU32Internal( ImGuiCol.Text ), clipRect );
}
}
if ( isActive && bufDisplay.Length == 0 && hint is not null )
RenderTextLines( drawList, drawPos, LabelText( hint ).Split( '\n' ), GetColorU32Internal( ImGuiCol.TextDisabled ), clipRect );
if ( isMultiline )
{
Dummy( new Vector2( textSize.x, textSize.y + style.FramePadding.y ) );
EndChild();
EndGroup();
g.LastItemData.ID = id;
g.LastItemData.InFlags = itemFlagsBackup;
g.LastItemData.StatusFlags = itemStatusBackup | (g.LastItemData.StatusFlags & ImGuiItemStatusFlags.HoveredWindow);
}
if ( labelSize.x > 0 && !isMerged )
RenderText( new Vector2( frameBb.Max.x + style.ItemInnerSpacing.x, frameBb.Min.y + style.FramePadding.y ), label );
if ( valueChanged && !g.InputTextNoMarkEdited )
MarkItemEdited( id );
else if ( valueChanged )
g.LastItemData.StatusFlags |= ImGuiItemStatusFlags.Edited;
if ( (flags & ImGuiInputTextFlags.EnterReturnsTrue) != 0 )
return validated;
return valueChanged;
}
private static void RenderTextLines( ImDrawList drawList, Vector2 pos, string[] lines, Color32 col, ImRect clip )
{
var g = G;
drawList.PushClipRect( clip.Min, clip.Max, true );
for ( int i = 0; i < lines.Length; i++ )
{
float y = pos.y + i * g.FontSize;
if ( y > clip.Max.y ) break;
if ( y + g.FontSize < clip.Min.y ) continue;
if ( lines[i].Length > 0 )
drawList.AddText( 0f, new Vector2( pos.x, y ), col, lines[i] );
}
drawList.PopClipRect();
}
private static void InsertText( ImGuiInputTextState state, string s, ImGuiInputTextFlags flags )
{
if ( state.HasSelection )
DeleteSelection( state );
if ( state.MaxLength > 0 )
{
int room = state.MaxLength - state.Text.Length;
if ( room <= 0 ) return;
if ( s.Length > room ) s = s.Substring( 0, room );
}
bool overwrite = (flags & ImGuiInputTextFlags.AlwaysOverwrite) != 0;
if ( overwrite && state.Cursor < state.Text.Length && s.Length == 1 && state.Text[state.Cursor] != '\n' )
state.Text = state.Text.Remove( state.Cursor, 1 );
state.Text = state.Text.Insert( state.Cursor, s );
state.Cursor += s.Length;
state.SelectStart = state.Cursor;
state.CursorAnim = -0.30f;
state.CursorFollow = true;
}
private static void DeleteSelection( ImGuiInputTextState state )
{
if ( !state.HasSelection ) return;
int min = state.SelMin;
state.Text = state.Text.Remove( min, state.SelMax - min );
state.Cursor = state.SelectStart = min;
}
private static void Undo( ImGuiInputTextState state )
{
if ( state.UndoStack.Count == 0 ) return;
state.RedoStack.Add( (state.Text, state.Cursor, state.SelectStart) );
var e = state.UndoStack[^1];
state.UndoStack.RemoveAt( state.UndoStack.Count - 1 );
state.Text = e.Text;
state.Cursor = e.Cursor;
state.SelectStart = e.Select;
state.ClampPositions();
state.CursorFollow = true;
}
private static void Redo( ImGuiInputTextState state )
{
if ( state.RedoStack.Count == 0 ) return;
state.UndoStack.Add( (state.Text, state.Cursor, state.SelectStart) );
var e = state.RedoStack[^1];
state.RedoStack.RemoveAt( state.RedoStack.Count - 1 );
state.Text = e.Text;
state.Cursor = e.Cursor;
state.SelectStart = e.Select;
state.ClampPositions();
state.CursorFollow = true;
}
private static void MoveCursorVertical( ImGuiInputTextState state, string displayText, int dir )
{
IndexToLineCol( displayText, state.Cursor, out int line, out int col, out int lineStartIdx );
var lines = displayText.Split( '\n' );
int target = line + dir;
if ( target < 0 )
{
state.Cursor = 0;
return;
}
if ( target >= lines.Length )
{
state.Cursor = displayText.Length;
return;
}
float x = TextWidthPrefix( lines[line], col );
int idx = 0;
for ( int i = 0; i < target; i++ )
idx += lines[i].Length + 1;
state.Cursor = idx + ColumnFromX( lines[target], x );
}
private static void RunCallback( ImGuiInputTextState state, ImGuiInputTextFlags flags, ImGuiInputTextCallback callback, ImGuiInputTextFlags eventFlag, ImGuiKey key )
{
var data = new ImGuiInputTextCallbackData
{
EventFlag = eventFlag,
Flags = flags,
EventKey = key,
CursorPos = state.Cursor,
SelectionStart = state.SelectStart,
SelectionEnd = state.Cursor,
BufSize = state.MaxLength > 0 ? state.MaxLength : int.MaxValue,
};
data.SetBufSilently( state.Text );
callback( data );
bool isReadOnly = (flags & ImGuiInputTextFlags.ReadOnly) != 0;
if ( data.BufDirty && !isReadOnly )
{
state.PushUndo();
var newText = data.Buf;
if ( state.MaxLength > 0 && newText.Length > state.MaxLength )
newText = newText.Substring( 0, state.MaxLength );
state.Text = newText;
state.CursorFollow = true;
}
state.Cursor = Math.Clamp( data.CursorPos, 0, state.Text.Length );
state.SelectStart = Math.Clamp( data.SelectionStart == data.SelectionEnd ? state.Cursor : data.SelectionStart, 0, state.Text.Length );
if ( data.SelectionStart != data.SelectionEnd )
state.Cursor = Math.Clamp( data.SelectionEnd, 0, state.Text.Length );
}
}