diff --git a/crates/benilla-ui/src/script/button.rs b/crates/benilla-ui/src/script/button.rs index ddf7db6c..8d7479bf 100644 --- a/crates/benilla-ui/src/script/button.rs +++ b/crates/benilla-ui/src/script/button.rs @@ -283,21 +283,7 @@ fn ensure_slot(lua: &Lua, this: &Table, slot: Slot) -> mlua::Result { .arena .create_region(h, kind, layer, 0) .ok_or_else(|| mlua::Error::runtime("stale frame handle"))?; - // A new highlight blends ADD, `SetHighlightTexture`'s default (`0x781d92`). The - // reference applies that mode, or a third argument's, on every call and to a handed-in - // Texture too (`0x7703f0` from `0x779050` and `0x779110`); here only a new region - // takes it. - model.region_data.insert( - rh, - RegionData { - blend: if slot == Slot::Highlight { - crate::script::BlendMode::Add - } else { - crate::script::BlendMode::default() - }, - ..Default::default() - }, - ); + model.region_data.insert(rh, RegionData::default()); model.touch_layout(); // a region entered the layout gate's read set if let Some(frame) = model.arena.frame_mut(h) { if let KindState::Button(bs) = &mut frame.kind_state { @@ -316,35 +302,40 @@ fn ensure_slot(lua: &Lua, this: &Table, slot: Slot) -> mlua::Result { Ok(model.region_id(rh)) } -/// Point `slot` at `rh` or at nothing, returning the different region it held for the caller to -/// free, as the slot store `0x778fd0` destroys the outgoing object. -fn swap_slot( - model: &mut Model, - h: FrameHandle, +/// Install `incoming` (a Texture object) into `slot`, or clear it, as the slot store `0x778fd0` +/// does: the same texture is a no-op (`0x778fe7`), the displaced one is destroyed (`0x77900a`), and +/// the incoming one moves into the button on the slot's own draw layer (`0x779017`, `0x779139` +/// for the highlight) before it is stored. +fn install_slot( + lua: &Lua, + this: &Table, slot: Slot, - rh: Option, -) -> mlua::Result> { - let frame = model - .arena - .frame_mut(h) - .ok_or_else(|| mlua::Error::runtime("stale frame handle"))?; - let KindState::Button(bs) = &mut frame.kind_state else { + incoming: Option, +) -> mlua::Result { + let h = frame_handle_of(lua, this)?; + let mut model = lua.app_data_mut::().expect("model app_data"); + if !matches!( + model.arena.frame(h).map(|f| &f.kind_state), + Some(KindState::Button(_)) + ) { return Err(mlua::Error::runtime("not a Button")); - }; - let outgoing = slot.get(bs).filter(|old| Some(*old) != rh); - slot.set(bs, rh); - model.touch_layout(); - Ok(outgoing) -} - -/// Free the region a slot gave up, never the one that moved in. -fn free_outgoing(lua: &Lua, outgoing: Option, incoming: RegionHandle) { - if let Some(old) = outgoing.filter(|old| *old != incoming) { - super::region::free_region( - &mut lua.app_data_mut::().expect("model app_data"), - old, - ); } + super::region::install_into_slot( + &mut model, + h, + incoming, + slot.shape().1, + false, + |ks| match ks { + KindState::Button(bs) => slot.get(bs), + _ => None, + }, + |ks, rh| { + if let KindState::Button(bs) = ks { + slot.set(bs, rh); + } + }, + ) } /// `SetTexture(texture | "path" | nil)`, forked on the argument's type (`0x781970`). The @@ -359,37 +350,40 @@ fn set_slot_texture( usage: &str, args: &MultiValue, ) -> mlua::Result<()> { + // The highlight's third argument names its blend, ADD without one or on a name that does not + // parse (`0x781d92`, `0x781db3` → `0x6f1920`); every other state has none. + let blend = (slot == Slot::Highlight).then(|| match args.get(1) { + Some(Value::String(n)) => n + .to_str() + .ok() + .and_then(|n| crate::script::BlendMode::parse(&n)) + .unwrap_or(crate::script::BlendMode::Add), + _ => crate::script::BlendMode::Add, + }); match args.front() { - // A Texture object is itself installed into the slot (`0x781b0b` → `0x778fd0`). + // 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 rh = super::region::region_handle_of(lua, t)?; - let h = frame_handle_of(lua, this)?; - let mut model = lua.app_data_mut::().expect("model app_data"); - // Any other widget raises (`0x781ab1`, message `0x87a1e0`). - if model.arena.region(rh).map(|r| r.kind) != Some(RegionKind::Texture) { - return Err(mlua::Error::runtime("Wrong object type, expected texture")); + let method = usage.split('(').next().unwrap_or(usage); + let rh = super::region::texture_object_arg(lua, this, t, method)?; + if install_slot(lua, this, slot, Some(rh))? { + if let Some(blend) = blend { + lua.app_data_mut::() + .expect("model app_data") + .region_data + .entry(rh) + .or_default() + .blend = blend; + } } - let outgoing = swap_slot(&mut model, h, slot, Some(rh))?; - drop(model); - free_outgoing(lua, outgoing, rh); 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) => { - let h = frame_handle_of(lua, this)?; - let mut model = lua.app_data_mut::().expect("model app_data"); - let outgoing = swap_slot(&mut model, h, slot, None)?; - drop(model); - if let Some(old) = outgoing { - super::region::free_region( - &mut lua.app_data_mut::().expect("model app_data"), - old, - ); - } - return Ok(()); - } + Some(Value::Nil) => return install_slot(lua, this, slot, None).map(|_| ()), _ => {} } @@ -419,6 +413,11 @@ fn set_slot_texture( // `""` clears the texture, which `QuestLogFrame.lua:165` relies on. data.texture = (!path.is_empty()).then_some(path); data.fill = None; + // The highlight applies its blend on every path call, to the old region as to a new one + // (`0x779082`, `0x7790e6`). + if let Some(blend) = blend { + data.blend = blend; + } Ok(()) } @@ -570,10 +569,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> { // Destroyed, not orphaned (`0x778d3c`). super::region::free_region(&mut model, dead); } - model.arena.set_region_owner(rh, Some(owner)); - if let Some(r) = model.arena.region_mut(rh) { - r.draw_layer = DrawLayer::Artwork; - } + super::region::link_region(&mut model, rh, owner, DrawLayer::Artwork); adopt_label(&mut model, owner, rh); // The layout's read set moved, and `GetTextWidth` reads the new label's extents. model.touch_layout(); diff --git a/crates/benilla-ui/src/script/colorselect.rs b/crates/benilla-ui/src/script/colorselect.rs index cabe4216..7d3ef164 100644 --- a/crates/benilla-ui/src/script/colorselect.rs +++ b/crates/benilla-ui/src/script/colorselect.rs @@ -10,7 +10,7 @@ use mlua::{Lua, Table, Value}; use super::object::{draw_layer_from_str, frame_handle_of}; use super::pointer::point_in_rect; -use super::region::region_wrapper; +use super::region::{install_into_slot, region_wrapper, texture_object_arg}; use super::{event, Model, RegionData}; use crate::layout::Rect; use crate::order::DrawLayer; @@ -68,6 +68,15 @@ impl Slot { *slot = Some(rh); } + fn set_opt(self, s: &mut ColorSelectState, rh: Option) { + match self { + Slot::Wheel => s.wheel = rh, + Slot::WheelThumb => s.wheel_thumb = rh, + Slot::ValueStrip => s.value_strip = rh, + Slot::ValueThumb => s.value_thumb = rh, + } + } + /// The layer a slot's region is born on: ARTWORK for the art (the wheel's layer 2, through /// `0x77fd10`), OVERLAY for the two markers above it. fn layer(self) -> DrawLayer { @@ -118,6 +127,48 @@ fn ensure_slot(lua: &Lua, this: &Table, slot: Slot, layer: Option) -> Ok(model.region_id(rh)) } +/// 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, + slot: Slot, + incoming: Option, +) -> mlua::Result<()> { + let h = frame_handle_of(lua, this)?; + let mut model = lua.app_data_mut::().expect("model app_data"); + if !matches!( + model.arena.frame(h).map(|f| &f.kind_state), + Some(KindState::ColorSelect(_)) + ) { + return Err(mlua::Error::runtime("not a ColorSelect")); + } + let thumb = matches!(slot, Slot::WheelThumb | Slot::ValueThumb); + let changed = install_into_slot( + &mut model, + h, + incoming, + slot.layer(), + thumb, + |ks| match ks { + KindState::ColorSelect(s) => slot.get(s), + _ => None, + }, + |ks, rh| { + if let KindState::ColorSelect(s) = ks { + slot.set_opt(s, rh); + } + }, + )?; + // 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); + } + Ok(()) +} + /// `SetTexture([path [, drawLayer]] | r, g, b [, a])`, plus an empty form that only creates /// the region: what `` means. A file-less slot is where the app renderer paints /// the disc and strip the reference computes (`0x78b580`, `0x78b8a0`). @@ -128,44 +179,54 @@ fn install_slot_texture( setter: &str, getter: &str, ) -> mlua::Result<()> { + let method = setter.to_string(); m.set( setter, - lua.create_function( - move |lua, (this, a1, a2, a3, a4): (Table, Value, Value, Value, Value)| { - let layer = match &a2 { - Value::String(s) => s.to_str().ok().and_then(|l| draw_layer_from_str(&l)), - _ => None, - }; - let id = ensure_slot(lua, &this, slot, layer)?; - let rh = { - let model = lua.app_data_ref::().expect("model app_data"); - *model.id_to_region.get(&id).expect("colorselect region id") - }; - let mut model = lua.app_data_mut::().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. - _ => {} + 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)); } - Ok(()) - }, - )?, + Some(Value::Nil) => return install_slot(lua, &this, slot, None), + _ => {} + } + let id = ensure_slot(lua, &this, slot, layer)?; + let rh = { + let model = lua.app_data_ref::().expect("model app_data"); + *model.id_to_region.get(&id).expect("colorselect region id") + }; + let mut model = lua.app_data_mut::().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. + _ => {} + } + Ok(()) + })?, )?; m.set( getter, diff --git a/crates/benilla-ui/src/script/region.rs b/crates/benilla-ui/src/script/region.rs index 7451f551..41ec4343 100644 --- a/crates/benilla-ui/src/script/region.rs +++ b/crates/benilla-ui/src/script/region.rs @@ -26,6 +26,124 @@ pub(super) fn region_handle_of(lua: &Lua, this: &Table) -> mlua::Result`. +pub(super) fn texture_object_arg( + lua: &Lua, + this: &Table, + arg: &Table, + method: &str, +) -> mlua::Result { + let who = { + let model = lua.app_data_ref::().expect("model app_data"); + let name = super::object::frame_handle_of(lua, this) + .ok() + .and_then(|h| model.arena.frame(h).and_then(|f| f.name.clone())); + name.unwrap_or_else(|| "".to_string()) + }; + if decode_id(arg).is_err() { + return Err(mlua::Error::runtime(format!( + "{who}:{method}(): Couldn't find 'this' in texture" + ))); + } + let model = lua.app_data_ref::().expect("model app_data"); + let rh = decode_id(arg) + .ok() + .and_then(|id| model.id_to_region.get(&id).copied()) + .filter(|&rh| model.arena.region(rh).map(|r| r.kind) == Some(RegionKind::Texture)); + rh.ok_or_else(|| { + mlua::Error::runtime(format!( + "{who}:{method}(): Wrong object type, expected texture" + )) + }) +} + +/// The client's re-link `0x77fd10(owner, layer, shown)` for a region a widget takes into a slot: +/// 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. +pub(super) fn link_region( + model: &mut Model, + rh: RegionHandle, + owner: crate::widget::FrameHandle, + layer: crate::order::DrawLayer, +) { + 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. + model.touch_layout(); + } + if let Some(r) = model.arena.region_mut(rh) { + r.draw_layer = layer; + } + model.region_data.entry(rh).or_default().hidden = false; +} + +/// 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. +pub(super) fn install_into_slot( + model: &mut Model, + owner: crate::widget::FrameHandle, + incoming: Option, + layer: crate::order::DrawLayer, + clear_points: bool, + get: impl Fn(&crate::widget::KindState) -> Option, + set: impl Fn(&mut crate::widget::KindState, Option), +) -> mlua::Result { + let current = model + .arena + .frame(owner) + .map(|f| get(&f.kind_state)) + .ok_or_else(|| mlua::Error::runtime("stale frame handle"))?; + if incoming == current { + return Ok(false); + } + if let Some(old) = current { + free_region(model, old); + } + if let Some(rh) = incoming { + link_region(model, rh, owner, layer); + if clear_points { + layout::clear_points(model, rh); + } + } + if let Some(f) = model.arena.frame_mut(owner) { + set(&mut f.kind_state, incoming); + } + model.touch_layout(); + 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. +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, + } +} + /// Argument 1 of a global that takes a Texture as its `this` (`SetLootPortrait` `0x4c2b40`, /// `SetInventoryPortaitTexture` `0x4c9150`), with the member prologue's three raises: a non-table /// (`0x847ef8`), a table with no object at `[0]` (`0x847ec0`), and any object but a Texture, the @@ -335,7 +453,7 @@ fn install_region_methods(lua: &Lua) -> mlua::Result<()> { lua.create_function(|lua, this: Table| { let rh = region_handle_of(lua, &this)?; let model = lua.app_data_ref::().expect("model"); - let shown = !model.region_data.get(&rh).is_some_and(|d| d.hidden); + let shown = region_shown(&model, rh); Ok(if shown { Value::Integer(1) } else { Value::Nil }) })?, )?; @@ -345,7 +463,7 @@ fn install_region_methods(lua: &Lua) -> mlua::Result<()> { lua.create_function(|lua, this: Table| { let rh = region_handle_of(lua, &this)?; let model = lua.app_data_ref::().expect("model"); - let shown = !model.region_data.get(&rh).is_some_and(|d| d.hidden); + let shown = region_shown(&model, rh); let owner_visible = model .arena .region(rh) diff --git a/crates/benilla-ui/src/script/region/layout.rs b/crates/benilla-ui/src/script/region/layout.rs index 9f27a217..c9e5292a 100644 --- a/crates/benilla-ui/src/script/region/layout.rs +++ b/crates/benilla-ui/src/script/region/layout.rs @@ -14,6 +14,18 @@ use super::{ }; /// Install the layout methods into `m`. +/// `ClearAllPoints` on a region (`0x767ed0`): a retarget onto no targets, so the cached graph +/// unlinks the edges with no re-derive. +pub(in crate::script) fn clear_points(model: &mut Model, rh: crate::widget::RegionHandle) { + let d = model.region_data.entry(rh).or_default(); + let old: Option> = + (!d.anchors.is_empty()).then(|| d.anchors.iter().map(|a| a.relative_to).collect()); + d.anchors.clear(); + if let Some(old) = old { + model.touch_layout_retarget_region(rh, &old, &[]); + } +} + pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> { // The explicit size fills the axes the anchors do not pin, so SetAllPoints's two corners // leave it unread. @@ -110,14 +122,7 @@ pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> { |lua, this: Table| { let rh = region_handle_of(lua, &this)?; let mut model = lua.app_data_mut::().expect("model"); - let d = model.region_data.entry(rh).or_default(); - // A retarget onto no targets: the cached graph unlinks the edges, no re-derive. - let old: Option> = - (!d.anchors.is_empty()).then(|| d.anchors.iter().map(|a| a.relative_to).collect()); - d.anchors.clear(); - if let Some(old) = old { - model.touch_layout_retarget_region(rh, &old, &[]); - } + clear_points(&mut model, rh); Ok(()) }, )?; diff --git a/crates/benilla-ui/src/script/slider.rs b/crates/benilla-ui/src/script/slider.rs index 59728383..d7365785 100644 --- a/crates/benilla-ui/src/script/slider.rs +++ b/crates/benilla-ui/src/script/slider.rs @@ -10,12 +10,12 @@ use mlua::{Lua, Table, Value}; use super::object::{draw_layer_from_str, frame_handle_of}; use super::pointer::point_in_rect; -use super::region::region_wrapper; +use super::region::{install_into_slot, region_wrapper, texture_object_arg}; use super::{event, Model, RegionData}; use crate::layout::Rect; use crate::order::DrawLayer; use crate::widget::{ - slider_fraction, slider_grab, FrameHandle, KindState, RegionKind, SliderState, + slider_fraction, slider_grab, FrameHandle, KindState, RegionHandle, RegionKind, SliderState, }; pub(super) const REG_SLIDER_METHODS: &str = "__benilla_slider_methods"; @@ -85,6 +85,35 @@ fn ensure_thumb(lua: &Lua, this: &Table, layer: Option) -> mlua::Resu Ok(model.region_id(rh)) } +/// Install `incoming` as the thumb, or clear it, through `0x789860`. +fn install_thumb(lua: &Lua, this: &Table, incoming: Option) -> mlua::Result<()> { + let h = frame_handle_of(lua, this)?; + let mut model = lua.app_data_mut::().expect("model app_data"); + if !matches!( + model.arena.frame(h).map(|f| &f.kind_state), + Some(KindState::Slider(_)) + ) { + return Err(mlua::Error::runtime("not a Slider")); + } + install_into_slot( + &mut model, + h, + incoming, + DrawLayer::Overlay, + true, + |ks| match ks { + KindState::Slider(sl) => sl.thumb, + _ => None, + }, + |ks, rh| { + if let KindState::Slider(sl) = ks { + sl.thumb = rh; + } + }, + ) + .map(|_| ()) +} + pub(super) fn install(lua: &Lua) -> mlua::Result<()> { let m = lua.create_table()?; @@ -151,46 +180,57 @@ 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(path [, layer])` or `(r, g, b [, a])`, a region's two `SetTexture` forms, on - // the thumb region, created on first use. + // `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. m.set( "SetThumbTexture", - lua.create_function( - |lua, (this, a1, a2, a3, a4): (Table, Value, Value, Value, Value)| { - let layer = match &a2 { - Value::String(s) => s.to_str().ok().and_then(|l| draw_layer_from_str(&l)), - _ => None, - }; - let id = ensure_thumb(lua, &this, layer)?; - let rh = { - let model = lua.app_data_ref::().expect("model app_data"); - *model.id_to_region.get(&id).expect("thumb region id") - }; - let mut model = lua.app_data_mut::().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), - }, - ]); - } - _ => {} + 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)); } - Ok(()) - }, - )?, + Some(Value::Nil) => return install_thumb(lua, &this, None), + _ => {} + } + let id = ensure_thumb(lua, &this, layer)?; + let rh = { + let model = lua.app_data_ref::().expect("model app_data"); + *model.id_to_region.get(&id).expect("thumb region id") + }; + let mut model = lua.app_data_mut::().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), + }, + ]); + } + _ => {} + } + Ok(()) + })?, )?; m.set( "GetThumbTexture", diff --git a/crates/benilla-ui/src/script/statusbar.rs b/crates/benilla-ui/src/script/statusbar.rs index c783cc47..c31a1659 100644 --- a/crates/benilla-ui/src/script/statusbar.rs +++ b/crates/benilla-ui/src/script/statusbar.rs @@ -7,10 +7,10 @@ use mlua::{Lua, Table, Value}; use super::object::{draw_layer_from_str, frame_handle_of}; -use super::region::region_wrapper; +use super::region::{install_into_slot, region_wrapper, texture_object_arg}; use super::{event, Model, RegionData}; use crate::order::DrawLayer; -use crate::widget::{KindState, RegionKind, StatusBarState}; +use crate::widget::{KindState, RegionHandle, RegionKind, StatusBarState}; /// Registry key of the StatusBar method table. pub(super) const REG_STATUSBAR_METHODS: &str = "__benilla_statusbar_methods"; @@ -90,6 +90,40 @@ fn ensure_bar(lua: &Lua, this: &Table, layer: Option) -> mlua::Result Ok(model.region_id(rh)) } +/// Install `incoming` as the bar texture, or clear it, through `0x7831f0`. +fn install_bar( + lua: &Lua, + this: &Table, + incoming: Option, + layer: DrawLayer, +) -> mlua::Result<()> { + let h = frame_handle_of(lua, this)?; + let mut model = lua.app_data_mut::().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, + h, + incoming, + layer, + false, + |ks| match ks { + KindState::StatusBar(sb) => sb.bar, + _ => None, + }, + |ks, rh| { + if let KindState::StatusBar(sb) = ks { + sb.bar = rh; + } + }, + ) + .map(|_| ()) +} + pub(super) fn install(lua: &Lua) -> mlua::Result<()> { let m = lua.create_table()?; @@ -143,46 +177,59 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> { })?, )?; - // A region's two `SetTexture` forms, `(path [, layer])` and `(r, g, b [, a])`, on the bar. + // `(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. m.set( "SetStatusBarTexture", - lua.create_function( - |lua, (this, a1, a2, a3, a4): (Table, Value, Value, Value, Value)| { - let layer = match &a2 { - Value::String(s) => s.to_str().ok().and_then(|l| draw_layer_from_str(&l)), - _ => None, - }; - let id = ensure_bar(lua, &this, layer)?; - let rh = { - let model = lua.app_data_ref::().expect("model app_data"); - *model.id_to_region.get(&id).expect("bar region id") - }; - let mut model = lua.app_data_mut::().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), - }, - ]); - } - _ => {} + 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, "SetCheckedTexture")?; + return install_bar(lua, &this, Some(rh), layer.unwrap_or(DrawLayer::Artwork)); } - Ok(()) - }, - )?, + 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::().expect("model app_data"); + *model.id_to_region.get(&id).expect("bar region id") + }; + let mut model = lua.app_data_mut::().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), + }, + ]); + } + _ => {} + } + Ok(()) + })?, )?; m.set( "GetStatusBarTexture", diff --git a/crates/benilla-ui/src/script/tests/adopt.rs b/crates/benilla-ui/src/script/tests/adopt.rs new file mode 100644 index 00000000..24851a37 --- /dev/null +++ b/crates/benilla-ui/src/script/tests/adopt.rs @@ -0,0 +1,547 @@ +//! A texture object handed to a widget's texture setter moves into that widget: unlinked from its +//! old parent (running the parent's region-removed hook), linked into the new one on the setter's +//! own draw layer (`0x77fd10`). + +use super::common::script; +use crate::script::*; + +fn drawn(s: &UiScript) -> Vec { + s.extract() + .iter() + .filter_map(|q| match &q.content { + QuadContent::Texture { path: Some(p), .. } => Some(p.clone()), + _ => None, + }) + .collect() +} + +/// `0x778fd0` → `0x77fd10(button, 2, 0)`: the adopted texture answers the button for its parent, +/// sits on ARTWORK whatever layer it was made on, and draws and hides with the button. +#[test] +fn a_state_slot_adopts_the_texture_into_the_button_on_artwork() { + let mut s = script(); + s.set_screen_size(800.0, 600.0); + s.run( + r#" + a = CreateFrame("Frame", "AdoptA") + a:SetPoint("CENTER", 0, 0) a:SetWidth(50) a:SetHeight(50) + a:SetFrameStrata("LOW") + t = a:CreateTexture(nil, "OVERLAY") + t:SetTexture("adopted") t:SetAllPoints(a) + b = CreateFrame("Button", "AdoptB") + b:SetPoint("CENTER", 0, 0) b:SetWidth(50) b:SetHeight(50) + b:SetFrameStrata("DIALOG") + c = CreateFrame("Frame", "AdoptC") + c:SetPoint("CENTER", 0, 0) c:SetWidth(50) c:SetHeight(50) + c:SetFrameStrata("HIGH") + ct = c:CreateTexture(nil, "BACKGROUND") + ct:SetTexture("mid") ct:SetAllPoints(c) + assert(t:GetParent() == a) + b:SetNormalTexture(t) + "#, + ) + .unwrap(); + s.run( + r#" + assert(t:GetParent() == b, "the button is the parent") + assert(t:GetDrawLayer() == "ARTWORK", "layer 2: " .. t:GetDrawLayer()) + assert(b:GetNormalTexture() == t) + "#, + ) + .unwrap(); + s.resolve(); + assert_eq!( + drawn(&s), + vec!["mid".to_string(), "adopted".to_string()], + "it draws at the button's strata, over the HIGH frame's texture, not at LOW" + ); + + s.run("b:Hide()").unwrap(); + s.resolve(); + assert_eq!( + drawn(&s), + vec!["mid".to_string()], + "it hides with the button, not with its old frame" + ); +} + +/// The pushed and disabled slots link the same way (`0x778fd0` is one function over the slot index). +#[test] +fn the_pushed_and_disabled_slots_adopt_on_artwork_too() { + let s = script(); + s.run( + r#" + a = CreateFrame("Frame") + b = CreateFrame("Button") + p = a:CreateTexture(nil, "BORDER") + d = a:CreateTexture(nil, "OVERLAY") + b:SetPushedTexture(p) b:SetDisabledTexture(d) + assert(p:GetParent() == b and d:GetParent() == b) + assert(p:GetDrawLayer() == "ARTWORK" and d:GetDrawLayer() == "ARTWORK") + "#, + ) + .unwrap(); +} + +/// `0x779110` → `0x77fd10(button, 4, 1)`: the highlight links on HIGHLIGHT. +#[test] +fn the_highlight_slot_adopts_on_highlight() { + let s = script(); + s.run( + r#" + a = CreateFrame("Frame") + b = CreateFrame("Button") + t = a:CreateTexture(nil, "ARTWORK") + b:SetHighlightTexture(t) + assert(t:GetParent() == b, "reparented") + assert(t:GetDrawLayer() == "HIGHLIGHT", t:GetDrawLayer()) + "#, + ) + .unwrap(); +} + +/// `0x785330` / `0x785440` → `0x77fd10(button, 3, 0)`: the checked pair links on OVERLAY. +#[test] +fn the_checked_pair_adopts_on_overlay() { + let s = script(); + s.run( + r#" + a = CreateFrame("Frame") + b = CreateFrame("CheckButton") + c = a:CreateTexture(nil, "BACKGROUND") + dc = a:CreateTexture(nil, "ARTWORK") + b:SetCheckedTexture(c) b:SetDisabledCheckedTexture(dc) + assert(c:GetParent() == b and dc:GetParent() == b) + assert(c:GetDrawLayer() == "OVERLAY" and dc:GetDrawLayer() == "OVERLAY") + "#, + ) + .unwrap(); +} + +/// Leaving a parent runs its region-removed hook (`0x77fd55`): the button that gave the texture up +/// no longer holds it. +#[test] +fn the_old_parent_lets_go_of_the_texture_it_gave_up() { + let s = script(); + s.run( + r#" + a = CreateFrame("Button", "GiverBtn") + a:SetNormalTexture("own") + t = a:GetNormalTexture() + b = CreateFrame("Button", "TakerBtn") + b:SetNormalTexture(t) + assert(a:GetNormalTexture() == nil, "the giver's slot let go") + assert(b:GetNormalTexture() == t and t:GetParent() == b) + a:SetNormalTexture("again") + assert(a:GetNormalTexture() ~= t and b:GetNormalTexture() == t, "and rebuilds its own") + "#, + ) + .unwrap(); +} + +/// A backdrop deletes its nine pieces by the pointers it keeps (`0x77f4d0`), wherever they have +/// moved to; the piece dies with its current parent's hook (`0x77fd10` links it there), which +/// empties the button's slot, so the slot never dangles and takes a fresh texture. +#[test] +fn an_old_frames_backdrop_teardown_leaves_the_new_parents_slot_empty_not_dangling() { + let s = script(); + s.run( + r#" + f = CreateFrame("Frame") + f:SetBackdrop({ bgFile = "bg", edgeFile = "edge" }) + piece = ({ f:GetRegions() })[1] + b = CreateFrame("Button") + b:SetPushedTexture(piece) + assert(piece:GetParent() == b and b:GetPushedTexture() == piece) + f:SetBackdrop(nil) + assert(b:GetPushedTexture() == nil, "the slot let go of the deleted piece") + assert(not pcall(piece.GetTexture, piece), "the piece is gone") + b:SetPushedTexture("fresh") + assert(b:GetPushedTexture():GetTexture() == "fresh") + "#, + ) + .unwrap(); +} + +/// The slot compare comes first (`0x778fe5`): the same texture again changes nothing, not its +/// layer and not its life. +#[test] +fn adopting_the_same_texture_twice_is_a_no_op() { + let s = script(); + s.run( + r#" + a = CreateFrame("Frame") + b = CreateFrame("Button") + t = a:CreateTexture(nil, "OVERLAY") + t:SetTexture("keep") + b:SetNormalTexture(t) + t:SetDrawLayer("BORDER") + b:SetNormalTexture(t) + assert(b:GetNormalTexture() == t and t:GetTexture() == "keep", "still held, still alive") + assert(t:GetDrawLayer() == "BORDER", "the second call did not re-layer it") + "#, + ) + .unwrap(); +} + +/// `0x7831f0`: the bar texture is the object, linked on the layer argument (ARTWORK without one), +/// and the old bar is deleted; nil clears the bar (`0x78fa6d`). +#[test] +fn a_status_bar_takes_a_texture_object_and_a_nil() { + let s = script(); + s.run( + r#" + a = CreateFrame("Frame") + sb = CreateFrame("StatusBar", "AdoptSB") + sb:SetStatusBarTexture("old") + old = sb:GetStatusBarTexture() + t = a:CreateTexture(nil, "OVERLAY") + sb:SetStatusBarTexture(t) + assert(sb:GetStatusBarTexture() == t, "installed") + assert(t:GetParent() == sb, "reparented") + assert(t:GetDrawLayer() == "ARTWORK", "default layer 2") + assert(not pcall(old.GetTexture, old), "the old bar texture was deleted") + u = a:CreateTexture(nil, "BACKGROUND") + sb:SetStatusBarTexture(u, "BORDER") + assert(u:GetParent() == sb and u:GetDrawLayer() == "BORDER", "the layer argument") + assert(not pcall(t.GetTexture, t), "and t, displaced, was deleted") + sb:SetStatusBarTexture(nil) + assert(sb:GetStatusBarTexture() == nil, "nil clears") + assert(not pcall(u.GetTexture, u)) + "#, + ) + .unwrap(); +} + +/// `0x789860`: the thumb links on OVERLAY (the layer argument is constant 3), its anchors are +/// cleared (`0x767ed0`) since the slider places it, and the old thumb is deleted. +#[test] +fn a_slider_thumb_object_links_on_overlay_with_its_anchors_cleared() { + let s = script(); + s.run( + r#" + a = CreateFrame("Frame") + sl = CreateFrame("Slider", "AdoptSl") + sl:SetThumbTexture("old") + old = sl:GetThumbTexture() + t = a:CreateTexture(nil, "BACKGROUND") + t:SetAllPoints(a) + assert(t:GetNumPoints() > 0) + sl:SetThumbTexture(t) + assert(sl:GetThumbTexture() == t and t:GetParent() == sl) + assert(t:GetDrawLayer() == "OVERLAY", t:GetDrawLayer()) + assert(t:GetNumPoints() == 0, "anchors cleared") + assert(not pcall(old.GetTexture, old), "old thumb deleted") + sl:SetThumbTexture(nil) + assert(sl:GetThumbTexture() == nil and not pcall(t.GetTexture, t)) + "#, + ) + .unwrap(); +} + +/// `0x78b580` and `0x78b8a0` link the wheel and the strip on ARTWORK (layer 2); `0x78b850` and +/// `0x78ba90` link the two markers on OVERLAY and clear their anchors. +#[test] +fn a_color_select_adopts_its_four_textures_on_the_reference_layers() { + let s = script(); + s.run( + r#" + a = CreateFrame("Frame") + cs = CreateFrame("ColorSelect", "AdoptCS") + local function mk(layer) + local t = a:CreateTexture(nil, layer) + t:SetAllPoints(a) + return t + end + w, wt, v, vt = mk("OVERLAY"), mk("BACKGROUND"), mk("OVERLAY"), mk("BACKGROUND") + cs:SetColorWheelTexture(w) + cs:SetColorWheelThumbTexture(wt) + cs:SetColorValueTexture(v) + cs:SetColorValueThumbTexture(vt) + for _, t in ipairs({ w, wt, v, vt }) do assert(t:GetParent() == cs) end + assert(cs:GetColorWheelTexture() == w and cs:GetColorValueTexture() == v) + assert(cs:GetColorWheelThumbTexture() == wt and cs:GetColorValueThumbTexture() == vt) + assert(w:GetDrawLayer() == "ARTWORK" and v:GetDrawLayer() == "ARTWORK") + assert(wt:GetDrawLayer() == "OVERLAY" and vt:GetDrawLayer() == "OVERLAY") + assert(w:GetNumPoints() > 0 and v:GetNumPoints() > 0, "art keeps its anchors") + assert(wt:GetNumPoints() == 0 and vt:GetNumPoints() == 0, "markers lose theirs") + cs:SetColorWheelThumbTexture(nil) + assert(cs:GetColorWheelThumbTexture() == nil and not pcall(wt.GetTexture, wt)) + "#, + ) + .unwrap(); +} + +/// A table that is no texture raises with the setter's own line (`0x781ab1`, `0x78f9d4`). +#[test] +fn a_non_texture_object_raises_in_the_widget_setters() { + let s = script(); + s.run( + r#" + f = CreateFrame("Frame") + sb = CreateFrame("StatusBar", "RaiseSB") + sl = CreateFrame("Slider", "RaiseSl") + cs = CreateFrame("ColorSelect", "RaiseCS") + "#, + ) + .unwrap(); + for (call, want) in [ + ( + "sb:SetStatusBarTexture(f)", + "RaiseSB:SetCheckedTexture(): Wrong object type, expected texture", + ), + ( + "sb:SetStatusBarTexture({})", + "RaiseSB:SetCheckedTexture(): Couldn't find 'this' in texture", + ), + ( + "sl:SetThumbTexture(f)", + "RaiseSl:SetThumbTexture(): Wrong object type, expected texture", + ), + ( + "cs:SetColorWheelTexture(f)", + "RaiseCS:SetColorWheelTexture(): Wrong object type, expected texture", + ), + ( + "cs:SetColorValueThumbTexture({})", + "RaiseCS:SetColorValueThumbTexture(): Couldn't find 'this' in texture", + ), + ] { + let err = s.run(call).unwrap_err().to_string(); + assert!(err.contains(want), "`{call}` -> {err}"); + } +} + +/// The third argument of `0x77fd10` is the region's own shown bit (`+0xc4`): a slot that takes a +/// hidden texture shows it (`0x77fd85`; the label `0x778d49`, the highlight `0x779139`, the bar +/// `0x78321b`, the thumb `0x789885`, the ColorSelect slots, a current state `0x779037`), and it +/// draws. +#[test] +fn a_slot_shows_the_hidden_texture_it_takes() { + let mut s = script(); + s.set_screen_size(800.0, 600.0); + s.run( + r#" + a = CreateFrame("Frame") + a:SetPoint("CENTER", 0, 0) a:SetWidth(50) a:SetHeight(50) + local function hidden(path) + local t = a:CreateTexture(nil, "ARTWORK") + t:SetTexture(path) t:SetAllPoints(a) t:Hide() + assert(not t:IsShown()) + return t + end + sb = CreateFrame("StatusBar", "ShowSB") + sb:SetPoint("CENTER", 0, 0) sb:SetWidth(50) sb:SetHeight(50) + bar = hidden("bar") sb:SetStatusBarTexture(bar) + assert(bar:IsShown(), "bar") + + b = CreateFrame("Button", "ShowBtn") + b:SetPoint("CENTER", 0, 0) b:SetWidth(50) b:SetHeight(50) + n = hidden("normal") b:SetNormalTexture(n) + assert(n:IsShown(), "the current state's texture") + h = hidden("hl") b:SetHighlightTexture(h) + assert(h:IsShown(), "highlight") + fs = a:CreateFontString(nil, "OVERLAY") fs:Hide() + b:SetFontString(fs) + assert(fs:IsShown(), "label") + + sl = CreateFrame("Slider", "ShowSl") + th = hidden("thumb") sl:SetThumbTexture(th) + assert(th:IsShown(), "thumb") + cs = CreateFrame("ColorSelect", "ShowCS") + w, wt = hidden("w"), hidden("wt") + cs:SetColorWheelTexture(w) cs:SetColorWheelThumbTexture(wt) + assert(w:IsShown() and wt:IsShown(), "ColorSelect slots") + "#, + ) + .unwrap(); + s.resolve(); + let d = drawn(&s); + for p in ["bar", "normal"] { + assert!(d.contains(&p.to_string()), "{p} draws: {d:?}"); + } +} + +/// With flag 0 (`0x77fdc8`..`0x77fddf`, and the state switch behind it) a Button's state texture +/// that is not the current one ends hidden, and shows when its state comes: a texture already on +/// the button at the same layer included, and a hidden one adopted into a later state too. The +/// checked pair follows `SetChecked` (`0x7854c0`). +#[test] +fn a_non_current_state_texture_is_hidden_until_its_state_comes() { + let s = script(); + s.run( + r#" + b = CreateFrame("CheckButton", "StateShownBtn") + b:SetNormalTexture("n") + own = b:CreateTexture(nil, "ARTWORK") + assert(own:IsShown(), "a fresh texture is shown") + b:SetPushedTexture(own) + assert(not own:IsShown(), "a same-parent same-layer re-slot with flag 0 hides it") + gone = b:CreateTexture(nil, "ARTWORK") gone:Hide() + b:SetDisabledTexture(gone) + assert(not gone:IsShown()) + b:SetButtonState("PUSHED") + assert(own:IsShown() and not b:GetNormalTexture():IsShown(), "the pushed state shows it") + b:SetButtonState("NORMAL") + assert(not own:IsShown() and b:GetNormalTexture():IsShown()) + b:Disable() + assert(gone:IsShown(), "a hidden texture adopted earlier shows in its state") + + c = b:CreateTexture(nil, "OVERLAY") + b:SetCheckedTexture(c) + assert(not c:IsShown(), "unchecked") + b:SetChecked(1) + assert(c:IsShown(), "checked") + "#, + ) + .unwrap(); +} + +/// `0x77fd9a`: a texture the button already owns still takes the slot's layer. +#[test] +fn a_texture_already_on_the_button_takes_the_slots_layer() { + let s = script(); + s.run( + r#" + b = CreateFrame("Button") + t = b:CreateTexture(nil, "BORDER") + b:SetNormalTexture(t) + assert(t:GetParent() == b and t:GetDrawLayer() == "ARTWORK", t:GetDrawLayer()) + h = b:CreateTexture(nil, "BACKGROUND") + b:SetHighlightTexture(h) + assert(h:GetDrawLayer() == "HIGHLIGHT") + "#, + ) + .unwrap(); +} + +/// `0x78b580` replaces the wheel's image with the generated one (`0x770300`) and `0x78b8a0` the +/// strip's, so the adopted art loses its file; the same texture again returns first +/// (`0x78b598`) and keeps what it was set to since. The markers keep their file. +#[test] +fn the_wheel_and_strip_lose_their_file_the_markers_keep_theirs() { + let s = script(); + s.run( + r#" + a = CreateFrame("Frame") + cs = CreateFrame("ColorSelect", "FileCS") + local function tex(path) local t = a:CreateTexture() t:SetTexture(path) return t end + w, v, wt = tex("wheel"), tex("strip"), tex("thumb") + cs:SetColorWheelTexture(w) cs:SetColorValueTexture(v) cs:SetColorWheelThumbTexture(wt) + assert(w:GetTexture() == nil and v:GetTexture() == nil, "art is generated") + assert(wt:GetTexture() == "thumb", "the marker keeps its file") + w:SetTexture("since") + cs:SetColorWheelTexture(w) + assert(w:GetTexture() == "since", "the same texture is a no-op") + "#, + ) + .unwrap(); +} + +/// `0x779110` and `0x779050` set the highlight's blend through `0x7703f0` on every call: ADD, or the +/// third argument's name (a name that does not parse leaves ADD, `0x6f1920`). The same texture +/// (`0x779122`) and nil (`0x779130`) skip it, and no other state has one. +#[test] +fn the_highlight_sets_its_blend_on_the_object_leg_and_the_path_leg() { + let s = script(); + s.run( + r#" + a = CreateFrame("Frame") + b = CreateFrame("Button") + t = a:CreateTexture() t:SetBlendMode("BLEND") + b:SetHighlightTexture(t) + assert(t:GetBlendMode() == "ADD", "object leg: " .. t:GetBlendMode()) + u = a:CreateTexture() + b:SetHighlightTexture(u, "MOD") + assert(u:GetBlendMode() == "MOD", "the third argument") + u:SetBlendMode("BLEND") + b:SetHighlightTexture(u, "MOD") + assert(u:GetBlendMode() == "BLEND", "the same texture skips it") + v = a:CreateTexture() v:SetBlendMode("BLEND") + b:SetHighlightTexture(v, "bogus") + assert(v:GetBlendMode() == "ADD", "an unparsed name leaves ADD") + b:SetHighlightTexture(nil) + assert(b:GetHighlightTexture() == nil) + + b:SetHighlightTexture("p") + hl = b:GetHighlightTexture() + assert(hl:GetBlendMode() == "ADD", "a new region") + hl:SetBlendMode("BLEND") + b:SetHighlightTexture("p2") + assert(hl:GetBlendMode() == "ADD", "path leg, every call") + b:SetHighlightTexture("p3", "MOD") + assert(hl:GetBlendMode() == "MOD" and hl:GetTexture() == "p3") + + n = a:CreateTexture() n:SetBlendMode("MOD") + b:SetNormalTexture(n) + assert(n:GetBlendMode() == "MOD", "a state texture's blend is its own") + "#, + ) + .unwrap(); +} + +/// The Button setters' raises (`0x87a210`/`0x87a1e0`, the highlight's `0x87a2cc`/`0x87a28c`, the +/// checked pair's `0x87b224`/`0x87b1e8` and `0x87c0e8`/`0x87c0a0`) name the receiver and the +/// method; a call with no argument raises Usage and clears nothing (`0x781b4f`). +#[test] +fn the_button_setters_raise_with_their_own_lines_and_no_argument_clears_nothing() { + let s = script(); + s.run( + r#" + f = CreateFrame("Frame") + b = CreateFrame("CheckButton", "RaiseBtn") + b:SetNormalTexture("n") b:SetHighlightTexture("h") b:SetCheckedTexture("c") + keep = { b:GetNormalTexture(), b:GetHighlightTexture(), b:GetCheckedTexture() } + "#, + ) + .unwrap(); + for (call, want) in [ + ( + "b:SetNormalTexture(f)", + "RaiseBtn:SetNormalTexture(): Wrong object type, expected texture", + ), + ( + "b:SetPushedTexture({})", + "RaiseBtn:SetPushedTexture(): Couldn't find 'this' in texture", + ), + ( + "b:SetHighlightTexture(f)", + "RaiseBtn:SetHighlightTexture(): Wrong object type, expected texture", + ), + ( + "b:SetHighlightTexture({})", + "RaiseBtn:SetHighlightTexture(): Couldn't find 'this' in texture", + ), + ( + "b:SetCheckedTexture(f)", + "RaiseBtn:SetCheckedTexture(): Wrong object type, expected texture", + ), + ( + "b:SetDisabledCheckedTexture({})", + "RaiseBtn:SetDisabledCheckedTexture(): Couldn't find 'this' in texture", + ), + ( + "b:SetNormalTexture()", + "Usage: RaiseBtn:SetNormalTexture(texture or \"texture\" or nil)", + ), + ( + "b:SetHighlightTexture()", + "Usage: RaiseBtn:SetHighlightTexture(texture or \"texture\" or nil [, \"blendmode\")", + ), + ( + "b:SetCheckedTexture()", + "Usage: RaiseBtn:SetCheckedTexture(texture or \"texture\" or nil)", + ), + ] { + let err = s.run(call).unwrap_err().to_string(); + assert!(err.contains(want), "`{call}` -> {err}"); + } + s.run( + r#" + assert(b:GetNormalTexture() == keep[1] and keep[1]:GetTexture() == "n") + assert(b:GetHighlightTexture() == keep[2] and keep[2]:GetTexture() == "h") + assert(b:GetCheckedTexture() == keep[3] and keep[3]:GetTexture() == "c") + "#, + ) + .unwrap(); +} diff --git a/crates/benilla-ui/src/script/tests/mod.rs b/crates/benilla-ui/src/script/tests/mod.rs index ebf0190b..b5d0915b 100644 --- a/crates/benilla-ui/src/script/tests/mod.rs +++ b/crates/benilla-ui/src/script/tests/mod.rs @@ -3,6 +3,7 @@ mod addon_argument_abi; mod addon_index_space; +mod adopt; mod anchors; mod backdrop; mod button; diff --git a/crates/benilla-ui/src/widget/kinds/mod.rs b/crates/benilla-ui/src/widget/kinds/mod.rs index 5cad5b73..67cb406e 100644 --- a/crates/benilla-ui/src/widget/kinds/mod.rs +++ b/crates/benilla-ui/src/widget/kinds/mod.rs @@ -943,6 +943,20 @@ 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 { + 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 {