A composable IMGUI framework for RimWorld mods. Provides nodes, layout, theming, typography, navigation, and adapters that bridge nodes into vanilla surfaces (gizmos, ITabs, MainTabs, FloatMenuOptions, ChoiceLetters, tooltips).
Built for and consumed by the RimWorld: Cosmere mod suite, but designed to be a shared dependency for any mod that wants composable, themed UI.
Subscribe on the Steam Workshop or download the latest release from the GitHub releases page. Mods that depend on Lightweave will list it as a required dependency.
<!-- About/About.xml -->
<modDependencies>
<li>
<packageId>Cosmere.Lightweave</packageId>
<displayName>Lightweave</displayName>
<downloadUrl>https://github.com/RimworldCosmere/Lightweave/releases/latest</downloadUrl>
</li>
</modDependencies>
<forceLoadAfter>
<li>Cosmere.Lightweave</li>
</forceLoadAfter><!-- YourMod.csproj -->
<ItemGroup>
<PackageReference Include="Cosmere.Lightweave" Version="*" GeneratePathProperty="true" />
</ItemGroup>PackageReference only wires up the compile-time reference. RimWorld loads DLLs from each mod's Assemblies/ folder at runtime, so you also need to drop a copy of Lightweave.dll (and Lightweave.pdb if you want symbols) there. Either copy it in by hand after a NuGet restore, or add a build target so it stays in sync:
<Target Name="CopyLightweaveToAssemblies" AfterTargets="Build">
<ItemGroup>
<LightweaveRuntime Include="$(PkgCosmere_Lightweave)/lib/**/Lightweave.dll" />
<LightweaveRuntime Include="$(PkgCosmere_Lightweave)/lib/**/Lightweave.pdb" />
</ItemGroup>
<Copy SourceFiles="@(LightweaveRuntime)" DestinationFolder="$(ProjectDir)../Assemblies/" SkipUnchangedFiles="true" />
</Target>The GeneratePathProperty="true" on the PackageReference is what makes $(PkgCosmere_Lightweave) resolve to the restored package directory.
dotnet build Lightweave.slnLightweave is two things at once, in this order:
- A consumable IMGUI primitive framework (
Layout/,Input/,Feedback/,Navigation/,Overlay/,Typography/,Data/,Doc/) — reusable, themed, documented, composable. - A new RimWorld UI implementation (
MainMenu/,LoadColony/,ModsConfig/,Options/,Playground/) that consumes those primitives.
The framework comes first. Feature dirs are the proving ground — if a feature can't be expressed in primitives, the framework is missing something. Every feature mistake is a framework gap.
Raw Widgets.* / GUI.* / UnityEngine.GUI* / Listing_Standard calls live only in Rendering/, Patch/, Adapter/, Polyfills/. Everywhere else — primitives and feature dirs — must compose existing primitives or call into Rendering/ helpers.
If you're about to write Widgets.X or new Color(...) inside a feature dir, stop. Either a primitive should own that, or an existing primitive is missing a prop. If a primitive does not exist for what you need, build the primitive first, then resume the feature.
Every public primitive in Layout/Input/Feedback/Navigation/Overlay/Typography/Data MUST have:
- A
[Doc(Id, Summary, WhenToUse, SourcePath, ...)]attribute on the class. - A static
Create(...)entry point in apublic static class <Name>. Style? style = null, string[]? classes = null, string? id = nullin the signature.[CallerLineNumber] int line = 0, [CallerFilePath] string file = ""at the end of the signature.node.ApplyStyling("<id>", style, classes, id)immediately afterNodeBuilder.New(...).- A
MeasureWidthcallback returning the natural content width. - A
Measurecallback (orPreferredHeight) so vertical layouts can size it. - At least one
[DocUsage]sample, plus[DocVariant]for each variant and[DocState]for each interaction state.
One primitive per file. File name matches class name exactly. Variants are enums named <Name>Variant in a sibling file.
Primitives hug content on both axes by default. They only fill the allocated rect when the caller passes Style.Width(Length.Grower) or Style.Height(Length.Grower). If you're sizing from allocatedRect.width/.height without first checking IsGrower, you've forced fill — fix it.
The node.Paint = (rect, paintChildren) => {...} callback runs every frame. Inside it:
- No
new Color(...),new Rect[](...),new List<...>(),new GUIStyle(), string concat, LINQ, orToString()on hot values. - No
ContentFinder<Texture2D>.Get(...)— cache textures in a[StaticConstructorOnStartup]holder. - Cache
GUIStyleinstances viaGuiStyleCache.GetOrCreate(font, pixelSize). - Cache gradient textures via
GradientTextureCache. - Allocate temporaries in the closure of
Create(), not insidePaint.
Inside primitives: Theme.Theme theme = RenderContext.Current.Theme; theme.GetColor(slot) for every color. Color.white, Color.black, new Color(0.79f, 0.65f, 0.37f) inside a primitive is wrong. Add a ThemeSlot enum value, resolve it in each theme, route the color through theme.GetColor.
The only exception is transient alpha multipliers (overlay alpha, hover wash) where the base color already came from a slot.
Primitives are string-agnostic. They take string and render it. Callers are responsible for .Translate(). A primitive must never call .Translate() internally. This keeps primitives reusable across contexts where the string source varies.
Primitives can break their API freely. The rules:
- Fix every caller in the same commit.
- Fix every
[DocUsage]/[DocVariant]/[DocState]sample on the changed primitive and on any primitive that composes it. - Build the solution clean, zero new warnings.
- Open the Playground, verify the primitive still renders correctly in every state.
There is no compatibility layer. The framework is internal to the mod and all consumers are owned.
using System;
using System.Runtime.CompilerServices;
using Cosmere.Lightweave.Doc;
using Cosmere.Lightweave.Rendering;
using Cosmere.Lightweave.Runtime;
using Cosmere.Lightweave.Tokens;
using Cosmere.Lightweave.Types;
using UnityEngine;
namespace Cosmere.Lightweave.<Category>;
[Doc(
Id = "<lowercase-name>",
Summary = "<one-line summary>",
WhenToUse = "<when a consumer should reach for this>",
SourcePath = "Lightweave/<Category>/<Name>.cs"
)]
public static class <Name> {
public static LightweaveNode Create(
// primitive-specific args first
Style? style = null,
string[]? classes = null,
string? id = null,
[CallerLineNumber] int line = 0,
[CallerFilePath] string file = ""
) {
LightweaveNode node = NodeBuilder.New("<Name>", line, file);
node.ApplyStyling("<lowercase-name>", style, classes, id);
node.MeasureWidth = () => 0f;
node.Measure = availableWidth => 0f;
node.Paint = (rect, paintChildren) => {
Theme.Theme theme = RenderContext.Current.Theme;
Style s = node.GetResolvedStyle();
// all colors via theme.GetColor(slot)
// all sizes via Rem / SpacingScale / RadiusScale
// no allocations in this scope
paintChildren();
};
return node;
}
[DocUsage]
public static DocSample DocsUsage() =>
new DocSample(() => Create(/* canonical args */));
}dotnet build Lightweave.slnclean, zero new warnings.- Launch RimWorld, open the Playground page for the primitive.
- Screenshot every
[DocVariant]and[DocState]entry. - Share screenshots before declaring the change done.
IMGUI state-bleed bugs (GUI.color leak, Text.Font leftover, missing scope restore) only surface at runtime. The Playground is the canary.
MIT. See LICENSE.