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:
- Text - the ink the paint chain arrived at. All text is this colour at one grade or another.
- The other pigments -
Yellow,Green,BlueandRed, the four meaning-carrying hues the theme supplies beside accent and spot, each drawn at a tone the framework states for either side of the theme. Warnings and errors are these. - Black and white - pure, and the same on either side of the theme.
- Accent and spot - the two pigments named for their role, each one of the theme's own colour effects laid over the text ink.
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
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.
| Ink | Effect | What 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:
| Setting | What the application wears |
|---|---|
ColorModeSetting::Auto | The mode the desktop asks for, followed while the application runs. The first answer and the default: a config that states nothing holds it. |
ColorModeSetting::Dark | Dark, whatever the desktop asks for. |
ColorModeSetting::Light | Light, 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.
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()));
})
};
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.
| Name | Answers |
|---|---|
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::Yellow | Warning. Drawn, never read - see below. |
InkWell::Green | Success, present, on. |
InkWell::Blue | Information, a link, a thing named. |
InkWell::Red | Error, destructive, off. |
InkWell::Black, InkWell::White | Pure 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.
[color yellow] reads, so that a text built from code can be written out and read back,
and is not an invitation.
InkGrade
| Grade | Share | Reads as |
|---|---|---|
InkGrade::Faint | 0.12 | Barely present - a hairline, a disabled mark. |
InkGrade::Subtle | 0.27 | A border, a divider. |
InkGrade::Muted | 0.60 | Secondary text. |
InkGrade::Strong | 0.80 | An object an accent is drawn on top of. |
InkGrade::Strongest | 1.00 | Body 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:
| Colour | Dark tone | Light tone |
|---|---|---|
Yellow | 1.00 / 0.90 | 1.00 / 0.51 |
Green | 1.00 / 0.84 | 1.00 / 0.49 |
Blue | 1.00 / 0.47 | 1.00 / 0.41 |
Red | 1.00 / 0.56 | 1.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.
| Call | Use |
|---|---|
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. |
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.
| Tag | Ink |
|---|---|
[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
| Name | Answers |
|---|---|
ColorModeSetting | Auto, 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:
- The object an icon draws - the sheet, the bin, the arrow's shaft - is a
grade.
InkGrade::Strongis the usual one. - The part that names the command - the front sheet in a copy, the head of an arrow, the trail behind an eraser - is the accent, so the icon says which of its shapes carries the verb.
- A meaning - warning, error, present, absent - is a semantic ink.
- A brand mark - the application's own icon and name - is the spot.
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);
}
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:
| Index | Named by a rule as | What 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.
| Operation | Effect |
|---|---|
ColorRuleOp::NoChange | Leaves the channel as it arrived. |
ColorRuleOp::Offset | Moves it by a distance. A relation between two colours. |
ColorRuleOp::Scale | Multiplies it. |
ColorRuleOp::Set | States 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.
| Member | What it states |
|---|---|
inputs | The inputs the rule reads, joined as "or". An empty set is at rest: the rule applies in full. |
andInputs | What must hold as well, joined as "and" - a surface when focused and the window focused. |
output | The PaintChannel the rule writes. |
effect | The 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.
| RuleInput | What it reads |
|---|---|
Hovered | The pointer over the control, and nothing else. A control reached by a key is not hovered. |
Pressed | A press held on the control. |
Focused | The control's own focus, whether or not its window has it. |
Selected | A checked box, an open tab, a picked item. A control that keeps its selection off its surface keeps it off its stroke as well. |
Disabled | The enabled factor turned over. |
TextHovered | The pointer on the control's own text. |
Current | A container's current item - the one a Shift range grows from. |
WindowFocused | How far the window the control stands in holds the focus. A menu or a tooltip reads the window it stands on. |
Keyboard | How far the keyboard drives the input. The same on every control of every window. |
Mouse | How far the mouse drives the input. The same on every control of every window. |
| Channel | Where it starts |
|---|---|
Surface | The surface the control stands on. A control whose rules move nothing paints no surface of its own. |
Stroke | The control's own surface. The focus ring takes it further - see The focus ring. |
Text | The ink the control above passes on, in the control's own surface's hue until a rule names one. |
Shadow | Black 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:
| List | Reaches |
|---|---|
| The control's own | The control that states them through AdjustPaintEvent::setOwnRules. |
of(UiElement) | Every control wearing that element. |
anyWindow | A form's root control, by its position, whatever element it wears. A root that wears none takes this list all the same. |
shared | Every control that wears an element. The Themes app calls it Any element. |
focusRing | The 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:
| Rule | Ink |
|---|---|
| when focused, and keyboard | Full contrast - no saturation, elevation 1. |
| when current | Full 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 focused | The accent. |
| when focused, and keyboard and window focused | The 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.
| Element | Stands on | What wears it |
|---|---|---|
Dialog, Menu, Tooltip | The bare colour of the mode | The root of a window of that kind: its surface, its ink, its frame's stroke and the shadow it casts. |
Page | Dialog | A page of content. |
Tab | Page | A tab in a strip. The open tab wears its page's surface, and its line runs the length of the page. |
Section | Page | A section of a page. |
SectionHeader | Section | The strip an expander shows its title on. A grid draws a section's line from its stroke. |
ToolBar | Page | A tool bar. |
DialogTitle | Dialog | A window's title bar. Its Selected rules follow the window's focus. |
Divider | Section | A divider. Its stroke is the line a grid's divider row carries. |
Grid | Section | A grid. Its stroke is the grid's outer border. |
GridHeader | Grid | The row of column names across the top of a grid. Its stroke is the lines between them. |
GridRow | Grid | Every row of a grid, groups and sections included. Its stroke is the lines between cells. |
Button | Section | A button with a surface at rest. |
ToolButton | Section | A button whose surface arrives with the pointer - a tool bar's, a menu line, a list item. It states no surface at rest. |
SelectedText | Section | The band behind selected text, and the ink drawn over it. |
SelectionIndicator | Section | The box of a check and the ring of a radio button, with the colour each takes when on. |
HoverIndicator | Section | The mark of a button whose IndicatorVisibility is Hover, with nothing at rest. |
Accent, Spot | Section | The two effects an ink names, ThemeColors::accent and spot. |
ScrollButton, ScrollThumb | Section | A scroll bar's buttons and its thumb. |
Testee, Bestee | Section | Test 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
Inkis right on every surface; aColoris 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, orredInk(grade)for a quieter one - and leave its tones toInkWell::pigmentTones, the one declaration serving both modes. - Take
controlContext().inkHslwhen an effect comes next, and let the last step be the one that makes aColor. - State a look for the user being on a control twice - when hovered, and mouse; when focused, and keyboard.
Hoveredis the pointer and nothing else. - Reach for the accent for interface emphasis and the spot for what stands apart from the interface.
- Ask
wornColorModewhich 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
setOwnRulesa 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::Darkis 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:
- 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. - 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.
- The stroke: this control's own surface with its Stroke rules applied, then the focus ring's list where the control takes input.
- 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 lightness is inherited. A control that knows better states it outright
through
AdjustPaintEvent::setLightness. - The disabled amount is applied as the chain is built, so an ink asked for on demand matches one that was resolved ahead of time.
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.