Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions TextControlBox/Core/CanvasUpdateManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
64 changes: 64 additions & 0 deletions TextControlBox/Core/CaretBlinkManager.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
using Microsoft.UI.Dispatching;
using System;

namespace TextControlBoxNS.Core;

/// <summary>
/// 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.
/// </summary>
internal class CaretBlinkManager
{
// Windows default caret blink interval (the GetCaretBlinkTime default is 530ms).
private const int BlinkIntervalMs = 530;

private CanvasUpdateManager canvasUpdateManager;
private DispatcherQueueTimer timer;

/// <summary>True while the caret should be painted for the current blink phase.</summary>
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();
};
}

/// <summary>Begin (or restart) blinking with the caret shown for a full interval.</summary>
public void Start()
{
IsCaretVisible = true;
if (timer != null && !timer.IsRunning)
timer.Start();
}

/// <summary>Stop blinking and leave the caret hidden (it only paints while focused).</summary>
public void Stop()
{
IsCaretVisible = false;
timer?.Stop();
}

/// <summary>
/// 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.
/// </summary>
public void NotifyCaretActivity()
{
IsCaretVisible = true;
if (timer != null && timer.IsRunning)
{
timer.Stop();
timer.Start();
}
}
}
7 changes: 6 additions & 1 deletion TextControlBox/Core/CoreTextControlBox.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -933,6 +936,8 @@ public void Unload()
horizontalScrollBar.Scroll -= scrollManager.HorizontalScrollBar_Scroll;
}

caretBlinkManager.Stop();

textRenderer.CheckDispose();
lineNumberRenderer.CheckDispose();

Expand Down
2 changes: 2 additions & 0 deletions TextControlBox/Core/FocusManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ public void SetFocus()

HasFocus = true;
eventsManager.CallGotFocus();
coreTextbox.caretBlinkManager.Start();
canvasUpdateManager.UpdateCursor();

coreTextbox.ChangeCursor(InputSystemCursorShape.IBeam);
Expand All @@ -38,6 +39,7 @@ public void RemoveFocus()
canvasUpdateManager.UpdateCursor();

HasFocus = false;
coreTextbox.caretBlinkManager.Stop();
coreTextbox.ChangeCursor(InputSystemCursorShape.Arrow);
}
}
28 changes: 18 additions & 10 deletions TextControlBox/Core/Renderer/CursorRenderer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ internal class CursorRenderer
private LineHighlighterRenderer lineHighlighterRenderer;
private EventsManager eventsManager;
private LongestLineManager longestLineManager;
private CaretBlinkManager caretBlinkManager;

public void Init(
CursorManager cursorManager,
Expand All @@ -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;
Expand All @@ -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)
Expand Down Expand Up @@ -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))
{
Expand Down