# LibMatic

Shared widget library for Infinite Loop Alchemist addons.

## Modules

### LibMatic-IconPicker-1.0

A modal icon-browser widget any consumer addon can open. Backed by `IconDataProviderMixin` on modern clients with a `GetMacroIcons` / `GetMacroItemIcons` fallback on older flavors.

```lua
local picker = LibStub("LibMatic-IconPicker-1.0")
picker:Open(currentFileID, function(chosen)
    if chosen then
        -- user picked an icon (fileDataID)
    else
        -- user cancelled
    end
end)
```

Public API:

- `lib:Open(currentIcon, callback)` - opens the modal. `currentIcon` is the fileDataID to preview as already-selected (may be `nil`). `callback` is required; called exactly once with the chosen fileDataID or `nil` if cancelled.
- `lib:Close()` - hides the modal without invoking the callback.
- `lib:IsOpen()` - boolean.

### LibMatic-ChangeLog-1.0

An in-game changelog popup any consumer addon can show. Renders a scrolling AceGUI window from a structured spec, version-gates auto-show, and builds the AceConfig styling panel.

```lua
local lib = LibStub("LibMatic-ChangeLog-1.0")
lib:Show({
    title = "MyAddon - Change Log",
    version = "1.2.0",
    footer = "For the full version history see CurseForge.",
    entries = {
        { version = "1.2.0", date = "05/21/2026", sections = {
            { label = "Added", body = "A new thing." },
        } },
    },
    style = MyAddonDB.profile.changeLogPopUp,
})
```

Public API:

- `lib:Show(spec)` - open or re-focus the popup.
- `lib:ShowIfNew(spec, lastSeenVersion)` - show only when `spec.version` is newer than `lastSeenVersion` and `spec.freshInstall` is not set. Returns `wasNewer`.
- `lib:Close()` - hide the popup.
- `lib:BuildOptionsTable(opts)` - return an AceConfig group for changelog styling. `opts.profile` is the live style table; `opts.defaults`, `opts.order`, `opts.name`, `opts.locale` are optional.

When AceGUI is unavailable the library degrades to a `StaticPopup`.

### LibMatic-CombatTextBounce-1.0

Bouncing combat-text animations. Attaches floating, bouncing text to a frame and plays one-off messages.

Public API:

- `lib:AttachBounce(target, opts)` - attach a managed bounce emitter to a frame.
- `lib:Show(text, opts)` - play a single bounce message.
- `lib:SetDefaultFont(path, size, outline, critOutline)`, `lib:SetBounceStyle(style)` / `lib:GetBounceStyle()`, `lib:SetAlwaysBounce(enabled, scale)` - configure behavior.
- `lib:CancelAll()` - stop every active animation. `lib:Demo()` - preview.

### LibMatic-FitText-1.0

Text-fitting mixins. Sizes a button to its label, or scales a font string down to fit a width.

Public API:

- `lib:Mixin(target, padding, canDecrease)` - mix fit-to-text behavior into a button.
- `lib:MixinFontString(fs, maxWidth)` - mix scale-to-fit behavior into a font string.

### LibMatic-RosterFrame-1.0

A resizable, scrollable roster window widget.

```lua
local lib = LibStub("LibMatic-RosterFrame-1.0")
local window = lib:New(config)
window:Show()
```

Public API:

- `lib:New(config)` - create a roster window, returns a window object.
- Window methods: `Show()`, `Hide()`, `Toggle()`, `IsShown()`, `GetFrame()`, `Refresh()`, `ScheduleRefresh()`, `SetHScroll(enabled)`, `SetZebra(enabled)`.

### LibMatic-Startup-1.0

The chat-frame load banner and slash-command help listing. Renders text only; the consumer keeps its own slash-command registration.

```lua
local Startup = LibStub("LibMatic-Startup-1.0")
Startup:Register({
    name = "MyAddon",
    useFactionColors = true,
    optionsCommand = "/myaddon",
    helpCommand = "/myaddonhelp",
    commands = {
        { command = "/myaddon", desc = "Toggle the main window." },
        { command = "/myaddonhelp", desc = "Show this list." },
    },
})
Startup:PrintBanner("MyAddon")
```

Public API:

