@@ -113,6 +113,24 @@ public sealed unsafe class MemoryEditor : IDisposable
113113 private sbyte * _dataInputBuf ;
114114 private sbyte * _addrInputBuf ;
115115
116+ /// <summary>
117+ /// Per-call payload handed to <see cref="DataInputCallback"/>.
118+ /// </summary>
119+ /// <remarks>
120+ /// The address of an instance is passed as <c>user_data</c>, so it lives on
121+ /// the stack for the duration of one <c>InputText</c> call and must stay
122+ /// blittable.
123+ /// <para>
124+ /// <see cref="DataInputUserData.CurrentBufOverwrite"/> is <c>char[3]</c> in
125+ /// C++ and holds the two hex digits plus a terminator.
126+ /// </para>
127+ /// </remarks>
128+ private struct DataInputUserData
129+ {
130+ public fixed sbyte CurrentBufOverwrite [ 3 ] ;
131+ public int CursorPos ;
132+ }
133+
116134 private bool _disposed ;
117135
118136 // Scratch buffer for the data preview; needs no persistence.
@@ -310,7 +328,11 @@ public void DrawContents(void* memDataVoid, nuint memSize, nuint baseDisplayAddr
310328 + ImGui . GetTextLineHeightWithSpacing ( ) * 3f ;
311329 }
312330
313- ImVec2 scrollingSize = new ( ) { x = - float . MinValue , y = - footerHeight } ;
331+ // C++ writes this as ImVec2(-FLT_MIN, -footer_height): -FLT_MIN is the
332+ // smallest positive normal negated, a sentinel meaning "fill the
333+ // remaining width". C#'s float.MinValue is -FLT_MAX instead, which
334+ // would make BeginChild clamp the width to 4 pixels and hide the data.
335+ ImVec2 scrollingSize = new ( ) { x = - 1.17549435e-38f , y = - footerHeight } ;
314336 int childFlags = ( int ) ImGuiChildFlags . ImGuiChildFlags_None ;
315337 int childWindowFlags = ( int ) ImGuiWindowFlags . ImGuiWindowFlags_NoMove
316338 | ( int ) ImGuiWindowFlags . ImGuiWindowFlags_NoNav ;
@@ -393,9 +415,10 @@ public void DrawContents(void* memDataVoid, nuint memSize, nuint baseDisplayAddr
393415 ? ImGui . GetColorU32 ( ( int ) ImGuiCol . ImGuiCol_TextDisabled )
394416 : colorText ;
395417
396- string addressFormat = OptUpperCaseHex ? "X" : "x" ;
418+ // .NET has no "%02X " equivalent: a trailing space turns "X2 " into a
419+ // custom format string that is emitted literally. The separator is
420+ // appended after formatting instead.
397421 string byteFormat = OptUpperCaseHex ? "X2" : "x2" ;
398- string byteSpaceFormat = OptUpperCaseHex ? "X2 " : "x2 " ;
399422
400423 MouseHovered = false ;
401424 MouseHoveredAddr = 0 ;
@@ -406,11 +429,11 @@ public void DrawContents(void* memDataVoid, nuint memSize, nuint baseDisplayAddr
406429 {
407430 nuint addr = ( nuint ) lineI * ( nuint ) Cols ;
408431
409- DrawAddressLine ( drawList , s , lineI , baseDisplayAddr , addr , colorText , addressFormat ) ;
432+ DrawAddressLine ( drawList , s , lineI , baseDisplayAddr , addr , colorText , OptUpperCaseHex ) ;
410433
411434 addr = ( nuint ) lineI * ( nuint ) Cols ;
412435 DrawHexCells ( drawList , memData , memSize , s , ref addr , colorText , colorDisabled ,
413- byteFormat , byteSpaceFormat , previewDataTypeSize , ref dataNext , ref dataEditingAddrNext ) ;
436+ byteFormat , previewDataTypeSize , ref dataNext , ref dataEditingAddrNext ) ;
414437
415438 if ( OptShowAscii )
416439 {
@@ -469,7 +492,8 @@ private void HandleGotoRequest(nuint memSize, ImVec2 availSize)
469492
470493 if ( _gotoAddr < memSize )
471494 {
472- ImGui . BeginChild ( "##scrolling" , null , 0 , 0 ) ;
495+ ImVec2 scrollingSize = new ( ) { x = 0f , y = 0f } ;
496+ ImGui . BeginChild ( "##scrolling" , & scrollingSize , 0 , 0 ) ;
473497 ImGui . SetScrollY ( ( _gotoAddr / ( nuint ) Cols ) * ImGui . GetTextLineHeight ( ) - availSize . y * 0.5f ) ;
474498 ImGui . EndChild ( ) ;
475499
@@ -543,18 +567,18 @@ private void HandleOptionsPopup(Sizes s, ImVec2 contentsPosStart, ImVec2 content
543567 // -----------------------------------------------------------------------
544568
545569 private void DrawAddressLine ( ImDrawList * drawList , Sizes s , int lineI , nuint baseDisplayAddr ,
546- nuint addr , uint colorText , string digitsFormat )
570+ nuint addr , uint colorText , bool upperCase )
547571 {
548572 _ = drawList ;
549573 _ = colorText ;
550574
551575 nuint displayAddr = baseDisplayAddr + addr ;
552- string text = FormatHex ( displayAddr , s . AddrDigitsCount ) + ": " ;
576+ string text = FormatHex ( displayAddr , s . AddrDigitsCount , OptUpperCaseHex ) + ": " ;
553577 ImGui . TextUnformatted ( text ) ;
554578 }
555579
556580 private void DrawHexCells ( ImDrawList * drawList , byte * memData , nuint memSize , Sizes s ,
557- ref nuint addr , uint colorText , uint colorDisabled , string byteFormat , string byteSpaceFormat ,
581+ ref nuint addr , uint colorText , uint colorDisabled , string byteFormat ,
558582 nuint previewDataTypeSize , ref bool dataNext , ref nuint dataEditingAddrNext )
559583 {
560584 for ( int n = 0 ; n < Cols && addr < memSize ; n ++ , addr ++ )
@@ -617,13 +641,13 @@ private void DrawHexCells(ImDrawList* drawList, byte* memData, nuint memSize, Si
617641 }
618642 else
619643 {
620- DrawStaticByte ( memData , addr , colorDisabled , byteFormat , byteSpaceFormat , ref dataEditingAddrNext ) ;
644+ DrawStaticByte ( memData , addr , colorDisabled , byteFormat , ref dataEditingAddrNext ) ;
621645 }
622646 }
623647 }
624648
625649 private void DrawStaticByte ( byte * memData , nuint addr , uint colorDisabled ,
626- string byteFormat , string byteSpaceFormat , ref nuint dataEditingAddrNext )
650+ string byteFormat , ref nuint dataEditingAddrNext )
627651 {
628652 byte b = ReadFn != null ? ReadFn ( memData , addr , UserData ) : memData [ addr ] ;
629653
@@ -645,7 +669,7 @@ private void DrawStaticByte(byte* memData, nuint addr, uint colorDisabled,
645669 }
646670 else
647671 {
648- ImGui . TextUnformatted ( b . ToString ( byteSpaceFormat ) ) ;
672+ ImGui . TextUnformatted ( b . ToString ( byteFormat ) + " " ) ;
649673 }
650674 }
651675 else if ( b == 0 && OptGreyOutZeroes )
@@ -656,7 +680,7 @@ private void DrawStaticByte(byte* memData, nuint addr, uint colorDisabled,
656680 }
657681 else
658682 {
659- ImGui . TextUnformatted ( b . ToString ( byteSpaceFormat ) ) ;
683+ ImGui . TextUnformatted ( b . ToString ( byteFormat ) + " " ) ;
660684 }
661685
662686 if ( ImGui . IsItemHovered ( ) != 0 )
@@ -672,19 +696,58 @@ private void DrawStaticByte(byte* memData, nuint addr, uint colorDisabled,
672696 }
673697 }
674698
699+ // Mirrors ImGuiInputTextCallbackData's HasSelection(). Kept as a helper
700+ // because the generated struct exposes fields only.
701+ private static bool HasSelection ( ImGuiInputTextCallbackData * data )
702+ => data ->SelectionStart != data ->SelectionEnd ;
703+
704+ /// <summary>
705+ /// Port of the <c>InputTextUserData::Callback</c> in the C++ original.
706+ /// </summary>
707+ /// <remarks>
708+ /// <c>InputText</c> owns the master copy of the text it edits, so when the
709+ /// user is not selecting anything the content is forced back to the byte
710+ /// currently in memory. Without this the widget would keep whatever was
711+ /// typed before the editor scrolled that address out of view.
712+ /// <para>
713+ /// The callback only runs because <c>CallbackAlways</c> is requested; Dear
714+ /// ImGui asserts the pointer is non-null in that case, so passing a flag
715+ /// without a callback aborts the process.
716+ /// </para>
717+ /// </remarks>
718+ [ UnmanagedCallersOnly ( CallConvs = [ typeof ( CallConvCdecl ) ] ) ]
719+ private static int DataInputCallback ( ImGuiInputTextCallbackData * data )
720+ {
721+ DataInputUserData * userData = ( DataInputUserData * ) data ->UserData ;
722+ if ( ! HasSelection ( data ) )
723+ {
724+ userData ->CursorPos = data ->CursorPos ;
725+ }
726+
727+ if ( data ->SelectionStart == 0 && data ->SelectionEnd == data ->BufTextLen )
728+ {
729+ ImGuiInputTextCallbackData . DeleteChars ( data , 0 , data ->BufTextLen ) ;
730+ ImGuiInputTextCallbackData . InsertChars ( data , 0 , new string ( userData ->CurrentBufOverwrite ) ) ;
731+ data ->SelectionStart = 0 ;
732+ data ->SelectionEnd = 2 ;
733+ data ->CursorPos = 0 ;
734+ }
735+
736+ return 0 ;
737+ }
738+
675739 private void DrawEditingByte ( ImDrawList * drawList , byte * memData , Sizes s , nuint addr ,
676740 string byteFormat , ref bool dataNext , ref nuint dataEditingAddrNext )
677741 {
678742 _ = drawList ;
679- _ = s ;
680743
681744 bool dataWrite = false ;
682745 ImGui . PushID ( ( void * ) addr ) ;
683746
684747 if ( _dataEditingTakeFocus )
685748 {
686749 ImGui . SetKeyboardFocusHere ( 0 ) ;
687- WriteCString ( _addrInputBuf , InputBufSize , FormatHex ( addr , s . AddrDigitsCount ) ) ;
750+ WriteCString ( _addrInputBuf , InputBufSize , FormatHex ( addr , s . AddrDigitsCount , OptUpperCaseHex ) ) ;
688751
689752 byte current = ReadFn != null ? ReadFn ( memData , addr , UserData ) : memData [ addr ] ;
690753 WriteCString ( _dataInputBuf , InputBufSize , current . ToString ( byteFormat ) ) ;
@@ -704,7 +767,12 @@ private void DrawEditingByte(ImDrawList* drawList, byte* memData, Sizes s, nuint
704767
705768 ImGui . SetNextItemWidth ( s . GlyphWidth * 2f ) ;
706769
707- if ( ImGui . InputText ( "##data" , _dataInputBuf , InputBufSize , flags ) != 0 )
770+ DataInputUserData userData = default ;
771+ userData . CursorPos = - 1 ;
772+ byte live = ReadFn != null ? ReadFn ( memData , addr , UserData ) : memData [ addr ] ;
773+ WriteCString ( userData . CurrentBufOverwrite , 3 , live . ToString ( byteFormat ) ) ;
774+
775+ if ( ImGui . InputText ( "##data" , _dataInputBuf , InputBufSize , flags , & DataInputCallback , & userData ) != 0 )
708776 {
709777 dataWrite = dataNext = true ;
710778 }
@@ -720,6 +788,12 @@ private void DrawEditingByte(ImDrawList* drawList, byte* memData, Sizes s, nuint
720788 dataWrite = dataNext = false ;
721789 }
722790
791+ // A cursor past the first digit means the user finished the byte.
792+ if ( userData . CursorPos >= 2 )
793+ {
794+ dataWrite = dataNext = true ;
795+ }
796+
723797 if ( ! ReadOnly && dataWrite && TryParseHexByte ( _dataInputBuf , out byte value ) )
724798 {
725799 if ( WriteFn != null )
@@ -811,18 +885,19 @@ private void DrawOptionsLine(Sizes s, void* memData, nuint memSize, nuint baseDi
811885
812886 ImGuiStyle * style = ImGui . GetStyle ( ) ;
813887 string rangeFormat = OptUpperCaseHex
814- ? $ "Range { FormatHex ( baseDisplayAddr , s . AddrDigitsCount ) } .."
815- : $ "Range { FormatHex ( baseDisplayAddr , s . AddrDigitsCount ) } ..";
888+ ? $ "Range { FormatHex ( baseDisplayAddr , s . AddrDigitsCount , OptUpperCaseHex ) } .."
889+ : $ "Range { FormatHex ( baseDisplayAddr , s . AddrDigitsCount , OptUpperCaseHex ) } ..";
816890
817- if ( ImGui . Button ( "Options" , null ) != 0 )
891+ ImVec2 optionsButtonSize = new ( ) { x = 0f , y = 0f } ;
892+ if ( ImGui . Button ( "Options" , & optionsButtonSize ) != 0 )
818893 {
819894 ImGui . OpenPopup ( "OptionsPopup" ) ;
820895 }
821896
822897 ImGui . SameLine ( 0f , - 1f ) ;
823898 _ = rangeFormat ;
824899 ImGui . TextUnformatted (
825- $ "Range { FormatHex ( baseDisplayAddr , s . AddrDigitsCount ) } ..{ FormatHex ( baseDisplayAddr + memSize - 1 , s . AddrDigitsCount ) } ") ;
900+ $ "Range { FormatHex ( baseDisplayAddr , s . AddrDigitsCount , OptUpperCaseHex ) } ..{ FormatHex ( baseDisplayAddr + memSize - 1 , s . AddrDigitsCount , OptUpperCaseHex ) } ") ;
826901
827902 ImGui . SameLine ( 0f , - 1f ) ;
828903 ImGui . SetNextItemWidth ( ( s . AddrDigitsCount + 1 ) * s . GlyphWidth + style ->FramePadding . x * 2f ) ;
@@ -1055,8 +1130,8 @@ private static string FormatBinary(byte* buf, int width)
10551130 // Small helpers
10561131 // -----------------------------------------------------------------------
10571132
1058- private static string FormatHex( nuint value , int digits )
1059- => value. ToString ( "X" + digits . ToString ( ) ) ;
1133+ private static string FormatHex( nuint value , int digits , bool upper )
1134+ => value. ToString ( ( upper ? "X" : "x" ) + digits . ToString ( ) ) ;
10601135
10611136 private static void WriteCString( sbyte * buffer , int size , string text )
10621137 {
0 commit comments