Просмотр исходного кода

Fix My Profile layout under RTL with LTR subtree and width pins

Pin the profile page container with geometric left/right anchors so
constraints match the forced LTR subtree. Use leading stack alignment
with explicit arranged-subview widths, reapply LTR when the view
joins a window, and drop the redundant document width constraint so
the form fills the scroll view predictably.

Co-authored-by: Cursor <cursoragent@cursor.com>
AhtashamShahzad1 недель назад: 3
Родитель
Сommit
51ab8cf6de

+ 2 - 2
App for Indeed/Views/DashboardView.swift

@@ -1219,8 +1219,8 @@ final class DashboardView: NSView, NSTextFieldDelegate {
1219 1219
             cvMakerPageContainer.topAnchor.constraint(equalTo: nonHomeHost.topAnchor),
1220 1220
             cvMakerPageContainer.bottomAnchor.constraint(equalTo: nonHomeHost.bottomAnchor),
1221 1221
 
1222
-            profilePageContainer.leadingAnchor.constraint(equalTo: nonHomeHost.leadingAnchor),
1223
-            profilePageContainer.trailingAnchor.constraint(equalTo: nonHomeHost.trailingAnchor),
1222
+            profilePageContainer.leftAnchor.constraint(equalTo: nonHomeHost.leftAnchor),
1223
+            profilePageContainer.rightAnchor.constraint(equalTo: nonHomeHost.rightAnchor),
1224 1224
             profilePageContainer.topAnchor.constraint(equalTo: nonHomeHost.topAnchor),
1225 1225
             profilePageContainer.bottomAnchor.constraint(equalTo: nonHomeHost.bottomAnchor)
1226 1226
         ])

+ 67 - 24
App for Indeed/Views/MyProfilePageView.swift

