Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support Morphorm/Subform layout #308

Open
Tracked by #345
nicoburns opened this issue Dec 30, 2022 · 7 comments
Open
Tracked by #345

Support Morphorm/Subform layout #308

nicoburns opened this issue Dec 30, 2022 · 7 comments
Labels
controversial This work requires a heightened standard of review due to implementation or design complexity enhancement New feature or request

Comments

@nicoburns
Copy link
Collaborator

nicoburns commented Dec 30, 2022

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

@nicoburns nicoburns added the enhancement New feature or request label Dec 30, 2022
@nicoburns nicoburns added the controversial This work requires a heightened standard of review due to implementation or design complexity label Dec 30, 2022
@nicoburns nicoburns added this to the 0.4 milestone Dec 30, 2022
@geom3trik
Copy link
Collaborator

geom3trik commented Dec 30, 2022

Wow, that's a very comprehensive summary of Morphorm! So I think there's some things I should point out that might be helpful.

  • child_space is closer to padding than it is to margin, but the way it actuall works is to override the spacing properties on the children if those properties are set to Auto.
  • The first pass of the algorithm is used to both find the first/last child and also to reset the cache. The reason you need to predetermine the first/last is because the nodes with PositionType::SelfDirected don't count towards being the first/last in a stack, and child_spacing will apply to those nodes regardless (if their spacing is auto).
  • You're right that some properties like border don't use all the variants of Units. Probably when you port you can use LengthPercentage, I just used Units to keep things simple.
  • The grid implementation is very simple and you might want to consider whether it's worth porting that part or not. It's nice to have the stretch units + grid but I suspect there are a lot of missing features.
  • You've probably already noticed that morphorm doesn't support wrapping.
  • I think I should talk a bit about what I'm calling content_size and how it's used in morphorm. There are two methods on node for content size, primary_content_size and secondary_content_size. The first is used when you want the node to 'hug' its content in the same axis, e.g. you want a label to be the width of its text content. The second one is used when you want the node to 'hug' its content in the off-axis, e.g. you want a multiline label/textbox to be the height of its text content. I suspect these can be conbined somehow.

I'm very curious to understand what you mean by 'lazily sizing children'?

Known Bugs

For the actual algorithm there are some bugs that you should be aware of. Mainly 2 things:

  • The secondary_content_size is only handled in phase 3 and is thus not properly propagated up the tree in phase 2, which results in things like containers not correctly sizing to accomodate a multiline textbox.
  • The min/max constraints aren't handled properly. I thought that sorting by min/max and then applying them would work but it doesn't. The correct solution, which I beleieve taffy does, is to loop over the stretch children and try to constrain them until all are constrained.

New Algorithm

So, now should mention that for a little while I've been working on a new and improved version of morphorm. It's not finished, but it already solves some of the problems you've identified. The code is on this branch: https://github.com/vizia/morphorm/tree/new-layout-no-wrap. The 'new' versions are files ending with a '2', e.g. layout2.rs. Sorry about the mess in the actual code, it's WIP.

The main difference in this branch is that the layout algorithm is now recursive instead of iterative. This means that phases 2 and 3 from before are combined into a single function. This eliminates the need for a Hierarchy trait and instead allows for a children method on the Node trait, which makes use of GATs to allow getting its children from an external tree (Tree associated type). Every method on Node, like before, has a parameter for an external store (Store associated type), which allows for both nodes that are just keys, and for nodes which contain their style properties.

The Cache is now also greatly simplified. Many interediate values are calculated on the fly (including first/last child now) so the cache only contains the size and position of a node, which are used as intermediates but are also the outputs of the algorithm. Also, the properties have changed to be direction agnostic. Instead of chld_left or child_top it's now child_main_before and child_cross_before and depends on the layout type of the parent. Not yet implemented but planned is RowReverse and ColumnReverse layout types.

