Files
benilla-octo/crates/benilla-ui/src/script/editbox/flush.rs
T
Sam SungandClaude Opus 5.5 c9ddd80342 Script errors reach the error handler bare, as 1.12.1 hands them (#865)
Script errors reached the Lua error handler as mlua's display text: a `runtime error: ` word and a `stack traceback:` block, which overflow the stock `ScriptErrors` dialog. And a raise in a `CreateFrame` template's OnLoad was logged as a warning and never reached the handler at all.

In the reference, handlers run under `lua_pcall` with the error function `0x703b40`, which calls the `seterrorhandler` function with the bare message. `CreateFrame` builds its template through the XML instantiator `0x6ee280`, whose OnLoad runs under that same function (`0x704d50`).

Now every caught error records two texts: the decorated one for benilla's log and the bare message for the handler. A load report pairs the two per row, and `CreateFrame` records its template's errors as script errors.

Tests: the handler hears bare messages from handlers, and a template OnLoad raise reaches it.

Fixes #697
Fixes #859

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-11 19:12:21 +07:00

157 lines
6.6 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 box's own per-frame update (`0x77a790`, vtable `0x81c910` slot `+0x38`), which the tick's
//! walk runs in the box's place, and its flush (`0x77d3e0`), which drains the dirty word.
use mlua::{Lua, Value};
use super::{event, fire_script, frame_id_of, relayout, with_eb, Model, CARET_WIDTH};
use crate::widget::{EditBoxState, FrameHandle};
/// The box's update after its Lua `OnUpdate` (`0x77a79a`): the flush (`0x77a7a1`), then the caret
/// blink (`0x77a7a6`). The walk runs it for every shown box, with an `OnUpdate` or without.
pub(in crate::script) fn update(lua: &Lua, h: FrameHandle, dt: f32) {
flush(lua, h);
blink(lua, h, dt);
}
/// The flush `0x77d3e0`. It samples bit 0 at entry (`0x77d3f2`); on bit 0 the relayout runs
/// (`0x77d447`) and the bit clears (`0x77d44c`); on bit 2 the caret leg runs (`0x77d475`), with
/// the blink and `OnCursorChanged`, and only then does the bit clear (`0x77d47a`); last,
/// `OnTextChanged` fires if bit 0 was set at entry (`0x77d481`–`0x77d498`). So a handler's edit
/// raises bits for the next flush, a frame later, and never loops inside one: an `OnTextChanged`
/// that sets its own text fires once more, next frame, and a caret moved by `OnCursorChanged` is
/// cleared with the bit.
/// Besides the walk, the box's `OnKeyDown` (`0x77b1e2`) and `OnMouseDown` (`0x77b819`) run it
/// before the key or click acts.
///
/// Deviation: the flush's promotion of bit 2 to bit 0 when the caret has left the display window
/// (`0x77d3f5`–`0x77d436`), with its IME legs (`0x77d417`–`0x77d431`), and the IME writer's
/// `or 6` (`0x77acde`, in `0x77ac60`) are not modelled: the promotion only relayouts, which
/// re-windows a single-line box whose window benilla keeps host-side, and fires nothing, since
/// bit 0 is sampled before it; benilla has no IME composition.
pub(super) fn flush(lua: &Lua, h: FrameHandle) {
let Some((entry, owed)) = with_eb(lua, h, |eb| (eb.dirty, eb.relayout_owed)) else {
return;
};
let text_changed = entry & EditBoxState::DIRTY_TEXT != 0;
if text_changed || owed {
let done = relayout::relayout(lua, h);
with_eb(lua, h, |eb| {
eb.dirty &= !EditBoxState::DIRTY_TEXT;
eb.relayout_owed = !done;
});
}
if entry & EditBoxState::DIRTY_CURSOR != 0 && caret(lua, h) {
with_eb(lua, h, |eb| eb.dirty &= !EditBoxState::DIRTY_CURSOR);
}
if text_changed {
fire_script(lua, frame_id_of(lua, h), "OnTextChanged");
}
}
/// The caret leg `0x77da80`: it restarts the blink (`0x77dd68`), fires `OnCursorChanged`, then
/// shows the caret in the focused box (`0x77de35`) and hides it elsewhere (`0x77de51`). So a box
/// that takes the keyboard, whose focus change raises bit 2 (`0x77afa8`), starts its caret solid.
/// `false` while the advance table is pending, for the next flush to retry.
fn caret(lua: &Lua, h: FrameHandle) -> bool {
with_eb(lua, h, |eb| eb.blink_accum = 0.0);
let placed = cursor_changed(lua, h);
let focused = lua
.app_data_ref::<Model>()
.expect("model app_data")
.focused_editbox
== Some(h);
with_eb(lua, h, |eb| eb.caret_shown = focused);
placed
}
/// `OnCursorChanged(x, y, w, h)` (`0x77de1b`, in the box's own units via `0x77dd5f`), `x` the caret's advance
/// along its line, `y` minus the row index times the row pitch, `w` the constant 4.0 and `h` the
/// line height. The arguments are computed only for a box with the script (`0x77dd8c`), from the
/// box's advance table, which the installed font engine measures here when the text or font
/// moved. `false` while that table is pending, with no engine installed: the host answers the
/// focused box's a tick later.
///
/// Deviation: `h` is the row pitch the advance answer carries, where the reference passes the line
/// height (`0x7727b0`): 0 for a single-line box, whose answer has no pitch, and for a multi-line
/// box taller than the line height only for a font with extra spacing (none in the stock UI).
fn cursor_changed(lua: &Lua, h: FrameHandle) -> bool {
let id = frame_id_of(lua, h);
if !event::has_widget_handler(lua, id, "OnCursorChanged") {
return true;
}
if !super::seam::advances_current(lua, h) {
return false;
}
let args = with_eb(lua, h, |eb| {
let display = eb.display();
if eb.advances.len() != display.len() + 1 {
return None;
}
let cursor_d = eb.text_to_display(eb.cursor).min(display.len());
let (row, x) = if eb.multi_line {
eb.caret_row_x(cursor_d)
} else {
(0, eb.advances[cursor_d])
};
let pitch = eb.cell_h;
// `y` starts at 0 and loses one pitch per row above the caret (`0x77dae1`, `0x77db31`),
// so row 0 reports +0, never the -0 that Lua prints as "-0".
let y = 0.0 - row as f32 * pitch;
Some((x, y, pitch))
})
.flatten();
let Some((x, y, pitch)) = args else {
return false;
};
// The advance table is in root units (measured at the box's scale, divided by the seam
// alone); the handler gets the box's own, the reference dividing by its effective scale
// (`0x77db23 fdiv [esi+0x7c]`).
let k = {
let model = lua.app_data_ref::<Model>().expect("model app_data");
crate::script::object::eff_scale(&model, h)
};
let (x, y, pitch) = (x / k, y / k, pitch / k);
if let Err(e) = event::fire_widget_handler(
lua,
id,
"OnCursorChanged",
vec![
Value::Number(f64::from(x)),
Value::Number(f64::from(y)),
Value::Number(f64::from(CARET_WIDTH)),
Value::Number(f64::from(pitch)),
],
) {
lua.app_data_mut::<Model>()
.expect("model app_data")
.record_lua_error(&e);
}
true
}
/// The caret blink (`0x77a7a6`–`0x77a84c`): past the period (0.5 s by default) the focused box's
/// caret toggles and the accumulator resets; only a period of exactly 0.0 keeps the caret solid
/// (`0x77a7b4`).
///
/// Deviation: not gated on the cursor lying inside the scroll window (`0x77a7bd`–`0x77a7d7`),
/// which benilla's host keeps around the caret.
fn blink(lua: &Lua, h: FrameHandle, dt: f32) {
let focused = lua
.app_data_ref::<Model>()
.expect("model app_data")
.focused_editbox
== Some(h);
if !focused {
return;
}
with_eb(lua, h, |eb| {
if eb.blink_period != 0.0 {
eb.blink_accum += dt;
if eb.blink_accum > eb.blink_period {
eb.caret_shown = !eb.caret_shown;
eb.blink_accum = 0.0;
}
}
});
}