Skip to main content

gpui_kit/foundation/
direction.rs

1//! Which way the interface reads, and the logical styling that follows.
2//!
3//! GPUI has no notion of a reading direction: `Styled` exposes `pl`/`pr`,
4//! `ml`/`mr`, `border_l`/`border_r` and a `TextAlign` of `Left`, `Center` and
5//! `Right`, all of them physical, and taffy's logical properties are not
6//! surfaced. So the direction is a global here, reaching components the same
7//! way [`Theme`](gpui_kit_theme::Theme) does: a host sets it once, components
8//! read it during render, and nothing is threaded through a builder argument.
9//!
10//! The vocabulary is *start* and *end* rather than left and right. Start is
11//! where reading begins, so it is the left edge in a left-to-right interface
12//! and the right edge in a right-to-left one. Anything that is genuinely
13//! physical — the edge a vertical scrollbar sits on, the axis of a chart —
14//! keeps saying left and right, because those do not move when the reading
15//! direction does.
16
17use gpui::{App, Global, Pixels, Styled, TextAlign};
18
19/// The direction the interface reads in.
20///
21/// Left to right is the default, so a host that never sets one renders
22/// exactly as it did before this existed.
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
24pub enum LayoutDirection {
25    #[default]
26    LeftToRight,
27    RightToLeft,
28}
29
30impl LayoutDirection {
31    pub fn is_rtl(self) -> bool {
32        matches!(self, Self::RightToLeft)
33    }
34
35    pub fn is_ltr(self) -> bool {
36        matches!(self, Self::LeftToRight)
37    }
38
39    /// The physical edge that reading begins at.
40    pub fn start(self) -> PhysicalSide {
41        match self {
42            Self::LeftToRight => PhysicalSide::Left,
43            Self::RightToLeft => PhysicalSide::Right,
44        }
45    }
46
47    /// The physical edge that reading ends at.
48    pub fn end(self) -> PhysicalSide {
49        self.start().opposite()
50    }
51
52    /// How far one step through reading order moves when a physical arrow key
53    /// is pressed. `None` for a key that is not a horizontal arrow.
54    ///
55    /// A `Left` arrow means "previous" while reading left to right and "next"
56    /// while reading right to left, so a component that steps a selection asks
57    /// this instead of matching on the key name.
58    pub fn arrow_step(self, key: &str) -> Option<i32> {
59        let forward = match self {
60            Self::LeftToRight => 1,
61            Self::RightToLeft => -1,
62        };
63        match key {
64            "right" => Some(forward),
65            "left" => Some(-forward),
66            _ => None,
67        }
68    }
69}
70
71/// An edge named by reading order rather than by geometry.
72#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
73pub enum LogicalSide {
74    Start,
75    End,
76}
77
78impl LogicalSide {
79    pub fn resolve(self, direction: LayoutDirection) -> PhysicalSide {
80        match self {
81            Self::Start => direction.start(),
82            Self::End => direction.end(),
83        }
84    }
85}
86
87/// An edge that does not move when the reading direction does.
88#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
89pub enum PhysicalSide {
90    Left,
91    Right,
92}
93
94impl PhysicalSide {
95    pub fn opposite(self) -> Self {
96        match self {
97            Self::Left => Self::Right,
98            Self::Right => Self::Left,
99        }
100    }
101
102    pub fn is_left(self) -> bool {
103        matches!(self, Self::Left)
104    }
105}
106
107/// The one place the active direction lives.
108#[derive(Debug, Default)]
109struct Direction(LayoutDirection);
110
111impl Global for Direction {}
112
113/// Reads the active reading direction from any context that dereferences to
114/// [`App`], the way [`ActiveTheme`](gpui_kit_theme::ActiveTheme) reads the
115/// theme.
116pub trait ActiveDirection {
117    fn layout_direction(&self) -> LayoutDirection;
118
119    fn is_rtl(&self) -> bool {
120        self.layout_direction().is_rtl()
121    }
122}
123
124impl ActiveDirection for App {
125    fn layout_direction(&self) -> LayoutDirection {
126        // Defaulted rather than required, so a host that installed nothing
127        // still gets the left-to-right layout it had before.
128        self.try_global::<Direction>()
129            .map(|direction| direction.0)
130            .unwrap_or_default()
131    }
132}
133
134/// Installs the direction global at its default. Idempotent.
135pub fn install(cx: &mut App) {
136    if !cx.has_global::<Direction>() {
137        cx.set_global(Direction::default());
138    }
139}
140
141/// Sets the reading direction and repaints every window.
142pub fn set_layout_direction(direction: LayoutDirection, cx: &mut App) {
143    cx.set_global(Direction(direction));
144    cx.refresh_windows();
145}
146
147/// Logical box model helpers, named by reading order.
148///
149/// Each one takes the direction explicitly rather than reaching for the
150/// global, because a component has already read it once by the time it styles
151/// anything and passing it keeps these usable in a test with no application.
152pub trait DirectionalExt: Styled + Sized {
153    /// A flex row that runs in reading order.
154    fn row_reading(self, direction: LayoutDirection) -> Self {
155        let element = self.flex().items_center();
156        if direction.is_rtl() {
157            element.flex_row_reverse()
158        } else {
159            element.flex_row()
160        }
161    }
162
163    /// Padding on the edge reading begins at.
164    fn ps(self, direction: LayoutDirection, length: Pixels) -> Self {
165        if direction.is_rtl() {
166            self.pr(length)
167        } else {
168            self.pl(length)
169        }
170    }
171
172    /// Padding on the edge reading ends at.
173    fn pe(self, direction: LayoutDirection, length: Pixels) -> Self {
174        if direction.is_rtl() {
175            self.pl(length)
176        } else {
177            self.pr(length)
178        }
179    }
180
181    /// Margin on the edge reading begins at.
182    fn ms(self, direction: LayoutDirection, length: Pixels) -> Self {
183        if direction.is_rtl() {
184            self.mr(length)
185        } else {
186            self.ml(length)
187        }
188    }
189
190    /// Margin on the edge reading ends at.
191    fn me(self, direction: LayoutDirection, length: Pixels) -> Self {
192        if direction.is_rtl() {
193            self.ml(length)
194        } else {
195            self.mr(length)
196        }
197    }
198
199    /// A border on the edge reading begins at.
200    fn border_s(self, direction: LayoutDirection, width: Pixels) -> Self {
201        if direction.is_rtl() {
202            self.border_r(width)
203        } else {
204            self.border_l(width)
205        }
206    }
207
208    /// A border on the edge reading ends at.
209    fn border_e(self, direction: LayoutDirection, width: Pixels) -> Self {
210        if direction.is_rtl() {
211            self.border_l(width)
212        } else {
213            self.border_r(width)
214        }
215    }
216
217    /// Text aligned to where reading begins.
218    fn text_start(self, direction: LayoutDirection) -> Self {
219        self.text_align(match direction {
220            LayoutDirection::LeftToRight => TextAlign::Left,
221            LayoutDirection::RightToLeft => TextAlign::Right,
222        })
223    }
224
225    /// Text aligned to where reading ends.
226    fn text_end(self, direction: LayoutDirection) -> Self {
227        self.text_align(match direction {
228            LayoutDirection::LeftToRight => TextAlign::Right,
229            LayoutDirection::RightToLeft => TextAlign::Left,
230        })
231    }
232}
233
234impl<T: Styled + Sized> DirectionalExt for T {}
235
236#[cfg(test)]
237mod tests {
238    use super::*;
239
240    #[test]
241    fn the_default_direction_reads_left_to_right() {
242        assert_eq!(LayoutDirection::default(), LayoutDirection::LeftToRight);
243        assert!(!LayoutDirection::default().is_rtl());
244        assert_eq!(LayoutDirection::default().start(), PhysicalSide::Left);
245    }
246
247    #[test]
248    fn a_logical_edge_swaps_and_a_physical_one_does_not() {
249        let ltr = LayoutDirection::LeftToRight;
250        let rtl = LayoutDirection::RightToLeft;
251
252        assert_eq!(LogicalSide::Start.resolve(ltr), PhysicalSide::Left);
253        assert_eq!(LogicalSide::Start.resolve(rtl), PhysicalSide::Right);
254        assert_eq!(LogicalSide::End.resolve(ltr), PhysicalSide::Right);
255        assert_eq!(LogicalSide::End.resolve(rtl), PhysicalSide::Left);
256
257        // A physical edge is the same edge in either reading direction: that
258        // is the whole reason it is spelled physically.
259        assert_eq!(PhysicalSide::Right.opposite(), PhysicalSide::Left);
260        assert_eq!(PhysicalSide::Right, PhysicalSide::Right);
261    }
262
263    #[test]
264    fn arrow_keys_step_the_way_the_reading_direction_says() {
265        let ltr = LayoutDirection::LeftToRight;
266        let rtl = LayoutDirection::RightToLeft;
267
268        assert_eq!(ltr.arrow_step("right"), Some(1));
269        assert_eq!(ltr.arrow_step("left"), Some(-1));
270        assert_eq!(rtl.arrow_step("right"), Some(-1));
271        assert_eq!(rtl.arrow_step("left"), Some(1));
272
273        // A vertical arrow is not a reading-order move in either direction.
274        assert_eq!(ltr.arrow_step("up"), None);
275        assert_eq!(rtl.arrow_step("down"), None);
276        assert_eq!(rtl.arrow_step("home"), None);
277    }
278}