MyDesktopWidget.SkinContract 3.25.0

dotnet add package MyDesktopWidget.SkinContract --version 3.25.0
                    
NuGet\Install-Package MyDesktopWidget.SkinContract -Version 3.25.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="MyDesktopWidget.SkinContract" Version="3.25.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="MyDesktopWidget.SkinContract" Version="3.25.0" />
                    
Directory.Packages.props
<PackageReference Include="MyDesktopWidget.SkinContract" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add MyDesktopWidget.SkinContract --version 3.25.0
                    
#r "nuget: MyDesktopWidget.SkinContract, 3.25.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package MyDesktopWidget.SkinContract@3.25.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=MyDesktopWidget.SkinContract&version=3.25.0
                    
Install as a Cake Addin
#tool nuget:?package=MyDesktopWidget.SkinContract&version=3.25.0
                    
Install as a Cake Tool

MyDesktopWidget.SkinContract

Reads and validates the two package formats MyDesktopWidget accepts — a skin and a theme — covering the manifest models, the readers, and every bound that decides whether one is valid.

These are the desktop engine's own readers, published so that anything else handling these packages applies exactly the rules the engine applies when it loads one. A second implementation drifts silently, and the failure lands on authors: a site that accepts packages which do not work, and rejects packages which do.

What it does

using MyDesktopWidget.Skins;
using MyDesktopWidget.Themes;
using MyDesktopWidget.Ambient;

// Both readers take the file's *text*, never a path: this library does no file access at all, so
// the caller decides where the bytes came from and applies its own limits before reading them.
var skin = SkinManifestReader.Read(skinJsonText);

if (!skin.Success)
{
    foreach (var error in skin.Errors)
    {
        Console.WriteLine(error);
    }
}

var theme = ThemeManifestReader.Read(themeJsonText);

if (!theme.Success)
{
    foreach (var error in theme.Errors)
    {
        Console.WriteLine(error);
    }
}

// An ambient scene - the full-screen idle display - is the third package type, added in 2.2.0.
var scene = AmbientSceneReader.Read(sceneJsonText);

if (scene.Scene is null)
{
    foreach (var error in scene.Errors)
    {
        Console.WriteLine(error);
    }
}
else
{
    // Valid, and separately: is it findable once published? Never refuse on these.
    foreach (var gap in AmbientSceneReader.DescribePublishingGaps(scene.Scene))
    {
        Console.WriteLine(gap);
    }
}

Resolving {t:} in a name

