Button state textures have one shown bit; widget setters take 1.12 forms (#917)
A Button state texture's visibility was kept apart from its region's shown bit, so a script `Show()` on a non-current state texture drew nothing. The bar, thumb and ColorSelect setters took a colour form and accepted arguments the reference rejects, the loader built their textures from `file=` or `<Color>` alone, and a bar texture answered nil to `GetTexCoordModifiesRect`. In the reference, a region has one shown bit that `Show`, `Hide` and the state switch all write. The setters fork on object, path, nil or usage. The loader builds each widget texture as a full Texture element, and installing a bar texture sets its texcoord flag and gives it all points. All of these now match: one shown bit, the reference's argument forms, full-element textures with their names and texcoords, and no tint without a bar. Tests pin each. Fixes #909 Fixes #908 Fixes #575 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5.5
parent
fa5568dcdc
commit
935b465c76
@@ -708,7 +708,7 @@ impl Loader<'_> {
|
||||
self.apply_title_region(el, wrapper, self_name, dbg_name);
|
||||
// 5b · per-kind LoadXML extras (EditBox `0x779fb0`), each gated on the element's own tag,
|
||||
// so a Frame wearing a <Button> template skips the Button steps.
|
||||
self.apply_statusbar(el, wrapper, dbg_name);
|
||||
self.apply_statusbar(el, wrapper, self_name, dbg_name);
|
||||
self.apply_slider(el, wrapper, self_name, dbg_name);
|
||||
self.apply_colorselect(el, wrapper, self_name, dbg_name);
|
||||
self.apply_button(el, wrapper, self_name, dbg_name);
|
||||
|
||||
@@ -40,50 +40,79 @@ impl Loader<'_> {
|
||||
));
|
||||
continue;
|
||||
}
|
||||
let rname: Option<String> = region
|
||||
.name()
|
||||
.map(|raw| framexml::resolve_name(raw, parent_name));
|
||||
let method = if is_texture {
|
||||
"CreateTexture"
|
||||
} else {
|
||||
"CreateFontString"
|
||||
};
|
||||
let region_wrapper: Table =
|
||||
match wrapper.call_method(method, (rname, Some(level.clone()))) {
|
||||
Ok(w) => w,
|
||||
Err(e) => {
|
||||
self.report.fault(format!("{dbg}: {method}: {e}"));
|
||||
continue;
|
||||
}
|
||||
};
|
||||
if region.attr_bool("hidden") {
|
||||
if let Err(e) = region_wrapper.call_method::<()>("Hide", ()) {
|
||||
self.report.fault(format!("{dbg}: region Hide: {e}"));
|
||||
}
|
||||
}
|
||||
if let Some(a) = region
|
||||
.attr("alpha")
|
||||
.and_then(|v| v.trim().parse::<f32>().ok())
|
||||
{
|
||||
self.call_region(®ion_wrapper, "SetAlpha", a, dbg);
|
||||
}
|
||||
self.apply_region_layout(region, ®ion_wrapper, parent_name, dbg, FontAttrs::Own);
|
||||
if is_fontstring {
|
||||
// Before the visual pass, so an explicit `<Color>` beats the font object's.
|
||||
self.apply_fontstring_font(region, ®ion_wrapper, dbg);
|
||||
}
|
||||
self.apply_region_visual(region, ®ion_wrapper, is_texture, dbg);
|
||||
// The implicit creation anchor, run after the region's LoadXML as the
|
||||
// reference does (`0x7701c0`, `0x771480`); an anchored region is left alone.
|
||||
if let Err(e) =
|
||||
crate::script::implicit_creation_anchor_lua(self.lua, ®ion_wrapper)
|
||||
{
|
||||
self.report.fault(format!("{dbg}: implicit anchor: {e}"));
|
||||
}
|
||||
self.build_region(region, wrapper, parent_name, &level, is_texture, dbg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The texture a widget element's child (`<BarTexture>`, `<ThumbTexture>`, a ColorSelect
|
||||
/// texture) names, built as the shared texture adder does (`0x6f26f0`): `inherits=` expanded,
|
||||
/// then the same region a `<Layers>` texture is, named, published, laid out, filled and given
|
||||
/// its implicit all-points. The widget then installs the object it returns.
|
||||
pub(super) fn build_texture_child(
|
||||
&mut self,
|
||||
raw: &Element,
|
||||
wrapper: &Table,
|
||||
parent_name: &str,
|
||||
level: &str,
|
||||
dbg: &str,
|
||||
) -> Option<Table> {
|
||||
let expanded = self.expand_region(raw);
|
||||
self.build_region(&expanded, wrapper, parent_name, level, true, dbg)
|
||||
}
|
||||
|
||||
/// One `<Layer>` region on `wrapper`, created on `level` and loaded: its name (`$parent`
|
||||
/// expanded, published as a global), `hidden`, `alpha`, layout, font and visual, then the
|
||||
/// implicit creation anchor, run after the region's LoadXML as the reference does (`0x7701c0`,
|
||||
/// `0x771480`); an anchored region is left alone. `None` when the creation raised.
|
||||
fn build_region(
|
||||
&mut self,
|
||||
region: &Element,
|
||||
wrapper: &Table,
|
||||
parent_name: &str,
|
||||
level: &str,
|
||||
is_texture: bool,
|
||||
dbg: &str,
|
||||
) -> Option<Table> {
|
||||
let rname: Option<String> = region
|
||||
.name()
|
||||
.map(|raw| framexml::resolve_name(raw, parent_name));
|
||||
let method = if is_texture {
|
||||
"CreateTexture"
|
||||
} else {
|
||||
"CreateFontString"
|
||||
};
|
||||
let region_wrapper: Table =
|
||||
match wrapper.call_method(method, (rname, Some(level.to_string()))) {
|
||||
Ok(w) => w,
|
||||
Err(e) => {
|
||||
self.report.fault(format!("{dbg}: {method}: {e}"));
|
||||
return None;
|
||||
}
|
||||
};
|
||||
if region.attr_bool("hidden") {
|
||||
if let Err(e) = region_wrapper.call_method::<()>("Hide", ()) {
|
||||
self.report.fault(format!("{dbg}: region Hide: {e}"));
|
||||
}
|
||||
}
|
||||
if let Some(a) = region
|
||||
.attr("alpha")
|
||||
.and_then(|v| v.trim().parse::<f32>().ok())
|
||||
{
|
||||
self.call_region(®ion_wrapper, "SetAlpha", a, dbg);
|
||||
}
|
||||
self.apply_region_layout(region, ®ion_wrapper, parent_name, dbg, FontAttrs::Own);
|
||||
if !is_texture {
|
||||
// Before the visual pass, so an explicit `<Color>` beats the font object's.
|
||||
self.apply_fontstring_font(region, ®ion_wrapper, dbg);
|
||||
}
|
||||
self.apply_region_visual(region, ®ion_wrapper, is_texture, dbg);
|
||||
if let Err(e) = crate::script::implicit_creation_anchor_lua(self.lua, ®ion_wrapper) {
|
||||
self.report.fault(format!("{dbg}: implicit anchor: {e}"));
|
||||
}
|
||||
Some(region_wrapper)
|
||||
}
|
||||
|
||||
/// A frame's direct-child `<FontString>`, its "special" string (a message frame's line font, an
|
||||
/// EditBox's text), on the OVERLAY layer.
|
||||
pub(super) fn apply_special_fontstrings(
|
||||
|
||||
@@ -129,6 +129,25 @@ impl Loader<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Install the texture a widget element's child (`<BarTexture>`, `<ThumbTexture>`, a ColorSelect
|
||||
/// texture) built as the widget's object, through the engine's own install, recording (not
|
||||
/// propagating) any error: the Lua setters take the object too, but the loader needs no Lua
|
||||
/// call to say it.
|
||||
pub(super) fn install_texture(
|
||||
&mut self,
|
||||
wrapper: &Table,
|
||||
tag: &str,
|
||||
texture: &Table,
|
||||
layer: Option<&str>,
|
||||
dbg: &str,
|
||||
) {
|
||||
if let Err(e) =
|
||||
crate::script::install_loaded_texture(self.lua, wrapper, tag, texture, layer)
|
||||
{
|
||||
self.report.fault(format!("{dbg}: <{tag}>: {e}"));
|
||||
}
|
||||
}
|
||||
|
||||
/// Call a region-wrapper method, recording (not propagating) any error.
|
||||
pub(super) fn call_region(
|
||||
&mut self,
|
||||
|
||||
@@ -46,7 +46,13 @@ impl Loader<'_> {
|
||||
|
||||
/// `<StatusBar>` (`0x782ef0`): a reversed `minValue`/`maxValue` pair is swapped, as
|
||||
/// `SetMinMaxValues` also does; `drawLayer` is the bar texture's layer, ARTWORK by default.
|
||||
pub(super) fn apply_statusbar(&mut self, el: &Element, wrapper: &Table, dbg: &str) {
|
||||
pub(super) fn apply_statusbar(
|
||||
&mut self,
|
||||
el: &Element,
|
||||
wrapper: &Table,
|
||||
self_name: &str,
|
||||
dbg: &str,
|
||||
) {
|
||||
if !el.tag.eq_ignore_ascii_case("StatusBar") {
|
||||
return;
|
||||
}
|
||||
@@ -63,25 +69,13 @@ impl Loader<'_> {
|
||||
if let Some(o) = el.attr("orientation") {
|
||||
self.call(wrapper, "SetOrientation", o.to_string(), dbg);
|
||||
}
|
||||
let layer = el.attr("drawLayer").map(str::to_string);
|
||||
let layer = el.attr("drawLayer");
|
||||
for bar in children_named(el, "BarTexture") {
|
||||
let color = children_named(bar, "Color").next().map(color_of);
|
||||
if let Some(file) = bar.attr("file") {
|
||||
self.call(
|
||||
wrapper,
|
||||
"SetStatusBarTexture",
|
||||
(file.to_string(), layer.clone()),
|
||||
dbg,
|
||||
);
|
||||
// A `<Color>` beside `file=` is discarded, as on any texture (`0x76fe20`);
|
||||
// `<BarColor>` is the bar's tint.
|
||||
} else if let Some(c) = color {
|
||||
self.call(
|
||||
wrapper,
|
||||
"SetStatusBarTexture",
|
||||
(c[0], c[1], c[2], c[3]),
|
||||
dbg,
|
||||
);
|
||||
// Built as any texture element is, and installed as the bar on `drawLayer`, ARTWORK
|
||||
// without one (`0x782f12`); `<BarColor>` is the bar's tint.
|
||||
let level = layer.unwrap_or("ARTWORK");
|
||||
if let Some(tex) = self.build_texture_child(bar, wrapper, self_name, level, dbg) {
|
||||
self.install_texture(wrapper, "BarTexture", &tex, layer, dbg);
|
||||
}
|
||||
}
|
||||
for bc in children_named(el, "BarColor") {
|
||||
@@ -425,38 +419,14 @@ impl Loader<'_> {
|
||||
self.call(wrapper, "SetValue", v, dbg);
|
||||
}
|
||||
}
|
||||
let layer = el.attr("drawLayer").map(str::to_string);
|
||||
let layer = el.attr("drawLayer");
|
||||
for raw in children_named(el, "ThumbTexture") {
|
||||
// As for a Button's state textures: `inherits=` expands, and a named thumb is
|
||||
// published (`ScrollFrame_OnScrollRangeChanged` reads it by name).
|
||||
let expanded = self.expand_region(raw);
|
||||
let tt = &expanded;
|
||||
if let Some(file) = tt.attr("file") {
|
||||
self.call(
|
||||
wrapper,
|
||||
"SetThumbTexture",
|
||||
(file.to_string(), layer.clone()),
|
||||
dbg,
|
||||
);
|
||||
} else if let Some(c) = children_named(tt, "Color").next().map(color_of) {
|
||||
self.call(wrapper, "SetThumbTexture", (c[0], c[1], c[2], c[3]), dbg);
|
||||
}
|
||||
if let Ok(region) = wrapper.call_method::<Table>("GetThumbTexture", ()) {
|
||||
if let Some(mode) = tt.attr("alphaMode") {
|
||||
self.call_region(®ion, "SetBlendMode", mode.to_string(), dbg);
|
||||
}
|
||||
self.apply_region_layout(tt, ®ion, self_name, dbg, FontAttrs::Own);
|
||||
if let Some(tc) = tex_coords_of(tt) {
|
||||
self.call_region(®ion, "SetTexCoord", tc, dbg);
|
||||
}
|
||||
if let Some(rname) = tt.name().map(|raw| framexml::resolve_name(raw, self_name)) {
|
||||
crate::script::region::publish_region_name(self.lua(), &rname, ®ion);
|
||||
if let Err(e) = self.lua().globals().set(rname.clone(), region) {
|
||||
self.report
|
||||
.warnings
|
||||
.push(format!("{dbg}: thumb-texture global '{rname}': {e}"));
|
||||
}
|
||||
}
|
||||
// Built as any texture element is (a named thumb is published, which
|
||||
// `ScrollFrame_OnScrollRangeChanged` reads by name), then installed as the thumb on
|
||||
// `drawLayer`, OVERLAY without one; the install clears its anchors (`0x789893`).
|
||||
let level = layer.unwrap_or("OVERLAY");
|
||||
if let Some(tex) = self.build_texture_child(raw, wrapper, self_name, level, dbg) {
|
||||
self.install_texture(wrapper, "ThumbTexture", &tex, layer, dbg);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -474,58 +444,18 @@ impl Loader<'_> {
|
||||
if !el.tag.eq_ignore_ascii_case("ColorSelect") {
|
||||
return;
|
||||
}
|
||||
let layer = el.attr("drawLayer").map(str::to_string);
|
||||
for (tag, setter, getter) in [
|
||||
(
|
||||
"ColorWheelTexture",
|
||||
"SetColorWheelTexture",
|
||||
"GetColorWheelTexture",
|
||||
),
|
||||
(
|
||||
"ColorWheelThumbTexture",
|
||||
"SetColorWheelThumbTexture",
|
||||
"GetColorWheelThumbTexture",
|
||||
),
|
||||
(
|
||||
"ColorValueTexture",
|
||||
"SetColorValueTexture",
|
||||
"GetColorValueTexture",
|
||||
),
|
||||
(
|
||||
"ColorValueThumbTexture",
|
||||
"SetColorValueThumbTexture",
|
||||
"GetColorValueThumbTexture",
|
||||
),
|
||||
for (tag, level) in [
|
||||
("ColorWheelTexture", "ARTWORK"),
|
||||
("ColorWheelThumbTexture", "OVERLAY"),
|
||||
("ColorValueTexture", "ARTWORK"),
|
||||
("ColorValueThumbTexture", "OVERLAY"),
|
||||
] {
|
||||
for raw in children_named(el, tag) {
|
||||
let expanded = self.expand_region(raw);
|
||||
let tt = &expanded;
|
||||
if let Some(file) = tt.attr("file") {
|
||||
self.call(wrapper, setter, (file.to_string(), layer.clone()), dbg);
|
||||
} else if let Some(c) = children_named(tt, "Color").next().map(color_of) {
|
||||
self.call(wrapper, setter, (c[0], c[1], c[2], c[3]), dbg);
|
||||
} else {
|
||||
self.call(wrapper, setter, (), dbg);
|
||||
}
|
||||
if let Ok(region) = wrapper.call_method::<Table>(getter, ()) {
|
||||
if let Some(mode) = tt.attr("alphaMode") {
|
||||
self.call_region(®ion, "SetBlendMode", mode.to_string(), dbg);
|
||||
}
|
||||
self.apply_region_layout(tt, ®ion, self_name, dbg, FontAttrs::Own);
|
||||
if let Some(tc) = tex_coords_of(tt) {
|
||||
self.call_region(®ion, "SetTexCoord", tc, dbg);
|
||||
}
|
||||
if let Some(rname) = tt.name().map(|raw| framexml::resolve_name(raw, self_name))
|
||||
{
|
||||
// Published both ways: the stock `<ColorValueTexture>` anchors to
|
||||
// `ColorPickerWheel` by name.
|
||||
crate::script::region::publish_region_name(self.lua(), &rname, ®ion);
|
||||
if let Err(e) = self.lua().globals().set(rname.clone(), region) {
|
||||
self.report
|
||||
.warnings
|
||||
.push(format!("{dbg}: {tag} global '{rname}': {e}"));
|
||||
}
|
||||
}
|
||||
// The element has no `drawLayer` (`0x78b3f0` never reads it): the slot links its
|
||||
// texture on its own layer. The wheel and the strip are painted by the client
|
||||
// whatever file or colour the element names (`0x770300`).
|
||||
if let Some(tex) = self.build_texture_child(raw, wrapper, self_name, level, dbg) {
|
||||
self.install_texture(wrapper, tag, &tex, None, dbg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,17 +45,37 @@ impl Slot {
|
||||
}
|
||||
|
||||
/// The three state slots go through [`ButtonState::set_state_slot`] (`0x778fd0`), which also
|
||||
/// shows the texture when it is the current state's; the others are plain fields.
|
||||
fn set(self, bs: &mut ButtonState, rh: Option<crate::widget::RegionHandle>) {
|
||||
/// shows the texture when it is the current state's, and the two checked slots refresh the
|
||||
/// pair (`0x7854c0` from `0x785330`/`0x785440`); the others are plain fields. The bits the
|
||||
/// store writes come back for the caller to apply.
|
||||
fn set(
|
||||
self,
|
||||
bs: &mut ButtonState,
|
||||
rh: Option<crate::widget::RegionHandle>,
|
||||
) -> crate::widget::ShownWrites {
|
||||
match self {
|
||||
Slot::Normal => bs.set_state_slot(ButtonVisualState::Normal, rh),
|
||||
Slot::Pushed => bs.set_state_slot(ButtonVisualState::Pushed, rh),
|
||||
Slot::Disabled => bs.set_state_slot(ButtonVisualState::Disabled, rh),
|
||||
Slot::Normal => return bs.set_state_slot(ButtonVisualState::Normal, rh),
|
||||
Slot::Pushed => return bs.set_state_slot(ButtonVisualState::Pushed, rh),
|
||||
Slot::Disabled => return bs.set_state_slot(ButtonVisualState::Disabled, rh),
|
||||
Slot::Highlight => bs.highlight = rh,
|
||||
Slot::Checked => bs.checked_tex = rh,
|
||||
Slot::DisabledChecked => bs.disabled_checked = rh,
|
||||
Slot::Checked => {
|
||||
bs.checked_tex = rh;
|
||||
return bs.refresh_checked(bs.checked, true);
|
||||
}
|
||||
Slot::DisabledChecked => {
|
||||
bs.disabled_checked = rh;
|
||||
return bs.refresh_checked(bs.checked, true);
|
||||
}
|
||||
Slot::Text => bs.text = rh,
|
||||
}
|
||||
crate::widget::ShownWrites::default()
|
||||
}
|
||||
|
||||
/// The link's third argument (`0x77fd10`): every slot links its texture shown (the highlight
|
||||
/// `0x779139`, the label `0x778d49`) but the state and checked ones, which pass 0 and are
|
||||
/// shown by the switch that follows.
|
||||
fn links_shown(self) -> bool {
|
||||
matches!(self, Slot::Highlight | Slot::Text)
|
||||
}
|
||||
|
||||
/// The region kind and default layer: state textures under the label, the highlight in its own
|
||||
@@ -192,6 +212,19 @@ fn with_button<T>(
|
||||
}
|
||||
}
|
||||
|
||||
/// [`with_button`] for a state machine call that writes shown bits: the writes land in the regions
|
||||
/// once the button's borrow is released.
|
||||
fn with_button_shown(
|
||||
lua: &Lua,
|
||||
this: &Table,
|
||||
f: impl FnOnce(&mut ButtonState) -> crate::widget::ShownWrites,
|
||||
) -> mlua::Result<()> {
|
||||
let writes = with_button(lua, this, f)?;
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
|
||||
super::region::apply_shown_writes(&mut model, writes);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// `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
|
||||
@@ -204,7 +237,7 @@ fn set_enabled(lua: &Lua, this: &Table, on: bool) -> mlua::Result<()> {
|
||||
.arena
|
||||
.frame_mut(h)
|
||||
.ok_or_else(|| mlua::Error::runtime("stale frame handle"))?;
|
||||
match &mut frame.kind_state {
|
||||
let writes = match &mut frame.kind_state {
|
||||
KindState::Button(bs) => bs.set_enabled(on),
|
||||
_ => return Err(mlua::Error::runtime("not a Button")),
|
||||
};
|
||||
@@ -214,18 +247,23 @@ fn set_enabled(lua: &Lua, this: &Table, on: bool) -> mlua::Result<()> {
|
||||
} else {
|
||||
frame.disabled_layers |= bit;
|
||||
}
|
||||
super::region::apply_shown_writes(&mut model, writes);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Run an engine state edge over frame `h` if it is a live Button. The reference has six, all of
|
||||
/// `0x779790`'s call sites but Lua's: enable, disable, hide, press, release and drag start; the
|
||||
/// cursor entering or leaving a button is not one.
|
||||
pub(super) fn edge(model: &mut Model, h: FrameHandle, f: impl FnOnce(&mut ButtonState)) {
|
||||
if let Some(frame) = model.arena.frame_mut(h) {
|
||||
if let KindState::Button(bs) = &mut frame.kind_state {
|
||||
f(bs);
|
||||
}
|
||||
}
|
||||
pub(super) fn edge(
|
||||
model: &mut Model,
|
||||
h: FrameHandle,
|
||||
f: impl FnOnce(&mut ButtonState) -> crate::widget::ShownWrites,
|
||||
) {
|
||||
let writes = match model.arena.frame_mut(h).map(|f| &mut f.kind_state) {
|
||||
Some(KindState::Button(bs)) => f(bs),
|
||||
_ => return,
|
||||
};
|
||||
super::region::apply_shown_writes(model, writes);
|
||||
}
|
||||
|
||||
/// Whether a hover edge on `h` reaches the base notify, the HIGHLIGHT layer and the Lua
|
||||
@@ -283,13 +321,21 @@ fn ensure_slot(lua: &Lua, this: &Table, slot: Slot) -> mlua::Result<u32> {
|
||||
.arena
|
||||
.create_region(h, kind, layer, 0)
|
||||
.ok_or_else(|| mlua::Error::runtime("stale frame handle"))?;
|
||||
model.region_data.insert(rh, RegionData::default());
|
||||
// Born with its shown bit clear, as `0x76fc40` zeroes it; the link writes the slot's
|
||||
// own (`0x77fd10`), and the store shows a current state's texture (`0x779037`).
|
||||
model.region_data.insert(
|
||||
rh,
|
||||
RegionData {
|
||||
hidden: !slot.links_shown(),
|
||||
..RegionData::default()
|
||||
},
|
||||
);
|
||||
model.touch_layout_region(rh);
|
||||
if let Some(frame) = model.arena.frame_mut(h) {
|
||||
if let KindState::Button(bs) = &mut frame.kind_state {
|
||||
slot.set(bs, Some(rh));
|
||||
}
|
||||
}
|
||||
let writes = match model.arena.frame_mut(h).map(|f| &mut f.kind_state) {
|
||||
Some(KindState::Button(bs)) => slot.set(bs, Some(rh)),
|
||||
_ => Default::default(),
|
||||
};
|
||||
super::region::apply_shown_writes(&mut model, writes);
|
||||
// Only a new region gets an implicit anchor: a state texture fills the button
|
||||
// (`0x778f9d`/`0x7790db`), a label goes to the adopter (`0x778dc0` → `0x778d20`).
|
||||
match slot {
|
||||
@@ -320,11 +366,13 @@ fn install_slot(
|
||||
) {
|
||||
return Err(mlua::Error::runtime("not a Button"));
|
||||
}
|
||||
super::region::install_into_slot(
|
||||
let writes = std::cell::RefCell::new(crate::widget::ShownWrites::default());
|
||||
let changed = super::region::install_into_slot(
|
||||
&mut model,
|
||||
h,
|
||||
incoming,
|
||||
slot.shape().1,
|
||||
slot.links_shown(),
|
||||
false,
|
||||
|ks| match ks {
|
||||
KindState::Button(bs) => slot.get(bs),
|
||||
@@ -332,10 +380,12 @@ fn install_slot(
|
||||
},
|
||||
|ks, rh| {
|
||||
if let KindState::Button(bs) = ks {
|
||||
slot.set(bs, rh);
|
||||
*writes.borrow_mut() = slot.set(bs, rh);
|
||||
}
|
||||
},
|
||||
)
|
||||
)?;
|
||||
super::region::apply_shown_writes(&mut model, writes.into_inner());
|
||||
Ok(changed)
|
||||
}
|
||||
|
||||
/// `Set<State>Texture(texture | "path" | nil)`, forked on the argument's type (`0x781970`). The
|
||||
@@ -360,14 +410,16 @@ fn set_slot_texture(
|
||||
.unwrap_or(crate::script::BlendMode::Add),
|
||||
_ => crate::script::BlendMode::Add,
|
||||
});
|
||||
match args.front() {
|
||||
// A Texture object is itself installed into the slot (`0x781b0b` → `0x778fd0`; the
|
||||
// highlight's `0x781eb7` → `0x779110`, which also sets its blend, `0x779144`, unless it
|
||||
// is the same texture, `0x779122`). Any other widget raises (`0x781ab1`, message
|
||||
// `0x87a1e0`; the highlight's `0x87a28c`).
|
||||
Some(Value::Table(t)) => {
|
||||
let method = usage.split('(').next().unwrap_or(usage);
|
||||
let rh = super::region::texture_object_arg(lua, this, t, method)?;
|
||||
// A Texture object is itself installed into the slot (`0x781b0b` → `0x778fd0`; the highlight's
|
||||
// `0x781eb7` → `0x779110`, which also sets its blend, `0x779144`, unless it is the same
|
||||
// texture, `0x779122`). Any other widget raises (`0x781ab1`, message `0x87a1e0`; the
|
||||
// highlight's `0x87a28c`). nil clears the slot and destroys the old region (`0x781b5a` →
|
||||
// `0x778fd0`): unhooked but alive, it would draw in every state. A string or a number is a
|
||||
// path (`0x781b33`, `lua_tostring` converting a number as `%.14g`); no argument is not nil
|
||||
// (`0x781b4f`) and raises the usage.
|
||||
let method = usage.split('(').next().unwrap_or(usage);
|
||||
let path = match super::region::slot_arg(lua, this, args, usage, method, true)? {
|
||||
super::region::SlotArg::Object(rh) => {
|
||||
if install_slot(lua, this, slot, Some(rh))? {
|
||||
if let Some(blend) = blend {
|
||||
lua.app_data_mut::<Model>()
|
||||
@@ -380,31 +432,8 @@ fn set_slot_texture(
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
// nil clears the slot and destroys the old region (`0x781b5a` → `0x778fd0`): unhooked but
|
||||
// alive, it would draw in every state. No argument is not nil (`0x781b4f`) and clears
|
||||
// nothing.
|
||||
Some(Value::Nil) => return install_slot(lua, this, slot, None).map(|_| ()),
|
||||
_ => {}
|
||||
}
|
||||
|
||||
// `lua_tostring` (`0x781b33`): a number converts as `%.14g`, as the stack's coercion does.
|
||||
let path = match args.front() {
|
||||
Some(v @ (Value::String(_) | Value::Number(_) | Value::Integer(_))) => {
|
||||
lua.coerce_string(v.clone())?.map(|s| s.to_string_lossy())
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
let Some(path) = path else {
|
||||
let who = {
|
||||
let h = frame_handle_of(lua, this)?;
|
||||
let model = lua.app_data_ref::<Model>().expect("model app_data");
|
||||
model
|
||||
.arena
|
||||
.frame(h)
|
||||
.and_then(|f| f.name.clone())
|
||||
.unwrap_or_else(|| "<unnamed>".to_string())
|
||||
};
|
||||
return Err(mlua::Error::runtime(format!("Usage: {who}:{usage}")));
|
||||
super::region::SlotArg::Clear => return install_slot(lua, this, slot, None).map(|_| ()),
|
||||
super::region::SlotArg::Path(path) => path,
|
||||
};
|
||||
let id = ensure_slot(lua, this, slot)?;
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
|
||||
@@ -570,7 +599,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
// Destroyed, not orphaned (`0x778d3c`).
|
||||
super::region::free_region(&mut model, dead);
|
||||
}
|
||||
super::region::link_region(&mut model, rh, owner, DrawLayer::Artwork);
|
||||
super::region::link_region(&mut model, rh, owner, DrawLayer::Artwork, true);
|
||||
adopt_label(&mut model, owner, rh);
|
||||
// A new owner, so a new scale for its node, and `GetTextWidth` reads its extents.
|
||||
model.touch_layout_region(rh);
|
||||
@@ -830,7 +859,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
};
|
||||
let first = lock.into_iter().next();
|
||||
let locked = super::binding_abi::bool_or_default(first.as_ref(), false);
|
||||
with_button(lua, &this, |bs| bs.set_button_state(new, locked))
|
||||
with_button_shown(lua, &this, |bs| bs.set_button_state(new, locked))
|
||||
})?,
|
||||
)?;
|
||||
m.set(
|
||||
@@ -888,7 +917,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
}),
|
||||
_ => false,
|
||||
};
|
||||
with_button(lua, &this, |bs| bs.checked = checked)
|
||||
with_button_shown(lua, &this, |bs| bs.refresh_checked(checked, false))
|
||||
})?,
|
||||
)?;
|
||||
c.set(
|
||||
@@ -978,21 +1007,25 @@ pub(super) fn click_button(lua: &Lua, id: u32, button: &str, down: bool, scripte
|
||||
return;
|
||||
};
|
||||
let is_check = frame.kind == FrameKind::CheckButton;
|
||||
match &mut frame.kind_state {
|
||||
let (fire, writes) = match &mut frame.kind_state {
|
||||
// A Button fires only while enabled;
|
||||
KindState::Button(bs) => {
|
||||
if bs.enabled() {
|
||||
if is_check {
|
||||
bs.checked = !bs.checked;
|
||||
}
|
||||
true
|
||||
let writes = if is_check {
|
||||
bs.refresh_checked(!bs.checked, false)
|
||||
} else {
|
||||
Default::default()
|
||||
};
|
||||
(true, writes)
|
||||
} else {
|
||||
false
|
||||
(false, Default::default())
|
||||
}
|
||||
}
|
||||
// any other kind with an `OnClick` just fires.
|
||||
_ => true,
|
||||
}
|
||||
_ => (true, Default::default()),
|
||||
};
|
||||
super::region::apply_shown_writes(&mut model, writes);
|
||||
fire
|
||||
};
|
||||
if !fire {
|
||||
return;
|
||||
|
||||
@@ -8,13 +8,15 @@
|
||||
|
||||
use mlua::{Lua, Table, Value};
|
||||
|
||||
use super::object::{draw_layer_from_str, frame_handle_of};
|
||||
use super::object::frame_handle_of;
|
||||
use super::pointer::point_in_rect;
|
||||
use super::region::{install_into_slot, region_wrapper, texture_object_arg};
|
||||
use super::{event, Model, RegionData};
|
||||
use super::region::{
|
||||
install_into_slot, new_texture_region, region_wrapper, set_texture_path, slot_arg, SlotArg,
|
||||
};
|
||||
use super::{event, Model};
|
||||
use crate::layout::Rect;
|
||||
use crate::order::DrawLayer;
|
||||
use crate::widget::{ColorSelectState, FrameHandle, KindState, RegionHandle, RegionKind};
|
||||
use crate::widget::{ColorSelectState, FrameHandle, KindState, RegionHandle};
|
||||
|
||||
/// Registry key of the ColorSelect method table (the MAXCSTACK discipline: a Lua-side root).
|
||||
pub(super) const REG_COLORSELECT_METHODS: &str = "__benilla_colorselect_methods";
|
||||
@@ -58,16 +60,6 @@ impl Slot {
|
||||
}
|
||||
}
|
||||
|
||||
fn set(self, s: &mut ColorSelectState, rh: RegionHandle) {
|
||||
let slot = match self {
|
||||
Slot::Wheel => &mut s.wheel,
|
||||
Slot::WheelThumb => &mut s.wheel_thumb,
|
||||
Slot::ValueStrip => &mut s.value_strip,
|
||||
Slot::ValueThumb => &mut s.value_thumb,
|
||||
};
|
||||
*slot = Some(rh);
|
||||
}
|
||||
|
||||
fn set_opt(self, s: &mut ColorSelectState, rh: Option<RegionHandle>) {
|
||||
match self {
|
||||
Slot::Wheel => s.wheel = rh,
|
||||
@@ -87,58 +79,34 @@ impl Slot {
|
||||
}
|
||||
}
|
||||
|
||||
/// Get-or-create one slot's texture region, `layer` re-layering an existing one; returns its id.
|
||||
fn ensure_slot(lua: &Lua, this: &Table, slot: Slot, layer: Option<DrawLayer>) -> mlua::Result<u32> {
|
||||
let h = frame_handle_of(lua, this)?;
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
|
||||
impl Slot {
|
||||
/// Whether the slot has a path leg: the markers do (`0x78b7b0`), the wheel and the strip take
|
||||
/// an object or nil alone (`0x78de90`, `0x78e450`).
|
||||
fn has_path_leg(self) -> bool {
|
||||
matches!(self, Slot::WheelThumb | Slot::ValueThumb)
|
||||
}
|
||||
|
||||
let existing = match &model
|
||||
.arena
|
||||
.frame(h)
|
||||
.ok_or_else(|| mlua::Error::runtime("stale frame handle"))?
|
||||
.kind_state
|
||||
{
|
||||
KindState::ColorSelect(s) => slot.get(s),
|
||||
_ => return Err(mlua::Error::runtime("not a ColorSelect")),
|
||||
};
|
||||
|
||||
let rh = match existing {
|
||||
Some(rh) => {
|
||||
if let (Some(l), Some(region)) = (layer, model.arena.region_mut(rh)) {
|
||||
region.draw_layer = l;
|
||||
}
|
||||
rh
|
||||
}
|
||||
None => {
|
||||
let rh = model
|
||||
.arena
|
||||
.create_region(h, RegionKind::Texture, layer.unwrap_or(slot.layer()), 0)
|
||||
.ok_or_else(|| mlua::Error::runtime("stale frame handle"))?;
|
||||
model.region_data.insert(rh, RegionData::default());
|
||||
model.touch_layout_region(rh);
|
||||
if let Some(frame) = model.arena.frame_mut(h) {
|
||||
if let KindState::ColorSelect(s) = &mut frame.kind_state {
|
||||
slot.set(s, rh);
|
||||
}
|
||||
}
|
||||
rh
|
||||
}
|
||||
};
|
||||
Ok(model.region_id(rh))
|
||||
fn of_tag(tag: &str) -> Option<Slot> {
|
||||
Some(match tag {
|
||||
"ColorWheelTexture" => Slot::Wheel,
|
||||
"ColorWheelThumbTexture" => Slot::WheelThumb,
|
||||
"ColorValueTexture" => Slot::ValueStrip,
|
||||
"ColorValueThumbTexture" => Slot::ValueThumb,
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Install `incoming` into `slot`, or clear it, through the slot hooks: the wheel `0x78b580` and
|
||||
/// the strip `0x78b8a0` link on ARTWORK and replace the texture's image with the generated one
|
||||
/// (`0x770300`), so it ends file-less here, where the renderer paints it; the markers
|
||||
/// `0x78b850`/`0x78ba90` link on OVERLAY and clear their anchors (`0x767ed0`).
|
||||
fn install_slot(
|
||||
lua: &Lua,
|
||||
this: &Table,
|
||||
fn install_slot_model(
|
||||
model: &mut Model,
|
||||
h: FrameHandle,
|
||||
slot: Slot,
|
||||
incoming: Option<RegionHandle>,
|
||||
) -> mlua::Result<()> {
|
||||
let h = frame_handle_of(lua, this)?;
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
|
||||
if !matches!(
|
||||
model.arena.frame(h).map(|f| &f.kind_state),
|
||||
Some(KindState::ColorSelect(_))
|
||||
@@ -147,10 +115,11 @@ fn install_slot(
|
||||
}
|
||||
let thumb = matches!(slot, Slot::WheelThumb | Slot::ValueThumb);
|
||||
let changed = install_into_slot(
|
||||
&mut model,
|
||||
model,
|
||||
h,
|
||||
incoming,
|
||||
slot.layer(),
|
||||
true,
|
||||
thumb,
|
||||
|ks| match ks {
|
||||
KindState::ColorSelect(s) => slot.get(s),
|
||||
@@ -164,14 +133,56 @@ fn install_slot(
|
||||
)?;
|
||||
// The same texture returned at the first compare (`0x78b598`) and keeps what it was set to.
|
||||
if let (true, Some(rh), false) = (changed, incoming, thumb) {
|
||||
super::region::clear_texture(&mut model, rh);
|
||||
super::region::clear_texture(model, rh);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// `Set<Slot>Texture([path [, drawLayer]] | r, g, b [, a])`, plus an empty form that only creates
|
||||
/// the region: what `<ColorWheelTexture/>` means. A file-less slot is where the app renderer paints
|
||||
/// the disc and strip the reference computes (`0x78b580`, `0x78b8a0`).
|
||||
/// A ColorSelect element's texture child (`0x78b3f0`): the texture the texture adder built,
|
||||
/// installed as the slot's object. The wheel and the strip are painted by the renderer whatever
|
||||
/// file or colour the element names.
|
||||
pub(super) fn install_loaded(
|
||||
model: &mut Model,
|
||||
h: FrameHandle,
|
||||
tag: &str,
|
||||
rh: RegionHandle,
|
||||
) -> mlua::Result<()> {
|
||||
let slot = Slot::of_tag(tag)
|
||||
.ok_or_else(|| mlua::Error::runtime(format!("not a ColorSelect texture: {tag}")))?;
|
||||
install_slot_model(model, h, slot, Some(rh))
|
||||
}
|
||||
|
||||
/// A marker's path leg (`0x78b7b0`): a marker that exists has its file swapped and nothing else;
|
||||
/// with none, a texture is built, given the file and installed.
|
||||
fn set_marker_path(lua: &Lua, this: &Table, slot: Slot, path: &str) -> mlua::Result<()> {
|
||||
let h = frame_handle_of(lua, this)?;
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
|
||||
let existing = match &model
|
||||
.arena
|
||||
.frame(h)
|
||||
.ok_or_else(|| mlua::Error::runtime("stale frame handle"))?
|
||||
.kind_state
|
||||
{
|
||||
KindState::ColorSelect(s) => slot.get(s),
|
||||
_ => return Err(mlua::Error::runtime("not a ColorSelect")),
|
||||
};
|
||||
match existing {
|
||||
Some(rh) => set_texture_path(&mut model, rh, path),
|
||||
None => {
|
||||
let rh = new_texture_region(&mut model, h, slot.layer())
|
||||
.ok_or_else(|| mlua::Error::runtime("stale frame handle"))?;
|
||||
set_texture_path(&mut model, rh, path);
|
||||
install_slot_model(&mut model, h, slot, Some(rh))?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// `Set<Slot>Texture(texture or "texture" or nil)` for the markers (`0x78e160`, `0x78e720`), a
|
||||
/// path leg beside the object and nil ones; the wheel and the strip (`0x78de90`, `0x78e450`) have
|
||||
/// `(texture or nil)` alone. No argument raises the usage, and there is no colour form. A file-less
|
||||
/// wheel or strip is where the app renderer paints the disc and strip the reference computes
|
||||
/// (`0x78b580`, `0x78b8a0`).
|
||||
fn install_slot_texture(
|
||||
lua: &Lua,
|
||||
m: &Table,
|
||||
@@ -180,53 +191,27 @@ fn install_slot_texture(
|
||||
getter: &str,
|
||||
) -> mlua::Result<()> {
|
||||
let method = setter.to_string();
|
||||
let usage = if slot.has_path_leg() {
|
||||
format!(r#"{setter}(texture or "texture" or nil)"#)
|
||||
} else {
|
||||
format!("{setter}(texture or nil)")
|
||||
};
|
||||
m.set(
|
||||
setter,
|
||||
lua.create_function(move |lua, (this, args): (Table, mlua::MultiValue)| {
|
||||
let nil = Value::Nil;
|
||||
let arg = |i: usize| args.get(i).unwrap_or(&nil);
|
||||
let (a1, a2, a3, a4) = (arg(0), arg(1), arg(2), arg(3));
|
||||
let layer = match a2 {
|
||||
Value::String(s) => s.to_str().ok().and_then(|l| draw_layer_from_str(&l)),
|
||||
_ => None,
|
||||
};
|
||||
match args.front() {
|
||||
Some(Value::Table(t)) => {
|
||||
let rh = texture_object_arg(lua, &this, t, &method)?;
|
||||
return install_slot(lua, &this, slot, Some(rh));
|
||||
let arg = slot_arg(lua, &this, &args, &usage, &method, slot.has_path_leg())?;
|
||||
let h = frame_handle_of(lua, &this)?;
|
||||
match arg {
|
||||
SlotArg::Object(rh) => {
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
|
||||
install_slot_model(&mut model, h, slot, Some(rh))
|
||||
}
|
||||
Some(Value::Nil) => return install_slot(lua, &this, slot, None),
|
||||
_ => {}
|
||||
SlotArg::Clear => {
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
|
||||
install_slot_model(&mut model, h, slot, None)
|
||||
}
|
||||
SlotArg::Path(path) => set_marker_path(lua, &this, slot, &path),
|
||||
}
|
||||
let id = ensure_slot(lua, &this, slot, layer)?;
|
||||
let rh = {
|
||||
let model = lua.app_data_ref::<Model>().expect("model app_data");
|
||||
*model.id_to_region.get(&id).expect("colorselect region id")
|
||||
};
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
|
||||
let data = model.region_data.entry(rh).or_default();
|
||||
match a1 {
|
||||
Value::String(s) => {
|
||||
data.texture = Some(s.to_str()?.to_string());
|
||||
data.fill = None;
|
||||
}
|
||||
Value::Number(_) | Value::Integer(_) => {
|
||||
data.texture = None;
|
||||
data.fill = Some([
|
||||
num_f32(a1),
|
||||
num_f32(a2),
|
||||
num_f32(a3),
|
||||
match a4 {
|
||||
Value::Nil => 1.0,
|
||||
v => num_f32(v),
|
||||
},
|
||||
]);
|
||||
}
|
||||
// The empty form: the region now exists and stays file-less.
|
||||
_ => {}
|
||||
}
|
||||
model.touch_layout_art(rh);
|
||||
Ok(())
|
||||
})?,
|
||||
)?;
|
||||
m.set(
|
||||
@@ -246,14 +231,6 @@ fn install_slot_texture(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn num_f32(v: &Value) -> f32 {
|
||||
match v {
|
||||
Value::Number(n) => *n as f32,
|
||||
Value::Integer(i) => *i as f32,
|
||||
_ => 0.0,
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
let m = lua.create_table()?;
|
||||
|
||||
|
||||
@@ -179,7 +179,7 @@ impl UiScript {
|
||||
});
|
||||
}
|
||||
}
|
||||
// A Button's non-current state textures emit no quad, and its ButtonText takes
|
||||
// A Button's state textures draw by their shown bit like any region, and its ButtonText takes
|
||||
// the current state's font instance (disabled > highlighted > normal).
|
||||
let mut state_font: Option<&FontObject> = None;
|
||||
let mut state_color: Option<[f32; 4]> = None;
|
||||
@@ -191,7 +191,7 @@ impl UiScript {
|
||||
let hovered = owner.is_some() && model.mouseover == owner;
|
||||
// The press is not read here: the state texture latches on the transition
|
||||
// (`ButtonState::set_state`). Only the Highlight, unlatched, reads hover.
|
||||
if !bs.region_visible(rh, hovered) {
|
||||
if !bs.highlight_draws(rh, hovered) {
|
||||
continue;
|
||||
}
|
||||
if bs.text == Some(rh) {
|
||||
|
||||
@@ -237,7 +237,7 @@ pub use quest::{
|
||||
QuestAction, QuestItemView, QuestPanel, QuestRewardSpell, QuestSelect, QuestState,
|
||||
};
|
||||
pub use quest_log::{QuestLogDetail, QuestLogEntryView, QuestLogObjectiveView, QuestLogState};
|
||||
pub(crate) use region::{apply_font_parts, implicit_creation_anchor_lua};
|
||||
pub(crate) use region::{apply_font_parts, implicit_creation_anchor_lua, install_loaded_texture};
|
||||
pub use reputation::{FactionEntry, ReputationSend, ReputationState};
|
||||
pub use session::SessionRequest;
|
||||
pub use shapeshift::ShapeshiftFormView;
|
||||
|
||||
@@ -63,18 +63,19 @@ pub(super) fn texture_object_arg(
|
||||
/// out of the old parent (its region-removed hook runs, `0x77fd55`) and into `owner` on `layer`,
|
||||
/// the layer set whether or not the parent changed (`0x77fd9a`).
|
||||
///
|
||||
/// The third argument is the region's own shown bit (`+0xc4`, what `IsShown` reads): leaving the
|
||||
/// old parent clears it (`0x77fd3d`) and `shown` writes it back (`0x77fd85`, `0x77fdc8`). Every
|
||||
/// widget that takes a texture calls with 1 (the bar, thumb, highlight, ColorSelect slots, the
|
||||
/// label) except a Button's state and checked slots, whose 0 is followed by the state switch
|
||||
/// writing the bit for the texture that shows (`0x779037`, `0x7797ea`, `0x785538`). benilla keeps
|
||||
/// that state side in [`crate::widget::ButtonState::region_visible`], so those too end with the
|
||||
/// script-side bit shown, and `IsShown` reads the state side beside it.
|
||||
/// The third argument is the region's own shown bit (`+0xc4`, what `Show`/`Hide` write and
|
||||
/// `IsShown` reads): leaving the old parent clears it (`0x77fd3d`) and `shown` writes it back
|
||||
/// (`0x77fd85`, `0x77fdc8`, and `0x77fdd9` clears it when the parent and layer stand). Every widget
|
||||
/// that takes a texture calls with 1 (the bar, thumb, highlight, ColorSelect slots, the label)
|
||||
/// except a Button's state and checked slots, whose 0 is followed by the state switch writing the
|
||||
/// bit for the texture that shows (`0x779037`, `0x7797ea`, `0x785538`): the caller applies those
|
||||
/// writes after the link.
|
||||
pub(super) fn link_region(
|
||||
model: &mut Model,
|
||||
rh: RegionHandle,
|
||||
owner: crate::widget::FrameHandle,
|
||||
layer: crate::order::DrawLayer,
|
||||
shown: bool,
|
||||
) {
|
||||
if model.arena.set_region_owner(rh, Some(owner)) {
|
||||
// The owner supplies the region's scale to layout, so a re-link is a layout change.
|
||||
@@ -83,20 +84,32 @@ pub(super) fn link_region(
|
||||
if let Some(r) = model.arena.region_mut(rh) {
|
||||
r.draw_layer = layer;
|
||||
}
|
||||
model.region_data.entry(rh).or_default().hidden = false;
|
||||
model.region_data.entry(rh).or_default().hidden = !shown;
|
||||
}
|
||||
|
||||
/// Apply a Button state machine's writes to the regions' shown bit, the one `Show`/`Hide` write.
|
||||
/// A region the machine names that has since been freed is skipped.
|
||||
pub(super) fn apply_shown_writes(model: &mut Model, writes: crate::widget::ShownWrites) {
|
||||
for (rh, shown) in writes.0 {
|
||||
if model.arena.region(rh).is_some() {
|
||||
model.region_data.entry(rh).or_default().hidden = !shown;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A widget's one-texture slot taking `incoming` (a Texture object, or nothing to clear), as every
|
||||
/// slot setter does (`0x778fd0`, `0x7831f0`, `0x789860`, `0x785330`, `0x78b580`..): the same
|
||||
/// texture is a no-op, the displaced one is deleted, the incoming one is linked in on `layer`
|
||||
/// (a thumb marker then loses its anchors, `0x767ed0`, since the widget places it), then it is
|
||||
/// stored. `get` and `set` read and write the slot in the widget's own state. Answers whether the
|
||||
/// slot changed, for a caller whose hook does more once it has.
|
||||
/// stored. `shown` is the link's third argument (see [`link_region`]). `get` and `set` read and
|
||||
/// write the slot in the widget's own state. Answers whether the slot changed, for a caller whose
|
||||
/// hook does more once it has.
|
||||
pub(super) fn install_into_slot(
|
||||
model: &mut Model,
|
||||
owner: crate::widget::FrameHandle,
|
||||
incoming: Option<RegionHandle>,
|
||||
layer: crate::order::DrawLayer,
|
||||
shown: bool,
|
||||
clear_points: bool,
|
||||
get: impl Fn(&crate::widget::KindState) -> Option<RegionHandle>,
|
||||
set: impl Fn(&mut crate::widget::KindState, Option<RegionHandle>),
|
||||
@@ -113,7 +126,7 @@ pub(super) fn install_into_slot(
|
||||
free_region(model, old);
|
||||
}
|
||||
if let Some(rh) = incoming {
|
||||
link_region(model, rh, owner, layer);
|
||||
link_region(model, rh, owner, layer, shown);
|
||||
if clear_points {
|
||||
layout::clear_points(model, rh);
|
||||
}
|
||||
@@ -129,21 +142,99 @@ pub(super) fn install_into_slot(
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
/// The region's shown bit as `IsShown` reads it (`0x79b9b0`, `[+0xc4]`): its own `Show`/`Hide`,
|
||||
/// and for a Button's state and checked textures also the state side that writes the bit for the
|
||||
/// one that shows.
|
||||
/// What a widget's texture setter was handed (`SetStatusBarTexture` `0x78f870`, `SetThumbTexture`
|
||||
/// `0x7902b0`, `SetNormalTexture` `0x781970`, the ColorSelect four): a Texture object, a path, or
|
||||
/// nil to clear.
|
||||
pub(super) enum SlotArg {
|
||||
Object(RegionHandle),
|
||||
Path(String),
|
||||
Clear,
|
||||
}
|
||||
|
||||
/// The setter's fork on its first argument. A table is a Texture object (`lua_type` 5), a string
|
||||
/// or a number a path where the setter has a path leg (`lua_isstring` `0x6f3510`, then
|
||||
/// `lua_tostring` `0x6f3690`, so `1` is the file `"1"`), nil (type 0) clears; anything else, no
|
||||
/// argument included (type -1), raises `Usage: <name>:<usage>`. `object_method` names the setter in
|
||||
/// the object leg's two raises, as each setter's own format string prints it.
|
||||
pub(super) fn slot_arg(
|
||||
lua: &Lua,
|
||||
this: &Table,
|
||||
args: &MultiValue,
|
||||
usage: &str,
|
||||
object_method: &str,
|
||||
path_leg: bool,
|
||||
) -> mlua::Result<SlotArg> {
|
||||
match args.front() {
|
||||
Some(Value::Table(t)) => {
|
||||
return texture_object_arg(lua, this, t, object_method).map(SlotArg::Object)
|
||||
}
|
||||
Some(Value::Nil) => return Ok(SlotArg::Clear),
|
||||
Some(v @ (Value::String(_) | Value::Number(_) | Value::Integer(_))) if path_leg => {
|
||||
if let Some(path) = lua.coerce_string(v.clone())? {
|
||||
return Ok(SlotArg::Path(path.to_string_lossy()));
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
let who = {
|
||||
let model = lua.app_data_ref::<Model>().expect("model app_data");
|
||||
super::object::frame_handle_of(lua, this)
|
||||
.ok()
|
||||
.and_then(|h| model.arena.frame(h).and_then(|f| f.name.clone()))
|
||||
.unwrap_or_else(|| "<unnamed>".to_string())
|
||||
};
|
||||
Err(mlua::Error::runtime(format!("Usage: {who}:{usage}")))
|
||||
}
|
||||
|
||||
/// A fresh anchorless Texture region of `owner` on `layer`, as the client builds the texture a
|
||||
/// path leg loads (`0x76fc40`) before it installs it as an object.
|
||||
pub(super) fn new_texture_region(
|
||||
model: &mut Model,
|
||||
owner: crate::widget::FrameHandle,
|
||||
layer: crate::order::DrawLayer,
|
||||
) -> Option<RegionHandle> {
|
||||
let rh = model
|
||||
.arena
|
||||
.create_region(owner, RegionKind::Texture, layer, 0)?;
|
||||
model.region_data.insert(rh, super::RegionData::default());
|
||||
model.touch_layout_region(rh);
|
||||
Some(rh)
|
||||
}
|
||||
|
||||
/// `CSimpleTexture::SetTexture(path)` (`0x770200`) on a region: the file replaces a solid fill, and
|
||||
/// `""` clears the texture, which `QuestLogFrame.lua:165` relies on.
|
||||
pub(super) fn set_texture_path(model: &mut Model, rh: RegionHandle, path: &str) {
|
||||
let data = model.region_data.entry(rh).or_default();
|
||||
data.texture = (!path.is_empty()).then(|| path.to_string());
|
||||
data.fill = None;
|
||||
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.
|
||||
pub(crate) fn install_loaded_texture(
|
||||
lua: &Lua,
|
||||
this: &Table,
|
||||
tag: &str,
|
||||
texture: &Table,
|
||||
layer: Option<&str>,
|
||||
) -> mlua::Result<()> {
|
||||
let h = super::object::frame_handle_of(lua, this)?;
|
||||
let rh = texture_object_arg(lua, this, texture, "SetStatusBarTexture")?;
|
||||
let layer = layer.and_then(super::object::draw_layer_from_str);
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
|
||||
match tag {
|
||||
"BarTexture" => super::statusbar::install_loaded(&mut model, h, rh, layer),
|
||||
"ThumbTexture" => super::slider::install_loaded(&mut model, h, rh, layer),
|
||||
_ => super::colorselect::install_loaded(&mut model, h, tag, rh),
|
||||
}
|
||||
}
|
||||
|
||||
/// The region's shown bit (`+0xc4`) as `IsShown` reads it (`0x79b9b0`): the one bit `Show`/`Hide`
|
||||
/// write, and a Button's state switch writes for its state and checked textures.
|
||||
fn region_shown(model: &Model, rh: RegionHandle) -> bool {
|
||||
if model.region_data.get(&rh).is_some_and(|d| d.hidden) {
|
||||
return false;
|
||||
}
|
||||
let owner = model.arena.region(rh).map(|r| r.owner);
|
||||
match owner
|
||||
.and_then(|o| model.arena.frame(o))
|
||||
.map(|f| &f.kind_state)
|
||||
{
|
||||
Some(crate::widget::KindState::Button(bs)) => bs.state_region_shown(rh).unwrap_or(true),
|
||||
_ => true,
|
||||
}
|
||||
!model.region_data.get(&rh).is_some_and(|d| d.hidden)
|
||||
}
|
||||
|
||||
/// Argument 1 of a global that takes a Texture as its `this` (`SetLootPortrait` `0x4c2b40`,
|
||||
|
||||
@@ -309,9 +309,9 @@ pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
)?;
|
||||
|
||||
// SetTexCoordModifiesRect/GetTexCoordModifiesRect (`0x79c080`/`0x79c120`, table `0x87c128`):
|
||||
// the setter is the only writer of `[texture+0x124]`, the getter answers 1 or nil. Stored, not
|
||||
// applied: there a set flag makes `SetTexCoord` re-derive the rect from the UV quad
|
||||
// (`0x770462`).
|
||||
// the setter writes `[texture+0x124]` (`0x79c113`), as does installing the texture as a
|
||||
// StatusBar's bar (`0x783220`); the getter answers 1 or nil. Stored, not applied: there a set
|
||||
// flag makes `SetTexCoord` re-derive the rect from the UV quad (`0x770462`).
|
||||
m.set(
|
||||
"SetTexCoordModifiesRect",
|
||||
lua.create_function(|lua, (this, arg): (Table, Value)| {
|
||||
|
||||
@@ -8,14 +8,16 @@
|
||||
|
||||
use mlua::{Lua, Table, Value};
|
||||
|
||||
use super::object::{draw_layer_from_str, frame_handle_of};
|
||||
use super::object::frame_handle_of;
|
||||
use super::pointer::point_in_rect;
|
||||
use super::region::{install_into_slot, region_wrapper, texture_object_arg};
|
||||
use super::{event, Model, RegionData};
|
||||
use super::region::{
|
||||
install_into_slot, new_texture_region, region_wrapper, set_texture_path, slot_arg, SlotArg,
|
||||
};
|
||||
use super::{event, Model};
|
||||
use crate::layout::Rect;
|
||||
use crate::order::DrawLayer;
|
||||
use crate::widget::{
|
||||
slider_fraction, slider_grab, FrameHandle, KindState, RegionHandle, RegionKind, SliderState,
|
||||
slider_fraction, slider_grab, FrameHandle, KindState, RegionHandle, SliderState,
|
||||
};
|
||||
|
||||
pub(super) const REG_SLIDER_METHODS: &str = "__benilla_slider_methods";
|
||||
@@ -39,56 +41,15 @@ fn with_slider<T>(
|
||||
}
|
||||
}
|
||||
|
||||
/// Get or create the thumb texture region and return its id; `layer` re-layers an existing thumb,
|
||||
/// and a new one defaults to OVERLAY (`0x789580`).
|
||||
fn ensure_thumb(lua: &Lua, this: &Table, layer: Option<DrawLayer>) -> mlua::Result<u32> {
|
||||
let h = frame_handle_of(lua, this)?;
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
|
||||
|
||||
let existing = match &model
|
||||
.arena
|
||||
.frame(h)
|
||||
.ok_or_else(|| mlua::Error::runtime("stale frame handle"))?
|
||||
.kind_state
|
||||
{
|
||||
KindState::Slider(s) => s.thumb,
|
||||
_ => return Err(mlua::Error::runtime("not a Slider")),
|
||||
};
|
||||
|
||||
let rh = match existing {
|
||||
Some(rh) => {
|
||||
if let (Some(l), Some(region)) = (layer, model.arena.region_mut(rh)) {
|
||||
region.draw_layer = l;
|
||||
}
|
||||
rh
|
||||
}
|
||||
None => {
|
||||
let rh = model
|
||||
.arena
|
||||
.create_region(
|
||||
h,
|
||||
RegionKind::Texture,
|
||||
layer.unwrap_or(DrawLayer::Overlay),
|
||||
0,
|
||||
)
|
||||
.ok_or_else(|| mlua::Error::runtime("stale frame handle"))?;
|
||||
model.region_data.insert(rh, RegionData::default());
|
||||
model.touch_layout_region(rh);
|
||||
if let Some(frame) = model.arena.frame_mut(h) {
|
||||
if let KindState::Slider(s) = &mut frame.kind_state {
|
||||
s.thumb = Some(rh);
|
||||
}
|
||||
}
|
||||
rh
|
||||
}
|
||||
};
|
||||
Ok(model.region_id(rh))
|
||||
}
|
||||
|
||||
/// Install `incoming` as the thumb, or clear it, through `0x789860`.
|
||||
fn install_thumb(lua: &Lua, this: &Table, incoming: Option<RegionHandle>) -> mlua::Result<()> {
|
||||
let h = frame_handle_of(lua, this)?;
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
|
||||
/// Install `incoming` as the thumb on `layer`, or clear it, through `0x789860`: the same texture
|
||||
/// is a no-op, the displaced one is deleted, the incoming one is linked in shown and loses its
|
||||
/// anchors (`0x789893`), the widget placing it.
|
||||
fn install_thumb_model(
|
||||
model: &mut Model,
|
||||
h: FrameHandle,
|
||||
incoming: Option<RegionHandle>,
|
||||
layer: DrawLayer,
|
||||
) -> mlua::Result<()> {
|
||||
if !matches!(
|
||||
model.arena.frame(h).map(|f| &f.kind_state),
|
||||
Some(KindState::Slider(_))
|
||||
@@ -96,10 +57,11 @@ fn install_thumb(lua: &Lua, this: &Table, incoming: Option<RegionHandle>) -> mlu
|
||||
return Err(mlua::Error::runtime("not a Slider"));
|
||||
}
|
||||
install_into_slot(
|
||||
&mut model,
|
||||
model,
|
||||
h,
|
||||
incoming,
|
||||
DrawLayer::Overlay,
|
||||
layer,
|
||||
true,
|
||||
true,
|
||||
|ks| match ks {
|
||||
KindState::Slider(sl) => sl.thumb,
|
||||
@@ -114,6 +76,44 @@ fn install_thumb(lua: &Lua, this: &Table, incoming: Option<RegionHandle>) -> mlu
|
||||
.map(|_| ())
|
||||
}
|
||||
|
||||
/// `<ThumbTexture>` (`0x7895f6`): the texture the texture adder built, installed as an object on
|
||||
/// the element's `drawLayer`, OVERLAY without one.
|
||||
pub(super) fn install_loaded(
|
||||
model: &mut Model,
|
||||
h: FrameHandle,
|
||||
rh: RegionHandle,
|
||||
layer: Option<DrawLayer>,
|
||||
) -> mlua::Result<()> {
|
||||
install_thumb_model(model, h, Some(rh), layer.unwrap_or(DrawLayer::Overlay))
|
||||
}
|
||||
|
||||
/// `SetThumbTexture`'s path leg (`0x7897c0`): a thumb that exists has its file swapped and nothing
|
||||
/// else; with none, a texture is built, given the file and installed on OVERLAY (`0x790431`'s
|
||||
/// layer).
|
||||
fn set_thumb_path(lua: &Lua, this: &Table, path: &str) -> mlua::Result<()> {
|
||||
let h = frame_handle_of(lua, this)?;
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
|
||||
let existing = match &model
|
||||
.arena
|
||||
.frame(h)
|
||||
.ok_or_else(|| mlua::Error::runtime("stale frame handle"))?
|
||||
.kind_state
|
||||
{
|
||||
KindState::Slider(s) => s.thumb,
|
||||
_ => return Err(mlua::Error::runtime("not a Slider")),
|
||||
};
|
||||
match existing {
|
||||
Some(rh) => set_texture_path(&mut model, rh, path),
|
||||
None => {
|
||||
let rh = new_texture_region(&mut model, h, DrawLayer::Overlay)
|
||||
.ok_or_else(|| mlua::Error::runtime("stale frame handle"))?;
|
||||
set_texture_path(&mut model, rh, path);
|
||||
install_thumb_model(&mut model, h, Some(rh), DrawLayer::Overlay)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
let m = lua.create_table()?;
|
||||
|
||||
@@ -180,57 +180,33 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
// A 1.12 Slider has no enabled state: `Enable`/`Disable`/`IsEnabled` exist only in the Button
|
||||
// table (`0x879d00`: `0x77fef0`, `0x77ffd0`, `0x7800b0`), so a Slider must not answer them.
|
||||
|
||||
// `SetThumbTexture(texture)` installs the object as the thumb, linked on OVERLAY (the constant
|
||||
// `0x790431` passes) with its anchors cleared (`0x789893`), displacing the old thumb; nil
|
||||
// clears it (`0x790473`). Otherwise `(path [, layer])` or `(r, g, b [, a])`, a region's two
|
||||
// `SetTexture` forms, on the thumb region, created on first use.
|
||||
// `SetThumbTexture(texture or "texture" or nil)` (`0x7902b0`): a Texture object is installed as
|
||||
// the thumb, linked on OVERLAY (the constant `0x790431` passes) with its anchors cleared
|
||||
// (`0x789893`), displacing the old thumb; a string or a number is the thumb's path
|
||||
// (`0x790442`), and nil clears it (`0x790473`). It reads no layer and has no colour form.
|
||||
m.set(
|
||||
"SetThumbTexture",
|
||||
lua.create_function(|lua, (this, args): (Table, mlua::MultiValue)| {
|
||||
let nil = Value::Nil;
|
||||
let arg = |i: usize| args.get(i).unwrap_or(&nil);
|
||||
let (a1, a2, a3, a4) = (arg(0), arg(1), arg(2), arg(3));
|
||||
let layer = match a2 {
|
||||
Value::String(s) => s.to_str().ok().and_then(|l| draw_layer_from_str(&l)),
|
||||
_ => None,
|
||||
};
|
||||
match args.front() {
|
||||
Some(Value::Table(t)) => {
|
||||
let rh = texture_object_arg(lua, &this, t, "SetThumbTexture")?;
|
||||
return install_thumb(lua, &this, Some(rh));
|
||||
match slot_arg(
|
||||
lua,
|
||||
&this,
|
||||
&args,
|
||||
r#"SetThumbTexture(texture or "texture" or nil)"#,
|
||||
"SetThumbTexture",
|
||||
true,
|
||||
)? {
|
||||
SlotArg::Object(rh) => {
|
||||
let h = frame_handle_of(lua, &this)?;
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
|
||||
install_thumb_model(&mut model, h, Some(rh), DrawLayer::Overlay)
|
||||
}
|
||||
Some(Value::Nil) => return install_thumb(lua, &this, None),
|
||||
_ => {}
|
||||
SlotArg::Clear => {
|
||||
let h = frame_handle_of(lua, &this)?;
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
|
||||
install_thumb_model(&mut model, h, None, DrawLayer::Overlay)
|
||||
}
|
||||
SlotArg::Path(path) => set_thumb_path(lua, &this, &path),
|
||||
}
|
||||
let id = ensure_thumb(lua, &this, layer)?;
|
||||
let rh = {
|
||||
let model = lua.app_data_ref::<Model>().expect("model app_data");
|
||||
*model.id_to_region.get(&id).expect("thumb region id")
|
||||
};
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
|
||||
let data = model.region_data.entry(rh).or_default();
|
||||
match a1 {
|
||||
Value::String(s) => {
|
||||
data.texture = Some(s.to_str()?.to_string());
|
||||
data.fill = None;
|
||||
}
|
||||
// A solid thumb and a path share one slot; each clears the other.
|
||||
Value::Number(_) | Value::Integer(_) => {
|
||||
data.texture = None;
|
||||
data.fill = Some([
|
||||
num_f32(a1),
|
||||
num_f32(a2),
|
||||
num_f32(a3),
|
||||
match a4 {
|
||||
Value::Nil => 1.0,
|
||||
v => num_f32(v),
|
||||
},
|
||||
]);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
model.touch_layout_art(rh);
|
||||
Ok(())
|
||||
})?,
|
||||
)?;
|
||||
m.set(
|
||||
@@ -274,14 +250,6 @@ fn fire_value_changed(lua: &Lua, this: &Table, changed: Option<f32>) -> mlua::Re
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn num_f32(v: &Value) -> f32 {
|
||||
match v {
|
||||
Value::Number(n) => *n as f32,
|
||||
Value::Integer(i) => *i as f32,
|
||||
_ => 0.0,
|
||||
}
|
||||
}
|
||||
|
||||
// ── Thumb geometry and drag ──────────────────────────────────────────────────────────────────
|
||||
|
||||
/// The thumb's rect at `fraction` along the track, centred across it and inset so it travels
|
||||
|
||||
@@ -7,10 +7,12 @@
|
||||
use mlua::{Lua, Table, Value};
|
||||
|
||||
use super::object::{draw_layer_from_str, frame_handle_of};
|
||||
use super::region::{install_into_slot, region_wrapper, texture_object_arg};
|
||||
use super::{event, Model, RegionData};
|
||||
use super::region::{
|
||||
install_into_slot, new_texture_region, region_wrapper, set_texture_path, slot_arg, SlotArg,
|
||||
};
|
||||
use super::{event, Model};
|
||||
use crate::order::DrawLayer;
|
||||
use crate::widget::{KindState, RegionHandle, RegionKind, StatusBarState};
|
||||
use crate::widget::{KindState, RegionHandle, StatusBarState};
|
||||
|
||||
/// Registry key of the StatusBar method table.
|
||||
pub(super) const REG_STATUSBAR_METHODS: &str = "__benilla_statusbar_methods";
|
||||
@@ -44,72 +46,27 @@ fn store_value(sb: &mut StatusBarState, v: f32) -> Option<f32> {
|
||||
})
|
||||
}
|
||||
|
||||
/// Get or create the bar texture region, `ARTWORK` by default as in the reference; `layer`
|
||||
/// re-layers an existing one.
|
||||
fn ensure_bar(lua: &Lua, this: &Table, layer: Option<DrawLayer>) -> mlua::Result<u32> {
|
||||
let h = frame_handle_of(lua, this)?;
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
|
||||
|
||||
let existing = match &model
|
||||
.arena
|
||||
.frame(h)
|
||||
.ok_or_else(|| mlua::Error::runtime("stale frame handle"))?
|
||||
.kind_state
|
||||
{
|
||||
KindState::StatusBar(sb) => sb.bar,
|
||||
_ => return Err(mlua::Error::runtime("not a StatusBar")),
|
||||
};
|
||||
|
||||
let rh = match existing {
|
||||
Some(rh) => {
|
||||
if let (Some(l), Some(region)) = (layer, model.arena.region_mut(rh)) {
|
||||
region.draw_layer = l;
|
||||
}
|
||||
rh
|
||||
}
|
||||
None => {
|
||||
let rh = model
|
||||
.arena
|
||||
.create_region(
|
||||
h,
|
||||
RegionKind::Texture,
|
||||
layer.unwrap_or(DrawLayer::Artwork),
|
||||
0,
|
||||
)
|
||||
.ok_or_else(|| mlua::Error::runtime("stale frame handle"))?;
|
||||
model.region_data.insert(rh, RegionData::default());
|
||||
model.touch_layout_region(rh);
|
||||
if let Some(frame) = model.arena.frame_mut(h) {
|
||||
if let KindState::StatusBar(sb) = &mut frame.kind_state {
|
||||
sb.bar = Some(rh);
|
||||
}
|
||||
}
|
||||
rh
|
||||
}
|
||||
};
|
||||
Ok(model.region_id(rh))
|
||||
}
|
||||
|
||||
/// Install `incoming` as the bar texture, or clear it, through `0x7831f0`.
|
||||
fn install_bar(
|
||||
lua: &Lua,
|
||||
this: &Table,
|
||||
/// Install `incoming` as the bar texture, or clear it, through `0x7831f0`: the same texture is a
|
||||
/// no-op, the displaced one is deleted, the incoming one is linked in on `layer` shown (`0x78321b`)
|
||||
/// and its texcoord-modifies-rect flag is set (`0x783220`).
|
||||
fn install_bar_model(
|
||||
model: &mut Model,
|
||||
h: crate::widget::FrameHandle,
|
||||
incoming: Option<RegionHandle>,
|
||||
layer: DrawLayer,
|
||||
) -> mlua::Result<()> {
|
||||
let h = frame_handle_of(lua, this)?;
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
|
||||
if !matches!(
|
||||
model.arena.frame(h).map(|f| &f.kind_state),
|
||||
Some(KindState::StatusBar(_))
|
||||
) {
|
||||
return Err(mlua::Error::runtime("not a StatusBar"));
|
||||
}
|
||||
install_into_slot(
|
||||
&mut model,
|
||||
let changed = install_into_slot(
|
||||
model,
|
||||
h,
|
||||
incoming,
|
||||
layer,
|
||||
true,
|
||||
false,
|
||||
|ks| match ks {
|
||||
KindState::StatusBar(sb) => sb.bar,
|
||||
@@ -120,8 +77,66 @@ fn install_bar(
|
||||
sb.bar = rh;
|
||||
}
|
||||
},
|
||||
)
|
||||
.map(|_| ())
|
||||
)?;
|
||||
if let (true, Some(rh)) = (changed, incoming) {
|
||||
model
|
||||
.region_data
|
||||
.entry(rh)
|
||||
.or_default()
|
||||
.tex_coord_modifies_rect = true;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn install_bar(
|
||||
lua: &Lua,
|
||||
this: &Table,
|
||||
incoming: Option<RegionHandle>,
|
||||
layer: DrawLayer,
|
||||
) -> mlua::Result<()> {
|
||||
let h = frame_handle_of(lua, this)?;
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
|
||||
install_bar_model(&mut model, h, incoming, layer)
|
||||
}
|
||||
|
||||
/// `<BarTexture>` (`0x782f63`): the texture the texture adder built, installed as an object on the
|
||||
/// element's `drawLayer`, ARTWORK without one.
|
||||
pub(super) fn install_loaded(
|
||||
model: &mut Model,
|
||||
h: crate::widget::FrameHandle,
|
||||
rh: RegionHandle,
|
||||
layer: Option<DrawLayer>,
|
||||
) -> mlua::Result<()> {
|
||||
install_bar_model(model, h, Some(rh), layer.unwrap_or(DrawLayer::Artwork))
|
||||
}
|
||||
|
||||
/// `SetStatusBarTexture`'s path leg (`0x783140`): a bar that exists has its file swapped and
|
||||
/// nothing else, its layer included; with none, a texture is built, given the file and installed
|
||||
/// on `layer` (`0x7831cb`).
|
||||
fn set_bar_path(lua: &Lua, this: &Table, path: &str, layer: DrawLayer) -> mlua::Result<()> {
|
||||
let h = frame_handle_of(lua, this)?;
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
|
||||
let existing = match &model
|
||||
.arena
|
||||
.frame(h)
|
||||
.ok_or_else(|| mlua::Error::runtime("stale frame handle"))?
|
||||
.kind_state
|
||||
{
|
||||
KindState::StatusBar(sb) => sb.bar,
|
||||
_ => return Err(mlua::Error::runtime("not a StatusBar")),
|
||||
};
|
||||
match existing {
|
||||
Some(rh) => set_texture_path(&mut model, rh, path),
|
||||
None => {
|
||||
let rh = new_texture_region(&mut model, h, layer)
|
||||
.ok_or_else(|| mlua::Error::runtime("stale frame handle"))?;
|
||||
set_texture_path(&mut model, rh, path);
|
||||
// `SetAllPoints` onto the bar (`0x7831bf`).
|
||||
super::region::implicit_creation_anchor(&mut model, rh);
|
||||
install_bar_model(&mut model, h, Some(rh), layer)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
@@ -177,59 +192,35 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
})?,
|
||||
)?;
|
||||
|
||||
// `(texture [, layer])` installs the object as the bar, linked on `layer` (ARTWORK without
|
||||
// one, `0x78f902`) and displacing the old bar (`0x7831f0`); nil clears it (`0x78fa6d`). Its
|
||||
// raises print `SetCheckedTexture`, the client's copy-pasted format strings (`0x87b1e8`,
|
||||
// `0x87b224`). Otherwise a region's two `SetTexture` forms, `(path [, layer])` and
|
||||
// `(r, g, b [, a])`, on the bar.
|
||||
// `SetStatusBarTexture(texture or "texture" or nil [, "layer"])` (`0x78f870`): a Texture object
|
||||
// is installed as the bar, linked on `layer` (ARTWORK without one, `0x78f902`) and displacing
|
||||
// the old bar (`0x7831f0`); a string or a number is the path of the bar (`0x783140`), and nil
|
||||
// clears it (`0x78fa6d`). There is no colour form. The object leg's raises print
|
||||
// `SetCheckedTexture`, the client's copy-pasted format strings (`0x87b1e8`, `0x87b224`).
|
||||
m.set(
|
||||
"SetStatusBarTexture",
|
||||
lua.create_function(|lua, (this, args): (Table, mlua::MultiValue)| {
|
||||
let nil = Value::Nil;
|
||||
let arg = |i: usize| args.get(i).unwrap_or(&nil);
|
||||
let (a1, a2, a3, a4) = (arg(0), arg(1), arg(2), arg(3));
|
||||
let layer = match a2 {
|
||||
Value::String(s) => s.to_str().ok().and_then(|l| draw_layer_from_str(&l)),
|
||||
// `lua_isstring` on the layer (`0x78f909`): a number takes the name parse too, and
|
||||
// finds no layer.
|
||||
let layer = match args.get(1) {
|
||||
Some(v @ (Value::String(_) | Value::Number(_) | Value::Integer(_))) => lua
|
||||
.coerce_string(v.clone())?
|
||||
.and_then(|l| draw_layer_from_str(&l.to_string_lossy())),
|
||||
_ => None,
|
||||
};
|
||||
match args.front() {
|
||||
Some(Value::Table(t)) => {
|
||||
let rh = texture_object_arg(lua, &this, t, "SetCheckedTexture")?;
|
||||
return install_bar(lua, &this, Some(rh), layer.unwrap_or(DrawLayer::Artwork));
|
||||
}
|
||||
Some(Value::Nil) => return install_bar(lua, &this, None, DrawLayer::Artwork),
|
||||
_ => {}
|
||||
}
|
||||
let id = ensure_bar(lua, &this, layer)?;
|
||||
let rh = {
|
||||
let model = lua.app_data_ref::<Model>().expect("model app_data");
|
||||
*model.id_to_region.get(&id).expect("bar region id")
|
||||
};
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
|
||||
let data = model.region_data.entry(rh).or_default();
|
||||
match a1 {
|
||||
Value::String(s) => {
|
||||
data.texture = Some(s.to_str()?.to_string());
|
||||
data.fill = None;
|
||||
}
|
||||
// A solid fill and a path clear each other; `SetStatusBarColor` is a separate
|
||||
// tint that multiplies either.
|
||||
Value::Number(_) | Value::Integer(_) => {
|
||||
data.texture = None;
|
||||
data.fill = Some([
|
||||
num_f32(a1),
|
||||
num_f32(a2),
|
||||
num_f32(a3),
|
||||
match a4 {
|
||||
Value::Nil => 1.0,
|
||||
v => num_f32(v),
|
||||
},
|
||||
]);
|
||||
}
|
||||
_ => {}
|
||||
.unwrap_or(DrawLayer::Artwork);
|
||||
match slot_arg(
|
||||
lua,
|
||||
&this,
|
||||
&args,
|
||||
r#"SetStatusBarTexture(texture or "texture" or nil [, "layer"])"#,
|
||||
"SetCheckedTexture",
|
||||
true,
|
||||
)? {
|
||||
SlotArg::Object(rh) => install_bar(lua, &this, Some(rh), layer),
|
||||
SlotArg::Clear => install_bar(lua, &this, None, DrawLayer::Artwork),
|
||||
SlotArg::Path(path) => set_bar_path(lua, &this, &path, layer),
|
||||
}
|
||||
model.touch_layout_art(rh);
|
||||
Ok(())
|
||||
})?,
|
||||
)?;
|
||||
m.set(
|
||||
@@ -258,9 +249,12 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
crate::script::object::as_f32(&g),
|
||||
crate::script::object::as_f32(&b),
|
||||
);
|
||||
let id = ensure_bar(lua, &this, None)?;
|
||||
// `0x78fc20` hands the colour to the bar (`0x782dd0`), which sets its vertex colour
|
||||
// only if a bar exists and keeps nothing otherwise.
|
||||
let Some(rh) = with_bar(lua, &this, |sb| sb.bar)? else {
|
||||
return Ok(());
|
||||
};
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
|
||||
let rh = *model.id_to_region.get(&id).expect("bar region id");
|
||||
model.region_data.entry(rh).or_default().vertex_color =
|
||||
Some([r, g, b, a.unwrap_or(1.0)]);
|
||||
Ok(())
|
||||
@@ -325,11 +319,3 @@ pub(super) fn fire_engine_value_changed(lua: &Lua, bar: crate::widget::FrameHand
|
||||
.record_lua_error(&e);
|
||||
}
|
||||
}
|
||||
|
||||
fn num_f32(v: &Value) -> f32 {
|
||||
match v {
|
||||
Value::Number(n) => *n as f32,
|
||||
Value::Integer(i) => *i as f32,
|
||||
_ => 0.0,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,10 +127,17 @@ fn a_state_with_no_texture_leaves_the_shown_one_standing() {
|
||||
);
|
||||
s.run("StickyBtn:SetDisabledTexture(\"Interface\\\\D.blp\")")
|
||||
.unwrap();
|
||||
// The slot store shows the new texture and moves the shown pointer onto it, but hides nothing
|
||||
// (`0x77902f`..`0x779041`): the normal art keeps its bit until the next switch clears it.
|
||||
let mut both = visible(&s, "StickyBtn");
|
||||
both.sort();
|
||||
assert_eq!(
|
||||
visible(&s, "StickyBtn"),
|
||||
vec!["Interface\\D.blp".to_string()],
|
||||
"and real disabled art displaces it on the spot"
|
||||
both,
|
||||
vec![
|
||||
"Interface\\D.blp".to_string(),
|
||||
"Interface\\N.blp".to_string()
|
||||
],
|
||||
"real disabled art joins it on the spot; the store hides nothing"
|
||||
);
|
||||
s.run("StickyBtn:Enable()").unwrap();
|
||||
assert_eq!(visible(&s, "StickyBtn"), n, "back to normal on Enable");
|
||||
|
||||
@@ -42,6 +42,7 @@ mod regions;
|
||||
mod resize_bounds;
|
||||
mod scrollframe;
|
||||
mod setparent;
|
||||
mod shown_bit;
|
||||
mod simplehtml;
|
||||
mod size_changed;
|
||||
mod slider;
|
||||
@@ -58,6 +59,7 @@ mod tooltip_unit;
|
||||
mod toplevel;
|
||||
mod update_walk;
|
||||
mod visibility;
|
||||
mod widget_args;
|
||||
mod widget_surface;
|
||||
mod worldframe;
|
||||
mod worldmap;
|
||||
|
||||
@@ -13,6 +13,7 @@ fn every_shape_c_colour_position_takes_nil_as_zero_and_never_raises() {
|
||||
T = F:CreateTexture(nil, "ARTWORK")
|
||||
FS = F:CreateFontString(nil, "ARTWORK")
|
||||
SB = CreateFrame("StatusBar", "SB")
|
||||
SB:SetStatusBarTexture("bar") -- a bar to tint: with none the call keeps nothing (`0x782dd0`)
|
||||
CS = CreateFrame("ColorSelect", "CS")
|
||||
"#,
|
||||
)
|
||||
|
||||
@@ -0,0 +1,238 @@
|
||||
//! A Button's state and checked textures have one shown bit each (`+0xc4`), the region's own:
|
||||
//! `Show`/`Hide` (`0x79b770`/`0x79b830`) and the state switch (`0x7797ea`, `0x785538`) write the
|
||||
//! same bit, `IsShown` (`0x79b9b0`) reads it and the draw follows it.
|
||||
|
||||
use super::common::script;
|
||||
use crate::script::*;
|
||||
|
||||
fn drawn(s: &UiScript) -> Vec<String> {
|
||||
s.extract()
|
||||
.iter()
|
||||
.filter_map(|q| match &q.content {
|
||||
QuadContent::Texture { path: Some(p), .. } => Some(p.clone()),
|
||||
_ => None,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn button(s: &mut UiScript, kind: &str) {
|
||||
s.set_screen_size(800.0, 600.0);
|
||||
s.run(&format!(
|
||||
r#"
|
||||
b = CreateFrame("{kind}", "ShownBitBtn")
|
||||
b:SetPoint("CENTER", 0, 0) b:SetWidth(50) b:SetHeight(50)
|
||||
b:SetNormalTexture("n") b:SetPushedTexture("p") b:SetDisabledTexture("d")
|
||||
n, p, d = b:GetNormalTexture(), b:GetPushedTexture(), b:GetDisabledTexture()
|
||||
"#
|
||||
))
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
/// `Show()` on the pushed texture of a NORMAL button writes its bit, which is all the draw reads
|
||||
/// (`0x76fea5`..`0x77fcb0`); the next state switch, `0x7797c0`, clears it again.
|
||||
#[test]
|
||||
fn show_on_a_state_texture_draws_it_until_the_next_state_switch() {
|
||||
let mut s = script();
|
||||
button(&mut s, "Button");
|
||||
s.run(r#"assert(n:IsShown() and not p:IsShown() and not d:IsShown())"#)
|
||||
.unwrap();
|
||||
s.resolve();
|
||||
assert_eq!(drawn(&s), ["n"], "only the current state draws");
|
||||
|
||||
s.run("p:Show() assert(p:IsShown())").unwrap();
|
||||
s.resolve();
|
||||
let d = drawn(&s);
|
||||
assert!(
|
||||
d.contains(&"p".to_string()) && d.contains(&"n".to_string()),
|
||||
"Show() on the pushed texture draws it beside the normal one: {d:?}"
|
||||
);
|
||||
|
||||
s.run(r#"b:SetButtonState("PUSHED")"#).unwrap();
|
||||
s.resolve();
|
||||
assert_eq!(
|
||||
drawn(&s),
|
||||
["p"],
|
||||
"the switch hides the shown one, shows the pushed one"
|
||||
);
|
||||
s.run(r#"b:SetButtonState("NORMAL") assert(not p:IsShown() and n:IsShown())"#)
|
||||
.unwrap();
|
||||
s.resolve();
|
||||
assert_eq!(
|
||||
drawn(&s),
|
||||
["n"],
|
||||
"the switch back clears the bit Show() wrote"
|
||||
);
|
||||
}
|
||||
|
||||
/// `Hide()` on the current state's texture clears the bit it draws by, and the switch away and
|
||||
/// back writes it again (`0x7797ea`).
|
||||
#[test]
|
||||
fn hide_on_the_current_state_texture_lasts_until_its_state_comes_again() {
|
||||
let mut s = script();
|
||||
button(&mut s, "Button");
|
||||
s.run("n:Hide() assert(not n:IsShown())").unwrap();
|
||||
s.resolve();
|
||||
assert!(drawn(&s).is_empty(), "{:?}", drawn(&s));
|
||||
s.run(r#"b:SetButtonState("PUSHED") b:SetButtonState("NORMAL") assert(n:IsShown())"#)
|
||||
.unwrap();
|
||||
s.resolve();
|
||||
assert_eq!(drawn(&s), ["n"]);
|
||||
}
|
||||
|
||||
/// The disabled state's switch (`Disable()` → `0x779790`) shows the disabled texture whatever
|
||||
/// `Hide()` did to it before.
|
||||
#[test]
|
||||
fn disable_shows_the_disabled_texture_a_script_hid() {
|
||||
let mut s = script();
|
||||
button(&mut s, "Button");
|
||||
s.run("d:Show() d:Hide() b:Disable() assert(d:IsShown() and not n:IsShown())")
|
||||
.unwrap();
|
||||
s.resolve();
|
||||
assert_eq!(drawn(&s), ["d"]);
|
||||
}
|
||||
|
||||
/// The checked pair's bits are written by the refresh `0x7854c0` alone: `Show()` on the checked
|
||||
/// texture of an unchecked button draws it, `SetChecked` clears both and shows the right one
|
||||
/// (`0x785538`), and a state switch that is not `Enable`/`Disable` leaves them (`0x779790` never
|
||||
/// calls the refresh).
|
||||
#[test]
|
||||
fn the_checked_pair_is_two_bits_the_refresh_writes() {
|
||||
let mut s = script();
|
||||
button(&mut s, "CheckButton");
|
||||
s.run(
|
||||
r#"
|
||||
b:SetCheckedTexture("c") b:SetDisabledCheckedTexture("dc")
|
||||
c, dc = b:GetCheckedTexture(), b:GetDisabledCheckedTexture()
|
||||
assert(not c:IsShown() and not dc:IsShown())
|
||||
c:Show()
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
s.resolve();
|
||||
assert!(drawn(&s).contains(&"c".to_string()), "{:?}", drawn(&s));
|
||||
|
||||
s.run(r#"b:SetChecked(1) assert(c:IsShown() and not dc:IsShown())"#)
|
||||
.unwrap();
|
||||
s.run(r#"b:SetButtonState("DISABLED") assert(c:IsShown() and not dc:IsShown(), "no refresh")"#)
|
||||
.unwrap();
|
||||
s.run(r#"b:Enable() assert(c:IsShown() and not dc:IsShown())"#)
|
||||
.unwrap();
|
||||
s.run(r#"b:Disable() assert(dc:IsShown() and not c:IsShown(), "Disable refreshes")"#)
|
||||
.unwrap();
|
||||
s.resolve();
|
||||
assert!(drawn(&s).contains(&"dc".to_string()), "{:?}", drawn(&s));
|
||||
s.run(r#"b:SetChecked(nil) assert(not c:IsShown() and not dc:IsShown())"#)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
/// The highlight is no shown-bit state: it links shown (`0x779139`) and the HIGHLIGHT layer's
|
||||
/// enable decides, so `Hide()` on it stays hidden through any state switch.
|
||||
#[test]
|
||||
fn the_highlight_keeps_its_own_bit_through_state_switches() {
|
||||
let mut s = script();
|
||||
button(&mut s, "Button");
|
||||
s.run(
|
||||
r#"
|
||||
b:SetHighlightTexture("h") h = b:GetHighlightTexture()
|
||||
assert(h:IsShown())
|
||||
h:Hide()
|
||||
b:SetButtonState("PUSHED") b:SetButtonState("NORMAL") b:Disable() b:Enable()
|
||||
assert(not h:IsShown())
|
||||
h:Show() assert(h:IsShown())
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
/// The engine's own edges write the bits the way `SetButtonState` does: a press (`0x779210`) and
|
||||
/// the release (`0x7792d0`), and the hide edge (`0x7791e0`) that un-pushes a button.
|
||||
#[test]
|
||||
fn the_press_release_and_hide_edges_write_the_bits() {
|
||||
let mut s = script();
|
||||
s.set_screen_size(800.0, 600.0);
|
||||
s.run(
|
||||
r#"
|
||||
b = CreateFrame("Button", "EdgeBtn")
|
||||
b:SetPoint("BOTTOMLEFT", 0, 0) b:SetWidth(100) b:SetHeight(100)
|
||||
b:SetNormalTexture("n") b:SetPushedTexture("p")
|
||||
n, p = b:GetNormalTexture(), b:GetPushedTexture()
|
||||
assert(n:IsShown() and not p:IsShown())
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
s.resolve();
|
||||
s.mouse_move(50.0, 50.0);
|
||||
s.mouse_button(50.0, 50.0, "LeftButton", true);
|
||||
s.run(r#"assert(p:IsShown() and not n:IsShown(), "pressed")"#)
|
||||
.unwrap();
|
||||
s.resolve();
|
||||
assert_eq!(drawn(&s), ["p"]);
|
||||
s.mouse_button(50.0, 50.0, "LeftButton", false);
|
||||
s.run(r#"assert(n:IsShown() and not p:IsShown(), "released")"#)
|
||||
.unwrap();
|
||||
s.resolve();
|
||||
assert_eq!(drawn(&s), ["n"]);
|
||||
|
||||
s.run(r#"b:SetButtonState("PUSHED") assert(p:IsShown() and not n:IsShown())"#)
|
||||
.unwrap();
|
||||
s.run(r#"b:Hide() assert(n:IsShown() and not p:IsShown(), "the hide edge un-pushes")"#)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
/// An XML `checked="true"` (`0x7851a6`, refresh with force 0) ends with the checked texture shown
|
||||
/// and the disabled pair hidden.
|
||||
#[test]
|
||||
fn an_xml_checked_check_button_shows_its_checked_texture() {
|
||||
let mut s = script();
|
||||
s.set_screen_size(800.0, 600.0);
|
||||
let doc = crate::framexml::parse(
|
||||
r#"<Ui>
|
||||
<CheckButton name="XmlTicked" checked="true">
|
||||
<Size><AbsDimension x="24" y="24"/></Size>
|
||||
<Anchors><Anchor point="CENTER"/></Anchors>
|
||||
<NormalTexture file="Interface\Up"/>
|
||||
<CheckedTexture file="Interface\Check"/>
|
||||
<DisabledCheckedTexture file="Interface\DisabledCheck"/>
|
||||
</CheckButton>
|
||||
<CheckButton name="XmlUnticked">
|
||||
<Size><AbsDimension x="24" y="24"/></Size>
|
||||
<Anchors><Anchor point="CENTER"/></Anchors>
|
||||
<CheckedTexture file="Interface\Check2"/>
|
||||
</CheckButton>
|
||||
</Ui>"#,
|
||||
)
|
||||
.unwrap();
|
||||
let report = crate::loader::load(&s, &doc, &|_: &str| None);
|
||||
assert!(report.errors.is_empty(), "errors: {:?}", report.errors);
|
||||
s.run(
|
||||
r#"
|
||||
assert(XmlTicked:GetCheckedTexture():IsShown(), "checked")
|
||||
assert(not XmlTicked:GetDisabledCheckedTexture():IsShown(), "disabled pair")
|
||||
assert(not XmlUnticked:GetCheckedTexture():IsShown(), "unchecked")
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
s.resolve();
|
||||
let d = drawn(&s);
|
||||
assert!(d.contains(&"Interface\\Check".to_string()), "{d:?}");
|
||||
assert!(!d.contains(&"Interface\\Check2".to_string()), "{d:?}");
|
||||
}
|
||||
|
||||
/// The CheckButton override of `SetEnabled` (`0x785490`) refreshes the pair (`0x7854aa`) whether or
|
||||
/// not the base changed anything, so an `Enable()` on an enabled button re-shows a checked texture
|
||||
/// a script hid.
|
||||
#[test]
|
||||
fn an_enable_that_changes_nothing_still_refreshes_the_checked_pair() {
|
||||
let mut s = script();
|
||||
button(&mut s, "CheckButton");
|
||||
s.run(
|
||||
r#"
|
||||
b:SetCheckedTexture("c") c = b:GetCheckedTexture()
|
||||
b:SetChecked(1)
|
||||
c:Hide() assert(not c:IsShown())
|
||||
b:Enable()
|
||||
assert(c:IsShown(), "refreshed")
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
@@ -73,7 +73,11 @@ fn statusbar_bar_region_scales_by_fraction_on_extract() {
|
||||
local b = CreateFrame("StatusBar", "SbFill")
|
||||
b:SetPoint("BOTTOMLEFT", nil, "BOTTOMLEFT", 100, 100)
|
||||
b:SetWidth(200); b:SetHeight(20)
|
||||
b:SetStatusBarTexture(0.2, 0.7, 0.2)
|
||||
-- A solid bar is a texture filled with a colour, installed as the object: the bar setter
|
||||
-- has no colour form (`0x78f870`).
|
||||
local fill = b:CreateTexture(nil, "ARTWORK")
|
||||
fill:SetTexture(0.2, 0.7, 0.2)
|
||||
b:SetStatusBarTexture(fill)
|
||||
b:SetMinMaxValues(0, 100)
|
||||
b:SetValue(25)
|
||||
"#,
|
||||
|
||||
@@ -0,0 +1,408 @@
|
||||
//! The argument forms of the widget texture setters (`SetStatusBarTexture` `0x78f870`,
|
||||
//! `SetThumbTexture` `0x7902b0`, the four ColorSelect setters `0x78de90`, `0x78e160`, `0x78e450`,
|
||||
//! `0x78e720`), and the bar texture's texcoord flag (`0x7831f0`). Each forks on `lua_type`
|
||||
//! (`0x6f3400`) and `lua_isstring` (`0x6f3510`), which takes a string or a number: a Texture object
|
||||
//! is installed, a string or number is loaded as a path, nil clears, anything else, no argument
|
||||
//! included, raises the usage. There is no colour form.
|
||||
|
||||
use super::common::script;
|
||||
use crate::script::*;
|
||||
|
||||
fn err(s: &UiScript, code: &str) -> String {
|
||||
s.run(code).unwrap_err().to_string()
|
||||
}
|
||||
|
||||
fn xml(s: &UiScript, text: &str) {
|
||||
let doc = crate::framexml::parse(text).expect("valid FrameXML");
|
||||
let report = crate::loader::load(s, &doc, &|_: &str| None);
|
||||
assert!(report.errors.is_empty(), "errors: {:?}", report.errors);
|
||||
}
|
||||
|
||||
/// `0x7831f0` writes `[texture+0x124] = 1` (`0x783220`) after it links the texture in, on the path
|
||||
/// leg (`0x7831cb`), the object leg (`0x78fa27`) and `LoadXML`'s (`0x782f63`);
|
||||
/// `GetTexCoordModifiesRect` (`0x79c120`) reads it.
|
||||
#[test]
|
||||
fn a_bar_texture_answers_one_to_get_tex_coord_modifies_rect() {
|
||||
let s = script();
|
||||
xml(
|
||||
&s,
|
||||
r#"<Ui>
|
||||
<StatusBar name="XmlFileBar"><BarTexture file="Interface\Bar"/></StatusBar>
|
||||
<StatusBar name="XmlColorBar"><BarTexture><Color r="1" g="0" b="0"/></BarTexture></StatusBar>
|
||||
</Ui>"#,
|
||||
);
|
||||
s.run(
|
||||
r#"
|
||||
assert(XmlFileBar:GetStatusBarTexture():GetTexCoordModifiesRect() == 1, "xml file")
|
||||
assert(XmlColorBar:GetStatusBarTexture():GetTexCoordModifiesRect() == 1, "xml colour")
|
||||
|
||||
local p = CreateFrame("StatusBar", "PathBar")
|
||||
p:SetStatusBarTexture("Interface\\Bar")
|
||||
assert(p:GetStatusBarTexture():GetTexCoordModifiesRect() == 1, "path")
|
||||
|
||||
local o = CreateFrame("StatusBar", "ObjBar")
|
||||
local t = o:CreateTexture(nil, "ARTWORK")
|
||||
assert(t:GetTexCoordModifiesRect() == nil, "a fresh texture")
|
||||
o:SetStatusBarTexture(t)
|
||||
assert(t:GetTexCoordModifiesRect() == 1, "object")
|
||||
|
||||
-- A bar that exists takes the path leg's SetTexture alone (`0x783140`), so what a script
|
||||
-- wrote to the flag stays.
|
||||
t:SetTexCoordModifiesRect(nil)
|
||||
o:SetStatusBarTexture("Interface\\Other")
|
||||
assert(o:GetStatusBarTexture() == t and t:GetTexCoordModifiesRect() == nil, "reused")
|
||||
-- A displaced texture is gone; its replacement is a new object and gets the flag.
|
||||
local u = o:CreateTexture(nil, "ARTWORK")
|
||||
o:SetStatusBarTexture(u)
|
||||
assert(u:GetTexCoordModifiesRect() == 1, "replacement")
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
/// `lua_isstring` accepts a number and `lua_tostring` converts it (`0x78fa35`..`0x78fa51`), so
|
||||
/// `1` is the file `"1"`, and the `(r, g, b)` form of an older client is the path `"0.2"` with the
|
||||
/// two other numbers read as a layer name that is none. `GetTexture` answers a path without its
|
||||
/// extension, so `"0.2"` reads `"0"`; the quad is what shows the whole file name.
|
||||
#[test]
|
||||
fn a_number_loads_as_its_own_path() {
|
||||
let mut s = script();
|
||||
s.set_screen_size(800.0, 600.0);
|
||||
s.run(
|
||||
r#"
|
||||
local sb = CreateFrame("StatusBar", "NumBar")
|
||||
sb:SetPoint("CENTER", 0, 0) sb:SetWidth(50) sb:SetHeight(50)
|
||||
sb:SetMinMaxValues(0, 1) sb:SetValue(1)
|
||||
sb:SetStatusBarTexture(0.2, 0.7, 0.2)
|
||||
assert(sb:GetStatusBarTexture():GetTexture() ~= "Solid Texture", "no solid fill")
|
||||
local sl = CreateFrame("Slider", "NumSlider")
|
||||
sl:SetThumbTexture(2, 0.5, 0.5, 1)
|
||||
assert(sl:GetThumbTexture():GetTexture() == "2")
|
||||
|
||||
local cs = CreateFrame("ColorSelect", "NumCS")
|
||||
cs:SetColorWheelThumbTexture(3)
|
||||
cs:SetColorValueThumbTexture(4)
|
||||
assert(cs:GetColorWheelThumbTexture():GetTexture() == "3")
|
||||
assert(cs:GetColorValueThumbTexture():GetTexture() == "4")
|
||||
|
||||
local one = CreateFrame("StatusBar", "NumBarOne")
|
||||
one:SetStatusBarTexture(1)
|
||||
assert(one:GetStatusBarTexture():GetTexture() == "1")
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
s.resolve();
|
||||
let paths: Vec<String> = s
|
||||
.extract()
|
||||
.iter()
|
||||
.filter_map(|q| match &q.content {
|
||||
QuadContent::Texture {
|
||||
path: Some(p),
|
||||
color: None,
|
||||
..
|
||||
} => Some(p.clone()),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
assert!(paths.contains(&"0.2".to_string()), "{paths:?}");
|
||||
}
|
||||
|
||||
/// No argument is not nil (`0x6f3400` answers -1), and any other type is no texture either: the
|
||||
/// usage line (`0x87b1a0`, `0x87b2f0`, `0x87adc0`, `0x87af40`, `0x87ad10`, `0x87ae90`) with the
|
||||
/// frame's name, and nothing changes.
|
||||
#[test]
|
||||
fn no_argument_and_other_types_raise_the_usage() {
|
||||
let s = script();
|
||||
s.run(
|
||||
r#"
|
||||
sb = CreateFrame("StatusBar", "UseBar")
|
||||
sl = CreateFrame("Slider", "UseSlider")
|
||||
cs = CreateFrame("ColorSelect", "UseCS")
|
||||
sb:SetStatusBarTexture("keep")
|
||||
sl:SetThumbTexture("keep")
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
for (call, usage) in [
|
||||
(
|
||||
"sb:SetStatusBarTexture()",
|
||||
r#"Usage: UseBar:SetStatusBarTexture(texture or "texture" or nil [, "layer"])"#,
|
||||
),
|
||||
(
|
||||
"sb:SetStatusBarTexture(true)",
|
||||
r#"Usage: UseBar:SetStatusBarTexture(texture or "texture" or nil [, "layer"])"#,
|
||||
),
|
||||
(
|
||||
"sl:SetThumbTexture()",
|
||||
r#"Usage: UseSlider:SetThumbTexture(texture or "texture" or nil)"#,
|
||||
),
|
||||
(
|
||||
"sl:SetThumbTexture(function() end)",
|
||||
r#"Usage: UseSlider:SetThumbTexture(texture or "texture" or nil)"#,
|
||||
),
|
||||
(
|
||||
"cs:SetColorWheelThumbTexture()",
|
||||
r#"Usage: UseCS:SetColorWheelThumbTexture(texture or "texture" or nil)"#,
|
||||
),
|
||||
(
|
||||
"cs:SetColorValueThumbTexture(false)",
|
||||
r#"Usage: UseCS:SetColorValueThumbTexture(texture or "texture" or nil)"#,
|
||||
),
|
||||
(
|
||||
"cs:SetColorWheelTexture()",
|
||||
"Usage: UseCS:SetColorWheelTexture(texture or nil)",
|
||||
),
|
||||
(
|
||||
"cs:SetColorValueTexture()",
|
||||
"Usage: UseCS:SetColorValueTexture(texture or nil)",
|
||||
),
|
||||
] {
|
||||
let e = err(&s, call);
|
||||
assert!(e.contains(usage), "`{call}` -> {e}");
|
||||
}
|
||||
s.run(
|
||||
r#"
|
||||
assert(sb:GetStatusBarTexture():GetTexture() == "keep")
|
||||
assert(sl:GetThumbTexture():GetTexture() == "keep")
|
||||
assert(cs:GetColorWheelTexture() == nil and cs:GetColorWheelThumbTexture() == nil)
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
/// The wheel and the strip have no path leg (`0x87ad10`, `0x87ae90`): a string or a number raises,
|
||||
/// a Texture object installs and nil clears.
|
||||
#[test]
|
||||
fn the_wheel_and_the_strip_take_a_texture_or_nil_only() {
|
||||
let s = script();
|
||||
s.run(
|
||||
r#"
|
||||
cs = CreateFrame("ColorSelect", "WheelCS")
|
||||
t = cs:CreateTexture(nil, "ARTWORK")
|
||||
cs:SetColorWheelTexture(t)
|
||||
assert(cs:GetColorWheelTexture() == t)
|
||||
cs:SetColorWheelTexture(nil)
|
||||
assert(cs:GetColorWheelTexture() == nil)
|
||||
t = cs:CreateTexture(nil, "ARTWORK")
|
||||
cs:SetColorValueTexture(t)
|
||||
assert(cs:GetColorValueTexture() == t)
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
for call in [
|
||||
r#"cs:SetColorWheelTexture("Interface\\Wheel")"#,
|
||||
"cs:SetColorWheelTexture(1)",
|
||||
r#"cs:SetColorValueTexture("Interface\\Strip")"#,
|
||||
] {
|
||||
let e = err(&s, call);
|
||||
assert!(e.contains("Usage: WheelCS:SetColor"), "`{call}` -> {e}");
|
||||
}
|
||||
s.run("assert(cs:GetColorValueTexture() == t)").unwrap();
|
||||
}
|
||||
|
||||
/// The thumb setters read no layer (`0x7902b0` has no layer argument): a second argument is
|
||||
/// ignored, and the thumb is OVERLAY. The bar reads one (`0x78f8fb`) for a bar it creates, and an
|
||||
/// existing bar keeps its layer (`0x783140` never reads it).
|
||||
#[test]
|
||||
fn the_thumbs_take_no_layer_and_an_existing_bar_keeps_its_own() {
|
||||
let s = script();
|
||||
s.run(
|
||||
r#"
|
||||
local sl = CreateFrame("Slider", "LayerSlider")
|
||||
sl:SetThumbTexture("p", "BACKGROUND")
|
||||
assert(sl:GetThumbTexture():GetDrawLayer() == "OVERLAY", sl:GetThumbTexture():GetDrawLayer())
|
||||
|
||||
local sb = CreateFrame("StatusBar", "LayerBar")
|
||||
sb:SetStatusBarTexture("a", "BORDER")
|
||||
assert(sb:GetStatusBarTexture():GetDrawLayer() == "BORDER")
|
||||
sb:SetStatusBarTexture("b", "HIGHLIGHT")
|
||||
assert(sb:GetStatusBarTexture():GetDrawLayer() == "BORDER", "an existing bar keeps its layer")
|
||||
assert(sb:GetStatusBarTexture():GetTexture() == "b")
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
/// What the loader reaches with the colour form the Lua setters no longer have: a `<Color>` in
|
||||
/// `<BarTexture>` or `<ThumbTexture>` is the texture's own fill, and a file-less ColorSelect
|
||||
/// element still creates its region.
|
||||
#[test]
|
||||
fn the_loader_still_fills_and_creates_what_the_setters_no_longer_take() {
|
||||
let mut s = script();
|
||||
s.set_screen_size(800.0, 600.0);
|
||||
xml(
|
||||
&s,
|
||||
r#"<Ui>
|
||||
<StatusBar name="FillBar" minValue="0" maxValue="100" defaultValue="100">
|
||||
<Size><AbsDimension x="100" y="10"/></Size>
|
||||
<Anchors><Anchor point="BOTTOMLEFT"/></Anchors>
|
||||
<BarTexture><Color r="0.25" g="0.5" b="0.75" a="1"/></BarTexture>
|
||||
</StatusBar>
|
||||
<Slider name="FillSlider">
|
||||
<Size><AbsDimension x="10" y="100"/></Size>
|
||||
<Anchors><Anchor point="TOPLEFT"/></Anchors>
|
||||
<ThumbTexture name="FillThumb"><Size><AbsDimension x="10" y="10"/></Size><Color r="0.75" g="0.5" b="0.25" a="1"/></ThumbTexture>
|
||||
</Slider>
|
||||
<ColorSelect name="BareCS">
|
||||
<Size><AbsDimension x="100" y="100"/></Size>
|
||||
<Anchors><Anchor point="CENTER"/></Anchors>
|
||||
<ColorWheelTexture/>
|
||||
<ColorWheelThumbTexture/>
|
||||
<ColorValueTexture/>
|
||||
<ColorValueThumbTexture/>
|
||||
</ColorSelect>
|
||||
</Ui>"#,
|
||||
);
|
||||
s.run(
|
||||
r#"
|
||||
assert(FillBar:GetStatusBarTexture() ~= nil and FillSlider:GetThumbTexture() ~= nil)
|
||||
assert(BareCS:GetColorWheelTexture() ~= nil and BareCS:GetColorWheelThumbTexture() ~= nil)
|
||||
assert(BareCS:GetColorValueTexture() ~= nil and BareCS:GetColorValueThumbTexture() ~= nil)
|
||||
assert(BareCS:GetColorWheelTexture():GetTexture() == nil)
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
s.resolve();
|
||||
let fills: Vec<[f32; 4]> = s
|
||||
.extract()
|
||||
.iter()
|
||||
.filter_map(|q| match &q.content {
|
||||
QuadContent::Texture {
|
||||
path: None,
|
||||
color: Some(c),
|
||||
..
|
||||
} => Some(*c),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
assert!(fills.contains(&[0.25, 0.5, 0.75, 1.0]), "{fills:?}");
|
||||
assert!(fills.contains(&[0.75, 0.5, 0.25, 1.0]), "{fills:?}");
|
||||
}
|
||||
|
||||
/// The bar's path leg gives a new bar texture all points on its bar (`0x7831bf`), as the XML leg's
|
||||
/// post-step does (`0x7701c0`); a texture handed in as an object keeps what it has.
|
||||
#[test]
|
||||
fn an_installed_bar_texture_has_all_points_on_its_bar() {
|
||||
let s = script();
|
||||
xml(
|
||||
&s,
|
||||
r#"<Ui>
|
||||
<StatusBar name="PtsXmlBar"><BarTexture file="Interface\Bar"/></StatusBar>
|
||||
</Ui>"#,
|
||||
);
|
||||
s.run(
|
||||
r#"
|
||||
local function onbar(t, bar)
|
||||
assert(t:GetNumPoints() == 2, "points: " .. t:GetNumPoints())
|
||||
local _, rel = t:GetPoint(1)
|
||||
assert(rel == bar, "relative to the bar")
|
||||
end
|
||||
onbar(PtsXmlBar:GetStatusBarTexture(), PtsXmlBar)
|
||||
|
||||
local p = CreateFrame("StatusBar", "PtsPathBar")
|
||||
p:SetStatusBarTexture("Interface\\Bar")
|
||||
onbar(p:GetStatusBarTexture(), p)
|
||||
|
||||
local o = CreateFrame("StatusBar", "PtsObjBar")
|
||||
local t = o:CreateTexture(nil, "ARTWORK")
|
||||
assert(t:GetNumPoints() == 0)
|
||||
o:SetStatusBarTexture(t)
|
||||
assert(t:GetNumPoints() == 0, "an object keeps what it has")
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
/// An empty `<BarTexture/>` or `<ThumbTexture/>` still installs the texture the texture adder
|
||||
/// builds (`0x782f63`, `0x7895f6`), and draws nothing.
|
||||
#[test]
|
||||
fn an_empty_bar_or_thumb_element_installs_a_texture() {
|
||||
let mut s = script();
|
||||
s.set_screen_size(800.0, 600.0);
|
||||
xml(
|
||||
&s,
|
||||
r#"<Ui>
|
||||
<StatusBar name="EmptyBar"><Size><AbsDimension x="50" y="10"/></Size>
|
||||
<Anchors><Anchor point="CENTER"/></Anchors><BarTexture/></StatusBar>
|
||||
<Slider name="EmptySlider"><Size><AbsDimension x="10" y="50"/></Size>
|
||||
<Anchors><Anchor point="TOPLEFT"/></Anchors><ThumbTexture/></Slider>
|
||||
</Ui>"#,
|
||||
);
|
||||
s.run(
|
||||
r#"
|
||||
assert(EmptyBar:GetStatusBarTexture() ~= nil, "bar")
|
||||
assert(EmptyBar:GetStatusBarTexture():GetTexCoordModifiesRect() == 1)
|
||||
assert(EmptySlider:GetThumbTexture() ~= nil, "thumb")
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
s.resolve();
|
||||
let textured = s
|
||||
.extract()
|
||||
.iter()
|
||||
.filter(|q| {
|
||||
matches!(
|
||||
q.content,
|
||||
QuadContent::Texture { path: Some(_), .. }
|
||||
| QuadContent::Texture { color: Some(_), .. }
|
||||
)
|
||||
})
|
||||
.count();
|
||||
assert_eq!(textured, 0, "an empty texture draws nothing");
|
||||
}
|
||||
|
||||
/// `SetStatusBarColor` (`0x78fc20` → `0x782dd0`) tints the bar only if one exists and keeps
|
||||
/// nothing otherwise: no bar, no effect, and a bar installed later is untinted.
|
||||
#[test]
|
||||
fn set_status_bar_color_with_no_bar_does_nothing() {
|
||||
let s = script();
|
||||
s.run(
|
||||
r#"
|
||||
local sb = CreateFrame("StatusBar", "TintBar")
|
||||
sb:SetStatusBarColor(1, 0, 0)
|
||||
assert(sb:GetStatusBarTexture() == nil, "no bar made")
|
||||
sb:SetStatusBarTexture("p")
|
||||
local r, g, b, a = sb:GetStatusBarColor()
|
||||
assert(r == 1 and g == 1 and b == 1 and a == 1, "untinted: " .. r .. g .. b)
|
||||
sb:SetStatusBarColor(0, 1, 0)
|
||||
r, g, b = sb:GetStatusBarColor()
|
||||
assert(r == 0 and g == 1 and b == 0, "tinted once there is a bar")
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
/// `<BarTexture>` and `<ThumbTexture>` go through the texture adder (`0x6f26f0`): a named one is
|
||||
/// published under its expanded name, and its `<TexCoords>` and `alphaMode` apply.
|
||||
#[test]
|
||||
fn a_named_bar_texture_is_published_and_keeps_its_texcoords() {
|
||||
let s = script();
|
||||
xml(
|
||||
&s,
|
||||
r#"<Ui>
|
||||
<StatusBar name="NamedBarFrame">
|
||||
<BarTexture name="$parentBar" file="Interface\Bar" alphaMode="ADD">
|
||||
<TexCoords left="0.1" right="0.9" top="0.2" bottom="0.8"/>
|
||||
</BarTexture>
|
||||
</StatusBar>
|
||||
<Slider name="NamedSliderFrame">
|
||||
<ThumbTexture name="$parentThumb" file="Interface\Knob">
|
||||
<TexCoords left="0.25" right="0.75" top="0" bottom="1"/>
|
||||
</ThumbTexture>
|
||||
</Slider>
|
||||
</Ui>"#,
|
||||
);
|
||||
s.run(
|
||||
r#"
|
||||
assert(NamedBarFrameBar == NamedBarFrame:GetStatusBarTexture(), "published")
|
||||
assert(NamedBarFrameBar:GetName() == "NamedBarFrameBar")
|
||||
local ulx, uly = NamedBarFrameBar:GetTexCoord()
|
||||
assert(math.abs(ulx - 0.1) < 1e-5 and math.abs(uly - 0.2) < 1e-5, ulx .. " " .. uly)
|
||||
assert(NamedBarFrameBar:GetBlendMode() == "ADD")
|
||||
assert(NamedSliderFrameThumb == NamedSliderFrame:GetThumbTexture(), "thumb published")
|
||||
ulx = NamedSliderFrameThumb:GetTexCoord()
|
||||
assert(math.abs(ulx - 0.25) < 1e-5)
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
@@ -381,9 +381,10 @@ pub(crate) struct RegionData {
|
||||
pub(crate) justify: crate::justify::Justify,
|
||||
/// The `<TexCoords>`/`SetTexCoord` mapping; `None` is the full texture.
|
||||
pub(crate) tex_coords: Option<TexCoords>,
|
||||
/// `SetTexCoordModifiesRect`, the reference's `+0x124` (written only at `0x79c113`, in
|
||||
/// `0x79c080`). Stored, not applied: there a set flag makes `SetTexCoord` re-derive the rect
|
||||
/// from the UV quad (`0x770462`); no stock or corpus code sets it.
|
||||
/// The reference's `+0x124`: `SetTexCoordModifiesRect` writes it (`0x79c113`, in `0x79c080`)
|
||||
/// and installing the texture as a StatusBar's bar sets it (`0x783220`, in `0x7831f0`). Stored,
|
||||
/// not applied: there a set flag makes `SetTexCoord` re-derive the rect from the UV quad
|
||||
/// (`0x770462`); no stock or corpus code sets it.
|
||||
pub(crate) tex_coord_modifies_rect: bool,
|
||||
/// Rotation about the centre, radians counterclockwise; always 0: 1.12 has no Texture setter.
|
||||
pub(crate) rotation: f32,
|
||||
|
||||
@@ -745,6 +745,23 @@ pub enum ButtonVisualState {
|
||||
Pushed,
|
||||
}
|
||||
|
||||
/// The writes to regions' shown bit (`+0xc4`) a Button's state machine makes, in order. The bit
|
||||
/// is the region's own, the one `Show`/`Hide` write and `IsShown` and the draw read, so the
|
||||
/// machine hands its writes back for the caller to apply to the regions.
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
||||
#[must_use = "the caller writes these bits into the regions"]
|
||||
pub struct ShownWrites(pub Vec<(RegionHandle, bool)>);
|
||||
|
||||
impl ShownWrites {
|
||||
fn hide(&mut self, rh: Option<RegionHandle>) {
|
||||
self.0.extend(rh.map(|rh| (rh, false)));
|
||||
}
|
||||
|
||||
fn show(&mut self, rh: Option<RegionHandle>) {
|
||||
self.0.extend(rh.map(|rh| (rh, true)));
|
||||
}
|
||||
}
|
||||
|
||||
/// A button's state. The drawn state texture is latched, not derived: the shown pointer
|
||||
/// (`+0x4c4`) moves only on a transition into a state that has a texture ([`Self::set_state`]).
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
@@ -848,22 +865,53 @@ impl ButtonState {
|
||||
|
||||
/// `SetState` (`0x779790`): entering a state with no texture keeps the shown one
|
||||
/// (`0x7797be`/`0x7797e2`). A button is NORMAL before `LoadXML` adds its art (ctor
|
||||
/// `0x7786a0`), so a press or `Disable()` without its own texture keeps the normal art.
|
||||
fn set_state(&mut self, new: ButtonVisualState) {
|
||||
/// `0x7786a0`), so a press or `Disable()` without its own texture keeps the normal art. The
|
||||
/// hide of the old (`0x7797c0`) runs only when the new state has a texture too, and the show of
|
||||
/// the new (`0x7797ea`) writes its bit.
|
||||
fn set_state(&mut self, new: ButtonVisualState) -> ShownWrites {
|
||||
if new == self.state {
|
||||
return;
|
||||
return ShownWrites::default();
|
||||
}
|
||||
self.state = new;
|
||||
let mut w = ShownWrites::default();
|
||||
if let Some(slot) = self.state_slot(new) {
|
||||
w.hide(self.shown);
|
||||
self.shown = Some(slot);
|
||||
w.show(Some(slot));
|
||||
}
|
||||
w
|
||||
}
|
||||
|
||||
/// `SetButtonState(state, locked)` (`0x779790`), the one writer: the lock is stored before the
|
||||
/// equal-state early-out (`0x77979d`), so it is set or cleared even when the state holds.
|
||||
pub fn set_button_state(&mut self, new: ButtonVisualState, locked: bool) {
|
||||
pub fn set_button_state(&mut self, new: ButtonVisualState, locked: bool) -> ShownWrites {
|
||||
self.locked = locked;
|
||||
self.set_state(new);
|
||||
self.set_state(new)
|
||||
}
|
||||
|
||||
/// `CSimpleCheckbox::Refresh` (`0x7854c0`): clears both checked textures' bits, then shows
|
||||
/// the one the checked flag and the state pick (`0x785538`), the disabled pair's when DISABLED
|
||||
/// and it exists, else the checked texture. `force` is its second argument: without it a flag
|
||||
/// that holds does nothing (`0x7854d0`). Only `SetChecked`, a click, `SetEnabled`, the two
|
||||
/// checked setters and the XML `checked=` attribute (`0x7851a6`) reach it; a state switch alone
|
||||
/// leaves the pair. The reference's `checked=` runs before the checked textures exist, so it
|
||||
/// shows nothing and each setter's refresh shows its texture; benilla's loader calls
|
||||
/// `SetChecked` after the textures, which ends in the same state.
|
||||
pub fn refresh_checked(&mut self, checked: bool, force: bool) -> ShownWrites {
|
||||
if !force && checked == self.checked {
|
||||
return ShownWrites::default();
|
||||
}
|
||||
self.checked = checked;
|
||||
let mut w = ShownWrites::default();
|
||||
w.hide(self.checked_tex);
|
||||
w.hide(self.disabled_checked);
|
||||
if checked {
|
||||
w.show(match self.disabled_checked {
|
||||
Some(dc) if !self.enabled() => Some(dc),
|
||||
_ => self.checked_tex,
|
||||
});
|
||||
}
|
||||
w
|
||||
}
|
||||
|
||||
/// `IsEnabled` (`0x7800b0`): the state alone; the class has no enabled flag.
|
||||
@@ -880,65 +928,89 @@ impl ButtonState {
|
||||
// Six of `0x779790`'s seven call sites, all passing `locked = 0`; the seventh is Lua's.
|
||||
|
||||
/// `SetEnabled` (`0x779160`): a no-op if nothing changes (`0x779175`/`0x77919c`), so enabling a
|
||||
/// pushed button neither un-pushes nor unlocks it; returns whether it ran.
|
||||
pub fn set_enabled(&mut self, on: bool) -> bool {
|
||||
if on == self.enabled() {
|
||||
return false;
|
||||
}
|
||||
self.set_button_state(
|
||||
if on {
|
||||
ButtonVisualState::Normal
|
||||
} else {
|
||||
ButtonVisualState::Disabled
|
||||
},
|
||||
false,
|
||||
);
|
||||
true
|
||||
/// pushed button neither un-pushes nor unlocks it. A CheckButton's override (`0x785490`) then
|
||||
/// refreshes the checked pair whether or not it changed, which a Button, with no checked
|
||||
/// textures, does not notice.
|
||||
pub fn set_enabled(&mut self, on: bool) -> ShownWrites {
|
||||
let mut w = if on == self.enabled() {
|
||||
ShownWrites::default()
|
||||
} else {
|
||||
self.set_button_state(
|
||||
if on {
|
||||
ButtonVisualState::Normal
|
||||
} else {
|
||||
ButtonVisualState::Disabled
|
||||
},
|
||||
false,
|
||||
)
|
||||
};
|
||||
w.0.extend(self.refresh_checked(self.checked, true).0);
|
||||
w
|
||||
}
|
||||
|
||||
/// The hide edge (`0x7791e0`): an unlocked, enabled button goes NORMAL; `<OnHide>` still fires.
|
||||
pub fn on_hide(&mut self) {
|
||||
pub fn on_hide(&mut self) -> ShownWrites {
|
||||
if self.state != ButtonVisualState::Disabled && !self.locked {
|
||||
self.set_button_state(ButtonVisualState::Normal, false);
|
||||
self.set_button_state(ButtonVisualState::Normal, false)
|
||||
} else {
|
||||
ShownWrites::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// The press edge (`0x779210`), past the registration and hit gates, so a registered click
|
||||
/// pushes the button even when the click itself does nothing.
|
||||
pub fn on_mouse_down(&mut self) {
|
||||
pub fn on_mouse_down(&mut self) -> ShownWrites {
|
||||
if !self.locked && self.state != ButtonVisualState::Disabled {
|
||||
self.set_button_state(ButtonVisualState::Pushed, false);
|
||||
self.set_button_state(ButtonVisualState::Pushed, false)
|
||||
} else {
|
||||
ShownWrites::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// The release edge (`0x7792d0`): a pushed, unlocked button goes NORMAL wherever the cursor
|
||||
/// is, even one Lua pushed. The caller skips it when the release ends a drag (`0x7792df`).
|
||||
pub fn on_mouse_up(&mut self) {
|
||||
pub fn on_mouse_up(&mut self) -> ShownWrites {
|
||||
if !self.locked && self.state == ButtonVisualState::Pushed {
|
||||
self.set_button_state(ButtonVisualState::Normal, false);
|
||||
self.set_button_state(ButtonVisualState::Normal, false)
|
||||
} else {
|
||||
ShownWrites::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// The drag-start edge (`0x7793f0`), before `<OnDragStart>`: it fires at the drag threshold
|
||||
/// (0.01 frame units, `0x81c468`) on a `RegisterForDrag` frame, not at the rect edge.
|
||||
pub fn on_drag_start(&mut self) {
|
||||
pub fn on_drag_start(&mut self) -> ShownWrites {
|
||||
if !self.locked && self.state != ButtonVisualState::Disabled {
|
||||
self.set_button_state(ButtonVisualState::Normal, false);
|
||||
self.set_button_state(ButtonVisualState::Normal, false)
|
||||
} else {
|
||||
ShownWrites::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// `SetNormalTexture` and kin (`0x778fd0`): stores the slot, shown only if it is the current
|
||||
/// state's (`0x779027`). The reference destroys a displaced texture (`0x77900a`); here the
|
||||
/// region is made once and repainted, so a held `GetNormalTexture()` stays live.
|
||||
pub fn set_state_slot(&mut self, state: ButtonVisualState, rh: Option<RegionHandle>) {
|
||||
match state {
|
||||
ButtonVisualState::Disabled => self.disabled = rh,
|
||||
ButtonVisualState::Normal => self.normal = rh,
|
||||
ButtonVisualState::Pushed => self.pushed = rh,
|
||||
/// `SetNormalTexture` and kin (`0x778fd0`): stores the slot; the state it is for being the
|
||||
/// current one, the texture becomes the shown one and its bit is written (`0x779027`..
|
||||
/// `0x779037`). A shown pointer at the displaced texture is dropped first (`0x778fe9`). The
|
||||
/// reference destroys a displaced texture (`0x77900a`); the caller frees it.
|
||||
pub fn set_state_slot(
|
||||
&mut self,
|
||||
state: ButtonVisualState,
|
||||
rh: Option<RegionHandle>,
|
||||
) -> ShownWrites {
|
||||
let slot = match state {
|
||||
ButtonVisualState::Disabled => &mut self.disabled,
|
||||
ButtonVisualState::Normal => &mut self.normal,
|
||||
ButtonVisualState::Pushed => &mut self.pushed,
|
||||
};
|
||||
let old = std::mem::replace(slot, rh);
|
||||
if self.shown == old {
|
||||
self.shown = None;
|
||||
}
|
||||
if state == self.state {
|
||||
let mut w = ShownWrites::default();
|
||||
if state == self.state && rh.is_some() {
|
||||
self.shown = rh;
|
||||
w.show(rh);
|
||||
}
|
||||
w
|
||||
}
|
||||
|
||||
/// The region-removed hook: a CheckButton drops its checked pair (`0x785230`, `+0x4e0` and
|
||||
@@ -961,38 +1033,12 @@ impl ButtonState {
|
||||
}
|
||||
}
|
||||
|
||||
/// The shown bit (`+0xc4`) of a state or checked texture, which the state switch writes for
|
||||
/// the one that shows (`0x7797ea`, `0x785538`) and clears on the rest: `None` for every other
|
||||
/// region, whose bit is its own `Show`/`Hide` alone.
|
||||
pub fn state_region_shown(&self, rh: RegionHandle) -> Option<bool> {
|
||||
let some = Some(rh);
|
||||
if some == self.normal || some == self.pushed || some == self.disabled {
|
||||
return Some(some == self.shown);
|
||||
}
|
||||
if some == self.checked_tex || some == self.disabled_checked {
|
||||
return Some(self.region_visible(rh, false));
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Whether a region of this button draws: a state texture only when shown, the highlight
|
||||
/// while hovered or locked, the checked pair by their rule, anything else always.
|
||||
pub fn region_visible(&self, rh: RegionHandle, hovered: bool) -> bool {
|
||||
let some = Some(rh);
|
||||
if some == self.normal || some == self.pushed || some == self.disabled {
|
||||
return some == self.shown;
|
||||
}
|
||||
if some == self.highlight {
|
||||
// No `enabled` term: `Disable()` turns the whole HIGHLIGHT layer off before this.
|
||||
return hovered || self.locked_highlight;
|
||||
}
|
||||
if some == self.checked_tex {
|
||||
return self.checked && (self.enabled() || self.disabled_checked.is_none());
|
||||
}
|
||||
if some == self.disabled_checked {
|
||||
return self.checked && !self.enabled();
|
||||
}
|
||||
true
|
||||
/// Whether the Highlight region draws: while hovered or locked. The reference gates it by its
|
||||
/// layer's enable (`0x76b6a0`/`0x76b6f0`), not by its shown bit, which it keeps from the
|
||||
/// link (`0x779139`); every other region of a Button draws by its own bit alone.
|
||||
pub fn highlight_draws(&self, rh: RegionHandle, hovered: bool) -> bool {
|
||||
// No `enabled` term: `Disable()` turns the whole HIGHLIGHT layer off before this.
|
||||
Some(rh) != self.highlight || hovered || self.locked_highlight
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -122,8 +122,8 @@ pub use kinds::{
|
||||
ButtonState, ButtonVisualState, ColorSelectState, EditAction, EditBoxState, EditOutcome,
|
||||
EditUnit, FrameKind, InsertMode, KindState, KindUpdate, MessageFrameState, MessageLine,
|
||||
MinimapState, ModelFileFacts, ModelFog, ModelLight, ModelPlayHead, ModelState, RegionKind,
|
||||
ScrollFrameState, ScrollingMessageState, SequenceFacts, SliderState, StatusBarState,
|
||||
TooltipAnchor, TooltipState, MINIMAP_DEFAULT_ARROW_MODEL, MINIMAP_DEFAULT_MASK,
|
||||
ScrollFrameState, ScrollingMessageState, SequenceFacts, ShownWrites, SliderState,
|
||||
StatusBarState, TooltipAnchor, TooltipState, MINIMAP_DEFAULT_ARROW_MODEL, MINIMAP_DEFAULT_MASK,
|
||||
MINIMAP_DEFAULT_PLAYER_MODEL, MINIMAP_DEFAULT_ZOOM, MINIMAP_ENGINE_CHILDREN,
|
||||
MINIMAP_ZOOM_LEVELS, TOOLTIP_DOUBLE_GAP, TOOLTIP_FADE_SECS, TOOLTIP_LINE_GAP, TOOLTIP_PAD,
|
||||
TOOLTIP_WRAP_WIDTH,
|
||||
|
||||
@@ -424,7 +424,7 @@ fn a_region_leaving_its_parent_empties_the_kind_slot_that_held_it() {
|
||||
match &mut a.frame_mut(check).unwrap().kind_state {
|
||||
KindState::Button(s) => {
|
||||
s.checked_tex = Some(c);
|
||||
s.set_state_slot(ButtonVisualState::Normal, Some(n));
|
||||
let _ = s.set_state_slot(ButtonVisualState::Normal, Some(n));
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user