Build on Daylight UI.

Your addon can find the Daylight UI frames, draw on them, add its settings to the Daylight UI options and ship ready-made profiles. This page walks through all of it.

Start here

A plugin is a normal addon that loads after Daylight UI. You don't need to ask anyone or get on a list. The license lets you make, share and upload addons that build on Daylight UI and change how it looks and works. Your addon stays your own work. The one thing you can't do is copy Daylight UI code or art into it.

Add Daylight UI as a dependency so the game loads it first:

## MyHeals.toc
## Interface: 16001
## Title: My Heals
## Author: You
## Version: 1.0
## Dependencies: DaylightUI

MyHeals.lua

Then say hello once, at the top of your first file:

DaylightUI:RegisterPlugin("MyHeals", {
    needs = { 1, 2 },   -- the API version you wrote it for
    icon = "Interface\\AddOns\\MyHeals\\icon.png",   -- white on clear, Daylight UI tints it
    color = "66cc99",
    short = "Heals",    -- its name in the options rail
})

local API = DaylightUI.API
local my = API.For("MyHeals")

Your title, author, version and website come from your .toc unless you pass them. Your plugin gets its own section in the Daylight UI options window, under a Plugins heading, and it's credited by name on the General > Plugins page.

API.For("MyHeals") gives you a handle tied to your name. Anything you make through it (movers, commands, settings) is kept separate from everything Daylight UI makes and from other plugins. If your code errors, the player's error report names your addon, not Daylight UI.

Find a frame

Any frame Daylight UI draws for a unit can be looked up. These return the frame, or nil when there isn't one (or that part of the UI is switched off).

-- a raid tile first, then a party tile, then the unit frame
local tile = API.GetUnitFrame("party2")
local plate = API.GetNameplate("nameplate4")
local button = API.GetActionButton(1, 5)   -- bar 1, button 5

for frame, unit in API.Frames("raid") do
    -- every raid tile that's in use
end

GetUnitFrame works like LibGetFrame, so it's handy for anchoring. If you ask for "player" while in a raid, you get your own raid tile. Frames you can see win over hidden ones.

There are eight kinds of frame:

KindWhat it is
"unit"Player, target, target of target, pet and focus
"party"Party tiles
"raid"Raid tiles
"partypet"The small pet tiles next to party members
"nameplate"Nameplates
"actionbutton"Buttons on the Daylight UI action bars
"shoptile"Item tiles in the Daylight UI vendor window. Their unit is "buy:3" or "buyback:3".
"craftrow"Recipe lines in the Daylight UI profession window. Their unit is the recipe's id.
local tile = API.GetShopTile("buy", 3)   -- the vendor's third item, if it's on show
local row = API.GetCraftRow(3914)         -- a recipe's line, if it's on show

Lookups only ever return real frames. Sample frames from test mode and the options window are never handed back, so you won't end up drawing on a preview by accident.

Hear when things change

Instead of polling or hooking, listen. Every frame event gives you (kind, frame, unit, isPreview).

EventWhen
FrameMadeOnce per frame, after Daylight UI has built it. Make your pieces here.
FrameUnitChangedThe frame now shows someone else. A tile got a new player, a nameplate got a new mob, or you picked a new target.
FrameRemovedThe frame let its unit go. Hide or clear what you put on it.
FrameStyledDaylight UI laid it out again after a setting changed. Move your pieces if they depend on its size.
local handle = my.On("FrameMade", function(kind, frame, unit, isPreview)
    if kind == "raid" then
        -- build something on this tile
    end
end)

API.Off(handle)   -- if you ever want to stop listening

A few things that are good to know:

  • If you start listening after some frames already exist, FrameMade is sent to you right away for each of them. The action bars are built before any plugin loads, so this is how you hear about their buttons.
  • Nameplates get reused. When one moves to a new mob you get FrameRemoved for the old unit before FrameUnitChanged for the new one. You never get FrameMade twice for the same frame.
  • A frame can be made before it has a unit. Handle the unit in both FrameMade and FrameUnitChanged and you're covered either way.
  • The target of target frame can send FrameUnitChanged more than once for one change. Make your handler fine with that.
  • Test mode and the options window's samples send these events too, with isPreview set to true. Draw your sample data on them so players can see your plugin while they set things up.
  • If your handler errors, Daylight UI and other plugins keep running, and the error carries your name.

