Architecture
Layers, each naming only what sits below it. The boundary is held by the project references rather than by intent — ArionUI.Core has no package references at all, and no Avalonia, WPF or Skia type appears in it, in ArionUI.Presentation or in ArionUI.Grid.
your app declares columns, picks a view
ArionUI.Rendering.Avalonia thin hosts: input mapping, the
ArionUI.Rendering.Wpf ArionGrid control, a frame clock
ArionUI.Rendering.WinForms (WinForms: ArionGridControl, not packaged)
ArionUI.Rendering.Skia the backend: frame renderer, glyph
cache, text shaping, SkiaRenderSurface;
shared by ALL hosts
ArionUI.Grid the grid itself: presenter, input,
zones, panes, ArionGridApi.
Draws through IRenderSurface
ArionUI.Presentation framework-neutral: views, columns, the
composition model, theme, style rules
ArionUI.Core no package references: engine, layout,
interaction, data projections
ArionUI.Gpu OpenGL ES 3.0 through a host-supplied
loader; no references at allThese are the open-source packages, Apache-2.0. The Pro packages — ArionUI.Grouping, Pivot, Charts, Charts3D, Timeline, Planning, Formatting, Layout and Export — sit beside this stack, not in it. Each builds only on the public surface of the engine and joins a view with one call; nothing in the engine knows about them beforehand.
What each layer owns
- ArionUI.Gpu
- The lowest layer: OpenGL ES 3.0 through a loader the host supplies, GL state discipline, offscreen targets with a depth buffer, shaders and streaming vertex buffers. It is below the core because the core's
IRenderSurfaceaccepts a GPU layer. - ArionUI.Core
- The engine. Virtualization, the layout solver, viewport maths, scroll diffing, hit testing, scroll physics, selection and interaction, and the data sources. It also declares
IRenderSurfaceandIViewHost— the two contracts everything above it is written against. - ArionUI.Presentation
- What a grid is, without saying how it is drawn: the
ArionViewbase and its implementations —GridView<T>,CardView,TreeView<T>,BoardView—ColumnDefinition<T>, theSlotEl/Cellcomposition model,ArionTheme,StyleRule, and the per-gridGridContext. - ArionUI.Grid
- The grid as a thing you interact with: presenter, input wiring, zones, panes, the self-drawn dialogs, accessibility, the UI test driver and the consumer facade
ArionGridApi. Backend- and host-neutral — it draws exclusively throughIRenderSurfaceand never names a Skia type. - ArionUI.Rendering.Skia
- The single backend: the frame renderer, the glyph cache, the surface implementation, the GPU blit pipeline and text shaping through HarfBuzz. All hosts share it.
- ArionUI.Rendering.Avalonia · .Wpf · .WinForms
- Thin hosts: input mapping, the control, a frame clock and the hand-off of the frame to the platform. The WPF host does the most work to get there — Skia draws through ANGLE on Direct3D 11 into a shared texture that goes into a
D3DImage, on its own render thread. The WinForms host renders on an ANGLE window of its own. The WinForms host builds and runs, but is not a package yet.
Views, not modes
ArionGrid.MainView holds an ArionView. Table, cards, tree and board are implementations of it in the open engine; pivot, chart and timeline are implementations in the Pro packages. A view owns its data projection, its drawing model and its behaviour — including its own header hit testing, taken by coordinate, because a pivot header is two-dimensional and a table header is not.
Written as modes instead, every one of those differences would have become a branch inside the grid, and each new view would have had to be threaded through all of them.
Scroll physics and interaction are core code
The scroll animator — a critically damped spring for scrollbar jumps, a fixed-duration ease-out for wheel notches, snapping to device pixels — is in ArionUI.Core. So is InteractionController: selection, editing, hover, drag and TSV copy, expressed over neutral ArionKey and ArionModifiers values. It consults per-row SlotTraits — selectable, editable, copyable, activatable, full-row span, summary row — rather than testing item types.
That is literally the same code on every host; a host only maps input, supplies a frame tick and a surface. It is what makes the hosts behave alike, and what the repository's parity scripts hold: scripted runs on Avalonia and WPF in five themes with their state lines compared, and the 3D scenes compared pixel by pixel across all three hosts.
Data projections chain, they do not copy
your list
→ ListItemsSource<T> count + indexer, nothing more
→ SortFilterSource an index mapping
→ GroupingSource or PivotSource (Pro packages)Each link is an index mapping in front of the one below, never a copy. Select-all and re-sorting are index work. The renderer knows about none of it: it asks for the item at a visible index and draws it, and GetItem returning null for a stale index is part of the contract rather than a failure — the render thread reads through a window the UI thread may have shrunk in between.
Your own source only has to be a count and an indexer. ObservableItemsSource<T> adds change notification so the view reshapes and repaints on its own. For rows that are not held in memory there are two: VirtualItemsSource fetches pages on demand, and RemoteItemsSource also sorts and filters on the server. RingItemsSource is a bounded, append-only source for event and log views, where the oldest rows fall off the front.
Two render paths behind one surface
The default path draws on the GPU through the host's Skia context. Where no GPU lease is available, the engine falls back automatically to a CPU path that shifts the existing bitmap and repaints only the strip of pixels newly scrolled into view, and it reports that it did. The CPU path therefore scales with the pixel area it has to repaint. Neither path scales with how many records you loaded — that is the whole design. The measured drawing cost at 1,000 and at 1,000,000 rows is on the documentation start page.
Text
Every string goes through HarfBuzz by default, with per-character fallback to an installed font for what the requested one does not carry, so a caret sits where the glyphs are rather than where character widths would put it. Programming ligatures are off (ArionFonts.Ligatures): a cell shows values, and a->b must not turn into an arrow. ArionFonts.Shaping = Simple switches back to one glyph per character, which is also the automatic fallback when the native library is missing. Bidirectional text inside a cell is laid out; the grid's own layout always runs left to right.
Smaller decisions that hold it up
- Separators are shared by neighbouring cells in the layout rather than drawn by each cell, so a doubled line cannot appear.
- Scroll offsets are subtracted in
doublebefore Skia sees them. Past 224 a float32 coordinate stops being able to represent adjacent pixels, and a long scroll would start to shimmer. - Everything snaps to the device pixel grid. One-pixel lines are drawn as exact device-pixel bands, so they stay crisp at 100, 125 and 150 percent scaling.
- Variable row heights run through
ISlotSizeProvider:VariableSizeProviderkeeps prefix sums for heights known up front,FenwickSizeProvidera Fenwick tree for rows measured as they come into view, so a changed height costs O(log n) and not a scan. - Texts, icons and sort culture belong to one grid (
GridContext), not to the process, so two grids in one window can speak two languages. - Invalidation is targeted. Hovering or editing repaints the affected rows, not the grid.
