Feature: Implement the Auto Layout constraint solver - #643
Conversation
|
@fredkiefer - you can let this PR marinade for a while. This is a feature, not a bug fix per se - I noted a lot of the plumbing existed, but wasn't wired up and I don't think even tested, so I wired it up and fixed the problems I could find against MacOS AppKit related behaviors. So this is a welcome diversion from test harnesses, but needs some solid testing by others in actual environments. |
|
I hope to find time to look into this over the weekend. It is too much to work on in the evening. |
|
Sounds good - tackle #692 if you could |
GSCassowarySolver was a stub, so activating layout constraints produced no solution and constrained views laid out to a zero rect. Implement the simplex orchestration on top of GSCSTableau: adding and removing required and non-required constraints, the artificial-variable path, objective optimisation, and reading the external variable values back out as a GSCSSolution. The supporting primitives had two errors that were never reached while the solver was stubbed: - GSCSVariable -isRestricted treated external variables as restricted and dummy variables as unrestricted, the opposite of the classification the tableau relies on for infeasible-row detection and exit-variable selection. - GSCSTableau -substituteOutTerm:withExpression:inExpression:subject: folded in the terms of the expression being modified instead of the terms of the substituting expression. GSAutoLayoutEngine -alignmentRectForView: and its supporting methods were also unimplemented; return the solved minX, minY, width and height for a view.
Implement -suggestEditVariable:equals: so a variable can be pinned to a succession of values, which the layout engine uses when an intrinsic size or a view geometry changes. On the first suggestion a non-required edit constraint is added for the variable; subsequent suggestions adjust its constant and restore feasibility with a dual optimisation pass. GSCSEditInfo now retains the objects it stores, exposes its error variables and previous constant, and tracks the constant as a CGFloat so fractional layout values are preserved.
Activation was a no-op, so -setActive:, +activateConstraints: and the anchor convenience methods never installed a constraint and constrained views were never laid out. Route an activated constraint to the layout engine of the view that owns it, which is the nearest ancestor shared by its two items, and take it back out on deactivation. Constraints activated before their view has a window are installed once the view moves into one. A constraint created with +constraintWithItem:... or an initialiser is now inactive until it is activated, matching AppKit. The added test drives a subview through the anchor API and checks the solved frame, and skips when no display is available.
When the layout engine is created for a window, add required constraints fixing the content view's origin and size to its current bounds. Subview constraints expressed relative to the content view then resolve to absolute positions in the content view's coordinate system without the application having to constrain the root view itself.
The layout engine multiplied a relational constraint's constant by -1 for the right, top and trailing attributes. This inverted the constant for anchor constraints such as [a.rightAnchor constraintEqualToAnchor: b.rightAnchor constant: -30], and gave the wrong result for the same attributes in a visual format string. Apply the constant as written, and negate the spacing in the visual format parser for vertical arrangements, where a positive spacing runs toward decreasing y in a non-flipped view.
The content view was pinned to its bounds once when the layout engine was created, so resizing a window left the pins stale and constrained subviews kept their original geometry. The engine now owns the content view's size constraints and refreshes them from the current bounds, and a content view lays its constrained subtree out again when its frame changes.
Give NSControl an intrinsic content size, its cell size, so a control can be laid out from its content without an explicit width or height constraint, and initialise the content hugging and compression resistance priorities to their default low and high values, which the engine uses to weight the intrinsic size constraints. Two supporting fixes were reached for the first time now that the hugging and compression constraints are added to a working solver: - GSCSConstraint +constraintWithLeftVariable:operator:rightVariable: released the right hand side expression before passing it to the constraint initialiser, which left the inequality constraint holding a freed expression. - GSCassowarySolver added a second edit constraint in -suggestEditVariable: equals: even when one had already been added for the variable, so the two disagreed on the value. Edit constraints are now recorded as they are added and reused.
1b6e4c0 to
4e77393
Compare
START_SET creates an autorelease pool and END_SET releases it, so the pool these tests create around the set is redundant.
| { | ||
| // FIXME Remove constraint from solver | ||
| FOR_IN(GSCSConstraint *, constraint, constraints) | ||
| [self addConstraint: constraint]; |
There was a problem hiding this comment.
This is fine for now, but later we should split up -addConstrain: so that we only solve the resulting constraints once here and not for each of the added ones separately. The same is true for removal.
fredkiefer
left a comment
There was a problem hiding this comment.
Very impressive code. My main concern is, whether there is any performance impact on applications not using this feature?
And as this is a real mayor contribution, I would like to leave it to @gcasa whether we merge that now.
|
I don't see it being particularly performance degrading - the code is actually reasonably light for the feature, and if it was a performance issue, I would be more inclined to look at the backend. I will make one change to -setFrame: and -setFrameSize: that will save about 5 ns per call, but it is on a hot path - other than that nothing else I can see that is costly. Also will change the -addConstraint: -removeConstraint: per your suggestion. |
NSView implements -removeConstraint: and -removeConstraints:, but neither is declared, so an application cannot call either one without a warning.
-addConstraints: and -removeConstraints: reached an optimal tableau and read the solution back once per constraint in the group. The solver constraints of a group are now collected and handed to the solver together, and the tracked views are measured against the result once.
-setFrame: and -setFrameSize: sent -_autoLayoutContentViewResized on every view, which sends -window and -contentView before it can return. A process that has created no layout engine now stops at a single test.
|
Measured on a driver that creates no constraints at all. 200000 The resize path now tests a flag set when the first layout engine is created, Constraints are now added and removed in one solver pass. The engine handed NSView.h now declares the two removal methods, which were implemented but |
|
Sounds great, thank you. I review in the morning! |
|
One note, +activateConstraints: still loops singular, so the batch path is only reached via [view addConstraints:]. Making the modern path batch means grouping constraints by container view - I did not do that, but can if you prefer. |
fredkiefer
left a comment
There was a problem hiding this comment.
Now the code looks even better. Thank you very much!
GSCassowarySolver was a stub, so activating layout constraints produced no solution and constrained views laid out to a zero rect. This implements the solver and wires up activation so the anchor and NSLayoutConstraint APIs actually position views.
Changes:
The GSCSVariable, GSCSTableau and GSCSEditInfo errors were never reached while the solver was stubbed.
The added gui tests drive subviews through the anchor and visual-format APIs and check the solved frames; they skip when no display is available.
Closes #540.