"A scroll container steals press-and-drag from every control inside it"
▸ SYMPTOM
A slider, scrub bar, knob, or drag handle sits inside a scrolling panel and stops responding to press-and-drag. The control still highlights on hover. Its click still fires. Only the drag is gone. No exception, no null, no log line.
The same markup works in one layout and fails in another. A card stack that reads fine across several columns loses every drag inside it once a docked or narrower panel reflows it to one column. Nothing in the control's code changed, so the report reads as "a working control broke." Verified on engine 26.08.05.
▸ CAUSE
A Panel with overflow: scroll and real overflow content answers Panel.WantsDrag = true. When a press arrives, Panel.FindDragTarget walks UP from the pointer to the first ancestor that says yes. The scroll container answers first, so it wins the drag before the child control's own handler ever sees the press. The container's OnDragStart calls StopPropagation and pans itself. The child never gets the event and never errors.
WantsDrag is true only under two conditions at once: the panel's computed overflow is Scroll on an axis, AND its ScrollSize on that axis is non-zero. A container that fits its content has nothing to scroll, so it answers false and never bids for the drag. That is why the identical markup behaves differently across layouts. The trap has a clean signature: no code change, only a layout change flips the container from bystander to drag-thief, and the symptom reads as a broken control rather than a layout crossing a threshold.
Mechanism source: Sandbox.Engine.dll, Systems/UI/Panel/Panel.Drag.cs (WantsDrag, FindDragTarget).
▸ FIX
Set Panel.CanDragScroll = false on the scroll container. This gates WantsDragScrolling only. OnMouseWheel and the scrollbar are a separate code path, so they keep working. You give up press-and-drag panning of the container and nothing else.
Two mechanical traps on the fix itself:
- If the scroll panel comes from a razor
@ref, that ref is a fresh panel object on every rebuild. SetCanDragScroll = falseinOnTreeBuilt, every build, not once at construction. A one-time set silently reverts on the next rebuild. - Set the property on the scrolling ancestor, not on the child control. Setting it on the child opts out a panel that was never asked for its own drag, while
FindDragTargetkeeps walking up to the real scrolling ancestor regardless.
▸ WHY IT WORKS
FindDragTarget resolves the drag to the nearest ancestor that claims it, and a scrolling container claims it by default. CanDragScroll = false removes that claim without touching the container's scroll capability, so the walk passes through to the control that should own the press. Verified live with an engine-readback A/B, not source-reading alone: a WantsDrag state probe read off the built panel tree reported 1 with the flag left on and 0 with it off, same panel, same frame, minutes apart, while the scrolls-flag stayed unchanged in both readings. That proves the scroll capability itself was untouched and only the drag claim moved.
- Published.