Skip to content

Introduce ViewConstraint and layoutWithConstraint - #45

Open
jdolan wants to merge 6 commits into
mainfrom
layout-measure-arrange
Open

Introduce ViewConstraint and layoutWithConstraint#45
jdolan wants to merge 6 commits into
mainfrom
layout-measure-arrange

Conversation

@jdolan

Copy link
Copy Markdown
Owner

ObjectivelyMVC's layout engine has had some fundamental issues for years. Namely, there are two ViewAutoresizing bits that pull in opposite directions: Contain and Fill. Scenarios where a child specified Fill and a parent specified Contain are ambiguous.

There was also a staleness bug in StackView that did not re-compute remaining size while laying out subviews. These sorts of bugs led to layoutSubviews not being idempotent.

This branch aims to fix these problems by introducing the concept of constraints, where a parent negotiates each child's size before proceeding to layout. It allows bottom-up size requests to propagate upward before top-down layout clobbers them.

StackView::layoutSubviews sized and positioned siblings from each
subview's leftover frame.w/h from the previous layout pass, so a
Contain-sized child's real intrinsic size was only known after it had
already been used (stale) to size and position its siblings within
the same pass -- no fixed point was guaranteed, and a Fill child
inside a Contain ancestor was flatly unresolvable, since the ancestor
can't hand a real bound to a child it's still sizing itself from.
Split layout into a bottom-up View::sizeThatSatisfies pass (what size
do you want, given this ViewConstraint) that runs to completion for a
dirty subtree before any frame is committed, followed by the existing
top-down View::layoutSubviews arrange pass. A Fill child measured
while its Contain ancestor is itself unresolved receives
ViewConstraintUnspecified and degrades to its own intrinsic size,
rather than reading a stale number; it only actually fills once an
ancestor hands it ViewConstraintEqual, during arrange.
View::sizeToSatisfy joins sizeToContain/sizeToFit/sizeToFill as the
resize-and-mark-dirty sibling of sizeThatSatisfies. layoutIfNeeded,
base layoutSubviews's arrange loop, StackView's arrange loop, and
Panel's contentView bypass each used to hand-duplicate a "resize (or
sizeToSatisfy), clearWarnings, layoutSubviews, needsLayout = false"
sequence, with a comment at each explaining why layoutIfNeeded itself
couldn't be called instead (it would re-derive its own, weaker guess
at the applicable constraint instead of using the one the caller
already resolved). That shared tail is now View::layoutWithConstraint
("what size do you want, given this ViewConstraint" -- resolved via
sizeToSatisfy, honoring ViewAutoresizingWidth/Height per axis) and
View::layoutWithSize ("this is your size" -- applied verbatim, no
negotiation, for StackView's and CollectionView's own distribution
math, which overrides a subview's size regardless of its own
autoresizing bits). needsLayout = false is now written in exactly one
place in the codebase instead of four, and layoutWithConstraint always
resolves regardless of isContainer -- gating it on isContainer (an
earlier version of this same change) skipped resizing entirely for a
Fill-only, non-container View, such as Slider's `bar`.
The `constrainedSize` field this went through along the way -- cached
by sizeThatSatisfies, consumed by StackView -- is gone; sizeThatSatisfies
is a pure function of a View's own state, and StackView holds its
subviews' measured sizes in a plain local array scoped to its own
layoutSubviews call, not on the View instances themselves.
Fixes found only by running Examples/Hello against real widgets, not
caught by the synthetic fixture tests: sizeThatSatisfies must skip a
Contain view whose own sizeThatFits override (Text, TableView, Select)
is meaningful only when that view opted into Contain/Fit itself
(TableView.c), a StackView subview without a matching autoresizing bit
must not have its distribution-computed size re-derived through a
constraint it never agreed to honor, and a View re-laid-out standalone
(e.g. a selected TableRowView, whose style rebind marks only itself
dirty) must trust its own established frame as exact rather than
merely an upper bound, or it shrinks back to its unconstrained content
size. Slider and TextView needed an explicit `min-width` in CSS for
the same reason a Contain view's authored size can't otherwise survive
being summed from children that have none of their own.
Adds Tests/ObjectivelyMVC/View.c (StackView and plain-View fixture
graphs asserting exact post-layout frame values) as a fourth
check_PROGRAMS entry alongside Selector/Style/Stylesheet -- there was
previously no ObjectivelyMVC-View test target at all.
Mirrors ObjectivelyMVC-Style: a command-line-tool target building
Tests/ObjectivelyMVC/View.c against the same frameworks, plus a shared
scheme, so the new View check_PROGRAMS test can run from Xcode like
Selector/Style/Stylesheet already could.
CopilotAI lite review requested due to automatic review settings September 1, 2026 01:11

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The new constraint pipeline introduces at least two confirmed behavior bugs (standalone relayout can force an axis to 0, and Panel’s contentView height constraint is ignored), which can cause incorrect layout at runtime.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Introduces a constraint-based sizing pass (ViewConstraint, sizeThatSatisfies, layoutWithConstraint) to make layout idempotent and to resolve historical ambiguity between Contain and Fill, while also fixing StackView measurement staleness by measuring subviews fresh per layout pass.

Changes:

  • Add ViewConstraint + new sizing/layout APIs (sizeThatSatisfies, sizeToSatisfy, layoutWithConstraint, layoutWithSize) and wire them into the default View layout pipeline.
  • Update key containers (e.g., StackView, Panel, CollectionView) to use constraint/size-driven layout entrypoints instead of resize + layoutIfNeeded.
  • Add a new View test suite covering the previously ambiguous and stale measurement scenarios; update CSS to add explicit min-width floors for widgets relying on contain/fill behavior.
File summaries
FileDescription
Tests/ObjectivelyMVC/View.cAdds layout regression tests for contain/fill ambiguity and StackView staleness/idempotency.
Tests/ObjectivelyMVC/Makefile.amRegisters the new View test binary in autotools test runner.
Tests/ObjectivelyMVC/.gitignoreIgnores the new View test executable.
Sources/ObjectivelyMVC/View.hDefines ViewConstraint and documents/declares new sizing/layout APIs.
Sources/ObjectivelyMVC/View.cImplements constraint-based sizing/layout and updates default layout behavior to use it.
Sources/ObjectivelyMVC/TableView.cAdjusts sizeThatFits to avoid returning expensive natural size when not acting as a container.
Sources/ObjectivelyMVC/StackView.cFixes stale measurement by measuring subviews fresh and laying them out via layoutWithSize.
Sources/ObjectivelyMVC/Panel.cSwitches content view layout from direct resize to constraint-based layout.
Sources/ObjectivelyMVC/CollectionView.cUses layoutWithSize for dictated item sizes during layout.
ObjectivelyMVC.xcodeproj/xcshareddata/xcschemes/ObjectivelyMVC-View.xcschemeAdds an Xcode scheme for the new View-focused test/target.
ObjectivelyMVC.xcodeproj/project.pbxprojAdds the new ObjectivelyMVC-View target and related build settings/dependencies.
Assets/stylesheet.css.hUpdates embedded stylesheet bytes/length for CSS changes.
Assets/stylesheet.cssAdds min-width rules (e.g., Slider/TextView) to ensure contain sizing has a non-zero floor.
Review details
  • Files reviewed: 13/13 changed files
  • Comments generated: 4
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +111 to +116
View *contentView = (View *) this->contentView;

const ViewConstraint w = MakeConstraint(ViewConstraintEqual, size.w);
const ViewConstraint h = MakeConstraint(ViewConstraintEqual, size.h);

$(contentView, layoutWithConstraint, w, h);
Comment on lines +1112 to +1119
ViewConstraint w, h;
if (self->frame.w || self->frame.h) {
w = MakeConstraint(ViewConstraintEqual, self->frame.w);
h = MakeConstraint(ViewConstraintEqual, self->frame.h);
} else {
w = MakeConstraint(ViewConstraintUnspecified, 0);
h = MakeConstraint(ViewConstraintUnspecified, 0);
}
Comment on lines +842 to +844
* @remarks This is the shared tail of View::layoutIfNeeded: resolve self's size via
* View::sizeToSatisfy if self is a container, then View::layoutSubviews, then clear
* `needsLayout`. It exists so a caller that already knows the correct ViewConstraint for a
Comment on lines +829 to +831
* @remarks The default implementation resolves each subview's size via View::layoutWithConstraint,
* offering `Exact` for a `ViewAutoresizingWidth`/`Height` subview (since this View's bounds are
* already final) or `Unspecified` otherwise (so the subview sizes itself from its own content).
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@jdolan
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Introduce ViewConstraint and layoutWithConstraint by jdolan · Pull Request #45 · jdolan/ObjectivelyMVC · GitHub
Skip to content

