# RUI — Raegx UI Library

Dark-themed widget library for WoW Midnight (12.x) addons. Registers with LibStub as `"RUI-1.0"`. Embed it the same way as Ace3 libs.

Visual language sourced from **DandersFrames** (a polished unit-frame addon): near-black backgrounds, subtle 1px borders, purple-blue accents, amber section delineation.

---

## Embedding in a consumer addon

```bash
# Copy the entire RUI folder into libs/
cp -r C:/Users/andre/repos/wow/RUI libs/RUI
```

Add to `libs/embeds.xml` (after LibStub, before closing tag):
```xml
<Script file="RUI\RUI.lua"/>
<Include file="RUI\embeds.xml"/>
```

Add to TOC:
```
## X-Embeds: ..., RUI
```

Acquire at runtime:
```lua
local RUI = LibStub("RUI-1.0")
```

LibStub deduplicates — if two addons embed the same version, only the first-loaded copy runs.

---

## Design system

### Color palette (`RUI.C`)

| Key | RGB (0–1) | Use |
|---|---|---|
| `BG` | 0.08, 0.08, 0.08 | Panel canvas background (near-black) |
| `PANEL` | 0.12, 0.12, 0.12 | Section fills, title bars |
| `ELEMENT` | 0.18, 0.18, 0.18 | Button and input backgrounds |
| `BORDER` | 0.25, 0.25, 0.25 | 1px edges on all widgets and panels |
| `ACCENT` | 0.45, 0.45, 0.95 | Purple-blue; title bar separators, active indicators |
| `HOVER` | 0.22, 0.22, 0.22 | Button OnEnter (slightly lighter than ELEMENT) |
| `PRESS` | 0.13, 0.13, 0.13 | Button OnMouseDown (darker than ELEMENT) |
| `TEXT` | 0.90, 0.90, 0.90 | Primary text: headings, values, button labels |
| `DIM` | 0.75, 0.75, 0.75 | Secondary text: labels above controls |
| `GOLD` | 0.72, 0.56, 0.12 | Amber section box borders |

**Rule:** use TEXT for anything the user reads as content; DIM for structural labels that guide layout. Never use full white (1,1,1) — it looks harsh against the dark background.

### Font specs (`RUI.F`)

| Key | Face | Size | Flags | Use |
|---|---|---|---|---|
| `TITLE` | Arial Narrow | 14 | OUTLINE | Dialog title bars |
| `HEADING` | Arial Narrow | 13 | OUTLINE | Panel section headers (`b:Header()`), section box titles (`b:BeginSection()`) |
| `BODY` | Arial Narrow | 12 | OUTLINE | Button labels, checkbox labels, body copy |
| `SMALL` | Friz Quadrata TT | 10 | — | Labels above controls; matches `GameFontHighlightSmall` |
| `VALUE` | Friz Quadrata TT | 11 | OUTLINE | Slider value readouts, numeric displays |

**OUTLINE is required on ARIALN fonts** (TITLE, HEADING, BODY). Arial Narrow anti-aliases aggressively on dark backgrounds without it.

**Do NOT add MONOCHROME.** `"OUTLINE,MONOCHROME"` removes all anti-aliasing and produces crispy/pixelated text.

**SMALL uses Friz Quadrata TT with no flags** — FRIZQT is WoW's built-in UI font, heavier than Arial Narrow and tuned for the engine. It renders cleanly at 10pt without any outline. This is what DandersFrames uses (`GameFontHighlightSmall`).

**Control labels use TEXT color, not DIM.** DIM (0.75) is for description/hint text between controls. Labels directly above sliders, dropdowns, and inputs must use TEXT (0.90) — using DIM makes them look faded and hard to read.

Apply with `RUI.Draw.SetFont(fontString, RUI.F.SMALL)` — includes ARIALN fallback if the font file fails.

### Spacing constants

