ClaFi Colour

The theme states hues; every colour is decided where it is used

Introduction

Nothing in the framework paints in a colour it wrote down. It paints in an ink: a name for a colour, resolved against the surface the painter stands on, in the colour mode it stands in, under the theme in effect. InkWell::textInk(InkGrade::Muted) is a quiet ink over whatever is behind it - grey text on a plain page, and a tinted grey on a tinted card, without the caller knowing which it landed on.

Every ink is an InkColor at a grade - a share of the distance between the surface and an ink. The colour says which ink the grade runs toward, and a caller cannot tell the families apart at the call site:

Module imports: naming an ink needs ClaFi.Core.System.InkWell. The types the names are built from - Ink, InkColor, InkGrade, Pigment, InkTone, ColorMode - come with ClaFi.Core.System.UiTypes. A theme's effects and rules, and the UiElement a control wears, live in ClaFi.Core.AppTheme_Colors. The form the paint path reads, BakedColorRule among it, lives in ClaFi.Core.AppTheme_Baked.

Quick Start

An ink becomes a colour where it is used, against the surface the painter stands on. A PaintEvent answers textRgb, accentRgb and spotRgb for the three families at a named step, and inkRgb for any Ink. An icon's PaintIconEvent answers the same four.

// In a control's paint handler.
Color body = event.textRgb(InkGrade::Strongest);     // the full ink
Color quiet = event.textRgb(InkGrade::Muted);        // a step down toward the surface
Color mark = event.accentRgb(InkGrade::Strongest);   // the theme's accent
Color warn = event.inkRgb(InkWell::Yellow);          // a pigment

event.canvas().drawPath(path, { PathDrawLayer::stroke(mark, event.scaledStrokeWidth(Thickness::Thin)) });

Text takes the ink itself rather than a colour, so a run keeps its meaning until the layout is drawn:

text << InkWell::textInk(InkGrade::Muted) << L"Ping "
     << PopColor{} << InkWell::accentInk() << L"24 ms";

// The same thing from a format string.
text << Fmt{ L"[color muted]Ping [/color][color accent]{} ms[/color]", ping };

Core Concepts

Surface and ink

Two colours travel down the control tree with every paint: the surface, which is the background in effect under the brush, and the ink, which is what the theme says text on that surface is drawn in. A control that fills its own background becomes the surface for its children. A control that states a text rule changes the ink for everything inside it.

Both are carried as Hsl in the ControlPaintContext the event hands out as controlContext(): surfaceHsl is where a grade of 0 lands, textHsl where a grade of 1 lands. Nothing derives from "the theme's background" - what a colour is measured against is what is actually behind it.

Grades

A grade is a share of the gap between those two ends. It owns no hue and no tone of its own: at 0 it lies flush with the surface and cannot be seen, at 1 it is the ink exactly. Stated as a share rather than as a luminosity, so a theme with quiet contrast keeps it - every step scales with what that theme declared readable.

Five steps are named, and a caller wanting one of its own states the share instead. Nothing rounds a stated grade to the nearest named one.

InkWell::textInk()                     // Strongest, the ink the chain arrived at
InkWell::textInk(InkGrade::Subtle)     // a named step
InkWell::textInk(0.45f)                // a share of your own
InkWell::surfaceInk()                  // 0 - the surface itself

The whole colour is mixed, not the luminosity alone, so a grade loses its colour as it loses its contrast. A quiet ink over a surface the palette has tinted picks that tint up on the way down. That is the mechanism working rather than an allowance made for it: grey text is not grey because the surface under it is not.

surfaceInk() is what something drawn in its own backdrop's colour asks for - a knockout band under a line that crosses another. It is invisible on the surface it was resolved against, and only there.

Pigments

A pigment is a hue the theme supplies. Two are named for their role - the accent and the spot, described next. The other four are named the way colours are named everywhere else: InkColor::Yellow, Green, Blue and Red. They carry meaning rather than emphasis, and are safe to use in small doses - an icon, a badge, a border. Each takes its hue from a region of the theme's harmony that every theme fills its own way, so it carries the colour its name states whatever the anchor is.

