Class Theme

java.lang.Object
limn.components.Theme

public final class Theme extends Object
Central design tokens: colors, corner radii, typography and spacing. Components read everything from the theme: zero hardcoded colors in widgets. Ships with a set of built-in palettes (see 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.

  • Field Details

    • MAX_CORNER_SCALE

      public static final float MAX_CORNER_SCALE
      The largest 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.
      See Also:
    • spacingSmall

      public final float spacingSmall
      See Also:
    • spacingMedium

      public final float spacingMedium
      See Also:
    • spacingLarge

      public final float spacingLarge
      See Also:
    • animEasing

      public final Easing animEasing
      Default timing curve for state transitions.
    • animHover

      public final double animHover
      Hover fade in/out (buttons, tabs, combo, rows).
      See Also:
    • animFocus

      public final double animFocus
      Focus-ring fade in/out.
      See Also:
    • animFade

      public final double animFade
      Generic show/hide and value fades: the progress bar, a chart's entry animation, and the check and dot marks. Deliberately not the scrollbar, which declares its own four durations because its reveal is asymmetric (it appears fast and leaves slowly, after a hold), and no single token can say that.
      See Also:
    • animTab

      public final double animTab
      Selected-tab indicator slide (TabbedPane).
      See Also:
    • animExpand

      public final double animExpand
      An Expander opening and closing: its arrow turning and its content sliding out.
      See Also:
    • animWindow

      public final double animWindow
      Whole-window show/hide fade (native dialogs, popups, floating windows).
      See Also:
  • Method Details

    • elevation

      public ElevationShadow elevation(Elevation level)
      The shadow this palette gives a surface lifted to a level.
      Parameters:
      level - the level
      Returns:
      its shadow; ElevationShadow.NONE for Elevation.FLAT
    • drawElevation

      public void drawElevation(Canvas canvas, RoundRect shape, Elevation level)
      Draws the shadow of a surface lifted to a level, under where the surface will be painted: call it before the surface's own fill. A widget that does adds elevation(level).reach() to its paintOutset(), so a repaint covers what the shadow touches.
      Parameters:
      canvas - the canvas
      shape - the surface's outline
      level - how far it is lifted
    • light

      public static Theme light()
      The built-in light palette.
    • dark

      public static Theme dark()
      The built-in dark palette.
    • limn

      public static Theme limn()
      The project's own palette: a deep violet canvas with a vivid violet accent, tuned so that body text, muted text, the accent's own label and the focus ring each clear WCAG AA on the surface they land on.
    • limnLight

      public static Theme limnLight()
      The light companion to limn(), held to the same contrast targets.
    • displayName

      public I18nString displayName()
      The name a picker shows, in the UI language; 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.
    • builtins

      public static List<Theme> builtins()
      Every palette that ships with the toolkit, in presentation order.
    • current

      public static Theme current()
      Returns:
      the process-wide active theme
    • of

      public static Theme of(Widget<?> widget)
      The palette 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().

      Parameters:
      widget - the widget about to paint
      Returns:
      the palette it paints with
    • followSystemContrast

      public static void followSystemContrast(boolean follow)
      Whether the palette every widget paints with follows the system's request for more contrast: true 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.
      Parameters:
      follow - whether to follow
    • followsSystemContrast

      public static boolean followsSystemContrast()
      Returns:
      whether the palette follows the system's request for more contrast
    • contrast

      public boolean contrast()
      Whether this is a contrast palette: one made for a user who asked the system for more contrast, or the shipped High Contrast. A component draws a few things otherwise in one: no shadow, every control with its edge, an opaque backdrop.
      Returns:
      whether it is
    • apply

      public CompletionStage<Boolean> apply()
      Switches to this palette and shows it: makes it current at once, which repaints every window (setCurrent(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();
      Returns:
      the stage 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 answers
      Throws:
      IllegalStateException - off the UI thread, or when no UiRuntime is installed
    • name

      public String name()
      Returns:
      this palette's name
    • isDark

      public boolean isDark()
      Returns:
      whether this is a dark palette
    • background

      public Color background()
      Returns:
      this palette's background
    • surface

      public Color surface()
      Returns:
      this palette's surface
    • surfaceRaised

      public Color surfaceRaised()
      Returns:
      this palette's surfaceRaised
    • primary

      public Color primary()
      Returns:
      this palette's primary
    • primaryText

      public Color primaryText()
      The accent as body-size text: 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(). 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.

      Returns:
      the accent, adjusted only as far as text needs
    • controlBorder

      public Color controlBorder()
      The edge that identifies a control: a field's box, a check box, a radio button, a secondary button, a segmented control's track, a slider's rail, a scroll bar's thumb, a split pane's divider. 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.

      Returns:
      the outline, darkened in a light palette or lightened in a dark one only as far as a control's edge needs
    • primaryHover

      public Color primaryHover()
      Returns:
      this palette's primaryHover
    • primaryPressed

      public Color primaryPressed()
      Returns:
      this palette's primaryPressed
    • onPrimary

      public Color onPrimary()
      Returns:
      this palette's onPrimary
    • text

      public Color text()
      Returns:
      this palette's text
    • textMuted

      public Color textMuted()
      Returns:
      this palette's textMuted
    • outline

      public Color outline()
      Returns:
      this palette's outline
    • focusRing

      public Color focusRing()
      Returns:
      this palette's focusRing
    • disabledFill

      public Color disabledFill()
      Returns:
      this palette's disabledFill
    • disabledText

      public Color disabledText()
      Returns:
      this palette's disabledText
    • scrim

      public Color scrim()
      Returns:
      this palette's scrim
    • danger

      public Color danger()
      Returns:
      this palette's danger
    • success

      public Color success()
      Returns:
      this palette's success
    • warning

      public Color warning()
      Returns:
      this palette's warning
    • info

      public Color info()
      Returns:
      this palette's info
    • cornerScale

      public float cornerScale()
      Returns:
      this palette's cornerScale
    • focusRingReach

      public float focusRingReach()
      How far past a control's box the focus ring drawn under this palette reaches, in logical points: the widest ring in the toolkit, a Strokes.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.

      Returns:
      the reach in logical points
    • fontFamily

      public String fontFamily()
      Returns:
      this palette's fontFamily
    • body

      public Font body()
      Returns:
      this palette's body
    • label

      public Font label()
      Returns:
      this palette's label
    • title

      public Font title()
      Returns:
      this palette's title
    • tokens

      public final SizeTokens tokens(ControlSize size)
      Metric tokens for a size step. The returned row (and the Fonts 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.

    • tokensFor

      public final SizeTokens tokensFor(Widget<?> widget)
      Tokens for the step resolved on widget, the one line components call.
    • setCurrent

      public static void setCurrent(Theme theme)
      Switches the process-wide palette, repaints every scene and tells whoever is listening, and does not change the font. A theme picker, a settings screen and a switch between light and dark want 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.

    • observeChanges

      public static Subscription observeChanges(Runnable listener)
      Subscribes to palette changes: the fifth process-wide axis, with the same handle the other four hand back.

      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.

      Parameters:
      listener - what to run when the palette changes
      Returns:
      a handle that unsubscribes; cancelling it twice is a no-op. UI thread
    • applyFontFamily

      public CompletionStage<Boolean> applyFontFamily()
      Has 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(). 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.

      Returns:
      a stage 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
      Throws:
      IllegalStateException - off the UI thread, or when no UiRuntime is installed
    • builder

      public static Theme.Builder builder(String name, boolean dark)
      A palette of this application's own, seeded from the built-in light() 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();
       
      Parameters:
      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 avoid
      dark - whether the palette is dark, which decides the seed and which tones the derive methods produce
    • toBuilder

      public Theme.Builder toBuilder()
      This palette's tones, in a builder: the form to start from when an application wants a built-in with two things changed rather than a palette of its own.

      The theme is unaffected: a Theme is immutable and the builder holds a copy.

    • equals

      public boolean equals(@Nullable Object other)
      Two palettes are equal when they carry the same name, mode, shape, typeface preference and colours, which is everything a palette is, and everything 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.

      Overrides:
      equals in class Object
    • hashCode

      public int hashCode()
      Overrides:
      hashCode in class Object
    • toString

      public String toString()
      The palette's name. A theme picker shows displayName(), not this.
      Overrides:
      toString in class Object