As you'll see, the new algorithm isn't finished yet. I'd say it's about 80% there and even already fixes the content-size bug I mentioned before, but is still missing the min/max size handling, grid, and extensive testing. I'm also not sure I'm handling content_size correctly as I can imagine situations where circular dependencies pop up and I can't yet think of an ideal solution (other than to hope users don't do that). I do think though that this new version will be easier to port to taffy. I will of course help with the porting effort, and let me know if you've got any questions.

@nicoburns
Copy link
Collaborator Author

@geom3trik Thanks for pointing me to the new-layout-no-wrap branch. That definitely looks like a much better starting point for porting to Taffy (esp. the iterative -> recursive change and the simplified caching).

Having said that, it does concern me somewhat that this version has slightly different behaviour and user-facing API (e.g. renamed styles) to the existing version. I was under the impression that there was more of a standard being followed, but it seems that it's more of a case of "the implementation is the specification"? In which case I think we'll need really nail down exactly how the algorithm ought to behave in corner cases, and try to be careful not to introduce discrepancies between the implementation in the morphorm crate and the Taffy implementation.

I think that ideally we should:

  • EITHER release a new version of the morphorm crate in tandem with the Taffy release including morphorm, such that the behaviour and API are the same.
  • OR deprecate the morphorm crate in favour of Taffy once a version of Taffy with Morphorm support has been released.

On style property changes

Also, the properties have changed to be direction agnostic. Instead of chld_left or child_top it's now child_main_before and child_cross_before and depends on the layout type of the parent. Not yet implemented but planned is RowReverse and ColumnReverse layout types.

I wonder how you'd feel about reverting this for the time being? My reasons being:

  1. I'm keen for Taffy to offer a strict superset of the layout functionality in the morphorm crate, such that there is no tradeoff to be made between features in the morphorm and features in Taffy. If you switch to Taffy then you get the full Morphorm functionality (+ extra algorithms). I feel like having different styles is going to muddy the waters, and make the upgrade path more difficult.
  2. I'd rather separate out implementation changes from behaviour/API changes. I feel like it's going to be hard to verify correctness if changing both at once.
  3. I also can't help but feel like names like child_main_before and child_cross_before are going to be very confusing to end users (whereas child_left and child_top are much more intuitive). I mean, even for someone like me who's been knee deep in CSS specifications for last few months, I still have to think about which axis is which. Note also that CSS doesn't provide flex-direction relative margin or padding properties. Although I suppose spacing is kind of Morphorm's answer to alignment too, which does come in flex-direction relative variants.

Regarding Content Sizing

A note on terminology: also like to use "content size". But you find it useful to know that CSS tends to call this "intrinsic size" https://developer.mozilla.org/en-US/docs/Glossary/Intrinsic_Size

I'm very curious to understand what you mean by 'lazily sizing children'?
The main difference in this branch is that the layout algorithm is now recursive instead of iterative.

I think these two things are broadly the same. By lazily sizing children I mean that the content sizes of nodes aren't computed in advance. Instead we start layout computation at the top of the tree, and parent node query the size of their children (passing in any relevant constraints) as required. These children can in turn query the sizes of their children, recursing down the tree as necessary.

In Taffy 0.3, it's a mutually recursive algorithm rather than a simple recursion, as a child node may be sized using a different algorithm. #326 introduces a nice innovation in this model by adding a measure_child_size function to the LayoutTree trait, which means that embedders of Taffy can intercept any queries for child size and run arbitrary code with full access to their own state to determine the correct size (this could call straight back into Taffy, it could implement something like text layout, it could forward the call through some internal interface (and/or implement some other layout algorithm), perhaps later calling back into Taffy to size descendent nodes further down the tree).

There are two methods on node for content size, primary_content_size and secondary_content_size. The first is used when you want the node to 'hug' its content in the same axis, e.g. you want a label to be the width of its text content. The second one is used when you want the node to 'hug' its content in the off-axis, e.g. you want a multiline label/textbox to be the height of its text content. I suspect these can be conbined somehow.

Yes, Taffy simply has a single measure_child_size method that returns the size in both axes. It will generally only use the result in one axis at a time, but it simplifies the API.

Regarding the Node trait

