diff --git a/TextControlBox/Core/CanvasUpdateManager.cs b/TextControlBox/Core/CanvasUpdateManager.cs
index 8f73aad..5a7aa56 100644
--- a/TextControlBox/Core/CanvasUpdateManager.cs
+++ b/TextControlBox/Core/CanvasUpdateManager.cs
@@ -23,9 +23,22 @@ public void UpdateCursor()
if (!coreTextbox.canvasCursor.ReadyToDraw)
return;
+ coreTextbox.caretBlinkManager?.NotifyCaretActivity();
+
//coreTextbox.canvasCursor.Invalidate();
_batchRedrawer.RequestRedraw(coreTextbox.canvasCursor);
}
+
+ // Redraw the caret canvas for a blink-phase toggle WITHOUT resetting the blink
+ // phase. UpdateCursor resets the phase via NotifyCaretActivity, so the blink
+ // timer must use this path or the caret would never blink.
+ public void RedrawCursorForBlink()
+ {
+ if (!coreTextbox.canvasCursor.ReadyToDraw)
+ return;
+
+ _batchRedrawer.RequestRedraw(coreTextbox.canvasCursor);
+ }
public void UpdateText()
{
if (!coreTextbox.canvasText.ReadyToDraw)
diff --git a/TextControlBox/Core/CaretBlinkManager.cs b/TextControlBox/Core/CaretBlinkManager.cs
new file mode 100644
index 0000000..c5f4fac
--- /dev/null
+++ b/TextControlBox/Core/CaretBlinkManager.cs
@@ -0,0 +1,64 @@
+using Microsoft.UI.Dispatching;
+using System;
+
+namespace TextControlBoxNS.Core;
+
+///
+/// Drives the always-on blinking of the text caret. While the editor is focused the
+/// caret toggles between visible and hidden on a fixed interval; cursor activity
+/// (typing, moving the caret) resets the phase so the caret stays solid for a full
+/// interval, matching standard text-editor behavior.
+///
+internal class CaretBlinkManager
+{
+ // Windows default caret blink interval (the GetCaretBlinkTime default is 530ms).
+ private const int BlinkIntervalMs = 530;
+
+ private CanvasUpdateManager canvasUpdateManager;
+ private DispatcherQueueTimer timer;
+
+ /// True while the caret should be painted for the current blink phase.
+ public bool IsCaretVisible { get; private set; } = true;
+
+ public void Init(CanvasUpdateManager canvasUpdateManager)
+ {
+ this.canvasUpdateManager = canvasUpdateManager;
+
+ timer = DispatcherQueue.GetForCurrentThread().CreateTimer();
+ timer.Interval = TimeSpan.FromMilliseconds(BlinkIntervalMs);
+ timer.Tick += (s, e) =>
+ {
+ IsCaretVisible = !IsCaretVisible;
+ canvasUpdateManager.RedrawCursorForBlink();
+ };
+ }
+
+ /// Begin (or restart) blinking with the caret shown for a full interval.
+ public void Start()
+ {
+ IsCaretVisible = true;
+ if (timer != null && !timer.IsRunning)
+ timer.Start();
+ }
+
+ /// Stop blinking and leave the caret hidden (it only paints while focused).
+ public void Stop()
+ {
+ IsCaretVisible = false;
+ timer?.Stop();
+ }
+
+ ///
+ /// Keep the caret solid for a full interval after cursor activity (typing, moving),
+ /// so it doesn't blink mid-keystroke. No-op until the blink timer is running.
+ ///
+ public void NotifyCaretActivity()
+ {
+ IsCaretVisible = true;
+ if (timer != null && timer.IsRunning)
+ {
+ timer.Stop();
+ timer.Start();
+ }
+ }
+}
diff --git a/TextControlBox/Core/CoreTextControlBox.xaml.cs b/TextControlBox/Core/CoreTextControlBox.xaml.cs
index 0e26204..8af75e1 100644
--- a/TextControlBox/Core/CoreTextControlBox.xaml.cs
+++ b/TextControlBox/Core/CoreTextControlBox.xaml.cs
@@ -36,6 +36,7 @@ internal sealed partial class CoreTextControlBox : UserControl
public readonly TextActionManager textActionManager;
public readonly TextRenderer textRenderer;
public readonly CursorRenderer cursorRenderer;
+ public readonly CaretBlinkManager caretBlinkManager;
public readonly ScrollManager scrollManager;
public readonly CurrentLineManager currentLineManager;
public readonly LongestLineManager longestLineManager;
@@ -91,6 +92,7 @@ public CoreTextControlBox()
textActionManager = new TextActionManager();
textRenderer = new TextRenderer();
cursorRenderer = new CursorRenderer();
+ caretBlinkManager = new CaretBlinkManager();
scrollManager = new ScrollManager();
currentLineManager = new CurrentLineManager();
longestLineManager = new LongestLineManager();
@@ -124,9 +126,10 @@ public CoreTextControlBox()
selectionRenderer.Init(selectionManager, textRenderer, eventsManager, scrollManager, zoomManager, designHelper, textManager);
flyoutHelper.Init(this);
canvasUpdateManager.Init(this);
+ caretBlinkManager.Init(canvasUpdateManager);
textActionManager.Init(this, textRenderer, undoRedo, currentLineManager, longestLineManager, canvasUpdateManager, textManager, selectionRenderer, cursorManager, scrollManager, eventsManager, stringManager, selectionManager, autoIndentionManager);
textRenderer.Init(cursorManager, designHelper, textLayoutManager, textManager, scrollManager, lineNumberRenderer, longestLineManager, this, searchManager, canvasUpdateManager, zoomManager, invisibleCharactersRenderer, linkRenderer, linkHighlightManager);
- cursorRenderer.Init(cursorManager, currentLineManager, textRenderer, focusManager, textManager, scrollManager, zoomManager, designHelper, lineHighlighterRenderer, eventsManager, longestLineManager);
+ cursorRenderer.Init(cursorManager, currentLineManager, textRenderer, focusManager, textManager, scrollManager, zoomManager, designHelper, lineHighlighterRenderer, eventsManager, longestLineManager, caretBlinkManager);
scrollManager.Init(this, canvasUpdateManager, textManager, textRenderer, cursorManager, zoomManager, VerticalScrollbar, HorizontalScrollbar);
currentLineManager.Init(cursorManager, textManager);
longestLineManager.Init(selectionManager, textManager, textRenderer);
@@ -933,6 +936,8 @@ public void Unload()
horizontalScrollBar.Scroll -= scrollManager.HorizontalScrollBar_Scroll;
}
+ caretBlinkManager.Stop();
+
textRenderer.CheckDispose();
lineNumberRenderer.CheckDispose();
diff --git a/TextControlBox/Core/FocusManager.cs b/TextControlBox/Core/FocusManager.cs
index 1f14b67..43e3f77 100644
--- a/TextControlBox/Core/FocusManager.cs
+++ b/TextControlBox/Core/FocusManager.cs
@@ -27,6 +27,7 @@ public void SetFocus()
HasFocus = true;
eventsManager.CallGotFocus();
+ coreTextbox.caretBlinkManager.Start();
canvasUpdateManager.UpdateCursor();
coreTextbox.ChangeCursor(InputSystemCursorShape.IBeam);
@@ -38,6 +39,7 @@ public void RemoveFocus()
canvasUpdateManager.UpdateCursor();
HasFocus = false;
+ coreTextbox.caretBlinkManager.Stop();
coreTextbox.ChangeCursor(InputSystemCursorShape.Arrow);
}
}
diff --git a/TextControlBox/Core/Renderer/CursorRenderer.cs b/TextControlBox/Core/Renderer/CursorRenderer.cs
index 65d36a8..499ecb9 100644
--- a/TextControlBox/Core/Renderer/CursorRenderer.cs
+++ b/TextControlBox/Core/Renderer/CursorRenderer.cs
@@ -23,6 +23,7 @@ internal class CursorRenderer
private LineHighlighterRenderer lineHighlighterRenderer;
private EventsManager eventsManager;
private LongestLineManager longestLineManager;
+ private CaretBlinkManager caretBlinkManager;
public void Init(
CursorManager cursorManager,
@@ -35,7 +36,8 @@ public void Init(
DesignHelper designHelper,
LineHighlighterRenderer lineHighlighterRenderer,
EventsManager eventsManager,
- LongestLineManager longestLineManager)
+ LongestLineManager longestLineManager,
+ CaretBlinkManager caretBlinkManager)
{
this.cursorManager = cursorManager;
this.currentLineManager = currentLineManager;
@@ -48,6 +50,7 @@ public void Init(
this.lineHighlighterRenderer = lineHighlighterRenderer;
this.eventsManager = eventsManager;
this.longestLineManager = longestLineManager;
+ this.caretBlinkManager = caretBlinkManager;
}
public void RenderCursor(CanvasTextLayout textLayout, int characterPosition, float xOffset, float y, float fontSize, CursorSize customSize, CanvasDrawEventArgs args, CanvasSolidColorBrush cursorColorBrush)
@@ -91,15 +94,20 @@ public void Draw(CanvasControl canvasText, CanvasControl canvasCursor, CanvasDra
if (characterPos > currentLineLength)
characterPos = currentLineLength;
- RenderCursor(
- textRenderer.CurrentLineTextLayout,
- characterPos,
- (float)-scrollManager.HorizontalScroll,
- renderPosY,
- zoomManager.ZoomedFontSize,
- _CursorSize,
- args,
- designHelper.CursorColorBrush);
+ // Only paint the caret during the "on" phase of the blink. The current-line highlighter
+ // below stays unconditional so the highlighted line does not flicker while the caret blinks.
+ if (caretBlinkManager.IsCaretVisible)
+ {
+ RenderCursor(
+ textRenderer.CurrentLineTextLayout,
+ characterPos,
+ (float)-scrollManager.HorizontalScroll,
+ renderPosY,
+ zoomManager.ZoomedFontSize,
+ _CursorSize,
+ args,
+ designHelper.CursorColorBrush);
+ }
if (!cursorManager.Equals(cursorManager.currentCursorPosition, cursorManager.oldCursorPosition))
{