Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
9cca937
Change markup: Move HorizontalScrollbar from ScrollGrid into MainGrid
Storik4pro Jan 2, 2026
ea0a99c
Add logic for calculating VerticalDrawOffset of text element; Applyin…
Storik4pro Jan 2, 2026
c56b737
Add textRenderer.VerticalDrawOffset to calculating distanse to the to…
Storik4pro Jan 2, 2026
0b3b067
Add textRenderer.VerticalDrawOffset to CanvasDrawEventArgs.DrawTextLa…
Storik4pro Jan 2, 2026
fe9079d
Add textRenderer.VerticalDrawOffset to DrawSelection (marginTop param)
Storik4pro Jan 2, 2026
5244270
Add ContentVerticalScrollOffset property; Add Canvas update requests …
Storik4pro Jan 2, 2026
14e1e0e
Add textRenderer.VerticalDrawOffset to first visible line counting fo…
Storik4pro Jan 2, 2026
0313b10
Add textRenderer.VerticalDrawOffset to first visible line counting fo…
Storik4pro Jan 2, 2026
6f649ee
Add textRenderer.VerticalDrawOffset to line number counting formula
Storik4pro Jan 2, 2026
99b668b
Add new VerticalScrollOffset sruct for make control VerticalScrollOff…
Storik4pro Jan 2, 2026
c9ea4d3
Add ContentVerticalScrollOffset property to control
Storik4pro Jan 2, 2026
c4de059
Add test for vertical scroll offset feature
Storik4pro Jan 2, 2026
b6ca3a2
Refactor code; Fix verticalScrollBar maximum; Fix start line calculat…
Storik4pro Jan 2, 2026
f058cdd
Add basic work with offset for ScrollToShowCursor function
Storik4pro Jan 2, 2026
07e4b06
Fix scrolling bug
Storik4pro Jan 2, 2026
d80287a
fix: cursor hidden with no scrollable lines and an offset defined
FrozenAssassine Jan 3, 2026
0a25e6a
ref: proper invalidation of canvas updates
FrozenAssassine Jan 3, 2026
0b70ee2
ref: simplified verticalScrollOffset struct a bit
FrozenAssassine Jan 3, 2026
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
2 changes: 2 additions & 0 deletions TextControlBox-TestApp/MainWindow.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ public MainWindow()
textbox.NumberOfSpacesForTab = 4;
textbox.ShowWhitespaceCharacters = true;

textbox.ContentVerticalScrollOffset = new TextControlBoxNS.Models.Structs.VerticalScrollOffset(100, 100);

SetWindowTheme(this, ElementTheme.Dark);

textbox.LinkClicked += Textbox_LinkClicked;
Expand Down
6 changes: 5 additions & 1 deletion TextControlBox/Core/CoreTextControlBox.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@
<ColumnDefinition Width="auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="auto"/>
</Grid.RowDefinitions>
<xaml:CanvasControl
Grid.Column="0"
x:Name="Canvas_LineNumber"
Expand Down Expand Up @@ -61,8 +65,8 @@
VerticalAlignment="Stretch"/>

<ScrollBar Grid.Column="2" PointerEntered="Scrollbar_PointerEntered" PointerExited="Scrollbar_PointerExited" SmallChange="10" Maximum="0" LargeChange="100" Minimum="0" x:Name="VerticalScrollbar" Value="0" IndicatorMode="MouseIndicator" Background="Transparent" HorizontalAlignment="Right" Orientation="Vertical" VerticalAlignment="Stretch"/>
<ScrollBar PointerEntered="Scrollbar_PointerEntered" PointerExited="Scrollbar_PointerExited" SmallChange="10" LargeChange="100" Minimum="0" Maximum="0" x:Name="HorizontalScrollbar" Value="0" IndicatorMode="MouseIndicator" Background="Transparent" HorizontalAlignment="Stretch" Orientation="Horizontal" VerticalAlignment="Bottom"/>
</Grid>
<ScrollBar Grid.Row="2" Grid.ColumnSpan="2" PointerEntered="Scrollbar_PointerEntered" PointerExited="Scrollbar_PointerExited" SmallChange="10" LargeChange="100" Minimum="0" Maximum="0" x:Name="HorizontalScrollbar" Value="0" IndicatorMode="MouseIndicator" Background="Transparent" HorizontalAlignment="Stretch" Orientation="Horizontal" VerticalAlignment="Bottom"/>
<controls:InputHandlerControl x:Name="inputHandler" GotFocus="InputManager_GotFocus" LostFocus="InputManager_LostFocus" PreviewKeyDown="InputHandler_KeyDown" TextEntered="InputHandler_TextEntered" Grid.Column="1" Opacity="0" Width="0" Height="0"/>
</Grid>
</UserControl>
14 changes: 14 additions & 0 deletions TextControlBox/Core/CoreTextControlBox.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
using TextControlBoxNS.Languages;
using TextControlBoxNS.Models;
using TextControlBoxNS.Models.Enums;
using TextControlBoxNS.Models.Structs;
using Windows.ApplicationModel.DataTransfer;
using Windows.Foundation;
using Windows.System;
Expand Down Expand Up @@ -985,6 +986,19 @@ public void EndActionGroup()
}
public bool IsGroupingActions => undoRedo.IsGroupingActions;

