Repository navigation
Expand file tree
/
Copy pathdwtheme.lua
More file actions
354 lines (304 loc) · 12.8 KB
/
Copy pathdwtheme.lua
File metadata and controls
354 lines (304 loc) · 12.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
--[[
* DriftwoodXI -- dwtheme
*
* The shared paint for every Driftwood addon window. Stock Ashita ImGui greys
* belong to everyone; these windows are the server's own UI, so they wear the
* launcher's colors instead -- the same deep-ocean-and-sea-glass palette the
* DriftwoodXI launcher paints itself with.
*
* WHERE THE COLORS COME FROM. The launcher writes its active Appearance theme
* to `<Ashita>/config/driftwood-theme.ini` every time the theme is applied --
* pick Nightshade in the launcher and the in-game windows go violet with it.
* The file is re-read every couple of seconds, so changing the launcher theme
* recolors an open game live, no reload required. No file (no launcher, or a
* theme never synced) means the built-in Driftwood house palette below: the
* windows are branded either way, the launcher is a paintbrush, not a
* dependency.
*
* HOW IT IS APPLIED. `push()` pushes the whole ImGui style-color set plus the
* rounding vars and returns what it pushed; `pop()` takes that token back.
* The pair wraps each addon's Begin/End -- OUTSIDE the render pcall, like
* Begin/End themselves, so the style stack stays balanced whatever the render
* pass does. Nothing here touches the global style: other addons keep their
* own look, which is rather the point.
*
* SEMANTIC COLORS. `colors.ok`, `.err`, `.warn`, `.hint`, `.badge`, `.inert`,
* `.pin` are stable table references the addons hand to TextColored; a reload
* rewrites their contents in place, so holding one in a local at file scope is
* safe and stays live.
*
* THIS FILE IS DUPLICATED into each dw* addon folder on purpose: an addon
* folder is the unit of distribution ("copy the folder into addons/"), and a
* shared copy in addons/libs would not travel with it. The copy in dwbags is
* the master; edit there and re-copy.
--]]
local imgui = require('imgui');
local M = {};
-- The Driftwood house palette -- the launcher's base theme tokens
-- (src/renderer/src/theme/tokens.css) in #RRGGBB / #RRGGBBAA form. Every key
-- the ini may carry has a default here, so a partial or absent file can never
-- leave a slot unpainted.
local DEFAULTS = {
bgDeep = '#050b13',
bg = '#081220',
surface = '#0d1a2bb8',
text = '#dce8f2',
textDim = '#8fa5b8',
textFaint = '#5b7186',
accent = '#45c6b8',
accentStrong = '#6fe3d5',
accentDeep = '#2b9d92',
accentInk = '#04211d',
wood = '#c9a87c',
woodDim = '#8a7357',
line = '#97b2c924',
lineStrong = '#97b2c947',
ok = '#4ade80',
warn = '#fbbf24',
err = '#f87171',
info = '#7dd3fc',
};
-- '#RRGGBB' or '#RRGGBBAA' -> { r, g, b, a } in 0..1, or nil if malformed.
local function parseHex(value)
if (type(value) ~= 'string') then
return nil;
end
local hex = value:match('^#(%x%x%x%x%x%x%x?%x?)$');
if (hex == nil or (#hex ~= 6 and #hex ~= 8)) then
return nil;
end
local r = tonumber(hex:sub(1, 2), 16) / 255;
local g = tonumber(hex:sub(3, 4), 16) / 255;
local b = tonumber(hex:sub(5, 6), 16) / 255;
local a = 1.0;
if (#hex == 8) then
a = tonumber(hex:sub(7, 8), 16) / 255;
end
return { r, g, b, a };
end
-- Linear blend of two colors' rgb, opaque result.
local function mix(a, b, t)
return {
a[1] + (b[1] - a[1]) * t,
a[2] + (b[2] - a[2]) * t,
a[3] + (b[3] - a[3]) * t,
1.0,
};
end
local function withAlpha(c, alpha)
return { c[1], c[2], c[3], alpha };
end
local TRANSPARENT = { 0, 0, 0, 0 };
-- Stable tables the addons hold references to; rebuild() rewrites the
-- contents, never the identity.
M.colors = {
ok = {}, -- confirmations ("Sent.")
err = {}, -- refusals and errors
warn = {}, -- unsaved / caution
info = {}, -- informational badges
hint = {}, -- the upgrade-marker seafoam
badge = {}, -- shipped-preset / read-only markers
inert = {}, -- rows that are present but not active
pin = {}, -- the [pin] marker
text = {},
dim = {},
};
-- The launcher theme's display name, for anything that wants to say it.
M.name = 'Driftwood';
local styleColors = {}; -- { { ImGuiCol_*, {r,g,b,a} }, ... }
local styleVars = {}; -- { { ImGuiStyleVar_*, value }, ... }
local function setInPlace(dst, src)
dst[1], dst[2], dst[3], dst[4] = src[1], src[2], src[3], src[4];
end
--[[
* Turn the ~18 semantic palette entries into the full ImGui slot list. Shades
* the palette does not spell out (hovered/active steps, translucent fills) are
* derived here with mix/alpha, so the ini stays small and semantic and every
* launcher theme -- light Shoreline included -- gets coherent widget states
* for free. Translucent accent fills rather than opaque ones are what keep
* button labels readable in both directions: over a dark ground the label is
* pale text on a tinted glass, over Shoreline's paper it is dark text on a
* pale wash.
--]]
local function rebuild(pal)
local bg = pal.bg;
local bgDeep = pal.bgDeep;
local text = pal.text;
local accent = pal.accent;
local strong = pal.accentStrong;
local deep = pal.accentDeep;
local wood = pal.wood;
styleColors = {
{ ImGuiCol_Text, text },
{ ImGuiCol_TextDisabled, pal.textFaint },
{ ImGuiCol_WindowBg, withAlpha(bg, 0.94) },
{ ImGuiCol_ChildBg, TRANSPARENT },
{ ImGuiCol_PopupBg, withAlpha(bgDeep, 0.96) },
{ ImGuiCol_Border, pal.lineStrong },
{ ImGuiCol_BorderShadow, TRANSPARENT },
{ ImGuiCol_FrameBg, withAlpha(mix(bg, accent, 0.10), 0.62) },
{ ImGuiCol_FrameBgHovered, withAlpha(mix(bg, accent, 0.20), 0.70) },
{ ImGuiCol_FrameBgActive, withAlpha(mix(bg, accent, 0.28), 0.78) },
{ ImGuiCol_TitleBg, withAlpha(bgDeep, 0.95) },
{ ImGuiCol_TitleBgActive, mix(bgDeep, deep, 0.55) },
{ ImGuiCol_TitleBgCollapsed, withAlpha(bgDeep, 0.80) },
{ ImGuiCol_MenuBarBg, withAlpha(bgDeep, 0.90) },
{ ImGuiCol_ScrollbarBg, withAlpha(bgDeep, 0.55) },
{ ImGuiCol_ScrollbarGrab, mix(bg, text, 0.22) },
{ ImGuiCol_ScrollbarGrabHovered, mix(bg, text, 0.32) },
{ ImGuiCol_ScrollbarGrabActive, accent },
{ ImGuiCol_CheckMark, strong },
{ ImGuiCol_SliderGrab, accent },
{ ImGuiCol_SliderGrabActive, strong },
{ ImGuiCol_Button, withAlpha(accent, 0.32) },
{ ImGuiCol_ButtonHovered, withAlpha(accent, 0.55) },
{ ImGuiCol_ButtonActive, withAlpha(strong, 0.70) },
{ ImGuiCol_Header, withAlpha(accent, 0.26) },
{ ImGuiCol_HeaderHovered, withAlpha(accent, 0.38) },
{ ImGuiCol_HeaderActive, withAlpha(accent, 0.50) },
{ ImGuiCol_Separator, pal.lineStrong },
{ ImGuiCol_SeparatorHovered, withAlpha(accent, 0.60) },
{ ImGuiCol_SeparatorActive, accent },
{ ImGuiCol_ResizeGrip, withAlpha(accent, 0.22) },
{ ImGuiCol_ResizeGripHovered, withAlpha(accent, 0.48) },
{ ImGuiCol_ResizeGripActive, withAlpha(strong, 0.70) },
{ ImGuiCol_InputTextCursor, strong },
{ ImGuiCol_TabHovered, withAlpha(accent, 0.48) },
{ ImGuiCol_Tab, withAlpha(mix(bg, accent, 0.14), 0.86) },
{ ImGuiCol_TabSelected, mix(bg, accent, 0.38) },
{ ImGuiCol_TabSelectedOverline, strong },
{ ImGuiCol_TabDimmed, withAlpha(mix(bg, accent, 0.06), 0.80) },
{ ImGuiCol_TabDimmedSelected, mix(bg, accent, 0.22) },
{ ImGuiCol_TabDimmedSelectedOverline, withAlpha(strong, 0.50) },
{ ImGuiCol_TableHeaderBg, mix(bgDeep, accent, 0.16) },
{ ImGuiCol_TableBorderStrong, pal.lineStrong },
{ ImGuiCol_TableBorderLight, pal.line },
{ ImGuiCol_TableRowBg, TRANSPARENT },
{ ImGuiCol_TableRowBgAlt, withAlpha(text, 0.04) },
{ ImGuiCol_TextLink, strong },
{ ImGuiCol_TextSelectedBg, withAlpha(accent, 0.35) },
{ ImGuiCol_NavCursor, strong },
{ ImGuiCol_NavWindowingHighlight, strong },
{ ImGuiCol_PlotLines, accent },
{ ImGuiCol_PlotLinesHovered, strong },
{ ImGuiCol_PlotHistogram, wood },
{ ImGuiCol_PlotHistogramHovered, strong },
{ ImGuiCol_DragDropTarget, wood },
};
-- The launcher's rounded-corner vocabulary, scaled down for game density.
styleVars = {
{ ImGuiStyleVar_WindowRounding, 8.0 },
{ ImGuiStyleVar_ChildRounding, 6.0 },
{ ImGuiStyleVar_PopupRounding, 6.0 },
{ ImGuiStyleVar_FrameRounding, 4.0 },
{ ImGuiStyleVar_ScrollbarRounding, 8.0 },
{ ImGuiStyleVar_GrabRounding, 4.0 },
{ ImGuiStyleVar_TabRounding, 5.0 },
{ ImGuiStyleVar_WindowBorderSize, 1.0 },
};
setInPlace(M.colors.ok, pal.ok);
setInPlace(M.colors.err, pal.err);
setInPlace(M.colors.warn, pal.warn);
setInPlace(M.colors.info, pal.info);
setInPlace(M.colors.hint, strong);
setInPlace(M.colors.badge, pal.info);
setInPlace(M.colors.inert, pal.textFaint);
setInPlace(M.colors.pin, wood);
setInPlace(M.colors.text, text);
setInPlace(M.colors.dim, pal.textDim);
end
-- Parse one ini body into a full palette (defaults fill the gaps), rebuild
-- the slot list from it. Returns true when the content was usable at all.
local function applyContent(raw)
local pal = {};
for key in pairs(DEFAULTS) do
pal[key] = parseHex(DEFAULTS[key]);
end
if (raw ~= nil) then
for key, value in raw:gmatch('([%w]+)%s*=%s*(#%x+)') do
if (DEFAULTS[key] ~= nil) then
local parsed = parseHex(value);
if (parsed ~= nil) then
pal[key] = parsed;
end
end
end
local name = raw:match('theme%s*=%s*([^\r\n]+)');
if (name ~= nil and #name > 0 and #name <= 64) then
M.name = name:gsub('%s+$', '');
end
else
M.name = 'Driftwood';
end
rebuild(pal);
end
-- The launcher-owned theme file inside the Ashita install. Resolved lazily:
-- AshitaCore exists whenever an addon is running, but there is no need to
-- touch it at require time.
local themePath = nil;
local function themeFile()
if (themePath == nil) then
local install = AshitaCore:GetInstallPath();
themePath = string.format('%s\\config\\driftwood-theme.ini', install:gsub('[\\/]+$', ''));
end
return themePath;
end
local lastRaw = nil; -- file content last applied, nil = defaults
local lastCheck = 0; -- os.time() of the last disk look
local function readRaw()
local file = io.open(themeFile(), 'rb');
if (file == nil) then
return nil;
end
local raw = file:read('*a');
file:close();
return raw;
end
--[[
* Re-read the ini at most every 2 seconds, and only rebuild when the bytes
* actually changed. A missing file is a real state, not an error: it selects
* the defaults, and keeps being checked -- so a launcher installed (or a theme
* first synced) mid-session takes effect without a reload. Everything is
* pcall'd because a theme must never be able to take an addon down; the worst
* a broken file can do is leave the previous coat of paint on.
--]]
local function maybeReload()
local now = os.time();
if (now - lastCheck < 2) then
return;
end
lastCheck = now;
local ok, raw = pcall(readRaw);
if (not ok or raw == lastRaw) then
return;
end
lastRaw = raw;
if (not pcall(applyContent, raw)) then
pcall(applyContent, nil);
end
end
-- First build happens at require time so push() always has a full list, file
-- or no file.
pcall(applyContent, nil);
maybeReload();
--[[
* Push the theme; returns the counts pop() needs. Call outside the render
* pcall, paired with pop() the way Begin is paired with End -- the token makes
* the balance explicit even if a future reload ever changed the list length
* between frames.
--]]
function M.push()
maybeReload();
for i = 1, #styleColors do
imgui.PushStyleColor(styleColors[i][1], styleColors[i][2]);
end
for i = 1, #styleVars do
imgui.PushStyleVar(styleVars[i][1], styleVars[i][2]);
end
return { #styleColors, #styleVars };
end
function M.pop(pushed)
imgui.PopStyleVar(pushed[2]);
imgui.PopStyleColor(pushed[1]);
end
return M;