- the four extra action bars become per-character options, off by default, on a new Action Bars options page — the toggles ride the server's own byte (`CMSG_SET_ACTIONBAR_TOGGLES` into `PLAYER_FIELD_BYTES`), so a fresh character's zero is the default for free; turning the bottom-left bar off gives the stance bar its previously unbuilt unraised look, end-caps and 64×64 rings - bag windows stop drawing straight through a raised bottom bar: the bottom band has ONE owner — the manage pass — the bag stack joins it through a listener seam, and a guard test walks all eight raisable-bar combinations across 24 bar/tenant pairs and refuses overlap - the item-push card takes the reference's real geometry, closing the gap 0887 measured - resizable addon windows stop at their bounds instead of pinning and walking across the screen: four per-frame f32s with 0.0 the disable sentinel, the clamp on the interactive drag only with the residual rebated out of the drag delta, `<ResizeBounds>` in XML honoured — and `SetFontString`'s three raises land exactly as the bytes have them Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
181 lines
9.3 KiB
Rust
181 lines
9.3 KiB
Rust
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, WorldMapBlackout, 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,
|
||
);
|
||
}
|
||
}
|
||
}
|
||
}
|