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 @@ -627,8 +627,8 @@ public final class GhosttySurfaceHostView: UIView {
private var resolvedBottomSafeAreaInset: CGFloat {
TerminalLetterboxGeometry.resolvedBottomSafeAreaInset(
viewInset: safeAreaInsets.bottom,
windowInset: window?.safeAreaInsets.bottom ?? 0,
capturedInset: capturedBottomSafeAreaInset,
windowInset: window?.safeAreaInsets.bottom,
capturedInset: capturedBottomSafeAreaInset > 0 ? capturedBottomSafeAreaInset : nil,
ancestorInsets: safeAreaAncestorBottomInsets
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1825,21 +1825,21 @@ public final class GhosttySurfaceView: UIView, TerminalSurfaceHosting {
///
/// The surface extends under the bottom safe area (the host applies
/// `ignoresSafeArea(.container, .bottom)`), so when the keyboard is down the
/// always-visible toolbar must clear this much to avoid the home indicator. Reads
/// the view's own inset, falling back to the window's, because `safeAreaInsets`
/// can be zero before the view is on a window.
/// always-visible toolbar must clear this much to avoid the home indicator.
/// The window or captured outer inset owns the reservation: this surface
/// slides for the keyboard, so its local inset changes with presentation.
private var safeAreaInsetsBottom: CGFloat {
TerminalLetterboxGeometry.resolvedBottomSafeAreaInset(
viewInset: safeAreaInsets.bottom,
windowInset: window?.safeAreaInsets.bottom ?? 0,
capturedInset: capturedBottomSafeAreaInset,
windowInset: window?.safeAreaInsets.bottom,
capturedInset: capturedBottomSafeAreaInset > 0 ? capturedBottomSafeAreaInset : nil,
ancestorInsets: safeAreaAncestorBottomInsets
)
}

/// Safe-area value captured outside the SwiftUI subtree that intentionally
/// ignores the terminal's bottom container region. This stays as a
/// fallback: a live view or window inset still wins when UIKit provides it.
/// fallback for the window, ahead of this moving surface's local inset.
private var capturedBottomSafeAreaInset: CGFloat = 0

/// Updates the outer safe-area fallback and immediately re-seats the dock
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,21 +93,21 @@ public struct TerminalLetterboxGeometry {
return CGSize(width: containerW, height: containerH)
}

/// Resolve the bottom safe-area inset, preferring the view's own inset and
/// falling back to the window's when the view inset is zero (it can be zero
/// before the view is on a window, and STALE for one layout pass right after
/// the keyboard hides).
/// Resolve the physical bottom safe area from the stationary outer layout.
///
/// Mirrors `GhosttySurfaceView.safeAreaInsetsBottom`. Factored out so the
/// "do not trust a zero view inset" rule is host-testable: passing a zero
/// (stale) view inset must return the window inset, not zero, so the
/// keyboard-down grid height does not briefly over-extend under the home
/// indicator and then snap back.
/// terminal grid stays independent of keyboard presentation. Sliding the
/// full-height surface changes its local safe area, including nonzero
/// intermediate values. Feeding those values back into the grid reservation
/// resizes and reflows the terminal, which changes the content measurement
/// and moves the surface again. Prefer the window or captured outer inset;
/// the local view is only a fallback before those sources are available.
///
/// - Parameters:
/// - viewInset: The view's `safeAreaInsets.bottom` (may be a stale 0).
/// - viewInset: The moving view's local inset, used only as a fallback.
/// - windowInset: The window's `safeAreaInsets.bottom` (authoritative
/// when the window reports it).
/// when the window reports it). `nil` means unavailable; `.some(0)` is
/// an authoritative zero on devices without a bottom reservation.
/// - capturedInset: A safe-area value captured outside an ignored
/// SwiftUI subtree, when UIKit cannot expose it to the terminal leaf.
/// - ancestorInsets: Safe-area values reported by UIKit ancestors. A
Expand All @@ -117,18 +117,18 @@ public struct TerminalLetterboxGeometry {
/// - Returns: The inset to reserve in points.
public static func resolvedBottomSafeAreaInset(
viewInset: CGFloat,
windowInset: CGFloat,
capturedInset: CGFloat = 0,
windowInset: CGFloat?,
capturedInset: CGFloat? = nil,
ancestorInsets: [CGFloat] = []
) -> CGFloat {
if viewInset > 0 {
return viewInset
if let windowInset {
return max(0, windowInset)
}
if windowInset > 0 {
return windowInset
if let capturedInset {
return max(0, capturedInset)
}
if capturedInset > 0 {
return capturedInset
if viewInset > 0 {
return viewInset
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
// Ancestors may add their own bottom chrome (for example a tab or
// navigation container), so use the smallest positive inset rather
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -241,10 +241,39 @@ struct TerminalLetterboxGeometryTests {
// and let the grid extend under the home indicator, then snap back. The
// resolver must take the window value instead.
#expect(TerminalLetterboxGeometry.resolvedBottomSafeAreaInset(viewInset: 0, windowInset: 34) == 34)
// When the view inset is present it wins (it is the most specific).
// Matching values agree; the local inset remains a pre-window fallback.
#expect(TerminalLetterboxGeometry.resolvedBottomSafeAreaInset(viewInset: 34, windowInset: 34) == 34)
#expect(TerminalLetterboxGeometry.resolvedBottomSafeAreaInset(viewInset: 34, windowInset: nil) == 34)
// Both zero (pre-window-attach) => 0.
#expect(TerminalLetterboxGeometry.resolvedBottomSafeAreaInset(viewInset: 0, windowInset: 0) == 0)
#expect(TerminalLetterboxGeometry.resolvedBottomSafeAreaInset(viewInset: 0, windowInset: nil) == 0)
// A reported zero is authoritative and must not fall through to the
// moving local inset.
#expect(TerminalLetterboxGeometry.resolvedBottomSafeAreaInset(viewInset: 34, windowInset: 0) == 0)
}

@Test("keyboard content movement cannot resize the terminal grid", arguments: [CGFloat(34), 9, 0, 59])
func movingSurfaceKeepsOuterSafeArea(viewInset: CGFloat) {
// The first Codex response moved the full-height surface by 25pt.
// Its local inset became 9pt, then 34pt after the resulting resize,
// alternating the grid between 60 and 62 rows on every frame.
for windowInset: CGFloat in [34, 0] {
let inset = TerminalLetterboxGeometry.resolvedBottomSafeAreaInset(
viewInset: viewInset,
windowInset: windowInset > 0 ? windowInset : nil,
capturedInset: 34,
ancestorInsets: [9, 34]
)
let container = TerminalLetterboxGeometry.terminalContainerSize(
bounds: CGSize(width: 440, height: 956),
composerBandHeight: 52,
toolbarHeight: 36,
bottomSafeAreaInset: inset,
chromeHidden: false,
topContentInset: 120
)
#expect(inset == 34)
#expect(container == CGSize(width: 440, height: 706))
}
}

@Test("resolved safe-area inset recovers the smallest positive ancestor")
Expand All @@ -256,14 +285,14 @@ struct TerminalLetterboxGeometryTests {
#expect(
TerminalLetterboxGeometry.resolvedBottomSafeAreaInset(
viewInset: 0,
windowInset: 0,
windowInset: nil,
ancestorInsets: [83, 34]
) == 34
)
#expect(
TerminalLetterboxGeometry.resolvedBottomSafeAreaInset(
viewInset: 0,
windowInset: 0,
windowInset: nil,
ancestorInsets: [0, -4]
) == 0
)
Expand All @@ -274,7 +303,7 @@ struct TerminalLetterboxGeometryTests {
#expect(
TerminalLetterboxGeometry.resolvedBottomSafeAreaInset(
viewInset: 0,
windowInset: 0,
windowInset: nil,
capturedInset: 34,
ancestorInsets: [83]
) == 34
Expand Down
Loading