Wrap aware SCI_SCROLLVERTICAL

Neil <[email protected]> Tue, 27 May 2025 19:50:05 -0700 (PDT)
Newsgroups gmane.comp.lib.scintilla.devel
Message-ID <[email protected]>
Text wrapping in Scintilla is a background activity that can take a long 
time to fully wrap a document. This may take seconds or even minutes for a 
really huge document or slow computer. It also means that applications 
manipulating the editor may encounter partially wrapped situations where 
calls do not have their intended behaviour.

An example is an application setting the scroll position to match the 
previous time that document was showing. Wrapping may move that scroll 
position out of view. There are several issues on the trackers referencing 
this problem such as this issue.

https://sourceforge.net/p/scintilla/bugs/1395/

A possible solution is to defer scrolling vertically until after wrapping 
has completed or, at least passed the desired scroll position so that the 
view is stable. This is the purpose of a proposed API 
SCI_SCROLLVERTICAL(docLine, subLine) which bases scroll positioning on a 
combination of a document line and a sub-line within that document line. 
This is more stable than using a visual line as it better handles changes 
in view width.

This API will first synchronously set the scroll position based on no 
wrapping information which is correct when wrap is off and an OK first 
approximation when wrapping is on. It remembers the desired (docLine, 
subLine) scroll position and uses that inside the wrapping code to maintain 
a stable view. The desired scroll position is forgotten once the document 
has been fully wrapped or the user manually scrolls.

If the user's manual scrolling did not forget the desired position then the 
effect is to scroll back to the desired position repeatedly which is 
frustrating. A problem then is to work out when a scroll is user-initiated. 
This is now done by treating every SetVerticalScrollPos call as user 
initiated except for those performed inside WrapLines. Earlier attempts 
tried to intercept the different ways a user can cause a scroll but the 
number of places grew and more would have to be added for each platform. 
This does require platform layers to call the superclass 
Editor::SetVerticalScrollPos() inside their own SetVerticalScrollPos 
implementation. There is a good chance that the user-initiated scrolling 
detection will need refinement.

Another approach would be to store the byte index (position) of the first 
character in the view as the scrolling anchor. That may ensure scroll 
restoration shows more of the initial text when the window changed width 
since saving but it relies on more expensive calls. 

From the application side, SciTE, for example has code like this when 
saving the scroll position.

ScrollDocWithOffset SciTEBase::GetCurrentScrollPosition() {
const SA::Line lineDisplayTop = wEditor.FirstVisibleLine();
const SA::Line lineDocTop = wEditor.DocLineFromVisible(lineDisplayTop);
const SA::Line subLineTop = lineDisplayTop - 
wEditor.VisibleFromDocLine(lineDocTop);
return { lineDocTop, subLineTop };
}

Here is SciTE's code when restoring a scroll position.

wEditor.ScrollVertical(fp.scrollPosition.lineDoc, 
fp.scrollPosition.subLine);

A patch implementing SCI_SCROLLVERTICAL is attached.

Applications will have to choose to use this new API for it to have any 
effect and platform layers will also need to add the one call mentioned 
above.

Neil

-- 
You received this message because you are subscribed to the Google Groups "scintilla-interest" group.
To unsubscribe from this group and stop receiving emails from it, send an email to [email protected].
To view this discussion visit https://groups.google.com/d/msgid/scintilla-interest/3c23667f-f426-4e75-9af9-5898f0b9872fn%40googlegroups.com.
ScrolVerticalEditor.patch (text/plain, 7.7 KB)
diff -r 4efb64d60440 call/ScintillaCall.cxx
--- a/call/ScintillaCall.cxx	Sun May 25 10:14:33 2025 +1000
+++ b/call/ScintillaCall.cxx	Tue May 27 10:36:17 2025 +1000
@@ -1375,6 +1375,10 @@
 	Call(Message::LineScroll, columns, lines);
 }
 
+void ScintillaCall::ScrollVertical(Line docLine, Line subLine) {
+	Call(Message::ScrollVertical, docLine, subLine);
+}
+
 void ScintillaCall::ScrollCaret() {
 	Call(Message::ScrollCaret);
 }
