Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -63,20 +63,28 @@ struct AnimatedImageView: View {
// preventing the upscale blur that Image(nsImage:) causes.
let nativeWidth = CGFloat(cgImage.width) / displayScale
let nativeHeight = CGFloat(cgImage.height) / displayScale
// Use definite dimensions to avoid _FlexFrameLayout inside
// LazyVStack cells. Cap both width and height (same logic as
// gifSize) so portrait images are bounded too.
let dimensionScale = min(maxDimension / max(nativeWidth, 1), maxDimension / max(nativeHeight, 1), 1.0)
let cappedWidth = nativeWidth * dimensionScale
let cappedHeight = nativeHeight * dimensionScale
Image(decorative: cgImage, scale: displayScale)
.resizable()
.interpolation(.high)
.aspectRatio(contentMode: .fit)
.frame(
maxWidth: min(nativeWidth, maxDimension),
maxHeight: min(nativeHeight, maxDimension)
)
.frame(width: cappedWidth, height: cappedHeight)
} else if let image = loadedImage {
// Fallback when CGImage extraction fails
// Fallback when CGImage extraction fails. Cap both dimensions
// using definite frame to avoid _FlexFrameLayout.
let size = image.size
let fallbackScale = (size.width > 0 && size.height > 0)
? min(maxDimension / size.width, maxDimension / size.height, 1.0)
: 1.0
Image(nsImage: image)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(maxWidth: maxDimension, maxHeight: maxDimension)
.frame(width: size.width * fallbackScale, height: size.height * fallbackScale)
} else {
VIconView(.image, size: 24)
.foregroundStyle(VColor.contentTertiary)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -141,8 +141,10 @@ struct MessageListView: View {
// In the inverted scroll, short content gravity-pulls to the
// visual bottom. Pin it to the pre-flip bottom (= visual top)
// so the first message always starts at the top of the viewport.
.frame(minHeight: viewportHeight.isFinite ? viewportHeight : nil,
alignment: .bottom)
// Uses Layout protocol instead of .frame(minHeight:alignment:)
// to avoid _FlexFrameLayout's O(n × depth) explicitAlignment
// cascade through the entire LazyVStack subtree.
.bottomAlignedMinHeight(viewportHeight.isFinite ? viewportHeight : nil)
}
.scrollContentBackground(.hidden)
.scrollDisabled(messages.isEmpty && !isSending)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import SwiftUI

/// Ensures content is at least `minHeight` tall, pinning the child to the
/// bottom edge when the child is shorter than the minimum. Drop-in replacement
/// for `.frame(minHeight:alignment: .bottom)` that avoids `_FlexFrameLayout`
/// and its O(n x depth) `explicitAlignment` cascade inside LazyVStack cells.
///
/// `_FlexFrameLayout` resolves `.bottom` alignment by calling
/// `explicitAlignment(.bottom)` on every descendant, which propagates
/// recursively through the entire subtree. This Layout-protocol
/// implementation achieves the same visual result in O(1) by positioning
/// the child via `placeSubviews` — no alignment query cascade.
///
/// Reference: [Layout.explicitAlignment](https://developer.apple.com/documentation/swiftui/layout/explicitalignment(of:in:proposal:subviews:cache:)-8ofeu)
public struct BottomAlignedMinHeightLayout: Layout {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

🚩 V-prefix naming convention not applied to new Design System Layout type

The new BottomAlignedMinHeightLayout struct and .bottomAlignedMinHeight() modifier live in clients/shared/DesignSystem/Modifiers/ but don't use the V prefix. The clients/AGENTS.md rule states: "All design system types — structs, enums, and view modifiers — must use the V prefix." However, the pre-existing WidthCapLayout (clients/shared/DesignSystem/Modifiers/WidthCapLayout.swift:9) and its .widthCap() modifier follow the exact same non-V-prefix pattern, as do other modifiers like PanelBackgroundModifier, CardModifier, ShimmerEffectModifier, PointerCursorModifier. The de facto convention for Layout-protocol implementations and modifier structs in this directory is to omit the V prefix on the struct itself (though some modifier methods like .vCard(), .vShimmer() do use it). The new code is consistent with the established codebase convention, though the team may want to align the AGENTS.md rule with the actual practice or migrate existing types.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

public let minHeight: CGFloat

public init(minHeight: CGFloat) {
self.minHeight = minHeight
}

public func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
guard let child = subviews.first else {
return CGSize(width: proposal.replacingUnspecifiedDimensions().width, height: minHeight)
}
let childSize = child.sizeThatFits(proposal)
return CGSize(
width: childSize.width,
height: max(childSize.height, minHeight)
)
}

public func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) {
guard let child = subviews.first else { return }
// Re-measure with the SAME proposal that sizeThatFits received.
// Using bounds.height would propose the expanded min-height to the
// child, which can return a different size than during measurement
// — causing SwiftUI to detect a layout inconsistency and
// re-evaluate the layout every frame.
let childSize = child.sizeThatFits(proposal)
// Pin child to bottom of bounds (same as alignment: .bottom).
let y = bounds.maxY - childSize.height
child.place(
at: CGPoint(x: bounds.origin.x, y: y),
anchor: .topLeading,
proposal: ProposedViewSize(width: childSize.width, height: childSize.height)
)
}
}

extension View {
/// Applies a minimum height with bottom alignment without creating
/// `_FlexFrameLayout`. When `minHeight` is nil, no constraint is applied.
@ViewBuilder
public func bottomAlignedMinHeight(_ minHeight: CGFloat?) -> some View {
if let minHeight {
BottomAlignedMinHeightLayout(minHeight: minHeight) { self }
} else {
self
}
}
}