Support multiple layout algorithms
alice-i-cecile opened this issue · comments
Tracking Issues:
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 enum
s, 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
More relevant conversation #205 (comment)