DioxusLabs / DioxusLabs/taffy

Support Morphorm/Subform layout

Open
#308 7 comments 9 reactions 0 assignees View on GitHub
controversial
Dominant language
Rust
Stars
3.6k
Forks
226
Avg merge
10h 41m
Merged PRs (30d)
40

Description

Now that CSS Grid support is nearing completion, I thought I'd take a look at what it would take to support Morphorm layout in Taffy (CC: @geom3trik). My findings are summarised below:

## Morphorm Styles

### Unique Types

`LayoutType` (corresponds to `Display` in Taffy):
```rust
enum LayoutType {
Row,
Column,
Grid,
}
```

`PositionType` (corresponds to `Position` in Taffy):
```rust
pub enum PositionType {
SelfDirected, // = Position::Absolute
ParentDirected, // = Position::Relative
}
```

`Units` (corresponds to `Dimension` in Taffy):
```rust
pub enum Units {
Pixels(f32), // = Dimension::Points
Percentage(f32), // = Dimension::Percent
Auto, // = Dimension::Auto
Stretch(f32), // Fill available space in proportion to argument value. No equivalent in Taffy!
}
```

### Style Properties

Using these types (mostly `Units`), Morphorm has the following style properties. Note:

- All properties are optional (their type is wrapped in Option). But I have omitted that below because it was noisy.
- Not all properties that use `Units` actually support all variants, for example `border` (and I think also `min_size` and `max_size`) only really supports "LengthPercentage" (Auto and Stretch resolve to zero).

| Property | Type | Taffy Equiv. | Description |
| --- | --- | --- | --- |
| **Layout Mode** | | | |
| `layout_mode` | `LayoutType` | `flex_direction` | Row vs. Column vs. Grid |
| `position_type` | `Position` | `position` | SelfDirected (absolute) vs. ParentDirected (in-flow) position |
| **Item size** | | | |
| `size` | `Size` | `size` | The preferred height and width of item |
| `min_size` | `Size` | `min_size` | The minimum height and width of the item |
| `max_size` | `Size` | `max_size` | The maximum height and width of the item |
| **Border** | | | |
| `border` | `Rect` | `border` | How large should the border be on each side? |
| **Morphorm Container** | | | |
| `child_spacing` | `Rect` | `padding` | Sets the default "spacing" (~margin) on each side of child nodes |
| `row_between` | `Units` | `gap.height` | Sets the default vertical "spacing" (~margin) between child nodes |
| `col_between` | `Units` | `gap.width` | Sets the default horizontal "spacing" (~margin) between child nodes |
| `grid_rows` | `Vec` | `grid_template_rows` | (Grid Container) Row definitions with a size for each row |
| `grid_cols` | `Vec` | `grid_tempalte_columns` | (Grid Container) Column definitions with a size for each column |
| **Morphorm Item** | | | |
| `spacing` | `Rect` | `margin` | The preferred spacing on each side of the item |
| `min_spacing` | `Rect` | - | The minimum spacing on each side of the item |
| `max_spacing` | `Rect` | - | The maximum spacing on each side of the item |
| `row_index` | `usize` | `grid_row.start` | (Grid Item) Zero-based index for the start row of the item |
| `col_index` | `usize` | `grid_column.start` | (Grid Item) Zero-based index for the start column of the item |
| `row_span` | `usize` | `grid_row.end` | (Grid Item) The number of rows the item spans |
| `col_span` | `usize` | `grid_column.end` | (Grid Item) The number of columns the item spans |

### Analysis
Nearly everything in Morphorm is simplified version of either Flexbox or CSS Grid. There are really three things that aren't:
- The `child_spacing` and `*_between` properties. CSS does not allow you to specify child margins on the parent like this. It does have the `align-items` and `justify-items` properties, but those are not as powerful.
- The `min_spacing` and `max_spacing` properties. CSS does not allow you to specify min or max values for margins.
- The `Stretch` variant of `Units`. Flexbox/CSS Grid do not allow you to express "fill available space" as a size like this. To achieve this with Flexbox/CSS Grid you need to use special properties that are specified separately.

