Skip to content

Add HUD editor onboarding tour - #1204

Merged
Wyvest merged 2 commits into
v1from
feat/hud-onboarding
Oct 20, 2026
Merged

Wyvest merged 2 commits into
v1from
feat/hud-onboarding

Conversation

@Deftu

@Deftu Deftu commented Oct 17, 2026

Copy link
Copy Markdown
Contributor

Adds a guided tour to the HUD editor that explains its main features.

When it shows

  • Starts automatically the first time the editor has two HUDs that can merge with each other (same background style, not already merged). With fewer, nothing shows and it stays pending.
  • Waits until the user is not dragging, resizing or holding the mouse.
  • Skipped on opens that deep-link to a HUD (edit or add from the main menu).
  • Can be replayed from the empty-canvas right-click menu ("Editor Tour").
  • Seen state is a hidden hudEditorTourSeen field in oneconfig.json.

Steps

  1. Welcome
  2. Add HUDs: spotlights the library
  3. Move and resize: spotlights a HUD
  4. Merge HUDs: hands-on. The overlay steps aside and an animated ghost shows where to stack a HUD so its background merges with a neighbour. Next unlocks once a new merge happens.
  5. Anchor HUDs: shows a non-interactive preview of the real right-click menu with Anchor highlighted
  6. Settings: opens the panel on the Settings tab
  7. Designer: switches to the Designer tab
  8. Fonts and text
  9. Done

Non-interactive steps block canvas input and HUD hotkeys. Arrow keys and Enter move between steps. All text is in en_us.json under oneconfig.hud.tour.*.

Changes

  • HudTour.kt (new): steps, spotlight overlay, card, drag hint
  • HudDesignStudio.kt: tour state, auto-start, input blocking, focus rects, merge detection
  • HudSettingsReset.kt: preview mode for the HUD context menu, optional tour row in the canvas menu
  • OneConfigConfig.java: hudEditorTourSeen and markHudEditorTourSeen()
  • en_us.json: tour strings

Tested on 26.3 Fabric.

@Wyvest Wyvest left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Left a few notes on the tour. The main ones: Escape is swallowed during blocked steps, the replay entry is gated on a merge pair even though the Done step promises it, and the merge-eligibility check is a copy of HudBackgroundMerge's that has already drifted.

}
.onKeyEvent { keyEvent ->
if (keyEvent.type != KeyEventType.KeyDown) return@onKeyEvent false
if (tourBlocksEditor) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While a non-interactive step is active this returns true for every KeyDown, so Escape never reaches ComposeScreen's super.keyPressed. It can't close the editor or skip the tour, and the only way out is clicking "Skip tour". During the interactive Merging step Escape works normally, so it's inconsistent too. Suggest letting Escape through, or mapping it to endTour(), and only consuming arrows and Enter.

undoEnabled = canUndo,
onRedo = { HudDesignSession.handleRedo(true) },
redoEnabled = canRedo,
onTour = if (pasteMenuOffset != null && hasTourMergePair()) ({ startTour() }) else null,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The replay entry only shows when hasTourMergePair() is true, but the Done step says "Right-click empty space and pick Editor Tour to see this again". After the user merges their two HUDs, or deletes one, the row disappears and the promised replay isn't available. The Merging step already handles the no-pair case with the task_add text, so this gate doesn't seem necessary.

hud !is LegacyHud && !hud.locked && !hud.hidden && hud.canMergeBackground() &&
hud.showBackground && hud.mergeBackground && hud.hasBackground()

private fun tourMergeCompatible(a: Hud, b: Hud): Boolean =

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

canTourMerge/tourMergeCompatible copy the eligibility rules from HudBackgroundMerge.computeGroups and the private Item.sameStyle, and they already differ: this checks LegacyHud where the real code checks LegacyHudMarker, and it skips the renderedW/H > 0 check. If a HUD the real merger rejects counts as a pair here, the tour auto-starts and shows the ghost hint, but no merge ever happens and Next stays disabled on Merging. Could HudBackgroundMerge expose one predicate that both places call?

tourStep == null && !isDragging && !isResizing && !pointerDown && hasTourMergePair()
}
}.first { it }
if (!OneConfigConfig.hudEditorTourSeen) startTour()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Auto-start waits out drag, resize and pointer-down, but not an open panel or a focused text field. A common path: the user clicks a second text HUD in the library, the panel opens, and they start typing its text. The pair now exists, so the tour starts, LaunchedEffect(tourStep) clears the selection and closes the panel, and keyFocusRequester takes focus, swallowing the rest of their typing. Might be worth also waiting until no panel is open or no text input has focus.

hoveredHud = null
}

fun startTour() {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HudDesignStudio stays composed across editor closes (RetainedVisibility), and nothing ends the tour on close. If the user closes mid-tour, tourStep and the forced panel/category stay as they are, tourPreviousCategory is never restored, and the next normal open drops them back into the same step. Closing during Anchoring also leaves the preview Popup at full opacity through the fade-out. Probably simplest to call endTour() when the editor closes.

var lastTourStep by remember { mutableStateOf(HudTourStep.entries.first()) }
tourStep?.let { lastTourStep = it }
AnimatedVisibility(
visible = tourStep != null,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After Skip or Done, AnimatedVisibility keeps the overlay composed through fadeOut with lastTourStep, which is non-interactive, so its full-screen pointerInput keeps eating clicks for the length of the fade. CHROME_TOUR_CARD also stays in chromeRects until dispose, so presses where the card was are still treated as chrome.

if (next != null) tourStep = next else if (delta > 0) endTour()
}

LaunchedEffect(tourStep) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Each step clears selectedHuds and closes the panel, and endTour() forces both empty again. Only the category is saved and restored. If you replay from the canvas menu while a HUD is selected with its settings open, the selection and panel are gone when the tour ends. Could snapshot those alongside tourPreviousCategory.

}

fun startTour() {
OneConfigConfig.markHudEditorTourSeen()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This marks the tour seen as soon as it starts, before the first step renders. If the game closes or crashes on Welcome, it never auto-shows again, and with the replay row gated on a merge pair the user might not be able to get it back. Marking it seen in endTour() (Skip/Done) seems safer.

}
.graphicsLayer { alpha = if (measured) 1f else 0f }
.then(cardModifier)
.pointerInput(Unit) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This consume-everything loop appears three times (here, line 180, and in HudSettingsReset.kt for the preview menu), and the copies differ slightly in pass and in hover/scroll handling. Modifier.blockInteraction in Common.kt and chromeBlocker in the studio already cover this. The overlay also blocks twice: it registers CHROME_TOUR for inChrome() and consumes every event.

undoEnabled = canUndo,
onRedo = { HudDesignSession.handleRedo(true) },
redoEnabled = canRedo,
onTour = if (pasteMenuOffset != null && hasTourMergePair()) ({ startTour() }) else null,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: hasTourMergePair() runs in the composable body on every recomposition while the paste menu is open. That's O(n²) pairs, each calling mergeGroupOf (a linear scan), and canMergeAny already computes the same thing in a derivedStateOf. Could hoist one remembered derived state keyed on instancesRevision and share it with the auto-start condition.

@Deftu

Deftu commented Oct 19, 2026

Copy link
Copy Markdown
Contributor Author

Thanks, addressed all of these in 08886cc. Escape now closes the editor (which ends the tour) instead of skipping it.

@Wyvest
Wyvest merged commit f59db61 into v1 Oct 20, 2026
22 of 24 checks passed
@Wyvest
Wyvest deleted the feat/hud-onboarding branch October 20, 2026 01:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

2 participants