Skip to content

Theming from Code

Everything the designer does is available programmatically.

Reading and writing the theme

using RevGaming.Skint;

SkintThemeSettings theme = ThemePersistenceService.GetTheme();
theme.headerText = Color.cyan;
ThemePersistenceService.SaveTheme(theme);          // coalesced, always lands
ThemePersistenceService.FlushPendingSave();        // force it now, if you need to

GetTheme() returns the live cached instance for the current editor skin. It never returns null — a missing or corrupt stored theme falls back to the built-in defaults and writes them, rather than leaving callers to null-check.

SaveTheme() coalesces bursts. Dragging a colour does not write a preference once per frame. Unlike a plain throttle, the final value in a burst is always retained and written — when the cooldown elapses, before a domain reload, on editor quit, or when you call FlushPendingSave() yourself.

SetCustomTheme() replaces the whole theme in one call:

ThemePersistenceService.SetCustomTheme(ThemePersistenceService.GetPureFlatNoirDefaults());

Presets

string[] names = ThemePresetUtility.GetAvailablePresets();
SkintThemeSettings loaded = ThemePresetUtility.LoadPreset(names[0]);
ThemePresetUtility.SavePreset(theme, "My Preset");

string folder = ThemePresetUtility.GetActiveThemeFolder();   // follows the editor skin

LoadPreset returns null if the file is missing, locked or corrupt rather than throwing — check it before use.

Reading a single colour

Color header = ThemeColorResolver.GetThemeColor(t => t.headerText);
SkintThemeSettings current = ThemeColorResolver.CurrentThemeSettings;

Invalidating cached styles

After changing a theme outside the designer:

StyleDrawer.ApplyLiveStyleRefresh();

Safe to call from OnGUI — it defers the rebuild through delayCall rather than doing it mid-pass, which would pull the styles out from under the layout that is reading them.

Colours take effect on the next repaint without this. Font sizes and spacing do not — they are baked into the cached GUIStyle objects, so they need the refresh.

Preset types of your own

ThemedPresetBlock is generic, so you can reuse SKINT's preset UI for your own serialisable settings type:

_result = ThemedPresetBlock.Draw<MySettings>(
    selectedIndex: _result.SelectedIndex,
    nameInput: _result.NameInput,
    presetDirectory: myFolder,
    trashDirectory: myTrashFolder,
    presetNames: _names,
    loadFunc: path => /* read and return MySettings, or null */,
    /* … */);

If your type implements IPresetSerializable<T>, the block saves CloneForSave() instead of the live object — which is what you want when the in-memory instance holds transient state that should not reach the file:

public class MySettings : IPresetSerializable<MySettings>
{
    [NonSerialized] public bool IsDirty;

    public MySettings CloneForSave()
    {
        var copy = (MySettings)MemberwiseClone();
        copy.IsDirty = false;
        return copy;
    }
}

File IO

SkintIO is SKINT's file layer, and it is public because the preset callbacks you supply will want it. Every method returns success and an error string rather than throwing or logging, so the caller decides whether a failure is a dialog, a status line or silence:

if (SkintIO.TryReadJson(path, out MySettings loaded, out string error))
    return loaded;

Debug.LogWarning($"[MyTool] {error}");
return null;

Validating a preset name

If you take a preset name from a user, validate it before building a path:

if (!SkintPaths.TryResolvePresetPath(folder, name, out string full, out string error))
{
    // `error` is already phrased for a user
    return;
}

This checks the name and confirms the resolved path stays inside the folder. Both matter: name validation alone relies on having enumerated every way a name can traverse, while comparing the resolved path asks the filesystem where the write would actually land.

Next