Introduce ViewConstraint and layoutWithConstraint - #45

Open
jdolan wants to merge 6 commits into
mainfrom
layout-measure-arrange
Open

Introduce ViewConstraint and layoutWithConstraint#45
jdolan wants to merge 6 commits into
mainfrom
layout-measure-arrange

Conversation

@jdolan

Copy link
Copy Markdown
Owner

ObjectivelyMVC's layout engine has had some fundamental issues for years. Namely, there are two ViewAutoresizing bits that pull in opposite directions: Contain and Fill. Scenarios where a child specified Fill and a parent specified Contain are ambiguous.

There was also a staleness bug in StackView that did not re-compute remaining size while laying out subviews. These sorts of bugs led to layoutSubviews not being idempotent.

This branch aims to fix these problems by introducing the concept of constraints, where a parent negotiates each child's size before proceeding to layout. It allows bottom-up size requests to propagate upward before top-down layout clobbers them.

StackView::layoutSubviews sized and positioned siblings from each
subview's leftover frame.w/h from the previous layout pass, so a
Contain-sized child's real intrinsic size was only known after it had
already been used (stale) to size and position its siblings within
the same pass -- no fixed point was guaranteed, and a Fill child
inside a Contain ancestor was flatly unresolvable, since the ancestor
can't hand a real bound to a child it's still sizing itself from.
Split layout into a bottom-up View::sizeThatSatisfies pass (what size
do you want, given this ViewConstraint) that runs to completion for a
dirty subtree before any frame is committed, followed by the existing
top-down View::layoutSubviews arrange pass. A Fill child measured
while its Contain ancestor is itself unresolved receives
ViewConstraintUnspecified and degrades to its own intrinsic size,
rather than reading a stale number; it only actually fills once an
ancestor hands it ViewConstraintEqual, during arrange.
View::sizeToSatisfy joins sizeToContain/sizeToFit/sizeToFill as the
resize-and-mark-dirty sibling of sizeThatSatisfies. layoutIfNeeded,
base layoutSubviews's arrange loop, StackView's arrange loop, and
Panel's contentView bypass each used to hand-duplicate a "resize (or
sizeToSatisfy), clearWarnings, layoutSubviews, needsLayout = false"
sequence, with a comment at each explaining why layoutIfNeeded itself
couldn't be called instead (it would re-derive its own, weaker guess
at the applicable constraint instead of using the one the caller
already resolved). That shared tail is now View::layoutWithConstraint
("what size do you want, given this ViewConstraint" -- resolved via
sizeToSatisfy, honoring ViewAutoresizingWidth/Height per axis) and
View::layoutWithSize ("this is your size" -- applied verbatim, no
negotiation, for StackView's and CollectionView's own distribution
math, which overrides a subview's size regardless of its own
autoresizing bits). needsLayout = false is now written in exactly one
place in the codebase instead of four, and layoutWithConstraint always
resolves regardless of isContainer -- gating it on isContainer (an
earlier version of this same change) skipped resizing entirely for a
Fill-only, non-container View, such as Slider's `bar`.
The `constrainedSize` field this went through along the way -- cached
by sizeThatSatisfies, consumed by StackView -- is gone; sizeThatSatisfies
is a pure function of a View's own state, and StackView holds its
subviews' measured sizes in a plain local array scoped to its own
layoutSubviews call, not on the View instances themselves.
Fixes found only by running Examples/Hello against real widgets, not
caught by the synthetic fixture tests: sizeThatSatisfies must skip a
Contain view whose own sizeThatFits override (Text, TableView, Select)
is meaningful only when that view opted into Contain/Fit itself
(TableView.c), a StackView subview without a matching autoresizing bit
must not have its distribution-computed size re-derived through a
constraint it never agreed to honor, and a View re-laid-out standalone
(e.g. a selected TableRowView, whose style rebind marks only itself
dirty) must trust its own established frame as exact rather than
merely an upper bound, or it shrinks back to its unconstrained content
size. Slider and TextView needed an explicit `min-width` in CSS for
the same reason a Contain view's authored size can't otherwise survive
being summed from children that have none of their own.
Adds Tests/ObjectivelyMVC/View.c (StackView and plain-View fixture
graphs asserting exact post-layout frame values) as a fourth
check_PROGRAMS entry alongside Selector/Style/Stylesheet -- there was
previously no ObjectivelyMVC-View test target at all.
Mirrors ObjectivelyMVC-Style: a command-line-tool target building
Tests/ObjectivelyMVC/View.c against the same frameworks, plus a shared
scheme, so the new View check_PROGRAMS test can run from Xcode like
Selector/Style/Stylesheet already could.
CopilotAI lite review requested due to automatic review settings September 1, 2026 01:11

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The new constraint pipeline introduces at least two confirmed behavior bugs (standalone relayout can force an axis to 0, and Panel’s contentView height constraint is ignored), which can cause incorrect layout at runtime.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Introduces a constraint-based sizing pass (ViewConstraint, sizeThatSatisfies, layoutWithConstraint) to make layout idempotent and to resolve historical ambiguity between Contain and Fill, while also fixing StackView measurement staleness by measuring subviews fresh per layout pass.

Changes:

  • Add ViewConstraint + new sizing/layout APIs (sizeThatSatisfies, sizeToSatisfy, layoutWithConstraint, layoutWithSize) and wire them into the default View layout pipeline.
  • Update key containers (e.g., StackView, Panel, CollectionView) to use constraint/size-driven layout entrypoints instead of resize + layoutIfNeeded.
  • Add a new View test suite covering the previously ambiguous and stale measurement scenarios; update CSS to add explicit min-width floors for widgets relying on contain/fill behavior.
File summaries
FileDescription
Tests/ObjectivelyMVC/View.cAdds layout regression tests for contain/fill ambiguity and StackView staleness/idempotency.
Tests/ObjectivelyMVC/Makefile.amRegisters the new View test binary in autotools test runner.
Tests/ObjectivelyMVC/.gitignoreIgnores the new View test executable.
Sources/ObjectivelyMVC/View.hDefines ViewConstraint and documents/declares new sizing/layout APIs.
Sources/ObjectivelyMVC/View.cImplements constraint-based sizing/layout and updates default layout behavior to use it.
Sources/ObjectivelyMVC/TableView.cAdjusts sizeThatFits to avoid returning expensive natural size when not acting as a container.
Sources/ObjectivelyMVC/StackView.cFixes stale measurement by measuring subviews fresh and laying them out via layoutWithSize.
Sources/ObjectivelyMVC/Panel.cSwitches content view layout from direct resize to constraint-based layout.
Sources/ObjectivelyMVC/CollectionView.cUses layoutWithSize for dictated item sizes during layout.
ObjectivelyMVC.xcodeproj/xcshareddata/xcschemes/ObjectivelyMVC-View.xcschemeAdds an Xcode scheme for the new View-focused test/target.
ObjectivelyMVC.xcodeproj/project.pbxprojAdds the new ObjectivelyMVC-View target and related build settings/dependencies.
Assets/stylesheet.css.hUpdates embedded stylesheet bytes/length for CSS changes.
Assets/stylesheet.cssAdds min-width rules (e.g., Slider/TextView) to ensure contain sizing has a non-zero floor.
Review details
  • Files reviewed: 13/13 changed files
  • Comments generated: 4
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +111 to +116
View *contentView = (View *) this->contentView;

const ViewConstraint w = MakeConstraint(ViewConstraintEqual, size.w);
const ViewConstraint h = MakeConstraint(ViewConstraintEqual, size.h);

$(contentView, layoutWithConstraint, w, h);
Comment on lines +1112 to +1119
ViewConstraint w, h;
if (self->frame.w || self->frame.h) {
w = MakeConstraint(ViewConstraintEqual, self->frame.w);
h = MakeConstraint(ViewConstraintEqual, self->frame.h);
} else {
w = MakeConstraint(ViewConstraintUnspecified, 0);
h = MakeConstraint(ViewConstraintUnspecified, 0);
}
Comment on lines +842 to +844
* @remarks This is the shared tail of View::layoutIfNeeded: resolve self's size via
* View::sizeToSatisfy if self is a container, then View::layoutSubviews, then clear
* `needsLayout`. It exists so a caller that already knows the correct ViewConstraint for a
Comment on lines +829 to +831
* @remarks The default implementation resolves each subview's size via View::layoutWithConstraint,
* offering `Exact` for a `ViewAutoresizingWidth`/`Height` subview (since this View's bounds are
* already final) or `Unspecified` otherwise (so the subview sizes itself from its own content).
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@jdolan
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Introduce ViewConstraint and layoutWithConstraint by jdolan · Pull Request #45 · jdolan/ObjectivelyMVC · GitHub
Skip to content

