Touch panel and gesture semantics
Evidence basis: source-verified at the pinned commit; tests exist (not executed for this page). Claims on this page were checked by reading the CNA source at commit 009d40f5; unless a sentence says otherwise, nothing here was built or executed. Checked by reading TouchPanel, TouchCollection, the gesture detector, the bridge and the touch suites at 009d40f5; the two fragments were syntax-checked with g++ -fsyntax-only against TARGET headers. Nothing was executed; touch hardware and Android devices remain unverified.
This page states what TouchPanel, TouchCollection, TouchLocation and GestureSample guarantee in CNA: when touch state advances, which of the two "connected" answers to trust, how gestures are filtered and time-stamped, and where CNA deliberately differs from FNA. It is for anyone writing touch or gesture controls, or testing them on a desktop. The step-by-step task is Tutorial 49: touch input and gestures; the event bridge, finger-id mapping and the gesture detector's thresholds are traced on Input internals: touch and gestures.
TouchPanel: event-driven state, advanced once per update
TouchPanel is static-only and offers GetCapabilities(), GetState() and ReadGesture() (TouchPanel.cpp). FNA fills its touch slots from a per-frame platform poll; CNA's bridge is event-driven, so the real input path records each finger in a panel-owned map keyed by touch id, and GetState() reports that map in id order, truncated to MAX_TOUCHES = 8. (The older slot arrays that FNA's polling fills still exist for SetFinger and take precedence when used, but the production bridge never feeds them.)
Advancing a frame is not GetState()'s job. TouchPanel::Update, reached from FrameworkDispatcher::Update once per Game::Update after a touch device is known to exist, copies current values to previous, retires released touches, promotes Pressed to Moved and runs the gesture detector's timing pass. Reads are therefore pure: GetStateIsPureAndRepeatedReadsWithinAFrameAreIdentical and ReleasedTouchIsVisibleForExactlyOnePostAdvanceReadRegardlessOfPriorReads in TouchEdgeCaseTests.cpp pin that a released touch is visible for exactly one frame however often the game reads. A consequence worth knowing: a game whose Update override does not call the base Game::Update, or a tool that never calls FrameworkDispatcher::Update, never advances touch state or gesture timing.
Coordinates arrive normalized. For GetState() the bridge multiplies them by the event's client size and passes them through the renderer's logical transform (the same one the mouse uses, see logical coordinates); for gestures they are multiplied by TouchPanel's display size and rounded to whole pixels. GraphicsDevice publishes that display size at construction and on every reset, and a touch event that arrives while it is still zero is dropped rather than collapsing every contact to the corner (ScalingProducesNoGestureWhenDisplaySizeIsZero).
Defects fixed before this snapshot
Three touch defects recorded against earlier code are gone. A cancelled finger was once ignored and stayed active; the bridge now treats TouchEventKind::Cancelled exactly like lifting the finger (FingerCanceledReleasesTouchLikeFingerUp). GetCapabilities() once relied only on an "a touch has been seen" flag, so an enumerated but untouched device looked disconnected; it now asks the platform's device enumeration first. And touch state used to advance inside GetState(), so a second read in the same frame mutated it; advancement now happens only in TouchPanel::Update.
Two different "connected" answers
The two connected flags do not use the same evidence, and mixing them up is a common porting bug.
| Query | Evidence | Use it for |
|---|---|---|
TouchPanel::GetCapabilities().getIsConnectedProperty() | On every call: the platform's live enumeration of touch devices, then the sticky observed-touch flag, then any live touch. It never changes touch state. | Device discovery: it can say "connected" before the first interaction. |
TouchCollection::getIsConnectedProperty() | Only the sticky flag, set by the first finger-down event. | Nothing more than "a touch has been seen"; it stays false for an enumerated but untouched device. |
Whether a finger is down now is the collection's Count. The fallbacks exist because some platforms enumerate a touch device only after it has been touched (FNA's notes name Windows), which FallsBackToStickyFlagWhenEnumerationLagsInteractionWindowsStyle reproduces. MaximumTouchCount reports 4 for any touch device and 0 otherwise: that is XNA's and FNA's fixed compatibility value, not the tracking cap, which is 8.
TouchCollection and TouchLocation
TouchCollection is a per-frame snapshot with Count, IsConnected, a by-index operator[] (which throws std::out_of_range past the end), Contains, IndexOf, and the CNAEXT helpers empty() and begin()/end() that allow a range-based for. It is immutable by contract only: getIsReadOnlyProperty() is hard-coded to true while Add, Clear and the non-const indexer still mutate the collection. That inconsistency is FNA's own, reproduced on purpose and pinned by IsReadOnlyIsAdvisoryAndMutationStillSucceedsLikeFna; code must not treat the flag as protection for a collection it hands out.
Each TouchLocation carries an Id that stays stable for one finger from press to release (the bridge maps each native 64-bit finger id to a small panel id allocated upward from 1 and frees it on release or cancel), a State (Pressed, Moved, Released, or Invalid for an empty or expired entry), a Position, the CNAEXT getPressureEXT() where the hardware reports pressure (1.0 for an emulated mouse touch), and TryGetPreviousLocation, which fails when the previous state is Invalid.
Worked example: per-finger drag tracking
Track fingers by Id, never by their position in the collection, which changes as other fingers come and go:
const TouchCollection touches = TouchPanel::GetState();
for (const TouchLocation& touch : touches)
{
switch (touch.getStateProperty())
{
case TouchLocationState::Pressed:
activeDrags[touch.getIdProperty()] = touch.getPositionProperty();
break;
case TouchLocationState::Moved:
if (auto it = activeDrags.find(touch.getIdProperty()); it != activeDrags.end())
{
PanCamera(touch.getPositionProperty() - it->second);
it->second = touch.getPositionProperty();
}
break;
case TouchLocationState::Released:
case TouchLocationState::Invalid:
activeDrags.erase(touch.getIdProperty());
break;
}
}
Syntax-checked against the TARGET headers with activeDrags as a std::map<int, Vector2> and PanCamera as the game's own function (not executed).
Gestures
GestureType is a bit-flag enumeration with eleven values: None = 0 and the ten gestures as powers of two from Tap = 1 through PinchComplete = 512 (DoubleTap, Hold, HorizontalDrag, VerticalDrag, FreeDrag, Pinch, Flick, DragComplete in between), with | and & operators so selections combine. Every one of the ten is recognized by the detector in GestureDetector.cpp, including both completion events; there is no declared gesture that silently lacks recognition.
EnabledGesturesis an active filter. The detector checks it before producing each sample, so disabled gestures are never queued; with the defaultNonenothing is recognized at all.ReadGesture()throwsSystem::InvalidOperationExceptionwhen the queue is empty, as in XNA; loop ongetIsGestureAvailableProperty().GestureSamplecarriesGestureType,Timestamp,Position/Deltaand, for two-finger gestures such asPinch,Position2/Delta2(left at their defaults otherwise), plus the CNAEXTgetFingerIdEXTProperty()andgetFingerId2EXTProperty()naming the fingers involved.- Suppression. While the CNAEXT
InputSuppressedEXTis set, which the gamer-services guide does around its overlays,GetState()is empty and the gesture queue is cleared and refuses new samples, so a tap on an overlay never reaches the game a frame later. - Desktop testing. The CNAEXT
setMouseTouchEmulationEnabledEXT(true)makes the left mouse button produce a synthetic finger through the same entry points a real one uses; it is off by default, matching XNA and FNA.
GestureSample::Timestamp deliberately differs from FNA
FNA computes the timestamp as TimeSpan.FromTicks(Environment.TickCount), but TickCount is in milliseconds while FromTicks expects 100-nanosecond ticks, so FNA's values are ten thousand times too small. CNA converts milliseconds of the monotonic clock to ticks (GetGestureTimestamp). Neither engine defines an absolute epoch, so compare timestamps only with each other; GestureTimestampIsNonNegativeAndAdvancesWithTheClock pins that two gestures under an advanced test clock have non-negative, strictly increasing stamps.
Worked example: filtered gestures
// Once: only these gestures are recognized and queued.
TouchPanel::setEnabledGesturesProperty(GestureType::Pinch | GestureType::Flick);
// Each Update:
while (TouchPanel::getIsGestureAvailableProperty())
{
const GestureSample gesture = TouchPanel::ReadGesture();
if (gesture.getGestureTypeProperty() == GestureType::Pinch)
{
// Position/Position2 are the two fingers; Delta/Delta2 their motion this frame.
ZoomCamera(gesture.getPositionProperty(), gesture.getPosition2Property());
}
}
Syntax-checked against the TARGET headers (not executed); ZoomCamera is the game's own function.
Where touch events come from
Touch is a platform question: the SDL3 platform maps finger events, the native X11 (XInput 2.2) and Wayland (wl_touch) platforms have touch services, and the SDL2 and Win32 mappers and the Terminal and Headless platforms deliver none. Android touch and real touchscreens were not exercised for this page; CNA's own manual verification log records touch hardware as not verified. The automated evidence is the touch, gesture and edge-case suites named above, run against injected events and a manual test clock.
Related pages
The same subject is explained at several altitudes. These are the neighbouring pages at each one.
- Internals
- Input internals: touch and gestures
- Tests and validation
- Test architecture and change recipes
- Deep dives
- The input model · Gamepads and haptics