A tooltip Lua sized itself kept being re-sized out from under it

benilla auto-sizes a GameTooltip in a pre-pass of every resolve, because
that is where the text measures are. The reference re-sizes one only from
its own content verbs and from Show: 0x530640 has six call sites
(0x52ad59, 0x52b029, 0x52e5ae, 0x52f869, 0x530615, 0x530aad), all inside
the tooltip code and none in the frame pump. So a SetWidth/SetHeight
written after that call stands, and ours was overwritten on the next frame.

ATSW2's recipe tooltip is the case grom hit: it Show()s, then sizes the
plate from its own line count and a column width floored at 350. Losing
that width collapsed the backdrop to the one blank line it pushes through
our AddLine and dropped every right column, anchored RIGHT -10 to the
plate, on top of its left column -- "Required reagents:" and
"Bags Bank Alts" sharing a pixel column in his screenshot.

TooltipState::authored_size: set by SetWidth/SetHeight through the
existing note_authored_size, cleared by any content change, by
SetMinimumWidth/SetPadding, and by Show. layout_tooltips skips a plate
that carries it.

Clearing it on Show matters: pfUI and pfQuest both SetWidth the shared
GameTooltip *before* Show, where the reference overwrites it -- honouring
those would pin every later hover at 180 or 100 px. Two tests pin both
halves.