Introduce ViewConstraint and layoutWithConstraint - #45

Open
jdolan wants to merge 6 commits into
mainfrom
layout-measure-arrange
Open

Introduce ViewConstraint and layoutWithConstraint#45
jdolan wants to merge 6 commits into
mainfrom
layout-measure-arrange

Conversation

@jdolan

Copy link
Copy Markdown
Owner

ObjectivelyMVC's layout engine has had some fundamental issues for years. Namely, there are two ViewAutoresizing bits that pull in opposite directions: Contain and Fill. Scenarios where a child specified Fill and a parent specified Contain are ambiguous.

There was also a staleness bug in StackView that did not re-compute remaining size while laying out subviews. These sorts of bugs led to layoutSubviews not being idempotent.

This branch aims to fix these problems by introducing the concept of constraints, where a parent negotiates each child's size before proceeding to layout. It allows bottom-up size requests to propagate upward before top-down layout clobbers them.

StackView::layoutSubviews sized and positioned siblings from each
subview's leftover frame.w/h from the previous layout pass, so a
Contain-sized child's real intrinsic size was only known after it had
already been used (stale) to size and position its siblings within
the same pass -- no fixed point was guaranteed, and a Fill child
inside a Contain ancestor was flatly unresolvable, since the ancestor
can't hand a real bound to a child it's still sizing itself from.
Split layout into a bottom-up View::sizeThatSatisfies pass (what size
do you want, given this ViewConstraint) that runs to completion for a
dirty subtree before any frame is committed, followed by the existing
top-down View::layoutSubviews arrange pass. A Fill child measured
while its Contain ancestor is itself unresolved receives
ViewConstraintUnspecified and degrades to its own intrinsic size,
rather than reading a stale number; it only actually fills once an
ancestor hands it ViewConstraintEqual, during arrange.
View::sizeToSatisfy joins sizeToContain/sizeToFit/sizeToFill as the
resize-and-mark-dirty sibling of sizeThatSatisfies. layoutIfNeeded,
base layoutSubviews's arrange loop, StackView's arrange loop, and
Panel's contentView bypass each used to hand-duplicate a "resize (or
sizeToSatisfy), clearWarnings, layoutSubviews, needsLayout = false"
sequence, with a comment at each explaining why layoutIfNeeded itself
couldn't be called instead (it would re-derive its own, weaker guess
at the applicable constraint instead of using the one the caller
already resolved). That shared tail is now View::layoutWithConstraint
("what size do you want, given this ViewConstraint" -- resolved via
sizeToSatisfy, honoring ViewAutoresizingWidth/Height per axis) and
View::layoutWithSize ("this is your size" -- applied verbatim, no
negotiation, for StackView's and CollectionView's own distribution
math, which overrides a subview's size regardless of its own
autoresizing bits). needsLayout = false is now written in exactly one
place in the codebase instead of four, and layoutWithConstraint always
resolves regardless of isContainer -- gating it on isContainer (an
earlier version of this same change) skipped resizing entirely for a
Fill-only, non-container View, such as Slider's `bar`.
The `constrainedSize` field this went through along the way -- cached
by sizeThatSatisfies, consumed by StackView -- is gone; sizeThatSatisfies
is a pure function of a View's own state, and StackView holds its
subviews' measured sizes in a plain local array scoped to its own
layoutSubviews call, not on the View instances themselves.
Fixes found only by running Examples/Hello against real widgets, not
caught by the synthetic fixture tests: sizeThatSatisfies must skip a
Contain view whose own sizeThatFits override (Text, TableView, Select)
is meaningful only when that view opted into Contain/Fit itself
(TableView.c), a StackView subview without a matching autoresizing bit
must not have its distribution-computed size re-derived through a
constraint it never agreed to honor, and a View re-laid-out standalone
(e.g. a selected TableRowView, whose style rebind marks only itself
dirty) must trust its own established frame as exact rather than
merely an upper bound, or it shrinks back to its unconstrained content
size. Slider and TextView needed an explicit `min-width` in CSS for
the same reason a Contain view's authored size can't otherwise survive
being summed from children that have none of their own.
Adds Tests/ObjectivelyMVC/View.c (StackView and plain-View fixture
graphs asserting exact post-layout frame values) as a fourth
check_PROGRAMS entry alongside Selector/Style/Stylesheet -- there was
previously no ObjectivelyMVC-View test target at all.
Mirrors ObjectivelyMVC-Style: a command-line-tool target building
Tests/ObjectivelyMVC/View.c against the same frameworks, plus a shared
scheme, so the new View check_PROGRAMS test can run from Xcode like
Selector/Style/Stylesheet already could.
CopilotAI lite review requested due to automatic review settings September 1, 2026 01:11

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The new constraint pipeline introduces at least two confirmed behavior bugs (standalone relayout can force an axis to 0, and Panel’s contentView height constraint is ignored), which can cause incorrect layout at runtime.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Introduces a constraint-based sizing pass (ViewConstraint, sizeThatSatisfies, layoutWithConstraint) to make layout idempotent and to resolve historical ambiguity between Contain and Fill, while also fixing StackView measurement staleness by measuring subviews fresh per layout pass.

Changes:

  • Add ViewConstraint + new sizing/layout APIs (sizeThatSatisfies, sizeToSatisfy, layoutWithConstraint, layoutWithSize) and wire them into the default View layout pipeline.
  • Update key containers (e.g., StackView, Panel, CollectionView) to use constraint/size-driven layout entrypoints instead of resize + layoutIfNeeded.
  • Add a new View test suite covering the previously ambiguous and stale measurement scenarios; update CSS to add explicit min-width floors for widgets relying on contain/fill behavior.
File summaries
FileDescription
Tests/ObjectivelyMVC/View.cAdds layout regression tests for contain/fill ambiguity and StackView staleness/idempotency.
Tests/ObjectivelyMVC/Makefile.amRegisters the new View test binary in autotools test runner.
Tests/ObjectivelyMVC/.gitignoreIgnores the new View test executable.
Sources/ObjectivelyMVC/View.hDefines ViewConstraint and documents/declares new sizing/layout APIs.
Sources/ObjectivelyMVC/View.cImplements constraint-based sizing/layout and updates default layout behavior to use it.
Sources/ObjectivelyMVC/TableView.cAdjusts sizeThatFits to avoid returning expensive natural size when not acting as a container.
Sources/ObjectivelyMVC/StackView.cFixes stale measurement by measuring subviews fresh and laying them out via layoutWithSize.
Sources/ObjectivelyMVC/Panel.cSwitches content view layout from direct resize to constraint-based layout.
Sources/ObjectivelyMVC/CollectionView.cUses layoutWithSize for dictated item sizes during layout.
ObjectivelyMVC.xcodeproj/xcshareddata/xcschemes/ObjectivelyMVC-View.xcschemeAdds an Xcode scheme for the new View-focused test/target.
ObjectivelyMVC.xcodeproj/project.pbxprojAdds the new ObjectivelyMVC-View target and related build settings/dependencies.
Assets/stylesheet.css.hUpdates embedded stylesheet bytes/length for CSS changes.
Assets/stylesheet.cssAdds min-width rules (e.g., Slider/TextView) to ensure contain sizing has a non-zero floor.
Review details
  • Files reviewed: 13/13 changed files
  • Comments generated: 4
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +111 to +116
View *contentView = (View *) this->contentView;

const ViewConstraint w = MakeConstraint(ViewConstraintEqual, size.w);
const ViewConstraint h = MakeConstraint(ViewConstraintEqual, size.h);

$(contentView, layoutWithConstraint, w, h);
Comment on lines +1112 to +1119
ViewConstraint w, h;
if (self->frame.w || self->frame.h) {
w = MakeConstraint(ViewConstraintEqual, self->frame.w);
h = MakeConstraint(ViewConstraintEqual, self->frame.h);
} else {
w = MakeConstraint(ViewConstraintUnspecified, 0);
h = MakeConstraint(ViewConstraintUnspecified, 0);
}
Comment on lines +842 to +844
* @remarks This is the shared tail of View::layoutIfNeeded: resolve self's size via
* View::sizeToSatisfy if self is a container, then View::layoutSubviews, then clear
* `needsLayout`. It exists so a caller that already knows the correct ViewConstraint for a
Comment on lines +829 to +831
* @remarks The default implementation resolves each subview's size via View::layoutWithConstraint,
* offering `Exact` for a `ViewAutoresizingWidth`/`Height` subview (since this View's bounds are
* already final) or `Unspecified` otherwise (so the subview sizes itself from its own content).
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@jdolan
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Introduce ViewConstraint and layoutWithConstraint by jdolan · Pull Request #45 · jdolan/ObjectivelyMVC · GitHub
Skip to content

