OFFSET偏移
← All articlesOFFSET / FIELD NOTESNO. 004

From assets to candidate window: the complete dynamic theme pipeline.

Connect Fcitx5 nine-slice assets, layout, and blur masks to Matugen colors and Nix runtime directories.

English translation · Code and commands follow the Chinese original.

IN THESE READING PATHSWho is working beneath the desktop? ↗
About 9 min readStart reading

A wallpaper-responsive Fcitx5 theme needs at least three things to work together: stretchable assets, colors generated into the right files, and an input method that reads that runtime configuration. Fixing only the rounded corners or changing only the palette can leave half the problem unresolved. Follow the assets, templates, and runtime directories to understand the entire chain.

Which Files Lie Between the Wallpaper and the Candidate Window?#

In the mellow-matugen theme project, for example, Matugen generates a color scheme from an image, templates write the colors into the theme configuration and highlight assets, and ClassicUI reads the generated theme. Nix and Home Manager supply packages, templates, and a writable runtime theme directory; they do not replace Matugen's per-image color generation. [1]

图片 → Matugen 调色方案
              ├─ theme.conf.tpl → theme.conf
              └─ highlight.svg.tpl → highlight.svg
                                      ↓
                        Fcitx5 ClassicUI 选择并读取主题

Nix 包 / Home Manager → 静态素材、模板、可写运行目录

In the theme directory, theme.conf defines the layout and asset references; panel.svg supplies the panel background; highlight.svg draws the candidate highlight; and blur-mask.svg supplies the blur-region mask. Files in the template directory are generation inputs. Fcitx ultimately reads the generated configuration and assets; it does not expand template placeholders for you.

This project dynamically generates highlight and text colors while keeping some backgrounds and masks static. Other themes may divide these responsibilities differently; this project's structure is not a mandatory format for all Fcitx themes.

Which Layer Do Images, Colors, and Layout Parameters Control?#

A ClassicUI theme's InputPanel and Menu describe the candidate panel and menu respectively. The following excerpts from the candidate-panel configuration show how the fields relate. They are not a complete standalone theme; image files and the other required fields still need to be supplied.

[InputPanel]
HighlightCandidateColor=#ffffff
HighlightColor=#ffffff

[InputPanel/Background]
Image=panel.svg

[InputPanel/Background/Margin]
Left=14
Right=14
Top=14
Bottom=14

[InputPanel/Highlight]
Image=highlight.svg

[InputPanel/ContentMargin]
Left=9
Right=9
Top=7
Bottom=7

[InputPanel/TextMargin]
Left=9
Right=9
Top=6
Bottom=7

For background drawing, first distinguish between an image and a configured color. When Image is set, the corresponding background primarily follows the image-asset rendering path. Changing Color or BorderColor does not automatically rewrite the SVG's fill. This is a common reason why “the template colors changed, but the window looks the same”: the edit did not reach the file actually controlling those pixels. [2]

Text colors also have specific targets. The selected candidate's text color must be designed together with its highlight background. Ordinary panel text, candidate highlights, and menu highlights are not all the same layer simply because their names sound similar. When checking the configuration, find fields by following the actual object being drawn.

Nine-slice Scaling Determines What Can Stretch#

Enlarging a background image does not simply scale the entire image proportionally. Nine-slice scaling divides it into four corners, four edges, and a center: corners retain their shape, top and bottom edges stretch horizontally, left and right edges stretch vertically, and the center scales in both directions. Related rules also apply to highlight backgrounds. [3]

Schematic of the slicing mechanism; not drawn to pixel scale.

The renderer understands slice boundaries; it does not automatically recognize rounded corners in the image. If a curve extends into a stretchable region, part of that curve will also be elongated. SVG's smooth scaling does not exempt it from these slicing rules. Gradients, strokes, and shadows in the asset also need to respect the boundaries.

30 Minus 15, Minus Another 15#

Suppose the loaded asset measures 30 × 30 and the slice margin on each side is 15. The remaining center dimensions are:

源中心宽度 = 30 − 15 − 15 = 0
源中心高度 = 30 − 15 − 15 = 0

