Преглед изворни кода

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 пре 1 недеља
родитељ
комит
5b1f47985e
1 измењених фајлова са 42 додато и 9 уклоњено
  1. 42 9
      meetings_app/ViewController.swift

+ 42 - 9
meetings_app/ViewController.swift

@@ -236,6 +236,8 @@ final class ViewController: NSViewController {
236 236
     private let launchMinContentSize = NSSize(width: 760, height: 600)
237 237
 
238 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 241
     private var sidebarRowViews: [SidebarPage: NSView] = [:]
240 242
     private var selectedSidebarPage: SidebarPage = .joinMeetings
241 243
     private var selectedZoomJoinMode: ZoomJoinMode = .id
@@ -332,7 +334,13 @@ final class ViewController: NSViewController {
332 334
     private let schedulePageCardsPerRow: Int = 3
333 335
     private let schedulePageCardSpacing: CGFloat = 20
334 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 344
     /// Match Join Meetings main content insets so the top auth/profile bar lines up with page edges.
337 345
     private let schedulePageLeadingInset: CGFloat = 28
338 346
     private let schedulePageTrailingInset: CGFloat = 28
@@ -730,16 +738,19 @@ private extension ViewController {
730 738
         applyWindowTitle(for: page)
731 739
 
732 740
         guard let host = mainContentHost else { return }
741
+        NSLayoutConstraint.deactivate(mainContentHostPinConstraints)
742
+        mainContentHostPinConstraints.removeAll()
733 743
         host.subviews.forEach { $0.removeFromSuperview() }
734 744
         let child = viewForPage(page)
735 745
         child.translatesAutoresizingMaskIntoConstraints = false
736 746
         host.addSubview(child)
737
-        NSLayoutConstraint.activate([
747
+        mainContentHostPinConstraints = [
738 748
             child.leadingAnchor.constraint(equalTo: host.leadingAnchor),
739 749
             child.trailingAnchor.constraint(equalTo: host.trailingAnchor),
740 750
             child.topAnchor.constraint(equalTo: host.topAnchor),
741 751
             child.bottomAnchor.constraint(equalTo: host.bottomAnchor)
742
-        ])
752
+        ]
753
+        NSLayoutConstraint.activate(mainContentHostPinConstraints)
743 754
     }
744 755
 
745 756
     private func showSettingsPopover() {
@@ -789,6 +800,8 @@ private extension ViewController {
789 800
         googleAccountPopover?.performClose(nil)
790 801
         googleAccountPopover = nil
791 802
 
803
+        NSLayoutConstraint.deactivate(mainContentHostPinConstraints)
804
+        mainContentHostPinConstraints.removeAll()
792 805
         mainContentHost = nil
793 806
         view.subviews.forEach { $0.removeFromSuperview() }
794 807
         setupRootView()
@@ -1713,11 +1726,14 @@ private extension ViewController {
1713 1726
         contentStack.addArrangedSubview(meetJoinSectionRow())
1714 1727
         contentStack.addArrangedSubview(joinActions)
1715 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 1733
         let dateHeading = textLabel(scheduleInitialHeadingText(), font: typography.dateHeading, color: palette.textSecondary)
1719 1734
         scheduleDateHeadingLabel = dateHeading
1720 1735
         contentStack.addArrangedSubview(dateHeading)
1736
+        contentStack.setCustomSpacing(joinPageDateToMeetingCardsSpacing, after: dateHeading)
1721 1737
 
1722 1738
         let cardsRow = scheduleCardsRow(meetings: [])
1723 1739
         contentStack.addArrangedSubview(cardsRow)
@@ -1748,22 +1764,31 @@ private extension ViewController {
1748 1764
         contentStack.orientation = .vertical
1749 1765
         contentStack.spacing = schedulePageStackSpacing
1750 1766
         contentStack.alignment = .width
1767
+        contentStack.distribution = .fill
1751 1768
 
1752 1769
         let header = schedulePageHeader()
1770
+        header.setContentHuggingPriority(.required, for: .vertical)
1771
+        header.setContentCompressionResistancePriority(.required, for: .vertical)
1753 1772
         contentStack.addArrangedSubview(header)
1773
+        contentStack.setCustomSpacing(schedulePageHeaderToDateSpacing, after: header)
1754 1774
 
1755 1775
         let heading = textLabel(schedulePageInitialHeadingText(), font: typography.dateHeading, color: palette.textSecondary)
1756 1776
         heading.alignment = .left
1777
+        heading.setContentHuggingPriority(.required, for: .vertical)
1778
+        heading.setContentCompressionResistancePriority(.required, for: .vertical)
1757 1779
         schedulePageDateHeadingLabel = heading
1758 1780
         contentStack.addArrangedSubview(heading)
1759 1781
 
1760 1782
         let rangeError = textLabel("", font: NSFont.systemFont(ofSize: 12, weight: .semibold), color: .systemRed)
1761 1783
         rangeError.alignment = .left
1762 1784
         rangeError.isHidden = true
1785
+        rangeError.setContentHuggingPriority(.required, for: .vertical)
1786
+        rangeError.setContentCompressionResistancePriority(.required, for: .vertical)
1763 1787
         schedulePageRangeErrorLabel = rangeError
1764 1788
         contentStack.addArrangedSubview(rangeError)
1765 1789
 
1766 1790
         let cardsContainer = makeSchedulePageCardsContainer()
1791
+        cardsContainer.setContentHuggingPriority(.defaultLow, for: .vertical)
1767 1792
         contentStack.addArrangedSubview(cardsContainer)
1768 1793
 
1769 1794
         panel.addSubview(contentStack)
@@ -2672,7 +2697,7 @@ private extension ViewController {
2672 2697
         container.translatesAutoresizingMaskIntoConstraints = false
2673 2698
         container.userInterfaceLayoutDirection = .leftToRight
2674 2699
         container.orientation = .vertical
2675
-        container.spacing = 14
2700
+        container.spacing = 8
2676 2701
         container.alignment = .width
2677 2702
 
2678 2703
         let titleRow = NSStackView()
@@ -2832,6 +2857,10 @@ private extension ViewController {
2832 2857
         scroll.borderType = .noBorder
2833 2858
         scroll.scrollerStyle = .overlay
2834 2859
         scroll.automaticallyAdjustsContentInsets = false
2860
+        let clip = TopAlignedClipView()
2861
+        clip.drawsBackground = false
2862
+        clip.postsBoundsChangedNotifications = true
2863
+        scroll.contentView = clip
2835 2864
         schedulePageCardsScrollView = scroll
2836 2865
         wrapper.addSubview(scroll)
2837 2866
 
@@ -2854,7 +2883,6 @@ private extension ViewController {
2854 2883
             stack.leadingAnchor.constraint(equalTo: scroll.contentView.leadingAnchor),
2855 2884
             stack.trailingAnchor.constraint(equalTo: scroll.contentView.trailingAnchor),
2856 2885
             stack.topAnchor.constraint(equalTo: scroll.contentView.topAnchor),
2857
-            stack.bottomAnchor.constraint(equalTo: scroll.contentView.bottomAnchor),
2858 2886
             stack.widthAnchor.constraint(equalTo: scroll.contentView.widthAnchor)
2859 2887
         ])
2860 2888
 
@@ -3058,6 +3086,7 @@ private extension ViewController {
3058 3086
         scroll.verticalScrollElasticity = .none
3059 3087
         scroll.autohidesScrollers = false
3060 3088
         scroll.borderType = .noBorder
3089
+        scroll.automaticallyAdjustsContentInsets = false
3061 3090
         scroll.heightAnchor.constraint(equalToConstant: 150).isActive = true
3062 3091
         scroll.widthAnchor.constraint(equalToConstant: viewportWidth).isActive = true
3063 3092
 
@@ -3074,12 +3103,11 @@ private extension ViewController {
3074 3103
         scroll.documentView = row
3075 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 3107
         NSLayoutConstraint.activate([
3079 3108
             row.leadingAnchor.constraint(equalTo: scroll.contentView.leadingAnchor),
3080 3109
             row.trailingAnchor.constraint(greaterThanOrEqualTo: scroll.contentView.trailingAnchor),
3081 3110
             row.topAnchor.constraint(equalTo: scroll.contentView.topAnchor),
3082
-            row.bottomAnchor.constraint(equalTo: scroll.contentView.bottomAnchor),
3083 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 3336
 /// Wraps the Google auth control and draws a circular accent ring with a light pulse while the signed-in avatar is hovered.
3304 3337
 private final class GoogleProfileAuthHostView: NSView {
3305 3338
     weak var authButton: NSButton? {
@@ -4510,7 +4543,6 @@ private extension ViewController {
4510 4543
             let empty = roundedContainer(cornerRadius: 10, color: palette.sectionCard)
4511 4544
             empty.translatesAutoresizingMaskIntoConstraints = false
4512 4545
             empty.heightAnchor.constraint(equalToConstant: 140).isActive = true
4513
-            empty.widthAnchor.constraint(equalTo: stack.widthAnchor).isActive = true
4514 4546
             styleSurface(empty, borderColor: palette.inputBorder, borderWidth: 1, shadow: false)
4515 4547
             let label = textLabel(googleOAuth.loadTokens() == nil ? "Connect to load schedule" : "No meetings for selected filters", font: typography.cardSubtitle, color: palette.textSecondary)
4516 4548
             label.translatesAutoresizingMaskIntoConstraints = false
@@ -4520,6 +4552,7 @@ private extension ViewController {
4520 4552
                 label.centerYAnchor.constraint(equalTo: empty.centerYAnchor)
4521 4553
             ])
4522 4554
             stack.addArrangedSubview(empty)
4555
+            empty.widthAnchor.constraint(equalTo: stack.widthAnchor).isActive = true
4523 4556
             return
4524 4557
         }
4525 4558