Chapter Scrolling
An issue that came up when designing the flow of the writing space is the scrolling of chapters. The goal is to combine a chapter title and a massive body of text into a single scrolling surface, while strictly preserving AvaloniaEdit's virtualization to avoid typing latency (virtualization is what allows the renderer to only draw what is on screen instead of the entire text).
Here are the specificities of how the writing space must work:
- It comprises two elements: a title and a text area.
- They are logically separate (selecting text in the body doesn't bleed into the title, and vice versa).
- Scrolling down the chapter moves the title up seamlessly (zero visual jitter).
- The title must retain its state (caret position, highlight) even when scrolled off-screen.
- The vertical scrollbar must sit at the far right edge of the window, but the text itself needs to be constrained and centered to a fully user-configurable width (calculated on a character-count basis).
This combination of constraints proved surprisingly hostile to standard UI paradigms. The title is a TextBox, whereas the text area is an AvaloniaEdit.TextEditor. Because the TextEditor handles its own highly-optimized logical scrolling, merging them into one scrollable view without breaking performance required a strict architectural approach.
The Traps and Failed Approaches
I explored several standard approaches, but each broke a fundamental constraint:
- Inline Title (VisualLineElementGenerator): Injecting the title directly into the text flow.
- Why it fails: It violates the logical separation of controls. Worse, because AvaloniaEdit aggressively virtualizes, when the title scrolls off-screen, the control is completely destroyed to save memory. When you scroll back up, a new
TextBoxis created, completely destroying the user's caret position and text selection.
- Why it fails: It violates the logical separation of controls. Worse, because AvaloniaEdit aggressively virtualizes, when the title scrolls off-screen, the control is completely destroyed to save memory. When you scroll back up, a new
- UI-Thread Sync Scrolling (RenderTransform): Overlaying the title and binding its Y-translation to the text editor's scroll offset.
- Why it fails: Data bindings run on the UI thread, while Avalonia handles scrolling on a highly optimized composition thread. This causes a 1-frame desync during fast momentum scrolling, resulting in unacceptable visual jitter.
- The
DockPanelWrapper Trap: Wrapping both the Title and theTextEditorin aDockPanelinside a standardScrollViewer.- Why it fails: This appears to work on small documents, but it silently destroys virtualization. A
ScrollVieweruses "logical" scrolling only if its direct child implementsILogicalScrollable. BecauseDockPaneldoes not, theScrollViewerfalls back to physical scrolling and grants theDockPanelan infinite vertical measure constraint. TheTextAreareceives this infinite height and renders every single line of the document into memory at once, causing massive memory spikes and freezing on large chapters.
- Why it fails: This appears to work on small documents, but it silently destroys virtualization. A
The Architecture: A Logical Proxy Panel
To satisfy all constraints without compromise, I have to bypass standard layout containers and intercept the framework's scrolling logic directly. The solution is building a custom DocumentEditorPanel (inheriting from Panel) that acts as a proxy.
This panel is placed as the direct child of the ScrollViewer. Crucially, it implements ILogicalScrollable.
Instead of inventing a virtualization engine from scratch, this proxy panel acts as a middleman. It tells the ScrollViewer "I know how to scroll logically!", but under the hood, it simply forwards all the complex math and hardware commands (like LineUp(), PageDown(), and inertia) directly to the internal AvaloniaEdit TextArea.
The synchronization happens entirely within the layout passes:
- MeasureOverride: The panel measures the
TextAreastrictly with the screen'sViewportsize (never infinity). This mathematically guarantees that AvaloniaEdit's virtualization remains 100% intact. - ArrangeOverride: When the user scrolls, the panel intercepts the offset. It uses it to physically translate the Title upwards. Once the title is pushed entirely out of view, the panel passes the remainder of the scroll offset logically to the
TextArea.
Because both the physical translation of the title and the logical offset of the text happen simultaneously in the exact same layout pass, there is absolutely zero jitter. Because the title remains safely in the visual tree, its UI state (focus, text selection) is permanently preserved.
Decoupling the Scrollbar
The final constraint is topological: placing the scrollbar at the edge of the application window while keeping the text centered.
Standard ScrollViewer behavior binds the scrollbar tightly to the content's edge. To break this, we redefine the ScrollViewer's ControlTemplate specifically for the writing space.
We deploy an asymmetric 4-column Grid:
- Column 0 (
*): Dynamic left margin. - Column 1 (Bound Width): The constrained workspace holding the
PART_ScrollContentPresenter(which houses our customDocumentEditorPanel). - Column 2 (
*): Dynamic right margin. - Column 3 (
Auto): ThePART_VerticalScrollBar, anchored to the far right.
This XAML topology completely decouples the visual scrollbar from the text boundaries. Because it's done at the template level, it preserves all native internal event routing (touch inertia, scroll wheel deltas, etc.).
By combining the custom ILogicalScrollable proxy panel with the asymmetric scrollbar template, we achieve a frictionless, fully virtualized writing environment that behaves exactly as designed, capable of handling hundreds of thousands of words without typing latency.