Skip to content

Commit 78a0395

Browse files
authored
Writing Direction Attribute Implementation (#1245)
* implementation * address review
1 parent 201a45b commit 78a0395

File tree

2 files changed

+122
-3
lines changed

2 files changed

+122
-3
lines changed

Sources/FoundationEssentials/AttributedString/FoundationAttributes.swift

Lines changed: 95 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,14 @@ extension AttributeScopes {
3434

3535
@available(macOS 13, iOS 16, tvOS 16, watchOS 9, *)
3636
public let durationField: DurationFieldAttribute
37-
37+
38+
/// The base writing direction of a paragraph.
39+
#if FOUNDATION_FRAMEWORK
40+
@_spi(AttributedStringWritingDirection)
41+
#endif
42+
@available(FoundationPreview 6.2, *)
43+
public let writingDirection: WritingDirectionAttribute
44+
3845
#if FOUNDATION_FRAMEWORK
3946
@available(FoundationPreview 0.1, *)
4047
public let agreementConcept: AgreementConceptAttribute
@@ -507,7 +514,21 @@ extension AttributeScopes.FoundationAttributes {
507514
case nanoseconds
508515
}
509516
}
510-
517+
518+
/// The attribute key for the base writing direction of a paragraph.
519+
#if FOUNDATION_FRAMEWORK
520+
@_spi(AttributedStringWritingDirection)
521+
#endif
522+
@available(FoundationPreview 6.2, *)
523+
@frozen
524+
public enum WritingDirectionAttribute: CodableAttributedStringKey {
525+
public typealias Value = AttributedString.WritingDirection
526+
public static let name: String = "Foundation.WritingDirectionAttribute"
527+
528+
public static let runBoundaries: AttributedString.AttributeRunBoundaries? = .paragraph
529+
public static let inheritedByAddedText = false
530+
}
531+
511532
#if FOUNDATION_FRAMEWORK
512533
@available(macOS 13, iOS 16, tvOS 16, watchOS 9, *)
513534
public struct LocalizedStringArgumentAttributes {
@@ -647,6 +668,9 @@ extension AttributeScopes.FoundationAttributes.ByteCountAttribute : Sendable {}
647668
@available(*, unavailable)
648669
extension AttributeScopes.FoundationAttributes.DurationFieldAttribute : Sendable {}
649670

671+
@available(*, unavailable)
672+
extension AttributeScopes.FoundationAttributes.WritingDirectionAttribute: Sendable {}
673+
650674
#if FOUNDATION_FRAMEWORK
651675

652676
@available(macOS, unavailable, introduced: 14.0)
@@ -819,3 +843,72 @@ extension AttributeScopes.FoundationAttributes.LocalizedNumberFormatAttribute.Va
819843
}
820844

821845
#endif // FOUNDATION_FRAMEWORK
846+
847+
extension AttributedString {
848+
/// The writing direction of a piece of text.
849+
///
850+
/// Writing direction defines the base direction in which bidirectional text
851+
/// lays out its directional runs. A directional run is a contigous sequence
852+
/// of characters that all have the same effective directionality, which can
853+
/// be determined using the Unicode BiDi algorithm. The ``leftToRight``
854+
/// writing direction puts the directional run that is placed first in the
855+
/// storage leftmost, and places subsequent directional runs towards the
856+
/// right. The ``rightToLeft`` writing direction puts the directional run
857+
/// that is placed first in the storage rightmost, and places subsequent
858+
/// directional runs towards the left.
859+
///
860+
/// Note that writing direction is a property separate from a text's
861+
/// alignment, its line layout direction, or its character direction.
862+
/// However, it is often used to determine the default alignment of a
863+
/// paragraph. E.g. English (a language with
864+
/// ``Locale/LanguageDirection-swift.enum/leftToRight``
865+
/// ``Locale/Language-swift.struct/characterDirection``) is usually aligned
866+
/// to the left, but may be centered or aligned to the right for special
867+
/// effect, or to be visually more appealing in a user interface.
868+
///
869+
/// For bidirectional text to be perceived as laid out correctly, make sure
870+
/// that the writing direction is set to the value equivalent to the
871+
/// ``Locale/Language-swift.struct/characterDirection`` of the primary
872+
/// language in the text. E.g. an English sentence that contains some
873+
/// Arabic (a language with
874+
/// ``Locale/LanguageDirection-swift.enum/rightToLeft``
875+
/// ``Locale/Language-swift.struct/characterDirection``) words, should use
876+
/// a ``leftToRight`` writing direction. An Arabic sentence that contains
877+
/// some English words, should use a ``rightToLeft`` writing direction.
878+
///
879+
/// Writing direction is always orthogonoal to the line layout direction
880+
/// chosen to display a certain text. The line layout direction is the
881+
/// direction in which a sequence of lines is placed in. E.g. English text
882+
/// is usually displayed with a line layout direction of
883+
/// ``Locale/LanguageDirection-swift.enum/topToBottom``. While languages do
884+
/// have an associated line language direction (see
885+
/// ``Locale/Language-swift.struct/lineLayoutDirection``), not all displays
886+
/// of text follow the line layout direction of the text's primary language.
887+
///
888+
/// Horizontal script is script with a line layout direction of either
889+
/// ``Locale/LanguageDirection-swift.enum/topToBottom`` or
890+
/// ``Locale/LanguageDirection-swift.enum/bottomToTop``. Vertical script
891+
/// has a ``Locale/LanguageDirection-swift.enum/leftToRight`` or
892+
/// ``Locale/LanguageDirection-swift.enum/rightToLeft`` line layout
893+
/// direction. In vertical scripts, a writing direction of ``leftToRight``
894+
/// is interpreted as top-to-bottom and a writing direction of
895+
/// ``rightToLeft`` is interpreted as bottom-to-top.
896+
#if FOUNDATION_FRAMEWORK
897+
@_spi(AttributedStringWritingDirection)
898+
#endif
899+
@available(FoundationPreview 6.2, *)
900+
@frozen
901+
public enum WritingDirection: Codable, Hashable, CaseIterable, Sendable {
902+
/// A left-to-right writing direction in horizontal script.
903+
///
904+
/// - Note: In vertical scripts, this equivalent to a top-to-bottom
905+
/// writing direction.
906+
case leftToRight
907+
908+
/// A right-to-left writing direction in horizontal script.
909+
///
910+
/// - Note: In vertical scripts, this equivalent to a bottom-to-top
911+
/// writing direction.
912+
case rightToLeft
913+
}
914+
}

Tests/FoundationEssentialsTests/AttributedString/AttributedStringTests.swift

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import TestSupport
1919
#endif // FOUNDATION_FRAMEWORK
2020

2121
#if FOUNDATION_FRAMEWORK
22-
@testable @_spi(AttributedString) import Foundation
22+
@testable @_spi(AttributedString) @_spi(AttributedStringWritingDirection) import Foundation
2323
// For testing default attribute scope conversion
2424
#if canImport(Accessibility)
2525
import Accessibility
@@ -2599,4 +2599,30 @@ E {
25992599
XCTAssertEqual(testConstrainedContainer.filter(inheritedByAddedText: true), AttributeContainer.testInt(2).testParagraphConstrained(3).testCharacterConstrained(4))
26002600
XCTAssertEqual(testConstrainedContainer.filter(inheritedByAddedText: false), AttributeContainer.testNonExtended(5))
26012601
}
2602+
2603+
func testWritingDirectionBehavior() throws {
2604+
// Indicate that this sentence is primarily right to left, because the English term "Swift" is embedded into an Arabic sentence.
2605+
var string = AttributedString("Swift مذهل!", attributes: .init().writingDirection(.rightToLeft))
2606+
2607+
XCTAssertEqual(string.writingDirection, .rightToLeft)
2608+
2609+
// To remove the information about the writing direction, set it to `nil`:
2610+
string.writingDirection = nil
2611+
2612+
XCTAssertEqual(string.writingDirection, nil)
2613+
2614+
let range = try XCTUnwrap(string.range(of: "Swift"))
2615+
2616+
// When setting or removing the value from a certain range, the value will always be applied to the entire paragraph(s) that intersect with that range:
2617+
string[range].writingDirection = .leftToRight
2618+
XCTAssertEqual(string.runs[\.writingDirection].count, 1)
2619+
2620+
string.append(AttributedString(" It is awesome for working with strings!"))
2621+
XCTAssertEqual(string.runs[\.writingDirection].count, 1)
2622+
XCTAssertEqual(string.writingDirection, .leftToRight)
2623+
2624+
string.append(AttributedString("\nThe new paragraph does not inherit the writing direction."))
2625+
XCTAssertEqual(string.runs[\.writingDirection].count, 2)
2626+
XCTAssertEqual(string.runs.last?.writingDirection, nil)
2627+
}
26022628
}

0 commit comments

Comments
 (0)