The theme supplies their hue and nothing else. How colourful the colour is and where it sits are the framework's to state, once for each colour mode, as a pair of InkTone in InkWell::pigmentTones. A semantic ink therefore reads the same on every surface in the form - which is the point. A yellow border going brown on a dark theme is a border doing its job; a warning icon going brown is one failing at it.

The grade fades a semantic colour toward the surface the way it fades the text ink, so at Strongest the colour stands at its own tone.

InkWell::Yellow                          // a warning, at its own tone
InkWell::redInk(InkGrade::Muted)         // a quiet red, part of the way back to the surface
A semantic colour states no elevation. Elevation is oriented by the colour mode by construction, which is right for a control element and wrong for a colour carrying a meaning. If you find yourself reaching for one, what you want is a grade.

Accent and spot

The palette carries two accent hues, and the theme states an effect for each. An accent or spot ink is a grade of the text ink with that effect applied to it, so what comes out is where the theme says such a colour lands rather than a relative of the ink underneath.

InkEffectWhat it is for
InkWell::accentInk()ThemeColors::accent The colour the interface answers with. It is the palette hue a selected button, a checked mark and the band behind selected text move toward, so an icon or a run drawn in it belongs to the controls around it.
InkWell::spotInk()ThemeColors::spot The second accent, for what stands apart from the interface rather than answers to it: a brand mark, the application's own name, a run of emphasised text. Spent sparingly, which is a decision the theme makes once rather than each caller remaking it.

Both take a grade, and the grade names the ink the effect is applied to rather than where the result ends up. A theme whose accent effect sets both saturation and elevation outright therefore answers the same colour at every grade, which is the usual case.

Elevation and colour mode

Elevation is an absolute coordinate, not a relation: 0 is the mode's floor, 1 its ceiling, and two elements at 0.6 stand at the same luminosity whatever each is drawn on. The colour mode orients it - up from the dark end on a dark theme, down from the light end on a light one - so one number serves both sides of the theme.

The direction is the control's own, not the theme's. PaintEvent::lightness() is the side a control stands on - 0 the dark end, 1 the light one, anything between a crossing part way through. It is inherited from the control above, and a control that knows better states it outright through AdjustPaintEvent::setLightness - a tab wearing its page's colour takes the page's side.

// The two directions of one mapping. The lightness is the element's, the floor the theme's.
float elevationOf(float luminosity, Lightness lightness, float luminosityFloor);
float luminosityOf(float elevation, Lightness lightness, float luminosityFloor);

A grade is not an elevation. A grade is a share of a journey between two colours the chain supplies; an elevation is a place on the luminosity axis. gradeOf(InkGrade) names the first, elevationOf the second.

The application's mode

The side of the theme the application is drawn on is the user's choice, kept in its config beside the theme's path, and every theme is worn in it. ColorModeSetting holds that choice, and it has three answers:

SettingWhat the application wears
ColorModeSetting::AutoThe mode the desktop asks for, followed while the application runs. The first answer and the default: a config that states nothing holds it.
ColorModeSetting::DarkDark, whatever the desktop asks for.
ColorModeSetting::LightLight, whatever the desktop asks for.

A setting is a choice and a ColorMode is a side. The setting is read into a mode where a theme is put on, so everything that paints holds a ColorMode and Auto never reaches a painter. wornColorMode is that reading, and the call to make when the question is which side the application stands on:

// The user's choice, stated the way a mode button states it. The config node's change puts
// the theme on again, and the application crosses to the side the choice names.
applyColorMode(appContext(), ColorModeSetting::Auto);

// The side the application is drawn on - under Auto, the desktop's.
ColorMode mode = wornColorMode(appContext());

Auto follows the desktop while the application runs. A change of the desktop's mode crosses every window to the other side of the theme, as a click on a mode button does, with no restart and nothing for a control to do. Windows answers with its app mode - Choose your default app mode in the colour settings, which is kept apart from the mode the taskbar wears. Wayland answers with the color-scheme the XDG desktop portal serves. A desktop stating no preference reads as Light: GNOME's default look states none, and so does a session without a portal.