```lua
RUI.PAD         = 16   -- left/right panel edge padding
RUI.BORDER_SIZE = 1    -- all borders are 1px
RUI.CONTROL_GAP = 4    -- vertical gap below every control row

-- Full row heights (label + control + gap):
-- Slider:      LABEL_H(14) + SLIDER_H(20) + CONTROL_GAP(4) = 38px
-- Dropdown:    LABEL_H(14) + DD_H(24)     + CONTROL_GAP(4) = 42px
-- Input:       LABEL_H(14) + INPUT_H(20)  + CONTROL_GAP(4) = 38px
-- ColorButton: LABEL_H(14) + COLOR_H(24)  + CONTROL_GAP(4) = 42px
-- Checkbox:    CHECK_H(20) + CONTROL_GAP(4)                 = 24px
-- Button:      BTN_H(22)   + CONTROL_GAP(4)                 = 26px
-- Header:      HEADER_H(22) + 14px gap before (not first)

-- Standard widths for ~700px wide Blizzard settings panels:
RUI.DD_W     = 300   -- full-width dropdown
RUI.DD_HALF  = 270   -- left-column dropdown (two-column layout)
RUI.SLD_W    = 430   -- full-width slider
RUI.SLD_HALF = 220   -- right-column slider
RUI.COL2_X   = 306   -- x offset for right column
```

### Draw layer discipline (from DandersFrames)

Always assign textures and frames to the correct layer. Mixing layers causes z-fighting and visual glitches.

| Layer | Use |
|---|---|
| `BACKGROUND` | Solid fill textures (panel bg, section fills) |
| `BORDER` | 1px border edges |
| `ARTWORK` | Control widgets (sliders, inputs, icons) |
| `OVERLAY` | Text that must sit above everything |

For complex frames that need text/icons on top of bars/backgrounds (DandersFrames pattern):
```lua
local overlay = CreateFrame("Frame", nil, parent)
overlay:SetAllPoints()
overlay:SetFrameLevel(parent:GetFrameLevel() + 25)
-- Put FontStrings and icon textures on overlay, not on parent
```

---

## The Builder pattern (most common path)

The Builder manages a y-cursor so you don't track positions manually. It's the right tool for Blizzard settings panels and most dialog content.

### Settings panel (full example)

```lua
local RUI = LibStub("RUI-1.0")
local LSM = LibStub("LibSharedMedia-3.0", true)

function MyAddon:OnInitialize()
    self.db = AceDB:New("MyAddonDB", defaults, true)
    if LSM then
        RUI.Media.RegisterBuiltinSounds(LSM)
        LSM.RegisterCallback(self, "LibSharedMedia_Registered", function(_, mediatype)
            if mediatype == "sound" then RUI.Media.InvalidateSoundCache() end
            if mediatype == "font"  then RUI.Media.InvalidateFontCache()  end
        end)
    end

    local panel = CreateFrame("Frame")
    local b = RUI:Builder(panel, LSM)
    local db = self.db.profile

    b:Header("Display")
    b:MakeSlider("Font Size", RUI.PAD, RUI.SLD_W, 10, 48, 2,
        function() return db.fontSize end,
        function(v) db.fontSize = v; self:ApplyFont() end)

    b:MakeColorButton("Text Color", RUI.PAD,
        function() return db.color.r, db.color.g, db.color.b end,
        function(r,g,b) db.color = {r=r,g=g,b=b}; self:ApplyColor() end)

    b:Header("Sounds")
    b:MakeSoundDropdown("Alert Sound", RUI.PAD, RUI.DD_W,
        function() return db.sound end,
        function(v) db.sound = v end)

    b:FinalizeOnShow()

    local category = Settings.RegisterCanvasLayoutCategory(panel, "MyAddon")
    Settings.RegisterAddOnCategory(category)
    self.optionsCategory = category
end

-- Open from slash command:
-- Settings.OpenToCategory(self.optionsCategory.ID)
```

### All Builder methods

```lua
local b = RUI:Builder(panel, lsm)  -- lsm can be nil

-- Layout
b:Header("Title")                               -- section heading; 14px gap before all but first
b:BeginSection("Box Title")                     -- starts GOLD-bordered box
b:EndSection()                                  -- closes the box
b:MakeSeparator(x, width)                       -- 1px rule; 8px advance
b:Spacer(px)                                    -- advance y by px, no widget
b:GetY() / b:SetY(y)                           -- read/set cursor for two-column layout

-- Widgets (all return the widget frame + Refresh fn)
b:MakeSlider(label, x, w, min, max, step, get, set)         → sld, ref
b:MakeDropdown(label, x, w, getEntries, get, set[,onSet][,name]) → dd, ref, lbl
b:MakeSoundDropdown(label, x, w, get, set[, name])          → dd, ref, lbl
b:MakeFontDropdown(label, x, w, get, set[,onSet][,name])    → dd, ref, lbl
b:MakeColorButton(label, x, get_rgb, set_rgb[, hasAlpha])   → btn, ref
b:MakeInput(label, x, w, get, set)                          → eb, ref, lbl  -- numeric
b:MakeTextInput(label, x, w, get, set[, maxLetters])        → eb, ref, lbl  -- free text
b:MakeCheckbox(label, get, set)                             → wrapper, ref
b:MakeButton(label, x, w, onClick)                          → btn
b:MakeIconButton(iconPath, label, x, w, onClick)            → btn

-- Finalize
b:FinalizeOnShow()   -- sets OnShow + OnRefresh to refresh all controls
```

