A drag pins one TOPLEFT anchor at the frame rect, as in 1.12.1 (#953)
A drag kept the frame's authored anchors: `GetPoint()` answered them afterwards, the frame kept following its old target, and a resize grip on a CENTER- or TOPLEFT-anchored frame moved the wrong edges. In 1.12.1 every drag enters at `0x7652b0`, whose first act pins the frame (`0x768430`): `ClearAllPoints`, one TOPLEFT on the screen root's TOPLEFT at the current rect, and an explicit size. The pump `0x768710` moves that anchor (case 4) or, for each of the eight grips, computes the clamped size and re-pins shifted by the left and top travel only (`0x76898a`). A frame with no rect starts no drag (`0x7652cd`). Drags now pin and pump that way, with one drag slot; `StartSizing` defaults to BOTTOMRIGHT (`0x776913`). Tests: the pin and `GetPoint` after a drag, the eight grips on a CENTER-anchored frame, the clamps, the layout cache round trip, and stock chat-window resize and off-screen drop. Fixes #173 🤖 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
aff59fd55b
commit
5af22799d0
@@ -6,8 +6,10 @@
|
||||
//! `FrameLevel:`, `X:`, `Y:`, `W:` and `H:` lines; ours is
|
||||
//! `benilla-config/layout/<realm>-<character>.txt`, keeping `Frame:`, `W:` and `H:`.
|
||||
//! Deviation: one `Point: <point> <relativeTo> <relativePoint> <x> <y>` line per anchor, `-` for
|
||||
//! the screen root, instead of `X:`/`Y:`, because a benilla drag moves the authored anchors
|
||||
//! rather than re-anchoring to one TOPLEFT, and a pair would lose them. There is no
|
||||
//! the screen root, instead of `X:`/`Y:`. A drag leaves one TOPLEFT against the screen root
|
||||
//! (`0x768430`), whose line holds the reference's X and Y unrounded, where the reference saves
|
||||
//! integers (`0x490fc4`–`0x490fdf`); a frame user-placed without a drag keeps its authored
|
||||
//! anchors here, where the reference saves its rect's top-left. There is no
|
||||
//! `FrameLevel:`: the reference saves an undocked window's raise, and benilla's raise does not
|
||||
//! persist.
|
||||
//!
|
||||
@@ -35,8 +37,8 @@ const SCREEN_TOKEN: &str = "-";
|
||||
const HEADER: &str = "\
|
||||
# benilla window layout — every frame the player has moved or resized (the client's userPlaced
|
||||
# bit). A relative of the reference's layout-cache.txt: same scope, same Frame:/W:/H: keys, but
|
||||
# anchors instead of its X:/Y: pair, because a benilla drag moves a frame's anchors rather than
|
||||
# collapsing it to a screen position. `-` as a Point: target means the screen root.
|
||||
# anchors instead of its X:/Y: pair; a dragged window has one, TOPLEFT on the screen's TOPLEFT.
|
||||
# `-` as a Point: target means the screen root.
|
||||
";
|
||||
|
||||
/// The character's file: its path, the VM it was restored into, and whether a write is owed.
|
||||
|
||||
@@ -591,3 +591,86 @@ fn an_undocked_tab_drags_again_and_a_plain_click_drops_the_window() {
|
||||
);
|
||||
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
||||
}
|
||||
|
||||
/// `(left, right, bottom, top)` of `frame`, after a resolve.
|
||||
fn edges(s: &mut UiScript, frame: &str) -> (f32, f32, f32, f32) {
|
||||
s.resolve();
|
||||
let (l, r, b, t): (f64, f64, f64, f64) = s
|
||||
.eval(&format!(
|
||||
"return {frame}:GetLeft(), {frame}:GetRight(), {frame}:GetBottom(), {frame}:GetTop()"
|
||||
))
|
||||
.unwrap();
|
||||
(l as f32, r as f32, b as f32, t as f32)
|
||||
}
|
||||
|
||||
/// Unlocking a docked window re-anchors it at CENTER (`FloatingChatFrame.lua:785-792`); a grip
|
||||
/// then moves only the grabbed edge, since the resize starts by pinning the window to one TOPLEFT
|
||||
/// anchor at its rect (`0x7652b0` → `0x768430`).
|
||||
#[test]
|
||||
fn an_unlocked_centred_window_resizes_from_the_grabbed_edge_only() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = chat_ui();
|
||||
reveal(&mut s);
|
||||
right_click(&mut s, "ChatFrame2Tab");
|
||||
assert!(s
|
||||
.eval::<bool>("return DropDownList1Button1:GetText() == UNLOCK_WINDOW")
|
||||
.unwrap());
|
||||
left_click(&mut s, "DropDownList1Button1");
|
||||
assert_eq!(
|
||||
s.eval::<(String, i64)>("return (ChatFrame2:GetPoint(1)), ChatFrame2:GetNumPoints()")
|
||||
.unwrap(),
|
||||
("CENTER".to_string(), 1),
|
||||
"FCF_ToggleLock undocked it and centred it"
|
||||
);
|
||||
|
||||
for (grip, dx, dy) in [("Right", 40.0, 0.0), ("Top", 0.0, 30.0)] {
|
||||
reveal_window(&mut s, "ChatFrame2");
|
||||
let before = edges(&mut s, "ChatFrame2");
|
||||
let name = format!("ChatFrame2Resize{grip}");
|
||||
let (x, y) = centre(&mut s, &name);
|
||||
assert_eq!(s.hit_test_name(x, y).as_deref(), Some(name.as_str()));
|
||||
s.mouse_button(x, y, "LeftButton", true);
|
||||
s.mouse_move(x + dx, y + dy);
|
||||
s.mouse_button(x + dx, y + dy, "LeftButton", false);
|
||||
let after = edges(&mut s, "ChatFrame2");
|
||||
assert_eq!(
|
||||
after,
|
||||
(before.0, before.1 + dx, before.2, before.3 + dy),
|
||||
"the {grip} grip moved its own edge and no other"
|
||||
);
|
||||
}
|
||||
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
||||
}
|
||||
|
||||
/// A tab dropped off the bottom of the screen: `FCF_ValidateChatFramePosition`'s
|
||||
/// `SetPoint("TOPLEFT", …)` (`FloatingChatFrame.lua:1304-1334`) replaces the drag's one TOPLEFT
|
||||
/// anchor, so the window comes back whole at its own size.
|
||||
#[test]
|
||||
fn a_tab_dropped_off_screen_puts_the_window_back_whole() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = chat_ui();
|
||||
s.run("FCF_SetLocked(ChatFrame1, nil)").unwrap();
|
||||
let (w, h) = (width(&mut s), height(&mut s));
|
||||
drag_tab(&mut s, 0.0, -600.0);
|
||||
let (n, point, rel, rp) = s
|
||||
.eval::<(i64, String, String, String)>(
|
||||
"local p, rel, rp = ChatFrame1:GetPoint(1) \
|
||||
return ChatFrame1:GetNumPoints(), p, rel:GetName(), rp",
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
(n, point.as_str(), rel.as_str(), rp.as_str()),
|
||||
(1, "TOPLEFT", "UIParent", "TOPLEFT"),
|
||||
"the validate's anchor is the only one"
|
||||
);
|
||||
assert_eq!((width(&mut s), height(&mut s)), (w, h), "at its own size");
|
||||
let want_top: f64 = s
|
||||
.eval("return MainMenuBar:GetHeight() + ChatFrame1Tab:GetHeight()")
|
||||
.unwrap();
|
||||
let top: f64 = s.eval("return ChatFrame1:GetTop()").unwrap();
|
||||
assert!(
|
||||
(top - want_top).abs() < 1e-3,
|
||||
"back on screen: {top} vs {want_top}"
|
||||
);
|
||||
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
||||
}
|
||||
|
||||
@@ -249,13 +249,12 @@ impl Anchors {
|
||||
self.0.iter().flatten()
|
||||
}
|
||||
|
||||
/// Each held anchor's offsets in point order; the point is not writable here, so an anchor
|
||||
/// The offsets of the anchor held for `point`; the point is not writable here, so an anchor
|
||||
/// never leaves its slot.
|
||||
#[inline]
|
||||
pub fn offsets_mut(&mut self) -> impl Iterator<Item = (&mut f32, &mut f32)> {
|
||||
self.0
|
||||
.iter_mut()
|
||||
.flatten()
|
||||
pub fn offset_mut(&mut self, point: Point) -> Option<(&mut f32, &mut f32)> {
|
||||
self.0[point.id() as usize]
|
||||
.as_mut()
|
||||
.map(|a| (&mut a.x_off, &mut a.y_off))
|
||||
}
|
||||
|
||||
|
||||
@@ -639,11 +639,10 @@ pub(crate) struct Model {
|
||||
pub(crate) drag_registered: HashMap<FrameHandle, HashSet<String>>,
|
||||
/// The drag gesture, armed by a press on a drag-registered frame, started past the threshold.
|
||||
pub(crate) drag: Option<cursor::DragGesture>,
|
||||
/// The one `StartMoving()` in flight, the client's `root+0xcfc` slot, apart from `drag` as in
|
||||
/// the reference: a move outlives the button, since mouse-up's auto-stop skips a Lua drag.
|
||||
/// The one move or size in flight, the client's `root+0xcfc` slot with its grip (`+0xd04`),
|
||||
/// apart from `drag` as in the reference: a `StartMoving` or `StartSizing` outlives the button,
|
||||
/// since mouse-up's auto-stop skips a Lua drag.
|
||||
pub(crate) moving: Option<super::object::FrameMove>,
|
||||
/// The `StartSizing` in flight, cleared by `StopMovingOrSizing` (`0x776990`).
|
||||
pub(crate) sizing: Option<super::object::FrameSizing>,
|
||||
/// A Slider thumb drag, left press to release or pointer leave, handled here with no Lua.
|
||||
pub(crate) slider_drag: Option<slider::SliderDrag>,
|
||||
/// A ColorSelect wheel or value-bar drag, press to release.
|
||||
@@ -1338,7 +1337,6 @@ impl Model {
|
||||
drag_registered: HashMap::new(),
|
||||
drag: None,
|
||||
moving: None,
|
||||
sizing: None,
|
||||
slider_drag: None,
|
||||
color_drag: None,
|
||||
gossip: None,
|
||||
|
||||
@@ -25,7 +25,7 @@ pub(crate) use layout_methods::eff_scale;
|
||||
pub(in crate::script) use layout_methods::settle;
|
||||
pub(crate) mod movable;
|
||||
pub(crate) mod toplevel;
|
||||
pub(crate) use movable::{cancel_on_hide, pump_move_or_size, FrameMove, FrameSizing};
|
||||
pub(crate) use movable::{cancel_on_hide, pump_move_or_size, FrameMove};
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────────────────────
|
||||
// id ↔ lightuserdata
|
||||
|
||||
@@ -1,47 +1,39 @@
|
||||
//! Frame methods: movable and resizable windows, and the drag pumps behind `StartMoving` and
|
||||
//! Frame methods: movable and resizable windows, and the drag pump behind `StartMoving` and
|
||||
//! `StartSizing`. The gesture that usually calls them, `OnDragStart`/`OnDragStop` (`0x76bf70`/
|
||||
//! `0x76c040`), is [`crate::script::cursor`]'s.
|
||||
//!
|
||||
//! The flags are bits of `[frame+0xb4]`, set by `0x76a3c0`: movable `0x100`, resizable `0x200`,
|
||||
//! userPlaced `0x1000`. A drag (`0x7652b0`) re-anchors the frame to one TOPLEFT against the screen
|
||||
//! root at its current rect (`0x768430`), raises it, sets userPlaced and takes the one drag slot
|
||||
//! (`root+0xcfc`). The pump (`0x7655b0`) adds each cursor delta, over the frame's scale, straight
|
||||
//! into that anchor's offsets (`0x768710` case 4), so stopping (`0x765640`) writes nothing.
|
||||
//!
|
||||
//! Here nothing re-anchors: a move translates every authored anchor, and a resize keeps the set.
|
||||
//! userPlaced `0x1000`. Every drag enters at `0x7652b0`, which pins the frame to one TOPLEFT
|
||||
//! anchor against the screen root at its current rect, with an explicit size (`0x768430`), then
|
||||
//! raises it, sets userPlaced and takes the one drag slot (`root+0xcfc`) with its grip (`+0xd04`).
|
||||
//! The pump (`0x7655b0` → `0x768710`) works on that anchor and size: a move, grip CENTER, adds
|
||||
//! each cursor delta over the frame's scale to the anchor's offsets (case 4), and each of the
|
||||
//! eight grips sizes the frame and re-pins it shifted by its left and top travel only. Stopping
|
||||
//! (`0x765640`) writes nothing, so after any drag `GetPoint` answers that one TOPLEFT.
|
||||
|
||||
use mlua::{Lua, MultiValue, Table, Value};
|
||||
|
||||
use crate::script::Model;
|
||||
use crate::layout::{Anchor, Anchors, Point};
|
||||
use crate::script::{Model, UiScript, SCREEN};
|
||||
use crate::widget::FrameHandle;
|
||||
|
||||
use super::frame_handle_of;
|
||||
use super::layout_methods::eff_scale;
|
||||
use super::{frame_handle_of, point_from_str};
|
||||
|
||||
/// The one in-flight move, the reference's drag slot, held in [`Model::moving`].
|
||||
/// The one move or size in flight, the reference's drag slot, held in [`Model::moving`].
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub(crate) struct FrameMove {
|
||||
/// The frame being moved (`root+0xcfc`).
|
||||
/// The frame being dragged (`root+0xcfc`).
|
||||
frame: FrameHandle,
|
||||
/// The grip (`root+0xd04`): CENTER moves the frame, any other point resizes it from there.
|
||||
grip: Point,
|
||||
/// The cursor position at the last pump (`root+0xd08`/`+0xd0c`), UI px, y-up.
|
||||
sample: (f32, f32),
|
||||
/// Whether mouse-up ends the move, the drag mode as Lua sees it: `0x766420` ends modes 1
|
||||
/// (modifier drag) and 2 (title region), never 3 (`StartMoving`).
|
||||
/// Whether mouse-up ends the drag, the drag mode as Lua sees it: `0x766420` ends modes 1
|
||||
/// (modifier drag) and 2 (title region), never 3 (`StartMoving`, `StartSizing`).
|
||||
pub(crate) auto_stop: bool,
|
||||
}
|
||||
|
||||
/// An in-flight `StartSizing` drag: which frame, which grip, and the cursor at the last pump.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub(crate) struct FrameSizing {
|
||||
frame: FrameHandle,
|
||||
/// The edges the named grip moves: `LEFT` the left edge, `BOTTOMRIGHT` the bottom and right.
|
||||
left: bool,
|
||||
right: bool,
|
||||
top: bool,
|
||||
bottom: bool,
|
||||
sample: (f32, f32),
|
||||
}
|
||||
|
||||
/// Populate `m`'s movable and resizable methods.
|
||||
pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
// SetMovable / IsMovable (`0x776420`/`0x7764d0`): a pure flag write, so clearing it mid-drag
|
||||
@@ -172,48 +164,28 @@ pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
start_moving(&mut model, h)
|
||||
})?,
|
||||
)?;
|
||||
// StartSizing(point) (`0x776830`; stock caller `FloatingChatFrame.lua:600`): a resize from the
|
||||
// named grip, ended by `StopMovingOrSizing`. The reference's per-grip switch (`0x768710`)
|
||||
// moves the named edges with the cursor and keeps the opposite ones.
|
||||
// StartSizing([point]) (`0x776830`; stock caller `FloatingChatFrame.lua:600`): a resize from
|
||||
// the named grip, ended by `StopMovingOrSizing`. The grip starts as BOTTOMRIGHT (`0x776913`)
|
||||
// and only a string naming a point, in any case, replaces it (`0x77691a`, `0x6f1840`).
|
||||
m.set(
|
||||
"StartSizing",
|
||||
lua.create_function(|lua, (this, point): (Table, Option<String>)| {
|
||||
lua.create_function(|lua, (this, point): (Table, Value)| {
|
||||
let h = frame_handle_of(lua, &this)?;
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model");
|
||||
if !model.arena.frame(h).is_some_and(|f| f.resizable) {
|
||||
return Err(not_flagged(&model, h, "resizable"));
|
||||
}
|
||||
// Same "do not start a second one" guard `start_moving` carries.
|
||||
if model.sizing.is_some() || model.moving.is_some() {
|
||||
// The same `root+0xcfc` guard `StartMoving` carries (`0x776947`).
|
||||
if model.moving.is_some() {
|
||||
return Ok(());
|
||||
}
|
||||
let p = point.unwrap_or_default().to_ascii_uppercase();
|
||||
let (left, right) = (p.contains("LEFT"), p.contains("RIGHT"));
|
||||
let (top, bottom) = (p.contains("TOP"), p.contains("BOTTOM"));
|
||||
// The raise and userPlaced belong to the drag entry `0x7652b0`, whatever the grip.
|
||||
super::toplevel::raise(&mut model, h);
|
||||
set_user_placed(&mut model, h);
|
||||
let sample = model.cursor_pos;
|
||||
// A CENTER grip is a move: the pump's case 4 shifts the anchor offsets and returns
|
||||
// before the size clamp (`0x768bfb`).
|
||||
if p == "CENTER" {
|
||||
model.moving = Some(FrameMove {
|
||||
frame: h,
|
||||
sample,
|
||||
// `StartSizing` enters as drag mode 3 (`0x7652b0`), which mouse-up never ends.
|
||||
auto_stop: false,
|
||||
});
|
||||
return Ok(());
|
||||
let grip = match &point {
|
||||
Value::String(s) => s.to_str().ok().and_then(|t| point_from_str(&t)),
|
||||
_ => None,
|
||||
}
|
||||
// A name that is no point starts an inert drag: raised and userPlaced, moving nothing.
|
||||
model.sizing = Some(FrameSizing {
|
||||
frame: h,
|
||||
left,
|
||||
right,
|
||||
top,
|
||||
bottom,
|
||||
sample,
|
||||
});
|
||||
.unwrap_or(Point::BottomRight);
|
||||
// Drag mode 3, which mouse-up never ends.
|
||||
begin_drag(&mut model, h, grip, false);
|
||||
Ok(())
|
||||
})?,
|
||||
)?;
|
||||
@@ -223,9 +195,6 @@ pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
lua.create_function(|lua, this: Table| {
|
||||
let h = frame_handle_of(lua, &this)?;
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model");
|
||||
if model.sizing.is_some_and(|sz| sz.frame == h) {
|
||||
model.sizing = None;
|
||||
}
|
||||
if model.moving.is_some_and(|mv| mv.frame == h) {
|
||||
model.moving = None;
|
||||
}
|
||||
@@ -284,53 +253,109 @@ fn not_flagged(model: &Model, h: FrameHandle, want: &str) -> mlua::Error {
|
||||
}
|
||||
|
||||
/// `StartMoving` (`0x776700` → `0x7652b0`): errors on a frame that is not movable (`0x77678b`),
|
||||
/// ignores a second move, then raises the frame, sets userPlaced and takes the slot, in that order.
|
||||
/// ignores a second drag, then starts a move, grip CENTER (`0x776819`).
|
||||
fn start_moving(model: &mut Model, h: FrameHandle) -> mlua::Result<()> {
|
||||
if !model.arena.frame(h).is_some_and(|f| f.movable) {
|
||||
return Err(not_flagged(model, h, "movable"));
|
||||
}
|
||||
// The `root+0xcfc != 0` guard (`0x7767e8`); what the reference does beyond not starting a
|
||||
// second move is untraced.
|
||||
// second drag is untraced.
|
||||
if model.moving.is_some() {
|
||||
return Ok(());
|
||||
}
|
||||
// The drag-start raise (`0x7652d7`); the worker applies the toplevel gate.
|
||||
super::toplevel::raise(model, h);
|
||||
set_user_placed(model, h);
|
||||
model.moving = Some(FrameMove {
|
||||
frame: h,
|
||||
sample: model.cursor_pos,
|
||||
// Mode 3: only `StopMovingOrSizing` ends it.
|
||||
auto_stop: false,
|
||||
});
|
||||
// Mode 3: only `StopMovingOrSizing` ends it.
|
||||
begin_drag(model, h, Point::Center, false);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Begin a title-region move, drag mode 2 (`0x7662c0` → `0x765320` → `0x7652b0`), with no movable
|
||||
/// check, since nothing on that path reads the bit. False when a move is already in flight.
|
||||
/// check, since nothing on that path reads the bit; `0x765320` asks for grip CENTER when it is not
|
||||
/// sizing (`0x765332` → `0x765464`). False when a drag is already in flight or did not start.
|
||||
pub(crate) fn start_title_move(model: &mut Model, h: FrameHandle) -> bool {
|
||||
if model.moving.is_some() {
|
||||
return false;
|
||||
}
|
||||
super::toplevel::raise(model, h);
|
||||
set_user_placed(model, h);
|
||||
model.moving = Some(FrameMove {
|
||||
frame: h,
|
||||
sample: model.cursor_pos,
|
||||
auto_stop: true,
|
||||
});
|
||||
true
|
||||
begin_drag(model, h, Point::Center, true)
|
||||
}
|
||||
|
||||
/// Set userPlaced as the drag entry `0x7652b0` does; the layout cache is dirtied only on the
|
||||
/// transition, since every title-region click runs this.
|
||||
fn set_user_placed(model: &mut Model, h: FrameHandle) {
|
||||
/// The drag entry `0x7652b0`: pin the frame (`0x7652c6` → `0x768430`) and, only when it has a rect
|
||||
/// to pin (`0x7652cd`), raise it (`0x7652d7`), set userPlaced (`0x7652e5`) and take the slot
|
||||
/// (`0x7652f3`–`0x76530f`). False when the frame has no rect, so nothing started.
|
||||
fn begin_drag(model: &mut Model, h: FrameHandle, grip: Point, auto_stop: bool) -> bool {
|
||||
let Some(repinned) = pin_top_left(model, h, (0.0, 0.0), (0.0, 0.0)) else {
|
||||
return false;
|
||||
};
|
||||
// The worker applies the toplevel gate.
|
||||
super::toplevel::raise(model, h);
|
||||
// The layout cache is dirtied on the bit's transition only, since every title-region click
|
||||
// runs this, and again when the pin rewrote what a saved row holds.
|
||||
if let Some(f) = model.arena.frame_mut(h) {
|
||||
if !f.user_placed {
|
||||
f.user_placed = true;
|
||||
model.user_placed_changed = true;
|
||||
}
|
||||
}
|
||||
if repinned {
|
||||
mark_user_placed_change(model, h);
|
||||
}
|
||||
model.moving = Some(FrameMove {
|
||||
frame: h,
|
||||
grip,
|
||||
sample: model.cursor_pos,
|
||||
auto_stop,
|
||||
});
|
||||
true
|
||||
}
|
||||
|
||||
/// `0x768430` against the screen root. With the layout settled first (`0x768464`–`0x76846f`), read
|
||||
/// the frame's rect (`0x76847a`), clear its anchors (`0x767620`), set one TOPLEFT on the screen's
|
||||
/// TOPLEFT at the rect's top-left moved by `travel` (`0x767c70`), and give it an explicit size,
|
||||
/// `size` per axis or the rect's where that is 0 (`0x7684e1`, `0x768508`). Offsets and size are in
|
||||
/// the frame's own units, screen over its effective scale (`[G+0x58]`). None when the frame has no
|
||||
/// rect (`0x768481`); else whether its anchors or size changed.
|
||||
fn pin_top_left(
|
||||
model: &mut Model,
|
||||
h: FrameHandle,
|
||||
size: (f32, f32),
|
||||
travel: (f32, f32),
|
||||
) -> Option<bool> {
|
||||
UiScript::resolve_layout(model);
|
||||
let r = *model.resolved.get(&h)?;
|
||||
let root = model.screen;
|
||||
let scale = f64::from(eff_scale(model, h));
|
||||
let inv = 1.0 / scale;
|
||||
let x = ((f64::from(r.left) + f64::from(travel.0)) - f64::from(root.left)) * inv;
|
||||
let y = ((f64::from(r.top) + f64::from(travel.1)) - f64::from(root.top)) * inv;
|
||||
let w = if size.0 == 0.0 {
|
||||
((f64::from(r.right) - f64::from(r.left)) / scale) as f32
|
||||
} else {
|
||||
size.0
|
||||
};
|
||||
let ht = if size.1 == 0.0 {
|
||||
((f64::from(r.top) - f64::from(r.bottom)) / scale) as f32
|
||||
} else {
|
||||
size.1
|
||||
};
|
||||
let pin = Anchors::from([Anchor::new(
|
||||
Point::TopLeft,
|
||||
SCREEN,
|
||||
Point::TopLeft,
|
||||
x as f32,
|
||||
y as f32,
|
||||
)]);
|
||||
let input = model.layout_inputs.entry(h).or_default();
|
||||
let changed = !input.anchors.bits_eq(&pin)
|
||||
|| input.width.to_bits() != w.to_bits()
|
||||
|| input.height.to_bits() != ht.to_bits();
|
||||
input.anchors = pin;
|
||||
input.width = w;
|
||||
input.height = ht;
|
||||
if changed {
|
||||
model.touch_layout_frame(h);
|
||||
}
|
||||
// The size it now carries is explicit, as a `SetWidth`/`SetHeight` would leave it.
|
||||
model.note_authored_size(h);
|
||||
Some(changed)
|
||||
}
|
||||
|
||||
/// A drag moved the frame: if it is user-placed, the layout cache owes its file a rewrite.
|
||||
@@ -340,32 +365,28 @@ fn mark_user_placed_change(model: &mut Model, h: FrameHandle) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Apply the `SetMinResize`/`SetMaxResize` bounds to a proposed size per axis, as `0x768710`
|
||||
/// does: only an exact 0.0 is unbounded, min applies before max so max wins a crossed pair, and
|
||||
/// with no bound there is no floor, so a size can go negative.
|
||||
fn clamp_resize(model: &Model, h: FrameHandle, w: f32, hgt: f32) -> (f32, f32) {
|
||||
let (min, max) = model
|
||||
.arena
|
||||
.frame(h)
|
||||
.map_or(((0.0, 0.0), (0.0, 0.0)), |f| (f.min_resize, f.max_resize));
|
||||
let axis = |v: f32, lo: f32, hi: f32| {
|
||||
let v = if lo != 0.0 && v < lo { lo } else { v };
|
||||
if hi != 0.0 && v > hi {
|
||||
hi
|
||||
} else {
|
||||
v
|
||||
}
|
||||
};
|
||||
(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()
|
||||
let Some(mv) = model.moving else {
|
||||
return false;
|
||||
};
|
||||
if model.arena.frame(mv.frame).is_none() {
|
||||
model.moving = None;
|
||||
return false;
|
||||
}
|
||||
let (dx, dy) = (pos.0 - mv.sample.0, pos.1 - mv.sample.1);
|
||||
// A zero delta on both axes skips the step (`0x7655d6`–`0x7655f4`).
|
||||
if dx != 0.0 || dy != 0.0 {
|
||||
drag_step(model, mv, dx, dy);
|
||||
}
|
||||
// The sample takes the whole delta, whatever a bound kept of it (`0x765610`–`0x765628`).
|
||||
model.moving = Some(FrameMove {
|
||||
sample: (mv.sample.0 + dx, mv.sample.1 + dy),
|
||||
..mv
|
||||
});
|
||||
true
|
||||
}
|
||||
|
||||
/// A hide ends the move or size whose target is the frame going hidden: the hide's de-registration
|
||||
@@ -375,118 +396,141 @@ 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.
|
||||
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;
|
||||
return;
|
||||
}
|
||||
let (dx, dy) = (pos.0 - sz.sample.0, pos.1 - sz.sample.1);
|
||||
if dx == 0.0 && dy == 0.0 {
|
||||
return;
|
||||
}
|
||||
let inv = 1.0 / eff_scale(model, sz.frame);
|
||||
let (dx, dy) = (dx * inv, dy * inv);
|
||||
let dw = if sz.right {
|
||||
dx
|
||||
} else if sz.left {
|
||||
-dx
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
let dh = if sz.top {
|
||||
dy
|
||||
} else if sz.bottom {
|
||||
-dy
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
let Some((w0, h0)) = model
|
||||
/// One pump step, `0x768710` by way of `0x76a660`, for the cursor delta `(dx, dy)` in screen units.
|
||||
/// The grips read the explicit size (`vtbl+0x1c`/`+0x20`, `G+0x50`/`+0x54`), size it by the travel
|
||||
/// over the frame's scale, clamp it, and re-pin TOPLEFT shifted by the left and top travel only
|
||||
/// (`0x768974` → `0x768430`); a move adds to the TOPLEFT anchor itself.
|
||||
fn drag_step(model: &mut Model, mv: FrameMove, dx: f32, dy: f32) {
|
||||
let h = mv.frame;
|
||||
let scale = f64::from(eff_scale(model, h));
|
||||
let inv = 1.0 / scale;
|
||||
let (mut w, mut ht) = model
|
||||
.layout_inputs
|
||||
.get(&sz.frame)
|
||||
.map(|i| (i.width, i.height))
|
||||
else {
|
||||
return;
|
||||
};
|
||||
// The resize bounds are read only here, as only `0x76a660` reads them in the reference.
|
||||
let (new_w, new_h) = clamp_resize(model, sz.frame, w0 + dw, h0 + dh);
|
||||
// The rebate (`0x768770`/`0x7687a2`): the residual a clamp swallowed comes back out of the
|
||||
// drag delta, so the moving edge stops at the bound instead of the window walking. The delta
|
||||
// is already in local units, and an ungripped axis is not rebated.
|
||||
let rebate = |d: f32, want: f32, got: f32, grew: bool, gripped: bool| {
|
||||
if !gripped {
|
||||
return d;
|
||||
}
|
||||
let residual = got - want;
|
||||
if grew {
|
||||
d + residual
|
||||
} else {
|
||||
d - residual
|
||||
}
|
||||
};
|
||||
let dx = rebate(dx, w0 + dw, new_w, sz.right, sz.left || sz.right);
|
||||
let dy = rebate(dy, h0 + dh, new_h, sz.top, sz.top || sz.bottom);
|
||||
let Some(input) = model.layout_inputs.get_mut(&sz.frame) else {
|
||||
return;
|
||||
};
|
||||
input.width = new_w;
|
||||
input.height = new_h;
|
||||
let mut moved = input.width.to_bits() != w0.to_bits() || input.height.to_bits() != h0.to_bits();
|
||||
// A LEFT or BOTTOM grip also shifts the anchors on its axis, so a frame anchored by that side
|
||||
// keeps its opposite edge. The reference instead re-anchors the frame to one TOPLEFT point at
|
||||
// `StartSizing` (`0x768430`).
|
||||
if !input.anchors.is_empty() && (sz.left || sz.bottom) {
|
||||
for (x_off, y_off) in input.anchors.offsets_mut() {
|
||||
if sz.left {
|
||||
*x_off += dx;
|
||||
}
|
||||
if sz.bottom {
|
||||
*y_off += dy;
|
||||
.get(&h)
|
||||
.map_or((0.0, 0.0), |i| (i.width, i.height));
|
||||
// `G+0x60`/`+0x68` bound the width and `G+0x5c`/`+0x64` the height.
|
||||
let (min, max) = model
|
||||
.arena
|
||||
.frame(h)
|
||||
.map_or(((0.0, 0.0), (0.0, 0.0)), |f| (f.min_resize, f.max_resize));
|
||||
let (mut dx, mut dy) = (dx, dy);
|
||||
let add = |v: f32, by: f64| (f64::from(v) + by) as f32;
|
||||
let x = Axis::new(min.0, max.0, scale);
|
||||
let y = Axis::new(min.1, max.1, scale);
|
||||
match mv.grip {
|
||||
// Case 4: the delta over the scale goes straight into the TOPLEFT anchor's offsets
|
||||
// (`0x768c15`–`0x768c2e`); with no TOPLEFT anchor it re-pins at the whole delta instead
|
||||
// (`0x768c00`, `0x768c0f`).
|
||||
Point::Center => {
|
||||
let moved = model
|
||||
.layout_inputs
|
||||
.get_mut(&h)
|
||||
.and_then(|i| i.anchors.offset_mut(Point::TopLeft))
|
||||
.map(|(x_off, y_off)| {
|
||||
*x_off = add(*x_off, f64::from(dx) / scale);
|
||||
*y_off = add(*y_off, f64::from(dy) / scale);
|
||||
})
|
||||
.is_some();
|
||||
if moved {
|
||||
model.touch_layout_frame(h);
|
||||
mark_user_placed_change(model, h);
|
||||
return;
|
||||
}
|
||||
}
|
||||
moved |= (sz.left && dx != 0.0) || (sz.bottom && dy != 0.0);
|
||||
Point::TopLeft => {
|
||||
w = add(w, -f64::from(dx) * inv);
|
||||
x.clamp_near(&mut w, &mut dx, -1.0);
|
||||
ht = add(ht, f64::from(dy) * inv);
|
||||
y.clamp_near(&mut ht, &mut dy, 1.0);
|
||||
}
|
||||
Point::Top => {
|
||||
ht = add(ht, f64::from(dy) / scale);
|
||||
y.clamp_near(&mut ht, &mut dy, 1.0);
|
||||
dx = 0.0;
|
||||
}
|
||||
Point::TopRight => {
|
||||
w = add(w, f64::from(dx) * inv);
|
||||
x.clamp_far(&mut w);
|
||||
dx = 0.0;
|
||||
ht = add(ht, f64::from(dy) * inv);
|
||||
y.clamp_near(&mut ht, &mut dy, 1.0);
|
||||
}
|
||||
Point::Left => {
|
||||
w = add(w, -f64::from(dx) / scale);
|
||||
x.clamp_near(&mut w, &mut dx, -1.0);
|
||||
dy = 0.0;
|
||||
}
|
||||
Point::Right => {
|
||||
w = add(w, f64::from(dx) / scale);
|
||||
x.clamp_far(&mut w);
|
||||
(dx, dy) = (0.0, 0.0);
|
||||
}
|
||||
Point::BottomLeft => {
|
||||
w = add(w, -f64::from(dx) * inv);
|
||||
x.clamp_near(&mut w, &mut dx, -1.0);
|
||||
ht = add(ht, -f64::from(dy) * inv);
|
||||
y.clamp_far(&mut ht);
|
||||
dy = 0.0;
|
||||
}
|
||||
Point::Bottom => {
|
||||
ht = add(ht, -f64::from(dy) / scale);
|
||||
y.clamp_far(&mut ht);
|
||||
(dx, dy) = (0.0, 0.0);
|
||||
}
|
||||
Point::BottomRight => {
|
||||
w = add(w, f64::from(dx) * inv);
|
||||
x.clamp_far(&mut w);
|
||||
ht = add(ht, -f64::from(dy) * inv);
|
||||
y.clamp_far(&mut ht);
|
||||
(dx, dy) = (0.0, 0.0);
|
||||
}
|
||||
}
|
||||
// A nonzero cursor delta can still move nothing (a grip dragged across its axis, or held at a
|
||||
// bound), so the layout is touched only on a real change.
|
||||
if moved {
|
||||
// No anchor is retargeted, so only this node is touched.
|
||||
model.touch_layout_frame(sz.frame);
|
||||
mark_user_placed_change(model, sz.frame);
|
||||
if pin_top_left(model, h, (w, ht), (dx, dy)) == Some(true) {
|
||||
mark_user_placed_change(model, h);
|
||||
}
|
||||
model.sizing = Some(FrameSizing { sample: pos, ..sz });
|
||||
}
|
||||
|
||||
/// 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;
|
||||
return;
|
||||
/// One axis's `SetMinResize`/`SetMaxResize` pair, as `0x768710` applies it: only an exact 0.0 is
|
||||
/// unbounded, a size under the minimum or over the maximum takes the bound, min before max so max
|
||||
/// wins a crossed pair, and with no bound there is no floor, so a size can go negative.
|
||||
struct Axis {
|
||||
min: f32,
|
||||
max: f32,
|
||||
scale: f64,
|
||||
}
|
||||
|
||||
impl Axis {
|
||||
fn new(min: f32, max: f32, scale: f64) -> Axis {
|
||||
Axis { min, max, scale }
|
||||
}
|
||||
let (dx, dy) = (pos.0 - mv.sample.0, pos.1 - mv.sample.1);
|
||||
if dx == 0.0 && dy == 0.0 {
|
||||
return;
|
||||
|
||||
/// Clamp a size whose far edge moves (the right or the bottom), which the pin does not follow.
|
||||
fn clamp_far(&self, v: &mut f32) {
|
||||
if self.min != 0.0 && *v < self.min {
|
||||
*v = self.min;
|
||||
}
|
||||
if self.max != 0.0 && *v > self.max {
|
||||
*v = self.max;
|
||||
}
|
||||
}
|
||||
// Anchor offsets are in the frame's own units, so the screen delta divides by its scale, as
|
||||
// `0x768710`'s case 4 does.
|
||||
let inv = 1.0 / eff_scale(model, mv.frame);
|
||||
let (dx, dy) = (dx * inv, dy * inv);
|
||||
if let Some(input) = model.layout_inputs.get_mut(&mv.frame) {
|
||||
if !input.anchors.is_empty() {
|
||||
for (x_off, y_off) in input.anchors.offsets_mut() {
|
||||
*x_off += dx;
|
||||
*y_off += dy;
|
||||
}
|
||||
// No target moves, so only this node is touched; a zero delta already returned above.
|
||||
model.touch_layout_frame(mv.frame);
|
||||
mark_user_placed_change(model, mv.frame);
|
||||
|
||||
/// Clamp a size whose anchored edge moves (the left or the top), taking the residual back out
|
||||
/// of that edge's travel so it stops on the bound, as TOPLEFT's case does at `0x768770`,
|
||||
/// `0x7687a2`, `0x7687dd` and `0x768817`. `grows` is the sign the travel sizes the frame with:
|
||||
/// +1 for the top edge, -1 for the left.
|
||||
fn clamp_near(&self, v: &mut f32, travel: &mut f32, grows: f64) {
|
||||
let mut seat = |v: &mut f32, b: f32| {
|
||||
*travel =
|
||||
(f64::from(*travel) + grows * (f64::from(b) - f64::from(*v)) * self.scale) as f32;
|
||||
*v = b;
|
||||
};
|
||||
if self.min != 0.0 && *v < self.min {
|
||||
seat(v, self.min);
|
||||
}
|
||||
if self.max != 0.0 && *v > self.max {
|
||||
seat(v, self.max);
|
||||
}
|
||||
}
|
||||
model.moving = Some(FrameMove { sample: pos, ..mv });
|
||||
}
|
||||
|
||||
@@ -0,0 +1,293 @@
|
||||
//! Every drag pins its frame to one TOPLEFT anchor against the screen at its current rect, with an
|
||||
//! explicit size (`0x7652b0` → `0x768430`), and the pump (`0x768710`) moves or resizes that one
|
||||
//! anchor and size.
|
||||
|
||||
use super::common::script;
|
||||
use crate::script::{FrameLayout, LayoutPoint, UiScript};
|
||||
|
||||
/// A 200×100 frame CENTER-anchored in a full-screen holder on an 800×600 screen: its rect is
|
||||
/// left 300, right 500, bottom 250, top 350.
|
||||
fn centred_box() -> UiScript {
|
||||
let mut s = script();
|
||||
s.set_screen_size(800.0, 600.0);
|
||||
s.run(
|
||||
r#"
|
||||
Holder = CreateFrame("Frame", "Holder")
|
||||
Holder:SetPoint("BOTTOMLEFT", 0, 0); Holder:SetWidth(800); Holder:SetHeight(600)
|
||||
Box = CreateFrame("Frame", "Box", Holder)
|
||||
Box:SetPoint("CENTER", Holder, "CENTER", 0, 0)
|
||||
Box:SetWidth(200); Box:SetHeight(100)
|
||||
Box:SetMovable(true); Box:SetResizable(true)
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
s.resolve();
|
||||
s.mouse_move(400.0, 300.0);
|
||||
s
|
||||
}
|
||||
|
||||
/// `(left, right, bottom, top)` after a resolve.
|
||||
fn edges(s: &mut UiScript) -> (f32, f32, f32, f32) {
|
||||
s.resolve();
|
||||
let (l, r, b, t) = s
|
||||
.eval::<(f64, f64, f64, f64)>(
|
||||
"return Box:GetLeft(), Box:GetRight(), Box:GetBottom(), Box:GetTop()",
|
||||
)
|
||||
.unwrap();
|
||||
(l as f32, r as f32, b as f32, t as f32)
|
||||
}
|
||||
|
||||
/// The one anchor: `(count, point, relativeTo is the screen, relativePoint, x, y)`.
|
||||
fn anchor(s: &UiScript) -> (i64, String, bool, String, f32, f32) {
|
||||
let (n, p, screen, rp, x, y) = s
|
||||
.eval::<(i64, String, bool, String, f64, f64)>(
|
||||
"local p, rel, rp, x, y = Box:GetPoint(1) \
|
||||
return Box:GetNumPoints(), p, rel == nil, rp, x, y",
|
||||
)
|
||||
.unwrap();
|
||||
(n, p, screen, rp, x as f32, y as f32)
|
||||
}
|
||||
|
||||
fn pinned_at(x: f32, y: f32) -> (i64, String, bool, String, f32, f32) {
|
||||
(1, "TOPLEFT".into(), true, "TOPLEFT".into(), x, y)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_move_pins_one_topleft_anchor_against_the_screen() {
|
||||
let mut s = centred_box();
|
||||
s.run("Box:StartMoving()").unwrap();
|
||||
assert_eq!(
|
||||
anchor(&s),
|
||||
pinned_at(300.0, -250.0),
|
||||
"the drag entry pins before any cursor move: top 350 on a 600 screen is y -250"
|
||||
);
|
||||
s.mouse_move(430.0, 280.0);
|
||||
assert_eq!(edges(&mut s), (330.0, 530.0, 230.0, 330.0));
|
||||
assert_eq!(
|
||||
anchor(&s),
|
||||
pinned_at(330.0, -270.0),
|
||||
"the move rides that anchor"
|
||||
);
|
||||
s.run("Box:StopMovingOrSizing()").unwrap();
|
||||
assert_eq!(
|
||||
anchor(&s),
|
||||
pinned_at(330.0, -270.0),
|
||||
"the stop writes nothing"
|
||||
);
|
||||
assert_eq!(
|
||||
s.eval::<(f64, f64)>("return Box:GetWidth(), Box:GetHeight()")
|
||||
.unwrap(),
|
||||
(200.0, 100.0),
|
||||
"with the size made explicit"
|
||||
);
|
||||
|
||||
// The old target no longer carries it.
|
||||
s.run("Holder:SetPoint(\"BOTTOMLEFT\", 50, 0)").unwrap();
|
||||
assert_eq!(edges(&mut s), (330.0, 530.0, 230.0, 330.0));
|
||||
assert!(s.errors().is_empty(), "{:?}", s.errors());
|
||||
}
|
||||
|
||||
/// `FCF_Set_NormalChat` (`FloatingChatFrame.lua:1422-1423`) pins a window with a bare
|
||||
/// `StartMoving()`/`StopMovingOrSizing()` pair; a frame sized only by two anchors gets the size
|
||||
/// its rect has.
|
||||
#[test]
|
||||
fn a_bare_start_and_stop_pins_a_stretched_frame_at_its_rect() {
|
||||
let mut s = script();
|
||||
s.set_screen_size(800.0, 600.0);
|
||||
s.run(
|
||||
r#"
|
||||
Box = CreateFrame("Frame", "Box")
|
||||
Box:SetPoint("BOTTOMLEFT", 100, 100)
|
||||
Box:SetPoint("TOPRIGHT", UIParent, "BOTTOMLEFT", 300, 200)
|
||||
Box:SetMovable(true)
|
||||
Box:StartMoving() Box:StopMovingOrSizing()
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(anchor(&s), pinned_at(100.0, -400.0));
|
||||
assert_eq!(
|
||||
s.eval::<(f64, f64)>("return Box:GetWidth(), Box:GetHeight()")
|
||||
.unwrap(),
|
||||
(200.0, 100.0)
|
||||
);
|
||||
assert!(s.errors().is_empty(), "{:?}", s.errors());
|
||||
}
|
||||
|
||||
/// Each grip moves only its own edges, whatever the frame was anchored by (`0x768710`'s eight
|
||||
/// size cases re-pin TOPLEFT shifted by the left and top travel only, `0x76898a`).
|
||||
#[test]
|
||||
fn each_grip_moves_only_its_own_edges() {
|
||||
for grip in [
|
||||
"TOPLEFT",
|
||||
"TOP",
|
||||
"TOPRIGHT",
|
||||
"LEFT",
|
||||
"RIGHT",
|
||||
"BOTTOMLEFT",
|
||||
"BOTTOM",
|
||||
"BOTTOMRIGHT",
|
||||
] {
|
||||
let mut s = centred_box();
|
||||
s.run(&format!("Box:StartSizing(\"{grip}\")")).unwrap();
|
||||
s.mouse_move(420.0, 290.0);
|
||||
s.mouse_move(430.0, 285.0); // +30, -15 in all
|
||||
let (dl, dr) = match (grip.contains("LEFT"), grip.contains("RIGHT")) {
|
||||
(true, _) => (30.0, 0.0),
|
||||
(_, true) => (0.0, 30.0),
|
||||
_ => (0.0, 0.0),
|
||||
};
|
||||
let (db, dt) = match (grip.contains("BOTTOM"), grip.contains("TOP")) {
|
||||
(true, _) => (-15.0, 0.0),
|
||||
(_, true) => (0.0, -15.0),
|
||||
_ => (0.0, 0.0),
|
||||
};
|
||||
let want = (300.0 + dl, 500.0 + dr, 250.0 + db, 350.0 + dt);
|
||||
assert_eq!(edges(&mut s), want, "{grip}");
|
||||
assert_eq!(
|
||||
anchor(&s),
|
||||
pinned_at(want.0, want.3 - 600.0),
|
||||
"{grip} leaves the one TOPLEFT pin"
|
||||
);
|
||||
s.run("Box:StopMovingOrSizing()").unwrap();
|
||||
assert!(s.errors().is_empty(), "{grip}: {:?}", s.errors());
|
||||
}
|
||||
}
|
||||
|
||||
/// The bounds clamp each case. Where the grip moves the anchor (a LEFT or TOP edge), the residual
|
||||
/// comes back out of the travel, so the edge stops on the bound (`0x768770`, `0x7687a2`,
|
||||
/// `0x7687dd`, `0x768817`); the far edges just clamp. The cursor sample always advances by the
|
||||
/// whole delta (`0x765610`), so a grip held past its bound answers the first move back.
|
||||
#[test]
|
||||
fn the_bounds_clamp_and_the_anchored_edge_stops_on_them() {
|
||||
let bounded = || {
|
||||
let s = centred_box();
|
||||
s.run("Box:SetMinResize(150, 80) Box:SetMaxResize(260, 120)")
|
||||
.unwrap();
|
||||
s
|
||||
};
|
||||
|
||||
let mut s = bounded();
|
||||
s.run("Box:StartSizing(\"LEFT\")").unwrap();
|
||||
s.mouse_move(480.0, 300.0);
|
||||
assert_eq!(
|
||||
edges(&mut s),
|
||||
(350.0, 500.0, 250.0, 350.0),
|
||||
"LEFT at the min"
|
||||
);
|
||||
s.mouse_move(460.0, 300.0);
|
||||
assert_eq!(
|
||||
edges(&mut s),
|
||||
(330.0, 500.0, 250.0, 350.0),
|
||||
"and back at once"
|
||||
);
|
||||
|
||||
let mut s = bounded();
|
||||
s.run("Box:StartSizing(\"TOP\")").unwrap();
|
||||
s.mouse_move(400.0, 350.0);
|
||||
assert_eq!(
|
||||
edges(&mut s),
|
||||
(300.0, 500.0, 250.0, 370.0),
|
||||
"TOP at the max"
|
||||
);
|
||||
|
||||
let mut s = bounded();
|
||||
s.run("Box:StartSizing(\"TOPLEFT\")").unwrap();
|
||||
s.mouse_move(300.0, 400.0);
|
||||
assert_eq!(
|
||||
edges(&mut s),
|
||||
(240.0, 500.0, 250.0, 370.0),
|
||||
"TOPLEFT at both maxima"
|
||||
);
|
||||
|
||||
let mut s = bounded();
|
||||
s.run("Box:StartSizing(\"BOTTOMRIGHT\")").unwrap();
|
||||
s.mouse_move(500.0, 360.0);
|
||||
assert_eq!(
|
||||
edges(&mut s),
|
||||
(300.0, 560.0, 270.0, 350.0),
|
||||
"BOTTOMRIGHT at the max width and the min height"
|
||||
);
|
||||
assert!(s.errors().is_empty(), "{:?}", s.errors());
|
||||
}
|
||||
|
||||
/// `StartSizing` starts from BOTTOMRIGHT and takes a grip only from a string naming a point,
|
||||
/// matched case-insensitively (`0x776913`, `0x6f1840`).
|
||||
#[test]
|
||||
fn a_missing_or_unknown_grip_is_bottomright() {
|
||||
for call in [
|
||||
"Box:StartSizing()",
|
||||
"Box:StartSizing(\"bogus\")",
|
||||
"Box:StartSizing(7)",
|
||||
"Box:StartSizing(\"bottomright\")",
|
||||
] {
|
||||
let mut s = centred_box();
|
||||
s.run(call).unwrap();
|
||||
s.mouse_move(420.0, 290.0);
|
||||
assert_eq!(edges(&mut s), (300.0, 520.0, 240.0, 350.0), "{call}");
|
||||
}
|
||||
let mut s = centred_box();
|
||||
s.run("Box:StartSizing(\"left\")").unwrap();
|
||||
s.mouse_move(420.0, 290.0);
|
||||
assert_eq!(
|
||||
edges(&mut s),
|
||||
(320.0, 500.0, 250.0, 350.0),
|
||||
"a lower-case name"
|
||||
);
|
||||
}
|
||||
|
||||
/// The pin needs a rect (`0x768320` fails, `0x7652cd`), so a frame that has none starts no drag:
|
||||
/// no userPlaced stamp and no slot.
|
||||
#[test]
|
||||
fn a_frame_with_no_rect_starts_no_drag() {
|
||||
let mut s = centred_box();
|
||||
s.run(
|
||||
r#"
|
||||
Lost = CreateFrame("Frame", "Lost")
|
||||
Lost:SetWidth(50); Lost:SetHeight(50)
|
||||
Lost:SetMovable(true)
|
||||
Lost:StartMoving()
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
assert!(!s.eval::<bool>("return Lost:IsUserPlaced()").unwrap());
|
||||
assert_eq!(s.eval::<i64>("return Lost:GetNumPoints()").unwrap(), 0);
|
||||
s.run("Box:StartMoving()").unwrap();
|
||||
s.mouse_move(410.0, 300.0);
|
||||
assert_eq!(
|
||||
edges(&mut s),
|
||||
(310.0, 510.0, 250.0, 350.0),
|
||||
"the slot was free"
|
||||
);
|
||||
assert!(s.errors().is_empty(), "{:?}", s.errors());
|
||||
}
|
||||
|
||||
/// After a drag the layout cache holds the one TOPLEFT anchor and the size, the reference's `X:`,
|
||||
/// `Y:`, `W:` and `H:` (`0x490e60`), and its apply seats that back.
|
||||
#[test]
|
||||
fn the_layout_cache_round_trips_the_pin() {
|
||||
let mut s = centred_box();
|
||||
s.run("Box:StartMoving()").unwrap();
|
||||
s.mouse_move(430.0, 280.0);
|
||||
s.run("Box:StopMovingOrSizing()").unwrap();
|
||||
let want = FrameLayout {
|
||||
name: "Box".into(),
|
||||
width: 200.0,
|
||||
height: 100.0,
|
||||
points: vec![LayoutPoint {
|
||||
point: "TOPLEFT".into(),
|
||||
relative_to: None,
|
||||
relative_point: "TOPLEFT".into(),
|
||||
x: 330.0,
|
||||
y: -270.0,
|
||||
}],
|
||||
};
|
||||
let saved = s.user_placed_layouts();
|
||||
assert_eq!(saved, std::slice::from_ref(&want));
|
||||
|
||||
let mut fresh = centred_box();
|
||||
fresh.restore_user_placed_layouts(saved);
|
||||
assert_eq!(edges(&mut fresh), (330.0, 530.0, 230.0, 330.0));
|
||||
assert_eq!(anchor(&fresh), pinned_at(330.0, -270.0));
|
||||
assert_eq!(fresh.user_placed_layouts(), [want]);
|
||||
assert!(fresh.errors().is_empty(), "{:?}", fresh.errors());
|
||||
}
|
||||
@@ -232,13 +232,14 @@ fn a_frame_moving_when_the_pointer_leaves_keeps_its_offsets() {
|
||||
s.eval::<(f64, f64)>("local _, _, _, x, y = Far:GetPoint() return x, y")
|
||||
.unwrap()
|
||||
};
|
||||
assert_eq!(point(&s), (800.0, 500.0));
|
||||
// The drag pinned TOPLEFT against the screen (`0x768430`): top 600 on a 768 screen.
|
||||
assert_eq!(point(&s), (800.0, -168.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!(point(&s), (800.0, -168.0), "the leave moves nothing");
|
||||
assert_eq!(s.eval::<f64>("return Far:GetLeft()").unwrap(), 800.0);
|
||||
assert_eq!(
|
||||
s.user_placed_layouts(),
|
||||
@@ -251,7 +252,7 @@ fn a_frame_moving_when_the_pointer_leaves_keeps_its_offsets() {
|
||||
// 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_eq!(point(&s), (810.0, -168.0));
|
||||
assert!(s.errors().is_empty(), "{:?}", s.errors());
|
||||
}
|
||||
|
||||
@@ -286,6 +287,8 @@ fn a_drag_cut_short_by_the_pointer_leaving_keeps_the_last_real_position() {
|
||||
.iter()
|
||||
.find(|l| l.name == "Here")
|
||||
.expect("Here is saved");
|
||||
assert_eq!((here.points[0].x, here.points[0].y), (130.0, 110.0));
|
||||
// Saved as the drag's TOPLEFT pin (`0x768430`): top 210 on a 768 screen.
|
||||
assert_eq!(here.points[0].point, "TOPLEFT");
|
||||
assert_eq!((here.points[0].x, here.points[0].y), (130.0, -558.0));
|
||||
assert!(s.errors().is_empty(), "{:?}", s.errors());
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ mod cooldown;
|
||||
mod create_frame_template;
|
||||
mod cvar_bindings;
|
||||
mod dispatch_bench;
|
||||
mod drag_pin;
|
||||
mod drag_threshold;
|
||||
mod end_to_end;
|
||||
mod engine_updates;
|
||||
|
||||
@@ -62,11 +62,12 @@ fn the_canonical_addon_idiom_moves_a_frame_and_the_position_survives_the_stop()
|
||||
assert_eq!(corner(&mut s), (200.0, 150.0), "no longer following");
|
||||
assert!(s.errors().is_empty(), "{:?}", s.errors());
|
||||
|
||||
// The reference moves the anchors in place, so GetPoint reads the frame's own point, dragged.
|
||||
// The drag re-anchored it to one TOPLEFT against the screen (`0x768430`), so GetPoint reads
|
||||
// that point, not the authored BOTTOMLEFT: top 230 on a 600 screen is y -370.
|
||||
let (point, x, y) = s
|
||||
.eval::<(String, f64, f64)>("local p, _, _, x, y = MovePanel:GetPoint() return p, x, y")
|
||||
.unwrap();
|
||||
assert_eq!((point.as_str(), x, y), ("BOTTOMLEFT", 200.0, 150.0));
|
||||
assert_eq!((point.as_str(), x, y), ("TOPLEFT", 200.0, -370.0));
|
||||
}
|
||||
|
||||
/// Each move applies only its own delta: the pump re-centers its sample after every step.
|
||||
@@ -163,7 +164,8 @@ fn stop_moving_or_sizing_is_harmless_with_nothing_moving_and_stops_only_its_own_
|
||||
assert!(s.errors().is_empty(), "{:?}", s.errors());
|
||||
}
|
||||
|
||||
/// A move translates every anchor, so a frame sized by two anchors keeps its size.
|
||||
/// A frame sized by two anchors keeps its size: the drag pins it to one anchor with the size its
|
||||
/// rect had (`0x768430`).
|
||||
#[test]
|
||||
fn a_frame_stretched_between_two_anchors_moves_rigidly() {
|
||||
let mut s = script();
|
||||
@@ -194,7 +196,7 @@ fn a_frame_stretched_between_two_anchors_moves_rigidly() {
|
||||
let size_after = s
|
||||
.eval::<(f64, f64)>("return StretchPanel:GetWidth(), StretchPanel:GetHeight()")
|
||||
.unwrap();
|
||||
assert_eq!((left, bottom), (130.0, 140.0), "both anchors translated");
|
||||
assert_eq!((left, bottom), (130.0, 140.0), "the pinned anchor moved");
|
||||
assert_eq!(size_after, (200.0, 100.0), "and the frame kept its size");
|
||||
assert!(s.errors().is_empty(), "{:?}", s.errors());
|
||||
}
|
||||
@@ -324,8 +326,7 @@ fn the_xml_movable_and_resizable_attributes_reach_the_methods() {
|
||||
}
|
||||
|
||||
/// `StartSizing(grip)` (`0x776830`, called from `FloatingChatFrame.lua:600`) moves the gripped
|
||||
/// edges and plants the opposite ones. Which edges a grip moves is untraced in the reference, whose
|
||||
/// verb has no inline math; this takes the anchor point's plain meaning, as that caller does.
|
||||
/// edges and plants the opposite ones (`0x768710`'s grip cases).
|
||||
#[test]
|
||||
fn start_sizing_moves_the_gripped_edge_and_plants_the_other() {
|
||||
let mut s = crate::script::UiScript::new().unwrap();
|
||||
|
||||
Reference in New Issue
Block a user