[RFC] Add support for RTL
RTL Support in Contour
Background
Contour is heavily used in the Cash App codebase, but it doesn't support RTL layouts and there are some things that make support non-trivial to implement.
One is Contour's design as a math driven layout. The math is intuitive when you're working with left and right because they're just raw numbers - you can do something like parent.left() + 8.xdip and it makes sense. It gets a little more difficult when you work with start and end because they're not just numbers, they're offsets from a reference point. So something like parent.start() + 8.xdip is a little weird. If you just treat parent.start() as a number, then this statement gives you a nice margin in LTR layouts but clips the view in RTL layouts. Intuitively what you want is not really "plus" but instead "increment away from start".
Another challenge is axis type safety. The existing type system does not allow startTo { parent.top() } and it seems reasonable to extend that by disallowing startTo { parent.left() }. At the same time it should be possible to do both startTo { parent.centerX() } and leftTo { parent.centerX() }. Finding a type hierarchy that supports valid use cases but not invalid ones is tricky.
This PR is one way that we could implement RTL support, but I'm relatively new to Contour so I'm far from confident it's the best way. I do think it highlights a lot of the challenges though, so I think it's a reasonable place to start the conversation. I'd love to get feedback from folks who have looked into this before or have more Contour expertise than I do!
Design
The core of this PR is splitting XInt into multiple subtypes. The hierarchy looks like this:

Start and end constraints require a StartEndCompatibleXInt, so you can do things like:
-
startTo { parent.start() }.endTo { parent.end() } -
startTo { parent.start() + 8.xdip }.endTo { parent.end() - 8.xdip } -
startTo { parent.centerX() }.widthOf { 48.xdip } -
startTo { sibling.start() }.endTo { sibling.end() } -
startTo { sibling.end() }.endTo { parent.end() } -
startTo { endOf(siblingA.end(), siblingB.end()) }
But not:
-
startTo { parent.left() } -
startTo { parent.start() }.rightTo { parent.right() } -
startTo { maxOf(siblingA.right(), siblingB.right()) }
Testing
I looked through and documented the first 60 or so uses of Contour in the Cash App codebase to get a feel for the types of constraints that get used in practice. The vast majority are just simple alignment to the parent or a sibling. I put together a sample app that tests about 30 common constraints based on what I saw:
- Center Horizontally to Parent Center
- Center Horizontally to Sibling { Left, Right, Start, End, Center }
- Left to Parent { Left, Center }
- Left to Sibling { Left, Right, Center }
- Right to Parent { Right, Center }
- Right to Sibling { Left, Right, Center }
- Start to Parent { Start, Center }
- Start to Sibling { Start, End, Center }
- End to Parent { End, Center }
- End to Sibling { Start, End, Center }
- Match Parent
- Match Sibling
All of those work as expected in the RTL version of Contour and I'm happy to share a demo if it's useful.
I also tested this with Cash App (not converting views to be RTL ready, just bumping the Contour version and fixing compilation errors). Everything compiles and seems to work in my light testing, but I definitely haven't explored the entire app in depth. Also happy to share a sample PR of what that would look like.
Any thoughts on this PR? @theisenp put a lot of effort into it
Oof, yeah should get this in. Will find some time when I get a chance.
Closing this out since we've deprecated Contour