libLuna Documentation
PL EN
GitHub

The look as data

Themes & tokens

In libLuna the look is not scattered across widgets. It lives in Theme: the palette, spacing, radii, typography and control metrics. Changing a theme means swapping data — not rewriting components.

Three layers of appearance

Variant

ThemePreference: System, Light or Dark. Chooses the base palette.

Accent

Accent: 9 colours. Replaces colors.accent and accent_fg.

Metrics

Metrics: per-widget heights, padding and sizes. Geometry is a token too.

Theme in code

rust · building a theme
use libluna::prelude::*;

// Two ready-made variants
let light = Theme::light();
let dark = Theme::dark();

// The same theme with another accent
let purple = Theme::light().accented(
    Accent::Purple.color(),
    Accent::Purple.foreground(),
);

// User preference + system state → a concrete theme
let theme = ThemePreference::System.resolve(true, Accent::Teal);
Theme fieldTypeContains
colorsColorSchemeLight or dark palette.
spacingSpacing3/6/12/18/24 spacing scale.
radiusRadius6/9/12/pill radii.
typographyTypographyStyles: title, heading, body, caption.
metricsMetricsGeometry for 32 widget groups.
appearanceAppearanceLight / Dark.

Palette

Token names are shared by both variants; only the values differ. Below is the light palette:

window_bg #fafafa
view_bg #ffffff
card_bg #ffffff
headerbar_bg #f2f2f2
sidebar_bg #ebebeb
text #2e2e2e
accent #3584e4
error #c01c28
warning #e5a50a
success #2ec27e

In the dark theme window_bg is #242424, card_bg is #303030 and text is #ffffff. The values come from ColorScheme::dark().

Accent colours — try them here

Clicking changes this page's accent (and stores it in localStorage). In an app, Frame::set_accent(...) does the same. The chosen accent feeds into Theme::accented.

rust · accent from an app
for accent in Accent::ALL {
    println!("{} → {:?}", accent.name(), accent.color());
}

frame.set_accent(Accent::Purple);
frame.set_preference(ThemePreference::Dark);

Spacing, radii, typography

Spacing

  • xs — 3 px
  • sm — 6 px
  • md — 12 px
  • lg — 18 px
  • xl — 24 px

Radius

  • small — 6 px
  • medium — 9 px
  • large — 12 px
  • pill — 999 px

Typography

  • title — 20 px / 700 / 26
  • heading — 15 px / 700 / 20
  • body — 14 px / 400 / 20
  • caption — 12 px / 400 / 16

FontStyle

  • size — logical px
  • weight — 400 / 500 / 700
  • line_height — line height
  • monospace — fixed-width face
rust · tokens while drawing
let pad = theme.spacing.md;
let radius = theme.radius.large;
let title = theme.typography.title;

scene.fill_rounded_rect(rect, radius, theme.colors.card_bg);
scene.text(
    TextRun::new(Point::new(rect.x() + pad, rect.y() + pad + title.size),
                 "Title", title.size)
        .with_weight(title.weight)
        .with_color(theme.colors.text),
);

Control metrics

Widgets do not hard-code heights. They read them from theme.metrics, so a style can change the title bar height or switch size. A few standard values:

GroupKeyDefault
headerbarheight / control38 / 30 px
buttonheight / pad_x34 / 14 px
switchwidth / height44 / 24 px
slidertrack_height / knob_r6 / 9 px
entryheight / pad_x34 / 12 px
tabsheight / min_width34 / 120 px
itemviewtile_w / tile_h100 / 76 px
toastheight / margin_bottom40 / 40 px
containergap8 px

The complete list of 32 groups is in the API index and rustdoc.

Styles from .theme files

You can also install a look the way GNOME/KDE do. The format is line-based with dotted keys; unknown keys are ignored (forward compatibility).

theme · nord.theme
__name__ = Nord
appearance = dark
accent = teal

colors.card_bg = #3b4252
colors.text = #eceff4
colors.window_bg = #2e3440
colors.sidebar_bg = #2e3440

radius.small = 5
radius.large = 10
metrics.button.height = 36
metrics.headerbar.height = 48
metrics.switch.width = 48
DirectoryPriority
~/.config/luna/themeshighest
~/.local/share/luna/themesmedium
/usr/share/luna/themessystem
LUNA_THEMES_DIRextra directories (dev/tests)
rust · ThemePatch
use libluna::core::{available_themes, ThemePatch};

for style in available_themes() {
    println!("{} ({})", style.name, style.id);
}

let patch = ThemePatch::from_file(path)?;
let theme = patch.build(Theme::light());

// or on an existing theme
let mut theme = Theme::dark();
patch.apply(&mut theme);

A style controls not only colours but geometry: metrics.* changes control sizes, while spacing.* and radius.* change padding and corner radii.

Changing the theme from an app

Frame holds the current ThemeSettings and saves changes immediately. The next frame receives the new theme.

rust · theme switcher
if clicked_light {
    frame.set_preference(ThemePreference::Light);
}
if clicked_dark {
    frame.set_preference(ThemePreference::Dark);
}
if clicked_auto {
    frame.set_preference(ThemePreference::System);
}

if clicked_purple {
    frame.set_accent(Accent::Purple);
}
if clicked_custom_style {
    frame.set_style(Some("nord".into()));
}

Settings go to ~/.config/luna/theme.conf. In System mode darkness comes from LUNA_SYSTEM_DARK (later via the D-Bus portal).