Measured, driving ATSW2's own OnEnter: plate 350.0 x 81.7, the two
columns 232 px apart.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
This commit is contained in:
2026-11-05 20:23:26 -07:00
co-authored by Claude Opus 5
parent 9860c092de
commit 14a1a7179e
6 changed files with 223 additions and 16 deletions
+7 -2
View File
@@ -76,9 +76,14 @@ impl Model {
/// A size was authored on `h` (`SetWidth`, `SetHeight`, or XML `<Size>` through them): the
/// implicit rect yields to it for good, as the reference's override does.
///
/// A `GameTooltip`'s auto-size yields to it too, but only until the plate's content next
/// changes or it is `Show`n again ([`crate::widget::TooltipState::authored_size`]).
pub(crate) fn note_authored_size(&mut self, h: FrameHandle) {
if let Some(KindState::Model(m)) = self.arena.frame_mut(h).map(|f| &mut f.kind_state) {
m.implicit_size = false;
match self.arena.frame_mut(h).map(|f| &mut f.kind_state) {
Some(KindState::Model(m)) => m.implicit_size = false,
Some(KindState::Tooltip(t)) => t.authored_size = true,
_ => {}
}
}
@@ -312,6 +312,75 @@ fn unmeasured_lines_hold_declared_size() {
.unwrap();
}
/// A size Lua writes once the plate is filled holds: the reference re-sizes a tooltip only from
/// its own content verbs and from `Show` (`0x530640`, six call sites — `0x52ad59`, `0x52b029`,
/// `0x52e5ae`, `0x52f869`, `0x530615`, `0x530aad` — all inside the tooltip code and none in the
/// frame pump), so nothing runs again to take it back. ATSW2's recipe tooltip is the case: it
/// `Show()`s and then sizes the plate from its own line count and column width
/// (`AdvancedTradeSkillWindow2.lua:5913-5920`). benilla auto-sizes in a pre-pass of every
/// `resolve`, which used to overwrite that on the very next frame — the plate drew one line tall
/// and its right column landed on top of its left one.
#[test]
fn a_size_authored_after_the_content_holds_until_the_content_changes() {
let mut s = script();
s.set_screen_size(800.0, 600.0);
s.run(
r#"
local a = CreateFrame("Button", "A9"); a:SetPoint("TOPLEFT", 100, -100); a:SetWidth(10); a:SetHeight(10)
local tt = CreateFrame("GameTooltip", "TT9")
tt:SetOwner(a, "ANCHOR_RIGHT")
tt:AddLine("one")
tt:Show()
tt:SetHeight(170)
tt:SetWidth(350)
"#,
)
.unwrap();
measure_all(&mut s, &[("one", 20.0, 12.0)]);
s.run(
r#"
assert(TT9:GetWidth() == 350, "authored width holds, got " .. TT9:GetWidth())
assert(TT9:GetHeight() == 170, "authored height holds, got " .. TT9:GetHeight())
"#,
)
.unwrap();
// Fresh content takes the plate back, both dimensions: one 20x12 line ⇒ 40 x 32.
s.run(r#"TT9:AddLine("two")"#).unwrap();
measure_all(&mut s, &[("one", 20.0, 12.0), ("two", 20.0, 12.0)]);
s.run(
r#"
assert(TT9:GetWidth() == 40, "auto width is back, got " .. TT9:GetWidth())
assert(TT9:GetHeight() == 46, "auto height is back, got " .. TT9:GetHeight())
"#,
)
.unwrap();
}
/// The other half of that rule: a size written BEFORE `Show` is overwritten, because `Show` is
/// itself one of the resize's call sites (`0x530aad`). That is what makes pfUI's
/// `GameTooltip:SetWidth(180)` ahead of its `Show` (`pfUI/modules/addons.lua:245`) and pfQuest's
/// `SetWidth(100)` (`pfQuest/config.lua:634`) no-ops on the reference too — freezing the shared
/// `GameTooltip` at their width for every hover after would be the regression.
#[test]
fn a_size_authored_before_show_is_overwritten_by_the_auto_size() {
let mut s = script();
s.set_screen_size(800.0, 600.0);
s.run(
r#"
local a = CreateFrame("Button", "A10"); a:SetPoint("TOPLEFT", 100, -100); a:SetWidth(10); a:SetHeight(10)
local tt = CreateFrame("GameTooltip", "TT10")
tt:SetOwner(a, "ANCHOR_RIGHT")
tt:AddLine("one")
tt:SetWidth(180)
tt:Show()
"#,
)
.unwrap();
measure_all(&mut s, &[("one", 20.0, 12.0)]);
s.run(r#"assert(TT10:GetWidth() == 40, "auto width wins, got " .. TT10:GetWidth())"#)
.unwrap();
}
#[test]
fn minimum_width_floors_autosize() {
let mut s = script();
+19 -11
View File
@@ -271,6 +271,8 @@ pub(super) fn clear_content(model: &mut Model, h: FrameHandle) {
if let Ok(t) = tip_mut(model, h) {
t.num_lines = 0;
t.min_width = 0.0;
// Fresh content: the auto-size takes the plate back from any Lua-authored size.
t.authored_size = false;
t.unit_token = None;
// The set-time guid goes with the token it was recorded beside: a cleared tooltip shows no
// unit, so `GetUnitGUID` must stop answering for the one it used to show.
@@ -503,6 +505,7 @@ pub(super) fn append_line(
}
if let Ok(t) = tip_mut(&mut model, h) {
t.num_lines = n;
t.authored_size = false;
}
cancel_fade(&mut model, h);
Ok(())
@@ -594,17 +597,22 @@ pub(super) fn layout_tooltips(model: &mut Model) {
if cursor_mode {
cursor_anchor(model, h);
}
let (num, lefts, rights, min_w, pad_w) = match model.arena.frame(h).map(|f| &f.kind_state) {
Some(KindState::Tooltip(t)) => (
t.num_lines,
t.left_lines.clone(),
t.right_lines.clone(),
t.min_width,
t.padding,
),
_ => continue,
};
if num == 0 {
let (num, lefts, rights, min_w, pad_w, authored) =
match model.arena.frame(h).map(|f| &f.kind_state) {
Some(KindState::Tooltip(t)) => (
t.num_lines,
t.left_lines.clone(),
t.right_lines.clone(),
t.min_width,
t.padding,
t.authored_size,
),
_ => continue,
};
// Lua sized this plate itself after its last content verb: leave it alone, size and right
// column both, until the content changes or it is `Show`n again. The reference's resize
// (`0x530640`) simply does not run again in between ([`TooltipState::authored_size`]).
if num == 0 || authored {
continue;
}
let n = num.min(lefts.len()).min(rights.len());
+17 -3
View File
@@ -397,7 +397,11 @@ pub(in crate::script) fn install(lua: &Lua) -> mlua::Result<()> {
m.set(
"SetMinimumWidth",
lua.create_function(|lua, (this, w): (Table, f32)| {
with_tip(lua, &this, |t| t.min_width = w.max(0.0))
with_tip(lua, &this, |t| {
t.min_width = w.max(0.0);
// An auto-size input: the plate goes back to auto-sizing, as it does on content.
t.authored_size = false;
})
})?,
)?;
// SetPadding(w): extra auto-size width that survives a content clear (ItemRefTooltip's 16
@@ -405,7 +409,10 @@ pub(in crate::script) fn install(lua: &Lua) -> mlua::Result<()> {
m.set(
"SetPadding",
lua.create_function(|lua, (this, w): (Table, f32)| {
with_tip(lua, &this, |t| t.padding = w.max(0.0))
with_tip(lua, &this, |t| {
t.padding = w.max(0.0);
t.authored_size = false;
})
})?,
)?;
// FadeOut(): alpha ramps to 0 over `TOOLTIP_FADE_SECS`, then the full hide; new content or
@@ -433,7 +440,14 @@ pub(in crate::script) fn install(lua: &Lua) -> mlua::Result<()> {
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
full_alpha(&mut model, h);
tip_mut(&mut model, h)
.map(|t| t.owner.is_some() && t.num_lines > 0)
.map(|t| {
// Show re-sizes from the content (`0x530aad` calls `0x530640`), so a size
// authored before it is overwritten — which is what makes pfUI's
// `GameTooltip:SetWidth(180)` ahead of its `Show` a no-op on the reference
// too (`pfUI/modules/addons.lua:245`). One written after it stands.
t.authored_size = false;
t.owner.is_some() && t.num_lines > 0
})
.unwrap_or(false)
};
match live {
+11
View File
@@ -208,6 +208,17 @@ pub struct TooltipState {
pub equipped_header_armed: bool,
/// `SetPadding`: extra width past the content (`ItemRef.xml:40` sets 16 for the close button).
pub padding: f32,
/// Lua sized this plate itself (`SetWidth`/`SetHeight`) since its content last changed, so
/// the auto-size yields to it until the content does change. The reference re-sizes a tooltip
/// only from its own content verbs and from `Show` — `0x530640` has six call sites
/// (`0x52ad59`, `0x52b029`, `0x52e5ae`, `0x52f869`, `0x530615`, `0x530aad`), all inside the
/// tooltip code and none in the frame pump — so a size written after that call stands.
/// benilla auto-sizes in a pre-pass of every `resolve` instead (the measures live there), and
/// without this flag that pre-pass overwrote the explicit size on the very next frame.
/// ATSW2's recipe tooltip is the case: it `Show()`s, then sizes the plate from its own line
/// count and column width (`AdvancedTradeSkillWindow2.lua:5913-5920`), and drew as a
/// one-line box with its right column on top of its left one.
pub authored_size: bool,
/// Line 1 came from XML (`ShoppingTooltipTemplate`), so later lines clone the previous fonts.
pub xml_declared_lines: bool,
/// What this tooltip is currently showing, for ClassicAPI's `GetItem`/`GetSpell`/`Has*`
+100
View File
@@ -4551,3 +4551,103 @@ vignetted by `ATSWBackgroundShadow`) — the one piece of this window version 1
for. **Hidden, not blanked**, because the addon re-sets the texture on every open and every
profession switch; `Hide` survives both, and a `hooksecurefunc` on `ATSW_UpdateBackground` is belt
and braces. Nothing anchors to either texture, so nothing moved.
## A tooltip the addon sized itself was re-sized out from under it every frame
grom, with a screenshot: *"the tooltip box when using atsw2 is not drawing correctly for some
reason."* In the shot the plate's border wraps the recipe's name and stops; the rest of the
tooltip's text hangs outside it over the window, and one line is two strings drawn on top of each
other — `Required reagents:` with `Bags Bank Alts` sitting on it.
Both are one defect, and it is ours.
### The rule the reference keeps
A `GameTooltip` auto-sizes from its lines in `0x530640`, which measures each line (`0x772890`) and
writes the plate. Scanning `.text` for `call` relocations to it finds **six** call sites —
`0x52ad59`, `0x52b029`, `0x52e5ae`, `0x52f869`, `0x530615` and `0x530aad` — all inside the tooltip's
own code, and the last of those is `Show` (`0x530a80`). **None is in the frame pump.** So the
reference re-sizes a tooltip when its content changes and when it is shown, and at no other time: a
`SetWidth`/`SetHeight` written after that call stands until the next one.
benilla auto-sizes in a pre-pass of every `resolve` instead (`script::tooltip::layout_tooltips`),
because that is where the text measures are. That is a fine place to *compute* it and the wrong
rule for *when*: the pre-pass overwrote the explicit size on the very next frame. Ten lines of Lua
prove it on their own, no addon involved:
```
tip1 SetWidth/SetHeight after Show immediately 350.0 x 170.0 | a few frames later 23.8 x 33.3
tip2 SetWidth/SetHeight before Show immediately 350.0 x 170.0 | a few frames later 23.8 x 33.3
```
ATSW2 builds its recipe tooltip by hand — its own line pool, its own `AddDoubleLine`, a
`ClearLines` that shadows ours — and sizes the plate itself at the end:
`Tooltip:Show()`, then `SetHeight(NumLines * 18 + 12 - …)`, then `SetWidth(MaxLineWidth)`, which is
floored at 350 (`AdvancedTradeSkillWindow2.lua:5913-5920`). Losing that width is both symptoms at
once: the backdrop collapses to the one blank line the addon pushed through our `AddLine`, and
every right column, anchored `RIGHT, -10` to the plate, lands on top of its left column.
### The fix
`TooltipState::authored_size`: set by `SetWidth`/`SetHeight` through the existing
`note_authored_size`, cleared by any content change (`clear_content`, `append_line`), by
`SetMinimumWidth`/`SetPadding`, and by `Show`. `layout_tooltips` skips a plate that carries it —
size and right-column flush both, since the reference's resize is all-or-nothing and simply does
not run again.
Clearing it on `Show` is not a detail. pfUI (`modules/addons.lua:245`) and pfQuest
(`config.lua:634`) both call `GameTooltip:SetWidth(…)` *before* `Show`, where the reference's resize
overwrites it — those calls are no-ops on a real client, and honouring them would freeze the shared
`GameTooltip` at 180 or 100 px wide for every hover afterwards. Two tests pin both halves, and
`unmeasured_lines_hold_declared_size` already covered the third case (a declared size holding until
the measures land).
Measured after, driving ATSW2's own `OnEnter`: plate **350.0 x 81.7**, `Required reagents:` at
x=566 and `Bags Bank Alts` at x=798 — 232 px apart, where before they shared a pixel column.
### The right-hand pane was black text on a dark panel
*"can we make the font on the right like yellow like the title or white so it's readable?"*
Measured rather than guessed — `GetTextColor()` on each string of that pane:
| string | colour | |
|---|---|---|
| `ATSWRecipeName` | `0/0/0` | the item's name, 17 pt, no outline |
| `ATSWTool1Text` | `0/0/0` | the tools line |
| `ATSWCostText`, `ATSWCraftDescription` | `0/0/0` | |
| `ATSWToolsLabel`, `ATSWReagentsLabel` | `1/0.82/0` | already right |
| `ATSWReagent1Title` | `1/1/1` | |
They are **black** by the addon's own design — `ATSWFont` and `CostFont` declare
`<Color r="0" g="0" b="0"/>` and the tool template's `<NormalFont>` does too — because the addon
paints this pane over its own light parchment. That parchment came off last round at his request,
so the black now sits on a dark pfUI backdrop. The skin repairs what the skin broke: the name goes
yellow like the title, the rest white.
The tools line needed more than a colour write, because it has three writers: `ATSW_ShowRecipe`
paints it `SetTextColor(R, 0, 0)` where `R` is 1 only when the tool is **missing**, the template's
`OnEnter` paints it white, and its `OnLeave` paints `(this.R, 0, 0)` back. Rather than chase all
three, the string's own `SetTextColor` is wrapped to send black to white and leave every other
colour alone — so a tool you lack is still red, which is the only thing that colour carries.
### A border for the task list
*"would also love to see some sort of border around the queued task box because its a little hard
to distinguish right now."*
`ATSWTaskScrollFrame` is the box's geometry (L=625 R=981 T=375 B=260 against a window of L=279
R=1047 T=722 B=210) but it is **not** the rows' parent and it is not shown: version 2 scrolls the
list itself and parents every `ATSWTask%d` to `ATSWFrame`, exactly as it does with the recipe rows
and `ATSWListScrollFrame`. A backdrop on a hidden frame draws nothing, so the panel is its own
frame anchored to that rect, held one frame level below the rows so it sits behind them.
### Driving ATSW2 in a capture needs version 1 out of the way
Both addons are installed and **both use the `ATSW` prefix** — `ATSWFrame`, `ATSWRecipe1`,
`ATSW_RECIPES_DISPLAYED`. A capture loads every folder under `AddOns/` regardless of what the
player has enabled, so version 1 took the globals and `getglobal("ATSWRecipe1")` answered nil while
a perfectly good trade-skill window was on screen — version 1's. `BENILLA_HOME=<scratch>` with an
`AddOns/` of symlinks to every folder but `AdvancedTradeSkillWindow`, and a `saved` symlink beside
it for `WOW_CAPTURE_SAVED`, gives a deterministic version-2 run without touching the player's
install.