slint_spatial_focus/
lib.rs1mod 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 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; let p2 = a.1 - b.0; p1 < 0.0 && p2 > 0.0 }
198
199#[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}