- `lib:Register(config)` - store a startup profile, returns a handle. Idempotent per `config.name`.
- `lib:Get(name)` - return a stored handle, or `nil`.
- `lib:PrintBanner(handleOrName)` - print the one-line load banner.
- `lib:GetBannerText(handleOrName)` - return the banner string without printing.
- `lib:PrintHelp(handleOrName)` - print the slash-command listing.
- `lib:GetHelpLines(handleOrName)` - return the help listing as a string array.
- `lib:GetAddOnVersion(addonName)` - read the addon `Version` from metadata, `"0.0.0"` on failure.
- `lib:GetBrandColor(handleOrName)` - return the resolved brand color escape.

`useFactionColors` themes the banner Alliance blue or Horde red by `UnitFactionGroup`.

### LibMatic-Settings-1.0

Ace3 page builders for the shared settings UI. Each builder returns a `type="group"` table; the consumer composes the outer options table and keeps its own `RegisterOptionsTable` / `AddToBlizOptions` calls.

```lua
local Settings = LibStub("LibMatic-Settings-1.0")
local args = {
    general   = Settings:BuildGeneralPage({ currentAddon = "MyAddon", nav = { factionIcons = true } }),
    alert     = Settings:BuildAlertPage({ notificationTypes = { { key = "hit", title = "Hit", defaultFormat = "New %s Hit: %d!" } } }),
    sound     = Settings:BuildSoundPage({ slots = { { key = "hit", label = "Hit Sound" } } }),
    social    = Settings:BuildSocialPage({ bind = { table = function() return MyAddon.db.profile.social end } }),
    changeLog = Settings:BuildChangeLogPage({ profile = function() return MyAddon.db.profile.changeLog end }),
    profiles  = Settings:BuildProfilesPage({ db = MyAddon.db }),
}
```

Public API:

- `lib:BuildGeneralPage(opts)` - General page with the cross-addon nav block. `opts.currentAddon` is excluded from the block; `opts.nav.factionIcons` swaps link icons by faction. Each released roster addon with a description renders a word-wrapped blurb under its icon and name, localized by client locale (see `GetAddonDescription`).
- `lib:BuildAlertPage(opts)` - tabbed Alert page. `opts.notificationTypes` generates one editable format-string per type; `opts.bounce` adds the bounce-style picker; `opts.position` adds a "Position Alert" button to the General tab that opens `LibMatic-AlertPositioner-1.0` (carrying `get`/`set`/`default`/`reopen` callbacks plus a `close` callback that hides the settings panel).
- `lib:BuildSoundPage(opts)` - Sound page. `opts.slots` generates one LibSharedMedia sound dropdown per slot; `opts.scan` adds a "Scan for Custom Sounds" button wired to `LibMatic-CustomMedia-1.0:Scan` (`opts.scan.addon` required, optional `name`/`desc`/`order`/`show`/`onResult(sounds, fonts)`). The whole Sound page, dropdowns and scan button alike, requires LibSharedMedia-3.0; without it the page renders a "not available" notice instead.
- `lib:BuildSocialPage(opts)` - Social page channel toggles and optional broadcast formats.
- `lib:BuildChangeLogPage(opts)` - passthrough to `LibMatic-ChangeLog-1.0:BuildOptionsTable`.
- `lib:BuildProfilesPage(opts)` - wraps `AceDBOptions-3.0`. `opts.db` required.
- `lib:ResolveShow(value)` - normalize a `show` flag (nil/boolean/function) to a boolean.
- `lib:GetRoster()` - the canonical ILA addon roster.
- `lib:GetAddonDescription(key)` - the localized one-line blurb for a roster key, resolved by `GetLocale()` (falls back to enUS), or `nil` if the addon has no description.

Every builder and every setting accepts a developer `show` flag (boolean or function). A page whose `show` resolves false returns `nil`; a setting whose `show` resolves false is hidden.

### LibMatic-AlertPositioner-1.0

A drag-to-place positioner for on-screen alerts. Opens a movable overlay with a looping mock crit so the player can see and place the alert, then commits or discards the new position.

