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
6 changes: 2 additions & 4 deletions Sources/MeridianCore/Stores/BrowserStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -730,15 +730,12 @@ public final class BrowserStore: ObservableObject {
guard let space = spaces.first(where: { $0.id == id }) else {
return
}
let shouldPreserveCommandBar = isCommandBarPresented && commandBarMode == .newTab
selectedSpaceID = id
selectedTabID = space.selectedTabID
?? space.favoriteTabIDs.first
?? space.pinnedTabIDs.first
?? space.regularTabIDs.first
if !shouldPreserveCommandBar {
hideCommandBar()
}
hideCommandBar()
refreshActivePageSecurityStatus()
schedulePersistSession()
}
Expand Down Expand Up @@ -780,6 +777,7 @@ public final class BrowserStore: ObservableObject {
updateTab(id) { tab in
tab.lastActiveDate = Date()
}
hideCommandBar()
refreshActivePageSecurityStatus()
persistSession()
}
Expand Down
31 changes: 31 additions & 0 deletions Sources/MeridianCore/Views/Sidebar/SidebarSpacePagerPolicy.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,37 @@ enum SidebarSpacePagerMetrics {
static let creationReturnAnimation: Animation = .smooth(duration: 0.24, extraBounce: 0.08)
}

struct SidebarSpacePagerPageTransitionPresentation: Equatable, Sendable {
let opacity: Double
let scale: CGFloat
let verticalOffset: CGFloat
}

enum SidebarSpacePagerPageTransition {
/// A compositor-only emphasis curve for page contents. Foregrounds remain
/// contrast-correct for their own themes, while the outgoing page recedes
/// and the incoming page resolves without publishing scroll geometry.
static func presentation(
forPhaseValue phaseValue: Double
) -> SidebarSpacePagerPageTransitionPresentation {
guard phaseValue.isFinite else {
return SidebarSpacePagerPageTransitionPresentation(
opacity: 1,
scale: 1,
verticalOffset: 0
)
}

let distance = min(abs(phaseValue), 1)
let easedDistance = distance * distance * (3 - 2 * distance)
return SidebarSpacePagerPageTransitionPresentation(
opacity: 1 - 0.42 * easedDistance,
scale: CGFloat(1 - 0.012 * easedDistance),
verticalOffset: CGFloat(2.5 * easedDistance)
)
}
}

enum SidebarSpacePagerHorizontalIntent: Equatable, Sendable {
case backward
case forward
Expand Down
33 changes: 33 additions & 0 deletions Sources/MeridianCore/Views/Sidebar/SidebarView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,17 @@ public struct SidebarView: View {
}
.accessibilityHidden(true)
}
.simultaneousGesture(sidebarInteractionGesture)
}

private var sidebarInteractionGesture: some Gesture {
DragGesture(minimumDistance: 0)
.onChanged { _ in
guard store.isCommandBarPresented else {
return
}
store.hideCommandBar()
}
}

private var browserControlsHeader: some View {
Expand Down Expand Up @@ -2898,6 +2909,27 @@ private struct WindowReader: NSViewRepresentable {
}
}

private func sidebarSpacePagerPageTransitionEffect(
_ content: EmptyVisualEffect,
phase: ScrollTransitionPhase
) -> some VisualEffect {
let presentation = SidebarSpacePagerPageTransition.presentation(
forPhaseValue: phase.value
)
return content
.opacity(presentation.opacity)
.scaleEffect(presentation.scale)
.offset(y: presentation.verticalOffset)
}

private struct SidebarSpacePagerPageTransitionModifier: ViewModifier {
func body(content: Content) -> some View {
content.scrollTransition(.interactive, axis: .horizontal) { content, phase in
sidebarSpacePagerPageTransitionEffect(content, phase: phase)
}
}
}

