Skip to content

Commit 5929601

Browse files
committed
Introduce Focusable
1 parent a38a883 commit 5929601

File tree

1 file changed

+182
-0
lines changed

1 file changed

+182
-0
lines changed

lib/Focusable.vala

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
/*
2+
* Copyright 2025 elementary, Inc. (https://elementary.io)
3+
* SPDX-License-Identifier: GPL-3.0-or-later
4+
*
5+
* Authored by: Leonhard Kargl <[email protected]>
6+
*/
7+
8+
public interface Gala.Focusable : Clutter.Actor {
9+
public enum FocusDirection {
10+
UP,
11+
DOWN,
12+
LEFT,
13+
RIGHT,
14+
NEXT,
15+
PREVIOUS;
16+
17+
public bool is_forward () {
18+
return this == DOWN || this == RIGHT || this == NEXT;
19+
}
20+
}
21+
22+
public bool focus (FocusDirection direction) {
23+
var focus_actor = get_stage ().get_key_focus ();
24+
25+
// We have focus so try to move it to a child
26+
if (focus_actor == this) {
27+
if (direction.is_forward ()) {
28+
return move_focus (direction);
29+
}
30+
31+
return false;
32+
}
33+
34+
// A child of us (or subchild) has focus, try to move it to the next one.
35+
// If that doesn't work and we are moving backwards focus us
36+
if (focus_actor != null && focus_actor is Focusable && focus_actor in this) {
37+
if (move_focus (direction)) {
38+
return true;
39+
}
40+
41+
if (direction.is_forward ()) {
42+
return false;
43+
} else {
44+
return grab_focus ();
45+
}
46+
}
47+
48+
// Focus is outside of us, try to take it
49+
if (direction.is_forward ()) {
50+
if (grab_focus ()) {
51+
return true;
52+
}
53+
54+
return move_focus (direction);
55+
} else {
56+
if (move_focus (direction)) {
57+
return true;
58+
}
59+
60+
return grab_focus ();
61+
}
62+
}
63+
64+
protected virtual bool move_focus (FocusDirection direction) {
65+
var focus_actor = get_stage ().get_key_focus ();
66+
67+
Focusable? focus_child = null;
68+
for (var child = get_first_child (); child != null; child = child.get_next_sibling ()) {
69+
if (focus_actor in child) {
70+
if (child is Focusable) {
71+
focus_child = (Focusable) child;
72+
}
73+
break;
74+
}
75+
}
76+
77+
var possible_children = new Gee.ArrayList<Focusable> ();
78+
possible_children.add_all_iterator (get_focusable_children ().filter ((c) => {
79+
if (focus_child == null || c == focus_child) {
80+
return true;
81+
}
82+
83+
var focus_rect = get_allocation_rect (focus_child);
84+
var rect = get_allocation_rect (c);
85+
86+
if ((direction == UP || direction == DOWN) && !rect.horiz_overlap (focus_rect) ||
87+
(direction == LEFT || direction == RIGHT) && !rect.vert_overlap (focus_rect)
88+
) {
89+
return false;
90+
}
91+
92+
return (
93+
direction == UP && rect.y + rect.height <= focus_rect.y ||
94+
direction == DOWN && rect.y >= focus_rect.y + focus_rect.height ||
95+
direction == LEFT && rect.x + rect.width <= focus_rect.x ||
96+
direction == RIGHT && rect.x >= focus_rect.x + focus_rect.width
97+
);
98+
}));
99+
100+
possible_children.sort ((a, b) => {
101+
if (direction == UP && a.y + a.height > b.y + b.height ||
102+
direction == DOWN && a.y < b.y ||
103+
direction == LEFT && a.x + a.width > b.x + b.width ||
104+
direction == RIGHT && a.x < b.x
105+
) {
106+
return -1;
107+
}
108+
109+
return 1;
110+
});
111+
112+
foreach (var child in possible_children) {
113+
if (child.focus (direction)) {
114+
return true;
115+
}
116+
}
117+
118+
return false;
119+
}
120+
121+
private Mtk.Rectangle get_allocation_rect (Clutter.Actor actor) {
122+
return {(int) actor.x, (int) actor.y, (int) actor.width, (int) actor.height};
123+
}
124+
125+
private Gee.List<Focusable> get_focusable_children () {
126+
var focusable_children = new Gee.ArrayList<Focusable> ();
127+
for (var child = get_first_child (); child != null; child = child.get_next_sibling ()) {
128+
if (child is Focusable) {
129+
focusable_children.add ((Focusable) child);
130+
}
131+
}
132+
return focusable_children;
133+
}
134+
135+
private bool grab_focus () {
136+
if (!can_focus ()) {
137+
return false;
138+
}
139+
140+
get_stage ().set_key_focus (this);
141+
142+
return true;
143+
}
144+
145+
public virtual bool can_focus () {
146+
return false;
147+
}
148+
149+
public void mark_root (Clutter.Stage stage) {
150+
stage.key_press_event.connect (on_key_press_event);
151+
}
152+
153+
private bool on_key_press_event (Clutter.Event event) {
154+
if (!mapped) {
155+
return Clutter.EVENT_PROPAGATE;
156+
}
157+
158+
switch (event.get_key_symbol ()) {
159+
case Clutter.Key.Tab:
160+
if (SHIFT_MASK in event.get_state ()) {
161+
focus (PREVIOUS);
162+
} else {
163+
focus (NEXT);
164+
}
165+
return Clutter.EVENT_STOP;
166+
case Clutter.Key.Up:
167+
focus (UP);
168+
return Clutter.EVENT_STOP;
169+
case Clutter.Key.Left:
170+
focus (LEFT);
171+
return Clutter.EVENT_STOP;
172+
case Clutter.Key.Down:
173+
focus (DOWN);
174+
return Clutter.EVENT_STOP;
175+
case Clutter.Key.Right:
176+
focus (RIGHT);
177+
return Clutter.EVENT_STOP;
178+
default:
179+
return Clutter.EVENT_PROPAGATE;
180+
}
181+
}
182+
}

0 commit comments

Comments
 (0)