the colour wheel draws, chat windows move, and the minimap ping returns
- the colour picker can pick a colour: the hue disc and the value strip are engine-generated pixels (no BLP in the chain is a wheel), reproduced from the reference's own lattice — a hard-cut circle at a literal V = 1.0, the brightness marker moving over a disc that never dims - chat windows move and resize once unlocked (Lock/Unlock Window on the tab menu; the reference's own 296×75 to 608×400 bounds), the border textures doubling as the grips, and the placement is remembered per character in benilla-config/layout; both windows ship locked, as 1.12's reset leaves them - the minimap ping is back and pinned: one world point the engine draws, the click converted in the right unit (the old path was ~27% off at 1080p), and no 10-yard auto-clear — that was the gossip marker's rule, and walking to your own ping was deleting it - the Lightwell can be clicked: a billboard card is a world root with no parent hop, so the hit resolved to nothing and the whole shaft published a null mouseover; casting a hunter shot chains Auto Shot through `Spell.dbc`'s modalNextSpell column (the mechanism nobody had read — the next snapshot carries the seam that makes it fire in play); tradeskill list rows stop tooltipping (that hover was ours, not Blizzard's), and OnMouseUp goes to the frame that took the press Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -188,17 +188,21 @@ impl UiScript {
|
||||
editbox::drag_update(&self.lua, x, y);
|
||||
let new_id = self.hit_test(x, y);
|
||||
#[allow(clippy::type_complexity)]
|
||||
let (old_id, drag_start, boundary_crossed, slider_change): (
|
||||
let (old_id, drag_start, boundary_crossed, slider_change, color_change): (
|
||||
Option<u32>,
|
||||
Option<(u32, String)>,
|
||||
bool,
|
||||
Option<(u32, f32)>,
|
||||
Option<(u32, f64, f64, f64)>,
|
||||
) = {
|
||||
let mut model = self.model_mut();
|
||||
model.cursor_pos = (x, y);
|
||||
// Advance an in-flight Slider thumb drag first — dragging within one frame crosses no
|
||||
// hover boundary, so this must run before the early return below (decision 0250 §5).
|
||||
let slider_change = super::slider::drag_move(&mut model, x, y);
|
||||
// The colour picker's wheel/strip drag, for the identical reason — and it fires on
|
||||
// every move, not only on a change (the widget has no change-gate).
|
||||
let color_change = super::colorselect::drag_move(&mut model, x, y);
|
||||
// A frame in flight from `StartMoving()` follows the cursor here for the same reason
|
||||
// (`object::movable`): the frame moves *under* the cursor, so the hover boundary never
|
||||
// crosses and a pump behind the early return would only ever advance when the cursor
|
||||
@@ -208,7 +212,8 @@ impl UiScript {
|
||||
let drag_start = cursor::maybe_start_drag(&mut model, (x, y));
|
||||
let new_handle = new_id.and_then(|id| model.id_to_frame.get(&id).copied());
|
||||
if new_handle == model.mouseover {
|
||||
(None, drag_start, false, slider_change) // no boundary crossed
|
||||
// no boundary crossed
|
||||
(None, drag_start, false, slider_change, color_change)
|
||||
} else {
|
||||
// Fire OnLeave only if the frame we're leaving is still live.
|
||||
let old_id = model.mouseover.and_then(|h| {
|
||||
@@ -220,7 +225,7 @@ impl UiScript {
|
||||
.flatten()
|
||||
});
|
||||
model.mouseover = new_handle;
|
||||
(old_id, drag_start, true, slider_change)
|
||||
(old_id, drag_start, true, slider_change, color_change)
|
||||
}
|
||||
};
|
||||
// The thumb-drag value change fires OnValueChanged (outside the borrow) — the scrollbar's
|
||||
@@ -235,6 +240,11 @@ impl UiScript {
|
||||
self.push_error(e);
|
||||
}
|
||||
}
|
||||
if let Some((id, r, g, b)) = color_change {
|
||||
if let Err(e) = super::colorselect::fire_by_id(&self.lua, id, r, g, b) {
|
||||
self.push_error(e);
|
||||
}
|
||||
}
|
||||
if let Some((id, button)) = drag_start {
|
||||
let btn = match self.lua.create_string(&button) {
|
||||
Ok(s) => Value::String(s),
|
||||
@@ -285,7 +295,9 @@ impl UiScript {
|
||||
|
||||
/// A mouse button transition at `(x, y)`: `button` is the WoW name (`"LeftButton"`,
|
||||
/// `"RightButton"`, …), `down` is press vs release. Fires `OnMouseDown(self, button)` on press or
|
||||
/// `OnMouseUp(self, button)` on release, on the captured frame.
|
||||
/// `OnMouseUp(self, button)` on release. **The press fires on the hit frame and the release on
|
||||
/// the frame that took the press** — the client's mouse capture (`root+0x80`); see the dispatch
|
||||
/// itself for the bytes and for the stuck-resize it prevents.
|
||||
///
|
||||
/// Separately, `OnClick` fires per the hit frame's `RegisterForClicks` set
|
||||
/// ([`button::wants_click`]): a **press** fires immediately when the frame registered
|
||||
@@ -352,6 +364,18 @@ impl UiScript {
|
||||
{
|
||||
self.model_mut().moving = None;
|
||||
}
|
||||
// The capture this release belongs to — read HERE because the borrow below *drains*
|
||||
// `mouse_down_on` for this button. See the `OnMouseUp` dispatch further down for why the
|
||||
// press target rather than the hit frame.
|
||||
let captured_id: Option<u32> = if down {
|
||||
None
|
||||
} else {
|
||||
let model = self.model_ref();
|
||||
model
|
||||
.mouse_down_on
|
||||
.get(button)
|
||||
.and_then(|h| model.frame_to_id.get(h).copied())
|
||||
};
|
||||
// The session clock, read before the borrow below — the double-click detector's only input
|
||||
// beyond the hit ([`Model::last_click`]). Same `GetTime()` seconds every script sees.
|
||||
let now = self.now();
|
||||
@@ -368,13 +392,22 @@ impl UiScript {
|
||||
// "<Button>ButtonDown"; a release fires it when press+release landed on the same frame AND
|
||||
// it registered "<Button>ButtonUp" — UNLESS a started drag is being resolved instead.
|
||||
#[allow(clippy::type_complexity)]
|
||||
let (click_id, drag_release, world_dropped, slider_jump, double_id, abandoned): (
|
||||
let (
|
||||
click_id,
|
||||
drag_release,
|
||||
world_dropped,
|
||||
slider_jump,
|
||||
double_id,
|
||||
abandoned,
|
||||
color_jump,
|
||||
): (
|
||||
Option<u32>,
|
||||
Option<cursor::DragRelease>,
|
||||
bool,
|
||||
Option<(u32, f32)>,
|
||||
Option<u32>,
|
||||
Option<crate::widget::FrameHandle>,
|
||||
Option<(u32, f64, f64, f64)>,
|
||||
) = {
|
||||
let mut model = self.model_mut();
|
||||
let hit_handle = hit_id.and_then(|id| model.id_to_frame.get(&id).copied());
|
||||
@@ -401,16 +434,27 @@ impl UiScript {
|
||||
} else {
|
||||
None
|
||||
};
|
||||
// A left press inside a ColorSelect's wheel or value strip captures the colour
|
||||
// drag AND jumps the colour to the click point in the same call — the widget's
|
||||
// press handler invokes its own cursor-position handler before returning
|
||||
// (`0x78bf70 call [eax+0x3c]`), so there is no such thing as a press that only
|
||||
// arms.
|
||||
let color_jump = if button == "LeftButton" {
|
||||
super::colorselect::begin_drag(&mut model, hit_handle, x, y)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let wants = format!("{button}Down");
|
||||
let click = hit_handle
|
||||
.filter(|&h| button::wants_click(&model, h, &wants))
|
||||
.and(hit_id);
|
||||
(click, None, false, jump, None, abandoned)
|
||||
(click, None, false, jump, None, abandoned, color_jump)
|
||||
} else {
|
||||
let pressed = model.mouse_down_on.remove(button);
|
||||
// A left release ends any in-flight thumb drag (decision 0250 §5).
|
||||
if button == "LeftButton" {
|
||||
super::slider::end_drag(&mut model);
|
||||
super::colorselect::end_drag(&mut model);
|
||||
}
|
||||
let same_frame = matches!(
|
||||
(pressed, hit_handle),
|
||||
@@ -478,7 +522,7 @@ impl UiScript {
|
||||
let double_id = double.and_then(|h| model.frame_to_id.get(&h).copied());
|
||||
// Exclusive, not additive — the double leg is taken *instead of* the single one.
|
||||
let click = if double_id.is_some() { None } else { click };
|
||||
(click, release, dropped, None, double_id, None)
|
||||
(click, release, dropped, None, double_id, None, None)
|
||||
}
|
||||
};
|
||||
// A track press's value jump fires OnValueChanged first (outside the borrow), the same
|
||||
@@ -493,8 +537,45 @@ impl UiScript {
|
||||
self.push_error(e);
|
||||
}
|
||||
}
|
||||
// The press's colour jump fires OnColorSelect the same way.
|
||||
if let Some((id, r, g, b)) = color_jump {
|
||||
if let Err(e) = super::colorselect::fire_by_id(&self.lua, id, r, g, b) {
|
||||
self.push_error(e);
|
||||
}
|
||||
}
|
||||
// ── `OnMouseUp` goes to the frame that took the PRESS, not to whatever is under the
|
||||
// cursor now — the mouse CAPTURE (`root+0x80`).
|
||||
//
|
||||
// The client's mouse-DOWN handler `0x7662c0` "resolves target = root+0x80 (existing
|
||||
// capture) else root+0x7c (hover frame) … then title-region drag or **capture**+OnMouseDown"
|
||||
// (wow-re ledger `0x7662c0`, VERIFIED), so a press takes the pointer.
|
||||
//
|
||||
// ⚠ **The release half is INFERRED, not carved.** That the capture is what the *UP*
|
||||
// handler dispatches through is an extrapolation from the DOWN handler: the sibling
|
||||
// `0x766420` (category `0xe`) carries only a bulk wave-classify label in wow-re's ledger,
|
||||
// not a derivation. The inference is strong — a capture nothing consults on release would
|
||||
// be pointless, and something has to clear `root+0x80` — but it is an inference, and this
|
||||
// is a whole-UI change (every press-on-A-release-on-B in the client now delivers to A). A
|
||||
// §5 is dispatched for `0x766420`'s target resolution (decision 1594); when it lands this
|
||||
// comment says VERIFIED or this code changes.
|
||||
//
|
||||
// We already track exactly the capture
|
||||
// — [`Model::mouse_down_on`], the press target per button, which `OnClick`'s same-frame
|
||||
// rule reads — and firing `OnMouseUp` at the *hit* frame instead was the one place it went
|
||||
// unused. This function's own doc has always said "on the captured frame"; the code did
|
||||
// not.
|
||||
//
|
||||
// It is not a nicety: a chat window's resize grip is a 16×16 button that the drag pulls
|
||||
// out from under the cursor the moment the size clamps at `SetMinResize`/`SetMaxResize`
|
||||
// (the rebate plants the edge on the bound while the cursor keeps going). Its
|
||||
// `OnMouseUp → FCF_StopResize` then never ran, and `Model::sizing` stayed held for the
|
||||
// rest of the session — the stuck-drag failure `cursor::abandon_drag` exists to prevent on
|
||||
// the other path.
|
||||
//
|
||||
// A press that captured NOTHING (over the world, or swallowed by a title region) falls
|
||||
// back to the hit frame, which is `root+0x7c`'s arm of the same resolve.
|
||||
let script = if down { "OnMouseDown" } else { "OnMouseUp" };
|
||||
if let Some(id) = hit_id {
|
||||
if let Some(id) = if down { hit_id } else { captured_id.or(hit_id) } {
|
||||
if let Err(e) = event::fire_widget_handler(&self.lua, id, script, vec![btn.clone()]) {
|
||||
self.push_error(e);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user