A manifest's name and description may carry {t:key} tokens, resolved against the widget's own lang/*.json. Anything displaying a widget's name has to resolve them the same way the engine does, or the same widget shows a different name in each place:

// The tables you read out of the package's lang/ folder, keyed by tag as the filename gives it.
var merged = SkinText.Merge(byTag, requestedTag: "pt-BR");
var title  = SkinText.ResolveNames(manifest.Name, merged);

Merge applies the engine's order — en → en-US → the neutral tag → the requested tag, each overwriting the last — so pt.json carries the language and pt-BR.json only what differs. A missing key resolves to the key itself, deliberately: an author who mistypes one sees it on screen. {sensor:} and {time:} are left verbatim, braces included, because a name is not a render target.

Both functions are pure. Reading lang/*.json out of an archive is the caller's — this library opens no file. And the desktop engine calls exactly these, so the two cannot drift.

Valid is not the same as publishable

Success means the package is one the engine would load or apply. That is deliberately not the question of whether it is fit to list, and the two have separate answers:

foreach (var gap in SkinManifestReader.DescribePublishingGaps(skin.Manifest))
{
    Console.WriteLine($"{gap.Code}: {gap.Message}");
}

category and version are both optional. A skin declaring neither is entirely valid and renders correctly — and is then missing from any list filtered by category, and can never be offered an update. Nothing in Validate reports it, because refusing a package that works would be wrong and would reject every package written before those fields existed.

DescribePublishingGaps returns exactly those cases, for a skin and for a theme. What you do with them is your policy — block the upload, warn, or prompt — but the rule and its wording are shared, so an author is told the same thing by the site and by the application.

And valid is not the same as visible

A third question, and the same rule applies to it: everything it reports is a package that loads and renders.

foreach (var gap in SkinManifestReader.DescribeRenderingGaps(skin.Manifest))
{
    Console.WriteLine($"{gap.Code} at {gap.Target}: {gap.Message}");
}

It exists for one case in particular. A widget's window is created at exactly the size its manifest declares, and an outer shadow is drawn outside the element — so the canonical card, a shape at 0,0 filling the widget, has nowhere to put one. Measured on a 320×200 widget: a square full-bleed card keeps 0 of 64,000 shadow pixels and a rounded one keeps 0.5%, in the notches its own corners leave. The manifest is valid, the widget renders, and the author sees a feature that appears to do nothing.

It is conservative on purpose. A partly-clipped shadow is not reported — a card flush to the top of a widget with its shadow falling below it is ordinary design — and a repeat child is measured against the widget rather than the group, because nothing clips a group.

Nothing in either gap list may become a validation error. Refusing a package that renders correctly apart from a flourish is the opposite of what this reader is for.

Gap codes, and the guarantee they come with

Since 3.0.0 every gap is a PackageGap — a code, a target and the message — rather than a bare sentence. The code exists so a consumer that owes more than one language has something stable to key a translation off; the message exists so one that meets an unfamiliar code still has something true to show a person.

gap.Code     // "element.shadow.no-room" — frozen once published
gap.Target   // "elements[2]", or "" when the gap is about the package itself
gap.Message  // the whole English sentence, naming the target and carrying any measured numbers

The vocabulary is open, and an unrecognised code must be treated as a gap you do not know about — never as an error. That is the contract this type is issued under and it binds both sides. Codes are strings rather than an enum precisely so that a code added in a later engine is unknown to an older consumer rather than unparseable; a value that will not parse in the middle of a stored document fails the whole read, which is a far worse failure than one unshown sentence. New codes are additive and will not carry a major version.

So do not drop Message. It is the fallback that makes an unknown code survivable, not a transitional courtesy, and it is what the engine itself writes to engine.log.

Target is separate from the prose deliberately, and appears in both. If you replace the English sentence with your own translation you still have to say which element — and the only place elements[2] appears in a sentence is the sentence you are replacing. It stays inside the message too, because that message has to stand alone for anyone who has no translation for the code.

PackageGapCodes.All enumerates what this version publishes, which is a convenience for building your translations. It is not a validity test: a newer engine will have more.

The rule above still holds, and a code makes it easier to break. A gap with a machine-readable identifier looks a great deal more like a rule than a sentence does. It is not one. Nothing carrying a gap code may ever become a validation error.

Skin validation covers the manifest schema and element types, colours, gradients and named theme colours, style classes, conditional rules, repeat groups, click actions, skin versions, and the bounds that make a third party's package safe to read — file sizes, image dimensions and entry containment.

Theme validation covers the theme schema, the id, the category, the colours a theme substitutes, the wallpaper it names — including the containment and extension rules that decide whether a path is safe — and the widgets it places.

Scene validation covers the schema version, the name and its length, the optional id, the background colour, each placed skin's id, its position and its scale, the canvas the coordinates were written against, and the inline elements array — which is checked through SkinManifestReader, so a scene's inline elements are held to exactly the rules a skin's are.

A scene's identity is optional, deliberately. id, description and schemaVersion were added in 2.3.0 and none of them is required, because every scene already on a user's disk was written without them. A missing id or description is reported by AmbientSceneReader.DescribePublishingGaps rather than refused; an absent schemaVersion reads as the current one. A marketplace is free to block an upload on a gap — that is what the gap is for — while the engine reads the file and carries on. An id that is present must satisfy the same rules a skin's does.

One question a scene raises that this library cannot answer. A scene names catalog skins by id, and whether those ids exist is your catalog's business, not the format's. Read bounds an id's shape and stops there. Treat a missing reference as a warning rather than a refusal: the engine draws such a scene with that skin absent rather than rejecting the file, so blocking the upload would refuse something that works.

What it deliberately does not do

It does not render anything, it does not read or write a running widget's state, it does not manage the folders scenes and themes live in, and it does not apply a theme. Applying one rewrites each skin's own skin.json, moves windows and changes the desktop wallpaper; that belongs to the engine and is not here. The library targets net10.0 and references nothing but the base class library, which is a property its tests enforce rather than a coincidence.

Versioning

This package versions the skin package format, not the application. A bump here means the format moved; the application ships on its own cadence and its version says nothing about this one.

3.1.0 adds tint and greyscale to image, animatedImage and frameStrip — declared on ISkinTintable — plus a tint on SkinRule. Purely additive: an older reader ignores both and validates the skin, and CurrentSchemaVersion stays 4. It also adds the first two gap codes since the vocabulary was published, rule.tint.not-a-picture and rule.tint.multi-frame, which is the open-vocabulary guarantee doing what it was designed for.

3.0.0 is an API break and not a schema break, which is an unusual combination here and worth stating plainly: CurrentSchemaVersion stays at 4, no manifest field changed, and no package means anything different than it did. All three describers return IReadOnlyList<PackageGap> where they returned IReadOnlyList<string> — see Gap codes above. If you call any of them you must recompile; you do not need to re-validate anything you have already accepted. Console.WriteLine(gap) still prints the sentence, because PackageGap.ToString() is the message.

2.0.0 is the first release that is not additive. ShapeElement.CornerRadius changed from double to SkinCornerRadius, because cornerRadius now accepts a number or an array of four — top-left, top-right, bottom-right, bottom-left, the order CSS uses. If you only read, validate and describe packages, you will not notice: nothing on Read, Validate, DescribePublishingGaps, DescribeRenderingGaps, SkinText or ThemeManifestReader touches that property. If you read element fields directly, that one moved.

Everything else in 2.0.0 is additive: shadow on shape and image, strokeDash, cornerRadius and shadow on image, stroke on text, radial gradients, a gauge cap, and DescribeRenderingGaps.

Licence

MIT, and it covers this library only. MyDesktopWidget itself is a commercial application published under separate terms — see the LICENSE file for exactly where the line falls.

https://mydesktopwidget.com

Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • net10.0

    • No dependencies.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
3.25.0 95 9/19/2026
3.24.0 96 9/19/2026
3.23.0 90 9/19/2026
3.22.0 91 9/19/2026
3.20.0 100 9/18/2026
3.19.0 89 9/18/2026
3.18.0 96 9/18/2026
3.17.0 106 9/6/2026
3.11.0 96 8/28/2026
3.10.0 99 8/28/2026
3.9.0 104 8/28/2026
3.8.0 103 8/28/2026
3.7.0 98 8/28/2026
3.6.0 103 8/28/2026
3.5.0 104 8/27/2026
3.4.0 103 8/27/2026
3.2.0 101 8/27/2026
3.1.0 101 8/27/2026
3.0.0 99 8/27/2026
2.12.0 102 8/27/2026
Loading failed

3.25.0 (2026-09-19) - SkinManifest gains zIndex, and IT ADDS A REFUSAL, so
the site wants this before an author can publish one. CurrentSchemaVersion stays 5.

zIndex orders a widget against the others IN THE SAME hostMode. Higher is nearer the front, the
default 0 is what every widget had before, and ties keep load order - so a library where nobody
writes one is unchanged. Asked for by the owner: the ability to draw a skin above the wallpaper but
below other widgets, which was previously whatever order the shell happened to give and could differ
between runs.

THE REFUSAL is a value outside -1000..1000. Nothing breaks at a larger number - the engine sorts, it
does not index - but an unbounded field invites 999999 as a way of saying "definitely on top", which
stops working the moment a second author does it. The message says what the field CAN do rather than
only the bound, because the number is rarely the author's real mistake.

IT CANNOT CROSS LAYERS, AND THAT IS WINDOWS RATHER THAN A CHOICE. A wallpaper widget is a child
window of the shell's wallpaper host and an overlay is a top-level window; they are not siblings, so
no value sorts one against the other. A large negative zIndex on an overlay puts it behind the other
overlays and does not push it onto the wallpaper layer. The reference says so in section 2.4 with
both directions spelled out, because it is the thing an author will expect it to do.

NO SCHEMA VERSION. An older engine ignores the property and draws the order it drew before, so a
widget carrying one loses nothing on an older install - which is why this is a refusal to adopt
rather than a version to gate.

3.24.0 (2026-09-19) - ONE NEW PUBLIC TYPE, no schema change, no new refusal,
CurrentSchemaVersion stays 5. Purely additive: a consumer that ignores it behaves exactly as on
3.23.0.

BundledScenes NAMES THE THREE AMBIENT SCENES EVERY INSTALLATION SHIPS - Big Clock, Matrix Terminal
and Live Desktop - with their ids, the default, and IsOne(name) for the ignore-case comparison the
engine actually uses. Asked for by the website, and it is the SensorIds argument a third time: a
theme's ambientScene names a scene BY NAME, the commonest correct case is a theme naming one of
these three, and recognising that case required hard-coding three literals of ours in their
repository where nothing could check them. Their listing page told an author's customer that a theme
"names a scene called 'Big Clock', which is not part of this download" - true, mildly alarming, and
exactly wrong about the one case needing no action.

THE IDENTITIES TRAVEL AND THE DESIGNS DO NOT, which is the boundary. A preset's canvas, its placed
skins and its scale are content the engine draws, and this package carries no rendering - so what
crosses is the id, the name and which one is the default. "Is this name one we ship?" is answerable;
"what does it look like?" still needs the product.

IsOne COMPARES ORDINAL-IGNORE-CASE where the engine's own lookup is culture-sensitive, deliberately:
the three names are ASCII so no culture disagrees about them, and a catalogue that answered
differently under a Turkish locale would be the worse failure by far.

THE DRIFT THIS PREVENTS HAD ALREADY HAPPENED HERE. AmbientScenePresets opened with "Two, and they are
chosen to show the two ends of what a scene can be" while its list returned three - a preset shipped
and the sentence describing the set did not move, inside the file that defines the set. That is the
failure the website predicted for their copy, one repository earlier, with nothing at stake but a
comment. The engine now builds its presets from this list and a guard holds the two to the same set,
so a fourth cannot arrive without this file moving.

3.23.0 (2026-09-19) - DOCUMENTATION ONLY. No code, no API change, no new
refusal, CurrentSchemaVersion stays 5. Two corrections to what section 11 says about a theme's
ambientScene, both asked for by the website while building their half of it.

WHAT THE NAME MATCHES IS NOW STATED, because a reader cannot infer it and a marketplace has to. It
matches an installed scene's `name`, exactly, ignoring case; a scene has no id it is keyed by, and
the engine turns the name into the folder it saves under. THE NAME IS A LITERAL AND IS NEVER A {t:}
TEMPLATE - nothing under ambient.json resolves one - so anything matching it after resolving it is
matching a string the engine never uses. That was found as a live divergence: the website stores an
Ambient listing's title as Resolve(name, languages), which for a scene whose name carries a token
produces a string no engine will ever match. The three presets' ids are given too (big-clock,
matrix-terminal, live-desktop) with the rule that a theme names a scene BY NAME and never by id.

AND THE SECTION DESCRIBED THE OLD BEHAVIOUR. It said applying such a theme "says nothing at the
time", which stopped being true the same day 1.1.44's install report gained a sentence for it. The
engine.log line it named is still real and is still the one that arrives late or never, since
Ambient Mode is off by default - which is why the install reports it as well. A document describing
the release before the one it ships in is the failure 3.21.0 was cut for.

NOTE FOR ANYONE TRACKING VERSIONS: 3.21.0 WAS NEVER PUSHED TO THE FEED. Measured 2026-09-19, the
flat container holds 33 versions and is missing eight - 2.10.0, 3.3.0, 3.12.0 through 3.16.0, and
3.21.0. Its content is inside 3.22.0, so nothing is lost; but a PackageReference on a version that
was never pushed restores the nearest one above it and says nothing, so read the flat container
rather than a release note when deciding what exists.

3.22.0 (2026-09-19) - DOCUMENTATION ONLY, and none of it is about skins.
No code, no API change, no new refusal, CurrentSchemaVersion stays 5; a consumer on 3.21.0 is running
correct code. It takes a number for 3.21.0's reason, which is the only reason this package ever moves
for prose: the documents travel INSIDE it, the website renders them straight out of the restore, and
editing bytes under a number already handed over mints a second 3.21.0.

launcher-schema-reference.md's `locked` row said "The user may not move or remove this tile" and was
wrong twice over. It named TWO of the five operations the flag governs - `resize`, `re-point` and
`displace` were absent - and it stated a GUARANTEE where the format offers advice: this reader
validates documents and not edits, so nothing can enforce a lock and anybody who can open the JSON
can clear it. The row now names all five and says which of the two it is, with the reasoning for the
two that are easy to leave out: re-pointing a tile is the cheap route to what `remove` already
refuses, and a move that displaces a locked tile is refused because a lock that only resists a direct
action is not a lock.

FOUND FROM THE OTHER SIDE, not from here. The Android launcher counted its own call sites and
reported that MyDesktopWidget.LauncherContract's own doc comment disagreed with itself; this document
turned out to be further out than either copy, and nobody had reported it because a table cell
describing a flag is read by authors and by no test. LauncherContract 2.4.1 carries the other half,
and LockedNamesTheSameOperationsTests now holds the two files to the same set of operations - they
ship in two different packages, so nothing else reads both.

3.21.0 (2026-09-18) - DOCUMENTATION ONLY. No code, no API change, no new
refusal and CurrentSchemaVersion stays 5; a consumer on 3.20.0 is running correct code. What moves
is the authoring documentation this package carries, which the website renders straight out of the
restored package rather than copying - so a reference that lags the reader is an author writing what
the published document describes and being refused for a rule it never stated. It had lagged two
releases. widget-schema-reference.md now documents 3.19.0's count, columns and itemWidth with the
exactly-one-source rule, the grid arithmetic, all five new refusals and the repeat.itemWidth.unused
gap; and 3.20.0's theme ambientScene with its two refusals, the one-name-for-the-whole-desktop
reasoning and what a user sees when the named scene is not installed, which is nothing at the time.
FOUR THINGS WERE WRONG THAT NOBODY REPORTED, found by reading rather than from the bug: the whole
time.date.* family and its per-cell is_today flags were undocumented, so the one reading a calendar
needs to colour today in was reachable only by reading our source; section 17 listed persisted state
among the things that do not exist while section 8 documents the file it is written to; rule 6 said
text never wraps, which textWrap has contradicted since 3.8.0; and the bundled widget count read 109
against a measured 155. making-widgets.md takes the same corrections in its own register. A document
that lags one release lags in the places nobody reported too.
3.20.0 (2026-09-18) - ThemeManifest gains ambientScene: the idle-display
scene a theme's desktop becomes when nobody is at it. Null changes nothing, exactly as wallpaper
does, and for the same reason - a theme that does not mention one leaves the user's alone. TWO
REFUSALS ARE ADDED, so the site wants this before an author can publish one: an ambientScene present
but blank (an author who meant to name a scene and did not, which would otherwise apply a scene
called "" and fall back to the default preset with nothing saying why), and a name longer than
AmbientScene.MaximumNameLength. That bound is REUSED rather than restated, which is the MaxIdLength
case: the site compares a theme's scene against a scene that exists, and that is sound only while the
two bounds agree. No schema version and CurrentSchemaVersion stays 1 for themes: an older reader
ignores the property and applies the theme, doing less - it simply leaves the idle display alone.
ONE NAME AND NOT ONE PER SCREEN, which is the decision rather than a simplification. The engine's own
EngineSettings.AmbientSceneByScreen is per-monitor and keyed by monitor BOUNDS - deliberately,
because a display name is a driver string and an index renumbers - and a theme is a package authored
on somebody else's monitors, so it can name no screen of the machine it lands on. A theme has never
had a multi-monitor concept at all: ThemeWidget carries an X and a Y and no screen, and wallpaper is
one picture for all of them. A per-screen choice the user already made is left alone and so outranks
the theme through the fallback chain AmbientSceneFor documents, which is HideOtherWidgets' argument:
clearing entries somebody chose is expensive in the one direction that removes things. EXISTENCE IS
NOT CHECKED AND CANNOT BE - whether a scene of this name is on the machine is a question about that
machine, the same split by which a scene names catalog skins this reader cannot vouch for; the shape
is bounded here and the engine reports a name it cannot find.
3.19.0 (2026-09-18) - RepeatElement gains count, columns and itemWidth: a
repeat may state how many times to run and lay its repetitions out as a grid. THE SITE MUST ADOPT
THIS BEFORE AN AUTHOR CAN PUBLISH ONE, and the reason is the 2026-08-28 rule rather than the fields
being new - an older reader ignores an unknown property AND ignores an illegal value inside it, so
until you are on 3.19.0 you accept manifests every 3.19.0 engine refuses. FIVE REFUSALS ARE ADDED,
named because "purely additive" is a claim about a valid document and says nothing about an invalid
one: a repeat naming no source at all (the existing message now offers count too); a repeat naming
MORE than one of count/over/overSensors/overList, which is new and was previously resolved by
whichever line of the loader ran first; a count outside 1..MaxRepeatCount (256, public, a cost bound
- MaxItems is not the equivalent because it only ever CAPS a number the machine supplied, while a
count is one the author supplies and it multiplies the controls a template builds); columns at or
below zero; and a columns above one with no itemWidth. A SIXTH WAS WRITTEN AS A REFUSAL AND IS A
GAP INSTEAD: an itemWidth with columns at one does nothing, and refusing it was the
points-on-a-rectangle argument until the bundled corpus answered it - two skins that ship already
carry BOTH keys, authored before either field existed and ignored ever since as unknown properties,
and so may any listing already published. That is 3.2.0's direction, where the exposure is to
packages already in a marketplace and the engine must not start refusing them, so it is reported as
repeat.itemWidth.unused - a new gap code, added to PackageGapCodes.All in the same commit as its
describer, which is the shape.shadow.polygon lesson. NO SCHEMA VERSION AND
CurrentSchemaVersion STAYS 5: these are properties on an existing type, so an older reader parses
the document, and a count repeat is then refused by that reader with an intelligible sentence
naming the three sources it knows - the 2.6.0 conic direction, a validation message rather than a
parse failure, which is the line 2.11.0 drew. Spacing now applies horizontally as well as
vertically, which changes nothing already written because a single-column repeat has no horizontal
stride. MaxItems is silently ignored for a counted repeat and says so in its own remarks: it is an
int with a default of 8, so no reader can tell "maxItems": 8 from a manifest that never mentioned
it, and capping an author's own forty-two cell month at eight is the justification-about-the-first-
subject trap. Also in this release, riding the bump because both change the nuspec or its prose and
neither may mint a second file under a version that already exists: PackageFile.AllowedExtensions
carries corrected pack figures - 155 skin folders and 9 themes, 990 .json, 7 .png and 2 .csx,
against the 105 widgets and 923 .json its remarks claimed, which were right when taken and grew.
3.18.0 (2026-09-07) - SkinShadow.IsVisible is no longer serialised. No
schema change, no new refusal, no API change and CurrentSchemaVersion stays 5: what moves is what
the writer emits. The property is computed with no setter, and System.Text.Json writes public
getters by default, so it was emitted and then silently dropped on read - every scene saved from
the inline element editor carried an isVisible nobody could edit. Nothing needs adopting first,
because no reader could ever populate it. The hazard it removes is a future one: UnknownFields
flags a name the model does not write, so the day the property were deleted every scene already
written would open not editable, naming a field its author never typed.
Every version this package has carried is listed in CHANGELOG.md in the MyDesktopWidget repository.