Files
benilla-octo/crates/benilla-ui/src/loader/geometry.rs
T
SamandClaude Fable 5 a0d2fc383b the world map blips are the reference's, and particles go untinted
- the world map speaks the reference's names: WorldMapTooltip is declared (six corpus addons index it; pfUI dies on it at file scope), the blackout frame takes the reference's spelling BlackoutWorld, and the four hand-written party blips become the reference's own unit-blip templates — all 44 blips as instances, each with its published $parentIcon, the name pfUI recolours group blips by class through; the 40 raid ones are declared but not positioned yet, named in the OUT list rather than faked
- the templates addons build their own buttons from are published: ItemButtonTemplate, ContainerFrameItemButtonTemplate and the bag-slot chain — most of the handler scripts were live the whole time (this client executes the reference's own container and paper-doll code off the player's patch chain), so what was missing was mostly declarations, not behaviour — the bank arm stays out and is named as the omission it is; OptionsFrameSliders becomes real, with the three Graphics rows reading it and re-reading on show, so an addon's write to a slider's bounds actually moves the slider; and the template census that had ranked these at "zero demand" learns to see a local bound to a literal — its table went from 3 rows to 7
- the bindings answer in 1.12's own shapes: Button:IsEnabled() answers the NUMBER 1 or 0 (0 is truthy in Lua, so the reference's own == 0 tests got false forever, and three of our files had hand-worked around the divergence rather than fixed it); the constructors' string arguments are four shapes, not one rule — pfUI passes a Button where a template name goes and the reference constructs the FontString and ignores it; SetInventoryItem returns THREE values, the reference's own paper doll destructuring repairCost; UnitIsVisible is object presence and nothing else (57 bytes, one branch, no distance — an out-of-range party member is exists-yes, visible-nil); the tapped pair reads the two dynamic-flags bits, and its repaint wire fires UNIT_FACTION on the tap bit — the half a passing predicate test never sees; UnitIsPartyLeader is two ORed legs covering disjoint failures (a held stranger leading their own party, and an out-of-range member with no object to read a flag from); and the four nameplate verbs take no argument and return zero values
- the ghost's wisps compose onto its body instead of over it — the four emitters drew at full strength over a body at alpha 0.5, and now inherit the instance alpha down the attachment chain the way the reference recomposes it per frame; the residual we had parked ("the wisps should inherit the tint too") is REFUTED at the bytes: a particle batch commits hardcoded white RGB, and even that never reaches the fragment — particles are never tinted, only the alpha crosses, applied to the alpha byte alone — so the shipped shader split was already right and no code changes

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-31 20:15:16 +07:00

