slint_spatial_focus/
lib.rs

1mod extensions;
2
3use extensions::{Inner, ItemRcExt, VisitorResult};
4use i_slint_core::api::Window;
5use i_slint_core::item_tree::ItemRc;
6use i_slint_core::lengths::LogicalRect;
7use i_slint_core::Coord;
8
9#[derive(Clone, Copy, PartialEq, Eq, Debug)]
10pub enum FocusMoveDirection {
11    Up,
12    Right,
13    Down,
14    Left,
15}
16
17pub trait MoveFocus {
18    /// Tries to move focus in a specified direction.s
19    /// Returns `Some(())` on success or `None` if the next item to focus is not found.
20    fn move_focus(&self, dir: FocusMoveDirection) -> Option<()>;
21}
22
23impl MoveFocus for Window {
24    fn move_focus(&self, dir: FocusMoveDirection) -> Option<()> {
25        let window = self.inner();
26
27        let mut focus_chain_item = window.focus_item.try_borrow().ok()?.upgrade()?;
28        let ctx = FocusMoveCtx::new(focus_chain_item.global_rect(), dir);
29
30        while let Some(parent) = focus_chain_item.parent_item() {
31            if let Some(target) = find_next_focusable_item(&parent, &focus_chain_item, &ctx) {
32                window.set_focus_item(&target, true);
33                return Some(());
34            }
35            focus_chain_item = parent;
36        }
37
38        None
39    }
40}
41
42#[derive(Clone, Copy, PartialEq, Eq, Debug)]
43enum SpatialAxis {
44    Horizontal,
45    Vertical,
46}
47
48#[derive(Clone, Copy, PartialEq, Eq, Debug)]
49enum SpatialDirection {
50    Forward,
51    Backward,
52}
53
54struct FocusMoveCtx {
55    pub axis: SpatialAxis,
56    pub dir: SpatialDirection,
57    pub focused_rect: LogicalRect,
58}
59
60impl FocusMoveCtx {
61    pub fn new(focused_rect: LogicalRect, move_dir: FocusMoveDirection) -> Self {
62        let (axis, dir) = match move_dir {
63            FocusMoveDirection::Up => (SpatialAxis::Vertical, SpatialDirection::Backward),
64            FocusMoveDirection::Right => (SpatialAxis::Horizontal, SpatialDirection::Forward),
65            FocusMoveDirection::Down => (SpatialAxis::Vertical, SpatialDirection::Forward),
66            FocusMoveDirection::Left => (SpatialAxis::Horizontal, SpatialDirection::Backward),
67        };
68
69        FocusMoveCtx {
70            axis,
71            dir,
72            focused_rect,
73        }
74    }
75}
76
77fn find_next_focusable_item(
78    parent: &ItemRc,
79    focus_chain_child: &ItemRc,
80    ctx: &FocusMoveCtx,
81) -> Option<ItemRc> {
82    let mut focusable_items = Vec::new();
83    let mut visitor = |item: &ItemRc| {
84        if item == focus_chain_child || !item.is_visible() {
85            return VisitorResult::Skip;
86        }
87
88        if item.is_focusable() {
89            focusable_items.push(item.clone());
90            return VisitorResult::Skip;
91        }
92
93        VisitorResult::Continue
94    };
95    parent.visit_children(&mut visitor);
96
97    let candidates: Vec<(ItemRc, LogicalRect)> = focusable_items
98        .iter()
99        .map(|i| (i.clone(), i.global_rect()))
100        .filter(|(_, r)| is_focus_target(r, ctx))
101        .collect();
102
103    let first = candidates.first()?;
104
105    let mut curr_i = first.0.clone();
106    let mut curr_d = distance(&first.1, ctx);
107    let mut curr_od = ort_distance(&first.1, ctx);
108
109    for (i, r) in &candidates[1..] {
110        let d = distance(r, ctx);
111        let od = ort_distance(r, ctx);
112
113        if (d - curr_d).abs() <= TOLERANCE {
114            if od < curr_od {
115                curr_od = od;
116                curr_i = i.clone();
117            }
118        } else if d < curr_d {
119            curr_d = d;
120            curr_od = od;
121            curr_i = i.clone();
122        }
123    }
124
125    Some(curr_i)
126}
127
128const TOLERANCE: Coord = 0.001;
129
130fn is_focus_target(r: &LogicalRect, ctx: &FocusMoveCtx) -> bool {
131    let f = ctx.focused_rect;
132    match (ctx.axis, ctx.dir) {
133        (SpatialAxis::Horizontal, SpatialDirection::Backward) => {
134            r.origin.x + r.width() - TOLERANCE <= f.origin.x
135        }
136        (SpatialAxis::Horizontal, SpatialDirection::Forward) => {
137            r.origin.x + TOLERANCE >= f.origin.x + f.width()
138        }
139        (SpatialAxis::Vertical, SpatialDirection::Backward) => {
140            r.origin.y + r.height() - TOLERANCE <= f.origin.y
141        }
142        (SpatialAxis::Vertical, SpatialDirection::Forward) => {
143            r.origin.y + TOLERANCE >= f.origin.y + f.height()
144        }
145    }
146}
147
148fn distance(r: &LogicalRect, ctx: &FocusMoveCtx) -> Coord {
149    let f = ctx.focused_rect;
150    let d = match (ctx.axis, ctx.dir) {
151        (SpatialAxis::Horizontal, SpatialDirection::Backward) => {
152            (r.origin.x + r.width()) - f.origin.x
153        }
154        (SpatialAxis::Horizontal, SpatialDirection::Forward) => {
155            r.origin.x - (f.origin.x + f.width())
156        }
157        (SpatialAxis::Vertical, SpatialDirection::Backward) => {
158            (r.origin.y + r.height()) - f.origin.y
159        }
160        (SpatialAxis::Vertical, SpatialDirection::Forward) => {
161            r.origin.y - (f.origin.y + f.height())
162        }
163    };
164
165    d.abs()
166}
167
168fn ort_distance(r: &LogicalRect, ctx: &FocusMoveCtx) -> Coord {
169    let f = ctx.focused_rect;
170    let (a, b) = match ctx.axis {
171        SpatialAxis::Horizontal => {
172            let a = (f.origin.y, f.origin.y + f.height());
173            let b = (r.origin.y, r.origin.y + r.height());
174            (a, b)
175        }
176        SpatialAxis::Vertical => {
177            let a = (f.origin.x, f.origin.x + f.width());
178            let b = (r.origin.x, r.origin.x + r.width());
179            (a, b)
180        }
181    };
182
183    if are_intersected(&a, &b) {
184        return 0.0;
185    }
186
187    let ca = a.0 + (a.1 - a.0) / 2.0;
188    let cb = b.0 + (b.1 - b.0) / 2.0;
189
190    (ca - cb).abs()
191}
192
193fn are_intersected(a: &(Coord, Coord), b: &(Coord, Coord)) -> bool {
194    let p1 = a.0 - b.1; // min(a.0, a.1) - max(b.0, b.1)
195    let p2 = a.1 - b.0; // max(a.0, a.1) - min(b.0, b.1)
196    p1 < 0.0 && p2 > 0.0 // Origin is inside the Minkowski difference, so segments are intersected
197}
198
199/// Initializes `on_move` callbacks of the global `SpatialFocus` object.
200#[macro_export]
201macro_rules! init {
202    ($app:expr) => {{
203        use slint_spatial_focus::{FocusMoveDirection, MoveFocus};
204
205        let app = $app;
206        let sf = app.global::<SpatialFocus>();
207
208        let weak = app.as_weak();
209        sf.on_move_up(move || {
210            if let Some(app) = weak.upgrade() {
211                app.window().move_focus(FocusMoveDirection::Up);
212            }
213        });
214
215        let weak = app.as_weak();
216        sf.on_move_dn(move || {
217            if let Some(app) = weak.upgrade() {
218                app.window().move_focus(FocusMoveDirection::Down);
219            }
220        });
221
222        let weak = app.as_weak();
223        sf.on_move_l(move || {
224            if let Some(app) = weak.upgrade() {
225                app.window().move_focus(FocusMoveDirection::Left);
226            }
227        });
228
229        let weak = app.as_weak();
230        sf.on_move_r(move || {
231            if let Some(app) = weak.upgrade() {
232                app.window().move_focus(FocusMoveDirection::Right);
233            }
234        });
235    }};
236}