Files
benilla-octo/crates/benilla-ui/src/script/object/anchor_args.rs
T
Sam SungandClaude Opus 5.5 377542daeb One anchor slot per point; SetPoint skips an unchanged anchor (#934)
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>
2026-10-12 03:19:11 +07:00

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)),
]))
}