```lua
local positioner = LibStub("LibMatic-AlertPositioner-1.0")
positioner:Open({
    get = function() return MyAddon.db.profile.alert.xPct, MyAddon.db.profile.alert.yPct end,
    set = function(xPct, yPct)
        MyAddon.db.profile.alert.xPct = xPct
        MyAddon.db.profile.alert.yPct = yPct
    end,
    default = function() return 50, 50 end,
    reopen = function() MyAddon:OpenSettings() end,
})
```

Public API:

- `lib:Open(opts)` - open the positioner overlay. `opts.get` returns the current `xPct, yPct` (center-relative percentages, 0-100); `opts.set(xPct, yPct)` is called on Accept; `opts.default` returns the default `xPct, yPct` for the Reset button; `opts.reopen` is called after the overlay closes (Accept or Cancel).

Accept commits the dragged position, Cancel discards it, Reset snaps the overlay to the default position. Escape behaves as Cancel. `LibMatic-CombatTextBounce-1.0` is a soft dependency for the mock crit bounce, with a local fallback when absent.

### LibMatic-CustomMedia-1.0

Lets players add their own sounds and fonts by dropping files into fixed slot
folders. Validates that each file is one the WoW client can actually load, then
registers the valid ones into LibSharedMedia-3.0 so they appear in the addon's
existing sound and font pickers.

```lua
local CM = LibStub("LibMatic-CustomMedia-1.0")
CM:Register({
    addon = "GearMatic",
    sounds = 10,
    fonts = 10,
    soundCache = GearMaticDB.global.customSoundSlots,
})
```

Public API:

- `lib:Register(opts)` - register an addon's custom media. `opts.addon` (required) is the addon folder name used to build the slot paths `Interface\AddOns\<addon>\CustomSounds\Sound<N>.ogg|mp3` and `...\CustomFonts\Font<N>.ttf|otf`. `opts.sounds`/`opts.fonts` are slot counts (default 10). `opts.soundCache` is a table the consumer persists in its SavedVariables; the library reads it to register sounds silently at login and mutates it in place on `Scan`. Fonts are validated silently at register time; sounds are registered from the cache. Returns `soundsFound, fontsFound`.
- `lib:Scan(addon)` - re-probe and re-register. Runs the audible sound probe (`PlaySoundFile` + immediate `StopSound`), rewrites the cache, and re-validates fonts. Returns `soundsFound, fontsFound`. Wired to the `BuildSoundPage` scan button.
- `lib:GetRegistered(addon)` - returns `{ sounds = { [index] = lsmName }, fonts = { [index] = lsmName } }` for the addon's currently registered slots.

A player drops a file while the game is closed, renames it to a slot name (`Sound5.ogg`, `Font5.ttf`), relaunches, and (for sounds) clicks the Scan button. LSM names are namespaced per addon (`"GearMatic Custom Sound 5"`) so two ILA addons never collide in the shared registry. Only `.ogg`/`.mp3` (sound) and `.ttf`/`.otf` (font) paths are ever referenced, and each is load-checked, so an unsupported or corrupt file is never registered. A newly added file is only seen after a full game restart, not a `/reload`. `LibSharedMedia-3.0` is a soft dependency; absent, registration is a no-op.

### LibMatic-Broadcast-1.0

Broadcasts a group chat message with optional single-announcer election. With election on, only one group member who runs the addon sends the message; receiver-side dedupe collapses simultaneous triggers. Renders text only.

```lua
local Broadcast = LibStub("LibMatic-Broadcast-1.0")
Broadcast:Register({ name = "MyAddon", commPrefix = "MYADDONBC", electionEnabled = true })
Broadcast:Broadcast("MyAddon", { text = "MyAddon: <name> needs a buff", marker = 8 })
```

Public API:

- `lib:Register(config)` - store a broadcast profile, register the comm prefix, start the peer registry. `config`: `name`, `commPrefix`, `electionEnabled` (default false), `dedupeSeconds` (default 30).
- `lib:Get(name)` - return a stored handle, or `nil`.
- `lib:Broadcast(handleOrName, opts)` - broadcast one message. `opts`: `text` (required), `marker` (required, raid-target icon 1-8), `dedupeKey`, `channel`, `electionEnabled`.
- `lib:GetPeers(handleOrName)` - group members running the addon.
- `lib:SetElectionEnabled(handleOrName, enabled)` - flip the dev flag at runtime.

