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

Layout of container with only stretchy children #124

Closed
fred-wang opened this issue Aug 6, 2019 · 10 comments
Closed

Layout of container with only stretchy children #124

fred-wang opened this issue Aug 6, 2019 · 10 comments
Labels
css / html5 Issues related to CSS or HTML5 interoperability MathML Core Issues affecting the MathML Core specification

Comments

@fred-wang
Copy link

These are the general rules from MathML3:

https://mathml-refresh.github.io/mathml/#horizontal-stretching-rules
"should stretch to cover the width of the other direct sub-expressions in the given element"

https://mathml-refresh.github.io/mathml/#vertical-stretching-rules
"should stretch to cover the height and depth (above and below the axis) of the non-stretchy direct sub-expressions in the mrow element"

And the exception when all the children are stretchy:
https://mathml-refresh.github.io/mathml/#rules-common-to-both-vertical-and-horizontal-stretching
"If a stretchy operator is required to stretch, but all other expressions in the containing element (as described above) are also stretchy, all elements that can stretch should grow to the maximum of the normal unstretched sizes of all elements in the containing object, if they can grow that large."

Currently MathML Core implements this exception for horizontal layout but not for vertical layout:
https://mathml-refresh.github.io/mathml-core/#algorithm-for-stretching-operators-along-the-inline
https://mathml-refresh.github.io/mathml-core/#algorithm-for-stretching-operators-along-the-block-axis

With this exception, when all the children are stretchy they have to be laid out twice: A first time without a stretch constraint and a second time with a stretch constraint. Note that these children are not just single-character operator but can be embellished operators corresponding a to a subtree of arbitrary size (fortunately, the stretching algorithms never happen again in this subtree).

I'm opening this issue to know how important this case is. Can we avoid this second layout by using a fixed minimum ascent/width instead e.g. 1.5em?

@fred-wang fred-wang added MathML Core Issues affecting the MathML Core specification css / html5 Issues related to CSS or HTML5 interoperability labels Aug 6, 2019
@davidcarlisle
Copy link
Collaborator

The exception seems slightly strange really.

it seems to say that if you have an mrow that just consists of (omitting the markup)

[ { ( ) ] }

then all the delimiters stretch based on the maximum unstretched size of each delimiter.

But

[ { ( . ) ] }

then each delimiter is stretched independently to cover just the .

Treating the first as if there was say an empty <mo></mo> instead of <mo>.</mo> would seem more natural, even if that ends up with something like | shrinking...

@NSoiffer
Copy link
Contributor

The idea behind this rule is that some stretchy chars may have short default versions and others somewhat taller ones. If the short one was on the outside (in the same mrow), then it would look poor if it didn't grow to cover the inner ones.

Having said that, in David's examples, it is likely (hopefully) that each pair of bracketing chars in it's own mrow. If true, then the rule only applies to the () pair. In general, I can't see how this would remotely be a time consideration because if there are only stretchy chars, it is likely there are only two chars in the mrow, so taking an extra pass is negligible, time-wise even if they are the core of an embellished operator.

Of course, one can construct a time consuming case with hundreds of stretchy chars, but it would not be remotely realistic. In practice, you could probably skip this case as the need to stretch beyond the default size if there are only stretchy chars is extermely remote.

@fred-wang
Copy link
Author

These exceptions require extra code and (conformance/fuzz) testing to ensure that the most general cases with complex embellished operators are properly handled. So the statement that in practice there are only trivial edge cases with two operators is actually an argument to remove/simplify them.

For the "only stretchy children", why can't one just use a fixed multiple of font metrics (e.g. 1.5em) as the minimum size if the goal is just to avoid inconsistent unstretched size? In practice, if fonts have large unstretched sizes for operator then these characters will already look weird before you even try to use them in this MathML edge case.

Or if the "only stretchy children" case is not important at all then I would suggest just skipping operator stretching in the container.

The horizontal case (munder/mover/mundeover with only stretchy children) seems slightly different than the vertical one though. What are the concrete use cases?

@fred-wang fred-wang added the need resolution Issues needing resolution at MathML Refresh CG meeting label Aug 22, 2019
@NSoiffer
Copy link
Contributor

For core, I'm ok with either dropping the special case or specifying a min size if you really feel it complicates the implementation (I don't -- see below). 'em's are typically used for horizontal measurements and 'ex's for vertical ones, so I would favor using an 'ex'-based min size if we go that route. In any case, 1.5ems is too big for a minimum since you want the normal version of most stretchy chars. Maybe 1em or 2ex (which are close to each other in size); maybe even .75em/1.5ex.

Specifying a min size still means that a test case should be present. The amount of code is smaller if a min size is given, but the code for the current spec seems pretty simple and is basically the same as the code for determining the min size when there non-stretchy chars present -- I think you just need a boolean foundNonStretchy and a single loop for the current spec:

foundNonStretchy = false
minsize = 0   // tuple of height/depth
for each char in mrow {
  if !foundNonStretchy && !isStretchy(char) {
     foundNonStretchy = true
     minsize = size(char)
  } else if !foundNonStretchy || !isStretchy(char) {
     minsize = max(minsize, size(char))
  }
}

For the horizontal case of non-nonstretchy chars, here's a case, although I admit it is a bit of a stretch ;-).
image
One can imagine munderover with under/over braces (that might be embellished with text/numbers) and inside of that is another munderoverwith some equilibrium double arrow and numbers/rates above and/or below it. That's a bit different than the mrow case because the inner munderovershould stretch to cover the other parts, but then when considering the outer munderover, the inner one is embellished so all three things to look at are stretchy operators. I haven't reread the spec in detail, but I think that's the way it works with horizontal stretchy chars.

