Explorar o código

Fix Schedule and Join Meetings layout and scroll behavior

- Activate empty-state width constraint after adding the view to the stack to avoid NSLayoutException.
- Deactivate and replace main content host edge constraints on each sidebar swap so constraints do not accumulate.
- Schedule cards NSScrollView: avoid stretching the document stack to the clip height; use a flipped NSClipView so short content stays top-aligned instead of bottom-anchored with a large gap above.
- Join Meetings: tighten spacing from the schedule header and date line to the meeting card strip; disable automatic content insets and drop the document bottom pin on the horizontal strip scroll view.

Made-with: Cursor
huzaifahayat12 hai 1 semana
pai
achega
5b1f47985e
Modificáronse 1 ficheiros con 42 adicións e 9 borrados
  1. 42 9
      meetings_app/ViewController.swift

+ 42 - 9
meetings_app/ViewController.swift

@@ -236,6 +236,8 @@ final class ViewController: NSViewController {
236
     private let launchMinContentSize = NSSize(width: 760, height: 600)
236
     private let launchMinContentSize = NSSize(width: 760, height: 600)
237
 
237
 
238
     private var mainContentHost: NSView?
238
     private var mainContentHost: NSView?
239
+    /// Pin constraints for the current page inside `mainContentHost`; deactivated before each swap so relayout never stacks duplicates.
240
+    private var mainContentHostPinConstraints: [NSLayoutConstraint] = []
239
     private var sidebarRowViews: [SidebarPage: NSView] = [:]
241
     private var sidebarRowViews: [SidebarPage: NSView] = [:]
240
     private var selectedSidebarPage: SidebarPage = .joinMeetings
242
     private var selectedSidebarPage: SidebarPage = .joinMeetings
241
     private var selectedZoomJoinMode: ZoomJoinMode = .id
243
     private var selectedZoomJoinMode: ZoomJoinMode = .id
@@ -332,7 +334,13 @@ final class ViewController: NSViewController {
332
     private let schedulePageCardsPerRow: Int = 3
334
     private let schedulePageCardsPerRow: Int = 3
333
     private let schedulePageCardSpacing: CGFloat = 20
335
     private let schedulePageCardSpacing: CGFloat = 20
334
     private let schedulePageCardHeight: CGFloat = 182
336
     private let schedulePageCardHeight: CGFloat = 182
335
-    private let schedulePageStackSpacing: CGFloat = 16
337
+    /// Match `makeJoinMeetingsContent` vertical rhythm between sections.
338
+    private let schedulePageStackSpacing: CGFloat = 14
339
+    /// Tighter gap from header block (title + filters) to the date line below.
340
+    private let schedulePageHeaderToDateSpacing: CGFloat = 10
341
+    /// Join Meetings: gap from “Schedule” row to date heading, and date heading to card strip (keeps cards aligned with the rest of the column).
342
+    private let joinPageScheduleHeaderToDateSpacing: CGFloat = 8
343
+    private let joinPageDateToMeetingCardsSpacing: CGFloat = 8
336
     /// Match Join Meetings main content insets so the top auth/profile bar lines up with page edges.
344
     /// Match Join Meetings main content insets so the top auth/profile bar lines up with page edges.
337
     private let schedulePageLeadingInset: CGFloat = 28
345
     private let schedulePageLeadingInset: CGFloat = 28
338
     private let schedulePageTrailingInset: CGFloat = 28
346
     private let schedulePageTrailingInset: CGFloat = 28
@@ -730,16 +738,19 @@ private extension ViewController {
730
         applyWindowTitle(for: page)
738
         applyWindowTitle(for: page)
731
 
739
 
732
         guard let host = mainContentHost else { return }
740
         guard let host = mainContentHost else { return }
741
+        NSLayoutConstraint.deactivate(mainContentHostPinConstraints)
742
+        mainContentHostPinConstraints.removeAll()
733
         host.subviews.forEach { $0.removeFromSuperview() }
743
         host.subviews.forEach { $0.removeFromSuperview() }
734
         let child = viewForPage(page)
744
         let child = viewForPage(page)
735
         child.translatesAutoresizingMaskIntoConstraints = false
745
         child.translatesAutoresizingMaskIntoConstraints = false
736
         host.addSubview(child)
746
         host.addSubview(child)
737
-        NSLayoutConstraint.activate([
747
+        mainContentHostPinConstraints = [
738
             child.leadingAnchor.constraint(equalTo: host.leadingAnchor),
748
             child.leadingAnchor.constraint(equalTo: host.leadingAnchor),
739
             child.trailingAnchor.constraint(equalTo: host.trailingAnchor),
749
             child.trailingAnchor.constraint(equalTo: host.trailingAnchor),
740
             child.topAnchor.constraint(equalTo: host.topAnchor),
750
             child.topAnchor.constraint(equalTo: host.topAnchor),
741
             child.bottomAnchor.constraint(equalTo: host.bottomAnchor)
751
             child.bottomAnchor.constraint(equalTo: host.bottomAnchor)
742
-        ])
752
+        ]
753
+        NSLayoutConstraint.activate(mainContentHostPinConstraints)
743
     }
754
     }
744
 
755
 
745
     private func showSettingsPopover() {
756
     private func showSettingsPopover() {
@@ -789,6 +800,8 @@ private extension ViewController {
789
         googleAccountPopover?.performClose(nil)
800
         googleAccountPopover?.performClose(nil)
790
         googleAccountPopover = nil
801
         googleAccountPopover = nil
791
 
802
 
803
+        NSLayoutConstraint.deactivate(mainContentHostPinConstraints)
804
+        mainContentHostPinConstraints.removeAll()
792
         mainContentHost = nil
805
         mainContentHost = nil
793
         view.subviews.forEach { $0.removeFromSuperview() }
806
         view.subviews.forEach { $0.removeFromSuperview() }
794
         setupRootView()
807
         setupRootView()
@@ -1713,11 +1726,14 @@ private extension ViewController {
1713
         contentStack.addArrangedSubview(meetJoinSectionRow())
1726
         contentStack.addArrangedSubview(meetJoinSectionRow())
1714
         contentStack.addArrangedSubview(joinActions)
1727
         contentStack.addArrangedSubview(joinActions)
1715
         contentStack.setCustomSpacing(26, after: joinActions)
1728
         contentStack.setCustomSpacing(26, after: joinActions)
1716
-        contentStack.addArrangedSubview(scheduleHeader())
1729
+        let scheduleHeaderView = scheduleHeader()
1730
+        contentStack.addArrangedSubview(scheduleHeaderView)
1731
+        contentStack.setCustomSpacing(joinPageScheduleHeaderToDateSpacing, after: scheduleHeaderView)
1717
 
1732
 
1718
         let dateHeading = textLabel(scheduleInitialHeadingText(), font: typography.dateHeading, color: palette.textSecondary)
1733
         let dateHeading = textLabel(scheduleInitialHeadingText(), font: typography.dateHeading, color: palette.textSecondary)
1719
         scheduleDateHeadingLabel = dateHeading
1734
         scheduleDateHeadingLabel = dateHeading
1720
         contentStack.addArrangedSubview(dateHeading)
1735
         contentStack.addArrangedSubview(dateHeading)
1736
+        contentStack.setCustomSpacing(joinPageDateToMeetingCardsSpacing, after: dateHeading)
1721
 
1737
 
1722
         let cardsRow = scheduleCardsRow(meetings: [])
1738
         let cardsRow = scheduleCardsRow(meetings: [])
1723
         contentStack.addArrangedSubview(cardsRow)
1739
         contentStack.addArrangedSubview(cardsRow)
@@ -1748,22 +1764,31 @@ private extension ViewController {
1748
         contentStack.orientation = .vertical
1764
         contentStack.orientation = .vertical
1749
         contentStack.spacing = schedulePageStackSpacing
1765
         contentStack.spacing = schedulePageStackSpacing
1750
         contentStack.alignment = .width
1766
         contentStack.alignment = .width
1767
+        contentStack.distribution = .fill
1751
 
1768
 
1752
         let header = schedulePageHeader()
1769
         let header = schedulePageHeader()
1770
+        header.setContentHuggingPriority(.required, for: .vertical)
1771
+        header.setContentCompressionResistancePriority(.required, for: .vertical)
1753
         contentStack.addArrangedSubview(header)
1772
         contentStack.addArrangedSubview(header)
1773
+        contentStack.setCustomSpacing(schedulePageHeaderToDateSpacing, after: header)
1754
 
1774
 
1755
         let heading = textLabel(schedulePageInitialHeadingText(), font: typography.dateHeading, color: palette.textSecondary)
1775
         let heading = textLabel(schedulePageInitialHeadingText(), font: typography.dateHeading, color: palette.textSecondary)
1756
         heading.alignment = .left
1776
         heading.alignment = .left
1777
+        heading.setContentHuggingPriority(.required, for: .vertical)
1778
+        heading.setContentCompressionResistancePriority(.required, for: .vertical)
1757
         schedulePageDateHeadingLabel = heading
1779
         schedulePageDateHeadingLabel = heading
1758
         contentStack.addArrangedSubview(heading)
1780
         contentStack.addArrangedSubview(heading)
1759
 
1781
 
1760
         let rangeError = textLabel("", font: NSFont.systemFont(ofSize: 12, weight: .semibold), color: .systemRed)
1782
         let rangeError = textLabel("", font: NSFont.systemFont(ofSize: 12, weight: .semibold), color: .systemRed)
1761
         rangeError.alignment = .left
1783
         rangeError.alignment = .left
1762
         rangeError.isHidden = true
1784
         rangeError.isHidden = true
1785
+        rangeError.setContentHuggingPriority(.required, for: .vertical)
1786
+        rangeError.setContentCompressionResistancePriority(.required, for: .vertical)
1763
         schedulePageRangeErrorLabel = rangeError
1787
         schedulePageRangeErrorLabel = rangeError
1764
         contentStack.addArrangedSubview(rangeError)
1788
         contentStack.addArrangedSubview(rangeError)
1765
 
1789
 
1766
         let cardsContainer = makeSchedulePageCardsContainer()
1790
         let cardsContainer = makeSchedulePageCardsContainer()
1791
+        cardsContainer.setContentHuggingPriority(.defaultLow, for: .vertical)
1767
         contentStack.addArrangedSubview(cardsContainer)
1792
         contentStack.addArrangedSubview(cardsContainer)
1768
 
1793
 
1769
         panel.addSubview(contentStack)
1794
         panel.addSubview(contentStack)
@@ -2672,7 +2697,7 @@ private extension ViewController {
2672
         container.translatesAutoresizingMaskIntoConstraints = false
2697
         container.translatesAutoresizingMaskIntoConstraints = false
2673
         container.userInterfaceLayoutDirection = .leftToRight
2698
         container.userInterfaceLayoutDirection = .leftToRight
2674
         container.orientation = .vertical
2699
         container.orientation = .vertical
2675
-        container.spacing = 14
2700
+        container.spacing = 8
2676
         container.alignment = .width
2701
         container.alignment = .width
2677
 
2702
 
2678
         let titleRow = NSStackView()
2703
         let titleRow = NSStackView()
@@ -2832,6 +2857,10 @@ private extension ViewController {
2832
         scroll.borderType = .noBorder
2857
         scroll.borderType = .noBorder
2833
         scroll.scrollerStyle = .overlay
2858
         scroll.scrollerStyle = .overlay
2834
         scroll.automaticallyAdjustsContentInsets = false
2859
         scroll.automaticallyAdjustsContentInsets = false
2860
+        let clip = TopAlignedClipView()
2861
+        clip.drawsBackground = false
2862
+        clip.postsBoundsChangedNotifications = true
2863
+        scroll.contentView = clip
2835
         schedulePageCardsScrollView = scroll
2864
         schedulePageCardsScrollView = scroll
2836
         wrapper.addSubview(scroll)
2865
         wrapper.addSubview(scroll)
2837
 
2866
 
@@ -2854,7 +2883,6 @@ private extension ViewController {
2854
             stack.leadingAnchor.constraint(equalTo: scroll.contentView.leadingAnchor),
2883
             stack.leadingAnchor.constraint(equalTo: scroll.contentView.leadingAnchor),
2855
             stack.trailingAnchor.constraint(equalTo: scroll.contentView.trailingAnchor),
2884
             stack.trailingAnchor.constraint(equalTo: scroll.contentView.trailingAnchor),
2856
             stack.topAnchor.constraint(equalTo: scroll.contentView.topAnchor),
2885
             stack.topAnchor.constraint(equalTo: scroll.contentView.topAnchor),
2857
-            stack.bottomAnchor.constraint(equalTo: scroll.contentView.bottomAnchor),
2858
             stack.widthAnchor.constraint(equalTo: scroll.contentView.widthAnchor)
2886
             stack.widthAnchor.constraint(equalTo: scroll.contentView.widthAnchor)
2859
         ])
2887
         ])
2860
 
2888
 
@@ -3058,6 +3086,7 @@ private extension ViewController {
3058
         scroll.verticalScrollElasticity = .none
3086
         scroll.verticalScrollElasticity = .none
3059
         scroll.autohidesScrollers = false
3087
         scroll.autohidesScrollers = false
3060
         scroll.borderType = .noBorder
3088
         scroll.borderType = .noBorder
3089
+        scroll.automaticallyAdjustsContentInsets = false
3061
         scroll.heightAnchor.constraint(equalToConstant: 150).isActive = true
3090
         scroll.heightAnchor.constraint(equalToConstant: 150).isActive = true
3062
         scroll.widthAnchor.constraint(equalToConstant: viewportWidth).isActive = true
3091
         scroll.widthAnchor.constraint(equalToConstant: viewportWidth).isActive = true
3063
 
3092
 
@@ -3074,12 +3103,11 @@ private extension ViewController {
3074
         scroll.documentView = row
3103
         scroll.documentView = row
3075
         scroll.contentView.postsBoundsChangedNotifications = true
3104
         scroll.contentView.postsBoundsChangedNotifications = true
3076
 
3105
 
3077
-        // Ensure the stack view determines content size for horizontal scrolling.
3106
+        // Pin top/leading/trailing only; avoid bottom == clip so the horizontal stack is not stretched vertically inside the clip (same as Schedule cards scroll).
3078
         NSLayoutConstraint.activate([
3107
         NSLayoutConstraint.activate([
3079
             row.leadingAnchor.constraint(equalTo: scroll.contentView.leadingAnchor),
3108
             row.leadingAnchor.constraint(equalTo: scroll.contentView.leadingAnchor),
3080
             row.trailingAnchor.constraint(greaterThanOrEqualTo: scroll.contentView.trailingAnchor),
3109
             row.trailingAnchor.constraint(greaterThanOrEqualTo: scroll.contentView.trailingAnchor),
3081
             row.topAnchor.constraint(equalTo: scroll.contentView.topAnchor),
3110
             row.topAnchor.constraint(equalTo: scroll.contentView.topAnchor),
3082
-            row.bottomAnchor.constraint(equalTo: scroll.contentView.bottomAnchor),
3083
             row.heightAnchor.constraint(equalToConstant: 150)
3111
             row.heightAnchor.constraint(equalToConstant: 150)
3084
         ])
3112
         ])
3085
 
3113
 
@@ -3300,6 +3328,11 @@ extension ViewController: NSWindowDelegate {
3300
     }
3328
     }
3301
 }
3329
 }
3302
 
3330
 
3331
+/// Default `NSClipView` uses a non-flipped coordinate system, so a document shorter than the visible area is anchored to the **bottom** of the clip, leaving a large gap above (e.g. Schedule empty state). Flipped coordinates match Auto Layout’s top-leading anchors and keep content top-aligned.
3332
+private final class TopAlignedClipView: NSClipView {
3333
+    override var isFlipped: Bool { true }
3334
+}
3335
+
3303
 /// Wraps the Google auth control and draws a circular accent ring with a light pulse while the signed-in avatar is hovered.
3336
 /// Wraps the Google auth control and draws a circular accent ring with a light pulse while the signed-in avatar is hovered.
3304
 private final class GoogleProfileAuthHostView: NSView {
3337
 private final class GoogleProfileAuthHostView: NSView {
3305
     weak var authButton: NSButton? {
3338
     weak var authButton: NSButton? {
@@ -4510,7 +4543,6 @@ private extension ViewController {
4510
             let empty = roundedContainer(cornerRadius: 10, color: palette.sectionCard)
4543
             let empty = roundedContainer(cornerRadius: 10, color: palette.sectionCard)
4511
             empty.translatesAutoresizingMaskIntoConstraints = false
4544
             empty.translatesAutoresizingMaskIntoConstraints = false
4512
             empty.heightAnchor.constraint(equalToConstant: 140).isActive = true
4545
             empty.heightAnchor.constraint(equalToConstant: 140).isActive = true
4513
-            empty.widthAnchor.constraint(equalTo: stack.widthAnchor).isActive = true
4514
             styleSurface(empty, borderColor: palette.inputBorder, borderWidth: 1, shadow: false)
4546
             styleSurface(empty, borderColor: palette.inputBorder, borderWidth: 1, shadow: false)
4515
             let label = textLabel(googleOAuth.loadTokens() == nil ? "Connect to load schedule" : "No meetings for selected filters", font: typography.cardSubtitle, color: palette.textSecondary)
4547
             let label = textLabel(googleOAuth.loadTokens() == nil ? "Connect to load schedule" : "No meetings for selected filters", font: typography.cardSubtitle, color: palette.textSecondary)
4516
             label.translatesAutoresizingMaskIntoConstraints = false
4548
             label.translatesAutoresizingMaskIntoConstraints = false
@@ -4520,6 +4552,7 @@ private extension ViewController {
4520
                 label.centerYAnchor.constraint(equalTo: empty.centerYAnchor)
4552
                 label.centerYAnchor.constraint(equalTo: empty.centerYAnchor)
4521
             ])
4553
             ])
4522
             stack.addArrangedSubview(empty)
4554
             stack.addArrangedSubview(empty)
4555
+            empty.widthAnchor.constraint(equalTo: stack.widthAnchor).isActive = true
4523
             return
4556
             return
4524
         }
4557
         }
4525
 
4558