gpui_kit/foundation/
direction.rs1use gpui::{App, Global, Pixels, Styled, TextAlign};
18
19#[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 pub fn start(self) -> PhysicalSide {
41 match self {
42 Self::LeftToRight => PhysicalSide::Left,
43 Self::RightToLeft => PhysicalSide::Right,
44 }
45 }
46
47 pub fn end(self) -> PhysicalSide {
49 self.start().opposite()
50 }
51
52 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#[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#[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#[derive(Debug, Default)]
109struct Direction(LayoutDirection);
110
111impl Global for Direction {}
112
113pub 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 self.try_global::<Direction>()
129 .map(|direction| direction.0)
130 .unwrap_or_default()
131 }
132}
133
134pub fn install(cx: &mut App) {
136 if !cx.has_global::<Direction>() {
137 cx.set_global(Direction::default());
138 }
139}
140
141pub fn set_layout_direction(direction: LayoutDirection, cx: &mut App) {
143 cx.set_global(Direction(direction));
144 cx.refresh_windows();
145}
146
147pub trait DirectionalExt: Styled + Sized {
153 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 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 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 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 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 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 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 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 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 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 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}