Details

Addon ID: 24608
Addon Version: 0.3.0
Expansion: 1.60
Upload date: Oct 06, 2026
Last Updated: Oct 06, 2026
Downloads: Less than 100
Website: Source Link

Expansion


Categories


Developers

Pmptasty
Pmptasty Creator

LibAceGUIWidgets

LibAceGUIWidgets is a shared AceGUI-3.0 widget and styling library — the common UI toolkit behind the TOG suite of addons. It registers reusable, drop-in AceGUI widget types and helpers so addons share one consistent look instead of each rebuilding the same frames.

This is a library, not a standalone addon. It has no window of its own — you only need it because another addon lists it as a dependency, which CurseForge installs automatically. Embeddable via LibStub. Its one setting is /lagw debug, which shows or hides its messages for addon authors (off unless an addon turns it on).

What it provides

  • ClearFrame — a movable, resizable main window with a themed title bar, saved size/position, and a bottom status bar (status text, Close button, info “i” tooltip, and an optional settings gear).
  • GroupFrame — a lightweight borderless container.
  • TLabel — a label + icon widget with tooltip support.
  • DatePicker — a date field with a calendar icon and a month popup (previous/next, click a day, Today, Clear); typing a date works too. An addon can fence it to a date range, make the date required so it can never be emptied, choose which day the week starts on, and tell it whose “today” to use — the realm’s, not your computer’s.
  • RowList — a virtualized scrolling data list (value-aware sortable headers, alt-row banding, class-color columns, stateful action icons, per-column header tooltips, row-hover callbacks, slim scrollbar, icon columns that draw a per-row picture such as a gold or grey coin while still sorting, and blanks that stay at the bottom either way). Sorting is stable, so identical rows keep the order they were given and two players looking at the same shared data see the same list; a column can sort on a different value than it displays, and can own its own cell tooltip separate from the row’s. An addon can scroll the list to a given entry and put a lasting selection highlight on it that stays with that entry through scrolling and re-sorting. A single cell can answer its own click — so one column reads as a link while every other click on it still does what the row does — and a column can strike through its text for rows that are done, won or expired, so they stay visible instead of vanishing. A list can also change its columns while it is open, for tables whose columns come from your data (one per raid, per season, per category). Click a column’s header for a filter menu — sort either way, then tick the values you want to see, with the choices in each column narrowing to what the other columns still show, grouped where it helps (a zone under its continent) and searchable — and a search box finds text anywhere in the list. Columns can size themselves to their widest entry, right-align numbers, and open high-to-low on the first click. Item, spell and player links in a list can be hovered and clicked like links in chat. Addons also get +/- tree rows for parents with children under them, tick-box columns, text that shortens to fit its column (“Ann, Bob +3”), clickable cells with their own tooltip, and rows that are read-only for players who may look but not change. A list can also light the row under your pointer, show group headings with indented rows under them, let you drag rows into a new order, go back to its original order on a third header click, search by words (including text a column does not show), reopen where you left it scrolled, show a button only on the rows it applies to, and size itself to its rows. An addon can also ask which cell on screen shows a given entry, to place a popup beside it.
  • Menus & scrolling — an anchored dropdown menu with cascading submenus and multi-select (check-marked rows that stay open and update live), placement control, click-outside to close, drawn above the window — plus a slim-scrollbar scroll box for content that overflows. Entries can also be headings, greyed out or spacers, carry their own tooltip, and individually decide whether clicking them closes the menu. The menu is shared by every addon on the library, so an addon can ask to close it only if it is the one that opened it. A long menu stops at what fits on your screen and tells you how many more there are, rather than running off the bottom. A menu, or a dropdown box, can carry its own search box, so a list of several hundred choices opens on the few that matter and narrows as you type. A choice whose action fails still closes the menu, and the error still reaches your error display.
  • Form helpers — a labelled dropdown box, a prompt/confirm dialog, a checkbox bound straight to a setting, and a search box (with a tokenised any-field matcher), which also comes as a ready-made AceGUI widget that drops straight into an addon’s row of filters, and a filter-strip layout that lines a row of controls up for you — every caption on one line, every control centred on one line under them, the same gap between each, wrapping onto a second row when the window is narrow — the pickers and inputs consumers were each hand-rolling. The dialog answers Enter and Escape even with nothing to type in, can show a single OK button, tells the addon when it opens and closes so a modal count always balances, and can be withdrawn by the addon that put it up — only ever its own prompt, never another addon’s. There is also a small form prompt for the two or three things an officer has to type — labelled fields, an error line that appears without the window closing, OK and Cancel, rows that pick from a list, show an item you can hover (with the addon’s own hover and click, such as hold-to-compare, if it has them) or are a tick-box, a small grey note under any field, a paragraph of text that can change each time it opens, and a hint line at the top; it can open beside the row or button that opened it — and a button that explains why it is greyed out, so a control you cannot press says what would make it pressable instead of just sitting there dead.
  • Copy boxes — the game will not let an addon put text on your clipboard, so a link or an error line is shown selected in a box, ready for Ctrl+C, with a copy icon drawn in the same style as the search box’s magnifying glass. The box cannot be typed over by accident: a stray key puts the text back. It comes as a popup or as a field inside a window.
  • Click bindings you set by doing them — a setting where you hold Alt, Ctrl and/or Shift and click to choose what a click does, such as “Alt + Left Click”, with a Default button. A plain click on its own is refused, because it would fire on every ordinary click.
  • Expandable list — a scrolling datasheet of collapsible label + value groups; a +/- toggle opens each to its indented child rows, any number of levels deep. A line can have its own click, its own tooltip and its own colour. Pooled rows, persistent expand state — for EP breakdowns, statsheets, and any drill-down tree.
  • Version & update helpers — a one-call version resolver for a status-bar / about line (the TOC version with the packager’s git-tag prefix stripped) and a one-call VersionCheck-1.0 hook-up (the “a guildmate is running a newer build” nudge) — so every suite addon wires these the same way instead of hand-rolling them.
  • Resize framework — drag grips, minimum and maximum bounds, a live re-layout callback while the drag is happening, and saved size, for any window — not just the library’s own frame types.
  • Window memory — one call gives a window a saved position and size that survives close, reopen and reload, with a floor a saved size can never fall below. The floor follows the size slider live on every kind of window, and a window it had to grow is saved at the grown size. When the window closes it lets go, so the game’s reused frame does not carry that floor into the next addon’s window.
  • Window docking — a window can attach itself to the edge of another one, follow it as it moves, stay put on screen when that window closes, re-attach when it opens again, and undock when you drag it away. When the window it is attached to closes for good, it lets go, so it never follows that window’s reused frame into another addon.
  • Bottom-row icons — the little cluster of icons beside a window’s Close button (settings, help, share, stop), laid out to one set of numbers so every window in the suite puts them in the same place at the same size. Each carries its own tooltip, read at the moment you hover so an icon can say “nothing to stop” when there is nothing to stop.
  • Selected-tab highlight — a soft accent band behind the selected tab’s label and a fainter one on mouse-over, because the stock AceGUI tab only greys out when chosen.
  • Button cooldowns — a rate-limited button counts itself down in its own caption, dims, and refuses the click until it is ready. The time left comes from the clock, so a loading screen cannot leave it stuck.
  • Breathe and Flash — the suite’s shared attention-getting fade, one effect with one set of numbers, plus a flash that stops by itself and a glow around the edge of the screen.
  • Symbol font — a bundled font (DejaVu Sans) and named symbols (arrows, check, cross, warning, ≤, ≥, ±, bullet, star, play, refresh) for labels the game’s fonts cannot draw.
  • One size slider, for the visually impaired — an addon can offer a single size setting from 80% to 200%, and everything the library draws grows with it together: text, icons and row heights in every list, menu, dialog, window bar, label and date picker. Windows keep the size you dragged them to and are only nudged bigger when their enlarged rows would no longer fit. The setting belongs to the addon that offers it, per client, and is never sent to anyone else.
  • Window settings — one window’s own scale, its background see-through, named size presets (a resizable one remembers the size it was closed at), a saved window brought back onto a smaller screen, and Escape that closes the newest popup before the window; plus a toolbar / side panel / detail panel layout, a quantity box with −, + and MAX, and a combat-safe button that casts or uses something.
  • Mistakes that speak up, to the people who can fix them — a few addon-side mistakes that used to fail silently are now caught: a list given no room to draw its rows, two list columns sharing a name, two addons picking different theme colours, and a window given two ways to remember its position. Each is kept in a record an addon’s own tests can check is empty, and printed as one line saying what went wrong and how to fix it — only in debug mode, so players never see them. /lagw debug turns that on or off (/lagw debug on, off, or reset to let addons decide), and it is remembered.
  • Clean hand-back of reused frames — the game reuses the frames addons build windows in, so anything an addon adds to one through the library (scripts, extra frames, tooltips) is taken off again when it closes and never turns up in the next addon’s window.
  • Polite redraws — an addon can check whether you have one of its menus open or are typing in one of its boxes, and wait until you are done before redrawing.
  • Plus brand/style tokens and small helpers (branded labels, auto-flipping tooltips that can be made wider for long help, popups placed under the thing that opened them, floating panels that close on a click outside, resize bounds, the accent colour as numbers).

