ui: GetCursorPosition answered 0,0, so nothing could drag or size a frame

`Model::cursor_pos` is read by the verb in two places and was written by
nothing in the workspace: declared, initialised to the origin, never assigned.
So `GetCursorPosition()` answered `0, 0` for every call of every session.

That kills the universal drag-and-size idiom — stamp the cursor on
`OnMouseDown`, subtract it each `OnUpdate`, size by the difference. Every
delta came out zero, the frame was re-anchored to its own current size sixty
times a second while the player dragged, and the addon's own "that was a click,
not a drag" guard then declined to save. Nothing raises and nothing logs.

grom reported it twice as OpenMapClassic's map window refusing to resize; the
round before this one painted its invisible grips and left this underneath.

Fed in `ui_script/input.rs` beside `set_mouse_buttons`, for the reason that
one states: the pointer feed below is skipped for a cursor off the window and
for a UI hidden by TOGGLEUI, and the reference answers through both. UI units,
y-up from the bottom-left, undivided — UIParent is scale 1 here, so an addon's
`x / frame:GetEffectiveScale()` lands in the frame's own units as on the
reference. The test pins the whole idiom rather than the getter, because the
getter answering *something* was never the question.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
This commit is contained in:
2026-11-02 21:15:38 -07:00
co-authored by Claude Opus 5
parent 5308b5a200
commit 36f504820d
3 changed files with 87 additions and 0 deletions
+10
View File
@@ -153,6 +153,16 @@ pub(super) fn feed_ui_input(
buttons.pressed(MouseButton::Other(4)),
buttons.pressed(MouseButton::Other(5)),
]);
// `GetCursorPosition()`: the device's own position, in UI units as `GetLeft()` gives them.
// Fed here rather than from the pointer feed below for the same reason the buttons are — that
// one is skipped for a cursor off the window or a hidden UI, and the reference answers through
// both. Nothing wrote it before, so the verb answered `0, 0` all session, and every addon that
// sizes or drags a frame by stamping the cursor and subtracting it measured a delta of zero:
// OpenMapClassic's map window could not be resized by its grips at all.
if let Some(cursor) = window.cursor_position() {
let s = super::seam_scale(window.height(), ui_scale.0);
script.set_cursor_position(cursor.x / s, (window.height() - cursor.y) / s);
}
// ── Mouse ── A cursor off the window, or a UI hidden by TOGGLEUI, skips only the mouse feed.
// The headless hover probe's aim stands in for a missing cursor, so `PointerOverUi` rises and
// falls over a panel in an automated run as it does for a person; a real pointer always wins.
+19
View File
@@ -920,6 +920,25 @@ impl UiScript {
model.mouse_buttons = down;
}
/// Push the cursor's position behind `GetCursorPosition()`, in UI units y-up from the
/// bottom-left — the space `GetLeft()`/`GetBottom()` answer in, since `UIParent` is scale 1
/// here, so the universal `x / frame:GetEffectiveScale()` idiom lands in the frame's own
/// units as it does on the reference.
///
/// Call it beside [`Self::set_mouse_buttons`], from the host's own cursor rather than the
/// pointer feed: that one is skipped for a cursor off the window and for a UI hidden by
/// TOGGLEUI, while `GetCursorPosition` answers through both.
///
/// Nothing wrote this before, so the verb answered `0, 0` for the whole session. It reads as
/// a frame that cannot be dragged or sized: the idiom is to stamp the cursor on mouse-down
/// and subtract it each `OnUpdate`, and a constant gives every frame a delta of zero.
/// OpenMapClassic's map window is exactly that shape (`Map/Window.lua`'s `Grip`) and could
/// not be resized at all.
pub fn set_cursor_position(&mut self, x: f32, y: f32) {
let mut model = self.model_mut();
model.cursor_pos = (x, y);
}
/// Push the player's WMO-containment state (the client's `0xceaa60`) onto every Minimap: it
/// picks which persisted zoom index `GetZoom`/`SetZoom` act on. Call it before the script
/// tick, on each inside/outside edge and whenever [`Self::minimap_widgets_created`] moves.
@@ -923,3 +923,61 @@ fn pfuis_nameplate_right_click_emulator_arms_and_then_synthesises_the_click() {
);
assert!(s.errors().is_empty(), "{:?}", s.errors());
}
/// `GetCursorPosition()` answers the host's own cursor, in the units `GetLeft()` gives — the
/// whole drag-and-size idiom, end to end.
///
/// The idiom is universal: stamp the cursor on `OnMouseDown`, subtract it each `OnUpdate`, size
/// the frame by the difference. `Model::cursor_pos` had no writer at all, so the verb answered
/// `0, 0` every call of every session, every delta came out zero, and a frame built this way
/// could not be dragged or sized by one pixel. grom reported it as OpenMapClassic's map window
/// refusing to resize (`Map/Window.lua`'s `Grip` is exactly this shape); nothing in the Lua was
/// wrong, and no error was raised anywhere, because a constant cursor is a perfectly well-formed
/// answer.
#[test]
fn get_cursor_position_drives_the_drag_to_size_idiom() {
let mut s = script();
s.set_screen_size(800.0, 600.0);
s.run(
r#"
local f = CreateFrame("Frame", "Sizer")
f:SetPoint("BOTTOMLEFT", 0, 0) f:SetWidth(200) f:SetHeight(100)
start = nil
function Grab()
local x, y = GetCursorPosition()
start = { x = x, y = y, w = f:GetWidth(), h = f:GetHeight() }
end
function Drag()
local x, y = GetCursorPosition()
local s = f:GetEffectiveScale()
f:SetWidth(start.w + (x - start.x) / s)
f:SetHeight(start.h + (y - start.y) / s)
end
function Size() return f:GetWidth(), f:GetHeight() end
"#,
)
.unwrap();
assert_eq!(
s.eval::<(f64, f64)>("return GetCursorPosition()").unwrap(),
(0.0, 0.0),
"nothing fed yet, so the verb answers the origin"
);
s.set_cursor_position(300.0, 250.0);
assert_eq!(
s.eval::<(f64, f64)>("return GetCursorPosition()").unwrap(),
(300.0, 250.0),
"UI units, y-up, undivided: UIParent is scale 1 here"
);
s.run("Grab()").unwrap();
s.set_cursor_position(360.0, 275.0);
s.run("Drag()").unwrap();
assert_eq!(
s.eval::<(f64, f64)>("return Size()").unwrap(),
(260.0, 125.0),
"the frame grew by exactly the cursor's own travel"
);
assert!(s.errors().is_empty(), "{:?}", s.errors());
}