Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Generic UI navigation system #41

Closed
wants to merge 33 commits into from
Closed

Conversation

nicopap
Copy link

@nicopap nicopap commented Nov 6, 2021

RENDERED

Two new components and a new system to define navigation nodes for the ECS UI.

On top of making it easy to implement controller navigation of in-game UIs, it enables keyboard navigation and answers accessibility concerns such as the ones raised in this issue.

Roadmap

  • Create a MVP that acts as a bevy plugin
  • Design doc of latest implementation
  • Solve open questions
    • Naming: NavFenceNavMenu?
    • Game-orientation and potential conflict with editor design
    • NavRequest and input handling
    • Any other that were not asked in this RFC?
  • Implement downward navigation optimization & "deep focus memory"
  • Mouse support
  • Find a name for the bevy crate, since bevy_ui_navigation is already taken (my bad 😢)
  • Add ui navigation system (RFC 41) bevy#5378
  • After merge, open issues for identified shortcomings and possible improvements:
    • Multicursor
    • tab-navigation
    • make public the TreeMenu systems (insert_tree_menus and set_first_focused)
    • Higher level wrapper for menu switching
    • Handling of Display::None (if it doesn't work already)
    • Robustness from deleting parent menus while inside of them
    • Optimizations, reduce number of iterated focusables in various places

@nicopap
Copy link
Author

nicopap commented Nov 6, 2021

Implementation going on here: https://github.com/nicopap/ui-navigation

@TheRawMeatball
Copy link
Member

This is super exciting, though I have concerns about having people manually build the navigation tree. I think all navigation info should be derived from the existing widget tree, at no extra cost to the developer. The way webpages handle this stuff should probably also be considered under prior art.

Also btw, if you have a discord account it'd be great if came in and said hi at #ui-dev. I don't think Github is ideal for the fast-paced brainstorming etc. that happens early on in design.

@nicopap
Copy link
Author

nicopap commented Nov 6, 2021

The user only has to add Navigable components to relevant nodes. With this, the system should be able to construct a flat navigation graph based solely on the Transform (or GlobalTransform maybe?) of the nodes.

They only need to add Container components if they want special behaviors for different menus (like the tabbed menu in the example). Or hierarchical menu navigation (like going option > graphical options > Resolution)

I think it's necessary to specify the navigable nodes, because there is a lot of irrelevant-to-navigation nodes in the UI tree. Some nodes only exist to specify the layout, others are just pictures etc.

`NavEvent::FocusChanged` or `NavEvent::Caught`.

The navigation system doesn't handle itself the state of the UI, but instead
emits the `FocusChanged` etc. events that the developer will need to read to,
Copy link
Member

Choose a reason for hiding this comment

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

IMO, this shouldn't be an event. Instead, toggle the value of Focused enum (like we have for buttons), and then let the developers use change detection for this.

Copy link
Member

Choose a reason for hiding this comment

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

While for the current system this'd be the best option, in the context of a possibly larger reworking of UI adding a separate concept of "UI events" with propagation and using said events for this would probably be better. Out of this RFC s scope though.

Copy link
Author

Choose a reason for hiding this comment

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

There is two reasons behind this:

  1. It's a way for me to completely hand-wave away everything but exactly the navigation algorithm
  2. It responds to that specific request from ndarilek

Let's go back to the menu. In the example code, I refer to `:container` and
`:navigable`. I've not explained yet what those are. Let's clear things up.

A _navigable_ is an element that can be focused. A _container_ is a node entity
Copy link
Member

Choose a reason for hiding this comment

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

I suspect we can just reuse the existing Interaction component, probably reflavoring to Focus. The venn diagram is a circle. so we can use the same component for both marking and storing the state.

@TheRawMeatball
Copy link
Member

The user only has to add Navigable components to relevant nodes. ... They only need to add Container components if they want special behaviors for different menus. ...

While I agree with Navigable, I'm not quite as sure on what Container gets us. Wouldn't a simple depth-first iteration over the ui tree where we only look at Navigables get us something basically equivalent?

@nicopap
Copy link
Author

nicopap commented Nov 6, 2021

I thought a bit more about the design. A few things:

  1. I want to get rid of the concept of Plane. Beside 2D movements with arrows, the only real use case for other "dimensions" is just going through a tabbed menu and going up/down the hierarchy. The idea of a "select" plane doesn't make sense and doesn't really work.
  2. The "one container within other containers" is too limiting and I feel it's not too much of a burden to have a tree with multiple branches.
  3. NavigableFocusable. Hopefully this clarifies the difference with Containers.

Tomorrow I'll add a roadmap for the implementation in the ui-navigation repo

@nicopap
Copy link
Author

nicopap commented Nov 6, 2021

The user only has to add Navigable components to relevant nodes. ... They only need to add Container components if they want special behaviors for different menus. ...

While I agree with Navigable, I'm not quite as sure on what Container gets us. Wouldn't a simple depth-first iteration over the ui tree where we only look at Navigables get us something basically equivalent?

The difference between Container and Navigable is that you can focus on a navigable, while the container cannot be focused. In the example I gave, it wouldn't make sense to be able to focus on the entire submenu, including the panels and the "ABC" menu. Only the specific panels and the individual circles should be able to be focused. The Container is still necessary because it separates the "ABC" menu from the "soul" menu. You would have to press A and B to go from one to another. The other thing is that the "ABC" menu is not the same kind of thing as the panels on the left side, and when cycling through the panels, you shouldn't be able to end up in the large menu.

A case can be made that you should. This was part of the initial design, but as I came up with a more concrete proposal, it became less practical.

In the implementation, the Container also act as hard boundary when searching siblings in the UI hierarchy, reducing the depths of searches, and specifying exactly where to look for focusable elements, rather than going through dead ends.

@nicopap
Copy link
Author

nicopap commented Nov 7, 2021

Updated plugin demo with reference and bevy-specific implementations. There is an example usage at flat_2d_nav.rs

I'm surprised how the ECS version is basically just a series of function call. Although the flipside is that it's way more error-prone than the "generic" version which enforces the invariants at the API level.

Next step is clarify this RFC. Afterward, I'll add Action/Cancel to the implementation which seems trivial.

@TheRawMeatball
Copy link
Member

The other thing is that the "ABC" menu is not the same kind of thing as the panels on the left side, and when cycling through the panels, you shouldn't be able to end up in the large menu. A case can be made that you should. This was part of the initial design, but as I came up with a more concrete proposal, it became less practical.

I'm very not sure whether we should block traversing out of a container node. A counter example might be a webpage or most other UI, where you can visit every focusable UI node by hitting tab. I don't think we should have hard boundaries.

@nicopap
Copy link
Author

nicopap commented Nov 7, 2021

I'm very not sure whether we should block traversing out of a container node. A counter example might be a webpage or most other UI, where you can visit every focusable UI node by hitting tab. I don't think we should have hard boundaries.

Nothing to worry about. Container (that I renamed for now into NavNode) is entirely optional in this example NavNode is not even imported.

In this example we explicitly use NavNode as a way to isolate some parts of the UI from other. You'll notice that we can still move from one container to the other, only the one explicitly marked with a NavNode is closed-off.

Maybe naming it NavigationBarrier would be more explicit?

@alice-i-cecile
Copy link
Member

Maybe naming it NavigationBarrier would be more explicit?

Yes please, although probably NavBarrier for consistency.

tldr:
* Get rid of concept of `Plane`
* A lot of renaming: Navigable => Focusable, Container => NavFence
* Put more emphasis on "single navigation path" limitations
* Add a `Terminology` section
* Improve illustrations to highlight the concept of `Active`
* Elude UI specification from example
* Use rust code for the algorithm
* Remove plain-text English explanation of the algorithm
rfcs/41-ui-navigation.md Outdated Show resolved Hide resolved
@JonahPlusPlus
Copy link

@alice-i-cecile told me to check this out. I got to say, I really like this design, and especially appreciate how NavCommands are sent by the app and not the library. If you add mouse support, I think you should make it another NavCommand that stores a Vec2 or something, and let people create the bindings themselves (unlike the current UI focus system). It requires more work on their side, but I think if bevy wants to work for bigger and more diverse titles, it needs flexibility.

@nicopap
Copy link
Author

nicopap commented Nov 10, 2021

I cooked up a new design! It solves basically all the unresolved questions. It's a relatively minor implementation change, but it's a major change in the way to think about the navigation graph, and it requires changing important parts of this RFC.

I'm quite fed up editing graphs in Inkscape, but I think this is the last stretch and having a clear documentation on top of the implementation would be golden. Hopefully a good formal source of inspiration for implementing ui navigation in other UI projects too.

@nicopap
Copy link
Author

nicopap commented Jul 22, 2022

I decided to copy parts of the bevy-ui-navigation API docs for this. Since those are well thought out.

@nicopap nicopap marked this pull request as ready for review July 22, 2022 12:16
rfcs/41-ui-navigation.md Outdated Show resolved Hide resolved
Comment on lines 514 to 516
That proxy component is `MenuSeed`,
you can initialize it either with the `Name` or `Entity` of the parent focusable of the menu.
A system will take all `MenuSeed` components and replace them with a `TreeMenu`.
Copy link
Author

Choose a reason for hiding this comment

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

Note that the current implementation has simplified this. MenuSetting is now a component, and we have another MenuBuilder component that still gets converted by the navigation plugin into a TreeMenu.

Issue is I don't like "MenuBuilder" and wait to have a better name before updating the RFC.

Comment on lines +21 to +24
- _transitive_: If `A` is _foo_ of `B` (swap _foo_ with anything such as
_child_, _left_ etc.)
Then `A` is _transitive foo_ of `B` if there is any chain of `C`, `D`,
`E` etc. such as `A` is _foo_ `C`, `C` is _foo_ `D`, ... , `E` is _foo_ `B`.
Copy link
Author

Choose a reason for hiding this comment

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

I'm so sorry. I hope this is a good enough explanation for everyone :D

Copy link

@Weibye Weibye left a comment

Choose a reason for hiding this comment

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

This is a good and well thought out design! I like the decoupling of everything and I think that is important for a robust and flexible design.

I do think there should be a NavRequest::Next and NavRequest::Previous (or something similar) to facilitate behaviours like tab-cycling through a menu

  • A user should be able to press next (tab) or previous (shift + tab) to bring focus to the different focusable elements within the menu
  • A developer should be able to define the order the elements should be cycled through
  • This could be completely unrelated to the geometric layout of the elements
  • I'm pretty sure this is an important feature for general accessibility and screen-reader support


`Focusable`s have a `prioritized` state that is set when they go from
active/focused to not active.
This is a form of memory that allows focus to go back to the last focused
Copy link

Choose a reason for hiding this comment

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

I think we need the ability for the developer to define:

"When coming back to this menu, should we focus on the last-focused element or should we focus on the initial default element"

There are use cases in apps (filling in a new form) or games where we want to treat a menu as "first-time-user-visits" even though the user has visited before. The FFX battle system also springs to mind where you can choose between these two modes, which has a profound effect on muscle-memorisation.

Copy link
Author

Choose a reason for hiding this comment

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

That's something I overlooked. Current design doesn't allow the user to modify the prioritized focusable. I think this can be enabled by providing a custom SystemParam.

Something that automates that could be a natural extension of the exposed API.
It may also help end users go with the best design first.

### User upheld invariants
Copy link

Choose a reason for hiding this comment

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

IMO there are much move severe invariants already in the engine, which to a some degree is needing to be upheld by the user (GlobalTransform + Transform for hierarchy to work comes to mind).

That in itself is not a good argument for adding more, but we are going to need a good solution for Archetype Invariants engine-wide anyway which this design would benefit from so I'm less nervous about this.

That being said, there is plenty of value is exploring alternatives but I don't think this should be that hard of a blocker in the name of getting some velocity on core UI features.

Comment on lines +333 to +335
The "tabs menu" is defined as a "scope menu", which means that
by default, the `LT` and `RT` gamepad buttons will navigate
the "tabs menu" regardless of the current focus position.
Copy link

Choose a reason for hiding this comment

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

I'm guessing this is just for illustrative purposes, but we should aim to separate button inputs from action events, which would be out of scope for this RFC (and I suspect @alice-i-cecile's input manager is making a good job of that) but could this be example be rewritten to use something like NavRequest::Next and NavRequest::Previous (as suggested above)?