The first window opens in the desktop's mode rather than crossing to it, because the desktop is asked before any window exists. On Wayland that first ask waits up to a second for the portal, and an answer later than that crosses the window when it comes.

An application may state a first-run default of its own, and states it only where the user has made no choice yet. The config is loaded before any application code runs, so a default written over a loaded choice would undo the Settings page on every start:

if (!application.configFolderExists())
    applyColorMode(application.context(), ColorModeSetting::Light);

What the theme does not reach - a picture chosen for each side, a scene with colours of its own - listens on Platform::events() for the desktop moving. The event carries nothing: it says ask again, and Platform::systemColorMode() answers. It is raised whatever the setting holds, so a listener asking which side the application wears asks wornColorMode:

// A member of the listener, so the connection goes when the listener does.
ScopedEventConnection m_desktopMode{
    Platform::events().connect<SystemColorModeEvent>([this](SystemColorModeEvent&) {
        pickPicture(wornColorMode(appContext()));
    })
};
Module imports: ColorModeSetting comes with ClaFi.Core.System.UiTypes, applyColorMode and wornColorMode with ClaFi.App.Themes, and Platform and SystemColorModeEvent with ClaFi.Core.Context.FormContext. A file reading the config node itself - appContext().colorMode() - imports ClaFi.Core.Dom_StdSerializers and ClaFi.Core.AppTheme_Colors as well: the setting's serializer and its names are declared there, and the node is an incomplete type without them.

Reference

InkWell

The one place the framework's colours are written down, and the whole of what a caller names. One entry per colour rather than one per icon, so two icons meaning the same thing cannot arrive at two yellows.

NameAnswers
textInk(InkGrade = Strongest)A named step between the surface and the ink.
textInk(float grade)A share of your own, for a step the framework does not draw in.
surfaceInk()Grade 0 - flush with the surface, and so invisible on it.
accentInk(InkGrade = Strongest)The theme's accent effect, at that grade.
spotInk(InkGrade = Strongest)The theme's spot effect, at that grade.
yellowInk, greenInk, blueInk, redInk (InkGrade = Strongest)A semantic colour at that grade.
blackInk, whiteInk (InkGrade = Strongest)Pure black or white at that grade, the same in both colour modes.
pigmentTones(Pigment)The tone a semantic colour is drawn at on each side of the theme.
InkWell::YellowWarning. Drawn, never read - see below.
InkWell::GreenSuccess, present, on.
InkWell::BlueInformation, a link, a thing named.
InkWell::RedError, destructive, off.
InkWell::Black, InkWell::WhitePure black and white, for a mark that has to read the same under every theme.

Every accessor that takes an InkGrade also takes a float, for a share of your own. The named colours are their accessors at Strongest.

Yellow is not a text colour. At the 4.5:1 reading threshold on a light page the whole warm arc from 40 to 120 degrees collapses to brown, so there is no colour in the yellow family that is both legible as text and recognisable as yellow. [color yellow] reads, so that a text built from code can be written out and read back, and is not an invitation.

InkGrade

GradeShareReads as
InkGrade::Faint0.12Barely present - a hairline, a disabled mark.
InkGrade::Subtle0.27A border, a divider.
InkGrade::Muted0.60Secondary text.
InkGrade::Strong0.80An object an accent is drawn on top of.
InkGrade::Strongest1.00Body text - the ink itself.

Pigment and InkTone

Pigment is Yellow, Green, Blue, Red - the four other pigments, each a region of the harmony's wheel rather than a place in an order. Accent and spot reach the paint through the theme's effects and have no entry here. InkTone is the pair that says how colourful and how light: the two travel together because a hue needs a different amount of colour to read at one luminosity than at another, so stating either without the other names half a colour. PigmentTones states one InkTone for each side of the theme.

struct InkTone { float saturation{ 1.0f }; float luminosity{ 0.5f }; };
struct PigmentTones { InkTone dark{}; InkTone light{}; };

// Declared once and serving both modes from that one declaration.
constexpr PigmentTones InkWell::pigmentTones(Pigment);

// The pigment a semantic colour takes its hue from, and none for the other colours.
constexpr std::optional<Pigment> pigmentOf(InkColor);