diff -r 4efb64d60440 cocoa/ScintillaCocoa.mm
--- a/cocoa/ScintillaCocoa.mm	Sun May 25 10:14:33 2025 +1000
+++ b/cocoa/ScintillaCocoa.mm	Tue May 27 10:36:17 2025 +1000
@@ -1951,6 +1951,7 @@
  * Modifies the vertical scroll position to make the current top line show up as such.
  */
 void ScintillaCocoa::SetVerticalScrollPos() {
+	Editor::SetVerticalScrollPos();
 	NSScrollView *scrollView = ScrollContainer();
 	if (scrollView) {
 		NSClipView *clipView = scrollView.contentView;
diff -r 4efb64d60440 gtk/ScintillaGTK.cxx
--- a/gtk/ScintillaGTK.cxx	Sun May 25 10:14:33 2025 +1000
+++ b/gtk/ScintillaGTK.cxx	Tue May 27 10:36:17 2025 +1000
@@ -1102,6 +1102,7 @@
 }
 
 void ScintillaGTK::SetVerticalScrollPos() {
+	Editor::SetVerticalScrollPos();
 	DwellEnd(true);
 	gtk_adjustment_set_value(GTK_ADJUSTMENT(adjustmentv), static_cast<gdouble>(topLine));
 }
diff -r 4efb64d60440 include/Scintilla.h
--- a/include/Scintilla.h	Sun May 25 10:14:33 2025 +1000
+++ b/include/Scintilla.h	Tue May 27 10:36:17 2025 +1000
@@ -561,6 +561,7 @@
 #define SCI_LINEFROMPOSITION 2166
 #define SCI_POSITIONFROMLINE 2167
 #define SCI_LINESCROLL 2168
+#define SCI_SCROLLVERTICAL 2817
 #define SCI_SCROLLCARET 2169
 #define SCI_SCROLLRANGE 2569
 #define SCI_REPLACESEL 2170
diff -r 4efb64d60440 include/Scintilla.iface
--- a/include/Scintilla.iface	Sun May 25 10:14:33 2025 +1000
+++ b/include/Scintilla.iface	Tue May 27 10:36:17 2025 +1000
@@ -1422,6 +1422,9 @@
 # Scroll horizontally and vertically.
 fun void LineScroll=2168(position columns, line lines)
 
+# Scroll vertically with allowance for wrapping.
+fun void ScrollVertical=2817(line docLine, line subLine)
+
 # Ensure the caret is visible.
 fun void ScrollCaret=2169(,)
 
diff -r 4efb64d60440 include/ScintillaCall.h
--- a/include/ScintillaCall.h	Sun May 25 10:14:33 2025 +1000
+++ b/include/ScintillaCall.h	Tue May 27 10:36:17 2025 +1000
@@ -389,6 +389,7 @@
 	Line LineFromPosition(Position pos);
 	Position PositionFromLine(Line line);
 	void LineScroll(Position columns, Line lines);
+	void ScrollVertical(Line docLine, Line subLine);
 	void ScrollCaret();
 	void ScrollRange(Position secondary, Position primary);
 	void ReplaceSel(const char *text);
diff -r 4efb64d60440 include/ScintillaMessages.h
--- a/include/ScintillaMessages.h	Sun May 25 10:14:33 2025 +1000
+++ b/include/ScintillaMessages.h	Tue May 27 10:36:17 2025 +1000
@@ -311,6 +311,7 @@
 	LineFromPosition = 2166,
 	PositionFromLine = 2167,
 	LineScroll = 2168,
+	ScrollVertical = 2817,
 	ScrollCaret = 2169,
 	ScrollRange = 2569,
 	ReplaceSel = 2170,
diff -r 4efb64d60440 qt/ScintillaEditBase/ScintillaQt.cpp
--- a/qt/ScintillaEditBase/ScintillaQt.cpp	Sun May 25 10:14:33 2025 +1000
+++ b/qt/ScintillaEditBase/ScintillaQt.cpp	Tue May 27 10:36:17 2025 +1000
@@ -280,6 +280,7 @@
 
 void ScintillaQt::SetVerticalScrollPos()
 {
+	Editor::SetVerticalScrollPos();
 	scrollArea->verticalScrollBar()->setValue(topLine);
 	emit verticalScrolled(topLine);
 }
diff -r 4efb64d60440 src/Editor.cxx
--- a/src/Editor.cxx	Sun May 25 10:14:33 2025 +1000
+++ b/src/Editor.cxx	Tue May 27 10:36:17 2025 +1000
@@ -202,6 +202,8 @@
 	recordingMacro = false;
 	foldAutomatic = AutomaticFold::None;
 
+	insideWrapScroll = false;
+
 	convertPastes = true;
 
 	SetRepresentations();
@@ -992,6 +994,11 @@
 	}
 }
 
