Skip to main content

eui_core/
direction.rs

1//! Round 2 (docs/round-2-contract.md §2, direction): `direction` may sit on
2//! ANY node and is inherited; everything direction-dependent reads the
3//! NODE's resolved direction: flex row order, logical insets and paddings,
4//! mirrored glyphs and `textAlign` start/end (the implicit alignment of
5//! text is `start`). Mirrors `src/direction.ts`; `fixtures/text-direction.json`
6//! locks it. The surface carries the result into [`crate::style::Visual`]
7//! (`direction`, `text_align`) so painters shape text with the node's
8//! direction as the bidi paragraph direction.
9
10use indexmap::IndexMap;
11use serde::{Deserialize, Serialize};
12use serde_json::{Map, Value};
13
14use crate::conditions::{resolve_conditions, ConditionContext};
15use crate::types::UiNode;
16
17/// A text direction.
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Hash, Serialize, Deserialize)]
19#[serde(rename_all = "lowercase")]
20pub enum TextDirection {
21    #[default]
22    Ltr,
23    Rtl,
24}
25
26impl TextDirection {
27    pub fn parse(s: &str) -> Option<TextDirection> {
28        match s {
29            "ltr" => Some(TextDirection::Ltr),
30            "rtl" => Some(TextDirection::Rtl),
31            _ => None,
32        }
33    }
34
35    pub fn as_str(self) -> &'static str {
36        match self {
37            TextDirection::Ltr => "ltr",
38            TextDirection::Rtl => "rtl",
39        }
40    }
41
42    /// The direction of a locale (`catalog/locale.json` `rtl`).
43    pub fn of_locale(locale: &str) -> TextDirection {
44        if crate::locale::text_direction(locale) == "rtl" {
45            TextDirection::Rtl
46        } else {
47            TextDirection::Ltr
48        }
49    }
50}
51
52/// A physical text alignment.
53#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Hash, Serialize, Deserialize)]
54#[serde(rename_all = "lowercase")]
55pub enum PhysicalAlign {
56    #[default]
57    Left,
58    Right,
59    Center,
60    Justify,
61}
62
63impl PhysicalAlign {
64    pub fn as_str(self) -> &'static str {
65        match self {
66            PhysicalAlign::Left => "left",
67            PhysicalAlign::Right => "right",
68            PhysicalAlign::Center => "center",
69            PhysicalAlign::Justify => "justify",
70        }
71    }
72}
73
74fn own_direction(style: Option<&Map<String, Value>>, ctx: Option<&ConditionContext>) -> Option<TextDirection> {
75    let style = style?;
76    let d = match ctx {
77        Some(ctx) => resolve_conditions(style, ctx).get("direction").cloned(),
78        None => style.get("direction").cloned(),
79    };
80    d.as_ref().and_then(Value::as_str).and_then(TextDirection::parse)
81}
82
83/// Every node's direction: its own style `direction` (resolved against
84/// `ctx` when given), else its parent's; the root's parent is the surface.
85/// Slots inherit from their owner. A template's items take the direction of
86/// the node that renders them.
87pub fn node_directions(root: &UiNode, surface: TextDirection, ctx: Option<&ConditionContext>) -> IndexMap<String, TextDirection> {
88    fn walk(n: &UiNode, inherited: TextDirection, ctx: Option<&ConditionContext>, out: &mut IndexMap<String, TextDirection>) {
89        let dir = own_direction(n.style.as_ref(), ctx).unwrap_or(inherited);
90        out.insert(n.id.clone(), dir);
91        for slot in n.slots.iter().flat_map(|s| s.values()) {
92            walk(slot, dir, ctx, out);
93        }
94        for c in &n.children {
95            walk(c, dir, ctx, out);
96        }
97    }
98    let mut out = IndexMap::new();
99    walk(root, surface, ctx, &mut out);
100    out
101}
102
103/// `start`/`end` (and an absent or unknown value = `start`) as the physical
104/// side for a direction; `left`, `right`, `center` and `justify` stay.
105pub fn physical_text_align(align: Option<&str>, direction: TextDirection) -> PhysicalAlign {
106    let rtl = direction == TextDirection::Rtl;
107    match align {
108        Some("left") => PhysicalAlign::Left,
109        Some("right") => PhysicalAlign::Right,
110        Some("center") => PhysicalAlign::Center,
111        Some("justify") => PhysicalAlign::Justify,
112        Some("end") => {
113            if rtl {
114                PhysicalAlign::Left
115            } else {
116                PhysicalAlign::Right
117            }
118        }
119        _ => {
120            if rtl {
121                PhysicalAlign::Right
122            } else {
123                PhysicalAlign::Left
124            }
125        }
126    }
127}
128
129/// A node's text alignment: its (flattened) style `textAlign`, else a
130/// Text's `align` prop, else `start`, resolved against its direction.
131pub fn text_align_of(style: &Map<String, Value>, props: &Map<String, Value>, direction: TextDirection) -> PhysicalAlign {
132    let align = style.get("textAlign").or_else(|| props.get("align")).and_then(Value::as_str);
133    physical_text_align(align, direction)
134}
135
136/// [`text_align_of`] for a reduced node (`nodeTextAlign`).
137pub fn node_text_align(node: &UiNode, direction: TextDirection, ctx: Option<&ConditionContext>) -> PhysicalAlign {
138    let empty = Map::new();
139    let style = match (node.style.as_ref(), ctx) {
140        (Some(s), Some(ctx)) => resolve_conditions(s, ctx),
141        (Some(s), None) => s.clone(),
142        (None, _) => empty,
143    };
144    text_align_of(&style, &node.props, direction)
145}