-
Notifications
You must be signed in to change notification settings - Fork 0
Drawing Scenes Brushes and Charts
Cursorial.Drawing is a retained, cached-raster drawing layer that sits on top of
the cell buffer. The unit of work is a Scene — a buffer you
paint through a DrawingContext and that re-rasters only when you mark it dirty — composited onto a target
by a SceneCompositor that repaints only what changed. Reach for it when you want WPF/Avalonia-style
brushes, gradients, a pen/box engine with automatic junctions, soft shadows, or charts, but you don't need
the full UI framework.
If you've used WPF's DrawingContext / Brush / Pen, the shapes will be familiar. The big difference is
that a terminal cell shows exactly one solid color, so an IBrush is resolved to a scalar Color per cell
at draw time — it never lives inside a cell or a Style.
A Scene owns its own cell buffer, cleared to transparent so unpainted cells contribute nothing when
composited. You paint by passing a delegate to Draw:
using Cursorial.Drawing;
using Cursorial.Drawing.Media;
using Cursorial.Output; // Color, Colors, TextAttributes
using Cursorial.Rendering; // Rect
var scene = Scene.Create(columns: 24, rows: 8);
scene.Draw(ctx =>
{
ctx.FillRectangle(new Rect(0, 0, 24, 8),
new LinearGradientBrush(Colors.Cyan, Colors.Blue));
ctx.DrawBox(new Rect(0, 0, 24, 8), Pens.Light); // junctions auto-resolve where strokes meet
ctx.DrawText(2, 2, "Cached & composited", Colors.White);
});Key facts about a scene:
-
It is a cached raster.
Drawre-rasters only when the sceneIsDirty. The expensive work — gradient sampling, junction resolution, text layout, curve interpolation — lives behind that gate, so re-callingDrawon an unchanged scene costs nothing. -
You own invalidation. The layer is memoryless (it does not record draw ops, so it cannot auto-detect
content change or re-flow on resize). Call
scene.Invalidate()when your content changes; invalidation is coarse (whole-scene). -
RasterVersionis a monotonic counter bumped on each actual re-raster (never on a clean no-op). Snapshot it before a compositing pass and compare after: a stable version means the raster content is unchanged and prior composite output is still valid. (This is exactly what the compositor uses internally.) - A scene does not know where it sits — it composites onto a target; placement is the compositor's job.
Use Scene.Create(columns, rows) for a persistent (owner-held) scene. For transient, per-frame scenes,
rent from a ScenePool (below) instead so buffers don't churn.
DrawingContext is the authoring surface handed to your Draw delegate. Coordinates are integer cells.
The methods you'll use most:
-
Set(column, row, grapheme, style)— a single scalar cell write. -
FillRectangle(rect, brush)(or aColor) — fills cell backgrounds only, leaving any glyph beneath showing through on composite. This is the scene's transparency model: a translucent sampled color is stored verbatim so the compositor can blend it. An overload takes separatebrushBoundsso a gradient can span a larger extent than the painted region. -
FillOpaque(rect, brush, attributes)— fills with space-bearing cells that occlude (hide) any glyph on a lower layer. Use it for opaque panels, modals, and menus drawn over content. To draw a bordered opaque panel, fill then stroke the border withoverwrite: true:ctx.FillOpaque(rect, color); ctx.DrawBox(rect, pen, overwrite: true);. -
DrawText(column, row, text, foreground, background?, baseStyle)— multi-line-capable text. AColoroverload covers the solid case; anIBrushoverload samples the brush per cell (so a gradient colors the text glyph by glyph). It interprets\r\n/\n/\ras line breaks, is grapheme-aware (wide clusters occupy two cells), and returns the text's bounding box as aSize. -
Pen strokes —
DrawLine,DrawBox,DrawRectangle(box + optional fill), and the titled variants below. Each has both aPenand a solid-Coloroverload.
Pen strokes are deferred: they accumulate so box-drawing junctions form automatically across separate
DrawLine/DrawBox calls, then flush once after your draw delegate returns — and they flush last, so
existing glyphs (like text) survive a box edge crossing them (text beats decoration). An axis-aligned line
uses box-drawing glyphs and forms junctions; a diagonal line rasterizes into braille dots at sub-cell
resolution.
scene.Draw(ctx =>
{
ctx.DrawBox(new Rect(0, 0, 20, 5), Pens.Light);
ctx.DrawLine(10, 0, 10, 4, Pens.Light); // a T-junction forms where this meets the box's top edge
});Wrap related strokes in a figure (using var figure = ctx.BeginFigure();) when you want junctions to
form among themselves but not with strokes outside the group — and to make a pen's gradient sample against
the union bounds of the figure's own strokes. BeginFigure(rect) pins explicit brush bounds instead.
Figures do not nest.
DrawTitledBox strokes a box with a title laid onto the top edge, the rule splitting around the label so it
never overstrikes the text. DrawPanel is the one-call "group box": an optional fill, a pen border, and an
optional PanelTitle.
ctx.DrawPanel(new Rect(0, 0, 30, 6),
pen: Pens.Rounded.WithColor(Colors.Cyan),
fill: new SolidColorBrush(Color.FromRgb(20, 24, 40)),
title: new PanelTitle("Status") { Position = TitlePosition.Center });A PanelTitle lifts implicitly from a string for the common left-aligned case, and carries its own brush,
position, and SGR attributes.
DrawDropShadow(element, geometry, shadowColor) paints a soft drop shadow cast by an element's silhouette —
draw it before the element. DrawInnerShadow(...) insets a shadow inside an element's edges — draw it
after the fill it insets. Both take a ShadowGeometry (radius, strength, which ShadowEdges cast); the
parameterless Drop() / Inner() factories give a soft black default.
ctx.DrawDropShadow(panelRect, ShadowGeometry.Drop(radius: 2), Color.FromRgba(0, 0, 0, 200));
ctx.DrawPanel(panelRect, Colors.White, fill: Brushes.Black);The shadow color must be RGB (a Default/palette color carries no alpha to fade). The translucent shadow
background rides to the compositor (darkening the layer beneath at composite time); a same-scene glyph it
falls on is dimmed at draw time.
DrawingContext carries a nesting clip/translate stack honored by every draw path — the per-cell writes,
the text and content paths, shadows, titled panels, and the deferred pen strokes and chart braille:
-
PushClip(rect)bounds subsequent draws to a rectangle (intersected with the current clip). -
PushTranslate(columns, rows)shifts subsequent draw coordinates (offsets may be negative — content scrolled above/left of its viewport). -
Push(clip, translateColumns, translateRows)is the common "give this widget a viewport" call.
Each returns a DrawingStateScope you dispose (via using) to pop. IsVisible(column, row) is the cheap
pre-test for whether a draw would land inside the active clip.
using (ctx.Push(clip: new Rect(0, 0, 10, 4), translateRows: -scrollOffset))
{
ctx.DrawText(0, 0, longBody, Colors.White); // clipped to the viewport, scrolled by the translate
}A brush is an immutable color source (IBrush) sampled per cell. The built-ins live in
Cursorial.Drawing.Media:
-
SolidColorBrush— one color (with optionalOpacity0–1). AColorconverts implicitly to aSolidColorBrush, and theBrushesstatic class offers shared singletons (Brushes.Red,Brushes.Transparent,Brushes.Default, …). -
LinearGradientBrush— a sweep betweenRelativePointendpoints (default a horizontal left→right sweep). The two-color constructornew LinearGradientBrush(start, end)is the common case; the stops-list constructor takes any number ofGradientStops. -
RadialGradientBrush— an SVG-style two-point radial (center, focal point, X/Y radii as fractions of the bounds). -
TileBrush/ImageBrush— sample a decoded RGBA image across the paint bounds.ImageBrushmaps one image into the bounds per aStretchmode;TileBrushrepeats a tile per aTileMode. Both have aFromPng(...)factory.
A Pen is a stroke definition: a color source (Brush) plus glyph-selecting attributes —
Weight (Light / Heavy / Double), Corners, Dash, EndCap, Junction mode, GlyphSet (Unicode or ASCII),
and SGR Attributes. Color is kept separate from stroke attributes, so the Pens presets are keyed on
attributes and you add color at the call site:
var pen = Pens.Heavy.WithColor(Colors.LightGreen).WithCorners(CornerStyle.Rounded);
ctx.DrawBox(new Rect(0, 0, 12, 4), pen);
// default(Pen) is a usable light/sharp/solid pen at the terminal default color:
ctx.DrawBox(new Rect(0, 0, 12, 4), default);A terminal stroke is always one cell wide — Weight selects a glyph family, never pixel thickness. There is
no implicit Color → Pen (or Color → IBrush) conversion; every pen-typed draw method ships a sibling
Color overload.
A SceneCompositor composites an ordered z-stack of SceneLayers onto a target buffer. Each layer pairs a
Scene with CompositeParameters — an integer cell offset, a uniform opacity (0–255), an optional
clip, and an optional blend mode. (Affine rotation/scale are deliberately absent — a cell grid can't
express sub-cell transforms.)
using Cursorial.Rendering;
var compositor = new SceneCompositor(Style.Default); // or a stored backdrop CellBuffer
var target = new CellBuffer(80, 24);
scene.Draw(draw); // owner-driven re-raster happens first
ReadOnlySpan<SceneLayer> layers =
[
new SceneLayer(background), // lower (composited first)
new SceneLayer(scene, new CompositeParameters(offsetColumn: 4, offsetRow: 2, opacity: 220)),
];
bool changed = compositor.Composite(layers, target.AsView());
// changed == false when nothing moved or re-rastered: the target is left untouched and your
// frame renderer's own diff emits nothing.The compositing invariant is what makes retained translucent scenes correct: a scene cell is never
composited onto a previously-composited cell — it is always composited onto the base (or a lower scene
freshly composited onto base, in z-order). Compositing a translucent scene onto its own prior output each
frame would saturate and drift; compositing onto base each frame is stable. The compositor maintains the
target incrementally (treat it as retained — don't clear it between frames): it computes the dirty-region
union from any scene that re-rastered (a bumped RasterVersion) or any layer whose parameters changed,
resets just that union to base, re-composites the intersecting layers bottom-up, and marks the union dirty so
a dirty-region-restricted renderer gets a correct bounded repaint.
ScenePool recycles scene backing buffers so per-frame scenes don't allocate. Rent(columns, rows) reuses a
freed buffer of the exact same dimensions when one is available; scene.Dispose() returns the buffer to its
pool. Persistent (cached) scenes are owner-held and made with Scene.Create instead.
var pool = new ScenePool();
using var frameScene = pool.Rent(40, 10); // returns to the pool on dispose
frameScene.Draw(ctx => ctx.DrawText(0, 0, "transient", Colors.White));Cursorial.Drawing.Charts renders line / scatter / bar charts (plus sparklines) into a rectangular area of a
DrawingContext. A chart is a draw-op, not a surface — wrap one in a scene yourself. There are two ways to
use them.
One-liner extension methods on DrawingContext for the common cases (no chart object needed):
using Cursorial.Drawing.Charts;
scene.Draw(ctx =>
{
ReadOnlySpan<double> samples = [3, 1, 4, 1, 5, 9, 2, 6];
ctx.BarChart(new Rect(0, 0, 30, 8), samples, Colors.Cyan);
ctx.Sparkline(0, 9, width: 30, samples, Colors.LightGreen);
ReadOnlySpan<PointD> series = [new(0, 0), new(1, 2), new(2, 1), new(3, 3)];
ctx.LineChart(new Rect(0, 11, 30, 8), series, Colors.Yellow, CurveInterpolation.CatmullRom);
ctx.ScatterChart(new Rect(0, 20, 30, 8), series, Colors.Magenta, MarkerStyle.Dot);
});Chart objects for the richer options — construct one (e.g. new BarChart(values, brush),
new LineChart(points, color), new MultiLineChart(series)) configured with init properties, then call
ctx.Chart(area, chart):
var chart = new BarChart(samples, new LinearGradientBrush(Colors.Cyan, Colors.Blue))
{
Orientation = BarOrientation.Vertical,
Gap = 1,
};
scene.Draw(ctx => ctx.Chart(new Rect(0, 0, 40, 10), chart));Notable behaviors:
-
Bars are signed and zero-anchored. Positives grow toward the far edge, negatives toward the near edge,
with fractional Block-Elements glyphs so lengths aren't cell-quantized. An explicit
Rangeis always expanded to include zero (a bar axis that omits zero misrepresents magnitude). -
Line/scatter charts use braille for sub-cell resolution;
CurveInterpolationcontrols how points connect (Linear,CatmullRom, …), andNaNvalues are gaps in the line. -
MultiLineChartshares one axis range across severalChartSeriesso they align;ResolveRange()gives you that range to hand to anAxesframe. - Every chart brush is sampled per cell against the chart area, so a gradient flows across the whole plot.
- Chart implementations clip to the context bounds and never throw.
- Rendering: the cell buffer & frame renderer — the layer beneath scenes; blending modes and alpha compositing.
-
Rendering: rich text & images — formatted text and images, which
DrawingContextcan also paint (DrawFormattedText,DrawContent). -
Animation — the time-free engine, with interpolators for
Color,Rect,Size,IBrush, andPenso scenes can animate. - UI overview — the framework that drives this layer's scenes through a render tree.
Cursorial.Core
Cursorial.Rendering
Drawing & Animation
Cursorial.UI
- Overview
- Layout & panels
- Controls
- Styling & themes
- Data binding
- Input & focus
- Windowing
- Animation & transitions
Declarative