An XML state texture installs a fresh region, as in 1.12.1 (#940)
An XML state texture on an instance over a template went through the Lua path leg, which reuses the populated region, so the template's TexCoords, size, tint, blend and anchors stayed on the new art. In 1.12.1 `Button::LoadXML` (`0x7788c0`) builds a fresh region for each state tag through the `<Layers>` adder (`0x6f26f0`) and hands it to the slot store (`0x778fd0`, `0x779110`; the checked pair `0x785330`, `0x785440`), which destroys the region it held. A fresh region has TexCoords 0..1 and BLEND (`0x76fc40`, `0x76fc64`), and an XML highlight keeps its own blend (`0x7789a4`). The XML leg now builds the region as a `<Layers>` texture and installs it, freeing the old one; the Lua leg is unchanged. An XML highlight with no `alphaMode` now draws BLEND. Tests: all six tags on a CheckButton template, the name kept, the Lua leg's region kept. Fixes #875 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
3bfec89085
commit
0fc40d3f87
@@ -2091,6 +2091,223 @@ mod loader_tests {
|
||||
"blank, not painted"
|
||||
);
|
||||
}
|
||||
|
||||
/// `LoadXML` re-enters itself for the template before the instance (`0x76985c`), and each
|
||||
/// state-texture element builds a new region through the `<Layers>` adder (`0x6f26f0`) that the
|
||||
/// slot store installs, destroying the one it displaces (`0x778fd0` via `0x77900a`, the
|
||||
/// highlight's `0x779110`, the checked pair's `0x785330`/`0x785440`). So an instance's element
|
||||
/// keeps nothing of the template's: the fresh region's TexCoords, alpha and BLEND (`0x76fc40`;
|
||||
/// the highlight keeps its own, `0x7789a4`), and `SetAllPoints` with no anchors (`0x7701c0`).
|
||||
#[test]
|
||||
fn an_instance_state_texture_replaces_the_templates() {
|
||||
const TAGS: [(&str, &str); 6] = [
|
||||
("NormalTexture", "GetNormalTexture"),
|
||||
("PushedTexture", "GetPushedTexture"),
|
||||
("DisabledTexture", "GetDisabledTexture"),
|
||||
("HighlightTexture", "GetHighlightTexture"),
|
||||
("CheckedTexture", "GetCheckedTexture"),
|
||||
("DisabledCheckedTexture", "GetDisabledCheckedTexture"),
|
||||
];
|
||||
let template: String = TAGS
|
||||
.iter()
|
||||
.map(|(tag, _)| {
|
||||
format!(
|
||||
r#"<{tag} name="$parentTmpl{tag}" file="Interface\Tmpl{tag}" alpha="0.5" alphaMode="MOD">
|
||||
<Size><AbsDimension x="10" y="10"/></Size>
|
||||
<Anchors><Anchor point="TOPLEFT"><Offset><AbsDimension x="3" y="-3"/></Offset></Anchor></Anchors>
|
||||
<TexCoords left="0.25" right="0.75" top="0.25" bottom="0.75"/>
|
||||
</{tag}>"#
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
let instance: String = TAGS
|
||||
.iter()
|
||||
.map(|(tag, _)| format!(r#"<{tag} file="Interface\Inst{tag}"/>"#))
|
||||
.collect();
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_screen_size(800.0, 600.0);
|
||||
let doc = parse(&format!(
|
||||
r#"<Ui>
|
||||
<CheckButton name="StateTmpl" virtual="true">
|
||||
<Size><AbsDimension x="40" y="20"/></Size>
|
||||
{template}
|
||||
</CheckButton>
|
||||
<CheckButton name="StateInst" inherits="StateTmpl">
|
||||
<Anchors><Anchor point="CENTER"/></Anchors>
|
||||
{instance}
|
||||
</CheckButton>
|
||||
<CheckButton name="LaidOutInst" inherits="StateTmpl">
|
||||
<Anchors><Anchor point="BOTTOMLEFT"/></Anchors>
|
||||
<NormalTexture file="Interface\Laid" alpha="0.25">
|
||||
<Size><AbsDimension x="20" y="16"/></Size>
|
||||
<Anchors><Anchor point="CENTER"/></Anchors>
|
||||
</NormalTexture>
|
||||
</CheckButton>
|
||||
</Ui>"#
|
||||
));
|
||||
let report = load(&s, &doc, &no_files);
|
||||
assert!(report.errors.is_empty(), "errors: {:?}", report.errors);
|
||||
|
||||
for (tag, getter) in TAGS {
|
||||
let got: (String, Vec<f32>, String, f32, i64, String, bool, f32, f32) = s
|
||||
.eval(&format!(
|
||||
r#"local t = StateInst:{getter}()
|
||||
local p, rel, rp, x, y = t:GetPoint(1)
|
||||
return t:GetTexture(), {{ t:GetTexCoord() }}, t:GetBlendMode(), t:GetAlpha(),
|
||||
t:GetNumPoints(), p .. ">" .. rp, rel == StateInst, x, y"#
|
||||
))
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
got,
|
||||
(
|
||||
format!("Interface\\Inst{tag}"),
|
||||
vec![0.0, 0.0, 0.0, 1.0, 1.0, 0.0, 1.0, 1.0],
|
||||
"BLEND".to_string(),
|
||||
1.0,
|
||||
2,
|
||||
"TOPLEFT>TOPLEFT".to_string(),
|
||||
true,
|
||||
0.0,
|
||||
0.0,
|
||||
),
|
||||
"<{tag}>: a fresh region, filling the button"
|
||||
);
|
||||
// The template's region was destroyed: its global is a dead handle, as the
|
||||
// reference's unregister leaves it (`0x701cd0`).
|
||||
assert!(
|
||||
!s.eval::<bool>(&format!(
|
||||
"return (pcall(StateInstTmpl{tag}.GetTexture, StateInstTmpl{tag}))"
|
||||
))
|
||||
.unwrap(),
|
||||
"<{tag}>: the template's region is gone"
|
||||
);
|
||||
}
|
||||
assert_eq!(
|
||||
s.eval::<i64>("return table.getn({ StateInst:GetRegions() })")
|
||||
.unwrap(),
|
||||
TAGS.len() as i64,
|
||||
"one region per slot, none of the template's left over"
|
||||
);
|
||||
|
||||
// An instance element's own size, anchors and alpha lay the fresh region out.
|
||||
let laid: (i64, String, f32, f32, f32, Vec<f32>) = s
|
||||
.eval(
|
||||
r#"local t = LaidOutInst:GetNormalTexture()
|
||||
return t:GetNumPoints(), (t:GetPoint(1)), t:GetWidth(), t:GetHeight(),
|
||||
t:GetAlpha(), { t:GetTexCoord() }"#,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
laid,
|
||||
(
|
||||
1,
|
||||
"CENTER".to_string(),
|
||||
20.0,
|
||||
16.0,
|
||||
0.25,
|
||||
vec![0.0, 0.0, 0.0, 1.0, 1.0, 0.0, 1.0, 1.0]
|
||||
)
|
||||
);
|
||||
|
||||
s.resolve();
|
||||
let drawn: Vec<String> = s
|
||||
.extract()
|
||||
.into_iter()
|
||||
.filter_map(|q| match q.content {
|
||||
QuadContent::Texture { path: Some(p), .. } => Some(p),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
assert!(
|
||||
!drawn.iter().any(|p| p.contains("Tmpl")),
|
||||
"no template art left drawing: {drawn:?}"
|
||||
);
|
||||
assert!(s.errors().is_empty(), "{:?}", s.errors());
|
||||
}
|
||||
|
||||
/// An instance's state texture named as its template's carries the name: `GetName()` reads the
|
||||
/// region's own (`SetName 0x76c650`, from `0x76fdf0`). The global stays the first holder's, as
|
||||
/// `0x701bd0` publishes only into an empty one (`0x701c8a`), so it is the destroyed template
|
||||
/// region's dead handle.
|
||||
#[test]
|
||||
fn a_same_named_instance_state_texture_keeps_its_name() {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_screen_size(800.0, 600.0);
|
||||
let doc = parse(
|
||||
r#"<Ui>
|
||||
<Button name="NamedTmpl" virtual="true">
|
||||
<Size><AbsDimension x="40" y="20"/></Size>
|
||||
<NormalTexture name="$parentNormal" file="Interface\Tmpl"/>
|
||||
</Button>
|
||||
<Button name="NamedInst" inherits="NamedTmpl">
|
||||
<Anchors><Anchor point="CENTER"/></Anchors>
|
||||
<NormalTexture name="$parentNormal" file="Interface\Inst"/>
|
||||
</Button>
|
||||
</Ui>"#,
|
||||
);
|
||||
let report = load(&s, &doc, &no_files);
|
||||
assert!(report.errors.is_empty(), "errors: {:?}", report.errors);
|
||||
let got: (String, String, bool) = s
|
||||
.eval(
|
||||
r#"local t = NamedInst:GetNormalTexture()
|
||||
return tostring(t:GetName()), t:GetTexture(),
|
||||
(pcall(NamedInstNormal.GetTexture, NamedInstNormal))"#,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
got,
|
||||
(
|
||||
"NamedInstNormal".to_string(),
|
||||
"Interface\\Inst".to_string(),
|
||||
false
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/// The Lua path leg (`0x781970` → `0x781b3f` → `0x778f10`) only swaps the file of a slot that
|
||||
/// holds a region (`0x770200`): the region, its TexCoords, blend, alpha and anchors stay.
|
||||
#[test]
|
||||
fn a_lua_path_on_a_loaded_state_texture_keeps_the_region() {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_screen_size(800.0, 600.0);
|
||||
let doc = parse(
|
||||
r#"<Ui>
|
||||
<Button name="PathBtn">
|
||||
<Size><AbsDimension x="40" y="20"/></Size>
|
||||
<Anchors><Anchor point="CENTER"/></Anchors>
|
||||
<NormalTexture file="Interface\Before" alphaMode="MOD">
|
||||
<Size><AbsDimension x="10" y="10"/></Size>
|
||||
<Anchors><Anchor point="TOPLEFT"/></Anchors>
|
||||
<TexCoords left="0.25" right="0.75" top="0.25" bottom="0.75"/>
|
||||
</NormalTexture>
|
||||
</Button>
|
||||
</Ui>"#,
|
||||
);
|
||||
let report = load(&s, &doc, &no_files);
|
||||
assert!(report.errors.is_empty(), "errors: {:?}", report.errors);
|
||||
let got: (bool, String, Vec<f32>, String, f32, i64, String) = s
|
||||
.eval(
|
||||
r#"local held = PathBtn:GetNormalTexture()
|
||||
held:SetAlpha(0.5)
|
||||
PathBtn:SetNormalTexture("Interface\\After")
|
||||
local t = PathBtn:GetNormalTexture()
|
||||
return t == held, t:GetTexture(), { t:GetTexCoord() }, t:GetBlendMode(),
|
||||
t:GetAlpha(), t:GetNumPoints(), (t:GetPoint(1))"#,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
got,
|
||||
(
|
||||
true,
|
||||
"Interface\\After".to_string(),
|
||||
vec![0.25, 0.25, 0.25, 0.75, 0.75, 0.25, 0.75, 0.75],
|
||||
"MOD".to_string(),
|
||||
0.5,
|
||||
1,
|
||||
"TOPLEFT".to_string(),
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
mod layer_blend_tests {
|
||||
|
||||
@@ -4,9 +4,7 @@ use crate::framexml::{self, Element};
|
||||
use crate::script::LabelFont;
|
||||
|
||||
use super::regions::FontAttrs;
|
||||
use super::{
|
||||
abs_dim, abs_value, children_named, children_named_any, color_of, tex_coords_of, Loader,
|
||||
};
|
||||
use super::{abs_dim, abs_value, children_named, children_named_any, color_of, Loader};
|
||||
|
||||
impl Loader<'_> {
|
||||
/// `<ScrollFrame>`'s `<ScrollChild>` (`LoadXML 0x786bc0`): after the base load (`0x769820`) it
|
||||
@@ -106,52 +104,25 @@ impl Loader<'_> {
|
||||
{
|
||||
return;
|
||||
}
|
||||
let tex = |tag: &str, method: &str, this: &mut Self| {
|
||||
// Each state-texture element is a new region from the `<Layers>` texture adder (`0x6f26f0`,
|
||||
// `0x778903`): named, laid out, painted and given its implicit all-points as any layer
|
||||
// texture is, then handed to the slot store, which destroys the region it displaces. An
|
||||
// instance's element over its template's therefore starts fresh: `LoadXML` runs the
|
||||
// template's level first (`0x76985c`), and nothing of that region survives the instance's.
|
||||
// The Lua path leg differs: it repaints the slot's region and keeps its state (`0x778f10`).
|
||||
let tex = |tag: &str, this: &mut Self| {
|
||||
for raw in children_named(el, tag) {
|
||||
// A state texture may inherit a virtual `<Texture>`, the stock button kit's only
|
||||
// form (`UIPanelButtonUpTexture`); it expands like a `<Layers>` region.
|
||||
let expanded = this.expand_region(raw);
|
||||
let t = &expanded;
|
||||
// The reference sends all four state textures through the `<Layers>` texture adder
|
||||
// (`0x6f26f0`, from `0x778903`), so a `<Color>` is the region's own colour fill; the
|
||||
// Lua setter has no colour form (`0x781970`). `""` creates the slot unpainted.
|
||||
let color = children_named(t, "Color").next().map(color_of);
|
||||
match t.attr("file") {
|
||||
Some(file) => this.call(wrapper, method, file.to_string(), dbg),
|
||||
None => this.call(wrapper, method, String::new(), dbg),
|
||||
}
|
||||
let getter = method.replacen("Set", "Get", 1);
|
||||
if let Ok(region) = wrapper.call_method::<Table>(getter.as_str(), ()) {
|
||||
if let (None, Some(c)) = (t.attr("file"), color) {
|
||||
this.call_region(®ion, "SetTexture", (c[0], c[1], c[2], c[3]), dbg);
|
||||
}
|
||||
if let Some(mode) = t.attr("alphaMode") {
|
||||
this.call_region(®ion, "SetBlendMode", mode.to_string(), dbg);
|
||||
}
|
||||
// The setter gave the region the runtime path's implicit anchors, but the XML
|
||||
// path places the authored ones first and the implicit step after (`0x778903`):
|
||||
// clear, lay out, then re-run it, or a leftover implicit corner would pin it.
|
||||
this.call_region(®ion, "ClearAllPoints", (), dbg);
|
||||
this.apply_region_layout(t, ®ion, self_name, dbg, FontAttrs::Own);
|
||||
if let Err(e) = crate::script::implicit_creation_anchor_lua(this.lua, ®ion) {
|
||||
this.report
|
||||
.errors
|
||||
.push(format!("{dbg}: implicit anchor: {e}"));
|
||||
}
|
||||
if let Some(tc) = tex_coords_of(t) {
|
||||
this.call_region(®ion, "SetTexCoord", tc, dbg);
|
||||
}
|
||||
// A named state texture is published both ways: the region registry for its
|
||||
// `GetName()`, `_G` for `getglobal` and a sibling's `relativeTo`.
|
||||
if let Some(rname) = t.name().map(|raw| framexml::resolve_name(raw, self_name))
|
||||
{
|
||||
crate::script::region::publish_region_name(this.lua(), &rname, ®ion);
|
||||
if let Err(e) = this.lua().globals().set(rname.clone(), region) {
|
||||
this.report
|
||||
.warnings
|
||||
.push(format!("{dbg}: state-texture global '{rname}': {e}"));
|
||||
}
|
||||
}
|
||||
let Some(region) =
|
||||
this.build_texture_child(raw, wrapper, self_name, "ARTWORK", dbg)
|
||||
else {
|
||||
continue; // build_texture_child already reported why
|
||||
};
|
||||
this.install_texture(wrapper, tag, ®ion, None, dbg);
|
||||
// Freeing the displaced region took its name out of the registry `GetName()`
|
||||
// reads, which a replacement of the same name takes back: the reference keeps the
|
||||
// name on the region (`0x76c650`). `_G` keeps its first holder (`0x701c8a`).
|
||||
if let Some(rname) = raw.name().map(|n| framexml::resolve_name(n, self_name)) {
|
||||
crate::script::region::publish_region_name(this.lua(), &rname, ®ion);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -207,13 +178,13 @@ impl Loader<'_> {
|
||||
}
|
||||
}
|
||||
}
|
||||
tex("NormalTexture", "SetNormalTexture", self);
|
||||
tex("PushedTexture", "SetPushedTexture", self);
|
||||
tex("DisabledTexture", "SetDisabledTexture", self);
|
||||
tex("HighlightTexture", "SetHighlightTexture", self);
|
||||
tex("NormalTexture", self);
|
||||
tex("PushedTexture", self);
|
||||
tex("DisabledTexture", self);
|
||||
tex("HighlightTexture", self);
|
||||
if is_check {
|
||||
tex("CheckedTexture", "SetCheckedTexture", self);
|
||||
tex("DisabledCheckedTexture", "SetDisabledCheckedTexture", self);
|
||||
tex("CheckedTexture", self);
|
||||
tex("DisabledCheckedTexture", self);
|
||||
if let Some(c) = el.attr("checked") {
|
||||
let checked = c.eq_ignore_ascii_case("true") || c == "1";
|
||||
self.call(wrapper, "SetChecked", checked, dbg);
|
||||
|
||||
@@ -360,6 +360,40 @@ fn install_slot(
|
||||
) -> mlua::Result<bool> {
|
||||
let h = frame_handle_of(lua, this)?;
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
|
||||
install_slot_model(&mut model, h, slot, incoming)
|
||||
}
|
||||
|
||||
/// The loader's install of the texture a state-texture element built through the `<Layers>`
|
||||
/// adder (`0x6f26f0`), never through a Lua setter: `Button::LoadXML` hands each new region to the
|
||||
/// slot store (`0x77890e`, `0x778940`, `0x778972`; the highlight's `0x7789a9`; the checked pair's
|
||||
/// `0x7851dd`, `0x78520e`), which destroys whatever the slot held, an inherited template's
|
||||
/// included, so a handle still held on it goes dead (`0x701cd0` clears its `[0]`). The highlight's
|
||||
/// store sets the region's own blend (`0x7789a4`), so it keeps it.
|
||||
pub(super) fn install_loaded(
|
||||
model: &mut Model,
|
||||
h: FrameHandle,
|
||||
tag: &str,
|
||||
rh: RegionHandle,
|
||||
) -> mlua::Result<()> {
|
||||
let slot = match tag {
|
||||
"NormalTexture" => Slot::Normal,
|
||||
"PushedTexture" => Slot::Pushed,
|
||||
"DisabledTexture" => Slot::Disabled,
|
||||
"HighlightTexture" => Slot::Highlight,
|
||||
"CheckedTexture" => Slot::Checked,
|
||||
"DisabledCheckedTexture" => Slot::DisabledChecked,
|
||||
_ => return Err(mlua::Error::runtime(format!("not a Button texture: {tag}"))),
|
||||
};
|
||||
install_slot_model(model, h, slot, Some(rh)).map(|_| ())
|
||||
}
|
||||
|
||||
/// [`install_slot`] on the model.
|
||||
fn install_slot_model(
|
||||
model: &mut Model,
|
||||
h: FrameHandle,
|
||||
slot: Slot,
|
||||
incoming: Option<RegionHandle>,
|
||||
) -> mlua::Result<bool> {
|
||||
if !matches!(
|
||||
model.arena.frame(h).map(|f| &f.kind_state),
|
||||
Some(KindState::Button(_))
|
||||
@@ -368,7 +402,7 @@ fn install_slot(
|
||||
}
|
||||
let writes = std::cell::RefCell::new(crate::widget::ShownWrites::default());
|
||||
let changed = super::region::install_into_slot(
|
||||
&mut model,
|
||||
model,
|
||||
h,
|
||||
incoming,
|
||||
slot.shape().1,
|
||||
@@ -384,7 +418,7 @@ fn install_slot(
|
||||
}
|
||||
},
|
||||
)?;
|
||||
super::region::apply_shown_writes(&mut model, writes.into_inner());
|
||||
super::region::apply_shown_writes(model, writes.into_inner());
|
||||
Ok(changed)
|
||||
}
|
||||
|
||||
|
||||
@@ -210,9 +210,9 @@ pub(super) fn set_texture_path(model: &mut Model, rh: RegionHandle, path: &str)
|
||||
model.touch_layout_art(rh);
|
||||
}
|
||||
|
||||
/// The loader's install of the texture a `<BarTexture>`, `<ThumbTexture>` or ColorSelect texture
|
||||
/// element (`tag`) built, as the widget's object, never through a Lua setter. `layer` is the
|
||||
/// element's `drawLayer` for the bar and the thumb.
|
||||
/// The loader's install of the texture a `<BarTexture>`, `<ThumbTexture>`, Button state-texture or
|
||||
/// ColorSelect texture element (`tag`) built, as the widget's object, never through a Lua setter.
|
||||
/// `layer` is the element's `drawLayer` for the bar and the thumb.
|
||||
pub(crate) fn install_loaded_texture(
|
||||
lua: &Lua,
|
||||
this: &Table,
|
||||
@@ -227,6 +227,12 @@ pub(crate) fn install_loaded_texture(
|
||||
match tag {
|
||||
"BarTexture" => super::statusbar::install_loaded(&mut model, h, rh, layer),
|
||||
"ThumbTexture" => super::slider::install_loaded(&mut model, h, rh, layer),
|
||||
"NormalTexture"
|
||||
| "PushedTexture"
|
||||
| "DisabledTexture"
|
||||
| "HighlightTexture"
|
||||
| "CheckedTexture"
|
||||
| "DisabledCheckedTexture" => super::button::install_loaded(&mut model, h, tag, rh),
|
||||
_ => super::colorselect::install_loaded(&mut model, h, tag, rh),
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user