Skip to content

Abstract flexbox over LayoutTree#202

Closed
jkelleyrtp wants to merge 475 commits intomainfrom
jk/abstract-over-storage
Closed

Abstract flexbox over LayoutTree#202
jkelleyrtp wants to merge 475 commits intomainfrom
jk/abstract-over-storage

Conversation

@jkelleyrtp
Copy link
Copy Markdown
Member

This PR makes the flexbox algorithm generic over a LayoutTree trait.

At the time of filing this PR, the LayoutTree trait mostly abstracts over the broadest set of functions that the Taffy struct satisfies. I don't know or think this is the best and most general solution to flexboxing: Taffy internally does caching of calculations that a user's LayoutTree shouldn't really be concerned about. Perhaps some of the state - like caching - could remain in a Taffy struct, but this level of specificity intrinsically ties a user's UI system to Taffy due to dependent data structures.

Generally, since Taffy uses its own types, most implementations will be tied to Taffy anyways, so perhaps this linking of user code to Taffy is not a big deal.

Also - this trait supplies a method that returns a FlexboxLayout (not the final layout, despite the confusing name) which might not be exactly a trait implementation would return the "current state" of the DOM Node. For instance, in Dioxus, we are using the CSS Cascade to supply the "current state" of the DOM node's style. I think before publishing out these changes, it might be worth further abstracting the trait to supply single-level Taffy attributes. Perhaps a "TaffyAttribute" enum could be supplied so use cases like Dioxus' cascading CSS could be implemented efficiently.

Closes #182

@alice-i-cecile alice-i-cecile added enhancement New feature or request code quality Make the code cleaner or prettier. controversial This work requires a heightened standard of review due to implementation or design complexity labels Jul 18, 2022
fn style(&self, node: Node) -> &FlexboxLayout;

/// Get the node's output "Final Layout"
fn layout(&self, node: Node) -> &Layout;
Copy link
Copy Markdown

@twop twop Jul 19, 2022

Choose a reason for hiding this comment

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

Unsolicited thoughts.

I was thinking about the change and would love to share some thoughts:

  1. If I understand correctly this approach aims to solve the fundamental decision with Taffy: who owns the notion of visual hierarchy? From my experience, you kinda have to have your own notion of visual hierarchy and then somehow keep your hierarchy and Taffy's in sync, this approach might help with that.
  2. As a side benefit of this approach you can have dynamic FlexboxLayout based on some external context (like in CSS). Although I'm curious if you can mark only the root node with mark_dirty or go through the entire subtree and call mark_dirty on each node recursively if you want to "cascade" new font size through the entire hierarchy (example: body {font-size: 14 -> 16}).
  3. Assuming that you want to implement your own LayoutTree, I wonder what kind of restrictions need to be applied to Node? It feels like SlotMap is very restrictive, but feels "right" for this use case, so I'm curious what a custom implementation might look like in regards to generating Nodes
  4. It feels to me that the methods in this trait can be roughly split into two categories: functions that deal with things that created outside of the library, and functions that deal with things that created inside of the library. Let me elaborate a bit, parent -> child relationships and FlexboxLayout are created and managed in user's code, but caching and Layout seem to be originated and used inside the library itself. So I wonder if this trait perhaps should be split in two: layout hierarchy and layout computation, note that "layout computation" can potentially be in the "core" Taffy and not a trait.
  5. As a general thought: this approach seems to introduce more cognitive complexity and I wonder if the benefits out-weight the cost for the broader audience. For example: for Dioxus use-case this trait saves X amount of memory and makes it Y% faster, but will these benefits to be as pronounced for the broader audience, if yes, what would it take to unleash the full potential?

Thanks for reading!

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Consider thoughts of this nature solicited by default! This is great feedback.

// for reference, CSS cascades require context, and storing a full flexbox layout for each node could be inefficient
//
/// Get the [`FlexboxLayout`] for this Node.
fn style(&self, node: Node) -> &FlexboxLayout;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I would like to suggest that FlexboxLayout is renamed to FlexboxStyle: the idea being to establish a naming convention that *Style is input and *Layout is output. FlexboxLayout is already in a file called style.rs and returned from a method called style, so this seems to improve naming consistency.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This makes a lot of sense, it is what it should have been all along.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I'm on board with that. IMO we should do it in a small seperate PR though.

Copy link
Copy Markdown

@bestouff bestouff Aug 23, 2022

Choose a reason for hiding this comment

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

I have thought a bit about this.

  1. CSS cascade is a bit on the complex side, but when you need it, you need it. That means that there shouldn't be 2 implementations of it: one for the Taffy properties (the flexbox data) and one for the rest of the UI properties (color, font, whatever). The former should be derived from the latter.
  2. A proper CSS implementation probably shouldn't store the full properties for each node but some kind of diff to its parent, to let the whole cascade (with selectors, etc.) work properly and not to take huge amount of memory.

So I like this style() method and I approve the FlexboxStyle name.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

and one for the rest of the UI properties (color, font, whatever).

This is out of scope for this library :) I'd like to keep this focused on pure layout, and let consumers (like bevy_ui or Dioxus) define their own tools for working with these sort of non-layout properties.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Yes it is, but my point is that the user will have to implement it anyway, so it probably should be reused; so having a style() method to ask the user how it resolved a particular Node's properties looks good to me.

@nicoburns
Copy link
Copy Markdown
Collaborator

nicoburns commented Jul 22, 2022

