Every <OnLoad> in the interface read text widths as 0

`seat_text_measurer`'s own doc says what it is for: *"so `GetStringWidth` right after `SetText`
measures, as the reference answers it inline (`0x79e510` → `0x772890`). Also seated before the
manifest loads, for every `<OnLoad>`."* It was not seated. `UiFontAtlas` is inserted by an
`Update` system through `Commands`, so it appears at the next command flush — and the interface
load runs on the `InWorld` state transition, which comes first. `seat_raster_seam_for_load` then
took its `else { return }` and the VM went through the whole load with **no text measurer at
all**, which `ensure_measured` answers by returning at its first line.

So for the entire UI load — all of FrameXML and every addon — `FontString:GetWidth()`,
`GetHeight()` and `GetStringWidth()` answered **0** for a string that plainly had text. Measured
on grom's install: dozens of them, including every unit-popup entry, every `StaticPopup` button
and the four radio labels this was found through.

What it costs is any `<OnLoad>` that lays something out from a label's width, which is an ordinary
thing for an addon to do. `AdvancedTradeSkillWindow2.lua:1642` puts its Name radio button at
`ATSWDifficultySortCheckboxText2:GetWidth() + 5` to the right of the Difficulty one: the stored
offset came out **5.0** where it should be 53.8, so "Difficulty" was drawn across "Category" 37 px
to its left. grom, 2026-11-05: *"its loading now, but barely functioning."* Re-running
`ATSW_OnLoad()` by hand from a live session gave 53.8 — the same code, the same frame, a measurer
away.

`ensure_font_atlas(&mut World)` is `init`'s body for a caller that holds the world, called from
`seat_raster_seam_for_load`; `init` keeps its own path for every later frame. A world that still
cannot build one (no chain, no window) now warns instead of loading the interface in silence.

Verified: the ATSW2 offset is 53.8 at load, and a walk of the whole loaded interface finds **0**
FontStrings that have text and measure 0 (dozens before). benilla-app 4243 pass / 12 fail, the
recorded baseline with an identical name set.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
This commit is contained in:
2026-11-05 11:48:29 -07:00
co-authored by Claude Opus 5
parent 6306fed1ca
commit 8c6fb4e425
4 changed files with 56 additions and 2 deletions
@@ -738,6 +738,16 @@ fn seat_raster_seam_for_load(world: &mut World, script: &mut UiScript) {
// out yet.
script.set_screen_size(w / s, h / s);
}
// Built here if it is not built yet: the atlas's own `init` is an `Update` system inserting
// through `Commands`, and this runs on the `InWorld` transition, which comes first — see
// `ui_text::engine::gpu::ensure_font_atlas` for what a missing measurer costs the load.
if !crate::ui_text::ensure_font_atlas(world) {
warn!(
"ui_script: no font atlas at world entry — every GetStringWidth during this UI load \
will answer 0, and any `<OnLoad>` laying out from a label's width will be wrong"
);
return;
}
let Some(atlas) = world.get_resource::<crate::ui_text::UiFontAtlas>() else {
return;
};
@@ -77,6 +77,50 @@ fn init(
});
}
/// [`init`] for a caller that holds the world, which is how the UI load reaches it.
///
/// [`init`] is an `Update` system that inserts through `Commands`, so the resource appears at the
/// next command flush — and the interface load runs on the `InWorld` state transition, which can
/// and does come first. The cost of missing it is not cosmetic: with no atlas there is no text
/// measurer, so for the whole of the UI load **every `GetStringWidth` and every FontString's
/// `GetWidth` answers 0**, where the reference measures inline and always knows (`0x79e510` →
/// `0x772890`). Every `<OnLoad>` in FrameXML and in every addon that lays something out from a
/// label's width reads a zero — `AdvancedTradeSkillWindow2.lua:1642` puts its Name radio button
/// `ATSWDifficultySortCheckboxText2:GetWidth() + 5` to the right of the Difficulty one and so
/// stacked the two labels on top of each other, 5 px apart instead of 53.8.
///
/// Returns whether an atlas is in the world afterwards.
pub(crate) fn ensure_font_atlas(world: &mut World) -> bool {
if world.contains_resource::<UiFontAtlas>() {
return true;
}
let scale_factor = {
let mut q = world.query_filtered::<&Window, With<PrimaryWindow>>();
match q.single(world) {
Ok(window) => window.scale_factor(),
Err(_) => return false,
}
};
let engine = {
let Some(world_assets) = world.get_resource::<WorldAssets>() else {
return false;
};
let Some(images) = world.get_resource::<Assets<Image>>() else {
return false;
};
match TextEngine::load(world_assets, images, scale_factor) {
Some(engine) => engine,
None => return false,
}
};
world.insert_resource(UiFontAtlas {
engine: Arc::new(Mutex::new(engine)),
generation: 0,
ellipsis: crate::ui_text::EllipsisMemo::default(),
});
true
}
/// The frame boundary: follow the window's DPI, carry out a pending reset, create the sheet's
/// texture once, and hand this frame's cells to the render world. A reset waits for this boundary
/// so no UV moves while quads that use it are still being pushed.
+1 -1
View File
@@ -37,7 +37,7 @@ use cosmic_text::{
use benilla_assets::{LockRecover, WorldAssets};
use faces::{hhea_ascent_ratio, register_font, CLIENT_FONTS};
pub(crate) use gpu::UiTextPlugin;
pub(crate) use gpu::{ensure_font_atlas, UiTextPlugin};
use super::outline::outlined_cell;
use super::pack::{Cell, Sheet};
+1 -1
View File
@@ -20,7 +20,7 @@ mod measurer;
mod outline;
mod pack;
pub(crate) use engine::{UiFontAtlas, UiTextPlugin};
pub(crate) use engine::{ensure_font_atlas, UiFontAtlas, UiTextPlugin};
use benilla_ui::script::Outline;
use benilla_ui::widget::RegionHandle;