@Weibye
Copy link

Weibye commented Jul 29, 2022

Imagine this skill tree. I'm suspecting that just using NavEvent::Move(direction) would allow the user to freely navigate between nodes as if they were not connected by this hierarchy. Would this design allow me to define navigation so that the user is forced to navigate along the white path?

image

Source: Star Wars Jedi: Fallen Order

@nicopap
Copy link
Author

nicopap commented Jul 29, 2022

I do think there should be a NavRequest::Next and NavRequest::Previous (or something similar) to facilitate behaviours like tab-cycling through a menu

Strongly agree, will add that to the design. Though I need to think what's the best way to let users specify ordering without being a burden.

Imagine this skill tree. I'm suspecting that just using NavEvent::Move(direction) would allow the user to freely navigate between nodes as if they were not connected by this hierarchy. Would this design allow me to define navigation so that the user is forced to navigate along the white path?

Yes. The default navigation option is very rudimentary and only cares about position, but you can disable it to replace with your own strategy (MenuNavigationStrategy trait)

@nicopap
Copy link
Author

nicopap commented Jul 30, 2022

@Weibye On second though, I think the tab cycling feature should wait. I'll add it to the "future" section. The PR is massive as it is, and I don't want to pile on functionalities. It's already going to be difficult to review as-is.