`electionEnabled` is a code-only developer flag, off by default. `marker` is emitted as the locale-safe `{rt1}`..`{rt8}` token. AceComm-3.0 is a soft dependency; absent, every broadcast is a direct send.

### LibMatic-AlertShadow-1.0

Strength-adjustable drop shadow for alert text and icons.

- `ApplyTextShadow(fontString, strength, opts)` - sets the font string shadow.
  `strength` is 0..1 (0 removes the shadow). `opts.color` (default black),
  `opts.offset` (default `{2, -2}`), `opts.maxAlpha` (default 1).
- `ApplyIconShadow(icon, strength, opts)` - attaches a cached black silhouette
  drop shadow behind an icon texture (icons have no native shadow). Idempotent;
  `strength == 0` hides it. Same `opts` as above plus `opts.scale` (default 1).

### LibMatic-OverlayGuard-1.0

Defers a callback while a native dark overlay is on screen, then replays it.

- `RunWhenClear(fn)` - runs `fn` now if clear, else queues it and runs it when the
  last overlay clears. Callbacks older than `maxHold` (default 10s) are dropped.
- `IsBlocked()` - true while a watched overlay is up.
- `RegisterWatch({ frame = <Frame>, key = "<id>" })` - defer against an extra frame.
- `SimulateOverlay(style, seconds)` -- test hook. Shows a look-alike native screen
  (`"levelup"`, `"renown"`, or `"cinematic"`; default `"levelup"`) for `seconds`
  (default 4), blocking the guard so an alert fired meanwhile is held and replays
  when the look-alike clears. Visuals are skipped headless; the block/flush logic
  always runs.

Default watches self-arm per client: cinematics and movies (all flavors via
`CINEMATIC_*` / `*_MOVIE` events), the EventToast banner frame, and the talking
head frame. A 20s per-watch timeout failsafe plus a 0.5s poll guarantee the queue
never stalls.
- Default watches (MINOR 6): cinematics, movies, loading screens, `EventToastManagerFrame`, `TalkingHeadFrame`, renown (`ExpansionLandingPage`, `CovenantRenownFrame`, `CovenantRenownToast`, legacy `MajorFactionRenownFrame`), `BossBanner`, `LevelUpDisplay`, and any visible or queued `AlertFrame` toast (loot, new appearance, achievement). `RegisterCondition(key, fn)` adds a polled blocker of your own; `UnregisterCondition(key)` removes it.

### LibMatic-Settings-1.0: BuildShadowControls

`BuildShadowControls(spec)` returns a `strength` range node (0..1, step 0.05) wired
to `spec.strength.get` / `spec.strength.set`, named via `spec.names.strength` /
`spec.names.strengthDesc`. Mirrors `BuildCountUpControls`.

### LibMatic-Settings-1.0: family control builders (MINOR 19)

Raw-node builders that return `args` tables (no wrapping group), same shape as `BuildCountUpControls`. Every control is optional: pass a `{ get, set }` binding to emit it, omit it to skip it. Labels inherit the lib defaults; override only through `spec.names`.

- `BuildMinimapControls{ enabled, locked }` - "Show Minimap Button", "Lock Minimap Button". Bind `enabled` as a positive boolean (the builder never inverts a `.hide` field for you, do that in the closure).
- `BuildFrameControls{ frameName, show, locked, scale, alpha, resetPosition }` - for any persistent on-screen frame (readout, widget, log window): "Show <frameName>", "Lock Position", "Scale" (50-200%), "Opacity" (20-100%), "Reset Position" button.
- `BuildAlertIconControls{ enabled, icon = { get, set, options = { { value, label, texture? } } }, size }` - "Show Alert Icon", "Alert Icon" picker (texture markup in the dropdown), "Icon Size" (12-64 px).
- `BuildAnimationControls{ iconPop, iconPopScale, stagger }` - "Animate Alert Icon", "Icon Pop Scale" (1.0-2.0), "Stagger" (0-1 s between simultaneous alerts).
- `BuildTooltipControls{ enabled, otherChars }` - "Show Summary in Tooltip", "Include Other Characters".
- `BuildSoundExtrasControls{ channel, rateLimit, oncePerLogin, inCombat }` - "Sound Channel" (Master/SFX/Music/Ambience/Dialog, `lib.SOUND_CHANNELS`), "Sound Rate Limit" (No limit .. 2 hours in seconds, `lib.SOUND_RATE_LIMITS`, pairs with `LibMatic-SoundThrottle-1.0`), "Play Sound Once Per Login", "Play Sounds In Combat".
- `BuildGeneralExtrasControls{ loginBanner, updateNotify, chatNotifications, debug }` - "Show Login Message" (gate `LibMatic-Startup:PrintBanner`), "Notify Me About Updates" (gate `LibMatic-VersionCheck`), "Chat Notifications", "Debug Mode".
- `BuildAlertGlobalsControls` gained `cooldown` ("Alert cooldown (seconds)", 0-120) and its display-duration reset button is now labeled "Reset Display Duration".

