1 ///
2 module fluid.popup_button;
3 
4 import fluid.node;
5 import fluid.utils;
6 import fluid.label;
7 import fluid.style;
8 import fluid.button;
9 import fluid.popup_frame;
10 
11 @safe:
12 
13 /// A button made to open popups.
14 alias popupButton = simpleConstructor!PopupButton;
15 
16 // For no known reason, this will not compile (producing the most misleading error of the century) if extending directly
17 // from Button.
18 
19 /// ditto
20 class PopupButton : ButtonImpl!Label {
21 
22     mixin enableInputActions;
23 
24     public {
25 
26         /// Popup enabled by this button.
27         PopupFrame popup;
28 
29         /// Popup this button belongs to, if any. Set automatically if the popup is spawned with `spawnPopup`.
30         PopupFrame parentPopup;
31 
32     }
33 
34     /// Create a new button.
35     /// Params:
36     ///     text          = Text for the button.
37     ///     popupChildren = Children to appear within the button.
38     this(string text, Node[] popupChildren...) {
39 
40         // Craft the popup
41         popup = popupFrame(popupChildren);
42 
43         super(text, delegate {
44 
45             // Parent popup active
46             if (parentPopup && parentPopup.isFocused)
47                 parentPopup.spawnChildPopup(popup);
48 
49             // No parent
50             else {
51                 popup.theme = theme;
52                 tree.spawnPopup(popup);
53             }
54 
55         });
56 
57     }
58 
59     override string toString() const {
60 
61         import std.format;
62         return format!"popupButton(%s)"(text);
63 
64     }
65 
66 }
67 
68 ///
69 unittest {
70 
71     auto myButton = popupButton("Options",
72         button("Edit", delegate { }),
73         button("Copy", delegate { }),
74         popupButton("Share",
75             button("SMS", delegate { }),
76             button("Via e-mail", delegate { }),
77             button("Send to device", delegate { }),
78         ),
79     );
80 
81 }
82 
83 unittest {
84 
85     import fluid.backend;
86 
87     string lastAction;
88 
89     void action(string text)() {
90         lastAction = text;
91     }
92 
93     Button[6] buttons;
94 
95     auto io = new HeadlessBackend;
96     auto root = popupButton("Options",
97         buttons[0] = button("Edit", &action!"edit"),
98         buttons[1] = button("Copy", &action!"copy"),
99         buttons[2] = popupButton("Share",
100             buttons[3] = button("SMS", &action!"sms"),
101             buttons[4] = button("Via e-mail", &action!"email"),
102             buttons[5] = button("Send to device", &action!"device"),
103         ),
104     );
105 
106     auto sharePopupButton = cast(PopupButton) buttons[2];
107     auto sharePopup = sharePopupButton.popup;
108 
109     root.io = io;
110     root.draw();
111 
112     import std.stdio;
113 
114     // Focus the button
115     {
116         io.nextFrame;
117         io.press(KeyboardKey.down);
118 
119         root.draw();
120 
121         assert(root.isFocused);
122     }
123 
124     // Press it
125     {
126         io.nextFrame;
127         io.release(KeyboardKey.down);
128         io.press(KeyboardKey.enter);
129 
130         root.draw();
131     }
132 
133     // Popup opens
134     {
135         io.nextFrame;
136         io.release(KeyboardKey.enter);
137 
138         root.draw();
139 
140         assert(buttons[0].isFocused, "The first button inside should be focused");
141     }
142 
143     // Go to the previous button, expecting wrap
144     {
145         io.nextFrame;
146         io.press(KeyboardKey.leftShift);
147         io.press(KeyboardKey.tab);
148 
149         root.draw();
150 
151         assert(buttons[2].isFocused, "The last button inside the first menu should be focused");
152     }
153 
154     // Press it
155     {
156         io.nextFrame;
157         io.release(KeyboardKey.leftShift);
158         io.release(KeyboardKey.tab);
159         io.press(KeyboardKey.enter);
160 
161         root.draw();
162     }
163 
164     // Wait for the popup to appear
165     {
166         io.nextFrame;
167         io.release(KeyboardKey.enter);
168 
169         root.draw();
170         assert(buttons[3].isFocused, "The first button of the second menu should be focused");
171     }
172 
173     // Press the up arrow, it should do nothing
174     {
175         io.nextFrame;
176         io.press(KeyboardKey.up);
177 
178         root.draw();
179         assert(buttons[3].isFocused);
180     }
181 
182     // Press the down arrow
183     {
184         io.nextFrame;
185         io.release(KeyboardKey.up);
186         io.press(KeyboardKey.down);
187 
188         root.draw();
189         assert(buttons[4].isFocused);
190     }
191 
192     // Press the button
193     {
194         io.nextFrame;
195         io.release(KeyboardKey.down);
196         io.press(KeyboardKey.enter);
197 
198         root.draw();
199         assert(buttons[4].isFocused);
200         assert(lastAction == "email");
201         assert(!sharePopup.isHidden);
202 
203     }
204 
205     // Close the popup by pressing escape
206     {
207         io.nextFrame;
208         io.release(KeyboardKey.enter);
209         io.press(KeyboardKey.escape);
210 
211         root.draw();
212 
213         // Need another frame for the tree action
214         io.nextFrame;
215         root.draw();
216 
217         assert(sharePopup.isHidden);
218     }
219 
220 }