The only really tricky thing here is the `width`/`height` properties. They are common to all algorithms, and furthermore by *both* parent and child nodes access this property. This means that the type of these properties really needs to be a single unified type. I thought this might be a blocker, however I have discovered that an upcoming CSS standard ([css-sizing-4](https://www.w3.org/TR/css-sizing-4)) actually does [define this functionality](https://www.w3.org/TR/css-sizing-4/#stretch-fit-sizing) and in fact even calls it `stretch`. The CSS version doesn't have the weighting parameter (although I have [opened an issue](https://github.com/w3c/csswg-drafts/issues/8267) proposing that it does - no idea if that's the right place to do that though) so it is roughly equivalent to `Stretch(1.0)`, but that would a pretty straightforward extension.

## Algorithm and Code Structure Differences

Morphorm is relative small and simple compared to Taffy's layout algorithms (yay!). Morphorm's `layout.rs` which contains the core implementation for both it's Grid and Row/Column algorithms is just over 1000 LoC. For comparison, Taffy's Flexbox implementation alone is around ~1700 LoC, and the CSS Grid implementation almost double that.

Morphorm uses a 3 phased approach to layout:

1. It computes the first and last child of every node in the tree (Note: I'm not quite sure why is necessary yet)
2. Working bottom-to-top, it computes the content size of each node
3. Working top-to-bottom it works down the tree and does final sizing, alignment and positioning for each node. This last step differs based on whether the node is a Grid node or a Row/Column node.

In contrast, Taffy uses a single phase approach which essentially corresponds to Phase 3 of Morphorm's approach. It also does things equivalent to Morphorm's Phase 2 (probably Phase 1 as well), but it does this lazily on demand.

Morphorm has fairly large/extensive persistent `Cache` object (See the [trait](https://github.com/vizia/morphorm/blob/main/src/cache.rs) and the [reference implementation](https://github.com/vizia/morphorm/blob/main/ecs/src/implementations.rs#L276)) which I believe it uses to store both intermediate computations and the output of it's layout. In contrast, Taffy stores it's intermediate computations locally within the computation function(s) and throws them away once it's done computing the size for a node. I have a feeling that the easiest way to get Morphorm to work with Taffy might be to make it work more like Taffy's other algorithms do and keep more of it's intermediate results internal: IMO keeping API surface small is going to be key to making this project feasible.

Morphorm's layout algorithms themselves obviously have differences (that being the whole point of supporting it!), but I don't think any of that (other than the already discussed `Stretch` variant of `Units`) is particularly relevant from an integration point of view. Taffy is already setup to let each container node have free reign over their children, so it should be straightforward to slip Morphorm's algorithms into that model.

## Implementation Proposal

My suggestion based on looking into this is that we don't attempt to use the `morphorm` crate as a dependency, and instead port the Morphorm code/algorithm into Taffy directly (but I'd definitely be interested to hear @geom3trik's opinion on this). This is based on the following:

- The Morphorm algorithm is fairly small/simple, and we have an existing standalone implementation (in Rust!) to copy, so porting it should be relatively straight forward.
- I think details of the existing implementation will make it difficult to depend on use it as a dependency: it has a different style system, it has a component-per-style system for it's storage, it requires a large interface for it's cache storage, it has different traits for implementors, etc.
- I think the algorithm code will require fairly extensive refactoring (making it run in a single pass, lazily sizing children) to integrate with other layout algorithms anyway.

More specifically, I would propose the following implementation plan:

1. Add a `Stretch(f32)` variant to Taffy's existing style system. Making it available on the `width`/`height` properties and implementing this variant for Flexbox/CSS Grid according to https://www.w3.org/TR/css-sizing-4/#stretch-fit-sizing (except with added support for weighting), and *maybe* on the `margin` and/or `position` properties (the latter two could be done later though).

2. Create a `morphorm` feature flag.

3. Add Morphorm style properties to `Style` (behind the feature flag). This will also involve working how to alter the `Display` type (e.g. do we add `MorphormRow`/`MorphormColumn`/`MorphormGrid` separately, or do we have a single `Morphorm` variant and sub-differentiate separately?)

4. Implement the Morphorm layout algorithms themselves, refactoring them to fit Taffy's structure as they are ported.

5. Consider improving Taffy's extension trait system (i.e. `LayoutTree` and `MeasureFunc`) with inspiration from Morphorm's traits (`Node`, `Hierarchy`, `Cache`). Possibly using the approach of "just having a Node trait" as proposed here https://github.com/DioxusLabs/taffy/issues/28#issuecomment-1124355022

6. Implement a fracturing of the `Style` into smaller pieces similar to the one Bevy UI is about to implement.

---

I'd love to know others' thoughts, but I'm quite encourage by what I'm seen: this actually looks quite achievable to me!

P.S. I also did some preliminary research on Flutter and Swift UI's layout systems, but I think that to a large degree their systems amount to being layout-agnostic, with each Component being able to implement arbitrary layout algorithms. So I think support for those systems will look more like "an extension point that allows custom algorithm" than an implementation of specific algorithm(s).

Contributor guide

Open the contributing guide

Research direction

Start by reviewing Morphorm's layout.rs, cache.rs, and the referenced cache implementation, then compare them with Taffy's Style, LayoutTree, and MeasureFunc structures. The issue proposes a feature flag, new style properties, Stretch sizing, and porting the layout algorithms; done would require an agreed design and a complete integrated implementation with suitable coverage.

Written by the indexing model from the issue text.

Assessment

Tech stack
css, rust
Domain
frontend
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
25/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.