You can also listen to StyleChanged (the player changed fonts, colors or the accent), SettingChanged (gives you scope, path and value), TestModeChanged, UnlockChanged and Login.

Draw on a frame

API.Attach(frame) gives you a plain frame that sits on top of the Daylight UI frame, the same size, above its art. Put your textures and text on it. You get the same one every time you call it, so it's safe to call from any event.

It fades and scales with the frame underneath, and it isn't protected, so you can draw on a raid tile in the middle of a fight.

Here's a whole plugin. It puts a small dot on the party and raid tiles of anyone who's dead, so a healer knows who needs a res.

DaylightUI:RegisterPlugin("ResReminder", { needs = { 1, 2 } })

local API = DaylightUI.API
local my = API.For("ResReminder")
local dots = {}

local function paint(frame, unit)
    local dot = dots[frame]
    if dot then
        dot:SetShown(unit ~= nil and UnitIsDeadOrGhost(unit))
    end
end

my.On("FrameMade", function(kind, frame, unit)
    if kind ~= "party" and kind ~= "raid" then return end
    local dot = API.Attach(frame):CreateTexture(nil, "OVERLAY")
    dot:SetSize(8, 8)
    dot:SetPoint("TOPRIGHT", -4, -4)
    dot:SetColorTexture(API.Accent())
    dots[frame] = dot
    paint(frame, unit)
end)

my.On("FrameUnitChanged", function(kind, frame, unit) paint(frame, unit) end)
my.On("FrameRemoved", function(kind, frame) paint(frame, nil) end)

-- the player changed their accent: recolor
my.On("StyleChanged", function()
    for frame, dot in pairs(dots) do dot:SetColorTexture(API.Accent()) end
end)

-- someone died or came back
local events = CreateFrame("Frame")
events:RegisterEvent("UNIT_HEALTH")
events:RegisterEvent("UNIT_FLAGS")
events:SetScript("OnEvent", function(_, _, unit)
    local frame = API.GetUnitFrame(unit)
    if frame then paint(frame, unit) end
end)

That's the whole thing. Nothing in it hooks into Daylight UI, so it keeps working when the tiles change in an update.

Only the frame itself is promised, plus the named parts below. What else is inside it (the health bar, the aura row) can change in any update, so don't anchor to those. If you need one, ask on the Daylight UI Discord.

Named parts

Vendor tiles and recipe lines hand you their own pieces by name, so you can tint them: API.GetPart(frame, "icon") is the icon texture and API.GetPart(frame, "name") the name text. Daylight UI paints them again every time the list fills, and sends FrameStyled right after, so that's where your paint goes.

The vendor and profession windows

Since 1.2 the lists in two Daylight UI windows are frames you can build on, the same way as raid tiles: the item tiles at a vendor and the recipe lines in the profession window. They're reused as the list changes (a search, another vendor, a skill-up), so treat them like nameplates: FrameUnitChanged when one shows something else, FrameRemoved when it's hidden or the window closes, and FrameStyled after every fill.

Marking items at a vendor

The vendor's list fills again whenever your money or bags change, so FrameStyled is also your refresh after a purchase. API.ShopItem(tile) tells you what a tile holds: tab, index, itemID, link, where tab is "buy" or "buyback" and index is the game's own, for GetMerchantItemInfo.

-- gray out the grimoires the player already knows
my.On("FrameStyled", function(kind, tile)
    if kind ~= "shoptile" then return end
    local tab, index, itemID = API.ShopItem(tile)
    if tab == "buy" and itemID and MyAddon:Known(itemID) then
        API.GetPart(tile, "icon"):SetVertexColor(0.4, 0.4, 0.4)
        API.GetPart(tile, "name"):SetTextColor(unpack(API.Colors().OFF))
    end
end)

A tile that keeps its place but holds another vendor's item gets FrameUnitChanged with the same unit, so read API.ShopItem again there. For a badge of your own, draw on API.Attach(tile) as with any other frame.

The profession window

