This document captures design and implementation notes for Canvas.
For end-user usage and examples, see Canvas.
Painter draws directly into the current CellBuffer during Render.Canvas : VisualPainter : Delegator<Action<CanvasContext>>
HorizontalAlignment = Align.StretchVerticalAlignment = Align.StretchCanvas does not have intrinsic content size. Its MeasureCore returns:
Min = (0, 0)Natural = (0, 0)Max = (∞, ∞)GrowX = 1 only when HorizontalAlignment == Align.StretchGrowY = 1 only when VerticalAlignment == Align.StretchThis makes Canvas behave like a flexible “fills available space” visual by default, but it can also be constrained
via min/max size like any other visual.
During RenderOverride:
Bounds is empty, does nothing.Painter is null, does nothing.Theme via GetTheme()CanvasStyle via GetStyle<CanvasStyle>()CanvasStyle.ResolveDefaultStyle(theme)CanvasContext and calls the painter callback.Painter is executed inside the normal render tracking context.
If the painter reads bindables/state, those reads will be tracked and changes will automatically schedule repaint.
CanvasContext draws with coordinates relative to the canvas origin:
(0,0) is the top-left cell of the canvas boundsThe framework already establishes clipping to the canvas Bounds before calling RenderOverride, but CanvasContext
also performs explicit bounds checks to keep drawing helpers safe.
CanvasContext is a small helper API around the underlying CellBuffer, providing:
Bounds / SizeClear() / Clear(rune, style)SetPixel(x, y, rune, style) (cell-level draw)DrawLine(...) (diagonal line; Bresenham-style cell rasterization)FillRect(...)DrawBox(...) with LineGlyphs (handles small widths/heights)DrawCircle(...) (midpoint circle algorithm; outlines only)WriteText(...) (clipped to the canvas bounds)The helper methods are intentionally simple, allocation-free, and optimized for small surfaces rather than large framebuffers.
Since there is no retained drawing state, a typical painter starts with ctx.Clear(...) and then redraws the full scene.
Some scenarios benefit from drawing at a higher resolution than the cell grid (e.g. thinner lines, smoother diagonals,
sparklines). Terminals support a family of Unicode 8-dot pattern glyphs (U+2800..U+28FF) where each terminal cell
can encode an internal 2×4 dot grid (8 sub-pixels). By rasterizing shapes into this dot grid and emitting one rune
per cell, Canvas can render “thin” strokes while staying in the normal cell rendering pipeline.
This feature is enabled via a Canvas-level boolean so it is easy to switch on/off without plumbing style records.
Add a bindable boolean directly on Canvas:
UseFinePixels : bool (default: false)Naming rationale:
When UseFinePixels = true, all existing CanvasContext drawing primitives should continue to work, but rasterize
using the fine 2×4 dot grid where relevant:
SetPixel(...): sets a single dot at the center of the target cell (instead of painting a full block rune).DrawLine(...): runs the line rasterizer on the dot grid (produces smoother diagonals).DrawCircle(...): rasterizes into dots (higher detail than coarse cell rasterization).DrawBox(...) / rectangle outlines: can use dots for thinner borders (implementation-defined, but should be
consistent).FillRect(...), WriteText(...): remain cell-based (they are already “full cell” operations).The intent is that users can toggle UseFinePixels without rewriting their painter code.
All existing drawing methods keep cell coordinates:
(0,0) remains the top-left cell.Internally, fine rasterization maps a cell coordinate (x, y) to the dot coordinate at the center of the cell:
dotX = x * 2 + 1dotY = y * 4 + 2Line endpoints, circle centers, and other primitive inputs use this mapping so the overall layout stays stable across modes.
Fine pixel mode can be implemented as a small dot-mask layer inside Canvas / CanvasContext:
0x2800 + mask) and write it to the CellBuffer.Because terminal cells can only carry a single foreground/background style:
Style written to that cell (or the
resolved CanvasStyle default when omitted).Canvas is immediate-mode and draws directly into the CellBuffer. Fine pixel mode should have deterministic ordering:
Resolved from the environment via CanvasStyle.Key:
DefaultRune (default: '█')DefaultStyle : Style?
theme.ForegroundTextStyle() (draw using “ink” on terminal default background).Tests that lock down current behavior:
src/XenoAtom.Terminal.UI.Tests/CanvasTests.cs
CanvasContext).Painter as the fast/low-overhead default.