[Pitch] Writing Direction Attribute

Hi all! I have a pitch for a new AttributedStringKey serving as a currency type for specifying the base writing direction of a paragraph on AttributedStrings. Feel free to read below and let me know if you have any comments/questions!


Writing Direction Attribute

  • Proposal: SF-NNNN
  • Authors: Max Obermeier
  • Review Manager: TBD
  • Status: Pitch
  • Implementation: Awaiting Implementation

Motivation

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:

  1. It is impossible to specify only the base writing direction without also specifying values for the remaining paragraph style properties.

  2. The attribute does not utilize advanced AttributedStringKey behaviors such as runBoundaries or inheritedByAddedText.

  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.

Proposed solution

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.


// 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))

// To remove the information about the writing direction, set it to `nil`:

string.writingDirection = nil

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.


let range = 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

assert(string.runs[\.writingDirection].count == 1)

// When adding text to a paragraph, the existing writingDirection is applied to the new text.

string.append(AttributedString(" It is awesome for working with strings!"))

assert(string.runs[\.writingDirection].count == 1)

assert(string.writingDirection == .leftToRight)

// When adding a new paragraph, the new paragraph does not inherit the writing direction of the preceding paragraph.

string.append(AttributedString("\nThe new paragraph does not inherit the writing direction."))

assert(string.runs[\.writingDirection].count == 2)

assert(string.runs.last?.writingDirection == nil)


For the full proposal including the detailed design and alternatives considered, check out the full proposal document on the swift-foundation repo.

6 Likes
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
}

I think it’s unfortunate that the cases use left–right terms, but also apply to vertical layout. Is the equivalence of left-to-right ↔ top-to-bottom (and right-to-left ↔ bottom-to-top) a common convention? Is there a more general terminology that does not favor horizontal layout?

Maybe it would make sense to consider separate attributes for line direction and character direction as is done in NSLocale. Or is that something that should be left to the system displaying the attributed string?

1 Like

@FlorianPircher beat me to it, but I am also concerned that this is pitched as having two cases.

There are two dimensions to consider when it comes to writing direction, and actually more variations than a 2x2 matrix would suggest among the world's languages:

Based on the pitch specifically mentioning certain existing (platform-specific?) APIs, it sounds like the API is tailored more narrowly for a specific use case—reading between the lines, is it a convenience API for an existing toggle? I think this would be good to spell out, perhaps with some context for those who are not already familiar that points to other APIs which cover scenarios out of scope for this type.

As it is, the text strongly suggests that the pitch is for a currency type appropriate for indicating a general "fundamental property of strings" (writing direction) which could be used in "any context that deals with (potentially) bidirectional strings." If that is the intention, then for sure there's a whole lot more than just "LTR" and "RTL."


(These settings would seem to close off any possible future support for boustrophedon text, I think?)

2 Likes

Thank you @FlorianPircher and @xwu for your feedback! @xwu your link was a really interesting read!

I'd like to split this discussion into two parts: First, whether the proposed WritingDirection has the correct number of cases, and second, whether those cases are named correctly.


@xwu, I think the article you linked uses a slightly different definition of "writing direction". This definition contains both the writingDirection defined here, but also what Foundation refers to as lineLayoutDirection:

The lineLayoutDirection as defined by Foundation is of type 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. Note that other frameworks, e.g. CSS, refer to this concept as "writing mode".

Note that there is a difference in scope between lineLayoutDirection/writing mode, and writingDirection. 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.

Now, you're right, lineLayoutDirection + writingDirection as proposed only cover the 2x2x2 matrix of "regular" layouts, but not irregular layouts such as "Boustrophedon" or "Variable".

For "Boustrophedon" , I think 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 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.

For "Variable" all modern languages indicated there, they are just listed under "Variable" because they can be written using multiple variants of the 2x2x2 matrix of regular layouts. However, each document must still choose one variant, so they are covered using the proposed API. As for Hieroglyphic and Mayan script, 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.


Now, to the second part, the naming for the two cases of WritingDirection. I decided to propose the names leftToRight and rightToLeft, mostly because this is the industry standard. Existing definitions on Apple's platforms, as well as CSS 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. As mentioned before, I also wanted to avoid situations where one can specify a writingDirection that is not orthogonal to the lineLayoutDirection.

Ideally, I would have preferred a solution where I could have provided topToBottom and bottomToTop 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. I could have added additional static members like this:

extension AttributedString.WritingDirection {
    public static let topToBottom: Self = .leftToRight
    public static let bottomToTop: Self = .rightToLeft
}

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.

Therefore, the best solution I saw was using the names that are industry standard, and clearly documenting their interpretation in vertical scripts.

8 Likes

This is excellent background--thank you. For the sake of exposition, some of this background would be useful text to have in the proposal that would help readers contextualize what's proposed.

I think it's clear that this is one API of several that together form a reasonable design. It's too bad about the switch exhaustivity limitations but I agree documentation is the way to go here.

4 Likes

@xwu thank you again for your input! I have updated the proposal document to include these discussion points in the alternatives considered section.

1 Like

Thank you all for the great discussion. I'd like to accept the proposal with this pitch since its scope is fairly small. Thanks again!

3 Likes