what does it mean for a widget to be given a size different from the one it returned from its layout method?
In doing the baseline work, I have been exploring making Widget::layout return a more general type than Size. Overtime, we have added additional metrics that are computed in layout. The first of these was the 'paint insets', which describe the area outside of the view's layout size where the view may draw; and now I've been adding baseline positions, which are offsets relative to the edge of the layout's size that are used to align horizontal text.
Currently, these additional metrics are added through calls on the LayoutCtx; for instance, LayoutCtx::set_paint_insets. The idea was to hopefully simplify this, by having all of these metrics be returned directly from the layout call, when applicable.
Of course, there's a problem.
This problem isn't specific to the redesign; it relates to the fact that the actual size of a widget is not the necessarily the size returned from the widget's layout method; it is a size chosen by whichever ancestor is managing the widget's WidgetPod. In the general case this should be the size returned by the widget, but this is not enforced; the parent can in theory chose an arbitrary size.
What does it mean for the parent to choose a size different from that returned by the child? This is not really written down anywhere; nor is there a clear guideline for how the child is expected to adapt when drawing to an unexpected size.
This potential conflict poses problems for things like baselines and paint insets. These metrics are relative to the size calculated in layout, and will be invalid if the widget ends up being provided with a different size.
I think we can improve on this situation, by clarifying our invariants and how we will behave when they are not met. I propose the following:
If a widget returns a valid size from layout (defined as a size that satisfies the provided constraints) the widget will always be given the same size in paint.
If the parent widget needs to paint the child in a size other than what the child requested, the child's paint method will be called with the child's expected size, and the parent will chose to either clip or overflow the content.
All metrics (such as baselines and paint insets) will be relative to the 'layout size', that is the size returned from Widget::layout.
WidgetPod::set_layout_rect may be replaced with WidgetPod::set_layout_pos?
The main advantage of this is that it significantly simplifies layout; The WidgetPod itself can stash the result of its child's layout method, and know that the provided values are valid.
I'm not sure about this last bit; in particular I'm not sure if we should maintain a distinction between the 'layout rect' returned by the child and the 'paint rect' determined by the parent. Having these be separate, and sharing an origin, would allow us to correctly adjust paint insets and baselines as appropriate, but would increase complexity somewhat.
Ultimately, this comes down to a fairly straight-forward question: what does it mean for a parent to provide a different size than that returned by the child? Is this an okay thing during normal execution, or does this violate some contract? If it violates some contract, and potentially interferes in other layout, I think that's okay; we can log some warnings and expect the user to fix their code. If however there is some good reason why this behaviour may be required, we should be more defensive.
I definitely think some clarification of what the assumptions are here is good.
I agree with your proposal on all points.
But I think this might get at a deeper issue - BoxConstraints aren't very rich. We can't communicate preference, or restrictions on aspect ratio, or anything other than a range of acceptable rect sizes. Maybe there's room for adding a bit more richness to the layout constraints? Aspect ratio, and a preferred size are just two possible examples I can think of, but there may be others (related to text or image placement, maybe?).
I think adding a bit of additional richness in constraints mirrors added richness in the returned layout.
Conversely, maybe richer constraints are additional API that doesn't belong in the base Widget trait. It certainly would likely complicate the layout implementation required.
I don't want to dismiss the idea of richer constraints, but I want to be wary of them; the beauty of the box layout model is it's simplicity, which allows it to work in a single pass over the tree, with no complicated constraint solving or other concerns. I would be reluctant to give up that simplicity.
I think that things like 'preferred sizes' are still expressible by convention; it's just up to the user to ensure that their widgets are being given appropriately flexible constraints, and then the widget has a lot of freedom to pick its desired size. This is true, for instance, with TextBox: if the constraints allow it will always choose a preferred width (WIDE_WIDGET_WIDTH) and if you want it to use another width you have to modify the constraints, with some method like fix_width or expand_width.
What does it mean for the parent to choose a size different from that returned by the child? This is not really written down anywhere; nor is there a clear guideline for how the child is expected to adapt when drawing to an unexpected size.
I know I've ran into situations like this that have been hard to understand what I'm doing wrong (or if it's the widgets I'm composing at fault). I feel like a great start is to get rid of this ambiguous behavior and see where we stand and what extra functionality we need to be expressive. Basically, I really like this proposal and even if it exposes layout challenges those layout challenges will be easier to solve without this ambiguity.
We can't communicate preference, or restrictions on aspect ratio, or anything other than a range of acceptable rect sizes
I did add a method for constraining preserving aspect ratio over L1 distance, so you can use this if you want to declare a fixed size.
I think that I agree that giving up simplicity is something to be wary of. I'm not sure where the trade-off line lies, though. I think there's a cliff somewhere where the complexity explodes, and I think we're far from that right now. But I don't want to think lightly on adding layers of complexity.
I think a key thing to bear in mind (and this is reminding me as much as anyone) is this is talking about defining rules and exploring alternatives in the basic layout model. Which is to say that all changes are expensive.
Maybe in situations that desire or require more richness, we should advocate for a more rich layout system in those places. I've experimented with this a bit out of tree, and maybe we should discuss that. But I think that's a different thread. Just pointing out that nothing disallows alternative layout systems on top of a system that we strive to keep simple.
the parent will chose to either clip or overflow the content
What about scaling the content to the new size instead? Or scaling while preserving aspect ratio? I can think of potential cases where I'd want those to be possible options (although ideally I'd never find myself in this situation in the first place).
I did add a method for constraining preserving aspect ratio over L1 distance, so you can use this if you want to declare a fixed size.
Right, and I think that as a widget is nice, but it's not built in to the layout system, and it asks for a specific aspect ratio. You can't define an acceptable range or anything (though I don't know how common a range of aspect ratios would be - I've always gotten by without it)
the parent will chose to either clip or overflow the content
What about scaling the content to the new size instead? Or scaling while preserving aspect ratio? I can think of potential cases where I'd want those to be possible options (although ideally I'd never find myself in this situation in the first place).
You could do this by wrapping the child in an explicit Scale widget or similar, but I wouldn't want this built in to the layout system itself, because it would break (or at best complicate) the math around paint insets and baseline offsets; the advantage of "draw at origin and clip or overflow as needed" is that those numbers remain correct, because the base geometry hasn't changed.
In any case: my proposal here is mostly about policy, that is about standardizing what behaviour is expected and what behaviour we consider 'bad', and would want to log warnings about.
Most helpful comment
You could do this by wrapping the child in an explicit
Scalewidget or similar, but I wouldn't want this built in to the layout system itself, because it would break (or at best complicate) the math around paint insets and baseline offsets; the advantage of "draw at origin and clip or overflow as needed" is that those numbers remain correct, because the base geometry hasn't changed.In any case: my proposal here is mostly about policy, that is about standardizing what behaviour is expected and what behaviour we consider 'bad', and would want to log warnings about.