use gpui::{App, Global, Pixels, Styled, TextAlign};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum LayoutDirection {
#[default]
LeftToRight,
RightToLeft,
}
impl LayoutDirection {
pub fn is_rtl(self) -> bool {
matches!(self, Self::RightToLeft)
}
pub fn is_ltr(self) -> bool {
matches!(self, Self::LeftToRight)
}
pub fn start(self) -> PhysicalSide {
match self {
Self::LeftToRight => PhysicalSide::Left,
Self::RightToLeft => PhysicalSide::Right,
}
}
pub fn end(self) -> PhysicalSide {
self.start().opposite()
}
pub fn arrow_step(self, key: &str) -> Option<i32> {
let forward = match self {
Self::LeftToRight => 1,
Self::RightToLeft => -1,
};
match key {
"right" => Some(forward),
"left" => Some(-forward),
_ => None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum LogicalSide {
Start,
End,
}
impl LogicalSide {
pub fn resolve(self, direction: LayoutDirection) -> PhysicalSide {
match self {
Self::Start => direction.start(),
Self::End => direction.end(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum PhysicalSide {
Left,
Right,
}
impl PhysicalSide {
pub fn opposite(self) -> Self {
match self {
Self::Left => Self::Right,
Self::Right => Self::Left,
}
}
pub fn is_left(self) -> bool {
matches!(self, Self::Left)
}
}
#[derive(Debug, Default)]
struct Direction(LayoutDirection);
impl Global for Direction {}
pub trait ActiveDirection {
fn layout_direction(&self) -> LayoutDirection;
fn is_rtl(&self) -> bool {
self.layout_direction().is_rtl()
}
}
impl ActiveDirection for App {
fn layout_direction(&self) -> LayoutDirection {
self.try_global::<Direction>()
.map(|direction| direction.0)
.unwrap_or_default()
}
}
pub fn install(cx: &mut App) {
if !cx.has_global::<Direction>() {
cx.set_global(Direction::default());
}
}
pub fn set_layout_direction(direction: LayoutDirection, cx: &mut App) {
cx.set_global(Direction(direction));
cx.refresh_windows();
}
pub trait DirectionalExt: Styled + Sized {
fn row_reading(self, direction: LayoutDirection) -> Self {
let element = self.flex().items_center();
if direction.is_rtl() {
element.flex_row_reverse()
} else {
element.flex_row()
}
}
fn ps(self, direction: LayoutDirection, length: Pixels) -> Self {
if direction.is_rtl() {
self.pr(length)
} else {
self.pl(length)
}
}
fn pe(self, direction: LayoutDirection, length: Pixels) -> Self {
if direction.is_rtl() {
self.pl(length)
} else {
self.pr(length)
}
}
fn ms(self, direction: LayoutDirection, length: Pixels) -> Self {
if direction.is_rtl() {
self.mr(length)
} else {
self.ml(length)
}
}
fn me(self, direction: LayoutDirection, length: Pixels) -> Self {
if direction.is_rtl() {
self.ml(length)
} else {
self.mr(length)
}
}
fn border_s(self, direction: LayoutDirection, width: Pixels) -> Self {
if direction.is_rtl() {
self.border_r(width)
} else {
self.border_l(width)
}
}
fn border_e(self, direction: LayoutDirection, width: Pixels) -> Self {
if direction.is_rtl() {
self.border_l(width)
} else {
self.border_r(width)
}
}
fn text_start(self, direction: LayoutDirection) -> Self {
self.text_align(match direction {
LayoutDirection::LeftToRight => TextAlign::Left,
LayoutDirection::RightToLeft => TextAlign::Right,
})
}
fn text_end(self, direction: LayoutDirection) -> Self {
self.text_align(match direction {
LayoutDirection::LeftToRight => TextAlign::Right,
LayoutDirection::RightToLeft => TextAlign::Left,
})
}
}
impl<T: Styled + Sized> DirectionalExt for T {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_default_direction_reads_left_to_right() {
assert_eq!(LayoutDirection::default(), LayoutDirection::LeftToRight);
assert!(!LayoutDirection::default().is_rtl());
assert_eq!(LayoutDirection::default().start(), PhysicalSide::Left);
}
#[test]
fn a_logical_edge_swaps_and_a_physical_one_does_not() {
let ltr = LayoutDirection::LeftToRight;
let rtl = LayoutDirection::RightToLeft;
assert_eq!(LogicalSide::Start.resolve(ltr), PhysicalSide::Left);
assert_eq!(LogicalSide::Start.resolve(rtl), PhysicalSide::Right);
assert_eq!(LogicalSide::End.resolve(ltr), PhysicalSide::Right);
assert_eq!(LogicalSide::End.resolve(rtl), PhysicalSide::Left);
assert_eq!(PhysicalSide::Right.opposite(), PhysicalSide::Left);
assert_eq!(PhysicalSide::Right, PhysicalSide::Right);
}
#[test]
fn arrow_keys_step_the_way_the_reading_direction_says() {
let ltr = LayoutDirection::LeftToRight;
let rtl = LayoutDirection::RightToLeft;
assert_eq!(ltr.arrow_step("right"), Some(1));
assert_eq!(ltr.arrow_step("left"), Some(-1));
assert_eq!(rtl.arrow_step("right"), Some(-1));
assert_eq!(rtl.arrow_step("left"), Some(1));
assert_eq!(ltr.arrow_step("up"), None);
assert_eq!(rtl.arrow_step("down"), None);
assert_eq!(rtl.arrow_step("home"), None);
}
}