From b15fb9abc6d4cbe32d01ccca69848359ab65e5e4 Mon Sep 17 00:00:00 2001 From: Mathih13 Date: Wed, 14 Oct 2026 09:57:15 +0200 Subject: [PATCH] A MessageFrame's link is text; only chat lines take link buttons (#1149) - A MessageFrame's link is text; only chat lines take link buttons - set_spans' doc says only a ScrollingMessageFrame's spans take buttons Co-Authored-By: Claude Opus 5.5 Co-Authored-By: Sam --- .../benilla-app/src/ui_script/extract/text.rs | 58 +++++++++++++++++++ crates/benilla-ui/src/script/hyperlink.rs | 13 +++-- .../benilla-ui/src/script/tests/hyperlink.rs | 30 ++++++++-- 3 files changed, 93 insertions(+), 8 deletions(-) diff --git a/crates/benilla-app/src/ui_script/extract/text.rs b/crates/benilla-app/src/ui_script/extract/text.rs index d1f27b5eb..b9db3ad70 100644 --- a/crates/benilla-app/src/ui_script/extract/text.rs +++ b/crates/benilla-app/src/ui_script/extract/text.rs @@ -480,6 +480,11 @@ mod caret_tests { fn app_with(lua: &str) -> App { let script = UiScript::new().unwrap(); script.run(lua).unwrap(); + app_on(script) + } + + /// The whole UI pass on a 1024×768 window (`s = 1`) over `script`. + pub(super) fn app_on(script: UiScript) -> App { let mut app = App::new(); app.insert_non_send_resource(script); app.insert_resource(UiFontAtlas::for_test(1.0).expect("the client faces open")); @@ -625,3 +630,56 @@ mod caret_tests { } } } + +/// A message line's links through the whole UI pass, on the stock `UIErrorsFrame`. +#[cfg(test)] +mod link_span_tests { + use benilla_ui::script::UiScript; + + use super::caret_tests::app_on; + use crate::ui_script::test_ui::load_ui; + + const LINE: &str = "|cff1eff00|Hitem:7073:0:0:0|h[Broken Fang]|h|r"; + + /// Stock `UIErrorsFrame` is a `` (`UIErrorsFrame.xml:4`), whose update + /// (`0x786200`) never takes a link button (`0x7a3880`), so its link line takes no mouse; a + /// ScrollingMessageFrame's same line does (`0x788ce7`). + #[test] + fn a_link_in_the_stock_errors_frame_takes_no_mouse() { + let _data = benilla_formats::wow_data_or_skip!(); + let script = UiScript::new().unwrap(); + for file in [ + "Interface\\FrameXML\\GlobalStrings.lua", + "Interface\\FrameXML\\Fonts.xml", + "Interface\\FrameXML\\UIParent.xml", + "Interface\\FrameXML\\UIErrorsFrame.xml", + ] { + load_ui(&script, file); + } + script + .run(&format!( + r#" + UIErrorsFrame:AddMessage("{LINE}", 1.0, 0.1, 0.1, 1.0) + CF = CreateFrame("ScrollingMessageFrame", "CF", UIParent) + CF:SetPoint("BOTTOMLEFT", 32, 100); CF:SetWidth(430); CF:SetHeight(120) + CF:SetFontObject(ChatFontNormal) + CF:AddMessage("{LINE}") + "# + )) + .unwrap(); + let app = app_on(script); + let s = app.world().non_send_resource::(); + assert_eq!( + s.eval::("return CF:GetNumChildren()").unwrap(), + 1, + "the chat line's link has its button" + ); + assert_eq!( + s.eval::("return UIErrorsFrame:GetNumChildren()") + .unwrap(), + 0, + "the error line's link is text" + ); + assert!(s.errors().is_empty(), "{:?}", s.errors()); + } +} diff --git a/crates/benilla-ui/src/script/hyperlink.rs b/crates/benilla-ui/src/script/hyperlink.rs index a8e8eb3f9..45d2044d4 100644 --- a/crates/benilla-ui/src/script/hyperlink.rs +++ b/crates/benilla-ui/src/script/hyperlink.rs @@ -78,9 +78,9 @@ fn fire(lua: &Lua, owner: FrameHandle, script: &str, args: &[&str]) { } } -/// Replace the spans on screen, `(text frame, y-up rect, link, text)`: each text frame's spans take -/// its pooled buttons in order, a new one built when the pool runs out, and the buttons left over, -/// or of a text frame with no span now, hide. +/// Replace the spans on screen, `(text frame, y-up rect, link, text)`: each ScrollingMessageFrame's +/// spans take its pooled buttons in order, a new one built when the pool runs out, and the buttons +/// left over, or of a text frame with no span now, hide. A MessageFrame's spans stay text. pub(super) fn set_spans(lua: &Lua, spans: Vec<(FrameHandle, Rect, String, String)>) { // One span: its rect, link and text. type Span = (Rect, String, String); @@ -100,7 +100,12 @@ pub(super) fn set_spans(lua: &Lua, spans: Vec<(FrameHandle, Rect, String, String .link_buttons .retain(|owner, _| model.arena.frame(*owner).is_some()); for (owner, list) in &by_owner { - if model.arena.frame(*owner).is_none() { + // Only the SMF (`0x788ce7`) and SimpleHTML (`0x78b052`) take one; ours draws no links. + if !model + .arena + .frame(*owner) + .is_some_and(|f| matches!(f.kind_state, KindState::ScrollingMessage(_))) + { continue; } for (i, (rect, link, text)) in list.iter().enumerate() { diff --git a/crates/benilla-ui/src/script/tests/hyperlink.rs b/crates/benilla-ui/src/script/tests/hyperlink.rs index 30ae22700..2f2f73017 100644 --- a/crates/benilla-ui/src/script/tests/hyperlink.rs +++ b/crates/benilla-ui/src/script/tests/hyperlink.rs @@ -29,14 +29,14 @@ fn chat() -> UiScript { ) .unwrap(); s.resolve(); - set_link(&mut s); + set_link(&mut s, "CF"); s } -fn set_link(s: &mut UiScript) { - let cf = s.model_ref().arena.lookup("CF").expect("CF"); +fn set_link(s: &mut UiScript, owner: &str) { + let owner = s.model_ref().arena.lookup(owner).expect("owner"); s.set_link_spans(vec![( - cf, + owner, Rect::new(120.0, 110.0, 134.0, 160.0), LINK.to_string(), MARKUP.to_string(), @@ -140,3 +140,25 @@ fn a_hittable_body_title_drags_but_its_link_still_clicks() { assert!(s.eval::("return CF:IsUserPlaced()").unwrap()); assert!(s.errors().is_empty(), "{:?}", s.errors()); } + +/// The take `0x7a3880` is called from the ScrollingMessageFrame and SimpleHTML updates alone; the +/// MessageFrame's update (`0x786200`) reaches neither, so a span on one builds no button and the +/// pointer over it passes through. +#[test] +fn a_message_frame_line_gets_no_link_button() { + let mut s = script(); + s.set_screen_size(800.0, 600.0); + s.run( + r#" + EF = CreateFrame("MessageFrame", "EF") + EF:SetPoint("BOTTOMLEFT", 100, 100); EF:SetWidth(430); EF:SetHeight(120) + "#, + ) + .unwrap(); + s.resolve(); + set_link(&mut s, "EF"); + assert_eq!(s.eval::("return EF:GetNumChildren()").unwrap(), 0); + s.mouse_move(130.0, 127.0); + assert!(s.eval::("return GetMouseFocus() == nil").unwrap()); + assert!(s.errors().is_empty(), "{:?}", s.errors()); +}