Files
benilla-octo/crates/benilla-ui/src/order.rs
T
SamandClaude Fable 5 251daa14c5 addons come alive: Bagnon draws, hovers and clicks
- Lua 5.0's generic-for and nested long strings are restored in a forked VM — 60 corpus addons stopped dying on one deleted opcode
- a day of demand-driven carving takes the 218-addon corpus from 24 addons surviving a session start to 114, with 144 loading without a single error
- Bagnon draws a real bag and answers real gestures — hover tooltip, pickup, use, drag — because our frames wear the reference's names, and the reference's own ContainerFrame.lua runs off the player's install instead of being transcribed
- the physics broad phase leaves: nothing in the client reads a contact, and it was measured at up to 41% of main-thread samples in real sessions

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-11 18:19:08 +07:00

690 lines
35 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! The draw-order primitive — the strata/layer vocabulary and the packed total-order key
//! (`ZKey`), plus the visible-tree [`traversal`] that realizes the client's painter order
//! (decision 0068).
//!
//! ## Ground truth (wow-5875-re, binary-verified)
//!
//! The 1.12.1 client draws in a **flat painter's order**, *not* a hierarchical walk: every visible
//! frame — children included — is an independent entry in a per-`(strata, level)` bucket
//! (`propagation.md`, `propagation-anchors.md`). A child frame with a *lower* strata/level than its
//! parent therefore draws *before* the parent — the order is global, keyed on the tuple below, not
//! on the tree shape.
//!
//! **Inside one `(strata, level)` bucket, the DRAW LAYER outranks the frame.** The five render
//! batches belong to the level node (`levelNode+0x1c`), not to any frame, and the emitter
//! `0x765920` loops `layer = 0..4` outside and frames inside. So a bucket emits every frame's
//! BACKGROUND, then every frame's BORDER, and so on — regions are *not* grouped behind their owning
//! frame. Within one layer the same is true of kind: the batch holds a quad sub-array and a text
//! sub-array, and `0x76fb00` drains all quads before any text, so **all** textures of a
//! `(strata, level, layer)` precede **all** its font strings. (Decision 0884, wow-re
//! `ui/scratch/draw-order-law.md`. This supersedes the earlier recorded key, which had no layer
//! term at all and read the last term backwards.)
//!
//! ## The total order (most- to least-significant)
//!
//! `(stratum, frame level, draw layer, texture<fontstring, frame link-stamp, is-region, sub-level,
//! declaration order)` — packed big-endian-by-priority into one sortable [`ZKey`] (`u64`), so the
//! whole render list is produced by one sort on the key. The `is-region` bit is benilla's own: the
//! binary has no frame-level drawable (only regions draw), so it sits *below* the layer key purely
//! to keep a frame's backdrop/scissor slot ahead of that frame's own regions.
//!
//! The **link-stamp** is the client's live intrusive-list position, not a creation index: a frame is
//! (re)linked at its bucket on becoming visible, on a strata change, and on a *changing* level set
//! — later link = drawn on top. A monotonic global counter bumped at those same moments is
//! order-equivalent within any one bucket, which is what [`crate::widget::WidgetArena`] keeps.
//!
//! **Below the layer, the frame term is exact for font strings and a deterministic stand-in for
//! textures.** The client re-sorts each layer's quad array by *texture handle* (`0x7731a0`) and
//! never sorts the text sub-array. A texture handle is an internal allocation our texture manager
//! assigns differently, so we reproduce the client's pre-sort array instead — identical wherever a
//! layer's quads share a texture or don't overlap. **No content may depend on the relative order of
//! two overlapping textures within one layer.**
//!
//! ## Era deltas (designed-for, not invented)
//!
//! - **`BLIZZARD` stratum** — the modern engine adds exactly one stratum *above* `TOOLTIP`
//! (decision 0068 §"strict superset"; the 1.12 `UI.xsd`↔Era diff). It is the last [`Strata`]
//! variant; 1.12 content never selects it.
//! - **`textureSubLevel`** — an i8 ordering knob *within* a draw layer, first-class in Era. 0884's
//! §5 found **no sub-level in 5875 at all** (`0x76a860` takes only `(region, layer)` and
//! head-inserts with no comparator; the per-layer header is a plain 0xc-byte triple with nowhere
//! to keep a sort key), correcting the earlier note that placed one in the 1.12 region lists. The
//! `sub_level` field stays as the Era delta and is inert for 1.12 content.
use crate::widget::{FrameHandle, RegionHandle, RegionKind, WidgetArena};
// ─────────────────────────────────────────────────────────────────────────────────────────────
// Strata — the top-level draw buckets
// ─────────────────────────────────────────────────────────────────────────────────────────────
/// The frame strata, in draw order (low → high). Variants 0..=8 are the client's nine
/// buckets, byte-verified in the `CSimpleTop` ctor's 9-loop (`propagation.md`): WORLD, BACKGROUND,
/// LOW, MEDIUM (the default, id 3), HIGH, DIALOG, FULLSCREEN, FULLSCREEN_DIALOG, TOOLTIP. `BLIZZARD`
/// (id 9) is the one stratum the modern engine adds *above* TOOLTIP (decision 0068) — an
/// extension point, never selected by 1.12 content.
#[repr(u8)]
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Strata {
/// id 0 — behind everything (the 3D world layer).
World = 0,
/// id 1.
Background = 1,
/// id 2.
Low = 2,
/// id 3 — the client default (`frameStrata` ctor value `3`, `frame-model.md`).
Medium = 3,
/// id 4.
High = 4,
/// id 5.
Dialog = 5,
/// id 6.
Fullscreen = 6,
/// id 7.
FullscreenDialog = 7,
/// id 8 — topmost in 1.12.
Tooltip = 8,
/// id 9 — **Era addition**, drawn above `Tooltip` (decision 0068). No 1.12 content uses it.
Blizzard = 9,
}
impl Strata {
/// Every stratum in draw order (low → high).
pub const ALL: [Strata; 10] = [
Strata::World,
Strata::Background,
Strata::Low,
Strata::Medium,
Strata::High,
Strata::Dialog,
Strata::Fullscreen,
Strata::FullscreenDialog,
Strata::Tooltip,
Strata::Blizzard,
];
/// The client bucket id (0..=9).
#[inline]
pub const fn index(self) -> u8 {
self as u8
}
}
impl Default for Strata {
/// `MEDIUM` — the client's default `frameStrata` (ctor writes `3`, `frame-model.md`).
fn default() -> Strata {
Strata::Medium
}
}
// ─────────────────────────────────────────────────────────────────────────────────────────────
// Draw layers — the within-frame region order
// ─────────────────────────────────────────────────────────────────────────────────────────────
/// The five draw layers a region sits in, in draw order (`frame-model.md`, the frame's five
/// region lists ~`0x1c0`): BACKGROUND, BORDER, ARTWORK, OVERLAY, HIGHLIGHT.
#[repr(u8)]
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum DrawLayer {
/// Drawn first (behind the frame's other regions).
Background = 0,
Border = 1,
/// The default layer for a texture/fontstring with no explicit `<Layer>`.
Artwork = 2,
Overlay = 3,
/// Drawn last (mouse-over highlights).
Highlight = 4,
}
impl DrawLayer {
/// The layer index (0..=4).
#[inline]
pub const fn index(self) -> u8 {
self as u8
}
}
impl Default for DrawLayer {
/// `ARTWORK` — the layer a texture/fontstring with no explicit `<Layer>` lands in.
fn default() -> DrawLayer {
DrawLayer::Artwork
}
}
// ─────────────────────────────────────────────────────────────────────────────────────────────
// ZKey — the packed total-order key
// ─────────────────────────────────────────────────────────────────────────────────────────────
//
// Bit layout (u64), most-significant field = highest draw priority. The whole render list is a
// single ascending sort on this key.
//
// bits 60..=63 (4) stratum Strata::index() 0..=9 (< 16)
// bits 44..=59 (16) frame level u16 (< 65536)
// bits 41..=43 (3) draw layer DrawLayer::index() 0..=4 (< 8)
// bit 40 (1) fontstring 0 = texture, 1 = fontstring — BUCKET-WIDE, above the frame
// bits 21..=39 (19) frame link-stamp monotonic (re)link seq (< 524288)
// bit 20 (1) is-region 0 = the frame itself, 1 = one of its regions
// bits 12..=19 (8) sub-level i8 biased by +128 (0..=255) — INERT on 1.12
// bits 0..=11 (12) declaration seq region index within its owner frame (< 4096)
//
// ## Why the draw layer outranks the frame (decision 0884 — §5-VERIFIED)
//
// **The five render batches belong to the LEVEL NODE, not to the frame** (`levelNode+0x1c`, five
// 0x30-byte batches), and the emitter `0x765920` loops `layer = 0..4` on the OUTSIDE and frames on
// the inside. So the draw layer is a **bucket-wide** key: every frame sharing a `(strata, level)`
// emits its BACKGROUND regions, then every frame its BORDER, then ARTWORK, then OVERLAY, then
// HIGHLIGHT. It is not a within-frame ordering — which is what benilla modeled until 0884, with
// the frame term ranked above the layer so a frame's regions stayed glued behind it.
//
// That inversion is what put the mirror timer's blue fill over its own border art and caption
// (director report, 2026-08-02). The reference builds those bars as a Frame whose OVERLAY layer
// carries the border + caption, with a child StatusBar whose `<BarTexture>` takes the default
// **ARTWORK** layer and whose only OnLoad is `SetFrameLevel(GetFrameLevel() - 1)`. That line does
// **not** put the child below the parent: a child is born at `parent.level + 1` (`SetParent
// 0x76ab10` @`0x76ab65`), so the `-1` lands it at *exactly* the parent's level. Its whole job is to
// **create the tie**, so the layer key can decide — ARTWORK(2) before OVERLAY(3), fill under
// chrome. Ranked below the frame, there was no tie to resolve and the child always trailed.
//
// **`texture < fontstring` is bucket-wide too, and outranks the frame.** A layer's batch holds two
// independent sub-arrays — the quad array `[batch+0x10]` (every `CSimpleTexture` appends via
// `0x7706e0` → `0x772fd0`) and the text sub-batch `[batch+0x18]` (`0x772e50` → `0x773080`) — and
// `0x76fb00` drains the WHOLE quad array before it touches the text one. So for two frames A, B at
// one `(strata, level, layer)`, each with a texture and a font string, the client emits
// `A.tex, B.tex, A.text, B.text` — never `A.tex, A.text, B.tex, B.text`.
//
// **The frame link-stamp below that is exact for font strings and a deterministic stand-in for
// textures.** The client re-sorts a layer's quad array by *texture handle* (`0x7731a0`'s qsort,
// comparator `0x7731c0`) and never sorts the text sub-batch. The handle is an internal allocation
// our texture manager assigns differently, so reproducing that sort is neither achievable nor
// desirable; we reproduce the client's PRE-sort array, which agrees wherever a layer's quads share
// a texture or don't overlap (the common case). **No content may depend on the relative order of
// two overlapping textures within one layer** — that is not a fidelity invariant.
//
// wow-re's own previously-recorded key — `(strata, level, insertion order)` — is superseded by the
// same §5: it carried no layer term at all.
//
// A frame's own entry zeroes the layer and everything below `is-region`, so it precedes its own
// BACKGROUND regions. That slot is benilla's backdrop/scissor hook, not the client's — in the
// binary a frame has no drawable of its own — so it sits *below* the layer key, never above it.
//
// **`sub-level` is inert on 1.12**: §5 found no sub-level in 5875 at all (`0x76a860` takes only
// `(region, layer)`, head-inserts with no comparator, and the per-layer header is a plain 0xc-byte
// triple with nowhere to keep a sort key). The field stays as the modeled Era delta; no 1.12
// content sets it, so it is always the `0` bias.
const STRATUM_SHIFT: u32 = 60;
const LEVEL_SHIFT: u32 = 44;
const LAYER_SHIFT: u32 = 41;
const FONTSTRING_SHIFT: u32 = 40;
const INSERTION_SHIFT: u32 = 21;
const IS_REGION_SHIFT: u32 = 20;
const SUBLEVEL_SHIFT: u32 = 12;
const DECL_SHIFT: u32 = 0;
pub(crate) const INSERTION_BITS: u32 = 19;
const DECL_BITS: u32 = 12;
/// The packed draw-order key: a single `u64` whose natural `Ord` *is* the client's total draw
/// order. Build one with [`ZKey::frame`] or [`ZKey::region`]; sort ascending to get painter order.
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct ZKey(u64);
impl ZKey {
/// The key for a **frame's own** entry — every region-level field is zero, so this sorts before
/// all of the frame's regions.
pub fn frame(strata: Strata, level: u16, insertion: u32) -> ZKey {
debug_assert!(
insertion < (1 << INSERTION_BITS),
"frame insertion {insertion} exceeds {INSERTION_BITS} bits"
);
let bits = (u64::from(strata.index()) << STRATUM_SHIFT)
| (u64::from(level) << LEVEL_SHIFT)
| (u64::from(insertion) << INSERTION_SHIFT);
ZKey(bits)
}
/// The key for one **region** of a frame. `strata`/`level`/`insertion` are the *owner frame's*
/// (so the region stays grouped behind its frame); `is_fontstring` places textures before
/// fontstrings at equal (layer, sub-level).
#[allow(clippy::too_many_arguments)]
pub fn region(
strata: Strata,
level: u16,
insertion: u32,
layer: DrawLayer,
sub_level: i8,
is_fontstring: bool,
decl: u16,
) -> ZKey {
debug_assert!(
insertion < (1 << INSERTION_BITS),
"frame insertion {insertion} exceeds {INSERTION_BITS} bits"
);
debug_assert!(
u32::from(decl) < (1 << DECL_BITS),
"region decl seq {decl} exceeds {DECL_BITS} bits"
);
// sub_level (i8, -128..=127) biased into 0..=255 so higher sub-level sorts later.
let sub_biased = (i16::from(sub_level) + 128) as u64;
let bits = (u64::from(strata.index()) << STRATUM_SHIFT)
| (u64::from(level) << LEVEL_SHIFT)
| (u64::from(insertion) << INSERTION_SHIFT)
| (1u64 << IS_REGION_SHIFT)
| (u64::from(layer.index()) << LAYER_SHIFT)
| (sub_biased << SUBLEVEL_SHIFT)
| (u64::from(is_fontstring) << FONTSTRING_SHIFT)
| (u64::from(decl) << DECL_SHIFT);
ZKey(bits)
}
/// The key a frame's own **content** draws at: its bare [`ZKey::frame`] slot promoted into its
/// region band at `layer` — behind that layer's textures, ahead of its declared font strings.
///
/// Content is what a widget kind draws *itself*, with no region to hang it on — today only a
/// ScrollingMessageFrame's ring lines, which the client materializes as the frame's own font
/// strings (its `<FontString>` child is their font instance, and a font string with no
/// `<Layer>` is `ARTWORK`). At the bare frame slot they sorted before **all** of the frame's
/// regions, so the chat window's own `$parentBackground` — a `BACKGROUND` texture, faded in
/// on hover — painted over the messages and dimmed them (director, 2026-07-26; the white
/// text measured 192, exactly `255 × (1 − 0.25)`, the box's own alpha). The frame slot stays
/// what it was — the backdrop/scissor setup that must precede everything the frame owns.
#[inline]
#[must_use]
pub const fn content(self, layer: DrawLayer) -> ZKey {
ZKey(
self.0
| (1u64 << IS_REGION_SHIFT)
| ((layer.index() as u64) << LAYER_SHIFT)
// Sub-level 0 in the same +128 bias `region` uses, and the font-string bit: content
// is text, so it takes the same textures-draw-first slot a font string would.
| (128u64 << SUBLEVEL_SHIFT)
| (1u64 << FONTSTRING_SHIFT),
)
}
/// The raw packed value (the sort key).
#[inline]
pub const fn raw(self) -> u64 {
self.0
}
/// Unpack this key back into its fields.
#[inline]
pub const fn parts(self) -> ZParts {
unpack(self.0)
}
}
/// The fields of a packed [`ZKey`], in significance order — the readable face of an opaque `u64`.
///
/// A draw-order key is a single number by design (one sort produces the whole render list), which
/// makes every question *about* an order — "why is this over that?", "which pairs would a re-rank
/// invert?" — unanswerable at a glance. [`unpack`] is that answer, and it is why
/// [`crate::script::ExtractedQuad::z`] can stay a bare `u64`.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct ZParts {
/// The frame strata bucket id (0..=9, [`Strata::index`]).
pub strata: u8,
/// The frame level within the strata bucket.
pub level: u16,
/// The draw layer (0..=4, [`DrawLayer::index`]) — bucket-wide, above the frame (decision 0884).
pub layer: u8,
/// `true` for a font string: all textures of a `(strata, level, layer)` precede all its text.
pub is_fontstring: bool,
/// The owning frame's link-stamp — its position in the client's intrusive bucket list.
pub insertion: u32,
/// `false` for a frame's own slot, `true` for one of its regions.
pub is_region: bool,
/// The Era `textureSubLevel` knob; inert for 1.12 content (always 0 there).
pub sub_level: i8,
/// Declaration order within the owning frame's layer.
pub decl: u16,
}
/// Unpack a raw [`ZKey`] (e.g. [`crate::script::ExtractedQuad::z`]) into its fields.
#[inline]
pub const fn unpack(raw: u64) -> ZParts {
ZParts {
strata: ((raw >> STRATUM_SHIFT) & 0xf) as u8,
level: ((raw >> LEVEL_SHIFT) & 0xffff) as u16,
layer: ((raw >> LAYER_SHIFT) & 0x7) as u8,
is_fontstring: (raw >> FONTSTRING_SHIFT) & 1 == 1,
insertion: ((raw >> INSERTION_SHIFT) & ((1 << INSERTION_BITS) - 1)) as u32,
is_region: (raw >> IS_REGION_SHIFT) & 1 == 1,
sub_level: (((raw >> SUBLEVEL_SHIFT) & 0xff) as i16 - 128) as i8,
decl: ((raw >> DECL_SHIFT) & ((1 << DECL_BITS) - 1)) as u16,
}
}
// ─────────────────────────────────────────────────────────────────────────────────────────────
// Traversal — the visible render list in the client's painter order
// ─────────────────────────────────────────────────────────────────────────────────────────────
/// What a [`ZKey`] entry points at: a frame's own draw slot, or one of its regions.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum ZTarget {
/// The frame itself (backdrop/scissor slot) — sorts before its regions.
Frame(FrameHandle),
/// A texture or fontstring belonging to a frame.
Region(RegionHandle),
}
/// Produce the render list in the client's exact `render_traverse_order 0x765650` order.
///
/// Only **effective-visible** frames contribute: because `effective_visible` is maintained across
/// the whole subtree by the arena's propagation (`set_shown`/`set_parent`), filtering on the flag is
/// equivalent to the client's "recurse to child frames, a hidden mid-tree frame blocks its subtree"
/// (`propagation.md`) — a child of a hidden frame already carries `effective_visible == false` and is
/// skipped here. Each visible frame emits its own [`ZTarget::Frame`] entry followed by one
/// [`ZTarget::Region`] per owned region; the returned vec is sorted ascending by [`ZKey`], which *is*
/// the total draw order (strata → frame level → **draw layer** → texture<fontstring → frame
/// link-stamp → is-region → sub-level → decl; the layer outranks the frame — see the `ZKey`
/// bit-layout note and decision 0884). Ties are impossible: distinct frames differ in link-stamp, a frame and its
/// regions differ in the is-region bit, and a frame's regions differ in the remaining fields.
///
/// Ordering only: this emits an entry for *every* region of a visible frame. Region-level
/// `Show`/`Hide` (the VisibleRegion bit, `region+0xc4`) is applied one layer up, where paint lives —
/// [`UiScript::extract`](crate::script::UiScript::extract) drops hidden regions before they become
/// quads. (The 1.12 region-draw cluster that would pin the *draw-time* skip is still flagged unread
/// in wow-re's findings; the flag itself and its setter `0x77fcb0` are recorded.)
pub fn traversal(arena: &WidgetArena) -> Vec<(ZTarget, ZKey)> {
let mut out: Vec<(ZTarget, ZKey)> = Vec::new();
for (fh, frame) in arena.iter_frames() {
if !frame.effective_visible {
continue;
}
let (strata, level, insertion) = (frame.strata, frame.level, frame.insertion_seq);
out.push((ZTarget::Frame(fh), ZKey::frame(strata, level, insertion)));
for &rh in &frame.regions {
let Some(region) = arena.region(rh) else {
continue;
};
// `Region:SetParent(nil)` orphaned this leaf: unlinked from every draw layer, still
// alive (see [`crate::widget::Region::detached`]). It draws nothing until re-parented.
if region.detached {
continue;
}
let key = ZKey::region(
strata,
level,
insertion,
region.draw_layer,
region.sub_level,
matches!(region.kind, RegionKind::FontString),
region.decl_seq as u16,
);
out.push((ZTarget::Region(rh), key));
}
}
out.sort_by_key(|&(_, k)| k);
out
}
// ─────────────────────────────────────────────────────────────────────────────────────────────
// Hit-testing — the mouse-focus capture walk
// ─────────────────────────────────────────────────────────────────────────────────────────────
/// Find the frame the cursor captures, per the documented top-down-by-draw-order mouse-focus model
/// (`propagation.md`): mouse focus walks frames **top-down in reverse draw order** (the
/// topmost-*drawn* frame first) and the first mouse-enabled, visible frame whose rect contains the
/// cursor captures.
///
/// ## Fidelity: spec-faithful, NOT byte-pinned
///
/// wow-5875-re located the real mouse-hit-test leaf (InputControl / CSimpleTop) *to its translation
/// unit* but **did not byte-pin it** — it is a flagged RE-time item, not an anchor (`anchors.md`:
/// "the exact mouse-hit-test leaf inside InputControl/CSimpleTop … NOT byte-pinned"). So this is
/// faithful to the *documented model*, **not** bit-exact to the binary. The one primitive that *is*
/// verified is `point_in_rect 0x76b020` (the clip/hit rect test); the top-down capture policy above
/// is the documented model, not a byte anchor.
///
/// Pure over its inputs: `sorted` is a draw-order-**ascending** list (as [`traversal`] returns), and
/// `hits(frame)` reports whether that frame is a capture candidate at the cursor — i.e. it is
/// mouse-enabled **and** its resolved rect contains the point. Visibility is already handled
/// upstream: [`traversal`] only emits effective-visible frames, so an (effective-)hidden frame never
/// reaches `hits`. Regions never capture (only [`ZTarget::Frame`] entries are considered). Returns
/// the first hit found walking from the top of the draw order downward.
pub fn hit_test<F: Fn(FrameHandle) -> bool>(
sorted: &[(ZTarget, ZKey)],
hits: F,
) -> Option<FrameHandle> {
for (target, _) in sorted.iter().rev() {
if let ZTarget::Frame(fh) = *target {
if hits(fh) {
return Some(fh);
}
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
// ── ZKey field ordering ────────────────────────────────────────────────────────────────────
#[test]
fn stratum_dominates_everything() {
// A TOOLTIP frame at level 0 draws after a WORLD frame at max level.
let lo = ZKey::frame(Strata::World, u16::MAX, 0);
let hi = ZKey::frame(Strata::Tooltip, 0, 0);
assert!(hi > lo);
// BLIZZARD (Era) is above TOOLTIP.
assert!(ZKey::frame(Strata::Blizzard, 0, 0) > ZKey::frame(Strata::Tooltip, u16::MAX, 0));
}
#[test]
fn level_then_insertion_within_stratum() {
let a = ZKey::frame(Strata::Medium, 1, 999);
let b = ZKey::frame(Strata::Medium, 2, 0);
assert!(b > a, "higher level draws later regardless of insertion");
let c = ZKey::frame(Strata::Medium, 1, 5);
let d = ZKey::frame(Strata::Medium, 1, 6);
assert!(d > c, "at equal level, later insertion draws later");
}
#[test]
fn frame_precedes_its_regions() {
let f = ZKey::frame(Strata::Medium, 3, 42);
// Even a region in the lowest layer with the most-negative sub-level sorts after the frame.
let r = ZKey::region(
Strata::Medium,
3,
42,
DrawLayer::Background,
i8::MIN,
false,
0,
);
assert!(r > f);
}
#[test]
fn layer_then_kind_then_frame_then_decl() {
let mk = |layer, sub, fs, decl| ZKey::region(Strata::Medium, 3, 42, layer, sub, fs, decl);
// The layer is the top term below (strata, level) — it dominates everything under it.
assert!(
mk(DrawLayer::Border, i8::MIN, false, 0) > mk(DrawLayer::Background, 127, true, 4095)
);
// Within a layer, KIND is next: all textures precede all fontstrings — and it now outranks
// sub-level (0884: the layer batch's two sub-arrays, quads drained before text).
assert!(
mk(DrawLayer::Artwork, i8::MIN, true, 0) > mk(DrawLayer::Artwork, 127, false, 4095)
);
// Below kind, declaration order decides within one frame's layer.
assert!(mk(DrawLayer::Artwork, 0, false, 2) > mk(DrawLayer::Artwork, 0, false, 1));
// Sub-level still orders below kind — inert on 1.12 (nothing sets it), kept for Era.
assert!(mk(DrawLayer::Artwork, -1, false, 0) < mk(DrawLayer::Artwork, 0, false, 0));
}
/// The law 0884 **replaced**, kept as a test so it cannot come back: a frame's regions are not
/// glued behind it. The draw layer is bucket-wide, so at one `(strata, level)` every frame's
/// BACKGROUND draws before any frame's OVERLAY — frame A's OVERLAY sorts *after* frame B's
/// BACKGROUND even though A linked first. That interleave is what puts the mirror timer's
/// ARTWORK fill under its own parent frame's OVERLAY border and caption.
#[test]
fn the_layer_interleaves_frames_it_does_not_group_them() {
let a_overlay = ZKey::region(Strata::Medium, 0, 10, DrawLayer::Overlay, 0, false, 0);
let b_background = ZKey::region(Strata::Medium, 0, 11, DrawLayer::Background, 0, false, 0);
assert!(
b_background < a_overlay,
"a later frame's BACKGROUND still draws before an earlier frame's OVERLAY"
);
// Same layer + same kind: the frame link-stamp decides, later on top.
let a_art = ZKey::region(Strata::Medium, 0, 10, DrawLayer::Artwork, 0, false, 0);
let b_art = ZKey::region(Strata::Medium, 0, 11, DrawLayer::Artwork, 0, false, 0);
assert!(
a_art < b_art,
"at equal layer+kind, later link draws on top"
);
// Kind spans frames: ALL textures of a (strata, level, layer) precede ALL its fontstrings.
let a_text = ZKey::region(Strata::Medium, 0, 10, DrawLayer::Artwork, 0, true, 0);
assert!(
b_art < a_text,
"a later frame's texture still precedes an earlier frame's fontstring"
);
// A frame's own slot is benilla's backdrop/scissor hook: it sits BELOW the layer key, so
// it precedes only its own regions, starting with its BACKGROUND.
let a_frame = ZKey::frame(Strata::Medium, 0, 10);
let a_bg = ZKey::region(Strata::Medium, 0, 10, DrawLayer::Background, 0, false, 0);
assert!(a_frame < a_bg);
}
// ── Traversal over a synthetic tree ────────────────────────────────────────────────────────
use crate::widget::{FrameKind, WidgetArena};
#[test]
fn traversal_snapshot_over_a_small_tree() {
let mut a = WidgetArena::new();
// A DIALOG-strata parent with a texture; a MEDIUM child (lower strata ⇒ draws *before* its
// parent, proving the order is flat, not hierarchical); and a hidden frame that must not
// appear at all.
let dialog = a.create(FrameKind::Frame, Some("Dialog".into()), None);
a.set_frame_strata(dialog, Strata::Dialog);
let dlg_bg = a
.create_region(dialog, RegionKind::Texture, DrawLayer::Background, 0)
.unwrap();
let dlg_text = a
.create_region(dialog, RegionKind::FontString, DrawLayer::Artwork, 0)
.unwrap();
// set_frame_strata forced the child to DIALOG too (subtree force), so make the child a
// top-level MEDIUM frame instead to exercise cross-strata draw order.
let medium = a.create(FrameKind::Frame, None, None); // MEDIUM, level 0
let med_art = a
.create_region(medium, RegionKind::Texture, DrawLayer::Artwork, 0)
.unwrap();
let hidden = a.create(FrameKind::Frame, None, None);
let _hidden_rgn = a
.create_region(hidden, RegionKind::Texture, DrawLayer::Artwork, 0)
.unwrap();
a.set_shown(hidden, false);
let list: Vec<ZTarget> = traversal(&a).into_iter().map(|(t, _)| t).collect();
// Expected painter order: MEDIUM frame + its region first (lower strata), then the DIALOG
// frame followed by its two regions (Background texture before Artwork fontstring). The
// hidden frame and its region are absent.
assert_eq!(
list,
vec![
ZTarget::Frame(medium),
ZTarget::Region(med_art),
ZTarget::Frame(dialog),
ZTarget::Region(dlg_bg),
ZTarget::Region(dlg_text),
]
);
}
/// The client's bucket re-add on show (`effective_visible_show 0x76ae10`, propagation.md):
/// within one (strata, level) bucket, a frame shown LATER draws over one declared later but
/// never hidden — the exact minimap case (MiniMapTrackingFrame is declared before
/// MinimapBackdrop, hidden at load, and its runtime Show must lift it over the backdrop's
/// ring art, as the director's reference A/B shows; decision 0557). A strata/level change on
/// a visible frame re-buckets to the tail the same way; on a hidden frame it does not bump
/// (the client's remove/add is visible-gated — it appends on the next show regardless).
#[test]
fn showing_a_frame_moves_it_to_its_buckets_tail() {
let mut a = WidgetArena::new();
let tracking = a.create(FrameKind::Frame, Some("Tracking".into()), None);
let backdrop = a.create(FrameKind::Frame, Some("Backdrop".into()), None);
let order = |a: &WidgetArena| -> Vec<ZTarget> {
traversal(a).into_iter().map(|(t, _)| t).collect()
};
// Declaration order while both start shown: tracking before backdrop.
assert_eq!(
order(&a),
vec![ZTarget::Frame(tracking), ZTarget::Frame(backdrop)]
);
// Hide-then-show (the XML's hidden="true" + the runtime Show()): tracking re-enters its
// bucket at the tail — above the backdrop.
a.set_shown(tracking, false);
a.set_shown(tracking, true);
assert_eq!(
order(&a),
vec![ZTarget::Frame(backdrop), ZTarget::Frame(tracking)]
);
// A no-op Show (already visible) is NOT a transition — no re-add, order stays.
a.set_shown(backdrop, true);
assert_eq!(
order(&a),
vec![ZTarget::Frame(backdrop), ZTarget::Frame(tracking)]
);
// A visible frame's level round-trip re-buckets it to the tail both times — the
// remove/add of set_frame_level. (backdrop: level 0 → 1 → 0, ends level 0 again but now
// draws over tracking.)
a.set_frame_level(backdrop, 1, false);
a.set_frame_level(backdrop, 0, false);
assert_eq!(
order(&a),
vec![ZTarget::Frame(tracking), ZTarget::Frame(backdrop)]
);
// The same round-trip while HIDDEN doesn't bump: after the show it sits at the tail
// because of the show itself, and a hidden frame is in no bucket to move.
a.set_shown(tracking, false);
a.set_frame_level(tracking, 1, false);
a.set_frame_level(tracking, 0, false);
a.set_shown(tracking, true);
assert_eq!(
order(&a),
vec![ZTarget::Frame(backdrop), ZTarget::Frame(tracking)]
);
}
// ── Hit-testing (pure core) ─────────────────────────────────────────────────────────────────
#[test]
fn hit_test_returns_topmost_matching_frame_and_skips_regions() {
let mut a = WidgetArena::new();
// Two overlapping frames; `top` (DIALOG) draws above `bottom` (MEDIUM). A region belongs to
// `top` and would sort *after* the frame in draw order — it must never be returned.
let bottom = a.create(FrameKind::Frame, None, None);
let top = a.create(FrameKind::Frame, None, None);
a.set_frame_strata(top, Strata::Dialog);
let _rgn = a
.create_region(top, RegionKind::Texture, DrawLayer::Artwork, 0)
.unwrap();
let sorted = traversal(&a);
// Both frames are candidates ⇒ the topmost-drawn (`top`) wins.
assert_eq!(hit_test(&sorted, |_| true), Some(top));
// Only `bottom` is a candidate ⇒ it wins even though `top` draws above it (top is transparent
// to hits, e.g. mouse-disabled).
assert_eq!(hit_test(&sorted, |fh| fh == bottom), Some(bottom));
// No candidate ⇒ no capture.
assert_eq!(hit_test(&sorted, |_| false), None);
}
}