Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "patch",
"comment": "Fixed: Touch screen events not properly captured for pressable and text inputs",
"packageName": "react-native-windows",
"email": "gordomacmaster@gmail.com",
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,7 @@ CompositionEventHandler::~CompositionEventHandler() {
pointerSource.PointerMoved(m_pointerMovedToken);
pointerSource.PointerCaptureLost(m_pointerCaptureLostToken);
pointerSource.PointerWheelChanged(m_pointerWheelChangedToken);
pointerSource.PointerExited(m_pointerExitedToken);
auto keyboardSource = winrt::Microsoft::UI::Input::InputKeyboardSource::GetForIsland(island);
keyboardSource.KeyDown(m_keyDownToken);
keyboardSource.KeyUp(m_keyUpToken);
Expand Down Expand Up @@ -1180,14 +1181,15 @@ void CompositionEventHandler::onPointerPressed(

PointerId pointerId = pointerPoint.PointerId();

auto staleTouch = std::find_if(m_activeTouches.begin(), m_activeTouches.end(), [pointerId](const auto &pair) {
return pair.second.touch.identifier == pointerId;
});
auto staleTouch = m_activeTouches.find(pointerId);

if (staleTouch != m_activeTouches.end()) {
// A pointer with this ID already exists - Should we fire a button cancel or something?
// assert(false);
return;
// A previous pointer with this ID was never properly released (e.g., app lost focus,
// pointer left window). Cancel the stale touch and clean it up so the new press can proceed.
if (staleTouch->second.eventEmitter) {
DispatchSynthesizedTouchCancelForActiveTouch(staleTouch->second, pointerPoint, keyModifiers);
}
m_activeTouches.erase(staleTouch);
}

const auto eventType = TouchEventType::Start;
Expand Down Expand Up @@ -1246,6 +1248,12 @@ void CompositionEventHandler::onPointerPressed(
targetComponentView = targetComponentView.Parent();
}

// Don't register the touch if no eventEmitter was found — inserting a null-emitter entry
// into m_activeTouches would block future presses with the same pointer ID.
if (!activeTouch.eventEmitter) {
return;
}

UpdateActiveTouch(activeTouch, ptScaled, ptLocal);

// activeTouch.touch.isPrimary = true;
Expand All @@ -1269,9 +1277,7 @@ void CompositionEventHandler::onPointerReleased(
winrt::Windows::System::VirtualKeyModifiers keyModifiers) noexcept {
int pointerId = pointerPoint.PointerId();

auto activeTouch = std::find_if(m_activeTouches.begin(), m_activeTouches.end(), [pointerId](const auto &pair) {
return pair.second.touch.identifier == pointerId;
});
auto activeTouch = m_activeTouches.find(pointerId);

if (activeTouch == m_activeTouches.end()) {
return;
Expand All @@ -1283,8 +1289,13 @@ void CompositionEventHandler::onPointerReleased(
facebook::react::Point ptLocal, ptScaled;
getTargetPointerArgs(fabricuiManager, pointerPoint, tag, ptScaled, ptLocal);

if (tag == -1)
if (tag == -1) {
if (activeTouch->second.eventEmitter) {
DispatchSynthesizedTouchCancelForActiveTouch(activeTouch->second, pointerPoint, keyModifiers);
}
m_activeTouches.erase(pointerId);
return;
}

auto targetComponentView = fabricuiManager->GetViewRegistry().componentViewDescriptorWithTag(tag).view;
auto args = winrt::make<winrt::Microsoft::ReactNative::Composition::Input::implementation::PointerRoutedEventArgs>(
Expand Down Expand Up @@ -1456,6 +1467,47 @@ facebook::react::PointerEvent CompositionEventHandler::CreatePointerEventFromAct
return event;
}

void CompositionEventHandler::DispatchSynthesizedTouchCancelForActiveTouch(
const ActiveTouch &cancelledTouch,
const winrt::Microsoft::ReactNative::Composition::Input::PointerPoint &pointerPoint,
winrt::Windows::System::VirtualKeyModifiers keyModifiers) {
if (!cancelledTouch.eventEmitter) {
return;
}

facebook::react::PointerEvent pointerEvent =
CreatePointerEventFromActiveTouch(cancelledTouch, TouchEventType::Cancel);
winrt::Microsoft::ReactNative::ComponentView targetView{nullptr};
facebook::react::SharedTouchEventEmitter emitter = cancelledTouch.eventEmitter;
auto pointerHandler = [emitter, pointerEvent](std::vector<winrt::Microsoft::ReactNative::ComponentView> &) {
emitter->onPointerCancel(pointerEvent);
};
HandleIncomingPointerEvent(pointerEvent, targetView, pointerPoint, keyModifiers, pointerHandler);

facebook::react::TouchEvent touchEvent;
touchEvent.changedTouches.insert(cancelledTouch.touch);

for (const auto &pair : m_activeTouches) {
if (!pair.second.eventEmitter || pair.second.eventEmitter != cancelledTouch.eventEmitter) {
continue;
}

if (touchEvent.changedTouches.find(pair.second.touch) != touchEvent.changedTouches.end()) {
continue;
}

touchEvent.touches.insert(pair.second.touch);
}

for (const auto &pair : m_activeTouches) {
if (pair.second.eventEmitter == cancelledTouch.eventEmitter) {
touchEvent.targetTouches.insert(pair.second.touch);
}
}
Comment on lines +1487 to +1506
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DispatchSynthesizedTouchCancelForActiveTouch builds touchEvent.touches by filtering to touches that share the same eventEmitter as the cancelled touch. This makes synthesized onTouchCancel payloads inconsistent with DispatchTouchEvent, which populates touches from all active touches (minus the changed touch for end-ish events). It can break JS code that relies on touches representing all current screen touches per the TouchEvent contract. Consider constructing touchEvent.touches the same way as DispatchTouchEvent (across m_activeTouches, excluding the cancelled touch), while still dispatching only to cancelledTouch.eventEmitter and keeping targetTouches emitter-scoped.

Copilot uses AI. Check for mistakes.

cancelledTouch.eventEmitter->onTouchCancel(touchEvent);
}

// If we have events that include multiple pointer updates, we should change arg from pointerId to vector<pointerId>
void CompositionEventHandler::DispatchTouchEvent(
TouchEventType eventType,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,11 @@ class CompositionEventHandler : public std::enable_shared_from_this<CompositionE
static void
UpdateActiveTouch(ActiveTouch &activeTouch, facebook::react::Point ptScaled, facebook::react::Point ptLocal) noexcept;

void DispatchSynthesizedTouchCancelForActiveTouch(
const ActiveTouch &cancelledTouch,
const winrt::Microsoft::ReactNative::Composition::Input::PointerPoint &pointerPoint,
winrt::Windows::System::VirtualKeyModifiers keyModifiers);

void UpdateCursor() noexcept;
void SetCursor(facebook::react::Cursor cursor, HCURSOR hcur) noexcept;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -697,10 +697,14 @@ void WindowsTextInputComponentView::OnPointerPressed(
wParam |= (XBUTTON2 << 16);
break;
}
wParam = PointerRoutedEventArgsToMouseWParam(args);
wParam |= PointerRoutedEventArgsToMouseWParam(args);
} else {
msg = WM_POINTERDOWN;
wParam = PointerPointToPointerWParam(pp);
if (IsDoubleClick()) {
msg = WM_LBUTTONDBLCLK;
} else {
msg = WM_LBUTTONDOWN;
}
wParam = PointerRoutedEventArgsToMouseWParam(args);
}

if (m_textServices && msg) {
Expand Down Expand Up @@ -762,10 +766,10 @@ void WindowsTextInputComponentView::OnPointerReleased(
wParam |= (XBUTTON2 << 16);
break;
}
wParam = PointerRoutedEventArgsToMouseWParam(args);
wParam |= PointerRoutedEventArgsToMouseWParam(args);
} else {
msg = WM_POINTERUP;
wParam = PointerPointToPointerWParam(pp);
msg = WM_LBUTTONUP;
wParam = PointerRoutedEventArgsToMouseWParam(args);
}

if (m_textServices && msg) {
Expand Down Expand Up @@ -819,19 +823,19 @@ void WindowsTextInputComponentView::OnPointerMoved(
msg = WM_MOUSEMOVE;
wParam = PointerRoutedEventArgsToMouseWParam(args);
} else {
msg = WM_POINTERUPDATE;
wParam = PointerPointToPointerWParam(pp);
msg = WM_MOUSEMOVE;
wParam = PointerRoutedEventArgsToMouseWParam(args);
}

if (m_textServices) {
LRESULT lresult;
DrawBlock db(*this);
auto hr = m_textServices->TxSendMessage(msg, static_cast<WPARAM>(wParam), static_cast<LPARAM>(lParam), &lresult);
args.Handled(hr != S_FALSE);
}

m_textServices->OnTxSetCursor(
DVASPECT_CONTENT, -1, nullptr, nullptr, nullptr, nullptr, nullptr, ptContainer.x, ptContainer.y);
m_textServices->OnTxSetCursor(
DVASPECT_CONTENT, -1, nullptr, nullptr, nullptr, nullptr, nullptr, ptContainer.x, ptContainer.y);
}
}

void WindowsTextInputComponentView::OnPointerWheelChanged(
Expand Down Expand Up @@ -933,7 +937,7 @@ bool WindowsTextInputComponentView::ShouldSubmit(
bool ctrlDown = (args.KeyboardSource().GetKeyState(winrt::Windows::System::VirtualKey::Control) &
winrt::Microsoft::UI::Input::VirtualKeyStates::Down) ==
winrt::Microsoft::UI::Input::VirtualKeyStates::Down;
bool altDown = (args.KeyboardSource().GetKeyState(winrt::Windows::System::VirtualKey::Control) &
bool altDown = (args.KeyboardSource().GetKeyState(winrt::Windows::System::VirtualKey::Menu) &
winrt::Microsoft::UI::Input::VirtualKeyStates::Down) ==
winrt::Microsoft::UI::Input::VirtualKeyStates::Down;
bool metaDown = (args.KeyboardSource().GetKeyState(winrt::Windows::System::VirtualKey::LeftWindows) &
Expand All @@ -944,7 +948,7 @@ bool WindowsTextInputComponentView::ShouldSubmit(
winrt::Microsoft::UI::Input::VirtualKeyStates::Down;
return (submitKeyEvent.shiftKey && shiftDown) || (submitKeyEvent.ctrlKey && ctrlDown) ||
(submitKeyEvent.altKey && altDown) || (submitKeyEvent.metaKey && metaDown) ||
(!submitKeyEvent.shiftKey && !submitKeyEvent.altKey && !submitKeyEvent.metaKey && !submitKeyEvent.altKey &&
(!submitKeyEvent.shiftKey && !submitKeyEvent.ctrlKey && !submitKeyEvent.altKey && !submitKeyEvent.metaKey &&
!shiftDown && !ctrlDown && !altDown && !metaDown);
} else {
shouldSubmit = false;
Expand Down