Skip to content

Support Morphorm/Subform layout #308

@nicoburns

Description

@nicoburns

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):

enum LayoutType {
    Row,
    Column,
    Grid,
}

PositionType (corresponds to Position in Taffy):

pub enum PositionType {
    SelfDirected, // = Position::Absolute
    ParentDirected, // = Position::Relative
}

Units (corresponds to Dimension in Taffy):

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<Units> size The preferred height and width of item
min_size Size<Units> min_size The minimum height and width of the item
max_size Size<Units> max_size The maximum height and width of the item
Border
border Rect<Units> border How large should the border be on each side?
Morphorm Container
child_spacing Rect<Units> 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<Units> grid_template_rows (Grid Container) Row definitions with a size for each row
grid_cols Vec<Units> grid_tempalte_columns (Grid Container) Column definitions with a size for each column
Morphorm Item
spacing Rect<Units> margin The preferred spacing on each side of the item
min_spacing Rect<Units> - The minimum spacing on each side of the item
max_spacing Rect<Units> - 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) actually does define this functionality and in fact even calls it stretch. The CSS version doesn't have the weighting parameter (although I have opened an issue 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 and the reference implementation) 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 Support multiple layout algorithms #28 (comment)

  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).

Metadata

Metadata

Assignees

No one assigned

    Labels

    controversialThis work requires a heightened standard of review due to implementation or design complexity

    Projects

    Status

    Todo

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions