A multi-line EditBox grows with its text; a Lua-made box draws its text (#729)
A multi-line EditBox kept its authored height, so a scroll frame around a long mail body or ticket had no range. A box made in Lua with no template never placed its text, so it drew nothing. In 1.12.1, the flush's relayout sets a multi-line box's height to its insets plus its wrapped text height, or one line when empty (`0x77d4d0` at `0x77d8ad`). Every resize, including the first layout, re-seats the text at the top-left inset (`0x77a8d0` → `0x77b8c0`). `SetTextInsets`, `GetTextInsets`, `SetMultiLine` and a font change re-seat it too. benilla now does the same. A focused multi-line box's caret and text are clipped only by a scroll frame, as the reference clips neither. Tests cover growth and shrink, the stock mail and ticket scroll ranges, each re-seat trigger, a Lua box's first layout, and the caret in an empty box and after a trailing newline. Fixes #722 Fixes #726 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
4c18c23c94
commit
f2e71ccebd
@@ -107,8 +107,13 @@ pub(super) fn emit(
|
||||
let (x0, top, cell_h) =
|
||||
crate::ui_text::line_origin(&mut atlas.lock(), drawn, host.rect, draw_justify, spec);
|
||||
ebox_geom = Some((x0, top, cell_h));
|
||||
text_clip = Some(host.clip.map_or(host.rect, |c| c.intersect(host.rect)));
|
||||
// A single-line box shows only what fits its text region (`0x77d858` sets the window's
|
||||
// substring), so the whole string drawn here is cut at the region. A multi-line region is
|
||||
// only as tall as its text, and the reference clips neither it nor the caret texture
|
||||
// (`E+0x368`), which after a trailing break stands a line below it: only a ScrollFrame's
|
||||
// clip applies.
|
||||
if !ui.multi_line {
|
||||
text_clip = Some(host.clip.map_or(host.rect, |c| c.intersect(host.rect)));
|
||||
draw_text = drawn;
|
||||
draw_rect = Rect::new(x0, host.rect.min.y, x0 + 100_000.0, host.rect.max.y);
|
||||
draw_justify = crate::ui_text::Justify {
|
||||
@@ -419,3 +424,133 @@ mod tests {
|
||||
assert_eq!(gaps, [1, 2].into_iter().collect());
|
||||
}
|
||||
}
|
||||
|
||||
/// The focused multi-line box's caret through the whole UI pass, on the client faces: the region
|
||||
/// that draws its text is only as tall as that text, and the caret must not be cut to it.
|
||||
#[cfg(test)]
|
||||
mod caret_tests {
|
||||
use bevy::prelude::*;
|
||||
use bevy::window::PrimaryWindow;
|
||||
|
||||
use benilla_ui::script::UiScript;
|
||||
|
||||
use super::super::{paint_script, tick_script, UiQuad, UiQuads};
|
||||
use crate::portrait::PortraitImages;
|
||||
use crate::ui_text::UiFontAtlas;
|
||||
|
||||
/// A focused 270-wide multi-line box at 14 px on a 1024×768 window (`s = 1`), holding `text`.
|
||||
fn app_with_focused_box(text: &str) -> App {
|
||||
let script = UiScript::new().unwrap();
|
||||
script
|
||||
.run(&format!(
|
||||
r#"
|
||||
E = CreateFrame("EditBox", "E")
|
||||
E:SetPoint("TOPLEFT", 100, -100)
|
||||
E:SetWidth(270); E:SetHeight(200)
|
||||
E:SetFont("Fonts\\FRIZQT__.TTF", 14)
|
||||
E:SetMultiLine(true)
|
||||
E:SetFocus()
|
||||
E:SetText({text:?})
|
||||
"#
|
||||
))
|
||||
.unwrap();
|
||||
let mut app = App::new();
|
||||
app.insert_non_send_resource(script);
|
||||
app.insert_resource(UiFontAtlas::for_test(1.0).expect("the client faces open"));
|
||||
app.init_resource::<UiQuads>();
|
||||
app.init_resource::<crate::ui_script::UiPassState>();
|
||||
app.init_resource::<Assets<Image>>();
|
||||
app.init_resource::<PortraitImages>();
|
||||
app.init_resource::<crate::portrait::BoothPanes>();
|
||||
app.init_resource::<crate::ui_models::UiModelTiles>();
|
||||
app.init_resource::<crate::minimap::MinimapWidget>();
|
||||
app.init_resource::<crate::ui_script::UiFrameCost>();
|
||||
app.init_resource::<crate::ui_script::UiCostWanted>();
|
||||
app.init_resource::<Time>();
|
||||
app.init_resource::<Time<Real>>();
|
||||
app.init_resource::<crate::ui_script::UiClock>();
|
||||
app.init_resource::<crate::ui_script::UiScaleCvar>();
|
||||
app.world_mut().spawn((
|
||||
Window {
|
||||
resolution: UVec2::new(1024, 768).into(),
|
||||
..default()
|
||||
},
|
||||
PrimaryWindow,
|
||||
));
|
||||
app.add_systems(Update, (tick_script, paint_script).chain());
|
||||
// The box seats and sizes itself, the host answers the advances, then the caret draws.
|
||||
for _ in 0..4 {
|
||||
app.update();
|
||||
}
|
||||
app
|
||||
}
|
||||
|
||||
/// The caret: the one untextured white quad.
|
||||
fn caret(app: &App) -> UiQuad {
|
||||
let quads = &app.world().resource::<UiQuads>().quads;
|
||||
let mut carets = quads
|
||||
.iter()
|
||||
.filter(|q| q.texture.is_none() && q.color[..3] == [1.0, 1.0, 1.0]);
|
||||
let c = carets
|
||||
.next()
|
||||
.expect("the focused box draws its caret")
|
||||
.clone();
|
||||
assert!(carets.next().is_none(), "one caret");
|
||||
c
|
||||
}
|
||||
|
||||
/// Whole and visible: a line tall, and no clip cuts any of it.
|
||||
fn assert_whole(c: &UiQuad) {
|
||||
assert!(c.rect.height() >= 10.0, "a line tall: {:?}", c.rect);
|
||||
if let Some(clip) = c.clip {
|
||||
assert_eq!(
|
||||
clip.intersect(c.rect),
|
||||
c.rect,
|
||||
"the clip cuts the caret: {clip:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_empty_focused_multi_line_box_draws_its_whole_caret() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let app = app_with_focused_box("");
|
||||
assert_whole(&caret(&app));
|
||||
}
|
||||
|
||||
/// After a trailing break the caret stands on the line the break opened, below the text.
|
||||
#[test]
|
||||
fn a_caret_after_a_trailing_newline_is_drawn_whole_below_the_text() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let one = caret(&app_with_focused_box("abc"));
|
||||
let after = caret(&app_with_focused_box("abc\n"));
|
||||
assert_whole(&after);
|
||||
assert_eq!(
|
||||
after.rect.min.y,
|
||||
one.rect.min.y + one.rect.height(),
|
||||
"one line down: {:?} against {:?}",
|
||||
after.rect,
|
||||
one.rect
|
||||
);
|
||||
}
|
||||
|
||||
/// The last line's descenders hang below its measured line, as a drop shadow would: no clip
|
||||
/// cuts them.
|
||||
#[test]
|
||||
fn a_focused_multi_line_boxs_last_line_keeps_its_descenders() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let app = app_with_focused_box("gjpqy");
|
||||
let quads = &app.world().resource::<UiQuads>().quads;
|
||||
let glyphs: Vec<&UiQuad> = quads.iter().filter(|q| q.texture.is_some()).collect();
|
||||
assert_eq!(glyphs.len(), 5, "the five glyphs draw");
|
||||
for g in glyphs {
|
||||
if let Some(clip) = g.clip {
|
||||
assert_eq!(
|
||||
clip.intersect(g.rect),
|
||||
g.rect,
|
||||
"a clip cuts a glyph: {clip:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -187,3 +187,53 @@ fn toggling_the_window_opens_it_and_asks_for_the_queue_status() {
|
||||
s.run("ToggleHelpFrame()").unwrap();
|
||||
assert!(!s.eval::<bool>("return HelpFrame:IsVisible()").unwrap());
|
||||
}
|
||||
|
||||
/// The ticket text is a multi-line box and the scroll child itself (`HelpFrame.xml`, 541×357 in
|
||||
/// the 378-tall `HelpFrameOpenTicketScrollFrame`): the box sizes itself to its wrapped text
|
||||
/// (`0x77d4d0` @`0x77d8ad`), so `ScrollingEdit_OnTextChanged`'s recompute ranges over all of it.
|
||||
#[test]
|
||||
fn the_ticket_text_grows_with_its_lines_and_its_scroll_frame_ranges_over_them() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = setup();
|
||||
// Wide glyphs and the stand-in's 12-unit line: the box's 500-letter cap must still wrap
|
||||
// past the frame.
|
||||
const GLYPH: f64 = 60.0;
|
||||
s.set_text_measurer(Box::new(super::FixedWidthFont(GLYPH as f32)));
|
||||
s.run("ShowUIPanel(HelpFrame) HelpFrame_ShowFrame(\"OpenTicket\")")
|
||||
.unwrap();
|
||||
s.resolve();
|
||||
s.tick(0.016);
|
||||
s.resolve();
|
||||
assert_eq!(
|
||||
s.eval::<f64>("return HelpFrameOpenTicketText:GetHeight()")
|
||||
.unwrap(),
|
||||
12.0,
|
||||
"empty, the box is one line: the authored 357 lasts until the first flush"
|
||||
);
|
||||
|
||||
s.run(r#"HelpFrameOpenTicketText:SetText(string.rep("x", 400))"#)
|
||||
.unwrap();
|
||||
s.tick(0.016);
|
||||
s.resolve();
|
||||
// Wrapped at the box's own 541: no insets.
|
||||
let height = (400.0 * GLYPH / 541.0).ceil() * 12.0;
|
||||
assert_eq!(
|
||||
s.eval::<f64>("return HelpFrameOpenTicketText:GetHeight()")
|
||||
.unwrap(),
|
||||
height
|
||||
);
|
||||
assert_eq!(
|
||||
s.eval::<f64>("return HelpFrameOpenTicketScrollFrame:GetVerticalScrollRange()")
|
||||
.unwrap(),
|
||||
height - 378.0
|
||||
);
|
||||
assert_eq!(
|
||||
s.eval::<f64>(
|
||||
"local _, max = HelpFrameOpenTicketScrollFrameScrollBar:GetMinMaxValues() return max"
|
||||
)
|
||||
.unwrap(),
|
||||
height - 378.0,
|
||||
"the range reached the scroll bar through OnScrollRangeChanged"
|
||||
);
|
||||
assert!(s.errors().is_empty(), "{:?}", s.errors());
|
||||
}
|
||||
|
||||
@@ -653,6 +653,18 @@ pub(super) fn test_engine(dpi: f32) -> Option<TextEngine> {
|
||||
#[cfg(test)]
|
||||
pub(super) const TEST_FACES: &[&str] = CLIENT_FONTS;
|
||||
|
||||
#[cfg(test)]
|
||||
impl UiFontAtlas {
|
||||
/// [`test_engine`] as the app holds it, for a host test that draws text; `None` as there.
|
||||
pub(crate) fn for_test(dpi: f32) -> Option<Self> {
|
||||
Some(UiFontAtlas {
|
||||
engine: Arc::new(Mutex::new(test_engine(dpi)?)),
|
||||
generation: 0,
|
||||
ellipsis: crate::ui_text::EllipsisMemo::default(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod differential_tests {
|
||||
use super::*;
|
||||
|
||||
@@ -66,3 +66,52 @@ impl TextMeasure for AtlasMeasurer {
|
||||
measure_request(&mut e, self.seam, req)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use benilla_ui::script::UiScript;
|
||||
|
||||
use super::AtlasMeasurer;
|
||||
|
||||
/// A multi-line EditBox is its text's measured height tall (`0x77d4d0` @`0x77d8ad`), one
|
||||
/// line's when empty, and the line law the host measures by (`0x5c2070`) folds a trailing
|
||||
/// break into the line it ends, so the break alone adds no line.
|
||||
#[test]
|
||||
fn a_multi_line_boxs_trailing_newline_adds_no_line() {
|
||||
benilla_formats::wow_data_or_skip!();
|
||||
let engine = super::super::engine::test_engine(1.0).expect("the client faces open");
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
s.set_text_measurer(Box::new(AtlasMeasurer::new(
|
||||
Arc::new(Mutex::new(engine)),
|
||||
1.0,
|
||||
)));
|
||||
s.run(
|
||||
r#"
|
||||
E = CreateFrame("EditBox", "E")
|
||||
E:SetAutoFocus(false); E:SetMultiLine(true)
|
||||
E:SetPoint("TOPLEFT", 100, -100); E:SetWidth(270); E:SetHeight(200)
|
||||
E:SetFont("Fonts\\MORPHEUS.TTF", 15)
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
s.resolve();
|
||||
let mut height = |text: &str| {
|
||||
s.run(&format!("E:SetText({text:?})")).unwrap();
|
||||
s.tick(0.016);
|
||||
s.resolve();
|
||||
s.eval::<f32>("return E:GetHeight()").unwrap()
|
||||
};
|
||||
let empty = height("");
|
||||
let one = height("abc");
|
||||
assert_eq!(one, empty, "an empty box is one line tall");
|
||||
assert_eq!(height("abc\n"), one, "the break alone");
|
||||
let two = height("abc\nd");
|
||||
assert!(
|
||||
(two - 2.0 * one).abs() < 1e-4,
|
||||
"the first character on the new line: {two} against {one}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,6 @@ use mlua::{Lua, Table, Value};
|
||||
use super::object::frame_handle_of;
|
||||
use super::types::{EditAction, EditUnit};
|
||||
use super::{event, Model, RegionData};
|
||||
use crate::layout::{Anchor, Point};
|
||||
use crate::order::{self, DrawLayer, ZTarget};
|
||||
use crate::widget::{EditBoxState, FrameHandle, FrameKind, KindState, RegionHandle, RegionKind};
|
||||
|
||||
@@ -120,12 +119,14 @@ pub(super) fn action(lua: &Lua, a: EditAction) -> bool {
|
||||
}
|
||||
|
||||
// Mouse, selection, clipboard and blink (`interact`); the host's advance round trip and text
|
||||
// geometry (`seam`).
|
||||
// geometry (`seam`); the text region's rect and a multi-line box's height (`relayout`).
|
||||
mod interact;
|
||||
mod relayout;
|
||||
mod seam;
|
||||
pub(super) use interact::{
|
||||
click, copy_selection, cut_selection, drag_end, drag_update, tick_blink,
|
||||
};
|
||||
pub(in crate::script) use relayout::{relayout_multi_line, reseat_resized};
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────────────────────
|
||||
// Focus model
|
||||
@@ -345,37 +346,22 @@ fn sync_text_region(lua: &Lua, h: FrameHandle) {
|
||||
}
|
||||
}
|
||||
|
||||
/// `SetTextInsets(l, r, t, b)`: store the insets and re-anchor the text region by them.
|
||||
/// `SetTextInsets(l, r, t, b)` (`0x77a6e0`): store the insets and re-seat the text region by
|
||||
/// them (`0x77a707` → `0x77b8c0`).
|
||||
fn set_text_insets(lua: &Lua, h: FrameHandle, l: f32, r: f32, t: f32, b: f32) {
|
||||
if with_eb(lua, h, |eb| eb.text_insets = [l, r, t, b]).is_none() {
|
||||
return;
|
||||
}
|
||||
let Some(rh) = ensure_text_region(lua, h) else {
|
||||
return;
|
||||
};
|
||||
write_inset_anchors(lua, h, rh, [l, r, t, b]);
|
||||
reseat_text_region(lua, h);
|
||||
}
|
||||
|
||||
/// Pin the text region's corners inside the box by the insets (y-up: the top inset moves its top
|
||||
/// down, the bottom inset its bottom up).
|
||||
fn write_inset_anchors(lua: &Lua, h: FrameHandle, rh: RegionHandle, [l, r, t, b]: [f32; 4]) {
|
||||
let owner = frame_id_of(lua, h);
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
|
||||
let pair = [
|
||||
Anchor::new(Point::TopLeft, owner, Point::TopLeft, l, -t),
|
||||
Anchor::new(Point::BottomRight, owner, Point::BottomRight, -r, b),
|
||||
];
|
||||
let data = model.region_data.entry(rh).or_default();
|
||||
let same = data.anchors.len() == 2
|
||||
&& data
|
||||
.anchors
|
||||
.iter()
|
||||
.zip(&pair)
|
||||
.all(|(a, b)| super::object::anchor_bits_eq(a, b));
|
||||
if !same {
|
||||
data.anchors = pair.to_vec();
|
||||
model.touch_layout();
|
||||
/// Run `0x77b8c0` on the box's text region, creating the region's data if nothing has yet.
|
||||
fn reseat_text_region(lua: &Lua, h: FrameHandle) {
|
||||
if ensure_text_region(lua, h).is_none() {
|
||||
return;
|
||||
}
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
|
||||
relayout::seat_text_region(&mut model, h);
|
||||
}
|
||||
|
||||
/// The wrapper for an EditBox's embedded text FontString, built by its ctor (`0x779bee`). The
|
||||
@@ -396,15 +382,15 @@ pub(crate) fn editbox_text_region_wrapper(lua: &Lua, frame: &Table) -> Option<Ta
|
||||
pub(crate) fn adopt_text_region(lua: &Lua, frame: &Table, region: &Table) -> mlua::Result<()> {
|
||||
let h = frame_handle_of(lua, frame)?;
|
||||
let rh = super::region::region_handle_of(lua, region)?;
|
||||
let Some((insets, multi_line)) = with_eb(lua, h, |eb| {
|
||||
let Some(multi_line) = with_eb(lua, h, |eb| {
|
||||
eb.text_region = Some(rh);
|
||||
(eb.text_insets, eb.multi_line)
|
||||
eb.multi_line
|
||||
}) else {
|
||||
return Ok(()); // not an EditBox
|
||||
};
|
||||
write_inset_anchors(lua, h, rh, insets);
|
||||
// Always `Some` text: an empty box still emits its Text quad for the host's caret.
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
|
||||
relayout::seat_text_region(&mut model, h);
|
||||
// Always `Some` text: an empty box still emits its Text quad for the host's caret.
|
||||
let data = model.region_data.entry(rh).or_default();
|
||||
data.text.get_or_insert_with(String::new);
|
||||
apply_text_region_justify(data, multi_line);
|
||||
@@ -414,6 +400,11 @@ pub(crate) fn adopt_text_region(lua: &Lua, frame: &Table, region: &Table) -> mlu
|
||||
/// Get or create the box's text FontString; an XML-declared one is wired by [`adopt_text_region`].
|
||||
pub(super) fn ensure_text_region(lua: &Lua, h: FrameHandle) -> Option<RegionHandle> {
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
|
||||
ensure_text_region_in(&mut model, h)
|
||||
}
|
||||
|
||||
/// [`ensure_text_region`] on a borrowed model.
|
||||
fn ensure_text_region_in(model: &mut Model, h: FrameHandle) -> Option<RegionHandle> {
|
||||
let (existing, multi_line) = match model.arena.frame(h).map(|f| &f.kind_state) {
|
||||
Some(KindState::EditBox(eb)) => (eb.text_region, eb.multi_line),
|
||||
_ => return None,
|
||||
@@ -443,9 +434,10 @@ pub(super) fn ensure_text_region(lua: &Lua, h: FrameHandle) -> Option<RegionHand
|
||||
Some(rh)
|
||||
}
|
||||
|
||||
/// Re-apply the text region's justify for the current `multiLine` flag: LoadXML adopts the
|
||||
/// `<FontString>` before it applies the box's flags.
|
||||
pub(super) fn refresh_text_region_justify(lua: &Lua, this: &Table) -> mlua::Result<()> {
|
||||
/// Re-apply the text region's justify and rect for the current `multiLine` flag, as
|
||||
/// `SetMultiLine` re-seats it (`0x77a63f` → `0x77b8c0`): LoadXML adopts the `<FontString>` before
|
||||
/// it applies the box's flags.
|
||||
pub(super) fn refresh_text_region(lua: &Lua, this: &Table) -> mlua::Result<()> {
|
||||
let h = frame_handle_of(lua, this)?;
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
|
||||
let Some(KindState::EditBox(eb)) = model.arena.frame(h).map(|f| &f.kind_state) else {
|
||||
@@ -455,6 +447,7 @@ pub(super) fn refresh_text_region_justify(lua: &Lua, this: &Table) -> mlua::Resu
|
||||
return Ok(());
|
||||
};
|
||||
apply_text_region_justify(model.region_data.entry(rh).or_default(), multi_line);
|
||||
relayout::seat_text_region(&mut model, h);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -562,6 +555,8 @@ const CARET_WIDTH: f32 = 4.0;
|
||||
mod methods;
|
||||
pub(super) use methods::install;
|
||||
|
||||
#[cfg(test)]
|
||||
mod relayout_tests;
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
|
||||
@@ -7,8 +7,8 @@ use crate::script::object::frame_handle_of;
|
||||
use crate::widget::EditBoxState;
|
||||
|
||||
use super::{
|
||||
clear_focus_handle, highlight_text, insert, set_focus_handle, set_text, set_text_insets,
|
||||
with_eb, REG_EDITBOX_METHODS,
|
||||
clear_focus_handle, highlight_text, insert, reseat_text_region, set_focus_handle, set_text,
|
||||
set_text_insets, with_eb, REG_EDITBOX_METHODS,
|
||||
};
|
||||
|
||||
fn with_editbox<T>(
|
||||
@@ -243,10 +243,13 @@ pub(in crate::script) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
},
|
||||
)?,
|
||||
)?;
|
||||
// GetTextInsets (`0x798c30` → `0x77a710`) copies the four out, then re-seats the text region
|
||||
// (`0x77a73f` → `0x77b8c0`), so the getter puts a script-moved region back.
|
||||
m.set(
|
||||
"GetTextInsets",
|
||||
lua.create_function(|lua, this: Table| {
|
||||
let ins = with_editbox(lua, &this, |eb| eb.text_insets)?;
|
||||
reseat_text_region(lua, frame_handle_of(lua, &this)?);
|
||||
Ok((ins[0], ins[1], ins[2], ins[3]))
|
||||
})?,
|
||||
)?;
|
||||
@@ -287,16 +290,17 @@ pub(in crate::script) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
)?;
|
||||
|
||||
for (name, set) in flag_setters() {
|
||||
let refresh_justify = name == "SetMultiLine";
|
||||
let refresh_text = name == "SetMultiLine";
|
||||
m.set(
|
||||
name,
|
||||
lua.create_function(move |lua, (this, v): (Table, Value)| {
|
||||
let on = !matches!(v, Value::Nil | Value::Boolean(false));
|
||||
with_editbox(lua, &this, |eb| set(eb, on))?;
|
||||
if refresh_justify {
|
||||
// A multi-line box anchors its text TOP, else MIDDLE, and the loader wires the
|
||||
if refresh_text {
|
||||
// A multi-line box seats its text TOP and as tall as the text, a single-line
|
||||
// one MIDDLE between the inset corners, and the loader wires the
|
||||
// `<FontString>` before the flags, so re-seat it.
|
||||
super::refresh_text_region_justify(lua, &this)?;
|
||||
super::refresh_text_region(lua, &this)?;
|
||||
}
|
||||
Ok(())
|
||||
})?,
|
||||
@@ -325,6 +329,12 @@ fn install_font_block(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
super::ensure_text_region(lua, h).ok_or_else(|| mlua::Error::runtime("not an EditBox"))
|
||||
},
|
||||
"EditBox",
|
||||
// The box listens on its font (vtable `0x81c8dc` slot 0, `0x77e2a0`): a change of face or
|
||||
// spacing (mask `0x11`) re-seats its text region (`0x77e2b5` → `0x77b8c0`).
|
||||
Some(|lua, this| {
|
||||
reseat_text_region(lua, frame_handle_of(lua, this)?);
|
||||
Ok(())
|
||||
}),
|
||||
)?;
|
||||
|
||||
// SetJustifyH/SetJustifyV return nothing. An unknown token raises `Usage:` (`0x87c77c`); a
|
||||
|
||||
@@ -0,0 +1,227 @@
|
||||
//! The text region's rect and a multi-line box's own height: `0x77b8c0` seats the FontString by
|
||||
//! the insets, and the flush's relayout `0x77d4d0` sizes a multi-line box to its text.
|
||||
|
||||
use mlua::Lua;
|
||||
|
||||
use crate::layout::{Anchor, Point, Rect};
|
||||
use crate::script::{MeasureRequest, Model};
|
||||
use crate::widget::{FrameHandle, KindState, RegionHandle};
|
||||
|
||||
/// `0x77b8c0`, the text FontString's rect. The reference writes nothing while the box's rect is
|
||||
/// unresolved (`0x77b8c7`); then it clears the points (`0x77b8da`), sets the width
|
||||
/// `boxW − (right + left)` for either kind (`0x77b905`), the height 0 multi-line (`0x77b91e`) or
|
||||
/// `boxH − (top + bottom)` single-line (`0x77b940`), and one `TOPLEFT` point at `(left, −top)`
|
||||
/// (`0x77b96b`).
|
||||
///
|
||||
/// Multi-line, benilla writes that shape, the width once the box has a rect: until then only the
|
||||
/// point is written, and the size is kept.
|
||||
///
|
||||
/// Single-line, benilla pins `TOPLEFT` and `BOTTOMRIGHT` at the inset corners and keeps no
|
||||
/// explicit size. That is a gap: it covers the same rect, but an explicit width here would be a
|
||||
/// wrap width, since benilla's single-line region holds the whole text where the reference's holds
|
||||
/// only the window's substring (`0x77d858`). A script sees the difference: `GetPoint` on the
|
||||
/// region answers two points where the reference answers one, and `GetWidth`/`GetHeight` answer
|
||||
/// the text's measure where the reference answers the explicit size.
|
||||
pub(super) fn seat_text_region(model: &mut Model, h: FrameHandle) {
|
||||
let Some(KindState::EditBox(eb)) = model.arena.frame(h).map(|f| &f.kind_state) else {
|
||||
return;
|
||||
};
|
||||
let (Some(rh), [l, r, t, b], multi_line) = (eb.text_region, eb.text_insets, eb.multi_line)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let owner = model.frame_id(h);
|
||||
let top_left = Anchor::new(Point::TopLeft, owner, Point::TopLeft, l, -t);
|
||||
let (anchors, size) = if multi_line {
|
||||
let width = multi_line_text_width(model, h, [l, r]);
|
||||
(vec![top_left], width.map(|w| Some((w, 0.0))))
|
||||
} else {
|
||||
let bottom_right = Anchor::new(Point::BottomRight, owner, Point::BottomRight, -r, b);
|
||||
(vec![top_left, bottom_right], Some(None))
|
||||
};
|
||||
let data = model.region_data.entry(rh).or_default();
|
||||
let same_anchors = data.anchors.len() == anchors.len()
|
||||
&& data
|
||||
.anchors
|
||||
.iter()
|
||||
.zip(&anchors)
|
||||
.all(|(a, b)| crate::script::object::anchor_bits_eq(a, b));
|
||||
// `None` keeps the size: a multi-line box with no rect yet.
|
||||
let size = size.unwrap_or(data.size);
|
||||
let size_moved = !crate::script::region::size_bits_eq(data.size, size);
|
||||
if !same_anchors {
|
||||
data.anchors = anchors;
|
||||
}
|
||||
data.size = size;
|
||||
if !same_anchors {
|
||||
model.touch_layout();
|
||||
} else if size_moved {
|
||||
model.touch_layout_region(rh);
|
||||
}
|
||||
if size_moved {
|
||||
// The width is the wrap width, which keys the measure.
|
||||
model.touch_measure(rh);
|
||||
}
|
||||
}
|
||||
|
||||
/// The EditBox's `OnSizeChanged` override (`0x77a8d0`, vtable `0x81c910` slot `+0x48`): after the
|
||||
/// base script fire (`0x77a8e0`), `0x77a8e7` re-seats the text region (`0x77b8c0`) on every resize
|
||||
/// of any box. `ApplyRect` (`0x76b580`) calls it when the width or height moved by the resize
|
||||
/// epsilon, the first resolve included, against the ctor's zero rect. `true` when a re-seat moved
|
||||
/// 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;
|
||||
for i in 0..model.arena.editbox_kinds().len() {
|
||||
let h = model.arena.editbox_kinds()[i];
|
||||
let Some(&now) = model.resolved.get(&h) else {
|
||||
continue; // no rect, no `ApplyRect`
|
||||
};
|
||||
let Some(KindState::EditBox(eb)) = model.arena.frame_mut(h).map(|f| &mut f.kind_state)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let before = eb
|
||||
.notified_rect
|
||||
.replace(now)
|
||||
.unwrap_or(Rect::new(0.0, 0.0, 0.0, 0.0));
|
||||
if !crate::layout::size_changed(before, now) {
|
||||
continue;
|
||||
}
|
||||
// The ctor built the text region; its data is seeded here if nothing has touched it.
|
||||
if super::ensure_text_region_in(model, h).is_some() {
|
||||
seat_text_region(model, h);
|
||||
}
|
||||
}
|
||||
model.layout_epoch != epoch
|
||||
}
|
||||
|
||||
/// A multi-line box's text width in its own units, `boxW − (right + left)`, from the rect the
|
||||
/// last resolve gave it; `None` before it has one.
|
||||
fn multi_line_text_width(model: &Model, h: FrameHandle, [l, r]: [f32; 2]) -> Option<f32> {
|
||||
let rect = model.resolved.get(&h)?;
|
||||
Some(rect.width() / crate::script::object::eff_scale(model, h) - (r + l))
|
||||
}
|
||||
|
||||
/// The flush's relayout tail (`0x77d4d0` @`0x77d863`–`0x77d8ad`) for every shown multi-line box,
|
||||
/// run from the tick ahead of the `OnTextChanged` drain, as the flush relayouts before it fires.
|
||||
/// The box's own height becomes `(insetTop + insetBottom) + h`, `h` its text's measured wrapped
|
||||
/// height (`0x7729b0`), or one line's (`0x7727b0(fs, 1)`) when that is exactly 0, as for empty
|
||||
/// text. It reads neither the authored height nor the rect, so the box grows and shrinks with its
|
||||
/// text and a `SetHeight` lasts until the next tick; a single-line box is never sized. The text
|
||||
/// region's width is the one [`seat_text_region`] last wrote, on a resize ([`reseat_resized`]),
|
||||
/// `SetTextInsets`, `GetTextInsets`, `SetMultiLine` or a font change.
|
||||
///
|
||||
/// Each tick relayouts every shown multi-line box, where the reference relayouts a box whose dirty
|
||||
/// bit 0 is set; every input of the height sets it (an edit, a resize, the insets, the font,
|
||||
/// `SetMultiLine`, the ctor), so both write the same height.
|
||||
pub(in crate::script) fn relayout_multi_line(lua: &Lua) {
|
||||
let boxes: Vec<FrameHandle> = {
|
||||
let model = lua.app_data_ref::<Model>().expect("model app_data");
|
||||
// The flush runs from the box's own update, pumped for shown frames only.
|
||||
model
|
||||
.arena
|
||||
.editbox_kinds()
|
||||
.iter()
|
||||
.copied()
|
||||
.filter(|&h| {
|
||||
model.arena.frame(h).is_some_and(|f| {
|
||||
f.effective_visible
|
||||
&& matches!(&f.kind_state, KindState::EditBox(eb) if eb.multi_line)
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
};
|
||||
for h in boxes {
|
||||
let Some(rh) = super::ensure_text_region(lua, h) else {
|
||||
continue;
|
||||
};
|
||||
// `0x7729b0` measures in the call; with no engine installed the measure lands a tick later.
|
||||
crate::script::measure::ensure_measured(lua, rh);
|
||||
let Some(text_h) = text_height(lua, h, rh) else {
|
||||
continue; // the measure is pending: the height waits for it
|
||||
};
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
|
||||
let [_, _, top, bottom] = match model.arena.frame(h).map(|f| &f.kind_state) {
|
||||
Some(KindState::EditBox(eb)) => eb.text_insets,
|
||||
_ => continue,
|
||||
};
|
||||
// `fld top; fadd bottom; fadd st, st(1)`.
|
||||
let height = (top + bottom) + text_h;
|
||||
let input = model.layout_inputs.entry(h).or_default();
|
||||
if input.height.to_bits() != height.to_bits() {
|
||||
input.height = height;
|
||||
model.touch_layout_frame(h);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The text's height for the box: its measure under the current key, or one line's when that is
|
||||
/// exactly 0.0 (`0x77d877` `fcom 0.0` → `0x77d88e`), as it is for empty text, which is never
|
||||
/// measured (`0x7729ff`). `None` while either measure is pending.
|
||||
fn text_height(lua: &Lua, h: FrameHandle, rh: RegionHandle) -> Option<f32> {
|
||||
let measured = {
|
||||
let model = lua.app_data_ref::<Model>().expect("model app_data");
|
||||
let d = model.region_data.get(&rh)?;
|
||||
if d.text.as_deref().is_none_or(str::is_empty) {
|
||||
0.0
|
||||
} else {
|
||||
let key = d.measure_key(owner_scale(&model, h));
|
||||
d.measured.filter(|m| m.key == key)?.h
|
||||
}
|
||||
};
|
||||
if measured == 0.0 {
|
||||
line_height(lua, h, rh)
|
||||
} else {
|
||||
Some(measured)
|
||||
}
|
||||
}
|
||||
|
||||
/// Any text that lays out as one line: its measured height is one line's.
|
||||
const ONE_LINE: &str = "0";
|
||||
|
||||
/// `0x7727b0(fs, 1)`, one line's height in the text region's font: the installed engine's measure
|
||||
/// of one line, the same measure a line of text takes, cached on the box under its key. `None`
|
||||
/// with no engine installed.
|
||||
fn line_height(lua: &Lua, h: FrameHandle, rh: RegionHandle) -> Option<f32> {
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
|
||||
let scale = owner_scale(&model, h);
|
||||
let req = {
|
||||
let d = model.region_data.get(&rh)?;
|
||||
let key = d.measure_key_of(ONE_LINE, scale);
|
||||
if let Some(KindState::EditBox(eb)) = model.arena.frame(h).map(|f| &f.kind_state) {
|
||||
if let Some((k, lh)) = eb.line_height {
|
||||
if k == key {
|
||||
return Some(lh);
|
||||
}
|
||||
}
|
||||
}
|
||||
MeasureRequest {
|
||||
id: model.region_to_id.get(&rh).copied().unwrap_or(0),
|
||||
font: d.font_path.clone(),
|
||||
height: d.font_height,
|
||||
text_height: d.text_height,
|
||||
wrap_width: None,
|
||||
outline: d.outline,
|
||||
scale,
|
||||
text: ONE_LINE.to_string(),
|
||||
key,
|
||||
}
|
||||
};
|
||||
// Taken out and put back, as `ensure_measured` does: the engine cannot re-enter the VM.
|
||||
let mut engine = model.measurer.take()?;
|
||||
let (_, lh, _) = engine.measure(&req);
|
||||
model.measurer = Some(engine);
|
||||
if let Some(KindState::EditBox(eb)) = model.arena.frame_mut(h).map(|f| &mut f.kind_state) {
|
||||
eb.line_height = Some((req.key, lh));
|
||||
}
|
||||
Some(lh)
|
||||
}
|
||||
|
||||
/// The scale a measure is keyed under, the owner's effective scale, as the measure loop reads it.
|
||||
fn owner_scale(model: &Model, h: FrameHandle) -> f32 {
|
||||
model
|
||||
.arena
|
||||
.frame(h)
|
||||
.map(|f| f.effective_scale)
|
||||
.unwrap_or(1.0)
|
||||
}
|
||||
@@ -0,0 +1,484 @@
|
||||
//! The text region's rect (`0x77b8c0`, re-seated on every resize by `0x77a8d0`) and a multi-line
|
||||
//! box's own height (`0x77d4d0` @`0x77d8ad`), driven through the Lua methods and the tick.
|
||||
|
||||
use crate::script::{MeasureRequest, QuadContent, TextMeasure, UiScript};
|
||||
|
||||
/// A stand-in font engine: each character [`CW`] wide and each line `.0` tall. Hard breaks split
|
||||
/// lines, a trailing one opening none as in the host's line law, and each line wraps by
|
||||
/// characters at the request's wrap width.
|
||||
struct Lines(f32);
|
||||
|
||||
const CW: f32 = 7.0;
|
||||
const LH: f32 = 14.0;
|
||||
|
||||
impl TextMeasure for Lines {
|
||||
fn measure(&mut self, req: &MeasureRequest) -> (f32, f32, f32) {
|
||||
let mut lines: Vec<&str> = req.text.split('\n').collect();
|
||||
if lines.len() > 1 && lines.last() == Some(&"") {
|
||||
lines.pop();
|
||||
}
|
||||
let width = |l: &str| l.chars().count() as f32 * CW;
|
||||
let natural = lines.iter().map(|l| width(l)).fold(0.0, f32::max);
|
||||
let wrap = req.wrap_width.filter(|w| *w > 0.0);
|
||||
let rows: f32 = lines
|
||||
.iter()
|
||||
.map(|l| match wrap {
|
||||
Some(w) if width(l) > w => (width(l) / w).ceil(),
|
||||
_ => 1.0,
|
||||
})
|
||||
.sum();
|
||||
(
|
||||
wrap.map_or(natural, |w| natural.min(w)),
|
||||
rows * self.0,
|
||||
natural,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn script() -> UiScript {
|
||||
let mut s = UiScript::new().expect("construct UiScript");
|
||||
s.set_screen_size(800.0, 600.0);
|
||||
s.set_text_measurer(Box::new(Lines(LH)));
|
||||
s
|
||||
}
|
||||
|
||||
/// One frame of the host loop: the tick (where the flush relayouts), then the resolve.
|
||||
fn frame(s: &mut UiScript) {
|
||||
s.tick(0.016);
|
||||
s.resolve();
|
||||
}
|
||||
|
||||
fn height(s: &UiScript, name: &str) -> f64 {
|
||||
s.eval(&format!("return {name}:GetHeight()")).unwrap()
|
||||
}
|
||||
|
||||
/// A 200-wide multi-line box with insets (left 15, right 13, top 2, bottom 3), authored 200 tall.
|
||||
fn inset_box(s: &mut UiScript) {
|
||||
s.run(
|
||||
r#"
|
||||
E = CreateFrame("EditBox", "E")
|
||||
E:SetAutoFocus(false)
|
||||
E:SetPoint("BOTTOMLEFT", 100, 50)
|
||||
E:SetWidth(200); E:SetHeight(200)
|
||||
E:SetTextInsets(15, 13, 2, 3)
|
||||
E:SetMultiLine(true)
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
s.resolve();
|
||||
}
|
||||
|
||||
/// The box is `(top + bottom) + h` tall, `h` the text's wrapped height, one line's when empty; it
|
||||
/// grows and shrinks with its text, and the authored 200 lasts only until the first flush.
|
||||
#[test]
|
||||
fn a_multi_line_box_is_as_tall_as_its_text_and_its_insets() {
|
||||
let mut s = script();
|
||||
inset_box(&mut s);
|
||||
assert_eq!(
|
||||
height(&s, "E"),
|
||||
200.0,
|
||||
"the authored height, before any flush"
|
||||
);
|
||||
|
||||
frame(&mut s);
|
||||
assert_eq!(
|
||||
height(&s, "E"),
|
||||
f64::from(5.0 + LH),
|
||||
"an empty box is one line tall: the first flush replaces the authored 200"
|
||||
);
|
||||
|
||||
s.run(r#"E:SetText("one\ntwo\nthree")"#).unwrap();
|
||||
frame(&mut s);
|
||||
assert_eq!(height(&s, "E"), f64::from(5.0 + 3.0 * LH), "three lines");
|
||||
|
||||
// 30 characters are 210 wide against the 172 between the side insets: two rows.
|
||||
s.run(r#"E:SetText(string.rep("x", 30))"#).unwrap();
|
||||
frame(&mut s);
|
||||
assert_eq!(
|
||||
height(&s, "E"),
|
||||
f64::from(5.0 + 2.0 * LH),
|
||||
"a line wraps at the box's width less its side insets"
|
||||
);
|
||||
|
||||
s.run(r#"E:SetText("")"#).unwrap();
|
||||
frame(&mut s);
|
||||
assert_eq!(height(&s, "E"), f64::from(5.0 + LH), "it shrinks back");
|
||||
assert!(s.errors().is_empty(), "{:?}", s.errors());
|
||||
}
|
||||
|
||||
/// The relayout runs ahead of the `OnTextChanged` fire (`0x77d447` before `0x77d498`), so the
|
||||
/// handler reads the new height.
|
||||
#[test]
|
||||
fn on_text_changed_sees_the_height_the_flush_just_set() {
|
||||
let mut s = script();
|
||||
inset_box(&mut s);
|
||||
s.run(r#"E:SetScript("OnTextChanged", function() SEEN = this:GetHeight() end)"#)
|
||||
.unwrap();
|
||||
frame(&mut s);
|
||||
s.run(r#"E:SetText("a\nb\nc\nd")"#).unwrap();
|
||||
s.tick(0.016);
|
||||
assert_eq!(
|
||||
s.eval::<f64>("return SEEN").unwrap(),
|
||||
f64::from(5.0 + 4.0 * LH)
|
||||
);
|
||||
}
|
||||
|
||||
/// A new raster re-measures the empty box's line: the host's `invalidate_text_measures` drops the
|
||||
/// cached line height with every other measure.
|
||||
#[test]
|
||||
fn a_new_raster_re_measures_the_empty_boxs_line() {
|
||||
let mut s = script();
|
||||
inset_box(&mut s);
|
||||
frame(&mut s);
|
||||
assert_eq!(height(&s, "E"), f64::from(5.0 + LH));
|
||||
s.set_text_measurer(Box::new(Lines(2.0 * LH)));
|
||||
s.invalidate_text_measures();
|
||||
frame(&mut s);
|
||||
assert_eq!(height(&s, "E"), f64::from(5.0 + 2.0 * LH));
|
||||
}
|
||||
|
||||
/// A `SetHeight` lasts until the next flush, which writes the text's height back over it.
|
||||
#[test]
|
||||
fn a_scripted_height_is_written_over_at_the_next_flush() {
|
||||
let mut s = script();
|
||||
inset_box(&mut s);
|
||||
s.run(r#"E:SetText("a\nb")"#).unwrap();
|
||||
frame(&mut s);
|
||||
s.run("E:SetHeight(300)").unwrap();
|
||||
assert_eq!(
|
||||
height(&s, "E"),
|
||||
300.0,
|
||||
"the script's height, until the flush"
|
||||
);
|
||||
frame(&mut s);
|
||||
assert_eq!(height(&s, "E"), f64::from(5.0 + 2.0 * LH));
|
||||
}
|
||||
|
||||
/// A trailing newline opens no line: the text's measure folds it into the line it ends, and the
|
||||
/// box takes that measure as it is.
|
||||
#[test]
|
||||
fn a_trailing_newline_alone_adds_no_line() {
|
||||
let mut s = script();
|
||||
inset_box(&mut s);
|
||||
s.run(r#"E:SetText("abc")"#).unwrap();
|
||||
frame(&mut s);
|
||||
let one = height(&s, "E");
|
||||
s.run(r#"E:SetText("abc\n")"#).unwrap();
|
||||
frame(&mut s);
|
||||
assert_eq!(height(&s, "E"), one, "the break alone");
|
||||
s.run(r#"E:SetText("abc\nd")"#).unwrap();
|
||||
frame(&mut s);
|
||||
assert_eq!(
|
||||
height(&s, "E"),
|
||||
one + f64::from(LH),
|
||||
"the first character on the new line"
|
||||
);
|
||||
}
|
||||
|
||||
/// Only a multi-line box is sized (`0x77d86a je` skips the write).
|
||||
#[test]
|
||||
fn a_single_line_box_keeps_its_height() {
|
||||
let mut s = script();
|
||||
s.run(
|
||||
r#"
|
||||
E = CreateFrame("EditBox", "E")
|
||||
E:SetAutoFocus(false)
|
||||
E:SetPoint("BOTTOMLEFT", 100, 50)
|
||||
E:SetWidth(200); E:SetHeight(32)
|
||||
E:SetText("a\nb\nc")
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
s.resolve();
|
||||
frame(&mut s);
|
||||
frame(&mut s);
|
||||
assert_eq!(height(&s, "E"), 32.0);
|
||||
}
|
||||
|
||||
/// The multi-line text region sits `TOPLEFT` at `(left, −top)`, as wide as the box less its side
|
||||
/// insets and as tall as its text, inside the box.
|
||||
#[test]
|
||||
fn the_text_region_is_pinned_top_left_as_wide_as_the_box_and_as_tall_as_its_text() {
|
||||
let mut s = script();
|
||||
inset_box(&mut s);
|
||||
s.run(r#"E:SetText("hello")"#).unwrap();
|
||||
frame(&mut s);
|
||||
frame(&mut s);
|
||||
let rect = s
|
||||
.extract()
|
||||
.into_iter()
|
||||
.find_map(|q| match (&q.content, q.rect) {
|
||||
(QuadContent::Text { text: Some(t), .. }, Some(r)) if t == "hello" => Some(r),
|
||||
_ => None,
|
||||
})
|
||||
.expect("the text draws");
|
||||
// The box is 5 + 14 tall from y 50: its top is 69.
|
||||
assert_eq!(
|
||||
(rect.left, rect.right, rect.top, rect.bottom),
|
||||
(115.0, 287.0, 67.0, 53.0)
|
||||
);
|
||||
}
|
||||
|
||||
/// The height's move fires the box's `OnSizeChanged(width, height)`, and a flush that keeps it
|
||||
/// fires nothing.
|
||||
#[test]
|
||||
fn on_size_changed_fires_when_the_text_moves_the_height() {
|
||||
let mut s = script();
|
||||
inset_box(&mut s);
|
||||
s.run(
|
||||
r#"
|
||||
fires = {}
|
||||
E:SetScript("OnSizeChanged", function() table.insert(fires, { w = arg1, h = arg2 }) end)
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
frame(&mut s);
|
||||
let fires = |s: &UiScript| s.eval::<i64>("return table.getn(fires)").unwrap();
|
||||
let last = |s: &UiScript| {
|
||||
s.eval::<(f64, f64)>("local f = fires[table.getn(fires)] return f.w, f.h")
|
||||
.unwrap()
|
||||
};
|
||||
assert_eq!(fires(&s), 1, "the first flush replaces the authored height");
|
||||
assert_eq!(last(&s), (200.0, f64::from(5.0 + LH)));
|
||||
|
||||
s.run(r#"E:SetText("a\nb\nc")"#).unwrap();
|
||||
frame(&mut s);
|
||||
assert_eq!(fires(&s), 2);
|
||||
assert_eq!(last(&s), (200.0, f64::from(5.0 + 3.0 * LH)));
|
||||
|
||||
frame(&mut s);
|
||||
s.run(r#"E:SetText("x\ny\nz")"#).unwrap();
|
||||
frame(&mut s);
|
||||
assert_eq!(fires(&s), 2, "same line count, same height: no fire");
|
||||
assert!(s.errors().is_empty(), "{:?}", s.errors());
|
||||
}
|
||||
|
||||
/// The stock mail shape (`MailFrame.xml`): `SendMailBodyEditBox`, 270×200 at `TOPLEFT (20, −10)`
|
||||
/// in the 300×255 scroll child of the 296×257 `SendMailScrollFrame`, whose `OnTextChanged` is
|
||||
/// `ScrollingEdit_OnTextChanged`. The box's height carries the text into the range union, so 30
|
||||
/// lines give `max(255, 10 + 30·L) − 257`.
|
||||
#[test]
|
||||
fn a_scroll_frame_ranges_over_the_whole_text_of_a_multi_line_box() {
|
||||
let mut s = script();
|
||||
s.run(
|
||||
r#"
|
||||
local sf = CreateFrame("ScrollFrame", "MailScroll", UIParent)
|
||||
sf:SetPoint("TOPLEFT", 21, -97); sf:SetWidth(296); sf:SetHeight(257)
|
||||
local child = CreateFrame("Frame", "MailScrollChild", sf)
|
||||
child:SetWidth(300); child:SetHeight(255)
|
||||
child:SetPoint("TOPLEFT", 0, 0)
|
||||
sf:SetScrollChild(child)
|
||||
local body = CreateFrame("EditBox", "MailBody", child)
|
||||
body:SetMultiLine(true); body:SetAutoFocus(false)
|
||||
body:SetWidth(270); body:SetHeight(200)
|
||||
body:SetPoint("TOPLEFT", 20, -10)
|
||||
body:SetScript("OnTextChanged", function() ScrollingEdit_OnTextChanged(MailScroll) end)
|
||||
sf:SetScript("OnScrollRangeChanged", function() RANGE = arg2 end)
|
||||
function ScrollingEdit_OnTextChanged(scrollFrame) scrollFrame:UpdateScrollChildRect() end
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
s.resolve();
|
||||
frame(&mut s);
|
||||
assert_eq!(
|
||||
s.eval::<f64>("return MailScroll:GetVerticalScrollRange()")
|
||||
.unwrap(),
|
||||
0.0,
|
||||
"an empty body is one line: the 255-tall child fits the 257-tall frame"
|
||||
);
|
||||
|
||||
s.run(
|
||||
r#"
|
||||
local lines = {}
|
||||
for i = 1, 30 do lines[i] = "line " .. i end
|
||||
MailBody:SetText(table.concat(lines, "\n"))
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
frame(&mut s);
|
||||
assert_eq!(
|
||||
height(&s, "MailBody"),
|
||||
f64::from(30.0 * LH),
|
||||
"the box itself spans the text, not only its text region"
|
||||
);
|
||||
let want = f64::from((10.0 + 30.0 * LH).max(255.0) - 257.0);
|
||||
assert_eq!(
|
||||
s.eval::<f64>("return RANGE").unwrap(),
|
||||
want,
|
||||
"the range ScrollingEdit_OnTextChanged's recompute reports"
|
||||
);
|
||||
assert_eq!(
|
||||
s.eval::<f64>("return MailScroll:GetVerticalScrollRange()")
|
||||
.unwrap(),
|
||||
want
|
||||
);
|
||||
assert!(s.errors().is_empty(), "{:?}", s.errors());
|
||||
}
|
||||
|
||||
/// The text quad's rect as `(left, right, top, bottom)`, `None` when it draws nowhere.
|
||||
fn text_rect(s: &UiScript, text: &str) -> Option<(f32, f32, f32, f32)> {
|
||||
s.extract().into_iter().find_map(|q| match &q.content {
|
||||
QuadContent::Text { text: Some(t), .. } if t == text => {
|
||||
q.rect.map(|r| (r.left, r.right, r.top, r.bottom))
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
|
||||
/// A box built in Lua, with no `<FontString>` and no `SetTextInsets`, has its text seated at its
|
||||
/// first resolve: `ApplyRect` (`0x76b580`) sees it resized from the ctor's zero rect and the
|
||||
/// override `0x77a8d0` runs `0x77b8c0` (`0x77a8e7`).
|
||||
#[test]
|
||||
fn a_lua_box_with_a_font_seats_its_text_at_its_first_resolve() {
|
||||
let mut s = script();
|
||||
s.run(
|
||||
r#"
|
||||
E = CreateFrame("EditBox", "E")
|
||||
E:SetAutoFocus(false)
|
||||
E:SetPoint("BOTTOMLEFT", 100, 50)
|
||||
E:SetWidth(200); E:SetHeight(32)
|
||||
E:SetFont("Fonts\\FRIZQT__.TTF", 14)
|
||||
E:SetText("hello")
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
s.resolve();
|
||||
assert_eq!(text_rect(&s, "hello"), Some((100.0, 300.0, 82.0, 50.0)));
|
||||
}
|
||||
|
||||
/// A later resize re-seats the text of any box: a multi-line box re-wraps at its new width, and a
|
||||
/// single-line box's moved text region is put back between its insets.
|
||||
#[test]
|
||||
fn a_resized_box_re_seats_its_text() {
|
||||
let mut s = script();
|
||||
inset_box(&mut s);
|
||||
// 30 characters are 210 wide: two rows in 200 less the side insets, one row in 300 less them.
|
||||
s.run(r#"E:SetText(string.rep("x", 30))"#).unwrap();
|
||||
frame(&mut s);
|
||||
frame(&mut s);
|
||||
assert_eq!(height(&s, "E"), f64::from(5.0 + 2.0 * LH));
|
||||
s.run("E:SetWidth(300)").unwrap();
|
||||
frame(&mut s);
|
||||
frame(&mut s);
|
||||
assert_eq!(
|
||||
text_rect(&s, &"x".repeat(30)).map(|r| (r.0, r.1)),
|
||||
Some((115.0, 387.0)),
|
||||
"the text region takes the new width less the side insets"
|
||||
);
|
||||
assert_eq!(
|
||||
height(&s, "E"),
|
||||
f64::from(5.0 + LH),
|
||||
"and the text re-wraps in it"
|
||||
);
|
||||
|
||||
s.run(
|
||||
r#"
|
||||
S = CreateFrame("EditBox", "S")
|
||||
S:SetAutoFocus(false)
|
||||
S:SetPoint("BOTTOMLEFT", 100, 200)
|
||||
S:SetWidth(200); S:SetHeight(32)
|
||||
S:SetText("single")
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
frame(&mut s);
|
||||
s.run(
|
||||
r#"
|
||||
local text = S:GetRegions()
|
||||
text:ClearAllPoints(); text:SetPoint("TOPLEFT", S, "TOPLEFT", 40, 40)
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
frame(&mut s);
|
||||
assert_ne!(
|
||||
text_rect(&s, "single").map(|r| r.0),
|
||||
Some(100.0),
|
||||
"the script moved the text region"
|
||||
);
|
||||
s.run("S:SetWidth(250)").unwrap();
|
||||
frame(&mut s);
|
||||
assert_eq!(text_rect(&s, "single"), Some((100.0, 350.0, 232.0, 200.0)));
|
||||
}
|
||||
|
||||
/// A single-line box at `(100, 200)`, 200×32 with insets (10, 20, 3, 5), whose text region a
|
||||
/// script has moved to `TOPLEFT (40, 40)`; `SEATED` is where `0x77b8c0` puts it.
|
||||
fn moved_text_region(s: &mut UiScript) {
|
||||
s.run(
|
||||
r#"
|
||||
S = CreateFrame("EditBox", "S")
|
||||
S:SetAutoFocus(false)
|
||||
S:SetPoint("BOTTOMLEFT", 100, 200)
|
||||
S:SetWidth(200); S:SetHeight(32)
|
||||
S:SetTextInsets(10, 20, 3, 5)
|
||||
S:SetText("single")
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
frame(s);
|
||||
assert_eq!(text_rect(s, "single"), Some(SEATED));
|
||||
move_text_region(s);
|
||||
}
|
||||
|
||||
const SEATED: (f32, f32, f32, f32) = (110.0, 280.0, 229.0, 205.0);
|
||||
|
||||
fn move_text_region(s: &mut UiScript) {
|
||||
s.run(
|
||||
r#"
|
||||
local text = S:GetRegions()
|
||||
text:ClearAllPoints(); text:SetPoint("TOPLEFT", S, "TOPLEFT", 40, 40)
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
frame(s);
|
||||
assert_eq!(
|
||||
text_rect(s, "single").map(|r| r.0),
|
||||
Some(140.0),
|
||||
"the script moved the text region"
|
||||
);
|
||||
}
|
||||
|
||||
/// `GetTextInsets` (`0x77a710`) copies the insets out, then re-seats (`0x77a73f` → `0x77b8c0`).
|
||||
#[test]
|
||||
fn get_text_insets_puts_a_moved_text_region_back() {
|
||||
let mut s = script();
|
||||
moved_text_region(&mut s);
|
||||
let insets = s
|
||||
.eval::<(f32, f32, f32, f32)>("return S:GetTextInsets()")
|
||||
.unwrap();
|
||||
assert_eq!(insets, (10.0, 20.0, 3.0, 5.0));
|
||||
frame(&mut s);
|
||||
assert_eq!(text_rect(&s, "single"), Some(SEATED));
|
||||
}
|
||||
|
||||
/// A new face or size notifies the box (`0x79f342` → `0x77e2a0`), which re-seats its text
|
||||
/// (`0x77e2b5`); the same font again sets no change bit (`0x79f2ef`), so it does not.
|
||||
#[test]
|
||||
fn set_font_puts_a_moved_text_region_back_when_the_font_changes() {
|
||||
let mut s = script();
|
||||
moved_text_region(&mut s);
|
||||
s.run(r#"S:SetFont("Fonts\\FRIZQT__.TTF", 14)"#).unwrap();
|
||||
frame(&mut s);
|
||||
assert_eq!(text_rect(&s, "single"), Some(SEATED));
|
||||
|
||||
move_text_region(&mut s);
|
||||
s.run(r#"S:SetFont("Fonts\\FRIZQT__.TTF", 14)"#).unwrap();
|
||||
frame(&mut s);
|
||||
assert_eq!(
|
||||
text_rect(&s, "single").map(|r| r.0),
|
||||
Some(140.0),
|
||||
"the same font changes nothing"
|
||||
);
|
||||
}
|
||||
|
||||
/// Linking another font object propagates into the box's font (`0x770c60` → `0x77e4b0`), whose
|
||||
/// notify re-seats the text (`0x77e4d7` → `0x77e2a0`).
|
||||
#[test]
|
||||
fn set_font_object_puts_a_moved_text_region_back() {
|
||||
let mut s = script();
|
||||
s.run(r#"F = CreateFont("ScratchFont") F:SetFont("Fonts\\FRIZQT__.TTF", 12)"#)
|
||||
.unwrap();
|
||||
moved_text_region(&mut s);
|
||||
s.run("S:SetFontObject(F)").unwrap();
|
||||
frame(&mut s);
|
||||
assert_eq!(text_rect(&s, "single"), Some(SEATED));
|
||||
}
|
||||
@@ -24,6 +24,11 @@ use crate::widget::RegionHandle;
|
||||
/// `EditBox` creates its own on demand. An error means the wrong receiver.
|
||||
pub(super) type ResolveRegion = fn(&Lua, &Table) -> mlua::Result<RegionHandle>;
|
||||
|
||||
/// A widget's listener on its font instance, run after `SetFont` loads a new face or size and
|
||||
/// after `SetFontObject` links another object: the notify `[vt+0x14]` that `0x79f342` and
|
||||
/// `0x77e4d7` call, which the EditBox answers (`0x77e2a0`).
|
||||
pub(super) type FontChanged = fn(&Lua, &Table) -> mlua::Result<()>;
|
||||
|
||||
/// `SetFont`'s shared argument gate (`0x79f210`, reached from Font `0x7a0270`, FontString
|
||||
/// `0x79d4f0` and EditBox `0x797210`): arg 2 must pass `lua_isstring` and arg 3 `lua_isnumber`,
|
||||
/// both coercing, else it raises `Usage: %s:SetFont("font", fontHeight [, flags])` (`0x87c69c`).
|
||||
@@ -58,6 +63,7 @@ pub(super) fn install(
|
||||
m: &Table,
|
||||
resolve: ResolveRegion,
|
||||
widget: &'static str,
|
||||
on_font_change: Option<FontChanged>,
|
||||
) -> mlua::Result<()> {
|
||||
// ── the font object ──
|
||||
// SetFontObject(font | "font" | nil) → nothing (`0x79ef10`, usage string `0x87c5cc`). Any
|
||||
@@ -67,23 +73,37 @@ pub(super) fn install(
|
||||
lua.create_function(move |lua, (this, font): (Table, Value)| {
|
||||
let name = super::font::resolve("SetFontObject", &font)?;
|
||||
let rh = resolve(lua, &this)?;
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model");
|
||||
// nil severs the link and keeps the paint; the reference only nulls the parent.
|
||||
let Some(name) = name else {
|
||||
model.region_data.entry(rh).or_default().font_object = None;
|
||||
return Ok(());
|
||||
let relinked = {
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model");
|
||||
// nil severs the link and keeps the paint; the reference only nulls the parent.
|
||||
let Some(name) = name else {
|
||||
model.region_data.entry(rh).or_default().font_object = None;
|
||||
return Ok(());
|
||||
};
|
||||
let Some(fo) = model.font_object(&name).cloned() else {
|
||||
return Err(mlua::Error::runtime(format!(
|
||||
"SetFontObject: no font object named '{name}' is registered"
|
||||
)));
|
||||
};
|
||||
let d = model.region_data.entry(rh).or_default();
|
||||
// The same object again changes nothing (`0x770c6a`).
|
||||
let relinked = d
|
||||
.font_object
|
||||
.as_deref()
|
||||
.is_none_or(|old| !old.eq_ignore_ascii_case(&name));
|
||||
d.font_object = Some(name);
|
||||
// The inherit mask stays: each local setter clears its bit (`FONTINSTANCE+0x2c`)
|
||||
// and nothing restores it, so a property set locally survives a later
|
||||
// `SetFontObject`.
|
||||
super::font::repaint(d, &fo);
|
||||
model.touch_measure(rh);
|
||||
relinked
|
||||
};
|
||||
let Some(fo) = model.font_object(&name).cloned() else {
|
||||
return Err(mlua::Error::runtime(format!(
|
||||
"SetFontObject: no font object named '{name}' is registered"
|
||||
)));
|
||||
};
|
||||
let d = model.region_data.entry(rh).or_default();
|
||||
d.font_object = Some(name);
|
||||
// The inherit mask stays: each local setter clears its bit (`FONTINSTANCE+0x2c`) and
|
||||
// nothing restores it, so a property set locally survives a later `SetFontObject`.
|
||||
super::font::repaint(d, &fo);
|
||||
model.touch_measure(rh);
|
||||
if relinked {
|
||||
if let Some(changed) = on_font_change {
|
||||
changed(lua, &this)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
})?,
|
||||
)?;
|
||||
@@ -118,6 +138,10 @@ pub(super) fn install(
|
||||
let (path, height) = set_font_args(&file, &height, widget)?;
|
||||
let rh = resolve(lua, &this)?;
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model");
|
||||
let before = model
|
||||
.region_data
|
||||
.get(&rh)
|
||||
.map(|d| (d.font_path.clone(), d.font_height, d.outline));
|
||||
// A failed load (`0x5c1ae0` under the `0x44d040` font cache) is the host's probe to
|
||||
// judge, as only the store knows the files; with no probe, a non-empty path loads.
|
||||
let ok =
|
||||
@@ -137,7 +161,16 @@ pub(super) fn install(
|
||||
d.outline = Outline::flags(&f);
|
||||
d.font_explicit.outline = true;
|
||||
}
|
||||
let after = (d.font_path.clone(), d.font_height, d.outline);
|
||||
model.touch_measure(rh);
|
||||
drop(model);
|
||||
// A loaded face that changed the path, height or flags notifies (`0x79f32f` sets
|
||||
// the change bits, `0x79f342` calls the notify); a failed load does not.
|
||||
if ok && before.as_ref() != Some(&after) {
|
||||
if let Some(changed) = on_font_change {
|
||||
changed(lua, &this)?;
|
||||
}
|
||||
}
|
||||
Ok(if ok { Value::Number(1.0) } else { Value::Nil })
|
||||
},
|
||||
)?,
|
||||
|
||||
@@ -164,6 +164,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
.ok_or_else(|| mlua::Error::runtime("not a MessageFrame"))
|
||||
},
|
||||
"MessageFrame",
|
||||
None,
|
||||
)?;
|
||||
|
||||
lua.set_named_registry_value(REG_MESSAGEFRAME_METHODS, m)?;
|
||||
|
||||
@@ -243,6 +243,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
.ok_or_else(|| mlua::Error::runtime("not a ScrollingMessageFrame"))
|
||||
},
|
||||
"ScrollingMessageFrame",
|
||||
None,
|
||||
)?;
|
||||
|
||||
lua.set_named_registry_value(REG_SCROLLINGMESSAGEFRAME_METHODS, m)?;
|
||||
|
||||
@@ -879,19 +879,30 @@ impl UiScript {
|
||||
}
|
||||
|
||||
/// Resolve and cache every frame's rect, which `GetWidth`/`GetHeight`/`extract` read, then fire
|
||||
/// `OnSizeChanged` for frames whose size moved ([`event::fire_size_changes`]).
|
||||
/// `OnSizeChanged` for frames whose size moved ([`event::fire_size_changes`]) and re-seat the
|
||||
/// text of every resized EditBox.
|
||||
pub fn resolve(&mut self) {
|
||||
self.resolve_and_measure();
|
||||
event::fire_size_changes(&self.lua);
|
||||
// The EditBox's own `OnSizeChanged` (`0x77a8d0`) runs after the script fire and re-seats a
|
||||
// resized box's text, which the reference's drain resolves in the same pass.
|
||||
let reseated = editbox::reseat_resized(&mut self.model_mut());
|
||||
if reseated {
|
||||
self.resolve_and_measure();
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolve, and with a font engine installed ([`Self::set_text_measurer`]) measure what the
|
||||
/// solve revealed and solve again, so a FontString's box is right in the frame its text was set.
|
||||
fn resolve_and_measure(&mut self) {
|
||||
{
|
||||
let mut model = self.model_mut();
|
||||
Self::resolve_layout(&mut model);
|
||||
}
|
||||
// With a font engine installed ([`Self::set_text_measurer`]), measure what the solve
|
||||
// revealed and solve again, so a FontString's box is right in the frame its text was set.
|
||||
if self.fill_measures() {
|
||||
let mut model = self.model_mut();
|
||||
Self::resolve_layout(&mut model);
|
||||
}
|
||||
event::fire_size_changes(&self.lua);
|
||||
}
|
||||
|
||||
/// Store host measurements `(id, w, h, natural_w, key)` for [`MeasureRequest`]s; the next
|
||||
@@ -960,6 +971,7 @@ impl UiScript {
|
||||
}
|
||||
KindState::EditBox(eb) => {
|
||||
eb.advances_key = eb.advances_key.wrapping_add(1);
|
||||
eb.line_height = None;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
@@ -200,5 +200,5 @@ pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
//
|
||||
// The font object, font, text colour and shadow getters and setters are type-guard shims over
|
||||
// one implementation each (`0x79f210` SetFont, `0x79f3b0` GetFont), shared with EditBox.
|
||||
super::super::font_block::install(lua, m, region_handle_of, "FontString")
|
||||
super::super::font_block::install(lua, m, region_handle_of, "FontString", None)
|
||||
}
|
||||
|
||||
@@ -77,8 +77,12 @@ impl super::UiScript {
|
||||
self.model_mut().now += f64::from(elapsed);
|
||||
// The focused edit box's caret blink (`0x77a790`, on the client's frame tick).
|
||||
editbox::tick_blink(&self.lua, elapsed);
|
||||
// Then `0x77a790`'s drain of the `OnTextChanged`s an edit only marked (`0x77a7a1`), before
|
||||
// the OnUpdate sweep, as in the reference.
|
||||
// Then the flush (`0x77d3e0`): a multi-line box's relayout sizes it to its text
|
||||
// (`0x77d4d0`), then the `OnTextChanged`s an edit only marked fire. benilla hoists it ahead
|
||||
// of the OnUpdate sweep; the reference runs it per box inside the pump's walk, from the
|
||||
// box's update (`0x77a790`, vtable `+0x38`), after that box's own Lua `OnUpdate`
|
||||
// (`0x77a79a`, then `0x77a7a1`).
|
||||
editbox::relayout_multi_line(&self.lua);
|
||||
editbox::drain_text_changed(&self.lua);
|
||||
// Then the caret flush (`0x77d3e0` → `0x77da80`): `OnCursorChanged` when the caret moved,
|
||||
// which `ScrollingEdit_OnUpdate` scrolls by.
|
||||
|
||||
@@ -470,11 +470,16 @@ impl RegionData {
|
||||
/// read treats a measure under another key as absent; the layout keeps the old box until the
|
||||
/// new one lands, except for empty text, which is never measured.
|
||||
pub(crate) fn measure_key(&self, scale: f32) -> u64 {
|
||||
use std::hash::{Hash, Hasher};
|
||||
let mut hasher = std::collections::hash_map::DefaultHasher::new();
|
||||
// By reference: `String` hashes as `str` does, and this runs for every FontString every
|
||||
// frame, so a clone would allocate for nothing.
|
||||
self.text.as_deref().unwrap_or("").hash(&mut hasher);
|
||||
self.measure_key_of(self.text.as_deref().unwrap_or(""), scale)
|
||||
}
|
||||
|
||||
/// [`Self::measure_key`] with `text` in place of the region's own.
|
||||
pub(crate) fn measure_key_of(&self, text: &str, scale: f32) -> u64 {
|
||||
use std::hash::{Hash, Hasher};
|
||||
let mut hasher = std::collections::hash_map::DefaultHasher::new();
|
||||
text.hash(&mut hasher);
|
||||
self.font_path.hash(&mut hasher);
|
||||
self.font_height.map(f32::to_bits).hash(&mut hasher);
|
||||
self.text_height.map(f32::to_bits).hash(&mut hasher);
|
||||
|
||||
@@ -79,8 +79,15 @@ pub struct EditBoxState {
|
||||
pub history_pos: Option<usize>,
|
||||
/// The live line stashed when browsing starts, restored when DOWN walks past the newest entry.
|
||||
pub history_draft: Option<String>,
|
||||
/// `SetTextInsets(l, r, t, b)`, applied as the text region's two corner anchors.
|
||||
/// `SetTextInsets(l, r, t, b)` (`+0x3ec`, `+0x3f4`, `+0x3f0`, `+0x3e8`), which seat the text
|
||||
/// region and pad a multi-line box's height.
|
||||
pub text_insets: [f32; 4],
|
||||
/// One line's measured height in the text region's font (`0x7727b0(fs, 1)`), the height of an
|
||||
/// empty multi-line box's text, with the measure key it was taken under.
|
||||
pub line_height: Option<(u64, f32)>,
|
||||
/// The box's rect when its resize was last checked, `ApplyRect`'s old rect (`0x76b580`);
|
||||
/// `None` before its first resolve, which compares against the ctor's zero rect.
|
||||
pub notified_rect: Option<crate::layout::Rect>,
|
||||
/// The host-measured width of `display[..i]` per byte `i`, a continuation byte repeating its
|
||||
/// lead's; hit-testing (`0x77d0d0`) and the scroll window read it. Empty until answered.
|
||||
pub advances: Vec<f32>,
|
||||
@@ -160,6 +167,8 @@ impl Default for EditBoxState {
|
||||
history_pos: None,
|
||||
history_draft: None,
|
||||
text_insets: [0.0; 4],
|
||||
line_height: None,
|
||||
notified_rect: None,
|
||||
advances: Vec::new(),
|
||||
advances_key: 0,
|
||||
rows: vec![0],
|
||||
|
||||
@@ -249,6 +249,9 @@ pub struct WidgetArena {
|
||||
tooltip_kinds: Vec<FrameHandle>,
|
||||
/// The live Minimaps, kept the same way, for the containment, zoom and arrow-facing feeds.
|
||||
minimap_kinds: Vec<FrameHandle>,
|
||||
/// The live EditBoxes, kept the same way, for the resize re-seat (`reseat_resized`) and the
|
||||
/// per-tick relayout of multi-line boxes.
|
||||
editbox_kinds: Vec<FrameHandle>,
|
||||
}
|
||||
|
||||
impl Default for WidgetArena {
|
||||
@@ -291,6 +294,7 @@ impl WidgetArena {
|
||||
ticked_kinds: Vec::new(),
|
||||
tooltip_kinds: Vec::new(),
|
||||
minimap_kinds: Vec::new(),
|
||||
editbox_kinds: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -312,6 +316,10 @@ impl WidgetArena {
|
||||
&self.minimap_kinds
|
||||
}
|
||||
|
||||
pub fn editbox_kinds(&self) -> &[FrameHandle] {
|
||||
&self.editbox_kinds
|
||||
}
|
||||
|
||||
// ── Read access ────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
pub fn frame(&self, h: FrameHandle) -> Option<&Frame> {
|
||||
@@ -499,6 +507,7 @@ impl WidgetArena {
|
||||
self.names.entry(n).or_insert(handle);
|
||||
}
|
||||
if matches!(kind, FrameKind::EditBox) {
|
||||
self.editbox_kinds.push(handle);
|
||||
self.build_editbox_engine_regions(handle);
|
||||
}
|
||||
if matches!(kind, FrameKind::Minimap) {
|
||||
@@ -556,6 +565,7 @@ impl WidgetArena {
|
||||
self.ticked_kinds.retain(|&t| t != h);
|
||||
self.tooltip_kinds.retain(|&t| t != h);
|
||||
self.minimap_kinds.retain(|&t| t != h);
|
||||
self.editbox_kinds.retain(|&t| t != h);
|
||||
|
||||
for c in children {
|
||||
self.destroy(c);
|
||||
|
||||
Reference in New Issue
Block a user