Introduce ViewConstraint and layoutWithConstraint - #45

Open
jdolan wants to merge 6 commits into
mainfrom
layout-measure-arrange
Open

Introduce ViewConstraint and layoutWithConstraint#45
jdolan wants to merge 6 commits into
mainfrom
layout-measure-arrange

Conversation

@jdolan

Copy link
Copy Markdown
Owner

ObjectivelyMVC's layout engine has had some fundamental issues for years. Namely, there are two ViewAutoresizing bits that pull in opposite directions: Contain and Fill. Scenarios where a child specified Fill and a parent specified Contain are ambiguous.

There was also a staleness bug in StackView that did not re-compute remaining size while laying out subviews. These sorts of bugs led to layoutSubviews not being idempotent.

This branch aims to fix these problems by introducing the concept of constraints, where a parent negotiates each child's size before proceeding to layout. It allows bottom-up size requests to propagate upward before top-down layout clobbers them.

StackView::layoutSubviews sized and positioned siblings from each
subview's leftover frame.w/h from the previous layout pass, so a
Contain-sized child's real intrinsic size was only known after it had
already been used (stale) to size and position its siblings within
the same pass -- no fixed point was guaranteed, and a Fill child
inside a Contain ancestor was flatly unresolvable, since the ancestor
can't hand a real bound to a child it's still sizing itself from.
Split layout into a bottom-up View::sizeThatSatisfies pass (what size
do you want, given this ViewConstraint) that runs to completion for a
dirty subtree before any frame is committed, followed by the existing
top-down View::layoutSubviews arrange pass. A Fill child measured
while its Contain ancestor is itself unresolved receives
ViewConstraintUnspecified and degrades to its own intrinsic size,
rather than reading a stale number; it only actually fills once an
ancestor hands it ViewConstraintEqual, during arrange.
View::sizeToSatisfy joins sizeToContain/sizeToFit/sizeToFill as the
resize-and-mark-dirty sibling of sizeThatSatisfies. layoutIfNeeded,
base layoutSubviews's arrange loop, StackView's arrange loop, and
Panel's contentView bypass each used to hand-duplicate a "resize (or
sizeToSatisfy), clearWarnings, layoutSubviews, needsLayout = false"
sequence, with a comment at each explaining why layoutIfNeeded itself
couldn't be called instead (it would re-derive its own, weaker guess
at the applicable constraint instead of using the one the caller
already resolved). That shared tail is now View::layoutWithConstraint
("what size do you want, given this ViewConstraint" -- resolved via
sizeToSatisfy, honoring ViewAutoresizingWidth/Height per axis) and
View::layoutWithSize ("this is your size" -- applied verbatim, no
negotiation, for StackView's and CollectionView's own distribution
math, which overrides a subview's size regardless of its own
autoresizing bits). needsLayout = false is now written in exactly one
place in the codebase instead of four, and layoutWithConstraint always
resolves regardless of isContainer -- gating it on isContainer (an
earlier version of this same change) skipped resizing entirely for a
Fill-only, non-container View, such as Slider's `bar`.
The `constrainedSize` field this went through along the way -- cached
by sizeThatSatisfies, consumed by StackView -- is gone; sizeThatSatisfies
is a pure function of a View's own state, and StackView holds its
subviews' measured sizes in a plain local array scoped to its own
layoutSubviews call, not on the View instances themselves.
Fixes found only by running Examples/Hello against real widgets, not
caught by the synthetic fixture tests: sizeThatSatisfies must skip a
Contain view whose own sizeThatFits override (Text, TableView, Select)
is meaningful only when that view opted into Contain/Fit itself
(TableView.c), a StackView subview without a matching autoresizing bit
must not have its distribution-computed size re-derived through a
constraint it never agreed to honor, and a View re-laid-out standalone
(e.g. a selected TableRowView, whose style rebind marks only itself
dirty) must trust its own established frame as exact rather than
merely an upper bound, or it shrinks back to its unconstrained content
size. Slider and TextView needed an explicit `min-width` in CSS for
the same reason a Contain view's authored size can't otherwise survive
being summed from children that have none of their own.
Adds Tests/ObjectivelyMVC/View.c (StackView and plain-View fixture
graphs asserting exact post-layout frame values) as a fourth
check_PROGRAMS entry alongside Selector/Style/Stylesheet -- there was
previously no ObjectivelyMVC-View test target at all.
Mirrors ObjectivelyMVC-Style: a command-line-tool target building
Tests/ObjectivelyMVC/View.c against the same frameworks, plus a shared
scheme, so the new View check_PROGRAMS test can run from Xcode like
Selector/Style/Stylesheet already could.
CopilotAI lite review requested due to automatic review settings September 1, 2026 01:11

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The new constraint pipeline introduces at least two confirmed behavior bugs (standalone relayout can force an axis to 0, and Panel’s contentView height constraint is ignored), which can cause incorrect layout at runtime.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Introduces a constraint-based sizing pass (ViewConstraint, sizeThatSatisfies, layoutWithConstraint) to make layout idempotent and to resolve historical ambiguity between Contain and Fill, while also fixing StackView measurement staleness by measuring subviews fresh per layout pass.

Changes:

  • Add ViewConstraint + new sizing/layout APIs (sizeThatSatisfies, sizeToSatisfy, layoutWithConstraint, layoutWithSize) and wire them into the default View layout pipeline.
  • Update key containers (e.g., StackView, Panel, CollectionView) to use constraint/size-driven layout entrypoints instead of resize + layoutIfNeeded.
  • Add a new View test suite covering the previously ambiguous and stale measurement scenarios; update CSS to add explicit min-width floors for widgets relying on contain/fill behavior.
File summaries
FileDescription
Tests/ObjectivelyMVC/View.cAdds layout regression tests for contain/fill ambiguity and StackView staleness/idempotency.
Tests/ObjectivelyMVC/Makefile.amRegisters the new View test binary in autotools test runner.
Tests/ObjectivelyMVC/.gitignoreIgnores the new View test executable.
Sources/ObjectivelyMVC/View.hDefines ViewConstraint and documents/declares new sizing/layout APIs.
Sources/ObjectivelyMVC/View.cImplements constraint-based sizing/layout and updates default layout behavior to use it.
Sources/ObjectivelyMVC/TableView.cAdjusts sizeThatFits to avoid returning expensive natural size when not acting as a container.
Sources/ObjectivelyMVC/StackView.cFixes stale measurement by measuring subviews fresh and laying them out via layoutWithSize.
Sources/ObjectivelyMVC/Panel.cSwitches content view layout from direct resize to constraint-based layout.
Sources/ObjectivelyMVC/CollectionView.cUses layoutWithSize for dictated item sizes during layout.
ObjectivelyMVC.xcodeproj/xcshareddata/xcschemes/ObjectivelyMVC-View.xcschemeAdds an Xcode scheme for the new View-focused test/target.
ObjectivelyMVC.xcodeproj/project.pbxprojAdds the new ObjectivelyMVC-View target and related build settings/dependencies.
Assets/stylesheet.css.hUpdates embedded stylesheet bytes/length for CSS changes.
Assets/stylesheet.cssAdds min-width rules (e.g., Slider/TextView) to ensure contain sizing has a non-zero floor.
Review details
  • Files reviewed: 13/13 changed files
  • Comments generated: 4
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +111 to +116
View *contentView = (View *) this->contentView;

const ViewConstraint w = MakeConstraint(ViewConstraintEqual, size.w);
const ViewConstraint h = MakeConstraint(ViewConstraintEqual, size.h);

$(contentView, layoutWithConstraint, w, h);
Comment on lines +1112 to +1119
ViewConstraint w, h;
if (self->frame.w || self->frame.h) {
w = MakeConstraint(ViewConstraintEqual, self->frame.w);
h = MakeConstraint(ViewConstraintEqual, self->frame.h);
} else {
w = MakeConstraint(ViewConstraintUnspecified, 0);
h = MakeConstraint(ViewConstraintUnspecified, 0);
}
Comment on lines +842 to +844
* @remarks This is the shared tail of View::layoutIfNeeded: resolve self's size via
* View::sizeToSatisfy if self is a container, then View::layoutSubviews, then clear
* `needsLayout`. It exists so a caller that already knows the correct ViewConstraint for a
Comment on lines +829 to +831
* @remarks The default implementation resolves each subview's size via View::layoutWithConstraint,
* offering `Exact` for a `ViewAutoresizingWidth`/`Height` subview (since this View's bounds are
* already final) or `Unspecified` otherwise (so the subview sizes itself from its own content).
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@jdolan
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' Introduce ViewConstraint and layoutWithConstraint by jdolan · Pull Request #45 · jdolan/ObjectivelyMVC · GitHub
Skip to content