@@ -22,6 +22,14 @@ private enum ProfilePagePalette {
22 22
 
23 23
 /// Keeps profile text left-aligned and LTR so fields do not collapse to a narrow trailing strip under RTL / natural alignment.
24 24
 private enum ProfileLayoutEnforcement {
25
+    static func applyForcedLTRSubtree(from root: NSView) {
26
+        var stack: [NSView] = [root]
27
+        while let view = stack.popLast() {
28
+            applyForcedLTR(to: view)
29
+            stack.append(contentsOf: view.subviews)
30
+        }
31
+    }
32
+
25 33
     static func applyForcedLTR(to view: NSView) {
26 34
         view.userInterfaceLayoutDirection = .leftToRight
27 35
     }
@@ -44,6 +52,15 @@ private enum ProfileLayoutEnforcement {
44 52
     }
45 53
 }
46 54
 
55
+private extension NSStackView {
56
+    /// For vertical stacks using `.leading` alignment (geometric left under mixed RTL), pin each arranged subview’s width to the stack so labels/fields stay full-width.
57
+    func pinAllArrangedSubviewWidthsEqualToStackWidth() {
58
+        for subview in arrangedSubviews {
59
+            subview.widthAnchor.constraint(equalTo: widthAnchor).isActive = true
60
+        }
61
+    }
62
+}
63
+
47 64
 /// Two fields side‑by‑side with a true 50/50 split, or stacked full‑width when compact. Avoids `NSStackView` collapsing paired columns to a narrow strip on the trailing edge.
48 65
 private final class ProfileDualFieldRow: NSView {
49 66
     private let leftView: NSView
@@ -158,6 +175,13 @@ final class MyProfilePageView: NSView {
158 175
         setup()
159 176
     }
160 177
 
178
+    override func viewDidMoveToWindow() {
179
+        super.viewDidMoveToWindow()
180
+        guard window != nil else { return }
181
+        ProfileLayoutEnforcement.applyForcedLTRSubtree(from: self)
182
+        needsLayout = true
183
+    }
184
+
161 185
     override func layout() {
162 186
         super.layout()
163 187
         if let layer = cardView.layer, layer.shadowOpacity > 0 {
@@ -243,7 +267,7 @@ final class MyProfilePageView: NSView {
243 267
 
244 268
         formStack.translatesAutoresizingMaskIntoConstraints = false
245 269
         formStack.orientation = .vertical
246
-        formStack.alignment = .width
270
+        formStack.alignment = .leading
247 271
         formStack.distribution = .fill
248 272
         formStack.spacing = 24
249 273
         formStack.edgeInsets = NSEdgeInsets(top: 32, left: 28, bottom: 32, right: 28)
@@ -266,7 +290,6 @@ final class MyProfilePageView: NSView {
266 290
             // Pin the document to the clip view’s geometric width so LTR/RTL semantics cannot slide the form.
267 291
             documentView.leftAnchor.constraint(equalTo: scrollView.contentView.leftAnchor),
268 292
             documentView.rightAnchor.constraint(equalTo: scrollView.contentView.rightAnchor),
269
-            documentView.widthAnchor.constraint(equalTo: scrollView.contentView.widthAnchor),
270 293
             documentView.topAnchor.constraint(equalTo: scrollView.contentView.topAnchor),
271 294
             documentView.bottomAnchor.constraint(equalTo: cardView.bottomAnchor, constant: Self.horizontalPageInset),
272 295
 
@@ -341,6 +364,8 @@ final class MyProfilePageView: NSView {
341 364
 
342 365
         appendWorkExperienceEntry()
343 366
         appendEducationEntry()
367
+
368
+        ProfileLayoutEnforcement.applyForcedLTRSubtree(from: self)
344 369
     }
345 370
 
346 371
     private func applyResponsiveRowsIfNeeded() {
@@ -364,6 +389,7 @@ final class MyProfilePageView: NSView {
364 389
         formStack.addArrangedSubview(view)
365 390
         view.setContentHuggingPriority(.defaultLow, for: .horizontal)
366 391
         view.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
392
+        view.widthAnchor.constraint(equalTo: formStack.widthAnchor).isActive = true
367 393
     }
368 394
 
369 395
     private func sectionHeading(_ text: String) -> NSView {
@@ -398,7 +424,7 @@ final class MyProfilePageView: NSView {
398 424
         label.font = .systemFont(ofSize: 12, weight: .medium)
399 425
         label.textColor = ProfilePagePalette.secondaryText
400 426
         label.translatesAutoresizingMaskIntoConstraints = false
401
-        label.setContentHuggingPriority(.defaultHigh, for: .horizontal)
427
+        label.setContentHuggingPriority(.defaultLow, for: .horizontal)
402 428
         ProfileLayoutEnforcement.applyLeftAlignedTextField(label)
403 429
 
404 430
         styleSingleLineField(field, placeholder: placeholder)
@@ -407,8 +433,8 @@ final class MyProfilePageView: NSView {
407 433
         let stack = NSStackView(views: [label, wrap])
408 434
         stack.orientation = .vertical
409 435
         stack.spacing = 8
410
-        // `.width` stretches label + field chrome to the row width; `.leading` collapses to intrinsic width and caused right-edge compression.
411
-        stack.alignment = .width
436
+        // `.leading` keeps rows on the geometric left; explicit widths keep labels/fields full-width (`.width` alone can still hug the trailing edge under RTL-style layout).
437
+        stack.alignment = .leading
412 438
         stack.translatesAutoresizingMaskIntoConstraints = false
413 439
         stack.userInterfaceLayoutDirection = .leftToRight
414 440
         ProfileLayoutEnforcement.applyForcedLTR(to: stack)
@@ -416,8 +442,9 @@ final class MyProfilePageView: NSView {
416 442
         wrap.setContentHuggingPriority(.defaultLow, for: .horizontal)
417 443
         wrap.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
418 444
         NSLayoutConstraint.activate([
419
-            wrap.widthAnchor.constraint(equalTo: stack.widthAnchor),
420
-            label.leftAnchor.constraint(equalTo: stack.leftAnchor)
445
+            label.leftAnchor.constraint(equalTo: stack.leftAnchor),
446
+            label.widthAnchor.constraint(equalTo: stack.widthAnchor),
447
+            wrap.widthAnchor.constraint(equalTo: stack.widthAnchor)
421 448
         ])
422 449
         return stack
423 450
     }
@@ -523,14 +550,16 @@ final class MyProfilePageView: NSView {
523 550
         let stack = NSStackView(views: [label, wrap])
524 551
         stack.orientation = .vertical
525 552
         stack.spacing = 8
526
-        stack.alignment = .width
553
+        stack.alignment = .leading
527 554
         stack.translatesAutoresizingMaskIntoConstraints = false
528 555
         stack.userInterfaceLayoutDirection = .leftToRight
529 556
         ProfileLayoutEnforcement.applyForcedLTR(to: stack)
530 557
         stack.setContentHuggingPriority(.defaultLow, for: .horizontal)
531 558
         wrap.setContentHuggingPriority(.defaultLow, for: .horizontal)
532 559
         NSLayoutConstraint.activate([
533
-            label.leftAnchor.constraint(equalTo: stack.leftAnchor)
560
+            label.leftAnchor.constraint(equalTo: stack.leftAnchor),
561
+            label.widthAnchor.constraint(equalTo: stack.widthAnchor),
562
+            wrap.widthAnchor.constraint(equalTo: stack.widthAnchor)
534 563
         ])
535 564
         return stack
536 565
     }
@@ -590,14 +619,16 @@ final class MyProfilePageView: NSView {
590 619
         let stack = NSStackView(views: [label, wrap])
591 620
         stack.orientation = .vertical
592 621
         stack.spacing = 8
593
-        stack.alignment = .width
622
+        stack.alignment = .leading
594 623
         stack.translatesAutoresizingMaskIntoConstraints = false
595 624
         stack.userInterfaceLayoutDirection = .leftToRight
596 625
         ProfileLayoutEnforcement.applyForcedLTR(to: stack)
597 626
         stack.setContentHuggingPriority(.defaultLow, for: .horizontal)
598 627
         wrap.setContentHuggingPriority(.defaultLow, for: .horizontal)
599 628
         NSLayoutConstraint.activate([
600
-            label.leftAnchor.constraint(equalTo: stack.leftAnchor)
629
+            label.leftAnchor.constraint(equalTo: stack.leftAnchor),
630
+            label.widthAnchor.constraint(equalTo: stack.widthAnchor),
631
+            wrap.widthAnchor.constraint(equalTo: stack.widthAnchor)
601 632
         ])
602 633
         return stack
603 634
     }
@@ -622,7 +653,7 @@ final class MyProfilePageView: NSView {
622 653
         let stack = NSStackView(views: [label, wrap, helper])
623 654
         stack.orientation = .vertical
624 655
         stack.spacing = 8
625
-        stack.alignment = .width
656
+        stack.alignment = .leading
626 657
         stack.translatesAutoresizingMaskIntoConstraints = false
627 658
         stack.userInterfaceLayoutDirection = .leftToRight
628 659
         ProfileLayoutEnforcement.applyForcedLTR(to: stack)
@@ -633,7 +664,10 @@ final class MyProfilePageView: NSView {
633 664
         }
634 665
         NSLayoutConstraint.activate([
635 666
             label.leftAnchor.constraint(equalTo: stack.leftAnchor),
636
-            helper.leftAnchor.constraint(equalTo: stack.leftAnchor)
667
+            label.widthAnchor.constraint(equalTo: stack.widthAnchor),
668
+            wrap.widthAnchor.constraint(equalTo: stack.widthAnchor),
669
+            helper.leftAnchor.constraint(equalTo: stack.leftAnchor),
670
+            helper.widthAnchor.constraint(equalTo: stack.widthAnchor)
637 671
         ])
638 672
         return stack
639 673
     }
@@ -677,17 +711,18 @@ final class MyProfilePageView: NSView {
677 711
         workExperienceRowsStack.translatesAutoresizingMaskIntoConstraints = false
678 712
         workExperienceRowsStack.orientation = .vertical
679 713
         workExperienceRowsStack.spacing = 20
680
-        workExperienceRowsStack.alignment = .width
714
+        workExperienceRowsStack.alignment = .leading
681 715
         workExperienceRowsStack.userInterfaceLayoutDirection = .leftToRight
682 716
         ProfileLayoutEnforcement.applyForcedLTR(to: workExperienceRowsStack)
683 717
 
684 718
         let outer = NSStackView(views: [headerRow, workExperienceRowsStack])
685 719
         outer.orientation = .vertical
686 720
         outer.spacing = 16
687
-        outer.alignment = .width
721
+        outer.alignment = .leading
688 722
         outer.translatesAutoresizingMaskIntoConstraints = false
689 723
         outer.userInterfaceLayoutDirection = .leftToRight
690 724
         ProfileLayoutEnforcement.applyForcedLTR(to: outer)
725
+        outer.pinAllArrangedSubviewWidthsEqualToStackWidth()
691 726
         return outer
692 727
     }
693 728
 
@@ -722,17 +757,18 @@ final class MyProfilePageView: NSView {
722 757
         educationRowsStack.translatesAutoresizingMaskIntoConstraints = false
723 758
         educationRowsStack.orientation = .vertical
724 759
         educationRowsStack.spacing = 16
725
-        educationRowsStack.alignment = .width
760
+        educationRowsStack.alignment = .leading
726 761
         educationRowsStack.userInterfaceLayoutDirection = .leftToRight
727 762
         ProfileLayoutEnforcement.applyForcedLTR(to: educationRowsStack)
728 763
 
729 764
         let outer = NSStackView(views: [headerRow, educationRowsStack])
730 765
         outer.orientation = .vertical
731 766
         outer.spacing = 16
732
-        outer.alignment = .width
767
+        outer.alignment = .leading
733 768
         outer.translatesAutoresizingMaskIntoConstraints = false
734 769
         outer.userInterfaceLayoutDirection = .leftToRight
735 770
         ProfileLayoutEnforcement.applyForcedLTR(to: outer)
771
+        outer.pinAllArrangedSubviewWidthsEqualToStackWidth()
736 772
         return outer
737 773
     }
738 774
 
@@ -748,6 +784,7 @@ final class MyProfilePageView: NSView {
748 784
         }
749 785
         workExperienceEntries.append(entry)
750 786
         workExperienceRowsStack.addArrangedSubview(entry)
787
+        entry.widthAnchor.constraint(equalTo: workExperienceRowsStack.widthAnchor).isActive = true
751 788
         renumberWorkExperienceEntries()
752 789
         refreshWorkExperienceDeleteButtons()
753 790
     }
@@ -786,6 +823,7 @@ final class MyProfilePageView: NSView {
786 823
         }
787 824
         educationEntries.append(entry)
788 825
         educationRowsStack.addArrangedSubview(entry)
826
+        entry.widthAnchor.constraint(equalTo: educationRowsStack.widthAnchor).isActive = true
789 827
         renumberEducationEntries()
790 828
         refreshEducationDeleteButtons()
791 829
     }
@@ -940,11 +978,12 @@ private final class WorkExperienceEntryView: NSView {
940 978
         let inner = NSStackView(views: [headerRow, jobCompanyRow, durationGroup, descriptionGroup])
941 979
         inner.orientation = .vertical
942 980
         inner.spacing = 16
943
-        inner.alignment = .width
981
+        inner.alignment = .leading
944 982
         inner.translatesAutoresizingMaskIntoConstraints = false
945 983
         inner.edgeInsets = NSEdgeInsets(top: 16, left: 18, bottom: 16, right: 18)
946 984
         inner.userInterfaceLayoutDirection = .leftToRight
947 985
         ProfileLayoutEnforcement.applyForcedLTR(to: inner)
986
+        inner.pinAllArrangedSubviewWidthsEqualToStackWidth()
948 987
 
949 988
         addSubview(inner)
950 989
         NSLayoutConstraint.activate([
@@ -987,7 +1026,7 @@ private final class WorkExperienceEntryView: NSView {
987 1026
         let stack = NSStackView(views: [label, wrap])
988 1027
         stack.orientation = .vertical
989 1028
         stack.spacing = 8
990
-        stack.alignment = .width
1029
+        stack.alignment = .leading
991 1030
         stack.translatesAutoresizingMaskIntoConstraints = false
992 1031
         stack.userInterfaceLayoutDirection = .leftToRight
993 1032
         ProfileLayoutEnforcement.applyForcedLTR(to: stack)
@@ -995,8 +1034,9 @@ private final class WorkExperienceEntryView: NSView {
995 1034
         wrap.setContentHuggingPriority(.defaultLow, for: .horizontal)
996 1035
         wrap.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
997 1036
         NSLayoutConstraint.activate([
998
-            wrap.widthAnchor.constraint(equalTo: stack.widthAnchor),
999
-            label.leftAnchor.constraint(equalTo: stack.leftAnchor)
1037
+            label.leftAnchor.constraint(equalTo: stack.leftAnchor),
1038
+            label.widthAnchor.constraint(equalTo: stack.widthAnchor),
1039
+            wrap.widthAnchor.constraint(equalTo: stack.widthAnchor)
1000 1040
         ])
1001 1041
         return stack
1002 1042
     }
@@ -1053,14 +1093,16 @@ private final class WorkExperienceEntryView: NSView {
1053 1093
         let stack = NSStackView(views: [label, wrap])
1054 1094
         stack.orientation = .vertical
1055 1095
         stack.spacing = 8
1056
-        stack.alignment = .width
1096
+        stack.alignment = .leading
1057 1097
         stack.translatesAutoresizingMaskIntoConstraints = false
1058 1098
         stack.userInterfaceLayoutDirection = .leftToRight
1059 1099
         ProfileLayoutEnforcement.applyForcedLTR(to: stack)
1060 1100
         stack.setContentHuggingPriority(.defaultLow, for: .horizontal)
1061 1101
         wrap.setContentHuggingPriority(.defaultLow, for: .horizontal)
1062 1102
         NSLayoutConstraint.activate([
1063
-            label.leftAnchor.constraint(equalTo: stack.leftAnchor)
1103
+            label.leftAnchor.constraint(equalTo: stack.leftAnchor),
1104
+            label.widthAnchor.constraint(equalTo: stack.widthAnchor),
1105
+            wrap.widthAnchor.constraint(equalTo: stack.widthAnchor)
1064 1106
         ])
1065 1107
         return stack
1066 1108
     }
@@ -1210,11 +1252,12 @@ private final class EducationEntryView: NSView {
1210 1252
         let inner = NSStackView(views: [headerRow, degreeInstitutionRow, yearGroup])
1211 1253
         inner.orientation = .vertical
1212 1254
         inner.spacing = 14
1213
-        inner.alignment = .width
1255
+        inner.alignment = .leading
1214 1256
         inner.translatesAutoresizingMaskIntoConstraints = false
1215 1257
         inner.edgeInsets = NSEdgeInsets(top: 14, left: 18, bottom: 14, right: 18)
1216 1258
         inner.userInterfaceLayoutDirection = .leftToRight
1217 1259
         ProfileLayoutEnforcement.applyForcedLTR(to: inner)
1260
+        inner.pinAllArrangedSubviewWidthsEqualToStackWidth()
1218 1261
 
1219 1262
         addSubview(inner)
1220 1263
         NSLayoutConstraint.activate([