Part of #41: the comments of `benilla-ui` (the Lua UI engine, its FrameXML loader and its widgets) are rewritten to the rule in `docs/METHOD.md`, which is what the code does and the reference fact behind it, in a line. 43,140 comment lines become 15,161. - **The mechanical strip.** The citations that were only a private record number go first, and each edit is shown to be a pure removal. - **Wrong facts corrected.** The rewrite corrects the comments that stated wrong facts. Among them: - `0x64a480` is the case-sensitive compare (a byte `repz cmpsb`), and `0x64a4c0` the case-insensitive one. - A frame's id sits at `+0xb0`, not `+0xb4`. `EnableKeyboard`/`IsKeyboardEnabled` are `0x776ec0`/`0x776f90`. - A region's unpinned edge has no fallback to its owner's rect: the owner supplies only the scale. - `$parent` expands to the nearest named ancestor, or "Top" when none is named (`0x76c5b0`), not simply to the parent. - A StatusBar fills from its internal normalised fraction, not from `GetValue()`. - The addresses of `StartDuel` and `StartDuelUnit` were swapped. - Many "untraced" notes now give the traced reference fact: the drag threshold, the per-grip sizing edges, the empty-`autoFocus` skip, and `SetTexCoordModifiesRect`'s default. - Stock FrameXML line citations are checked against the stock 1.12 files and corrected. `ref-` file names become the stock file names. - **Deviation labels.** "Deviation:" now marks only a choice with a reason. A surface that exists only for the retired Era addon target says so plainly: it is not 1.12. - **Verification.** Every file strips to identical code, and `cargo fmt --check` is clean. Nine second readers compared every file old against new, restored the facts the rewrite had dropped, and checked each new citation against the code, the stock files, vmangos and the reference. Behaviour differences the rewrite surfaced are filed as their own issues (#131–#142), and more are being validated. The comments here state what the code does. String literals are untouched; they are #92. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
404 lines
14 KiB
Rust
404 lines
14 KiB
Rust
//! The frame backdrop as the 1.12 client builds it: the tiled background and 8-piece border that a
|
||
//! `<Backdrop>` element or `SetBackdrop` installs. This module owns the data ([`Backdrop`]) and the
|
||
//! geometry ([`pieces`], screen-space quads with per-corner UVs); the app renders them.
|
||
|
||
use crate::layout::Rect;
|
||
|
||
/// The default `edgeSize`, logical 32: the constructor writes device 0.025 (`DAT_0081c9b0`).
|
||
pub const DEFAULT_EDGE_SIZE: f32 = 32.0;
|
||
|
||
/// `BackgroundInsets`: the background's inset from the frame rect; the border is never inset.
|
||
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
||
pub struct Insets {
|
||
pub left: f32,
|
||
pub right: f32,
|
||
pub top: f32,
|
||
pub bottom: f32,
|
||
}
|
||
|
||
/// A frame's installed backdrop (the 0x68-byte struct at `frame+0x1ac`): exactly the keys the
|
||
/// reader accepts (`0x7776e0`) plus the two colours. The colours default to opaque white; the XML
|
||
/// parser defaults a present but partial `<Color>` or `<BorderColor>` to black.
|
||
#[derive(Clone, Debug, PartialEq)]
|
||
pub struct Backdrop {
|
||
/// The background texture (`+0x24`); none or empty draws no background.
|
||
pub bg_file: Option<String>,
|
||
/// The 8-slice border texture (`+0x30`); none or empty draws no border.
|
||
pub edge_file: Option<String>,
|
||
/// Tile the background, else stretch it (`+0x40`).
|
||
pub tile: bool,
|
||
/// `tileSize` in logical px (`+0x4c`); 0 tiles at `edge_size` (`0x77f0c0`).
|
||
pub tile_size: f32,
|
||
/// `edgeSize` in logical px (`+0x48`): the border piece size and the edge tiling period.
|
||
pub edge_size: f32,
|
||
/// `BackgroundInsets` (`+0x50..0x5c`).
|
||
pub insets: Insets,
|
||
/// The background's vertex colour (`+0x60`, `SetBackdropColor`).
|
||
pub bg_color: [f32; 4],
|
||
/// All 8 border pieces' vertex colour (`+0x64`, `SetBackdropBorderColor`).
|
||
pub border_color: [f32; 4],
|
||
}
|
||
|
||
impl Default for Backdrop {
|
||
fn default() -> Self {
|
||
// The constructor (`0x77e5f0`).
|
||
Self {
|
||
bg_file: None,
|
||
edge_file: None,
|
||
tile: false,
|
||
tile_size: 0.0,
|
||
edge_size: DEFAULT_EDGE_SIZE,
|
||
insets: Insets::default(),
|
||
bg_color: [1.0, 1.0, 1.0, 1.0],
|
||
border_color: [1.0, 1.0, 1.0, 1.0],
|
||
}
|
||
}
|
||
}
|
||
|
||
impl Backdrop {
|
||
fn has_bg(&self) -> bool {
|
||
self.bg_file.as_deref().is_some_and(|s| !s.is_empty())
|
||
}
|
||
fn has_border(&self) -> bool {
|
||
self.edge_file.as_deref().is_some_and(|s| !s.is_empty())
|
||
}
|
||
}
|
||
|
||
/// One drawable piece, the background or one of the 8 border pieces: explicit corners, not a rect,
|
||
/// so the bottom-right inset quirk can slant it, and per-corner UVs, since the top and bottom edges
|
||
/// are rotated 90° (`0x77f0c0`). Both arrays run `[TL, TR, BR, BL]`, y-up.
|
||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||
pub struct BackdropPiece {
|
||
/// Corner positions in screen px.
|
||
pub corners: [[f32; 2]; 4],
|
||
/// Corner UVs in texture space.
|
||
pub uvs: [[f32; 2]; 4],
|
||
/// The texture needs repeat addressing. The corners stay in `[0,1]` but share the edge texture,
|
||
/// so they carry `true` too and the border batches as one texture (`0x77e8d0`).
|
||
pub tile: bool,
|
||
/// Drawn with `bg_file` and `bg_color`, else with `edge_file` and `border_color`.
|
||
pub is_bg: bool,
|
||
}
|
||
|
||
const SLICE: f32 = 0.125;
|
||
|
||
/// Pull a piece's UVs half a texel inward on every axis that does not tile, so bilinear
|
||
/// magnification cannot sample the neighbouring slice of the shared border atlas (in
|
||
/// `ChatBubble-Backdrop`, a white alpha-0 column: a pale line through the speech bubble). A tiling
|
||
/// axis, a strip's `v` past 1, is left exact, or each period would land short and walk the art.
|
||
#[must_use]
|
||
pub fn inset_atlas_bleed(uvs: [[f32; 2]; 4], tex_w: f32, tex_h: f32) -> [[f32; 2]; 4] {
|
||
let mut out = uvs;
|
||
for (axis, tex) in [(0usize, tex_w), (1usize, tex_h)] {
|
||
if tex < 2.0 {
|
||
continue; // a 1-texel axis has no interior
|
||
}
|
||
let (lo, hi) = uvs.iter().fold((f32::MAX, f32::MIN), |(lo, hi), c| {
|
||
(lo.min(c[axis]), hi.max(c[axis]))
|
||
});
|
||
// A range of exactly 1, the corners' `v`, is bounded and gets the inset.
|
||
if hi - lo > 1.0 + f32::EPSILON {
|
||
continue;
|
||
}
|
||
let half = 0.5 / tex;
|
||
if hi - lo <= 2.0 * half {
|
||
continue; // degenerate: the inset would invert the range
|
||
}
|
||
for c in &mut out {
|
||
c[axis] += if c[axis] <= lo { half } else { -half };
|
||
}
|
||
}
|
||
out
|
||
}
|
||
|
||
/// Axis-aligned corners `[TL, TR, BR, BL]`, y-up.
|
||
fn aa_corners(x0: f32, x1: f32, y_bottom: f32, y_top: f32) -> [[f32; 2]; 4] {
|
||
[
|
||
[x0, y_top], // TL
|
||
[x1, y_top], // TR
|
||
[x1, y_bottom], // BR
|
||
[x0, y_bottom], // BL
|
||
]
|
||
}
|
||
|
||
/// The drawable pieces for a frame's resolved rect (y-up), in paint order: the background (layer
|
||
/// `BACKGROUND`), then the 8 border pieces (layer `BORDER`). Geometry `0x77e8d0`, UVs `0x77f0c0`.
|
||
pub fn pieces(frame: Rect, bd: &Backdrop) -> Vec<BackdropPiece> {
|
||
let mut out = Vec::with_capacity(9);
|
||
let e = bd.edge_size;
|
||
let (l, r, b, t) = (frame.left, frame.right, frame.bottom, frame.top);
|
||
|
||
// ── The background piece ──
|
||
if bd.has_bg() {
|
||
let (il, ir, it, ib) = (
|
||
bd.insets.left,
|
||
bd.insets.right,
|
||
bd.insets.top,
|
||
bd.insets.bottom,
|
||
);
|
||
// The four anchors, y-up. Bottom-right takes the top inset, as the reference does
|
||
// (`0x77e9ae` loads `+0x50`, top, not `+0x54`); invisible when the two are equal.
|
||
let corners = [
|
||
[l + il, t - it],
|
||
[r - ir, t - it],
|
||
[r - ir, b + it], // BR: the top inset
|
||
[l + il, b + ib],
|
||
];
|
||
// Untiled, no `SetTexCoord` runs and the background stretches over `[0,1]` (`0x77f0c0`).
|
||
let uvs = if bd.tile {
|
||
let period = if bd.tile_size != 0.0 { bd.tile_size } else { e };
|
||
let bg_w = (r - ir) - (l + il);
|
||
let bg_h = (t - it) - (b + ib);
|
||
let (wt, ht) = (bg_w / period, bg_h / period);
|
||
[[0.0, 0.0], [wt, 0.0], [wt, ht], [0.0, ht]]
|
||
} else {
|
||
[[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]]
|
||
};
|
||
out.push(BackdropPiece {
|
||
corners,
|
||
uvs,
|
||
tile: bd.tile,
|
||
is_bg: true,
|
||
});
|
||
}
|
||
|
||
// ── The 8 border pieces ──
|
||
if bd.has_border() {
|
||
// Edge strips tile `run` periods between the corners, 0 below two edges (`0x77f0c0`).
|
||
let w_run = ((r - l) / e - 2.0).max(0.0);
|
||
let h_run = ((t - b) / e - 2.0).max(0.0);
|
||
|
||
let mut edge = |x0: f32, x1: f32, y0: f32, y1: f32, uvs: [[f32; 2]; 4]| {
|
||
out.push(BackdropPiece {
|
||
corners: aa_corners(x0, x1, y0, y1),
|
||
uvs,
|
||
tile: true,
|
||
is_bg: false,
|
||
});
|
||
};
|
||
|
||
// LEFT, slice 0, upright.
|
||
edge(
|
||
l,
|
||
l + e,
|
||
b + e,
|
||
t - e,
|
||
[[0.0, 0.0], [SLICE, 0.0], [SLICE, h_run], [0.0, h_run]],
|
||
);
|
||
// RIGHT, slice 1, upright.
|
||
edge(
|
||
r - e,
|
||
r,
|
||
b + e,
|
||
t - e,
|
||
[
|
||
[SLICE, 0.0],
|
||
[2.0 * SLICE, 0.0],
|
||
[2.0 * SLICE, h_run],
|
||
[SLICE, h_run],
|
||
],
|
||
);
|
||
// TOP, slice 2, rotated: atlas `u` runs down the screen, atlas `v` right to left.
|
||
edge(
|
||
l + e,
|
||
r - e,
|
||
t - e,
|
||
t,
|
||
[
|
||
[2.0 * SLICE, w_run],
|
||
[2.0 * SLICE, 0.0],
|
||
[3.0 * SLICE, 0.0],
|
||
[3.0 * SLICE, w_run],
|
||
],
|
||
);
|
||
// BOTTOM, slice 3, rotated as TOP.
|
||
edge(
|
||
l + e,
|
||
r - e,
|
||
b,
|
||
b + e,
|
||
[
|
||
[3.0 * SLICE, w_run],
|
||
[3.0 * SLICE, 0.0],
|
||
[4.0 * SLICE, 0.0],
|
||
[4.0 * SLICE, w_run],
|
||
],
|
||
);
|
||
// The corners, slices 4 to 7, upright.
|
||
let corner_uv = |i: f32| {
|
||
let u0 = i * SLICE;
|
||
let u1 = (i + 1.0) * SLICE;
|
||
[[u0, 0.0], [u1, 0.0], [u1, 1.0], [u0, 1.0]]
|
||
};
|
||
edge(l, l + e, t - e, t, corner_uv(4.0)); // TOPLEFT
|
||
edge(r - e, r, t - e, t, corner_uv(5.0)); // TOPRIGHT
|
||
edge(l, l + e, b, b + e, corner_uv(6.0)); // BOTTOMLEFT
|
||
edge(r - e, r, b, b + e, corner_uv(7.0)); // BOTTOMRIGHT
|
||
}
|
||
|
||
out
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
|
||
/// On the `ChatBubble-Backdrop` geometry: 256×32, eight 32-texel slices.
|
||
#[test]
|
||
fn the_bleed_inset_lands_on_texel_centres_and_spares_tiling() {
|
||
const W: f32 = 256.0;
|
||
const H: f32 = 32.0;
|
||
let half_u = 0.5 / W;
|
||
|
||
let corner = [[0.5, 0.0], [0.625, 0.0], [0.625, 1.0], [0.5, 1.0]];
|
||
let got = inset_atlas_bleed(corner, W, H);
|
||
// 128.5 and 159.5 are the centres of the slice's first and last texels.
|
||
let us: Vec<f32> = got.iter().map(|c| c[0] * W).collect();
|
||
assert!((us[0] - 128.5).abs() < 1e-3, "left u at {} texels", us[0]);
|
||
assert!((us[1] - 159.5).abs() < 1e-3, "right u at {} texels", us[1]);
|
||
let vs: Vec<f32> = got.iter().map(|c| c[1] * H).collect();
|
||
assert!((vs[0] - 0.5).abs() < 1e-3, "top v at {} texels", vs[0]);
|
||
assert!((vs[2] - 31.5).abs() < 1e-3, "bottom v at {} texels", vs[2]);
|
||
|
||
let strip = [[0.25, 8.0], [0.25, 0.0], [0.375, 0.0], [0.375, 8.0]];
|
||
let got = inset_atlas_bleed(strip, W, H);
|
||
for (g, s) in got.iter().zip(strip.iter()) {
|
||
assert_eq!(g[1], s[1], "the tiling axis must be bit-exact");
|
||
}
|
||
assert!((got[0][0] - (0.25 + half_u)).abs() < 1e-6);
|
||
assert!((got[2][0] - (0.375 - half_u)).abs() < 1e-6);
|
||
|
||
for i in 0u8..8 {
|
||
let (u0, u1) = (f32::from(i) * SLICE, f32::from(i + 1) * SLICE);
|
||
let piece = [[u0, 0.0], [u1, 0.0], [u1, 1.0], [u0, 1.0]];
|
||
for c in inset_atlas_bleed(piece, W, H) {
|
||
assert!(
|
||
c[0] > u0 && c[0] < u1,
|
||
"slice {i}: u {} escaped ({u0},{u1})",
|
||
c[0]
|
||
);
|
||
}
|
||
}
|
||
|
||
assert_eq!(inset_atlas_bleed(corner, 0.0, 0.0), corner);
|
||
let sliver = [[0.0, 0.0], [0.001, 0.0], [0.001, 1.0], [0.0, 1.0]];
|
||
assert_eq!(
|
||
inset_atlas_bleed(sliver, W, H)[0][0],
|
||
0.0,
|
||
"too thin to inset"
|
||
);
|
||
}
|
||
|
||
fn tooltip_backdrop(edge_size: f32, inset: f32) -> Backdrop {
|
||
Backdrop {
|
||
bg_file: Some("Interface\\Tooltips\\UI-Tooltip-Background".into()),
|
||
edge_file: Some("Interface\\Tooltips\\UI-Tooltip-Border".into()),
|
||
tile: true,
|
||
tile_size: edge_size,
|
||
edge_size,
|
||
insets: Insets {
|
||
left: inset,
|
||
right: inset,
|
||
top: inset,
|
||
bottom: inset,
|
||
},
|
||
..Default::default()
|
||
}
|
||
}
|
||
|
||
// The tooltip's shape on a 200×100 frame (`Rect::new` takes bottom, left, top, right).
|
||
#[test]
|
||
fn piece_geometry_200x100_edge16_inset5() {
|
||
let frame = Rect::new(0.0, 0.0, 100.0, 200.0);
|
||
let bd = tooltip_backdrop(16.0, 5.0);
|
||
let ps = pieces(frame, &bd);
|
||
assert_eq!(ps.len(), 9);
|
||
|
||
// Symmetric insets hide the bottom-right quirk.
|
||
let bg = ps[0];
|
||
assert!(bg.is_bg);
|
||
assert_eq!(bg.corners[0], [5.0, 95.0]); // TL
|
||
assert_eq!(bg.corners[1], [195.0, 95.0]); // TR
|
||
assert_eq!(bg.corners[2], [195.0, 5.0]); // BR
|
||
assert_eq!(bg.corners[3], [5.0, 5.0]); // BL
|
||
assert_eq!(bg.uvs[0], [0.0, 0.0]);
|
||
assert!((bg.uvs[1][0] - 190.0 / 16.0).abs() < 1e-4);
|
||
assert!((bg.uvs[2][1] - 90.0 / 16.0).abs() < 1e-4);
|
||
|
||
// Border pieces 1 to 8: LEFT, RIGHT, TOP, BOTTOM, TL, TR, BL, BR, flush inside the frame.
|
||
let left = ps[1];
|
||
assert!(!left.is_bg && left.tile);
|
||
assert_eq!(
|
||
left.corners,
|
||
[[0.0, 84.0], [16.0, 84.0], [16.0, 16.0], [0.0, 16.0]]
|
||
);
|
||
// h_run = 100/16 - 2 = 4.25.
|
||
assert_eq!(left.uvs[0], [0.0, 0.0]);
|
||
assert_eq!(left.uvs[1], [0.125, 0.0]);
|
||
assert!((left.uvs[3][1] - 4.25).abs() < 1e-4);
|
||
|
||
// TOP, rotated; w_run = 200/16 - 2 = 10.5.
|
||
let top = ps[3];
|
||
assert_eq!(top.corners[0], [16.0, 100.0]); // TL
|
||
assert_eq!(top.corners[2], [184.0, 84.0]); // BR
|
||
assert_eq!(top.uvs[0][0], 0.25); // TL u
|
||
assert_eq!(top.uvs[3][0], 0.375); // BL u
|
||
assert!((top.uvs[0][1] - 10.5).abs() < 1e-4); // TL v = w_run
|
||
assert_eq!(top.uvs[1][1], 0.0); // TR v = 0
|
||
|
||
// BOTTOMRIGHT, slice 7, upright.
|
||
let br = ps[8];
|
||
assert_eq!(br.corners[0], [184.0, 16.0]); // TL
|
||
assert_eq!(br.uvs[0], [0.875, 0.0]);
|
||
assert_eq!(br.uvs[2], [1.0, 1.0]);
|
||
}
|
||
|
||
#[test]
|
||
fn bg_bottomright_rides_top_inset_bug() {
|
||
let frame = Rect::new(0.0, 0.0, 100.0, 200.0);
|
||
let bd = Backdrop {
|
||
insets: Insets {
|
||
left: 3.0,
|
||
right: 4.0,
|
||
top: 7.0,
|
||
bottom: 9.0,
|
||
},
|
||
..tooltip_backdrop(16.0, 0.0)
|
||
};
|
||
let bg = pieces(frame, &bd)[0];
|
||
assert_eq!(bg.corners[3], [3.0, 9.0]);
|
||
// BR takes the top inset, 7, not the bottom's 9.
|
||
assert_eq!(bg.corners[2], [196.0, 7.0]);
|
||
assert_eq!(bg.corners[0], [3.0, 93.0]); // TL
|
||
assert_eq!(bg.corners[1], [196.0, 93.0]); // TR
|
||
}
|
||
|
||
#[test]
|
||
fn run_clamps_to_zero_below_two_edges() {
|
||
// 20/16 - 2 = -0.75.
|
||
let frame = Rect::new(0.0, 0.0, 20.0, 20.0);
|
||
let bd = tooltip_backdrop(16.0, 5.0);
|
||
let ps = pieces(frame, &bd);
|
||
let left = ps[1]; // LEFT edge
|
||
assert_eq!(left.uvs[3][1], 0.0, "h_run clamped to 0"); // BL v
|
||
let top = ps[3]; // TOP edge
|
||
assert_eq!(top.uvs[0][1], 0.0, "w_run clamped to 0"); // TL v
|
||
}
|
||
|
||
#[test]
|
||
fn presence_gates_pieces() {
|
||
let frame = Rect::new(0.0, 0.0, 100.0, 100.0);
|
||
assert!(pieces(frame, &Backdrop::default()).is_empty());
|
||
let bg_only = Backdrop {
|
||
bg_file: Some("bg".into()),
|
||
..Default::default()
|
||
};
|
||
assert_eq!(pieces(frame, &bg_only).len(), 1);
|
||
let border_only = Backdrop {
|
||
edge_file: Some("edge".into()),
|
||
..Default::default()
|
||
};
|
||
assert_eq!(pieces(frame, &border_only).len(), 8);
|
||
}
|
||
}
|