GameTooltip's fade, owner and health bar follow the 1.12.1 lifecycle (#907)

GameTooltip's lifecycle took shortcuts the reference does not: a 0.5 s linear fade armed once, a fade cancelled by new lines, cursor-anchored plates that faded instead of hiding, a SetOwner that never hid first, the last anchor kept after a hide, and a health bar shown and hidden without its handlers.

In the reference, FadeOut re-arms a 2 s countdown that holds full alpha for half and then ramps, or hides at once at the cursor anchor. Only the owner core, the show and FadeOut write the fade. SetOwner runs the tooltip's own hide first. The un-own leaves ANCHOR_NONE. The clear hides the bar through its own transition.

Every builder, verb and world or minimap seat now goes through these functions; an uncached item hides and un-owns as in the reference.

Tests pin each of these.

Fixes #889
Fixes #134
Fixes #896
Fixes #893
Fixes #895
Fixes #843

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Sam Sung
2026-10-11 23:33:17 +07:00
committed by GitHub
co-authored by Claude Opus 5.5
parent b63d6e3609
commit 0df676394e
20 changed files with 935 additions and 550 deletions
@@ -889,6 +889,8 @@ pub(super) fn seed_ui_fixture(
"mouseover",
Some(benilla_ui::script::UnitState {
exists: true,
// A unit is found by its guid; the builder hides for guid 0 (`0x52a04e`).
guid: 0xF130_0000_0044_0001,
name: Some("Stormwind Guard".into()),
health: 38,
max_health: 50,
@@ -916,6 +918,7 @@ pub(super) fn seed_ui_fixture(
"mouseover",
Some(benilla_ui::script::UnitState {
exists: true,
guid: 0x0000_0000_0000_0B0B,
is_player: true,
name: Some("Bob".into()),
level: 60,
+5 -5
View File
@@ -107,9 +107,9 @@ pub(in crate::minimap) fn emit_quest_dots(
color: [tint, tint, tint, ctx.alpha],
..default()
});
if let (Some(c), Some(ui)) = (ctx.cursor, ctx.cursor_ui) {
if let Some(c) = ctx.cursor {
if rect.contains(c) {
*hover = MinimapBlipHover::Npc(npc, ui, grey);
*hover = MinimapBlipHover::Npc(npc, grey);
}
}
}
@@ -200,11 +200,11 @@ pub(in crate::minimap) fn emit_tracking_dots(
color: [tint, tint, tint, ctx.alpha],
..default()
});
if let (Some(c), Some(ui)) = (ctx.cursor, ctx.cursor_ui) {
if let Some(c) = ctx.cursor {
if rect.contains(c) {
*hover = match name {
DotName::Guid => MinimapBlipHover::Npc(guid, ui, grey),
DotName::Known(n) => MinimapBlipHover::TrackedGo(n, ui, grey),
DotName::Guid => MinimapBlipHover::Npc(guid, grey),
DotName::Known(n) => MinimapBlipHover::TrackedGo(n, grey),
};
}
}
+26 -33
View File
@@ -144,13 +144,13 @@ const POI_ICON_PX: f32 = 16.0;
pub(crate) enum MinimapBlipHover {
#[default]
None,
/// A landmark's name and the cursor's UI-space point, the tooltip's seat.
Landmark(String, Vec2),
/// A unit dot's guid, resolved through the name cache, the cursor point, and whether the dot
/// drew grey; the tooltip line greys the same way (`|cffb0b0b0`).
Npc(u64, Vec2, bool),
/// A tracked GameObject dot: its template name, the cursor point and the grey flag.
TrackedGo(String, Vec2, bool),
/// A landmark's name.
Landmark(String),
/// A unit dot's guid, resolved through the name cache, and whether the dot drew grey; the
/// tooltip line greys the same way (`|cffb0b0b0`).
Npc(u64, bool),
/// A tracked GameObject dot: its template name and the grey flag.
TrackedGo(String, bool),
}
/// The blip layer's system inputs, tupled to keep `emit_minimap` under Bevy's 16-parameter cap.
@@ -199,9 +199,6 @@ pub(super) struct BlipCtx {
pub(super) wz: f32,
/// The cursor in the quads' y-down logical px.
pub(super) cursor: Option<Vec2>,
/// The cursor in the y-up root units; the reference seats the blip tooltip at the cursor, its
/// exact offset untraced.
pub(super) cursor_ui: Option<Vec2>,
/// Window px per root unit: everything else here is window px, while Lua
/// ([`Minimap:PingLocation`](super::ping)) hands root units.
pub(super) seam: f32,
@@ -306,9 +303,9 @@ pub(super) fn emit_landmarks(
color: [1.0, 1.0, 1.0, ctx.alpha],
..default()
});
if let (Some(c), Some(ui)) = (ctx.cursor, ctx.cursor_ui) {
if let Some(c) = ctx.cursor {
if rect.contains(c) {
*hover = MinimapBlipHover::Landmark(poi.name.clone(), ui);
*hover = MinimapBlipHover::Landmark(poi.name.clone());
}
}
}
@@ -318,9 +315,9 @@ pub(super) fn emit_landmarks(
continue;
};
let rect = push_rim_arrow(ctx, poi.pos, tex, quads);
if let (Some(c), Some(ui)) = (ctx.cursor, ctx.cursor_ui) {
if let Some(c) = ctx.cursor {
if rect.contains(c) {
*hover = MinimapBlipHover::Landmark(poi.name.clone(), ui);
*hover = MinimapBlipHover::Landmark(poi.name.clone());
}
}
}
@@ -402,15 +399,15 @@ pub(super) fn emit_party_arrows(
}
}
/// Show the hovered blip's name; a unit's resolves through the name cache. Hover loss fades the
/// tooltip, never hides it at once. Runs after `UnitFeed`, so a same-frame move from a world
/// hover to a blip ends on the blip's tooltip.
/// Show the hovered blip's name; a unit's resolves through the name cache. Hover loss hides the
/// tooltip at once (`0x4eb0c0`). Runs after `UnitFeed`, so a same-frame move from a world hover to
/// a blip ends on the blip's tooltip.
pub(super) fn drive_blip_tooltip(
script: Option<NonSendMut<UiScript>>,
hover: Res<MinimapBlipHover>,
names: Res<NameCache>,
commands: Res<NetCommands>,
mut last: Local<crate::ui_script::VmMemo<Option<(String, Vec2)>>>,
mut last: Local<crate::ui_script::VmMemo<Option<String>>>,
) {
let Some(mut script) = script else {
return;
@@ -418,26 +415,22 @@ pub(super) fn drive_blip_tooltip(
let last = last.get(&script);
let show = match &*hover {
MinimapBlipHover::None => None,
MinimapBlipHover::Landmark(name, at) => Some((name.clone(), *at, false)),
MinimapBlipHover::Npc(npc, at, grey) => names
MinimapBlipHover::Landmark(name) => Some((name.clone(), false)),
MinimapBlipHover::Npc(npc, grey) => names
.resolve(*npc, &commands)
.map(|n| (n.to_string(), *at, *grey)),
MinimapBlipHover::TrackedGo(name, at, grey) => Some((name.clone(), *at, *grey)),
.map(|n| (n.to_string(), *grey)),
MinimapBlipHover::TrackedGo(name, grey) => Some((name.clone(), *grey)),
};
match (&show, &*last) {
// Same blip: the reference's blip tooltip follows the cursor.
(Some((t, at, _)), Some((lt, lat))) if t == lt => {
if at != lat {
script.world_tooltip_move(at.x, at.y);
*last = Some((t.clone(), *at));
}
}
(Some((t, at, grey)), _) => {
script.minimap_tooltip(t, at.x, at.y, *grey);
*last = Some((t.clone(), *at));
// Same blip: the plate, owned in mode 6, follows the cursor in its own update.
(Some((t, _)), Some(lt)) if t == lt => {}
(Some((t, grey)), _) => {
script.minimap_tooltip(t, *grey);
*last = Some(t.clone());
}
// No blip hovered: `0x4eb0c0` hides the plate directly, with no fade.
(None, Some(_)) => {
script.world_tooltip_fade();
script.world_tooltip_hide();
*last = None;
}
(None, None) => {}
-5
View File
@@ -636,11 +636,6 @@ fn emit_minimap(
wy: (wy / q).round() * q,
wz: wow[2],
cursor,
// The cursor in the root's units (y-up, ÷ the seam scale): the tooltip anchor resolves
// in them, not window px.
cursor_ui: cursor
.zip(win)
.map(|(c, w)| crate::ui_script::window_to_ui(w.height(), c)),
seam,
}
});
-1
View File
@@ -189,7 +189,6 @@ mod tests {
wy: 0.0,
wz: 0.0,
cursor: None,
cursor_ui: None,
seam: 1.0,
}
}
@@ -37,6 +37,7 @@ fn harness(extra: &[&str]) -> UiScript {
fn wolf() -> UnitState {
UnitState {
exists: true,
guid: 0xF130_0000_4500_0001,
name: Some("Timber Wolf".into()),
health: 30,
max_health: 50,
@@ -232,6 +233,8 @@ fn your_own_portrait_explains_the_menu_instead_of_showing_your_health() {
s.set_unit(
"target",
Some(UnitState {
// Another unit than `player`, so `UnitIsUnit("target", "player")` is false.
guid: 0x0000_0000_0000_0C0C,
is_player: true,
player_controlled: true,
name: Some("Someone".into()),
@@ -472,7 +475,7 @@ fn buff_hover_hangs_below_left_of_the_button() {
fn a_cursor_seated_gameobject_plate_is_owned() {
benilla_formats::wow_data_or_skip!();
let mut s = harness(&[]);
assert!(s.world_tooltip_gameobject("Brill", &[], Some((512.0, 384.0))));
assert!(s.world_tooltip_gameobject("Brill", &[], true));
let owned: bool = s.eval("return GameTooltip:IsOwned(UIParent)").unwrap();
assert!(
owned,
@@ -491,7 +494,7 @@ fn a_cursor_seated_gameobject_plate_survives_an_addons_on_show_hook() {
// Questie's hook, in one line: the plate's own show event calls Show() again.
s.run(r#"GameTooltip:SetScript("OnShow", function() GameTooltip:Show() end)"#)
.unwrap();
assert!(s.world_tooltip_gameobject("Brill", &[], Some((512.0, 384.0))));
assert!(s.world_tooltip_gameobject("Brill", &[], true));
let shown: bool = s
.eval("return GameTooltip:IsShown() and true or false")
.unwrap();
@@ -510,7 +513,7 @@ fn a_cursor_seated_gameobject_plate_survives_an_addons_on_show_hook() {
fn a_corner_seated_gameobject_plate_is_owned_and_shown() {
benilla_formats::wow_data_or_skip!();
let mut s = harness(&[]);
assert!(s.world_tooltip_gameobject("Ironforge Main Gate", &[], None));
assert!(s.world_tooltip_gameobject("Ironforge Main Gate", &[], false));
let owned: bool = s.eval("return GameTooltip:IsOwned(UIParent)").unwrap();
assert!(owned, "the corner plate is owned; errors: {:?}", s.errors());
let shown: bool = s
@@ -526,7 +529,7 @@ fn a_corner_seated_gameobject_plate_survives_an_addons_on_show_hook() {
let mut s = harness(&[]);
s.run(r#"GameTooltip:SetScript("OnShow", function() GameTooltip:Show() end)"#)
.unwrap();
assert!(s.world_tooltip_gameobject("Ironforge Main Gate", &[], None));
assert!(s.world_tooltip_gameobject("Ironforge Main Gate", &[], false));
let shown: bool = s
.eval("return GameTooltip:IsShown() and true or false")
.unwrap();
@@ -1562,6 +1562,8 @@ fn status_bar_text_paints_the_player_numerals_but_not_the_targets() {
let alive = |health: u32, power: u32, power_type: u8| UnitState {
exists: true,
// The tooltip builder finds a unit by its guid (`0x52a021`).
guid: 0xF130_0000_0000_0B0D,
name: Some("Somebody".into()),
health,
max_health: 100,
+23 -8
View File
@@ -58,6 +58,13 @@ enum LastHover {
Corpse(u64),
}
/// The publisher's teardown (`0x4928d5`–`0x492909`): every change of the mouseover away from a
/// non-null one requests the fade (`0x530ae0`) before the new plate builds, so a cursor-seated
/// plate hides and the next one shows afresh; a first hover has nothing to tear down.
fn fades_on_change(last: LastHover, next: LastHover) -> bool {
last != LastHover::None && last != next
}
/// The world-hover driver's memory, one `SystemParam` because the driver is at Bevy's 16-param
/// ceiling.
#[derive(bevy::ecs::system::SystemParam)]
@@ -163,6 +170,10 @@ fn drive_mouseover_tooltip(
script.set_unit("mouseover", Some(state));
*published = true;
if *last != LastHover::Unit(guid) || last_lines.as_ref() != Some(&key) {
// A late name rebuilds under the same hover with no teardown.
if fades_on_change(*last, LastHover::Unit(guid)) {
script.world_tooltip_fade();
}
script.world_tooltip_unit("mouseover");
*last = LastHover::Unit(guid);
*last_lines = Some(key);
@@ -213,7 +224,10 @@ fn drive_mouseover_tooltip(
) else {
return;
};
script.world_tooltip_gameobject(&plate, &[], None);
if fades_on_change(*last, LastHover::Corpse(guid)) {
script.world_tooltip_fade();
}
script.world_tooltip_gameobject(&plate, &[], false);
*last = LastHover::Corpse(guid);
return;
}
@@ -252,10 +266,8 @@ fn drive_mouseover_tooltip(
*trace = line;
}
}
// The cursor arm follows the pointer; the corner arm has nothing to re-seat.
if let Some((x, y)) = cursor_ui {
script.world_tooltip_move(x, y);
}
// A cursor-seated plate follows the pointer in its own update (mode 6); the corner arm
// has nothing to re-seat.
return;
}
if cursor_seated && cursor_ui.is_none() {
@@ -344,12 +356,15 @@ fn drive_mouseover_tooltip(
_ => {}
}
}
script.world_tooltip_gameobject(&template.name, &lines, cursor_ui);
if fades_on_change(*last, LastHover::Go(guid)) {
script.world_tooltip_fade();
}
script.world_tooltip_gameobject(&template.name, &lines, cursor_seated);
*last = LastHover::Go(guid);
return;
}
if !matches!(*last, LastHover::None) {
// Hover lost: arm the fade (`0x492909` → `0x530ae0`).
if fades_on_change(*last, LastHover::None) {
// Hover lost: the same teardown, with no plate after it.
script.world_tooltip_fade();
*last = LastHover::None;
}
@@ -1323,3 +1323,19 @@ fn the_feed_builds_the_pet_views_against_the_pet_and_rebuilds_them_on_its_level(
)
);
}
/// `0x492890` tears down on every change away from a non-null mouseover (`0x4928d5`), loss and
/// replacement alike, and not for a first hover or an unchanged one.
#[test]
fn every_change_away_from_a_hover_requests_the_fade() {
let (a, b) = (LastHover::Go(1), LastHover::Go(2));
assert!(fades_on_change(a, b), "one object replaced by another");
assert!(
fades_on_change(a, LastHover::Unit(2)),
"an object replaced by a unit"
);
assert!(fades_on_change(LastHover::Unit(1), LastHover::Corpse(3)));
assert!(fades_on_change(a, LastHover::None), "hover lost");
assert!(!fades_on_change(LastHover::None, a), "a first hover");
assert!(!fades_on_change(a, a), "unchanged");
}
@@ -72,9 +72,9 @@ fn a_frame_walked_after_the_tooltip_sees_this_frames_fade() {
)
.unwrap();
take_log(&s);
s.tick(0.3);
assert_eq!(take_log(&s), "shown@0.4", "this frame's ramp, not the last");
s.tick(0.3);
s.tick(1.5);
assert_eq!(take_log(&s), "shown@0.5", "this frame's ramp, not the last");
s.tick(0.5);
assert_eq!(
take_log(&s),
"hide hidden",
+194 -2
View File
@@ -331,7 +331,7 @@ fn fadeout_ramps_then_hides() {
)
.unwrap();
// Half the ramp: still shown, alpha well below 1.
s.tick(0.25);
s.tick(1.5);
s.run(r#"assert(TT5:IsShown(), "still shown mid-fade")"#)
.unwrap();
s.resolve();
@@ -366,7 +366,7 @@ fn fadeout_ramps_then_hides() {
assert_eq!(fresh_alpha, 1.0, "fresh content restored full alpha");
// A full ramp ends hidden and cleared.
s.run("TT5:FadeOut()").unwrap();
s.tick(0.6);
s.tick(2.1);
s.run(
r#"
assert(not TT5:IsShown(), "hidden at ramp end")
@@ -878,3 +878,195 @@ fn tooltip_anchor_type_round_trips_every_reachable_mode() {
s.warnings()
);
}
/// `FadeOut` (`0x530ae0`) arms `[+0x3b8]` with the 2.0 s at `0x808e7c` on every call
/// (`0x530b10`); the update (`0x530b20`) holds 255 while at least half remains (`0x7ffa24` = 0.5,
/// `0x530c0c`) and below it sets `trunc(255 × left / half)` (`0x530c18`–`0x530c2c`), then hides at
/// zero (`0x530bc8`–`0x530be9`).
#[test]
fn fadeout_holds_a_second_then_ramps_over_the_next_and_every_call_rearms() {
let mut s = script();
s.run(
r#"
FadeHost = CreateFrame("Frame", "FadeHost")
FadeTip = CreateFrame("GameTooltip", "FadeTip")
FadeTip:SetOwner(FadeHost, "ANCHOR_RIGHT")
FadeTip:SetText("Timber Wolf")
FadeTip:FadeOut()
"#,
)
.unwrap();
let alpha = |s: &UiScript| s.eval::<f32>("return FadeTip:GetAlpha()").unwrap();
let shown = |s: &UiScript| s.eval::<bool>("return FadeTip:IsShown() == 1").unwrap();
s.tick(0.5);
assert_eq!(alpha(&s), 1.0, "1.5 s left of 2: the hold");
assert!(shown(&s));
s.tick(1.0);
assert_eq!(
alpha(&s),
127.0 / 255.0,
"0.5 s left: trunc(255 × 0.5 / 1.0)"
);
assert!(shown(&s));
s.tick(0.5);
assert!(!shown(&s), "2.0 s: the fade ends in the hide");
// A second call restarts the countdown from the full duration.
s.run(
r#"
FadeTip:SetOwner(FadeHost, "ANCHOR_RIGHT")
FadeTip:SetText("Timber Wolf")
FadeTip:FadeOut()
"#,
)
.unwrap();
s.tick(1.5);
assert_eq!(alpha(&s), 127.0 / 255.0);
s.run("FadeTip:FadeOut()").unwrap();
s.tick(0.5);
assert_eq!(alpha(&s), 1.0, "re-armed: 1.5 s left, back in the hold");
s.tick(1.0);
assert!(
shown(&s),
"the first arm's end has passed, the second's has not"
);
s.tick(0.5);
assert!(!shown(&s));
assert!(s.take_errors().is_empty());
}
/// `FadeOut` on an `ANCHOR_CURSOR` plate (mode 6) hides it at once through the hide slot
/// (`0x530ae3`–`0x530aff`), which un-owns it; the shown bit is not written, so only visibility
/// drops.
#[test]
fn fadeout_on_a_cursor_anchored_tooltip_hides_at_once() {
let mut s = script();
s.run(
r#"
hid = 0
CurHost = CreateFrame("Frame", "CurHost")
CurTip = CreateFrame("GameTooltip", "CurTip")
CurTip:SetScript("OnHide", function() hid = hid + 1 end)
CurTip:SetOwner(CurHost, "ANCHOR_CURSOR")
CurTip:AddLine("Copper Ore")
CurTip:Show()
assert(CurTip:IsVisible())
hid = 0
CurTip:FadeOut()
assert(not CurTip:IsVisible(), "mode 6 hides at once")
assert(CurTip:IsShown(), "through the hide slot alone: the shown bit stays (0x530aee)")
assert(hid == 1, "OnHide fired in the call")
assert(not CurTip:IsOwned(CurHost), "the hide slot un-owns")
assert(CurTip:NumLines() == 0, "and clears")
assert(CurTip:GetAlpha() == 1, "no fade ran")
"#,
)
.unwrap();
assert!(s.take_errors().is_empty());
}
/// Only `FadeOut`, the show, the `SetOwner` core and the ctor write the fade flag `[+0x3b4]`;
/// `AddLine` (`0x531630`) and `AddDoubleLine` append through `0x5303b0` and leave it.
#[test]
fn a_line_added_mid_fade_keeps_fading() {
let mut s = script();
s.run(
r#"
LateHost = CreateFrame("Frame", "LateHost")
LateTip = CreateFrame("GameTooltip", "LateTip")
LateTip:SetOwner(LateHost, "ANCHOR_RIGHT")
LateTip:SetText("Linen Cloth")
LateTip:FadeOut()
"#,
)
.unwrap();
s.tick(1.5);
s.run(
r#"
LateTip:AddLine("Sell Price: 1c")
LateTip:AddDoubleLine("Vendor", "1c")
assert(math.abs(LateTip:GetAlpha() - 127 / 255) < 1e-6, "the appends leave the ramp, got " .. LateTip:GetAlpha())
"#,
)
.unwrap();
s.tick(0.5);
s.run(r#"assert(not LateTip:IsShown(), "the fade ends on time")"#)
.unwrap();
assert!(s.take_errors().is_empty());
}
/// `SetOwner` (`0x5310d0`) clears the shown bit (`0x531179`) and calls the tooltip's own hide
/// (`0x531183` → `0x530a60`: the core with no owner, which clears and fires `OnTooltipCleared`,
/// then the base hide's `OnHide`), and only then runs the core with the new owner, which clears
/// and fires again.
#[test]
fn set_owner_hides_the_tooltip_first() {
let mut s = script();
s.run(
r#"
log = {}
OwnA = CreateFrame("Frame", "OwnA")
OwnB = CreateFrame("Frame", "OwnB")
ReTip = CreateFrame("GameTooltip", "ReTip")
ReTip:SetScript("OnTooltipCleared", function() table.insert(log, "cleared") end)
ReTip:SetScript("OnHide", function() table.insert(log, "hide") end)
ReTip:SetOwner(OwnA, "ANCHOR_RIGHT")
ReTip:SetText("first hover")
assert(ReTip:IsShown())
log = {}
ReTip:SetOwner(OwnB, "ANCHOR_LEFT")
assert(table.concat(log, ",") == "cleared,hide,cleared", "got " .. table.concat(log, ","))
assert(not ReTip:IsShown(), "hidden until the next content call")
assert(ReTip:IsOwned(OwnB), "owned by the new owner")
assert(ReTip:GetAnchorType() == "ANCHOR_LEFT")
"#,
)
.unwrap();
assert!(s.take_errors().is_empty());
}
/// Every hide un-owns through the core with no owner, which stores mode 7 (`0x530012`), so
/// `GetAnchorType` (`0x5313e0`) answers `"ANCHOR_NONE"` until the next `SetOwner`.
#[test]
fn get_anchor_type_answers_anchor_none_after_a_hide() {
let mut s = script();
s.run(
r#"
NoneHost = CreateFrame("Frame", "NoneHost")
NoneTip = CreateFrame("GameTooltip", "NoneTip")
NoneTip:SetOwner(NoneHost, "ANCHOR_RIGHT")
NoneTip:SetText("Peacebloom")
assert(NoneTip:GetAnchorType() == "ANCHOR_RIGHT", "owned: the name round-trips")
NoneTip:Hide()
assert(NoneTip:GetAnchorType() == "ANCHOR_NONE", "got " .. NoneTip:GetAnchorType())
"#,
)
.unwrap();
assert!(s.take_errors().is_empty());
}
/// The fade's end sets full alpha (`0x530bd8`) before the hide slot (`0x530be9`), so the hide's
/// `OnTooltipCleared` reads 255, not the last step of the ramp.
#[test]
fn the_fade_end_restores_full_alpha_before_its_hide() {
let mut s = script();
s.run(
r#"
seen = nil
EndHost = CreateFrame("Frame", "EndHost")
EndTip = CreateFrame("GameTooltip", "EndTip")
EndTip:SetOwner(EndHost, "ANCHOR_RIGHT")
EndTip:SetText("Silverleaf")
EndTip:SetScript("OnTooltipCleared", function() seen = EndTip:GetAlpha() end)
EndTip:FadeOut()
"#,
)
.unwrap();
s.tick(1.5);
s.run(r#"assert(seen == nil and EndTip:GetAlpha() < 0.5)"#)
.unwrap();
s.tick(0.5);
s.run(r#"assert(not EndTip:IsShown() and seen == 1, "cleared at alpha " .. tostring(seen))"#)
.unwrap();
assert!(s.take_errors().is_empty());
}
@@ -424,8 +424,12 @@ fn ts_recipe(spell_id: u32, name: &str, group: (u32, u32, &str)) -> TradeSkillRe
fn set_trade_skill_item_indexes_visible_rows_not_raw_recipes() {
let mut s = script();
s.set_screen_size(800.0, 600.0);
s.run(r#"local tt = CreateFrame("GameTooltip", "TT")"#)
.unwrap();
// Owned, as the stock hover owns it before the setter; an unowned tooltip's show hides it.
s.run(
r#"local tt = CreateFrame("GameTooltip", "TT")
tt:SetOwner(CreateFrame("Frame", "TTOwner"), "ANCHOR_NONE")"#,
)
.unwrap();
// Two groups, two recipes each. Visible order: [1] "Bolts" header, [2] Alpha Bolt,
// [3] Beta Bolt, [4] "Cloak" header, [5] Alpha Cloak, [6] Beta Cloak.
@@ -568,6 +572,11 @@ fn set_merchant_compare_item_answers_one_or_nil_per_candidate_slot() {
r#"
CreateFrame("GameTooltip", "ShoppingTooltip1"):Hide()
CreateFrame("GameTooltip", "ShoppingTooltip2"):Hide()
-- Owned, as the stock compare owns them before the setter; an unowned tooltip's show
-- hides it.
local owner = CreateFrame("Frame", "CompareOwner")
ShoppingTooltip1:SetOwner(owner, "ANCHOR_NONE")
ShoppingTooltip2:SetOwner(owner, "ANCHOR_NONE")
"#,
)
.unwrap();
@@ -189,6 +189,7 @@ fn item_render_hides_the_unit_health_bar() {
s.set_unit(
"mouseover",
Some(UnitState {
guid: 0xF130_0000_0000_0100,
exists: true,
name: Some("Timber Wolf".into()),
health: 30,
@@ -212,6 +213,8 @@ fn item_render_hides_the_unit_health_bar() {
local tt = CreateFrame("GameTooltip", "GameTooltip"); tt:Hide()
local bar = CreateFrame("StatusBar", "GameTooltipStatusBar", tt)
bar:SetPoint("TOPLEFT", tt, "BOTTOMLEFT", 2, -1); bar:SetWidth(100); bar:SetHeight(8)
-- The stock default anchor owns the plate; an unowned one's show hides it.
tt:SetScript("OnTooltipSetDefaultAnchor", function() GameTooltip:SetOwner(Slot6, "ANCHOR_NONE") end)
"#,
)
.unwrap();
@@ -840,3 +843,46 @@ fn random_property_template_hover_shows_the_placeholder() {
"an instance's known roll replaces the placeholder — got {texts:?}"
);
}
/// An item whose template is not cached ends the builder in its own hide (`0x52b72e`–`0x52b73f`),
/// which un-owns and clears: the clear zeroes the remembered item id (`0x5300dd`), so the cache
/// callback (`0x52e530`) finds nothing to rebuild (`0x52e55e`), and the stock hover loop's
/// `IsOwned` refresh (`ContainerFrame.lua:657`) stops. The plate stays down until the next hover.
#[test]
fn an_uncached_item_hides_and_un_owns_until_the_next_hover() {
let mut s = script();
s.run(
r#"
shows = 0
UncSlot = CreateFrame("Button", "UncSlot")
UncTip = CreateFrame("GameTooltip", "UncTip")
UncTip:SetScript("OnShow", function() shows = shows + 1 end)
UncTip:SetOwner(UncSlot, "ANCHOR_RIGHT")
SetItemByIdForTest(UncTip, 4242)
assert(UncTip:NumLines() == 0, "no line for a template in flight")
assert(not UncTip:IsShown(), "the builder's own hide")
assert(not UncTip:IsOwned(UncSlot), "and its un-own: the hover loop stops")
assert(shows == 0)
"#,
)
.unwrap();
s.set_item_template(
4242,
ItemTemplateView {
name: "Copper Bar".into(),
quality: 1,
..Default::default()
},
);
s.tick(0.1);
s.run(
r#"
assert(not UncTip:IsShown(), "the template's arrival rebuilds nothing")
UncTip:SetOwner(UncSlot, "ANCHOR_RIGHT")
SetItemByIdForTest(UncTip, 4242)
assert(UncTip:IsShown() and UncTipTextLeft1:GetText() == "Copper Bar", "the next hover")
"#,
)
.unwrap();
assert!(s.take_errors().is_empty());
}
@@ -263,6 +263,7 @@ fn the_tooltip_title_is_the_pvp_name() {
s.set_unit(
"target",
Some(UnitState {
guid: 0xF130_0000_0000_0100,
exists: true,
name: Some("Defias Civilian".into()),
level: 20,
@@ -297,6 +298,7 @@ fn faction_line_and_civilian_gate() {
s.set_unit(
"target",
Some(UnitState {
guid: 0xF130_0000_0000_0101,
exists: true,
name: Some("Marshal McBride".into()),
level: 20,
@@ -325,6 +327,7 @@ fn faction_line_and_civilian_gate() {
s.set_unit(
"target",
Some(UnitState {
guid: 0xF130_0000_0000_0102,
exists: true,
name: Some("Defias Civilian".into()),
level: 20,
@@ -351,6 +354,7 @@ fn faction_line_and_civilian_gate() {
s.set_unit(
"target",
Some(UnitState {
guid: 0xF130_0000_0000_0103,
exists: true,
name: Some("Defias Civilian".into()),
level: 25,
@@ -386,6 +390,7 @@ fn level_line_variants() {
s.set_unit(
"target",
Some(UnitState {
guid: 0xF130_0000_0000_0104,
exists: true,
name: Some("Bandit".into()),
level: 32,
@@ -411,6 +416,7 @@ fn level_line_variants() {
s.set_unit(
"target",
Some(UnitState {
guid: 0xF130_0000_0000_0105,
exists: true,
name: Some("Slain Wolf".into()),
level: 3,
@@ -432,6 +438,7 @@ fn level_line_variants() {
s.set_unit(
"target",
Some(UnitState {
guid: 0xF130_0000_0000_0106,
exists: true,
name: Some("Kazzak".into()),
level: 63,
@@ -451,6 +458,7 @@ fn level_line_variants() {
.unwrap();
// A hostile unit (reaction 2 or less) 10 levels up reads "??".
let ten_up = |reaction: u8, is_player: bool| UnitState {
guid: 0xF130_0000_0000_0107,
exists: true,
name: Some("Elder".into()),
level: 70,
@@ -512,6 +520,7 @@ fn a_players_level_line_ignores_the_snapshots_creature_type() {
)
.unwrap();
let druid = |reaction: u8, creature_type: Option<&str>| UnitState {
guid: 0xF130_0000_0000_0108,
exists: true,
name: Some("Fenwick".into()),
level: 40,
@@ -624,32 +633,34 @@ fn world_hover_drive_and_health_watcher() {
s.set_unit("target", Some(as_target));
s.run(r#"assert(GameTooltipStatusBar:GetValue() == 8, "the watcher follows the guid")"#)
.unwrap();
s.tick(0.5);
s.tick(2.0);
s.run(r#"assert(not GameTooltip:IsShown(), "faded out after the ramp")"#)
.unwrap();
assert!(s.take_errors().is_empty());
}
/// The minimap blip tooltip (`minimap_tooltip`): refused without `UIParent`, otherwise one line
/// seated above the cursor, world-owned so the shared fade hides it on hover loss.
/// The minimap blip tooltip (`minimap_tooltip`): refused without `Minimap`, otherwise one line
/// seated above the cursor in mode 6, which follows the cursor and hides at once on hover loss.
#[test]
fn minimap_blip_tooltip_shows_and_fades() {
fn minimap_blip_tooltip_shows_follows_and_hides() {
let mut s = script();
s.set_screen_size(800.0, 600.0);
s.run(r#"CreateFrame("GameTooltip", "GameTooltip")"#)
.unwrap();
assert!(
!s.minimap_tooltip("Stormwind", 700.0, 500.0, false),
"no UIParent yet: the show refuses"
!s.minimap_tooltip("Stormwind", false),
"no Minimap yet: the show refuses"
);
s.run(
r#"
local up = CreateFrame("Frame", "UIParent")
up:SetPoint("BOTTOMLEFT", 0, 0); up:SetWidth(800); up:SetHeight(600)
CreateFrame("Minimap", "Minimap", up)
"#,
)
.unwrap();
assert!(s.minimap_tooltip("Stormwind", 795.0, 500.0, false));
s.mouse_move(795.0, 500.0);
assert!(s.minimap_tooltip("Stormwind", false));
s.run(
r#"
assert(GameTooltipTextLeft1:GetText() == "Stormwind")
@@ -669,13 +680,18 @@ fn minimap_blip_tooltip_shows_and_fades() {
r#"assert(GameTooltip:GetRight() <= 800, "clamped inside the screen, got " .. tostring(GameTooltip:GetRight()))"#,
)
.unwrap();
s.world_tooltip_move(400.0, 300.0);
s.run(r#"assert(GameTooltipTextLeft1:GetText() == "Stormwind", "content survives a move")"#)
.unwrap();
// Hover loss rides the shared world fade.
s.world_tooltip_fade();
s.tick(0.6);
s.run(r#"assert(not GameTooltip:IsShown(), "faded out after the ramp")"#)
s.mouse_move(400.0, 300.0);
s.resolve();
s.run(
r#"
assert(GameTooltipTextLeft1:GetText() == "Stormwind", "content survives a move")
assert(GameTooltip:GetBottom() == 300, "the plate follows the cursor")
"#,
)
.unwrap();
// Hover loss hides at once.
s.world_tooltip_hide();
s.run(r#"assert(not GameTooltip:IsShown(), "hidden with no fade")"#)
.unwrap();
assert!(s.take_errors().is_empty());
}
@@ -691,6 +707,7 @@ fn a_cursor_seated_plate_lands_on_the_cursor_under_the_ui_scale() {
r#"
local up = CreateFrame("Frame", "UIParent")
up:SetPoint("BOTTOMLEFT", 0, 0); up:SetPoint("TOPRIGHT", 0, 0)
CreateFrame("Minimap", "Minimap", up)
CreateFrame("GameTooltip", "GameTooltip", UIParent)
"#,
)
@@ -711,16 +728,219 @@ fn a_cursor_seated_plate_lands_on_the_cursor_under_the_ui_scale() {
)
.unwrap()
};
assert!(s.minimap_tooltip("Stormwind", 400.0, 300.0, false));
s.mouse_move(400.0, 300.0);
assert!(s.minimap_tooltip("Stormwind", false));
let (x, y) = at(&mut s);
assert!(
(x - 400.0).abs() < 1e-3 && (y - 300.0).abs() < 1e-3,
"({x}, {y})"
);
s.world_tooltip_move(200.0, 100.0);
s.mouse_move(200.0, 100.0);
let (x, y) = at(&mut s);
assert!(
(x - 200.0).abs() < 1e-3 && (y - 100.0).abs() < 1e-3,
"({x}, {y})"
);
}
/// The unit builder shows `<name>StatusBar` with a full show (`0x52a13c`/`0x52a146`), and the
/// tooltip clear hides it the same way (`0x53009b`/`0x5300a1`) only while the tooltip holds a unit
/// (`0x530070`), first in the clear, so its `OnHide` runs before `OnTooltipCleared`.
#[test]
fn the_health_bar_shows_and_hides_through_its_own_transitions() {
let mut s = script();
s.set_unit("target", Some(wolf()));
s.run(
r#"
log = {}
BarHost = CreateFrame("Frame", "BarHost")
BarTip = CreateFrame("GameTooltip", "BarTip")
local bar = CreateFrame("StatusBar", "BarTipStatusBar", BarTip)
bar:Hide()
bar:SetScript("OnShow", function() table.insert(log, "bar show") end)
bar:SetScript("OnHide", function() table.insert(log, "bar hide") end)
BarTip:SetScript("OnTooltipCleared", function() table.insert(log, "cleared") end)
BarTip:SetScript("OnHide", function() table.insert(log, "tip hide") end)
BarTip:SetOwner(BarHost, "ANCHOR_RIGHT")
log = {}
assert(BarTip:SetUnit("target") == 1)
assert(BarTipStatusBar:IsShown(), "the unit shows the bar")
local n = 0
for _, e in ipairs(log) do if e == "bar show" then n = n + 1 end end
assert(n == 1, "the bar's OnShow fired once: " .. table.concat(log, ","))
log = {}
BarTip:ClearLines()
assert(not BarTipStatusBar:IsShown(), "the clear hides the bar")
assert(table.concat(log, ",") == "bar hide,cleared", "got " .. table.concat(log, ","))
-- No unit held: the clear leaves the bar alone.
BarTip:SetOwner(BarHost, "ANCHOR_RIGHT")
BarTip:SetText("Peacebloom")
BarTipStatusBar:Show()
log = {}
BarTip:ClearLines()
assert(BarTipStatusBar:IsShown(), "no unit: the bar stays")
assert(table.concat(log, ",") == "cleared", "got " .. table.concat(log, ","))
-- The hide's clear runs before the base hide.
BarTip:SetOwner(BarHost, "ANCHOR_RIGHT")
BarTip:SetUnit("target")
log = {}
BarTip:Hide()
assert(table.concat(log, ",") == "bar hide,cleared,tip hide", "got " .. table.concat(log, ","))
"#,
)
.unwrap();
assert!(s.take_errors().is_empty());
}
/// The world handler's fade request is `0x530ae0`, which hides a mode-6 plate at once; a
/// cursor-seated GameObject plate is owned in mode 6 (`0x492a1e`), so hover loss hides it.
#[test]
fn a_cursor_seated_world_plate_hides_at_once_on_hover_loss() {
let mut s = script();
s.set_screen_size(800.0, 600.0);
s.run(
r#"
local up = CreateFrame("Frame", "UIParent"); up:SetAllPoints()
CreateFrame("GameTooltip", "GameTooltip")
"#,
)
.unwrap();
assert!(s.world_tooltip_gameobject("Brill", &[], true));
s.run(r#"assert(GameTooltip:GetAnchorType() == "ANCHOR_CURSOR", "seated in mode 6")"#)
.unwrap();
s.world_tooltip_fade();
s.run(r#"assert(not GameTooltip:IsVisible(), "gone at once")"#)
.unwrap();
assert!(s.take_errors().is_empty());
}
/// The minimap blip tooltip is owned by `Minimap` in mode 6 (`0x4eb19c`, `0x4eb587`), and when no blip is hovered
/// `0x4eb0c0` hides it directly (`0x4eb12b`–`0x4eb166`), with no fade.
#[test]
fn a_minimap_blip_plate_hides_at_once_on_hover_loss() {
let mut s = script();
s.set_screen_size(800.0, 600.0);
s.run(
r#"
local up = CreateFrame("Frame", "UIParent"); up:SetAllPoints()
CreateFrame("Minimap", "Minimap", up)
CreateFrame("GameTooltip", "GameTooltip")
"#,
)
.unwrap();
assert!(s.minimap_tooltip("Stormwind", false));
s.run(
r#"
assert(GameTooltip:IsShown())
assert(GameTooltip:IsOwned(Minimap), "owned by the Minimap, whose probe it is")
assert(not GameTooltip:IsOwned(UIParent))
assert(GameTooltip:GetAnchorType() == "ANCHOR_CURSOR", "seated in mode 6")
"#,
)
.unwrap();
s.world_tooltip_hide();
s.run(r#"assert(not GameTooltip:IsShown(), "gone at once")"#)
.unwrap();
assert!(s.take_errors().is_empty());
}
/// The world handler tears down on every mouseover change (`0x492909`), so moving from one
/// cursor-seated object straight to another hides the first plate (mode 6, `0x530ae3`) and shows
/// the second afresh. Each seat clears twice: the SetOwner core's clear (`0x52ffe6`), then the
/// builder's own (`0x52aa40`).
#[test]
fn a_cursor_plate_replaced_by_another_hides_then_shows_and_clears_twice_per_seat() {
let mut s = script();
s.set_screen_size(800.0, 600.0);
s.run(
r#"
log = {}
local up = CreateFrame("Frame", "UIParent"); up:SetAllPoints()
local tt = CreateFrame("GameTooltip", "GameTooltip"); tt:Hide()
tt:SetScript("OnTooltipCleared", function() table.insert(log, "cleared") end)
tt:SetScript("OnShow", function() table.insert(log, "show") end)
tt:SetScript("OnHide", function() table.insert(log, "hide") end)
"#,
)
.unwrap();
assert!(s.world_tooltip_gameobject("Brill", &[], true));
s.run(r#"assert(table.concat(log, ",") == "cleared,cleared,show", table.concat(log, ",")) log = {}"#)
.unwrap();
// The driver's change: the teardown, then the next seat.
s.world_tooltip_fade();
assert!(s.world_tooltip_gameobject("Tirisfal Glades", &[], true));
s.run(
r#"
assert(table.concat(log, ",") == "cleared,hide,cleared,cleared,show", table.concat(log, ","))
assert(GameTooltipTextLeft1:GetText() == "Tirisfal Glades")
"#,
)
.unwrap();
assert!(s.take_errors().is_empty());
}
/// A Lua content re-set of the world plate (an `UPDATE_MOUSEOVER_UNIT` handler's `SetUnit`) keeps
/// it the world plate, so hover loss still fades it; a re-own by a window's `SetOwner` does not.
#[test]
fn hover_loss_fades_a_re_set_world_plate_but_not_a_re_owned_one() {
let mut s = script();
s.set_screen_size(800.0, 600.0);
s.set_unit("mouseover", Some(wolf()));
s.run(
r#"
UIParent = CreateFrame("Frame", "UIParent"); UIParent:SetAllPoints()
local tt = CreateFrame("GameTooltip", "GameTooltip"); tt:Hide()
tt:SetScript("OnTooltipSetDefaultAnchor", function()
GameTooltip:SetOwner(UIParent, "ANCHOR_NONE")
end)
tt:RegisterEvent("UPDATE_MOUSEOVER_UNIT")
tt:SetScript("OnEvent", function() GameTooltip:SetUnit("mouseover") end)
"#,
)
.unwrap();
assert!(s.world_tooltip_unit("mouseover"));
assert!(
s.world_tooltip_up(),
"the re-set plate is still the world's"
);
s.world_tooltip_fade();
s.tick(2.0);
s.run(r#"assert(not GameTooltip:IsShown(), "faded out")"#)
.unwrap();
assert!(s.world_tooltip_unit("mouseover"));
s.run(
r#"
GameTooltip:SetOwner(CreateFrame("Button", "Bag1"), "ANCHOR_RIGHT")
GameTooltip:SetText("Bag slot")
"#,
)
.unwrap();
assert!(!s.world_tooltip_up());
s.world_tooltip_fade();
s.tick(2.0);
s.run(r#"assert(GameTooltip:IsShown() and GameTooltip:GetAlpha() == 1, "the window's tooltip stays")"#)
.unwrap();
assert!(s.take_errors().is_empty());
}
/// A unit is found by its guid (`0x52a021`); guid 0 finds no object and the builder hides through
/// its own hide (`0x52a04e`–`0x52a058`), so `SetUnit` answers nil.
#[test]
fn a_unit_with_guid_zero_is_no_unit() {
let mut s = script();
s.set_unit("target", Some(UnitState { guid: 0, ..wolf() }));
s.run(
r#"
ZeroHost = CreateFrame("Frame", "ZeroHost")
ZeroTip = CreateFrame("GameTooltip", "ZeroTip")
ZeroTip:SetOwner(ZeroHost, "ANCHOR_RIGHT")
assert(ZeroTip:SetUnit("target") == nil, "no object, no plate")
assert(not ZeroTip:IsShown() and not ZeroTip:IsOwned(ZeroHost))
"#,
)
.unwrap();
assert!(s.take_errors().is_empty());
}
+184 -62
View File
@@ -267,9 +267,9 @@ pub(super) fn write_cell(
model.touch_measure(rh);
}
/// Hides and blanks every line cell and resets the counters; the caller fires `OnTooltipCleared`
/// outside the model borrow.
pub(super) fn clear_content(model: &mut Model, h: FrameHandle) {
/// Blanks every line cell and resets the counters and the subject: the model half of the clear
/// ([`clear_silent`]).
fn clear_content(model: &mut Model, h: FrameHandle) {
let (lefts, rights) = match model.arena.frame(h).map(|f| &f.kind_state) {
Some(KindState::Tooltip(t)) => (t.left_lines.clone(), t.right_lines.clone()),
_ => return,
@@ -289,20 +289,37 @@ pub(super) fn clear_content(model: &mut Model, h: FrameHandle) {
t.min_width = 0.0;
// Clear drops the subject and unregisters its health watcher (`0x5300ad`, `0x53007c`).
t.unit_guid = None;
t.world_owned = false;
}
// New content replaces a spell render still waiting on its view.
model.spell_tooltip_waits.remove(&h);
// The `<name>StatusBar` health bar is unit content: it hides with the lines, and the next
// unit render re-shows it.
let bar = model
.arena
.frame(h)
.and_then(|f| f.name.clone())
.and_then(|n| model.arena.lookup(&format!("{n}StatusBar")));
}
/// The tooltip's `<name>StatusBar` health bar (`[+0x348]`), if it has one.
pub(super) fn status_bar(model: &Model, h: FrameHandle) -> Option<FrameHandle> {
let name = model.arena.frame(h)?.name.as_deref()?;
model.arena.lookup(&format!("{name}StatusBar"))
}
/// The tooltip clear (`0x530050`) without its last act, the `OnTooltipCleared` fire: while the
/// tooltip holds a unit (`0x530070`) the health bar hides by its own transition, shown bit then
/// hide slot (`0x53009b`/`0x5300a1`), so its `OnHide` runs; then the lines, counters and subject.
pub(super) fn clear_silent(lua: &Lua, h: FrameHandle) {
let bar = {
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
let held = tip_mut(&mut model, h).is_ok_and(|t| t.unit_guid.is_some());
held.then(|| status_bar(&model, h)).flatten()
};
if let Some(bar) = bar {
model.arena.set_shown(bar, false);
visibility::set_shown(lua, bar, false);
}
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
clear_content(&mut model, h);
}
/// The tooltip clear (`0x530050`): [`clear_silent`], then `OnTooltipCleared` (`0x530263`).
pub(super) fn clear(lua: &Lua, h: FrameHandle) {
clear_silent(lua, h);
fire_cleared(lua, h);
}
/// Fires `OnTooltipCleared`; a script error is recorded, never propagated.
@@ -318,23 +335,28 @@ pub(super) fn fire_cleared(lua: &Lua, h: FrameHandle) {
}
}
/// A content `Set*`'s tail: shown with lines, hidden without. The reference leaves an uncached
/// tooltip cleared and collapsed to nothing; this plate keeps its declared size, so it hides. It
/// takes the base transitions past the tooltip's own ([`hide_override`] would un-own it), to keep
/// the owner, which the hover loop's `IsOwned` needs.
/// A content `Set*`'s tail: the builders' show, the shown bit and the show slot (`0x52a711`/
/// `0x52a71b`, `0x52e14e`), whose override restores full alpha and disarms a fade, or, for a plate
/// with no owner or no lines, hides and un-owns it (`0x530aba`). An item builder whose template is
/// not cached ends the same way, by its own hide (`0x52b735`–`0x52b73f`), and its clear zeroes the
/// remembered item id (`0x5300dd`) that the cache callback `0x52e530` needs (`0x52e55e`), so the
/// plate stays down until the next hover.
///
/// A spell render still waiting on its view ([`Model::spell_tooltip_waits`], filled by the host
/// ahead of a hover) is the one exception: the reference reads spells synchronously and has no
/// such state. That plate takes the base hide, keeping its owner and its wait, so the view's
/// arrival renders it in place.
pub(super) fn show_or_hide_empty(lua: &Lua, h: FrameHandle) {
let lines = {
let waiting = {
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
match tip_mut(&mut model, h) {
Ok(t) => t.num_lines,
Err(_) => 0,
}
let empty = tip_mut(&mut model, h).is_ok_and(|t| t.num_lines == 0);
empty && model.spell_tooltip_waits.contains_key(&h)
};
visibility::set_shown_bit(lua, h, lines > 0);
if lines > 0 {
visibility::base_show(lua, h);
} else {
if waiting {
visibility::set_shown_bit(lua, h, false);
visibility::base_hide(lua, h);
} else {
visibility::set_shown(lua, h, true);
}
}
@@ -360,44 +382,125 @@ pub(in crate::script) fn show_override(lua: &Lua, h: FrameHandle) {
}
/// The tooltip's hide (`0x530a60`, slot `+0x84`), however the hide comes, its parent's included:
/// the SetOwner core with no owner (`0x52ffe0`), then the base hide. The core clears the content
/// and fires `OnTooltipCleared` (`0x530050`, whose last act is the fire at `0x530263`), then
/// restores full alpha (`0x52fff4`), disarms the fade (`0x530002`) and drops the owner
/// (`0x53000c`); the mode it leaves at 7 (`0x530012`) is not kept (see `GetAnchorType`). It runs
/// whether or not the tooltip is shown: only the base hide gates on visibility (`0x76ad5b`).
/// the SetOwner core with no owner ([`set_owner_core`]), then the base hide. It runs whether or
/// not the tooltip is shown: only the base hide gates on visibility (`0x76ad5b`).
pub(in crate::script) fn hide_override(lua: &Lua, h: FrameHandle) {
{
let is_tip = {
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
if tip_mut(&mut model, h).is_err() {
drop(model);
visibility::base_hide(lua, h);
return;
}
clear_content(&mut model, h);
}
fire_cleared(lua, h);
{
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
full_alpha(&mut model, h);
if let Ok(t) = tip_mut(&mut model, h) {
t.owner = None;
}
tip_mut(&mut model, h).is_ok()
};
if is_tip {
set_owner_core(lua, h, None, TooltipAnchor::None, 0.0, 0.0);
}
visibility::base_hide(lua, h);
}
/// New content cancels a running fade at full alpha; with no fade running, an alpha the addon set
/// stays, since only the paths in [`full_alpha`] reset it.
fn cancel_fade(model: &mut Model, h: FrameHandle) {
if let Ok(t) = tip_mut(model, h) {
if t.fade_left.take().is_some() {
model.arena.set_alpha(h, 1.0);
/// The SetOwner core (`0x52ffe0`), reached from the Lua `SetOwner` after its hide, from the
/// tooltip's own hide with no owner, and from the engine's mode-6 seats (`0x492a1e`, `0x4eb587`).
/// It clears and fires `OnTooltipCleared` ([`clear`], `0x52ffe6`), restores full alpha
/// (`0x52fff4`), disarms the fade (`0x530002`), stores the owner (`0x53000c`) and mode 7,
/// `ANCHOR_NONE` (`0x530012`); only an owner overwrites the mode with the caller's (`0x530031`)
/// and applies the anchor (`0x52fe90`, [`apply_anchor`]).
pub(in crate::script) fn set_owner_core(
lua: &Lua,
h: FrameHandle,
owner: Option<FrameHandle>,
anchor: TooltipAnchor,
x: f32,
y: f32,
) {
clear(lua, h);
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
full_alpha(&mut model, h);
let Ok(t) = tip_mut(&mut model, h) else {
return;
};
t.owner = owner;
t.anchor = TooltipAnchor::None;
// A re-own, or the hide's un-own, ends the world plate; a content re-set alone does not.
t.world_owned = false;
if let Some(owner) = owner {
t.anchor = anchor;
apply_anchor(&mut model, h, owner, anchor, x, y);
}
}
/// The anchor apply (`0x52fe90`, with the core's 1): `ANCHOR_PRESERVE` returns first
/// (`0x52fead`); every other mode clears all points (`0x767ed0` at `0x52fec2`); modes 0..5 then
/// set one point on the owner (`0x52ffbc`), and 6 and 7 stop there. The clear lets
/// `GameTooltip_SetDefaultAnchor` (GameTooltip.lua:76-77) set one point with no stale owner
/// anchor; mode 6 is then placed per frame by `cursor_anchor`.
fn apply_anchor(
model: &mut Model,
h: FrameHandle,
owner: FrameHandle,
anchor: TooltipAnchor,
x: f32,
y: f32,
) {
let pts = match anchor {
TooltipAnchor::Right => Some((Point::BottomLeft, Point::TopRight)),
TooltipAnchor::Left => Some((Point::BottomRight, Point::TopLeft)),
TooltipAnchor::TopRight => Some((Point::BottomRight, Point::TopRight)),
TooltipAnchor::TopLeft => Some((Point::BottomLeft, Point::TopLeft)),
TooltipAnchor::BottomRight => Some((Point::TopLeft, Point::BottomRight)),
TooltipAnchor::BottomLeft => Some((Point::TopRight, Point::BottomLeft)),
TooltipAnchor::Cursor | TooltipAnchor::None | TooltipAnchor::Preserve => None,
};
// The drop is a named retarget to no targets, not a conservative touch.
if matches!(anchor, TooltipAnchor::Cursor | TooltipAnchor::None) {
let dropped: Option<Vec<u32>> = match model.layout_inputs.get_mut(&h) {
Some(input) if !input.anchors.is_empty() => {
Some(input.anchors.drain(..).map(|a| a.relative_to).collect())
}
_ => None,
};
if let Some(old_targets) = dropped {
model.touch_layout_retarget_frame(h, &old_targets, &[]);
}
}
if let Some((own, rel)) = pts {
let owner_id = model.frame_id(owner);
let new = Anchor::new(own, owner_id, rel, x, y);
let input = model.layout_inputs.entry(h).or_default();
// No-op when unchanged, so the bag hover's per-frame SetOwner does not dirty the layout.
let same = input.anchors.len() == 1
&& crate::script::object::anchor_bits_eq(&input.anchors[0], &new);
if !same {
// A named retarget: the cached layout graph re-points this edge instead of being
// rebuilt.
let old_targets: Vec<u32> = input.anchors.iter().map(|a| a.relative_to).collect();
input.anchors = vec![new];
model.touch_layout_retarget_frame(h, &old_targets, &[owner_id]);
}
}
}
/// `SetAlpha(255)`, unconditional: the SetOwner core `0x52ffe0` does it first (`0x52fff4`), and
/// Show (`0x530a80`) and its self-hide (`0x530a60`, the core with no owner) reach it too.
/// The fade request (`0x530ae0`), from the Lua `FadeOut` (`0x531fba`) and the world mouseover's
/// change (`0x492909`). A cursor-anchored plate, mode 6, hides at once through the hide slot,
/// with no shown-bit write, and is disarmed (`0x530ae3`–`0x530af4`); any other mode arms the fade
/// with the full duration on every call (`0x530b00`–`0x530b10`), so a second call restarts it.
pub(in crate::script) fn fade_out(lua: &Lua, h: FrameHandle) {
{
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
let Ok(t) = tip_mut(&mut model, h) else {
return;
};
if t.anchor != TooltipAnchor::Cursor {
t.fade_left = Some(TOOLTIP_FADE_SECS);
return;
}
}
visibility::hide(lua, h);
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
if let Ok(t) = tip_mut(&mut model, h) {
t.fade_left = None;
}
}
/// `SetAlpha(255)` and the fade disarmed, unconditional: the SetOwner core `0x52ffe0` does both
/// (`0x52fff4`, `0x530002`), and Show (`0x530a80`) and its self-hide (`0x530a60`, the core with no
/// owner) reach them too.
fn full_alpha(model: &mut Model, h: FrameHandle) {
if let Ok(t) = tip_mut(model, h) {
t.fade_left = None;
@@ -524,9 +627,9 @@ fn text_of(v: Option<&Value>) -> String {
}
}
/// Appends one line and cancels any fade, without showing the tooltip: `AddLine` callers call
/// `Show`. The reference's line core drops a line whose sides are both empty (`0x530270`); this
/// appends it.
/// Appends one line, without showing the tooltip or touching a fade (`0x5303b0`): `AddLine`
/// callers call `Show`. The reference's line core drops a line whose sides are both empty
/// (`0x530270`); this appends it.
pub(super) fn append_line(
lua: &Lua,
this: &Table,
@@ -562,7 +665,6 @@ pub(super) fn append_line(
if let Ok(t) = tip_mut(&mut model, h) {
t.num_lines = n;
}
cancel_fade(&mut model, h);
Ok(())
}
@@ -706,9 +808,11 @@ pub(super) fn layout_tooltips(model: &mut Model) {
}
/// The tooltip's own update after its `OnUpdate` (`0x530b20`, from `0x530b9c`): an armed fade
/// counts down by the elapsed time (`0x530baa`–`0x530bc2`) and ramps the alpha; at zero the full
/// counts down by the elapsed time (`0x530baa`–`0x530bc2`). At zero or below it ends in the full
/// hide (`0x530bd8` full alpha, `0x530be9` the hide slot), whose `OnHide` runs here, in the
/// tooltip's place in the walk. Nothing gates it on shown, owner or lines.
/// tooltip's place in the walk. Otherwise the alpha is 255 while at least half the duration
/// remains (`0x7ffa24` = 0.5, `0x530c0c`), else `trunc(255 × left / half)` (`0x530c18`–`0x530c2c`,
/// `0x40a2b0` truncates). Nothing gates it on shown, owner or lines.
pub(in crate::script) fn update(lua: &Lua, h: FrameHandle, elapsed: f32) {
let done = {
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
@@ -723,16 +827,34 @@ pub(in crate::script) fn update(lua: &Lua, h: FrameHandle, elapsed: f32) {
return;
};
let left = left - elapsed;
tip.fade_left = Some(left);
if left <= 0.0 {
true
} else {
tip.fade_left = Some(left);
model.arena.set_alpha(h, left / TOOLTIP_FADE_SECS);
model.arena.set_alpha(h, fade_alpha(left));
false
}
};
// The end: shown bit cleared, then the hide slot, as the Lua `Hide` does.
// The end: full alpha (`0x530bd8`), then the shown bit cleared and the hide slot (`0x530bdf`,
// `0x530be9`), as the Lua `Hide` does, so the hide's `OnTooltipCleared` sees 255.
if done {
lua.app_data_mut::<Model>()
.expect("model app_data")
.arena
.set_alpha(h, 1.0);
set_shown(lua, h, false);
}
}
/// The fade's alpha with `left` seconds to go, in whole 255ths as the reference's `SetAlpha`
/// takes them. The product is formed in f64, where `left × 255` is exact as in the x87 original.
fn fade_alpha(left: f32) -> f32 {
let half = f64::from(TOOLTIP_FADE_SECS) * 0.5;
let left = f64::from(left);
let units = if left < half {
(left / half * 255.0).trunc()
} else {
255.0
};
(units / 255.0) as f32
}
+52 -107
View File
@@ -7,123 +7,79 @@ use crate::script::binding_abi::optional_string;
use crate::script::object::frame_handle_of;
use crate::script::region::region_handle_of;
use crate::script::Model;
use crate::widget::{KindState, TooltipAnchor, TOOLTIP_FADE_SECS, TOOLTIP_LINE_GAP, TOOLTIP_PAD};
use crate::widget::{KindState, TooltipAnchor, TOOLTIP_LINE_GAP, TOOLTIP_PAD};
use super::{
append_line, bool_arg, clear_content, fire_cleared, full_alpha, parse_line_color,
parse_line_tail, set_shown, text_of, tip_mut, with_tip, write_cell, REG_TOOLTIP_METHODS,
append_line, bool_arg, clear, parse_line_color, parse_line_tail, set_owner_core, set_shown,
text_of, tip_mut, with_tip, write_cell, REG_TOOLTIP_METHODS,
};
pub(in crate::script) fn install(lua: &Lua) -> mlua::Result<()> {
let m = lua.create_table()?;
// SetOwner(owner, anchorType [, x, y]): the binding `0x5310d0` calls the core `0x52ffe0`,
// which sets full alpha (`0x52fff4`), disarms the fade, stores the owner (`0x53000c`), mode
// and offsets, then applies the anchor (`0x52fe90`). The content clears and OnTooltipCleared
// fires, so a new owner never inherits the last hover's lines or money.
// SetOwner(owner, anchorType [, x, y]) (`0x5310d0`): first the tooltip's own hide, the shown
// bit cleared (`0x531179`) and the hide slot (`0x531183`), so a shown tooltip fires
// `OnTooltipCleared` and `OnHide` and stays hidden until a content call shows it; then the
// core with the new owner ([`set_owner_core`], `0x5313cf`), which clears and fires again.
//
// A missing, non-string or unknown anchorType is mode 0, ANCHOR_LEFT, with no error: the
// binding's local starts at 0 (`0x53120d`). The core's pre-store of 7 (`0x530012`) survives
// only a NULL owner, the un-own path (`0x530a60`) of every hide.
//
// The anchor apply returns first for ANCHOR_PRESERVE (8, `0x52fead`), then clears all points
// (`0x767ed0` at `0x52fec2`) for modes 0..7; modes 0..5 then set a point (`0x52ffbc`).
m.set(
"SetOwner",
lua.create_function(
|lua, (this, owner, anchor, x, y): (Table, Table, Value, Option<f32>, Option<f32>)| {
|lua, (this, owner, anchor, x, y): (Table, Value, Value, Option<f32>, Option<f32>)| {
let h = frame_handle_of(lua, &this)?;
set_shown(lua, h, false);
// The owner is read after the hide (`0x531189`), so a bad one raises on a
// hidden tooltip.
let owner: Table = lua.unpack(owner)?;
let owner_h = frame_handle_of(lua, &owner)?;
// The gate is `lua_isstring` (`0x6f3510`): a string or number is read, and any
// other type counts as absent rather than raising.
let anchor = optional_string(lua, &anchor);
// The warning on an unknown name is ours; the reference is silent, and Lua sees
// no difference.
let anchor = match anchor
.as_deref()
.map(str::to_ascii_uppercase)
.as_deref()
.unwrap_or("")
{
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
clear_content(&mut model, h);
full_alpha(&mut model, h);
tip_mut(&mut model, h)?.owner = Some(owner_h);
let owner_id = model.frame_id(owner_h);
// Resolved once: the placement and `GetAnchorType` both read this value. The
// warning on an unknown name is ours; the reference is silent, and Lua sees no
// difference.
let anchor = match anchor
.as_deref()
.map(str::to_ascii_uppercase)
.as_deref()
.unwrap_or("")
{
"ANCHOR_LEFT" => TooltipAnchor::Left,
"ANCHOR_RIGHT" => TooltipAnchor::Right,
"ANCHOR_BOTTOMLEFT" => TooltipAnchor::BottomLeft,
"ANCHOR_BOTTOMRIGHT" => TooltipAnchor::BottomRight,
"ANCHOR_TOPLEFT" => TooltipAnchor::TopLeft,
"ANCHOR_TOPRIGHT" => TooltipAnchor::TopRight,
"ANCHOR_CURSOR" => TooltipAnchor::Cursor,
"ANCHOR_NONE" => TooltipAnchor::None,
"ANCHOR_PRESERVE" => TooltipAnchor::Preserve,
"" => TooltipAnchor::Left,
other => {
model.record_warning(format!(
"ANCHOR_LEFT" => TooltipAnchor::Left,
"ANCHOR_RIGHT" => TooltipAnchor::Right,
"ANCHOR_BOTTOMLEFT" => TooltipAnchor::BottomLeft,
"ANCHOR_BOTTOMRIGHT" => TooltipAnchor::BottomRight,
"ANCHOR_TOPLEFT" => TooltipAnchor::TopLeft,
"ANCHOR_TOPRIGHT" => TooltipAnchor::TopRight,
"ANCHOR_CURSOR" => TooltipAnchor::Cursor,
"ANCHOR_NONE" => TooltipAnchor::None,
"ANCHOR_PRESERVE" => TooltipAnchor::Preserve,
"" => TooltipAnchor::Left,
other => {
lua.app_data_mut::<Model>()
.expect("model app_data")
.record_warning(format!(
"SetOwner: unknown anchor '{other}' (mode 0, ANCHOR_LEFT)"
));
TooltipAnchor::Left
}
};
tip_mut(&mut model, h)?.anchor = anchor;
let pts = match anchor {
TooltipAnchor::Right => Some((Point::BottomLeft, Point::TopRight)),
TooltipAnchor::Left => Some((Point::BottomRight, Point::TopLeft)),
TooltipAnchor::TopRight => Some((Point::BottomRight, Point::TopRight)),
TooltipAnchor::TopLeft => Some((Point::BottomLeft, Point::TopLeft)),
TooltipAnchor::BottomRight => Some((Point::TopLeft, Point::BottomRight)),
TooltipAnchor::BottomLeft => Some((Point::TopRight, Point::BottomLeft)),
TooltipAnchor::Cursor | TooltipAnchor::None | TooltipAnchor::Preserve => {
None
}
};
// Modes 6 and 7 clear all points and stop. The clear lets
// `GameTooltip_SetDefaultAnchor` (GameTooltip.lua:76-77) set one point with no
// stale owner anchor; mode 6 is then placed per frame by `cursor_anchor`. The
// drop is a named retarget to no targets, not a conservative touch.
if matches!(anchor, TooltipAnchor::Cursor | TooltipAnchor::None) {
let dropped = match model.layout_inputs.get_mut(&h) {
Some(input) if !input.anchors.is_empty() => {
Some(input.anchors.drain(..).map(|a| a.relative_to).collect())
}
_ => None,
};
if let Some(old_targets) = dropped {
let old_targets: Vec<u32> = old_targets;
model.touch_layout_retarget_frame(h, &old_targets, &[]);
}
TooltipAnchor::Left
}
if let Some((own, rel)) = pts {
let new =
Anchor::new(own, owner_id, rel, x.unwrap_or(0.0), y.unwrap_or(0.0));
let input = model.layout_inputs.entry(h).or_default();
// No-op when unchanged, so the bag hover's per-frame SetOwner does not
// dirty the layout.
let same = input.anchors.len() == 1
&& crate::script::object::anchor_bits_eq(&input.anchors[0], &new);
if !same {
// A named retarget: the cached layout graph re-points this edge
// instead of being rebuilt.
let old_targets: Vec<u32> =
input.anchors.iter().map(|a| a.relative_to).collect();
input.anchors = vec![new];
model.touch_layout_retarget_frame(h, &old_targets, &[owner_id]);
}
}
}
fire_cleared(lua, h);
};
set_owner_core(
lua,
h,
Some(owner_h),
anchor,
x.unwrap_or(0.0),
y.unwrap_or(0.0),
);
Ok(())
},
)?,
)?;
// GetAnchorType(): the last SetOwner's mode by name (`0x5313e0`, name table `0x531530`), never
// nil; a never-owned tooltip answers "ANCHOR_NONE". The reference also answers it after any
// hide, whose un-own leaves mode 7; this keeps the last mode. Addons compare it with what they
// passed to SetOwner, so the name must round-trip.
// GetAnchorType(): `[+0x318]` by name (`0x5313e0`, name table `0x531530`), never nil. While
// owned it is the last SetOwner's mode; a never-owned tooltip, and any tooltip since a hide
// (whose un-own stores mode 7, `0x530012`), answers "ANCHOR_NONE".
m.set(
"GetAnchorType",
lua.create_function(|lua, this: Table| {
@@ -166,11 +122,7 @@ pub(in crate::script) fn install(lua: &Lua) -> mlua::Result<()> {
_ => 1.0,
};
let wrap = bool_arg(args.get(5));
{
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
clear_content(&mut model, h);
}
fire_cleared(lua, h);
clear(lua, h);
append_line(lua, &this, (text, [r, g, b, a]), None, wrap)?;
set_shown(lua, h, true);
Ok(())
@@ -235,11 +187,7 @@ pub(in crate::script) fn install(lua: &Lua) -> mlua::Result<()> {
"ClearLines",
lua.create_function(|lua, this: Table| {
let h = frame_handle_of(lua, &this)?;
{
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
clear_content(&mut model, h);
}
fire_cleared(lua, h);
clear(lua, h);
Ok(())
})?,
)?;
@@ -316,16 +264,13 @@ pub(in crate::script) fn install(lua: &Lua) -> mlua::Result<()> {
with_tip(lua, &this, |t| t.padding = w.max(0.0))
})?,
)?;
// FadeOut(): alpha ramps to 0 over `TOOLTIP_FADE_SECS`, then the full hide; new content or
// Show cancels it.
// FadeOut() (`0x531f00`): the fade request ([`super::fade_out`]).
m.set(
"FadeOut",
lua.create_function(|lua, this: Table| {
with_tip(lua, &this, |tip| {
if tip.fade_left.is_none() {
tip.fade_left = Some(TOOLTIP_FADE_SECS);
}
})
let h = frame_handle_of(lua, &this)?;
super::fade_out(lua, h);
Ok(())
})?,
)?;
// The content methods: items, spells and auras, talents, units.
@@ -13,7 +13,7 @@
use mlua::{Lua, MultiValue, Table, Value};
use super::object::frame_handle_of;
use super::tooltip::{append_line, clear_content, fire_cleared, show_or_hide_empty};
use super::tooltip::{append_line, clear, clear_silent, show_or_hide_empty};
use super::{ItemTemplateView, Model};
mod names;
@@ -151,11 +151,7 @@ pub(super) fn render_by_id(
fb_q: Option<u32>,
) -> mlua::Result<()> {
let h = frame_handle_of(lua, this)?;
{
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
clear_content(&mut model, h);
}
fire_cleared(lua, h);
clear(lua, h);
match view_of(lua, item_id) {
Some(v) => {
render_view(lua, this, &v, BuilderFlags::default(), None)?;
@@ -185,10 +181,7 @@ fn set_quest_item_view(
Some(it) => render_by_id(lua, this, it.item_id, it.name.clone(), Some(it.quality)),
None => {
let h = frame_handle_of(lua, this)?;
{
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
clear_content(&mut model, h);
}
clear_silent(lua, h);
super::tooltip::show_or_hide_empty(lua, h);
Ok(())
}
@@ -284,11 +277,7 @@ pub(super) fn install_methods(lua: &Lua, m: &Table) -> mlua::Result<()> {
return Ok(());
};
let h = frame_handle_of(lua, &this)?;
{
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
clear_content(&mut model, h);
}
fire_cleared(lua, h);
clear(lua, h);
// The reference's format is `"%s|Hitem:%d:%d:%d:%d|h[%s]|h%s"` (`0x8549c8`), and
// `SetHyperlink` (`0x532181`) parses it into a block (p6=1): the enchant id to slot 0
// (`+0x3d0`), the roll to `+0x424` (expanded into slots 2..6 by `0x52b7bf`), the
@@ -393,11 +382,7 @@ pub(super) fn install_methods(lua: &Lua, m: &Table) -> mlua::Result<()> {
currently_equipped,
name_only,
};
{
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
clear_content(&mut model, h);
}
fire_cleared(lua, h);
clear(lua, h);
match view_of(lua, item_id) {
Some(v) => render_view(lua, &this, &v, flags, Some(&inst))?,
None => {
@@ -486,11 +471,7 @@ pub(super) fn install_methods(lua: &Lua, m: &Table) -> mlua::Result<()> {
None => return Ok(MultiValue::from_vec(vec![Value::Nil])),
}
};
{
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
clear_content(&mut model, h);
}
fire_cleared(lua, h);
clear(lua, h);
match view_of(lua, item_id) {
Some(v) => {
render_view(lua, &this, &v, BuilderFlags::default(), Some(&inst))?;
@@ -551,11 +532,7 @@ pub(super) fn install_methods(lua: &Lua, m: &Table) -> mlua::Result<()> {
let Some(item) = item.filter(|i| i.item_id != 0) else {
return Ok(());
};
{
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
clear_content(&mut model, h);
}
fire_cleared(lua, h);
clear(lua, h);
let inst = render::ItemInstance {
name: item.name.clone(),
..Default::default()
@@ -600,11 +577,7 @@ pub(super) fn install_methods(lua: &Lua, m: &Table) -> mlua::Result<()> {
let Some(row) = row else {
return Ok(());
};
{
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
clear_content(&mut model, h);
}
fire_cleared(lua, h);
clear(lua, h);
// The row's app-composed name has the suffix joined, as `0x5d8b00` does; the lines
// come from the roll id, the reference's `+0x424` (`0x52b7bf`).
let inst = render::ItemInstance {
@@ -653,11 +626,7 @@ pub(super) fn install_methods(lua: &Lua, m: &Table) -> mlua::Result<()> {
let Some(entry) = entry else {
return Ok(());
};
{
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
clear_content(&mut model, h);
}
fire_cleared(lua, h);
clear(lua, h);
let inst = render::ItemInstance {
name: entry.name.clone(),
enchants: roll_enchants(lua, entry.random_property_id),
@@ -832,11 +801,7 @@ pub(super) fn install_methods(lua: &Lua, m: &Table) -> mlua::Result<()> {
list.get(index.saturating_sub(1)).cloned()
};
let Some(row) = row else { return Ok(()) };
{
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
clear_content(&mut model, h);
}
fire_cleared(lua, h);
clear(lua, h);
match view_of(lua, row.item_id) {
Some(v) => {
render_view(lua, &this, &v, BuilderFlags::default(), None)?;
@@ -891,11 +856,7 @@ pub(super) fn install_methods(lua: &Lua, m: &Table) -> mlua::Result<()> {
if item_id == 0 {
return Ok(());
}
{
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
clear_content(&mut model, h);
}
fire_cleared(lua, h);
clear(lua, h);
// A block source (`0x5355fa`, p6=1) carrying the attachment's roll: no placeholder.
let inst = render::ItemInstance {
name,
@@ -933,11 +894,7 @@ pub(super) fn install_methods(lua: &Lua, m: &Table) -> mlua::Result<()> {
if item_id == 0 {
return Ok(());
}
{
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
clear_content(&mut model, h);
}
fire_cleared(lua, h);
clear(lua, h);
// A block source (`0x5359d9`, p6=1) carrying the listing's roll: no placeholder.
let inst = render::ItemInstance {
name,
@@ -970,11 +927,7 @@ pub(super) fn install_methods(lua: &Lua, m: &Table) -> mlua::Result<()> {
if item_id == 0 {
return Ok(());
}
{
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
clear_content(&mut model, h);
}
fire_cleared(lua, h);
clear(lua, h);
if let Some(v) = view_of(lua, item_id) {
render_view(lua, &this, &v, BuilderFlags::default(), None)?;
}
@@ -1000,11 +953,7 @@ pub(super) fn install_methods(lua: &Lua, m: &Table) -> mlua::Result<()> {
if item_id == 0 {
return Ok(());
}
{
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
clear_content(&mut model, h);
}
fire_cleared(lua, h);
clear(lua, h);
if let Some(v) = view_of(lua, item_id) {
render_view(lua, &this, &v, BuilderFlags::default(), None)?;
}
+10 -26
View File
@@ -16,7 +16,7 @@
use mlua::{Lua, Table, Value};
use super::object::{frame_handle_of, frame_wrapper};
use super::tooltip::{append_line, clear_content, fire_cleared, tip_mut};
use super::tooltip::{append_line, clear, clear_silent, tip_mut};
use super::{CraftTooltip, Model, TrainerTooltip};
use crate::widget::FrameHandle;
@@ -223,9 +223,9 @@ fn answer_wait(lua: &Lua, h: FrameHandle, spell_id: u32, alt_caster: bool) -> ml
model.spell_tooltip_waits.remove(&h);
return Ok(());
}
clear_content(&mut model, h);
(model.frame_id(h), wait)
};
clear_silent(lua, h);
let this = frame_wrapper(lua, id)?;
fill_spell(
lua,
@@ -436,11 +436,7 @@ pub(super) fn set_spell_with_talent(
talent: TalentLines,
) -> mlua::Result<()> {
let h = frame_handle_of(lua, this)?;
{
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
clear_content(&mut model, h);
}
fire_cleared(lua, h);
clear(lua, h);
if talent.next_desc.is_none() {
super::talent::ask_next_rank(lua, talent.next_spell);
}
@@ -474,10 +470,7 @@ fn set_reward_spell(
Some((id, name)) => set_spell_by_id(lua, this, id, name, SpellRenderOpts::default(), None),
None => {
let h = frame_handle_of(lua, this)?;
{
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
clear_content(&mut model, h);
}
clear_silent(lua, h);
super::tooltip::show_or_hide_empty(lua, h);
Ok(())
}
@@ -493,11 +486,7 @@ pub(super) fn set_spell_by_id(
remaining: Option<String>,
) -> mlua::Result<()> {
let h = frame_handle_of(lua, this)?;
{
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
clear_content(&mut model, h);
}
fire_cleared(lua, h);
clear(lua, h);
fill_spell(lua, this, h, spell_id, fallback_name, opts, remaining)
}
@@ -721,11 +710,7 @@ pub(super) fn install_methods(lua: &Lua, m: &Table) -> mlua::Result<()> {
};
let (spell_id, fallback_name) = hit.unwrap_or((0, None));
let h = frame_handle_of(lua, &this)?;
{
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
clear_content(&mut model, h);
}
fire_cleared(lua, h);
clear(lua, h);
match spell_view_of(lua, spell_id, false) {
Some(v) => {
append_line(lua, &this, (v.name.clone(), GOLD), None, false)?;
@@ -806,14 +791,13 @@ pub(super) fn install_methods(lua: &Lua, m: &Table) -> mlua::Result<()> {
0x80 => super::tooltip_item::render_by_id(lua, &this, a.action, None, None),
0x40 => {
let h = frame_handle_of(lua, &this)?;
clear(lua, h);
// The same 1..36 lookup as `GetActionText` and the bar icon, so the three
// agree on a slot's macro.
let name = {
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
clear_content(&mut model, h);
// The same 1..36 lookup as `GetActionText` and the bar icon, so the
// three agree on a slot's macro.
let model = lua.app_data_ref::<Model>().expect("model app_data");
model.macros.get(a.action as usize).map(|m| m.name.clone())
};
fire_cleared(lua, h);
if let Some(name) = name {
append_line(lua, &this, (name, WHITE), None, false)?;
}
+93 -206
View File
@@ -7,12 +7,11 @@
use mlua::{Lua, Table};
use super::object::frame_handle_of;
use super::tooltip::{append_line, clear_content, fire_cleared, show_or_hide_empty, tip_mut};
use super::tooltip::{append_line, clear, set_owner_core, show_or_hide_empty};
use super::unit::{is_civilian_kill, level_reads_unknown, unknownobject};
use super::{KindState, Model, UnitState};
use crate::layout::{Anchor, Point};
use crate::strings::{fill, Arg};
use crate::widget::{FrameHandle, TOOLTIP_FADE_SECS};
use crate::widget::{FrameHandle, TooltipAnchor};
const WHITE: [f32; 4] = [1.0, 1.0, 1.0, 1.0];
const GREEN: [f32; 4] = [0.0, 1.0, 0.0, 1.0];
@@ -98,26 +97,32 @@ fn level_line(lua: &Lua, u: &UnitState, player_level: u32) -> Option<String> {
Some(fill(&get(key)?, &args))
}
/// Render the unit tooltip for `token`'s current snapshot; returns whether the unit existed.
/// Render the unit tooltip for `token`'s current snapshot (`0x529fe0`); returns whether the unit
/// existed. The clear first (`0x52a003`); no unit hides the tooltip through its own hide, shown bit
/// then slot (`0x52a050`/`0x52a058`). A unit is held (`0x52a070`), its bar seeded and shown
/// (`0x52a0e0`–`0x52a146`) before any line, and the lines end in the show (`0x52a711`/`0x52a71b`).
fn render_unit(lua: &Lua, this: &Table, token: &str) -> mlua::Result<bool> {
let h = frame_handle_of(lua, this)?;
// A unit is its object, found by guid (`0x52a021`); guid 0 finds none.
let (unit, player_level) = {
let model = lua.app_data_mut::<Model>().expect("model app_data");
(
model.unit(token).cloned().filter(|u| u.exists),
model
.unit(token)
.cloned()
.filter(|u| u.exists && u.guid != 0),
model.player_req.level,
)
};
{
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
clear_content(&mut model, h);
}
fire_cleared(lua, h);
clear(lua, h);
let Some(u) = unit else {
set_subject(lua, h, None);
show_or_hide_empty(lua, h);
super::visibility::set_shown(lua, h, false);
return Ok(false);
};
set_subject(lua, h, Some(u.guid));
if let Some(bar) = seed_bar(lua, h, &u) {
super::visibility::set_shown(lua, bar, true);
}
// The name: `CGUnit_C::GetUnitName` `0x609210` (`0x52a187`), whose every miss falls to
// `UNKNOWNOBJECT`, as `UnitName`'s does, then decorated by `0x609370` with the flag a literal
// `1` (`0x52a19b`), as `UnitPVPName` does, so no CVar gates it: a ranked player's rank and
@@ -152,9 +157,6 @@ fn render_unit(lua: &Lua, this: &Table, token: &str) -> mlua::Result<bool> {
if u.racial_leader && u.pvp {
append_line(lua, this, ("Leader".into(), WHITE), None, false)?;
}
// An unresolved guid (0) is no subject: nothing can push for it.
set_subject(lua, h, Some(u.guid).filter(|&g| g != 0));
update_bar(lua, h, Some(&u));
show_or_hide_empty(lua, h);
Ok(true)
}
@@ -179,36 +181,27 @@ fn set_subject(lua: &Lua, h: FrameHandle, guid: Option<u64>) {
}
}
/// Drive the `<name>StatusBar` child from a unit snapshot (`None` hides it); FrameXML's
/// `HealthBar_OnValueChanged` colours the fill.
fn update_bar(lua: &Lua, h: FrameHandle, unit: Option<&UnitState>) {
/// Seed the `<name>StatusBar` child from a unit snapshot, as the builder (`0x52a0eb` min/max,
/// `0x52a12e` value) and the health watcher (`0x529560`) do, firing `OnValueChanged` on a change;
/// FrameXML's `HealthBar_OnValueChanged` colours the fill. Neither shows it. Returns the bar.
fn seed_bar(lua: &Lua, h: FrameHandle, u: &UnitState) -> Option<FrameHandle> {
let bar = {
let model = lua.app_data_mut::<Model>().expect("model app_data");
let name = model.arena.frame(h).and_then(|f| f.name.clone());
name.and_then(|n| model.arena.lookup(&format!("{n}StatusBar")))
let model = lua.app_data_ref::<Model>().expect("model app_data");
super::tooltip::status_bar(&model, h)?
};
let Some(bar) = bar else { return };
let (changed, shown) = {
let changed = {
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
let Some(f) = model.arena.frame_mut(bar) else {
return;
let Some(KindState::StatusBar(sb)) = model.arena.frame_mut(bar).map(|f| &mut f.kind_state)
else {
return None;
};
match (&mut f.kind_state, unit) {
(KindState::StatusBar(sb), Some(u)) => {
sb.min = 0.0;
sb.max = u.max_health.max(1) as f32;
let v = u.health.min(u.max_health.max(1)) as f32;
let changed = (sb.value - v).abs() > f32::EPSILON;
sb.value = v;
(changed, true)
}
_ => (false, false),
}
sb.min = 0.0;
sb.max = u.max_health.max(1) as f32;
let v = u.health.min(u.max_health.max(1)) as f32;
let changed = (sb.value - v).abs() > f32::EPSILON;
sb.value = v;
changed
};
{
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
model.arena.set_shown(bar, shown);
}
if changed {
let (id, value) = {
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
@@ -229,6 +222,7 @@ fn update_bar(lua: &Lua, h: FrameHandle, unit: Option<&UnitState>) {
.record_lua_error(&e);
}
}
Some(bar)
}
/// A `set_unit` push of the unit a tooltip shows updates its health bar without rebuilding the
@@ -261,16 +255,16 @@ pub(super) fn on_unit_push(lua: &Lua, token: &str) {
(unit.clone(), hits)
};
for h in hits {
update_bar(lua, h, Some(&unit));
seed_bar(lua, h, &unit);
}
}
impl super::UiScript {
/// Show the world-mouseover unit tooltip: fire `OnTooltipSetDefaultAnchor`, render `token`,
/// fire `UPDATE_MOUSEOVER_UNIT`. Call on a hover-target change; a re-show mid-fade returns to
/// full alpha. `false` when there is no tooltip frame or no such unit.
/// fire `UPDATE_MOUSEOVER_UNIT`. Call on a hover-target change; the render's show returns a
/// fading plate to full alpha. `false` when there is no tooltip frame or no such unit.
pub fn world_tooltip_unit(&mut self, token: &str) -> bool {
let h = {
let (h, id) = {
let mut model = self.model_mut();
let Some(h) = model.arena.lookup("GameTooltip") else {
return false;
@@ -278,7 +272,6 @@ impl super::UiScript {
let id = model.frame_id(h);
(h, id)
};
let (h, id) = h;
if let Err(e) =
super::event::fire_widget_handler(&self.lua, id, "OnTooltipSetDefaultAnchor", vec![])
{
@@ -300,15 +293,6 @@ impl super::UiScript {
};
if shown {
set_world_owned(&self.lua, h);
let mut model = self.model_mut();
if let Some(f) = model.arena.frame_mut(h) {
if let KindState::Tooltip(t) = &mut f.kind_state {
if t.fade_left.take().is_some() {
model.arena.set_alpha(h, 1.0);
}
}
}
drop(model);
self.fire_event("UPDATE_MOUSEOVER_UNIT", vec![]);
}
shown
@@ -316,17 +300,17 @@ impl super::UiScript {
/// Show the world-mouseover GameObject tooltip (builder `0x52aa20`): the name in gold, then
/// the caller's lock lines. The anchor forks on the object's `[vtbl+0x5c]`: true (`0x492a01`)
/// seats it at the cursor, anchor state 6 (`0x52ffe0(owner, 6, 0, 0)`); false (`0x492a42`)
/// keeps the default corner, as units (`0x492983`) always do. `cursor` is that verdict, and
/// [`Self::world_tooltip_move`] follows a cursor-seated plate. Hover loss arms the unit flow's
/// fade, where the reference's loss path (`0x530ae0`) hides an anchor-state-6 plate at once.
/// owns it by `UIParent` in mode 6, `ANCHOR_CURSOR` (`0x52ffe0(UIParent, 6, 0, 0)` at
/// `0x492a1e`), which the tooltip's own update then keeps on the cursor; false (`0x492a42`)
/// fires `OnTooltipSetDefaultAnchor` for the default corner, as units (`0x492983`) always do.
/// `cursor` is that verdict.
pub fn world_tooltip_gameobject(
&mut self,
name: &str,
lines: &[(String, TooltipTint)],
cursor: Option<(f32, f32)>,
cursor: bool,
) -> bool {
let (h, id, root, root_id) = {
let (h, id, root) = {
let mut model = self.model_mut();
let Some(h) = model.arena.lookup("GameTooltip") else {
return false;
@@ -334,42 +318,14 @@ impl super::UiScript {
let Some(root) = model.arena.lookup("UIParent") else {
return false;
};
let (id, root_id) = (model.frame_id(h), model.frame_id(root));
(h, id, root, root_id)
(h, model.frame_id(h), root)
};
match cursor {
// The cursor arm: centred above the pointer; compare-then-touch, so a still pointer
// never re-layouts.
Some((ui_x, ui_y)) => {
let mut model = self.model_mut();
// The owner: this arm is the SetOwner core (`0x52ffe0`), which stores it at
// `+0x314` (`0x53000c`); the other arms get theirs from FrameXML's
// `OnTooltipSetDefaultAnchor`. Without one, any Lua `GameTooltip:Show()` hides the
// plate: `0x530a80` shows only when `+0x314` and `+0x31c` are both set, else it
// takes `0x530a60`.
if let Ok(t) = tip_mut(&mut model, h) {
t.owner = Some(root);
}
let new = cursor_anchor_at(&model, h, root_id, ui_x, ui_y);
let input = model.layout_inputs.entry(h).or_default();
let same = input.anchors.len() == 1
&& super::object::anchor_bits_eq(&input.anchors[0], &new);
if !same {
input.anchors = vec![new];
model.touch_layout();
}
}
// The corner arm: the default-anchor handler the unit flow fires too.
None => {
if let Err(e) = super::event::fire_widget_handler(
&self.lua,
id,
"OnTooltipSetDefaultAnchor",
vec![],
) {
self.push_error(e);
}
}
if cursor {
set_owner_core(&self.lua, h, Some(root), TooltipAnchor::Cursor, 0.0, 0.0);
} else if let Err(e) =
super::event::fire_widget_handler(&self.lua, id, "OnTooltipSetDefaultAnchor", vec![])
{
self.push_error(e);
}
let wrapper = match super::object::frame_wrapper(&self.lua, id) {
Ok(w) => w,
@@ -379,11 +335,7 @@ impl super::UiScript {
}
};
let render = || -> mlua::Result<()> {
{
let mut model = self.lua.app_data_mut::<Model>().expect("model app_data");
clear_content(&mut model, h);
}
fire_cleared(&self.lua, h);
clear(&self.lua, h);
append_line(&self.lua, &wrapper, (name.to_string(), GOLD), None, false)?;
for (text, tint) in lines {
let colour = match tint {
@@ -400,35 +352,25 @@ impl super::UiScript {
return false;
}
set_world_owned(&self.lua, h);
{
let mut model = self.model_mut();
if let Some(f) = model.arena.frame_mut(h) {
if let KindState::Tooltip(t) = &mut f.kind_state {
if t.fade_left.take().is_some() {
model.arena.set_alpha(h, 1.0);
}
}
}
}
show_or_hide_empty(&self.lua, h);
true
}
/// Show the minimap blip tooltip (an `AreaPOI` or quest-dot name): one gold line, faint gold
/// for a cross-interior dot, its bottom at the given UI point above the cursor.
/// [`Self::world_tooltip_move`] follows the pointer, and hover loss arms
/// [`Self::world_tooltip_fade`].
pub fn minimap_tooltip(&mut self, text: &str, ui_x: f32, ui_y: f32, grey: bool) -> bool {
let (h, id, root_id) = {
/// Show the minimap blip tooltip (an `AreaPOI` or quest-dot name), from `Minimap`'s own hover
/// probe `0x4eb0c0`: owned by `Minimap` itself (`0x4eb19c` keeps `this + 0x24`, pushed at
/// `0x4eb57d`) in mode 6, `ANCHOR_CURSOR` (`0x4eb587`), so the tooltip's own update keeps it on
/// the cursor; one gold line, faint gold for a cross-interior dot; then the show
/// (`0x4eb85f`/`0x4eb869`). Hover loss is [`Self::world_tooltip_hide`].
pub fn minimap_tooltip(&mut self, text: &str, grey: bool) -> bool {
let (h, id, root) = {
let mut model = self.model_mut();
let Some(h) = model.arena.lookup("GameTooltip") else {
return false;
};
let Some(root) = model.arena.lookup("UIParent") else {
let Some(root) = model.arena.lookup("Minimap") else {
return false;
};
let (id, root_id) = (model.frame_id(h), model.frame_id(root));
(h, id, root_id)
(h, model.frame_id(h), root)
};
let wrapper = match super::object::frame_wrapper(&self.lua, id) {
Ok(w) => w,
@@ -437,22 +379,7 @@ impl super::UiScript {
return false;
}
};
{
let mut model = self.model_mut();
clear_content(&mut model, h);
// Anchor only: the frame's own clamp flag (`Frame::clamped_to_screen`, G flags bit 4)
// slides a plate near the window edge back on screen.
let new = cursor_anchor_at(&model, h, root_id, ui_x, ui_y);
let input = model.layout_inputs.entry(h).or_default();
// Compare-then-touch, so a still cursor does not dirty the layout.
let same =
input.anchors.len() == 1 && super::object::anchor_bits_eq(&input.anchors[0], &new);
if !same {
input.anchors = vec![new];
model.touch_layout();
}
}
fire_cleared(&self.lua, h);
set_owner_core(&self.lua, h, Some(root), TooltipAnchor::Cursor, 0.0, 0.0);
// A cross-interior entry's `|cffb0b0b0` wrap modulates the gold: faint gold, not grey.
let dim = 0xb0 as f32 / 255.0;
let color = if grey {
@@ -465,76 +392,50 @@ impl super::UiScript {
return false;
}
set_world_owned(&self.lua, h);
{
let mut model = self.model_mut();
if let Some(f) = model.arena.frame_mut(h) {
if let KindState::Tooltip(t) = &mut f.kind_state {
if t.fade_left.take().is_some() {
model.arena.set_alpha(h, 1.0);
}
}
}
}
show_or_hide_empty(&self.lua, h);
true
}
/// Re-seat the shown world-owned tooltip at a new pointer point, anchor only; else a no-op.
pub fn world_tooltip_move(&mut self, ui_x: f32, ui_y: f32) {
let mut model = self.model_mut();
let Some(h) = model.arena.lookup("GameTooltip") else {
return;
};
let Some(root) = model.arena.lookup("UIParent") else {
return;
};
let owned_shown = model
.arena
.frame(h)
.map(|f| matches!(&f.kind_state, KindState::Tooltip(t) if t.world_owned) && f.shown)
.unwrap_or(false);
if !owned_shown {
return;
}
let root_id = model.frame_id(root);
let new = cursor_anchor_at(&model, h, root_id, ui_x, ui_y);
let input = model.layout_inputs.entry(h).or_default();
// Compare-then-touch: this runs on every pointer event.
let same =
input.anchors.len() == 1 && super::object::anchor_bits_eq(&input.anchors[0], &new);
if !same {
input.anchors = vec![new];
model.touch_layout();
}
/// Whether `GameTooltip` is still the shown plate a world hover put up, with no `SetOwner` or
/// hide since; a Lua content re-set (an `UPDATE_MOUSEOVER_UNIT` handler's `SetUnit`) keeps it.
pub fn world_tooltip_up(&mut self) -> bool {
self.world_plate().is_some()
}
/// Whether `GameTooltip` is still the shown plate a world hover put up, with no Lua
/// `SetOwner`, content `Set*` or `Hide` since.
pub fn world_tooltip_up(&mut self) -> bool {
/// The world plate, when [`Self::world_tooltip_up`].
fn world_plate(&mut self) -> Option<FrameHandle> {
let model = self.model_mut();
let Some(h) = model.arena.lookup("GameTooltip") else {
return false;
};
let h = model.arena.lookup("GameTooltip")?;
model
.arena
.frame(h)
.map(|f| matches!(&f.kind_state, KindState::Tooltip(t) if t.world_owned) && f.shown)
.unwrap_or(false)
.is_some_and(|f| {
matches!(&f.kind_state, KindState::Tooltip(t) if t.world_owned) && f.shown
})
.then_some(h)
}
/// On hover loss, arm the world tooltip's timed fade (the reference's unit plate fades rather
/// than hiding); a no-op unless a world-owned plate is shown.
/// On world hover loss, the fade request (`0x492909` → `0x530ae0`): a cursor-seated plate hides
/// at once, any other fades. The reference requests it whenever the previous mouseover goes,
/// whoever holds the tooltip. Moving onto a window, its hover walk runs the WorldFrame's leave,
/// which publishes the empty mouseover and fades (`0x483b10` → `0x483b46`), before the window's
/// enter takes the tooltip (`0x766229` before `0x766239`). The host's world drive runs in the
/// feed phase, before the UI input pass, so it learns of the loss only after the window's enter
/// has run. So this acts only while the world plate is up ([`Self::world_tooltip_up`]): a
/// tooltip re-owned since, by a window or by an addon while the pointer stays on the world, is
/// left alone where the reference would fade it, and a hidden one is not touched.
pub fn world_tooltip_fade(&mut self) {
let mut model = self.model_mut();
let Some(h) = model.arena.lookup("GameTooltip") else {
return;
};
if let Some(f) = model.arena.frame_mut(h) {
if let KindState::Tooltip(t) = &mut f.kind_state {
if t.world_owned && f.shown && t.fade_left.is_none() {
t.fade_left = Some(TOOLTIP_FADE_SECS);
}
}
if let Some(h) = self.world_plate() {
super::tooltip::fade_out(&self.lua, h);
}
}
/// On minimap blip hover loss, the direct hide (`0x4eb0c0`: shown bit, then the hide slot,
/// `0x4eb15e`–`0x4eb166`), with no fade. The reference hides whoever holds the tooltip; as in
/// [`Self::world_tooltip_fade`], this acts only while the world plate is still up.
pub fn world_tooltip_hide(&mut self) {
if let Some(h) = self.world_plate() {
super::visibility::set_shown(&self.lua, h, false);
}
}
}
@@ -558,17 +459,3 @@ pub(super) fn install_methods(lua: &Lua, m: &Table) -> mlua::Result<()> {
)?;
Ok(())
}
/// The plate's BOTTOM on the cursor: `(ui_x, ui_y)` is in the screen root's units, and an anchor
/// offset is in the tooltip's own, so it is divided by the tooltip's effective scale, the UI scale
/// included, as the cursor arm `0x530b20` does ([`super::tooltip`]'s `cursor_anchor`).
fn cursor_anchor_at(model: &Model, h: FrameHandle, root_id: u32, ui_x: f32, ui_y: f32) -> Anchor {
let s = super::object::eff_scale(model, h);
Anchor::new(
Point::Bottom,
root_id,
Point::BottomLeft,
ui_x / s,
ui_y / s,
)
}
+10 -5
View File
@@ -242,19 +242,23 @@ pub struct TooltipState {
pub right_lines: Vec<RegionHandle>,
/// `SetOwner`'s frame, dropped on hide.
pub owner: Option<FrameHandle>,
/// The last `SetOwner`'s mode, read back by `GetAnchorType()`.
/// The anchor mode (`+0x318`), read back by `GetAnchorType()`: the last `SetOwner`'s while
/// owned, `None` (mode 7) since a hide.
pub anchor: TooltipAnchor,
/// `SetMinimumWidth`: a floor on the auto-sized width, cleared with the content.
pub min_width: f32,
/// `FadeOut()`'s countdown in seconds (`[+0x3b4]` armed, `[+0x3b8]` left): the tooltip's own
/// update counts it down by its elapsed, so it stands still while the tooltip is not walked.
/// Any fresh content or `Show` cancels it.
/// `FadeOut` arms it; the show, the SetOwner core (so every hide) and a cursor-mode `FadeOut`
/// disarm it, and nothing else.
pub fade_left: Option<f32>,
/// The guid of the unit this tooltip shows (`+0x368`/`+0x36c`, stored at `0x52a070`), whose
/// health watcher (`0x467e70`, registered at `0x52a0a8`) drives the bar; a `set_unit` push of
/// that unit, under any token, re-drives it.
pub unit_guid: Option<u64>,
/// Shows world-hover content, which fades when the hover is lost; a window hover never fades.
/// The plate a world hover put up, set after its build and cleared by the next SetOwner core
/// (a Lua `SetOwner`, an engine seat, or any hide's un-own), not by a content re-set. Hover loss
/// fades only such a plate.
pub world_owned: bool,
/// Armed for a compare render: the next `SetInventoryItem` prepends "Currently Equipped"
/// (`[arg+0x18]`, not the compact `[arg+0x14]`), surviving the stock caller's `SetOwner` in
@@ -272,8 +276,9 @@ pub const TOOLTIP_PAD: f32 = 10.0;
pub const TOOLTIP_LINE_GAP: f32 = 2.0;
/// The least double-line column gap, the stock `TextRightN` offset; the reference's is untraced.
pub const TOOLTIP_DOUBLE_GAP: f32 = 40.0;
/// `FadeOut`'s ramp length in seconds; the reference's constant is untraced.
pub const TOOLTIP_FADE_SECS: f32 = 0.5;
/// `FadeOut`'s duration in seconds, `[0x808e7c]` = 2.0: the plate holds full alpha for the first
/// half and ramps over the second.
pub const TOOLTIP_FADE_SECS: f32 = 2.0;
/// A wrap-flagged line's width, pinned when it is appended; the reference's column is untraced.
pub const TOOLTIP_WRAP_WIDTH: f32 = 260.0;