Skip to content

Writing Direction Attribute Implementation #1245

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Apr 11, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,14 @@ extension AttributeScopes {

@available(macOS 13, iOS 16, tvOS 16, watchOS 9, *)
public let durationField: DurationFieldAttribute


/// The base writing direction of a paragraph.
#if FOUNDATION_FRAMEWORK
@_spi(AttributedStringWritingDirection)
#endif
@available(FoundationPreview 6.2, *)
public let writingDirection: WritingDirectionAttribute

#if FOUNDATION_FRAMEWORK
@available(FoundationPreview 0.1, *)
public let agreementConcept: AgreementConceptAttribute
Expand Down Expand Up @@ -507,7 +514,21 @@ extension AttributeScopes.FoundationAttributes {
case nanoseconds
}
}


/// The attribute key for the base writing direction of a paragraph.
#if FOUNDATION_FRAMEWORK
@_spi(AttributedStringWritingDirection)
#endif
@available(FoundationPreview 6.2, *)
@frozen
public enum WritingDirectionAttribute: CodableAttributedStringKey {
public typealias Value = AttributedString.WritingDirection
public static let name: String = "Foundation.WritingDirectionAttribute"

public static let runBoundaries: AttributedString.AttributeRunBoundaries? = .paragraph
public static let inheritedByAddedText = false
}

#if FOUNDATION_FRAMEWORK
@available(macOS 13, iOS 16, tvOS 16, watchOS 9, *)
public struct LocalizedStringArgumentAttributes {
Expand Down Expand Up @@ -647,6 +668,9 @@ extension AttributeScopes.FoundationAttributes.ByteCountAttribute : Sendable {}
@available(*, unavailable)
extension AttributeScopes.FoundationAttributes.DurationFieldAttribute : Sendable {}

@available(*, unavailable)
extension AttributeScopes.FoundationAttributes.WritingDirectionAttribute: Sendable {}

#if FOUNDATION_FRAMEWORK

@available(macOS, unavailable, introduced: 14.0)
Expand Down Expand Up @@ -819,3 +843,72 @@ extension AttributeScopes.FoundationAttributes.LocalizedNumberFormatAttribute.Va
}

#endif // FOUNDATION_FRAMEWORK

extension AttributedString {
/// The writing direction of a piece of text.
///
/// Writing direction defines the base direction in which bidirectional text
/// lays out its directional runs. A directional run is a contigous sequence
/// of characters that all have the same effective directionality, which can
/// be determined using the Unicode BiDi algorithm. The ``leftToRight``
/// writing direction puts the directional run that is placed first in the
/// storage leftmost, and places subsequent directional runs towards the
/// right. The ``rightToLeft`` writing direction puts the directional run
/// that is placed first in the storage rightmost, and places subsequent
/// directional runs towards the left.
///
/// Note that writing direction is a property separate from a text's
/// alignment, its line layout direction, or its character direction.
/// However, it is often used to determine the default alignment of a
/// paragraph. E.g. English (a language with
/// ``Locale/LanguageDirection-swift.enum/leftToRight``
/// ``Locale/Language-swift.struct/characterDirection``) is usually aligned
/// to the left, but may be centered or aligned to the right for special
/// effect, or to be visually more appealing in a user interface.
///
/// For bidirectional text to be perceived as laid out correctly, make sure
/// that the writing direction is set to the value equivalent to the
/// ``Locale/Language-swift.struct/characterDirection`` of the primary
/// language in the text. E.g. an English sentence that contains some
/// Arabic (a language with
/// ``Locale/LanguageDirection-swift.enum/rightToLeft``
/// ``Locale/Language-swift.struct/characterDirection``) words, should use
/// a ``leftToRight`` writing direction. An Arabic sentence that contains
/// some English words, should use a ``rightToLeft`` writing direction.
///
/// Writing direction is always orthogonoal to the line layout direction
/// chosen to display a certain text. The line layout direction is the
/// direction in which a sequence of lines is placed in. E.g. English text
/// is usually displayed with a line layout direction of
/// ``Locale/LanguageDirection-swift.enum/topToBottom``. While languages do
/// have an associated line language direction (see
/// ``Locale/Language-swift.struct/lineLayoutDirection``), not all displays
/// of text follow the line layout direction of the text's primary language.
///
/// Horizontal script is script with a line layout direction of either
/// ``Locale/LanguageDirection-swift.enum/topToBottom`` or
/// ``Locale/LanguageDirection-swift.enum/bottomToTop``. Vertical script
/// has a ``Locale/LanguageDirection-swift.enum/leftToRight`` or
/// ``Locale/LanguageDirection-swift.enum/rightToLeft`` line layout
/// direction. In vertical scripts, a writing direction of ``leftToRight``
/// is interpreted as top-to-bottom and a writing direction of
/// ``rightToLeft`` is interpreted as bottom-to-top.
#if FOUNDATION_FRAMEWORK
@_spi(AttributedStringWritingDirection)
#endif
@available(FoundationPreview 6.2, *)
@frozen
public enum WritingDirection: Codable, Hashable, CaseIterable, Sendable {
/// A left-to-right writing direction in horizontal script.
///
/// - Note: In vertical scripts, this equivalent to a top-to-bottom
/// writing direction.
case leftToRight

/// A right-to-left writing direction in horizontal script.
///
/// - Note: In vertical scripts, this equivalent to a bottom-to-top
/// writing direction.
case rightToLeft
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import TestSupport
#endif // FOUNDATION_FRAMEWORK

#if FOUNDATION_FRAMEWORK
@testable @_spi(AttributedString) import Foundation
@testable @_spi(AttributedString) @_spi(AttributedStringWritingDirection) import Foundation
// For testing default attribute scope conversion
#if canImport(Accessibility)
import Accessibility
Expand Down Expand Up @@ -2599,4 +2599,30 @@ E {
XCTAssertEqual(testConstrainedContainer.filter(inheritedByAddedText: true), AttributeContainer.testInt(2).testParagraphConstrained(3).testCharacterConstrained(4))
XCTAssertEqual(testConstrainedContainer.filter(inheritedByAddedText: false), AttributeContainer.testNonExtended(5))
}

func testWritingDirectionBehavior() throws {
// Indicate that this sentence is primarily right to left, because the English term "Swift" is embedded into an Arabic sentence.
var string = AttributedString("Swift مذهل!", attributes: .init().writingDirection(.rightToLeft))

XCTAssertEqual(string.writingDirection, .rightToLeft)

// To remove the information about the writing direction, set it to `nil`:
string.writingDirection = nil

XCTAssertEqual(string.writingDirection, nil)

let range = try XCTUnwrap(string.range(of: "Swift"))

// 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:
string[range].writingDirection = .leftToRight
XCTAssertEqual(string.runs[\.writingDirection].count, 1)

string.append(AttributedString(" It is awesome for working with strings!"))
XCTAssertEqual(string.runs[\.writingDirection].count, 1)
XCTAssertEqual(string.writingDirection, .leftToRight)

string.append(AttributedString("\nThe new paragraph does not inherit the writing direction."))
XCTAssertEqual(string.runs[\.writingDirection].count, 2)
XCTAssertEqual(string.runs.last?.writingDirection, nil)
}
}