The tick drains the layout around the walk; size gates in layout units (#922)
The tick ran the OnUpdate walk without resolving the layout around it, so a resize made in an event handler fired `OnSizeChanged` after the walk, and some walk slots resolved on their own to compensate. Separately, `OnSizeChanged` gated on 2⁻²² in benilla's units, and an emptied FontString kept its old measured box in the incremental layout. In the reference, the pump drains the layout queue before the walk and again after it, each drain firing `OnSizeChanged`, then re-picks the hover. ApplyRect compares sizes against 2⁻²² in its own units, where the screen diagonal is 1. The tick now drains before and after the walk and drops the stand-in resolves. Size gates scale 2⁻²² by the layout unit, and a FontString's text going empty or back names it on the layout when its node changes. Tests pin each. Fixes #912 Fixes #741 Fixes #914 Fixes #911 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
935b465c76
commit
ff3d15b132
@@ -512,7 +512,8 @@ pub(super) fn tick_script(
|
||||
let us_tick = lap();
|
||||
// ── The FontString measure round-trip, before the resolve ────────────────────────────────
|
||||
// A request reads only region data, never a resolved rect, and `resolve` writes no region's
|
||||
// size, so measuring first lets the frame's one resolve see the answers.
|
||||
// size, so measuring first lets the resolve below see the answers. The tick drained the layout
|
||||
// after its walk; this resolve takes the measures and what the tick's tail moved.
|
||||
let mut measured_any = false;
|
||||
if let Some(atlas) = font_atlas.as_deref_mut() {
|
||||
measured_any = measure_fontstrings(&mut script, atlas, s, &mut ui_cost, ui_cost_wanted.0);
|
||||
|
||||
@@ -16,7 +16,9 @@ use std::collections::VecDeque;
|
||||
const UNSET: f64 = f64::INFINITY;
|
||||
|
||||
/// The least width or height change that fires `OnSizeChanged`: 2^-22, bits `0x34800000` at
|
||||
/// `0x8029d4`, tested in `ApplyRect` (`0x76b580`).
|
||||
/// `0x8029d4`, tested in `ApplyRect` (`0x76b5e7`, `0x76b602`) in the reference's layout units, the
|
||||
/// screen diagonal as 1; in benilla's units that is this times the layout unit
|
||||
/// (`Model::size_eps`).
|
||||
pub const SIZE_EPS: f64 = 2.384_185_791_015_625e-7;
|
||||
|
||||
/// The least edge move against the cached rect for the client to re-store it and `ApplyRect`, 1e-5
|
||||
@@ -336,14 +338,14 @@ pub fn assemble_rect(edges: [f32; 4], clamp: bool, extent_x: f32, extent_y: f32)
|
||||
}
|
||||
}
|
||||
|
||||
/// `ApplyRect`'s `OnSizeChanged` test (`0x76b580`): width or height moved by at least
|
||||
/// [`SIZE_EPS`], the deltas taken in `f64` from the `f32` edges.
|
||||
pub fn size_changed(old: Rect, new: Rect) -> bool {
|
||||
/// `ApplyRect`'s `OnSizeChanged` test (`0x76b580`): width or height moved by at least `eps`,
|
||||
/// [`SIZE_EPS`] in the units the rects are in, the deltas taken in `f64` from the `f32` edges.
|
||||
pub fn size_changed(old: Rect, new: Rect, eps: f64) -> bool {
|
||||
let dw =
|
||||
(f64::from(new.right) - f64::from(new.left)) - (f64::from(old.right) - f64::from(old.left));
|
||||
let dh =
|
||||
(f64::from(new.top) - f64::from(new.bottom)) - (f64::from(old.top) - f64::from(old.bottom));
|
||||
dw.abs() >= SIZE_EPS || dh.abs() >= SIZE_EPS
|
||||
dw.abs() >= eps || dh.abs() >= eps
|
||||
}
|
||||
|
||||
// ── The composed resolver: `assemble` over six recursion-guarded edge resolvers ──────────────
|
||||
@@ -1187,9 +1189,12 @@ mod tests {
|
||||
let r = Rect::new(0.0, 0.0, 40.0, 100.0);
|
||||
// 1e-7 is under SIZE_EPS.
|
||||
let tiny = Rect::new(0.0, 0.0, 40.0, 100.0 + 1e-7);
|
||||
assert!(!size_changed(r, tiny));
|
||||
assert!(!size_changed(r, tiny, SIZE_EPS));
|
||||
let big = Rect::new(0.0, 0.0, 40.0, 100.001);
|
||||
assert!(size_changed(r, big));
|
||||
assert!(size_changed(r, big, SIZE_EPS));
|
||||
// In units of 1280 to the reference's one, 1e-4 is under the threshold.
|
||||
let jitter = Rect::new(0.0, 0.0, 40.0, 100.0001);
|
||||
assert!(!size_changed(r, jitter, SIZE_EPS * 1280.0));
|
||||
}
|
||||
|
||||
/// The buffers live across rounds, so `begin` must leave nothing behind: round 2 anchors to a
|
||||
|
||||
@@ -490,6 +490,7 @@ pub(crate) fn set_color(model: &mut Model, fh: FrameHandle, border: bool, color:
|
||||
/// After a resolve: re-run the texcoords of every backdrop whose frame rect changed size, as the
|
||||
/// rect application `0x76b580` calls `0x77f0c0` (at `0x76b61e`); a move alone re-runs nothing.
|
||||
pub(crate) fn after_resolve(model: &mut Model) {
|
||||
let eps = model.size_eps();
|
||||
let Model {
|
||||
backdrops,
|
||||
resolved,
|
||||
@@ -503,7 +504,7 @@ pub(crate) fn after_resolve(model: &mut Model) {
|
||||
};
|
||||
if inst
|
||||
.ran_for
|
||||
.is_some_and(|prev| !layout::size_changed(prev, rect))
|
||||
.is_some_and(|prev| !layout::size_changed(prev, rect, eps))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -506,8 +506,11 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
let id = ensure_slot(lua, &this, Slot::Text)?;
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
|
||||
let rh = *model.id_to_region.get(&id).expect("text region id");
|
||||
model.region_data.entry(rh).or_default().text = Some(text);
|
||||
let data = model.region_data.entry(rh).or_default();
|
||||
let was_shown = data.text.as_deref().is_some_and(|t| !t.is_empty());
|
||||
data.text = Some(text);
|
||||
model.touch_measure(rh);
|
||||
model.touch_layout_text(rh, was_shown);
|
||||
Ok(())
|
||||
})?,
|
||||
)?;
|
||||
|
||||
@@ -376,8 +376,11 @@ fn sync_text_region(lua: &Lua, h: FrameHandle) {
|
||||
});
|
||||
if let Some(display) = display {
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
|
||||
model.region_data.entry(rh).or_default().text = Some(display);
|
||||
let data = model.region_data.entry(rh).or_default();
|
||||
let was_shown = data.text.as_deref().is_some_and(|t| !t.is_empty());
|
||||
data.text = Some(display);
|
||||
model.touch_measure(rh);
|
||||
model.touch_layout_text(rh, was_shown);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -471,6 +471,9 @@ fn up_and_down_move_a_multi_line_boxs_caret_by_rows() {
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
// The flush sizes the box to its two rows; the drain after the walk resizes it, and the
|
||||
// re-seat that runs fires once more at the next flush (`0x77a8d0` → `0x77b8c0`).
|
||||
s.tick(0.016);
|
||||
s.tick(0.016);
|
||||
take_log(&s);
|
||||
let row = |back| EditAction::Move {
|
||||
@@ -568,3 +571,30 @@ fn on_cursor_changed_answers_in_the_boxs_own_units() {
|
||||
);
|
||||
assert!(s.errors().is_empty(), "{:?}", s.errors());
|
||||
}
|
||||
|
||||
/// A box created and anchored between frames, as an event handler does: the pump's drain before
|
||||
/// the walk (`0x7656ba`) gives it its first rect, whose resize runs the box's `OnSizeChanged`
|
||||
/// override (`0x76b63e` → `0x77a8d0`); the re-seat ORs its bits into the ctor's (`0x77ba7f`), so the
|
||||
/// walk's flush (`0x77a7a1`) fires `OnTextChanged` once, and the frames after it fire nothing.
|
||||
#[test]
|
||||
fn a_box_anchored_between_frames_fires_on_text_changed_once() {
|
||||
let mut s = script();
|
||||
s.run(
|
||||
r#"
|
||||
N = 0
|
||||
local e = CreateFrame("EditBox", "Fresh")
|
||||
e:SetAutoFocus(false)
|
||||
e:SetPoint("BOTTOMLEFT", 100, 50)
|
||||
e:SetWidth(200); e:SetHeight(32)
|
||||
e:SetScript("OnTextChanged", function() N = N + 1 end)
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
// The host's frame: the tick, then its own resolve for the draw.
|
||||
for _ in 0..3 {
|
||||
s.tick(0.016);
|
||||
s.resolve();
|
||||
}
|
||||
assert_eq!(s.eval::<i64>("return N").unwrap(), 1);
|
||||
assert!(s.errors().is_empty(), "{:?}", s.errors());
|
||||
}
|
||||
|
||||
@@ -74,6 +74,7 @@ pub(super) fn seat_text_region(model: &mut Model, h: FrameHandle) {
|
||||
/// a layout input, for the caller to resolve it in the same pass, as the reference's drain does.
|
||||
pub(in crate::script) fn reseat_resized(model: &mut Model) -> bool {
|
||||
let epoch = model.layout_epoch;
|
||||
let eps = model.size_eps();
|
||||
for i in 0..model.arena.editbox_kinds().len() {
|
||||
let h = model.arena.editbox_kinds()[i];
|
||||
let Some(&now) = model.resolved.get(&h) else {
|
||||
@@ -87,7 +88,7 @@ pub(in crate::script) fn reseat_resized(model: &mut Model) -> bool {
|
||||
.notified_rect
|
||||
.replace(now)
|
||||
.unwrap_or(Rect::new(0.0, 0.0, 0.0, 0.0));
|
||||
if !crate::layout::size_changed(before, now) {
|
||||
if !crate::layout::size_changed(before, now, eps) {
|
||||
continue;
|
||||
}
|
||||
// The ctor built the text region; its data is seeded here if nothing has touched it.
|
||||
|
||||
@@ -97,8 +97,18 @@ fn frame_node_hash(input: &LayoutInput, over: Option<Anchor>) -> u64 {
|
||||
node.finish()
|
||||
}
|
||||
|
||||
/// A region's per-node input hash: its anchors, explicit size, measured text extent and owner,
|
||||
/// whose effective scale the sweep reads (`None` for an owner with no layout id).
|
||||
/// The measured extent the region sweep sizes a FontString by: the stored measure while the text
|
||||
/// is non-empty, used without a key check, so changed text keeps its last box while the
|
||||
/// re-measure is in flight; empty text is never measured, so it has none, never a stale box.
|
||||
#[inline]
|
||||
pub(super) fn effective_measure(data: &RegionData) -> Option<super::MeasuredText> {
|
||||
data.measured
|
||||
.filter(|_| data.text.as_deref().is_some_and(|t| !t.is_empty()))
|
||||
}
|
||||
|
||||
/// 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.
|
||||
#[inline]
|
||||
pub(super) fn region_node_hash(data: &RegionData, owner: Option<u32>) -> u64 {
|
||||
let mut node = InputFingerprint::default();
|
||||
@@ -111,16 +121,17 @@ pub(super) fn region_node_hash(data: &RegionData, owner: Option<u32>) -> u64 {
|
||||
}
|
||||
None => node.feed(u64::MAX),
|
||||
}
|
||||
match data.measured {
|
||||
let unsized_axis = data.size.is_none_or(|(w, h)| w == 0.0 || h == 0.0);
|
||||
match effective_measure(data).filter(|_| unsized_axis) {
|
||||
Some(m) => {
|
||||
node.f32(m.w);
|
||||
node.f32(m.h);
|
||||
}
|
||||
None => node.feed(u64::MAX),
|
||||
}
|
||||
// The art, only where an axis authored 0 takes its span from it (`content_span`): a new
|
||||
// texture moves such a region, while a sized region's art cannot move its rect.
|
||||
if !data.anchors.is_empty() && data.size.is_none_or(|(w, h)| w == 0.0 || h == 0.0) {
|
||||
// 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) => {
|
||||
@@ -752,13 +763,14 @@ impl PreambleProf {
|
||||
}
|
||||
|
||||
/// `OnSizeChanged`'s "after": queue `(id, width, height)` for each watched frame whose size moved
|
||||
/// by the `ApplyRect 0x76b580` test ([`crate::layout::size_changed`], `ε = _DAT_008029d4`), so a
|
||||
/// by the `ApplyRect 0x76b580` test ([`crate::layout::size_changed`] at [`Model::size_eps`]), so a
|
||||
/// pure move never fires. A frame with no rect at entry compares against the zero rect
|
||||
/// `CSimpleFrame`'s ctor leaves; one that loses its rect had no `ApplyRect` and fires nothing.
|
||||
fn queue_size_changes(
|
||||
watched: &[(FrameHandle, Option<Rect>)],
|
||||
resolved: &HashMap<FrameHandle, Rect>,
|
||||
frame_to_id: &HashMap<FrameHandle, u32>,
|
||||
eps: f64,
|
||||
out: &mut Vec<(u32, f32, f32)>,
|
||||
) {
|
||||
for &(h, before) in watched {
|
||||
@@ -766,7 +778,7 @@ fn queue_size_changes(
|
||||
continue;
|
||||
};
|
||||
let before = before.unwrap_or(Rect::new(0.0, 0.0, 0.0, 0.0));
|
||||
if !layout::size_changed(before, now) {
|
||||
if !layout::size_changed(before, now, eps) {
|
||||
continue;
|
||||
}
|
||||
let Some(&id) = frame_to_id.get(&h) else {
|
||||
@@ -860,6 +872,13 @@ impl UiScript {
|
||||
}
|
||||
|
||||
fn resolve_layout_inner(model: &mut Model) {
|
||||
// Text that emptied or filled since the last resolve: named now where its node's hash
|
||||
// moved, ahead of the epoch's read, as if named at the write.
|
||||
if !model.text_flips.is_empty() {
|
||||
for rh in std::mem::take(&mut model.text_flips) {
|
||||
model.touch_layout_content(rh);
|
||||
}
|
||||
}
|
||||
// ── Tier 1: the mutation epoch ────────────────────────────────────────────────────────
|
||||
// No layout touch since the last converged resolve: nothing the fingerprint reads moved,
|
||||
// so skip everything. Under verify, fall through so tier 2 checks the claim.
|
||||
@@ -884,6 +903,7 @@ impl UiScript {
|
||||
.iter()
|
||||
.map(|&h| (h, model.resolved.get(&h).copied()))
|
||||
.collect();
|
||||
let size_eps = model.size_eps();
|
||||
pre.watched = pre.lap();
|
||||
// ── The graph: the ledger's cache, or a fresh derivation ─────────────────────────────
|
||||
// A `Some` ledger claims every write since the graph was built named its node and kept
|
||||
@@ -1113,12 +1133,8 @@ impl UiScript {
|
||||
// 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
|
||||
// reference sizes a zero width to the unwrapped line. The measure is used without
|
||||
// a key check, so changed text keeps its last box while the re-measure is in
|
||||
// flight; empty text is never measured, so it counts as zero, never a stale box.
|
||||
let measured = data
|
||||
.measured
|
||||
.filter(|_| data.text.as_deref().is_some_and(|t| !t.is_empty()));
|
||||
// reference sizes a zero width to the unwrapped line.
|
||||
let measured = effective_measure(data);
|
||||
let mut height = data.size.map_or(0.0, |s| s.1);
|
||||
let mut width = data.size.map_or(0.0, |s| s.0);
|
||||
if let Some(m) = &measured {
|
||||
@@ -1239,7 +1255,13 @@ impl UiScript {
|
||||
// so a touch during this pass (the tooltip pre-pass, a minted id) re-opens it. The
|
||||
// fingerprint hashes inputs only, so the next unmutated resolve would match it.
|
||||
*layout_epoch_resolved = Some(epoch_at_entry);
|
||||
queue_size_changes(&watched, resolved, frame_to_id, pending_size_changed);
|
||||
queue_size_changes(
|
||||
&watched,
|
||||
resolved,
|
||||
frame_to_id,
|
||||
size_eps,
|
||||
pending_size_changed,
|
||||
);
|
||||
return;
|
||||
}
|
||||
if round + 1 == round_cap {
|
||||
@@ -1256,7 +1278,13 @@ impl UiScript {
|
||||
// The cycle bail: its rects are this frame's, so their size changes still fire, and the
|
||||
// scope forgets a graph that never settled.
|
||||
scope.invalidate();
|
||||
queue_size_changes(&watched, resolved, frame_to_id, pending_size_changed);
|
||||
queue_size_changes(
|
||||
&watched,
|
||||
resolved,
|
||||
frame_to_id,
|
||||
size_eps,
|
||||
pending_size_changed,
|
||||
);
|
||||
}
|
||||
|
||||
/// Derive the layout graph from scratch: the roster, the reverse edges, the per-node hashes and
|
||||
|
||||
@@ -886,7 +886,8 @@ impl UiScript {
|
||||
.map(|(_, f)| f.effective_alpha)
|
||||
}
|
||||
|
||||
/// Resolve and cache every frame's rect, which `GetWidth`/`GetHeight`/`extract` read, then fire
|
||||
/// The layout drain (`0x768ed0`), which [`Self::tick`] runs before and after its walk: resolve
|
||||
/// and cache every frame's rect, which `GetWidth`/`GetHeight`/`extract` read, then fire
|
||||
/// `OnSizeChanged` for frames whose size moved ([`event::fire_size_changes`]) and re-seat the
|
||||
/// text of every resized EditBox.
|
||||
pub fn resolve(&mut self) {
|
||||
|
||||
@@ -227,6 +227,9 @@ pub(crate) struct Model {
|
||||
|
||||
/// Frames the last resolve resized, `(id, width, height)`, owed `OnSizeChanged` outside it.
|
||||
pub(crate) pending_size_changed: Vec<(u32, f32, f32)>,
|
||||
/// Regions whose text emptied or filled since the last resolve, which names each one whose
|
||||
/// node hash moved ([`Model::touch_layout_text`]).
|
||||
pub(crate) text_flips: Vec<RegionHandle>,
|
||||
|
||||
/// Script errors caught from `pcall`'d handlers, drained by the host each frame.
|
||||
pub(crate) errors: Vec<String>,
|
||||
@@ -1122,6 +1125,7 @@ impl Model {
|
||||
mouse_capture: None,
|
||||
last_click: HashMap::new(),
|
||||
pending_size_changed: Vec::new(),
|
||||
text_flips: Vec::new(),
|
||||
errors: Vec::new(),
|
||||
pending_error_dispatch: Vec::new(),
|
||||
diagnostics: Default::default(),
|
||||
@@ -1579,10 +1583,38 @@ impl Model {
|
||||
/// 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
|
||||
/// next resolve names it if it moved the node's hash. Checked there, not here, so a clear and
|
||||
/// rebuild of the same text (the hover tooltip's, every frame) leaves the epoch alone. A
|
||||
/// same-side write leaves the layout to the measure ledger.
|
||||
pub(crate) fn touch_layout_text(&mut self, rh: RegionHandle, was_shown: bool) {
|
||||
let shown = self
|
||||
.region_data
|
||||
.get(&rh)
|
||||
.is_some_and(|d| d.text.as_deref().is_some_and(|t| !t.is_empty()));
|
||||
if shown != was_shown {
|
||||
self.text_flips.push(rh);
|
||||
}
|
||||
}
|
||||
|
||||
/// Names anchored region `rh` unless its node still holds its converged hash.
|
||||
pub(crate) fn touch_layout_content(&mut self, rh: RegionHandle) {
|
||||
let Some(d) = self.region_data.get(&rh) else {
|
||||
return;
|
||||
};
|
||||
if d.anchors.is_empty() || d.size.is_some_and(|(w, h)| w != 0.0 && h != 0.0) {
|
||||
if d.anchors.is_empty() {
|
||||
return;
|
||||
}
|
||||
if let Some(&id) = self.region_to_id.get(&rh) {
|
||||
|
||||
@@ -35,6 +35,12 @@ impl Model {
|
||||
768.0 * (a * a + 1.0).sqrt()
|
||||
}
|
||||
|
||||
/// `ApplyRect`'s size threshold, 2⁻²² in the reference's layout units (`0x76b5e7`,
|
||||
/// `0x76b602`, `[0x8029d4]`), in ours: [`crate::layout::SIZE_EPS`] times the layout unit.
|
||||
pub(crate) fn size_eps(&self) -> f64 {
|
||||
crate::layout::SIZE_EPS * f64::from(self.layout_unit())
|
||||
}
|
||||
|
||||
/// The implicit rect: a pane with no authored size takes its file's bounding-box extent, in
|
||||
/// layout units, as the reference's geometry overrides answer it (`0x76d080`, `0x76d0d0`).
|
||||
pub(crate) fn apply_implicit_rect(&mut self, h: FrameHandle) {
|
||||
|
||||
@@ -236,8 +236,9 @@ pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
|
||||
/// Resolve the layout now if anything moved, as the reference answers a geometry query against
|
||||
/// current layout; a settled tree costs one epoch compare. Deviation: `OnSizeChanged` fires at the
|
||||
/// next `UiScript::resolve`, a tick after the reference's (`ApplyRect 0x76b580`), because running
|
||||
/// Lua handlers inside a binding risks a borrow panic or unbounded recursion.
|
||||
/// next layout drain (`UiScript::resolve`), not inside the getter as the reference's forced
|
||||
/// `ApplyRect 0x76b580` does, because running Lua handlers inside a binding risks a borrow panic
|
||||
/// or unbounded recursion.
|
||||
fn settle(lua: &Lua) {
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model");
|
||||
crate::script::UiScript::resolve_layout(&mut model);
|
||||
|
||||
@@ -18,12 +18,14 @@ pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
let rh = region_handle_of(lua, &this)?;
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model");
|
||||
let data = model.region_data.entry(rh).or_default();
|
||||
let was_shown = data.text.as_deref().is_some_and(|t| !t.is_empty());
|
||||
data.text = text;
|
||||
// Fresh text draws whole: an armed write-on gradient belongs to the old string.
|
||||
data.alpha_gradient = None;
|
||||
// SetText must not touch the layout, since the extent has not moved yet; it names
|
||||
// itself on the measure ledger.
|
||||
// New text names itself on the measure ledger, and on the layout only when it empties
|
||||
// or fills, which drops or takes up the measure the sweep reads.
|
||||
model.touch_measure(rh);
|
||||
model.touch_layout_text(rh, was_shown);
|
||||
Ok(())
|
||||
})?,
|
||||
)?;
|
||||
|
||||
@@ -113,29 +113,6 @@ fn subtree_span(model: &Model, f: FrameHandle, axis: Axis) -> Option<(f32, f32)>
|
||||
span
|
||||
}
|
||||
|
||||
/// Every FontString under `f` (its own regions, then its visible children's), in tree order.
|
||||
fn subtree_font_strings(model: &Model, f: FrameHandle) -> Vec<crate::widget::RegionHandle> {
|
||||
let mut out = Vec::new();
|
||||
let Some(frame) = model.arena.frame(f) else {
|
||||
return out;
|
||||
};
|
||||
for &rh in &frame.regions {
|
||||
if model
|
||||
.arena
|
||||
.region(rh)
|
||||
.is_some_and(|r| matches!(r.kind, crate::widget::RegionKind::FontString))
|
||||
{
|
||||
out.push(rh);
|
||||
}
|
||||
}
|
||||
for &ch in &frame.children {
|
||||
if model.arena.frame(ch).is_some_and(|c| c.effective_visible) {
|
||||
out.extend(subtree_font_strings(model, ch));
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
let m = lua.create_table()?;
|
||||
|
||||
@@ -302,59 +279,31 @@ fn fire_scroll(lua: &Lua, this: &Table, script: &str, value: f32) -> mlua::Resul
|
||||
/// in the reference's units (the screen diagonal as 1) (`0x786ed7`–`0x786efd`), which is that times the
|
||||
/// layout unit in ours.
|
||||
///
|
||||
/// The reference drains its layout before the walk (`0x7656ba`) and measures text at once; ours
|
||||
/// lands both a frame later, so a marked frame measures its child's FontStrings and resolves first
|
||||
/// (`QuestLog_UpdateQuestDetails` marks right after its `SetText`s, `QuestLogFrame.lua:458`). An
|
||||
/// unmarked frame reads its size as the last resolve left it, so a resize not yet resolved marks
|
||||
/// it at the update after that resolve.
|
||||
/// The rects it reads are the ones the pump's drain before the walk (`0x7656ba`) resolved: the
|
||||
/// recompute reads them through `0x768320`, which never resolves, so a resize made earlier in the
|
||||
/// walk counts from the next tick, and the size it notes is the one that drain applied.
|
||||
pub(super) fn update(lua: &Lua, h: FrameHandle) {
|
||||
let marked = {
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
|
||||
let rect = model.resolved.get(&h).copied();
|
||||
let Some(s) = scroll_state(&mut model, h) else {
|
||||
return;
|
||||
};
|
||||
see_size(s, rect);
|
||||
s.range_dirty
|
||||
};
|
||||
if !marked {
|
||||
return;
|
||||
}
|
||||
let strings = {
|
||||
let model = lua.app_data_ref::<Model>().expect("model app_data");
|
||||
match model.arena.frame(h).map(|f| &f.kind_state) {
|
||||
Some(KindState::Scroll(s)) => s
|
||||
.child
|
||||
.map(|c| subtree_font_strings(&model, c))
|
||||
.unwrap_or_default(),
|
||||
_ => Vec::new(),
|
||||
}
|
||||
};
|
||||
for rh in strings {
|
||||
super::measure::ensure_measured(lua, rh);
|
||||
}
|
||||
super::UiScript::resolve_layout(&mut lua.app_data_mut::<Model>().expect("model app_data"));
|
||||
// The reference fires `OnSizeChanged` from its layout drain, ahead of the walk.
|
||||
event::fire_size_changes(lua);
|
||||
let notify = {
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
|
||||
let rect = model.resolved.get(&h).copied();
|
||||
let has_child = matches!(
|
||||
model.arena.frame(h).map(|f| &f.kind_state),
|
||||
Some(KindState::Scroll(s)) if s.child.is_some()
|
||||
);
|
||||
let eps = model.size_eps();
|
||||
let Some(s) = scroll_state(&mut model, h) else {
|
||||
return;
|
||||
};
|
||||
see_size(s, rect, eps);
|
||||
if !s.range_dirty {
|
||||
return;
|
||||
}
|
||||
let has_child = s.child.is_some();
|
||||
let measured = (rect.is_some() && has_child).then(|| {
|
||||
[
|
||||
scroll_range(&model, h, Axis::Horizontal),
|
||||
scroll_range(&model, h, Axis::Vertical),
|
||||
]
|
||||
});
|
||||
let eps = crate::layout::SIZE_EPS * f64::from(model.layout_unit());
|
||||
let Some(s) = scroll_state(&mut model, h) else {
|
||||
return;
|
||||
};
|
||||
// The resolve may have moved the frame: that is the size this update has seen.
|
||||
see_size(s, rect);
|
||||
let moved = measured.filter(|new| {
|
||||
let old = std::mem::replace(&mut s.range, *new);
|
||||
(0..2).any(|i| (f64::from(new[i]) - f64::from(old[i])).abs() >= eps)
|
||||
@@ -397,13 +346,12 @@ fn scroll_state(model: &mut Model, h: FrameHandle) -> Option<&mut ScrollFrameSta
|
||||
}
|
||||
|
||||
/// Note the frame's resolved size; a change since the last note marks the range, by the width or
|
||||
/// height move that fires `OnSizeChanged` ([`crate::layout::size_changed`]). No rect, no note.
|
||||
fn see_size(s: &mut ScrollFrameState, rect: Option<crate::layout::Rect>) {
|
||||
/// height move that fires `OnSizeChanged` ([`crate::layout::size_changed`] at `eps`,
|
||||
/// [`Model::size_eps`]). No rect, no note.
|
||||
fn see_size(s: &mut ScrollFrameState, rect: Option<crate::layout::Rect>, eps: f64) {
|
||||
let Some(r) = rect else { return };
|
||||
let size = [r.width(), r.height()];
|
||||
let moved = |i: usize| {
|
||||
(f64::from(size[i]) - f64::from(s.seen_size[i])).abs() >= crate::layout::SIZE_EPS
|
||||
};
|
||||
let moved = |i: usize| (f64::from(size[i]) - f64::from(s.seen_size[i])).abs() >= eps;
|
||||
if moved(0) || moved(1) {
|
||||
s.range_dirty = true;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,17 @@
|
||||
//! every resolve must equal a from-scratch solve of the same model, rects and graph alike.
|
||||
|
||||
use super::common::script;
|
||||
use crate::script::UiScript;
|
||||
use crate::script::{MeasureRequest, TextMeasure, UiScript};
|
||||
|
||||
/// A stand-in font engine, so a text-sized FontString takes a box: 6 wide per character, 12 tall.
|
||||
struct Block;
|
||||
|
||||
impl TextMeasure for Block {
|
||||
fn measure(&mut self, req: &MeasureRequest) -> (f32, f32, f32) {
|
||||
let w = req.text.chars().count() as f32 * 6.0;
|
||||
(w, 12.0, w)
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolve, then require the cached graph and its rects to equal a from-scratch solve.
|
||||
fn resolve_matches_scratch(s: &mut UiScript, what: &str) {
|
||||
@@ -133,7 +143,7 @@ fn step(rng: &mut Rng) -> Write {
|
||||
let k2 = rng.next() % 1000;
|
||||
let w = if rng.coin(4) { 0 } else { rng.pick(60) };
|
||||
let h = if rng.coin(4) { 0 } else { rng.pick(60) };
|
||||
let lua = match rng.next() % 20 {
|
||||
let lua = match rng.next() % 21 {
|
||||
// A frame born under any frame, anchored to any node it may read.
|
||||
0 | 1 => {
|
||||
let kind = if rng.coin(3) { "Button" } else { "Frame" };
|
||||
@@ -271,6 +281,15 @@ fn step(rng: &mut Rng) -> Write {
|
||||
),
|
||||
// The host's UI scale, `UIParent`'s whole subtree.
|
||||
18 => return Write::UiScale([0.64, 0.8, 1.0][(k % 3) as usize]),
|
||||
// A font string's text set, emptied or restored: its measured box comes and goes.
|
||||
20 => format!(
|
||||
"local n = table.getn(R)
|
||||
if n > 0 then
|
||||
local t = R[math.mod({k}, n) + 1]
|
||||
if t.SetText then t:SetText(({{ '', 'hello', 'a longer line', nil }})[{}]) end
|
||||
end",
|
||||
k2 % 4 + 1
|
||||
),
|
||||
// Two backdrops torn down or built in one pass.
|
||||
_ => format!(
|
||||
"local n = table.getn(F)
|
||||
@@ -282,9 +301,9 @@ fn step(rng: &mut Rng) -> Write {
|
||||
Write::Lua(lua)
|
||||
}
|
||||
|
||||
/// A random run of births, frees, re-parents, re-owns, retargets, scrolls and scale changes, in
|
||||
/// batches of one to four writes per resolve: after every resolve the cached graph and its rects
|
||||
/// equal a from-scratch solve, and none of them derived the graph.
|
||||
/// A random run of births, frees, re-parents, re-owns, retargets, scrolls, scale changes and text
|
||||
/// set, emptied and restored, in batches of one to four writes per resolve: after every resolve
|
||||
/// the cached graph and its rects equal a from-scratch solve, and none of them derived the graph.
|
||||
#[test]
|
||||
fn random_structural_writes_resolve_as_a_scratch_solve_would() {
|
||||
for seed in [
|
||||
@@ -295,6 +314,7 @@ fn random_structural_writes_resolve_as_a_scratch_solve_would() {
|
||||
] {
|
||||
let mut s = script();
|
||||
s.set_screen_size(800.0, 600.0);
|
||||
s.set_text_measurer(Box::new(Block));
|
||||
s.run(SETUP).expect("setup");
|
||||
s.run("raised = 0").expect("counter");
|
||||
s.resolve();
|
||||
|
||||
@@ -514,3 +514,59 @@ fn a_settled_region_is_refound_after_each_measure_input_write() {
|
||||
assert_eq!(reqs[0].text, "Go");
|
||||
assert!(s.errors().is_empty(), "{:?}", s.errors());
|
||||
}
|
||||
|
||||
/// A text-sized FontString spans its measure while it has text and the one-unit floor once its
|
||||
/// text is empty (`0x772930`); emptying and restoring the text moves its box both ways, and each
|
||||
/// resolve equals a from-scratch solve.
|
||||
#[test]
|
||||
fn an_emptied_fontstring_drops_to_the_floor_and_a_restored_one_regains_its_box() {
|
||||
let mut s = script();
|
||||
s.set_screen_size(800.0, 600.0);
|
||||
s.set_text_measurer(Box::new(BlockFont));
|
||||
s.run(
|
||||
r#"
|
||||
local f = CreateFrame("Frame", "Host")
|
||||
f:SetPoint("BOTTOMLEFT", 0, 0); f:SetWidth(400); f:SetHeight(300)
|
||||
Label = f:CreateFontString("Label", "ARTWORK")
|
||||
Label:SetPoint("BOTTOMLEFT", 10, 10)
|
||||
Tail = CreateFrame("Frame", "Tail")
|
||||
Tail:SetPoint("LEFT", Label, "RIGHT", 0, 0); Tail:SetWidth(5); Tail:SetHeight(5)
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
let step = |s: &mut UiScript, lua: &str| -> f32 {
|
||||
s.run(lua).unwrap();
|
||||
s.resolve();
|
||||
let mut model = s.model_mut();
|
||||
if let Some(why) = UiScript::scratch_divergence(&mut model) {
|
||||
panic!("after {lua}: {why}");
|
||||
}
|
||||
drop(model);
|
||||
s.eval::<f32>("return Tail:GetLeft()").unwrap()
|
||||
};
|
||||
assert_eq!(
|
||||
step(&mut s, r#"Label:SetText("hello")"#),
|
||||
10.0 + 5.0 * PER_CHAR
|
||||
);
|
||||
assert_eq!(step(&mut s, r#"Label:SetText("")"#), 11.0, "the floor");
|
||||
// Swept for another reason while empty: still the floor.
|
||||
assert_eq!(
|
||||
step(&mut s, r#"Label:SetPoint("BOTTOMLEFT", 20, 10)"#),
|
||||
21.0
|
||||
);
|
||||
assert_eq!(
|
||||
step(&mut s, r#"Label:SetText("hello")"#),
|
||||
20.0 + 5.0 * PER_CHAR,
|
||||
"the same text again, under its old measure key"
|
||||
);
|
||||
assert_eq!(
|
||||
step(&mut s, r#"Label:SetText(nil)"#),
|
||||
21.0,
|
||||
"nil empties too"
|
||||
);
|
||||
assert_eq!(
|
||||
step(&mut s, r#"Label:SetText("hello")"#),
|
||||
20.0 + 5.0 * PER_CHAR
|
||||
);
|
||||
assert!(s.errors().is_empty(), "{:?}", s.errors());
|
||||
}
|
||||
|
||||
@@ -312,8 +312,8 @@ fn vertical_scroll_fires_as_given_and_the_marked_range_fires_range_changed() {
|
||||
assert!(s.errors().is_empty(), "{:?}", s.errors());
|
||||
}
|
||||
|
||||
/// The recompute resolves for itself, so a resize earlier in the same tick counts; the app
|
||||
/// resolves only once per frame, after the walk.
|
||||
/// The pump drains the layout before its walk (`0x7656ba`), so a resize made since the last tick
|
||||
/// counts in this tick's recompute, with no resolve of the host's in between.
|
||||
#[test]
|
||||
fn the_recompute_sees_a_same_tick_resize_with_no_intervening_resolve() {
|
||||
let mut s = script();
|
||||
@@ -346,8 +346,8 @@ fn the_recompute_sees_a_same_tick_resize_with_no_intervening_resolve() {
|
||||
assert_eq!(
|
||||
s.eval::<f32>("return seen_hi").unwrap(),
|
||||
400.0,
|
||||
"the recompute must resolve the JUST-set height itself, not report the stale \
|
||||
(pre-resize) range from the last full UiScript::resolve"
|
||||
"the drain before the walk resolves the JUST-set height, so the recompute does not \
|
||||
report the stale (pre-resize) range from the last full UiScript::resolve"
|
||||
);
|
||||
assert_eq!(
|
||||
s.eval::<f32>("return SF:GetVerticalScrollRange()").unwrap(),
|
||||
@@ -511,7 +511,8 @@ fn hit_test_denies_a_button_clipped_out_and_admits_it_once_scrolled_into_view()
|
||||
);
|
||||
}
|
||||
|
||||
/// The reference measures text synchronously, as stock `QuestLog_UpdateQuestDetails` relies on.
|
||||
/// The reference measures text synchronously and drains the layout before the walk (`0x7656ba`),
|
||||
/// as stock `QuestLog_UpdateQuestDetails` relies on: the text set before the tick is in the range.
|
||||
#[test]
|
||||
fn the_recompute_measures_the_childs_text_before_it_reads_the_range() {
|
||||
struct Rows;
|
||||
@@ -833,3 +834,27 @@ fn a_resize_marks_the_range() {
|
||||
assert_eq!(ranged(&s), (2, 300.0, 300.0));
|
||||
assert!(s.errors().is_empty(), "{:?}", s.errors());
|
||||
}
|
||||
|
||||
/// The mark rides the frame's `OnSizeChanged` override, behind `ApplyRect`'s gate (`0x76b5e7`,
|
||||
/// `0x76b602`), which is 2⁻²² in the reference's units, the screen diagonal as 1: a jitter under it
|
||||
/// marks nothing, so a child's unmarked resize stays unreported until a real resize.
|
||||
#[test]
|
||||
fn a_sub_threshold_jitter_does_not_mark_the_range() {
|
||||
let mut s = script();
|
||||
s.set_screen_size(800.0, 600.0);
|
||||
s.run(RANGED).unwrap();
|
||||
s.run("SF:SetScrollChild(Child)").unwrap();
|
||||
s.resolve();
|
||||
s.tick(0.016);
|
||||
assert_eq!(ranged(&s), (1, 400.0, 400.0));
|
||||
s.run("Child:SetHeight(700) SF:SetHeight(200.0001)")
|
||||
.unwrap();
|
||||
s.resolve();
|
||||
s.tick(0.016);
|
||||
assert_eq!(ranged(&s), (1, 400.0, 400.0), "a jitter marks nothing");
|
||||
s.run("SF:SetHeight(201)").unwrap();
|
||||
s.resolve();
|
||||
s.tick(0.016);
|
||||
assert_eq!(ranged(&s), (2, 499.0, 499.0));
|
||||
assert!(s.errors().is_empty(), "{:?}", s.errors());
|
||||
}
|
||||
|
||||
@@ -184,3 +184,95 @@ fn the_on_size_changed_watch_list_is_exactly_the_frames_carrying_the_script() {
|
||||
s.run(r#"Watched:SetScript("OnSizeChanged", nil)"#).unwrap();
|
||||
assert_eq!(watched(&s), 0, "clearing the script un-watches the frame");
|
||||
}
|
||||
|
||||
/// The pump drains the layout before its walk (`0x7656ba` → `0x768ed0`), and the drain's
|
||||
/// `ApplyRect` fires `OnSizeChanged`: a resize made in an event handler fires it ahead of that
|
||||
/// tick's `OnUpdate`.
|
||||
#[test]
|
||||
fn a_resize_from_an_event_fires_on_size_changed_before_the_walk() {
|
||||
let mut s = script();
|
||||
s.set_screen_size(800.0, 600.0);
|
||||
s.run(
|
||||
r#"
|
||||
LOG = {}
|
||||
P = CreateFrame("Frame", "DrainPanel")
|
||||
P:SetPoint("BOTTOMLEFT", 0, 0)
|
||||
P:SetWidth(10); P:SetHeight(10)
|
||||
P:RegisterEvent("PLAYER_ENTERING_WORLD")
|
||||
P:SetScript("OnEvent", function() this:SetWidth(50) end)
|
||||
P:SetScript("OnSizeChanged", function() table.insert(LOG, "size" .. arg1) end)
|
||||
P:SetScript("OnUpdate", function() table.insert(LOG, "update") end)
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
s.resolve();
|
||||
s.tick(0.016);
|
||||
s.run("LOG = {}").unwrap();
|
||||
s.queue_event("PLAYER_ENTERING_WORLD", Vec::new());
|
||||
s.tick(0.016);
|
||||
assert_eq!(
|
||||
s.eval::<String>("return table.concat(LOG, ' ')").unwrap(),
|
||||
"size50 update"
|
||||
);
|
||||
assert!(s.errors().is_empty(), "{:?}", s.errors());
|
||||
}
|
||||
|
||||
/// The pump drains again after its walk (`0x765799`), so a resize made in an `OnUpdate` fires
|
||||
/// `OnSizeChanged` inside the same tick, before the host reads the frame.
|
||||
#[test]
|
||||
fn a_resize_from_on_update_fires_on_size_changed_in_the_same_tick() {
|
||||
let mut s = script();
|
||||
s.set_screen_size(800.0, 600.0);
|
||||
s.run(
|
||||
r#"
|
||||
LOG = {}
|
||||
P = CreateFrame("Frame", "DrainPanel")
|
||||
P:SetPoint("BOTTOMLEFT", 0, 0)
|
||||
P:SetWidth(10); P:SetHeight(10)
|
||||
P:SetScript("OnSizeChanged", function() table.insert(LOG, "size" .. arg1) end)
|
||||
P:SetScript("OnUpdate", function()
|
||||
table.insert(LOG, "update")
|
||||
if GROW then GROW = nil; this:SetWidth(70) end
|
||||
end)
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
s.resolve();
|
||||
s.tick(0.016);
|
||||
s.run("LOG = {} GROW = true").unwrap();
|
||||
s.tick(0.016);
|
||||
assert_eq!(
|
||||
s.eval::<String>("return table.concat(LOG, ' ')").unwrap(),
|
||||
"update size70"
|
||||
);
|
||||
assert!(s.errors().is_empty(), "{:?}", s.errors());
|
||||
}
|
||||
|
||||
/// `ApplyRect` compares the size move against 2⁻²² (`0x76b5e7`, `0x76b602`, `[0x8029d4]`) in the
|
||||
/// reference's layout units, where the screen diagonal is 1: in ours, 2⁻²² × the layout unit,
|
||||
/// about 3.05e-4 at 4:3. A jitter under it fires nothing; a real move fires.
|
||||
#[test]
|
||||
fn a_sub_threshold_jitter_fires_nothing_and_a_real_move_fires() {
|
||||
let mut s = script();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
s.run(
|
||||
r#"
|
||||
fires = 0
|
||||
P = CreateFrame("Frame", "JitterPanel")
|
||||
P:SetPoint("BOTTOMLEFT", 0, 0)
|
||||
P:SetWidth(100); P:SetHeight(40)
|
||||
P:SetScript("OnSizeChanged", function() fires = fires + 1 end)
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
s.resolve();
|
||||
assert_eq!(s.eval::<i64>("return fires").unwrap(), 1, "the birth");
|
||||
// 1e-4 is a thousand times 2⁻²² but a third of the reference's threshold here.
|
||||
s.run("P:SetWidth(100.0001); P:SetHeight(40.0001)").unwrap();
|
||||
s.resolve();
|
||||
assert_eq!(s.eval::<i64>("return fires").unwrap(), 1, "a jitter");
|
||||
s.run("P:SetWidth(101)").unwrap();
|
||||
s.resolve();
|
||||
assert_eq!(s.eval::<i64>("return fires").unwrap(), 2, "a real move");
|
||||
assert!(s.errors().is_empty(), "{:?}", s.errors());
|
||||
}
|
||||
|
||||
@@ -257,3 +257,42 @@ fn a_mid_walk_raise_moves_a_frame_ahead_into_a_walked_level_so_it_waits_a_tick()
|
||||
assert_eq!(take_log(&s), "r w");
|
||||
assert!(s.errors().is_empty(), "{:?}", s.errors());
|
||||
}
|
||||
|
||||
/// The hover re-pick at the pump's tail (`0x7657be`) follows the drain after the walk
|
||||
/// (`0x765799`), so it hits against the rects the walk's handlers just moved: a frame moved under
|
||||
/// the still cursor as the hovered one hides gets the `OnEnter`.
|
||||
#[test]
|
||||
fn the_hover_re_pick_reads_the_rects_the_walk_moved() {
|
||||
let mut s = script();
|
||||
s.set_screen_size(800.0, 600.0);
|
||||
s.run(
|
||||
r#"
|
||||
LOG = {}
|
||||
A = CreateFrame("Frame", "PickA")
|
||||
A:SetPoint("BOTTOMLEFT", 0, 0); A:SetWidth(50); A:SetHeight(50)
|
||||
A:EnableMouse(true)
|
||||
A:SetScript("OnEnter", function() table.insert(LOG, "enterA") end)
|
||||
A:SetScript("OnLeave", function() table.insert(LOG, "leaveA") end)
|
||||
B = CreateFrame("Frame", "PickB")
|
||||
B:SetPoint("BOTTOMLEFT", 300, 300); B:SetWidth(50); B:SetHeight(50)
|
||||
B:EnableMouse(true)
|
||||
B:SetScript("OnEnter", function() table.insert(LOG, "enterB") end)
|
||||
Mover = CreateFrame("Frame", "PickMover")
|
||||
Mover:SetScript("OnUpdate", function()
|
||||
if GO then
|
||||
GO = nil
|
||||
A:Hide()
|
||||
B:SetPoint("BOTTOMLEFT", 0, 0)
|
||||
end
|
||||
end)
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
s.resolve();
|
||||
s.mouse_move(25.0, 25.0);
|
||||
assert_eq!(take_log(&s), "enterA");
|
||||
s.run("GO = true").unwrap();
|
||||
s.tick(0.016);
|
||||
assert_eq!(take_log(&s), "leaveA enterB");
|
||||
assert!(s.errors().is_empty(), "{:?}", s.errors());
|
||||
}
|
||||
|
||||
@@ -71,11 +71,13 @@ pub(crate) fn fire_event_into(lua: &Lua, event: &str, args: Vec<ScriptValue>) {
|
||||
|
||||
impl super::UiScript {
|
||||
/// Advance a frame: the `GetTime()` clock and queued events, then the pump `0x765650` and the
|
||||
/// draw pass `0x7657d0`, in that order (`0x764340`, `0x764347`). The pump is the update walk,
|
||||
/// each linked frame's update in strata, level and link order: its `OnUpdate(self, elapsed)`
|
||||
/// (`0x704f10`) and its kind's own update after it (an EditBox's flush, a tooltip's fade, a
|
||||
/// model pane's clock, a message frame's line fades, a scroll frame's range); then the hover
|
||||
/// re-pick at its tail (`0x7657be`). The draw pass fires `OnUpdateModel`.
|
||||
/// draw pass `0x7657d0`, in that order (`0x764340`, `0x764347`). The pump drains the layout
|
||||
/// queue (`0x768ed0`, at `0x7656ba`), whose rect applications fire `OnSizeChanged`; runs the
|
||||
/// update walk, each linked frame's update in strata, level and link order: its
|
||||
/// `OnUpdate(self, elapsed)` (`0x704f10`) and its kind's own update after it (an EditBox's
|
||||
/// flush, a tooltip's fade, a model pane's clock, a message frame's line fades, a scroll frame's
|
||||
/// range); drains the layout again (`0x765799`); then re-picks the hover against those rects
|
||||
/// (`0x7657be`). The draw pass fires `OnUpdateModel`.
|
||||
pub fn tick(&mut self, elapsed: f32) {
|
||||
self.model_mut().now += f64::from(elapsed);
|
||||
// Events queued since the last tick fire before this tick's OnUpdate.
|
||||
@@ -83,7 +85,9 @@ impl super::UiScript {
|
||||
for (event, args) in pending {
|
||||
self.fire_event(&event, args);
|
||||
}
|
||||
self.resolve();
|
||||
self.update_walk(elapsed);
|
||||
self.resolve();
|
||||
// A frame hid or showed under a still cursor: re-run the hover walk at the saved position,
|
||||
// as the reference's pump tail does (`0x765650` → `0x7660d0`). The hidden frame's OnLeave
|
||||
// already fired, so only the new winner's `OnEnter` fires.
|
||||
@@ -147,16 +151,13 @@ impl UiScript {
|
||||
/// then `0x7658e2`), level 0 up, a level's frames first linked first (`0x765920`). So the
|
||||
/// panes are read per stratum when its turn comes, and a pane an earlier handler of that
|
||||
/// stratum hid still paints this frame; only the callback's own gates ([`paints`]) are read at
|
||||
/// the call.
|
||||
///
|
||||
/// The pump drains the layout after the walk (`0x765799`), so the paint reads rects the walk's
|
||||
/// handlers moved: the pass resolves first.
|
||||
/// the call. The rects are the ones the pump's last drain (`0x765799`) left: the paint reads
|
||||
/// them through `0x768320`, which never resolves.
|
||||
fn paint_model_panes(&mut self) {
|
||||
let mut model = self.model_mut();
|
||||
if model.on_update_model_frames.is_empty() {
|
||||
return;
|
||||
}
|
||||
Self::resolve_layout(&mut model);
|
||||
// Kept by `SetScript`; a destroyed frame's handle compacts out on its first miss.
|
||||
if model
|
||||
.on_update_model_frames
|
||||
|
||||
@@ -258,6 +258,7 @@ pub(super) fn write_cell(
|
||||
color: [f32; 4],
|
||||
) {
|
||||
let d = model.region_data.entry(rh).or_default();
|
||||
let was_shown = d.text.as_deref().is_some_and(|t| !t.is_empty());
|
||||
d.text = Some(text.to_string());
|
||||
d.vertex_color = Some(color);
|
||||
// The line's own colour, as if set by `SetTextColor`: a later
|
||||
@@ -265,6 +266,7 @@ pub(super) fn write_cell(
|
||||
d.font_explicit.color = true;
|
||||
d.hidden = false;
|
||||
model.touch_measure(rh);
|
||||
model.touch_layout_text(rh, was_shown);
|
||||
}
|
||||
|
||||
/// Blanks every line cell and resets the counters and the subject: the model half of the clear
|
||||
@@ -276,12 +278,14 @@ fn clear_content(model: &mut Model, h: FrameHandle) {
|
||||
};
|
||||
for rh in lefts.into_iter().chain(rights) {
|
||||
if let Some(d) = model.region_data.get_mut(&rh) {
|
||||
let was_shown = d.text.as_deref().is_some_and(|t| !t.is_empty());
|
||||
d.text = None;
|
||||
d.hidden = true;
|
||||
// `measured` and the wrap pin (`size`) stay: the measure cache is keyed by content
|
||||
// ([`RegionData::measure_key`]) and `append_line` rewrites the pin, so the hover
|
||||
// loop's per-frame clear and rebuild (`ContainerFrameItemButton_OnUpdate`) re-measures
|
||||
// and re-lays out nothing.
|
||||
// nothing, and its lines' layout re-hashes to the converged node and solves nothing.
|
||||
model.touch_layout_text(rh, was_shown);
|
||||
}
|
||||
}
|
||||
if let Ok(t) = tip_mut(model, h) {
|
||||
|
||||
Reference in New Issue
Block a user