Skip to main content

snora_core/
direction.rs

1//! Logical layout direction — the foundation of ABDD (Accessible By
2//! Default and by Design).
3//!
4//! snora expresses layout in terms of **logical edges** ([`Edge::Start`] /
5//! [`Edge::End`]) rather than physical directions (left / right). An
6//! application picks a [`LayoutDirection`] at runtime, and the engine maps
7//! logical edges to physical positions accordingly.
8
9/// Reading direction of the application's layout.
10///
11/// This is a framework-level setting. Individual widgets do not need to be
12/// re-authored for RTL — the engine consumes this value at every point
13/// where "left" or "right" would otherwise be hardcoded (sidebar side, toast
14/// anchor, header end-controls, etc.).
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
16pub enum LayoutDirection {
17    /// Left-to-right (e.g. English, Japanese, most languages).
18    #[default]
19    Ltr,
20    /// Right-to-left (e.g. Arabic, Hebrew, Persian).
21    Rtl,
22}
23
24impl LayoutDirection {
25    /// Flip the direction. Useful for a user-facing "Flip LTR / RTL" toggle
26    /// during development or accessibility preference changes.
27    #[must_use]
28    pub fn flipped(self) -> Self {
29        match self {
30            LayoutDirection::Ltr => LayoutDirection::Rtl,
31            LayoutDirection::Rtl => LayoutDirection::Ltr,
32        }
33    }
34
35    /// Returns `true` if the logical [`Edge::Start`] maps to the *physical*
36    /// left side of the screen under this direction.
37    ///
38    /// Useful for engines when they need to decide whether a start-anchored
39    /// element should be pushed first or last in a horizontal row.
40    #[must_use]
41    pub fn start_is_left(self) -> bool {
42        matches!(self, LayoutDirection::Ltr)
43    }
44}
45
46/// A logical position along a primary axis.
47///
48/// `Start` is the side a reader's eye begins at; `End` is where it finishes.
49/// In LTR this maps to (Left, Right); in RTL it maps to (Right, Left).
50#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
51pub enum Edge {
52    Start,
53    End,
54}
55
56impl Edge {
57    /// Resolve this logical edge to a physical side for a given direction.
58    ///
59    /// Returns `true` for "left", `false` for "right".
60    #[must_use]
61    pub fn is_left_under(self, direction: LayoutDirection) -> bool {
62        match (direction, self) {
63            (LayoutDirection::Ltr, Edge::Start) => true,
64            (LayoutDirection::Ltr, Edge::End) => false,
65            (LayoutDirection::Rtl, Edge::Start) => false,
66            (LayoutDirection::Rtl, Edge::End) => true,
67        }
68    }
69}
70
71#[cfg(test)]
72mod tests {
73    use super::*;
74
75    #[test]
76    fn edge_mapping_is_consistent() {
77        assert!(Edge::Start.is_left_under(LayoutDirection::Ltr));
78        assert!(!Edge::End.is_left_under(LayoutDirection::Ltr));
79        assert!(!Edge::Start.is_left_under(LayoutDirection::Rtl));
80        assert!(Edge::End.is_left_under(LayoutDirection::Rtl));
81    }
82
83    #[test]
84    fn flipping_is_idempotent_twice() {
85        let d = LayoutDirection::Ltr;
86        assert_eq!(d, d.flipped().flipped());
87    }
88}