The widgets are addon-agnostic: each consumer supplies its own accent color, class-color table, tooltip owner and, if it offers one, its size setting, so the library owns structure and styling while your addon keeps its identity. It composes with LibLocaleOverride for localized tab and dropdown fonts.

Requirements

  • Ace3 (for AceGUI-3.0) — required.
  • VersionCheck-1.0 — optional; only the update-reminder helper uses it, and the library works fine without it.

Recent Updates

v0.2.9 — quiet by default, a /lagw switch, compare-on-hover in forms, and a menu that closes on a failed choice

New

  • /lagw debug. Turns the library’s messages for addon authors on or off. /lagw debug on and off win over whatever an addon chose, /lagw debug on its own switches it, and /lagw debug reset lets the addons decide again. It tells you which way it is set, and remembers it.
  • An item in a small form window can keep its addon’s own hover and click. An addon that shows the comparison tooltip while you hold the compare key, or uses its own chat-link click, now keeps that on the item line of the library’s form window. Asked for by TOGProfessionMaster, whose bank request window had lost hold-to-compare.

Changed

  • Messages meant for addon authors no longer appear in your chat. Lines such as “RowList has 1 row(s) but is showing none” describe a mistake in an addon’s code that a player cannot fix, and one turned up in chat between guild messages. They now appear only in debug mode. Addons’ own tests still see every one.
  • Under the hood. The offline test suite is at 1011 checks.

Fixed

  • A menu stayed open when the choice you clicked failed. If the action behind a menu choice hit an error, the menu stayed on screen and anything waiting for it to close kept waiting. It now closes, and the error still goes to your error display. Found by TOGProfessionMaster’s tests.

v0.2.8 — a copy box with a copy icon, a click you set by doing it, and a docked window that lets go

New

  • A copy box for links and error lines. The game will not let an addon put text on your clipboard, so every addon was building its own “press Ctrl+C” popup. There is now one: the text shows already selected, with a copy icon in the same style as the search box’s magnifying glass, and a stray keypress cannot wipe it — the text is simply put back. It can pop up, or sit as a field inside a window, like the Discord invite in TOGTools. Asked for by TOGTools.
  • Choose a click by doing it. A setting where you hold Alt, Ctrl and/or Shift and click to say which click an addon should listen for, with a Default button to go back. A plain click on its own is refused, because it would go off on every ordinary click. Built so ItemDB’s “where to get it” click can move off Alt + Left Click, which Gargul also uses.
  • A docked window can be let go of. An addon can detach a window from the one it is docked to, leaving it where it is on screen.

Changed

  • Under the hood. The offline test suite is at 992 checks, and every line of the library’s code, including the new file, is exercised by it. Two older changelog versions moved to the archive file to keep the release notes under GitHub’s size limit.

Fixed

  • A docked window could jump onto another addon’s window. The game reuses the frames addons build their windows in. A window docked to one of those stayed tied to the frame after its window closed, so when another addon opened a window with that frame, the docked window attached itself there. It now lets go automatically when the window it was docked to closes. Reported by TOGTools.