private struct SidebarSpacePagerView: View {
let snapshot: SidebarSpacePagerSnapshot
let navigationRequest: SidebarSpacePagerNavigationRequest?
Expand Down Expand Up @@ -2959,6 +2991,7 @@ private struct SidebarSpacePagerView: View {
pageView(page)
.id(page.id)
.frame(width: pageWidth, height: proxy.size.height, alignment: .top)
.modifier(SidebarSpacePagerPageTransitionModifier())
// Once horizontal paging owns the gesture, tab rows no
// longer participate in hit testing until the pager is idle.
.allowsHitTesting(!scrollIsActive)
Expand Down
18 changes: 6 additions & 12 deletions Tests/MeridianBrowserTests/BrowserStoreTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -589,7 +589,7 @@ final class BrowserStoreTests: XCTestCase {
XCTAssertEqual(store.commandBarFocusRequest, previousFocusRequest + 2)
}

func testSelectingSpacePreservesNewTabCommandBar() throws {
func testSelectingSpaceClosesNewTabCommandBar() throws {
let store = BrowserStore()
let initialSpaceID = try XCTUnwrap(store.selectedSpaceID)
let targetSpace = store.createSpace(name: "Work")
Expand All @@ -599,23 +599,17 @@ final class BrowserStoreTests: XCTestCase {
store.selectSpace(targetSpace.id)

XCTAssertEqual(store.selectedSpaceID, targetSpace.id)
XCTAssertTrue(store.isCommandBarPresented)
XCTAssertEqual(store.commandBarMode, .newTab)
XCTAssertFalse(store.isCommandBarPresented)
XCTAssertEqual(store.commandBarMode, .address)
}

func testNewTabCommandBarSubmissionUsesSelectedTargetSpaceAfterSwitching() throws {
func testSelectingTabClosesNewTabCommandBar() throws {
let store = BrowserStore()
let initialSpaceID = try XCTUnwrap(store.selectedSpaceID)
let targetSpace = store.createSpace(name: "Work")
store.selectSpace(initialSpaceID)
let selectedTabID = try XCTUnwrap(store.selectedTabID)

store.beginNewTab()
store.selectSpace(targetSpace.id)
store.submitAddressInput("example.com")
store.selectTab(selectedTabID)

let activeTab = try XCTUnwrap(store.activeTab)
XCTAssertEqual(activeTab.parentSpaceID, targetSpace.id)
XCTAssertTrue(store.selectedSpace?.regularTabIDs.contains(activeTab.id) ?? false)
XCTAssertFalse(store.isCommandBarPresented)
XCTAssertEqual(store.commandBarMode, .address)
}
Expand Down
38 changes: 38 additions & 0 deletions Tests/MeridianBrowserTests/SidebarSpacePagerSelectionTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,44 @@ private final class SidebarAddressMorphRendererSpy: SidebarAddressMorphRendering
}

final class SidebarSpacePagerSelectionTests: XCTestCase {
func testPageTransitionKeepsSettledPageExact() {
XCTAssertEqual(
SidebarSpacePagerPageTransition.presentation(forPhaseValue: 0),
SidebarSpacePagerPageTransitionPresentation(
opacity: 1,
scale: 1,
verticalOffset: 0
)
)
XCTAssertEqual(
SidebarSpacePagerPageTransition.presentation(forPhaseValue: .nan),
SidebarSpacePagerPageTransitionPresentation(
opacity: 1,
scale: 1,
verticalOffset: 0
)
)
}

func testPageTransitionCrossfadesSymmetricallyWithoutHidingPages() {
let leading = SidebarSpacePagerPageTransition.presentation(
forPhaseValue: -0.5
)
let trailing = SidebarSpacePagerPageTransition.presentation(
forPhaseValue: 0.5
)
let edge = SidebarSpacePagerPageTransition.presentation(
forPhaseValue: 1
)

XCTAssertEqual(leading, trailing)
XCTAssertEqual(leading.opacity, 0.79, accuracy: 0.0001)
XCTAssertEqual(leading.scale, 0.994, accuracy: 0.0001)
XCTAssertEqual(leading.verticalOffset, 1.25, accuracy: 0.0001)
XCTAssertEqual(edge.opacity, 0.58, accuracy: 0.0001)
XCTAssertGreaterThan(edge.opacity, 0.5)
}

func testSpaceDragPayloadRoundTripsSpaceID() {
let spaceID = UUID()

Expand Down