EventGives youWhen
CraftShownThe window opened.
CraftHiddenIt closed.
CraftProfessionChangedskillLineIt shows a profession, on opening too.
CraftRecipeSelectedrecipeIDA recipe is picked: a click, your own call, or the first one when a profession opens.
CraftStartedrecipeID, countCreate or Create all started.
CraftMaderecipeID, done, countOne more made.
CraftStoppedrecipeID, done, countMaking ended: all made, stopped, or a cast failed.
CallWhat it does
API.SelectCraftRecipe(recipeID)Picks a recipe as a click would: it lights up, scrolls into view and shows on the right. true when it did.
API.GetCraftProfession()The open profession: skillLine, name, or nil while the window is shut.
my.AttachCraftPane(width)A panel of yours on the window's right edge.
-- a side panel that follows the picked recipe
local pane = my.AttachCraftPane(220)
local title = API.Text(pane, "head")
title:SetPoint("TOPLEFT", 14, -14)

my.On("CraftRecipeSelected", function(recipeID)
    title:SetText(C_TradeSkillUI.GetRecipeInfo(recipeID).name)
end)

The pane is the window's full height, in its shell. It moves, scales, opens and closes with the window, and the game's other windows make room for it. Two plugins' panes sit side by side. Ask again with another width to resize yours.

Players can switch these windows off, or use DaylightMogul's in their place. Then there are no tiles, lines, pane or Craft events. If your addon should work either way, listen to the game's own MERCHANT_SHOW and TRADE_SKILL_SHOW too.

Look like you belong

If you want your pieces to match Daylight UI, use the same tools it uses. They all read the player's font, accent and sizes at the moment you call them, so call them when you paint and again on StyleChanged.

CallWhat you get
API.Skin(frame, opts)The Daylight UI border, band, bevel and shadow, on any frame. Pass { window = true } for a window.
API.Text(parent, size, justify)Text in the player's font. Sizes are "tiny", "small", "body", "head" and "title".
API.Bar(parent)A status bar with the Daylight UI gradient fill
API.Button(parent, text, height)The Daylight UI button, with its hover trace and click sound
API.Close(parent, onClick)The close X from every Daylight UI window
API.Switch(parent)An on/off switch
API.Input(parent, opts)A text box. { search = true } makes a search box, onChange tells you what was typed.
API.Menu(owner, keys, values, current, onPick)A dropdown menu
API.Accent()The player's accent color as r, g, b
API.Colors()The text colors:VALUE for numbers, TEXT for sentences, LABEL for labels, plus GOOD, WARN, BAD and a few more
API.Font()The font file the player picked
API.FontFile(key)The file of a font your font row saved. "general" is the player's font.
API.ClassColor(token)A class color, softened to match the unit frames
API.Fade(region, alpha, speed)Eases something of yours to an alpha. Speed is "FAST", "NORMAL" or "SLOW".

None of this is required. It's there if you want a plugin that feels like part of the UI.

Settings

Plugin settings live in a module, the same way Daylight UI keeps its own. That way they're saved in the player's profile, copied when they copy a profile and shared when they share one.

DaylightUI:RegisterModule("ResReminder", {
    plugin = "ResReminder",
    label = "Res Reminder",
    defaults = { dots = true },
    OnEnable = function(mod) --[[ start up ]] end,
})

local on = API.GetSetting("ResReminder", "dots")

Pages you add to the module's options show up in your own section of the options window:

DaylightUI:RegisterModule("ResReminder", {
    plugin = "ResReminder",
    label = "Res Reminder",
    defaults = { dots = true, size = 8, font = "general" },
    options = { pages = {
        { id = "main", label = "Res Reminder", desc = "One line under the page's title.",
          groups = {
              { label = "Dots", items = {
                  { type = "toggle", key = "dots", label = "Mark dead players",
                    desc = "A small dot on the tile of anyone who needs a res." },
                  { type = "slider", key = "size", label = "Dot size", min = 4, max = 16, step = 1, unit = "px",
                    disabled = function(get) return not get("dots") end },
                  API.FontRow("font", "Font", "The font of the res count."),
              } },
          } },
    } },
})

Every kind of row

TypeFields
toggleAn on/off switch.
slidermin, max, step, unit ("px", "%" or "x")
dropdownvalues = { key = "Shown word" }
colorA color picker, its value { r, g, b }
buttonfunc, run on a click. No key.
textA line of words on the page. No key.
API.FontRow(key, label, desc)The font picker Daylight UI's own pages use, each font shown in itself. Its value is a font's key; "general" follows the player's font. Turn it into a file with API.FontFile.

