|
| 1 | +# Writing Direction Attribute |
| 2 | + |
| 3 | +* Proposal: [SF-0022](NNNN-writing-direction-attribute.md) |
| 4 | +* Authors: [Max Obermeier](https://github.com/themomax) |
| 5 | +* Review Manager: [Tina L](https://github.com/itingliu) |
| 6 | +* Status: **Approved and Implemented** |
| 7 | +* Implementation: [swiftlang/swift-foundation#1245](https://github.com/swiftlang/swift-foundation/pull/1245) |
| 8 | +* Review: ([pitch](https://forums.swift.org/t/pitch-writing-direction-attribute/78924)) |
| 9 | + |
| 10 | +## Introduction |
| 11 | + |
| 12 | +Adds an `AttributedStringKey` for the base writing direction of a paragraph. |
| 13 | + |
| 14 | +## Motivation |
| 15 | + |
| 16 | +`AttributedString` currently has no way to express the base writing direction of a paragraph as a standalone property. Some UI frameworks, such as UIKit or AppKit define a pargraph style property that includes - among other properties - the base writing direction. This attribute originated in the context of `NSAttributedString` and has a couple of disadvantages: |
| 17 | + |
| 18 | + 1. It is impossible to specify only the base writing direction without also specifying values for the remaining paragraph style properties. |
| 19 | + 2. The attribute does not utilize advanced `AttributedStringKey` behaviors such as `runBoundaries` or `inheritedByAddedText`. |
| 20 | + 3. Writing direction is a fundamental property of strings that is not only relevant in UI frameworks, but needs to be communicated in any context that deals with (potentially) bidirectional strings. |
| 21 | + |
| 22 | +## Proposed solution |
| 23 | + |
| 24 | +This proposal adds a new `AttributedString.WritingDirection` enum with two cases `leftToRight` and `rightToLeft`, along with a new key `WritingDirectionAttribute`, which is included in `AttributeScopes.FoundationAttributes` under the name `writingDirection`. |
| 25 | + |
| 26 | +```swift |
| 27 | +// Indicate that this sentence is primarily right to left, because the English term "Swift" is embedded into an Arabic sentence. |
| 28 | +var string = AttributedString("Swift مذهل!", attributes: .init().writingDirection(.rightToLeft)) |
| 29 | + |
| 30 | +// To remove the information about the writing direction, set it to `nil`: |
| 31 | +string.writingDirection = nil |
| 32 | +``` |
| 33 | + |
| 34 | +Since the base writing direction is defined at a paragraph level, the attribute specifies `runBoundaries = .paragraph`. Since the writing direction of one paragraph is independent of the next, the attribute is not `inheritedByAddedText`. |
| 35 | + |
| 36 | +```swift |
| 37 | +let range = string.range(of: "Swift")! |
| 38 | + |
| 39 | +// 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: |
| 40 | +string[range].writingDirection = .leftToRight |
| 41 | +assert(string.runs[\.writingDirection].count == 1) |
| 42 | + |
| 43 | +// When adding text to a paragraph, the existing writingDirection is applied to the new text. |
| 44 | +string.append(AttributedString(" It is awesome for working with strings!")) |
| 45 | +assert(string.runs[\.writingDirection].count == 1) |
| 46 | +assert(string.writingDirection == .leftToRight) |
| 47 | + |
| 48 | +// When adding a new paragraph, the new paragraph does not inherit the writing direction of the preceding paragraph. |
| 49 | +string.append(AttributedString("\nThe new paragraph does not inherit the writing direction.")) |
| 50 | +assert(string.runs[\.writingDirection].count == 2) |
| 51 | +assert(string.runs.last?.writingDirection == nil) |
| 52 | +``` |
| 53 | + |
| 54 | +## Detailed design |
| 55 | + |
| 56 | +```swift |
| 57 | +extension AttributedString { |
| 58 | + /// The writing direction of a piece of text. |
| 59 | + /// |
| 60 | + /// Writing direction defines the base direction in which bidirectional text |
| 61 | + /// lays out its directional runs. A directional run is a contigous sequence |
| 62 | + /// of characters that all have the same effective directionality, which can |
| 63 | + /// be determined using the Unicode BiDi algorithm. The ``leftToRight`` |
| 64 | + /// writing direction puts the directional run that is placed first in the |
| 65 | + /// storage leftmost, and places subsequent directional runs towards the |
| 66 | + /// right. The ``rightToLeft`` writing direction puts the directional run |
| 67 | + /// that is placed first in the storage rightmost, and places subsequent |
| 68 | + /// directional runs towards the left. |
| 69 | + /// |
| 70 | + /// Note that writing direction is a property separate from a text's |
| 71 | + /// alignment, its line layout direction, or its character direction. |
| 72 | + /// However, it is often used to determine the default alignment of a |
| 73 | + /// paragraph. E.g. English (a language with |
| 74 | + /// ``Locale/LanguageDirection-swift.enum/leftToRight`` |
| 75 | + /// ``Locale/Language-swift.struct/characterDirection``) is usually aligned |
| 76 | + /// to the left, but may be centered or aligned to the right for special |
| 77 | + /// effect, or to be visually more appealing in a user interface. |
| 78 | + /// |
| 79 | + /// For bidirectional text to be perceived as laid out correctly, make sure |
| 80 | + /// that the writing direction is set to the value equivalent to the |
| 81 | + /// ``Locale/Language-swift.struct/characterDirection`` of the primary |
| 82 | + /// language in the text. E.g. an English sentence that contains some |
| 83 | + /// Arabic (a language with |
| 84 | + /// ``Locale/LanguageDirection-swift.enum/rightToLeft`` |
| 85 | + /// ``Locale/Language-swift.struct/characterDirection``) words, should use |
| 86 | + /// a ``leftToRight`` writing direction. An Arabic sentence that contains |
| 87 | + /// some English words, should use a ``rightToLeft`` writing direction. |
| 88 | + /// |
| 89 | + /// Writing direction is always orthogonoal to the line layout direction |
| 90 | + /// chosen to display a certain text. The line layout direction is the |
| 91 | + /// direction in which a sequence of lines is placed in. E.g. English text |
| 92 | + /// is usually displayed with a line layout direction of |
| 93 | + /// ``Locale/LanguageDirection-swift.enum/topToBottom``. While languages do |
| 94 | + /// have an associated line language direction (see |
| 95 | + /// ``Locale/Language-swift.struct/lineLayoutDirection``), not all displays |
| 96 | + /// of text follow the line layout direction of the text's primary language. |
| 97 | + /// |
| 98 | + /// Horizontal script is script with a line layout direction of either |
| 99 | + /// ``Locale/LanguageDirection-swift.enum/topToBottom`` or |
| 100 | + /// ``Locale/LanguageDirection-swift.enum/bottomToTop``. Vertical script |
| 101 | + /// has a ``Locale/LanguageDirection-swift.enum/leftToRight`` or |
| 102 | + /// ``Locale/LanguageDirection-swift.enum/rightToLeft`` line layout |
| 103 | + /// direction. In vertical scripts, a writing direction of ``leftToRight`` |
| 104 | + /// is interpreted as top-to-bottom and a writing direction of |
| 105 | + /// ``rightToLeft`` is interpreted as bottom-to-top. |
| 106 | + @available(FoundationPreview 6.2, *) |
| 107 | + @frozen |
| 108 | + public enum WritingDirection: Codable, Hashable, CaseIterable, Sendable { |
| 109 | + /// A left-to-right writing direction in horizontal script. |
| 110 | + /// |
| 111 | + /// - Note: In vertical scripts, this equivalent to a top-to-bottom |
| 112 | + /// writing direction. |
| 113 | + case leftToRight |
| 114 | + |
| 115 | + /// A right-to-left writing direction in horizontal script. |
| 116 | + /// |
| 117 | + /// - Note: In vertical scripts, this equivalent to a bottom-to-top |
| 118 | + /// writing direction. |
| 119 | + case rightToLeft |
| 120 | + } |
| 121 | +} |
| 122 | + |
| 123 | +extension AttributeScopes.FoundationAttributes { |
| 124 | + /// The base writing direction of a paragraph. |
| 125 | + @available(FoundationPreview 6.2, *) |
| 126 | + public let writingDirection: WritingDirectionAttribute |
| 127 | + |
| 128 | + /// The attribute key for the base writing direction of a paragraph. |
| 129 | + @available(FoundationPreview 6.2, *) |
| 130 | + @frozen |
| 131 | + public enum WritingDirectionAttribute: CodableAttributedStringKey { |
| 132 | + public typealias Value = AttributedString.WritingDirection |
| 133 | + public static let name: String = "Foundation.WritingDirection" |
| 134 | + |
| 135 | + public static let runBoundaries: AttributedString |
| 136 | + .AttributeRunBoundaries? = .paragraph |
| 137 | + |
| 138 | + public static let inheritedByAddedText = false |
| 139 | + } |
| 140 | +} |
| 141 | + |
| 142 | +@available(*, unavailable) |
| 143 | +extension AttributeScopes.FoundationAttributes.WritingDirectionAttribute: Sendable { } |
| 144 | +``` |
| 145 | + |
| 146 | +## Source compatibility |
| 147 | + |
| 148 | +These changes are additive-only and do not impact the source compatibility of existing apps. |
| 149 | + |
| 150 | +## Implications on adoption |
| 151 | + |
| 152 | +These new APIs will be annotated with `FoundationPreview 6.2` availability. |
| 153 | + |
| 154 | +## Future directions |
| 155 | + |
| 156 | +### Automatically determining the writing direction of text based on string analysis |
| 157 | + |
| 158 | +As detailed in the documentation for `WritingDirection`, for bidirectional text to be perceived as laid out correctly, one should make sure that the writing direction is set to the value equivalent to the character direction of the primary language in the text. Foundation could provide API that automatically determines the appropriate writing direction by analyzing the (strong) directionality of characters in a string or the directionality resulting from applying the BiDi algorithm to the string. |
| 159 | + |
| 160 | +## Alternatives considered |
| 161 | + |
| 162 | +### Adding `natural` or `unknown` `WritingDirection`s |
| 163 | + |
| 164 | +Other definitions of writing direction types, e.g. [UIKit's `NSWritingDirection`](https://developer.apple.com/documentation/uikit/nswritingdirection), include a `natural` case, indicating that the writing direction should be determined through other strategies. The value of any `AttributedStringKey` is optional in an `AttributedString` or `AttributeContainer`, so the absence of the attribute shall be intepreted as `natural` or `unknown`. |
| 165 | + |
| 166 | +If a package, framework, or project offers multiple strategies to determine the effective writing direction of a paragraph, it should define a separate setting - as an `AttributedStringKey` or otherwise - that defines how the absence of the `writingDirection` value is to be interpreted. |
| 167 | + |
| 168 | +### Adding more WritingDirection cases for vertical script |
| 169 | + |
| 170 | +Often times, "writing direction" is used with a broader definition than here, basically encompassing all aspects of a script's layout, including line layout and character direction. We have decided to use a narrower definition, because of a number of reasons: |
| 171 | + |
| 172 | +The [`lineLayoutDirection`](https://developer.apple.com/documentation/foundation/locale/language/4020200-linelayoutdirection) as defined by Foundation is of type [`LanguageDirection`](https://developer.apple.com/documentation/foundation/nslocale/languagedirection). 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 `topToBottom`. Other frameworks, e.g. [CSS](https://developer.mozilla.org/en-US/docs/Web/CSS/writing-mode), refer to this concept as "writing mode". |
| 173 | + |
| 174 | +There is a difference in scope between `lineLayoutDirection`/writing mode, and `writingDirection` as defined here. The former is always defined for a whole "layout", whereas `writingDirection` is defined on a part of the layout where line contents flow together, i.e. a paragraph. That is why `lineLayoutDirection` should not be a property of a subrange of an `AttributedString`, but should be defined on a UI component/document level. In short, you cannot change the `lineLayoutDirection` mid-page, so it should be kept separate from the `WritingDirectionAttribute`. To account for the fact that the `writingDirection` must always be orthogonal to the `lineLayoutDirection`, I only propose two cases with documented meaning for both horizontal and vertical script, avoiding the possibility for invalid configurations (e.g. a `topToBottom` `lineLayoutDefinition` (meaning text within a line has to flow horizontally) paired with a `topToBottom` `writingDirection`. |
| 175 | + |
| 176 | +Based on that definition, `lineLayoutDirection` + `writingDirection` only cover the 2x2x2 matrix of "regular" layouts, but not irregular layouts such as "Boustrophedon". |
| 177 | + |
| 178 | +For "Boustrophedon" , the reality to acknowledge there is that it just isn't part of the widely supported scripts in modern computer systems. E.g. the writing direction attribute used by [CSS](https://developer.mozilla.org/en-US/docs/Web/CSS/direction) also only lists `ltr` and `rtl`. If we wanted to add support for switching line flow direction within a paragraph at some point in the future, I think this should be expressed as a separate attribute that integrates with `writingDirection`. E.g. `writingDirection` would specify the direction for the first line, and the separate attribute could specify the pattern for when to switch line flow direction. |
| 179 | + |
| 180 | +Other scripts, such as Hieroglyphic and Mayan script, use variable writing directions. They are again not widely supported by modern computer systems and the layout there even requires some amount of artistic intent, so one would probably carefully lay out each line as its own paragraph with separate writing direction anyway. |
| 181 | + |
| 182 | +Therefore, the proposed definition satisfies all use-cases commonly supported by modern computer systems, and is even extensible where it could provide support for more niche scripts in the future. |
| 183 | + |
| 184 | +### Naming of the WritingDirection cases |
| 185 | + |
| 186 | +The naming for the two cases of `WritingDirection`, `leftToRight` and `rightToLeft`, was chosen mostly because this is the industry standard. [Existing definitions on Apple's platforms](https://developer.apple.com/documentation/uikit/nswritingdirection), as well as [CSS](https://developer.mozilla.org/en-US/docs/Web/CSS/direction) use this terminology. I don't think diverging from this industry standard in favor of terms neutral to the axis of the script (vertical vs horizontal) would be a win for developers. I also wanted to avoid situations where one can specify a `writingDirection` that is not orthogonal to the `lineLayoutDirection`. |
| 187 | + |
| 188 | +Ideally, I would have preferred a solution where `topToBottom` and `bottomToTop` could have been provided as alternative case _names_ for `leftToRight` and `rightToLeft` respectively, so one can use those names e.g. in a `switch` statement that is written in the context of a vertical script. However, Swift doesn't support having two names for the same enum `case` at the moment. Adding additional static members like this would have been possible: |
| 189 | + |
| 190 | +```swift |
| 191 | +extension AttributedString.WritingDirection { |
| 192 | + public static let topToBottom: Self = .leftToRight |
| 193 | + public static let bottomToTop: Self = .rightToLeft |
| 194 | +} |
| 195 | +``` |
| 196 | + |
| 197 | +However, if you try using `topToBottom` and `bottomToTop` defined like this in a `switch` statement, Swift will produce a compiler error because the cases `leftToRight` and `rightToLeft` are not covered by the `switch` statement. |
| 198 | + |
| 199 | +Therefore, the best solution seemed to be using the names that are industry standard, and clearly documenting their interpretation in vertical scripts. |
| 200 | + |
| 201 | +### Invalidating `WritingDirection` on text changes |
| 202 | + |
| 203 | +The `AttributeScopes.FoundationAttributes.WritingDirectionAttribute` may be added by an applicaiton or framework as the result of character analysis. In that case it would be great if the writing direction was removed again automatically whenever the characters of the paragraph changed. |
| 204 | + |
| 205 | +This could be accomplished using a simple `invalidationCondition` with value `.textChanged` on the `WritingDirectionAttribute`. However, that would mean that even after explicitly setting the writing direction to a value that was not determined via character analysis, the writing direction attribute would invalidate every time the character changes. Instead, we would only want this behavior in the scenario where the attribute value was actually determined via character analysis. |
| 206 | + |
| 207 | +For the writing direction to behave correctly, both for the case case of an explicit writing direction and one determined via character analysis, a second attribute would be needed: |
| 208 | + |
| 209 | +`AttributeScopes.FoundationAttributes.WritingDirectionAnalysisAttribute` would have `runBoundaries` `paragraph`, `inheritedByAddedText = false`, and `invalidationCondition` `.textChanged`, so it essentially resets to `nil` every time the characters change. |
| 210 | + |
| 211 | +Then, we add a `invalidationCondition = [.attributeChanged(AttributeScopes.FoundationAttributes.WritingDirectionAnalysisAttribute)]` to `WritingDirectionAttribute`. That way, the explicit writing direction should only reset when the text changes _and_ the writing direction was determined via string analysis, which would add `AttributeScopes.FoundationAttributes.WritingDirectionAnalysisAttribute` to the analized ranges. |
| 212 | + |
| 213 | +`AttributedString` currently does not offer a mechanism for determining writing direction based on character analysis. Any future proposals adding such mechanism should consider adding the `WritingDirectionAnalysisAttribute`. |
| 214 | + |
| 215 | +## Acknowledgments |
| 216 | + |
| 217 | +Special thanks to all those who contributed to the direction of this proposal, especially Karan Miśra for providing a lot of helpful insight! |
0 commit comments