Hover walk: no arg1, held during a move, no fling, published mouseover (#941)
Four pointer defects: OnEnter and OnLeave set `arg1 = true`; a frame being moved or sized still ran the hover walk; the pointer leaving the window fed `f32::MIN` to a move in flight, flinging the frame off-screen; and the mouseover token read the world pick, not the published mouseover. In 1.12.1 both scripts run through `0x702690`, which sets no arg global. While `[root+0xcfc]` holds a move or size target, the walk pumps it (`0x7655b0`) and returns before any probe (`0x766164`–`0x7661a0`); a hide clears that slot (`0x76492e`). The pump only sees real cursor events. The token resolver reads the published pair `[0xb4e2c8]` (`0x515970`). OnEnter and OnLeave fire with no arguments, `mouse_move` pumps a move or size and returns, a hide ends it, the pointer leave has its own call, and the aura feed reads the published mouseover. Tests bite on the parent for each. Fixes #932 Fixes #933 Fixes #936 Fixes #938 🤖 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
0fc40d3f87
commit
73623e31fa
@@ -93,7 +93,14 @@ impl Plugin for UiAuraPlugin {
|
||||
.init_resource::<PlayerAuraCache>()
|
||||
.init_resource::<AuraFeedMemory>()
|
||||
// Feed before the VM dispatch, drain after it.
|
||||
.add_systems(Update, feed_auras.in_set(UnitFeed).in_set(AuraEvents))
|
||||
// After this frame's world publish, so the gain names the unit in the same frame.
|
||||
.add_systems(
|
||||
Update,
|
||||
feed_auras
|
||||
.in_set(UnitFeed)
|
||||
.in_set(AuraEvents)
|
||||
.after(crate::ui_tooltip::MouseoverPublish),
|
||||
)
|
||||
.add_systems(Update, drain_aura_cancels.after(UiInput))
|
||||
// Torn down only at the session's end, not with the avatar, which a worldport drops.
|
||||
.add_systems(OnExit(ClientState::InWorld), end_session_aura_state);
|
||||
@@ -377,7 +384,8 @@ fn tracking_state_of(
|
||||
})
|
||||
}
|
||||
|
||||
/// The units the resolver's guid globals name, from `UnitTokens`' own sources.
|
||||
/// The units the resolver's guid globals name, from `UnitTokens`' own sources; the mouseover is
|
||||
/// the VM's published one.
|
||||
#[derive(bevy::ecs::system::SystemParam)]
|
||||
struct AuraUnits<'w> {
|
||||
selection: Res<'w, Selection>,
|
||||
@@ -385,8 +393,6 @@ struct AuraUnits<'w> {
|
||||
// `Option`: a UI-only harness lacks their plugins; without the party's no group token names
|
||||
// anyone.
|
||||
group: Option<Res<'w, crate::ui_party::GroupState>>,
|
||||
hovered: Option<Res<'w, crate::target::Hovered>>,
|
||||
hovered_go: Option<Res<'w, crate::target::HoveredObject>>,
|
||||
interact: Option<Res<'w, crate::ui_session::InteractNpc>>,
|
||||
}
|
||||
|
||||
@@ -530,6 +536,13 @@ fn feed_auras(
|
||||
)
|
||||
};
|
||||
let target_guid = units.selection.guid.unwrap_or(0);
|
||||
// The resolver reads the published pair `[0xb4e2c8]` (`0x515bb6`, `0x515bdb`), not the pick,
|
||||
// and takes it only as a unit (`0x515bca mov ecx,8`, `0x515bd9 je`): a GameObject, a corpse or
|
||||
// nothing names nobody.
|
||||
let mouseover = script
|
||||
.world_mouseover()
|
||||
.filter(|&guid| held(guid).is_some_and(|s| s.is_unit()))
|
||||
.unwrap_or(0);
|
||||
let catalog_moved = spells.as_ref().is_some_and(|s| s.is_changed());
|
||||
let unit_changed = memo.units.feed(
|
||||
&mut script,
|
||||
@@ -538,14 +551,7 @@ fn feed_auras(
|
||||
me: (self_guid.0, changed),
|
||||
pet: pet_guid,
|
||||
target: target_guid,
|
||||
// The world pick, as `UnitTokens` resolves `"mouseover"`.
|
||||
mouseover: units
|
||||
.hovered
|
||||
.as_deref()
|
||||
.and_then(|h| {
|
||||
h.mouseover(&units.hovered_go.as_deref().copied().unwrap_or_default())
|
||||
})
|
||||
.map_or(0, |(_, guid)| guid),
|
||||
mouseover,
|
||||
npc: units.interact.as_deref().and_then(|i| i.1).unwrap_or(0),
|
||||
held,
|
||||
buffs_visible,
|
||||
|
||||
@@ -965,10 +965,18 @@ mod tests {
|
||||
(selection.target, selection.guid) = (Some(mob), Some(MOB));
|
||||
let mut hovered = app.world_mut().resource_mut::<crate::target::Hovered>();
|
||||
(hovered.target, hovered.guid) = (Some(add), Some(ADD));
|
||||
publish_mouseover(&mut app, Some(ADD));
|
||||
app.update();
|
||||
app
|
||||
}
|
||||
|
||||
/// The world mouseover as the publisher (`0x492890`) writes it to `[0xb4e2c8]`.
|
||||
fn publish_mouseover(app: &mut App, guid: Option<u64>) {
|
||||
app.world_mut()
|
||||
.non_send_resource_mut::<UiScript>()
|
||||
.publish_world_mouseover(guid);
|
||||
}
|
||||
|
||||
/// `UnitDebuff` reads the list of whatever unit the token names, through the resolver
|
||||
/// (`0x515970`, called at `0x519542`): the mouseover, a member's target, any depth of `target`
|
||||
/// chain, in any case, and the player's own list at the end of a chain that comes back to us.
|
||||
@@ -1036,11 +1044,31 @@ mod tests {
|
||||
guid: Some(0xF110_0000_0000_0004),
|
||||
distance: 5.0,
|
||||
});
|
||||
publish_mouseover(&mut app, Some(0xF110_0000_0000_0004));
|
||||
app.update();
|
||||
assert!(listed(&mut app, "mouseover", true).is_empty());
|
||||
assert!(listed(&mut app, "mouseovertarget", true).is_empty());
|
||||
}
|
||||
|
||||
/// A hovered plate retired after the pick: its leave (`0x7cb8b0`) publishes the empty
|
||||
/// mouseover, while the pick still names the plate's unit. The resolver reads the published
|
||||
/// pair (`0x515bb6`, `0x515bdb`), so in the next frame `"mouseover"` and its chains name
|
||||
/// nobody.
|
||||
#[test]
|
||||
fn a_retired_hovered_plate_names_no_mouseover_in_the_next_frame() {
|
||||
let mut app = chain_app();
|
||||
assert_eq!(listed(&mut app, "mouseover", true), [770]);
|
||||
assert_eq!(listed(&mut app, "mouseovertarget", true), [6788]);
|
||||
app.world_mut()
|
||||
.resource_mut::<crate::target::Hovered>()
|
||||
.plate = true;
|
||||
publish_mouseover(&mut app, None);
|
||||
app.update();
|
||||
assert!(listed(&mut app, "mouseover", true).is_empty());
|
||||
assert!(listed(&mut app, "mouseovertarget", true).is_empty());
|
||||
assert!(listed(&mut app, "mouseovertargettarget", true).is_empty());
|
||||
}
|
||||
|
||||
/// A raid member's target that is out of view but on the roster reads its record, as
|
||||
/// `UnitDebuff`'s roster walk finds it (`0x519b25`); a hop off a unit not held is nobody.
|
||||
#[test]
|
||||
|
||||
@@ -194,7 +194,7 @@ pub(super) fn feed_ui_input(
|
||||
// The OS pointer left: leave the hovered frame once, and disarm any press or drag every
|
||||
// frame, since no release ends it and a stale gesture would fire on re-entry.
|
||||
if hover.0.take().is_some() {
|
||||
script.mouse_move(f32::MIN, f32::MIN);
|
||||
script.pointer_left_hover();
|
||||
}
|
||||
script.pointer_left_window();
|
||||
}
|
||||
|
||||
@@ -26,12 +26,19 @@ use spell_feed::feed_spell_tooltips;
|
||||
|
||||
pub struct UiTooltipPlugin;
|
||||
|
||||
/// The frame's world-mouseover publish ([`UiScript::publish_world_mouseover`]), which the readers
|
||||
/// of the published guid follow.
|
||||
#[derive(SystemSet, Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub(crate) struct MouseoverPublish;
|
||||
|
||||
impl Plugin for UiTooltipPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.add_systems(
|
||||
Update,
|
||||
(
|
||||
drive_mouseover_tooltip.in_set(UnitFeed),
|
||||
drive_mouseover_tooltip
|
||||
.in_set(UnitFeed)
|
||||
.in_set(MouseoverPublish),
|
||||
// After the trainer and quest feeds, so a list that lands this frame is hoverable
|
||||
// in its tick; outside `UnitFeed`, which the trainer feed follows, so it takes that
|
||||
// set's gate.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
//! Handler firing: the FrameScript calling convention for events, ticks and show/hide. The
|
||||
//! reference sets `this` (`0x872e64`), `event` (`0x84b648`) and `arg1..argN` (`0x8722dc`) around
|
||||
//! the call, restoring each after, and `pcall`s with no arguments; the handler here also gets the
|
||||
//! same values as `(self, event, ...)` arguments, which 1.12 does not pass.
|
||||
//! the call, restoring each after, and `pcall`s with no arguments; so does [`invoke_with_globals`],
|
||||
//! which sets only the args it is given, so a script fired with none leaves `arg1..argN` as found.
|
||||
//! Handler errors return to the caller, which records them in [`super::Model::errors`].
|
||||
|
||||
use std::borrow::Cow;
|
||||
|
||||
@@ -1001,6 +1001,33 @@ impl UiScript {
|
||||
|
||||
// ── Input: the pointer leaving the window ────────────────────────────────────────────────────
|
||||
|
||||
/// The OS pointer left the window: the hovered frame loses the hover, with its leave notify
|
||||
/// ([`mouseover::leave`]) as a walk that found nothing would run it. It is no cursor event, so
|
||||
/// it feeds no position: no probe runs and a move or size in flight holds its last sample
|
||||
/// (`[root+0xd08]`/`[root+0xd0c]`), as the reference's pump only sees real cursor events
|
||||
/// (`0x7655b0`). The saved cursor position stays too.
|
||||
pub fn pointer_left_hover(&mut self) {
|
||||
let leave = {
|
||||
let mut model = self.model_mut();
|
||||
// A frame destroyed since it took the hover gets no leave.
|
||||
let old = model
|
||||
.mouseover
|
||||
.take()
|
||||
.filter(|&h| model.arena.frame(h).is_some());
|
||||
old.map(|h| {
|
||||
let script = model
|
||||
.frame_to_id
|
||||
.get(&h)
|
||||
.copied()
|
||||
.filter(|_| button::hover_notify_runs(&model, Some(h)));
|
||||
(h, script)
|
||||
})
|
||||
};
|
||||
if let Some((h, script)) = leave {
|
||||
mouseover::leave(&self.lua, h, script);
|
||||
}
|
||||
}
|
||||
|
||||
/// The OS pointer left the window, so no release will be fed: clears [`Model::drag`], which
|
||||
/// would start a drag on re-entry, and [`Model::mouse_down_on`], which would click the frame
|
||||
/// it left. The app calls it where it fires the synthetic `OnLeave`.
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
//! mouseover from its own update (`0x481790`), which picks only while the WorldFrame holds the
|
||||
//! hover (`0x4817ae`), the app's world drive.
|
||||
|
||||
use mlua::{Lua, Value};
|
||||
use mlua::Lua;
|
||||
|
||||
use crate::widget::{FrameHandle, FrameKind};
|
||||
|
||||
@@ -35,9 +35,11 @@ fn leave_class(model: &Model, h: FrameHandle) -> LeaveClass {
|
||||
}
|
||||
}
|
||||
|
||||
/// A frame's leave notify, by its class, around its `OnLeave(true)`; `script` is the id whose
|
||||
/// `OnLeave` fires, `None` where the notify skips it (a disabled Button's `0x7794e0`). The walk's
|
||||
/// motion leave and the removal tail's leave (`0x764cce`) both come here.
|
||||
/// A frame's leave notify, by its class, around its `OnLeave`; `script` is the id whose `OnLeave`
|
||||
/// fires, `None` where the notify skips it (a disabled Button's `0x7794e0`). The walk's motion
|
||||
/// leave and the removal tail's leave (`0x764cce`) both come here. The notify's own argument, 0 for
|
||||
/// a motion leave and 1 for a removal, only clears the drag arm (`0x76b725`); `OnLeave` runs
|
||||
/// through the plain runner (`0x76b753` → `0x702690`), which sets no arg global.
|
||||
pub(super) fn leave(lua: &Lua, h: FrameHandle, script: Option<u32>) {
|
||||
let class = leave_class(&model(lua), h);
|
||||
match class {
|
||||
@@ -50,7 +52,7 @@ pub(super) fn leave(lua: &Lua, h: FrameHandle, script: Option<u32>) {
|
||||
LeaveClass::World | LeaveClass::Other => {}
|
||||
}
|
||||
if let Some(id) = script {
|
||||
if let Err(e) = event::fire_widget_handler(lua, id, "OnLeave", vec![Value::Boolean(true)]) {
|
||||
if let Err(e) = event::fire_widget_handler(lua, id, "OnLeave", Vec::new()) {
|
||||
model(lua).record_lua_error(&e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ mod layout_methods;
|
||||
pub(crate) use layout_methods::eff_scale;
|
||||
pub(crate) mod movable;
|
||||
pub(crate) mod toplevel;
|
||||
pub(crate) use movable::{advance_move, advance_size, FrameMove, FrameSizing};
|
||||
pub(crate) use movable::{cancel_on_hide, pump_move_or_size, FrameMove, FrameSizing};
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────────────────────
|
||||
// id ↔ lightuserdata
|
||||
|
||||
@@ -359,8 +359,29 @@ fn clamp_resize(model: &Model, h: FrameHandle, w: f32, hgt: f32) -> (f32, f32) {
|
||||
(axis(w, min.0, max.0), axis(hgt, min.1, max.1))
|
||||
}
|
||||
|
||||
/// The hover walk's move/size branch (`0x766164`–`0x766193`): while the drag slot holds a target,
|
||||
/// the walk pumps it to `pos` (`0x7655b0`) and returns before any probe. True when one is in
|
||||
/// flight, a target whose frame is gone dropped first.
|
||||
pub(crate) fn pump_move_or_size(model: &mut Model, pos: (f32, f32)) -> bool {
|
||||
advance_move(model, pos);
|
||||
advance_size(model, pos);
|
||||
model.moving.is_some() || model.sizing.is_some()
|
||||
}
|
||||
|
||||
/// A hide ends the move or size whose target is the frame going hidden: the hide's de-registration
|
||||
/// (`0x76ad9d` → `0x764920`, flag 0) clears the slot when it holds the frame
|
||||
/// (`0x76492e`–`0x76493a` → `0x765640`).
|
||||
pub(crate) fn cancel_on_hide(model: &mut Model, h: FrameHandle) {
|
||||
if model.moving.is_some_and(|mv| mv.frame == h) {
|
||||
model.moving = None;
|
||||
}
|
||||
if model.sizing.is_some_and(|sz| sz.frame == h) {
|
||||
model.sizing = None;
|
||||
}
|
||||
}
|
||||
|
||||
/// Pump an in-flight `StartSizing`: the gripped edges follow the cursor, the opposite ones stay.
|
||||
pub(crate) fn advance_size(model: &mut Model, pos: (f32, f32)) {
|
||||
fn advance_size(model: &mut Model, pos: (f32, f32)) {
|
||||
let Some(sz) = model.sizing else { return };
|
||||
if model.arena.frame(sz.frame).is_none() {
|
||||
model.sizing = None;
|
||||
@@ -441,10 +462,8 @@ pub(crate) fn advance_size(model: &mut Model, pos: (f32, f32)) {
|
||||
model.sizing = Some(FrameSizing { sample: pos, ..sz });
|
||||
}
|
||||
|
||||
/// Pump an in-flight move to the cursor at `pos` (`0x7655b0`). Called from
|
||||
/// [`crate::script::UiScript::mouse_move`] before its hover-boundary early return, since a frame
|
||||
/// dragged under the cursor crosses no boundary.
|
||||
pub(crate) fn advance_move(model: &mut Model, pos: (f32, f32)) {
|
||||
/// Pump an in-flight move to the cursor at `pos` (`0x7655b0`).
|
||||
fn advance_move(model: &mut Model, pos: (f32, f32)) {
|
||||
let Some(mv) = model.moving else { return };
|
||||
if model.arena.frame(mv.frame).is_none() {
|
||||
model.moving = None;
|
||||
|
||||
@@ -178,11 +178,26 @@ impl UiScript {
|
||||
model.arena.frame(fh)?.name.clone()
|
||||
}
|
||||
|
||||
/// Move the cursor to `(x, y)`. When the hovered frame changes, fires `OnLeave(self, true)` on
|
||||
/// the old one if still live and `OnEnter(self, true)` on the new; a disabled Button takes the
|
||||
/// hover but fires neither ([`button::hover_notify_runs`]). Drags and frame moves advance
|
||||
/// first. Returns the hovered frame id.
|
||||
/// Move the cursor to `(x, y)`. When the hovered frame changes, fires `OnLeave` on the old one
|
||||
/// if still live and `OnEnter` on the new, both with the arg globals untouched; a disabled
|
||||
/// Button takes the hover but fires neither ([`button::hover_notify_runs`]). A frame move or
|
||||
/// size in flight takes the move instead, and nothing else runs. Returns the hovered frame id.
|
||||
pub fn mouse_move(&mut self, x: f32, y: f32) -> Option<u32> {
|
||||
{
|
||||
let mut model = self.model_mut();
|
||||
// The walk saves the event before its move/size check (`0x76615f`), so the re-pick's
|
||||
// position follows the cursor through a drag.
|
||||
model.cursor_pos = (x, y);
|
||||
// A move or size in flight is pumped and the walk returns (`0x766164`–`0x7661a0`)
|
||||
// before any probe (`vtbl+0x3c`): no hit test, no drag start (`0x76b320`), no
|
||||
// EditBox, slider or colour thumb drag (`0x77a860`, `0x789ba0`, `0x78bd80`), no
|
||||
// Minimap probe (`0x4eb0c0`), and the hover holds until the drop.
|
||||
if super::object::pump_move_or_size(&mut model, (x, y)) {
|
||||
return model
|
||||
.mouseover
|
||||
.and_then(|h| model.frame_to_id.get(&h).copied());
|
||||
}
|
||||
}
|
||||
// A drag-active EditBox extends its selection on every move (`0x77a860`).
|
||||
editbox::drag_update(&self.lua, x, y);
|
||||
let (new_handle, minimap_missed) = self.hover_walk(x, y);
|
||||
@@ -200,14 +215,11 @@ impl UiScript {
|
||||
Option<(u32, f64, f64, f64)>,
|
||||
) = {
|
||||
let mut model = self.model_mut();
|
||||
model.cursor_pos = (x, y);
|
||||
// Drags advance on every move, before the hover test: a drag within one frame crosses
|
||||
// no hover boundary.
|
||||
let slider_change = super::slider::drag_move(&mut model, x, y);
|
||||
// The colour picker fires on every move, not only on a change: it has no change gate.
|
||||
let color_change = super::colorselect::drag_move(&mut model, x, y);
|
||||
super::object::advance_move(&mut model, (x, y));
|
||||
super::object::advance_size(&mut model, (x, y));
|
||||
let drag_start = cursor::maybe_start_drag(&mut model, (x, y));
|
||||
let new_handle = new_id.and(new_handle);
|
||||
if new_handle == model.mouseover {
|
||||
@@ -262,9 +274,8 @@ impl UiScript {
|
||||
super::mouseover::leave(&self.lua, h, script);
|
||||
}
|
||||
if let Some(nid) = enter_id {
|
||||
if let Err(e) =
|
||||
event::fire_widget_handler(&self.lua, nid, "OnEnter", vec![Value::Boolean(true)])
|
||||
{
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,291 @@
|
||||
//! The cursor-move hover walk (`0x7660d0`): what its `OnEnter` and `OnLeave` see, and its early
|
||||
//! return while a frame is moving or sizing.
|
||||
|
||||
use super::common::script;
|
||||
use crate::script::UiScript;
|
||||
|
||||
/// The joined `LOG` entries, emptied.
|
||||
fn take_log(s: &UiScript) -> String {
|
||||
s.eval::<String>("local out = table.concat(LOG, ' ') LOG = {} return out")
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
/// `OnEnter` and `OnLeave` run through the plain runner (`0x702690` → `0x704d50`), whose call
|
||||
/// (`0x704e75` → `0x6f41a0`) passes no arguments and sets no global but `this`: the arg globals
|
||||
/// keep what the last event left, on the walk's enter and leave and on a hide's leave.
|
||||
#[test]
|
||||
fn on_enter_and_on_leave_leave_the_arg_globals_untouched() {
|
||||
let mut s = script();
|
||||
s.set_screen_size(800.0, 600.0);
|
||||
s.run(
|
||||
r#"
|
||||
LOG = {}
|
||||
local function rec(tag)
|
||||
return function() table.insert(LOG, tag .. "=" .. tostring(arg1) .. "," .. tostring(arg2)) end
|
||||
end
|
||||
A = CreateFrame("Frame", "ArgA")
|
||||
A:SetPoint("BOTTOMLEFT", 100, 100); A:SetWidth(100); A:SetHeight(100)
|
||||
A:EnableMouse(true)
|
||||
A:SetScript("OnEnter", rec("enterA"))
|
||||
A:SetScript("OnLeave", rec("leaveA"))
|
||||
B = CreateFrame("Frame", "ArgB")
|
||||
B:SetPoint("BOTTOMLEFT", 300, 100); B:SetWidth(100); B:SetHeight(100)
|
||||
B:EnableMouse(true)
|
||||
B:SetScript("OnEnter", rec("enterB"))
|
||||
B:SetScript("OnLeave", rec("leaveB"))
|
||||
arg1, arg2 = "sentinel", "second"
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
s.resolve();
|
||||
|
||||
s.mouse_move(150.0, 150.0);
|
||||
assert_eq!(take_log(&s), "enterA=sentinel,second");
|
||||
s.mouse_move(350.0, 150.0);
|
||||
assert_eq!(
|
||||
take_log(&s),
|
||||
"leaveA=sentinel,second enterB=sentinel,second",
|
||||
"a boundary crossing sets no arg global on either side"
|
||||
);
|
||||
// The removal tail's leave (`0x764cce`) runs through the same base leave and runner.
|
||||
s.run("B:Hide()").unwrap();
|
||||
assert_eq!(take_log(&s), "leaveB=sentinel,second");
|
||||
assert_eq!(
|
||||
s.eval::<String>("return arg1 .. ',' .. arg2").unwrap(),
|
||||
"sentinel,second"
|
||||
);
|
||||
assert!(s.errors().is_empty(), "{:?}", s.errors());
|
||||
}
|
||||
|
||||
/// Three mouse-enabled frames on a 1024×768 screen: `Here` under the cursor's start, `There` to
|
||||
/// its right, and `Far`, movable and resizable, well away from both. Each logs its enter and
|
||||
/// leave.
|
||||
fn three_frames() -> UiScript {
|
||||
let mut s = script();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
s.run(
|
||||
r#"
|
||||
LOG = {}
|
||||
local function frame(name, x, y)
|
||||
local f = CreateFrame("Frame", name)
|
||||
f:SetPoint("BOTTOMLEFT", x, y); f:SetWidth(100); f:SetHeight(100)
|
||||
f:EnableMouse(true)
|
||||
f:SetScript("OnEnter", function() table.insert(LOG, "enter" .. name) end)
|
||||
f:SetScript("OnLeave", function() table.insert(LOG, "leave" .. name) end)
|
||||
return f
|
||||
end
|
||||
Here = frame("Here", 100, 100)
|
||||
There = frame("There", 300, 100)
|
||||
Far = frame("Far", 700, 500)
|
||||
Far:SetMovable(true)
|
||||
Far:SetResizable(true)
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
s.resolve();
|
||||
s.mouse_move(150.0, 150.0);
|
||||
assert_eq!(take_log(&s), "enterHere");
|
||||
s
|
||||
}
|
||||
|
||||
fn focus(s: &UiScript) -> String {
|
||||
s.eval::<String>("local f = GetMouseFocus() return f and f:GetName() or 'nil'")
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
/// While `[root+0xcfc]` holds a move or size target (`0x7652fc`), the walk runs the move/size
|
||||
/// tick (`0x766193` → `0x7655b0`) and returns (`0x7661a0`) before any probe: the hover stays and
|
||||
/// no enter or leave fires until `StopMovingOrSizing` clears the slot (`0x765640`).
|
||||
fn assert_walk_waits_for_the_drop(start: &str) {
|
||||
let mut s = three_frames();
|
||||
s.run(start).unwrap();
|
||||
s.mouse_move(350.0, 150.0);
|
||||
s.resolve();
|
||||
assert_eq!(take_log(&s), "", "{start}: no enter or leave mid-drag");
|
||||
assert_eq!(focus(&s), "Here", "{start}: the hover stays put mid-drag");
|
||||
s.mouse_move(352.0, 150.0);
|
||||
assert_eq!(take_log(&s), "", "{start}: still nothing on the next move");
|
||||
|
||||
s.run("Far:StopMovingOrSizing()").unwrap();
|
||||
assert_eq!(take_log(&s), "", "{start}: the stop alone walks nothing");
|
||||
s.mouse_move(355.0, 150.0);
|
||||
assert_eq!(
|
||||
take_log(&s),
|
||||
"leaveHere enterThere",
|
||||
"{start}: the next move after the drop walks again"
|
||||
);
|
||||
assert_eq!(focus(&s), "There");
|
||||
assert!(s.errors().is_empty(), "{:?}", s.errors());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_frame_moving_skips_the_hover_walk_until_the_drop() {
|
||||
assert_walk_waits_for_the_drop("Far:StartMoving()");
|
||||
// The move itself still follows the cursor.
|
||||
let mut s = three_frames();
|
||||
s.run("Far:StartMoving()").unwrap();
|
||||
s.mouse_move(350.0, 150.0);
|
||||
s.resolve();
|
||||
assert_eq!(s.eval::<f64>("return Far:GetLeft()").unwrap(), 900.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_frame_sizing_skips_the_hover_walk_until_the_drop() {
|
||||
assert_walk_waits_for_the_drop("Far:StartSizing(\"RIGHT\")");
|
||||
// The size itself still follows the cursor.
|
||||
let mut s = three_frames();
|
||||
s.run("Far:StartSizing(\"RIGHT\")").unwrap();
|
||||
s.mouse_move(350.0, 150.0);
|
||||
s.resolve();
|
||||
assert_eq!(s.eval::<f64>("return Far:GetWidth()").unwrap(), 300.0);
|
||||
}
|
||||
|
||||
/// The pump tail's re-pick (`0x7657be`) is the same walk on the saved event, so it waits for the
|
||||
/// drop too: a frame shown under the cursor mid-drag gets no `OnEnter`.
|
||||
#[test]
|
||||
fn the_tick_re_pick_waits_for_the_drop_too() {
|
||||
let mut s = three_frames();
|
||||
s.run("Here:Hide() Far:StartMoving()").unwrap();
|
||||
assert_eq!(take_log(&s), "leaveHere");
|
||||
s.run("Here:Show()").unwrap();
|
||||
s.tick(0.016);
|
||||
assert_eq!(
|
||||
take_log(&s),
|
||||
"",
|
||||
"the re-pick finds the slot set and returns"
|
||||
);
|
||||
assert_eq!(focus(&s), "nil");
|
||||
s.run("Far:StopMovingOrSizing()").unwrap();
|
||||
s.mouse_move(151.0, 150.0);
|
||||
assert_eq!(take_log(&s), "enterHere");
|
||||
}
|
||||
|
||||
/// The drag-start check lives in the base probe (`0x76b320`), which the early return skips, so a
|
||||
/// frame that starts moving on its press never reaches `OnDragStart`, however far it is dragged.
|
||||
#[test]
|
||||
fn a_move_started_on_the_press_never_reaches_the_drag_start() {
|
||||
let mut s = three_frames();
|
||||
s.run(
|
||||
r#"
|
||||
starts = 0
|
||||
Here:SetMovable(true)
|
||||
Here:RegisterForDrag("LeftButton")
|
||||
Here:SetScript("OnMouseDown", function() this:StartMoving() end)
|
||||
Here:SetScript("OnMouseUp", function() this:StopMovingOrSizing() end)
|
||||
Here:SetScript("OnDragStart", function() starts = starts + 1 end)
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
s.mouse_button(150.0, 150.0, "LeftButton", true);
|
||||
s.mouse_move(250.0, 150.0);
|
||||
s.mouse_move(350.0, 150.0);
|
||||
s.mouse_button(350.0, 150.0, "LeftButton", false);
|
||||
assert_eq!(s.eval::<i64>("return starts").unwrap(), 0);
|
||||
assert!(s.errors().is_empty(), "{:?}", s.errors());
|
||||
}
|
||||
|
||||
/// Hiding the moving frame cancels the move: the hide's de-registration (`0x76ad9d` →
|
||||
/// `0x764920`) clears the slot when it holds the frame (`0x76492e`–`0x76493a` → `0x765640`), so
|
||||
/// the next move walks again.
|
||||
#[test]
|
||||
fn hiding_the_moving_frame_ends_the_move_and_the_walk_resumes() {
|
||||
let mut s = three_frames();
|
||||
s.run("Far:StartMoving()").unwrap();
|
||||
s.mouse_move(250.0, 150.0);
|
||||
assert_eq!(take_log(&s), "");
|
||||
s.run("Far:Hide()").unwrap();
|
||||
s.mouse_move(350.0, 150.0);
|
||||
assert_eq!(take_log(&s), "leaveHere enterThere");
|
||||
s.run("Far:Show()").unwrap();
|
||||
s.resolve();
|
||||
assert_eq!(
|
||||
s.eval::<f64>("return Far:GetLeft()").unwrap(),
|
||||
800.0,
|
||||
"the move ended with the hide: the later cursor delta did not reach it"
|
||||
);
|
||||
}
|
||||
|
||||
/// The pointer leaving the window ends the hover with the hovered frame's `OnLeave`, and the next
|
||||
/// real move walks again.
|
||||
#[test]
|
||||
fn the_pointer_leaving_the_window_leaves_the_hovered_frame() {
|
||||
let mut s = three_frames();
|
||||
s.pointer_left_hover();
|
||||
s.pointer_left_window();
|
||||
assert_eq!(take_log(&s), "leaveHere");
|
||||
assert_eq!(focus(&s), "nil");
|
||||
s.mouse_move(150.0, 150.0);
|
||||
assert_eq!(take_log(&s), "enterHere");
|
||||
assert!(s.errors().is_empty(), "{:?}", s.errors());
|
||||
}
|
||||
|
||||
/// A move in flight follows real cursor events only (`0x7655b0` adds each event's delta from the
|
||||
/// last sample, `[root+0xd08]`/`[root+0xd0c]`): the pointer leaving moves nothing and stamps no
|
||||
/// layout, and the first real move back applies its delta from the last real sample.
|
||||
#[test]
|
||||
fn a_frame_moving_when_the_pointer_leaves_keeps_its_offsets() {
|
||||
let mut s = three_frames();
|
||||
s.run("Far:StartMoving()").unwrap();
|
||||
s.mouse_move(250.0, 150.0);
|
||||
s.resolve();
|
||||
let point = |s: &UiScript| {
|
||||
s.eval::<(f64, f64)>("local _, _, _, x, y = Far:GetPoint() return x, y")
|
||||
.unwrap()
|
||||
};
|
||||
assert_eq!(point(&s), (800.0, 500.0));
|
||||
let saved = s.user_placed_layouts();
|
||||
|
||||
s.pointer_left_hover();
|
||||
s.pointer_left_window();
|
||||
s.resolve();
|
||||
assert_eq!(point(&s), (800.0, 500.0), "the leave moves nothing");
|
||||
assert_eq!(s.eval::<f64>("return Far:GetLeft()").unwrap(), 800.0);
|
||||
assert_eq!(
|
||||
s.user_placed_layouts(),
|
||||
saved,
|
||||
"the layout cache keeps the last real position"
|
||||
);
|
||||
assert_eq!(take_log(&s), "leaveHere", "the hover ends mid-move too");
|
||||
|
||||
// The move is `StartMoving`'s, which only `StopMovingOrSizing` ends: back in the window, the
|
||||
// next real event moves it by its delta from the last real one.
|
||||
s.mouse_move(260.0, 150.0);
|
||||
s.resolve();
|
||||
assert_eq!(point(&s), (810.0, 500.0));
|
||||
assert!(s.errors().is_empty(), "{:?}", s.errors());
|
||||
}
|
||||
|
||||
/// The canonical drag idiom: the pointer leaving mid-drag ends the gesture with `OnDragStop`,
|
||||
/// whose `StopMovingOrSizing` keeps the frame where the last real move put it.
|
||||
#[test]
|
||||
fn a_drag_cut_short_by_the_pointer_leaving_keeps_the_last_real_position() {
|
||||
let mut s = three_frames();
|
||||
s.run(
|
||||
r#"
|
||||
stops = 0
|
||||
Here:SetMovable(true)
|
||||
Here:RegisterForDrag("LeftButton")
|
||||
Here:SetScript("OnDragStart", function() this:StartMoving() end)
|
||||
Here:SetScript("OnDragStop", function() stops = stops + 1 this:StopMovingOrSizing() end)
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
s.mouse_button(150.0, 150.0, "LeftButton", true);
|
||||
s.mouse_move(170.0, 150.0); // past the threshold: OnDragStart, StartMoving
|
||||
s.mouse_move(200.0, 160.0);
|
||||
s.resolve();
|
||||
assert_eq!(s.eval::<f64>("return Here:GetLeft()").unwrap(), 130.0);
|
||||
s.pointer_left_hover();
|
||||
s.pointer_left_window();
|
||||
s.resolve();
|
||||
assert_eq!(s.eval::<i64>("return stops").unwrap(), 1);
|
||||
assert_eq!(s.eval::<f64>("return Here:GetLeft()").unwrap(), 130.0);
|
||||
assert_eq!(s.eval::<f64>("return Here:GetBottom()").unwrap(), 110.0);
|
||||
let saved = s.user_placed_layouts();
|
||||
let here = saved
|
||||
.iter()
|
||||
.find(|l| l.name == "Here")
|
||||
.expect("Here is saved");
|
||||
assert_eq!((here.points[0].x, here.points[0].y), (130.0, 110.0));
|
||||
assert!(s.errors().is_empty(), "{:?}", s.errors());
|
||||
}
|
||||
@@ -27,6 +27,7 @@ mod gm_ticket;
|
||||
mod handler_prof;
|
||||
mod host_state;
|
||||
mod hover_publish;
|
||||
mod hover_walk;
|
||||
mod input;
|
||||
mod keep_rect;
|
||||
mod keyboard;
|
||||
|
||||
@@ -93,6 +93,9 @@ pub(super) fn base_hide(lua: &Lua, h: FrameHandle) {
|
||||
if !model.arena.hide_step(h) {
|
||||
return;
|
||||
}
|
||||
// The de-registration ends a move or size on this frame before its index removal
|
||||
// (`0x76493a`, then `0x764942`).
|
||||
super::object::cancel_on_hide(&mut model, h);
|
||||
leave_mouse_index(&mut model, h)
|
||||
};
|
||||
if let Some(script) = left {
|
||||
|
||||
Reference in New Issue
Block a user