From 9cca93704ccb91c1d6cfe80ce8e50c4c748ff7a1 Mon Sep 17 00:00:00 2001 From: Storik4pro Date: Fri, 2 Jan 2026 20:46:31 +0300 Subject: [PATCH 01/18] Change markup: Move HorizontalScrollbar from ScrollGrid into MainGrid Now horizontal scrollbar doesn't interfere text editing and selection on last line (Before this change user can misclick on scrollbar instead of text line) --- TextControlBox/Core/CoreTextControlBox.xaml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/TextControlBox/Core/CoreTextControlBox.xaml b/TextControlBox/Core/CoreTextControlBox.xaml index de7d0f9..e42f66b 100644 --- a/TextControlBox/Core/CoreTextControlBox.xaml +++ b/TextControlBox/Core/CoreTextControlBox.xaml @@ -23,6 +23,10 @@ + + + + - + \ No newline at end of file From ea0a99cfcf46035cead9b390996e19b3f863b49d Mon Sep 17 00:00:00 2001 From: Storik4pro Date: Fri, 2 Jan 2026 20:54:07 +0300 Subject: [PATCH 02/18] Add logic for calculating VerticalDrawOffset of text element; Applying VerticalDrawOffset to class functions VerticalDrawOffset will be applyed to: - CalculateLinesToRender function - SearchHighlightsRenderer.RenderHighlights (OffsetTop param) - CanvasDrawingSession.DrawTextLayout (Y param) - invisibleCharactersRenderer.DrawTabsAndSpaces (singleLineHeight param) Also: - UpdateScrollOffset function will be added for handle control vertical scroll offset changing - float VerticalDrawOffset variable (default value is 0) - double TopScrollOffset and BottomScrollOffset variables --- TextControlBox/Core/Renderer/TextRenderer.cs | 48 ++++++++++++++++++-- 1 file changed, 43 insertions(+), 5 deletions(-) diff --git a/TextControlBox/Core/Renderer/TextRenderer.cs b/TextControlBox/Core/Renderer/TextRenderer.cs index 01246b0..3ae23c1 100644 --- a/TextControlBox/Core/Renderer/TextRenderer.cs +++ b/TextControlBox/Core/Renderer/TextRenderer.cs @@ -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; @@ -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; + + private double TopScrollOffset = 0; + private double BottomScrollOffset = 0; private CursorManager cursorManager; private TextManager textManager; @@ -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 @@ -99,16 +112,18 @@ 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) / 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); - int startLine = (int)((scrollManager.VerticalScroll * scrollManager.DefaultVerticalScrollSensitivity) / singleLineHeight); + int startLine = (int)(((scrollManager.VerticalScroll - VerticalDrawOffset) * scrollManager.DefaultVerticalScrollSensitivity) / singleLineHeight); startLine = Math.Min(startLine, textManager.LinesCount); + if (startLine < 0) startLine = 0; + int linesToRender = Math.Min(linesToRenderCount, textManager.LinesCount - startLine); return (startLine, linesToRender); @@ -117,6 +132,29 @@ public void UpdateCurrentLineTextLayout(CanvasControl canvasText) public void Draw(CanvasControl canvasText, CanvasDrawEventArgs args) { + double verticalScroll = scrollManager.VerticalScroll; + if (scrollManager.VerticalScroll < 6 && scrollManager.VerticalScroll != 0) + { + verticalScroll = 0; + } + + 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; + } + } + + VerticalDrawOffset = drawOffset - SingleLineHeight; + //Create resources and layouts: if (NeedsTextFormatUpdate || TextFormat == null || lineNumberRenderer.LineNumberTextFormat == null) { @@ -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); From c56b737ba16f3c937700023f0513386e25bdc951 Mon Sep 17 00:00:00 2001 From: Storik4pro Date: Fri, 2 Jan 2026 20:55:54 +0300 Subject: [PATCH 03/18] Add textRenderer.VerticalDrawOffset to calculating distanse to the top of cursor position formula --- TextControlBox/Core/Renderer/CursorRenderer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TextControlBox/Core/Renderer/CursorRenderer.cs b/TextControlBox/Core/Renderer/CursorRenderer.cs index 8934928..9bc9df9 100644 --- a/TextControlBox/Core/Renderer/CursorRenderer.cs +++ b/TextControlBox/Core/Renderer/CursorRenderer.cs @@ -80,7 +80,7 @@ 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) From 0b3b067d5d545acf7305e236668c2197c56432cb Mon Sep 17 00:00:00 2001 From: Storik4pro Date: Fri, 2 Jan 2026 20:56:56 +0300 Subject: [PATCH 04/18] Add textRenderer.VerticalDrawOffset to CanvasDrawEventArgs.DrawTextLayout (Y param) --- TextControlBox/Core/Renderer/LineNumberRenderer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TextControlBox/Core/Renderer/LineNumberRenderer.cs b/TextControlBox/Core/Renderer/LineNumberRenderer.cs index 90d9e46..06d3b74 100644 --- a/TextControlBox/Core/Renderer/LineNumberRenderer.cs +++ b/TextControlBox/Core/Renderer/LineNumberRenderer.cs @@ -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() From fe9079d6d71cc6fab6f046fd9e124a469283a2de Mon Sep 17 00:00:00 2001 From: Storik4pro Date: Fri, 2 Jan 2026 20:57:57 +0300 Subject: [PATCH 05/18] Add textRenderer.VerticalDrawOffset to DrawSelection (marginTop param) --- TextControlBox/Core/Renderer/SelectionRenderer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TextControlBox/Core/Renderer/SelectionRenderer.cs b/TextControlBox/Core/Renderer/SelectionRenderer.cs index 24c5618..e2e6418 100644 --- a/TextControlBox/Core/Renderer/SelectionRenderer.cs +++ b/TextControlBox/Core/Renderer/SelectionRenderer.cs @@ -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, From 524427058bc710dd57db5145eedd131ff779b71f Mon Sep 17 00:00:00 2001 From: Storik4pro Date: Fri, 2 Jan 2026 20:59:21 +0300 Subject: [PATCH 06/18] Add ContentVerticalScrollOffset property; Add Canvas update requests if ContentVerticalScrollOffset property changed --- TextControlBox/Core/CoreTextControlBox.xaml.cs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/TextControlBox/Core/CoreTextControlBox.xaml.cs b/TextControlBox/Core/CoreTextControlBox.xaml.cs index 2a39099..ffbf69c 100644 --- a/TextControlBox/Core/CoreTextControlBox.xaml.cs +++ b/TextControlBox/Core/CoreTextControlBox.xaml.cs @@ -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; @@ -985,6 +986,23 @@ public void EndActionGroup() } public bool IsGroupingActions => undoRedo.IsGroupingActions; + private VerticalScrollOffset _ContentVerticalScrollOffset = new(0); + + public VerticalScrollOffset ContentVerticalScrollOffset + { + get => _ContentVerticalScrollOffset; + set + { + _ContentVerticalScrollOffset = value; + + textRenderer.UpdateScrollOffset(value); + Canvas_Text.UpdateLayout(); + Canvas_Selection.UpdateLayout(); + Canvas_Cursor.UpdateLayout(); + Canvas_LineNumber.UpdateLayout(); + } + } + public bool EnableSyntaxHighlighting { get; set; } = true; public SyntaxHighlightLanguage SyntaxHighlighting From 14e1e0ef514418dbe3028d13a9efaf8d3bd4baba Mon Sep 17 00:00:00 2001 From: Storik4pro Date: Fri, 2 Jan 2026 21:01:07 +0300 Subject: [PATCH 07/18] Add textRenderer.VerticalDrawOffset to first visible line counting formula --- TextControlBox/Core/PointerActionsManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TextControlBox/Core/PointerActionsManager.cs b/TextControlBox/Core/PointerActionsManager.cs index 148484c..bf8c924 100644 --- a/TextControlBox/Core/PointerActionsManager.cs +++ b/TextControlBox/Core/PointerActionsManager.cs @@ -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; } From 0313b107b20f69a4a4621001657bb959d9486629 Mon Sep 17 00:00:00 2001 From: Storik4pro Date: Fri, 2 Jan 2026 21:01:19 +0300 Subject: [PATCH 08/18] Add textRenderer.VerticalDrawOffset to first visible line counting formula --- TextControlBox/Core/ScrollManager.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/TextControlBox/Core/ScrollManager.cs b/TextControlBox/Core/ScrollManager.cs index dad7bdf..8848009 100644 --- a/TextControlBox/Core/ScrollManager.cs +++ b/TextControlBox/Core/ScrollManager.cs @@ -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(); } @@ -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(); } From 6f649ee706d11390e0ada0fdc8ba83d5af93041a Mon Sep 17 00:00:00 2001 From: Storik4pro Date: Fri, 2 Jan 2026 21:01:49 +0300 Subject: [PATCH 09/18] Add textRenderer.VerticalDrawOffset to line number counting formula --- TextControlBox/Helper/CursorHelper.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TextControlBox/Helper/CursorHelper.cs b/TextControlBox/Helper/CursorHelper.cs index fa66a91..35f8c64 100644 --- a/TextControlBox/Helper/CursorHelper.cs +++ b/TextControlBox/Helper/CursorHelper.cs @@ -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); } From 99b668bba1ad1f639fe2e4446513b8783bb957c3 Mon Sep 17 00:00:00 2001 From: Storik4pro Date: Fri, 2 Jan 2026 21:04:36 +0300 Subject: [PATCH 10/18] Add new VerticalScrollOffset sruct for make control VerticalScrollOffset management easier --- .../Models/Structs/VerticalScrollOffset.cs | 151 ++++++++++++++++++ 1 file changed, 151 insertions(+) create mode 100644 TextControlBox/Models/Structs/VerticalScrollOffset.cs diff --git a/TextControlBox/Models/Structs/VerticalScrollOffset.cs b/TextControlBox/Models/Structs/VerticalScrollOffset.cs new file mode 100644 index 0000000..71bf89a --- /dev/null +++ b/TextControlBox/Models/Structs/VerticalScrollOffset.cs @@ -0,0 +1,151 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Windows.Foundation; + +namespace TextControlBoxNS.Models.Structs +{ + /// + /// Describes the offset between content and vertical scroll area borders. + /// Two Double values describe the Top and Bottom offsets, respectively. + /// + public struct VerticalScrollOffset + { + private double _Top; + + private double _Bottom; + + /// + /// The top offset of content + /// + public double Top + { + get + { + return _Top; + } + set + { + _Top = value; + } + } + + /// + /// The bottom offset of content + /// + + public double Bottom + { + get + { + return _Bottom; + } + set + { + _Bottom = value; + } + } + + /// + /// Creates new VerticalScrollOffset object + /// + /// The top and bottom content offset + public VerticalScrollOffset(double uniformLength) + { + _Top = (_Bottom = uniformLength); + } + + /// + /// Creates new VerticalScrollOffset object + /// + /// The top offset of content + /// The bottom offset of content + public VerticalScrollOffset(double top, double bottom) + { + _Top = top; + _Bottom = bottom; + } + + + /// + public override string ToString() + { + return ToString(CultureInfo.InvariantCulture); + } + + internal string ToString(CultureInfo cultureInfo) + { + char numericListSeparator = TokenizerHelper.GetNumericListSeparator(cultureInfo); + StringBuilder stringBuilder = new StringBuilder(64); + stringBuilder.Append(InternalToString(_Top, cultureInfo)); + stringBuilder.Append(numericListSeparator); + stringBuilder.Append(InternalToString(_Bottom, cultureInfo)); + return stringBuilder.ToString(); + } + + internal string InternalToString(double l, CultureInfo cultureInfo) + { + if (double.IsNaN(l)) + { + return "Unset"; + } + + return Convert.ToString(l, cultureInfo); + } + + /// + public override bool Equals(object obj) + { + if (obj is VerticalScrollOffset verticalScrollOffset) + { + return this == verticalScrollOffset; + } + + return false; + } + /// + /// Indicates whether this instance and a specified object are equal. + /// + /// true if verticalScrollOffset and this instance are represent the same value; otherwise, false. + public readonly bool Equals(VerticalScrollOffset verticalScrollOffset) + { + return this == verticalScrollOffset; + } + + /// + public override int GetHashCode() + { + return _Top.GetHashCode() ^ _Bottom.GetHashCode(); + } + + /// + /// Compares two VerticalScrollOffset objects + /// + /// + /// + /// true if offsets are same; otherwise, false. + public static bool operator ==(VerticalScrollOffset t1, VerticalScrollOffset t2) + { + if (t1._Top == t2._Top) + { + return t1._Bottom == t2._Bottom; + } + + return false; + } + + /// + /// Compares two VerticalScrollOffset objects + /// + /// + /// + /// true if offsets are different; otherwise, false. + public static bool operator !=(VerticalScrollOffset t1, VerticalScrollOffset t2) + { + return !(t1 == t2); + } + } +} From c9ea4d376f7f18b1e9409cd5ea8d653b5bede7cf Mon Sep 17 00:00:00 2001 From: Storik4pro Date: Fri, 2 Jan 2026 21:04:58 +0300 Subject: [PATCH 11/18] Add ContentVerticalScrollOffset property to control --- TextControlBox/TextControlBox.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/TextControlBox/TextControlBox.cs b/TextControlBox/TextControlBox.cs index 6f75e64..37e2ab0 100644 --- a/TextControlBox/TextControlBox.cs +++ b/TextControlBox/TextControlBox.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using TextControlBoxNS.Core; using TextControlBoxNS.Models; +using TextControlBoxNS.Models.Structs; using Windows.Foundation; namespace TextControlBoxNS; @@ -664,6 +665,15 @@ public bool AddLines(int start, string[] text) return coreTextBox.AddLines(start, text); } + /// + /// Gets or sets content scroll offset + /// + public VerticalScrollOffset ContentVerticalScrollOffset + { + get => coreTextBox.ContentVerticalScrollOffset; + set => coreTextBox.ContentVerticalScrollOffset = value; + } + /// /// 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 From c4de05936f559c642c082182fd4b1b7d8b12b1ab Mon Sep 17 00:00:00 2001 From: Storik4pro Date: Fri, 2 Jan 2026 21:05:30 +0300 Subject: [PATCH 12/18] Add test for vertical scroll offset feature --- TextControlBox-TestApp/MainWindow.xaml.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/TextControlBox-TestApp/MainWindow.xaml.cs b/TextControlBox-TestApp/MainWindow.xaml.cs index f58a602..714de55 100644 --- a/TextControlBox-TestApp/MainWindow.xaml.cs +++ b/TextControlBox-TestApp/MainWindow.xaml.cs @@ -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; From b6ca3a2ab4adc5eecd20c23aad32a129ecfbef83 Mon Sep 17 00:00:00 2001 From: Storik4pro Date: Fri, 2 Jan 2026 22:54:20 +0300 Subject: [PATCH 13/18] Refactor code; Fix verticalScrollBar maximum; Fix start line calculating; Make TopScrollOffset and BottomScrollOffset field public for get Code refactor: - Move offset calculating logic to local function --- TextControlBox/Core/Renderer/TextRenderer.cs | 26 +++++++++++--------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/TextControlBox/Core/Renderer/TextRenderer.cs b/TextControlBox/Core/Renderer/TextRenderer.cs index 3ae23c1..1ec3813 100644 --- a/TextControlBox/Core/Renderer/TextRenderer.cs +++ b/TextControlBox/Core/Renderer/TextRenderer.cs @@ -30,8 +30,8 @@ internal class TextRenderer public string OldRenderedText = null; public float VerticalDrawOffset { get; private set; } = 0; - private double TopScrollOffset = 0; - private double BottomScrollOffset = 0; + public double TopScrollOffset { get; private set; } = 0; + public double BottomScrollOffset { get; private set; } = 0; private CursorManager cursorManager; private TextManager textManager; @@ -112,14 +112,14 @@ 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 + BottomScrollOffset) / 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 - VerticalDrawOffset) * scrollManager.DefaultVerticalScrollSensitivity) / singleLineHeight); + int startLine = (int)(((scrollManager.VerticalScroll - VerticalDrawOffset) * scrollManager.DefaultVerticalScrollSensitivity - TopScrollOffset) / singleLineHeight); startLine = Math.Min(startLine, textManager.LinesCount); if (startLine < 0) startLine = 0; @@ -129,14 +129,9 @@ public void UpdateCurrentLineTextLayout(CanvasControl canvasText) return (startLine, linesToRender); } - - public void Draw(CanvasControl canvasText, CanvasDrawEventArgs args) + public float CalculateDrawOffset() { double verticalScroll = scrollManager.VerticalScroll; - if (scrollManager.VerticalScroll < 6 && scrollManager.VerticalScroll != 0) - { - verticalScroll = 0; - } double scrollCoeff = scrollManager.verticalScrollBar.Maximum / scrollManager.VerticalScroll; @@ -152,8 +147,13 @@ public void Draw(CanvasControl canvasText, CanvasDrawEventArgs args) drawOffset = SingleLineHeight; } } + return drawOffset - SingleLineHeight; + } + - VerticalDrawOffset = drawOffset - SingleLineHeight; + public void Draw(CanvasControl canvasText, CanvasDrawEventArgs args) + { + VerticalDrawOffset = CalculateDrawOffset(); //Create resources and layouts: if (NeedsTextFormatUpdate || TextFormat == null || lineNumberRenderer.LineNumberTextFormat == null) @@ -227,5 +227,7 @@ public void Draw(CanvasControl canvasText, CanvasDrawEventArgs args) { canvasUpdateManager.UpdateLineNumbers(); } + canvasUpdateManager.UpdateSelection(); // Possible bad for performanse + canvasUpdateManager.UpdateCursor(); // Possible bad for performanse } } From f058cdd5b72228ab1057ec94f47eb46eceead46d Mon Sep 17 00:00:00 2001 From: Storik4pro Date: Fri, 2 Jan 2026 22:55:39 +0300 Subject: [PATCH 14/18] Add basic work with offset for ScrollToShowCursor function --- TextControlBox/Core/ScrollManager.cs | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/TextControlBox/Core/ScrollManager.cs b/TextControlBox/Core/ScrollManager.cs index 8848009..bd9e033 100644 --- a/TextControlBox/Core/ScrollManager.cs +++ b/TextControlBox/Core/ScrollManager.cs @@ -133,10 +133,29 @@ 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; + + 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; + } else if (textRenderer.NumberOfStartLine > cursorManager.LineNumber) - verticalScrollBar.Value = (cursorManager.LineNumber - 1) * textRenderer.SingleLineHeight / DefaultVerticalScrollSensitivity; + { + verticalScrollBar.Value = ((cursorManager.LineNumber - 1) * textRenderer.SingleLineHeight + globalOffset) / DefaultVerticalScrollSensitivity; + } + 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(); From 07e4b0636a2a48aee438e6091b6f1191f70ee07e Mon Sep 17 00:00:00 2001 From: Storik4pro Date: Fri, 2 Jan 2026 23:00:59 +0300 Subject: [PATCH 15/18] Fix scrolling bug Fixed bug: - When cursor located out of viewport local offset not applied to vertical scroll value --- TextControlBox/Core/ScrollManager.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/TextControlBox/Core/ScrollManager.cs b/TextControlBox/Core/ScrollManager.cs index bd9e033..aaa8a99 100644 --- a/TextControlBox/Core/ScrollManager.cs +++ b/TextControlBox/Core/ScrollManager.cs @@ -134,6 +134,7 @@ public void ScrollPageDown() public void UpdateScrollToShowCursor(bool update = true) { double globalOffset = textRenderer.VerticalDrawOffset == 0 ? textRenderer.TopScrollOffset : 0; + double localOffset = textRenderer.VerticalDrawOffset; if (cursorManager.LineNumber == 0) { @@ -141,11 +142,11 @@ public void UpdateScrollToShowCursor(bool update = true) } else if (textRenderer.NumberOfStartLine + textRenderer.NumberOfRenderedLines <= cursorManager.LineNumber) { - verticalScrollBar.Value = ((cursorManager.LineNumber - textRenderer.NumberOfRenderedLines + 1) * textRenderer.SingleLineHeight + globalOffset) / DefaultVerticalScrollSensitivity; + 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 + globalOffset) / DefaultVerticalScrollSensitivity; + verticalScrollBar.Value = ((cursorManager.LineNumber - 1) * textRenderer.SingleLineHeight + globalOffset) / DefaultVerticalScrollSensitivity + localOffset; } else if (textRenderer.VerticalDrawOffset != 0) { From d80287ae60ed7f54758de623f93eed85c3e9b8b3 Mon Sep 17 00:00:00 2001 From: Julius Kirsch Date: Sat, 3 Jan 2026 11:35:55 +0100 Subject: [PATCH 16/18] fix: cursor hidden with no scrollable lines and an offset defined --- TextControlBox/Core/Renderer/CursorRenderer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TextControlBox/Core/Renderer/CursorRenderer.cs b/TextControlBox/Core/Renderer/CursorRenderer.cs index 9bc9df9..b874798 100644 --- a/TextControlBox/Core/Renderer/CursorRenderer.cs +++ b/TextControlBox/Core/Renderer/CursorRenderer.cs @@ -83,7 +83,7 @@ public void Draw(CanvasControl canvasText, CanvasControl canvasCursor, CanvasDra 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); From 0a25e6a4bb0d0b908f967ac98b477e848cb19ccd Mon Sep 17 00:00:00 2001 From: Julius Kirsch Date: Sat, 3 Jan 2026 11:42:08 +0100 Subject: [PATCH 17/18] ref: proper invalidation of canvas updates --- TextControlBox/Core/CoreTextControlBox.xaml.cs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/TextControlBox/Core/CoreTextControlBox.xaml.cs b/TextControlBox/Core/CoreTextControlBox.xaml.cs index ffbf69c..183b487 100644 --- a/TextControlBox/Core/CoreTextControlBox.xaml.cs +++ b/TextControlBox/Core/CoreTextControlBox.xaml.cs @@ -994,12 +994,8 @@ public VerticalScrollOffset ContentVerticalScrollOffset set { _ContentVerticalScrollOffset = value; - textRenderer.UpdateScrollOffset(value); - Canvas_Text.UpdateLayout(); - Canvas_Selection.UpdateLayout(); - Canvas_Cursor.UpdateLayout(); - Canvas_LineNumber.UpdateLayout(); + canvasUpdateManager.UpdateAll(); } } From 0b70ee236c3d5f783c379b1b394f8903b8456a86 Mon Sep 17 00:00:00 2001 From: Julius Kirsch Date: Sat, 3 Jan 2026 11:43:45 +0100 Subject: [PATCH 18/18] ref: simplified verticalScrollOffset struct a bit --- .../Models/Structs/VerticalScrollOffset.cs | 206 +++++++----------- 1 file changed, 77 insertions(+), 129 deletions(-) diff --git a/TextControlBox/Models/Structs/VerticalScrollOffset.cs b/TextControlBox/Models/Structs/VerticalScrollOffset.cs index 71bf89a..e6359a5 100644 --- a/TextControlBox/Models/Structs/VerticalScrollOffset.cs +++ b/TextControlBox/Models/Structs/VerticalScrollOffset.cs @@ -1,151 +1,99 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Windows.Foundation; +namespace TextControlBoxNS.Models.Structs; -namespace TextControlBoxNS.Models.Structs +/// +/// Describes the offset between content and vertical scroll area borders. +/// Two Double values describe the Top and Bottom offsets, respectively. +/// +public struct VerticalScrollOffset { /// - /// Describes the offset between content and vertical scroll area borders. - /// Two Double values describe the Top and Bottom offsets, respectively. + /// The top offset of content /// - public struct VerticalScrollOffset - { - private double _Top; - - private double _Bottom; + public double Top { get; set; } - /// - /// The top offset of content - /// - public double Top - { - get - { - return _Top; - } - set - { - _Top = value; - } - } - - /// - /// The bottom offset of content - /// - - public double Bottom - { - get - { - return _Bottom; - } - set - { - _Bottom = value; - } - } + /// + /// The bottom offset of content + /// - /// - /// Creates new VerticalScrollOffset object - /// - /// The top and bottom content offset - public VerticalScrollOffset(double uniformLength) - { - _Top = (_Bottom = uniformLength); - } + public double Bottom { get; set; } - /// - /// Creates new VerticalScrollOffset object - /// - /// The top offset of content - /// The bottom offset of content - public VerticalScrollOffset(double top, double bottom) - { - _Top = top; - _Bottom = bottom; - } + /// + /// Creates new VerticalScrollOffset object + /// + /// The top and bottom content offset + public VerticalScrollOffset(double uniformLength) + { + Top = (Bottom = uniformLength); + } + /// + /// Creates new VerticalScrollOffset object + /// + /// The top offset of content + /// The bottom offset of content + public VerticalScrollOffset(double top, double bottom) + { + this.Top = top; + this.Bottom = bottom; + } - /// - public override string ToString() - { - return ToString(CultureInfo.InvariantCulture); - } - internal string ToString(CultureInfo cultureInfo) - { - char numericListSeparator = TokenizerHelper.GetNumericListSeparator(cultureInfo); - StringBuilder stringBuilder = new StringBuilder(64); - stringBuilder.Append(InternalToString(_Top, cultureInfo)); - stringBuilder.Append(numericListSeparator); - stringBuilder.Append(InternalToString(_Bottom, cultureInfo)); - return stringBuilder.ToString(); - } + /// + public override string ToString() + { + return $"{Top}, {Bottom}"; + } - internal string InternalToString(double l, CultureInfo cultureInfo) + /// + public override bool Equals(object obj) + { + if (obj is VerticalScrollOffset verticalScrollOffset) { - if (double.IsNaN(l)) - { - return "Unset"; - } - - return Convert.ToString(l, cultureInfo); + return this == verticalScrollOffset; } - /// - public override bool Equals(object obj) - { - if (obj is VerticalScrollOffset verticalScrollOffset) - { - return this == verticalScrollOffset; - } + return false; + } + + /// + /// Indicates whether this instance and a specified object are equal. + /// + /// true if verticalScrollOffset and this instance are represent the same value; otherwise, false. + public readonly bool Equals(VerticalScrollOffset verticalScrollOffset) + { + return this == verticalScrollOffset; + } - return false; - } - /// - /// Indicates whether this instance and a specified object are equal. - /// - /// true if verticalScrollOffset and this instance are represent the same value; otherwise, false. - public readonly bool Equals(VerticalScrollOffset verticalScrollOffset) - { - return this == verticalScrollOffset; - } + /// + public override int GetHashCode() + { + return Top.GetHashCode() ^ Bottom.GetHashCode(); + } - /// - public override int GetHashCode() + /// + /// Compares two VerticalScrollOffset objects + /// + /// + /// + /// true if offsets are same; otherwise, false. + public static bool operator ==(VerticalScrollOffset t1, VerticalScrollOffset t2) + { + if (t1.Top == t2.Top) { - return _Top.GetHashCode() ^ _Bottom.GetHashCode(); + return t1.Bottom == t2.Bottom; } - /// - /// Compares two VerticalScrollOffset objects - /// - /// - /// - /// true if offsets are same; otherwise, false. - public static bool operator ==(VerticalScrollOffset t1, VerticalScrollOffset t2) - { - if (t1._Top == t2._Top) - { - return t1._Bottom == t2._Bottom; - } - - return false; - } + return false; + } - /// - /// Compares two VerticalScrollOffset objects - /// - /// - /// - /// true if offsets are different; otherwise, false. - public static bool operator !=(VerticalScrollOffset t1, VerticalScrollOffset t2) - { - return !(t1 == t2); - } + /// + /// Compares two VerticalScrollOffset objects + /// + /// + /// + /// true if offsets are different; otherwise, false. + public static bool operator !=(VerticalScrollOffset t1, VerticalScrollOffset t2) + { + return !(t1 == t2); } }