### LibMatic-Settings-1.0: BuildImageSelect

`BuildImageSelect{ name, desc, order, width, imageSize, options = { { value, label, texture? | textures? } }, get, set }` returns one AceConfig `select` node whose dropdown rows carry inline texture previews (`|T...|t`) before the label, the same control GearMatic uses for its indicator and skull pickers. `ImageOptionValues(options, size)` returns the `values`/`sorting` pair on its own for hand-built nodes. Use it for ANY "pick a look" setting: alert icon, marker style, glyph, border, texture theme.

### LibMatic-MarkerPack-1.0

Shared marker textures promoted from GearMatic so every addon can flag items the same way: upgrade/downgrade indicators in two styles (`chevron`, `arrow`, each with an `upAlt` variant) and best-in-slot skulls in three styles (`fanged`, `grim`, `classic`). Files ship under `LibMatic-MarkerPack-1.0/Textures/`.

- `lib:GetIndicator(addon, style, direction, embed?, alt?)` - texture path for `"up"` or `"down"`; unknown styles fall back to chevron.
- `lib:GetSkull(addon, style, embed?)` - texture path for the skull style; default fanged.
- `lib:GetPath(addon, file, embed?)`, `lib:GetBasePath(addon, embed?)` - raw paths, same embed convention as SoundPack (`Libs\LibMatic\`).
- `lib:IndicatorOptionValues(addon, embed?, names?)`, `lib:SkullOptionValues(...)` - AceConfig `values`/`sorting` pairs with inline texture markup, used by `LibMatic-Settings-1.0:BuildMarkerControls{ addon, embed?, indicatorStyle, skullStyle, size }` ("Indicator style", "Skull style", "Marker size" 10-40 px).
- `lib.INDICATOR_STYLES`, `lib.SKULL_STYLES`, `lib.DEFAULT_INDICATOR`, `lib.DEFAULT_SKULL`.

## Embedding

LibMatic is the single source of truth. Consumer addons do not copy the modules in; they reference this repo two ways.

Local development: symlink the consumer's `Libs/LibMatic` at this repo, so the running addon always uses the live library.

```
ln -s /path/to/libmatic <consumer>/Libs/LibMatic
```

The symlink is listed in the consumer's `.gitignore`; it is a local-machine artifact, not committed.

Packaging: the consumer's `.pkgmeta` declares LibMatic as an external, so the BigWigs packager fetches this repo into `Libs/LibMatic` when it builds the release zip.

```yaml
externals:
  Libs/LibMatic:
    url: git@github.com:InfiniteLoopAlchemist/LibMatic.git
    branch: main
```

This repo's own `.pkgmeta` ignore list keeps `docs` and dev files out of consumer packages. Either way the consumer's `embeds.xml` references the modules at the same paths, after LibStub:

```xml
<Script file="Libs\LibMatic\LibStub\LibStub.lua"/>
<Include file="Libs\LibMatic\LibMatic-IconPicker-1.0\LibMatic-IconPicker-1.0.xml"/>
```

LibStub's version arbitration means the highest-minor copy across all loaded addons wins.

## Standalone test

Install the LibMatic folder as a standalone addon, log in, type `/libmatictest` (alias `/lmt`). Subcommands: `changelog`, `startup`, `settings`, `broadcast`. With no subcommand it opens the icon picker.

## License

All Rights Reserved. See LICENSE. LibStub keeps its own original license.