Introduce ViewConstraint and layoutWithConstraint - #45

Open
jdolan wants to merge 6 commits into
mainfrom
layout-measure-arrange
Open

Introduce ViewConstraint and layoutWithConstraint#45
jdolan wants to merge 6 commits into
mainfrom
layout-measure-arrange

Conversation

@jdolan

Copy link
Copy Markdown
Owner

ObjectivelyMVC's layout engine has had some fundamental issues for years. Namely, there are two ViewAutoresizing bits that pull in opposite directions: Contain and Fill. Scenarios where a child specified Fill and a parent specified Contain are ambiguous.

There was also a staleness bug in StackView that did not re-compute remaining size while laying out subviews. These sorts of bugs led to layoutSubviews not being idempotent.

This branch aims to fix these problems by introducing the concept of constraints, where a parent negotiates each child's size before proceeding to layout. It allows bottom-up size requests to propagate upward before top-down layout clobbers them.

StackView::layoutSubviews sized and positioned siblings from each
subview's leftover frame.w/h from the previous layout pass, so a
Contain-sized child's real intrinsic size was only known after it had
already been used (stale) to size and position its siblings within
the same pass -- no fixed point was guaranteed, and a Fill child
inside a Contain ancestor was flatly unresolvable, since the ancestor
can't hand a real bound to a child it's still sizing itself from.
Split layout into a bottom-up View::sizeThatSatisfies pass (what size
do you want, given this ViewConstraint) that runs to completion for a
dirty subtree before any frame is committed, followed by the existing
top-down View::layoutSubviews arrange pass. A Fill child measured
while its Contain ancestor is itself unresolved receives
ViewConstraintUnspecified and degrades to its own intrinsic size,
rather than reading a stale number; it only actually fills once an
ancestor hands it ViewConstraintEqual, during arrange.
View::sizeToSatisfy joins sizeToContain/sizeToFit/sizeToFill as the
resize-and-mark-dirty sibling of sizeThatSatisfies. layoutIfNeeded,
base layoutSubviews's arrange loop, StackView's arrange loop, and
Panel's contentView bypass each used to hand-duplicate a "resize (or
sizeToSatisfy), clearWarnings, layoutSubviews, needsLayout = false"
sequence, with a comment at each explaining why layoutIfNeeded itself
couldn't be called instead (it would re-derive its own, weaker guess
at the applicable constraint instead of using the one the caller
already resolved). That shared tail is now View::layoutWithConstraint
("what size do you want, given this ViewConstraint" -- resolved via
sizeToSatisfy, honoring ViewAutoresizingWidth/Height per axis) and
View::layoutWithSize ("this is your size" -- applied verbatim, no
negotiation, for StackView's and CollectionView's own distribution
math, which overrides a subview's size regardless of its own
autoresizing bits). needsLayout = false is now written in exactly one
place in the codebase instead of four, and layoutWithConstraint always
resolves regardless of isContainer -- gating it on isContainer (an
earlier version of this same change) skipped resizing entirely for a
Fill-only, non-container View, such as Slider's `bar`.
The `constrainedSize` field this went through along the way -- cached
by sizeThatSatisfies, consumed by StackView -- is gone; sizeThatSatisfies
is a pure function of a View's own state, and StackView holds its
subviews' measured sizes in a plain local array scoped to its own
layoutSubviews call, not on the View instances themselves.
Fixes found only by running Examples/Hello against real widgets, not
caught by the synthetic fixture tests: sizeThatSatisfies must skip a
Contain view whose own sizeThatFits override (Text, TableView, Select)
is meaningful only when that view opted into Contain/Fit itself
(TableView.c), a StackView subview without a matching autoresizing bit
must not have its distribution-computed size re-derived through a
constraint it never agreed to honor, and a View re-laid-out standalone
(e.g. a selected TableRowView, whose style rebind marks only itself
dirty) must trust its own established frame as exact rather than
merely an upper bound, or it shrinks back to its unconstrained content
size. Slider and TextView needed an explicit `min-width` in CSS for
the same reason a Contain view's authored size can't otherwise survive
being summed from children that have none of their own.
Adds Tests/ObjectivelyMVC/View.c (StackView and plain-View fixture
graphs asserting exact post-layout frame values) as a fourth
check_PROGRAMS entry alongside Selector/Style/Stylesheet -- there was
previously no ObjectivelyMVC-View test target at all.
Mirrors ObjectivelyMVC-Style: a command-line-tool target building
Tests/ObjectivelyMVC/View.c against the same frameworks, plus a shared
scheme, so the new View check_PROGRAMS test can run from Xcode like
Selector/Style/Stylesheet already could.
CopilotAI lite review requested due to automatic review settings September 1, 2026 01:11

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The new constraint pipeline introduces at least two confirmed behavior bugs (standalone relayout can force an axis to 0, and Panel’s contentView height constraint is ignored), which can cause incorrect layout at runtime.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Introduces a constraint-based sizing pass (ViewConstraint, sizeThatSatisfies, layoutWithConstraint) to make layout idempotent and to resolve historical ambiguity between Contain and Fill, while also fixing StackView measurement staleness by measuring subviews fresh per layout pass.

Changes:

  • Add ViewConstraint + new sizing/layout APIs (sizeThatSatisfies, sizeToSatisfy, layoutWithConstraint, layoutWithSize) and wire them into the default View layout pipeline.
  • Update key containers (e.g., StackView, Panel, CollectionView) to use constraint/size-driven layout entrypoints instead of resize + layoutIfNeeded.
  • Add a new View test suite covering the previously ambiguous and stale measurement scenarios; update CSS to add explicit min-width floors for widgets relying on contain/fill behavior.
File summaries
FileDescription
Tests/ObjectivelyMVC/View.cAdds layout regression tests for contain/fill ambiguity and StackView staleness/idempotency.
Tests/ObjectivelyMVC/Makefile.amRegisters the new View test binary in autotools test runner.
Tests/ObjectivelyMVC/.gitignoreIgnores the new View test executable.
Sources/ObjectivelyMVC/View.hDefines ViewConstraint and documents/declares new sizing/layout APIs.
Sources/ObjectivelyMVC/View.cImplements constraint-based sizing/layout and updates default layout behavior to use it.
Sources/ObjectivelyMVC/TableView.cAdjusts sizeThatFits to avoid returning expensive natural size when not acting as a container.
Sources/ObjectivelyMVC/StackView.cFixes stale measurement by measuring subviews fresh and laying them out via layoutWithSize.
Sources/ObjectivelyMVC/Panel.cSwitches content view layout from direct resize to constraint-based layout.
Sources/ObjectivelyMVC/CollectionView.cUses layoutWithSize for dictated item sizes during layout.
ObjectivelyMVC.xcodeproj/xcshareddata/xcschemes/ObjectivelyMVC-View.xcschemeAdds an Xcode scheme for the new View-focused test/target.
ObjectivelyMVC.xcodeproj/project.pbxprojAdds the new ObjectivelyMVC-View target and related build settings/dependencies.
Assets/stylesheet.css.hUpdates embedded stylesheet bytes/length for CSS changes.
Assets/stylesheet.cssAdds min-width rules (e.g., Slider/TextView) to ensure contain sizing has a non-zero floor.
Review details
  • Files reviewed: 13/13 changed files
  • Comments generated: 4
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +111 to +116
View *contentView = (View *) this->contentView;

const ViewConstraint w = MakeConstraint(ViewConstraintEqual, size.w);
const ViewConstraint h = MakeConstraint(ViewConstraintEqual, size.h);

$(contentView, layoutWithConstraint, w, h);
Comment on lines +1112 to +1119
ViewConstraint w, h;
if (self->frame.w || self->frame.h) {
w = MakeConstraint(ViewConstraintEqual, self->frame.w);
h = MakeConstraint(ViewConstraintEqual, self->frame.h);
} else {
w = MakeConstraint(ViewConstraintUnspecified, 0);
h = MakeConstraint(ViewConstraintUnspecified, 0);
}
Comment on lines +842 to +844
* @remarks This is the shared tail of View::layoutIfNeeded: resolve self's size via
* View::sizeToSatisfy if self is a container, then View::layoutSubviews, then clear
* `needsLayout`. It exists so a caller that already knows the correct ViewConstraint for a
Comment on lines +829 to +831
* @remarks The default implementation resolves each subview's size via View::layoutWithConstraint,
* offering `Exact` for a `ViewAutoresizingWidth`/`Height` subview (since this View's bounds are
* already final) or `Unspecified` otherwise (so the subview sizes itself from its own content).
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@jdolan
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Introduce ViewConstraint and layoutWithConstraint by jdolan · Pull Request #45 · jdolan/ObjectivelyMVC · GitHub
Skip to content

