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

Add Display::Content layout type #9731

Closed
viridia opened this issue Sep 9, 2023 · 5 comments
Closed

Add Display::Content layout type #9731

viridia opened this issue Sep 9, 2023 · 5 comments
Labels
A-UI Graphical user interfaces, styles, layouts, and widgets C-Enhancement A new feature

Comments

@viridia
Copy link
Contributor

viridia commented Sep 9, 2023

What problem does this solve or what need does it fill?

As we move towards a more reactive UI framework, there is a question of how to support conditional rendering and "foreach" nodes.

By conditional rendering, I mean a subtree of the UI that either exists or does not exist based on some condition, such as a modal dialog, a game mode, a tab panel and so on. Generally speaking, when a UI mode is not visible, you don't want to simply "hide" the elements, you want them to not exist at all - because the existence of these nodes often has side-effects beyond simply their impact on the layout.

Similarly, it is often the case that you'll have an array of items that are rendered from some data source that is an iterator, such as a list of saved games.

Immediate-mode frameworks have no problem with either of these patterns. However, a UI that retains elements wants to preserve the node tree where possible, otherwise you lose state (things like cursor position, text selection, focus and accessibility cues). This means that we want to avoid re-generating nodes when they have not changed. There are various strategies for this: the "React" approach is to generate a new list of child entities and compare that list with the old one; The "Solid" approach is to remember each child's index relative to its parent, and then "patch" the list as needed.

However, conditional and iterative UI elements are a challenge because they change the size of the child entity list, and alter all of the indices, making both comparing and patching difficult. For example, if I have a UI node whose children consist of a list of saved games followed by a "save" button, then the list index of that save button is going to change when additional save games are added. This is a challenge for both the React and Solid approaches.

For conditional nodes, a dummy "placeholder" node can be used to take up a child slot when the child is not being rendered. However, you then need to inform the layout engine to ignore this node in layout calculations. For lists, it's more complex, because a simple placeholder won't work.

What solution would you like?

The solution to all these problems is Display::Content. This is an idea that is relatively new in CSS, and is not supported in all browsers yet, but makes sense in the context of Bevy UI. The basic idea is simple: a node with Display::Content is handled by the layout engine as if it were replaced by its children. That is, the children of the element are hoisted up one level by the layout engine and rendered in place of that element. If the element with Display::Content has no children, then it is simply ignored.

For conditional rendering, we create a placeholder node that represents the condition itself, and its single child is actually the conditionally-rendered content (or no child if the condition is not enabled). For iterative rendering, the placeholder node has a variable number of children depending on the number of elements in the array. In both cases, it means we can re-evaluate and re-generate the children of the conditional node, without altering the list indices of any of its siblings.

What alternative(s) have you considered?

The alternative (that I can think of) is to keep track of child node indices as a separate data structure, but this requires a lot more bookkeeping.

@viridia viridia added C-Enhancement A new feature S-Needs-Triage This issue needs to be labelled labels Sep 9, 2023
@ickshonpe
Copy link
Contributor

ickshonpe commented Sep 9, 2023

Simple example of why this is useful (if I understand it right):

We implement a game menu:

menu

We save a game.

(I realise now that this menu doesn't really make sense, just suppose that pressing "New Game" creates a save file)

This adds a new "Load Save 3" button to the menu:

menu-3

The menu's implementation inserts the new child at an index calculated from the previous number of "Load Save" buttons plus the number of other buttons before the "Load Save" buttons in the list of children.

This is fragile and complicated, so we wrap the "Load Save" buttons inside a single child node instead:

menu-4

Now we don't have to worry about indices. Instead, the "Load Save" button nodes are added as children to child 1.

This is okay but now we're bothered about the empty space at the bottom of the menu.

We decide that we want to divide the space up evenly inbetween the buttons, so we set AlignItems::SpaceBetween on the parent node. But:

menu-5

child 1 is considered a single item by the layout algorithm so space isn't added between the "Load Save" buttons.

However, if we had a Display::Contents style property then we could set it on child 1. Then child 1's children would be hoisted up the hierarchy and arranged as though they are children of the root parent menu node:

menu-6

@ickshonpe
Copy link
Contributor

ickshonpe commented Sep 9, 2023

I'm not 100% sure but I think I could implement this in Bevy without too much difficulty or requiring Taffy support. Style updates would need to be modified to walk the layout tree instead of iterating through a style query maybe.

@viridia
Copy link
Contributor Author

viridia commented Sep 9, 2023

Your analysis is 100% correct.

I realize that the example of a save menu is a bit artificial, but you do find this pattern in React/Solid code a lot where a list of items is rendered at the same level as other items.

Let me take the time to describe how you would handle these cases without implementing Display::Content: Basically when you render the children of a UI node, you would build the same structure as described - that is, conditional nodes and iterative nodes would have placeholder parents. You then run a separate "flatten" step after rendering, and then set the actual children of the node to the flat array. However, you also keep around a copy of the non-flat array in a separate component, and when you are diffing / patching you use that non-flat array as your base of reference.

Before taking this on, I would suggest checking to make sure that this is something that the new UI framework is actually going to use.

@JMS55 JMS55 added A-UI Graphical user interfaces, styles, layouts, and widgets and removed S-Needs-Triage This issue needs to be labelled labels Sep 10, 2023
@viridia
Copy link
Contributor Author

viridia commented Sep 12, 2023

So, I'm surprised and pleased that you actually implemented this so quickly. I have a few more notes.

  1. Another use case for this is to implement something like React.Fragment.

  2. I've been forging ahead with my own work on UI, and it turns out that the alternative approach is not as hard as I expected. The way this works is that view components which render an element tree return an enum that looks like this:

#[derive(Debug, PartialEq, Clone)]
pub(crate) enum TemplateOutput {
    // Means that nothing was rendered. This can represent either an initial state
    // before the first render, or a conditional render operation.
    Empty,

    // Template rendered a single node
    Node(Entity),

    // Template rendered a fragment or a list of nodes.
    Fragment(Box<[TemplateOutput]>),
}

impl TemplateOutput {
    /// Flattens the list of entities into a vector.
    fn flatten(&self, out: &mut Vec<Entity>) { ... }
}

Once a view element has called the render function of its children, it has a vector of TemplateOutput objects. We can then call flatten to obtain a flat vector of UiNodes entity ids. This list of entities is then passed to .replace_children().

So the question for me now is whether to keep my workaround, or adopt the Display::Contents solution. (Note that Display::Contents has additional use cases beyond flattening template output, so it's not wasted work either way.)

@viridia
Copy link
Contributor Author

viridia commented Nov 28, 2023

So, as I've continued in my UI research, I've determined that I no longer need this feature; this problem can be solved at a higher level. So if you want to close this, feel free.

@viridia viridia closed this as not planned Won't fix, can't repro, duplicate, stale Dec 5, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-UI Graphical user interfaces, styles, layouts, and widgets C-Enhancement A new feature
Projects
None yet
Development

No branches or pull requests

3 participants