181 lines
9.3 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use mlua::Table;
use crate::framexml::{self, Element};
use super::{abs_dim, children_named, Loader};
impl Loader<'_> {
/// LoadXML attribute handling (rf24 `0x769820`): the subset the v1 object model exposes, plus a
/// warn-once for the attributes whose methods don't exist yet (a next-phase gap, not a workaround).
pub(super) fn apply_attrs(&mut self, el: &Element, wrapper: &Table, dbg: &str) {
if el.attr_bool("hidden") {
self.call(wrapper, "Hide", (), dbg);
}
if let Some(strata) = el.attr("frameStrata") {
self.call(wrapper, "SetFrameStrata", strata.to_string(), dbg);
}
if let Some(level) = el.attr("frameLevel") {
if let Ok(n) = level.parse::<i64>() {
self.call(wrapper, "SetFrameLevel", n, dbg);
}
}
if let Some(alpha) = el.attr("alpha") {
if let Ok(a) = alpha.parse::<f32>() {
self.call(wrapper, "SetAlpha", a, dbg);
}
}
if let Some(id) = el.attr("id") {
if let Ok(n) = id.parse::<i64>() {
self.call(wrapper, "SetID", n, dbg);
}
}
// `clampedToScreen="true"` → SetClampedToScreen (rf24 `0x768cc0` — geometry flags bit4,
// the layout resolve's screen clamp; GameTooltip frames carry it by construction).
if el.attr_bool("clampedToScreen") {
self.call(wrapper, "SetClampedToScreen", true, dbg);
}
// `enableMouse` lands as the real EnableMouse call (the hit test gates on it —
// pointer.rs): a ref frame authored mouse-blocking (StaticPopup, BlackoutWorld, the
// minimap cluster) must actually capture. The loader ignored it for a while after the
// native landed — the stale "v1 gap" note here — which left those frames click-through
// and the minimap deaf to its ping click (0434 phase 6c's root cause).
if el.attr_bool("enableMouse") {
self.call(wrapper, "EnableMouse", true, dbg);
}
// `<HitRectInsets><AbsInset left= right= top= bottom=/></HitRectInsets>` (also accepted
// inline on the element) → SetHitRectInsets: the frame's MOUSE rect, inset from its
// resolved rect. Absent sides read 0, so a partial element insets only what it names.
if let Some(ins) = children_named(el, "HitRectInsets").next() {
let src = children_named(ins, "AbsInset").next().unwrap_or(ins);
let side = |k: &str| {
src.attr(k)
.and_then(|v| v.trim().parse::<f32>().ok())
.unwrap_or(0.0)
};
self.call(
wrapper,
"SetHitRectInsets",
(side("left"), side("right"), side("top"), side("bottom")),
dbg,
);
}
// `<ResizeBounds><minResize><AbsDimension x= y=/></minResize><maxResize>…` → the resize
// quad (rf24 `0x769820`'s child-loop arm at `0x769baa`; wow-re
// `system/ui/scratch/resize-bounds-and-button-fontstring.md` §4). Three clauses from that
// carve rather than from the shape of the element:
//
// · **Both pairs are written unconditionally once `<ResizeBounds>` matches**, each side
// defaulting to `0` before its lookup — so a block carrying only `<minResize>` RESETS
// the max pair to unbounded. That is why this writes both calls, never just the one it
// found.
// · `x` is the WIDTH and `y` the HEIGHT, through the same logical→internal transform
// `<Size>` uses — the same `abs_dim` reader, and the same inline-or-`AbsDimension` pair
// of forms.
// · `<Size>` and `<Anchors>` are consumed by the base-class `CLayoutFrame::LoadXML`
// *before* the child loop is entered at all, so they always precede this structurally
// whatever the document order — and neither path clamps, so an authored `<Size>`
// outside the authored bounds survives load unchanged.
//
// The shipped 1.12.1 FrameXML uses it exactly once (`FloatingChatFrame.xml:223`,
// min 296×75 / max 608×400) and never calls the Lua setters at all.
if let Some(rb) = children_named(el, "ResizeBounds").next() {
for (tag, verb) in [("minResize", "SetMinResize"), ("maxResize", "SetMaxResize")] {
let (x, y) = children_named(rb, tag).next().map_or((None, None), abs_dim);
self.call(wrapper, verb, (x.unwrap_or(0.0), y.unwrap_or(0.0)), dbg);
}
}
// `movable`/`resizable` → SetMovable/SetResizable — the same flag word the methods write
// (`0x76a3c0` with mask 0x100 / 0x200; wow-re `rf24-framexml-loader.md` records the loader
// calling that very setter). Both were in the gap list below until the movable family
// landed; leaving them there would have left every `movable="true"` reference window
// undraggable while `SetMovable` worked from Lua.
if el.attr_bool("movable") {
self.call(wrapper, "SetMovable", true, dbg);
}
if el.attr_bool("resizable") {
self.call(wrapper, "SetResizable", true, dbg);
}
// `toplevel` → SetToplevel — the third bit of that same flag word (`0x76a3c0` mask `0x1`,
// XML site `0x7698ec`; wow-re `ui/scratch/toplevel-raise.md`), and the third attribute to
// graduate out of the gap list below for the same reason: the raise law is built
// (`script::object::toplevel`), so accepting the attribute now means the behaviour, not
// silence. 82 corpus addons and thirteen of our own frames declare it.
if el.attr_bool("toplevel") {
self.call(wrapper, "SetToplevel", true, dbg);
}
// `enableKeyboard="true"` — the XML half of the flag, which enables BOTH key kinds
// (`scripts-auto-enable.md` §1-2). The flag is now real and round-trips; key delivery is
// still not gated on it, which the method's own doc states rather than this warning.
if el.attr_bool("enableKeyboard") {
self.call(wrapper, "EnableKeyboard", true, dbg);
}
}
/// `<Size>` → SetWidth/SetHeight (rf24 `0x767800`). Accepts either `<Size><AbsDimension x= y=/>`
/// or the inline `<Size x= y=/>` form; a dimension that's absent is left untouched (the client's
/// "0 = derive"). ALL `<Size>` children apply, in document order — template expansion appends the
/// instance's children after the template's ([`crate::framexml::expand`]), and the client simply
/// processes each child in turn, so an instance's own `<Size>` overwrites its template's. (Taking
/// only `.next()` here silently pinned every templated frame to the TEMPLATE's size; caught on
/// the quest log's Abandon button — 125×21 in the instance XML, 80×22 on screen.)
pub(super) fn apply_size(&mut self, el: &Element, wrapper: &Table, dbg: &str) {
for size in children_named(el, "Size") {
let (x, y) = abs_dim(size);
if let Some(w) = x {
self.call(wrapper, "SetWidth", w, dbg);
}
if let Some(h) = y {
self.call(wrapper, "SetHeight", h, dbg);
}
}
}
/// `<Anchors>` → SetPoint per `<Anchor point= relativeTo= relativePoint=><Offset .../></Anchor>`
/// (rf24 `0x767800`). `relativePoint` defaults to `point`; `relativeTo` is `$parent`-substituted
/// (rf27) and passed by name (the object model resolves it, falling back to the parent when
/// absent/unresolved); a missing `point` is skipped with a warning ("Invalid anchor point").
/// The `setAllPoints="true"` shorthand applies first, like the region path — a frame carrying
/// only the attribute (WorldMapFrame's chrome layers) pins TOPLEFT+BOTTOMRIGHT to its parent.
pub(super) fn apply_anchors(
&mut self,
el: &Element,
wrapper: &Table,
parent_name: &str,
dbg: &str,
) {
if el.attr_bool("setAllPoints") {
self.call(wrapper, "SetAllPoints", (), dbg);
}
for anchors in children_named(el, "Anchors") {
for anchor in children_named(anchors, "Anchor") {
let Some(point) = anchor.attr("point") else {
self.report
.warnings
.push(format!("{dbg}: <Anchor> without a point; skipped"));
continue;
};
let rel_point = anchor.attr("relativePoint").unwrap_or(point).to_string();
let rel_to: Option<String> = anchor
.attr("relativeTo")
.map(|r| framexml::resolve_name(r, parent_name));
let (x, y) = children_named(anchor, "Offset")
.next()
.map(abs_dim)
.unwrap_or((None, None));
self.call(
wrapper,
"SetPoint",
(
point.to_string(),
rel_to,
rel_point,
x.unwrap_or(0.0),
y.unwrap_or(0.0),
),
dbg,
);
}
}
}
}