Introduce ViewConstraint and layoutWithConstraint - #45

Open
jdolan wants to merge 6 commits into
mainfrom
layout-measure-arrange
Open

Introduce ViewConstraint and layoutWithConstraint#45
jdolan wants to merge 6 commits into
mainfrom
layout-measure-arrange

Conversation

@jdolan

Copy link
Copy Markdown
Owner

ObjectivelyMVC's layout engine has had some fundamental issues for years. Namely, there are two ViewAutoresizing bits that pull in opposite directions: Contain and Fill. Scenarios where a child specified Fill and a parent specified Contain are ambiguous.

There was also a staleness bug in StackView that did not re-compute remaining size while laying out subviews. These sorts of bugs led to layoutSubviews not being idempotent.

This branch aims to fix these problems by introducing the concept of constraints, where a parent negotiates each child's size before proceeding to layout. It allows bottom-up size requests to propagate upward before top-down layout clobbers them.

StackView::layoutSubviews sized and positioned siblings from each
subview's leftover frame.w/h from the previous layout pass, so a
Contain-sized child's real intrinsic size was only known after it had
already been used (stale) to size and position its siblings within
the same pass -- no fixed point was guaranteed, and a Fill child
inside a Contain ancestor was flatly unresolvable, since the ancestor
can't hand a real bound to a child it's still sizing itself from.
Split layout into a bottom-up View::sizeThatSatisfies pass (what size
do you want, given this ViewConstraint) that runs to completion for a
dirty subtree before any frame is committed, followed by the existing
top-down View::layoutSubviews arrange pass. A Fill child measured
while its Contain ancestor is itself unresolved receives
ViewConstraintUnspecified and degrades to its own intrinsic size,
rather than reading a stale number; it only actually fills once an
ancestor hands it ViewConstraintEqual, during arrange.
View::sizeToSatisfy joins sizeToContain/sizeToFit/sizeToFill as the
resize-and-mark-dirty sibling of sizeThatSatisfies. layoutIfNeeded,
base layoutSubviews's arrange loop, StackView's arrange loop, and
Panel's contentView bypass each used to hand-duplicate a "resize (or
sizeToSatisfy), clearWarnings, layoutSubviews, needsLayout = false"
sequence, with a comment at each explaining why layoutIfNeeded itself
couldn't be called instead (it would re-derive its own, weaker guess
at the applicable constraint instead of using the one the caller
already resolved). That shared tail is now View::layoutWithConstraint
("what size do you want, given this ViewConstraint" -- resolved via
sizeToSatisfy, honoring ViewAutoresizingWidth/Height per axis) and
View::layoutWithSize ("this is your size" -- applied verbatim, no
negotiation, for StackView's and CollectionView's own distribution
math, which overrides a subview's size regardless of its own
autoresizing bits). needsLayout = false is now written in exactly one
place in the codebase instead of four, and layoutWithConstraint always
resolves regardless of isContainer -- gating it on isContainer (an
earlier version of this same change) skipped resizing entirely for a
Fill-only, non-container View, such as Slider's `bar`.
The `constrainedSize` field this went through along the way -- cached
by sizeThatSatisfies, consumed by StackView -- is gone; sizeThatSatisfies
is a pure function of a View's own state, and StackView holds its
subviews' measured sizes in a plain local array scoped to its own
layoutSubviews call, not on the View instances themselves.
Fixes found only by running Examples/Hello against real widgets, not
caught by the synthetic fixture tests: sizeThatSatisfies must skip a
Contain view whose own sizeThatFits override (Text, TableView, Select)
is meaningful only when that view opted into Contain/Fit itself
(TableView.c), a StackView subview without a matching autoresizing bit
must not have its distribution-computed size re-derived through a
constraint it never agreed to honor, and a View re-laid-out standalone
(e.g. a selected TableRowView, whose style rebind marks only itself
dirty) must trust its own established frame as exact rather than
merely an upper bound, or it shrinks back to its unconstrained content
size. Slider and TextView needed an explicit `min-width` in CSS for
the same reason a Contain view's authored size can't otherwise survive
being summed from children that have none of their own.
Adds Tests/ObjectivelyMVC/View.c (StackView and plain-View fixture
graphs asserting exact post-layout frame values) as a fourth
check_PROGRAMS entry alongside Selector/Style/Stylesheet -- there was
previously no ObjectivelyMVC-View test target at all.
Mirrors ObjectivelyMVC-Style: a command-line-tool target building
Tests/ObjectivelyMVC/View.c against the same frameworks, plus a shared
scheme, so the new View check_PROGRAMS test can run from Xcode like
Selector/Style/Stylesheet already could.
CopilotAI lite review requested due to automatic review settings September 1, 2026 01:11

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The new constraint pipeline introduces at least two confirmed behavior bugs (standalone relayout can force an axis to 0, and Panel’s contentView height constraint is ignored), which can cause incorrect layout at runtime.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Introduces a constraint-based sizing pass (ViewConstraint, sizeThatSatisfies, layoutWithConstraint) to make layout idempotent and to resolve historical ambiguity between Contain and Fill, while also fixing StackView measurement staleness by measuring subviews fresh per layout pass.

Changes:

  • Add ViewConstraint + new sizing/layout APIs (sizeThatSatisfies, sizeToSatisfy, layoutWithConstraint, layoutWithSize) and wire them into the default View layout pipeline.
  • Update key containers (e.g., StackView, Panel, CollectionView) to use constraint/size-driven layout entrypoints instead of resize + layoutIfNeeded.
  • Add a new View test suite covering the previously ambiguous and stale measurement scenarios; update CSS to add explicit min-width floors for widgets relying on contain/fill behavior.
File summaries
FileDescription
Tests/ObjectivelyMVC/View.cAdds layout regression tests for contain/fill ambiguity and StackView staleness/idempotency.
Tests/ObjectivelyMVC/Makefile.amRegisters the new View test binary in autotools test runner.
Tests/ObjectivelyMVC/.gitignoreIgnores the new View test executable.
Sources/ObjectivelyMVC/View.hDefines ViewConstraint and documents/declares new sizing/layout APIs.
Sources/ObjectivelyMVC/View.cImplements constraint-based sizing/layout and updates default layout behavior to use it.
Sources/ObjectivelyMVC/TableView.cAdjusts sizeThatFits to avoid returning expensive natural size when not acting as a container.
Sources/ObjectivelyMVC/StackView.cFixes stale measurement by measuring subviews fresh and laying them out via layoutWithSize.
Sources/ObjectivelyMVC/Panel.cSwitches content view layout from direct resize to constraint-based layout.
Sources/ObjectivelyMVC/CollectionView.cUses layoutWithSize for dictated item sizes during layout.
ObjectivelyMVC.xcodeproj/xcshareddata/xcschemes/ObjectivelyMVC-View.xcschemeAdds an Xcode scheme for the new View-focused test/target.
ObjectivelyMVC.xcodeproj/project.pbxprojAdds the new ObjectivelyMVC-View target and related build settings/dependencies.
Assets/stylesheet.css.hUpdates embedded stylesheet bytes/length for CSS changes.
Assets/stylesheet.cssAdds min-width rules (e.g., Slider/TextView) to ensure contain sizing has a non-zero floor.
Review details
  • Files reviewed: 13/13 changed files
  • Comments generated: 4
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +111 to +116
View *contentView = (View *) this->contentView;

const ViewConstraint w = MakeConstraint(ViewConstraintEqual, size.w);
const ViewConstraint h = MakeConstraint(ViewConstraintEqual, size.h);

$(contentView, layoutWithConstraint, w, h);
Comment on lines +1112 to +1119
ViewConstraint w, h;
if (self->frame.w || self->frame.h) {
w = MakeConstraint(ViewConstraintEqual, self->frame.w);
h = MakeConstraint(ViewConstraintEqual, self->frame.h);
} else {
w = MakeConstraint(ViewConstraintUnspecified, 0);
h = MakeConstraint(ViewConstraintUnspecified, 0);
}
Comment on lines +842 to +844
* @remarks This is the shared tail of View::layoutIfNeeded: resolve self's size via
* View::sizeToSatisfy if self is a container, then View::layoutSubviews, then clear
* `needsLayout`. It exists so a caller that already knows the correct ViewConstraint for a
Comment on lines +829 to +831
* @remarks The default implementation resolves each subview's size via View::layoutWithConstraint,
* offering `Exact` for a `ViewAutoresizingWidth`/`Height` subview (since this View's bounds are
* already final) or `Unspecified` otherwise (so the subview sizes itself from its own content).
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@jdolan
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Introduce ViewConstraint and layoutWithConstraint by jdolan · Pull Request #45 · jdolan/ObjectivelyMVC · GitHub
Skip to content

