# Edit Mode Layer Control (Ctrl+Scroll) — Implementation Plan

> **Status (Jul 2026): IMPLEMENTED** (unstaged, pending /reload). 17 systems adopted. Deviations from plan: Minimap excluded (deliberate LOW-strata for Blizzard minimap-button compatibility — cross-strata levels don't stack), Datatexts deferred (dynamic bars, no central registry), Tracked/DamageMeter deferred per Tier-3; all four keep reserved DEFAULT_ORDER slots. Pet moved to rank 1 (below Player), replacing its old relative level demote. Neighbor-flash skipped (tooltip names the neighbor). Reset UI button not built (`ResetOrder` API exists).

**Goal:** In Edit Mode, Ctrl+mouse-scroll over a selected Orbit frame raises/lowers its whole system one **layer** in a single ordered HUD stack. Users see order ("above Action Bars"), never numbers. Order is per-profile.

**Foundation decision (user-approved direction):** this work executes the deferred red-flag item — `StrataEngine` goes registration-based; Core stops hardcoding plugin names as the source of truth.

---

## Verified current state (anchors)

| Fact | Where |
|---|---|
| StrataEngine: `level = base + index × 50` per entity, order profile-persisted in `Layouts.Orbit.Orbit_StrataEngine`, `Global_HUD` scope seeded with 9 names | `Core/Infrastructure/StrataEngine.lua`, `Constants.lua:79` (`StrataBlockReserve = 50`), boot at `Init.lua:164` |
| Only 2 of 9 registered entities actually consume it | `GroupFrame.lua:442/498`, `BossFrame.lua:134/231` (both fan levels across children: container −1, members +index) |
| Edit Mode wheel is TAKEN: plain wheel = anchor-padding, Shift = ×10; debounced via `wheelDebounce` + `C.Selection.WheelDebounce`; combat + canvas-active guarded | `Selection.lua:259-261` → `Drag:OnMouseWheel` (`Drag.lua:532`) — **Ctrl is free** |
| Selection overlay knows its frame via `.parent`; feedback surface exists (`OrbitPositionTooltip`, topmost strata, fade-timer machinery) | `Drag.lua:544`, `Core/EditMode/Frame/Selection/Tooltip.lua` (`Engine.SelectionTooltip`) |
| SYSTEM_ID strings align with the engine's entity names for unit/group/boss/minimap/datatexts; **ActionBars uses NUMERIC per-bar `systemIndex`** (1..N + `VEHICLE_EXIT_INDEX`) | plugin headers; `ActionBars.lua:137/441` |
| `GetFrameLevel` ERRORS on unregistered entity — wheel path must never call it blind | `StrataEngine.lua:51` |
| Frame-level ceiling 10,000 per strata → ~200 slots at reserve 50; intra-block offsets stay < 50 (raid +40 max, boss +5) | design constraint |

**Entity resolution rule (because of ActionBars):** never sniff `frame.systemIndex`. Registration explicitly tags every root frame it manages: `frame.orbitStrataEntity = entityID`. Untagged frame under the wheel = not layerable (silent no-op). Multi-frame plugins tag all roots with the same entity — wheeling any action bar moves the ActionBars layer.

---

## Phase 1 — StrataEngine rework (registration-based + Move)

`Core/Infrastructure/StrataEngine.lua`:

1. **`Register(scopeID, entityID, applier, label)`** — replaces the defaults-list-as-registry. `applier(level)` is the plugin's own level-assertion closure (only the plugin knows its internal fan-out); stored volatile in `Engine._appliers`. `label` = localized display name for feedback. Registration tags are the plugin's job (see rule above).
   - Order persistence unchanged: if `entityID` already in the persisted `entities` array, keep the user's slot. If new, insert by **default-rank walk**: find the last persisted entity whose rank in `DEFAULT_ORDER` is below the newcomer's, insert after it (stable joins into customized profiles; append if unranked).
2. **`DEFAULT_ORDER`** — the current 9 names plus Tier-2 additions (below), kept as *data* (initial order + reset target), no longer the registry. `PopulateDefaults()` shrinks to `InitializeScope("Global_HUD", 1)` + nothing else; `Init.lua:164` call site stays.
3. **Queries:** `GetIndex(scope, entity)` → index|nil (non-erroring), `GetCount(scope)`, `GetNeighbor(scope, entity, dir)` → entityID|nil, `GetLabel(scope, entity)`. `GetFrameLevel` unchanged (hard consumers may still error-fast).
4. **`Move(scope, entity, delta)`** → clamp to [1, count]; `table.remove`+`table.insert` in the persisted array; returns false when already at the end (caller shows nothing). On success: `ReapplyAll(scope)` — every applier called with its fresh level through `Orbit.ErrorHandler:Wrap` (sanctioned infra boundary; one broken applier must not strand the rest) — then `Orbit.EventBus:Fire("ORBIT_STRATA_CHANGED", scope)` for passive listeners (canvas previews etc.).
5. **`ResetOrder(scope)`** — restore `DEFAULT_ORDER` ∩ registered (Phase 4 UI hook).

## Phase 2 — Input + feedback

1. **`Drag.lua:OnMouseWheel`** — insert the Ctrl branch after the existing debounce/combat guards and **before** the canvas/anchor early-returns (layering must work on frames without anchor partners):
   ```lua
   if IsControlKeyDown() then
       local entity = parent.orbitStrataEntity
       if entity and Orbit.StrataEngine:Move("Global_HUD", entity, delta > 0 and 1 or -1) then
           Engine.SelectionTooltip:ShowLayerInfo(selectionOverlay, entity)
       end
       return
   end
   ```
   Existing `wheelDebounce` covers step-rate; Shift keeps meaning ×10 padding on the plain-wheel path only.
2. **`Selection/Tooltip.lua:ShowLayerInfo(overlay, entity)`** — reuse `OrbitPositionTooltip` + fade timer: line 1 `L.CFG_EM_LAYER_F:format(index, count)` ("Layer %d of %d"), line 2 `L.CFG_EM_LAYER_ABOVE_F:format(GetLabel(neighborBelow))` / "Bottom layer". Localization: 3-4 `CFG_EM_LAYER_*` keys ×9 locales (labels come from plugin `displayName`s — already localized `PLG_NAME_*`).
3. **Neighbor flash (nice-to-have, same phase if trivial):** pulse the crossed system's selection overlay alpha via `Engine.FrameSelection`'s overlay registry; skip if the registry lookup isn't clean — the tooltip already names the neighbor.

## Phase 3 — Adoption roster

Each adoption = tag root frame(s) + one `Register` call in `OnLoad` + applier; delete now-redundant hardcoded level math.

- **Tier 1 (registered today → gain appliers):**
  `Orbit_PlayerFrame`, `Orbit_TargetFrame`, `Orbit_PlayerPetFrame` (single root, `root:SetFrameLevel(level)`); `Orbit_GroupFrames` + `Orbit_BossFrames` (move the existing `:442/:498` / `:134/:231` math into the applier verbatim); `Orbit_ActionBars` (tag every bar container; applier levels all bars — optional `+barIndex` inside the block); `Orbit_CooldownViewer` (verify SYSTEM_ID + that viewer roots are Orbit containers, not raw Blizzard frames; applier queues via `CombatManager:QueueUpdate` since bars/viewers sit near secure children — Edit Mode is OOC, but appliers also run at boot/profile-switch); `Orbit_Minimap`, `Orbit_Datatexts` (tag each bar).
- **Tier 2 (new registrations, extend `DEFAULT_ORDER`):** `Orbit_FocusFrame`, `Orbit_PlayerPower`, `Orbit_PlayerResources`, `Orbit_StatusWidget`, `Orbit_Objectives`, `Orbit_RaidPanel`, `Orbit_MicroMenu`, `Orbit_BagBar`, `Orbit_QueueStatus`, `Orbit_TalkingHead`.
- **Tier 3 (deferred):** Tracked containers + DamageMeter windows are multi-instance — v1 registers **one entity per plugin** (instances share a layer); per-instance entities only if users ask. **Excluded:** `Orbit_TooltipAnchor` (invisible anchor rect), `Orbit_MinimapButton` (tiny, topmost-ish already).

## Phase 4 — Polish, docs, verification

- Optional: Ctrl+Shift+scroll = send to top/bottom; "Reset layer order" button (Global tab, calls `ResetOrder`).
- Docs: rewrite the StrataEngine paragraph in `Core/README.md`/`Core/Infrastructure` README (registration-based, wheel UX); note the wheel triad (plain=padding, Shift=×10, Ctrl=layer) in `Core/EditMode` README. `CONTRIBUTE.md:240` row stays valid.
- Lint gates: `check-localization.py`, `check-comments.py`, `check-readmes.py`.
- Memory: update `project_redflag_fixes_jul2026` (deferred item executed).

**/reload checklist:** fresh profile → order matches `DEFAULT_ORDER`; existing profile → old order kept, Tier-2 systems slot in at sensible ranks; Ctrl+scroll on Player over Target flips which renders on top and survives `/reload` + profile switch; ends clamp with "Top/Bottom layer" feedback; plain wheel still adjusts padding, Shift×10 intact; canvas-active frames ignore Ctrl+scroll; group/boss internal ordering (container −1, member +index) unchanged after a move; action bars all move as one layer; no `ADDON_ACTION_BLOCKED` after a mid-session `/reload` in combat (appliers queue OOC).

## Risks / open decisions

1. **Intra-block ceiling:** member fan-out must stay < 50 (raid 40 ✓, boss 5 ✓) — assert in applier during dev.
2. **CDM frames may be reparented Blizzard frames** — if so, keep their applier writes to the Orbit container only (never level raw Blizzard children); verify at Phase 3.
3. **Tier-3 granularity** (per-plugin vs per-instance for Tracked/DamageMeter) — v1 = per-plugin; revisit on feedback.
4. **Numeric-systemIndex plugins beyond ActionBars** (Datatexts bars?) — the tag rule makes this moot, but confirm each root gets tagged during Phase 3.
