Skip to content

Commit edcf9ac

Browse files
[Proposal] Writing Direction Attribute (#1234)
* add the writing direction attribute proposal * add alternatives considered for wiritng direction cases and naming * update status and implementation * Update and rename NNNN-writing-direction-attribute.md to 0022-writing-direction-attribute.md --------- Co-authored-by: Jeremy Schonfeld <[email protected]>
1 parent 99c5c0c commit edcf9ac

File tree

1 file changed

+217
-0
lines changed

1 file changed

+217
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
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

Comments
 (0)