This is no longer a “very narrow center”; it is a center with no area. In a public issue, an Fcitx maintainer pointed out that these parameters were unreasonable and suggested reducing each margin by 1. The value 14 corrects this particular set of dimensions; it is not a universal number for every rounded-corner asset. [4]

四边设为 14:
源中心 = (30 − 14 − 14) × (30 − 14 − 14) = 2 × 2

若目标为 100 × 40:
目标中心 = (100 − 14 − 14) × (40 − 14 − 14) = 72 × 12

The stretchable region now has a real area to sample. The source cited here skips source slices whose width or height is zero or negative. A zero-area case should therefore be described as a degenerate slice, rather than assuming it causes “division by zero.” Geometry tells you whether the parameters are reasonable; the final border appearance still depends on the asset and the rendered result. [2]

For SVG, also check the intrinsic dimensions obtained by the loader. viewBox defines an internal coordinate system; it cannot simply be treated as the pixel width and height used here. The loading path examined in this article reads intrinsic pixel dimensions. Basing every calculation on 30 just because the viewBox says 30 × 30 may mean using the wrong data from the outset.

Three Types of Margin, Three Different Responsibilities#

FieldPrimary RoleSymptom to Check First
Background/MarginDefine the asset's slice boundariesDistorted rounded corners or borders
ContentMarginAllocate space around the content areaContent too close to or too far from the panel edge
TextMarginContribute to text-item layout and dimensionsSpacing around text and the relationship between highlight and text

These three sets of numbers may happen to be identical, but they are not interchangeable. Repeatedly increasing background slice margins to move text inward may fail to fix the layout and instead make the center degenerate. Candidate panels and menus also have their own parameters. After fixing the panel, inspect the menu separately. [3]

Color Templates Must Cover Both Backgrounds and Text#

Matugen's color roles are paired: for example, primary can be used for an accent background, with on_primary for its content. Changing only the highlight background while retaining the old text color can easily destroy contrast when switching between light and dark schemes.

The following two template excerpts belong to different files. SVG path data has been omitted, so this excerpt cannot be used as a complete SVG file.

# theme.conf.tpl:选中候选的文字色
HighlightCandidateColor={{colors.on_primary.light.hex}}

# highlight.svg.tpl:高亮素材中的填充色
<path fill="{{colors.primary.light.hex}}" d="..." />

Pin the light theme to light and the dark theme to dark to generate a matched pair in one run. default follows the mode selected for that run. If both files use default when you intend to maintain one light and one dark theme, both outputs may use the same mode. The current project templates use explicit branches, retaining the existing structure for shapes and non-dynamic layout. [5]

When registering outputs, do not register only theme.conf and forget highlight.svg. The four TOML sections below register two outputs for each of the two themes. Every /path/to/... is an absolute-path placeholder to replace with the actual template location and writable runtime theme directory.

[templates.fcitx5-light-theme]
input_path = "/path/to/templates/mellow-matugen/theme.conf.tpl"
output_path = "/path/to/themes/mellow-matugen/theme.conf"

[templates.fcitx5-light-highlight]
input_path = "/path/to/templates/mellow-matugen/highlight.svg.tpl"
output_path = "/path/to/themes/mellow-matugen/highlight.svg"

[templates.fcitx5-dark-theme]
input_path = "/path/to/templates/mellow-matugen-dark/theme.conf.tpl"
output_path = "/path/to/themes/mellow-matugen-dark/theme.conf"

[templates.fcitx5-dark-highlight]
input_path = "/path/to/templates/mellow-matugen-dark/highlight.svg.tpl"
output_path = "/path/to/themes/mellow-matugen-dark/highlight.svg"

Matugen's template registrations render the same color scheme into each output. The following example generates colors from an image; prepare the configuration and writable output directories first: [6]

matugen image /path/to/wallpaper.png -m dark -t scheme-content

After generation, inspect the files first: does theme.conf still contain brace-delimited placeholders, has the fill color in highlight.svg actually changed, and do the light and dark outputs use the correct roles? Having the right colors in the configuration and having Fcitx load that configuration are still two separate checks.

Immutable Inputs and Writable Outputs Need Separate Locations#

