Layout writes invalidate as 1.12.1 does; only a reached node re-resolves (#964)
The solver took any node's recomputed rect whenever it resolved, because about 55 layout writes named a node without invalidating it. So a freed region stayed in its dependents' anchors, `SetTexture` re-resolved a failed texture, and a scroll child kept its authored anchors behind a per-solve override. In 1.12.1 a node re-resolves only when invalidated or a target stores a new rect (`0x768d20`, slot 0 `0x768cf0`). `SetTexture` only dirties the draw (`0x770200`). A destructor frees its dependents' anchors to it (`0x768e20` → `0x767e70`, no invalidate). A scroll child is re-anchored to one TOPLEFT (`0x787100`). Every write now invalidates exactly where the reference's does, and the solver's guess, the hold and the art latch are gone. The tooltip invalidates only on its relayout (`0x530640`). Tests bite on the parent; the verifier is green; the stock UI's quads are byte-identical. Fixes #947 Fixes #949 Fixes #958 🤖 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
a9dd325bb7
commit
51a2ce5586
@@ -677,6 +677,11 @@ fn structural_change_costs() {
|
||||
r#"
|
||||
BenchHost = CreateFrame("Frame", "BenchHost", UIParent)
|
||||
BenchHost:SetPoint("CENTER", 0, 0); BenchHost:SetWidth(200); BenchHost:SetHeight(100)
|
||||
BenchText = BenchHost:CreateFontString(nil, "ARTWORK", "GameFontNormal")
|
||||
BenchText:SetPoint("TOPLEFT", BenchHost, "TOPLEFT", 4, -4); BenchText:SetText("bench")
|
||||
BenchArt = BenchHost:CreateTexture(nil, "ARTWORK")
|
||||
BenchArt:SetPoint("BOTTOMLEFT", BenchHost, "BOTTOMLEFT", 4, 4)
|
||||
BenchArt:SetTexture("Interface\\Buttons\\UI-CheckBox-Check")
|
||||
bench_n = 0
|
||||
bench_spec = { bgFile = "Interface\\Tooltips\\UI-Tooltip-Background",
|
||||
edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", tile = true, tileSize = 16,
|
||||
@@ -687,8 +692,28 @@ fn structural_change_costs() {
|
||||
for _ in 0..4 {
|
||||
app_frame(&mut s);
|
||||
}
|
||||
let cases: [(&str, &str); 5] = [
|
||||
let cases: [(&str, &str); 9] = [
|
||||
("no-op", "bench_n = bench_n + 1"),
|
||||
// Setters an OnUpdate calls every frame with the value already held: each is a write the
|
||||
// reference invalidates for, and must not re-resolve the world.
|
||||
(
|
||||
"SetWidth same",
|
||||
"bench_n = bench_n + 1; BenchHost:SetWidth(200)",
|
||||
),
|
||||
(
|
||||
"SetPoint same",
|
||||
"bench_n = bench_n + 1; BenchHost:SetPoint('CENTER', 0, 0)",
|
||||
),
|
||||
(
|
||||
"SetText same",
|
||||
"bench_n = bench_n + 1; BenchText:SetText('bench')",
|
||||
),
|
||||
(
|
||||
"SetTexture swap",
|
||||
"bench_n = bench_n + 1; BenchArt:SetTexture(math.mod(bench_n, 2) == 0 and \
|
||||
'Interface\\\\Buttons\\\\UI-CheckBox-Check' or \
|
||||
'Interface\\\\Buttons\\\\UI-CheckBox-Up')",
|
||||
),
|
||||
(
|
||||
"SetWidth",
|
||||
"bench_n = bench_n + 1; BenchHost:SetWidth(200 + math.mod(bench_n, 2))",
|
||||
@@ -712,17 +737,23 @@ fn structural_change_costs() {
|
||||
];
|
||||
const N: u32 = 200;
|
||||
for (name, lua) in cases {
|
||||
let d0 = s.layout_derivations();
|
||||
let (d0, s0, r0) = (s.layout_derivations(), s.layout_solves(), s.layout_rounds());
|
||||
let mut widest = (0, 0);
|
||||
let t = Instant::now();
|
||||
for _ in 0..N {
|
||||
s.run(lua).unwrap();
|
||||
app_frame(&mut s);
|
||||
std::hint::black_box(s.extract());
|
||||
let (f, r) = s.layout_last_scope();
|
||||
widest = (widest.0.max(f), widest.1.max(r));
|
||||
}
|
||||
println!(
|
||||
"STRUCT {name:<14} {:.3} ms/iter derivations/iter={:.2}",
|
||||
"STRUCT {name:<15} {:.3} ms/iter derivations/iter={:.2} solves/iter={:.2} \
|
||||
rounds/iter={:.2} widest scope={widest:?}",
|
||||
ms(t) / f64::from(N),
|
||||
(s.layout_derivations() - d0) as f64 / f64::from(N),
|
||||
(s.layout_solves() - s0) as f64 / f64::from(N),
|
||||
(s.layout_rounds() - r0) as f64 / f64::from(N),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -232,6 +232,20 @@ impl Anchors {
|
||||
held
|
||||
}
|
||||
|
||||
/// Empty every slot naming a target in `dying`, returning whether any did: the unhook a dying
|
||||
/// layout object runs on each dependent (`0x767e70`, from `0x768e20`).
|
||||
#[inline]
|
||||
pub fn release_targets(&mut self, dying: &[u32]) -> bool {
|
||||
let mut freed = false;
|
||||
for slot in &mut self.0 {
|
||||
if slot.is_some_and(|a| dying.contains(&a.relative_to)) {
|
||||
*slot = None;
|
||||
freed = true;
|
||||
}
|
||||
}
|
||||
freed
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.0.iter().all(Option::is_none)
|
||||
@@ -905,27 +919,15 @@ impl LayoutSolver {
|
||||
self.live.push(h);
|
||||
}
|
||||
|
||||
/// [`set_frame`](Self::set_frame) with `anchor` as the only anchor: a ScrollFrame child's.
|
||||
pub fn set_frame_anchored(&mut self, h: Handle, src: &LayoutInput, anchor: Anchor) {
|
||||
self.ensure(h);
|
||||
self.touch(h);
|
||||
let dst = &mut self.input[h as usize];
|
||||
dst.anchors = Anchors::from([anchor]);
|
||||
Self::copy_fields(dst, src);
|
||||
self.is_frame[h as usize] = true;
|
||||
self.live.push(h);
|
||||
}
|
||||
|
||||
/// The state node `h` was in when this resolve began, its rect while its valid bit is set,
|
||||
/// and whether it was invalidated since (`0x7680e0`: its own write, a cleared valid bit).
|
||||
///
|
||||
/// The reference resolves only what is invalidated: by its own write, or by a target's
|
||||
/// resolve that stored a new rect (`0x768cf0`). A failed resolve stores nothing and skips that
|
||||
/// slot (`0x768d55`), and `ClearAllPoints` invalidates nothing (`0x767620`). So a node not
|
||||
/// reached keeps this state when its resolve fails or reads a target with no rect. Any other
|
||||
/// node not reached is recomputed, which gives back its state only if every write that moved
|
||||
/// its inputs since also invalidated it; the solver cannot tell one that did not. The rect is
|
||||
/// also what the keep test ([`set_keep_eps`](Self::set_keep_eps)) holds a new one against.
|
||||
/// resolve that stored a new rect (`0x768cf0`). A node the resolve does not reach keeps this
|
||||
/// state, whatever its inputs now give, as a write that invalidates nothing (`ClearAllPoints`
|
||||
/// `0x767620`, a dying target's unhook `0x767e70`, `SetTexture` `0x770200`) leaves it. The
|
||||
/// rect is also what the keep test ([`set_keep_eps`](Self::set_keep_eps)) holds a new one
|
||||
/// against.
|
||||
pub fn set_entry(&mut self, h: Handle, rect: Option<Rect>, invalidated: bool) {
|
||||
self.ensure(h);
|
||||
self.touch(h);
|
||||
@@ -951,30 +953,18 @@ impl LayoutSolver {
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether node `h` takes the rect its inputs give it, by the rule [`set_entry`]
|
||||
/// documents: when it was invalidated, when an anchor target stored a new rect, or when the
|
||||
/// rect resolved against targets that all hold one. `reads` lists its anchor targets.
|
||||
/// Whether the resolve reaches node `h`, by the rule [`set_entry`] documents: it has no entry
|
||||
/// state, it was invalidated, or an anchor target stored a new rect. Only then does it take
|
||||
/// the rect its inputs give it, or fail. `reads` lists its anchor targets. Not modelled yet:
|
||||
/// invalidating a suspended (hidden) object reaches every dependent at once (`0x768178`–
|
||||
/// `0x76819c`).
|
||||
///
|
||||
/// [`set_entry`]: Self::set_entry
|
||||
pub fn takes_resolve(
|
||||
&self,
|
||||
h: Handle,
|
||||
resolved: bool,
|
||||
reads: impl Iterator<Item = Handle>,
|
||||
) -> bool {
|
||||
pub fn reached(&self, h: Handle, mut reads: impl Iterator<Item = Handle>) -> bool {
|
||||
let i = h as usize;
|
||||
if !self.has_entry.get(i).copied().unwrap_or(false) || self.pending[i] {
|
||||
return true;
|
||||
}
|
||||
let mut reads_failed = false;
|
||||
for t in reads {
|
||||
if self.rect(t).is_none() {
|
||||
reads_failed = true;
|
||||
} else if self.changed(t) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
resolved && !reads_failed
|
||||
!self.has_entry.get(i).copied().unwrap_or(false)
|
||||
|| self.pending[i]
|
||||
|| reads.any(|t| self.changed(t))
|
||||
}
|
||||
|
||||
/// The entry rect of node `h`, which it keeps when the resolve does not reach it.
|
||||
@@ -1060,14 +1050,8 @@ impl LayoutSolver {
|
||||
|
||||
fn resolve_one(&mut self, h: Handle) {
|
||||
let i = h as usize;
|
||||
let outcome = {
|
||||
let input = &self.input[i];
|
||||
let rects = &self.rect;
|
||||
resolve_rect(input, |t| rects.get(t as usize).copied().flatten())
|
||||
};
|
||||
let anchors = self.input[i].anchors;
|
||||
let resolved = matches!(outcome, ResolveOutcome::Resolved(_));
|
||||
if !self.takes_resolve(h, resolved, anchors.iter().map(|a| a.relative_to)) {
|
||||
if !self.reached(h, anchors.iter().map(|a| a.relative_to)) {
|
||||
// Not reached: the entry state stands, rect or none.
|
||||
self.rect[i] = self.cached[i];
|
||||
if self.rect[i].is_none() {
|
||||
@@ -1075,6 +1059,11 @@ impl LayoutSolver {
|
||||
}
|
||||
return;
|
||||
}
|
||||
let outcome = {
|
||||
let input = &self.input[i];
|
||||
let rects = &self.rect;
|
||||
resolve_rect(input, |t| rects.get(t as usize).copied().flatten())
|
||||
};
|
||||
match outcome {
|
||||
ResolveOutcome::Resolved(r) => {
|
||||
let kept = self.cached[i].filter(|&c| keeps_rect(c, r, self.keep_eps));
|
||||
@@ -1770,44 +1759,4 @@ mod tests {
|
||||
);
|
||||
assert!(s.unresolvable().is_empty());
|
||||
}
|
||||
|
||||
/// 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() {
|
||||
const A: Handle = 1;
|
||||
let mut s = LayoutSolver::new();
|
||||
let input = LayoutInput::sized(
|
||||
vec![
|
||||
Anchor::new(Point::TopLeft, SCREEN, Point::TopLeft, 0.0, 0.0),
|
||||
Anchor::new(Point::BottomRight, SCREEN, Point::BottomRight, 0.0, 0.0),
|
||||
],
|
||||
100.0,
|
||||
40.0,
|
||||
);
|
||||
|
||||
// Plain: both anchors pin all four edges, so the size is ignored.
|
||||
s.begin();
|
||||
s.set_external(SCREEN, screen_rect());
|
||||
s.set_frame(A, &input);
|
||||
s.solve();
|
||||
assert_eq!(
|
||||
bits(s.rect(A).expect("resolved")),
|
||||
bits(Rect::new(0.0, 0.0, 600.0, 800.0))
|
||||
);
|
||||
|
||||
// Overridden: one TOPLEFT pin at (10, -5), so the 100x40 size decides the other edges.
|
||||
s.begin();
|
||||
s.set_external(SCREEN, screen_rect());
|
||||
s.set_frame_anchored(
|
||||
A,
|
||||
&input,
|
||||
Anchor::new(Point::TopLeft, SCREEN, Point::TopLeft, 10.0, -5.0),
|
||||
);
|
||||
s.solve();
|
||||
assert_eq!(
|
||||
bits(s.rect(A).expect("resolved")),
|
||||
bits(Rect::new(555.0, 10.0, 595.0, 110.0))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -307,10 +307,14 @@ impl Installed {
|
||||
pub(crate) fn install(model: &mut Model, fh: FrameHandle, spec: Option<Backdrop>) {
|
||||
// Each freed piece names its node, and each built one below, so the next resolve takes the
|
||||
// swap through the cached graph.
|
||||
let old = model.backdrops.remove(&fh);
|
||||
for rh in old.iter().flat_map(|o| o.pieces).flatten() {
|
||||
super::region::free_region(model, rh);
|
||||
}
|
||||
let old: Vec<RegionHandle> = model
|
||||
.backdrops
|
||||
.remove(&fh)
|
||||
.iter()
|
||||
.flat_map(|o| o.pieces)
|
||||
.flatten()
|
||||
.collect();
|
||||
super::region::free_regions(model, &old);
|
||||
let Some(spec) = spec.filter(|_| model.arena.frame(fh).is_some()) else {
|
||||
return;
|
||||
};
|
||||
@@ -365,8 +369,9 @@ pub(crate) fn install(model: &mut Model, fh: FrameHandle, spec: Option<Backdrop>
|
||||
);
|
||||
}
|
||||
}
|
||||
// Each piece is invalidated after its points are set (`0x77e9ca`..`0x77ec77`, `0x77ed05`..).
|
||||
for rh in pieces.iter().flatten() {
|
||||
model.touch_layout_region(*rh);
|
||||
model.invalidate_region(*rh, true);
|
||||
}
|
||||
model.backdrops.insert(
|
||||
fh,
|
||||
@@ -425,8 +430,9 @@ pub(crate) fn set_metrics(
|
||||
};
|
||||
(d.anchors, d.size) = piece_geometry(owner, layout);
|
||||
}
|
||||
// Set as `0x77e8d0` sets them, so invalidated as there.
|
||||
for rh in pieces.iter().flatten() {
|
||||
model.touch_layout_region(*rh);
|
||||
model.invalidate_region(*rh, true);
|
||||
}
|
||||
if let Some(rect) = model.resolved.get(&fh).copied() {
|
||||
run_texcoords(model, fh, rect);
|
||||
|
||||
@@ -481,7 +481,7 @@ fn set_slot_texture(
|
||||
if let Some(blend) = blend {
|
||||
data.blend = blend;
|
||||
}
|
||||
model.touch_layout_art(rh);
|
||||
model.note_art(rh);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -341,10 +341,11 @@ fn lay_out(model: &mut Model, i: usize, g: BubbleGeometry) {
|
||||
-g.margin,
|
||||
),
|
||||
]);
|
||||
// Once per geometry change: each node it wrote re-derives.
|
||||
model.touch_layout_frame(frame);
|
||||
model.touch_layout_region(tail);
|
||||
model.touch_layout_region(text);
|
||||
// Once per geometry change: each node it wrote is invalidated, as the bubble build's
|
||||
// `SetPoint`s with the invalidate flag do (`0x4b0b6b`, `0x4b0b94`).
|
||||
model.invalidate_frame(frame, true);
|
||||
model.invalidate_region(tail, true);
|
||||
model.invalidate_region(text, true);
|
||||
model.chat_bubbles.bubbles[i].laid_out = Some(g);
|
||||
}
|
||||
|
||||
@@ -393,7 +394,8 @@ fn fit_width(model: &mut Model, i: usize, g: BubbleGeometry) {
|
||||
d.size = Some((width, 0.0));
|
||||
}
|
||||
model.touch_measure(text);
|
||||
model.touch_layout_region(text);
|
||||
// `SetWidth`, which invalidates (`0x7683d0`).
|
||||
model.invalidate_region(text, true);
|
||||
}
|
||||
|
||||
/// The per-frame write, each property only when it moved.
|
||||
@@ -457,7 +459,8 @@ fn drive(
|
||||
seat.0,
|
||||
seat.1,
|
||||
)]);
|
||||
model.touch_layout_region(text);
|
||||
// `SetPoint` with the invalidate flag (`0x4b157a`).
|
||||
model.invalidate_region(text, true);
|
||||
model.chat_bubbles.bubbles[i].seat = Some(seat);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -474,7 +474,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
data.texture = path;
|
||||
data.circular = true;
|
||||
data.portrait_unit = None;
|
||||
model.touch_layout_art(rh);
|
||||
model.note_art(rh);
|
||||
Ok(())
|
||||
})?,
|
||||
)?;
|
||||
|
||||
@@ -53,9 +53,9 @@ pub(super) fn seat_text_region(model: &mut Model, h: FrameHandle) {
|
||||
data.anchors = anchors;
|
||||
}
|
||||
data.size = size;
|
||||
if !same_anchors || size_moved {
|
||||
model.touch_layout_region(rh);
|
||||
}
|
||||
// The seat's points and size, each set with an invalidate (`0x77b905`, `0x77b91e`/`0x77b940`,
|
||||
// `0x77b96b`).
|
||||
model.invalidate_region(rh, !same_anchors || size_moved);
|
||||
if size_moved {
|
||||
// The width is the wrap width, which keys the measure.
|
||||
model.touch_measure(rh);
|
||||
@@ -137,10 +137,10 @@ pub(super) fn relayout(lua: &Lua, h: FrameHandle) -> bool {
|
||||
// `fld top; fadd bottom; fadd st, st(1)`.
|
||||
let height = (top + bottom) + text_h;
|
||||
let input = model.layout_inputs.entry(h).or_default();
|
||||
if input.height.to_bits() != height.to_bits() {
|
||||
input.height = height;
|
||||
model.touch_layout_frame(h);
|
||||
}
|
||||
let moved = input.height.to_bits() != height.to_bits();
|
||||
input.height = height;
|
||||
// `SetHeight`, which invalidates (`0x7683f0`).
|
||||
model.invalidate_frame(h, moved);
|
||||
true
|
||||
}
|
||||
|
||||
|
||||
@@ -189,8 +189,10 @@ fn place(model: &mut Model, h: FrameHandle, rect: Rect, link: &str, text: &str)
|
||||
input.anchors = pin;
|
||||
input.width = w;
|
||||
input.height = ht;
|
||||
model.touch_layout_frame(h);
|
||||
model.note_authored_size(h);
|
||||
}
|
||||
// The points are cleared (`0x7a3415`), one set with the invalidate flag (`0x7a3441`), and the
|
||||
// size set through the setters, which invalidate (`0x7a3457`, `0x7a346b`).
|
||||
model.invalidate_frame(h, changed);
|
||||
changed
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::layout::{self, Anchor, LayoutInput, Point, Rect};
|
||||
use crate::widget::{FrameHandle, FrameKind, KindState, RegionHandle};
|
||||
use crate::layout::{self, Anchor, LayoutInput, Rect};
|
||||
use crate::widget::{FrameHandle, RegionHandle};
|
||||
|
||||
use super::types::RegionData;
|
||||
use super::{MeasureRequest, Model, UiScript, SCREEN};
|
||||
@@ -90,13 +90,9 @@ const NO_NODE: u64 = 0;
|
||||
|
||||
/// A frame's per-node hash; the full derive and the incremental pass must agree bit for bit.
|
||||
#[inline]
|
||||
fn frame_node_hash(input: &LayoutInput, over: Option<Anchor>) -> u64 {
|
||||
fn frame_node_hash(input: &LayoutInput) -> u64 {
|
||||
let mut node = InputFingerprint::default();
|
||||
node.input(input);
|
||||
match over {
|
||||
Some(a) => node.anchors(std::slice::from_ref(&a)),
|
||||
None => node.feed(u64::MAX),
|
||||
}
|
||||
node.finish()
|
||||
}
|
||||
|
||||
@@ -111,7 +107,8 @@ pub(super) fn effective_measure(data: &RegionData) -> Option<super::MeasuredText
|
||||
|
||||
/// A region's per-node input hash: its anchors, explicit size, owner, whose effective scale the
|
||||
/// sweep reads (`None` for an owner with no layout id), and, where an axis authored 0 takes its
|
||||
/// span from them, its effective measure and its art: exactly what the sweep reads.
|
||||
/// span from it, its effective measure. Not a Texture's art, which only a resolve that reaches the
|
||||
/// node reads, so art set since moves no node, as `SetTexture` invalidates none (`0x770200`).
|
||||
#[inline]
|
||||
pub(super) fn region_node_hash(data: &RegionData, owner: Option<u32>) -> u64 {
|
||||
let mut node = InputFingerprint::default();
|
||||
@@ -132,20 +129,6 @@ pub(super) fn region_node_hash(data: &RegionData, owner: Option<u32>) -> u64 {
|
||||
}
|
||||
None => node.feed(u64::MAX),
|
||||
}
|
||||
// The art (`content_span`): a new texture moves such a region, while a sized region's art
|
||||
// cannot move its rect.
|
||||
if !data.anchors.is_empty() && unsized_axis {
|
||||
node.feed(data.fill.is_some() as u64);
|
||||
match &data.texture {
|
||||
Some(path) => {
|
||||
node.feed(path.len() as u64);
|
||||
for b in path.as_bytes() {
|
||||
node.feed(u64::from(*b));
|
||||
}
|
||||
}
|
||||
None => node.feed(u64::MAX),
|
||||
}
|
||||
}
|
||||
node.finish()
|
||||
}
|
||||
|
||||
@@ -211,16 +194,10 @@ pub(crate) struct LayoutScope {
|
||||
dirty: Vec<bool>,
|
||||
/// The closure's worklist.
|
||||
stack: Vec<u32>,
|
||||
/// The cached frame roster, `(handle, id, ScrollFrame anchor override)`, kept across resolves.
|
||||
plan: Vec<(FrameHandle, u32, Option<Anchor>)>,
|
||||
/// The cached frame roster, `(handle, id)`, kept across resolves.
|
||||
plan: Vec<(FrameHandle, u32)>,
|
||||
/// The cached roster of live, anchored regions.
|
||||
regions: Vec<RegionRow>,
|
||||
/// Per ScrollFrame id, the live child it held when last derived: the one frame whose anchors
|
||||
/// its override replaces.
|
||||
scroll_child: HashMap<u32, FrameHandle>,
|
||||
/// Per child, the ScrollFrames holding it, `(handle, id)`: [`Self::scroll_child`] inverted, so
|
||||
/// a re-derived frame finds its override without a scan.
|
||||
claims: HashMap<FrameHandle, Vec<(FrameHandle, u32)>>,
|
||||
/// An incremental pass's `(id, hash)` writes to [`Self::last`], applied only on convergence.
|
||||
staged: Vec<(u32, u64)>,
|
||||
/// A re-derive's target list, reused.
|
||||
@@ -263,8 +240,6 @@ impl LayoutScope {
|
||||
self.stack.clear();
|
||||
self.plan.clear();
|
||||
self.regions.clear();
|
||||
self.scroll_child.clear();
|
||||
self.claims.clear();
|
||||
self.staged.clear();
|
||||
}
|
||||
|
||||
@@ -384,33 +359,18 @@ impl LayoutScope {
|
||||
}
|
||||
}
|
||||
|
||||
/// Record that ScrollFrame `sid` (`sh`) now holds `child`; the child it held before, if it
|
||||
/// changed, so the caller re-derives both.
|
||||
fn hold(
|
||||
&mut self,
|
||||
sh: FrameHandle,
|
||||
sid: u32,
|
||||
child: Option<FrameHandle>,
|
||||
) -> Option<FrameHandle> {
|
||||
let before = match child {
|
||||
Some(c) => self.scroll_child.insert(sid, c),
|
||||
None => self.scroll_child.remove(&sid),
|
||||
};
|
||||
if before == child {
|
||||
return None;
|
||||
}
|
||||
if let Some(old) = before {
|
||||
if let Some(v) = self.claims.get_mut(&old) {
|
||||
v.retain(|&(_, s)| s != sid);
|
||||
if v.is_empty() {
|
||||
self.claims.remove(&old);
|
||||
}
|
||||
/// The nodes this graph records reading `id`'s rect. It describes the model while the ledger
|
||||
/// is armed (`Model::layout_touched` is `Some`), but for the nodes named since.
|
||||
pub(crate) fn readers_of(&self, id: u32) -> impl Iterator<Item = u32> + '_ {
|
||||
let mut e = self.dep_head.get(id as usize).copied().unwrap_or(u32::MAX);
|
||||
std::iter::from_fn(move || {
|
||||
if e == u32::MAX {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
if let Some(c) = child {
|
||||
self.claims.entry(c).or_default().push((sh, sid));
|
||||
}
|
||||
before
|
||||
let from = self.dep_to[e as usize];
|
||||
e = self.dep_next[e as usize];
|
||||
Some(from)
|
||||
})
|
||||
}
|
||||
|
||||
/// Whether `id` is a node whose converged hash is `hash`: a write that leaves it there moved
|
||||
@@ -507,11 +467,6 @@ impl LayoutScope {
|
||||
v.sort_unstable();
|
||||
v
|
||||
};
|
||||
let over_eq = |a: Option<Anchor>, b: Option<Anchor>| match (a, b) {
|
||||
(None, None) => true,
|
||||
(Some(a), Some(b)) => a.bits_eq(&b),
|
||||
_ => false,
|
||||
};
|
||||
let n = self
|
||||
.node_of
|
||||
.len()
|
||||
@@ -527,10 +482,7 @@ impl LayoutScope {
|
||||
let r = s.regions[(n & !REGION_TAG) as usize];
|
||||
format!("region {:?} owner {:?}", r.rh, r.owner)
|
||||
}
|
||||
n => format!(
|
||||
"frame {:?} override {:?}",
|
||||
s.plan[n as usize].0, s.plan[n as usize].2
|
||||
),
|
||||
n => format!("frame {:?}", s.plan[n as usize].0),
|
||||
}
|
||||
};
|
||||
let same_node = match (a, b) {
|
||||
@@ -541,10 +493,7 @@ impl LayoutScope {
|
||||
self.regions[(a & !REGION_TAG) as usize]
|
||||
== fresh.regions[(b & !REGION_TAG) as usize]
|
||||
}
|
||||
(a, b) => {
|
||||
let (x, y) = (self.plan[a as usize], fresh.plan[b as usize]);
|
||||
x.0 == y.0 && x.1 == y.1 && over_eq(x.2, y.2)
|
||||
}
|
||||
(a, b) => self.plan[a as usize] == fresh.plan[b as usize],
|
||||
};
|
||||
if !same_node {
|
||||
return Some((
|
||||
@@ -583,30 +532,7 @@ impl LayoutScope {
|
||||
));
|
||||
}
|
||||
}
|
||||
let held = |s: &LayoutScope| {
|
||||
let mut v: Vec<(u32, FrameHandle)> =
|
||||
s.scroll_child.iter().map(|(&id, &c)| (id, c)).collect();
|
||||
v.sort_unstable_by_key(|e| e.0);
|
||||
v
|
||||
};
|
||||
let (sa, sb) = (held(self), held(fresh));
|
||||
if sa != sb {
|
||||
return Some((0, format!("ScrollFrames hold {sa:?} cached, {sb:?} fresh")));
|
||||
}
|
||||
// The inverse must agree with its source, entry for entry.
|
||||
let claimed: usize = self.claims.values().map(Vec::len).sum();
|
||||
let inverse_ok = claimed == sa.len()
|
||||
&& sa.iter().all(|&(id, c)| {
|
||||
self.claims
|
||||
.get(&c)
|
||||
.is_some_and(|v| v.iter().any(|&(_, s)| s == id))
|
||||
});
|
||||
(!inverse_ok).then(|| {
|
||||
(
|
||||
0,
|
||||
format!("ScrollFrame claims {:?} disagree with {sa:?}", self.claims),
|
||||
)
|
||||
})
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
@@ -646,11 +572,12 @@ fn node_inputs(model: &Model, id: u32) -> String {
|
||||
if let Some(h) = model.id_to_frame.get(&id) {
|
||||
let f = model.arena.frame(*h);
|
||||
return format!(
|
||||
"frame {:?} {:?}, effective scale {:?}, clamped {:?}",
|
||||
"frame {:?} {:?}, effective scale {:?}, clamped {:?}, unsettled {}",
|
||||
f.and_then(|f| f.name.clone()),
|
||||
model.layout_inputs.get(h),
|
||||
f.map(|f| f.effective_scale),
|
||||
f.map(|f| f.clamped_to_screen),
|
||||
model.layout_unsettled.contains(&id),
|
||||
);
|
||||
}
|
||||
let Some(rh) = model.id_to_region.get(&id) else {
|
||||
@@ -661,13 +588,14 @@ fn node_inputs(model: &Model, id: u32) -> String {
|
||||
match model.region_data.get(rh) {
|
||||
Some(d) => format!(
|
||||
"region owner {owner:?} (id {owner_id:?}), anchors {:?}, size {:?}, measured {:?} \
|
||||
(text {} bytes), texture {:?}, fill {}",
|
||||
(text {} bytes), texture {:?}, fill {}, unsettled {}",
|
||||
d.anchors,
|
||||
d.size,
|
||||
d.measured.map(|m| (m.w, m.h)),
|
||||
d.text.as_deref().map_or(0, str::len),
|
||||
d.texture,
|
||||
d.fill.is_some()
|
||||
d.fill.is_some(),
|
||||
model.layout_unsettled.contains(&id),
|
||||
),
|
||||
None => format!("region owner {owner:?} with no data"),
|
||||
}
|
||||
@@ -681,7 +609,7 @@ fn layout_prof_enabled() -> bool {
|
||||
|
||||
/// `WOW_LAYOUT_VERIFY=1`, on in this crate's tests: every shortcut of the resolve (a gate skip, a
|
||||
/// tier-1-quiet frame, an incremental pass, the measure ledger) is checked against the full answer.
|
||||
fn layout_verify_enabled() -> bool {
|
||||
pub(super) fn layout_verify_enabled() -> bool {
|
||||
static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
|
||||
*ON.get_or_init(|| cfg!(test) || std::env::var("WOW_LAYOUT_VERIFY").as_deref() == Ok("1"))
|
||||
}
|
||||
@@ -697,8 +625,6 @@ struct PreambleProf {
|
||||
watched: u128,
|
||||
/// The ids rebuild and the scale and clamp sync into `layout_inputs`.
|
||||
ids: u128,
|
||||
/// The ScrollFrame override map.
|
||||
scroll: u128,
|
||||
/// `region_resolved`'s liveness retain.
|
||||
retain: u128,
|
||||
/// `LayoutScope::begin_full`.
|
||||
@@ -741,7 +667,6 @@ impl PreambleProf {
|
||||
let total = self.tooltip
|
||||
+ self.watched
|
||||
+ self.ids
|
||||
+ self.scroll
|
||||
+ self.retain
|
||||
+ self.begin
|
||||
+ self.fp_frames
|
||||
@@ -749,14 +674,13 @@ impl PreambleProf {
|
||||
+ self.seed;
|
||||
eprintln!(
|
||||
"[layout-pre] skips={} incr={} frames={frames} anchored={regions} total_us={total} \
|
||||
tooltip={} watched={} ids={} scroll={} retain={} begin={} \
|
||||
tooltip={} watched={} ids={} retain={} begin={} \
|
||||
fp_frames={} fp_regions={} seed={}",
|
||||
u8::from(skips),
|
||||
u8::from(self.incremental),
|
||||
self.tooltip,
|
||||
self.watched,
|
||||
self.ids,
|
||||
self.scroll,
|
||||
self.retain,
|
||||
self.begin,
|
||||
self.fp_frames,
|
||||
@@ -958,12 +882,17 @@ impl UiScript {
|
||||
texture_size_probe,
|
||||
layout_force,
|
||||
layout_screen,
|
||||
layout_unsettled,
|
||||
..
|
||||
} = model;
|
||||
let mut full_scope = false;
|
||||
// The nodes this resolve stores without the keep test; a write that cleared the resolved
|
||||
// bit of no node (an unanchored region's measure) leaves nothing to store.
|
||||
let mut forced = std::mem::take(layout_force);
|
||||
// Each is reached now, so it holds what its inputs give again.
|
||||
for id in forced.iter().chain(&pending) {
|
||||
layout_unsettled.remove(id);
|
||||
}
|
||||
// An invalidated region with no anchors is no node, and its resolve can only fail
|
||||
// (`0x768d55`): its valid bit clears, a Texture keeps its last quad. So does one whose
|
||||
// owner's scale moved, which the cascade invalidates (`0x76ad06`).
|
||||
@@ -1169,7 +1098,7 @@ impl UiScript {
|
||||
let entry_frames: Vec<Option<Rect>> = solve_frames
|
||||
.iter()
|
||||
.map(|&i| {
|
||||
let (h, id, _) = scope.plan[i as usize];
|
||||
let (h, id) = scope.plan[i as usize];
|
||||
(!is_forced(id))
|
||||
.then(|| resolved.get(&h).copied())
|
||||
.flatten()
|
||||
@@ -1216,25 +1145,22 @@ impl UiScript {
|
||||
solver.set_entry(scope.regions[i as usize].id, cached, invalid);
|
||||
}
|
||||
for &i in &seed_frames {
|
||||
let (h, id, _) = scope.plan[i as usize];
|
||||
let (h, id) = scope.plan[i as usize];
|
||||
if let Some(r) = resolved.get(&h) {
|
||||
solver.set_external(id, *r);
|
||||
}
|
||||
}
|
||||
for (&i, &cached) in solve_frames.iter().zip(&entry_frames) {
|
||||
let (h, id, over) = scope.plan[i as usize];
|
||||
let (h, id) = scope.plan[i as usize];
|
||||
let Some(input) = layout_inputs.get(&h) else {
|
||||
continue;
|
||||
};
|
||||
match over {
|
||||
Some(a) => solver.set_frame_anchored(id, input, a),
|
||||
None => solver.set_frame(id, input),
|
||||
}
|
||||
solver.set_frame(id, input);
|
||||
solver.set_entry(id, cached, is_invalid(id));
|
||||
}
|
||||
solver.solve();
|
||||
for &i in &solve_frames {
|
||||
let (h, id, _) = scope.plan[i as usize];
|
||||
let (h, id) = scope.plan[i as usize];
|
||||
match solver.rect(id) {
|
||||
Some(r) => {
|
||||
if resolved.get(&h) != Some(&r) {
|
||||
@@ -1272,6 +1198,25 @@ impl UiScript {
|
||||
if prof {
|
||||
n_regions_swept += 1;
|
||||
}
|
||||
// Not reached since its entry: it keeps its state, rect or none (see
|
||||
// `LayoutSolver::set_entry`).
|
||||
if !solver.reached(region_id, data.anchors.iter().map(|a| a.relative_to)) {
|
||||
match solver.entry(region_id) {
|
||||
Some(kept) => {
|
||||
solver.set_external(region_id, kept);
|
||||
if region_resolved.get(&rh) != Some(&kept) {
|
||||
region_resolved.insert(rh, kept);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
None => {
|
||||
if region_resolved.remove(&rh).is_some() {
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
// The owner supplies the region's scale and nothing else: no fallback edges.
|
||||
let scale = arena.frame(owner).map(|f| f.effective_scale).unwrap_or(1.0);
|
||||
// An axis authored 0, width or height, takes the host-measured extent: the
|
||||
@@ -1297,6 +1242,8 @@ impl UiScript {
|
||||
// A texture's unset axis takes its texel extent (`content_span`): the reference's
|
||||
// size getter is virtual for both region classes. It must apply before the
|
||||
// resolve, as `combine_edge` reads a zero span as no size and leaves an edge unset.
|
||||
// The getter runs in the resolve (`0x767579`, `0x7675e9`), loading the art first if
|
||||
// it must (`0x44b3b0`), so only a resolve that reaches the node reads it.
|
||||
if !is_fontstring && (width == 0.0 || height == 0.0) {
|
||||
if let Some((cw, ch)) = content_span(data, texture_size_probe.as_ref()) {
|
||||
if width == 0.0 {
|
||||
@@ -1331,26 +1278,6 @@ impl UiScript {
|
||||
let vertical = axis(edges[0], edges[2]);
|
||||
let horizontal = axis(edges[1], edges[3]);
|
||||
let resolved_edges = vertical.zip(horizontal);
|
||||
// Not reached since its entry: it keeps its state, rect or none (see
|
||||
// `LayoutSolver::set_entry`).
|
||||
let reads = data.anchors.iter().map(|a| a.relative_to);
|
||||
if !solver.takes_resolve(region_id, resolved_edges.is_some(), reads) {
|
||||
match solver.entry(region_id) {
|
||||
Some(kept) => {
|
||||
solver.set_external(region_id, kept);
|
||||
if region_resolved.get(&rh) != Some(&kept) {
|
||||
region_resolved.insert(rh, kept);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
None => {
|
||||
if region_resolved.remove(&rh).is_some() {
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
let Some(((bottom, top), (left, right))) = resolved_edges else {
|
||||
// The failed resolve (`0x768d55`) clears the valid bit, so the getters and a
|
||||
// FontString's draw lose the rect; a Texture keeps the quad its last good
|
||||
@@ -1508,40 +1435,6 @@ impl UiScript {
|
||||
}
|
||||
pre.ids = pre.lap();
|
||||
|
||||
// A live ScrollFrame's live child is anchored, for this solve only, TOPLEFT to its TOPLEFT
|
||||
// at the raw, unnegated `(horizontal, vertical)` offsets, as the reference's re-anchor
|
||||
// `0x787100` passes them to `SetPoint`: y up, so a positive vertical lifts the child. Its
|
||||
// authored anchors and size are untouched, so `SetScrollChild(nil)` needs no restore.
|
||||
let mut scroll_child_anchor: HashMap<FrameHandle, Anchor> = HashMap::new();
|
||||
let mut scroll_holds: Vec<(FrameHandle, u32, FrameHandle)> = Vec::new();
|
||||
for &(h, id) in &ids {
|
||||
let Some(frame) = arena.frame(h) else {
|
||||
continue;
|
||||
};
|
||||
if frame.kind != FrameKind::ScrollFrame {
|
||||
continue;
|
||||
}
|
||||
let KindState::Scroll(state) = &frame.kind_state else {
|
||||
continue;
|
||||
};
|
||||
let Some(child) = state.child else { continue };
|
||||
if arena.frame(child).is_none() {
|
||||
continue; // a stale/destroyed child contributes no override
|
||||
}
|
||||
scroll_holds.push((h, id, child));
|
||||
scroll_child_anchor.insert(
|
||||
child,
|
||||
Anchor::new(
|
||||
Point::TopLeft,
|
||||
id,
|
||||
Point::TopLeft,
|
||||
state.horizontal,
|
||||
state.vertical,
|
||||
),
|
||||
);
|
||||
}
|
||||
pre.scroll = pre.lap();
|
||||
|
||||
// The reference resolves one graph in which frames and regions anchor each other; ours
|
||||
// alternates a frame solve and a region sweep until a round changes no rect, compared
|
||||
// exactly. The round cap is the node count, so only a cycle exhausts it, warned like the
|
||||
@@ -1554,17 +1447,15 @@ impl UiScript {
|
||||
|
||||
// ── The change gate ───────────────────────────────────────────────────────────────────
|
||||
// Fingerprint what the rounds read, to skip them when nothing moved since the last
|
||||
// converged resolve: the screen; each live frame's id, synced `LayoutInput` and
|
||||
// ScrollFrame override; each anchored region's node hash (anchors, explicit size, measured
|
||||
// extent, the art of an unsized axis, owner) and liveness. Seeded rects are outputs, a
|
||||
// converged resolve: the screen; each live frame's id and synced `LayoutInput`; each
|
||||
// anchored region's node hash (anchors, explicit size, measured
|
||||
// extent, owner) and liveness. A Texture's art is no input: the sweep reads it afresh only
|
||||
// for a node a write named or the resolve reaches. Seeded rects are outputs, a
|
||||
// function of these and of the rects the keep test held them against, which a re-run with
|
||||
// the same inputs keeps again, so they are not hashed.
|
||||
// The same walk hashes each node alone into `LayoutScope::now` and records its edges.
|
||||
// Sized past `next_id` for the ids the region walk below may mint.
|
||||
scope.begin_full(*next_id as usize + region_data.len() + 1);
|
||||
for (h, id, child) in scroll_holds {
|
||||
scope.hold(h, id, Some(child));
|
||||
}
|
||||
pre.begin = pre.lap();
|
||||
let mut fp = InputFingerprint::default();
|
||||
fp.rect(*screen);
|
||||
@@ -1572,29 +1463,18 @@ impl UiScript {
|
||||
let Some(input) = layout_inputs.get(&h) else {
|
||||
continue;
|
||||
};
|
||||
let over = scroll_child_anchor.get(&h).copied();
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
let i = scope.plan.len() as u32;
|
||||
fp.feed(u64::from(id));
|
||||
fp.input(input);
|
||||
match over {
|
||||
Some(a) => fp.anchors(std::slice::from_ref(&a)),
|
||||
None => fp.feed(u64::MAX),
|
||||
}
|
||||
if let Some(slot) = scope.now.get_mut(id as usize) {
|
||||
*slot = frame_node_hash(input, over);
|
||||
*slot = frame_node_hash(input);
|
||||
scope.node_of[id as usize] = i;
|
||||
}
|
||||
scope.plan.push((h, id, over));
|
||||
// A frame reads its anchor targets' rects, or only its ScrollFrame override's target,
|
||||
// as the round loop's `set_frame_anchored` does.
|
||||
match over {
|
||||
Some(a) => scope.link(id, a.relative_to),
|
||||
None => {
|
||||
for a in &input.anchors {
|
||||
scope.link(id, a.relative_to);
|
||||
}
|
||||
}
|
||||
scope.plan.push((h, id));
|
||||
// A frame reads its anchor targets' rects.
|
||||
for a in &input.anchors {
|
||||
scope.link(id, a.relative_to);
|
||||
}
|
||||
}
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
@@ -1614,8 +1494,8 @@ impl UiScript {
|
||||
fed_regions += 1;
|
||||
let live = arena.region(rh);
|
||||
let owner_id = live.and_then(|r| frame_to_id.get(&r.owner).copied());
|
||||
// The node hash covers every input the sweep reads (anchors, size, measure, the art
|
||||
// of an unsized axis, the owner), so the two tiers read one set.
|
||||
// The node hash covers every input the sweep reads (anchors, size, measure, the
|
||||
// owner), so the two tiers read one set.
|
||||
let node = region_node_hash(data, owner_id);
|
||||
fp.feed(rh.fingerprint_bits());
|
||||
fp.feed(node);
|
||||
@@ -1667,8 +1547,7 @@ impl UiScript {
|
||||
/// unlinked (a region freed or cleared), or keeps its slot with its edges re-pointed and its
|
||||
/// inputs re-synced, exactly as [`Self::derive_layout_graph`] would build it. Each node whose
|
||||
/// hash or edges moved is seeded, and the closure taken. `false` gives up for the derivation:
|
||||
/// a frame leaving the roster, or a frame two ScrollFrames claim, whose override the
|
||||
/// derivation picks by map order.
|
||||
/// a frame leaving the roster.
|
||||
fn rederive_named(model: &mut Model, mut named: Vec<u32>, pre: &mut PreambleProf) -> bool {
|
||||
let Model {
|
||||
arena,
|
||||
@@ -1686,14 +1565,7 @@ impl UiScript {
|
||||
..
|
||||
} = model;
|
||||
scope.begin_incremental(*next_id as usize);
|
||||
// ScrollFrames first, so a child re-derived after them reads the claims they recorded.
|
||||
let is_scroller = |id: u32| {
|
||||
id_to_frame
|
||||
.get(&id)
|
||||
.and_then(|&h| arena.frame(h))
|
||||
.is_some_and(|f| f.kind == FrameKind::ScrollFrame)
|
||||
};
|
||||
named.sort_unstable_by_key(|&id| (!is_scroller(id), id));
|
||||
named.sort_unstable();
|
||||
named.dedup();
|
||||
let mut targets = std::mem::take(&mut scope.targets);
|
||||
let mut at = 0;
|
||||
@@ -1718,39 +1590,6 @@ impl UiScript {
|
||||
scope.targets = targets;
|
||||
return false;
|
||||
}
|
||||
// A ScrollFrame records the live child it holds, and that child re-derives after
|
||||
// it, as the override is the child's; so does a child it let go.
|
||||
if frame.kind == FrameKind::ScrollFrame {
|
||||
let child = match &frame.kind_state {
|
||||
KindState::Scroll(state) => {
|
||||
state.child.filter(|&c| arena.frame(c).is_some())
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
let let_go = scope.hold(h, id, child);
|
||||
for c in child.into_iter().chain(let_go) {
|
||||
named.extend(frame_to_id.get(&c));
|
||||
}
|
||||
}
|
||||
// The override of the one ScrollFrame holding this frame; a child two hold gives
|
||||
// up, as the derivation picks between them by map order.
|
||||
let over = match scope.claims.get(&h).map(Vec::as_slice) {
|
||||
None | Some([]) => None,
|
||||
Some(&[(sh, sid)]) => match arena.frame(sh).map(|f| &f.kind_state) {
|
||||
Some(KindState::Scroll(state)) => Some(Anchor::new(
|
||||
Point::TopLeft,
|
||||
sid,
|
||||
Point::TopLeft,
|
||||
state.horizontal,
|
||||
state.vertical,
|
||||
)),
|
||||
_ => None,
|
||||
},
|
||||
Some(_) => {
|
||||
scope.targets = targets;
|
||||
return false;
|
||||
}
|
||||
};
|
||||
// The derivation's sync of the arena's scale and clamp into the layout input.
|
||||
let (scale, clamp) = (frame.effective_scale, frame.clamped_to_screen);
|
||||
let input = layout_inputs.entry(h).or_default();
|
||||
@@ -1764,21 +1603,17 @@ impl UiScript {
|
||||
input.extent_y = screen.top;
|
||||
}
|
||||
targets.clear();
|
||||
match over {
|
||||
Some(a) => targets.push(a.relative_to),
|
||||
None => targets.extend(input.anchors.iter().map(|a| a.relative_to)),
|
||||
}
|
||||
let hash = frame_node_hash(input, over);
|
||||
targets.extend(input.anchors.iter().map(|a| a.relative_to));
|
||||
let hash = frame_node_hash(input);
|
||||
let relinked = if n == u32::MAX {
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
{
|
||||
scope.node_of[id as usize] = scope.plan.len() as u32;
|
||||
}
|
||||
scope.plan.push((h, id, over));
|
||||
scope.plan.push((h, id));
|
||||
scope.relink(id, &targets);
|
||||
true
|
||||
} else {
|
||||
scope.plan[n as usize].2 = over;
|
||||
scope.relink(id, &targets)
|
||||
};
|
||||
scope.note(id, hash, relinked);
|
||||
|
||||
@@ -140,9 +140,8 @@ impl super::UiScript {
|
||||
f.user_placed = true;
|
||||
}
|
||||
}
|
||||
if changed {
|
||||
model.touch_layout_frame(h);
|
||||
}
|
||||
// A row applied invalidates the frame (`0x490710`).
|
||||
model.invalidate_frame(h, changed);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -134,7 +134,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
data.texture = None;
|
||||
data.fill = None;
|
||||
data.circular = true;
|
||||
model.touch_layout_art(rh);
|
||||
model.note_art(rh);
|
||||
Ok(Value::Integer(1))
|
||||
})?,
|
||||
)?;
|
||||
|
||||
@@ -150,6 +150,12 @@ pub(crate) struct Model {
|
||||
/// node nothing invalidated keeps a state a recompute could change
|
||||
/// ([`Model::invalidate_frame`]).
|
||||
pub(crate) layout_pending: Vec<u32>,
|
||||
/// Layout ids whose inputs a write moved without invalidating them since they were last
|
||||
/// invalidated: a dying target's unhook (`0x768e20` → `0x767e70` with a zero flag), new art
|
||||
/// (`SetTexture` `0x770200`), a re-link into another frame (`0x77fd10`). Each holds a state a
|
||||
/// re-resolve need not give back, so a write the reference invalidates for is not skipped as
|
||||
/// a no-op on it ([`Model::invalidate_frame`]).
|
||||
pub(crate) layout_unsettled: HashSet<u32>,
|
||||
/// The screen the last converged resolve laid out against: a frame anchored to it is reached
|
||||
/// when it moves, as by any target's new rect.
|
||||
pub(crate) layout_screen: Option<Rect>,
|
||||
@@ -1173,6 +1179,7 @@ impl Model {
|
||||
text_flips: Vec::new(),
|
||||
layout_force: Vec::new(),
|
||||
layout_pending: Vec::new(),
|
||||
layout_unsettled: HashSet::new(),
|
||||
layout_screen: None,
|
||||
errors: Vec::new(),
|
||||
pending_error_dispatch: Vec::new(),
|
||||
@@ -1613,7 +1620,9 @@ impl Model {
|
||||
/// A write to region `rh`'s layout inputs or liveness: its anchors, size, measured extent,
|
||||
/// art or owner. Its node re-derives at the next resolve, joining or leaving the graph as it
|
||||
/// gained or lost its anchors. An id is minted only for an anchored region, the only kind that
|
||||
/// can be a node; an anchorless one with no id has nothing in the graph to move.
|
||||
/// can be a node; an anchorless one with no id has nothing in the graph to move. Naming
|
||||
/// invalidates nothing: a write the reference invalidates for goes through
|
||||
/// [`Self::invalidate_region`].
|
||||
pub(crate) fn touch_layout_region(&mut self, rh: RegionHandle) {
|
||||
match self.region_to_id.get(&rh) {
|
||||
Some(&id) => self.name_layout_node(id),
|
||||
@@ -1628,20 +1637,6 @@ impl Model {
|
||||
}
|
||||
}
|
||||
|
||||
/// A write to region `rh`'s art, its texture or solid fill: a layout input only where an
|
||||
/// anchored region takes an axis authored 0 from it ([`super::layout::content_span`]), and
|
||||
/// named only when it moved the node's hash, so a repaint of the same art leaves the gate shut.
|
||||
pub(crate) fn touch_layout_art(&mut self, rh: RegionHandle) {
|
||||
if self
|
||||
.region_data
|
||||
.get(&rh)
|
||||
.is_some_and(|d| d.size.is_some_and(|(w, h)| w != 0.0 && h != 0.0))
|
||||
{
|
||||
return;
|
||||
}
|
||||
self.touch_layout_content(rh);
|
||||
}
|
||||
|
||||
/// A write to region `rh`'s text, which was non-empty iff `was_shown`: the sweep reads a
|
||||
/// FontString's measure only while its text is non-empty
|
||||
/// ([`super::layout::effective_measure`]), so a flip between empty and not is noted, and the
|
||||
@@ -1658,7 +1653,9 @@ impl Model {
|
||||
}
|
||||
}
|
||||
|
||||
/// Names anchored region `rh` unless its node still holds its converged hash.
|
||||
/// Text that emptied or filled on anchored region `rh`, which moves the measure its resolve
|
||||
/// reads: a new string clears the valid bit and invalidates (`0x771ea4`), unless the node
|
||||
/// still holds its converged hash, as when a clear and rebuild restored the same text.
|
||||
pub(crate) fn touch_layout_content(&mut self, rh: RegionHandle) {
|
||||
let Some(d) = self.region_data.get(&rh) else {
|
||||
return;
|
||||
@@ -1678,10 +1675,10 @@ impl Model {
|
||||
return;
|
||||
}
|
||||
}
|
||||
self.touch_layout_region(rh);
|
||||
self.clear_region_valid(rh);
|
||||
}
|
||||
|
||||
/// The same for frame `h`: its anchors, size, scale, clamp or scroll override.
|
||||
/// The same for frame `h`: its anchors, size, scale or clamp.
|
||||
pub(crate) fn touch_layout_frame(&mut self, h: FrameHandle) {
|
||||
match self.frame_to_id.get(&h) {
|
||||
Some(&id) => self.name_layout_node(id),
|
||||
@@ -1695,7 +1692,8 @@ impl Model {
|
||||
/// A write the reference invalidates frame `h` for (`0x7680e0`): `SetPoint`, `SetWidth`,
|
||||
/// `SetClampedToScreen` and the like, which pass `changed` for whether it moved an input. One
|
||||
/// that moved nothing is skipped, as a re-resolve gives back the rect the frame holds, unless
|
||||
/// the frame holds one a re-resolve would not give it ([`Self::frame_holds_stale`]).
|
||||
/// the frame holds one a re-resolve would not give it ([`Self::frame_holds_stale`]): only an
|
||||
/// invalidated node, or one whose target stored a new rect, takes a new rect.
|
||||
pub(crate) fn invalidate_frame(&mut self, h: FrameHandle, changed: bool) {
|
||||
if !changed && !self.frame_holds_stale(h) {
|
||||
return;
|
||||
@@ -1729,9 +1727,50 @@ impl Model {
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether frame `h` holds a rect its inputs would not resolve to now: it has no anchors, or
|
||||
/// reads a target with no rect, so the rect stands only because nothing invalidated it since.
|
||||
/// A write that moved region `rh`'s layout inputs and invalidated nothing (a re-link into
|
||||
/// another frame, `0x77fd10`; a dying target's unhook, `0x767e70`): its node re-derives, and
|
||||
/// it keeps its state until the resolve reaches it.
|
||||
pub(crate) fn unsettle_region(&mut self, rh: RegionHandle) {
|
||||
self.touch_layout_region(rh);
|
||||
if let Some(&id) = self.region_to_id.get(&rh) {
|
||||
self.layout_unsettled.insert(id);
|
||||
}
|
||||
}
|
||||
|
||||
/// [`Self::unsettle_region`] for frame `h`.
|
||||
pub(crate) fn unsettle_frame(&mut self, h: FrameHandle) {
|
||||
self.touch_layout_frame(h);
|
||||
if let Some(&id) = self.frame_to_id.get(&h) {
|
||||
self.layout_unsettled.insert(id);
|
||||
}
|
||||
}
|
||||
|
||||
/// New art on region `rh` (`SetTexture` `0x770200`, the colour form `0x770360`): it marks
|
||||
/// only the draw dirty (`0x77fdf0`), so the layout is not touched. An anchored texture with an
|
||||
/// axis sized 0 reads its art in its next reaching resolve, so until then it holds a state
|
||||
/// that resolve need not give back.
|
||||
pub(crate) fn note_art(&mut self, rh: RegionHandle) {
|
||||
let Some(&id) = self.region_to_id.get(&rh) else {
|
||||
return;
|
||||
};
|
||||
if self.region_data.get(&rh).is_some_and(|d| {
|
||||
!d.anchors.is_empty() && d.size.is_none_or(|(w, h)| w == 0.0 || h == 0.0)
|
||||
}) {
|
||||
self.layout_unsettled.insert(id);
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether frame `h` holds a state its inputs would not resolve to now: a write moved them
|
||||
/// without invalidating it ([`Self::layout_unsettled`]), or it holds a rect with no anchors
|
||||
/// or reading a target with no rect, which stands only because nothing invalidated it since.
|
||||
pub(crate) fn frame_holds_stale(&self, h: FrameHandle) -> bool {
|
||||
if self
|
||||
.frame_to_id
|
||||
.get(&h)
|
||||
.is_some_and(|id| self.layout_unsettled.contains(id))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if !self.resolved.contains_key(&h) {
|
||||
return false;
|
||||
}
|
||||
@@ -1742,6 +1781,13 @@ impl Model {
|
||||
|
||||
/// [`Self::frame_holds_stale`] for region `rh`.
|
||||
pub(crate) fn region_holds_stale(&self, rh: RegionHandle) -> bool {
|
||||
if self
|
||||
.region_to_id
|
||||
.get(&rh)
|
||||
.is_some_and(|id| self.layout_unsettled.contains(id))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if !self.region_resolved.contains_key(&rh) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -86,9 +86,8 @@ impl Model {
|
||||
input.width.to_bits() != w.to_bits() || input.height.to_bits() != ht.to_bits();
|
||||
input.width = w;
|
||||
input.height = ht;
|
||||
if changed {
|
||||
self.touch_layout_frame(h);
|
||||
}
|
||||
// The model-ready size write invalidates the frame (`0x76ce70`).
|
||||
self.invalidate_frame(h, changed);
|
||||
if let Some(KindState::Model(m)) = self.arena.frame_mut(h).map(|f| &mut f.kind_state) {
|
||||
m.implicit_size = true;
|
||||
}
|
||||
|
||||
@@ -596,11 +596,12 @@ impl Plate {
|
||||
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.
|
||||
model.touch_layout_frame(frame);
|
||||
model.touch_layout_frame(bar);
|
||||
// Once per geometry change: each node it wrote is invalidated, as the plate setup's
|
||||
// `SetPoint`/`SetAllPoints` with the invalidate flag do (`0x7cb456`..`0x7cb806`).
|
||||
model.invalidate_frame(frame, true);
|
||||
model.invalidate_frame(bar, true);
|
||||
for rh in [border, glow, name, level, skull, raid] {
|
||||
model.touch_layout_region(rh);
|
||||
model.invalidate_region(rh, true);
|
||||
}
|
||||
model.nameplates.plates[i].laid_out = Some(g);
|
||||
}
|
||||
@@ -633,7 +634,8 @@ impl Plate {
|
||||
state.top_centre.0,
|
||||
state.top_centre.1,
|
||||
)]);
|
||||
model.touch_layout_frame(frame);
|
||||
// `SetPoint` with the invalidate flag (`0x50a044`).
|
||||
model.invalidate_frame(frame, true);
|
||||
}
|
||||
|
||||
if last.is_none() {
|
||||
@@ -670,7 +672,8 @@ impl Plate {
|
||||
if last.as_ref().is_none_or(|l| l.name != state.name) {
|
||||
model.region_data.entry(name).or_default().text = Some(state.name.clone());
|
||||
model.touch_measure(name);
|
||||
model.touch_layout_region(name);
|
||||
// A new string clears the valid bit and invalidates (`0x771ea4`).
|
||||
model.clear_region_valid(name);
|
||||
}
|
||||
if last.as_ref().is_none_or(|l| l.hovered != state.hovered) {
|
||||
let c = if state.hovered {
|
||||
@@ -700,7 +703,7 @@ impl Plate {
|
||||
data.hidden = false;
|
||||
model.region_data.entry(skull).or_default().hidden = true;
|
||||
model.touch_measure(level);
|
||||
model.touch_layout_region(level);
|
||||
model.clear_region_valid(level);
|
||||
}
|
||||
// No level yet and no skull: the seat stays empty.
|
||||
(None, false) => {
|
||||
|
||||
@@ -410,9 +410,8 @@ fn pin_top_left(
|
||||
input.anchors = pin;
|
||||
input.width = w;
|
||||
input.height = ht;
|
||||
if changed {
|
||||
model.touch_layout_frame(h);
|
||||
}
|
||||
// The size setters and a closing `0x7680e0(1)` invalidate (`0x768533`).
|
||||
model.invalidate_frame(h, changed);
|
||||
// The size it now carries is explicit, as a `SetWidth`/`SetHeight` would leave it.
|
||||
model.note_authored_size(h);
|
||||
Some(changed)
|
||||
@@ -494,7 +493,8 @@ fn drag_step(model: &mut Model, mv: FrameMove, dx: f32, dy: f32) {
|
||||
})
|
||||
.is_some();
|
||||
if moved {
|
||||
model.touch_layout_frame(h);
|
||||
// The offsets move and the frame is invalidated (`0x768c31`).
|
||||
model.invalidate_frame(h, true);
|
||||
mark_user_placed_change(model, h);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -78,8 +78,9 @@ pub(super) fn link_region(
|
||||
shown: bool,
|
||||
) {
|
||||
if model.arena.set_region_owner(rh, Some(owner)) {
|
||||
// The owner supplies the region's scale to layout, so a re-link is a layout change.
|
||||
model.touch_layout_region(rh);
|
||||
// The owner supplies the region's scale to layout; the re-link invalidates nothing, so
|
||||
// the region keeps its rect until the resolve reaches it.
|
||||
model.unsettle_region(rh);
|
||||
}
|
||||
if let Some(r) = model.arena.region_mut(rh) {
|
||||
r.draw_layer = layer;
|
||||
@@ -207,7 +208,7 @@ pub(super) fn set_texture_path(model: &mut Model, rh: RegionHandle, path: &str)
|
||||
let data = model.region_data.entry(rh).or_default();
|
||||
data.texture = (!path.is_empty()).then(|| path.to_string());
|
||||
data.fill = None;
|
||||
model.touch_layout_art(rh);
|
||||
model.note_art(rh);
|
||||
}
|
||||
|
||||
/// The loader's install of the texture a `<BarTexture>`, `<ThumbTexture>`, Button state-texture or
|
||||
@@ -274,7 +275,7 @@ pub(super) fn clear_texture(model: &mut Model, rh: RegionHandle) {
|
||||
data.texture = None;
|
||||
data.fill = None;
|
||||
data.portrait_unit = None;
|
||||
model.touch_layout_art(rh);
|
||||
model.note_art(rh);
|
||||
}
|
||||
|
||||
/// Apply the font parts an XML element supplies (`font=`, `<FontHeight>`, `outline=`), any of
|
||||
@@ -379,7 +380,7 @@ pub(super) fn install_in_game(lua: &Lua) -> mlua::Result<()> {
|
||||
data.texture = None;
|
||||
data.fill = None;
|
||||
data.circular = true;
|
||||
model.touch_layout_art(rh);
|
||||
model.note_art(rh);
|
||||
Ok(())
|
||||
})?,
|
||||
)?;
|
||||
@@ -402,7 +403,7 @@ pub(super) fn install_in_game(lua: &Lua) -> mlua::Result<()> {
|
||||
data.texture = Some(path);
|
||||
data.circular = true;
|
||||
data.portrait_unit = None;
|
||||
model.touch_layout_art(rh);
|
||||
model.note_art(rh);
|
||||
Ok(())
|
||||
})?,
|
||||
)?;
|
||||
@@ -527,8 +528,9 @@ fn install_region_methods(lua: &Lua) -> mlua::Result<()> {
|
||||
_ => return Err(wrong_type()),
|
||||
};
|
||||
if model.arena.set_region_owner(rh, new_owner) {
|
||||
// The owner supplies the region's scale to layout, so a re-link is a layout change.
|
||||
model.touch_layout_region(rh);
|
||||
// The owner supplies the region's scale to layout; the re-link invalidates nothing
|
||||
// (`0x77fd10`), so the region keeps its rect until the resolve reaches it.
|
||||
model.unsettle_region(rh);
|
||||
}
|
||||
Ok(())
|
||||
},
|
||||
@@ -677,29 +679,108 @@ pub(super) fn region_type_name(model: &Model, rh: RegionHandle) -> &'static str
|
||||
}
|
||||
}
|
||||
|
||||
/// Free a region and every trace of its identity (arena slot, paint, rect, name, id), so a held
|
||||
/// wrapper raises as stale; the reference frees too, as `CSimpleButton::SetFontString 0x778d20`
|
||||
/// deletes the label it replaces.
|
||||
/// Free a region; see [`free_regions`].
|
||||
pub(crate) fn free_region(model: &mut Model, rh: RegionHandle) {
|
||||
model.region_data.remove(&rh);
|
||||
model.region_resolved.remove(&rh);
|
||||
model.region_kept.remove(&rh);
|
||||
let name = model.arena.region(rh).and_then(|r| r.name.clone());
|
||||
if let Some(id) = model.region_to_id.remove(&rh) {
|
||||
model.id_to_region.remove(&id);
|
||||
// The lookup entry goes only if this region is the one it holds; a later holder of the
|
||||
// same name was never in it.
|
||||
if let Some(name) = name {
|
||||
if model.region_by_name.get(&name) == Some(&id) {
|
||||
model.region_by_name.remove(&name);
|
||||
free_regions(model, &[rh]);
|
||||
}
|
||||
|
||||
/// Free regions and every trace of their identity (arena slot, paint, rect, name, id), so a held
|
||||
/// wrapper raises as stale. The reference destroys them: `CSimpleButton::SetFontString` the label
|
||||
/// it replaces (`0x778d3c`), a state-texture setter the texture it displaces (`0x77900a`), a
|
||||
/// backdrop teardown its pieces (`0x77f4d0`), a SimpleHTML parse its old blocks (`0x78a5aa`). The
|
||||
/// destructor (`0x767720` → `0x768e20`) unhooks every object anchored to a dying one with
|
||||
/// `0x767e70(dependent, dying, 0)`: each anchor naming it is freed, and the zero flag queues no
|
||||
/// resolve, so the dependent keeps its state until the resolve reaches it.
|
||||
pub(crate) fn free_regions(model: &mut Model, dying: &[RegionHandle]) {
|
||||
let mut ids = Vec::with_capacity(dying.len());
|
||||
for &rh in dying {
|
||||
model.region_data.remove(&rh);
|
||||
model.region_resolved.remove(&rh);
|
||||
model.region_kept.remove(&rh);
|
||||
let name = model.arena.region(rh).and_then(|r| r.name.clone());
|
||||
if let Some(id) = model.region_to_id.remove(&rh) {
|
||||
model.id_to_region.remove(&id);
|
||||
model.layout_unsettled.remove(&id);
|
||||
// The lookup entry goes only if this region is the one it holds; a later holder of the
|
||||
// same name was never in it.
|
||||
if let Some(name) = name {
|
||||
if model.region_by_name.get(&name) == Some(&id) {
|
||||
model.region_by_name.remove(&name);
|
||||
}
|
||||
}
|
||||
// Its node leaves the cached graph with its edges at the next resolve.
|
||||
model.name_layout_node(id);
|
||||
ids.push(id);
|
||||
}
|
||||
model.arena.destroy_region(rh);
|
||||
}
|
||||
// Only a region with an id can be an anchor target.
|
||||
if ids.is_empty() {
|
||||
return;
|
||||
}
|
||||
let mut regions: Vec<RegionHandle> = Vec::new();
|
||||
let mut frames: Vec<crate::widget::FrameHandle> = Vec::new();
|
||||
match &model.layout_touched {
|
||||
// The cached graph's readers of each dying id, and every node a write named since it was
|
||||
// built, are all that can anchor to one.
|
||||
Some(named) => {
|
||||
let mut maybe = named.clone();
|
||||
for &id in &ids {
|
||||
maybe.extend(model.layout_scope.readers_of(id));
|
||||
}
|
||||
maybe.sort_unstable();
|
||||
maybe.dedup();
|
||||
for id in maybe {
|
||||
if let Some(&rh) = model.id_to_region.get(&id) {
|
||||
if let Some(d) = model.region_data.get_mut(&rh) {
|
||||
if d.anchors.release_targets(&ids) {
|
||||
regions.push(rh);
|
||||
}
|
||||
}
|
||||
} else if let Some(&h) = model.id_to_frame.get(&id) {
|
||||
if let Some(i) = model.layout_inputs.get_mut(&h) {
|
||||
if i.anchors.release_targets(&ids) {
|
||||
frames.push(h);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// No graph to vouch for: every object.
|
||||
None => {
|
||||
for (&rh, d) in model.region_data.iter_mut() {
|
||||
if d.anchors.release_targets(&ids) {
|
||||
regions.push(rh);
|
||||
}
|
||||
}
|
||||
for (&h, i) in model.layout_inputs.iter_mut() {
|
||||
if i.anchors.release_targets(&ids) {
|
||||
frames.push(h);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Its node leaves the cached graph with its edges at the next resolve. What anchored to
|
||||
// it is not invalidated (`0x768e20` unhooks each with `0x767e70`, flag 0), so it keeps
|
||||
// its state.
|
||||
model.name_layout_node(id);
|
||||
}
|
||||
model.arena.destroy_region(rh);
|
||||
// Under `WOW_LAYOUT_VERIFY` the graph's answer is checked against every object.
|
||||
if model.layout_touched.is_some() && super::layout::layout_verify_enabled() {
|
||||
let missed = model
|
||||
.region_data
|
||||
.values()
|
||||
.map(|d| &d.anchors)
|
||||
.chain(model.layout_inputs.values().map(|i| &i.anchors))
|
||||
.any(|a| a.iter().any(|a| ids.contains(&a.relative_to)));
|
||||
assert!(
|
||||
!missed,
|
||||
"WOW_LAYOUT_VERIFY: an object anchored to a freed region was not among the cached \
|
||||
graph's readers of it or the nodes named since"
|
||||
);
|
||||
}
|
||||
// Each dependent re-derives its edges and keeps the state it has.
|
||||
for rh in regions {
|
||||
model.unsettle_region(rh);
|
||||
}
|
||||
for h in frames {
|
||||
model.unsettle_frame(h);
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn region_owner_id(model: &mut Model, rh: RegionHandle) -> u32 {
|
||||
@@ -738,7 +819,8 @@ pub(crate) fn implicit_creation_anchor(model: &mut Model, rh: RegionHandle) {
|
||||
}
|
||||
RegionKind::Title => return,
|
||||
}
|
||||
model.touch_layout_region(rh);
|
||||
// Both set their points with the invalidate flag (`0x7701ec`, `0x7714bc`).
|
||||
model.invalidate_region(rh, true);
|
||||
}
|
||||
|
||||
/// The middle-row point a justify word selects, as the FontString post-step (`0x771480`) and the
|
||||
@@ -767,7 +849,8 @@ pub(crate) fn anchor_unanchored_at(
|
||||
return;
|
||||
}
|
||||
data.anchors = Anchors::from([Anchor::new(point, owner_id, point, 0.0, 0.0)]);
|
||||
model.touch_layout_region(rh);
|
||||
// The adopter's `SetPoint` invalidates (`0x778d96`, flag 1).
|
||||
model.invalidate_region(rh, true);
|
||||
}
|
||||
|
||||
/// [`implicit_creation_anchor`] for the loader, which holds region wrappers.
|
||||
|
||||
@@ -199,11 +199,9 @@ pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
};
|
||||
loaded
|
||||
};
|
||||
// Only the art-sized shape can move, and only a new art: touching the layout on
|
||||
// every repaint would reopen the resolve's change gate each frame.
|
||||
lua.app_data_mut::<Model>()
|
||||
.expect("model")
|
||||
.touch_layout_art(rh);
|
||||
// The draw only (`0x77fdf0`); an art-sized texture now holds a state its next
|
||||
// reaching resolve need not give back.
|
||||
lua.app_data_mut::<Model>().expect("model").note_art(rh);
|
||||
Ok(if loaded {
|
||||
Value::Number(1.0)
|
||||
} else {
|
||||
@@ -485,8 +483,18 @@ impl crate::script::UiScript {
|
||||
pub fn set_texture_size_probe(&mut self, probe: crate::script::TextureSizeProbe) {
|
||||
let mut model = self.model_mut();
|
||||
model.texture_size_probe = Some(probe);
|
||||
// Every art-sized region's span may move, and no per-node hash holds a probe's answer: a
|
||||
// full-scope rebuild, as for a screen move.
|
||||
// The reference has every art's size at hand (`0x44b3b0` loads it), so a probe installed
|
||||
// late re-resolves each texture that takes a span from its art, as if it had been there.
|
||||
let art_sized: Vec<u32> = model
|
||||
.region_data
|
||||
.iter()
|
||||
.filter(|(_, d)| {
|
||||
!d.anchors.is_empty() && d.size.is_none_or(|(w, h)| w == 0.0 || h == 0.0)
|
||||
})
|
||||
.filter_map(|(rh, _)| model.region_to_id.get(rh).copied())
|
||||
.collect();
|
||||
model.layout_pending.extend(art_sized);
|
||||
// No per-node hash holds a probe's answer: a full-scope rebuild, as for a screen move.
|
||||
model.layout_scope.invalidate();
|
||||
model.touch_layout();
|
||||
}
|
||||
|
||||
@@ -143,20 +143,13 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
}
|
||||
};
|
||||
// Every call marks the range (`0x786d16`), the same child again included.
|
||||
let old_child = with_scroll(lua, &this, |s| {
|
||||
with_scroll(lua, &this, |s| {
|
||||
s.range_dirty = true;
|
||||
std::mem::replace(&mut s.child, new_child)
|
||||
s.child = new_child;
|
||||
})?;
|
||||
if old_child != new_child {
|
||||
// The child takes this frame's override anchor and the old one drops it; the
|
||||
// ScrollFrame's node re-derives its new child with it.
|
||||
let h = frame_handle_of(lua, &this)?;
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model");
|
||||
model.touch_layout_frame(h);
|
||||
if let Some(old) = old_child {
|
||||
model.touch_layout_frame(old);
|
||||
}
|
||||
}
|
||||
// The old child keeps its point and is not invalidated (`0x786ce6`–`0x786cf4` only
|
||||
// flags it); the new one is re-anchored on every call (`0x786d11`).
|
||||
reanchor_child(lua, &this)?;
|
||||
Ok(())
|
||||
})?,
|
||||
)?;
|
||||
@@ -192,11 +185,8 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
if !changed {
|
||||
return Ok(());
|
||||
}
|
||||
// The child's override offset: the ScrollFrame's node re-derives its child.
|
||||
let h = frame_handle_of(lua, &this)?;
|
||||
lua.app_data_mut::<Model>()
|
||||
.expect("model")
|
||||
.touch_layout_frame(h);
|
||||
// The re-anchor at the new offset (`0x786dd8` → `0x787100`).
|
||||
reanchor_child(lua, &this)?;
|
||||
fire_scroll(lua, &this, "OnVerticalScroll", px)
|
||||
})?,
|
||||
)?;
|
||||
@@ -226,10 +216,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
if !changed {
|
||||
return Ok(());
|
||||
}
|
||||
let h = frame_handle_of(lua, &this)?;
|
||||
lua.app_data_mut::<Model>()
|
||||
.expect("model")
|
||||
.touch_layout_frame(h);
|
||||
reanchor_child(lua, &this)?;
|
||||
fire_scroll(lua, &this, "OnHorizontalScroll", px)
|
||||
})?,
|
||||
)?;
|
||||
@@ -357,3 +344,32 @@ fn see_size(s: &mut ScrollFrameState, rect: Option<crate::layout::Rect>, eps: f6
|
||||
}
|
||||
s.seen_size = size;
|
||||
}
|
||||
|
||||
/// The re-anchor (`0x787100`): when the frame holds a child, clear its points (`0x787110` →
|
||||
/// `0x767620`) and set one TOPLEFT on the frame's TOPLEFT at the raw `(horizontal, vertical)`
|
||||
/// offsets, y up, with the invalidate flag (`0x787136`). A real write on the child: its own
|
||||
/// points are gone, and it keeps the new one when the frame lets it go.
|
||||
fn reanchor_child(lua: &Lua, this: &Table) -> mlua::Result<()> {
|
||||
let (child, x, y) = with_scroll(lua, this, |s| (s.child, s.horizontal, s.vertical))?;
|
||||
let Some(child) = child else {
|
||||
return Ok(());
|
||||
};
|
||||
let h = frame_handle_of(lua, this)?;
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model");
|
||||
if model.arena.frame(child).is_none() {
|
||||
return Ok(());
|
||||
}
|
||||
let sid = model.frame_id(h);
|
||||
let pin = crate::layout::Anchors::from([crate::layout::Anchor::new(
|
||||
crate::layout::Point::TopLeft,
|
||||
sid,
|
||||
crate::layout::Point::TopLeft,
|
||||
x,
|
||||
y,
|
||||
)]);
|
||||
let input = model.layout_inputs.entry(child).or_default();
|
||||
let changed = !input.anchors.bits_eq(&pin);
|
||||
input.anchors = pin;
|
||||
model.invalidate_frame(child, changed);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -166,16 +166,14 @@ fn resolve_font(model: &Model, ef: &ElementFont) -> BlockPaint {
|
||||
/// `CSimpleHTML::SetText` (`0x78a3a0`): free the old blocks, parse, build. Returns `usedMarkup`
|
||||
/// (`0x78a519`), which the Lua shim drops.
|
||||
pub(crate) fn set_text(model: &mut Model, fh: FrameHandle, raw: &str) -> bool {
|
||||
// Free the previous parse's regions first, as the reference does, or a book's next page
|
||||
// draws over the last.
|
||||
// Free the previous parse's regions first, as the reference does (`0x78a580`), or a book's
|
||||
// next page draws over the last.
|
||||
let old = model
|
||||
.simple_html
|
||||
.get_mut(&fh)
|
||||
.map(|s| std::mem::take(&mut s.blocks))
|
||||
.unwrap_or_default();
|
||||
for rh in old {
|
||||
free_block(model, rh);
|
||||
}
|
||||
crate::script::region::free_regions(model, &old);
|
||||
|
||||
let (frame_name, hyperlink_format) = {
|
||||
let name = model
|
||||
@@ -289,7 +287,7 @@ fn build(model: &mut Model, fh: FrameHandle, blocks: &[Block]) {
|
||||
else {
|
||||
break; // unreachable, as above
|
||||
};
|
||||
// Minted though nothing anchors to an image; the resolve and `free_block` use it.
|
||||
// Minted though nothing anchors to an image; the resolve and the free use it.
|
||||
model.region_id(rh);
|
||||
// `align` picks the corner; 8, 16 and 32 get no anchor at all (`0x78ac61`, a jump
|
||||
// to `0x78ace8`), so the image draws nothing, here as there.
|
||||
@@ -332,9 +330,10 @@ fn build(model: &mut Model, fh: FrameHandle, blocks: &[Block]) {
|
||||
}
|
||||
}
|
||||
|
||||
// Each new block joins the graph; the old ones' frees named theirs.
|
||||
// Each new block's `SetPoint` invalidates it (`0x78ae0c`, flag 1); the old ones' frees named
|
||||
// theirs.
|
||||
for &rh in &made {
|
||||
model.touch_layout_region(rh);
|
||||
model.invalidate_region(rh, true);
|
||||
}
|
||||
model.simple_html.entry(fh).or_default().blocks = made;
|
||||
}
|
||||
@@ -356,10 +355,6 @@ fn anchor_for(prev_block: Option<u32>, frame_id: u32, point: Point, next_y: f32)
|
||||
}
|
||||
}
|
||||
|
||||
fn free_block(model: &mut Model, rh: RegionHandle) {
|
||||
crate::script::region::free_region(model, rh);
|
||||
}
|
||||
|
||||
// ── The loader's seam ────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// Apply the `font=`, `<FontHeight>` and `outline=` an XML `<FontString>` or `<FontStringHeaderN>`
|
||||
|
||||
@@ -195,7 +195,7 @@ fn emblem_texture(lua: &Lua, this: Value, texture: Value, upper: bool) -> mlua::
|
||||
data.fill = None;
|
||||
data.portrait_unit = None;
|
||||
data.circular = false;
|
||||
model.touch_layout_art(rh);
|
||||
model.note_art(rh);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -198,7 +198,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
let art = model.taxi.as_ref().map(|t| t.art.clone());
|
||||
if let Some(art) = art {
|
||||
model.region_data.entry(rh).or_default().texture = Some(art);
|
||||
model.touch_layout_art(rh);
|
||||
model.note_art(rh);
|
||||
}
|
||||
Ok(())
|
||||
})?,
|
||||
|
||||
@@ -0,0 +1,211 @@
|
||||
//! A layout object's destructor (`0x767720` → `0x768e20`) unhooks each object anchored to it with
|
||||
//! `0x767e70(dependent, dying, 0)`: every anchor slot naming the dying object is freed, and the
|
||||
//! zero flag skips the invalidate (`0x7680e0`). So `GetNumPoints` (`0x7a2260`) drops and `GetPoint`
|
||||
//! no longer answers those points, while the dependent keeps its rect until its own next
|
||||
//! invalidate. A region dies to `Button:SetFontString` (`0x778d3c`), a state-texture setter
|
||||
//! (`0x77900a`), a backdrop teardown (`0x77f4d0`) and a SimpleHTML re-parse (`0x78a5aa`).
|
||||
|
||||
use super::common::script;
|
||||
use crate::script::UiScript;
|
||||
|
||||
/// Resolve, then require the cached graph and its rects to equal a from-scratch solve.
|
||||
fn resolve_matches_scratch(s: &mut UiScript, what: &str) {
|
||||
s.resolve();
|
||||
let mut model = s.model_mut();
|
||||
if let Some(why) = UiScript::scratch_divergence(&mut model) {
|
||||
panic!("{what}: {why}");
|
||||
}
|
||||
}
|
||||
|
||||
fn num(s: &mut UiScript, expr: &str) -> Option<f64> {
|
||||
s.eval::<Option<f64>>(&format!("return {expr}")).unwrap()
|
||||
}
|
||||
|
||||
/// The issue's sequence: a texture anchored to a button's label and to the button; the label
|
||||
/// replaced. The anchor to the destroyed label is gone, the other stands, and the texture keeps
|
||||
/// its rect, which the anchor that is left would move, until its own invalidate.
|
||||
#[test]
|
||||
fn a_replaced_label_leaves_its_dependents_anchors() {
|
||||
let mut s = script();
|
||||
s.set_screen_size(800.0, 600.0);
|
||||
s.run(
|
||||
r#"
|
||||
b = CreateFrame("Button", "B")
|
||||
b:SetPoint("BOTTOMLEFT", nil, "BOTTOMLEFT", 100, 100)
|
||||
b:SetWidth(200); b:SetHeight(40)
|
||||
b:SetFontString(b:CreateFontString("Old", "ARTWORK"))
|
||||
Old:SetWidth(50); Old:SetHeight(10)
|
||||
t = b:CreateTexture("T", "OVERLAY")
|
||||
t:SetTexture(1, 1, 1); t:SetWidth(10); t:SetHeight(10)
|
||||
t:SetPoint("LEFT", b:GetFontString(), "RIGHT")
|
||||
t:SetPoint("TOP", b, "TOP")
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
s.resolve();
|
||||
assert_eq!(s.eval::<i64>("return t:GetNumPoints()").unwrap(), 2);
|
||||
assert_eq!(
|
||||
num(&mut s, "t:GetLeft()"),
|
||||
Some(225.0),
|
||||
"right of the label"
|
||||
);
|
||||
|
||||
s.run(r#"b:SetFontString(b:CreateFontString("New", "ARTWORK"))"#)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
s.eval::<i64>("return t:GetNumPoints()").unwrap(),
|
||||
1,
|
||||
"the anchor to the destroyed label is freed"
|
||||
);
|
||||
assert_eq!(
|
||||
s.eval::<i64>("return table.getn({ t:GetPoint(2) })")
|
||||
.unwrap(),
|
||||
0,
|
||||
"and no second point answers"
|
||||
);
|
||||
assert!(s
|
||||
.eval::<bool>(
|
||||
r#"local p, r, rp = t:GetPoint(1); return p == "TOP" and r == b and rp == "TOP""#
|
||||
)
|
||||
.unwrap());
|
||||
|
||||
resolve_matches_scratch(&mut s, "the label replaced");
|
||||
assert_eq!(
|
||||
num(&mut s, "t:GetLeft()"),
|
||||
Some(225.0),
|
||||
"the unhook queued no resolve"
|
||||
);
|
||||
// Its own invalidate resolves it from the anchor left: centred under the button's top.
|
||||
s.run("t:SetWidth(12)").unwrap();
|
||||
resolve_matches_scratch(&mut s, "the texture's own invalidate");
|
||||
assert_eq!(num(&mut s, "t:GetLeft()"), Some(194.0));
|
||||
assert!(s.errors().is_empty(), "{:?}", s.errors());
|
||||
}
|
||||
|
||||
/// A texture and a frame each anchored to a backdrop piece and to the frame: the teardown frees
|
||||
/// the piece's anchors only, and both keep their rects.
|
||||
#[test]
|
||||
fn a_backdrop_teardown_frees_the_anchors_to_its_pieces() {
|
||||
let mut s = script();
|
||||
s.set_screen_size(800.0, 600.0);
|
||||
s.run(
|
||||
r#"
|
||||
f = CreateFrame("Frame")
|
||||
f:SetPoint("BOTTOMLEFT", nil, "BOTTOMLEFT", 100, 100)
|
||||
f:SetWidth(100); f:SetHeight(100)
|
||||
f:SetBackdrop({ bgFile = "bg", edgeFile = "edge" })
|
||||
local piece = ({ f:GetRegions() })[1]
|
||||
local g = CreateFrame("Frame")
|
||||
dep = g:CreateTexture(nil, "ARTWORK")
|
||||
dep:SetTexture("dep"); dep:SetWidth(10); dep:SetHeight(10)
|
||||
dep:SetPoint("TOPLEFT", piece, "TOPLEFT", 5, -5)
|
||||
dep:SetPoint("BOTTOM", f, "BOTTOM", 0, 20)
|
||||
h = CreateFrame("Frame")
|
||||
h:SetWidth(10); h:SetHeight(10)
|
||||
h:SetPoint("TOPLEFT", piece, "TOPLEFT", 3, -3)
|
||||
h:SetPoint("BOTTOM", f, "BOTTOM", 0, 30)
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
s.resolve();
|
||||
let before = (
|
||||
num(&mut s, "dep:GetLeft()"),
|
||||
num(&mut s, "dep:GetTop()"),
|
||||
num(&mut s, "h:GetLeft()"),
|
||||
num(&mut s, "h:GetTop()"),
|
||||
);
|
||||
assert!(before.0.is_some() && before.2.is_some(), "{before:?}");
|
||||
|
||||
s.run("f:SetBackdrop(nil)").unwrap();
|
||||
assert_eq!(s.eval::<i64>("return dep:GetNumPoints()").unwrap(), 1);
|
||||
assert_eq!(s.eval::<i64>("return h:GetNumPoints()").unwrap(), 1);
|
||||
assert!(s
|
||||
.eval::<bool>(r#"local p, r = dep:GetPoint(1); return p == "BOTTOM" and r == f"#)
|
||||
.unwrap());
|
||||
|
||||
resolve_matches_scratch(&mut s, "the backdrop torn down");
|
||||
let after = (
|
||||
num(&mut s, "dep:GetLeft()"),
|
||||
num(&mut s, "dep:GetTop()"),
|
||||
num(&mut s, "h:GetLeft()"),
|
||||
num(&mut s, "h:GetTop()"),
|
||||
);
|
||||
assert_eq!(after, before, "no resolve was queued for either");
|
||||
assert!(s.errors().is_empty(), "{:?}", s.errors());
|
||||
}
|
||||
|
||||
/// A ScrollFrame's re-anchor clears its child's points (`0x787110`) and sets one on the frame, so
|
||||
/// the child holds no anchor to a label it was placed on, and the label's destruction leaves it be.
|
||||
#[test]
|
||||
fn a_scroll_child_holds_no_anchor_to_a_label_it_was_placed_on() {
|
||||
let mut s = script();
|
||||
s.set_screen_size(800.0, 600.0);
|
||||
s.run(
|
||||
r#"
|
||||
b = CreateFrame("Button", "B")
|
||||
b:SetPoint("BOTTOMLEFT", nil, "BOTTOMLEFT", 100, 100)
|
||||
b:SetWidth(200); b:SetHeight(40)
|
||||
b:SetFontString(b:CreateFontString("Old", "ARTWORK"))
|
||||
Old:SetWidth(50); Old:SetHeight(10)
|
||||
sf = CreateFrame("ScrollFrame", "SF")
|
||||
sf:SetPoint("TOPLEFT", nil, "TOPLEFT", 10, -10)
|
||||
sf:SetWidth(50); sf:SetHeight(50)
|
||||
c = CreateFrame("Frame", "Child")
|
||||
c:SetWidth(5); c:SetHeight(5)
|
||||
c:SetPoint("TOPLEFT", Old, "TOPLEFT")
|
||||
sf:SetScrollChild(c)
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
s.resolve();
|
||||
let left = num(&mut s, "c:GetLeft()");
|
||||
assert_eq!(left, Some(10.0), "placed by the override");
|
||||
|
||||
assert_eq!(s.eval::<i64>("return c:GetNumPoints()").unwrap(), 1);
|
||||
s.run(r#"b:SetFontString(b:CreateFontString("New", "ARTWORK"))"#)
|
||||
.unwrap();
|
||||
assert!(s
|
||||
.eval::<bool>(r#"local p, r = c:GetPoint(1); return c:GetNumPoints() == 1 and r == sf"#)
|
||||
.unwrap());
|
||||
resolve_matches_scratch(&mut s, "the label replaced");
|
||||
assert_eq!(num(&mut s, "c:GetLeft()"), left);
|
||||
assert!(s.errors().is_empty(), "{:?}", s.errors());
|
||||
}
|
||||
|
||||
/// The re-link into another frame (`0x77fd10`) invalidates nothing: a region moved under a frame of
|
||||
/// another scale keeps its rect until its own invalidate, which then reads the new owner's scale.
|
||||
#[test]
|
||||
fn a_region_reowned_keeps_its_rect_until_its_own_invalidate() {
|
||||
let mut s = script();
|
||||
s.set_screen_size(800.0, 600.0);
|
||||
s.run(
|
||||
r#"
|
||||
a = CreateFrame("Frame", "A"); a:SetWidth(10); a:SetHeight(10)
|
||||
a:SetPoint("BOTTOMLEFT", nil, "BOTTOMLEFT", 0, 0)
|
||||
b = CreateFrame("Frame", "B"); b:SetWidth(10); b:SetHeight(10)
|
||||
b:SetPoint("BOTTOMLEFT", nil, "BOTTOMLEFT", 0, 0); b:SetScale(2)
|
||||
x = CreateFrame("Frame", "X"); x:SetWidth(100); x:SetHeight(100)
|
||||
x:SetPoint("BOTTOMLEFT", nil, "BOTTOMLEFT", 100, 100)
|
||||
t = a:CreateTexture("T", "ARTWORK"); t:SetTexture(1, 1, 1)
|
||||
t:SetWidth(8); t:SetHeight(8); t:SetPoint("BOTTOMLEFT", x, "BOTTOMLEFT", 0, 0)
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
s.resolve();
|
||||
assert_eq!(num(&mut s, "T:GetRight()"), Some(108.0));
|
||||
s.run("t:SetParent(b)").unwrap();
|
||||
resolve_matches_scratch(&mut s, "re-owned");
|
||||
assert_eq!(
|
||||
num(&mut s, "T:GetRight()"),
|
||||
Some(54.0),
|
||||
"the rect it had, read in the new owner's units"
|
||||
);
|
||||
s.run("t:SetWidth(8)").unwrap();
|
||||
resolve_matches_scratch(&mut s, "its own invalidate");
|
||||
assert_eq!(
|
||||
num(&mut s, "T:GetRight()"),
|
||||
Some(58.0),
|
||||
"8 units at scale 2, from 100"
|
||||
);
|
||||
assert!(s.errors().is_empty(), "{:?}", s.errors());
|
||||
}
|
||||
@@ -86,10 +86,10 @@ const POINTS: [&str; 9] = [
|
||||
|
||||
/// The Lua state the steps act on: `N` every node in creation order, `F` the frames, `R` the
|
||||
/// regions, `B` the buttons, `S` the ScrollFrames, `owned[child]` the ScrollFrame that holds it.
|
||||
/// `deps[o][point]` mirrors each anchor's target and `held[c]` each scroll override, so `target`
|
||||
/// `deps[o][point]` mirrors each anchor's target and `held[c]` each scroll claim, so `target`
|
||||
/// picks any node, older or newer, whose rect does not already read `o`'s: the graph stays acyclic.
|
||||
const SETUP: &str = r#"
|
||||
N, F, R, B, S, at, owned, deps, held = {}, {}, {}, {}, {}, {}, {}, {}, {}
|
||||
N, F, R, B, S, at, owned, deps, held, pieced = {}, {}, {}, {}, {}, {}, {}, {}, {}, {}
|
||||
function add(o) table.insert(N, o); at[o] = table.getn(N); deps[o] = {} end
|
||||
-- Whether `a`'s rect reads `b`'s, through anchors and scroll overrides.
|
||||
function reaches(a, b)
|
||||
@@ -181,15 +181,25 @@ fn step(rng: &mut Rng) -> Write {
|
||||
"local f = F[math.mod({k}, table.getn(F)) + 1]
|
||||
if f:GetBackdrop() then f:SetBackdrop(nil) else f:SetBackdrop(spec) end"
|
||||
),
|
||||
// A node anchored to a backdrop piece, so a teardown frees what something reads.
|
||||
// A node, a texture or a frame, anchored to a fresh backdrop piece (the last region the
|
||||
// build links) and, at another point, to any node, so a later teardown frees one of its
|
||||
// anchors and it stays a node.
|
||||
5 => format!(
|
||||
"local f = F[math.mod({k}, table.getn(F)) + 1]
|
||||
local piece = ({{ f:GetRegions() }})[1]
|
||||
if piece and at[f] and not at[piece] then
|
||||
deps[piece] = {{ f }}
|
||||
local t = f:CreateTexture(nil, 'OVERLAY'); add(t); table.insert(R, t)
|
||||
t:SetWidth(5); t:SetHeight(5); point(t, '{p}', piece, '{rp}', {x}, {y})
|
||||
end"
|
||||
f:SetBackdrop(spec)
|
||||
local rs = {{ f:GetRegions() }}
|
||||
local piece = rs[table.getn(rs)]
|
||||
deps[piece] = {{ f }}
|
||||
local o
|
||||
if {k2} < 600 then
|
||||
o = f:CreateTexture(nil, 'OVERLAY'); add(o); table.insert(R, o)
|
||||
else
|
||||
o = CreateFrame('Frame', nil, f); add(o); table.insert(F, o)
|
||||
end
|
||||
o:SetWidth(5); o:SetHeight(5)
|
||||
point(o, '{p}', piece, '{rp}', {x}, {y})
|
||||
if '{p}' ~= '{rp}' then point(o, '{rp}', target(o, {k2}), '{p}', {y}, {x}) end
|
||||
table.insert(pieced, {{ o, o:GetNumPoints() }})"
|
||||
),
|
||||
// A button's slot region set, or freed by a nil; or two freed in one pass.
|
||||
6 => format!(
|
||||
@@ -260,6 +270,7 @@ fn step(rng: &mut Rng) -> Write {
|
||||
local old = sf:GetScrollChild()
|
||||
if old then owned[old] = nil; held[old] = nil end
|
||||
sf:SetScrollChild(c); owned[c] = sf; held[c] = sf
|
||||
deps[c] = {{ TOPLEFT = sf }}
|
||||
break
|
||||
end
|
||||
end
|
||||
@@ -348,6 +359,22 @@ fn random_structural_writes_resolve_as_a_scratch_solve_would() {
|
||||
"return table.getn(N), raised, table.getn(F), table.getn(R), newer",
|
||||
)
|
||||
.expect("count");
|
||||
// Dependents of a backdrop piece that a later teardown or rebuild freed: each lost the
|
||||
// anchor to it, and the scratch solve above agreed with every pass in between.
|
||||
let unhooked = s
|
||||
.eval::<i64>(
|
||||
"local n = 0
|
||||
for _, e in pieced do
|
||||
local ok, now = pcall(e[1].GetNumPoints, e[1])
|
||||
if ok and now < e[2] then n = n + 1 end
|
||||
end
|
||||
return n",
|
||||
)
|
||||
.expect("unhooked");
|
||||
assert!(
|
||||
unhooked > 10,
|
||||
"seed {seed:#x}: only {unhooked} dependents of a backdrop piece saw it freed"
|
||||
);
|
||||
eprintln!(
|
||||
"seed {seed:#x}: {writes} writes, {nodes} nodes ({frames} frames, {regions} \
|
||||
regions), {raised} raised, {newer} anchors to a newer node"
|
||||
@@ -372,10 +399,10 @@ fn random_structural_writes_resolve_as_a_scratch_solve_would() {
|
||||
}
|
||||
}
|
||||
|
||||
/// A frame two ScrollFrames claim takes the derivation, whose override the cached graph cannot
|
||||
/// name; the result still equals a scratch solve, and a lone claim stays incremental.
|
||||
/// A frame two ScrollFrames claim holds the last one's re-anchor (`0x787100`), one real point,
|
||||
/// through the cached graph: no derivation.
|
||||
#[test]
|
||||
fn a_child_two_scrollframes_claim_falls_back_to_the_derivation() {
|
||||
fn a_child_two_scrollframes_claim_holds_the_last_re_anchor() {
|
||||
let mut s = script();
|
||||
s.set_screen_size(800.0, 600.0);
|
||||
s.run(
|
||||
@@ -394,14 +421,13 @@ fn a_child_two_scrollframes_claim_falls_back_to_the_derivation() {
|
||||
s.run("a:SetScrollChild(c); a:SetVerticalScroll(5)")
|
||||
.expect("one claim");
|
||||
resolve_matches_scratch(&mut s, "one claim");
|
||||
assert_eq!(s.layout_derivations(), derived, "one claim is named");
|
||||
s.run("b:SetScrollChild(c)").expect("two claims");
|
||||
resolve_matches_scratch(&mut s, "two claims");
|
||||
assert_eq!(
|
||||
s.layout_derivations(),
|
||||
derived + 1,
|
||||
"two claims give up for the derivation"
|
||||
);
|
||||
assert_eq!(s.layout_derivations(), derived, "every claim is named");
|
||||
assert!(s
|
||||
.eval::<bool>("local p, r = c:GetPoint(1); return c:GetNumPoints() == 1 and r == b")
|
||||
.unwrap());
|
||||
assert_eq!(s.eval::<f64>("return c:GetLeft()").unwrap(), 200.0);
|
||||
}
|
||||
|
||||
/// A frame whose id is minted after a region on it joined the graph (the engine builds some
|
||||
|
||||
@@ -19,6 +19,7 @@ mod cvar_bindings;
|
||||
mod dispatch_bench;
|
||||
mod drag_pin;
|
||||
mod drag_threshold;
|
||||
mod dying_target;
|
||||
mod end_to_end;
|
||||
mod engine_updates;
|
||||
mod events;
|
||||
@@ -65,6 +66,7 @@ mod structure_queries;
|
||||
mod talent;
|
||||
mod taxi;
|
||||
mod texcoord_font;
|
||||
mod texture_art;
|
||||
mod tooltip;
|
||||
mod tooltip_item;
|
||||
mod tooltip_spell;
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
use super::common::script;
|
||||
use crate::layout::Rect;
|
||||
use crate::script::QuadContent;
|
||||
use crate::script::{QuadContent, UiScript};
|
||||
|
||||
/// The rect of the first Texture quad whose path is `marker`.
|
||||
fn marker_rect(quads: &[crate::script::ExtractedQuad], marker: &str) -> Option<Rect> {
|
||||
@@ -140,15 +140,64 @@ fn scroll_child_top_tracks_vertical_scroll_unclamped_then_restores_on_clear() {
|
||||
"a negative offset lowers the child: frame top 500 - 30"
|
||||
);
|
||||
|
||||
// Clearing the child stops the override, and its own untouched anchor applies again.
|
||||
// Clearing the child leaves its one point alone and invalidates nothing (`0x786ce6`): it
|
||||
// stays at the scroll frame's corner, and follows the frame when the frame moves.
|
||||
s.run("SF:SetScrollChild(nil)").unwrap();
|
||||
s.resolve();
|
||||
let quads = s.extract();
|
||||
assert_eq!(
|
||||
marker_rect(&quads, "marker:child").map(|r| r.top),
|
||||
Some(580.0),
|
||||
"authored anchor survives: screen top 600 - 20"
|
||||
Some(470.0),
|
||||
"the re-anchor's point stands: frame top 500 - 30"
|
||||
);
|
||||
s.run(r#"SF:SetPoint("TOPLEFT", 0, -150)"#).unwrap();
|
||||
s.resolve();
|
||||
assert_eq!(
|
||||
marker_rect(&s.extract(), "marker:child").map(|r| r.top),
|
||||
Some(420.0),
|
||||
"still on the frame's corner: frame top 450 - 30"
|
||||
);
|
||||
}
|
||||
|
||||
/// The re-anchor (`0x787100`) is a real write on the child: its points are cleared (`0x787110`)
|
||||
/// and one TOPLEFT on the scroll frame's TOPLEFT is set at the offsets (`0x787136`), at
|
||||
/// `SetScrollChild` and at each offset change; the authored points are gone.
|
||||
#[test]
|
||||
fn the_scroll_child_holds_one_topleft_on_the_scroll_frame() {
|
||||
let mut s = script();
|
||||
s.set_screen_size(800.0, 600.0);
|
||||
s.run(
|
||||
r#"
|
||||
sf = CreateFrame("ScrollFrame", "SF")
|
||||
sf:SetPoint("TOPLEFT", 0, -100); sf:SetWidth(300); sf:SetHeight(200)
|
||||
c = CreateFrame("Frame", "Child")
|
||||
c:SetPoint("CENTER", UIParent); c:SetPoint("BOTTOM", 0, 10)
|
||||
c:SetWidth(100); c:SetHeight(400)
|
||||
sf:SetScrollChild(c)
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
let point = |s: &mut UiScript| {
|
||||
s.eval::<(i64, String, bool, String, f64, f64)>(
|
||||
"local p, r, rp, x, y = c:GetPoint(1); return c:GetNumPoints(), p, r == sf, rp, x, y",
|
||||
)
|
||||
.unwrap()
|
||||
};
|
||||
assert_eq!(
|
||||
point(&mut s),
|
||||
(1, "TOPLEFT".into(), true, "TOPLEFT".into(), 0.0, 0.0)
|
||||
);
|
||||
s.run("sf:SetVerticalScroll(30); sf:SetHorizontalScroll(-12)")
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
point(&mut s),
|
||||
(1, "TOPLEFT".into(), true, "TOPLEFT".into(), -12.0, 30.0),
|
||||
"scrolling moves the one point"
|
||||
);
|
||||
s.resolve();
|
||||
assert_eq!(s.eval::<f64>("return c:GetTop()").unwrap(), 530.0);
|
||||
assert_eq!(s.eval::<f64>("return c:GetLeft()").unwrap(), -12.0);
|
||||
assert!(s.errors().is_empty(), "{:?}", s.errors());
|
||||
}
|
||||
|
||||
/// `0x786db0` acts only when the offset differs from the stored one; a value past the range is
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
//! `SetTexture` (`0x770200`, the colour form `0x770360`) writes the texture slot and marks the
|
||||
//! draw batch dirty (`0x77fdf0`); it never invalidates the layout (`0x7680e0`). A texture's axis
|
||||
//! sized 0 takes its texel extent only when the texture resolves: the resolver reads the size
|
||||
//! through the geometry vtable (`0x767579`, `0x7675e9`), which a Texture answers from its art
|
||||
//! (`0x770720`, loading it first if it must, `0x44b3b0`). New art moves nothing until the
|
||||
//! texture's own next invalidate, or a target's new rect.
|
||||
|
||||
use super::common::script;
|
||||
use crate::layout::Rect;
|
||||
use crate::script::{UiScript, ZTarget};
|
||||
|
||||
/// Resolve, then require the cached graph and its rects to equal a from-scratch solve.
|
||||
fn resolve_matches_scratch(s: &mut UiScript, what: &str) {
|
||||
s.resolve();
|
||||
let mut model = s.model_mut();
|
||||
if let Some(why) = UiScript::scratch_divergence(&mut model) {
|
||||
panic!("{what}: {why}");
|
||||
}
|
||||
}
|
||||
|
||||
fn num(s: &mut UiScript, expr: &str) -> Option<f64> {
|
||||
s.eval::<Option<f64>>(&format!("return {expr}")).unwrap()
|
||||
}
|
||||
|
||||
/// The rect the render list draws region `name` at, if it draws.
|
||||
fn drawn(s: &UiScript, name: &str) -> Option<Rect> {
|
||||
let rh = {
|
||||
let m = s.model_ref();
|
||||
let id = *m.region_by_name.get(name).expect("a named region");
|
||||
*m.id_to_region.get(&id).expect("a live region")
|
||||
};
|
||||
s.extract()
|
||||
.into_iter()
|
||||
.find(|q| q.target == ZTarget::Region(rh))
|
||||
.and_then(|q| q.rect)
|
||||
}
|
||||
|
||||
/// `Owner` at `(100, 100)`–`(300, 200)`, with art `Art` 64×32 and `Big` 128×64.
|
||||
fn owner() -> UiScript {
|
||||
let mut s = script();
|
||||
s.set_screen_size(800.0, 600.0);
|
||||
s.set_texture_size_probe(Box::new(|p| match p {
|
||||
"Art" => Some((64, 32)),
|
||||
"Big" => Some((128, 64)),
|
||||
_ => None,
|
||||
}));
|
||||
s.run(
|
||||
r#"
|
||||
f = CreateFrame("Frame", "Owner")
|
||||
f:SetPoint("BOTTOMLEFT", nil, "BOTTOMLEFT", 100, 100)
|
||||
f:SetWidth(200); f:SetHeight(100)
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
s
|
||||
}
|
||||
|
||||
/// One anchor and no size: with no art the resolve fails for want of a span. Art set after
|
||||
/// leaves it failed, the path form and the colour form alike, until its own invalidate.
|
||||
#[test]
|
||||
fn a_failed_texture_stays_failed_when_its_art_arrives() {
|
||||
let mut s = owner();
|
||||
s.run(
|
||||
r#"
|
||||
T = f:CreateTexture("T", "ARTWORK"); T:SetPoint("TOPLEFT", f, "TOPLEFT", 0, 0)
|
||||
C = f:CreateTexture("C", "ARTWORK"); C:SetPoint("TOPLEFT", f, "TOPLEFT", 0, 0)
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
s.resolve();
|
||||
assert_eq!(num(&mut s, "T:GetLeft()"), None, "no art, no span");
|
||||
assert_eq!(num(&mut s, "C:GetLeft()"), None);
|
||||
|
||||
s.run(r#"T:SetTexture("Art"); C:SetTexture(1, 1, 1)"#)
|
||||
.unwrap();
|
||||
resolve_matches_scratch(&mut s, "art on failed textures");
|
||||
assert_eq!(
|
||||
num(&mut s, "T:GetLeft()"),
|
||||
None,
|
||||
"SetTexture queued no resolve"
|
||||
);
|
||||
assert_eq!(num(&mut s, "C:GetLeft()"), None);
|
||||
assert_eq!(drawn(&s, "T"), None, "and it draws nothing");
|
||||
assert_eq!(drawn(&s, "C"), None);
|
||||
// The size getter reads the art live (`0x770720`), resolved or not.
|
||||
assert_eq!(num(&mut s, "T:GetWidth()"), Some(64.0));
|
||||
|
||||
s.run(
|
||||
r#"T:SetPoint("TOPLEFT", f, "TOPLEFT", 1, 0); C:SetPoint("TOPLEFT", f, "TOPLEFT", 2, 0)"#,
|
||||
)
|
||||
.unwrap();
|
||||
resolve_matches_scratch(&mut s, "their own invalidates");
|
||||
assert_eq!(num(&mut s, "T:GetRight()"), Some(165.0), "the art's 64");
|
||||
assert_eq!(num(&mut s, "T:GetBottom()"), Some(168.0), "and its 32");
|
||||
assert_eq!(num(&mut s, "C:GetRight()"), Some(110.0), "the solid's 8");
|
||||
assert!(s.errors().is_empty(), "{:?}", s.errors());
|
||||
}
|
||||
|
||||
/// A texture resolved at its art's size keeps that rect when the art changes, and takes the new
|
||||
/// art's size when a target stores a new rect (`0x768cf0`).
|
||||
#[test]
|
||||
fn a_resolved_texture_keeps_its_rect_when_its_art_changes() {
|
||||
let mut s = owner();
|
||||
s.run(
|
||||
r#"T = f:CreateTexture("T", "ARTWORK"); T:SetTexture("Art")
|
||||
T:SetPoint("TOPLEFT", f, "TOPLEFT", 0, 0)"#,
|
||||
)
|
||||
.unwrap();
|
||||
s.resolve();
|
||||
assert_eq!(num(&mut s, "T:GetRight()"), Some(164.0));
|
||||
let before = drawn(&s, "T").expect("drawn");
|
||||
|
||||
s.run(r#"T:SetTexture("Big")"#).unwrap();
|
||||
resolve_matches_scratch(&mut s, "new art");
|
||||
assert_eq!(num(&mut s, "T:GetRight()"), Some(164.0), "the rect stands");
|
||||
assert_eq!(drawn(&s, "T"), Some(before), "and draws there");
|
||||
assert_eq!(
|
||||
num(&mut s, "T:GetWidth()"),
|
||||
Some(128.0),
|
||||
"the getter reads the art"
|
||||
);
|
||||
|
||||
// The owner's new rect reaches the texture, whose resolve reads the art it holds now.
|
||||
s.run("f:SetHeight(120)").unwrap();
|
||||
resolve_matches_scratch(&mut s, "the target moved");
|
||||
assert_eq!(
|
||||
num(&mut s, "T:GetRight()"),
|
||||
Some(228.0),
|
||||
"the new art's 128"
|
||||
);
|
||||
assert_eq!(num(&mut s, "T:GetTop()"), Some(220.0));
|
||||
assert!(s.errors().is_empty(), "{:?}", s.errors());
|
||||
}
|
||||
|
||||
/// A write the reference invalidates for, though it moves no input, re-resolves a texture whose
|
||||
/// art changed since: `SetWidth` always invalidates (`0x7683d0`), so the new art's span lands.
|
||||
#[test]
|
||||
fn a_same_value_invalidate_after_new_art_takes_the_art() {
|
||||
let mut s = owner();
|
||||
s.run(
|
||||
r#"T = f:CreateTexture("T", "ARTWORK"); T:SetTexture("Art")
|
||||
T:SetPoint("TOPLEFT", f, "TOPLEFT", 0, 0); T:SetWidth(0)"#,
|
||||
)
|
||||
.unwrap();
|
||||
s.resolve();
|
||||
assert_eq!(num(&mut s, "T:GetRight()"), Some(164.0));
|
||||
s.run(r#"T:SetTexture("Big")"#).unwrap();
|
||||
resolve_matches_scratch(&mut s, "new art");
|
||||
assert_eq!(
|
||||
num(&mut s, "T:GetRight()"),
|
||||
Some(164.0),
|
||||
"nothing reached it"
|
||||
);
|
||||
s.run("T:SetWidth(0)").unwrap();
|
||||
resolve_matches_scratch(&mut s, "the same width");
|
||||
assert_eq!(
|
||||
num(&mut s, "T:GetRight()"),
|
||||
Some(228.0),
|
||||
"the invalidate read the new art"
|
||||
);
|
||||
assert!(s.errors().is_empty(), "{:?}", s.errors());
|
||||
}
|
||||
@@ -1174,3 +1174,44 @@ fn the_fade_end_restores_full_alpha_before_its_hide() {
|
||||
.unwrap();
|
||||
assert!(s.take_errors().is_empty());
|
||||
}
|
||||
|
||||
/// The tooltip is invalidated by its relayout (`0x530640`, ending in `0x7680e0(1)` at `0x530a52`),
|
||||
/// which only Show and the line writers run (`0x530aad`, `0x52ad59`, ..): a cleared plate keeps its
|
||||
/// rect through an unrelated resolve, and the next line's relayout resolves it without points.
|
||||
#[test]
|
||||
fn only_the_relayout_invalidates_the_tooltip() {
|
||||
let mut s = script();
|
||||
s.set_screen_size(800.0, 600.0);
|
||||
s.run(
|
||||
r#"
|
||||
local owner = CreateFrame("Button", "Slot")
|
||||
owner:SetPoint("TOPLEFT", 100, -100); owner:SetWidth(40); owner:SetHeight(40)
|
||||
local tt = CreateFrame("GameTooltip", "TT")
|
||||
tt:SetOwner(owner, "ANCHOR_RIGHT")
|
||||
tt:AddLine("Tough Jerky", 1, 1, 1)
|
||||
tt:Show()
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
s.resolve();
|
||||
measure_all(&mut s, &[("Tough Jerky", 80.0, 14.0)]);
|
||||
let left = |s: &mut UiScript| s.eval::<Option<f64>>("return TT:GetLeft()").unwrap();
|
||||
assert_eq!(left(&mut s), Some(140.0));
|
||||
s.run(
|
||||
r#"TT:ClearAllPoints()
|
||||
local other = CreateFrame("Frame"); other:SetWidth(5); other:SetHeight(5)
|
||||
other:SetPoint("CENTER", 0, 0)"#,
|
||||
)
|
||||
.unwrap();
|
||||
s.resolve();
|
||||
s.resolve();
|
||||
assert_eq!(left(&mut s), Some(140.0), "nothing ran the relayout");
|
||||
s.run(r#"TT:AddLine("Soulbound")"#).unwrap();
|
||||
s.resolve();
|
||||
assert_eq!(
|
||||
left(&mut s),
|
||||
None,
|
||||
"the relayout invalidated a plate with no points"
|
||||
);
|
||||
assert!(s.errors().is_empty(), "{:?}", s.errors());
|
||||
}
|
||||
|
||||
@@ -189,7 +189,8 @@ fn ensure_lines(lua: &Lua, this: &Table, n: usize) -> mlua::Result<()> {
|
||||
}]);
|
||||
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
|
||||
// A line row's points are set with the invalidate flag (`0x530197`).
|
||||
model.invalidate_region(left, true);
|
||||
}
|
||||
let left_id = model.region_id(left);
|
||||
|
||||
@@ -220,7 +221,7 @@ fn ensure_lines(lua: &Lua, this: &Table, n: usize) -> mlua::Result<()> {
|
||||
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
|
||||
model.invalidate_region(right, true);
|
||||
}
|
||||
let right_id = model.region_id(right);
|
||||
|
||||
@@ -377,6 +378,7 @@ pub(in crate::script) fn show_override(lua: &Lua, h: FrameHandle) {
|
||||
let live = tip_mut(&mut model, h).is_ok_and(|t| t.owner.is_some() && t.num_lines > 0);
|
||||
if live {
|
||||
full_alpha(&mut model, h);
|
||||
owe_relayout(&mut model, h);
|
||||
}
|
||||
live
|
||||
};
|
||||
@@ -465,11 +467,11 @@ fn apply_anchor(
|
||||
let owner_id = model.frame_id(owner);
|
||||
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.
|
||||
if !input.anchors.bits_eq(&new) {
|
||||
input.anchors = new;
|
||||
model.touch_layout_frame(h);
|
||||
}
|
||||
// No-op when unchanged, so the bag hover's per-frame SetOwner does not dirty the layout;
|
||||
// a real one is `SetPoint` with the invalidate flag (`0x52ff12`..`0x52ffb2`).
|
||||
let changed = !input.anchors.bits_eq(&new);
|
||||
input.anchors = new;
|
||||
model.invalidate_frame(h, changed);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -649,9 +651,9 @@ pub(super) fn append_line(
|
||||
if let Some(d) = model.region_data.get_mut(&lh) {
|
||||
if d.size != pin {
|
||||
d.size = pin;
|
||||
// A size-only write takes the named touch: a conservative one would re-derive the
|
||||
// whole layout graph on every hover that turns a line between wrapped and plain.
|
||||
model.touch_layout_region(lh);
|
||||
// `SetWidth`, which invalidates (`0x7683d0`), named: a conservative touch would
|
||||
// re-derive the whole layout graph on every hover that turns a line wrapped or plain.
|
||||
model.invalidate_region(lh, true);
|
||||
// The wrap pin is the measure key's wrap-width input.
|
||||
model.touch_measure(lh);
|
||||
}
|
||||
@@ -662,9 +664,21 @@ pub(super) fn append_line(
|
||||
if let Ok(t) = tip_mut(&mut model, h) {
|
||||
t.num_lines = n;
|
||||
}
|
||||
owe_relayout(&mut model, h);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// A line writer or the show ran the relayout (`0x530640`, from `0x52ad59`, `0x52b029`,
|
||||
/// `0x52e5ae`, `0x52f869`, `0x530615`, `0x530aad`), which does nothing without an owner
|
||||
/// (`0x530651`) and else ends in an unconditional invalidate (`0x530a52`).
|
||||
pub(super) fn owe_relayout(model: &mut Model, h: FrameHandle) {
|
||||
if let Ok(t) = tip_mut(model, h) {
|
||||
if t.owner.is_some() {
|
||||
t.relayout_owed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mod verbs;
|
||||
pub(super) use verbs::install;
|
||||
|
||||
@@ -708,9 +722,10 @@ fn cursor_anchor(model: &mut Model, h: FrameHandle) {
|
||||
);
|
||||
let units = model.layout_units();
|
||||
let input = model.layout_inputs.entry(h).or_default();
|
||||
// `0x767c70`'s change detect: the BOTTOM slot is replaced only on a real change.
|
||||
// `0x767c70`'s change detect: the BOTTOM slot is replaced, and the plate invalidated
|
||||
// (`0x530b97`, flag 1), only on a real change.
|
||||
if input.anchors.set(new, units) {
|
||||
model.touch_layout_frame(h);
|
||||
model.invalidate_frame(h, true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -733,6 +748,16 @@ pub(super) fn layout_tooltips(model: &mut Model) {
|
||||
if cursor_mode {
|
||||
cursor_anchor(model, h);
|
||||
}
|
||||
// The relayout's closing invalidate (`0x530a52`), whatever it computed. Its own writes
|
||||
// below push when they move the plate; past those, a re-resolve gives back the rect the
|
||||
// plate holds unless that rect is stale, so only then is the invalidate paid here.
|
||||
let owed = tip_mut(model, h).is_ok_and(|t| std::mem::take(&mut t.relayout_owed));
|
||||
if owed && model.frame_holds_stale(h) {
|
||||
model.note_layout_frame_write(h);
|
||||
if let Some(&id) = model.frame_to_id.get(&h) {
|
||||
model.layout_pending.push(id);
|
||||
}
|
||||
}
|
||||
let (num, lefts, rights, min_w, pad_w) = match model.arena.frame(h).map(|f| &f.kind_state) {
|
||||
Some(KindState::Tooltip(t)) => (
|
||||
t.num_lines,
|
||||
@@ -783,6 +808,8 @@ pub(super) fn layout_tooltips(model: &mut Model) {
|
||||
// that these inputs changed. The size goes through `SetWidth`/`SetHeight` (`0x530a2e`,
|
||||
// `0x530a39`), which invalidate the tooltip.
|
||||
model.note_layout_frame_write(h);
|
||||
// A size the measures moved since the relayout ran: benilla's measures land a tick after
|
||||
// the line write, where the reference's relayout measured in the call.
|
||||
if moved {
|
||||
if let Some(&id) = model.frame_to_id.get(&h) {
|
||||
model.layout_pending.push(id);
|
||||
@@ -805,6 +832,9 @@ pub(super) fn layout_tooltips(model: &mut Model) {
|
||||
.is_some_and(|d| d.anchors.set(new, units));
|
||||
if stored {
|
||||
model.note_layout_region_write(rights[i]);
|
||||
if let Some(&id) = model.region_to_id.get(&rights[i]) {
|
||||
model.layout_pending.push(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -151,6 +151,7 @@ pub(in crate::script) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
None => (String::new(), [1.0, 1.0, 1.0, 1.0]),
|
||||
};
|
||||
write_cell(&mut model, lh, &(current + &extra), color);
|
||||
super::owe_relayout(&mut model, h);
|
||||
Ok(())
|
||||
})?,
|
||||
)?;
|
||||
@@ -242,8 +243,9 @@ pub(in crate::script) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
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);
|
||||
// Two line rows, their points set with the invalidate flag.
|
||||
model.invalidate_region(lh, true);
|
||||
model.invalidate_region(rh, true);
|
||||
let t = tip_mut(&mut model, h)?;
|
||||
t.left_lines.push(lh);
|
||||
t.right_lines.push(rh);
|
||||
|
||||
@@ -281,6 +281,9 @@ pub struct TooltipState {
|
||||
pub padding: f32,
|
||||
/// Line 1 came from XML (`ShoppingTooltipTemplate`), so later lines clone the previous fonts.
|
||||
pub xml_declared_lines: bool,
|
||||
/// A relayout (`0x530640`) is owed: Show and the line writers run it with an owner, and it
|
||||
/// ends by invalidating the plate (`0x530a52`). The layout pre-pass pays it.
|
||||
pub relayout_owed: bool,
|
||||
}
|
||||
|
||||
/// The plate's text inset: the stock template seats `TextLeft1` at TOPLEFT (10, −10).
|
||||
|
||||
@@ -724,7 +724,7 @@ impl WidgetArena {
|
||||
Some(d)
|
||||
}
|
||||
|
||||
/// Free one region, as `CSimpleHTML::SetText` pool-frees the last parse's blocks (`0x78adc6`)
|
||||
/// Free one region, as `CSimpleHTML::SetText` deletes the last parse's blocks (`0x78a580`)
|
||||
/// and a backdrop's teardown its nine pieces (`0x77f4d0`). The caller drops the region's
|
||||
/// script-model state too, and never frees a region some state holds by handle.
|
||||
pub fn destroy_region(&mut self, h: RegionHandle) -> bool {
|
||||
|
||||
Reference in New Issue
Block a user