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
15 changes: 9 additions & 6 deletions Sources/BetterTileCore/AmbientLayoutReconciler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,9 @@ public enum AmbientLayoutTransition: Sendable {
/// The single-window latch fired in any layout mode.
case placeSingleWindow(session: LayoutSession, placement: Placement)
/// Apply the derived Bento placements. A work area requests authoritative
/// settlement after a successful commit and deliberately remains absent
/// from `session.lastWorkArea` until that settlement succeeds.
/// settlement after a successful commit. It waits in
/// `session.pendingWorkArea` and reaches `session.lastWorkArea` only when
/// that settlement succeeds.
case applyLayout(session: LayoutSession, placements: [Placement], settleWorkArea: BTRect?)

public var session: LayoutSession {
Expand Down Expand Up @@ -88,9 +89,9 @@ public struct AmbientLayoutReconciler: Sendable {
var session = original
let before = session.bentoState
let frames = Dictionary(uniqueKeysWithValues: observation.windows.map { ($0.id, $0.frame) })
let workAreaChanged = session.lastWorkArea.map {
!$0.approximatelyEquals(observation.display.visibleFrame, tolerance: 0.5)
} ?? false
let workAreaChanged = [session.lastWorkArea, session.pendingWorkArea].contains { area in
area.map { !$0.approximatelyEquals(observation.display.visibleFrame, tolerance: 0.5) } ?? false
}

var shouldApply = false
if session.mode == .bento, !session.automaticWritesSuspended {
Expand Down Expand Up @@ -170,7 +171,9 @@ public struct AmbientLayoutReconciler: Sendable {
: nil

session.lastObservedFrames = frames
if settleWorkArea == nil {
if let settleWorkArea {
session.pendingWorkArea = settleWorkArea
} else {
session.lastWorkArea = observation.display.visibleFrame
}

Expand Down
8 changes: 7 additions & 1 deletion Sources/BetterTileCore/LayoutSessions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,13 @@ public struct LayoutSession: Hashable, Sendable {
public var excludedFocusWindowIDs: Set<WindowID>
public var bentoReinsertionAnchors: [WindowID: BentoReinsertionAnchor]
public var lastObservedFrames: [WindowID: BTRect]
public var lastWorkArea: BTRect?
/// The work area the layout was last confirmed for.
public var lastWorkArea: BTRect? {
didSet { pendingWorkArea = nil }
}
/// The work area of a layout written but not yet confirmed. The windows
/// already moved for it, so changing back to `lastWorkArea` is a change.
public var pendingWorkArea: BTRect?
public var isBentoInitialized: Bool
public var hasAppliedSingleWindowPlacement: Bool
/// Corroborates a window's disappearance before its pane is given up.
Expand Down
40 changes: 40 additions & 0 deletions Tests/BetterTileCoreTests/AmbientLayoutReconcilerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -616,3 +616,43 @@ func aTemporarilyMissingBentoPeerDoesNotMaximizeTheRemainingWindow(knownMember:
}
#expect(stableSession == thirdSession)
}

/// The work area changes and changes back before the first layout settles,
/// as when a display or Dock change is undone quickly. The windows already
/// moved for the first change, so the change back must lay them out again.
@Test func aWorkAreaThatChangesBackBeforeSettlingIsAppliedAgain() throws {
let windows = [ambientLeft, ambientRight]
let smaller = BTRect(x: 0, y: 0, width: 1_200, height: 720)
let session = LayoutSession(
displayID: ambientDisplayID,
mode: .bento,
bentoState: horizontalPaneState(windows),
windowIDs: Set(windows.map(\.id)),
bentoInsertionOrder: windows.map(\.id),
lastWorkArea: ambientBounds
)
let reconciler = ambientReconciler()
let shrink = reconciler.transition(
session: session,
observation: ambientObservation(
windows: windows,
display: DisplaySnapshot(id: ambientDisplayID, frame: ambientBounds, visibleFrame: smaller)
)
)
guard case let .applyLayout(shrunk, placements, _) = shrink else {
Issue.record("A smaller work area must apply the layout")
return
}
let moved = windows.map { window in
var window = window
window.frame = placements.first { $0.windowID == window.id }?.frame ?? window.frame
return window
}

let back = reconciler.transition(session: shrunk, observation: ambientObservation(windows: moved))
guard case let .applyLayout(_, restored, _) = back else {
Issue.record("The work area changed back, so the windows must be laid out for it again")
return
}
#expect(restored.allSatisfy { $0.frame.maxY == ambientBounds.maxY })
}
Loading