From 2167b17696f2b88a7e9219368be4bdb0aaed19e9 Mon Sep 17 00:00:00 2001 From: austinpower1258 Date: Fri, 29 May 2026 21:50:38 -0700 Subject: [PATCH 1/2] Support natural RTL direction in text composers --- Sources/ContentView.swift | 6 ++++ Sources/Feed/FeedPanelView.swift | 3 ++ Sources/NaturalTextCompositionDirection.swift | 33 +++++++++++++++++++ Sources/TextBoxInput.swift | 18 +++++++++- cmux.xcodeproj/project.pbxproj | 4 +++ 5 files changed, 63 insertions(+), 1 deletion(-) create mode 100644 Sources/NaturalTextCompositionDirection.swift diff --git a/Sources/ContentView.swift b/Sources/ContentView.swift index 35efd3db0e..72a6e05e6c 100644 --- a/Sources/ContentView.swift +++ b/Sources/ContentView.swift @@ -4486,6 +4486,7 @@ struct ContentView: View { width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude ) + textView.configureCmuxNaturalWritingDirectionForComposedText() scrollView.documentView = textView placeholderField.translatesAutoresizingMaskIntoConstraints = false @@ -4768,6 +4769,7 @@ struct ContentView: View { view.placeholder = placeholder view.maximumHeight = maxHeight view.textView.string = text + view.textView.applyCmuxNaturalWritingDirectionToComposedText() view.textView.delegate = context.coordinator view.textView.setAccessibilityLabel(accessibilityLabel) view.textView.setAccessibilityIdentifier(accessibilityIdentifier) @@ -4803,6 +4805,7 @@ struct ContentView: View { if nsView.textView.string != text { context.coordinator.isProgrammaticMutation = true nsView.textView.string = text + nsView.textView.applyCmuxNaturalWritingDirectionToComposedText() context.coordinator.isProgrammaticMutation = false } nsView.onMeasuredHeightChange = { [weak coordinator = context.coordinator] height in @@ -12622,6 +12625,7 @@ private struct FeedbackComposerMessageEditor: NSViewRepresentable { let view = FeedbackComposerMessageEditorView() view.placeholder = placeholder view.textView.string = text + view.textView.applyCmuxNaturalWritingDirectionToComposedText() view.textView.delegate = context.coordinator view.textView.setAccessibilityLabel(accessibilityLabel) view.textView.setAccessibilityIdentifier(accessibilityIdentifier) @@ -12632,6 +12636,7 @@ private struct FeedbackComposerMessageEditor: NSViewRepresentable { func updateNSView(_ nsView: FeedbackComposerMessageEditorView, context: Context) { if nsView.textView.string != text { nsView.textView.string = text + nsView.textView.applyCmuxNaturalWritingDirectionToComposedText() nsView.refreshTextLayout() } nsView.placeholder = placeholder @@ -12730,6 +12735,7 @@ final class FeedbackComposerMessageEditorView: NSView { width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude ) + textView.configureCmuxNaturalWritingDirectionForComposedText() scrollView.documentView = textView addSubview(scrollView) diff --git a/Sources/Feed/FeedPanelView.swift b/Sources/Feed/FeedPanelView.swift index 81bd2156d3..cb06918c02 100644 --- a/Sources/Feed/FeedPanelView.swift +++ b/Sources/Feed/FeedPanelView.swift @@ -3257,6 +3257,7 @@ private final class FeedInlineTextEditorView: NSView { width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude ) + textView.configureCmuxNaturalWritingDirectionForComposedText() addSubview(textView) placeholderField.textColor = .placeholderTextColor @@ -3451,6 +3452,7 @@ private struct FeedInlineTextField: NSViewRepresentable { let view = FeedInlineTextEditorView(frame: .zero) view.textView.delegate = context.coordinator view.textView.string = text + view.textView.applyCmuxNaturalWritingDirectionToComposedText() view.textView.onActivate = { [weak coordinator = context.coordinator] in coordinator?.activateField() } @@ -3478,6 +3480,7 @@ private struct FeedInlineTextField: NSViewRepresentable { if nsView.textView.string != text, !nsView.textView.hasMarkedText() { context.coordinator.isProgrammaticMutation = true nsView.textView.string = text + nsView.textView.applyCmuxNaturalWritingDirectionToComposedText() context.coordinator.isProgrammaticMutation = false nsView.refreshMetrics() } diff --git a/Sources/NaturalTextCompositionDirection.swift b/Sources/NaturalTextCompositionDirection.swift new file mode 100644 index 0000000000..440a227126 --- /dev/null +++ b/Sources/NaturalTextCompositionDirection.swift @@ -0,0 +1,33 @@ +import AppKit + +extension NSTextView { + func configureCmuxNaturalWritingDirectionForComposedText() { + baseWritingDirection = .natural + alignment = .natural + + let paragraphStyle = cmuxNaturalComposedTextParagraphStyle() + defaultParagraphStyle = paragraphStyle + + var attributes = typingAttributes + attributes[.paragraphStyle] = paragraphStyle + typingAttributes = attributes + } + + func applyCmuxNaturalWritingDirectionToComposedText() { + configureCmuxNaturalWritingDirectionForComposedText() + + let fullRange = NSRange(location: 0, length: (string as NSString).length) + guard fullRange.length > 0 else { return } + + textStorage?.setAlignment(.natural, range: fullRange) + textStorage?.setBaseWritingDirection(.natural, range: fullRange) + } + + func cmuxNaturalComposedTextParagraphStyle() -> NSParagraphStyle { + let paragraphStyle = (defaultParagraphStyle?.mutableCopy() as? NSMutableParagraphStyle) + ?? NSMutableParagraphStyle() + paragraphStyle.alignment = .natural + paragraphStyle.baseWritingDirection = .natural + return paragraphStyle.copy() as? NSParagraphStyle ?? paragraphStyle + } +} diff --git a/Sources/TextBoxInput.swift b/Sources/TextBoxInput.swift index 7fa0c9aee2..fffe1b2809 100644 --- a/Sources/TextBoxInput.swift +++ b/Sources/TextBoxInput.swift @@ -3690,6 +3690,7 @@ struct TextBoxInputView: NSViewRepresentable { textView.textContainerInset = TextBoxLayout.textInset textView.textContainer?.lineFragmentPadding = 0 textView.registerForDraggedTypes([.fileURL]) + textView.configureCmuxNaturalWritingDirectionForComposedText() let scrollView = NSScrollView() scrollView.drawsBackground = false @@ -3727,6 +3728,7 @@ struct TextBoxInputView: NSViewRepresentable { } if textView.inlineAttachments().isEmpty && textView.plainText() != text { textView.string = text + textView.applyCmuxNaturalWritingDirectionToComposedText() } updateTextView(textView, context: context) } @@ -3750,6 +3752,7 @@ struct TextBoxInputView: NSViewRepresentable { textView.onInsertFileURLs = onInsertFileURLs textView.onChooseFiles = onChooseFiles textView.refreshInlineAttachmentCells(font: font, foregroundColor: foregroundColor) + textView.configureCmuxNaturalWritingDirectionForComposedText() textView.recenterSingleLineTextContainer() textView.wantsLayer = true textView.layer?.backgroundColor = NSColor.clear.cgColor @@ -3886,6 +3889,16 @@ final class TextBoxInputTextView: NSTextView { attachmentPreviewPopover?.isShown == true } + override init(frame frameRect: NSRect, textContainer container: NSTextContainer?) { + super.init(frame: frameRect, textContainer: container) + configureCmuxNaturalWritingDirectionForComposedText() + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + deinit { dismissMentionCompletions() removeAttachmentKeyDownMonitor() @@ -3997,6 +4010,7 @@ final class TextBoxInputTextView: NSTextView { dismissMentionCompletions() clearAttachmentFocus(dismissPreview: true) textStorage?.setAttributedString(NSAttributedString(string: "")) + configureCmuxNaturalWritingDirectionForComposedText() recenterSingleLineTextContainer() didChangeText() } @@ -4019,6 +4033,7 @@ final class TextBoxInputTextView: NSTextView { dismissMentionCompletions() clearAttachmentFocus(dismissPreview: true) textStorage?.setAttributedString(content) + applyCmuxNaturalWritingDirectionToComposedText() refreshInlineAttachmentCells( font: font ?? NSFont.systemFont(ofSize: NSFont.systemFontSize), foregroundColor: textColor ?? .labelColor @@ -5398,7 +5413,8 @@ final class TextBoxInputTextView: NSTextView { [ .font: explicitFont ?? font ?? NSFont.systemFont(ofSize: NSFont.systemFontSize), .foregroundColor: explicitForegroundColor ?? textColor ?? .labelColor, - .baselineOffset: textBaselineOffsetForCurrentContent() + .baselineOffset: textBaselineOffsetForCurrentContent(), + .paragraphStyle: cmuxNaturalComposedTextParagraphStyle() ] } diff --git a/cmux.xcodeproj/project.pbxproj b/cmux.xcodeproj/project.pbxproj index 0510406821..b3483ac3d9 100644 --- a/cmux.xcodeproj/project.pbxproj +++ b/cmux.xcodeproj/project.pbxproj @@ -315,6 +315,7 @@ E1000000A1B2C3D4E5F60718 /* MenuKeyEquivalentRoutingUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1000001A1B2C3D4E5F60718 /* MenuKeyEquivalentRoutingUITests.swift */; }; C0DE3150A00000000000001 /* MinimalModeSidebarControls.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0DE3150A00000000000002 /* MinimalModeSidebarControls.swift */; }; B9000015A1B2C3D4E5F60719 /* MultiWindowNotificationsUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9000016A1B2C3D4E5F60719 /* MultiWindowNotificationsUITests.swift */; }; + C0DE50070000000000000001 /* NaturalTextCompositionDirection.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0DE50070000000000000002 /* NaturalTextCompositionDirection.swift */; }; 734F49D37E543DD01C2F4FEF /* NotificationAndMenuBarTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2C075029771815DD5DA1332 /* NotificationAndMenuBarTests.swift */; }; A5001094 /* NotificationsPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001091 /* NotificationsPage.swift */; }; 4378399A7C0245EF8186F306 /* OmnibarAndToolsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B09C007F42697761B5F1A2AB /* OmnibarAndToolsTests.swift */; }; @@ -930,6 +931,7 @@ E1000001A1B2C3D4E5F60718 /* MenuKeyEquivalentRoutingUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuKeyEquivalentRoutingUITests.swift; sourceTree = ""; }; C0DE3150A00000000000002 /* MinimalModeSidebarControls.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Update/MinimalModeSidebarControls.swift; sourceTree = ""; }; B9000016A1B2C3D4E5F60719 /* MultiWindowNotificationsUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultiWindowNotificationsUITests.swift; sourceTree = ""; }; + C0DE50070000000000000002 /* NaturalTextCompositionDirection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NaturalTextCompositionDirection.swift; sourceTree = ""; }; D2C075029771815DD5DA1332 /* NotificationAndMenuBarTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationAndMenuBarTests.swift; sourceTree = ""; }; A5001091 /* NotificationsPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsPage.swift; sourceTree = ""; }; B09C007F42697761B5F1A2AB /* OmnibarAndToolsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OmnibarAndToolsTests.swift; sourceTree = ""; }; @@ -1428,6 +1430,7 @@ C7934BB35B66491B1BCA8064 /* MenuBarExtraController.swift */, 2F0C07000000000000000001 /* CmuxMainWindow.swift */, 2F0C06000000000000000001 /* MainWindowVisibilityController.swift */, + C0DE50070000000000000002 /* NaturalTextCompositionDirection.swift */, A500D011A1B2C3D4E5F60718 /* DebugLogging.swift */, D35B71010000000000000002 /* StartupBreadcrumbLog.swift */, A50016B0A1B2C3D4E5F60718 /* SessionSnapshotDebugBenchmark.swift */, @@ -2319,6 +2322,7 @@ 1A8BEE693C9E4C3190CB7F20 /* MenuBarExtraController.swift in Sources */, 3865A0033865A0033865A003 /* MenubarSearchPopover.swift in Sources */, C0DE3150A00000000000001 /* MinimalModeSidebarControls.swift in Sources */, + C0DE50070000000000000001 /* NaturalTextCompositionDirection.swift in Sources */, A5001094 /* NotificationsPage.swift in Sources */, D0B1001CA1B2C3D4E5F60001 /* PaneDropRoutingSupport.swift in Sources */, A5001400 /* Panel.swift in Sources */, From 3aa84a167e0a3341e96ecb7dc63c2bf9cd8f69b7 Mon Sep 17 00:00:00 2001 From: austinpower1258 Date: Fri, 29 May 2026 21:59:31 -0700 Subject: [PATCH 2/2] Address composer RTL review feedback --- Sources/NaturalTextCompositionDirection.swift | 2 +- Sources/TextBoxInput.swift | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/Sources/NaturalTextCompositionDirection.swift b/Sources/NaturalTextCompositionDirection.swift index 440a227126..a645fa0bf6 100644 --- a/Sources/NaturalTextCompositionDirection.swift +++ b/Sources/NaturalTextCompositionDirection.swift @@ -28,6 +28,6 @@ extension NSTextView { ?? NSMutableParagraphStyle() paragraphStyle.alignment = .natural paragraphStyle.baseWritingDirection = .natural - return paragraphStyle.copy() as? NSParagraphStyle ?? paragraphStyle + return paragraphStyle.copy() as! NSParagraphStyle } } diff --git a/Sources/TextBoxInput.swift b/Sources/TextBoxInput.swift index fffe1b2809..46173d26d9 100644 --- a/Sources/TextBoxInput.swift +++ b/Sources/TextBoxInput.swift @@ -3690,7 +3690,6 @@ struct TextBoxInputView: NSViewRepresentable { textView.textContainerInset = TextBoxLayout.textInset textView.textContainer?.lineFragmentPadding = 0 textView.registerForDraggedTypes([.fileURL]) - textView.configureCmuxNaturalWritingDirectionForComposedText() let scrollView = NSScrollView() scrollView.drawsBackground = false @@ -3752,7 +3751,6 @@ struct TextBoxInputView: NSViewRepresentable { textView.onInsertFileURLs = onInsertFileURLs textView.onChooseFiles = onChooseFiles textView.refreshInlineAttachmentCells(font: font, foregroundColor: foregroundColor) - textView.configureCmuxNaturalWritingDirectionForComposedText() textView.recenterSingleLineTextContainer() textView.wantsLayer = true textView.layer?.backgroundColor = NSColor.clear.cgColor