//! The `Slider` method surface — the `CSimpleSlider` widget behavior over the kind tag (factory //! `0x6eee40`). A value in `[min, max]` with a step and orientation, driving a thumb texture along //! the track. //! //! Grounded in wow-re's byte-verified LoadXML table (RF-28, `rf28-typed-widget-loadxml.md`): the //! `` sub-element (default layer OVERLAY=3), `minValue`/`maxValue`/`valueStep`/ //! `defaultValue`, the `orientation` shared enum (HORIZONTAL=0/VERTICAL=1, `0x811b00`), and the //! widget's own `OnValueChanged` script slot (`+0x330`). The **thumb-position** mechanism — the //! thumb's rect placed at the value fraction along the orientation axis, applied at extract — is //! faithful to the *documented* widget model, not byte-pinned (same posture as StatusBar's fill and //! ScrollFrame's scroll; decisions 0112/0250). //! //! `SetValue` fires `OnValueChanged` **only on an actual change** ([`SliderState::store_value`]). //! That is load-bearing, not an optimization: the real scrollbar template wires //! `OnValueChanged → this:GetParent():SetVerticalScroll(arg1)` and the ScrollFrame's //! `OnVerticalScroll → scrollbar:SetValue(arg1)` back the other way, so a fire-always `SetValue` //! would recurse forever (`UIPanelTemplates.xml`). The change-gate breaks the loop after one hop. //! //! The methods live in their own registry table, consulted by the frame `__index` dispatcher only //! for Slider frames — so duck-typing addons (`if frame:GetThumbTexture() then …`) see `nil` on //! every other kind, exactly as against the client's per-class method sets. 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::{event, Model, RegionData}; use crate::layout::Rect; use crate::order::DrawLayer; use crate::widget::{FrameHandle, KindState, RegionKind, SliderState}; /// Registry key of the Slider method table (the MAXCSTACK discipline: Lua-side root, named key). pub(super) const REG_SLIDER_METHODS: &str = "__benilla_slider_methods"; /// Run `f` over a frame's Slider state under one short write borrow. Errors if `this` is not a live /// Slider (unreachable through the kind dispatcher, but the method table is a plain Lua value — a /// caller could fish it out and misapply it). fn with_slider( lua: &Lua, this: &Table, f: impl FnOnce(&mut SliderState) -> T, ) -> mlua::Result { let h = frame_handle_of(lua, this)?; let mut model = lua.app_data_mut::().expect("model app_data"); let frame = model .arena .frame_mut(h) .ok_or_else(|| mlua::Error::runtime("stale frame handle"))?; match &mut frame.kind_state { KindState::Slider(s) => Ok(f(s)), _ => Err(mlua::Error::runtime("not a Slider")), } } /// Get-or-create the thumb texture region (`SetThumbTexture`/``); `layer` re-layers an /// existing thumb. Returns the region's id (for wrapper lookup). The widget default layer is OVERLAY /// (RF-28 — the Slider's `drawLayer` attr defaults OVERLAY=3, unlike StatusBar's ARTWORK bar). fn ensure_thumb(lua: &Lua, this: &Table, layer: Option) -> mlua::Result { let h = frame_handle_of(lua, this)?; let mut model = lua.app_data_mut::().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(); // a region entered the layout gate's read set (decision 0740) 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)) } pub(super) fn install(lua: &Lua) -> mlua::Result<()> { let m = lua.create_table()?; m.set( "SetMinMaxValues", lua.create_function(|lua, (this, min, max): (Table, f32, f32)| { // Unlike StatusBar, a reversed pair is NOT swapped (the Slider LoadXML stores min + // (max−min) and does no swap, RF-28). The held value re-clamps into the new range; a // move fires OnValueChanged — a value change the caller didn't set explicitly is still a // change (the store_value clamp guards a degenerate max false, "VERTICAL" => true, _ => { return Err(mlua::Error::runtime(format!( "SetOrientation: unknown orientation '{o}'" ))) } }; with_slider(lua, &this, |s| s.vertical = vertical) })?, )?; m.set( "GetOrientation", lua.create_function(|lua, this: Table| { let v = with_slider(lua, &this, |s| s.vertical)?; Ok(if v { "VERTICAL" } else { "HORIZONTAL" }.to_string()) })?, )?; // Enable/Disable/IsEnabled — a disabled slider ignores thumb drag (gated in the pointer path). m.set( "Enable", lua.create_function(|lua, this: Table| with_slider(lua, &this, |s| s.enabled = true))?, )?; m.set( "Disable", lua.create_function(|lua, this: Table| with_slider(lua, &this, |s| s.enabled = false))?, )?; m.set( "IsEnabled", // 1.12 returns 1/nil; the house API target is the Era boolean (decision 0068) — every // transcribed `if slider:IsEnabled()` reads both identically. lua.create_function(|lua, this: Table| with_slider(lua, &this, |s| s.enabled))?, )?; // SetThumbTexture(path [, drawLayer]) | SetThumbTexture(r, g, b [, a]) — the same two forms as a // region's SetTexture, targeting the thumb region (created on first use). Mirrors // SetStatusBarTexture; the thumb defaults to the OVERLAY layer. 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()), Value::Number(_) | Value::Integer(_) => { data.color = Some([ num_f32(&a1), num_f32(&a2), num_f32(&a3), match &a4 { Value::Nil => 1.0, v => num_f32(v), }, ]); } _ => {} } Ok(()) }, )?, )?; m.set( "GetThumbTexture", lua.create_function(|lua, this: Table| { let thumb = with_slider(lua, &this, |s| s.thumb)?; let id = { let mut model = lua.app_data_mut::().expect("model app_data"); thumb.map(|rh| model.region_id(rh)) }; match id { Some(id) => Ok(Value::Table(region_wrapper(lua, id)?)), None => Ok(Value::Nil), } })?, )?; lua.set_named_registry_value(REG_SLIDER_METHODS, m)?; Ok(()) } /// Fire `OnValueChanged(self, value)` if `changed` carries the new value (RF-28: the Slider's own /// script slot `+0x330`). Fired outside any model borrow; errors go to [`Model::errors`]. fn fire_value_changed(lua: &Lua, this: &Table, changed: Option) -> mlua::Result<()> { let Some(value) = changed else { return Ok(()) }; let id = { let h = frame_handle_of(lua, this)?; let mut model = lua.app_data_mut::().expect("model app_data"); model.frame_id(h) }; if let Err(e) = event::fire_widget_handler( lua, id, "OnValueChanged", vec![Value::Number(f64::from(value))], ) { lua.app_data_mut::() .expect("model app_data") .errors .push(e.to_string()); } Ok(()) } /// A Lua number-ish → f32 (nil/other → 0.0), for the color-form arguments. fn num_f32(v: &Value) -> f32 { match v { Value::Number(n) => *n as f32, Value::Integer(i) => *i as f32, _ => 0.0, } } // ── Thumb geometry + drag (decision 0250 §4/§5) ────────────────────────────────────────────── /// A Slider's thumb rect: the thumb of size `thumb_size` placed at `fraction` along the slider `r`'s /// track and centered on the cross-axis. The thumb stays **inset** within the track (travel = /// trackLen − thumbLen), the way WoW scrollbar knobs visibly do. VERTICAL runs y-up-inverted — /// `fraction` 0 (value=min) sits the thumb flush at the **top** (a scrollbar at 0 is scrolled to the /// top), `fraction` 1 flush at the bottom. A thumb with no explicit size fills the slider (zero /// travel). This is the single geometry source shared by `extract` (render) and [`begin_drag`] /// (hit-test), so render and input never disagree. Returns a [`Rect`] as `(bottom, left, top, right)`. pub(super) fn thumb_rect( r: Rect, thumb_size: Option<(f32, f32)>, vertical: bool, fraction: f32, ) -> Rect { let (tw, th) = thumb_size.unwrap_or((r.width(), r.height())); if vertical { let travel = (r.height() - th).max(0.0); let top = r.top - fraction * travel; // f=0 → track top, f=1 → track bottom let cx = (r.left + r.right) * 0.5; Rect::new(top - th, cx - tw * 0.5, top, cx + tw * 0.5) } else { let travel = (r.width() - tw).max(0.0); let left = r.left + fraction * travel; let cy = (r.bottom + r.top) * 0.5; Rect::new(cy - th * 0.5, left, cy + th * 0.5, left + tw) } } /// The in-flight thumb drag (decision 0250 §5): the slider being dragged + the grab offset (the /// cursor's distance from the thumb's leading edge along the axis at grab time), so the thumb tracks /// the cursor without jumping. Engine C++-equivalent — no Lua, like the real client's scrollbar. #[derive(Clone, Copy)] pub(crate) struct SliderDrag { pub(crate) slider: FrameHandle, /// cursor-along-axis − thumb-leading-edge at grab (VERTICAL: `y − thumb.top`; HORIZONTAL: /// `x − thumb.left`). pub(crate) grab_offset: f32, } /// On a LeftButton press at `(x, y)` whose hit frame is `hit`: if that frame is an **enabled** /// Slider with a resolved rect and the cursor lies on its thumb, begin a drag capture (records /// [`Model::slider_drag`]). Returns whether a capture began — the caller needs nothing from it (the /// press still fires OnMouseDown normally). A press off the thumb (on the track) captures nothing: /// only the thumb drags, matching the vanilla scrollbar where the arrows do the stepping. pub(super) fn begin_drag(model: &mut Model, hit: Option, x: f32, y: f32) -> bool { let Some(h) = hit else { return false }; let Some(r) = model.resolved.get(&h).copied() else { return false; }; let (enabled, vertical, fraction, thumb) = match model.arena.frame(h).map(|f| &f.kind_state) { Some(KindState::Slider(s)) => (s.enabled, s.vertical, s.fraction(), s.thumb), _ => return false, }; if !enabled { return false; } let size = thumb .and_then(|rh| model.region_data.get(&rh)) .and_then(|d| d.size); let trect = thumb_rect(r, size, vertical, fraction); if !point_in_rect(trect, x, y) { return false; } let grab_offset = if vertical { y - trect.top } else { x - trect.left }; model.slider_drag = Some(SliderDrag { slider: h, grab_offset, }); true } /// On a pointer move at `(x, y)` while a thumb is captured: recompute the value from the cursor /// position (absolute, drift-free — the grab offset keeps the same thumb point under the cursor) and /// store it. Returns `Some((frame id, new value))` if the value actually changed — the caller fires /// `OnValueChanged` outside the model borrow — else `None` (no capture, no travel, or no change). pub(super) fn drag_move(model: &mut Model, x: f32, y: f32) -> Option<(u32, f32)> { let SliderDrag { slider, grab_offset, } = *model.slider_drag.as_ref()?; let r = model.resolved.get(&slider).copied()?; let (min, max, vertical, thumb) = match model.arena.frame(slider).map(|f| &f.kind_state) { Some(KindState::Slider(s)) => (s.min, s.max, s.vertical, s.thumb), _ => return None, // slider destroyed mid-drag }; let (tw, th) = thumb .and_then(|rh| model.region_data.get(&rh)) .and_then(|d| d.size) .unwrap_or((r.width(), r.height())); let fraction = if vertical { let travel = r.height() - th; if travel <= 0.0 { return None; // nothing to scroll } ((r.top - (y - grab_offset)) / travel).clamp(0.0, 1.0) } else { let travel = r.width() - tw; if travel <= 0.0 { return None; } (((x - grab_offset) - r.left) / travel).clamp(0.0, 1.0) }; let value = min + fraction * (max - min); let changed = match model.arena.frame_mut(slider).map(|f| &mut f.kind_state) { Some(KindState::Slider(s)) => s.store_value(value), _ => None, }; changed.map(|v| (model.frame_id(slider), v)) } /// Release any in-flight thumb drag (LeftButton up, or the pointer leaving the window). pub(super) fn end_drag(model: &mut Model) { model.slider_drag = None; }