-
Notifications
You must be signed in to change notification settings - Fork 0
Rubric Frontends
Rubric delegates its settings screen to whichever config-GUI library the user has installed. This is the whole "Rubric is not a GUI library" thing in practice.
- YetAnotherConfigLib (YACL) — modern, sleek, YACL-native controllers. Client-side only. Recommended default.
-
Cloth Config — older, feature-rich, better nested-category support, has a proper
RequirementAPI for conditionally-disabled entries. Client-side only.
Install one or both. Rubric picks a provider at screen-open time based on gui.preferredFrontend in Rubric's own config:
-
AUTO— YACL first, Cloth second, whichever is installed. The default. -
YACL— force YACL. Falls back to Cloth if YACL isn't installed. -
CLOTH— force Cloth. Falls back to YACL if Cloth isn't installed.
Neither installed → the placeholder screen (see below).
If neither library is installed but the user clicks a Rubric-backed config button in ModMenu / Catalogue, they get a vanilla NoFrontendScreen that:
- Explains no frontend is installed and suggests YACL.
- Shows the on-disk path of the config file.
- Has an "Open Folder" and "Open Config" button (via
Util.getPlatform().openFile(...)— cross-platform). - Has a "Done" button to return.
The intent: even without a GUI library installed, users can still find and hand-edit the file.
ModMenu is Fabric-only; the corresponding surface on NeoForge is the mod-list Config button that ships with NG proper (and is picked up by Catalogue — see below).
Rubric ships ModMenuIntegration (implements ModMenuApi) on the Fabric side. Two hookups:
-
getModConfigScreenFactory()— surfaces Rubric's own config under therubricmod entry. -
getProvidedConfigScreenFactories()— surfaces every consumer's config under its own mod entry. Keyed by@Config#id, which must match the consumer'sfabric.mod.json#idfor ModMenu to attach the factory.
If your mod owns more than one Rubric config, register each via Rubric.register(...). The ModMenuIntegration groups them by mod ID and surfaces a ConfigChooserScreen when there's more than one — a vanilla-widget list of buttons, one per config.
To register an extra config under the same mod ID as a different @Config#id:
// Directly on the GUI hub — bypasses schema-id inference.
RubricGui.registerScreen("mymod", secondManager);ModMenu queries getProvidedConfigScreenFactories() lazily on every mod-list open, so managers registered after game start still appear. getModConfigScreenFactory() is queried once at mod-discovery time, but Rubric returns a factory that looks up the registry on click — same lazy semantics. Registration order doesn't matter for either.
Catalogue works on both loaders but discovers configs differently on each.
Catalogue on NG uses the same IConfigScreenFactory extension point that ships with NeoForge proper. Rubric registers itself via that extension point in RubricNeoForgeClientMod, and every consumer mod does the same for its own configs:
container.registerExtensionPoint(
IConfigScreenFactory.class,
(mc, parent) -> RubricGui.openFor(Minecraft.getInstance(), parent, myManager)
);No timing edge cases — the extension point is queried per-click, so registration order between Rubric and consumer mods doesn't matter.
Catalogue on Fabric does not read ModMenu's API. It discovers config screens through a Rubric-specific hookup declared in fabric.mod.json#custom.catalogue.configFactory, pointing at a class with two static methods.
Rubric ships CatalogueIntegration implementing both:
-
createConfigScreen(Screen parent, ModContainer container)— invoked lazily on every click of Rubric's own Config button. Returns Rubric's own config screen. Because the invocation is per-click, registration timing between Rubric's client-init and Catalogue's client-init doesn't matter for the own-mod path. -
createConfigProvider()— invoked once at Catalogue's client-init and cached. Returns aMap<modId, BiFunction<Screen, ModContainer, Screen>>of screens Rubric provides for other mods (i.e. every consumer that registered withRubric.register(...)).
The map returned by createConfigProvider() is a snapshot of Rubric's registry at the moment Catalogue calls it. That call happens during Catalogue's ClientModInitializer#onInitializeClient(), which Fabric fires alongside every other mod's client-init — order is not guaranteed.
If a consumer mod's onInitializeClient() calls Rubric.register(...) after Catalogue's own onInitializeClient() runs, that consumer's entry misses the snapshot and its Config button will not appear in Catalogue. There's no per-click re-query mechanism upstream today.
Practical implications:
-
Rubric's own config: always robust — handled by
createConfigScreen, invoked per click. - Consumer configs: usually work, but the exact outcome depends on the client-entrypoint firing order for that installation. A consumer's button may show up on one launch and not on another if load order shifts.
Workarounds if you're a consumer mod author hitting this:
- Register your config in
ModInitializer#onInitialize()(main entrypoint) instead ofClientModInitializer#onInitializeClient().mainentrypoints fire before anycliententrypoints, so the registry is populated before Catalogue snapshots it. - If your registration must be client-side, the cleanest fix is upstream in Catalogue itself — a bug report / PR asking for lazy re-query would help every mod using Catalogue's Fabric integration.
Menulogue (a separate MrCrayfish mod) walks ModMenu API entrypoints and re-exposes their results through Catalogue's hookup. Installing Menulogue alongside Catalogue works around the same class of bug for any mod that ships ModMenu integration — including Rubric's — but requires the end user to install another mod, which we'd rather not require.
Adding a new frontend is a plugin — no changes to Rubric proper. The ScreenProvider SPI is compiled into each loader integration (Fabric prod uses intermediary MC mappings; NG prod uses Mojmap — the two can't share a single compiled artifact that references MC types). Same interface shape on both:
// com.oliveryasuna.mc.rubric.fabric.gui — on Fabric
// com.oliveryasuna.mc.rubric.neoforge.gui — on NeoForge
public interface ScreenProvider {
String id(); // lowercase, e.g. "cloth", "yacl"
Screen create(Minecraft client, Screen parent, ConfigManager<?> manager);
}If you're writing a frontend for both loaders, you'll implement the interface twice — the bodies are identical, only the import package changes.
Implementation:
- Return
nullfromcreate(...)to defer to the next provider (Rubric tries the preferred first, then any others in registration order, then falls back to theNoFrontendScreen). - Provide a stable
id(). It's whatFrontendenum values andgui.preferredFrontendreference.
Register on the client:
RubricGui.registerProvider(new MyScreenProvider());Look at YaclScreenProvider and ClothScreenProvider for reference. Common helpers live in ScreenProviders (static utilities) and ScreenBuildContext (per-screen state).
Both providers walk the Schema tree and emit widgets. Shared helpers:
-
ScreenProviders.displayName(entry, meta, showSuffixes)— entry label with optional[restart, server]tag suffixes. -
ScreenProviders.findRange(meta)/findOneOf(meta)— locate constraint validators. -
ScreenProviders.useSlider(meta, range)— the heuristic for slider vs. field widget. -
ScreenProviders.parseColor(hex, fallback)/formatColor(rgb)— hex ↔ int for@Widget(COLOR).
ScreenBuildContext holds the staged edits map + the ConfigManager + snapshot of the GUI flags. On save the context does manager.set(path, value) per staged entry then manager.save() once.
Not every Rubric annotation maps to a widget in every frontend. See Frontend comparison for the matrix and where the gaps are.
- Frontend comparison — YACL vs Cloth.
-
Self-config — the
gui.preferredFrontendknob.
Rubric
Reference
Runtime
GUI
Help