ui: the HIGHLIGHT draw layer is the highlight state, and starts off
A texture an addon made with `CreateTexture(nil, "HIGHLIGHT")` drew all the
time. Only the region `SetHighlightTexture` installs was hover-gated, through
`ButtonState::region_visible`; anything else an addon left in layer 4 was an
ordinary region that nothing ever hid.
In the reference that layer is the highlight *state*, not a place to keep
highlight art, and four things move it:
OnEnter 0x76b6a0 test [frame+0xf8]; push 4; call 0x76a710 EnableDrawLayer
OnLeave 0x76b6f0 test [frame+0xf8]; push 4; call 0x76a730 DisableDrawLayer
Lock 0x76c2e0 writes [frame+0xf8], then enables; on an unlock disables
only when this frame is not the mouse focus ([root+0x7c])
Disable 0x7791bb disables it outright
`0x77983c` settles what the layer means: a Button picks its label's font
instance by reading that layer's own enabled flag at [frame+0x1a8], which is
[frame+0x198] + 4*4, and takes the highlight font object at +0x3b8 when it is
set. So the layer starts off, and every path that can highlight a frame moves
it -- taking whatever the addon put there along.
What this was costing: hand-built column headers and list rows, which are
everywhere. The <Below Average> guild market's Browse page puts
UI-Character-Tab-Highlight in that layer on each of its nine sort headers, so
every header wore a permanent blue glow bar, and its Statistics page showed all
six filter buttons lit at once instead of the two that were selected.
The nameplate glow lives in layer 4 too, and its hover feed drove only the
region's shown flag. Its own OnEnter override chains back into the base notify
(0x7cb8a5 -> 0x76b6a0), so it now moves the layer with it; addons still read
mouseover from glow:IsShown().
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
This commit is contained in:
@@ -13,7 +13,7 @@ use crate::justify::Justify;
|
||||
use crate::order::DrawLayer;
|
||||
use crate::widget::{
|
||||
ButtonFont, ButtonState, ButtonVisualState, FrameHandle, FrameKind, KindState, RegionHandle,
|
||||
RegionKind,
|
||||
RegionKind, HIGHLIGHT_OFF,
|
||||
};
|
||||
|
||||
pub(in crate::script) const REG_BUTTON_METHODS: &str = "__benilla_button_methods";
|
||||
@@ -192,27 +192,70 @@ fn with_button<T>(
|
||||
}
|
||||
}
|
||||
|
||||
/// Draw layer 4, HIGHLIGHT, as a whole -- not only the region `SetHighlightTexture` made.
|
||||
///
|
||||
/// The reference keeps the highlight *state* in that layer's own enabled flag
|
||||
/// (`[frame+0x198] + 4*4`), which is why `0x77983c` reads it to decide whether a Button's label
|
||||
/// takes the highlight font object. Everything that can highlight a frame therefore moves the
|
||||
/// layer, and anything an addon left in it moves with them. See [`HIGHLIGHT_OFF`].
|
||||
pub(super) fn set_highlight_layer(model: &mut Model, h: FrameHandle, on: bool) {
|
||||
if let Some(frame) = model.arena.frame_mut(h) {
|
||||
if on {
|
||||
frame.disabled_layers &= !HIGHLIGHT_OFF;
|
||||
} else {
|
||||
frame.disabled_layers |= HIGHLIGHT_OFF;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether `LockHighlight()` is holding the layer on for `h`. Only a Button has the method, so
|
||||
/// anything else answers false.
|
||||
fn highlight_locked(model: &Model, h: FrameHandle) -> bool {
|
||||
matches!(
|
||||
model.arena.frame(h).map(|f| &f.kind_state),
|
||||
Some(KindState::Button(bs)) if bs.locked_highlight
|
||||
)
|
||||
}
|
||||
|
||||
/// The highlight half of a hover edge: `OnEnter` (`0x76b6a0`) enables the layer and `OnLeave`
|
||||
/// (`0x76b6f0`) disables it, each after `test [frame+0xf8]` skips it while `LockHighlight` holds.
|
||||
/// A disabled Button reaches neither notify (`0x779490`/`0x7794e0` guard on `[+0x328]`), so the
|
||||
/// layer does not move there either -- and it is already off, from `Disable()`.
|
||||
pub(super) fn hover_highlight(model: &mut Model, h: Option<FrameHandle>, entering: bool) {
|
||||
let Some(h) = h else { return };
|
||||
if !hover_notify_runs(model, Some(h)) || highlight_locked(model, h) {
|
||||
return;
|
||||
}
|
||||
set_highlight_layer(model, h, entering);
|
||||
}
|
||||
|
||||
/// `Enable()`/`Disable()`. `Disable` (`0x77ffd0`) reaches `SetEnabled` (`0x779160`), which also
|
||||
/// turns draw layer 4, HIGHLIGHT, off (`0x7791bb`) in the array `DisableDrawLayer` writes, so every
|
||||
/// region in that layer goes dark, not only the HighlightTexture. `Enable()` here turns it back
|
||||
/// on; the reference restores it only on a hovered button, by re-running its `<OnEnter>`
|
||||
/// (`0x779183`), and never while `LockHighlight` holds (`[frame+0xf8]`).
|
||||
/// region in that layer goes dark, not only the HighlightTexture. `Enable()` restores it only on a
|
||||
/// hovered button, by re-running its `<OnEnter>` (`0x779183`), and never while `LockHighlight`
|
||||
/// holds (`[frame+0xf8]`) -- a button enabled under the cursor lights up, one enabled anywhere
|
||||
/// else does not.
|
||||
fn set_enabled(lua: &Lua, this: &Table, on: bool) -> mlua::Result<()> {
|
||||
let h = frame_handle_of(lua, this)?;
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
|
||||
let hovered = model.mouseover == Some(h);
|
||||
let frame = model
|
||||
.arena
|
||||
.frame_mut(h)
|
||||
.ok_or_else(|| mlua::Error::runtime("stale frame handle"))?;
|
||||
match &mut frame.kind_state {
|
||||
KindState::Button(bs) => bs.set_enabled(on),
|
||||
let locked = match &mut frame.kind_state {
|
||||
KindState::Button(bs) => {
|
||||
bs.set_enabled(on);
|
||||
bs.locked_highlight
|
||||
}
|
||||
_ => return Err(mlua::Error::runtime("not a Button")),
|
||||
};
|
||||
let bit = 1u8 << DrawLayer::Highlight.index();
|
||||
if on {
|
||||
frame.disabled_layers &= !bit;
|
||||
if hovered || locked {
|
||||
frame.disabled_layers &= !HIGHLIGHT_OFF;
|
||||
}
|
||||
} else {
|
||||
frame.disabled_layers |= bit;
|
||||
frame.disabled_layers |= HIGHLIGHT_OFF;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -796,17 +839,30 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
)?,
|
||||
)?;
|
||||
|
||||
// LockHighlight (`0x782840`) and UnlockHighlight: the highlight shows regardless of hover.
|
||||
// LockHighlight (`0x782840`) and UnlockHighlight (`0x782920`), both through `0x76c2e0`: the
|
||||
// highlight shows regardless of hover. That function writes `[frame+0xf8]` and then moves draw
|
||||
// layer 4 with it -- enabling it on a lock, and on an unlock disabling it *unless this frame is
|
||||
// the mouse focus* (`cmp [root+0x7c], ecx`), so unlocking under the cursor leaves it lit.
|
||||
m.set(
|
||||
"LockHighlight",
|
||||
lua.create_function(|lua, this: Table| {
|
||||
with_button(lua, &this, |bs| bs.locked_highlight = true)
|
||||
let h = frame_handle_of(lua, &this)?;
|
||||
with_button(lua, &this, |bs| bs.locked_highlight = true)?;
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
|
||||
set_highlight_layer(&mut model, h, true);
|
||||
Ok(())
|
||||
})?,
|
||||
)?;
|
||||
m.set(
|
||||
"UnlockHighlight",
|
||||
lua.create_function(|lua, this: Table| {
|
||||
with_button(lua, &this, |bs| bs.locked_highlight = false)
|
||||
let h = frame_handle_of(lua, &this)?;
|
||||
with_button(lua, &this, |bs| bs.locked_highlight = false)?;
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
|
||||
if model.mouseover != Some(h) {
|
||||
set_highlight_layer(&mut model, h, false);
|
||||
}
|
||||
Ok(())
|
||||
})?,
|
||||
)?;
|
||||
|
||||
|
||||
@@ -138,6 +138,9 @@ pub(super) fn fire_visibility_changes(lua: &Lua, changed: Vec<FrameHandle>) {
|
||||
let notified = super::button::hover_notify_runs(&model, hidden_hover);
|
||||
if let Some(m) = hidden_hover {
|
||||
model.mouseover = None;
|
||||
// The leave carries its half of the highlight with it: the layer must not stay enabled
|
||||
// on a frame that is no longer under the cursor, or it comes back lit on the next show.
|
||||
super::button::hover_highlight(&mut model, Some(m), false);
|
||||
if model.drag.as_ref().is_some_and(|d| d.source == m) {
|
||||
model.drag = None;
|
||||
}
|
||||
|
||||
@@ -893,6 +893,11 @@ impl Plate {
|
||||
model.region_data.entry(name).or_default().vertex_color = Some(c);
|
||||
// The glow really shows, as addons read mouseover from it; it only paints nothing.
|
||||
model.region_data.entry(glow).or_default().hidden = !state.hovered;
|
||||
// The glow lives in draw layer 4, HIGHLIGHT, which is off until something highlights
|
||||
// the frame. The plate's own `OnEnter` override chains back into the base notify
|
||||
// (`0x7cb8a5` -> `0x76b6a0`), so the hover that shows the region enables the layer
|
||||
// under it; the region's shown flag alone would leave it dark.
|
||||
super::button::set_highlight_layer(model, frame, state.hovered);
|
||||
}
|
||||
|
||||
if last.as_ref().is_none_or(|l| {
|
||||
|
||||
@@ -237,6 +237,10 @@ impl UiScript {
|
||||
// is still the hover: the reference moves `[root+0x7c]` before either notify.
|
||||
let old_id = old_id.filter(|_| button::hover_notify_runs(&model, old_handle));
|
||||
let enter_id = new_id.filter(|_| button::hover_notify_runs(&model, new_handle));
|
||||
// Both notifies move draw layer 4, HIGHLIGHT, before the Lua handler runs, so an
|
||||
// `<OnEnter>` that reads the frame already sees the highlighted state.
|
||||
button::hover_highlight(&mut model, old_handle, false);
|
||||
button::hover_highlight(&mut model, new_handle, true);
|
||||
(old_id, drag_start, enter_id, slider_change, color_change)
|
||||
}
|
||||
};
|
||||
|
||||
@@ -223,6 +223,164 @@ fn disabling_a_button_takes_its_whole_highlight_layer() {
|
||||
assert!(s.errors().is_empty(), "{:?}", s.errors());
|
||||
}
|
||||
|
||||
/// Draw layer 4, HIGHLIGHT, is the highlight *state*, not a place for art that is always on: the
|
||||
/// base hover notifies toggle the whole layer (`0x76b6a0` enables, `0x76b6f0` disables), which is
|
||||
/// why a Button picks its highlight font by reading the layer's own flag (`0x77983c` tests
|
||||
/// `[frame+0x1a8]`). So it starts off, and a region an addon creates in it stays dark until the
|
||||
/// cursor arrives. Hand-built column headers and list rows all over the addon world depend on it:
|
||||
/// EGM's guild market puts `UI-Character-Tab-Highlight` there and never hides it itself.
|
||||
#[test]
|
||||
fn a_highlight_layer_region_is_dark_until_the_cursor_is_on_the_frame() {
|
||||
let mut s = script();
|
||||
s.set_screen_size(800.0, 600.0);
|
||||
s.run(
|
||||
r#"
|
||||
local f = CreateFrame("Frame", "HiLayer")
|
||||
f:SetPoint("BOTTOMLEFT", 0, 0); f:SetWidth(100); f:SetHeight(100)
|
||||
f:EnableMouse(true)
|
||||
local base = f:CreateTexture(nil, "ARTWORK")
|
||||
base:SetAllPoints(f); base:SetTexture("Interface\\Base.blp")
|
||||
local hi = f:CreateTexture(nil, "HIGHLIGHT")
|
||||
hi:SetAllPoints(f); hi:SetTexture("Interface\\Hi.blp")
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
s.resolve();
|
||||
|
||||
let art = |s: &UiScript| -> Vec<String> {
|
||||
s.extract()
|
||||
.iter()
|
||||
.filter(|q| s.quad_owner_name(q.target).as_deref() == Some("HiLayer"))
|
||||
.filter_map(|q| match &q.content {
|
||||
QuadContent::Texture { path: Some(p), .. } => Some(p.clone()),
|
||||
_ => None,
|
||||
})
|
||||
.collect()
|
||||
};
|
||||
|
||||
s.mouse_move(400.0, 400.0);
|
||||
assert_eq!(
|
||||
art(&s),
|
||||
vec!["Interface\\Base.blp".to_string()],
|
||||
"the layer is off at rest, so only the ARTWORK region draws"
|
||||
);
|
||||
assert_eq!(
|
||||
s.eval::<i64>(r#"return HiLayer:IsShown()"#).unwrap(),
|
||||
1,
|
||||
"the frame itself is shown; it is the layer that is off"
|
||||
);
|
||||
|
||||
s.mouse_move(50.0, 50.0);
|
||||
let v = art(&s);
|
||||
assert!(
|
||||
v.contains(&"Interface\\Hi.blp".to_string()),
|
||||
"the cursor on the frame enables the layer: {v:?}"
|
||||
);
|
||||
|
||||
s.mouse_move(400.0, 400.0);
|
||||
assert_eq!(
|
||||
art(&s),
|
||||
vec!["Interface\\Base.blp".to_string()],
|
||||
"and leaving disables it again"
|
||||
);
|
||||
assert!(s.errors().is_empty(), "{:?}", s.errors());
|
||||
}
|
||||
|
||||
/// `LockHighlight`/`UnlockHighlight` (`0x782840`/`0x782920`) both reach `0x76c2e0`, which writes
|
||||
/// `[frame+0xf8]` and then moves layer 4 with it -- enabling it on a lock, and on an unlock
|
||||
/// disabling it *unless this frame is the mouse focus* (`cmp [root+0x7c], ecx`). That last branch
|
||||
/// is what keeps a selected trade-skill row lit when the cursor happens to be on it.
|
||||
#[test]
|
||||
fn lock_highlight_drives_the_layer_and_unlocking_under_the_cursor_leaves_it_lit() {
|
||||
let mut s = script();
|
||||
s.set_screen_size(800.0, 600.0);
|
||||
s.run(
|
||||
r#"
|
||||
local b = CreateFrame("Button", "LockBtn")
|
||||
b:SetPoint("BOTTOMLEFT", 0, 0); b:SetWidth(100); b:SetHeight(100)
|
||||
local hi = b:CreateTexture(nil, "HIGHLIGHT")
|
||||
hi:SetAllPoints(b); hi:SetTexture("Interface\\Hi.blp")
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
s.resolve();
|
||||
|
||||
let lit = |s: &UiScript| -> bool {
|
||||
s.extract()
|
||||
.iter()
|
||||
.filter(|q| s.quad_owner_name(q.target).as_deref() == Some("LockBtn"))
|
||||
.any(|q| {
|
||||
matches!(&q.content, QuadContent::Texture { path: Some(p), .. }
|
||||
if p == "Interface\\Hi.blp")
|
||||
})
|
||||
};
|
||||
|
||||
s.mouse_move(400.0, 400.0);
|
||||
assert!(!lit(&s), "off at rest");
|
||||
|
||||
s.run("LockBtn:LockHighlight()").unwrap();
|
||||
assert!(lit(&s), "a lock lights it with the cursor elsewhere");
|
||||
|
||||
s.mouse_move(50.0, 50.0);
|
||||
s.run("LockBtn:UnlockHighlight()").unwrap();
|
||||
assert!(
|
||||
lit(&s),
|
||||
"unlocking under the cursor leaves it lit: the hover owns it now"
|
||||
);
|
||||
|
||||
s.mouse_move(400.0, 400.0);
|
||||
assert!(!lit(&s), "and leaving finally takes it");
|
||||
|
||||
s.run("LockBtn:LockHighlight() LockBtn:UnlockHighlight()")
|
||||
.unwrap();
|
||||
assert!(!lit(&s), "unlocking away from the cursor disables it");
|
||||
assert!(s.errors().is_empty(), "{:?}", s.errors());
|
||||
}
|
||||
|
||||
/// `Enable()` does not light a button on its own: the reference restores the layer only by
|
||||
/// re-running `<OnEnter>` on a hovered button (`0x779183`), so a form that enables its Submit
|
||||
/// button when the fields are filled does not leave it glowing from across the screen.
|
||||
#[test]
|
||||
fn enable_lights_the_highlight_only_under_the_cursor() {
|
||||
let mut s = script();
|
||||
s.set_screen_size(800.0, 600.0);
|
||||
s.run(
|
||||
r#"
|
||||
local b = CreateFrame("Button", "EnBtn")
|
||||
b:SetPoint("BOTTOMLEFT", 0, 0); b:SetWidth(100); b:SetHeight(100)
|
||||
local hi = b:CreateTexture(nil, "HIGHLIGHT")
|
||||
hi:SetAllPoints(b); hi:SetTexture("Interface\\Hi.blp")
|
||||
b:Disable()
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
s.resolve();
|
||||
|
||||
let lit = |s: &UiScript| -> bool {
|
||||
s.extract()
|
||||
.iter()
|
||||
.filter(|q| s.quad_owner_name(q.target).as_deref() == Some("EnBtn"))
|
||||
.any(|q| {
|
||||
matches!(&q.content, QuadContent::Texture { path: Some(p), .. }
|
||||
if p == "Interface\\Hi.blp")
|
||||
})
|
||||
};
|
||||
|
||||
s.mouse_move(400.0, 400.0);
|
||||
s.run("EnBtn:Enable()").unwrap();
|
||||
assert!(!lit(&s), "enabled away from the cursor stays dark");
|
||||
|
||||
s.mouse_move(50.0, 50.0);
|
||||
assert!(lit(&s), "and the cursor arriving lights it");
|
||||
|
||||
s.run("EnBtn:Disable() EnBtn:Enable()").unwrap();
|
||||
assert!(
|
||||
lit(&s),
|
||||
"enabled under the cursor comes back lit, as the re-run OnEnter would"
|
||||
);
|
||||
assert!(s.errors().is_empty(), "{:?}", s.errors());
|
||||
}
|
||||
|
||||
/// `CButton::OnMouseDown` shows the pushed texture when the button is registered for that mouse
|
||||
/// button, up or down (`0x77924b`: `[this+0x330] & (m | m << 8)`), whether or not a click fires.
|
||||
#[test]
|
||||
|
||||
@@ -136,6 +136,20 @@ pub use kinds::{
|
||||
|
||||
// ── Frame and Region nodes ───────────────────────────────────────────────────────────────────
|
||||
|
||||
/// [`Frame::disabled_layers`] with layer 4, HIGHLIGHT, off: a frame's resting state.
|
||||
///
|
||||
/// In the reference that layer is the highlight state itself, not merely where highlight art
|
||||
/// lives. The base hover notifies toggle the whole layer -- `OnEnter` (`0x76b6a0`) enables it
|
||||
/// (`0x76a710`), `OnLeave` (`0x76b6f0`) disables it (`0x76a730`), both skipped while
|
||||
/// `LockHighlight` holds (`[frame+0xf8]`) -- `LockHighlight` drives it directly (`0x76c2e0`), and
|
||||
/// `SetEnabled(false)` turns it off outright (`0x7791bb`). A Button even picks its label's font
|
||||
/// instance by reading the layer's own flag: `0x77983c` tests `[frame+0x1a8]`, which is
|
||||
/// `[frame+0x198] + 4*4`, and takes the highlight font object at `+0x3b8` when it is set.
|
||||
///
|
||||
/// So it has to start off. An addon writing `CreateTexture(nil, "HIGHLIGHT")` is relying on that
|
||||
/// -- a hand-built column header or list row puts its hover art there and never hides it itself.
|
||||
pub const HIGHLIGHT_OFF: u8 = 1 << DrawLayer::Highlight.index();
|
||||
|
||||
/// A frame node, the modelled subset of `CSimpleFrame`. The `effective_*` fields and the tree
|
||||
/// links belong to the [`WidgetArena`] mutators: change state through them so it propagates.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
@@ -144,6 +158,9 @@ pub struct Frame {
|
||||
/// Layers switched off by `DisableDrawLayer` (`0x775680`) and back on by `EnableDrawLayer`
|
||||
/// (`0x7755b0`), bit `n` for [`DrawLayer::index`] `n`: a frame-level mask, so each region
|
||||
/// keeps its own shown state underneath.
|
||||
///
|
||||
/// Layer 4, HIGHLIGHT, starts here set -- that layer is the client's highlight *state*, not a
|
||||
/// place to put art that is always on. See [`HIGHLIGHT_OFF`].
|
||||
pub disabled_layers: u8,
|
||||
/// The global name, which a later duplicate carries without owning ([`WidgetArena::lookup`]).
|
||||
pub name: Option<String>,
|
||||
@@ -468,7 +485,7 @@ impl WidgetArena {
|
||||
let effective_alpha = alpha;
|
||||
|
||||
let frame = Frame {
|
||||
disabled_layers: 0,
|
||||
disabled_layers: HIGHLIGHT_OFF,
|
||||
kind,
|
||||
name: name.clone(),
|
||||
parent,
|
||||
|
||||
Reference in New Issue
Block a user