فهرست منبع

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 2 ماه پیش
والد
کامیت
51ab8cf6de
2فایلهای تغییر یافته به همراه69 افزوده شده و 26 حذف شده
  1. 2 2
      App for Indeed/Views/DashboardView.swift
  2. 67 24
      App for Indeed/Views/MyProfilePageView.swift

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

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

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

@@ -22,6 +22,14 @@ private enum ProfilePagePalette {
 
 /// Keeps profile text left-aligned and LTR so fields do not collapse to a narrow trailing strip under RTL / natural alignment.
 private enum ProfileLayoutEnforcement {
+    static func applyForcedLTRSubtree(from root: NSView) {
+        var stack: [NSView] = [root]
+        while let view = stack.popLast() {
+            applyForcedLTR(to: view)
+            stack.append(contentsOf: view.subviews)
+        }
+    }
+
     static func applyForcedLTR(to view: NSView) {
         view.userInterfaceLayoutDirection = .leftToRight
     }
@@ -44,6 +52,15 @@ private enum ProfileLayoutEnforcement {
     }
 }
 
+private extension NSStackView {
+    /// 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.
+    func pinAllArrangedSubviewWidthsEqualToStackWidth() {
+        for subview in arrangedSubviews {
+            subview.widthAnchor.constraint(equalTo: widthAnchor).isActive = true
+        }
+    }
+}
+
 /// 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.
 private final class ProfileDualFieldRow: NSView {
     private let leftView: NSView
@@ -158,6 +175,13 @@ final class MyProfilePageView: NSView {
         setup()
     }
 
+    override func viewDidMoveToWindow() {
+        super.viewDidMoveToWindow()
+        guard window != nil else { return }
+        ProfileLayoutEnforcement.applyForcedLTRSubtree(from: self)
+        needsLayout = true
+    }
+
     override func layout() {
         super.layout()
         if let layer = cardView.layer, layer.shadowOpacity > 0 {
@@ -243,7 +267,7 @@ final class MyProfilePageView: NSView {
 
         formStack.translatesAutoresizingMaskIntoConstraints = false
         formStack.orientation = .vertical
-        formStack.alignment = .width
+        formStack.alignment = .leading
         formStack.distribution = .fill
         formStack.spacing = 24
         formStack.edgeInsets = NSEdgeInsets(top: 32, left: 28, bottom: 32, right: 28)
@@ -266,7 +290,6 @@ final class MyProfilePageView: NSView {
             // Pin the document to the clip view’s geometric width so LTR/RTL semantics cannot slide the form.
             documentView.leftAnchor.constraint(equalTo: scrollView.contentView.leftAnchor),
             documentView.rightAnchor.constraint(equalTo: scrollView.contentView.rightAnchor),
-            documentView.widthAnchor.constraint(equalTo: scrollView.contentView.widthAnchor),
             documentView.topAnchor.constraint(equalTo: scrollView.contentView.topAnchor),
             documentView.bottomAnchor.constraint(equalTo: cardView.bottomAnchor, constant: Self.horizontalPageInset),
 
@@ -341,6 +364,8 @@ final class MyProfilePageView: NSView {
 
         appendWorkExperienceEntry()
         appendEducationEntry()
+
+        ProfileLayoutEnforcement.applyForcedLTRSubtree(from: self)
     }
 
     private func applyResponsiveRowsIfNeeded() {
@@ -364,6 +389,7 @@ final class MyProfilePageView: NSView {
         formStack.addArrangedSubview(view)
         view.setContentHuggingPriority(.defaultLow, for: .horizontal)
         view.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
+        view.widthAnchor.constraint(equalTo: formStack.widthAnchor).isActive = true
     }
 
     private func sectionHeading(_ text: String) -> NSView {
@@ -398,7 +424,7 @@ final class MyProfilePageView: NSView {
         label.font = .systemFont(ofSize: 12, weight: .medium)
         label.textColor = ProfilePagePalette.secondaryText
         label.translatesAutoresizingMaskIntoConstraints = false
-        label.setContentHuggingPriority(.defaultHigh, for: .horizontal)
+        label.setContentHuggingPriority(.defaultLow, for: .horizontal)
         ProfileLayoutEnforcement.applyLeftAlignedTextField(label)
 
         styleSingleLineField(field, placeholder: placeholder)
@@ -407,8 +433,8 @@ final class MyProfilePageView: NSView {
         let stack = NSStackView(views: [label, wrap])
         stack.orientation = .vertical
         stack.spacing = 8
-        // `.width` stretches label + field chrome to the row width; `.leading` collapses to intrinsic width and caused right-edge compression.
-        stack.alignment = .width
+        // `.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).
+        stack.alignment = .leading
         stack.translatesAutoresizingMaskIntoConstraints = false
         stack.userInterfaceLayoutDirection = .leftToRight
         ProfileLayoutEnforcement.applyForcedLTR(to: stack)
@@ -416,8 +442,9 @@ final class MyProfilePageView: NSView {
         wrap.setContentHuggingPriority(.defaultLow, for: .horizontal)
         wrap.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
         NSLayoutConstraint.activate([
-            wrap.widthAnchor.constraint(equalTo: stack.widthAnchor),
-            label.leftAnchor.constraint(equalTo: stack.leftAnchor)
+            label.leftAnchor.constraint(equalTo: stack.leftAnchor),
+            label.widthAnchor.constraint(equalTo: stack.widthAnchor),
+            wrap.widthAnchor.constraint(equalTo: stack.widthAnchor)
         ])
         return stack
     }
@@ -523,14 +550,16 @@ final class MyProfilePageView: NSView {
         let stack = NSStackView(views: [label, wrap])
         stack.orientation = .vertical
         stack.spacing = 8
-        stack.alignment = .width
+        stack.alignment = .leading
         stack.translatesAutoresizingMaskIntoConstraints = false
         stack.userInterfaceLayoutDirection = .leftToRight
         ProfileLayoutEnforcement.applyForcedLTR(to: stack)
         stack.setContentHuggingPriority(.defaultLow, for: .horizontal)
         wrap.setContentHuggingPriority(.defaultLow, for: .horizontal)
         NSLayoutConstraint.activate([
-            label.leftAnchor.constraint(equalTo: stack.leftAnchor)
+            label.leftAnchor.constraint(equalTo: stack.leftAnchor),
+            label.widthAnchor.constraint(equalTo: stack.widthAnchor),
+            wrap.widthAnchor.constraint(equalTo: stack.widthAnchor)
         ])
         return stack
     }
@@ -590,14 +619,16 @@ final class MyProfilePageView: NSView {
         let stack = NSStackView(views: [label, wrap])
         stack.orientation = .vertical
         stack.spacing = 8
-        stack.alignment = .width
+        stack.alignment = .leading
         stack.translatesAutoresizingMaskIntoConstraints = false
         stack.userInterfaceLayoutDirection = .leftToRight
         ProfileLayoutEnforcement.applyForcedLTR(to: stack)
         stack.setContentHuggingPriority(.defaultLow, for: .horizontal)
         wrap.setContentHuggingPriority(.defaultLow, for: .horizontal)
         NSLayoutConstraint.activate([
-            label.leftAnchor.constraint(equalTo: stack.leftAnchor)
+            label.leftAnchor.constraint(equalTo: stack.leftAnchor),
+            label.widthAnchor.constraint(equalTo: stack.widthAnchor),
+            wrap.widthAnchor.constraint(equalTo: stack.widthAnchor)
         ])
         return stack
     }
@@ -622,7 +653,7 @@ final class MyProfilePageView: NSView {
         let stack = NSStackView(views: [label, wrap, helper])
         stack.orientation = .vertical
         stack.spacing = 8
-        stack.alignment = .width
+        stack.alignment = .leading
         stack.translatesAutoresizingMaskIntoConstraints = false
         stack.userInterfaceLayoutDirection = .leftToRight
         ProfileLayoutEnforcement.applyForcedLTR(to: stack)
@@ -633,7 +664,10 @@ final class MyProfilePageView: NSView {
         }
         NSLayoutConstraint.activate([
             label.leftAnchor.constraint(equalTo: stack.leftAnchor),
-            helper.leftAnchor.constraint(equalTo: stack.leftAnchor)
+            label.widthAnchor.constraint(equalTo: stack.widthAnchor),
+            wrap.widthAnchor.constraint(equalTo: stack.widthAnchor),
+            helper.leftAnchor.constraint(equalTo: stack.leftAnchor),
+            helper.widthAnchor.constraint(equalTo: stack.widthAnchor)
         ])
         return stack
     }
@@ -677,17 +711,18 @@ final class MyProfilePageView: NSView {
         workExperienceRowsStack.translatesAutoresizingMaskIntoConstraints = false
         workExperienceRowsStack.orientation = .vertical
         workExperienceRowsStack.spacing = 20
-        workExperienceRowsStack.alignment = .width
+        workExperienceRowsStack.alignment = .leading
         workExperienceRowsStack.userInterfaceLayoutDirection = .leftToRight
         ProfileLayoutEnforcement.applyForcedLTR(to: workExperienceRowsStack)
 
         let outer = NSStackView(views: [headerRow, workExperienceRowsStack])
         outer.orientation = .vertical
         outer.spacing = 16
-        outer.alignment = .width
+        outer.alignment = .leading
         outer.translatesAutoresizingMaskIntoConstraints = false
         outer.userInterfaceLayoutDirection = .leftToRight
         ProfileLayoutEnforcement.applyForcedLTR(to: outer)
+        outer.pinAllArrangedSubviewWidthsEqualToStackWidth()
         return outer
     }
 
@@ -722,17 +757,18 @@ final class MyProfilePageView: NSView {
         educationRowsStack.translatesAutoresizingMaskIntoConstraints = false
         educationRowsStack.orientation = .vertical
         educationRowsStack.spacing = 16
-        educationRowsStack.alignment = .width
+        educationRowsStack.alignment = .leading
         educationRowsStack.userInterfaceLayoutDirection = .leftToRight
         ProfileLayoutEnforcement.applyForcedLTR(to: educationRowsStack)
 
         let outer = NSStackView(views: [headerRow, educationRowsStack])
         outer.orientation = .vertical
         outer.spacing = 16
-        outer.alignment = .width
+        outer.alignment = .leading
         outer.translatesAutoresizingMaskIntoConstraints = false
         outer.userInterfaceLayoutDirection = .leftToRight
         ProfileLayoutEnforcement.applyForcedLTR(to: outer)
+        outer.pinAllArrangedSubviewWidthsEqualToStackWidth()
         return outer
     }
 
@@ -748,6 +784,7 @@ final class MyProfilePageView: NSView {
         }
         workExperienceEntries.append(entry)
         workExperienceRowsStack.addArrangedSubview(entry)
+        entry.widthAnchor.constraint(equalTo: workExperienceRowsStack.widthAnchor).isActive = true
         renumberWorkExperienceEntries()
         refreshWorkExperienceDeleteButtons()
     }
@@ -786,6 +823,7 @@ final class MyProfilePageView: NSView {
         }
         educationEntries.append(entry)
         educationRowsStack.addArrangedSubview(entry)
+        entry.widthAnchor.constraint(equalTo: educationRowsStack.widthAnchor).isActive = true
         renumberEducationEntries()
         refreshEducationDeleteButtons()
     }
@@ -940,11 +978,12 @@ private final class WorkExperienceEntryView: NSView {
         let inner = NSStackView(views: [headerRow, jobCompanyRow, durationGroup, descriptionGroup])
         inner.orientation = .vertical
         inner.spacing = 16
-        inner.alignment = .width
+        inner.alignment = .leading
         inner.translatesAutoresizingMaskIntoConstraints = false
         inner.edgeInsets = NSEdgeInsets(top: 16, left: 18, bottom: 16, right: 18)
         inner.userInterfaceLayoutDirection = .leftToRight
         ProfileLayoutEnforcement.applyForcedLTR(to: inner)
+        inner.pinAllArrangedSubviewWidthsEqualToStackWidth()
 
         addSubview(inner)
         NSLayoutConstraint.activate([
@@ -987,7 +1026,7 @@ private final class WorkExperienceEntryView: NSView {
         let stack = NSStackView(views: [label, wrap])
         stack.orientation = .vertical
         stack.spacing = 8
-        stack.alignment = .width
+        stack.alignment = .leading
         stack.translatesAutoresizingMaskIntoConstraints = false
         stack.userInterfaceLayoutDirection = .leftToRight
         ProfileLayoutEnforcement.applyForcedLTR(to: stack)
@@ -995,8 +1034,9 @@ private final class WorkExperienceEntryView: NSView {
         wrap.setContentHuggingPriority(.defaultLow, for: .horizontal)
         wrap.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
         NSLayoutConstraint.activate([
-            wrap.widthAnchor.constraint(equalTo: stack.widthAnchor),
-            label.leftAnchor.constraint(equalTo: stack.leftAnchor)
+            label.leftAnchor.constraint(equalTo: stack.leftAnchor),
+            label.widthAnchor.constraint(equalTo: stack.widthAnchor),
+            wrap.widthAnchor.constraint(equalTo: stack.widthAnchor)
         ])
         return stack
     }
@@ -1053,14 +1093,16 @@ private final class WorkExperienceEntryView: NSView {
         let stack = NSStackView(views: [label, wrap])
         stack.orientation = .vertical
         stack.spacing = 8
-        stack.alignment = .width
+        stack.alignment = .leading
         stack.translatesAutoresizingMaskIntoConstraints = false
         stack.userInterfaceLayoutDirection = .leftToRight
         ProfileLayoutEnforcement.applyForcedLTR(to: stack)
         stack.setContentHuggingPriority(.defaultLow, for: .horizontal)
         wrap.setContentHuggingPriority(.defaultLow, for: .horizontal)
         NSLayoutConstraint.activate([
-            label.leftAnchor.constraint(equalTo: stack.leftAnchor)
+            label.leftAnchor.constraint(equalTo: stack.leftAnchor),
+            label.widthAnchor.constraint(equalTo: stack.widthAnchor),
+            wrap.widthAnchor.constraint(equalTo: stack.widthAnchor)
         ])
         return stack
     }
@@ -1210,11 +1252,12 @@ private final class EducationEntryView: NSView {
         let inner = NSStackView(views: [headerRow, degreeInstitutionRow, yearGroup])
         inner.orientation = .vertical
         inner.spacing = 14
-        inner.alignment = .width
+        inner.alignment = .leading
         inner.translatesAutoresizingMaskIntoConstraints = false
         inner.edgeInsets = NSEdgeInsets(top: 14, left: 18, bottom: 14, right: 18)
         inner.userInterfaceLayoutDirection = .leftToRight
         ProfileLayoutEnforcement.applyForcedLTR(to: inner)
+        inner.pinAllArrangedSubviewWidthsEqualToStackWidth()
 
         addSubview(inner)
         NSLayoutConstraint.activate([