@fred-wang
Copy link
Author

For core, I'm ok with either dropping the special case or specifying a min size if you really feel it complicates the implementation (I don't -- see below). 'em's are typically used for horizontal measurements and 'ex's for vertical ones, so I would favor using an 'ex'-based min size if we go that route. In any case, 1.5ems is too big for a minimum since you want the normal version of most stretchy chars. Maybe 1em or 2ex (which are close to each other in size); maybe even .75em/1.5ex.

Specifying a min size still means that a test case should be present. The amount of code is smaller if a min size is given, but the code for the current spec seems pretty simple and is basically the same as the code for determining the min size when there non-stretchy chars present -- I think you just need a boolean foundNonStretchy and a single loop for the current spec:

foundNonStretchy = false
minsize = 0   // tuple of height/depth
for each char in mrow {
  if !foundNonStretchy && !isStretchy(char) {
     foundNonStretchy = true
     minsize = size(char)
  } else if !foundNonStretchy || !isStretchy(char) {
     minsize = max(minsize, size(char))
  }
}

This is really oversimplified and does not correspond to browser layout... You don't have "char" but child nodes and to get the "size" of a child you need to perform layout on it. And again, embellished operators are complex, can be of arbitrary size, involves embellished & space-like definitions. We need a lot of test cases to check that things are laid out correctly, that you have correct preferred width, that rendering is updated after dynamic changes etc.

Currently non-stretchy children are laid out to measure the target size and then the embellished stretchy operators are laid out with that size. But in the all-stretchy cases, children must be laid out twice (first without stretching and then with stretching):
https://mathml-refresh.github.io/mathml-core/#algorithm-for-stretching-operators-along-the-inline

All of this has been causing bugs and issues in WebKit and Gecko in the past so I doubt it is as simple as just reading a minimal size from the font metrics or just skipping stretching. I'm not questioning whether that's technically possible, I just want to be sure that we don't add extra implementation/testing costs for something that so far does not seem really useful.

For the horizontal case of non-nonstretchy chars, here's a case, although I admit it is a bit of a stretch ;-).
image
One can imagine munderover with under/over braces (that might be embellished with text/numbers) and inside of that is another munderoverwith some equilibrium double arrow and numbers/rates above and/or below it. That's a bit different than the mrow case because the inner munderovershould stretch to cover the other parts, but then when considering the outer munderover, the inner one is embellished so all three things to look at are stretchy operators. I haven't reread the spec in detail, but I think that's the way it works with horizontal stretchy chars.

Well you are just explaining the spec with a somewhat artificial example. I still don't see any concrete use case that would justify two layout passes. So sorry I'm still not convinced :-/

@fred-wang
Copy link
Author

fred-wang commented Oct 25, 2019

Another issue to consider here:

mrow1
|
mrow2
|        \ 
mo     mspace height="100px"

If mrow1 lays out mrow2 without any stretch constraint in order to get an unstreched size, mrow2 will still try to perform operator stretching on the mo element. I guess a solution would be that mrow1 passes 0 as a target size or extend the stretch constraint with a definition of "do not stretch".

@NSoiffer
Copy link
Contributor

NSoiffer commented Nov 4, 2019

I do not see what you think is a problem. Where would the size constraint on mrow1 (or any mrow) come from? You can't set it on math or mstyle in core. It can only come from the mo and would percolate upward/outward.

I'm probably misunderstanding what you are getting at. Can you explain in more detail about the stretch constraints?

@fred-wang
Copy link
Author

I do not see what you think is a problem. Where would the size constraint on mrow1 (or any mrow) come from? You can't set it on math or mstyle in core. It can only come from the mo and would percolate upward/outward.

CSS layout constraints are passed from top to bottom.

@fred-wang
Copy link
Author

This is explained in https://mathml-refresh.github.io/mathml-core/#dfn-algorithm-for-stretching-operators-along-the-block-axis ;

  • mrow1 will skip step 1 (no stretch constraint), 2 (mrow2 is embellished op) and will finally arrive to step 5.3 to layout mrow2 with a stretch constraint.
  • mrow2 will thus do step 1 (stretch constraint passed by mrow1) that is layout the mo with a stretch constraint equal to 0 and thus ignore the mspace embellishment.

If we modify the algo to make mrow1 first determine the unstretched size of mrow2 in order to calculate the stretch size, then we cannot just layout mrow2 without stretch constraint for that purpose. Otherwise mrow2 will skip 1 (no stretch constraint) will do 2 (layout mspace) and will finally arrive to 5.3 that is layout mo with the stretch constraint calculated from the mspace height. But we want the unstretched size of mrow2!

Anyway, I don't want to spend time during the next meeting to explain the stretch algo againn. I think the question here is just whether people want to include the unstretched size or not. And if they do want it,
a reasonable proposal is just to pass 0 (or any special value) as stretch constraint in order to calculate the unstretched size of embellished op children.

@fred-wang
Copy link
Author

Consensus from 2019/11/11: Pass a 0 target size when calculating unstretched sizes, to avoid issues with nested embellished operators

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
css / html5 Issues related to CSS or HTML5 interoperability MathML Core Issues affecting the MathML Core specification
Projects
None yet
Development

No branches or pull requests

3 participants