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; 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 diff --git a/TextControlBox/Core/CoreTextControlBox.xaml.cs b/TextControlBox/Core/CoreTextControlBox.xaml.cs index 2a39099..183b487 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,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 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; } diff --git a/TextControlBox/Core/Renderer/CursorRenderer.cs b/TextControlBox/Core/Renderer/CursorRenderer.cs index 8934928..b874798 100644 --- a/TextControlBox/Core/Renderer/CursorRenderer.cs +++ b/TextControlBox/Core/Renderer/CursorRenderer.cs @@ -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); 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() 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, diff --git a/TextControlBox/Core/Renderer/TextRenderer.cs b/TextControlBox/Core/Renderer/TextRenderer.cs index 01246b0..1ec3813 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; + + public double TopScrollOffset { get; private set; } = 0; + public double BottomScrollOffset { get; private set; } = 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,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) { @@ -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); @@ -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 } } diff --git a/TextControlBox/Core/ScrollManager.cs b/TextControlBox/Core/ScrollManager.cs index dad7bdf..aaa8a99 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(); } @@ -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(); 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); } diff --git a/TextControlBox/Models/Structs/VerticalScrollOffset.cs b/TextControlBox/Models/Structs/VerticalScrollOffset.cs new file mode 100644 index 0000000..e6359a5 --- /dev/null +++ b/TextControlBox/Models/Structs/VerticalScrollOffset.cs @@ -0,0 +1,99 @@ +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 +{ + /// + /// The top offset of content + /// + public double Top { get; set; } + + /// + /// The bottom offset of content + /// + + public double Bottom { get; set; } + + /// + /// 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 $"{Top}, {Bottom}"; + } + + /// + 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); + } +} 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