Theming and styling
Every colour, metric and type size is a property on a plain object. There is no control template and no visual tree to restyle — immediate-mode drawing is the point, and it is also why a theme change is one assignment.
The built-in themes
control.VisualTheme = ArionTheme.Light();
// also: Dark(), Obsidian(), Ivory(), Carbon(),
// HighContrastDark(), HighContrastLight()Setting it forces a full redraw and nothing else. Dark and Light are the pair most apps want; Obsidian, Ivory and Carbon are alternatives with their own character. The same assignment is available on the facade as grid.Api.State.VisualTheme, and on every host.
The two high-contrast presets are there because an immediate-mode surface is invisible to the Windows high-contrast setting. By default the hosts follow that setting and switch to them on their own, then restore your theme when the mode ends; set AutoHighContrast to false on the control to manage it yourself.
A theme carries its density too — RowHeight and RowSeparator travel with it, and the grid stamps them onto the template on its own. That is deliberate: Carbon reads as Carbon partly because its rows are 22px with no hairlines and the zebra carries the rhythm, and the same palette at a taller row height is a different design.
Deriving one
A theme is an immutable record, so deriving one cannot disturb the theme a control is already drawing with. Use a with expression on a built-in:
var brand = ArionTheme.Dark() with
{
Accent = new Rgba(0x2E, 0x5A, 0x8D),
AccentTint = new Rgba(0x1F, 0x2C, 0x40),
FontFamily = "Segoe UI",
RowHeight = 24,
};
control.VisualTheme = brand;The copy is shallow, which is correct: Type, the typography, is itself immutable and can be shared.
Accent is the one colour for selection edge, focus ring, sort indicator, active funnel, checkbox mark and progress fill. It is separate from Blue, which is a status colour, so a theme with a gold accent keeps blue statuses. Change the one you mean.
What a theme carries
- Surfaces
Background,RowEven,RowOdd,RowHover,HeaderFill,GroupHeaderFill,GroupHeaderFillSticky,CardFill,CardBorder,GridLine,Border.- Ink
Text,TextDim,TextFaint,ChipText,GroupChevron.- Accent and state
Accent,AccentTint,AccentDeep,AccentGradientFrom,SelectionFill,FocusBorder,EditBackground,EditSelection,SearchHighlight,SearchHighlightActive.- Semantic colours
Green,Amber,Red,Blue— what a badge or a rule should reach for instead of a literal.- Scroll bars
ScrollThumb,ScrollThumbHot,ScrollTrack.- Type and metrics
FontFamily,Type(anArionTypographywith the individual sizes and optional family overrides),RowHeight,RowSeparator,CellPaddingX,CellTextOverflow,BadgeStyle, and the focus frame's inset, width and radius.
Reading the theme from your own code
Palette is a facade over the current theme, and it is what draw code should use. A literal survives the switch to dark and stops being legible; Palette.TextDim does not.
TypedColumn.Number<Account>("Amount", a => (double?)a.Amount, format: "N2")
.Style(a => a.Amount >= 90_000, foreground: Palette.Amber, bold: true)
.Style(a => a.Amount < 0, foreground: Palette.Red)ArionTheme.Current is the ambient the palette reads. It is per-thread on purpose: a render pass stamps its own control's theme on its own thread, so two controls with different themes cannot make each other draw a frame in the wrong one. To borrow a theme for one synchronous stretch, use a scope:
using (ArionTheme.Use(exportTheme))
{
DrawReportHeader(surface);
} // the thread's previous theme is restoredRow-level rules
A StyleRule is a predicate over the item plus the overrides it applies. Passed to the view, it colours whole rows; Column.Style is the same mechanism scoped to one column.
var rules = new[]
{
new StyleRule(o => ((Account)o).Amount < 0,
new CellStyle(Foreground: Palette.Red, Weight: FontWeight.Bold)),
new StyleRule(o => ((Account)o).Status == Status.OnHold,
new CellStyle(Background: Palette.AccentTint,
LeftEdge: Palette.Amber)),
};
var view = new GridView<Account>(columns, source, rowRules: rules);CellStyle's fields are all optional, so a rule only says what it changes: background, foreground, weight, a left-edge marker, font size and family, alignment, a border, extra horizontal padding. When several rules match, the later one wins field by field. Rules run while the cells are drawn, for the cells in view: the cost follows the viewport rather than the size of the data, and live data restyles itself without a call. The predicates run on the render thread, so keep them cheap and do not touch UI-thread state in them.
The order things win in
Four layers, and the order is fixed:
- Theme — the ground everything starts from.
- Column style — what the column declared for itself.
- Style rules — conditional, so they only speak when they match.
- Interaction state — hover, selection, focus, the edit session.
Interaction wins last, and that is the useful part: a hovered row is a translucent layer over whatever it already showed, so a red figure stays red under the highlight and a selected row still shows its conditional formatting. A highlight that replaced the fill would hide exactly the state the user is pointing at.
Conditional formatting beyond a flat colour — data bars, colour scales, icon sets — with a rule manager the user can open, is the ArionUI.Formatting package, free during the preview. Enable it per view with view.Formatting().EnableFormattingMenus(grid.Api, view). StyleRule and Column.Style on this page need no package.
Boards and the drawn controls are styled by style sheets with selectors and a cascade, over the same themes: Styling. What the grid cannot do yet is on What it does not do yet.
