Skip to main content

ui/
rtl.rs

1// rtl -- right-to-left layout: every window mirrored as a whole (as Windows' WS_EX_LAYOUTRTL)
2//
3//   rtl() -> bool                    the windows are mirrored now
4//   set_rtl(Some(on) | None)         force it, or None: from the UI language (Arabic, Hebrew,
5//                                    Persian, Urdu, ...: i18n::rtl_language); command line
6//                                    --rtl / --ltr
7//   ltr_begin(x, w) / ltr_end()      a region of the window whose content keeps its reading
8//                                    order (text fields, canvases, colour pads): it moves to its
9//                                    mirrored place as a whole
10//   ltr_x(x) -> x                    a logical x (the pointer: wp.ctx.mx) inside the outermost
11//                                    open region, so the region's own maths stays LTR
12//
13// Layout and hit testing stay in logical (LTR) coordinates: a box's first child is its
14// logical start.  Before a window is rendered, every recorded command is mirrored across
15// it (cmd_mirror): boxes, clips, lines and shapes flip; text, images and custom drawings
16// move but keep their direction; directional icons (chevrons, back / forward, undo / redo,
17// indent) are swapped for their mirror images.  Input comes back the same way: the pointer x
18// is mirrored, a horizontal wheel turns round, plain Left / Right swap (text fields swap
19// them back).  What leaves the UI is physical: sems (scripts, the agent), popup rects of OS
20// popup windows, the platform's hit tests, resize edges and the IME caret.
21
22use alloc::vec::Vec;
23
24use super::consts::*;
25use super::keys::*;
26use super::Ui;
27use crate::cmd::Cmd;
28
29/// a region that keeps its LTR content: records [start, end) of a layer
30#[derive(Clone, Copy)]
31pub(crate) struct Island {
32    layer: u8,
33    start: u32,
34    end: u32,
35    x: i32,
36    w: i32,
37}
38
39/// the per-window part (with the command lists)
40#[derive(Default)]
41pub(crate) struct W {
42    pub(crate) islands: Vec<Island>,
43    /// open ltr_begin regions (indices into islands; usize::MAX: not recorded)
44    open: Vec<usize>,
45    /// the command lists are mirrored already (cleared with them)
46    pub(crate) mirrored: bool,
47}
48
49impl W {
50    pub(crate) fn reset(&mut self) {
51        self.islands.clear();
52        self.open.clear();
53        self.mirrored = false;
54    }
55}
56
57// icons that point somewhere: each one is drawn as its partner in a mirrored window
58const MIRROR_ICONS: [(u32, u32); 11] = [
59    (IC_CHEV_LEFT, IC_CHEV_RIGHT),
60    (IC_CHEV_LEFT_SMALL, IC_CHEV_RIGHT_SMALL),
61    (IC_CARET_LEFT, IC_CARET_RIGHT),
62    (IC_BACK, IC_FORWARD),
63    (IC_PREVIOUS, IC_NEXT),
64    (IC_UNDO, IC_REDO),
65    (IC_OFFICE_CHEVRON_LEFT, IC_OFFICE_CHEVRON_RIGHT),
66    (IC_OFFICE_PREVIOUS, IC_OFFICE_NEXT),
67    (IC_OFFICE_UNDO, IC_OFFICE_REDO),
68    (IC_OFFICE_INDENT, IC_OFFICE_OUTDENT),
69    (IC_OFFICE_INSERT_LEFT, IC_OFFICE_INSERT_RIGHT),
70];
71
72/// the mirror image of an icon (itself when it has no direction)
73pub fn mirror_icon(cp: u32) -> u32 {
74    for (a, b) in MIRROR_ICONS {
75        if cp == a {
76            return b;
77        }
78        if cp == b {
79            return a;
80        }
81    }
82    cp
83}
84
85impl Ui {
86    /// the windows are mirrored now (right-to-left layout)
87    pub fn rtl(&self) -> bool {
88        self.rtl
89    }
90
91    /// mirror the windows (Some(true)), never (Some(false)) or as the UI language reads (None)
92    pub fn set_rtl(&mut self, on: Option<bool>) {
93        self.rtl_mode = match on {
94            None => 0,
95            Some(true) => 1,
96            Some(false) => 2,
97        };
98        self.rtl_update();
99    }
100
101    // frame_begin: the mode for this frame
102    pub(crate) fn rtl_update(&mut self) {
103        let on = match self.rtl_mode {
104            1 => true,
105            2 => false,
106            _ => crate::i18n::rtl_language(),
107        };
108        if on != self.rtl {
109            self.rtl = on;
110            self.dirty_force_full();
111        }
112    }
113
114    /// ltr_begin(x, w): the following commands of this layer keep their LTR order inside the
115    /// columns x .. x + w (px); pairs with ltr_end
116    pub fn ltr_begin(&mut self, x: i32, w: i32) {
117        let layer = self.wp.cmd.layer;
118        let start = self.wp.cmd.layers[layer].len() as u32;
119        let r = &mut self.wp.rtl;
120        if !self.rtl {
121            r.open.push(usize::MAX);
122            return;
123        }
124        r.islands.push(Island { layer: layer as u8, start, end: u32::MAX, x, w });
125        r.open.push(r.islands.len() - 1);
126    }
127
128    /// close the region of ltr_begin
129    pub fn ltr_end(&mut self) {
130        let Ui { wp, .. } = self;
131        if let Some(k) = wp.rtl.open.pop() {
132            if let Some(i) = wp.rtl.islands.get_mut(k) {
133                // the end in the region's own layer (a popup opened inside does not count)
134                i.end = wp.cmd.layers[i.layer as usize].len() as u32;
135            }
136        }
137    }
138
139    // the outermost open region (x, w)
140    fn ltr_outer(&self) -> Option<(i32, i32)> {
141        let r = &self.wp.rtl;
142        r.open.iter().find(|&&k| k != usize::MAX).and_then(|&k| r.islands.get(k)).map(|i| (i.x, i.w))
143    }
144
145    /// a logical x (the pointer) as the open ltr_begin region sees it: its content is not
146    /// mirrored, so the pointer is mirrored back across the region
147    pub fn ltr_x(&self, x: i32) -> i32 {
148        match self.ltr_outer() {
149            Some((ix, iw)) => self.rtl_map(x, ix, iw),
150            None => x,
151        }
152    }
153
154    /// a rectangle drawn inside the open ltr_begin region as a logical one (for records and
155    /// hit boxes, which are logical)
156    pub(crate) fn ltr_rect(&self, r: [i32; 4]) -> [i32; 4] {
157        match self.ltr_outer() {
158            Some((ix, iw)) if self.rtl => [ix.wrapping_add(ix).wrapping_add(iw).wrapping_sub(r[0]).wrapping_sub(r[2]), r[1], r[2], r[3]],
159            _ => r,
160        }
161    }
162
163    /// rtl_map(x, ix, iw): a logical x as a region ix .. ix + iw that keeps its LTR content
164    /// sees it (the same before its ltr_begin)
165    pub fn rtl_map(&self, x: i32, ix: i32, iw: i32) -> i32 {
166        if self.rtl { ix.wrapping_add(ix).wrapping_add(iw).wrapping_sub(1).wrapping_sub(x) } else { x }
167    }
168
169    /// a logical point x of this window in window px (the IME caret): mirrored, or moved with
170    /// the open ltr_begin region
171    pub(crate) fn phys_x(&self, x: i32) -> i32 {
172        if !self.rtl {
173            return x;
174        }
175        let w = self.wp.rt.fb.w;
176        match self.ltr_outer() {
177            Some((ix, iw)) => x.wrapping_add(w - ix - ix - iw),
178            None => w.wrapping_sub(x),
179        }
180    }
181
182    /// a logical rectangle of this window in window px
183    pub(crate) fn phys_rect(&self, r: [i32; 4]) -> [i32; 4] {
184        if !self.rtl {
185            return r;
186        }
187        [self.wp.rt.fb.w.wrapping_sub(r[0]).wrapping_sub(r[2]), r[1], r[2], r[3]]
188    }
189
190    // input_event: the pointer and the keys of a mirrored window into logical ones
191    pub(crate) fn rtl_input(&self, ev: &mut plat::Event) {
192        if !self.rtl {
193            return;
194        }
195        match ev.typ {
196            plat::E_MOVE | plat::E_DOWN | plat::E_UP if ev.x != -1 => {
197                ev.x = self.wp.rt.fb.w.wrapping_sub(1).wrapping_sub(ev.x);
198            }
199            plat::E_WHEEL => {
200                // the horizontal part (high 16 bits) turns round
201                let wx = (ev.data as i32) >> 16;
202                ev.data = (ev.data & 0xFFFF) | ((wx.wrapping_neg() as u32) << 16);
203            }
204            plat::E_KEY_DOWN if ev.mods as u32 & (MOD_CTRL | MOD_ALT) == 0 => {
205                ev.data = swap_arrow(ev.data);
206            }
207            _ => {}
208        }
209    }
210
211    /// a key of the queue as a text field reads it: Left / Right swapped back in a mirrored
212    /// window (text keeps its reading order)
213    pub(crate) fn rtl_text_key(&self, k: u32) -> u32 {
214        if self.rtl && (k >> 16) & (MOD_CTRL | MOD_ALT) == 0 { swap_arrow(k) } else { k }
215    }
216
217    /// cmd_mirror: mirror the recorded commands of the live window across it (once per
218    /// recording; render_live)
219    pub(crate) fn cmd_mirror(&mut self) {
220        if !self.rtl || self.wp.rtl.mirrored {
221            return;
222        }
223        self.wp.rtl.mirrored = true;
224        let w = self.wp.rt.fb.w;
225        let Ui { wp, text, .. } = self;
226        for (l, list) in wp.cmd.layers.iter_mut().enumerate() {
227            // the move of each record: None = mirror, Some(d) = shift by d (in an island)
228            let mut shift: Vec<Option<i32>> = alloc::vec![None; list.len()];
229            for i in wp.rtl.islands.iter().filter(|i| i.layer as usize == l) {
230                let d = w - i.x - i.x - i.w;
231                let end = (i.end as usize).min(list.len());
232                for s in shift.iter_mut().take(end).skip(i.start as usize) {
233                    if s.is_none() {
234                        *s = Some(d); // outer regions come first: they decide
235                    }
236                }
237            }
238            for (c, s) in list.iter_mut().zip(shift) {
239                match s {
240                    Some(d) => shift_cmd(c, d),
241                    None => mirror_cmd(c, w, text),
242                }
243            }
244        }
245    }
246}
247
248fn swap_arrow(k: u32) -> u32 {
249    match k & 0xFFFF {
250        VK_LEFT => (k & !0xFFFF) | VK_RIGHT,
251        VK_RIGHT => (k & !0xFFFF) | VK_LEFT,
252        _ => k,
253    }
254}
255
256// a record moved by d px
257fn shift_cmd(c: &mut Cmd, d: i32) {
258    match c {
259        Cmd::Unclip => {}
260        Cmd::Circle { x, .. } | Cmd::PanelFx { x, .. } => *x = x.wrapping_add(d << 8),
261        Cmd::Line { x0, x1, .. } => {
262            *x0 = x0.wrapping_add(d << 8);
263            *x1 = x1.wrapping_add(d << 8);
264        }
265        Cmd::Rect { x, .. }
266        | Cmd::RRect { x, .. }
267        | Cmd::Text { x, .. }
268        | Cmd::Icon { x, .. }
269        | Cmd::Image { x, .. }
270        | Cmd::Clip { x, .. }
271        | Cmd::Custom { x, .. }
272        | Cmd::Shadow { x, .. } => *x = x.wrapping_add(d),
273    }
274}
275
276// a record mirrored across a window w px wide: boxes flip, text / images / custom drawings
277// keep their direction at the mirrored place, directional icons swap
278fn mirror_cmd(c: &mut Cmd, w: i32, text: &mut text::Text) {
279    let w8 = w << 8;
280    match c {
281        Cmd::Unclip => {}
282        Cmd::Circle { x, .. } => *x = w8.wrapping_sub(*x),
283        Cmd::PanelFx { x, w: pw, .. } => *x = w8.wrapping_sub(*x).wrapping_sub(*pw),
284        Cmd::Line { x0, x1, .. } => {
285            *x0 = w8.wrapping_sub(*x0);
286            *x1 = w8.wrapping_sub(*x1);
287        }
288        Cmd::Rect { x, w: rw, .. }
289        | Cmd::RRect { x, w: rw, .. }
290        | Cmd::Image { x, w: rw, .. }
291        | Cmd::Clip { x, w: rw, .. }
292        | Cmd::Custom { x, w: rw, .. }
293        | Cmd::Shadow { x, w: rw, .. } => *x = w.wrapping_sub(*x).wrapping_sub(*rw),
294        Cmd::Text { x, font, size, maxw, s, .. } => {
295            text.set_font(*font as u32, *size);
296            let mut tw = text.width_n(s, s.len());
297            if *maxw != 0 {
298                tw = tw.min(*maxw);
299            }
300            *x = w.wrapping_sub(*x).wrapping_sub(tw);
301        }
302        Cmd::Icon { x, cp, size, .. } => {
303            *x = w.wrapping_sub(*x).wrapping_sub(((*size as u32) >> 8) as i32);
304            *cp = mirror_icon(*cp);
305        }
306    }
307}