Explorar o código

Fix input text disappearing after results appear in text tools.

Stabilize scroll layouts and editor sizing so source text stays visible on summarizer, paraphrasing, and punctuation checker pages.

Co-authored-by: Cursor <cursoragent@cursor.com>
AhtashamShahzad1 hai 1 mes
pai
achega
e1a372e910

+ 1 - 0
gramora/Utilities/AppTheme.swift

@@ -33,6 +33,7 @@ enum AppTheme {
     static let inputCornerRadius: CGFloat = 12
     static let textEditorHorizontalInset: CGFloat = 16
     static let textEditorVerticalInset: CGFloat = 18
+    static let compactTextEditorHeight: CGFloat = 240
 
     static let background = Color(red: 0.973, green: 0.980, blue: 0.984)
     static let sidebarBackground = Color.white

+ 31 - 0
gramora/Utilities/View+ScrollSafeTextEditor.swift

@@ -0,0 +1,31 @@
+import SwiftUI
+
+extension View {
+    @ViewBuilder
+    func scrollSafeTextEditorFrame(compact: Bool) -> some View {
+        if compact {
+            frame(maxWidth: .infinity)
+                .frame(height: AppTheme.compactTextEditorHeight)
+        } else {
+            frame(maxWidth: .infinity, maxHeight: .infinity)
+        }
+    }
+
+    @ViewBuilder
+    func scrollSafeEditorSectionFrame(compact: Bool) -> some View {
+        if compact {
+            frame(maxWidth: .infinity)
+        } else {
+            frame(maxWidth: .infinity, maxHeight: .infinity)
+        }
+    }
+
+    @ViewBuilder
+    func scrollSafeContentCardFrame(compact: Bool) -> some View {
+        if compact {
+            frame(maxWidth: .infinity, alignment: .topLeading)
+        } else {
+            frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
+        }
+    }
+}

+ 36 - 1
gramora/Views/Components/ThinCaretTextEditor.swift

@@ -1,6 +1,33 @@
 import AppKit
 import SwiftUI
 
+private final class TextEditorScrollView: NSScrollView {
+    override func layout() {
+        super.layout()
+        syncTextContainerWidth()
+    }
+
+    override func resizeSubviews(withOldSize oldSize: NSSize) {
+        super.resizeSubviews(withOldSize: oldSize)
+        syncTextContainerWidth()
+    }
+
+    fileprivate func syncTextContainerWidthIfNeeded() {
+        syncTextContainerWidth()
+    }
+
+    private func syncTextContainerWidth() {
+        guard let textView = documentView as? NSTextView,
+              let container = textView.textContainer else { return }
+
+        let width = contentView.bounds.width
+        guard width > 1 else { return }
+
+        container.containerSize = NSSize(width: width, height: .greatestFiniteMagnitude)
+        textView.setFrameSize(NSSize(width: width, height: textView.frame.height))
+    }
+}
+
 private final class ThinCaretTextView: NSTextView {
     private let caretWidth: CGFloat = 1
     private let caretHeightBoost: CGFloat = 2
@@ -82,7 +109,7 @@ struct ThinCaretTextEditor: NSViewRepresentable {
     }
 
     func makeNSView(context: Context) -> NSScrollView {
-        let scrollView = NSScrollView()
+        let scrollView = TextEditorScrollView()
         scrollView.hasVerticalScroller = true
         scrollView.hasHorizontalScroller = false
         scrollView.autohidesScrollers = true
@@ -137,6 +164,14 @@ struct ThinCaretTextEditor: NSViewRepresentable {
 
         if textView.string != text {
             textView.string = text
+            textView.needsDisplay = true
+        }
+
+        textView.textColor = NSColor(AppTheme.textPrimary)
+        textView.font = NSFont.systemFont(ofSize: 14)
+
+        if let scrollView = scrollView as? TextEditorScrollView {
+            scrollView.syncTextContainerWidthIfNeeded()
         }
     }
 

+ 36 - 16
gramora/Views/ParaphrasingView.swift

@@ -3,13 +3,17 @@ import SwiftUI
 struct ParaphrasingView: View {
     @StateObject private var viewModel = ParaphrasingViewModel()
 
+    private var showsOutputLayout: Bool {
+        !viewModel.paraphrasedText.isEmpty || viewModel.isParaphrasing
+    }
+
     var body: some View {
         GeometryReader { proxy in
             let dynamicHorizontalPadding = min(max(proxy.size.width * 0.045, AppTheme.contentPadding), 52)
             let dynamicBottomPadding = min(max(proxy.size.height * 0.04, 20), 40)
 
             Group {
-                if viewModel.paraphrasedText.isEmpty {
+                if !showsOutputLayout {
                     VStack(alignment: .leading, spacing: 0) {
                         header
                             .padding(.bottom, 24)
@@ -107,19 +111,13 @@ struct ParaphrasingView: View {
     }
 
     private var contentCard: some View {
-        VStack(alignment: .leading, spacing: AppTheme.contentCardSpacing) {
+        let compactEditor = showsOutputLayout
+
+        return VStack(alignment: .leading, spacing: AppTheme.contentCardSpacing) {
             YourContentHeader()
 
             VStack(alignment: .leading, spacing: 6) {
-                ThinCaretTextEditor(
-                    text: $viewModel.text,
-                    placeholder: "Type or paste your text here and press the paraphrase button...",
-                    maxWords: ParaphrasingViewModel.maxWords,
-                    onWordLimitReached: viewModel.notifyWordLimitReached
-                )
-                    .font(.system(size: 14))
-                    .foregroundStyle(AppTheme.textPrimary)
-                    .frame(maxWidth: .infinity, maxHeight: .infinity)
+                textEditor
                     .background(Color.white)
                     .clipShape(RoundedRectangle(cornerRadius: AppTheme.inputCornerRadius))
                     .overlay(
@@ -132,7 +130,7 @@ struct ParaphrasingView: View {
                     .foregroundStyle(AppTheme.textMuted)
                     .frame(maxWidth: .infinity, alignment: .trailing)
             }
-            .frame(maxWidth: .infinity, maxHeight: .infinity)
+            .scrollSafeEditorSectionFrame(compact: compactEditor)
 
             HStack(spacing: 8) {
                 ToolbarButton(title: "Paste Text", iconName: "doc.on.clipboard") {
@@ -151,7 +149,7 @@ struct ParaphrasingView: View {
             }
         }
         .inputContentCardPadding()
-        .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
+        .scrollSafeContentCardFrame(compact: compactEditor)
         .background(AppTheme.cardBackground)
         .clipShape(RoundedRectangle(cornerRadius: AppTheme.cardCornerRadius))
         .overlay(
@@ -161,6 +159,18 @@ struct ParaphrasingView: View {
         .shadow(color: AppTheme.cardShadow, radius: 16, y: 6)
     }
 
+    private var textEditor: some View {
+        ThinCaretTextEditor(
+            text: $viewModel.text,
+            placeholder: "Type or paste your text here and press the paraphrase button...",
+            maxWords: ParaphrasingViewModel.maxWords,
+            onWordLimitReached: viewModel.notifyWordLimitReached
+        )
+        .font(.system(size: 14))
+        .foregroundStyle(AppTheme.textPrimary)
+        .scrollSafeTextEditorFrame(compact: showsOutputLayout)
+    }
+
     private var outputCard: some View {
         VStack(alignment: .leading, spacing: 12) {
             HStack {
@@ -173,11 +183,21 @@ struct ParaphrasingView: View {
                 ToolbarButton(title: "Copy", iconName: "doc.on.doc") {
                     viewModel.copyParaphrasedText()
                 }
+                .disabled(viewModel.paraphrasedText.isEmpty)
+                .opacity(viewModel.paraphrasedText.isEmpty ? 0.55 : 1)
             }
 
-            Text(viewModel.paraphrasedText)
-                .font(.system(size: 14))
-                .foregroundStyle(AppTheme.textPrimary)
+            Group {
+                if viewModel.isParaphrasing && viewModel.paraphrasedText.isEmpty {
+                    Text("Paraphrasing...")
+                        .font(.system(size: 14))
+                        .foregroundStyle(AppTheme.textMuted)
+                } else {
+                    Text(viewModel.paraphrasedText)
+                        .font(.system(size: 14))
+                        .foregroundStyle(AppTheme.textPrimary)
+                }
+            }
                 .frame(maxWidth: .infinity, alignment: .leading)
                 .padding(16)
                 .background(AppTheme.tealLight.opacity(0.35))

+ 36 - 16
gramora/Views/PunctuationCheckerView.swift

@@ -3,13 +3,17 @@ import SwiftUI
 struct PunctuationCheckerView: View {
     @StateObject private var viewModel = PunctuationCheckerViewModel()
 
+    private var showsOutputLayout: Bool {
+        !viewModel.correctedText.isEmpty || viewModel.isChecking
+    }
+
     var body: some View {
         GeometryReader { proxy in
             let dynamicHorizontalPadding = min(max(proxy.size.width * 0.045, AppTheme.contentPadding), 52)
             let dynamicBottomPadding = min(max(proxy.size.height * 0.04, 20), 40)
 
             Group {
-                if viewModel.correctedText.isEmpty {
+                if !showsOutputLayout {
                     VStack(alignment: .leading, spacing: 0) {
                         header
                             .padding(.bottom, 24)
@@ -107,19 +111,13 @@ struct PunctuationCheckerView: View {
     }
 
     private var contentCard: some View {
-        VStack(alignment: .leading, spacing: AppTheme.contentCardSpacing) {
+        let compactEditor = showsOutputLayout
+
+        return VStack(alignment: .leading, spacing: AppTheme.contentCardSpacing) {
             YourContentHeader()
 
             VStack(alignment: .leading, spacing: 6) {
-                ThinCaretTextEditor(
-                    text: $viewModel.text,
-                    placeholder: "Type or paste your text here and press the check button...",
-                    maxWords: PunctuationCheckerViewModel.maxWords,
-                    onWordLimitReached: viewModel.notifyWordLimitReached
-                )
-                    .font(.system(size: 14))
-                    .foregroundStyle(AppTheme.textPrimary)
-                    .frame(maxWidth: .infinity, maxHeight: .infinity)
+                textEditor
                     .background(Color.white)
                     .clipShape(RoundedRectangle(cornerRadius: AppTheme.inputCornerRadius))
                     .overlay(
@@ -132,7 +130,7 @@ struct PunctuationCheckerView: View {
                     .foregroundStyle(AppTheme.textMuted)
                     .frame(maxWidth: .infinity, alignment: .trailing)
             }
-            .frame(maxWidth: .infinity, maxHeight: .infinity)
+            .scrollSafeEditorSectionFrame(compact: compactEditor)
 
             HStack(spacing: 8) {
                 ToolbarButton(title: "Paste Text", iconName: "doc.on.clipboard") {
@@ -151,7 +149,7 @@ struct PunctuationCheckerView: View {
             }
         }
         .inputContentCardPadding()
-        .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
+        .scrollSafeContentCardFrame(compact: compactEditor)
         .background(AppTheme.cardBackground)
         .clipShape(RoundedRectangle(cornerRadius: AppTheme.cardCornerRadius))
         .overlay(
@@ -161,6 +159,18 @@ struct PunctuationCheckerView: View {
         .shadow(color: AppTheme.cardShadow, radius: 16, y: 6)
     }
 
+    private var textEditor: some View {
+        ThinCaretTextEditor(
+            text: $viewModel.text,
+            placeholder: "Type or paste your text here and press the check button...",
+            maxWords: PunctuationCheckerViewModel.maxWords,
+            onWordLimitReached: viewModel.notifyWordLimitReached
+        )
+        .font(.system(size: 14))
+        .foregroundStyle(AppTheme.textPrimary)
+        .scrollSafeTextEditorFrame(compact: showsOutputLayout)
+    }
+
     private var outputCard: some View {
         VStack(alignment: .leading, spacing: 12) {
             HStack {
@@ -173,11 +183,21 @@ struct PunctuationCheckerView: View {
                 ToolbarButton(title: "Copy", iconName: "doc.on.doc") {
                     viewModel.copyCorrectedText()
                 }
+                .disabled(viewModel.correctedText.isEmpty)
+                .opacity(viewModel.correctedText.isEmpty ? 0.55 : 1)
             }
 
-            Text(viewModel.correctedText)
-                .font(.system(size: 14))
-                .foregroundStyle(AppTheme.textPrimary)
+            Group {
+                if viewModel.isChecking && viewModel.correctedText.isEmpty {
+                    Text("Checking punctuation...")
+                        .font(.system(size: 14))
+                        .foregroundStyle(AppTheme.textMuted)
+                } else {
+                    Text(viewModel.correctedText)
+                        .font(.system(size: 14))
+                        .foregroundStyle(AppTheme.textPrimary)
+                }
+            }
                 .frame(maxWidth: .infinity, alignment: .leading)
                 .padding(16)
                 .background(AppTheme.tealLight.opacity(0.35))

+ 36 - 16
gramora/Views/SummarizerView.swift

@@ -3,13 +3,17 @@ import SwiftUI
 struct SummarizerView: View {
     @StateObject private var viewModel = SummarizerViewModel()
 
+    private var showsOutputLayout: Bool {
+        !viewModel.summary.isEmpty || viewModel.isSummarizing
+    }
+
     var body: some View {
         GeometryReader { proxy in
             let dynamicHorizontalPadding = min(max(proxy.size.width * 0.045, AppTheme.contentPadding), 52)
             let dynamicBottomPadding = min(max(proxy.size.height * 0.04, 20), 40)
 
             Group {
-                if viewModel.summary.isEmpty {
+                if !showsOutputLayout {
                     VStack(alignment: .leading, spacing: 0) {
                         header
                             .padding(.bottom, 24)
@@ -107,19 +111,13 @@ struct SummarizerView: View {
     }
 
     private var contentCard: some View {
-        VStack(alignment: .leading, spacing: AppTheme.contentCardSpacing) {
+        let compactEditor = showsOutputLayout
+
+        return VStack(alignment: .leading, spacing: AppTheme.contentCardSpacing) {
             YourContentHeader()
 
             VStack(alignment: .leading, spacing: 6) {
-                ThinCaretTextEditor(
-                    text: $viewModel.text,
-                    placeholder: "Type or paste your text here and press the summarize button...",
-                    maxWords: SummarizerViewModel.maxWords,
-                    onWordLimitReached: viewModel.notifyWordLimitReached
-                )
-                    .font(.system(size: 14))
-                    .foregroundStyle(AppTheme.textPrimary)
-                    .frame(maxWidth: .infinity, maxHeight: .infinity)
+                textEditor
                     .background(Color.white)
                     .clipShape(RoundedRectangle(cornerRadius: AppTheme.inputCornerRadius))
                     .overlay(
@@ -132,7 +130,7 @@ struct SummarizerView: View {
                     .foregroundStyle(AppTheme.textMuted)
                     .frame(maxWidth: .infinity, alignment: .trailing)
             }
-            .frame(maxWidth: .infinity, maxHeight: .infinity)
+            .scrollSafeEditorSectionFrame(compact: compactEditor)
 
             HStack(spacing: 8) {
                 ToolbarButton(title: "Paste Text", iconName: "doc.on.clipboard") {
@@ -151,7 +149,7 @@ struct SummarizerView: View {
             }
         }
         .inputContentCardPadding()
-        .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
+        .scrollSafeContentCardFrame(compact: compactEditor)
         .background(AppTheme.cardBackground)
         .clipShape(RoundedRectangle(cornerRadius: AppTheme.cardCornerRadius))
         .overlay(
@@ -161,6 +159,18 @@ struct SummarizerView: View {
         .shadow(color: AppTheme.cardShadow, radius: 16, y: 6)
     }
 
+    private var textEditor: some View {
+        ThinCaretTextEditor(
+            text: $viewModel.text,
+            placeholder: "Type or paste your text here and press the summarize button...",
+            maxWords: SummarizerViewModel.maxWords,
+            onWordLimitReached: viewModel.notifyWordLimitReached
+        )
+        .font(.system(size: 14))
+        .foregroundStyle(AppTheme.textPrimary)
+        .scrollSafeTextEditorFrame(compact: showsOutputLayout)
+    }
+
     private var outputCard: some View {
         VStack(alignment: .leading, spacing: 12) {
             HStack {
@@ -173,11 +183,21 @@ struct SummarizerView: View {
                 ToolbarButton(title: "Copy", iconName: "doc.on.doc") {
                     viewModel.copySummary()
                 }
+                .disabled(viewModel.summary.isEmpty)
+                .opacity(viewModel.summary.isEmpty ? 0.55 : 1)
             }
 
-            Text(viewModel.summary)
-                .font(.system(size: 14))
-                .foregroundStyle(AppTheme.textPrimary)
+            Group {
+                if viewModel.isSummarizing && viewModel.summary.isEmpty {
+                    Text("Summarizing...")
+                        .font(.system(size: 14))
+                        .foregroundStyle(AppTheme.textMuted)
+                } else {
+                    Text(viewModel.summary)
+                        .font(.system(size: 14))
+                        .foregroundStyle(AppTheme.textPrimary)
+                }
+            }
                 .frame(maxWidth: .infinity, alignment: .leading)
                 .padding(16)
                 .background(AppTheme.tealLight.opacity(0.35))