The tones the four semantic colours are drawn at, measured at the 3:1 graphics threshold against pages at luminosity 0.13 and 0.87:

ColourDark toneLight tone
Yellow1.00 / 0.901.00 / 0.51
Green1.00 / 0.841.00 / 0.49
Blue1.00 / 0.471.00 / 0.41
Red1.00 / 0.561.00 / 0.55

A yellow's light tone reads as amber rather than lemon because nothing brighter clears 3:1 on a light page - a bright yellow is a light one - which is why every system draws that icon amber on light and lemon on dark.

Resolving an ink

An ink is resolved by the event the painter holds, against the surface and the lightness that event carries. PaintEvent and PaintIconEvent answer the same calls, each a forward to the event's ControlPaintContext, which is where the resolution happens.

CallUse
event.textRgb(grade), accentRgb(grade), spotRgb(grade)A family at a named step - the usual spelling, in a control and in an icon.
event.inkRgb(ink)Any Ink - a pigment, black or white, or a share of your own.
event.inkRgb(pigment, tone)A pigment's hue at a tone stated outright, with nothing placing it.
event.inkRgb(pigment, saturation, elevation)A pigment placed by elevation, with the control's lightness doing the orienting.
event.controlContext().inkHsl(ink)The resolution before it becomes a Color. Take this when a colour effect is applied next.
The pipeline runs one way. Hsl becomes Color where a canvas method needs one, and never back. explicit Hsl(const Color&) exists to ingest a colour a caller stated, not to read back one the model produced - the backward half of that trip is lossy, and a rule applied to the result is a rule applied to a rounding error.

Naming an ink in text

A text run carries the Ink itself, so it is recoloured by the control it lands in and by the theme in effect. Streamed in code, an ink can be written as the one part that differs from the text ink at Strongest:

text << InkGrade::Muted << L"Ping " << PopColor{}           // the text ink at a step
     << InkColor::Accent << L"24 ms" << PopColor{}         // a colour at Strongest
     << InkWell::redInk(InkGrade::Muted) << L" late";   // anything else, as the Ink it is

A format tag names an ink by its colour and grade and states no tone of its own - the tones are InkWell's. Both are spelt as their members in lower case, the colour first, and either can be left out: the colour is then text and the grade strongest.