`getEntries` for `MakeDropdown` must return `{{text=displayName, value=storedValue}, ...}`.

---

## Two-column layout

Save and restore the y cursor to render two columns at the same vertical position, then advance past whichever column is deeper.

```lua
b:Header("Font")
local topY = b:GetY()

-- Left column
b:MakeFontDropdown("Face",    RUI.PAD,   RUI.DD_HALF, ...)
b:MakeDropdown("Outline",     RUI.PAD,   RUI.DD_HALF, ...)
local leftY = b:GetY()

-- Right column (reset y to where the left column started)
b:SetY(topY)
b:MakeSlider("Size",          RUI.COL2_X, RUI.SLD_HALF, ...)
b:MakeColorButton("Color",    RUI.COL2_X, ...)

-- Advance past whichever column reached further down (y values are negative)
b:SetY(math.min(leftY, b:GetY()))
```

Standard two-column widths add up to ≤ 668px (panel width minus 2×PAD):
- Left dropdown (DD_HALF=270) + right slider (SLD_HALF=220) = 490px ✓
- Left dropdown (DD_HALF=270) + right color button = fits ✓

---

## Standalone widget API

Use the `RUI.Widget.*` functions directly when building custom frames outside the Builder. You manage x and y yourself.

```lua
-- All widgets: caller sets position on the returned frame/FontString
local sld, ref = RUI.Widget.Slider(parent, label, x, y, width, min, max, step, get, set)
local dd,  ref = RUI.Widget.Dropdown(parent, label, x, y, width, getEntries, get, set)
local dd,  ref = RUI.Widget.SoundDropdown(parent, label, x, y, width, get, set, lsm)
local dd,  ref = RUI.Widget.FontDropdown(parent, label, x, y, width, get, set, lsm)
local btn, ref = RUI.Widget.ColorButton(parent, label, x, y, get_rgb, set_rgb[, hasAlpha])
local eb,  ref = RUI.Widget.Input(parent, label, x, y, width, get, set)      -- numeric only
local eb,  ref = RUI.Widget.TextInput(parent, label, x, y, width, get, set)  -- free text
local wrap,ref = RUI.Widget.Checkbox(parent, label, get, set)     -- SetPoint on wrap
local fs       = RUI.Widget.Label(parent, text, style)            -- SetPoint on fs
local tex      = RUI.Widget.Separator(parent, x, y, width)
local btn      = RUI.Widget.Button(parent, label, width, height, onClick)   -- SetPoint
local btn      = RUI.Widget.IconButton(parent, iconPath, label, w, h, onClick)
```

All three text fields (`Input`, `TextInput`, `NumberInput`) share one chrome via
`RUI.Draw.MakeInputBox` — ELEMENT fill, 1px BORDER edge, `GameFontHighlightSmall` —
so a field and a dropdown in the same column read as the same control family.
Never build one from `InputBoxTemplate`: that is Blizzard's tan search-field art,
and it anchors its left cap at `x=-5`, putting the field's visible edge 5px left of
everything else in the column.

`Refresh()` re-reads the current value from `get()` and updates the widget. Call it after programmatically changing saved variables.

Dropdown, SoundDropdown, FontDropdown, and Input also return their **label** FontString as a
third value. Use it to grey out a whole row together with its control when a feature is toggled
off — fading only the control while its label stays bright reads as a rendering bug:

```lua
local function SetRowEnabled(ctrl, lbl, enabled)
    local a = enabled and 1 or 0.35
    ctrl:SetAlpha(a)
    if lbl then lbl:SetAlpha(a) end
    if ctrl.SetEnabled then ctrl:SetEnabled(enabled) end
    ctrl:EnableMouse(enabled)
    if not enabled and ctrl.ClearFocus then ctrl:ClearFocus() end
end
```

