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>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
af281a05f7
commit
377542daeb
+236
-15
@@ -141,6 +141,170 @@ impl Anchor {
|
||||
y_off,
|
||||
}
|
||||
}
|
||||
|
||||
/// Bit-exact equality, as the layout gate's fingerprint compares, so the two always agree.
|
||||
pub fn bits_eq(&self, other: &Anchor) -> bool {
|
||||
self.point == other.point
|
||||
&& self.relative_to == other.relative_to
|
||||
&& self.relative_point == other.relative_point
|
||||
&& self.x_off.to_bits() == other.x_off.to_bits()
|
||||
&& self.y_off.to_bits() == other.y_off.to_bits()
|
||||
}
|
||||
}
|
||||
|
||||
/// The client's aspect globals, both written only by `0x41ad10(a)`: `[0x832a4c] = f32(a · 0.75)`
|
||||
/// (`0x41ad24`) and `[0x832a44] = f32(a / √(a² + 1))`, the quotient kept unrounded before its
|
||||
/// multiply (`0x41ad3e` `fst`, `0x41ad44`, `0x41ad47`).
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct LayoutUnits {
|
||||
g3: f32,
|
||||
g44: f32,
|
||||
}
|
||||
|
||||
impl LayoutUnits {
|
||||
/// From the screen aspect `a`, width over height.
|
||||
pub fn from_aspect(a: f32) -> LayoutUnits {
|
||||
let a64 = f64::from(a);
|
||||
LayoutUnits {
|
||||
g3: (a64 * 0.75) as f32,
|
||||
g44: ((1.0 / (a64 * a64 + 1.0).sqrt()) * a64) as f32,
|
||||
}
|
||||
}
|
||||
|
||||
/// A Lua offset as `SetPoint`'s binding stores it, in layout units (`0x7a28a3`..`0x7a28e3`):
|
||||
/// `f32(f32(v / ([0x832a4c] · 1024)) · [0x832a44])`.
|
||||
pub fn store(self, v: f32) -> f32 {
|
||||
let q = (f64::from(v) / (f64::from(self.g3) * 1024.0)) as f32;
|
||||
(f64::from(self.g44) * f64::from(q)) as f32
|
||||
}
|
||||
}
|
||||
|
||||
/// A region's anchors, one slot per point: the client's `anchorPoints[9]` (`G+0x4`, a frame's
|
||||
/// `+0x28`), indexed by point id. Setting a point replaces its slot (`SetPoint` stores at
|
||||
/// `0x767d33`), so the order anchors were set in is not kept, and every walk runs in point order,
|
||||
/// TOPLEFT to BOTTOMRIGHT, as `GetPoint` (`0x7a23f5`) and `GetNumPoints` (`0x7a2311`) walk them.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
||||
pub struct Anchors([Option<Anchor>; 9]);
|
||||
|
||||
impl Anchors {
|
||||
pub const fn new() -> Anchors {
|
||||
Anchors([None; 9])
|
||||
}
|
||||
|
||||
/// The anchor held for `point`.
|
||||
#[inline]
|
||||
pub fn get(&self, point: Point) -> Option<&Anchor> {
|
||||
self.0[point.id() as usize].as_ref()
|
||||
}
|
||||
|
||||
/// Store `a` in its point's slot, returning the anchor it replaces.
|
||||
#[inline]
|
||||
pub fn insert(&mut self, a: Anchor) -> Option<Anchor> {
|
||||
self.0[a.point.id() as usize].replace(a)
|
||||
}
|
||||
|
||||
/// `SetPoint`'s store (`0x767c70`): the anchor held for `a.point` stands when it has the same
|
||||
/// target (`0x767ca9`) and relative point (`0x767cb1`) and both offsets, as the client stores
|
||||
/// them ([`LayoutUnits::store`]), differ by less than 2⁻²² (`0x767cb6`..`0x767cde`), and the
|
||||
/// call then stores nothing; else `a` replaces it. Returns whether it stored. A NaN offset
|
||||
/// never matches, as the client's unordered compare.
|
||||
pub fn set(&mut self, a: Anchor, units: LayoutUnits) -> bool {
|
||||
let near = |p: f32, q: f32| {
|
||||
(f64::from(units.store(p)) - f64::from(units.store(q))).abs() < SIZE_EPS
|
||||
};
|
||||
let unchanged = self.get(a.point).is_some_and(|h| {
|
||||
h.relative_to == a.relative_to
|
||||
&& h.relative_point == a.relative_point
|
||||
&& near(h.x_off, a.x_off)
|
||||
&& near(h.y_off, a.y_off)
|
||||
});
|
||||
if !unchanged {
|
||||
self.insert(a);
|
||||
}
|
||||
!unchanged
|
||||
}
|
||||
|
||||
/// Empty every slot, returning whether any was held (`ClearAllPoints`, `0x767620`).
|
||||
#[inline]
|
||||
pub fn clear(&mut self) -> bool {
|
||||
let held = !self.is_empty();
|
||||
self.0 = [None; 9];
|
||||
held
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.0.iter().all(Option::is_none)
|
||||
}
|
||||
|
||||
/// The occupied slots, `GetNumPoints`'s count.
|
||||
#[inline]
|
||||
pub fn len(&self) -> usize {
|
||||
self.0.iter().filter(|s| s.is_some()).count()
|
||||
}
|
||||
|
||||
/// The held anchors in point order.
|
||||
#[inline]
|
||||
pub fn iter(&self) -> std::iter::Flatten<std::slice::Iter<'_, Option<Anchor>>> {
|
||||
self.0.iter().flatten()
|
||||
}
|
||||
|
||||
/// Each held anchor's offsets in point order; the point is not writable here, so an anchor
|
||||
/// never leaves its slot.
|
||||
#[inline]
|
||||
pub fn offsets_mut(&mut self) -> impl Iterator<Item = (&mut f32, &mut f32)> {
|
||||
self.0
|
||||
.iter_mut()
|
||||
.flatten()
|
||||
.map(|a| (&mut a.x_off, &mut a.y_off))
|
||||
}
|
||||
|
||||
/// The `n`-th held anchor in point order, from 0.
|
||||
#[inline]
|
||||
pub fn nth(&self, n: usize) -> Option<&Anchor> {
|
||||
self.iter().nth(n)
|
||||
}
|
||||
|
||||
/// The first held anchor in point order.
|
||||
#[inline]
|
||||
pub fn first(&self) -> Option<&Anchor> {
|
||||
self.iter().next()
|
||||
}
|
||||
|
||||
/// Slot-for-slot [`Anchor::bits_eq`].
|
||||
pub fn bits_eq(&self, other: &Anchors) -> bool {
|
||||
self.0.iter().zip(&other.0).all(|(a, b)| match (a, b) {
|
||||
(Some(a), Some(b)) => a.bits_eq(b),
|
||||
(None, None) => true,
|
||||
_ => false,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Each anchor into its point's slot, a later one on the same point replacing an earlier one.
|
||||
impl FromIterator<Anchor> for Anchors {
|
||||
fn from_iter<I: IntoIterator<Item = Anchor>>(iter: I) -> Anchors {
|
||||
let mut out = Anchors::new();
|
||||
for a in iter {
|
||||
out.insert(a);
|
||||
}
|
||||
out
|
||||
}
|
||||
}
|
||||
|
||||
impl<const N: usize> From<[Anchor; N]> for Anchors {
|
||||
fn from(a: [Anchor; N]) -> Anchors {
|
||||
a.into_iter().collect()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> IntoIterator for &'a Anchors {
|
||||
type Item = &'a Anchor;
|
||||
type IntoIter = std::iter::Flatten<std::slice::Iter<'a, Option<Anchor>>>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.iter()
|
||||
}
|
||||
}
|
||||
|
||||
/// A frame's layout input, the geometry fields the client's resolver reads: `anchorPoints[9]`
|
||||
@@ -148,8 +312,7 @@ impl Anchor {
|
||||
/// (bit 4 of `G+0x3c`).
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct LayoutInput {
|
||||
/// Slotted by `point`, the later of two on one point winning (`SetPoint`, `0x767c70`).
|
||||
pub anchors: Vec<Anchor>,
|
||||
pub anchors: Anchors,
|
||||
/// `0.0` is the no-size sentinel (`0x7ffd74`): the axis derives from the anchors.
|
||||
pub width: f32,
|
||||
pub height: f32,
|
||||
@@ -167,7 +330,7 @@ pub struct LayoutInput {
|
||||
impl Default for LayoutInput {
|
||||
fn default() -> LayoutInput {
|
||||
LayoutInput {
|
||||
anchors: Vec::new(),
|
||||
anchors: Anchors::new(),
|
||||
width: 0.0,
|
||||
height: 0.0,
|
||||
scale: 1.0,
|
||||
@@ -180,9 +343,13 @@ impl Default for LayoutInput {
|
||||
|
||||
impl LayoutInput {
|
||||
/// A frame with the given anchors and explicit size, scale `1.0`, no clamp.
|
||||
pub fn sized(anchors: Vec<Anchor>, width: f32, height: f32) -> LayoutInput {
|
||||
pub fn sized(
|
||||
anchors: impl IntoIterator<Item = Anchor>,
|
||||
width: f32,
|
||||
height: f32,
|
||||
) -> LayoutInput {
|
||||
LayoutInput {
|
||||
anchors,
|
||||
anchors: anchors.into_iter().collect(),
|
||||
width,
|
||||
height,
|
||||
..LayoutInput::default()
|
||||
@@ -637,7 +804,7 @@ pub fn resolve_rect_edges(
|
||||
/// [`set_external`](Self::set_external) and [`set_frame`](Self::set_frame), [`solve`](Self::solve).
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct LayoutSolver {
|
||||
/// Overwritten in place, so each anchors `Vec` keeps its allocation.
|
||||
/// Overwritten in place, so a steady round allocates nothing.
|
||||
input: Vec<LayoutInput>,
|
||||
is_frame: Vec<bool>,
|
||||
/// Externals as seeded, frames as they resolve.
|
||||
@@ -723,8 +890,7 @@ impl LayoutSolver {
|
||||
self.ensure(h);
|
||||
self.touch(h);
|
||||
let dst = &mut self.input[h as usize];
|
||||
dst.anchors.clear();
|
||||
dst.anchors.extend_from_slice(&src.anchors);
|
||||
dst.anchors = src.anchors;
|
||||
Self::copy_fields(dst, src);
|
||||
self.is_frame[h as usize] = true;
|
||||
self.live.push(h);
|
||||
@@ -735,8 +901,7 @@ impl LayoutSolver {
|
||||
self.ensure(h);
|
||||
self.touch(h);
|
||||
let dst = &mut self.input[h as usize];
|
||||
dst.anchors.clear();
|
||||
dst.anchors.push(anchor);
|
||||
dst.anchors = Anchors::from([anchor]);
|
||||
Self::copy_fields(dst, src);
|
||||
self.is_frame[h as usize] = true;
|
||||
self.live.push(h);
|
||||
@@ -775,8 +940,8 @@ impl LayoutSolver {
|
||||
let h = self.live[idx];
|
||||
let mut seen: [Handle; 9] = [0; 9];
|
||||
let mut n_seen = 0usize;
|
||||
for a_idx in 0..self.input[h as usize].anchors.len() {
|
||||
let d = self.input[h as usize].anchors[a_idx].relative_to;
|
||||
let anchors = self.input[h as usize].anchors;
|
||||
for d in anchors.iter().map(|a| a.relative_to) {
|
||||
if !self.is_frame.get(d as usize).copied().unwrap_or(false) {
|
||||
continue;
|
||||
}
|
||||
@@ -866,6 +1031,15 @@ mod tests {
|
||||
Rect::new(0.0, 0.0, 600.0, 800.0)
|
||||
}
|
||||
|
||||
/// `0x41ad10` at 4:3: `[0x832a4c] = 1.0` and `[0x832a44] = 0.8` (as `f32`), so an offset
|
||||
/// stores as `f32(f32(v / 1024) · 0.8)`.
|
||||
#[test]
|
||||
fn layout_units_at_four_thirds() {
|
||||
let u = LayoutUnits::from_aspect(4.0 / 3.0);
|
||||
assert_eq!(u, LayoutUnits { g3: 1.0, g44: 0.8 });
|
||||
assert_eq!(u.store(1280.0), (1.25_f64 * f64::from(0.8_f32)) as f32);
|
||||
}
|
||||
|
||||
fn bits(r: Rect) -> [u32; 4] {
|
||||
[
|
||||
r.bottom.to_bits(),
|
||||
@@ -874,6 +1048,53 @@ mod tests {
|
||||
r.right.to_bits(),
|
||||
]
|
||||
}
|
||||
|
||||
/// One slot per point, walked in point order whatever order the points were set in.
|
||||
#[test]
|
||||
fn anchors_keep_one_slot_per_point_in_point_order() {
|
||||
let br = Anchor::new(Point::BottomRight, SCREEN, Point::BottomRight, -1.0, 1.0);
|
||||
let tl = Anchor::new(Point::TopLeft, SCREEN, Point::TopLeft, 1.0, -1.0);
|
||||
let tl2 = Anchor::new(Point::TopLeft, 7, Point::Center, 2.0, -2.0);
|
||||
let a = Anchors::from([br, tl, tl2]);
|
||||
assert_eq!(a.len(), 2);
|
||||
assert_eq!(a.iter().copied().collect::<Vec<_>>(), vec![tl2, br]);
|
||||
assert_eq!(a.nth(1), Some(&br));
|
||||
assert_eq!(a.nth(2), None);
|
||||
}
|
||||
|
||||
/// `SetPoint`'s compare (`0x767cb6`..`0x767cde`): under the threshold on both axes with the
|
||||
/// same target and relative point stands; past it, NaN, or another target or relative point
|
||||
/// replaces. At 4:3 a layout unit is 1280 FrameXML units, so 2⁻²² is about 3.05e-4.
|
||||
#[test]
|
||||
fn anchors_set_keeps_an_anchor_within_the_threshold() {
|
||||
let eps = LayoutUnits::from_aspect(4.0 / 3.0);
|
||||
let held = Anchor::new(Point::Center, SCREEN, Point::Center, 10.0, 20.0);
|
||||
let mut a = Anchors::from([held]);
|
||||
let with = |x: f32, y: f32| Anchor::new(Point::Center, SCREEN, Point::Center, x, y);
|
||||
assert!(!a.set(with(10.0001, 19.9999), eps));
|
||||
assert_eq!(a.get(Point::Center), Some(&held), "nothing is stored");
|
||||
for (x, y) in [(10.001, 20.0), (10.0, 19.999), (f32::NAN, 20.0)] {
|
||||
let mut b = a;
|
||||
assert!(b.set(with(x, y), eps), "({x}, {y})");
|
||||
assert!(b.get(Point::Center).unwrap().x_off.to_bits() == x.to_bits());
|
||||
}
|
||||
let mut b = a;
|
||||
assert!(b.set(
|
||||
Anchor::new(Point::Center, 3, Point::Center, 10.0, 20.0),
|
||||
eps
|
||||
));
|
||||
let mut b = a;
|
||||
assert!(b.set(
|
||||
Anchor::new(Point::Center, SCREEN, Point::Top, 10.0, 20.0),
|
||||
eps
|
||||
));
|
||||
let mut b = a;
|
||||
assert!(b.set(
|
||||
Anchor::new(Point::Top, SCREEN, Point::Center, 10.0, 20.0),
|
||||
eps
|
||||
));
|
||||
assert_eq!(b.len(), 2);
|
||||
}
|
||||
fn assert_rect(got: ResolveOutcome, want: Rect) {
|
||||
match got {
|
||||
ResolveOutcome::Resolved(r) => {
|
||||
@@ -1062,13 +1283,13 @@ mod tests {
|
||||
#[test]
|
||||
fn scale_interaction() {
|
||||
let f = LayoutInput {
|
||||
anchors: vec![Anchor::new(
|
||||
anchors: Anchors::from([Anchor::new(
|
||||
Point::TopLeft,
|
||||
SCREEN,
|
||||
Point::TopLeft,
|
||||
10.0,
|
||||
-5.0,
|
||||
)],
|
||||
)]),
|
||||
width: 200.0,
|
||||
height: 50.0,
|
||||
scale: 2.0,
|
||||
@@ -1395,7 +1616,7 @@ mod tests {
|
||||
assert!(s.unresolvable().is_empty());
|
||||
}
|
||||
|
||||
/// The override keeps the size and scale but replaces the anchors, and the reused anchors `Vec`
|
||||
/// The override keeps the size and scale but replaces the anchors, and the reused input slot
|
||||
/// must not leak the last round's.
|
||||
#[test]
|
||||
fn set_frame_anchored_replaces_only_the_anchors() {
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::layout::{self, Anchor, Point, Rect};
|
||||
use crate::layout::{self, Anchor, Anchors, Point, Rect};
|
||||
use crate::order::DrawLayer;
|
||||
use crate::widget::{FrameHandle, RegionHandle, RegionKind};
|
||||
|
||||
@@ -383,7 +383,7 @@ pub(crate) fn install(model: &mut Model, fh: FrameHandle, spec: Option<Backdrop>
|
||||
|
||||
/// A piece's anchors on frame `owner` and its explicit size, `None` where the anchors span both
|
||||
/// axes.
|
||||
fn piece_geometry(owner: u32, (anchors, size): PieceLayout) -> (Vec<Anchor>, Option<(f32, f32)>) {
|
||||
fn piece_geometry(owner: u32, (anchors, size): PieceLayout) -> (Anchors, Option<(f32, f32)>) {
|
||||
let anchors = anchors
|
||||
.into_iter()
|
||||
.flatten()
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
|
||||
use std::collections::{HashMap, VecDeque};
|
||||
|
||||
use crate::layout::{Anchor, Point};
|
||||
use crate::layout::{Anchor, Anchors, Point};
|
||||
use crate::order::DrawLayer;
|
||||
use crate::widget::{FrameHandle, FrameKind, RegionHandle, RegionKind};
|
||||
|
||||
@@ -318,20 +318,20 @@ fn lay_out(model: &mut Model, i: usize, g: BubbleGeometry) {
|
||||
// A border-unit square, its TOPRIGHT on the frame's BOTTOM lifted a quarter unit.
|
||||
let tail_data = model.region_data.entry(tail).or_default();
|
||||
tail_data.size = Some((g.border, g.border));
|
||||
tail_data.anchors = vec![Anchor::new(
|
||||
tail_data.anchors = Anchors::from([Anchor::new(
|
||||
Point::TopRight,
|
||||
frame_id,
|
||||
Point::Bottom,
|
||||
0.0,
|
||||
g.border / 4.0,
|
||||
)];
|
||||
)]);
|
||||
|
||||
model.region_data.entry(text).or_default().font_height = Some(g.text_height);
|
||||
model.touch_measure(text);
|
||||
|
||||
// The frame hugs the text: TOPLEFT and BOTTOMRIGHT on the text's, out by the margin.
|
||||
let input = model.layout_inputs.entry(frame).or_default();
|
||||
input.anchors = vec![
|
||||
input.anchors = Anchors::from([
|
||||
Anchor::new(Point::TopLeft, text_id, Point::TopLeft, -g.margin, g.margin),
|
||||
Anchor::new(
|
||||
Point::BottomRight,
|
||||
@@ -340,7 +340,7 @@ fn lay_out(model: &mut Model, i: usize, g: BubbleGeometry) {
|
||||
g.margin,
|
||||
-g.margin,
|
||||
),
|
||||
];
|
||||
]);
|
||||
// Once per geometry change: each node it wrote re-derives.
|
||||
model.touch_layout_frame(frame);
|
||||
model.touch_layout_region(tail);
|
||||
@@ -450,13 +450,13 @@ fn drive(
|
||||
);
|
||||
if last_seat != Some(seat) {
|
||||
let data = model.region_data.entry(text).or_default();
|
||||
data.anchors = vec![Anchor::new(
|
||||
data.anchors = Anchors::from([Anchor::new(
|
||||
Point::Bottom,
|
||||
world_id,
|
||||
Point::BottomLeft,
|
||||
seat.0,
|
||||
seat.1,
|
||||
)];
|
||||
)]);
|
||||
model.touch_layout_region(text);
|
||||
model.chat_bubbles.bubbles[i].seat = Some(seat);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
use mlua::Lua;
|
||||
|
||||
use crate::layout::{Anchor, Point, Rect};
|
||||
use crate::layout::{Anchor, Anchors, Point, Rect};
|
||||
use crate::script::{MeasureRequest, Model};
|
||||
use crate::widget::{EditBoxState, FrameHandle, KindState, RegionHandle};
|
||||
|
||||
@@ -39,18 +39,13 @@ pub(super) fn seat_text_region(model: &mut Model, h: FrameHandle) {
|
||||
let top_left = Anchor::new(Point::TopLeft, owner, Point::TopLeft, l, -t);
|
||||
let (anchors, size) = if multi_line {
|
||||
let width = multi_line_text_width(model, h, [l, r]);
|
||||
(vec![top_left], width.map(|w| Some((w, 0.0))))
|
||||
(Anchors::from([top_left]), width.map(|w| Some((w, 0.0))))
|
||||
} else {
|
||||
let bottom_right = Anchor::new(Point::BottomRight, owner, Point::BottomRight, -r, b);
|
||||
(vec![top_left, bottom_right], Some(None))
|
||||
(Anchors::from([top_left, bottom_right]), Some(None))
|
||||
};
|
||||
let data = model.region_data.entry(rh).or_default();
|
||||
let same_anchors = data.anchors.len() == anchors.len()
|
||||
&& data
|
||||
.anchors
|
||||
.iter()
|
||||
.zip(&anchors)
|
||||
.all(|(a, b)| crate::script::object::anchor_bits_eq(a, b));
|
||||
let same_anchors = data.anchors.bits_eq(&anchors);
|
||||
// `None` keeps the size: a multi-line box with no rect yet.
|
||||
let size = size.unwrap_or(data.size);
|
||||
let size_moved = !crate::script::region::size_bits_eq(data.size, size);
|
||||
|
||||
@@ -39,16 +39,19 @@ impl InputFingerprint {
|
||||
self.feed(u64::from(v.to_bits()));
|
||||
}
|
||||
|
||||
/// The anchors in point order, then their count.
|
||||
#[inline]
|
||||
fn anchors(&mut self, anchors: &[Anchor]) {
|
||||
self.feed(anchors.len() as u64);
|
||||
fn anchors<'a>(&mut self, anchors: impl IntoIterator<Item = &'a Anchor>) {
|
||||
let mut n = 0u64;
|
||||
for a in anchors {
|
||||
self.feed(u64::from(a.point.id()));
|
||||
self.feed(u64::from(a.relative_to));
|
||||
self.feed(u64::from(a.relative_point.id()));
|
||||
self.f32(a.x_off);
|
||||
self.f32(a.y_off);
|
||||
n += 1;
|
||||
}
|
||||
self.feed(n);
|
||||
}
|
||||
|
||||
#[inline]
|
||||
@@ -505,7 +508,7 @@ impl LayoutScope {
|
||||
};
|
||||
let over_eq = |a: Option<Anchor>, b: Option<Anchor>| match (a, b) {
|
||||
(None, None) => true,
|
||||
(Some(a), Some(b)) => super::object::anchor_bits_eq(&a, &b),
|
||||
(Some(a), Some(b)) => a.bits_eq(&b),
|
||||
_ => false,
|
||||
};
|
||||
let n = self
|
||||
@@ -1221,8 +1224,7 @@ impl UiScript {
|
||||
}
|
||||
}
|
||||
}
|
||||
scratch.anchors.clear();
|
||||
scratch.anchors.extend_from_slice(&data.anchors);
|
||||
scratch.anchors = data.anchors;
|
||||
scratch.width = width;
|
||||
scratch.height = height;
|
||||
scratch.scale = scale;
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
//! root (saved as no target, the `nil` `GetPoint` answers there) or a named frame; a frame anchored
|
||||
//! to anything else is skipped whole, never half-restored.
|
||||
|
||||
use crate::layout::Anchor;
|
||||
use crate::layout::{Anchor, Anchors};
|
||||
use crate::widget::FrameHandle;
|
||||
|
||||
use super::object::{point_from_str, point_name};
|
||||
@@ -85,7 +85,7 @@ impl super::UiScript {
|
||||
let Some(h) = model.arena.lookup(&l.name) else {
|
||||
continue; // a window this build does not have
|
||||
};
|
||||
let mut anchors: Vec<Anchor> = Vec::with_capacity(l.points.len());
|
||||
let mut anchors = Anchors::new();
|
||||
let mut usable = true;
|
||||
for p in &l.points {
|
||||
// `frame_id` mints the id lazily, so a target nothing has anchored to yet resolves.
|
||||
@@ -99,7 +99,7 @@ impl super::UiScript {
|
||||
point_from_str(&p.relative_point),
|
||||
) {
|
||||
(Some(point), Some(rel), Some(rp)) => {
|
||||
anchors.push(Anchor::new(point, rel, rp, p.x, p.y));
|
||||
anchors.insert(Anchor::new(point, rel, rp, p.x, p.y));
|
||||
}
|
||||
_ => {
|
||||
usable = false;
|
||||
|
||||
@@ -41,6 +41,14 @@ impl Model {
|
||||
crate::layout::SIZE_EPS * f64::from(self.layout_unit())
|
||||
}
|
||||
|
||||
/// The aspect globals `SetPoint`'s unchanged-anchor test quantizes offsets by
|
||||
/// ([`crate::layout::Anchors::set`]), for the screen aspect [`Self::layout_unit`] reads.
|
||||
pub(crate) fn layout_units(&self) -> crate::layout::LayoutUnits {
|
||||
let (w, h) = (self.screen.width(), self.screen.height());
|
||||
let a = if h > 0.0 && w > 0.0 { w / h } else { 4.0 / 3.0 };
|
||||
crate::layout::LayoutUnits::from_aspect(a)
|
||||
}
|
||||
|
||||
/// The drain's keep threshold, 1e-5 in the reference's layout units (`[0x80c5c8]`, tested in
|
||||
/// `0x768d20`), in ours: [`crate::layout::RESOLVE_EPS`] times the layout unit.
|
||||
pub(crate) fn resolve_eps(&self) -> f64 {
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
use mlua::Lua;
|
||||
|
||||
use crate::layout::{Anchor, Point};
|
||||
use crate::layout::{Anchor, Anchors, Point};
|
||||
use crate::order::DrawLayer;
|
||||
use crate::widget::{FrameHandle, FrameKind, KindState, RegionHandle, RegionKind};
|
||||
|
||||
@@ -525,32 +525,32 @@ impl Plate {
|
||||
input.height = g.height;
|
||||
|
||||
for rh in [border, glow] {
|
||||
model.region_data.entry(rh).or_default().anchors = vec![
|
||||
model.region_data.entry(rh).or_default().anchors = Anchors::from([
|
||||
Anchor::new(Point::TopLeft, plate_id, Point::TopLeft, 0.0, 0.0),
|
||||
Anchor::new(Point::BottomRight, plate_id, Point::BottomRight, 0.0, 0.0),
|
||||
];
|
||||
]);
|
||||
}
|
||||
|
||||
let bar_input = model.layout_inputs.entry(bar).or_default();
|
||||
bar_input.anchors = vec![Anchor::new(
|
||||
bar_input.anchors = Anchors::from([Anchor::new(
|
||||
Point::BottomLeft,
|
||||
plate_id,
|
||||
Point::BottomLeft,
|
||||
g.bar_off_x,
|
||||
g.bar_off_y,
|
||||
)];
|
||||
)]);
|
||||
bar_input.width = g.bar_width;
|
||||
bar_input.height = g.bar_height;
|
||||
|
||||
// The name's (`0x7cb456`) and level's (`0x7cb7f5`) anchors, which `_Nameplates` checks.
|
||||
let name_data = model.region_data.entry(name).or_default();
|
||||
name_data.anchors = vec![Anchor::new(
|
||||
name_data.anchors = Anchors::from([Anchor::new(
|
||||
Point::Bottom,
|
||||
plate_id,
|
||||
Point::Center,
|
||||
0.0,
|
||||
0.0,
|
||||
)];
|
||||
)]);
|
||||
name_data.font_height = Some(g.name_height);
|
||||
|
||||
for rh in [name, level] {
|
||||
@@ -561,29 +561,30 @@ impl Plate {
|
||||
}
|
||||
|
||||
let level_data = model.region_data.entry(level).or_default();
|
||||
level_data.anchors = vec![Anchor::new(
|
||||
level_data.anchors = Anchors::from([Anchor::new(
|
||||
Point::Center,
|
||||
plate_id,
|
||||
Point::BottomRight,
|
||||
-g.level_off_x,
|
||||
g.level_off_y,
|
||||
)];
|
||||
)]);
|
||||
level_data.font_height = Some(g.level_height);
|
||||
level_data.font_explicit.height = true;
|
||||
model.touch_measure(level);
|
||||
|
||||
let skull_data = model.region_data.entry(skull).or_default();
|
||||
skull_data.anchors = vec![Anchor::new(
|
||||
skull_data.anchors = Anchors::from([Anchor::new(
|
||||
Point::Center,
|
||||
plate_id,
|
||||
Point::BottomRight,
|
||||
-g.level_off_x,
|
||||
g.level_off_y,
|
||||
)];
|
||||
)]);
|
||||
skull_data.size = Some((g.skull_size, g.skull_size));
|
||||
|
||||
let raid_data = model.region_data.entry(raid).or_default();
|
||||
raid_data.anchors = vec![Anchor::new(Point::Right, plate_id, Point::Left, 0.0, 0.0)];
|
||||
raid_data.anchors =
|
||||
Anchors::from([Anchor::new(Point::Right, plate_id, Point::Left, 0.0, 0.0)]);
|
||||
raid_data.size = Some((g.raid_size, g.raid_size));
|
||||
|
||||
// Once per geometry change: each node it wrote re-derives.
|
||||
@@ -616,13 +617,13 @@ impl Plate {
|
||||
.is_none_or(|l| l.top_centre != state.top_centre)
|
||||
{
|
||||
let input = model.layout_inputs.entry(frame).or_default();
|
||||
input.anchors = vec![Anchor::new(
|
||||
input.anchors = Anchors::from([Anchor::new(
|
||||
Point::Top,
|
||||
world_id,
|
||||
Point::BottomLeft,
|
||||
state.top_centre.0,
|
||||
state.top_centre.1,
|
||||
)];
|
||||
)]);
|
||||
model.touch_layout_frame(frame);
|
||||
}
|
||||
|
||||
|
||||
@@ -24,7 +24,6 @@ mod layout_methods;
|
||||
pub(crate) use layout_methods::eff_scale;
|
||||
pub(crate) mod movable;
|
||||
pub(crate) mod toplevel;
|
||||
pub(crate) use layout_methods::anchor_bits_eq;
|
||||
pub(crate) use movable::{advance_move, advance_size, FrameMove, FrameSizing};
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -195,3 +195,33 @@ pub(crate) fn resolve_xml_relative_to(lua: &Lua, name: &str) -> Option<Table> {
|
||||
// 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)),
|
||||
]))
|
||||
}
|
||||
|
||||
@@ -2,13 +2,16 @@
|
||||
|
||||
use mlua::{Lua, MultiValue, Table, Value};
|
||||
|
||||
use crate::layout::{Anchor, Point};
|
||||
use crate::layout::{Anchor, Anchors, Point};
|
||||
use crate::script::region_map::{set_shared, Side};
|
||||
use crate::script::{Model, SCREEN};
|
||||
use crate::widget::FrameHandle;
|
||||
|
||||
use super::anchor_args::{parse_set_all_points, parse_set_point, resolve_rel_target, UNNAMED};
|
||||
use super::{frame_handle_of, frame_parent_token_base, frame_wrapper, point_name};
|
||||
use super::anchor_args::{
|
||||
get_point_index, get_point_values, parse_set_all_points, parse_set_point, resolve_rel_target,
|
||||
UNNAMED,
|
||||
};
|
||||
use super::{frame_handle_of, frame_parent_token_base, frame_wrapper};
|
||||
|
||||
/// The receiver's name for the error strings and its `$parent` base, read under one short `Model`
|
||||
/// borrow that ends before the ladder's `_G` read.
|
||||
@@ -37,51 +40,40 @@ pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model");
|
||||
// Every layout setter mutates only on a real change, so a per-frame caller setting the
|
||||
// same value never reopens the layout gate.
|
||||
let cleared = match model.layout_inputs.get_mut(&h) {
|
||||
Some(input) if !input.anchors.is_empty() => {
|
||||
input.anchors.clear();
|
||||
true
|
||||
}
|
||||
_ => false,
|
||||
};
|
||||
let cleared = model
|
||||
.layout_inputs
|
||||
.get_mut(&h)
|
||||
.is_some_and(|input| input.anchors.clear());
|
||||
if cleared {
|
||||
model.touch_layout_frame(h);
|
||||
}
|
||||
Ok(())
|
||||
})?;
|
||||
// GetPoint([n]) → point, relativeTo, relativePoint, x, y of the n-th anchor (default 1);
|
||||
// relativeTo is nil for a screen-root anchor, since `SCREEN` has no wrapper.
|
||||
// GetPoint([n]) → point, relativeTo, relativePoint, x, y of the n-th anchor in point order
|
||||
// (`0x7a2340`); relativeTo is nil for a screen-root anchor, since `SCREEN` has no wrapper.
|
||||
set_shared(
|
||||
lua,
|
||||
m,
|
||||
Side::Frame,
|
||||
"GetPoint",
|
||||
|lua, (this, n): (Table, Option<i64>)| {
|
||||
|lua, (this, n): (Table, Value)| {
|
||||
let h = frame_handle_of(lua, &this)?;
|
||||
let idx = get_point_index(lua, n)?;
|
||||
let anchor = {
|
||||
let model = lua.app_data_ref::<Model>().expect("model");
|
||||
let idx = (n.unwrap_or(1).max(1) - 1) as usize;
|
||||
model
|
||||
.layout_inputs
|
||||
.get(&h)
|
||||
.and_then(|i| i.anchors.get(idx))
|
||||
.cloned()
|
||||
.and_then(|i| i.anchors.nth(idx))
|
||||
.copied()
|
||||
};
|
||||
let Some(a) = anchor else {
|
||||
return Ok((Value::Nil, Value::Nil, Value::Nil, Value::Nil, Value::Nil));
|
||||
};
|
||||
let rel = if a.relative_to == SCREEN {
|
||||
Value::Nil
|
||||
} else {
|
||||
Value::Table(frame_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)),
|
||||
))
|
||||
get_point_values(lua, anchor, |rel| {
|
||||
Ok(if rel == SCREEN {
|
||||
Value::Nil
|
||||
} else {
|
||||
Value::Table(frame_wrapper(lua, rel)?)
|
||||
})
|
||||
})
|
||||
},
|
||||
)?;
|
||||
// GetNumPoints(): on the Region method table (`0x87c9b8`), so every widget answers it.
|
||||
@@ -115,15 +107,10 @@ pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
Anchor::new(Point::BottomRight, rel_id, Point::BottomRight, 0.0, 0.0),
|
||||
];
|
||||
let input = model.layout_inputs.entry(h).or_default();
|
||||
let same = input.anchors.len() == 2
|
||||
&& input
|
||||
.anchors
|
||||
.iter()
|
||||
.zip(&pair)
|
||||
.all(|(a, b)| anchor_bits_eq(a, b));
|
||||
if !same {
|
||||
input.anchors.clear();
|
||||
input.anchors.extend_from_slice(&pair);
|
||||
// The reference clears and sets both (`0x767db0`); only a bit-identical pair skips.
|
||||
let pair = Anchors::from(pair);
|
||||
if !input.anchors.bits_eq(&pair) {
|
||||
input.anchors = pair;
|
||||
model.touch_layout_frame(h);
|
||||
}
|
||||
Ok(())
|
||||
@@ -286,34 +273,17 @@ fn set_point(lua: &Lua, this: &Table, args: &MultiValue) -> mlua::Result<()> {
|
||||
let rel_to_id = resolve_rel_target(&model, &p.target, &who, "SetPoint", me, parent)?;
|
||||
let (point, rel_point, x, y) = (p.point, p.rel_point, p.x, p.y);
|
||||
|
||||
let units = model.layout_units();
|
||||
let input = model.layout_inputs.entry(h).or_default();
|
||||
let new = Anchor::new(point, rel_to_id, rel_point, x, y);
|
||||
// A no-op only when the identical anchor is already last and no earlier one has this point,
|
||||
// mirroring the retain and push below; bits compared, as the fingerprint tells -0.0 from 0.0.
|
||||
let same_at_tail = input
|
||||
if input
|
||||
.anchors
|
||||
.last()
|
||||
.is_some_and(|a| anchor_bits_eq(a, &new))
|
||||
&& !input.anchors[..input.anchors.len() - 1]
|
||||
.iter()
|
||||
.any(|a| a.point == point);
|
||||
if !same_at_tail {
|
||||
input.anchors.retain(|a| a.point != point);
|
||||
input.anchors.push(new);
|
||||
.set(Anchor::new(point, rel_to_id, rel_point, x, y), units)
|
||||
{
|
||||
model.touch_layout_frame(h);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Bit-exact anchor equality, as the layout gate's fingerprint compares, so the two always agree.
|
||||
pub(crate) fn anchor_bits_eq(a: &Anchor, b: &Anchor) -> bool {
|
||||
a.point == b.point
|
||||
&& a.relative_to == b.relative_to
|
||||
&& a.relative_point == b.relative_point
|
||||
&& a.x_off.to_bits() == b.x_off.to_bits()
|
||||
&& a.y_off.to_bits() == b.y_off.to_bits()
|
||||
}
|
||||
|
||||
/// The default `relativeTo`: the parent, or [`SCREEN`] for a top-level frame (`0x76c6e0`).
|
||||
fn default_parent_id(model: &mut Model, h: FrameHandle) -> u32 {
|
||||
match model.arena.frame(h).and_then(|f| f.parent) {
|
||||
|
||||
@@ -421,12 +421,12 @@ pub(crate) fn advance_size(model: &mut Model, pos: (f32, f32)) {
|
||||
// keeps its opposite edge. The reference instead re-anchors the frame to one TOPLEFT point at
|
||||
// `StartSizing` (`0x768430`).
|
||||
if !input.anchors.is_empty() && (sz.left || sz.bottom) {
|
||||
for a in &mut input.anchors {
|
||||
for (x_off, y_off) in input.anchors.offsets_mut() {
|
||||
if sz.left {
|
||||
a.x_off += dx;
|
||||
*x_off += dx;
|
||||
}
|
||||
if sz.bottom {
|
||||
a.y_off += dy;
|
||||
*y_off += dy;
|
||||
}
|
||||
}
|
||||
moved |= (sz.left && dx != 0.0) || (sz.bottom && dy != 0.0);
|
||||
@@ -460,9 +460,9 @@ pub(crate) fn advance_move(model: &mut Model, pos: (f32, f32)) {
|
||||
let (dx, dy) = (dx * inv, dy * inv);
|
||||
if let Some(input) = model.layout_inputs.get_mut(&mv.frame) {
|
||||
if !input.anchors.is_empty() {
|
||||
for a in &mut input.anchors {
|
||||
a.x_off += dx;
|
||||
a.y_off += dy;
|
||||
for (x_off, y_off) in input.anchors.offsets_mut() {
|
||||
*x_off += dx;
|
||||
*y_off += dy;
|
||||
}
|
||||
// No target moves, so only this node is touched; a zero delta already returned above.
|
||||
model.touch_layout_frame(mv.frame);
|
||||
|
||||
@@ -4,13 +4,13 @@
|
||||
use mlua::{Lua, MultiValue, Table, Value};
|
||||
|
||||
use super::object::anchor_args::{parse_set_point, resolve_rel_target, UNNAMED};
|
||||
use super::object::{anchor_bits_eq, decode_id, id_to_lud, NamedTarget};
|
||||
use super::object::{decode_id, id_to_lud, NamedTarget};
|
||||
use super::region_map::{set_shared, Side};
|
||||
use super::{
|
||||
Model, REG_FONTSTRING_META, REG_FONTSTRING_METHODS, REG_REGION_META, REG_REGION_METHODS,
|
||||
REG_TEXTURE_META, REG_TEXTURE_METHODS, REG_TITLE_META, REG_TITLE_METHODS, REG_WRAPPERS, SCREEN,
|
||||
};
|
||||
use crate::layout::Anchor;
|
||||
use crate::layout::{Anchor, Anchors};
|
||||
use crate::widget::{RegionHandle, RegionKind};
|
||||
|
||||
/// Resolve `self` (a region wrapper) to its live [`RegionHandle`].
|
||||
@@ -699,14 +699,14 @@ pub(crate) fn implicit_creation_anchor(model: &mut Model, rh: RegionHandle) {
|
||||
use crate::layout::Point;
|
||||
match kind {
|
||||
RegionKind::Texture => {
|
||||
data.anchors = vec![
|
||||
data.anchors = Anchors::from([
|
||||
Anchor::new(Point::TopLeft, owner_id, Point::TopLeft, 0.0, 0.0),
|
||||
Anchor::new(Point::BottomRight, owner_id, Point::BottomRight, 0.0, 0.0),
|
||||
];
|
||||
]);
|
||||
}
|
||||
RegionKind::FontString => {
|
||||
let point = justify_anchor_point(data.justify.0);
|
||||
data.anchors = vec![Anchor::new(point, owner_id, point, 0.0, 0.0)];
|
||||
data.anchors = Anchors::from([Anchor::new(point, owner_id, point, 0.0, 0.0)]);
|
||||
}
|
||||
RegionKind::Title => return,
|
||||
}
|
||||
@@ -738,7 +738,7 @@ pub(crate) fn anchor_unanchored_at(
|
||||
if !data.anchors.is_empty() {
|
||||
return;
|
||||
}
|
||||
data.anchors = vec![Anchor::new(point, owner_id, point, 0.0, 0.0)];
|
||||
data.anchors = Anchors::from([Anchor::new(point, owner_id, point, 0.0, 0.0)]);
|
||||
model.touch_layout_region(rh);
|
||||
}
|
||||
|
||||
@@ -791,17 +791,13 @@ pub(super) fn region_set_point(lua: &Lua, this: &Table, args: &MultiValue) -> ml
|
||||
let rel_to_id = resolve_rel_target(&model, &p.target, &who, "SetPoint", me, owner)?;
|
||||
let (point, rel_point, x, y) = (p.point, p.rel_point, p.x, p.y);
|
||||
|
||||
// The frame's store and its unchanged-anchor test (`0x767c70`).
|
||||
let units = model.layout_units();
|
||||
let data = model.region_data.entry(rh).or_default();
|
||||
let new = Anchor::new(point, rel_to_id, rel_point, x, y);
|
||||
// A no-op only when the bit-identical anchor is already the tail and no earlier entry has
|
||||
// this point, as for frames.
|
||||
let same_at_tail = data.anchors.last().is_some_and(|a| anchor_bits_eq(a, &new))
|
||||
&& !data.anchors[..data.anchors.len() - 1]
|
||||
.iter()
|
||||
.any(|a| a.point == point);
|
||||
if !same_at_tail {
|
||||
data.anchors.retain(|a| a.point != point);
|
||||
data.anchors.push(new);
|
||||
if data
|
||||
.anchors
|
||||
.set(Anchor::new(point, rel_to_id, rel_point, x, y), units)
|
||||
{
|
||||
model.touch_layout_region(rh);
|
||||
}
|
||||
Ok(())
|
||||
|
||||
@@ -2,9 +2,11 @@
|
||||
|
||||
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::layout::{Anchor, Anchors, Point};
|
||||
use crate::script::object::anchor_args::{
|
||||
get_point_index, get_point_values, parse_set_all_points, resolve_rel_target,
|
||||
};
|
||||
use crate::script::object::frame_wrapper;
|
||||
use crate::script::region_map::{set_shared, Side};
|
||||
use crate::script::{Model, SCREEN};
|
||||
|
||||
@@ -16,11 +18,8 @@ use super::{
|
||||
/// Install the layout methods into `m`.
|
||||
/// `ClearAllPoints` on a region (`0x767ed0`): the region leaves the cached graph with its edges.
|
||||
pub(in crate::script) fn clear_points(model: &mut Model, rh: crate::widget::RegionHandle) {
|
||||
let d = model.region_data.entry(rh).or_default();
|
||||
// Mutates only on a real change; an unanchored region leaves the graph.
|
||||
let cleared = !d.anchors.is_empty();
|
||||
d.anchors.clear();
|
||||
if cleared {
|
||||
if model.region_data.entry(rh).or_default().anchors.clear() {
|
||||
model.touch_layout_region(rh);
|
||||
}
|
||||
}
|
||||
@@ -145,15 +144,10 @@ pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
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);
|
||||
// The reference clears and sets both (`0x767db0`); only a bit-identical pair skips.
|
||||
let pair = Anchors::from(pair);
|
||||
if !data.anchors.bits_eq(&pair) {
|
||||
data.anchors = pair;
|
||||
model.touch_layout_region(rh);
|
||||
}
|
||||
Ok(())
|
||||
@@ -198,48 +192,38 @@ pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
.map_or(0, |d| d.anchors.len() as i64))
|
||||
})?;
|
||||
|
||||
// GetPoint([n]): the n-th anchor (1-based, default 1) as point, relativeTo, relativePoint, x
|
||||
// and y, or five nils. Frames and regions share one id space and a region may anchor to a
|
||||
// sibling region, so relativeTo is answered with the matching wrapper kind.
|
||||
// GetPoint([n]): the frame's binding (`0x7a2340`), the n-th anchor in point order. Frames and
|
||||
// regions share one id space and a region may anchor to a sibling region, so relativeTo is
|
||||
// answered with the matching wrapper kind.
|
||||
set_shared(
|
||||
lua,
|
||||
m,
|
||||
Side::Region,
|
||||
"GetPoint",
|
||||
|lua, (this, n): (Table, Option<i64>)| {
|
||||
|lua, (this, n): (Table, Value)| {
|
||||
let rh = region_handle_of(lua, &this)?;
|
||||
let idx = get_point_index(lua, n)?;
|
||||
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()
|
||||
.and_then(|d| d.anchors.nth(idx))
|
||||
.copied()
|
||||
};
|
||||
let Some(a) = anchor else {
|
||||
return Ok((Value::Nil, Value::Nil, Value::Nil, Value::Nil, Value::Nil));
|
||||
};
|
||||
let rel = {
|
||||
get_point_values(lua, anchor, |rel| {
|
||||
let is_frame = {
|
||||
let model = lua.app_data_ref::<Model>().expect("model");
|
||||
model.id_to_frame.contains_key(&a.relative_to)
|
||||
model.id_to_frame.contains_key(&rel)
|
||||
};
|
||||
if a.relative_to == SCREEN {
|
||||
Ok(if rel == SCREEN {
|
||||
Value::Nil
|
||||
} else if is_frame {
|
||||
Value::Table(frame_wrapper(lua, a.relative_to)?)
|
||||
Value::Table(frame_wrapper(lua, rel)?)
|
||||
} 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)),
|
||||
))
|
||||
Value::Table(super::region_wrapper(lua, rel)?)
|
||||
})
|
||||
})
|
||||
},
|
||||
)?;
|
||||
Ok(())
|
||||
|
||||
@@ -26,7 +26,7 @@ use mlua::{FromLua, Lua, MultiValue, Table, Value};
|
||||
use super::object::frame_handle_of;
|
||||
use super::{FontObject, FontShadow, Model, Outline, RegionData};
|
||||
use crate::justify::{self, Justify};
|
||||
use crate::layout::{Anchor, Point};
|
||||
use crate::layout::{Anchor, Anchors, Point};
|
||||
use crate::order::DrawLayer;
|
||||
use crate::widget::{FrameHandle, FrameKind, RegionHandle, RegionKind};
|
||||
|
||||
@@ -234,7 +234,12 @@ fn build(model: &mut Model, fh: FrameHandle, blocks: &[Block]) {
|
||||
};
|
||||
let id = model.region_id(rh);
|
||||
let mut d = RegionData {
|
||||
anchors: vec![anchor_for(prev_block, frame_id, Point::TopLeft, next_y)],
|
||||
anchors: Anchors::from([anchor_for(
|
||||
prev_block,
|
||||
frame_id,
|
||||
Point::TopLeft,
|
||||
next_y,
|
||||
)]),
|
||||
// No height: the rect takes the wrapped text's height from the measure round
|
||||
// trip, as the client's font engine gives it (`0x7729b0`, `0x5c2070`).
|
||||
size: Some((width, 0.0)),
|
||||
@@ -286,7 +291,7 @@ fn build(model: &mut Model, fh: FrameHandle, blocks: &[Block]) {
|
||||
_ => None,
|
||||
};
|
||||
let anchors = point
|
||||
.map(|p| vec![anchor_for(prev_block, frame_id, p, next_y)])
|
||||
.map(|p| Anchors::from([anchor_for(prev_block, frame_id, p, next_y)]))
|
||||
.unwrap_or_default();
|
||||
model.region_data.insert(
|
||||
rh,
|
||||
|
||||
@@ -0,0 +1,213 @@
|
||||
//! One anchor slot per point, as the reference keeps them (`anchorPoints[9]`, frame `+0x28`):
|
||||
//! `SetPoint`'s unchanged-anchor test (`0x767c70`) and `GetPoint`'s walk in point order
|
||||
//! (`0x7a2340`).
|
||||
|
||||
use super::common::script;
|
||||
use crate::script::{Model, UiScript};
|
||||
|
||||
/// The mutation epoch, bumped by every write into the layout read set.
|
||||
fn epoch(s: &UiScript) -> u64 {
|
||||
s.lua()
|
||||
.app_data_ref::<Model>()
|
||||
.expect("model app_data")
|
||||
.layout_epoch
|
||||
}
|
||||
|
||||
/// A frame and a texture on it, each with two anchors set out of point order.
|
||||
fn setup(s: &mut UiScript) {
|
||||
s.set_screen_size(800.0, 600.0);
|
||||
s.run(
|
||||
r#"
|
||||
f = CreateFrame("Frame", "Slots")
|
||||
f:SetPoint("BOTTOMRIGHT", nil, "BOTTOMRIGHT", -20, 30)
|
||||
f:SetPoint("TOPLEFT", nil, "TOPLEFT", 10, -10)
|
||||
t = f:CreateTexture(nil, "ARTWORK")
|
||||
t:ClearAllPoints()
|
||||
t:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", -4, 4)
|
||||
t:SetPoint("TOPLEFT", f, "TOPLEFT", 4, -4)
|
||||
"#,
|
||||
)
|
||||
.expect("setup");
|
||||
s.resolve();
|
||||
}
|
||||
|
||||
/// Re-applying an anchor that is not the last one set matches the one held for its point, so the
|
||||
/// call returns before the store and the queue (`0x767ca9`..`0x767cde` → `0x767d5e`).
|
||||
#[test]
|
||||
fn re_setting_an_earlier_anchor_touches_no_layout() {
|
||||
let mut s = script();
|
||||
setup(&mut s);
|
||||
for (who, call) in [
|
||||
(
|
||||
"frame",
|
||||
r#"f:SetPoint("BOTTOMRIGHT", nil, "BOTTOMRIGHT", -20, 30)"#,
|
||||
),
|
||||
(
|
||||
"region",
|
||||
r#"t:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", -4, 4)"#,
|
||||
),
|
||||
] {
|
||||
let before = epoch(&s);
|
||||
s.run(call).expect("re-set");
|
||||
assert_eq!(epoch(&s), before, "{who}: an unchanged anchor is a no-op");
|
||||
}
|
||||
assert!(s.errors().is_empty(), "{:?}", s.errors());
|
||||
}
|
||||
|
||||
/// Both offsets compare within 2⁻²² of the reference's layout unit (`[0x8029d4]`), so float noise
|
||||
/// in a computed offset changes nothing and is not stored; a step past it is a change. At 800×600
|
||||
/// the unit is 1280 FrameXML units, so the threshold is about 3.05e-4.
|
||||
#[test]
|
||||
fn an_offset_within_the_threshold_is_unchanged() {
|
||||
let mut s = script();
|
||||
setup(&mut s);
|
||||
for (who, noisy, getter) in [
|
||||
(
|
||||
"frame",
|
||||
r#"f:SetPoint("TOPLEFT", nil, "TOPLEFT", 10.0001, -10.0001)"#,
|
||||
"local _, _, _, x = f:GetPoint(1); return x",
|
||||
),
|
||||
(
|
||||
"region",
|
||||
r#"t:SetPoint("TOPLEFT", f, "TOPLEFT", 4.0001, -4.0001)"#,
|
||||
"local _, _, _, x = t:GetPoint(1); return x",
|
||||
),
|
||||
] {
|
||||
let before = epoch(&s);
|
||||
let x0: f64 = s.eval(getter).unwrap();
|
||||
s.run(noisy).expect("noisy re-set");
|
||||
assert_eq!(
|
||||
epoch(&s),
|
||||
before,
|
||||
"{who}: noise under the threshold is a no-op"
|
||||
);
|
||||
let x1: f64 = s.eval(getter).unwrap();
|
||||
assert_eq!(
|
||||
x1, x0,
|
||||
"{who}: and the held offset is kept, not overwritten"
|
||||
);
|
||||
}
|
||||
let before = epoch(&s);
|
||||
s.run(r#"f:SetPoint("TOPLEFT", nil, "TOPLEFT", 10.001, -10)"#)
|
||||
.unwrap();
|
||||
assert_ne!(epoch(&s), before, "a step past the threshold is a change");
|
||||
let x: f64 = s
|
||||
.eval("local _, _, _, x = f:GetPoint(1); return x")
|
||||
.unwrap();
|
||||
assert_eq!(x as f32, 10.001_f32);
|
||||
assert!(s.errors().is_empty(), "{:?}", s.errors());
|
||||
}
|
||||
|
||||
/// `GetPoint(n)` answers the n-th occupied slot by point id, TOPLEFT 0 through BOTTOMRIGHT 8 (the
|
||||
/// ids of the name table `0x811a38`, whose entries run alphabetically), whatever order the anchors
|
||||
/// were set in (`0x7a23f5`..`0x7a240b`).
|
||||
#[test]
|
||||
fn get_point_counts_in_point_order() {
|
||||
let mut s = script();
|
||||
setup(&mut s);
|
||||
for who in ["f", "t"] {
|
||||
let (p1, p2): (String, String) = s
|
||||
.eval(&format!("return ({who}:GetPoint(1)), ({who}:GetPoint(2))"))
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
(p1.as_str(), p2.as_str()),
|
||||
("TOPLEFT", "BOTTOMRIGHT"),
|
||||
"{who}"
|
||||
);
|
||||
let p: String = s.eval(&format!("return ({who}:GetPoint())")).unwrap();
|
||||
assert_eq!(p, "TOPLEFT", "{who}: the default is the first");
|
||||
}
|
||||
// SetAllPoints, then a TOPLEFT re-set: the reference still answers TOPLEFT first.
|
||||
let (p1, p2): (String, String) = s
|
||||
.eval(
|
||||
r#"
|
||||
local g = CreateFrame("Frame")
|
||||
g:SetAllPoints(f)
|
||||
g:SetPoint("TOPLEFT", f, "TOPLEFT", 1, -1)
|
||||
return (g:GetPoint(1)), (g:GetPoint(2))
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!((p1.as_str(), p2.as_str()), ("TOPLEFT", "BOTTOMRIGHT"));
|
||||
}
|
||||
|
||||
/// `n` is `lua_isnumber`-tested and truncated (`0x6f34d0`, `0x40a2b0`); an index at or below the
|
||||
/// first, or a non-number, answers the first anchor. Past the last, no value is pushed
|
||||
/// (`0x7a240d`).
|
||||
#[test]
|
||||
fn get_point_index_and_past_the_last() {
|
||||
let mut s = script();
|
||||
setup(&mut s);
|
||||
for who in ["f", "t"] {
|
||||
let n: i64 = s
|
||||
.eval(&format!(
|
||||
"local function n(...) return arg.n end; return n({who}:GetPoint(3))"
|
||||
))
|
||||
.unwrap();
|
||||
assert_eq!(n, 0, "{who}: past the last anchor answers nothing");
|
||||
for arg in ["0", "-4", "{}", "'x'", "1.9"] {
|
||||
let p: String = s.eval(&format!("return ({who}:GetPoint({arg}))")).unwrap();
|
||||
assert_eq!(p, "TOPLEFT", "{who}:GetPoint({arg})");
|
||||
}
|
||||
for arg in ["2", "'2'", "2.5"] {
|
||||
let p: String = s.eval(&format!("return ({who}:GetPoint({arg}))")).unwrap();
|
||||
assert_eq!(p, "BOTTOMRIGHT", "{who}:GetPoint({arg})");
|
||||
}
|
||||
}
|
||||
assert!(s.errors().is_empty(), "{:?}", s.errors());
|
||||
}
|
||||
|
||||
/// `GetNumPoints` counts the occupied slots (`0x7a2311`..`0x7a231b`): a re-set point keeps one.
|
||||
#[test]
|
||||
fn get_num_points_counts_occupied_slots() {
|
||||
let mut s = script();
|
||||
setup(&mut s);
|
||||
s.run(
|
||||
r#"
|
||||
f:SetPoint("TOPLEFT", nil, "TOPLEFT", 50, -50)
|
||||
t:SetPoint("TOPLEFT", f, "TOPLEFT", 8, -8)
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
let (nf, nt): (i64, i64) = s.eval("return f:GetNumPoints(), t:GetNumPoints()").unwrap();
|
||||
assert_eq!((nf, nt), (2, 2));
|
||||
let n: i64 = s
|
||||
.eval("f:ClearAllPoints(); return f:GetNumPoints()")
|
||||
.unwrap();
|
||||
assert_eq!(n, 0);
|
||||
}
|
||||
|
||||
/// The compare runs on the stored values, each offset quantized to `f32` layout units as the
|
||||
/// binding stores it (`0x7a28a3`..`0x7a28e3`), not on the raw difference. At 1280×1024 (aspect
|
||||
/// 1.25, about 1229.4 FrameXML units per layout unit) three `f32` ulps above 650 move the stored
|
||||
/// value by 2⁻²² or more, though the raw step (1.83e-4) is under the threshold's 2.93e-4.
|
||||
#[test]
|
||||
fn the_threshold_compares_the_quantized_stored_offsets() {
|
||||
let mut s = script();
|
||||
s.set_screen_size(1280.0, 1024.0);
|
||||
s.run(r#"f = CreateFrame("Frame"); f:SetPoint("TOPLEFT", nil, "TOPLEFT", 650, 0)"#)
|
||||
.unwrap();
|
||||
s.resolve();
|
||||
let before = epoch(&s);
|
||||
s.run(r#"f:SetPoint("TOPLEFT", nil, "TOPLEFT", 650.0001831054688, 0)"#)
|
||||
.unwrap();
|
||||
assert_ne!(
|
||||
epoch(&s),
|
||||
before,
|
||||
"the stored offsets differ by 2^-22 or more"
|
||||
);
|
||||
let x: f64 = s
|
||||
.eval("local _, _, _, x = f:GetPoint(1); return x")
|
||||
.unwrap();
|
||||
assert_eq!(x, 650.0001831054688);
|
||||
|
||||
// And the other way: five ulps above 700 (a raw 3.05e-4, past the raw threshold) store
|
||||
// within 2^-22 of 700's value, so the call is a no-op.
|
||||
s.run(r#"f:SetPoint("TOPLEFT", nil, "TOPLEFT", 700, 0)"#)
|
||||
.unwrap();
|
||||
s.resolve();
|
||||
let before = epoch(&s);
|
||||
s.run(r#"f:SetPoint("TOPLEFT", nil, "TOPLEFT", 700.0003051757812, 0)"#)
|
||||
.unwrap();
|
||||
assert_eq!(epoch(&s), before, "the stored offsets are within 2^-22");
|
||||
}
|
||||
@@ -435,13 +435,13 @@ fn an_owner_minted_after_its_region_joined_takes_the_owner_edge() {
|
||||
.expect("region");
|
||||
let d = m.region_data.entry(rh).or_default();
|
||||
d.size = Some((8.0, 8.0));
|
||||
d.anchors = vec![crate::layout::Anchor::new(
|
||||
d.anchors = crate::layout::Anchors::from([crate::layout::Anchor::new(
|
||||
crate::layout::Point::TopLeft,
|
||||
root_id,
|
||||
crate::layout::Point::TopLeft,
|
||||
5.0,
|
||||
-5.0,
|
||||
)];
|
||||
)]);
|
||||
m.touch_layout_region(rh);
|
||||
(host, rh)
|
||||
};
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
mod addon_argument_abi;
|
||||
mod addon_index_space;
|
||||
mod adopt;
|
||||
mod anchor_slots;
|
||||
mod anchors;
|
||||
mod backdrop;
|
||||
mod button;
|
||||
|
||||
@@ -1118,12 +1118,8 @@ fn the_region_map_readers_answer_the_way_the_edges_do() {
|
||||
);
|
||||
assert_eq!(
|
||||
s.arity("RRPlate:GetPoint(7)").unwrap(),
|
||||
5,
|
||||
"an out-of-range index still answers five values, all nil"
|
||||
);
|
||||
assert!(
|
||||
s.eval::<bool>("return RRPlate:GetPoint(7) == nil").unwrap(),
|
||||
"…and the first of them is nil"
|
||||
0,
|
||||
"an out-of-range index pushes no value (`0x7a240d`)"
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -80,6 +80,77 @@ fn line_stack_autosize_and_right_flush() {
|
||||
assert_eq!(l2.top, 536.0);
|
||||
}
|
||||
|
||||
/// The layout re-points each shown right cell by its RIGHT slot alone, `SetPoint(RIGHT, leftCell,
|
||||
/// LEFT, width, 0)` (`0x53094f`): a point an addon added beside it keeps its offset, and after a
|
||||
/// `ClearAllPoints` the RIGHT point comes back.
|
||||
#[test]
|
||||
fn the_right_column_write_sets_the_right_slot_only() {
|
||||
let mut s = script();
|
||||
s.set_screen_size(800.0, 600.0);
|
||||
s.run(
|
||||
r#"
|
||||
local owner = CreateFrame("Button", "SlotR")
|
||||
owner:SetPoint("TOPLEFT", 100, -100); owner:SetWidth(40); owner:SetHeight(40)
|
||||
local tt = CreateFrame("GameTooltip", "TTR")
|
||||
tt:SetOwner(owner, "ANCHOR_RIGHT")
|
||||
tt:AddLine("Tough Jerky", 1, 1, 1)
|
||||
tt:AddDoubleLine("One-Hand", "Sword", 1, 1, 1, 1, 1, 1)
|
||||
tt:Show()
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
s.resolve();
|
||||
measure_all(
|
||||
&mut s,
|
||||
&[
|
||||
("Tough Jerky", 80.0, 14.0),
|
||||
("One-Hand", 50.0, 12.0),
|
||||
("Sword", 30.0, 12.0),
|
||||
],
|
||||
);
|
||||
// maxw = max(80, 50 + 40 + 30) = 120.
|
||||
let right_anchor = r#"
|
||||
local p, rel, rp, x, y = TTRTextRight2:GetPoint(N)
|
||||
return p, rel == TTRTextLeft2, rp, x, y
|
||||
"#;
|
||||
|
||||
// An addon adds LEFT without clearing; LEFT precedes RIGHT in slot order.
|
||||
s.run(r#"TTRTextRight2:SetPoint("LEFT", TTRTextLeft2, "RIGHT", 7, 0)"#)
|
||||
.unwrap();
|
||||
s.resolve();
|
||||
s.resolve();
|
||||
let (p, x): (String, f64) = s
|
||||
.eval("local p, _, _, x = TTRTextRight2:GetPoint(1); return p, x")
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
(p.as_str(), x),
|
||||
("LEFT", 7.0),
|
||||
"the addon's point keeps its offset"
|
||||
);
|
||||
let (p, rel, rp, x, y): (String, bool, String, f64, f64) =
|
||||
s.eval(&right_anchor.replace('N', "2")).unwrap();
|
||||
assert_eq!(
|
||||
(p.as_str(), rel, rp.as_str(), x, y),
|
||||
("RIGHT", true, "LEFT", 120.0, 0.0)
|
||||
);
|
||||
|
||||
s.run("TTRTextRight2:ClearAllPoints()").unwrap();
|
||||
s.resolve();
|
||||
s.resolve();
|
||||
assert_eq!(
|
||||
s.eval::<i64>("return TTRTextRight2:GetNumPoints()")
|
||||
.unwrap(),
|
||||
1,
|
||||
"the layout sets RIGHT again after a ClearAllPoints"
|
||||
);
|
||||
let (p, rel, rp, x, y): (String, bool, String, f64, f64) =
|
||||
s.eval(&right_anchor.replace('N', "1")).unwrap();
|
||||
assert_eq!(
|
||||
(p.as_str(), rel, rp.as_str(), x, y),
|
||||
("RIGHT", true, "LEFT", 120.0, 0.0)
|
||||
);
|
||||
}
|
||||
|
||||
/// An empty line is a one-unit row plus its 2 px gap, as the reference's size getters floor a
|
||||
/// FontString at one FrameXML unit; the row and the plate read that floor from one constant.
|
||||
#[test]
|
||||
@@ -804,6 +875,39 @@ fn anchor_cursor_follows_the_cursor_every_frame() {
|
||||
);
|
||||
}
|
||||
|
||||
/// The cursor update (`0x530b20`) makes one `SetPoint` for BOTTOM (`0x767c70`) and clears
|
||||
/// nothing, so a point an addon set after `SetOwner` stays held beside it.
|
||||
#[test]
|
||||
fn anchor_cursor_keeps_a_point_set_after_set_owner() {
|
||||
let mut s = script();
|
||||
s.set_screen_size(800.0, 600.0);
|
||||
s.run(
|
||||
r#"
|
||||
KeepTip = CreateFrame("GameTooltip", "KeepTip")
|
||||
KeepHost = CreateFrame("Frame", "KeepHost")
|
||||
KeepTip:SetOwner(KeepHost, "ANCHOR_CURSOR")
|
||||
KeepTip:AddLine("Copper Ore")
|
||||
KeepTip:SetWidth(120) KeepTip:SetHeight(40)
|
||||
KeepTip:Show()
|
||||
KeepTip:SetPoint("BOTTOMRIGHT", KeepHost, "BOTTOMRIGHT", 0, 0)
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
for (x, y) in [(300.0, 200.0), (120.0, 480.0)] {
|
||||
s.mouse_move(x, y);
|
||||
s.resolve();
|
||||
assert_eq!(
|
||||
s.eval::<i64>("return KeepTip:GetNumPoints()").unwrap(),
|
||||
2,
|
||||
"BOTTOM and the addon's BOTTOMRIGHT"
|
||||
);
|
||||
let (p1, p2): (String, String) = s
|
||||
.eval("return (KeepTip:GetPoint(1)), (KeepTip:GetPoint(2))")
|
||||
.unwrap();
|
||||
assert_eq!((p1.as_str(), p2.as_str()), ("BOTTOM", "BOTTOMRIGHT"));
|
||||
}
|
||||
}
|
||||
|
||||
/// `GetAnchorType` (`0x5313e0`, table `0x854198`) answers one string, `[+0x318]` through the name
|
||||
/// table `0x531530`; `_Nameplates.lua:479` compares it with what it passed to `SetOwner`.
|
||||
#[test]
|
||||
|
||||
@@ -17,7 +17,7 @@ use super::object::{frame_handle_of, publish_global};
|
||||
use super::region::region_wrapper;
|
||||
use super::visibility;
|
||||
use super::{FontObject, Model, RegionData};
|
||||
use crate::layout::{Anchor, Point};
|
||||
use crate::layout::{Anchor, Anchors, Point};
|
||||
use crate::order::DrawLayer;
|
||||
use crate::widget::{
|
||||
FrameHandle, KindState, RegionKind, TooltipAnchor, TooltipState, TOOLTIP_DOUBLE_GAP,
|
||||
@@ -168,7 +168,7 @@ fn ensure_lines(lua: &Lua, this: &Table, n: usize) -> mlua::Result<()> {
|
||||
d.hidden = true;
|
||||
// Left-justified as in the template; it shows once a wrap line pins its width.
|
||||
d.justify.set_h(super::JustifyH::Left);
|
||||
d.anchors = vec![match prev_left {
|
||||
d.anchors = Anchors::from([match prev_left {
|
||||
None => Anchor::new(
|
||||
Point::TopLeft,
|
||||
frame_id,
|
||||
@@ -186,7 +186,7 @@ fn ensure_lines(lua: &Lua, this: &Table, n: usize) -> mlua::Result<()> {
|
||||
-TOOLTIP_LINE_GAP,
|
||||
)
|
||||
}
|
||||
}];
|
||||
}]);
|
||||
model.region_data.insert(left, d);
|
||||
model.touch_measure(left); // an adopted cell can arrive text-in-hand
|
||||
model.touch_layout_region(left); // a line row entered the layout graph
|
||||
@@ -214,7 +214,10 @@ fn ensure_lines(lua: &Lua, this: &Table, n: usize) -> mlua::Result<()> {
|
||||
};
|
||||
d.hidden = true;
|
||||
d.justify.set_h(super::JustifyH::Right);
|
||||
d.anchors = vec![Anchor::new(Point::Right, left_id, Point::Right, 0.0, 0.0)];
|
||||
// RIGHT on the left cell's LEFT, the point the layout re-sets with the plate
|
||||
// width as x (`0x53094f`), as the template's `$parentTextRight1` declares it.
|
||||
d.anchors =
|
||||
Anchors::from([Anchor::new(Point::Right, left_id, Point::Left, 0.0, 0.0)]);
|
||||
model.region_data.insert(right, d);
|
||||
model.touch_measure(right); // an adopted cell can arrive text-in-hand
|
||||
model.touch_layout_region(right); // a line row entered the layout graph
|
||||
@@ -452,26 +455,21 @@ fn apply_anchor(
|
||||
TooltipAnchor::Cursor | TooltipAnchor::None | TooltipAnchor::Preserve => None,
|
||||
};
|
||||
if matches!(anchor, TooltipAnchor::Cursor | TooltipAnchor::None) {
|
||||
let dropped = match model.layout_inputs.get_mut(&h) {
|
||||
Some(input) if !input.anchors.is_empty() => {
|
||||
input.anchors.clear();
|
||||
true
|
||||
}
|
||||
_ => false,
|
||||
};
|
||||
let dropped = model
|
||||
.layout_inputs
|
||||
.get_mut(&h)
|
||||
.is_some_and(|input| input.anchors.clear());
|
||||
if dropped {
|
||||
model.touch_layout_frame(h);
|
||||
}
|
||||
}
|
||||
if let Some((own, rel)) = pts {
|
||||
let owner_id = model.frame_id(owner);
|
||||
let new = Anchor::new(own, owner_id, rel, x, y);
|
||||
let new = Anchors::from([Anchor::new(own, owner_id, rel, x, y)]);
|
||||
let input = model.layout_inputs.entry(h).or_default();
|
||||
// No-op when unchanged, so the bag hover's per-frame SetOwner does not dirty the layout.
|
||||
let same = input.anchors.len() == 1
|
||||
&& crate::script::object::anchor_bits_eq(&input.anchors[0], &new);
|
||||
if !same {
|
||||
input.anchors = vec![new];
|
||||
if !input.anchors.bits_eq(&new) {
|
||||
input.anchors = new;
|
||||
model.touch_layout_frame(h);
|
||||
}
|
||||
}
|
||||
@@ -710,15 +708,12 @@ fn cursor_anchor(model: &mut Model, h: FrameHandle) {
|
||||
cx / scale,
|
||||
cy / scale,
|
||||
);
|
||||
let units = model.layout_units();
|
||||
let input = model.layout_inputs.entry(h).or_default();
|
||||
// `0x767c70`'s change detect: the one slot is replaced only on a real change.
|
||||
let same =
|
||||
input.anchors.len() == 1 && crate::script::object::anchor_bits_eq(&input.anchors[0], &new);
|
||||
if same {
|
||||
return;
|
||||
// `0x767c70`'s change detect: the BOTTOM slot is replaced only on a real change.
|
||||
if input.anchors.set(new, units) {
|
||||
model.touch_layout_frame(h);
|
||||
}
|
||||
input.anchors = vec![new];
|
||||
model.touch_layout_frame(h);
|
||||
}
|
||||
|
||||
pub(super) fn layout_tooltips(model: &mut Model) {
|
||||
@@ -787,20 +782,22 @@ pub(super) fn layout_tooltips(model: &mut Model) {
|
||||
// Inside the resolve, so a note and not a touch; the incremental pass still has to hear
|
||||
// that these inputs changed.
|
||||
model.note_layout_frame_write(h);
|
||||
// Right-flush each double line: left.right + (maxw - left.width) is the text inset.
|
||||
for (i, (l, r)) in rows.iter().enumerate() {
|
||||
// Right-flush each shown right cell: one `SetPoint(RIGHT, leftCell, LEFT, width, 0)`
|
||||
// (`0x530910`..`0x53094f`), which leaves any other point the cell holds alone.
|
||||
let units = model.layout_units();
|
||||
for (i, (_, r)) in rows.iter().enumerate() {
|
||||
if r.is_none() {
|
||||
continue;
|
||||
}
|
||||
let lw = l.map_or(0.0, |(w, _)| w);
|
||||
let mut wrote = false;
|
||||
if let Some(d) = model.region_data.get_mut(&rights[i]) {
|
||||
if let Some(a) = d.anchors.first_mut() {
|
||||
a.x_off = maxw - lw;
|
||||
wrote = true;
|
||||
}
|
||||
}
|
||||
if wrote {
|
||||
let Some(&left_id) = model.region_to_id.get(&lefts[i]) else {
|
||||
continue;
|
||||
};
|
||||
let new = Anchor::new(Point::Right, left_id, Point::Left, maxw, 0.0);
|
||||
let stored = model
|
||||
.region_data
|
||||
.get_mut(&rights[i])
|
||||
.is_some_and(|d| d.anchors.set(new, units));
|
||||
if stored {
|
||||
model.note_layout_region_write(rights[i]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
use mlua::{Lua, Table, Value, Variadic};
|
||||
|
||||
use crate::layout::{Anchor, Point};
|
||||
use crate::layout::{Anchor, Anchors, Point};
|
||||
use crate::script::binding_abi::optional_string;
|
||||
use crate::script::object::frame_handle_of;
|
||||
use crate::script::region::region_handle_of;
|
||||
@@ -233,13 +233,14 @@ pub(in crate::script) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
let d = model.region_data.entry(lh).or_default();
|
||||
d.hidden = true;
|
||||
d.justify.set_h(crate::script::JustifyH::Left);
|
||||
d.anchors = vec![anchor];
|
||||
d.anchors = Anchors::from([anchor]);
|
||||
}
|
||||
{
|
||||
let d = model.region_data.entry(rh).or_default();
|
||||
d.hidden = true;
|
||||
d.justify.set_h(crate::script::JustifyH::Right);
|
||||
d.anchors = vec![Anchor::new(Point::Right, left_id, Point::Right, 0.0, 0.0)];
|
||||
d.anchors =
|
||||
Anchors::from([Anchor::new(Point::Right, left_id, Point::Right, 0.0, 0.0)]);
|
||||
}
|
||||
model.touch_layout_region(lh); // two line rows entered the layout graph
|
||||
model.touch_layout_region(rh);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use mlua::{Lua, Value};
|
||||
|
||||
use crate::layout::{Anchor, Rect};
|
||||
use crate::layout::{Anchors, Rect};
|
||||
use crate::order::ZTarget;
|
||||
|
||||
/// A value the host passes into Lua, such as a `fire_event` argument, without an mlua handle.
|
||||
@@ -370,7 +370,7 @@ pub(crate) struct RegionData {
|
||||
/// extent, never the owner's rect; one still unset leaves the region unresolved (`0x767a20`).
|
||||
/// Empty is a Lua region nobody anchored, which never draws: every other has an authored or
|
||||
/// creation-time anchor ([`super::region::implicit_creation_anchor`]).
|
||||
pub(crate) anchors: Vec<Anchor>,
|
||||
pub(crate) anchors: Anchors,
|
||||
/// `SetDesaturated` (`0x79c1e0`): the renderer greys the texel, so the binding reports support.
|
||||
pub(crate) desaturated: bool,
|
||||
/// `SetNonSpaceWrap`/`CanNonSpaceWrap` (`0x79e9f0`/`0x79ead0`), `None` on. Stored, not applied:
|
||||
|
||||
Reference in New Issue
Block a user