UiNavigationHelpers.cpp 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535
  1. /*
  2. * Copyright (c) Contributors to the Open 3D Engine Project.
  3. * For complete copyright and license terms please see the LICENSE at the root of this distribution.
  4. *
  5. * SPDX-License-Identifier: Apache-2.0 OR MIT
  6. *
  7. */
  8. #include "UiNavigationHelpers.h"
  9. #include <LyShine/Bus/UiElementBus.h>
  10. #include <LyShine/Bus/UiNavigationBus.h>
  11. #include <LyShine/Bus/UiInteractableBus.h>
  12. #include <AzFramework/Input/Devices/Gamepad/InputDeviceGamepad.h>
  13. #include <AzFramework/Input/Devices/Keyboard/InputDeviceKeyboard.h>
  14. #include <AzFramework/Input/Devices/VirtualKeyboard/InputDeviceVirtualKeyboard.h>
  15. namespace UiNavigationHelpers
  16. {
  17. ////////////////////////////////////////////////////////////////////////////////////////////////////
  18. Command MapInputChannelIdToUiNavigationCommand(const AzFramework::InputChannelId& inputChannelId,
  19. AzFramework::ModifierKeyMask activeModifierKeys)
  20. {
  21. if (inputChannelId == AzFramework::InputDeviceGamepad::Button::DU ||
  22. inputChannelId == AzFramework::InputDeviceGamepad::ThumbStickDirection::LU ||
  23. inputChannelId == AzFramework::InputDeviceKeyboard::Key::NavigationArrowUp)
  24. {
  25. return Command::Up;
  26. }
  27. if (inputChannelId == AzFramework::InputDeviceGamepad::Button::DD ||
  28. inputChannelId == AzFramework::InputDeviceGamepad::ThumbStickDirection::LD ||
  29. inputChannelId == AzFramework::InputDeviceKeyboard::Key::NavigationArrowDown)
  30. {
  31. return Command::Down;
  32. }
  33. if (inputChannelId == AzFramework::InputDeviceGamepad::Button::DL ||
  34. inputChannelId == AzFramework::InputDeviceGamepad::ThumbStickDirection::LL ||
  35. inputChannelId == AzFramework::InputDeviceKeyboard::Key::NavigationArrowLeft)
  36. {
  37. return Command::Left;
  38. }
  39. if (inputChannelId == AzFramework::InputDeviceGamepad::Button::DR ||
  40. inputChannelId == AzFramework::InputDeviceGamepad::ThumbStickDirection::LR ||
  41. inputChannelId == AzFramework::InputDeviceKeyboard::Key::NavigationArrowRight)
  42. {
  43. return Command::Right;
  44. }
  45. bool enterPressed = inputChannelId == AzFramework::InputDeviceKeyboard::Key::EditEnter ||
  46. inputChannelId == AzFramework::InputDeviceVirtualKeyboard::Command::EditEnter;
  47. bool shiftModifierPressed = (static_cast<int>(activeModifierKeys) & static_cast<int>(AzFramework::ModifierKeyMask::ShiftAny)) != 0;
  48. if (inputChannelId == AzFramework::InputDeviceGamepad::Button::A ||
  49. (enterPressed && !shiftModifierPressed))
  50. {
  51. return Command::Enter;
  52. }
  53. if (inputChannelId == AzFramework::InputDeviceGamepad::Button::B ||
  54. inputChannelId == AzFramework::InputDeviceKeyboard::Key::Escape ||
  55. (enterPressed && shiftModifierPressed))
  56. {
  57. return Command::Back;
  58. }
  59. if (inputChannelId == AzFramework::InputDeviceKeyboard::Key::NavigationEnd)
  60. {
  61. return Command::NavEnd;
  62. }
  63. if (inputChannelId == AzFramework::InputDeviceKeyboard::Key::NavigationHome)
  64. {
  65. return Command::NavHome;
  66. }
  67. return Command::Unknown;
  68. }
  69. ////////////////////////////////////////////////////////////////////////////////////////////////////
  70. AZ::EntityId GetNextElement(AZ::EntityId curEntityId, Command command,
  71. const LyShine::EntityArray& navigableElements, AZ::EntityId defaultEntityId,
  72. ValidationFunction isValidResult, AZ::EntityId parentElement)
  73. {
  74. AZ::EntityId nextEntityId;
  75. bool found = false;
  76. do
  77. {
  78. nextEntityId.SetInvalid();
  79. UiNavigationInterface::NavigationMode navigationMode = UiNavigationInterface::NavigationMode::None;
  80. UiNavigationBus::EventResult(navigationMode, curEntityId, &UiNavigationBus::Events::GetNavigationMode);
  81. if (navigationMode == UiNavigationInterface::NavigationMode::Custom)
  82. {
  83. // Ask the current interactable what the next interactable should be
  84. nextEntityId = FollowCustomLink(curEntityId, command);
  85. if (nextEntityId.IsValid())
  86. {
  87. // Skip over elements that are not valid
  88. if (isValidResult(nextEntityId))
  89. {
  90. found = true;
  91. }
  92. else
  93. {
  94. curEntityId = nextEntityId;
  95. }
  96. }
  97. else
  98. {
  99. found = true;
  100. }
  101. }
  102. else if (navigationMode == UiNavigationInterface::NavigationMode::Automatic)
  103. {
  104. nextEntityId = SearchForNextElement(curEntityId, command, navigableElements, parentElement);
  105. found = true;
  106. }
  107. else
  108. {
  109. // If navigationMode is None we should never get here via keyboard navigation
  110. // and we may not be able to get to other elements from here (e.g. this could be
  111. // a full screen button in the background). So go to the passed in default.
  112. nextEntityId = defaultEntityId;
  113. found = true;
  114. }
  115. } while (!found);
  116. return nextEntityId;
  117. }
  118. ////////////////////////////////////////////////////////////////////////////////////////////////////
  119. AZ::EntityId SearchForNextElement(AZ::EntityId curElement, Command command,
  120. const LyShine::EntityArray& navigableElements, AZ::EntityId parentElement)
  121. {
  122. // Check if the current element is a descendant of the parent of the navigable elements.
  123. // If it isn't a descendant, then priority is given to the navigable elements
  124. // that are visible within their parent's bounds
  125. bool isCurElementDescendantOfParentElement = false;
  126. if (parentElement.IsValid())
  127. {
  128. UiElementBus::EventResult(isCurElementDescendantOfParentElement, curElement, &UiElementBus::Events::IsAncestor, parentElement);
  129. }
  130. UiTransformInterface::Rect parentRect;
  131. parentRect.Set(0.0f, 0.0f, 0.0f, 0.0f);
  132. AZ::Matrix4x4 parentTransformFromViewport;
  133. if (parentElement.IsValid() && !isCurElementDescendantOfParentElement)
  134. {
  135. UiTransformBus::Event(parentElement, &UiTransformBus::Events::GetCanvasSpaceRectNoScaleRotate, parentRect);
  136. UiTransformBus::Event(parentElement, &UiTransformBus::Events::GetTransformFromViewport, parentTransformFromViewport);
  137. }
  138. UiTransformInterface::RectPoints srcPoints;
  139. UiTransformBus::Event(curElement, &UiTransformBus::Events::GetViewportSpacePoints, srcPoints);
  140. AZ::Vector2 srcCenter = srcPoints.GetCenter();
  141. // Go through the navigable elements and find the closest element to the current hover interactable
  142. float shortestDist = FLT_MAX;
  143. float shortestCenterToCenterDist = FLT_MAX;
  144. AZ::EntityId closestElement;
  145. float shortestOutsideDist = FLT_MAX;
  146. float shortestOutsideCenterToCenterDist = FLT_MAX;
  147. AZ::EntityId closestOutsideElement;
  148. for (auto navigableElement : navigableElements)
  149. {
  150. UiTransformInterface::RectPoints destPoints;
  151. UiTransformBus::Event(navigableElement->GetId(), &UiTransformBus::Events::GetViewportSpacePoints, destPoints);
  152. AZ::Vector2 destCenter = destPoints.GetCenter();
  153. bool correctDirection = false;
  154. if (command == Command::Up)
  155. {
  156. correctDirection = destCenter.GetY() < srcPoints.GetAxisAlignedTopLeft().GetY();
  157. }
  158. else if (command == Command::Down)
  159. {
  160. correctDirection = destCenter.GetY() > srcPoints.GetAxisAlignedBottomLeft().GetY();
  161. }
  162. else if (command == Command::Left)
  163. {
  164. correctDirection = destCenter.GetX() < srcPoints.GetAxisAlignedTopLeft().GetX();
  165. }
  166. else if (command == Command::Right)
  167. {
  168. correctDirection = destCenter.GetX() > srcPoints.GetAxisAlignedTopRight().GetX();
  169. }
  170. if (correctDirection)
  171. {
  172. // Calculate an overlap value from 0 to 1
  173. float overlapValue = 0.0f;
  174. if (command == Command::Up || command == Command::Down)
  175. {
  176. float srcLeft = srcPoints.GetAxisAlignedTopLeft().GetX();
  177. float srcRight = srcPoints.GetAxisAlignedTopRight().GetX();
  178. float destLeft = destPoints.GetAxisAlignedTopLeft().GetX();
  179. float destRight = destPoints.GetAxisAlignedTopRight().GetX();
  180. if ((srcLeft <= destLeft && srcRight >= destRight)
  181. || (srcLeft >= destLeft && srcRight <= destRight))
  182. {
  183. overlapValue = 1.0f;
  184. }
  185. else
  186. {
  187. float x1 = max(srcLeft, destLeft);
  188. float x2 = min(srcRight, destRight);
  189. if (x1 <= x2)
  190. {
  191. float overlap = x2 - x1;
  192. overlapValue = max(overlap / (srcRight - srcLeft), overlap / (destRight - destLeft));
  193. }
  194. }
  195. }
  196. else // Command::Left || Command::Right
  197. {
  198. float destTop = destPoints.GetAxisAlignedTopLeft().GetY();
  199. float destBottom = destPoints.GetAxisAlignedBottomLeft().GetY();
  200. float srcTop = srcPoints.GetAxisAlignedTopLeft().GetY();
  201. float srcBottom = srcPoints.GetAxisAlignedBottomLeft().GetY();
  202. if ((srcTop <= destTop && srcBottom >= destBottom)
  203. || (srcTop >= destTop && srcBottom <= destBottom))
  204. {
  205. overlapValue = 1.0f;
  206. }
  207. else
  208. {
  209. float y1 = max(srcTop, destTop);
  210. float y2 = min(srcBottom, destBottom);
  211. if (y1 <= y2)
  212. {
  213. float overlap = y2 - y1;
  214. overlapValue = max(overlap / (srcBottom - srcTop), overlap / (destBottom - destTop));
  215. }
  216. }
  217. }
  218. // Set src and dest points used for distance test
  219. AZ::Vector2 srcPoint;
  220. AZ::Vector2 destPoint;
  221. if ((command == Command::Up) || command == Command::Down)
  222. {
  223. float srcY;
  224. float destY;
  225. if (command == Command::Up)
  226. {
  227. srcY = srcPoints.GetAxisAlignedTopLeft().GetY();
  228. destY = destPoints.GetAxisAlignedBottomLeft().GetY();
  229. if (destY > srcY)
  230. {
  231. destY = srcY;
  232. }
  233. }
  234. else // Command::Down
  235. {
  236. srcY = srcPoints.GetAxisAlignedBottomLeft().GetY();
  237. destY = destPoints.GetAxisAlignedTopLeft().GetY();
  238. if (destY < srcY)
  239. {
  240. destY = srcY;
  241. }
  242. }
  243. srcPoint = AZ::Vector2((overlapValue < 1.0f ? srcCenter.GetX() : destCenter.GetX()), srcY);
  244. destPoint = AZ::Vector2(destCenter.GetX(), destY);
  245. }
  246. else // Command::Left || Command::Right
  247. {
  248. float srcX;
  249. float destX;
  250. if (command == Command::Left)
  251. {
  252. srcX = srcPoints.GetAxisAlignedTopLeft().GetX();
  253. destX = destPoints.GetAxisAlignedTopRight().GetX();
  254. if (destX > srcX)
  255. {
  256. destX = srcX;
  257. }
  258. }
  259. else // Command::Right
  260. {
  261. srcX = srcPoints.GetAxisAlignedTopRight().GetX();
  262. destX = destPoints.GetAxisAlignedTopLeft().GetX();
  263. if (destX < srcX)
  264. {
  265. destX = srcX;
  266. }
  267. }
  268. srcPoint = AZ::Vector2(srcX, (overlapValue < 1.0f ? srcCenter.GetY() : destCenter.GetY()));
  269. destPoint = AZ::Vector2(destX, destCenter.GetY());
  270. }
  271. // Calculate angle distance value from 0 to 1
  272. float angleDist;
  273. AZ::Vector2 dir = destPoint - srcPoint;
  274. float angle = RAD2DEG(atan2(-dir.GetY(), dir.GetX()));
  275. if (angle < 0.0f)
  276. {
  277. angle += 360.0f;
  278. }
  279. if (command == Command::Up)
  280. {
  281. angleDist = fabs(90.0f - angle);
  282. }
  283. else if (command == Command::Down)
  284. {
  285. angleDist = fabs(270.0f - angle);
  286. }
  287. else if (command == Command::Left)
  288. {
  289. angleDist = fabs(180.0f - angle);
  290. }
  291. else // Command::Right
  292. {
  293. angleDist = fabs((angle <= 180.0f ? 0.0f : 360.0f) - angle);
  294. }
  295. float angleValue = angleDist / 90.0f;
  296. // Calculate final distance value biased by overlap and angle values
  297. float dist = (destPoint - srcPoint).GetLength();
  298. const float distMultConstant = 1.0f;
  299. dist += dist * distMultConstant * angleValue * (1.0f - overlapValue);
  300. bool inside = true;
  301. if (parentElement.IsValid() && !isCurElementDescendantOfParentElement)
  302. {
  303. // Check if the element is inside the bounds of its parent
  304. UiTransformInterface::RectPoints destPointsFromViewport = destPoints.Transform(parentTransformFromViewport);
  305. AZ::Vector2 center = destPointsFromViewport.GetCenter();
  306. inside = (center.GetX() >= parentRect.left &&
  307. center.GetX() <= parentRect.right &&
  308. center.GetY() >= parentRect.top &&
  309. center.GetY() <= parentRect.bottom);
  310. }
  311. if (inside)
  312. {
  313. if (dist < shortestDist)
  314. {
  315. shortestDist = dist;
  316. shortestCenterToCenterDist = (destCenter - srcCenter).GetLengthSq();
  317. closestElement = navigableElement->GetId();
  318. }
  319. else if (dist == shortestDist)
  320. {
  321. // Break a tie using center to center distance
  322. float centerToCenterDist = (destCenter - srcCenter).GetLengthSq();
  323. if (centerToCenterDist < shortestCenterToCenterDist)
  324. {
  325. shortestCenterToCenterDist = centerToCenterDist;
  326. closestElement = navigableElement->GetId();
  327. }
  328. }
  329. }
  330. else
  331. {
  332. if (dist < shortestOutsideDist)
  333. {
  334. shortestOutsideDist = dist;
  335. shortestOutsideCenterToCenterDist = (destCenter - srcCenter).GetLengthSq();
  336. closestOutsideElement = navigableElement->GetId();
  337. }
  338. else if (dist == shortestOutsideDist)
  339. {
  340. // Break a tie using center to center distance
  341. float centerToCenterDist = (destCenter - srcCenter).GetLengthSq();
  342. if (centerToCenterDist < shortestOutsideCenterToCenterDist)
  343. {
  344. shortestOutsideCenterToCenterDist = centerToCenterDist;
  345. closestOutsideElement = navigableElement->GetId();
  346. }
  347. }
  348. }
  349. }
  350. }
  351. return closestElement.IsValid() ? closestElement : closestOutsideElement;
  352. }
  353. ////////////////////////////////////////////////////////////////////////////////////////////////////
  354. AZ::EntityId FollowCustomLink(AZ::EntityId curEntityId, Command command)
  355. {
  356. AZ::EntityId nextEntityId;
  357. // Ask the current interactable what the next interactable should be
  358. if (command == Command::Up)
  359. {
  360. UiNavigationBus::EventResult(nextEntityId, curEntityId, &UiNavigationBus::Events::GetOnUpEntity);
  361. }
  362. else if (command == Command::Down)
  363. {
  364. UiNavigationBus::EventResult(nextEntityId, curEntityId, &UiNavigationBus::Events::GetOnDownEntity);
  365. }
  366. else if (command == Command::Left)
  367. {
  368. UiNavigationBus::EventResult(nextEntityId, curEntityId, &UiNavigationBus::Events::GetOnLeftEntity);
  369. }
  370. else if (command == Command::Right)
  371. {
  372. UiNavigationBus::EventResult(nextEntityId, curEntityId, &UiNavigationBus::Events::GetOnRightEntity);
  373. }
  374. return nextEntityId;
  375. }
  376. ////////////////////////////////////////////////////////////////////////////////////////////////////
  377. bool IsInteractableNavigable(AZ::EntityId interactableEntityId)
  378. {
  379. bool navigable = false;
  380. UiNavigationInterface::NavigationMode navigationMode = UiNavigationInterface::NavigationMode::None;
  381. UiNavigationBus::EventResult(navigationMode, interactableEntityId, &UiNavigationBus::Events::GetNavigationMode);
  382. if (navigationMode != UiNavigationInterface::NavigationMode::None)
  383. {
  384. // Check if the interactable is enabled
  385. bool isEnabled = false;
  386. UiElementBus::EventResult(isEnabled, interactableEntityId, &UiElementBus::Events::IsEnabled);
  387. if (isEnabled)
  388. {
  389. // Check if the interactable is handling events
  390. UiInteractableBus::EventResult(navigable, interactableEntityId, &UiInteractableBus::Events::IsHandlingEvents);
  391. }
  392. }
  393. return navigable;
  394. }
  395. ////////////////////////////////////////////////////////////////////////////////////////////////////
  396. bool IsElementInteractableAndNavigable(AZ::EntityId entityId)
  397. {
  398. bool navigable = false;
  399. // Check if the element handles navigation events, we are specifically looking for interactables
  400. if (UiInteractableBus::FindFirstHandler(entityId))
  401. {
  402. navigable = IsInteractableNavigable(entityId);
  403. }
  404. return navigable;
  405. }
  406. ////////////////////////////////////////////////////////////////////////////////////////////////////
  407. void FindNavigableInteractables(AZ::EntityId parentElement, AZ::EntityId ignoreElement, LyShine::EntityArray& result)
  408. {
  409. LyShine::EntityArray elements;
  410. UiElementBus::EventResult(elements, parentElement, &UiElementBus::Events::GetChildElements);
  411. AZStd::list<AZ::Entity*> elementList(elements.begin(), elements.end());
  412. while (!elementList.empty())
  413. {
  414. auto& entity = elementList.front();
  415. // Check if the element handles navigation events, we are specifically looking for interactables
  416. bool handlesNavigationEvents = false;
  417. if (UiInteractableBus::FindFirstHandler(entity->GetId()))
  418. {
  419. UiNavigationInterface::NavigationMode navigationMode = UiNavigationInterface::NavigationMode::None;
  420. UiNavigationBus::EventResult(navigationMode, entity->GetId(), &UiNavigationBus::Events::GetNavigationMode);
  421. handlesNavigationEvents = (navigationMode != UiNavigationInterface::NavigationMode::None);
  422. }
  423. // Check if the element is enabled
  424. bool isEnabled = false;
  425. UiElementBus::EventResult(isEnabled, entity->GetId(), &UiElementBus::Events::IsEnabled);
  426. bool navigable = false;
  427. if (handlesNavigationEvents && isEnabled && (!ignoreElement.IsValid() || entity->GetId() != ignoreElement))
  428. {
  429. // Check if the element is handling events
  430. bool isHandlingEvents = false;
  431. UiInteractableBus::EventResult(isHandlingEvents, entity->GetId(), &UiInteractableBus::Events::IsHandlingEvents);
  432. navigable = isHandlingEvents;
  433. }
  434. if (navigable)
  435. {
  436. result.push_back(entity);
  437. }
  438. if (!handlesNavigationEvents && isEnabled)
  439. {
  440. LyShine::EntityArray childElements;
  441. UiElementBus::EventResult(childElements, entity->GetId(), &UiElementBus::Events::GetChildElements);
  442. elementList.insert(elementList.end(), childElements.begin(), childElements.end());
  443. }
  444. elementList.pop_front();
  445. }
  446. }
  447. ////////////////////////////////////////////////////////////////////////////////////////////////////
  448. AZ::EntityId FindAncestorNavigableInteractable(AZ::EntityId childInteractable, bool ignoreAutoActivatedAncestors)
  449. {
  450. AZ::EntityId parent;
  451. UiElementBus::EventResult(parent, childInteractable, &UiElementBus::Events::GetParentEntityId);
  452. while (parent.IsValid())
  453. {
  454. if (UiNavigationHelpers::IsElementInteractableAndNavigable(parent))
  455. {
  456. if (ignoreAutoActivatedAncestors)
  457. {
  458. // Check if this hover interactable should automatically go to an active state
  459. bool autoActivated = false;
  460. UiInteractableBus::EventResult(autoActivated, parent, &UiInteractableBus::Events::GetIsAutoActivationEnabled);
  461. if (!autoActivated)
  462. {
  463. break;
  464. }
  465. }
  466. else
  467. {
  468. break;
  469. }
  470. }
  471. AZ::EntityId newParent = parent;
  472. parent.SetInvalid();
  473. UiElementBus::EventResult(parent, newParent, &UiElementBus::Events::GetParentEntityId);
  474. }
  475. return parent;
  476. }
  477. } // namespace UiNavigationHelpers