the mailbox takes typing again, and events carry their real arguments
- the mail window's send tab takes typing again: the subject, body and money boxes were taking no input while the recipient box swallowed every key, and a sent letter clears its subject and item icon instead of putting them straight back (2145) - seven events fire the way the reference does: UNIT_DYNAMIC_FLAGS fires at all, swapping two identical stacks between bank slots fires PLAYERBANKSLOTS_CHANGED twice, the pet sheet's training points repaint, and ITEM_LOCK_CHANGED loses its invented arguments (2140) - nine widget methods 1.12 addons probe for are in: ShaguTweaks stops recolouring textures the reference leaves alone, pfUI and _Nameplates stop raising on a tooltip's GetAnchorType, and 63 AceEvent addons get RegisterAllEvents (2142) - six post-1.12 methods leave the widget surface (SetSize, SetShown, HasFocus, SetFormattedText and two tooltip verbs), so an addon reaching for a later client's API finds what a real 1.12 client gives it (2142, 2146) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -351,7 +351,7 @@ fn charter_tooltip_lines(script: &UiScript, pos: Option<(i64, u32)>) -> Vec<Stri
|
||||
local a = getglobal("BenillaProbeTipAnchor")
|
||||
if not a then
|
||||
a = CreateFrame("Button", "BenillaProbeTipAnchor")
|
||||
a:SetPoint("CENTER", 0, 0); a:SetSize(10, 10)
|
||||
a:SetPoint("CENTER", 0, 0); a:SetWidth(10); a:SetHeight(10)
|
||||
end
|
||||
GameTooltip:SetOwner(a, "ANCHOR_RIGHT")
|
||||
GameTooltip:SetBagItem({bag}, {slot})
|
||||
|
||||
@@ -344,7 +344,29 @@ fn feed_death(
|
||||
if death_net.spirit_healer.is_some() && memo.confirm_generation != death_net.confirm_generation
|
||||
{
|
||||
memo.confirm_generation = death_net.confirm_generation;
|
||||
script.fire_event("CONFIRM_XP_LOSS", vec![]);
|
||||
// **`arg1` is the XP the resurrection will cost** — byte-read at the fire site
|
||||
// (`0x5df837`, `SignalEvent2(396, "%d", …)`), because the argument gate (2140) found this
|
||||
// fired argless and the shapes table says one number:
|
||||
//
|
||||
// ```
|
||||
// 5df80f mov ecx,[ebx+0xe68] ; the player's descriptor window
|
||||
// 5df815 mov edx,[ecx+0x844] ; +0x844 -> field 188 + 529 = PLAYER_NEXT_LEVEL_XP
|
||||
// 5df81e fild [ebp+8]
|
||||
// 5df821 fmul ds:0x80ae90 ; 0.05f
|
||||
// 5df827 call 0x40a2b0 ; _ftol — TRUNCATES, not rounds
|
||||
// ```
|
||||
//
|
||||
// and `0x5df806`'s arm pushes **0** when the object is not the local player, which is why
|
||||
// an absent field answers 0 here rather than suppressing the event.
|
||||
//
|
||||
// Nothing in 1.12 FrameXML reads it — `UIParent.lua:399` asks `GetResSicknessDuration()`
|
||||
// instead — so this is fidelity for the callers that are not ours, exactly 1776's reason
|
||||
// for settling `PLAYERBANKSLOTS_CHANGED`'s shape when nothing read that either.
|
||||
let xp_cost = store
|
||||
.0
|
||||
.player_next_level_xp()
|
||||
.map_or(0, |next| (f64::from(next) * 0.05) as i64);
|
||||
script.fire_event("CONFIRM_XP_LOSS", vec![ScriptValue::Int(xp_cost)]);
|
||||
}
|
||||
|
||||
// ── The corpse-run range gate (0308 §5): fires the reference's range events on the edges. ──
|
||||
|
||||
@@ -111,7 +111,7 @@ impl Plugin for HoverLogPlugin {
|
||||
fn tooltip_context(script: &UiScript) -> Option<(String, i64, String)> {
|
||||
let chunk = r#"
|
||||
if not GameTooltip or not GameTooltip:IsShown() then return "" end
|
||||
local owner = GameTooltip:GetOwner()
|
||||
local owner = GameTooltip:BenillaGetTooltipOwner()
|
||||
local name = "(no owner)"
|
||||
if owner and owner.GetName and owner:GetName() then name = owner:GetName() end
|
||||
local first = ""
|
||||
|
||||
@@ -91,6 +91,13 @@ struct CharFeedMemo {
|
||||
last_stats: Option<UnitCombatStats>,
|
||||
last_inv: Option<InventorySlots>,
|
||||
last_bank_bags: Option<BankBagSlots>,
|
||||
/// The item guid **of the bag itself** in each of the six bank bag slots (absolute player
|
||||
/// slots 63..68). `PLAYERBANKSLOTS_CHANGED`'s producer discriminator for this band — the same
|
||||
/// question `ui_items::feed`'s `SlotGuids::vault` answers for the vault, and for the same
|
||||
/// reason: the pushed view cannot tell two instances of one bag model apart, so a swap of two
|
||||
/// identical bags between two slots reads as no change and the reference fires twice
|
||||
/// (decision 2140).
|
||||
last_bank_bag_guids: [u64; BANK_BAG_SLOT_COUNT],
|
||||
/// The gate's counter memories (1439) — the stores whose lazy resolves poison `is_changed`
|
||||
/// for this feed (the equipment templates' ask-once, the enchant-name creator lookups).
|
||||
items_objects: gate::Watch,
|
||||
@@ -1186,15 +1193,26 @@ pub(crate) fn feed_char(
|
||||
// One transition for both bands: they are the same descriptor read at two offsets, and
|
||||
// `UNIT_INVENTORY_CHANGED` is the one repaint signal either has. Pushing them separately
|
||||
// would fire it twice for a single wire update.
|
||||
// `PLAYERBANKSLOTS_CHANGED`'s two producers, planned off the OLD memo before the push
|
||||
// replaces it. `false` = P1, the player-descriptor path (`0x5ddd6e`, no arguments — a bag
|
||||
// arriving, leaving or exchanged); `true` = P2, the item-object path (`0x4c728d`,
|
||||
// `arg1 = "player"` — the same bag, its own fields changed). The full carve and why the guid
|
||||
// is the only sound discriminator are at `ui_items::feed`'s vault twin; decision 2140.
|
||||
let bank_bag_guids: [u64; BANK_BAG_SLOT_COUNT] =
|
||||
std::array::from_fn(|i| store.0.player_bank_bag_slot(i as u8).unwrap_or(0));
|
||||
let repainted: Vec<bool> = (0..BANK_BAG_SLOT_COUNT)
|
||||
.filter_map(|i| {
|
||||
if bank_bag_guids[i] != memo.last_bank_bag_guids[i] {
|
||||
Some(false)
|
||||
} else {
|
||||
let was = memo.last_bank_bags.as_ref().and_then(|b| b[i].as_ref());
|
||||
(!same_item(was, bank_bags[i].as_ref())).then_some(true)
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
memo.last_bank_bag_guids = bank_bag_guids;
|
||||
if memo.last_inv.as_ref() != Some(&inv) || memo.last_bank_bags.as_ref() != Some(&bank_bags) {
|
||||
gate.audit("feed_char", "the inventory snapshot");
|
||||
// Read off the OLD memo, before the push replaces it.
|
||||
let repainted: Vec<usize> = (0..BANK_BAG_SLOT_COUNT)
|
||||
.filter(|&i| {
|
||||
let was = memo.last_bank_bags.as_ref().and_then(|b| b[i].as_ref());
|
||||
!same_item(was, bank_bags[i].as_ref())
|
||||
})
|
||||
.collect();
|
||||
script.set_inventory_slots(inv.clone());
|
||||
script.set_bank_bag_slots(bank_bags.clone());
|
||||
script.fire_event(
|
||||
@@ -1213,20 +1231,29 @@ pub(crate) fn feed_char(
|
||||
// announces the band whose data it owns, and `feed_char` is ordered first, so both fire
|
||||
// after their own push.
|
||||
//
|
||||
// **It carries NO arguments** (CARVED — wow-re `system/object-layer/scratch/
|
||||
// bank-slot-event-law.md`). The descriptor watcher's fire site `0x5ddd6e` calls
|
||||
// `FrameScript_SignalEvent 0x703e50`, which is `__fastcall(ecx = id)` with a plain `ret`
|
||||
// and no vararg push at all — zero Lua values. (The image's only other fire site,
|
||||
// `0x4c728d` in the item-GUID→slot notifier, pushes the literal string `"player"`, not a
|
||||
// slot; nothing in FrameXML reads either, both consumers branching on `event` alone.) An
|
||||
// event is fired per changed slot, as the watcher does — the slot travels in *how many*
|
||||
// times it fires, never in an argument.
|
||||
for _ in repainted {
|
||||
script.fire_event("PLAYERBANKSLOTS_CHANGED", vec![]);
|
||||
}
|
||||
// **Which arguments it carries depends on WHICH producer fired** (CARVED — wow-re
|
||||
// `system/object-layer/scratch/bank-slot-event-law.md` §3/§4, folded back in 2140). The
|
||||
// descriptor watcher `0x5ddd6e` calls `FrameScript_SignalEvent 0x703e50`, an
|
||||
// `__fastcall(ecx = id)` with a plain `ret` and no vararg push — zero Lua values; the
|
||||
// item-object notifier `0x4c728d` goes through `SignalEvent2` and pushes the literal
|
||||
// string `"player"`. benilla fired the argless one for both until 2140. An event is fired
|
||||
// per changed slot, as the watcher does — the slot travels in *how many* times it fires,
|
||||
// never in an argument (1776).
|
||||
memo.last_inv = Some(inv);
|
||||
memo.last_bank_bags = Some(bank_bags);
|
||||
}
|
||||
// Outside the push block, because the identical-bag swap it exists for moves no view at all.
|
||||
if !repainted.is_empty() {
|
||||
gate.audit("feed_char", "a bank bag slot transition");
|
||||
}
|
||||
for same_bag in repainted {
|
||||
let args = if same_bag {
|
||||
vec![ScriptValue::Str("player".to_string())]
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
script.fire_event("PLAYERBANKSLOTS_CHANGED", args);
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether two bank-bag views hold the same ITEM — everything but `locked`, which is
|
||||
@@ -1342,6 +1369,8 @@ mod tests {
|
||||
const F_BANK_BAG_1: u16 = 612;
|
||||
/// `OBJECT_FIELD_ENTRY` on the item object.
|
||||
const F_OBJECT_ENTRY: u16 = 3;
|
||||
/// `ITEM_FIELD_STACK_COUNT` — one of the six item fields the reference's own watcher covers.
|
||||
const F_ITEM_STACK_COUNT: u16 = 14;
|
||||
/// "Traveler's Backpack" — any container entry; the feed only needs it to resolve.
|
||||
const BAG_ENTRY: u32 = 4500;
|
||||
const BAG: u64 = 0x4000_0000_0000_0abc;
|
||||
@@ -1420,6 +1449,74 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// **The second producer** (decision 2140). The same bag, its own `ITEM_FIELD_STACK_COUNT`
|
||||
/// moving, is not the descriptor path — it is `0x4c7180`'s item-object path, which goes
|
||||
/// through `SignalEvent2` and pushes the unit token `"player"`. benilla fired the argless
|
||||
/// descriptor shape for both.
|
||||
#[test]
|
||||
fn a_bank_bags_own_field_moving_fires_the_item_object_producer() {
|
||||
let mut app = world_with_bank_bag(Some(BAG));
|
||||
app.world_mut().run_system_once(feed_char).unwrap();
|
||||
assert!(seen(&mut app).contains(&"PLAYERBANKSLOTS_CHANGED nil".to_string()));
|
||||
|
||||
// The SAME bag guid, restacked. Only the item object moved.
|
||||
app.world_mut().resource_mut::<Items>().insert_object(
|
||||
BAG,
|
||||
ObjectFields::from_pairs(&[(F_OBJECT_ENTRY, BAG_ENTRY), (F_ITEM_STACK_COUNT, 3)]),
|
||||
);
|
||||
app.world_mut().run_system_once(feed_char).unwrap();
|
||||
let events = seen(&mut app);
|
||||
assert!(
|
||||
events.contains(&"PLAYERBANKSLOTS_CHANGED player".to_string()),
|
||||
"the item path pushes the token, got {events:?}"
|
||||
);
|
||||
}
|
||||
|
||||
/// Two IDENTICAL bags exchanged between two bank bag slots — the case the pushed view cannot
|
||||
/// see, because two instances of one bag model push the same `InvSlotView`. The reference
|
||||
/// watches the descriptor GUIDs, so it fires the argless event twice; benilla, diffing the
|
||||
/// view, fired nothing at all. Same shape as 1777's `BAG_CLOSED`, one band over.
|
||||
#[test]
|
||||
fn two_identical_bags_swapped_between_bank_bag_slots_still_announce() {
|
||||
let mut app = world_with_bank_bag(Some(BAG));
|
||||
// A second, byte-identical bag in bank bag slot 2.
|
||||
const BAG2: u64 = 0x4000_0000_0000_0abd;
|
||||
// The two bank bag slots' guids, rewritten wholesale — `ObjectFields` has no setter, and
|
||||
// a fresh store is what a descriptor update produces anyway.
|
||||
let set_slots = |app: &mut App, a: u64, b: u64| {
|
||||
let mut q = app
|
||||
.world_mut()
|
||||
.query_filtered::<&mut ObjectStore, With<SelfPlayer>>();
|
||||
let mut store = q.single_mut(app.world_mut()).unwrap();
|
||||
*store = ObjectStore(ObjectFields::from_pairs(&[
|
||||
(F_BANK_BAG_1, a as u32),
|
||||
(F_BANK_BAG_1 + 1, (a >> 32) as u32),
|
||||
(F_BANK_BAG_1 + 2, b as u32),
|
||||
(F_BANK_BAG_1 + 3, (b >> 32) as u32),
|
||||
]));
|
||||
};
|
||||
set_slots(&mut app, BAG, BAG2);
|
||||
app.world_mut().resource_mut::<Items>().insert_object(
|
||||
BAG2,
|
||||
ObjectFields::from_pairs(&[(F_OBJECT_ENTRY, BAG_ENTRY)]),
|
||||
);
|
||||
app.world_mut().run_system_once(feed_char).unwrap();
|
||||
let _ = seen(&mut app);
|
||||
|
||||
// Exchange them. Every pushed view is equal before and after.
|
||||
set_slots(&mut app, BAG2, BAG);
|
||||
app.world_mut().run_system_once(feed_char).unwrap();
|
||||
let events = seen(&mut app);
|
||||
assert_eq!(
|
||||
events
|
||||
.iter()
|
||||
.filter(|e| *e == "PLAYERBANKSLOTS_CHANGED nil")
|
||||
.count(),
|
||||
2,
|
||||
"one argless event per slot whose guid moved, got {events:?}"
|
||||
);
|
||||
}
|
||||
|
||||
/// The other half of the same rule: a LOCK flip is not a slot change. The reference has a
|
||||
/// separate event for it (`ITEM_LOCK_CHANGED`, fired by `ui_items::feed_containers` off
|
||||
/// `LockTransitions`), and firing the repaint event for it too would announce a change to the
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
use bevy::prelude::*;
|
||||
|
||||
use benilla_protocol::messages::BAG_PLAYER_INVENTORY;
|
||||
use benilla_ui::script::{ScriptValue, UiScript, EQUIPMENT_BAG};
|
||||
use benilla_ui::script::{UiScript, EQUIPMENT_BAG};
|
||||
|
||||
use crate::items::Items;
|
||||
use crate::net::{ClientCommand, NetCommands, ObjectStore, SelfPlayer};
|
||||
@@ -574,10 +574,7 @@ pub(super) fn drain_container_uses(
|
||||
.map(|store| slot_guid_count(Some(store), bag, slot, &ladder.items))
|
||||
.unwrap_or((0, 0));
|
||||
pending_items.add([(bag, slot, guid, count)]);
|
||||
script.fire_event(
|
||||
"ITEM_LOCK_CHANGED",
|
||||
vec![ScriptValue::Int(bag), ScriptValue::Int(i64::from(slot))],
|
||||
);
|
||||
script.fire_event("ITEM_LOCK_CHANGED", Vec::new());
|
||||
let _ = ladder.commands.0.send(ClientCommand::OpenItem {
|
||||
bag_index,
|
||||
slot: wire_slot,
|
||||
@@ -831,11 +828,10 @@ pub(crate) fn send_container_move(
|
||||
(mv.src_bag, mv.src_slot, src_guid, src_count),
|
||||
(mv.dst_bag, mv.dst_slot, dst_guid, dst_count),
|
||||
]);
|
||||
for (bag, slot) in [(mv.src_bag, mv.src_slot), (mv.dst_bag, mv.dst_slot)] {
|
||||
script.fire_event(
|
||||
"ITEM_LOCK_CHANGED",
|
||||
vec![ScriptValue::Int(bag), ScriptValue::Int(i64::from(slot))],
|
||||
);
|
||||
// One per locked end, as the reference's own per-slot unlock does — the slot travels in
|
||||
// how many times it fires, never in an argument (see `feed`'s note).
|
||||
for _ in 0..2 {
|
||||
script.fire_event("ITEM_LOCK_CHANGED", Vec::new());
|
||||
}
|
||||
}
|
||||
true
|
||||
@@ -873,10 +869,7 @@ pub(super) fn drain_container_destroys(
|
||||
});
|
||||
let (guid, stack) = slot_guid_count(store, bag, slot, &items);
|
||||
pending.add([(bag, slot, guid, stack)]);
|
||||
script.fire_event(
|
||||
"ITEM_LOCK_CHANGED",
|
||||
vec![ScriptValue::Int(bag), ScriptValue::Int(i64::from(slot))],
|
||||
);
|
||||
script.fire_event("ITEM_LOCK_CHANGED", Vec::new());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -50,6 +50,29 @@ pub(crate) struct FeedMemory {
|
||||
/// [`ContainerState`] therefore cannot answer: two different empty 16-slot bags swapped
|
||||
/// between two slots produce identical container states and must still close both windows.
|
||||
bag_guids: [u64; 10],
|
||||
/// The item guid in each of the **24 vault slots** (absolute player slots 39..62), indexed
|
||||
/// `slot − 1`. Diffed frame to frame to tell `PLAYERBANKSLOTS_CHANGED`'s two producers apart —
|
||||
/// see [`SlotGuids`] and the fire site below. Same reason [`Self::bag_guids`] exists: the
|
||||
/// pushed [`ContainerState`] cannot answer a question about item IDENTITY, because two
|
||||
/// different instances of one template push the same slot view.
|
||||
vault_guids: [u64; BANK_SLOTS as usize],
|
||||
}
|
||||
|
||||
/// The player descriptor's own item-slot guids, as the reference's watchers read them — the ten
|
||||
/// bag slots and the 24 vault slots, in one value because they are one registration family
|
||||
/// (`0x5dd8a0` installs `0x5ddcf0` over four bands of `PLAYER_FIELD_*_SLOT_*` with `len = 8`, the
|
||||
/// guid's width).
|
||||
///
|
||||
/// These ride beside the pushed containers rather than inside them because they are read off the
|
||||
/// player descriptor, not built from the containers — and because they answer what a derived view
|
||||
/// cannot: *which item*, not *what it looks like*.
|
||||
#[derive(Clone, Copy, Default, Debug, PartialEq, Eq)]
|
||||
pub(crate) struct SlotGuids {
|
||||
/// Container ids 1..=10 — the four equipped bag slots, then the six bank bag slots — indexed
|
||||
/// `id − 1`. `BAG_CLOSED`'s diff (decision 1777).
|
||||
pub bags: [u64; 10],
|
||||
/// The 24 vault slots, indexed `slot − 1`. `PLAYERBANKSLOTS_CHANGED`'s producer discriminator.
|
||||
pub vault: [u64; BANK_SLOTS as usize],
|
||||
}
|
||||
|
||||
/// A spell's tooltip text: the $-substituted description (the real "Use: Restores 392 to 653
|
||||
@@ -1276,7 +1299,10 @@ pub(crate) fn feed_containers(
|
||||
&mut script,
|
||||
memory,
|
||||
player.is_some().then_some(fresh),
|
||||
std::array::from_fn(|i| player.map_or(0, |s| bag_slot_guid(s, i as i64 + 1))),
|
||||
SlotGuids {
|
||||
bags: std::array::from_fn(|i| player.map_or(0, |s| bag_slot_guid(s, i as i64 + 1))),
|
||||
vault: std::array::from_fn(|i| player.map_or(0, |s| vault_slot_guid(s, i as u8))),
|
||||
},
|
||||
transitioned,
|
||||
) {
|
||||
gate.audit("feed_containers", "a bag diff or a lock event");
|
||||
@@ -1284,8 +1310,8 @@ pub(crate) fn feed_containers(
|
||||
}
|
||||
|
||||
/// The feed's outward half: diff `source` against what was last pushed, push each changed bag into
|
||||
/// the VM and fire the reference's events — `BAG_UPDATE(bagID)` (`PLAYERBANKSLOTS_CHANGED(slot)`
|
||||
/// for the vault), one `BAG_UPDATE_DELAYED` per batch, then the lock transitions.
|
||||
/// the VM and fire the reference's events — `BAG_UPDATE(bagID)` (`PLAYERBANKSLOTS_CHANGED` for
|
||||
/// the vault), one `BAG_UPDATE_DELAYED` per batch, then the lock transitions.
|
||||
///
|
||||
/// **`source: None` means the self player STORE is absent this frame — "no data source", never
|
||||
/// "the player has no items."** The two absent windows are pre-arrival at login (the fresh VM's
|
||||
@@ -1305,11 +1331,12 @@ pub(crate) fn apply_container_source(
|
||||
script: &mut UiScript,
|
||||
memory: &mut FeedMemory,
|
||||
source: Option<HashMap<i64, ContainerState>>,
|
||||
// The ten bag slots' own guids ([`bag_slot_guid`]) — `BAG_CLOSED`'s diff. Rides beside the
|
||||
// source rather than inside it because it is read off the player descriptor, not built from
|
||||
// the containers, and the absent-source path must leave the memory alone rather than diff
|
||||
// against zeros (that path is the logout despawn window — see this function's own note).
|
||||
bag_guids: [u64; 10],
|
||||
// The player's item-slot guids ([`bag_slot_guid`], [`vault_slot_guid`]) — `BAG_CLOSED`'s diff
|
||||
// and `PLAYERBANKSLOTS_CHANGED`'s producer discriminator. Rides beside the source rather than
|
||||
// inside it because it is read off the player descriptor, not built from the containers, and
|
||||
// the absent-source path must leave the memory alone rather than diff against zeros (that path
|
||||
// is the logout despawn window — see this function's own note).
|
||||
guids: SlotGuids,
|
||||
transitioned: Vec<(i64, u32)>,
|
||||
) -> bool {
|
||||
// Diff whole bags; push + fire BAG_UPDATE per transition, one BAG_UPDATE_DELAYED per batch. A
|
||||
@@ -1318,16 +1345,20 @@ pub(crate) fn apply_container_source(
|
||||
// below rather than leaning on that invariant.
|
||||
let mut pushed = false;
|
||||
if let Some(fresh) = source {
|
||||
pushed |= diff_and_push(script, memory, fresh, bag_guids);
|
||||
pushed |= diff_and_push(script, memory, fresh, guids);
|
||||
}
|
||||
// The lock-transition event (decision 0218: the bag windows' own repaint trigger) — after the
|
||||
// container push above, so a listener's repaint reads the corrected `.locked` state.
|
||||
pushed |= !transitioned.is_empty();
|
||||
for (bag, slot) in transitioned {
|
||||
script.fire_event(
|
||||
"ITEM_LOCK_CHANGED",
|
||||
vec![ScriptValue::Int(bag), ScriptValue::Int(i64::from(slot))],
|
||||
);
|
||||
// **`ITEM_LOCK_CHANGED` carries NO arguments** (decision 2140, found by the argument gate).
|
||||
// All five fire sites in the image — `0x495415`, `0x495455`, `0x49557d` (the local lock/unlock
|
||||
// paths), `0x5d859a` and `0x5d94b9` (the item field watchers) — are
|
||||
// `FrameScript_SignalEvent 0x703e50`, an `__fastcall(ecx = id)` with a plain `ret` and no vararg
|
||||
// mechanism at all, so the event cannot carry one. Every stock consumer repaints from `this`
|
||||
// (`ContainerFrame.lua:39`, `PaperDollFrame.lua:601`, `BankFrame.lua:209`). The `(bag, slot)`
|
||||
// pair benilla pushed is the LATER clients' shape, invented here.
|
||||
for _ in transitioned {
|
||||
script.fire_event("ITEM_LOCK_CHANGED", Vec::new());
|
||||
}
|
||||
// Whether anything went into the VM — the caller's gate audit reads it (1439).
|
||||
pushed
|
||||
@@ -1341,6 +1372,12 @@ pub(crate) fn apply_container_source(
|
||||
/// the descriptor bytes `0x540`–`0x55f` (equipped bags 19–22) and `0x6a0`–`0x6cf` (bank bags
|
||||
/// 63–68) with callback `0x4f8ec0`, and the backpack and keyring loops with `0x4f8db0`, which
|
||||
/// never reaches the event at all. So exactly these ten slots close a window.
|
||||
/// The guid of the item in vault slot `i` (0-based, 24 of them — absolute player slots 39..62).
|
||||
/// `0` = empty. The band `0x5dd8a0`'s L2 loop registers `0x5ddcf0` over (`edi=0x5e0`..`0x698`).
|
||||
fn vault_slot_guid(store: &ObjectStore, i: u8) -> u64 {
|
||||
store.0.player_bank_slot(i).unwrap_or(0)
|
||||
}
|
||||
|
||||
fn bag_slot_guid(store: &ObjectStore, id: i64) -> u64 {
|
||||
match id {
|
||||
1..=4 => store.0.player_inv_slot(BAG_SLOT_FIRST + (id as u8 - 1)),
|
||||
@@ -1355,7 +1392,7 @@ fn diff_and_push(
|
||||
script: &mut UiScript,
|
||||
memory: &mut FeedMemory,
|
||||
fresh: HashMap<i64, ContainerState>,
|
||||
bag_guids: [u64; 10],
|
||||
guids: SlotGuids,
|
||||
) -> bool {
|
||||
// **`BAG_CLOSED` — the only thing in the image that hides an open bag window** (CARVED, wow-re
|
||||
// `system/ui/scratch/equipped-bag-slot-events.md`: one fire site, `0x4f92b5`, and
|
||||
@@ -1371,7 +1408,7 @@ fn diff_and_push(
|
||||
.filter(|&id| {
|
||||
let (was, now) = (
|
||||
memory.bag_guids[id as usize - 1],
|
||||
bag_guids[id as usize - 1],
|
||||
guids.bags[id as usize - 1],
|
||||
);
|
||||
was != 0 && now != was
|
||||
})
|
||||
@@ -1381,12 +1418,55 @@ fn diff_and_push(
|
||||
let emptied: Vec<i64> = closed
|
||||
.iter()
|
||||
.copied()
|
||||
.filter(|&id| bag_guids[id as usize - 1] == 0)
|
||||
.filter(|&id| guids.bags[id as usize - 1] == 0)
|
||||
.collect();
|
||||
memory.bag_guids = bag_guids;
|
||||
memory.bag_guids = guids.bags;
|
||||
for id in &closed {
|
||||
script.fire_event("BAG_CLOSED", vec![ScriptValue::Int(*id)]);
|
||||
}
|
||||
// **`PLAYERBANKSLOTS_CHANGED` has TWO producers in the reference, and they carry different
|
||||
// arguments** (CARVED — wow-re `system/object-layer/scratch/bank-slot-event-law.md` §3/§4;
|
||||
// decision 2140). benilla had one fire for both, so half of them were argless where the
|
||||
// reference pushes a string:
|
||||
//
|
||||
// * **P1, the player-descriptor path** — `0x5ddcf0`'s watcher sees the slot's own GUID field
|
||||
// change and fires `0x5ddd6e`, `FrameScript_SignalEvent 0x703e50`, `__fastcall(ecx = id)`
|
||||
// with a plain `ret`: **zero Lua values**. This is an item arriving, leaving, or being
|
||||
// exchanged for a different one.
|
||||
// * **P2, the item-object path** — the changed ITEM's own watched fields move (stack count,
|
||||
// spell charges, enchantment, flags, durability, entry) or its `CGItem` enters the world;
|
||||
// `0x4c7180` resolves it to a flat slot and fires `0x4c728d`,
|
||||
// `SignalEvent2(347, "%s", "player")`: **`arg1` is the unit token `"player"`**. This is the
|
||||
// same item, changed.
|
||||
//
|
||||
// So the discriminator is the slot's item GUID, not its pushed view — and that is not a
|
||||
// refinement, it is the only thing that answers: two different instances of one template push
|
||||
// an identical `ContainerSlot`, so a view diff reads a swap of two identical stacks as *no
|
||||
// change at all* and fires nothing where the reference fires P1 twice. Exactly 1777's reason
|
||||
// for diffing `BAG_CLOSED` on the bag's own guid, one band over.
|
||||
//
|
||||
// Planned here, before `memory` is rewritten, and fired below after the push — a listener must
|
||||
// repaint off the corrected state. It is deliberately OUTSIDE the `changed` gate for the same
|
||||
// identical-swap reason: that transition moves no container state.
|
||||
let vault: Vec<bool> = {
|
||||
let empty = HashMap::new();
|
||||
let now = fresh.get(&BANK_CONTAINER).map_or(&empty, |c| &c.slots);
|
||||
let was = memory
|
||||
.pushed
|
||||
.get(&BANK_CONTAINER)
|
||||
.map_or(&empty, |c| &c.slots);
|
||||
(1..=u32::from(BANK_SLOTS))
|
||||
.filter_map(|slot| {
|
||||
let i = slot as usize - 1;
|
||||
if guids.vault[i] != memory.vault_guids[i] {
|
||||
Some(false)
|
||||
} else {
|
||||
(now.get(&slot) != was.get(&slot)).then_some(true)
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
};
|
||||
memory.vault_guids = guids.vault;
|
||||
let changed: Vec<i64> = fresh
|
||||
.keys()
|
||||
.chain(memory.pushed.keys())
|
||||
@@ -1428,12 +1508,12 @@ fn diff_and_push(
|
||||
for &bag in &changed {
|
||||
script.set_container(bag, fresh.get(&bag).cloned());
|
||||
}
|
||||
// Ahead of BAG_UPDATE below, which is the reference handler's own order.
|
||||
for (bag, slot) in restacked {
|
||||
script.fire_event(
|
||||
"ITEM_LOCK_CHANGED",
|
||||
vec![ScriptValue::Int(bag), ScriptValue::Int(i64::from(slot))],
|
||||
);
|
||||
// Ahead of BAG_UPDATE below, which is the reference handler's own order. One event per
|
||||
// restacked slot and no arguments (see the note above) — `restacked` keeps the `(bag,
|
||||
// slot)` pairs rather than a count because they are what identifies the slot for anyone
|
||||
// reading this at a breakpoint; the event itself has never carried them.
|
||||
for _ in &restacked {
|
||||
script.fire_event("ITEM_LOCK_CHANGED", Vec::new());
|
||||
}
|
||||
// Name the bags, not just the count: "3 changed" can't tell you WHICH container moved, and
|
||||
// the negative ids (−1 bank, −2 keyring) are exactly the ones you go looking for.
|
||||
@@ -1460,34 +1540,149 @@ fn diff_and_push(
|
||||
// NEITHER `BAG_UPDATE` nor `BAG_CLOSED`: `0x4f8cc0` installs its container listener
|
||||
// over the six bank-bag fields (`0x6a0`–`0x6c8`) and over nothing in the 24 vault
|
||||
// fields (`0x5e0`–`0x698`).
|
||||
if bag == BANK_CONTAINER {
|
||||
let empty = HashMap::new();
|
||||
let now_slots = fresh.get(&bag).map(|c| &c.slots).unwrap_or(&empty);
|
||||
let was_slots = memory.pushed.get(&bag).map(|c| &c.slots).unwrap_or(&empty);
|
||||
for slot in 1..=u32::from(BANK_SLOTS) {
|
||||
if now_slots.get(&slot) != was_slots.get(&slot) {
|
||||
script.fire_event("PLAYERBANKSLOTS_CHANGED", vec![]);
|
||||
}
|
||||
}
|
||||
} else if !emptied.contains(&bag) {
|
||||
if bag != BANK_CONTAINER && !emptied.contains(&bag) {
|
||||
script.fire_event("BAG_UPDATE", vec![ScriptValue::Int(bag)]);
|
||||
}
|
||||
}
|
||||
memory.pushed = fresh;
|
||||
return true;
|
||||
}
|
||||
// A `BAG_CLOSED` with nothing else to say still went into the VM — two identical bags swapped
|
||||
// between two slots change no container state at all.
|
||||
!closed.is_empty()
|
||||
// The vault band's announce, planned above. One event per changed slot, as the watcher does —
|
||||
// the slot travels in *how many times it fires*, never in an argument (1776).
|
||||
for &same_item in &vault {
|
||||
let args = if same_item {
|
||||
vec![ScriptValue::Str("player".into())]
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
script.fire_event("PLAYERBANKSLOTS_CHANGED", args);
|
||||
}
|
||||
// A `BAG_CLOSED` or a vault event with nothing else to say still went into the VM — two
|
||||
// identical bags (or two identical items) swapped between two slots change no container state
|
||||
// at all.
|
||||
!changed.is_empty() || !closed.is_empty() || !vault.is_empty()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{apply_container_source, charges_count, FeedMemory};
|
||||
use super::{
|
||||
apply_container_source, charges_count, ContainerSlot, FeedMemory, SlotGuids,
|
||||
BANK_CONTAINER, BANK_SLOTS,
|
||||
};
|
||||
|
||||
/// No bag in any of the ten bag slots — every test below drives the backpack (container 0),
|
||||
/// which has no bag slot behind it and can never raise `BAG_CLOSED`.
|
||||
const NO_BAGS: [u64; 10] = [0; 10];
|
||||
/// No bag in any of the ten bag slots, nothing in the vault — every test below drives the
|
||||
/// backpack (container 0), which has no bag slot behind it and can never raise `BAG_CLOSED`.
|
||||
const NO_BAGS: SlotGuids = SlotGuids {
|
||||
bags: [0; 10],
|
||||
vault: [0; BANK_SLOTS as usize],
|
||||
};
|
||||
|
||||
/// **`PLAYERBANKSLOTS_CHANGED` has two producers and they carry different arguments**
|
||||
/// (decision 2140; CARVED, wow-re `object-layer/scratch/bank-slot-event-law.md` §3/§4).
|
||||
///
|
||||
/// benilla fired the argless one for every transition, which is right for half of them and
|
||||
/// wrong for the other half — and the half it got wrong is the one a view diff cannot even
|
||||
/// see. The three arms below are the three the carve distinguishes:
|
||||
///
|
||||
/// · a slot's item GUID changes (arrive / leave / exchange) → `0x5ddd6e`, **no arguments**;
|
||||
/// · the same item's own fields change (a restack) → `0x4c728d`, **`arg1 = "player"`**;
|
||||
/// · two IDENTICAL items exchanged between two slots → the pushed views are equal, so the
|
||||
/// container diff sees nothing at all, and the reference fires the argless one **twice**.
|
||||
#[test]
|
||||
fn playerbankslots_changed_names_its_producer_by_the_slot_guid() {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.run(
|
||||
"SEEN = {} \
|
||||
local f = CreateFrame('Frame') \
|
||||
f:RegisterEvent('PLAYERBANKSLOTS_CHANGED') \
|
||||
f:SetScript('OnEvent', function() \
|
||||
table.insert(SEEN, event .. ' ' .. tostring(arg1)) end)",
|
||||
)
|
||||
.unwrap();
|
||||
let seen = |s: &mut UiScript| {
|
||||
let out = s.eval::<Vec<String>>("return SEEN").unwrap();
|
||||
s.run("SEEN = {}").unwrap();
|
||||
out
|
||||
};
|
||||
// The vault, with `slot` holding `count` of item 1234.
|
||||
let vault = |slot: u32, count: u32| {
|
||||
HashMap::from([(
|
||||
BANK_CONTAINER,
|
||||
ContainerState {
|
||||
name: None,
|
||||
num_slots: u32::from(BANK_SLOTS),
|
||||
slots: HashMap::from([(
|
||||
slot,
|
||||
ContainerSlot {
|
||||
item_id: 1234,
|
||||
count,
|
||||
..Default::default()
|
||||
},
|
||||
)]),
|
||||
},
|
||||
)])
|
||||
};
|
||||
let mut guids = NO_BAGS;
|
||||
let mut memory = FeedMemory::default();
|
||||
|
||||
// An item ARRIVES in vault slot 1 — the descriptor path, no arguments.
|
||||
guids.vault[0] = 0xF00D;
|
||||
apply_container_source(&mut s, &mut memory, Some(vault(1, 5)), guids, Vec::new());
|
||||
assert_eq!(seen(&mut s), vec!["PLAYERBANKSLOTS_CHANGED nil"]);
|
||||
|
||||
// The SAME item restacks — its own `ITEM_FIELD_STACK_COUNT` moved, which is the item
|
||||
// watcher's path, and the reference pushes the unit token there.
|
||||
apply_container_source(&mut s, &mut memory, Some(vault(1, 9)), guids, Vec::new());
|
||||
assert_eq!(seen(&mut s), vec!["PLAYERBANKSLOTS_CHANGED player"]);
|
||||
|
||||
// Nothing moved at all.
|
||||
apply_container_source(&mut s, &mut memory, Some(vault(1, 9)), guids, Vec::new());
|
||||
assert!(seen(&mut s).is_empty(), "an unchanged vault says nothing");
|
||||
|
||||
// Two IDENTICAL stacks exchanged between slots 1 and 2. Both slot views are equal before
|
||||
// and after, so `fresh == memory.pushed` and the container diff is empty — the guids are
|
||||
// the only witness, and the reference fires twice.
|
||||
let two = || {
|
||||
HashMap::from([(
|
||||
BANK_CONTAINER,
|
||||
ContainerState {
|
||||
name: None,
|
||||
num_slots: u32::from(BANK_SLOTS),
|
||||
slots: HashMap::from([
|
||||
(
|
||||
1,
|
||||
ContainerSlot {
|
||||
item_id: 1234,
|
||||
count: 9,
|
||||
..Default::default()
|
||||
},
|
||||
),
|
||||
(
|
||||
2,
|
||||
ContainerSlot {
|
||||
item_id: 1234,
|
||||
count: 9,
|
||||
..Default::default()
|
||||
},
|
||||
),
|
||||
]),
|
||||
},
|
||||
)])
|
||||
};
|
||||
guids.vault[1] = 0xBEEF;
|
||||
apply_container_source(&mut s, &mut memory, Some(two()), guids, Vec::new());
|
||||
assert_eq!(seen(&mut s), vec!["PLAYERBANKSLOTS_CHANGED nil"]);
|
||||
guids.vault.swap(0, 1);
|
||||
let pushed = apply_container_source(&mut s, &mut memory, Some(two()), guids, Vec::new());
|
||||
assert_eq!(
|
||||
seen(&mut s),
|
||||
vec![
|
||||
"PLAYERBANKSLOTS_CHANGED nil".to_string(),
|
||||
"PLAYERBANKSLOTS_CHANGED nil".to_string()
|
||||
],
|
||||
"the swap the pushed containers cannot see"
|
||||
);
|
||||
assert!(pushed, "and the feed reports that it spoke to the VM");
|
||||
}
|
||||
|
||||
/// **`BAG_CLOSED`, the only thing that hides an open bag window** — and the three ways to get
|
||||
/// its condition wrong (CARVED, wow-re `system/ui/scratch/equipped-bag-slot-events.md`).
|
||||
@@ -1535,7 +1730,7 @@ mod tests {
|
||||
|
||||
// Equip a bag into the first equipped bag slot: BAG_UPDATE, and no BAG_CLOSED — the slot
|
||||
// was empty, which is the `0x4f9247 je` arm.
|
||||
guids[0] = 0xAAAA;
|
||||
guids.bags[0] = 0xAAAA;
|
||||
apply_container_source(&mut s, &mut memory, Some(pouch(6)), guids, Vec::new());
|
||||
assert_eq!(seen(&mut s), vec!["BAG_UPDATE 1"]);
|
||||
|
||||
@@ -1549,12 +1744,12 @@ mod tests {
|
||||
|
||||
// SWAP for a different bag: closed FIRST, then updated. This is the arm a "the bag went
|
||||
// away" reading gets wrong — the slot is still occupied.
|
||||
guids[0] = 0xBBBB;
|
||||
guids.bags[0] = 0xBBBB;
|
||||
apply_container_source(&mut s, &mut memory, Some(pouch(10)), guids, Vec::new());
|
||||
assert_eq!(seen(&mut s), vec!["BAG_CLOSED 1", "BAG_UPDATE 1"]);
|
||||
|
||||
// UNEQUIP: closed, and **no** BAG_UPDATE.
|
||||
guids[0] = 0;
|
||||
guids.bags[0] = 0;
|
||||
apply_container_source(&mut s, &mut memory, Some(HashMap::new()), guids, Vec::new());
|
||||
assert_eq!(seen(&mut s), vec!["BAG_CLOSED 1"]);
|
||||
|
||||
@@ -1583,11 +1778,11 @@ mod tests {
|
||||
])
|
||||
};
|
||||
let mut guids = NO_BAGS;
|
||||
guids[4] = 0x1111;
|
||||
guids[5] = 0x2222;
|
||||
guids.bags[4] = 0x1111;
|
||||
guids.bags[5] = 0x2222;
|
||||
apply_container_source(&mut s, &mut memory, Some(two()), guids, Vec::new());
|
||||
let _ = seen(&mut s);
|
||||
guids.swap(4, 5);
|
||||
guids.bags.swap(4, 5);
|
||||
let pushed = apply_container_source(&mut s, &mut memory, Some(two()), guids, Vec::new());
|
||||
assert_eq!(seen(&mut s), vec!["BAG_CLOSED 5", "BAG_CLOSED 6"]);
|
||||
assert!(
|
||||
@@ -1662,8 +1857,9 @@ mod tests {
|
||||
}
|
||||
|
||||
/// **The spent-ammo signal** (decision 1509, B267's second half). A stack ticking down must
|
||||
/// fire `ITEM_LOCK_CHANGED` for that slot, **before** the `BAG_UPDATE` for its bag — the
|
||||
/// reference's `ITEM_FIELD_STACK_COUNT` mirror handler's own order.
|
||||
/// fire `ITEM_LOCK_CHANGED`, **before** the `BAG_UPDATE` for its bag — the reference's
|
||||
/// `ITEM_FIELD_STACK_COUNT` mirror handler's own order. The event carries no arguments
|
||||
/// (2140): `0x5d94b9` is a `SignalEvent` site, which has no vararg mechanism at all.
|
||||
///
|
||||
/// This is not cosmetic ordering. Quiver's auto-shot timer has no other way to learn a shot
|
||||
/// fired: it starts the reload drain from this event, and without it the bar fills once and
|
||||
@@ -1730,8 +1926,11 @@ mod tests {
|
||||
);
|
||||
assert_eq!(
|
||||
s.eval::<String>("return table.concat(ORDER, ' ')").unwrap(),
|
||||
"ITEM_LOCK_CHANGED:0,1 BAG_UPDATE:0,nil",
|
||||
"the shot signal fires for (bag 0, slot 1) and precedes BAG_UPDATE"
|
||||
"ITEM_LOCK_CHANGED:nil,nil BAG_UPDATE:0,nil",
|
||||
"the shot signal fires — with NO arguments, as all five of the reference's fire sites \
|
||||
do (decision 2140) — and precedes BAG_UPDATE. The slot it names travels in the fact \
|
||||
that it fired at all, which is enough: every consumer repaints from its own `this`, \
|
||||
and Quiver's shot timer only needs to know that A shot happened"
|
||||
);
|
||||
|
||||
// A DIFFERENT item in the same slot: a swap. The count differs too, and it must still
|
||||
|
||||
@@ -584,8 +584,11 @@ fn feed_mail(
|
||||
for index in std::mem::take(&mut mail.close_inbox) {
|
||||
script.fire_event("CLOSE_INBOX_ITEM", vec![ScriptValue::Int(i64::from(index))]);
|
||||
}
|
||||
// `SMSG_SEND_MAIL_RESULT 0x4ad050`, in its own order (wow-re `mail-interaction.md` §4): the
|
||||
// GlobalString toast, then — on `action 0 / reason 0` only — the compose-tab reset `0x4acdc0(1)`
|
||||
// with its three events, and **last, on every path, `MAIL_FAILED`** (`0x4ad15f`, unconditional:
|
||||
// a successful send fires it too, as the generic "the send resolved, unblock the form" signal).
|
||||
for ack in std::mem::take(&mut mail.send_acks) {
|
||||
script.fire_event("MAIL_FAILED", vec![]);
|
||||
let key = if ack.ok {
|
||||
Some(MAIL_SENT_KEY)
|
||||
} else {
|
||||
@@ -594,9 +597,9 @@ fn feed_mail(
|
||||
let line = key.and_then(|k| crate::ui_action::keyed_line(&script, k));
|
||||
crate::ui_action::show_messages(&mut script, &mut sink, "ui_mail", line);
|
||||
if ack.ok {
|
||||
script.fire_event("MAIL_SEND_SUCCESS", vec![]);
|
||||
script.clear_send_mail_item();
|
||||
script.reset_compose_tab();
|
||||
}
|
||||
script.fire_event("MAIL_FAILED", vec![]);
|
||||
}
|
||||
|
||||
// The macro subject, resolved before the row walk borrows the name cache again. `None` until
|
||||
@@ -657,12 +660,12 @@ fn feed_mail(
|
||||
// The selection is cleared on open AND close (`0x4ace07`, 1970); the stock tab's reset
|
||||
// on MAIL_SHOW re-selects row 1.
|
||||
script.clear_stationery();
|
||||
// The open core's own order (wow-re `mail-interaction.md` §1): store the mailbox, register
|
||||
// the interaction target, **reset the compose tab** — a fresh window carries no stale
|
||||
// attachment or money — and only THEN `MAIL_SHOW`. The reset's `MAIL_SEND_SUCCESS` is the
|
||||
// byte-verified open side-effect, so the page-turn sound on open is faithful, not a bug.
|
||||
script.reset_compose_tab();
|
||||
script.fire_event("MAIL_SHOW", vec![]);
|
||||
// The compose tab resets on open — a fresh window carries no stale attachment/money.
|
||||
script.clear_send_mail_item();
|
||||
// Byte-verified open side-effect (wow-re §5): the client's compose-tab reset fires
|
||||
// MAIL_SEND_SUCCESS on open too, so the page-turn sound on open is faithful, not a bug.
|
||||
script.fire_event("MAIL_SEND_SUCCESS", vec![]);
|
||||
} else if closed {
|
||||
script.clear_stationery();
|
||||
script.fire_event("MAIL_CLOSED", vec![]);
|
||||
|
||||
@@ -322,11 +322,22 @@ fn feed_pet_stats(
|
||||
// line is `if ( arg1 == this.unit )`, so an argless fire reaches nobody.
|
||||
script.fire_event("UNIT_HAPPINESS", vec![ScriptValue::Str("pet".into())]);
|
||||
}
|
||||
// Both of these are the SAME bridge as `UNIT_HAPPINESS` above — named unit-descriptor fields
|
||||
// (141/142 `PET_EXPERIENCE`/`PET_NEXT_LEVEL_EXP`, 149 `TRAINING_POINTS`), whose events the
|
||||
// reference dispatches through `0x515e50`'s token fan-out, `SignalEvent2(id, "%s", token)`.
|
||||
// They were argless here for months, two lines under a comment stating the law, and the cost
|
||||
// was not theoretical: `PetPaperDollFrame_OnEvent` gives `UNIT_PET_EXPERIENCE` a named branch
|
||||
// but routes `UNIT_PET_TRAINING_POINTS` to its final `elseif ( arg1 == "pet" )` catch-all
|
||||
// (`PetPaperDollFrame.lua:44`), so an argless fire reached nobody and the pet page's training
|
||||
// points never repainted off the event at all.
|
||||
if xp_moved {
|
||||
script.fire_event("UNIT_PET_EXPERIENCE", vec![]);
|
||||
script.fire_event("UNIT_PET_EXPERIENCE", vec![ScriptValue::Str("pet".into())]);
|
||||
}
|
||||
if training_moved {
|
||||
script.fire_event("UNIT_PET_TRAINING_POINTS", vec![]);
|
||||
script.fire_event(
|
||||
"UNIT_PET_TRAINING_POINTS",
|
||||
vec![ScriptValue::Str("pet".into())],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1182,7 +1182,7 @@ fn the_split_frame_raises_over_a_same_stratum_window() {
|
||||
local w = CreateFrame("Frame", "FakeBagnon", UIParent)
|
||||
w:SetFrameStrata("HIGH")
|
||||
w:SetPoint("BOTTOMLEFT", 0, 0)
|
||||
w:SetSize(1024, 768)
|
||||
w:SetWidth(1024); w:SetHeight(768)
|
||||
local bg = w:CreateTexture(nil, "BACKGROUND")
|
||||
bg:SetTexture("Interface\\FakeBagnonBG")
|
||||
bg:SetAllPoints()
|
||||
|
||||
@@ -1395,7 +1395,13 @@ fn bagnon_forevers_records_survive_the_logout_boundary() {
|
||||
// The logout boundary: the despawn frame (no self store) and then the shutdown's own events.
|
||||
// The record the write persists must be the bags the player actually had.
|
||||
let mut memory = crate::ui_items::feed::FeedMemory::default();
|
||||
crate::ui_items::feed::apply_container_source(&mut s, &mut memory, None, [0; 10], Vec::new());
|
||||
crate::ui_items::feed::apply_container_source(
|
||||
&mut s,
|
||||
&mut memory,
|
||||
None,
|
||||
Default::default(),
|
||||
Vec::new(),
|
||||
);
|
||||
s.fire_event("PLAYER_LEAVING_WORLD", Vec::new());
|
||||
s.fire_event("PLAYER_LOGOUT", Vec::new());
|
||||
let (size, item) = s
|
||||
|
||||
@@ -1038,7 +1038,7 @@ fn the_duration_line_reads_the_real_global_strings() {
|
||||
s.run(
|
||||
r#"
|
||||
BENILLA_ANCHOR = CreateFrame("Button", "BF9")
|
||||
BENILLA_ANCHOR:SetPoint("CENTER", 0, 0); BENILLA_ANCHOR:SetSize(10, 10)
|
||||
BENILLA_ANCHOR:SetPoint("CENTER", 0, 0); BENILLA_ANCHOR:SetWidth(10); BENILLA_ANCHOR:SetHeight(10)
|
||||
BENILLA_TIP = CreateFrame("GameTooltip", "TT9")
|
||||
"#,
|
||||
)
|
||||
|
||||
@@ -335,9 +335,9 @@ fn a_rare_payload_raises_the_typed_confirm_with_okay_disabled() {
|
||||
.unwrap(),
|
||||
"hasEditBox raises the narrow box"
|
||||
);
|
||||
assert!(
|
||||
s.eval::<bool>("return StaticPopup1EditBox:HasFocus()")
|
||||
.unwrap(),
|
||||
assert_eq!(
|
||||
s.focused_editbox_name().as_deref(),
|
||||
Some("StaticPopup1EditBox"),
|
||||
"the entry's OnShow focuses the box, so the player can type straight away"
|
||||
);
|
||||
assert_eq!(
|
||||
@@ -478,18 +478,19 @@ fn no_on_the_typed_confirm_clears_and_leaves_the_box_empty_for_next_time() {
|
||||
fn escape_in_the_typed_confirms_box_is_swallowed_as_the_reference_leaves_it() {
|
||||
let mut s = setup();
|
||||
drop_in_world(&mut s, 871, "Flurry Axe", 4);
|
||||
assert!(s
|
||||
.eval::<bool>("return StaticPopup1EditBox:HasFocus()")
|
||||
.unwrap());
|
||||
assert_eq!(
|
||||
s.focused_editbox_name().as_deref(),
|
||||
Some("StaticPopup1EditBox")
|
||||
);
|
||||
|
||||
assert!(s.key_input("ESCAPE"), "the focused box consumes ESCAPE");
|
||||
assert!(
|
||||
s.eval::<bool>("return StaticPopup1:IsVisible()").unwrap(),
|
||||
"the dialog stays: the entry names no escape handler"
|
||||
);
|
||||
assert!(
|
||||
s.eval::<bool>("return StaticPopup1EditBox:HasFocus()")
|
||||
.unwrap(),
|
||||
assert_eq!(
|
||||
s.focused_editbox_name().as_deref(),
|
||||
Some("StaticPopup1EditBox"),
|
||||
"and the box keeps focus"
|
||||
);
|
||||
assert!(s.cursor_item().is_some(), "the item is still on the cursor");
|
||||
|
||||
@@ -0,0 +1,385 @@
|
||||
//! **The event ARGUMENT-shape gate** — `reference/1.12-events.tsv` against what benilla actually
|
||||
//! pushes at each fire site (decision 2140).
|
||||
//!
|
||||
//! Two gates already guard the event seam by NAME. [`super::reference_ui`]'s
|
||||
//! `every_event_a_chain_file_registers_has_a_producer` asks whether something fires what a stock
|
||||
//! file listens for (1889); `every_event_we_fire_is_an_event_the_reference_has` asks whether a
|
||||
//! name we fire is a 1.12 name at all (1883). **Neither has ever looked at the arguments**, and an
|
||||
//! event name is a plain string at both ends — so a fire with the wrong argument shape is silent
|
||||
//! on both sides. It is 1842's finding one API over: the right name, the wrong shape.
|
||||
//!
|
||||
//! It was not hypothetical. Three live divergences on the first run:
|
||||
//!
|
||||
//! * `PLAYERBANKSLOTS_CHANGED` — two producers in the reference with **different** shapes
|
||||
//! (`0x5ddd6e` argless, `0x4c728d` `"player"`), one shape here.
|
||||
//! * `UNIT_PET_EXPERIENCE` and `UNIT_PET_TRAINING_POINTS` — argless here, `%s` there. The second
|
||||
//! one reaches `PetPaperDollFrame_OnEvent` only through its final `elseif ( arg1 == "pet" )`
|
||||
//! catch-all, so an argless fire reached **nobody**: the stock pet page never repainted its
|
||||
//! training points off the event at all.
|
||||
//!
|
||||
//! ## What is compared, and what the table can and cannot say
|
||||
//!
|
||||
//! The oracle is wow-re's two byte-level censuses, vendored by `scripts/gen-reference-events.py`.
|
||||
//! Its `conf` column carries the gate rule per row, exactly as `1.12-shapes.tsv` does:
|
||||
//!
|
||||
//! * `exact` — every producer that reaches the event has a known shape. **This is the column this
|
||||
//! gate reads.**
|
||||
//! * `advisory` — a contributing site declares more varargs than its caller pushes (three sites,
|
||||
//! image-wide). Not gated: a faithful client must not copy a bug that hands Lua undefined
|
||||
//! values.
|
||||
//! * `none` — no producer family reaches it in that census. **Not a claim that it has none** (the
|
||||
//! census's own header says so in capitals), so a `none` row is skipped, never failed. That is
|
||||
//! where the whole `CHAT_MSG_*` family sits: those reach Lua through a formatted wrapper the
|
||||
//! two-helper census does not see.
|
||||
//!
|
||||
//! ## Two deliberate narrowings
|
||||
//!
|
||||
//! **Only a fire whose event name is a literal is measured.** `ui_unit.rs` builds the ten power
|
||||
//! events with `format!("UNIT_{}", power_token(…))`, and there is no name to key on at the call
|
||||
//! site. Those are covered one gate over — `reference_ui`'s `CONSTRUCTED` list — and their single
|
||||
//! `tok()` argument is the same shape all ten share.
|
||||
//!
|
||||
//! **Only a literal `ScriptValue` element's KIND is asserted.** An argument built from a variable
|
||||
//! (`vec![ScriptValue::Int(bag)]` is literal enough — the *variant* is what types it, not the
|
||||
//! value) is typed by its variant; anything that is not a `ScriptValue::` constructor at all
|
||||
//! counts toward the arity and matches any kind. Arity is the assertion that catches the bug
|
||||
//! class; kinds are the free extra.
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
/// One row of `reference/1.12-events.tsv`.
|
||||
struct Row {
|
||||
/// The alternatives, each a per-slot kind list. `%s` -> `s`, `%d`/`%u`/`%f` -> `n`.
|
||||
shapes: Vec<Vec<char>>,
|
||||
producers: String,
|
||||
}
|
||||
|
||||
fn table() -> BTreeMap<String, Row> {
|
||||
let tsv = concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/../../reference/1.12-events.tsv"
|
||||
);
|
||||
let text = std::fs::read_to_string(tsv).expect("reference/1.12-events.tsv");
|
||||
let mut out = BTreeMap::new();
|
||||
for line in text.lines() {
|
||||
if line.starts_with('#') || line.starts_with("name\t") {
|
||||
continue;
|
||||
}
|
||||
let f: Vec<&str> = line.split('\t').collect();
|
||||
// name ids producers arg_formats conf note
|
||||
if f.len() < 5 || f[4] != "exact" {
|
||||
continue;
|
||||
}
|
||||
let shapes = f[3]
|
||||
.split('|')
|
||||
.map(|alt| {
|
||||
if alt == "()" {
|
||||
return Vec::new();
|
||||
}
|
||||
alt.split('%')
|
||||
.skip(1)
|
||||
.map(|d| match d.chars().next() {
|
||||
Some('s') => 's',
|
||||
_ => 'n',
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.collect();
|
||||
out.insert(
|
||||
f[0].to_string(),
|
||||
Row {
|
||||
shapes,
|
||||
producers: f[2].to_string(),
|
||||
},
|
||||
);
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Split `body` at its top-level commas — `vec![…]`'s elements, with nested calls left alone.
|
||||
fn elements(body: &str) -> Vec<String> {
|
||||
let mut out = Vec::new();
|
||||
let (mut depth, mut cur, mut in_str, mut esc) = (0i32, String::new(), false, false);
|
||||
for c in body.chars() {
|
||||
if in_str {
|
||||
cur.push(c);
|
||||
if esc {
|
||||
esc = false;
|
||||
} else if c == '\\' {
|
||||
esc = true;
|
||||
} else if c == '"' {
|
||||
in_str = false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
match c {
|
||||
'"' => {
|
||||
in_str = true;
|
||||
cur.push(c);
|
||||
}
|
||||
'(' | '[' | '{' => {
|
||||
depth += 1;
|
||||
cur.push(c);
|
||||
}
|
||||
')' | ']' | '}' => {
|
||||
depth -= 1;
|
||||
cur.push(c);
|
||||
}
|
||||
',' if depth == 0 => {
|
||||
out.push(std::mem::take(&mut cur));
|
||||
}
|
||||
_ => cur.push(c),
|
||||
}
|
||||
}
|
||||
if !cur.trim().is_empty() {
|
||||
out.push(cur);
|
||||
}
|
||||
out.into_iter()
|
||||
.map(|e| e.trim().to_string())
|
||||
.filter(|e| !e.is_empty())
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// The kind an argument expression pushes: `s` string, `n` number, `b` boolean, `x` nil,
|
||||
/// `?` not determinable from the source.
|
||||
fn kind_of(expr: &str) -> char {
|
||||
let e = expr.trim_start_matches('&').trim();
|
||||
for (variant, k) in [
|
||||
("ScriptValue::Str", 's'),
|
||||
("ScriptValue::Int", 'n'),
|
||||
("ScriptValue::Number", 'n'),
|
||||
("ScriptValue::Bool", 'b'),
|
||||
("ScriptValue::Nil", 'x'),
|
||||
] {
|
||||
if e.starts_with(variant) {
|
||||
return k;
|
||||
}
|
||||
}
|
||||
'?'
|
||||
}
|
||||
|
||||
/// Every `(event, argument kinds, where)` benilla fires with a literal name.
|
||||
fn fire_sites() -> Vec<(String, Vec<char>, String)> {
|
||||
let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.parent()
|
||||
.and_then(|p| p.parent())
|
||||
.expect("workspace root")
|
||||
.join("crates");
|
||||
let mut out = Vec::new();
|
||||
let mut stack = vec![root];
|
||||
while let Some(dir) = stack.pop() {
|
||||
for e in std::fs::read_dir(&dir).into_iter().flatten().flatten() {
|
||||
let p = e.path();
|
||||
if p.is_dir() {
|
||||
stack.push(p);
|
||||
continue;
|
||||
}
|
||||
if p.extension().is_none_or(|x| x != "rs") {
|
||||
continue;
|
||||
}
|
||||
// A test's own synthetic fires are not a surface we ship — the same rule
|
||||
// `every_event_we_fire_is_an_event_the_reference_has` holds, plus the inline
|
||||
// `#[cfg(test)]` tail a file-name rule cannot see.
|
||||
if p.to_string_lossy().contains("test") {
|
||||
continue;
|
||||
}
|
||||
let whole = std::fs::read_to_string(&p).unwrap_or_default();
|
||||
let text = whole
|
||||
.split_once("#[cfg(test)]")
|
||||
.map_or(whole.as_str(), |(head, _)| head);
|
||||
// Split so this file cannot match its own scanner (the 1883 gate's lesson).
|
||||
for call in [
|
||||
concat!("fire_event", "("),
|
||||
concat!("fire_event_into", "("),
|
||||
concat!("pending_events.push", "(("),
|
||||
] {
|
||||
let mut from = 0;
|
||||
while let Some(i) = text[from..].find(call) {
|
||||
let at = from + i + call.len();
|
||||
from = at;
|
||||
let rest = text[at..].trim_start();
|
||||
let rest = rest.strip_prefix("lua,").map_or(rest, str::trim_start);
|
||||
let Some(body) = rest.strip_prefix('"') else {
|
||||
continue;
|
||||
};
|
||||
let Some(end) = body.find('"') else { continue };
|
||||
let (name, after) = (&body[..end], &body[end + 1..]);
|
||||
let Some(args) = after.trim_start().strip_prefix(',') else {
|
||||
continue;
|
||||
};
|
||||
let args = args.trim_start();
|
||||
let kinds = if let Some(v) = args.strip_prefix("vec![") {
|
||||
let mut depth = 1i32;
|
||||
let mut close = v.len();
|
||||
for (k, c) in v.char_indices() {
|
||||
match c {
|
||||
'[' => depth += 1,
|
||||
']' => {
|
||||
depth -= 1;
|
||||
if depth == 0 {
|
||||
close = k;
|
||||
break;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
elements(&v[..close]).iter().map(|e| kind_of(e)).collect()
|
||||
} else if args.starts_with("Vec::new()") {
|
||||
Vec::new()
|
||||
} else {
|
||||
// An argument list built elsewhere — arity unknowable from here.
|
||||
continue;
|
||||
};
|
||||
out.push((name.to_string(), kinds, p.display().to_string()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// **Every event benilla fires must carry the arguments the reference's producer pushes.**
|
||||
#[test]
|
||||
fn every_event_we_fire_carries_the_reference_s_arguments() {
|
||||
let table = table();
|
||||
assert!(
|
||||
table.len() > 250,
|
||||
"the events table gave only {} gateable rows — it stopped being read",
|
||||
table.len()
|
||||
);
|
||||
let sites = fire_sites();
|
||||
assert!(
|
||||
sites.len() > 200,
|
||||
"the walker found only {} literal fire sites — it stopped matching, which would make this \
|
||||
gate silently vacuous",
|
||||
sites.len()
|
||||
);
|
||||
|
||||
let mut checked = 0usize;
|
||||
let mut wrong: Vec<String> = Vec::new();
|
||||
for (name, kinds, at) in &sites {
|
||||
let Some(row) = table.get(name) else { continue };
|
||||
checked += 1;
|
||||
let fits = row.shapes.iter().any(|want| {
|
||||
want.len() == kinds.len()
|
||||
&& want
|
||||
.iter()
|
||||
.zip(kinds)
|
||||
.all(|(w, g)| *g == '?' || *g == *w || (*w == 'n' && *g == 'n'))
|
||||
});
|
||||
if !fits {
|
||||
let shapes: Vec<String> = row
|
||||
.shapes
|
||||
.iter()
|
||||
.map(|s| format!("({})", s.iter().collect::<String>()))
|
||||
.collect();
|
||||
wrong.push(format!(
|
||||
"{name} fired as ({}) from {at} — the reference's {} producer(s) push {}",
|
||||
kinds.iter().collect::<String>(),
|
||||
row.producers,
|
||||
shapes.join(" | ")
|
||||
));
|
||||
}
|
||||
}
|
||||
assert!(
|
||||
checked >= 175,
|
||||
"only {checked} fires were measurable against the table — coverage collapsed"
|
||||
);
|
||||
wrong.sort();
|
||||
wrong.dedup();
|
||||
assert!(
|
||||
wrong.is_empty(),
|
||||
"{} fire site(s) push the wrong argument shape. An event name is a plain string at both \
|
||||
ends, so nothing else in the build can see this:\n {}",
|
||||
wrong.len(),
|
||||
wrong.join("\n ")
|
||||
);
|
||||
}
|
||||
|
||||
/// **Every event the reference's unit-field bridge produces must have a producer here.**
|
||||
///
|
||||
/// This is the half that let `UNIT_DYNAMIC_FLAGS` sit unfired for the whole life of the client.
|
||||
/// [`super::reference_ui::every_event_a_chain_file_registers_has_a_producer`] asks whether
|
||||
/// anything fires what a **stock chain file** listens for — and *nothing in 1.12 FrameXML
|
||||
/// registers `UNIT_DYNAMIC_FLAGS`, so the hole was invisible to it by construction. The corpus is
|
||||
/// where the demand was (`CT_UnitFrames/CT_TargetFrame.xml:200`, `TipBuddy/TipBuddy.lua:17`), and
|
||||
/// the corpus is not in this repo.
|
||||
///
|
||||
/// So this gate asks the question from the REFERENCE's side instead, over the one family where
|
||||
/// that is bounded and exact: `0x51bbb0` registers one watch per named unit-window field, so
|
||||
/// every event id below `0xb6` that has a name in the client's own table is produced by that
|
||||
/// bridge, always with the unit token. Twenty-five names, one producer function on our side
|
||||
/// ([`crate::ui_unit::fire_transitions`] and the two pet-stat feeds), and no judgment call about
|
||||
/// which of them "should" exist — the reference fires all of them.
|
||||
///
|
||||
/// A CENSUS with a declared set, like 1889's: what must not happen is a new name appearing.
|
||||
#[test]
|
||||
fn every_unit_field_bridge_event_has_a_producer() {
|
||||
/// Produced by the reference's bridge, by nothing here. Each needs a reason, and an entry
|
||||
/// that comes into production fails the gate so the list cannot outlive its fix.
|
||||
const UNPRODUCED: &[(&str, &str)] = &[(
|
||||
"UNIT_LOYALTY",
|
||||
"field 138, the hunter pet's loyalty level. benilla's pet-stat feed (`ui_pet_stats`) \
|
||||
reads happiness and training points off `PET_BYTES` but never the loyalty field, and no \
|
||||
window it feeds shows a loyalty line — `PetPaperDollFrame` does not register it either. \
|
||||
Unbuilt, not wrong: the day the pet page grows the line, this entry goes with it",
|
||||
)];
|
||||
|
||||
let table = table();
|
||||
let bridge: Vec<&String> = table
|
||||
.iter()
|
||||
.filter(|(_, r)| r.producers.contains("unit-field-bridge"))
|
||||
.map(|(n, _)| n)
|
||||
.collect();
|
||||
assert!(
|
||||
bridge.len() >= 25,
|
||||
"only {} bridge events in the table — it stopped being read",
|
||||
bridge.len()
|
||||
);
|
||||
let fired: std::collections::HashSet<String> = fire_sites()
|
||||
.into_iter()
|
||||
.map(|(n, _, _)| n)
|
||||
.chain(
|
||||
// The ten power events are built with `format!("UNIT_{}", power_token(…))`, so no
|
||||
// literal exists to scan for — the same declared family `reference_ui`'s CONSTRUCTED
|
||||
// list carries, and an entry there is a promise that something fires it.
|
||||
[
|
||||
"MANA",
|
||||
"RAGE",
|
||||
"FOCUS",
|
||||
"ENERGY",
|
||||
"HAPPINESS",
|
||||
"MAXMANA",
|
||||
"MAXRAGE",
|
||||
"MAXFOCUS",
|
||||
"MAXENERGY",
|
||||
"MAXHAPPINESS",
|
||||
]
|
||||
.iter()
|
||||
.map(|p| format!("UNIT_{p}")),
|
||||
)
|
||||
.collect();
|
||||
|
||||
let expected: std::collections::HashSet<&str> = UNPRODUCED.iter().map(|(e, _)| *e).collect();
|
||||
let missing: Vec<&str> = bridge
|
||||
.iter()
|
||||
.map(|n| n.as_str())
|
||||
.filter(|n| !fired.contains(*n) && !expected.contains(n))
|
||||
.collect();
|
||||
assert!(
|
||||
missing.is_empty(),
|
||||
"the reference fires these off a named unit descriptor field and benilla fires them \
|
||||
nowhere. No stock file need listen for one — the corpus does, and an addon that \
|
||||
registers an event nothing produces hears silence with no error anywhere:\n {missing:?}"
|
||||
);
|
||||
let now_fired: Vec<&str> = UNPRODUCED
|
||||
.iter()
|
||||
.map(|(e, _)| *e)
|
||||
.filter(|e| fired.contains(*e))
|
||||
.collect();
|
||||
assert!(
|
||||
now_fired.is_empty(),
|
||||
"these now HAVE a producer — take them out of UNPRODUCED so the list keeps meaning what \
|
||||
it says: {now_fired:?}"
|
||||
);
|
||||
}
|
||||
@@ -1980,9 +1980,9 @@ mod clip_plumb_tests {
|
||||
r#"
|
||||
local frame = CreateFrame("ScrollFrame", "SF")
|
||||
frame:SetPoint("TOPLEFT", 0, -100) -- screen top 768 -> frame top 668
|
||||
frame:SetSize(300, 200) -- frame bottom 300, right 300
|
||||
frame:SetWidth(300); frame:SetHeight(200) -- frame bottom 300, right 300
|
||||
local child = CreateFrame("Frame", "Child")
|
||||
child:SetSize(300, 600)
|
||||
child:SetWidth(300); child:SetHeight(600)
|
||||
frame:SetScrollChild(child)
|
||||
local marker = child:CreateTexture(nil, "ARTWORK")
|
||||
marker:SetTexture(1, 0, 0) -- pathless colored quad: no BLP asset needed
|
||||
@@ -2070,7 +2070,7 @@ mod clip_plumb_tests {
|
||||
r#"
|
||||
local plain = CreateFrame("Frame", "Plain")
|
||||
plain:SetPoint("TOPLEFT", 0, 0)
|
||||
plain:SetSize(50, 50)
|
||||
plain:SetWidth(50); plain:SetHeight(50)
|
||||
local m = plain:CreateTexture(nil, "ARTWORK")
|
||||
m:SetTexture(0, 1, 0)
|
||||
m:SetAllPoints() -- templateless Lua region: no implicit anchor (1310)
|
||||
@@ -2134,7 +2134,7 @@ mod align_tests {
|
||||
for i = 1, 2 do
|
||||
local f = CreateFrame("Frame", "F" .. i)
|
||||
f:SetPoint("TOPLEFT", 0, 0)
|
||||
f:SetSize(50, 50)
|
||||
f:SetWidth(50); f:SetHeight(50)
|
||||
for j = 1, 2 do
|
||||
local t = f:CreateTexture(nil, "ARTWORK")
|
||||
t:SetTexture(i / 4, j / 4, 0)
|
||||
@@ -2271,7 +2271,7 @@ mod extract_gate_tests {
|
||||
r#"
|
||||
local plain = CreateFrame("Frame", "Plain")
|
||||
plain:SetPoint("TOPLEFT", 0, 0)
|
||||
plain:SetSize(50, 50)
|
||||
plain:SetWidth(50); plain:SetHeight(50)
|
||||
marker = plain:CreateTexture(nil, "ARTWORK")
|
||||
marker:SetTexture(1, 0, 0)
|
||||
marker:SetAllPoints() -- templateless Lua region: no implicit anchor (1310)
|
||||
@@ -2379,11 +2379,11 @@ mod extract_gate_tests {
|
||||
r#"
|
||||
local doll = CreateFrame("PlayerModel", "CharacterModelFrame")
|
||||
doll:SetPoint("TOPLEFT", 0, 0)
|
||||
doll:SetSize(233, 224)
|
||||
doll:SetWidth(233); doll:SetHeight(224)
|
||||
doll:SetUnit("player")
|
||||
local stray = CreateFrame("PlayerModel", "SomeAddonsModelPane")
|
||||
stray:SetPoint("TOPLEFT", 300, 0)
|
||||
stray:SetSize(233, 224)
|
||||
stray:SetWidth(233); stray:SetHeight(224)
|
||||
stray:SetUnit("player")
|
||||
"#,
|
||||
);
|
||||
@@ -2501,7 +2501,7 @@ mod extract_gate_tests {
|
||||
r#"
|
||||
local plain = CreateFrame("Frame", "Plain")
|
||||
plain:SetPoint("TOPLEFT", 0, 0)
|
||||
plain:SetSize(50, 50)
|
||||
plain:SetWidth(50); plain:SetHeight(50)
|
||||
marker = plain:CreateTexture(nil, "ARTWORK")
|
||||
marker:SetTexture(0, 0, 1)
|
||||
marker:SetAllPoints() -- templateless Lua region: no implicit anchor (1310)
|
||||
@@ -2560,7 +2560,7 @@ mod extract_gate_tests {
|
||||
r#"
|
||||
local plain = CreateFrame("Frame", "Plain")
|
||||
plain:SetPoint("TOPLEFT", 0, 0)
|
||||
plain:SetSize(50, 50)
|
||||
plain:SetWidth(50); plain:SetHeight(50)
|
||||
marker = plain:CreateTexture(nil, "ARTWORK")
|
||||
marker:SetTexture(0, 1, 0)
|
||||
marker:SetAllPoints() -- templateless Lua region: no implicit anchor (1310)
|
||||
@@ -2587,7 +2587,7 @@ mod extract_gate_tests {
|
||||
local function box(name, x, r, g, b)
|
||||
local f = CreateFrame("Frame", name)
|
||||
f:SetPoint("TOPLEFT", x, 0)
|
||||
f:SetSize(50, 50)
|
||||
f:SetWidth(50); f:SetHeight(50)
|
||||
local t = f:CreateTexture(nil, "ARTWORK")
|
||||
t:SetTexture(r, g, b)
|
||||
t:SetAllPoints()
|
||||
|
||||
@@ -920,6 +920,11 @@ mod panel_template_tests;
|
||||
#[cfg(test)]
|
||||
mod shape_gate;
|
||||
|
||||
/// The event ARGUMENT-shape gate (decision 2140) — the same question one API over: the two event
|
||||
/// gates in `reference_ui` compare names, and nothing compared what a fire site pushes.
|
||||
#[cfg(test)]
|
||||
mod event_shape_gate;
|
||||
|
||||
/// The reference's BasicControls.xml — TEXT/message/_ERRORMESSAGE and the ScriptErrors dialog,
|
||||
/// none of which benilla itself calls: every test enters from Lua the way an addon does.
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -771,7 +771,7 @@ fn shipped_panel_slot_pushable_promotes_to_center() {
|
||||
r#"
|
||||
local loot = CreateFrame("Frame", "LootFrame")
|
||||
loot:Hide()
|
||||
loot:SetSize(50, 50)
|
||||
loot:SetWidth(50); loot:SetHeight(50)
|
||||
ShowUIPanel(loot)
|
||||
"#,
|
||||
)
|
||||
@@ -1257,8 +1257,8 @@ fn a_dead_player_opens_only_whiledead_windows() {
|
||||
// Stand-ins for two shipped rows: GossipFrame (no whileDead) and QuestLogFrame (whileDead=1).
|
||||
// Bare frames are enough — the guard runs before any seat is chosen.
|
||||
s.run(
|
||||
r#"local g = CreateFrame("Frame", "GossipFrame") g:SetSize(50, 50) g:Hide()
|
||||
local q = CreateFrame("Frame", "QuestLogFrame") q:SetSize(50, 50) q:Hide()
|
||||
r#"local g = CreateFrame("Frame", "GossipFrame") g:SetWidth(50); g:SetHeight(50) g:Hide()
|
||||
local q = CreateFrame("Frame", "QuestLogFrame") q:SetWidth(50); q:SetHeight(50) q:Hide()
|
||||
ShowUIPanel(GossipFrame)"#,
|
||||
)
|
||||
.unwrap();
|
||||
@@ -1321,9 +1321,9 @@ fn a_frame_arriving_at_center_puts_the_child_windows_away() {
|
||||
// holds left, TradeFrame (pushable 1) then ARRIVES at center (UIParent.lua l.734-741's
|
||||
// else-arm: the incumbent outranks nobody, the newcomer settles at center).
|
||||
s.run(
|
||||
r#"local m = CreateFrame("Frame", "OpenMailFrame") m:SetSize(50, 50)
|
||||
local a = CreateFrame("Frame", "MerchantFrame") a:SetSize(50, 50) a:Hide()
|
||||
local b = CreateFrame("Frame", "TradeFrame") b:SetSize(50, 50) b:Hide()
|
||||
r#"local m = CreateFrame("Frame", "OpenMailFrame") m:SetWidth(50); m:SetHeight(50)
|
||||
local a = CreateFrame("Frame", "MerchantFrame") a:SetWidth(50); a:SetHeight(50) a:Hide()
|
||||
local b = CreateFrame("Frame", "TradeFrame") b:SetWidth(50); b:SetHeight(50) b:Hide()
|
||||
ShowUIPanel(MerchantFrame)
|
||||
ShowUIPanel(TradeFrame)"#,
|
||||
)
|
||||
@@ -1344,7 +1344,7 @@ fn a_frame_arriving_at_center_puts_the_child_windows_away() {
|
||||
s.run(
|
||||
r#"HideUIPanel(TradeFrame) HideUIPanel(MerchantFrame)
|
||||
OpenMailFrame:Show()
|
||||
local l = CreateFrame("Frame", "LootFrame") l:SetSize(50, 50) l:Hide()
|
||||
local l = CreateFrame("Frame", "LootFrame") l:SetWidth(50); l:SetHeight(50) l:Hide()
|
||||
ShowUIPanel(LootFrame)
|
||||
ShowUIPanel(MerchantFrame)"#,
|
||||
)
|
||||
|
||||
@@ -1296,7 +1296,8 @@ fn selection_change_resets_detail_scroll_but_a_quest_log_update_refresh_does_not
|
||||
}
|
||||
|
||||
/// The SHARED item tooltip on a quest reward row (the director's "same tooltip as vendor items"):
|
||||
/// hovering the reward fires the row's OnEnter → `SetItemById` → the ask-once store. First hover
|
||||
/// hovering the reward fires the row's OnEnter → `SetQuestLogItem` → the shared id-keyed render
|
||||
/// → the ask-once store. First hover
|
||||
/// (store cold) shows the fallback name line AND records the ask; after the app's push, a re-hover
|
||||
/// renders the full stat head — the identical lines a vendor row/bag slot gets for this item.
|
||||
#[test]
|
||||
|
||||
@@ -559,7 +559,13 @@ fn spending_an_arrow_starts_quivers_reload_through_the_real_item_feed() {
|
||||
)]))
|
||||
};
|
||||
let mut memory = FeedMemory::default();
|
||||
apply_container_source(&mut s, &mut memory, quiver(200), [0; 10], Vec::new());
|
||||
apply_container_source(
|
||||
&mut s,
|
||||
&mut memory,
|
||||
quiver(200),
|
||||
Default::default(),
|
||||
Vec::new(),
|
||||
);
|
||||
|
||||
s.fire_event("START_AUTOREPEAT_SPELL", Vec::new());
|
||||
s.tick(0.1);
|
||||
@@ -568,7 +574,13 @@ fn spending_an_arrow_starts_quivers_reload_through_the_real_item_feed() {
|
||||
|
||||
// The server tells us the stack is one lighter. That is a fired shot, and the only way the
|
||||
// addon can ever know it.
|
||||
apply_container_source(&mut s, &mut memory, quiver(199), [0; 10], Vec::new());
|
||||
apply_container_source(
|
||||
&mut s,
|
||||
&mut memory,
|
||||
quiver(199),
|
||||
Default::default(),
|
||||
Vec::new(),
|
||||
);
|
||||
s.tick(0.1);
|
||||
|
||||
let (_, reloading, reload_left, _) = shot_state(&mut s);
|
||||
|
||||
@@ -97,7 +97,7 @@ fn app_frame_ticked(s: &mut UiScript, dt: f32) {
|
||||
fn install_changing_tooltip(s: &UiScript, owner: &str, func: &str) {
|
||||
s.run(&format!(
|
||||
r#"
|
||||
local a = CreateFrame("Button", "{owner}"); a:SetPoint("CENTER", 0, 0); a:SetSize(10, 10)
|
||||
local a = CreateFrame("Button", "{owner}"); a:SetPoint("CENTER", 0, 0); a:SetWidth(10); a:SetHeight(10)
|
||||
{func}_n = 0
|
||||
function {func}()
|
||||
{func}_n = {func}_n + 1
|
||||
@@ -534,7 +534,7 @@ fn a_tooltip_line_flipping_wrapped_to_plain_costs_no_graph_derivation() {
|
||||
s.run(
|
||||
r#"
|
||||
FlipOwner = CreateFrame("Button", "FlipOwner"); FlipOwner:SetPoint("CENTER", 0, 0)
|
||||
FlipOwner:SetSize(10, 10)
|
||||
FlipOwner:SetWidth(10); FlipOwner:SetHeight(10)
|
||||
flip_n = 0
|
||||
-- The two shapes a hover alternates between. The trailing `1` on the wrap arm is
|
||||
-- `AddLine`'s positional wrapText flag (the byte-pinned 0x531630 signature) — it is what
|
||||
@@ -606,7 +606,7 @@ fn a_hover_sweep_across_owners_costs_no_graph_derivation() {
|
||||
r#"
|
||||
for i = 1, 12 do
|
||||
local b = CreateFrame("Button", "SweepOwner" .. i)
|
||||
b:SetPoint("CENTER", 0, 0); b:SetSize(10, 10)
|
||||
b:SetPoint("CENTER", 0, 0); b:SetWidth(10); b:SetHeight(10)
|
||||
end
|
||||
sweep_n = 0
|
||||
-- A different owner AND a different line shape every step: the two halves of a real sweep
|
||||
@@ -679,7 +679,7 @@ fn an_action_bar_hover_sweep_costs_no_graph_derivation() {
|
||||
r#"
|
||||
for i = 1, 12 do
|
||||
local b = CreateFrame("Button", "BarOwner" .. i)
|
||||
b:SetPoint("CENTER", 0, 0); b:SetSize(36, 36)
|
||||
b:SetPoint("CENTER", 0, 0); b:SetWidth(36); b:SetHeight(36)
|
||||
end
|
||||
bar_n = 0
|
||||
-- `GameTooltip_SetDefaultAnchor`'s body, which is what every action button actually runs:
|
||||
@@ -754,7 +754,7 @@ fn a_bag_addon_hover_sweep_costs_no_graph_derivation() {
|
||||
r#"
|
||||
for i = 1, 12 do
|
||||
local b = CreateFrame("Button", "BagOwner" .. i)
|
||||
b:SetPoint("CENTER", 0, 0); b:SetSize(37, 37)
|
||||
b:SetPoint("CENTER", 0, 0); b:SetWidth(37); b:SetHeight(37)
|
||||
end
|
||||
bag_n = 0
|
||||
function bag_hover()
|
||||
|
||||
@@ -419,13 +419,37 @@ const WIDGET_PROBES: &[(&str, &str, &str)] = &[
|
||||
("0x87abb0", "ColorSelect", "PGColor"),
|
||||
("0x87b960", "MessageFrame", "PGMessage"),
|
||||
("0x87b5c0", "ScrollingMessageFrame", "PGScrollMsg"),
|
||||
("0x878948", "PlayerModel", "PGModel"),
|
||||
("0x878948", "Model", "PGModel"),
|
||||
// ── The eleven tables this list did NOT cover until 2142 ──────────────────────────────────
|
||||
// Twelve of the reference's twenty-three widget tables were mapped, so 11 classes — 190 of
|
||||
// its 481 widget rows, `GameTooltip`'s 46 among them — were skipped by BOTH the arity gate
|
||||
// and the kind gate, silently and by omission rather than by a stated narrowing. The
|
||||
// identities are the corpus census's (2142), each read off its table's own distinctive
|
||||
// members rather than guessed: `SetOwner`/`AddDoubleLine` is GameTooltip's, `CopyFontObject`
|
||||
// the Font object's, `PingLocation` the Minimap's, `Dress`/`TryOn` DressUpModel's,
|
||||
// `CycleVariation` TabardModel's, `StartMovie` MovieFrame's, `SetSlot` LootButton's.
|
||||
("0x854198", "GameTooltip", "PGTip"),
|
||||
("0x84c538", "Minimap", "PGMinimap"),
|
||||
("0x84ee40", "TabardModel", "PGTabard"),
|
||||
("0x84f190", "DressUpModel", "PGDress"),
|
||||
// `PlayerModel` is `Model` plus three verbs, so its own table needs its own instance — the
|
||||
// `0x878948` row above is now the plain `Model` it always was.
|
||||
("0x84f1fc", "PlayerModel", "PGPlayerModel"),
|
||||
("0x87ab4c", "MovieFrame", "PGMovie"),
|
||||
("0x847ce4", "LootButton", "PGLoot"),
|
||||
// The base map every widget misses into (its 19 names are exactly `script/mod.rs`'s
|
||||
// `REGION_MAP_METHODS`). It has no class of its own, so it is probed on a Frame — which is
|
||||
// what "its own class" means for a base: every widget answers it.
|
||||
("0x87c9b8", "Frame", "PGRegionBase"),
|
||||
];
|
||||
|
||||
/// The two region classes, which are made by a frame rather than by `CreateFrame`.
|
||||
const REGION_PROBES: &[(&str, &str)] = &[
|
||||
("0x87c128", "PGFrame:CreateTexture('PGTex')"),
|
||||
("0x87c1d8", "PGFrame:CreateFontString('PGFS')"),
|
||||
// The FONT OBJECT (`CreateFont`), not a FontString — 22 methods of its own, and the third
|
||||
// thing here that `CreateFrame` cannot make (2142).
|
||||
("0x87c7c8", "CreateFont('PGFontObject')"),
|
||||
];
|
||||
|
||||
/// **The widget half of the gate** (decision 1843) — the half that would have caught 1840's
|
||||
@@ -461,7 +485,7 @@ fn every_widget_method_answers_the_reference_s_return_arity() {
|
||||
}
|
||||
}
|
||||
assert!(
|
||||
made.len() >= 8,
|
||||
made.len() >= 20,
|
||||
"only {} widget classes could be instantiated — the probe set is broken",
|
||||
made.len()
|
||||
);
|
||||
@@ -511,7 +535,10 @@ fn every_widget_method_answers_the_reference_s_return_arity() {
|
||||
}
|
||||
|
||||
assert!(
|
||||
checked >= 60,
|
||||
// 121 at 2142, when the probe set went from 12 of the reference's 23 widget tables to
|
||||
// all 23. The floor it replaces was 60 — set at the coverage of 1843's day, and never
|
||||
// raised, so eleven whole classes could go unprobed without this number moving.
|
||||
checked >= 110,
|
||||
"the widget gate measured only {checked} methods — it has stopped covering anything"
|
||||
);
|
||||
assert!(
|
||||
@@ -788,7 +815,8 @@ fn every_widget_method_answers_the_reference_s_return_kinds() {
|
||||
}
|
||||
|
||||
assert!(
|
||||
checked >= 40,
|
||||
// 119 at 2142 — the same widening. The floor it replaces was 40.
|
||||
checked >= 110,
|
||||
"the widget kind gate measured only {checked} methods — it has stopped covering anything"
|
||||
);
|
||||
assert!(
|
||||
|
||||
@@ -681,7 +681,9 @@ fn no_shipped_script_sets_a_global_string_key_as_display_text() {
|
||||
// — so XML comment blocks are tracked across lines and Lua `--` lines are dropped.
|
||||
fn sink_literals(line: &str) -> Vec<&str> {
|
||||
let mut out = Vec::new();
|
||||
for sink in [":SetText(", ":SetFormattedText(", ":SetButtonText("] {
|
||||
// `:SetFormattedText(` used to sit between these two and never could have matched —
|
||||
// it is not a 1.12 verb, so no shipped script writes it (2142 retired ours).
|
||||
for sink in [":SetText(", ":SetButtonText("] {
|
||||
let mut rest = line;
|
||||
while let Some(at) = rest.find(sink) {
|
||||
rest = &rest[at + sink.len()..];
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
use benilla_protocol::messages::TaxiMask;
|
||||
use bevy::prelude::*;
|
||||
|
||||
use benilla_ui::script::{ScriptValue, TaxiUiState, UiScript};
|
||||
use benilla_ui::script::{TaxiUiState, UiScript};
|
||||
|
||||
use crate::names::NameCache;
|
||||
use crate::net::{ClientCommand, NetCommands};
|
||||
@@ -238,12 +238,14 @@ fn feed_taxi(
|
||||
if fresh != *last || (fresh.is_some() && name_changed) {
|
||||
script.set_taxi(fresh.clone());
|
||||
match (&*last, &fresh) {
|
||||
(None, Some(_)) | (Some(_), Some(_)) => script.fire_event(
|
||||
"TAXIMAP_OPENED",
|
||||
vec![ScriptValue::Str(
|
||||
flightmaster_name.clone().unwrap_or_default(),
|
||||
)],
|
||||
),
|
||||
// **No arguments** — `0x4dba96` is the event's one fire site image-wide and it is a
|
||||
// `FrameScript_SignalEvent 0x703e50`, `__fastcall(ecx = id)` with a plain `ret` and
|
||||
// no vararg push at all. The flight master's name we used to pass was an invention:
|
||||
// `TaxiFrame_OnEvent` reads `UnitName("npc")` for it and never looks at `arg1`
|
||||
// (decision 2140, found by the argument gate).
|
||||
(None, Some(_)) | (Some(_), Some(_)) => {
|
||||
script.fire_event("TAXIMAP_OPENED", Vec::new());
|
||||
}
|
||||
(Some(_), None) => script.fire_event("TAXIMAP_CLOSED", vec![]),
|
||||
(None, None) => {}
|
||||
}
|
||||
|
||||
@@ -294,7 +294,7 @@ mod ellipsis_memo_tests {
|
||||
r#"
|
||||
local f = CreateFrame("Frame", "MemoHost")
|
||||
f:SetPoint("TOPLEFT", 0, 0)
|
||||
f:SetSize(100, 100)
|
||||
f:SetWidth(100); f:SetHeight(100)
|
||||
for i = 1, 8 do
|
||||
local t = f:CreateTexture(nil, "ARTWORK")
|
||||
t:SetTexture(1, 0, 0)
|
||||
|
||||
@@ -966,6 +966,11 @@ pub(crate) fn snapshot(
|
||||
// sets it without being a player.
|
||||
player_controlled: store.0.unit_flags() & 0x8 != 0,
|
||||
flags: store.0.unit_flags(),
|
||||
// The raw `UNIT_DYNAMIC_FLAGS` dword the event of the same name fires on — the `flags`
|
||||
// arm above, a hundred descriptor fields over. `tapped`/`tapped_by_player` up the struct
|
||||
// are two of its bits; this is the whole word, because the reference's watch is a memcmp
|
||||
// over the dword and not a bit test.
|
||||
dynamic_flags: store.0.unit_dynamic_flags(),
|
||||
owner: store
|
||||
.0
|
||||
.unit_summoned_by()
|
||||
@@ -1315,6 +1320,25 @@ pub(crate) fn fire_transitions(
|
||||
if prev.is_some() && changed(|u| u64::from(u.player_flags)) {
|
||||
script.fire_event("PLAYER_FLAGS_CHANGED", vec![tok()]);
|
||||
}
|
||||
// `UNIT_DYNAMIC_FLAGS` (id 137) — the third arm of the same bridge, and one benilla had
|
||||
// **never fired at all**, which is a different failure from firing it wrong: an addon that
|
||||
// registers it hears nothing, forever, and there is no error anywhere to say so.
|
||||
//
|
||||
// Nothing in 1.12 FrameXML registers it, which is exactly why no gate saw the hole — the
|
||||
// producer gate's oracle is what a stock chain file listens for (`reference_ui::
|
||||
// every_event_a_chain_file_registers_has_a_producer`), and no stock file listens for this one.
|
||||
// The corpus does: `CT_UnitFrames/CT_TargetFrame.xml:200` and `TipBuddy/TipBuddy.lua:17`
|
||||
// register it, and both are repaint wires for the tapped/grey-bar state — the same state
|
||||
// `UnitIsTapped` publishes and that the `UNIT_FACTION` arm below repaints the stock frames on.
|
||||
//
|
||||
// Byte-verified, not inherited (see [`UnitState::dynamic_flags`]): the name-table slot for
|
||||
// 137 resolves to `"UNIT_DYNAMIC_FLAGS"`, and the watch length is one dword, so the trigger
|
||||
// is the RAW word and not the four bits this struct decodes. `prev.is_some()` for the same
|
||||
// reason the two arms above carry it — a unit's first snapshot is its CREATE, and the create
|
||||
// block runs no notify pass.
|
||||
if prev.is_some() && changed(|u| u64::from(u.dynamic_flags)) {
|
||||
script.fire_event("UNIT_DYNAMIC_FLAGS", vec![tok()]);
|
||||
}
|
||||
// The POWER pair is named per resource in 1.12, not once with the token as arg2: the reference's
|
||||
// `UnitFrameManaBar_Initialize` registers `UNIT_MANA`/`UNIT_RAGE`/`UNIT_FOCUS`/`UNIT_ENERGY`/
|
||||
// `UNIT_HAPPINESS` and the five `UNIT_MAX*` twins (`UnitFrame.lua:190-199`), and nothing in
|
||||
@@ -2397,6 +2421,60 @@ mod tests {
|
||||
assert!(fired(Some(with(0x1)), with(0x1)).is_empty());
|
||||
}
|
||||
|
||||
/// **`UNIT_DYNAMIC_FLAGS` — an event benilla had never fired at all** (decision 2140).
|
||||
///
|
||||
/// Same bridge, same shape, the third of the three raw-dword arms: id 137 is the unit-window
|
||||
/// index of descriptor field 143, the name-table slot for 137 has exactly one writer in the
|
||||
/// image and it points at the string `"UNIT_DYNAMIC_FLAGS"`, and the watch length is 4 — one
|
||||
/// dword — so the gate is a `repe cmpsb` over the whole word and **any** bit fires it.
|
||||
///
|
||||
/// The control below is bit `0x2` (`UNIT_DYNFLAG_TRACK_UNIT`, Hunter's Mark), which no
|
||||
/// `UnitState` field decodes: an arm driven off `tapped`/`tapped_by_player` passes every other
|
||||
/// assertion here and fails that one.
|
||||
#[test]
|
||||
fn unit_dynamic_flags_fires_per_token_on_any_bit_and_never_on_first_sight() {
|
||||
let fired = |prev: Option<UnitState>, cur: UnitState| -> Vec<String> {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.run(
|
||||
r#"
|
||||
SEEN = {}
|
||||
local f = CreateFrame("Frame")
|
||||
f:RegisterEvent("UNIT_DYNAMIC_FLAGS")
|
||||
f:SetScript("OnEvent", function() table.insert(SEEN, event .. ":" .. arg1) end)
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
fire_transitions(&mut s, "target", prev.as_ref(), &cur);
|
||||
s.eval::<Vec<String>>("return SEEN").unwrap()
|
||||
};
|
||||
let with = |dyn_flags: u32| UnitState {
|
||||
exists: true,
|
||||
has_object: true,
|
||||
dynamic_flags: dyn_flags,
|
||||
tapped: dyn_flags & 0x4 != 0,
|
||||
tapped_by_player: dyn_flags & 0x8 != 0,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
// The bit the corpus registers this event for: `0x4` TAPPED, the grey-bar state
|
||||
// (`CT_UnitFrames/CT_TargetFrame.xml:200`, `TipBuddy/TipBuddy.lua:17`).
|
||||
assert_eq!(
|
||||
fired(Some(with(0)), with(0x4)),
|
||||
vec!["UNIT_DYNAMIC_FLAGS:target".to_string()],
|
||||
"arg1 is the unit token, and there is no arg2"
|
||||
);
|
||||
// The control that proves the trigger is the RAW DWORD: a bit this struct never decodes.
|
||||
assert_eq!(
|
||||
fired(Some(with(0)), with(0x2)),
|
||||
vec!["UNIT_DYNAMIC_FLAGS:target".to_string()],
|
||||
"an undecoded bit still fires it — the watch is a memcmp over the dword"
|
||||
);
|
||||
// The create block runs no notify pass (1098 §4), like both arms beside it.
|
||||
assert!(fired(None, with(0x4)).is_empty());
|
||||
// And the control that stops the rest passing by firing always.
|
||||
assert!(fired(Some(with(0x4)), with(0x4)).is_empty());
|
||||
}
|
||||
|
||||
/// **The two SELF-ONLY arms of the same handler, each on its own bits** — the half that was
|
||||
/// over-firing, and the half that was missing (decision 2078).
|
||||
///
|
||||
|
||||
@@ -74,6 +74,30 @@ fn row(sender: &str, subject: &str, was_read: bool, item_id: u32, cod: u32) -> M
|
||||
}
|
||||
}
|
||||
|
||||
/// One Linen Cloth in the backpack — the send tab's attachment fixture. `ClickSendMailItemButton`
|
||||
/// reads the cursor, and `GetSendMailItem` answers off the bag slot it came from, so the row needs
|
||||
/// a real name and texture for the stock tab to title the letter with.
|
||||
fn one_linen_backpack() -> benilla_ui::script::ContainerState {
|
||||
let mut slots = std::collections::HashMap::new();
|
||||
slots.insert(
|
||||
1,
|
||||
benilla_ui::script::ContainerSlot {
|
||||
item_id: 2589,
|
||||
count: 20,
|
||||
quality: Some(1),
|
||||
texture: Some("Interface\\Icons\\INV_Fabric_Linen_01".into()),
|
||||
link: Some("|cffffffff|Hitem:2589|h[Linen Cloth]|h|r".into()),
|
||||
bar_placeable: true,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
benilla_ui::script::ContainerState {
|
||||
name: Some("Backpack".into()),
|
||||
num_slots: 16,
|
||||
slots,
|
||||
}
|
||||
}
|
||||
|
||||
/// A one-page inbox (2 mails).
|
||||
fn small_inbox() -> MailState {
|
||||
MailState {
|
||||
@@ -256,7 +280,7 @@ fn a_letter_and_the_centre_occupant_evict_each_other() {
|
||||
|
||||
// The director's setup: mailbox left, character sheet pushed to centre beside it.
|
||||
s.run(
|
||||
r#"local c = CreateFrame("Frame", "CharacterFrame") c:SetSize(50, 50) c:Hide()
|
||||
r#"local c = CreateFrame("Frame", "CharacterFrame") c:SetWidth(50); c:SetHeight(50) c:Hide()
|
||||
ShowUIPanel(CharacterFrame)"#,
|
||||
)
|
||||
.unwrap();
|
||||
@@ -745,3 +769,114 @@ fn the_open_letters_ring_icon_is_masked_but_the_inboxs_is_not() {
|
||||
got {mail_icon:?}"
|
||||
);
|
||||
}
|
||||
|
||||
/// **"I can't even type anything in the mailbox window"** — the director's report, at its cause.
|
||||
///
|
||||
/// The send tab has five edit boxes, and exactly one of them — `SendMailNameEditBox` — carries an
|
||||
/// XML `<OnChar>` (`SendMailFrame_SendeeAutocomplete`). That handler auto-enables the box for the
|
||||
/// keyboard walk, and the box is the first-registered keyboard frame in the window, so on every
|
||||
/// keystroke the walk reached it first and the **base** `CSimpleFrame::OnChar` gate consumed:
|
||||
/// script present → fire → stop. Nothing downstream of it ever saw a character. The reference
|
||||
/// cannot do that — `CSimpleEditBox` replaces slot `+0x5c` with `0x77a900`, which asks about focus
|
||||
/// and declines (`0x77a956`) when another box owns it, and never chains to the base gate at all
|
||||
/// ([`benilla_ui`]'s `script::keyboard::is_editbox`, decision 2145).
|
||||
///
|
||||
/// The one box that DID work is the assertion's control: the autocomplete box types, once per
|
||||
/// character, and the four that were dead now type too.
|
||||
#[test]
|
||||
fn every_send_tab_box_takes_a_keystroke_not_just_the_one_with_an_onchar() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
load_ui(&s);
|
||||
s.fire_event("MAIL_SHOW", vec![]);
|
||||
s.run("MailFrameTab_OnClick(2)").unwrap();
|
||||
s.resolve();
|
||||
assert!(s.eval::<bool>("return SendMailFrame:IsVisible()").unwrap());
|
||||
|
||||
// The box whose handler was eating everyone else's keys: it still types, and its OnChar still
|
||||
// fires exactly once per character (the insert path's fire — never the walk's).
|
||||
s.run("SendMailNameEditBox:SetText('') SendMailNameEditBox:SetFocus()")
|
||||
.unwrap();
|
||||
s.run("SendMailChars = 0 \
|
||||
SendMailNameEditBox:SetScript('OnChar', function() SendMailChars = SendMailChars + 1 end)")
|
||||
.unwrap();
|
||||
for c in ["T", "h", "r"] {
|
||||
assert!(s.char_input(c), "the focused box consumes");
|
||||
}
|
||||
assert_eq!(
|
||||
s.eval::<String>("return SendMailNameEditBox:GetText()")
|
||||
.unwrap(),
|
||||
"Thr"
|
||||
);
|
||||
assert_eq!(
|
||||
s.eval::<i64>("return SendMailChars").unwrap(),
|
||||
3,
|
||||
"OnChar fires once per character, from the insert — not a second time from the walk"
|
||||
);
|
||||
|
||||
// …and the four that took nothing at all. The money boxes are `numeric`, so they get digits.
|
||||
for (box_name, typed, expect) in [
|
||||
("SendMailSubjectEditBox", ["H", "e", "y"], "Hey"),
|
||||
("SendMailBodyEditBox", ["o", "d", "d"], "odd"),
|
||||
("SendMailMoneyGold", ["1", "2", "3"], "123"),
|
||||
("SendMailMoneySilver", ["4", "5", "5"], "45"), // letters="2"
|
||||
] {
|
||||
s.run(&format!("{box_name}:SetText('') {box_name}:SetFocus()"))
|
||||
.unwrap();
|
||||
for c in typed {
|
||||
assert!(s.char_input(c), "{box_name} consumes the keystroke");
|
||||
}
|
||||
assert_eq!(
|
||||
s.eval::<String>(&format!("return {box_name}:GetText()"))
|
||||
.unwrap(),
|
||||
expect,
|
||||
"{box_name} takes what was typed into it"
|
||||
);
|
||||
}
|
||||
assert!(s.take_errors().is_empty(), "and nothing raised on the way");
|
||||
}
|
||||
|
||||
/// **"Sending an item still leaves the subject and the item image in the input"** — the director's
|
||||
/// second report, at its cause: the compose-tab reset used to fire `MAIL_SEND_SUCCESS` *before* it
|
||||
/// dropped the attachment, so the stock `SendMailFrame_Reset`'s own tail
|
||||
/// (`SendMailFrame_Update` → `GetSendMailItem`) painted the just-sent item straight back into the
|
||||
/// form it had blanked one line earlier. `UiScript::reset_compose_tab` is now `0x4acdc0(1)` whole —
|
||||
/// zero the globals, THEN tail-fire the three events — so there is no window in which the two
|
||||
/// disagree (decision 2145).
|
||||
#[test]
|
||||
fn the_compose_reset_clears_the_subject_and_the_attachment_together() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
load_ui(&s);
|
||||
s.set_container(0, Some(one_linen_backpack()));
|
||||
s.fire_event("MAIL_SHOW", vec![]);
|
||||
s.run("MailFrameTab_OnClick(2)").unwrap();
|
||||
|
||||
// Attach the item the way the player does: pick it up, click the send slot. The stock tab
|
||||
// names the letter after it, which is the text that used to survive the send.
|
||||
s.run("PickupContainerItem(0, 1) ClickSendMailItemButton()")
|
||||
.unwrap();
|
||||
s.fire_event("MAIL_SEND_INFO_UPDATE", vec![]);
|
||||
assert_eq!(
|
||||
s.eval::<String>("return SendMailSubjectEditBox:GetText()")
|
||||
.unwrap(),
|
||||
"Linen Cloth (20)",
|
||||
"the stock tab titles the letter after its attachment"
|
||||
);
|
||||
|
||||
// The send lands. This is the whole edge: one call, and the form is clean when the FrameXML
|
||||
// handler reads it.
|
||||
s.reset_compose_tab();
|
||||
assert_eq!(
|
||||
s.eval::<String>("return SendMailSubjectEditBox:GetText()")
|
||||
.unwrap(),
|
||||
"",
|
||||
"the subject does not come back from the attachment that just left"
|
||||
);
|
||||
assert!(
|
||||
s.eval::<bool>("return GetSendMailItem() == nil").unwrap(),
|
||||
"and the attachment is gone with it"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
//! `dump_widget_methods` — print benilla's widget METHOD surface, asked of a real VM.
|
||||
//!
|
||||
//! ```text
|
||||
//! cargo run -q -p benilla-ui --example dump_widget_methods # class<TAB>method, one per line
|
||||
//! ```
|
||||
//!
|
||||
//! The sibling of [`dump_globals`](../dump_globals.rs). The measurement itself lives in
|
||||
//! `benilla_ui::script::widget_method_census` — its module doc is the *why*, and the widget-surface
|
||||
//! gate (decision 2142) reads the same function, so the instrument you look at by hand and the gate
|
||||
//! that fails the build can never disagree about what our surface is.
|
||||
use benilla_ui::script::{widget_method_census, UiScript};
|
||||
|
||||
fn main() -> mlua::Result<()> {
|
||||
for (class, method) in widget_method_census(&UiScript::new()?)? {
|
||||
println!("{class}\t{method}");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -1001,7 +1001,7 @@ mod loader_tests {
|
||||
s.resolve();
|
||||
s.mouse_button(50.0, 10.0, "LeftButton", true);
|
||||
s.mouse_button(50.0, 10.0, "LeftButton", false);
|
||||
assert!(s.eval::<bool>("return XmlEdit:HasFocus()").unwrap());
|
||||
assert_eq!(s.focused_editbox_name().as_deref(), Some("XmlEdit"));
|
||||
s.run("typed = false").unwrap();
|
||||
assert!(s.char_input("7"));
|
||||
s.tick(0.0);
|
||||
@@ -1039,8 +1039,7 @@ mod loader_tests {
|
||||
))
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
s.eval::<bool>(&format!("return {name}:HasFocus()"))
|
||||
.unwrap(),
|
||||
s.focused_editbox_name().as_deref() == Some(name),
|
||||
want,
|
||||
"{name}: autoFocus should be {want} (absent = the ctor default, ON)",
|
||||
);
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
|
||||
use mlua::{Lua, MultiValue, Value};
|
||||
|
||||
use super::binding_abi::{bool_or_default, number_arg, predicate};
|
||||
use super::binding_abi::{bool_or_default, flag, number_arg};
|
||||
use super::Model;
|
||||
|
||||
/// One queue slot as the app pushes it — the reference's `0x20`-byte slot (§2.2) plus the map
|
||||
@@ -371,7 +371,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
"CanJoinBattlefieldAsGroup",
|
||||
lua.create_function(|lua, ()| {
|
||||
let model = lua.app_data_ref::<Model>().expect("model app_data");
|
||||
Ok(predicate(model.battlefield_list.group_queue))
|
||||
Ok(flag(model.battlefield_list.group_queue))
|
||||
})?,
|
||||
)?;
|
||||
|
||||
|
||||
@@ -272,52 +272,6 @@ fn trim_g(s: &str) -> &str {
|
||||
s.trim_end_matches('0').trim_end_matches('.')
|
||||
}
|
||||
|
||||
/// A **predicate's return**: the NUMBER `1` for true, `nil` for false — never a Lua boolean.
|
||||
///
|
||||
/// The return-side counterpart to [`bool_or_default`], and the same class of fact: 1.12's widget
|
||||
/// predicates (1830) and its 29 `Unit*` predicates (2043) do not push booleans. `lua_pushboolean
|
||||
/// 0x6f39f0` has seven call sites in the whole image; **not one is inside a widget registrar body,
|
||||
/// and no binding in the 83-entry unit table at `0x850438` calls it at all** — those predicates go
|
||||
/// through `lua_pushnumber 0x6f3810` (the constant double `1.0`) / `lua_pushnil 0x6f37f0`, exactly
|
||||
/// one value at every live `ret`.
|
||||
///
|
||||
/// **Not a whole-image absolute, and 2048 corrects 2043 for saying so.** `lua_pushboolean` is real
|
||||
/// and one of its seven callers *is* a registered FrameScript binding: `IsPetAttackActive
|
||||
/// 0x4be0e0` answers a genuine Lua `true`/`false` and **never nil** (ours matches — see
|
||||
/// [`super::pet`]). The other six are `rawequal`/`pcall`/`xpcall` in the base table `0x811e28` and
|
||||
/// two unregistered helpers. So "1.12 has no `lua_pushboolean`" is false; "these two predicate
|
||||
/// families never reach it" is what the bytes support, and is what this helper encodes.
|
||||
///
|
||||
/// **The false leg is settled per body, never inherited from a neighbour.** `IsEnabled 0x7800b0`
|
||||
/// is number-`1`/number-`0`-and-never-nil (`setne` + `fild`), which is what makes
|
||||
/// `FriendsFrame.lua:404`'s `== 0` live code — so it does NOT come through here. Inside the unit
|
||||
/// table itself the *numeric getters* answer the number `0.0` where the predicates answer `nil`
|
||||
/// (`UnitLevel 0x518144`, `UnitMana 0x5177c8`, `UnitXP 0x5173f5`, ~15 more; `UnitSex 0x517f9f`
|
||||
/// pushes `2.0`).
|
||||
///
|
||||
/// **Truthiness hides the difference and direct comparison inverts it**, which is why this survived
|
||||
/// so long. `if frame:IsVisible()` reads the same either way; `if frame:IsVisible() == nil` does
|
||||
/// not — under a boolean a hidden frame answers `false`, and `false == nil` is FALSE, so the caller
|
||||
/// concludes the frame is visible exactly when it is not. The 1.12 addon corpus has 21 such direct
|
||||
/// comparisons (`IsVisible() == nil` ×9, `GetChecked() == 1` ×6, `IsVisible() ~= nil` ×4, and one
|
||||
/// each of `~= 1` / `== 1`) across Questie, AtlasQuest, CT_BagMod, MikScrollingBattleText's options
|
||||
/// and `_dl`.
|
||||
///
|
||||
/// The reference proves its own shape without needing the bytes: stock `UIOptionsFrame.xml:310`
|
||||
/// saves a checkbox as `SHOW_BUFF_DURATIONS = tostring(this:GetChecked())` and stock
|
||||
/// `BuffFrame.lua:71` reads it back as `== "1"`. That round-trip only closes if `GetChecked`
|
||||
/// returns the number 1 — `tostring(true)` is `"true"`, and buff timers would never appear.
|
||||
///
|
||||
/// Adopting it is strictly safer than what it replaces: every `if x` and `not x` site reads
|
||||
/// identically, and only the direct comparisons change — from wrong to right.
|
||||
pub(crate) fn predicate(b: bool) -> Value {
|
||||
if b {
|
||||
Value::Integer(1)
|
||||
} else {
|
||||
Value::Nil
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn bool_or_default(v: Option<&Value>, default: bool) -> bool {
|
||||
let Some(v) = v else {
|
||||
return default; // LUA_TNONE
|
||||
@@ -373,6 +327,52 @@ pub(crate) fn bool_or_default(v: Option<&Value>, default: bool) -> bool {
|
||||
///
|
||||
/// Enforced, not remembered: `ui_script::shape_gate::no_query_binding_answers_a_lua_boolean`
|
||||
/// probes the whole registered query surface and fails on any Lua boolean.
|
||||
///
|
||||
/// ## What `predicate` said, folded in (2142)
|
||||
///
|
||||
/// This helper had a byte-identical TWIN sixty lines up the same file — `predicate`, 49 call
|
||||
/// sites to this one's 77 — carrying its own half of the evidence. 2118 consolidated five *local*
|
||||
/// copies into this one and left that one standing, which is the same drift one level up: a house
|
||||
/// rule with two homes gets re-derived at each new file, and the file that picks the wrong one is
|
||||
/// invisible against the others. The twin is gone; everything it recorded is below.
|
||||
///
|
||||
///
|
||||
/// The return-side counterpart to [`bool_or_default`], and the same class of fact: 1.12's widget
|
||||
/// predicates (1830) and its 29 `Unit*` predicates (2043) do not push booleans. `lua_pushboolean
|
||||
/// 0x6f39f0` has seven call sites in the whole image; **not one is inside a widget registrar body,
|
||||
/// and no binding in the 83-entry unit table at `0x850438` calls it at all** — those predicates go
|
||||
/// through `lua_pushnumber 0x6f3810` (the constant double `1.0`) / `lua_pushnil 0x6f37f0`, exactly
|
||||
/// one value at every live `ret`.
|
||||
///
|
||||
/// **Not a whole-image absolute, and 2048 corrects 2043 for saying so.** `lua_pushboolean` is real
|
||||
/// and one of its seven callers *is* a registered FrameScript binding: `IsPetAttackActive
|
||||
/// 0x4be0e0` answers a genuine Lua `true`/`false` and **never nil** (ours matches — see
|
||||
/// [`super::pet`]). The other six are `rawequal`/`pcall`/`xpcall` in the base table `0x811e28` and
|
||||
/// two unregistered helpers. So "1.12 has no `lua_pushboolean`" is false; "these two predicate
|
||||
/// families never reach it" is what the bytes support, and is what this helper encodes.
|
||||
///
|
||||
/// **The false leg is settled per body, never inherited from a neighbour.** `IsEnabled 0x7800b0`
|
||||
/// is number-`1`/number-`0`-and-never-nil (`setne` + `fild`), which is what makes
|
||||
/// `FriendsFrame.lua:404`'s `== 0` live code — so it does NOT come through here. Inside the unit
|
||||
/// table itself the *numeric getters* answer the number `0.0` where the predicates answer `nil`
|
||||
/// (`UnitLevel 0x518144`, `UnitMana 0x5177c8`, `UnitXP 0x5173f5`, ~15 more; `UnitSex 0x517f9f`
|
||||
/// pushes `2.0`).
|
||||
///
|
||||
/// **Truthiness hides the difference and direct comparison inverts it**, which is why this survived
|
||||
/// so long. `if frame:IsVisible()` reads the same either way; `if frame:IsVisible() == nil` does
|
||||
/// not — under a boolean a hidden frame answers `false`, and `false == nil` is FALSE, so the caller
|
||||
/// concludes the frame is visible exactly when it is not. The 1.12 addon corpus has 21 such direct
|
||||
/// comparisons (`IsVisible() == nil` ×9, `GetChecked() == 1` ×6, `IsVisible() ~= nil` ×4, and one
|
||||
/// each of `~= 1` / `== 1`) across Questie, AtlasQuest, CT_BagMod, MikScrollingBattleText's options
|
||||
/// and `_dl`.
|
||||
///
|
||||
/// The reference proves its own shape without needing the bytes: stock `UIOptionsFrame.xml:310`
|
||||
/// saves a checkbox as `SHOW_BUFF_DURATIONS = tostring(this:GetChecked())` and stock
|
||||
/// `BuffFrame.lua:71` reads it back as `== "1"`. That round-trip only closes if `GetChecked`
|
||||
/// returns the number 1 — `tostring(true)` is `"true"`, and buff timers would never appear.
|
||||
///
|
||||
/// Adopting it is strictly safer than what it replaces: every `if x` and `not x` site reads
|
||||
/// identically, and only the direct comparisons change — from wrong to right.
|
||||
pub(crate) fn flag(b: bool) -> Value {
|
||||
if b {
|
||||
Value::Integer(1)
|
||||
|
||||
@@ -393,7 +393,11 @@ fn ensure_slot(lua: &Lua, this: &Table, slot: Slot) -> mlua::Result<u32> {
|
||||
model.region_data.insert(
|
||||
rh,
|
||||
RegionData {
|
||||
additive: slot == Slot::Highlight,
|
||||
blend: if slot == Slot::Highlight {
|
||||
crate::script::BlendMode::Add
|
||||
} else {
|
||||
crate::script::BlendMode::default()
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
@@ -934,6 +938,40 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
},
|
||||
)?,
|
||||
)?;
|
||||
// GetTextColor() → r, g, b, a — **FOUR values** (`0x781100`, table `0x879d00`, argc 1, arity 4,
|
||||
// kinds `(number,number,number,number)`), the same shape as the FontString/Font-object twins
|
||||
// (`0x79d840` table `0x87c1d8`, `0x79f680`) this codebase already answers. Three is the
|
||||
// plausible wrong answer, and the shapes table calls the name `name-not-unique` precisely
|
||||
// because it is registered on seven tables; this is the Button one.
|
||||
//
|
||||
// **Read like `GetFont` right above, not like `SetTextColor` right above that.** The reference
|
||||
// reads the colour back off the button's NORMAL embedded `CSimpleFont`, and an unset local
|
||||
// colour there still resolves through what the normal state INHERITS
|
||||
// (`<NormalFont>`/`SetTextFontObject`) — which is how a `GameMenuButtonTemplate` button answers
|
||||
// `GameFontNormal`'s colour before anything calls `SetTextColor`. Falling back to white would
|
||||
// be wrong in exactly the case the corpus cares about: reading a stock button's colour to
|
||||
// restore it after a temporary recolour. White is the last resort, matching every other colour
|
||||
// getter here (`GetVertexColor`, `FontString:GetTextColor`) — the untinted default a region
|
||||
// with no colour anywhere draws at.
|
||||
//
|
||||
// Completeness rather than a live break: the census found no corpus site with a Button
|
||||
// receiver (every measured `GetTextColor` call is on a FontString or a font object). It is here
|
||||
// because the reference registers it and the widget shape gate can then cover it.
|
||||
m.set(
|
||||
"GetTextColor",
|
||||
lua.create_function(|lua, this: Table| {
|
||||
let (own, inherits) =
|
||||
with_button(lua, &this, |bs| (bs.normal_color, bs.normal_font.clone()))?;
|
||||
let c = own.unwrap_or_else(|| {
|
||||
let model = lua.app_data_ref::<Model>().expect("model app_data");
|
||||
inherits
|
||||
.and_then(|n| model.font_object(&n))
|
||||
.and_then(|f| f.color)
|
||||
.unwrap_or([1.0, 1.0, 1.0, 1.0])
|
||||
});
|
||||
Ok((c[0], c[1], c[2], c[3]))
|
||||
})?,
|
||||
)?;
|
||||
m.set(
|
||||
"SetHighlightTextColor",
|
||||
lua.create_function(
|
||||
@@ -1006,9 +1044,12 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
// `guildButtonDisabled` read BOTH spellings and said so); those workarounds go with this
|
||||
// change, which is 1719's bar — a prior call revisited because a new measurement arrived.
|
||||
//
|
||||
// Slider carries its own `IsEnabled` (slider.rs) and is deliberately NOT changed here: the
|
||||
// note above is the BUTTON method table, and whether the slider widget shares the contract is
|
||||
// unverified. Assuming they match is exactly the guess this comment exists to have avoided.
|
||||
// Slider used to carry its own `IsEnabled` (slider.rs), left alone here because "whether the
|
||||
// slider widget shares the contract is unverified". It does not share it — it does not HAVE
|
||||
// one: 1.12 registers `Enable`/`Disable`/`IsEnabled` on this table alone, and the Slider's
|
||||
// LoadXML `0x789580` has no `enabled` attribute either, so ours was a superset. The trio came
|
||||
// off the Slider rather than being brought into line with this contract; see the note at its
|
||||
// old site in `slider.rs`.
|
||||
m.set(
|
||||
"IsEnabled",
|
||||
lua.create_function(|lua, this: Table| {
|
||||
@@ -1144,7 +1185,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
"GetChecked",
|
||||
lua.create_function(|lua, this: Table| {
|
||||
let checked = with_button(lua, &this, |bs| bs.checked)?;
|
||||
Ok(crate::script::binding_abi::predicate(checked))
|
||||
Ok(crate::script::binding_abi::flag(checked))
|
||||
})?,
|
||||
)?;
|
||||
lua.set_named_registry_value(REG_CHECKBUTTON_METHODS, c)?;
|
||||
|
||||
@@ -137,13 +137,13 @@ mod tests {
|
||||
click_a, click_b = 0, 0
|
||||
drag_button = nil
|
||||
local a = CreateFrame("Frame", "A")
|
||||
a:SetPoint("BOTTOMLEFT", 0, 0); a:SetSize(400, 600); a:EnableMouse(true)
|
||||
a:SetPoint("BOTTOMLEFT", 0, 0); a:SetWidth(400); a:SetHeight(600); a:EnableMouse(true)
|
||||
a:RegisterForDrag("LeftButton")
|
||||
a:SetScript("OnDragStart", function(self, button) drag_starts = drag_starts + 1; drag_button = button end)
|
||||
a:SetScript("OnDragStop", function(self) drag_stops = drag_stops + 1 end)
|
||||
a:SetScript("OnClick", function(self) click_a = click_a + 1 end)
|
||||
local b = CreateFrame("Frame", "B")
|
||||
b:SetPoint("BOTTOMLEFT", 400, 0); b:SetSize(400, 600); b:EnableMouse(true)
|
||||
b:SetPoint("BOTTOMLEFT", 400, 0); b:SetWidth(400); b:SetHeight(600); b:EnableMouse(true)
|
||||
b:SetScript("OnReceiveDrag", function(self) receives = receives + 1 end)
|
||||
b:SetScript("OnClick", function(self) click_b = click_b + 1 end)
|
||||
"#,
|
||||
@@ -201,7 +201,7 @@ mod tests {
|
||||
r#"
|
||||
clicks = 0
|
||||
local a = CreateFrame("Frame", "A")
|
||||
a:SetPoint("BOTTOMLEFT", 0, 0); a:SetSize(400, 600); a:EnableMouse(true)
|
||||
a:SetPoint("BOTTOMLEFT", 0, 0); a:SetWidth(400); a:SetHeight(600); a:EnableMouse(true)
|
||||
a:RegisterForDrag("LeftButton")
|
||||
a:SetScript("OnClick", function(self) clicks = clicks + 1 end)
|
||||
"#,
|
||||
@@ -223,7 +223,7 @@ mod tests {
|
||||
r#"
|
||||
clicks = 0
|
||||
local a = CreateFrame("Frame", "A")
|
||||
a:SetPoint("BOTTOMLEFT", 0, 0); a:SetSize(400, 600); a:EnableMouse(true)
|
||||
a:SetPoint("BOTTOMLEFT", 0, 0); a:SetWidth(400); a:SetHeight(600); a:EnableMouse(true)
|
||||
a:RegisterForDrag("LeftButton")
|
||||
a:SetScript("OnClick", function(self) clicks = clicks + 1 end)
|
||||
"#,
|
||||
@@ -252,7 +252,7 @@ mod tests {
|
||||
r#"
|
||||
heard = 0
|
||||
local a = CreateFrame("Frame", "A")
|
||||
a:SetPoint("BOTTOMLEFT", 0, 0); a:SetSize(400, 600); a:EnableMouse(true)
|
||||
a:SetPoint("BOTTOMLEFT", 0, 0); a:SetWidth(400); a:SetHeight(600); a:EnableMouse(true)
|
||||
a:RegisterForDrag("LeftButton")
|
||||
local f = CreateFrame("Frame", "Listener")
|
||||
f:RegisterEvent("DELETE_ITEM_CONFIRM")
|
||||
@@ -395,7 +395,7 @@ mod tests {
|
||||
WorldFrame = CreateFrame("WorldFrame", "WorldFrame") WorldFrame:SetAllPoints()
|
||||
heard = 0
|
||||
local a = CreateFrame("Frame", "A")
|
||||
a:SetPoint("BOTTOMLEFT", 0, 0); a:SetSize(400, 600); a:EnableMouse(true)
|
||||
a:SetPoint("BOTTOMLEFT", 0, 0); a:SetWidth(400); a:SetHeight(600); a:EnableMouse(true)
|
||||
local f = CreateFrame("Frame", "Listener")
|
||||
f:RegisterEvent("DELETE_ITEM_CONFIRM")
|
||||
f:SetScript("OnEvent", function() heard = heard + 1 end)
|
||||
@@ -502,7 +502,7 @@ mod tests {
|
||||
r#"
|
||||
heard = 0
|
||||
local a = CreateFrame("Frame", "A")
|
||||
a:SetPoint("BOTTOMLEFT", 0, 0); a:SetSize(400, 600); a:EnableMouse(true)
|
||||
a:SetPoint("BOTTOMLEFT", 0, 0); a:SetWidth(400); a:SetHeight(600); a:EnableMouse(true)
|
||||
local f = CreateFrame("Frame", "Listener")
|
||||
f:RegisterEvent("DELETE_ITEM_CONFIRM")
|
||||
f:SetScript("OnEvent", function() heard = heard + 1 end)
|
||||
@@ -538,7 +538,7 @@ mod tests {
|
||||
r#"
|
||||
heard = 0
|
||||
local a = CreateFrame("Frame", "A")
|
||||
a:SetPoint("BOTTOMLEFT", 0, 0); a:SetSize(400, 600); a:EnableMouse(true)
|
||||
a:SetPoint("BOTTOMLEFT", 0, 0); a:SetWidth(400); a:SetHeight(600); a:EnableMouse(true)
|
||||
a:RegisterForDrag("LeftButton")
|
||||
local f = CreateFrame("Frame", "Listener")
|
||||
f:RegisterEvent("DELETE_ITEM_CONFIRM")
|
||||
@@ -587,7 +587,7 @@ mod tests {
|
||||
r#"
|
||||
stops = 0
|
||||
local a = CreateFrame("Frame", "A")
|
||||
a:SetPoint("BOTTOMLEFT", 100, 100); a:SetSize(200, 100)
|
||||
a:SetPoint("BOTTOMLEFT", 100, 100); a:SetWidth(200); a:SetHeight(100)
|
||||
a:EnableMouse(true); a:SetMovable(true)
|
||||
a:RegisterForDrag("LeftButton")
|
||||
a:SetScript("OnDragStart", function() this:StartMoving() end)
|
||||
@@ -637,7 +637,7 @@ mod tests {
|
||||
r#"
|
||||
stops, starts = 0, 0
|
||||
local a = CreateFrame("Frame", "A")
|
||||
a:SetPoint("BOTTOMLEFT", 100, 100); a:SetSize(200, 100)
|
||||
a:SetPoint("BOTTOMLEFT", 100, 100); a:SetWidth(200); a:SetHeight(100)
|
||||
a:EnableMouse(true); a:SetMovable(true)
|
||||
a:RegisterForDrag("LeftButton")
|
||||
a:SetScript("OnDragStart", function() starts = starts + 1; this:StartMoving() end)
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
use mlua::{Lua, Value};
|
||||
|
||||
use super::{queue_cursor_update, CursorMoney, CursorPayload};
|
||||
use crate::script::binding_abi::{number_arg, predicate};
|
||||
use crate::script::binding_abi::{flag, number_arg};
|
||||
use crate::script::Model;
|
||||
|
||||
/// The coin icon for an amount of copper — `GetCoinIcon 0x48d4e0`'s table, SIGNED thresholds
|
||||
@@ -161,10 +161,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
"CursorHasMoney",
|
||||
lua.create_function(|lua, ()| {
|
||||
let model = lua.app_data_ref::<Model>().expect("model app_data");
|
||||
Ok(predicate(matches!(
|
||||
model.cursor,
|
||||
Some(CursorPayload::Money(_))
|
||||
)))
|
||||
Ok(flag(matches!(model.cursor, Some(CursorPayload::Money(_)))))
|
||||
})?,
|
||||
)?;
|
||||
g.set(
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
|
||||
use mlua::{Lua, Value};
|
||||
|
||||
use super::binding_abi::{bool_or_default, coerced_number, predicate};
|
||||
use super::binding_abi::{bool_or_default, coerced_number, flag};
|
||||
use super::Model;
|
||||
|
||||
/// The area spirit healer's aura, `0xA18` — the one spell id `CancelAreaSpiritHeal` cancels, and
|
||||
@@ -172,7 +172,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
"IsInMeetingStoneQueue",
|
||||
lua.create_function(|lua, ()| {
|
||||
let model = lua.app_data_ref::<Model>().expect("model app_data");
|
||||
Ok(predicate(model.meeting_stone_area != 0))
|
||||
Ok(flag(model.meeting_stone_area != 0))
|
||||
})?,
|
||||
)?;
|
||||
|
||||
@@ -195,7 +195,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
"CheckPetUntrainerDist",
|
||||
lua.create_function(|lua, ()| {
|
||||
let model = lua.app_data_ref::<Model>().expect("model app_data");
|
||||
Ok(predicate(model.pet_untrainer_pending))
|
||||
Ok(flag(model.pet_untrainer_pending))
|
||||
})?,
|
||||
)?;
|
||||
// §9: the confirm arm — the latch's guid, the money gate (`ERR_NOT_ENOUGH_MONEY`, no packet)
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
use mlua::{Lua, Table, Value};
|
||||
|
||||
use crate::script::object::frame_handle_of;
|
||||
use crate::script::Model;
|
||||
use crate::widget::EditBoxState;
|
||||
|
||||
use super::{
|
||||
@@ -127,14 +126,12 @@ pub(in crate::script) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
Ok(())
|
||||
})?,
|
||||
)?;
|
||||
m.set(
|
||||
"HasFocus",
|
||||
lua.create_function(|lua, this: Table| {
|
||||
let h = frame_handle_of(lua, &this)?;
|
||||
let model = lua.app_data_ref::<Model>().expect("model app_data");
|
||||
Ok(model.focused_editbox == Some(h))
|
||||
})?,
|
||||
)?;
|
||||
// **No `HasFocus`.** 1.12's EditBox table registers the two setters above and no getter at
|
||||
// all, and an addon that wants the answer keeps its own flag around `SetFocus`/`ClearFocus` —
|
||||
// `pfQuest/browser.lua:760` documents exactly that and ships the workaround. Ours was an
|
||||
// unexplained superset a feature-detecting addon would take the wrong branch on (1188, and
|
||||
// the census that removed it, 2142). Host-side, the focus cell reads back through
|
||||
// [`crate::script::UiScript::focused_editbox_name`].
|
||||
|
||||
// GetInputLanguage() / ToggleInputLanguage() — `0x799550` / `0x799610`, the edit box's
|
||||
// **IME** language, not the chat language: `ChatEdit_OnInputLanguageChanged` shows
|
||||
|
||||
@@ -32,7 +32,7 @@ fn an_autofocus_box_self_acquires_on_the_first_event() {
|
||||
.unwrap();
|
||||
// No show transition has run for this box, so nothing owns the keyboard yet.
|
||||
assert!(!s.has_keyboard_focus());
|
||||
assert!(!s.eval::<bool>("return E:HasFocus()").unwrap());
|
||||
assert_eq!(s.focused_editbox_name(), None);
|
||||
|
||||
// The first char self-acquires focus AND processes that same event.
|
||||
assert!(
|
||||
@@ -40,7 +40,7 @@ fn an_autofocus_box_self_acquires_on_the_first_event() {
|
||||
"an autoFocus box consumes the acquiring event"
|
||||
);
|
||||
assert!(s.has_keyboard_focus());
|
||||
assert!(s.eval::<bool>("return E:HasFocus()").unwrap());
|
||||
assert_eq!(s.focused_editbox_name().as_deref(), Some("E"));
|
||||
assert_eq!(s.eval::<String>("return E:GetText()").unwrap(), "a");
|
||||
}
|
||||
|
||||
@@ -62,8 +62,9 @@ fn an_autofocus_box_takes_the_keyboard_when_it_is_shown() {
|
||||
assert!(!s.has_keyboard_focus(), "hidden and unfocused to start");
|
||||
|
||||
s.run("E:Show()").unwrap();
|
||||
assert!(
|
||||
s.eval::<bool>("return E:HasFocus()").unwrap(),
|
||||
assert_eq!(
|
||||
s.focused_editbox_name().as_deref(),
|
||||
Some("E"),
|
||||
"showing an autoFocus box focuses it",
|
||||
);
|
||||
|
||||
@@ -108,18 +109,19 @@ fn the_show_focus_is_refused_without_autofocus_or_with_the_keyboard_taken() {
|
||||
)
|
||||
.unwrap();
|
||||
s.run("LATE:Show()").unwrap();
|
||||
assert!(
|
||||
s.eval::<bool>("return HOLDER:HasFocus()").unwrap(),
|
||||
assert_eq!(
|
||||
s.focused_editbox_name().as_deref(),
|
||||
Some("Holder"),
|
||||
"a shown autoFocus box does not steal a focus that is already held",
|
||||
);
|
||||
assert!(!s.eval::<bool>("return LATE:HasFocus()").unwrap());
|
||||
|
||||
// And hiding a box that does NOT hold the keyboard leaves it where it is — `0x77e410`'s own
|
||||
// per-box guard (`cmp ecx,eax; jne ret`), which is what makes the override's unconditional
|
||||
// tail-jmp harmless.
|
||||
s.run("LATE:Hide()").unwrap();
|
||||
assert!(
|
||||
s.eval::<bool>("return HOLDER:HasFocus()").unwrap(),
|
||||
assert_eq!(
|
||||
s.focused_editbox_name().as_deref(),
|
||||
Some("Holder"),
|
||||
"hiding an unfocused box does not clear somebody else's focus",
|
||||
);
|
||||
assert!(s.errors().is_empty(), "{:?}", s.errors());
|
||||
@@ -179,7 +181,7 @@ fn set_focus_on_hidden_box_is_a_noop() {
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
assert!(!s.eval::<bool>("return E:HasFocus()").unwrap());
|
||||
assert_eq!(s.focused_editbox_name(), None);
|
||||
assert!(!s.has_keyboard_focus());
|
||||
assert_eq!(
|
||||
s.eval::<i64>("return gained").unwrap(),
|
||||
@@ -198,7 +200,7 @@ fn click_focuses_regardless_of_autofocus_and_transition_order_is_lost_then_gaine
|
||||
local function wire(name, y)
|
||||
local f = CreateFrame("EditBox", name)
|
||||
f:SetPoint("BOTTOMLEFT", nil, "BOTTOMLEFT", 0, y)
|
||||
f:SetSize(100, 20)
|
||||
f:SetWidth(100); f:SetHeight(20)
|
||||
f:SetScript("OnEditFocusGained", function() table.insert(log, "gained"..name) end)
|
||||
f:SetScript("OnEditFocusLost", function() table.insert(log, "lost"..name) end)
|
||||
end
|
||||
@@ -212,11 +214,11 @@ fn click_focuses_regardless_of_autofocus_and_transition_order_is_lost_then_gaine
|
||||
// Neither box has autoFocus, yet a click focuses each.
|
||||
s.mouse_button(50.0, 10.0, "LeftButton", true);
|
||||
s.mouse_button(50.0, 10.0, "LeftButton", false);
|
||||
assert!(s.eval::<bool>("return A:HasFocus()").unwrap());
|
||||
assert_eq!(s.focused_editbox_name().as_deref(), Some("A"));
|
||||
|
||||
s.mouse_button(50.0, 110.0, "LeftButton", true);
|
||||
s.mouse_button(50.0, 110.0, "LeftButton", false);
|
||||
assert!(s.eval::<bool>("return B:HasFocus()").unwrap());
|
||||
assert_eq!(s.focused_editbox_name().as_deref(), Some("B"));
|
||||
|
||||
let log: Vec<String> = s.eval("return log").unwrap();
|
||||
assert_eq!(log, vec!["gainedA", "lostA", "gainedB"]);
|
||||
@@ -473,8 +475,9 @@ fn enter_escape_tab_space_fire_their_slots() {
|
||||
.unwrap();
|
||||
assert!(s.key_input("ENTER")); // single-line → OnEnterPressed
|
||||
assert!(s.key_input("ESCAPE")); // fires, does NOT release focus
|
||||
assert!(
|
||||
s.eval::<bool>("return E:HasFocus()").unwrap(),
|
||||
assert_eq!(
|
||||
s.focused_editbox_name().as_deref(),
|
||||
Some("E"),
|
||||
"ESCAPE keeps focus"
|
||||
);
|
||||
assert!(s.key_input("TAB"));
|
||||
@@ -1353,7 +1356,7 @@ fn creating_a_box_does_not_focus_it_the_way_showing_one_does() {
|
||||
!s.has_keyboard_focus(),
|
||||
"a box born visible has not been SHOWN, so it takes no keyboard",
|
||||
);
|
||||
assert!(!s.eval::<bool>("return E:HasFocus()").unwrap());
|
||||
assert_eq!(s.focused_editbox_name(), None);
|
||||
assert!(s.errors().is_empty(), "{:?}", s.errors());
|
||||
}
|
||||
|
||||
|
||||
@@ -46,6 +46,46 @@ pub(super) fn fire_global(lua: &Lua, event: &str, args: &[ScriptValue]) {
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
fire_all_event_listeners(lua, event, args);
|
||||
}
|
||||
|
||||
/// The `RegisterAllEvents()` half of one dispatch: every frame that asked for the whole stream,
|
||||
/// after the event's own listeners and skipping any frame that was already visited as one of them.
|
||||
///
|
||||
/// **After, and de-duplicated, because that is where such a frame would sit if the registration
|
||||
/// were expanded.** A frame joins the all-events set later than the frames already listening for a
|
||||
/// given event, so it takes the tail of that event's list; and a frame holding both an all-events
|
||||
/// registration and a `RegisterEvent` for *this* event is one listener, not two — `RegisterEvent`'s
|
||||
/// own `if !list.contains(&h)` is the same rule one level down.
|
||||
///
|
||||
/// Same mid-dispatch discipline as the walk above (`0x703ee8`, decision 1324): the next frame is
|
||||
/// re-found by position each step, so a handler that unregisters the walk's successor stops the
|
||||
/// dispatch there rather than robbing it.
|
||||
pub(super) fn fire_all_event_listeners(lua: &Lua, event: &str, args: &[ScriptValue]) {
|
||||
let model_mut = || lua.app_data_mut::<Model>().expect("model app_data set");
|
||||
let mut at = model_mut().all_event_frames.first().copied();
|
||||
while let Some(h) = at {
|
||||
let mut model = model_mut();
|
||||
let Some(pos) = model.all_event_frames.iter().position(|&x| x == h) else {
|
||||
break;
|
||||
};
|
||||
let next = model.all_event_frames.get(pos + 1).copied();
|
||||
let already = model
|
||||
.event_to_frames
|
||||
.get(event)
|
||||
.is_some_and(|l| l.contains(&h));
|
||||
let id = model.frame_id(h);
|
||||
drop(model);
|
||||
if !already {
|
||||
if let Err(e) = fire_event_handler(lua, id, event, args) {
|
||||
lua.app_data_mut::<Model>()
|
||||
.expect("model app_data")
|
||||
.errors
|
||||
.push(e.to_string());
|
||||
}
|
||||
}
|
||||
at = next;
|
||||
}
|
||||
}
|
||||
|
||||
/// Fire a frame's `OnEvent` (both conventions) with the given args.
|
||||
|
||||
@@ -487,7 +487,11 @@ impl UiScript {
|
||||
color: has_texture
|
||||
.then(|| texture_color(fill, data.vertex_color))
|
||||
.flatten(),
|
||||
additive: data.additive,
|
||||
// ADD is the one blend mode the renderer acts on; the other four of
|
||||
// the client's `alphaMode` enum are carried on `RegionData::blend`
|
||||
// (and answered by `GetBlendMode`) but drawn as straight alpha — the
|
||||
// stated v1 gap, see [`crate::script::BlendMode`].
|
||||
additive: data.blend == crate::script::BlendMode::Add,
|
||||
tex_coords: data.tex_coords,
|
||||
circular: data.circular,
|
||||
portrait_unit: data.portrait_unit,
|
||||
|
||||
@@ -71,6 +71,7 @@
|
||||
|
||||
use mlua::{Lua, Table, Value};
|
||||
|
||||
use super::binding_abi;
|
||||
use super::object::publish_global;
|
||||
use crate::justify;
|
||||
|
||||
@@ -293,9 +294,22 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
"GetObjectType",
|
||||
lua.create_function(|_, _this: Table| Ok("Font"))?,
|
||||
)?;
|
||||
// **`1`/`nil`, never a Lua boolean** (decision 2118's law; 2142 found this copy). The
|
||||
// reference's Font object has its OWN `IsObjectType` (`0x79fe60`, table `0x87c7c8`) distinct
|
||||
// from the Region base's (`0x7a1290`, `0x87c9b8`), and the shapes table types both
|
||||
// `(nil) | (number)`. This one answered a Rust `bool` — the fourth copy of a house rule that
|
||||
// three neighbours got right, which is exactly the drift 2118 consolidated `flag` to stop, and
|
||||
// it was invisible until the shape gate learned to probe the Font object at all.
|
||||
//
|
||||
// The ARGUMENT contract is a separate question and is not claimed here: `region.rs`'s twin
|
||||
// documents the reference's `Usage: %s:IsObjectType("TYPE")` raise and its number-stringifying
|
||||
// arm from a byte read of `0x7a1290`; nobody has read `0x79fe60`'s, so this copy keeps mlua's
|
||||
// own coercion rather than pretending to that finding.
|
||||
m.set(
|
||||
"IsObjectType",
|
||||
lua.create_function(|_, (_this, ty): (Table, String)| Ok(ty.eq_ignore_ascii_case("font")))?,
|
||||
lua.create_function(|_, (_this, ty): (Table, String)| {
|
||||
Ok(binding_abi::flag(ty.eq_ignore_ascii_case("font")))
|
||||
})?,
|
||||
)?;
|
||||
m.set(
|
||||
"GetName",
|
||||
|
||||
@@ -60,7 +60,7 @@
|
||||
use mlua::Lua;
|
||||
|
||||
use super::{event, Model};
|
||||
use crate::widget::FrameHandle;
|
||||
use crate::widget::{FrameHandle, FrameKind};
|
||||
|
||||
/// Which channel a dispatch is on — the two walks the reference registers separately.
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
@@ -102,6 +102,31 @@ fn has_script(lua: &Lua, id: u32, name: &str) -> bool {
|
||||
event::has_widget_handler(lua, id, name)
|
||||
}
|
||||
|
||||
/// Is `h` a `CSimpleEditBox`?
|
||||
///
|
||||
/// **A box in the walk is asked about FOCUS, never about a script slot.** Its ctor (`0x779ce8`)
|
||||
/// registers it in the kind-0 *and* kind-1 buckets, and its vtable `0x81c910` **replaces** the base
|
||||
/// input slots — `+0x5c` → `0x77a900` (char), `+0x60` → `0x77b160` (key-down) — and never chains to
|
||||
/// them. wow-re `system/ui/scratch/frame-key-script-delivery.md` §3, at the bytes: *"There is no
|
||||
/// `call 0x76b760` anywhere in it, so the generic `OnChar` slot `+0x180` is unreachable on an
|
||||
/// editbox."* The override's own tail is the decline this predicate exists to reach:
|
||||
/// `0x77a952 cmp esi,eax` — I am the focus owner → insert and consume — else
|
||||
/// `0x77a956 xor eax,eax`, **return 0, and the walk continues past me**. `0x77b160` has the same
|
||||
/// shape at `0x77b1c7`/`0x77b21e`.
|
||||
///
|
||||
/// Without this, an unfocused box carrying an XML `<OnChar>` ran the base gate and **ate every
|
||||
/// keystroke aimed at its neighbours**: stock `SendMailNameEditBox` has one
|
||||
/// (`SendMailFrame_SendeeAutocomplete`) and is the first-registered keyboard frame in the mail
|
||||
/// window, so the send tab's subject, body and three money boxes took no input at all — the whole
|
||||
/// window typed dead except the one box the handler is on (decision 2145).
|
||||
fn is_editbox(lua: &Lua, h: FrameHandle) -> bool {
|
||||
let model = lua.app_data_ref::<Model>().expect("model app_data");
|
||||
model
|
||||
.arena
|
||||
.frame(h)
|
||||
.is_some_and(|f| f.kind == FrameKind::EditBox)
|
||||
}
|
||||
|
||||
/// The dispatcher for one channel — [`Channel`]'s walk, gate and fire, in the reference's order.
|
||||
///
|
||||
/// Returns whether the event was **consumed**, which is what the caller must use to suppress the
|
||||
@@ -129,6 +154,13 @@ fn walk(lua: &Lua, channel: Channel, arg: &str) -> bool {
|
||||
}
|
||||
continue;
|
||||
}
|
||||
// Someone else holds the focus (or nothing does and this box is not `autoFocus`): the
|
||||
// override returns 0 and the walk moves on. The base slot is unreachable here — see
|
||||
// [`is_editbox`]. A no-focus self-acquire still happens, at [`super::editbox::route`],
|
||||
// when the whole walk declines.
|
||||
if is_editbox(lua, h) {
|
||||
continue;
|
||||
}
|
||||
let id = {
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
|
||||
model.frame_id(h)
|
||||
@@ -214,6 +246,11 @@ pub(super) fn frame_key_input(lua: &Lua, key: &str) -> bool {
|
||||
return false; // the box owns this key; its chord path handles it
|
||||
}
|
||||
}
|
||||
// …and an UNfocused box declines instead of gating on `+0x188`/`+0x190` ([`is_editbox`]),
|
||||
// so it can never steal a neighbour's BACKSPACE either.
|
||||
if is_editbox(lua, h) {
|
||||
continue;
|
||||
}
|
||||
let id = {
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
|
||||
model.frame_id(h)
|
||||
|
||||
@@ -1432,9 +1432,12 @@ impl UiScript {
|
||||
|
||||
// The ScrollFrame mechanism (decision 0112): a live ScrollFrame with a live scroll child
|
||||
// overrides the child's own anchors for this solve — `SetScrollChild` pins the child TOPLEFT
|
||||
// to the scrollframe's TOPLEFT, offset by the live vertical scroll (`(0, vertical)`; XML
|
||||
// y-positive-up, so a positive offset lifts the child, bringing content below the fold into
|
||||
// view — `frame top 500, vertical 40 ⇒ child top 540`). This is a LOCAL map, consulted only
|
||||
// to the scrollframe's TOPLEFT, offset by the two live scroll offsets
|
||||
// (`(horizontal, vertical)`, both RAW and unnegated, exactly as the reference's re-anchor
|
||||
// `0x787100` hands them to `SetPoint`). XML y-positive-up, so a positive vertical lifts the
|
||||
// child, bringing content below the fold into view — `frame top 500, vertical 40 ⇒ child top
|
||||
// 540`; x grows right, so a positive horizontal pushes the child right and it is a NEGATIVE
|
||||
// horizontal that scrolls right (see `ScrollFrameState::horizontal`). This is a LOCAL map, consulted only
|
||||
// while building each round's graph below — the child's authored `LayoutInput.anchors` are
|
||||
// never touched, so `SetScrollChild(nil)` needs no restore: the override just stops being
|
||||
// computed. The child's own width/height (from its own `LayoutInput`) stay whatever they are;
|
||||
@@ -1456,7 +1459,13 @@ impl UiScript {
|
||||
}
|
||||
scroll_child_anchor.insert(
|
||||
child,
|
||||
Anchor::new(Point::TopLeft, id, Point::TopLeft, 0.0, state.vertical),
|
||||
Anchor::new(
|
||||
Point::TopLeft,
|
||||
id,
|
||||
Point::TopLeft,
|
||||
state.horizontal,
|
||||
state.vertical,
|
||||
),
|
||||
);
|
||||
}
|
||||
pre.scroll = pre.lap();
|
||||
|
||||
@@ -226,7 +226,7 @@ impl super::UiScript {
|
||||
|
||||
/// Drain the `SendMail` intent, if one was queued since the last drain — folds in the last
|
||||
/// `SetSendMailMoney`/`SetSendMailCOD` amounts and the attached item's `(bag, slot)`. The
|
||||
/// attachment is left in place (a failed send keeps it; [`Self::clear_send_mail_item`] drops it
|
||||
/// attachment is left in place (a failed send keeps it; [`Self::reset_compose_tab`] drops it
|
||||
/// on success).
|
||||
pub fn take_mail_send(&mut self) -> Option<MailSendRequest> {
|
||||
let mut model = self.model_mut();
|
||||
@@ -243,13 +243,37 @@ impl super::UiScript {
|
||||
})
|
||||
}
|
||||
|
||||
/// Drop the send tab's attached item + money/COD amounts — the app calls this on
|
||||
/// `MAIL_SEND_SUCCESS` (the reference `SendMailFrame_Reset`, MailFrame.lua l.49/552).
|
||||
pub fn clear_send_mail_item(&mut self) {
|
||||
let mut model = self.model_mut();
|
||||
model.mail_send_item = None;
|
||||
model.mail_send_money = 0;
|
||||
model.mail_send_cod = 0;
|
||||
/// **`0x4acdc0(1)` — the client's compose-tab reset**, whole. Zeroes the send tab's attachment
|
||||
/// (`0xb6ef90/94`), money (`0xb6efa4`) and COD (`0xb6efa8`) globals and then **tail-fires its
|
||||
/// three events**, in this order: `SEND_MAIL_MONEY_CHANGED`, `SEND_MAIL_COD_CHANGED`,
|
||||
/// `MAIL_SEND_SUCCESS` (`@0x4ace14/1e/28` — wow-re `system/ui/scratch/mail-interaction.md`
|
||||
/// §1/§4).
|
||||
///
|
||||
/// **The fire belongs to the reset, and that is the whole point of this shape.** Both call
|
||||
/// sites used to fire `MAIL_SEND_SUCCESS` themselves, and the send-result one fired it
|
||||
/// *before* clearing — so the stock `SendMailFrame_Reset` ran while `GetSendMailItem()` still
|
||||
/// answered with the item that had just been sent, and its own `SendMailFrame_Update()` tail
|
||||
/// put the item's name straight back into the subject box and its texture back on
|
||||
/// `SendMailPackageButton`. Nothing re-ran the update after the clear landed, so a sent letter
|
||||
/// left its subject and its icon sitting in the form (director's report, decision 2145).
|
||||
///
|
||||
/// `MAIL_SEND_SUCCESS` is **overloaded** — it means "the compose form is now clean", not "a
|
||||
/// mail was sent" (the anomaly wow-re verified twice: opening a mailbox fires it too).
|
||||
pub fn reset_compose_tab(&mut self) {
|
||||
{
|
||||
let mut model = self.model_mut();
|
||||
model.mail_send_item = None;
|
||||
model.mail_send_money = 0;
|
||||
model.mail_send_cod = 0;
|
||||
}
|
||||
// Fired here, immediately, rather than queued on `pending_events`: this is a host-side
|
||||
// edge (the mail system driving the VM), not a Lua binding queueing work for the next
|
||||
// tick, and its order against the caller's own `MAIL_SHOW`/`MAIL_FAILED` is the law.
|
||||
// Spelled out one call each — the three are a fixed sequence at three addresses, and the
|
||||
// chain-file event census (`ui_script::reference_ui`) reads producers as literals.
|
||||
self.fire_event("SEND_MAIL_MONEY_CHANGED", Vec::new()); // 0x4ace14
|
||||
self.fire_event("SEND_MAIL_COD_CHANGED", Vec::new()); // 0x4ace1e
|
||||
self.fire_event("MAIL_SEND_SUCCESS", Vec::new()); // 0x4ace28
|
||||
}
|
||||
|
||||
/// Drop the attachment alone — `SendMail`'s attached-item-gone abort (`ERR_ITEM_NOT_FOUND`, no
|
||||
@@ -1223,7 +1247,7 @@ mod tests {
|
||||
}));
|
||||
s.run("ClickSendMailItemButton()").unwrap();
|
||||
s.run("SetSendMailMoney(99) SetSendMailCOD(5)").unwrap();
|
||||
s.clear_send_mail_item();
|
||||
s.reset_compose_tab();
|
||||
select_default_stationery(&mut s);
|
||||
s.run("SendMail('x','y','z')").unwrap();
|
||||
let req = s.take_mail_send().unwrap();
|
||||
|
||||
@@ -89,6 +89,11 @@ mod handler_prof;
|
||||
mod screenshot;
|
||||
mod tabard;
|
||||
pub use handler_prof::HandlerRow;
|
||||
|
||||
/// The widget-method surface measurement — shared by the `dump_widget_methods` example and the
|
||||
/// widget-surface gate (decision 2142).
|
||||
mod surface;
|
||||
pub use surface::widget_method_census;
|
||||
mod inspect;
|
||||
mod item_stats;
|
||||
mod item_text;
|
||||
@@ -262,9 +267,9 @@ pub use trainer::{
|
||||
TrainerState, TrainerTooltip, TRAINER_GROUP_KNOWN,
|
||||
};
|
||||
pub use types::{
|
||||
EditAction, EditBoxTextUi, EditOutcome, EditUnit, ExtractedQuad, FontObject, FontShadow,
|
||||
Gradient, JustifyH, JustifyV, LineMeasureRequest, MeasureRequest, Outline, QuadContent,
|
||||
ScriptValue, TexCoords,
|
||||
BlendMode, EditAction, EditBoxTextUi, EditOutcome, EditUnit, ExtractedQuad, FontObject,
|
||||
FontShadow, Gradient, JustifyH, JustifyV, LineMeasureRequest, MeasureRequest, Outline,
|
||||
QuadContent, ScriptValue, TexCoords,
|
||||
};
|
||||
pub(crate) use types::{FontExplicit, MeasuredText, RegionData};
|
||||
pub use unit::{
|
||||
@@ -336,11 +341,20 @@ pub(crate) const REGION_LEAF_SHARED: [&str; 9] = [
|
||||
/// has `SetGradientAlpha` where FontString has `SetAlphaGradient`: a near-miss pair, and we install
|
||||
/// only the FontString one.
|
||||
///
|
||||
/// The tail three are OURS, not 1.12's, and are parked here rather than pruned: `SetPortraitToTexture`
|
||||
/// and `SetRotation` are texture verbs the carve's 22 does not list, and `SetSize` is an Era
|
||||
/// geometry verb absent from the Region map. Removing a superset is a separate question per name —
|
||||
/// this landing partitions, it does not prune.
|
||||
pub(crate) const TEXTURE_ONLY_METHODS: [&str; 11] = [
|
||||
/// **Every name here is the client's own now.** Three of ours were parked in this list pending a
|
||||
/// per-name check, and all three are gone: `SetRotation` (1.12 registers it only on PlayerModel,
|
||||
/// `0x84f1fc`/`0x505f00`, and the world-map arrow that justified it is a Model frame now — see
|
||||
/// `region/paint.rs` at its old site), `SetPortraitToTexture` (1.12 has it as an engine GLOBAL,
|
||||
/// never a Texture method — `region.rs` registers it there, and a test pins the absence), and
|
||||
/// `SetSize`, the Era geometry verb that was in neither client map and that nothing outside our
|
||||
/// own test scaffolding called (decision 2142's census). The 1244/1245 landing partitioned rather
|
||||
/// than pruned and said the pruning was a separate question per name; this is the answer to three
|
||||
/// of them.
|
||||
///
|
||||
/// Going the other way: `GetBlendMode`, `SetTexCoordModifiesRect` and `GetTexCoordModifiesRect`
|
||||
/// joined the list, all three of them in the client's Texture map `0x87c128` (`0x79a890` /
|
||||
/// `0x79c080` / `0x79c120`), and all three of them missing here until then.
|
||||
pub(crate) const TEXTURE_ONLY_METHODS: [&str; 12] = [
|
||||
"SetGradient",
|
||||
"SetGradientAlpha",
|
||||
"GetTexture",
|
||||
@@ -348,22 +362,22 @@ pub(crate) const TEXTURE_ONLY_METHODS: [&str; 11] = [
|
||||
"GetTexCoord",
|
||||
"SetTexCoord",
|
||||
"SetBlendMode",
|
||||
"GetBlendMode",
|
||||
"SetTexCoordModifiesRect",
|
||||
"GetTexCoordModifiesRect",
|
||||
"SetDesaturated",
|
||||
"GetVertexColor",
|
||||
"SetRotation",
|
||||
"SetSize",
|
||||
];
|
||||
|
||||
/// **FontString-only** — the font/text/justify/shadow block plus the string metrics.
|
||||
///
|
||||
/// The tail two are OURS: `SetFormattedText` is not in the client's 32, and `SetSize` is the same
|
||||
/// Era geometry verb the Texture list carries. Parked, not pruned, pending their own checks.
|
||||
///
|
||||
/// `GetStringHeight` was here and is GONE (1251's first prune): byte-verified absent from 1.12 in
|
||||
/// every encoding, ours was a byte-identical duplicate of `GetHeight`, and every call site — two of
|
||||
/// our own XML files, two tests, and `Button:GetTextHeight`'s delegate — now goes through the
|
||||
/// Region method the reference itself uses.
|
||||
pub(crate) const FONTSTRING_ONLY_METHODS: [&str; 23] = [
|
||||
/// **Every name here is the client's own now.** Three of ours were parked in this list:
|
||||
/// `GetStringHeight` went first (1251 — byte-verified absent, and ours was a byte-identical
|
||||
/// duplicate of `GetHeight`), and `SetFormattedText` (not among the client's 32) and
|
||||
/// `SetSize` (the Era geometry verb the Texture list carried too) went with 2142's census, which
|
||||
/// found neither in the stock chain nor in either addon corpus. The era spelling of
|
||||
/// `SetFormattedText` is `SetText(format(fmt, ...))`; of `SetSize`, `SetWidth` + `SetHeight`.
|
||||
pub(crate) const FONTSTRING_ONLY_METHODS: [&str; 21] = [
|
||||
"SetFont",
|
||||
"GetFont",
|
||||
"SetFontObject",
|
||||
@@ -385,8 +399,6 @@ pub(crate) const FONTSTRING_ONLY_METHODS: [&str; 23] = [
|
||||
"SetNonSpaceWrap",
|
||||
"CanNonSpaceWrap",
|
||||
"SetAlphaGradient",
|
||||
"SetFormattedText",
|
||||
"SetSize",
|
||||
];
|
||||
|
||||
pub(crate) const REGION_MAP_METHODS: [&str; 19] = [
|
||||
@@ -426,7 +438,10 @@ pub const SCREEN: crate::layout::Handle = 0;
|
||||
/// each kind dispatching to its own value-changed slot. The eight
|
||||
/// `On*Pressed`/text/focus slots are the EditBox's specialized scripts (RF-0082 §2): a focused EditBox
|
||||
/// fires ONLY these, never generic `OnKeyDown`/`OnChar` (its C++ override replaces those slots).
|
||||
/// `OnVerticalScroll`/`OnScrollRangeChanged` are the ScrollFrame's own slots (decision 0112).
|
||||
/// `OnHorizontalScroll`/`OnVerticalScroll`/`OnScrollRangeChanged` are the ScrollFrame's own slots
|
||||
/// (decision 0112; the reference's `[+0x32c]`/`[+0x334]`/`[+0x33c]`, script-name map `0x786c40`).
|
||||
/// `OnHorizontalScroll` joined the other two with the horizontal offset pair — it is fired by
|
||||
/// `SetHorizontalScroll`, which is what earns it the row below.
|
||||
/// `OnDragStart`/`OnDragStop`/`OnReceiveDrag` are the drag trio (decision 0216 §3) — driven by
|
||||
/// `RegisterForDrag` + the same mouse path as the six mouse handlers above, not a separate one.
|
||||
/// `OnColorSelect` is the ColorSelect's own slot (RF-28 `+0x338`), fired by its `SetColorRGB`.
|
||||
@@ -451,7 +466,7 @@ pub const SCREEN: crate::layout::Handle = 0;
|
||||
/// corpus call sites across 91 addons); it removes working behaviour from the 23 sites that remain,
|
||||
/// so it is still a change to make deliberately rather than as a side effect of widening this list
|
||||
/// — but the FrameXML half of "with FrameXML fixed first" is most of the way there now.
|
||||
const SCRIPT_KINDS: [&str; 38] = [
|
||||
const SCRIPT_KINDS: [&str; 39] = [
|
||||
"OnLoad",
|
||||
"OnEvent",
|
||||
"OnUpdate",
|
||||
@@ -482,6 +497,7 @@ const SCRIPT_KINDS: [&str; 38] = [
|
||||
"OnCursorChanged",
|
||||
"OnEditFocusGained",
|
||||
"OnEditFocusLost",
|
||||
"OnHorizontalScroll",
|
||||
"OnVerticalScroll",
|
||||
"OnScrollRangeChanged",
|
||||
"OnDragStart",
|
||||
@@ -1370,6 +1386,21 @@ impl UiScript {
|
||||
.is_some_and(|h| model.arena.frame(h).is_some_and(|f| f.effective_visible))
|
||||
}
|
||||
|
||||
/// **Which** EditBox holds the focus, by name — [`Self::has_keyboard_focus`] answers
|
||||
/// *whether*. Unfiltered by visibility, because the focus cell itself is: a box that hides
|
||||
/// while focused keeps the cell until something clears it, and the tests that watch the
|
||||
/// hand-off between two boxes are watching exactly that cell.
|
||||
///
|
||||
/// This is the host-side read of `Model::focused_editbox`. There is no Lua verb for it and
|
||||
/// there must not be: 1.12's EditBox table has `SetFocus`/`ClearFocus` and no getter, which
|
||||
/// is why `pfQuest/browser.lua:760` ships its own focus flag rather than asking (decision
|
||||
/// 2142).
|
||||
pub fn focused_editbox_name(&self) -> Option<String> {
|
||||
let model = self.model_ref();
|
||||
let h = model.focused_editbox?;
|
||||
model.arena.frame(h)?.name.clone()
|
||||
}
|
||||
|
||||
/// How many resolves the layout change gate has let through (`Model::layout_solves`) — the
|
||||
/// gate's effectiveness, readable by tests and the app's cost meters (a per-frame delta of 0
|
||||
/// means the fingerprint judged the frame quiet).
|
||||
|
||||
@@ -132,7 +132,7 @@ pub(crate) struct Model {
|
||||
/// The frame arena (create/destroy + show/hide/strata/level/scale/alpha propagation).
|
||||
pub(crate) arena: WidgetArena,
|
||||
/// Per-frame layout input (anchors/size/scale). Every live frame has one (created at
|
||||
/// `CreateFrame`); `SetPoint`/`SetSize`/… mutate it; `resolve` runs the graph over them.
|
||||
/// `CreateFrame`); `SetPoint`/`SetWidth`/… mutate it; `resolve` runs the graph over them.
|
||||
pub(crate) layout_inputs: HashMap<FrameHandle, LayoutInput>,
|
||||
/// The last [`UiScript::resolve`] result: each resolvable frame's rect. Empty until `resolve`.
|
||||
pub(crate) resolved: HashMap<FrameHandle, Rect>,
|
||||
@@ -374,6 +374,20 @@ pub(crate) struct Model {
|
||||
pub(crate) event_to_frames: HashMap<String, Vec<FrameHandle>>,
|
||||
/// `frame → its registered events` (for UnregisterEvent / cleanup).
|
||||
pub(crate) frame_events: HashMap<FrameHandle, HashSet<String>>,
|
||||
/// The frames registered for **every** event (`RegisterAllEvents`), in registration order —
|
||||
/// the same ordered-Vec-never-a-set discipline as [`Self::event_to_frames`], and dispatched
|
||||
/// after it for the same reason: a frame that asks for all events asks *after* the frames
|
||||
/// already listening for a given one, so it takes the tail of that event's list.
|
||||
///
|
||||
/// **A flag rather than an expansion.** 1.12's event ids run to `0x225` = 549
|
||||
/// (`FrameScript_InitEvents`), so writing the frame into every per-event list on registration
|
||||
/// would mean 549 vector pushes per call and 549 scans per `UnregisterAllEvents` — and would
|
||||
/// also invent a name list this engine has no business owning (ours registers by string, and
|
||||
/// an event the server sends that no name list knows about would be silently excluded).
|
||||
/// `UnregisterAllEvents` empties this alongside the per-event registrations, which is the half
|
||||
/// AceEvent-2.0 depends on: it calls `frame:UnregisterAllEvents()` and then re-registers each
|
||||
/// individual event it still wants.
|
||||
pub(crate) all_event_frames: Vec<FrameHandle>,
|
||||
|
||||
/// The EditBox that currently owns keyboard focus — the engine's twin of the client's
|
||||
/// class-owned focus global `DAT_00cf4dc8` (`CSimpleEditBox* E`, 0 = none; RF-0082 §1). A focused
|
||||
@@ -1828,6 +1842,7 @@ impl Model {
|
||||
dirty_editboxes: Vec::new(),
|
||||
event_to_frames: HashMap::new(),
|
||||
frame_events: HashMap::new(),
|
||||
all_event_frames: Vec::new(),
|
||||
focused_editbox: None,
|
||||
mouseover: None,
|
||||
hover_repick: false,
|
||||
|
||||
@@ -148,7 +148,7 @@ impl Model {
|
||||
}
|
||||
}
|
||||
|
||||
/// A size was AUTHORED on `h` (`SetWidth`/`SetHeight`/`SetSize`, the XML `<Size>` through
|
||||
/// A size was AUTHORED on `h` (`SetWidth`/`SetHeight`, the XML `<Size>` through
|
||||
/// them): an implicit rect no longer applies to it, for good — the reference's override
|
||||
/// yields to any authored size.
|
||||
pub(crate) fn note_authored_size(&mut self, h: FrameHandle) {
|
||||
|
||||
@@ -47,6 +47,37 @@ pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
Ok(())
|
||||
})?,
|
||||
)?;
|
||||
// `RegisterAllEvents()` — this frame's `OnEvent` receives EVERY event that is dispatched
|
||||
// (`0x774c20`, table `0x878ec0`, argc 1, arity 0). Cleared by `UnregisterAllEvents` below, and
|
||||
// by nothing else: there is no `UnregisterAllEvents`-less way back out, which is exactly how
|
||||
// the reference's pair reads.
|
||||
//
|
||||
// **Latent, not live, and the widest-carried gap the corpus census found.** Nothing in either
|
||||
// addon corpus calls it *today*, but 63 vanilla addons and 6 of the top 20 ship
|
||||
// **AceEvent-2.0**, whose `AceEvent:RegisterAllEvents(handler)` runs
|
||||
// `AceEvent.frame:RegisterAllEvents()` the first time any addon asks for all events — so the
|
||||
// first one that does raised here. AceEvent is also why the *clearing* half is load-bearing
|
||||
// rather than tidy: while all-events is on it deliberately stops calling
|
||||
// `frame:UnregisterEvent(event)` at all, and `AceEvent:UnregisterAllEvents()` gets back to a
|
||||
// per-event registration by calling `frame:UnregisterAllEvents()` and then re-registering each
|
||||
// event it still wants. A `RegisterAllEvents` that outlived that call would leave every Ace2
|
||||
// addon on the whole event stream forever.
|
||||
//
|
||||
// Registered as a FLAG (the frame joins `Model::all_event_frames`), never as an expansion into
|
||||
// a name list — see that field for why, and `tick::fire_event_into` for the dispatch.
|
||||
m.set(
|
||||
"RegisterAllEvents",
|
||||
lua.create_function(|lua, this: Table| {
|
||||
let h = frame_handle_of(lua, &this)?;
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model");
|
||||
// Registration order, and re-registering keeps the original position — the same law
|
||||
// `RegisterEvent` above holds, for the same reason.
|
||||
if !model.all_event_frames.contains(&h) {
|
||||
model.all_event_frames.push(h);
|
||||
}
|
||||
Ok(())
|
||||
})?,
|
||||
)?;
|
||||
// `UnregisterAllEvents()` — drop every registration this frame holds, in one call.
|
||||
//
|
||||
// 10 corpus addons stop on it (decision 1195), and the idiom is why: an addon's "disable me"
|
||||
@@ -67,6 +98,9 @@ pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
list.retain(|x| x != &h);
|
||||
}
|
||||
}
|
||||
// …and the all-events registration, which is a registration like any other. See
|
||||
// `RegisterAllEvents` above: AceEvent-2.0's own re-registration path depends on this.
|
||||
model.all_event_frames.retain(|x| x != &h);
|
||||
Ok(())
|
||||
})?,
|
||||
)?;
|
||||
@@ -320,12 +354,15 @@ pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
/// * **`OnAttributeChanged`** (1 site, `Roid-Macros`) — **raising, permanently.** It is 2.0's secure
|
||||
/// frame/attribute system; there is no such slot in any 1.12 resolver. That addon is asking for a
|
||||
/// later client and should hear so.
|
||||
/// * **`OnHorizontalScroll` · `OnHyperlinkEnter` · `OnHyperlinkLeave` · `OnMessageScrollChanged` ·
|
||||
/// * **`OnHyperlinkEnter` · `OnHyperlinkLeave` · `OnMessageScrollChanged` ·
|
||||
/// `OnMovieFinished`/`ShowSubtitle`/`HideSubtitle` · `OnInputLanguageChanged`** — **raising.**
|
||||
/// Real 1.12 slots that we do not fire, and measured at **zero** call sites across the
|
||||
/// 218-addon corpus, so there is nothing to weigh against the trap: they land when their
|
||||
/// mechanism does (horizontal scroll isn't modeled at all — see
|
||||
/// [`crate::script::scrollframe`]'s module doc).
|
||||
/// mechanism does.
|
||||
/// * **`OnHorizontalScroll`** — **accepted, because `SetHorizontalScroll` fires it.** It was on
|
||||
/// the line above while "horizontal scroll isn't modeled at all", which stopped being true when
|
||||
/// the ScrollFrame's horizontal offset pair landed beside the vertical one
|
||||
/// ([`crate::script::scrollframe`]); `aux-addon` reads and writes both axes.
|
||||
/// * **`OnUpdateModel` · `OnAnimFinished`** — **accepted since decision 2007**, because the tick
|
||||
/// fires them: the model pane's scene clock runs in this engine now (`tick.rs`), `OnUpdateModel`
|
||||
/// at the top of every paint of a visible pane with a file (`0x76d1a0`) and `OnAnimFinished`
|
||||
|
||||
@@ -14,7 +14,10 @@ use super::{decode_id, draw_layer_from_str, frame_handle_of, frame_wrapper, stra
|
||||
|
||||
/// Populate `m`'s visibility/hierarchy/strata/backdrop/mouse methods (see the module doc).
|
||||
pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
// Show / Hide / visibility
|
||||
// Show / Hide / visibility. **`SetShown` is not here and must not come back**: the branchless
|
||||
// setter belongs to a later expansion — no 1.12 method table registers it, and neither the
|
||||
// stock chain nor either addon corpus calls it (decision 2142's census). A frame is shown or
|
||||
// hidden by the two verbs the era has.
|
||||
m.set(
|
||||
"Show",
|
||||
lua.create_function(|lua, this: Table| set_shown(lua, &this, true))?,
|
||||
@@ -23,21 +26,12 @@ pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
"Hide",
|
||||
lua.create_function(|lua, this: Table| set_shown(lua, &this, false))?,
|
||||
)?;
|
||||
// SetShown(bool) — the live API's branchless Show/Hide (a consensus call across the 0068
|
||||
// target addons; Lua truthiness, so SetShown(nil) hides).
|
||||
m.set(
|
||||
"SetShown",
|
||||
lua.create_function(|lua, (this, shown): (Table, Value)| {
|
||||
let show = !matches!(shown, Value::Nil | Value::Boolean(false));
|
||||
set_shown(lua, &this, show)
|
||||
})?,
|
||||
)?;
|
||||
m.set(
|
||||
"IsShown",
|
||||
lua.create_function(|lua, this: Table| {
|
||||
let h = frame_handle_of(lua, &this)?;
|
||||
let model = lua.app_data_ref::<Model>().expect("model");
|
||||
Ok(crate::script::binding_abi::predicate(
|
||||
Ok(crate::script::binding_abi::flag(
|
||||
model.arena.frame(h).map(|f| f.shown).unwrap_or(false),
|
||||
))
|
||||
})?,
|
||||
@@ -47,7 +41,7 @@ pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
lua.create_function(|lua, this: Table| {
|
||||
let h = frame_handle_of(lua, &this)?;
|
||||
let model = lua.app_data_ref::<Model>().expect("model");
|
||||
Ok(crate::script::binding_abi::predicate(
|
||||
Ok(crate::script::binding_abi::flag(
|
||||
model
|
||||
.arena
|
||||
.frame(h)
|
||||
@@ -759,7 +753,7 @@ pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
lua.create_function(|lua, this: Table| {
|
||||
let h = frame_handle_of(lua, &this)?;
|
||||
let model = lua.app_data_ref::<Model>().expect("model");
|
||||
Ok(crate::script::binding_abi::predicate(
|
||||
Ok(crate::script::binding_abi::flag(
|
||||
model.arena.is_mouse_enabled(h),
|
||||
))
|
||||
})?,
|
||||
@@ -795,7 +789,7 @@ pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
lua.create_function(|lua, this: Table| {
|
||||
let h = frame_handle_of(lua, &this)?;
|
||||
let model = lua.app_data_ref::<Model>().expect("model");
|
||||
Ok(crate::script::binding_abi::predicate(
|
||||
Ok(crate::script::binding_abi::flag(
|
||||
model.arena.is_keyboard_enabled(h),
|
||||
))
|
||||
})?,
|
||||
@@ -863,7 +857,7 @@ pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
lua.create_function(|lua, this: Table| {
|
||||
let h = frame_handle_of(lua, &this)?;
|
||||
let model = lua.app_data_ref::<Model>().expect("model");
|
||||
Ok(crate::script::binding_abi::predicate(
|
||||
Ok(crate::script::binding_abi::flag(
|
||||
model.arena.is_mouse_wheel_enabled(h),
|
||||
))
|
||||
})?,
|
||||
@@ -889,7 +883,7 @@ pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
lua.create_function(|lua, this: Table| {
|
||||
let h = frame_handle_of(lua, &this)?;
|
||||
let model = lua.app_data_ref::<Model>().expect("model");
|
||||
Ok(crate::script::binding_abi::predicate(
|
||||
Ok(crate::script::binding_abi::flag(
|
||||
model.arena.is_clamped_to_screen(h),
|
||||
))
|
||||
})?,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! Frame method-table cluster: anchoring and size — `SetPoint`/`ClearAllPoints`/`GetPoint`/
|
||||
//! `SetAllPoints`/`SetWidth`/`SetHeight`/`SetSize`/`GetWidth`/`GetHeight` and the resolved-edge
|
||||
//! readers (`GetLeft`/`GetRight`/`GetTop`/`GetBottom`). Split out of [`super`] purely for size — see
|
||||
//! `SetAllPoints`/`SetWidth`/`SetHeight`/`GetWidth`/`GetHeight` and the resolved-edge readers
|
||||
//! (`GetLeft`/`GetRight`/`GetTop`/`GetBottom`). Split out of [`super`] purely for size — see
|
||||
//! its module doc for the shared id/handle plumbing and method-table wiring.
|
||||
|
||||
use mlua::{Lua, Table, Value};
|
||||
@@ -41,7 +41,7 @@ fn prefetch_relative_to(
|
||||
|
||||
/// Populate `m`'s layout (anchor/size) methods (see the module doc).
|
||||
pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
// Layout: SetPoint / ClearAllPoints / SetSize / SetWidth / SetHeight / GetWidth / GetHeight
|
||||
// Layout: SetPoint / ClearAllPoints / SetWidth / SetHeight / GetWidth / GetHeight
|
||||
m.set(
|
||||
"SetPoint",
|
||||
lua.create_function(
|
||||
@@ -209,23 +209,9 @@ pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
Ok(())
|
||||
})?,
|
||||
)?;
|
||||
m.set(
|
||||
"SetSize",
|
||||
lua.create_function(|lua, (this, w, ht): (Table, f32, f32)| {
|
||||
let h = frame_handle_of(lua, &this)?;
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model");
|
||||
let input = model.layout_inputs.entry(h).or_default();
|
||||
let changed =
|
||||
input.width.to_bits() != w.to_bits() || input.height.to_bits() != ht.to_bits();
|
||||
input.width = w;
|
||||
input.height = ht;
|
||||
if changed {
|
||||
model.touch_layout_frame(h);
|
||||
}
|
||||
model.note_authored_size(h);
|
||||
Ok(())
|
||||
})?,
|
||||
)?;
|
||||
// **No `SetSize`.** It is an Era geometry verb, in neither the Frame nor the Region method
|
||||
// table of 1.12 — and neither the stock chain nor either addon corpus writes it (decision
|
||||
// 2142's census). The two setters above are the era's whole size surface.
|
||||
m.set(
|
||||
"GetWidth",
|
||||
lua.create_function(|lua, this: Table| {
|
||||
@@ -370,7 +356,7 @@ pub(crate) fn eff_scale(model: &Model, h: FrameHandle) -> f32 {
|
||||
/// `GetWidth`/`GetHeight`: the resolved rect's span in LOCAL units (screen ÷ effective scale — the
|
||||
/// client returns the value as authored, and `SetWidth(w)` on a scaled frame resolves to `w·scale`
|
||||
/// screen px) if `resolve` has produced one, else the explicit size the frame was given
|
||||
/// (`SetWidth`/`SetHeight`/`SetSize`, already local) — matching the client's "0 = derive".
|
||||
/// (`SetWidth`/`SetHeight`, already local) — matching the client's "0 = derive".
|
||||
fn size_read(model: &Model, h: FrameHandle, width: bool) -> f32 {
|
||||
if let Some(r) = model.resolved.get(&h) {
|
||||
let span = if width { r.width() } else { r.height() };
|
||||
|
||||
@@ -405,11 +405,11 @@ fn set_flag(lua: &Lua, this: &Table, which: Flag, value: bool) -> mlua::Result<(
|
||||
}
|
||||
|
||||
/// `IsMovable`/`IsResizable`/`IsUserPlaced` — the NUMBER 1 or nil, never a boolean (1830,
|
||||
/// [`crate::script::binding_abi::predicate`]).
|
||||
/// [`crate::script::binding_abi::flag`]).
|
||||
fn get_flag(lua: &Lua, this: &Table, which: Flag) -> mlua::Result<Value> {
|
||||
let h = frame_handle_of(lua, this)?;
|
||||
let model = lua.app_data_ref::<Model>().expect("model");
|
||||
Ok(crate::script::binding_abi::predicate(
|
||||
Ok(crate::script::binding_abi::flag(
|
||||
model.arena.frame(h).is_some_and(|f| match which {
|
||||
Flag::Movable => f.movable,
|
||||
Flag::Resizable => f.resizable,
|
||||
|
||||
@@ -132,9 +132,7 @@ pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
lua.create_function(|lua, this: Table| {
|
||||
let h = frame_handle_of(lua, &this)?;
|
||||
let model = lua.app_data_ref::<Model>().expect("model");
|
||||
Ok(crate::script::binding_abi::predicate(
|
||||
model.arena.is_toplevel(h),
|
||||
))
|
||||
Ok(crate::script::binding_abi::flag(model.arena.is_toplevel(h)))
|
||||
})?,
|
||||
)?;
|
||||
// Raise() — `0x775a50` → `0x76a5b0` → `0x7650f0(this, force = 1)`. Note it is legal on any
|
||||
|
||||
@@ -484,11 +484,13 @@ fn install_region_methods(lua: &Lua) -> mlua::Result<()> {
|
||||
// ours was ONE table for both, so a Texture answered `SetText` and a FontString answered
|
||||
// `SetTexture`: a superset in both directions.
|
||||
//
|
||||
// **Partitioned, not pruned.** Every name we install keeps a home; what changes is which leaf
|
||||
// can see it. Removing the five names that are in NEITHER client map (`SetPortraitToTexture`,
|
||||
// `SetRotation`, `SetSize`, `SetFormattedText`, `GetStringHeight`) is a separate question per
|
||||
// name — and getting a split wrong REMOVES verbs addons use, which is worse than the superset
|
||||
// it fixes.
|
||||
// **Partitioned first, then pruned name by name.** The 1244/1245 split only decided which
|
||||
// leaf could SEE each name, because getting a split wrong REMOVES verbs addons use, which is
|
||||
// worse than the superset it fixes. The five that were in NEITHER client map were left in
|
||||
// place as a separate question per name, and all five have since been answered:
|
||||
// `GetStringHeight` (1251), `SetRotation` and `SetPortraitToTexture` (the latter a 1.12
|
||||
// GLOBAL, and it lives there now), and `SetFormattedText` and `SetSize` (2142). Both leaves
|
||||
// are the client's own lists now.
|
||||
//
|
||||
// Copied out of the full table rather than installed twice, so one implementation stands behind
|
||||
// both visibilities — and note the carve's warning that the shared names use the IDENTICAL
|
||||
@@ -892,7 +894,8 @@ pub(super) fn region_set_point(
|
||||
/// Two details of the FontString row are worth spelling out, because both were wrong here before
|
||||
/// and neither is guessable from the name:
|
||||
///
|
||||
/// * **The authored value WINS.** The old code preferred the measure and fell back to `SetSize`;
|
||||
/// * **The authored value WINS.** The old code preferred the measure and fell back to the
|
||||
/// authored size;
|
||||
/// the reference's `jp` at `0x77294a` skips the measure entirely when the authored value is
|
||||
/// non-zero. Per axis, not per region — `<Size x="290" y="0"/>` takes 290 from the author and
|
||||
/// the height from the text.
|
||||
|
||||
@@ -56,25 +56,8 @@ pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
})?,
|
||||
)?;
|
||||
|
||||
m.set(
|
||||
"SetSize",
|
||||
lua.create_function(|lua, (this, w, h): (Table, f32, f32)| {
|
||||
let rh = region_handle_of(lua, &this)?;
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model");
|
||||
let d = model.region_data.entry(rh).or_default();
|
||||
let new = Some((w, h));
|
||||
let changed = !size_bits_eq(d.size, new);
|
||||
d.size = new;
|
||||
if changed {
|
||||
// A size write moves no edge and no roster membership (decision 1388) — and on
|
||||
// a FontString the width is the WRAP width, a measure-key input, so it names
|
||||
// itself on the measure ledger too.
|
||||
model.touch_layout_region(rh);
|
||||
model.touch_measure(rh);
|
||||
}
|
||||
Ok(())
|
||||
})?,
|
||||
)?;
|
||||
// **No `SetSize`** — the frame twin's note in `object/layout_methods.rs` applies here
|
||||
// unchanged: an Era verb 1.12's Region map does not carry (decision 2142).
|
||||
|
||||
m.set(
|
||||
"GetWidth",
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
use mlua::{Lua, MultiValue, Table, Value};
|
||||
|
||||
use crate::script::object::{as_f32, draw_layer_from_str, draw_layer_name};
|
||||
use crate::script::{Model, TexCoords};
|
||||
use crate::script::{BlendMode, Model, TexCoords};
|
||||
|
||||
/// Resolve `self` (a region wrapper) to its live [`RegionHandle`].
|
||||
use super::region_handle_of;
|
||||
@@ -411,17 +411,97 @@ pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
})?,
|
||||
)?;
|
||||
|
||||
// SetBlendMode("BLEND"|"ADD"|…) — the shared alphaMode enum (0x811aa8); only ADD changes
|
||||
// draw behavior in v1 (DISABLE/ALPHAKEY/MOD are accepted as straight alpha, a stated gap).
|
||||
// SetBlendMode("DISABLE"|"ALPHAKEY"|"BLEND"|"ADD"|"MOD") — `0x79a950`, the shared alphaMode
|
||||
// enum `0x811aa8`. Only ADD changes draw behavior in v1 (DISABLE/ALPHAKEY/MOD are accepted as
|
||||
// straight alpha, a stated gap — see [`BlendMode`]); an unrecognised name leaves the mode
|
||||
// alone, which is what the enum-table lookup does with a string it cannot match.
|
||||
//
|
||||
// The mode is STORED as the mode. It used to collapse to `additive = (mode == "ADD")` on the
|
||||
// way in, which is everything the renderer needs and strictly less than `GetBlendMode` has to
|
||||
// answer — a texture set to `"MOD"` would have read back as `"BLEND"`.
|
||||
m.set(
|
||||
"SetBlendMode",
|
||||
lua.create_function(|lua, (this, mode): (Table, String)| {
|
||||
let rh = region_handle_of(lua, &this)?;
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model");
|
||||
model.region_data.entry(rh).or_default().additive = mode.eq_ignore_ascii_case("ADD");
|
||||
if let Some(blend) = BlendMode::parse(&mode) {
|
||||
model.region_data.entry(rh).or_default().blend = blend;
|
||||
}
|
||||
Ok(())
|
||||
})?,
|
||||
)?;
|
||||
// GetBlendMode() — ONE string, the enum's own spelling (`0x79a890`, table `0x87c128`, argc 1,
|
||||
// arity 1, kinds `(string?)`). A Texture nothing has called the setter on answers `"BLEND"`,
|
||||
// the CSimpleTexture ctor's `[+0xd0] = 2` (`0x76fc64`).
|
||||
//
|
||||
// **This one was answering nil, and nil is not an error here — it is a wrong picture.**
|
||||
// `ShaguTweaks/mods/dark-ui-elements.lua:169` reads
|
||||
// `elseif region.GetBlendMode and region:GetBlendMode() == "ADD" then` while recolouring a
|
||||
// Blizzard frame's children: the `and` guard means a missing method never raised, it just took
|
||||
// the other branch, so every additive texture in the frame got the dark recolour the reference
|
||||
// leaves alone. The kinds column's `string?` is the reference's own nil leg (the name pointer
|
||||
// can be NULL for a mode outside the table); the five modes are all this engine can hold, so
|
||||
// nothing here reaches it.
|
||||
m.set(
|
||||
"GetBlendMode",
|
||||
lua.create_function(|lua, this: Table| {
|
||||
let rh = region_handle_of(lua, &this)?;
|
||||
let model = lua.app_data_ref::<Model>().expect("model");
|
||||
Ok(model
|
||||
.region_data
|
||||
.get(&rh)
|
||||
.map_or(BlendMode::default(), |d| d.blend)
|
||||
.name())
|
||||
})?,
|
||||
)?;
|
||||
|
||||
// SetTexCoordModifiesRect(flag) / GetTexCoordModifiesRect() — `0x79c080` / `0x79c120`, table
|
||||
// `0x87c128`. The setter writes the reference's `[texture+0x124]` and is its only writer; the
|
||||
// getter answers `1`/`nil` (kinds `(nil) | (number)`, the predicate law — decision 2118 — not a
|
||||
// Lua boolean).
|
||||
//
|
||||
// **The flag's GEOMETRY effect is not wired, and that is stated rather than implied.** In the
|
||||
// reference the flag gates a rect-recompute leg (`ui.md:4619`, `0x770462`): with it set, a
|
||||
// `SetTexCoord` re-derives the region's own rect from the UV quad instead of leaving the rect
|
||||
// where the anchors put it and resampling inside it. Wiring that means the region resolve in
|
||||
// `region::layout` reading this flag and taking the rect from `RegionData::tex_coords` — a
|
||||
// resolve-order change. Nothing in the stock UI or in either addon corpus calls the SETTER, so
|
||||
// there is no measured case to build it against; what has a caller is the GETTER —
|
||||
// `pfUI/modules/thirdparty-tbc.lua:319` does a bare `if icon:GetTexCoordModifiesRect() then` on
|
||||
// a Texture to choose between two `SetTexCoord` rectangles, and against this VM that raised.
|
||||
// So: the flag is stored, answered truthfully, and read by nothing. See
|
||||
// [`RegionData::tex_coord_modifies_rect`].
|
||||
m.set(
|
||||
"SetTexCoordModifiesRect",
|
||||
lua.create_function(|lua, (this, arg): (Table, Value)| {
|
||||
let rh = region_handle_of(lua, &this)?;
|
||||
// The reference's shared flag-argument truth table (`0x6f1c10(L, 2, default)`), whose
|
||||
// DEFAULT byte for this binding is unread — no corpus site calls the setter at all, so
|
||||
// the no-argument leg is doubly unreachable and `false` is the conservative pick rather
|
||||
// than a claim about `0x79c080`.
|
||||
let on = crate::script::binding_abi::bool_or_default(Some(&arg), false);
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model");
|
||||
model
|
||||
.region_data
|
||||
.entry(rh)
|
||||
.or_default()
|
||||
.tex_coord_modifies_rect = on;
|
||||
Ok(())
|
||||
})?,
|
||||
)?;
|
||||
m.set(
|
||||
"GetTexCoordModifiesRect",
|
||||
lua.create_function(|lua, this: Table| {
|
||||
let rh = region_handle_of(lua, &this)?;
|
||||
let model = lua.app_data_ref::<Model>().expect("model");
|
||||
Ok(crate::script::binding_abi::flag(
|
||||
model
|
||||
.region_data
|
||||
.get(&rh)
|
||||
.is_some_and(|d| d.tex_coord_modifies_rect),
|
||||
))
|
||||
})?,
|
||||
)?;
|
||||
|
||||
m.set(
|
||||
"SetDrawLayer",
|
||||
@@ -464,22 +544,23 @@ pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
})?,
|
||||
)?;
|
||||
|
||||
// SetRotation(radians) — spin the texture about its center, counterclockwise-positive (the
|
||||
// later-era Texture API, shipped early: the world-map player arrow's stand-in rotation —
|
||||
// see `QuadContent::Texture::rotation`). No-arg/nil resets to 0.
|
||||
m.set(
|
||||
"SetRotation",
|
||||
lua.create_function(|lua, (this, radians): (Table, Option<f32>)| {
|
||||
let rh = region_handle_of(lua, &this)?;
|
||||
lua.app_data_mut::<Model>()
|
||||
.expect("model")
|
||||
.region_data
|
||||
.entry(rh)
|
||||
.or_default()
|
||||
.rotation = radians.unwrap_or(0.0);
|
||||
Ok(())
|
||||
})?,
|
||||
)?;
|
||||
// `SetRotation` WAS here, on the Texture leaf, and is GONE. 1.12 registers the name once, in
|
||||
// the **PlayerModel** table `0x84f1fc` (`0x505f00`, argc 2 — the paper doll's rotate arrows),
|
||||
// which we already answer through `modelframe`; it is in neither region map, and the carve
|
||||
// above lists it among the five names that are in NEITHER (`texture-fontstring-method-split.md`).
|
||||
// Ours was a later-era Texture verb shipped early for the world-map player arrow's stand-in
|
||||
// rotation — and that arrow has since become a real Model frame driven by `ModelState::facing`
|
||||
// (`script::worldmap_arrow`), so the verb's own reason went with it.
|
||||
//
|
||||
// Removal is safe by census, not by assumption: every `SetRotation` in this repo, in
|
||||
// `assets/ui`, in the stock FrameXML/GlueXML and in both addon corpora has a MODEL receiver —
|
||||
// `pfUI/api/ui-widgets.lua:580`'s `EnableClickRotate` hooks a modelframe's OnUpdate, and
|
||||
// `CustomNameplates/options.lua:308` is `optionsFrame.preview.model:SetRotation(0.61)`. Not one
|
||||
// texture receiver anywhere.
|
||||
//
|
||||
// `RegionData::rotation` and `QuadContent::Texture::rotation` now have no writer left. They are
|
||||
// deliberately NOT pruned in the same change: that plumbing runs into the app's quad emit and
|
||||
// its removal is a wider prune with its own review, not a tail on a method-surface fix.
|
||||
|
||||
// SetTexCoord(left, right, top, bottom) — the 4-edge form (XML `<TexCoords>`): a UV sub-rect in
|
||||
// 0..1 texture space (top-left origin) the Texture region samples, slicing quadrant/atlas art
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
//! EditBox's table gets them. The justify law likewise lives once in [`crate::justify`].
|
||||
//!
|
||||
//! What legitimately stays here is the surface a FontString alone has: the string itself
|
||||
//! (`SetText`/`SetFormattedText`/`GetText`), the measured extents, `Set/GetJustifyH`/`V`,
|
||||
//! `SetNonSpaceWrap`/`CanNonSpaceWrap`, and `SetTextHeight`.
|
||||
//! (`SetText`/`GetText`), the measured extents, `Set/GetJustifyH`/`V`, `SetNonSpaceWrap`/
|
||||
//! `CanNonSpaceWrap`, and `SetTextHeight`.
|
||||
//!
|
||||
//! The per-property override a region has over its inherited font object is unaffected — that is
|
||||
//! the severance mask (`FontExplicit`), which the shared block writes the same way this file did.
|
||||
@@ -44,26 +44,11 @@ pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
})?,
|
||||
)?;
|
||||
|
||||
// SetFormattedText(fmt, ...) = SetText(format(fmt, ...)) — routed through the stdlib's
|
||||
// positional-aware `format` so `%N$s` specs behave (a consensus call across the 0068 targets).
|
||||
m.set(
|
||||
"SetFormattedText",
|
||||
lua.create_function(|lua, (this, args): (Table, mlua::MultiValue)| {
|
||||
let format: mlua::Function = lua
|
||||
.globals()
|
||||
.get::<Table>("string")?
|
||||
.get::<mlua::Function>("format")?;
|
||||
// The formatted result is bytes too — `%s` of a sliced string is a sliced string.
|
||||
let text = format.call::<mlua::String>(args)?.to_string_lossy();
|
||||
let rh = region_handle_of(lua, &this)?;
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model");
|
||||
let data = model.region_data.entry(rh).or_default();
|
||||
data.text = Some(text);
|
||||
data.alpha_gradient = None;
|
||||
model.touch_measure(rh);
|
||||
Ok(())
|
||||
})?,
|
||||
)?;
|
||||
// **No `SetFormattedText`.** A later-expansion name: absent from the client's 32-entry
|
||||
// FontString map, from the stock 1.12 chain, and from both addon corpora (2142's census). The
|
||||
// era spelling is `SetText(format(fmt, ...))`, and `format` here is already the
|
||||
// positional-aware one ([`crate::strings`]), so `%N$s` behaves at the call site the same way
|
||||
// it behaved inside this shim.
|
||||
|
||||
// GetText — **an EMPTY string comes back as `nil`, and that substitution is the getter's own**
|
||||
// (`FontString:GetText 0x79d690`, wow-re
|
||||
@@ -138,7 +123,7 @@ pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
// GetStringWidth is the **natural, unwrapped** extent — never the declared box, and never the
|
||||
// wrapped one (wow-re `fontstring-overflow.md`, "The measurement echo": the reference's getter
|
||||
// re-measures the raw text with NO wrap constraint). Unlike `GetWidth` below it deliberately
|
||||
// does NOT fall back to an explicit `SetSize`: the declared width is the very thing a caller
|
||||
// does NOT fall back to a declared `SetWidth`: that width is the very thing a caller
|
||||
// asks this to be independent of. A kit that sizes a box from this number and then sets a width
|
||||
// on the string — which is what the reference's own `PanelTemplates_TabResize` does — would
|
||||
// otherwise read its own output back as its next input and never settle (decision 0997, the
|
||||
|
||||
@@ -53,8 +53,9 @@
|
||||
//! **Exactly the 19, and no more.** `Show`/`Hide`/`IsShown`/`IsVisible`/`SetAlpha`/`GetAlpha` look
|
||||
//! like they belong and do not: Frame and Texture each register their *own*, at different
|
||||
//! addresses (`texture-fontstring-method-split.md` §3), so `WorldFrame.Show(someTexture)` fails on
|
||||
//! the real client and must keep failing here. `SetSize` is in neither table — it is 1.12-absent
|
||||
//! and ours, so it stays where it is. The map is the unit.
|
||||
//! the real client and must keep failing here. (`SetSize` used to sit outside the 19 for the
|
||||
//! opposite reason — in neither table because 1.12 has no such verb at all; decision 2142 removed
|
||||
//! it rather than filing it.) The map is the unit.
|
||||
|
||||
use mlua::{Function, Lua, MultiValue, Table, Value};
|
||||
|
||||
|
||||
@@ -1,18 +1,28 @@
|
||||
//! The `ScrollFrame` method surface (`CSimpleScrollFrame`) — the ScrollFrame mechanism (decision
|
||||
//! 0112, the engine's last structural gap): `SetScrollChild`/`GetScrollChild`, the vertical scroll
|
||||
//! offset (`SetVerticalScroll`/`GetVerticalScroll`), and the live range
|
||||
//! (`GetVerticalScrollRange`/`UpdateScrollChildRect`). The offset setter and the range are
|
||||
//! byte-pinned (decisions 1338, 2017): `0x786db0` stores the offset VERBATIM — the engine never
|
||||
//! clamps it — and `0x786e30` measures the range off the scroll child's subtree. The rest is
|
||||
//! spec-faithful to the documented widget contract (the Era `ScrollFrameTemplate` Lua drives its
|
||||
//! scrollbar off exactly these), same posture as StatusBar's fill. Horizontal scroll
|
||||
//! (`SetHorizontalScroll`/…) is out of scope: no 1.12 template drives it.
|
||||
//! 0112, the engine's last structural gap): `SetScrollChild`/`GetScrollChild`, the two scroll
|
||||
//! offsets (`Set`/`GetVerticalScroll`, `Set`/`GetHorizontalScroll`), and the live ranges
|
||||
//! (`Get{Vertical,Horizontal}ScrollRange`/`UpdateScrollChildRect`). The offset setters and the
|
||||
//! ranges are byte-pinned (decisions 1338, 2017): `0x786db0` stores the offset VERBATIM — the
|
||||
//! engine never clamps it — and `0x786e30` measures both ranges off the scroll child's subtree. The
|
||||
//! rest is spec-faithful to the documented widget contract (the Era `ScrollFrameTemplate` Lua
|
||||
//! drives its scrollbar off exactly these), same posture as StatusBar's fill.
|
||||
//!
|
||||
//! The actual geometry — the scroll child's anchors pinned to the frame + the scroll offset, and
|
||||
//! **The two axes are one mechanism, not a mechanism and a special case.** The reference's nine
|
||||
//! ScrollFrame bindings are four symmetric pairs plus `UpdateScrollChildRect`, and the C++ under
|
||||
//! them is literally cloned: `0x786d30` (horizontal) is the byte-clone of `0x786db0` (vertical)
|
||||
//! against `[+0x324]`/`[+0x32c]` instead of `[+0x328]`/`[+0x334]`, and one `0x786e30` computes
|
||||
//! both ranges out of one subtree union (wow-re `scrollframe-offset-and-range-law.md`). This module
|
||||
//! is written that way too — one helper per concept, taken on the axis asked for. That the
|
||||
//! horizontal half went unwritten until the corpus asked for it (`aux-addon` reads and writes both
|
||||
//! axes in one function) is why this doc used to say horizontal scroll "is out of scope: no 1.12
|
||||
//! template drives it" — true of the stock templates, and never true of the addons.
|
||||
//!
|
||||
//! The actual geometry — the scroll child's anchors pinned to the frame + the scroll offsets, and
|
||||
//! the clip every descendant of the child draws/hits within — lives in [`super::mod@super`]'s
|
||||
//! `resolve`/`extract`/`hit_test` (the layout-graph override + the ancestor-walk clip helper); this
|
||||
//! module is the thin Lua binding over the per-frame [`ScrollFrameState`], plus the two script
|
||||
//! hooks (`OnVerticalScroll`/`OnScrollRangeChanged`) a scrollbar's `OnValueChanged`/`OnLoad` wires to.
|
||||
//! module is the thin Lua binding over the per-frame [`ScrollFrameState`], plus the three script
|
||||
//! hooks (`OnVerticalScroll`/`OnHorizontalScroll`/`OnScrollRangeChanged`) a scrollbar's
|
||||
//! `OnValueChanged`/`OnLoad` wires to.
|
||||
//!
|
||||
//! The methods live in their own registry table, consulted by the frame `__index` dispatcher only
|
||||
//! for ScrollFrame frames — so duck-typing addons (`if frame.SetScrollChild then …`) see `nil` on
|
||||
@@ -47,9 +57,23 @@ fn with_scroll<T>(
|
||||
}
|
||||
}
|
||||
|
||||
/// `GetVerticalScrollRange()`'s live computation: `max(0, contentExtent − frameHeight)` from the
|
||||
/// **resolved** rects — `0.0` when the frame is unresolved or there is no child. Never cached: the
|
||||
/// scroll offset always clamps against the current layout, not a stale snapshot.
|
||||
/// Which axis a scroll offset, span or range is taken on.
|
||||
///
|
||||
/// One type rather than two code paths, because the reference computes both ranges in ONE pass
|
||||
/// over ONE subtree union (`0x786e30`: `[+0x31c] = max(0, maxRIGHT − minLEFT − width) / scale`,
|
||||
/// `[+0x320] = max(0, maxTOP − minBOTTOM − height) / scale`) — the axes differ only in which pair
|
||||
/// of edges the union tracks and which frame dimension is subtracted.
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
enum Axis {
|
||||
/// `maxRIGHT − minLEFT` against the frame's width — `GetHorizontalScrollRange`, `[+0x31c]`.
|
||||
Horizontal,
|
||||
/// `maxTOP − minBOTTOM` against the frame's height — `GetVerticalScrollRange`, `[+0x320]`.
|
||||
Vertical,
|
||||
}
|
||||
|
||||
/// `Get{Vertical,Horizontal}ScrollRange()`'s live computation: `max(0, contentExtent − frameExtent)`
|
||||
/// from the **resolved** rects — `0.0` when the frame is unresolved or there is no child. Never
|
||||
/// cached: the scroll offset always clamps against the current layout, not a stale snapshot.
|
||||
///
|
||||
/// **The content extent is the scroll child's whole SUBTREE, not the child frame's own height**
|
||||
/// (wow-re `system/ui/scratch/simplehtml-markup-engine.md` §4.5, byte-identified during the
|
||||
@@ -66,7 +90,7 @@ fn with_scroll<T>(
|
||||
/// directly — the guild-info EditBox, the options panes, every faux list — is unchanged, because
|
||||
/// there its own rect already IS its subtree.
|
||||
///
|
||||
/// In **LOCAL units**, each height divided by the owning frame's effective scale — i.e. exactly
|
||||
/// In **LOCAL units**, each extent divided by the owning frame's effective scale — i.e. exactly
|
||||
/// `child:GetHeight() - self:GetHeight()`, the widget contract this method states. The resolved
|
||||
/// rects are screen px, and the scroll offset is NOT: `SetVerticalScroll` becomes the scroll
|
||||
/// child's anchor y-offset, which the solver multiplies by that child's scale (`resolve`'s
|
||||
@@ -75,7 +99,7 @@ fn with_scroll<T>(
|
||||
/// stops short of the end and the tail of the content is unreachable. Invisible at scale 1.0,
|
||||
/// which is every 1.12-native window; the era-scaled options window (ERA_WINDOW_SCALE 0.78) is the
|
||||
/// first real-scroll customer that isn't, and it lost 22% of its travel.
|
||||
fn scroll_range(model: &Model, h: FrameHandle) -> f32 {
|
||||
fn scroll_range(model: &Model, h: FrameHandle, axis: Axis) -> f32 {
|
||||
let child = match model.arena.frame(h).map(|f| &f.kind_state) {
|
||||
Some(KindState::Scroll(s)) => s.child,
|
||||
_ => None,
|
||||
@@ -89,28 +113,39 @@ fn scroll_range(model: &Model, h: FrameHandle) -> f32 {
|
||||
.filter(|s| s.abs() >= 1e-6)
|
||||
.unwrap_or(1.0)
|
||||
};
|
||||
let Some(frame_h) = model.resolved.get(&h).map(|r| r.height() / scale_of(h)) else {
|
||||
let Some(viewport) = model.resolved.get(&h).map(|r| {
|
||||
let extent = match axis {
|
||||
Axis::Horizontal => r.width(),
|
||||
Axis::Vertical => r.height(),
|
||||
};
|
||||
extent / scale_of(h)
|
||||
}) else {
|
||||
return 0.0;
|
||||
};
|
||||
let Some((top, bottom)) = subtree_span(model, child) else {
|
||||
let Some((far, near)) = subtree_span(model, child, axis) else {
|
||||
return 0.0;
|
||||
};
|
||||
((top - bottom) / scale_of(child) - frame_h).max(0.0)
|
||||
((far - near) / scale_of(child) - viewport).max(0.0)
|
||||
}
|
||||
|
||||
/// The vertical span `(top, bottom)` of `f`'s subtree in SCREEN px — its own resolved rect unioned
|
||||
/// with every VISIBLE descendant region and child frame, recursively (`0x786f80`). `None` when
|
||||
/// nothing in the subtree resolved.
|
||||
/// The span of `f`'s subtree along `axis`, in SCREEN px, as `(far, near)` — `(top, bottom)`
|
||||
/// vertically, `(right, left)` horizontally — its own resolved rect unioned with every VISIBLE
|
||||
/// descendant region and child frame, recursively (`0x786f80`). `None` when nothing in the subtree
|
||||
/// resolved.
|
||||
///
|
||||
/// Visibility is the client's own guard on both lists: a hidden block contributes no scroll range,
|
||||
/// which is what stops a window's parked art from inventing travel nobody can use.
|
||||
fn subtree_span(model: &Model, f: FrameHandle) -> Option<(f32, f32)> {
|
||||
fn subtree_span(model: &Model, f: FrameHandle, axis: Axis) -> Option<(f32, f32)> {
|
||||
let edges = |r: &crate::layout::Rect| match axis {
|
||||
Axis::Horizontal => (r.right, r.left),
|
||||
Axis::Vertical => (r.top, r.bottom),
|
||||
};
|
||||
let frame = model.arena.frame(f)?;
|
||||
let mut span: Option<(f32, f32)> = model.resolved.get(&f).map(|r| (r.top, r.bottom));
|
||||
let union = |s: &mut Option<(f32, f32)>, t: f32, b: f32| {
|
||||
let mut span: Option<(f32, f32)> = model.resolved.get(&f).map(edges);
|
||||
let union = |s: &mut Option<(f32, f32)>, far: f32, near: f32| {
|
||||
*s = Some(match *s {
|
||||
Some((ot, ob)) => (ot.max(t), ob.min(b)),
|
||||
None => (t, b),
|
||||
Some((of, on)) => (of.max(far), on.min(near)),
|
||||
None => (far, near),
|
||||
});
|
||||
};
|
||||
for &rh in &frame.regions {
|
||||
@@ -118,15 +153,16 @@ fn subtree_span(model: &Model, f: FrameHandle) -> Option<(f32, f32)> {
|
||||
continue;
|
||||
}
|
||||
if let Some(r) = model.region_resolved.get(&rh) {
|
||||
union(&mut span, r.top, r.bottom);
|
||||
let (far, near) = edges(r);
|
||||
union(&mut span, far, near);
|
||||
}
|
||||
}
|
||||
for &ch in &frame.children {
|
||||
if !model.arena.frame(ch).is_some_and(|c| c.effective_visible) {
|
||||
continue;
|
||||
}
|
||||
if let Some((t, b)) = subtree_span(model, ch) {
|
||||
union(&mut span, t, b);
|
||||
if let Some((far, near)) = subtree_span(model, ch, axis) {
|
||||
union(&mut span, far, near);
|
||||
}
|
||||
}
|
||||
span
|
||||
@@ -242,7 +278,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
return Ok(());
|
||||
}
|
||||
lua.app_data_mut::<Model>().expect("model").touch_layout();
|
||||
fire_vertical_scroll(lua, &this, px)
|
||||
fire_scroll(lua, &this, "OnVerticalScroll", px)
|
||||
})?,
|
||||
)?;
|
||||
m.set(
|
||||
@@ -254,7 +290,52 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
lua.create_function(|lua, this: Table| {
|
||||
let h = frame_handle_of(lua, &this)?;
|
||||
let model = lua.app_data_ref::<Model>().expect("model app_data");
|
||||
Ok(scroll_range(&model, h))
|
||||
Ok(scroll_range(&model, h, Axis::Vertical))
|
||||
})?,
|
||||
)?;
|
||||
|
||||
// ── The horizontal axis: the same three verbs on `[+0x324]`/`[+0x31c]` ─────────────────────
|
||||
//
|
||||
// `SetHorizontalScroll(px)` `0x7912f0` → `0x786d30`, which is the BYTE-CLONE of
|
||||
// `SetVerticalScroll`'s `0x786db0` against the horizontal fields (wow-re
|
||||
// `scrollframe-offset-and-range-law.md` §2.2; `item9-firing34-glue786.md` identified the pair).
|
||||
// So every law stated above holds verbatim on this axis: no clamp, the same 2⁻²² change-gate
|
||||
// against the OLD offset alone, the same re-anchor, and `OnHorizontalScroll(self, offset)`
|
||||
// fired only on a real change. Nothing here is a horizontal special case.
|
||||
//
|
||||
// The one thing that IS specific to this axis is the SIGN, and it is the reference's:
|
||||
// `0x787100` passes both offsets to `SetPoint(TOPLEFT, self, TOPLEFT, +hScroll, +vScroll)`
|
||||
// unnegated, so positive x pushes the child right (revealing nothing) where positive y lifts it
|
||||
// (revealing content below). `aux-addon/tabs/search/filter.lua:315-322` — the corpus caller
|
||||
// that made this pair worth writing, reading both axes in one line and writing both in the next
|
||||
// — bounds x into `[min(0, frameWidth - contentWidth - 10), 0]` and y into
|
||||
// `[0, contentHeight - frameHeight]`, which is the same asymmetry read off the other side of
|
||||
// the API. See [`ScrollFrameState::horizontal`].
|
||||
m.set(
|
||||
"SetHorizontalScroll",
|
||||
lua.create_function(|lua, (this, px): (Table, f32)| {
|
||||
let changed = with_scroll(lua, &this, |s| {
|
||||
let changed = s.horizontal.to_bits() != px.to_bits();
|
||||
s.horizontal = px;
|
||||
changed
|
||||
})?;
|
||||
if !changed {
|
||||
return Ok(());
|
||||
}
|
||||
lua.app_data_mut::<Model>().expect("model").touch_layout();
|
||||
fire_scroll(lua, &this, "OnHorizontalScroll", px)
|
||||
})?,
|
||||
)?;
|
||||
m.set(
|
||||
"GetHorizontalScroll",
|
||||
lua.create_function(|lua, this: Table| with_scroll(lua, &this, |s| s.horizontal))?,
|
||||
)?;
|
||||
m.set(
|
||||
"GetHorizontalScrollRange",
|
||||
lua.create_function(|lua, this: Table| {
|
||||
let h = frame_handle_of(lua, &this)?;
|
||||
let model = lua.app_data_ref::<Model>().expect("model app_data");
|
||||
Ok(scroll_range(&model, h, Axis::Horizontal))
|
||||
})?,
|
||||
)?;
|
||||
// UpdateScrollChildRect() — the Era template calls this after resizing its content to re-drive
|
||||
@@ -292,21 +373,33 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
for rh in strings {
|
||||
super::measure::ensure_measured(lua, rh);
|
||||
}
|
||||
let (id, range) = {
|
||||
let (id, x_range, y_range) = {
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
|
||||
super::UiScript::resolve_layout(&mut model);
|
||||
let range = scroll_range(&model, h);
|
||||
(model.frame_id(h), range)
|
||||
let x_range = scroll_range(&model, h, Axis::Horizontal);
|
||||
let y_range = scroll_range(&model, h, Axis::Vertical);
|
||||
(model.frame_id(h), x_range, y_range)
|
||||
};
|
||||
// That forced resolve can have MOVED sizes — this method exists precisely because the
|
||||
// caller just resized its content — so drain the queue before the range notify: the
|
||||
// reference fires `OnSizeChanged` from `ApplyRect`, i.e. during the layout, not after.
|
||||
event::fire_size_changes(lua);
|
||||
// `OnScrollRangeChanged(self, xRange, yRange)` — **horizontal first**, and that order is
|
||||
// byte-derived rather than assumed: at `0x786eff` the vertical result is on top of the
|
||||
// x87 stack, is `fstp`'d first and therefore pushed DEEPEST, so it lands as `arg2`
|
||||
// (wow-re `scrollframe-offset-and-range-law.md` §3.6, where two of the round's own
|
||||
// workers published it the other way round). It is why stock
|
||||
// `UIPanelScrollFrameTemplate` hands `arg2` to `ScrollFrame_OnScrollRangeChanged`.
|
||||
// `arg1` was a hardcoded `0.0` here for as long as there was no horizontal range to
|
||||
// put in it.
|
||||
if let Err(e) = event::fire_widget_handler(
|
||||
lua,
|
||||
id,
|
||||
"OnScrollRangeChanged",
|
||||
vec![Value::Number(0.0), Value::Number(f64::from(range))],
|
||||
vec![
|
||||
Value::Number(f64::from(x_range)),
|
||||
Value::Number(f64::from(y_range)),
|
||||
],
|
||||
) {
|
||||
lua.app_data_mut::<Model>()
|
||||
.expect("model app_data")
|
||||
@@ -321,20 +414,18 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Fire `OnVerticalScroll(self, offset)` (the ScrollFrame's own script slot). Fired outside any
|
||||
/// model borrow; errors go to [`Model::errors`] like every other widget handler.
|
||||
fn fire_vertical_scroll(lua: &Lua, this: &Table, value: f32) -> mlua::Result<()> {
|
||||
/// Fire `On{Vertical,Horizontal}Scroll(self, offset)` (the ScrollFrame's own script slots — the
|
||||
/// reference's `[+0x334]`/`[+0x32c]`, fired from the setter clones `0x786db0`/`0x786d30`). Fired
|
||||
/// outside any model borrow; errors go to [`Model::errors`] like every other widget handler.
|
||||
fn fire_scroll(lua: &Lua, this: &Table, script: &str, value: f32) -> mlua::Result<()> {
|
||||
let id = {
|
||||
let h = frame_handle_of(lua, this)?;
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
|
||||
model.frame_id(h)
|
||||
};
|
||||
if let Err(e) = event::fire_widget_handler(
|
||||
lua,
|
||||
id,
|
||||
"OnVerticalScroll",
|
||||
vec![Value::Number(f64::from(value))],
|
||||
) {
|
||||
if let Err(e) =
|
||||
event::fire_widget_handler(lua, id, script, vec![Value::Number(f64::from(value))])
|
||||
{
|
||||
lua.app_data_mut::<Model>()
|
||||
.expect("model app_data")
|
||||
.errors
|
||||
|
||||
@@ -171,21 +171,23 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
})?,
|
||||
)?;
|
||||
|
||||
// Enable/Disable/IsEnabled — a disabled slider ignores thumb drag (gated in the pointer path).
|
||||
m.set(
|
||||
"Enable",
|
||||
lua.create_function(|lua, this: Table| with_slider(lua, &this, |s| s.enabled = true))?,
|
||||
)?;
|
||||
m.set(
|
||||
"Disable",
|
||||
lua.create_function(|lua, this: Table| with_slider(lua, &this, |s| s.enabled = false))?,
|
||||
)?;
|
||||
m.set(
|
||||
"IsEnabled",
|
||||
// 1.12 returns 1/nil; the house API target is the Era boolean (decision 0068) — every
|
||||
// transcribed `if slider:IsEnabled()` reads both identically.
|
||||
lua.create_function(|lua, this: Table| with_slider(lua, &this, |s| s.enabled))?,
|
||||
)?;
|
||||
// `Enable`/`Disable`/`IsEnabled` WERE here and are GONE, with the `enabled` state they drove.
|
||||
// **A 1.12 Slider has no enabled state at all**: the three names are registered once each, in
|
||||
// the BUTTON table `0x879d00` (`0x77fef0`/`0x77ffd0`/`0x7800b0`), and the Slider's own LoadXML
|
||||
// `0x789580` takes `drawLayer`/`minValue`/`maxValue`/`valueStep`/`defaultValue`/`orientation`
|
||||
// and nothing else (wow-re `rf28-typed-widget-loadxml.md` §Slider) — so there was no way in
|
||||
// from Lua *or* from XML, and the flag could only ever read `true`.
|
||||
//
|
||||
// They were a superset in PRESENCE, which is what 1189 records the cost of: a duck-typing addon
|
||||
// that branches on `if widget.IsEnabled then` reads a Slider as a Button. Ours also answered a
|
||||
// Lua BOOLEAN, which the reference has no query that does (decision 2118) — `IsEnabled 0x7800b0`
|
||||
// is number-1/number-0 and never even nil.
|
||||
//
|
||||
// Removed rather than corrected because nothing calls them on a Slider receiver: a
|
||||
// receiver-typed grep over this repo, `assets/ui`, the stock FrameXML/GlueXML and both addon
|
||||
// corpora (110 top-20 + 219 vanilla) finds zero `slider*:Enable/Disable/IsEnabled` sites.
|
||||
// `begin_drag` below lost its `enabled` gate with them — behaviour-neutral, since no caller
|
||||
// could clear the flag.
|
||||
|
||||
// SetThumbTexture(path [, drawLayer]) | SetThumbTexture(r, g, b [, a]) — the same two forms as a
|
||||
// region's SetTexture, targeting the thumb region (created on first use). Mirrors
|
||||
@@ -339,10 +341,15 @@ pub(crate) struct SliderDrag {
|
||||
pub(crate) grab_offset: f32,
|
||||
}
|
||||
|
||||
/// On a LeftButton press at `(x, y)` whose hit frame is `hit`: if that frame is an **enabled**
|
||||
/// Slider with a resolved rect, begin a drag capture (records [`Model::slider_drag`]). Where the
|
||||
/// press grabs is [`slider_grab`]'s to say — one law, shared with the glue lane's scrollbars, so
|
||||
/// an in-game bar and a character-screen bar cannot drift apart on feel.
|
||||
/// On a LeftButton press at `(x, y)` whose hit frame is `hit`: if that frame is a Slider with a
|
||||
/// resolved rect, begin a drag capture (records [`Model::slider_drag`]). Where the press grabs is
|
||||
/// [`slider_grab`]'s to say — one law, shared with the glue lane's scrollbars, so an in-game bar
|
||||
/// and a character-screen bar cannot drift apart on feel.
|
||||
///
|
||||
/// There is no enabled gate: 1.12 gives a Slider no enabled state to gate on (see `install`'s note
|
||||
/// where `Enable`/`Disable`/`IsEnabled` used to be). A slider that should not take the press is
|
||||
/// kept off it the way the reference keeps anything off it — `EnableMouse(false)`, upstream of here
|
||||
/// in the hit test.
|
||||
///
|
||||
/// Returns `Some((frame id, new value))` when the press itself changed the value (the track jump)
|
||||
/// — the caller fires `OnValueChanged` outside the model borrow, exactly like [`drag_move`]'s
|
||||
@@ -355,13 +362,10 @@ pub(super) fn begin_drag(
|
||||
) -> Option<(u32, f32)> {
|
||||
let h = hit?;
|
||||
let r = model.resolved.get(&h).copied()?;
|
||||
let (enabled, vertical, fraction, thumb) = match model.arena.frame(h).map(|f| &f.kind_state) {
|
||||
Some(KindState::Slider(s)) => (s.enabled, s.vertical, s.fraction(), s.thumb),
|
||||
let (vertical, fraction, thumb) = match model.arena.frame(h).map(|f| &f.kind_state) {
|
||||
Some(KindState::Slider(s)) => (s.vertical, s.fraction(), s.thumb),
|
||||
_ => return None,
|
||||
};
|
||||
if !enabled {
|
||||
return None;
|
||||
}
|
||||
// No thumb region → `0x789ba0`'s `+0x328` gate: the press still captures (the dispatcher's
|
||||
// `mov [ebx+0x80],esi` is unconditional), the value never moves.
|
||||
let Some(size) = thumb_extent(model, thumb) else {
|
||||
|
||||
@@ -0,0 +1,191 @@
|
||||
//! **benilla's widget METHOD surface, asked of a running VM** — the shared measurement behind
|
||||
//! the `dump_widget_methods` example and the widget-surface gate
|
||||
//! (`script::tests::widget_surface`, decision 2142).
|
||||
//!
|
||||
//! It exists for the same reason `dump_globals` does (decisions 1188, 1189): **it is a run, not a
|
||||
//! grep**. `_G` is not the whole surface an addon can tell apart — most of what an addon touches
|
||||
//! is reached through a *widget*, and an addon's test for "does this client have X" is
|
||||
//! `if frame.SetBackdrop then`, i.e. what the `__index` chain answers on a live instance. A regex
|
||||
//! over `m.set("Name", …)` cannot answer that: it cannot see which registry table a kind actually
|
||||
//! chains to, it cannot see the Texture/FontString leaf split ([`super::region`]), and it reads a
|
||||
//! name registered under a `format!` family as one method.
|
||||
//!
|
||||
//! So every row is one live object being asked `type(obj.Name) == "function"`, exactly as an addon
|
||||
//! asks it. The only thing **not** asked of the VM is the *candidate name list* — a metatable whose
|
||||
//! `__index` is a Rust dispatcher is not enumerable from Lua, so the candidates are the union of
|
||||
//! the keys of the VM's own method tables ([`METHOD_TABLES`]), read out of the Lua registry at
|
||||
//! runtime rather than out of the source. A key that no longer resolves to a table is a hard error
|
||||
//! rather than a silent loss of candidates: a quietly-missing candidate source would under-report
|
||||
//! the surface, which is the exact failure this measurement exists to prevent.
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
use mlua::{Table, Value};
|
||||
|
||||
use super::UiScript;
|
||||
|
||||
/// The named-registry method tables the VM installs — the **candidate name source**, read from the
|
||||
/// live registry.
|
||||
///
|
||||
/// This is not the class chain (the chain is `object::kind_method_registries`' business and this
|
||||
/// instrument deliberately does not model it — it asks the instance instead). It is only the
|
||||
/// universe of names worth probing: every name benilla registers on any widget appears as a key in
|
||||
/// one of these, so the union is a superset of our surface and the per-instance probe decides
|
||||
/// membership.
|
||||
const METHOD_TABLES: &[&str] = &[
|
||||
"__benilla_frame_methods",
|
||||
"__benilla_region_methods",
|
||||
"__benilla_texture_methods",
|
||||
"__benilla_fontstring_methods",
|
||||
"__benilla_title_methods",
|
||||
"__benilla_font_methods",
|
||||
"__benilla_button_methods",
|
||||
"__benilla_checkbutton_methods",
|
||||
"__benilla_lootbutton_methods",
|
||||
"__benilla_editbox_methods",
|
||||
"__benilla_statusbar_methods",
|
||||
"__benilla_slider_methods",
|
||||
"__benilla_scrollframe_methods",
|
||||
"__benilla_simplehtml_methods",
|
||||
"__benilla_colorselect_methods",
|
||||
"__benilla_model_methods",
|
||||
"__benilla_playermodel_methods",
|
||||
"__benilla_dressupmodel_methods",
|
||||
"__benilla_tabardmodel_methods",
|
||||
"__benilla_plain_messageframe_methods",
|
||||
"__benilla_scrollingmessageframe_methods",
|
||||
"__benilla_minimap_methods",
|
||||
"__benilla_tooltip_methods",
|
||||
];
|
||||
|
||||
/// Every widget/region/font class benilla can hand an addon, and one Lua expression that makes one.
|
||||
///
|
||||
/// **Ordered, and the order is load-bearing**: the region and font rows are made *by* the plain
|
||||
/// frame the first row creates, so `Frame` comes first. Each instance is published as the global
|
||||
/// `DW_<Class>`; the frame kinds also publish their own `CreateFrame` name, which is harmless.
|
||||
///
|
||||
/// `TaxiRouteFrame` is absent on purpose — it is a registered `CreateFrame` type that *is* a
|
||||
/// `Frame` and nothing else (decision 1828), so it would be the `Frame` row twice.
|
||||
const CLASSES: &[(&str, &str)] = &[
|
||||
("Frame", r#"CreateFrame("Frame", "DWFrameN", UIParent)"#),
|
||||
(
|
||||
"WorldFrame",
|
||||
r#"CreateFrame("WorldFrame", "DWWorldN", UIParent)"#,
|
||||
),
|
||||
("Button", r#"CreateFrame("Button", "DWButtonN", UIParent)"#),
|
||||
(
|
||||
"LootButton",
|
||||
r#"CreateFrame("LootButton", "DWLootN", UIParent)"#,
|
||||
),
|
||||
(
|
||||
"CheckButton",
|
||||
r#"CreateFrame("CheckButton", "DWCheckN", UIParent)"#,
|
||||
),
|
||||
("EditBox", r#"CreateFrame("EditBox", "DWEditN", UIParent)"#),
|
||||
(
|
||||
"StatusBar",
|
||||
r#"CreateFrame("StatusBar", "DWStatusN", UIParent)"#,
|
||||
),
|
||||
("Slider", r#"CreateFrame("Slider", "DWSliderN", UIParent)"#),
|
||||
(
|
||||
"ScrollFrame",
|
||||
r#"CreateFrame("ScrollFrame", "DWScrollN", UIParent)"#,
|
||||
),
|
||||
("Model", r#"CreateFrame("Model", "DWModelN", UIParent)"#),
|
||||
(
|
||||
"PlayerModel",
|
||||
r#"CreateFrame("PlayerModel", "DWPlayerModelN", UIParent)"#,
|
||||
),
|
||||
(
|
||||
"DressUpModel",
|
||||
r#"CreateFrame("DressUpModel", "DWDressN", UIParent)"#,
|
||||
),
|
||||
(
|
||||
"TabardModel",
|
||||
r#"CreateFrame("TabardModel", "DWTabardN", UIParent)"#,
|
||||
),
|
||||
(
|
||||
"MessageFrame",
|
||||
r#"CreateFrame("MessageFrame", "DWMessageN", UIParent)"#,
|
||||
),
|
||||
(
|
||||
"ScrollingMessageFrame",
|
||||
r#"CreateFrame("ScrollingMessageFrame", "DWScrollMsgN", UIParent)"#,
|
||||
),
|
||||
(
|
||||
"ColorSelect",
|
||||
r#"CreateFrame("ColorSelect", "DWColorN", UIParent)"#,
|
||||
),
|
||||
(
|
||||
"SimpleHTML",
|
||||
r#"CreateFrame("SimpleHTML", "DWHtmlN", UIParent)"#,
|
||||
),
|
||||
(
|
||||
"MovieFrame",
|
||||
r#"CreateFrame("MovieFrame", "DWMovieN", UIParent)"#,
|
||||
),
|
||||
(
|
||||
"GameTooltip",
|
||||
r#"CreateFrame("GameTooltip", "DWTooltipN", UIParent)"#,
|
||||
),
|
||||
(
|
||||
"Minimap",
|
||||
r#"CreateFrame("Minimap", "DWMinimapN", UIParent)"#,
|
||||
),
|
||||
("Texture", r#"DW_Frame:CreateTexture("DWTexN")"#),
|
||||
("FontString", r#"DW_Frame:CreateFontString("DWFSN")"#),
|
||||
("TitleRegion", r#"DW_Frame:CreateTitleRegion()"#),
|
||||
("Font", r#"CreateFont("DWFontN")"#),
|
||||
];
|
||||
|
||||
/// Every `(class, method)` pair this VM answers — one live instance per class, probed with the
|
||||
/// membership test an addon writes.
|
||||
///
|
||||
/// **A class that cannot be instantiated is REPORTED, never skipped**: its row is
|
||||
/// `(class, "!NOT-INSTANTIABLE")`. A census that silently drops a class reads as "that class has
|
||||
/// no methods", which is the wrong answer to a different question.
|
||||
pub fn widget_method_census(script: &UiScript) -> mlua::Result<Vec<(String, String)>> {
|
||||
let mut candidates: BTreeSet<String> = BTreeSet::new();
|
||||
for key in METHOD_TABLES {
|
||||
let table: Table = script.lua().named_registry_value(key).map_err(|e| {
|
||||
mlua::Error::runtime(format!(
|
||||
"candidate source '{key}' is not a table in this VM ({e}) — a renamed or retired \
|
||||
method table would silently shrink this census; fix the list, do not ignore it"
|
||||
))
|
||||
})?;
|
||||
for pair in table.pairs::<Value, Value>() {
|
||||
let (k, _) = pair?;
|
||||
if let Value::String(s) = k {
|
||||
candidates.insert(s.to_str()?.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
// Hand the names to the VM once; the per-class probe is then one chunk, not one per name.
|
||||
let names = script.lua().create_table()?;
|
||||
for (i, n) in candidates.iter().enumerate() {
|
||||
names.set(i + 1, n.as_str())?;
|
||||
}
|
||||
script.lua().globals().set("DW_NAMES", names)?;
|
||||
|
||||
let mut rows: Vec<(String, String)> = Vec::new();
|
||||
for (class, expr) in CLASSES {
|
||||
if let Err(e) = script.run(&format!("DW_{class} = {expr}")) {
|
||||
eprintln!("{class}: could not be instantiated: {e}");
|
||||
rows.push(((*class).to_string(), "!NOT-INSTANTIABLE".to_string()));
|
||||
continue;
|
||||
}
|
||||
let found: Vec<String> = script.eval(&format!(
|
||||
"local o = DW_{class} \
|
||||
local out = {{}} \
|
||||
for i = 1, table.getn(DW_NAMES) do \
|
||||
local n = DW_NAMES[i] \
|
||||
if type(o[n]) == 'function' then table.insert(out, n) end \
|
||||
end \
|
||||
return out"
|
||||
))?;
|
||||
for name in found {
|
||||
rows.push(((*class).to_string(), name));
|
||||
}
|
||||
}
|
||||
rows.sort();
|
||||
Ok(rows)
|
||||
}
|
||||
@@ -242,7 +242,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
|
||||
// `UnitOnTaxi(unit)` (`0x517a40`) → **the number 1 or nil**, never a Lua boolean — the unit
|
||||
// predicate family's one return shape (decisions 1830, 2043; the push law is
|
||||
// `crate::script::binding_abi::predicate`). It lives here rather than beside its 22 siblings
|
||||
// `crate::script::binding_abi::flag`). It lives here rather than beside its 22 siblings
|
||||
// because the ride flag it reads is this module's, not `UnitState`'s — which is exactly how it
|
||||
// came to be the one predicate still answering a `bool` after the family was fixed.
|
||||
//
|
||||
@@ -260,7 +260,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
let unit =
|
||||
crate::script::binding_abi::string_arg(lua, unit, r#"Usage: UnitOnTaxi("unit")"#)?;
|
||||
let model = lua.app_data_ref::<Model>().expect("model");
|
||||
Ok(crate::script::binding_abi::predicate(
|
||||
Ok(crate::script::binding_abi::flag(
|
||||
unit.eq_ignore_ascii_case("player") && model.taxi_riding,
|
||||
))
|
||||
})?,
|
||||
|
||||
@@ -15,7 +15,7 @@ fn setpoint_explicit_nil_relative_to_keeps_offsets() {
|
||||
r#"
|
||||
local f = CreateFrame("Frame", "Nil")
|
||||
f:SetPoint("TOPLEFT", nil, "TOPLEFT", 40, -40)
|
||||
f:SetSize(300, 200)
|
||||
f:SetWidth(300); f:SetHeight(200)
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
@@ -40,7 +40,7 @@ fn setpoint_resolve_size_and_rect() {
|
||||
r#"
|
||||
local f = CreateFrame("Frame", "Sized")
|
||||
f:SetPoint("TOPLEFT", 10, -5) -- relativeTo = screen (default), relativePoint = TOPLEFT
|
||||
f:SetSize(200, 50)
|
||||
f:SetWidth(200); f:SetHeight(50)
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
@@ -68,7 +68,7 @@ fn setpoint_resolve_size_and_rect() {
|
||||
#[test]
|
||||
fn getwidth_falls_back_to_explicit_size_before_resolve() {
|
||||
let s = script();
|
||||
// No SetPoint ⇒ unresolvable; GetWidth returns the explicit SetSize value.
|
||||
// No SetPoint ⇒ unresolvable; GetWidth returns the explicit SetWidth value.
|
||||
let w: f32 = s
|
||||
.eval(r#"local f = CreateFrame("Frame"); f:SetWidth(123); return f:GetWidth()"#)
|
||||
.unwrap();
|
||||
|
||||
@@ -13,7 +13,7 @@ fn backdrop_installs_and_extracts_pieces_with_colors() {
|
||||
r#"
|
||||
local f = CreateFrame("Frame", "Plate")
|
||||
f:SetPoint("TOPLEFT", nil, "TOPLEFT", 100, -100)
|
||||
f:SetSize(200, 100)
|
||||
f:SetWidth(200); f:SetHeight(100)
|
||||
f:SetBackdrop({
|
||||
bgFile = "bg", edgeFile = "edge", tile = true,
|
||||
tileSize = 16, edgeSize = 16,
|
||||
@@ -129,7 +129,7 @@ fn get_backdrop_round_trips_through_set_backdrop() {
|
||||
r#"
|
||||
local f = CreateFrame("Frame", "BC2Plate")
|
||||
f:SetPoint("CENTER")
|
||||
f:SetSize(54, 54)
|
||||
f:SetWidth(54); f:SetHeight(54)
|
||||
f:SetBackdrop({ bgFile = "bg", edgeFile = "edge", tile = true,
|
||||
tileSize = 32, edgeSize = 32,
|
||||
insets = { left = 11, right = 12, top = 12, bottom = 11 } })
|
||||
@@ -166,7 +166,7 @@ fn set_backdrop_nil_tears_down() {
|
||||
r#"
|
||||
local f = CreateFrame("Frame", "Plate2")
|
||||
f:SetPoint("CENTER")
|
||||
f:SetSize(100, 100)
|
||||
f:SetWidth(100); f:SetHeight(100)
|
||||
f:SetBackdrop({ bgFile = "bg", edgeFile = "edge" })
|
||||
f:SetBackdrop(nil)
|
||||
"#,
|
||||
|
||||
@@ -34,7 +34,7 @@ fn button_state_textures_switch_with_interaction() {
|
||||
s.run(
|
||||
r#"
|
||||
local b = CreateFrame("Button", "StateBtn")
|
||||
b:SetPoint("BOTTOMLEFT", 0, 0); b:SetSize(100, 100)
|
||||
b:SetPoint("BOTTOMLEFT", 0, 0); b:SetWidth(100); b:SetHeight(100)
|
||||
b:SetNormalTexture("Interface\\N.blp")
|
||||
b:SetPushedTexture("Interface\\P.blp")
|
||||
b:SetDisabledTexture("Interface\\D.blp")
|
||||
@@ -110,7 +110,7 @@ fn a_state_with_no_texture_leaves_the_shown_one_standing() {
|
||||
s.run(
|
||||
r#"
|
||||
local b = CreateFrame("Button", "StickyBtn")
|
||||
b:SetPoint("BOTTOMLEFT", 0, 0); b:SetSize(100, 100)
|
||||
b:SetPoint("BOTTOMLEFT", 0, 0); b:SetWidth(100); b:SetHeight(100)
|
||||
b:SetNormalTexture("Interface\\N.blp")
|
||||
"#,
|
||||
)
|
||||
@@ -162,7 +162,7 @@ fn a_state_with_no_texture_leaves_the_shown_one_standing() {
|
||||
s.run(
|
||||
r#"
|
||||
local b = CreateFrame("Button", "BornDeadBtn")
|
||||
b:SetPoint("BOTTOMLEFT", 200, 0); b:SetSize(100, 100)
|
||||
b:SetPoint("BOTTOMLEFT", 200, 0); b:SetWidth(100); b:SetHeight(100)
|
||||
b:Disable()
|
||||
b:SetNormalTexture("Interface\\N.blp")
|
||||
"#,
|
||||
@@ -191,7 +191,7 @@ fn disabling_a_button_takes_its_whole_highlight_layer() {
|
||||
s.run(
|
||||
r#"
|
||||
local b = CreateFrame("Button", "LayerBtn")
|
||||
b:SetPoint("BOTTOMLEFT", 0, 0); b:SetSize(100, 100)
|
||||
b:SetPoint("BOTTOMLEFT", 0, 0); b:SetWidth(100); b:SetHeight(100)
|
||||
b:SetNormalTexture("Interface\\N.blp")
|
||||
b:SetHighlightTexture("Interface\\H.blp")
|
||||
local own = b:CreateTexture(nil, "HIGHLIGHT")
|
||||
@@ -271,7 +271,7 @@ fn any_registered_mouse_button_shows_the_pushed_texture() {
|
||||
r#"
|
||||
local function slot(name, x)
|
||||
local b = CreateFrame("Button", name)
|
||||
b:SetPoint("BOTTOMLEFT", x, 0); b:SetSize(100, 100)
|
||||
b:SetPoint("BOTTOMLEFT", x, 0); b:SetWidth(100); b:SetHeight(100)
|
||||
b:SetNormalTexture("Interface\\" .. name .. "N.blp")
|
||||
b:SetPushedTexture("Interface\\" .. name .. "P.blp")
|
||||
return b
|
||||
@@ -331,7 +331,7 @@ fn set_button_state_drives_the_pushed_visual() {
|
||||
s.run(
|
||||
r#"
|
||||
local b = CreateFrame("Button", "PushBtn")
|
||||
b:SetPoint("BOTTOMLEFT", 0, 0); b:SetSize(100, 100)
|
||||
b:SetPoint("BOTTOMLEFT", 0, 0); b:SetWidth(100); b:SetHeight(100)
|
||||
b:SetNormalTexture("Interface\\N.blp")
|
||||
b:SetPushedTexture("Interface\\P.blp")
|
||||
"#,
|
||||
@@ -375,7 +375,7 @@ fn disabled_button_swallows_clicks_checkbutton_toggles_before_onclick() {
|
||||
r#"
|
||||
clicks, seen_checked = 0, nil
|
||||
local cb = CreateFrame("CheckButton", "Toggler")
|
||||
cb:SetPoint("BOTTOMLEFT", 0, 0); cb:SetSize(100, 100)
|
||||
cb:SetPoint("BOTTOMLEFT", 0, 0); cb:SetWidth(100); cb:SetHeight(100)
|
||||
cb:SetScript("OnClick", function(self, button, down)
|
||||
clicks = clicks + 1
|
||||
seen_checked = self:GetChecked()
|
||||
@@ -422,7 +422,7 @@ fn default_registration_is_left_click_only_right_click_reaches_nothing() {
|
||||
r#"
|
||||
clicks = 0
|
||||
local btn = CreateFrame("Button", "Vendor")
|
||||
btn:SetPoint("BOTTOMLEFT", 0, 0); btn:SetSize(100, 100)
|
||||
btn:SetPoint("BOTTOMLEFT", 0, 0); btn:SetWidth(100); btn:SetHeight(100)
|
||||
btn:SetScript("OnClick", function(self, button, down) clicks = clicks + 1 end)
|
||||
"#,
|
||||
)
|
||||
@@ -445,7 +445,7 @@ fn register_for_clicks_grows_right_click_and_carries_the_button_name() {
|
||||
r#"
|
||||
clicks, click_btn, arg1_btn = 0, nil, nil
|
||||
local btn = CreateFrame("Button", "Vendor")
|
||||
btn:SetPoint("BOTTOMLEFT", 0, 0); btn:SetSize(100, 100)
|
||||
btn:SetPoint("BOTTOMLEFT", 0, 0); btn:SetWidth(100); btn:SetHeight(100)
|
||||
btn:RegisterForClicks("LeftButtonUp", "RightButtonUp")
|
||||
btn:SetScript("OnClick", function(self, button, down)
|
||||
clicks = clicks + 1
|
||||
@@ -478,7 +478,7 @@ fn down_registration_fires_on_press_and_toggles_checked_once() {
|
||||
r#"
|
||||
clicks, seen_down = 0, nil
|
||||
local cb = CreateFrame("CheckButton", "QuickSell")
|
||||
cb:SetPoint("BOTTOMLEFT", 0, 0); cb:SetSize(100, 100)
|
||||
cb:SetPoint("BOTTOMLEFT", 0, 0); cb:SetWidth(100); cb:SetHeight(100)
|
||||
cb:RegisterForClicks("LeftButtonDown")
|
||||
cb:SetScript("OnClick", function(self, button, down)
|
||||
clicks = clicks + 1
|
||||
@@ -509,9 +509,9 @@ fn highlight_is_additive_and_state_textures_fill_then_anchor() {
|
||||
s.run(
|
||||
r#"
|
||||
local b = CreateFrame("Button", "AddBtn")
|
||||
b:SetPoint("BOTTOMLEFT", 100, 100); b:SetSize(36, 36)
|
||||
b:SetPoint("BOTTOMLEFT", 100, 100); b:SetWidth(36); b:SetHeight(36)
|
||||
b:SetNormalTexture("Interface\\Ring.blp")
|
||||
b:GetNormalTexture():SetSize(64, 64)
|
||||
local nt = b:GetNormalTexture(); nt:SetWidth(64); nt:SetHeight(64)
|
||||
b:SetHighlightTexture("Interface\\Hi.blp")
|
||||
"#,
|
||||
)
|
||||
@@ -533,7 +533,7 @@ fn highlight_is_additive_and_state_textures_fill_then_anchor() {
|
||||
));
|
||||
// A fresh state texture gets the creation-path implicit SetAllPoints (decision 1310 — the
|
||||
// reference's string setters anchor a freshly built texture to the button outright), whose
|
||||
// two corners pin all four edges: the later SetSize(64) is structurally unread and the ring
|
||||
// two corners pin all four edges: the later 64px size is structurally unread and the ring
|
||||
// FILLS the 36px button.
|
||||
let r = find("Interface\\Ring.blp").rect.unwrap();
|
||||
assert_eq!(
|
||||
@@ -608,7 +608,7 @@ fn button_label_repaints_by_state_font_object() {
|
||||
s.run(
|
||||
r#"
|
||||
b = CreateFrame("Button", "FontBtn")
|
||||
b:SetPoint("CENTER", 0, 0); b:SetSize(100, 20)
|
||||
b:SetPoint("CENTER", 0, 0); b:SetWidth(100); b:SetHeight(20)
|
||||
b:SetText("Label")
|
||||
b:SetTextFontObject("GoldFont")
|
||||
b:SetDisabledFontObject("GrayFont")
|
||||
@@ -696,7 +696,7 @@ fn a_locked_or_hovered_button_wears_its_highlight_font_over_its_normal_color() {
|
||||
s.run(
|
||||
r#"
|
||||
b = CreateFrame("Button", "RowBtn")
|
||||
b:SetPoint("BOTTOMLEFT", 100, 100); b:SetSize(100, 20)
|
||||
b:SetPoint("BOTTOMLEFT", 100, 100); b:SetWidth(100); b:SetHeight(20)
|
||||
b:SetText("Rough Copper Vest")
|
||||
b:SetTextFontObject("RowNormal")
|
||||
b:SetHighlightFontObject("RowHighlight")
|
||||
@@ -1103,7 +1103,7 @@ fn a_button_labels_own_setfont_survives_the_state_font_repoint() {
|
||||
s.run(
|
||||
r#"
|
||||
b = CreateFrame("Button", "SkinnedBtn")
|
||||
b:SetPoint("CENTER", 0, 0); b:SetSize(100, 20)
|
||||
b:SetPoint("CENTER", 0, 0); b:SetWidth(100); b:SetHeight(20)
|
||||
b:SetText("Label")
|
||||
b:SetTextFontObject("TemplateFont")
|
||||
b:GetFontString():SetFont("Interface\\Addons\\Skin\\Fonts\\porky.ttf", 18, "OUTLINE")
|
||||
@@ -1189,7 +1189,7 @@ fn a_state_texture_slot_takes_an_object_and_a_nil() {
|
||||
s.run(
|
||||
r#"
|
||||
b = CreateFrame("Button", "SlotBtn")
|
||||
b:SetPoint("BOTTOMLEFT", 0, 0); b:SetSize(100, 100)
|
||||
b:SetPoint("BOTTOMLEFT", 0, 0); b:SetWidth(100); b:SetHeight(100)
|
||||
b:SetNormalTexture("Interface\\N.blp")
|
||||
-- Bongos' own idiom: build the highlight yourself and hand the object over.
|
||||
hl = b:CreateTexture()
|
||||
@@ -1260,7 +1260,7 @@ fn an_unlocked_scripted_push_is_released_by_the_next_mouse_release() {
|
||||
s.run(
|
||||
r#"
|
||||
row = CreateFrame("Button", "TabletRow")
|
||||
row:SetPoint("BOTTOMLEFT", 0, 0); row:SetSize(100, 100)
|
||||
row:SetPoint("BOTTOMLEFT", 0, 0); row:SetWidth(100); row:SetHeight(100)
|
||||
row:SetNormalTexture("Interface\\RowN.blp")
|
||||
row:SetPushedTexture("Interface\\RowP.blp")
|
||||
row:SetButtonState("PUSHED") -- Tablet-2.0's call, verbatim: no lock argument
|
||||
@@ -1301,10 +1301,10 @@ fn a_locked_state_ignores_the_mouse_and_enable_disable_clears_the_lock() {
|
||||
s.run(
|
||||
r#"
|
||||
micro = CreateFrame("Button", "MicroButton")
|
||||
micro:SetPoint("BOTTOMLEFT", 0, 0); micro:SetSize(100, 100)
|
||||
micro:SetPoint("BOTTOMLEFT", 0, 0); micro:SetWidth(100); micro:SetHeight(100)
|
||||
micro:SetButtonState("PUSHED", 1) -- MainMenuBarMicroButtons.lua, verbatim
|
||||
pin = CreateFrame("Button", "PinnedNormal")
|
||||
pin:SetPoint("BOTTOMLEFT", 200, 0); pin:SetSize(100, 100)
|
||||
pin:SetPoint("BOTTOMLEFT", 200, 0); pin:SetWidth(100); pin:SetHeight(100)
|
||||
pin:SetButtonState("NORMAL", 1)
|
||||
"#,
|
||||
)
|
||||
@@ -1367,7 +1367,7 @@ fn the_hover_is_not_an_input_to_the_press_state() {
|
||||
s.run(
|
||||
r#"
|
||||
b = CreateFrame("Button", "HeldButton")
|
||||
b:SetPoint("BOTTOMLEFT", 0, 0); b:SetSize(100, 100)
|
||||
b:SetPoint("BOTTOMLEFT", 0, 0); b:SetWidth(100); b:SetHeight(100)
|
||||
b:SetNormalTexture("Interface\\HeldN.blp")
|
||||
b:SetPushedTexture("Interface\\HeldP.blp")
|
||||
"#,
|
||||
@@ -1416,7 +1416,7 @@ fn hiding_a_held_button_un_presses_it() {
|
||||
s.run(
|
||||
r#"
|
||||
h = CreateFrame("Button", "HidButton")
|
||||
h:SetPoint("BOTTOMLEFT", 0, 0); h:SetSize(100, 100)
|
||||
h:SetPoint("BOTTOMLEFT", 0, 0); h:SetWidth(100); h:SetHeight(100)
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
@@ -1438,3 +1438,86 @@ fn hiding_a_held_button_un_presses_it() {
|
||||
s.mouse_button(50.0, 50.0, "LeftButton", false);
|
||||
assert!(s.errors().is_empty(), "{:?}", s.errors());
|
||||
}
|
||||
|
||||
/// `Button:GetTextColor()` — **FOUR** values, r/g/b/a (`0x781100`, table `0x879d00`, argc 1,
|
||||
/// arity 4, kinds `(number,number,number,number)`). Three is the plausible wrong answer, and the
|
||||
/// shapes table flags the name `name-not-unique` because seven tables register it — this asserts
|
||||
/// the BUTTON one, on a Button, and its two inheritance legs.
|
||||
///
|
||||
/// Completeness rather than a live break: no corpus site has a Button receiver today (every
|
||||
/// measured `GetTextColor` is on a FontString or a font object). It is here because the reference
|
||||
/// registers it, so the widget shape gate can cover it from now on.
|
||||
#[test]
|
||||
fn button_get_text_color_answers_four_values_through_the_state_font() {
|
||||
let s = script();
|
||||
s.register_font_object(
|
||||
"BtnGold",
|
||||
FontObject {
|
||||
color: Some([1.0, 0.82, 0.0, 1.0]),
|
||||
height: Some(12.0),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
s.run(
|
||||
r#"
|
||||
Plain = CreateFrame("Button", "PlainBtn")
|
||||
Themed = CreateFrame("Button", "ThemedBtn")
|
||||
Themed:SetTextFontObject("BtnGold")
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
s.eval::<i64>("return select('#', PlainBtn:GetTextColor())")
|
||||
.unwrap(),
|
||||
4,
|
||||
"arity 4 — not 3, the plausible wrong answer"
|
||||
);
|
||||
let kinds: String = s
|
||||
.eval(
|
||||
r#"local r,g,b,a = PlainBtn:GetTextColor()
|
||||
return type(r)..","..type(g)..","..type(b)..","..type(a)"#,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(kinds, "number,number,number,number");
|
||||
|
||||
// A button with nothing set anywhere is the untinted white every other colour getter here
|
||||
// (`GetVertexColor`, `FontString:GetTextColor`) answers.
|
||||
let plain: Vec<f32> = (1..=4)
|
||||
.map(|i| {
|
||||
s.eval::<f32>(&format!("return select({i}, PlainBtn:GetTextColor())"))
|
||||
.unwrap()
|
||||
})
|
||||
.collect();
|
||||
assert_eq!(plain, vec![1.0, 1.0, 1.0, 1.0]);
|
||||
|
||||
// With no local colour it reads THROUGH what the normal state inherits — the same leg
|
||||
// `Button:GetFont` takes, and the one a stock `GameMenuButtonTemplate` button relies on.
|
||||
let themed: Vec<f32> = (1..=4)
|
||||
.map(|i| {
|
||||
s.eval::<f32>(&format!("return select({i}, ThemedBtn:GetTextColor())"))
|
||||
.unwrap()
|
||||
})
|
||||
.collect();
|
||||
assert_eq!(themed, vec![1.0, 0.82, 0.0, 1.0]);
|
||||
|
||||
// A local SetTextColor wins, alpha included, and round-trips.
|
||||
s.run("ThemedBtn:SetTextColor(0.1, 0.2, 0.3, 0.4)").unwrap();
|
||||
let set: Vec<f32> = (1..=4)
|
||||
.map(|i| {
|
||||
s.eval::<f32>(&format!("return select({i}, ThemedBtn:GetTextColor())"))
|
||||
.unwrap()
|
||||
})
|
||||
.collect();
|
||||
assert_eq!(set, vec![0.1, 0.2, 0.3, 0.4]);
|
||||
|
||||
// A CheckButton reaches it through Button's table, as it does the rest of the trio.
|
||||
assert_eq!(
|
||||
s.eval::<i64>(
|
||||
r#"local c = CreateFrame("CheckButton", "ChkColorBtn")
|
||||
return select('#', c:GetTextColor())"#
|
||||
)
|
||||
.unwrap(),
|
||||
4
|
||||
);
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ fn end_to_end_two_frame_tree_extracts_in_zkey_order() {
|
||||
-- same strata/level) with a texture and a fontstring. ~30 lines of ordinary FrameXML Lua.
|
||||
local parent = CreateFrame("Frame", "Root")
|
||||
parent:SetPoint("TOPLEFT", 0, 0) -- anchored to the screen root
|
||||
parent:SetSize(400, 300)
|
||||
parent:SetWidth(400); parent:SetHeight(300)
|
||||
-- SetAllPoints on each region: a templateless Lua region gets NO implicit anchor
|
||||
-- (decision 1310 — rect-less, never drawn), so real addon code anchors it, and so do we.
|
||||
local pbg = parent:CreateTexture(nil, "BACKGROUND")
|
||||
@@ -23,7 +23,7 @@ fn end_to_end_two_frame_tree_extracts_in_zkey_order() {
|
||||
|
||||
local child = CreateFrame("Frame", "Leaf", parent)
|
||||
child:SetPoint("TOPLEFT", parent, "TOPLEFT", 10, -10)
|
||||
child:SetSize(100, 50)
|
||||
child:SetWidth(100); child:SetHeight(50)
|
||||
local cbg = child:CreateTexture(nil, "ARTWORK")
|
||||
cbg:SetTexture("Interface\\Child.blp")
|
||||
cbg:SetAllPoints()
|
||||
|
||||
@@ -270,3 +270,112 @@ fn mid_dispatch_removal_of_the_next_stops_and_append_is_visited() {
|
||||
"a tail-append during dispatch is still visited this dispatch"
|
||||
);
|
||||
}
|
||||
|
||||
/// `Frame:RegisterAllEvents()` — the frame's `OnEvent` receives every event dispatched
|
||||
/// (`0x774c20`, table `0x878ec0`, argc 1, arity 0), and `UnregisterAllEvents` clears that state
|
||||
/// along with the per-event ones.
|
||||
///
|
||||
/// **The clearing half is the load-bearing one.** AceEvent-2.0 — shipped by 63 vanilla addons and
|
||||
/// 6 of the top 20 — turns all-events on once (`AceEvent.frame:RegisterAllEvents()`), deliberately
|
||||
/// stops calling `frame:UnregisterEvent(event)` while it is on, and gets back to per-event
|
||||
/// registration by calling `frame:UnregisterAllEvents()` and then re-registering each event it
|
||||
/// still wants. A `RegisterAllEvents` that survived that call would leave every Ace2 addon on the
|
||||
/// whole event stream for the session.
|
||||
#[test]
|
||||
fn register_all_events_takes_every_event_and_unregister_all_clears_it() {
|
||||
let mut s = script();
|
||||
s.run(
|
||||
r#"
|
||||
seen = {}
|
||||
All = CreateFrame("Frame", "AllEv")
|
||||
All:SetScript("OnEvent", function() table.insert(seen, event) end)
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Arity 0 — the verb answers nothing.
|
||||
assert_eq!(
|
||||
s.eval::<i64>("return select('#', All:RegisterAllEvents())")
|
||||
.unwrap(),
|
||||
0
|
||||
);
|
||||
|
||||
// Anything dispatched now reaches it, including an event no name list could have enumerated.
|
||||
for ev in [
|
||||
"PLAYER_LOGIN",
|
||||
"UNIT_HEALTH",
|
||||
"SOME_SERVER_EVENT_NOBODY_LISTED",
|
||||
] {
|
||||
s.fire_event(ev, vec![]);
|
||||
}
|
||||
assert_eq!(
|
||||
s.eval::<i64>("return table.getn(seen)").unwrap(),
|
||||
3,
|
||||
"every event dispatched, whatever its name"
|
||||
);
|
||||
assert_eq!(
|
||||
s.eval::<String>("return seen[3]").unwrap(),
|
||||
"SOME_SERVER_EVENT_NOBODY_LISTED"
|
||||
);
|
||||
|
||||
// Registering twice is a no-op, and a frame holding BOTH an all-events registration and a
|
||||
// RegisterEvent for the same event is one listener, not two — the same rule `RegisterEvent`'s
|
||||
// own `if not already in the list` holds one level down.
|
||||
s.run(r#"seen = {}; All:RegisterAllEvents(); All:RegisterEvent("UNIT_HEALTH")"#)
|
||||
.unwrap();
|
||||
s.fire_event("UNIT_HEALTH", vec![]);
|
||||
assert_eq!(
|
||||
s.eval::<i64>("return table.getn(seen)").unwrap(),
|
||||
1,
|
||||
"fired once, not twice"
|
||||
);
|
||||
|
||||
// UnregisterAllEvents clears BOTH: the explicit UNIT_HEALTH registration and the all-events one.
|
||||
s.run("seen = {}; All:UnregisterAllEvents()").unwrap();
|
||||
for ev in ["UNIT_HEALTH", "PLAYER_LOGIN"] {
|
||||
s.fire_event(ev, vec![]);
|
||||
}
|
||||
assert_eq!(
|
||||
s.eval::<i64>("return table.getn(seen)").unwrap(),
|
||||
0,
|
||||
"the all-events registration does not outlive UnregisterAllEvents"
|
||||
);
|
||||
|
||||
// …and the AceEvent path back: re-register the individual events it still wants.
|
||||
s.run(r#"All:RegisterEvent("UNIT_HEALTH")"#).unwrap();
|
||||
for ev in ["UNIT_HEALTH", "PLAYER_LOGIN"] {
|
||||
s.fire_event(ev, vec![]);
|
||||
}
|
||||
assert_eq!(
|
||||
s.eval::<i64>("return table.getn(seen)").unwrap(),
|
||||
1,
|
||||
"back to exactly one event"
|
||||
);
|
||||
assert!(s.take_errors().is_empty());
|
||||
}
|
||||
|
||||
/// An all-events frame dispatches AFTER the event's own listeners — where it would sit if the
|
||||
/// registration were expanded into every per-event list, since it joined later than they did.
|
||||
/// Cross-frame order is a law consumers depend on (the two ZoneText frames writing one FontString);
|
||||
/// it does not stop being one because a listener asked for everything.
|
||||
#[test]
|
||||
fn an_all_events_listener_runs_after_the_events_own() {
|
||||
let mut s = script();
|
||||
s.run(
|
||||
r#"
|
||||
order = {}
|
||||
Named = CreateFrame("Frame", "NamedEv")
|
||||
Named:SetScript("OnEvent", function() table.insert(order, "named") end)
|
||||
Named:RegisterEvent("PLAYER_LOGIN")
|
||||
|
||||
Everything = CreateFrame("Frame", "EveryEv")
|
||||
Everything:SetScript("OnEvent", function() table.insert(order, "all") end)
|
||||
Everything:RegisterAllEvents()
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
s.fire_event("PLAYER_LOGIN", vec![]);
|
||||
assert_eq!(s.eval::<String>("return order[1]").unwrap(), "named");
|
||||
assert_eq!(s.eval::<String>("return order[2]").unwrap(), "all");
|
||||
assert_eq!(s.eval::<i64>("return table.getn(order)").unwrap(), 2);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
//! Small frame-API wins: GetLocale, SetAllPoints, SetFormattedText, SetShown, and GetPoint readback.
|
||||
//! Small frame-API wins: GetLocale, SetAllPoints, and GetPoint readback.
|
||||
|
||||
use super::common::script;
|
||||
|
||||
/// Two era names, asserted together because neither earns a file of its own.
|
||||
///
|
||||
/// This test carried two more legs until decision 2142, `SetFormattedText` and `SetShown`, and
|
||||
/// both went with the verbs: neither is in a 1.12 method table. What the first of them actually
|
||||
/// proved — that `%N$s` reorders rather than consumes — is `string.format`'s own behaviour and is
|
||||
/// pinned where it lives, in [`crate::strings`].
|
||||
#[test]
|
||||
fn consensus_api_small_wins() {
|
||||
fn get_locale_and_set_all_points() {
|
||||
let mut s = script();
|
||||
s.set_screen_size(800.0, 600.0);
|
||||
// GetLocale — benilla is enUS-data-only.
|
||||
@@ -12,13 +18,11 @@ fn consensus_api_small_wins() {
|
||||
s.run(
|
||||
r#"
|
||||
p = CreateFrame("Frame", "SAP_Parent")
|
||||
p:SetPoint("BOTTOMLEFT", 100, 100); p:SetSize(200, 50)
|
||||
p:SetPoint("BOTTOMLEFT", 100, 100); p:SetWidth(200); p:SetHeight(50)
|
||||
c = CreateFrame("Frame", "SAP_Child", p)
|
||||
c:SetAllPoints() -- default: the parent
|
||||
f = CreateFrame("Frame", "SAP_Free")
|
||||
f:SetAllPoints("SAP_Parent") -- by name
|
||||
fs = p:CreateFontString(nil, "OVERLAY")
|
||||
fs:SetFormattedText("%2$s %1$s (%3$d)", "world", "hello", 7)
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
@@ -34,20 +38,6 @@ fn consensus_api_small_wins() {
|
||||
)
|
||||
.unwrap();
|
||||
assert!(ok, "SetAllPoints matched the target rect");
|
||||
|
||||
// SetFormattedText routes through the positional-aware format.
|
||||
assert_eq!(
|
||||
s.eval::<String>("return fs:GetText()").unwrap(),
|
||||
"hello world (7)"
|
||||
);
|
||||
|
||||
// SetShown: truthy shows, nil/false hides.
|
||||
s.run("p:SetShown(false)").unwrap();
|
||||
assert!(!s.eval::<bool>("return p:IsShown()").unwrap());
|
||||
s.run("p:SetShown(1)").unwrap();
|
||||
assert!(s.eval::<bool>("return p:IsShown()").unwrap());
|
||||
s.run("p:SetShown(nil)").unwrap();
|
||||
assert!(!s.eval::<bool>("return p:IsShown()").unwrap());
|
||||
assert!(s.errors().is_empty(), "{:?}", s.errors());
|
||||
}
|
||||
|
||||
|
||||
@@ -12,10 +12,10 @@ fn enable_mouse_gates_hit_testing() {
|
||||
-- Two full-screen frames. `b` is created later (drawn on top) but mouse-disabled, so it is
|
||||
-- transparent to hits; the enabled frame behind it (`a`) must capture.
|
||||
local a = CreateFrame("Frame", "A")
|
||||
a:SetPoint("BOTTOMLEFT", 0, 0); a:SetSize(800, 600); a:EnableMouse(true)
|
||||
a:SetPoint("BOTTOMLEFT", 0, 0); a:SetWidth(800); a:SetHeight(600); a:EnableMouse(true)
|
||||
a:SetScript("OnEnter", function(self) who = self:GetName() end)
|
||||
local b = CreateFrame("Frame", "B")
|
||||
b:SetPoint("BOTTOMLEFT", 0, 0); b:SetSize(800, 600); b:EnableMouse(false)
|
||||
b:SetPoint("BOTTOMLEFT", 0, 0); b:SetWidth(800); b:SetHeight(600); b:EnableMouse(false)
|
||||
b:SetScript("OnEnter", function(self) who = self:GetName() end)
|
||||
-- 1/nil, not a boolean (1830): this is the exact comparison shape that inverts, so the
|
||||
-- test asserts it rather than leaning on truthiness.
|
||||
@@ -51,10 +51,10 @@ fn hit_order_is_strata_then_level_then_the_earlier_linked_frame() {
|
||||
r#"
|
||||
who = nil
|
||||
local a = CreateFrame("Frame", "A")
|
||||
a:SetPoint("BOTTOMLEFT", 0, 0); a:SetSize(800, 600); a:EnableMouse(true)
|
||||
a:SetPoint("BOTTOMLEFT", 0, 0); a:SetWidth(800); a:SetHeight(600); a:EnableMouse(true)
|
||||
a:SetScript("OnEnter", function(self) who = self:GetName() end)
|
||||
local b = CreateFrame("Frame", "B")
|
||||
b:SetPoint("BOTTOMLEFT", 0, 0); b:SetSize(800, 600); b:EnableMouse(true)
|
||||
b:SetPoint("BOTTOMLEFT", 0, 0); b:SetWidth(800); b:SetHeight(600); b:EnableMouse(true)
|
||||
b:SetScript("OnEnter", function(self) who = self:GetName() end)
|
||||
"#,
|
||||
)
|
||||
@@ -96,7 +96,7 @@ fn mouse_move_fires_enter_then_leave_across_a_boundary_with_correct_self() {
|
||||
enters, leaves = 0, 0
|
||||
enter_self_ok, leave_self_ok = false, false
|
||||
local a = CreateFrame("Frame", "A") -- left half only
|
||||
a:SetPoint("BOTTOMLEFT", 0, 0); a:SetSize(400, 600); a:EnableMouse(true)
|
||||
a:SetPoint("BOTTOMLEFT", 0, 0); a:SetWidth(400); a:SetHeight(600); a:EnableMouse(true)
|
||||
a:SetScript("OnEnter", function(self) enters = enters + 1; enter_self_ok = (self == a) end)
|
||||
a:SetScript("OnLeave", function(self) leaves = leaves + 1; leave_self_ok = (self == a) end)
|
||||
"#,
|
||||
@@ -127,10 +127,10 @@ fn onclick_fires_on_press_release_same_frame_not_when_release_lands_elsewhere()
|
||||
r#"
|
||||
clicks_a, clicks_b, click_btn = 0, 0, nil
|
||||
local a = CreateFrame("Frame", "A") -- left half
|
||||
a:SetPoint("BOTTOMLEFT", 0, 0); a:SetSize(400, 600); a:EnableMouse(true)
|
||||
a:SetPoint("BOTTOMLEFT", 0, 0); a:SetWidth(400); a:SetHeight(600); a:EnableMouse(true)
|
||||
a:SetScript("OnClick", function(self, button, down) clicks_a = clicks_a + 1; click_btn = button end)
|
||||
local b = CreateFrame("Frame", "B") -- right half
|
||||
b:SetPoint("BOTTOMLEFT", 400, 0); b:SetSize(400, 600); b:EnableMouse(true)
|
||||
b:SetPoint("BOTTOMLEFT", 400, 0); b:SetWidth(400); b:SetHeight(600); b:EnableMouse(true)
|
||||
b:SetScript("OnClick", function(self, button, down) clicks_b = clicks_b + 1 end)
|
||||
"#,
|
||||
)
|
||||
@@ -168,14 +168,14 @@ fn hidden_or_effective_hidden_frame_never_captures() {
|
||||
entered = false
|
||||
-- own shown = false
|
||||
local own = CreateFrame("Frame", "OwnHidden")
|
||||
own:SetPoint("BOTTOMLEFT", 0, 0); own:SetSize(800, 600); own:EnableMouse(true)
|
||||
own:SetPoint("BOTTOMLEFT", 0, 0); own:SetWidth(800); own:SetHeight(600); own:EnableMouse(true)
|
||||
own:SetScript("OnEnter", function() entered = true end)
|
||||
own:Hide()
|
||||
-- effective-hidden: child is shown but its parent is hidden
|
||||
local parent = CreateFrame("Frame", "Par")
|
||||
parent:SetPoint("BOTTOMLEFT", 0, 0); parent:SetSize(800, 600)
|
||||
parent:SetPoint("BOTTOMLEFT", 0, 0); parent:SetWidth(800); parent:SetHeight(600)
|
||||
local child = CreateFrame("Frame", "Ch", parent)
|
||||
child:SetPoint("BOTTOMLEFT", 0, 0); child:SetSize(800, 600); child:EnableMouse(true)
|
||||
child:SetPoint("BOTTOMLEFT", 0, 0); child:SetWidth(800); child:SetHeight(600); child:EnableMouse(true)
|
||||
child:SetScript("OnEnter", function() entered = true end)
|
||||
parent:Hide()
|
||||
"#,
|
||||
@@ -203,7 +203,7 @@ fn mouse_wheel_passes_delta_to_the_captured_frame() {
|
||||
r#"
|
||||
wheel = nil
|
||||
local a = CreateFrame("Frame", "A")
|
||||
a:SetPoint("BOTTOMLEFT", 0, 0); a:SetSize(800, 600); a:EnableMouse(true)
|
||||
a:SetPoint("BOTTOMLEFT", 0, 0); a:SetWidth(800); a:SetHeight(600); a:EnableMouse(true)
|
||||
-- The wheel is its own index and its own flag: SetScript never auto-enables (that law is
|
||||
-- XML-load-time only), so a runtime-created frame needs this explicitly, exactly as it
|
||||
-- needs EnableMouse. Mouse-enablement alone puts nothing in the wheel index.
|
||||
@@ -231,7 +231,7 @@ fn hit_rect_insets_shrink_the_mouse_rect_only() {
|
||||
r#"
|
||||
entered = false
|
||||
local a = CreateFrame("Frame", "A")
|
||||
a:SetPoint("BOTTOMLEFT", 0, 0); a:SetSize(100, 100); a:EnableMouse(true)
|
||||
a:SetPoint("BOTTOMLEFT", 0, 0); a:SetWidth(100); a:SetHeight(100); a:EnableMouse(true)
|
||||
a:SetScript("OnEnter", function() entered = true end)
|
||||
a:SetHitRectInsets(5, 5, 18, 5)
|
||||
local l, r, t, b = a:GetHitRectInsets()
|
||||
@@ -375,7 +375,7 @@ fn a_second_fast_click_fires_on_double_click_instead_of_the_second_on_click() {
|
||||
r#"
|
||||
clicks, doubles, dblbtn = 0, 0, nil
|
||||
local b = CreateFrame("Button", "DblB")
|
||||
b:SetPoint("BOTTOMLEFT", 0, 0); b:SetSize(200, 200); b:EnableMouse(true)
|
||||
b:SetPoint("BOTTOMLEFT", 0, 0); b:SetWidth(200); b:SetHeight(200); b:EnableMouse(true)
|
||||
b:SetScript("OnClick", function(self, button) clicks = clicks + 1 end)
|
||||
b:SetScript("OnDoubleClick", function(self, button) doubles = doubles + 1 dblbtn = button end)
|
||||
"#,
|
||||
@@ -432,7 +432,7 @@ fn a_frame_with_no_double_click_handler_keeps_every_rapid_on_click() {
|
||||
r#"
|
||||
clicks = 0
|
||||
local b = CreateFrame("Button", "NoDbl")
|
||||
b:SetPoint("BOTTOMLEFT", 0, 0); b:SetSize(200, 200); b:EnableMouse(true)
|
||||
b:SetPoint("BOTTOMLEFT", 0, 0); b:SetWidth(200); b:SetHeight(200); b:EnableMouse(true)
|
||||
b:SetScript("OnClick", function() clicks = clicks + 1 end)
|
||||
"#,
|
||||
)
|
||||
@@ -465,9 +465,9 @@ fn the_double_click_boundary_is_time_and_frame_and_clicks_pair_up() {
|
||||
f:SetScript("OnDoubleClick", function() doubles = doubles + 1 end)
|
||||
end
|
||||
local a = CreateFrame("Button", "DblA")
|
||||
a:SetPoint("BOTTOMLEFT", 0, 0); a:SetSize(100, 100); a:EnableMouse(true); watch(a)
|
||||
a:SetPoint("BOTTOMLEFT", 0, 0); a:SetWidth(100); a:SetHeight(100); a:EnableMouse(true); watch(a)
|
||||
local b = CreateFrame("Button", "DblBB")
|
||||
b:SetPoint("BOTTOMLEFT", 300, 0); b:SetSize(100, 100); b:EnableMouse(true); watch(b)
|
||||
b:SetPoint("BOTTOMLEFT", 300, 0); b:SetWidth(100); b:SetHeight(100); b:EnableMouse(true); watch(b)
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
@@ -522,7 +522,7 @@ fn a_multi_registered_button_pairs_a_left_click_with_a_right_one() {
|
||||
r#"
|
||||
doubles, dblbtn = 0, nil
|
||||
local b = CreateFrame("Button", "MixDbl")
|
||||
b:SetPoint("BOTTOMLEFT", 0, 0); b:SetSize(200, 200); b:EnableMouse(true)
|
||||
b:SetPoint("BOTTOMLEFT", 0, 0); b:SetWidth(200); b:SetHeight(200); b:EnableMouse(true)
|
||||
b:RegisterForClicks("LeftButtonUp", "RightButtonUp")
|
||||
b:SetScript("OnDoubleClick", function(self, button) doubles = doubles + 1 dblbtn = button end)
|
||||
"#,
|
||||
@@ -555,7 +555,7 @@ fn a_half_finished_double_click_survives_the_cursor_leaving_the_window() {
|
||||
r#"
|
||||
doubles = 0
|
||||
local b = CreateFrame("Button", "LeaveDbl")
|
||||
b:SetPoint("BOTTOMLEFT", 0, 0); b:SetSize(200, 200); b:EnableMouse(true)
|
||||
b:SetPoint("BOTTOMLEFT", 0, 0); b:SetWidth(200); b:SetHeight(200); b:EnableMouse(true)
|
||||
b:SetScript("OnDoubleClick", function() doubles = doubles + 1 end)
|
||||
"#,
|
||||
)
|
||||
@@ -585,7 +585,7 @@ fn set_script_on_double_click_is_accepted_because_something_fires_it() {
|
||||
r#"
|
||||
ran = false
|
||||
Btn = CreateFrame("Button", "DblAccepted")
|
||||
Btn:SetPoint("BOTTOMLEFT", 0, 0); Btn:SetSize(100, 100); Btn:EnableMouse(true)
|
||||
Btn:SetPoint("BOTTOMLEFT", 0, 0); Btn:SetWidth(100); Btn:SetHeight(100); Btn:EnableMouse(true)
|
||||
Btn:SetScript("OnDoubleClick", function() ran = true end)
|
||||
"#,
|
||||
)
|
||||
@@ -625,8 +625,8 @@ fn the_unfired_script_kinds_still_raise_rather_than_silently_accepting() {
|
||||
"OnAttributeChanged",
|
||||
// Real 1.12 slots we do not fire, and zero corpus call sites. (`OnUpdateModel` and
|
||||
// `OnAnimFinished` left this list with decision 2007: the tick's model pass fires
|
||||
// them — `script::tests::model_clock`.)
|
||||
"OnHorizontalScroll",
|
||||
// them — `script::tests::model_clock`. `OnHorizontalScroll` left it with the ScrollFrame's
|
||||
// horizontal offset pair, which fires it — `script::tests::scrollframe`.)
|
||||
"OnHyperlinkEnter",
|
||||
"OnMessageScrollChanged",
|
||||
"OnInputLanguageChanged",
|
||||
@@ -653,9 +653,9 @@ fn hiding_the_hovered_frame_fires_its_onleave_before_onhide() {
|
||||
r#"
|
||||
log = {}
|
||||
local win = CreateFrame("Frame", "Win")
|
||||
win:SetPoint("BOTTOMLEFT", 0, 0); win:SetSize(800, 600)
|
||||
win:SetPoint("BOTTOMLEFT", 0, 0); win:SetWidth(800); win:SetHeight(600)
|
||||
local slot = CreateFrame("Button", "Slot", win)
|
||||
slot:SetPoint("BOTTOMLEFT", 100, 100); slot:SetSize(100, 100); slot:EnableMouse(true)
|
||||
slot:SetPoint("BOTTOMLEFT", 100, 100); slot:SetWidth(100); slot:SetHeight(100); slot:EnableMouse(true)
|
||||
slot:SetScript("OnLeave", function() table.insert(log, "leave") end)
|
||||
slot:SetScript("OnHide", function() table.insert(log, "hide") end)
|
||||
"#,
|
||||
@@ -682,7 +682,7 @@ fn hiding_an_unhovered_frame_fires_no_onleave() {
|
||||
r#"
|
||||
leaves = 0
|
||||
local a = CreateFrame("Button", "Away")
|
||||
a:SetPoint("BOTTOMLEFT", 500, 500); a:SetSize(50, 50); a:EnableMouse(true)
|
||||
a:SetPoint("BOTTOMLEFT", 500, 500); a:SetWidth(50); a:SetHeight(50); a:EnableMouse(true)
|
||||
a:SetScript("OnLeave", function() leaves = leaves + 1 end)
|
||||
"#,
|
||||
)
|
||||
@@ -706,10 +706,10 @@ fn the_repick_re_hovers_without_a_mouse_move() {
|
||||
r#"
|
||||
entered = {}
|
||||
local under = CreateFrame("Button", "Under")
|
||||
under:SetPoint("BOTTOMLEFT", 100, 100); under:SetSize(100, 100); under:EnableMouse(true)
|
||||
under:SetPoint("BOTTOMLEFT", 100, 100); under:SetWidth(100); under:SetHeight(100); under:EnableMouse(true)
|
||||
under:SetScript("OnEnter", function() table.insert(entered, "under") end)
|
||||
local over = CreateFrame("Button", "Over")
|
||||
over:SetPoint("BOTTOMLEFT", 100, 100); over:SetSize(100, 100); over:EnableMouse(true)
|
||||
over:SetPoint("BOTTOMLEFT", 100, 100); over:SetWidth(100); over:SetHeight(100); over:EnableMouse(true)
|
||||
over:SetScript("OnEnter", function() table.insert(entered, "over") end)
|
||||
-- Say "on top" with the LEVEL, not with declaration order: at equal levels the sweep takes
|
||||
-- the earlier-linked frame (`Under`), which is the tie law, not what this test is about.
|
||||
@@ -752,7 +752,7 @@ fn two_frames() -> crate::script::UiScript {
|
||||
log = {}
|
||||
local function mk(name, x)
|
||||
local f = CreateFrame("Frame", name)
|
||||
f:SetPoint("BOTTOMLEFT", x, 200); f:SetSize(200, 200); f:EnableMouse(true)
|
||||
f:SetPoint("BOTTOMLEFT", x, 200); f:SetWidth(200); f:SetHeight(200); f:EnableMouse(true)
|
||||
f:SetScript("OnMouseDown", function(self) table.insert(log, self:GetName()..":down") end)
|
||||
f:SetScript("OnMouseUp", function(self) table.insert(log, self:GetName()..":up") end)
|
||||
return f
|
||||
|
||||
@@ -184,3 +184,90 @@ fn a_raising_handler_still_consumes_and_is_recorded() {
|
||||
s.errors()
|
||||
);
|
||||
}
|
||||
|
||||
/// **A `CSimpleEditBox` in the walk is asked about FOCUS, never about a script slot** (§1's vtable
|
||||
/// table + `0x77a900`). Its ctor registers it in both key buckets, and vtable `0x81c910` replaces
|
||||
/// `+0x5c`/`+0x60` with overrides that *never chain to the base* — "there is no `call 0x76b760`
|
||||
/// anywhere in it, so the generic `OnChar` slot `+0x180` is unreachable on an editbox". An
|
||||
/// unfocused box therefore takes the `0x77a956 xor eax,eax` leg: decline, walk continues.
|
||||
///
|
||||
/// Modelling the box as a plain frame here is not a subtle divergence. Stock
|
||||
/// `SendMailNameEditBox` carries an XML `<OnChar>` (`SendMailFrame_SendeeAutocomplete`), which
|
||||
/// auto-enables it, and it registers first in the mail window — so it consumed every keystroke and
|
||||
/// the send tab's other four boxes took no input at all (decision 2145).
|
||||
#[test]
|
||||
fn an_unfocused_editbox_declines_rather_than_eating_its_neighbours_keys() {
|
||||
let mut s = script();
|
||||
s.run(
|
||||
r#"
|
||||
decoyChars = 0
|
||||
decoy = CreateFrame("EditBox", "KbDecoyBox")
|
||||
decoy:SetAutoFocus(false)
|
||||
decoy:EnableKeyboard(true)
|
||||
decoy:SetScript("OnChar", function() decoyChars = decoyChars + 1 end)
|
||||
target = CreateFrame("EditBox", "KbTargetBox")
|
||||
target:SetAutoFocus(false)
|
||||
target:SetFocus()
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
assert!(s.char_input("k"), "the focused box consumes");
|
||||
assert_eq!(
|
||||
s.eval::<String>("return KbTargetBox:GetText()").unwrap(),
|
||||
"k",
|
||||
"the keystroke reaches the box that holds the focus"
|
||||
);
|
||||
assert_eq!(
|
||||
s.eval::<i64>("return decoyChars").unwrap(),
|
||||
0,
|
||||
"the unfocused box's own OnChar is unreachable from the walk — it declines"
|
||||
);
|
||||
|
||||
// Give the decoy the focus and its handler is reachable again — through the INSERT, which is
|
||||
// where an editbox's `OnChar` is fired from (`0x77c200`), not through the walk's base gate.
|
||||
s.run("KbDecoyBox:SetFocus()").unwrap();
|
||||
assert!(s.char_input("j"), "…and now it is the one that consumes");
|
||||
assert_eq!(s.eval::<i64>("return decoyChars").unwrap(), 1);
|
||||
assert_eq!(
|
||||
s.eval::<String>("return KbDecoyBox:GetText()").unwrap(),
|
||||
"j"
|
||||
);
|
||||
assert_eq!(
|
||||
s.eval::<String>("return KbTargetBox:GetText()").unwrap(),
|
||||
"k",
|
||||
"and the box that lost focus keeps what it had"
|
||||
);
|
||||
assert!(s.errors().is_empty(), "{:?}", s.errors());
|
||||
}
|
||||
|
||||
/// The same clause on the key-down channel (`+0x60` → `0x77b160`, focus guard at `0x77b1c7`), which
|
||||
/// is the [`super::super::keyboard::frame_key_input`] walk: an unfocused box carrying only an
|
||||
/// `OnKeyUp` must not swallow a BACKSPACE the focused box is waiting for. The plain-frame
|
||||
/// asymmetry above is real *for plain frames*; on a box the base gate is not reached at all.
|
||||
#[test]
|
||||
fn an_unfocused_editbox_does_not_swallow_a_focused_boxs_editing_key() {
|
||||
let mut s = script();
|
||||
s.run(
|
||||
r#"
|
||||
decoy = CreateFrame("EditBox", "KbDecoyBox2")
|
||||
decoy:SetAutoFocus(false)
|
||||
decoy:EnableKeyboard(true)
|
||||
decoy:SetScript("OnKeyUp", function() end)
|
||||
target = CreateFrame("EditBox", "KbTargetBox2")
|
||||
target:SetAutoFocus(false)
|
||||
target:SetText("ab")
|
||||
target:SetFocus()
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
s.editbox_action(crate::script::EditAction::Delete {
|
||||
unit: crate::script::EditUnit::Char,
|
||||
back: true,
|
||||
});
|
||||
assert_eq!(
|
||||
s.eval::<String>("return KbTargetBox2:GetText()").unwrap(),
|
||||
"a",
|
||||
"the backspace reached the focused box"
|
||||
);
|
||||
assert!(s.errors().is_empty(), "{:?}", s.errors());
|
||||
}
|
||||
|
||||
@@ -187,7 +187,7 @@ fn the_hover_re_enter_loop_neither_re_measures_nor_re_solves() {
|
||||
s.run(
|
||||
r#"
|
||||
local owner = CreateFrame("Button", "Slot")
|
||||
owner:SetPoint("TOPLEFT", 100, -100); owner:SetSize(40, 40)
|
||||
owner:SetPoint("TOPLEFT", 100, -100); owner:SetWidth(40); owner:SetHeight(40)
|
||||
tt = CreateFrame("GameTooltip", "TT")
|
||||
-- One bag-slot OnEnter: the clear (SetOwner) + the content rebuild, verbatim in shape.
|
||||
function reenter()
|
||||
|
||||
@@ -14,13 +14,13 @@ fn measured_fontstring_height_feeds_frame_anchors() {
|
||||
s.run(
|
||||
r#"
|
||||
local w = CreateFrame("Frame", "Win")
|
||||
w:SetPoint("TOPLEFT", 0, -100); w:SetSize(384, 512)
|
||||
w:SetPoint("TOPLEFT", 0, -100); w:SetWidth(384); w:SetHeight(512)
|
||||
local g = w:CreateFontString("Greeting", "ARTWORK")
|
||||
g:SetText("a long greeting that wraps")
|
||||
g:SetWidth(270)
|
||||
g:SetPoint("TOPLEFT", 33, -91)
|
||||
local row = CreateFrame("Button", "Row1")
|
||||
row:SetSize(300, 16)
|
||||
row:SetWidth(300); row:SetHeight(16)
|
||||
row:SetPoint("TOPLEFT", "Greeting", "BOTTOMLEFT", -10, -20)
|
||||
"#,
|
||||
)
|
||||
@@ -74,7 +74,7 @@ fn a_changed_text_reads_zero_until_its_own_measure_lands() {
|
||||
s.run(
|
||||
r#"
|
||||
local w = CreateFrame("Frame", "Win")
|
||||
w:SetPoint("TOPLEFT", 0, -100); w:SetSize(384, 512)
|
||||
w:SetPoint("TOPLEFT", 0, -100); w:SetWidth(384); w:SetHeight(512)
|
||||
local h = w:CreateFontString("Header", "ARTWORK")
|
||||
h:SetText("Say: ")
|
||||
h:SetPoint("LEFT", 13, 0)
|
||||
@@ -129,14 +129,14 @@ fn zero_width_fontstring_autosizes_to_its_line() {
|
||||
s.run(
|
||||
r#"
|
||||
local w = CreateFrame("Frame", "Win")
|
||||
w:SetPoint("TOPLEFT", 0, -100); w:SetSize(384, 512)
|
||||
w:SetPoint("TOPLEFT", 0, -100); w:SetWidth(384); w:SetHeight(512)
|
||||
local label = w:CreateFontString("FromLabel", "ARTWORK")
|
||||
label:SetText("From:")
|
||||
label:SetSize(0, 16)
|
||||
label:SetWidth(0); label:SetHeight(16)
|
||||
label:SetPoint("TOPRIGHT", "Win", "TOPLEFT", 114, -45)
|
||||
local value = w:CreateFontString("FromValue", "ARTWORK")
|
||||
value:SetText("Thrall")
|
||||
value:SetSize(110, 0)
|
||||
value:SetWidth(110); value:SetHeight(0)
|
||||
value:SetPoint("LEFT", "FromLabel", "RIGHT", 5, 0)
|
||||
"#,
|
||||
)
|
||||
@@ -181,7 +181,7 @@ fn frame_scale_rides_the_quads_and_the_measure_key() {
|
||||
s.run(
|
||||
r#"
|
||||
local w = CreateFrame("Frame", "Win")
|
||||
w:SetPoint("TOPLEFT", 0, -100); w:SetSize(400, 300)
|
||||
w:SetPoint("TOPLEFT", 0, -100); w:SetWidth(400); w:SetHeight(300)
|
||||
w:SetScale(0.8)
|
||||
local t = w:CreateFontString("ScaledLabel", "ARTWORK")
|
||||
t:SetText("Options")
|
||||
@@ -234,7 +234,7 @@ fn invalidate_text_measures_reopens_the_round_trip() {
|
||||
s.run(
|
||||
r#"
|
||||
local w = CreateFrame("Frame", "Win")
|
||||
w:SetPoint("TOPLEFT", 0, -100); w:SetSize(400, 300)
|
||||
w:SetPoint("TOPLEFT", 0, -100); w:SetWidth(400); w:SetHeight(300)
|
||||
local t = w:CreateFontString("Label", "ARTWORK")
|
||||
t:SetText("Keybindings")
|
||||
t:SetPoint("TOPLEFT", 10, -10)
|
||||
@@ -382,7 +382,7 @@ fn a_width_read_in_the_tick_that_set_the_text_is_not_zero() {
|
||||
s.run(
|
||||
r#"
|
||||
local f = CreateFrame("Frame", "Host")
|
||||
f:SetPoint("TOPLEFT"); f:SetSize(400, 300)
|
||||
f:SetPoint("TOPLEFT"); f:SetWidth(400); f:SetHeight(300)
|
||||
local fs = f:CreateFontString("Label", "ARTWORK")
|
||||
fs:SetPoint("TOPLEFT")
|
||||
-- the corpus idiom: set, then measure, in one statement sequence
|
||||
@@ -408,7 +408,7 @@ fn with_no_engine_installed_the_round_trip_is_still_the_only_answer() {
|
||||
s.run(
|
||||
r#"
|
||||
local f = CreateFrame("Frame", "Host")
|
||||
f:SetPoint("TOPLEFT"); f:SetSize(400, 300)
|
||||
f:SetPoint("TOPLEFT"); f:SetWidth(400); f:SetHeight(300)
|
||||
local fs = f:CreateFontString("Label", "ARTWORK")
|
||||
fs:SetPoint("TOPLEFT")
|
||||
fs:SetText("Onewarrior")
|
||||
@@ -435,7 +435,7 @@ fn resolve_closes_the_round_trip_when_an_engine_is_installed() {
|
||||
s.run(
|
||||
r#"
|
||||
local f = CreateFrame("Frame", "Host")
|
||||
f:SetPoint("TOPLEFT"); f:SetSize(400, 300)
|
||||
f:SetPoint("TOPLEFT"); f:SetWidth(400); f:SetHeight(300)
|
||||
local fs = f:CreateFontString("Label", "ARTWORK")
|
||||
fs:SetPoint("TOPLEFT")
|
||||
fs:SetWidth(35) -- a declared width ⇒ the two extents differ
|
||||
@@ -469,7 +469,7 @@ fn a_synchronous_measure_satisfies_the_batch_request_too() {
|
||||
s.run(
|
||||
r#"
|
||||
local f = CreateFrame("Frame", "Host")
|
||||
f:SetPoint("TOPLEFT"); f:SetSize(400, 300)
|
||||
f:SetPoint("TOPLEFT"); f:SetWidth(400); f:SetHeight(300)
|
||||
local fs = f:CreateFontString("Label", "ARTWORK")
|
||||
fs:SetPoint("TOPLEFT")
|
||||
fs:SetText("hello")
|
||||
@@ -505,7 +505,7 @@ fn a_settled_region_is_refound_after_each_measure_input_write() {
|
||||
s.run(
|
||||
r#"
|
||||
local f = CreateFrame("Frame", "Host")
|
||||
f:SetPoint("TOPLEFT"); f:SetSize(400, 300)
|
||||
f:SetPoint("TOPLEFT"); f:SetWidth(400); f:SetHeight(300)
|
||||
local fs = f:CreateFontString("Label", "ARTWORK")
|
||||
fs:SetPoint("TOPLEFT")
|
||||
fs:SetText("one")
|
||||
@@ -514,7 +514,8 @@ fn a_settled_region_is_refound_after_each_measure_input_write() {
|
||||
.unwrap();
|
||||
let writes: &[(&str, &str)] = &[
|
||||
("SetText", "Label:SetText('two')"),
|
||||
("SetFormattedText", "Label:SetFormattedText('n=%d', 3)"),
|
||||
// `SetFormattedText` was a second row here; it is gone (2142), and its era spelling
|
||||
// `SetText(format(...))` is the row above's path exactly.
|
||||
(
|
||||
"SetFont",
|
||||
r#"Label:SetFont("Fonts\\FRIZQT__.TTF", 14, "OUTLINE")"#,
|
||||
@@ -550,7 +551,7 @@ fn a_settled_region_is_refound_after_each_measure_input_write() {
|
||||
s.run(
|
||||
r#"
|
||||
local b = CreateFrame("Button", "Btn")
|
||||
b:SetPoint("TOPLEFT", 0, -50); b:SetSize(80, 22)
|
||||
b:SetPoint("TOPLEFT", 0, -50); b:SetWidth(80); b:SetHeight(22)
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
@@ -49,6 +49,7 @@ mod tooltip_spell;
|
||||
mod tooltip_unit;
|
||||
mod toplevel;
|
||||
mod visibility;
|
||||
mod widget_surface;
|
||||
mod worldframe;
|
||||
mod worldmap;
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ fn movable_panel() -> UiScript {
|
||||
starts, stops = 0, 0
|
||||
Panel = CreateFrame("Frame", "MovePanel")
|
||||
Panel:SetPoint("BOTTOMLEFT", 100, 100)
|
||||
Panel:SetSize(200, 80)
|
||||
Panel:SetWidth(200); Panel:SetHeight(80)
|
||||
Panel:EnableMouse(true)
|
||||
Panel:SetMovable(true)
|
||||
Panel:RegisterForDrag("LeftButton")
|
||||
@@ -113,7 +113,7 @@ fn start_moving_on_a_frame_that_is_not_movable_raises_and_moves_nothing() {
|
||||
r#"
|
||||
Fixed = CreateFrame("Frame", "FixedPanel")
|
||||
Fixed:SetPoint("BOTTOMLEFT", 100, 100)
|
||||
Fixed:SetSize(200, 80)
|
||||
Fixed:SetWidth(200); Fixed:SetHeight(80)
|
||||
ok, err = pcall(function() Fixed:StartMoving() end)
|
||||
"#,
|
||||
)
|
||||
@@ -153,7 +153,7 @@ fn stop_moving_or_sizing_is_harmless_with_nothing_moving_and_stops_only_its_own_
|
||||
r#"
|
||||
Other = CreateFrame("Frame", "OtherPanel")
|
||||
Other:SetPoint("BOTTOMLEFT", 400, 400)
|
||||
Other:SetSize(50, 50)
|
||||
Other:SetWidth(50); Other:SetHeight(50)
|
||||
MovePanel:StopMovingOrSizing() -- nothing is moving
|
||||
MovePanel:StopMovingOrSizing() -- twice
|
||||
OtherPanel:StopMovingOrSizing()
|
||||
@@ -191,7 +191,7 @@ fn a_frame_stretched_between_two_anchors_moves_rigidly() {
|
||||
s.run(
|
||||
r#"
|
||||
Back = CreateFrame("Frame", "StretchBack")
|
||||
Back:SetPoint("BOTTOMLEFT", 0, 0); Back:SetSize(800, 600)
|
||||
Back:SetPoint("BOTTOMLEFT", 0, 0); Back:SetWidth(800); Back:SetHeight(600)
|
||||
Stretch = CreateFrame("Frame", "StretchPanel", Back)
|
||||
Stretch:SetPoint("BOTTOMLEFT", Back, "BOTTOMLEFT", 100, 100)
|
||||
Stretch:SetPoint("TOPRIGHT", Back, "BOTTOMLEFT", 300, 200)
|
||||
@@ -229,7 +229,7 @@ fn a_scaled_frame_tracks_the_cursor_one_to_one_on_screen() {
|
||||
r#"
|
||||
Scaled = CreateFrame("Frame", "ScaledPanel")
|
||||
Scaled:SetPoint("BOTTOMLEFT", 100, 100)
|
||||
Scaled:SetSize(100, 100)
|
||||
Scaled:SetWidth(100); Scaled:SetHeight(100)
|
||||
Scaled:SetScale(2)
|
||||
Scaled:SetMovable(true)
|
||||
Scaled:StartMoving()
|
||||
@@ -266,7 +266,7 @@ fn the_three_flags_default_off_round_trip_and_user_placed_is_guarded() {
|
||||
s.run(
|
||||
r#"
|
||||
F = CreateFrame("Frame", "FlagPanel")
|
||||
F:SetPoint("BOTTOMLEFT", 10, 10); F:SetSize(50, 50)
|
||||
F:SetPoint("BOTTOMLEFT", 10, 10); F:SetWidth(50); F:SetHeight(50)
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
@@ -425,7 +425,7 @@ fn a_title_region_is_a_plain_region_and_creating_it_twice_is_destructive() {
|
||||
r#"
|
||||
TFrame = CreateFrame("Frame", "TFrame")
|
||||
TFrame:SetPoint("BOTTOMLEFT", 100, 100)
|
||||
TFrame:SetSize(200, 80)
|
||||
TFrame:SetWidth(200); TFrame:SetHeight(80)
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
@@ -549,7 +549,7 @@ fn a_title_region_drag_swallows_the_press_and_ends_on_release() {
|
||||
downs = 0
|
||||
TP = CreateFrame("Frame", "TP")
|
||||
TP:SetPoint("BOTTOMLEFT", 100, 100)
|
||||
TP:SetSize(200, 80)
|
||||
TP:SetWidth(200); TP:SetHeight(80)
|
||||
TP:EnableMouse(true)
|
||||
TP:SetScript("OnMouseDown", function() downs = downs + 1 end)
|
||||
TP:CreateTitleRegion():SetAllPoints(TP)
|
||||
|
||||
@@ -29,10 +29,10 @@ fn setpoint_relative_to_resolves_an_alias_global() {
|
||||
-- The reference UI's own frames.
|
||||
Bar = CreateFrame("Frame", "Bar")
|
||||
Bar:SetPoint("BOTTOMLEFT", nil, "BOTTOMLEFT", 100, 50)
|
||||
Bar:SetSize(200, 40)
|
||||
Bar:SetWidth(200); Bar:SetHeight(40)
|
||||
for i = 1, 5 do
|
||||
local b = CreateFrame("Button", "StockButton" .. i, Bar)
|
||||
b:SetSize(30, 30)
|
||||
b:SetWidth(30); b:SetHeight(30)
|
||||
end
|
||||
|
||||
-- The addon's aliases: plain globals, no frame's name (Bartender2's Alias.lua).
|
||||
@@ -84,14 +84,14 @@ fn setpoint_relative_to_resolves_a_region_alias_global() {
|
||||
r#"
|
||||
Panel = CreateFrame("Frame", "Panel")
|
||||
Panel:SetPoint("BOTTOMLEFT", nil, "BOTTOMLEFT", 0, 0)
|
||||
Panel:SetSize(400, 300)
|
||||
Panel:SetWidth(400); Panel:SetHeight(300)
|
||||
local tex = Panel:CreateTexture("PanelSwatch", "ARTWORK")
|
||||
tex:SetPoint("BOTTOMLEFT", "Panel", "BOTTOMLEFT", 10, 20)
|
||||
tex:SetSize(50, 50)
|
||||
tex:SetWidth(50); tex:SetHeight(50)
|
||||
Swatch = PanelSwatch -- the alias
|
||||
|
||||
Tag = CreateFrame("Frame", "Tag", Panel)
|
||||
Tag:SetSize(10, 10)
|
||||
Tag:SetWidth(10); Tag:SetHeight(10)
|
||||
Tag:SetPoint("BOTTOMLEFT", "Swatch", "BOTTOMRIGHT", 4, 0)
|
||||
"#,
|
||||
)
|
||||
@@ -117,10 +117,10 @@ fn setpoint_relative_to_ignores_a_non_widget_global() {
|
||||
r#"
|
||||
Host = CreateFrame("Frame", "Host")
|
||||
Host:SetPoint("BOTTOMLEFT", nil, "BOTTOMLEFT", 10, 10)
|
||||
Host:SetSize(100, 100)
|
||||
Host:SetWidth(100); Host:SetHeight(100)
|
||||
NotAWidget = 5
|
||||
Child = CreateFrame("Frame", "Child", Host)
|
||||
Child:SetSize(20, 20)
|
||||
Child:SetWidth(20); Child:SetHeight(20)
|
||||
Child:SetPoint("BOTTOMLEFT", "NotAWidget", "BOTTOMRIGHT", 0, 0)
|
||||
"#,
|
||||
)
|
||||
@@ -150,17 +150,17 @@ fn setpoint_relative_to_expands_parent_against_the_first_named_ancestor() {
|
||||
r#"
|
||||
Root = CreateFrame("Frame", "Root")
|
||||
Root:SetPoint("BOTTOMLEFT", nil, "BOTTOMLEFT", 0, 0)
|
||||
Root:SetSize(300, 200)
|
||||
Root:SetWidth(300); Root:SetHeight(200)
|
||||
|
||||
Peg = CreateFrame("Frame", "RootPeg", Root)
|
||||
Peg:SetPoint("BOTTOMLEFT", "Root", "BOTTOMLEFT", 20, 30)
|
||||
Peg:SetSize(10, 10)
|
||||
Peg:SetWidth(10); Peg:SetHeight(10)
|
||||
|
||||
-- An ANONYMOUS link in between: the walk skips it and lands on Root.
|
||||
Anon = CreateFrame("Frame", nil, Root)
|
||||
Anon:SetAllPoints(Root)
|
||||
Kid = CreateFrame("Frame", "Kid", Anon)
|
||||
Kid:SetSize(5, 5)
|
||||
Kid:SetWidth(5); Kid:SetHeight(5)
|
||||
Kid:SetPoint("BOTTOMLEFT", "$parentPeg", "BOTTOMRIGHT", 0, 0)
|
||||
"#,
|
||||
)
|
||||
@@ -227,7 +227,7 @@ fn setallpoints_resolves_an_alias_global() {
|
||||
r#"
|
||||
Plate = CreateFrame("Frame", "Plate")
|
||||
Plate:SetPoint("BOTTOMLEFT", nil, "BOTTOMLEFT", 30, 40)
|
||||
Plate:SetSize(120, 60)
|
||||
Plate:SetWidth(120); Plate:SetHeight(60)
|
||||
PlateAlias = Plate
|
||||
Overlay = CreateFrame("Frame", "Overlay")
|
||||
Overlay:SetAllPoints("PlateAlias")
|
||||
|
||||
@@ -48,8 +48,15 @@ fn reference() -> Vec<(String, String)> {
|
||||
fn allowed_beyond_1_12() -> HashSet<&'static str> {
|
||||
[
|
||||
// ── our Lua runtime is 5.1 where 1.12's is 5.0 ────────────────────────────────────────
|
||||
// `select` is load-bearing and stays on purpose — our transcribed FrameXML uses it in 16
|
||||
// files as the 5.1 spelling of 5.0's implicit `arg` table.
|
||||
// `select`'s old reason here — "our transcribed FrameXML uses it in 16 files as the 5.1
|
||||
// spelling of 5.0's implicit `arg` table" — **was true and is not any more**: 1751's
|
||||
// stock-first migration retired those files, and `assets/ui`'s seven survivors contain
|
||||
// zero `select(` call sites (measured 2026-09-09, decision 2142). What keeps it is now a
|
||||
// different fact, and a weaker one: it is mlua's, not ours to remove without replacing the
|
||||
// 5.1 varargs it comes with. The corpus reads it as a version probe —
|
||||
// `pfUI/libs/libpredict.lua:141` and `ShaguTweaks/libs/libpredict.lua:82` both branch on
|
||||
// `elseif select and UnitCastingInfo then`, and only our NOT having `UnitCastingInfo`
|
||||
// keeps them off the TBC cast path.
|
||||
//
|
||||
// `_G` is the last one inherited rather than chosen: 1.12's base library does not export
|
||||
// it (an addon reaches the globals with `getfenv(0)`, which is what AceLibrary does), but
|
||||
@@ -64,9 +71,38 @@ fn allowed_beyond_1_12() -> HashSet<&'static str> {
|
||||
"_G",
|
||||
"select",
|
||||
// ── WoW API past 1.12 — 1188 phase 5's list, and the reason that phase exists ─────────
|
||||
// Every one of these predates 1189 and is used by our own transcribed FrameXML today.
|
||||
// Resolving each means either replacing it with its 1.12 equivalent (`UnitPower` →
|
||||
// `UnitMana`, which 1.12 has and we do not) or recording why it stays.
|
||||
// **The reason written here was "every one of these is used by our own transcribed
|
||||
// FrameXML today", and as of 2026-09-09 that is true of exactly ONE of the sixteen**
|
||||
// (`SubmitChatInput`, in the dev-only `ScriptLogFrame.xml`) — a corpus census measured it
|
||||
// and this file's own claim was checked against `assets/ui`'s seven remaining files, name
|
||||
// by name, before this comment was rewritten (decision 2142). 1751's migration retired the
|
||||
// windows that used the rest; the list outlived its justification, which is exactly how
|
||||
// 2118 found the modifier keys still answering booleans years after 0068's reason for it
|
||||
// had gone.
|
||||
//
|
||||
// **What they are NOT is load-bearing for the addon corpus, and 2143 corrects 2142 for
|
||||
// saying so.** The claim written here was that the vanilla ecosystem assumes several of
|
||||
// these exist — `pfUI/modules/loot.lua` calling `wipe` 25 times and defining it nowhere,
|
||||
// so ours "makes that module run at all". That is wrong twice over, and the error is
|
||||
// instructive: **a "1.12 addon" in that corpus is usually a MULTI-CLIENT addon**. pfUI
|
||||
// ships one codebase for three clients and gates per module —
|
||||
// `pfUI:RegisterModule("loot", "vanilla:tbc", …)`, matched by
|
||||
// `strfind(version, pfUI.expansion)` at `pfUI.lua:250` — so a TBC-era habit sits in code
|
||||
// the vanilla client reaches. The `wipe` calls are in `RequestRolls`,
|
||||
// `BuildSpecialRecipientsMenu` and `ClearRolls`: the master-looter's roll helper, not the
|
||||
// loot window, and that path throws on a real 1.12 client too. And `strsplit` was worse —
|
||||
// pfUI has its own `pfUI.api.strsplit` (`api/api.lua:16`) and ShaguTweaks its own
|
||||
// `ShaguTweaks.strsplit`; the bare calls are inside the vendored `libs/libpredict.lua`,
|
||||
// which has TBC branches of its own.
|
||||
//
|
||||
// So our superset was HIDING those latent nil-calls, never enabling anything. The one
|
||||
// real interaction left is the other direction: `AtlasLoot/Core/AtlasLoot.lua:2119`/`:2148`
|
||||
// define GLOBAL `strsplit`/`strtrim` unconditionally, with incompatible pattern-based
|
||||
// two-return semantics, and clobber ours for everything loaded after them — so ours is not
|
||||
// authoritative at runtime either.
|
||||
//
|
||||
// Resolving each means replacing it with its 1.12 equivalent (`UnitPower` → `UnitMana`,
|
||||
// which 1.12 has and we do not), deleting it, or recording why it stays.
|
||||
//
|
||||
// Three left with 1751's gossip work: `GetNumGossipQuests`, `GetGossipQuestInfo` and
|
||||
// `SelectGossipQuest` were benilla's own single-list shape over the gossip packet's quest
|
||||
@@ -332,12 +368,12 @@ fn region_set_parent_relinks_the_draw_owner_and_leaves_anchors_alone() {
|
||||
s.run(
|
||||
r#"
|
||||
A = CreateFrame("Frame", "SPOwnerA")
|
||||
A:SetPoint("BOTTOMLEFT", 0, 0) A:SetSize(100, 50)
|
||||
A:SetPoint("BOTTOMLEFT", 0, 0) A:SetWidth(100); A:SetHeight(50)
|
||||
B = CreateFrame("Frame", "SPOwnerB")
|
||||
B:SetPoint("BOTTOMLEFT", 300, 0) B:SetSize(100, 50)
|
||||
B:SetPoint("BOTTOMLEFT", 300, 0) B:SetWidth(100); B:SetHeight(50)
|
||||
Spark = A:CreateTexture("SPSpark", "ARTWORK")
|
||||
Spark:SetTexture("Interface\\SPGlow")
|
||||
Spark:SetSize(24, 24)
|
||||
Spark:SetWidth(24); Spark:SetHeight(24)
|
||||
Spark:SetPoint("TOPLEFT", 4, -4)
|
||||
"#,
|
||||
)
|
||||
@@ -1161,7 +1197,7 @@ fn every_region_map_method_is_callable_on_a_texture_and_a_fontstring() {
|
||||
s.run(
|
||||
r#"
|
||||
RMOwner = CreateFrame("Frame", "RMOwner")
|
||||
RMOwner:SetPoint("BOTTOMLEFT", 0, 0) RMOwner:SetSize(100, 50)
|
||||
RMOwner:SetPoint("BOTTOMLEFT", 0, 0) RMOwner:SetWidth(100); RMOwner:SetHeight(50)
|
||||
RMTex = RMOwner:CreateTexture("RMTex", "ARTWORK")
|
||||
RMStr = RMOwner:CreateFontString("RMStr", "ARTWORK")
|
||||
"#,
|
||||
@@ -1197,9 +1233,9 @@ fn the_region_map_readers_answer_the_way_the_edges_do() {
|
||||
s.run(
|
||||
r#"
|
||||
RRFrame = CreateFrame("Frame", "RRFrame")
|
||||
RRFrame:SetPoint("BOTTOMLEFT", 100, 200) RRFrame:SetSize(200, 100)
|
||||
RRFrame:SetPoint("BOTTOMLEFT", 100, 200) RRFrame:SetWidth(200); RRFrame:SetHeight(100)
|
||||
RRPlate = RRFrame:CreateTexture("RRPlate", "ARTWORK")
|
||||
RRPlate:SetPoint("TOPLEFT", 10, -10) RRPlate:SetSize(50, 20)
|
||||
RRPlate:SetPoint("TOPLEFT", 10, -10) RRPlate:SetWidth(50); RRPlate:SetHeight(20)
|
||||
-- the sibling-region anchor the real XML uses everywhere
|
||||
RRLabel = RRFrame:CreateFontString("RRLabel", "OVERLAY")
|
||||
RRLabel:SetPoint("LEFT", RRPlate, "RIGHT", 4, 0)
|
||||
|
||||
@@ -40,10 +40,10 @@ fn region_texture_anchored_topleft_resolves_exact_rect() {
|
||||
r#"
|
||||
local f = CreateFrame("Frame", "Owner")
|
||||
f:SetPoint("BOTTOMLEFT", 0, 0) -- owner [bottom 0, left 0, top 50, right 100]
|
||||
f:SetSize(100, 50)
|
||||
f:SetWidth(100); f:SetHeight(50)
|
||||
local t = f:CreateTexture("Tex", "ARTWORK")
|
||||
t:SetTexture("Interface\\Icon")
|
||||
t:SetSize(24, 24)
|
||||
t:SetWidth(24); t:SetHeight(24)
|
||||
t:SetPoint("TOPLEFT", 4, -4)
|
||||
"#,
|
||||
)
|
||||
@@ -69,7 +69,7 @@ fn region_fontstring_span_floors_at_one_unit_until_measured() {
|
||||
r#"
|
||||
local f = CreateFrame("Frame", "Owner")
|
||||
f:SetPoint("BOTTOMLEFT", 0, 0) -- owner [0, 0, 50, 100]
|
||||
f:SetSize(100, 50)
|
||||
f:SetWidth(100); f:SetHeight(50)
|
||||
local fs = f:CreateFontString("FS", "ARTWORK")
|
||||
fs:SetText("Name")
|
||||
fs:SetJustifyH("LEFT")
|
||||
@@ -116,7 +116,7 @@ fn a_zero_width_solid_spans_eight_units_and_its_artless_twin_gets_no_rect() {
|
||||
r#"
|
||||
local f = CreateFrame("Frame", "Owner")
|
||||
f:SetPoint("BOTTOMLEFT", 0, 0) -- owner [0, 0, 50, 100]
|
||||
f:SetSize(100, 50)
|
||||
f:SetWidth(100); f:SetHeight(50)
|
||||
Fill = f:CreateTexture("Fill", "BORDER")
|
||||
Fill:SetTexture(1, 1, 1, 1) -- the <Color> form: an 8x8 solid
|
||||
Fill:SetWidth(0); Fill:SetHeight(13)
|
||||
@@ -153,10 +153,10 @@ fn templateless_lua_region_without_anchors_never_draws() {
|
||||
r#"
|
||||
local f = CreateFrame("Frame", "Owner")
|
||||
f:SetPoint("BOTTOMLEFT", 0, 0) -- owner [0, 0, 50, 100]
|
||||
f:SetSize(100, 50)
|
||||
f:SetWidth(100); f:SetHeight(50)
|
||||
local t = f:CreateTexture("Tex", "ARTWORK")
|
||||
t:SetTexture("Interface\\Ring")
|
||||
t:SetSize(24, 24) -- no anchors, and no template ⇒ no rect, no draw
|
||||
t:SetWidth(24); t:SetHeight(24) -- no anchors, and no template ⇒ no rect, no draw
|
||||
assert(t:GetLeft() == nil, "a rect-less region reads nil edges")
|
||||
"#,
|
||||
)
|
||||
@@ -178,10 +178,10 @@ fn region_set_all_points_fills_owner() {
|
||||
r#"
|
||||
local f = CreateFrame("Frame", "Owner")
|
||||
f:SetPoint("BOTTOMLEFT", 0, 0) -- owner [0, 0, 50, 100]
|
||||
f:SetSize(100, 50)
|
||||
f:SetWidth(100); f:SetHeight(50)
|
||||
local t = f:CreateTexture("Tex", "ARTWORK")
|
||||
t:SetTexture("Interface\\Fill")
|
||||
t:SetSize(24, 24) -- size present, but setAllPoints wins
|
||||
t:SetWidth(24); t:SetHeight(24) -- size present, but setAllPoints wins
|
||||
t:SetAllPoints()
|
||||
"#,
|
||||
)
|
||||
@@ -205,14 +205,14 @@ fn region_anchors_to_sibling_region_by_name() {
|
||||
s.run(
|
||||
r#"
|
||||
local row = CreateFrame("Button", "Row")
|
||||
row:SetPoint("BOTTOMLEFT", 100, 100); row:SetSize(153, 44)
|
||||
row:SetPoint("BOTTOMLEFT", 100, 100); row:SetWidth(153); row:SetHeight(44)
|
||||
-- Plate first: its target doesn't exist yet at SetPoint time in XML order terms; the
|
||||
-- name lookup happens at SetPoint (both exist by then in real loads), the RECT ordering
|
||||
-- is the fixpoint's job.
|
||||
local plate = row:CreateTexture("RowPlate", "BACKGROUND")
|
||||
plate:SetSize(128, 78)
|
||||
plate:SetWidth(128); plate:SetHeight(78)
|
||||
local slot = row:CreateTexture("RowSlot", "BACKGROUND")
|
||||
slot:SetSize(64, 64)
|
||||
slot:SetWidth(64); slot:SetHeight(64)
|
||||
slot:SetPoint("TOPLEFT", "Row", "TOPLEFT", -13, 13)
|
||||
plate:SetPoint("LEFT", "RowSlot", "RIGHT", -9, -18)
|
||||
"#,
|
||||
@@ -266,9 +266,9 @@ fn set_portrait_texture_binds_unit_token_then_settexture_clears_it() {
|
||||
r#"
|
||||
local f = CreateFrame("Frame", "PFrame")
|
||||
f:SetPoint("TOPLEFT", 0, 0)
|
||||
f:SetSize(100, 100)
|
||||
f:SetWidth(100); f:SetHeight(100)
|
||||
local p = f:CreateTexture("PFramePortrait", "BACKGROUND")
|
||||
p:SetSize(64, 64)
|
||||
p:SetWidth(64); p:SetHeight(64)
|
||||
p:SetPoint("TOPLEFT", 0, 0)
|
||||
SetPortraitTexture(p, "player")
|
||||
"#,
|
||||
@@ -319,7 +319,7 @@ fn benilla_set_booth_texture_binds_square() {
|
||||
r#"
|
||||
local f = CreateFrame("Frame", "DollFrame")
|
||||
f:SetPoint("TOPLEFT", 0, 0)
|
||||
f:SetSize(200, 300)
|
||||
f:SetWidth(200); f:SetHeight(300)
|
||||
local m = f:CreateTexture("DollFrameModel", "ARTWORK")
|
||||
m:SetAllPoints()
|
||||
BenillaSetBoothTexture(m, "paperdoll")
|
||||
@@ -359,11 +359,11 @@ fn long_region_chain_resolves_and_a_frame_binds_to_its_tail() {
|
||||
r#"
|
||||
local f = CreateFrame("Frame", "Book")
|
||||
f:SetPoint("TOPLEFT", 0, 0)
|
||||
f:SetSize(384, 512)
|
||||
f:SetWidth(384); f:SetHeight(512)
|
||||
for i = 1, 12 do
|
||||
local t = f:CreateTexture("Line" .. i, "ARTWORK")
|
||||
t:SetTexture("Interface\\Line" .. i)
|
||||
t:SetSize(300, 10)
|
||||
t:SetWidth(300); t:SetHeight(10)
|
||||
if i == 1 then
|
||||
t:SetPoint("TOPLEFT", "Book", "TOPLEFT", 5, -5)
|
||||
else
|
||||
@@ -371,7 +371,7 @@ fn long_region_chain_resolves_and_a_frame_binds_to_its_tail() {
|
||||
end
|
||||
end
|
||||
local b = CreateFrame("Button", "TailButton", f)
|
||||
b:SetSize(147, 41)
|
||||
b:SetWidth(147); b:SetHeight(41)
|
||||
b:SetPoint("TOPLEFT", "Line12", "BOTTOMLEFT", 0, -6)
|
||||
"#,
|
||||
)
|
||||
@@ -414,13 +414,13 @@ fn child_frame_layers_regions_render_after_the_fixpoint() {
|
||||
r#"
|
||||
local parent = CreateFrame("Frame", "Win")
|
||||
parent:SetPoint("TOPLEFT", 10, -10)
|
||||
parent:SetSize(400, 300)
|
||||
parent:SetWidth(400); parent:SetHeight(300)
|
||||
local child = CreateFrame("Frame", "SubPanel", parent)
|
||||
child:SetPoint("TOPLEFT", "Win", "TOPLEFT", 20, -20)
|
||||
child:SetSize(200, 100)
|
||||
child:SetWidth(200); child:SetHeight(100)
|
||||
local fs = child:CreateFontString("SubText", "ARTWORK")
|
||||
fs:SetPoint("TOPLEFT", "SubPanel", "TOPLEFT", 5, -5)
|
||||
fs:SetSize(150, 12)
|
||||
fs:SetWidth(150); fs:SetHeight(12)
|
||||
fs:SetText("hello from the child")
|
||||
local tex = child:CreateTexture("SubTex", "BACKGROUND")
|
||||
tex:SetTexture("Interface\\SubFill")
|
||||
@@ -462,7 +462,7 @@ fn region_alpha_is_its_own_and_multiplies_the_owner_frames() {
|
||||
r#"
|
||||
f = CreateFrame("Frame", "AlphaOwner")
|
||||
f:SetPoint("BOTTOMLEFT", nil, "BOTTOMLEFT", 0, 0)
|
||||
f:SetSize(10, 10)
|
||||
f:SetWidth(10); f:SetHeight(10)
|
||||
tex = f:CreateTexture("AlphaTex", "ARTWORK")
|
||||
tex:SetTexture("Interface\\Foo")
|
||||
assert(tex:GetAlpha() == 1, "an untouched region is opaque")
|
||||
@@ -895,11 +895,22 @@ fn the_two_region_leaves_answer_their_own_maps() {
|
||||
"GetTexture",
|
||||
"SetTexCoord",
|
||||
"SetBlendMode",
|
||||
"GetBlendMode",
|
||||
"SetTexCoordModifiesRect",
|
||||
"GetTexCoordModifiesRect",
|
||||
"GetVertexColor",
|
||||
] {
|
||||
assert!(has(&s, "Tex", m), "a Texture answers {m}");
|
||||
assert!(!has(&s, "Str", m), "a FontString must NOT answer {m}");
|
||||
}
|
||||
// `SetRotation` is 1.12's PLAYERMODEL verb (`0x84f1fc`/`0x505f00`), not a region one — it is in
|
||||
// neither leaf map, and ours used to put it on Texture.
|
||||
for leaf in ["Tex", "Str"] {
|
||||
assert!(
|
||||
!has(&s, leaf, "SetRotation"),
|
||||
"SetRotation is not a region method"
|
||||
);
|
||||
}
|
||||
// FontString-only.
|
||||
for m in [
|
||||
"SetText",
|
||||
@@ -1012,7 +1023,7 @@ fn the_size_getters_take_the_author_first_then_the_natural_width_and_wrapped_hei
|
||||
s.run(
|
||||
r#"
|
||||
local f = CreateFrame("Frame", "Owner")
|
||||
f:SetPoint("BOTTOMLEFT", 0, 0); f:SetSize(100, 50)
|
||||
f:SetPoint("BOTTOMLEFT", 0, 0); f:SetWidth(100); f:SetHeight(50)
|
||||
Sized = f:CreateFontString("Sized", "ARTWORK")
|
||||
Sized:SetPoint("TOPLEFT"); Sized:SetWidth(300); Sized:SetText("Name")
|
||||
Auto = f:CreateFontString("Auto", "ARTWORK")
|
||||
@@ -1054,7 +1065,7 @@ fn an_empty_string_reads_back_one_unit_and_a_pending_measure_reads_back_zero() {
|
||||
s.run(
|
||||
r#"
|
||||
local f = CreateFrame("Frame", "Owner")
|
||||
f:SetPoint("BOTTOMLEFT", 0, 0); f:SetSize(100, 50)
|
||||
f:SetPoint("BOTTOMLEFT", 0, 0); f:SetWidth(100); f:SetHeight(50)
|
||||
Pending = f:CreateFontString("Pending", "ARTWORK")
|
||||
Pending:SetPoint("TOPLEFT"); Pending:SetText("Name")
|
||||
Empty = f:CreateFontString("Empty", "ARTWORK")
|
||||
@@ -1080,7 +1091,7 @@ fn a_textures_getters_report_its_texel_span_on_an_unsized_axis() {
|
||||
s.run(
|
||||
r#"
|
||||
local f = CreateFrame("Frame", "Owner")
|
||||
f:SetPoint("BOTTOMLEFT", 0, 0); f:SetSize(100, 50)
|
||||
f:SetPoint("BOTTOMLEFT", 0, 0); f:SetWidth(100); f:SetHeight(50)
|
||||
Art = f:CreateTexture("Art", "ARTWORK")
|
||||
Art:SetTexture("Interface\\Crest"); Art:SetPoint("TOPLEFT")
|
||||
Half = f:CreateTexture("Half", "ARTWORK")
|
||||
@@ -1214,9 +1225,9 @@ fn set_portrait_texture_folds_the_token_to_lowercase() {
|
||||
r#"
|
||||
local f = CreateFrame("Frame", "NFrame")
|
||||
f:SetPoint("TOPLEFT", 0, 0)
|
||||
f:SetSize(100, 100)
|
||||
f:SetWidth(100); f:SetHeight(100)
|
||||
local p = f:CreateTexture("NFramePortrait", "BACKGROUND")
|
||||
p:SetSize(64, 64)
|
||||
p:SetWidth(64); p:SetHeight(64)
|
||||
p:SetPoint("TOPLEFT", 0, 0)
|
||||
SetPortraitTexture(p, "NPC")
|
||||
"#,
|
||||
@@ -1278,11 +1289,7 @@ fn an_empty_fontstring_reads_back_nil_and_an_edit_box_does_not() {
|
||||
|
||||
// Never written, and every shape of an empty write, all nil.
|
||||
assert_eq!(text_of(&s, "fresh"), None);
|
||||
for write in [
|
||||
r#"fresh:SetText("")"#,
|
||||
r#"fresh:SetText(nil)"#,
|
||||
r#"fresh:SetFormattedText("%s", "")"#,
|
||||
] {
|
||||
for write in [r#"fresh:SetText("")"#, r#"fresh:SetText(nil)"#] {
|
||||
s.run(write).unwrap();
|
||||
assert_eq!(text_of(&s, "fresh"), None, "after `{write}`");
|
||||
}
|
||||
@@ -1386,3 +1393,144 @@ fn an_unresolvable_set_texture_keeps_the_art_the_region_had() {
|
||||
s.run("t:SetTexture(nil)").unwrap();
|
||||
assert!(s.eval::<bool>("return t:GetTexture() == nil").unwrap());
|
||||
}
|
||||
|
||||
/// `Texture:GetBlendMode()` — ONE string, the reference's own spelling, round-tripping whatever
|
||||
/// `SetBlendMode` was given (`0x79a890`/`0x79a950`, table `0x87c128`, argc 1, arity 1, kinds
|
||||
/// `(string?)`).
|
||||
///
|
||||
/// The untouched default is asserted alongside the round trip because it is the load-bearing half:
|
||||
/// `ShaguTweaks/mods/dark-ui-elements.lua:169` guards a recolour with
|
||||
/// `region.GetBlendMode and region:GetBlendMode() == "ADD"`, so the answer an ordinary texture gives
|
||||
/// decides whether the reference's additive art is left alone. `"BLEND"` is the CSimpleTexture
|
||||
/// ctor's `[+0xd0] = 2` (`0x76fc64`).
|
||||
#[test]
|
||||
fn get_blend_mode_answers_one_string_and_defaults_to_the_ctors_blend() {
|
||||
let s = crate::script::UiScript::new().unwrap();
|
||||
s.run(
|
||||
r#"
|
||||
BlendOwner = CreateFrame("Frame", "BlendOwner")
|
||||
BTex = BlendOwner:CreateTexture("BTex", "ARTWORK")
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Arity 1 and kind string, measured the way the shape gate measures it.
|
||||
assert_eq!(
|
||||
s.eval::<i64>("return select('#', BTex:GetBlendMode())")
|
||||
.unwrap(),
|
||||
1,
|
||||
"arity 1"
|
||||
);
|
||||
assert_eq!(
|
||||
s.eval::<String>("return type(BTex:GetBlendMode())")
|
||||
.unwrap(),
|
||||
"string",
|
||||
"kind string"
|
||||
);
|
||||
assert_eq!(
|
||||
s.eval::<String>("return BTex:GetBlendMode()").unwrap(),
|
||||
"BLEND",
|
||||
"an untouched texture is the ctor's mode 2"
|
||||
);
|
||||
|
||||
// Every one of the enum's five round-trips, including the three the renderer flattens.
|
||||
for mode in ["DISABLE", "ALPHAKEY", "BLEND", "ADD", "MOD"] {
|
||||
s.run(&format!(r#"BTex:SetBlendMode("{mode}")"#)).unwrap();
|
||||
assert_eq!(
|
||||
s.eval::<String>("return BTex:GetBlendMode()").unwrap(),
|
||||
mode,
|
||||
"round trip through the setter"
|
||||
);
|
||||
}
|
||||
// Case-insensitive in, canonical out — the setter coerces, the getter answers the table's name.
|
||||
s.run(r#"BTex:SetBlendMode("add")"#).unwrap();
|
||||
assert_eq!(
|
||||
s.eval::<String>("return BTex:GetBlendMode()").unwrap(),
|
||||
"ADD"
|
||||
);
|
||||
// A name outside the enum leaves the mode alone rather than inventing one.
|
||||
s.run(r#"BTex:SetBlendMode("NOT_A_MODE")"#).unwrap();
|
||||
assert_eq!(
|
||||
s.eval::<String>("return BTex:GetBlendMode()").unwrap(),
|
||||
"ADD",
|
||||
"an unknown name changes nothing"
|
||||
);
|
||||
|
||||
// A FontString is not a Texture: neither half of the pair is on its map.
|
||||
assert!(s
|
||||
.eval::<bool>(
|
||||
r#"local fs = BlendOwner:CreateFontString("BStr", "ARTWORK")
|
||||
return fs.GetBlendMode == nil and fs.SetBlendMode == nil"#
|
||||
)
|
||||
.unwrap());
|
||||
}
|
||||
|
||||
/// `Texture:GetTexCoordModifiesRect()` — the 1.12 predicate return, `1`/`nil` and never a Lua
|
||||
/// boolean (`0x79c120`, table `0x87c128`, argc 1, arity 1, kinds `(nil) | (number)`; decision 2118).
|
||||
/// `pfUI/modules/thirdparty-tbc.lua:319` calls it bare on a Texture.
|
||||
///
|
||||
/// The flag's effect on the region's rect is deliberately NOT wired (see
|
||||
/// `RegionData::tex_coord_modifies_rect`), so this asserts the state and the shape — and asserts
|
||||
/// the un-wiring too, by checking that setting the flag moves no rect: a future change that wires
|
||||
/// the geometry will fail here and have to say so.
|
||||
#[test]
|
||||
fn tex_coord_modifies_rect_is_one_slash_nil_and_moves_no_rect_yet() {
|
||||
let mut s = crate::script::UiScript::new().unwrap();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
s.run(
|
||||
r#"
|
||||
TCMOwner = CreateFrame("Frame", "TCMOwner")
|
||||
TCMOwner:SetPoint("BOTTOMLEFT", nil, "BOTTOMLEFT", 100, 100)
|
||||
TCMOwner:SetWidth(200); TCMOwner:SetHeight(200)
|
||||
TCMTex = TCMOwner:CreateTexture("TCMTex", "ARTWORK")
|
||||
TCMTex:SetTexture("Interface\\TcmArt")
|
||||
TCMTex:SetPoint("BOTTOMLEFT", TCMOwner, "BOTTOMLEFT", 0, 0)
|
||||
TCMTex:SetWidth(64); TCMTex:SetHeight(32)
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
s.resolve();
|
||||
let before = region_tex_rect(&s, "TcmArt");
|
||||
|
||||
assert_eq!(
|
||||
s.eval::<i64>("return select('#', TCMTex:GetTexCoordModifiesRect())")
|
||||
.unwrap(),
|
||||
1,
|
||||
"arity 1"
|
||||
);
|
||||
assert!(
|
||||
s.eval::<bool>("return TCMTex:GetTexCoordModifiesRect() == nil")
|
||||
.unwrap(),
|
||||
"unset is nil, not false"
|
||||
);
|
||||
|
||||
s.run("TCMTex:SetTexCoordModifiesRect(1)").unwrap();
|
||||
assert!(
|
||||
s.eval::<bool>("return TCMTex:GetTexCoordModifiesRect() == 1")
|
||||
.unwrap(),
|
||||
"set is the NUMBER 1, not true"
|
||||
);
|
||||
assert_eq!(
|
||||
s.eval::<String>("return type(TCMTex:GetTexCoordModifiesRect())")
|
||||
.unwrap(),
|
||||
"number",
|
||||
"kind number, never boolean"
|
||||
);
|
||||
|
||||
// The flag is state only: a SetTexCoord under it still leaves the rect where the anchors and
|
||||
// the size put it. When the geometry leg is built, this assertion is the one that must change.
|
||||
s.run("TCMTex:SetTexCoord(0, 0.25, 0, 0.5)").unwrap();
|
||||
s.resolve();
|
||||
assert_eq!(
|
||||
region_tex_rect(&s, "TcmArt"),
|
||||
before,
|
||||
"the flag does not (yet) let SetTexCoord re-derive the region's rect"
|
||||
);
|
||||
|
||||
s.run("TCMTex:SetTexCoordModifiesRect(nil)").unwrap();
|
||||
assert!(
|
||||
s.eval::<bool>("return TCMTex:GetTexCoordModifiesRect() == nil")
|
||||
.unwrap(),
|
||||
"cleared back to nil"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -36,6 +36,11 @@ fn scrollframe_methods_exist_only_on_scrollframes() {
|
||||
return (type(sf.SetScrollChild) == "function") and (plain.SetScrollChild == nil)
|
||||
and (type(sf.SetVerticalScroll) == "function") and (plain.SetVerticalScroll == nil)
|
||||
and (type(sf.GetVerticalScrollRange) == "function")
|
||||
-- the horizontal trio lives in the same class table, not on every Frame
|
||||
and (type(sf.SetHorizontalScroll) == "function") and (plain.SetHorizontalScroll == nil)
|
||||
and (type(sf.GetHorizontalScroll) == "function") and (plain.GetHorizontalScroll == nil)
|
||||
and (type(sf.GetHorizontalScrollRange) == "function")
|
||||
and (plain.GetHorizontalScrollRange == nil)
|
||||
and (type(sf.Show) == "function") -- base methods still reachable through the fallback
|
||||
"#,
|
||||
)
|
||||
@@ -81,13 +86,13 @@ fn scroll_child_top_tracks_vertical_scroll_unclamped_then_restores_on_clear() {
|
||||
r#"
|
||||
local frame = CreateFrame("ScrollFrame", "SF")
|
||||
frame:SetPoint("TOPLEFT", 0, -100) -- screen top 600 -> frame top 500
|
||||
frame:SetSize(300, 200) -- frame bottom 300
|
||||
frame:SetWidth(300); frame:SetHeight(200) -- frame bottom 300
|
||||
|
||||
local child = CreateFrame("Frame", "Child")
|
||||
-- Authored anchor: relative to the screen (child has no parent) — overridden while scrolled,
|
||||
-- and must be exactly what re-applies once the scroll child is cleared.
|
||||
child:SetPoint("TOPLEFT", 20, -20)
|
||||
child:SetSize(300, 600)
|
||||
child:SetWidth(300); child:SetHeight(600)
|
||||
local marker = child:CreateTexture(nil, "ARTWORK")
|
||||
marker:SetTexture("marker:child")
|
||||
marker:SetAllPoints() -- templateless Lua regions carry no implicit anchor (decision 1310)
|
||||
@@ -170,10 +175,10 @@ fn set_vertical_scroll_fires_on_vertical_scroll_only_on_a_change() {
|
||||
Fires = {}
|
||||
local frame = CreateFrame("ScrollFrame", "SF")
|
||||
frame:SetPoint("TOPLEFT", 0, -100)
|
||||
frame:SetSize(300, 200)
|
||||
frame:SetWidth(300); frame:SetHeight(200)
|
||||
frame:SetScript("OnVerticalScroll", function() table.insert(Fires, arg1) end)
|
||||
local child = CreateFrame("Frame", "Child")
|
||||
child:SetSize(300, 600)
|
||||
child:SetWidth(300); child:SetHeight(600)
|
||||
frame:SetScrollChild(child)
|
||||
frame:SetVerticalScroll(0) -- already 0: nothing
|
||||
frame:SetVerticalScroll(40) -- a change
|
||||
@@ -199,9 +204,9 @@ fn vertical_scroll_range_is_live_and_zero_when_unresolved_or_childless_or_shorte
|
||||
r#"
|
||||
local frame = CreateFrame("ScrollFrame", "SF")
|
||||
frame:SetPoint("TOPLEFT", 0, -100)
|
||||
frame:SetSize(300, 200)
|
||||
frame:SetWidth(300); frame:SetHeight(200)
|
||||
local child = CreateFrame("Frame", "Child")
|
||||
child:SetSize(300, 600)
|
||||
child:SetWidth(300); child:SetHeight(600)
|
||||
frame:SetScrollChild(child)
|
||||
"#,
|
||||
)
|
||||
@@ -221,7 +226,7 @@ fn vertical_scroll_range_is_live_and_zero_when_unresolved_or_childless_or_shorte
|
||||
);
|
||||
|
||||
// A child shorter than the frame: 0, not negative.
|
||||
s.run("Child:SetSize(300, 150)").unwrap();
|
||||
s.run("Child:SetWidth(300); Child:SetHeight(150)").unwrap();
|
||||
s.resolve();
|
||||
assert_eq!(
|
||||
s.eval::<f32>("return SF:GetVerticalScrollRange()").unwrap(),
|
||||
@@ -251,10 +256,10 @@ fn vertical_scroll_range_is_local_units_on_a_scaled_frame() {
|
||||
r#"
|
||||
local frame = CreateFrame("ScrollFrame", "SF")
|
||||
frame:SetPoint("TOPLEFT", 0, -100)
|
||||
frame:SetSize(300, 200)
|
||||
frame:SetWidth(300); frame:SetHeight(200)
|
||||
frame:SetScale(0.5)
|
||||
local child = CreateFrame("Frame", "Child", frame)
|
||||
child:SetSize(300, 600)
|
||||
child:SetWidth(300); child:SetHeight(600)
|
||||
frame:SetScrollChild(child)
|
||||
"#,
|
||||
)
|
||||
@@ -299,9 +304,9 @@ fn vertical_scroll_fires_as_given_and_update_rect_fires_range_changed() {
|
||||
r#"
|
||||
local frame = CreateFrame("ScrollFrame", "SF")
|
||||
frame:SetPoint("TOPLEFT", 0, -100)
|
||||
frame:SetSize(300, 200)
|
||||
frame:SetWidth(300); frame:SetHeight(200)
|
||||
local child = CreateFrame("Frame", "Child")
|
||||
child:SetSize(300, 600)
|
||||
child:SetWidth(300); child:SetHeight(600)
|
||||
frame:SetScrollChild(child)
|
||||
|
||||
seen_v = nil
|
||||
@@ -345,9 +350,9 @@ fn update_scroll_child_rect_sees_a_same_tick_resize_with_no_intervening_resolve(
|
||||
r#"
|
||||
local frame = CreateFrame("ScrollFrame", "SF")
|
||||
frame:SetPoint("TOPLEFT", 0, -100)
|
||||
frame:SetSize(300, 200)
|
||||
frame:SetWidth(300); frame:SetHeight(200)
|
||||
local child = CreateFrame("Frame", "Child")
|
||||
child:SetSize(300, 200) -- starts exactly frame-height: range 0
|
||||
child:SetWidth(300); child:SetHeight(200) -- starts exactly frame-height: range 0
|
||||
frame:SetScrollChild(child)
|
||||
"#,
|
||||
)
|
||||
@@ -358,7 +363,7 @@ fn update_scroll_child_rect_sees_a_same_tick_resize_with_no_intervening_resolve(
|
||||
r#"
|
||||
seen_hi = nil
|
||||
SF:SetScript("OnScrollRangeChanged", function(self, lo, hi) seen_hi = hi end)
|
||||
Child:SetSize(300, 600) -- grows well past the frame — all in this ONE tick
|
||||
Child:SetWidth(300); Child:SetHeight(600) -- grows well past the frame — all in this ONE tick
|
||||
SF:UpdateScrollChildRect()
|
||||
"#,
|
||||
)
|
||||
@@ -389,10 +394,10 @@ fn extract_clips_the_scroll_childs_whole_subtree_and_leaves_a_sibling_unclipped(
|
||||
r#"
|
||||
local frame = CreateFrame("ScrollFrame", "SF")
|
||||
frame:SetPoint("TOPLEFT", 0, -100) -- top 500
|
||||
frame:SetSize(300, 200) -- bottom 300, right 300
|
||||
frame:SetWidth(300); frame:SetHeight(200) -- bottom 300, right 300
|
||||
|
||||
local child = CreateFrame("Frame", "Child")
|
||||
child:SetSize(300, 600)
|
||||
child:SetWidth(300); child:SetHeight(600)
|
||||
frame:SetScrollChild(child)
|
||||
local cm = child:CreateTexture(nil, "ARTWORK")
|
||||
cm:SetTexture("marker:child")
|
||||
@@ -400,14 +405,14 @@ fn extract_clips_the_scroll_childs_whole_subtree_and_leaves_a_sibling_unclipped(
|
||||
-- a grandchild FRAME (a real arena descendant of the scroll child) with its own region
|
||||
local grand = CreateFrame("Frame", "Grand", child)
|
||||
grand:SetPoint("TOPLEFT", child, "TOPLEFT", 0, 0)
|
||||
grand:SetSize(50, 50)
|
||||
grand:SetWidth(50); grand:SetHeight(50)
|
||||
local gm = grand:CreateTexture(nil, "ARTWORK")
|
||||
gm:SetTexture("marker:grand")
|
||||
|
||||
-- a sibling entirely outside the ScrollFrame
|
||||
local sib = CreateFrame("Frame", "Sib")
|
||||
sib:SetPoint("TOPLEFT", 0, 0)
|
||||
sib:SetSize(50, 50)
|
||||
sib:SetWidth(50); sib:SetHeight(50)
|
||||
local sm = sib:CreateTexture(nil, "ARTWORK")
|
||||
sm:SetTexture("marker:sib")
|
||||
"#,
|
||||
@@ -441,11 +446,11 @@ fn nested_scrollframes_intersect_their_clip_rects() {
|
||||
r#"
|
||||
local outer = CreateFrame("ScrollFrame", "Outer")
|
||||
outer:SetPoint("TOPLEFT", 0, -50) -- top 550
|
||||
outer:SetSize(400, 300) -- bottom 250, right 400
|
||||
outer:SetWidth(400); outer:SetHeight(300) -- bottom 250, right 400
|
||||
local om = outer:CreateTexture(nil, "ARTWORK"); om:SetTexture("marker:outer")
|
||||
|
||||
local c1 = CreateFrame("Frame", "C1")
|
||||
c1:SetSize(400, 1000)
|
||||
c1:SetWidth(400); c1:SetHeight(1000)
|
||||
outer:SetScrollChild(c1)
|
||||
local c1m = c1:CreateTexture(nil, "ARTWORK"); c1m:SetTexture("marker:c1")
|
||||
|
||||
@@ -454,11 +459,11 @@ fn nested_scrollframes_intersect_their_clip_rects() {
|
||||
-- Outer's right edge — an intersection distinct from either rect alone.
|
||||
local inner = CreateFrame("ScrollFrame", "Inner", c1)
|
||||
inner:SetPoint("TOPLEFT", "C1", "TOPLEFT", 350, -30)
|
||||
inner:SetSize(200, 150)
|
||||
inner:SetWidth(200); inner:SetHeight(150)
|
||||
local im = inner:CreateTexture(nil, "ARTWORK"); im:SetTexture("marker:inner")
|
||||
|
||||
local c2 = CreateFrame("Frame", "C2", inner)
|
||||
c2:SetSize(200, 500)
|
||||
c2:SetWidth(200); c2:SetHeight(500)
|
||||
inner:SetScrollChild(c2)
|
||||
local leaf = c2:CreateTexture(nil, "ARTWORK"); leaf:SetTexture("marker:leaf")
|
||||
"#,
|
||||
@@ -514,16 +519,16 @@ fn hit_test_denies_a_button_clipped_out_and_admits_it_once_scrolled_into_view()
|
||||
r#"
|
||||
local frame = CreateFrame("ScrollFrame", "SF")
|
||||
frame:SetPoint("TOPLEFT", 0, -100) -- top 500
|
||||
frame:SetSize(300, 200) -- bottom 300
|
||||
frame:SetWidth(300); frame:SetHeight(200) -- bottom 300
|
||||
frame:EnableMouse(false) -- isolate the button's own clip-gated hit (§5's subject)
|
||||
|
||||
local child = CreateFrame("Frame", "Child")
|
||||
child:SetSize(300, 600)
|
||||
child:SetWidth(300); child:SetHeight(600)
|
||||
frame:SetScrollChild(child)
|
||||
|
||||
local btn = CreateFrame("Button", "Btn", child)
|
||||
btn:SetPoint("TOPLEFT", "Child", "TOPLEFT", 0, -400)
|
||||
btn:SetSize(50, 20)
|
||||
btn:SetWidth(50); btn:SetHeight(20)
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
@@ -568,9 +573,9 @@ fn update_scroll_child_rect_measures_the_childs_text_before_it_reads_the_range()
|
||||
s.run(
|
||||
r#"
|
||||
local sf = CreateFrame("ScrollFrame", "SF", UIParent)
|
||||
sf:SetPoint("TOPLEFT", 0, 0); sf:SetSize(100, 50)
|
||||
sf:SetPoint("TOPLEFT", 0, 0); sf:SetWidth(100); sf:SetHeight(50)
|
||||
local child = CreateFrame("Frame", "SFChild", sf)
|
||||
child:SetSize(100, 50); sf:SetScrollChild(child)
|
||||
child:SetWidth(100); child:SetHeight(50); sf:SetScrollChild(child)
|
||||
local fs = child:CreateFontString("SFText", "ARTWORK")
|
||||
fs:SetPoint("TOPLEFT", 0, 0); fs:SetWidth(70)
|
||||
sf:SetScript("OnScrollRangeChanged", function() RANGE = arg2 end)
|
||||
@@ -586,3 +591,172 @@ fn update_scroll_child_rect_measures_the_childs_text_before_it_reads_the_range()
|
||||
"56px of measured text under a 50px frame: the range is known at the call, not a frame later"
|
||||
);
|
||||
}
|
||||
|
||||
/// The horizontal axis is the vertical one's byte-clone (`0x786d30` against `0x786db0`), so this is
|
||||
/// the vertical trio's test on the other axis: the offset is stored VERBATIM and re-anchors the
|
||||
/// child, `OnHorizontalScroll` fires only on a real change, and the range is
|
||||
/// `max(0, subtreeWidth - frameWidth)`.
|
||||
///
|
||||
/// **The sign is the point.** `0x787100` hands both offsets to `SetPoint(TOPLEFT, self, TOPLEFT,
|
||||
/// +hScroll, +vScroll)` unnegated, and x grows right — so a positive horizontal pushes the child
|
||||
/// RIGHT where a positive vertical lifts it. `aux-addon/tabs/search/filter.lua:315-322` corroborates
|
||||
/// it from the caller's side (it bounds x into `[frameWidth - contentWidth - 10, 0]` and y into
|
||||
/// `[0, contentHeight - frameHeight]`), which is why it is asserted here rather than assumed.
|
||||
#[test]
|
||||
fn scroll_child_left_tracks_horizontal_scroll_unclamped_and_fires_on_change() {
|
||||
let mut s = script();
|
||||
s.set_screen_size(800.0, 600.0);
|
||||
|
||||
s.run(
|
||||
r#"
|
||||
local frame = CreateFrame("ScrollFrame", "HSF")
|
||||
frame:SetPoint("TOPLEFT", 0, -100) -- frame left 0, top 500
|
||||
frame:SetWidth(300); frame:SetHeight(200) -- frame right 300
|
||||
|
||||
local child = CreateFrame("Frame", "HChild")
|
||||
child:SetPoint("TOPLEFT", 20, -20)
|
||||
child:SetWidth(500); child:SetHeight(200)
|
||||
local marker = child:CreateTexture(nil, "ARTWORK")
|
||||
marker:SetTexture("marker:hchild")
|
||||
marker:SetAllPoints()
|
||||
|
||||
frame:SetScrollChild(child)
|
||||
hfired = {}
|
||||
frame:SetScript("OnHorizontalScroll", function() table.insert(hfired, arg1) end)
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
s.resolve();
|
||||
|
||||
assert_eq!(
|
||||
s.eval::<f32>("return HSF:GetHorizontalScroll()").unwrap(),
|
||||
0.0,
|
||||
"a fresh ScrollFrame is at 0 on both axes"
|
||||
);
|
||||
assert_eq!(
|
||||
marker_rect(&s.extract(), "marker:hchild").map(|r| r.left),
|
||||
Some(0.0),
|
||||
"horizontal=0 -> child left = frame left"
|
||||
);
|
||||
|
||||
// Positive pushes the child RIGHT (the reference's unnegated xOff) …
|
||||
s.run("HSF:SetHorizontalScroll(40)").unwrap();
|
||||
s.resolve();
|
||||
assert_eq!(
|
||||
marker_rect(&s.extract(), "marker:hchild").map(|r| r.left),
|
||||
Some(40.0),
|
||||
"a positive horizontal offset moves the child right"
|
||||
);
|
||||
// … and it is the NEGATIVE direction that scrolls right, which is the half aux-addon relies on.
|
||||
s.run("HSF:SetHorizontalScroll(-60)").unwrap();
|
||||
s.resolve();
|
||||
assert_eq!(
|
||||
marker_rect(&s.extract(), "marker:hchild").map(|r| r.left),
|
||||
Some(-60.0),
|
||||
"a negative horizontal offset scrolls right"
|
||||
);
|
||||
|
||||
// No clamp — the range is 500-300 = 200 and 450 is stored and applied whole (decision 2017's
|
||||
// law, which is `0x786d30`'s as much as `0x786db0`'s: neither reads the range).
|
||||
assert_eq!(
|
||||
s.eval::<f32>("return HSF:GetHorizontalScrollRange()")
|
||||
.unwrap(),
|
||||
200.0,
|
||||
"range = subtree width - frame width"
|
||||
);
|
||||
s.run("HSF:SetHorizontalScroll(450)").unwrap();
|
||||
s.resolve();
|
||||
assert_eq!(
|
||||
s.eval::<f32>("return HSF:GetHorizontalScroll()").unwrap(),
|
||||
450.0,
|
||||
"stored verbatim, past the range"
|
||||
);
|
||||
|
||||
// Arity/kind, the way the shape gate measures them.
|
||||
assert_eq!(
|
||||
s.eval::<i64>("return select('#', HSF:GetHorizontalScroll())")
|
||||
.unwrap(),
|
||||
1
|
||||
);
|
||||
assert_eq!(
|
||||
s.eval::<i64>("return select('#', HSF:GetHorizontalScrollRange())")
|
||||
.unwrap(),
|
||||
1
|
||||
);
|
||||
assert_eq!(
|
||||
s.eval::<String>("return type(HSF:GetHorizontalScroll())")
|
||||
.unwrap(),
|
||||
"number"
|
||||
);
|
||||
assert_eq!(
|
||||
s.eval::<String>("return type(HSF:GetHorizontalScrollRange())")
|
||||
.unwrap(),
|
||||
"number"
|
||||
);
|
||||
assert_eq!(
|
||||
s.eval::<i64>("return select('#', HSF:SetHorizontalScroll(450))")
|
||||
.unwrap(),
|
||||
0,
|
||||
"the setter answers nothing"
|
||||
);
|
||||
|
||||
// The change-gate: four setter calls moved the value (40, -60, 450, and 450 again is not one).
|
||||
let fired: Vec<f32> = (1..=s.eval::<i64>("return table.getn(hfired)").unwrap())
|
||||
.map(|i| s.eval::<f32>(&format!("return hfired[{i}]")).unwrap())
|
||||
.collect();
|
||||
assert_eq!(
|
||||
fired,
|
||||
vec![40.0, -60.0, 450.0],
|
||||
"OnHorizontalScroll fires once per ACTUAL change, with the new offset"
|
||||
);
|
||||
|
||||
// The two axes are independent state, not one offset behind two names.
|
||||
s.run("HSF:SetVerticalScroll(25)").unwrap();
|
||||
assert_eq!(
|
||||
s.eval::<f32>("return HSF:GetHorizontalScroll()").unwrap(),
|
||||
450.0
|
||||
);
|
||||
assert_eq!(
|
||||
s.eval::<f32>("return HSF:GetVerticalScroll()").unwrap(),
|
||||
25.0
|
||||
);
|
||||
}
|
||||
|
||||
/// `UpdateScrollChildRect` notifies `OnScrollRangeChanged(self, xRange, yRange)` — **horizontal
|
||||
/// first**. Byte-derived, not guessed: the vertical result leaves `0x786e30` on top of the x87
|
||||
/// stack, is `fstp`'d first and so pushed deepest, landing as `arg2` (wow-re
|
||||
/// `scrollframe-offset-and-range-law.md` §3.6 — two of that round's own workers published it the
|
||||
/// other way round, which is why it is asserted here). `arg1` was a hardcoded `0.0` for as long as
|
||||
/// there was no horizontal range to put in it.
|
||||
#[test]
|
||||
fn update_scroll_child_rect_reports_both_ranges_horizontal_first() {
|
||||
let mut s = script();
|
||||
s.set_screen_size(800.0, 600.0);
|
||||
s.run(
|
||||
r#"
|
||||
local frame = CreateFrame("ScrollFrame", "BSF")
|
||||
frame:SetPoint("TOPLEFT", 0, -100)
|
||||
frame:SetWidth(300); frame:SetHeight(200)
|
||||
local child = CreateFrame("Frame", "BChild")
|
||||
child:SetPoint("TOPLEFT", 0, 0)
|
||||
child:SetWidth(500); child:SetHeight(600)
|
||||
frame:SetScrollChild(child)
|
||||
seen = nil
|
||||
frame:SetScript("OnScrollRangeChanged", function() seen = { arg1, arg2 } end)
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
s.resolve();
|
||||
s.run("BSF:UpdateScrollChildRect()").unwrap();
|
||||
assert_eq!(
|
||||
s.eval::<f32>("return seen[1]").unwrap(),
|
||||
200.0,
|
||||
"arg1 is the HORIZONTAL range (500 - 300)"
|
||||
);
|
||||
assert_eq!(
|
||||
s.eval::<f32>("return seen[2]").unwrap(),
|
||||
400.0,
|
||||
"arg2 is the VERTICAL range (600 - 200)"
|
||||
);
|
||||
assert!(s.take_errors().is_empty());
|
||||
}
|
||||
|
||||
@@ -122,7 +122,7 @@ fn page(s: &UiScript) {
|
||||
f:SetTextColor(0.18, 0.12, 0.06)
|
||||
Page = CreateFrame("SimpleHTML", "Page")
|
||||
Page:SetPoint("TOPLEFT", 0, 0)
|
||||
Page:SetSize(270, 304)
|
||||
Page:SetWidth(270); Page:SetHeight(304)
|
||||
Page:SetFontObject("P", ItemTextFontNormal)
|
||||
"#,
|
||||
)
|
||||
|
||||
@@ -20,7 +20,7 @@ fn a_resize_fires_on_size_changed_and_a_move_does_not() {
|
||||
fires, w, h = 0, nil, nil
|
||||
Panel = CreateFrame("Frame", "SizedPanel")
|
||||
Panel:SetPoint("BOTTOMLEFT", 100, 100)
|
||||
Panel:SetSize(200, 50)
|
||||
Panel:SetWidth(200); Panel:SetHeight(50)
|
||||
Panel:SetScript("OnSizeChanged", function(self, aw, ah)
|
||||
fires = fires + 1 w = aw h = ah
|
||||
end)
|
||||
@@ -70,7 +70,7 @@ fn an_anchor_driven_resize_fires_it_too() {
|
||||
fires, w = 0, nil
|
||||
Parent = CreateFrame("Frame", "SizeParent")
|
||||
Parent:SetPoint("BOTTOMLEFT", 0, 0)
|
||||
Parent:SetSize(300, 200)
|
||||
Parent:SetWidth(300); Parent:SetHeight(200)
|
||||
Child = CreateFrame("Frame", "SizeChild", Parent)
|
||||
Child:SetPoint("BOTTOMLEFT", Parent, "BOTTOMLEFT", 0, 0)
|
||||
Child:SetPoint("TOPRIGHT", Parent, "TOPRIGHT", 0, 0)
|
||||
@@ -107,7 +107,7 @@ fn a_handler_that_resizes_its_own_frame_settles_instead_of_spinning() {
|
||||
fires = 0
|
||||
Sq = CreateFrame("Frame", "SquarePanel")
|
||||
Sq:SetPoint("BOTTOMLEFT", 0, 0)
|
||||
Sq:SetSize(200, 50)
|
||||
Sq:SetWidth(200); Sq:SetHeight(50)
|
||||
-- The idiom: "keep me square". It writes back into the very input that fired it.
|
||||
Sq:SetScript("OnSizeChanged", function(self, aw, ah)
|
||||
fires = fires + 1
|
||||
@@ -148,7 +148,7 @@ fn set_script_on_size_changed_is_accepted_because_the_resolve_pass_fires_it() {
|
||||
ran = false
|
||||
Accepted = CreateFrame("Frame", "SizeAccepted")
|
||||
Accepted:SetPoint("BOTTOMLEFT", 0, 0)
|
||||
Accepted:SetSize(10, 10)
|
||||
Accepted:SetWidth(10); Accepted:SetHeight(10)
|
||||
Accepted:SetScript("OnSizeChanged", function() ran = true end)
|
||||
"#,
|
||||
)
|
||||
@@ -180,7 +180,7 @@ fn the_on_size_changed_watch_list_is_exactly_the_frames_carrying_the_script() {
|
||||
fires = 0
|
||||
Watched = CreateFrame("Frame", "WatchedPanel")
|
||||
Watched:SetPoint("BOTTOMLEFT", 0, 0)
|
||||
Watched:SetSize(10, 10)
|
||||
Watched:SetWidth(10); Watched:SetHeight(10)
|
||||
Other = CreateFrame("Frame", "UnwatchedPanel")
|
||||
Other:SetScript("OnShow", function() end) -- another kind never enrols
|
||||
local bump = function() fires = fires + 1 end
|
||||
@@ -199,7 +199,8 @@ fn the_on_size_changed_watch_list_is_exactly_the_frames_carrying_the_script() {
|
||||
assert_eq!(watched(&s), 1, "one frame carries the script");
|
||||
|
||||
s.resolve();
|
||||
s.run("Watched:SetSize(40, 40)").unwrap();
|
||||
s.run("Watched:SetWidth(40); Watched:SetHeight(40)")
|
||||
.unwrap();
|
||||
s.resolve();
|
||||
assert_eq!(
|
||||
s.eval::<i64>("return fires").unwrap(),
|
||||
|
||||
@@ -27,10 +27,10 @@ fn slider_thumb_draws_at_value_fraction_along_the_track() {
|
||||
local sl = CreateFrame("Slider", "SlRender")
|
||||
-- A vertical scrollbar: 16 wide, 100 tall, bottom-left at (100, 100) -> track y in [100, 200].
|
||||
sl:SetPoint("BOTTOMLEFT", nil, "BOTTOMLEFT", 100, 100)
|
||||
sl:SetSize(16, 100)
|
||||
sl:SetWidth(16); sl:SetHeight(100)
|
||||
sl:SetThumbTexture("Interface\\Buttons\\UI-ScrollBar-Knob")
|
||||
local t = sl:GetThumbTexture()
|
||||
t:SetSize(16, 16)
|
||||
t:SetWidth(16); t:SetHeight(16)
|
||||
sl:SetMinMaxValues(0, 100)
|
||||
sl:SetValue(0)
|
||||
"#,
|
||||
@@ -73,9 +73,9 @@ fn slider_thumb_drag_maps_cursor_to_value() {
|
||||
r#"
|
||||
bar = CreateFrame("Slider", "SlDrag")
|
||||
bar:SetPoint("BOTTOMLEFT", nil, "BOTTOMLEFT", 100, 100)
|
||||
bar:SetSize(16, 100)
|
||||
bar:SetWidth(16); bar:SetHeight(100)
|
||||
bar:SetThumbTexture("Interface\\Buttons\\UI-ScrollBar-Knob")
|
||||
bar:GetThumbTexture():SetSize(16, 16)
|
||||
local thumb = bar:GetThumbTexture(); thumb:SetWidth(16); thumb:SetHeight(16)
|
||||
bar:SetMinMaxValues(0, 100)
|
||||
bar:SetValue(0)
|
||||
"#,
|
||||
@@ -104,16 +104,16 @@ fn slider_thumb_drag_maps_cursor_to_value() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn slider_track_press_seats_the_thumb_and_a_disabled_slider_ignores_it() {
|
||||
fn slider_track_press_seats_the_thumb_and_a_mouseless_slider_ignores_it() {
|
||||
let mut s = script();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
s.run(
|
||||
r#"
|
||||
bar = CreateFrame("Slider", "SlTrack")
|
||||
bar:SetPoint("BOTTOMLEFT", nil, "BOTTOMLEFT", 100, 100)
|
||||
bar:SetSize(16, 100)
|
||||
bar:SetWidth(16); bar:SetHeight(100)
|
||||
bar:SetThumbTexture("Interface\\Buttons\\UI-ScrollBar-Knob")
|
||||
bar:GetThumbTexture():SetSize(16, 16)
|
||||
local thumb = bar:GetThumbTexture(); thumb:SetWidth(16); thumb:SetHeight(16)
|
||||
bar:SetMinMaxValues(0, 100)
|
||||
bar:SetValue(0)
|
||||
fired = {}
|
||||
@@ -142,18 +142,25 @@ fn slider_track_press_seats_the_thumb_and_a_disabled_slider_ignores_it() {
|
||||
assert_eq!(v, 100.0, "the gesture drags on without re-grabbing");
|
||||
s.mouse_button(108.0, 108.0, "LeftButton", false);
|
||||
|
||||
// A disabled slider ignores a press anywhere — thumb and track alike.
|
||||
s.run(r#"SlTrack:SetValue(0); SlTrack:Disable(); fired = {}"#)
|
||||
// A slider the mouse cannot reach ignores a press anywhere — thumb and track alike. This was
|
||||
// `SlTrack:Disable()` until the Button-only `Enable`/`Disable`/`IsEnabled` trio came off the
|
||||
// Slider table (1.12 registers them on `0x879d00` alone, and the Slider's LoadXML has no
|
||||
// `enabled` attribute either). `EnableMouse(false)` is how the reference keeps a slider off a
|
||||
// press, and it gates upstream in the hit test rather than inside `begin_drag`.
|
||||
s.run(r#"SlTrack:SetValue(0); SlTrack:EnableMouse(false); fired = {}"#)
|
||||
.unwrap();
|
||||
for y in [192.0, 150.0] {
|
||||
s.mouse_button(108.0, y, "LeftButton", true);
|
||||
s.mouse_move(108.0, 130.0);
|
||||
let v: f32 = s.eval("return SlTrack:GetValue()").unwrap();
|
||||
assert_eq!(v, 0.0, "disabled slider does not move (press at y={y})");
|
||||
assert_eq!(
|
||||
v, 0.0,
|
||||
"mouse-disabled slider does not move (press at y={y})"
|
||||
);
|
||||
s.mouse_button(108.0, 130.0, "LeftButton", false);
|
||||
}
|
||||
let n: usize = s.eval("return table.getn(fired)").unwrap();
|
||||
assert_eq!(n, 0, "disabled: no OnValueChanged at all");
|
||||
assert_eq!(n, 0, "mouse disabled: no OnValueChanged at all");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -235,17 +242,30 @@ fn slider_setvalue_fires_onvaluechanged_only_on_change() {
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
/// `Enable`/`Disable`/`IsEnabled` are a **Button** trio in 1.12 (table `0x879d00`), and a Slider
|
||||
/// must not answer them: a duck-typing addon that branches on `if widget.IsEnabled then` reads a
|
||||
/// superset as the wrong class (1189/1250 §5). This asserts the removal *and* its control — the
|
||||
/// same three names still present on a Button, so a regression that emptied the Button table would
|
||||
/// fail here rather than pass by accident.
|
||||
#[test]
|
||||
fn slider_enable_disable_roundtrips() {
|
||||
fn a_slider_does_not_answer_the_buttons_enable_trio() {
|
||||
let s = script();
|
||||
s.run(
|
||||
r#"
|
||||
local sl = CreateFrame("Slider", "SlEnable")
|
||||
assert(sl:IsEnabled(), "enabled by ctor")
|
||||
sl:Disable()
|
||||
assert(not sl:IsEnabled(), "disabled")
|
||||
sl:Enable()
|
||||
assert(sl:IsEnabled(), "re-enabled")
|
||||
for _, name in ipairs({ "Enable", "Disable", "IsEnabled" }) do
|
||||
assert(sl[name] == nil, "Slider must not answer " .. name)
|
||||
end
|
||||
local b = CreateFrame("Button", "SlEnableControl")
|
||||
for _, name in ipairs({ "Enable", "Disable", "IsEnabled" }) do
|
||||
assert(type(b[name]) == "function", "Button still answers " .. name)
|
||||
end
|
||||
-- The Button's own predicate is the NUMBER 1 / the NUMBER 0, never a Lua boolean — its
|
||||
-- false leg is 0 rather than nil, settled per body at `0x7800b0`'s `setne`+`fild`
|
||||
-- (wow-re `button-enabled-state.md`; decision 2118's `binding_abi::flag` doc).
|
||||
assert(b:IsEnabled() == 1, "1, not true")
|
||||
b:Disable()
|
||||
assert(b:IsEnabled() == 0, "0, not false and not nil")
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
@@ -279,9 +299,9 @@ fn slider_scrollbar_wiring_does_not_recurse() {
|
||||
r#"
|
||||
local sf = CreateFrame("ScrollFrame", "SlSF")
|
||||
sf:SetPoint("TOPLEFT", nil, "TOPLEFT", 0, 0)
|
||||
sf:SetSize(100, 100)
|
||||
sf:SetWidth(100); sf:SetHeight(100)
|
||||
local child = CreateFrame("Frame", "SlSFChild", sf)
|
||||
child:SetSize(100, 300) -- 200px taller than the frame -> scroll range 200
|
||||
child:SetWidth(100); child:SetHeight(300) -- 200px taller than the frame -> scroll range 200
|
||||
sf:SetScrollChild(child)
|
||||
|
||||
bar = CreateFrame("Slider", "SlSFBar")
|
||||
@@ -399,7 +419,7 @@ fn a_thumb_with_no_authored_size_takes_its_arts_texel_span_and_still_drags() {
|
||||
-- Slider and not to the mouse-enabled host sitting under it.
|
||||
host = CreateFrame("Frame", "DdHost", nil)
|
||||
host:SetPoint("BOTTOMLEFT", nil, "BOTTOMLEFT", 100, 100)
|
||||
host:SetSize(80, 170)
|
||||
host:SetWidth(80); host:SetHeight(170)
|
||||
host:SetFrameStrata("FULLSCREEN_DIALOG")
|
||||
host:EnableMouse(true)
|
||||
bar = CreateFrame("Slider", "DdSlider", host)
|
||||
|
||||
@@ -72,7 +72,7 @@ fn statusbar_bar_region_scales_by_fraction_on_extract() {
|
||||
r#"
|
||||
local b = CreateFrame("StatusBar", "SbFill")
|
||||
b:SetPoint("BOTTOMLEFT", nil, "BOTTOMLEFT", 100, 100)
|
||||
b:SetSize(200, 20)
|
||||
b:SetWidth(200); b:SetHeight(20)
|
||||
b:SetStatusBarTexture(0.2, 0.7, 0.2)
|
||||
b:SetMinMaxValues(0, 100)
|
||||
b:SetValue(25)
|
||||
@@ -137,7 +137,7 @@ fn statusbar_bar_region_crops_its_texture_rather_than_stretching_it() {
|
||||
r#"
|
||||
local b = CreateFrame("StatusBar", "SbCrop")
|
||||
b:SetPoint("BOTTOMLEFT", nil, "BOTTOMLEFT", 0, 0)
|
||||
b:SetSize(200, 20)
|
||||
b:SetWidth(200); b:SetHeight(20)
|
||||
b:SetStatusBarTexture("Interface\\TargetingFrame\\UI-StatusBar")
|
||||
b:SetMinMaxValues(0, 100)
|
||||
b:SetValue(25)
|
||||
|
||||
@@ -147,7 +147,7 @@ fn set_talent_renders_the_interleaved_tooltip() {
|
||||
);
|
||||
s.run(
|
||||
r#"
|
||||
local a = CreateFrame("Button", "TB1"); a:SetPoint("CENTER", 0, 0); a:SetSize(10, 10)
|
||||
local a = CreateFrame("Button", "TB1"); a:SetPoint("CENTER", 0, 0); a:SetWidth(10); a:SetHeight(10)
|
||||
local tt = CreateFrame("GameTooltip", "TT")
|
||||
tt:SetOwner(a, "ANCHOR_RIGHT")
|
||||
tt:SetTalent(1, 1)
|
||||
@@ -183,7 +183,7 @@ fn set_talent_locked_reqs_and_the_ask_once_miss() {
|
||||
// No spell view pushed for Ignite (11119): the render falls back, the ask is recorded.
|
||||
s.run(
|
||||
r#"
|
||||
local a = CreateFrame("Button", "TB2"); a:SetPoint("CENTER", 0, 0); a:SetSize(10, 10)
|
||||
local a = CreateFrame("Button", "TB2"); a:SetPoint("CENTER", 0, 0); a:SetWidth(10); a:SetHeight(10)
|
||||
local tt = CreateFrame("GameTooltip", "TT2")
|
||||
tt:SetOwner(a, "ANCHOR_RIGHT")
|
||||
tt:SetTalent(1, 2)
|
||||
|
||||
@@ -31,7 +31,7 @@ fn line_stack_autosize_and_right_flush() {
|
||||
s.run(
|
||||
r#"
|
||||
local owner = CreateFrame("Button", "Slot")
|
||||
owner:SetPoint("TOPLEFT", 100, -100); owner:SetSize(40, 40)
|
||||
owner:SetPoint("TOPLEFT", 100, -100); owner:SetWidth(40); owner:SetHeight(40)
|
||||
local tt = CreateFrame("GameTooltip", "TT")
|
||||
tt:SetOwner(owner, "ANCHOR_RIGHT")
|
||||
tt:AddLine("Tough Jerky", 1, 1, 1)
|
||||
@@ -101,7 +101,7 @@ fn empty_line_is_a_one_unit_row_and_the_chain_stays_inside_the_plate() {
|
||||
s.run(
|
||||
r#"
|
||||
local owner = CreateFrame("Button", "Slot2")
|
||||
owner:SetPoint("TOPLEFT", 100, -100); owner:SetSize(40, 40)
|
||||
owner:SetPoint("TOPLEFT", 100, -100); owner:SetWidth(40); owner:SetHeight(40)
|
||||
local tt = CreateFrame("GameTooltip", "TTE")
|
||||
tt:SetOwner(owner, "ANCHOR_RIGHT")
|
||||
tt:AddLine("Marshal McBride", 0.25, 0.75, 0.25)
|
||||
@@ -154,15 +154,15 @@ fn owner_clear_and_hide_lifecycle() {
|
||||
s.run(
|
||||
r#"
|
||||
cleared = 0
|
||||
local a = CreateFrame("Button", "A"); a:SetPoint("CENTER", 0, 0); a:SetSize(10, 10)
|
||||
local b = CreateFrame("Button", "B"); b:SetPoint("CENTER", 50, 0); b:SetSize(10, 10)
|
||||
local a = CreateFrame("Button", "A"); a:SetPoint("CENTER", 0, 0); a:SetWidth(10); a:SetHeight(10)
|
||||
local b = CreateFrame("Button", "B"); b:SetPoint("CENTER", 50, 0); b:SetWidth(10); b:SetHeight(10)
|
||||
local tt = CreateFrame("GameTooltip", "TT2")
|
||||
tt:SetScript("OnTooltipCleared", function() cleared = cleared + 1 end)
|
||||
tt:SetOwner(a, "ANCHOR_RIGHT")
|
||||
tt:AddLine("first hover")
|
||||
tt:Show()
|
||||
assert(tt:IsOwned(a) and not tt:IsOwned(b), "owned by a")
|
||||
assert(tt:GetOwner() == a, "GetOwner returns the owner wrapper")
|
||||
assert(tt:BenillaGetTooltipOwner() == a, "the owner wrapper reads back")
|
||||
tt:SetOwner(b, "ANCHOR_LEFT")
|
||||
assert(cleared >= 1, "SetOwner cleared the old content")
|
||||
assert(tt:NumLines() == 0, "content cleared on re-own")
|
||||
@@ -190,7 +190,7 @@ fn settext_shows_and_both_addline_shapes() {
|
||||
s.set_screen_size(800.0, 600.0);
|
||||
s.run(
|
||||
r#"
|
||||
local a = CreateFrame("Button", "A3"); a:SetPoint("CENTER", 0, 0); a:SetSize(10, 10)
|
||||
local a = CreateFrame("Button", "A3"); a:SetPoint("CENTER", 0, 0); a:SetWidth(10); a:SetHeight(10)
|
||||
local tt = CreateFrame("GameTooltip", "TT3")
|
||||
tt:Hide() -- the XML instance ships hidden="true"; CreateFrame defaults shown
|
||||
tt:SetOwner(a, "ANCHOR_RIGHT")
|
||||
@@ -248,9 +248,9 @@ fn unmeasured_lines_hold_declared_size() {
|
||||
s.set_screen_size(800.0, 600.0);
|
||||
s.run(
|
||||
r#"
|
||||
local a = CreateFrame("Button", "A6"); a:SetPoint("TOPLEFT", 100, -100); a:SetSize(10, 10)
|
||||
local a = CreateFrame("Button", "A6"); a:SetPoint("TOPLEFT", 100, -100); a:SetWidth(10); a:SetHeight(10)
|
||||
local tt = CreateFrame("GameTooltip", "TT6")
|
||||
tt:SetSize(120, 32)
|
||||
tt:SetWidth(120); tt:SetHeight(32)
|
||||
tt:SetOwner(a, "ANCHOR_RIGHT")
|
||||
tt:AddLine("Tough Jerky")
|
||||
tt:AddDoubleLine("One-Hand", "Sword")
|
||||
@@ -291,7 +291,7 @@ fn minimum_width_floors_autosize() {
|
||||
s.set_screen_size(800.0, 600.0);
|
||||
s.run(
|
||||
r#"
|
||||
local a = CreateFrame("Button", "A4"); a:SetPoint("TOPLEFT", 100, -100); a:SetSize(10, 10)
|
||||
local a = CreateFrame("Button", "A4"); a:SetPoint("TOPLEFT", 100, -100); a:SetWidth(10); a:SetHeight(10)
|
||||
local tt = CreateFrame("GameTooltip", "TT4")
|
||||
tt:SetOwner(a, "ANCHOR_RIGHT")
|
||||
tt:AddLine("tiny")
|
||||
@@ -316,7 +316,7 @@ fn fadeout_ramps_then_hides() {
|
||||
s.run(
|
||||
r#"
|
||||
cleared = 0
|
||||
local a = CreateFrame("Button", "A5"); a:SetPoint("CENTER", 0, 0); a:SetSize(10, 10)
|
||||
local a = CreateFrame("Button", "A5"); a:SetPoint("CENTER", 0, 0); a:SetWidth(10); a:SetHeight(10)
|
||||
local tt = CreateFrame("GameTooltip", "TT5")
|
||||
tt:SetScript("OnTooltipCleared", function() cleared = cleared + 1 end)
|
||||
tt:SetOwner(a, "ANCHOR_RIGHT")
|
||||
@@ -387,7 +387,7 @@ fn owner_anchored_tooltip_clamps_to_screen() {
|
||||
s.run(
|
||||
r#"
|
||||
local owner = CreateFrame("Button", "ZoneTextBtn")
|
||||
owner:SetPoint("TOPRIGHT", -100, 0); owner:SetSize(90, 14)
|
||||
owner:SetPoint("TOPRIGHT", -100, 0); owner:SetWidth(90); owner:SetHeight(14)
|
||||
local tt = CreateFrame("GameTooltip", "TTC")
|
||||
assert(tt:IsClampedToScreen(), "a GameTooltip clamps by construction")
|
||||
assert(not owner:IsClampedToScreen(), "a plain frame does not")
|
||||
@@ -539,7 +539,7 @@ fn an_emptied_pooled_line_drops_its_stale_box_and_the_plate_still_contains_the_c
|
||||
s.run(
|
||||
r#"
|
||||
local owner = CreateFrame("Button", "SlotR")
|
||||
owner:SetPoint("TOPLEFT", 100, -100); owner:SetSize(40, 40)
|
||||
owner:SetPoint("TOPLEFT", 100, -100); owner:SetWidth(40); owner:SetHeight(40)
|
||||
local tt = CreateFrame("GameTooltip", "TTR")
|
||||
tt:SetOwner(owner, "ANCHOR_RIGHT")
|
||||
tt:AddLine("Marshal McBride")
|
||||
@@ -589,3 +589,92 @@ fn an_emptied_pooled_line_drops_its_stale_box_and_the_plate_still_contains_the_c
|
||||
.unwrap();
|
||||
assert!(s.take_errors().is_empty());
|
||||
}
|
||||
|
||||
/// `GameTooltip:GetAnchorType()` — ONE string, the reference's own spelling, round-tripping
|
||||
/// whatever `SetOwner` was given (`0x5313e0`, table `0x854198`, argc 1, arity 1, kinds `(string)`,
|
||||
/// reading `[+0x318]` back through the name table `0x531530`).
|
||||
///
|
||||
/// The round trip is the whole point rather than a nicety: `_Nameplates/_Nameplates.lua:479` is
|
||||
/// `if GameTooltip:GetAnchorType() ~= Anchor then GameTooltip:SetOwner(Column, Anchor) end`, so an
|
||||
/// answer that never equals what the setter was handed would re-own the plate every OnUpdate; and
|
||||
/// `pfUI/modules/tooltip.lua:97` compares against the literal `"ANCHOR_NONE"`.
|
||||
#[test]
|
||||
fn tooltip_anchor_type_round_trips_every_reachable_mode() {
|
||||
let s = script();
|
||||
s.run(
|
||||
r#"
|
||||
AnchorTip = CreateFrame("GameTooltip", "AnchorTip")
|
||||
AnchorOwner = CreateFrame("Frame", "AnchorOwner")
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Arity 1, kind string, and NO nil leg — the shapes row is `(string)` with no alternative, so
|
||||
// a plate nobody has owned still answers a string.
|
||||
assert_eq!(
|
||||
s.eval::<i64>("return select('#', AnchorTip:GetAnchorType())")
|
||||
.unwrap(),
|
||||
1,
|
||||
"arity 1"
|
||||
);
|
||||
assert_eq!(
|
||||
s.eval::<String>("return type(AnchorTip:GetAnchorType())")
|
||||
.unwrap(),
|
||||
"string",
|
||||
"kind string, never nil"
|
||||
);
|
||||
assert_eq!(
|
||||
s.eval::<String>("return AnchorTip:GetAnchorType()")
|
||||
.unwrap(),
|
||||
"ANCHOR_NONE",
|
||||
"a plate nothing has owned is anchored to nothing"
|
||||
);
|
||||
|
||||
// The eight modes SetOwner accepts, each answered back verbatim — ANCHOR_PRESERVE included,
|
||||
// which the reference stores like any other rather than resolving back to what it preserved.
|
||||
for mode in [
|
||||
"ANCHOR_RIGHT",
|
||||
"ANCHOR_LEFT",
|
||||
"ANCHOR_TOPRIGHT",
|
||||
"ANCHOR_TOPLEFT",
|
||||
"ANCHOR_BOTTOMRIGHT",
|
||||
"ANCHOR_BOTTOMLEFT",
|
||||
"ANCHOR_NONE",
|
||||
"ANCHOR_PRESERVE",
|
||||
] {
|
||||
s.run(&format!(r#"AnchorTip:SetOwner(AnchorOwner, "{mode}")"#))
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
s.eval::<String>("return AnchorTip:GetAnchorType()")
|
||||
.unwrap(),
|
||||
mode,
|
||||
"round trip through SetOwner"
|
||||
);
|
||||
}
|
||||
|
||||
// Lower case in, the reference's canonical spelling out — SetOwner already folds the token.
|
||||
s.run(r#"AnchorTip:SetOwner(AnchorOwner, "anchor_left")"#)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
s.eval::<String>("return AnchorTip:GetAnchorType()")
|
||||
.unwrap(),
|
||||
"ANCHOR_LEFT"
|
||||
);
|
||||
|
||||
// An anchor SetOwner cannot honour is recorded as the ANCHOR_RIGHT it is actually PLACED by,
|
||||
// never as the string it was handed — the getter reports the plate, not the request.
|
||||
// `ANCHOR_CURSOR` is the live case (nine corpus files ask for it; mode 6 follows the cursor per
|
||||
// frame and this engine has no such driver), and SetOwner warns rather than pretending.
|
||||
s.run(r#"AnchorTip:SetOwner(AnchorOwner, "ANCHOR_CURSOR")"#)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
s.eval::<String>("return AnchorTip:GetAnchorType()")
|
||||
.unwrap(),
|
||||
"ANCHOR_RIGHT"
|
||||
);
|
||||
assert!(
|
||||
s.warnings().iter().any(|w| w.contains("ANCHOR_CURSOR")),
|
||||
"the unhonoured anchor is warned, not silently taken: {:?}",
|
||||
s.warnings()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -93,7 +93,7 @@ fn shift_compare_fires_seats_and_renders_the_compare_shape() {
|
||||
);
|
||||
s.run(
|
||||
r#"
|
||||
local a = CreateFrame("Button", "Slot"); a:SetPoint("CENTER", 0, 0); a:SetSize(10, 10)
|
||||
local a = CreateFrame("Button", "Slot"); a:SetPoint("CENTER", 0, 0); a:SetWidth(10); a:SetHeight(10)
|
||||
-- CreateFrame'd frames start SHOWN; the shipped XML instances are hidden="true".
|
||||
CreateFrame("GameTooltip", "GameTooltip"):Hide()
|
||||
CreateFrame("GameTooltip", "ShoppingTooltip1"):Hide()
|
||||
@@ -101,7 +101,7 @@ fn shift_compare_fires_seats_and_renders_the_compare_shape() {
|
||||
compare_calls = {}
|
||||
for slot = 11, 12 do
|
||||
local f = CreateFrame("Button", "Doll" .. slot)
|
||||
f:SetPoint("CENTER", 100, 0); f:SetSize(8, 8)
|
||||
f:SetPoint("CENTER", 100, 0); f:SetWidth(8); f:SetHeight(8)
|
||||
f.invSlotId = slot
|
||||
f:RegisterEvent("SHOW_COMPARE_TOOLTIP")
|
||||
f:SetScript("OnEvent", function()
|
||||
@@ -213,7 +213,7 @@ fn set_inventory_item_renders_full_outside_compare() {
|
||||
);
|
||||
s.run(
|
||||
r#"
|
||||
local a = CreateFrame("Button", "Slot9"); a:SetPoint("CENTER", 0, 0); a:SetSize(10, 10)
|
||||
local a = CreateFrame("Button", "Slot9"); a:SetPoint("CENTER", 0, 0); a:SetWidth(10); a:SetHeight(10)
|
||||
local tt = CreateFrame("GameTooltip", "TT")
|
||||
tt:SetOwner(a, "ANCHOR_RIGHT")
|
||||
assert(tt:SetInventoryItem("player", 16) == 1, "occupied slot answers 1")
|
||||
@@ -266,7 +266,7 @@ fn set_hyperlink_renders_items_and_ignores_other_links() {
|
||||
);
|
||||
s.run(
|
||||
r#"
|
||||
local a = CreateFrame("Button", "Slot10"); a:SetPoint("CENTER", 0, 0); a:SetSize(10, 10)
|
||||
local a = CreateFrame("Button", "Slot10"); a:SetPoint("CENTER", 0, 0); a:SetWidth(10); a:SetHeight(10)
|
||||
local tt = CreateFrame("GameTooltip", "TT")
|
||||
tt:SetOwner(a, "ANCHOR_RIGHT")
|
||||
tt:SetHyperlink("|cff1eff00|Hitem:7002:0:0:0|h[New Loop]|h|r")
|
||||
|
||||
@@ -23,11 +23,11 @@ fn item_line_law_and_red_requirements() {
|
||||
});
|
||||
s.run(
|
||||
r#"
|
||||
local a = CreateFrame("Button", "Slot"); a:SetPoint("CENTER", 0, 0); a:SetSize(10, 10)
|
||||
local a = CreateFrame("Button", "Slot"); a:SetPoint("CENTER", 0, 0); a:SetWidth(10); a:SetHeight(10)
|
||||
local tt = CreateFrame("GameTooltip", "TT")
|
||||
tt:SetOwner(a, "ANCHOR_RIGHT")
|
||||
tt:SetItemById(871)
|
||||
assert(tt:IsShown(), "SetItemById shows")
|
||||
tt:BenillaSetItemById(871)
|
||||
assert(tt:IsShown(), "BenillaSetItemById shows")
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
@@ -95,10 +95,10 @@ fn red_lines_track_player_state() {
|
||||
});
|
||||
s.run(
|
||||
r#"
|
||||
local a = CreateFrame("Button", "Slot2"); a:SetPoint("CENTER", 0, 0); a:SetSize(10, 10)
|
||||
local a = CreateFrame("Button", "Slot2"); a:SetPoint("CENTER", 0, 0); a:SetWidth(10); a:SetHeight(10)
|
||||
local tt = CreateFrame("GameTooltip", "TT")
|
||||
tt:SetOwner(a, "ANCHOR_RIGHT")
|
||||
tt:SetItemById(871)
|
||||
tt:BenillaSetItemById(871)
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
@@ -122,7 +122,7 @@ fn red_lines_track_player_state() {
|
||||
/// The §5-verified families folded back 2026-07-10 (tooltip-content-law.md): SIGNABLE green,
|
||||
/// UNIQUE before STARTS_QUEST, LOCKED red, six-equal resistances collapse to the ALL line (and
|
||||
/// Holy never prints singly), a known taught spell reds "Already known", the description gold —
|
||||
/// and NO openable line: `SetItemById` is a template source, and the whole openable/readable/
|
||||
/// and NO openable line: `BenillaSetItemById` is a template source, and the whole openable/readable/
|
||||
/// creator tail rides the ref's item-OBJECT gate (`0x52e1c7`/`0x52e2e0` — byte-read 2026-07-20;
|
||||
/// the instance-tail law itself is in [`instance_tail_creator_and_readable`]).
|
||||
#[test]
|
||||
@@ -154,10 +154,10 @@ fn verified_families_signable_locked_resists_known() {
|
||||
});
|
||||
s.run(
|
||||
r#"
|
||||
local a = CreateFrame("Button", "Slot5"); a:SetPoint("CENTER", 0, 0); a:SetSize(10, 10)
|
||||
local a = CreateFrame("Button", "Slot5"); a:SetPoint("CENTER", 0, 0); a:SetWidth(10); a:SetHeight(10)
|
||||
local tt = CreateFrame("GameTooltip", "TT")
|
||||
tt:SetOwner(a, "ANCHOR_RIGHT")
|
||||
tt:SetItemById(5518)
|
||||
tt:BenillaSetItemById(5518)
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
@@ -208,7 +208,7 @@ fn verified_families_signable_locked_resists_known() {
|
||||
s.run(
|
||||
r#"
|
||||
TT:SetOwner(Slot5, "ANCHOR_RIGHT")
|
||||
TT:SetItemById(5519)
|
||||
TT:BenillaSetItemById(5519)
|
||||
assert(TT:NumLines() == 1, "a lone Holy resist prints no line, got " .. TT:NumLines())
|
||||
"#,
|
||||
)
|
||||
@@ -243,10 +243,10 @@ fn proficiency_and_reputation_reds() {
|
||||
s.set_player_req_state(req.clone());
|
||||
s.run(
|
||||
r#"
|
||||
local a = CreateFrame("Button", "Slot9"); a:SetPoint("CENTER", 0, 0); a:SetSize(10, 10)
|
||||
local a = CreateFrame("Button", "Slot9"); a:SetPoint("CENTER", 0, 0); a:SetWidth(10); a:SetHeight(10)
|
||||
local tt = CreateFrame("GameTooltip", "TT")
|
||||
tt:SetOwner(a, "ANCHOR_RIGHT")
|
||||
tt:SetItemById(871)
|
||||
tt:BenillaSetItemById(871)
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
@@ -281,7 +281,7 @@ fn proficiency_and_reputation_reds() {
|
||||
req2.proficiency.insert(2, 1 << 0);
|
||||
req2.rep_ranks.insert(72, 4);
|
||||
s.set_player_req_state(req2.clone());
|
||||
s.run(r#"TT:SetOwner(Slot9, "ANCHOR_RIGHT"); TT:SetItemById(871)"#)
|
||||
s.run(r#"TT:SetOwner(Slot9, "ANCHOR_RIGHT"); TT:BenillaSetItemById(871)"#)
|
||||
.unwrap();
|
||||
let lines = lines_of(&mut s);
|
||||
assert_eq!(
|
||||
@@ -300,7 +300,7 @@ fn proficiency_and_reputation_reds() {
|
||||
let mut alt = item;
|
||||
alt.proficiency_alt = Some(0);
|
||||
s.set_item_template(871, alt);
|
||||
s.run(r#"TT:SetOwner(Slot9, "ANCHOR_RIGHT"); TT:SetItemById(871)"#)
|
||||
s.run(r#"TT:SetOwner(Slot9, "ANCHOR_RIGHT"); TT:BenillaSetItemById(871)"#)
|
||||
.unwrap();
|
||||
let lines = lines_of(&mut s);
|
||||
assert_eq!(
|
||||
@@ -328,7 +328,7 @@ fn proficiency_and_reputation_reds() {
|
||||
let mut req3 = req2.clone();
|
||||
req3.proficiency.insert(2, 1 << 15);
|
||||
s.set_player_req_state(req3.clone());
|
||||
s.run(r#"TT:SetOwner(Slot9, "ANCHOR_RIGHT"); TT:SetItemById(872)"#)
|
||||
s.run(r#"TT:SetOwner(Slot9, "ANCHOR_RIGHT"); TT:BenillaSetItemById(872)"#)
|
||||
.unwrap();
|
||||
let lines = lines_of(&mut s);
|
||||
// `INVTYPE_WEAPONOFFHAND`, NOT `INVTYPE_SHIELD` — one enUS sentence, two keys, and only the
|
||||
@@ -345,7 +345,7 @@ fn proficiency_and_reputation_reds() {
|
||||
);
|
||||
req3.can_dual_wield = true;
|
||||
s.set_player_req_state(req3);
|
||||
s.run(r#"TT:SetOwner(Slot9, "ANCHOR_RIGHT"); TT:SetItemById(872)"#)
|
||||
s.run(r#"TT:SetOwner(Slot9, "ANCHOR_RIGHT"); TT:BenillaSetItemById(872)"#)
|
||||
.unwrap();
|
||||
let lines = lines_of(&mut s);
|
||||
assert_eq!(
|
||||
@@ -365,7 +365,7 @@ fn proficiency_and_reputation_reds() {
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
s.run(r#"TT:SetOwner(Slot9, "ANCHOR_RIGHT"); TT:SetItemById(118)"#)
|
||||
s.run(r#"TT:SetOwner(Slot9, "ANCHOR_RIGHT"); TT:BenillaSetItemById(118)"#)
|
||||
.unwrap();
|
||||
let lines = lines_of(&mut s);
|
||||
assert_eq!(
|
||||
@@ -387,7 +387,7 @@ fn proficiency_and_reputation_reds() {
|
||||
);
|
||||
s.run(
|
||||
r#"
|
||||
TT:SetOwner(Slot9, "ANCHOR_RIGHT"); TT:SetItemById(889)
|
||||
TT:SetOwner(Slot9, "ANCHOR_RIGHT"); TT:BenillaSetItemById(889)
|
||||
assert(TTTextLeft2:GetText() == "[INVTYPE_FINGER]")
|
||||
assert(TTTextRight2:GetText() == nil or TTTextRight2:GetText() == "",
|
||||
"a ring never prints its Miscellaneous type")
|
||||
@@ -416,13 +416,13 @@ fn required_level_one_is_hidden() {
|
||||
}
|
||||
s.run(
|
||||
r#"
|
||||
local a = CreateFrame("Button", "SlotR"); a:SetPoint("CENTER", 0, 0); a:SetSize(10, 10)
|
||||
local a = CreateFrame("Button", "SlotR"); a:SetPoint("CENTER", 0, 0); a:SetWidth(10); a:SetHeight(10)
|
||||
local tt = CreateFrame("GameTooltip", "TT")
|
||||
tt:SetOwner(a, "ANCHOR_RIGHT"); tt:SetItemById(11)
|
||||
tt:SetOwner(a, "ANCHOR_RIGHT"); tt:BenillaSetItemById(11)
|
||||
assert(tt:NumLines() == 1, "req 0: name only, got " .. tt:NumLines())
|
||||
tt:SetOwner(a, "ANCHOR_RIGHT"); tt:SetItemById(12)
|
||||
tt:SetOwner(a, "ANCHOR_RIGHT"); tt:BenillaSetItemById(12)
|
||||
assert(tt:NumLines() == 1, "req 1 hides like req 0, got " .. tt:NumLines())
|
||||
tt:SetOwner(a, "ANCHOR_RIGHT"); tt:SetItemById(13)
|
||||
tt:SetOwner(a, "ANCHOR_RIGHT"); tt:BenillaSetItemById(13)
|
||||
assert(tt:NumLines() == 2, "req 2 prints, got " .. tt:NumLines())
|
||||
assert(TTTextLeft2:GetText() == "[MIN_LEVEL 2]")
|
||||
"#,
|
||||
@@ -515,7 +515,7 @@ fn charter_lines_sit_between_the_name_and_the_signable_line() {
|
||||
);
|
||||
s.run(
|
||||
r#"
|
||||
local a = CreateFrame("Button", "SlotC"); a:SetPoint("CENTER", 0, 0); a:SetSize(10, 10)
|
||||
local a = CreateFrame("Button", "SlotC"); a:SetPoint("CENTER", 0, 0); a:SetWidth(10); a:SetHeight(10)
|
||||
local tt = CreateFrame("GameTooltip", "TT")
|
||||
tt:SetOwner(a, "ANCHOR_RIGHT"); tt:SetBagItem(0, 1)
|
||||
"#,
|
||||
@@ -651,7 +651,7 @@ fn instance_tail_creator_and_readable() {
|
||||
);
|
||||
s.run(
|
||||
r#"
|
||||
local a = CreateFrame("Button", "SlotL"); a:SetPoint("CENTER", 0, 0); a:SetSize(10, 10)
|
||||
local a = CreateFrame("Button", "SlotL"); a:SetPoint("CENTER", 0, 0); a:SetWidth(10); a:SetHeight(10)
|
||||
local tt = CreateFrame("GameTooltip", "TT")
|
||||
tt:SetOwner(a, "ANCHOR_RIGHT"); tt:SetBagItem(0, 1)
|
||||
"#,
|
||||
@@ -770,7 +770,7 @@ fn a_runtime_bound_instance_overrides_the_bind_line_to_soulbound() {
|
||||
);
|
||||
s.run(
|
||||
r#"
|
||||
local a = CreateFrame("Button", "Slot"); a:SetPoint("CENTER", 0, 0); a:SetSize(10, 10)
|
||||
local a = CreateFrame("Button", "Slot"); a:SetPoint("CENTER", 0, 0); a:SetWidth(10); a:SetHeight(10)
|
||||
local tt = CreateFrame("GameTooltip", "TT")
|
||||
tt:SetOwner(Slot, "ANCHOR_RIGHT")
|
||||
tt:SetBagItem(0, 1)
|
||||
@@ -804,7 +804,7 @@ fn a_runtime_bound_instance_overrides_the_bind_line_to_soulbound() {
|
||||
assert_eq!(bind_line(&mut s).0, "[ITEM_BIND_ON_EQUIP]");
|
||||
|
||||
// Control 2: a TEMPLATE hover carries no instance at all, so nothing can override.
|
||||
s.run(r#"TT:SetItemById(871)"#).unwrap();
|
||||
s.run(r#"TT:BenillaSetItemById(871)"#).unwrap();
|
||||
assert_eq!(bind_line(&mut s).0, "[ITEM_BIND_ON_EQUIP]");
|
||||
|
||||
// Control 3: the override's other arm is ITEM_BIND_QUEST — the same text 4|5 already print.
|
||||
|
||||
@@ -55,7 +55,7 @@ fn bag_item_money_law_and_fallback() {
|
||||
s.run(
|
||||
r#"
|
||||
money_fired = nil
|
||||
local a = CreateFrame("Button", "Slot3"); a:SetPoint("CENTER", 0, 0); a:SetSize(10, 10)
|
||||
local a = CreateFrame("Button", "Slot3"); a:SetPoint("CENTER", 0, 0); a:SetWidth(10); a:SetHeight(10)
|
||||
local tt = CreateFrame("GameTooltip", "TT")
|
||||
tt:SetScript("OnTooltipAddMoney", function(self) money_fired = arg1 end)
|
||||
-- No merchant: no money handler fires.
|
||||
@@ -129,10 +129,10 @@ fn wrap_lines_measure_wrapped_in_one_pass_and_survive_the_reenter_loop() {
|
||||
);
|
||||
s.run(
|
||||
r#"
|
||||
local a = CreateFrame("Button", "Slot4"); a:SetPoint("TOPLEFT", 10, -10); a:SetSize(10, 10)
|
||||
local a = CreateFrame("Button", "Slot4"); a:SetPoint("TOPLEFT", 10, -10); a:SetWidth(10); a:SetHeight(10)
|
||||
local tt = CreateFrame("GameTooltip", "TT")
|
||||
tt:SetOwner(a, "ANCHOR_RIGHT")
|
||||
tt:SetItemById(7)
|
||||
tt:BenillaSetItemById(7)
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
@@ -174,7 +174,7 @@ fn wrap_lines_measure_wrapped_in_one_pass_and_survive_the_reenter_loop() {
|
||||
s.run(
|
||||
r#"
|
||||
TT:SetOwner(Slot4, "ANCHOR_RIGHT")
|
||||
TT:SetItemById(7)
|
||||
TT:BenillaSetItemById(7)
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
@@ -219,10 +219,10 @@ fn item_render_hides_the_unit_health_bar() {
|
||||
);
|
||||
s.run(
|
||||
r#"
|
||||
local a = CreateFrame("Button", "Slot6"); a:SetPoint("CENTER", 0, 0); a:SetSize(10, 10)
|
||||
local a = CreateFrame("Button", "Slot6"); a:SetPoint("CENTER", 0, 0); a:SetWidth(10); a:SetHeight(10)
|
||||
local tt = CreateFrame("GameTooltip", "GameTooltip"); tt:Hide()
|
||||
local bar = CreateFrame("StatusBar", "GameTooltipStatusBar", tt)
|
||||
bar:SetPoint("TOPLEFT", tt, "BOTTOMLEFT", 2, -1); bar:SetSize(100, 8)
|
||||
bar:SetPoint("TOPLEFT", tt, "BOTTOMLEFT", 2, -1); bar:SetWidth(100); bar:SetHeight(8)
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
@@ -232,7 +232,7 @@ fn item_render_hides_the_unit_health_bar() {
|
||||
s.run(
|
||||
r#"
|
||||
GameTooltip:SetOwner(Slot6, "ANCHOR_RIGHT")
|
||||
GameTooltip:SetItemById(9)
|
||||
GameTooltip:BenillaSetItemById(9)
|
||||
assert(not GameTooltipStatusBar:IsShown(), "item content hides the unit bar")
|
||||
assert(GameTooltip:IsShown(), "the item tooltip itself shows")
|
||||
"#,
|
||||
@@ -278,7 +278,7 @@ fn real_instance_hover_renders_live_durability() {
|
||||
);
|
||||
s.run(
|
||||
r#"
|
||||
local a = CreateFrame("Button", "Slot"); a:SetPoint("CENTER", 0, 0); a:SetSize(10, 10)
|
||||
local a = CreateFrame("Button", "Slot"); a:SetPoint("CENTER", 0, 0); a:SetWidth(10); a:SetHeight(10)
|
||||
local tt = CreateFrame("GameTooltip", "TT")
|
||||
tt:SetOwner(Slot, "ANCHOR_RIGHT")
|
||||
tt:SetBagItem(0, 2)
|
||||
@@ -289,7 +289,7 @@ fn real_instance_hover_renders_live_durability() {
|
||||
end
|
||||
assert(found == "[DURABILITY 30/40]", "live pair on a bag hover, got " .. tostring(found))
|
||||
-- The template/link hover of the SAME item keeps the authored full pair.
|
||||
tt:SetItemById(2264)
|
||||
tt:BenillaSetItemById(2264)
|
||||
found = nil
|
||||
for i = 1, tt:NumLines() do
|
||||
local t = getglobal("TTTextLeft" .. i):GetText()
|
||||
@@ -339,7 +339,7 @@ fn broken_instance_hover_renders_zero_durability() {
|
||||
);
|
||||
s.run(
|
||||
r#"
|
||||
local a = CreateFrame("Button", "Slot"); a:SetPoint("CENTER", 0, 0); a:SetSize(10, 10)
|
||||
local a = CreateFrame("Button", "Slot"); a:SetPoint("CENTER", 0, 0); a:SetWidth(10); a:SetHeight(10)
|
||||
local tt = CreateFrame("GameTooltip", "TT")
|
||||
tt:SetOwner(Slot, "ANCHOR_RIGHT")
|
||||
tt:SetBagItem(0, 1)
|
||||
@@ -415,7 +415,7 @@ fn an_enchanted_instance_renders_its_enchant_line_before_durability() {
|
||||
);
|
||||
s.run(
|
||||
r#"
|
||||
local a = CreateFrame("Button", "Slot"); a:SetPoint("CENTER", 0, 0); a:SetSize(10, 10)
|
||||
local a = CreateFrame("Button", "Slot"); a:SetPoint("CENTER", 0, 0); a:SetWidth(10); a:SetHeight(10)
|
||||
local tt = CreateFrame("GameTooltip", "TT")
|
||||
tt:SetOwner(Slot, "ANCHOR_RIGHT")
|
||||
tt:SetBagItem(0, 1)
|
||||
@@ -442,7 +442,7 @@ fn an_enchanted_instance_renders_its_enchant_line_before_durability() {
|
||||
);
|
||||
|
||||
// The control: the same item as a TEMPLATE hover has no instance, so no enchant line.
|
||||
s.run(r#"TT:SetItemById(22816)"#).unwrap();
|
||||
s.run(r#"TT:BenillaSetItemById(22816)"#).unwrap();
|
||||
let lines = super::lines_of(&mut s);
|
||||
assert!(
|
||||
!lines.iter().any(|(t, _)| t.starts_with("Agility")),
|
||||
@@ -514,7 +514,7 @@ fn enchant_line_colour_is_per_slot_and_sign() {
|
||||
);
|
||||
s.run(
|
||||
r#"
|
||||
local a = CreateFrame("Button", "Slot"); a:SetPoint("CENTER", 0, 0); a:SetSize(10, 10)
|
||||
local a = CreateFrame("Button", "Slot"); a:SetPoint("CENTER", 0, 0); a:SetWidth(10); a:SetHeight(10)
|
||||
local tt = CreateFrame("GameTooltip", "TT")
|
||||
tt:SetOwner(Slot, "ANCHOR_RIGHT")
|
||||
tt:SetBagItem(0, 1)
|
||||
@@ -597,7 +597,7 @@ fn temporary_enchant_line_carries_its_countdown_and_charges() {
|
||||
);
|
||||
s.run(
|
||||
r#"
|
||||
local a = CreateFrame("Button", "Slot"); a:SetPoint("CENTER", 0, 0); a:SetSize(10, 10)
|
||||
local a = CreateFrame("Button", "Slot"); a:SetPoint("CENTER", 0, 0); a:SetWidth(10); a:SetHeight(10)
|
||||
local tt = CreateFrame("GameTooltip", "TT")
|
||||
tt:SetOwner(Slot, "ANCHOR_RIGHT")
|
||||
tt:SetBagItem(0, 1)
|
||||
@@ -623,7 +623,7 @@ fn temporary_enchant_line_carries_its_countdown_and_charges() {
|
||||
/// `ITEM_RANDOM_ENCHANT` arm is unreachable (§E1's fork). The row's name is the suffix-joined one
|
||||
/// (`0x5d8b00`, the same string `GetLootSlotInfo` returns), so the plate reads "… of the Monkey".
|
||||
///
|
||||
/// This is the reported bug's exact shape: through the template path (`SetItemById`) the same
|
||||
/// This is the reported bug's exact shape: through the template path (`BenillaSetItemById`) the same
|
||||
/// hover printed the ITEM_RANDOM_ENCHANT placeholder until the item reached a bag.
|
||||
#[test]
|
||||
fn a_looted_roll_shows_its_lines_and_never_the_placeholder() {
|
||||
@@ -680,7 +680,7 @@ fn a_looted_roll_shows_its_lines_and_never_the_placeholder() {
|
||||
);
|
||||
s.run(
|
||||
r#"
|
||||
local a = CreateFrame("Button", "Slot"); a:SetPoint("CENTER", 0, 0); a:SetSize(10, 10)
|
||||
local a = CreateFrame("Button", "Slot"); a:SetPoint("CENTER", 0, 0); a:SetWidth(10); a:SetHeight(10)
|
||||
local tt = CreateFrame("GameTooltip", "TT")
|
||||
tt:SetOwner(Slot, "ANCHOR_RIGHT")
|
||||
tt:SetLootItem(1)
|
||||
@@ -738,7 +738,7 @@ fn a_looted_item_with_no_roll_shows_neither_line_nor_placeholder() {
|
||||
}));
|
||||
s.run(
|
||||
r#"
|
||||
local a = CreateFrame("Button", "Slot"); a:SetPoint("CENTER", 0, 0); a:SetSize(10, 10)
|
||||
local a = CreateFrame("Button", "Slot"); a:SetPoint("CENTER", 0, 0); a:SetWidth(10); a:SetHeight(10)
|
||||
local tt = CreateFrame("GameTooltip", "TT")
|
||||
tt:SetOwner(Slot, "ANCHOR_RIGHT")
|
||||
tt:SetLootItem(1)
|
||||
@@ -790,7 +790,7 @@ fn a_linked_roll_shows_its_lines_and_never_the_placeholder() {
|
||||
);
|
||||
s.run(
|
||||
r#"
|
||||
local a = CreateFrame("Button", "Slot"); a:SetPoint("CENTER", 0, 0); a:SetSize(10, 10)
|
||||
local a = CreateFrame("Button", "Slot"); a:SetPoint("CENTER", 0, 0); a:SetWidth(10); a:SetHeight(10)
|
||||
local tt = CreateFrame("GameTooltip", "TT")
|
||||
tt:SetOwner(Slot, "ANCHOR_RIGHT")
|
||||
tt:SetHyperlink("|cff1eff00|Hitem:8888:0:584:0|h[Bloodrazor of the Monkey]|h|r")
|
||||
@@ -835,10 +835,10 @@ fn random_property_template_hover_shows_the_placeholder() {
|
||||
);
|
||||
s.run(
|
||||
r#"
|
||||
local a = CreateFrame("Button", "Slot"); a:SetPoint("CENTER", 0, 0); a:SetSize(10, 10)
|
||||
local a = CreateFrame("Button", "Slot"); a:SetPoint("CENTER", 0, 0); a:SetWidth(10); a:SetHeight(10)
|
||||
local tt = CreateFrame("GameTooltip", "TT")
|
||||
tt:SetOwner(Slot, "ANCHOR_RIGHT")
|
||||
tt:SetItemById(8888)
|
||||
tt:BenillaSetItemById(8888)
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
@@ -60,10 +60,10 @@ fn item_set_block_counts_and_colors() {
|
||||
s.set_item_set(161, set_view.clone());
|
||||
s.run(
|
||||
r#"
|
||||
local a = CreateFrame("Button", "Slot10"); a:SetPoint("CENTER", 0, 0); a:SetSize(10, 10)
|
||||
local a = CreateFrame("Button", "Slot10"); a:SetPoint("CENTER", 0, 0); a:SetWidth(10); a:SetHeight(10)
|
||||
local tt = CreateFrame("GameTooltip", "TT")
|
||||
tt:SetOwner(a, "ANCHOR_RIGHT")
|
||||
tt:SetItemById(6303)
|
||||
tt:BenillaSetItemById(6303)
|
||||
-- The spacer is the reference's own literal `0x854b2c` — a space and a newline, which is
|
||||
-- ONE row carrying one space, not the empty string (an empty AddLine adds no line at all
|
||||
-- in the reference: `0x530270` bails before its `inc [esi+0x31c]`).
|
||||
@@ -129,7 +129,7 @@ fn item_set_block_counts_and_colors() {
|
||||
s.set_item_set(161, gated);
|
||||
s.run(
|
||||
r#"
|
||||
TT:SetOwner(Slot10, "ANCHOR_RIGHT"); TT:SetItemById(6303)
|
||||
TT:SetOwner(Slot10, "ANCHOR_RIGHT"); TT:BenillaSetItemById(6303)
|
||||
assert(TTTextLeft5:GetText() == "[MIN_SKILL Leatherworking 250]",
|
||||
"the skill line sits between header and members")
|
||||
assert(TTTextLeft6:GetText() == " Defias Mark")
|
||||
@@ -150,7 +150,7 @@ fn item_set_block_counts_and_colors() {
|
||||
let mut req = PlayerReqState::default();
|
||||
req.skills.insert(165, 300);
|
||||
s.set_player_req_state(req);
|
||||
s.run(r#"TT:SetOwner(Slot10, "ANCHOR_RIGHT"); TT:SetItemById(6303)"#)
|
||||
s.run(r#"TT:SetOwner(Slot10, "ANCHOR_RIGHT"); TT:BenillaSetItemById(6303)"#)
|
||||
.unwrap();
|
||||
let lines = lines_of(&mut s);
|
||||
assert_eq!(
|
||||
@@ -171,7 +171,7 @@ fn item_set_block_counts_and_colors() {
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
s.run(r#"TT:SetOwner(Slot10, "ANCHOR_RIGHT"); TT:SetItemById(9999)"#)
|
||||
s.run(r#"TT:SetOwner(Slot10, "ANCHOR_RIGHT"); TT:BenillaSetItemById(9999)"#)
|
||||
.unwrap();
|
||||
assert_eq!(s.take_item_set_asks(), vec![1], "the set ask recorded");
|
||||
assert!(s.take_errors().is_empty());
|
||||
@@ -206,7 +206,7 @@ fn quest_item_hovers_render_the_rows_item_by_id() {
|
||||
}));
|
||||
s.run(
|
||||
r#"
|
||||
local a = CreateFrame("Button", "Q1"); a:SetPoint("CENTER", 0, 0); a:SetSize(10, 10)
|
||||
local a = CreateFrame("Button", "Q1"); a:SetPoint("CENTER", 0, 0); a:SetWidth(10); a:SetHeight(10)
|
||||
local tt = CreateFrame("GameTooltip", "TT")
|
||||
tt:SetOwner(a, "ANCHOR_RIGHT"); tt:SetQuestItem("choice", 1)
|
||||
"#,
|
||||
|
||||
@@ -36,7 +36,7 @@ fn spellbook_hover_renders_the_verified_shape() {
|
||||
});
|
||||
s.run(
|
||||
r#"
|
||||
local a = CreateFrame("Button", "SB1"); a:SetPoint("CENTER", 0, 0); a:SetSize(10, 10)
|
||||
local a = CreateFrame("Button", "SB1"); a:SetPoint("CENTER", 0, 0); a:SetWidth(10); a:SetHeight(10)
|
||||
local tt = CreateFrame("GameTooltip", "TT")
|
||||
tt:SetOwner(a, "ANCHOR_RIGHT")
|
||||
tt:SetSpell(1, "spell")
|
||||
@@ -102,7 +102,7 @@ fn a_pet_book_hover_reads_the_pets_book_not_the_players() {
|
||||
});
|
||||
s.run(
|
||||
r#"
|
||||
local a = CreateFrame("Button", "SB1"); a:SetPoint("CENTER", 0, 0); a:SetSize(10, 10)
|
||||
local a = CreateFrame("Button", "SB1"); a:SetPoint("CENTER", 0, 0); a:SetWidth(10); a:SetHeight(10)
|
||||
local tt = CreateFrame("GameTooltip", "TT")
|
||||
tt:SetOwner(a, "ANCHOR_RIGHT")
|
||||
tt:SetSpell(1, "pet")
|
||||
@@ -152,7 +152,7 @@ fn tracking_hover_renders_gold_name_over_white_description() {
|
||||
);
|
||||
s.run(
|
||||
r#"
|
||||
local f = CreateFrame("Frame", "TRK"); f:SetPoint("CENTER", 0, 0); f:SetSize(10, 10)
|
||||
local f = CreateFrame("Frame", "TRK"); f:SetPoint("CENTER", 0, 0); f:SetWidth(10); f:SetHeight(10)
|
||||
local tt = CreateFrame("GameTooltip", "TT")
|
||||
tt:SetOwner(f, "ANCHOR_BOTTOMLEFT")
|
||||
tt:SetTrackingSpell()
|
||||
@@ -206,7 +206,7 @@ fn passive_omits_the_casttime_line() {
|
||||
});
|
||||
s.run(
|
||||
r#"
|
||||
local a = CreateFrame("Button", "SB2"); a:SetPoint("CENTER", 0, 0); a:SetSize(10, 10)
|
||||
local a = CreateFrame("Button", "SB2"); a:SetPoint("CENTER", 0, 0); a:SetWidth(10); a:SetHeight(10)
|
||||
local tt = CreateFrame("GameTooltip", "TT")
|
||||
tt:SetOwner(a, "ANCHOR_RIGHT")
|
||||
tt:SetSpell(1, "spell")
|
||||
@@ -260,7 +260,7 @@ fn player_buff_hover_is_the_aura_variant() {
|
||||
-- the engine reads the string table at all, not what the strings say.
|
||||
SPELL_TIME_REMAINING_MIN = "<%d m>"
|
||||
SPELL_TIME_REMAINING_MIN_P1 = "<%d mm>"
|
||||
local a = CreateFrame("Button", "BF1"); a:SetPoint("CENTER", 0, 0); a:SetSize(10, 10)
|
||||
local a = CreateFrame("Button", "BF1"); a:SetPoint("CENTER", 0, 0); a:SetWidth(10); a:SetHeight(10)
|
||||
local tt = CreateFrame("GameTooltip", "TT")
|
||||
tt:SetOwner(a, "ANCHOR_RIGHT")
|
||||
-- 0, not 1: SetPlayerBuff takes a 1.12 CACHE POSITION (see `script::aura`'s header).
|
||||
@@ -329,7 +329,7 @@ fn player_buff_hover_names_the_dispel_class_in_gold() {
|
||||
);
|
||||
s.run(
|
||||
r#"
|
||||
local a = CreateFrame("Button", "BF1"); a:SetPoint("CENTER", 0, 0); a:SetSize(10, 10)
|
||||
local a = CreateFrame("Button", "BF1"); a:SetPoint("CENTER", 0, 0); a:SetWidth(10); a:SetHeight(10)
|
||||
local tt = CreateFrame("GameTooltip", "TT")
|
||||
tt:SetOwner(a, "ANCHOR_RIGHT")
|
||||
tt:SetPlayerBuff(0)
|
||||
@@ -380,7 +380,7 @@ fn requirement_and_reagent_lines_render_in_law_order() {
|
||||
});
|
||||
s.run(
|
||||
r#"
|
||||
local a = CreateFrame("Button", "SB2"); a:SetPoint("CENTER", 0, 0); a:SetSize(10, 10)
|
||||
local a = CreateFrame("Button", "SB2"); a:SetPoint("CENTER", 0, 0); a:SetWidth(10); a:SetHeight(10)
|
||||
local tt = CreateFrame("GameTooltip", "TT")
|
||||
tt:SetOwner(a, "ANCHOR_RIGHT")
|
||||
tt:SetSpell(1, "spell")
|
||||
@@ -458,7 +458,7 @@ fn unit_buff_and_debuff_hover_render_the_aura_variant_without_remaining() {
|
||||
);
|
||||
s.run(
|
||||
r#"
|
||||
local a = CreateFrame("Button", "TF1"); a:SetPoint("CENTER", 0, 0); a:SetSize(10, 10)
|
||||
local a = CreateFrame("Button", "TF1"); a:SetPoint("CENTER", 0, 0); a:SetWidth(10); a:SetHeight(10)
|
||||
local tt = CreateFrame("GameTooltip", "TT")
|
||||
tt:SetOwner(a, "ANCHOR_BOTTOMRIGHT", 15, -25)
|
||||
tt:SetUnitDebuff("target", 1)
|
||||
@@ -535,7 +535,7 @@ fn player_buff_hover_indexes_the_cache_position_not_a_filtered_ordinal() {
|
||||
);
|
||||
s.run(
|
||||
r#"
|
||||
local a = CreateFrame("Button", "BF1"); a:SetPoint("CENTER", 0, 0); a:SetSize(10, 10)
|
||||
local a = CreateFrame("Button", "BF1"); a:SetPoint("CENTER", 0, 0); a:SetWidth(10); a:SetHeight(10)
|
||||
local tt = CreateFrame("GameTooltip", "TT")
|
||||
local function at(i, extra)
|
||||
tt:SetOwner(a, "ANCHOR_RIGHT")
|
||||
@@ -587,7 +587,7 @@ fn action_hover_delegates_by_kind() {
|
||||
);
|
||||
s.run(
|
||||
r#"
|
||||
local a = CreateFrame("Button", "AB1"); a:SetPoint("CENTER", 0, 0); a:SetSize(10, 10)
|
||||
local a = CreateFrame("Button", "AB1"); a:SetPoint("CENTER", 0, 0); a:SetWidth(10); a:SetHeight(10)
|
||||
local tt = CreateFrame("GameTooltip", "TT")
|
||||
tt:SetOwner(a, "ANCHOR_RIGHT")
|
||||
tt:SetAction(24)
|
||||
@@ -638,7 +638,7 @@ fn action_hover_on_a_macro_slot_shows_its_name_in_white() {
|
||||
);
|
||||
s.run(
|
||||
r#"
|
||||
local a = CreateFrame("Button", "AB1"); a:SetPoint("CENTER", 0, 0); a:SetSize(10, 10)
|
||||
local a = CreateFrame("Button", "AB1"); a:SetPoint("CENTER", 0, 0); a:SetWidth(10); a:SetHeight(10)
|
||||
local tt = CreateFrame("GameTooltip", "TT")
|
||||
tt:SetOwner(a, "ANCHOR_RIGHT")
|
||||
tt:SetAction(24)
|
||||
@@ -739,7 +739,7 @@ fn set_trainer_service_selects_the_builder_and_never_renders_its_own_line() {
|
||||
|
||||
s.run(
|
||||
r#"
|
||||
local a = CreateFrame("Button", "AB1"); a:SetPoint("CENTER", 0, 0); a:SetSize(10, 10)
|
||||
local a = CreateFrame("Button", "AB1"); a:SetPoint("CENTER", 0, 0); a:SetWidth(10); a:SetHeight(10)
|
||||
CreateFrame("GameTooltip", "TT")
|
||||
|
||||
-- Row 1 is the "Arms" header: a no-op, not a shifted hit onto a neighbouring service.
|
||||
@@ -855,7 +855,7 @@ fn set_craft_spell_selects_the_builder_like_the_trainer_hover_does() {
|
||||
|
||||
s.run(
|
||||
r#"
|
||||
local a = CreateFrame("Button", "AB2"); a:SetPoint("CENTER", 0, 0); a:SetSize(10, 10)
|
||||
local a = CreateFrame("Button", "AB2"); a:SetPoint("CENTER", 0, 0); a:SetWidth(10); a:SetHeight(10)
|
||||
CreateFrame("GameTooltip", "TT")
|
||||
TT:SetOwner(a, "ANCHOR_RIGHT")
|
||||
TT:SetCraftSpell(1)
|
||||
@@ -914,7 +914,7 @@ fn the_remaining_line_is_gated_on_until_cancelled_not_on_the_duration() {
|
||||
SPELL_TIME_REMAINING_SEC = "<%d s>"
|
||||
SPELL_TIME_REMAINING_SEC_P1 = "<%d ss>"
|
||||
BENILLA_ANCHOR = CreateFrame("Button", "BF2")
|
||||
BENILLA_ANCHOR:SetPoint("CENTER", 0, 0); BENILLA_ANCHOR:SetSize(10, 10)
|
||||
BENILLA_ANCHOR:SetPoint("CENTER", 0, 0); BENILLA_ANCHOR:SetWidth(10); BENILLA_ANCHOR:SetHeight(10)
|
||||
BENILLA_TIP = CreateFrame("GameTooltip", "TT2")
|
||||
"#,
|
||||
)
|
||||
@@ -1003,7 +1003,7 @@ fn quest_reward_spell_getters_and_hovers() {
|
||||
s.set_spell_tooltip(133, fireball());
|
||||
s.run(
|
||||
r#"
|
||||
local a = CreateFrame("Button", "Q1"); a:SetPoint("CENTER", 0, 0); a:SetSize(10, 10)
|
||||
local a = CreateFrame("Button", "Q1"); a:SetPoint("CENTER", 0, 0); a:SetWidth(10); a:SetHeight(10)
|
||||
local tt = CreateFrame("GameTooltip", "TT")
|
||||
"#,
|
||||
)
|
||||
|
||||
@@ -61,7 +61,7 @@ fn creature_line_law() {
|
||||
});
|
||||
s.run(
|
||||
r#"
|
||||
local a = CreateFrame("Button", "UF1"); a:SetPoint("CENTER", 0, 0); a:SetSize(10, 10)
|
||||
local a = CreateFrame("Button", "UF1"); a:SetPoint("CENTER", 0, 0); a:SetWidth(10); a:SetHeight(10)
|
||||
local tt = CreateFrame("GameTooltip", "TT")
|
||||
tt:SetOwner(a, "ANCHOR_RIGHT")
|
||||
assert(tt:SetUnit("target") == 1, "SetUnit returns 1 on a live unit")
|
||||
@@ -130,7 +130,7 @@ fn a_pending_name_titles_unknownobject_and_the_answer_replaces_it() {
|
||||
s.run(
|
||||
r#"
|
||||
UNKNOWNOBJECT = "Unknown" -- GlobalStrings.lua:4444, enUS
|
||||
local a = CreateFrame("Button", "UF1"); a:SetPoint("CENTER", 0, 0); a:SetSize(10, 10)
|
||||
local a = CreateFrame("Button", "UF1"); a:SetPoint("CENTER", 0, 0); a:SetWidth(10); a:SetHeight(10)
|
||||
local tt = CreateFrame("GameTooltip", "TT")
|
||||
tt:SetOwner(a, "ANCHOR_RIGHT")
|
||||
assert(tt:SetUnit("target") == 1, "a resolved unit shows, name or no name")
|
||||
@@ -241,7 +241,7 @@ fn faction_line_and_civilian_gate() {
|
||||
);
|
||||
s.run(
|
||||
r#"
|
||||
local a = CreateFrame("Button", "UF9"); a:SetPoint("CENTER", 0, 0); a:SetSize(10, 10)
|
||||
local a = CreateFrame("Button", "UF9"); a:SetPoint("CENTER", 0, 0); a:SetWidth(10); a:SetHeight(10)
|
||||
local tt = CreateFrame("GameTooltip", "TT")
|
||||
tt:SetOwner(a, "ANCHOR_RIGHT")
|
||||
tt:SetUnit("target")
|
||||
@@ -329,7 +329,7 @@ fn level_line_variants() {
|
||||
);
|
||||
s.run(
|
||||
r#"
|
||||
local a = CreateFrame("Button", "UF2"); a:SetPoint("CENTER", 0, 0); a:SetSize(10, 10)
|
||||
local a = CreateFrame("Button", "UF2"); a:SetPoint("CENTER", 0, 0); a:SetWidth(10); a:SetHeight(10)
|
||||
local tt = CreateFrame("GameTooltip", "TT")
|
||||
tt:SetOwner(a, "ANCHOR_RIGHT")
|
||||
tt:SetUnit("target")
|
||||
@@ -450,7 +450,7 @@ fn world_hover_drive_and_health_watcher() {
|
||||
getglobal("GameTooltipTextLeft1"):SetTextColor(1, 0, 0)
|
||||
end)
|
||||
local bar = CreateFrame("StatusBar", "GameTooltipStatusBar", tt)
|
||||
bar:SetPoint("TOPLEFT", tt, "BOTTOMLEFT", 2, -1); bar:SetSize(100, 8)
|
||||
bar:SetPoint("TOPLEFT", tt, "BOTTOMLEFT", 2, -1); bar:SetWidth(100); bar:SetHeight(8)
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
@@ -512,7 +512,7 @@ fn minimap_blip_tooltip_shows_and_fades() {
|
||||
s.run(
|
||||
r#"
|
||||
local up = CreateFrame("Frame", "UIParent")
|
||||
up:SetPoint("BOTTOMLEFT"); up:SetSize(800, 600)
|
||||
up:SetPoint("BOTTOMLEFT"); up:SetWidth(800); up:SetHeight(600)
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
@@ -42,13 +42,13 @@ fn board_and_dialog() -> UiScript {
|
||||
r#"
|
||||
Board = CreateFrame("Frame", "Board")
|
||||
Board:SetPoint("BOTTOMLEFT", 100, 100)
|
||||
Board:SetSize(300, 300)
|
||||
Board:SetWidth(300); Board:SetHeight(300)
|
||||
Board:SetFrameLevel(5)
|
||||
Board:CreateTexture(nil, "ARTWORK"):SetTexture("Board.blp")
|
||||
|
||||
Dialog = CreateFrame("Frame", "Dialog")
|
||||
Dialog:SetPoint("BOTTOMLEFT", 200, 200) -- overlaps Board's top-right quadrant
|
||||
Dialog:SetSize(300, 300)
|
||||
Dialog:SetWidth(300); Dialog:SetHeight(300)
|
||||
Dialog:CreateTexture(nil, "ARTWORK"):SetTexture("Dialog.blp")
|
||||
Dialog:Hide()
|
||||
"#,
|
||||
@@ -200,7 +200,7 @@ fn the_trigger_is_the_effective_visibility_transition_not_the_show_call() {
|
||||
r#"
|
||||
Holder = CreateFrame("Frame", "Holder")
|
||||
Holder:SetPoint("BOTTOMLEFT", 200, 200)
|
||||
Holder:SetSize(300, 300)
|
||||
Holder:SetWidth(300); Holder:SetHeight(300)
|
||||
Holder:Hide()
|
||||
Dialog:SetParent(Holder)
|
||||
Dialog:SetToplevel(true)
|
||||
@@ -239,7 +239,7 @@ fn a_raise_on_a_frame_that_overlaps_nothing_is_a_total_no_op() {
|
||||
r#"
|
||||
Dialog:ClearAllPoints()
|
||||
Dialog:SetPoint("BOTTOMLEFT", 600, 450) -- clear of Board's (100,100)-(400,400)
|
||||
Dialog:SetSize(100, 100)
|
||||
Dialog:SetWidth(100); Dialog:SetHeight(100)
|
||||
Dialog:SetToplevel(true)
|
||||
Dialog:Show()
|
||||
"#,
|
||||
@@ -311,20 +311,20 @@ fn a_raise_can_never_lift_a_frame_out_of_its_stratum() {
|
||||
r#"
|
||||
Med = CreateFrame("Frame", "Med")
|
||||
Med:SetPoint("BOTTOMLEFT", 100, 100)
|
||||
Med:SetSize(300, 300)
|
||||
Med:SetWidth(300); Med:SetHeight(300)
|
||||
Med:CreateTexture(nil, "ARTWORK"):SetTexture("Med.blp")
|
||||
|
||||
LowOther = CreateFrame("Frame", "LowOther")
|
||||
LowOther:SetFrameStrata("LOW")
|
||||
LowOther:SetPoint("BOTTOMLEFT", 100, 100)
|
||||
LowOther:SetSize(300, 300)
|
||||
LowOther:SetWidth(300); LowOther:SetHeight(300)
|
||||
LowOther:SetFrameLevel(4)
|
||||
LowOther:CreateTexture(nil, "ARTWORK"):SetTexture("LowOther.blp")
|
||||
|
||||
LowTop = CreateFrame("Frame", "LowTop")
|
||||
LowTop:SetFrameStrata("LOW")
|
||||
LowTop:SetPoint("BOTTOMLEFT", 150, 150)
|
||||
LowTop:SetSize(300, 300)
|
||||
LowTop:SetWidth(300); LowTop:SetHeight(300)
|
||||
LowTop:SetToplevel(true)
|
||||
LowTop:CreateTexture(nil, "ARTWORK"):SetTexture("LowTop.blp")
|
||||
LowTop:Hide()
|
||||
@@ -408,11 +408,11 @@ fn compaction_bounds_the_raise_across_repeated_shows() {
|
||||
r#"
|
||||
A = CreateFrame("Frame", "A")
|
||||
A:SetPoint("BOTTOMLEFT", 100, 100)
|
||||
A:SetSize(300, 300)
|
||||
A:SetWidth(300); A:SetHeight(300)
|
||||
A:SetToplevel(true)
|
||||
B = CreateFrame("Frame", "B")
|
||||
B:SetPoint("BOTTOMLEFT", 200, 200)
|
||||
B:SetSize(300, 300)
|
||||
B:SetWidth(300); B:SetHeight(300)
|
||||
B:SetToplevel(true)
|
||||
"#,
|
||||
)
|
||||
@@ -451,7 +451,7 @@ fn lua_raise_acts_on_the_nearest_toplevel_ancestor_and_is_silent_without_one() {
|
||||
Kid = CreateFrame("Frame", "Kid", Dialog)
|
||||
Loose = CreateFrame("Frame", "Loose")
|
||||
Loose:SetPoint("BOTTOMLEFT", 200, 200)
|
||||
Loose:SetSize(300, 300)
|
||||
Loose:SetWidth(300); Loose:SetHeight(300)
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
@@ -575,7 +575,7 @@ fn pressing_a_child_of_a_toplevel_window_raises_the_window() {
|
||||
Dialog:Show()
|
||||
Knob = CreateFrame("Frame", "Knob", Dialog)
|
||||
Knob:SetPoint("CENTER", Dialog, "CENTER")
|
||||
Knob:SetSize(40, 40)
|
||||
Knob:SetWidth(40); Knob:SetHeight(40)
|
||||
Knob:EnableMouse(true)
|
||||
Board:SetFrameLevel(9)
|
||||
Dialog:SetFrameLevel(0)
|
||||
@@ -643,12 +643,12 @@ fn a_chorded_press_raises_the_held_frame_not_the_one_under_the_cursor() {
|
||||
-- gate is occlusion, so a frame overlapping nothing would decline whatever we press.
|
||||
Other = CreateFrame("Frame", "Other")
|
||||
Other:SetPoint("BOTTOMLEFT", 500, 100)
|
||||
Other:SetSize(200, 200)
|
||||
Other:SetWidth(200); Other:SetHeight(200)
|
||||
Other:EnableMouse(true)
|
||||
Other:SetToplevel(true)
|
||||
Tile = CreateFrame("Frame", "Tile")
|
||||
Tile:SetPoint("BOTTOMLEFT", 660, 260) -- clips Other's far corner only
|
||||
Tile:SetSize(60, 60)
|
||||
Tile:SetWidth(60); Tile:SetHeight(60)
|
||||
Tile:SetFrameLevel(9)
|
||||
|
||||
Board:SetFrameLevel(9)
|
||||
@@ -725,7 +725,7 @@ fn a_raise_with_a_hidden_child_keeps_the_windows_own_siblings_level() {
|
||||
Other:SetFrameStrata("DIALOG")
|
||||
Other:SetFrameLevel(1)
|
||||
Other:SetPoint("BOTTOMLEFT", 50, 50)
|
||||
Other:SetSize(400, 400)
|
||||
Other:SetWidth(400); Other:SetHeight(400)
|
||||
Other:EnableMouse(true)
|
||||
Other:SetToplevel(true)
|
||||
OtherKid = CreateFrame("Frame", "OtherKid", Other) -- level 2
|
||||
@@ -734,13 +734,13 @@ fn a_raise_with_a_hidden_child_keeps_the_windows_own_siblings_level() {
|
||||
Win:SetFrameStrata("DIALOG")
|
||||
Win:SetFrameLevel(1)
|
||||
Win:SetPoint("BOTTOMLEFT", 200, 100)
|
||||
Win:SetSize(400, 400)
|
||||
Win:SetWidth(400); Win:SetHeight(400)
|
||||
Win:EnableMouse(true)
|
||||
Win:SetToplevel(true)
|
||||
|
||||
Close = CreateFrame("Button", "Close", Win) -- level 2, visible with Win
|
||||
Close:SetPoint("BOTTOMRIGHT", Win, "BOTTOMRIGHT", -15, 15)
|
||||
Close:SetSize(100, 21)
|
||||
Close:SetWidth(100); Close:SetHeight(21)
|
||||
Close:EnableMouse(true)
|
||||
Close:SetScript("OnEnter", function(self) hovered = self:GetName() end)
|
||||
Close:SetScript("OnClick", function(self) clicked = self:GetName() end)
|
||||
|
||||
@@ -0,0 +1,404 @@
|
||||
//! **The widget-method surface gate** — the sibling of [`super::reference_surface`], for the API
|
||||
//! an addon reaches through a *widget* rather than through `_G` (decision 2142).
|
||||
//!
|
||||
//! `our_globals_stay_inside_the_1_12_surface` has guarded the global namespace since 1189, and the
|
||||
//! shape gate (1842/1843/2118) guards the arity and return kinds of the methods we *have*. Between
|
||||
//! them sat the question neither asks: **is the set of method NAMES on each widget class the set
|
||||
//! 1.12 registers?** A method the reference has and we do not is invisible to the shape gate,
|
||||
//! which skips what does not exist; a method we have and the reference does not is invisible to
|
||||
//! everything, and is the superset `api-coverage.sh`'s header calls "not free" — an addon that
|
||||
//! feature-detects (`if region.GetBlendMode then`) takes a path we cannot honour, and the failure
|
||||
//! surfaces far from the cause.
|
||||
//!
|
||||
//! A corpus census over 110 top-20 and 219 vanilla addons found both directions live at once
|
||||
//! (2142): `ShaguTweaks` probes `GetBlendMode`, which we were missing, and silently recolours
|
||||
//! textures the reference leaves alone; `pfUI` and `_Nameplates` call `GameTooltip:GetAnchorType`
|
||||
//! bare and raise. The demand is in the corpus, which is not in this repo — so this gate does not
|
||||
//! measure demand. It measures the SURFACE, and every entry in the two declared lists below is
|
||||
//! where the reading of the demand got written down.
|
||||
//!
|
||||
//! ## The reference side is a CHAIN, not 23 flat surfaces
|
||||
//!
|
||||
//! The reference's 23 widget registrar tables are mutually **disjoint** — a class's real surface is
|
||||
//! its own table plus the tables it misses into. Comparing a live `Frame` against `0x878ec0` alone
|
||||
//! reports 87 of its 88 methods as beyond-1.12, which is how you get a gate nobody believes.
|
||||
//! [`WIDGET_CHAINS`] is that inheritance, and its two non-obvious rows are byte-settled, not
|
||||
//! guessed: `0x87c9b8` is the base `Region` map (its 19 names are exactly [`super::super`]'s
|
||||
//! `REGION_MAP_METHODS`), and `0x847ce4` is `LootButton`, whose whole surface of its own is
|
||||
//! `SetSlot` (decision 1799, read off the registrar's own `mov edx,1`).
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
use crate::script::{widget_method_census, UiScript};
|
||||
|
||||
/// `(class, its own registrar table, the tables it inherits)`.
|
||||
///
|
||||
/// `WorldFrame` is a `Frame` to Lua (1983/1984) and `TitleRegion` a bare `Region`, so neither has a
|
||||
/// table of its own among the 23. `TaxiRouteFrame` is absent for the same reason (1828).
|
||||
const WIDGET_CHAINS: &[(&str, &str, &[&str])] = &[
|
||||
("Frame", FRAME, &[REGION]),
|
||||
("WorldFrame", FRAME, &[REGION]),
|
||||
("Button", BUTTON, FRAME_CHAIN),
|
||||
("LootButton", "0x847ce4", &[BUTTON, FRAME, REGION]),
|
||||
("CheckButton", "0x87bf74", &[BUTTON, FRAME, REGION]),
|
||||
("EditBox", "0x87bb68", FRAME_CHAIN),
|
||||
("StatusBar", "0x87b010", FRAME_CHAIN),
|
||||
("Slider", "0x87b260", FRAME_CHAIN),
|
||||
("ScrollFrame", "0x87b3c0", FRAME_CHAIN),
|
||||
("Model", MODEL, FRAME_CHAIN),
|
||||
("PlayerModel", PLAYER_MODEL, &[MODEL, FRAME, REGION]),
|
||||
(
|
||||
"DressUpModel",
|
||||
"0x84f190",
|
||||
&[PLAYER_MODEL, MODEL, FRAME, REGION],
|
||||
),
|
||||
(
|
||||
"TabardModel",
|
||||
"0x84ee40",
|
||||
&[PLAYER_MODEL, MODEL, FRAME, REGION],
|
||||
),
|
||||
("MessageFrame", "0x87b960", FRAME_CHAIN),
|
||||
("ScrollingMessageFrame", "0x87b5c0", FRAME_CHAIN),
|
||||
("ColorSelect", "0x87abb0", FRAME_CHAIN),
|
||||
("SimpleHTML", "0x87ba80", FRAME_CHAIN),
|
||||
("MovieFrame", "0x87ab4c", FRAME_CHAIN),
|
||||
("GameTooltip", "0x854198", FRAME_CHAIN),
|
||||
("Minimap", "0x84c538", FRAME_CHAIN),
|
||||
("Texture", "0x87c128", &[REGION]),
|
||||
("FontString", "0x87c1d8", &[REGION]),
|
||||
// The font OBJECT (`CreateFont`) carries its own `GetName`/`GetObjectType`/`IsObjectType` and
|
||||
// inherits nothing.
|
||||
("Font", "0x87c7c8", &[]),
|
||||
("TitleRegion", REGION, &[]),
|
||||
];
|
||||
const REGION: &str = "0x87c9b8";
|
||||
const FRAME: &str = "0x878ec0";
|
||||
const BUTTON: &str = "0x879d00";
|
||||
const MODEL: &str = "0x878948";
|
||||
const PLAYER_MODEL: &str = "0x84f1fc";
|
||||
const FRAME_CHAIN: &[&str] = &[FRAME, REGION];
|
||||
|
||||
/// Methods the reference registers that this client does not answer — **a list that may only
|
||||
/// shrink**, one row per name, naming every class it is missing on and why it has not been built.
|
||||
///
|
||||
/// The corpus demand behind each is 2142's census. A name with real, live callers does not belong
|
||||
/// here: it belongs implemented.
|
||||
const NOT_YET_ANSWERED: &[(&str, &str, &str)] = &[
|
||||
// ── The MovieFrame trio: zero addon demand, and the stock GLUE screen calls all three ──────
|
||||
// `_extracted_gluexml/MovieFrame.lua:23/30/35/47/71`. The glue screens stay ours permanently
|
||||
// (0068 §8), so the intro-movie screen is owed these however little the ecosystem wants them —
|
||||
// which makes this the one group here that is a real backlog item rather than a name nobody
|
||||
// has ever typed. benilla has no movie playback at all, so it is a feature, not a verb.
|
||||
(
|
||||
"StartMovie",
|
||||
"MovieFrame",
|
||||
"the intro-movie screen's own verb; benilla plays no movie yet",
|
||||
),
|
||||
("StopMovie", "MovieFrame", "the same, on the skip path"),
|
||||
(
|
||||
"EnableSubtitles",
|
||||
"MovieFrame",
|
||||
"the same — `MovieFrame.lua:35` passes `GetMovieSubtitles()`",
|
||||
),
|
||||
// ── The Minimap's four content setters ────────────────────────────────────────────────────
|
||||
// benilla draws the minimap's blips, arrow and mask from the app side (`minimap/blips`), never
|
||||
// through Lua; the stock UI does not call these either. Wiring them means routing the art
|
||||
// choice back through the VM, which nothing has asked for.
|
||||
(
|
||||
"SetArrowModel",
|
||||
"Minimap",
|
||||
"app-side art; no caller in the stock UI or either corpus",
|
||||
),
|
||||
("SetBlipTexture", "Minimap", "the same"),
|
||||
("SetIconTexture", "Minimap", "the same"),
|
||||
("SetPlayerModel", "Minimap", "the same"),
|
||||
// ── The Button font-object and text-colour family ─────────────────────────────────────────
|
||||
// Seven getters and one setter over a button's disabled/highlight/pushed font state. benilla
|
||||
// keeps that state (`button.rs`'s `Slot` triple) but publishes none of it back. **Zero call
|
||||
// sites in 329 addon folders and zero in the stock UI** — the whole family is completeness.
|
||||
(
|
||||
"GetDisabledFontObject",
|
||||
"Button CheckButton LootButton",
|
||||
"the button font-state family; no caller anywhere measured",
|
||||
),
|
||||
(
|
||||
"GetDisabledTextColor",
|
||||
"Button CheckButton LootButton",
|
||||
"the same family",
|
||||
),
|
||||
(
|
||||
"GetHighlightFontObject",
|
||||
"Button CheckButton LootButton",
|
||||
"the same family",
|
||||
),
|
||||
(
|
||||
"GetHighlightTextColor",
|
||||
"Button CheckButton LootButton",
|
||||
"the same family",
|
||||
),
|
||||
(
|
||||
"GetTextFontObject",
|
||||
"Button CheckButton LootButton",
|
||||
"the same family",
|
||||
),
|
||||
(
|
||||
"GetPushedTextOffset",
|
||||
"Button CheckButton LootButton",
|
||||
"the same family",
|
||||
),
|
||||
(
|
||||
"SetPushedTextOffset",
|
||||
"Button CheckButton LootButton",
|
||||
"the same family",
|
||||
),
|
||||
// ── Line spacing, on the five classes that carry text ─────────────────────────────────────
|
||||
// A real layout property benilla's text layout does not model — a setter here is not a store,
|
||||
// it is a change to how `ui_text` lays out wrapped lines. No caller anywhere measured.
|
||||
(
|
||||
"GetSpacing",
|
||||
"EditBox Font FontString MessageFrame ScrollingMessageFrame",
|
||||
"line spacing is not modelled by `ui_text`'s layout; no caller anywhere measured",
|
||||
),
|
||||
(
|
||||
"SetSpacing",
|
||||
"EditBox Font FontString MessageFrame ScrollingMessageFrame",
|
||||
"the same — the setter is a layout change, not a stored value",
|
||||
),
|
||||
// ── EditBox mode read-backs ───────────────────────────────────────────────────────────────
|
||||
// The four modes are all SET here (`editbox`'s `SetAutoFocus`/`SetMultiLine`/`SetNumeric`/
|
||||
// `SetPassword`) and none is readable. Cheap, and nothing has ever asked.
|
||||
(
|
||||
"IsAutoFocus",
|
||||
"EditBox",
|
||||
"the mode is set and not readable back; no caller anywhere measured",
|
||||
),
|
||||
("IsMultiLine", "EditBox", "the same"),
|
||||
("IsNumeric", "EditBox", "the same"),
|
||||
("IsPassword", "EditBox", "the same"),
|
||||
// ── ScrollingMessageFrame's line-position surface ─────────────────────────────────────────
|
||||
// The scroll POSITION verbs (`ScrollUp`/`ScrollDown`/`ScrollToTop`…) are all here; these four
|
||||
// report or set where that position sits, which our message buffer tracks internally and does
|
||||
// not publish. No caller anywhere measured.
|
||||
(
|
||||
"GetCurrentLine",
|
||||
"ScrollingMessageFrame",
|
||||
"the buffer's position is internal; no caller measured",
|
||||
),
|
||||
("GetCurrentScroll", "ScrollingMessageFrame", "the same"),
|
||||
("GetNumLinesDisplayed", "ScrollingMessageFrame", "the same"),
|
||||
("SetScrollFromBottom", "ScrollingMessageFrame", "the same"),
|
||||
// ── The colour picker's HSV pair ──────────────────────────────────────────────────────────
|
||||
// `ColorSelect` publishes the RGB pair; the HSV twin would need the conversion the reference
|
||||
// does in C. The stock `ColorPickerFrame` uses RGB only, and so does every corpus caller.
|
||||
(
|
||||
"GetColorHSV",
|
||||
"ColorSelect",
|
||||
"the RGB pair is published; nothing measured asks for HSV",
|
||||
),
|
||||
("SetColorHSV", "ColorSelect", "the same"),
|
||||
// ── One texture read-back ─────────────────────────────────────────────────────────────────
|
||||
(
|
||||
"IsDesaturated",
|
||||
"Texture",
|
||||
"`SetDesaturated` is here and not readable back; no caller measured",
|
||||
),
|
||||
];
|
||||
|
||||
/// Methods this client answers that 1.12 does not register on that class — **each a deliberate,
|
||||
/// reasoned exception, never a quiet superset**. Same contract as
|
||||
/// [`super::reference_surface::allowed_beyond_1_12`]: a name here must say why it stays.
|
||||
const ALLOWED_BEYOND: &[(&str, &str, &str)] = &[];
|
||||
|
||||
/// The reference's widget rows, `table_va -> {name}`.
|
||||
fn reference_tables() -> BTreeMap<String, BTreeSet<String>> {
|
||||
let tsv = concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/../../reference/1.12-shapes.tsv"
|
||||
);
|
||||
let text = std::fs::read_to_string(tsv).expect("reference/1.12-shapes.tsv");
|
||||
let mut out: BTreeMap<String, BTreeSet<String>> = BTreeMap::new();
|
||||
for line in text.lines() {
|
||||
if line.starts_with('#') || line.starts_with("name\t") {
|
||||
continue;
|
||||
}
|
||||
let f: Vec<&str> = line.split('\t').collect();
|
||||
if f.len() < 5 || f[4] != "widget" {
|
||||
continue;
|
||||
}
|
||||
out.entry(f[3].to_string())
|
||||
.or_default()
|
||||
.insert(f[0].to_string());
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// `class -> the effective reference surface` (own table + everything it inherits).
|
||||
fn reference_surface_by_class() -> BTreeMap<&'static str, BTreeSet<String>> {
|
||||
let tables = reference_tables();
|
||||
assert!(
|
||||
tables.len() == 23,
|
||||
"the shapes table gave {} widget registrar tables, not the reference's 23 — it changed \
|
||||
shape, and every count below is against the wrong denominator",
|
||||
tables.len()
|
||||
);
|
||||
let mut out = BTreeMap::new();
|
||||
for (class, own, bases) in WIDGET_CHAINS {
|
||||
let mut set = tables.get(*own).cloned().unwrap_or_default();
|
||||
for base in *bases {
|
||||
set.extend(tables.get(*base).cloned().unwrap_or_default());
|
||||
}
|
||||
assert!(!set.is_empty(), "{class}: empty reference surface");
|
||||
out.insert(*class, set);
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// `class -> what this VM answers`, asked of a live instance of each.
|
||||
fn ours_by_class() -> BTreeMap<String, BTreeSet<String>> {
|
||||
let script = UiScript::new().expect("VM");
|
||||
let rows = widget_method_census(&script).expect("census");
|
||||
let mut out: BTreeMap<String, BTreeSet<String>> = BTreeMap::new();
|
||||
for (class, method) in rows {
|
||||
assert_ne!(
|
||||
method, "!NOT-INSTANTIABLE",
|
||||
"{class} could not be instantiated — the census cannot speak for a class it never made"
|
||||
);
|
||||
out.entry(class).or_default().insert(method);
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// A declared list, `name -> (classes, reason)`.
|
||||
fn declared(list: &[(&str, &str, &str)]) -> BTreeMap<String, BTreeSet<String>> {
|
||||
list.iter()
|
||||
.map(|(name, classes, _)| {
|
||||
(
|
||||
(*name).to_string(),
|
||||
classes.split_whitespace().map(str::to_string).collect(),
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Render a `class -> names` map as the declared-list rows it would need, so a failure hands you
|
||||
/// the entry to paste rather than a set difference to transcribe.
|
||||
fn as_rows(diff: &BTreeMap<String, BTreeSet<String>>) -> String {
|
||||
let mut by_name: BTreeMap<&String, Vec<&String>> = BTreeMap::new();
|
||||
for (class, names) in diff {
|
||||
for n in names {
|
||||
by_name.entry(n).or_default().push(class);
|
||||
}
|
||||
}
|
||||
by_name
|
||||
.iter()
|
||||
.map(|(name, classes)| {
|
||||
format!(
|
||||
" (\"{name}\", \"{}\", \"WHY\"),",
|
||||
classes
|
||||
.iter()
|
||||
.map(|c| c.as_str())
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ")
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n")
|
||||
}
|
||||
|
||||
/// Every widget method the 1.12 client registers on a class is answered by ours, or listed.
|
||||
#[test]
|
||||
fn every_1_12_widget_method_is_answered() {
|
||||
let reference = reference_surface_by_class();
|
||||
let ours = ours_by_class();
|
||||
let listed = declared(NOT_YET_ANSWERED);
|
||||
|
||||
let mut missing: BTreeMap<String, BTreeSet<String>> = BTreeMap::new();
|
||||
let mut total = 0usize;
|
||||
for (class, want) in &reference {
|
||||
let have = ours.get(*class).cloned().unwrap_or_default();
|
||||
total += want.len();
|
||||
let gap: BTreeSet<String> = want
|
||||
.iter()
|
||||
.filter(|n| !have.contains(*n))
|
||||
.filter(|n| !listed.get(*n).is_some_and(|c| c.contains(*class)))
|
||||
.cloned()
|
||||
.collect();
|
||||
if !gap.is_empty() {
|
||||
missing.insert((*class).to_string(), gap);
|
||||
}
|
||||
}
|
||||
assert!(
|
||||
total > 2_000,
|
||||
"only {total} class/method pairs on the reference side — the chain stopped resolving"
|
||||
);
|
||||
assert!(
|
||||
missing.is_empty(),
|
||||
"the 1.12 client registers these on a class benilla does not answer them on, and they are \
|
||||
not listed:\n{}\n\nImplement it, or add the row to NOT_YET_ANSWERED with the reason it \
|
||||
has not been built.",
|
||||
as_rows(&missing)
|
||||
);
|
||||
|
||||
// A listed name that came back fails, so the list cannot outlive its fix.
|
||||
let mut healed: Vec<String> = Vec::new();
|
||||
for (name, classes, _) in NOT_YET_ANSWERED {
|
||||
for class in classes.split_whitespace() {
|
||||
if ours.get(class).is_some_and(|h| h.contains(*name)) {
|
||||
healed.push(format!("{class}:{name}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
assert!(
|
||||
healed.is_empty(),
|
||||
"these are answered now — take them out of NOT_YET_ANSWERED so the list keeps meaning what \
|
||||
it says: {healed:?}"
|
||||
);
|
||||
}
|
||||
|
||||
/// Every widget method benilla answers is one 1.12 registers on that class, or a listed exception.
|
||||
#[test]
|
||||
fn our_widget_methods_stay_inside_the_1_12_surface() {
|
||||
let reference = reference_surface_by_class();
|
||||
let ours = ours_by_class();
|
||||
let listed = declared(ALLOWED_BEYOND);
|
||||
|
||||
let mut beyond: BTreeMap<String, BTreeSet<String>> = BTreeMap::new();
|
||||
for (class, have) in &ours {
|
||||
let Some(want) = reference.get(class.as_str()) else {
|
||||
panic!("{class} is instantiable here but has no row in WIDGET_CHAINS");
|
||||
};
|
||||
let extra: BTreeSet<String> = have
|
||||
.iter()
|
||||
.filter(|n| !want.contains(*n))
|
||||
// Our host bridge and the VM's own internals declare themselves ours by prefix, the
|
||||
// same discipline `reference_surface` uses for globals.
|
||||
.filter(|n| !n.starts_with("Benilla") && !n.starts_with("__benilla_"))
|
||||
.filter(|n| !listed.get(*n).is_some_and(|c| c.contains(class)))
|
||||
.cloned()
|
||||
.collect();
|
||||
if !extra.is_empty() {
|
||||
beyond.insert(class.clone(), extra);
|
||||
}
|
||||
}
|
||||
assert!(
|
||||
beyond.is_empty(),
|
||||
"benilla answers these on a class the 1.12 client does not register them on:\n{}\n\n\
|
||||
1.12 is the target (decision 1188). Either remove it, give it its 1.12 spelling, or add \
|
||||
the row to ALLOWED_BEYOND WITH the reason it has to stay — an unexplained superset is \
|
||||
what 1189 had to roll back, and an addon that feature-detects one takes a path we cannot \
|
||||
honour.",
|
||||
as_rows(&beyond)
|
||||
);
|
||||
|
||||
let mut gone: Vec<String> = Vec::new();
|
||||
for (name, classes, _) in ALLOWED_BEYOND {
|
||||
for class in classes.split_whitespace() {
|
||||
if !ours.get(class).is_some_and(|h| h.contains(*name)) {
|
||||
gone.push(format!("{class}:{name}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
assert!(
|
||||
gone.is_empty(),
|
||||
"these are no longer exposed — take them out of ALLOWED_BEYOND: {gone:?}"
|
||||
);
|
||||
}
|
||||
@@ -96,6 +96,7 @@ pub(crate) fn fire_event_into(lua: &Lua, event: &str, args: Vec<ScriptValue>) {
|
||||
at = next;
|
||||
}
|
||||
}
|
||||
event::fire_all_event_listeners(lua, event, &args);
|
||||
}
|
||||
|
||||
impl super::UiScript {
|
||||
|
||||
@@ -9,7 +9,7 @@ use crate::layout::{Anchor, Point};
|
||||
use crate::script::object::{frame_handle_of, frame_wrapper};
|
||||
use crate::script::region::region_handle_of;
|
||||
use crate::script::Model;
|
||||
use crate::widget::{KindState, TOOLTIP_LINE_GAP, TOOLTIP_PAD};
|
||||
use crate::widget::{KindState, TooltipAnchor, TOOLTIP_LINE_GAP, TOOLTIP_PAD};
|
||||
|
||||
use super::{
|
||||
append_line, bool_arg, cancel_fade, clear_content, fire_cleared, hide_tooltip, now,
|
||||
@@ -25,7 +25,19 @@ pub(in crate::script) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
// point the plate per the anchor law. The six owner anchors are the documented 1.12 API set
|
||||
// (spec-faithful, same posture as StatusBar's contract; the corpus exercises RIGHT/LEFT/
|
||||
// BOTTOMRIGHT); ANCHOR_NONE leaves pointing to the caller (GameTooltip_SetDefaultAnchor),
|
||||
// ANCHOR_PRESERVE keeps the previous placement.
|
||||
// ANCHOR_PRESERVE keeps the previous placement. The mode is stored on the tooltip and read back
|
||||
// by `GetAnchorType` below.
|
||||
//
|
||||
// **The `anchorType`-omitted default here is ANCHOR_RIGHT and the reference's is ANCHOR_NONE.**
|
||||
// The reference's SetOwner core defaults `[+0x318]` to mode 7 = ANCHOR_NONE (`0x530012`, wow-re
|
||||
// `bag-portrait-and-appendtext.md` §5) — no `SetPoint` at all, the plate left wherever it was.
|
||||
// Two stock 1.12 files reach that leg (`QuestTimerFrame.xml:19`, `WorldStateFrame.xml:681`,
|
||||
// both `GameTooltip:SetOwner(this)`), and neither is on `benilla.toc` yet, so nothing today
|
||||
// takes it. It is left alone deliberately rather than quietly flipped: our ANCHOR_NONE arm
|
||||
// below DROPS the plate's anchors where the reference merely skips the SetPoint, so changing
|
||||
// the default would land those two hovers at the layout origin rather than "wherever it was" —
|
||||
// the two questions have to be settled together, and the second one is a visible-placement
|
||||
// change. Recorded here so the next reader sees a stated divergence, not an oversight.
|
||||
m.set(
|
||||
"SetOwner",
|
||||
lua.create_function(
|
||||
@@ -45,20 +57,43 @@ pub(in crate::script) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
cancel_fade(&mut model, h);
|
||||
tip_mut(&mut model, h)?.owner = Some(owner_h);
|
||||
let owner_id = model.frame_id(owner_h);
|
||||
let anchor = anchor.unwrap_or_else(|| "ANCHOR_RIGHT".into());
|
||||
let pts = match anchor.to_ascii_uppercase().as_str() {
|
||||
"ANCHOR_RIGHT" => Some((Point::BottomLeft, Point::TopRight)),
|
||||
"ANCHOR_LEFT" => Some((Point::BottomRight, Point::TopLeft)),
|
||||
"ANCHOR_TOPRIGHT" => Some((Point::BottomRight, Point::TopRight)),
|
||||
"ANCHOR_TOPLEFT" => Some((Point::BottomLeft, Point::TopLeft)),
|
||||
"ANCHOR_BOTTOMRIGHT" => Some((Point::TopLeft, Point::BottomRight)),
|
||||
"ANCHOR_BOTTOMLEFT" => Some((Point::TopRight, Point::BottomLeft)),
|
||||
"ANCHOR_NONE" | "ANCHOR_PRESERVE" => None,
|
||||
// The anchor mode is resolved ONCE, into the reference's own enum, and both
|
||||
// the placement below and `GetAnchorType`'s answer are taken from that one
|
||||
// value — so the plate and the getter can never tell different stories. An
|
||||
// unrecognised string (`ANCHOR_CURSOR` among them, which this engine cannot
|
||||
// honour — see [`TooltipAnchor::Cursor`]) warns and is recorded as the
|
||||
// ANCHOR_RIGHT it is actually placed by, not as the string it was handed.
|
||||
let anchor = match anchor
|
||||
.as_deref()
|
||||
.map(str::to_ascii_uppercase)
|
||||
.as_deref()
|
||||
.unwrap_or("ANCHOR_RIGHT")
|
||||
{
|
||||
"ANCHOR_RIGHT" => TooltipAnchor::Right,
|
||||
"ANCHOR_LEFT" => TooltipAnchor::Left,
|
||||
"ANCHOR_TOPRIGHT" => TooltipAnchor::TopRight,
|
||||
"ANCHOR_TOPLEFT" => TooltipAnchor::TopLeft,
|
||||
"ANCHOR_BOTTOMRIGHT" => TooltipAnchor::BottomRight,
|
||||
"ANCHOR_BOTTOMLEFT" => TooltipAnchor::BottomLeft,
|
||||
"ANCHOR_NONE" => TooltipAnchor::None,
|
||||
"ANCHOR_PRESERVE" => TooltipAnchor::Preserve,
|
||||
other => {
|
||||
model.record_warning(format!(
|
||||
"SetOwner: unknown anchor '{other}' (kept ANCHOR_RIGHT)"
|
||||
));
|
||||
Some((Point::BottomLeft, Point::TopRight))
|
||||
TooltipAnchor::Right
|
||||
}
|
||||
};
|
||||
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
|
||||
}
|
||||
};
|
||||
match pts {
|
||||
@@ -84,7 +119,7 @@ pub(in crate::script) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
model.touch_layout_retarget_frame(h, &old_targets, &[owner_id]);
|
||||
}
|
||||
}
|
||||
None if anchor.eq_ignore_ascii_case("ANCHOR_NONE") => {
|
||||
None if anchor == TooltipAnchor::None => {
|
||||
// The caller points it (ClearAllPoints+SetPoint) — drop ours now so a
|
||||
// stale owner anchor never wins the frame the caller forgets to.
|
||||
//
|
||||
@@ -109,7 +144,10 @@ pub(in crate::script) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
model.touch_layout_retarget_frame(h, &old_targets, &[]);
|
||||
}
|
||||
}
|
||||
None => {} // ANCHOR_PRESERVE
|
||||
// ANCHOR_PRESERVE. `TooltipAnchor::Cursor` shares the arm for
|
||||
// exhaustiveness only — the string match above never produces it (it warns
|
||||
// and records ANCHOR_RIGHT instead), so nothing reaches here that way.
|
||||
None => {}
|
||||
}
|
||||
}
|
||||
fire_cleared(lua, h);
|
||||
@@ -117,8 +155,16 @@ pub(in crate::script) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
},
|
||||
)?,
|
||||
)?;
|
||||
// GameTooltip:BenillaGetTooltipOwner() — **ours, and the prefix is what makes it honest.**
|
||||
// 1.12 registers `IsOwned` (ask about ONE frame) and no getter at all, so there is no
|
||||
// reference spelling for "which frame owns this plate" to be faithful to; a `GetOwner` here
|
||||
// would be an unexplained superset an addon can feature-detect (1188, and the census that
|
||||
// moved it, 2142). Under the `Benilla` prefix it is unreachable by accident from an addon
|
||||
// that means to call a WoW function. Its one caller is the dev-only hover recorder
|
||||
// (`benilla-app/src/hover_log.rs`), which needs the owner's NAME per frame so a trace reads
|
||||
// the same names the director sees.
|
||||
m.set(
|
||||
"GetOwner",
|
||||
"BenillaGetTooltipOwner",
|
||||
lua.create_function(|lua, this: Table| {
|
||||
let owner = with_tip(lua, &this, |t| t.owner)?;
|
||||
let id = {
|
||||
@@ -133,6 +179,28 @@ pub(in crate::script) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
}
|
||||
})?,
|
||||
)?;
|
||||
// GetAnchorType() — ONE string, the mode the last SetOwner placed this plate by
|
||||
// (`0x5313e0`, table `0x854198`, argc 1, arity 1, kinds `(string)`; it reads `[+0x318]` back
|
||||
// through the 9-entry name table `0x531530`). The kinds column has no nil alternative and this
|
||||
// does not answer one: a tooltip nothing has owned yet answers `"ANCHOR_NONE"`, which is
|
||||
// [`TooltipAnchor`]'s default and the same mode the reference's own SetOwner core falls back to.
|
||||
//
|
||||
// Two corpus callers, both unguarded, both raising against this VM until now, and they are the
|
||||
// two shapes the verb has: `pfUI/modules/tooltip.lua:97` compares against ONE mode
|
||||
// (`if GameTooltip:GetAnchorType() == "ANCHOR_NONE" then` — its OnShow reposition, so an equality
|
||||
// on the exact reference spelling), and `_Nameplates/_Nameplates.lua:479` compares against a
|
||||
// mode it computed (`if GameTooltip:GetAnchorType() ~= Anchor then GameTooltip:SetOwner(Column,
|
||||
// Anchor) end` — a re-SetOwner suppressor, so an answer that never equals what SetOwner was
|
||||
// given would re-own the plate on every OnUpdate). Both need the string to round-trip through
|
||||
// the setter verbatim, which is why the mode is resolved once up there and read straight back
|
||||
// here rather than re-derived from the plate's anchors.
|
||||
m.set(
|
||||
"GetAnchorType",
|
||||
lua.create_function(|lua, this: Table| {
|
||||
let anchor = with_tip(lua, &this, |t| t.anchor)?;
|
||||
Ok(anchor.name())
|
||||
})?,
|
||||
)?;
|
||||
// IsOwned(frame) — the hover re-enter loop's gate (ref ContainerFrame.lua OnUpdate): true
|
||||
// only while `frame` is the live owner; the owner drops on hide, so a hidden tooltip never
|
||||
// resurrects from a stale OnUpdate.
|
||||
@@ -391,7 +459,7 @@ pub(in crate::script) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
})?,
|
||||
)?;
|
||||
|
||||
// The item content channels (SetItemById/SetBagItem/SetMerchantItem/SetBuybackItem) live in
|
||||
// The item content channels (BenillaSetItemById/SetBagItem/SetMerchantItem/SetBuybackItem) live in
|
||||
// their own module — the one shared renderer + the red usable law (decision 0274 P1).
|
||||
crate::script::tooltip_item::install_methods(lua, &m)?;
|
||||
// The spell/aura/action content channels (SetSpell/SetShapeshift/SetPlayerBuff/SetAction) —
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
//! The engine **item-tooltip renderer** (decision 0274 P1) — the one line law every item hover
|
||||
//! renders through, mirroring the real client's single shared C++ renderer (`0x52b650`, behind
|
||||
//! 8 of the 9 `Set*Item` bindings — wow-re `ui/scratch/tooltip-money.md`). The entry methods
|
||||
//! (`SetItemById`, `SetBagItem`, `SetMerchantItem`, `SetBuybackItem`) register into the
|
||||
//! (`SetBagItem`, `SetMerchantItem`, `SetBuybackItem`, and the prefixed `BenillaSetItemById` —
|
||||
//! ours, because 1.12 has no id-keyed door onto the shared renderer) register into the
|
||||
//! GameTooltip kind table beside the widget verbs ([`super::tooltip`]).
|
||||
//!
|
||||
//! **The line law is BYTE-VERIFIED** — the 0274 §5 verdict on `0x52b650`'s emission order
|
||||
@@ -32,7 +33,7 @@
|
||||
//! while the merchant window is open and repair mode is off — the engine computes
|
||||
//! `SellPrice × stack` and fires the `OnTooltipAddMoney` script; FrameXML renders the coins
|
||||
//! (`SetTooltipMoney`). A zero sell price in that context prints `ITEM_UNSELLABLE`
|
||||
//! (`0x854a74`, pushed at `0x52e4a3`). Template sources (`SetMerchantItem`/`SetItemById`/quest
|
||||
//! (`0x854a74`, pushed at `0x52e4a3`). Template sources (`SetMerchantItem`/`BenillaSetItemById`/quest
|
||||
//! rows) never show money, per the same law.
|
||||
|
||||
use mlua::{Lua, MultiValue, Table, Value};
|
||||
@@ -187,7 +188,7 @@ fn hyperlink_item_fields(link: &str) -> Option<(u32, u32, u32)> {
|
||||
(item_id != 0).then_some((item_id, enchant_id, random_property_id))
|
||||
}
|
||||
|
||||
/// The shared id-keyed render (`SetItemById`/`SetHyperlink`): template hit → the full line law
|
||||
/// The shared id-keyed render (`BenillaSetItemById`/`SetHyperlink`): template hit → the full line law
|
||||
/// (+ the compare arm when this is the main GameTooltip); miss → the ask + a name-only line.
|
||||
fn render_by_id(
|
||||
lua: &Lua,
|
||||
@@ -369,11 +370,22 @@ pub(super) fn on_shift_edge(lua: &Lua, down: bool) {
|
||||
|
||||
/// Register the item content channels into the GameTooltip kind method table.
|
||||
pub(super) fn install_methods(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
// GameTooltip:SetItemById(itemId [, fallbackName, fallbackQuality]) — the id-keyed hover
|
||||
// (quest reward rows, loot rows; a benilla extension beside the era surface). Template
|
||||
// source: no money row, per the byte-verified sell-price law.
|
||||
// GameTooltip:BenillaSetItemById(itemId [, fallbackName, fallbackQuality]) — the id-keyed
|
||||
// hover (quest reward rows, loot rows).
|
||||
//
|
||||
// **Ours, and the `Benilla` prefix is what keeps it honest.** 1.12's nine `Set*Item` bindings
|
||||
// all name a CONTAINER — a bag slot, a merchant row, a loot slot — because the reference's
|
||||
// callers always have one; nothing there takes a bare item id. Ours do: the channels in this
|
||||
// module reach [`render_by_id`] as a Rust call, but the ones that live in a sibling module
|
||||
// (`tooltip_spell`'s action-bar item arm, `SetTrainerService`, `SetCraftSpell`) reach it
|
||||
// through the wrapper table, and that needs a NAME. Spelling that name like a WoW function
|
||||
// would be an unexplained superset an addon can feature-detect (1188; the census that moved
|
||||
// it, 2142); under the prefix it is unreachable by accident from an addon that means to call
|
||||
// a WoW function, and no addon has reason to call it at all.
|
||||
//
|
||||
// Template source: no money row, per the byte-verified sell-price law.
|
||||
m.set(
|
||||
"SetItemById",
|
||||
"BenillaSetItemById",
|
||||
lua.create_function(
|
||||
|lua, (this, item_id, fb_name, fb_q): (Table, u32, Option<String>, Option<u32>)| {
|
||||
render_by_id(lua, &this, item_id, fb_name, fb_q)
|
||||
@@ -739,7 +751,7 @@ pub(super) fn install_methods(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
// supplies the instance block, p6=1), and that is the whole point of it existing here: a loot
|
||||
// slot is no item object, so its rolled random-suffix enchants reach the builder through the
|
||||
// block — never through `ITEM_FIELD_ENCHANTMENT`, which the wire does not carry for loot.
|
||||
// Hovering a "… of the Monkey" drop through the template path `SetItemById` instead printed
|
||||
// Hovering a "… of the Monkey" drop through the template path `BenillaSetItemById` instead printed
|
||||
// the `<Random enchantment>` placeholder until the item was in the bag (decision 1547).
|
||||
//
|
||||
// `slot` is the 1-based display row, like every other loot getter; the coin pile and a
|
||||
@@ -1281,7 +1293,7 @@ pub(super) fn install_methods(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
|
||||
// GameTooltip:SetTradeSkillItem(skillIndex [, reagentIndex]) — the tradeskill window's item
|
||||
// hover (decision 0437 phase 2): with reagentIndex, that recipe's reagent's item; without, the
|
||||
// recipe's PRODUCT item. Routes through the same id-keyed renderer SetItemById uses. A product
|
||||
// recipe's PRODUCT item. Routes through the same id-keyed renderer BenillaSetItemById uses. A product
|
||||
// id of 0 (a pure-effect recipe) or an in-flight ask-once answer both fall to render_by_id's own
|
||||
// name-only fallback line (the recipe's name for the product channel, the reagent's own —
|
||||
// possibly still-nil — name for the reagent channel) rather than a no-op: that fallback already
|
||||
|
||||
@@ -439,7 +439,7 @@ pub(super) fn render_view(
|
||||
// "No id source" is the reference's own three-way fork (§E1): a wrapped gift, or no item
|
||||
// object AND no caller-supplied instance block (`+0x440 == 0`). Ours reads the same: a hover
|
||||
// that passes NO [`ItemInstance`] is a p6=0 leg — the template sources (merchant, quest,
|
||||
// craft, buyback, send-mail, the compare legs, `SetItemById`) — plus the wrapped-gift bit.
|
||||
// craft, buyback, send-mail, the compare legs, `BenillaSetItemById`) — plus the wrapped-gift bit.
|
||||
//
|
||||
// **A block-supplying source never prints the placeholder, even carrying no ids at all.** The
|
||||
// fork tests the block's presence, not its contents, so `SetLootItem`/`SetHyperlink`/
|
||||
|
||||
@@ -698,7 +698,7 @@ pub(super) fn install_methods(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
),
|
||||
0x80 => {
|
||||
// Route through the shared item renderer (the id-keyed entry).
|
||||
let f: mlua::Function = this.get("SetItemById")?;
|
||||
let f: mlua::Function = this.get("BenillaSetItemById")?;
|
||||
f.call::<()>((this.clone(), a.action))
|
||||
}
|
||||
0x40 => {
|
||||
@@ -750,7 +750,7 @@ pub(super) fn install_methods(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
// `0x52b650`. No fallback name: an item id of 0 or a template still in flight
|
||||
// renders an EMPTY tooltip, which is the builder's own early-out.
|
||||
TrainerTooltip::Item(item_id) => {
|
||||
let f: mlua::Function = this.get("SetItemById")?;
|
||||
let f: mlua::Function = this.get("BenillaSetItemById")?;
|
||||
f.call::<()>((this.clone(), item_id))
|
||||
}
|
||||
TrainerTooltip::Spell {
|
||||
@@ -796,7 +796,7 @@ pub(super) fn install_methods(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
};
|
||||
match subject {
|
||||
CraftTooltip::Item(item_id) => {
|
||||
let f: mlua::Function = this.get("SetItemById")?;
|
||||
let f: mlua::Function = this.get("BenillaSetItemById")?;
|
||||
f.call::<()>((this.clone(), item_id))
|
||||
}
|
||||
CraftTooltip::Spell(spell_id) => {
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
|
||||
use mlua::{Lua, Value};
|
||||
|
||||
use super::binding_abi::{number_arg, predicate};
|
||||
use super::binding_abi::{flag, number_arg};
|
||||
use super::Model;
|
||||
|
||||
/// `FlagTutorial`'s clamp: `0 ≤ n − 1 < 0x32` — the fifty ids `GlobalStrings.lua` names.
|
||||
@@ -57,7 +57,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
.tutorial_bank
|
||||
.as_ref()
|
||||
.is_some_and(|b| b.iter().any(|&byte| byte != 0xFF));
|
||||
Ok(predicate(enabled))
|
||||
Ok(flag(enabled))
|
||||
})?,
|
||||
)?;
|
||||
|
||||
|
||||
@@ -167,10 +167,15 @@ pub enum QuadContent {
|
||||
/// only carries the token; resolving it to a rendered image is the app's job (the modern
|
||||
/// 2D portrait is a high-res model bake).
|
||||
portrait_unit: Option<String>,
|
||||
/// On-screen rotation about the quad center, radians, **counterclockwise-positive** (the
|
||||
/// `SetRotation` texture API — a later-era method benilla ships early: the world-map
|
||||
/// player arrow spins by it, standing in for the reference's engine arrow model; 0203
|
||||
/// flags the stand-in). `0.0` for the overwhelmingly common unrotated case.
|
||||
/// On-screen rotation about the quad center, radians, **counterclockwise-positive**.
|
||||
///
|
||||
/// **Always `0.0` now — nothing writes it any more.** It carried a later-era
|
||||
/// `Texture:SetRotation` benilla shipped early so the world-map player arrow could spin,
|
||||
/// standing in for the reference's engine arrow model (0203 flags the stand-in). That arrow
|
||||
/// became a real Model frame driven by `ModelState::facing` (`script::worldmap_arrow`), and
|
||||
/// the verb has since come off the Texture map — 1.12 registers `SetRotation` on PlayerModel
|
||||
/// alone (`0x84f1fc`). The plumbing is left standing rather than pruned in the same change,
|
||||
/// because it runs into the app's quad emit; see [`RegionData::rotation`].
|
||||
rotation: f32,
|
||||
/// `Texture:SetDesaturated(1)` — draw the sampled texel as its **luminance** instead of
|
||||
/// its colour (decision 1327). The renderer greys the texel and *then* modulates by
|
||||
@@ -279,6 +284,67 @@ pub enum JustifyV {
|
||||
Bottom,
|
||||
}
|
||||
|
||||
/// A Texture's blend mode — the client's shared `alphaMode` enum, the `{value, name}` table at
|
||||
/// `0x811aa8`: `DISABLE=0 · ALPHAKEY=1 · BLEND=2 · ADD=3 · MOD=4` (wow-re
|
||||
/// `system/ui/ui.md:1383`, `scratch/rf28-typed-widget-loadxml.md:22`). It is one enum reached two
|
||||
/// ways — the XML `alphaMode=` attribute and `Texture:SetBlendMode` — and read back by
|
||||
/// `Texture:GetBlendMode`.
|
||||
///
|
||||
/// **`Blend` is the default because the CSimpleTexture ctor writes it**: `0x76fc64 mov
|
||||
/// [esi+0xd0],2`, which `ActionButtonTemplate.xml`'s `<NormalTexture>` keeps by declaring no
|
||||
/// `alphaMode` (wow-re `scratch/button-state-texture-path-setter.md`, where the value is
|
||||
/// load-bearing for a different question and therefore evidence rather than assertion — mode 1
|
||||
/// would alpha-test a half-alpha vertex colour away entirely, mode 2 ghosts it). It is the answer
|
||||
/// every ordinary texture gives `ShaguTweaks/mods/dark-ui-elements.lua:169`'s
|
||||
/// `region:GetBlendMode() == "ADD"` guard, so getting it wrong is the same recolouring bug one
|
||||
/// branch over.
|
||||
///
|
||||
/// **Only ADD changes what this engine draws.** The renderer takes the ADD/not-ADD bit
|
||||
/// ([`QuadContent::Texture::additive`]); `DISABLE`, `ALPHAKEY` and `MOD` are carried faithfully
|
||||
/// through the setter and the getter and drawn as straight alpha — the stated v1 gap, unchanged by
|
||||
/// storing the whole mode instead of a bool.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
pub enum BlendMode {
|
||||
/// `DISABLE` = 0 — `GL_ONE, GL_ZERO`, alpha reference 0. Drawn as straight alpha here.
|
||||
Disable,
|
||||
/// `ALPHAKEY` = 1 — `GL_ONE, GL_ZERO` with alpha reference 224/255. Drawn as straight alpha here.
|
||||
AlphaKey,
|
||||
/// `BLEND` = 2 — `GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA`. The ctor's value, hence the default.
|
||||
#[default]
|
||||
Blend,
|
||||
/// `ADD` = 3 — `GL_SRC_ALPHA, GL_ONE`. The one mode the renderer acts on.
|
||||
Add,
|
||||
/// `MOD` = 4 — `GL_DST_COLOR, GL_ZERO`. Drawn as straight alpha here.
|
||||
Mod,
|
||||
}
|
||||
|
||||
impl BlendMode {
|
||||
/// Parse the enum's own spelling, as `SetBlendMode` and the XML `alphaMode=` attribute take it.
|
||||
/// Case-insensitive (the Lua binding coerces through the shared string-arg law); `None` for a
|
||||
/// name outside the table's five.
|
||||
pub fn parse(s: &str) -> Option<BlendMode> {
|
||||
match s.to_ascii_uppercase().as_str() {
|
||||
"DISABLE" => Some(BlendMode::Disable),
|
||||
"ALPHAKEY" => Some(BlendMode::AlphaKey),
|
||||
"BLEND" => Some(BlendMode::Blend),
|
||||
"ADD" => Some(BlendMode::Add),
|
||||
"MOD" => Some(BlendMode::Mod),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// The reference's own spelling, as `GetBlendMode()` answers it (the name column of `0x811aa8`).
|
||||
pub const fn name(self) -> &'static str {
|
||||
match self {
|
||||
BlendMode::Disable => "DISABLE",
|
||||
BlendMode::AlphaKey => "ALPHAKEY",
|
||||
BlendMode::Blend => "BLEND",
|
||||
BlendMode::Add => "ADD",
|
||||
BlendMode::Mod => "MOD",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A `FontString`'s glyph outline (the XML `outline`/`<Font outline=>` attr, OUTLINETYPE): `NONE`
|
||||
/// (the common case), `NORMAL`, or `THICK`. Resolved through the font registry and readable via
|
||||
/// `GetFont`; the v1 text renderer does **not** yet rasterize outlines (a documented gap — the atlas
|
||||
@@ -510,9 +576,17 @@ pub(crate) struct RegionData {
|
||||
/// opaque, the next `length` ramp 1→0, the rest invisible. `None` = no gradient (every
|
||||
/// FontString but an armed quest description). Cleared by `SetText` — fresh text draws whole.
|
||||
pub(crate) alpha_gradient: Option<(f32, f32)>,
|
||||
/// WoW `ADD` blend (`SetBlendMode("ADD")` / XML `alphaMode` — the shared enum `0x811aa8`).
|
||||
/// Highlight state textures default to it (the client's `SetHighlightTexture` contract).
|
||||
pub(crate) additive: bool,
|
||||
/// The region's blend mode (`SetBlendMode` / XML `alphaMode` — the reference's `[texture+0xd0]`,
|
||||
/// the shared enum `0x811aa8`). Highlight state textures are created at
|
||||
/// [`BlendMode::Add`] (the client's `SetHighlightTexture` contract); everything else starts at
|
||||
/// the ctor's [`BlendMode::Blend`].
|
||||
///
|
||||
/// **The whole mode is kept, not just "is it ADD"**, because `GetBlendMode` answers it back as
|
||||
/// a string and a getter that could only ever say `"BLEND"` or `"ADD"` would answer `"BLEND"`
|
||||
/// to a texture the caller set to `"MOD"`. The renderer still reads only the ADD/not-ADD
|
||||
/// distinction ([`QuadContent::Texture::additive`]) — that is unchanged, and the three modes it
|
||||
/// flattens are the stated gap they always were.
|
||||
pub(crate) blend: BlendMode,
|
||||
/// Explicit region size (`SetWidth`/`SetHeight`/XML `<Size>`); `None` = derive. Size fills the
|
||||
/// axis the anchors don't pin (the client's "0 = derive") — so under a texture's implicit
|
||||
/// SetAllPoints (two corners pin everything) an authored size is structurally unread, which is
|
||||
@@ -550,8 +624,24 @@ pub(crate) struct RegionData {
|
||||
/// The `<TexCoords>`/`SetTexCoord` UV mapping ([`TexCoords`]: the 4-edge crop, or the 8-arg
|
||||
/// affine quad). `None` = the full texture. Slices the quadrant/atlas art (decision 0084).
|
||||
pub(crate) tex_coords: Option<TexCoords>,
|
||||
/// On-screen rotation about the region center (`SetRotation`, radians, counterclockwise-
|
||||
/// positive) — see [`QuadContent::Texture::rotation`].
|
||||
/// `Texture:SetTexCoordModifiesRect(flag)` / `GetTexCoordModifiesRect()` — the reference's
|
||||
/// `[texture+0x124]`, whose sole writer is `SetTexCoordModifiesRect 0x79c080`
|
||||
/// (`0x79c113 mov [edi+0x124],eax`) and which is BSS-zero otherwise (wow-re
|
||||
/// `scratch/taxiroute-widget-type.md`, `ui.md:5594`).
|
||||
///
|
||||
/// **State only, and the geometry half is NOT wired.** In the reference the flag gates a
|
||||
/// rect-recompute leg (`ui.md:4619`, `0x770462`): with it set, a `SetTexCoord` re-derives the
|
||||
/// region's own rect from the UV quad rather than leaving the rect alone and merely resampling
|
||||
/// inside it. Honouring that here means `region::layout`'s resolve reading this flag and
|
||||
/// deriving the region's rect from [`Self::tex_coords`] instead of from its anchors and size —
|
||||
/// a resolve-order change, not a paint one, and nothing in either addon corpus or the stock UI
|
||||
/// calls the SETTER, so there is no measured case to build it against. Stored and answered
|
||||
/// truthfully, unread by layout; stated here rather than left to be discovered.
|
||||
pub(crate) tex_coord_modifies_rect: bool,
|
||||
/// On-screen rotation about the region center, radians, counterclockwise-positive.
|
||||
/// **Unwritable: the `Texture:SetRotation` that was its only setter is gone** (1.12 registers
|
||||
/// the name on PlayerModel alone) — see [`QuadContent::Texture::rotation`] for why it is still
|
||||
/// here.
|
||||
pub(crate) rotation: f32,
|
||||
/// The named font object this FontString last resolved (`inherits=`/`SetFontObject`) — our
|
||||
/// `FONTINSTANCE+0x028 parentFontObject`, and a **live** link: mutating that object
|
||||
|
||||
@@ -3,11 +3,11 @@
|
||||
//! through the parent's `with_unit`/`pick_unit_token` helpers.
|
||||
//!
|
||||
//! **Every predicate here returns through one function** — [`super::unit_predicate`] when it reads
|
||||
//! a snapshot field, [`predicate`] when it computes its own bool. Neither ever hands mlua a Rust
|
||||
//! `bool`: all 29 of the reference's unit predicates push the constant double `1.0`
|
||||
//! (`lua_pushnumber 0x6f3810`) or `nil` (`lua_pushnil 0x6f37f0`), exactly one value at every live
|
||||
//! `ret`, and **no binding in the 83-entry table at `0x850438` calls `lua_pushboolean 0x6f39f0` at
|
||||
//! all** (decisions 1830, 2043, 2048). A new predicate that open-codes
|
||||
//! a snapshot field, [`flag`](super::super::binding_abi::flag) when it computes its own bool.
|
||||
//! Neither ever hands mlua a Rust `bool`: all 29 of the reference's unit predicates push the
|
||||
//! constant double `1.0` (`lua_pushnumber 0x6f3810`) or `nil` (`lua_pushnil 0x6f37f0`), one value
|
||||
//! at every live `ret`, and **no binding in the 83-entry table at `0x850438` calls
|
||||
//! `lua_pushboolean 0x6f39f0` at all** (decisions 1830, 2043, 2048). A new predicate that open-codes
|
||||
//! `Value::Integer(1)`/`Value::Nil`, or returns a `bool`, is the drift those records exist to stop.
|
||||
//!
|
||||
//! **The scope of that claim is the unit table, not "the binding surface"** — 2043 said the wider
|
||||
@@ -17,7 +17,7 @@
|
||||
|
||||
use mlua::{Lua, Value};
|
||||
|
||||
use super::super::binding_abi::predicate;
|
||||
use super::super::binding_abi::flag;
|
||||
use super::super::Model;
|
||||
use super::{
|
||||
check_unit_token, classification_word, grey_band, level_reads_unknown, pick_unit_token,
|
||||
@@ -112,7 +112,7 @@ pub(in crate::script) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
})?;
|
||||
// One value, and it is the NUMBER 1 or nil — never a boolean, the same shape the
|
||||
// rest of this family answers in.
|
||||
Ok(predicate(hit))
|
||||
Ok(flag(hit))
|
||||
})?,
|
||||
)?;
|
||||
}
|
||||
@@ -154,7 +154,7 @@ pub(in crate::script) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
.as_ref()
|
||||
.and_then(|t| model.unit(t))
|
||||
.map_or(0, |u| u.guid);
|
||||
Ok(predicate(by_flag || guid == model.party.leader_guid))
|
||||
Ok(flag(by_flag || guid == model.party.leader_guid))
|
||||
})?,
|
||||
)?;
|
||||
|
||||
@@ -417,7 +417,7 @@ pub(in crate::script) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
)?);
|
||||
let token = pick_unit_token(&a, &b);
|
||||
let r = with_unit(lua, &token, 0u8, |u| u.reaction)?;
|
||||
Ok(predicate((1..=2).contains(&r)))
|
||||
Ok(flag((1..=2).contains(&r)))
|
||||
})?,
|
||||
)?;
|
||||
g.set(
|
||||
@@ -437,7 +437,7 @@ pub(in crate::script) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
)?);
|
||||
let token = pick_unit_token(&a, &b);
|
||||
let r = with_unit(lua, &token, 0u8, |u| u.reaction)?;
|
||||
Ok(predicate(r >= 5))
|
||||
Ok(flag(r >= 5))
|
||||
})?,
|
||||
)?;
|
||||
|
||||
@@ -467,7 +467,7 @@ pub(in crate::script) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
)?);
|
||||
let model = lua.app_data_ref::<Model>().expect("model app_data");
|
||||
let player_level = model.player_req.level;
|
||||
Ok(predicate(
|
||||
Ok(flag(
|
||||
unit.and_then(|u| model.unit(&u))
|
||||
.is_some_and(|u| super::is_civilian_kill(u, player_level)),
|
||||
))
|
||||
@@ -482,7 +482,7 @@ pub(in crate::script) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
"UnitPlayerControlled",
|
||||
lua.create_function(|lua, unit: Option<String>| {
|
||||
let model = lua.app_data_ref::<Model>().expect("model app_data");
|
||||
Ok(predicate(
|
||||
Ok(flag(
|
||||
unit.and_then(|u| model.unit(&u))
|
||||
.is_some_and(|u| u.player_controlled),
|
||||
))
|
||||
@@ -525,7 +525,7 @@ pub(in crate::script) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
if !ua.exists || !ub.exists {
|
||||
return Ok(Value::Nil);
|
||||
}
|
||||
Ok(predicate(a == b || (ua.guid != 0 && ua.guid == ub.guid)))
|
||||
Ok(flag(a == b || (ua.guid != 0 && ua.guid == ub.guid)))
|
||||
})?,
|
||||
)?;
|
||||
|
||||
@@ -549,7 +549,7 @@ pub(in crate::script) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
"Usage: UnitAffectingCombat(\"unit\")",
|
||||
)?;
|
||||
let hot = with_unit(lua, &Some(token), false, |u| u.exists && u.in_combat)?;
|
||||
Ok(predicate(hot))
|
||||
Ok(flag(hot))
|
||||
})?,
|
||||
)?;
|
||||
|
||||
@@ -578,7 +578,7 @@ pub(in crate::script) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
"HasFullControl",
|
||||
lua.create_function(|lua, ()| {
|
||||
let model = lua.app_data_ref::<Model>().expect("model app_data");
|
||||
Ok(predicate(model.player_control))
|
||||
Ok(flag(model.player_control))
|
||||
})?,
|
||||
)?;
|
||||
|
||||
@@ -617,7 +617,7 @@ pub(in crate::script) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
!model.party.members.is_empty()
|
||||
};
|
||||
let hit = grouped && (in_group(u.guid) || in_group(u.owner));
|
||||
Ok(predicate(hit))
|
||||
Ok(flag(hit))
|
||||
})?,
|
||||
)?;
|
||||
}
|
||||
@@ -638,7 +638,7 @@ pub(in crate::script) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
.iter()
|
||||
.any(|m| m.guid != 0 && m.guid == u.guid)
|
||||
});
|
||||
Ok(predicate(hit))
|
||||
Ok(flag(hit))
|
||||
})?,
|
||||
)?;
|
||||
|
||||
@@ -668,7 +668,7 @@ pub(in crate::script) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
.members
|
||||
.iter()
|
||||
.any(|m| m.guid != 0 && m.guid == u.guid)));
|
||||
Ok(predicate(hit))
|
||||
Ok(flag(hit))
|
||||
})?,
|
||||
)?;
|
||||
|
||||
@@ -695,7 +695,7 @@ pub(in crate::script) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
let ok = with_unit(lua, &token, false, |u| {
|
||||
u.exists && u.is_player && u.reaction >= 5
|
||||
})?;
|
||||
Ok(predicate(ok))
|
||||
Ok(flag(ok))
|
||||
})?,
|
||||
)?;
|
||||
|
||||
@@ -895,7 +895,7 @@ pub(in crate::script) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
r#"Usage: UnitHasRelicSlot("unit")"#,
|
||||
)?);
|
||||
let has = with_unit(lua, &token, false, |u| u.has_relic_slot)?;
|
||||
Ok(predicate(has))
|
||||
Ok(flag(has))
|
||||
})?,
|
||||
)?;
|
||||
|
||||
@@ -1210,7 +1210,7 @@ pub(in crate::script) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
"IsResting",
|
||||
lua.create_function(|lua, ()| {
|
||||
let model = lua.app_data_ref::<Model>().expect("model app_data");
|
||||
Ok(predicate(model.resting))
|
||||
Ok(flag(model.resting))
|
||||
})?,
|
||||
)?;
|
||||
// PartialPlayTime() / NoPlayTime() → 1/nil: the two anti-addiction play-time regimes, read
|
||||
@@ -1223,14 +1223,14 @@ pub(in crate::script) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
"PartialPlayTime",
|
||||
lua.create_function(|lua, ()| {
|
||||
let model = lua.app_data_ref::<Model>().expect("model app_data");
|
||||
Ok(predicate(model.partial_play_time))
|
||||
Ok(flag(model.partial_play_time))
|
||||
})?,
|
||||
)?;
|
||||
g.set(
|
||||
"NoPlayTime",
|
||||
lua.create_function(|lua, ()| {
|
||||
let model = lua.app_data_ref::<Model>().expect("model app_data");
|
||||
Ok(predicate(model.no_play_time))
|
||||
Ok(flag(model.no_play_time))
|
||||
})?,
|
||||
)?;
|
||||
// GetBillingTimeRested() → the account's rested billing MINUTES, as one number, always
|
||||
@@ -1456,7 +1456,7 @@ pub(in crate::script) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
if had {
|
||||
model.target_clear = true;
|
||||
}
|
||||
Ok(predicate(had))
|
||||
Ok(flag(had))
|
||||
})?,
|
||||
)?;
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
//!
|
||||
//! **Every predicate in this family answers the number `1` or `nil`, never a Lua boolean**, and it
|
||||
//! answers it through the one helper — [`unit_predicate`], over
|
||||
//! [`super::binding_abi::predicate`]. Decision 2043; the law is 1830's, which settled the same
|
||||
//! [`super::binding_abi::flag`]. Decision 2043; the law is 1830's, which settled the same
|
||||
//! question for the widget predicates a family earlier.
|
||||
//!
|
||||
//! v1 gaps, stated not hidden: the snapshot carries only the *active* power slot (see the
|
||||
@@ -217,6 +217,24 @@ pub struct UnitState {
|
||||
/// and `0x1000`/`0x2000` (the play-time regimes) all move without touching either. Firing off
|
||||
/// the decoded pair would silently under-announce every one of them.
|
||||
pub player_flags: u32,
|
||||
/// `UNIT_DYNAMIC_FLAGS` (descriptor index 143), raw — what **`UNIT_DYNAMIC_FLAGS`** the
|
||||
/// *event* fires on, the [`Self::flags`] pattern a hundred fields over.
|
||||
///
|
||||
/// The event is id **137**, and the id is not arbitrary: the reference's generic bridge
|
||||
/// `0x51bbb0` registers one watch per unit-window field whose name-table slot is non-NULL, and
|
||||
/// the id it dispatches is that field's own window index — `137 + 6 = 143`, this field.
|
||||
/// Byte-verified here rather than taken from the note: `0xbe1198 + 137*4 = 0xbe13bc` has
|
||||
/// exactly one writer image-wide (`0x51ad8b`), and the pointer it stores resolves to
|
||||
/// `"UNIT_DYNAMIC_FLAGS"`. The watch **length** is 4 — selector byte `[0x51bc98 + 137] = 4`,
|
||||
/// jump-table entry `0x51bbf9 mov eax,4` — so the gate is a `repe cmpsb` over exactly this one
|
||||
/// dword against the object's shadow copy (`0x4655bb`), and **any** bit moving fires it.
|
||||
///
|
||||
/// Raw for the same reason [`Self::player_flags`] is, and the cost of getting it wrong is
|
||||
/// larger here: this struct decodes four of its bits ([`Self::tapped`],
|
||||
/// [`Self::tapped_by_player`], and — through the store — lootable `0x1` and dead-looking
|
||||
/// `0x20`), while `0x2` (tracked, Hunter's Mark) and `0x10` moves without touching any of
|
||||
/// them. Firing off the decoded subset would silently under-announce those.
|
||||
pub dynamic_flags: u32,
|
||||
/// The unit's owner — `UNIT_FIELD_SUMMONEDBY`, else its charmer, else its creator; `0` for
|
||||
/// nobody's. What `UnitPlayerOrPetInParty`/`InRaid` read for the "or pet" half (1958).
|
||||
pub owner: u64,
|
||||
@@ -757,7 +775,7 @@ fn with_unit<T>(
|
||||
/// Rust `bool`, and mlua pushes tag 1. That is the one shape 1.12 cannot produce: `UnitExists`
|
||||
/// (`0x515fb0`) pushes `lua_pushnumber` (`0x6f3810`, tag 3, the double `1.0`) or `lua_pushnil`
|
||||
/// (`0x6f37f0`, tag 0) and nothing else, and so does every other predicate in the family. So the
|
||||
/// whole family goes through [`super::binding_abi::predicate`] — 1830's widget law, which is the
|
||||
/// whole family goes through [`super::binding_abi::flag`] — 1830's widget law, which is the
|
||||
/// *same* law, finally applied to the unit surface (decision 2043). A predicate that hand-computes
|
||||
/// its own bool calls that helper directly; one that reads a snapshot field calls this. Both end at
|
||||
/// the one push site, which is what stops the family drifting apart again.
|
||||
@@ -770,9 +788,7 @@ fn unit_predicate(
|
||||
token: &Option<String>,
|
||||
f: impl FnOnce(&UnitState) -> bool,
|
||||
) -> mlua::Result<mlua::Value> {
|
||||
Ok(super::binding_abi::predicate(with_unit(
|
||||
lua, token, false, f,
|
||||
)?))
|
||||
Ok(super::binding_abi::flag(with_unit(lua, token, false, f)?))
|
||||
}
|
||||
|
||||
/// The `Unit*`/`GetQuestGreenRange` Lua binding registrations — split from this module's
|
||||
|
||||
@@ -361,9 +361,9 @@ mod tests {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
s.run(
|
||||
r#"WM = CreateFrame("Frame", "WM") WM:SetSize(1024, 768) WM:SetPoint("CENTER")
|
||||
r#"WM = CreateFrame("Frame", "WM") WM:SetWidth(1024); WM:SetHeight(768) WM:SetPoint("CENTER")
|
||||
Detail = CreateFrame("Frame", "WorldMapDetailFrame", WM)
|
||||
Detail:SetSize(1002, 668) Detail:SetPoint("CENTER")"#,
|
||||
Detail:SetWidth(1002); Detail:SetHeight(668) Detail:SetPoint("CENTER")"#,
|
||||
)
|
||||
.unwrap();
|
||||
s
|
||||
|
||||
@@ -259,6 +259,67 @@ impl KindState {
|
||||
}
|
||||
}
|
||||
|
||||
/// A tooltip's anchor mode — the reference's `+0x318`, which `SetOwner` writes and
|
||||
/// `GetAnchorType 0x5313e0` reads back through the 9-entry `{value, name}` table `0x531530`
|
||||
/// (wow-re `bag-portrait-and-appendtext.md` §5; the mode ids in
|
||||
/// `hover-hide-and-tooltip-owner-law.md` §4).
|
||||
///
|
||||
/// **All nine of the reference's modes are named here even though `SetOwner` cannot reach
|
||||
/// [`TooltipAnchor::Cursor`].** The variant is not decoration: mode 6 is the cursor-following
|
||||
/// tooltip, driven per frame by the class's own OnUpdate override `0x530b20` (re-anchor to
|
||||
/// `[root+0x1118/0x111c]`), and nine corpus files ask for it by name — `pfUI`'s tooltip, xpbar and
|
||||
/// chat modules, `pfQuest/browser.lua`, `TipBuddy`. Following the cursor is a mechanism this engine
|
||||
/// does not have yet, and 1203 says a verb is not stubbed to make a caller happy, so
|
||||
/// `tooltip::verbs`'s `SetOwner` still warns on the string and keeps its previous placement rather
|
||||
/// than silently pretending. Naming the variant is what keeps that gap visible instead of implicit.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
pub enum TooltipAnchor {
|
||||
/// `ANCHOR_LEFT` — the plate's right edge on the owner's left.
|
||||
Left,
|
||||
/// `ANCHOR_RIGHT` — the plate's left edge on the owner's right; the stock UI's overwhelming
|
||||
/// default (87 of the 123 `ANCHOR_*` mentions in 1.12's FrameXML).
|
||||
Right,
|
||||
/// `ANCHOR_BOTTOMLEFT`.
|
||||
BottomLeft,
|
||||
/// `ANCHOR_BOTTOMRIGHT`.
|
||||
BottomRight,
|
||||
/// `ANCHOR_TOPLEFT`.
|
||||
TopLeft,
|
||||
/// `ANCHOR_TOPRIGHT`.
|
||||
TopRight,
|
||||
/// `ANCHOR_CURSOR` — mode 6, the plate re-anchored to the live cursor every frame. Reachable
|
||||
/// in the reference and NOT constructible here; see the type doc.
|
||||
Cursor,
|
||||
/// `ANCHOR_NONE` — mode 7: the owner is recorded and no `SetPoint` is applied, leaving the
|
||||
/// caller to point the plate (`GameTooltip_SetDefaultAnchor` does exactly that on the next
|
||||
/// line). **The default**, because it is the mode the reference's own `SetOwner` core stores
|
||||
/// when the caller passes no anchor string (`0x530012`), and the only one of the nine that is
|
||||
/// true of a plate nothing has anchored yet.
|
||||
#[default]
|
||||
None,
|
||||
/// `ANCHOR_PRESERVE` — mode 8: the owner is recorded and the previous placement is kept
|
||||
/// (`0x52fe90` skips the anchor-apply). Stored like any other mode, so `GetAnchorType` answers
|
||||
/// `"ANCHOR_PRESERVE"` after one — the reference does not resolve it back to what it preserved.
|
||||
Preserve,
|
||||
}
|
||||
|
||||
impl TooltipAnchor {
|
||||
/// The reference's own spelling, as `GetAnchorType()` answers it (table `0x531530`).
|
||||
pub const fn name(self) -> &'static str {
|
||||
match self {
|
||||
TooltipAnchor::Left => "ANCHOR_LEFT",
|
||||
TooltipAnchor::Right => "ANCHOR_RIGHT",
|
||||
TooltipAnchor::BottomLeft => "ANCHOR_BOTTOMLEFT",
|
||||
TooltipAnchor::BottomRight => "ANCHOR_BOTTOMRIGHT",
|
||||
TooltipAnchor::TopLeft => "ANCHOR_TOPLEFT",
|
||||
TooltipAnchor::TopRight => "ANCHOR_TOPRIGHT",
|
||||
TooltipAnchor::Cursor => "ANCHOR_CURSOR",
|
||||
TooltipAnchor::None => "ANCHOR_NONE",
|
||||
TooltipAnchor::Preserve => "ANCHOR_PRESERVE",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A `GameTooltip`'s runtime state (decision 0274). The line *text/color/wrap* is not duplicated
|
||||
/// here — each line pair is a real named FontString region (`<name>TextLeftN`/`TextRightN`,
|
||||
/// engine-created on demand, published as Lua globals exactly like the real template's 30
|
||||
@@ -282,6 +343,10 @@ pub struct TooltipState {
|
||||
/// `SetOwner`'s frame — dropped on hide (`IsOwned` is the hover re-enter loop's gate,
|
||||
/// ref `ContainerFrame.lua` OnUpdate).
|
||||
pub owner: Option<FrameHandle>,
|
||||
/// The anchor mode the last `SetOwner` placed this plate by, read back by `GetAnchorType()` —
|
||||
/// the reference's `+0x318` (wow-re `bag-portrait-and-appendtext.md` §5,
|
||||
/// `hover-hide-and-tooltip-owner-law.md` §4).
|
||||
pub anchor: TooltipAnchor,
|
||||
/// `SetMinimumWidth(w)` — a floor on the auto-sized width (the ref's money-row floor).
|
||||
/// Cleared (0.0) by `ClearLines`/hide, like the content.
|
||||
pub min_width: f32,
|
||||
@@ -407,7 +472,7 @@ pub struct ModelState {
|
||||
/// written by the engine because the pane authored no size (decision 2015). The geometry
|
||||
/// getters `0x76d080`/`0x76d0d0` answer it whenever no size is authored; here it is written
|
||||
/// into the layout input when the file's facts land and re-derived when the screen's aspect
|
||||
/// moves, and an authored `SetWidth`/`SetHeight`/`SetSize` clears it for good.
|
||||
/// moves, and an authored `SetWidth`/`SetHeight` clears it for good.
|
||||
pub implicit_size: bool,
|
||||
/// The pane's yaw in radians — `CSimpleModel+0x39c`. **One slot, written by two verbs on two
|
||||
/// different classes**: `Model:SetFacing` (`0x76dce0`) and `PlayerModel:SetRotation`
|
||||
@@ -961,15 +1026,29 @@ pub const MINIMAP_DEFAULT_PLAYER_MODEL: &str = "Interface\\Minimap\\MinimapArrow
|
||||
|
||||
/// A `CSimpleScrollFrame`'s runtime state: the frame whose anchors are overridden to track the
|
||||
/// scroll offset ([`crate::script::UiScript::resolve`]'s scroll-child override), and the current
|
||||
/// vertical scroll position. `SetVerticalScroll` stores the offset VERBATIM — the reference's
|
||||
/// `0x786db0` never reads the range (decision 2017) — and the range is always computed live from
|
||||
/// the resolved rects (never cached here), so this struct carries only the two members the
|
||||
/// client's `SetScrollChild`/`SetVerticalScroll` actually set (`[+0x318]`, `[+0x328]`).
|
||||
/// scroll position on each axis. `SetVerticalScroll` stores the offset VERBATIM — the reference's
|
||||
/// `0x786db0` never reads the range (decision 2017) — and the ranges are always computed live from
|
||||
/// the resolved rects (never cached here), so this struct carries only the three members the
|
||||
/// client's `SetScrollChild`/`SetHorizontalScroll`/`SetVerticalScroll` actually set (`[+0x318]`,
|
||||
/// `[+0x324]`, `[+0x328]`).
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub struct ScrollFrameState {
|
||||
/// The scroll child (`SetScrollChild`) — the one frame whose content pans within this frame's
|
||||
/// rect. `None` = no child (nothing to clip or offset).
|
||||
pub child: Option<FrameHandle>,
|
||||
/// The horizontal scroll offset in px (`SetHorizontalScroll`), unclamped — the reference's
|
||||
/// `[+0x324]`, whose setter `0x786d30` is the byte-clone of the vertical one's `0x786db0`.
|
||||
///
|
||||
/// **The sign is the mirror of the vertical one below, and that is the reference's, not a
|
||||
/// slip.** `0x787100` re-anchors the child as
|
||||
/// `SetPoint(TOPLEFT, self, TOPLEFT, +[+0x324], +[+0x328])` — both offsets handed over raw,
|
||||
/// neither negated (wow-re `scrollframe-offset-and-range-law.md` §2.3/§6.2). x grows right, so
|
||||
/// a POSITIVE horizontal offset pushes the child right and reveals nothing new; scrolling
|
||||
/// right takes a NEGATIVE one, where scrolling down takes a positive vertical.
|
||||
/// `aux-addon/tabs/search/filter.lua:315-322` corroborates it from the caller's side: it
|
||||
/// bounds x into `[min(0, frameWidth - contentWidth - 10), 0]` and y into
|
||||
/// `[0, contentHeight - frameHeight]`.
|
||||
pub horizontal: f32,
|
||||
/// The vertical scroll offset in px (`SetVerticalScroll`), unclamped — the reference's
|
||||
/// `[+0x328]`. XML y-positive-up: a positive offset lifts the child
|
||||
/// (`child.top = scrollframe.top + vertical`), bringing content below the fold into view. The
|
||||
@@ -1480,9 +1559,6 @@ pub struct SliderState {
|
||||
/// `true` = VERTICAL (the ctor default; value maps along the track's height, min at the top),
|
||||
/// `false` = HORIZONTAL (`orientation`; shared enum `0x811b00` HORIZONTAL=0/VERTICAL=1).
|
||||
pub vertical: bool,
|
||||
/// `Enable`/`Disable` (`IsEnabled`). A disabled slider does not respond to thumb drag; the ctor
|
||||
/// enables it (the interactive-widget ctors take mouse — Button/EditBox/ScrollFrame do too).
|
||||
pub enabled: bool,
|
||||
/// The thumb texture region (`SetThumbTexture`/`<ThumbTexture>`), created on first set. A
|
||||
/// renderer positions this region's rect at [`Self::fraction`] along the orientation axis.
|
||||
pub thumb: Option<RegionHandle>,
|
||||
@@ -1498,7 +1574,6 @@ impl Default for SliderState {
|
||||
has_value: false,
|
||||
step: 0.0,
|
||||
vertical: true,
|
||||
enabled: true,
|
||||
thumb: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -177,7 +177,7 @@ pub use kinds::{
|
||||
ButtonState, ButtonVisualState, ColorSelectState, EditAction, EditBoxState, EditOutcome,
|
||||
EditUnit, FrameKind, InsertMode, KindState, MessageFrameState, MessageLine, MinimapState,
|
||||
ModelFileFacts, ModelFog, ModelLight, ModelPlayHead, ModelState, RegionKind, ScrollFrameState,
|
||||
ScrollingMessageState, SequenceFacts, SliderState, StatusBarState, TooltipState,
|
||||
ScrollingMessageState, SequenceFacts, SliderState, StatusBarState, TooltipAnchor, TooltipState,
|
||||
MINIMAP_DEFAULT_ARROW_MODEL, MINIMAP_DEFAULT_MASK, MINIMAP_DEFAULT_PLAYER_MODEL,
|
||||
MINIMAP_DEFAULT_ZOOM, MINIMAP_ENGINE_CHILDREN, MINIMAP_ZOOM_LEVELS, TOOLTIP_DOUBLE_GAP,
|
||||
TOOLTIP_FADE_SECS, TOOLTIP_LINE_GAP, TOOLTIP_PAD, TOOLTIP_WRAP_WIDTH,
|
||||
|
||||
Reference in New Issue
Block a user