`ColorButton` keeps its label as a child of the button, so fading the button alone is enough.

---

## Containers

### `RUI.Panel(parent [, name])` → Frame

Base dark canvas. Always start here — every settings panel and dialog root is a Panel.

```lua
local panel = RUI.Panel(UIParent, "MyAddonPanel")
panel:SetSize(600, 400)
panel:SetPoint("CENTER")
```

### `RUI.Section(parent, title, width, height)` → Frame

A pre-sized sub-frame with PANEL background + GOLD border + title label. Use when you need a physically separate frame (e.g., inside a Dialog). For the y-cursor pattern use `b:BeginSection()` instead.

```lua
local box = RUI.Section(dialog.content, "Options", 360, 120)
box:SetPoint("TOPLEFT", dialog.content, "TOPLEFT", 10, -10)
```

### `RUI.ScrollFrame(parent, width, height)` → `{scroll, child}`

Scrollable area. Place widgets on `child`. Set `child:SetHeight()` after populating.

Built on `ScrollFrameTemplate` (never `UIPanelScrollFrameTemplate` — that one is
deprecated and anchors its scrollbar *outside* the frame's right edge, so the bar
hangs off any dialog it sits in). The `MinimalScrollBar` is themed with
`RUI.Draw.StyleMenuScrollBar` and re-anchored 4px inside the frame's border;
`child` is `width - 20` so content never runs under it.

```lua
local sf = RUI.ScrollFrame(panel, 400, 300)
sf.scroll:SetPoint("TOPLEFT", panel, "TOPLEFT", 10, -10)
sf.child:SetHeight(600)  -- taller than scroll area; scrollbar appears automatically
```

### `RUI.Dialog(name, title, width, height)` → Frame

Floating modal. Title bar is draggable. `dialog.content` is the inner frame — pass it to the Builder or place widgets directly.

```lua
local dlg = RUI.Dialog("MyAddonDialog", "Configure", 420, 320)

local b = RUI:Builder(dlg.content, lsm)
b:Header("Settings")
b:MakeSlider(...)
-- No FinalizeOnShow needed; call dlg:Show() when you want to open it.

-- Open/close:
dlg:Show()
dlg:Hide()
```

---

## DandersFrames design principles

These are the conventions that make frames look polished. Follow them for any new frame.

### 1. Everything is dark — no Blizzard default frames
Never use `UIPanelButtonTemplate` or other Blizzard templates for visible UI in your layout. They use the Blizzard light/gold theme and will look jarring against the dark palette. Use `RUI.Widget.Button`, `RUI.Panel`, etc. exclusively for styled surfaces.

### 2. Layer hierarchy is strict
In WoW frames, draw layer order is BACKGROUND → BORDER → ARTWORK → OVERLAY → HIGHLIGHT. Never put a fill texture on BORDER or a border texture on BACKGROUND — it breaks when child frames overlap.

### 3. 1px BORDER-colored edges everywhere
- All interactive widgets: 1px BORDER-colored edges
- All panel containers: 1px BORDER-colored edges
- Section boxes (`b:BeginSection`): 1px BORDER-colored edges + PANEL background

Section boxes use `C_PANEL` (0.12) as background and `C_BORDER` (0.25) for the border — the same subtle treatment as DandersFrames. The ACCENT-colored title inside the box provides the visual hierarchy; a contrasting border color is not needed and looks dated.

### 4. ACCENT drives all section and group headings
The purple-blue ACCENT color appears on `b:Header()` section headings and `b:BeginSection()` box titles — this is the signature visual element that ties the UI together. It should NOT appear on borders, backgrounds, or control labels. The Dialog title bar separator also uses ACCENT as its one accent line.

### 5. Interactive elements must have hover + press states
Every `Button` must change color on OnEnter (→ HOVER), OnMouseDown (→ PRESS), and revert on OnLeave/OnMouseUp (→ ELEMENT). Buttons without hover state feel broken. The `RUI.Widget.Button` handles this automatically.

### 6. Control labels use TEXT; description text uses DIM
Control labels (the small text directly above sliders, dropdowns, inputs) use TEXT color (0.90 grey) — they must be fully legible. DIM color (0.75 grey) is for supplementary description/hint text that sits between controls. Never apply DIM to control labels; it makes them look faded and hard to read.

