Changelog¶
All notable changes to SKINT are documented here.
One package, one assembly. SKINT ships as a folder you copy into
Assets/, compiled by a single editor-only assembly definition. There is no SKU split, so every entry below applies to every install.To check what you have: there is no version constant yet. Compare this file against the one in your
Assets/Skint/Documentation/folder.
Rules for this document
- Honest, not promotional. Awkwardness and mistakes are recorded alongside the things that worked.
- A pain point is recorded before it is fixed, then the fix is recorded. The order matters. A friction written up only after it was solved reads as a solved problem; written before, it is a record of what this was actually like to use.
- Mechanism, not outcome. Why a defect happened and how it was reachable, not only that it is gone.
- Deliberate absence is named as deliberate, so it is not mistaken for an oversight.
- Behaviour changes that produce no compile error are flagged as such.
- History is never rewritten. A reversed decision gets a new entry explaining the change.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
[Unreleased]¶
The first release since the package was audited, and it is overwhelmingly correctness. Spacing becomes themeable and the designer gains a status line, but those are the smaller half: most of what follows is behaviour that was wrong, found by an audit and a remediation round run against it.
Two of the fixes are the kind that only show up when someone else is watching. SKINT compiled into player builds, and SKINT overwrote a preference belonging to another product on every repaint. Both had been true for the package's whole life.
Read the Migration section at the end even if nothing else here applies to you. Several entries change behaviour with no compile error, and one block of them is a straightforward compile break: public members that existed only to be unreachable have been removed.
Added¶
Spacing is themeable — four tokens drive every padding and margin SKINT draws. panelPadding, panelSpacing, controlPadding and controlSpacing live on the theme beside the colours and font sizes, and appear in the designer under Spacing.
- They are bases, not per-edge values. Twenty hardcoded sites used fifteen distinct padding and margin values between them;
SkintGeometryderives all fifteen from the four with fixed offsets, so the outer panel stays two points wider than the base and a foldout four. Raising one token retunes the layout and keeps the relationships, rather than requiring fifteen values to be re-tuned against each other. - Sixty independent edges is what was not shipped, and that is deliberate. Exposing every edge would have put about sixty integers in the designer, which nobody tunes individually. The cost of the decision is real and is stated rather than glossed: an individual edge can no longer be set in isolation.
- It does not eliminate the magic numbers. It names them, puts them in one file, and turns them into a density control. That is the honest description of what it buys.
- Every token is clamped at zero, because a delta subtracted from a hand-edited or slider-minimum value would otherwise produce a negative
RectOffset, and Unity lays controls on top of each other when it gets one. - Defaults reproduce the previous hardcoded literals exactly, asserted value by value by
GeometryTests. Ten presets ship, and nothing else would have failed if the defaults had drifted — every one of them would simply have looked different, silently.
A Widget Gallery window. Window ▸ Skint ▸ Widget Gallery renders every themed control under the live theme. Previously the same code lived inside the one window alongside the real tool.
The preset block reports what it did. Save, delete, restore and reset write to a status line under the buttons.
- The undo button names the preset it will restore. Which preset the single level of undo was holding was not discoverable anywhere in the UI.
- A second delete warns which preset it has just made un-undoable, and where that file still is. Undo is single level, so the first delete's file was previously recoverable only by knowing to look in
Presets/UIThemes/.Trash, and nothing said so. - Failure dialogs are unchanged, and that is not an oversight. A failed save already raised a dialog and logged a warning. Successes were the silent case — which on a destructive action is the wrong way round, because the one outcome you most want confirmation of was the one giving none.
A WCAG AA contrast guard over all ten shipped presets. Computes WCAG 2.x relative luminance and fails on anything under 4.5:1 for meaningful text. Two exclusions, both on the record: disabled text, which WCAG 1.4.3 exempts, and Pastel Piss and Rab Mode, which exist to demonstrate extremes.
SKINT locates itself at runtime. SkintPaths resolves the install folder from its own assembly definition instead of a compile-time Assets/Skint constant. Installing the folder anywhere else previously broke preset saving and loading with no clear error, and EnsureFoldersExist then created an empty Assets/Skint tree beside the real one — which made the failure look like missing presets rather than a wrong path. This is not about packaging: SKINT ships as a folder copy by design, and customers put folders where it suits them.
81 EditMode tests and a one-command verification script. tools/verify.ps1 runs the suite and a player build and exits non-zero if either fails or if any SKINT assembly reaches the player. tools/install-hooks.ps1 installs it as a pre-push hook.
Fixed¶
SKINT compiled into player builds. Everyone. Sixteen of seventeen source files had no assembly definition, so they landed in Assembly-CSharp — ten of them referencing UnityEditor. An editor-only asmdef (includePlatforms: ["Editor"]) now scopes the package. Measured rather than assumed: the shipped Assembly-CSharp.dll went from 48,128 bytes carrying SKINT symbols to 3,584 bytes carrying none, and the build check now fails if that regresses.
SKINT overwrote another product's preference on every repaint. Everyone, and anyone who also owns Snap Studio Pro. The theme selection was stored under SnapPro_UITheme, a key belonging to Snap Studio Pro. EditorPrefs is global to a Unity installation rather than per project, so this overwrote that product's setting in every project on the machine — and the read path wrote on every repaint, not merely on change. SKINT now writes only keys it owns, and PrefKeyTests asserts across the whole theme path that no SnapPro_* key moves, comparing each as both a string and an int because a key read at the wrong type yields the default and would pass trivially.
Theme edits were discarded at the next domain reload. Editing a colour mutated the live cached instance and nothing persisted it, so every edit survived exactly until Unity recompiled.
The save throttle dropped the last edit of a drag. A plain throttle discards anything arriving during the cooldown, so the final value of a colour drag — the one the user actually chose — was the one most likely to be thrown away. Replaced with a trailing-edge debounce that always retains and writes the final value in a burst: when the cooldown elapses, before a domain reload, on editor quit, or on demand through FlushPendingSave.
The theme did not follow the editor skin. Switching Unity between dark and light left the previous skin's theme cached and drawing. The cache now records which skin it was built for and a watcher invalidates it on change, because a cache built for the other skin is stale regardless of its age.
Five partial cache-clear paths, each forgetting something different. There is now one complete clear. It also drops the generated textures and the folder-label cache, which two of the five did not.
Textures reclaimed by Unity left stale cache entries. Generated textures carry HideFlags.DontSave, so Unity can collect one while the dictionary still holds the reference. Entries are validated on read.
A preset name went straight into Path.Combine, so a name containing a traversal segment wrote outside the presets folder. Names are now validated — path separators, the volume separator, platform-invalid characters, all-dot names and Windows reserved device names — and the resolved path is confirmed to stay inside the folder. Both checks are kept because 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. Spaces remain valid: nine of the ten shipped presets have them.
Every file and JSON operation could throw out of OnGUI or store corruption. An unreadable or malformed preset previously wrote its raw contents straight into EditorPrefs, so the corruption was saved and then failed on every later read instead. All file and JSON work now returns success and an error string, and GetTheme falls back to defaults rather than returning null — which had made every caller throw, since nothing null-checked it.
Deleting a preset was irreversible and overwriting was unguarded. Deletes move to a restorable trash with an undo that survives a recompile, because the undo target is held in EditorPrefs rather than in memory. Overwrites ask first, since nothing brings an overwritten preset back. Deleting the same name twice used to throw rather than trash it, because File.Move rejects an existing destination.
The theme editor sized its columns from the window, not the panel. Content clipped in a docked layout. The drawer now measures the panel it was handed with a zero-height control rect. The measurement is committed at the end of a Repaint pass, never where it is taken — IMGUI runs Layout and Repaint as separate passes over the same code and both must emit the same number of layout groups, so committing at the top of Repaint let the two passes choose different column counts and produced "You are pushing more GUIClips than you are popping" with a corrupted layout. Found by causing it.
Labels were not associated with their controls. Twelve field methods now draw their label through EditorGUILayout.PrefixLabel. ThemedLabel(string, string) is deliberately not converted: it is a label, not a field, and a prefix column would misalign it against everything around it.
Search-match contrast. Re-tuned. The guard that now protects it found thirteen shortfalls across the preset set where the original audit reported four — the audit's four reproduced exactly, so its method was sound and its coverage was not.
Removed¶
The theme selector, and about a hundred unreachable colour literals. SkintUITheme had exactly one member, Custom, so the guard CurrentTheme == SkintUITheme.Custom && selector != null in GetThemeColor was always true and the dark/light fallback arguments beside every selector were unreachable in every build that ever shipped — roughly a hundred colour values maintained in parallel with the theme and never drawn.
It was also a hazard rather than only dead weight. StyleCache.CurrentTheme cast an int straight out of EditorPrefs — global to the installation — and migrated a value across from SnapPro_UITheme. Any out-of-range value there made that guard false, which silently replaced the user's entire theme with those literals and made the designer return early with "This theme is not editable. Switch to 'Custom'" — an option the enum no longer offered. A blank designer, with no way out from the UI.
Stated precisely, because the difference matters: no *_UITheme key exists on the development machine, so this was latent, not live. Whether Snap Studio Pro ever wrote a non-zero value is not established. Removing the selection removes the failure mode either way.
A parallel SkintLite implementation, in its own namespace, with zero references.
Roughly 224 lines of further dead code, including four unused GUIStyleFactory factory methods, three vestigial public members inherited from the parent product, and an eager default-theme seeder superseded by the lazy path in GetTheme.
Changed¶
Upgraded to Unity 6000.3.25f1 from 6000.3.5f2.
Every preset carries all 45 theme fields. A field no preset mentions is a field that loading a preset cannot change — it silently inherits whatever the current theme held, so the preset does not fully describe a look. Two colours previously appeared in no preset at all, and Dark.json was additionally missing four.
StyleCache.cs went from 972 lines to 717, holding one class with one concern. The theme model moved to Core/SkintThemeSettings.cs and five GUIStyle extension methods moved to GUIStyleExtensions.cs, where a file already existed for exactly that purpose.
The development project no longer carries the Unity URP template. Six unused packages, TutorialInfo/, the template readme and the input actions asset are gone. This is the harness, not the shipped package, so it changes nothing you receive — recorded because it changes the verification build, which is now 2.9 MB smaller and four seconds faster.
Internal¶
No behaviour change for consumers. Recorded because these items change what the release gates catch, and because two of them are corrections to claims made during the audit itself.
- The audit's dead-code list named live code. It identified the no-arg
CreateTabHeaderStyle,CreateSectionHeaderStyleandCreateFoldoutLabelStyleinStyleCacheas the dead half of a duplication. They are live — one caller each, from their own getters. The genuinely dead copies were the four inGUIStyleFactory, with zero call sites. Deleting what the audit pointed at would have broken the build. The list was re-derived rather than trusted, because the code had changed substantially since the audit was written. - A count carried through the remediation notes was wrong. The three-argument
GetThemeColorwas recorded as having 53 call sites. It had 17; most call sites already used the one-argument overload. - The test harness would have corrupted the preferences it existed to protect.
EditorPrefsvalues are typed, and the harness backed every owned key up withGetString— which on an int key yields an empty string and writes that back on restore. Owned keys are now split by type.SnapPro_UIThemewas also removed from that list entirely: it is a foreign key, and backing a key up means writing it. - Test fixtures cannot call a style getter. They build from
EditorStyles, which throws "You can only call GUI functions from inside OnGUI" outside a GUI callback. Fixtures that need the theme-resolution path callThemeColorResolver.GetThemeColordirectly, which is the same code the getters reach. - Hosted CI cannot work on this licence, and the workflow says so. game-ci supports three activation strategies: a Pro/Plus serial, a
.ulflicence file, or a floating licence server. Unity 6 Personal uses entitlement licensing and produces no.ulf, soUNITY_EMAILandUNITY_PASSWORDalone yield "License activation strategy could not be determined". The workflow is kept and disabled; verification is delivered by a pre-push hook instead. This is a deliberate choice, not a fallback. - Two Unity behaviours the verification script exists to work around.
-runTestsreturns exit code 0 even when tests fail — a baseline run here returned 0 with five failures — so the results XML is parsed instead. AndBuildSummary.totalErrorsis non-zero on a perfectly good build, so the player check keys onBuildResult. - IMGUI behaviour is verified by hand, because no EditMode test can render it. The designer's column reflow was driven across both breakpoints seventeen times in both directions, and the preset status messages exercised end to end, with the console at zero errors throughout.
Compatibility¶
- Unity 6000.3 LTS (6.3). Supported by Unity until December 2027.
- Editor only. Windows, macOS and Linux editors. Nothing ships in a player.
- No dependencies.
UnityEngine,UnityEditorand the BCL only. No packages, no third-party code, no network access, no telemetry. - Render pipeline irrelevant. SKINT draws IMGUI editor UI and touches no pipeline.
Migration¶
Read this even if nothing above applied to you. Most of this release is correctness work, and a fix to something that behaved wrongly is still a change to what your project does today.
Public members were removed. This is a compile break if you called them. All of them were unreachable, vestigial or inherited from the parent product and undocumented, but removal is removal:
SkintUITheme(the enum),StyleCache.CurrentThemeThemeColorResolver.GetThemeColor(Color dark, Color light, Func<…> selector)— use the one-argument overload; the fallbacks it took were never returnedStyleDrawer.DrawThemeSelectorno longer takesref SkintUIThemeStyleCache.DrawCustomHelpBox,GetThumbnailLabelStyle,GetThumbnailBoxStyle,RefreshAfterThemeChangeThemePresetUtility.DrawPresetDropdown,ThemePersistenceService.InitializeDefaultThemesGUIStyleFactory.CreateToggleStyle,CreateSectionHeaderStyle,CreateFoldoutLabelStyle,CreateTabHeaderStyle— theStyleCachemembers of those names are unaffected- The
SetFont,SetPadding,SetMargin,SetAlignmentandSetWordWrapextension methods moved fromStyleCachetoGUIStyleExtensions. Same namespace, so extension-style calls are unaffected; an explicitStyleCache.SetFont(style, …)will not compile.
These change behaviour with no compile error:
- Theme edits now persist. They previously vanished at the next domain reload. If anything in your workflow depended on the editor forgetting, it will not.
- Changing Unity's editor skin now switches the theme and the preset list with it.
- Deleting a preset now moves it to
Presets/UIThemes/.Trashrather than removing it, and the folder accumulates until you clear it. Deleting the same name twice replaces the older trashed copy, which matches the single level of undo offered. - Saving over an existing preset now prompts. A script-driven save path that expected silence will now raise a dialog.
- Theming works again for anyone it had silently stopped working for. If a non-zero value existed under
Skint_UIThemeorSnapPro_UITheme, SKINT was drawing hardcoded fallback colours instead of your theme and the designer showed nothing but a message. Both keys are now ignored entirely.
Skint_UITheme is left in EditorPrefs rather than deleted. It is no longer read or written. EditorPrefs is shared across projects, and deleting a key on someone's behalf is not this package's business.
Preset files gained four fields. A preset saved by this version carries the spacing tokens. An older SKINT reading one ignores unknown fields and is unaffected; this version reading an older preset supplies the defaults from the class rather than reading zero, so an old preset looks exactly as it always did. That is the mechanism the whole no-migration claim rests on, so it is asserted by a test rather than assumed.
If you removed RevGaming.Skint.Editor.asmdef, SKINT's sources fall back into Assembly-CSharp and start compiling into your player build, where their UnityEditor references will fail. It is not optional scaffolding.
Baseline — 2026-10-02¶
Imported at the audited baseline. The state of the package before the work above; recorded for provenance rather than as a release.