DioxusLabs / taffy

A high performance rust-powered UI layout library

Home Page:https://docs.rs/taffy

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Support multiple layout algorithms

alice-i-cecile opened this issue · comments

Tracking Issues:

  • Add feature flag for Flexbox: #298
  • CSS Grid: #204
  • Morphorm: #308

This library will be dramatically more useful if it can be used to support multiple layout strategies. This is useful because:

  • it centralizes community effort
  • it enables fair benchmarks
  • it lets users easily swap out approaches
  • it creates the potential for blended layouts in the same app

Must have:

  • each layout algorithm lives behind its own feature flag
  • this crate provides a relatively unified interface to them
    • a trait seems like the natural fit here

Nice to have:

  • low or zero abstraction overhead
  • the ability to nest multiple distinct layout strategies, ala flutter
  • competitive benchmarks between the different strategies
  • rosetta-stone comparisons of practical examples, demonstrating how to create the same / comparable layouts in different paradigms
  • users can create their own layout algorithms using our primitives in an interoperable way

Possible layout algorithms we may want to support:

  • hstack / vstack
  • Swift-UI style
  • Flutter style
  • CSS grid
  • Morphorm
  • Cassowary

I have some ideas for this, encoded in my now-dead attempt at a layouting engine. (The name might sound familiar; I never made this public till just now because it was never able to do much of anything.) The way it supported multiple layouts was by making a distinction between container nodes and item (aka leaf) nodes.

Nodes have a method that can convert itself into a container, and that method decides which layouting algorithm its items will be subject to. So for example, you'd have a flex method, a grid method, and so on. Actual code example here. Items are added to a &mut container type returned by the flex/grid/whatever method that are defined like this, for example, which keeps the interface pretty uniform for all layout algorithms.

Here's what creating an item looks like using this API, and here's creating a container. There are some other examples in files in the view folder in that repo as well. This API was mainly designed with the Elm architecture in mind (since iced uses that to great success and I was trying to copy the developer ergonomics it provides).

Nested layouts: Since the layouting method is decided at layout-time, you can easily (from the user's perspective, anyway) support nested distinct layouts.

Feature flags: It should be possible to put these methods and their configuration types behind feature flags.

Unified interface: The interface would be unified via the return type from the methods like Node::flex, we could theoretically consume the original crate's configuration through arguments to those methods and return a custom type that can receive that container's children.

Custom algorithms: I haven't considered this at all but as a quick guess I'm imagining one could implement a method like Node::custom_layout<T, U>(&mut self, layout: U, solver: /* some sort of callback capable of processing T with U options*/) -> &mut T where T would be like flex::Children from the code linked above, which would then be used to add children, and U which is the configuration of the container itself.

Performance: I have absolutely no idea since I never actually got any of this working to a point where it's worth benchmarking 😅 .

That's very promising: I think the Container vs. Leaf distinction is quite natural, and should transfer well across algorithms.

The Hierarchy and [Node]) traits by @geom3trik from Morphorm looks like interesting prior art too.

The Hierarchy and [Node]) traits by @geom3trik from Morphorm looks like interesting prior art too.

I realised recently that a better approach is to just have the Node trait but with some associated types for Store, which represents the case where node properties are not owned by the node, and for Tree, which represents the case where the hierarchy is stored externally.

Then there's just a method on the node trait for returning an iterator on the children, and any layout algorithm can be described recursively.

The Hierarchy trait before required that the tree be described separately. This new approach allows for both the case where nodes own their properties and children, and also the case where they are stored externally.

The Hierarchy and [Node]) traits by @geom3trik from Morphorm looks like interesting prior art too.

Ah; at first I was confused what purpose a trait for nodes would serve, that example helped me understand. That seems to be using an ECS architecture, and a trait makes sense for that, since all the objects are stored "flatly".

My project organized things hierarchically with enums, so naturally I also used an enum for that: see the definition, the use in Node, and the use in the actual solver. For custom algorithm support, a Custom variant with some generics would probably do the trick.

I guess it depends on whether this project wants to do ECS, something like what I've come up with, or something else entirely. (I don't have any real preference for any particular approach since I haven't really explored the alternatives.)

Ah; at first I was confused what purpose a trait for nodes would serve, that example helped me understand. That seems to be using an ECS architecture, and a trait makes sense for that, since all the objects are stored "flatly".

That is definitely true for the Hierarchy trait. However, as I mentioned above, my new approach that I'm working on just has a trait for nodes which works for both an ECS approach and for where nodes own their children. I think a trait-based approach like this could be used to abstract an interface for different algorithms.

Using enums to express the tree isn't something I had thought of, but I guess that only really makes sense if you want to force a distinction between containers and non-containers. In morphorm, any node can contain children and then it's up to the user to design an API where it's possible or not to add those children to a node.

Possible layout algorithms we may want to support:

hstack / vstack
Swift-style
CSS grid
Morphorm
Cassowary

Any specific ordering or prioritization for these? I might tackle one if I have some time.

CSS grid is probably the highest priority due to its popularity. After that, my personal favorite is Morphorm, then Cassowary.

Once those three are in we can see what sort of gaps are still left; I don't think there's a lot of need to support very similar APIs.

EDIT: the other dead simple "layout algorithm" I want is the identity layout algorithm. Users just pass in absolute positionings and we return them. This is both a useful escape hatch and serves as a great test ground for the broader architecture.

Could it support a Flutter-style (one pass) layout algorithm? Or is that covered by one of the others? I refer to: https://medium.com/snapp-mobile/flutter-anatomy-layout-internals-part-1-de99b772ab99

CSS grid also works in combination with Flexbox, I use the gap properly quite often with Flexbox during web development.
I wonder how we can make some algorithms work in combination and some not, where it makes sense.

Mhmm. I think we'll want a few diverse layout algorithms in place first, then start experimenting with patterns there.

If we need to define pairwise compatibility rules, we can use a generic trait that takes the other layout algorithm, ala From.

Regarding leaf nodes, would it make sense for them to be "just another layout type"? So you end up with:

enum Display {
     Flexbox,
     CSSGrid,
     Leaf,
     // More layout algorithms here...
}

where the Leaf layout type works similarly to MeasureFunc in the current codebase which takes a bunch of context (we may want to include more than are currently passed in - such as min/max constraints) as parameters (which may used or ignored as the consumer of the library sees fit) and returns a width/height (the mechanism behind the calculation being out of the scope of this library).

Leaf could also potentially be called Custom, as I believe it would also serve as an escape hatch for integrating additional layout algorithms with Taffy.

Oh that's a cool idea. The idea of "Leaf" as a layout algorithm is pretty appealing 🤔 I'm a bit nervous to use an enum rather than a trait here: like you say, I want an escape hatch for custom layouts if we can get away with it.

More relevant conversation #205 (comment)