- addons load in the order NTFS lists a folder, case-insensitive with an underscore before the letters, the order the vanilla corpus was written against and the one that decides which addon provides a shared library (2178) - GetNumAddOns and the index form of every AddOn verb walk a title-sorted list that hides the Blizzard addons the server says to hide, and the character-select AddOns list is in title order (2175) - SetPoint and SetAllPoints raise the reference's own errors for a frame name that does not exist or an omitted relative frame instead of quietly anchoring to the parent, and an explicit nil means the screen root (2176) - a tooltip owned with ANCHOR_CURSOR follows the mouse, which pfUI's tooltip, xpbar and chat modules, pfQuest's browser and TipBuddy ask for; an omitted or unknown anchor is ANCHOR_LEFT (2176) - a $parent name handed to CreateFrame, CreateTexture or CreateFontString expands as it does in XML, so FonzAppraiser's thirty dropdowns and pfQuest's search icon get the names they asked for (2176) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
284 lines
12 KiB
Rust
284 lines
12 KiB
Rust
//! Region method-table cluster: **layout** — size, anchors and the resolved-rect readers.
|
|
//! Split out of `region.rs` at the 0716 file-size budget.
|
|
|
|
use mlua::{Lua, MultiValue, Table, Value};
|
|
|
|
use crate::layout::{Anchor, Point};
|
|
use crate::script::object::anchor_args::{parse_set_all_points, resolve_rel_target};
|
|
use crate::script::object::{anchor_bits_eq, frame_wrapper, point_name};
|
|
use crate::script::{Model, SCREEN};
|
|
|
|
/// Resolve `self` (a region wrapper) to its live [`RegionHandle`].
|
|
use super::{
|
|
measured_wh, region_handle_of, region_ladder_context, region_owner_id, region_set_point,
|
|
size_bits_eq,
|
|
};
|
|
|
|
/// Populate `m`'s layout methods (see the module doc).
|
|
pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
|
// Region explicit size — fills the axes the region's anchors don't pin (unread under an
|
|
// implicit SetAllPoints's two corners; decision 1310).
|
|
m.set(
|
|
"SetWidth",
|
|
lua.create_function(|lua, (this, w): (Table, 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, d.size.map_or(0.0, |s| s.1)));
|
|
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(())
|
|
})?,
|
|
)?;
|
|
|
|
m.set(
|
|
"SetHeight",
|
|
lua.create_function(|lua, (this, h): (Table, 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((d.size.map_or(0.0, |s| s.0), 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",
|
|
lua.create_function(|lua, this: Table| Ok(measured_wh(lua, &this)?.0))?,
|
|
)?;
|
|
|
|
m.set(
|
|
"GetHeight",
|
|
lua.create_function(|lua, this: Table| Ok(measured_wh(lua, &this)?.1))?,
|
|
)?;
|
|
|
|
// GetLeft/GetRight/GetTop/GetBottom — the region's RESOLVED edges in its OWNER's units (y-up;
|
|
// screen ÷ the owner's effective scale, the frame twin's law in `object/layout_methods.rs` —
|
|
// a region shares its owner's scale, and a texture inside the scaled world map answered
|
|
// screen units here while its owner answered local ones, decision 1985). Every drawable
|
|
// region carries anchors (authored or the creation-path implicit anchor, decision 1310) and
|
|
// reads its resolved rect; a templateless Lua region nobody anchored never resolves → nil,
|
|
// same as pre-resolve.
|
|
for (name, pick) in [
|
|
("GetLeft", 0u8),
|
|
("GetRight", 1u8),
|
|
("GetTop", 2u8),
|
|
("GetBottom", 3u8),
|
|
] {
|
|
m.set(
|
|
name,
|
|
lua.create_function(move |lua, this: Table| {
|
|
let rh = region_handle_of(lua, &this)?;
|
|
let model = lua.app_data_ref::<Model>().expect("model");
|
|
let inv = 1.0 / owner_scale(&model, rh);
|
|
Ok(model.region_resolved.get(&rh).map(|r| {
|
|
inv * match pick {
|
|
0 => r.left,
|
|
1 => r.right,
|
|
2 => r.top,
|
|
_ => r.bottom,
|
|
}
|
|
}))
|
|
})?,
|
|
)?;
|
|
}
|
|
|
|
// Region anchors: SetPoint/ClearAllPoints/SetAllPoints mirror the frame versions
|
|
// ([`super::object`]) but write [`super::RegionData::anchors`]. An unspecified `relativeTo`
|
|
// defaults to the **owner frame**; a named one may be a frame or a sibling region (the real
|
|
// XML anchors regions to sibling regions everywhere — merchant label plate → `$parentSlot`).
|
|
m.set(
|
|
"SetPoint",
|
|
lua.create_function(|lua, (this, rest): (Table, MultiValue)| {
|
|
region_set_point(lua, &this, &rest)
|
|
})?,
|
|
)?;
|
|
|
|
m.set(
|
|
"ClearAllPoints",
|
|
lua.create_function(|lua, this: Table| {
|
|
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();
|
|
// Names its node, the frame twin's rule and for the frame twin's reason (decision
|
|
// 2114): clearing every anchor is a retarget onto an EMPTY target list, so the cached
|
|
// graph's edges are unlinked rather than the whole graph re-derived.
|
|
let old: Option<Vec<u32>> =
|
|
(!d.anchors.is_empty()).then(|| d.anchors.iter().map(|a| a.relative_to).collect());
|
|
d.anchors.clear();
|
|
if let Some(old) = old {
|
|
model.touch_layout_retarget_region(rh, &old, &[]);
|
|
}
|
|
Ok(())
|
|
})?,
|
|
)?;
|
|
|
|
m.set(
|
|
"SetAllPoints",
|
|
lua.create_function(|lua, (this, rest): (Table, MultiValue)| {
|
|
let rh = region_handle_of(lua, &this)?;
|
|
// `who`/`$parent` first, then the `_G` read, then the guard — `region_ladder_context`.
|
|
let (who, base) = region_ladder_context(lua, rh);
|
|
let target = parse_set_all_points(lua, rest.front(), &base);
|
|
let mut model = lua.app_data_mut::<Model>().expect("model");
|
|
let me = model.region_id(rh);
|
|
let owner = region_owner_id(&mut model, rh);
|
|
let rel_id = resolve_rel_target(&model, &target, &who, "SetAllPoints", me, owner)?;
|
|
let pair = [
|
|
Anchor::new(Point::TopLeft, rel_id, Point::TopLeft, 0.0, 0.0),
|
|
Anchor::new(Point::BottomRight, rel_id, Point::BottomRight, 0.0, 0.0),
|
|
];
|
|
let data = model.region_data.entry(rh).or_default();
|
|
let same = data.anchors.len() == 2
|
|
&& data
|
|
.anchors
|
|
.iter()
|
|
.zip(&pair)
|
|
.all(|(a, b)| anchor_bits_eq(a, b));
|
|
if !same {
|
|
data.anchors.clear();
|
|
data.anchors.extend_from_slice(&pair);
|
|
model.touch_layout();
|
|
}
|
|
Ok(())
|
|
})?,
|
|
)?;
|
|
|
|
// ── The rest of the Region map `0xcf54b4` (wow-re `font-object-lua-surface.md`) ──────────────
|
|
//
|
|
// These four landed together because the MAP is the unit, not the name. `SetParent` shipped
|
|
// alone when one addon line named it, and its own getter stayed missing for months — which is
|
|
// how `TheoryCraft\TheoryCraftUI.lua:720` (`buttontext:GetParent()`, a FontString) died every
|
|
// session, and how the per-kind census came to read `115 GetParent (missing on Texture,
|
|
// FontString)`. The set is closed and byte-verified, so it is asserted as a set in
|
|
// `tests/reference_surface.rs` rather than grown a name at a time.
|
|
|
|
// GetParent() → the OWNER frame's wrapper. A region always has one (`region_owner_id` falls
|
|
// back to the owner for every unresolved case), so unlike the frame twin this never answers nil.
|
|
m.set(
|
|
"GetParent",
|
|
lua.create_function(|lua, this: Table| {
|
|
let rh = region_handle_of(lua, &this)?;
|
|
let owner = {
|
|
let mut model = lua.app_data_mut::<Model>().expect("model");
|
|
region_owner_id(&mut model, rh)
|
|
};
|
|
frame_wrapper(lua, owner)
|
|
})?,
|
|
)?;
|
|
|
|
// GetCenter() → the resolved rect's midpoint, or a nil PAIR before the first resolve — the same
|
|
// contract, and the same source, as the GetLeft/GetRight/GetTop/GetBottom readers above.
|
|
//
|
|
// **Deliberately unscaled, where the frame twin divides by GetEffectiveScale.** The region edge
|
|
// readers report raw resolved units; scaling only the centre would make `GetCenter()` disagree
|
|
// with `(GetLeft() + GetRight()) / 2` on any scaled subtree — a contradiction inside one method
|
|
// table is worse than a missing division, and regions have no scale of their own to divide by.
|
|
m.set(
|
|
"GetCenter",
|
|
lua.create_function(|lua, this: Table| {
|
|
let rh = region_handle_of(lua, &this)?;
|
|
let model = lua.app_data_ref::<Model>().expect("model");
|
|
let inv = 1.0 / owner_scale(&model, rh);
|
|
Ok(match model.region_resolved.get(&rh) {
|
|
Some(r) => (
|
|
Value::Number(f64::from(inv * (r.left + r.right) * 0.5)),
|
|
Value::Number(f64::from(inv * (r.bottom + r.top) * 0.5)),
|
|
),
|
|
None => (Value::Nil, Value::Nil),
|
|
})
|
|
})?,
|
|
)?;
|
|
|
|
// GetNumPoints() → how many anchors this region carries. Absent on our FRAMES too, which is the
|
|
// same drift one table up; this side is what the corpus named.
|
|
m.set(
|
|
"GetNumPoints",
|
|
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(0, |d| d.anchors.len() as i64))
|
|
})?,
|
|
)?;
|
|
|
|
// GetPoint([n]) → point, relativeTo, relativePoint, xOfs, yOfs — the n-th (1-based, default
|
|
// first) anchor, mirroring the frame twin including its out-of-range answer (five nils).
|
|
//
|
|
// **The relativeTo dispatch is the part a frame-shaped copy gets wrong.** Region anchors live in
|
|
// ONE id space with frames and may target a sibling REGION — the real XML does it everywhere
|
|
// (`region.rs`'s `resolve_target`: frames first, then the region-name registry). So the id is
|
|
// matched against both tables and answered with the matching wrapper kind; handing back a frame
|
|
// wrapper for a region id would be a working-looking handle onto the wrong object.
|
|
m.set(
|
|
"GetPoint",
|
|
lua.create_function(|lua, (this, n): (Table, Option<i64>)| {
|
|
let rh = region_handle_of(lua, &this)?;
|
|
let anchor = {
|
|
let model = lua.app_data_ref::<Model>().expect("model");
|
|
let idx = (n.unwrap_or(1).max(1) - 1) as usize;
|
|
model
|
|
.region_data
|
|
.get(&rh)
|
|
.and_then(|d| d.anchors.get(idx))
|
|
.cloned()
|
|
};
|
|
let Some(a) = anchor else {
|
|
return Ok((Value::Nil, Value::Nil, Value::Nil, Value::Nil, Value::Nil));
|
|
};
|
|
let rel = {
|
|
let is_frame = {
|
|
let model = lua.app_data_ref::<Model>().expect("model");
|
|
model.id_to_frame.contains_key(&a.relative_to)
|
|
};
|
|
if a.relative_to == SCREEN {
|
|
Value::Nil
|
|
} else if is_frame {
|
|
Value::Table(frame_wrapper(lua, a.relative_to)?)
|
|
} else {
|
|
Value::Table(super::region_wrapper(lua, a.relative_to)?)
|
|
}
|
|
};
|
|
Ok((
|
|
Value::String(lua.create_string(point_name(a.point))?),
|
|
rel,
|
|
Value::String(lua.create_string(point_name(a.relative_point))?),
|
|
Value::Number(f64::from(a.x_off)),
|
|
Value::Number(f64::from(a.y_off)),
|
|
))
|
|
})?,
|
|
)?;
|
|
Ok(())
|
|
}
|
|
|
|
/// The region's owner frame's effective scale (1 for an orphan) — the divisor that turns a
|
|
/// resolved screen rect into the owner's units, the space every region getter answers in.
|
|
fn owner_scale(model: &Model, rh: crate::widget::RegionHandle) -> f32 {
|
|
model
|
|
.arena
|
|
.region(rh)
|
|
.map(|r| crate::script::object::eff_scale(model, r.owner))
|
|
.unwrap_or(1.0)
|
|
}
|