Every row takes label and desc (one or two sentences on what it does in play), disabled and hidden (functions of get, which reads your settings), and reload = true when the change needs a /reload. Rows with a value take a key: on your own pages its default sits in the module's defaults; on a group you put on a Daylight UI page, give the row a default.

Putting a group on a Daylight UI page

Sometimes a setting belongs next to the ones it works with. A healer plugin's switch makes more sense on the Raid page than off in its own corner.

my.AddOptionsGroup("UnitFrames.raid", {
    label = "Res reminder",
    items = {
        { type = "toggle", key = "dots", label = "Mark dead players",
          desc = "A small dot on the tile of anyone who needs a res." },
    },
})
  • The group shows up with "From ResReminder" next to its title, so players know where it came from.
  • Rows save into your module's settings and leave the Daylight UI settings alone. Your module has to be registered (the scope is your plugin's name, or scope = "..." on the group).
  • Page names are the module and the page, like "UnitFrames.raid", "UnitFrames.castbars", "Nameplates.plates", "Nameplates.auras" or "ActionBars.bars".
  • If the page isn't there yet (its part of the UI is off, say), your group waits and appears when the page does.
  • A row Daylight UI can't read (a typo in its type, a missing label) is left out with one line in chat. The rest of the page keeps working.
  • The General > Plugins, Profiles and Gradients pages rebuild themselves, so they don't take groups from plugins.

Movers, fades, commands and skins

These go through your API.For handle, so your names can't clash with anyone else's.

-- an unlock handle for your frame, with the same grid and magnets as the rest of the UI
my.RegisterMover(myFrame, "panel", "Res Reminder", { "CENTER", nil, "CENTER", 0, -200 })

-- the four fade choices every part of Daylight UI has; its settings live at path in your module
my.RegisterFade({ key = "panel", scope = "ResReminder", path = "fade",
                  frames = function() return { myFrame } end })

-- /dui res
my.RegisterCommand("res", function(rest) print("Res Reminder", rest) end, "Res Reminder settings")

-- a box for your piece in the test mode panel (listen to TestModeChanged to show samples)
my.RegisterTestPart("dots", "Res dots")

-- dress another addon's window in the Daylight UI look once that addon loads
my.RegisterSkin("SomeAddon", function(Kit)
    if not SomeAddonFrame then return false end   -- not made yet: asked again later
    Kit.Window(SomeAddonFrame)
    Kit.Auto(SomeAddonFrame)
end)

A /dui word that's already taken is refused with a line in chat. RegisterSkin returns false when the Daylight UI Skins module isn't installed or is turned off for that character. If it just hasn't loaded yet, your skin waits for it.

What the Kit holds

Kit is the set of tools the built-in skins use on the game's own templates. Most windows need only the first two.