A Nix package can install themes and templates into the store as fixed inputs, but Matugen cannot write dynamic results directly into the immutable store. The Home Manager module prepares writable copies of the runtime themes, while template inputs can continue to come from files supplied by the package.

The following fragment assumes the project is already a flake input named fcitx5-matugen-theme and that inputs has been passed to the relevant Home Manager module. It demonstrates the project's interface and does not automatically perform system activation.

# 已将项目作为 flake input,并将 inputs 参数传给模块时
imports = [ inputs.fcitx5-matugen-theme.homeManagerModules.default ];
programs.fcitx5-matugen = {
  enable = true;
  themeSet = "both";
  style = "blur";
  installMatugenTemplates = true;
  installRuntimeThemes = true;
};

ClassicUI then needs to select the actual theme directory name. The following configuration-key excerpt should be merged into your existing managed configuration; it contains no additional section heading.

Theme=mellow-matugen
DarkTheme=mellow-matugen-dark
UseDarkTheme=True

In the project version pinned for this article, each Home Manager activation recopies the runtime themes. Writable does not therefore mean that the last generated result is retained: after activation, regenerate the dynamic colors according to your workflow. Without understanding this overwrite relationship, you may mistake “activation refreshed the assets” for random Matugen failures. [7]

Also distinguish between a theme actually being generated, the configuration actually pointing to it, and the running ClassicUI actually loading it. Multiple sources with the same theme name, light/dark theme options, and update timing can all make the on-screen result differ from the file you just inspected.

Blur Masks Inherit the Asset's Geometric Constraints#

When constructing the blur-mask configuration, the relevant source copies the background configuration, then substitutes the mask image and clears the overlay image. In other words, background slice margins also participate in mask processing; the mask is not an arbitrary second image exempt from background geometry rules. Check the asset dimensions and rounded-corner regions together. [2]

The mask describes the region where blur is requested. The final result also depends on compositor support, the relevant protocols, and user configuration. Neither a transparent panel nor the existence of a mask file alone proves that desktop blur is working. Inspect theme-image rendering and compositor blur separately.

Locate Problems by Output, Then Inspect the Entire Candidate Window#

SymptomCheck First
Abnormal rounded corners or bordersIntrinsic dimensions, slice boundaries, and whether the center has an area
Incorrect text positionTextMargin, ContentMargin, and the separate panel and menu parameters
Text color changes, but the highlight stays the sameWhether highlight.svg is registered, generated, and read by the current theme
Generation fails with a write errorWhether the output points into the store and whether the directory is writable
Generated files are correct, but the screen is unchangedSelected theme, light/dark options, and loading time
Original colors return after activationWhether the runtime copy was refreshed and regeneration is needed
Transparency without blurMask geometry, compositor support, and configuration

Visual acceptance testing should cover short and long candidates, different scale factors, light and dark themes, and menus. Code inspection can rule out invalid slices and missing outputs, but it cannot replace actual rendering. The nine-slice diagram in this article illustrates the mechanism; it is not a runtime screenshot.

One maintenance detail is easy to miss: non-dynamic layout fields duplicated in the base configuration and light/dark templates need to be updated together. If you maintain multiple variants over time, you can extract the layout into a shared source and generate each configuration from it. This is a possible way to reduce duplication, not a claim that the current project has already been refactored. Separate the responsibilities of dynamic colors and fixed layout so that later changes do not overwrite one another.

References & further reading

  1. Theme Project Structure and Usage (Pinned Version) ↗
  2. Fcitx ClassicUI Rendering and Mask Source ↗
  3. Official Fcitx 5 Theme Format ↗
  4. Maintainer Explanation of Zero-area Slices ↗
  5. Matugen Templates for the Theme ↗
  6. Matugen Template Registration Example ↗
  7. Theme Home Manager Module (Pinned Version) ↗

Adapted from technical notes. Examples use generic names.

Continue along a reading path

Who is working beneath the desktop?

  1. 01The other side of rwx.
  2. 02After login, who takes over the desktop?
  3. 03From assets to candidate window: the complete dynamic theme pipeline.
CONTINUE READING / NEXT ARTICLEIf configuration is reproducible, why do apps still forget?

From GNOME defaults and Home Manager activation to Flatpak's management scope, distinguish declarations, preferences, and data.