Files
benilla-octo/crates/benilla-ui/src/script/object/toplevel.rs
T
Sam SungandClaude Opus 5.5 1efd9a7163 Lua bindings coerce numbers, flags and colours as 1.12.1 does (#991)
Lua bindings now read their arguments the way the 1.12.1 client does:

- Numeric strings parse as Storm's `SStrToInt` (`0x64ac60`) and `SStrToFloat` (`0x64aaa0`). That covers channel keys, `GetNumber` and `item:` fields.
- Flag arguments go through `GetBoolOrDefault` (`0x6f1c10`) with each site's default. That is about 40 bindings, `SetChecked()` and `ShowHelm()` among them.
- Colour channels take numeric strings (`lua_tonumber`), and alphas are `isnumber`-gated: no raise, 1.0 by default. A three-argument `SetVertexColor` keeps the current alpha (`0x79ae0a`). So action-bar rings dimmed by a drag stay dimmed, as in 1.12.1.
- `SetTexture("1")` takes the colour form. MessageFrame channels follow `0x795590`.
- `LoggingChat`/`LoggingCombat` return the flag.

Binding-level tests use the reference's values.

Fixes #135
Fixes #144
Fixes #160
Fixes #161

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 13:41:47 +07:00

152 lines
6.1 KiB
Rust

//! Frame methods: the `toplevel` flag, `Raise` and `Lower`, and the raise every trigger runs.
//!
//! The flag is bit `0x1` of `[frame+0xb4]`, set by `0x76a3c0` from XML (`0x7698ec`) or
//! `SetToplevel` (`0x775440`), and setting it raises nothing. `CSimpleTop::Raise` (`0x7650f0`):
//!
//! 1. Walks up `+0x9c` to the nearest toplevel self-or-ancestor; with none it does nothing.
//! 2. With `force`, resolves the frame's layout and tests whether it is overlapped: a visible frame
//! of its stratum at its level or above, neither itself nor a descendant, meets its rect.
//! 3. Only then compacts the stratum's levels below its high-water bound, hidden frames among
//! them (`0x764eb0`), and sets its level to the bound, moving same-stratum children by the same
//! delta (`0x76a4f0`).
//!
//! The stratum never changes. The overlapped bit is recomputed per raise, never stored: only the
//! `force = 0` trigger (`0x764a4c`) reads a stored bit, and it is not wired, so neither is the
//! per-tick pass that keeps it (`0x7657d0`). The wired triggers are `Raise` (`0x775b0a`), Show
//! (`0x76aeec`), drag start (`0x7652d7`) and mouse-down (`0x766392`).
use mlua::{Lua, MultiValue, Table};
use crate::layout::Rect;
use crate::script::{Model, UiScript};
use crate::widget::FrameHandle;
use super::frame_handle_of;
/// Populate `m`'s toplevel and raise methods.
pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
// SetToplevel's argument goes through `GetBoolOrDefault` (`0x6f1c10`) with default 1
// (`0x775440`, `0x7754ce`): a bare call sets it, an explicit nil clears it.
m.set(
"SetToplevel",
lua.create_function(|lua, (this, args): (Table, MultiValue)| {
let on = crate::script::binding_abi::bool_or_default(args.front(), true);
let h = frame_handle_of(lua, &this)?;
lua.app_data_mut::<Model>()
.expect("model")
.arena
.set_toplevel(h, on);
Ok(())
})?,
)?;
m.set(
"IsToplevel",
lua.create_function(|lua, this: Table| {
let h = frame_handle_of(lua, &this)?;
let model = lua.app_data_ref::<Model>().expect("model");
Ok(crate::script::binding_abi::flag(model.arena.is_toplevel(h)))
})?,
)?;
// Raise() (`0x775a50` → `0x76a5b0` → `0x7650f0`, `force = 1`): legal on any frame, and silent
// when no toplevel self-or-ancestor exists.
m.set(
"Raise",
lua.create_function(|lua, this: Table| {
let h = frame_handle_of(lua, &this)?;
let mut model = lua.app_data_mut::<Model>().expect("model");
raise(&mut model, h);
Ok(())
})?,
)?;
// Lower() does nothing in the reference: `0x775b20` → `0x76a5c0` → `0x7652a0`, which is
// `xor eax,eax; ret 4`.
m.set("Lower", lua.create_function(|_, _this: Table| Ok(()))?)?;
Ok(())
}
/// `CSimpleTop::Raise(frame, force = 1)` (`0x7650f0`), the module doc's three steps; true, as in
/// the reference, when a toplevel self-or-ancestor was found, whether or not it moved.
pub(in crate::script) fn raise(model: &mut Model, frame: FrameHandle) -> bool {
// 1. The nearest toplevel self-or-ancestor, else nothing.
let Some(t) = nearest_toplevel(model, frame) else {
return false;
};
// 2. Resolve the pending layout (`0x76513e`), then test for overlap. Deviation: an
// `OnSizeChanged` that resolve produces fires at the next `UiScript::resolve`, because a
// Lua handler must not run in the middle of a level compaction.
UiScript::resolve_layout(model);
if !overlapped(model, t) {
return true;
}
// 3. The raise: compact the occupied levels below the stratum's bound, then land on the bound.
let Some(strata) = model.arena.frame(t).map(|f| f.strata) else {
return true;
};
let count = model.arena.compact_levels(strata);
model.arena.set_frame_level(t, count, true);
true
}
/// The Show trigger (`0x76ae10` at `0x76aee0`): on becoming visible, raise only if the frame's own
/// toplevel bit is set, so showing a child never lifts its window.
pub(in crate::script) fn raise_on_show(model: &mut Model, h: FrameHandle) {
if model.arena.is_toplevel(h) {
raise(model, h);
}
}
/// 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);
let mut guard = 0usize;
while let Some(h) = cur {
let f = model.arena.frame(h)?;
if f.toplevel {
return Some(h);
}
cur = f.parent;
guard += 1;
if guard > bound {
break;
}
}
None
}
/// Step 2 (`0x7650f0` inline, `0x765a90` standalone): a visible frame of `t`'s stratum at `t`'s
/// level or above, neither `t` nor its descendant (`0x767010`), whose rect meets `t`'s with area.
fn overlapped(model: &Model, t: FrameHandle) -> bool {
let Some(tf) = model.arena.frame(t) else {
return false;
};
let (strata, level) = (tf.strata, tf.level);
let Some(t_rect) = model.resolved.get(&t).copied() else {
// No resolved rect: not on screen, so nothing overlaps it.
return false;
};
model.arena.iter_frames().any(|(h, f)| {
h != t
&& f.effective_visible
&& f.strata == strata
&& f.level >= level
&& !model.arena.is_ancestor(t, h)
&& model
.resolved
.get(&h)
.is_some_and(|r| rects_overlap(t_rect, *r))
})
}
/// Whether two rects intersect with area: the reference reads both (`0x768320`), intersects them
/// (`0x766c70`) and tests the result (`0x766bc0`).
fn rects_overlap(a: Rect, b: Rect) -> bool {
let i = crate::script::clip::intersect_rect(a, b);
i.width() > 0.0 && i.height() > 0.0
}