Layout-mode drag, title press takes the slot, links are their own button (#961)

A title-region press refused while another drag held the slot, `SetLayoutMode` stored nothing, and a press on a chat link made the chat frame itself the target, so every link click started a title move of the window.

In 1.12.1 the drag entry takes the slot unread (`0x7652b0`); a mid-drag press goes to the frozen hover (`0x76637f`). With `SetLayoutMode` (`0x488540`) on, a Ctrl or Alt press drags the nearest movable toplevel (`0x765490`) from a 3×3 grip grid (`0x765320`) and is consumed, world included; its release only ends the drag (`0x7664e0`). Each link span is its own Button (`0x7a3240`) routing `OnHyperlinkEnter`/`Leave`/`Click` to the parent (`0x7a3780`–`0x7a37e0`).

These now hold; a release goes to the capture whatever its button (`0x76642b`), and number arguments keep `_ftol`'s low dword.

Tests bite on the parent for each.

Fixes #951
Fixes #952
Fixes #957

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Sam Sung
2026-10-12 07:22:54 +07:00
committed by GitHub
co-authored by Claude Opus 5.5
parent 108c354fc6
commit 985dabff44
24 changed files with 1049 additions and 135 deletions
+4 -1
View File
@@ -708,6 +708,9 @@ pub(super) fn latch_world_mouse(
// ground-targeting veto (`+0x3c`, `0x7cba30`) arrives through `PointerOverUi`
// (`UiScript::set_nameplate_hit_test_veto`). The wheel still zooms over a plate.
pointer_over_ui: Res<crate::ui_script::PointerOverUi>,
// A press the UI consumed with nothing hovered, the layout drag's: no frame saw it, the
// world's included (`0x766353`).
press_consumed: Option<Res<crate::ui_script::PlayerUiPressConsumed>>,
mut rig: ResMut<CameraControl>,
cameras: Query<&Camera, With<FlyCam>>,
window: Single<&Window, With<PrimaryWindow>>,
@@ -721,7 +724,7 @@ pub(super) fn latch_world_mouse(
let Ok(camera) = cameras.single() else {
return;
};
let over_ui = pointer_over_ui.0;
let over_ui = pointer_over_ui.0 || press_consumed.is_some_and(|c| c.0);
let world_press = rig.look.is_some() || (cursor_in_viewport(&window, camera) && !over_ui);
let wm = &mut rig.world_mouse;
wm.update(&buttons, world_press);
@@ -178,6 +178,25 @@ fn mouselook_start_turns_the_camera_and_the_character_with_no_button_held() {
assert!(w.resource::<CameraControl>().is_looking());
}
/// A right press over bare world that the UI consumed, as a layout-mode Alt press is
/// (`0x766353`, before any frame sees it), turns nothing; the same press unconsumed turns.
#[test]
fn a_press_the_ui_consumed_over_the_world_turns_nothing() {
let mut w = world(false);
w.insert_resource(crate::ui_script::PlayerUiPressConsumed(true));
press(&mut w, MouseButton::Right);
frame(&mut w, 40.0);
assert_eq!(w.resource::<CameraControl>().look, None);
assert_eq!(yaw(&mut w), 0.0, "the camera did not turn");
let mut w = world(false);
w.insert_resource(crate::ui_script::PlayerUiPressConsumed(false));
press(&mut w, MouseButton::Right);
frame(&mut w, 40.0);
assert_eq!(w.resource::<CameraControl>().look, Some(LookButton::Right));
assert!(yaw(&mut w) < 0.0, "an unconsumed press turns it");
}
/// `MouselookStop` (`0x514240`) ends the session: the cursor comes back and the mouse turns
/// nothing, and `IsMouselooking` answers nil.
#[test]
@@ -536,9 +536,13 @@ fn drag_named_tab(s: &mut UiScript, window: &str, dx: f32, dy: f32) {
Some(tab),
"the tab is hit"
);
// As a player's mouse runs it: the hover is on the tab before the press, and a frame resolves
// the moved layout before the release tests the cursor against the tab.
s.mouse_move(tx, ty);
s.mouse_button(tx, ty, "LeftButton", true);
s.mouse_move(tx + DRAG_STARTS_AT, ty);
s.mouse_move(tx + dx, ty + dy);
s.resolve();
s.mouse_button(tx + dx, ty + dy, "LeftButton", false);
s.resolve();
}
@@ -571,6 +575,7 @@ fn an_undocked_tab_drags_again_and_a_plain_click_drops_the_window() {
reveal_window(&mut s, "ChatFrame2");
let (tx, ty) = centre(&mut s, "ChatFrame2Tab");
s.mouse_move(tx, ty);
s.mouse_button(tx, ty, "LeftButton", true);
assert!(
s.eval::<bool>("return MOVING_CHATFRAME == ChatFrame2")
+10 -3
View File
@@ -9,7 +9,10 @@ use bevy::window::PrimaryWindow;
use benilla_ui::script::UiScript;
use super::{CursorPayloadHeld, PlayerUiClickConsumed, PlayerUiHover, UiKeyboardCapture};
use super::{
CursorPayloadHeld, PlayerUiClickConsumed, PlayerUiHover, PlayerUiPressConsumed,
UiKeyboardCapture,
};
use crate::bindings::WheelNotches;
use crate::textinput::{self, keymap, HostClipboard};
@@ -20,6 +23,7 @@ use crate::textinput::{self, keymap, HostClipboard};
pub(super) struct PointerFeed<'w> {
hover: ResMut<'w, PlayerUiHover>,
click_consumed: ResMut<'w, PlayerUiClickConsumed>,
press_consumed: ResMut<'w, PlayerUiPressConsumed>,
hovered: Res<'w, crate::target::Hovered>,
hovered_object: Res<'w, crate::target::HoveredObject>,
occlusion: Res<'w, crate::target::PickOcclusion>,
@@ -101,12 +105,14 @@ pub(super) fn feed_ui_input(
// capture pins it: skip the mouse half whole, else-arm included, whose `pointer_left_window`
// would disarm the probe's gesture between its press and release.
let synthetic = pointer.synthetic.0 || pointer.capture_pinned.0;
let (hover, click_consumed, payload_held) = (
let (hover, click_consumed, press_consumed, payload_held) = (
&mut pointer.hover,
&mut pointer.click_consumed,
&mut pointer.press_consumed,
&mut pointer.payload_held,
);
click_consumed.0 = false;
press_consumed.0 = false;
let Some(mut script) = script else {
capture.typing = false;
capture.arrows_fall_through = false;
@@ -183,7 +189,8 @@ pub(super) fn feed_ui_input(
click_consumed.0 = true;
}
}
script.mouse_button(x, y, name, true);
// A press the UI consumed is not the world's, hovered frame or not.
press_consumed.0 |= script.mouse_button(x, y, name, true);
}
if buttons.just_released(btn) {
script.mouse_button(x, y, name, false);
+7
View File
@@ -122,6 +122,12 @@ pub(crate) struct UiKeyboardCapture {
#[derive(Resource, Default)]
pub(crate) struct PlayerUiClickConsumed(pub(crate) bool);
/// A press the UI consumed this frame, whatever is under the pointer: the layout drag's press
/// returns at `0x766353`–`0x76635b`, before any frame, the world frame included, sees it, so the
/// world latch takes no press this frame even with no UI frame hovered.
#[derive(Resource, Default)]
pub(crate) struct PlayerUiPressConsumed(pub(crate) bool);
/// Whether the cursor carries a payload, a `Send` mirror. A click on sky with one held never
/// deselects: the reference gates its nothing-leg `SetSelection(0,0)` on no payload (`0x492d30`).
#[derive(Resource, Default)]
@@ -298,6 +304,7 @@ impl Plugin for UiScriptPlugin {
.init_resource::<PlayerUiHover>()
.init_resource::<UiKeyboardCapture>()
.init_resource::<PlayerUiClickConsumed>()
.init_resource::<PlayerUiPressConsumed>()
.init_resource::<CursorPayloadHeld>()
.init_resource::<UiClock>()
.init_resource::<AddOnIdentity>()
+31 -8
View File
@@ -16,16 +16,25 @@
use mlua::{Lua, Value};
/// Shape A: `is-number` (`0x6f34d0`), `tonumber` (`0x6f3620`), then the truncating `0x40a2b0`,
/// raising the binding's `Usage:` on failure. A missing argument fails like a wrong-typed one
/// (`0x6f3410` returns NULL past `L->top`); a numeric string passes. `0x40a2b0` stores a qword but
/// the bindings consume only the low dword, hence `i32`.
///
/// Deviation: past ±2^63 Rust saturates where the x87 stores its integer-indefinite pattern,
/// because no real argument reaches that range.
/// `_ftol` (`0x40a2b0`) as the bindings consume it: the x87 rounding control set to chop, a
/// `fistp` to a qword, and its low dword. What the qword cannot hold (an infinity, NaN, ±2^63 and
/// beyond) stores the integer-indefinite pattern `0x8000_0000_0000_0000`, whose low dword is 0.
pub(crate) fn ftol(n: f64) -> i32 {
const TWO_63: f64 = 9_223_372_036_854_775_808.0;
let t = n.trunc();
if (-TWO_63..TWO_63).contains(&t) {
t as i64 as i32
} else {
0
}
}
/// Shape A: `is-number` (`0x6f34d0`), `tonumber` (`0x6f3620`), then the truncating `0x40a2b0`
/// ([`ftol`]), raising the binding's `Usage:` on failure. A missing argument fails like a
/// wrong-typed one (`0x6f3410` returns NULL past `L->top`); a numeric string passes.
pub(crate) fn number_arg(lua: &Lua, v: Value, usage: &'static str) -> mlua::Result<i32> {
match lua.coerce_number(v)? {
Some(n) => Ok(n as i64 as i32),
Some(n) => Ok(ftol(n)),
None => Err(mlua::Error::RuntimeError(usage.into())),
}
}
@@ -218,6 +227,20 @@ mod tests {
Lua::new()
}
/// `_ftol`'s low dword: chopped toward zero, wrapped to 32 bits, and 0 for whatever the qword
/// `fistp` cannot hold.
#[test]
fn ftol_keeps_the_low_dword_of_the_x87_qword() {
assert_eq!(ftol(-2.9), -2);
assert_eq!(ftol(2.9), 2);
assert_eq!(ftol(3e9), 3_000_000_000_i64 as i32);
assert_eq!(ftol(4_294_967_301.0), 5);
assert_eq!(ftol(-9_223_372_036_854_775_808.0), 0);
for none in [f64::INFINITY, f64::NEG_INFINITY, f64::NAN, 9.3e18, -9.3e18] {
assert_eq!(ftol(none), 0, "{none}");
}
}
#[test]
fn bool_or_default_is_not_lua_truthiness() {
let lua = lua();
+11
View File
@@ -1006,6 +1006,17 @@ pub(super) fn wants_press_visual(model: &Model, h: FrameHandle, button: &str) ->
/// (`0x7826c0` passes 1, the mouse `0x779280`/`0x7793a4` pass 0): a LootButton then does nothing at
/// all, not even its `OnClick` (`0x4c182b`); every other kind ignores it.
pub(super) fn click_button(lua: &Lua, id: u32, button: &str, down: bool, scripted: bool) {
// A hyperlink button's click slot (`+0x94`, `0x7a3510`) is its text frame's
// `OnHyperlinkClick`, in place of the Button's `OnClick` firer.
let target = lua
.app_data_ref::<Model>()
.expect("model app_data")
.id_to_frame
.get(&id)
.copied();
if target.is_some_and(|h| super::hyperlink::click(lua, h, button)) {
return;
}
let mut take_loot = None;
let fire = {
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
+21 -8
View File
@@ -53,17 +53,29 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
})?,
)?;
// Three setters whose state has no reader here, so each takes its argument as the reference
// does, never raising, and stores nothing; all return zero values.
// `SetLayoutMode([mode])` (`0x488540`): a number or numeric string (`0x6f34d0`) truncates
// (`0x40a2b0`, [`super::binding_abi::ftol`]) into the UI root's `+0xcf8`, anything else
// stores 1 (`0x488573`); never raises, returns zero values. The press handler reads it
// (`0x766301`).
g.set(
"SetLayoutMode",
lua.create_function(|lua, v: Value| {
let mode = lua.coerce_number(v)?.map_or(1, super::binding_abi::ftol);
lua.app_data_mut::<Model>()
.expect("model app_data")
.layout_mode = mode;
Ok(())
})?,
)?;
// Two setters whose state has no reader here, so each takes its argument as the reference
// does, never raising, and stores nothing; both return zero values.
// - `SetEuropeanNumbers(flag)` (`0x48dec0`): `GetBoolOrDefault(1, true)` into `[0xcf4d24]`,
// which turns on `FontString:SetText`'s digit-group pass (`0x771daa` into `0x771c50`).
// benilla's `SetText` has no such pass.
// - `SetLayoutMode(mode)` (`0x488540`): a number truncates into the UI root's `+0xcf8`, anything
// else stores 1; read by the root's mouse handler (`0x766301`) for a frame-dragging mode
// benilla does not build.
// - `SetConsoleKey(key)` (`0x488640`): a one-character or named key becomes the console's
// toggle key (`0x63cb00`), a non-string the default `0x30e`. benilla has no console screen.
for name in ["SetEuropeanNumbers", "SetLayoutMode", "SetConsoleKey"] {
for name in ["SetEuropeanNumbers", "SetConsoleKey"] {
g.set(
name,
lua.create_function(|_, _: MultiValue| Ok(MultiValue::new()))?,
@@ -361,9 +373,10 @@ mod tests {
);
}
/// The three setters never raise, whatever they are handed, and return zero values.
/// The two stateless setters and `SetLayoutMode` never raise, whatever they are handed, and
/// return zero values.
#[test]
fn the_stateless_setters_take_any_argument() {
fn the_setters_take_any_argument() {
let s = UiScript::new().unwrap();
for name in ["SetEuropeanNumbers", "SetLayoutMode", "SetConsoleKey"] {
for arg in ["", "nil", "1", "\"x\"", "{}", "\"ESCAPE\""] {
+196
View File
@@ -0,0 +1,196 @@
//! A text frame's hyperlink spans as the reference builds them: each span is a frame of its own, a
//! `CSimpleHyperlinkButton` (ctor `0x7a3240`) on the Button constructor (`0x7786a0`), so a
//! mouse-enabled child of the text frame with no name and no scripts. It takes the hover, the
//! press, the capture and the click like any Button, and its vtable (`0x81d5b0`) differs from the
//! Button's (`0x81c7f8`) in three code slots, each handing its link (`+0x4dc`) and text (`+0x4e0`)
//! to the text frame: the enter (`+0x4c`, `0x7a34b0`) to `OnHyperlinkEnter`, the leave (`+0x50`,
//! `0x7a34e0`) to `OnHyperlinkLeave`, and the click (`+0x94`, `0x7a3510`) to `OnHyperlinkClick`
//! (the parent's firers `0x7a3780`/`0x7a37b0`/`0x7a37e0`). The enter and the leave pass the link
//! alone (`"%s"`, `0x82e280`); the click passes the link, the text and the button name. Its click
//! mask is `LeftButtonUp` and `RightButtonUp` (`0x500`, `0x7a327d`).
//!
//! The text frame keeps a pool (`[+0x314]`): a span takes a button from it (`0x7a3880`), which is
//! placed over the span and shown (`0x7a3350`), and a span gone hides its button again (`0x7a3920`),
//! freeing nothing. The app lays the text out, so its spans arrive here as screen rects.
use mlua::{Lua, Value};
use crate::layout::{Anchor, Anchors, Point, Rect};
use crate::widget::{FrameHandle, FrameKind, KindState};
use super::{event, visibility, Model, UiScript, SCREEN};
/// The text frame and the `(link, text)` a hyperlink button carries, if `h` is one.
pub(super) fn owner_link(model: &Model, h: FrameHandle) -> Option<(FrameHandle, String, String)> {
let f = model.arena.frame(h)?;
match &f.kind_state {
KindState::Button(bs) => {
let (link, text) = bs.hyperlink.clone()?;
Some((f.parent?, link, text))
}
_ => None,
}
}
/// Fire the text frame's `OnHyperlinkEnter` or `OnHyperlinkLeave` for the button `h`, with the link
/// alone; false when `h` is not a hyperlink button, whose own enter or leave then runs.
pub(super) fn hover(lua: &Lua, h: FrameHandle, enter: bool) -> bool {
let Some((owner, link, _)) = owner_link(&lua.app_data_ref::<Model>().expect("model"), h) else {
return false;
};
let script = if enter {
"OnHyperlinkEnter"
} else {
"OnHyperlinkLeave"
};
fire(lua, owner, script, &[&link]);
true
}
/// Fire the text frame's `OnHyperlinkClick(link, text, button)` for the button `h`; false when `h`
/// is not a hyperlink button, whose own `OnClick` then runs.
pub(super) fn click(lua: &Lua, h: FrameHandle, button: &str) -> bool {
let Some((owner, link, text)) = owner_link(&lua.app_data_ref::<Model>().expect("model"), h)
else {
return false;
};
fire(lua, owner, "OnHyperlinkClick", &[&link, &text, button]);
true
}
fn fire(lua: &Lua, owner: FrameHandle, script: &str, args: &[&str]) {
let id = {
let mut model = lua.app_data_mut::<Model>().expect("model");
if model.arena.frame(owner).is_none() {
return;
}
model.frame_id(owner)
};
let args: mlua::Result<Vec<Value>> = args
.iter()
.map(|a| lua.create_string(a).map(Value::String))
.collect();
let result = args.and_then(|args| event::fire_widget_handler(lua, id, script, args));
if let Err(e) = result {
lua.app_data_mut::<Model>()
.expect("model")
.record_lua_error(&e);
}
}
/// Replace the spans on screen, `(text frame, y-up rect, link, text)`: each text frame's spans take
/// its pooled buttons in order, a new one built when the pool runs out, and the buttons left over,
/// or of a text frame with no span now, hide.
pub(super) fn set_spans(lua: &Lua, spans: Vec<(FrameHandle, Rect, String, String)>) {
// One span: its rect, link and text.
type Span = (Rect, String, String);
let mut by_owner: Vec<(FrameHandle, Vec<Span>)> = Vec::new();
for (owner, rect, link, text) in spans {
match by_owner.iter_mut().find(|(o, _)| *o == owner) {
Some((_, list)) => list.push((rect, link, text)),
None => by_owner.push((owner, vec![(rect, link, text)])),
}
}
let (mut show, mut hide) = (Vec::new(), Vec::new());
let mut moved = false;
{
let mut model = lua.app_data_mut::<Model>().expect("model");
let model = &mut *model;
model
.link_buttons
.retain(|owner, _| model.arena.frame(*owner).is_some());
for (owner, list) in &by_owner {
if model.arena.frame(*owner).is_none() {
continue;
}
for (i, (rect, link, text)) in list.iter().enumerate() {
let pooled = model
.link_buttons
.get(owner)
.and_then(|pool| pool.get(i))
.copied();
let h = pooled.unwrap_or_else(|| build(model, *owner));
moved |= place(model, h, *rect, link, text);
if !model.arena.frame(h).is_some_and(|f| f.shown) {
show.push(h);
}
}
}
for (owner, pool) in &model.link_buttons {
let used = by_owner
.iter()
.find(|(o, _)| o == owner)
.map_or(0, |(_, list)| list.len());
hide.extend(
pool.iter()
.skip(used)
.filter(|&&h| model.arena.frame(h).is_some_and(|f| f.shown)),
);
}
}
for h in hide {
visibility::set_shown(lua, h, false);
}
for h in show {
visibility::set_shown(lua, h, true);
}
// The pointer hit-tests resolved rects, so a moved button is solved before the next input.
if moved {
UiScript::resolve_layout(&mut lua.app_data_mut::<Model>().expect("model"));
}
}
/// `0x7a3880`'s miss: a new button, a Button child of the text frame in its stratum one level up,
/// as a child is made, registered for `LeftButtonUp` and `RightButtonUp`. It is born hidden, so the
/// caller's show is its one transition, as `0x7a3350`'s `Show` is.
fn build(model: &mut Model, owner: FrameHandle) -> FrameHandle {
let h = model.arena.create(FrameKind::Button, None, Some(owner));
if let Some((strata, level)) = model.arena.frame(owner).map(|f| (f.strata, f.level)) {
model.arena.set_frame_strata(h, strata);
model.arena.set_frame_level(h, level + 1, false);
}
model.arena.set_shown(h, false);
if let Some(KindState::Button(bs)) = model.arena.frame_mut(h).map(|f| &mut f.kind_state) {
bs.registered_clicks = ["LeftButtonUp", "RightButtonUp"]
.into_iter()
.map(String::from)
.collect();
}
model.frame_id(h);
model.layout_inputs.entry(h).or_default();
model.link_buttons.entry(owner).or_default().push(h);
h
}
/// `0x7a3350`: store the link and text, and seat the button over the span, one TOPLEFT on the
/// screen with an explicit size, in its own units. True when its anchors or size changed.
fn place(model: &mut Model, h: FrameHandle, rect: Rect, link: &str, text: &str) -> bool {
if let Some(KindState::Button(bs)) = model.arena.frame_mut(h).map(|f| &mut f.kind_state) {
if bs
.hyperlink
.as_ref()
.is_none_or(|(l, t)| l != link || t != text)
{
bs.hyperlink = Some((link.to_string(), text.to_string()));
}
}
let scale = f64::from(super::object::eff_scale(model, h));
let root = model.screen;
let x = ((f64::from(rect.left) - f64::from(root.left)) / scale) as f32;
let y = ((f64::from(rect.top) - f64::from(root.top)) / scale) as f32;
let w = ((f64::from(rect.right) - f64::from(rect.left)) / scale) as f32;
let ht = ((f64::from(rect.top) - f64::from(rect.bottom)) / scale) as f32;
let pin = Anchors::from([Anchor::new(Point::TopLeft, SCREEN, Point::TopLeft, x, y)]);
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();
if changed {
input.anchors = pin;
input.width = w;
input.height = ht;
model.touch_layout_frame(h);
model.note_authored_size(h);
}
changed
}
+9 -13
View File
@@ -68,6 +68,7 @@ mod gm_ticket;
mod gossip;
mod guild;
mod handler_prof;
mod hyperlink;
mod screenshot;
mod tabard;
pub use handler_prof::HandlerRow;
@@ -409,7 +410,7 @@ pub const SCREEN: crate::layout::Handle = 0;
/// ScrollFrame's three scroll kinds are `+0x32c`/`+0x334`/`+0x33c` (script-name map `0x786c40`).
/// The EditBox's vtable (`0x81c910`) replaces the key and char slots: an EditBox never fires
/// `OnKeyDown`, and fires `OnChar` only from `Insert`, with the inserted text (`0x77c13c`).
const SCRIPT_KINDS: [&str; 39] = [
const SCRIPT_KINDS: [&str; 41] = [
"OnLoad",
"OnEvent",
"OnUpdate",
@@ -445,9 +446,12 @@ const SCRIPT_KINDS: [&str; 39] = [
"OnDragStart",
"OnDragStop",
"OnReceiveDrag",
// A release over a message-frame hyperlink span, `OnHyperlinkClick(link, text, button)`;
// `ChatFrameTemplate` passes it to `SetItemRef` (`ChatFrame.xml:15`, `ChatFrame.lua:1534`).
// A text frame's hyperlink button hands it these ([`hyperlink`]): the click as
// `OnHyperlinkClick(link, text, button)`, which `ChatFrameTemplate` passes to `SetItemRef`
// (`ChatFrame.xml:15`, `ChatFrame.lua:1534`), and the enter and leave with the link alone.
"OnHyperlinkClick",
"OnHyperlinkEnter",
"OnHyperlinkLeave",
// The GameTooltip's engine-fired scripts: money render, money clear and world-hover default
// placement, all three wired by the stock template (`GameTooltipTemplate.xml:617-625`).
"OnTooltipAddMoney",
@@ -860,17 +864,9 @@ impl UiScript {
}
/// Replace the hyperlink spans `(frame, y-up rect, link, markup)` the app rasterized this
/// frame; a release inside one fires `OnHyperlinkClick` on its frame ([`pointer`]).
/// frame: each is a hyperlink button over its span, a child of its frame ([`hyperlink`]).
pub fn set_link_spans(&mut self, spans: Vec<(FrameHandle, Rect, String, String)>) {
let mut model = self.model_mut();
model.link_spans.clear();
for (fh, rect, link, markup) in spans {
model
.link_spans
.entry(fh)
.or_default()
.push((rect, link, markup));
}
hyperlink::set_spans(&self.lua, spans);
}
/// Whether the frame with this global name is shown with every ancestor (`IsVisible()`).
+8 -3
View File
@@ -171,8 +171,9 @@ pub(crate) struct Model {
pub(crate) layout_last_scope: (usize, usize),
/// Fixpoint rounds run across all solves.
pub(crate) layout_rounds: u64,
/// Per frame, rasterized links `(y-up rect, link, full |H…|h markup)` for `OnHyperlinkClick`.
pub(crate) link_spans: HashMap<FrameHandle, Vec<(Rect, String, String)>>,
/// Per text frame, its pool of hyperlink buttons (`[+0x314]`), the shown ones first
/// ([`super::hyperlink`]).
pub(crate) link_buttons: HashMap<FrameHandle, Vec<FrameHandle>>,
/// Region visuals (texture, colour, text) and layout (anchors, size, justify).
pub(crate) region_data: HashMap<RegionHandle, RegionData>,
/// Each frame's backdrop plate (`<Backdrop>` or `SetBackdrop`, the client's `frame+0x1ac`):
@@ -643,6 +644,9 @@ pub(crate) struct Model {
/// 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>,
/// `SetLayoutMode`'s value, the root's `+0xcf8` (0 at start, `0x7642a5`): while non-zero, a
/// press with Control or Alt held is a layout drag (`0x766301`).
pub(crate) layout_mode: i32,
/// 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.
@@ -1131,7 +1135,7 @@ impl Model {
layout_gate_walks: 0,
layout_rounds: 0,
resolved: HashMap::new(),
link_spans: HashMap::new(),
link_buttons: HashMap::new(),
region_data: HashMap::new(),
backdrops: HashMap::new(),
simple_html: simplehtml::SimpleHtmlStates::new(),
@@ -1337,6 +1341,7 @@ impl Model {
drag_registered: HashMap::new(),
drag: None,
moving: None,
layout_mode: 0,
slider_drag: None,
color_drag: None,
gossip: None,
@@ -41,6 +41,11 @@ fn leave_class(model: &Model, h: FrameHandle) -> LeaveClass {
/// 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>) {
// A hyperlink button's leave is its text frame's `OnHyperlinkLeave` (`0x7a34e0`), in place of
// the Button's.
if super::hyperlink::hover(lua, h, false) {
return;
}
let class = leave_class(&model(lua), h);
match class {
// `0x7cb8bf`: `0x492890` with an empty mouseover and an empty "previous", first.
@@ -211,9 +211,9 @@ pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
}
/// `SetScript(name, func)`: stores the closure under one of [`SCRIPT_KINDS`], else raises.
/// Deviation: real 1.12 kinds nothing fires yet raise too (`OnHyperlinkEnter`, `OnHyperlinkLeave`,
/// `OnMessageScrollChanged`, `OnInputLanguageChanged`, the movie frame's), because an accepted
/// handler that never runs fails silently. `OnAttributeChanged` is 2.0's, with no 1.12 slot.
/// Deviation: real 1.12 kinds nothing fires yet raise too (`OnMessageScrollChanged`,
/// `OnInputLanguageChanged`, the movie frame's), because an accepted handler that never runs fails
/// silently. `OnAttributeChanged` is 2.0's, with no 1.12 slot.
fn set_script(lua: &Lua, this: &Table, name: &str, func: Option<Function>) -> mlua::Result<()> {
let kind = SCRIPT_KINDS
.iter()
+64 -4
View File
@@ -270,17 +270,77 @@ fn start_moving(model: &mut Model, h: FrameHandle) -> mlua::Result<()> {
/// Begin a title-region move, drag mode 2 (`0x7662c0` → `0x765320` → `0x7652b0`), with no movable
/// 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.
/// sizing (`0x765332` → `0x765464`). Neither reads the slot first, so a drag in flight is displaced
/// and nothing runs on its frame. False when the frame has no rect, so nothing started.
pub(crate) fn start_title_move(model: &mut Model, h: FrameHandle) -> bool {
if model.moving.is_some() {
begin_drag(model, h, Point::Center, true)
}
/// Begin the layout-mode modifier drag on the toplevel window `h` that `0x765490`'s walk lifted,
/// from its flag test on: Control held sizes and needs the window resizable (`0x765570`), else it
/// moves and needs it movable (`0x765560`). Then `0x765320` with drag mode 1, which the release
/// ends: grip CENTER for a move, the cell of `grid_grip` for a size. False when nothing started.
pub(crate) fn start_layout_drag(
model: &mut Model,
h: FrameHandle,
at: (f32, f32),
sizing: bool,
) -> bool {
let allowed = model
.arena
.frame(h)
.is_some_and(|f| if sizing { f.resizable } else { f.movable });
if !allowed {
return false;
}
begin_drag(model, h, Point::Center, true)
let grip = if sizing {
grid_grip(model, h, at)
} else {
Some(Point::Center)
};
grip.is_some_and(|grip| begin_drag(model, h, grip, true))
}
/// `0x765320`'s sizing grip: with the layout resolved (`0x76533e` → `0x7680e0`), the cell of a 3×3
/// grid of the frame's rect (`0x768320`) that `at` falls in, each axis cut at a quarter and three
/// quarters (`0.25` at `0x8029b0`, `3.0` at `0x80297c`), a cut belonging to the cell past it; the
/// middle cell is CENTER, a move (`0x765384`–`0x765464`). The x offset and the quarter-width stay
/// in x87 registers, the y offset and the quarter-height are stored as floats (`0x765390`,
/// `0x7653ab`). None when the frame has no rect (`0x765379`).
fn grid_grip(model: &mut Model, h: FrameHandle, (x, y): (f32, f32)) -> Option<Point> {
UiScript::resolve_layout(model);
let r = *model.resolved.get(&h)?;
let dx = f64::from(x) - f64::from(r.left);
let quarter_w = (f64::from(r.right) - f64::from(r.left)) * 0.25;
let dy = f64::from((f64::from(y) - f64::from(r.bottom)) as f32);
let quarter_h = f64::from(((f64::from(r.top) - f64::from(r.bottom)) * 0.25) as f32);
// 0 the near cell (left, bottom), 1 the middle, 2 the far.
let cell = |d: f64, quarter: f64| {
if d < quarter {
0
} else if d >= quarter * 3.0 {
2
} else {
1
}
};
Some(match (cell(dx, quarter_w), cell(dy, quarter_h)) {
(0, 2) => Point::TopLeft,
(1, 2) => Point::Top,
(2, 2) => Point::TopRight,
(0, 1) => Point::Left,
(2, 1) => Point::Right,
(0, 0) => Point::BottomLeft,
(1, 0) => Point::Bottom,
(2, 0) => Point::BottomRight,
_ => Point::Center,
})
}
/// 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.
/// (`0x7652f3`–`0x76530f`) over whatever held it. 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;
@@ -97,8 +97,12 @@ pub(in crate::script) fn raise_on_show(model: &mut Model, h: FrameHandle) {
}
}
/// Step 1: the first frame up `+0x9c` with the toplevel bit (`0x7650fb..0x765135`).
fn nearest_toplevel(model: &Model, frame: FrameHandle) -> Option<FrameHandle> {
/// Step 1: the first frame up `+0x9c` with the toplevel bit (`0x7650fb..0x765135`), the walk the
/// layout drag's lift repeats (`0x766fe0`).
pub(in crate::script) fn nearest_toplevel(
model: &Model,
frame: FrameHandle,
) -> Option<FrameHandle> {
// A chain longer than the frame count is a cycle, which `SetParent` already refuses.
let bound = model.arena.iter_frames().count();
let mut cur = Some(frame);
+100 -75
View File
@@ -2,10 +2,11 @@
//! turn host pointer events into FrameScript handler calls. [`cursor`] owns the payload and
//! gesture state; this file dispatches.
//!
//! A frame captures the cursor when it is mouse-enabled and effective-visible and its rect and
//! effective ScrollFrame clip hold the point (the reference's probe `0x76b020`), taking the first
//! in the order of the hover walk (`0x7661cd` in `0x7660d0`), which [`crate::order::hit_test`]
//! implements.
//! A frame captures the cursor when it is in the mouse index, which `0x76af00` enters it into
//! while it has the mouse bit (`[+0xcc] & 4`) and is visible (`[+0xd4]`), and the probe `0x76b020`
//! takes the point: its rect is valid, and its hit rect and effective ScrollFrame clip hold the
//! point. The first such frame in the order of the hover walk (`0x7661cd` in `0x7660d0`) wins,
//! which [`crate::order::hit_test`] implements.
use std::collections::HashMap;
@@ -58,6 +59,19 @@ pub(super) fn install(lua: &mlua::Lua) -> mlua::Result<()> {
Ok(())
}
/// The frame a release of `button` goes to: the release handler reads only the capture
/// (`0x76642b`), which `mouse_down_on` keeps per button as the press that set it, `None` once that
/// frame hid. A button with no press of its own, one a title or layout press took, releases onto
/// whatever capture is held.
fn release_target(model: &Model, button: &str) -> Option<FrameHandle> {
match model.mouse_down_on.get(button) {
Some(&pressed) => pressed,
None => model
.mouse_capture
.filter(|&c| model.arena.frame(c).is_some()),
}
}
/// Whether `h` is the world frame, whose mouse hits belong to the 3D world; it wins the hover walk
/// only when nothing else mouse-enabled is under the cursor.
fn is_world_handle(model: &Model, h: FrameHandle) -> bool {
@@ -81,15 +95,6 @@ fn over_world(model: &Model, target: Option<FrameHandle>) -> bool {
target.is_none_or(|h| is_world_handle(model, h))
}
/// Whether `(x, y)` falls inside one of this frame's hyperlink spans, the engine half of the
/// reference's per-span `CSimpleHyperlinkButton` children.
fn link_span_hit(model: &Model, fh: FrameHandle, x: f32, y: f32) -> bool {
model
.link_spans
.get(&fh)
.is_some_and(|spans| spans.iter().any(|(r, _, _)| point_in_rect(*r, x, y)))
}
impl UiScript {
/// Whether a frame id names the world frame: hit-tested like any frame, so scripts on it fire,
/// but not counted by the app as over the UI.
@@ -125,11 +130,10 @@ impl UiScript {
}),
);
let hit = order::hit_test(&sorted, |fh| {
// Mouse-enabled, or over one of the frame's hyperlink spans. The chat frame takes no
// mouse (`[+0xcc] = 0`); the reference gives each span a mouse-enabled
// `CSimpleHyperlinkButton` child (`0x7a3240`) that routes its scripts to the parent, so
// a chat window hits only over a link and drags by its tab, not its body.
let indexed = model.arena.is_mouse_enabled(fh) || link_span_hit(&model, fh, x, y);
// In the mouse index. The chat frame takes no mouse (`[+0xcc] = 0`): its links are
// Button children of their own ([`super::hyperlink`]), so a chat window hits only over
// a link and drags by its tab, not its body.
let indexed = model.arena.is_mouse_enabled(fh);
// The nameplate's `+0x3c` veto (`0x7cba30`), not its mouse-enabled bit: while a
// ground-targeted spell is armed a plate refuses the hit before its rect is tested,
// so the reticle is placed through it.
@@ -274,9 +278,13 @@ impl UiScript {
super::mouseover::leave(&self.lua, h, script);
}
if let Some(nid) = enter_id {
// 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);
// A hyperlink button's enter is its text frame's `OnHyperlinkEnter` (`0x7a34b0`); the
// base enter's plain runner (`0x76b6e5` → `0x702690`) sets no arg global.
let routed = new_handle.is_some_and(|h| super::hyperlink::hover(&self.lua, h, true));
if !routed {
if let Err(e) = event::fire_widget_handler(&self.lua, nid, "OnEnter", Vec::new()) {
self.push_error(e);
}
}
}
new_id
@@ -294,12 +302,55 @@ impl UiScript {
.is_some_and(|r| point_in_rect(*r, x, y))
}
/// `0x765490`'s walk: strata FULLSCREEN_DIALOG down to WORLD, never TOOLTIP (the loop starts
/// below `root+0xcf4`, `0x7654a3`), each level high to low, and each level's list from its head
/// (the terminator's `next`, `[level+0x14]`) along each link's `next` (`+4`, `0x766aa0`). A
/// frame links at the tail (`0x7648c9` → `0x7668c0` → `0x766921`), so that is the oldest link
/// first, the hover sweep's order. Every shown frame is probed with `0x76b020` alone, whatever
/// its mouse bit. The first hit is lifted to its nearest toplevel self-or-ancestor
/// (`0x766fe0`); one with none gives up the rest of its stratum and the walk goes on in the
/// next below (`0x76554a` → `0x765532`).
fn layout_drag_target(&self, x: f32, y: f32) -> Option<FrameHandle> {
let model = self.model_ref();
let sorted = order::traversal(&model.arena);
let scroll_sources = scroll_clip_sources(&model);
let mut below = order::Strata::Tooltip;
loop {
let hit = order::hit_test(&sorted, |fh| {
model.arena.frame(fh).is_some_and(|f| f.strata < below)
&& frame_contains(&model, &scroll_sources, fh, x, y)
})?;
if let Some(t) = super::object::toplevel::nearest_toplevel(&model, hit) {
return Some(t);
}
below = model.arena.frame(hit)?.strata;
}
}
/// A mouse button press or release at `(x, y)`; `button` is the WoW name (`"LeftButton"`, …).
/// `OnClick` follows `RegisterForClicks` ([`button::wants_click`], default `{"LeftButtonUp"}`):
/// a press fires it for `"<Button>ButtonDown"`, a release on the pressed frame for
/// `"<Button>ButtonUp"`. Its `down` argument is not 1.12, whose firer (`0x779540`) passes only
/// the button name. Returns whether the UI consumed the event.
pub fn mouse_button(&mut self, x: f32, y: f32, button: &str, down: bool) -> bool {
// ── The layout drag, ahead of everything ─────────────────────────────────────────────────
//
// With `SetLayoutMode` on, a press with Control (`0x41f8f0(1)`) or Alt (`0x41f8f0(2)`) held
// goes to `0x765490` as drag mode 1, Control asking for a size, and the handler returns
// consumed whatever that started (`0x766301`–`0x76635b`): no raise, title-region test,
// capture or press slot.
if down {
let (_, ctrl, alt) = self.model_ref().modifiers;
if self.model_ref().layout_mode != 0 && (ctrl || alt) {
let target = self.layout_drag_target(x, y);
let mut model = self.model_mut();
model.cursor_pos = (x, y);
if let Some(h) = target {
crate::script::object::movable::start_layout_drag(&mut model, h, (x, y), ctrl);
}
return true;
}
}
let hit_id = self.hit_test(x, y);
// ── The press target: the capture, else the hover ──────────────────────────────────────
//
@@ -309,6 +360,11 @@ impl UiScript {
// drag arm, a Button's press art and Down click), runs on that one frame. So a second
// button pressed over another frame fires on the held one. `mouse_down_on` keeps the
// frame per button for the release.
//
// The hover is what the last walk left. Every press follows a move to its point, so that
// is the frame under it, except while a move or size holds the slot: the walk then pumps
// it and returns before its sweep (`0x766164`), and the hover stays the frame it was when
// the drag began.
let mut release_lost = false;
let press_target: Option<crate::widget::FrameHandle> = if down {
let mut model = self.model_mut();
@@ -328,7 +384,12 @@ impl UiScript {
let held = model
.mouse_capture
.filter(|&c| model.arena.frame(c).is_some());
held.or_else(|| hit_id.and_then(|id| model.id_to_frame.get(&id).copied()))
let hover = if model.moving.is_some() {
model.mouseover.filter(|&h| model.arena.frame(h).is_some())
} else {
hit_id.and_then(|id| model.id_to_frame.get(&id).copied())
};
held.or(hover)
} else {
None
};
@@ -347,9 +408,9 @@ impl UiScript {
// ── The title region swallows the press ─────────────────────────────────────────────────
//
// A press inside the target's title region starts a mode-2 move and ends there:
// `0x7662c0` tests the region first and only a miss reaches `0x7663e6`, the capture and
// `OnMouseDown`. So no `OnMouseDown`, drag arm, `OnClick` or double-click stamp, and the
// event is consumed.
// `0x7662c0` tests the region before the capture (`0x766397`–`0x7663e5`) and only a miss
// reaches `0x7663e6`, the capture and `OnMouseDown`. So no `OnMouseDown`, drag arm,
// `OnClick` or double-click stamp, and the event is consumed.
if let Some(h) = press_target {
if self.title_region_hit(h, x, y) {
self.model_mut().cursor_pos = (x, y);
@@ -357,11 +418,14 @@ impl UiScript {
return true;
}
}
// ── The release ends a title move ───────────────────────────────────────────────────────
// ── The release ends a title move, and that is all it does ─────────────────────────────
//
// `0x766420` cancels move modes 1 (modifier drag) and 2 (title region) on release and
// leaves mode 3 (`StartMoving`) to the addon's `StopMovingOrSizing`;
// `FrameMove::auto_stop` is that distinction.
// `0x766420` tests the drag mode before anything else (`0x76646d`–`0x76647c`): any release
// during mode 1 (modifier drag) or 2 (title region) clears the slot (`0x765640`) and
// returns consumed, with no `OnMouseUp`, drag stop or click on the capture, which it
// leaves held for a later release. Mode 3 (`StartMoving`) is left to the addon's
// `StopMovingOrSizing`; `FrameMove::auto_stop` is that distinction. The button is up all
// the same, so its own press entry goes.
if !down
&& self
.model_ref()
@@ -369,19 +433,17 @@ impl UiScript {
.as_ref()
.is_some_and(|m| m.auto_stop)
{
self.model_mut().moving = None;
let mut model = self.model_mut();
model.moving = None;
model.mouse_down_on.remove(button);
return true;
}
// The press this release belongs to, read before the borrow below drains `mouse_down_on`.
// The frame this release goes to, read before the borrow below drains `mouse_down_on`.
let captured_id: Option<u32> = if down {
None
} else {
let model = self.model_ref();
model
.mouse_down_on
.get(button)
.copied()
.flatten()
.and_then(|h| model.frame_to_id.get(&h).copied())
release_target(&model, button).and_then(|h| model.frame_to_id.get(&h).copied())
};
// The `GetTime()` clock for the double-click detector, read before the borrow.
let now = self.now();
@@ -456,8 +518,8 @@ impl UiScript {
.and(press_id);
(click, None, false, jump, None, abandoned, color_jump)
} else {
let pressed = release_target(&model, button);
let entry = model.mouse_down_on.remove(button);
let pressed = entry.flatten();
// The pressed frame hid (`None` entry): the capture is gone, so nothing of this
// release runs, the world click included (`over_world(None)` would take it).
let capture_lost = entry == Some(None);
@@ -539,43 +601,6 @@ impl UiScript {
self.push_error(e);
}
}
// A release in a hyperlink span fires `OnHyperlinkClick(link, markup, button)` on the
// owning frame, except after a started drag.
if !down && !drag_release.as_ref().is_some_and(|r| r.started) {
if let Some(id) = hit_id {
let span = {
let model = self.model_ref();
model
.id_to_frame
.get(&id)
.and_then(|fh| model.link_spans.get(fh))
.and_then(|spans| {
spans
.iter()
.find(|(r, _, _)| point_in_rect(*r, x, y))
.map(|(_, l, m)| (l.clone(), m.clone()))
})
};
if let Some((link, markup)) = span {
let args = match (
self.lua.create_string(&link),
self.lua.create_string(&markup),
) {
(Ok(l), Ok(m)) => {
vec![Value::String(l), Value::String(m), btn.clone()]
}
_ => Vec::new(),
};
if !args.is_empty() {
if let Err(e) =
event::fire_widget_handler(&self.lua, id, "OnHyperlinkClick", args)
{
self.push_error(e);
}
}
}
}
}
// A left press in an EditBox places the cursor, starts a selection drag and focuses it
// whatever its `autoFocus`, before any `OnClick` (`0x77b800`).
if button == "LeftButton" {
@@ -0,0 +1,142 @@
//! A text frame's hyperlink span is a frame of its own, a `CSimpleHyperlinkButton` (`0x7a3240`):
//! a mouse-enabled Button child that takes the hover, the press and the click, and hands its
//! enter, leave and click to the text frame's `OnHyperlinkEnter`/`OnHyperlinkLeave`/
//! `OnHyperlinkClick`.
use super::common::script;
use crate::layout::Rect;
use crate::script::UiScript;
const LINK: &str = "item:7073";
const MARKUP: &str = "|Hitem:7073|h[Broken Fang]|h";
/// A chat-like `CF` as `FloatingChatFrameTemplate` builds it (movable, toplevel, a full-frame title
/// region, no mouse) at (100, 100) 430×120 on 800×600, with one link span over [110..160]×[120..134].
fn chat() -> UiScript {
let mut s = script();
s.set_screen_size(800.0, 600.0);
s.run(
r##"
log = {}
CF = CreateFrame("ScrollingMessageFrame", "CF")
CF:SetToplevel(true); CF:SetMovable(true)
CF:SetPoint("BOTTOMLEFT", 100, 100); CF:SetWidth(430); CF:SetHeight(120)
CF:CreateTitleRegion():SetAllPoints(CF)
CF:SetScript("OnHyperlinkClick", function()
table.insert(log, "click:"..arg1.."#"..arg2.."#"..arg3)
end)
"##,
)
.unwrap();
s.resolve();
set_link(&mut s);
s
}
fn set_link(s: &mut UiScript) {
let cf = s.model_ref().arena.lookup("CF").expect("CF");
s.set_link_spans(vec![(
cf,
Rect::new(120.0, 110.0, 134.0, 160.0),
LINK.to_string(),
MARKUP.to_string(),
)]);
}
fn log_of(s: &UiScript) -> String {
s.eval::<String>("return table.concat(log, ' ')").unwrap()
}
fn click(s: &mut UiScript, (x, y): (f32, f32), button: &str) {
s.mouse_move(x, y);
s.mouse_button(x, y, button, true);
s.mouse_button(x, y, button, false);
}
/// The press lands on the link's button, whose title-region test finds none, so the chat window
/// is neither pinned nor user-placed; the button's click (`0x7a3510`) fires the window's
/// `OnHyperlinkClick(link, text, button)` (`0x7a37e0`), for either button (`0x500`, `0x7a327d`).
#[test]
fn a_link_click_fires_on_hyperlink_click_and_leaves_the_window_alone() {
let mut s = chat();
click(&mut s, (130.0, 127.0), "LeftButton");
click(&mut s, (130.0, 127.0), "RightButton");
assert_eq!(
log_of(&s),
format!("click:{LINK}#{MARKUP}#LeftButton click:{LINK}#{MARKUP}#RightButton")
);
assert!(!s.eval::<bool>("return CF:IsUserPlaced()").unwrap());
let (point, x, y) = s
.eval::<(String, f64, f64)>("local p, _, _, x, y = CF:GetPoint() return p, x, y")
.unwrap();
assert_eq!(
(point.as_str(), x, y),
("BOTTOMLEFT", 100.0, 100.0),
"never pinned"
);
assert!(s.errors().is_empty(), "{:?}", s.errors());
}
/// The hover over a link is the link's button, a child of the window, not the window.
#[test]
fn the_mouse_focus_over_a_link_is_its_button() {
let mut s = chat();
s.mouse_move(130.0, 127.0);
assert!(s
.eval::<bool>(
"local f = GetMouseFocus() return f ~= nil and f ~= CF and f:GetParent() == CF"
)
.unwrap());
assert!(s
.eval::<bool>("return GetMouseFocus():GetObjectType() == 'Button'")
.unwrap());
s.mouse_move(300.0, 150.0);
assert!(
s.eval::<bool>("return GetMouseFocus() == nil").unwrap(),
"the body takes no mouse"
);
}
/// The button's enter and leave (`0x7a34b0`/`0x7a34e0`) fire the window's `OnHyperlinkEnter` and
/// `OnHyperlinkLeave` with the link alone (`"%s"`, `0x7a37a0`); a span that goes away while hovered
/// hides its button, whose removal leaves.
#[test]
fn hovering_a_link_fires_on_hyperlink_enter_and_leave() {
let mut s = chat();
s.run(
r#"
CF:SetScript("OnHyperlinkEnter", function() table.insert(log, "enter:"..arg1) end)
CF:SetScript("OnHyperlinkLeave", function() table.insert(log, "leave:"..arg1) end)
"#,
)
.unwrap();
s.mouse_move(130.0, 127.0);
s.mouse_move(300.0, 150.0);
s.mouse_move(130.0, 127.0);
s.set_link_spans(Vec::new());
assert_eq!(
log_of(&s),
format!("enter:{LINK} leave:{LINK} enter:{LINK} leave:{LINK}")
);
assert!(s.errors().is_empty(), "{:?}", s.errors());
}
/// With the body made hittable, a press on it outside a link title-drags the window, and a press
/// on the link, a child a level above it, still clicks the link.
#[test]
fn a_hittable_body_title_drags_but_its_link_still_clicks() {
let mut s = chat();
s.run("CF:EnableMouse(true)").unwrap();
s.resolve();
click(&mut s, (130.0, 127.0), "LeftButton");
assert_eq!(log_of(&s), format!("click:{LINK}#{MARKUP}#LeftButton"));
assert!(!s.eval::<bool>("return CF:IsUserPlaced()").unwrap());
s.mouse_move(300.0, 150.0);
s.mouse_button(300.0, 150.0, "LeftButton", true);
s.mouse_move(350.0, 170.0);
s.mouse_button(350.0, 170.0, "LeftButton", false);
s.resolve();
assert_eq!(s.eval::<f64>("return CF:GetLeft()").unwrap(), 150.0);
assert!(s.eval::<bool>("return CF:IsUserPlaced()").unwrap());
assert!(s.errors().is_empty(), "{:?}", s.errors());
}
+5 -8
View File
@@ -590,7 +590,6 @@ fn the_unfired_script_kinds_still_raise_rather_than_silently_accepting() {
// A 2.0 secure-frame slot; no 1.12 resolver has it.
"OnAttributeChanged",
// Real 1.12 slots that benilla does not fire.
"OnHyperlinkEnter",
"OnMessageScrollChanged",
"OnInputLanguageChanged",
] {
@@ -846,18 +845,16 @@ fn hiding_another_frame_keeps_the_press() {
assert_eq!(log_of(&mut s), "A:down A:up");
}
/// `Model::mouse_down_on` is keyed by button name, so the capture is per button.
/// The release handler reads only the capture (`0x76642b`) and its mouse-up (`0x76c040`) fires
/// `OnMouseUp` with no test of which button pressed, so a release of a button never pressed goes
/// to the frame another button holds.
#[test]
fn the_capture_is_per_button() {
fn a_release_goes_to_the_capture_whatever_its_button() {
let mut s = two_frames();
s.mouse_button(200.0, 300.0, "LeftButton", true); // A holds the left button
s.mouse_move(500.0, 300.0);
s.mouse_button(500.0, 300.0, "RightButton", false); // a right release, never pressed
assert_eq!(
log_of(&mut s),
"A:down",
"the right button captured nothing, so its release dispatches nothing"
);
assert_eq!(log_of(&mut s), "A:down A:up");
}
/// A disabled Button fires neither `OnEnter` nor `OnLeave`: `CSimpleButton::OnEnter` (`0x779490`)
@@ -0,0 +1,271 @@
//! `SetLayoutMode` and the modifier drag it arms: while the UI root's mode (`+0xcf8`) is non-zero,
//! a press with Control or Alt held (`0x766301`–`0x76634e`) moves or sizes the toplevel window
//! under the cursor (`0x765490`) and reaches nothing else.
use super::common::script;
use crate::script::UiScript;
/// A toplevel, movable and resizable `Win` at (100, 100) 200×100 with a child `Kid` over its
/// middle, on 800×600, every press and release logged.
fn window() -> UiScript {
let mut s = script();
s.set_screen_size(800.0, 600.0);
s.run(
r#"
log = {}
local function note(f)
f:SetScript("OnMouseDown", function() table.insert(log, f:GetName()..":down") end)
f:SetScript("OnMouseUp", function() table.insert(log, f:GetName()..":up") end)
end
Win = CreateFrame("Frame", "Win")
Win:SetToplevel(true); Win:SetMovable(true); Win:SetResizable(true)
Win:SetPoint("BOTTOMLEFT", 100, 100); Win:SetWidth(200); Win:SetHeight(100)
Win:EnableMouse(true)
note(Win)
Kid = CreateFrame("Button", "Kid", Win)
Kid:SetPoint("CENTER", Win, "CENTER", 0, 0); Kid:SetWidth(40); Kid:SetHeight(20)
Kid:EnableMouse(true)
note(Kid)
"#,
)
.unwrap();
s.resolve();
s
}
/// `(left, bottom, width, height)` of a frame, after a resolve.
fn rect(s: &mut UiScript, name: &str) -> (f64, f64, f64, f64) {
s.resolve();
s.eval::<(f64, f64, f64, f64)>(&format!(
"local f = getglobal('{name}') return f:GetLeft(), f:GetBottom(), f:GetWidth(), f:GetHeight()"
))
.unwrap()
}
fn log_of(s: &UiScript) -> String {
s.eval::<String>("return table.concat(log, ' ')").unwrap()
}
/// A press and a move by `(dx, dy)`, then the release, with `(ctrl, alt)` held throughout.
fn modifier_drag(
s: &mut UiScript,
(x, y): (f32, f32),
(dx, dy): (f32, f32),
(ctrl, alt): (bool, bool),
) {
s.set_modifiers(false, ctrl, alt);
s.mouse_move(x, y);
s.mouse_button(x, y, "LeftButton", true);
s.mouse_move(x + dx, y + dy);
s.mouse_button(x + dx, y + dy, "LeftButton", false);
s.set_modifiers(false, false, false);
}
/// Alt over the child moves its toplevel window (`0x766fe0` lifts the hit), marks it user-placed
/// (`0x7652e5`), and the press reaches no script: the branch returns before the capture and the
/// press slot. The release ends the move (`0x766420`, mode 1) and reaches no script either.
#[test]
fn an_alt_press_in_layout_mode_moves_the_toplevel_window() {
let mut s = window();
s.run("SetLayoutMode(1)").unwrap();
assert!(!s.eval::<bool>("return Win:IsUserPlaced()").unwrap());
modifier_drag(&mut s, (200.0, 150.0), (60.0, 20.0), (false, true));
assert_eq!(rect(&mut s, "Win"), (160.0, 120.0, 200.0, 100.0));
assert!(s.eval::<bool>("return Win:IsUserPlaced()").unwrap());
s.mouse_move(400.0, 400.0);
assert_eq!(
rect(&mut s, "Win"),
(160.0, 120.0, 200.0, 100.0),
"the release ended it"
);
assert_eq!(
log_of(&s),
"",
"neither the press nor the release reached a script"
);
assert!(s.errors().is_empty(), "{:?}", s.errors());
}
/// Control sizes from the cell of a 3×3 grid the press falls in, the rect cut at a quarter and
/// three quarters per axis (`0x765384`–`0x765464`); the middle cell moves. A cut belongs to the
/// cell past it: x at exactly a quarter is the middle column.
#[test]
fn a_control_press_sizes_from_the_grid_cell_and_the_middle_moves() {
let mut s = window();
s.run("SetLayoutMode(1)").unwrap();
// Top-right cell: right of 250, above 175.
modifier_drag(&mut s, (290.0, 195.0), (20.0, 10.0), (true, false));
assert_eq!(
rect(&mut s, "Win"),
(100.0, 100.0, 220.0, 110.0),
"TOPRIGHT"
);
// Now 220×110 from (100, 100): the left column is x < 155, the middle row 127.5..182.5.
modifier_drag(&mut s, (154.0, 150.0), (-10.0, 0.0), (true, false));
assert_eq!(rect(&mut s, "Win"), (90.0, 100.0, 230.0, 110.0), "LEFT");
// Now 230×110 from (90, 100): x 147.5 is exactly a quarter in.
modifier_drag(&mut s, (147.5, 150.0), (10.0, 10.0), (true, false));
assert_eq!(
rect(&mut s, "Win"),
(100.0, 110.0, 230.0, 110.0),
"the middle cell moves"
);
assert_eq!(log_of(&s), "");
assert!(s.errors().is_empty(), "{:?}", s.errors());
}
/// The window must be movable for Alt and resizable for Control (`0x765553`–`0x765573`); a press
/// that starts nothing is consumed all the same (`0x766353`).
#[test]
fn a_window_without_the_flag_does_not_move_and_still_eats_the_press() {
let mut s = window();
s.run("SetLayoutMode(1) Win:SetResizable(false)").unwrap();
modifier_drag(&mut s, (290.0, 195.0), (20.0, 10.0), (true, false));
assert_eq!(
rect(&mut s, "Win"),
(100.0, 100.0, 200.0, 100.0),
"Control needs resizable"
);
s.run("Win:SetResizable(true) Win:SetMovable(false)")
.unwrap();
modifier_drag(&mut s, (200.0, 150.0), (60.0, 20.0), (false, true));
assert_eq!(
rect(&mut s, "Win"),
(100.0, 100.0, 200.0, 100.0),
"Alt needs movable"
);
assert!(!s.eval::<bool>("return Win:IsUserPlaced()").unwrap());
assert_eq!(log_of(&s), "", "both presses were consumed");
assert!(s.errors().is_empty(), "{:?}", s.errors());
}
/// With the mode 0, or without Control or Alt, a press takes the ordinary path. The mode starts at
/// 0 (`0x7642a5`), and `SetLayoutMode` truncates a number (`0x40a2b0`) and stores 1 for anything
/// else (`0x488573`).
#[test]
fn mode_zero_or_no_modifier_passes_the_press_through() {
let mut s = window();
modifier_drag(&mut s, (200.0, 150.0), (60.0, 20.0), (false, true));
assert_eq!(log_of(&s), "Kid:down Kid:up", "mode 0 at start");
s.run("log = {} SetLayoutMode(0.9)").unwrap();
modifier_drag(&mut s, (200.0, 150.0), (60.0, 20.0), (false, true));
assert_eq!(log_of(&s), "Kid:down Kid:up", "0.9 truncates to 0");
s.run("log = {} SetLayoutMode('x')").unwrap();
modifier_drag(&mut s, (200.0, 150.0), (60.0, 20.0), (false, false));
assert_eq!(log_of(&s), "Kid:down Kid:up", "no modifier held");
assert_eq!(rect(&mut s, "Win"), (100.0, 100.0, 200.0, 100.0));
modifier_drag(&mut s, (200.0, 150.0), (60.0, 20.0), (false, true));
assert_eq!(
rect(&mut s, "Win"),
(160.0, 120.0, 200.0, 100.0),
"a non-number stored 1"
);
assert!(s.errors().is_empty(), "{:?}", s.errors());
}
/// `0x765490`'s own walk: strata FULLSCREEN_DIALOG down to WORLD, never TOOLTIP (`0x7654a3` starts
/// below `root+0xcf4`), probing with `0x76b020`, which reads no mouse bit. A hit with no toplevel
/// self-or-ancestor gives up its whole stratum and the walk goes on below it (`0x76554a` →
/// `0x765532`).
#[test]
fn the_walk_skips_tooltip_and_a_stratum_whose_hit_has_no_toplevel() {
let mut s = window();
s.run(
r#"
Win:EnableMouse(false); Kid:EnableMouse(false)
Cover = CreateFrame("Frame", "Cover")
Cover:SetFrameStrata("HIGH")
Cover:SetPoint("BOTTOMLEFT", 0, 0); Cover:SetWidth(800); Cover:SetHeight(600)
Top = CreateFrame("Frame", "Top", Cover)
Top:SetToplevel(true); Top:SetMovable(true)
Top:SetPoint("BOTTOMLEFT", 500, 500); Top:SetWidth(50); Top:SetHeight(50)
Cover:SetFrameLevel(5)
Under = CreateFrame("Frame", "Under")
Under:SetFrameStrata("HIGH"); Under:SetToplevel(true); Under:SetMovable(true)
Under:SetPoint("BOTTOMLEFT", 150, 120); Under:SetWidth(100); Under:SetHeight(60)
Tip = CreateFrame("Frame", "Tip")
Tip:SetFrameStrata("TOOLTIP"); Tip:SetToplevel(true); Tip:SetMovable(true)
Tip:SetPoint("BOTTOMLEFT", 150, 120); Tip:SetWidth(100); Tip:SetHeight(60)
SetLayoutMode(1)
"#,
)
.unwrap();
s.resolve();
modifier_drag(&mut s, (200.0, 150.0), (60.0, 20.0), (false, true));
assert_eq!(rect(&mut s, "Win"), (160.0, 120.0, 200.0, 100.0));
assert_eq!(rect(&mut s, "Tip"), (150.0, 120.0, 100.0, 60.0));
assert_eq!(
rect(&mut s, "Under"),
(150.0, 120.0, 100.0, 60.0),
"below Cover in HIGH"
);
assert_eq!(rect(&mut s, "Cover"), (0.0, 0.0, 800.0, 600.0));
assert!(s.errors().is_empty(), "{:?}", s.errors());
}
/// A release while the modifier drag holds the slot ends it and nothing else: no `OnMouseUp` on
/// the capture a chorded press left (`0x76646d` → `0x7664e0`).
#[test]
fn a_release_that_ends_a_layout_drag_reaches_no_script() {
let mut s = window();
s.run("SetLayoutMode(1)").unwrap();
s.mouse_move(120.0, 110.0);
s.mouse_button(120.0, 110.0, "RightButton", true); // capture Win
s.set_modifiers(false, false, true);
s.mouse_button(120.0, 110.0, "LeftButton", true); // the layout drag
s.set_modifiers(false, false, false);
s.mouse_button(120.0, 110.0, "RightButton", false);
assert_eq!(
log_of(&s),
"Win:down",
"the right release reached no script"
);
s.mouse_move(220.0, 110.0);
assert_eq!(rect(&mut s, "Win").0, 100.0, "and ended the drag");
// The capture is still Win: the left release, the drag over, goes to it (`0x766498`–
// `0x7664a4`) and, with no button held, clears it (`0x7664bb`).
s.run("Win:SetScript('OnMouseUp', function() table.insert(log, 'Win:up:'..arg1) end)")
.unwrap();
s.mouse_button(220.0, 110.0, "LeftButton", false);
assert_eq!(log_of(&s), "Win:down Win:up:LeftButton");
s.mouse_move(200.0, 150.0);
s.mouse_button(200.0, 150.0, "LeftButton", true);
s.mouse_button(200.0, 150.0, "LeftButton", false);
assert_eq!(
log_of(&s),
"Win:down Win:up:LeftButton Kid:down Kid:up",
"no capture or held button is left behind"
);
assert!(s.errors().is_empty(), "{:?}", s.errors());
}
/// A layout press is consumed even where it hits nothing: the handler returns before any frame,
/// the world's included, sees it (`0x766353`–`0x76635b`).
#[test]
fn a_layout_press_over_nothing_is_consumed() {
let mut s = window();
s.set_modifiers(false, false, true);
assert!(!s.mouse_button(700.0, 500.0, "RightButton", true), "mode 0");
s.mouse_button(700.0, 500.0, "RightButton", false);
s.run("SetLayoutMode(1)").unwrap();
assert!(s.mouse_button(700.0, 500.0, "RightButton", true));
}
/// `SetLayoutMode` keeps the low dword of `_ftol` (`0x40a2b0`, `fistp` to a qword): what the x87
/// cannot hold (an infinity, NaN, 2^63 and up) stores its integer-indefinite pattern, whose low
/// dword is 0, so those turn the mode off; 2^32 + 1 keeps 1.
#[test]
fn set_layout_mode_keeps_the_low_dword_of_ftol() {
let mut s = window();
let armed = |s: &mut UiScript, arg: &str| {
s.run(&format!("log = {{}} SetLayoutMode({arg})")).unwrap();
modifier_drag(s, (200.0, 150.0), (0.0, 0.0), (false, true));
log_of(s).is_empty()
};
for off in ["1/0", "-1/0", "0/0", "2^63", "2^64", "2^32", "-2^63"] {
assert!(!armed(&mut s, off), "SetLayoutMode({off}) is off");
}
for on in ["2^32 + 1", "-2^31", "-1.5"] {
assert!(armed(&mut s, on), "SetLayoutMode({on}) is on");
}
}
@@ -30,10 +30,12 @@ mod handler_prof;
mod host_state;
mod hover_publish;
mod hover_walk;
mod hyperlink;
mod input;
mod keep_rect;
mod keyboard;
mod layout_gate;
mod layout_mode;
mod layout_structure;
mod line_spacing;
mod measure;
@@ -671,3 +671,127 @@ fn the_apply_seats_position_behind_movable_and_size_behind_resizable() {
);
assert!(s.errors().is_empty(), "{:?}", s.errors());
}
// ── The drag slot is taken, never refused (`0x7652b0`) ──────────────────────────────────────────
/// A title-region frame `A` at (100, 100) 200×80 and a movable `B` at (500, 100), on 800×600.
fn title_and_mover() -> UiScript {
let mut s = script();
s.set_screen_size(800.0, 600.0);
s.run(
r#"
log = {}
A = CreateFrame("Frame", "A")
A:SetPoint("BOTTOMLEFT", 100, 100); A:SetWidth(200); A:SetHeight(80)
A:EnableMouse(true)
A:SetScript("OnMouseUp", function() table.insert(log, "A:up:"..arg1) end)
local title = A:CreateTitleRegion()
title:SetPoint("TOPLEFT", A, "TOPLEFT", 0, 0)
title:SetPoint("BOTTOMRIGHT", A, "TOPRIGHT", 0, -20)
B = CreateFrame("Frame", "B")
B:SetPoint("BOTTOMLEFT", 500, 100); B:SetWidth(100); B:SetHeight(100)
B:SetMovable(true)
"#,
)
.unwrap();
s.resolve();
s
}
fn lefts(s: &mut UiScript) -> (f64, f64) {
s.resolve();
s.eval::<(f64, f64)>("return A:GetLeft(), B:GetLeft()")
.unwrap()
}
/// A title press while a scripted `StartMoving` holds the slot takes it: the drag entry stores the
/// new frame over the old one with no test of the slot (`0x7652f3`–`0x76530f`), and nothing runs
/// on the frame it displaced, which simply stops following.
#[test]
fn a_title_press_takes_the_slot_from_a_running_move() {
let mut s = title_and_mover();
s.mouse_move(150.0, 170.0); // the hover: A, in its title strip
s.run("B:StartMoving()").unwrap();
s.mouse_button(150.0, 170.0, "LeftButton", true);
// The slot holds A now, so B's own stop clears nothing (`0x776990` tests the holder).
s.run("B:StopMovingOrSizing()").unwrap();
s.mouse_move(250.0, 170.0);
assert_eq!(
lefts(&mut s),
(200.0, 500.0),
"A follows; B stopped where it was"
);
assert!(s.eval::<bool>("return A:IsUserPlaced()").unwrap());
// The title move is mode 2, which the release ends.
s.mouse_button(250.0, 170.0, "LeftButton", false);
s.mouse_move(350.0, 170.0);
assert_eq!(lefts(&mut s), (200.0, 500.0));
assert!(s.errors().is_empty(), "{:?}", s.errors());
}
/// The press goes to the capture, else the hover (`0x76637f`–`0x766389`), and a walk that finds a
/// move in flight pumps it and returns before its sweep (`0x766164`): the hover stays the frame it
/// was when the move began. So with a window riding the cursor (as a clicked chat tab leaves it),
/// a press over another frame's title region reaches the riding window, not that frame.
#[test]
fn a_press_during_a_move_goes_to_the_hover_the_move_froze() {
let mut s = script();
s.set_screen_size(800.0, 600.0);
s.run(
r#"
log = {}
W = CreateFrame("Frame", "W")
W:SetPoint("BOTTOMLEFT", 100, 100); W:SetWidth(200); W:SetHeight(100)
W:EnableMouse(true); W:SetMovable(true)
W:SetScript("OnMouseDown", function() table.insert(log, "W:down"); this:StartMoving() end)
T = CreateFrame("Frame", "T")
T:SetFrameStrata("HIGH"); T:SetToplevel(true); T:SetMovable(true)
T:SetPoint("BOTTOMLEFT", 400, 100); T:SetWidth(200); T:SetHeight(100)
T:EnableMouse(true)
T:SetScript("OnMouseDown", function() table.insert(log, "T:down") end)
T:CreateTitleRegion():SetAllPoints(T)
"#,
)
.unwrap();
s.resolve();
s.mouse_move(150.0, 150.0);
s.mouse_button(150.0, 150.0, "LeftButton", true); // W starts moving (mode 3)
s.mouse_button(150.0, 150.0, "LeftButton", false); // which the release does not end
s.mouse_move(450.0, 150.0); // W rides to 400, under T
assert_eq!(s.hit_test_name(450.0, 150.0).as_deref(), Some("T"));
s.mouse_button(450.0, 150.0, "LeftButton", true);
s.mouse_button(450.0, 150.0, "LeftButton", false);
s.mouse_move(500.0, 150.0);
s.resolve();
assert_eq!(
s.eval::<(f64, f64)>("return W:GetLeft(), T:GetLeft()")
.unwrap(),
(450.0, 400.0),
"W still rides; T never moved"
);
assert_eq!(
s.eval::<String>("return table.concat(log, ' ')").unwrap(),
"W:down W:down"
);
assert!(s.errors().is_empty(), "{:?}", s.errors());
}
/// A release while a title move (mode 2) holds the slot ends it and does nothing else
/// (`0x76646d`–`0x76647c` → `0x7664e0`): no `OnMouseUp` on the capture a chorded press left.
#[test]
fn a_release_that_ends_a_title_move_fires_nothing() {
let mut s = title_and_mover();
s.mouse_move(150.0, 120.0); // A, below its title strip
s.mouse_button(150.0, 120.0, "RightButton", true); // capture A
s.mouse_move(150.0, 170.0);
s.mouse_button(150.0, 170.0, "LeftButton", true); // A's title region: mode 2
s.mouse_button(150.0, 170.0, "RightButton", false);
assert_eq!(
s.eval::<String>("return table.concat(log, ' ')").unwrap(),
"",
"the right release ended the move and reached no script"
);
s.mouse_move(250.0, 170.0);
assert_eq!(lefts(&mut s).0, 100.0, "and the move is over");
assert!(s.errors().is_empty(), "{:?}", s.errors());
}
+1 -6
View File
@@ -123,14 +123,9 @@ fn walk_children(lua: &Lua, parent: FrameHandle, step: fn(&Lua, FrameHandle)) {
}
}
/// Whether the frame is in the mouse input index when visible: its mouse bit (`[+0xcc]` bit 2),
/// or a hyperlink span, which the reference gives a mouse-enabled child of its own (`0x7a3240`).
/// Whether the frame is in the mouse input index when visible: its mouse bit (`[+0xcc]` bit 2).
fn takes_mouse(model: &Model, h: FrameHandle) -> bool {
model.arena.is_mouse_enabled(h)
|| model
.link_spans
.get(&h)
.is_some_and(|spans| !spans.is_empty())
}
/// The Lua `EnableMouse` (`0x777070`). Enabling inserts with flag -1 and arms nothing
@@ -770,6 +770,9 @@ pub struct ButtonState {
pub loot_slot: Option<u32>,
/// A CheckButton's checked flag (`+0x4dc`, XML `checked`).
pub checked: bool,
/// A `CSimpleHyperlinkButton`'s link (`+0x4dc`) and text (`+0x4e0`), which its enter, leave
/// and click hand to its text frame (`script::hyperlink`); `None` on any other Button.
pub hyperlink: Option<(String, String)>,
/// `<NormalTexture>`/`SetNormalTexture` (`+0x4bc`).
pub normal: Option<RegionHandle>,
/// `<PushedTexture>` (`+0x4c0`); a button with none keeps what it showed ([`Self::set_state`]).
@@ -824,6 +827,7 @@ impl Default for ButtonState {
loot_slot: None,
locked: false,
checked: false,
hyperlink: None,
normal: None,
pushed: None,
disabled: None,
+1 -1
View File
@@ -300,7 +300,7 @@ impl Default for WidgetArena {
/// (`0x778771`, CheckButton through it), Slider (`0x789467`), ColorSelect (`0x78b2b5`), EditBox
/// (`0x779ced`..`0x779d03`, with char and key) and Minimap (`0x4edc38`) set its mouse bit. The
/// ScrollFrame and ScrollingMessageFrame ctors set none; a chat link takes clicks through a
/// mouse-enabled child per span (`0x7a3240`), which `script::pointer` models as rects.
/// mouse-enabled Button child per span (`0x7a3240`), which `script::hyperlink` builds.
pub fn mouse_enabled_by_ctor(kind: FrameKind) -> bool {
matches!(
kind,