v0.2.7 — a tick-box, a note and a paragraph in the small form prompt

New

  • The small form prompt can hold a tick-box. A line can be a tick-box with its label beside it, such as “clear the balances of characters who left the guild”, instead of a box to type in. Asked for by Dibs, which moved its two officer windows onto the library because of it.
  • A short note under any line of the form. A small grey line under a field can explain it (“negative to deduct”), change while the window is open, or go away with no gap left behind.
  • A paragraph of text at the top of the form, rewritten each time it opens — who is away and for how long, say — with the window growing or shrinking to fit.
  • An addon can find the cell a list shows an entry in, to place a popup beside it or to check it in its own tests.

Changed

  • Under the hood. A form prompt that uses neither addition is laid out exactly as before, and the tests check that first. The offline test suite is at 947 checks, and every line of the library’s code is exercised by it.

v0.2.6 — the suite orange by default, and nothing left behind on a closed window

New

  • Lists that do more. The row under your pointer can light up. Rows can be dragged into a new order. A list can show group headings with their rows under them, and indent rows to show what belongs to what. A third click on a column’s title can put the list back in its original order. Search finds every word you type in any order, and can look at text a column does not show — so “stamina” finds every recipe that gives stamina. A list can reopen where you left it scrolled, show a button on only the rows it applies to, and size itself to its rows.
  • Window settings. An addon can offer a scale for one window on its own, a background see-through setting, and switching between size presets (a fixed compact size and a free-to-resize one, each remembering its own size, even when you close the window without switching). A window saved on a bigger screen comes back fitted to yours.
  • Escape closes the newest popup first. With a small window open over a big one, Escape closes the small one and leaves the big one open; the next press closes that.
  • A quantity box. − and + buttons, a box to type in and a MAX button, with the buttons greying out at the limits and saying why.
  • A button that casts or uses something from an addon’s window, through the game’s own secure path, that holds any change asked for in combat until combat ends.
  • A window layout with a toolbar along the top, a side panel, a detail panel along the bottom and the main list filling what is left, which keeps up as you resize.
  • A flash that stops by itself to catch your eye, and a glow around the edge of the screen.
  • Wider help tooltips, so several paragraphs are not squeezed into a tall narrow column, and trees any number of levels deep in expandable lists (a profession, its specialisations, their members).
  • The small form prompt can offer a list to pick from, or show an item you can hover and shift-click.

Changed

  • The default colour is the suite orange, not gold. Only addons that never chose a colour see the change. Gold is the game’s own colour for normal text, not the suite’s.
  • Under the hood. The game reuses the frames addons build their windows in. Anything these additions put on a window is taken off again when it closes, so it cannot turn up in the next addon’s window. The offline test suite is at 925 checks, and every line of the library’s code is exercised by it.

Fixed

  • A list could show one row fewer than it had room for at some size-slider settings, because the row height was not a whole number of pixels.

v0.2.5 — a closed window takes its size limits with it, one search box, and rows of filters that line up

New

  • One search box for every suite window. Questbook and TOGBankClassic each carried their own copy of the search field that sits in a row of filters. It now lives in the library, so both behave the same and a fix to one reaches both. It grows with the size slider, as the rest of the library does. Asked for by Questbook, which already uses it.
  • Rows of filters that line up. A row of search boxes, drop-downs, boxes to type in, tick-boxes and buttons came out ragged: the game’s own controls each put their label and their box at different heights, so no two kinds sat level and the gaps between them were uneven. The library now lays such a row out itself: every caption on one line, every control centred on one line under them, the same gap between each, and a second row when the window is too narrow for one. It grows with the size slider. Asked for by Questbook; FastGuildInvite’s filters can use it too.

Changed

  • Under the hood. The offline test suite is at 714 checks, and every line of every file the library ships is now exercised by it — including the few in the date picker, the resize code and the start-up file that had no test before. The developer notes were checked against the code and brought up to date.

Fixed

  • Closing a remembered window could make another addon’s window grow. The game reuses the frames addons build their windows in. A window that remembered its size left its minimum size and its link to the size slider on the frame when it closed. The next time you moved the slider, whichever window had been given that frame since grew to match. TOGBankClassic measured a closed Guild Bank window’s frame growing from 900 to 1568 wide. Closing a window now clears both. Reported by TOGBankClassic.

Older versions (v0.2.4 and earlier) and full developer notes are in the library's CHANGELOG.md (ships with the library).

For addon authors

Everything below is for people building against the library. If you are a player, you can stop at Requirements — there is nothing here to configure.

local W = LibStub("LibAceGUIWidgets-1.0", true)   -- silent: nil if not installed
if not W then return end

local minor = LibStub.minors["LibAceGUIWidgets-1.0"] or 0
if minor >= 25 then rl:SetSort("when", true) end  -- open the list already sorted

-- MINOR 26: give any frame the grips, bounds, live callbacks and size persistence
if W.MakeResizable then
    local h = W:MakeResizable(myWindow, {
        minW = 400, minH = 200, maxW = 1200, maxH = 900,
        grips  = "SE S E",            -- a SET of grips, not one corner; `false` for none
        status = MyDB.window,         -- width/height/left/top, written when a drag ends
        onResize     = function(f, w, ht) list:Refresh() end,  -- LIVE, once per draw
        onResizeStop = function(f, w, ht) rebuildSomethingSlow() end,
    })
    h:SetEnabled(false)               -- a locked window: grips hidden, drags refused
end

-- MINOR 27: a row click tells you WHICH button and hands you the row frame to anchor to
if minor >= 27 then
    opts.onRowClick = function(entry, idx, rl, button, rowFrame)
        if button == "RightButton" then
            W:OpenMenu(rowFrame, { { text = "Whisper", onClick = function() whisper(entry.name) end } })
        end
    end
end

-- MINOR 28: a date picker, window memory, the shared breath, and a font that draws symbols
if AceGUI:GetWidgetVersion("LAGW-DatePicker") then
    local since = AceGUI:Create("LAGW-DatePicker")
    since:SetLabel("Since")
    since:SetCallback("OnValueChanged", function(widget, event, ts)   -- ts = local midnight, or nil
        filter.since = ts; refresh()
    end)
