ui: OnHyperlinkEnter/OnHyperlinkLeave fire as the cursor crosses a link span
SetScript raised "unsupported script 'OnHyperlinkEnter'" for chat-link addons, because only OnHyperlinkClick was registered. The reference fires the enter/leave pair on the owning frame with (link, text) as the hover crosses a link span, so the kinds are now accepted and fired from the pointer's move and when a hovered frame hides.
This commit is contained in:
@@ -128,7 +128,7 @@ pub(super) fn fire_visibility_changes(lua: &Lua, changed: Vec<FrameHandle>) {
|
||||
// before its `OnHide` (`0x764ba0`'s tail in `0x76ad50`, the leave at `0x764cce`), clears the
|
||||
// hover and the drag-arm (`+0x100`/`+0x104`) and arms the re-pick; a show arms it too
|
||||
// (`0x764b8d`). This leave is how a tooltip closes with its window; the reference has no other.
|
||||
let left: Option<(FrameHandle, u32)> = {
|
||||
let (left, link_left): (Option<(FrameHandle, u32)>, Option<(u32, String, String)>) = {
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model");
|
||||
let hidden_hover = model
|
||||
.mouseover
|
||||
@@ -136,6 +136,19 @@ pub(super) fn fire_visibility_changes(lua: &Lua, changed: Vec<FrameHandle>) {
|
||||
// The leave is a virtual call (`0x764cce`, `[vtable+0x50]`): a disabled Button's own
|
||||
// `0x7794e0` skips its `OnLeave`, while the hover and the drag-arm still clear.
|
||||
let notified = super::button::hover_notify_runs(&model, hidden_hover);
|
||||
// A hidden frame's hovered hyperlink span leaves with it, so a chat-link tooltip closes.
|
||||
let link_hidden = model
|
||||
.hover_link
|
||||
.as_ref()
|
||||
.is_some_and(|(fh, _, _)| items.iter().any(|&(h, _, vis)| h == *fh && !vis));
|
||||
let link_left = if link_hidden {
|
||||
model
|
||||
.hover_link
|
||||
.take()
|
||||
.map(|(fh, link, markup)| (model.frame_id(fh), link, markup))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if let Some(m) = hidden_hover {
|
||||
model.mouseover = None;
|
||||
if model.drag.as_ref().is_some_and(|d| d.source == m) {
|
||||
@@ -143,10 +156,27 @@ pub(super) fn fire_visibility_changes(lua: &Lua, changed: Vec<FrameHandle>) {
|
||||
}
|
||||
}
|
||||
model.hover_repick |= hidden_hover.is_some() || items.iter().any(|&(_, _, vis)| vis);
|
||||
hidden_hover
|
||||
.filter(|_| notified)
|
||||
.map(|m| (m, model.frame_id(m)))
|
||||
(
|
||||
hidden_hover
|
||||
.filter(|_| notified)
|
||||
.map(|m| (m, model.frame_id(m))),
|
||||
link_left,
|
||||
)
|
||||
};
|
||||
if let Some((oid, link, markup)) = link_left {
|
||||
if let (Ok(l), Ok(m)) = (lua.create_string(&link), lua.create_string(&markup)) {
|
||||
if let Err(e) = fire_widget_handler(
|
||||
lua,
|
||||
oid,
|
||||
"OnHyperlinkLeave",
|
||||
vec![Value::String(l), Value::String(m)],
|
||||
) {
|
||||
lua.app_data_mut::<Model>()
|
||||
.expect("model")
|
||||
.record_script_error(e.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some((_, oid)) = left {
|
||||
if let Err(e) = fire_widget_handler(lua, oid, "OnLeave", vec![Value::Boolean(true)]) {
|
||||
lua.app_data_mut::<Model>()
|
||||
|
||||
@@ -383,7 +383,7 @@ pub const SCREEN: crate::layout::Handle = 0;
|
||||
/// ScrollFrame's three scroll kinds are `+0x32c`/`+0x334`/`+0x33c` (script-name map `0x786c40`).
|
||||
/// The EditBox's vtable (`0x81c910`) replaces the key and char slots: an EditBox never fires
|
||||
/// `OnKeyDown`, and fires `OnChar` only from `Insert`, with the inserted text (`0x77c13c`).
|
||||
const SCRIPT_KINDS: [&str; 39] = [
|
||||
const SCRIPT_KINDS: [&str; 41] = [
|
||||
"OnLoad",
|
||||
"OnEvent",
|
||||
"OnUpdate",
|
||||
@@ -420,6 +420,10 @@ const SCRIPT_KINDS: [&str; 39] = [
|
||||
// A release over a message-frame hyperlink span, `OnHyperlinkClick(link, text, button)`;
|
||||
// `ChatFrameTemplate` passes it to `SetItemRef` (`ChatFrame.xml:15`, `ChatFrame.lua:1534`).
|
||||
"OnHyperlinkClick",
|
||||
// The per-span hover pair, fired by [`pointer`]'s move as the cursor crosses a link span of the
|
||||
// hovered frame (`0x4e2e30`/`0x4e2ee0`, the chat frames' item and ability tooltips).
|
||||
"OnHyperlinkEnter",
|
||||
"OnHyperlinkLeave",
|
||||
// The GameTooltip's engine-fired scripts: money render, money clear and world-hover default
|
||||
// placement, all three wired by the stock template (`GameTooltipTemplate.xml:617-625`).
|
||||
"OnTooltipAddMoney",
|
||||
|
||||
@@ -196,6 +196,9 @@ pub(crate) struct Model {
|
||||
/// position and a newly exposed frame gets `OnEnter` without a mouse move: the reference's
|
||||
/// `[root+0x1100]` flag (set at `0x764cbb`/`0x764b8d`, pumped at `0x7657a1` into `0x7660d0`).
|
||||
pub(crate) hover_repick: bool,
|
||||
/// The hyperlink span the cursor is inside, `(frame, link, markup)`, for the next move's
|
||||
/// `OnHyperlinkLeave`/`OnHyperlinkEnter`; `None` off every span.
|
||||
pub(crate) hover_link: Option<(FrameHandle, String, String)>,
|
||||
/// Per button, the frame its press landed on, for `OnClick`'s same-frame release test.
|
||||
pub(crate) mouse_down_on: HashMap<String, FrameHandle>,
|
||||
/// The client's one mouse-capture slot (`root+0x80`), read by the mouse-down raise. Set at
|
||||
@@ -1045,6 +1048,7 @@ impl Model {
|
||||
focused_editbox: None,
|
||||
mouseover: None,
|
||||
hover_repick: false,
|
||||
hover_link: None,
|
||||
mouse_down_on: HashMap::new(),
|
||||
mouse_capture: None,
|
||||
last_click: HashMap::new(),
|
||||
|
||||
@@ -211,9 +211,9 @@ pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
}
|
||||
|
||||
/// `SetScript(name, func)`: stores the closure under one of [`SCRIPT_KINDS`], else raises.
|
||||
/// Deviation: real 1.12 kinds nothing fires yet raise too (`OnHyperlinkEnter`, `OnHyperlinkLeave`,
|
||||
/// `OnMessageScrollChanged`, `OnInputLanguageChanged`, the movie frame's), because an accepted
|
||||
/// handler that never runs fails silently. `OnAttributeChanged` is 2.0's, with no 1.12 slot.
|
||||
/// Deviation: real 1.12 kinds nothing fires yet raise too (`OnMessageScrollChanged`,
|
||||
/// `OnInputLanguageChanged`, the movie frame's), because an accepted handler that never runs fails
|
||||
/// silently. `OnAttributeChanged` is 2.0's, with no 1.12 slot.
|
||||
fn set_script(lua: &Lua, this: &Table, name: &str, func: Option<Function>) -> mlua::Result<()> {
|
||||
let kind = SCRIPT_KINDS
|
||||
.iter()
|
||||
|
||||
@@ -90,6 +90,33 @@ impl UiScript {
|
||||
.is_some_and(|h| is_world_handle(&model, *h))
|
||||
}
|
||||
|
||||
/// The hyperlink span containing `(x, y)` on the frame `id`, as `(frame, link, markup)`.
|
||||
fn span_at(&self, id: u32, x: f32, y: f32) -> Option<(FrameHandle, String, String)> {
|
||||
let model = self.model_ref();
|
||||
let fh = model.id_to_frame.get(&id).copied()?;
|
||||
model
|
||||
.link_spans
|
||||
.get(&fh)?
|
||||
.iter()
|
||||
.find(|(r, _, _)| point_in_rect(*r, x, y))
|
||||
.map(|(_, link, markup)| (fh, link.clone(), markup.clone()))
|
||||
}
|
||||
|
||||
/// Fire a hyperlink script with `(link, markup)`, recording a handler error.
|
||||
fn fire_hyperlink(&mut self, id: u32, script: &str, link: &str, markup: &str) {
|
||||
let (Ok(l), Ok(m)) = (self.lua.create_string(link), self.lua.create_string(markup)) else {
|
||||
return;
|
||||
};
|
||||
if let Err(e) = event::fire_widget_handler(
|
||||
&self.lua,
|
||||
id,
|
||||
script,
|
||||
vec![Value::String(l), Value::String(m)],
|
||||
) {
|
||||
self.push_error(e);
|
||||
}
|
||||
}
|
||||
|
||||
/// The handle of the frame that captures `(x, y)`, for a caller that checks it against a
|
||||
/// handle set taken earlier (an anonymous frame has no name). Needs [`Self::resolve`] first.
|
||||
pub fn hit_test_frame(&self, x: f32, y: f32) -> Option<FrameHandle> {
|
||||
@@ -239,6 +266,25 @@ impl UiScript {
|
||||
self.push_error(e);
|
||||
}
|
||||
}
|
||||
// Crossing into or out of a hyperlink span fires `OnHyperlinkEnter`/`OnHyperlinkLeave`
|
||||
// `(link, text)` on the owning frame; a chat frame carries no `OnEnter` of its own but its
|
||||
// spans do, and the leave must fire when the cursor drops onto bare text in the same frame.
|
||||
let new_span = new_id.and_then(|id| self.span_at(id, x, y));
|
||||
let old_span = std::mem::replace(&mut self.model_mut().hover_link, new_span.clone());
|
||||
if old_span != new_span {
|
||||
if let Some((fh, link, markup)) = old_span {
|
||||
let id = self.model_ref().frame_to_id.get(&fh).copied();
|
||||
if let Some(id) = id {
|
||||
self.fire_hyperlink(id, "OnHyperlinkLeave", &link, &markup);
|
||||
}
|
||||
}
|
||||
if let Some((fh, link, markup)) = new_span {
|
||||
let id = self.model_ref().frame_to_id.get(&fh).copied();
|
||||
if let Some(id) = id {
|
||||
self.fire_hyperlink(id, "OnHyperlinkEnter", &link, &markup);
|
||||
}
|
||||
}
|
||||
}
|
||||
new_id
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user