Inspired by your Node trait (and also Druid's and Iced's Widget traits), I've started making a similar change to Taffy's LayoutTree trait (hasn't been renamed yet) over in #326.

It's slightly different to the Node trait in Morphorm in that you can't directly access a Node instance of a child. Instead you can access styles of direct children, and if you want to recurse down the tree you have to call either the measure_child_size or perform_child_layout methods. The reference implementations of these (if you don't implement LayoutTree yourself) simply create a new instance of LayoutTree representing the child and call back into the layout algorithm computation function with this instance, but it gives embedders of Taffy a lot of freedom as explained in the content sizing section above.

I'd love to get your thoughts / review on the design of this interface.

Regarding the Grid implementation

I think you might be right that the grid feature isn't worth porting. My reasoning is slightly different, which is that it just seems much more similar to CSS Grid than the Morphorm Row/Column are to Flexbox. Notably, CSS Grid already has stretch-like units in the form of the fr unit. I think the only thing CSS Grid doesn't have is stretch units for gaps between columns, but that seems like an uncommon use case to me.

Also, in terms of current state, the CSS Grid implementation is a lot more robust in edge cases and does things like automatically generate extra rows/columns if you place an item out of bounds. This could of course be implemented for Morphorm Grid, but I'm not sure if it's worth the effort given how similar they are.

Miscellaneous

You're right that some properties like border don't use all the variants of Units. Probably when you port you can use LengthPercentage, I just used Units to keep things simple.

Excellent. Taffy used to do the same, but we've recently switched to being strict. And I think we would indeed want to continue that for new algorithms. Higher level libraries would of course be free to provide a simpler interface if they wanted.

child_space is closer to padding than it is to margin, but the way it actually works is to override the spacing properties on the children if those properties are set to Auto.

I see what you mean in terms of which sides/spaces it affects, but I think the spacing property on children in morphorm is effectively the same as the margin property on children in CSS. So to "to override the spacing properties on the children" would be directly equivalent to overriding the margin property on those children (only on the relevant sides). Of course there is no way to control margins in this in CSS, and auto margins mean something else (they're like stretch!), but still.

You've probably already noticed that morphorm doesn't support wrapping.

I wonder if the best approach to this is to leave it that way, and if people want to use wrapping then they can use Flexbox instead? I guess there's nothing stopping us from taking that approach for now, and we can always implement wrapping in Morphorm later.

@geom3trik
Copy link
Collaborator

I was under the impression that there was more of a standard being followed, but it seems that it's more of a case of "the implementation is the specification"? In which case I think we'll need really nail down exactly how the algorithm ought to behave in corner cases, and try to be careful not to introduce discrepancies between the implementation in the morphorm crate and the Taffy implementation.

So the original implementation comes from an application called Subform which no longer exists. Someone on the bevy discord linked to this post https://subformapp.com/articles/why-not-flexbox/, which inspired me to implement from scratch the features shown in the marketing material on that site. Which is to say, as far as I know there is no specification and I did not find any code to use as reference.

I based the orignal algorithm on a once up/once down approach as described in a video I saw explaining the flutter layout algorithm, but using iterators instead of recursion (which as you know I've since changed). The original subform layout used direction-agnostic properties, which I changed to left, top, right, bottom, etc for ease of use. If it's helpful I can try and write some kind of specification when I can find the time.

I think that ideally we should:

  • EITHER release a new version of the morphorm crate in tandem with the Taffy release including morphorm, such that the behaviour and API are the same.
  • OR deprecate the morphorm crate in favour of Taffy once a version of Taffy with Morphorm support has been released.

This might be something easier to decide when some of the other parts have been figured out. I don't particularly like the idea of depreciating my own crate but I can see the advantages to having taffy handle flexbox, morphorm, and grid in one place.

On style property changes

Also, the properties have changed to be direction agnostic. Instead of chld_left or child_top it's now child_main_before and child_cross_before and depends on the layout type of the parent. Not yet implemented but planned is RowReverse and ColumnReverse layout types.

I wonder how you'd feel about reverting this for the time being? My reasons being:

  1. I'm keen for Taffy to offer a strict superset of the layout functionality in the morphorm crate, such that there is no tradeoff to be made between features in the morphorm and features in Taffy. If you switch to Taffy then you get the full Morphorm functionality (+ extra algorithms). I feel like having different styles is going to muddy the waters, and make the upgrade path more difficult.

I'm not sure I fully understand what you mean here. Are you meaning that you'd like for users to be able to use the same properties, like left etc. but then apply a different algorithm (flexbox or morphorm)? I think with the differences between flexbox and morphorm I'm not sure I see the issue with requiring different properties.

  1. I'd rather separate out implementation changes from behaviour/API changes. I feel like it's going to be hard to verify correctness if changing both at once.

We're talking about changes between taffy and morphorm here?

  1. I also can't help but feel like names like child_main_before and child_cross_before are going to be very confusing to end users (whereas child_left and child_top are much more intuitive).

I definitely agree with you here, but I'm concerned that not having the properties as direction agnostic would make it too restrictive if you want a layout that can adapt to direction changes. But I'm curious how this is currently handled in taffy? Because flexbox is also direction agnostic but margins/padding are not right?

I'm not against changing it back to the non-direction-agnostic properties though. Users of vizia are already accustomed to the left,right, etc. properties so it will be more work to provide internal conversions to direction agnostic ones anyway, I'm just concerned about how to best provide localization of layout.

Regarding Content Sizing

A note on terminology: also like to use "content size". But you find it useful to know that CSS tends to call this "intrinsic size" https://developer.mozilla.org/en-US/docs/Glossary/Intrinsic_Size

Ah, thanks for the info. I've seen that term used before but did not realise it was what I was calling content-size.

In Taffy 0.3, it's a mutually recursive algorithm rather than a simple recursion, as a child node may be sized using a different algorithm. #326 introduces a nice innovation in this model by adding a measure_child_size function to the LayoutTree trait, which means that embedders of Taffy can intercept any queries for child size and run arbitrary code with full access to their own state to determine the correct size (this could call straight back into Taffy, it could implement something like text layout, it could forward the call through some internal interface (and/or implement some other layout algorithm), perhaps later calling back into Taffy to size descendent nodes further down the tree).

This sounds like what I was trying to do with content_size on the Node. I'll have to take a closer look at this.

Regarding the Node trait

Inspired by your Node trait (and also Druid's and Iced's Widget traits), I've started making a similar change to Taffy's LayoutTree trait (hasn't been renamed yet) over in #326.

It's slightly different to the Node trait in Morphorm in that you can't directly access a Node instance of a child. Instead you can access styles of direct children, and if you want to recurse down the tree you have to call either the measure_child_size or perform_child_layout methods. The reference implementations of these (if you don't implement LayoutTree yourself) simply create a new instance of LayoutTree representing the child and call back into the layout algorithm computation function with this instance, but it gives embedders of Taffy a lot of freedom as explained in the content sizing section above.

I'd love to get your thoughts / review on the design of this interface.

This sounds intriguing and I will definitely check it out and give my thoughts. Something I haven't implemented yet with my current Node approach is a way to skip nodes. So performing layout on the root will recurse the entire tree. Your approach sounds like it has a solution to this.

Regarding the Grid implementation

I think you might be right that the grid feature isn't worth porting. My reasoning is slightly different, which is that it just seems much more similar to CSS Grid than the Morphorm Row/Column are to Flexbox. Notably, CSS Grid already has stretch-like units in the form of the fr unit. I think the only thing CSS Grid doesn't have is stretch units for gaps between columns, but that seems like an uncommon use case to me.

I think I would agree with you here. And if morphorm is eventually incorporated into taffy then taffy already has what looks like a very robust grid implementation. The only counter-argument I would have is that it would be nice for users using morphorm to be able to use the morphorm stretch type with grids, but that's subjective.

Miscellaneous

You've probably already noticed that morphorm doesn't support wrapping.

I wonder if the best approach to this is to leave it that way, and if people want to use wrapping then they can use Flexbox instead? I guess there's nothing stopping us from taking that approach for now, and we can always implement wrapping in Morphorm later.

I agree here too. I've found wrapping quite challenging to implement because of all the interdependencies between the axes of a node, particularly when trying to compute the stretch space and size combined with the auto size 'hugging' of the parent.

@nicoburns
Copy link
Collaborator Author

Which is to say, as far as I know there is no specification and I did not find any code to use as reference.

Yeah, I couldn't find a specification either. There is https://github.com/lynaghk/subform-layout as a reference implementation, but it's obfuscated and in any case not under an OSS licence.

The original subform layout used direction-agnostic properties, which I changed to left, top, right, bottom, etc for ease of use. If it's helpful I can try and write some kind of specification when I can find the time.

Oh interesting. A specification would be great, as would a test suite - preferrably in a declarative machine-parsable format (HTML/XML?) that we could parse to generate tests for Taffy. But I wouldn't consider either blocking.

I don't particularly like the idea of depreciating my own crate but I can see the advantages to having taffy handle flexbox, morphorm, and grid in one place.

Yeah, fair enough. I also don't think it would be fair to expect you to. You would obviously be welcome to come and maintain the implementation in Taffy. But I suspect it would make it harder to experiment and evolve to algorithm, and would come additional levels of scrutiny and review.

I'm not sure I fully understand what you mean here. Are you meaning that you'd like for users to be able to use the same properties, like left etc. but then apply a different algorithm (flexbox or morphorm)? I think with the differences between flexbox and morphorm I'm not sure I see the issue with requiring different properties.

No, I agree that separate properties seem reasonable (although some like width/height probably make sense to be the same). What I am wanting to be the same is the style properties in the morphorm crate and the style properties in the Taffy implementation of Morphorm (i.e. I want to avoid the situation where the morphorm has left, top, etc properties while the Taffy implementation of Morphorm has main_axis_before, etc, or vice versa).

Something I haven't implemented yet with my current Node approach is a way to skip nodes. So performing layout on the root will recurse the entire tree. Your approach sounds like it has a solution to this.

Taffy skips measuring a node's contents if that node's width/height property can be resolved to a fixed size, (e.g. it has a pixel size, or a percentage size and the container has a defined size in that axis) then that size will be used). It also caches measured sizes (and the inputs used to generate those), and will simply return the value from the cache if possible. Although in most cases, it does still need to recurse through every node in the tree at least once to "perform layout". Because even if we know their size, children still ultimately need to position their own descendants.

I definitely agree with you here, but I'm concerned that not having the properties as direction agnostic would make it too restrictive if you want a layout that can adapt to direction changes. But I'm curious how this is currently handled in taffy? Because flexbox is also direction agnostic but margins/padding are not right?

So, CSS doesn't define flex-direction relative padding/margins. The only thing which flex-direction affects are the flex-start and flex-end alignment properties, and the order in which the children are positioned. However, it does have relative padding/margin (and width, height and much more) properties that change relative to the direction property (which switches between left-to-right and right-to-left text directions), and the writing-mode property (which switches between horizontal and vertical text directions).

If it were up to me, I'd probably initially implement Morphorm in Taffy with "absolute" properties (left, right, etc), and then look at implementing direction, writing-mode, and relative styles for all algorithms later.

Out of interest, what is the use case for RowReverse/ColumnReverse? I don't think I've ever used these personally. Or are you more worried about switching between Row and Column?

@geom3trik
Copy link
Collaborator

Yeah, I couldn't find a specification either. There is https://github.com/lynaghk/subform-layout as a reference implementation, but it's obfuscated and in any case not under an OSS licence.

Ah yes I do remember finding that, but I chose not to use it because of the license. In the end the only thing I really took from the subform stuff is the concept of stretch space and size as well as the Auto variant of the units and how it works.

Oh interesting. A specification would be great, as would a test suite - preferrably in a declarative machine-parsable format (HTML/XML?) that we could parse to generate tests for Taffy. But I wouldn't consider either blocking.

I can write up a specification based partly on the morphorm implementation and partly on how I would like it to work. A test suite would be great, and I'm implementing some tests as I rewrite the algorithm but something auto-generated from a declarative format would be great!

No, I agree that separate properties seem reasonable (although some like width/height probably make sense to be the same). What I am wanting to be the same is the style properties in the morphorm crate and the style properties in the Taffy implementation of Morphorm (i.e. I want to avoid the situation where the morphorm has left, top, etc properties while the Taffy implementation of Morphorm has main_axis_before, etc, or vice versa).

Oh I see what you mean. Well hopefully a specification will solve this problem. Then both implementations (if it ends up being separate) can follow the same spec.

Taffy skips measuring a node's contents if that node's width/height property can be resolved to a fixed size, (e.g. it has a pixel size, or a percentage size and the container has a defined size in that axis) then that size will be used). It also caches measured sizes (and the inputs used to generate those), and will simply return the value from the cache if possible. Although in most cases, it does still need to recurse through every node in the tree at least once to "perform layout". Because even if we know their size, children still ultimately need to position their own descendants.

Right yeh, those sound like good optimisations.

So, CSS doesn't define flex-direction relative padding/margins. The only thing which flex-direction affects are the flex-start and flex-end alignment properties, and the order in which the children are positioned. However, it does have relative padding/margin (and width, height and much more) properties that change relative to the direction property (which switches between left-to-right and right-to-left text directions), and the writing-mode property (which switches between horizontal and vertical text directions).

If it were up to me, I'd probably initially implement Morphorm in Taffy with "absolute" properties (left, right, etc), and then look at implementing direction, writing-mode, and relative styles for all algorithms later.

Ah I was not aware of the writing-mode property, which seems to apply to both text and layout (and flexbox?). This is perhaps a more ergonomic option than the direction-agnostic properties. I think knowing this I would agree with your suggestion of using left, right, etc. and to add some form of direction property.

Out of interest, what is the use case for RowReverse/ColumnReverse? I don't think I've ever used these personally. Or are you more worried about switching between Row and Column?

This was my attempt to allow the user to specify the direction that elements should be stacked for localized layouts, e.g. right-to-left locales, but it looks like writing-mode would cover this. Though I'm curious now why these are available in flexbox?

@nicoburns nicoburns mentioned this issue Jan 30, 2023
34 tasks
@nicoburns
Copy link
Collaborator Author

nicoburns commented Mar 6, 2023

In discussions with @geom3trik it has become apparent that Morphorm's algorithm is not as fixed/finished as I initially assumed it to be (it is based on Subform layout, but that was also never formally specified (at least not publically), and that part of this task will likely be to pin down exactly how the algorithm ought to work.

Also through those discussions (and further influenced by @hecrj's push back on integrating Taffy's existing layout modes into Iced), it has become clear to me that the clearest justification for Morphorm's existence in Taffy (more so even than it's simplified mental model) is as a single-pass counterpoint to the CSS algorithms (Flexbox and CSS Grid). That is, a morphorm container should never call measure_size on it's children, and should call perform_layout on each child exactly once.

With this in mind, I now think that we ought to make Morphorm layout modes as fully-featured as possible (so in particular, I am now considering things wrapping and grid layout as back in scope) under the hard constraint of being single-pass, and the fuzzier constraint of having a simple mental model. This will enable systems that want utmost layout performance (or that don't want to deal with the caching required for fast multipass layout) to use Taffy by disabling the slower CSS modes, although we should also attempt to make these two groups of layout modes work together so that systems that want to enable all Taffy layout modes can do so.

In implementing this we may wish to take inspiration from Flutter's layout model implements just such a single-pass layout model (it turns out that while Flutter includes a flexbox-like layout mode, it isn't actually flexbox compatible precisely because it is a single-pass variant on the same general idea).

Reading on Flutter's layout model:

@alice-i-cecile
Copy link
Collaborator

Very nice writeup and goals. I'm in favor of both "simple mental model" and "fully featured", so I'm on board with this direction.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
controversial This work requires a heightened standard of review due to implementation or design complexity enhancement New feature or request
Projects
Status: Todo
Development

No branches or pull requests

3 participants