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)) {