The stacked mount reads as one document in view mode — title, body and the thread share a single scroll
Preview, stacked (over/under): title, the rendered body and the comment thread now stack in one continuous document with one scroll, instead of the fixed ≈3:2 split with each pane keeping its own. Edit mode keeps the split unchanged (an editor needs a stable scroll of its own), and the beside mount is untouched. CardBodySurface gains a `scrolls` flag: false switches off the hosted NSScrollView's scroller and elasticity and reports the NSTextView's own height for the proposed width via `sizeThatFits`, the layout-manager height-fit trick CommentBodyView already uses one level up. CardCommentsPane gains an `embeddedProxy`: supplied, it renders the same header, find bar, rows and composer without wrapping them in a second ScrollView, driving scrollTo off the outer document's proxy instead of its own. CardWindowView composes the two behind a new pure predicate, CommentsMount.showsContinuousDocument(mode:), tested in CardCommentsLayoutTests. The continuous↔split swap within stacked mount is a genuine remount of the body pane (two independent scrolls can't become one shared scroll by reconfiguration) — the same accepted cost the raw-source outlet already takes elsewhere in this window. No new animation on that swap, matching this file's existing precedent (the raw-source swap and the beside↔stacked mount switch are both instant cuts today). Decisions recorded on the card's thread, flagged for owner review where they're user-visible. Claude-Session: https://claude.ai/code/session_014PtZdPwqZuqEDLc6wZMtEy
This commit is contained in:
@@ -64,6 +64,21 @@ struct CardBodySurface: NSViewRepresentable {
|
||||
let isTaskToggleEnabled: Bool
|
||||
/// Byte offset and the state the user saw — straight through to `BoardStore.toggleTaskMarker`.
|
||||
let onToggleTask: (Int, Bool) -> Void
|
||||
/// Whether this surface scrolls on its own — `true`, the default, for every mount and mode
|
||||
/// today. `false` is the stacked mount's continuous Preview arrangement
|
||||
/// (`CardWindowView.continuousStackedContent`), where this surface's rendered content is one
|
||||
/// section of a single shared document scroll rather than its own scrolling region: the hosted
|
||||
/// `NSScrollView` loses its scroller and elasticity (the outer `ScrollView` owns wheel/trackpad
|
||||
/// scrolling instead) and `sizeThatFits(_:nsView:context:)` below reports the text's own height
|
||||
/// for whatever width it is proposed, rather than the view answering nothing and falling back to
|
||||
/// a frame nothing constrains.
|
||||
var scrolls: Bool = true
|
||||
|
||||
/// The height a measurement pass lays out into while `scrolls` is `false` — tall enough that no
|
||||
/// card body reaches it, finite so the arithmetic stays well-defined. `CommentBodyView`'s own
|
||||
/// constant, applied one level down through the hosting scroll view rather than straight to the
|
||||
/// represented view.
|
||||
private static let embeddedLayoutCeiling: CGFloat = 100_000
|
||||
|
||||
func makeCoordinator() -> Coordinator {
|
||||
Coordinator()
|
||||
@@ -133,6 +148,17 @@ struct CardBodySurface: NSViewRepresentable {
|
||||
scrollView.autohidesScrollers = true
|
||||
scrollView.drawsBackground = false
|
||||
scrollView.findBarPosition = .aboveContent
|
||||
// **`scrolls == false` switches this scroll view off** — no scroller to grab, no elastic
|
||||
// bounce to fight the outer `ScrollView`'s own wheel/trackpad scrolling, which is what
|
||||
// "embedded in an outer scroll" (the continuous arrangement) actually means at the AppKit
|
||||
// layer. `sizeThatFits` below is the other half: without it this view would still have
|
||||
// nothing to report and would collapse to zero height inside an unbounded `ScrollView`
|
||||
// proposal.
|
||||
if !scrolls {
|
||||
scrollView.hasVerticalScroller = false
|
||||
scrollView.verticalScrollElasticity = .none
|
||||
scrollView.horizontalScrollElasticity = .none
|
||||
}
|
||||
|
||||
context.coordinator.textView = textView
|
||||
context.coordinator.session = session
|
||||
@@ -158,6 +184,33 @@ struct CardBodySurface: NSViewRepresentable {
|
||||
return scrollView
|
||||
}
|
||||
|
||||
/// **The intrinsic height, while `scrolls` is `false`.** `nil` otherwise — the default
|
||||
/// (unimplemented) answer, which lets a scrolling instance keep taking whatever frame its
|
||||
/// `.frame(maxHeight: .infinity)` modifier proposes exactly as it always has.
|
||||
///
|
||||
/// `CommentBodyView.sizeThatFits`'s own trick, one level down through the hosting scroll view:
|
||||
/// the container tracks the text view's own frame width (`widthTracksTextView`), so forcing that
|
||||
/// frame to the proposed width *is* how the width is proposed at all, and `ensureLayout` is not
|
||||
/// optional — `usedRect` only means something once the glyphs are laid, and an unlaid container
|
||||
/// answers a zero-height rect, which would collapse the whole body to nothing.
|
||||
func sizeThatFits(_ proposal: ProposedViewSize, nsView scrollView: NSScrollView, context: Context) -> CGSize? {
|
||||
guard !scrolls else { return nil }
|
||||
guard let textView = scrollView.documentView as? NSTextView,
|
||||
let container = textView.textContainer,
|
||||
let layoutManager = textView.layoutManager
|
||||
else { return nil }
|
||||
guard let width = proposal.width, width > 0, width.isFinite else { return nil }
|
||||
|
||||
textView.frame = NSRect(x: 0, y: 0, width: width, height: Self.embeddedLayoutCeiling)
|
||||
layoutManager.ensureLayout(for: container)
|
||||
let usedHeight = layoutManager.usedRect(for: container).height
|
||||
// `usedRect` is the text alone; the gutter inset on both edges is this surface's own, added
|
||||
// back in rather than folded into the container width above (`textContainerInset` already
|
||||
// does that subtraction for `widthTracksTextView`, so doing it twice here would double it).
|
||||
let totalHeight = usedHeight + textView.textContainerInset.height * 2
|
||||
return CGSize(width: width, height: totalHeight.rounded(.up))
|
||||
}
|
||||
|
||||
func updateNSView(_ scrollView: NSScrollView, context: Context) {
|
||||
let coordinator = context.coordinator
|
||||
coordinator.onToggleTask = onToggleTask
|
||||
|
||||
Reference in New Issue
Block a user