Anchors were a list: SetPoint re-applied an unchanged anchor, GetPoint counted in the order points were set and answered five nils past the last, and the cursor tooltip replaced every anchor each frame. In 1.12.1 a frame holds one anchor per point. SetPoint (`0x767c70`) returns at `0x767d5e` with no store and no layout queue when that point's anchor has the same target and relative point and both offsets differ by under 2⁻²² layout units (`[0x8029d4]`). GetPoint (`0x7a2340`) walks the nine slots in point order and pushes nothing past the last (`0x7a240d`). The `ANCHOR_CURSOR` update sets BOTTOM alone (`0x530b20`). Anchors are now nine slots, and SetPoint, GetPoint and the cursor update follow those rules. Tests: an unchanged re-set and an in-threshold offset touch no layout, GetPoint order and indexes, a cursor tooltip keeps an addon's second point. Fixes #926 Fixes #131 Fixes #879 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
228 lines
8.7 KiB
Rust
228 lines
8.7 KiB
Rust
//! The argument ladder of `SetPoint` (`0x7a2540`) and `SetAllPoints` (`0x7a2940`): the reference
|
|
//! registers both once, in the Region method table (`0x87c9b8`), so every widget type runs the
|
|
//! same function. Every failure leg raises; none falls back to the parent or does nothing.
|
|
|
|
use mlua::{Lua, MultiValue, Table, Value};
|
|
|
|
use crate::layout::Point;
|
|
use crate::script::{Model, SCREEN};
|
|
|
|
use super::{decode_id, point_from_str, prefetch_named_target, resolve_named_target, NamedTarget};
|
|
|
|
/// The name the reference prints for a widget that has none (the literal at `0x84c7f0`).
|
|
pub(crate) const UNNAMED: &str = "<unnamed>";
|
|
|
|
/// `relativeTo` by the four tags `lua_type(L,3)` accepts (`0x7a25e6`..`0x7a2620`); any other tag,
|
|
/// absent included, raises Usage in `SetPoint`.
|
|
pub(crate) enum RelTarget {
|
|
/// A widget wrapper table (`0x7a26c9`); the reference runs no `IsA` check on it.
|
|
Wrapper(Table),
|
|
/// A name string, already read out of `_G` with no `Model` guard alive ([`NamedTarget`]).
|
|
Named(NamedTarget),
|
|
/// An explicit `nil`: the screen root `ds:[0xcf0bd8]` (`0x7a2710`), not the parent.
|
|
Screen,
|
|
/// A number: the `SetPoint(point, x, y)` shorthand (`0x7a270e`), against the default parent.
|
|
DefaultParent,
|
|
}
|
|
|
|
/// `SetPoint`'s parsed arguments (`0x7a2540`), ready for the caller's own commit.
|
|
pub(crate) struct PointArgs {
|
|
pub(crate) point: Point,
|
|
pub(crate) target: RelTarget,
|
|
pub(crate) rel_point: Point,
|
|
pub(crate) x: f32,
|
|
pub(crate) y: f32,
|
|
}
|
|
|
|
/// The reference's `Usage:` raise (`0x87cc28`).
|
|
fn usage(who: &str) -> mlua::Error {
|
|
mlua::Error::runtime(format!(
|
|
"Usage: {who}:SetPoint(\"point\" [, region or nil] [, \"relativePoint\"] \
|
|
[, offsetX, offsetY])"
|
|
))
|
|
}
|
|
|
|
/// `0x87cd04`: the point-name scan `0x6f1840` rejected `point` or a string `relativePoint`.
|
|
fn unknown_point(who: &str) -> mlua::Error {
|
|
mlua::Error::runtime(format!("{who}:SetPoint(): Unknown region point"))
|
|
}
|
|
|
|
/// `0x87ccd4` for `SetPoint`, `0x87cd84` for `SetAllPoints`: a raise, never a fallback.
|
|
pub(crate) fn no_such_region(who: &str, verb: &str, name: &str) -> mlua::Error {
|
|
mlua::Error::runtime(format!(
|
|
"{who}:{verb}(): Couldn't find region named '{name}'"
|
|
))
|
|
}
|
|
|
|
/// `0x87cca8` / `0x87cd54`, from the self compare at `0x7a2764` / `0x7a2aa8`.
|
|
fn anchor_to_self(who: &str, verb: &str) -> mlua::Error {
|
|
mlua::Error::runtime(format!("{who}:{verb}(): trying to anchor to itself"))
|
|
}
|
|
|
|
/// `lua_isnumber` (`0x6f34d0`): a number, or a string Lua reads as one.
|
|
fn as_number(lua: &Lua, v: &Value) -> Option<f32> {
|
|
lua.coerce_number(v.clone())
|
|
.ok()
|
|
.flatten()
|
|
.map(|n| n as f32)
|
|
}
|
|
|
|
/// `lua_isstring` (`0x6f3510`): a string or a number.
|
|
fn as_string(lua: &Lua, v: &Value) -> Option<String> {
|
|
match v {
|
|
Value::String(_) | Value::Number(_) | Value::Integer(_) => lua
|
|
.coerce_string(v.clone())
|
|
.ok()
|
|
.flatten()
|
|
.and_then(|s| s.to_str().ok().map(|r| r.to_string())),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
/// Parse `SetPoint` in the reference's order: both tag gates (Usage), the point-name scan,
|
|
/// `relativePoint`, the offsets; so an unknown point with no `relativeTo` raises Usage. The caller
|
|
/// computes `who` and `parent_base` first, since an `__index` on `_G` can call back into a binding.
|
|
pub(crate) fn parse_set_point(
|
|
lua: &Lua,
|
|
args: &MultiValue,
|
|
who: &str,
|
|
parent_base: &str,
|
|
) -> mlua::Result<PointArgs> {
|
|
let point_str = args.front().and_then(|v| as_string(lua, v));
|
|
let Some(point_str) = point_str else {
|
|
return Err(usage(who));
|
|
};
|
|
|
|
let Some(rel_arg) = args.get(1) else {
|
|
return Err(usage(who));
|
|
};
|
|
let target = match rel_arg {
|
|
Value::String(s) => RelTarget::Named(match s.to_str() {
|
|
Ok(n) => prefetch_named_target(lua, n.as_ref(), Some(parent_base)),
|
|
Err(_) => NamedTarget::unreadable(),
|
|
}),
|
|
Value::Table(t) => RelTarget::Wrapper(t.clone()),
|
|
Value::Nil => RelTarget::Screen,
|
|
Value::Number(_) | Value::Integer(_) => RelTarget::DefaultParent,
|
|
_ => return Err(usage(who)),
|
|
};
|
|
|
|
let point = point_from_str(&point_str).ok_or_else(|| unknown_point(who))?;
|
|
|
|
// `relativePoint` defaults to `point` (`0x7a2697`) and is consumed only when strictly a string
|
|
// (`0x7a2810`), else the offsets start here; the number arm reads one slot earlier.
|
|
let mut cur = if matches!(target, RelTarget::DefaultParent) {
|
|
1
|
|
} else {
|
|
2
|
|
};
|
|
let mut rel_point = point;
|
|
if let Some(Value::String(s)) = args.get(cur) {
|
|
let name = s.to_str()?;
|
|
rel_point = point_from_str(name.as_ref()).ok_or_else(|| unknown_point(who))?;
|
|
cur += 1;
|
|
}
|
|
|
|
// Both offsets or neither: one failing `lua_isnumber` zeroes both (`0x7a2888` → `0x7a28e6`).
|
|
let (x, y) = match (
|
|
args.get(cur).and_then(|v| as_number(lua, v)),
|
|
args.get(cur + 1).and_then(|v| as_number(lua, v)),
|
|
) {
|
|
(Some(x), Some(y)) => (x, y),
|
|
_ => (0.0, 0.0),
|
|
};
|
|
|
|
Ok(PointArgs {
|
|
point,
|
|
target,
|
|
rel_point,
|
|
x,
|
|
y,
|
|
})
|
|
}
|
|
|
|
/// Parse `SetAllPoints([relativeTo])` (`0x7a2940`). Unlike `SetPoint`, an omitted argument takes
|
|
/// the layout parent, and a number is a name (`lua_isstring`, `0x7a29ea`): `SetAllPoints(42)`
|
|
/// looks up `_G["42"]`.
|
|
pub(crate) fn parse_set_all_points(lua: &Lua, arg: Option<&Value>, parent_base: &str) -> RelTarget {
|
|
match arg {
|
|
Some(Value::Table(t)) => RelTarget::Wrapper(t.clone()),
|
|
Some(Value::Nil) => RelTarget::Screen,
|
|
Some(v) => match as_string(lua, v) {
|
|
Some(n) => RelTarget::Named(prefetch_named_target(lua, &n, Some(parent_base))),
|
|
None => RelTarget::DefaultParent,
|
|
},
|
|
None => RelTarget::DefaultParent,
|
|
}
|
|
}
|
|
|
|
/// Resolve a [`RelTarget`] to the layout id to anchor to, raising the reference's errors; `me` is
|
|
/// the receiver, for the self-anchor check. Deviation: a wrapper that decodes to no live widget
|
|
/// takes `default_parent`, because the reference, with no `IsA` check, reads a wild pointer.
|
|
pub(crate) fn resolve_rel_target(
|
|
model: &Model,
|
|
target: &RelTarget,
|
|
who: &str,
|
|
verb: &str,
|
|
me: u32,
|
|
default_parent: u32,
|
|
) -> mlua::Result<u32> {
|
|
let id = match target {
|
|
RelTarget::Wrapper(t) => decode_id(t)
|
|
.ok()
|
|
.filter(|id| model.id_to_frame.contains_key(id) || model.id_to_region.contains_key(id))
|
|
.unwrap_or(default_parent),
|
|
RelTarget::Named(nt) => {
|
|
resolve_named_target(model, nt).ok_or_else(|| no_such_region(who, verb, &nt.name))?
|
|
}
|
|
RelTarget::Screen => SCREEN,
|
|
RelTarget::DefaultParent => default_parent,
|
|
};
|
|
if id == me {
|
|
return Err(anchor_to_self(who, verb));
|
|
}
|
|
Ok(id)
|
|
}
|
|
|
|
/// The XML `<Anchor relativeTo>` lookup (`CLayoutFrame::LoadXML 0x767800`), `$parent` already
|
|
/// expanded: the Lua string arm's resolver (`0x76c700`), but a miss reports "Couldn't find
|
|
/// relative frame" (`0x878440`) and skips the anchor (`0x767969`) without raising or falling back.
|
|
pub(crate) fn resolve_xml_relative_to(lua: &Lua, name: &str) -> Option<Table> {
|
|
// One `_G` read, its wrapper returned: an `__index` may answer a second read differently.
|
|
let nt = prefetch_named_target(lua, name, None);
|
|
let model = lua.app_data_ref::<Model>().expect("model");
|
|
resolve_named_target(&model, &nt)?;
|
|
// A wrapper, not a name, so the Lua ladder's raise can never reach a loading document.
|
|
nt.wrapper.clone()
|
|
}
|
|
|
|
/// `GetPoint`'s index (`0x7a2340`), from 0: an `is-number` argument (`0x6f34d0`), truncated
|
|
/// toward zero (`0x40a2b0`), less one; any other argument, or one at or below 1, picks the first
|
|
/// anchor, since the walk takes the first held slot whose count reaches the index (`0x7a23fe`).
|
|
pub(crate) fn get_point_index(lua: &Lua, n: Value) -> mlua::Result<usize> {
|
|
let idx = match lua.coerce_number(n)? {
|
|
Some(n) => (n as i64 as i32).wrapping_sub(1),
|
|
None => 0,
|
|
};
|
|
Ok(idx.max(0) as usize)
|
|
}
|
|
|
|
/// `GetPoint`'s answer for `anchor` (`0x7a2416`..`0x7a24d6`): point, `relativeTo`, relative point
|
|
/// and the two offsets; no anchor at the index pushes no value at all (`0x7a240d`).
|
|
pub(crate) fn get_point_values(
|
|
lua: &Lua,
|
|
anchor: Option<crate::layout::Anchor>,
|
|
rel: impl FnOnce(u32) -> mlua::Result<Value>,
|
|
) -> mlua::Result<MultiValue> {
|
|
let Some(a) = anchor else {
|
|
return Ok(MultiValue::new());
|
|
};
|
|
Ok(MultiValue::from_vec(vec![
|
|
Value::String(lua.create_string(super::point_name(a.point))?),
|
|
rel(a.relative_to)?,
|
|
Value::String(lua.create_string(super::point_name(a.relative_point))?),
|
|
Value::Number(f64::from(a.x_off)),
|
|
Value::Number(f64::from(a.y_off)),
|
|
]))
|
|
}
|