pfUI's Who list raised on every row, because 1.12's font colours are tables and it calls methods
`NORMAL_FONT_COLOR:GetRGB()` is how pfUI colours each row of the /who list
(`modules/socialmod.lua:145`) and the master-loot menu (`modules/loot.lua`, five
more). 1.12 ships that global as a loose `{ r, g, b }` table; every client from
Legion on makes it a `ColorMixin` object. grom's session carried 26 `attempt to
call method 'GetRGB' (a nil value)` raises, all of them from opening the Who tab,
and the list drew uncoloured.
The six `*_FONT_COLOR` tables 1.12 declares now answer `ColorMixin`'s methods
through a metatable rather than a `Mixin` copy: these tables are FrameXML's, and
a 1.12 addon is free to walk one with `pairs`, so `__index` leaves
`pairs(NORMAL_FONT_COLOR)` answering exactly the keys it answers on the
reference. `WHITE_FONT_COLOR`, which 1.12 has no global for at all and pfUI's
totem tooltip and minimap label both index, is created here too.
In `manifest.rs` beside the other load-time repairs rather than next to
`ColorMixin` itself, for the same mechanical reason as `/focus`: the tables do
not exist until `Fonts.xml` has run, and the compat layer installs before
FrameXML loads.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
This commit is contained in:
@@ -71,6 +71,10 @@ fn bootstrap_positions(script: &UiScript) -> Vec<String> {
|
||||
error!("ui_script: focus slash commands: {e}");
|
||||
return vec![format!("focus slash commands: {e}")];
|
||||
}
|
||||
if let Err(e) = install_font_colour_objects(script) {
|
||||
error!("ui_script: font colour objects: {e}");
|
||||
return vec![format!("font colour objects: {e}")];
|
||||
}
|
||||
Vec::new()
|
||||
}
|
||||
|
||||
@@ -109,6 +113,65 @@ if SlashCmdList then
|
||||
end
|
||||
"#;
|
||||
|
||||
/// ClassicAPI (`docs/ADDON_COMPAT.md`): 1.12's `*_FONT_COLOR` globals answer `ColorMixin`'s
|
||||
/// methods, and `WHITE_FONT_COLOR` exists at all.
|
||||
///
|
||||
/// 1.12 ships these as loose `{ r, g, b }` tables (`Fonts.xml`); every client from Legion on
|
||||
/// makes them `ColorMixin` objects, and addon code written in that dialect calls methods on them
|
||||
/// rather than reading the fields. pfUI does, in both directions at once:
|
||||
///
|
||||
/// - `modules/socialmod.lua:145` colours each Who row with `NORMAL_FONT_COLOR:GetRGB()`, and
|
||||
/// `modules/loot.lua` does the same five times for the master-loot menu. grom's session carried
|
||||
/// **26** `attempt to call method 'GetRGB' (a nil value)` raises, every one of them from opening
|
||||
/// the Who tab — the list drew uncoloured and each row cost a traceback.
|
||||
/// - `modules/totems.lua:22` and `modules/minimap.lua:198` call `WHITE_FONT_COLOR:GetRGB()` and
|
||||
/// `:GetRGBA()`, and 1.12 has no such global at all, so those index a nil.
|
||||
///
|
||||
/// Both are a one-line gap in the same place, so both are closed here.
|
||||
///
|
||||
/// **A metatable, not `Mixin`.** Upstream's `CreateColor` copies the methods in as fields, which is
|
||||
/// right for an object it owns. These tables are not ours: FrameXML built them, FrameXML reads
|
||||
/// them, and a 1.12 addon is free to walk one with `pairs`. `__index` leaves `pairs(NORMAL_FONT_COLOR)`
|
||||
/// answering exactly the three keys it answers on the reference while the methods still resolve.
|
||||
/// `a` is the one field added, because `GetRGBA` has to answer something and modern WoW's own
|
||||
/// `NORMAL_FONT_COLOR` is opaque; `.a` on a 1.12 colour table is nil, and nothing in the stock UI
|
||||
/// reads it.
|
||||
///
|
||||
/// Here rather than in `benilla-ui`'s `compat/classic_api/color_mixin.rs` for the same mechanical
|
||||
/// reason as [`install_focus_slash_commands`]: the tables do not exist until `Fonts.xml` has run,
|
||||
/// and the compat layer installs before FrameXML loads.
|
||||
pub(super) fn install_font_colour_objects(script: &UiScript) -> Result<(), String> {
|
||||
script.run(FONT_COLOUR_OBJECTS).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// The `*_FONT_COLOR` tables 1.12 declares (`reference/1.12-globals.tsv`), named one by one: a
|
||||
/// sweep of `_G` for a key ending in `_FONT_COLOR` would also catch an addon's own table of the
|
||||
/// same name and give it a metatable it did not ask for.
|
||||
const FONT_COLOUR_OBJECTS: &str = r#"
|
||||
if ColorMixin then
|
||||
local meta = { __index = ColorMixin }
|
||||
local function upgrade(c)
|
||||
-- An existing metatable is somebody's own arrangement; leave it be.
|
||||
if type(c) ~= "table" or getmetatable(c) then return c end
|
||||
if c.a == nil then c.a = 1 end
|
||||
setmetatable(c, meta)
|
||||
return c
|
||||
end
|
||||
|
||||
upgrade(NORMAL_FONT_COLOR)
|
||||
upgrade(HIGHLIGHT_FONT_COLOR)
|
||||
upgrade(GRAY_FONT_COLOR)
|
||||
upgrade(GREEN_FONT_COLOR)
|
||||
upgrade(RED_FONT_COLOR)
|
||||
upgrade(PASSIVE_SPELL_FONT_COLOR)
|
||||
|
||||
-- Not a 1.12 global: modern WoW's `WHITE_FONT_COLOR = CreateColor(1, 1, 1)`.
|
||||
if not WHITE_FONT_COLOR then
|
||||
WHITE_FONT_COLOR = upgrade({ r = 1, g = 1, b = 1 })
|
||||
end
|
||||
end
|
||||
"#;
|
||||
|
||||
/// SuperWoW (`docs/ADDON_COMPAT.md`): the macro body limit is 255 letters in 1.12 and 510 under
|
||||
/// SuperWoW. [`benilla_ui::script::MAX_MACRO_BODY`] already stores the raised cap; two things in
|
||||
/// the stock UI still carry the old number from the player's own install and need resetting at
|
||||
@@ -351,6 +414,76 @@ pub(crate) fn load_ingame_ui(
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// pfUI's Who list and master-loot menu colour their rows with `NORMAL_FONT_COLOR:GetRGB()`,
|
||||
/// and its totem tooltip and minimap zone label with `WHITE_FONT_COLOR`'s. 1.12 has loose
|
||||
/// tables for the first and no global at all for the second, which is 26 raises in one of
|
||||
/// grom's sessions, all of them from opening the Who tab.
|
||||
#[test]
|
||||
fn the_font_colour_globals_answer_colormixin_methods() {
|
||||
let s = UiScript::new().unwrap();
|
||||
// The shape `Fonts.xml` leaves behind.
|
||||
s.run("NORMAL_FONT_COLOR = { r = 1, g = 0.82, b = 0 }")
|
||||
.unwrap();
|
||||
install_font_colour_objects(&s).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
s.eval::<(f64, f64, f64)>("return NORMAL_FONT_COLOR:GetRGB()")
|
||||
.unwrap(),
|
||||
(1.0, 0.82, 0.0),
|
||||
"pfUI `modules/socialmod.lua:145`"
|
||||
);
|
||||
assert_eq!(
|
||||
s.eval::<(f64, f64, f64, f64)>("return WHITE_FONT_COLOR:GetRGBA()")
|
||||
.unwrap(),
|
||||
(1.0, 1.0, 1.0, 1.0),
|
||||
"pfUI `modules/minimap.lua:198`; 1.12 has no such global, so this one is ours"
|
||||
);
|
||||
|
||||
// The fields FrameXML itself reads are untouched, and `pairs` still answers the keys a
|
||||
// 1.12 addon expects — which is why this is a metatable and not a `Mixin` copy.
|
||||
assert_eq!(
|
||||
s.eval::<f64>("return NORMAL_FONT_COLOR.g").unwrap(),
|
||||
0.82,
|
||||
"`OptionsFrameAdapters.lua` reads `.r/.g/.b` directly"
|
||||
);
|
||||
let mut keys: Vec<String> = s
|
||||
.eval::<Vec<String>>(
|
||||
"local t = {} for k in pairs(NORMAL_FONT_COLOR) do table.insert(t, k) end return t",
|
||||
)
|
||||
.unwrap();
|
||||
keys.sort();
|
||||
assert_eq!(
|
||||
keys,
|
||||
vec!["a", "b", "g", "r"],
|
||||
"no methods leak into `pairs`"
|
||||
);
|
||||
}
|
||||
|
||||
/// A table that already carries a metatable is somebody else's object, and a second run of
|
||||
/// the patch (a `ReloadUI`) must not stack anything.
|
||||
#[test]
|
||||
fn upgrading_a_colour_table_leaves_an_existing_metatable_alone() {
|
||||
let s = UiScript::new().unwrap();
|
||||
s.run(
|
||||
"NORMAL_FONT_COLOR = setmetatable({ r = 1, g = 1, b = 1 }, \
|
||||
{ __index = function() return \"mine\" end })",
|
||||
)
|
||||
.unwrap();
|
||||
install_font_colour_objects(&s).unwrap();
|
||||
assert_eq!(
|
||||
s.eval::<String>("return NORMAL_FONT_COLOR.anything")
|
||||
.unwrap(),
|
||||
"mine",
|
||||
"an existing metatable is left as it was found"
|
||||
);
|
||||
assert!(
|
||||
s.eval::<bool>("return rawget(NORMAL_FONT_COLOR, \"a\") == nil")
|
||||
.unwrap(),
|
||||
"and no field is added to it either (rawget: this table's own `__index` answers \
|
||||
every key)"
|
||||
);
|
||||
}
|
||||
|
||||
/// ClassicAPI's `/focus` and `/clearfocus` (`docs/ADDON_COMPAT.md`): registered into the
|
||||
/// table the stock `ChatFrame.lua` declares, and reachable by the same walk every typed or
|
||||
/// macro line takes ([`UiScript::run_slash_command`]).
|
||||
|
||||
Reference in New Issue
Block a user