ChoicePopupController is a UIViewComponent that renders an N-button modal — Yes/No, Save/Discard/Cancel, or any custom set. Buttons are built dynamically from a ChoicePopupChoice[] list (text + color), and there are two ways to drive it:
ChoicePopupArgs. The push is awaitable: PushAsync<int> resolves with the clicked choice index.ChoicePopupController on the same GameObject as its caller, driven through OnButtonClick + Fade.FadeIn().
One prefab in a global navigation graph serves every confirmation in the game — no per-screen popup copies. Register the popup prefab on a NavNode (typically dismissMode: Pop with a Dim backdrop, hidden until pushed), then push it with args:
async void OnReturnToMenuClicked()
{
var result = await LunaNavigation.PushAsync<int>(_confirmDest, new ChoicePopupArgs
{
Header = "Return to Main Menu",
Body = "Unsaved progress will be lost.",
Choices = new[]
{
new ChoicePopupChoice("Return", "red"),
new ChoicePopupChoice("Cancel", "slate"),
},
});
if (!result.IsDismissed && result.Value == 0)
{
// First choice (index 0) — the affirmative action by convention.
ReturnToMainMenu();
}
}ChoicePopupArgs fields are optional — a null/empty Header, Body, or Choices keeps the value serialized on the prefab, so a generic "Are you sure?" popup works with new ChoicePopupArgs { Header = "Delete Save 3" }.LunaNavigation.Pop(i), resolving the PushAsync<int> task. Keep the convention that the first choice is the "do it" action so callers can test result.Value == 0.result.IsDismissed is true and no choice index is delivered. Treat it like the cancel button.GameSaveViewList captures the slot in Save & Load).The GameFull sample registers exactly one such node (confirm, in its global graph) and routes the pause menu's return-to-menu confirm and all save-list overwrite/delete confirms through it.
A ChoicePopupController without a nav node — e.g. living on the same GameObject as the view that uses it — is driven manually. On load it forces itself start-hidden (a node-less popup would otherwise show its placeholder texts the moment the host view opens), and a click just fades it back out instead of popping:
[SerializeField] ChoicePopupController _popup;
void OnDeleteClicked()
{
_popup.TextHeader = "Delete save?";
_popup.TextBody = "This cannot be undone.";
_popup.Choices = new[]
{
new ChoicePopupChoice("Delete", "red"),
new ChoicePopupChoice("Cancel", "slate"),
};
_popup.OnButtonClick += OnConfirmDelete;
_popup.UIView.Fade.FadeIn();
}
void OnConfirmDelete(int index)
{
if (index == 0) DeleteSave();
_popup.OnButtonClick -= OnConfirmDelete;
}Prefer the nav-destination mode for new work — the embedded mode duplicates a popup per host view and can't be awaited.
The popup expects three named elements in the UXML hosted by the view's PanelRenderer:
| Element | Name |
|---|---|
Label | Header |
Label | Body |
VisualElement | ButtonContainer |
The controller fills ButtonContainer with one Button per ChoicePopupChoice on fade-in.
Add ChoicePopupController to the GameObject. In the inspector:
_textHeader / _textBody — default labels (used when a push passes no override)._choices — the default button list (text + color name)._buttonClasses — extra USS classes applied to every generated button (e.g. "btn-xl").
For the nav-destination mode, also register the prefab on a NavNode in your graph and reference that destination key from callers.
namespace CupkekGames.Luna;
public class ChoicePopupController : UIViewComponent| Property | Type | Description |
|---|---|---|
Choices | ChoicePopupChoice[] | Button list. Re-assigning rebuilds buttons on next fade-in. |
TextHeader | string | Read/write the header label. |
TextBody | string | Read/write the body label. |
public event Action<int> OnButtonClick;Fires with the index of the clicked choice (in both modes, before the popup resolves). After a click all buttons are disabled, then the popup pops (nav mode) or fades out (embedded mode).
The per-push pipeline is virtual, so specialized confirms can extend the base instead of duplicating it — the Showcase sample's purchase modal subclasses ChoicePopupController with extra preview/burst behavior this way:
| Virtual | Called | Override to |
|---|---|---|
ApplyArgs(ChoicePopupArgs) | On fade-in, when the push carried args | Apply extra fields from a ChoicePopupArgs subclass. |
RebuildButtons() | On every fade-in | Replace dynamic button generation (e.g. fixed UXML buttons). |
ResolveChoice(int) | After a click | Delay or decorate the resolution (default: Pop(i) with a node, FadeOut() without). |
public class ChoicePopupArgs
{
public string Header;
public string Body;
public ChoicePopupChoice[] Choices;
}Passed as the push args; read on fade-in via GetArgs<ChoicePopupArgs>(). Subclass it to carry extra data for a ChoicePopupController subclass.
public ChoicePopupChoice(string text, string color);color is a Luna color-class name ("lime", "red", "slate", etc. — see colors). Applied as a USS class on the button alongside btn.
PushAsync sees IsDismissed); a UIViewActionEscape(Fade.FadeOut) covers the embedded mode.PushAsync<T> / Pop<T>, dismiss modesSettings
Theme
Light
Contrast
Material
Dark
Dim
Material Dark
System
Sidebar(Light & Contrast only)
Font Family
DM Sans
Wix
Inclusive Sans
AR One Sans
Direction