CallFor
Kit.Window(frame, opts)A whole window: its art gone, the window shell, the title font, the close X
Kit.Auto(root, opts)Walks a window and dresses every control in it: buttons, boxes, tabs, checkboxes, dropdowns, scroll bars, item buttons, bars. opts.skip is a set of frames to leave alone.
Kit.Panel(frame), Kit.Inset(frame), Kit.SidePane(pane)A side panel, an inset a shade darker, a detail pane beside a list
Kit.Button, Kit.Close, Kit.Tab, Kit.SideTab, Kit.Check, Kit.EditBox, Kit.DropdownOne control of the game's templates
Kit.IconButton, Kit.PageButton, Kit.CollapseButtonA small icon button, a Prev or Next button, a list heading's +/-
Kit.Item(btn), Kit.Slot(slot)An item button, an equipment slot: the icon cropped, the quality outline
Kit.StatusBar(bar), Kit.ProgressBar(bar)A status bar, a bar drawn with textures
Kit.ScrollBar(bar)A scroll bar, old style or new: the thin pill, the arrows hidden but still there
Kit.EachRow(box, fn, key)A ScrollBox list: fn(row) for every row now and after each update, since rows are reused
Kit.Row(row, keep, selected, shell)One list row. selected is a function of the row (or the game's selection texture); shell = false draws a plain line instead of a box. Kit.RepaintRow(row) after the pick changes.
Kit.Strip(frame), Kit.Font(fs, size), Kit.Light(fs)Hide a frame's art, put the player's font on text, make dark parchment text light
-- a ScrollBox list in the Daylight UI look
Kit.EachRow(SomeAddonFrame.ScrollBox, function(row)
    Kit.Row(row, nil, function(r) return r.isSelected end, false)
end)
Kit.ScrollBar(SomeAddonFrame.ScrollBar)

Your data in a data text slot

The data texts (the strip under the minimap and the data bar along the bottom) take any LibDataBroker-1.1 object, the way most data bars do. Make one and it shows up in every slot menu under Info Panel > Data texts, next to the Daylight UI texts. There's nothing else to call, and the same object works in any other broker display.

local ldb = LibStub("LibDataBroker-1.1")
local obj = ldb:NewDataObject("ResReminder", {
    type = "data source",   -- or "launcher"
    label = "Res",          -- the word before the text
    text = "0",             -- or value = 0, suffix = "dead"
    icon = "Interface\\Icons\\Spell_Holy_Resurrection",
    OnClick = function(slot, button) end,
    OnTooltipShow = function(tooltip)
        tooltip:AddLine("Dead group members")
    end,
})
obj.text = "2"   -- every slot showing it redraws
  • Ship the library yourself. Daylight UI only reads LibDataBroker-1.1; bring it (with LibStub and CallbackHandler-1.0) in your addon.
  • Colors. Your text can carry color codes. Without any, it takes the player's accent, like the built-in texts. Your icon keeps its own colors; iconCoords and iconR/iconG/iconB are followed.
  • Your own tooltip. Give OnEnter(slot) and OnLeave(slot) instead of OnTooltipShow and the hover is yours.
  • Late objects. An object made after login (an addon loaded on demand) joins the menus then. A slot the player set to it stays empty until it's made, then fills.
  • No protected actions. OnClick runs from a plain button, so it can't cast spells or use items.
  • Lots of changes are fine. Changes are gathered for a tenth of a second before a redraw.

Profile packs

A pack is a ready-made setup, like ElvUI's UI packs. Set up the UI the way you like it, export your profile with /dui export and register the string:

DaylightUI.RegisterPack("MyPack", {
    label = "My UI",
    author = "You",
    version = "1.2",
    desc = "One line on what it is.",
    screen = { 1920, 1080 },   -- the screen size you made it on
    updates = "offer",         -- or "never" (the default)
    profiles = {
        { name = "My UI Healer", text = "!DUI1_...", sub = "One line.", role = "Healer" },
    },
})

Your pack shows up in the setup guide and on the Profiles page, credited to you. A profile is only made when the player picks it. Positions are pulled in to fit smaller screens. With updates = "offer", players get an Update button when your version goes up, and nothing changes unless they press it.

A pack can also set up other addons (Details, say) with extras. The player sees each one as a switch on the setup guide's last page.

The game's rules still apply

WoW: Forever runs on the modern client with its addon restrictions on. A few rules bite plugins the most:

  • Secret values. In combat, in boss fights, PvP matches and some other places, many values (health, auras and more) come back as secrets. You can hand them to a font string, a status bar or a texture, but you can't compare them, do math on them or check if they're true. issecretvalue(x) tells you. Some, like UnitHealth, are secret all the time.
  • Protected frames. The Daylight UI unit frames and tiles are secure. You can't move, show, hide or resize them in combat. Draw on the Attach frame instead and leave the tile itself alone.
  • Don't hook the game's methods. On this client a hooksecurefunc on a game frame's method can break the game's own calls. Listen to events instead.
  • Only DaylightUI.API is promised. Everything else inside Daylight UI can change in any update. If the API is missing something you need, ask.

Versions

DaylightUI.API.major and .minor say which version is installed. This page describes 1.2, which added the vendor and profession windows, named parts and the font row. A plugin that uses none of them can keep needs = { 1, 1 }.

  • The minor number goes up when something is added. Your plugin keeps working.
  • The major number only goes up when something promised changes or goes away. If a call gets renamed, the old name keeps working until then.
  • If your plugin needs a newer version than the player has, it stays off and tells them with one line in chat. It never throws an error at them.

Every Daylight UI update is tested against a fake plugin that uses every call on this page, so a change that breaks one gets caught before it ships.

Questions?

Come ask in the Discord. Say what you're building, especially if the API gets in your way.