1 module nodes.focus_chain;
2 
3 import fluid;
4 import fluid.future.pipe;
5 
6 @safe:
7 
8 alias focusTracker = nodeBuilder!FocusTracker;
9 
10 class FocusTracker : Node, Focusable {
11 
12     mixin enableInputActions;
13 
14     FocusIO focusIO;
15 
16     int pressCalls;
17     int focusImplCalls;
18 
19     override void resizeImpl(Vector2) {
20         require(focusIO);
21         minSize = Vector2();
22     }
23 
24     override void drawImpl(Rectangle, Rectangle) {
25 
26     }
27 
28     override bool blocksInput() const {
29         return isDisabled || isDisabledInherited;
30     }
31 
32     @(FluidInputAction.press)
33     void press() {
34         assert(!blocksInput);
35         pressCalls++;
36     }
37 
38     bool focusImpl() {
39         assert(!blocksInput);
40         focusImplCalls++;
41         return true;
42     }
43 
44     void focus() {
45         if (!blocksInput) {
46             focusIO.currentFocus = this;
47         }
48     }
49 
50     bool isFocused() const {
51         return focusIO.isFocused(this);
52     }
53 
54     alias opEquals = typeof(super).opEquals;
55 
56     override bool opEquals(const Object other) const {
57         return super.opEquals(other);
58     }
59 
60 }
61 
62 @("FocusChain keeps track of current focus")
63 unittest {
64 
65     int one;
66     int two;
67     Button incrementOne;
68     Button incrementTwo;
69 
70     auto root = focusChain(
71         vspace(
72             incrementOne = button("One", delegate { one++; }),
73             incrementTwo = button("Two", delegate { two++; }),
74         ),
75     );
76 
77     root.draw();
78     root.currentFocus = incrementOne;
79     assert(!root.wasInputHandled);
80     assert(one == 0);
81     assert(two == 0);
82     assert(root.runInputAction!(FluidInputAction.press));
83     assert( root.wasInputHandled);
84     assert(one == 1);
85     assert(two == 0);
86     assert(root.runInputAction!(FluidInputAction.press));
87     assert(one == 2);
88     assert(two == 0);
89 
90     root.currentFocus = incrementTwo;
91     assert(one == 2);
92     assert(two == 0);
93     assert(root.runInputAction!(FluidInputAction.press));
94     assert(one == 2);
95     assert(two == 1);
96     assert( root.wasInputHandled);
97 
98 }
99 
100 @("Multiple nodes can be focused if they belong to different focus spaces")
101 unittest {
102 
103     FocusChain focus1, focus2;
104     Button button1, button2;
105     int one, two;
106 
107     auto root = vspace(
108         focus1 = focusChain(
109             button1 = button("One", delegate { one++; }),
110         ),
111         focus2 = focusChain(
112             button2 = button("Two", delegate { two++; }),
113         ),
114     );
115 
116     root.draw();
117     button1.focus();
118     button2.focus();
119     assert(button1.isFocused);
120     assert(button2.isFocused);
121     assert(focus1.currentFocus.opEquals(button1));
122     assert(focus2.currentFocus.opEquals(button2));
123 
124     focus1.runInputAction!(FluidInputAction.press);
125     assert(one == 1);
126     assert(two == 0);
127     focus2.runInputAction!(FluidInputAction.press);
128     assert(one == 1);
129     assert(two == 1);
130 
131 }
132 
133 @("FocusChain can be nested")
134 unittest {
135 
136     FocusChain focus1, focus2;
137     Button button1, button2;
138     int one, two;
139 
140     auto root = vspace(
141         focus1 = focusChain(
142             vspace(
143                 button1 = button("One", delegate { one++; }),
144                 focus2 = focusChain(
145                     button2 = button("Two", delegate { two++; }),
146                 ),
147             ),
148         ),
149     );
150 
151     root.draw();
152     button1.focus();
153     button2.focus();
154 
155     assert(focus1.currentFocus.opEquals(button1));
156     assert(focus2.currentFocus.opEquals(button2));
157 
158 }
159 
160 @("FocusChain supports tabbing")
161 unittest {
162 
163     Button[3] buttons;
164 
165     auto root = focusChain(
166         vspace(
167             buttons[0] = button("One", delegate { }),
168             buttons[1] = button("Two", delegate { }),
169             buttons[2] = button("Three", delegate { }),
170         ),
171     );
172     root.draw();
173     buttons[0].focus();
174     assert(root.isFocused(buttons[0]));
175 
176     root.runInputAction!(FluidInputAction.focusNext);
177     root.draw();
178     assert(root.isFocused(buttons[1]));
179 
180     root.runInputAction!(FluidInputAction.focusNext);
181     root.draw();
182     assert(root.isFocused(buttons[2]));
183 
184     root.runInputAction!(FluidInputAction.focusNext);
185     root.draw();
186     assert(root.isFocused(buttons[0]));
187 
188 }
189 @("FocusChain supports tabbing (chained)")
190 unittest {
191 
192     Button[3] buttons;
193 
194     auto root = focusChain(
195         vspace(
196             buttons[0] = button("One", delegate { }),
197             buttons[1] = button("Two", delegate { }),
198             buttons[2] = button("Three", delegate { }),
199         ),
200     );
201     root.draw();
202     buttons[0].focus();
203     assert(root.isFocused(buttons[0]));
204 
205     const frames = root.focusNext
206         .thenAssertEquals(buttons[1])
207         .then(() => root.focusNext)
208         .thenAssertEquals(buttons[2])
209         .then(() => root.focusNext)
210         .thenAssertEquals(buttons[0])
211         .runWhileDrawing(root, 5);
212 
213     assert(frames == 3);
214 
215 }
216 
217 @("FocusChain automatically focuses first item on tab")
218 unittest {
219 
220     Button[3] buttons;
221     auto root = focusChain(
222         vspace(
223             buttons[0] = button("One", delegate { }),
224             buttons[1] = button("Two", delegate { }),
225             buttons[2] = button("Three", delegate { }),
226         ),
227     );
228 
229     assert(root.currentFocus is null);
230 
231     // Via chains
232     root.focusNext()
233         .thenAssertEquals(buttons[0])
234         .then(() => assert(root.isFocused(buttons[0])))
235         .runWhileDrawing(root, 1);
236 
237     // Via input actions
238     root.clearFocus();
239     assert(!root.isFocused(buttons[0]));
240     root.runInputAction!(FluidInputAction.focusNext);
241     root.draw();
242     assert(root.isFocused(buttons[0]));
243 
244 }
245 
246 @("FocusChain focuses the last item on shift tab")
247 unittest {
248 
249     Button[3] buttons;
250     auto root = focusChain(
251         vspace(
252             buttons[0] = button("One", delegate { }),
253             buttons[1] = button("Two", delegate { }),
254             buttons[2] = button("Three", delegate { }),
255         ),
256     );
257 
258     assert(root.currentFocus is null);
259 
260     // Via chains
261     root.focusPrevious()
262         .thenAssertEquals(buttons[2])
263         .then(() => assert(root.isFocused(buttons[2])))
264         .runWhileDrawing(root, 1);
265 
266     // Via input actions
267     root.clearFocus();
268     assert(!root.isFocused(buttons[2]));
269     root.runInputAction!(FluidInputAction.focusPrevious);
270     root.draw();
271     assert(root.isFocused(buttons[2]));
272 
273 }
274 
275 @("FocusChain tabbing wraps")
276 unittest {
277 
278     Button[3] buttons;
279     auto root = focusChain(
280         vspace(
281             buttons[0] = button("One", delegate { }),
282             vspace(
283                 buttons[1] = button("Two", delegate { }),
284             ),
285             buttons[2] = button("Three", delegate { }),
286         ),
287     );
288 
289     root.focusNext()
290         .thenAssertEquals(buttons[0])
291         .then(() => root.focusNext())
292         .thenAssertEquals(buttons[1])
293         .then(() => root.focusNext())
294         .thenAssertEquals(buttons[2])
295         .then(() => root.focusNext())
296         .thenAssertEquals(buttons[0])
297         .runWhileDrawing(root, 4);
298 
299     root.clearFocus();
300     root.focusPrevious()
301         .thenAssertEquals(buttons[2])
302         .then(() => root.focusPrevious())
303         .thenAssertEquals(buttons[1])
304         .then(() => root.focusPrevious())
305         .thenAssertEquals(buttons[0])
306         .then(() => root.focusPrevious())
307         .thenAssertEquals(buttons[2])
308         .runWhileDrawing(root, 4);
309 
310 }
311 
312 @("FocusChain supports directional movement")
313 unittest {
314 
315     Button[5] buttons;
316     auto root = focusChain(
317         vspace(
318             buttons[0] = button("Zero", delegate { }),
319             hspace(
320                 buttons[1] = button("One", delegate { }),
321                 buttons[2] = button("Two", delegate { }),
322                 buttons[3] = button("Three", delegate { }),
323             ),
324             buttons[4] = button("Four", delegate { }),
325         ),
326     );
327 
328     root.currentFocus = buttons[0];
329     root.draw();
330 
331     // Vertical focus
332     root.focusBelow().thenAssertEquals(buttons[1])
333         .then(() => root.nextFrame)
334         .then(() => root.focusBelow).thenAssertEquals(buttons[4])
335         .then(() => root.nextFrame)
336         .then(() => root.focusAbove).thenAssertEquals(buttons[1])
337         .then(() => root.nextFrame)
338 
339         // Horizontal
340         .then(() => root.focusToRight).thenAssertEquals(buttons[2])
341         .then(() => root.nextFrame)
342         .then(() => root.focusToRight).thenAssertEquals(buttons[3])
343         .then(() => root.nextFrame)
344         .then(() => root.focusToRight).thenAssertEquals(null)
345         .then(() => assert(root.isFocused(buttons[3])))
346 
347         // Vertical, again
348         .then(() => root.focusAbove).thenAssertEquals(buttons[0])
349         .runWhileDrawing(root, 12);
350 
351 }
352 
353 @("FocusChain calls focusImpl as a fallback")
354 unittest {
355 
356     auto map = InputMapping();
357     map.bindNew!(FluidInputAction.press)(KeyboardIO.codes.space);
358 
359     auto tracker = focusTracker();
360     auto focus = focusChain(tracker);
361     auto root = inputMapChain(map, focus);
362 
363     root.draw();
364     assert(tracker.focusImplCalls == 0);
365 
366     focus.currentFocus = tracker;
367     root.draw();
368 
369     assert(tracker.pressCalls == 0);
370     assert(tracker.focusImplCalls == 1);
371 
372     focus.emitEvent(KeyboardIO.press.space);
373     root.draw();
374 
375     assert(tracker.pressCalls == 1);
376     assert(tracker.focusImplCalls == 1);
377 
378     focus.emitEvent(KeyboardIO.hold.space);
379     root.draw();
380 
381     assert(tracker.pressCalls == 1);
382     assert(tracker.focusImplCalls == 2);
383 
384     // Unrelated input actions cannot trigger fallback
385     focus.runInputAction!(FluidInputAction.press);
386     assert(tracker.pressCalls == 2);
387     focus.runInputAction!(FluidInputAction.contextMenu);
388     assert(tracker.pressCalls == 2);
389     assert(tracker.focusImplCalls == 2);
390 
391 }
392 
393 @("FocusChain calls focusImpl if there is no ActionIO")
394 unittest {
395 
396     auto tracker = focusTracker();
397     auto focus = focusChain(tracker);
398     auto root = focus;
399 
400     root.draw();
401     assert(tracker.focusImplCalls == 0);
402 
403     focus.currentFocus = tracker;
404     root.draw();
405 
406     assert(tracker.focusImplCalls == 1);
407 
408 }
409 
410 @("FocusChain doesn't trigger events on disabled nodes")
411 unittest {
412 
413     auto tracker = focusTracker();
414     auto focus = focusChain(tracker);
415     auto root = focus;
416 
417     // Focused for a frame while enabled
418     focus.currentFocus = tracker;
419     root.draw();
420     assert(tracker.focusImplCalls == 1);
421 
422     // Disabled while focused
423     tracker.disable();
424     root.draw();
425     assert(tracker.focusImplCalls == 1);
426     assert(tracker.pressCalls == 0);
427 
428     root.runInputAction!(FluidInputAction.press);
429     root.draw();
430     assert(tracker.pressCalls == 0);
431 
432 
433 }
434 
435 @("Tabbing skips over disabled nodes")
436 unittest {
437 
438     Button btn1, btn2, btn3;
439 
440     auto root = focusChain(
441         vspace(
442             btn1 = button("One", delegate { }),
443             btn2 = button(.disabled, "Two", delegate { }),
444             btn3 = button("Three", delegate { }),
445         ),
446     );
447 
448     root.currentFocus = btn1;
449     root.focusNext()
450         .thenAssertEquals(btn3)
451         .then(() => root.focusNext)
452         .thenAssertEquals(btn1)
453         .then(() => root.focusPrevious)
454         .thenAssertEquals(btn3)
455         .then(() => root.focusPrevious)
456         .thenAssertEquals(btn1)
457         .runWhileDrawing(root);
458 
459 }
460 
461 @("Positional focus skips over disabled nodes")
462 unittest {
463 
464     Button btn1, btn2, btn3;
465 
466     auto root = focusChain(
467         vspace(
468             btn1 = button("One", delegate { }),
469             btn2 = button(.disabled, "Two", delegate { }),
470             btn3 = button("Three", delegate { }),
471         ),
472     );
473 
474     root.currentFocus = btn1;
475     root.draw();
476     root.focusBelow()
477         .thenAssertEquals(btn3)
478         .then(() => root.focusBelow)
479         .thenAssertEquals(null)
480         .then(() => root.focusAbove)
481         .thenAssertEquals(btn1)
482         .then(() => root.focusAbove)
483         .thenAssertEquals(null)
484         .runWhileDrawing(root);
485 
486 }
487 
488 @("Text can be typed into FocusChain")
489 unittest {
490 
491     char[8] buffer;
492 
493     auto focus = focusChain();
494     focus.typeText("Text");
495 
496     // Multiple reads are possible
497     foreach (i; 0..4) {
498         int offset;
499         auto text = focus.readText(buffer, offset);
500         assert(text == "Text");
501         assert(offset == 4);
502         assert(buffer[0..4] == "Text");
503         assert(focus.readText(buffer, offset) is null);
504     }
505 
506 }
507 
508 @("FocusChain supports writing text longer than the buffer")
509 unittest {
510 
511     char[8] buffer;
512 
513     auto focus = focusChain();
514     focus.typeText("Hello, World!");
515 
516     foreach (i; 0..4) {
517         int offset;
518         assert(focus.readText(buffer, offset) == "Hello, W");
519         assert(offset == 8);
520         assert(focus.readText(buffer, offset) == "orld!");
521         assert(offset == 13);
522     }
523 
524 }
525 
526 @("FocusChain: text input is readable from input actions")
527 unittest {
528 
529     auto input = textInput();
530     auto focus = focusChain(input);
531     auto root = chain(
532         inputMapChain(InputMapping.init),
533         focus
534     );
535 
536     focus.currentFocus = input;
537     focus.typeText("Hello");
538     root.draw();
539 
540     assert(input.value == "Hello");
541 
542 }