How-To: Brightness & Safe Zone Calibration
Goal: give the player a full-screen gamma calibration screen and a title-safe frame screen, both adjusted live so they judge the real picture instead of a preview.
1. What already exists
| Screen class | Row class | What it edits | Named action tag |
|---|---|---|---|
UCrimsonBrightnessEditor | UCrimsonSettingAction_BrightnessEditor | UCrimsonSettingsLocal::SetDisplayGamma, clamped 1.0 - 3.0 | CrimsonSettings.Action.Brightness |
UCrimsonSafeZoneEditor | UCrimsonSettingAction_SafeZoneEditor | UCrimsonSettingsLocal::SetSafeZone, which feeds SSafeZone::SetGlobalSafeZoneScale | CrimsonSettings.Action.SafeZone |
Both screen classes are Abstract UCommonActivatableWidgets that also implement ICrimsonSettingActionInterface - the plugin ships behavior, you author the Widget Blueprint. Each action row supplies a child scalar setting rather than being a plain button, which is what makes Done write through the registry so the change tracker sees it and Apply lights up. Writing UCrimsonSettingsLocal directly would land the value and leave the screen looking untouched.
2. Author the screen widgets
Create a Widget Blueprint parented to each class and give it these binds. Both classes want the same four names.
| Widget name | Type | Purpose |
|---|---|---|
Switcher_SafeZoneMessage | UWidgetSwitcher | Holds the instruction line. The class switches it and rewrites the text per input device. |
RichText_Default | UCommonRichTextBlock | The instruction line. Authored with placeholder body text; the class writes the live text. |
Button_Back | UCommonButtonBase | Restores the value the screen opened on and closes. |
Button_Done | UCommonButtonBase | Commits through the row's child setting and closes. |
Then add the instrument each screen needs between its heading and the instruction line: for brightness, a step wedge with a low-value mark overlaid on the black end (the player raises gamma until the mark stops separating from its background); for safe zone, a frame outline wrapped in a SafeZone whose slot has bIsTitleSafe set and SafeAreaScale 1 on all four sides.
W_BrightnessEditor and W_SafeZoneEditor (/CrimsonCore/UserInterface/Settings/Screens/) are complete worked examples. Study the instrument in each: the brightness one is 24 flat Slate steps stepped in sRGB with a 56 px mark on the black end; the safe-zone one is a SafeZone-wrapped 2 px outline sitting in the ROOT overlay rather than the content column, because it marks the screen's edges. Duplicate either as a starting point.3. Push them when a row fires
An action row broadcasts the registry's named-action event with the setting and its action tag. Subscribe on your screen, match the tag, push the matching widget onto your Menu layer, and call ExecuteActionForSetting(ActionTag, Setting) on it so it knows which child setting to write through.
OnSettingNamedActionEvent, a native DECLARE_EVENT_TwoParams. Blueprint binds On Setting Named Action (OnSettingNamedActionBP), a BlueprintAssignable mirror fired with the same Setting and Action immediately after the native one - assign it inside your Create Registry event, on the registry you just built, before returning it. Either route works, so this page is Blueprint work end to end.// MySettingsScreen.h#include "Widgets/CrimsonSettingScreen.h"#include "MySettingsScreen.generated.h"UCLASS(Abstract)class UMySettingsScreen : public UCrimsonSettingScreen{GENERATED_BODY()protected:virtual void NativeOnInitialized() override;UPROPERTY(EditDefaultsOnly, Category = "Editors")TSoftClassPtr<UCommonActivatableWidget> BrightnessEditorClass;UPROPERTY(EditDefaultsOnly, Category = "Editors")TSoftClassPtr<UCommonActivatableWidget> SafeZoneEditorClass;private:void HandleNamedAction(UCrimsonSetting* Setting, FGameplayTag ActionTag);};// MySettingsScreen.cpp#include "MySettingsScreen.h"#include "CrimsonSettingRegistry.h"#include "CrimsonSettingsTags.h"void UMySettingsScreen::NativeOnInitialized(){Super::NativeOnInitialized();if (UCrimsonSettingRegistry* Reg = GetOrCreateRegistry()){Reg->OnSettingNamedActionEvent.AddUObject(this, &ThisClass::HandleNamedAction);}}void UMySettingsScreen::HandleNamedAction(UCrimsonSetting* Setting, FGameplayTag ActionTag){if (ActionTag == CrimsonSettingsTags::Action_Brightness){// Push BrightnessEditorClass onto your Menu layer, then call// ICrimsonSettingActionInterface::Execute_ExecuteActionForSetting(Widget, ActionTag, Setting).}else if (ActionTag == CrimsonSettingsTags::Action_SafeZone){// Same, with SafeZoneEditorClass.}else if (ActionTag == CrimsonSettingsTags::Action_Benchmark){RunHardwareBenchmark();}}
NativeOnAnalogValueChanged on Gamepad_RightY, past a 0.2 dead zone, and NativeOnMouseWheel) - no Input Action asset is involved.Both screens name a desired focus target (Back, or Done when
bCanCancel is off) and ask for a Menu FUIInputConfig, so a pad lands on a button the moment the screen opens. Back restores the value the screen opened on and is the CommonUI back action, so B / Circle closes it the way it closes every other screen. The instruction line names the right control for the device in hand, re-resolved on every input-method change.4. Safe zone on desktop
SSafeZone scales the platform's title-safe margin, and on Windows that margin is zero unless a debug override is running. Scaling zero gives zero, so the frame would not visibly move - engine behavior, not a bug in the screen.FCrimsonSettingEditCondition_SafeZoneEditable therefore kills the row unless one of two things is true: the platform declares the Platform.Trait.SupportsSafeZoneEditing trait (add it to PlatformTraits under [/Script/CommonUI.CommonUISettings] in your Config/<Platform>/<Platform>Game.ini - the shipping path for consoles), or the display actually reports title-safe padding right now.To bring the row up on PC for calibration work, open the console in a PIE or standalone session and run:
r.DebugSafeZone.TitleRatio 0.9
That is the whole recipe. The CVar makes FDisplayMetrics::ApplyDefaultSafeZones fill TitleSafePaddingSize with 5% of the display on each edge and broadcasts FCoreDelegates::OnSafeFrameChangedEvent, which every live SSafeZone listens to - so the HUD insets immediately. Reopen the settings screen, or navigate away and back so the registry regenerates, and the Safe Zone row is there. r.DebugSafeZone.TitleRatio 1 puts it all back and the row disappears again.
SSafeZone::UpdateSafeMargin reads FSlateApplicationBase::GetCachedDisplayMetrics, and nothing rebuilds that cache when the CVar changes - Slate seeds it at startup and refreshes it only when the OS reports a display change. Left alone, the ratio changes, every safe zone dutifully recomputes, and every one of them reads the same stale zero.FCrimsonSettingsModule subscribes to OnSafeFrameChangedEvent and rebuilds the metrics when the ratio has actually changed. Multicast delegates run their subscribers in reverse registration order, and every SSafeZone subscribes when it is constructed - long after the module started - so by the time the module's handler runs the safe zones have already recomputed from the stale cache. The module therefore broadcasts the event once more (re-entrancy guarded) after re-seeding, and that second pass is the one that moves the HUD.SSafeZone; it cannot inset a widget that is not in one. Your HUD layout must have a SafeZone as its root, with its slot's bIsTitleSafe set and SafeAreaScale 1 on all four sides. Menu-layer layouts are deliberately not wrapped - menus are drawn edge to edge and each screen insets its own content. Without the HUD wrap, the calibration screen's own frame moves and nothing behind it does, which reads as the setting being broken.The per-edge alternative, if you want asymmetric padding, is SafeZone.EnableOverrides 1 plus the four SafeZone.Ratio.* values. Unlike r.DebugSafeZone.TitleRatio these do not broadcast OnSafeFrameChangedEvent on change - the metrics rebuild inside UCrimsonSettingsLocal::IsTitleSafePaddingAvailable() is what picks them up, so open the settings screen after setting them.
r.DebugSafeZone.TitleRatio 0.9 set, the Safe Zone row appears after a regenerate, and moving it visibly pulls a SafeZone-rooted HUD in from the screen edges.See also
- How-To: Use the Ready-Made Collections - where both rows come from.
- How-To: First-Run Setup Wizard - the usual first place a player is asked to calibrate gamma.
- API Reference -> Presentation -
UCrimsonBrightnessEditorandUCrimsonSafeZoneEditor.