benilla: initial public release
A from-scratch World of Warcraft 1.12.1 client in Rust + Bevy. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,189 @@
|
||||
//! Frame method-table cluster: event registration + script handlers (`RegisterEvent`/
|
||||
//! `UnregisterEvent`/`SetScript`/`GetScript`), the drag-gesture registration (`RegisterForDrag`,
|
||||
//! decision 0216 §3), and region creation (`CreateTexture`/`CreateFontString`). Split out of
|
||||
//! [`super`] purely for size — see its module doc for the shared id/handle plumbing and
|
||||
//! method-table wiring.
|
||||
|
||||
use std::collections::HashSet;
|
||||
|
||||
use mlua::{Function, Lua, MultiValue, Table, Value};
|
||||
|
||||
use crate::script::region::region_wrapper;
|
||||
use crate::script::{Model, RegionData, REG_SCRIPTS, SCRIPT_KINDS};
|
||||
use crate::widget::RegionKind;
|
||||
|
||||
use super::{decode_id, draw_layer_from_str, frame_handle_of, publish_global};
|
||||
|
||||
/// Populate `m`'s event/script and region-creation methods (see the module doc).
|
||||
pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
// Events + scripts
|
||||
m.set(
|
||||
"RegisterEvent",
|
||||
lua.create_function(|lua, (this, event): (Table, String)| {
|
||||
let h = frame_handle_of(lua, &this)?;
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model");
|
||||
// Ordered listener list (the client's SignalEvent walks a LIST — order is law);
|
||||
// re-register keeps the original position (no duplicate fire).
|
||||
let list = model.event_to_frames.entry(event.clone()).or_default();
|
||||
if !list.contains(&h) {
|
||||
list.push(h);
|
||||
}
|
||||
model.frame_events.entry(h).or_default().insert(event);
|
||||
Ok(())
|
||||
})?,
|
||||
)?;
|
||||
m.set(
|
||||
"UnregisterEvent",
|
||||
lua.create_function(|lua, (this, event): (Table, String)| {
|
||||
let h = frame_handle_of(lua, &this)?;
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model");
|
||||
if let Some(list) = model.event_to_frames.get_mut(&event) {
|
||||
list.retain(|x| x != &h);
|
||||
}
|
||||
if let Some(set) = model.frame_events.get_mut(&h) {
|
||||
set.remove(&event);
|
||||
}
|
||||
Ok(())
|
||||
})?,
|
||||
)?;
|
||||
m.set(
|
||||
"SetScript",
|
||||
lua.create_function(
|
||||
|lua, (this, name, func): (Table, String, Option<Function>)| {
|
||||
set_script(lua, &this, &name, func)
|
||||
},
|
||||
)?,
|
||||
)?;
|
||||
m.set(
|
||||
"GetScript",
|
||||
lua.create_function(|lua, (this, name): (Table, String)| get_script(lua, &this, &name))?,
|
||||
)?;
|
||||
// RegisterForDrag(...varargs of button names) — the drag-gesture twin of `RegisterForClicks`
|
||||
// (decision 0216 §3), but on the SHARED table: any Frame can be a drag source, not just a
|
||||
// Button. Replace-the-set semantics (empty varargs clears); `crate::script::cursor`'s
|
||||
// arm/start/release path consults the set case-insensitively (the `RegisterForClicks`
|
||||
// precedent). Pruning on frame destroy: see [`Model::drag_registered`]'s doc — nothing in
|
||||
// this engine destroys a frame yet, so this map is in the same boat as `scripts`/
|
||||
// `frame_events`.
|
||||
m.set(
|
||||
"RegisterForDrag",
|
||||
lua.create_function(|lua, (this, args): (Table, MultiValue)| {
|
||||
let h = frame_handle_of(lua, &this)?;
|
||||
let mut set = HashSet::new();
|
||||
for v in args.iter() {
|
||||
if let Value::String(s) = v {
|
||||
set.insert(s.to_str()?.to_string());
|
||||
}
|
||||
}
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model");
|
||||
model.drag_registered.insert(h, set);
|
||||
Ok(())
|
||||
})?,
|
||||
)?;
|
||||
// Regions
|
||||
m.set(
|
||||
"CreateTexture",
|
||||
lua.create_function(
|
||||
|lua, (this, name, layer): (Table, Option<String>, Option<String>)| {
|
||||
create_region(lua, &this, RegionKind::Texture, name, layer)
|
||||
},
|
||||
)?,
|
||||
)?;
|
||||
m.set(
|
||||
"CreateFontString",
|
||||
lua.create_function(
|
||||
|lua, (this, name, layer): (Table, Option<String>, Option<String>)| {
|
||||
create_region(lua, &this, RegionKind::FontString, name, layer)
|
||||
},
|
||||
)?,
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_script(lua: &Lua, this: &Table, name: &str, func: Option<Function>) -> mlua::Result<()> {
|
||||
let kind = SCRIPT_KINDS
|
||||
.iter()
|
||||
.copied()
|
||||
.find(|&k| k.eq_ignore_ascii_case(name))
|
||||
.ok_or_else(|| mlua::Error::runtime(format!("SetScript: unsupported script '{name}'")))?;
|
||||
let h = frame_handle_of(lua, this)?;
|
||||
let id = lua.app_data_mut::<Model>().expect("model").frame_id(h);
|
||||
|
||||
// Store the closure Lua-side (REG_SCRIPTS[id][kind]); update the Rust presence mirror.
|
||||
let scripts: Table = lua.named_registry_value(REG_SCRIPTS)?;
|
||||
let per: Table = match scripts.get::<Value>(id)? {
|
||||
Value::Table(t) => t,
|
||||
_ => {
|
||||
let t = lua.create_table()?;
|
||||
scripts.set(id, t.clone())?;
|
||||
t
|
||||
}
|
||||
};
|
||||
match func {
|
||||
Some(f) => {
|
||||
per.set(kind, f)?;
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model");
|
||||
model.scripts.entry(h).or_default().insert(kind);
|
||||
}
|
||||
None => {
|
||||
per.set(kind, Value::Nil)?;
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model");
|
||||
if let Some(set) = model.scripts.get_mut(&h) {
|
||||
set.remove(&kind);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_script(lua: &Lua, this: &Table, name: &str) -> mlua::Result<Value> {
|
||||
let kind = match SCRIPT_KINDS
|
||||
.iter()
|
||||
.copied()
|
||||
.find(|&k| k.eq_ignore_ascii_case(name))
|
||||
{
|
||||
Some(k) => k,
|
||||
None => return Ok(Value::Nil),
|
||||
};
|
||||
let id = decode_id(this)?;
|
||||
let scripts: Table = lua.named_registry_value(REG_SCRIPTS)?;
|
||||
match scripts.get::<Value>(id)? {
|
||||
Value::Table(t) => t.get::<Value>(kind),
|
||||
_ => Ok(Value::Nil),
|
||||
}
|
||||
}
|
||||
|
||||
fn create_region(
|
||||
lua: &Lua,
|
||||
this: &Table,
|
||||
kind: RegionKind,
|
||||
name: Option<String>,
|
||||
layer: Option<String>,
|
||||
) -> mlua::Result<Table> {
|
||||
let owner = frame_handle_of(lua, this)?;
|
||||
let dl = layer
|
||||
.as_deref()
|
||||
.and_then(draw_layer_from_str)
|
||||
.unwrap_or_default();
|
||||
|
||||
let id = {
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model");
|
||||
let rh = model
|
||||
.arena
|
||||
.create_region(owner, kind, dl, 0)
|
||||
.ok_or_else(|| mlua::Error::runtime("CreateTexture/FontString: dead owner frame"))?;
|
||||
model.region_data.insert(rh, RegionData::default());
|
||||
model.region_id(rh)
|
||||
};
|
||||
|
||||
let wrapper = region_wrapper(lua, id)?;
|
||||
if let Some(name) = name {
|
||||
publish_global(lua, &name, &wrapper)?;
|
||||
// Publish into the region-name registry too (first-wins, the frame rule) — this is what
|
||||
// lets a sibling region's SetPoint name us as its `relativeTo` (see `resolve_target`).
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model");
|
||||
model.region_names.entry(name).or_insert(id);
|
||||
}
|
||||
Ok(wrapper)
|
||||
}
|
||||
@@ -0,0 +1,420 @@
|
||||
//! Frame method-table cluster: Show/Hide/visibility, identity/hierarchy (`GetName`/`GetParent`/
|
||||
//! `SetParent`), strata/level/scale/alpha, the Backdrop mechanism (`SetBackdrop`/`SetBackdropColor`/
|
||||
//! `SetBackdropBorderColor`), and mouse-enable. Split out of [`super`] purely for size — the shared
|
||||
//! id/handle plumbing, `CreateFrame`, and the method-table wiring stay there; this module's
|
||||
//! [`install`] just populates its share of the one shared method table.
|
||||
|
||||
use mlua::{Lua, Table, Value};
|
||||
|
||||
use crate::order::Strata;
|
||||
use crate::script::{event, Backdrop, Insets, Model};
|
||||
|
||||
use super::{decode_id, frame_handle_of, frame_wrapper, strata_from_str};
|
||||
|
||||
/// Populate `m`'s visibility/hierarchy/strata/backdrop/mouse methods (see the module doc).
|
||||
pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
// Show / Hide / visibility
|
||||
m.set(
|
||||
"Show",
|
||||
lua.create_function(|lua, this: Table| set_shown(lua, &this, true))?,
|
||||
)?;
|
||||
m.set(
|
||||
"Hide",
|
||||
lua.create_function(|lua, this: Table| set_shown(lua, &this, false))?,
|
||||
)?;
|
||||
// SetShown(bool) — the live API's branchless Show/Hide (a consensus call across the 0068
|
||||
// target addons; Lua truthiness, so SetShown(nil) hides).
|
||||
m.set(
|
||||
"SetShown",
|
||||
lua.create_function(|lua, (this, shown): (Table, Value)| {
|
||||
let show = !matches!(shown, Value::Nil | Value::Boolean(false));
|
||||
set_shown(lua, &this, show)
|
||||
})?,
|
||||
)?;
|
||||
m.set(
|
||||
"IsShown",
|
||||
lua.create_function(|lua, this: Table| {
|
||||
let h = frame_handle_of(lua, &this)?;
|
||||
let model = lua.app_data_ref::<Model>().expect("model");
|
||||
Ok(model.arena.frame(h).map(|f| f.shown).unwrap_or(false))
|
||||
})?,
|
||||
)?;
|
||||
m.set(
|
||||
"IsVisible",
|
||||
lua.create_function(|lua, this: Table| {
|
||||
let h = frame_handle_of(lua, &this)?;
|
||||
let model = lua.app_data_ref::<Model>().expect("model");
|
||||
Ok(model
|
||||
.arena
|
||||
.frame(h)
|
||||
.map(|f| f.effective_visible)
|
||||
.unwrap_or(false))
|
||||
})?,
|
||||
)?;
|
||||
// Identity / hierarchy
|
||||
m.set(
|
||||
"GetName",
|
||||
lua.create_function(|lua, this: Table| {
|
||||
let h = frame_handle_of(lua, &this)?;
|
||||
let name = {
|
||||
let model = lua.app_data_ref::<Model>().expect("model");
|
||||
model.arena.frame(h).and_then(|f| f.name.clone())
|
||||
};
|
||||
match name {
|
||||
Some(n) => Ok(Value::String(lua.create_string(&n)?)),
|
||||
None => Ok(Value::Nil),
|
||||
}
|
||||
})?,
|
||||
)?;
|
||||
// GetID/SetID — the app-meaning-free numeric label (XML `id=`, the client's `+0xb4`): a
|
||||
// dropdown row's list position, a tab index. Default 0 (see `Frame::wow_id`).
|
||||
m.set(
|
||||
"GetID",
|
||||
lua.create_function(|lua, this: Table| {
|
||||
let h = frame_handle_of(lua, &this)?;
|
||||
let model = lua.app_data_ref::<Model>().expect("model");
|
||||
Ok(model.arena.frame(h).map(|f| f.wow_id).unwrap_or(0))
|
||||
})?,
|
||||
)?;
|
||||
m.set(
|
||||
"SetID",
|
||||
lua.create_function(|lua, (this, id): (Table, i64)| {
|
||||
let h = frame_handle_of(lua, &this)?;
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model");
|
||||
if let Some(f) = model.arena.frame_mut(h) {
|
||||
f.wow_id = id;
|
||||
}
|
||||
Ok(())
|
||||
})?,
|
||||
)?;
|
||||
m.set(
|
||||
"GetParent",
|
||||
lua.create_function(|lua, this: Table| {
|
||||
let h = frame_handle_of(lua, &this)?;
|
||||
let parent_id = {
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model");
|
||||
model
|
||||
.arena
|
||||
.frame(h)
|
||||
.and_then(|f| f.parent)
|
||||
.map(|p| model.frame_id(p))
|
||||
};
|
||||
match parent_id {
|
||||
Some(pid) => Ok(Value::Table(frame_wrapper(lua, pid)?)),
|
||||
None => Ok(Value::Nil),
|
||||
}
|
||||
})?,
|
||||
)?;
|
||||
m.set(
|
||||
"SetParent",
|
||||
lua.create_function(|lua, (this, parent): (Table, Value)| {
|
||||
let h = frame_handle_of(lua, &this)?;
|
||||
let new_parent = {
|
||||
let model = lua.app_data_ref::<Model>().expect("model");
|
||||
match &parent {
|
||||
Value::Table(t) => decode_id(t)
|
||||
.ok()
|
||||
.and_then(|id| model.id_to_frame.get(&id).copied()),
|
||||
Value::String(s) => {
|
||||
s.to_str().ok().and_then(|n| model.arena.lookup(n.as_ref()))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
};
|
||||
let changed = {
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model");
|
||||
model.arena.set_parent(h, new_parent)
|
||||
};
|
||||
event::fire_visibility_changes(lua, changed);
|
||||
Ok(())
|
||||
})?,
|
||||
)?;
|
||||
// Strata / level / scale / alpha
|
||||
m.set(
|
||||
"SetFrameStrata",
|
||||
lua.create_function(|lua, (this, strata): (Table, String)| {
|
||||
let h = frame_handle_of(lua, &this)?;
|
||||
let s = strata_from_str(&strata)
|
||||
.ok_or_else(|| mlua::Error::runtime(format!("unknown frameStrata '{strata}'")))?;
|
||||
lua.app_data_mut::<Model>()
|
||||
.expect("model")
|
||||
.arena
|
||||
.set_frame_strata(h, s);
|
||||
Ok(())
|
||||
})?,
|
||||
)?;
|
||||
m.set(
|
||||
"GetFrameStrata",
|
||||
lua.create_function(|lua, this: Table| {
|
||||
let h = frame_handle_of(lua, &this)?;
|
||||
let model = lua.app_data_ref::<Model>().expect("model");
|
||||
let s = model.arena.frame(h).map(|f| f.strata).unwrap_or_default();
|
||||
Ok(strata_name(s).to_string())
|
||||
})?,
|
||||
)?;
|
||||
m.set(
|
||||
"SetFrameLevel",
|
||||
lua.create_function(|lua, (this, level): (Table, i64)| {
|
||||
let h = frame_handle_of(lua, &this)?;
|
||||
let lvl = level.clamp(0, i64::from(u16::MAX)) as u16;
|
||||
lua.app_data_mut::<Model>()
|
||||
.expect("model")
|
||||
.arena
|
||||
.set_frame_level(h, lvl, true);
|
||||
Ok(())
|
||||
})?,
|
||||
)?;
|
||||
m.set(
|
||||
"GetFrameLevel",
|
||||
lua.create_function(|lua, this: Table| {
|
||||
let h = frame_handle_of(lua, &this)?;
|
||||
let model = lua.app_data_ref::<Model>().expect("model");
|
||||
Ok(i64::from(
|
||||
model.arena.frame(h).map(|f| f.level).unwrap_or(0),
|
||||
))
|
||||
})?,
|
||||
)?;
|
||||
m.set(
|
||||
"SetScale",
|
||||
lua.create_function(|lua, (this, scale): (Table, f32)| {
|
||||
let h = frame_handle_of(lua, &this)?;
|
||||
lua.app_data_mut::<Model>()
|
||||
.expect("model")
|
||||
.arena
|
||||
.set_scale(h, scale);
|
||||
Ok(())
|
||||
})?,
|
||||
)?;
|
||||
m.set(
|
||||
"GetScale",
|
||||
lua.create_function(|lua, this: Table| {
|
||||
let h = frame_handle_of(lua, &this)?;
|
||||
let model = lua.app_data_ref::<Model>().expect("model");
|
||||
Ok(model.arena.frame(h).map(|f| f.scale).unwrap_or(1.0))
|
||||
})?,
|
||||
)?;
|
||||
m.set(
|
||||
"SetAlpha",
|
||||
lua.create_function(|lua, (this, alpha): (Table, f32)| {
|
||||
let h = frame_handle_of(lua, &this)?;
|
||||
// The 1.12 API clamps to 0..1 — ref Lua leans on it (fade code that passes a 0..255
|
||||
// alpha stays fully opaque until the value falls below 1/255).
|
||||
lua.app_data_mut::<Model>()
|
||||
.expect("model")
|
||||
.arena
|
||||
.set_alpha(h, alpha.clamp(0.0, 1.0));
|
||||
Ok(())
|
||||
})?,
|
||||
)?;
|
||||
m.set(
|
||||
"GetAlpha",
|
||||
lua.create_function(|lua, this: Table| {
|
||||
let h = frame_handle_of(lua, &this)?;
|
||||
let model = lua.app_data_ref::<Model>().expect("model");
|
||||
Ok(model.arena.frame(h).map(|f| f.alpha).unwrap_or(1.0))
|
||||
})?,
|
||||
)?;
|
||||
// Backdrop (backdrop-mechanism.md): SetBackdrop(table|nil) installs (or, with nil, tears down)
|
||||
// the frame's tiled bg + 8-piece border plate. The two color setters tint the bg / all 8 border
|
||||
// pieces (never the reverse — spec §4). The Lua-table SetBackdrop defaults both colors to WHITE
|
||||
// (the ctor), so a caller must SetBackdropColor after to tint (the tooltip's OnLoad does).
|
||||
m.set(
|
||||
"SetBackdrop",
|
||||
lua.create_function(|lua, (this, arg): (Table, Value)| {
|
||||
let h = frame_handle_of(lua, &this)?;
|
||||
let bd = match arg {
|
||||
Value::Nil => None,
|
||||
Value::Table(t) => Some(backdrop_from_table(&t)?),
|
||||
other => {
|
||||
return Err(mlua::Error::runtime(format!(
|
||||
"SetBackdrop: expected a table or nil, got {}",
|
||||
other.type_name()
|
||||
)))
|
||||
}
|
||||
};
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model");
|
||||
match bd {
|
||||
Some(b) => {
|
||||
model.backdrops.insert(h, b);
|
||||
}
|
||||
None => {
|
||||
model.backdrops.remove(&h);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
})?,
|
||||
)?;
|
||||
m.set(
|
||||
"SetBackdropColor",
|
||||
lua.create_function(
|
||||
|lua, (this, r, g, b, a): (Table, f32, f32, f32, Option<f32>)| {
|
||||
let h = frame_handle_of(lua, &this)?;
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model");
|
||||
if let Some(bd) = model.backdrops.get_mut(&h) {
|
||||
bd.bg_color = [r, g, b, a.unwrap_or(1.0)];
|
||||
}
|
||||
Ok(())
|
||||
},
|
||||
)?,
|
||||
)?;
|
||||
m.set(
|
||||
"SetBackdropBorderColor",
|
||||
lua.create_function(
|
||||
|lua, (this, r, g, b, a): (Table, f32, f32, f32, Option<f32>)| {
|
||||
let h = frame_handle_of(lua, &this)?;
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model");
|
||||
if let Some(bd) = model.backdrops.get_mut(&h) {
|
||||
bd.border_color = [r, g, b, a.unwrap_or(1.0)];
|
||||
}
|
||||
Ok(())
|
||||
},
|
||||
)?,
|
||||
)?;
|
||||
m.set(
|
||||
"GetBackdropColor",
|
||||
lua.create_function(|lua, this: Table| {
|
||||
let h = frame_handle_of(lua, &this)?;
|
||||
let model = lua.app_data_ref::<Model>().expect("model");
|
||||
match model.backdrops.get(&h) {
|
||||
Some(bd) => Ok((
|
||||
bd.bg_color[0],
|
||||
bd.bg_color[1],
|
||||
bd.bg_color[2],
|
||||
bd.bg_color[3],
|
||||
)),
|
||||
None => Ok((1.0, 1.0, 1.0, 1.0)),
|
||||
}
|
||||
})?,
|
||||
)?;
|
||||
m.set(
|
||||
"GetBackdropBorderColor",
|
||||
lua.create_function(|lua, this: Table| {
|
||||
let h = frame_handle_of(lua, &this)?;
|
||||
let model = lua.app_data_ref::<Model>().expect("model");
|
||||
match model.backdrops.get(&h) {
|
||||
Some(bd) => Ok((
|
||||
bd.border_color[0],
|
||||
bd.border_color[1],
|
||||
bd.border_color[2],
|
||||
bd.border_color[3],
|
||||
)),
|
||||
None => Ok((1.0, 1.0, 1.0, 1.0)),
|
||||
}
|
||||
})?,
|
||||
)?;
|
||||
// Mouse interaction (EnableMouse gates hit-testing; keyboard focus is out of scope)
|
||||
m.set(
|
||||
"EnableMouse",
|
||||
lua.create_function(|lua, (this, enable): (Table, bool)| {
|
||||
let h = frame_handle_of(lua, &this)?;
|
||||
lua.app_data_mut::<Model>()
|
||||
.expect("model")
|
||||
.arena
|
||||
.set_mouse_enabled(h, enable);
|
||||
Ok(())
|
||||
})?,
|
||||
)?;
|
||||
m.set(
|
||||
"IsMouseEnabled",
|
||||
lua.create_function(|lua, this: Table| {
|
||||
let h = frame_handle_of(lua, &this)?;
|
||||
let model = lua.app_data_ref::<Model>().expect("model");
|
||||
Ok(model.arena.is_mouse_enabled(h))
|
||||
})?,
|
||||
)?;
|
||||
// Clamp-to-screen (`0x776c00`/`0x776cb0`, geometry flags bit4 — layout.md): the layout resolve
|
||||
// keeps the frame's assembled rect inside the window, size preserved. GameTooltip frames
|
||||
// default true by construction (widget::Frame::clamped_to_screen — decision 0352).
|
||||
m.set(
|
||||
"SetClampedToScreen",
|
||||
lua.create_function(|lua, (this, clamp): (Table, bool)| {
|
||||
let h = frame_handle_of(lua, &this)?;
|
||||
lua.app_data_mut::<Model>()
|
||||
.expect("model")
|
||||
.arena
|
||||
.set_clamped_to_screen(h, clamp);
|
||||
Ok(())
|
||||
})?,
|
||||
)?;
|
||||
m.set(
|
||||
"IsClampedToScreen",
|
||||
lua.create_function(|lua, this: Table| {
|
||||
let h = frame_handle_of(lua, &this)?;
|
||||
let model = lua.app_data_ref::<Model>().expect("model");
|
||||
Ok(model.arena.is_clamped_to_screen(h))
|
||||
})?,
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Parse a Lua `SetBackdrop` table into a [`Backdrop`] (backdrop-mechanism.md §1). The keys read —
|
||||
/// exactly the compiled reader's set — are `bgFile`/`edgeFile` (strings), `tile` (boolean, default
|
||||
/// false), `tileSize`/`edgeSize` (numbers; a missing number leaves the ctor default: tileSize 0,
|
||||
/// edgeSize 32), and `insets{left,right,top,bottom}` (numbers, each default 0). A non-string file or
|
||||
/// non-number size is treated as absent (the client's per-key type gate). Colors stay the ctor
|
||||
/// white — SetBackdrop never reads a color key; the color setters do that.
|
||||
fn backdrop_from_table(t: &Table) -> mlua::Result<Backdrop> {
|
||||
let mut bd = Backdrop::default();
|
||||
if let Ok(Value::String(s)) = t.get::<Value>("bgFile") {
|
||||
bd.bg_file = Some(s.to_str()?.to_string());
|
||||
}
|
||||
if let Ok(Value::String(s)) = t.get::<Value>("edgeFile") {
|
||||
bd.edge_file = Some(s.to_str()?.to_string());
|
||||
}
|
||||
// `tile` — Lua truthiness (nil/false ⇒ false; a missing key reads as Nil).
|
||||
bd.tile = !matches!(
|
||||
t.get::<Value>("tile").unwrap_or(Value::Nil),
|
||||
Value::Nil | Value::Boolean(false)
|
||||
);
|
||||
if let Ok(Value::Number(n)) = t.get::<Value>("tileSize") {
|
||||
bd.tile_size = n as f32;
|
||||
} else if let Ok(Value::Integer(n)) = t.get::<Value>("tileSize") {
|
||||
bd.tile_size = n as f32;
|
||||
}
|
||||
if let Ok(Value::Number(n)) = t.get::<Value>("edgeSize") {
|
||||
bd.edge_size = n as f32;
|
||||
} else if let Ok(Value::Integer(n)) = t.get::<Value>("edgeSize") {
|
||||
bd.edge_size = n as f32;
|
||||
}
|
||||
if let Ok(Value::Table(ins)) = t.get::<Value>("insets") {
|
||||
let num = |k: &str| -> f32 {
|
||||
match ins.get::<Value>(k) {
|
||||
Ok(Value::Number(n)) => n as f32,
|
||||
Ok(Value::Integer(n)) => n as f32,
|
||||
_ => 0.0,
|
||||
}
|
||||
};
|
||||
bd.insets = Insets {
|
||||
left: num("left"),
|
||||
right: num("right"),
|
||||
top: num("top"),
|
||||
bottom: num("bottom"),
|
||||
};
|
||||
}
|
||||
Ok(bd)
|
||||
}
|
||||
|
||||
fn strata_name(s: Strata) -> &'static str {
|
||||
match s {
|
||||
Strata::World => "WORLD",
|
||||
Strata::Background => "BACKGROUND",
|
||||
Strata::Low => "LOW",
|
||||
Strata::Medium => "MEDIUM",
|
||||
Strata::High => "HIGH",
|
||||
Strata::Dialog => "DIALOG",
|
||||
Strata::Fullscreen => "FULLSCREEN",
|
||||
Strata::FullscreenDialog => "FULLSCREEN_DIALOG",
|
||||
Strata::Tooltip => "TOOLTIP",
|
||||
Strata::Blizzard => "BLIZZARD",
|
||||
}
|
||||
}
|
||||
|
||||
fn set_shown(lua: &Lua, this: &Table, shown: bool) -> mlua::Result<()> {
|
||||
let h = frame_handle_of(lua, this)?;
|
||||
let changed = {
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model");
|
||||
model.arena.set_shown(h, shown)
|
||||
};
|
||||
event::fire_visibility_changes(lua, changed);
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,357 @@
|
||||
//! Frame method-table cluster: anchoring and size — `SetPoint`/`ClearAllPoints`/`GetPoint`/
|
||||
//! `SetAllPoints`/`SetWidth`/`SetHeight`/`SetSize`/`GetWidth`/`GetHeight` and the resolved-edge
|
||||
//! readers (`GetLeft`/`GetRight`/`GetTop`/`GetBottom`). Split out of [`super`] purely for size — see
|
||||
//! its module doc for the shared id/handle plumbing and method-table wiring.
|
||||
|
||||
use mlua::{Lua, Table, Value};
|
||||
|
||||
use crate::layout::{Anchor, Point};
|
||||
use crate::script::{Model, SCREEN};
|
||||
use crate::widget::FrameHandle;
|
||||
|
||||
use super::{as_f32, decode_id, frame_handle_of, frame_wrapper, point_from_str, point_name};
|
||||
|
||||
/// Populate `m`'s layout (anchor/size) methods (see the module doc).
|
||||
pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
// Layout: SetPoint / ClearAllPoints / SetSize / SetWidth / SetHeight / GetWidth / GetHeight
|
||||
m.set(
|
||||
"SetPoint",
|
||||
lua.create_function(
|
||||
|lua, (this, p, a2, a3, a4, a5): (Table, String, Value, Value, Value, Value)| {
|
||||
set_point(lua, &this, &p, [a2, a3, a4, a5])
|
||||
},
|
||||
)?,
|
||||
)?;
|
||||
m.set(
|
||||
"ClearAllPoints",
|
||||
lua.create_function(|lua, this: Table| {
|
||||
let h = frame_handle_of(lua, &this)?;
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model");
|
||||
if let Some(input) = model.layout_inputs.get_mut(&h) {
|
||||
input.anchors.clear();
|
||||
}
|
||||
Ok(())
|
||||
})?,
|
||||
)?;
|
||||
// GetPoint([n]) → point, relativeTo, relativePoint, xOfs, yOfs — the n-th (1-based, default
|
||||
// first) anchor. relativeTo is nil when the target is the screen root (the client returns
|
||||
// UIParent there; benilla has no UIParent wrapper yet — stated, a consensus-list call).
|
||||
m.set(
|
||||
"GetPoint",
|
||||
lua.create_function(|lua, (this, n): (Table, Option<i64>)| {
|
||||
let h = frame_handle_of(lua, &this)?;
|
||||
let anchor = {
|
||||
let model = lua.app_data_ref::<Model>().expect("model");
|
||||
let idx = (n.unwrap_or(1).max(1) - 1) as usize;
|
||||
model
|
||||
.layout_inputs
|
||||
.get(&h)
|
||||
.and_then(|i| i.anchors.get(idx))
|
||||
.cloned()
|
||||
};
|
||||
let Some(a) = anchor else {
|
||||
return Ok((Value::Nil, Value::Nil, Value::Nil, Value::Nil, Value::Nil));
|
||||
};
|
||||
let rel = if a.relative_to == SCREEN {
|
||||
Value::Nil
|
||||
} else {
|
||||
Value::Table(frame_wrapper(lua, a.relative_to)?)
|
||||
};
|
||||
Ok((
|
||||
Value::String(lua.create_string(point_name(a.point))?),
|
||||
rel,
|
||||
Value::String(lua.create_string(point_name(a.relative_point))?),
|
||||
Value::Number(f64::from(a.x_off)),
|
||||
Value::Number(f64::from(a.y_off)),
|
||||
))
|
||||
})?,
|
||||
)?;
|
||||
// SetAllPoints([relativeTo]) — pin TOPLEFT+BOTTOMRIGHT to the target (default: the parent),
|
||||
// the XML `setAllPoints="true"` behavior as a method (rf24 `0x767800`'s SetAllPoints path).
|
||||
m.set(
|
||||
"SetAllPoints",
|
||||
lua.create_function(|lua, (this, target): (Table, Value)| {
|
||||
let h = frame_handle_of(lua, &this)?;
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model");
|
||||
let rel_id: u32 = match &target {
|
||||
// Same id_to_region fallback as `set_point` above (and `region.rs`'s own
|
||||
// `resolve_target`, which already checked both) — a table wrapper may name a region.
|
||||
Value::Table(t) => decode_id(t)
|
||||
.ok()
|
||||
.filter(|id| {
|
||||
model.id_to_frame.contains_key(id) || model.id_to_region.contains_key(id)
|
||||
})
|
||||
.unwrap_or_else(|| default_parent_id(&mut model, h)),
|
||||
Value::String(s) => {
|
||||
// Frames first, then the region-name registry: the real XML anchors frames to
|
||||
// REGIONS too (gossip option rows → the greeting FontString) — resolve() binds
|
||||
// region targets in its second round.
|
||||
let named = s.to_str().ok().and_then(|n| {
|
||||
model
|
||||
.arena
|
||||
.lookup(n.as_ref())
|
||||
.map(|hh| model.frame_id(hh))
|
||||
.or_else(|| model.region_names.get(n.as_ref()).copied())
|
||||
});
|
||||
named.unwrap_or_else(|| default_parent_id(&mut model, h))
|
||||
}
|
||||
_ => default_parent_id(&mut model, h),
|
||||
};
|
||||
let input = model.layout_inputs.entry(h).or_default();
|
||||
input.anchors.clear();
|
||||
input.anchors.push(Anchor::new(
|
||||
Point::TopLeft,
|
||||
rel_id,
|
||||
Point::TopLeft,
|
||||
0.0,
|
||||
0.0,
|
||||
));
|
||||
input.anchors.push(Anchor::new(
|
||||
Point::BottomRight,
|
||||
rel_id,
|
||||
Point::BottomRight,
|
||||
0.0,
|
||||
0.0,
|
||||
));
|
||||
Ok(())
|
||||
})?,
|
||||
)?;
|
||||
m.set(
|
||||
"SetWidth",
|
||||
lua.create_function(|lua, (this, w): (Table, f32)| {
|
||||
let h = frame_handle_of(lua, &this)?;
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model");
|
||||
model.layout_inputs.entry(h).or_default().width = w;
|
||||
Ok(())
|
||||
})?,
|
||||
)?;
|
||||
m.set(
|
||||
"SetHeight",
|
||||
lua.create_function(|lua, (this, ht): (Table, f32)| {
|
||||
let h = frame_handle_of(lua, &this)?;
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model");
|
||||
model.layout_inputs.entry(h).or_default().height = ht;
|
||||
Ok(())
|
||||
})?,
|
||||
)?;
|
||||
m.set(
|
||||
"SetSize",
|
||||
lua.create_function(|lua, (this, w, ht): (Table, f32, f32)| {
|
||||
let h = frame_handle_of(lua, &this)?;
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model");
|
||||
let input = model.layout_inputs.entry(h).or_default();
|
||||
input.width = w;
|
||||
input.height = ht;
|
||||
Ok(())
|
||||
})?,
|
||||
)?;
|
||||
m.set(
|
||||
"GetWidth",
|
||||
lua.create_function(|lua, this: Table| {
|
||||
let h = frame_handle_of(lua, &this)?;
|
||||
let model = lua.app_data_ref::<Model>().expect("model");
|
||||
Ok(size_read(&model, h, true))
|
||||
})?,
|
||||
)?;
|
||||
m.set(
|
||||
"GetHeight",
|
||||
lua.create_function(|lua, this: Table| {
|
||||
let h = frame_handle_of(lua, &this)?;
|
||||
let model = lua.app_data_ref::<Model>().expect("model");
|
||||
Ok(size_read(&model, h, false))
|
||||
})?,
|
||||
)?;
|
||||
|
||||
// GetCenter() → centerX, centerY — the resolved rect's center in LOCAL units (y-up; screen ÷
|
||||
// the frame's effective scale — the client's convention: coordinate getters report the frame's
|
||||
// own scaled space, and callers divide GetCursorPosition (screen px) by GetEffectiveScale to
|
||||
// meet them there; the ref world map's hover math does exactly that). nil pair before the
|
||||
// first resolve, like the edge readers.
|
||||
m.set(
|
||||
"GetCenter",
|
||||
lua.create_function(|lua, this: Table| {
|
||||
let h = frame_handle_of(lua, &this)?;
|
||||
let model = lua.app_data_ref::<Model>().expect("model");
|
||||
let inv = 1.0 / eff_scale(&model, h);
|
||||
Ok(match model.resolved.get(&h) {
|
||||
Some(r) => (
|
||||
Value::Number(f64::from((r.left + r.right) * 0.5 * inv)),
|
||||
Value::Number(f64::from((r.bottom + r.top) * 0.5 * inv)),
|
||||
),
|
||||
None => (Value::Nil, Value::Nil),
|
||||
})
|
||||
})?,
|
||||
)?;
|
||||
|
||||
// GetEffectiveScale() — the frame's real effective scale (parentScale · ownScale, the arena's
|
||||
// propagated product). benilla has no uiScale CVar, so the root factor is 1; a SetScale'd
|
||||
// subtree (the windowed world map) reports its true factor, and the reference's
|
||||
// `GetCursorPosition()/GetEffectiveScale()` transcriptions convert screen→local correctly.
|
||||
m.set(
|
||||
"GetEffectiveScale",
|
||||
lua.create_function(|lua, this: Table| {
|
||||
let h = frame_handle_of(lua, &this)?;
|
||||
let model = lua.app_data_ref::<Model>().expect("model");
|
||||
Ok(f64::from(eff_scale(&model, h)))
|
||||
})?,
|
||||
)?;
|
||||
|
||||
// GetLeft/GetRight/GetTop/GetBottom — the frame's RESOLVED edges in LOCAL units (y-up, so
|
||||
// GetBottom is height-from-screen-bottom, the era semantics; `GetRect 0x768320`'s Lua faces,
|
||||
// layout.rs's module doc; screen ÷ effective scale like GetCenter). `nil` before the first
|
||||
// resolve — callers treat that as "not laid out yet" (the ref's own FauxScrollFrame code
|
||||
// nil-checks these too).
|
||||
for (name, pick) in [
|
||||
("GetLeft", 0u8),
|
||||
("GetRight", 1u8),
|
||||
("GetTop", 2u8),
|
||||
("GetBottom", 3u8),
|
||||
] {
|
||||
m.set(
|
||||
name,
|
||||
lua.create_function(move |lua, this: Table| {
|
||||
let h = frame_handle_of(lua, &this)?;
|
||||
let model = lua.app_data_mut::<Model>().expect("model");
|
||||
let inv = 1.0 / eff_scale(&model, h);
|
||||
Ok(model.resolved.get(&h).map(|r| {
|
||||
inv * match pick {
|
||||
0 => r.left,
|
||||
1 => r.right,
|
||||
2 => r.top,
|
||||
_ => r.bottom,
|
||||
}
|
||||
}))
|
||||
})?,
|
||||
)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The frame's effective scale, ε-guarded (a zero would poison the local-unit division).
|
||||
fn eff_scale(model: &Model, h: FrameHandle) -> f32 {
|
||||
let s = model
|
||||
.arena
|
||||
.frame(h)
|
||||
.map(|f| f.effective_scale)
|
||||
.unwrap_or(1.0);
|
||||
if s.abs() < 1e-6 {
|
||||
1.0
|
||||
} else {
|
||||
s
|
||||
}
|
||||
}
|
||||
|
||||
/// `GetWidth`/`GetHeight`: the resolved rect's span in LOCAL units (screen ÷ effective scale — the
|
||||
/// client returns the value as authored, and `SetWidth(w)` on a scaled frame resolves to `w·scale`
|
||||
/// screen px) if `resolve` has produced one, else the explicit size the frame was given
|
||||
/// (`SetWidth`/`SetHeight`/`SetSize`, already local) — matching the client's "0 = derive".
|
||||
fn size_read(model: &Model, h: FrameHandle, width: bool) -> f32 {
|
||||
if let Some(r) = model.resolved.get(&h) {
|
||||
let span = if width { r.width() } else { r.height() };
|
||||
return span / eff_scale(model, h);
|
||||
}
|
||||
model
|
||||
.layout_inputs
|
||||
.get(&h)
|
||||
.map(|i| if width { i.width } else { i.height })
|
||||
.unwrap_or(0.0)
|
||||
}
|
||||
|
||||
/// `SetPoint(point [, relativeTo [, relativePoint]] [, x, y])` (RF-0023/RF-0024). Defaults per the
|
||||
/// client: `relativeTo` = the frame's parent (the screen root if top-level), `relativePoint` =
|
||||
/// `point`. Overload disambiguated by argument *type* (a string/table after `point` is `relativeTo`;
|
||||
/// a number is the first offset).
|
||||
fn set_point(lua: &Lua, this: &Table, point: &str, rest: [Value; 4]) -> mlua::Result<()> {
|
||||
let point = point_from_str(point)
|
||||
.ok_or_else(|| mlua::Error::runtime(format!("SetPoint: unknown point '{point}'")))?;
|
||||
let h = frame_handle_of(lua, this)?;
|
||||
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model");
|
||||
|
||||
// relativeTo: a table (wrapper) or a string (name), else default to the parent/screen. A table
|
||||
// wrapper may name a FRAME or a REGION (a Button anchored to a FontString by direct reference —
|
||||
// e.g. `button:SetPoint("TOPLEFT", someFontString, "BOTTOMLEFT", ...)` — is exactly as legal in
|
||||
// the real client as anchoring to a frame, and the string-branch below already accepts both), so
|
||||
// this must check id_to_region too, not just id_to_frame: an id that only lives in id_to_region
|
||||
// used to fail the frame-only filter and silently fall back to the default parent, misdirecting
|
||||
// the anchor with no error (caught via the quest-log reward rows, which anchor Buttons to
|
||||
// FontString regions by table reference — QuestLogFrame.xml's own comment on the port).
|
||||
let mut cursor = 0usize;
|
||||
let rel_to_id: u32 = match rest.first() {
|
||||
Some(Value::Table(t)) => {
|
||||
cursor = 1;
|
||||
decode_id(t)
|
||||
.ok()
|
||||
.filter(|id| {
|
||||
model.id_to_frame.contains_key(id) || model.id_to_region.contains_key(id)
|
||||
})
|
||||
.unwrap_or_else(|| default_parent_id(&mut model, h))
|
||||
}
|
||||
Some(Value::String(s)) => {
|
||||
cursor = 1;
|
||||
// Frames first, then regions by name (see SetAllPoints above — same rationale).
|
||||
let named = s.to_str().ok().and_then(|n| {
|
||||
model
|
||||
.arena
|
||||
.lookup(n.as_ref())
|
||||
.map(|hh| model.frame_id(hh))
|
||||
.or_else(|| model.region_names.get(n.as_ref()).copied())
|
||||
});
|
||||
named.unwrap_or_else(|| {
|
||||
// The parent fallback is the client's behavior, but a *named* target that
|
||||
// doesn't resolve is almost always a bug (a typo, or an XML forward reference —
|
||||
// anchors resolve at SetPoint time, so a target must be declared before its
|
||||
// dependents). Say so instead of silently misdirecting the anchor: this exact
|
||||
// silence cost a hunt twice (QuestLogFrame's reward rows, ItemTextFrame's
|
||||
// scrollbar track).
|
||||
let who = model
|
||||
.arena
|
||||
.frame(h)
|
||||
.and_then(|f| f.name.clone())
|
||||
.unwrap_or_else(|| "<anonymous>".into());
|
||||
model.warnings.push(format!(
|
||||
"SetPoint({who}): relativeTo '{}' does not resolve — anchored to the parent",
|
||||
s.to_str().ok().as_deref().unwrap_or("<non-utf8>")
|
||||
));
|
||||
default_parent_id(&mut model, h)
|
||||
})
|
||||
}
|
||||
// An *explicit* `nil` relativeTo (`SetPoint(point, nil, relPoint, x, y)`) means "default
|
||||
// target" but still occupies its argument slot — consume it so the relativePoint/offsets
|
||||
// that follow line up. Only a leading *number* (the `SetPoint(point, x, y)` overload) or a
|
||||
// truly-absent arg leaves the cursor at 0.
|
||||
Some(Value::Nil) => {
|
||||
cursor = 1;
|
||||
default_parent_id(&mut model, h)
|
||||
}
|
||||
_ => default_parent_id(&mut model, h),
|
||||
};
|
||||
|
||||
// relativePoint: a string in the next slot, else defaults to `point`.
|
||||
let mut rel_point = point;
|
||||
if let Some(Value::String(s)) = rest.get(cursor) {
|
||||
if let Some(p) = s.to_str().ok().and_then(|n| point_from_str(n.as_ref())) {
|
||||
rel_point = p;
|
||||
cursor += 1;
|
||||
}
|
||||
}
|
||||
|
||||
let x = rest.get(cursor).map(as_f32).unwrap_or(0.0);
|
||||
let y = rest.get(cursor + 1).map(as_f32).unwrap_or(0.0);
|
||||
|
||||
let input = model.layout_inputs.entry(h).or_default();
|
||||
input.anchors.retain(|a| a.point != point);
|
||||
input
|
||||
.anchors
|
||||
.push(Anchor::new(point, rel_to_id, rel_point, x, y));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The default `relativeTo` id for a `SetPoint` with no explicit target: the frame's parent id, or
|
||||
/// [`SCREEN`] if it is top-level (the client anchors top-level frames to `UIParent`/the screen root).
|
||||
fn default_parent_id(model: &mut Model, h: FrameHandle) -> u32 {
|
||||
match model.arena.frame(h).and_then(|f| f.parent) {
|
||||
Some(p) => model.frame_id(p),
|
||||
None => SCREEN,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user