Is it worth thinking about how this will interact with supporting multiple layout algorithms? From some preliminary reading of the code, I believe we will probably want to split out the Taffy struct into something which simply holds data (i.e. something that implements LayoutTree here, and an entirely separate struct/module (possibly stateless) that implements the flexbox algorithm.

EDIT: I've been working with this code more closely, and I've realised that this PR more or less already implements this.

@geom3trik
Copy link
Copy Markdown
Collaborator

On the subject of supporting multiple layout algorithms, since I know that Morphorm is being looked at as a candidate, this isn't too far away from what Morphorm does with its Hierarchy trait. The main difference being that Morphorm requires the user to provide iterators over the whole tree while this approach simply queries nodes for their parent and children, which I think is much better. I'm going to see if I can adapt Morphorm to work in the same way. The tricky part for integrating multiple algorithms is going to be the mismatch between the style properties that each is using I think.

@alice-i-cecile
Copy link
Copy Markdown
Collaborator

The tricky part for integrating multiple algorithms is going to be the mismatch between the style properties that each is using I think.

Agreed. We should start with an MVP where we can't mix styles in the same tree. Then once that's built and we have a couple candidates, we can start looking to Flutter to figure out if there's a good interface by which we can let them talk.

/// Taffy caches results of computations for nodes to not need re-caculating nodes it already knows
///
/// When a node does not have a cache, Taffy will layout that node appropriately.
fn primary_cache(&mut self, node: Node) -> &mut Option<Cache>;
Copy link
Copy Markdown

@bestouff bestouff Aug 23, 2022

Choose a reason for hiding this comment

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

I do not understand how this cache works (when is it invalidated, etc.).

Nevermind I get it; all nodes have to contain the space for 2 Cache structs.

/// remains the same between re-layouts.
pub trait LayoutTree {
/// Get the list of children IDs for the given node
fn children(&self, node: Node) -> &[Node];
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Maybe here you restrict too much the type of the underlying children set (which now has to be a Vec or something like that) and it's be better to have something like

     fn children(&self, node: Node) -> impl Iterator<Item=Node>;

(which also has a trivial implementation). Now if the LayoutTree implementor uses e.g. a BTreeMap as the underlying storage it still can trivially implement children().

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Definitely agree with this :) This sort of flexibility is really nice for public traits.

We should probably use IntoIterator not Iterator though; as that's the trait that's implemented directly on collections.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Wouldn't IntoIterator consume the collection (into_iter() takes self by value) ? I think we just want using iter() here.

@bestouff
Copy link
Copy Markdown

[...] I know that Morphorm is being looked at as a candidate [...]

You made me have a look at it. It's really cool (even if the API is a bit clunky at times) but in my small test it doesn't implement wrapping; wouldn't that be required for Taffy to use it ?

@bestouff
Copy link
Copy Markdown

Note for later: TaffyECS should probably be conditionally compiled, so somebody that only needs the LayoutTree trait could get rid of unneeded dependencies (e.g. slotmap). But this doesn't need to be in that PR.

@nicoburns
Copy link
Copy Markdown
Collaborator

[...] I know that Morphorm is being looked at as a candidate [...]

You made me have a look at it. It's really cool (even if the API is a bit clunky at times) but in my small test it doesn't implement wrapping; wouldn't that be required for Taffy to use it ?

I also looked into this recently, and it does seem cool. I think Taffy would use it as another alternative algorithm (e.g. a Display::Morphorm mode) rather than an implementation detail for the existing one. So lacking features wouldn't preclude it. It's earlier, but I think taffy aims to be somewhat of a "one stop shop" for layout.

Note for later: TaffyECS should probably be conditionally compiled, so somebody that only needs the LayoutTree trait could get rid of unneeded dependencies (e.g. slotmap). But this doesn't need to be in that PR.

This makes sense.

@nicoburns
Copy link
Copy Markdown
Collaborator

@jkelleyrtp What's the status of this PR? I've been basing my work off of it, and I'm concerned that it's now drifting further and further from main over time. My own take is that the changes in this PR are broadly good, and that while we might want to further tweak the LayoutTree trait abstraction, it might be best to get this merged and iterate on it later.

(side note: I believe that the recent force-push to main has broken existing pull requests and that they will need to be rebased on top of the latest main).

@alice-i-cecile
Copy link
Copy Markdown
Collaborator

My own take is that the changes in this PR are broadly good, and that while we might want to further tweak the LayoutTree trait abstraction, it might be best to get this merged and iterate on it later.

This is my position as well :) I'd like to avoid long-lived branches, especially for foundational data structures.

@geom3trik
Copy link
Copy Markdown
Collaborator

You made me have a look at it. It's really cool (even if the API is a bit clunky at times) but in my small test it doesn't implement wrapping; wouldn't that be required for Taffy to use it ?

Somehow missed this before. You're right that Morphorm doesn't currently support wrapping but it's something I'm planning to add for a rewrite that I'm working on at the moment (there's already a working but not very well tested implementation).

Once I've finished this rewrite then I think Morphorm will be in a better position to be integrated with taffy in some way if that's still desired.

@alice-i-cecile
Copy link
Copy Markdown
Collaborator

Once I've finished this rewrite then I think Morphorm will be in a better position to be integrated with taffy in some way if that's still desired.

It is!

@nicoburns
Copy link
Copy Markdown
Collaborator

Closing as this work was merged via a rebased version of this branch (#238)

@nicoburns nicoburns closed this Oct 25, 2022
@nicoburns nicoburns deleted the jk/abstract-over-storage branch February 8, 2023 14:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

code quality Make the code cleaner or prettier. controversial This work requires a heightened standard of review due to implementation or design complexity enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Introduce some tree abstraction