end
if W.PersistWindow then
    W:PersistWindow(window, MyDB.char.windows.mailbox, {   -- YOUR table; the library keeps no copy
        width = 560, height = 480, minWidth = 420, minHeight = 300,
    })
end
if W.Breathe then W:Breathe(urgentLine) end                -- 1 -> 0.35 -> 1, eased, looping
if W.StopBreathing then W:StopBreathing(urgentLine) end    -- Stop + alpha 1
if W.SymbolFont then                                       -- a font that CAN draw arrows / checks / warnings
    statusLine:SetFontObject(W.SymbolFont)
    statusLine:SetText((W:Symbol("check") or "[OK]") .. " Synced")
end

-- MINOR 30: the picker knows its range and whose "today" it is; menus get titles, disabled
-- rows, per-row tooltips and a per-row close; lists sort stably; two new controls.
if minor >= 30 then
    when:SetRange(lastYear, nextYear)        -- outside days disabled, month paging clamps
    when:SetTodayFunc(function()             -- the SERVER's day, not the box's
        local t = C_DateAndTime.GetCurrentCalendarTime(); return t.year, t.month, t.monthDay
    end)
    when:SetRequired(true)                   -- no Clear; an emptied field reverts
    when:SetFirstWeekday(2)                  -- 1 = Sunday (default) .. 7 = Saturday

    W:OpenMenu(anchor, {
        { text = "Ranks", isTitle = true },                          -- a heading, inert
        { text = "Officer", checked = isOfficer, onClick = toggle }, -- keeps the menu open
        { text = "", disabled = true },                              -- a spacer
        { text = "Re-process", keepOpen = false, onClick = run,      -- ... but THIS one closes it
          tooltipTitle = "Re-process", tooltipText = "Applies the filter again." },
    }, { keepOpen = true })

    local _, token = W:ShowDialog(parent, { prompt = "Delete?", hasEdit = false, cancelText = false,
        onShow = function(d) CalendarFrame_PushModal(d) end,   -- balanced: onHide runs once,
        onHide = function(d) CalendarFrame_PopModal(d) end,    -- however it closes
        onAccept = doDelete })                                 -- Enter accepts, Escape cancels
    W:CloseDialog(token)          -- dismiss YOUR prompt; a no-op if the shared frame moved on

    rl = W.RowList:New(parent, {                               -- ties keep SetData order now
        tiebreak = function(a, b) return a.id < b.id end,      -- ... and this decides them first
        columns = { { key = "received", sortValue = function(e) return e.sortKey end,
                      onCellEnter = function(entry, idx, list, cellFrame) showSourceTip(entry, cellFrame) end,
                      onCellLeave = function() GameTooltip:Hide() end } },
    })

    local cb = W:CreateCheckbox(panel, { label = "Show world buffs", tipTitle = "World buffs",
        get = function() return db.show end, set = function(on) db.show = on end })
    W:DockWindow(myWindow, { to = CalendarFrame, point = "TOPLEFT", relPoint = "TOPRIGHT",
        y = -16, snapDistance = 40, status = db.window })      -- docked by default; drag to undock
end

-- MINOR 31: jump to a row and mark it; close only YOUR menu; a floor that is a function of the scale
if minor >= 31 then
    rl:ScrollToEntry(function(q) return q.id == questId end)   -- top row where the clamp allows
    rl:SetSelected(function(q) return q.id == questId end)     -- a lasting accent highlight, by entry
    local picked = rl:GetSelected()

    if not W:CloseMenuFor(nameBox, otherBox) then end          -- false: the open menu is not yours

    W:PersistWindow(window, db.window, {                       -- the floor follows the slider live,
        minWidth = function(s) return 784 * math.max(1, s) / s end,   -- on EVERY window kind; and a
        minHeight = 400,                                       -- part-fixed-art floor as a function
    })
end

