Layout-mode drag, title press takes the slot, links are their own button (#961)
A title-region press refused while another drag held the slot, `SetLayoutMode` stored nothing, and a press on a chat link made the chat frame itself the target, so every link click started a title move of the window. In 1.12.1 the drag entry takes the slot unread (`0x7652b0`); a mid-drag press goes to the frozen hover (`0x76637f`). With `SetLayoutMode` (`0x488540`) on, a Ctrl or Alt press drags the nearest movable toplevel (`0x765490`) from a 3×3 grip grid (`0x765320`) and is consumed, world included; its release only ends the drag (`0x7664e0`). Each link span is its own Button (`0x7a3240`) routing `OnHyperlinkEnter`/`Leave`/`Click` to the parent (`0x7a3780`–`0x7a37e0`). These now hold; a release goes to the capture whatever its button (`0x76642b`), and number arguments keep `_ftol`'s low dword. Tests bite on the parent for each. Fixes #951 Fixes #952 Fixes #957 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
108c354fc6
commit
985dabff44
@@ -2,10 +2,11 @@
|
||||
//! turn host pointer events into FrameScript handler calls. [`cursor`] owns the payload and
|
||||
//! gesture state; this file dispatches.
|
||||
//!
|
||||
//! A frame captures the cursor when it is mouse-enabled and effective-visible and its rect and
|
||||
//! effective ScrollFrame clip hold the point (the reference's probe `0x76b020`), taking the first
|
||||
//! in the order of the hover walk (`0x7661cd` in `0x7660d0`), which [`crate::order::hit_test`]
|
||||
//! implements.
|
||||
//! A frame captures the cursor when it is in the mouse index, which `0x76af00` enters it into
|
||||
//! while it has the mouse bit (`[+0xcc] & 4`) and is visible (`[+0xd4]`), and the probe `0x76b020`
|
||||
//! takes the point: its rect is valid, and its hit rect and effective ScrollFrame clip hold the
|
||||
//! point. The first such frame in the order of the hover walk (`0x7661cd` in `0x7660d0`) wins,
|
||||
//! which [`crate::order::hit_test`] implements.
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
@@ -58,6 +59,19 @@ pub(super) fn install(lua: &mlua::Lua) -> mlua::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The frame a release of `button` goes to: the release handler reads only the capture
|
||||
/// (`0x76642b`), which `mouse_down_on` keeps per button as the press that set it, `None` once that
|
||||
/// frame hid. A button with no press of its own, one a title or layout press took, releases onto
|
||||
/// whatever capture is held.
|
||||
fn release_target(model: &Model, button: &str) -> Option<FrameHandle> {
|
||||
match model.mouse_down_on.get(button) {
|
||||
Some(&pressed) => pressed,
|
||||
None => model
|
||||
.mouse_capture
|
||||
.filter(|&c| model.arena.frame(c).is_some()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether `h` is the world frame, whose mouse hits belong to the 3D world; it wins the hover walk
|
||||
/// only when nothing else mouse-enabled is under the cursor.
|
||||
fn is_world_handle(model: &Model, h: FrameHandle) -> bool {
|
||||
@@ -81,15 +95,6 @@ fn over_world(model: &Model, target: Option<FrameHandle>) -> bool {
|
||||
target.is_none_or(|h| is_world_handle(model, h))
|
||||
}
|
||||
|
||||
/// Whether `(x, y)` falls inside one of this frame's hyperlink spans, the engine half of the
|
||||
/// reference's per-span `CSimpleHyperlinkButton` children.
|
||||
fn link_span_hit(model: &Model, fh: FrameHandle, x: f32, y: f32) -> bool {
|
||||
model
|
||||
.link_spans
|
||||
.get(&fh)
|
||||
.is_some_and(|spans| spans.iter().any(|(r, _, _)| point_in_rect(*r, x, y)))
|
||||
}
|
||||
|
||||
impl UiScript {
|
||||
/// Whether a frame id names the world frame: hit-tested like any frame, so scripts on it fire,
|
||||
/// but not counted by the app as over the UI.
|
||||
@@ -125,11 +130,10 @@ impl UiScript {
|
||||
}),
|
||||
);
|
||||
let hit = order::hit_test(&sorted, |fh| {
|
||||
// Mouse-enabled, or over one of the frame's hyperlink spans. The chat frame takes no
|
||||
// mouse (`[+0xcc] = 0`); the reference gives each span a mouse-enabled
|
||||
// `CSimpleHyperlinkButton` child (`0x7a3240`) that routes its scripts to the parent, so
|
||||
// a chat window hits only over a link and drags by its tab, not its body.
|
||||
let indexed = model.arena.is_mouse_enabled(fh) || link_span_hit(&model, fh, x, y);
|
||||
// In the mouse index. The chat frame takes no mouse (`[+0xcc] = 0`): its links are
|
||||
// Button children of their own ([`super::hyperlink`]), so a chat window hits only over
|
||||
// a link and drags by its tab, not its body.
|
||||
let indexed = model.arena.is_mouse_enabled(fh);
|
||||
// The nameplate's `+0x3c` veto (`0x7cba30`), not its mouse-enabled bit: while a
|
||||
// ground-targeted spell is armed a plate refuses the hit before its rect is tested,
|
||||
// so the reticle is placed through it.
|
||||
@@ -274,9 +278,13 @@ impl UiScript {
|
||||
super::mouseover::leave(&self.lua, h, script);
|
||||
}
|
||||
if let Some(nid) = enter_id {
|
||||
// The base enter's plain runner (`0x76b6e5` → `0x702690`) sets no arg global.
|
||||
if let Err(e) = event::fire_widget_handler(&self.lua, nid, "OnEnter", Vec::new()) {
|
||||
self.push_error(e);
|
||||
// A hyperlink button's enter is its text frame's `OnHyperlinkEnter` (`0x7a34b0`); the
|
||||
// base enter's plain runner (`0x76b6e5` → `0x702690`) sets no arg global.
|
||||
let routed = new_handle.is_some_and(|h| super::hyperlink::hover(&self.lua, h, true));
|
||||
if !routed {
|
||||
if let Err(e) = event::fire_widget_handler(&self.lua, nid, "OnEnter", Vec::new()) {
|
||||
self.push_error(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
new_id
|
||||
@@ -294,12 +302,55 @@ impl UiScript {
|
||||
.is_some_and(|r| point_in_rect(*r, x, y))
|
||||
}
|
||||
|
||||
/// `0x765490`'s walk: strata FULLSCREEN_DIALOG down to WORLD, never TOOLTIP (the loop starts
|
||||
/// below `root+0xcf4`, `0x7654a3`), each level high to low, and each level's list from its head
|
||||
/// (the terminator's `next`, `[level+0x14]`) along each link's `next` (`+4`, `0x766aa0`). A
|
||||
/// frame links at the tail (`0x7648c9` → `0x7668c0` → `0x766921`), so that is the oldest link
|
||||
/// first, the hover sweep's order. Every shown frame is probed with `0x76b020` alone, whatever
|
||||
/// its mouse bit. The first hit is lifted to its nearest toplevel self-or-ancestor
|
||||
/// (`0x766fe0`); one with none gives up the rest of its stratum and the walk goes on in the
|
||||
/// next below (`0x76554a` → `0x765532`).
|
||||
fn layout_drag_target(&self, x: f32, y: f32) -> Option<FrameHandle> {
|
||||
let model = self.model_ref();
|
||||
let sorted = order::traversal(&model.arena);
|
||||
let scroll_sources = scroll_clip_sources(&model);
|
||||
let mut below = order::Strata::Tooltip;
|
||||
loop {
|
||||
let hit = order::hit_test(&sorted, |fh| {
|
||||
model.arena.frame(fh).is_some_and(|f| f.strata < below)
|
||||
&& frame_contains(&model, &scroll_sources, fh, x, y)
|
||||
})?;
|
||||
if let Some(t) = super::object::toplevel::nearest_toplevel(&model, hit) {
|
||||
return Some(t);
|
||||
}
|
||||
below = model.arena.frame(hit)?.strata;
|
||||
}
|
||||
}
|
||||
|
||||
/// A mouse button press or release at `(x, y)`; `button` is the WoW name (`"LeftButton"`, …).
|
||||
/// `OnClick` follows `RegisterForClicks` ([`button::wants_click`], default `{"LeftButtonUp"}`):
|
||||
/// a press fires it for `"<Button>ButtonDown"`, a release on the pressed frame for
|
||||
/// `"<Button>ButtonUp"`. Its `down` argument is not 1.12, whose firer (`0x779540`) passes only
|
||||
/// the button name. Returns whether the UI consumed the event.
|
||||
pub fn mouse_button(&mut self, x: f32, y: f32, button: &str, down: bool) -> bool {
|
||||
// ── The layout drag, ahead of everything ─────────────────────────────────────────────────
|
||||
//
|
||||
// With `SetLayoutMode` on, a press with Control (`0x41f8f0(1)`) or Alt (`0x41f8f0(2)`) held
|
||||
// goes to `0x765490` as drag mode 1, Control asking for a size, and the handler returns
|
||||
// consumed whatever that started (`0x766301`–`0x76635b`): no raise, title-region test,
|
||||
// capture or press slot.
|
||||
if down {
|
||||
let (_, ctrl, alt) = self.model_ref().modifiers;
|
||||
if self.model_ref().layout_mode != 0 && (ctrl || alt) {
|
||||
let target = self.layout_drag_target(x, y);
|
||||
let mut model = self.model_mut();
|
||||
model.cursor_pos = (x, y);
|
||||
if let Some(h) = target {
|
||||
crate::script::object::movable::start_layout_drag(&mut model, h, (x, y), ctrl);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
let hit_id = self.hit_test(x, y);
|
||||
// ── The press target: the capture, else the hover ──────────────────────────────────────
|
||||
//
|
||||
@@ -309,6 +360,11 @@ impl UiScript {
|
||||
// drag arm, a Button's press art and Down click), runs on that one frame. So a second
|
||||
// button pressed over another frame fires on the held one. `mouse_down_on` keeps the
|
||||
// frame per button for the release.
|
||||
//
|
||||
// The hover is what the last walk left. Every press follows a move to its point, so that
|
||||
// is the frame under it, except while a move or size holds the slot: the walk then pumps
|
||||
// it and returns before its sweep (`0x766164`), and the hover stays the frame it was when
|
||||
// the drag began.
|
||||
let mut release_lost = false;
|
||||
let press_target: Option<crate::widget::FrameHandle> = if down {
|
||||
let mut model = self.model_mut();
|
||||
@@ -328,7 +384,12 @@ impl UiScript {
|
||||
let held = model
|
||||
.mouse_capture
|
||||
.filter(|&c| model.arena.frame(c).is_some());
|
||||
held.or_else(|| hit_id.and_then(|id| model.id_to_frame.get(&id).copied()))
|
||||
let hover = if model.moving.is_some() {
|
||||
model.mouseover.filter(|&h| model.arena.frame(h).is_some())
|
||||
} else {
|
||||
hit_id.and_then(|id| model.id_to_frame.get(&id).copied())
|
||||
};
|
||||
held.or(hover)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
@@ -347,9 +408,9 @@ impl UiScript {
|
||||
// ── The title region swallows the press ─────────────────────────────────────────────────
|
||||
//
|
||||
// A press inside the target's title region starts a mode-2 move and ends there:
|
||||
// `0x7662c0` tests the region first and only a miss reaches `0x7663e6`, the capture and
|
||||
// `OnMouseDown`. So no `OnMouseDown`, drag arm, `OnClick` or double-click stamp, and the
|
||||
// event is consumed.
|
||||
// `0x7662c0` tests the region before the capture (`0x766397`–`0x7663e5`) and only a miss
|
||||
// reaches `0x7663e6`, the capture and `OnMouseDown`. So no `OnMouseDown`, drag arm,
|
||||
// `OnClick` or double-click stamp, and the event is consumed.
|
||||
if let Some(h) = press_target {
|
||||
if self.title_region_hit(h, x, y) {
|
||||
self.model_mut().cursor_pos = (x, y);
|
||||
@@ -357,11 +418,14 @@ impl UiScript {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// ── The release ends a title move ───────────────────────────────────────────────────────
|
||||
// ── The release ends a title move, and that is all it does ─────────────────────────────
|
||||
//
|
||||
// `0x766420` cancels move modes 1 (modifier drag) and 2 (title region) on release and
|
||||
// leaves mode 3 (`StartMoving`) to the addon's `StopMovingOrSizing`;
|
||||
// `FrameMove::auto_stop` is that distinction.
|
||||
// `0x766420` tests the drag mode before anything else (`0x76646d`–`0x76647c`): any release
|
||||
// during mode 1 (modifier drag) or 2 (title region) clears the slot (`0x765640`) and
|
||||
// returns consumed, with no `OnMouseUp`, drag stop or click on the capture, which it
|
||||
// leaves held for a later release. Mode 3 (`StartMoving`) is left to the addon's
|
||||
// `StopMovingOrSizing`; `FrameMove::auto_stop` is that distinction. The button is up all
|
||||
// the same, so its own press entry goes.
|
||||
if !down
|
||||
&& self
|
||||
.model_ref()
|
||||
@@ -369,19 +433,17 @@ impl UiScript {
|
||||
.as_ref()
|
||||
.is_some_and(|m| m.auto_stop)
|
||||
{
|
||||
self.model_mut().moving = None;
|
||||
let mut model = self.model_mut();
|
||||
model.moving = None;
|
||||
model.mouse_down_on.remove(button);
|
||||
return true;
|
||||
}
|
||||
// The press this release belongs to, read before the borrow below drains `mouse_down_on`.
|
||||
// The frame this release goes to, read before the borrow below drains `mouse_down_on`.
|
||||
let captured_id: Option<u32> = if down {
|
||||
None
|
||||
} else {
|
||||
let model = self.model_ref();
|
||||
model
|
||||
.mouse_down_on
|
||||
.get(button)
|
||||
.copied()
|
||||
.flatten()
|
||||
.and_then(|h| model.frame_to_id.get(&h).copied())
|
||||
release_target(&model, button).and_then(|h| model.frame_to_id.get(&h).copied())
|
||||
};
|
||||
// The `GetTime()` clock for the double-click detector, read before the borrow.
|
||||
let now = self.now();
|
||||
@@ -456,8 +518,8 @@ impl UiScript {
|
||||
.and(press_id);
|
||||
(click, None, false, jump, None, abandoned, color_jump)
|
||||
} else {
|
||||
let pressed = release_target(&model, button);
|
||||
let entry = model.mouse_down_on.remove(button);
|
||||
let pressed = entry.flatten();
|
||||
// The pressed frame hid (`None` entry): the capture is gone, so nothing of this
|
||||
// release runs, the world click included (`over_world(None)` would take it).
|
||||
let capture_lost = entry == Some(None);
|
||||
@@ -539,43 +601,6 @@ impl UiScript {
|
||||
self.push_error(e);
|
||||
}
|
||||
}
|
||||
// A release in a hyperlink span fires `OnHyperlinkClick(link, markup, button)` on the
|
||||
// owning frame, except after a started drag.
|
||||
if !down && !drag_release.as_ref().is_some_and(|r| r.started) {
|
||||
if let Some(id) = hit_id {
|
||||
let span = {
|
||||
let model = self.model_ref();
|
||||
model
|
||||
.id_to_frame
|
||||
.get(&id)
|
||||
.and_then(|fh| model.link_spans.get(fh))
|
||||
.and_then(|spans| {
|
||||
spans
|
||||
.iter()
|
||||
.find(|(r, _, _)| point_in_rect(*r, x, y))
|
||||
.map(|(_, l, m)| (l.clone(), m.clone()))
|
||||
})
|
||||
};
|
||||
if let Some((link, markup)) = span {
|
||||
let args = match (
|
||||
self.lua.create_string(&link),
|
||||
self.lua.create_string(&markup),
|
||||
) {
|
||||
(Ok(l), Ok(m)) => {
|
||||
vec![Value::String(l), Value::String(m), btn.clone()]
|
||||
}
|
||||
_ => Vec::new(),
|
||||
};
|
||||
if !args.is_empty() {
|
||||
if let Err(e) =
|
||||
event::fire_widget_handler(&self.lua, id, "OnHyperlinkClick", args)
|
||||
{
|
||||
self.push_error(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// A left press in an EditBox places the cursor, starts a selection drag and focuses it
|
||||
// whatever its `autoFocus`, before any `OnClick` (`0x77b800`).
|
||||
if button == "LeftButton" {
|
||||
|
||||
Reference in New Issue
Block a user