+Sci::Line Editor::DisplayLineFromDocSub(LineDocSub line) const noexcept {
+	return pcs->DisplayFromDoc(line.lineDoc) + std::min(
+		line.subLine, static_cast<Sci::Line>(pcs->GetHeight(line.lineDoc) - 1));
+}
+
 void Editor::ScrollText(Sci::Line /* linesToMove */) {
 	//Platform::DebugPrintf("Editor::ScrollText %d\n", linesToMove);
 	Redraw();
@@ -1679,8 +1686,15 @@
 		// Decide where to start wrapping
 		Sci::Line lineToWrap = wrapPending.start;
 		Sci::Line lineToWrapEnd = std::min(wrapPending.end, pdoc->LinesTotal());
+
 		const Sci::Line lineDocTop = pcs->DocFromDisplay(topLine);
-		const Sci::Line subLineTop = topLine - pcs->DisplayFromDoc(lineDocTop);
+		LineDocSub lineScrollTo;
+		if (scrollToAfterWrap) {
+			lineScrollTo = scrollToAfterWrap.value();
+		} else {
+			const Sci::Line subLineTop = topLine - pcs->DisplayFromDoc(lineDocTop);
+			lineScrollTo = { lineDocTop, subLineTop };
+		}
 		if (ws == WrapScope::wsVisible) {
 			lineToWrap = std::clamp(lineDocTop-5, wrapPending.start, pdoc->LinesTotal());
 			// Priority wrap to just after visible area.
@@ -1731,21 +1745,23 @@
 
 				wrapOccurred = WrapBlock(surface, lineToWrap, lineToWrapEnd);
 
-				goodTopLine = pcs->DisplayFromDoc(lineDocTop) + std::min(
-					subLineTop, static_cast<Sci::Line>(pcs->GetHeight(lineDocTop)-1));
+				goodTopLine = DisplayLineFromDocSub(lineScrollTo);
 			}
 		}
 
 		// If wrapping is done, bring it to resting position
 		if (wrapPending.start >= lineEndNeedWrap) {
 			wrapPending.Reset();
+			scrollToAfterWrap.reset();
 		}
 	}
 
 	if (wrapOccurred) {
+		insideWrapScroll = true;
 		SetScrollBars();
 		SetTopLine(std::clamp<Sci::Line>(goodTopLine, 0, MaxScrollPos()));
 		SetVerticalScrollPos();
+		insideWrapScroll = false;
 	}
 
 	return wrapOccurred;
@@ -1990,6 +2006,12 @@
 	return 1;
 }
 
+void Editor::SetVerticalScrollPos() {
+	if (!insideWrapScroll) {
+		scrollToAfterWrap.reset();
+	}
+}
+
 // Empty method is overridden on GTK+ to show / hide scrollbars
 void Editor::ReconfigureScrollBars() {}
 
@@ -5545,6 +5567,8 @@
 
 	SetRepresentations();
 
+	scrollToAfterWrap.reset();
+
 	// Reset the contraction state to fully shown.
 	pcs->Clear();
 	pcs->InsertLines(0, pdoc->LinesTotal() - 1);
@@ -6554,6 +6578,15 @@
 		HorizontalScrollTo(xOffset + static_cast<int>(static_cast<int>(wParam) * vs.spaceWidth));
 		return 1;
 
+	case Message::ScrollVertical:
+		if (Wrapping()) {
+			scrollToAfterWrap.emplace(wParam, lParam);
+		} else {
+			scrollToAfterWrap.reset();
+		}
+		ScrollTo(DisplayLineFromDocSub({static_cast<Sci::Line>(wParam), lParam}));
+		break;
+
 	case Message::SetXOffset:
 		xOffset = static_cast<int>(wParam);
 		ContainerNeedsUpdate(Update::HScroll);
diff -r 4efb64d60440 src/Editor.h
--- a/src/Editor.h	Sun May 25 10:14:33 2025 +1000
+++ b/src/Editor.h	Tue May 27 10:36:17 2025 +1000
@@ -283,6 +283,12 @@
 	// Wrapping support
 	WrapPending wrapPending;
 	ActionDuration durationWrapOneByte;
+	bool insideWrapScroll;
+	struct LineDocSub {
+		Scintilla::Line lineDoc = 0;
+		Scintilla::Line subLine = 0;
+	};
+	std::optional<LineDocSub> scrollToAfterWrap;
 
 	bool convertPastes;
 
@@ -373,6 +379,7 @@
 	void RememberCurrentSelectionForRedoOntoStack();
 
 	void ScrollTo(Sci::Line line, bool moveThumb=true);
+	Sci::Line DisplayLineFromDocSub(LineDocSub line) const noexcept;
 	virtual void ScrollText(Sci::Line linesToMove);
 	void HorizontalScrollTo(int xPos);
 	void VerticalCentreCaret();
@@ -417,7 +424,7 @@
 	Sci::Position FormatRange(Scintilla::Message iMessage, Scintilla::uptr_t wParam, Scintilla::sptr_t lParam);
 	long TextWidth(Scintilla::uptr_t style, const char *text);
 
-	virtual void SetVerticalScrollPos() = 0;
+	virtual void SetVerticalScrollPos();
 	virtual void SetHorizontalScrollPos() = 0;
 	virtual bool ModifyScrollBars(Sci::Line nMax, Sci::Line nPage) = 0;
 	virtual void ReconfigureScrollBars();
diff -r 4efb64d60440 win32/ScintillaWin.cxx
--- a/win32/ScintillaWin.cxx	Sun May 25 10:14:33 2025 +1000
+++ b/win32/ScintillaWin.cxx	Tue May 27 10:36:17 2025 +1000
@@ -2700,6 +2700,7 @@
 }
 
 void ScintillaWin::SetVerticalScrollPos() {
+	Editor::SetVerticalScrollPos();
 	ChangeScrollPos(SB_VERT, topLine);
 }