### 7. Value readouts use VALUE font (Friz Quadrata TT)
Slider value readouts, numeric counters, and stat-like displays use `RUI.F.VALUE` (Friz Quadrata TT with OUTLINE). This gives numeric information a distinct "WoW stat" feel that separates it from label text.

### 8. Movable frames always use SetClampedToScreen
Every draggable frame (`RUI.Dialog` does this automatically) must call `SetClampedToScreen(true)`. Frames that can escape the screen frustrate users.

### 9. Use a content overlay frame for text over bars
DandersFrames puts all text on a child `contentOverlay` frame at frameLevel +25. Do the same any time text must sit above a StatusBar or texture — set the overlay frame's level explicitly rather than relying on draw layer alone.

```lua
local overlay = CreateFrame("Frame", nil, parent)
overlay:SetAllPoints()
overlay:SetFrameLevel(parent:GetFrameLevel() + 25)
local nameText = overlay:CreateFontString(nil, "ARTWORK")
```

### 10. Alpha fades over Show/Hide for smooth UX
When hiding a frame conditionally (e.g., out-of-range fading), animate alpha rather than toggling visibility:
```lua
frame:SetAlpha(0)   -- invisible but still in layout
frame:SetAlpha(1)   -- visible
-- NOT: frame:Hide() / frame:Show() for conditional display
```

### 11. Anchoring inside a panel
Always anchor child widgets to their **panel**'s TOPLEFT, not to each other in a chain. Chained anchors cause the entire layout to shift if one element changes size. The Builder enforces this — it anchors every widget to `self.panel` via explicit (x, y) offsets.

---

## Common anti-patterns

| What | Why | Instead |
|---|---|---|
| AceConfig/AceGUI for sound selection | AceGUI Dropdown can't host per-row buttons | Use `b:MakeSoundDropdown` (native DropdownButton) |
| `frame:Show()` / `frame:Hide()` for range fading | Abrupt; no animation | Use `frame:SetAlpha()` transitions |
| Anchoring child to previous child | Layout breaks if a widget resizes | Anchor all widgets to `panel` TOPLEFT with explicit (x, y) |
| Full white text (1,1,1) on dark bg | Harsh, eye-straining | Use `RUI.C.TEXT` (0.9) for content, `RUI.C.DIM` (0.75) for labels |
| BORDER layer for fill textures | Renders on top of border edges | Use BACKGROUND layer for fills |
| Using a single frame for drag + content | Drag handler taints content clicks | Use a separate drag overlay (DandersFrames pattern) |
| `Settings.OpenToCategory(id)` with hardcoded id | Midnight assigns IDs at runtime | Store the returned `category` object; use `category.ID` |
| `b:FinalizeOnShow()` skipped | Controls show stale values on first open | Always call it; Blizzard Settings calls `OnRefresh` on navigate |

---

## LSM integration reference

```lua
-- OnInitialize:
RUI.Media.RegisterBuiltinSounds(LSM)
LSM.RegisterCallback(self, "LibSharedMedia_Registered", function(_, mediatype)
    if mediatype == "sound" then RUI.Media.InvalidateSoundCache() end
    if mediatype == "font"  then RUI.Media.InvalidateFontCache()  end
end)

-- Play a sound stored by LSM name:
RUI.Media.PlaySound(LSM, db.sound)   -- no-ops on "None" or bad path

-- Resolve a font path:
local path = RUI.Media.GetFontPath(LSM, db.fontName)
fontString:SetFont(path, db.fontSize, db.fontFlags)
```

---

## Migrating from AddonOptionsPanel

`RUI:Builder()` is a drop-in superset. Change two lines:

```lua
-- Before
local AOP = LibStub("AddonOptionsPanel-1.0")
local b = AOP:New(panel, lsm)

-- After
local RUI = LibStub("RUI-1.0")
local b = RUI:Builder(panel, lsm)
```

All `b:Header`, `b:MakeSlider`, `b:MakeDropdown`, `b:MakeSoundDropdown`, `b:MakeColorButton`, `b:MakeInput`, `b:MakeButton`, `b:BeginSection`, `b:EndSection`, `b:GetY`, `b:SetY`, and `b:FinalizeOnShow` calls are unchanged.

New methods added: `b:MakeCheckbox`, `b:MakeFontDropdown`, `b:MakeSeparator`, `b:Spacer`, `b:MakeIconButton`.

Remove the AddonOptionsPanel embed once migrated (delete from `libs/embeds.xml` and `X-Embeds`).
