diff --git a/dxaml/test/native/external/controls/toggleswitch/ToggleSwitchIntegrationTests.cpp b/dxaml/test/native/external/controls/toggleswitch/ToggleSwitchIntegrationTests.cpp index 5c5346bb85..eb7051b06c 100644 --- a/dxaml/test/native/external/controls/toggleswitch/ToggleSwitchIntegrationTests.cpp +++ b/dxaml/test/native/external/controls/toggleswitch/ToggleSwitchIntegrationTests.cpp @@ -652,4 +652,105 @@ namespace Microsoft { namespace UI { namespace Xaml { namespace Tests { namespac RunOnUIThread([&]() { VERIFY_IS_FALSE(toggleSwitch->IsOn); }); } + void ToggleSwitchIntegrationTests::KnobMaintainsMarginsFromTrackDuringDrag() + { + TestCleanupWrapper cleanup; + xaml_controls::ToggleSwitch^ toggleSwitch = nullptr; + xaml::FrameworkElement^ knob = nullptr; + xaml::FrameworkElement^ knobBounds = nullptr; + xaml::FrameworkElement^ knobOff = nullptr; + xaml::FrameworkElement^ knobOn = nullptr; + + RunOnUIThread([&]() + { + toggleSwitch = ref new xaml_controls::ToggleSwitch(); + toggleSwitch->IsOn = false; + toggleSwitch->HorizontalAlignment = xaml::HorizontalAlignment::Center; + TestServices::WindowHelper->WindowContent = toggleSwitch; + }); + TestServices::WindowHelper->WaitForIdle(); + + RunOnUIThread([&]() + { + knob = TreeHelper::GetVisualChildByName(toggleSwitch, L"SwitchKnob"); + knobBounds = TreeHelper::GetVisualChildByName(toggleSwitch, L"SwitchKnobBounds"); + knobOff = TreeHelper::GetVisualChildByName(toggleSwitch, L"SwitchKnobOff"); + knobOn = TreeHelper::GetVisualChildByName(toggleSwitch, L"SwitchKnobOn"); + VERIFY_IS_NOT_NULL(knob); + VERIFY_IS_NOT_NULL(knobBounds); + VERIFY_IS_NOT_NULL(knobOff); + VERIFY_IS_NOT_NULL(knobOn); + }); + + auto dragAndVerifyMargin = [&](bool isOn, int dragDeltaX) + { + RunOnUIThread([&]() + { + toggleSwitch->IsOn = isOn; + }); + TestServices::WindowHelper->WaitForIdle(); + + bool isMouseButtonDown = false; + TestCleanupWrapper releaseMouse([&]() + { + if (isMouseButtonDown) + { + TestServices::InputHelper->MouseButtonUp(knob, 0, 0, MouseButton::Left); + TestServices::WindowHelper->WaitForIdle(); + } + }); + + TestServices::InputHelper->MouseButtonDown(knob, 0, 0, MouseButton::Left); + isMouseButtonDown = true; + TestServices::WindowHelper->WaitForIdle(); + + TestServices::InputHelper->MoveMouse(knob, dragDeltaX, 0); + TestServices::WindowHelper->WaitForIdle(); + + RunOnUIThread([&]() + { + VERIFY_IS_TRUE(ControlHelper::IsInVisualState(toggleSwitch, L"CommonStates", L"Pressed")); + VERIFY_IS_TRUE(ControlHelper::IsInVisualState(toggleSwitch, L"ToggleStates", L"Dragging")); + + auto knobVisual = isOn ? knobOn : knobOff; + const auto knobVisualOrigin = + knobVisual->TransformToVisual(knobBounds)->TransformPoint({ 0, 0 }); + const double knobVisualWidth = knobVisual->ActualWidth; + const double trackWidth = knobBounds->ActualWidth; + + LOG_OUTPUT( + L"IsOn: %d, knob visual origin: %f, knob visual width: %f, track width: %f", + isOn, + knobVisualOrigin.X, + knobVisualWidth, + trackWidth); + + if (isOn) + { + VERIFY_IS_GREATER_THAN( + knobVisualOrigin.X, + 0.0, + L"Knob left edge should not touch the track boundary while dragging from On."); + } + else + { + VERIFY_IS_LESS_THAN( + knobVisualOrigin.X + knobVisualWidth, + trackWidth, + L"Knob right edge should not touch the track boundary while dragging from Off."); + } + }); + + TestServices::InputHelper->MouseButtonUp(knob, 0, 0, MouseButton::Left); + isMouseButtonDown = false; + TestServices::WindowHelper->WaitForIdle(); + }; + + LOG_OUTPUT(L"Dragging from Off toward the right track boundary."); + dragAndVerifyMargin(false /*isOn*/, 100 /*dragDeltaX*/); + + LOG_OUTPUT(L"Dragging from On toward the left track boundary."); + dragAndVerifyMargin(true /*isOn*/, -100 /*dragDeltaX*/); + } + } } } } } } // Microsoft::UI::Xaml::Tests::Controls::ToggleSwitch diff --git a/dxaml/test/native/external/controls/toggleswitch/ToggleSwitchIntegrationTests.h b/dxaml/test/native/external/controls/toggleswitch/ToggleSwitchIntegrationTests.h index 8dd47c37f3..6ed5959d87 100644 --- a/dxaml/test/native/external/controls/toggleswitch/ToggleSwitchIntegrationTests.h +++ b/dxaml/test/native/external/controls/toggleswitch/ToggleSwitchIntegrationTests.h @@ -71,7 +71,11 @@ namespace Microsoft { namespace UI { namespace Xaml { namespace Tests { namespac BEGIN_TEST_METHOD(DoesNotToggleUsingDirectionalKeys) TEST_METHOD_PROPERTY(L"Description", L"Validates that pressing Home, End, Up, Down, Left, and Right does not toggle the state of the control.") END_TEST_METHOD() + + BEGIN_TEST_METHOD(KnobMaintainsMarginsFromTrackDuringDrag) + TEST_METHOD_PROPERTY(L"Description", L"Validates that the knob maintains proper margins from track boundaries when dragged in its expanded/pressed state.") + TEST_METHOD_PROPERTY(L"TestPass:IncludeOnlyOn", L"Desktop") + END_TEST_METHOD() }; } } } } } } - diff --git a/dxaml/xcp/dxaml/lib/ToggleSwitch_Partial.cpp b/dxaml/xcp/dxaml/lib/ToggleSwitch_Partial.cpp index 182d9d9af8..14fdcbc7f4 100644 --- a/dxaml/xcp/dxaml/lib/ToggleSwitch_Partial.cpp +++ b/dxaml/xcp/dxaml/lib/ToggleSwitch_Partial.cpp @@ -28,6 +28,8 @@ ToggleSwitch::ToggleSwitch() : m_maxCurtainTranslation(0), m_maxKnobTranslation(0), + m_dragVisualClampMax(-1), + m_dragVisualClampMin(-1), m_minCurtainTranslation(0), m_minKnobTranslation(0), @@ -456,6 +458,20 @@ ToggleSwitch::SetTranslations() translation = std::min(m_knobTranslation, m_maxKnobTranslation); translation = std::max(translation, m_minKnobTranslation); + // Keep the expanded knob visual inside the track without changing the + // translation range used for toggle thresholds and animations. + if (m_isDragging) + { + if (m_dragVisualClampMax >= 0) + { + translation = std::min(translation, m_dragVisualClampMax); + } + if (m_dragVisualClampMin >= 0) + { + translation = std::max(translation, m_dragVisualClampMin); + } + } + IFC(m_spKnobTransform->put_X(translation)); if (pToggleSwitchTemplateSettingsNoRef) @@ -813,6 +829,7 @@ ToggleSwitch::DragStartedHandler( IFC(Focus(xaml::FocusState_Pointer, &isFocused)); IFC(GetTranslations()); IFC(UpdateVisualState(TRUE)); + IFC(AdjustTranslationBoundsForDrag()); IFC(SetTranslations()); Cleanup: @@ -852,13 +869,15 @@ ToggleSwitch::DragCompletedHandler( IFC(pArgs->get_Canceled(&isCanceled)); + m_isDragging = FALSE; + m_dragVisualClampMax = -1; + m_dragVisualClampMin = -1; + if (isCanceled) { goto Cleanup; } - m_isDragging = FALSE; - IFC(MoveCompleted(m_wasDragged)); Cleanup: @@ -997,6 +1016,71 @@ ToggleSwitch::SizeChangedHandler( RRETURN(hr); } +_Check_return_ HRESULT +ToggleSwitch::AdjustTranslationBoundsForDrag() +{ + m_dragVisualClampMax = -1; + m_dragVisualClampMin = -1; + + if (!m_tpKnob || !m_tpKnobBounds) + { + return S_OK; + } + + BOOLEAN isOn = FALSE; + IFC_RETURN(get_IsOn(&isOn)); + + ctl::ComPtr spKnobVisualDO; + if (!isOn) + { + IFC_RETURN(GetTemplateChild(wrl_wrappers::HStringReference(STR_LEN_PAIR(L"SwitchKnobOff")).Get(), &spKnobVisualDO)); + } + else + { + IFC_RETURN(GetTemplateChild(wrl_wrappers::HStringReference(STR_LEN_PAIR(L"SwitchKnobOn")).Get(), &spKnobVisualDO)); + } + + auto spKnobVisualElement = spKnobVisualDO.AsOrNull(); + auto spKnobVisualUIElement = spKnobVisualDO.AsOrNull(); + + if (spKnobVisualElement && spKnobVisualUIElement) + { + DOUBLE knobWidth = 0; + DOUBLE knobVisualWidth = 0; + ctl::ComPtr spKnobUIElement; + ctl::ComPtr spTransform; + wf::Point knobVisualOrigin = {}; + + IFC_RETURN(m_tpKnob->get_ActualWidth(&knobWidth)); + IFC_RETURN(spKnobVisualElement->get_ActualWidth(&knobVisualWidth)); + IFC_RETURN(m_tpKnob.As(&spKnobUIElement)); + IFC_RETURN(spKnobVisualUIElement->TransformToVisual(spKnobUIElement.Get(), &spTransform)); + IFC_RETURN(spTransform->TransformPoint({}, &knobVisualOrigin)); + + if (!isOn) + { + const DOUBLE restingMargin = knobVisualOrigin.X; + const DOUBLE clampMax = m_maxKnobTranslation - restingMargin; + if (restingMargin > 0 && clampMax >= m_minKnobTranslation) + { + m_dragVisualClampMax = clampMax; + } + } + else + { + const DOUBLE restingMargin = + knobWidth - knobVisualOrigin.X - knobVisualWidth; + const DOUBLE clampMin = m_minKnobTranslation + restingMargin; + if (restingMargin > 0 && clampMin <= m_maxKnobTranslation) + { + m_dragVisualClampMin = clampMin; + } + } + } + + return S_OK; +} + // Whether the given key may cause the ToggleSwitch to toggle. BOOLEAN ToggleSwitch::HandlesKey( @@ -1018,6 +1102,8 @@ _Check_return_ HRESULT ToggleSwitch::OnIsEnabledChanged(_In_ IsEnabledChangedEve if (!bIsEnabled) { m_isDragging = FALSE; + m_dragVisualClampMax = -1; + m_dragVisualClampMin = -1; m_isPointerOver = FALSE; } IFC(UpdateVisualState()); @@ -1037,6 +1123,8 @@ ToggleSwitch::OnVisibilityChanged() if (xaml::Visibility_Visible != visibility) { m_isDragging = FALSE; + m_dragVisualClampMax = -1; + m_dragVisualClampMin = -1; m_isPointerOver = FALSE; } diff --git a/dxaml/xcp/dxaml/lib/ToggleSwitch_Partial.h b/dxaml/xcp/dxaml/lib/ToggleSwitch_Partial.h index 9c94df84b0..142e74aae2 100644 --- a/dxaml/xcp/dxaml/lib/ToggleSwitch_Partial.h +++ b/dxaml/xcp/dxaml/lib/ToggleSwitch_Partial.h @@ -127,6 +127,9 @@ namespace DirectUI _In_ IInspectable *pSender, _In_ xaml::ISizeChangedEventArgs *pArgs); + _Check_return_ HRESULT + AdjustTranslationBoundsForDrag(); + _Check_return_ HRESULT TapHandler( _In_ IInspectable *pSender, @@ -156,6 +159,10 @@ namespace DirectUI DOUBLE m_maxKnobTranslation; + // Visual-only drag limits; -1 means no additional clamp. + DOUBLE m_dragVisualClampMax; + DOUBLE m_dragVisualClampMin; + DOUBLE m_curtainTranslation; DOUBLE m_minCurtainTranslation;