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}