There is a few features that I decided to delegate to future endeavor. What I will do is that if the ui navigation PR is merged, I'll immediately open an issue for each functionalities I left behind (and a tracking issue on top). This also allows more piecemeal PRs that are better for people to familiarize themselves with the ui-navigation code (I especially want the bus factor on ui-navigation to increase over 1 if possible) Is this fine for you?

@Weibye
Copy link

Weibye commented Jul 30, 2022

On second though, I think the tab cycling feature should wait. I'll add it to the "future" section. The PR is massive as it is, and I don't want to pile on functionalities. It's already going to be difficult to review as-is.

There is a few features that I decided to delegate to future endeavor. What I will do is that if the ui navigation PR is merged, I'll immediately open an issue for each functionalities I left behind (and a tracking issue on top). This also allows more piecemeal PRs that are better for people to familiarize themselves with the ui-navigation code (I especially want the bus factor on ui-navigation to increase over 1 if possible) Is this fine for you?

Yup, perfectly fine :) And this should probably be our preferred approach when dealing with large features like this.

(I especially want the bus factor on ui-navigation to increase over 1 if possible)

Could you elaborate what you mean by this?

@nicopap
Copy link
Author

nicopap commented Jul 30, 2022

I currently am the only person who really understands the code for ui navigation. I hope I wrote enough (internal) docs + this RFC for other people to be able to understand and contribute, but at lest for a short period I'll probably the only one confident enough to modify the navigation code. I want this to be as short a period as possible.