TagInk
[color strongest]textInk()
[color muted]textInk(InkGrade::Muted)
[color accent]accentInk()
[color spot]spotInk()
[color green], [color blue], [color red]The semantic inks of those names.
[color red muted]redInk(InkGrade::Muted) - any colour at any step.
[color accent 0.45]accentInk(0.45f) - a share between the steps.
[color #RRGGBB], [color {}]A colour stated outright, which the theme does not touch.

See the TextEngine manual for the rest of the format language.

The application's mode

NameAnswers
ColorModeSettingAuto, Dark or Light - the user's choice, as the ColorMode node of the config's Interface section holds it. Kept in the file by name: ColorMode=Auto.
AppContext::colorMode()That node. Its change puts the stored theme on again.
applyColorMode(AppContext&, ColorModeSetting)States the choice in the config, which keeps it between sessions where the user allowed storing.
wornColorMode(const AppContext&)The side the application is drawn on: the setting's, or the desktop's under Auto. Asked each time rather than kept, so it moves with the desktop.
Platform::systemColorMode()The mode the desktop asks applications to be drawn in, whatever the setting holds. Light where the desktop states no preference.
Platform::events()Where SystemColorModeEvent is raised, once for each change of the desktop's mode.

Writing an Icon

An icon states inks, never colours. Which ink depends on what the shape means:

void paint(PaintIconEvent& event)
{
    const float strokeWidth = event.scaledStrokeWidth(Thickness::Thin);
    Color body = event.textRgb(InkGrade::Strong);
    Color accent = event.accentRgb(InkGrade::Strongest);

    canvas.drawPath(bodyPath, { PathDrawLayer::stroke(body, strokeWidth) }, &transform);
    canvas.drawPath(headPath, { PathDrawLayer::stroke(accent, strokeWidth) }, &transform);
}
Two things stand outside the model. PathArt is painting rather than interface and is free to use any colours it likes. Brand marks are a fixed identity, and answer to whoever owns the brand rather than to the theme.

What a Theme Owns

The palette

A palette is strictly a set of hues. No saturation, no luminosity - those belong to whoever knows what the colour means. ThemeColors::paletteHues holds three, and all three are assigned:

IndexNamed by a rule asWhat it is
paletteHues[0]ColorRuleHueOp::PaletteColor1 The hue that tints surfaces, and so the hue every grey in the form is made of.
paletteHues[1]ColorRuleHueOp::PaletteColor2 The accent - the hue the interactive states and the accent ink are drawn from.
paletteHues[2]ColorRuleHueOp::PaletteColor3 The spot - the second accent, and the tooltip's tint.

Separately, anchorHue and harmonyKind derive the harmony the four other pigments are taken from. The harmony is derived rather than stored, so a file carries the two inputs and not the result.

Colour effects

A ColorEffect is three channels - hue, saturation, elevation - each stating what happens to the colour handed to it. Everything a theme says about a colour is said in these.

OperationEffect
ColorRuleOp::NoChangeLeaves the channel as it arrived.
ColorRuleOp::OffsetMoves it by a distance. A relation between two colours.
ColorRuleOp::ScaleMultiplies it.
ColorRuleOp::SetStates where it lands, absolutely, read in the control's own direction.

The hue channel is its own type, because a hue is chosen from a short list rather than moved: PaletteColor1, PaletteColor2, PaletteColor3, NoChange, or ExactValue with a hue written into the effect.

ColorEffect accent{
    { ColorRuleHueOp::PaletteColor2 },   // H - the palette's accent hue
    { ColorRuleOp::Set, 1.0f },          // S
    { ColorRuleOp::Set, 0.544542f }      // E - an elevation, not a luminosity
};

All three operations run in elevation space, so what an effect states is read in the direction the control it lands on stands in. Set 0.0 comes out at the floor on a dark theme and white on a light one, which is how a mark gets ink that contrasts with it under either mode.

ThemeColors::accent and ThemeColors::spot are bare effects: the two inks a theme states itself, applied wherever an ink names them. The accent is also what a mark that says a thing is on is drawn in - a text caret, the indicator under an open tab - through ControlPaintContext::indicatorRgb. A rule that wants the accent's colour states its values in its own effect, as the focus ring's rules do.

Colour rules

A ColorRule is what a theme says about an element: the inputs it reads, the channel it writes, and the effect it applies there.

MemberWhat it states
inputsThe inputs the rule reads, joined as "or". An empty set is at rest: the rule applies in full.
andInputsWhat must hold as well, joined as "and" - a surface when focused and the window focused.
outputThe PaintChannel the rule writes.
effectThe ColorEffect applied to that channel, as far as the inputs stand.

Every input is a factor from 0 to 1 that follows its state through an animation, so a rule fades in as its input rises. The inputs joined as "or" stand where any one of them would take them - 1 - (1 - a) * (1 - b) - and the ones joined as "and" multiply that. A rule reading "when hovered, and mouse and current" applies as far as all three hold.

RuleInputWhat it reads
HoveredThe pointer over the control, and nothing else. A control reached by a key is not hovered.
PressedA press held on the control.
FocusedThe control's own focus, whether or not its window has it.
SelectedA checked box, an open tab, a picked item. A control that keeps its selection off its surface keeps it off its stroke as well.
DisabledThe enabled factor turned over.
TextHoveredThe pointer on the control's own text.
CurrentA container's current item - the one a Shift range grows from.
WindowFocusedHow far the window the control stands in holds the focus. A menu or a tooltip reads the window it stands on.
KeyboardHow far the keyboard drives the input. The same on every control of every window.
MouseHow far the mouse drives the input. The same on every control of every window.
ChannelWhere it starts
SurfaceThe surface the control stands on. A control whose rules move nothing paints no surface of its own.
StrokeThe control's own surface. The focus ring takes it further - see The focus ring.
TextThe ink the control above passes on, in the control's own surface's hue until a rule names one.
ShadowBlack in the stroke's hue, the stroke as it stands before the ring. It is read at the dark end in both modes with no floor, so elevation here is luminosity. A window's blur, offset, spread and opacity are metrics, in WindowShadow.

A button's look under the pointer, as the framework's own theme states it, is two rules: the look gated on the mouse, and the same look for a key that reached the button - see The pointer and the keys.

ColorRule{
    .inputs{ RuleInput::Hovered },
    .andInputs{ RuleInput::Mouse },
    .effect{
        { ColorRuleOp::Offset, 0.085609f },     // S
        { ColorRuleOp::Offset, 0.126926f }      // E
    }
},
ColorRule{
    .inputs{ RuleInput::Focused },
    .andInputs{ RuleInput::Keyboard },
    .effect{
        { ColorRuleOp::Offset, 0.085609f },     // S
        { ColorRuleOp::Offset, 0.126926f }      // E
    }
}

Where a control's rules come from

ThemeColors::rules is a ThemeRules: a list per element, and three lists that reach across elements. A control takes its lists in this order, each applied over what the one before it left:

ListReaches
The control's ownThe control that states them through AdjustPaintEvent::setOwnRules.
of(UiElement)Every control wearing that element.
anyWindowA form's root control, by its position, whatever element it wears. A root that wears none takes this list all the same.
sharedEvery control that wears an element. The Themes app calls it Any element.
focusRingThe own stroke of every control that takes input, read after the rest - see The focus ring.

A control wears an element in one of two ways. A RichControl takes it as a property, beside its metric block. A control that decides for itself states it while its paint is adjusted, and what it states there has the last word.

// As a property.
Panel& m_card{ m_stack.add<Panel>(
    themeMetrics().page,
    UiElement::Section
) };

// From the control's own adjustment.
void Badge::adjustPaint(AdjustPaintEvent& event)
{
    RichControl::adjustPaint(event);
    event.setColorRules(UiElement::ToolButton);
}

A control that wears no element takes its own rules and, at a form's root, the window's. Neither an element's list nor the shared one reaches it.

A control with something to say that no theme can - a tab taking on the colour of the page it opens - states rules of its own. They are baked rules, BakedColorRule, handed over as a span that has to outlive the paint: a member of the control, or a rule of the baked set the pass carries.

// A member, so the span outlives the paint.
BakedColorRule m_pageRule{ .inputs{ RuleInput::Hovered, RuleInput::Selected } };

void Tab::adjustPaint(AdjustPaintEvent& event)
{
    ...
    m_pageRule.effect.setExactHsl(m_actualPageHsl, event.lightness());
    event.setOwnRules({ &m_pageRule, 1 });
}

The pointer and the keys

Hovered means the pointer is over the control, and only the form's hit test moves it. A control the user reached by a key is focused, not hovered, and reads through Focused and the keyboard. So a look that answers the user being on a control is stated twice: when hovered, and mouse, and when focused, and keyboard. With one of the two controllers on, one item is lit.

Keyboard and Mouse are the two input controllers' factors. The pointer turns the mouse on and the keyboard off, a key does the opposite, and a bare modifier turns the keyboard on and leaves the mouse as it is - so both are on only after a modifier. Each fades on an animation slot of its own, the two mirrored so that a switch is one crossfade. Nothing knows which controls read them, so every visible window repaints for the length of the fade. The Platform manual says where the switches come from.

The focus ring

The ring is the control's own stroke, taken further by the theme's focusRing list - Focus ring in the Themes app. The list reaches every control whose Interactivity is not None, whatever element it wears, and it is read after the stroke's own lists. Only its Stroke rules are read, and only for the control's own stroke: a line a control raises through PaintEvent::strokeRgbOn - a grid cell's, a divider row's - does not take the ring.

How far the list reaches the stroke is how much ring there is, and that share sets the ring's weight as well as its colour. The border thickens toward ThemeMetrics::focusedBorder by it, and a surface that grows in as it arrives is held at full size by it, since a ring marks the control's frame and not its surface. Both are paint effects, and neither moves any layout. The shadow takes its hue from the stroke before the ring, so a focused control does not tint the shadow it casts.

The framework's own theme states four rules:

RuleInk
when focused, and keyboardFull contrast - no saturation, elevation 1.
when currentFull contrast. A container's current item wears the ring under the mouse as well, since it marks where a Shift range grows from.
when hovered, and mouse and current and window focusedThe accent.
when focused, and keyboard and window focusedThe accent.

So the ring is drawn in the accent only while its window holds the focus. The accent's values are written into these rules, so a theme that moves its accent moves the ring by editing them too.

Elements

A UiElement names what a control is to the theme. Three elements open a window of their own, and every other stands on one - the colour a rule changes is the one its base leaves. A window root's surface names its hue and sets its saturation and elevation outright, since what stands behind a window is not the theme's to know.

ElementStands onWhat wears it
Dialog, Menu, TooltipThe bare colour of the modeThe root of a window of that kind: its surface, its ink, its frame's stroke and the shadow it casts.
PageDialogA page of content.
TabPageA tab in a strip. The open tab wears its page's surface, and its line runs the length of the page.
SectionPageA section of a page.
SectionHeaderSectionThe strip an expander shows its title on. A grid draws a section's line from its stroke.
ToolBarPageA tool bar.
DialogTitleDialogA window's title bar. Its Selected rules follow the window's focus.
DividerSectionA divider. Its stroke is the line a grid's divider row carries.
GridSectionA grid. Its stroke is the grid's outer border.
GridHeaderGridThe row of column names across the top of a grid. Its stroke is the lines between them.
GridRowGridEvery row of a grid, groups and sections included. Its stroke is the lines between cells.
ButtonSectionA button with a surface at rest.
ToolButtonSectionA button whose surface arrives with the pointer - a tool bar's, a menu line, a list item. It states no surface at rest.
SelectedTextSectionThe band behind selected text, and the ink drawn over it.
SelectionIndicatorSectionThe box of a check and the ring of a radio button, with the colour each takes when on.
HoverIndicatorSectionThe mark of a button whose IndicatorVisibility is Hover, with nothing at rest.
Accent, SpotSectionThe two effects an ink names, ThemeColors::accent and spot.
ScrollButton, ScrollThumbSectionA scroll bar's buttons and its thumb.
Testee, BesteeSectionTest subjects nothing in the framework wears. Only the rules a theme states for them reach them.

Whether a control carries a surface at rest is its element's to say: ToolButton states no rule at rest, so its surface exists only as far as its states carry it. How that surface arrives is geometry, and geometry is a metric. ControlMetrics::surfaceGrowInScale is the size it grows in from, as a fraction of the size it settles at: 0.83 in ThemeMetrics::toolButton, and 1 - a surface that fades in at full size - everywhere else.

Do and Don't

Do

  • Name the ink, resolve where you paint. The same Ink is right on every surface; a Color is right on one.
  • Use a grade for anything grey. Grey is the ladder between the surface and the ink, not a colour.
  • Name a meaning with a semantic colour - InkWell::Red, or redInk(grade) for a quieter one - and leave its tones to InkWell::pigmentTones, the one declaration serving both modes.
  • Take controlContext().inkHsl when an effect comes next, and let the last step be the one that makes a Color.
  • State a look for the user being on a control twice - when hovered, and mouse; when focused, and keyboard. Hovered is the pointer and nothing else.
  • Reach for the accent for interface emphasis and the spot for what stands apart from the interface.
  • Ask wornColorMode which side the application is on. Under Auto the answer is the desktop's, and it moves while the application runs.

Don't

  • Don't convert a produced colour back. Hsl{ someResolvedColor } is a round trip through the lossy direction.
  • Don't cache a resolved colour across surfaces or across a theme change. Resolving is cheap; being wrong is not.
  • Don't give a semantic ink an elevation. An icon that means something means it on both sides of the theme.
  • Don't write a hex literal in a control or an icon. If the model cannot say it, that is a gap in the model.
  • Don't hand setOwnRules a temporary. It keeps a span, and the rules it names are read after the adjustment returns.
  • Don't ask for yellow text. There is no legible yellow; use the shape or the icon to carry it.
  • Don't read the setting as a side. colorMode().get() == ColorModeSetting::Dark is false on a dark desktop under Auto.

How It Works

The resolver

ControlPaintContext::inkHsl is the one place a name becomes a colour. Every ink is read the same way: its colour picks the far end of the mix, and the grade mixes toward that end from the surface. For the accent and the spot the far end is the theme's effect laid over the chain's ink - laid over the mixed ink, an effect that sets its channels outright would answer the same colour at every grade.

// The far end of the mix: a semantic colour's hue, pure black or white, the theme's accent
// or spot effect over the chain's ink, or the chain's ink itself.
Hsl inkEnd = textHsl;
const std::optional<Pigment> pigment = pigmentOf(ink.color);
if (pigment)
    inkEnd = m_bakedColors->pigmentHsl(*pigment, lightness);
else if (ink.color == InkColor::Black)
    inkEnd = { 0.0f, 0.0f, 0.0f };
else if (ink.color == InkColor::White)
    inkEnd = { 0.0f, 0.0f, 1.0f };
else if (ink.color == InkColor::Accent or ink.color == InkColor::Spot)
    m_bakedColors->effectOf(ink.color).applyTo(inkEnd, 1.0f, lightness);

Hsl result;
if (ink.grade == 1.0f)
    result = inkEnd;
else if (ink.grade == 0.0f)
    result = surfaceHsl;
else
    result = Hsl{ surfaceHsl, inkEnd, ink.grade };

if (disabledAmount)
    return Hsl{ result, surfaceHsl, disabledAmount };

Two properties follow from the shape. A semantic colour, black and white read nothing of the chain but the surface they fade toward, so at Strongest each is the same colour under every element in the form. And the disabled fade travels in the colour model like every other move here - an ink that cannot be used slides toward the surface it stands on, round the hue wheel rather than through grey.

The transform

A theme's transform shapes the elevation axis its rules are read on, and the Themes app edits it on the Palette page of its Design view. It states one value, ThemeColors::darkModeFloor: the luminosity elevation 0 is lifted to at the dark end. There the axis runs from the floor to white rather than from black - the luminosity an elevation names is taken to floor + (1 - floor) * luminosity - so every elevation stays reachable, elevationOf stays the inverse of luminosityOf, and each step a theme states is scaled by 1 - floor.

The floor is taken by 1 - lightness. Light mode is left as it is, and a crossing between the modes fades the floor in or out. With the dark end lifted, one design reads right at both ends, which is why a theme states no mode of its own.

Every elevation a theme states is lifted - surfaces, strokes, effects and text rules alike - so a check, a caret, a focus ring and accented text rise with the surfaces around them. Text stands near the top of the axis, where the lift barely moves it: the ink on a dark page still reaches white, and only an ink set low, such as the one over a check mark, lands on the floor rather than on black. The floor lifts elevations only: saturations and a window's shadow are read without it.

A colour below the floor has no elevation and clamps to the end it lies past. The floor itself lies below 1, where the axis would have no room left, and the Themes app's slider stops at 0.5.

The chain a control carries

PaintEvent accumulates the chain on the way down. Each control applies its own rules to what it was handed, and what it passes on is what its children measure against. The channels are resolved in one order, because each seeds the next:

  1. The surface: the Surface rules over what the control stands on - a host's answer where one was stated through AdjustPaintEvent::setSurfaceHsl, the parent's surface otherwise. A control whose rules move nothing contributes nothing to it.
  2. The ink: the parent's ink, in this surface's hue as far as neither a rule nor a host has named one, with this control's Text rules applied. A Text rule raising saturation alone tints toward what the text is on.
  3. The stroke: this control's own surface with its Stroke rules applied, then the focus ring's list where the control takes input.
  4. The shadow: the parent's shadow, in the hue of the stroke before the ring as far as no rule has named one, with the Shadow rules applied.

The band behind selected text and the indicator are resolved when asked for - selectionRgb and indicatorRgb on the ControlPaintContext - since most controls never ask. The band reads the inputs of the control drawing it, which owns the selection. The indicator reads none of them, so a control's own state does not reach its caret or a tab's indicator.

All manuals Top