private VerticalScrollOffset _ContentVerticalScrollOffset = new(0);

public VerticalScrollOffset ContentVerticalScrollOffset
{
get => _ContentVerticalScrollOffset;
set
{
_ContentVerticalScrollOffset = value;
textRenderer.UpdateScrollOffset(value);
canvasUpdateManager.UpdateAll();
}
}

public bool EnableSyntaxHighlighting { get; set; } = true;

public SyntaxHighlightLanguage SyntaxHighlighting
Expand Down
2 changes: 1 addition & 1 deletion TextControlBox/Core/PointerActionsManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -393,7 +393,7 @@ public void PointerWheelAction(ZoomManager zoomManager, PointerRoutedEventArgs e
{
scrollManager.verticalScrollBar.Value -= (delta * scrollManager._VerticalScrollSensitivity) / scrollManager.DefaultVerticalScrollSensitivity;
//Only update when a line was scrolled
if ((int)(scrollManager.verticalScrollBar.Value / textRenderer.SingleLineHeight * scrollManager.DefaultVerticalScrollSensitivity) != textRenderer.NumberOfStartLine)
if ((int)(scrollManager.verticalScrollBar.Value / textRenderer.SingleLineHeight * scrollManager.DefaultVerticalScrollSensitivity + textRenderer.VerticalDrawOffset) != textRenderer.NumberOfStartLine)
{
needsUpdate = true;
}
Expand Down
4 changes: 2 additions & 2 deletions TextControlBox/Core/Renderer/CursorRenderer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -80,10 +80,10 @@ public void Draw(CanvasControl canvasText, CanvasControl canvasCursor, CanvasDra
float singleLineHeight = textRenderer.SingleLineHeight;

//Calculate the distance to the top for the cursorposition and render the cursor
float renderPosY = (float)((cursorManager.LineNumber - startLine) * singleLineHeight) + singleLineHeight / scrollManager.DefaultVerticalScrollSensitivity;
float renderPosY = (float)((cursorManager.LineNumber - startLine) * singleLineHeight) + singleLineHeight / scrollManager.DefaultVerticalScrollSensitivity + textRenderer.VerticalDrawOffset;

//Out of display-region:
if (renderPosY > linesToRender * singleLineHeight || renderPosY < 0)
if (renderPosY > linesToRender * singleLineHeight + textRenderer.VerticalDrawOffset || renderPosY < 0)
return;

textRenderer.UpdateCurrentLineTextLayout(canvasText);
Expand Down
2 changes: 1 addition & 1 deletion TextControlBox/Core/Renderer/LineNumberRenderer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ public void Draw(CanvasControl canvas, CanvasDrawEventArgs args, float spaceBetw

OldLineNumberTextToRender = LineNumberTextToRender;
LineNumberTextLayout = textLayoutManager.CreateTextLayout(canvas, LineNumberTextFormat, LineNumberTextToRender, posX, (float)canvas.Size.Height);
args.DrawingSession.DrawTextLayout(LineNumberTextLayout, 10, textRenderer.SingleLineHeight, designHelper.LineNumberColorBrush);
args.DrawingSession.DrawTextLayout(LineNumberTextLayout, 10, textRenderer.VerticalDrawOffset + textRenderer.SingleLineHeight, designHelper.LineNumberColorBrush);
}

public void CreateLineNumberTextFormat()
Expand Down
2 changes: 1 addition & 1 deletion TextControlBox/Core/Renderer/SelectionRenderer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ public void Draw(CanvasControl canvasSelection, CanvasDrawEventArgs args)
textRenderer.DrawnTextLayout,
args,
(float)-scrollManager.HorizontalScroll,
textRenderer.SingleLineHeight / scrollManager.DefaultVerticalScrollSensitivity,
textRenderer.SingleLineHeight / scrollManager.DefaultVerticalScrollSensitivity + textRenderer.VerticalDrawOffset,
textRenderer.NumberOfStartLine,
textRenderer.NumberOfRenderedLines,
zoomManager.ZoomedFontSize,
Expand Down
52 changes: 46 additions & 6 deletions TextControlBox/Core/Renderer/TextRenderer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using System.Runtime.InteropServices;
using TextControlBoxNS.Core.Text;
using TextControlBoxNS.Helper;
using TextControlBoxNS.Models.Structs;
using Windows.Foundation;

namespace TextControlBoxNS.Core.Renderer;
Expand All @@ -27,6 +28,10 @@ internal class TextRenderer
public int NumberOfRenderedLines = 0;
public string RenderedText = "";
public string OldRenderedText = null;
public float VerticalDrawOffset { get; private set; } = 0;

public double TopScrollOffset { get; private set; } = 0;
public double BottomScrollOffset { get; private set; } = 0;

private CursorManager cursorManager;
private TextManager textManager;
Expand Down Expand Up @@ -75,6 +80,14 @@ public void Init(
this.invisibleCharactersRenderer = invisibleCharactersRenderer;
this.linkRenderer = linkRenderer;
this.linkHighlightManager = linkHighlightManager;

UpdateScrollOffset(coreTextbox.ContentVerticalScrollOffset);
}

public void UpdateScrollOffset(VerticalScrollOffset verticalScrollOffset)
{
this.TopScrollOffset = verticalScrollOffset.Top;
this.BottomScrollOffset = verticalScrollOffset.Bottom;
}

//Check whether the current line is outside the bounds of the visible area
Expand All @@ -99,24 +112,49 @@ public void UpdateCurrentLineTextLayout(CanvasControl canvasText)
var singleLineHeight = SingleLineHeight;

//Measure text position and apply the value to the scrollbar
scrollManager.verticalScrollBar.Maximum = ((textManager.LinesCount + 1) * singleLineHeight - scrollGrid.ActualHeight) / scrollManager.DefaultVerticalScrollSensitivity;
scrollManager.verticalScrollBar.Maximum = ((textManager.LinesCount + 1) * singleLineHeight - scrollGrid.ActualHeight + BottomScrollOffset + TopScrollOffset) / scrollManager.DefaultVerticalScrollSensitivity;
scrollManager.verticalScrollBar.ViewportSize = coreTextbox.canvasText.ActualHeight;

//Calculate number of lines that need to be rendered
int linesToRenderCount = (int)(coreTextbox.canvasText.ActualHeight / singleLineHeight);
linesToRenderCount = Math.Min(linesToRenderCount, textManager.LinesCount);
linesToRenderCount = Math.Min(Math.Max(linesToRenderCount, 1), textManager.LinesCount);

int startLine = (int)((scrollManager.VerticalScroll * scrollManager.DefaultVerticalScrollSensitivity) / singleLineHeight);
int startLine = (int)(((scrollManager.VerticalScroll - VerticalDrawOffset) * scrollManager.DefaultVerticalScrollSensitivity - TopScrollOffset) / singleLineHeight);
startLine = Math.Min(startLine, textManager.LinesCount);

if (startLine < 0) startLine = 0;

int linesToRender = Math.Min(linesToRenderCount, textManager.LinesCount - startLine);

return (startLine, linesToRender);
}

public float CalculateDrawOffset()
{
double verticalScroll = scrollManager.VerticalScroll;

double scrollCoeff = scrollManager.verticalScrollBar.Maximum / scrollManager.VerticalScroll;

double realScrollPosition = verticalScroll * scrollManager.DefaultVerticalScrollSensitivity - TopScrollOffset;
double preCalcOffset = realScrollPosition < 0 ? -realScrollPosition : SingleLineHeight;

float drawOffset = (float)(preCalcOffset < SingleLineHeight ? SingleLineHeight : Math.Floor(preCalcOffset / SingleLineHeight) * SingleLineHeight);

if (drawOffset > SingleLineHeight)
{
if (scrollCoeff == 1)
{
drawOffset = SingleLineHeight;
}
}
return drawOffset - SingleLineHeight;
}


public void Draw(CanvasControl canvasText, CanvasDrawEventArgs args)
{
VerticalDrawOffset = CalculateDrawOffset();

//Create resources and layouts:
if (NeedsTextFormatUpdate || TextFormat == null || lineNumberRenderer.LineNumberTextFormat == null)
{
Expand Down Expand Up @@ -171,13 +209,13 @@ public void Draw(CanvasControl canvasText, CanvasDrawEventArgs args)
searchManager.MatchingSearchLines,
searchManager.searchParameter.SearchExpression,
(float)-scrollManager.HorizontalScroll,
SingleLineHeight / scrollManager.DefaultVerticalScrollSensitivity,
SingleLineHeight / scrollManager.DefaultVerticalScrollSensitivity + VerticalDrawOffset,
designHelper._Design.SearchHighlightColor
);

ccls.DrawTextLayout(DrawnTextLayout, (float)-scrollManager.HorizontalScroll, SingleLineHeight, designHelper.TextColorBrush);
ccls.DrawTextLayout(DrawnTextLayout, (float)-scrollManager.HorizontalScroll, VerticalDrawOffset + SingleLineHeight, designHelper.TextColorBrush);

invisibleCharactersRenderer.DrawTabsAndSpaces(args, ccls, RenderedText, DrawnTextLayout, SingleLineHeight);
invisibleCharactersRenderer.DrawTabsAndSpaces(args, ccls, RenderedText, DrawnTextLayout, VerticalDrawOffset + SingleLineHeight);
}
args.DrawingSession.DrawImage(canvasCommandList);

Expand All @@ -189,5 +227,7 @@ public void Draw(CanvasControl canvasText, CanvasDrawEventArgs args)
{
canvasUpdateManager.UpdateLineNumbers();
}
canvasUpdateManager.UpdateSelection(); // Possible bad for performanse
canvasUpdateManager.UpdateCursor(); // Possible bad for performanse
}
}
30 changes: 25 additions & 5 deletions TextControlBox/Core/ScrollManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ internal void HorizontalScrollBar_Scroll(object sender, ScrollEventArgs e)
internal void VerticalScrollBar_Scroll(object sender, ScrollEventArgs e)
{
//only update when a line was scrolled
if ((int)(verticalScrollBar.Value / textRenderer.SingleLineHeight * DefaultVerticalScrollSensitivity) != textRenderer.NumberOfStartLine)
if ((int)(verticalScrollBar.Value / textRenderer.SingleLineHeight * DefaultVerticalScrollSensitivity + textRenderer.VerticalDrawOffset ) != textRenderer.NumberOfStartLine)
{
canvasHelper.UpdateAll();
}
Expand All @@ -66,7 +66,7 @@ internal void VerticalScrollBar_Scroll(object sender, ScrollEventArgs e)
public void UpdateWhenScrolled()
{
//only update when a line was scrolled
if ((int)(verticalScrollBar.Value / textRenderer.SingleLineHeight) != textRenderer.NumberOfStartLine)
if ((int)(verticalScrollBar.Value / textRenderer.SingleLineHeight + textRenderer.VerticalDrawOffset) != textRenderer.NumberOfStartLine)
{
canvasHelper.UpdateAll();
}
Expand Down Expand Up @@ -133,10 +133,30 @@ public void ScrollPageDown()
}
public void UpdateScrollToShowCursor(bool update = true)
{
if (textRenderer.NumberOfStartLine + textRenderer.NumberOfRenderedLines <= cursorManager.LineNumber)
verticalScrollBar.Value = (cursorManager.LineNumber - textRenderer.NumberOfRenderedLines + 1) * textRenderer.SingleLineHeight / DefaultVerticalScrollSensitivity;
double globalOffset = textRenderer.VerticalDrawOffset == 0 ? textRenderer.TopScrollOffset : 0;
double localOffset = textRenderer.VerticalDrawOffset;

if (cursorManager.LineNumber == 0)
{
verticalScrollBar.Value = 0;
}
else if (textRenderer.NumberOfStartLine + textRenderer.NumberOfRenderedLines <= cursorManager.LineNumber)
{
verticalScrollBar.Value = ((cursorManager.LineNumber - textRenderer.NumberOfRenderedLines + 1) * textRenderer.SingleLineHeight + globalOffset) / DefaultVerticalScrollSensitivity + localOffset;
}
else if (textRenderer.NumberOfStartLine > cursorManager.LineNumber)
verticalScrollBar.Value = (cursorManager.LineNumber - 1) * textRenderer.SingleLineHeight / DefaultVerticalScrollSensitivity;
{
verticalScrollBar.Value = ((cursorManager.LineNumber - 1) * textRenderer.SingleLineHeight + globalOffset) / DefaultVerticalScrollSensitivity + localOffset;
}
else if (textRenderer.VerticalDrawOffset != 0)
{
int realLineDisplayedCount = (int)((verticalScrollBar.ViewportSize - textRenderer.VerticalDrawOffset) / textRenderer.SingleLineHeight);
if (realLineDisplayedCount <= cursorManager.LineNumber)
{
double offsetToMove = ((cursorManager.LineNumber - realLineDisplayedCount + 1) * textRenderer.SingleLineHeight);
verticalScrollBar.Value = ((cursorManager.LineNumber - 1) * textRenderer.SingleLineHeight) / DefaultVerticalScrollSensitivity + offsetToMove;
}
}

if (update)
canvasHelper.UpdateAll();
Expand Down
2 changes: 1 addition & 1 deletion TextControlBox/Helper/CursorHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ internal class CursorHelper
public static int GetCursorLineFromPoint(TextRenderer textRenderer, Point point)
{
//Calculate the relative linenumber, where the pointer was pressed at
int linenumber = (int)(point.Y / textRenderer.SingleLineHeight);
int linenumber = (int)((point.Y - textRenderer.VerticalDrawOffset) / textRenderer.SingleLineHeight);
linenumber += textRenderer.NumberOfStartLine;
return Math.Clamp(linenumber, 0, textRenderer.NumberOfStartLine + textRenderer.NumberOfRenderedLines - 1);
}
Expand Down
99 changes: 99 additions & 0 deletions TextControlBox/Models/Structs/VerticalScrollOffset.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
namespace TextControlBoxNS.Models.Structs;

/// <summary>
/// Describes the offset between content and vertical scroll area borders.
/// Two Double values describe the Top and Bottom offsets, respectively.
/// </summary>
public struct VerticalScrollOffset
{
/// <summary>
/// The top offset of content
/// </summary>
public double Top { get; set; }

/// <summary>
/// The bottom offset of content
/// </summary>

public double Bottom { get; set; }

/// <summary>
/// Creates new VerticalScrollOffset object
/// </summary>
/// <param name="uniformLength">The top and bottom content offset</param>
public VerticalScrollOffset(double uniformLength)
{
Top = (Bottom = uniformLength);
}

/// <summary>
/// Creates new VerticalScrollOffset object
/// </summary>
/// <param name="top">The top offset of content</param>
/// <param name="bottom">The bottom offset of content</param>
public VerticalScrollOffset(double top, double bottom)
{
this.Top = top;
this.Bottom = bottom;
}


/// <inheritdoc/>
public override string ToString()
{
return $"{Top}, {Bottom}";
}

/// <inheritdoc/>
public override bool Equals(object obj)
{
if (obj is VerticalScrollOffset verticalScrollOffset)
{
return this == verticalScrollOffset;
}

return false;
}

/// <summary>
/// Indicates whether this instance and a specified object are equal.
/// </summary>
/// <returns>true if verticalScrollOffset and this instance are represent the same value; otherwise, false.</returns>
public readonly bool Equals(VerticalScrollOffset verticalScrollOffset)
{
return this == verticalScrollOffset;
}

/// <inheritdoc/>
public override int GetHashCode()
{
return Top.GetHashCode() ^ Bottom.GetHashCode();
}

/// <summary>
/// Compares two VerticalScrollOffset objects
/// </summary>
/// <param name="t1"></param>
/// <param name="t2"></param>
/// <returns>true if offsets are same; otherwise, false.</returns>
public static bool operator ==(VerticalScrollOffset t1, VerticalScrollOffset t2)
{
if (t1.Top == t2.Top)
{
return t1.Bottom == t2.Bottom;
}

return false;
}

/// <summary>
/// Compares two VerticalScrollOffset objects
/// </summary>
/// <param name="t1"></param>
/// <param name="t2"></param>
/// <returns>true if offsets are different; otherwise, false.</returns>
public static bool operator !=(VerticalScrollOffset t1, VerticalScrollOffset t2)
{
return !(t1 == t2);
}
}
10 changes: 10 additions & 0 deletions TextControlBox/TextControlBox.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.Collections.Generic;
using TextControlBoxNS.Core;
using TextControlBoxNS.Models;
using TextControlBoxNS.Models.Structs;
using Windows.Foundation;

namespace TextControlBoxNS;
Expand Down Expand Up @@ -664,6 +665,15 @@ public bool AddLines(int start, string[] text)
return coreTextBox.AddLines(start, text);
}

/// <summary>
/// Gets or sets content scroll offset
/// </summary>
public VerticalScrollOffset ContentVerticalScrollOffset
{
get => coreTextBox.ContentVerticalScrollOffset;
set => coreTextBox.ContentVerticalScrollOffset = value;
}

/// <summary>
/// returns the current tabs and spaces detected from the loaded document.
/// with useSpacesInsteadTabs indicates whether spaces are used instead of tabs and with spaces the number of spaces used for a tab
Expand Down