-- MINOR 33: the bottom-row icons, a button that says why it is greyed, the officer form, live columns
if minor >= 33 then
    local icons = W:DressBottomRow(window, {
        { key = "gear", texture = "Interface\Icons\Trade_Engineering", texCoord = { 0.08, 0.92, 0.08, 0.92 },
          tipTitle = "Settings", tipBody = "Open the options.", onClick = openOptions },
    })
    -- ... and W:UndressBottomRow(window) when it closes: AceGUI pools the frame
    local btn = W:CreateExplainedButton(panel, { text = "Award", tipTitle = "Award",
        tipBody = "Give the points.", onClick = award })
    btn._lagwReason = "Only officers can award points."    -- drawn in red under the body when greyed
    rl:SetColumns(columnsFor(raids))                       -- a live re-column; identical sets are a no-op
    assert(#W:Diagnostics() == 0)                          -- in YOUR spec: nothing this library reported
end

-- MINOR 34: a RowList that filters, searches, sizes itself, takes links and lets go of its parent
if minor >= 34 then
    rl = W.RowList:New(host.content, {
        hyperlinks   = true,                               -- |H links hover and click like chat
        headerMenu   = true,                               -- every header opens the column menu
        externalSort = true, onSortChanged = function(key, desc, list) resortTree(key, desc) end,
        tooltipOwner = MyTooltipOwner,                     -- this list only; lib.config is session-wide
        columns = {
            { key = "open", expander = true, width = 16, onToggle = function(e, open) expand(e, open) end },
            { key = "name", header = "Name" },
            { key = "zone", header = "Zone", autoFit = true, minWidth = 60, filterable = true,
              filterGroup = function(e) return e.continent end },
            { key = "mem",  header = "Memory", width = 60, align = "RIGHT", sortDescDefault = true,
              gapBefore = 8 },
            { key = "rank", header = "Rank", width = 50, button = true,
              onClick = function(e) editRank(e) end, cellTip = function(e) return e.rankNote end },
        },
    })
    rl:SetSearch(searchBox:GetText())                      -- nil or "" clears
    host:SetCallback("OnRelease", function() rl:Detach() end)   -- AceGUI reuses host's frame
    -- entry._readonly = true greys that row's checkbox and button cells
end

-- MINOR 35: the search box as an AceGUI widget; a released persisted window is forgotten
if AceGUI:GetWidgetVersion("LAGW-SearchBox") then
    local box = AceGUI:Create("LAGW-SearchBox")
    box:SetPlaceholder("Quest or zone")                    -- nil restores "Search"
    box:SetCallback("OnTextChanged", function(widget, event, text) rl:SetSearch(text) end)
    box:SetText(db.query)                                  -- silent: a redraw cannot loop
    strip:AddChild(box)
end
if W.NewStrip then                                         -- captions on one line, controls on one
    local strip = W:NewStrip()                             -- line, one gap; wraps when narrow
    window:AddChild(strip)
    W:StripAdd(strip, searchBox)                           -- controls go in UNLABELLED
    W:StripAdd(strip, zoneDropdown, "Zone")                -- the caption, over its control
    W:FitCheckBox(forMe); W:StripAdd(strip, forMe)
    local rowWidth = W:StripRowWidth(strip)                -- the real row, for your window floor
end
if W.ForgetWindow then W:ForgetWindow(window) end          -- stop persisting an open window;
                                                           -- Release already does this for you

-- MINOR 36: pool-safe additions, window settings, two-stage Escape, a stepper, a secure button, a dock
if minor >= 36 then
    W:WidgetFrameScripts(widget, { OnMouseDown = onDown }) -- raw scripts, put back on Release
    W:AttachWidgetTooltip(widget, "Title", "Body")         -- no HookScript left on the pooled frame
    W:OnWidgetRelease(widget, "mine", function(w) cleanUp(w) end)

    W:SetWindowScale(window, db.scale)                     -- 0.5 - 1.5, keeps the window's own anchors
    W:SetWindowOpacity(window, db.alpha)                   -- backdrops only; stock again on Release
    W:SetWindowProfile(window, "compact", { width = 420, height = 300, resizable = false })
                                                           -- after PersistWindow; locked turns resizing off;
                                                           -- a resizable one saves its size on Release too
    W:ClampWindow(window)                                  -- back onto the screen (PersistWindow does it)
    W:EscapeLayer(window, { popup, dialog })               -- Escape: newest popup first, then the window
    W:AttachRawFrames(widget, myIcon)                      -- hidden, unanchored, back to UIParent on Release

    W:ShowTooltip(anchor, "Help", longText, { minWidth = 320 })   -- a wider floor, put back on hide
    W:AnchorPopup(popup, row, { gap = 2 })                 -- under the row, or over it near the bottom
    W:Flash(icon, { times = 3 }); W:FlashScreen()          -- stops by itself; an edge glow in the accent
    if not (W:IsMenuOpenFor(window) or W:IsInputFocusedIn(window)) then redraw()
    else W:OnMenuClosed(window, redraw) end                -- one-shot, per owner

    local st = W:CreateStepper(panel, { min = 1, max = function() return canMake() end,
        step = 1, maxButton = true, onValueChanged = function(v) db.qty = v end })
    st:Refresh()                                           -- on BAG_UPDATE; never wipes what is typed

    local b = W:CreateSecureActionButton(myOwnFrame, { text = "Craft" })   -- nil in combat
    b:SetAction({ type = "macro", macrotext = "/cast Tailoring" })        -- queued in combat

    local dock = W:NewDockLayout(tabGroup, { top = 28, right = 220, bottom = "auto" })
    rl = W.RowList:New(dock.center, { hoverHighlight = true, sortCycle = "three",
        reorderable = true, onReorder = function(from, to) move(from, to); rl:SetData(data) end,
        isHeader = function(e) return e.isGroup and e.title end, indentStep = 12,   -- or e._header / e._indent
        searchText = function(e) return e.statText end,    -- the search reads it; words in any order
        fitContent = { maxRows = 12 },                     -- the parent's height follows the rows
        onScroll = function(list, offset) db.scroll = offset end,
        columns = { { key = "src", button = true, width = 40, show = function(e) return e.inBank end,
                      text = function(e) return "[Bank]" end, tip = function(e) return "Bank", age(e) end },
                    { key = "icon", icon = function(e) return e.texture end, width = 18, iconTexCoord = true },
                    { key = "who", format = function(v, e, width, measure) return fit(v, width, measure) end } } })
    rl:SetScrollOffset(db.scroll or 0)                     -- reopen where the player left it

    W:CreateExpandableList(panel, {}):SetData(tree)        -- children may carry children, any depth
    W:CreateFormDialog({ rows = { { label = "Reagent", kind = "dropdown", items = listReagents },
                                  { label = "Item", kind = "item", link = itemLink } } })
end

-- MINOR 37: a tick-box row, per-row notes and a body paragraph in the form prompt; the cell a list
-- shows an entry in
if minor >= 37 then
    local f = W:CreateFormDialog({ name = "MyAdjust", title = "Adjust", rows = {
        { label = "Amount", numeric = true, note = "Negative to deduct." },
        { label = "Clear balances of characters who left the guild", kind = "check", value = false,
          onChanged = function(on, dialog) db.clearLeavers = on end },
    } })
    f:SetNote(1, isMasterLooter and "Awarded to the whole raid." or nil)   -- nil: no line, no gap
    f:SetBody(absenceSummary())                           -- a paragraph over the rows; height follows
    local clear = f.fields[2]:GetValue()                  -- a boolean; SetValue(bool) is silent

    local cell = rl:CellFrameFor(function(e) return e.id == id end, "source")
    if cell then W:AnchorPopup(popup, cell) end           -- the overlay on an onCell* column, else the
                                                          -- cell; nil when scrolled off or not in view
end

-- MINOR 38: a read-only copy box (popup or inline), a click binding captured by doing it, and a dock
-- that lets go
if W.ShowCopyBox then
    local dialog, token = W:ShowCopyBox(inviteUrl)        -- selected, focused, read-only; W:CloseDialog(token)
end
if (AceGUI:GetWidgetVersion("LAGW-CopyField") or 0) >= 2 then
    local field = AceGUI:Create("LAGW-CopyField")         -- the same box inside a window
    field:SetLabel("Discord"); field:SetText(inviteUrl); field:SetFullWidth(true)
    group:AddChild(field)
end
if W.IsClickBinding then
    local cb = AceGUI:Create("LAGW-ClickBinding")         -- click to arm, then click holding the modifiers
    cb:SetLabel("Open 'Where to get it'")
    cb:SetDefault("ALT-LeftButton")                       -- adds a Default button
    cb:SetValue(db.whereClick)                            -- silent; "ALT-", "CTRL-", "SHIFT-" then the button
    cb:SetCallback("OnValueChanged", function(_, _, binding) db.whereClick = binding end)
    if W:IsClickBinding(db.whereClick, button) then open() end   -- exact modifiers; nil button = modifiers only
end
if W.UndockWindow then
    W:UndockWindow(myWindow)                              -- pinned where it is; status.docked kept
end                                                       -- a released AceGUI target lets go by itself

-- MINOR 39: chat diagnostics only in debug; the item row takes your own hover and click
if W.IsDebug then
    W:Configure{ debug = function() return MyDB.profile.debug end }   -- read at each report
    -- the player's /lagw debug on|off wins over this both ways; /lagw debug reset hands it back
    W:CreateFormDialog({ rows = { { label = "Item", kind = "item", link = itemLink,
        onEnter = function(btn, link) MyItemLink.SetItem(btn, link) end,   -- hold-to-compare
        onLeave = function(btn) MyItemLink.EndHover(btn) end,
        onClick = function(btn, link, mouseButton) MyItemLink.Click(link) end } } })
end
  • Embed by dependency, never by copying. The widget types register with AceGUI under shared names, and AceGUI resolves a duplicate registration as “keep whichever version is not lower” — so two copies at the same version are decided by load order, which neither addon controls. This library registers its three original types at version 31 (since MINOR 36) for exactly that reason, and a test pins it. The three are raised together: the last time one moved alone, the other two sat tied with a competing copy for an unknown length of time and nothing said so. The date picker is LAGW-DatePicker, the search box LAGW-SearchBox, and the newer types LAGW-Stepper, LAGW-CopyField and LAGW-ClickBinding — prefixed, because none has a legacy name to preserve or a competing copy to out-version.
  • LAGW-SearchBox keeps AceGUI’s EditBox contract (MINOR 35). SetText fires nothing. The player’s typing and the clear-X fire OnTextChanged(widget, event, text); the clear-X fires from its own OnClick, because the template empties the box programmatically. Also GetText, SetPlaceholder, SetMaxLetters, SetDisabled, SetFocus / ClearFocus / HasFocus, and OnEnter / OnLeave / OnEnterPressed(text). It is released empty, and its 24px height follows the UI scale.
  • LAGW-Strip wants its controls unlabelled (MINOR 35). AceGUI’s labelled EditBox and Dropdown put their boxes at different depths inside different-height frames, which is exactly what Flow cannot line up. Add each control bare and pass its caption to W:StripAdd, which makes a gold TLabel of it, placed over the control at the control’s width. A hidden control takes no space, so call DoLayout after hiding or showing one. The gap (W.STRIP_GAP, 8), the row gap and the caption line follow the UI scale. A row is at least 26px and grows to its tallest control. Captions live in the group’s userdata, which AceGUI empties on release.
  • Releasing a PersistWindow window forgets it (MINOR 35). AceGUI pools the widget and its frame for every addon, so Release now drops the persist record and its scale listener and restores the resize bounds from before the first PersistWindow. W:ForgetWindow(widget) does the same without a release. A ClearFrame’s own resize handle is restored, not destroyed, because the pooled frame would otherwise have no grips.
  • The date picker’s value is a day, and SetValue is silent. It hands you time() of local midnight on the chosen day, or nil for “no limit”, and snaps whatever you pass to SetValue to the same — so a range filter compares like with like. SetValue fires nothing, as AceGUI’s EditBox:SetText fires nothing; every user action fires OnValueChanged, and only when the value changed. A click in the field opens the calendar and never closes it (a second click is the caret being placed); the arrow toggles. Its popup is one shared frame on UIParent at TOOLTIP strata (the menus’ reason: a popup parented into your window inherits its clipping), re-owned per widget and closed on Escape, click-outside, the owner hiding, release and disable. The pure arithmetic is public as W.DatePicker — parse, format, midnight, snap, grid — so you can drop your own date parser.
  • MINOR 30 changed one call’s MEANING, and it is the reason this is 0.2.0. A second W:MakeResizable on a frame that already has a handle now merges: options you pass replace, options you omit keep their value, and onResize = false removes a callback. Before, the omitted ones were reset — so W:MakeResizable(clearFrame, { onResize = fn }), the obvious way to add a live callback, silently nil-ed ClearFrame’s own size persistence and reset its 400x200 minimum, with nothing to see until a reload. Gate with minor >= 30; on an older copy set W:GetResizeHandle(w).onResize = fn instead, which is safe everywhere.
  • The date picker’s four new settings are per widget and reset when it returns to AceGUI’s pool (SetRange, SetTodayFunc, SetRequired, SetFirstWeekday; widget Version 3, gate with AceGUI:GetWidgetVersion("LAGW-DatePicker") >= 3). SetValue is deliberately not range-checked — the range governs what a user can pick, and a stored date must still load — but a required picker handed nil takes today, pulled into the range. The pure arithmetic gained inRange, clampMonth, today(fn) and a week-start argument on grid.
  • Menu rows take isTitle, disabled, tooltipTitle/tooltipText and a per-row keepOpen that overrides the menu’s in either direction. All of it is re-applied on every render because menu rows are pooled — a row that was a heading last time comes back an ordinary choice.
  • W:ShowDialog gained cancelText = false, onShow/onHide, and Enter/Escape without an edit box. onHide runs exactly once however the dialog closes — OK, Cancel, Escape, or a later ShowDialog replacing it — so a PushModal/PopModal pair always balances. The keyboard half is installed only for a dialog shown out of combat: passing other keys through needs SetPropagateKeyboardInput, which insecure code may not call in combat, and a frame that takes the keyboard without it stops the player moving. Escape still closes it in combat, through UISpecialFrames. It also returns a token as its second value, and W:CloseDialog(token) closes only that prompt. One dialog frame is shared by every addon in the session, so dialog:Hide() is not a safe way to dismiss your own — by then the frame may be showing somebody else’s question, and hiding it would run their onHide. A token whose prompt has already closed, or been replaced, matches nothing and is a no-op. parent, by contrast, is re-applied on every call, so the dialog is never stuck inside an earlier caller’s window — and what that costs is that the prompt inherits the parent’s visibility: parented into a window, it closes with that window. For a confirm about that window that is the feature; for anything that should outlive it, pass UIParent.
  • The menu stack is shared the same way, and W:CloseMenuFor(anchor, ...) is the ownership-aware close (MINOR 31). W:CloseMenu() closes whatever is open, whoever opened it; opening yours already closed theirs, so that looks harmless until a menu is opened after yours went away and your own close path (Escape in a search field) fires. CloseMenuFor closes the stack only if one of the anchors you name owns it and returns whether it did. Gate with if W.CloseMenuFor then.
  • RowList can jump to an entry and mark one (MINOR 31). rl:ScrollToEntry(entry | predicate) scrolls so the entry is the top visible row and returns whether it was found, in the order the rows are drawn; “top” holds only where the clamp allows — the list never scrolls past its end, so an entry on the last page comes into view lower down, where the scrollbar would leave it. rl:SetSelected(entry | predicate | nil) puts one persistent accent-tinted highlight on a row by entry identity and returns the entry; it follows through pooling, scroll and a re-sort, hides off-page, and SetData drops it unless the same entry is still present — an equal-looking row in a rebuilt model is a stranger. There is no click-to-select built in: whether a click selects is your rule, so call SetSelected from your onRowClick.
  • RowList sorting is stable, and a column can own a hover. Ties keep SetData order; New takes tiebreak(a, b) (an identity order, not flipped by the sort arrow) and a column takes sortValue(entry) so a display string can sort on a real key. col.onCellEnter/onCellLeave lay a mouse-enabled overlay over just that cell (a text cell is a FontString and cannot take the mouse); the entry is resolved when the pointer arrives, never captured when the pooled row was built, and a click on the overlay is forwarded to onRowClick with the row frame so right-click menus still work over that column.
  • W:CreateCheckbox shows what get() says, not what it was clicked to. { label, tipTitle, tipBody, get, set, size, font } on UICheckButtonTemplate at the 26x26 options size, following the UI scale. It re-reads get() when built, every time it is shown, and on cb:Refresh(); a click hands the new state to set and then re-reads, so a setter that refuses a change is seen refusing rather than leaving a tick the database disagrees with. You position it; cb.label is the FontString.
  • W:DockWindow is standalone, not an option on PersistWindow. { to, point, relPoint, x, y, snapDistance, status }, and it deliberately did not become a PersistWindow flag: PersistWindow owns an AceGUI window’s position through its status table, and a raw CreateFrame window is not that — two owners of one position would fight. One window uses one of them, and since MINOR 32 that is enforced: the second of the two calls on one window returns nil and, in debug (MINOR 39), prints one line naming the call to drop. It hooks OnShow and OnDragStop with HookScript, so set your own scripts before calling it or a later SetScript replaces the hook and docking silently stops; h:Apply(), h:Moved(), h:SetDockEnabled(bool) and h:IsDocked() are the manual route.
  • The resize framework lives in LibAceGUIWidgets-Resize.lua as of MINOR 30 — it was in the core file before, and nothing about it changed but its address. This matters only if you embed individual files rather than the folder: the TOC loads it immediately after the core file, and without it the first ClearFrame you build raises rather than quietly producing a window that cannot be resized. Embedding the folder, as intended, needs no action.
  • The floating-panel plumbing is public. W:CreateFloatingFrame(opts) builds the panel this suite floats above a window with (UIParent, TOOLTIP strata, the shared backdrop, an opaque fill, an optional global name that also lists it for Escape); W:OwnFloating(frame, { isOver, close }) closes it on a click outside, listening only while shown; W:RegisterSpecialFrame(name) adds a name to the Escape list once. The menus, the date popup and the dialog are the three callers.
  • W:PersistWindow keeps no copy of your coordinates. Your table goes straight to SetStatusTable, so AceGUI’s own drag and resize writes land in your save, and clearing top/left and calling ApplyStatus recenters. A saved size under minWidth/minHeight is raised in the table before it is applied — AceGUI applies a status table as-is, and a window narrower than its floor cannot be dragged wider. Give one floor and the frame’s other bound is kept, not reset. A nil table applies the defaults to a fresh table and returns it; a widget without SetStatusTable gets nil back. The floor follows the UI scale live on every window kind as of MINOR 31 — before, only a ClearFrame did (its resize handle carries the listener) and an AceGUI Frame kept its draw-time floor and size until a reload — and the saved size is raised with the frame, so the window a scale change grew is the one restored next open. minWidth/minHeight may be a function(scale) returning a scale-1.0 value, re-evaluated on every change: the form for a floor that is partly fixed art (stock template chrome, your own pixel insets), which a whole multiply puts under its content below 1.0. A pre-divided number is right only at the scale it was computed at, and re-calling PersistWindow from your own scale listener would race the library’s; the function form has no race.
  • The symbol font works on FontStrings you own, and only those. W.SymbolFont / W.SymbolFontSmall are font objects on the bundled DejaVu Sans; W:SymbolFontPath() is the path for your own SetFont; W:Symbol(name) returns the character or nil, so W:Symbol("check") or "[OK]" degrades on an older copy. A GameTooltip line carries the tooltip’s own font and SetFont on it sticks to that line slot for every later tooltip; chat frames have one font for every line. Keep ASCII in both, or draw the text on your own FontStrings. Do not bundle a font per addon — the suite’s fonts live here, once.
  • W:Breathe is idempotent and its defaults are the fleet’s numbers. One animation group per region, reused and re-parameterised on every call — a pooled frame reacquired never grows a second one. Put it on a Frame if your specs need to drive it offline; the harness model’s textures and font strings cannot animate, and the helper returns nil rather than raising for a region that cannot.
  • W:Diagnostics() is the record of what this library reported (MINOR 33). Three faults in your code are reported rather than raised: a RowList with rows and no room to draw them (pass onEmptyRender(rl, count) to handle it yourself), two columns sharing a key, and two addons setting different accents. Each appends an entry here, because no test can assert on a chat line. Build your windows in a spec and assert.equal(0, #W:Diagnostics()); W:ClearDiagnostics() empties it. The chat line is debug-only as of MINOR 39: W:Configure{ debug = true } or debug = function() ... end (read at each report) turns it on, W:IsDebug() answers, and the player’s /lagw debug on|off|reset (W:SetDebug) overrides every addon’s setting and is saved in LibAceGUIWidgetsDB. The record is kept either way.
  • Feature-gate on LibStub.minors["LibAceGUIWidgets-1.0"]. Additions are made so an older embedded copy degrades quietly rather than erroring, so a plain if W.SomeHelper then is enough for most helpers. The number matters when behaviour changed rather than appeared.
  • Theming is yours, structure is the library’s. W:Configure{ accent, classColors, tooltipOwner } — the library never reaches into your addon for state. The accent defaults to the suite orange (ffFF8000, since MINOR 36), so a suite addon need not set it. lib.config is one table for every addon in the session, so a hook set there reaches every other addon’s widgets too. For a RowList, pass refontHook, classDisplay and tooltipOwner to RowList:New instead (MINOR 34); they override the global ones for that list only.
  • Every tooltip here hands the anchor back when the pointer leaves (MINOR 34). W:HideTooltip(frame) hides and then calls GameTooltip_SetDefaultAnchor, so the next addon to Show() without a SetOwner is not drawn at our frame. It does nothing if a different frame has taken the tooltip since. Use it in your own OnLeave for anything you anchored with W:AnchorTooltip or drew with W:ShowTooltip(anchor, title, body).
  • RowList in MINOR 34, the parts that are not obvious from the example. A filterable header opens the column menu instead of sorting; a plain header still sorts on click unless you pass headerMenu. Filter values are what the cell shows, escape-stripped, and the menu draws only as many as fit on screen (W:MenuCapacity()), with Select all / Clear all covering the rest. An expander reports the new value to onToggle and does not write entry[key] — you rebuild the data and call SetData — while a checkbox column with an onToggle still writes it for you (without one, the box is display-only). col.justify applies to button cells only; use col.align for a text cell, because honouring justify there would move every consumer’s columns that already pass it. hyperlinks makes the rows take the mouse, which is why it is opt-in. Call Refresh() after changing the data in place: it rebuilds the sorted and filtered view.
  • onResize is the reason to use MakeResizable rather than hand-rolling StartSizing. The client only reports a resize when the drag ends, so a window whose contents must re-lay-out during the drag has nothing to hook. It fires live, throttled to at most one call per frame draw — un-throttled means a full re-render per mouse pixel, which is why “just hook OnSizeChanged” is not the answer. Calling MakeResizable twice on one frame is idempotent: same handle back, reconfigured, never a second set of grips. If the window is also movable, its mover must write left/top into the same status table — this never sees a move.
  • onRowClick receives the mouse button and the row frame as of MINOR 27. onRowClick(entry, idx, rl, button, rowFrame) — both were already in hand at the call site and were discarded, so a right-click menu meant calling GetMouseButtonClicked() yourself and stashing the row frame from onRowEnter, trusting that OnEnter always precedes OnMouseDown on the same row. Additive: a handler that takes only entry is unchanged. On an older copy button is nil and a right-click branch silently never fires, so gate on the number.
  • W:EnableVersionCheck keeps looking after a miss as of MINOR 27, and before that it did not. The lookup cached a miss as permanent: the first consumer to call it before VersionCheck-1.0 had loaded froze the answer for every consumer for the rest of the session, silently. The resolution is still lazy — call it from your own ADDON_LOADED — and only a hit is cached now. Nothing to change on your side; a bug fix has no symbol to feature-detect, so >= 27 is the signal if you need to depend on it.
  • W:AttachTooltip is idempotent as of MINOR 26, and that changed behaviour. It used to install a fresh HookScript pair per call, so re-attaching to refresh a tooltip’s wording stacked one handler per refresh and the first call’s stale text still drew. The text now lives on the frame and a later call updates it. It also takes only strings and numbers as text: anything else draws no line rather than raising bad argument #1 to 'SetText' inside your OnEnter, where the traceback names the library and not your call site.
  • Three RowList contracts that used to fail confusingly and are now absorbed or enforced: a conditional entry in actions (cond and action or nil) is compacted away rather than dropping an icon or raising mid-draw; at most one column may omit width and a second is now a construction-time error naming it; and the sort is stable as of MINOR 30 — rows whose sort cells tie keep the order you handed to SetData, and New takes a tiebreak(a, b) for data that can reach different clients in different orders. Before MINOR 30 exactly-equal cells could land in any order.
  • An offline test suite ships with the source (not in the packaged addon): lua Tests/wowapi/run.lua, no game client and no build step. 1011 checks, and every executable line of every shipped file is reached (lua Tests/wowapi/coverage.lua fails below 100%). It is built on the shared WoWAPITesting harness, so a consumer can write specs against this library’s real files. As of MINOR 26 it asserts real geometry — grip rects, and that every RowList header column sits over its own cells to the pixel — as of MINOR 27 it asserts the ## Interface list against Ace3’s, so this library can never again claim fewer clients than the toolkit it requires, and as of MINOR 28 it builds the date picker through real AceGUI and clicks its way through the calendar.
  • Consumers request, this library authors. Do not add a widget or helper by editing this library from your own repo — a shared library changed from inside whichever consumer happened to be open is a change to every consumer, decided by accident. Ask (the Discord below is the channel), stage whatever unblocks you inside your own addon, and it gets implemented, spec’d and versioned here — the four additions in v0.1.11 all arrived that way, from one consumer, in one afternoon.

Full developer notes for every release are in CHANGELOG.md, and the README.md carries the complete API tour.

Credits

Pimptasty — author. Widget code extracted and decoupled from FastGuildInvite.

Community

Bug reports, feature requests, or questions: Join the Discord.

You must be logged in to leave a comment.

Comments (0)