Introduce ViewConstraint and layoutWithConstraint - #45

Open
jdolan wants to merge 6 commits into
mainfrom
layout-measure-arrange
Open

Introduce ViewConstraint and layoutWithConstraint#45
jdolan wants to merge 6 commits into
mainfrom
layout-measure-arrange

Conversation

@jdolan

Copy link
Copy Markdown
Owner

ObjectivelyMVC's layout engine has had some fundamental issues for years. Namely, there are two ViewAutoresizing bits that pull in opposite directions: Contain and Fill. Scenarios where a child specified Fill and a parent specified Contain are ambiguous.

There was also a staleness bug in StackView that did not re-compute remaining size while laying out subviews. These sorts of bugs led to layoutSubviews not being idempotent.

This branch aims to fix these problems by introducing the concept of constraints, where a parent negotiates each child's size before proceeding to layout. It allows bottom-up size requests to propagate upward before top-down layout clobbers them.

StackView::layoutSubviews sized and positioned siblings from each
subview's leftover frame.w/h from the previous layout pass, so a
Contain-sized child's real intrinsic size was only known after it had
already been used (stale) to size and position its siblings within
the same pass -- no fixed point was guaranteed, and a Fill child
inside a Contain ancestor was flatly unresolvable, since the ancestor
can't hand a real bound to a child it's still sizing itself from.
Split layout into a bottom-up View::sizeThatSatisfies pass (what size
do you want, given this ViewConstraint) that runs to completion for a
dirty subtree before any frame is committed, followed by the existing
top-down View::layoutSubviews arrange pass. A Fill child measured
while its Contain ancestor is itself unresolved receives
ViewConstraintUnspecified and degrades to its own intrinsic size,
rather than reading a stale number; it only actually fills once an
ancestor hands it ViewConstraintEqual, during arrange.
View::sizeToSatisfy joins sizeToContain/sizeToFit/sizeToFill as the
resize-and-mark-dirty sibling of sizeThatSatisfies. layoutIfNeeded,
base layoutSubviews's arrange loop, StackView's arrange loop, and
Panel's contentView bypass each used to hand-duplicate a "resize (or
sizeToSatisfy), clearWarnings, layoutSubviews, needsLayout = false"
sequence, with a comment at each explaining why layoutIfNeeded itself
couldn't be called instead (it would re-derive its own, weaker guess
at the applicable constraint instead of using the one the caller
already resolved). That shared tail is now View::layoutWithConstraint
("what size do you want, given this ViewConstraint" -- resolved via
sizeToSatisfy, honoring ViewAutoresizingWidth/Height per axis) and
View::layoutWithSize ("this is your size" -- applied verbatim, no
negotiation, for StackView's and CollectionView's own distribution
math, which overrides a subview's size regardless of its own
autoresizing bits). needsLayout = false is now written in exactly one
place in the codebase instead of four, and layoutWithConstraint always
resolves regardless of isContainer -- gating it on isContainer (an
earlier version of this same change) skipped resizing entirely for a
Fill-only, non-container View, such as Slider's `bar`.
The `constrainedSize` field this went through along the way -- cached
by sizeThatSatisfies, consumed by StackView -- is gone; sizeThatSatisfies
is a pure function of a View's own state, and StackView holds its
subviews' measured sizes in a plain local array scoped to its own
layoutSubviews call, not on the View instances themselves.
Fixes found only by running Examples/Hello against real widgets, not
caught by the synthetic fixture tests: sizeThatSatisfies must skip a
Contain view whose own sizeThatFits override (Text, TableView, Select)
is meaningful only when that view opted into Contain/Fit itself
(TableView.c), a StackView subview without a matching autoresizing bit
must not have its distribution-computed size re-derived through a
constraint it never agreed to honor, and a View re-laid-out standalone
(e.g. a selected TableRowView, whose style rebind marks only itself
dirty) must trust its own established frame as exact rather than
merely an upper bound, or it shrinks back to its unconstrained content
size. Slider and TextView needed an explicit `min-width` in CSS for
the same reason a Contain view's authored size can't otherwise survive
being summed from children that have none of their own.
Adds Tests/ObjectivelyMVC/View.c (StackView and plain-View fixture
graphs asserting exact post-layout frame values) as a fourth
check_PROGRAMS entry alongside Selector/Style/Stylesheet -- there was
previously no ObjectivelyMVC-View test target at all.
Mirrors ObjectivelyMVC-Style: a command-line-tool target building
Tests/ObjectivelyMVC/View.c against the same frameworks, plus a shared
scheme, so the new View check_PROGRAMS test can run from Xcode like
Selector/Style/Stylesheet already could.
CopilotAI lite review requested due to automatic review settings September 1, 2026 01:11

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The new constraint pipeline introduces at least two confirmed behavior bugs (standalone relayout can force an axis to 0, and Panel’s contentView height constraint is ignored), which can cause incorrect layout at runtime.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Introduces a constraint-based sizing pass (ViewConstraint, sizeThatSatisfies, layoutWithConstraint) to make layout idempotent and to resolve historical ambiguity between Contain and Fill, while also fixing StackView measurement staleness by measuring subviews fresh per layout pass.

Changes:

  • Add ViewConstraint + new sizing/layout APIs (sizeThatSatisfies, sizeToSatisfy, layoutWithConstraint, layoutWithSize) and wire them into the default View layout pipeline.
  • Update key containers (e.g., StackView, Panel, CollectionView) to use constraint/size-driven layout entrypoints instead of resize + layoutIfNeeded.
  • Add a new View test suite covering the previously ambiguous and stale measurement scenarios; update CSS to add explicit min-width floors for widgets relying on contain/fill behavior.
File summaries
FileDescription
Tests/ObjectivelyMVC/View.cAdds layout regression tests for contain/fill ambiguity and StackView staleness/idempotency.
Tests/ObjectivelyMVC/Makefile.amRegisters the new View test binary in autotools test runner.
Tests/ObjectivelyMVC/.gitignoreIgnores the new View test executable.
Sources/ObjectivelyMVC/View.hDefines ViewConstraint and documents/declares new sizing/layout APIs.
Sources/ObjectivelyMVC/View.cImplements constraint-based sizing/layout and updates default layout behavior to use it.
Sources/ObjectivelyMVC/TableView.cAdjusts sizeThatFits to avoid returning expensive natural size when not acting as a container.
Sources/ObjectivelyMVC/StackView.cFixes stale measurement by measuring subviews fresh and laying them out via layoutWithSize.
Sources/ObjectivelyMVC/Panel.cSwitches content view layout from direct resize to constraint-based layout.
Sources/ObjectivelyMVC/CollectionView.cUses layoutWithSize for dictated item sizes during layout.
ObjectivelyMVC.xcodeproj/xcshareddata/xcschemes/ObjectivelyMVC-View.xcschemeAdds an Xcode scheme for the new View-focused test/target.
ObjectivelyMVC.xcodeproj/project.pbxprojAdds the new ObjectivelyMVC-View target and related build settings/dependencies.
Assets/stylesheet.css.hUpdates embedded stylesheet bytes/length for CSS changes.
Assets/stylesheet.cssAdds min-width rules (e.g., Slider/TextView) to ensure contain sizing has a non-zero floor.
Review details
  • Files reviewed: 13/13 changed files
  • Comments generated: 4
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +111 to +116
View *contentView = (View *) this->contentView;

const ViewConstraint w = MakeConstraint(ViewConstraintEqual, size.w);
const ViewConstraint h = MakeConstraint(ViewConstraintEqual, size.h);

$(contentView, layoutWithConstraint, w, h);
Comment on lines +1112 to +1119
ViewConstraint w, h;
if (self->frame.w || self->frame.h) {
w = MakeConstraint(ViewConstraintEqual, self->frame.w);
h = MakeConstraint(ViewConstraintEqual, self->frame.h);
} else {
w = MakeConstraint(ViewConstraintUnspecified, 0);
h = MakeConstraint(ViewConstraintUnspecified, 0);
}
Comment on lines +842 to +844
* @remarks This is the shared tail of View::layoutIfNeeded: resolve self's size via
* View::sizeToSatisfy if self is a container, then View::layoutSubviews, then clear
* `needsLayout`. It exists so a caller that already knows the correct ViewConstraint for a
Comment on lines +829 to +831
* @remarks The default implementation resolves each subview's size via View::layoutWithConstraint,
* offering `Exact` for a `ViewAutoresizingWidth`/`Height` subview (since this View's bounds are
* already final) or `Unspecified` otherwise (so the subview sizes itself from its own content).
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@jdolan
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); Introduce ViewConstraint and layoutWithConstraint by jdolan · Pull Request #45 · jdolan/ObjectivelyMVC · GitHub
Skip to content