Comment on lines +825 to +827
The design of this RFC _hinges on_ the whole tree been loaded in ECS while
executing navigation requests, and would work poorly if menus were spawned and
despawned dynamically.
Copy link

Choose a reason for hiding this comment

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

My mind keeps coming back to this statement. It is an extremely common use-case to not know the data ahead of time: I want to select a villager from a list. The game queries the list of villagers currently available, and spawns buttons for them so that I can click on one. Or just a search bar in an application.

Does this mean that we cannot support those use cases at all?

Copy link
Member

Choose a reason for hiding this comment

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

Agreed, now that you highlight this I share your concerns.

Copy link
Author

Choose a reason for hiding this comment

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

So. To be precise, this section is really meant to contrast with the pattern of spawning and despawning each menu as they are created.

The limitation here (I've not tested it) I think is only when despawning the parent menu (for example despawning the main menu when entering the options menu), not when creating dynamically new menus you can enter, or despawning menus that do not have focus.

When you go back one level of menus (pressing backspace or clicking a "back" button), the navigation algo reads the Entity of the button the menu is "reachable from" (aka the button that activated the menu), and if that Entity is despawned, probably bad things happen (panic! most likely), as we now sort of "dereference a dangling Entity".

However, the reverse case of creating new menus at runtime is widely supported, the MenuBuilder::ParentEntity enum variant should help with runtime spawning of new menus, since you can just give it whatever Entity with a Focusable component you have available. It does mean however that the submenus must be created before the button is activated (for example when it gets focused/hovered).

It is not necessary for the whole tree to be loaded at once, but rather than the path back to the root menu is. For support for giving back focus to the last focused element (prioritized) you would also need to keep the Focusables and menu tree of already-explored menus loaded. But typically, for a dropdown, this isn't something people expect.

This is why I mentioned an "infinitely deep recursive menu" as an edge case ui-navigation couldn't support.

I'm currently making a sort of "infinite weapon upgrade menu" as a cool example to demonstrate how easy it is to use your own navigation methods (using Sprite and Text2d) and generating new menus on the fly.

Copy link
Author

Choose a reason for hiding this comment

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

When it comes to creating new Focusables in an existing menu, it should be alright. But it's always going to be difficult when removing them (beside the mandatory minimum 1 Focusable in a menu). TreeMenu has a active_child field for avoiding iterating all children, but it completely violates the single-source-of-truth principle which makes despawning Focusables dangerous (you must not despawn a focused or active focusable)

I'll probably have to rework access to the active_child to fallback to the ECS hierarchy when it doesn't exist anymore.

Copy link

Choose a reason for hiding this comment

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

That is good to know, and still impressive work with all of this. Is the last remaining hurdle then to find a good way to remove leaf nodes in the navigation tree dynamically?

The requirement that the path back to root must exist I think is acceptable as long as we can dynamically remove leaf nodes

@nicopap
Copy link
Author

nicopap commented Jan 19, 2023

This needs to be split into three different changes

  1. Event-based interactions
  2. simple gamepad-based UI controls
  3. menu navigation.

It's simply not feasible to integrate such a huge chunk of changes in bevy without more fine grained reviews.

I'll close this RFC. I've no short term plans on opening the 3 RFCs the split would require.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
Status: Done
Development

Successfully merging this pull request may close these issues.

6 participants