messageframe: a scroll that moves the view fires OnMessageScrollChanged

WIM sets this script on every window it builds — the history frame at load, then
one per correspondent (six whisper windows in one session of his log) — and we
refused the name by raise, which killed the rest of the builder function each
time, not just the handler.

It was on the deliberate "real 1.12 kinds nothing fires raise rather than
silently accept" list. That rule is right and this was the wrong side of it: the
slot now fires, from the ScrollingMessageFrame's own scroll verbs when a scroll
actually moves the view, which is what a chat addon hangs its scroll-down arrow
on. A scroll refused at either end moves nothing and says nothing; whether the
reference fires one there is untraced.

The raise list keeps its rule and its test — `OnInputLanguageChanged` and the
movie frame's are still raises, and a name leaves that list only when something
fires it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
This commit is contained in:
2026-10-17 18:14:17 -06:00
co-authored by Claude Opus 5
parent 15f069667e
commit 13989fb09b
5 changed files with 76 additions and 18 deletions
@@ -59,19 +59,32 @@ fn scroll(
op: impl FnOnce(&mut ScrollingMessageState, usize),
) -> mlua::Result<()> {
let h = frame_handle_of(lua, this)?;
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
let viewport_rows = UiScript::message_viewport_rows(&model, h);
let frame = model
.arena
.frame_mut(h)
.ok_or_else(|| mlua::Error::runtime("stale frame handle"))?;
match &mut frame.kind_state {
KindState::ScrollingMessage(smf) => {
op(smf, viewport_rows);
Ok(())
let id = crate::script::object::decode_id(this)?;
let moved = {
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
let viewport_rows = UiScript::message_viewport_rows(&model, h);
let frame = model
.arena
.frame_mut(h)
.ok_or_else(|| mlua::Error::runtime("stale frame handle"))?;
match &mut frame.kind_state {
KindState::ScrollingMessage(smf) => {
let before = smf.scroll_offset;
op(smf, viewport_rows);
smf.scroll_offset != before
}
_ => return Err(mlua::Error::runtime("not a ScrollingMessageFrame")),
}
_ => Err(mlua::Error::runtime("not a ScrollingMessageFrame")),
};
// `OnMessageScrollChanged`, with no model borrow alive: the handler re-enters Lua and may call
// straight back in. The scroll frame's own slot, the one a chat addon hangs its scroll-down
// arrow on (`WIM_HistoryFrameMessageListScrollingMessageFrame` and every whisper window it
// builds). Only a scroll that *moved* fires; whether the reference also fires one refused at
// an end is untraced.
if moved {
crate::script::event::fire_widget_handler(lua, id, "OnMessageScrollChanged", Vec::new())?;
}
Ok(())
}
pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
@@ -890,6 +903,45 @@ mod tests {
.unwrap();
}
/// WIM's mechanism: it hangs its scroll-down arrow on the frame's own
/// `OnMessageScrollChanged` and sets it on every window it builds, so the name has to be
/// accepted *and* fired. A scroll refused at an end moved nothing and says nothing.
#[test]
fn a_scroll_that_moves_the_view_fires_the_frames_scroll_script() {
let mut s = UiScript::new().unwrap();
s.set_screen_size(800.0, 600.0);
s.run(
"local f = CreateFrame('ScrollingMessageFrame', 'CF')\n\
f:SetPoint('BOTTOMLEFT', 32, 90)\n\
f:SetWidth(430)\n\
f:SetHeight(34)\n\
scrolls = 0\n\
f:SetScript('OnMessageScrollChanged', function() scrolls = scrolls + 1 end)",
)
.unwrap();
for i in 1..=8 {
s.run(&format!("CF:AddMessage('line {i}')")).unwrap();
}
assert_eq!(
s.eval::<i64>("return scrolls").unwrap(),
0,
"nothing scrolled yet"
);
s.run("CF:ScrollUp()").unwrap();
assert_eq!(s.eval::<i64>("return scrolls").unwrap(), 1);
s.run("CF:ScrollToBottom()").unwrap();
assert_eq!(s.eval::<i64>("return scrolls").unwrap(), 2);
// Already at the bottom: refused, so no edge.
s.run("CF:ScrollDown()").unwrap();
assert_eq!(
s.eval::<i64>("return scrolls").unwrap(),
2,
"a scroll that cannot move fires nothing"
);
assert!(s.take_errors().is_empty());
}
#[test]
fn lua_addmessage_and_scroll_surface() {
let s = UiScript::new().unwrap();