Introduce ViewConstraint and layoutWithConstraint - #45

Open
jdolan wants to merge 6 commits into
mainfrom
layout-measure-arrange
Open

Introduce ViewConstraint and layoutWithConstraint#45
jdolan wants to merge 6 commits into
mainfrom
layout-measure-arrange

Conversation

@jdolan

Copy link
Copy Markdown
Owner

ObjectivelyMVC's layout engine has had some fundamental issues for years. Namely, there are two ViewAutoresizing bits that pull in opposite directions: Contain and Fill. Scenarios where a child specified Fill and a parent specified Contain are ambiguous.

There was also a staleness bug in StackView that did not re-compute remaining size while laying out subviews. These sorts of bugs led to layoutSubviews not being idempotent.

This branch aims to fix these problems by introducing the concept of constraints, where a parent negotiates each child's size before proceeding to layout. It allows bottom-up size requests to propagate upward before top-down layout clobbers them.

StackView::layoutSubviews sized and positioned siblings from each
subview's leftover frame.w/h from the previous layout pass, so a
Contain-sized child's real intrinsic size was only known after it had
already been used (stale) to size and position its siblings within
the same pass -- no fixed point was guaranteed, and a Fill child
inside a Contain ancestor was flatly unresolvable, since the ancestor
can't hand a real bound to a child it's still sizing itself from.
Split layout into a bottom-up View::sizeThatSatisfies pass (what size
do you want, given this ViewConstraint) that runs to completion for a
dirty subtree before any frame is committed, followed by the existing
top-down View::layoutSubviews arrange pass. A Fill child measured
while its Contain ancestor is itself unresolved receives
ViewConstraintUnspecified and degrades to its own intrinsic size,
rather than reading a stale number; it only actually fills once an
ancestor hands it ViewConstraintEqual, during arrange.
View::sizeToSatisfy joins sizeToContain/sizeToFit/sizeToFill as the
resize-and-mark-dirty sibling of sizeThatSatisfies. layoutIfNeeded,
base layoutSubviews's arrange loop, StackView's arrange loop, and
Panel's contentView bypass each used to hand-duplicate a "resize (or
sizeToSatisfy), clearWarnings, layoutSubviews, needsLayout = false"
sequence, with a comment at each explaining why layoutIfNeeded itself
couldn't be called instead (it would re-derive its own, weaker guess
at the applicable constraint instead of using the one the caller
already resolved). That shared tail is now View::layoutWithConstraint
("what size do you want, given this ViewConstraint" -- resolved via
sizeToSatisfy, honoring ViewAutoresizingWidth/Height per axis) and
View::layoutWithSize ("this is your size" -- applied verbatim, no
negotiation, for StackView's and CollectionView's own distribution
math, which overrides a subview's size regardless of its own
autoresizing bits). needsLayout = false is now written in exactly one
place in the codebase instead of four, and layoutWithConstraint always
resolves regardless of isContainer -- gating it on isContainer (an
earlier version of this same change) skipped resizing entirely for a
Fill-only, non-container View, such as Slider's `bar`.
The `constrainedSize` field this went through along the way -- cached
by sizeThatSatisfies, consumed by StackView -- is gone; sizeThatSatisfies
is a pure function of a View's own state, and StackView holds its
subviews' measured sizes in a plain local array scoped to its own
layoutSubviews call, not on the View instances themselves.
Fixes found only by running Examples/Hello against real widgets, not
caught by the synthetic fixture tests: sizeThatSatisfies must skip a
Contain view whose own sizeThatFits override (Text, TableView, Select)
is meaningful only when that view opted into Contain/Fit itself
(TableView.c), a StackView subview without a matching autoresizing bit
must not have its distribution-computed size re-derived through a
constraint it never agreed to honor, and a View re-laid-out standalone
(e.g. a selected TableRowView, whose style rebind marks only itself
dirty) must trust its own established frame as exact rather than
merely an upper bound, or it shrinks back to its unconstrained content
size. Slider and TextView needed an explicit `min-width` in CSS for
the same reason a Contain view's authored size can't otherwise survive
being summed from children that have none of their own.
Adds Tests/ObjectivelyMVC/View.c (StackView and plain-View fixture
graphs asserting exact post-layout frame values) as a fourth
check_PROGRAMS entry alongside Selector/Style/Stylesheet -- there was
previously no ObjectivelyMVC-View test target at all.
Mirrors ObjectivelyMVC-Style: a command-line-tool target building
Tests/ObjectivelyMVC/View.c against the same frameworks, plus a shared
scheme, so the new View check_PROGRAMS test can run from Xcode like
Selector/Style/Stylesheet already could.
CopilotAI lite review requested due to automatic review settings September 1, 2026 01:11

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The new constraint pipeline introduces at least two confirmed behavior bugs (standalone relayout can force an axis to 0, and Panel’s contentView height constraint is ignored), which can cause incorrect layout at runtime.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Introduces a constraint-based sizing pass (ViewConstraint, sizeThatSatisfies, layoutWithConstraint) to make layout idempotent and to resolve historical ambiguity between Contain and Fill, while also fixing StackView measurement staleness by measuring subviews fresh per layout pass.

Changes:

  • Add ViewConstraint + new sizing/layout APIs (sizeThatSatisfies, sizeToSatisfy, layoutWithConstraint, layoutWithSize) and wire them into the default View layout pipeline.
  • Update key containers (e.g., StackView, Panel, CollectionView) to use constraint/size-driven layout entrypoints instead of resize + layoutIfNeeded.
  • Add a new View test suite covering the previously ambiguous and stale measurement scenarios; update CSS to add explicit min-width floors for widgets relying on contain/fill behavior.
File summaries
FileDescription
Tests/ObjectivelyMVC/View.cAdds layout regression tests for contain/fill ambiguity and StackView staleness/idempotency.
Tests/ObjectivelyMVC/Makefile.amRegisters the new View test binary in autotools test runner.
Tests/ObjectivelyMVC/.gitignoreIgnores the new View test executable.
Sources/ObjectivelyMVC/View.hDefines ViewConstraint and documents/declares new sizing/layout APIs.
Sources/ObjectivelyMVC/View.cImplements constraint-based sizing/layout and updates default layout behavior to use it.
Sources/ObjectivelyMVC/TableView.cAdjusts sizeThatFits to avoid returning expensive natural size when not acting as a container.
Sources/ObjectivelyMVC/StackView.cFixes stale measurement by measuring subviews fresh and laying them out via layoutWithSize.
Sources/ObjectivelyMVC/Panel.cSwitches content view layout from direct resize to constraint-based layout.
Sources/ObjectivelyMVC/CollectionView.cUses layoutWithSize for dictated item sizes during layout.
ObjectivelyMVC.xcodeproj/xcshareddata/xcschemes/ObjectivelyMVC-View.xcschemeAdds an Xcode scheme for the new View-focused test/target.
ObjectivelyMVC.xcodeproj/project.pbxprojAdds the new ObjectivelyMVC-View target and related build settings/dependencies.
Assets/stylesheet.css.hUpdates embedded stylesheet bytes/length for CSS changes.
Assets/stylesheet.cssAdds min-width rules (e.g., Slider/TextView) to ensure contain sizing has a non-zero floor.
Review details
  • Files reviewed: 13/13 changed files
  • Comments generated: 4
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +111 to +116
View *contentView = (View *) this->contentView;

const ViewConstraint w = MakeConstraint(ViewConstraintEqual, size.w);
const ViewConstraint h = MakeConstraint(ViewConstraintEqual, size.h);

$(contentView, layoutWithConstraint, w, h);
Comment on lines +1112 to +1119
ViewConstraint w, h;
if (self->frame.w || self->frame.h) {
w = MakeConstraint(ViewConstraintEqual, self->frame.w);
h = MakeConstraint(ViewConstraintEqual, self->frame.h);
} else {
w = MakeConstraint(ViewConstraintUnspecified, 0);
h = MakeConstraint(ViewConstraintUnspecified, 0);
}
Comment on lines +842 to +844
* @remarks This is the shared tail of View::layoutIfNeeded: resolve self's size via
* View::sizeToSatisfy if self is a container, then View::layoutSubviews, then clear
* `needsLayout`. It exists so a caller that already knows the correct ViewConstraint for a
Comment on lines +829 to +831
* @remarks The default implementation resolves each subview's size via View::layoutWithConstraint,
* offering `Exact` for a `ViewAutoresizingWidth`/`Height` subview (since this View's bounds are
* already final) or `Unspecified` otherwise (so the subview sizes itself from its own content).
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@jdolan