diff --git a/crates/benilla-app/src/ui_script/input.rs b/crates/benilla-app/src/ui_script/input.rs index 4b39665d..21abea05 100644 --- a/crates/benilla-app/src/ui_script/input.rs +++ b/crates/benilla-app/src/ui_script/input.rs @@ -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. diff --git a/crates/benilla-ui/src/script/mod.rs b/crates/benilla-ui/src/script/mod.rs index 70158b8b..8acf84e8 100644 --- a/crates/benilla-ui/src/script/mod.rs +++ b/crates/benilla-ui/src/script/mod.rs @@ -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. diff --git a/crates/benilla-ui/src/script/tests/input.rs b/crates/benilla-ui/src/script/tests/input.rs index 31573c93..2f5aed17 100644 --- a/crates/benilla-ui/src/script/tests/input.rs +++ b/crates/benilla-ui/src/script/tests/input.rs @@ -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()); +}