Files
benilla-octo/crates/benilla-ui/src/loader/geometry.rs
T
SamandClaude Fable 5 348f55e37e four extra action bars as options, and the bottom band gets one owner
- 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>
2026-08-21 18:12:19 +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, 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,
);
}
}
}
}