1.12 registers no `GetStringHeight` (`script/button.rs`: `0x782390` reads the label's height instead) and ClassicAPI backports it, so `compat/classic_api/frame.rs` is its home. That file had it on its own not-built list under "needs per-line layout facts the host measurer is not asked for today" — true of `GetNumLines` and `GetWrappedWidth`, and not of this one: the whole-string height is the other half of the number `GetStringWidth` already reads. `measure::current_measure` now owns the staleness rule for both, so the two cannot disagree about when a measure has gone stale, and a changed string still reads 0 rather than the previous string's height. It is not cosmetic. AceGUI-3.0 calls it **unguarded** — `AceGUIWidget-Label.lua:54` is `height = label:GetStringHeight()` and `AceGUIWidget-CheckBox.lua:90` is `self:SetHeight(28 + self.desc:GetStringHeight())` — so every Ace options panel on this install drew its labels and checkbox descriptions at the library's own "avoid zero-height labels" fallback of one pixel: a window with all of its prose invisible. `addon_harness --probe AI_VoiceOver` builds that panel's full 32-widget tree with zero errors and every `Label` at `624x1`, which is what "can't see the actual options in VoiceOver" was. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
117 lines
4.9 KiB
Rust
117 lines
4.9 KiB
Rust
//! The synchronous text measure: the host's font engine installed into the VM, so
|
|
//! `GetStringWidth` answers inside the Lua call that asked, as the reference's does (`0x79e510`
|
|
//! calls its font engine, `0x772890`). UI code measures in the tick it sets text
|
|
//! (`MoneyFrame.lua:202`). With no measurer installed, metrics stay 0 until the
|
|
//! [`MeasureRequest`](super::MeasureRequest) round-trip fills them a frame later.
|
|
|
|
use mlua::Lua;
|
|
|
|
use super::{MeasureRequest, MeasuredText, Model};
|
|
use crate::widget::RegionHandle;
|
|
|
|
/// The host's font engine as the VM sees it. It must answer with the same code as the batch
|
|
/// round-trip, or a string's width depends on when it was asked for.
|
|
pub trait TextMeasure {
|
|
/// `(laid_out_w, laid_out_h, natural_w)` for `req` in frame-local units; `natural_w`, the
|
|
/// unwrapped width, is what `GetStringWidth` reports. It runs with the model mutably borrowed,
|
|
/// so it must not re-enter Lua.
|
|
fn measure(&mut self, req: &MeasureRequest) -> (f32, f32, f32);
|
|
}
|
|
|
|
/// This FontString's measure as it stands *right now*, measuring first when a host engine is
|
|
/// installed ([`ensure_measured`]); `None` when nothing has been measured under the key the
|
|
/// region's current text, font and width make — which every metric read answers as 0, never as
|
|
/// the previous string's number.
|
|
///
|
|
/// One owner for that staleness rule, because two readers want the same measure for different
|
|
/// halves of it: the stock `GetStringWidth` takes `natural_w` (`region/text.rs`) and ClassicAPI's
|
|
/// `GetStringHeight` takes `h` (`compat/classic_api/frame.rs`).
|
|
pub(super) fn current_measure(lua: &Lua, rh: RegionHandle) -> Option<MeasuredText> {
|
|
ensure_measured(lua, rh);
|
|
let model = lua.app_data_ref::<Model>().expect("model app_data");
|
|
let d = model.region_data.get(&rh)?;
|
|
// The measure is keyed by the drawn size, so the owner's scale is part of the key.
|
|
let scale = model
|
|
.arena
|
|
.region(rh)
|
|
.and_then(|r| model.arena.frame(r.owner))
|
|
.map(|f| f.effective_scale)
|
|
.unwrap_or(1.0);
|
|
d.measured.filter(|m| m.key == d.measure_key(scale))
|
|
}
|
|
|
|
/// Measure region `rh` now if a measurer is installed and its stored measure is stale; a current
|
|
/// one is left alone, so a per-frame poll does not re-measure.
|
|
pub(super) fn ensure_measured(lua: &Lua, rh: RegionHandle) {
|
|
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
|
|
if model.measurer.is_none() {
|
|
return;
|
|
}
|
|
let Some(req) = super::layout::measure_request_for(&mut model, rh) else {
|
|
return; // not a FontString, no text, or already current
|
|
};
|
|
// Taken out and put back: the measurer needs `&mut self` while the model is borrowed, and it
|
|
// cannot re-enter the VM, so nothing sees the gap.
|
|
let mut engine = model.measurer.take().expect("checked above");
|
|
let (w, h, natural_w) = engine.measure(&req);
|
|
model.measurer = Some(engine);
|
|
let new = MeasuredText {
|
|
w,
|
|
h,
|
|
natural_w,
|
|
key: req.key,
|
|
};
|
|
let mut moved = false;
|
|
if let Some(d) = model.region_data.get_mut(&rh) {
|
|
moved = super::types::MeasuredText::layout_moved(d.measured, new);
|
|
// The key always lands: a stale key re-requests forever.
|
|
d.measured = Some(new);
|
|
}
|
|
if moved {
|
|
// Only a box that moved touches the layout, as in `set_measured_text`: a same-width
|
|
// re-measure (a countdown tick) must not cost a whole-roster hash.
|
|
model.touch_layout_region(rh);
|
|
}
|
|
}
|
|
|
|
impl super::UiScript {
|
|
/// Install the host's font engine, so metric reads answer inside the tick that asked. Replace
|
|
/// it whenever the window size or `uiScale` changes: glyph advances snap to whole physical
|
|
/// pixels.
|
|
pub fn set_text_measurer(&mut self, measurer: Box<dyn TextMeasure>) {
|
|
self.model_mut().measurer = Some(measurer);
|
|
}
|
|
|
|
/// Whether a [`TextMeasure`] is installed; the host then skips its batch measure pass.
|
|
pub fn has_text_measurer(&self) -> bool {
|
|
self.model_mut().measurer.is_some()
|
|
}
|
|
|
|
/// Answer every pending FontString measure inline, so a box is right in the frame its text
|
|
/// was set; returns whether anything changed.
|
|
pub(super) fn fill_measures(&mut self) -> bool {
|
|
if !self.has_text_measurer() {
|
|
return false;
|
|
}
|
|
let requests = self.fontstrings_needing_measure();
|
|
if requests.is_empty() {
|
|
return false;
|
|
}
|
|
let answers: Vec<(u32, f32, f32, f32, u64)> = {
|
|
let mut model = self.model_mut();
|
|
let mut engine = model.measurer.take().expect("checked above");
|
|
let answers = requests
|
|
.iter()
|
|
.map(|r| {
|
|
let (w, h, natural_w) = engine.measure(r);
|
|
(r.id, w, h, natural_w, r.key)
|
|
})
|
|
.collect();
|
|
model.measurer = Some(engine);
|
|
answers
|
|
};
|
|
self.set_measured_text(&answers);
|
|
true
|
|
}
|
|
}
|