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:
| Kind | What 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).
| Event | When |
|---|---|
FrameMade | Once per frame, after Daylight UI has built it. Make your pieces here. |
FrameUnitChanged | The frame now shows someone else. A tile got a new player, a nameplate got a new mob, or you picked a new target. |
FrameRemoved | The frame let its unit go. Hide or clear what you put on it. |
FrameStyled | Daylight 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,
FrameMadeis 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
FrameRemovedfor the old unit beforeFrameUnitChangedfor the new one. You never getFrameMadetwice for the same frame. - A frame can be made before it has a unit. Handle the unit in both
FrameMadeandFrameUnitChangedand you're covered either way. - The target of target frame can send
FrameUnitChangedmore than once for one change. Make your handler fine with that. - Test mode and the options window's samples send these events too, with
isPreviewset totrue. 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.
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
| Event | Gives you | When |
|---|---|---|
CraftShown | The window opened. | |
CraftHidden | It closed. | |
CraftProfessionChanged | skillLine | It shows a profession, on opening too. |
CraftRecipeSelected | recipeID | A recipe is picked: a click, your own call, or the first one when a profession opens. |
CraftStarted | recipeID, count | Create or Create all started. |
CraftMade | recipeID, done, count | One more made. |
CraftStopped | recipeID, done, count | Making ended: all made, stopped, or a cast failed. |
| Call | What 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.
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.
| Call | What 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
| Type | Fields |
|---|---|
toggle | An on/off switch. |
slider | min, max, step, unit ("px", "%" or "x") |
dropdown | values = { key = "Shown word" } |
color | A color picker, its value { r, g, b } |
button | func, run on a click. No key. |
text | A 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.
| Call | For |
|---|---|
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.Dropdown | One control of the game's templates |
Kit.IconButton, Kit.PageButton, Kit.CollapseButton | A 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;
iconCoordsandiconR/iconG/iconBare followed. - Your own tooltip. Give
OnEnter(slot)andOnLeave(slot)instead ofOnTooltipShowand 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.
OnClickruns 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, likeUnitHealth, 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
Attachframe instead and leave the tile itself alone. - Don't hook the game's methods. On this client a
hooksecurefuncon a game frame's method can break the game's own calls. Listen to events instead. - Only
DaylightUI.APIis 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.