primary
- Returns:
- this palette's
primary
builtins()):
light()/dark(), the project's own limn() and
limnLight(), plus
ones drawn from familiar editor palettes (Darkling, Draculite, Nordic,
Arch Dark, Onyx Dark, Monoko Pro, Grovebox Dark, Solaris light/dark,
Octo Light, High contrast).
Switching one is one call: theme.apply(), which makes the palette
current and repaints every window, the colour each clears to included, and switches to the
palette's font once that font's faces are in memory. A widget reads the palette it paints with
through of(Widget), never through a field.
An application can build its own. builder(String, boolean) starts from
a working palette and toBuilder() starts from this one, so a palette of your own
costs only the tones that make it yours; Theme.Token enumerates them so that
code treating them alike does not have to name all of them.
What a palette carries is colour, plus one metric: cornerScale, how
round the corners are. Spacing and typography stay process-wide and identical in every
palette; see tokens(limn.scene.ControlSize) for why that boundary falls exactly where it does, and why
a radius is the one metric allowed across it. A palette also answers one number it does not
choose, focusRingReach(): how far the focus ring reaches past a control, which is what
every layout declares by default for the children it does not clip.
static final class static enum final Easingfinal doubleExpander opening and closing: its arrow turning and its content sliding out.final doublefinal doublefinal doublefinal doublefinal doublestatic final floatcornerScale a palette may ask for.final floatfinal floatfinal floatapply()setCurrent(limn.components.Theme)), and switches the UI font to its fontFamily once that
family's faces are in memory (applyFontFamily()).Font.DEFAULT_FAMILY resolve to this palette's fontFamily once that
family's regular and bold faces are in memory: the act that makes a palette's type
preference visible, and the font half of apply().body()static Theme.Builderbuiltins()booleancontrast()floatstatic Themecurrent()danger()static Themedark()name stays the
identifier.voiddrawElevation(Canvas canvas,
RoundRect shape,
Elevation level) booleanThemeFormat writes.floatStrokes.FOCUS_RING pen centred
Strokes.FOCUS_GAP_BUTTON outside a button's edge, which is
Strokes.FOCUS_RING_OUTSET, 3 points.static booleanstatic voidfollowSystemContrast(boolean follow) true unless an application said otherwise.inthashCode()info()booleanisDark()label()static Themelight()static Themelimn()static Themelimn(), held to the same contrast targets.name()static SubscriptionobserveChanges(Runnable listener) static Themewidget paints with: the one resolver every component reads its colours
through.outline()primary()primary() where it already reads as text on every
surface, and otherwise primary moved toward text() in steps of one hundredth only
until it clears 4.5:1 on background(), surface() and
surfaceRaised().scrim()static voidsetCurrent(Theme theme) success()surface()text()title()final SizeTokenstokens(ControlSize size) final SizeTokenswidget, the one line components call.toString()warning()cornerScale a palette may ask for. Past this every control in
the toolkit is already a pill (a radius beyond half the shorter side is capped when
it is drawn), so a higher number would change nothing and only look like it might.Expander opening and closing: its arrow turning and its content sliding out.level - the levelElevationShadow.NONE for Elevation.FLATelevation(level).reach() to its paintOutset(), so a repaint covers what the
shadow touches.canvas - the canvasshape - the surface's outlinelevel - how far it is liftedlimn(), held to the same contrast targets.name stays the
identifier. Only the descriptive palettes translate (Light,
Dark, High Contrast); the rest are the names of the palettes
they came from and read the same everywhere.widget paints with: the one resolver every component reads its colours
through. A widget answers the process-wide current(), except a widget printed in a
limn.print.Document, which answers the document's palette; the point of asking
through the widget is that a palette per subtree can be added behind this call, as the
locale, the direction and the control size already resolve per subtree, without any
component changing. A static on this class rather than a method on
Widget, because limn.scene names no limn.components type.
While the system asks for more contrast (SystemPreferences.increaseContrast()),
a widget on screen answers the contrast palette of current() instead: the user's
own colours where the platform gives a palette (a Windows contrast theme), and otherwise
current() strengthened, with text at 7:1 and every edge, ring and accent at 4.5:1 on
its surfaces. current() is unchanged, and followSystemContrast(boolean)
refuses it. A printed page keeps its document's palette. Whoever reads colours out of this
palette to keep them hears the contrast palette come, go or change through
observeChanges(java.lang.Runnable), as it hears a switch of current().
widget - the widget about to painttrue unless an application said otherwise. With false an
application keeps its palette while the user has asked for more contrast, which leaves it
to bring its own contrast mode: following the platform's colour and contrast settings is
what EN 301 549 clause 11.7 asks of software. Every window repaints, and the
observeChanges(java.lang.Runnable) listeners are told. UI thread.follow - whether to followsetCurrent(limn.components.Theme)), and switches the UI font to its fontFamily once that
family's faces are in memory (applyFontFamily()). The call a theme picker or a
settings screen makes, and the one to make before the first window opens, so that the
first frame is already in this palette's colours. UI thread.
The colours now, the font once it is resident. A palette is a repaint and never a re-measure, so its colours are in force from the next frame. Its font is a relayout, and a family the operating system supplies may first need its regular and bold faces read from disk, tens of megabytes for a CJK face. That read happens on a worker and the switch is made on the UI thread when it lands, so no frame of a theme picker reads a font file; until then text stays in the family it was in. A family that needs no read, the toolkit's own face (which every built-in palette names) or one whose faces are already in memory, switches on the UI thread's next turn, which for a call made from an input handler comes before the next frame is drawn. A palette whose family is already the default switches nothing and lays nothing out again.
The later choice wins. A font switch this call is still waiting to make is
overtaken by the next apply or applyFontFamily(), and by
Fonts.setDefaultFamily(java.lang.String) or Fonts.setDefaultFamilyWhenResident(java.lang.String), and then
switches nothing, so a picker stepped through palettes ends on the last one's font and a
family never lands after the user has moved on, including when the later palette names the
toolkit's own face.
When the font has to be in force as the call returns, as for a capture or a test
that draws next, or an application whose first frame must already be in the palette's
font, follow this with Fonts.setDefaultFamily(theme.fontFamily()): it switches at
once and overtakes the switch this call is waiting to make, and the faces are then read on
the UI thread by the first measure that needs them. Waiting on the returned stage before
drawing works too, where the UI thread is free to run until it completes.
With a runtime and no backend, as under a headless test's, nothing can be read and the font switches on the runtime's next turn: a test that measures in the palette's font drains the runtime first, or switches synchronously as above. Without a runtime this throws, as the palette switch always has.
Every scene whose application chose no background with Scene.setBackground(limn.graphics.Color)
clears to this palette's background() from its next frame on, in every window,
those opened later included. One that chose a colour keeps it: a scene that clears to
transparent does so to let the desktop through, which is not a palette's decision to
overrule.
Theme.limnLight().apply();Fonts.setDefaultFamilyWhenResident(java.lang.String) answers: completed on the UI
thread, on a later turn and never inside this call, with true once this
palette's family is the default family, or false when a later choice
overtook it; it does not complete if the backend shuts down first. The palette has
switched before the call returns, whatever the stage answersIllegalStateException - off the UI thread, or when no UiRuntime is installednamebackgroundsurfacesurfaceRaisedprimaryprimary() where it already reads as text on every
surface, and otherwise primary moved toward text() in steps of one hundredth only
until it clears 4.5:1 on background(), surface() and
surfaceRaised(). Derived, not a token of the theme file, so a palette of your own
gets it without a key to set, and a primary that passes is returned as it is.
It is the colour a tertiary button writes its caption
in and Tone.ACCENT draws text in. A fill keeps primary(): the bar there is
3:1 for a filled shape, which primary is solved for, and text on a fill is
onPrimary(). Moving toward the text colour keeps the accent's hue and gives up some
of its saturation: lighter in a dark palette and darker in a light one.
outline() where it already clears 3:1 on background(),
surface() and surfaceRaised(), and otherwise outline moved toward
text() in steps of one hundredth only until it does: WCAG 2.1 1.4.11 asks 3:1 of
what identifies a control and its states. Derived, not a token of the theme file,
as primaryText() is.
outline() stays the colour of what groups or divides without being a control (a
card's edge, a divider between rows, a table's header rules, a popup's edge), which 1.4.11
asks nothing of, and which every shipped palette draws at about 2:1 so that it stays quiet.
primaryHoverprimaryPressedonPrimarytexttextMutedoutlinefocusRingdisabledFilldisabledTextscrimdangersuccesswarninginfocornerScaleStrokes.FOCUS_RING pen centred
Strokes.FOCUS_GAP_BUTTON outside a button's edge, which is
Strokes.FOCUS_RING_OUTSET, 3 points.
It is what a layout declares for its children unless it is told another number. A
row, a column, a padding and every other layout that does not clip lets what its children
draw outside themselves show outside it too, and partial rendering knows that only if the
layout says so: a repaint pass skips a subtree whose box, grown by what its root declares it
paints outside itself, misses the pass. So each layout declares this reach, and a control
whose ring touches the layout's edge keeps the ring when only the band just outside the
layout repaints, a neighbour's hover or a caret beside it. A layout whose children reach
farther, or never reach past it, says so with its paintOverflow.
The same number for every palette, because the ring's pen and gap are locked rather than
tabled and no palette chooses them, so neither the builder nor the saved format carries it.
It is asked of the palette, through of(Widget), so that the layouts follow the theme
in effect for them rather than a constant of their own. Nothing measures from it: it moves
which pixels a frame repaints and never a box, so, like the corner scale, it costs a palette
switch no relayout.
fontFamilybodylabeltitleFonts inside it)
are stable for the life of this palette, so == holds and the backend's
identity-keyed font memo keeps hitting. Never build tokens per call, and never call
Font.withSize to derive a step's font.
Every metric here is palette-independent except the corner radii, which
follow cornerScale. At the default scale this returns the process-wide row
itself, so a palette without a shape of its own is indistinguishable from what shipped
before shape existed.
Why the radii are allowed out and nothing else is. No scene re-measures on
setCurrent(limn.components.Theme) -- each repaints, and none measures again (see there) -- so a palette
that could change a metric something measures from would
change every measurement in every window with no relayout behind it. A radius is the one
metric nothing measures from: no onMeasure, onLayout,
baselineOffset or paintOutset in this toolkit reads one, which
ThemeShapeTest asserts by measuring a tree at two shapes and demanding the same
numbers. Break that and shape stops being safe, not just the test.
Two further traps stand in the way of any metric that is measured from. The
six float token fields above are inlined at their call sites (JLS 13.1), so a
per-palette value cannot reach a field read at all, which is exactly why the three
radius fields are deprecated rather than made to follow the scale. And the nine
token-backed fields read the static MEDIUM row, so they would be invisible to every
unmigrated call site. If per-palette type or spacing is ever wanted, both
prerequisites are required: something has to answer observeChanges(java.lang.Runnable) with a
relayout() for every live scene, which is a decision about what every palette
switch costs, and those fields have to be initialized from this.tokens(MEDIUM) in
the constructor.
widget, the one line components call.apply(), which is this plus the font. This alone is the call for a colour
well that switches the palette on every frame of a drag, where asking for the font again on
every frame is not wanted: such a screen asks with applyFontFamily() only on the
frames where the family moved. UI thread, when a user-interface runtime is installed.
Every scene repaints, without being handed over. A scene subscribes to the change
as it subscribes to the four measurement axes, through limn.scene.internal, since
Scene is limn.scene, the layer this package is built on, and names no type
of this one. Its clear colour follows too, unless its application chose one with
Scene.setBackground(limn.graphics.Color). Until 2026-10-18 the scenes were the caller's to repaint, one
by one, and the clear colour was copied out of the palette when a scene was built: a window
opened after a switch, or never handed to apply(), drew this palette's widgets on the
previous palette's canvas.
The answer is a repaint and not a re-measure, and that is the restriction that
keeps a palette switch cheap enough for a colour well to make one on every frame of a drag.
A palette carries colours and corner
radii, and nothing in this toolkit measures from a radius -- no onMeasure,
onLayout, baselineOffset or paintOutset reads one, which
ThemeShapeTest asserts by measuring a tree at two shapes and demanding the same
numbers. A palette's type preference is not carried here either: it reaches the tree
through applyFontFamily(), which is Fonts' axis and does relayout.
A listener runs when the palette widgets paint with may have changed: when
setCurrent(limn.components.Theme) (or apply()) switches current(), when
followSystemContrast changes, and when the system's request for more contrast
changes the contrast palette of(limn.scene.Widget<?>) lays over current(): turned on or off, or a
Windows contrast theme's colours changed while it is followed. Once for each such change;
a change of the system's preferences that leaves of(limn.scene.Widget<?>)'s answer as it was (the
appearance, less motion, a request the application refused or made while current()
is itself a contrast palette) runs nothing. Until 2026-10-25 only the first two were told,
so a colour read out of the palette and kept stayed in the old one when the user turned
contrast on.
listener - what to run when the palette changesFont.DEFAULT_FAMILY resolve to this palette's fontFamily once that
family's regular and bold faces are in memory: the act that makes a palette's type
preference visible, and the font half of apply(). It is
Fonts.setDefaultFamilyWhenResident(java.lang.String) with this palette's family, so the faces are
read on a worker and the frame that shows the new font reads no file for them; an italic
face, not read ahead, is still read where it is first drawn. Nothing changes inside this
call.
Separate from setCurrent(limn.components.Theme) on purpose, and the separation is the contract:
switching a palette is a repaint, while switching a font is a relayout of every scene and,
the first time a family is named, a read of its faces. This call keeps that read off the UI
thread, but it still costs a stage, a post and a lookup in the backend's faces each time,
and a relayout whenever the family moves. Folding it into setCurrent would put
those into every palette change, including the ones a colour well makes on every frame of
a drag; such a screen calls setCurrent on each frame and this only when the
family moved.
A palette that expresses no preference carries Font.DEFAULT_FAMILY, so calling
this for one restores the toolkit's own face rather than leaving the previous palette's
choice in place, and overtakes a switch an earlier palette or a font picker is still
waiting to make: the later choice wins, as Fonts.setDefaultFamilyWhenResident
says. For a family that is already the default, with nothing waiting, it switches nothing
and notifies no listener, so nothing is laid out again. Both directions are therefore safe
to call unconditionally.
For a switch that has to be in force when the call returns, as a capture or a test that
draws next needs, call Fonts.setDefaultFamily(theme.fontFamily()) instead: it
switches at once, and the first measure in the new family reads its faces on the UI
thread, inside that frame. With a runtime and no backend, nothing can be read and this
switches on the runtime's next turn.
UI thread.
true once this palette's family is the default family, or
false when a later choice overtook it; it does not complete if the backend
shuts down firstIllegalStateException - off the UI thread, or when no UiRuntime is installedlight() or
dark() so that every tone is already a working one and only what makes the
palette distinct has to be spelled out.
Theme mine = Theme.builder("Ocean", true)
.background(Color.rgb(0x0B1A24))
.surface(Color.rgb(0x11242F))
.surfaceRaised(Color.rgb(0x1A3340))
.primary(Color.rgb(0x4FD1C5))
.onPrimary(Color.rgb(0x04141A))
.deriveAccentStates()
.deriveDisabled()
.build();
mine.apply();
name - the palette's identifier and its fallback display text; see
Theme.Builder.name, which explains why a name shared with a built-in
is a name to avoiddark - whether the palette is dark, which decides the seed and which
tones the derive methods produceThe theme is unaffected: a Theme is immutable and the builder holds a
copy.
ThemeFormat writes.
The metrics do not appear here because they are process-wide: only the corner scale and
the family name are a palette's to choose, and both are compared.
Here so that an editor can ask "has this been changed" and a parser can be held to a round trip. The toolkit itself still compares the active palette by identity.
displayName(), not this.